@tuongaz/seeflow 0.1.41 → 0.1.42

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 (38) hide show
  1. package/README.md +2 -15
  2. package/dist/web/assets/{index-C029S3KL.js → index-BPUoNIBm.js} +1541 -1541
  3. package/dist/web/assets/{index-BwdVgB2y.css → index-BlkUOp7f.css} +1 -1
  4. package/dist/web/assets/{index.es-Ylk3HlXb.js → index.es-mje3R_63.js} +1 -1
  5. package/dist/web/assets/{jspdf.es.min-Bf66gPs3.js → jspdf.es.min-DX3imOs2.js} +3 -3
  6. package/dist/web/index.html +2 -2
  7. package/examples/ecommerce-platform/.seeflow/flow.json +47 -47
  8. package/examples/ecommerce-platform/.seeflow/style.json +10 -10
  9. package/examples/order-pipeline/.seeflow/flow.json +17 -17
  10. package/examples/order-pipeline/.seeflow/style.json +4 -4
  11. package/package.json +1 -1
  12. package/src/api.ts +101 -14
  13. package/src/atomic-write.ts +16 -0
  14. package/src/cli-e2e.ts +420 -0
  15. package/src/cli-helpers.ts +65 -0
  16. package/src/cli.ts +371 -17
  17. package/src/mcp.ts +116 -23
  18. package/src/merge.ts +1 -1
  19. package/src/node-files.ts +45 -0
  20. package/src/operations.ts +304 -98
  21. package/src/proxy.ts +35 -6
  22. package/src/registry.ts +2 -1
  23. package/src/schema.ts +31 -25
  24. package/src/short-id.ts +24 -0
  25. package/src/status-runner.ts +9 -8
  26. package/src/watcher.ts +14 -14
  27. /package/examples/ecommerce-platform/.seeflow/{details/auth-service.md → nodes/node-3zFtHg6ENc/detail.md} +0 -0
  28. /package/examples/ecommerce-platform/.seeflow/{details/cart-service.md → nodes/node-5F424NWbEu/detail.md} +0 -0
  29. /package/examples/ecommerce-platform/.seeflow/{details/api-gateway.md → nodes/node-CbwYqb7NfB/detail.md} +0 -0
  30. /package/examples/ecommerce-platform/.seeflow/{scripts/platform-health.html → nodes/node-XwygzfKPZ5/view.html} +0 -0
  31. /package/examples/ecommerce-platform/.seeflow/{details/notification-service.md → nodes/node-fkptXw7uvs/detail.md} +0 -0
  32. /package/examples/ecommerce-platform/.seeflow/{details/product-service.md → nodes/node-kwBY8YPmYM/detail.md} +0 -0
  33. /package/examples/ecommerce-platform/.seeflow/{details/payment-service.md → nodes/node-mPqan8rFYN/detail.md} +0 -0
  34. /package/examples/ecommerce-platform/.seeflow/{details/order-service.md → nodes/node-yKrg9DV5fJ/detail.md} +0 -0
  35. /package/examples/order-pipeline/.seeflow/{details/inventory-service.md → nodes/node-GXTKUcE3ye/detail.md} +0 -0
  36. /package/examples/order-pipeline/.seeflow/{details/post-orders.md → nodes/node-XKIyds0TDg/detail.md} +0 -0
  37. /package/examples/order-pipeline/.seeflow/{details/payment-service.md → nodes/node-YOYiHJpY0i/detail.md} +0 -0
  38. /package/examples/order-pipeline/.seeflow/{details/fulfillment-service.md → nodes/node-zUIH7WFnhK/detail.md} +0 -0
package/src/cli.ts CHANGED
@@ -1,6 +1,7 @@
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 } from './cli-helpers.ts';
4
5
  import { createEventBus } from './events.ts';
5
6
  import { seeflowHome } from './paths.ts';
6
7
  import { defaultProcessSpawner } from './process-spawner.ts';
