@tuongaz/seeflow 0.1.97 → 0.1.99

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 (84) hide show
  1. package/README.md +21 -0
  2. package/dist/web/assets/{architectureDiagram-3BPJPVTR-XQzgHME4.js → architectureDiagram-3BPJPVTR-B-zze1dQ.js} +1 -1
  3. package/dist/web/assets/{blockDiagram-GPEHLZMM-D79pgdno.js → blockDiagram-GPEHLZMM-DdXfGidV.js} +1 -1
  4. package/dist/web/assets/{c4Diagram-AAUBKEIU-DdpMasob.js → c4Diagram-AAUBKEIU-D1bZOhXl.js} +1 -1
  5. package/dist/web/assets/channel-BKgs5-7F.js +1 -0
  6. package/dist/web/assets/{chart-BS3qBv6b.js → chart-Dg9qryEw.js} +1 -1
  7. package/dist/web/assets/{chunk-2J33WTMH-DMiLaW3V.js → chunk-2J33WTMH-DerfLWVn.js} +1 -1
  8. package/dist/web/assets/{chunk-4BX2VUAB-BxRQSTSU.js → chunk-4BX2VUAB-3NxUSAZt.js} +1 -1
  9. package/dist/web/assets/{chunk-55IACEB6-B8VO9ECP.js → chunk-55IACEB6-DaUPCrIV.js} +1 -1
  10. package/dist/web/assets/{chunk-727SXJPM-CtI4DnVU.js → chunk-727SXJPM-DFbT_Bo7.js} +1 -1
  11. package/dist/web/assets/{chunk-AQP2D5EJ-BUDEtGcc.js → chunk-AQP2D5EJ-Btrl-RqP.js} +1 -1
  12. package/dist/web/assets/{chunk-FMBD7UC4-XRBBZk8O.js → chunk-FMBD7UC4-lKFmDkFq.js} +1 -1
  13. package/dist/web/assets/{chunk-ND2GUHAM-D0exlO6X.js → chunk-ND2GUHAM-CHh_WD0m.js} +1 -1
  14. package/dist/web/assets/{chunk-QZHKN3VN-CnyiTlpq.js → chunk-QZHKN3VN-D-wu0rxE.js} +1 -1
  15. package/dist/web/assets/classDiagram-4FO5ZUOK-DCeegQEz.js +1 -0
  16. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-DCeegQEz.js +1 -0
  17. package/dist/web/assets/{code-block-CLrCA7Xe.js → code-block-fnJ45JTt.js} +1 -1
  18. package/dist/web/assets/{cose-bilkent-S5V4N54A-B4D1urlH.js → cose-bilkent-S5V4N54A-CBI67v3W.js} +1 -1
  19. package/dist/web/assets/{dagre-BM42HDAG-CNe7Uulx.js → dagre-BM42HDAG-6y6lYDbG.js} +1 -1
  20. package/dist/web/assets/{diagram-2AECGRRQ--mgpxm9o.js → diagram-2AECGRRQ-DGNDrzfp.js} +1 -1
  21. package/dist/web/assets/{diagram-5GNKFQAL-CGHMTFDB.js → diagram-5GNKFQAL-zYDwXAIN.js} +1 -1
  22. package/dist/web/assets/{diagram-KO2AKTUF-D31GLzm7.js → diagram-KO2AKTUF-DmS_BzKx.js} +1 -1
  23. package/dist/web/assets/{diagram-LMA3HP47-Bs2BLtxH.js → diagram-LMA3HP47-CJbeNPH9.js} +1 -1
  24. package/dist/web/assets/{diagram-OG6HWLK6-CXUZ873r.js → diagram-OG6HWLK6-iiJwcdIX.js} +1 -1
  25. package/dist/web/assets/{erDiagram-TEJ5UH35-DL-eedkW.js → erDiagram-TEJ5UH35-h8Y_gYmT.js} +1 -1
  26. package/dist/web/assets/{flowDiagram-I6XJVG4X-BQCu7G6G.js → flowDiagram-I6XJVG4X-Z5lwuHdZ.js} +1 -1
  27. package/dist/web/assets/{ganttDiagram-6RSMTGT7-CyBhrhQa.js → ganttDiagram-6RSMTGT7-BPEZFeN5.js} +1 -1
  28. package/dist/web/assets/{gitGraphDiagram-PVQCEYII-D9OqlmQL.js → gitGraphDiagram-PVQCEYII-sJZWgc6i.js} +1 -1
  29. package/dist/web/assets/iconify-B5rYXYrW.js +1 -0
  30. package/dist/web/assets/index-Cirud96V.js +8629 -0
  31. package/dist/web/assets/index-DCY1MDbo.css +1 -0
  32. package/dist/web/assets/{index.es-BpDX3yd0.js → index.es-CqlCK90H.js} +1 -1
  33. package/dist/web/assets/{infoDiagram-5YYISTIA-CMxwx_2B.js → infoDiagram-5YYISTIA-DSDcNKWM.js} +1 -1
  34. package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-DD1y6qVy.js → ishikawaDiagram-YF4QCWOH-Djo12Wsr.js} +1 -1
  35. package/dist/web/assets/{journeyDiagram-JHISSGLW-CHo991VZ.js → journeyDiagram-JHISSGLW-B1PQjr20.js} +1 -1
  36. package/dist/web/assets/{jspdf.es.min-C8_HZhlK.js → jspdf.es.min-EKwwYZfH.js} +3 -3
  37. package/dist/web/assets/{kanban-definition-UN3LZRKU-CoYkI8Ob.js → kanban-definition-UN3LZRKU-YeErCOhe.js} +1 -1
  38. package/dist/web/assets/{linear-CQGcGLyB.js → linear-D5FHHJNO.js} +1 -1
  39. package/dist/web/assets/{markdown-Bud9JO0j.js → markdown-CuvYg4wg.js} +1 -1
  40. package/dist/web/assets/{mermaid.core-1G8gw6AK.js → mermaid.core-8qjHlhCB.js} +4 -4
  41. package/dist/web/assets/{mindmap-definition-RKZ34NQL-CJHnwtSU.js → mindmap-definition-RKZ34NQL-DpUz-nrq.js} +1 -1
  42. package/dist/web/assets/{pieDiagram-4H26LBE5-CXrXwuPG.js → pieDiagram-4H26LBE5-CYCE9du8.js} +1 -1
  43. package/dist/web/assets/{quadrantDiagram-W4KKPZXB-BVJKIfMF.js → quadrantDiagram-W4KKPZXB-x8WbTOtV.js} +1 -1
  44. package/dist/web/assets/{requirementDiagram-4Y6WPE33-ZFgLHB2Y.js → requirementDiagram-4Y6WPE33-DIcJaM60.js} +1 -1
  45. package/dist/web/assets/{sankeyDiagram-5OEKKPKP-cP9rHdFK.js → sankeyDiagram-5OEKKPKP-CwbrztT0.js} +1 -1
  46. package/dist/web/assets/{sequenceDiagram-3UESZ5HK-BbruCi6T.js → sequenceDiagram-3UESZ5HK-CWfsvCJO.js} +1 -1
  47. package/dist/web/assets/{stateDiagram-AJRCARHV-CqGbTDXI.js → stateDiagram-AJRCARHV-DvjKwgrV.js} +1 -1
  48. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-DnRvfzCf.js +1 -0
  49. package/dist/web/assets/{time-CXSgtiIX.js → time-VOoCbkBT.js} +1 -1
  50. package/dist/web/assets/{timeline-definition-PNZ67QCA-B4in6942.js → timeline-definition-PNZ67QCA-B07zsWxv.js} +1 -1
  51. package/dist/web/assets/{vennDiagram-CIIHVFJN-D3Esdgtc.js → vennDiagram-CIIHVFJN-jzI6Z26y.js} +1 -1
  52. package/dist/web/assets/{wardley-L42UT6IY-CqOLhiLD.js → wardley-L42UT6IY-CKJh2fVX.js} +1 -1
  53. package/dist/web/assets/{wardleyDiagram-YWT4CUSO-DalbSLu7.js → wardleyDiagram-YWT4CUSO-BCBh8YXT.js} +1 -1
  54. package/dist/web/assets/{xychartDiagram-2RQKCTM6-Bgnuf0j-.js → xychartDiagram-2RQKCTM6-BEvx0rsP.js} +1 -1
  55. package/dist/web/index.html +2 -2
  56. package/package.json +2 -2
  57. package/src/api.ts +22 -0
  58. package/src/cli-manifest.ts +118 -0
  59. package/src/cli.ts +113 -0
  60. package/src/icons/extract-zip.ts +31 -0
  61. package/src/icons/fetcher.ts +31 -0
  62. package/src/icons/index-store.ts +45 -0
  63. package/src/icons/installer-types.ts +16 -0
  64. package/src/icons/installer.ts +76 -0
  65. package/src/icons/jobs.ts +78 -0
  66. package/src/icons/list-helper.ts +36 -0
  67. package/src/icons/lock.ts +13 -0
  68. package/src/icons/normalize-aws.ts +19 -0
  69. package/src/icons/normalize-azure.ts +11 -0
  70. package/src/icons/paths.ts +14 -0
  71. package/src/icons/remove.ts +16 -0
  72. package/src/icons/router.ts +206 -0
  73. package/src/icons/vendors.ts +52 -0
  74. package/src/layout.ts +1 -0
  75. package/src/operations.ts +23 -0
  76. package/src/schema-catalog.ts +3 -1
  77. package/src/schema.ts +66 -11
  78. package/src/server.ts +16 -0
  79. package/dist/web/assets/channel-l7nIO4lY.js +0 -1
  80. package/dist/web/assets/classDiagram-4FO5ZUOK-BPFTU8oh.js +0 -1
  81. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-BPFTU8oh.js +0 -1
  82. package/dist/web/assets/index-BKZTnCOL.js +0 -8624
  83. package/dist/web/assets/index-I8_SAWCr.css +0 -1
  84. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-e0EPpmUp.js +0 -1
