@tuongaz/seeflow 0.1.25 → 0.1.27
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/dist/web/assets/index-BotEftAD.css +1 -0
- package/dist/web/assets/{index-BJ7xSozm.js → index-CdNWAi1U.js} +4 -4
- package/dist/web/assets/{index.es-B3xFOWmE.js → index.es-CPyvUCV3.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-Dh_oxn-h.js → jspdf.es.min-Dkq0NSxE.js} +3 -3
- package/dist/web/index.html +2 -2
- package/examples/ecommerce-platform/.seeflow/{seeflow.json → architecture.json} +14 -77
- package/examples/ecommerce-platform/.seeflow/details/api-gateway.md +14 -0
- package/examples/ecommerce-platform/.seeflow/details/auth-service.md +9 -0
- package/examples/ecommerce-platform/.seeflow/details/cart-service.md +10 -0
- package/examples/ecommerce-platform/.seeflow/details/notification-service.md +13 -0
- package/examples/ecommerce-platform/.seeflow/details/order-service.md +16 -0
- package/examples/ecommerce-platform/.seeflow/details/payment-service.md +16 -0
- package/examples/ecommerce-platform/.seeflow/details/product-service.md +10 -0
- package/examples/ecommerce-platform/.seeflow/style.json +85 -0
- package/examples/order-pipeline/.seeflow/architecture.json +93 -0
- package/examples/order-pipeline/.seeflow/details/fulfillment-service.md +21 -0
- package/examples/order-pipeline/.seeflow/details/inventory-service.md +23 -0
- package/examples/order-pipeline/.seeflow/details/payment-service.md +23 -0
- package/examples/order-pipeline/.seeflow/details/post-orders.md +19 -0
- package/examples/order-pipeline/.seeflow/scripts/play.ts +2 -2
- package/examples/order-pipeline/.seeflow/style.json +42 -0
- package/package.json +1 -1
- package/src/api.ts +118 -118
- package/src/cli.ts +13 -13
- package/src/demo.ts +6 -6
- package/src/diagram.ts +4 -4
- package/src/events.ts +14 -14
- package/src/file-ref.ts +79 -0
- package/src/mcp.ts +117 -89
- package/src/merge.ts +190 -0
- package/src/operations.ts +415 -416
- package/src/proxy.ts +31 -31
- package/src/registry.ts +32 -20
- package/src/schema.ts +252 -8
- package/src/sdk-template.ts +2 -2
- package/src/sdk-writer.ts +2 -2
- package/src/server.ts +2 -2
- package/src/status-runner.ts +34 -38
- package/src/watcher.ts +165 -114
- package/dist/web/assets/index-Dwa7Bp5j.css +0 -1
- package/examples/order-pipeline/.seeflow/seeflow.json +0 -123
package/src/api.ts
CHANGED
|
@@ -19,20 +19,22 @@ import {
|
|
|
19
19
|
PositionBodySchema,
|
|
20
20
|
RegisterBodySchema,
|
|
21
21
|
ReorderBodySchema,
|
|
22
|
+
type ValidateBody,
|
|
22
23
|
addConnectorImpl,
|
|
23
24
|
addNodeImpl,
|
|
24
25
|
createProjectImpl,
|
|
25
26
|
deleteConnectorImpl,
|
|
26
|
-
|
|
27
|
+
deleteFlowImpl,
|
|
27
28
|
deleteNodeImpl,
|
|
28
|
-
|
|
29
|
+
getFlowImpl,
|
|
29
30
|
listDemosImpl,
|
|
30
31
|
moveNodeImpl,
|
|
31
32
|
patchConnectorImpl,
|
|
32
33
|
patchNodeImpl,
|
|
33
|
-
|
|
34
|
+
registerFlowImpl,
|
|
34
35
|
reorderNodeImpl,
|
|
35
|
-
|
|
36
|
+
resolveFilePath,
|
|
37
|
+
validateImpl,
|
|
36
38
|
} from './operations.ts';
|
|
37
39
|
import type { ProcessSpawner } from './process-spawner.ts';
|
|
38
40
|
import {
|
|
@@ -45,13 +47,14 @@ import {
|
|
|
45
47
|
stopAllPlays as defaultStopAllPlays,
|
|
46
48
|
} from './proxy.ts';
|
|
47
49
|
import type { Registry } from './registry.ts';
|
|
48
|
-
import {
|
|
50
|
+
import { FlowSchema } from './schema.ts';
|
|
49
51
|
import { type Spawner, defaultSpawner } from './shellout.ts';
|
|
50
52
|
import type { StatusRunner } from './status-runner.ts';
|
|
51
|
-
import
|
|
53
|
+
import { readMergedFlow } from './watcher.ts';
|
|
54
|
+
import type { FlowWatcher } from './watcher.ts';
|
|
52
55
|
|
|
53
56
|
const EmitBodySchema = z.object({
|
|
54
|
-
|
|
57
|
+
flowId: z.string().min(1),
|
|
55
58
|
nodeId: z.string().min(1),
|
|
56
59
|
status: z.enum(['running', 'done', 'error']),
|
|
57
60
|
runId: z.string().optional(),
|
|
@@ -167,7 +170,7 @@ function pickUploadFilename(assetsDir: string, base: string, ext: string): strin
|
|
|
167
170
|
export interface ApiOptions {
|
|
168
171
|
registry: Registry;
|
|
169
172
|
events?: EventBus;
|
|
170
|
-
watcher?:
|
|
173
|
+
watcher?: FlowWatcher;
|
|
171
174
|
/** Injectable shellout for tests; defaults to Bun.spawn fire-and-forget. */
|
|
172
175
|
spawner?: Spawner;
|
|
173
176
|
/** Override `process.platform` for tests covering darwin/win32/linux branches. */
|
|
@@ -195,7 +198,7 @@ export interface ApiOptions {
|
|
|
195
198
|
export interface ProxyFacade {
|
|
196
199
|
runPlay(options: RunPlayOptions): Promise<PlayResult>;
|
|
197
200
|
runReset(options: RunResetOptions): Promise<ResetResult>;
|
|
198
|
-
stopAllPlays(
|
|
201
|
+
stopAllPlays(flowId: string): Promise<void>;
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
export const defaultProxyFacade: ProxyFacade = {
|
|
@@ -213,7 +216,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
213
216
|
const projectBaseDir = options.projectBaseDir;
|
|
214
217
|
const api = new Hono();
|
|
215
218
|
|
|
216
|
-
api.post('/
|
|
219
|
+
api.post('/flows/register', async (c) => {
|
|
217
220
|
let body: unknown;
|
|
218
221
|
try {
|
|
219
222
|
body = await c.req.json();
|
|
@@ -226,16 +229,16 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
226
229
|
return c.json({ error: 'Invalid register body', issues: parsed.error.issues }, 400);
|
|
227
230
|
}
|
|
228
231
|
|
|
229
|
-
const result = await
|
|
232
|
+
const result = await registerFlowImpl({ registry, watcher }, parsed.data);
|
|
230
233
|
switch (result.kind) {
|
|
231
234
|
case 'ok':
|
|
232
235
|
return c.json(result.data);
|
|
233
236
|
case 'fileNotFound':
|
|
234
|
-
return c.json({ error: `
|
|
237
|
+
return c.json({ error: `Flow file not found: ${result.path}` }, 400);
|
|
235
238
|
case 'badJson':
|
|
236
|
-
return c.json({ error: '
|
|
239
|
+
return c.json({ error: 'Flow file is not valid JSON', detail: result.detail }, 400);
|
|
237
240
|
case 'badSchema':
|
|
238
|
-
return c.json({ error: '
|
|
241
|
+
return c.json({ error: 'Flow file failed schema validation', issues: result.issues }, 400);
|
|
239
242
|
case 'sdkWriteFailed':
|
|
240
243
|
return c.json(
|
|
241
244
|
{
|
|
@@ -248,12 +251,12 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
248
251
|
}
|
|
249
252
|
});
|
|
250
253
|
|
|
251
|
-
// POST /api/
|
|
254
|
+
// POST /api/flows/validate — dry-run validation. The skill's diagram
|
|
252
255
|
// pipeline calls this between assemble and register to decide whether to
|
|
253
256
|
// rewire. Runs the Zod schema, the soft node cap, and the tier playability
|
|
254
257
|
// check. Filesystem-bound checks (harness coverage, event emitter index)
|
|
255
258
|
// stay in the skill since the studio doesn't see the user's $TARGET.
|
|
256
|
-
api.post('/
|
|
259
|
+
api.post('/flows/validate', async (c) => {
|
|
257
260
|
let body: unknown;
|
|
258
261
|
try {
|
|
259
262
|
body = await c.req.json();
|
|
@@ -267,6 +270,23 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
267
270
|
return c.json(validateDemo(parsed.data));
|
|
268
271
|
});
|
|
269
272
|
|
|
273
|
+
// POST /api/validate — stateless schema validator for the architecture +
|
|
274
|
+
// optional style files. No flow id, no registry side-effects, no file://
|
|
275
|
+
// resolution (validation is structural only). Returns 200 even on
|
|
276
|
+
// validation failure — the result is the validation report itself.
|
|
277
|
+
api.post('/validate', async (c) => {
|
|
278
|
+
let body: unknown;
|
|
279
|
+
try {
|
|
280
|
+
body = await c.req.json();
|
|
281
|
+
} catch {
|
|
282
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
283
|
+
}
|
|
284
|
+
if (!body || typeof body !== 'object' || !('architecture' in body)) {
|
|
285
|
+
return c.json({ error: 'Body must be { architecture, style? }' }, 400);
|
|
286
|
+
}
|
|
287
|
+
return c.json(validateImpl(body as ValidateBody));
|
|
288
|
+
});
|
|
289
|
+
|
|
270
290
|
// POST /api/diagram/propose-scope — Phase 2 helper. The skill POSTs the
|
|
271
291
|
// scan-result.json shape and gets back ranked entry-point candidates.
|
|
272
292
|
// Pure compute; skill writes the response to intermediate/entry-candidates.json.
|
|
@@ -345,20 +365,20 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
345
365
|
}
|
|
346
366
|
});
|
|
347
367
|
|
|
348
|
-
api.get('/
|
|
368
|
+
api.get('/flows', (c) => {
|
|
349
369
|
const result = listDemosImpl({ registry });
|
|
350
370
|
return c.json(result.data);
|
|
351
371
|
});
|
|
352
372
|
|
|
353
|
-
api.get('/
|
|
354
|
-
const result = await
|
|
373
|
+
api.get('/flows/:id', async (c) => {
|
|
374
|
+
const result = await getFlowImpl({ registry, watcher }, c.req.param('id'));
|
|
355
375
|
switch (result.kind) {
|
|
356
376
|
case 'ok':
|
|
357
377
|
return c.json(result.data);
|
|
358
378
|
case 'notFound':
|
|
359
379
|
return c.json({ error: 'not found' }, 404);
|
|
360
380
|
case 'fileNotFound':
|
|
361
|
-
return c.json({ error: `
|
|
381
|
+
return c.json({ error: `Flow file not found: ${result.path}` }, 404);
|
|
362
382
|
}
|
|
363
383
|
});
|
|
364
384
|
|
|
@@ -548,8 +568,8 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
548
568
|
return c.json({ path: `assets/${finalName}` });
|
|
549
569
|
});
|
|
550
570
|
|
|
551
|
-
api.delete('/
|
|
552
|
-
const result =
|
|
571
|
+
api.delete('/flows/:id', (c) => {
|
|
572
|
+
const result = deleteFlowImpl({ registry, watcher }, c.req.param('id'));
|
|
553
573
|
switch (result.kind) {
|
|
554
574
|
case 'ok':
|
|
555
575
|
return c.json({ ok: true });
|
|
@@ -558,7 +578,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
558
578
|
}
|
|
559
579
|
});
|
|
560
580
|
|
|
561
|
-
api.post('/
|
|
581
|
+
api.post('/flows/:id/play/:nodeId', async (c) => {
|
|
562
582
|
const id = c.req.param('id');
|
|
563
583
|
const nodeId = c.req.param('nodeId');
|
|
564
584
|
const entry = registry.getById(id);
|
|
@@ -567,27 +587,18 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
567
587
|
|
|
568
588
|
// Always re-read from disk so the user's most recent edit (validated or
|
|
569
589
|
// not yet observed by the watcher) drives the actual fetch.
|
|
570
|
-
const fullPath =
|
|
590
|
+
const fullPath = resolveFilePath(entry.repoPath, entry.architecturePath);
|
|
571
591
|
if (!existsSync(fullPath)) {
|
|
572
|
-
return c.json({ error: `
|
|
573
|
-
}
|
|
574
|
-
let raw: unknown;
|
|
575
|
-
try {
|
|
576
|
-
raw = await Bun.file(fullPath).json();
|
|
577
|
-
} catch (err) {
|
|
578
|
-
return c.json(
|
|
579
|
-
{
|
|
580
|
-
error: `Demo file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
581
|
-
},
|
|
582
|
-
400,
|
|
583
|
-
);
|
|
592
|
+
return c.json({ error: `Flow file not found: ${fullPath}` }, 404);
|
|
584
593
|
}
|
|
585
|
-
const
|
|
586
|
-
if (!
|
|
587
|
-
|
|
594
|
+
const merged = readMergedFlow(fullPath);
|
|
595
|
+
if (!merged.flow) {
|
|
596
|
+
const error = merged.error ?? 'Flow read failed';
|
|
597
|
+
const status = error.startsWith('Invalid JSON in') ? 400 : 400;
|
|
598
|
+
return c.json({ error }, status);
|
|
588
599
|
}
|
|
589
600
|
|
|
590
|
-
const node =
|
|
601
|
+
const node = merged.flow.nodes.find((n) => n.id === nodeId);
|
|
591
602
|
if (!node) return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
|
|
592
603
|
if (
|
|
593
604
|
node.type === 'shapeNode' ||
|
|
@@ -613,7 +624,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
613
624
|
|
|
614
625
|
const result = await proxy.runPlay({
|
|
615
626
|
events,
|
|
616
|
-
|
|
627
|
+
flowId: id,
|
|
617
628
|
nodeId,
|
|
618
629
|
cwd: entry.repoPath,
|
|
619
630
|
action: node.data.playAction,
|
|
@@ -628,40 +639,29 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
628
639
|
return c.json(result);
|
|
629
640
|
});
|
|
630
641
|
|
|
631
|
-
// POST /api/
|
|
642
|
+
// POST /api/flows/:id/reset — the "Restart demo" workflow (US-008). Order:
|
|
632
643
|
// 1. Stop every live play-script + every long-running status-script for
|
|
633
644
|
// this demo in parallel — both must complete before any reset script
|
|
634
645
|
// spawns so the script sees no stragglers.
|
|
635
646
|
// 2. Run the demo's `resetAction` script (if declared); any non-zero exit
|
|
636
647
|
// becomes a 502 to the caller but does NOT suppress reload/restart.
|
|
637
|
-
// 3. Broadcast `
|
|
648
|
+
// 3. Broadcast `flow:reload` unconditionally so the canvas re-fetches.
|
|
638
649
|
// 4. Fire-and-forget `statusRunner.restart` so the next status batch is
|
|
639
650
|
// spawning by the time the response lands. Individual spawn failures
|
|
640
651
|
// surface via console.warn but never fail the /reset call.
|
|
641
|
-
api.post('/
|
|
652
|
+
api.post('/flows/:id/reset', async (c) => {
|
|
642
653
|
const id = c.req.param('id');
|
|
643
654
|
const entry = registry.getById(id);
|
|
644
655
|
if (!entry) return c.json({ error: 'unknown demo' }, 404);
|
|
645
656
|
if (!events) return c.json({ error: 'events not enabled' }, 500);
|
|
646
657
|
|
|
647
|
-
const fullPath =
|
|
658
|
+
const fullPath = resolveFilePath(entry.repoPath, entry.architecturePath);
|
|
648
659
|
if (!existsSync(fullPath)) {
|
|
649
|
-
return c.json({ error: `
|
|
650
|
-
}
|
|
651
|
-
let raw: unknown;
|
|
652
|
-
try {
|
|
653
|
-
raw = await Bun.file(fullPath).json();
|
|
654
|
-
} catch (err) {
|
|
655
|
-
return c.json(
|
|
656
|
-
{
|
|
657
|
-
error: `Demo file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
658
|
-
},
|
|
659
|
-
400,
|
|
660
|
-
);
|
|
660
|
+
return c.json({ error: `Flow file not found: ${fullPath}` }, 404);
|
|
661
661
|
}
|
|
662
|
-
const
|
|
663
|
-
if (!
|
|
664
|
-
return c.json({ error:
|
|
662
|
+
const merged = readMergedFlow(fullPath);
|
|
663
|
+
if (!merged.flow) {
|
|
664
|
+
return c.json({ error: merged.error ?? 'Flow read failed' }, 400);
|
|
665
665
|
}
|
|
666
666
|
|
|
667
667
|
// 1. Stop every play + status script in parallel. await BOTH before
|
|
@@ -672,7 +672,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
672
672
|
await Promise.all(stopPromises);
|
|
673
673
|
|
|
674
674
|
// 2. Run resetAction (if declared).
|
|
675
|
-
const resetAction =
|
|
675
|
+
const resetAction = merged.flow.resetAction;
|
|
676
676
|
let calledResetAction = false;
|
|
677
677
|
let resetActionError: string | undefined;
|
|
678
678
|
|
|
@@ -680,7 +680,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
680
680
|
calledResetAction = true;
|
|
681
681
|
const result = await proxy.runReset({
|
|
682
682
|
events,
|
|
683
|
-
|
|
683
|
+
flowId: id,
|
|
684
684
|
cwd: entry.repoPath,
|
|
685
685
|
action: resetAction,
|
|
686
686
|
});
|
|
@@ -693,8 +693,8 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
693
693
|
// the canvas should still refresh from disk in case the user just
|
|
694
694
|
// edited the file.
|
|
695
695
|
events.broadcast({
|
|
696
|
-
type: '
|
|
697
|
-
|
|
696
|
+
type: 'flow:reload',
|
|
697
|
+
flowId: id,
|
|
698
698
|
payload: {},
|
|
699
699
|
});
|
|
700
700
|
|
|
@@ -717,7 +717,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
717
717
|
// the second (and only other) place the studio mutates user files — the
|
|
718
718
|
// first being the SDK helper write in `register`. Atomic write via tempfile
|
|
719
719
|
// + rename keeps editor diffs clean and avoids corruption mid-write.
|
|
720
|
-
api.patch('/
|
|
720
|
+
api.patch('/flows/:id/nodes/:nodeId/position', async (c) => {
|
|
721
721
|
const id = c.req.param('id');
|
|
722
722
|
const nodeId = c.req.param('nodeId');
|
|
723
723
|
|
|
@@ -736,14 +736,14 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
736
736
|
switch (result.kind) {
|
|
737
737
|
case 'ok':
|
|
738
738
|
return c.json({ ok: true, position: result.data.position });
|
|
739
|
-
case '
|
|
739
|
+
case 'flowNotFound':
|
|
740
740
|
return c.json({ error: 'unknown demo' }, 404);
|
|
741
741
|
case 'fileNotFound':
|
|
742
|
-
return c.json({ error: `
|
|
742
|
+
return c.json({ error: `Flow file not found: ${result.path}` }, 404);
|
|
743
743
|
case 'badJson':
|
|
744
|
-
return c.json({ error: `
|
|
744
|
+
return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
|
|
745
745
|
case 'badSchema':
|
|
746
|
-
return c.json({ error: '
|
|
746
|
+
return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
|
|
747
747
|
case 'unknownNode':
|
|
748
748
|
return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
|
|
749
749
|
case 'writeFailed':
|
|
@@ -758,7 +758,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
758
758
|
// toBack (remove + push/unshift), and toIndex (pin to an absolute index)
|
|
759
759
|
// which the undo path uses to faithfully revert forward/backward gestures
|
|
760
760
|
// even if the array changed between the original op and the undo.
|
|
761
|
-
api.patch('/
|
|
761
|
+
api.patch('/flows/:id/nodes/:nodeId/order', async (c) => {
|
|
762
762
|
const id = c.req.param('id');
|
|
763
763
|
const nodeId = c.req.param('nodeId');
|
|
764
764
|
|
|
@@ -777,14 +777,14 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
777
777
|
switch (result.kind) {
|
|
778
778
|
case 'ok':
|
|
779
779
|
return c.json({ ok: true });
|
|
780
|
-
case '
|
|
780
|
+
case 'flowNotFound':
|
|
781
781
|
return c.json({ error: 'unknown demo' }, 404);
|
|
782
782
|
case 'fileNotFound':
|
|
783
|
-
return c.json({ error: `
|
|
783
|
+
return c.json({ error: `Flow file not found: ${result.path}` }, 404);
|
|
784
784
|
case 'badJson':
|
|
785
|
-
return c.json({ error: `
|
|
785
|
+
return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
|
|
786
786
|
case 'badSchema':
|
|
787
|
-
return c.json({ error: '
|
|
787
|
+
return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
|
|
788
788
|
case 'unknownNode':
|
|
789
789
|
return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
|
|
790
790
|
case 'writeFailed':
|
|
@@ -797,9 +797,9 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
797
797
|
// the high-frequency drag fast-path above) flows through here. The mutation
|
|
798
798
|
// is performed against the raw parsed JSON (so unknown v2 fields the schema
|
|
799
799
|
// doesn't yet recognize survive round-trips) and the WHOLE resulting demo
|
|
800
|
-
// is re-validated through
|
|
800
|
+
// is re-validated through FlowSchema before commit, preventing partial
|
|
801
801
|
// writes from breaking invariants like the connector→node superRefine.
|
|
802
|
-
api.patch('/
|
|
802
|
+
api.patch('/flows/:id/nodes/:nodeId', async (c) => {
|
|
803
803
|
const id = c.req.param('id');
|
|
804
804
|
const nodeId = c.req.param('nodeId');
|
|
805
805
|
|
|
@@ -818,14 +818,14 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
818
818
|
switch (result.kind) {
|
|
819
819
|
case 'ok':
|
|
820
820
|
return c.json({ ok: true });
|
|
821
|
-
case '
|
|
821
|
+
case 'flowNotFound':
|
|
822
822
|
return c.json({ error: 'unknown demo' }, 404);
|
|
823
823
|
case 'fileNotFound':
|
|
824
|
-
return c.json({ error: `
|
|
824
|
+
return c.json({ error: `Flow file not found: ${result.path}` }, 404);
|
|
825
825
|
case 'badJson':
|
|
826
|
-
return c.json({ error: `
|
|
826
|
+
return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
|
|
827
827
|
case 'badSchema':
|
|
828
|
-
return c.json({ error: '
|
|
828
|
+
return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
|
|
829
829
|
case 'unknownNode':
|
|
830
830
|
return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
|
|
831
831
|
case 'writeFailed':
|
|
@@ -834,9 +834,9 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
834
834
|
});
|
|
835
835
|
|
|
836
836
|
// POST a new node into the demo. Body is the node payload (id auto-generated
|
|
837
|
-
// server-side if absent). Atomicity + final-
|
|
837
|
+
// server-side if absent). Atomicity + final-FlowSchema validation match the
|
|
838
838
|
// PATCH path above, so a malformed node never produces a half-written file.
|
|
839
|
-
api.post('/
|
|
839
|
+
api.post('/flows/:id/nodes', async (c) => {
|
|
840
840
|
const id = c.req.param('id');
|
|
841
841
|
|
|
842
842
|
let body: unknown;
|
|
@@ -853,25 +853,25 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
853
853
|
switch (result.kind) {
|
|
854
854
|
case 'ok':
|
|
855
855
|
return c.json({ ok: true, id: result.data.id, node: result.data.node });
|
|
856
|
-
case '
|
|
856
|
+
case 'flowNotFound':
|
|
857
857
|
return c.json({ error: 'unknown demo' }, 404);
|
|
858
858
|
case 'fileNotFound':
|
|
859
|
-
return c.json({ error: `
|
|
859
|
+
return c.json({ error: `Flow file not found: ${result.path}` }, 404);
|
|
860
860
|
case 'badJson':
|
|
861
|
-
return c.json({ error: `
|
|
861
|
+
return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
|
|
862
862
|
case 'badSchema':
|
|
863
|
-
return c.json({ error: '
|
|
863
|
+
return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
|
|
864
864
|
case 'writeFailed':
|
|
865
865
|
return c.json({ error: `Failed to write demo file: ${result.message}` }, 500);
|
|
866
866
|
}
|
|
867
867
|
});
|
|
868
868
|
|
|
869
869
|
// DELETE a node and cascade-remove every connector with source === nodeId or
|
|
870
|
-
// target === nodeId in the same atomic write. Final-
|
|
870
|
+
// target === nodeId in the same atomic write. Final-FlowSchema validation
|
|
871
871
|
// is still run after the mutation — connector cascade closure means it
|
|
872
872
|
// should always pass, but the check makes the failure mode honest if the
|
|
873
873
|
// file had a pre-existing schema violation we'd otherwise paper over.
|
|
874
|
-
api.delete('/
|
|
874
|
+
api.delete('/flows/:id/nodes/:nodeId', async (c) => {
|
|
875
875
|
const id = c.req.param('id');
|
|
876
876
|
const nodeId = c.req.param('nodeId');
|
|
877
877
|
|
|
@@ -879,14 +879,14 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
879
879
|
switch (result.kind) {
|
|
880
880
|
case 'ok':
|
|
881
881
|
return c.json({ ok: true });
|
|
882
|
-
case '
|
|
882
|
+
case 'flowNotFound':
|
|
883
883
|
return c.json({ error: 'unknown demo' }, 404);
|
|
884
884
|
case 'fileNotFound':
|
|
885
|
-
return c.json({ error: `
|
|
885
|
+
return c.json({ error: `Flow file not found: ${result.path}` }, 404);
|
|
886
886
|
case 'badJson':
|
|
887
|
-
return c.json({ error: `
|
|
887
|
+
return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
|
|
888
888
|
case 'badSchema':
|
|
889
|
-
return c.json({ error: '
|
|
889
|
+
return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
|
|
890
890
|
case 'unknownNode':
|
|
891
891
|
return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
|
|
892
892
|
case 'writeFailed':
|
|
@@ -897,11 +897,11 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
897
897
|
// PATCH a single connector — partial update of label/style/color/direction
|
|
898
898
|
// and (optionally) kind + per-kind payload fields. When `kind` changes,
|
|
899
899
|
// stale kind-specific fields are dropped before the merge. The whole demo
|
|
900
|
-
// is re-validated through
|
|
900
|
+
// is re-validated through FlowSchema before commit so the discriminated
|
|
901
901
|
// union catches missing-required-fields (e.g. kind='event' without
|
|
902
902
|
// eventName) and the superRefine still gates source/target referential
|
|
903
903
|
// integrity.
|
|
904
|
-
api.patch('/
|
|
904
|
+
api.patch('/flows/:id/connectors/:connId', async (c) => {
|
|
905
905
|
const id = c.req.param('id');
|
|
906
906
|
const connId = c.req.param('connId');
|
|
907
907
|
|
|
@@ -920,14 +920,14 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
920
920
|
switch (result.kind) {
|
|
921
921
|
case 'ok':
|
|
922
922
|
return c.json({ ok: true });
|
|
923
|
-
case '
|
|
923
|
+
case 'flowNotFound':
|
|
924
924
|
return c.json({ error: 'unknown demo' }, 404);
|
|
925
925
|
case 'fileNotFound':
|
|
926
|
-
return c.json({ error: `
|
|
926
|
+
return c.json({ error: `Flow file not found: ${result.path}` }, 404);
|
|
927
927
|
case 'badJson':
|
|
928
|
-
return c.json({ error: `
|
|
928
|
+
return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
|
|
929
929
|
case 'badSchema':
|
|
930
|
-
return c.json({ error: '
|
|
930
|
+
return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
|
|
931
931
|
case 'unknownConnector':
|
|
932
932
|
return c.json({ error: `Unknown connectorId: ${connId}` }, 404);
|
|
933
933
|
case 'writeFailed':
|
|
@@ -938,8 +938,8 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
938
938
|
// POST a new connector. Body is the connector payload; `id` is auto-generated
|
|
939
939
|
// server-side if absent and `kind` defaults to 'default' (the no-semantics
|
|
940
940
|
// user-drawn variant). Source/target referential integrity is enforced by
|
|
941
|
-
//
|
|
942
|
-
api.post('/
|
|
941
|
+
// FlowSchema's superRefine on the post-mutation parse.
|
|
942
|
+
api.post('/flows/:id/connectors', async (c) => {
|
|
943
943
|
const id = c.req.param('id');
|
|
944
944
|
|
|
945
945
|
let body: unknown;
|
|
@@ -960,14 +960,14 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
960
960
|
switch (result.kind) {
|
|
961
961
|
case 'ok':
|
|
962
962
|
return c.json({ ok: true, id: result.data.id });
|
|
963
|
-
case '
|
|
963
|
+
case 'flowNotFound':
|
|
964
964
|
return c.json({ error: 'unknown demo' }, 404);
|
|
965
965
|
case 'fileNotFound':
|
|
966
|
-
return c.json({ error: `
|
|
966
|
+
return c.json({ error: `Flow file not found: ${result.path}` }, 404);
|
|
967
967
|
case 'badJson':
|
|
968
|
-
return c.json({ error: `
|
|
968
|
+
return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
|
|
969
969
|
case 'badSchema':
|
|
970
|
-
return c.json({ error: '
|
|
970
|
+
return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
|
|
971
971
|
case 'writeFailed':
|
|
972
972
|
return c.json({ error: `Failed to write demo file: ${result.message}` }, 500);
|
|
973
973
|
}
|
|
@@ -975,7 +975,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
975
975
|
|
|
976
976
|
// DELETE a connector. Just removes the entry from demo.connectors — node
|
|
977
977
|
// deletion is what cascades, not connector deletion.
|
|
978
|
-
api.delete('/
|
|
978
|
+
api.delete('/flows/:id/connectors/:connId', async (c) => {
|
|
979
979
|
const id = c.req.param('id');
|
|
980
980
|
const connId = c.req.param('connId');
|
|
981
981
|
|
|
@@ -983,14 +983,14 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
983
983
|
switch (result.kind) {
|
|
984
984
|
case 'ok':
|
|
985
985
|
return c.json({ ok: true });
|
|
986
|
-
case '
|
|
986
|
+
case 'flowNotFound':
|
|
987
987
|
return c.json({ error: 'unknown demo' }, 404);
|
|
988
988
|
case 'fileNotFound':
|
|
989
|
-
return c.json({ error: `
|
|
989
|
+
return c.json({ error: `Flow file not found: ${result.path}` }, 404);
|
|
990
990
|
case 'badJson':
|
|
991
|
-
return c.json({ error: `
|
|
991
|
+
return c.json({ error: `Flow file is not valid JSON: ${result.message}` }, 400);
|
|
992
992
|
case 'badSchema':
|
|
993
|
-
return c.json({ error: '
|
|
993
|
+
return c.json({ error: 'Flow failed schema validation', issues: result.issues }, 400);
|
|
994
994
|
case 'unknownConnector':
|
|
995
995
|
return c.json({ error: `Unknown connectorId: ${connId}` }, 404);
|
|
996
996
|
case 'writeFailed':
|
|
@@ -1013,9 +1013,9 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
1013
1013
|
return c.json({ error: 'Invalid emit body', issues: parsed.error.issues }, 400);
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
|
-
const {
|
|
1017
|
-
if (!registry.getById(
|
|
1018
|
-
return c.json({ error: `Unknown
|
|
1016
|
+
const { flowId, nodeId, status, runId, payload } = parsed.data;
|
|
1017
|
+
if (!registry.getById(flowId)) {
|
|
1018
|
+
return c.json({ error: `Unknown flowId: ${flowId}` }, 404);
|
|
1019
1019
|
}
|
|
1020
1020
|
|
|
1021
1021
|
const extras =
|
|
@@ -1027,7 +1027,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
1027
1027
|
|
|
1028
1028
|
events.broadcast({
|
|
1029
1029
|
type: EMIT_STATUS_TO_EVENT[status],
|
|
1030
|
-
|
|
1030
|
+
flowId,
|
|
1031
1031
|
payload: eventPayload,
|
|
1032
1032
|
});
|
|
1033
1033
|
|
|
@@ -1035,9 +1035,9 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
1035
1035
|
});
|
|
1036
1036
|
|
|
1037
1037
|
api.get('/events', (c) => {
|
|
1038
|
-
const
|
|
1039
|
-
if (!
|
|
1040
|
-
if (!registry.getById(
|
|
1038
|
+
const flowId = c.req.query('flowId');
|
|
1039
|
+
if (!flowId) return c.json({ error: 'flowId query param required' }, 400);
|
|
1040
|
+
if (!registry.getById(flowId)) return c.json({ error: 'unknown flowId' }, 404);
|
|
1041
1041
|
if (!events) return c.json({ error: 'events not enabled' }, 500);
|
|
1042
1042
|
|
|
1043
1043
|
return streamSSE(c, async (stream) => {
|
|
@@ -1053,7 +1053,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
1053
1053
|
}
|
|
1054
1054
|
};
|
|
1055
1055
|
|
|
1056
|
-
const unsubscribe = events.subscribe(
|
|
1056
|
+
const unsubscribe = events.subscribe(flowId, (e) => {
|
|
1057
1057
|
queue.push({ event: e.type, data: JSON.stringify({ ts: e.ts, ...(e.payload as object) }) });
|
|
1058
1058
|
wake();
|
|
1059
1059
|
});
|
|
@@ -1068,7 +1068,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
1068
1068
|
// and trigger a re-fetch on the frontend.
|
|
1069
1069
|
await stream.writeSSE({
|
|
1070
1070
|
event: 'hello',
|
|
1071
|
-
data: JSON.stringify({
|
|
1071
|
+
data: JSON.stringify({ flowId, ts: Date.now() }),
|
|
1072
1072
|
});
|
|
1073
1073
|
|
|
1074
1074
|
try {
|