@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.
Files changed (107) hide show
  1. package/README.md +40 -0
  2. package/dist/web/assets/{architectureDiagram-3BPJPVTR-D5iHwVvy.js → architectureDiagram-3BPJPVTR-id0XTZQC.js} +1 -1
  3. package/dist/web/assets/{blockDiagram-GPEHLZMM-MAYYm7FM.js → blockDiagram-GPEHLZMM-Cjvfg0ZP.js} +1 -1
  4. package/dist/web/assets/{c4Diagram-AAUBKEIU-7P7yfHg1.js → c4Diagram-AAUBKEIU-Dyq-0e8Q.js} +1 -1
  5. package/dist/web/assets/channel-Ajb6KiL3.js +1 -0
  6. package/dist/web/assets/{chart-C68vupBE.js → chart-DuTGW-Dj.js} +1 -1
  7. package/dist/web/assets/{chunk-2J33WTMH-Bb4cSusI.js → chunk-2J33WTMH-DsD65OzD.js} +1 -1
  8. package/dist/web/assets/{chunk-4BX2VUAB-DXYpcpTh.js → chunk-4BX2VUAB-BpytKE8P.js} +1 -1
  9. package/dist/web/assets/{chunk-55IACEB6-BxuYKDnf.js → chunk-55IACEB6-DIILAUq9.js} +1 -1
  10. package/dist/web/assets/{chunk-727SXJPM-DbWlxAr2.js → chunk-727SXJPM-C4ih-gTo.js} +1 -1
  11. package/dist/web/assets/{chunk-AQP2D5EJ-DT8S1q80.js → chunk-AQP2D5EJ-BsYoWdVM.js} +1 -1
  12. package/dist/web/assets/{chunk-FMBD7UC4-Dc0wDuZz.js → chunk-FMBD7UC4-Db6L0z4p.js} +1 -1
  13. package/dist/web/assets/{chunk-ND2GUHAM-CqLLK6H0.js → chunk-ND2GUHAM-BNLqZYMx.js} +1 -1
  14. package/dist/web/assets/{chunk-QZHKN3VN-CxF7nkDI.js → chunk-QZHKN3VN-DL5PK45j.js} +1 -1
  15. package/dist/web/assets/classDiagram-4FO5ZUOK-Cgw6ezRo.js +1 -0
  16. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-Cgw6ezRo.js +1 -0
  17. package/dist/web/assets/{code-block-DR9fiK_U.js → code-block-C1SJv-Al.js} +1 -1
  18. package/dist/web/assets/{cose-bilkent-S5V4N54A-BflFbtY2.js → cose-bilkent-S5V4N54A-ChX5nR0f.js} +1 -1
  19. package/dist/web/assets/{dagre-BM42HDAG-BJ5UdyYS.js → dagre-BM42HDAG-BXeL3fEN.js} +1 -1
  20. package/dist/web/assets/{diagram-2AECGRRQ-D0M8fCf7.js → diagram-2AECGRRQ-B6WtmEP-.js} +1 -1
  21. package/dist/web/assets/{diagram-5GNKFQAL-D67gAMS4.js → diagram-5GNKFQAL-SXs7ALwM.js} +1 -1
  22. package/dist/web/assets/{diagram-KO2AKTUF-XX62HBG-.js → diagram-KO2AKTUF-D5zylPYo.js} +1 -1
  23. package/dist/web/assets/{diagram-LMA3HP47-DCFq3Oac.js → diagram-LMA3HP47-CByIUlQF.js} +1 -1
  24. package/dist/web/assets/{diagram-OG6HWLK6-Be392NCN.js → diagram-OG6HWLK6-BH1MfUqV.js} +1 -1
  25. package/dist/web/assets/{erDiagram-TEJ5UH35-DP4eP0as.js → erDiagram-TEJ5UH35-BOOnRFBh.js} +1 -1
  26. package/dist/web/assets/{flowDiagram-I6XJVG4X-Ch1GVJ9R.js → flowDiagram-I6XJVG4X-BynWDHJP.js} +1 -1
  27. package/dist/web/assets/{ganttDiagram-6RSMTGT7-DtvkTizu.js → ganttDiagram-6RSMTGT7-Cgq_djyN.js} +1 -1
  28. package/dist/web/assets/{gitGraphDiagram-PVQCEYII-YGcuBgb9.js → gitGraphDiagram-PVQCEYII-ciGSgmfT.js} +1 -1
  29. package/dist/web/assets/index-DiakpHyc.js +8619 -0
  30. package/dist/web/assets/{index-DljfurDC.css → index-fl8DS9WO.css} +1 -1
  31. package/dist/web/assets/{index.es-jrsJPbYZ.js → index.es-C7TtaIfa.js} +1 -1
  32. package/dist/web/assets/{infoDiagram-5YYISTIA-wce0BORz.js → infoDiagram-5YYISTIA-DqMb3_c-.js} +1 -1
  33. package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-u2MvPgdW.js → ishikawaDiagram-YF4QCWOH-CAO6KqQU.js} +1 -1
  34. package/dist/web/assets/{journeyDiagram-JHISSGLW-BsOyrTiA.js → journeyDiagram-JHISSGLW-Di8MsLTo.js} +1 -1
  35. package/dist/web/assets/{jspdf.es.min-ptMERvnN.js → jspdf.es.min-Cq4dY-lT.js} +3 -3
  36. package/dist/web/assets/{kanban-definition-UN3LZRKU-BaraYV9q.js → kanban-definition-UN3LZRKU-ClOmVNcX.js} +1 -1
  37. package/dist/web/assets/{linear-BVqXcDUJ.js → linear-B3OKBKaT.js} +1 -1
  38. package/dist/web/assets/{markdown-DqP0Cywq.js → markdown-Dg8NEx1K.js} +1 -1
  39. package/dist/web/assets/{mermaid.core-CakR_vo1.js → mermaid.core-Bw-m7bH-.js} +4 -4
  40. package/dist/web/assets/{mindmap-definition-RKZ34NQL-CO5AsZw3.js → mindmap-definition-RKZ34NQL-CUBA1zfc.js} +1 -1
  41. package/dist/web/assets/{pieDiagram-4H26LBE5-CiDJY-kx.js → pieDiagram-4H26LBE5-Dux5HvSU.js} +1 -1
  42. package/dist/web/assets/{quadrantDiagram-W4KKPZXB-BS6oN3s_.js → quadrantDiagram-W4KKPZXB-DU3gQGo3.js} +1 -1
  43. package/dist/web/assets/{requirementDiagram-4Y6WPE33-CNbUR_FF.js → requirementDiagram-4Y6WPE33-CD3A_U9j.js} +1 -1
  44. package/dist/web/assets/{sankeyDiagram-5OEKKPKP-0Esj5uzm.js → sankeyDiagram-5OEKKPKP-Cd4mc26P.js} +1 -1
  45. package/dist/web/assets/{sequenceDiagram-3UESZ5HK-DR3U38Zi.js → sequenceDiagram-3UESZ5HK-Da0iOMgq.js} +1 -1
  46. package/dist/web/assets/{stateDiagram-AJRCARHV-C50RQjWe.js → stateDiagram-AJRCARHV-P94LaOD2.js} +1 -1
  47. package/dist/web/assets/stateDiagram-v2-BHNVJYJU--JLHF28o.js +1 -0
  48. package/dist/web/assets/{time-C_2J9tFX.js → time-0JEErjjJ.js} +1 -1
  49. package/dist/web/assets/{timeline-definition-PNZ67QCA-BQXyo2r_.js → timeline-definition-PNZ67QCA-BqAYomix.js} +1 -1
  50. package/dist/web/assets/{vennDiagram-CIIHVFJN-DZJ8M3EA.js → vennDiagram-CIIHVFJN-BWuPhfIM.js} +1 -1
  51. package/dist/web/assets/{wardley-L42UT6IY-B96HtW3i.js → wardley-L42UT6IY-iiGkgUQj.js} +1 -1
  52. package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BHkQ79WC.js → wardleyDiagram-YWT4CUSO-CtqzFQXL.js} +1 -1
  53. package/dist/web/assets/{xychartDiagram-2RQKCTM6-B_f8koGI.js → xychartDiagram-2RQKCTM6-BGrOXndI.js} +1 -1
  54. package/dist/web/index.html +2 -2
  55. package/examples/component-showcase/seeflow.json +6 -0
  56. package/examples/ecommerce-platform/seeflow.json +6 -0
  57. package/examples/order-pipeline/seeflow.json +6 -0
  58. package/package.json +1 -1
  59. package/src/api.ts +739 -94
  60. package/src/cli-e2e.ts +24 -13
  61. package/src/cli-helpers.ts +26 -0
  62. package/src/cli-manifest.ts +330 -87
  63. package/src/cli-ops.ts +56 -2
  64. package/src/cli.ts +228 -81
  65. package/src/cors.ts +93 -0
  66. package/src/jq-filter.ts +253 -0
  67. package/src/mcp-shim.ts +114 -7
  68. package/src/mcp-ui.ts +126 -0
  69. package/src/mcp.ts +258 -97
  70. package/src/node-files.ts +18 -7
  71. package/src/operations.ts +68 -32
  72. package/src/project-scanner.ts +105 -0
  73. package/src/registry.ts +79 -18
  74. package/src/route-resolve.ts +41 -0
  75. package/src/schema.ts +54 -0
  76. package/src/server.ts +24 -3
  77. package/src/slugify.ts +16 -0
  78. package/dist/web/assets/channel-BjsQQK93.js +0 -1
  79. package/dist/web/assets/classDiagram-4FO5ZUOK-p3FY5uNC.js +0 -1
  80. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-p3FY5uNC.js +0 -1
  81. package/dist/web/assets/index-Bg3PU4Ev.js +0 -8614
  82. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-BbNrmkIR.js +0 -1
  83. /package/examples/component-showcase/{flow.json → flows/main/flow.json} +0 -0
  84. /package/examples/component-showcase/{nodes → flows/main/nodes}/chart/spec.json +0 -0
  85. /package/examples/component-showcase/{nodes → flows/main/nodes}/counter/spec.json +0 -0
  86. /package/examples/component-showcase/{nodes → flows/main/nodes}/fetcher/actions/refresh.ts +0 -0
  87. /package/examples/component-showcase/{nodes → flows/main/nodes}/fetcher/spec.json +0 -0
  88. /package/examples/component-showcase/{nodes → flows/main/nodes}/form/spec.json +0 -0
  89. /package/examples/component-showcase/{style.json → flows/main/style.json} +0 -0
  90. /package/examples/ecommerce-platform/{flow.json → flows/main/flow.json} +0 -0
  91. /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-3zFtHg6ENc/detail.md +0 -0
  92. /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-5F424NWbEu/detail.md +0 -0
  93. /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-CbwYqb7NfB/detail.md +0 -0
  94. /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-XwygzfKPZ5/view.html +0 -0
  95. /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-fkptXw7uvs/detail.md +0 -0
  96. /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-kwBY8YPmYM/detail.md +0 -0
  97. /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-mPqan8rFYN/detail.md +0 -0
  98. /package/examples/ecommerce-platform/{nodes → flows/main/nodes}/node-yKrg9DV5fJ/detail.md +0 -0
  99. /package/examples/ecommerce-platform/{scripts → flows/main/scripts}/play.ts +0 -0
  100. /package/examples/ecommerce-platform/{style.json → flows/main/style.json} +0 -0
  101. /package/examples/order-pipeline/{flow.json → flows/main/flow.json} +0 -0
  102. /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-GXTKUcE3ye/detail.md +0 -0
  103. /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-XKIyds0TDg/detail.md +0 -0
  104. /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-YOYiHJpY0i/detail.md +0 -0
  105. /package/examples/order-pipeline/{nodes → flows/main/nodes}/node-zUIH7WFnhK/detail.md +0 -0
  106. /package/examples/order-pipeline/{scripts → flows/main/scripts}/play.ts +0 -0
  107. /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 { createRegistry } from './registry.ts';
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({ registry: createRegistry() });
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 { drainStdin, loadBody, printError, printOk, printOutcome } from './cli-helpers.ts';
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 Create a new project (--path <dir> --name <name> [--description <text>])
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 <id> Get flow details
193
- flows:graph <id> List nodes + connectors without inlined file content
194
- flows:delete <id> Unregister a flow
195
- flows:layout <id> Apply ELK layout, writing style.json (--json/--file/--stdin optional)
196
- flow:add-bulk <id> Add many nodes + connectors atomically (--json/--file/--stdin; body { nodes?, connectors? })
197
- nodes:add <id> Add a node (--json/--file/--stdin)
198
- nodes:get <id> <n> Get a node with detail / html content inlined
199
- nodes:patch <id> <n> Patch a node (--json/--file/--stdin)
200
- nodes:move <id> <n> Move a node (--x N --y N)
201
- nodes:reorder <id> <n> Reorder a node (--op forward|backward|toFront|toBack|toIndex [--index N])
202
- nodes:delete <id> <n> Delete a node
203
- connectors:add <id> Add a connector (--json/--file/--stdin)
204
- connectors:patch <id> <connId> Patch a connector (--json/--file/--stdin)
205
- connectors:delete <id> <connId> Delete a connector
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 <id> <n> Trigger a play on node <n>
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 <id> End-to-end validate a registered flow (--skip-nodes a,b)
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
- if (registry.getByRepoPathAndFlowPath(destDir, flowPath)) return;
367
-
368
- const flowFile = join(destDir, flowPath);
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
- const parsed = FlowSchema.safeParse(demo);
379
- if (!parsed.success) return;
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 flowId = requireArg(1, '<flowId>');
720
+ const { slug } = requireProjectFlow();
670
721
  const ops = createCliOperations();