@@ -0,0 +1,206 @@
1
+ import { join } from 'node:path';
2
+ import { Hono } from 'hono';
3
+ import { streamSSE } from 'hono/streaming';
4
+ import { z } from 'zod';
5
+ import { fetchWithProgress } from './fetcher.ts';
6
+ import { readIndex } from './index-store.ts';
7
+ import type { InstallEvent } from './installer-types.ts';
8
+ import { type InstallerDeps, installIconPack as defaultInstallIconPack } from './installer.ts';
9
+ import type { JobRegistry } from './jobs.ts';
10
+ import { summarizePacks } from './list-helper.ts';
11
+ import { iconCacheRoot } from './paths.ts';
12
+ import { removeIconPack } from './remove.ts';
13
+ import { vendorDescriptor } from './vendors.ts';
14
+
15
+ export type IconFetcher = (url: string) => Promise<Buffer>;
16
+ export type IconInstaller = typeof defaultInstallIconPack;
17
+
18
+ export interface IconsRouterDeps {
19
+ jobs: JobRegistry;
20
+ /** Defaults to seeflowHome()/icons. Tests inject a tmpdir. */
21
+ cacheRoot?: string;
22
+ /** Defaults to fetchWithProgress (real network). Tests inject a fixture. */
23
+ fetcher?: IconFetcher;
24
+ /** Defaults to installIconPack. Tests can swap. */
25
+ installer?: IconInstaller;
26
+ }
27
+
28
+ const VendorSchema = z.enum(['aws', 'azure']);
29
+ const InstallBodySchema = z.object({
30
+ vendor: VendorSchema,
31
+ acceptTerms: z.boolean().optional(),
32
+ packUrl: z.string().optional(),
33
+ });
34
+
35
+ export function createIconsRouter(deps: IconsRouterDeps): Hono {
36
+ const app = new Hono();
37
+ const installer = deps.installer ?? defaultInstallIconPack;
38
+ const getCacheRoot = () => deps.cacheRoot ?? iconCacheRoot();
39
+ const getFetcher = (): IconFetcher => deps.fetcher ?? ((url) => fetchWithProgress(url));
40
+
41
+ app.get('/packs', (c) => {
42
+ const idx = readIndex(getCacheRoot());
43
+ return c.json({ packs: summarizePacks(idx) });
44
+ });
45
+
46
+ app.get('/licenses/:vendor', (c) => {
47
+ const parsed = VendorSchema.safeParse(c.req.param('vendor'));
48
+ if (!parsed.success) return c.json({ error: 'unknown vendor' }, 404);
49
+ const desc = vendorDescriptor(parsed.data);
50
+ return c.json({
51
+ vendor: desc.vendor,
52
+ label: desc.label,
53
+ summary: desc.licenseSummary,
54
+ url: desc.licenseUrl,
55
+ requiresAcceptance: desc.requiresAcceptance,
56
+ });
57
+ });
58
+
59
+ // Constrain `:filename` to `<name>.svg` so this route doesn't shadow
60
+ // /licenses/:vendor, /packs/:vendor, or /jobs/:id at the two-segment level.
61
+ app.get('/:vendor/:filename{[^/]+\\.svg}', async (c) => {
62
+ const vendorParsed = VendorSchema.safeParse(c.req.param('vendor'));
63
+ if (!vendorParsed.success) return c.json({ error: 'unknown vendor' }, 404);
64
+ const vendor = vendorParsed.data;
65
+ const filename = c.req.param('filename');
66
+ const name = filename.slice(0, -4);
67
+
68
+ const idx = readIndex(getCacheRoot());
69
+ const pack = idx.packs[vendor];
70
+ const hint = `POST /api/icons/install { "vendor": "${vendor}" }`;
71
+ if (!pack) {
72
+ return c.json({ error: `vendor ${vendor} is not installed`, install: hint }, 404);
73
+ }
74
+ const rel = pack.icons[name];
75
+ if (!rel) {
76
+ return c.json({ error: `icon ${vendor}:${name} not found`, install: hint }, 404);
77
+ }
78
+ const abs = join(getCacheRoot(), rel);
79
+ const file = Bun.file(abs);
80
+ if (!(await file.exists())) {
81
+ return c.json({ error: `icon file missing on disk: ${rel}` }, 404);
82
+ }
83
+ return new Response(file.stream(), {
84
+ headers: {
85
+ 'content-type': 'image/svg+xml',
86
+ 'cache-control': 'public, max-age=31536000, immutable',
87
+ },
88
+ });
89
+ });
90
+
91
+ app.post('/install', async (c) => {
92
+ let body: unknown;
93
+ try {
94
+ body = await c.req.json();
95
+ } catch {
96
+ return c.json({ error: 'Body must be valid JSON' }, 400);
97
+ }
98
+ const parsed = InstallBodySchema.safeParse(body);
99
+ if (!parsed.success) {
100
+ return c.json({ error: 'Invalid install body', issues: parsed.error.issues }, 400);
101
+ }
102
+ const { vendor, acceptTerms, packUrl } = parsed.data;
103
+
104
+ const existing = deps.jobs.inFlightFor(vendor);
105
+ if (existing !== undefined) {
106
+ return c.json({ error: `install for ${vendor} already in flight`, jobId: existing }, 409);
107
+ }
108
+
109
+ let jobId: string;
110
+ try {
111
+ jobId = deps.jobs.create(vendor);
112
+ } catch (err) {
113
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 409);
114
+ }
115
+
116
+ const installerDeps: InstallerDeps = {
117
+ cacheRoot: getCacheRoot(),
118
+ now: Date.now,
119
+ version: () => new Date().toISOString().slice(0, 10),
120
+ fetcher: getFetcher(),
121
+ };
122
+
123
+ // Fire-and-forget: pump installer events into the job registry. The SSE
124
+ // route below replays buffered events on subscribe and races live ones, so
125
+ // the response can return jobId immediately and the client can subscribe
126
+ // whenever it likes.
127
+ (async () => {
128
+ try {
129
+ for await (const ev of installer({ vendor, acceptTerms, packUrl }, installerDeps)) {
130
+ deps.jobs.append(jobId, ev);
131
+ }
132
+ } catch (err) {
133
+ deps.jobs.append(jobId, {
134
+ type: 'error',
135
+ vendor,
136
+ message: err instanceof Error ? err.message : String(err),
137
+ });
138
+ } finally {
139
+ deps.jobs.markComplete(jobId);
140
+ }
141
+ })();
142
+
143
+ return c.json({ jobId });
144
+ });
145
+
146
+ app.get('/jobs/:id/events', (c) => {
147
+ const id = c.req.param('id');
148
+ return streamSSE(c, async (stream) => {
149
+ let active = true;
150
+ let ended = false;
151
+ const queue: InstallEvent[] = [];
152
+ let resume: (() => void) | null = null;
153
+
154
+ const wake = () => {
155
+ if (resume) {
156
+ const r = resume;
157
+ resume = null;
158
+ r();
159
+ }
160
+ };
161
+
162
+ const unsubscribe = deps.jobs.subscribe(
163
+ id,
164
+ (ev) => {
165
+ queue.push(ev);
166
+ wake();
167
+ },
168
+ () => {
169
+ ended = true;
170
+ wake();
171
+ },
172
+ );
173
+
174
+ stream.onAbort(() => {
175
+ active = false;
176
+ unsubscribe();
177
+ wake();
178
+ });
179
+
180
+ try {
181
+ while (active) {
182
+ while (queue.length > 0) {
183
+ const next = queue.shift();
184
+ if (!next) break;
185
+ await stream.writeSSE({ data: JSON.stringify(next) });
186
+ }
187
+ if (ended || !active) break;
188
+ await new Promise<void>((r) => {
189
+ resume = r;
190
+ });
191
+ }
192
+ } finally {
193
+ unsubscribe();
194
+ }
195
+ });
196
+ });
197
+
198
+ app.delete('/packs/:vendor', (c) => {
199
+ const parsed = VendorSchema.safeParse(c.req.param('vendor'));
200
+ if (!parsed.success) return c.json({ error: 'unknown vendor' }, 404);
201
+ removeIconPack(parsed.data, { cacheRoot: getCacheRoot() });
202
+ return c.json({ removed: parsed.data });
203
+ });
204
+
205
+ return app;
206
+ }
@@ -0,0 +1,52 @@
1
+ import { canonicalAwsName } from './normalize-aws.ts';
2
+ import { canonicalAzureName } from './normalize-azure.ts';
3
+ import type { IconVendor } from './paths.ts';
4
+
5
+ export interface VendorDescriptor {
6
+ vendor: IconVendor;
7
+ label: string;
8
+ defaultPackUrl: string;
9
+ /** Short summary; UI shows it inline. Authoritative terms live at `licenseUrl`. */
10
+ licenseSummary: string;
11
+ licenseUrl: string;
12
+ /** Whether the user must affirmatively accept terms before install. */
13
+ requiresAcceptance: boolean;
14
+ /** Filename → canonical kebab-name; null = skip entry. */
15
+ canonicalName: (filename: string) => string | null;
16
+ }
17
+
18
+ const AWS: VendorDescriptor = {
19
+ vendor: 'aws',
20
+ label: 'AWS',
21
+ // AWS re-issues this asset every release and removes prior versions; the
22
+ // q1-2025 path now 403s from CloudFront. Keep this in sync with whatever
23
+ // the Icon-package link at https://aws.amazon.com/architecture/icons/
24
+ // currently points to.
25
+ defaultPackUrl:
26
+ 'https://d1.awsstatic.com/onedam/marketing-channels/website/aws/en_US/architecture/approved/architecture-icons/Icon-package_04302026.4705b90f5aa45b019271a2699e9ce9b97b941ee1.zip',
27
+ licenseSummary:
28
+ 'Free to use in architecture diagrams. Attribution required for any public publication. See license URL for full terms.',
29
+ licenseUrl: 'https://aws.amazon.com/architecture/icons/',
30
+ requiresAcceptance: false,
31
+ canonicalName: canonicalAwsName,
32
+ };
33
+
34
+ const AZURE: VendorDescriptor = {
35
+ vendor: 'azure',
36
+ label: 'Microsoft Azure',
37
+ defaultPackUrl: 'https://arch-center.azureedge.net/icons/Azure_Public_Service_Icons_V20.zip',
38
+ licenseSummary:
39
+ 'Microsoft requires explicit acceptance of the Azure architecture icon terms before use. Icons may not be modified and must not be used to imply Microsoft endorsement. See license URL for full terms.',
40
+ licenseUrl: 'https://learn.microsoft.com/en-us/azure/architecture/icons/',
41
+ requiresAcceptance: true,
42
+ canonicalName: canonicalAzureName,
43
+ };
44
+
45
+ export const VENDOR_DESCRIPTORS: Record<IconVendor, VendorDescriptor> = {
46
+ aws: AWS,
47
+ azure: AZURE,
48
+ };
49
+
50
+ export function vendorDescriptor(vendor: IconVendor): VendorDescriptor {
51
+ return VENDOR_DESCRIPTORS[vendor];
52
+ }
package/src/layout.ts CHANGED
@@ -79,6 +79,7 @@ const DEFAULT_DIMENSIONS: Record<FlowNode['type'], { width: number; height: numb
79
79
  html: { width: 320, height: 200 },
80
80
  icon: { width: 80, height: 80 },
81
81
  component: { width: 320, height: 240 },
82
+ linkflow: { width: 240, height: 100 },
82
83
  };
