@tuongaz/seeflow 0.1.83 → 0.1.85

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 (73) hide show
  1. package/dist/web/assets/{architectureDiagram-3BPJPVTR-CHtgIZZ1.js → architectureDiagram-3BPJPVTR-BxjUpYZP.js} +1 -1
  2. package/dist/web/assets/{blockDiagram-GPEHLZMM-D4zjpGBX.js → blockDiagram-GPEHLZMM-BwW58gmi.js} +1 -1
  3. package/dist/web/assets/{c4Diagram-AAUBKEIU-zSpjD7ZC.js → c4Diagram-AAUBKEIU-Dzt08ghL.js} +1 -1
  4. package/dist/web/assets/channel-H6DSsj4q.js +1 -0
  5. package/dist/web/assets/{chart-D6WVIyqJ.js → chart-DpfirWPs.js} +1 -1
  6. package/dist/web/assets/{chunk-2J33WTMH-Cr7utMp9.js → chunk-2J33WTMH-Bxngez4X.js} +1 -1
  7. package/dist/web/assets/{chunk-4BX2VUAB-B6PCB6H4.js → chunk-4BX2VUAB-BOogOMBc.js} +1 -1
  8. package/dist/web/assets/{chunk-55IACEB6-D_PUAOtb.js → chunk-55IACEB6-P8EKMZOA.js} +1 -1
  9. package/dist/web/assets/{chunk-727SXJPM-DTQLQ_nx.js → chunk-727SXJPM-D2YRvYNf.js} +1 -1
  10. package/dist/web/assets/{chunk-AQP2D5EJ-OBgrFsDS.js → chunk-AQP2D5EJ-DE2NipXE.js} +1 -1
  11. package/dist/web/assets/{chunk-FMBD7UC4-Cqo28fuY.js → chunk-FMBD7UC4-DDhJtfWk.js} +1 -1
  12. package/dist/web/assets/{chunk-ND2GUHAM-DcquCeO4.js → chunk-ND2GUHAM-D3XJtrli.js} +1 -1
  13. package/dist/web/assets/{chunk-QZHKN3VN-SKsRSBcC.js → chunk-QZHKN3VN-BdMZCj9e.js} +1 -1
  14. package/dist/web/assets/classDiagram-4FO5ZUOK-BAWJcCIq.js +1 -0
  15. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-BAWJcCIq.js +1 -0
  16. package/dist/web/assets/{code-block-CQcwHam8.js → code-block-sw_8m1aF.js} +1 -1
  17. package/dist/web/assets/{cose-bilkent-S5V4N54A-Ds8DGArP.js → cose-bilkent-S5V4N54A-aEoPvgjp.js} +1 -1
  18. package/dist/web/assets/{dagre-BM42HDAG-CqMyTq3c.js → dagre-BM42HDAG-p9EyJsU9.js} +1 -1
  19. package/dist/web/assets/{diagram-2AECGRRQ-CWEIApTd.js → diagram-2AECGRRQ-D5JkQMH9.js} +1 -1
  20. package/dist/web/assets/{diagram-5GNKFQAL-8TEzgir4.js → diagram-5GNKFQAL-N-K8VEKH.js} +1 -1
  21. package/dist/web/assets/{diagram-KO2AKTUF-UNKtOw9m.js → diagram-KO2AKTUF-YYXTKlRp.js} +1 -1
  22. package/dist/web/assets/{diagram-LMA3HP47-D6QoRbi6.js → diagram-LMA3HP47-DB6IV5pI.js} +1 -1
  23. package/dist/web/assets/{diagram-OG6HWLK6-CQqkl0nk.js → diagram-OG6HWLK6-CYpsa5S_.js} +1 -1
  24. package/dist/web/assets/{erDiagram-TEJ5UH35-Clv5A-5B.js → erDiagram-TEJ5UH35-DG_ieSJb.js} +1 -1
  25. package/dist/web/assets/{flowDiagram-I6XJVG4X-4hg7rn9v.js → flowDiagram-I6XJVG4X-DhkYdddf.js} +1 -1
  26. package/dist/web/assets/{ganttDiagram-6RSMTGT7-3PAgpAEA.js → ganttDiagram-6RSMTGT7-Dch6LnJg.js} +1 -1
  27. package/dist/web/assets/{gitGraphDiagram-PVQCEYII-D5g_5z1o.js → gitGraphDiagram-PVQCEYII-BEdh0-R-.js} +1 -1
  28. package/dist/web/assets/index-2wkTDsTh.js +8619 -0
  29. package/dist/web/assets/{index-fl8DS9WO.css → index-DzEkjMbu.css} +1 -1
  30. package/dist/web/assets/{index.es-Cgv8Vcjx.js → index.es-BPbregt5.js} +1 -1
  31. package/dist/web/assets/{infoDiagram-5YYISTIA-BuFXAl4D.js → infoDiagram-5YYISTIA-Bg5TB2bn.js} +1 -1
  32. package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-DOE-GUy-.js → ishikawaDiagram-YF4QCWOH-C9wgnFPt.js} +1 -1
  33. package/dist/web/assets/{journeyDiagram-JHISSGLW-B5BXjJgy.js → journeyDiagram-JHISSGLW-DXoECOxR.js} +1 -1
  34. package/dist/web/assets/{jspdf.es.min-_GQ7N2xd.js → jspdf.es.min-D0_5WkpV.js} +3 -3
  35. package/dist/web/assets/{kanban-definition-UN3LZRKU-DTLQ2kuG.js → kanban-definition-UN3LZRKU-Cul7RbDt.js} +1 -1
  36. package/dist/web/assets/{linear-DCKgJuDe.js → linear-CZ3VwjbT.js} +1 -1
  37. package/dist/web/assets/{markdown-DQ9_ZAvJ.js → markdown-EWVEPYpm.js} +1 -1
  38. package/dist/web/assets/{mermaid.core-Cf2CUPpR.js → mermaid.core-Ceyl9DjX.js} +4 -4
  39. package/dist/web/assets/{mindmap-definition-RKZ34NQL-CI-_BCCZ.js → mindmap-definition-RKZ34NQL-DCBC1TsK.js} +1 -1
  40. package/dist/web/assets/{pieDiagram-4H26LBE5-BdTv4DaU.js → pieDiagram-4H26LBE5-CqccSO7S.js} +1 -1
  41. package/dist/web/assets/{quadrantDiagram-W4KKPZXB-pqQ7pKT1.js → quadrantDiagram-W4KKPZXB-e7-sWjUK.js} +1 -1
  42. package/dist/web/assets/{requirementDiagram-4Y6WPE33-CCP9fYhL.js → requirementDiagram-4Y6WPE33-BBwVJS4I.js} +1 -1
  43. package/dist/web/assets/{sankeyDiagram-5OEKKPKP-CpMAd0Mj.js → sankeyDiagram-5OEKKPKP-BCNgzA-3.js} +1 -1
  44. package/dist/web/assets/{sequenceDiagram-3UESZ5HK-BWg19eGL.js → sequenceDiagram-3UESZ5HK-BjjeSbs9.js} +1 -1
  45. package/dist/web/assets/{stateDiagram-AJRCARHV-BliITsKN.js → stateDiagram-AJRCARHV-tl6fq8NN.js} +1 -1
  46. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-7VEgV0SV.js +1 -0
  47. package/dist/web/assets/{time-Dqs-aHL0.js → time-BQz-ZWra.js} +1 -1
  48. package/dist/web/assets/{timeline-definition-PNZ67QCA-Bk8P84Jg.js → timeline-definition-PNZ67QCA-Cc_C7lIC.js} +1 -1
  49. package/dist/web/assets/{vennDiagram-CIIHVFJN-CMtXVM_H.js → vennDiagram-CIIHVFJN-BwgTN2w7.js} +1 -1
  50. package/dist/web/assets/{wardley-L42UT6IY-CHP-BfXr.js → wardley-L42UT6IY-CxmFlIp3.js} +1 -1
  51. package/dist/web/assets/{wardleyDiagram-YWT4CUSO-DInIu6_2.js → wardleyDiagram-YWT4CUSO-DMEdETxd.js} +1 -1
  52. package/dist/web/assets/{xychartDiagram-2RQKCTM6-B0gobaUy.js → xychartDiagram-2RQKCTM6-B2xg8rTK.js} +1 -1
  53. package/dist/web/index.html +2 -2
  54. package/package.json +1 -1
  55. package/src/api.ts +7 -112
  56. package/src/cli-helpers.ts +3 -0
  57. package/src/cli-manifest.ts +61 -15
  58. package/src/cli-ops.ts +2 -2
  59. package/src/cli.ts +75 -5
  60. package/src/events.ts +0 -1
  61. package/src/mcp.ts +2 -2
  62. package/src/merge.ts +0 -3
  63. package/src/operations.ts +77 -4
  64. package/src/project-scanner.ts +18 -0
  65. package/src/proxy.ts +1 -193
  66. package/src/schema-catalog.ts +1 -3
  67. package/src/schema.ts +0 -10
  68. package/src/server.ts +1 -2
  69. package/dist/web/assets/channel-CXa0j4Re.js +0 -1
  70. package/dist/web/assets/classDiagram-4FO5ZUOK-DGPggFxp.js +0 -1
  71. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-DGPggFxp.js +0 -1
  72. package/dist/web/assets/index-CaE1zYAX.js +0 -8619
  73. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-BCerI8Jf.js +0 -1