@@ -40,6 +41,67 @@ const flagValue = (name: string): string | undefined => {
40
41
 
41
42
  const hasFlag = (name: string): boolean => argv.includes(`--${name}`);
42
43
 
44
+ const requireArg = (idx: number, name: string): string => {
45
+ const v = argv[idx];
46
+ if (!v || v.startsWith('--')) {
47
+ printError(`Missing required positional argument: ${name}`);
48
+ }
49
+ return v as string;
50
+ };
51
+
52
+ async function studioUrlOrDie(noStart: boolean): Promise<{ url: string; port: number }> {
53
+ const config = readConfig();
54
+ const overrideUrl = process.env.SEEFLOW_STUDIO_URL?.replace(/\/+$/, '');
55
+ const url = overrideUrl ?? studioUrl(config);
56
+ await ensureStudioRunning(url, config.port, noStart);
57
+ return { url, port: config.port };
58
+ }
59
+
60
+ async function bodyFromFlags(): Promise<unknown> {
61
+ return loadBody(
62
+ { json: flagValue('json'), file: flagValue('file'), stdin: hasFlag('stdin') },
63
+ drainStdin,
64
+ );
65
+ }
66
+
67
+ async function postJson(url: string, body: unknown): Promise<Response> {
68
+ return fetch(url, {
69
+ method: 'POST',
70
+ headers: { 'content-type': 'application/json' },
71
+ body: JSON.stringify(body),
72
+ });
73
+ }
74
+
75
+ async function patchJson(url: string, body: unknown): Promise<Response> {
76
+ return fetch(url, {
77
+ method: 'PATCH',
78
+ headers: { 'content-type': 'application/json' },
79
+ body: JSON.stringify(body),
80
+ });
81
+ }
82
+
83
+ async function deleteRequest(url: string): Promise<Response> {
84
+ return fetch(url, { method: 'DELETE' });
85
+ }
86
+
87
+ async function handleResponse(res: Response): Promise<unknown> {
88
+ const text = await res.text();
89
+ let parsed: unknown = text;
90
+ try {
91
+ parsed = JSON.parse(text);
92
+ } catch {
93
+ /* keep raw text */
94
+ }
95
+ if (!res.ok) {
96
+ const detail =
97
+ typeof parsed === 'object' && parsed !== null
98
+ ? JSON.stringify(parsed)
99
+ : String(parsed).slice(0, 500);
100
+ printError(`Studio returned ${res.status}: ${detail}`);
101
+ }
102
+ return parsed;
103
+ }
104
+
43
105
  const DEBUG = hasFlag('debug') || process.env.SEEFLOW_DEBUG === '1';
44
106
  const dbg = (msg: string) => {
45
107
  if (DEBUG) console.error(`[debug] ${msg}`);
@@ -58,9 +120,44 @@ if (argv.includes('--version') || argv.includes('-v')) {
58
120
  await runStop();
59
121
  } else if (sub === 'register') {
60
122
  await runRegister();
61
- } else if (['unregister', 'list'].includes(sub)) {
62
- console.log(`seeflow ${sub}: not implemented (M1.B)`);
63
- process.exit(0);
123
+ } else if (sub === 'flows:register') {
124
+ await runRegister();
125
+ } else if (sub === 'projects:create') {
126
+ await runProjectsCreate();
127
+ } else if (sub === 'flows:list') {
128
+ await runFlowsList();
129
+ } else if (sub === 'flows:get') {
130
+ await runFlowsGet();
131
+ } else if (sub === 'flows:delete') {
132
+ await runFlowsDelete();
133
+ } else if (sub === 'flows:layout') {
134
+ await runFlowsLayout();
135
+ } else if (sub === 'flows:play') {
136
+ await runFlowsPlay();
137
+ } else if (sub === 'nodes:add') {
138
+ await runNodesAdd();
139
+ } else if (sub === 'nodes:add-bulk') {
140
+ await runNodesAddBulk();
141
+ } else if (sub === 'nodes:patch') {
142
+ await runNodesPatch();
143
+ } else if (sub === 'nodes:move') {
144
+ await runNodesMove();
145
+ } else if (sub === 'nodes:reorder') {
146
+ await runNodesReorder();
147
+ } else if (sub === 'nodes:delete') {
148
+ await runNodesDelete();
149
+ } else if (sub === 'connectors:add') {
150
+ await runConnectorsAdd();
151
+ } else if (sub === 'connectors:add-bulk') {
152
+ await runConnectorsAddBulk();
153
+ } else if (sub === 'connectors:patch') {
154
+ await runConnectorsPatch();
155
+ } else if (sub === 'connectors:delete') {
156
+ await runConnectorsDelete();
157
+ } else if (sub === 'validate') {
158
+ await runValidate();
159
+ } else if (sub === 'e2e') {
160
+ await runE2e();
64
161
  } else {
65
162
  console.error(`Unknown subcommand: ${sub}`);
66
163
  printHelp();
@@ -76,32 +173,58 @@ Usage:
76
173
  npx -y @tuongaz/seeflow@latest [command] [options]
77
174
 
78
175
  Commands:
79
- start Start the SeeFlow Studio server (default port 4321) — default when no command is given
80
- stop Stop a background studio instance
81
- register Register a demo repo with the running studio
82
- version Print the CLI version
83
- help Show this help message
176
+ start Start the SeeFlow Studio server (default port 4321) — default when no command is given
177
+ stop Stop a background studio instance
178
+ register Register a demo repo with the running studio (alias of flows:register)
179
+ flows:register Register a demo repo with the running studio
180
+ projects:create Create a new project (--name <name>)
181
+ flows:list List registered flows
182
+ flows:get <id> Get flow details
183
+ flows:delete <id> Unregister a flow
184
+ flows:layout <id> POST a layout payload (--json/--file/--stdin)
185
+ flows:play <id> <n> Trigger a play on node <n>
186
+ nodes:add <id> Add a node (--json/--file/--stdin)
187
+ nodes:add-bulk <id> Add many nodes (--json/--file/--stdin)
188
+ nodes:patch <id> <n> Patch a node (--json/--file/--stdin)
189
+ nodes:move <id> <n> Move a node (--x N --y N)
190
+ nodes:reorder <id> <n> Reorder a node (--op forward|backward|toFront|toBack|toIndex [--index N])
191
+ nodes:delete <id> <n> Delete a node
192
+ connectors:add <id> Add a connector (--json/--file/--stdin)
193
+ connectors:add-bulk <id> Add many connectors (--json/--file/--stdin)
194
+ connectors:patch <id> <connId> Patch a connector (--json/--file/--stdin)
195
+ connectors:delete <id> <connId> Delete a connector
196
+ validate Schema-validate a flow.json (--file <file> [--style <file>])
197
+ e2e <id> End-to-end validate a registered flow (--skip-nodes a,b)
198
+ version Print the CLI version
199
+ help Show this help message
84
200
 
85
201
  Global options:
86
- --version, -v Print the CLI version and exit
202
+ --version, -v Print the CLI version and exit
203
+ --no-start Fail if studio is not already running
204
+
205
+ Body source flags (where applicable):
206
+ --json '<JSON>' Inline JSON body
207
+ --file <path> Read JSON body from file
208
+ --stdin Read JSON body from stdin
87
209
 
88
210
  Options (start):
89
- --port <n> Listen on port n (default: 4321)
90
- --foreground Run attached to the terminal (default: background)
91
- --daemon Deprecated alias — background is already the default
92
- --debug Verbose logs + pipe daemon output to ~/.seeflow/seeflow.log
211
+ --port <n> Listen on port n (default: 4321)
212
+ --foreground Run attached to the terminal (default: background)
213
+ --daemon Deprecated alias — background is already the default
214
+ --debug Verbose logs + pipe daemon output to ~/.seeflow/seeflow.log
93
215
 
94
216
  Options (register):
95
- --path <dir> Path to repo root (default: current directory)
96
- --flow <file> Path to flow JSON, relative to repo root
97
- (default: .seeflow/flow.json)
98
- --no-start Fail if studio is not already running
217
+ --path <dir> Path to repo root (default: current directory)
218
+ --flow <file> Path to flow JSON, relative to repo root
219
+ (default: .seeflow/flow.json)
99
220
 
100
221
  Examples:
101
222
  npx -y @tuongaz/seeflow@latest
102
223
  npx -y @tuongaz/seeflow@latest --port 8080
103
224
  npx -y @tuongaz/seeflow@latest start --foreground
104
225
  npx -y @tuongaz/seeflow@latest register --path ./my-app
226
+ npx -y @tuongaz/seeflow@latest projects:create --name "Checkout"
227
+ npx -y @tuongaz/seeflow@latest flows:list
105
228
  npx -y @tuongaz/seeflow@latest stop
106
229
  `.trim(),
107
230
  );
@@ -448,3 +571,234 @@ async function waitForHealth(url: string, timeoutMs: number): Promise<boolean> {
448
571
  }
449
572
  return false;
450
573
  }
574
+
575
+ // ---- HTTP-passthrough subcommands ----------------------------------------
576
+
577
+ async function runProjectsCreate() {
578
+ const name = flagValue('name');
579
+ if (!name) printError('Missing required flag: --name');
580
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
581
+ const res = await postJson(`${url}/api/projects`, { name });
582
+ const body = (await handleResponse(res)) as object;
583
+ printOk(body);
584
+ }
585
+
586
+ async function runFlowsList() {
587
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
588
+ const res = await fetch(`${url}/api/flows`);
589
+ const body = (await handleResponse(res)) as unknown[];
590
+ printOk({ flows: body });
591
+ }
592
+
593
+ async function runFlowsGet() {
594
+ const flowId = requireArg(1, '<flowId>');
595
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
596
+ const res = await fetch(`${url}/api/flows/${encodeURIComponent(flowId)}`);
597
+ const body = (await handleResponse(res)) as object;
598
+ printOk(body);
599
+ }
600
+
601
+ async function runFlowsDelete() {
602
+ const flowId = requireArg(1, '<flowId>');
603
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
604
+ const res = await deleteRequest(`${url}/api/flows/${encodeURIComponent(flowId)}`);
605
+ const body = (await handleResponse(res)) as object;
606
+ printOk(body);
607
+ }
608
+
609
+ async function runFlowsLayout() {
610
+ const flowId = requireArg(1, '<flowId>');
611
+ const body = await bodyFromFlags();
612
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
613
+ const res = await postJson(`${url}/api/flows/${encodeURIComponent(flowId)}/layout`, body);
614
+ const out = (await handleResponse(res)) as object;
615
+ printOk(out);
616
+ }
617
+
618
+ async function runFlowsPlay() {
619
+ const flowId = requireArg(1, '<flowId>');
620
+ const nodeId = requireArg(2, '<nodeId>');
621
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
622
+ const res = await postJson(
623
+ `${url}/api/flows/${encodeURIComponent(flowId)}/play/${encodeURIComponent(nodeId)}`,
624
+ {},
625
+ );
626
+ const out = (await handleResponse(res)) as object;
627
+ printOk(out);
628
+ }
629
+
630
+ async function runNodesAdd() {
631
+ const flowId = requireArg(1, '<flowId>');
632
+ const body = await bodyFromFlags();
633
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
634
+ const res = await postJson(`${url}/api/flows/${encodeURIComponent(flowId)}/nodes`, body);
635
+ const out = (await handleResponse(res)) as object;
636
+ printOk(out);
637
+ }
638
+
639
+ async function runNodesAddBulk() {
640
+ const flowId = requireArg(1, '<flowId>');
641
+ const body = await bodyFromFlags();
642
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
643
+ const res = await postJson(`${url}/api/flows/${encodeURIComponent(flowId)}/nodes/bulk`, body);
644
+ const out = (await handleResponse(res)) as object;
645
+ printOk(out);
646
+ }
647
+
648
+ async function runNodesPatch() {
649
+ const flowId = requireArg(1, '<flowId>');
650
+ const nodeId = requireArg(2, '<nodeId>');
651
+ const body = await bodyFromFlags();
652
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
653
+ const res = await patchJson(
654
+ `${url}/api/flows/${encodeURIComponent(flowId)}/nodes/${encodeURIComponent(nodeId)}`,
655
+ body,
656
+ );
657
+ const out = (await handleResponse(res)) as object;
658
+ printOk(out);
659
+ }
660
+
661
+ async function runNodesMove() {
662
+ const flowId = requireArg(1, '<flowId>');
663
+ const nodeId = requireArg(2, '<nodeId>');
664
+ const xRaw = flagValue('x');
665
+ const yRaw = flagValue('y');
666
+ if (xRaw === undefined || yRaw === undefined) {
667
+ printError('nodes:move requires --x <n> --y <n>');
668
+ }
669
+ const x = Number(xRaw);
670
+ const y = Number(yRaw);
671
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
672
+ printError('--x and --y must be finite numbers');
673
+ }
674
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
675
+ const res = await patchJson(
676
+ `${url}/api/flows/${encodeURIComponent(flowId)}/nodes/${encodeURIComponent(nodeId)}/position`,
677
+ { x, y },
678
+ );
679
+ const out = (await handleResponse(res)) as object;
680
+ printOk(out);
681
+ }
682
+
683
+ async function runNodesReorder() {
684
+ const flowId = requireArg(1, '<flowId>');
685
+ const nodeId = requireArg(2, '<nodeId>');
686
+ const op = flagValue('op');
687
+ if (!op) printError('nodes:reorder requires --op forward|backward|toFront|toBack|toIndex');
688
+ let body: Record<string, unknown> = { op };
689
+ if (op === 'toIndex') {
690
+ const idxRaw = flagValue('index');
691
+ if (idxRaw === undefined) printError('nodes:reorder --op toIndex requires --index <n>');
692
+ const index = Number(idxRaw);
693
+ if (!Number.isInteger(index) || index < 0) {
694
+ printError('--index must be a non-negative integer');
695
+ }
696
+ body = { op, index };
697
+ }
698
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
699
+ const res = await patchJson(
700
+ `${url}/api/flows/${encodeURIComponent(flowId)}/nodes/${encodeURIComponent(nodeId)}/order`,
701
+ body,
702
+ );
703
+ const out = (await handleResponse(res)) as object;
704
+ printOk(out);
705
+ }
706
+
707
+ async function runNodesDelete() {
708
+ const flowId = requireArg(1, '<flowId>');
709
+ const nodeId = requireArg(2, '<nodeId>');
710
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
711
+ const res = await deleteRequest(
712
+ `${url}/api/flows/${encodeURIComponent(flowId)}/nodes/${encodeURIComponent(nodeId)}`,
713
+ );
714
+ const out = (await handleResponse(res)) as object;
715
+ printOk(out);
716
+ }
717
+
718
+ async function runConnectorsAdd() {
719
+ const flowId = requireArg(1, '<flowId>');
720
+ const body = await bodyFromFlags();
721
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
722
+ const res = await postJson(`${url}/api/flows/${encodeURIComponent(flowId)}/connectors`, body);
723
+ const out = (await handleResponse(res)) as object;
724
+ printOk(out);
725
+ }
726
+
727
+ async function runConnectorsAddBulk() {
728
+ const flowId = requireArg(1, '<flowId>');
729
+ const body = await bodyFromFlags();
730
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
731
+ const res = await postJson(
732
+ `${url}/api/flows/${encodeURIComponent(flowId)}/connectors/bulk`,
733
+ body,
734
+ );
735
+ const out = (await handleResponse(res)) as object;
736
+ printOk(out);
737
+ }
738
+
739
+ async function runConnectorsPatch() {
740
+ const flowId = requireArg(1, '<flowId>');
741
+ const connId = requireArg(2, '<connectorId>');
742
+ const body = await bodyFromFlags();
743
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
744
+ const res = await patchJson(
745
+ `${url}/api/flows/${encodeURIComponent(flowId)}/connectors/${encodeURIComponent(connId)}`,
746
+ body,
747
+ );
748
+ const out = (await handleResponse(res)) as object;
749
+ printOk(out);
750
+ }
751
+
752
+ async function runConnectorsDelete() {
753
+ const flowId = requireArg(1, '<flowId>');
754
+ const connId = requireArg(2, '<connectorId>');
755
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
756
+ const res = await deleteRequest(
757
+ `${url}/api/flows/${encodeURIComponent(flowId)}/connectors/${encodeURIComponent(connId)}`,
758
+ );
759
+ const out = (await handleResponse(res)) as object;
760
+ printOk(out);
761
+ }
762
+
763
+ async function runValidate() {
764
+ const file = flagValue('file');
765
+ const styleFile = flagValue('style');
766
+ if (!file) printError('Missing required flag: --file <flow.json>');
767
+ let flow: unknown;
768
+ try {
769
+ flow = JSON.parse(readFileSync(file as string, 'utf8'));
770
+ } catch (err) {
771
+ printError(`Failed to read ${file}: ${err instanceof Error ? err.message : String(err)}`);
772
+ }
773
+ let style: unknown;
774
+ if (styleFile) {
775
+ try {
776
+ style = JSON.parse(readFileSync(styleFile, 'utf8'));
777
+ } catch (err) {
778
+ printError(
779
+ `Failed to read ${styleFile}: ${err instanceof Error ? err.message : String(err)}`,
780
+ );
781
+ }
782
+ }
783
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
784
+ const res = await postJson(`${url}/api/validate`, { flow, style });
785
+ const body = (await handleResponse(res)) as { ok?: boolean; issues?: unknown[] };
786
+ if (body.ok === false) {
787
+ printError(`Schema validation failed: ${JSON.stringify(body.issues ?? [])}`);
788
+ }
789
+ printOk(body);
790
+ }
791
+
792
+ async function runE2e() {
793
+ const flowId = requireArg(1, '<flowId>');
794
+ const skipNodesRaw = flagValue('skip-nodes');
795
+ const skipNodes = skipNodesRaw ? skipNodesRaw.split(',').filter(Boolean) : [];
796
+ const { url } = await studioUrlOrDie(hasFlag('no-start'));
797
+ const { validateEndToEnd } = await import('./cli-e2e.ts');
798
+ const report = await validateEndToEnd({ flowId, url, skipNodes });
799
+ if (!report.ok) {
800
+ process.stderr.write(`${JSON.stringify(report)}\n`);
801
+ process.exit(1);
802
+ }
803
+ printOk(report);
804
+ }