83
84
 
84
85
  // Sticky / text variants are floating annotations. They never participate in
package/src/operations.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  EdgePinSchema,
33
33
  type Flow,
34
34
  FlowSchema,
35
+ LinkflowTargetSchema,
35
36
  NodeTypeSchema,
36
37
  PlayActionSchema,
37
38
  type ResolvedFlow,
@@ -156,6 +157,12 @@ export const NodePatchBodySchema = z
156
157
  // reparse + SSE broadcast, but splitFlow strips it from flow.json so the
157
158
  // sidecar is the source of truth on disk.
158
159
  spec: ComponentSpecSchema.optional(),
160
+ // type:'linkflow'-only: slug pair { project, flow } naming the target flow.
161
+ // Lands at data.target. Explicit `null` clears the field (mergeNodeUpdates
162
+ // strips the key from disk), so undo of a link/edit reverts a previously-
163
+ // unset target back to unlinked. The post-merge ResolvedFlowSchema reparse
164
+ // gates that this is only valid on type:'linkflow'.
165
+ target: LinkflowTargetSchema.nullable().optional(),
159
166
  })
160
167
  .strict();
161
168
  export type NodePatchBody = z.infer<typeof NodePatchBodySchema>;
@@ -189,6 +196,7 @@ const NODE_DATA_PATCH_KEYS = [
189
196
  'statusAction',
190
197
  'stateSource',
191
198
  'spec',
199
+ 'target',
192
200
  ] as const satisfies ReadonlyArray<keyof NodePatchBody>;
