@tuongaz/seeflow 0.1.26 → 0.1.28

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 (45) hide show
  1. package/bin/seeflow +0 -0
  2. package/bin/seeflow-mcp +0 -0
  3. package/dist/web/assets/{index-BMaMEi2a.js → index--9KvdiJU.js} +1584 -1584
  4. package/dist/web/assets/index-XIY68z9O.css +1 -0
  5. package/dist/web/assets/{index.es-M1iBDKG6.js → index.es-CehYgUiq.js} +1 -1
  6. package/dist/web/assets/{jspdf.es.min-xZpq8bcn.js → jspdf.es.min-CaFlbpn0.js} +3 -3
  7. package/dist/web/index.html +2 -2
  8. package/examples/ecommerce-platform/.seeflow/{seeflow.json → architecture.json} +22 -77
  9. package/examples/ecommerce-platform/.seeflow/details/api-gateway.md +14 -0
  10. package/examples/ecommerce-platform/.seeflow/details/auth-service.md +9 -0
  11. package/examples/ecommerce-platform/.seeflow/details/cart-service.md +10 -0
  12. package/examples/ecommerce-platform/.seeflow/details/notification-service.md +13 -0
  13. package/examples/ecommerce-platform/.seeflow/details/order-service.md +16 -0
  14. package/examples/ecommerce-platform/.seeflow/details/payment-service.md +16 -0
  15. package/examples/ecommerce-platform/.seeflow/details/product-service.md +10 -0
  16. package/examples/ecommerce-platform/.seeflow/scripts/platform-health.html +42 -0
  17. package/examples/ecommerce-platform/.seeflow/style.json +98 -0
  18. package/examples/order-pipeline/.seeflow/architecture.json +93 -0
  19. package/examples/order-pipeline/.seeflow/details/fulfillment-service.md +21 -0
  20. package/examples/order-pipeline/.seeflow/details/inventory-service.md +23 -0
  21. package/examples/order-pipeline/.seeflow/details/payment-service.md +23 -0
  22. package/examples/order-pipeline/.seeflow/details/post-orders.md +19 -0
  23. package/examples/order-pipeline/.seeflow/scripts/play.ts +2 -2
  24. package/examples/order-pipeline/.seeflow/style.json +42 -0
  25. package/package.json +4 -2
  26. package/public/runtime/tailwind.js +931 -24378
  27. package/src/api.ts +118 -118
  28. package/src/cli.ts +13 -13
  29. package/src/demo.ts +6 -6
  30. package/src/diagram.ts +4 -4
  31. package/src/events.ts +14 -14
  32. package/src/file-ref.ts +79 -0
  33. package/src/mcp.ts +117 -89
  34. package/src/merge.ts +190 -0
  35. package/src/operations.ts +415 -416
  36. package/src/proxy.ts +31 -31
  37. package/src/registry.ts +32 -20
  38. package/src/schema.ts +252 -8
  39. package/src/sdk-template.ts +2 -2
  40. package/src/sdk-writer.ts +2 -2
  41. package/src/server.ts +14 -7
  42. package/src/status-runner.ts +34 -38
  43. package/src/watcher.ts +165 -114
  44. package/dist/web/assets/index-BotEftAD.css +0 -1
  45. package/examples/order-pipeline/.seeflow/seeflow.json +0 -123
package/src/proxy.ts CHANGED
@@ -31,7 +31,7 @@ export interface PlayResult {
31
31
 
32
32
  export interface RunPlayOptions {
33
33
  events: EventBus;
34
- demoId: string;
34
+ flowId: string;
35
35
  nodeId: string;
36
36
  /** Project root (`<repoPath>`). Script resolves under `<cwd>/.seeflow/`. */
37
37
  cwd: string;
@@ -103,24 +103,24 @@ async function writeStdinPayload(handle: SpawnHandle, input: unknown): Promise<v
103
103
  }
104
104
  }
105
105
 
106
- // Live play-script handles indexed by demoId. Populated by runPlay() on spawn;
106
+ // Live play-script handles indexed by flowId. Populated by runPlay() on spawn;
107
107
  // entries are removed when each handle's `exited` promise resolves (success