@@ -133,9 +133,10 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
133
133
  name: 'register',
134
134
  synopsis: 'seeflow register [--path <dir>] [--flow <file>]',
135
135
  description:
136
- 'Register a demo repo with the studio. Reads <repoPath>/<flow> (defaulting ' +
137
- 'to ./flow.json), validates the schema, and writes an entry to ' +
138
- '~/.seeflow/registry.json. Alias of flows:register.',
136
+ 'Register a demo repo with the studio. Manifest-aware: when <repoPath>/seeflow.json ' +
137
+ 'exists, scans every declared flow under flows/<id>/flow.json and upserts one entry ' +
138
+ 'per flow. Otherwise reads <repoPath>/<flow> (defaulting to ./flow.json) as a ' +
139
+ 'single-flow project. Writes to ~/.seeflow/registry.json. Alias of flows:register.',
139
140
  category: 'flows',
140
141
  args: [],
141
142
  flags: [
@@ -143,20 +144,25 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
143
144
  {
144
145
  name: 'flow',
145
146
  valuePlaceholder: '<file>',
146
- description: 'Path to flow.json relative to repo root (default: flow.json)',
147
+ description:
148
+ 'Path to flow.json relative to repo root (default: flow.json). Ignored when seeflow.json exists.',
147
149
  },
148
150
  ],