193
201
 
194
202
  const EXTERNALIZED_FIELD_NAMES = new Set<string>(EXTERNALIZED_NODE_FIELDS.map((e) => e.field));
@@ -261,6 +269,21 @@ const SEMANTIC_KEYS_BY_TYPE: Record<z.infer<typeof NodeTypeSchema>, ReadonlySet<
261
269
  // semantic-key set covers only the universal capability fields so retype
262
270
  // never drags `spec` through `data`. (US-007 wires the sidecar writer.)
263
271
  component: GEOMETRIC_SEMANTIC_KEYS,
272
+ // Linkflow nodes carry an optional `target` slug pair pointing at another
273
+ // flow. Retype preserves it alongside the universal semantic keys; the
274
+ // post-merge ResolvedFlowSchema reparse drops it when retyping AWAY from
275
+ // linkflow.
276
+ linkflow: new Set([
277
+ 'name',
278
+ 'description',
279
+ 'detail',
280
+ 'icon',
281
+ 'stateSource',
282
+ 'handlerModule',
283
+ 'playAction',
284
+ 'statusAction',
285
+ 'target',
286
+ ]),
264
287
  };
265
288
 
266
289
  // Visual data keys — routed to style.json on write by splitFlow. Kept here
@@ -5,7 +5,7 @@
5
5
  // module load — each call returns a fresh shallow copy so callers can't
