@tuongaz/seeflow 0.1.98 → 0.1.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/web/assets/{architectureDiagram-3BPJPVTR-DU1dz67I.js → architectureDiagram-3BPJPVTR-hjhO-xaL.js} +1 -1
- package/dist/web/assets/{blockDiagram-GPEHLZMM-DSmqToeJ.js → blockDiagram-GPEHLZMM-CMWnlTJT.js} +1 -1
- package/dist/web/assets/{c4Diagram-AAUBKEIU-CNP992lo.js → c4Diagram-AAUBKEIU-CnWgitpw.js} +1 -1
- package/dist/web/assets/channel-CQpZJl1M.js +1 -0
- package/dist/web/assets/{chart-DF01veTJ.js → chart-DTSNuntU.js} +1 -1
- package/dist/web/assets/{chunk-2J33WTMH-LVhrEzuA.js → chunk-2J33WTMH-DG5egXsT.js} +1 -1
- package/dist/web/assets/{chunk-4BX2VUAB-7QT-8Ea8.js → chunk-4BX2VUAB-lOed0Foz.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-DekK1DpQ.js → chunk-55IACEB6-C9KQcfLA.js} +1 -1
- package/dist/web/assets/{chunk-727SXJPM-B2Ar-3-Y.js → chunk-727SXJPM-8t-uC2Ju.js} +1 -1
- package/dist/web/assets/{chunk-AQP2D5EJ-DFL9j35G.js → chunk-AQP2D5EJ-BLDdZjsM.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-BDQ2jKs8.js → chunk-FMBD7UC4-BjCqyGJ2.js} +1 -1
- package/dist/web/assets/{chunk-ND2GUHAM-DbrZoSgU.js → chunk-ND2GUHAM-uw9ctj03.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-BBiKBSEG.js → chunk-QZHKN3VN-DMnoB-y4.js} +1 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-CwZ4WJSj.js +1 -0
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-CwZ4WJSj.js +1 -0
- package/dist/web/assets/{code-block-Dqo0Jd1G.js → code-block-DyzYmdFD.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-CGpaWYc4.js → cose-bilkent-S5V4N54A-BzvW-9-7.js} +1 -1
- package/dist/web/assets/{dagre-BM42HDAG-DTV9Prbv.js → dagre-BM42HDAG-DfzTR0-z.js} +1 -1
- package/dist/web/assets/{diagram-2AECGRRQ-DLqTKsZ1.js → diagram-2AECGRRQ-CK_xOFz2.js} +1 -1
- package/dist/web/assets/{diagram-5GNKFQAL-WeRlWvVf.js → diagram-5GNKFQAL-DptRneW0.js} +1 -1
- package/dist/web/assets/{diagram-KO2AKTUF-Bla9fFa3.js → diagram-KO2AKTUF-2X9C_tjo.js} +1 -1
- package/dist/web/assets/{diagram-LMA3HP47-JmfK87A4.js → diagram-LMA3HP47-t9UANm6t.js} +1 -1
- package/dist/web/assets/{diagram-OG6HWLK6-CWLzBnkl.js → diagram-OG6HWLK6-zK_vf5lM.js} +1 -1
- package/dist/web/assets/{erDiagram-TEJ5UH35-zIaBabk9.js → erDiagram-TEJ5UH35-C4HWFw3h.js} +1 -1
- package/dist/web/assets/{flowDiagram-I6XJVG4X-Bue3Noo8.js → flowDiagram-I6XJVG4X-CY0SovEU.js} +1 -1
- package/dist/web/assets/{ganttDiagram-6RSMTGT7-BHdGoBmM.js → ganttDiagram-6RSMTGT7-Bmfqfrnr.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PVQCEYII-DY8i3xDG.js → gitGraphDiagram-PVQCEYII-DqJl_j29.js} +1 -1
- package/dist/web/assets/iconify-Ba-DtAuF.js +1 -0
- package/dist/web/assets/index-B2D6t5p2.css +1 -0
- package/dist/web/assets/index-CJQRpgGJ.js +8629 -0
- package/dist/web/assets/{index.es-PfKZum8P.js → index.es-B-rJYIct.js} +1 -1
- package/dist/web/assets/{infoDiagram-5YYISTIA-CjlY190O.js → infoDiagram-5YYISTIA-BzG_xiVf.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-UlDRmAcX.js → ishikawaDiagram-YF4QCWOH-DqXBV4G-.js} +1 -1
- package/dist/web/assets/{journeyDiagram-JHISSGLW-BQU5iM6n.js → journeyDiagram-JHISSGLW-CHzEKKtN.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-DZwedYLb.js → jspdf.es.min-tO5wkRTA.js} +3 -3
- package/dist/web/assets/{kanban-definition-UN3LZRKU-BHjSnSao.js → kanban-definition-UN3LZRKU-C2dtNhfm.js} +1 -1
- package/dist/web/assets/{linear-DfckWaYF.js → linear-CklvVgUa.js} +1 -1
- package/dist/web/assets/{markdown-DK4rNWyg.js → markdown-BYnvrw2L.js} +1 -1
- package/dist/web/assets/{mermaid.core-Q5Rkziel.js → mermaid.core-DBZcsQGT.js} +4 -4
- package/dist/web/assets/{mindmap-definition-RKZ34NQL-Cwl5O3Cf.js → mindmap-definition-RKZ34NQL-D_9hRbXB.js} +1 -1
- package/dist/web/assets/{pieDiagram-4H26LBE5-CfuqpLij.js → pieDiagram-4H26LBE5-B437WT8U.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-W4KKPZXB-DPMXHfS6.js → quadrantDiagram-W4KKPZXB-D8YRFe6P.js} +1 -1
- package/dist/web/assets/{requirementDiagram-4Y6WPE33-DnkSyvnm.js → requirementDiagram-4Y6WPE33-tZJdPmfT.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-5OEKKPKP-qgBj0gIh.js → sankeyDiagram-5OEKKPKP-Fj4tIoUj.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-3UESZ5HK-Dv8lcfkG.js → sequenceDiagram-3UESZ5HK-1eSo4sGa.js} +1 -1
- package/dist/web/assets/{stateDiagram-AJRCARHV-BHhRAlSF.js → stateDiagram-AJRCARHV-DkioA-Ys.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-COESjPKl.js +1 -0
- package/dist/web/assets/{time-DLVqCHvN.js → time--AmUoxli.js} +1 -1
- package/dist/web/assets/{timeline-definition-PNZ67QCA-Dj-PUcDW.js → timeline-definition-PNZ67QCA-5VsFM6UF.js} +1 -1
- package/dist/web/assets/{vennDiagram-CIIHVFJN-BVu8roqR.js → vennDiagram-CIIHVFJN-BZRRJSDD.js} +1 -1
- package/dist/web/assets/{wardley-L42UT6IY-V3a5jBUh.js → wardley-L42UT6IY-CRdqY7KN.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BMdJmf1X.js → wardleyDiagram-YWT4CUSO-B_00NweE.js} +1 -1
- package/dist/web/assets/{xychartDiagram-2RQKCTM6-iibKxvlU.js → xychartDiagram-2RQKCTM6-F3eOVTTc.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/src/api.ts +22 -0
- package/src/cli-manifest.ts +118 -0
- package/src/cli.ts +113 -0
- package/src/icons/extract-zip.ts +31 -0
- package/src/icons/fetcher.ts +31 -0
- package/src/icons/index-store.ts +45 -0
- package/src/icons/installer-types.ts +16 -0
- package/src/icons/installer.ts +76 -0
- package/src/icons/jobs.ts +78 -0
- package/src/icons/list-helper.ts +36 -0
- package/src/icons/lock.ts +13 -0
- package/src/icons/normalize-aws.ts +19 -0
- package/src/icons/normalize-azure.ts +11 -0
- package/src/icons/paths.ts +14 -0
- package/src/icons/remove.ts +16 -0
- package/src/icons/router.ts +211 -0
- package/src/icons/vendors.ts +52 -0
- package/src/layout.ts +1 -0
- package/src/operations.ts +23 -0
- package/src/schema-catalog.ts +2 -0
- package/src/schema.ts +66 -11
- package/src/server.ts +16 -0
- package/dist/web/assets/channel-v9-wsv2r.js +0 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-CY00ktDc.js +0 -1
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-CY00ktDc.js +0 -1
- package/dist/web/assets/index-B-NP-7Oo.js +0 -8624
- package/dist/web/assets/index-I8_SAWCr.css +0 -1
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-Z4OeqbFi.js +0 -1
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
// No `immutable` — pack reinstalls (e.g. after an extractor bugfix)
|
|
87
|
+
// can change the bytes served at the same /api/icons/<vendor>/<name>.svg
|
|
88
|
+
// URL. `immutable` would tell the browser to skip revalidation even on
|
|
89
|
+
// hard refresh, locking users on bad cached content. A 1-day max-age
|
|
90
|
+
// keeps the picker fast without trapping fixed icons behind cache.
|
|
91
|
+
'cache-control': 'public, max-age=86400',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
app.post('/install', async (c) => {
|
|
97
|
+
let body: unknown;
|
|
98
|
+
try {
|
|
99
|
+
body = await c.req.json();
|
|
100
|
+
} catch {
|
|
101
|
+
return c.json({ error: 'Body must be valid JSON' }, 400);
|
|
102
|
+
}
|
|
103
|
+
const parsed = InstallBodySchema.safeParse(body);
|
|
104
|
+
if (!parsed.success) {
|
|
105
|
+
return c.json({ error: 'Invalid install body', issues: parsed.error.issues }, 400);
|
|
106
|
+
}
|
|
107
|
+
const { vendor, acceptTerms, packUrl } = parsed.data;
|
|
108
|
+
|
|
109
|
+
const existing = deps.jobs.inFlightFor(vendor);
|
|
110
|
+
if (existing !== undefined) {
|
|
111
|
+
return c.json({ error: `install for ${vendor} already in flight`, jobId: existing }, 409);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let jobId: string;
|
|
115
|
+
try {
|
|
116
|
+
jobId = deps.jobs.create(vendor);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 409);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const installerDeps: InstallerDeps = {
|
|
122
|
+
cacheRoot: getCacheRoot(),
|
|
123
|
+
now: Date.now,
|
|
124
|
+
version: () => new Date().toISOString().slice(0, 10),
|
|
125
|
+
fetcher: getFetcher(),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Fire-and-forget: pump installer events into the job registry. The SSE
|
|
129
|
+
// route below replays buffered events on subscribe and races live ones, so
|
|
130
|
+
// the response can return jobId immediately and the client can subscribe
|
|
131
|
+
// whenever it likes.
|
|
132
|
+
(async () => {
|
|
133
|
+
try {
|
|
134
|
+
for await (const ev of installer({ vendor, acceptTerms, packUrl }, installerDeps)) {
|
|
135
|
+
deps.jobs.append(jobId, ev);
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
deps.jobs.append(jobId, {
|
|
139
|
+
type: 'error',
|
|
140
|
+
vendor,
|
|
141
|
+
message: err instanceof Error ? err.message : String(err),
|
|
142
|
+
});
|
|
143
|
+
} finally {
|
|
144
|
+
deps.jobs.markComplete(jobId);
|
|
145
|
+
}
|
|
146
|
+
})();
|
|
147
|
+
|
|
148
|
+
return c.json({ jobId });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
app.get('/jobs/:id/events', (c) => {
|
|
152
|
+
const id = c.req.param('id');
|
|
153
|
+
return streamSSE(c, async (stream) => {
|
|
154
|
+
let active = true;
|
|
155
|
+
let ended = false;
|
|
156
|
+
const queue: InstallEvent[] = [];
|
|
157
|
+
let resume: (() => void) | null = null;
|
|
158
|
+
|
|
159
|
+
const wake = () => {
|
|
160
|
+
if (resume) {
|
|
161
|
+
const r = resume;
|
|
162
|
+
resume = null;
|
|
163
|
+
r();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const unsubscribe = deps.jobs.subscribe(
|
|
168
|
+
id,
|
|
169
|
+
(ev) => {
|
|
170
|
+
queue.push(ev);
|
|
171
|
+
wake();
|
|
172
|
+
},
|
|
173
|
+
() => {
|
|
174
|
+
ended = true;
|
|
175
|
+
wake();
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
stream.onAbort(() => {
|
|
180
|
+
active = false;
|
|
181
|
+
unsubscribe();
|
|
182
|
+
wake();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
while (active) {
|
|
187
|
+
while (queue.length > 0) {
|
|
188
|
+
const next = queue.shift();
|
|
189
|
+
if (!next) break;
|
|
190
|
+
await stream.writeSSE({ data: JSON.stringify(next) });
|
|
191
|
+
}
|
|
192
|
+
if (ended || !active) break;
|
|
193
|
+
await new Promise<void>((r) => {
|
|
194
|
+
resume = r;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
unsubscribe();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
app.delete('/packs/:vendor', (c) => {
|
|
204
|
+
const parsed = VendorSchema.safeParse(c.req.param('vendor'));
|
|
205
|
+
if (!parsed.success) return c.json({ error: 'unknown vendor' }, 404);
|
|
206
|
+
removeIconPack(parsed.data, { cacheRoot: getCacheRoot() });
|
|
207
|
+
return c.json({ removed: parsed.data });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return app;
|
|
211
|
+
}
|
|
@@ -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
|
package/src/schema-catalog.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
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
|
|
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-Q5Rkziel.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-B2Ar-3-Y.js";import{a as i}from"./mermaid.core-Q5Rkziel.js";import"./index-B-NP-7Oo.js";import"./chunk-FMBD7UC4-BDQ2jKs8.js";import"./chunk-ND2GUHAM-DbrZoSgU.js";import"./chunk-55IACEB6-DekK1DpQ.js";import"./chunk-2J33WTMH-LVhrEzuA.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-B2Ar-3-Y.js";import{a as i}from"./mermaid.core-Q5Rkziel.js";import"./index-B-NP-7Oo.js";import"./chunk-FMBD7UC4-BDQ2jKs8.js";import"./chunk-ND2GUHAM-DbrZoSgU.js";import"./chunk-55IACEB6-DekK1DpQ.js";import"./chunk-2J33WTMH-LVhrEzuA.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};
|