149
151
  outputs: {
150
152
  okExample: { id: 'abc12345', slug: 'checkout' },
151
- errorKinds: ['fileNotFound', 'badJson', 'badSchema'],
153
+ errorKinds: ['fileNotFound', 'badJson', 'badSchema', 'manifestInvalid'],
152
154
  },
153
155
  requiresStudio: false,
154
- examples: ['seeflow register', 'seeflow register --path ./my-app'],
156
+ examples: [
157
+ 'seeflow register',
158
+ 'seeflow register --path ./my-app',
159
+ 'seeflow register --path ./checkout # manifest-aware re-scan',
160
+ ],
155
161
  },
156
162
  {
157
163
  name: 'flows:register',
158
164
  synopsis: 'seeflow flows:register [--path <dir>] [--flow <file>]',
159
- description: 'Register a demo repo. Identical behaviour to `register`.',
165
+ description: 'Register a demo repo. Identical behaviour to `register` (manifest-aware).',
160
166
  category: 'flows',
161
167
  args: [],
162
168
  flags: [
@@ -164,27 +170,40 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
164
170
  {
165
171
  name: 'flow',
166
172
  valuePlaceholder: '<file>',
167
- description: 'Path to flow.json relative to repo root (default: flow.json)',
173
+ description:
174
+ 'Path to flow.json relative to repo root (default: flow.json). Ignored when seeflow.json exists.',
168
175
  },
169
176
  ],
170
177
  body: { schemaRef: 'RegisterBody' },
171
178
  outputs: {
172
179
  okExample: { id: 'abc12345', slug: 'checkout' },
173
- errorKinds: ['fileNotFound', 'badJson', 'badSchema'],
180
+ errorKinds: ['fileNotFound', 'badJson', 'badSchema', 'manifestInvalid'],
174
181
  },
175
182
  requiresStudio: false,
176
183
  examples: ['seeflow flows:register --path ./my-app'],
177
184
  },