6
6
  // mutate the cached payload.
7
7
 
8
- import { componentCatalog } from '@seeflow/canvas/catalog';
8
+ import { componentCatalog } from './vendored-canvas-catalog.ts';
9
9
  import type { ZodTypeAny } from 'zod';
10
10
  import { zodToJsonSchema } from 'zod-to-json-schema';
11
11
  import {
@@ -23,6 +23,7 @@ import {
23
23
  FlowHtmlNodeSchema,
24
24
  FlowIconNodeSchema,
25
25
  FlowImageNodeSchema,
26
+ FlowLinkflowNodeSchema,
26
27
  FlowQueueNodeSchema,
27
28
  FlowRectangleNodeSchema,
28
29
  FlowServerNodeSchema,
@@ -144,6 +145,7 @@ const PAYLOADS: Record<string, SchemaPayload> = {
144
145
  html: toJsonSchema(FlowHtmlNodeSchema),
145
146
  icon: toJsonSchema(FlowIconNodeSchema),
146
147
  component: toJsonSchema(FlowComponentNodeSchema),
148
+ linkflow: toJsonSchema(FlowLinkflowNodeSchema),
147
149
  },
148
150
  notes: [
149
151
  "type:'image' data.path must start with 'nodes/<id>/'.",
package/src/schema.ts CHANGED
@@ -85,7 +85,7 @@ const NodeSemanticBaseShape = {
85
85
  .string()
86
86
  .optional()
87
87
  .describe(
88
- "Decorative header glyph (Lucide icon name in kebab-case, e.g. 'database', 'cloud-upload'). Falls back to a placeholder when unknown. On type:'icon' nodes the icon IS the visual and is required.",
88
+ "Decorative header glyph. Encoded as `vendor:name` unprefixed values are Lucide kebab-case (e.g. 'database', 'cloud-upload'); prefixed values target installed icon packs ('aws:lambda', 'azure:functions') or the iconify catalog ('iconify:logos:google-cloud'). Falls back to a placeholder when unknown. On type:'icon' nodes the icon IS the visual and is required.",
89
89
  ),
90
90
  };
91
91
 
@@ -184,10 +184,17 @@ const NodeCapabilitiesShape = {
184
184
  ),
185
185
  };
186
186
 
187
- // 15 flat node types. The first 11 are geometric/illustrative and share
188
- // GeometricNodeData. `image`, `html`, `icon`, `component` carry per-type
189
- // fields. The renderer picks the SVG / chrome by `type`; the schema treats
190
- // them (apart from the per-type fields below) as identical.
187
+ // Flow ids are URL-safe and folder-safe: lowercase alphanumerics + dashes,
188
+ // must start with an alphanumeric character. Same pattern enforced by the
189
+ // manifest CRUD endpoints (POST/PATCH /api/projects/:project/flows[/:flow]).
190
+ // Defined early so the linkflow node data shapes below can reference it; the
191
+ // SeeflowManifest schemas at the bottom of the file reuse the same constant.
192
+ export const FlowIdPattern = /^[a-z0-9][a-z0-9-]*$/;
193
+
194
+ // 16 flat node types. The first 11 are geometric/illustrative and share
195
+ // GeometricNodeData. `image`, `html`, `icon`, `component`, `linkflow` carry
196
+ // per-type fields. The renderer picks the SVG / chrome by `type`; the schema
197
+ // treats them (apart from the per-type fields below) as identical.
191
198
  export const GEOMETRIC_NODE_TYPES = [
192
199
  'rectangle',
193
200
  'ellipse',
@@ -208,6 +215,7 @@ export const NodeTypeSchema = z.enum([
208
215
  'html',
209
216
  'icon',
210
217
  'component',
218
+ 'linkflow',
211
219
  ]);
212
220
 
213
221
  // --- Component node spec/action schemas --------------------------------------
@@ -318,6 +326,29 @@ const ResolvedComponentNodeData = z.object({
318
326
  autoSize: z.boolean().optional(),
319
327
  });
320
328
 
329
+ // Linkflow node — clickable "go to another flow" link. `target` is optional
330
+ // because freshly-dropped nodes start unlinked; once the user picks a flow
331
+ // via the picker dialog, the studio patches both `project` and `flow` (each
332
+ // matching FlowIdPattern). Target existence (does the project + flow still
333
+ // resolve to a known flow?) is a render-time concern, not a parse-time one:
334
+ // renames/deletes still parse cleanly so undo / cross-project picks work
335
+ // without the schema rejecting them.
336
+ export const LinkflowTargetSchema = z.object({
337
+ project: z.string().regex(FlowIdPattern, {
338
+ message: 'target.project must match /^[a-z0-9][a-z0-9-]*$/',
339
+ }),
340
+ flow: z.string().regex(FlowIdPattern, {
341
+ message: 'target.flow must match /^[a-z0-9][a-z0-9-]*$/',
342
+ }),
343
+ });
344
+
345
+ const ResolvedLinkflowNodeData = z.object({
346
+ ...NodeSemanticBaseShape,
347
+ ...NodeVisualBaseShape,
348
+ ...NodeCapabilitiesShape,
349
+ target: LinkflowTargetSchema.optional(),
350
+ });
351
+
321
352
  const NodeBaseShape = {
322
353
  id: z.string().min(1),
323
354
  position: PositionSchema,
@@ -350,6 +381,11 @@ const NodeSchema = z.discriminatedUnion('type', [
350
381
  type: z.literal('component'),
351
382
  data: ResolvedComponentNodeData,
352
383
  }),
384
+ z.object({
385
+ ...NodeBaseShape,
386
+ type: z.literal('linkflow'),
387
+ data: ResolvedLinkflowNodeData,
388
+ }),
353
389
  ]);
354
390
 
355
391
  // Connector — unchanged by the flat-types refactor.
@@ -538,7 +574,7 @@ const FlowIconNodeData = z
538
574
  .string()
539
575
  .min(1)
540
576
  .describe(
541
- "Required Lucide icon name (kebab-case, e.g. 'cloud-upload', 'database'). On type:'icon' nodes the icon IS the visual — overrides the optional decorative `icon` from the semantic base.",
577
+ "Required icon. Encoded as `vendor:name` — unprefixed values are Lucide kebab-case (e.g. 'cloud-upload', 'database'); prefixed values target installed icon packs ('aws:lambda', 'azure:functions') or the iconify catalog ('iconify:logos:google-cloud'). On type:'icon' nodes the icon IS the visual — overrides the optional decorative `icon` from the semantic base.",
542
578
  ),
543
579
  alt: z.string().optional().describe('Accessibility alt text for the icon glyph.'),
544
580
  })
