@nuucognition/flint-cli 0.5.6-dev.4 → 0.5.6-dev.5

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,936 @@
1
+ import {
2
+ addPlateDeclaration,
3
+ getFlintConfigDir,
4
+ getPlateDeclarations,
5
+ nameFormats,
6
+ readFlintToml,
7
+ setPlateDeclaration
8
+ } from "./chunk-LLLVBA4Q.js";
9
+
10
+ // ../../packages/flint/dist/chunk-LCGQD7MB.js
11
+ import { spawn } from "child_process";
12
+ import { exec } from "child_process";
13
+ import { mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
14
+ import { basename, join, relative, resolve } from "path";
15
+ import { promisify } from "util";
16
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
17
+ var execAsync = promisify(exec);
18
+ var DEFAULT_PLATES_SOURCE = "NUU-Cognition/plates";
19
+ function normalizePlateName(name) {
20
+ return nameFormats(name.replace(/[-_]+/g, " ")).slug;
21
+ }
22
+ function isSlug(value) {
23
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
24
+ }
25
+ async function fileExists(path) {
26
+ try {
27
+ await stat(path);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ function getPlateStateDir(flintPath) {
34
+ return join(getFlintConfigDir(flintPath), "plates");
35
+ }
36
+ function getPlateAssetsDir(flintPath) {
37
+ return join(getFlintConfigDir(flintPath), "plates");
38
+ }
39
+ function getPlateAssetPath(flintPath, plateName) {
40
+ return join(getPlateAssetsDir(flintPath), plateName);
41
+ }
42
+ function getPlateStatePath(flintPath, plateName) {
43
+ return join(getPlateStateDir(flintPath), `${plateName}.json`);
44
+ }
45
+ async function readPlateState(flintPath, plateName) {
46
+ try {
47
+ const content = await readFile(getPlateStatePath(flintPath, plateName), "utf8");
48
+ return JSON.parse(content);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ async function writePlateState(flintPath, plateName, state) {
54
+ const dir = getPlateStateDir(flintPath);
55
+ await mkdir(dir, { recursive: true });
56
+ await writeFile(getPlateStatePath(flintPath, plateName), JSON.stringify(state, null, 2) + "\n", "utf8");
57
+ }
58
+ async function listPlateStates(flintPath) {
59
+ const dir = getPlateStateDir(flintPath);
60
+ try {
61
+ const entries = await readdir(dir, { withFileTypes: true });
62
+ const states = await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map(async (entry) => {
63
+ const plateName = entry.name.replace(/\.json$/, "");
64
+ const state = await readPlateState(flintPath, plateName);
65
+ return state ? [plateName, state] : null;
66
+ }));
67
+ return Object.fromEntries(states.filter((entry) => entry != null));
68
+ } catch {
69
+ return {};
70
+ }
71
+ }
72
+ function normalizePlateSource(nameOrSource) {
73
+ const trimmed = nameOrSource.trim();
74
+ if (trimmed.includes("/")) {
75
+ return trimmed;
76
+ }
77
+ return DEFAULT_PLATES_SOURCE;
78
+ }
79
+ function resolvePlateReleaseName(nameOrSource, source) {
80
+ const trimmed = nameOrSource.trim();
81
+ if (!trimmed.includes("/")) {
82
+ return normalizePlateName(trimmed);
83
+ }
84
+ const repo = basename(source).replace(/^plate-/, "");
85
+ if (repo === "plates") {
86
+ throw new Error(`Cannot infer plate name from monorepo source "${source}". Use the plate slug instead.`);
87
+ }
88
+ return normalizePlateName(repo);
89
+ }
90
+ function extractVersionFromTag(tag) {
91
+ const match = tag.match(/@(\d+\.\d+\.\d+(?:[-+][^/]+)?)$/);
92
+ return match?.[1] ?? null;
93
+ }
94
+ async function fetchPlateRelease(source, plateName, requestedVersion) {
95
+ const releaseTag = `plate-${plateName}@${requestedVersion}`;
96
+ const url = requestedVersion ? `https://api.github.com/repos/${source}/releases/tags/${encodeURIComponent(releaseTag)}` : `https://api.github.com/repos/${source}/releases`;
97
+ const response = await fetch(url, {
98
+ headers: {
99
+ "Accept": "application/vnd.github+json",
100
+ "User-Agent": "flint-cli"
101
+ }
102
+ });
103
+ if (!response.ok) {
104
+ throw new Error(`Failed to fetch release metadata for ${source} (${response.status})`);
105
+ }
106
+ const payload = requestedVersion ? [await response.json()] : await response.json();
107
+ const releases = payload.filter((release) => !release.draft && !release.prerelease).map((release) => {
108
+ const tag = typeof release.tag_name === "string" ? release.tag_name : "";
109
+ const version = extractVersionFromTag(tag);
110
+ const assets = Array.isArray(release.assets) ? release.assets : [];
111
+ const tarball = assets.find((asset) => typeof asset?.name === "string" && asset.name.endsWith(".tar.gz"));
112
+ const assetUrl = typeof tarball?.url === "string" ? tarball.url : "";
113
+ const assetName = typeof tarball?.name === "string" ? tarball.name : "";
114
+ if (!tag || !version || !assetUrl || !assetName || !tag.startsWith(`plate-${plateName}@`)) {
115
+ return null;
116
+ }
117
+ return {
118
+ tag,
119
+ version,
120
+ asset: {
121
+ name: assetName,
122
+ url: assetUrl
123
+ }
124
+ };
125
+ }).filter((release) => release != null);
126
+ if (releases.length === 0) {
127
+ throw new Error(`No release tarballs found for ${source}`);
128
+ }
129
+ if (requestedVersion) {
130
+ const match = releases.find((release) => release.version === requestedVersion);
131
+ if (!match) {
132
+ throw new Error(`Version ${requestedVersion} was not found for ${source}`);
133
+ }
134
+ return match;
135
+ }
136
+ return releases[0];
137
+ }
138
+ async function downloadPlateReleaseAsset(source, release) {
139
+ const tempDir = await mkdtemp("/tmp/flint-plate-release-");
140
+ const archivePath = join(tempDir, release.asset.name);
141
+ const response = await fetch(release.asset.url, {
142
+ headers: {
143
+ "Accept": "application/octet-stream",
144
+ "User-Agent": "flint-cli"
145
+ }
146
+ });
147
+ if (!response.ok) {
148
+ throw new Error(`Failed to download ${source} release asset (${response.status})`);
149
+ }
150
+ const buffer = Buffer.from(await response.arrayBuffer());
151
+ await writeFile(archivePath, buffer);
152
+ return archivePath;
153
+ }
154
+ async function extractPlateArchive(archivePath) {
155
+ const tempDir = await mkdtemp("/tmp/flint-plate-extract-");
156
+ await execAsync(`tar -xzf "${archivePath}" -C "${tempDir}"`);
157
+ return tempDir;
158
+ }
159
+ function validateManifest(manifest, manifestPath) {
160
+ if (!manifest || typeof manifest !== "object") {
161
+ throw new Error(`Invalid plate.yaml at ${manifestPath}`);
162
+ }
163
+ const record = manifest;
164
+ const name = typeof record.name === "string" ? record.name.trim() : "";
165
+ const title = typeof record.title === "string" ? record.title.trim() : "";
166
+ const entry = typeof record.entry === "string" ? record.entry.trim() : "";
167
+ if (!name || !title || !entry) {
168
+ throw new Error(`plate.yaml is missing required fields (name, title, entry): ${manifestPath}`);
169
+ }
170
+ if (!isSlug(name)) {
171
+ throw new Error(`Invalid plate name "${name}" in ${manifestPath}`);
172
+ }
173
+ const tools = Array.isArray(record.tools) ? record.tools.map((tool) => {
174
+ if (!tool || typeof tool !== "object") {
175
+ throw new Error(`Invalid tool declaration in ${manifestPath}`);
176
+ }
177
+ const toolRecord = tool;
178
+ const toolName = typeof toolRecord.name === "string" ? toolRecord.name.trim() : "";
179
+ const command = typeof toolRecord.command === "string" ? toolRecord.command.trim() : "";
180
+ const description = typeof toolRecord.description === "string" ? toolRecord.description.trim() : "";
181
+ if (!toolName || !command || !description) {
182
+ throw new Error(`Tool declaration is missing required fields in ${manifestPath}`);
183
+ }
184
+ return { name: toolName, command, description };
185
+ }) : void 0;
186
+ const handles = Array.isArray(record.handles) ? record.handles.map((handle) => {
187
+ if (!handle || typeof handle !== "object") {
188
+ throw new Error(`Invalid handle declaration in ${manifestPath}`);
189
+ }
190
+ const handleRecord = handle;
191
+ const tag = typeof handleRecord.tag === "string" ? handleRecord.tag.trim() : "";
192
+ if (!tag) {
193
+ throw new Error(`Handle declaration is missing required tag in ${manifestPath}`);
194
+ }
195
+ return {
196
+ tag,
197
+ default: handleRecord.default === true
198
+ };
199
+ }) : void 0;
200
+ const actions = Array.isArray(record.actions) ? record.actions.map((action) => {
201
+ if (!action || typeof action !== "object") {
202
+ throw new Error(`Invalid action declaration in ${manifestPath}`);
203
+ }
204
+ const actionRecord = action;
205
+ const id = typeof actionRecord.id === "string" ? actionRecord.id.trim() : "";
206
+ const categoryValue = typeof actionRecord.category === "string" ? actionRecord.category.trim() : "";
207
+ const category = categoryValue === "cross-plate" || categoryValue === "universal" ? categoryValue : "universal";
208
+ const label = typeof actionRecord.label === "string" ? actionRecord.label.trim() : "";
209
+ if (!id || !label) {
210
+ throw new Error(`Action declaration is missing required fields in ${manifestPath}`);
211
+ }
212
+ return {
213
+ id,
214
+ category,
215
+ label,
216
+ description: typeof actionRecord.description === "string" ? actionRecord.description : void 0,
217
+ icon: typeof actionRecord.icon === "string" ? actionRecord.icon : void 0
218
+ };
219
+ }) : void 0;
220
+ return {
221
+ name,
222
+ title,
223
+ entry,
224
+ version: typeof record.version === "string" ? record.version : void 0,
225
+ description: typeof record.description === "string" ? record.description : void 0,
226
+ icon: typeof record.icon === "string" ? record.icon : void 0,
227
+ shard: typeof record.shard === "string" ? record.shard : void 0,
228
+ dev: typeof record.dev === "string" ? record.dev : void 0,
229
+ api: Array.isArray(record.api) ? record.api.filter((value) => typeof value === "string") : void 0,
230
+ handles,
231
+ actions,
232
+ tools
233
+ };
234
+ }
235
+ async function readPlateManifest(platePath) {
236
+ const manifestPath = join(platePath, "plate.yaml");
237
+ const content = await readFile(manifestPath, "utf8");
238
+ return validateManifest(parseYaml(content, { logLevel: "error" }), manifestPath);
239
+ }
240
+ function resolvePlateEntryPath(platePath, manifest) {
241
+ return resolve(platePath, manifest.entry);
242
+ }
243
+ var PLATE_SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
244
+ async function getNewestMtime(dir) {
245
+ let max = 0;
246
+ try {
247
+ const entries = await readdir(dir, { withFileTypes: true });
248
+ for (const entry of entries) {
249
+ if (PLATE_SKIP_DIRS.has(entry.name)) continue;
250
+ const full = join(dir, entry.name);
251
+ if (entry.isDirectory()) {
252
+ const sub = await getNewestMtime(full);
253
+ if (sub > max) max = sub;
254
+ } else {
255
+ const s = await stat(full);
256
+ if (s.mtimeMs > max) max = s.mtimeMs;
257
+ }
258
+ }
259
+ } catch {
260
+ }
261
+ return max;
262
+ }
263
+ async function hasPlateSourceTree(platePath) {
264
+ return await fileExists(join(platePath, "src")) || await fileExists(join(platePath, "package.json")) || await fileExists(join(platePath, "vite.config.ts"));
265
+ }
266
+ async function checkPlateStale(platePath, manifest) {
267
+ if (!await hasPlateSourceTree(platePath)) {
268
+ return false;
269
+ }
270
+ const entryPath = resolvePlateEntryPath(platePath, manifest);
271
+ const entryStat = await stat(entryPath).catch(() => null);
272
+ if (!entryStat) return false;
273
+ const sourceMtime = await getNewestMtime(platePath);
274
+ return sourceMtime > entryStat.mtimeMs;
275
+ }
276
+ async function fetchDevManifest(devUrl) {
277
+ try {
278
+ const url = new URL("/plate.yaml", devUrl);
279
+ const response = await fetch(url.href, { signal: AbortSignal.timeout(2e3) });
280
+ if (!response.ok) return null;
281
+ const content = await response.text();
282
+ return validateManifest(parseYaml(content, { logLevel: "error" }), url.href);
283
+ } catch {
284
+ return null;
285
+ }
286
+ }
287
+ function createSyntheticPlateInfo(flintPath, plateName, state, declaration, devManifest) {
288
+ const inferred = nameFormats((state?.title ?? declaration?.title ?? plateName).replace(/-/g, " "));
289
+ const title = devManifest?.title ?? state?.title ?? declaration?.title ?? inferred.proper;
290
+ return {
291
+ declarationName: plateName,
292
+ directoryName: plateName,
293
+ path: getPlateAssetPath(flintPath, plateName),
294
+ built: Boolean(state?.devUrl),
295
+ stale: false,
296
+ dev: Boolean(state?.devUrl),
297
+ devUrl: state?.devUrl,
298
+ manifest: devManifest ?? {
299
+ name: plateName,
300
+ title,
301
+ entry: "./dist/index.html",
302
+ version: state?.currentVersion ?? declaration?.version ?? "0.1.0",
303
+ shard: state?.shard,
304
+ tools: [],
305
+ actions: [],
306
+ handles: []
307
+ }
308
+ };
309
+ }
310
+ async function scanInstalledPlates(flintPath, declarations, states) {
311
+ const platesDir = getPlateAssetsDir(flintPath);
312
+ const entries = await readdir(platesDir, { withFileTypes: true }).catch(() => []);
313
+ const infos = await Promise.all(entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
314
+ const absolutePath = join(platesDir, entry.name);
315
+ if (!await fileExists(join(absolutePath, "plate.yaml"))) {
316
+ return null;
317
+ }
318
+ const manifest = await readPlateManifest(absolutePath);
319
+ const declarationName = Object.entries(declarations).find(
320
+ ([key, declaration]) => normalizePlateName(key) === normalizePlateName(manifest.name) || normalizePlateName(declaration.title ?? "") === normalizePlateName(manifest.title)
321
+ )?.[0] ?? manifest.name;
322
+ const state = states[manifest.name] ?? states[declarationName];
323
+ const built = await fileExists(resolvePlateEntryPath(absolutePath, manifest));
324
+ const stale = built ? await checkPlateStale(absolutePath, manifest) : false;
325
+ return {
326
+ declarationName,
327
+ directoryName: entry.name,
328
+ path: absolutePath,
329
+ built: state?.devUrl ? true : built,
330
+ stale,
331
+ dev: Boolean(state?.devUrl),
332
+ devUrl: state?.devUrl,
333
+ manifest
334
+ };
335
+ }));
336
+ return infos.filter((info) => info != null);
337
+ }
338
+ async function initPlateRepo(flintPath, plateName, url) {
339
+ const plate = await getPlate(flintPath, plateName);
340
+ if (!plate) {
341
+ throw new Error(`Plate not found: ${plateName}`);
342
+ }
343
+ const plateDir = plate.path;
344
+ const gitDir = join(plateDir, ".git");
345
+ const isGitRepo = await fileExists(gitDir);
346
+ if (!isGitRepo) {
347
+ await execAsync("git init", { cwd: plateDir });
348
+ }
349
+ const gitignorePath = join(plateDir, ".gitignore");
350
+ if (!await fileExists(gitignorePath)) {
351
+ await writeFile(gitignorePath, "node_modules/\ndist/\n", "utf8");
352
+ }
353
+ try {
354
+ const { stdout: existingRemote } = await execAsync("git remote get-url origin", { cwd: plateDir });
355
+ if (existingRemote.trim() !== url) {
356
+ await execAsync(`git remote set-url origin "${url}"`, { cwd: plateDir });
357
+ }
358
+ } catch {
359
+ await execAsync(`git remote add origin "${url}"`, { cwd: plateDir });
360
+ }
361
+ await execAsync("git add .", { cwd: plateDir });
362
+ let hasChanges = false;
363
+ try {
364
+ const { stdout: statusOut } = await execAsync("git status --porcelain", { cwd: plateDir });
365
+ hasChanges = statusOut.trim().length > 0;
366
+ } catch {
367
+ hasChanges = true;
368
+ }
369
+ if (hasChanges) {
370
+ await execAsync('git commit -m "Update plate"', { cwd: plateDir });
371
+ }
372
+ let pushed = false;
373
+ try {
374
+ await execAsync("git push -u origin main", { cwd: plateDir, timeout: 6e4 });
375
+ pushed = true;
376
+ } catch {
377
+ try {
378
+ await execAsync("git push -u origin HEAD", { cwd: plateDir, timeout: 6e4 });
379
+ pushed = true;
380
+ } catch {
381
+ }
382
+ }
383
+ return { initialized: !isGitRepo, pushed, platePath: plateDir };
384
+ }
385
+ async function clonePlateFromRepo(flintPath, name, url, targetPath) {
386
+ const absolutePath = resolve(flintPath, targetPath);
387
+ await mkdir(getPlateAssetsDir(flintPath), { recursive: true });
388
+ if (await fileExists(absolutePath)) {
389
+ throw new Error(`Plate directory already exists: ${targetPath}`);
390
+ }
391
+ try {
392
+ await execAsync(`git clone --depth 1 "${url}" "${absolutePath}"`, { timeout: 6e4 });
393
+ } catch (error) {
394
+ const message = error instanceof Error ? error.message : String(error);
395
+ throw new Error(`Failed to clone plate "${name}": ${message}`);
396
+ }
397
+ const gitDir = join(absolutePath, ".git");
398
+ try {
399
+ await rm(gitDir, { recursive: true, force: true });
400
+ } catch {
401
+ }
402
+ return absolutePath;
403
+ }
404
+ async function updatePlateFromRepo(flintPath, name, url, targetPath) {
405
+ const absolutePath = resolve(flintPath, targetPath);
406
+ if (await fileExists(absolutePath)) {
407
+ await rm(absolutePath, { recursive: true, force: true });
408
+ }
409
+ return clonePlateFromRepo(flintPath, name, url, targetPath);
410
+ }
411
+ async function syncPlateRepos(flintPath, declarations) {
412
+ const results = [];
413
+ for (const [name, decl] of Object.entries(declarations)) {
414
+ if (!decl.repo || !decl.path) continue;
415
+ const absolutePath = resolve(flintPath, decl.path);
416
+ const exists = await fileExists(absolutePath);
417
+ try {
418
+ if (exists) {
419
+ results.push({ name, status: "skipped" });
420
+ } else {
421
+ await clonePlateFromRepo(flintPath, name, decl.repo, decl.path);
422
+ results.push({ name, status: "cloned" });
423
+ }
424
+ } catch (err) {
425
+ results.push({
426
+ name,
427
+ status: "error",
428
+ error: err instanceof Error ? err.message : String(err)
429
+ });
430
+ }
431
+ }
432
+ return results;
433
+ }
434
+ async function listPlates(flintPath) {
435
+ const declarations = await getPlateDeclarations(flintPath);
436
+ const states = await listPlateStates(flintPath);
437
+ const installed = await scanInstalledPlates(flintPath, declarations, states);
438
+ const infos = [...installed];
439
+ const unresolvedDevPlates = [];
440
+ for (const [name, declaration] of Object.entries(declarations)) {
441
+ if (installed.some((info) => normalizePlateName(info.manifest.name) === normalizePlateName(name))) {
442
+ continue;
443
+ }
444
+ const state = states[name];
445
+ if (state?.devUrl) {
446
+ unresolvedDevPlates.push({ name, state, declaration });
447
+ } else {
448
+ infos.push(createSyntheticPlateInfo(flintPath, name, state, declaration));
449
+ }
450
+ }
451
+ for (const [name, state] of Object.entries(states)) {
452
+ if (!state.devUrl) {
453
+ continue;
454
+ }
455
+ if (installed.some((info) => normalizePlateName(info.manifest.name) === normalizePlateName(name))) {
456
+ continue;
457
+ }
458
+ if (unresolvedDevPlates.some((p) => p.name === name)) {
459
+ continue;
460
+ }
461
+ unresolvedDevPlates.push({ name, state, declaration: declarations[name] });
462
+ }
463
+ const devManifests = await Promise.all(
464
+ unresolvedDevPlates.map(async (p) => {
465
+ const manifest = p.state?.devUrl ? await fetchDevManifest(p.state.devUrl) : null;
466
+ return { ...p, manifest };
467
+ })
468
+ );
469
+ for (const { name, state, declaration, manifest } of devManifests) {
470
+ infos.push(createSyntheticPlateInfo(flintPath, name, state, declaration, manifest));
471
+ }
472
+ const seenNames = /* @__PURE__ */ new Set();
473
+ for (const info of infos) {
474
+ const normalized = normalizePlateName(info.manifest.name);
475
+ if (seenNames.has(normalized)) {
476
+ throw new Error(`Duplicate plate manifest name detected: ${info.manifest.name}`);
477
+ }
478
+ seenNames.add(normalized);
479
+ }
480
+ return infos.sort((left, right) => left.manifest.title.localeCompare(right.manifest.title));
481
+ }
482
+ async function getPlate(flintPath, plateName) {
483
+ const normalized = normalizePlateName(plateName);
484
+ const plates = await listPlates(flintPath);
485
+ return plates.find(
486
+ (plate) => normalizePlateName(plate.declarationName) === normalized || normalizePlateName(plate.manifest.name) === normalized || normalizePlateName(plate.manifest.title) === normalized
487
+ ) ?? null;
488
+ }
489
+ function renderPlateManifestYaml(slug, title, shard) {
490
+ const manifest = {
491
+ name: slug,
492
+ title,
493
+ version: "0.1.0",
494
+ description: `${title} Plate`,
495
+ entry: "./dist/index.html",
496
+ dev: "./src/index.tsx",
497
+ api: ["GET /api/plates", "GET /api/artifacts"]
498
+ };
499
+ if (shard) {
500
+ manifest.shard = shard;
501
+ }
502
+ return stringifyYaml(manifest).trimEnd() + "\n";
503
+ }
504
+ async function createPlate(flintPath, displayName, options = {}) {
505
+ const { proper, slug } = nameFormats(displayName);
506
+ if (!proper.trim()) {
507
+ throw new Error("Plate name is required");
508
+ }
509
+ const existing = await getPlate(flintPath, slug);
510
+ if (existing) {
511
+ throw new Error(`Plate "${slug}" already exists`);
512
+ }
513
+ const platePath = getPlateAssetPath(flintPath, slug);
514
+ if (await fileExists(platePath)) {
515
+ throw new Error(`Plate directory already exists: ${relative(flintPath, platePath)}`);
516
+ }
517
+ await mkdir(join(platePath, "src"), { recursive: true });
518
+ await mkdir(join(platePath, "tools"), { recursive: true });
519
+ await writeFile(join(platePath, "plate.yaml"), renderPlateManifestYaml(slug, proper, options.shard), "utf8");
520
+ await writeFile(join(platePath, "package.json"), JSON.stringify({
521
+ name: `@plates/${slug}`,
522
+ private: true,
523
+ version: "0.1.0",
524
+ type: "module",
525
+ scripts: {
526
+ dev: "vite",
527
+ build: "vite build"
528
+ },
529
+ dependencies: {
530
+ "@nuucognition/plate-sdk": "workspace:*",
531
+ react: "^19.0.0",
532
+ "react-dom": "^19.0.0"
533
+ },
534
+ devDependencies: {
535
+ "@types/react": "^19.0.0",
536
+ "@types/react-dom": "^19.0.0",
537
+ "@vitejs/plugin-react": "^4.0.0",
538
+ typescript: "^5.0.0",
539
+ vite: "^6.0.0"
540
+ }
541
+ }, null, 2) + "\n", "utf8");
542
+ await writeFile(join(platePath, "tsconfig.json"), JSON.stringify({
543
+ compilerOptions: {
544
+ target: "ES2020",
545
+ useDefineForClassFields: true,
546
+ lib: ["DOM", "DOM.Iterable", "ES2020"],
547
+ allowJs: false,
548
+ skipLibCheck: true,
549
+ esModuleInterop: true,
550
+ allowSyntheticDefaultImports: true,
551
+ strict: true,
552
+ forceConsistentCasingInFileNames: true,
553
+ module: "ESNext",
554
+ moduleResolution: "Bundler",
555
+ resolveJsonModule: true,
556
+ isolatedModules: true,
557
+ noEmit: true,
558
+ jsx: "react-jsx"
559
+ },
560
+ include: ["src"]
561
+ }, null, 2) + "\n", "utf8");
562
+ await writeFile(join(platePath, "vite.config.ts"), `import { defineConfig } from 'vite';
563
+ import react from '@vitejs/plugin-react';
564
+
565
+ export default defineConfig({
566
+ base: '/plates/${slug}/',
567
+ plugins: [react()],
568
+ build: {
569
+ outDir: 'dist',
570
+ },
571
+ });
572
+ `, "utf8");
573
+ await writeFile(join(platePath, "index.html"), `<!doctype html>
574
+ <html lang="en">
575
+ <head>
576
+ <meta charset="UTF-8" />
577
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
578
+ <title>${proper}</title>
579
+ </head>
580
+ <body>
581
+ <div id="root"></div>
582
+ <script type="module" src="/src/index.tsx"></script>
583
+ </body>
584
+ </html>
585
+ `, "utf8");
586
+ await writeFile(join(platePath, ".gitignore"), "node_modules/\ndist/\n", "utf8");
587
+ await writeFile(join(platePath, "src", "index.tsx"), `import React from 'react';
588
+ import ReactDOM from 'react-dom/client';
589
+ import { getPlateContext } from '@nuucognition/plate-sdk';
590
+ import { App } from './App';
591
+
592
+ async function bootstrap() {
593
+ let connected = false;
594
+
595
+ try {
596
+ await getPlateContext();
597
+ connected = true;
598
+ } catch {
599
+ connected = false;
600
+ }
601
+
602
+ ReactDOM.createRoot(document.getElementById('root')!).render(
603
+ <React.StrictMode>
604
+ <App connected={connected} />
605
+ </React.StrictMode>,
606
+ );
607
+ }
608
+
609
+ void bootstrap();
610
+ `, "utf8");
611
+ await writeFile(join(platePath, "src", "App.tsx"), `import { usePlateContext } from '@nuucognition/plate-sdk';
612
+
613
+ export function App({ connected }: { connected: boolean }) {
614
+ let plateName = '${slug}';
615
+ let serverUrl = 'Not connected';
616
+
617
+ try {
618
+ const context = usePlateContext();
619
+ plateName = context.plateName;
620
+ serverUrl = context.serverUrl;
621
+ } catch {
622
+ // Render the disconnected state below.
623
+ }
624
+
625
+ return (
626
+ <main style={{ fontFamily: 'ui-sans-serif, system-ui, sans-serif', padding: 24 }}>
627
+ <h1 style={{ margin: 0, fontSize: 28 }}>${proper}</h1>
628
+ <p style={{ color: '#666', maxWidth: 640 }}>
629
+ This is the starter Plate scaffold. Replace this view with your interface.
630
+ </p>
631
+ <div style={{ border: '1px solid #ddd', borderRadius: 12, padding: 16, marginTop: 24 }}>
632
+ <div><strong>Plate:</strong> {plateName}</div>
633
+ <div><strong>SDK:</strong> {connected ? 'Connected to Steel' : 'Standalone preview mode'}</div>
634
+ <div><strong>Server:</strong> {serverUrl}</div>
635
+ </div>
636
+ </main>
637
+ );
638
+ }
639
+ `, "utf8");
640
+ await addPlateDeclaration(flintPath, slug, relative(flintPath, platePath), { title: proper });
641
+ return await getPlate(flintPath, slug);
642
+ }
643
+ async function validatePairedShard(flintPath, manifest) {
644
+ if (!manifest.shard) {
645
+ return [];
646
+ }
647
+ const config = await readFlintToml(flintPath);
648
+ const hasShard = Boolean(config?.shards?.[manifest.shard]);
649
+ if (hasShard) {
650
+ return [];
651
+ }
652
+ return [`${manifest.title} expects the "${manifest.shard}" shard, but it is not declared in flint.toml.`];
653
+ }
654
+ async function installExtractedPlate(flintPath, source, release, extractedPath, pinnedVersion) {
655
+ const manifest = await readPlateManifest(extractedPath);
656
+ const targetPath = getPlateAssetPath(flintPath, manifest.name);
657
+ await mkdir(getPlateAssetsDir(flintPath), { recursive: true });
658
+ await rm(targetPath, { recursive: true, force: true });
659
+ await rename(extractedPath, targetPath);
660
+ const previousState = await readPlateState(flintPath, manifest.name);
661
+ const now = (/* @__PURE__ */ new Date()).toISOString();
662
+ await setPlateDeclaration(flintPath, manifest.name, {
663
+ source,
664
+ version: pinnedVersion ?? release.version,
665
+ title: manifest.title
666
+ });
667
+ await writePlateState(flintPath, manifest.name, {
668
+ version: 1,
669
+ plate: manifest.name,
670
+ source,
671
+ currentVersion: release.version,
672
+ installedAt: previousState?.installedAt ?? now,
673
+ updatedAt: now,
674
+ title: manifest.title,
675
+ shard: manifest.shard,
676
+ devUrl: previousState?.devUrl,
677
+ releaseTag: release.tag,
678
+ assetUrl: release.asset.url
679
+ });
680
+ const plate = await getPlate(flintPath, manifest.name);
681
+ if (!plate) {
682
+ throw new Error(`Installed plate "${manifest.name}" could not be resolved`);
683
+ }
684
+ return {
685
+ plate,
686
+ source,
687
+ version: release.version,
688
+ releaseTag: release.tag,
689
+ warnings: await validatePairedShard(flintPath, manifest)
690
+ };
691
+ }
692
+ async function installPlate(flintPath, nameOrSource, options = {}) {
693
+ const declarations = await getPlateDeclarations(flintPath);
694
+ const declaration = declarations[nameOrSource];
695
+ const source = normalizePlateSource(declaration?.source ?? nameOrSource);
696
+ const releaseName = resolvePlateReleaseName(declaration ? nameOrSource : nameOrSource, source);
697
+ const pinnedVersion = options.version ?? declaration?.version;
698
+ const release = await fetchPlateRelease(source, releaseName, pinnedVersion);
699
+ const archivePath = await downloadPlateReleaseAsset(source, release);
700
+ const extractedRoot = await extractPlateArchive(archivePath);
701
+ try {
702
+ return await installExtractedPlate(flintPath, source, release, extractedRoot, pinnedVersion);
703
+ } catch (error) {
704
+ await rm(extractedRoot, { recursive: true, force: true }).catch(() => {
705
+ });
706
+ throw error;
707
+ }
708
+ }
709
+ async function updatePlate(flintPath, plateName) {
710
+ const declarations = await getPlateDeclarations(flintPath);
711
+ const states = await listPlateStates(flintPath);
712
+ const names = plateName ? [plateName] : Array.from(/* @__PURE__ */ new Set([...Object.keys(declarations), ...Object.keys(states)]));
713
+ const results = [];
714
+ for (const name of names) {
715
+ const declaration = declarations[name];
716
+ const state = states[name];
717
+ const source = normalizePlateSource(declaration?.source ?? state?.source ?? name);
718
+ const releaseName = resolvePlateReleaseName(name, source);
719
+ const pinnedVersion = declaration?.version;
720
+ const release = await fetchPlateRelease(source, releaseName, pinnedVersion);
721
+ if (state?.currentVersion === release.version && !pinnedVersion) {
722
+ continue;
723
+ }
724
+ if (state?.currentVersion === release.version && pinnedVersion === release.version) {
725
+ continue;
726
+ }
727
+ const archivePath = await downloadPlateReleaseAsset(source, release);
728
+ const extractedRoot = await extractPlateArchive(archivePath);
729
+ try {
730
+ results.push(await installExtractedPlate(flintPath, source, release, extractedRoot, pinnedVersion));
731
+ } catch (error) {
732
+ await rm(extractedRoot, { recursive: true, force: true }).catch(() => {
733
+ });
734
+ throw error;
735
+ }
736
+ }
737
+ return results;
738
+ }
739
+ async function syncDeclaredPlates(flintPath, declarations) {
740
+ const plateDeclarations = declarations ?? await getPlateDeclarations(flintPath);
741
+ const states = await listPlateStates(flintPath);
742
+ const results = [];
743
+ for (const [name, declaration] of Object.entries(plateDeclarations)) {
744
+ if (!declaration.source) {
745
+ continue;
746
+ }
747
+ const state = states[name];
748
+ const targetVersion = declaration.version;
749
+ if (state?.currentVersion && (!targetVersion || state.currentVersion === targetVersion)) {
750
+ results.push({ name, status: "skipped", version: state.currentVersion });
751
+ continue;
752
+ }
753
+ try {
754
+ const installResult = await installPlate(flintPath, name, { version: targetVersion });
755
+ results.push({ name, status: "installed", version: installResult.version });
756
+ } catch (error) {
757
+ results.push({
758
+ name,
759
+ status: "error",
760
+ error: error instanceof Error ? error.message : String(error)
761
+ });
762
+ }
763
+ }
764
+ return results;
765
+ }
766
+ async function setPlateDevUrl(flintPath, plateName, devUrl) {
767
+ const normalized = normalizePlateName(plateName);
768
+ const existing = await readPlateState(flintPath, normalized);
769
+ const proper = nameFormats(plateName.replace(/-/g, " ")).proper;
770
+ const nextState = {
771
+ version: 1,
772
+ plate: normalized,
773
+ title: existing?.title ?? proper,
774
+ source: existing?.source,
775
+ currentVersion: existing?.currentVersion,
776
+ installedAt: existing?.installedAt,
777
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
778
+ shard: existing?.shard,
779
+ releaseTag: existing?.releaseTag,
780
+ assetUrl: existing?.assetUrl,
781
+ devUrl
782
+ };
783
+ await writePlateState(flintPath, normalized, nextState);
784
+ return nextState;
785
+ }
786
+ async function clearPlateDevUrl(flintPath, plateName) {
787
+ const normalized = normalizePlateName(plateName);
788
+ const existing = await readPlateState(flintPath, normalized);
789
+ if (!existing) {
790
+ return null;
791
+ }
792
+ const nextState = {
793
+ ...existing,
794
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
795
+ devUrl: void 0
796
+ };
797
+ await writePlateState(flintPath, normalized, nextState);
798
+ return nextState;
799
+ }
800
+ function resolveToolCommand(toolPath) {
801
+ if (toolPath.endsWith(".ts")) {
802
+ return { command: "npx", args: ["tsx", toolPath] };
803
+ }
804
+ if (toolPath.endsWith(".js") || toolPath.endsWith(".mjs")) {
805
+ return { command: "node", args: [toolPath] };
806
+ }
807
+ return { command: toolPath, args: [] };
808
+ }
809
+ function spawnBufferedProcess(command, args, options) {
810
+ return new Promise((resolvePromise, reject) => {
811
+ const child = spawn(command, args, {
812
+ cwd: options.cwd,
813
+ env: {
814
+ ...process.env,
815
+ ...options.env
816
+ },
817
+ stdio: ["ignore", "pipe", "pipe"]
818
+ });
819
+ let stdout = "";
820
+ let stderr = "";
821
+ let settled = false;
822
+ const finish = (result) => {
823
+ if (settled) {
824
+ return;
825
+ }
826
+ settled = true;
827
+ resolvePromise(result);
828
+ };
829
+ const timer = options.timeoutMs ? setTimeout(() => {
830
+ child.kill("SIGKILL");
831
+ reject(new Error("tool_timeout"));
832
+ }, options.timeoutMs) : null;
833
+ child.stdout?.on("data", (chunk) => {
834
+ stdout += chunk.toString();
835
+ });
836
+ child.stderr?.on("data", (chunk) => {
837
+ stderr += chunk.toString();
838
+ });
839
+ child.on("error", (error) => {
840
+ if (timer) clearTimeout(timer);
841
+ reject(error);
842
+ });
843
+ child.on("close", (code) => {
844
+ if (timer) clearTimeout(timer);
845
+ finish({
846
+ exitCode: code ?? 1,
847
+ stdout: stdout.trim(),
848
+ stderr: stderr.trim(),
849
+ command: [command, ...args].join(" ")
850
+ });
851
+ });
852
+ });
853
+ }
854
+ async function installPlateDeps(flintPath, plateName) {
855
+ const plate = await getPlate(flintPath, plateName);
856
+ if (!plate) {
857
+ throw new Error(`Plate not found: ${plateName}`);
858
+ }
859
+ return spawnBufferedProcess("pnpm", ["install"], { cwd: plate.path });
860
+ }
861
+ async function buildPlate(flintPath, plateName) {
862
+ const plate = await getPlate(flintPath, plateName);
863
+ if (!plate) {
864
+ throw new Error(`Plate not found: ${plateName}`);
865
+ }
866
+ const result = await spawnBufferedProcess("pnpm", ["build"], { cwd: plate.path });
867
+ if (result.exitCode !== 0) {
868
+ return result;
869
+ }
870
+ const entryPath = resolvePlateEntryPath(plate.path, plate.manifest);
871
+ if (!await fileExists(entryPath)) {
872
+ throw new Error(`Plate build did not produce entry file: ${relative(plate.path, entryPath)}`);
873
+ }
874
+ return result;
875
+ }
876
+ async function runPlateTool(flintPath, plateName, toolName, args = [], options = {}) {
877
+ const plate = await getPlate(flintPath, plateName);
878
+ if (!plate) {
879
+ throw new Error(`Plate not found: ${plateName}`);
880
+ }
881
+ const tool = plate.manifest.tools?.find((entry) => normalizePlateName(entry.name) === normalizePlateName(toolName));
882
+ if (!tool) {
883
+ throw new Error(`Tool not found: ${toolName}`);
884
+ }
885
+ const commandPath = resolve(plate.path, tool.command);
886
+ if (!await fileExists(commandPath)) {
887
+ throw new Error(`Tool command not found: ${relative(plate.path, commandPath)}`);
888
+ }
889
+ const runner = resolveToolCommand(commandPath);
890
+ return spawnBufferedProcess(runner.command, [...runner.args, ...args], {
891
+ cwd: flintPath,
892
+ env: {
893
+ FLINT_ROOT: resolve(flintPath),
894
+ PLATE_ROOT: plate.path,
895
+ PLATE_NAME: plate.manifest.name
896
+ },
897
+ timeoutMs: options.timeoutMs
898
+ });
899
+ }
900
+ async function listPlateTools(flintPath) {
901
+ const plates = await listPlates(flintPath);
902
+ return plates.flatMap((plate) => (plate.manifest.tools ?? []).map((tool) => ({ plate, tool })));
903
+ }
904
+ function spawnPlateDevServer(flintPath, plate) {
905
+ return spawn("pnpm", ["dev"], {
906
+ cwd: plate.path,
907
+ env: {
908
+ ...process.env,
909
+ FLINT_ROOT: resolve(flintPath),
910
+ PLATE_ROOT: plate.path,
911
+ PLATE_NAME: plate.manifest.name
912
+ },
913
+ stdio: "inherit"
914
+ });
915
+ }
916
+
917
+ export {
918
+ readPlateManifest,
919
+ initPlateRepo,
920
+ clonePlateFromRepo,
921
+ updatePlateFromRepo,
922
+ syncPlateRepos,
923
+ listPlates,
924
+ getPlate,
925
+ createPlate,
926
+ installPlate,
927
+ updatePlate,
928
+ syncDeclaredPlates,
929
+ setPlateDevUrl,
930
+ clearPlateDevUrl,
931
+ installPlateDeps,
932
+ buildPlate,
933
+ runPlateTool,
934
+ listPlateTools,
935
+ spawnPlateDevServer
936
+ };