108
- // AND error paths). `stopAllPlays(demoId)` consults this map to terminate
108
+ // AND error paths). `stopAllPlays(flowId)` consults this map to terminate
109
109
  // every in-flight play for a demo on /reset.
110
110
  const livePlayHandles = new Map<string, Set<SpawnHandle>>();
111
111
 
112
- function registerLiveHandle(demoId: string, handle: SpawnHandle): void {
113
- let set = livePlayHandles.get(demoId);
112
+ function registerLiveHandle(flowId: string, handle: SpawnHandle): void {
113
+ let set = livePlayHandles.get(flowId);
114
114
  if (!set) {
115
115
  set = new Set();
116
- livePlayHandles.set(demoId, set);
116
+ livePlayHandles.set(flowId, set);
117
117
  }
118
118
  set.add(handle);
119
119
  handle.exited.finally(() => {
120
- const current = livePlayHandles.get(demoId);
120
+ const current = livePlayHandles.get(flowId);
121
121
  if (!current) return;
122
122
  current.delete(handle);
123
- if (current.size === 0) livePlayHandles.delete(demoId);
123
+ if (current.size === 0) livePlayHandles.delete(flowId);
124
124
  });
125
125
  }
126
126
 
@@ -138,21 +138,21 @@ async function killWithGrace(handle: SpawnHandle): Promise<void> {
138
138
  }
139
139
  }
140
140
 