671
- const result = await ops.getFlow(flowId);
722
+ const result = await ops.getFlow(slug);
672
723
  printOutcome(result);
673
724
  }
674
725
 
675
726
  async function runFlowsGraph() {
676
- const flowId = requireArg(1, '<flowId>');
727
+ const { slug } = requireProjectFlow();
677
728
  const ops = createCliOperations();
678
- const result = await ops.getFlowGraph(flowId);
729
+ const result = await ops.getFlowGraph(slug);
679
730
  printOutcome(result);
680
731
  }
681
732
 
682
733
  async function runFlowsDelete() {
683
- const flowId = requireArg(1, '<flowId>');
684
- const ops = createCliOperations();
685
- const result = ops.deleteFlow(flowId);
686
- printOutcome(result);
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 flowId = requireArg(1, '<flowId>');
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(flowId, options);
817
+ const result = await ops.applyLayout(slug, options);
699
818
  printOutcome(result);
700
819
  }
701
820
 
702
821
  async function runFlowsPlay() {
703
- const flowId = requireArg(1, '<flowId>');
704
- const nodeId = requireArg(2, '<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(flowId)}/play/${encodeURIComponent(nodeId)}`,
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 flowId = requireArg(1, '<flowId>');
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(flowId, body as Record<string, unknown>);
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 flowId = requireArg(1, '<flowId>');
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(flowId, parsed.data);
891
+ const result = await ops.addBulk(slug, parsed.data);
773
892
  printOutcome(result);
774
893
  }
775
894
 
776
895
  async function runNodesGet() {
777
- const flowId = requireArg(1, '<flowId>');
778
- const nodeId = requireArg(2, '<nodeId>');
896
+ const { slug } = requireProjectFlow();
897
+ const nodeId = requirePositional(0, '<nodeId>');
779
898
  const ops = createCliOperations();
780
- const result = await ops.getNode(flowId, nodeId);
899
+ const result = await ops.getNode(slug, nodeId);
781
900
  printOutcome(result);
782
901
  }
783
902
 
784
903
  async function runNodesPatch() {
785
- const flowId = requireArg(1, '<flowId>');
786
- const nodeId = requireArg(2, '<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(flowId, nodeId, parsed.data);
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 flowId = requireArg(1, '<flowId>');
799
- const nodeId = requireArg(2, '<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(flowId, nodeId, { x, y });
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 flowId = requireArg(1, '<flowId>');
817
- const nodeId = requireArg(2, '<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(flowId, nodeId, parsed.data);
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 flowId = requireArg(1, '<flowId>');
841
- const nodeId = requireArg(2, '<nodeId>');
959
+ const { slug } = requireProjectFlow();
960
+ const nodeId = requirePositional(0, '<nodeId>');
842
961
  const ops = createCliOperations();
843
- const result = await ops.deleteNode(flowId, nodeId);
962
+ const result = await ops.deleteNode(slug, nodeId);
844
963
  printOutcome(result);
845
964
  }
846
965
 
847
966
  async function runConnectorsAdd() {
848
- const flowId = requireArg(1, '<flowId>');
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(flowId, body as Record<string, unknown>);
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 flowId = requireArg(1, '<flowId>');
860
- const connId = requireArg(2, '<connectorId>');
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(flowId, connId, parsed.data);
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 flowId = requireArg(1, '<flowId>');
873
- const connId = requireArg(2, '<connectorId>');
991
+ const { slug } = requireProjectFlow();
992
+ const connId = requirePositional(0, '<connectorId>');
874
993
  const ops = createCliOperations();
875
- const result = await ops.deleteConnector(flowId, connId);
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
- printOk({ categories: listSchemaCategories() });
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
- printOk({ name: category, schemas: payload.schemas, notes: payload.notes });
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 flowId = requireArg(1, '<flowId>');
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({ flowId, url, skipNodes });
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);