@tuongaz/seeflow 0.1.77 → 0.1.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/web/assets/{architectureDiagram-3BPJPVTR-D5iHwVvy.js → architectureDiagram-3BPJPVTR-id0XTZQC.js} +1 -1
- package/dist/web/assets/{blockDiagram-GPEHLZMM-MAYYm7FM.js → blockDiagram-GPEHLZMM-Cjvfg0ZP.js} +1 -1
- package/dist/web/assets/{c4Diagram-AAUBKEIU-7P7yfHg1.js → c4Diagram-AAUBKEIU-Dyq-0e8Q.js} +1 -1
- package/dist/web/assets/channel-Ajb6KiL3.js +1 -0
- package/dist/web/assets/{chart-C68vupBE.js → chart-DuTGW-Dj.js} +1 -1
- package/dist/web/assets/{chunk-2J33WTMH-Bb4cSusI.js → chunk-2J33WTMH-DsD65OzD.js} +1 -1
- package/dist/web/assets/{chunk-4BX2VUAB-DXYpcpTh.js → chunk-4BX2VUAB-BpytKE8P.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-BxuYKDnf.js → chunk-55IACEB6-DIILAUq9.js} +1 -1
- package/dist/web/assets/{chunk-727SXJPM-DbWlxAr2.js → chunk-727SXJPM-C4ih-gTo.js} +1 -1
- package/dist/web/assets/{chunk-AQP2D5EJ-DT8S1q80.js → chunk-AQP2D5EJ-BsYoWdVM.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-Dc0wDuZz.js → chunk-FMBD7UC4-Db6L0z4p.js} +1 -1
- package/dist/web/assets/{chunk-ND2GUHAM-CqLLK6H0.js → chunk-ND2GUHAM-BNLqZYMx.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-CxF7nkDI.js → chunk-QZHKN3VN-DL5PK45j.js} +1 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-Cgw6ezRo.js +1 -0
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-Cgw6ezRo.js +1 -0
- package/dist/web/assets/{code-block-DR9fiK_U.js → code-block-C1SJv-Al.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-BflFbtY2.js → cose-bilkent-S5V4N54A-ChX5nR0f.js} +1 -1
- package/dist/web/assets/{dagre-BM42HDAG-BJ5UdyYS.js → dagre-BM42HDAG-BXeL3fEN.js} +1 -1
- package/dist/web/assets/{diagram-2AECGRRQ-D0M8fCf7.js → diagram-2AECGRRQ-B6WtmEP-.js} +1 -1
- package/dist/web/assets/{diagram-5GNKFQAL-D67gAMS4.js → diagram-5GNKFQAL-SXs7ALwM.js} +1 -1
- package/dist/web/assets/{diagram-KO2AKTUF-XX62HBG-.js → diagram-KO2AKTUF-D5zylPYo.js} +1 -1
- package/dist/web/assets/{diagram-LMA3HP47-DCFq3Oac.js → diagram-LMA3HP47-CByIUlQF.js} +1 -1
- package/dist/web/assets/{diagram-OG6HWLK6-Be392NCN.js → diagram-OG6HWLK6-BH1MfUqV.js} +1 -1
- package/dist/web/assets/{erDiagram-TEJ5UH35-DP4eP0as.js → erDiagram-TEJ5UH35-BOOnRFBh.js} +1 -1
- package/dist/web/assets/{flowDiagram-I6XJVG4X-Ch1GVJ9R.js → flowDiagram-I6XJVG4X-BynWDHJP.js} +1 -1
- package/dist/web/assets/{ganttDiagram-6RSMTGT7-DtvkTizu.js → ganttDiagram-6RSMTGT7-Cgq_djyN.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PVQCEYII-YGcuBgb9.js → gitGraphDiagram-PVQCEYII-ciGSgmfT.js} +1 -1
- package/dist/web/assets/index-DiakpHyc.js +8619 -0
- package/dist/web/assets/{index-DljfurDC.css → index-fl8DS9WO.css} +1 -1
- package/dist/web/assets/{index.es-jrsJPbYZ.js → index.es-C7TtaIfa.js} +1 -1
- package/dist/web/assets/{infoDiagram-5YYISTIA-wce0BORz.js → infoDiagram-5YYISTIA-DqMb3_c-.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-u2MvPgdW.js → ishikawaDiagram-YF4QCWOH-CAO6KqQU.js} +1 -1
- package/dist/web/assets/{journeyDiagram-JHISSGLW-BsOyrTiA.js → journeyDiagram-JHISSGLW-Di8MsLTo.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-ptMERvnN.js → jspdf.es.min-Cq4dY-lT.js} +3 -3
- package/dist/web/assets/{kanban-definition-UN3LZRKU-BaraYV9q.js → kanban-definition-UN3LZRKU-ClOmVNcX.js} +1 -1
- package/dist/web/assets/{linear-BVqXcDUJ.js → linear-B3OKBKaT.js} +1 -1
- package/dist/web/assets/{markdown-DqP0Cywq.js → markdown-Dg8NEx1K.js} +1 -1
- package/dist/web/assets/{mermaid.core-CakR_vo1.js → mermaid.core-Bw-m7bH-.js} +4 -4
- package/dist/web/assets/{mindmap-definition-RKZ34NQL-CO5AsZw3.js → mindmap-definition-RKZ34NQL-CUBA1zfc.js} +1 -1
- package/dist/web/assets/{pieDiagram-4H26LBE5-CiDJY-kx.js → pieDiagram-4H26LBE5-Dux5HvSU.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-W4KKPZXB-BS6oN3s_.js → quadrantDiagram-W4KKPZXB-DU3gQGo3.js} +1 -1
- package/dist/web/assets/{requirementDiagram-4Y6WPE33-CNbUR_FF.js → requirementDiagram-4Y6WPE33-CD3A_U9j.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-5OEKKPKP-0Esj5uzm.js → sankeyDiagram-5OEKKPKP-Cd4mc26P.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-3UESZ5HK-DR3U38Zi.js → sequenceDiagram-3UESZ5HK-Da0iOMgq.js} +1 -1
- package/dist/web/assets/{stateDiagram-AJRCARHV-C50RQjWe.js → stateDiagram-AJRCARHV-P94LaOD2.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU--JLHF28o.js +1 -0
- package/dist/web/assets/{time-C_2J9tFX.js → time-0JEErjjJ.js} +1 -1
- package/dist/web/assets/{timeline-definition-PNZ67QCA-BQXyo2r_.js → timeline-definition-PNZ67QCA-BqAYomix.js} +1 -1
- package/dist/web/assets/{vennDiagram-CIIHVFJN-DZJ8M3EA.js → vennDiagram-CIIHVFJN-BWuPhfIM.js} +1 -1
- package/dist/web/assets/{wardley-L42UT6IY-B96HtW3i.js → wardley-L42UT6IY-iiGkgUQj.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BHkQ79WC.js → wardleyDiagram-YWT4CUSO-CtqzFQXL.js} +1 -1
- package/dist/web/assets/{xychartDiagram-2RQKCTM6-B_f8koGI.js → xychartDiagram-2RQKCTM6-BGrOXndI.js} +1 -1
- package/dist/web/index.html +2 -2
- package/examples/component-showcase/seeflow.json +6 -0
- package/examples/ecommerce-platform/seeflow.json +6 -0
- package/examples/order-pipeline/seeflow.json +6 -0
- package/package.json +1 -1
- package/src/api.ts +739 -94
- package/src/cli-e2e.ts +24 -13
- package/src/cli-helpers.ts +26 -0
- package/src/cli-manifest.ts +330 -87
- package/src/cli-ops.ts +56 -2
- package/src/cli.ts +228 -81
- package/src/cors.ts +93 -0
- package/src/jq-filter.ts +253 -0
- package/src/mcp-shim.ts +114 -7
- package/src/mcp-ui.ts +126 -0
- package/src/mcp.ts +258 -97
- package/src/node-files.ts +18 -7
- package/src/operations.ts +68 -32
- package/src/project-scanner.ts +105 -0
- package/src/registry.ts +79 -18
- package/src/route-resolve.ts +41 -0
- package/src/schema.ts +54 -0
- package/src/server.ts +24 -3
- package/src/slugify.ts +16 -0
- package/dist/web/assets/channel-BjsQQK93.js +0 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-p3FY5uNC.js +0 -1
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-p3FY5uNC.js +0 -1
- package/dist/web/assets/index-Bg3PU4Ev.js +0 -8614
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-BbNrmkIR.js +0 -1
- /package/examples/component-showcase/{flow.json → flows/main/flow.json} +0 -0
- /package/examples/component-showcase/{nodes → flows/main/nodes}/chart/spec.json +0 -0
- /package/examples/component-showcase/{nodes → flows/main/nodes}/counter/spec.json +0 -0
- /package/examples/component-showcase/{nodes → flows/main/nodes}/fetcher/actions/refresh.ts +0 -0
- /package/examples/component-showcase/{nodes → flows/main/nodes}/fetcher/spec.json +0 -0
- /package/examples/component-showcase/{nodes → flows/main/nodes}/form/spec.json +0 -0
- /package/examples/component-showcase/{style.json → flows/main/style.json} +0 -0
- /package/examples/ecommerce-platform/{flow.json → flows/main/flow.json} +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-3zFtHg6ENc/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-5F424NWbEu/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-CbwYqb7NfB/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-XwygzfKPZ5/view.html +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-fkptXw7uvs/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-kwBY8YPmYM/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-mPqan8rFYN/detail.md +0 -0
- /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-yKrg9DV5fJ/detail.md +0 -0
- /package/examples/ecommerce-platform/{scripts → flows/main/scripts}/play.ts +0 -0
- /package/examples/ecommerce-platform/{style.json → flows/main/style.json} +0 -0
- /package/examples/order-pipeline/{flow.json → flows/main/flow.json} +0 -0
- /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-GXTKUcE3ye/detail.md +0 -0
- /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-XKIyds0TDg/detail.md +0 -0
- /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-YOYiHJpY0i/detail.md +0 -0
- /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-zUIH7WFnhK/detail.md +0 -0
- /package/examples/order-pipeline/{scripts → flows/main/scripts}/play.ts +0 -0
- /package/examples/order-pipeline/{style.json → flows/main/style.json} +0 -0
package/src/cli-ops.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type Operations, createOperations } from './operations.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { type ScanError, scanProject } from './project-scanner.ts';
|
|
3
|
+
import {
|
|
4
|
+
type FlowEntry,
|
|
5
|
+
type Registry,
|
|
6
|
+
createRegistry,
|
|
7
|
+
manifestOnlyEntryFilter,
|
|
8
|
+
} from './registry.ts';
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
11
|
* Build a single Operations handle for in-process CLI use.
|
|
@@ -14,5 +20,53 @@ import { createRegistry } from './registry.ts';
|
|
|
14
20
|
* is undefined in the CLI.
|
|
15
21
|
*/
|
|
16
22
|
export function createCliOperations(): Operations {
|
|
17
|
-
return createOperations({
|
|
23
|
+
return createOperations({
|
|
24
|
+
registry: createRegistry({ isLoadableEntry: manifestOnlyEntryFilter }),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RegisterProjectOpts {
|
|
29
|
+
/** Absolute (or relative) path of the project root that contains a
|
|
30
|
+
* `seeflow.json` manifest plus one `flows/<id>/flow.json` per declared flow. */
|
|
31
|
+
repoPath: string;
|
|
32
|
+
/** Registry handle to upsert into. Defaults to a fresh `createRegistry()` —
|
|
33
|
+
* long-lived consumers (the server's seed path, the in-process CLI ops)
|
|
34
|
+
* should pass their own to share state across calls. */
|
|
35
|
+
registry?: Registry;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type RegisterProjectOutcome =
|
|
39
|
+
| { kind: 'ok'; projectSlug: string; entries: FlowEntry[] }
|
|
40
|
+
| ScanError;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Scan a project root and register every flow declared in its `seeflow.json`.
|
|
44
|
+
* Produces one `FlowEntry` per `ScannedFlow` — `projectSlug` is shared across
|
|
45
|
+
* the resulting entries, `flowSlug` mirrors the manifest entry id, and the
|
|
46
|
+
* entry whose id matches `manifest.defaultFlow` is marked `isDefault: true`.
|
|
47
|
+
*
|
|
48
|
+
* The legacy single-flow `ops.registerFlow` path in operations.ts still backs
|
|
49
|
+
* the `/api/flows/register` HTTP endpoint until US-007 rewrites the route
|
|
50
|
+
* tree. `registerProject` is the manifest-driven replacement the CLI uses
|
|
51
|
+
* from US-004 onward.
|
|
52
|
+
*/
|
|
53
|
+
export function registerProject(opts: RegisterProjectOpts): RegisterProjectOutcome {
|
|
54
|
+
const registry = opts.registry ?? createRegistry({ isLoadableEntry: manifestOnlyEntryFilter });
|
|
55
|
+
const scan = scanProject(opts.repoPath);
|
|
56
|
+
if (scan.kind !== 'ok') return scan;
|
|
57
|
+
|
|
58
|
+
const entries: FlowEntry[] = scan.flows.map((flow) =>
|
|
59
|
+
registry.upsert({
|
|
60
|
+
name: flow.name,
|
|
61
|
+
description: scan.manifest.description,
|
|
62
|
+
repoPath: opts.repoPath,
|
|
63
|
+
flowPath: flow.flowPath,
|
|
64
|
+
projectSlug: scan.projectSlug,
|
|
65
|
+
flowSlug: flow.id,
|
|
66
|
+
isDefault: flow.isDefault,
|
|
67
|
+
icon: flow.icon,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return { kind: 'ok', projectSlug: scan.projectSlug, entries };
|
|
18
72
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { closeSync, cpSync, existsSync, mkdirSync, openSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
drainStdin,
|
|
6
|
+
loadBody,
|
|
7
|
+
parseProjectFlow,
|
|
8
|
+
printError,
|
|
9
|
+
printOk,
|
|
10
|
+
printOutcome,
|
|
11
|
+
} from './cli-helpers.ts';
|
|
5
12
|
import { COMMAND_MANIFEST, renderCommandHelp, renderCommandList } from './cli-manifest.ts';
|
|
6
|
-
import { createCliOperations } from './cli-ops.ts';
|
|
13
|
+
import { createCliOperations, registerProject } from './cli-ops.ts';
|
|
7
14
|
import { createEventBus } from './events.ts';
|
|
15
|
+
import { JqError, applyJq } from './jq-filter.ts';
|
|
8
16
|
import type { LayoutOptions } from './layout.ts';
|
|
9
17
|
import {
|
|
10
18
|
ConnectorPatchBodySchema,
|
|
@@ -14,7 +22,7 @@ import {
|
|
|
14
22
|
} from './operations.ts';
|
|
15
23
|
import { PROJECT_FLOW_FILENAME, seeflowHome } from './paths.ts';
|
|
16
24
|
import { defaultProcessSpawner } from './process-spawner.ts';
|
|
17
|
-
import { type Registry, createRegistry } from './registry.ts';
|
|
25
|
+
import { type Registry, createRegistry, manifestOnlyEntryFilter } from './registry.ts';
|
|
18
26
|
import {
|
|
19
27
|
DEFAULT_CONFIG,
|
|
20
28
|
clearPid,
|
|
@@ -60,6 +68,53 @@ const requireArg = (idx: number, name: string): string => {
|
|
|
60
68
|
return v as string;
|
|
61
69
|
};
|
|
62
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Walk argv (after the subcommand at argv[0]) and return non-flag positionals
|
|
73
|
+
* in order. Skips `--name value` and `--name=value` pairs so callers can mix
|
|
74
|
+
* positional arguments freely with --project/--flow (US-020). Boolean-style
|
|
75
|
+
* `--name` flags (e.g. `--no-start`, `--stdin`) are detected because the next
|
|
76
|
+
* argv entry either starts with `--` or is out of bounds.
|
|
77
|
+
*/
|
|
78
|
+
const positionalArgs = (): string[] => {
|
|
79
|
+
const out: string[] = [];
|
|
80
|
+
for (let i = 1; i < argv.length; i++) {
|
|
81
|
+
const arg = argv[i] as string;
|
|
82
|
+
if (arg.startsWith('--')) {
|
|
83
|
+
// `--name=value` is self-contained; `--name value` consumes the next slot
|
|
84
|
+
// only when that slot is not itself another flag (boolean flag otherwise).
|
|
85
|
+
if (!arg.includes('=')) {
|
|
86
|
+
const next = argv[i + 1];
|
|
87
|
+
if (next !== undefined && !next.startsWith('--')) i++;
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
out.push(arg);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const requirePositional = (idx: number, name: string): string => {
|
|
97
|
+
const v = positionalArgs()[idx];
|
|
98
|
+
if (!v) printError(`Missing required positional argument: ${name}`);
|
|
99
|
+
return v as string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the (project, flow) pair every flow-scoped CLI verb expects (US-020).
|
|
104
|
+
* Surfaces clear `Missing required flag: --project|--flow` errors through
|
|
105
|
+
* `printError` instead of throwing — matches the rest of the CLI's exit
|
|
106
|
+
* behaviour. The returned `slug` (`${project}/${flow}`) is what the in-process
|
|
107
|
+
* `ops.*` calls accept via `registry.resolve(idOrSlug)`.
|
|
108
|
+
*/
|
|
109
|
+
const requireProjectFlow = (): { project: string; flow: string; slug: string } => {
|
|
110
|
+
try {
|
|
111
|
+
const { project, flow } = parseProjectFlow(argv);
|
|
112
|
+
return { project, flow, slug: `${project}/${flow}` };
|
|
113
|
+
} catch (err) {
|
|
114
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
63
118
|
async function studioUrlOrDie(noStart: boolean): Promise<{ url: string; port: number }> {
|
|
64
119
|
const config = readConfig();
|
|
65
120
|
const overrideUrl = process.env.SEEFLOW_STUDIO_URL?.replace(/\/+$/, '');
|
|
@@ -131,6 +186,10 @@ if (argv.includes('--version') || argv.includes('-v')) {
|
|
|
131
186
|
await runFlowsGet();
|
|
132
187
|
} else if (sub === 'flows:graph') {
|
|
133
188
|
await runFlowsGraph();
|
|
189
|
+
} else if (sub === 'flows:create') {
|
|
190
|
+
await runFlowsCreate();
|
|
191
|
+
} else if (sub === 'flows:rename') {
|
|
192
|
+
await runFlowsRename();
|
|
134
193
|
} else if (sub === 'flows:delete') {
|
|
135
194
|
await runFlowsDelete();
|
|
136
195
|
} else if (sub === 'flows:layout') {
|
|
@@ -186,23 +245,25 @@ Commands (work without a running studio):
|
|
|
186
245
|
stop Stop a background studio instance
|
|
187
246
|
register Register a demo repo, writing to ~/.seeflow/registry.json (alias of flows:register)
|
|
188
247
|
flows:register Register a demo repo
|
|
189
|
-
projects:create
|
|
248
|
+
projects:create Scaffold a new project (writes <path>/seeflow.json + <path>/flows/main/flow.json) — (--path <dir> --name <name> [--description <text>])
|
|
190
249
|
flows:list List registered flows
|
|
191
250
|
flows:summary List registered flows (id + name + description only)
|
|
192
|
-
flows:get <
|
|
193
|
-
flows:graph
|
|
194
|
-
flows:
|
|
195
|
-
flows:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
nodes:
|
|
200
|
-
nodes:
|
|
201
|
-
nodes:
|
|
202
|
-
nodes:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
connectors:
|
|
251
|
+
flows:get Get flow details (--project <p> --flow <f>)
|
|
252
|
+
flows:graph List nodes + connectors without inlined file content (--project <p> --flow <f>)
|
|
253
|
+
flows:create Create a new flow within a project (--project <p> --flow <id> --name <n> [--icon <i>])
|
|
254
|
+
flows:rename Rename a flow id/name/icon (--project <p> --flow <id> [--new-id <x>] [--name <n>] [--icon <i>])
|
|
255
|
+
flows:delete Delete a flow (--project <p> --flow <f> [--new-default <other>])
|
|
256
|
+
flows:layout Apply ELK layout, writing style.json (--project <p> --flow <f>) [--json/--file/--stdin]
|
|
257
|
+
flow:add-bulk Add many nodes + connectors atomically (--project <p> --flow <f>) [--json/--file/--stdin; body { nodes?, connectors? }]
|
|
258
|
+
nodes:add Add a node (--project <p> --flow <f>) [--json/--file/--stdin]
|
|
259
|
+
nodes:get <n> Get a node with detail / html content inlined (--project <p> --flow <f>)
|
|
260
|
+
nodes:patch <n> Patch a node (--project <p> --flow <f>) [--json/--file/--stdin]
|
|
261
|
+
nodes:move <n> Move a node (--project <p> --flow <f> --x N --y N)
|
|
262
|
+
nodes:reorder <n> Reorder a node (--project <p> --flow <f> --op forward|backward|toFront|toBack|toIndex [--index N])
|
|
263
|
+
nodes:delete <n> Delete a node (--project <p> --flow <f>)
|
|
264
|
+
connectors:add Add a connector (--project <p> --flow <f>) [--json/--file/--stdin]
|
|
265
|
+
connectors:patch <connId> Patch a connector (--project <p> --flow <f>) [--json/--file/--stdin]
|
|
266
|
+
connectors:delete <connId> Delete a connector (--project <p> --flow <f>)
|
|
206
267
|
validate Schema-validate a flow.json (--file <file> [--style <file>])
|
|
207
268
|
schema [<category>] Get the flow.json schema. No arg → category index;
|
|
208
269
|
category arg → full JSON Schema(s) for that category
|
|
@@ -213,10 +274,10 @@ Commands (work without a running studio):
|
|
|
213
274
|
'ids connector 12').
|
|
214
275
|
|
|
215
276
|
Commands (require a running studio):
|
|
216
|
-
flows:play <
|
|
277
|
+
flows:play <n> Trigger a play on node <n> (--project <p> --flow <f>)
|
|
217
278
|
emit <id> <n> <st> Broadcast a status event for node <n> (st: running|done|error)
|
|
218
279
|
[--run-id <id>] [--payload <json>] [--studio-url <url>]
|
|
219
|
-
e2e
|
|
280
|
+
e2e End-to-end validate a registered flow (--project <p> --flow <f> [--skip-nodes a,b])
|
|
220
281
|
|
|
221
282
|
Meta:
|
|
222
283
|
version Print the CLI version
|
|
@@ -318,7 +379,7 @@ async function runStart() {
|
|
|
318
379
|
// persist the chosen address so other subcommands can find us
|
|
319
380
|
writeConfig({ port, host: config.host });
|
|
320
381
|
|
|
321
|
-
const registry = createRegistry();
|
|
382
|
+
const registry = createRegistry({ isLoadableEntry: manifestOnlyEntryFilter });
|
|
322
383
|
const events = createEventBus();
|
|
323
384
|
const statusRunner = createStatusRunner({ registry, events, spawner: defaultProcessSpawner });
|
|
324
385
|
const server = serve({ port, hostname: config.host, registry, events, statusRunner });
|
|
@@ -355,7 +416,6 @@ async function seedExamples(registry: Registry) {
|
|
|
355
416
|
|
|
356
417
|
async function seedExample(registry: Registry, exampleName: string) {
|
|
357
418
|
const destDir = join(seeflowHome(), exampleName);
|
|
358
|
-
const flowPath = PROJECT_FLOW_FILENAME;
|
|
359
419
|
|
|
360
420
|
// Always sync from source so that schema changes and example updates are
|
|
361
421
|
// reflected on every startup, even when the dest directory already exists.
|
|
@@ -363,23 +423,14 @@ async function seedExample(registry: Registry, exampleName: string) {
|
|
|
363
423
|
if (!existsSync(srcDir)) return;
|
|
364
424
|
cpSync(srcDir, destDir, { recursive: true });
|
|
365
425
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (!existsSync(flowFile)) return;
|
|
370
|
-
|
|
371
|
-
let demo: unknown;
|
|
372
|
-
try {
|
|
373
|
-
demo = await Bun.file(flowFile).json();
|
|
374
|
-
} catch {
|
|
426
|
+
const outcome = registerProject({ repoPath: destDir, registry });
|
|
427
|
+
if (outcome.kind !== 'ok') {
|
|
428
|
+
console.warn(`Skipped example ${exampleName}: ${JSON.stringify(outcome)}`);
|
|
375
429
|
return;
|
|
376
430
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
registry.upsert({ name: parsed.data.name, repoPath: destDir, flowPath });
|
|
382
|
-
console.log(`Seeded example: ${parsed.data.name} → ${destDir}`);
|
|
431
|
+
for (const entry of outcome.entries) {
|
|
432
|
+
console.log(`Seeded example: ${entry.name} → ${destDir} (${entry.slug})`);
|
|
433
|
+
}
|
|
383
434
|
}
|
|
384
435
|
|
|
385
436
|
async function spawnDaemon(port: number, host: string) {
|
|
@@ -666,28 +717,96 @@ async function runFlowsSummary() {
|
|
|
666
717
|
}
|
|
667
718
|
|
|
668
719
|
async function runFlowsGet() {
|
|
669
|
-
const
|
|
720
|
+
const { slug } = requireProjectFlow();
|
|
670
721
|
const ops = createCliOperations();
|
|
671
|
-
const result = await ops.getFlow(
|
|
722
|
+
const result = await ops.getFlow(slug);
|
|
672
723
|
printOutcome(result);
|
|
673
724
|
}
|
|
674
725
|
|
|
675
726
|
async function runFlowsGraph() {
|
|
676
|
-
const
|
|
727
|
+
const { slug } = requireProjectFlow();
|
|
677
728
|
const ops = createCliOperations();
|
|
678
|
-
const result = await ops.getFlowGraph(
|
|
729
|
+
const result = await ops.getFlowGraph(slug);
|
|
679
730
|
printOutcome(result);
|
|
680
731
|
}
|
|
681
732
|
|
|
682
733
|
async function runFlowsDelete() {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
734
|
+
await runFlowsDeleteManifest();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async function runFlowsCreate() {
|
|
738
|
+
const project = flagValue('project');
|
|
739
|
+
if (!project) printError('Missing required flag: --project');
|
|
740
|
+
const flow = flagValue('flow');
|
|
741
|
+
if (!flow) printError('Missing required flag: --flow');
|
|
742
|
+
const name = flagValue('name');
|
|
743
|
+
if (!name) printError('Missing required flag: --name');
|
|
744
|
+
const icon = flagValue('icon');
|
|
745
|
+
|
|
746
|
+
const body: { id: string; name: string; icon?: string } = {
|
|
747
|
+
id: flow as string,
|
|
748
|
+
name: name as string,
|
|
749
|
+
};
|
|
750
|
+
if (icon !== undefined) body.icon = icon;
|
|
751
|
+
|
|
752
|
+
const { url } = await studioUrlOrDie(hasFlag('no-start'));
|
|
753
|
+
const res = await postJson(
|
|
754
|
+
`${url}/api/projects/${encodeURIComponent(project as string)}/flows`,
|
|
755
|
+
body,
|
|
756
|
+
);
|
|
757
|
+
const out = (await handleResponse(res)) as object;
|
|
758
|
+
printOk(out);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function runFlowsRename() {
|
|
762
|
+
const project = flagValue('project');
|
|
763
|
+
if (!project) printError('Missing required flag: --project');
|
|
764
|
+
const flow = flagValue('flow');
|
|
765
|
+
if (!flow) printError('Missing required flag: --flow');
|
|
766
|
+
const newId = flagValue('new-id');
|
|
767
|
+
const name = flagValue('name');
|
|
768
|
+
const icon = flagValue('icon');
|
|
769
|
+
if (newId === undefined && name === undefined && icon === undefined) {
|
|
770
|
+
printError('flows:rename requires at least one of --new-id, --name, --icon');
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const body: { id?: string; name?: string; icon?: string } = {};
|
|
774
|
+
if (newId !== undefined) body.id = newId;
|
|
775
|
+
if (name !== undefined) body.name = name;
|
|
776
|
+
if (icon !== undefined) body.icon = icon;
|
|
777
|
+
|
|
778
|
+
const { url } = await studioUrlOrDie(hasFlag('no-start'));
|
|
779
|
+
const res = await fetch(
|
|
780
|
+
`${url}/api/projects/${encodeURIComponent(project as string)}/flows/${encodeURIComponent(flow as string)}`,
|
|
781
|
+
{
|
|
782
|
+
method: 'PATCH',
|
|
783
|
+
headers: { 'content-type': 'application/json' },
|
|
784
|
+
body: JSON.stringify(body),
|
|
785
|
+
},
|
|
786
|
+
);
|
|
787
|
+
const out = (await handleResponse(res)) as object;
|
|
788
|
+
printOk(out);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function runFlowsDeleteManifest() {
|
|
792
|
+
const project = flagValue('project');
|
|
793
|
+
if (!project) printError('Missing required flag: --project');
|
|
794
|
+
const flow = flagValue('flow');
|
|
795
|
+
if (!flow) printError('Missing required flag: --flow');
|
|
796
|
+
const newDefault = flagValue('new-default');
|
|
797
|
+
|
|
798
|
+
const query = newDefault !== undefined ? `?newDefault=${encodeURIComponent(newDefault)}` : '';
|
|
799
|
+
const { url } = await studioUrlOrDie(hasFlag('no-start'));
|
|
800
|
+
const res = await fetch(
|
|
801
|
+
`${url}/api/projects/${encodeURIComponent(project as string)}/flows/${encodeURIComponent(flow as string)}${query}`,
|
|
802
|
+
{ method: 'DELETE' },
|
|
803
|
+
);
|
|
804
|
+
const out = (await handleResponse(res)) as object;
|
|
805
|
+
printOk(out);
|
|
687
806
|
}
|
|
688
807
|
|
|
689
808
|
async function runFlowsLayout() {
|
|
690
|
-
const
|
|
809
|
+
const { slug } = requireProjectFlow();
|
|
691
810
|
// Body is optional — `{ options? }` shape if provided. Empty when omitted.
|
|
692
811
|
let options: LayoutOptions | undefined;
|
|
693
812
|
if (hasFlag('json') || hasFlag('file') || hasFlag('stdin')) {
|
|
@@ -695,16 +814,16 @@ async function runFlowsLayout() {
|
|
|
695
814
|
options = body?.options;
|
|
696
815
|
}
|
|
697
816
|
const ops = createCliOperations();
|
|
698
|
-
const result = await ops.applyLayout(
|
|
817
|
+
const result = await ops.applyLayout(slug, options);
|
|
699
818
|
printOutcome(result);
|
|
700
819
|
}
|
|
701
820
|
|
|
702
821
|
async function runFlowsPlay() {
|
|
703
|
-
const
|
|
704
|
-
const nodeId =
|
|
822
|
+
const { project, flow } = requireProjectFlow();
|
|
823
|
+
const nodeId = requirePositional(0, '<nodeId>');
|
|
705
824
|
const { url } = await studioUrlOrDie(hasFlag('no-start'));
|
|
706
825
|
const res = await postJson(
|
|
707
|
-
`${url}/api/flows/${encodeURIComponent(
|
|
826
|
+
`${url}/api/projects/${encodeURIComponent(project)}/flows/${encodeURIComponent(flow)}/play/${encodeURIComponent(nodeId)}`,
|
|
708
827
|
{},
|
|
709
828
|
);
|
|
710
829
|
const out = (await handleResponse(res)) as object;
|
|
@@ -751,52 +870,52 @@ async function runEmit() {
|
|
|
751
870
|
}
|
|
752
871
|
|
|
753
872
|
async function runNodesAdd() {
|
|
754
|
-
const
|
|
873
|
+
const { slug } = requireProjectFlow();
|
|
755
874
|
const body = await bodyFromFlags();
|
|
756
875
|
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
757
876
|
printError('Body must be an object');
|
|
758
877
|
}
|
|
759
878
|
const ops = createCliOperations();
|
|
760
|
-
const result = await ops.addNode(
|
|
879
|
+
const result = await ops.addNode(slug, body as Record<string, unknown>);
|
|
761
880
|
printOutcome(result);
|
|
762
881
|
}
|
|
763
882
|
|
|
764
883
|
async function runFlowAddBulk() {
|
|
765
|
-
const
|
|
884
|
+
const { slug } = requireProjectFlow();
|
|
766
885
|
const body = await bodyFromFlags();
|
|
767
886
|
const parsed = FlowBulkBodySchema.safeParse(body);
|
|
768
887
|
if (!parsed.success) {
|
|
769
888
|
printError(`Invalid flow:add-bulk body: ${JSON.stringify(parsed.error.issues)}`);
|
|
770
889
|
}
|
|
771
890
|
const ops = createCliOperations();
|
|
772
|
-
const result = await ops.addBulk(
|
|
891
|
+
const result = await ops.addBulk(slug, parsed.data);
|
|
773
892
|
printOutcome(result);
|
|
774
893
|
}
|
|
775
894
|
|
|
776
895
|
async function runNodesGet() {
|
|
777
|
-
const
|
|
778
|
-
const nodeId =
|
|
896
|
+
const { slug } = requireProjectFlow();
|
|
897
|
+
const nodeId = requirePositional(0, '<nodeId>');
|
|
779
898
|
const ops = createCliOperations();
|
|
780
|
-
const result = await ops.getNode(
|
|
899
|
+
const result = await ops.getNode(slug, nodeId);
|
|
781
900
|
printOutcome(result);
|
|
782
901
|
}
|
|
783
902
|
|
|
784
903
|
async function runNodesPatch() {
|
|
785
|
-
const
|
|
786
|
-
const nodeId =
|
|
904
|
+
const { slug } = requireProjectFlow();
|
|
905
|
+
const nodeId = requirePositional(0, '<nodeId>');
|
|
787
906
|
const body = await bodyFromFlags();
|
|
788
907
|
const parsed = NodePatchBodySchema.safeParse(body);
|
|
789
908
|
if (!parsed.success) {
|
|
790
909
|
printError(`Invalid nodes:patch body: ${JSON.stringify(parsed.error.issues)}`);
|
|
791
910
|
}
|
|
792
911
|
const ops = createCliOperations();
|
|
793
|
-
const result = await ops.patchNode(
|
|
912
|
+
const result = await ops.patchNode(slug, nodeId, parsed.data);
|
|
794
913
|
printOutcome(result);
|
|
795
914
|
}
|
|
796
915
|
|
|
797
916
|
async function runNodesMove() {
|
|
798
|
-
const
|
|
799
|
-
const nodeId =
|
|
917
|
+
const { slug } = requireProjectFlow();
|
|
918
|
+
const nodeId = requirePositional(0, '<nodeId>');
|
|
800
919
|
const xRaw = flagValue('x');
|
|
801
920
|
const yRaw = flagValue('y');
|
|
802
921
|
if (xRaw === undefined || yRaw === undefined) {
|
|
@@ -808,13 +927,13 @@ async function runNodesMove() {
|
|
|
808
927
|
printError('--x and --y must be finite numbers');
|
|
809
928
|
}
|
|
810
929
|
const ops = createCliOperations();
|
|
811
|
-
const result = await ops.moveNode(
|
|
930
|
+
const result = await ops.moveNode(slug, nodeId, { x, y });
|
|
812
931
|
printOutcome(result);
|
|
813
932
|
}
|
|
814
933
|
|
|
815
934
|
async function runNodesReorder() {
|
|
816
|
-
const
|
|
817
|
-
const nodeId =
|
|
935
|
+
const { slug } = requireProjectFlow();
|
|
936
|
+
const nodeId = requirePositional(0, '<nodeId>');
|
|
818
937
|
const op = flagValue('op');
|
|
819
938
|
if (!op) printError('nodes:reorder requires --op forward|backward|toFront|toBack|toIndex');
|
|
820
939
|
let raw: Record<string, unknown> = { op };
|
|
@@ -832,47 +951,47 @@ async function runNodesReorder() {
|
|
|
832
951
|
printError(`Invalid nodes:reorder body: ${JSON.stringify(parsed.error.issues)}`);
|
|
833
952
|
}
|
|
834
953
|
const ops = createCliOperations();
|
|
835
|
-
const result = await ops.reorderNode(
|
|
954
|
+
const result = await ops.reorderNode(slug, nodeId, parsed.data);
|
|
836
955
|
printOutcome(result);
|
|
837
956
|
}
|
|
838
957
|
|
|
839
958
|
async function runNodesDelete() {
|
|
840
|
-
const
|
|
841
|
-
const nodeId =
|
|
959
|
+
const { slug } = requireProjectFlow();
|
|
960
|
+
const nodeId = requirePositional(0, '<nodeId>');
|
|
842
961
|
const ops = createCliOperations();
|
|
843
|
-
const result = await ops.deleteNode(
|
|
962
|
+
const result = await ops.deleteNode(slug, nodeId);
|
|
844
963
|
printOutcome(result);
|
|
845
964
|
}
|
|
846
965
|
|
|
847
966
|
async function runConnectorsAdd() {
|
|
848
|
-
const
|
|
967
|
+
const { slug } = requireProjectFlow();
|
|
849
968
|
const body = await bodyFromFlags();
|
|
850
969
|
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
851
970
|
printError('Body must be an object');
|
|
852
971
|
}
|
|
853
972
|
const ops = createCliOperations();
|
|
854
|
-
const result = await ops.addConnector(
|
|
973
|
+
const result = await ops.addConnector(slug, body as Record<string, unknown>);
|
|
855
974
|
printOutcome(result);
|
|
856
975
|
}
|
|
857
976
|
|
|
858
977
|
async function runConnectorsPatch() {
|
|
859
|
-
const
|
|
860
|
-
const connId =
|
|
978
|
+
const { slug } = requireProjectFlow();
|
|
979
|
+
const connId = requirePositional(0, '<connectorId>');
|
|
861
980
|
const body = await bodyFromFlags();
|
|
862
981
|
const parsed = ConnectorPatchBodySchema.safeParse(body);
|
|
863
982
|
if (!parsed.success) {
|
|
864
983
|
printError(`Invalid connectors:patch body: ${JSON.stringify(parsed.error.issues)}`);
|
|
865
984
|
}
|
|
866
985
|
const ops = createCliOperations();
|
|
867
|
-
const result = await ops.patchConnector(
|
|
986
|
+
const result = await ops.patchConnector(slug, connId, parsed.data);
|
|
868
987
|
printOutcome(result);
|
|
869
988
|
}
|
|
870
989
|
|
|
871
990
|
async function runConnectorsDelete() {
|
|
872
|
-
const
|
|
873
|
-
const connId =
|
|
991
|
+
const { slug } = requireProjectFlow();
|
|
992
|
+
const connId = requirePositional(0, '<connectorId>');
|
|
874
993
|
const ops = createCliOperations();
|
|
875
|
-
const result = await ops.deleteConnector(
|
|
994
|
+
const result = await ops.deleteConnector(slug, connId);
|
|
876
995
|
printOutcome(result);
|
|
877
996
|
}
|
|
878
997
|
|
|
@@ -906,9 +1025,14 @@ async function runValidate() {
|
|
|
906
1025
|
|
|
907
1026
|
async function runSchema() {
|
|
908
1027
|
const category = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined;
|
|
1028
|
+
const jqFilter = flagValue('jq');
|
|
909
1029
|
const { listSchemaCategories, getSchemaCategory } = await import('./schema-catalog.ts');
|
|
910
1030
|
if (!category) {
|
|
911
|
-
|
|
1031
|
+
const base = { categories: listSchemaCategories() };
|
|
1032
|
+
if (jqFilter !== undefined) {
|
|
1033
|
+
printOk({ result: applyJqOrDie(base, jqFilter) });
|
|
1034
|
+
}
|
|
1035
|
+
printOk(base);
|
|
912
1036
|
}
|
|
913
1037
|
const payload = getSchemaCategory(category as string);
|
|
914
1038
|
if (!payload) {
|
|
@@ -917,16 +1041,39 @@ async function runSchema() {
|
|
|
917
1041
|
process.stderr.write(`${JSON.stringify({ error: message, code: 'notFound', available })}\n`);
|
|
918
1042
|
process.exit(3);
|
|
919
1043
|
}
|
|
920
|
-
|
|
1044
|
+
const base = { name: category, schemas: payload.schemas, notes: payload.notes };
|
|
1045
|
+
if (jqFilter !== undefined) {
|
|
1046
|
+
printOk({ name: category, result: applyJqOrDie(base, jqFilter) });
|
|
1047
|
+
}
|
|
1048
|
+
printOk(base);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Apply a --jq filter and unwrap a single-result stream into the value
|
|
1052
|
+
// itself; multi-output streams (from `[]` or `|`) come through as arrays
|
|
1053
|
+
// so downstream consumers can tell `.foo[]` (multiple) apart from `.foo`
|
|
1054
|
+
// (single value that happens to be an array). On parse / type errors exits
|
|
1055
|
+
// with code 2 (badJq).
|
|
1056
|
+
function applyJqOrDie(input: unknown, filterStr: string): unknown {
|
|
1057
|
+
try {
|
|
1058
|
+
const stream = applyJq(input, filterStr);
|
|
1059
|
+
if (stream.length === 1) return stream[0];
|
|
1060
|
+
return stream;
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
if (err instanceof JqError) {
|
|
1063
|
+
process.stderr.write(`${JSON.stringify({ error: err.message, code: 'badJq' })}\n`);
|
|
1064
|
+
process.exit(2);
|
|
1065
|
+
}
|
|
1066
|
+
throw err;
|
|
1067
|
+
}
|
|
921
1068
|
}
|
|
922
1069
|
|
|
923
1070
|
async function runE2e() {
|
|
924
|
-
const
|
|
1071
|
+
const { project, flow } = requireProjectFlow();
|
|
925
1072
|
const skipNodesRaw = flagValue('skip-nodes');
|
|
926
1073
|
const skipNodes = skipNodesRaw ? skipNodesRaw.split(',').filter(Boolean) : [];
|
|
927
1074
|
const { url } = await studioUrlOrDie(hasFlag('no-start'));
|
|
928
1075
|
const { validateEndToEnd } = await import('./cli-e2e.ts');
|
|
929
|
-
const report = await validateEndToEnd({
|
|
1076
|
+
const report = await validateEndToEnd({ project, flow, url, skipNodes });
|
|
930
1077
|
if (!report.ok) {
|
|
931
1078
|
process.stderr.write(`${JSON.stringify(report)}\n`);
|
|
932
1079
|
process.exit(1);
|