141
- // Kill every live play-script for `demoId` (SIGTERM → 2s grace → SIGKILL in
142
- // parallel) and wait for each to exit. Idempotent on an unknown demoId. The
143
- // map is keyed by demoId so a stop on demo A never touches demo B.
144
- export async function stopAllPlays(demoId: string): Promise<void> {
145
- const set = livePlayHandles.get(demoId);
141
+ // Kill every live play-script for `flowId` (SIGTERM → 2s grace → SIGKILL in
142
+ // parallel) and wait for each to exit. Idempotent on an unknown flowId. The
143
+ // map is keyed by flowId so a stop on demo A never touches demo B.
144
+ export async function stopAllPlays(flowId: string): Promise<void> {
145
+ const set = livePlayHandles.get(flowId);
146
146
  if (!set || set.size === 0) return;
147
147
  const handles = [...set];
148
148
  // Clear eagerly so a parallel runPlay can't double-count an entry we're
149
149
  // about to await on. The exited.finally() will no-op the second delete.
150
- livePlayHandles.delete(demoId);
150
+ livePlayHandles.delete(flowId);
151
151
  await Promise.all(handles.map((h) => killWithGrace(h)));
152
152
  }
153
153
 
154
154
  export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
155
- const { events, demoId, nodeId, cwd, action } = options;
155
+ const { events, flowId, nodeId, cwd, action } = options;
156
156
  const spawner = options.spawner ?? defaultProcessSpawner;
157
157
  const runId = crypto.randomUUID();
158
158
 
@@ -160,7 +160,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
160
160
  if (!resolved.ok) {
161
161
  events.broadcast({
162
162
  type: 'node:error',
163
- demoId,
163
+ flowId,
164
164
  payload: { nodeId, runId, message: SCRIPT_PATH_ESCAPE },
165
165
  });
166
166
  return { runId, error: SCRIPT_PATH_ESCAPE };
@@ -168,7 +168,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
168
168
 
169
169
  events.broadcast({
170
170
  type: 'node:running',
171
- demoId,
171
+ flowId,
172
172
  payload: {
173
173
  nodeId,
174
174
  runId,
@@ -179,7 +179,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
179
179
 
180
180
  const wantsStdin = action.input !== undefined;
181
181
  const env = buildChildEnv({
182
- SEEFLOW_DEMO_ID: demoId,
182
+ SEEFLOW_DEMO_ID: flowId,
183
183
  SEEFLOW_NODE_ID: nodeId,
184
184
  SEEFLOW_RUN_ID: runId,
185
185
  });
@@ -196,13 +196,13 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
196
196
  const message = err instanceof Error ? err.message : String(err);
197
197
  events.broadcast({
198
198
  type: 'node:error',
199
- demoId,
199
+ flowId,
200
200
  payload: { nodeId, runId, message },
201
201
  });
202
202
  return { runId, error: message };
203
203
  }
204
204
 
205
- registerLiveHandle(demoId, handle);
205
+ registerLiveHandle(flowId, handle);
206
206
 
207
207
  // Drain stdout AND stderr CONCURRENTLY with the process running so OS pipe
208
208
  // buffers (~64 KB) don't fill up and deadlock the child.
@@ -242,7 +242,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
242
242
  const message = `script timed out after ${timeoutMs}ms`;
243
243
  events.broadcast({
244
244
  type: 'node:error',
245
- demoId,
245
+ flowId,
246
246
  payload: { nodeId, runId, message },
247
247
  });
248
248
  return { runId, error: message };
@@ -260,7 +260,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
260
260
  }
261
261
  events.broadcast({
262
262
  type: 'node:done',
263
- demoId,
263
+ flowId,
264
264
  payload: { nodeId, runId, status: 200, body },
265
265
  });
266
266
  return { runId, status: 200, body };
@@ -271,7 +271,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
271
271
  const message = truncated.length > 0 ? truncated : `script exited with code ${code}`;
272
272
  events.broadcast({
273
273
  type: 'node:error',
274
- demoId,
274
+ flowId,
275
275
  payload: { nodeId, runId, message },
276
276
  });
277
277
  return { runId, error: message };
@@ -279,7 +279,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
279
279
 
280
280
  export interface RunResetOptions {
281
281
  events: EventBus;
282
- demoId: string;
282
+ flowId: string;
283
283
  /** Project root (`<repoPath>`). Script resolves under `<cwd>/.seeflow/`. */
284
284
  cwd: string;
285
285
  action: ResetAction;
@@ -300,21 +300,21 @@ export interface ResetResult {
300
300
  // returned shape. Callers (the /reset endpoint) decide what HTTP status to
301
301
  // surface; this returns `{ ok }` plus body/error so the endpoint can map.
302
302
  export async function runReset(options: RunResetOptions): Promise<ResetResult> {
303
- const { events, demoId, cwd, action } = options;
303
+ const { events, flowId, cwd, action } = options;
304
304
  const spawner = options.spawner ?? defaultProcessSpawner;
305
305
 
306
306
  const resolved = resolveScript(cwd, action.scriptPath);
307
307
  if (!resolved.ok) {
308
308
  events.broadcast({
309
309
  type: 'demo:reset',
310
- demoId,
310
+ flowId,
311
311
  payload: { ok: false, error: SCRIPT_PATH_ESCAPE },
312
312
  });
313
313
  return { ok: false, error: SCRIPT_PATH_ESCAPE };
314
314
  }
315
315
 
316
316
  const wantsStdin = action.input !== undefined;
317
- const env = buildChildEnv({ SEEFLOW_DEMO_ID: demoId });
317
+ const env = buildChildEnv({ SEEFLOW_DEMO_ID: flowId });
318
318
 
319
319
  let handle: SpawnHandle;
320
320
  try {
@@ -328,7 +328,7 @@ export async function runReset(options: RunResetOptions): Promise<ResetResult> {
328
328
  const message = err instanceof Error ? err.message : String(err);
329
329
  events.broadcast({
330
330
  type: 'demo:reset',
331
- demoId,
331
+ flowId,
332
332
  payload: { ok: false, error: message },
333
333
  });
334
334
  return { ok: false, error: message };
@@ -357,7 +357,7 @@ export async function runReset(options: RunResetOptions): Promise<ResetResult> {
357
357
  const message = `reset script timed out after ${timeoutMs}ms`;
358
358
  events.broadcast({
359
359
  type: 'demo:reset',
360
- demoId,
360
+ flowId,
361
361
  payload: { ok: false, error: message },
362
362
  });
363
363
  return { ok: false, error: message };
@@ -375,7 +375,7 @@ export async function runReset(options: RunResetOptions): Promise<ResetResult> {
375
375
  }
376
376
  events.broadcast({
377
377
  type: 'demo:reset',
378
- demoId,
378
+ flowId,
379
379
  payload: { ok: true, body },
380
380
  });
381
381
  return { ok: true, body };
@@ -386,7 +386,7 @@ export async function runReset(options: RunResetOptions): Promise<ResetResult> {
386
386
  const message = truncated.length > 0 ? truncated : `reset script exited with code ${code}`;
387
387
  events.broadcast({
388
388
  type: 'demo:reset',
389
- demoId,
389
+ flowId,
390
390
  payload: { ok: false, error: message },
391
391
  });
392
392
  return { ok: false, error: message };
package/src/registry.ts CHANGED
@@ -2,12 +2,12 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { seeflowHome } from './paths.ts';
4
4
 
5
- export interface DemoEntry {
5
+ export interface FlowEntry {
6
6
  id: string;
7
7
  slug: string;
8
8
  name: string;
9
9
  repoPath: string;
10
- demoPath: string;
10
+ architecturePath: string;
11
11
  lastModified: number;
12
12
  valid: boolean;
13
13
  }
@@ -15,18 +15,21 @@ export interface DemoEntry {
15
15
  export interface RegisterInput {
16
16
  name: string;
17
17
  repoPath: string;
18
- demoPath: string;
18
+ architecturePath: string;
19
19
  valid?: boolean;
20
20
  lastModified?: number;
21
21
  }
22
22
 
23
23
  export interface Registry {
24
- list(): DemoEntry[];
25
- getById(id: string): DemoEntry | undefined;
26
- getBySlug(slug: string): DemoEntry | undefined;
27
- getByRepoPath(repoPath: string): DemoEntry | undefined;
28
- getByRepoPathAndDemoPath(repoPath: string, demoPath: string): DemoEntry | undefined;
29
- upsert(input: RegisterInput): DemoEntry;
24
+ list(): FlowEntry[];
25
+ getById(id: string): FlowEntry | undefined;
26
+ getBySlug(slug: string): FlowEntry | undefined;
27
+ getByRepoPath(repoPath: string): FlowEntry | undefined;
28
+ getByRepoPathAndArchitecturePath(
29
+ repoPath: string,
30
+ architecturePath: string,
31
+ ): FlowEntry | undefined;
32
+ upsert(input: RegisterInput): FlowEntry;
30
33
  remove(id: string): boolean;
31
34
  }
32
35
 
@@ -44,7 +47,7 @@ export function slugify(name: string): string {
44
47
 
45
48
  export function createRegistry(options: { path?: string } = {}): Registry {
46
49
  const path = options.path ?? defaultRegistryPath();
47
- const entries = new Map<string, DemoEntry>();
50
+ const entries = new Map<string, FlowEntry>();
48
51
 
49
52
  if (existsSync(path)) {
50
53
  try {
@@ -57,7 +60,13 @@ export function createRegistry(options: { path?: string } = {}): Registry {
57
60
  typeof e.slug === 'string' &&
58
61
  typeof e.repoPath === 'string'
59
62
  ) {
60
- entries.set(e.id, e as DemoEntry);
63
+ if (typeof e.architecturePath !== 'string') {
64
+ console.warn(
65
+ `[registry] ignoring legacy entry ${e.id} (${e.slug}) — pre-split format, please re-register`,
66
+ );
67
+ continue;
68
+ }
69
+ entries.set(e.id, e as FlowEntry);
61
70
  }
62
71
  }
63
72
  }
@@ -71,16 +80,19 @@ export function createRegistry(options: { path?: string } = {}): Registry {
71
80
  writeFileSync(path, JSON.stringify([...entries.values()], null, 2));
72
81
  };
73
82
 
74
- const findByRepoPath = (repoPath: string): DemoEntry | undefined => {
83
+ const findByRepoPath = (repoPath: string): FlowEntry | undefined => {
75
84
  for (const e of entries.values()) {
76
85
  if (e.repoPath === repoPath) return e;
77
86
  }
78
87
  return undefined;
79
88
  };
80
89
 
81
- const findByRepoPathAndDemoPath = (repoPath: string, demoPath: string): DemoEntry | undefined => {
90
+ const findByRepoPathAndArchitecturePath = (
91
+ repoPath: string,
92
+ architecturePath: string,
93
+ ): FlowEntry | undefined => {
82
94
  for (const e of entries.values()) {
83
- if (e.repoPath === repoPath && e.demoPath === demoPath) return e;
95
+ if (e.repoPath === repoPath && e.architecturePath === architecturePath) return e;
84
96
  }
85
97
  return undefined;
86
98
  };
@@ -98,16 +110,16 @@ export function createRegistry(options: { path?: string } = {}): Registry {
98
110
  getById: (id) => entries.get(id),
99
111
  getBySlug: (slug) => [...entries.values()].find((e) => e.slug === slug),
100
112
  getByRepoPath: findByRepoPath,
101
- getByRepoPathAndDemoPath: findByRepoPathAndDemoPath,
113
+ getByRepoPathAndArchitecturePath: findByRepoPathAndArchitecturePath,
102
114
  upsert(input) {
103
115
  const lastModified = input.lastModified ?? Date.now();
104
116
  const valid = input.valid ?? true;
105
- const existing = findByRepoPathAndDemoPath(input.repoPath, input.demoPath);
117
+ const existing = findByRepoPathAndArchitecturePath(input.repoPath, input.architecturePath);
106
118
  if (existing) {
107
- const updated: DemoEntry = {
119
+ const updated: FlowEntry = {
108
120
  ...existing,
109
121
  name: input.name,
110
- demoPath: input.demoPath,
122
+ architecturePath: input.architecturePath,
111
123
  lastModified,
112
124
  valid,
113
125
  };
@@ -117,12 +129,12 @@ export function createRegistry(options: { path?: string } = {}): Registry {
117
129
  }
118
130
  const id = crypto.randomUUID();
119
131
  const slug = uniqueSlug(slugify(input.name));
120
- const entry: DemoEntry = {
132
+ const entry: FlowEntry = {
121
133
  id,
122
134
  slug,
123
135
  name: input.name,
124
136
  repoPath: input.repoPath,
125
- demoPath: input.demoPath,
137
+ architecturePath: input.architecturePath,
126
138
  lastModified,
127
139
  valid,
128
140
  };
package/src/schema.ts CHANGED
@@ -396,21 +396,21 @@ const ConnectorSchema = z.discriminatedUnion('kind', [
396
396
  DefaultConnectorSchema,
397
397
  ]);
398
398
 
399
- export const DemoSchema = z
399
+ export const FlowSchema = z
400
400
  .object({
401
- version: z.literal(1),
401
+ version: z.literal(2),
402
402
  name: z.string().min(1),
403
403
  nodes: z.array(NodeSchema),
404
404
  connectors: z.array(ConnectorSchema),
405
405
  // Optional one-shot script the studio runs when the user clicks Restart.
406
406
  // Lets the running app wipe its own in-memory state. The studio kills
407
- // every live play + status script for the demo BEFORE invoking this
407
+ // every live play + status script for the flow BEFORE invoking this
408
408
  // script (US-008), so the script sees no stragglers.
409
409
  resetAction: ResetActionSchema.optional(),
410
410
  })
411
- .superRefine((demo, ctx) => {
412
- const nodeIds = new Set(demo.nodes.map((n) => n.id));
413
- demo.connectors.forEach((c, idx) => {
411
+ .superRefine((flow, ctx) => {
412
+ const nodeIds = new Set(flow.nodes.map((n) => n.id));
413
+ flow.connectors.forEach((c, idx) => {
414
414
  if (!nodeIds.has(c.source)) {
415
415
  ctx.addIssue({
416
416
  code: z.ZodIssueCode.custom,
@@ -428,8 +428,8 @@ export const DemoSchema = z
428
428
  });
429
429
  });
430
430
 
431
- export type Demo = z.infer<typeof DemoSchema>;
432
- export type DemoNode = z.infer<typeof NodeSchema>;
431
+ export type Flow = z.infer<typeof FlowSchema>;
432
+ export type FlowNode = z.infer<typeof NodeSchema>;
433
433
  export type ShapeNode = z.infer<typeof ShapeNodeSchema>;
434
434
  export type ImageNode = z.infer<typeof ImageNodeSchema>;
435
435
  export type IconNode = z.infer<typeof IconNodeSchema>;
@@ -452,3 +452,247 @@ export type StatusAction = z.infer<typeof StatusActionSchema>;
452
452
  export type StatusReport = z.infer<typeof StatusReportSchema>;
453
453
  export type ResetAction = z.infer<typeof ResetActionSchema>;
454
454
  export type StateSource = z.infer<typeof StateSourceSchema>;
455
+
456
+ // =============================================================================
457
+ // Architecture schema — pure semantic data, every visual/layout field stripped.
458
+ // What lives on disk in <project>/.seeflow/architecture.json after the split.
459
+ // =============================================================================
460
+
461
+ const ArchitectureNodeDataBaseShape = {
462
+ name: z.string().min(1),
463
+ kind: z.string().min(1),
464
+ stateSource: StateSourceSchema,
465
+ handlerModule: z.string().optional(),
466
+ icon: z.string().optional(),
467
+ ...NodeDescriptionBaseShape,
468
+ };
469
+
470
+ const ArchitecturePlayNodeDataSchema = z
471
+ .object({
472
+ ...ArchitectureNodeDataBaseShape,
473
+ playAction: PlayActionSchema,
474
+ statusAction: StatusActionSchema.optional(),
475
+ })
476
+ .strict();
477
+
478
+ const ArchitectureStateNodeDataSchema = z
479
+ .object({
480
+ ...ArchitectureNodeDataBaseShape,
481
+ playAction: PlayActionSchema.optional(),
482
+ statusAction: StatusActionSchema.optional(),
483
+ })
484
+ .strict();
485
+
486
+ const ArchitectureShapeNodeDataSchema = z
487
+ .object({
488
+ shape: ShapeKindSchema,
489
+ name: z.string().optional(),
490
+ ...NodeDescriptionBaseShape,
491
+ })
492
+ .strict();
493
+
494
+ const ArchitectureImageNodeDataSchema = z
495
+ .object({
496
+ path: z.string().min(1).refine(isCleanRelativePath, {
497
+ message: 'path must be a relative path under .seeflow/ (no absolute / traversal)',
498
+ }),
499
+ alt: z.string().optional(),
500
+ ...NodeDescriptionBaseShape,
501
+ })
502
+ .strict();
503
+
504
+ const ArchitectureIconNodeDataSchema = z
505
+ .object({
506
+ icon: z.string().min(1),
507
+ alt: z.string().optional(),
508
+ name: z.string().optional(),
509
+ ...NodeDescriptionBaseShape,
510
+ })
511
+ .strict();
512
+
513
+ const ArchitectureHtmlNodeDataSchema = z
514
+ .object({
515
+ htmlPath: z.string().min(1).refine(isCleanRelativePath, {
516
+ message: 'htmlPath must be a relative path under .seeflow/ (no absolute / traversal)',
517
+ }),
518
+ name: z.string().optional(),
519
+ icon: z.string().optional(),
520
+ ...NodeDescriptionBaseShape,
521
+ })
522
+ .strict();
523
+
524
+ const ArchitectureNodeBaseShape = {
525
+ id: z.string().min(1),
526
+ };
527
+
528
+ const ArchitectureNodeSchema = z.discriminatedUnion('type', [
529
+ z
530
+ .object({
531
+ ...ArchitectureNodeBaseShape,
532
+ type: z.literal('playNode'),
533
+ data: ArchitecturePlayNodeDataSchema,
534
+ })
535
+ .strict(),
536
+ z
537
+ .object({
538
+ ...ArchitectureNodeBaseShape,
539
+ type: z.literal('stateNode'),
540
+ data: ArchitectureStateNodeDataSchema,
541
+ })
542
+ .strict(),
543
+ z
544
+ .object({
545
+ ...ArchitectureNodeBaseShape,
546
+ type: z.literal('shapeNode'),
547
+ data: ArchitectureShapeNodeDataSchema,
548
+ })
549
+ .strict(),
550
+ z
551
+ .object({
552
+ ...ArchitectureNodeBaseShape,
553
+ type: z.literal('imageNode'),
554
+ data: ArchitectureImageNodeDataSchema,
555
+ })
556
+ .strict(),
557
+ z
558
+ .object({
559
+ ...ArchitectureNodeBaseShape,
560
+ type: z.literal('iconNode'),
561
+ data: ArchitectureIconNodeDataSchema,
562
+ })
563
+ .strict(),
564
+ z
565
+ .object({
566
+ ...ArchitectureNodeBaseShape,
567
+ type: z.literal('htmlNode'),
568
+ data: ArchitectureHtmlNodeDataSchema,
569
+ })
570
+ .strict(),
571
+ ]);
572
+
573
+ const ArchitectureConnectorBaseShape = {
574
+ id: z.string().min(1),
575
+ source: z.string().min(1),
576
+ target: z.string().min(1),
577
+ label: z.string().optional(),
578
+ };
579
+
580
+ const ArchitectureConnectorSchema = z.discriminatedUnion('kind', [
581
+ z
582
+ .object({
583
+ ...ArchitectureConnectorBaseShape,
584
+ kind: z.literal('http'),
585
+ method: HttpMethodSchema.optional(),
586
+ url: z.string().min(1).optional(),
587
+ })
588
+ .strict(),
589
+ z
590
+ .object({
591
+ ...ArchitectureConnectorBaseShape,
592
+ kind: z.literal('event'),
593
+ eventName: z.string().min(1),
594
+ })
595
+ .strict(),
596
+ z
597
+ .object({
598
+ ...ArchitectureConnectorBaseShape,
599
+ kind: z.literal('queue'),
600
+ queueName: z.string().min(1),
601
+ })
602
+ .strict(),
603
+ z
604
+ .object({
605
+ ...ArchitectureConnectorBaseShape,
606
+ kind: z.literal('default'),
607
+ })
608
+ .strict(),
609
+ ]);
610
+
611
+ export const ArchitectureSchema = z
612
+ .object({
613
+ version: z.literal(2),
614
+ name: z.string().min(1),
615
+ resetAction: ResetActionSchema.optional(),
616
+ nodes: z.array(ArchitectureNodeSchema),
617
+ connectors: z.array(ArchitectureConnectorSchema),
618
+ })
619
+ .strict()
620
+ .superRefine((arch, ctx) => {
621
+ const ids = new Set(arch.nodes.map((n) => n.id));
622
+ arch.connectors.forEach((c, idx) => {
623
+ if (!ids.has(c.source)) {
624
+ ctx.addIssue({
625
+ code: z.ZodIssueCode.custom,
626
+ path: ['connectors', idx, 'source'],
627
+ message: `Connector ${c.id} references unknown source node: ${c.source}`,
628
+ });
629
+ }
630
+ if (!ids.has(c.target)) {
631
+ ctx.addIssue({
632
+ code: z.ZodIssueCode.custom,
633
+ path: ['connectors', idx, 'target'],
634
+ message: `Connector ${c.id} references unknown target node: ${c.target}`,
635
+ });
636
+ }
637
+ });
638
+ });
639
+
640
+ export type Architecture = z.infer<typeof ArchitectureSchema>;
641
+ export type ArchitectureNode = z.infer<typeof ArchitectureNodeSchema>;
642
+ export type ArchitectureConnector = z.infer<typeof ArchitectureConnectorSchema>;
643
+
644
+ // =============================================================================
645
+ // Style schema — keyed map of presentation overrides, side-table by id.
646
+ // What lives on disk in <project>/.seeflow/style.json (optional file).
647
+ // =============================================================================
648
+
649
+ const NodeStyleSchema = z
650
+ .object({
651
+ position: PositionSchema.optional(),
652
+ width: z.number().positive().optional(),
653
+ height: z.number().positive().optional(),
654
+ borderColor: ColorTokenSchema.optional(),
655
+ backgroundColor: ColorTokenSchema.optional(),
656
+ borderSize: z.number().positive().optional(),
657
+ borderStyle: z.enum(['solid', 'dashed', 'dotted']).optional(),
658
+ fontSize: z.number().positive().optional(),
659
+ textColor: ColorTokenSchema.optional(),
660
+ cornerRadius: z.number().min(0).optional(),
661
+ locked: z.boolean().optional(),
662
+ // imageNode-specific
663
+ borderWidth: z.number().min(1).max(8).optional(),
664
+ // iconNode-specific
665
+ color: ColorTokenSchema.optional(),
666
+ strokeWidth: z.number().min(0.5).max(4).optional(),
667
+ // htmlNode-specific
668
+ autoSize: z.boolean().optional(),
669
+ })
670
+ .strict();
671
+
672
+ const ConnectorStyleEntrySchema = z
673
+ .object({
674
+ sourceHandle: SourceHandleIdSchema.optional(),
675
+ targetHandle: TargetHandleIdSchema.optional(),
676
+ sourceHandleAutoPicked: z.boolean().optional(),
677
+ targetHandleAutoPicked: z.boolean().optional(),
678
+ sourcePin: EdgePinSchema.optional(),
679
+ targetPin: EdgePinSchema.optional(),
680
+ style: ConnectorStyleSchema.optional(),
681
+ color: ColorTokenSchema.optional(),
682
+ direction: ConnectorDirectionSchema.optional(),
683
+ borderSize: z.number().positive().optional(),
684
+ path: ConnectorPathSchema.optional(),
685
+ fontSize: z.number().positive().optional(),
686
+ })
687
+ .strict();
688
+
689
+ export const StyleSchema = z
690
+ .object({
691
+ nodes: z.record(z.string(), NodeStyleSchema).optional(),
692
+ connectors: z.record(z.string(), ConnectorStyleEntrySchema).optional(),
693
+ })
694
+ .strict();
695
+
696
+ export type Style = z.infer<typeof StyleSchema>;
697
+ export type NodeStyle = z.infer<typeof NodeStyleSchema>;
698
+ export type ConnectorStyleEntry = z.infer<typeof ConnectorStyleEntrySchema>;
@@ -20,7 +20,7 @@ const readEnv = (key: string): string | undefined => {
20
20
  };
21
21
 
22
22
  export async function emit(
23
- demoId: string,
23
+ flowId: string,
24
24
  nodeId: string,
25
25
  status: EmitStatus,
26
26
  opts: EmitOptions = {},
@@ -31,7 +31,7 @@ export async function emit(
31
31
  await fetch(\`\${base}/api/emit\`, {
32
32
  method: 'POST',
33
33
  headers: { 'content-type': 'application/json' },
34
- body: JSON.stringify({ demoId, nodeId, status, runId: opts.runId, payload: opts.payload }),
34
+ body: JSON.stringify({ flowId, nodeId, status, runId: opts.runId, payload: opts.payload }),
35
35
  });
36
36
  }
37
37
  `;
package/src/sdk-writer.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import type { Demo } from './schema.ts';
3
+ import type { Flow } from './schema.ts';
4
4
  import { EMIT_TEMPLATE } from './sdk-template.ts';
5
5
 
6
6
  export type SdkWriteOutcome = 'skipped' | 'written' | 'present';
@@ -16,7 +16,7 @@ export interface SdkWriteResult {
16
16
  * node with `stateSource.kind === 'event'`. Idempotent: existing files are
17
17
  * never overwritten. The only place M1's CLI mutates a user repo.
18
18
  */
19
- export function writeSdkEmitIfNeeded(repoPath: string, demo: Demo): SdkWriteResult {
19
+ export function writeSdkEmitIfNeeded(repoPath: string, demo: Flow): SdkWriteResult {
20
20
  const hasEventState = demo.nodes.some(
21
21
  (n) =>
22
22
  n.type !== 'shapeNode' &&