@tuongaz/seeflow 0.1.98 → 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-DU1dz67I.js → architectureDiagram-3BPJPVTR-B-zze1dQ.js} +1 -1
  3. package/dist/web/assets/{blockDiagram-GPEHLZMM-DSmqToeJ.js → blockDiagram-GPEHLZMM-DdXfGidV.js} +1 -1
  4. package/dist/web/assets/{c4Diagram-AAUBKEIU-CNP992lo.js → c4Diagram-AAUBKEIU-D1bZOhXl.js} +1 -1
  5. package/dist/web/assets/channel-BKgs5-7F.js +1 -0
  6. package/dist/web/assets/{chart-DF01veTJ.js → chart-Dg9qryEw.js} +1 -1
  7. package/dist/web/assets/{chunk-2J33WTMH-LVhrEzuA.js → chunk-2J33WTMH-DerfLWVn.js} +1 -1
  8. package/dist/web/assets/{chunk-4BX2VUAB-7QT-8Ea8.js → chunk-4BX2VUAB-3NxUSAZt.js} +1 -1
  9. package/dist/web/assets/{chunk-55IACEB6-DekK1DpQ.js → chunk-55IACEB6-DaUPCrIV.js} +1 -1
  10. package/dist/web/assets/{chunk-727SXJPM-B2Ar-3-Y.js → chunk-727SXJPM-DFbT_Bo7.js} +1 -1
  11. package/dist/web/assets/{chunk-AQP2D5EJ-DFL9j35G.js → chunk-AQP2D5EJ-Btrl-RqP.js} +1 -1
  12. package/dist/web/assets/{chunk-FMBD7UC4-BDQ2jKs8.js → chunk-FMBD7UC4-lKFmDkFq.js} +1 -1
  13. package/dist/web/assets/{chunk-ND2GUHAM-DbrZoSgU.js → chunk-ND2GUHAM-CHh_WD0m.js} +1 -1
  14. package/dist/web/assets/{chunk-QZHKN3VN-BBiKBSEG.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-Dqo0Jd1G.js → code-block-fnJ45JTt.js} +1 -1
  18. package/dist/web/assets/{cose-bilkent-S5V4N54A-CGpaWYc4.js → cose-bilkent-S5V4N54A-CBI67v3W.js} +1 -1
  19. package/dist/web/assets/{dagre-BM42HDAG-DTV9Prbv.js → dagre-BM42HDAG-6y6lYDbG.js} +1 -1
  20. package/dist/web/assets/{diagram-2AECGRRQ-DLqTKsZ1.js → diagram-2AECGRRQ-DGNDrzfp.js} +1 -1
  21. package/dist/web/assets/{diagram-5GNKFQAL-WeRlWvVf.js → diagram-5GNKFQAL-zYDwXAIN.js} +1 -1
  22. package/dist/web/assets/{diagram-KO2AKTUF-Bla9fFa3.js → diagram-KO2AKTUF-DmS_BzKx.js} +1 -1
  23. package/dist/web/assets/{diagram-LMA3HP47-JmfK87A4.js → diagram-LMA3HP47-CJbeNPH9.js} +1 -1
  24. package/dist/web/assets/{diagram-OG6HWLK6-CWLzBnkl.js → diagram-OG6HWLK6-iiJwcdIX.js} +1 -1
  25. package/dist/web/assets/{erDiagram-TEJ5UH35-zIaBabk9.js → erDiagram-TEJ5UH35-h8Y_gYmT.js} +1 -1
  26. package/dist/web/assets/{flowDiagram-I6XJVG4X-Bue3Noo8.js → flowDiagram-I6XJVG4X-Z5lwuHdZ.js} +1 -1
  27. package/dist/web/assets/{ganttDiagram-6RSMTGT7-BHdGoBmM.js → ganttDiagram-6RSMTGT7-BPEZFeN5.js} +1 -1
  28. package/dist/web/assets/{gitGraphDiagram-PVQCEYII-DY8i3xDG.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-PfKZum8P.js → index.es-CqlCK90H.js} +1 -1
  33. package/dist/web/assets/{infoDiagram-5YYISTIA-CjlY190O.js → infoDiagram-5YYISTIA-DSDcNKWM.js} +1 -1
  34. package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-UlDRmAcX.js → ishikawaDiagram-YF4QCWOH-Djo12Wsr.js} +1 -1
  35. package/dist/web/assets/{journeyDiagram-JHISSGLW-BQU5iM6n.js → journeyDiagram-JHISSGLW-B1PQjr20.js} +1 -1
  36. package/dist/web/assets/{jspdf.es.min-DZwedYLb.js → jspdf.es.min-EKwwYZfH.js} +3 -3
  37. package/dist/web/assets/{kanban-definition-UN3LZRKU-BHjSnSao.js → kanban-definition-UN3LZRKU-YeErCOhe.js} +1 -1
  38. package/dist/web/assets/{linear-DfckWaYF.js → linear-D5FHHJNO.js} +1 -1
  39. package/dist/web/assets/{markdown-DK4rNWyg.js → markdown-CuvYg4wg.js} +1 -1
  40. package/dist/web/assets/{mermaid.core-Q5Rkziel.js → mermaid.core-8qjHlhCB.js} +4 -4
  41. package/dist/web/assets/{mindmap-definition-RKZ34NQL-Cwl5O3Cf.js → mindmap-definition-RKZ34NQL-DpUz-nrq.js} +1 -1
  42. package/dist/web/assets/{pieDiagram-4H26LBE5-CfuqpLij.js → pieDiagram-4H26LBE5-CYCE9du8.js} +1 -1
  43. package/dist/web/assets/{quadrantDiagram-W4KKPZXB-DPMXHfS6.js → quadrantDiagram-W4KKPZXB-x8WbTOtV.js} +1 -1
  44. package/dist/web/assets/{requirementDiagram-4Y6WPE33-DnkSyvnm.js → requirementDiagram-4Y6WPE33-DIcJaM60.js} +1 -1
  45. package/dist/web/assets/{sankeyDiagram-5OEKKPKP-qgBj0gIh.js → sankeyDiagram-5OEKKPKP-CwbrztT0.js} +1 -1
  46. package/dist/web/assets/{sequenceDiagram-3UESZ5HK-Dv8lcfkG.js → sequenceDiagram-3UESZ5HK-CWfsvCJO.js} +1 -1
  47. package/dist/web/assets/{stateDiagram-AJRCARHV-BHhRAlSF.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-DLVqCHvN.js → time-VOoCbkBT.js} +1 -1
  50. package/dist/web/assets/{timeline-definition-PNZ67QCA-Dj-PUcDW.js → timeline-definition-PNZ67QCA-B07zsWxv.js} +1 -1
  51. package/dist/web/assets/{vennDiagram-CIIHVFJN-BVu8roqR.js → vennDiagram-CIIHVFJN-jzI6Z26y.js} +1 -1
  52. package/dist/web/assets/{wardley-L42UT6IY-V3a5jBUh.js → wardley-L42UT6IY-CKJh2fVX.js} +1 -1
  53. package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BMdJmf1X.js → wardleyDiagram-YWT4CUSO-BCBh8YXT.js} +1 -1
  54. package/dist/web/assets/{xychartDiagram-2RQKCTM6-iibKxvlU.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 +2 -0
  77. package/src/schema.ts +66 -11
  78. package/src/server.ts +16 -0
  79. package/dist/web/assets/channel-v9-wsv2r.js +0 -1
  80. package/dist/web/assets/classDiagram-4FO5ZUOK-CY00ktDc.js +0 -1
  81. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-CY00ktDc.js +0 -1
  82. package/dist/web/assets/index-B-NP-7Oo.js +0 -8624
  83. package/dist/web/assets/index-I8_SAWCr.css +0 -1
  84. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-Z4OeqbFi.js +0 -1
package/src/cli.ts CHANGED
@@ -196,6 +196,8 @@ if (argv.includes('--version') || argv.includes('-v')) {
196
196
  await runFlowsDelete();
197
197
  } else if (sub === 'flows:layout') {
198
198
  await runFlowsLayout();
199
+ } else if (sub === 'icons') {
200
+ await runIcons();
199
201
  } else if (sub === 'flow:add-bulk') {
200
202
  await runFlowAddBulk();
201
203
  } else if (sub === 'flows:play') {
@@ -286,6 +288,12 @@ Commands (work without a running studio):
286
288
  when seeding a flow.json (e.g. 'ids node 10', then
287
289
  'ids connector 12').
288
290
 
291
+ Icons (local cache):
292
+ icons list List installed and available vendor packs (aws, azure)
293
+ icons add <v> Install a vendor pack (v: aws|azure) [--accept-terms] [--pack-url <url>]
294
+ icons update <v> Re-install a vendor pack — same flags as add
295
+ icons remove <v> Remove an installed vendor pack (idempotent)
296
+
289
297
  Commands (require a running studio):
290
298
  flows:play <n> Trigger a play on node <n> (--project <p> --flow <f>)
291
299
  emit <id> <n> <st> Broadcast a status event for node <n> (st: running|done|error)
@@ -1218,3 +1226,108 @@ async function runE2e() {
1218
1226
  }
1219
1227
  printOk(report);
1220
1228
  }
1229
+
1230
+ async function runIcons() {
1231
+ const action = argv[1];
1232
+ switch (action) {
1233
+ case undefined:
1234
+ case 'list':
1235
+ await runIconsList();
1236
+ break;
1237
+ case 'add':
1238
+ await runIconsAdd();
1239
+ break;
1240
+ case 'update':
1241
+ await runIconsUpdate();
1242
+ break;
1243
+ case 'remove':
1244
+ await runIconsRemove();
1245
+ break;
1246
+ default:
1247
+ console.error(`Unknown icons action: ${action}`);
1248
+ console.error('Usage: seeflow icons {list|add|update|remove} ...');
1249
+ process.exit(1);
1250
+ }
1251
+ }
1252
+
1253
+ async function runIconsList() {
1254
+ const { iconCacheRoot } = await import('./icons/paths.ts');
1255
+ const { readIndex } = await import('./icons/index-store.ts');
1256
+ const { summarizePacks } = await import('./icons/list-helper.ts');
1257
+ const idx = readIndex(iconCacheRoot());
1258
+ printOk({ packs: summarizePacks(idx) });
1259
+ }
1260
+
1261
+ type IconVendorSlug = 'aws' | 'azure';
1262
+
1263
+ function parseIconVendor(action: 'add' | 'remove'): IconVendorSlug {
1264
+ const vendors: readonly IconVendorSlug[] = ['aws', 'azure'];
1265
+ const raw = argv[2];
1266
+ if (!raw || raw.startsWith('--')) {
1267
+ const suffix = action === 'add' ? ' [--accept-terms] [--pack-url <url>]' : '';
1268
+ console.error(`Usage: seeflow icons ${action} <vendor>${suffix}`);
1269
+ process.exit(1);
1270
+ }
1271
+ if (!(vendors as readonly string[]).includes(raw)) {
1272
+ console.error(`Unknown vendor: ${raw}. Expected one of: ${vendors.join(', ')}.`);
1273
+ process.exit(1);
1274
+ }
1275
+ return raw as IconVendorSlug;
1276
+ }
1277
+
1278
+ async function runIconsAdd() {
1279
+ const vendor = parseIconVendor('add');
1280
+ const acceptTerms = hasFlag('accept-terms');
1281
+ const packUrl = flagValue('pack-url');
1282
+
1283
+ const { installIconPack } = await import('./icons/installer.ts');
1284
+ const { fetchWithProgress } = await import('./icons/fetcher.ts');
1285
+ const { iconCacheRoot } = await import('./icons/paths.ts');
1286
+
1287
+ const gen = installIconPack(
1288
+ { vendor, acceptTerms, packUrl },
1289
+ {
1290
+ cacheRoot: iconCacheRoot(),
1291
+ now: () => Date.now(),
1292
+ version: () => new Date().toISOString().slice(0, 10),
1293
+ fetcher: (url: string) =>
1294
+ fetchWithProgress(url, {
1295
+ onProgress: (bytes) => process.stderr.write(`download-progress ${vendor} ${bytes}\n`),
1296
+ }),
1297
+ },
1298
+ );
1299
+
1300
+ for await (const ev of gen) {
1301
+ if (ev.type === 'download-started') {
1302
+ process.stderr.write(`download-started ${ev.vendor}\n`);
1303
+ } else if (ev.type === 'download-progress') {
1304
+ process.stderr.write(`download-progress ${ev.vendor} ${ev.receivedBytes}\n`);
1305
+ } else if (ev.type === 'extracting') {
1306
+ process.stderr.write(`extracting ${ev.vendor}\n`);
1307
+ } else if (ev.type === 'indexing') {
1308
+ process.stderr.write(`indexing ${ev.vendor} (${ev.iconCount} icons)\n`);
1309
+ } else if (ev.type === 'terms-required') {
1310
+ printError(
1311
+ `Vendor '${ev.vendor}' requires accepting the license at ${ev.licenseUrl}. Re-run with --accept-terms.`,
1312
+ );
1313
+ } else if (ev.type === 'error') {
1314
+ printError(`Install failed: ${ev.message}`);
1315
+ } else if (ev.type === 'done') {
1316
+ printOk({ installed: ev.vendor, version: ev.version, iconCount: ev.iconCount });
1317
+ }
1318
+ }
1319
+ printError('Install ended without a terminal event.');
1320
+ }
1321
+
1322
+ async function runIconsUpdate() {
1323
+ // Installer's rmSync-then-extract handles wipe; behaviour matches add.
1324
+ await runIconsAdd();
1325
+ }
1326
+
1327
+ async function runIconsRemove() {
1328
+ const vendor = parseIconVendor('remove');
1329
+ const { removeIconPack } = await import('./icons/remove.ts');
1330
+ const { iconCacheRoot } = await import('./icons/paths.ts');
1331
+ removeIconPack(vendor, { cacheRoot: iconCacheRoot() });
1332
+ printOk({ removed: vendor });
1333
+ }
@@ -0,0 +1,31 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, normalize, sep } from 'node:path';
3
+ import { unzipSync } from 'fflate';
4
+
5
+ export async function extractZipToDir(buffer: Buffer, destDir: string): Promise<string[]> {
6
+ const entries = unzipSync(new Uint8Array(buffer));
7
+ const written: string[] = [];
8
+ const root = normalize(destDir) + sep;
9
+ for (const [entryPath, data] of Object.entries(entries)) {
10
+ if (!entryPath.toLowerCase().endsWith('.svg')) continue;
11
+ const segments = entryPath.split(/[\\/]/);
12
+ if (segments.some((s) => s === '..')) {
13
+ throw new Error(`Zip entry escapes destination: ${entryPath}`);
14
+ }
15
+ // Skip macOS AppleDouble metadata. The AWS zip ships these alongside the
16
+ // real SVGs (in __MACOSX/ and as ._-prefixed siblings); they have an .svg
17
+ // extension but the contents are binary, which renders as broken icons
18
+ // and pollutes the index with arch-amazon-* duplicate entries.
19
+ if (segments.includes('__MACOSX')) continue;
20
+ const flatName = segments.pop();
21
+ if (!flatName || flatName.startsWith('._')) continue;
22
+ const target = normalize(join(destDir, flatName));
23
+ if (!target.startsWith(root)) {
24
+ throw new Error(`Zip entry escapes destination: ${entryPath}`);
25
+ }
26
+ mkdirSync(dirname(target), { recursive: true });
27
+ writeFileSync(target, data);
28
+ written.push(flatName);
29
+ }
30
+ return written;
31
+ }
@@ -0,0 +1,31 @@
1
+ export interface FetchWithProgressOptions {
2
+ onProgress?: (receivedBytes: number) => void;
3
+ fetchFn?: (url: string) => Promise<Response>;
4
+ }
5
+
6
+ export async function fetchWithProgress(
7
+ url: string,
8
+ opts: FetchWithProgressOptions = {},
9
+ ): Promise<Buffer> {
10
+ const fetchFn = opts.fetchFn ?? ((u: string) => fetch(u));
11
+ const res = await fetchFn(url);
12
+ if (!res.ok) {
13
+ throw new Error(`fetch ${url} failed: ${res.status} ${res.statusText}`);
14
+ }
15
+ if (!res.body) {
16
+ throw new Error(`fetch ${url} returned no body`);
17
+ }
18
+
19
+ const reader = res.body.getReader();
20
+ const chunks: Uint8Array[] = [];
21
+ let received = 0;
22
+ while (true) {
23
+ const { done, value } = await reader.read();
24
+ if (done) break;
25
+ if (!value) continue;
26
+ chunks.push(value);
27
+ received += value.byteLength;
28
+ opts.onProgress?.(received);
29
+ }
30
+ return Buffer.concat(chunks);
31
+ }
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { writeFileAtomic } from '../atomic-write.ts';
4
+ import type { IconVendor } from './paths.ts';
5
+
6
+ export interface InstalledPack {
7
+ vendor: IconVendor;
8
+ version: string;
9
+ installedAt: number;
10
+ sizeBytes: number;
11
+ /** Map of canonical icon name → cache-root-relative SVG path. */
12
+ icons: Record<string, string>;
13
+ }
14
+
15
+ export interface IconIndex {
16
+ version: 1;
17
+ packs: Partial<Record<IconVendor, InstalledPack>>;
18
+ }
19
+
20
+ const EMPTY: IconIndex = { version: 1, packs: {} };
21
+
22
+ export function readIndex(cacheRoot: string): IconIndex {
23
+ const file = join(cacheRoot, 'index.json');
24
+ if (!existsSync(file)) return { ...EMPTY };
25
+ try {
26
+ const parsed = JSON.parse(readFileSync(file, 'utf8')) as IconIndex;
27
+ if (parsed?.version !== 1 || typeof parsed.packs !== 'object') return { ...EMPTY };
28
+ return parsed;
29
+ } catch {
30
+ return { ...EMPTY };
31
+ }
32
+ }
33
+
34
+ export function writeIndex(cacheRoot: string, idx: IconIndex): void {
35
+ writeFileAtomic(join(cacheRoot, 'index.json'), JSON.stringify(idx, null, 2));
36
+ }
37
+
38
+ export function upsertPack(idx: IconIndex, pack: InstalledPack): IconIndex {
39
+ return { version: 1, packs: { ...idx.packs, [pack.vendor]: pack } };
40
+ }
41
+
42
+ export function removePack(idx: IconIndex, vendor: IconVendor): IconIndex {
43
+ const { [vendor]: _omit, ...rest } = idx.packs;
44
+ return { version: 1, packs: rest };
45
+ }
@@ -0,0 +1,16 @@
1
+ import type { IconVendor } from './paths.ts';
2
+
3
+ export type InstallEvent =
4
+ | { type: 'terms-required'; vendor: IconVendor; licenseUrl: string }
5
+ | { type: 'download-started'; vendor: IconVendor; expectedBytes: number | null }
6
+ | { type: 'download-progress'; vendor: IconVendor; receivedBytes: number }
7
+ | { type: 'extracting'; vendor: IconVendor }
8
+ | { type: 'indexing'; vendor: IconVendor; iconCount: number }
9
+ | { type: 'done'; vendor: IconVendor; version: string; iconCount: number }
10
+ | { type: 'error'; vendor: IconVendor; message: string };
11
+
12
+ export interface InstallOptions {
13
+ acceptTerms?: boolean;
14
+ /** Optional URL override for tests; production picks the vendor default. */
15
+ packUrl?: string;
16
+ }
@@ -0,0 +1,76 @@
1
+ import { mkdirSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { extractZipToDir } from './extract-zip.ts';
4
+ import { readIndex, upsertPack, writeIndex } from './index-store.ts';
5
+ import type { InstallEvent, InstallOptions } from './installer-types.ts';
6
+ import { withVendorLock } from './lock.ts';
7
+ import type { IconVendor } from './paths.ts';
8
+ import { vendorDescriptor } from './vendors.ts';
9
+
10
+ export interface InstallerDeps {
11
+ cacheRoot: string;
12
+ now: () => number;
13
+ version: () => string;
14
+ fetcher: (url: string) => Promise<Buffer>;
15
+ }
16
+
17
+ export async function* installIconPack(
18
+ args: { vendor: IconVendor } & InstallOptions,
19
+ deps: InstallerDeps,
20
+ ): AsyncGenerator<InstallEvent> {
21
+ const desc = vendorDescriptor(args.vendor);
22
+ if (desc.requiresAcceptance && !args.acceptTerms) {
23
+ yield { type: 'terms-required', vendor: args.vendor, licenseUrl: desc.licenseUrl };
24
+ return;
25
+ }
26
+
27
+ const events: InstallEvent[] = [];
28
+ const lockPath = join(deps.cacheRoot, '.locks', `${args.vendor}.lock`);
29
+ await withVendorLock(lockPath, async () => {
30
+ try {
31
+ events.push({ type: 'download-started', vendor: args.vendor, expectedBytes: null });
32
+ const buffer = await deps.fetcher(args.packUrl ?? desc.defaultPackUrl);
33
+
34
+ events.push({ type: 'extracting', vendor: args.vendor });
35
+ const version = deps.version();
36
+ const destDir = join(deps.cacheRoot, args.vendor, version);
37
+ rmSync(destDir, { recursive: true, force: true });
38
+ mkdirSync(destDir, { recursive: true });
39
+ const writtenFilenames = await extractZipToDir(buffer, destDir);
40
+
41
+ const icons: Record<string, string> = {};
42
+ for (const filename of writtenFilenames) {
43
+ const canonical = desc.canonicalName(filename);
44
+ if (!canonical) continue;
45
+ icons[canonical] = `${args.vendor}/${version}/${filename}`;
46
+ }
47
+ events.push({ type: 'indexing', vendor: args.vendor, iconCount: Object.keys(icons).length });
48
+
49
+ const sizeBytes = Object.values(icons).length;
50
+ const idx = readIndex(deps.cacheRoot);
51
+ const next = upsertPack(idx, {
52
+ vendor: args.vendor,
53
+ version,
54
+ installedAt: deps.now(),
55
+ sizeBytes,
56
+ icons,
57
+ });
58
+ writeIndex(deps.cacheRoot, next);
59
+
60
+ events.push({
61
+ type: 'done',
62
+ vendor: args.vendor,
63
+ version,
64
+ iconCount: Object.keys(icons).length,
65
+ });
66
+ } catch (err) {
67
+ events.push({
68
+ type: 'error',
69
+ vendor: args.vendor,
70
+ message: err instanceof Error ? err.message : String(err),
71
+ });
72
+ }
73
+ });
74
+
75
+ for (const ev of events) yield ev;
76
+ }
@@ -0,0 +1,78 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { InstallEvent } from './installer-types.ts';
3
+ import type { IconVendor } from './paths.ts';
4
+
5
+ interface Job {
6
+ id: string;
7
+ vendor: IconVendor;
8
+ events: InstallEvent[];
9
+ complete: boolean;
10
+ subscribers: Set<(ev: InstallEvent) => void>;
11
+ endSubscribers: Set<() => void>;
12
+ }
13
+
14
+ export interface JobRegistry {
15
+ create(vendor: IconVendor): string;
16
+ append(id: string, ev: InstallEvent): void;
17
+ markComplete(id: string): void;
18
+ get(id: string): Job | undefined;
19
+ subscribe(id: string, onEvent: (ev: InstallEvent) => void, onEnd: () => void): () => void;
20
+ inFlightFor(vendor: IconVendor): string | undefined;
21
+ }
22
+
23
+ export function createJobRegistry(): JobRegistry {
24
+ const jobs = new Map<string, Job>();
25
+ return {
26
+ create(vendor) {
27
+ for (const j of jobs.values()) {
28
+ if (j.vendor === vendor && !j.complete) {
29
+ throw new Error(`Install for vendor ${vendor} already in flight (job ${j.id})`);
30
+ }
31
+ }
32
+ const id = randomUUID();
33
+ jobs.set(id, {
34
+ id,
35
+ vendor,
36
+ events: [],
37
+ complete: false,
38
+ subscribers: new Set(),
39
+ endSubscribers: new Set(),
40
+ });
41
+ return id;
42
+ },
43
+ append(id, ev) {
44
+ const j = jobs.get(id);
45
+ if (!j) return;
46
+ j.events.push(ev);
47
+ for (const sub of j.subscribers) sub(ev);
48
+ },
49
+ markComplete(id) {
50
+ const j = jobs.get(id);
51
+ if (!j) return;
52
+ j.complete = true;
53
+ for (const onEnd of j.endSubscribers) onEnd();
54
+ },
55
+ get: (id) => jobs.get(id),
56
+ subscribe(id, onEvent, onEnd) {
57
+ const j = jobs.get(id);
58
+ if (!j) return () => undefined;
59
+ for (const ev of j.events) onEvent(ev);
60
+ if (j.complete) {
61
+ onEnd();
62
+ return () => undefined;
63
+ }
64
+ j.subscribers.add(onEvent);
65
+ j.endSubscribers.add(onEnd);
66
+ return () => {
67
+ j.subscribers.delete(onEvent);
68
+ j.endSubscribers.delete(onEnd);
69
+ };
70
+ },
71
+ inFlightFor(vendor) {
72
+ for (const j of jobs.values()) {
73
+ if (j.vendor === vendor && !j.complete) return j.id;
74
+ }
75
+ return undefined;
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,36 @@
1
+ import type { IconIndex } from './index-store.ts';
2
+ import type { IconVendor } from './paths.ts';
3
+
4
+ const ALL: IconVendor[] = ['aws', 'azure'];
5
+
6
+ export type PackSummary =
7
+ | {
8
+ vendor: IconVendor;
9
+ installed: true;
10
+ version: string;
11
+ iconCount: number;
12
+ sizeBytes: number;
13
+ /**
14
+ * Canonical icon names (kebab-case) installed in this pack, sorted
15
+ * alphabetically. Mirrors the canvas-side PackSummary so apps/web can
16
+ * pass the wire JSON straight through to the picker's vendor grids.
17
+ */
18
+ iconNames: string[];
19
+ }
20
+ | { vendor: IconVendor; installed: false };
21
+
22
+ export function summarizePacks(idx: IconIndex): PackSummary[] {
23
+ return ALL.map((vendor) => {
24
+ const p = idx.packs[vendor];
25
+ if (!p) return { vendor, installed: false };
26
+ const names = Object.keys(p.icons).sort();
27
+ return {
28
+ vendor,
29
+ installed: true,
30
+ version: p.version,
31
+ iconCount: names.length,
32
+ sizeBytes: p.sizeBytes,
33
+ iconNames: names,
34
+ };
35
+ });
36
+ }
@@ -0,0 +1,13 @@
1
+ const queues = new Map<string, Promise<unknown>>();
2
+
3
+ export async function withVendorLock<T>(lockPath: string, fn: () => Promise<T>): Promise<T> {
4
+ const prev = queues.get(lockPath) ?? Promise.resolve();
5
+ const run = prev.then(fn, fn);
6
+ const tracked = run.catch(() => undefined);
7
+ queues.set(lockPath, tracked);
8
+ try {
9
+ return await run;
10
+ } finally {
11
+ if (queues.get(lockPath) === tracked) queues.delete(lockPath);
12
+ }
13
+ }
@@ -0,0 +1,19 @@
1
+ const STRIP_PREFIXES = ['Arch_AWS-', 'Arch_Amazon-', 'Arch-Category_', 'Arch_'];
2
+ const SIZE_SUFFIX = /_(?:16|32|48|64)$/;
3
+
4
+ export function canonicalAwsName(filename: string): string | null {
5
+ if (!filename.toLowerCase().endsWith('.svg')) return null;
6
+ let base = filename.slice(0, -'.svg'.length);
7
+ for (const prefix of STRIP_PREFIXES) {
8
+ if (base.startsWith(prefix)) {
9
+ base = base.slice(prefix.length);
10
+ break;
11
+ }
12
+ }
13
+ base = base.replace(SIZE_SUFFIX, '');
14
+ const kebab = base
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ .replace(/^-+|-+$/g, '');
18
+ return kebab.length > 0 ? kebab : null;
19
+ }
@@ -0,0 +1,11 @@
1
+ export function canonicalAzureName(filename: string): string | null {
2
+ if (!filename.toLowerCase().endsWith('.svg')) return null;
3
+ let base = filename.slice(0, -'.svg'.length);
4
+ base = base.replace(/^\d+-icon-service-/i, '');
5
+ base = base.replace(/^icon-service-/i, '');
6
+ const kebab = base
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9]+/g, '-')
9
+ .replace(/^-+|-+$/g, '');
10
+ return kebab.length > 0 ? kebab : null;
11
+ }
@@ -0,0 +1,14 @@
1
+ import { join } from 'node:path';
2
+ import { seeflowHome } from '../paths.ts';
3
+
4
+ export type IconVendor = 'aws' | 'azure';
5
+
6
+ export const iconCacheRoot = (): string => join(seeflowHome(), 'icons');
7
+
8
+ export const iconVendorRoot = (vendor: IconVendor, version: string): string =>
9
+ join(iconCacheRoot(), vendor, version);
10
+
11
+ export const iconLockPath = (vendor: IconVendor): string =>
12
+ join(iconCacheRoot(), '.locks', `${vendor}.lock`);
13
+
14
+ export const iconIndexPath = (): string => join(iconCacheRoot(), 'index.json');
@@ -0,0 +1,16 @@
1
+ import { mkdirSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { readIndex, removePack, writeIndex } from './index-store.ts';
4
+ import type { IconVendor } from './paths.ts';
5
+
6
+ export interface RemoveIconPackDeps {
7
+ cacheRoot: string;
8
+ }
9
+
10
+ export function removeIconPack(vendor: IconVendor, deps: RemoveIconPackDeps): void {
11
+ rmSync(join(deps.cacheRoot, vendor), { recursive: true, force: true });
12
+ const idx = readIndex(deps.cacheRoot);
13
+ if (!idx.packs[vendor]) return;
14
+ mkdirSync(deps.cacheRoot, { recursive: true });
15
+ writeIndex(deps.cacheRoot, removePack(idx, vendor));
16
+ }