@@ -561,6 +597,21 @@ const FlowComponentNodeData = z
561
597
  })
562
598
  .strict();
563
599
 
600
+ // Linkflow node, on-disk shape. `target` carries the slug pair that names
601
+ // another flow in the registry; optional because a freshly-dropped link node
602
+ // is unlinked until the picker commits a choice. Target existence is checked
603
+ // at render time (broken-link state), never at parse time, so renames /
604
+ // deletes still parse cleanly.
605
+ const FlowLinkflowNodeData = z
606
+ .object({
607
+ ...NodeSemanticBaseShape,
608
+ ...NodeCapabilitiesShape,
609
+ target: LinkflowTargetSchema.optional().describe(
610
+ "Slug pair { project, flow } naming the flow this node links to. Both fields are matched against /^[a-z0-9][a-z0-9-]*$/. Omitted on freshly-dropped nodes — the picker dialog patches it once the user picks a target. Cross-project links are allowed (project may differ from the host flow's project).",
611
+ ),
612
+ })
613
+ .strict();
614
+
564
615
  const FlowNodeBaseShape = {
565
616
  id: z.string().min(1),
566
617
  };
@@ -618,6 +669,14 @@ export const FlowComponentNodeSchema = z
618
669
  })
619
670
  .strict();
620
671
 