178
185
  {
179
186
  name: 'flows:list',
180
- synopsis: 'seeflow flows:list',
181
- description: 'List every registered flow with id, slug, name, repoPath, and valid flag.',
187
+ synopsis: 'seeflow flows:list [--project <p>]',
188
+ description:
189
+ 'List every registered flow with id, slug, name, repoPath, and valid flag. With ' +
190
+ '--project <p>, filters to one project and returns { projectSlug, flows: [{ flowSlug, ' +
191
+ 'name, icon?, isDefault, valid }] }.',
182
192
  category: 'flows',
183
193
  args: [],
184
- flags: [],
185
- outputs: { okExample: { flows: [{ id: 'abc12345', slug: 'checkout', name: 'Checkout' }] } },
194
+ flags: [
195
+ {
196
+ name: 'project',
197
+ valuePlaceholder: '<p>',
198
+ description: 'Project slug — filters output to flows under this project',
199
+ },
200
+ ],
201
+ outputs: {
202
+ okExample: { flows: [{ id: 'abc12345', slug: 'checkout', name: 'Checkout' }] },
203
+ errorKinds: ['projectNotFound'],
204
+ },
186
205
  requiresStudio: false,
187
- examples: ['seeflow flows:list'],
206
+ examples: ['seeflow flows:list', 'seeflow flows:list --project order-pipeline'],
188
207
  },
189
208
  {
190
209
  name: 'flows:summary',
@@ -488,6 +507,33 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
488
507
  'seeflow projects:create --path ./checkout --name "Checkout" --description "Cart + payments flow"',
489
508
  ],
490
509
  },
510
+ {
511
+ name: 'projects:list',
512
+ synopsis: 'seeflow projects:list',
513
+ description:
514
+ 'List every registered project: groups the registry by projectSlug and reads each ' +
515
+ 'seeflow.json for the human-readable name + defaultFlow. Falls back to the ' +
516
+ "projectSlug and the registry's isDefault entry when a manifest is missing or " +
517
+ 'malformed, so a partially-broken project still surfaces.',
518
+ category: 'project',
519
+ args: [],
520
+ flags: [],
521
+ outputs: {
522
+ okExample: {
523
+ projects: [
524
+ {
525
+ projectSlug: 'checkout',
526
+ name: 'Checkout',
527
+ defaultFlow: 'main',
528
+ flowCount: 2,
529
+ repoPath: '/abs/path/to/.seeflow/checkout',
530
+ },
531
+ ],
532
+ },
533
+ },
534
+ requiresStudio: false,
535
+ examples: ['seeflow projects:list'],
536
+ },
491
537
  // ---- nodes -------------------------------------------------------------
492
538
  {
493
539
  name: 'nodes:add',
@@ -860,7 +906,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
860
906
  'Optional named schema within the category — e.g. for `node`: ' +
861
907
  'rectangle, ellipse, sticky, text, database, server, user, queue, ' +
862
908
  'cloud, image, html, icon, component. For `action`: playAction, ' +
863
- 'statusAction, resetAction, statusReport, componentAction. For ' +
909
+ 'statusAction, statusReport, componentAction. For ' +
864
910
  '`componentSpec`: componentSpec, componentSpecElement.',
865
911
  },
866
912
  ],
