Compare commits

...

10 Commits

Author SHA1 Message Date
xingyue 0207f93303 fix: npm test → bun test per review 2026-05-13 17:42:11 +08:00
xingyue e1423f196b refactor: delegate publish to publish-all.sh, remove duplicated discovery+topo logic
- Remove inline auto-discover + Kahn's topo sort from publish.sh (was duplicating publish-all.sh)
- Remove inline publish loop + smoke test (publish-all.sh handles both)
- publish.sh now: bump version → replace workspace:* → build → test → call publish-all.sh → restore → commit
- Net: -97 lines, single source of truth for package discovery and publish order
2026-05-13 17:32:08 +08:00
xingyue ae6954a02f fix(publish): auto-discover packages + pre-publish test gate
What: Replace hardcoded PUBLISH_ORDER with auto-discovery of all
non-private packages, sorted by topological dependency order (Kahn's).
Add a test gate (npm test) after build, before publish.

Why: The manual list was missing workflow-gateway and workflow-agent-react,
causing them to never get published. Any future package additions would
have the same problem.

Changes:
- scripts/publish.sh: Replace static PUBLISH_ORDER array with node script
  that reads all packages/*/package.json, filters out private, and
  topologically sorts by @uncaged/* internal dependencies
- scripts/publish.sh: Add npm test step between build and publish,
  aborting on failure
2026-05-13 17:22:50 +08:00
xingyue aede8f7613 chore: publish v0.3.21
小橘 <xiaoju@shazhou.work>
2026-05-13 17:10:39 +08:00
xiaomo 6d1e0498ba Merge pull request 'refactor(dashboard): replace ELK with custom spine layout' (#235) from refactor/dashboard-custom-spine-layout into main 2026-05-13 09:03:34 +00:00
xingyue 6cce5e2593 chore: publish v0.3.20
小橘 <xiaoju@shazhou.work>
2026-05-13 17:00:43 +08:00
xingyue d3a7ed9062 chore: publish v0.3.19
小橘 <xiaoju@shazhou.work>
2026-05-13 16:56:55 +08:00
xingyue e7f733c393 refactor(dashboard): replace ELK with custom spine layout
What: Replace ELK layout engine with a hand-written spine layout that
topologically sorts nodes into a vertical main path with feedback edges
routed to the right side.

Why: ELK's layered algorithm spreads the graph too wide when handling
feedback (back) edges, causing fitView to shrink nodes until text is
unreadable. Our workflow graphs are predominantly linear pipelines with
feedback loops — a custom layout handles this topology much better.

Changes:
- packages/workflow-dashboard/src/components/workflow-graph/use-layout.ts:
  rewrite from async ELK to synchronous spine layout — topo-sort extracts
  main path, nodes stack vertically, feedback edges get right-side routing
- packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx:
  add custom SVG path for feedback edges (right-side arc with Q curves),
  use typed isFeedback/isSelfLoop fields from ConditionEdgeData
- packages/workflow-dashboard/src/components/workflow-graph/types.ts:
  rename elkLabelX/Y to labelX/Y, add isFeedback and isSelfLoop fields
- packages/workflow-dashboard/src/components/workflow-graph/workflow-graph.tsx:
  remove ReactFlowProvider/useReactFlow/useEffect fitView workaround
  (no longer needed — layout is synchronous), simplify component
- packages/workflow-dashboard/package.json: remove elkjs and dagre deps
2026-05-13 16:54:04 +08:00
xingyue d4bb4a9324 Merge pull request 'fix(cli): point bin to dist/cli.js instead of src/cli.ts' (#234) from fix/cli-bin-path into main 2026-05-13 08:43:41 +00:00
xiaomo 39540d9ae8 Merge pull request 'fix(dashboard): address ELK layout review feedback' (#233) from fix/dashboard-elk-review-feedback into main 2026-05-13 08:40:32 +00:00
22 changed files with 251 additions and 244 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/cli-workflow",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"src",
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-cursor",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-hermes",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-llm",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-react",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-cas",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
-2
View File
@@ -13,9 +13,7 @@
"preview": "vite preview"
},
"dependencies": {
"@dagrejs/dagre": "^3.0.0",
"@xyflow/react": "^12.10.2",
"elkjs": "^0.11.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-markdown": "^10.1.0",
@@ -6,6 +6,42 @@ import {
} from "@xyflow/react";
import type { ConditionEdgeData } from "./types.ts";
// Must match the FEEDBACK_OFFSET_X in use-layout.ts
const FEEDBACK_OFFSET_X = 100;
// Radius for feedback edge corners
const FEEDBACK_RADIUS = 16;
/**
* Build an SVG path for a feedback (back) edge that routes to the right of the nodes.
* The path goes: source right → arc → vertical up → arc → target right
*/
function feedbackPath(
sourceX: number,
sourceY: number,
targetX: number,
targetY: number,
): string {
const rightX = Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X;
const r = FEEDBACK_RADIUS;
// Start from source right side, go right, then up, then left to target right side
const segments = [
`M ${sourceX} ${sourceY}`,
// Horizontal to the right
`L ${rightX - r} ${sourceY}`,
// Arc turning upward
`Q ${rightX} ${sourceY} ${rightX} ${sourceY - r}`,
// Vertical upward
`L ${rightX} ${targetY + r}`,
// Arc turning left
`Q ${rightX} ${targetY} ${rightX - r} ${targetY}`,
// Horizontal left to target
`L ${targetX} ${targetY}`,
];
return segments.join(" ");
}
export function ConditionEdge(props: EdgeProps) {
const {
id,
@@ -23,25 +59,41 @@ export function ConditionEdge(props: EdgeProps) {
const edgeData = data as ConditionEdgeData | undefined;
const isFallback = edgeData?.isFallback ?? false;
const isSelfLoop = source === target;
const isFeedback = edgeData?.isFeedback ?? false;
const [path, defaultLabelX, defaultLabelY] = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
borderRadius: isSelfLoop ? 20 : 8,
offset: isSelfLoop ? 50 : undefined,
});
let path: string;
let defaultLabelX: number;
let defaultLabelY: number;
if (isFeedback) {
// Custom feedback path routed to the right
path = feedbackPath(sourceX, sourceY, targetX, targetY);
const rightX = Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X;
defaultLabelX = rightX;
defaultLabelY = (sourceY + targetY) / 2;
} else {
const result = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
borderRadius: isSelfLoop ? 20 : 8,
offset: isSelfLoop ? 50 : undefined,
});
path = result[0];
defaultLabelX = result[1];
defaultLabelY = result[2];
}
const stroke = isFallback ? "var(--color-text-muted)" : "var(--color-accent)";
const strokeDasharray = isFallback ? "5 4" : undefined;
const label = edgeData?.condition ?? "";
// Use ELK-computed label position if available, otherwise fall back to ReactFlow default
const labelX = edgeData?.elkLabelX ?? defaultLabelX;
const labelY = edgeData?.elkLabelY ?? defaultLabelY;
// Use pre-computed label position if available, otherwise fall back to default
const labelX = edgeData?.labelX ?? defaultLabelX;
const labelY = edgeData?.labelY ?? defaultLabelY;
return (
<>
@@ -21,8 +21,10 @@ export type ConditionEdgeData = {
condition: string;
conditionDescription: string | null;
isFallback: boolean;
elkLabelX: number | null;
elkLabelY: number | null;
isFeedback: boolean;
isSelfLoop: boolean;
labelX: number | null;
labelY: number | null;
[key: string]: unknown;
};
@@ -1,6 +1,5 @@
import type { Edge, Node } from "@xyflow/react";
import ELK, { type ElkExtendedEdge, type ElkNode } from "elkjs/lib/elk.bundled.js";
import { useEffect, useState } from "react";
import { useMemo } from "react";
import type { WorkflowGraphEdge } from "../../api.ts";
import type { ConditionEdgeData, NodeState, RoleNodeData, TerminalNodeData } from "./types.ts";
@@ -10,6 +9,11 @@ const ROLE_NODE_WIDTH = 180;
const ROLE_NODE_HEIGHT = 60;
const TERMINAL_NODE_SIZE = 40;
// Vertical gap between nodes in the spine
const LAYER_GAP = 80;
// Horizontal offset for feedback (back) edges routed on the right side
const FEEDBACK_OFFSET_X = 100;
type LayoutInput = {
edges: readonly WorkflowGraphEdge[];
roles: Record<string, { description: string }>;
@@ -21,15 +25,6 @@ type LayoutResult = {
edges: Edge[];
};
function collectNodeIds(edges: readonly WorkflowGraphEdge[]): Set<string> {
const ids = new Set<string>();
for (const e of edges) {
ids.add(e.from);
ids.add(e.to);
}
return ids;
}
function nodeSize(id: string): { width: number; height: number } {
if (id === START_ID || id === END_ID) {
return { width: TERMINAL_NODE_SIZE, height: TERMINAL_NODE_SIZE };
@@ -41,6 +36,75 @@ function edgeKey(e: WorkflowGraphEdge): string {
return `${e.from}->${e.to}::${e.condition}`;
}
/**
* Extract the linear spine from the graph using topological ordering.
* Forward edges go from lower rank to higher rank; feedback edges go backwards.
* Self-loops are neither forward nor feedback — they're handled separately.
*/
function extractSpine(edges: readonly WorkflowGraphEdge[]): string[] {
// Collect all node IDs
const ids = new Set<string>();
for (const e of edges) {
ids.add(e.from);
ids.add(e.to);
}
// Build adjacency for forward edges only (non-self-loop, non-FALLBACK-back)
// Strategy: BFS from __start__, picking the first non-FALLBACK forward edge,
// or FALLBACK if no other option.
const forwardAdj = new Map<string, string[]>();
for (const e of edges) {
if (e.from === e.to) continue;
const existing = forwardAdj.get(e.from) ?? [];
existing.push(e.to);
forwardAdj.set(e.from, existing);
}
// Walk the main path: prefer non-FALLBACK edges for the spine ordering
const visited = new Set<string>();
const spine: string[] = [];
// Build a set of "primary" next targets per node (non-FALLBACK first)
const primaryNext = new Map<string, string>();
const edgesByFrom = new Map<string, WorkflowGraphEdge[]>();
for (const e of edges) {
if (e.from === e.to) continue;
const list = edgesByFrom.get(e.from) ?? [];
list.push(e);
edgesByFrom.set(e.from, list);
}
// For each node, the "primary" next is the first non-FALLBACK target,
// or the FALLBACK target if all edges are FALLBACK
for (const [from, edgeList] of edgesByFrom) {
const nonFallback = edgeList.find((e) => e.condition !== "FALLBACK");
const fallback = edgeList.find((e) => e.condition === "FALLBACK");
primaryNext.set(from, nonFallback?.to ?? fallback?.to ?? "");
}
// Walk the spine from __start__
let current: string | null = START_ID;
while (current !== null && !visited.has(current)) {
visited.add(current);
spine.push(current);
const next = primaryNext.get(current);
if (next !== undefined && next !== "" && !visited.has(next)) {
current = next;
} else {
current = null;
}
}
// Add any remaining nodes not on the main path (shouldn't normally happen)
for (const id of ids) {
if (!visited.has(id)) {
spine.push(id);
}
}
return spine;
}
function buildRoleNode(
id: string,
pos: { x: number; y: number },
@@ -72,143 +136,95 @@ function buildTerminalNode(
};
}
function buildEdge(e: WorkflowGraphEdge, elkEdgeMap: Map<string, ElkExtendedEdge>): Edge<ConditionEdgeData> {
const isFallback = e.condition === "FALLBACK";
const key = edgeKey(e);
const elkEdge = elkEdgeMap.get(key);
// Extract ELK's computed label position
let labelX: number | null = null;
let labelY: number | null = null;
if (elkEdge?.labels && elkEdge.labels.length > 0) {
const label = elkEdge.labels[0];
if (label.x !== undefined && label.y !== undefined) {
labelX = label.x + (label.width ?? 0) / 2;
labelY = label.y + (label.height ?? 0) / 2;
}
function computeLayout(input: LayoutInput): LayoutResult {
const spine = extractSpine(input.edges);
const rank = new Map<string, number>();
for (let i = 0; i < spine.length; i++) {
rank.set(spine[i], i);
}
return {
id: key,
source: e.from,
target: e.to,
type: "condition",
data: {
condition: e.condition,
conditionDescription: e.conditionDescription,
isFallback,
elkLabelX: labelX,
elkLabelY: labelY,
},
};
}
// Position nodes along a vertical spine, centered horizontally
const centerX = ROLE_NODE_WIDTH / 2; // left edge at x=0, center at width/2
const nodePositions = new Map<string, { x: number; y: number; w: number; h: number }>();
const elk = new ELK();
async function computeLayout(input: LayoutInput): Promise<LayoutResult> {
const ids = collectNodeIds(input.edges);
const elkNodes: ElkNode[] = [];
for (const id of ids) {
let y = 0;
for (const id of spine) {
const size = nodeSize(id);
elkNodes.push({ id, width: size.width, height: size.height });
}
const elkEdges: ElkExtendedEdge[] = input.edges
.filter((e) => e.from !== e.to)
.map((e) => ({
id: edgeKey(e),
sources: [e.from],
targets: [e.to],
labels: e.condition !== ""
? [{ text: e.condition, width: Math.max(e.condition.length * 7 + 16, 60), height: 22 }]
: [],
}));
const graph: ElkNode = {
id: "root",
layoutOptions: {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
// Node spacing
"elk.spacing.nodeNode": "30",
"elk.layered.spacing.nodeNodeBetweenLayers": "50",
// Edge spacing — keep edges apart from each other and from nodes
"elk.spacing.edgeNode": "25",
"elk.spacing.edgeEdge": "15",
"elk.layered.spacing.edgeNodeBetweenLayers": "25",
"elk.layered.spacing.edgeEdgeBetweenLayers": "15",
// Edge routing
"elk.edgeRouting": "ORTHOGONAL",
"elk.layered.mergeEdges": "false",
// Node placement
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
// Edge label placement
"elk.edgeLabels.placement": "CENTER",
// Crossing minimization
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
// Compaction
"elk.layered.compaction.postCompaction.strategy": "EDGE_LENGTH",
// Cycle breaking — keep main flow top-to-bottom
"elk.layered.cycleBreaking.strategy": "DEPTH_FIRST",
},
children: elkNodes,
edges: elkEdges,
};
const laid = await elk.layout(graph);
// Build map of ELK edge results for label positions
const elkEdgeMap = new Map<string, ElkExtendedEdge>();
for (const e of laid.edges ?? []) {
elkEdgeMap.set(e.id, e);
// Center-align all nodes on the spine
const x = centerX - size.width / 2;
nodePositions.set(id, { x, y, w: size.width, h: size.height });
y += size.height + LAYER_GAP;
}
// Build nodes
const nodes: Node[] = [];
for (const child of laid.children ?? []) {
const pos = { x: child.x ?? 0, y: child.y ?? 0 };
const state = input.nodeStates.get(child.id) ?? "default";
if (child.id === START_ID || child.id === END_ID) {
nodes.push(buildTerminalNode(child.id, pos, state));
for (const id of spine) {
const pos = nodePositions.get(id);
if (pos === undefined) continue;
const state = input.nodeStates.get(id) ?? "default";
if (id === START_ID || id === END_ID) {
nodes.push(buildTerminalNode(id, { x: pos.x, y: pos.y }, state));
} else {
nodes.push(buildRoleNode(child.id, pos, input.roles, state));
nodes.push(buildRoleNode(id, { x: pos.x, y: pos.y }, input.roles, state));
}
}
const edges: Edge[] = input.edges.map((e) => buildEdge(e, elkEdgeMap));
// Build edges with label positions
// For feedback edges (target rank < source rank), we'll compute label at midpoint
// of the right-side arc. The actual SVG path is drawn by ConditionEdge component.
const edges: Edge[] = input.edges.map((e) => {
const isFallback = e.condition === "FALLBACK";
const isSelfLoop = e.from === e.to;
const sourceRank = rank.get(e.from) ?? 0;
const targetRank = rank.get(e.to) ?? 0;
const isFeedback = !isSelfLoop && targetRank <= sourceRank;
const sourcePos = nodePositions.get(e.from);
const targetPos = nodePositions.get(e.to);
let labelX: number | null = null;
let labelY: number | null = null;
if (sourcePos !== undefined && targetPos !== undefined) {
if (isFeedback) {
// Label on the right side of the feedback arc
const rightX = centerX + ROLE_NODE_WIDTH / 2 + FEEDBACK_OFFSET_X;
const midY = (sourcePos.y + sourcePos.h / 2 + targetPos.y + targetPos.h / 2) / 2;
labelX = rightX;
labelY = midY;
} else if (!isSelfLoop) {
// Forward edge: label between source bottom and target top
const midX = centerX;
const midY = (sourcePos.y + sourcePos.h + targetPos.y) / 2;
labelX = midX;
labelY = midY;
}
// Self-loop: let ReactFlow default handle it
}
return {
id: edgeKey(e),
source: e.from,
target: e.to,
type: "condition",
data: {
condition: e.condition,
conditionDescription: e.conditionDescription,
isFallback,
isFeedback,
isSelfLoop,
labelX,
labelY,
},
};
});
return { nodes, edges };
}
const EMPTY_LAYOUT: LayoutResult = { nodes: [], edges: [] };
export function useLayout(input: LayoutInput): LayoutResult {
const [layout, setLayout] = useState<LayoutResult>(EMPTY_LAYOUT);
const edgeJson = JSON.stringify(input.edges);
const roleJson = JSON.stringify(input.roles);
useEffect(() => {
let cancelled = false;
const parsed = {
edges: JSON.parse(edgeJson) as readonly WorkflowGraphEdge[],
roles: JSON.parse(roleJson) as Record<string, { description: string }>,
nodeStates: input.nodeStates,
};
computeLayout(parsed)
.then((result) => {
if (!cancelled) setLayout(result);
})
.catch((err: unknown) => {
if (!cancelled) {
// biome-ignore lint/suspicious/noConsole: layout error reporting
console.error("ELK layout failed:", err);
}
});
return () => {
cancelled = true;
};
}, [edgeJson, roleJson, input.nodeStates]);
return layout;
return useMemo(
() => computeLayout(input),
[input.edges, input.roles, input.nodeStates],
);
}
@@ -6,11 +6,9 @@ import {
type NodeTypes,
type OnNodeClick,
ReactFlow,
ReactFlowProvider,
useReactFlow,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import type { WorkflowGraph as WorkflowGraphData } from "../../api.ts";
import { ConditionEdge } from "./condition-edge.tsx";
import { RoleNode } from "./role-node.tsx";
@@ -39,30 +37,12 @@ function handleRoleNodeClick(onRoleClick: (roleName: string) => void, node: Node
onRoleClick(node.id);
}
function WorkflowGraphInner({ graph, roles, nodeStates, onNodeClick }: Props) {
export function WorkflowGraph({ graph, roles, nodeStates, onNodeClick }: Props) {
const layout = useLayout({ edges: graph.edges, roles, nodeStates });
const { fitView } = useReactFlow();
const onNodeClickHandler: OnNodeClick | undefined =
onNodeClick !== null ? (_e, node) => handleRoleNodeClick(onNodeClick, node) : undefined;
// Re-fit when layout changes (ELK is async)
// Use requestAnimationFrame + setTimeout to ensure ReactFlow has processed nodes
useEffect(() => {
if (layout.nodes.length > 0) {
let cancelled = false;
requestAnimationFrame(() => {
if (cancelled) return;
setTimeout(() => {
if (!cancelled) fitView({ padding: 0.1, duration: 300 });
}, 300);
});
return () => {
cancelled = true;
};
}
}, [layout.nodes, layout.edges, fitView]);
const styledEdges = useMemo(
() =>
layout.edges.map((e) => ({
@@ -77,25 +57,17 @@ function WorkflowGraphInner({ graph, roles, nodeStates, onNodeClick }: Props) {
[layout.edges],
);
// Generate a stable key that changes when layout changes, to force ReactFlow remount + fitView
const layoutKey = useMemo(
() => layout.nodes.map((n) => `${n.id}:${n.position.x}:${n.position.y}`).join(","),
[layout.nodes],
);
return (
<ReactFlow
key={layoutKey}
nodes={layout.nodes}
edges={styledEdges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodeClick={onNodeClickHandler}
fitView
fitViewOptions={{ padding: 0.1, minZoom: 0.1, maxZoom: 1.5 }}
minZoom={0.1}
maxZoom={1.5}
defaultViewport={{ x: 0, y: 0, zoom: 0.5 }}
fitViewOptions={{ padding: 0.15 }}
minZoom={0.3}
maxZoom={2}
nodesDraggable={false}
nodesConnectable={false}
elementsSelectable={false}
@@ -107,11 +79,3 @@ function WorkflowGraphInner({ graph, roles, nodeStates, onNodeClick }: Props) {
</ReactFlow>
);
}
export function WorkflowGraph(props: Props) {
return (
<ReactFlowProvider>
<WorkflowGraphInner {...props} />
</ReactFlowProvider>
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-execute",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-gateway",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-protocol",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-reactor",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-register",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-runtime",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-template-develop",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-template-solve-issue",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-util-agent",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-util",
"version": "0.3.18",
"version": "0.3.21",
"files": [
"dist",
"package.json"
+11 -36
View File
@@ -13,8 +13,7 @@ set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
GITEA_TOKEN="${GITEA_TOKEN:?GITEA_TOKEN is required}"
GITEA_NPM_REGISTRY="https://git.shazhou.work/api/packages/uncaged/npm/"
GITEA_TOKEN="${GITEA_TOKEN:?GITEA_TOKEN is required}" # needed by .npmrc
# ─── Version ─────────────────────────────────────────────────────────────────
current_version() {
@@ -36,24 +35,6 @@ CURRENT=$(current_version)
VERSION=$(bump_version "$CURRENT" "${1:?Usage: publish.sh <version|patch|minor|major>}")
echo "📦 Publish: $CURRENT$VERSION"
# ─── Topological publish order ───────────────────────────────────────────────
PUBLISH_ORDER=(
workflow-protocol
workflow-util
workflow-cas
workflow-runtime
workflow-reactor
workflow-register
workflow-execute
cli-workflow
workflow-util-agent
workflow-agent-cursor
workflow-agent-hermes
workflow-agent-llm
workflow-template-develop
workflow-template-solve-issue
)
# ─── Bump version ────────────────────────────────────────────────────────────
echo "🔢 Bumping versions..."
for dir in packages/*/; do
@@ -92,22 +73,16 @@ done
echo "🔨 Building..."
npm run build
# ─── Publish ─────────────────────────────────────────────────────────────────
echo "🚀 Publishing..."
cat > "$REPO_ROOT/.npmrc" <<EOF
@uncaged:registry=${GITEA_NPM_REGISTRY}
//${GITEA_NPM_REGISTRY#https://}:_authToken=${GITEA_TOKEN}
EOF
# ─── Self-test ────────────────────────────────────────────────────────────────
echo "🧪 Running tests..."
if ! bun test; then
echo "❌ Tests failed — aborting publish"
exit 1
fi
FAIL=0
for pkg_dir in "${PUBLISH_ORDER[@]}"; do
if (cd "packages/$pkg_dir" && npm publish 2>&1); then
echo " ✅ @uncaged/$pkg_dir@$VERSION"
else
echo " ❌ @uncaged/$pkg_dir"
FAIL=1
fi
done
# ─── Publish (delegate to publish-all.sh) ────────────────────────────────────
echo "🚀 Publishing via publish-all.sh..."
"$REPO_ROOT/scripts/publish-all.sh"
# ─── Restore workspace:* ─────────────────────────────────────────────────────
echo "🔄 Restoring workspace:*..."
@@ -136,4 +111,4 @@ git commit -m "chore: publish v${VERSION}
小橘 <xiaoju@shazhou.work>"
git push
[[ "$FAIL" -eq 0 ]] && echo "✅ v${VERSION} published" || echo "⚠️ v${VERSION} published with errors"
echo "✅ v${VERSION} published"