@tuongaz/seeflow 0.1.42 → 0.1.51
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 +14 -0
- package/dist/web/assets/{index-BPUoNIBm.js → index-CFn1Jdmi.js} +17 -17
- package/dist/web/assets/{index-BlkUOp7f.css → index-DSfixlbD.css} +1 -1
- package/dist/web/assets/{index.es-mje3R_63.js → index.es-DQFAA-Eu.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-DX3imOs2.js → jspdf.es.min-D7KeFi-m.js} +3 -3
- package/dist/web/index.html +2 -2
- package/examples/ecommerce-platform/.seeflow/flow.json +8 -8
- package/examples/order-pipeline/.seeflow/flow.json +4 -4
- package/package.json +2 -1
- package/src/api.ts +138 -102
- package/src/cli-e2e.ts +10 -6
- package/src/cli-helpers.ts +79 -0
- package/src/cli-manifest.ts +772 -0
- package/src/cli-ops.ts +18 -0
- package/src/cli.ts +164 -137
- package/src/events.ts +2 -1
- package/src/file-ref.ts +27 -16
- package/src/mcp.ts +104 -35
- package/src/merge.ts +3 -0
- package/src/node-files.ts +5 -2
- package/src/operations.ts +341 -7
- package/src/registry-watcher.ts +86 -0
- package/src/registry.ts +132 -24
- package/src/schema.ts +2 -0
- package/src/server.ts +9 -0
- package/src/watcher.ts +32 -2
package/src/cli-ops.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Operations, createOperations } from './operations.ts';
|
|
2
|
+
import { createRegistry } from './registry.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a single Operations handle for in-process CLI use.
|
|
6
|
+
*
|
|
7
|
+
* The CLI has no watcher and no statusRunner — play/reset are server-only
|
|
8
|
+
* features that still go via HTTP. When a CLI mutates a flow file, the
|
|
9
|
+
* running studio's flow watcher picks up the disk write externally and
|
|
10
|
+
* broadcasts flow:reload to connected SPA clients.
|
|
11
|
+
*
|
|
12
|
+
* `OperationsDeps` doesn't currently carry an EventBus, so we don't pass one;
|
|
13
|
+
* the *Impl functions only broadcast through `watcher.broadcastReload`, which
|
|
14
|
+
* is undefined in the CLI.
|
|
15
|
+
*/
|
|
16
|
+
export function createCliOperations(): Operations {
|
|
17
|
+
return createOperations({ registry: createRegistry() });
|
|
18
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,8 +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 } from './cli-helpers.ts';
|
|
4
|
+
import { drainStdin, loadBody, printError, printOk, printOutcome } from './cli-helpers.ts';
|
|
5
|
+
import { COMMAND_MANIFEST, renderCommandHelp, renderCommandList } from './cli-manifest.ts';
|
|
6
|
+
import { createCliOperations } from './cli-ops.ts';
|
|
5
7
|
import { createEventBus } from './events.ts';
|
|
8
|
+
import type { LayoutOptions } from './layout.ts';
|
|
9
|
+
import {
|
|
10
|
+
ConnectorPatchBodySchema,
|
|
11
|
+
ConnectorsBulkBodySchema,
|
|
12
|
+
NodePatchBodySchema,
|
|
13
|
+
NodesBulkBodySchema,
|
|
14
|
+
ReorderBodySchema,
|
|
15
|
+
} from './operations.ts';
|
|
6
16
|
import { seeflowHome } from './paths.ts';
|
|
7
17
|
import { defaultProcessSpawner } from './process-spawner.ts';
|
|
8
18
|
import { type Registry, createRegistry } from './registry.ts';
|
|
@@ -72,18 +82,6 @@ async function postJson(url: string, body: unknown): Promise<Response> {
|
|
|
72
82
|
});
|
|
73
83
|
}
|
|
74
84
|
|
|
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
85
|
async function handleResponse(res: Response): Promise<unknown> {
|
|
88
86
|
const text = await res.text();
|
|
89
87
|
let parsed: unknown = text;
|
|
@@ -111,7 +109,7 @@ const daemonLogPath = () => join(seeflowHome(), 'seeflow.log');
|
|
|
111
109
|
if (argv.includes('--version') || argv.includes('-v')) {
|
|
112
110
|
await printVersion();
|
|
113
111
|
} else if (sub === 'help' || sub === '--help' || sub === '-h') {
|
|
114
|
-
|
|
112
|
+
await runHelp();
|
|
115
113
|
} else if (sub === 'version') {
|
|
116
114
|
await printVersion();
|
|
117
115
|
} else if (!sub || sub === 'start' || sub.startsWith('-')) {
|
|
@@ -126,8 +124,12 @@ if (argv.includes('--version') || argv.includes('-v')) {
|
|
|
126
124
|
await runProjectsCreate();
|
|
127
125
|
} else if (sub === 'flows:list') {
|
|
128
126
|
await runFlowsList();
|
|
127
|
+
} else if (sub === 'flows:summary') {
|
|
128
|
+
await runFlowsSummary();
|
|
129
129
|
} else if (sub === 'flows:get') {
|
|
130
130
|
await runFlowsGet();
|
|
131
|
+
} else if (sub === 'flows:graph') {
|
|
132
|
+
await runFlowsGraph();
|
|
131
133
|
} else if (sub === 'flows:delete') {
|
|
132
134
|
await runFlowsDelete();
|
|
133
135
|
} else if (sub === 'flows:layout') {
|
|
@@ -138,6 +140,8 @@ if (argv.includes('--version') || argv.includes('-v')) {
|
|
|
138
140
|
await runNodesAdd();
|
|
139
141
|
} else if (sub === 'nodes:add-bulk') {
|
|
140
142
|
await runNodesAddBulk();
|
|
143
|
+
} else if (sub === 'nodes:get') {
|
|
144
|
+
await runNodesGet();
|
|
141
145
|
} else if (sub === 'nodes:patch') {
|
|
142
146
|
await runNodesPatch();
|
|
143
147
|
} else if (sub === 'nodes:move') {
|
|
@@ -172,19 +176,21 @@ seeflow — local studio for file-defined interactive demos
|
|
|
172
176
|
Usage:
|
|
173
177
|
npx -y @tuongaz/seeflow@latest [command] [options]
|
|
174
178
|
|
|
175
|
-
Commands:
|
|
179
|
+
Commands (work without a running studio):
|
|
176
180
|
start Start the SeeFlow Studio server (default port 4321) — default when no command is given
|
|
177
181
|
stop Stop a background studio instance
|
|
178
|
-
register Register a demo repo
|
|
179
|
-
flows:register Register a demo repo
|
|
182
|
+
register Register a demo repo, writing to ~/.seeflow/registry.json (alias of flows:register)
|
|
183
|
+
flows:register Register a demo repo
|
|
180
184
|
projects:create Create a new project (--name <name>)
|
|
181
185
|
flows:list List registered flows
|
|
186
|
+
flows:summary List registered flows (id + name + description only)
|
|
182
187
|
flows:get <id> Get flow details
|
|
188
|
+
flows:graph <id> List nodes + connectors without inlined file content
|
|
183
189
|
flows:delete <id> Unregister a flow
|
|
184
|
-
flows:layout <id>
|
|
185
|
-
flows:play <id> <n> Trigger a play on node <n>
|
|
190
|
+
flows:layout <id> Apply ELK layout, writing style.json (--json/--file/--stdin optional)
|
|
186
191
|
nodes:add <id> Add a node (--json/--file/--stdin)
|
|
187
192
|
nodes:add-bulk <id> Add many nodes (--json/--file/--stdin)
|
|
193
|
+
nodes:get <id> <n> Get a node with detail / html content inlined
|
|
188
194
|
nodes:patch <id> <n> Patch a node (--json/--file/--stdin)
|
|
189
195
|
nodes:move <id> <n> Move a node (--x N --y N)
|
|
190
196
|
nodes:reorder <id> <n> Reorder a node (--op forward|backward|toFront|toBack|toIndex [--index N])
|
|
@@ -194,13 +200,18 @@ Commands:
|
|
|
194
200
|
connectors:patch <id> <connId> Patch a connector (--json/--file/--stdin)
|
|
195
201
|
connectors:delete <id> <connId> Delete a connector
|
|
196
202
|
validate Schema-validate a flow.json (--file <file> [--style <file>])
|
|
203
|
+
|
|
204
|
+
Commands (require a running studio):
|
|
205
|
+
flows:play <id> <n> Trigger a play on node <n>
|
|
197
206
|
e2e <id> End-to-end validate a registered flow (--skip-nodes a,b)
|
|
207
|
+
|
|
208
|
+
Meta:
|
|
198
209
|
version Print the CLI version
|
|
199
210
|
help Show this help message
|
|
200
211
|
|
|
201
212
|
Global options:
|
|
202
213
|
--version, -v Print the CLI version and exit
|
|
203
|
-
--no-start
|
|
214
|
+
--no-start For flows:play / e2e: fail instead of auto-starting the studio
|
|
204
215
|
|
|
205
216
|
Body source flags (where applicable):
|
|
206
217
|
--json '<JSON>' Inline JSON body
|
|
@@ -230,6 +241,19 @@ Examples:
|
|
|
230
241
|
);
|
|
231
242
|
}
|
|
232
243
|
|
|
244
|
+
async function runHelp() {
|
|
245
|
+
const target = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined;
|
|
246
|
+
if (target) {
|
|
247
|
+
try {
|
|
248
|
+
console.log(renderCommandHelp(target));
|
|
249
|
+
} catch (err) {
|
|
250
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
console.log(renderCommandList());
|
|
255
|
+
}
|
|
256
|
+
|
|
233
257
|
async function runStart() {
|
|
234
258
|
const config = readConfig();
|
|
235
259
|
const portArg = flagValue('port');
|
|
@@ -458,10 +482,6 @@ function isEsrch(err: unknown): boolean {
|
|
|
458
482
|
async function runRegister() {
|
|
459
483
|
const repoPath = resolve(flagValue('path') ?? '.');
|
|
460
484
|
const demoPathArg = flagValue('flow') ?? DEFAULT_FLOW_PATH;
|
|
461
|
-
const noStart = hasFlag('no-start');
|
|
462
|
-
const config = readConfig();
|
|
463
|
-
const overrideUrl = process.env.SEEFLOW_STUDIO_URL?.replace(/\/+$/, '');
|
|
464
|
-
const url = overrideUrl ?? studioUrl(config);
|
|
465
485
|
|
|
466
486
|
const fullPath = isAbsolute(demoPathArg) ? demoPathArg : join(repoPath, demoPathArg);
|
|
467
487
|
if (!existsSync(fullPath)) {
|
|
@@ -487,38 +507,28 @@ async function runRegister() {
|
|
|
487
507
|
process.exit(1);
|
|
488
508
|
}
|
|
489
509
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
name: parsed.data.name,
|
|
499
|
-
repoPath,
|
|
500
|
-
flowPath: demoPathArg,
|
|
501
|
-
}),
|
|
502
|
-
});
|
|
503
|
-
} catch (err) {
|
|
504
|
-
console.error(`Could not reach studio at ${url}: ${String(err)}`);
|
|
505
|
-
console.error('Start it first: seeflow start');
|
|
506
|
-
process.exit(1);
|
|
510
|
+
const ops = createCliOperations();
|
|
511
|
+
const result = await ops.registerFlow({
|
|
512
|
+
name: parsed.data.name,
|
|
513
|
+
repoPath,
|
|
514
|
+
flowPath: demoPathArg,
|
|
515
|
+
});
|
|
516
|
+
if (result.kind !== 'ok') {
|
|
517
|
+
printOutcome(result);
|
|
507
518
|
}
|
|
519
|
+
const body = result.data;
|
|
508
520
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
521
|
+
// Show the studio URL only when a daemon is actually running.
|
|
522
|
+
const pid = readPid();
|
|
523
|
+
if (pid && isPidAlive(pid)) {
|
|
524
|
+
const config = readConfig();
|
|
525
|
+
const overrideUrl = process.env.SEEFLOW_STUDIO_URL?.replace(/\/+$/, '');
|
|
526
|
+
const url = overrideUrl ?? studioUrl(config);
|
|
527
|
+
console.log(`Registered "${parsed.data.name}" → ${url}/d/${body.slug}`);
|
|
528
|
+
} else {
|
|
529
|
+
console.log(`Registered "${parsed.data.name}" (slug: ${body.slug})`);
|
|
513
530
|
}
|
|
514
531
|
|
|
515
|
-
const body = (await res.json()) as {
|
|
516
|
-
id: string;
|
|
517
|
-
slug: string;
|
|
518
|
-
sdk?: { outcome: 'written' | 'present' | 'skipped'; filePath: string | null };
|
|
519
|
-
};
|
|
520
|
-
console.log(`Registered "${parsed.data.name}" → ${url}/d/${body.slug}`);
|
|
521
|
-
|
|
522
532
|
if (body.sdk?.outcome === 'written') {
|
|
523
533
|
console.log(`Wrote ${body.sdk.filePath} (event-bound state node detected)`);
|
|
524
534
|
} else if (body.sdk?.outcome === 'present') {
|
|
@@ -577,42 +587,55 @@ async function waitForHealth(url: string, timeoutMs: number): Promise<boolean> {
|
|
|
577
587
|
async function runProjectsCreate() {
|
|
578
588
|
const name = flagValue('name');
|
|
579
589
|
if (!name) printError('Missing required flag: --name');
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
printOk(body);
|
|
590
|
+
const ops = createCliOperations();
|
|
591
|
+
const result = await ops.createProject({ name: name as string });
|
|
592
|
+
printOutcome(result);
|
|
584
593
|
}
|
|
585
594
|
|
|
586
595
|
async function runFlowsList() {
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
596
|
+
const ops = createCliOperations();
|
|
597
|
+
const result = ops.listFlows();
|
|
598
|
+
printOk({ flows: result.data });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function runFlowsSummary() {
|
|
602
|
+
const ops = createCliOperations();
|
|
603
|
+
const result = ops.listFlowsSummary();
|
|
604
|
+
printOk({ flows: result.data });
|
|
591
605
|
}
|
|
592
606
|
|
|
593
607
|
async function runFlowsGet() {
|
|
594
608
|
const flowId = requireArg(1, '<flowId>');
|
|
595
|
-
const
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
609
|
+
const ops = createCliOperations();
|
|
610
|
+
const result = await ops.getFlow(flowId);
|
|
611
|
+
printOutcome(result);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function runFlowsGraph() {
|
|
615
|
+
const flowId = requireArg(1, '<flowId>');
|
|
616
|
+
const ops = createCliOperations();
|
|
617
|
+
const result = await ops.getFlowGraph(flowId);
|
|
618
|
+
printOutcome(result);
|
|
599
619
|
}
|
|
600
620
|
|
|
601
621
|
async function runFlowsDelete() {
|
|
602
622
|
const flowId = requireArg(1, '<flowId>');
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
printOk(body);
|
|
623
|
+
const ops = createCliOperations();
|
|
624
|
+
const result = ops.deleteFlow(flowId);
|
|
625
|
+
printOutcome(result);
|
|
607
626
|
}
|
|
608
627
|
|
|
609
628
|
async function runFlowsLayout() {
|
|
610
629
|
const flowId = requireArg(1, '<flowId>');
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
630
|
+
// Body is optional — `{ options? }` shape if provided. Empty when omitted.
|
|
631
|
+
let options: LayoutOptions | undefined;
|
|
632
|
+
if (hasFlag('json') || hasFlag('file') || hasFlag('stdin')) {
|
|
633
|
+
const body = (await bodyFromFlags()) as { options?: LayoutOptions } | undefined;
|
|
634
|
+
options = body?.options;
|
|
635
|
+
}
|
|
636
|
+
const ops = createCliOperations();
|
|
637
|
+
const result = await ops.applyLayout(flowId, options);
|
|
638
|
+
printOutcome(result);
|
|
616
639
|
}
|
|
617
640
|
|
|
618
641
|
async function runFlowsPlay() {
|
|
@@ -630,32 +653,45 @@ async function runFlowsPlay() {
|
|
|
630
653
|
async function runNodesAdd() {
|
|
631
654
|
const flowId = requireArg(1, '<flowId>');
|
|
632
655
|
const body = await bodyFromFlags();
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
656
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
657
|
+
printError('Body must be an object');
|
|
658
|
+
}
|
|
659
|
+
const ops = createCliOperations();
|
|
660
|
+
const result = await ops.addNode(flowId, body as Record<string, unknown>);
|
|
661
|
+
printOutcome(result);
|
|
637
662
|
}
|
|
638
663
|
|
|
639
664
|
async function runNodesAddBulk() {
|
|
640
665
|
const flowId = requireArg(1, '<flowId>');
|
|
641
666
|
const body = await bodyFromFlags();
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
667
|
+
const parsed = NodesBulkBodySchema.safeParse(body);
|
|
668
|
+
if (!parsed.success) {
|
|
669
|
+
printError(`Invalid nodes:add-bulk body: ${JSON.stringify(parsed.error.issues)}`);
|
|
670
|
+
}
|
|
671
|
+
const ops = createCliOperations();
|
|
672
|
+
const result = await ops.addNodesBulk(flowId, parsed.data);
|
|
673
|
+
printOutcome(result);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function runNodesGet() {
|
|
677
|
+
const flowId = requireArg(1, '<flowId>');
|
|
678
|
+
const nodeId = requireArg(2, '<nodeId>');
|
|
679
|
+
const ops = createCliOperations();
|
|
680
|
+
const result = await ops.getNode(flowId, nodeId);
|
|
681
|
+
printOutcome(result);
|
|
646
682
|
}
|
|
647
683
|
|
|
648
684
|
async function runNodesPatch() {
|
|
649
685
|
const flowId = requireArg(1, '<flowId>');
|
|
650
686
|
const nodeId = requireArg(2, '<nodeId>');
|
|
651
687
|
const body = await bodyFromFlags();
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
);
|
|
657
|
-
const
|
|
658
|
-
|
|
688
|
+
const parsed = NodePatchBodySchema.safeParse(body);
|
|
689
|
+
if (!parsed.success) {
|
|
690
|
+
printError(`Invalid nodes:patch body: ${JSON.stringify(parsed.error.issues)}`);
|
|
691
|
+
}
|
|
692
|
+
const ops = createCliOperations();
|
|
693
|
+
const result = await ops.patchNode(flowId, nodeId, parsed.data);
|
|
694
|
+
printOutcome(result);
|
|
659
695
|
}
|
|
660
696
|
|
|
661
697
|
async function runNodesMove() {
|
|
@@ -671,13 +707,9 @@ async function runNodesMove() {
|
|
|
671
707
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
672
708
|
printError('--x and --y must be finite numbers');
|
|
673
709
|
}
|
|
674
|
-
const
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
{ x, y },
|
|
678
|
-
);
|
|
679
|
-
const out = (await handleResponse(res)) as object;
|
|
680
|
-
printOk(out);
|
|
710
|
+
const ops = createCliOperations();
|
|
711
|
+
const result = await ops.moveNode(flowId, nodeId, { x, y });
|
|
712
|
+
printOutcome(result);
|
|
681
713
|
}
|
|
682
714
|
|
|
683
715
|
async function runNodesReorder() {
|
|
@@ -685,7 +717,7 @@ async function runNodesReorder() {
|
|
|
685
717
|
const nodeId = requireArg(2, '<nodeId>');
|
|
686
718
|
const op = flagValue('op');
|
|
687
719
|
if (!op) printError('nodes:reorder requires --op forward|backward|toFront|toBack|toIndex');
|
|
688
|
-
let
|
|
720
|
+
let raw: Record<string, unknown> = { op };
|
|
689
721
|
if (op === 'toIndex') {
|
|
690
722
|
const idxRaw = flagValue('index');
|
|
691
723
|
if (idxRaw === undefined) printError('nodes:reorder --op toIndex requires --index <n>');
|
|
@@ -693,71 +725,67 @@ async function runNodesReorder() {
|
|
|
693
725
|
if (!Number.isInteger(index) || index < 0) {
|
|
694
726
|
printError('--index must be a non-negative integer');
|
|
695
727
|
}
|
|
696
|
-
|
|
728
|
+
raw = { op, index };
|
|
697
729
|
}
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
);
|
|
703
|
-
const
|
|
704
|
-
|
|
730
|
+
const parsed = ReorderBodySchema.safeParse(raw);
|
|
731
|
+
if (!parsed.success) {
|
|
732
|
+
printError(`Invalid nodes:reorder body: ${JSON.stringify(parsed.error.issues)}`);
|
|
733
|
+
}
|
|
734
|
+
const ops = createCliOperations();
|
|
735
|
+
const result = await ops.reorderNode(flowId, nodeId, parsed.data);
|
|
736
|
+
printOutcome(result);
|
|
705
737
|
}
|
|
706
738
|
|
|
707
739
|
async function runNodesDelete() {
|
|
708
740
|
const flowId = requireArg(1, '<flowId>');
|
|
709
741
|
const nodeId = requireArg(2, '<nodeId>');
|
|
710
|
-
const
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
);
|
|
714
|
-
const out = (await handleResponse(res)) as object;
|
|
715
|
-
printOk(out);
|
|
742
|
+
const ops = createCliOperations();
|
|
743
|
+
const result = await ops.deleteNode(flowId, nodeId);
|
|
744
|
+
printOutcome(result);
|
|
716
745
|
}
|
|
717
746
|
|
|
718
747
|
async function runConnectorsAdd() {
|
|
719
748
|
const flowId = requireArg(1, '<flowId>');
|
|
720
749
|
const body = await bodyFromFlags();
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
750
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
751
|
+
printError('Body must be an object');
|
|
752
|
+
}
|
|
753
|
+
const ops = createCliOperations();
|
|
754
|
+
const result = await ops.addConnector(flowId, body as Record<string, unknown>);
|
|
755
|
+
printOutcome(result);
|
|
725
756
|
}
|
|
726
757
|
|
|
727
758
|
async function runConnectorsAddBulk() {
|
|
728
759
|
const flowId = requireArg(1, '<flowId>');
|
|
729
760
|
const body = await bodyFromFlags();
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
);
|
|
735
|
-
const
|
|
736
|
-
|
|
761
|
+
const parsed = ConnectorsBulkBodySchema.safeParse(body);
|
|
762
|
+
if (!parsed.success) {
|
|
763
|
+
printError(`Invalid connectors:add-bulk body: ${JSON.stringify(parsed.error.issues)}`);
|
|
764
|
+
}
|
|
765
|
+
const ops = createCliOperations();
|
|
766
|
+
const result = await ops.addConnectorsBulk(flowId, parsed.data);
|
|
767
|
+
printOutcome(result);
|
|
737
768
|
}
|
|
738
769
|
|
|
739
770
|
async function runConnectorsPatch() {
|
|
740
771
|
const flowId = requireArg(1, '<flowId>');
|
|
741
772
|
const connId = requireArg(2, '<connectorId>');
|
|
742
773
|
const body = await bodyFromFlags();
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
);
|
|
748
|
-
const
|
|
749
|
-
|
|
774
|
+
const parsed = ConnectorPatchBodySchema.safeParse(body);
|
|
775
|
+
if (!parsed.success) {
|
|
776
|
+
printError(`Invalid connectors:patch body: ${JSON.stringify(parsed.error.issues)}`);
|
|
777
|
+
}
|
|
778
|
+
const ops = createCliOperations();
|
|
779
|
+
const result = await ops.patchConnector(flowId, connId, parsed.data);
|
|
780
|
+
printOutcome(result);
|
|
750
781
|
}
|
|
751
782
|
|
|
752
783
|
async function runConnectorsDelete() {
|
|
753
784
|
const flowId = requireArg(1, '<flowId>');
|
|
754
785
|
const connId = requireArg(2, '<connectorId>');
|
|
755
|
-
const
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
);
|
|
759
|
-
const out = (await handleResponse(res)) as object;
|
|
760
|
-
printOk(out);
|
|
786
|
+
const ops = createCliOperations();
|
|
787
|
+
const result = await ops.deleteConnector(flowId, connId);
|
|
788
|
+
printOutcome(result);
|
|
761
789
|
}
|
|
762
790
|
|
|
763
791
|
async function runValidate() {
|
|
@@ -780,9 +808,8 @@ async function runValidate() {
|
|
|
780
808
|
);
|
|
781
809
|
}
|
|
782
810
|
}
|
|
783
|
-
const
|
|
784
|
-
const
|
|
785
|
-
const body = (await handleResponse(res)) as { ok?: boolean; issues?: unknown[] };
|
|
811
|
+
const ops = createCliOperations();
|
|
812
|
+
const body = ops.validate({ flow, style });
|
|
786
813
|
if (body.ok === false) {
|
|
787
814
|
printError(`Schema validation failed: ${JSON.stringify(body.issues ?? [])}`);
|
|
788
815
|
}
|
package/src/events.ts
CHANGED
package/src/file-ref.ts
CHANGED
|
@@ -14,13 +14,18 @@ const isCleanRelativePath = (p: string): boolean => {
|
|
|
14
14
|
const invalidMarker = (rawPath: string) => `[seeflow: invalid file:// path '${rawPath}']`;
|
|
15
15
|
const missingMarker = (rawPath: string) => `[seeflow: missing file '${rawPath}']`;
|
|
16
16
|
|
|
17
|
+
const looksLikeFlowNode = (obj: Record<string, unknown>): obj is { id: string; data: object } =>
|
|
18
|
+
typeof obj.id === 'string' && obj.data !== null && typeof obj.data === 'object';
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* Resolve every `file://<relative-path>` string in `raw` by reading the file
|
|
19
|
-
* under `<seeflowRoot
|
|
20
|
-
*
|
|
22
|
+
* under `<seeflowRoot>/nodes/<nodeId>/` (node-relative) and substituting its
|
|
23
|
+
* UTF-8 content. Strings outside any enclosing flow node are treated as
|
|
24
|
+
* invalid — every supported file:// ref currently lives inside `node.data`.
|
|
21
25
|
*
|
|
22
|
-
* Returns the mutated tree plus the sorted, de-duplicated list of relative
|
|
23
|
-
* paths that resolved cleanly (the watcher tracks these for live reload
|
|
26
|
+
* Returns the mutated tree plus the sorted, de-duplicated list of seeflow-root-relative
|
|
27
|
+
* paths that resolved cleanly (the watcher tracks these for live reload, so the
|
|
28
|
+
* external contract uses `nodes/<id>/<file>` even though the source string is short).
|
|
24
29
|
*/
|
|
25
30
|
export function resolveFileRefs(
|
|
26
31
|
raw: unknown,
|
|
@@ -34,46 +39,52 @@ export function resolveFileRefs(
|
|
|
34
39
|
seeflowRealRoot = seeflowRoot;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
const resolveString = (s: string): string => {
|
|
42
|
+
const resolveString = (s: string, nodeId: string | null): string => {
|
|
38
43
|
if (!s.startsWith(FILE_PREFIX)) return s;
|
|
39
44
|
const relPath = s.slice(FILE_PREFIX.length);
|
|
40
45
|
if (!isCleanRelativePath(relPath)) return invalidMarker(relPath);
|
|
46
|
+
if (nodeId === null) return invalidMarker(relPath);
|
|
41
47
|
|
|
42
|
-
const
|
|
43
|
-
|
|
48
|
+
const seeflowRelPath = `nodes/${nodeId}/${relPath}`;
|
|
49
|
+
const abs = join(seeflowRoot, seeflowRelPath);
|
|
50
|
+
if (!existsSync(abs)) return missingMarker(seeflowRelPath);
|
|
44
51
|
|
|
45
52
|
// Symlink-escape defense: resolve realpath and confirm it stays inside root.
|
|
46
53
|
let realAbs: string;
|
|
47
54
|
try {
|
|
48
55
|
realAbs = realpathSync(abs);
|
|
49
56
|
} catch {
|
|
50
|
-
return missingMarker(
|
|
57
|
+
return missingMarker(seeflowRelPath);
|
|
51
58
|
}
|
|
52
59
|
const rel = relative(seeflowRealRoot, realAbs);
|
|
53
60
|
if (rel.startsWith('..') || isAbsolute(rel)) return invalidMarker(relPath);
|
|
54
61
|
|
|
55
62
|
try {
|
|
56
63
|
const content = readFileSync(realAbs, 'utf8');
|
|
57
|
-
refs.add(
|
|
64
|
+
refs.add(seeflowRelPath);
|
|
58
65
|
return content;
|
|
59
66
|
} catch {
|
|
60
|
-
return missingMarker(
|
|
67
|
+
return missingMarker(seeflowRelPath);
|
|
61
68
|
}
|
|
62
69
|
};
|
|
63
70
|
|
|
64
|
-
const walk = (node: unknown): unknown => {
|
|
65
|
-
if (typeof node === 'string') return resolveString(node);
|
|
66
|
-
if (Array.isArray(node)) return node.map(walk);
|
|
71
|
+
const walk = (node: unknown, nodeId: string | null): unknown => {
|
|
72
|
+
if (typeof node === 'string') return resolveString(node, nodeId);
|
|
73
|
+
if (Array.isArray(node)) return node.map((v) => walk(v, nodeId));
|
|
67
74
|
if (node && typeof node === 'object') {
|
|
75
|
+
const obj = node as Record<string, unknown>;
|
|
76
|
+
// Entering a flow node carves out a new resolution context for its subtree:
|
|
77
|
+
// any file:// inside `data` now resolves relative to nodes/<id>/.
|
|
78
|
+
const childNodeId = looksLikeFlowNode(obj) ? obj.id : nodeId;
|
|
68
79
|
const out: Record<string, unknown> = {};
|
|
69
|
-
for (const [k, v] of Object.entries(
|
|
70
|
-
out[k] = walk(v);
|
|
80
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
81
|
+
out[k] = walk(v, childNodeId);
|
|
71
82
|
}
|
|
72
83
|
return out;
|
|
73
84
|
}
|
|
74
85
|
return node;
|
|
75
86
|
};
|
|
76
87
|
|
|
77
|
-
const resolved = walk(raw);
|
|
88
|
+
const resolved = walk(raw, null);
|
|
78
89
|
return { resolved, refs: [...refs].sort() };
|
|
79
90
|
}
|