package/src/cli-ops.ts CHANGED
@@ -10,8 +10,8 @@ import {
10
10
  /**
11
11
  * Build a single Operations handle for in-process CLI use.
12
12
  *
13
- * The CLI has no watcher and no statusRunner — play/reset are server-only
14
- * features that still go via HTTP. When a CLI mutates a flow file, the
13
+ * The CLI has no watcher and no statusRunner — play is server-only
14
+ * and still goes via HTTP. When a CLI mutates a flow file, the
15
15
  * running studio's flow watcher picks up the disk write externally and
16
16
  * broadcasts flow:reload to connected SPA clients.
17
17
  *
package/src/cli.ts CHANGED
@@ -178,6 +178,8 @@ if (argv.includes('--version') || argv.includes('-v')) {
178
178
  await runRegister();
179
179
  } else if (sub === 'projects:create') {
180
180
  await runProjectsCreate();
181
+ } else if (sub === 'projects:list') {
182
+ await runProjectsList();
181
183
  } else if (sub === 'flows:list') {
182
184
  await runFlowsList();
183
185
  } else if (sub === 'flows:summary') {
@@ -243,10 +245,11 @@ Usage:
243
245
  Commands (work without a running studio):
244
246
  start Start the SeeFlow Studio server (default port 4321) — default when no command is given
245
247
  stop Stop a background studio instance
246
- register Register a demo repo, writing to ~/.seeflow/registry.json (alias of flows:register)
247
- flows:register Register a demo repo
248
+ register Register a demo repo. Manifest-aware: when <repoPath>/seeflow.json exists, re-scans every declared flow; otherwise reads <repoPath>/<flow> (defaults to flow.json) as a single-flow project (alias of flows:register)
249
+ flows:register Register a demo repo (manifest-aware — same behaviour as register)
248
250
  projects:create Scaffold a new project (writes <path>/seeflow.json + <path>/flows/main/flow.json) — (--path <dir> --name <name> [--description <text>])
249
- flows:list List registered flows
251
+ projects:list List every registered project with projectSlug, name, defaultFlow, flowCount
252
+ flows:list List registered flows. With --project <p>, filters to one project (returns flowSlug, name, icon?, isDefault per flow)
250
253
  flows:summary List registered flows (id + name + description only)
251
254
  flows:get Get flow details (--project <p> --flow <f>)
252
255
  flows:graph List nodes + connectors without inlined file content (--project <p> --flow <f>)
@@ -594,12 +597,24 @@ function isEsrch(err: unknown): boolean {
594
597
 
595
598
  async function runRegister() {
596
599
  const repoPath = resolve(flagValue('path') ?? '.');
597
- const demoPathArg = flagValue('flow') ?? DEFAULT_FLOW_PATH;
598
600
 
601
+ // Manifest-aware path: if <repoPath>/seeflow.json is on disk, scan and
602
+ // upsert every declared flow in one shot.
603
+ if (existsSync(join(repoPath, 'seeflow.json'))) {
604
+ await runRegisterManifest(repoPath);
605
+ return;
606
+ }
607
+
608
+ // Legacy single-flow path: pre-manifest projects (and skill tests that
609
+ // exercise registerFlow directly) still pass a bare flow.json at the
610
+ // root. Read it, schema-validate, upsert one entry.
611
+ const demoPathArg = flagValue('flow') ?? DEFAULT_FLOW_PATH;
599
612
  const fullPath = isAbsolute(demoPathArg) ? demoPathArg : join(repoPath, demoPathArg);
600
613
  if (!existsSync(fullPath)) {
601
614
  console.error(`No demo file at ${fullPath}`);
602
- console.error(`Create ${DEFAULT_FLOW_PATH} in your repo, or pass --flow <path>.`);
615
+ console.error(
616
+ `Create ${DEFAULT_FLOW_PATH} in your repo, or pass --flow <path>. For manifest-driven projects, place a seeflow.json at the repo root.`,
617
+ );
603
618
  process.exit(1);
604
619
  }
605
620
 
@@ -643,6 +658,48 @@ async function runRegister() {
643
658
  }
644
659
  }
645
660
 
661
+ async function runRegisterManifest(repoPath: string) {
662
+ const outcome = registerProject({ repoPath });
663
+ if (outcome.kind !== 'ok') {
664
+ switch (outcome.kind) {
665
+ case 'manifest-invalid':
666
+ console.error(`${join(repoPath, 'seeflow.json')} is invalid: ${outcome.message}`);
667
+ process.exit(2);
668
+ break;
669
+ case 'manifest-missing':
670
+ // Defensive — runRegister gated on existsSync, so this should not fire.
671
+ console.error(`No seeflow.json at ${repoPath}`);
672
+ process.exit(3);
673
+ break;
674
+ case 'legacy-root-flow':
675
+ console.error(
676
+ `${repoPath} has a legacy root flow.json but no seeflow.json. Migrate it into the new flows/<id>/ layout before re-registering.`,
677
+ );
678
+ process.exit(3);
679
+ break;
680
+ case 'flow-json-missing':
681
+ console.error(
682
+ `Manifest declares flow "${outcome.flowId}" but ${outcome.flowPath} is missing.`,
683
+ );
684
+ process.exit(3);
685
+ break;
686
+ }
687
+ process.exit(1);
688
+ }
689
+
690
+ const pid = readPid();
691
+ const live = pid !== undefined && isPidAlive(pid);
692
+ const overrideUrl = process.env.SEEFLOW_STUDIO_URL?.replace(/\/+$/, '');
693
+ const baseUrl = live ? (overrideUrl ?? studioUrl(readConfig())) : null;
694
+
695
+ for (const entry of outcome.entries) {
696
+ const tail = baseUrl
697
+ ? ` → ${baseUrl}/projects/${outcome.projectSlug}/flows/${entry.flowSlug}`
698
+ : ` (slug: ${entry.slug})`;
699
+ console.log(`Registered "${entry.name}"${tail}`);
700
+ }
701
+ }
702
+
646
703
  async function ensureStudioRunning(url: string, port: number, noStart: boolean) {
647
704
  if (await healthOk(url)) return;
648
705
 
@@ -709,10 +766,23 @@ async function runProjectsCreate() {
709
766
 
710
767
  async function runFlowsList() {
711
768
  const ops = createCliOperations();
769
+ const project = flagValue('project');
770
+ if (project) {
771
+ const result = ops.listFlowsByProject(project);
772
+ if (result.kind !== 'ok') printOutcome(result);
773
+ printOk({ projectSlug: result.data.projectSlug, flows: result.data.flows });
774
+ return;
775
+ }
712
776
  const result = ops.listFlows();
713
777
  printOk({ flows: result.data });
714
778
  }
715
779
 
780
+ function runProjectsList() {
781
+ const ops = createCliOperations();
782
+ const result = ops.listProjects();
783
+ printOk({ projects: result.data });
784
+ }
785
+
716
786
  async function runFlowsSummary() {
717
787
  const ops = createCliOperations();
718
788
  const result = ops.listFlowsSummary();
package/src/events.ts CHANGED
@@ -6,7 +6,6 @@
6
6
 
7
7
  export type StudioEventType =
8
8
  | 'flow:reload'
9
- | 'demo:reset'
10
9
  | 'node:running'
11
10
  | 'node:done'
12
11
  | 'node:error'
package/src/mcp.ts CHANGED
@@ -295,7 +295,7 @@ const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
295
295
  'spec, or flow envelope looks like before authoring writes. Categories: ' +
296
296
  '`flow`, `node` (13 flat variants — rectangle/ellipse/sticky/text/' +
297
297
  'database/server/user/queue/cloud/image/html/icon/component), ' +
298
- '`connector`, `action` (playAction/statusAction/resetAction/statusReport/' +
298
+ '`connector`, `action` (playAction/statusAction/statusReport/' +
299
299
  "componentAction), `componentSpec` (sidecar shape for type:'component' " +
300
300
  'nodes), `style`.',
301
301
  inputSchema: {
@@ -311,7 +311,7 @@ const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
311
311
  'Optional named schema within the category (requires `name`). For ' +
312
312
  "name='node': rectangle, ellipse, sticky, text, database, server, " +
313
313
  'user, queue, cloud, image, html, icon, component. For ' +
314
- "name='action': playAction, statusAction, resetAction, statusReport, " +
314
+ "name='action': playAction, statusAction, statusReport, " +
315
315
  "componentAction. For name='componentSpec': componentSpec, " +
316
316
  'componentSpecElement.',
317
317
  },
package/src/merge.ts CHANGED
@@ -32,7 +32,6 @@ export function mergeFlowAndStyle(flow: Flow, style: Style): ResolvedFlow {
32
32
  version: flow.version,
33
33
  name: flow.name,
34
34
  ...(flow.description !== undefined ? { description: flow.description } : {}),
35
- ...(flow.resetAction ? { resetAction: flow.resetAction } : {}),
36
35
  nodes: mergedNodes,
37
36
  connectors: mergedConnectors,
38
37
  } as ResolvedFlow;
@@ -114,7 +113,6 @@ export function splitFlow(resolved: {
114
113
  version: number;
115
114
  name: string;
116
115
  description?: string;
117
- resetAction?: unknown;
118
116
  nodes: Array<Record<string, unknown>>;
119
117
  connectors: Array<Record<string, unknown>>;
120
118
  }): { flow: Record<string, unknown>; style: Record<string, unknown> } {
@@ -187,7 +185,6 @@ export function splitFlow(resolved: {
187
185
  connectors: flowConnectors,
188
186
  };
189
187
  if (resolved.description !== undefined) flow.description = resolved.description;
190
- if (resolved.resetAction !== undefined) flow.resetAction = resolved.resetAction;
191
188
 
192
189
  const style: Record<string, unknown> = {};
193
190
  if (Object.keys(styleNodes).length > 0) style.nodes = styleNodes;
package/src/operations.ts CHANGED
@@ -22,8 +22,8 @@ import {
22
22
  removeNodeDir,
23
23
  writeNodeFile,
24
24
  } from './node-files.ts';
25
- import { scanProject } from './project-scanner.ts';
26
- import { type Registry, slugify } from './registry.ts';
25
+ import { readProjectManifest, scanProject } from './project-scanner.ts';
26
+ import { type FlowEntry, type Registry, slugify } from './registry.ts';
27
27
  import {
28
28
  ColorTokenSchema,
29
29
  ComponentSpecSchema,
@@ -404,6 +404,31 @@ export interface CreateProjectSuccess {
404
404
 
405
405
  export type ListFlowsOutcome = { kind: 'ok'; data: FlowListItem[] };
406
406
 
407
+ export interface ProjectListItem {
408
+ projectSlug: string;
409
+ name: string;
410
+ description?: string;
411
+ defaultFlow: string;
412
+ flowCount: number;
413
+ repoPath: string;
414
+ }
415
+
416
+ export type ListProjectsOutcome = { kind: 'ok'; data: ProjectListItem[] };
417
+
418
+ export interface ProjectFlowListItem {
419
+ id: string;
420
+ slug: string;
421
+ flowSlug: string;
422
+ name: string;
423
+ icon?: string;
424
+ isDefault: boolean;
425
+ valid: boolean;
426
+ }
427
+
428
+ export type ListFlowsByProjectOutcome =
429
+ | { kind: 'ok'; data: { projectSlug: string; flows: ProjectFlowListItem[] } }
430
+ | { kind: 'projectNotFound'; projectSlug: string };
431
+
407
432
  // Minimal projection for agent/CLI discovery — `description` and `name` come
408
433
  // from the live watcher snapshot when available so author edits to flow.json
409
434
  // surface immediately; fall back to the registry value at startup before
@@ -737,7 +762,6 @@ function readRawFlowAndStyle(flowPath: string): ReadRawResult {
737
762
  type MutateMergedFlowMutator<E> = (flow: {
738
763
  version: number;
739
764
  name: string;
740
- resetAction?: unknown;
741
765
  nodes: Array<Record<string, unknown>>;
742
766
  connectors: Array<Record<string, unknown>>;
743
767
  }) => { kind: 'ok' } | E;
@@ -783,7 +807,6 @@ export async function mutateMergedFlow<E extends { kind: string }>(
783
807
  const merged = inlinedFlow as unknown as {
784
808
  version: number;
785
809
  name: string;
786
- resetAction?: unknown;
787
810
  nodes: Array<Record<string, unknown>>;
788
811
  connectors: Array<Record<string, unknown>>;
789
812
  };
@@ -951,6 +974,52 @@ export function listDemosImpl(deps: OperationsDeps): ListFlowsOutcome {
951
974
  return { kind: 'ok', data };
952
975
  }
953
976
 
977
+ export function listProjectsImpl(deps: OperationsDeps): ListProjectsOutcome {
978
+ const grouped = new Map<string, FlowEntry[]>();
979
+ for (const entry of deps.registry.list()) {
980
+ const existing = grouped.get(entry.projectSlug);
981
+ if (existing) existing.push(entry);
982
+ else grouped.set(entry.projectSlug, [entry]);
983
+ }
984
+ const data: ProjectListItem[] = [];
985
+ for (const [projectSlug, entries] of grouped) {
986
+ const head = entries[0];
987
+ if (!head) continue;
988
+ const manifest = readProjectManifest(head.repoPath);
989
+ const defaultEntry = entries.find((e) => e.isDefault) ?? head;
990
+ data.push({
991
+ projectSlug,
992
+ name: manifest?.name ?? projectSlug,
993
+ ...(manifest?.description !== undefined ? { description: manifest.description } : {}),
994
+ defaultFlow: manifest?.defaultFlow ?? defaultEntry.flowSlug,
995
+ flowCount: entries.length,
996
+ repoPath: head.repoPath,
997
+ });
998
+ }
999
+ return { kind: 'ok', data };
1000
+ }
1001
+
1002
+ export function listFlowsByProjectImpl(
1003
+ deps: OperationsDeps,
1004
+ projectSlug: string,
1005
+ ): ListFlowsByProjectOutcome {
1006
+ const entries = deps.registry.list().filter((e) => e.projectSlug === projectSlug);
1007
+ if (entries.length === 0) return { kind: 'projectNotFound', projectSlug };
1008
+ const flows: ProjectFlowListItem[] = entries.map((e) => {
1009
+ const fileExists = existsSync(resolveFilePath(e.repoPath, e.flowPath));
1010
+ return {
1011
+ id: e.id,
1012
+ slug: e.slug,
1013
+ flowSlug: e.flowSlug,
1014
+ name: e.name,
1015
+ ...(e.icon !== undefined ? { icon: e.icon } : {}),
1016
+ isDefault: e.isDefault,
1017
+ valid: e.valid && fileExists,
1018
+ };
1019
+ });
1020
+ return { kind: 'ok', data: { projectSlug, flows } };
1021
+ }
1022
+
954
1023
  export function listFlowsSummaryImpl(deps: OperationsDeps): ListFlowsSummaryOutcome {
955
1024
  const { registry, watcher } = deps;
956
1025
  const data = registry.list().map((e) => {
@@ -1899,6 +1968,8 @@ export async function applyLayoutImpl(
1899
1968
  export interface Operations {
1900
1969
  listFlows(): ReturnType<typeof listDemosImpl>;
1901
1970
  listFlowsSummary(): ReturnType<typeof listFlowsSummaryImpl>;
1971
+ listProjects(): ReturnType<typeof listProjectsImpl>;
1972
+ listFlowsByProject(projectSlug: string): ReturnType<typeof listFlowsByProjectImpl>;
1902
1973
  getFlow(id: string): ReturnType<typeof getFlowImpl>;
1903
1974
  getFlowGraph(id: string): ReturnType<typeof getFlowGraphImpl>;
1904
1975
  getNode(flowId: string, nodeId: string): ReturnType<typeof getNodeImpl>;
@@ -1946,6 +2017,8 @@ export function createOperations(deps: OperationsDeps): Operations {
1946
2017
  return {
1947
2018
  listFlows: () => listDemosImpl(deps),
1948
2019
  listFlowsSummary: () => listFlowsSummaryImpl(deps),
2020
+ listProjects: () => listProjectsImpl(deps),
2021
+ listFlowsByProject: (projectSlug) => listFlowsByProjectImpl(deps, projectSlug),
1949
2022
  getFlow: (id) => getFlowImpl(deps, id),
1950
2023
  getFlowGraph: (id) => getFlowGraphImpl(deps, id),
1951
2024
  getNode: (flowId, nodeId) => getNodeImpl(deps, flowId, nodeId),
@@ -47,6 +47,24 @@ export type ScanResult =
47
47
  const MANIFEST_FILENAME = 'seeflow.json';
48
48
  const LEGACY_FLOW_FILENAME = 'flow.json';
49
49
 
50
+ /**
51
+ * Best-effort manifest read for listing routes / CLI listing verbs.
52
+ * Returns `null` when the manifest is missing or malformed — callers fall
53
+ * back to derived defaults (projectSlug, isDefault entry) so one broken
54
+ * project does not collapse the whole listing.
55
+ */
56
+ export function readProjectManifest(repoPath: string): SeeflowManifest | null {
57
+ const manifestPath = join(repoPath, MANIFEST_FILENAME);
58
+ if (!existsSync(manifestPath)) return null;
59
+ try {
60
+ const raw = JSON.parse(readFileSync(manifestPath, 'utf8'));
61
+ const parsed = SeeflowManifestSchema.safeParse(raw);
62
+ return parsed.success ? parsed.data : null;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
50
68
  export function scanProject(repoPath: string): ScanResult {
51
69
  const manifestPath = join(repoPath, MANIFEST_FILENAME);
52
70
  if (!existsSync(manifestPath)) {