672
+ export const FlowLinkflowNodeSchema = z
673
+ .object({
674
+ ...FlowNodeBaseShape,
675
+ type: z.literal('linkflow'),
676
+ data: FlowLinkflowNodeData,
677
+ })
678
+ .strict();
679
+
621
680
  const FlowNodeSchema = z.discriminatedUnion('type', [
622
681
  FlowRectangleNodeSchema,
623
682
  FlowEllipseNodeSchema,
@@ -634,6 +693,7 @@ const FlowNodeSchema = z.discriminatedUnion('type', [
634
693
  FlowHtmlNodeSchema,
635
694
  FlowIconNodeSchema,
636
695
  FlowComponentNodeSchema,
696
+ FlowLinkflowNodeSchema,
637
697
  ]);
638
698
 
639
699
  const FlowConnectorBaseShape = {
@@ -765,11 +825,6 @@ export type ConnectorStyleEntry = z.infer<typeof ConnectorStyleEntrySchema>;
765
825
  // can consume.
766
826
  // =============================================================================
767
827
 
768
- // Flow ids are URL-safe and folder-safe: lowercase alphanumerics + dashes,
769
- // must start with an alphanumeric character. Same pattern enforced by the
770
- // manifest CRUD endpoints (POST/PATCH /api/projects/:project/flows[/:flow]).
771
- export const FlowIdPattern = /^[a-z0-9][a-z0-9-]*$/;
772
-
773
828
  const SeeflowManifestFlowEntrySchema = z.object({
774
829
  id: z.string().regex(FlowIdPattern, {
775
830
  message: 'flow id must match /^[a-z0-9][a-z0-9-]*$/',
package/src/server.ts CHANGED
@@ -7,6 +7,8 @@ import { type ProxyFacade, createApi } from './api.ts';
7
7
  import { createCorsMiddleware } from './cors.ts';
8
8
  import { createDemoRouter } from './demo.ts';
9
9
  import { type EventBus, createEventBus } from './events.ts';
10
+ import { type JobRegistry, createJobRegistry } from './icons/jobs.ts';
11
+ import type { IconFetcher } from './icons/router.ts';
10
12
  import { createMcpServer } from './mcp.ts';
11
13
  import { seeflowHome } from './paths.ts';
12
14
  import { type ProcessSpawner, defaultProcessSpawner } from './process-spawner.ts';
@@ -67,6 +69,16 @@ export interface CreateAppOptions {
67
69
  * `Bun.serve` binds — useful for the ephemeral-port boot in
68
70
  * `mcp-shim.ts` where the URL isn't known until the server is up. */
69
71
  httpUrl?: string;
72
+ /** Shared icon-install job registry. Defaults to a per-app registry created
73
+ * in createApp so SSE replays survive across requests within the same
74
+ * studio process. Integration tests inject their own to assert state. */
75
+ iconJobs?: JobRegistry;
76
+ /** Override the icon-cache root. Production resolves it from `seeflowHome()`
77
+ * inside the router; tests pass an isolated tmpdir. */
78
+ iconCacheRoot?: string;
79
+ /** Override the icon installer's fetcher. Production uses fetchWithProgress
80
+ * (real network); integration tests inject a fixture-returning closure. */
81
+ iconFetcher?: IconFetcher;
70
82
  }
71
83
 
72
84
  const DEFAULT_VITE_DEV_URL = 'http://localhost:5173';
@@ -95,6 +107,7 @@ export function createApp(options: CreateAppOptions = {}): Hono {
95
107
  const statusRunner =
96
108
  options.statusRunner ??
97
109
  createStatusRunner({ registry, events, spawner: defaultProcessSpawner });
110
+ const iconJobs = options.iconJobs ?? createJobRegistry();
98
111
 
99
112
  if (watcher && (options.watchAllOnBoot ?? true)) {
100
113
  watcher.watchAll();
@@ -155,6 +168,9 @@ export function createApp(options: CreateAppOptions = {}): Hono {
155
168
  statusRunner,
156
169
  processSpawner: options.processSpawner,
157
170
  proxy: options.proxy,
171
+ iconJobs,
172
+ iconCacheRoot: options.iconCacheRoot,
173
+ iconFetcher: options.iconFetcher,
158
174
  }),
159
175
  );
160
176
 
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-1G8gw6AK.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as e,C as t}from"./chunk-727SXJPM-CtI4DnVU.js";import{a as i}from"./mermaid.core-1G8gw6AK.js";import"./index-BKZTnCOL.js";import"./chunk-FMBD7UC4-XRBBZk8O.js";import"./chunk-ND2GUHAM-D0exlO6X.js";import"./chunk-55IACEB6-B8VO9ECP.js";import"./chunk-2J33WTMH-DMiLaW3V.js";import"./purify.es-CLGrRn1w.js";import"./step-CWvwoXpJ.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as e,C as t}from"./chunk-727SXJPM-CtI4DnVU.js";import{a as i}from"./mermaid.core-1G8gw6AK.js";import"./index-BKZTnCOL.js";import"./chunk-FMBD7UC4-XRBBZk8O.js";import"./chunk-ND2GUHAM-D0exlO6X.js";import"./chunk-55IACEB6-B8VO9ECP.js";import"./chunk-2J33WTMH-DMiLaW3V.js";import"./purify.es-CLGrRn1w.js";import"./step-CWvwoXpJ.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};