@seed-app-studio/cli 0.1.1-canary.0

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.
@@ -0,0 +1,972 @@
1
+ import { access, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createHash } from "node:crypto";
5
+ import { validateSeedAppBundleManifest, validateSeedAppManifest } from "@seed-app-studio/protocol";
6
+ //#region src/scaffold.ts
7
+ async function initSeedAppProject(options) {
8
+ const rootDir = path.resolve(options.rootDir);
9
+ await assertCanWriteRoot(rootDir, options.force ?? false);
10
+ const files = createTemplateFiles(options);
11
+ await mkdir(path.join(rootDir, "src"), { recursive: true });
12
+ await Promise.all(files.map((file) => writeFile(path.join(rootDir, file.path), file.content, "utf8")));
13
+ return {
14
+ rootDir,
15
+ files: files.map((file) => file.path).toSorted()
16
+ };
17
+ }
18
+ async function assertCanWriteRoot(rootDir, force) {
19
+ await mkdir(rootDir, { recursive: true });
20
+ if ((await readdir(rootDir)).length > 0 && !force) throw new Error("Target directory is not empty. Pass --force to scaffold into it.");
21
+ }
22
+ function createTemplateFiles(options) {
23
+ const packageName = normalizePackageName(options.appId);
24
+ const manifest = {
25
+ schemaVersion: 1,
26
+ id: options.appId,
27
+ version: "0.1.0",
28
+ name: options.appName,
29
+ kind: "vertical-app",
30
+ entry: {
31
+ type: "iframe",
32
+ url: options.devUrl
33
+ },
34
+ contributes: { launcher: { label: options.appName } },
35
+ runtime: {
36
+ sdkVersion: "^0.1.0",
37
+ framework: "react"
38
+ },
39
+ capabilities: {
40
+ agent: ["run"],
41
+ memory: ["search"],
42
+ mcp: ["listTools", "callTool"],
43
+ workspace: ["write"]
44
+ },
45
+ grantDefaults: {
46
+ "agent.run": "enabled",
47
+ "memory.search": "enabled",
48
+ "mcp.listTools": "enabled",
49
+ "mcp.callTool": "ask",
50
+ "workspace.write": "ask"
51
+ },
52
+ userConfigurable: {
53
+ "mcp.listTools": true,
54
+ "mcp.callTool": true,
55
+ "workspace.write": true
56
+ }
57
+ };
58
+ return [
59
+ {
60
+ path: "package.json",
61
+ content: json({
62
+ name: packageName,
63
+ version: "0.1.0",
64
+ private: true,
65
+ type: "module",
66
+ scripts: {
67
+ dev: "vite --host 127.0.0.1",
68
+ build: "vite build",
69
+ typecheck: "tsc --noEmit"
70
+ },
71
+ dependencies: {
72
+ "@seed-app-studio/sdk": options.sdkDependency ?? "^0.1.0",
73
+ "@vitejs/plugin-react": "^5.0.0",
74
+ vite: "^7.0.0",
75
+ typescript: "^5.8.3",
76
+ react: "^19.0.0",
77
+ "react-dom": "^19.0.0"
78
+ },
79
+ devDependencies: {
80
+ "@types/react": "^19.0.0",
81
+ "@types/react-dom": "^19.0.0"
82
+ }
83
+ })
84
+ },
85
+ {
86
+ path: "index.html",
87
+ content: "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Seed App</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"/src/main.tsx\"><\/script>\n </body>\n</html>\n"
88
+ },
89
+ {
90
+ path: "src/main.tsx",
91
+ content: "import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { App } from './App.js';\nimport './styles.css';\n\ncreateRoot(document.getElementById('root')!).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>\n);\n"
92
+ },
93
+ {
94
+ path: "src/App.tsx",
95
+ content: createAppSource(options.appId)
96
+ },
97
+ {
98
+ path: "src/styles.css",
99
+ content: "body {\n margin: 0;\n font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #f7f8fa;\n color: #1d2129;\n}\n\nmain {\n box-sizing: border-box;\n min-height: 100vh;\n padding: 32px;\n}\n\nbutton {\n border: 0;\n border-radius: 6px;\n background: #165dff;\n color: white;\n cursor: pointer;\n font: inherit;\n padding: 10px 14px;\n}\n\npre {\n overflow: auto;\n border-radius: 6px;\n background: white;\n padding: 16px;\n}\n"
100
+ },
101
+ {
102
+ path: "seed-app.json",
103
+ content: json(manifest)
104
+ },
105
+ {
106
+ path: "tsconfig.json",
107
+ content: json({
108
+ compilerOptions: {
109
+ target: "ES2022",
110
+ useDefineForClassFields: true,
111
+ lib: [
112
+ "DOM",
113
+ "DOM.Iterable",
114
+ "ES2022"
115
+ ],
116
+ allowJs: false,
117
+ skipLibCheck: true,
118
+ esModuleInterop: true,
119
+ allowSyntheticDefaultImports: true,
120
+ strict: true,
121
+ forceConsistentCasingInFileNames: true,
122
+ module: "ESNext",
123
+ moduleResolution: "Bundler",
124
+ resolveJsonModule: true,
125
+ isolatedModules: true,
126
+ noEmit: true,
127
+ jsx: "react-jsx"
128
+ },
129
+ include: ["src"]
130
+ })
131
+ },
132
+ {
133
+ path: "vite.config.ts",
134
+ content: "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({\n plugins: [react()],\n server: {\n host: '127.0.0.1',\n port: 5173,\n },\n});\n"
135
+ }
136
+ ];
137
+ }
138
+ function createAppSource(appId) {
139
+ return `import { useEffect, useState } from 'react';
140
+ import { createSeedAppClient, createWindowSeedAppTransport, installSeedAppConsoleBridge, type SeedAppClient, type SeedAppHandshakeResult } from '@seed-app-studio/sdk';
141
+
142
+ export function App() {
143
+ const [client, setClient] = useState<SeedAppClient>();
144
+ const [handshake, setHandshake] = useState<SeedAppHandshakeResult | null>(null);
145
+ const [agentResult, setAgentResult] = useState<unknown>(null);
146
+ const [error, setError] = useState<string | null>(null);
147
+
148
+ useEffect(() => {
149
+ const activeClient = createSeedAppClient({
150
+ transport: createWindowSeedAppTransport({ timeoutMs: 5000 }),
151
+ });
152
+ setClient(activeClient);
153
+ const consoleBridge = installSeedAppConsoleBridge();
154
+ console.log('[seed-app] initializing SDK handshake');
155
+ let cancelled = false;
156
+ activeClient
157
+ .handshake({ appId: '${appId}', sdkVersion: '0.1.0' })
158
+ .then((result) => {
159
+ console.info('[seed-app] handshake completed', result);
160
+ if (!cancelled) setHandshake(result);
161
+ })
162
+ .catch((reason: unknown) => {
163
+ console.warn('[seed-app] handshake failed', reason);
164
+ if (!cancelled) setError(reason instanceof Error ? reason.message : String(reason));
165
+ });
166
+ return () => {
167
+ cancelled = true;
168
+ consoleBridge.uninstall();
169
+ activeClient.close();
170
+ };
171
+ }, []);
172
+
173
+ const runAgent = async () => {
174
+ if (!client) return;
175
+ try {
176
+ console.log('[seed-app] running agent story');
177
+ const result = await client.agent.run({ input: 'Draft a short account plan.' });
178
+ console.info('[seed-app] agent story completed', result);
179
+ setAgentResult(result);
180
+ } catch (reason) {
181
+ console.error('[seed-app] agent story failed', reason);
182
+ setError(reason instanceof Error ? reason.message : String(reason));
183
+ }
184
+ };
185
+
186
+ return (
187
+ <main>
188
+ <h1>Seed App</h1>
189
+ <p>Handshake: {handshake ? 'connected' : 'waiting'}</p>
190
+ <button type="button" onClick={runAgent} disabled={!client}>
191
+ Run agent
192
+ </button>
193
+ {error ? <p role="alert">{error}</p> : null}
194
+ <pre>{JSON.stringify({ handshake, agentResult }, null, 2)}</pre>
195
+ </main>
196
+ );
197
+ }
198
+ `;
199
+ }
200
+ function normalizePackageName(appId) {
201
+ return appId.replace(/[^a-z0-9.-]/gi, "-").toLowerCase();
202
+ }
203
+ function json(value) {
204
+ return `${JSON.stringify(value, null, 2)}\n`;
205
+ }
206
+ //#endregion
207
+ //#region src/zipWriter.ts
208
+ const CRC32_TABLE = createCrc32Table();
209
+ async function collectZipSourceFiles(rootDir) {
210
+ const files = [];
211
+ await collectFiles(rootDir, rootDir, files);
212
+ return files.toSorted((left, right) => left.archivePath.localeCompare(right.archivePath));
213
+ }
214
+ async function writeZipArchive(files, outFile) {
215
+ const chunks = [];
216
+ const entries = [];
217
+ let offset = 0;
218
+ const preparedFiles = await Promise.all(files.map(async (file) => ({
219
+ archivePath: normalizeArchivePath$1(file.archivePath),
220
+ data: typeof file.data === "string" ? Buffer.from(file.data) : file.data ?? await readFile(file.absolutePath)
221
+ })));
222
+ for (const file of preparedFiles) {
223
+ const { archivePath, data } = file;
224
+ const fileName = Buffer.from(archivePath);
225
+ const crc32 = computeCrc32(data);
226
+ const localHeader = createLocalFileHeader(fileName, crc32, data.length);
227
+ chunks.push(localHeader, fileName, data);
228
+ entries.push({
229
+ archivePath,
230
+ crc32,
231
+ size: data.length,
232
+ offset
233
+ });
234
+ offset += localHeader.length + fileName.length + data.length;
235
+ }
236
+ const centralDirectoryOffset = offset;
237
+ for (const entry of entries) {
238
+ const fileName = Buffer.from(entry.archivePath);
239
+ const centralHeader = createCentralDirectoryHeader(fileName, entry);
240
+ chunks.push(centralHeader, fileName);
241
+ offset += centralHeader.length + fileName.length;
242
+ }
243
+ chunks.push(createEndOfCentralDirectory(entries.length, offset - centralDirectoryOffset, centralDirectoryOffset));
244
+ await writeFile(outFile, Buffer.concat(chunks));
245
+ }
246
+ async function collectFiles(rootDir, currentDir, files) {
247
+ const entries = await readdir(currentDir, { withFileTypes: true });
248
+ await Promise.all(entries.map(async (entry) => {
249
+ const absolutePath = path.join(currentDir, entry.name);
250
+ if (entry.isDirectory()) {
251
+ await collectFiles(rootDir, absolutePath, files);
252
+ return;
253
+ }
254
+ if (entry.isFile()) files.push({
255
+ absolutePath,
256
+ archivePath: path.relative(rootDir, absolutePath)
257
+ });
258
+ }));
259
+ }
260
+ function createLocalFileHeader(fileName, crc32, size) {
261
+ const buffer = Buffer.alloc(30);
262
+ buffer.writeUInt32LE(67324752, 0);
263
+ buffer.writeUInt16LE(20, 4);
264
+ buffer.writeUInt16LE(0, 6);
265
+ buffer.writeUInt16LE(0, 8);
266
+ writeDosTimestamp(buffer, 10);
267
+ buffer.writeUInt32LE(crc32, 14);
268
+ buffer.writeUInt32LE(size, 18);
269
+ buffer.writeUInt32LE(size, 22);
270
+ buffer.writeUInt16LE(fileName.length, 26);
271
+ buffer.writeUInt16LE(0, 28);
272
+ return buffer;
273
+ }
274
+ function createCentralDirectoryHeader(fileName, entry) {
275
+ const buffer = Buffer.alloc(46);
276
+ buffer.writeUInt32LE(33639248, 0);
277
+ buffer.writeUInt16LE(20, 4);
278
+ buffer.writeUInt16LE(20, 6);
279
+ buffer.writeUInt16LE(0, 8);
280
+ buffer.writeUInt16LE(0, 10);
281
+ writeDosTimestamp(buffer, 12);
282
+ buffer.writeUInt32LE(entry.crc32, 16);
283
+ buffer.writeUInt32LE(entry.size, 20);
284
+ buffer.writeUInt32LE(entry.size, 24);
285
+ buffer.writeUInt16LE(fileName.length, 28);
286
+ buffer.writeUInt16LE(0, 30);
287
+ buffer.writeUInt16LE(0, 32);
288
+ buffer.writeUInt16LE(0, 34);
289
+ buffer.writeUInt16LE(0, 36);
290
+ buffer.writeUInt32LE(0, 38);
291
+ buffer.writeUInt32LE(entry.offset, 42);
292
+ return buffer;
293
+ }
294
+ function createEndOfCentralDirectory(entryCount, directorySize, directoryOffset) {
295
+ const buffer = Buffer.alloc(22);
296
+ buffer.writeUInt32LE(101010256, 0);
297
+ buffer.writeUInt16LE(0, 4);
298
+ buffer.writeUInt16LE(0, 6);
299
+ buffer.writeUInt16LE(entryCount, 8);
300
+ buffer.writeUInt16LE(entryCount, 10);
301
+ buffer.writeUInt32LE(directorySize, 12);
302
+ buffer.writeUInt32LE(directoryOffset, 16);
303
+ buffer.writeUInt16LE(0, 20);
304
+ return buffer;
305
+ }
306
+ function writeDosTimestamp(buffer, offset) {
307
+ const date = /* @__PURE__ */ new Date("2026-01-01T00:00:00Z");
308
+ const timeValue = date.getUTCHours() << 11 | date.getUTCMinutes() << 5 | Math.floor(date.getUTCSeconds() / 2);
309
+ const dateValue = date.getUTCFullYear() - 1980 << 9 | date.getUTCMonth() + 1 << 5 | date.getUTCDate();
310
+ buffer.writeUInt16LE(timeValue, offset);
311
+ buffer.writeUInt16LE(dateValue, offset + 2);
312
+ }
313
+ function computeCrc32(data) {
314
+ let crc = 4294967295;
315
+ for (const byte of data) crc = crc >>> 8 ^ CRC32_TABLE[(crc ^ byte) & 255];
316
+ return (crc ^ 4294967295) >>> 0;
317
+ }
318
+ function createCrc32Table() {
319
+ return Array.from({ length: 256 }, (_, index) => {
320
+ let value = index;
321
+ for (let bit = 0; bit < 8; bit += 1) value = value & 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
322
+ return value >>> 0;
323
+ });
324
+ }
325
+ function normalizeArchivePath$1(input) {
326
+ return input.split(path.sep).join("/");
327
+ }
328
+ //#endregion
329
+ //#region src/exampleArchive.ts
330
+ const EXAMPLE_ID = "react-spa-stories";
331
+ const DEFAULT_OUT_FILE = "seed-react-spa-stories.zip";
332
+ async function writeSeedAppExampleArchive(options = {}) {
333
+ const exampleDir = await resolveExampleDir();
334
+ const outFile = path.resolve(options.outFile ?? DEFAULT_OUT_FILE);
335
+ const files = await prepareExampleFiles(exampleDir, options);
336
+ await writeZipArchive(files, outFile);
337
+ return {
338
+ exampleId: EXAMPLE_ID,
339
+ outFile,
340
+ files: files.map((file) => file.archivePath)
341
+ };
342
+ }
343
+ async function prepareExampleFiles(exampleDir, options) {
344
+ const files = await collectZipSourceFiles(exampleDir);
345
+ if (!options.sdkDependency && !options.cliDependency) return files;
346
+ const packageFile = files.find((file) => file.archivePath === "package.json");
347
+ if (!packageFile) throw new Error("Seed app example package.json was not found.");
348
+ const packageJson = JSON.parse(await readFile(packageFile.absolutePath, "utf8"));
349
+ if (options.sdkDependency) packageJson.dependencies = {
350
+ ...packageJson.dependencies,
351
+ "@seed-app-studio/sdk": options.sdkDependency
352
+ };
353
+ if (options.cliDependency) packageJson.devDependencies = {
354
+ ...packageJson.devDependencies,
355
+ "@seed-app-studio/cli": options.cliDependency
356
+ };
357
+ return files.map((file) => file.archivePath === "package.json" ? {
358
+ absolutePath: file.absolutePath,
359
+ archivePath: file.archivePath,
360
+ data: `${JSON.stringify(packageJson, null, 2)}\n`
361
+ } : file);
362
+ }
363
+ async function resolveExampleDir() {
364
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
365
+ const candidates = [path.resolve(moduleDir, "../examples", EXAMPLE_ID), path.resolve(moduleDir, "../../examples", EXAMPLE_ID)];
366
+ const exampleDir = (await Promise.all(candidates.map(async (candidate) => {
367
+ try {
368
+ await access(path.join(candidate, "seed-app.json"));
369
+ return candidate;
370
+ } catch {
371
+ return;
372
+ }
373
+ }))).find((candidate) => typeof candidate === "string");
374
+ if (exampleDir) return exampleDir;
375
+ throw new Error(`Seed app example "${EXAMPLE_ID}" was not found.`);
376
+ }
377
+ //#endregion
378
+ //#region src/packageArchive.ts
379
+ const DEFAULT_MANIFEST_PATH$1 = "seed-app.json";
380
+ const DEFAULT_BUNDLE_PATH$1 = "seed-app.bundle.json";
381
+ const DEFAULT_REPORT_PATH$1 = "seed-app.integrity-report.json";
382
+ async function writeSeedAppPackageArchive(options) {
383
+ const rootDir = path.resolve(options.rootDir);
384
+ const outFile = path.resolve(rootDir, options.zipFile);
385
+ const manifestFile = toPackageSourceFile(rootDir, options.manifestPath ?? DEFAULT_MANIFEST_PATH$1);
386
+ const bundleFile = toPackageSourceFile(rootDir, options.bundlePath ?? DEFAULT_BUNDLE_PATH$1);
387
+ const files = dedupeArchiveFiles([
388
+ manifestFile,
389
+ bundleFile,
390
+ ...(await readBundleManifest(bundleFile.absolutePath)).assets.map((asset) => toPackageSourceFile(rootDir, asset.path)),
391
+ reportSourceFile(rootDir, options.reportPath ?? DEFAULT_REPORT_PATH$1, options.report)
392
+ ]).filter((file) => path.resolve(file.absolutePath) !== outFile);
393
+ await mkdir(path.dirname(outFile), { recursive: true });
394
+ await writeZipArchive(files, outFile);
395
+ return {
396
+ outFile,
397
+ files: files.map((file) => file.archivePath)
398
+ };
399
+ }
400
+ async function inspectSeedAppPackageArchive(options) {
401
+ const archiveFile = path.isAbsolute(options.zipFile) ? path.resolve(options.zipFile) : path.resolve(options.rootDir, options.zipFile);
402
+ const issues = [];
403
+ const entries = await readZipArchiveEntries(archiveFile, issues);
404
+ const entryMap = new Map(entries.map((entry) => [entry.archivePath, entry]));
405
+ const manifestEntry = requireArchiveEntry(entries, DEFAULT_MANIFEST_PATH$1, issues);
406
+ const bundleEntry = requireArchiveEntry(entries, DEFAULT_BUNDLE_PATH$1, issues);
407
+ const reportEntry = requireArchiveEntry(entries, DEFAULT_REPORT_PATH$1, issues);
408
+ const manifestInput = readJsonArchiveEntry(manifestEntry, issues);
409
+ const bundleInput = readJsonArchiveEntry(bundleEntry, issues);
410
+ const reportInput = readJsonArchiveEntry(reportEntry, issues);
411
+ const manifestResult = validateSeedAppManifest(manifestInput);
412
+ const bundleResult = validateSeedAppBundleManifest(bundleInput);
413
+ if (manifestEntry && !manifestResult.success) issues.push(...manifestResult.errors);
414
+ if (bundleEntry && !bundleResult.success) issues.push(...bundleResult.errors);
415
+ validateIntegrityReport(reportEntry, reportInput, issues);
416
+ if (bundleResult.success) validateArchiveAssets(entryMap, bundleResult.data, issues);
417
+ const expectedTextFound = options.expectedText ? hasExpectedText(entries, options.expectedText) : void 0;
418
+ if (options.expectedText && !expectedTextFound) issues.push({
419
+ path: "expectText",
420
+ message: "Expected text marker was not found in archive contents."
421
+ });
422
+ return {
423
+ ok: issues.length === 0,
424
+ archiveFile,
425
+ files: entries.map((entry) => entry.archivePath),
426
+ manifestPath: manifestEntry?.archivePath,
427
+ bundlePath: bundleEntry?.archivePath,
428
+ reportPath: reportEntry?.archivePath,
429
+ appId: manifestResult.success ? manifestResult.data.id : void 0,
430
+ version: manifestResult.success ? manifestResult.data.version : void 0,
431
+ assetCount: bundleResult.success ? bundleResult.data.assets.length : 0,
432
+ expectedText: options.expectedText,
433
+ expectedTextFound,
434
+ issues
435
+ };
436
+ }
437
+ async function readBundleManifest(bundlePath) {
438
+ const result = validateSeedAppBundleManifest(JSON.parse(await readFile(bundlePath, "utf8")));
439
+ if (!result.success) throw new Error("Cannot create a package archive from an invalid bundle manifest.");
440
+ return result.data;
441
+ }
442
+ function toPackageSourceFile(rootDir, inputPath) {
443
+ const archivePath = toArchivePath(rootDir, inputPath);
444
+ return {
445
+ absolutePath: path.join(rootDir, archivePath),
446
+ archivePath
447
+ };
448
+ }
449
+ function reportSourceFile(rootDir, inputPath, report) {
450
+ const archivePath = isInsideRoot(rootDir, path.resolve(rootDir, inputPath)) ? toArchivePath(rootDir, inputPath) : DEFAULT_REPORT_PATH$1;
451
+ return {
452
+ absolutePath: path.join(rootDir, archivePath),
453
+ archivePath,
454
+ data: `${JSON.stringify(report, null, 2)}\n`
455
+ };
456
+ }
457
+ function toArchivePath(rootDir, inputPath) {
458
+ const absolutePath = path.isAbsolute(inputPath) ? path.resolve(inputPath) : path.resolve(rootDir, inputPath);
459
+ if (!isInsideRoot(rootDir, absolutePath)) throw new Error(`Package archive path must stay inside the app root: ${inputPath}`);
460
+ return normalizeArchivePath(path.relative(rootDir, absolutePath));
461
+ }
462
+ function isInsideRoot(rootDir, absolutePath) {
463
+ const relativePath = path.relative(rootDir, absolutePath);
464
+ return relativePath === "" || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
465
+ }
466
+ function dedupeArchiveFiles(files) {
467
+ const seen = /* @__PURE__ */ new Set();
468
+ return files.filter((file) => {
469
+ if (seen.has(file.archivePath)) return false;
470
+ seen.add(file.archivePath);
471
+ return true;
472
+ });
473
+ }
474
+ function normalizeArchivePath(input) {
475
+ return input.split(path.sep).join("/");
476
+ }
477
+ async function readZipArchiveEntries(archiveFile, issues) {
478
+ try {
479
+ return parseZipArchiveEntries(await readFile(archiveFile));
480
+ } catch (error) {
481
+ issues.push({
482
+ path: archiveFile,
483
+ message: error instanceof Error ? error.message : "Archive is unreadable."
484
+ });
485
+ return [];
486
+ }
487
+ }
488
+ function parseZipArchiveEntries(archive) {
489
+ const entries = [];
490
+ let offset = 0;
491
+ while (offset + 4 <= archive.length) {
492
+ const signature = archive.readUInt32LE(offset);
493
+ if (signature === 33639248 || signature === 101010256) break;
494
+ if (signature !== 67324752) throw new Error(`Unsupported zip header at offset ${offset}.`);
495
+ if (offset + 30 > archive.length) throw new Error("Zip local file header is truncated.");
496
+ const compressionMethod = archive.readUInt16LE(offset + 8);
497
+ const compressedSize = archive.readUInt32LE(offset + 18);
498
+ const uncompressedSize = archive.readUInt32LE(offset + 22);
499
+ const fileNameLength = archive.readUInt16LE(offset + 26);
500
+ const extraFieldLength = archive.readUInt16LE(offset + 28);
501
+ const fileNameStart = offset + 30;
502
+ const dataStart = fileNameStart + fileNameLength + extraFieldLength;
503
+ const dataEnd = dataStart + compressedSize;
504
+ if (dataEnd > archive.length) throw new Error("Zip file entry data is truncated.");
505
+ const archivePath = normalizeArchivePath(archive.subarray(fileNameStart, fileNameStart + fileNameLength).toString("utf8"));
506
+ if (compressionMethod !== 0) throw new Error(`Unsupported compression method ${compressionMethod} for ${archivePath}.`);
507
+ if (compressedSize !== uncompressedSize) throw new Error(`Compressed entry size is unsupported for ${archivePath}.`);
508
+ entries.push({
509
+ archivePath,
510
+ data: Buffer.from(archive.subarray(dataStart, dataEnd)),
511
+ size: uncompressedSize
512
+ });
513
+ offset = dataEnd;
514
+ }
515
+ if (entries.length === 0) throw new Error("Archive does not contain local file entries.");
516
+ return entries;
517
+ }
518
+ function requireArchiveEntry(entries, fileName, issues) {
519
+ const entry = entries.find((candidate) => candidate.archivePath === fileName) ?? entries.find((candidate) => candidate.archivePath.endsWith(`/${fileName}`));
520
+ if (!entry) issues.push({
521
+ path: fileName,
522
+ message: `Archive must include ${fileName}.`
523
+ });
524
+ return entry;
525
+ }
526
+ function readJsonArchiveEntry(entry, issues) {
527
+ if (!entry) return void 0;
528
+ try {
529
+ return JSON.parse(entry.data.toString("utf8"));
530
+ } catch {
531
+ issues.push({
532
+ path: entry.archivePath,
533
+ message: "Archive entry must contain valid JSON."
534
+ });
535
+ return;
536
+ }
537
+ }
538
+ function validateIntegrityReport(reportEntry, reportInput, issues) {
539
+ if (!reportEntry) return;
540
+ if (!isRecord(reportInput)) {
541
+ issues.push({
542
+ path: reportEntry.archivePath,
543
+ message: "Integrity report must be an object."
544
+ });
545
+ return;
546
+ }
547
+ if (reportInput.ok !== true) issues.push({
548
+ path: `${reportEntry.archivePath}.ok`,
549
+ message: "Integrity report must be successful."
550
+ });
551
+ }
552
+ function validateArchiveAssets(entryMap, bundle, issues) {
553
+ for (const asset of bundle.assets) {
554
+ const archivePath = normalizeAssetPath$1(asset.path);
555
+ const entry = entryMap.get(archivePath);
556
+ if (!entry) {
557
+ issues.push({
558
+ path: `assets.${asset.path}`,
559
+ message: "Bundle asset is missing from archive."
560
+ });
561
+ continue;
562
+ }
563
+ if (sha256Digest$1(entry.data) !== asset.sha256) issues.push({
564
+ path: `assets.${asset.path}.sha256`,
565
+ message: "Archive asset sha256 does not match bundle manifest."
566
+ });
567
+ if (entry.size !== asset.size) issues.push({
568
+ path: `assets.${asset.path}.size`,
569
+ message: "Archive asset size does not match bundle manifest."
570
+ });
571
+ }
572
+ if (!entryMap.has(normalizeAssetPath$1(bundle.entryHtml))) issues.push({
573
+ path: "bundle.entryHtml",
574
+ message: "Archive must include bundle entryHtml."
575
+ });
576
+ }
577
+ function hasExpectedText(entries, expectedText) {
578
+ const marker = Buffer.from(expectedText);
579
+ return entries.some((entry) => entry.data.indexOf(marker) >= 0);
580
+ }
581
+ function sha256Digest$1(content) {
582
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
583
+ }
584
+ function normalizeAssetPath$1(input) {
585
+ return normalizeArchivePath(input).replace(/^\/+/, "");
586
+ }
587
+ function isRecord(value) {
588
+ return typeof value === "object" && value !== null && !Array.isArray(value);
589
+ }
590
+ //#endregion
591
+ //#region src/validator.ts
592
+ const DEFAULT_MANIFEST_PATH = "seed-app.json";
593
+ const DEFAULT_BUNDLE_PATH = "seed-app.bundle.json";
594
+ const DEFAULT_REPORT_PATH = "seed-app.integrity-report.json";
595
+ async function validateSeedAppPackage(options) {
596
+ const manifestPath = resolveInputPath(options.rootDir, options.manifestPath ?? DEFAULT_MANIFEST_PATH);
597
+ const bundlePath = resolveInputPath(options.rootDir, options.bundlePath ?? DEFAULT_BUNDLE_PATH);
598
+ const issues = [];
599
+ const manifestInput = await readJsonFile(manifestPath, "manifest", issues);
600
+ const bundleInput = await readJsonFile(bundlePath, "bundle", issues);
601
+ const manifestResult = validateSeedAppManifest(manifestInput);
602
+ const bundleResult = validateSeedAppBundleManifest(bundleInput);
603
+ if (!manifestResult.success) issues.push(...manifestResult.errors);
604
+ if (!bundleResult.success) issues.push(...bundleResult.errors);
605
+ const manifest = manifestResult.success ? manifestResult.data : void 0;
606
+ const bundle = bundleResult.success ? bundleResult.data : void 0;
607
+ const assetReports = bundle ? await validateAssets(options.rootDir, bundle, issues) : [];
608
+ const entryIntegrity = bundle ? await computeEntryIntegrity(options.rootDir, bundle, issues) : void 0;
609
+ const computedBundleChecksum = bundle ? computeBundleChecksum(bundle, assetReports) : void 0;
610
+ if (manifest && bundle) validateManifestBundle(manifest, bundle, computedBundleChecksum, entryIntegrity, options.gatewayChecksum, issues);
611
+ return {
612
+ ok: issues.length === 0,
613
+ appId: manifest?.id,
614
+ version: manifest?.version,
615
+ entryHtml: bundle?.entryHtml,
616
+ entryIntegrity,
617
+ computedBundleChecksum,
618
+ manifestPath,
619
+ bundlePath,
620
+ assets: assetReports,
621
+ issues
622
+ };
623
+ }
624
+ async function writeSeedAppPackageReport(options) {
625
+ await writeGeneratedBundleManifestIfMissing(options);
626
+ const report = await validateSeedAppPackage(options);
627
+ await writeFile(resolveInputPath(options.rootDir, options.outFile ?? DEFAULT_REPORT_PATH), `${JSON.stringify(report, null, 2)}\n`, "utf8");
628
+ return report;
629
+ }
630
+ async function writeGeneratedBundleManifestIfMissing(options) {
631
+ const rootDir = path.resolve(options.rootDir);
632
+ const bundlePath = resolveInputPath(rootDir, options.bundlePath ?? DEFAULT_BUNDLE_PATH);
633
+ if (await fileExists(bundlePath)) return;
634
+ const manifestPath = resolveInputPath(rootDir, options.manifestPath ?? DEFAULT_MANIFEST_PATH);
635
+ const manifestResult = validateSeedAppManifest(JSON.parse(await readFile(manifestPath, "utf8")));
636
+ if (!manifestResult.success) return;
637
+ const files = await collectBundleFiles(path.join(rootDir, "dist"), rootDir);
638
+ const indexFile = files.find((file) => normalizeAssetPath(file.relativePath) === "dist/index.html");
639
+ if (!indexFile) return;
640
+ const assets = await Promise.all(files.map(async (file) => {
641
+ const [content, fileStat] = await Promise.all([readFile(file.absolutePath), stat(file.absolutePath)]);
642
+ return {
643
+ path: normalizeAssetPath(file.relativePath),
644
+ sha256: sha256Digest(content),
645
+ size: fileStat.size,
646
+ contentType: contentTypeForPath(file.relativePath)
647
+ };
648
+ }));
649
+ const bundle = {
650
+ format: "static-spa",
651
+ entryHtml: normalizeAssetPath(indexFile.relativePath),
652
+ baseUrl: `seed-app://${manifestResult.data.id}/`,
653
+ assets
654
+ };
655
+ await writeFile(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`, "utf8");
656
+ }
657
+ function validateManifestBundle(manifest, bundle, computedBundleChecksum, entryIntegrity, gatewayChecksum, issues) {
658
+ if (manifest.bundle?.entryHtml && manifest.bundle.entryHtml !== bundle.entryHtml) issues.push({
659
+ path: "manifest.bundle.entryHtml",
660
+ message: "Manifest bundle entryHtml does not match bundle manifest."
661
+ });
662
+ if (manifest.bundle?.integrity && entryIntegrity && manifest.bundle.integrity !== entryIntegrity) issues.push({
663
+ path: "manifest.bundle.integrity",
664
+ message: "Manifest bundle integrity does not match computed entry SRI."
665
+ });
666
+ if (bundle.integrity && entryIntegrity && bundle.integrity !== entryIntegrity) issues.push({
667
+ path: "bundle.integrity",
668
+ message: "Bundle manifest integrity does not match computed entry SRI."
669
+ });
670
+ if (manifest.bundle?.checksum && computedBundleChecksum && manifest.bundle.checksum !== computedBundleChecksum) issues.push({
671
+ path: "manifest.bundle.checksum",
672
+ message: "Manifest bundle checksum does not match computed bundle checksum."
673
+ });
674
+ if (gatewayChecksum && manifest.bundle?.checksum && gatewayChecksum !== manifest.bundle.checksum) issues.push({
675
+ path: "gatewayChecksum",
676
+ message: "Gateway checksum does not match manifest bundle checksum."
677
+ });
678
+ }
679
+ async function validateAssets(rootDir, bundle, issues) {
680
+ const reports = await Promise.all(bundle.assets.map(async (asset) => {
681
+ const assetPath = resolveInputPath(rootDir, asset.path);
682
+ try {
683
+ const [content, fileStat] = await Promise.all([readFile(assetPath), stat(assetPath)]);
684
+ const actualSha256 = sha256Digest(content);
685
+ const actualSize = fileStat.size;
686
+ const ok = actualSha256 === asset.sha256 && actualSize === asset.size;
687
+ if (actualSha256 !== asset.sha256) issues.push({
688
+ path: `assets.${asset.path}.sha256`,
689
+ message: "Asset sha256 does not match file content."
690
+ });
691
+ if (actualSize !== asset.size) issues.push({
692
+ path: `assets.${asset.path}.size`,
693
+ message: "Asset size does not match file size."
694
+ });
695
+ return {
696
+ path: asset.path,
697
+ expectedSha256: asset.sha256,
698
+ actualSha256,
699
+ expectedSize: asset.size,
700
+ actualSize,
701
+ ok
702
+ };
703
+ } catch {
704
+ issues.push({
705
+ path: `assets.${asset.path}`,
706
+ message: "Asset file is missing or unreadable."
707
+ });
708
+ return {
709
+ path: asset.path,
710
+ expectedSha256: asset.sha256,
711
+ expectedSize: asset.size,
712
+ ok: false
713
+ };
714
+ }
715
+ }));
716
+ if (!bundle.assets.some((asset) => normalizeAssetPath(asset.path) === normalizeAssetPath(bundle.entryHtml))) issues.push({
717
+ path: "bundle.entryHtml",
718
+ message: "entryHtml must exist in bundle assets."
719
+ });
720
+ return reports;
721
+ }
722
+ async function computeEntryIntegrity(rootDir, bundle, issues) {
723
+ const entryAsset = bundle.assets.find((asset) => normalizeAssetPath(asset.path) === normalizeAssetPath(bundle.entryHtml));
724
+ if (!entryAsset) return void 0;
725
+ try {
726
+ const content = await readFile(resolveInputPath(rootDir, entryAsset.path));
727
+ return `sha512-${createHash("sha512").update(content).digest("base64")}`;
728
+ } catch {
729
+ issues.push({
730
+ path: "bundle.entryHtml",
731
+ message: "entryHtml file is missing or unreadable."
732
+ });
733
+ return;
734
+ }
735
+ }
736
+ function computeBundleChecksum(bundle, assets) {
737
+ const payload = {
738
+ baseUrl: bundle.baseUrl,
739
+ entryHtml: bundle.entryHtml,
740
+ format: bundle.format,
741
+ integrity: bundle.integrity,
742
+ assets: assets.map((asset) => ({
743
+ path: normalizeAssetPath(asset.path),
744
+ sha256: asset.actualSha256 ?? asset.expectedSha256,
745
+ size: asset.actualSize ?? asset.expectedSize
746
+ })).toSorted((left, right) => left.path.localeCompare(right.path))
747
+ };
748
+ return sha256Digest(Buffer.from(JSON.stringify(payload)));
749
+ }
750
+ async function collectBundleFiles(currentDir, rootDir) {
751
+ const entries = await readdir(currentDir, { withFileTypes: true });
752
+ return (await Promise.all(entries.map(async (entry) => {
753
+ const absolutePath = path.join(currentDir, entry.name);
754
+ if (entry.isDirectory()) return collectBundleFiles(absolutePath, rootDir);
755
+ if (entry.isFile()) return [{
756
+ absolutePath,
757
+ relativePath: path.relative(rootDir, absolutePath)
758
+ }];
759
+ return [];
760
+ }))).flat().toSorted((left, right) => left.relativePath.localeCompare(right.relativePath));
761
+ }
762
+ async function fileExists(filePath) {
763
+ try {
764
+ await access(filePath);
765
+ return true;
766
+ } catch {
767
+ return false;
768
+ }
769
+ }
770
+ function contentTypeForPath(input) {
771
+ const extension = path.extname(input).toLowerCase();
772
+ if (extension === ".html") return "text/html";
773
+ if (extension === ".js" || extension === ".mjs") return "text/javascript";
774
+ if (extension === ".css") return "text/css";
775
+ if (extension === ".json") return "application/json";
776
+ if (extension === ".svg") return "image/svg+xml";
777
+ if (extension === ".png") return "image/png";
778
+ if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
779
+ return "application/octet-stream";
780
+ }
781
+ async function readJsonFile(filePath, label, issues) {
782
+ try {
783
+ return JSON.parse(await readFile(filePath, "utf8"));
784
+ } catch {
785
+ issues.push({
786
+ path: label,
787
+ message: `${label} JSON is missing, unreadable, or invalid.`
788
+ });
789
+ return;
790
+ }
791
+ }
792
+ function sha256Digest(content) {
793
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
794
+ }
795
+ function normalizeAssetPath(input) {
796
+ return input.replace(/^\/+/, "");
797
+ }
798
+ function resolveInputPath(rootDir, input) {
799
+ return path.isAbsolute(input) ? input : path.join(rootDir, input);
800
+ }
801
+ //#endregion
802
+ //#region src/commands.ts
803
+ function parseSeedAppCliArgs(args, cwd = process.cwd()) {
804
+ const [commandArg, ...rest] = args;
805
+ const command = commandArg === "init" || commandArg === "package" || commandArg === "validate" || commandArg === "example" || commandArg === "inspect-archive" ? commandArg : "help";
806
+ const invocation = {
807
+ command,
808
+ rootDir: cwd,
809
+ force: false,
810
+ json: false
811
+ };
812
+ if (command === "init" && rest[0] && !rest[0].startsWith("-")) invocation.rootDir = rest.shift() ?? cwd;
813
+ for (let index = 0; index < rest.length; index += 1) {
814
+ const arg = rest[index];
815
+ if (arg === "--root") invocation.rootDir = readOptionValue(rest, index += 1, "--root");
816
+ else if (arg === "--id") invocation.appId = readOptionValue(rest, index += 1, "--id");
817
+ else if (arg === "--name") invocation.appName = readOptionValue(rest, index += 1, "--name");
818
+ else if (arg === "--dev-url") invocation.devUrl = readOptionValue(rest, index += 1, "--dev-url");
819
+ else if (arg === "--manifest") invocation.manifestPath = readOptionValue(rest, index += 1, "--manifest");
820
+ else if (arg === "--bundle") invocation.bundlePath = readOptionValue(rest, index += 1, "--bundle");
821
+ else if (arg === "--gateway-checksum") invocation.gatewayChecksum = readOptionValue(rest, index += 1, "--gateway-checksum");
822
+ else if (arg === "--out") invocation.outFile = readOptionValue(rest, index += 1, "--out");
823
+ else if (arg === "--zip") invocation.zipFile = readOptionValue(rest, index += 1, "--zip");
824
+ else if (arg === "--expect-text") invocation.expectedText = readOptionValue(rest, index += 1, "--expect-text");
825
+ else if (arg === "--sdk-dependency") invocation.sdkDependency = readOptionValue(rest, index += 1, "--sdk-dependency");
826
+ else if (arg === "--cli-dependency") invocation.cliDependency = readOptionValue(rest, index += 1, "--cli-dependency");
827
+ else if (arg === "--force") invocation.force = true;
828
+ else if (arg === "--json") invocation.json = true;
829
+ }
830
+ return invocation;
831
+ }
832
+ async function runSeedAppCli(invocation) {
833
+ if (invocation.command === "help") return {
834
+ exitCode: 0,
835
+ stdout: usageText(),
836
+ stderr: ""
837
+ };
838
+ if (invocation.command === "init") {
839
+ const result = await initSeedAppProject(toInitOptions(invocation));
840
+ return {
841
+ exitCode: 0,
842
+ stdout: invocation.json ? `${JSON.stringify(result, null, 2)}\n` : `Seed app project created at ${result.rootDir}\n${result.files.map((file) => `- ${file}`).join("\n")}\n`,
843
+ stderr: ""
844
+ };
845
+ }
846
+ if (invocation.command === "example") {
847
+ const result = await writeSeedAppExampleArchive(toExampleOptions(invocation));
848
+ return {
849
+ exitCode: 0,
850
+ stdout: invocation.json ? `${JSON.stringify(result, null, 2)}\n` : `Seed app example archive written to ${result.outFile}\n${result.files.map((file) => `- ${file}`).join("\n")}\n`,
851
+ stderr: ""
852
+ };
853
+ }
854
+ if (invocation.command === "inspect-archive") {
855
+ const inspection = await inspectSeedAppPackageArchive(toArchiveInspectOptions(invocation));
856
+ const stdout = invocation.json ? `${JSON.stringify(inspection, null, 2)}\n` : formatArchiveInspection(inspection);
857
+ return {
858
+ exitCode: inspection.ok ? 0 : 1,
859
+ stdout,
860
+ stderr: ""
861
+ };
862
+ }
863
+ const options = {
864
+ rootDir: invocation.rootDir,
865
+ manifestPath: invocation.manifestPath,
866
+ bundlePath: invocation.bundlePath,
867
+ gatewayChecksum: invocation.gatewayChecksum
868
+ };
869
+ const report = invocation.command === "package" ? await runPackageCommand({
870
+ ...options,
871
+ outFile: invocation.outFile,
872
+ zipFile: invocation.zipFile
873
+ }) : await validateSeedAppPackage(options);
874
+ const stdout = invocation.json ? `${JSON.stringify(report, null, 2)}\n` : formatReport(report);
875
+ return {
876
+ exitCode: report.ok ? 0 : 1,
877
+ stdout,
878
+ stderr: ""
879
+ };
880
+ }
881
+ async function runPackageCommand(options) {
882
+ const report = await writeSeedAppPackageReport(options);
883
+ if (!options.zipFile || !report.ok) return report;
884
+ const packageArchive = await writeSeedAppPackageArchive({
885
+ rootDir: options.rootDir,
886
+ manifestPath: options.manifestPath,
887
+ bundlePath: options.bundlePath,
888
+ reportPath: options.outFile,
889
+ report,
890
+ zipFile: options.zipFile
891
+ });
892
+ return {
893
+ ...report,
894
+ packageArchive
895
+ };
896
+ }
897
+ function toArchiveInspectOptions(invocation) {
898
+ if (!invocation.zipFile) throw new Error("--zip requires a value.");
899
+ return {
900
+ rootDir: invocation.rootDir,
901
+ zipFile: invocation.zipFile,
902
+ expectedText: invocation.expectedText
903
+ };
904
+ }
905
+ function toExampleOptions(invocation) {
906
+ return {
907
+ outFile: invocation.outFile,
908
+ sdkDependency: invocation.sdkDependency,
909
+ cliDependency: invocation.cliDependency
910
+ };
911
+ }
912
+ function toInitOptions(invocation) {
913
+ if (!invocation.appId) throw new Error("--id requires a value.");
914
+ if (!invocation.appName) throw new Error("--name requires a value.");
915
+ return {
916
+ rootDir: invocation.rootDir,
917
+ appId: invocation.appId,
918
+ appName: invocation.appName,
919
+ devUrl: invocation.devUrl ?? "http://localhost:5173",
920
+ sdkDependency: invocation.sdkDependency,
921
+ force: invocation.force
922
+ };
923
+ }
924
+ function readOptionValue(args, index, option) {
925
+ const value = args[index];
926
+ if (!value) throw new Error(`${option} requires a value.`);
927
+ return value;
928
+ }
929
+ function formatReport(report) {
930
+ const lines = [
931
+ report.ok ? "Seed app validation passed." : "Seed app validation failed.",
932
+ `App: ${report.appId ?? "unknown"} ${report.version ?? ""}`.trim(),
933
+ `Manifest: ${report.manifestPath}`,
934
+ `Bundle: ${report.bundlePath}`
935
+ ];
936
+ if (report.packageArchive) lines.push(`Archive: ${report.packageArchive.outFile}`);
937
+ if (report.computedBundleChecksum) lines.push(`Bundle checksum: ${report.computedBundleChecksum}`);
938
+ if (report.entryIntegrity) lines.push(`Entry integrity: ${report.entryIntegrity}`);
939
+ if (report.issues.length > 0) {
940
+ lines.push("Issues:");
941
+ report.issues.forEach((issue) => lines.push(`- ${issue.path}: ${issue.message}`));
942
+ }
943
+ return `${lines.join("\n")}\n`;
944
+ }
945
+ function formatArchiveInspection(inspection) {
946
+ const lines = [
947
+ inspection.ok ? "Seed app archive smoke passed." : "Seed app archive smoke failed.",
948
+ `Archive: ${inspection.archiveFile}`,
949
+ `App: ${inspection.appId ?? "unknown"} ${inspection.version ?? ""}`.trim(),
950
+ `Files: ${inspection.files.length}`,
951
+ `Assets: ${inspection.assetCount}`
952
+ ];
953
+ if (inspection.expectedText) lines.push(`Expected text: ${inspection.expectedTextFound ? "found" : "missing"}`);
954
+ if (inspection.issues.length > 0) {
955
+ lines.push("Issues:");
956
+ inspection.issues.forEach((issue) => lines.push(`- ${issue.path}: ${issue.message}`));
957
+ }
958
+ return `${lines.join("\n")}\n`;
959
+ }
960
+ function usageText() {
961
+ return [
962
+ "Usage:",
963
+ " seed-app init <dir> --id app.id --name \"App Name\" [--dev-url http://localhost:5173] [--sdk-dependency <specifier>] [--force] [--json]",
964
+ " seed-app validate [--root dir] [--manifest seed-app.json] [--bundle seed-app.bundle.json] [--gateway-checksum sha256:...] [--json]",
965
+ " seed-app package [--root dir] [--out seed-app.integrity-report.json] [--zip seed-app-package.zip] [--json]",
966
+ " seed-app inspect-archive [--root dir] --zip seed-app-package.zip [--expect-text marker] [--json]",
967
+ " seed-app example [--out seed-react-spa-stories.zip] [--sdk-dependency <specifier>] [--cli-dependency <specifier>] [--json]",
968
+ ""
969
+ ].join("\n");
970
+ }
971
+ //#endregion
972
+ export { inspectSeedAppPackageArchive as a, initSeedAppProject as c, writeSeedAppPackageReport as i, runSeedAppCli as n, writeSeedAppPackageArchive as o, validateSeedAppPackage as r, writeSeedAppExampleArchive as s, parseSeedAppCliArgs as t };