truecanvas 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  readPackage,
15
15
  runScript,
16
16
  startHub
17
- } from "./chunk-FJO64PGM.js";
17
+ } from "./chunk-S5PJFVUC.js";
18
18
  import {
19
19
  CHUNK,
20
20
  decodeFrame,
@@ -22,7 +22,7 @@ import {
22
22
  responseHeaders,
23
23
  safePath,
24
24
  viewerHtml
25
- } from "./chunk-7KJVGN4O.js";
25
+ } from "./chunk-O6YS366Z.js";
26
26
  import {
27
27
  DEVICES,
28
28
  Workspace,
@@ -52,6 +52,7 @@ import {
52
52
  detectPm,
53
53
  findByPath,
54
54
  findNode,
55
+ nodeBin,
55
56
  parseCanvas,
56
57
  run,
57
58
  toolEnv
@@ -70,15 +71,15 @@ import {
70
71
  } from "./chunk-C4SATRZT.js";
71
72
 
72
73
  // src/cli/index.ts
73
- import { spawn } from "child_process";
74
- import fs9 from "fs";
75
- import path11 from "path";
74
+ import { spawn as spawn2 } from "child_process";
75
+ import fs10 from "fs";
76
+ import path12 from "path";
76
77
  import { parseArgs } from "util";
77
78
 
78
79
  // src/server/http.ts
79
80
  import http from "http";
80
- import fs6 from "fs";
81
- import path8 from "path";
81
+ import fs7 from "fs";
82
+ import path9 from "path";
82
83
  import { randomUUID } from "crypto";
83
84
  import { fileURLToPath } from "url";
84
85
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -86,7 +87,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
86
87
  import launchEditor from "launch-editor";
87
88
 
88
89
  // src/server/mcp.ts
89
- import path3 from "path";
90
+ import path4 from "path";
90
91
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
91
92
  import { z } from "zod";
92
93
 
@@ -284,6 +285,7 @@ async function replaceAsync(input, re, fn) {
284
285
  }
285
286
 
286
287
  // src/share/publish.ts
288
+ import { createHash } from "crypto";
287
289
  import fs2 from "fs";
288
290
  import os from "os";
289
291
  import path2 from "path";
@@ -341,36 +343,57 @@ async function publishSnapshot(site, dir, manifest, opts = {}) {
341
343
  for (const f of manifest.frames) for (const p of [f.html, f.image]) files.push({ local: path2.join(dir, p), remote: `${manifest.id}/${p}` });
342
344
  const skipped = [];
343
345
  let uploaded = 0;
344
- const queue2 = [...files];
345
- const worker = async () => {
346
- for (let f = queue2.shift(); f; f = queue2.shift()) {
347
- const body = fs2.readFileSync(f.local);
348
- if (body.length > MAX_FILE) {
349
- skipped.push(f.remote);
350
- continue;
346
+ const fitting = files.filter((f) => {
347
+ if (fs2.statSync(f.local).size <= MAX_FILE) return true;
348
+ skipped.push(f.remote);
349
+ return false;
350
+ });
351
+ if (can.includes("batch")) {
352
+ uploaded += await sendBatches(site, base, fitting.map((f) => ({ local: f.local, path: f.remote, type: contentType(f.local) })));
353
+ } else {
354
+ const queue2 = [...fitting];
355
+ const worker = async () => {
356
+ for (let f = queue2.shift(); f; f = queue2.shift()) {
357
+ await request(site, "PUT", `${base}/files`, fs2.readFileSync(f.local), { "x-path": f.remote, "content-type": contentType(f.local) });
358
+ uploaded++;
351
359
  }
352
- await request(site, "PUT", `${base}/files`, body, { "x-path": f.remote, "content-type": contentType(f.local) });
353
- uploaded++;
354
- }
355
- };
356
- await Promise.all(Array.from({ length: 4 }, worker));
360
+ };
361
+ await Promise.all(Array.from({ length: 4 }, worker));
362
+ }
357
363
  if (skipped.some((s) => s.endsWith(".html"))) throw new Error(`A frame is too large to upload (over 4.4 MB): ${skipped.filter((s) => s.endsWith(".html")).join(", ")}`);
358
364
  if (opts.live && "dir" in opts.live) {
359
365
  const liveDir = opts.live.dir;
360
- const liveFiles = walk(liveDir);
361
- const pending = [...liveFiles];
362
- const liveWorker = async () => {
363
- for (let rel = pending.shift(); rel; rel = pending.shift()) {
364
- const body = fs2.readFileSync(path2.join(liveDir, rel));
365
- if (body.length > MAX_FILE) {
366
- skipped.push(`live/${rel}`);
367
- continue;
368
- }
369
- await request(site, "PUT", `${base}/live/files?version=${manifest.id}`, body, { "x-path": rel, "content-type": contentType(rel) });
370
- uploaded++;
366
+ const liveFiles = walk(liveDir).filter((rel) => {
367
+ if (fs2.statSync(path2.join(liveDir, rel)).size <= MAX_FILE) return true;
368
+ skipped.push(`live/${rel}`);
369
+ return false;
370
+ });
371
+ if (can.includes("live-blobs")) {
372
+ const hashes = /* @__PURE__ */ new Map();
373
+ for (const rel of liveFiles) hashes.set(rel, createHash("sha256").update(fs2.readFileSync(path2.join(liveDir, rel))).digest("hex"));
374
+ const { missing } = await request(site, "POST", `${base}/live/manifest?version=${manifest.id}`, { files: Object.fromEntries(hashes) });
375
+ const byHash = new Map([...hashes].map(([rel, hash]) => [hash, rel]));
376
+ const pending = missing.map((h) => byHash.get(h)).filter(Boolean);
377
+ if (can.includes("batch")) uploaded += await sendBatches(site, base, pending.map((rel) => ({ local: path2.join(liveDir, rel), hash: hashes.get(rel) })));
378
+ else {
379
+ const blobWorker = async () => {
380
+ for (let rel = pending.shift(); rel; rel = pending.shift()) {
381
+ await request(site, "PUT", `${base}/live/blobs`, fs2.readFileSync(path2.join(liveDir, rel)), { "x-hash": hashes.get(rel), "content-type": "application/octet-stream" });
382
+ uploaded++;
383
+ }
384
+ };
385
+ await Promise.all(Array.from({ length: 6 }, blobWorker));
371
386
  }
372
- };
373
- await Promise.all(Array.from({ length: 6 }, liveWorker));
387
+ } else {
388
+ const pending = [...liveFiles];
389
+ const liveWorker = async () => {
390
+ for (let rel = pending.shift(); rel; rel = pending.shift()) {
391
+ await request(site, "PUT", `${base}/live/files?version=${manifest.id}`, fs2.readFileSync(path2.join(liveDir, rel)), { "x-path": rel, "content-type": contentType(rel) });
392
+ uploaded++;
393
+ }
394
+ };
395
+ await Promise.all(Array.from({ length: 6 }, liveWorker));
396
+ }
374
397
  }
375
398
  const change = {
376
399
  ...opts.password !== void 0 ? { password: opts.password } : {},
@@ -389,6 +412,35 @@ async function publishSnapshot(site, dir, manifest, opts = {}) {
389
412
  const invited = opts.invite?.length ? await inviteTo(site, share.slug, opts.invite) : [];
390
413
  return { url: done.url, version: manifest.id, versions: done.versions, uploaded, skipped, password, live: done.live ?? null, access, invited };
391
414
  }
415
+ async function sendBatches(site, base, items) {
416
+ const LIMIT = 4 * 1024 * 1024;
417
+ const batches = [];
418
+ let current = [];
419
+ let size = 0;
420
+ for (const item of items) {
421
+ const n = fs2.statSync(item.local).size;
422
+ if (current.length && (size + n > LIMIT || current.length >= 150)) {
423
+ batches.push(current);
424
+ current = [];
425
+ size = 0;
426
+ }
427
+ current.push(item);
428
+ size += n;
429
+ }
430
+ if (current.length) batches.push(current);
431
+ const send = async (batch) => {
432
+ const bodies = batch.map((i) => fs2.readFileSync(i.local));
433
+ const head = Buffer.from(JSON.stringify({ files: batch.map((i, k) => ({ path: i.path, hash: i.hash, type: i.type, size: bodies[k].length })) }));
434
+ const len = Buffer.alloc(4);
435
+ len.writeUInt32BE(head.length);
436
+ await request(site, "POST", `${base}/files/batch`, Buffer.concat([len, head, ...bodies]), { "content-type": "application/octet-stream" });
437
+ };
438
+ const queue2 = [...batches];
439
+ await Promise.all(Array.from({ length: 3 }, async () => {
440
+ for (let b = queue2.shift(); b; b = queue2.shift()) await send(b);
441
+ }));
442
+ return items.length;
443
+ }
392
444
  async function inviteTo(site, slug, emails) {
393
445
  const res = await request(site, "POST", `/api/shares/${slug}/invites`, { emails });
394
446
  return res.invited;
@@ -414,6 +466,21 @@ async function updateAccess(site, project, canvas, change) {
414
466
  const share = await request(site, "GET", `/api/shares/${found.slug}`);
415
467
  return { url: `${site.url}/s/${found.slug}`, access: access ?? share.access ?? null, invited };
416
468
  }
469
+ async function linkInfo(site, project, canvas) {
470
+ const found = await request(site, "GET", `/api/shares?project=${encodeURIComponent(project)}&canvas=${encodeURIComponent(canvas)}`);
471
+ if (!found.slug) return null;
472
+ const share = await request(site, "GET", `/api/shares/${found.slug}`);
473
+ const last = share.versions?.at(-1);
474
+ return {
475
+ url: `${site.url}/s/${found.slug}`,
476
+ title: share.title ?? canvas,
477
+ access: share.access ?? null,
478
+ password: !!share.password,
479
+ versions: share.versions?.length ?? 0,
480
+ updatedAt: last?.createdAt ?? null,
481
+ live: !!last?.live
482
+ };
483
+ }
417
484
  async function deleteShare(site, project, canvas) {
418
485
  const found = await request(site, "GET", `/api/shares?project=${encodeURIComponent(project)}&canvas=${encodeURIComponent(canvas)}`);
419
486
  if (!found.slug) return false;
@@ -485,6 +552,114 @@ function contentType(file) {
485
552
  return types[ext] ?? "application/octet-stream";
486
553
  }
487
554
 
555
+ // src/share/export.ts
556
+ import { spawn } from "child_process";
557
+ import fs3 from "fs";
558
+ import path3 from "path";
559
+ var SKIP = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", ".truecanvas", "out", ".turbo", ".vercel", ".swc", "pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"]);
560
+ var CONFIGS = ["next.config.ts", "next.config.mts", "next.config.mjs", "next.config.js", "next.config.cjs"];
561
+ function canExport(config) {
562
+ return config.framework === "next" || config.framework === "vite";
563
+ }
564
+ async function exportSite(config) {
565
+ const root = config.root;
566
+ if (config.framework === "vite") {
567
+ const out = path3.join(root, "node_modules", ".cache", "truecanvas", "live-export");
568
+ const cleanup2 = () => fs3.rmSync(out, { recursive: true, force: true });
569
+ try {
570
+ await run2(root, bin(root, "vite", "bin/vite.js"), ["build", "--outDir", out, "--emptyOutDir"]);
571
+ return { dir: out, cleanup: cleanup2 };
572
+ } catch (err) {
573
+ cleanup2();
574
+ throw err;
575
+ }
576
+ }
577
+ const work = path3.join(root, ".truecanvas", "live-export");
578
+ fs3.rmSync(work, { recursive: true, force: true });
579
+ fs3.mkdirSync(work, { recursive: true });
580
+ const ignore = path3.join(root, ".truecanvas", ".gitignore");
581
+ if (!fs3.existsSync(ignore)) fs3.writeFileSync(ignore, "# Truecanvas: generated files\n*\n");
582
+ const cleanup = () => fs3.rmSync(work, { recursive: true, force: true });
583
+ try {
584
+ copyProject(config, work);
585
+ wrapConfig(work);
586
+ await run2(work, bin(root, "next", "dist/bin/next"), ["build"]);
587
+ const out = path3.join(work, "out");
588
+ if (!fs3.existsSync(path3.join(out, "index.html"))) throw new Error("The build finished without a static site (no out/index.html).");
589
+ return { dir: out, cleanup };
590
+ } catch (err) {
591
+ cleanup();
592
+ throw err;
593
+ }
594
+ }
595
+ function copyProject(config, work) {
596
+ const root = config.root;
597
+ const leaveOut = new Set([config.routeDir, config.canvasDir].map((p) => path3.normalize(p)));
598
+ for (const name of fs3.readdirSync(root)) {
599
+ if (SKIP.has(name) || leaveOut.has(name)) continue;
600
+ fs3.cpSync(path3.join(root, name), path3.join(work, name), {
601
+ recursive: true,
602
+ filter: (src) => path3.basename(src) !== "node_modules" && !leaveOut.has(path3.normalize(path3.relative(root, src)))
603
+ });
604
+ }
605
+ const appDir = path3.join(work, config.appDir);
606
+ fs3.rmSync(path3.join(appDir, "api"), { recursive: true, force: true });
607
+ for (const dir of [work, path3.join(work, "src")]) for (const f of ["middleware.ts", "middleware.js", "proxy.ts", "proxy.js"]) fs3.rmSync(path3.join(dir, f), { force: true });
608
+ }
609
+ function wrapConfig(work) {
610
+ const found = CONFIGS.find((f) => fs3.existsSync(path3.join(work, f)));
611
+ const ts = found ? /\.m?ts$/.test(found) : true;
612
+ let imports = "const base = {};";
613
+ if (found) {
614
+ const renamed = `next.config.truecanvas-base${path3.extname(found)}`;
615
+ fs3.renameSync(path3.join(work, found), path3.join(work, renamed));
616
+ imports = `import base from "./${renamed}";`;
617
+ }
618
+ const body = `// @ts-nocheck
619
+ // Generated by Truecanvas: the project's config, built as a static site for a share link.
620
+ ${imports}
621
+ export default async function config(phase, ctx) {
622
+ let c = base;
623
+ if (typeof c === "function") c = await c(phase, ctx);
624
+ const { rewrites, redirects, headers, ...rest } = c ?? {};
625
+ return { ...rest, output: "export", typescript: { ...rest.typescript, ignoreBuildErrors: true }, images: { ...rest.images, unoptimized: true } };
626
+ }
627
+ `;
628
+ fs3.writeFileSync(path3.join(work, ts ? "next.config.ts" : "next.config.mjs"), body);
629
+ }
630
+ function bin(root, pkg, file) {
631
+ const p = path3.join(root, "node_modules", pkg, file);
632
+ if (!fs3.existsSync(p)) throw new Error(`Couldn't find ${pkg} in ${path3.join(root, "node_modules")}. Install the project's dependencies first.`);
633
+ return p;
634
+ }
635
+ function run2(cwd, script, args) {
636
+ return new Promise((resolve, reject) => {
637
+ const env = toolEnv({ NODE_ENV: "production", NEXT_TELEMETRY_DISABLED: "1" });
638
+ const node = nodeBin();
639
+ if (!node) return reject(new Error("Couldn't find Node.js to build the app."));
640
+ const child = spawn(node, [script, ...args], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
641
+ const log = [];
642
+ const keep = (b) => {
643
+ for (const line of b.toString().split("\n")) if (line.trim()) log.push(line);
644
+ if (log.length > 40) log.splice(0, log.length - 40);
645
+ };
646
+ child.stdout.on("data", keep);
647
+ child.stderr.on("data", keep);
648
+ const timer = setTimeout(() => child.kill("SIGTERM"), 15 * 6e4);
649
+ child.on("error", (err) => {
650
+ clearTimeout(timer);
651
+ reject(err);
652
+ });
653
+ child.on("close", (code) => {
654
+ clearTimeout(timer);
655
+ if (code === 0) return resolve();
656
+ const tail = log.filter((l) => !/^\s*(▲|✓|○|ƒ|●|Route|Creating|Collecting|Generating|Finalizing|Linting|Compiled)/.test(l)).slice(-6).join("\n");
657
+ reject(new Error(`The app couldn't be built as a static site:
658
+ ${tail || `exit code ${code}`}`));
659
+ });
660
+ });
661
+ }
662
+
488
663
  // src/server/mcp.ts
489
664
  var INSTRUCTIONS = `Truecanvas is a design canvas whose layers are the project's real React components.
490
665
  A canvas is a .tsx file (canvas/<name>.canvas.tsx) with <Frame> artboards; every edit you make is written to that file as clean TSX and appears live in the user's editor window.
@@ -1111,13 +1286,14 @@ ${msgs}`;
1111
1286
  ...canvasArg,
1112
1287
  frames: z.array(z.string()).optional().describe("Frame names to share; omit for all."),
1113
1288
  title: z.string().optional().describe('Title the client sees, e.g. "Homepage, round 2".'),
1114
- live: z.string().optional().describe('A live version clients open with "View live": the https URL where the app runs, or a static build folder (Next `out/`, Vite `dist/`, relative to the project) that the review site hosts.'),
1289
+ live: z.string().optional().describe("Another real site to use: the https URL where the app runs, or a static build folder (Next `out/`, Vite `dist/`, relative to the project). Usually omitted: the app is built as static pages automatically."),
1290
+ frozen: z.boolean().optional().describe("Leave the real site out (frozen frames only). By default the app is built as static pages so clients scroll and click the actual pages."),
1115
1291
  invite: z.array(z.string()).optional().describe("Emails of people to invite (the client). Each gets an email with a link that signs them in and opens the design. Only invite addresses the user gave you."),
1116
1292
  access: z.enum(["invited", "password", "public"]).optional().describe("Who can open the link: invited people (the default for new links on review sites with sign-in), a password (the user gives it to you), or anyone with the link. Existing links keep theirs unless this is set."),
1117
1293
  password: z.string().optional().describe(`The link's password, with access "password".`)
1118
1294
  }
1119
1295
  },
1120
- async ({ canvas, frames, title, live, invite, access, password }) => {
1296
+ async ({ canvas, frames, title, live, frozen, invite, access, password }) => {
1121
1297
  try {
1122
1298
  const c2 = resolveCanvas(canvas);
1123
1299
  presence(c2, "looking", "Preparing a share link");
@@ -1129,14 +1305,24 @@ ${msgs}`;
1129
1305
  ].filter(Boolean);
1130
1306
  const site = reviewSite();
1131
1307
  if (!site) return text([`\u2713 Snapshot ready (${manifest.frames.length} frames). No review site is set up, so this is a local preview: ${preview}`, "To get client links, the user runs `npx truecanvas share setup`.", ...notes].join("\n"));
1132
- const liveSite = live ? /^https?:\/\//.test(live) ? { url: live } : { dir: path3.resolve(ws.config.root, live) } : void 0;
1133
- const published = await publishSnapshot(site, dir, manifest, { title, live: liveSite, access, password, invite: invite?.length ? parseEmails(invite) : void 0 });
1308
+ let liveSite = live ? /^https?:\/\//.test(live) ? { url: live } : { dir: path4.resolve(ws.config.root, live) } : void 0;
1309
+ let built = null;
1310
+ if (!liveSite && !frozen && canExport(ws.config) && (await siteFeatures(site).catch(() => [])).includes("live-files")) {
1311
+ presence(c2, "looking", "Building the real site");
1312
+ try {
1313
+ built = await exportSite(ws.config);
1314
+ liveSite = { dir: built.dir };
1315
+ } catch (err) {
1316
+ notes.push(`The real site couldn't be built, so this version has frozen frames only: ${err.message}`);
1317
+ }
1318
+ }
1319
+ const published = await publishSnapshot(site, dir, manifest, { title, live: liveSite, access, password, invite: invite?.length ? parseEmails(invite) : void 0 }).finally(() => built?.cleanup());
1134
1320
  const who = published.access === "invited" ? ", invited people only" : published.access === "public" ? ", anyone with the link" : published.password ? ", password protected" : "";
1135
1321
  return text(
1136
1322
  [
1137
1323
  `\u2713 Shared ${manifest.frames.length} frame(s): ${published.url}`,
1138
1324
  `Version ${published.versions} of this link${who}.`,
1139
- published.live ? `Live version: ${published.live}` : "",
1325
+ published.live ? "The real site is included: clients scroll and click the actual pages." : "",
1140
1326
  ...published.invited.map((i) => i.error ? `Couldn't email ${i.email}: ${i.error}` : `Invited ${i.email} (emailed a sign-in link).`),
1141
1327
  published.access === "invited" && !published.invited.length ? "Only invited people and the studio can open it: share again with `invite` to invite the client." : "",
1142
1328
  ...notes
@@ -1354,13 +1540,13 @@ ${out.out.slice(-1500)}`));
1354
1540
 
1355
1541
  // src/server/thumbnails.ts
1356
1542
  import crypto2 from "crypto";
1357
- import fs3 from "fs";
1358
- import path4 from "path";
1543
+ import fs4 from "fs";
1544
+ import path5 from "path";
1359
1545
  var Thumbnails = class _Thumbnails {
1360
1546
  constructor(ws, shots) {
1361
1547
  this.ws = ws;
1362
1548
  this.shots = shots;
1363
- this.dir = path4.join(ws.config.root, "node_modules", ".cache", "truecanvas", "thumbs");
1549
+ this.dir = path5.join(ws.config.root, "node_modules", ".cache", "truecanvas", "thumbs");
1364
1550
  }
1365
1551
  ws;
1366
1552
  shots;
@@ -1386,8 +1572,8 @@ var Thumbnails = class _Thumbnails {
1386
1572
  key(spec, props) {
1387
1573
  let stamp = "";
1388
1574
  try {
1389
- const file = spec.library ? path4.join(this.ws.config.root, "node_modules", spec.library, "package.json") : path4.join(this.ws.config.root, spec.file);
1390
- stamp = String(fs3.statSync(file).mtimeMs);
1575
+ const file = spec.library ? path5.join(this.ws.config.root, "node_modules", spec.library, "package.json") : path5.join(this.ws.config.root, spec.file);
1576
+ stamp = String(fs4.statSync(file).mtimeMs);
1391
1577
  } catch {
1392
1578
  stamp = "0";
1393
1579
  }
@@ -1398,14 +1584,14 @@ var Thumbnails = class _Thumbnails {
1398
1584
  const spec = this.ws.catalog.get(name);
1399
1585
  if (!spec) throw new Error(`Unknown component ${name}`);
1400
1586
  const props = _Thumbnails.previewProps(spec, preset);
1401
- const file = path4.join(this.dir, `${name}-${this.key(spec, props)}.png`);
1402
- if (fs3.existsSync(file)) return fs3.readFileSync(file);
1587
+ const file = path5.join(this.dir, `${name}-${this.key(spec, props)}.png`);
1588
+ if (fs4.existsSync(file)) return fs4.readFileSync(file);
1403
1589
  const job = this.queue.then(async () => {
1404
- if (fs3.existsSync(file)) return fs3.readFileSync(file);
1590
+ if (fs4.existsSync(file)) return fs4.readFileSync(file);
1405
1591
  const url = `${this.ws.config.appUrl}/truecanvas/__preview?component=${encodeURIComponent(name)}&props=${encodeURIComponent(JSON.stringify(props))}`;
1406
1592
  const png = await this.shots.element(url, { width: 640, height: 480 });
1407
- fs3.mkdirSync(this.dir, { recursive: true });
1408
- fs3.writeFileSync(file, png);
1593
+ fs4.mkdirSync(this.dir, { recursive: true });
1594
+ fs4.writeFileSync(file, png);
1409
1595
  return png;
1410
1596
  });
1411
1597
  this.queue = job.catch(() => void 0);
@@ -1414,14 +1600,14 @@ var Thumbnails = class _Thumbnails {
1414
1600
  };
1415
1601
 
1416
1602
  // src/server/screenshot.ts
1417
- import fs4 from "fs";
1603
+ import fs5 from "fs";
1418
1604
  import os2 from "os";
1419
- import path5 from "path";
1605
+ import path6 from "path";
1420
1606
  function findChromium() {
1421
1607
  if (process.env.TRUECANVAS_CHROME) return process.env.TRUECANVAS_CHROME;
1422
- const cache2 = process.env.PLAYWRIGHT_BROWSERS_PATH || (process.platform === "win32" ? path5.join(process.env.LOCALAPPDATA ?? path5.join(os2.homedir(), "AppData", "Local"), "ms-playwright") : process.platform === "darwin" ? path5.join(os2.homedir(), "Library", "Caches", "ms-playwright") : path5.join(os2.homedir(), ".cache", "ms-playwright"));
1423
- if (fs4.existsSync(cache2)) {
1424
- const shells = fs4.readdirSync(cache2).filter((d) => d.startsWith("chromium_headless_shell-")).sort().reverse();
1608
+ const cache2 = process.env.PLAYWRIGHT_BROWSERS_PATH || (process.platform === "win32" ? path6.join(process.env.LOCALAPPDATA ?? path6.join(os2.homedir(), "AppData", "Local"), "ms-playwright") : process.platform === "darwin" ? path6.join(os2.homedir(), "Library", "Caches", "ms-playwright") : path6.join(os2.homedir(), ".cache", "ms-playwright"));
1609
+ if (fs5.existsSync(cache2)) {
1610
+ const shells = fs5.readdirSync(cache2).filter((d) => d.startsWith("chromium_headless_shell-")).sort().reverse();
1425
1611
  for (const d of shells) {
1426
1612
  for (const sub of [
1427
1613
  "chrome-headless-shell-linux64/chrome-headless-shell",
@@ -1431,14 +1617,14 @@ function findChromium() {
1431
1617
  "chrome-headless-shell-win64/chrome-headless-shell.exe",
1432
1618
  "chrome-win/headless_shell.exe"
1433
1619
  ]) {
1434
- const p = path5.join(cache2, d, sub);
1435
- if (fs4.existsSync(p)) return p;
1620
+ const p = path6.join(cache2, d, sub);
1621
+ if (fs5.existsSync(p)) return p;
1436
1622
  }
1437
1623
  }
1438
1624
  }
1439
1625
  const programFiles = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(Boolean);
1440
1626
  for (const p of [
1441
- ...programFiles.flatMap((dir) => [path5.join(dir, "Google", "Chrome", "Application", "chrome.exe"), path5.join(dir, "Microsoft", "Edge", "Application", "msedge.exe")]),
1627
+ ...programFiles.flatMap((dir) => [path6.join(dir, "Google", "Chrome", "Application", "chrome.exe"), path6.join(dir, "Microsoft", "Edge", "Application", "msedge.exe")]),
1442
1628
  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
1443
1629
  "/usr/bin/chromium-browser",
1444
1630
  "/usr/bin/chromium",
@@ -1447,7 +1633,7 @@ function findChromium() {
1447
1633
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1448
1634
  "/Applications/Chromium.app/Contents/MacOS/Chromium"
1449
1635
  ]) {
1450
- if (fs4.existsSync(p)) return p;
1636
+ if (fs5.existsSync(p)) return p;
1451
1637
  }
1452
1638
  return void 0;
1453
1639
  }
@@ -1665,9 +1851,9 @@ ${root.outerHTML}`, css, canvases, height };
1665
1851
  var appRun = { state: "external", log: [] };
1666
1852
 
1667
1853
  // src/server/review.ts
1668
- import fs5 from "fs";
1669
- import path6 from "path";
1670
- import { createHash } from "crypto";
1854
+ import fs6 from "fs";
1855
+ import path7 from "path";
1856
+ import { createHash as createHash2 } from "crypto";
1671
1857
  var actorName = (e) => e.actor.kind === "agent" ? e.actor.name : e.actor.kind === "file" ? "code editor" : "you";
1672
1858
  async function versionAt(ws, canvas, ref) {
1673
1859
  const rel = ws.relFile(canvas);
@@ -1683,14 +1869,14 @@ async function versionAt(ws, canvas, ref) {
1683
1869
  }
1684
1870
  var readNow = (ws, file) => {
1685
1871
  try {
1686
- return fs5.readFileSync(path6.join(ws.config.root, file), "utf8");
1872
+ return fs6.readFileSync(path7.join(ws.config.root, file), "utf8");
1687
1873
  } catch {
1688
1874
  return null;
1689
1875
  }
1690
1876
  };
1691
1877
  async function reviewPage(ws, canvas, ref, since) {
1692
1878
  const rel = ws.relFile(canvas);
1693
- const exists = fs5.existsSync(ws.file(canvas));
1879
+ const exists = fs6.existsSync(ws.file(canvas));
1694
1880
  const old = await versionAt(ws, canvas, ref);
1695
1881
  if (!exists && old.source === null) return null;
1696
1882
  const now = exists ? ws.read(canvas) : null;
@@ -1760,7 +1946,7 @@ function renderFrame(ws, shots, canvas, frame, side, ref) {
1760
1946
  async function render(ws, shots, canvas, frame, side, ref) {
1761
1947
  let source;
1762
1948
  let files = {};
1763
- if (side === "after") source = fs5.existsSync(ws.file(canvas)) ? ws.read(canvas) : null;
1949
+ if (side === "after") source = fs6.existsSync(ws.file(canvas)) ? ws.read(canvas) : null;
1764
1950
  else {
1765
1951
  const old = await versionAt(ws, canvas, ref);
1766
1952
  source = old.source;
@@ -1771,12 +1957,12 @@ async function render(ws, shots, canvas, frame, side, ref) {
1771
1957
  const f = doc.frames.find((x) => x.frameName === frame);
1772
1958
  if (!f) return null;
1773
1959
  const linkedNow = f.page ? [f.page, ...layoutFiles(ws.config, f.page)].map((x) => readNow(ws, x) ?? "").join("\0") : "";
1774
- const key = createHash("sha1").update([canvas, frame, side, source, JSON.stringify(files), side === "after" ? linkedNow : ""].join("\0")).digest("hex");
1960
+ const key = createHash2("sha1").update([canvas, frame, side, source, JSON.stringify(files), side === "after" ? linkedNow : ""].join("\0")).digest("hex");
1775
1961
  const hit = cache.get(key);
1776
1962
  if (hit) return hit;
1777
1963
  let name = canvas;
1778
1964
  const snapshot = `${canvas}.${side === "before" ? "base" : "now"}`;
1779
- if (side === "before" || !fs5.existsSync(ws.file(canvas))) name = writeCompare(ws.config, canvas, source, files, snapshot);
1965
+ if (side === "before" || !fs6.existsSync(ws.file(canvas))) name = writeCompare(ws.config, canvas, source, files, snapshot);
1780
1966
  try {
1781
1967
  const device = findDevice(f.device);
1782
1968
  const png = await shots.frame({ canvas: name, frame, width: f.width, height: f.height, theme: f.theme ?? "light", scale: Math.min(2, Math.max(0.4, 1e3 / f.width)), mobile: device ? device.kind !== "desktop" : false, jpeg: true, maxHeight: 6e3 });
@@ -1811,7 +1997,7 @@ function prDesignSection(pages, images) {
1811
1997
  }
1812
1998
 
1813
1999
  // src/share/sync.ts
1814
- import path7 from "path";
2000
+ import path8 from "path";
1815
2001
  var EVERY = 2e4;
1816
2002
  var LOOKUP_EVERY = 5 * 6e4;
1817
2003
  var CommentSync = class {
@@ -1863,7 +2049,7 @@ var CommentSync = class {
1863
2049
  async slugOf(site, canvas) {
1864
2050
  const known = this.links.get(canvas);
1865
2051
  if (known && (known.slug || Date.now() - known.at < LOOKUP_EVERY)) return known.slug;
1866
- const project = path7.basename(this.ws.config.root);
2052
+ const project = path8.basename(this.ws.config.root);
1867
2053
  const res = await call(site, "GET", `/api/shares?project=${encodeURIComponent(project)}&canvas=${encodeURIComponent(canvas)}`);
1868
2054
  this.links.set(canvas, { slug: res.slug, at: Date.now() });
1869
2055
  return res.slug;
@@ -2091,8 +2277,8 @@ var LiveSession = class {
2091
2277
  }
2092
2278
  // ---------- the tunnel ----------
2093
2279
  /** The app URL for a tunneled path, or null if it would leave the app. */
2094
- target(path12, ws = false) {
2095
- const safe = safePath(path12);
2280
+ target(path13, ws = false) {
2281
+ const safe = safePath(path13);
2096
2282
  if (!safe) return null;
2097
2283
  const url = new URL(safe, this.app);
2098
2284
  if (url.origin !== this.app.origin) return null;
@@ -2217,8 +2403,8 @@ var LiveSession = class {
2217
2403
  }
2218
2404
  });
2219
2405
  }
2220
- openRelay(id, path12, protocols) {
2221
- const target = this.target(path12, true);
2406
+ openRelay(id, path13, protocols) {
2407
+ const target = this.target(path13, true);
2222
2408
  if (!target) {
2223
2409
  this.send({ t: "ws-close", id, code: 1008, reason: "Not part of the app" });
2224
2410
  return;
@@ -2335,17 +2521,23 @@ var MIME = {
2335
2521
  ".ico": "image/x-icon"
2336
2522
  };
2337
2523
  async function startServer(ws) {
2524
+ let shareStep = null;
2338
2525
  const { config } = ws;
2339
2526
  const editorDir = fileURLToPath(new URL("./editor/", import.meta.url));
2340
2527
  const shots = new Screenshotter(config.appUrl);
2341
2528
  let prCache = null;
2342
2529
  const thumbs = new Thumbnails(ws, shots);
2343
2530
  const hub = process.env.TRUECANVAS_HUB_URL;
2344
- const commentSync = new CommentSync(
2345
- ws,
2346
- hub ? (e) => void fetch(`${hub}/api/hub/notify`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ path: config.root, ...e }), signal: AbortSignal.timeout(5e3) }).catch(() => {
2347
- }) : void 0
2348
- );
2531
+ const clients = /* @__PURE__ */ new Set();
2532
+ const commentSync = new CommentSync(ws, (e) => {
2533
+ const data2 = `data: ${JSON.stringify({ type: "client-comment", ...e })}
2534
+
2535
+ `;
2536
+ for (const res of clients) res.write(data2);
2537
+ if (hub)
2538
+ void fetch(`${hub}/api/hub/notify`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ path: config.root, ...e }), signal: AbortSignal.timeout(5e3) }).catch(() => {
2539
+ });
2540
+ });
2349
2541
  commentSync.start();
2350
2542
  const syncRegistry = () => ws.catalog.load().then((list) => syncComponentRegistry(config, list)).catch(() => {
2351
2543
  });
@@ -2353,7 +2545,6 @@ async function startServer(ws) {
2353
2545
  const origin = `http://localhost:${config.port}`;
2354
2546
  const allowedHosts = /* @__PURE__ */ new Set([`localhost:${config.port}`, `127.0.0.1:${config.port}`]);
2355
2547
  const allowedOrigins = /* @__PURE__ */ new Set([origin, `http://127.0.0.1:${config.port}`]);
2356
- const clients = /* @__PURE__ */ new Set();
2357
2548
  const broadcast = (e) => {
2358
2549
  const data2 = `data: ${JSON.stringify(e)}
2359
2550
 
@@ -2367,7 +2558,7 @@ async function startServer(ws) {
2367
2558
  `;
2368
2559
  for (const res of clients) res.write(data2);
2369
2560
  };
2370
- const sessions = new Sessions(path8.basename(config.root), config.appUrl, {
2561
+ const sessions = new Sessions(path9.basename(config.root), config.appUrl, {
2371
2562
  onEvent: (canvas, e) => {
2372
2563
  if (e.t === "comments") void commentSync.syncAll();
2373
2564
  else sendRaw({ type: "room", canvas, event: e });
@@ -2429,7 +2620,7 @@ async function startServer(ws) {
2429
2620
  appUrl: config.appUrl,
2430
2621
  framework: config.framework,
2431
2622
  root: config.root,
2432
- projectName: path8.basename(config.root),
2623
+ projectName: path9.basename(config.root),
2433
2624
  canvases: ws.canvases(),
2434
2625
  darkMode: config.darkMode,
2435
2626
  agents: [...ws.agents.values()],
@@ -2579,7 +2770,7 @@ async function startServer(ws) {
2579
2770
  for (const file of [f.page, ...layoutFiles(config, f.page)]) {
2580
2771
  const old = await ws.git.show(ref, file);
2581
2772
  if (old === null) continue;
2582
- const now = fs6.existsSync(path8.join(config.root, file)) ? fs6.readFileSync(path8.join(config.root, file), "utf8") : null;
2773
+ const now = fs7.existsSync(path9.join(config.root, file)) ? fs7.readFileSync(path9.join(config.root, file), "utf8") : null;
2583
2774
  if (old !== now) {
2584
2775
  files[file] = old;
2585
2776
  touched.add(f.frameName);
@@ -2709,26 +2900,53 @@ async function startServer(ws) {
2709
2900
  }
2710
2901
  case "POST /api/open-file": {
2711
2902
  const { file } = await readJson(req);
2712
- const abs = path8.resolve(config.root, file);
2713
- const rel = path8.relative(config.root, abs);
2714
- if (rel.startsWith("..") || path8.isAbsolute(rel)) return json(res, 400, { error: "Outside project." });
2903
+ const abs = path9.resolve(config.root, file);
2904
+ const rel = path9.relative(config.root, abs);
2905
+ if (rel.startsWith("..") || path9.isAbsolute(rel)) return json(res, 400, { error: "Outside project." });
2715
2906
  launchEditor(abs, config.editor);
2716
2907
  return json(res, 200, { ok: true });
2717
2908
  }
2718
2909
  case "GET /api/share/status": {
2719
2910
  const site = reviewSite();
2720
2911
  const features = site ? await siteFeatures(site).catch(() => []) : [];
2721
- return json(res, 200, { site: site ? site.url : null, features });
2912
+ return json(res, 200, { site: site ? site.url : null, features, realSite: canExport(config) && features.includes("live-files") });
2722
2913
  }
2914
+ case "GET /api/share/link": {
2915
+ const canvas = url.searchParams.get("canvas") ?? "";
2916
+ assertCanvasName(canvas);
2917
+ const site = reviewSite();
2918
+ return json(res, 200, { link: site ? await linkInfo(site, path9.basename(config.root), canvas).catch(() => null) : null });
2919
+ }
2920
+ case "GET /api/share/progress":
2921
+ return json(res, 200, { step: shareStep });
2723
2922
  case "POST /api/share/snapshot": {
2724
- const { canvas, frames, local, title, password, live, access, invite } = await readJson(req);
2923
+ const { canvas, frames, local, title, password, live, access, invite, realSite } = await readJson(req);
2725
2924
  assertCanvasName(canvas);
2726
- const { dir, manifest } = await createSnapshot(ws, shots, canvas, { frames });
2727
- const preview = `${origin}/share/${encodeURIComponent(canvas)}/${manifest.id}/`;
2728
2925
  const site = local ? null : reviewSite();
2729
- const published = site ? await publishSnapshot(site, dir, manifest, { title, password, live, access, invite: invite?.length ? parseEmails(invite) : void 0 }) : null;
2730
- if (published) commentSync.forget(canvas);
2731
- return json(res, 200, { manifest, preview, published });
2926
+ let built = null;
2927
+ let realSiteError = null;
2928
+ try {
2929
+ shareStep = "Rendering every frame through your app";
2930
+ const { dir, manifest } = await createSnapshot(ws, shots, canvas, { frames });
2931
+ const preview = `${origin}/share/${encodeURIComponent(canvas)}/${manifest.id}/`;
2932
+ let liveSite = live;
2933
+ if (site && !live && realSite !== false && canExport(config) && (await siteFeatures(site).catch(() => [])).includes("live-files")) {
2934
+ shareStep = "Building the real site";
2935
+ try {
2936
+ built = await exportSite(config);
2937
+ liveSite = { dir: built.dir };
2938
+ } catch (err) {
2939
+ realSiteError = err.message;
2940
+ }
2941
+ }
2942
+ if (site) shareStep = `Uploading to ${site.url.replace(/^https?:\/\//, "")}`;
2943
+ const published = site ? await publishSnapshot(site, dir, manifest, { title, password, live: liveSite, access, invite: invite?.length ? parseEmails(invite) : void 0 }) : null;
2944
+ if (published) commentSync.forget(canvas);
2945
+ return json(res, 200, { manifest, preview, published, realSiteError });
2946
+ } finally {
2947
+ built?.cleanup();
2948
+ shareStep = null;
2949
+ }
2732
2950
  }
2733
2951
  // live sessions: clients follow the canvas live on its link (editor button, CLI, MCP, desktop tray)
2734
2952
  case "GET /api/session":
@@ -2770,24 +2988,24 @@ async function startServer(ws) {
2770
2988
  return json(res, 404, { error: `No route ${route}` });
2771
2989
  }
2772
2990
  function serveStatic(res, pathname) {
2773
- let file = path8.join(editorDir, pathname === "/" ? "index.html" : pathname);
2774
- if (!inside(editorDir, file) || !fs6.existsSync(file) || fs6.statSync(file).isDirectory()) file = path8.join(editorDir, "index.html");
2775
- if (!fs6.existsSync(file)) {
2991
+ let file = path9.join(editorDir, pathname === "/" ? "index.html" : pathname);
2992
+ if (!inside(editorDir, file) || !fs7.existsSync(file) || fs7.statSync(file).isDirectory()) file = path9.join(editorDir, "index.html");
2993
+ if (!fs7.existsSync(file)) {
2776
2994
  res.writeHead(500, { "content-type": "text/plain" });
2777
2995
  return res.end("Editor bundle missing. Run `pnpm build` in packages/truecanvas.");
2778
2996
  }
2779
- const ext = path8.extname(file);
2997
+ const ext = path9.extname(file);
2780
2998
  const immutable = pathname.startsWith("/assets/");
2781
2999
  res.writeHead(200, { "content-type": MIME[ext] ?? "application/octet-stream", "cache-control": immutable ? "public, max-age=31536000, immutable" : "no-cache" });
2782
- fs6.createReadStream(file).on("error", () => res.destroy()).pipe(res);
3000
+ fs7.createReadStream(file).on("error", () => res.destroy()).pipe(res);
2783
3001
  }
2784
3002
  function serveShare(res, rest) {
2785
3003
  const root = sharesDir(config.root);
2786
- let file = path8.join(root, decodeURIComponent(rest));
2787
- if (rest.endsWith("/")) file = path8.join(file, "index.html");
2788
- if (!inside(root, file) || !fs6.existsSync(file) || fs6.statSync(file).isDirectory()) return json(res, 404, { error: "Not found" });
2789
- res.writeHead(200, { "content-type": MIME[path8.extname(file)] ?? "application/octet-stream", "cache-control": "no-cache", "x-content-type-options": "nosniff", "access-control-allow-origin": "*" });
2790
- fs6.createReadStream(file).on("error", () => res.destroy()).pipe(res);
3004
+ let file = path9.join(root, decodeURIComponent(rest));
3005
+ if (rest.endsWith("/")) file = path9.join(file, "index.html");
3006
+ if (!inside(root, file) || !fs7.existsSync(file) || fs7.statSync(file).isDirectory()) return json(res, 404, { error: "Not found" });
3007
+ res.writeHead(200, { "content-type": MIME[path9.extname(file)] ?? "application/octet-stream", "cache-control": "no-cache", "x-content-type-options": "nosniff", "access-control-allow-origin": "*" });
3008
+ fs7.createReadStream(file).on("error", () => res.destroy()).pipe(res);
2791
3009
  }
2792
3010
  const server = http.createServer(async (req, res) => {
2793
3011
  try {
@@ -2807,8 +3025,8 @@ async function startServer(ws) {
2807
3025
  else res.end();
2808
3026
  }
2809
3027
  });
2810
- const canvasDir = path8.join(config.root, config.canvasDir);
2811
- fs6.mkdirSync(canvasDir, { recursive: true });
3028
+ const canvasDir = path9.join(config.root, config.canvasDir);
3029
+ fs7.mkdirSync(canvasDir, { recursive: true });
2812
3030
  const pending = /* @__PURE__ */ new Map();
2813
3031
  const onCanvasFile = (filename) => {
2814
3032
  if (!filename) return;
@@ -2833,7 +3051,7 @@ async function startServer(ws) {
2833
3051
  const watchCanvasDir = () => {
2834
3052
  let ino = null;
2835
3053
  try {
2836
- ino = fs6.statSync(canvasDir).ino;
3054
+ ino = fs7.statSync(canvasDir).ino;
2837
3055
  } catch {
2838
3056
  ino = null;
2839
3057
  }
@@ -2842,21 +3060,21 @@ async function startServer(ws) {
2842
3060
  canvasWatcher = null;
2843
3061
  watchedIno = ino;
2844
3062
  if (ino === null) return;
2845
- canvasWatcher = fs6.watch(canvasDir, (_event, filename) => onCanvasFile(filename));
3063
+ canvasWatcher = fs7.watch(canvasDir, (_event, filename) => onCanvasFile(filename));
2846
3064
  canvasWatcher.on("error", () => {
2847
3065
  canvasWatcher?.close();
2848
3066
  canvasWatcher = null;
2849
3067
  });
2850
- for (const f of fs6.readdirSync(canvasDir)) onCanvasFile(f);
3068
+ for (const f of fs7.readdirSync(canvasDir)) onCanvasFile(f);
2851
3069
  };
2852
3070
  watchCanvasDir();
2853
3071
  setInterval(watchCanvasDir, 2e3).unref();
2854
- const appDirAbs = path8.join(config.root, config.appDir);
2855
- if (config.framework === "next" && fs6.existsSync(appDirAbs)) {
3072
+ const appDirAbs = path9.join(config.root, config.appDir);
3073
+ if (config.framework === "next" && fs7.existsSync(appDirAbs)) {
2856
3074
  const appPending = /* @__PURE__ */ new Map();
2857
- const appWatcher = fs6.watch(appDirAbs, { recursive: true }, (_event, filename) => {
3075
+ const appWatcher = fs7.watch(appDirAbs, { recursive: true }, (_event, filename) => {
2858
3076
  if (!filename || !/(^|[\\/])(page|layout)\.[jt]sx$/.test(filename) || /^truecanvas[\\/]/.test(filename)) return;
2859
- const rel = path8.join(config.appDir, filename).split(path8.sep).join("/");
3077
+ const rel = path9.join(config.appDir, filename).split(path9.sep).join("/");
2860
3078
  clearTimeout(appPending.get(rel));
2861
3079
  appPending.set(
2862
3080
  rel,
@@ -2873,9 +3091,9 @@ async function startServer(ws) {
2873
3091
  }
2874
3092
  let catalogTimer;
2875
3093
  for (const dir of componentRoots(config.components)) {
2876
- const abs = path8.join(config.root, dir);
2877
- if (!fs6.existsSync(abs)) continue;
2878
- const componentWatcher = fs6.watch(abs, { recursive: true }, () => {
3094
+ const abs = path9.join(config.root, dir);
3095
+ if (!fs7.existsSync(abs)) continue;
3096
+ const componentWatcher = fs7.watch(abs, { recursive: true }, () => {
2879
3097
  clearTimeout(catalogTimer);
2880
3098
  catalogTimer = setTimeout(() => {
2881
3099
  ws.catalog.invalidate();
@@ -2911,8 +3129,8 @@ function dedent(code, col) {
2911
3129
  return code.split("\n").map((l, i) => i > 0 && l.startsWith(pad) ? l.slice(col) : l).join("\n");
2912
3130
  }
2913
3131
  function inside(dir, file) {
2914
- const rel = path8.relative(dir, file);
2915
- return !!rel && !rel.startsWith("..") && !path8.isAbsolute(rel);
3132
+ const rel = path9.relative(dir, file);
3133
+ return !!rel && !rel.startsWith("..") && !path9.isAbsolute(rel);
2916
3134
  }
2917
3135
 
2918
3136
  // src/server/stdio.ts
@@ -2954,9 +3172,9 @@ async function runStdioBridge(mcpUrl) {
2954
3172
  import { createRequire } from "module";
2955
3173
 
2956
3174
  // src/hub/desktop.ts
2957
- import fs7 from "fs";
3175
+ import fs8 from "fs";
2958
3176
  import os3 from "os";
2959
- import path9 from "path";
3177
+ import path10 from "path";
2960
3178
  var ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
2961
3179
  <defs>
2962
3180
  <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff9466"/><stop offset="1" stop-color="#ea6a3c"/></linearGradient>
@@ -2970,15 +3188,15 @@ var ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
2970
3188
  <path fill="#f6f5f1" fill-rule="evenodd" d="M236 236h552v168H236z M428 316h168v472H428z"/>
2971
3189
  </svg>
2972
3190
  `;
2973
- var data = () => process.env.XDG_DATA_HOME ?? path9.join(os3.homedir(), ".local", "share");
3191
+ var data = () => process.env.XDG_DATA_HOME ?? path10.join(os3.homedir(), ".local", "share");
2974
3192
  function installDesktop(cli, port) {
2975
- const dir = path9.join(data(), "truecanvas");
2976
- fs7.mkdirSync(dir, { recursive: true });
2977
- const icon = path9.join(dir, "icon.svg");
2978
- fs7.writeFileSync(icon, ICON);
2979
- const launcher = path9.join(dir, "launch.sh");
3193
+ const dir = path10.join(data(), "truecanvas");
3194
+ fs8.mkdirSync(dir, { recursive: true });
3195
+ const icon = path10.join(dir, "icon.svg");
3196
+ fs8.writeFileSync(icon, ICON);
3197
+ const launcher = path10.join(dir, "launch.sh");
2980
3198
  const url = `http://localhost:${port}`;
2981
- fs7.writeFileSync(
3199
+ fs8.writeFileSync(
2982
3200
  launcher,
2983
3201
  `#!/bin/sh
2984
3202
  # Truecanvas launcher: start the hub if needed, then open its window.
@@ -2987,7 +3205,7 @@ CLI=${JSON.stringify(cli)}
2987
3205
  URL=${JSON.stringify(url)}
2988
3206
  LOG="\${XDG_STATE_HOME:-$HOME/.local/state}/truecanvas-hub.log"
2989
3207
  # apps started from the desktop don't get your shell's PATH: next, pnpm and npx need it
2990
- export PATH=${JSON.stringify([path9.dirname(process.execPath), ...(process.env.PATH ?? "").split(path9.delimiter)].filter((p, i, a) => p && a.indexOf(p) === i).join(path9.delimiter))}
3208
+ export PATH=${JSON.stringify([path10.dirname(process.execPath), ...(process.env.PATH ?? "").split(path10.delimiter)].filter((p, i, a) => p && a.indexOf(p) === i).join(path10.delimiter))}
2991
3209
  mkdir -p "$(dirname "$LOG")"
2992
3210
 
2993
3211
  if ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; then
@@ -2996,7 +3214,7 @@ if ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; then
2996
3214
  while [ $i -lt 60 ] && ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; do sleep 0.25; i=$((i+1)); done
2997
3215
  fi
2998
3216
 
2999
- PROFILE=${JSON.stringify(path9.join(dir, "window"))}
3217
+ PROFILE=${JSON.stringify(path10.join(dir, "window"))}
3000
3218
  for b in google-chrome google-chrome-stable chromium-browser chromium brave-browser microsoft-edge; do
3001
3219
  if command -v "$b" >/dev/null 2>&1; then
3002
3220
  exec "$b" --app="$URL" --class=Truecanvas --user-data-dir="$PROFILE" --no-first-run --no-default-browser-check
@@ -3006,10 +3224,10 @@ exec xdg-open "$URL"
3006
3224
  `,
3007
3225
  { mode: 493 }
3008
3226
  );
3009
- const apps = path9.join(data(), "applications");
3010
- fs7.mkdirSync(apps, { recursive: true });
3011
- const desktop = path9.join(apps, "truecanvas.desktop");
3012
- fs7.writeFileSync(
3227
+ const apps = path10.join(data(), "applications");
3228
+ fs8.mkdirSync(apps, { recursive: true });
3229
+ const desktop = path10.join(apps, "truecanvas.desktop");
3230
+ fs8.writeFileSync(
3013
3231
  desktop,
3014
3232
  `[Desktop Entry]
3015
3233
  Type=Application
@@ -3025,11 +3243,11 @@ StartupWMClass=Truecanvas
3025
3243
  StartupNotify=true
3026
3244
  `
3027
3245
  );
3028
- let command = path9.join(os3.homedir(), ".local", "bin", "truecanvas");
3029
- const ours = !fs7.existsSync(command) || fs7.readFileSync(command, "utf8").includes("# truecanvas wrapper");
3246
+ let command = path10.join(os3.homedir(), ".local", "bin", "truecanvas");
3247
+ const ours = !fs8.existsSync(command) || fs8.readFileSync(command, "utf8").includes("# truecanvas wrapper");
3030
3248
  if (ours) {
3031
- fs7.mkdirSync(path9.dirname(command), { recursive: true });
3032
- fs7.writeFileSync(command, `#!/bin/sh
3249
+ fs8.mkdirSync(path10.dirname(command), { recursive: true });
3250
+ fs8.writeFileSync(command, `#!/bin/sh
3033
3251
  # truecanvas wrapper (installed by \`truecanvas desktop\`)
3034
3252
  exec ${JSON.stringify(process.execPath)} ${JSON.stringify(cli)} "$@"
3035
3253
  `, { mode: 493 });
@@ -3037,19 +3255,19 @@ exec ${JSON.stringify(process.execPath)} ${JSON.stringify(cli)} "$@"
3037
3255
  return { desktop, launcher, icon, command };
3038
3256
  }
3039
3257
  function launcherPath() {
3040
- const p = path9.join(data(), "truecanvas", "launch.sh");
3041
- return fs7.existsSync(p) ? p : null;
3258
+ const p = path10.join(data(), "truecanvas", "launch.sh");
3259
+ return fs8.existsSync(p) ? p : null;
3042
3260
  }
3043
3261
  function uninstallDesktop() {
3044
3262
  const removed = [];
3045
- const cmd = path9.join(os3.homedir(), ".local", "bin", "truecanvas");
3046
- if (fs7.existsSync(cmd) && fs7.readFileSync(cmd, "utf8").includes("# truecanvas wrapper")) {
3047
- fs7.rmSync(cmd);
3263
+ const cmd = path10.join(os3.homedir(), ".local", "bin", "truecanvas");
3264
+ if (fs8.existsSync(cmd) && fs8.readFileSync(cmd, "utf8").includes("# truecanvas wrapper")) {
3265
+ fs8.rmSync(cmd);
3048
3266
  removed.push(cmd);
3049
3267
  }
3050
- for (const f of [path9.join(data(), "applications", "truecanvas.desktop"), path9.join(data(), "truecanvas")]) {
3051
- if (fs7.existsSync(f)) {
3052
- fs7.rmSync(f, { recursive: true, force: true });
3268
+ for (const f of [path10.join(data(), "applications", "truecanvas.desktop"), path10.join(data(), "truecanvas")]) {
3269
+ if (fs8.existsSync(f)) {
3270
+ fs8.rmSync(f, { recursive: true, force: true });
3053
3271
  removed.push(f);
3054
3272
  }
3055
3273
  }
@@ -3062,8 +3280,8 @@ import { createInterface } from "readline/promises";
3062
3280
 
3063
3281
  // src/cli/doctor.ts
3064
3282
  import { execFile } from "child_process";
3065
- import fs8 from "fs";
3066
- import path10 from "path";
3283
+ import fs9 from "fs";
3284
+ import path11 from "path";
3067
3285
  var major = (v) => v ? Number(/(\d+)(?:\.(\d+))?/.exec(v)?.[1] ?? 0) : 0;
3068
3286
  function has(cmd, args, cwd) {
3069
3287
  return new Promise((resolve) => execFile(cmd, args, { cwd, timeout: 8e3 }, (err, stdout, stderr) => resolve(err ? null : `${stdout}${stderr}`)));
@@ -3090,7 +3308,7 @@ async function doctor(root, cliVersion) {
3090
3308
  const tw = depVersion(root, "tailwindcss");
3091
3309
  add(tw ? { label: "Tailwind CSS", status: "ok", detail: tw } : { label: "Tailwind CSS", status: "warn", detail: "not installed", fix: "Optional, but the layout and style controls write Tailwind classes" });
3092
3310
  const config = loadConfig(root);
3093
- if (config.framework === "next") add(fs8.existsSync(path10.join(root, config.appDir)) ? { label: "App Router", status: "ok", detail: config.appDir } : { label: "App Router", status: "fail", detail: `no ${config.appDir}/`, fix: "Truecanvas renders through the App Router (app/ or src/app/)" });
3311
+ if (config.framework === "next") add(fs9.existsSync(path11.join(root, config.appDir)) ? { label: "App Router", status: "ok", detail: config.appDir } : { label: "App Router", status: "fail", detail: `no ${config.appDir}/`, fix: "Truecanvas renders through the App Router (app/ or src/app/)" });
3094
3312
  const tc = installedIn(root);
3095
3313
  if (!tc.declared) add({ label: "Truecanvas in the project", status: "fail", detail: "not a dependency", fix: "npx truecanvas (sets it up)" });
3096
3314
  else if (!tc.installed) add({ label: "Truecanvas in the project", status: "fail", detail: "declared but not installed", fix: `${pm} install` });
@@ -3099,15 +3317,15 @@ async function doctor(root, cliVersion) {
3099
3317
  const wrapped = configStatus(root, config.framework);
3100
3318
  const configLabel = config.framework === "vite" ? "vite.config" : "next.config";
3101
3319
  const uses = config.framework === "vite" ? "has the truecanvas() plugin" : "uses withTruecanvas()";
3102
- add(wrapped.wrapped ? { label: configLabel, status: "ok", detail: `${path10.basename(wrapped.file)} ${uses}` } : { label: configLabel, status: "fail", detail: wrapped.file ? "Truecanvas not enabled" : "missing", fix: "npx truecanvas init" });
3320
+ add(wrapped.wrapped ? { label: configLabel, status: "ok", detail: `${path11.basename(wrapped.file)} ${uses}` } : { label: configLabel, status: "fail", detail: wrapped.file ? "Truecanvas not enabled" : "missing", fix: "npx truecanvas init" });
3103
3321
  const scripts = pkg.scripts ?? {};
3104
3322
  const script = Object.entries(scripts).find(([, v]) => v.includes("truecanvas"));
3105
3323
  add(script ? { label: "Start script", status: "ok", detail: runScript(pm, script[0]) } : { label: "Start script", status: "warn", detail: "none", fix: 'npx truecanvas init (adds "canvas": "truecanvas dev")' });
3106
- const ignore = fs8.existsSync(path10.join(root, ".gitignore")) ? fs8.readFileSync(path10.join(root, ".gitignore"), "utf8") : "";
3324
+ const ignore = fs9.existsSync(path11.join(root, ".gitignore")) ? fs9.readFileSync(path11.join(root, ".gitignore"), "utf8") : "";
3107
3325
  add(ignore.includes(config.routeDir) ? { label: "Generated route ignored", status: "ok" } : { label: "Generated route ignored", status: "warn", detail: `${config.routeDir}/ isn't in .gitignore`, fix: "npx truecanvas init" });
3108
3326
  add(hasAgentConfig(root) ? { label: "Agents", status: "ok", detail: ".mcp.json lists Truecanvas" } : { label: "Agents", status: "warn", detail: "not connected for this project", fix: "npx truecanvas connect (then commit .mcp.json)" });
3109
3327
  const chrome = findChromium();
3110
- add(chrome ? { label: "Screenshots", status: "ok", detail: path10.basename(chrome) } : { label: "Screenshots", status: "warn", detail: "no Chromium found", fix: "npx playwright install chromium-headless-shell (or install Chrome)" });
3328
+ add(chrome ? { label: "Screenshots", status: "ok", detail: path11.basename(chrome) } : { label: "Screenshots", status: "warn", detail: "no Chromium found", fix: "npx playwright install chromium-headless-shell (or install Chrome)" });
3111
3329
  const git = await has("git", ["rev-parse", "--is-inside-work-tree"], root);
3112
3330
  add(git ? { label: "Git", status: "ok" } : { label: "Git", status: "warn", detail: "not a repository", fix: "git init (branches, history, comments and compare use git)" });
3113
3331
  const gh = await has("gh", ["auth", "status"], root);
@@ -3134,7 +3352,8 @@ Usage
3134
3352
  truecanvas desktop Add Truecanvas to your app launcher (Linux); "desktop remove" undoes it
3135
3353
  truecanvas share [canvas] Share a canvas as a link for clients (needs truecanvas running).
3136
3354
  --password <p>, --no-password, --revoke, --restore, --delete, --title <t>, --local (preview only),
3137
- --live <url|dir>: a live version clients open with "View live" (where the app runs, or a static build)
3355
+ The real site (your app built as static pages) is included, so clients click the actual pages;
3356
+ --frozen leaves it out, --live <url|dir> uses another one (where the app runs, or a static build)
3138
3357
  --invite <emails>: invite people by email (they sign in with a link; new links are invited-only)
3139
3358
  --access invited|password|public: who can open the link
3140
3359
  --link-only: with --invite or --access, change the link without a new version
@@ -3235,6 +3454,7 @@ async function main() {
3235
3454
  delete: { type: "boolean" },
3236
3455
  token: { type: "string" },
3237
3456
  live: { type: "string" },
3457
+ frozen: { type: "boolean" },
3238
3458
  invite: { type: "string", multiple: true },
3239
3459
  access: { type: "string" },
3240
3460
  "link-only": { type: "boolean" },
@@ -3259,9 +3479,9 @@ async function main() {
3259
3479
  }
3260
3480
  if (!await ensureSetup(root2, pkgDir, version, yes)) return;
3261
3481
  if (await openInWindow(root2, values.port ? Number(values.port) : hubPort())) return;
3262
- const local = path11.join(root2, "node_modules", "truecanvas", "dist", "cli.js");
3263
- if (fs9.existsSync(local) && fs9.realpathSync(local) !== fs9.realpathSync(cli)) {
3264
- const child = spawn(process.execPath, [local, "dev", ...process.argv.slice(2).filter((a) => a !== "start" && a !== "-y" && a !== "--yes")], { stdio: "inherit" });
3482
+ const local = path12.join(root2, "node_modules", "truecanvas", "dist", "cli.js");
3483
+ if (fs10.existsSync(local) && fs10.realpathSync(local) !== fs10.realpathSync(cli)) {
3484
+ const child = spawn2(process.execPath, [local, "dev", ...process.argv.slice(2).filter((a) => a !== "start" && a !== "-y" && a !== "--yes")], { stdio: "inherit" });
3265
3485
  for (const sig of ["SIGINT", "SIGTERM"]) process.on(sig, () => child.kill(sig));
3266
3486
  child.on("exit", (code) => process.exit(code ?? 0));
3267
3487
  return;
@@ -3342,7 +3562,7 @@ async function main() {
3342
3562
  return;
3343
3563
  }
3344
3564
  const root = process.cwd();
3345
- if (!fs9.existsSync(path11.join(root, "package.json"))) {
3565
+ if (!fs10.existsSync(path12.join(root, "package.json"))) {
3346
3566
  console.error("Run truecanvas from your app's root (no package.json here).");
3347
3567
  process.exit(1);
3348
3568
  }
@@ -3352,7 +3572,7 @@ async function main() {
3352
3572
  const report = initProject(config);
3353
3573
  ws.ensureRoute();
3354
3574
  console.log(`
3355
- ${c.accent("\u25C6")} ${c.bold("Truecanvas")} is set up in ${c.bold(path11.basename(root))}
3575
+ ${c.accent("\u25C6")} ${c.bold("Truecanvas")} is set up in ${c.bold(path12.basename(root))}
3356
3576
  `);
3357
3577
  for (const line of report.done) console.log(` ${c.green("\u2713")} ${line}`);
3358
3578
  console.log(` ${c.green("\u2713")} Canvas folder ${c.bold(config.canvasDir)} and dev route ${c.bold(config.routeDir)}`);
@@ -3384,7 +3604,7 @@ async function main() {
3384
3604
  console.error(site ? "Name the canvas: truecanvas share <canvas> --delete" : `No review site yet: ${c.accent("npx truecanvas share setup")}`);
3385
3605
  process.exit(1);
3386
3606
  }
3387
- const done = await deleteShare(site, path11.basename(root), canvas);
3607
+ const done = await deleteShare(site, path12.basename(root), canvas);
3388
3608
  console.log(done ? ` ${c.green("\u2713")} Deleted the link of ${canvas}, with its versions and comments.` : ` ${canvas} has no link.`);
3389
3609
  return;
3390
3610
  }
@@ -3395,7 +3615,7 @@ async function main() {
3395
3615
  process.exit(1);
3396
3616
  }
3397
3617
  const change = { ...values.revoke ? { revoked: true } : values.restore ? { revoked: false } : {}, ...values["no-password"] ? { password: null } : {} };
3398
- const url = await updateShare(site, path11.basename(root), canvas, change);
3618
+ const url = await updateShare(site, path12.basename(root), canvas, change);
3399
3619
  console.log(` ${c.green("\u2713")} ${values.revoke ? "Link revoked" : values.restore ? "Link restored" : "Password removed"}: ${url}`);
3400
3620
  return;
3401
3621
  }
@@ -3418,7 +3638,7 @@ async function main() {
3418
3638
  process.exit(1);
3419
3639
  }
3420
3640
  try {
3421
- const done = await updateAccess(site, path11.basename(root), canvas, { access, password: values.password, invite });
3641
+ const done = await updateAccess(site, path12.basename(root), canvas, { access, password: values.password, invite });
3422
3642
  console.log(` ${c.green("\u2713")} ${done.url} ${c.dim(`(${accessLabel(done.access)})`)}`);
3423
3643
  printInvited(done.invited);
3424
3644
  } catch (err) {
@@ -3440,8 +3660,8 @@ async function main() {
3440
3660
  const publish = !values.local && !!reviewSite();
3441
3661
  console.log(` ${c.dim(`Rendering ${canvas} through your app${publish ? " and publishing it" : ""}\u2026`)}`);
3442
3662
  const password = values.password;
3443
- const live = values.live ? /^https?:\/\//.test(values.live) ? { url: values.live } : { dir: path11.resolve(values.live) } : void 0;
3444
- if (live && "dir" in live && !fs9.existsSync(path11.join(live.dir, "index.html"))) {
3663
+ const live = values.live ? /^https?:\/\//.test(values.live) ? { url: values.live } : { dir: path12.resolve(values.live) } : void 0;
3664
+ if (live && "dir" in live && !fs10.existsSync(path12.join(live.dir, "index.html"))) {
3445
3665
  console.error(` ${c.red("\u2717")} No index.html in ${live.dir}. Pass a static build folder (Next \`out/\`, Vite \`dist/\`) or the URL where the app runs.`);
3446
3666
  process.exit(1);
3447
3667
  }
@@ -3452,7 +3672,7 @@ async function main() {
3452
3672
  const res = await fetch(`${base}/api/share/snapshot`, {
3453
3673
  method: "POST",
3454
3674
  headers: { "content-type": "application/json", origin: base },
3455
- body: JSON.stringify({ canvas, local: !publish, title: values.title, password, live, access, invite })
3675
+ body: JSON.stringify({ canvas, local: !publish, title: values.title, password, live, access, invite, realSite: !values.frozen })
3456
3676
  });
3457
3677
  const body = await res.json();
3458
3678
  if (!res.ok) {
@@ -3460,14 +3680,16 @@ async function main() {
3460
3680
  process.exit(1);
3461
3681
  }
3462
3682
  const { manifest, published } = body;
3463
- console.log(` ${c.green("\u2713")} ${manifest.frames.length} frame${manifest.frames.length === 1 ? "" : "s"} frozen: no scripts, no API calls.`);
3683
+ console.log(` ${c.green("\u2713")} ${manifest.frames.length} frame${manifest.frames.length === 1 ? "" : "s"} rendered.`);
3464
3684
  if (manifest.external.length) console.log(` ${c.yellow("!")} While rendering, the app called ${manifest.external.join(", ")}. Whatever it showed is in the snapshot: use sample data for client links.`);
3465
3685
  if (manifest.missing.length) console.log(` ${c.yellow("!")} ${manifest.missing.length} asset${manifest.missing.length === 1 ? "" : "s"} couldn't be fetched.`);
3466
3686
  if (published?.skipped.length) console.log(` ${c.yellow("!")} Left out (over 4.4 MB): ${published.skipped.join(", ")}`);
3467
3687
  if (published) {
3468
3688
  const who = published.access ? `, ${accessLabel(published.access)}` : published.password ? ", password protected" : "";
3469
3689
  console.log(` ${c.green("\u2713")} Shared: ${c.bold(published.url)} ${c.dim(`(version ${published.versions}${who})`)}`);
3470
- if (published.live) console.log(` ${c.green("\u2713")} Live: ${published.live} ${c.dim(published.access !== "public" && (published.access || published.password) ? "(opens from the link's View live button)" : "")}`);
3690
+ if (published.live) console.log(` ${c.green("\u2713")} Real site included: clients scroll and click the actual pages.`);
3691
+ if (body.realSiteError) console.log(` ${c.yellow("!")} The real site couldn't be built, so this version has frozen frames only:
3692
+ ${c.dim(body.realSiteError.replace(/^/gm, " "))}`);
3471
3693
  printInvited(published.invited);
3472
3694
  if (published.access === "invited" && !published.invited.length) console.log(` ${c.dim("Only invited people and your studio can open it. Invite someone:")} ${c.accent(`npx truecanvas share ${canvas} --invite name@client.com --link-only`)}`);
3473
3695
  console.log("");
@@ -3571,12 +3793,12 @@ async function openWindow(cli, port) {
3571
3793
  const url = `http://localhost:${port}`;
3572
3794
  const launcher = launcherPath();
3573
3795
  if (launcher) {
3574
- spawn(launcher, [], { stdio: "ignore", detached: true }).unref();
3796
+ spawn2(launcher, [], { stdio: "ignore", detached: true }).unref();
3575
3797
  return console.log(`${c.accent("\u25C6")} Opening Truecanvas\u2026`);
3576
3798
  }
3577
3799
  const up = await fetch(`${url}/api/hub/state`, { signal: AbortSignal.timeout(1500) }).then((r) => r.ok).catch(() => false);
3578
3800
  if (!up) {
3579
- spawn(process.execPath, [cli, "hub", "--port", String(port), "--no-open"], { stdio: "ignore", detached: true }).unref();
3801
+ spawn2(process.execPath, [cli, "hub", "--port", String(port), "--no-open"], { stdio: "ignore", detached: true }).unref();
3580
3802
  for (let i = 0; i < 40 && !await fetch(`${url}/api/hub/state`).then((r) => r.ok).catch(() => false); i++) await new Promise((r) => setTimeout(r, 250));
3581
3803
  }
3582
3804
  openBrowser(url);
@@ -3589,7 +3811,7 @@ async function openInWindow(root, port) {
3589
3811
  const post = (route, body) => fetch(`${hub}${route}`, { method: "POST", headers: { "content-type": "application/json", origin: hub }, body: JSON.stringify(body) }).catch(() => null);
3590
3812
  await post("/api/hub/add", { path: root });
3591
3813
  await post("/api/hub/open", { path: root, focus: "1" });
3592
- console.log(`${c.accent("\u25C6")} Opened ${c.bold(path11.basename(root))} in the Truecanvas window ${c.dim(`(${hub})`)}.`);
3814
+ console.log(`${c.accent("\u25C6")} Opened ${c.bold(path12.basename(root))} in the Truecanvas window ${c.dim(`(${hub})`)}.`);
3593
3815
  return true;
3594
3816
  }
3595
3817
  async function prompt(question) {
@@ -3613,7 +3835,7 @@ async function ensureSetup(root, pkgDir, version, yes, force = false) {
3613
3835
  const tc = installedIn(root);
3614
3836
  const wrapped = configStatus(root).wrapped;
3615
3837
  if (tc.declared && tc.installed && wrapped && !force) return true;
3616
- const name = readPackage(root)?.name ?? path11.basename(root);
3838
+ const name = readPackage(root)?.name ?? path12.basename(root);
3617
3839
  if (!yes) {
3618
3840
  if (!process.stdin.isTTY) {
3619
3841
  console.error(`Truecanvas isn't set up in ${name} yet. Run again with --yes to set it up.`);
@@ -3684,10 +3906,10 @@ function startApp(root, appUrl, framework) {
3684
3906
  const port = new URL(appUrl).port || (framework === "vite" ? "5173" : "3000");
3685
3907
  const devArgs = framework === "vite" ? ["--port", port, "--strictPort", "--host", "127.0.0.1"] : ["dev", "-p", port, "-H", "127.0.0.1"];
3686
3908
  const pkg = framework === "vite" ? "vite" : "next";
3687
- const bin = binScript(root, pkg);
3688
- const [cmd, args] = bin ? [process.execPath, [bin, ...devArgs]] : [process.platform === "win32" ? "npx.cmd" : "npx", [pkg, ...devArgs]];
3909
+ const bin2 = binScript(root, pkg);
3910
+ const [cmd, args] = bin2 ? [process.execPath, [bin2, ...devArgs]] : [process.platform === "win32" ? "npx.cmd" : "npx", [pkg, ...devArgs]];
3689
3911
  const label = framework === "vite" ? "vite" : "next";
3690
- const child = spawn(cmd, args, { cwd: root, env: toolEnv(process.env.NO_COLOR ? {} : { FORCE_COLOR: "1" }), stdio: ["ignore", "pipe", "pipe"], shell: !bin && process.platform === "win32" });
3912
+ const child = spawn2(cmd, args, { cwd: root, env: toolEnv(process.env.NO_COLOR ? {} : { FORCE_COLOR: "1" }), stdio: ["ignore", "pipe", "pipe"], shell: !bin2 && process.platform === "win32" });
3691
3913
  child.on("error", (err) => console.error(` ${c.dim(`${label} \u2502`)} couldn't start ${pkg}: ${err.message}`));
3692
3914
  const prefix = c.dim(`${label} \u2502 `);
3693
3915
  for (const stream of [child.stdout, child.stderr]) {
@@ -3709,24 +3931,24 @@ function startApp(root, appUrl, framework) {
3709
3931
  }
3710
3932
  function binScript(root, pkg) {
3711
3933
  try {
3712
- const req = createRequire(path11.join(root, "package.json"));
3934
+ const req = createRequire(path12.join(root, "package.json"));
3713
3935
  const manifest = req.resolve(`${pkg}/package.json`);
3714
- const { bin } = JSON.parse(fs9.readFileSync(manifest, "utf8"));
3715
- const rel = typeof bin === "string" ? bin : bin?.[pkg];
3716
- return rel ? path11.join(path11.dirname(manifest), rel) : null;
3936
+ const { bin: bin2 } = JSON.parse(fs10.readFileSync(manifest, "utf8"));
3937
+ const rel = typeof bin2 === "string" ? bin2 : bin2?.[pkg];
3938
+ return rel ? path12.join(path12.dirname(manifest), rel) : null;
3717
3939
  } catch {
3718
3940
  return null;
3719
3941
  }
3720
3942
  }
3721
3943
  function stopTree(child, signal) {
3722
- if (process.platform === "win32" && child.pid) spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }).on("error", () => {
3944
+ if (process.platform === "win32" && child.pid) spawn2("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }).on("error", () => {
3723
3945
  });
3724
3946
  else child.kill(signal);
3725
3947
  }
3726
3948
  function openBrowser(url) {
3727
3949
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3728
3950
  try {
3729
- spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).on("error", () => {
3951
+ spawn2(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).on("error", () => {
3730
3952
  }).unref();
3731
3953
  } catch {
3732
3954
  }