truecanvas 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -114,11 +114,14 @@ The **Truecanvas window** (`npx truecanvas open`) manages several projects in ta
114
114
 
115
115
  **Share** (top right in the editor), `npx truecanvas share <canvas>`, or your agent's `share_canvas` renders every frame through your app and freezes it into static HTML and CSS: no scripts, so a link can never call your API or leak a session. It's published to your own review site as a new version of that page's link. Clients open it in any browser, with an optional password, and pin comments on the frames. Their threads sync into the canvas's comments while Truecanvas runs; your replies and your agent's (`reply_comment`, `resolve_comment`) go back to the link, under your studio's name. Comments you start in Truecanvas stay internal. `--revoke` takes a link down and `--delete` removes it with its comments.
116
116
 
117
- The review site is `packages/review`, a small Next.js app you deploy once to your Vercel account:
117
+ Add `--live <dir|url>` (or `live` in `share_canvas`) and clients also get **View live** on each frame: the real site at that frame's width, animations and interactions included. Pass a static build (Next `out/`, Vite `dist/`) for the review site to host, or the URL where the app already runs.
118
118
 
119
- 1. Create a Vercel project from this repo with **Root Directory** `packages/review`, and add a private **Blob** store and **Upstash for Redis** (both have free tiers).
120
- 2. Set `REVIEW_TOKEN` (a long random string), and optionally `BRAND_NAME` and `BRAND_LOGO` (a logo URL) for your studio's branding.
121
- 3. Add your domain (for example `review.your-studio.com`), then connect Truecanvas: `npx truecanvas share setup https://review.your-studio.com`.
119
+ You deploy the review site once, on your own domain:
120
+
121
+ - **Cloudflare** (`packages/review-worker`, recommended): Workers, D1 and R2 on the free plan, hosts live sites. See its [README](packages/review-worker/README.md).
122
+ - **Vercel** (`packages/review`): create a project with **Root Directory** `packages/review`, add a private **Blob** store and **Upstash for Redis**, set `REVIEW_TOKEN` (a long random string) and optionally `BRAND_NAME` and `BRAND_LOGO`, then add your domain. Frozen frames and comments only.
123
+
124
+ Then connect Truecanvas: `npx truecanvas share setup https://review.your-studio.com`.
122
125
 
123
126
  Without a review site, Share still makes a local preview of what clients would see.
124
127
 
@@ -126,7 +126,22 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
126
126
  #pop { left: 8px !important; right: 8px; top: auto !important; bottom: 8px; width: auto; max-height: 60dvh; }
127
127
  #panel { left: 8px; right: 8px; top: 56px; bottom: 68px; width: auto; }
128
128
  }
129
- @media (prefers-reduced-motion: reduce) { .frame-box iframe { transition: none; } }
129
+ /* live site */
130
+ .live-btn { height: 22px; min-width: 0; margin-left: 8px; padding: 0 8px 0 6px; gap: 4px; border-radius: 6px; vertical-align: middle;
131
+ background: var(--accent-soft); color: var(--accent); font-size: 11.5px; font-weight: 600; }
132
+ .live-btn:hover { background: var(--accent); color: #fff; }
133
+ #live { position: fixed; inset: 0; z-index: 50; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 16px;
134
+ background: rgba(12,12,11,.6); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); animation: fade-in .16s var(--ease-out); }
135
+ @keyframes fade-in { from { opacity: 0; } }
136
+ .live-bar { display: flex; align-items: center; gap: 10px; max-width: 100%; height: 40px; padding: 0 6px 0 14px; border-radius: 12px;
137
+ background: var(--panel-solid); border: 1px solid var(--line); box-shadow: 0 8px 28px -10px rgba(0,0,0,.35); }
138
+ .live-bar b { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
139
+ .live-bar a { color: var(--text); white-space: nowrap; text-decoration: none; padding: 0 8px; height: 30px; display: inline-flex; align-items: center; border-radius: 8px; }
140
+ .live-bar a:hover { background: var(--line); }
141
+ .live-device { position: relative; overflow: hidden; border-radius: 10px; background: #fff; box-shadow: 0 30px 80px -20px rgba(0,0,0,.6); animation: pop-in .2s var(--ease-out); }
142
+ .live-device iframe { position: absolute; left: 0; top: 0; border: 0; transform-origin: 0 0; background: #fff; }
143
+ @media (max-width: 640px) { .live-bar .meta { display: none; } }
144
+ @media (prefers-reduced-motion: reduce) { .frame-box iframe { transition: none; } #live, .live-device { animation: none; } }
130
145
  </style>
131
146
  </head>
132
147
  <body>
@@ -137,6 +152,10 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
137
152
  </header>
138
153
  <aside id="panel" hidden aria-label="Comments"><div class="panel-head"><span>Comments</span><button id="panel-close" aria-label="Close">\u2715</button></div><div class="panel-list" id="panel-list"></div></aside>
139
154
  <div id="pop" hidden role="dialog" aria-label="Comment"></div>
155
+ <div id="live" hidden role="dialog" aria-modal="true" aria-label="Live site">
156
+ <div class="live-bar"><b id="live-name"></b><span class="meta" id="live-size"></span><a id="live-tab" target="_blank" rel="noopener">Open in a new tab</a><button id="live-close" aria-label="Close live site" title="Close (Esc)">\u2715</button></div>
157
+ <div class="live-device" id="live-device"></div>
158
+ </div>
140
159
  <footer><div class="pill">
141
160
  <button id="out" aria-label="Zoom out" title="Zoom out (-)">\u2212</button>
142
161
  <button id="zoom" aria-label="Zoom to fit" title="Zoom to fit (1)">100%</button>
@@ -258,10 +277,12 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
258
277
  const el = document.createElement("div");
259
278
  el.className = "frame";
260
279
  el.style.cssText = "left:" + (f.x - minX) + "px;top:" + (f.y - minY) + "px;width:" + f.width + "px;height:" + f.height + "px";
261
- el.innerHTML = '<div class="frame-label"><b>' + esc(f.name) + "</b>" + f.width + " \xD7 " + Math.round(f.height) + "</div>" +
280
+ el.innerHTML = '<div class="frame-label"><b>' + esc(f.name) + "</b>" + f.width + " \xD7 " + Math.round(f.height) + (manifest.live ? '<button class="live-btn" title="Open the live site at this width"><svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M7 4.5v15l13-7.5z"/></svg>View live</button>' : "") + "</div>" +
262
281
  '<div class="frame-box" style="width:' + f.width + "px;height:" + f.height + 'px"><img alt="" src="' + esc(f.image) + '"><iframe sandbox title="' + esc(f.name) + '" scrolling="no" loading="lazy" src="' + esc(f.html) + '"></iframe></div>';
263
282
  const iframe = el.querySelector("iframe");
264
283
  iframe.addEventListener("load", () => iframe.classList.add("ready"));
284
+ const liveBtn = el.querySelector(".live-btn");
285
+ if (liveBtn) liveBtn.onclick = (e) => { e.stopPropagation(); openLive(f); };
265
286
  el.dataset.name = f.name;
266
287
  world.appendChild(el);
267
288
  labels.push(el.querySelector(".frame-label"));
@@ -319,8 +340,8 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
319
340
  let pinch = null;
320
341
  let moved = false, downAt = null;
321
342
  stage.addEventListener("pointerdown", (e) => {
322
- // pins are buttons: they take their own clicks
323
- if (e.target.closest && e.target.closest(".pin")) return;
343
+ // pins and View live are buttons: they take their own clicks
344
+ if (e.target.closest && e.target.closest(".pin, .live-btn")) return;
324
345
  stage.setPointerCapture(e.pointerId);
325
346
  moved = false;
326
347
  downAt = { x: e.clientX, y: e.clientY };
@@ -378,6 +399,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
378
399
  document.getElementById("out").onclick = () => zoomAt(z / 1.25, ...center());
379
400
  zoomLabel.onclick = () => (tall() ? fitWidth() : fit());
380
401
  addEventListener("keydown", (e) => {
402
+ if (liveOpen()) { if (e.key === "Escape") closeLive(); return; }
381
403
  if (e.target.closest && e.target.closest("input, textarea, select")) return;
382
404
  if (e.key === "Escape") { closePop(); setCommenting(false); return; }
383
405
  if ((e.key === "c" || e.key === "C") && canComment && !e.metaKey && !e.ctrlKey) { setCommenting(!commenting); return; }
@@ -389,6 +411,53 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
389
411
  });
390
412
  addEventListener("resize", apply);
391
413
 
414
+ // ---------- live site: the real thing, at the frame's width ----------
415
+ const liveBox = document.getElementById("live");
416
+ const liveDevice = document.getElementById("live-device");
417
+ let liveFrame = null, liveReturn = null;
418
+ const liveOpen = () => !liveBox.hidden;
419
+ // a realistic screen for the width: desktop, tablet or phone
420
+ const viewportHeight = (w) => (w >= 1024 ? 900 : w > 500 ? 1024 : 844);
421
+ function sizeLive() {
422
+ if (!liveFrame) return;
423
+ const w = liveFrame.width, h = viewportHeight(w);
424
+ const s = Math.min(1, (innerWidth - 32) / w, (innerHeight - 32 - 50) / h);
425
+ liveDevice.style.width = Math.round(w * s) + "px";
426
+ liveDevice.style.height = Math.round(h * s) + "px";
427
+ const iframe = liveDevice.querySelector("iframe");
428
+ iframe.style.width = w + "px";
429
+ iframe.style.height = h + "px";
430
+ iframe.style.transform = s < 1 ? "scale(" + s + ")" : "";
431
+ document.getElementById("live-size").textContent = w + " \xD7 " + h + (s < 1 ? " \xB7 " + Math.round(s * 100) + "%" : "");
432
+ }
433
+ function openLive(f) {
434
+ liveFrame = f;
435
+ liveReturn = document.activeElement;
436
+ liveDevice.textContent = "";
437
+ // its own origin: scripts run there, never on the review site
438
+ const iframe = document.createElement("iframe");
439
+ iframe.title = "Live site: " + f.name;
440
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox");
441
+ iframe.setAttribute("referrerpolicy", "no-referrer");
442
+ iframe.src = manifest.live.url;
443
+ liveDevice.appendChild(iframe);
444
+ document.getElementById("live-name").textContent = f.name;
445
+ document.getElementById("live-tab").href = manifest.live.url;
446
+ liveBox.hidden = false;
447
+ sizeLive();
448
+ document.getElementById("live-close").focus();
449
+ }
450
+ function closeLive() {
451
+ liveBox.hidden = true;
452
+ liveDevice.textContent = "";
453
+ liveFrame = null;
454
+ if (liveReturn && liveReturn.focus) liveReturn.focus();
455
+ }
456
+ document.getElementById("live-close").onclick = closeLive;
457
+ // a click on the backdrop closes it too
458
+ liveBox.addEventListener("click", (e) => { if (e.target === liveBox) closeLive(); });
459
+ addEventListener("resize", sizeLive);
460
+
392
461
  // ---------- comments (on a review site: the manifest says so) ----------
393
462
  var canComment = !!manifest.comments;
394
463
  var commenting = false;
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  viewerHtml
4
- } from "./chunk-M7B3W7EP.js";
4
+ } from "./chunk-DV76O5SZ.js";
5
5
  import {
6
6
  DEVICES,
7
7
  EditError,
@@ -48,13 +48,13 @@ import {
48
48
  // src/cli/index.ts
49
49
  import { spawn as spawn3 } from "child_process";
50
50
  import fs13 from "fs";
51
- import path14 from "path";
51
+ import path15 from "path";
52
52
  import { parseArgs } from "util";
53
53
 
54
54
  // src/server/http.ts
55
55
  import http from "http";
56
56
  import fs6 from "fs";
57
- import path7 from "path";
57
+ import path8 from "path";
58
58
  import { randomUUID } from "crypto";
59
59
  import { fileURLToPath } from "url";
60
60
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -62,6 +62,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
62
62
  import launchEditor from "launch-editor";
63
63
 
64
64
  // src/server/mcp.ts
65
+ import path3 from "path";
65
66
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
66
67
  import { z } from "zod";
67
68
 
@@ -283,6 +284,15 @@ async function connectReviewSite(url, token) {
283
284
  }
284
285
  var MAX_FILE = 4.4 * 1024 * 1024;
285
286
  async function publishSnapshot(site, dir, manifest, opts = {}) {
287
+ if (opts.live) {
288
+ const { features = [] } = await request(site, "GET", "/api/shares");
289
+ const needed = "url" in opts.live ? "live-url" : "live-files";
290
+ if (!features.includes(needed))
291
+ throw new Error(
292
+ "url" in opts.live ? "This review site doesn't support live sites yet. Update it (packages/review-worker) to share a live URL." : "This review site doesn't host live sites. Deploy packages/review-worker with LIVE_HOST_SUFFIX set, or pass the URL where the site runs instead."
293
+ );
294
+ if ("dir" in opts.live && !fs2.statSync(opts.live.dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`No build folder at ${opts.live.dir}`);
295
+ }
286
296
  const share = await request(site, "POST", "/api/shares", { project: manifest.project, canvas: manifest.canvas, title: opts.title ?? manifest.canvas });
287
297
  const base = `/api/shares/${share.slug}`;
288
298
  const known = new Set((await request(site, "GET", `${base}/assets`)).assets);
@@ -305,10 +315,28 @@ async function publishSnapshot(site, dir, manifest, opts = {}) {
305
315
  };
306
316
  await Promise.all(Array.from({ length: 4 }, worker));
307
317
  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(", ")}`);
318
+ if (opts.live && "dir" in opts.live) {
319
+ const liveDir = opts.live.dir;
320
+ const liveFiles = walk(liveDir);
321
+ const pending = [...liveFiles];
322
+ const liveWorker = async () => {
323
+ for (let rel = pending.shift(); rel; rel = pending.shift()) {
324
+ const body = fs2.readFileSync(path2.join(liveDir, rel));
325
+ if (body.length > MAX_FILE) {
326
+ skipped.push(`live/${rel}`);
327
+ continue;
328
+ }
329
+ await request(site, "PUT", `${base}/live/files?version=${manifest.id}`, body, { "x-path": rel, "content-type": contentType(rel) });
330
+ uploaded++;
331
+ }
332
+ };
333
+ await Promise.all(Array.from({ length: 6 }, liveWorker));
334
+ }
308
335
  const change = { ...opts.password !== void 0 ? { password: opts.password } : {}, ...opts.title ? { title: opts.title } : {} };
309
336
  if (Object.keys(change).length) await request(site, "PATCH", base, change);
310
- const done = await request(site, "POST", `${base}/versions`, { id: manifest.id, createdAt: manifest.createdAt, frames: manifest.frames });
311
- return { url: done.url, version: manifest.id, versions: done.versions, uploaded, skipped, password: opts.password === void 0 ? share.password : !!opts.password };
337
+ const live = opts.live ? "url" in opts.live ? { url: opts.live.url } : { files: true } : void 0;
338
+ const done = await request(site, "POST", `${base}/versions`, { id: manifest.id, createdAt: manifest.createdAt, frames: manifest.frames, live });
339
+ return { url: done.url, version: manifest.id, versions: done.versions, uploaded, skipped, password: opts.password === void 0 ? share.password : !!opts.password, live: done.live ?? null };
312
340
  }
313
341
  async function deleteShare(site, project, canvas) {
314
342
  const found = await request(site, "GET", `/api/shares?project=${encodeURIComponent(project)}&canvas=${encodeURIComponent(canvas)}`);
@@ -339,12 +367,33 @@ async function request(site, method, route, body, headers = {}) {
339
367
  if (!res.ok) throw new Error(`Review site: ${data2.error ?? `${res.status} ${res.statusText}`}`);
340
368
  return data2;
341
369
  }
370
+ function walk(root, rel = "") {
371
+ const out = [];
372
+ for (const entry of fs2.readdirSync(path2.join(root, rel), { withFileTypes: true })) {
373
+ if (entry.name.startsWith(".")) continue;
374
+ const child = rel ? `${rel}/${entry.name}` : entry.name;
375
+ if (entry.isDirectory()) out.push(...walk(root, child));
376
+ else if (entry.isFile()) out.push(child);
377
+ }
378
+ return out;
379
+ }
342
380
  function contentType(file) {
343
381
  const ext = path2.extname(file).slice(1).toLowerCase();
344
382
  const types = {
345
383
  html: "text/html; charset=utf-8",
384
+ css: "text/css; charset=utf-8",
385
+ js: "text/javascript; charset=utf-8",
386
+ mjs: "text/javascript; charset=utf-8",
387
+ json: "application/json",
388
+ map: "application/json",
389
+ txt: "text/plain; charset=utf-8",
390
+ xml: "application/xml",
391
+ webmanifest: "application/manifest+json",
392
+ wasm: "application/wasm",
393
+ pdf: "application/pdf",
346
394
  png: "image/png",
347
395
  jpg: "image/jpeg",
396
+ jpeg: "image/jpeg",
348
397
  webp: "image/webp",
349
398
  avif: "image/avif",
350
399
  gif: "image/gif",
@@ -985,10 +1034,11 @@ ${msgs}`;
985
1034
  inputSchema: {
986
1035
  ...canvasArg,
987
1036
  frames: z.array(z.string()).optional().describe("Frame names to share; omit for all."),
988
- title: z.string().optional().describe('Title the client sees, e.g. "Homepage, round 2".')
1037
+ title: z.string().optional().describe('Title the client sees, e.g. "Homepage, round 2".'),
1038
+ 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.')
989
1039
  }
990
1040
  },
991
- async ({ canvas, frames, title }) => {
1041
+ async ({ canvas, frames, title, live }) => {
992
1042
  try {
993
1043
  const c2 = resolveCanvas(canvas);
994
1044
  presence(c2, "looking", "Preparing a share link");
@@ -1000,8 +1050,16 @@ ${msgs}`;
1000
1050
  ].filter(Boolean);
1001
1051
  const site = reviewSite();
1002
1052
  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"));
1003
- const published = await publishSnapshot(site, dir, manifest, { title });
1004
- return text([`\u2713 Shared ${manifest.frames.length} frame(s): ${published.url}`, `Version ${published.versions} of this link${published.password ? ", password protected" : ""}.`, ...notes].join("\n"));
1053
+ const liveSite = live ? /^https?:\/\//.test(live) ? { url: live } : { dir: path3.resolve(ws.config.root, live) } : void 0;
1054
+ const published = await publishSnapshot(site, dir, manifest, { title, live: liveSite });
1055
+ return text(
1056
+ [
1057
+ `\u2713 Shared ${manifest.frames.length} frame(s): ${published.url}`,
1058
+ `Version ${published.versions} of this link${published.password ? ", password protected" : ""}.`,
1059
+ published.live ? `Live version: ${published.live}` : "",
1060
+ ...notes
1061
+ ].filter(Boolean).join("\n")
1062
+ );
1005
1063
  } catch (e) {
1006
1064
  return fail(e);
1007
1065
  }
@@ -1179,12 +1237,12 @@ ${out.out.slice(-1500)}`));
1179
1237
  // src/server/thumbnails.ts
1180
1238
  import crypto2 from "crypto";
1181
1239
  import fs3 from "fs";
1182
- import path3 from "path";
1240
+ import path4 from "path";
1183
1241
  var Thumbnails = class _Thumbnails {
1184
1242
  constructor(ws, shots) {
1185
1243
  this.ws = ws;
1186
1244
  this.shots = shots;
1187
- this.dir = path3.join(ws.config.root, "node_modules", ".cache", "truecanvas", "thumbs");
1245
+ this.dir = path4.join(ws.config.root, "node_modules", ".cache", "truecanvas", "thumbs");
1188
1246
  }
1189
1247
  ws;
1190
1248
  shots;
@@ -1210,7 +1268,7 @@ var Thumbnails = class _Thumbnails {
1210
1268
  key(spec, props) {
1211
1269
  let stamp = "";
1212
1270
  try {
1213
- const file = spec.library ? path3.join(this.ws.config.root, "node_modules", spec.library, "package.json") : path3.join(this.ws.config.root, spec.file);
1271
+ const file = spec.library ? path4.join(this.ws.config.root, "node_modules", spec.library, "package.json") : path4.join(this.ws.config.root, spec.file);
1214
1272
  stamp = String(fs3.statSync(file).mtimeMs);
1215
1273
  } catch {
1216
1274
  stamp = "0";
@@ -1222,7 +1280,7 @@ var Thumbnails = class _Thumbnails {
1222
1280
  const spec = this.ws.catalog.get(name);
1223
1281
  if (!spec) throw new Error(`Unknown component ${name}`);
1224
1282
  const props = _Thumbnails.previewProps(spec, preset);
1225
- const file = path3.join(this.dir, `${name}-${this.key(spec, props)}.png`);
1283
+ const file = path4.join(this.dir, `${name}-${this.key(spec, props)}.png`);
1226
1284
  if (fs3.existsSync(file)) return fs3.readFileSync(file);
1227
1285
  const job = this.queue.then(async () => {
1228
1286
  if (fs3.existsSync(file)) return fs3.readFileSync(file);
@@ -1240,10 +1298,10 @@ var Thumbnails = class _Thumbnails {
1240
1298
  // src/server/screenshot.ts
1241
1299
  import fs4 from "fs";
1242
1300
  import os2 from "os";
1243
- import path4 from "path";
1301
+ import path5 from "path";
1244
1302
  function findChromium() {
1245
1303
  if (process.env.TRUECANVAS_CHROME) return process.env.TRUECANVAS_CHROME;
1246
- const cache2 = process.env.PLAYWRIGHT_BROWSERS_PATH || (process.platform === "win32" ? path4.join(process.env.LOCALAPPDATA ?? path4.join(os2.homedir(), "AppData", "Local"), "ms-playwright") : process.platform === "darwin" ? path4.join(os2.homedir(), "Library", "Caches", "ms-playwright") : path4.join(os2.homedir(), ".cache", "ms-playwright"));
1304
+ 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"));
1247
1305
  if (fs4.existsSync(cache2)) {
1248
1306
  const shells = fs4.readdirSync(cache2).filter((d) => d.startsWith("chromium_headless_shell-")).sort().reverse();
1249
1307
  for (const d of shells) {
@@ -1255,14 +1313,14 @@ function findChromium() {
1255
1313
  "chrome-headless-shell-win64/chrome-headless-shell.exe",
1256
1314
  "chrome-win/headless_shell.exe"
1257
1315
  ]) {
1258
- const p = path4.join(cache2, d, sub);
1316
+ const p = path5.join(cache2, d, sub);
1259
1317
  if (fs4.existsSync(p)) return p;
1260
1318
  }
1261
1319
  }
1262
1320
  }
1263
1321
  const programFiles = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(Boolean);
1264
1322
  for (const p of [
1265
- ...programFiles.flatMap((dir) => [path4.join(dir, "Google", "Chrome", "Application", "chrome.exe"), path4.join(dir, "Microsoft", "Edge", "Application", "msedge.exe")]),
1323
+ ...programFiles.flatMap((dir) => [path5.join(dir, "Google", "Chrome", "Application", "chrome.exe"), path5.join(dir, "Microsoft", "Edge", "Application", "msedge.exe")]),
1266
1324
  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
1267
1325
  "/usr/bin/chromium-browser",
1268
1326
  "/usr/bin/chromium",
@@ -1487,7 +1545,7 @@ ${root.outerHTML}`, css, canvases, height };
1487
1545
 
1488
1546
  // src/server/review.ts
1489
1547
  import fs5 from "fs";
1490
- import path5 from "path";
1548
+ import path6 from "path";
1491
1549
  import { createHash } from "crypto";
1492
1550
  var actorName = (e) => e.actor.kind === "agent" ? e.actor.name : e.actor.kind === "file" ? "code editor" : "you";
1493
1551
  async function versionAt(ws, canvas, ref) {
@@ -1504,7 +1562,7 @@ async function versionAt(ws, canvas, ref) {
1504
1562
  }
1505
1563
  var readNow = (ws, file) => {
1506
1564
  try {
1507
- return fs5.readFileSync(path5.join(ws.config.root, file), "utf8");
1565
+ return fs5.readFileSync(path6.join(ws.config.root, file), "utf8");
1508
1566
  } catch {
1509
1567
  return null;
1510
1568
  }
@@ -1650,7 +1708,7 @@ async function readJson(req, limit = 8 * 1024 * 1024) {
1650
1708
  }
1651
1709
 
1652
1710
  // src/share/sync.ts
1653
- import path6 from "path";
1711
+ import path7 from "path";
1654
1712
  var EVERY = 2e4;
1655
1713
  var LOOKUP_EVERY = 5 * 6e4;
1656
1714
  var CommentSync = class {
@@ -1700,7 +1758,7 @@ var CommentSync = class {
1700
1758
  async slugOf(site, canvas) {
1701
1759
  const known = this.links.get(canvas);
1702
1760
  if (known && (known.slug || Date.now() - known.at < LOOKUP_EVERY)) return known.slug;
1703
- const project = path6.basename(this.ws.config.root);
1761
+ const project = path7.basename(this.ws.config.root);
1704
1762
  const res = await call(site, "GET", `/api/shares?project=${encodeURIComponent(project)}&canvas=${encodeURIComponent(canvas)}`);
1705
1763
  this.links.set(canvas, { slug: res.slug, at: Date.now() });
1706
1764
  return res.slug;
@@ -1885,7 +1943,7 @@ async function startServer(ws) {
1885
1943
  appUrl: config.appUrl,
1886
1944
  framework: config.framework,
1887
1945
  root: config.root,
1888
- projectName: path7.basename(config.root),
1946
+ projectName: path8.basename(config.root),
1889
1947
  canvases: ws.canvases(),
1890
1948
  darkMode: config.darkMode,
1891
1949
  agents: [...ws.agents.values()],
@@ -2035,7 +2093,7 @@ async function startServer(ws) {
2035
2093
  for (const file of [f.page, ...layoutFiles(config, f.page)]) {
2036
2094
  const old = await ws.git.show(ref, file);
2037
2095
  if (old === null) continue;
2038
- const now = fs6.existsSync(path7.join(config.root, file)) ? fs6.readFileSync(path7.join(config.root, file), "utf8") : null;
2096
+ const now = fs6.existsSync(path8.join(config.root, file)) ? fs6.readFileSync(path8.join(config.root, file), "utf8") : null;
2039
2097
  if (old !== now) {
2040
2098
  files[file] = old;
2041
2099
  touched.add(f.frameName);
@@ -2165,9 +2223,9 @@ async function startServer(ws) {
2165
2223
  }
2166
2224
  case "POST /api/open-file": {
2167
2225
  const { file } = await readJson(req);
2168
- const abs = path7.resolve(config.root, file);
2169
- const rel = path7.relative(config.root, abs);
2170
- if (rel.startsWith("..") || path7.isAbsolute(rel)) return json(res, 400, { error: "Outside project." });
2226
+ const abs = path8.resolve(config.root, file);
2227
+ const rel = path8.relative(config.root, abs);
2228
+ if (rel.startsWith("..") || path8.isAbsolute(rel)) return json(res, 400, { error: "Outside project." });
2171
2229
  launchEditor(abs, config.editor);
2172
2230
  return json(res, 200, { ok: true });
2173
2231
  }
@@ -2176,12 +2234,12 @@ async function startServer(ws) {
2176
2234
  return json(res, 200, { site: site ? site.url : null });
2177
2235
  }
2178
2236
  case "POST /api/share/snapshot": {
2179
- const { canvas, frames, local, title, password } = await readJson(req);
2237
+ const { canvas, frames, local, title, password, live } = await readJson(req);
2180
2238
  assertCanvasName(canvas);
2181
2239
  const { dir, manifest } = await createSnapshot(ws, shots, canvas, { frames });
2182
2240
  const preview = `${origin}/share/${encodeURIComponent(canvas)}/${manifest.id}/`;
2183
2241
  const site = local ? null : reviewSite();
2184
- const published = site ? await publishSnapshot(site, dir, manifest, { title, password }) : null;
2242
+ const published = site ? await publishSnapshot(site, dir, manifest, { title, password, live }) : null;
2185
2243
  if (published) commentSync.forget(canvas);
2186
2244
  return json(res, 200, { manifest, preview, published });
2187
2245
  }
@@ -2195,23 +2253,23 @@ async function startServer(ws) {
2195
2253
  return json(res, 404, { error: `No route ${route}` });
2196
2254
  }
2197
2255
  function serveStatic(res, pathname) {
2198
- let file = path7.join(editorDir, pathname === "/" ? "index.html" : pathname);
2199
- if (!inside(editorDir, file) || !fs6.existsSync(file) || fs6.statSync(file).isDirectory()) file = path7.join(editorDir, "index.html");
2256
+ let file = path8.join(editorDir, pathname === "/" ? "index.html" : pathname);
2257
+ if (!inside(editorDir, file) || !fs6.existsSync(file) || fs6.statSync(file).isDirectory()) file = path8.join(editorDir, "index.html");
2200
2258
  if (!fs6.existsSync(file)) {
2201
2259
  res.writeHead(500, { "content-type": "text/plain" });
2202
2260
  return res.end("Editor bundle missing. Run `pnpm build` in packages/truecanvas.");
2203
2261
  }
2204
- const ext = path7.extname(file);
2262
+ const ext = path8.extname(file);
2205
2263
  const immutable = pathname.startsWith("/assets/");
2206
2264
  res.writeHead(200, { "content-type": MIME[ext] ?? "application/octet-stream", "cache-control": immutable ? "public, max-age=31536000, immutable" : "no-cache" });
2207
2265
  fs6.createReadStream(file).on("error", () => res.destroy()).pipe(res);
2208
2266
  }
2209
2267
  function serveShare(res, rest) {
2210
2268
  const root = sharesDir(config.root);
2211
- let file = path7.join(root, decodeURIComponent(rest));
2212
- if (rest.endsWith("/")) file = path7.join(file, "index.html");
2269
+ let file = path8.join(root, decodeURIComponent(rest));
2270
+ if (rest.endsWith("/")) file = path8.join(file, "index.html");
2213
2271
  if (!inside(root, file) || !fs6.existsSync(file) || fs6.statSync(file).isDirectory()) return json(res, 404, { error: "Not found" });
2214
- res.writeHead(200, { "content-type": MIME[path7.extname(file)] ?? "application/octet-stream", "cache-control": "no-cache", "x-content-type-options": "nosniff", "access-control-allow-origin": "*" });
2272
+ 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": "*" });
2215
2273
  fs6.createReadStream(file).on("error", () => res.destroy()).pipe(res);
2216
2274
  }
2217
2275
  const server = http.createServer(async (req, res) => {
@@ -2232,7 +2290,7 @@ async function startServer(ws) {
2232
2290
  else res.end();
2233
2291
  }
2234
2292
  });
2235
- const canvasDir = path7.join(config.root, config.canvasDir);
2293
+ const canvasDir = path8.join(config.root, config.canvasDir);
2236
2294
  fs6.mkdirSync(canvasDir, { recursive: true });
2237
2295
  const pending = /* @__PURE__ */ new Map();
2238
2296
  const onCanvasFile = (filename) => {
@@ -2276,12 +2334,12 @@ async function startServer(ws) {
2276
2334
  };
2277
2335
  watchCanvasDir();
2278
2336
  setInterval(watchCanvasDir, 2e3).unref();
2279
- const appDirAbs = path7.join(config.root, config.appDir);
2337
+ const appDirAbs = path8.join(config.root, config.appDir);
2280
2338
  if (config.framework === "next" && fs6.existsSync(appDirAbs)) {
2281
2339
  const appPending = /* @__PURE__ */ new Map();
2282
2340
  const appWatcher = fs6.watch(appDirAbs, { recursive: true }, (_event, filename) => {
2283
2341
  if (!filename || !/(^|[\\/])(page|layout)\.[jt]sx$/.test(filename) || /^truecanvas[\\/]/.test(filename)) return;
2284
- const rel = path7.join(config.appDir, filename).split(path7.sep).join("/");
2342
+ const rel = path8.join(config.appDir, filename).split(path8.sep).join("/");
2285
2343
  clearTimeout(appPending.get(rel));
2286
2344
  appPending.set(
2287
2345
  rel,
@@ -2298,7 +2356,7 @@ async function startServer(ws) {
2298
2356
  }
2299
2357
  let catalogTimer;
2300
2358
  for (const dir of componentRoots(config.components)) {
2301
- const abs = path7.join(config.root, dir);
2359
+ const abs = path8.join(config.root, dir);
2302
2360
  if (!fs6.existsSync(abs)) continue;
2303
2361
  const componentWatcher = fs6.watch(abs, { recursive: true }, () => {
2304
2362
  clearTimeout(catalogTimer);
@@ -2335,8 +2393,8 @@ function dedent(code, col) {
2335
2393
  return code.split("\n").map((l, i) => i > 0 && l.startsWith(pad) ? l.slice(col) : l).join("\n");
2336
2394
  }
2337
2395
  function inside(dir, file) {
2338
- const rel = path7.relative(dir, file);
2339
- return !!rel && !rel.startsWith("..") && !path7.isAbsolute(rel);
2396
+ const rel = path8.relative(dir, file);
2397
+ return !!rel && !rel.startsWith("..") && !path8.isAbsolute(rel);
2340
2398
  }
2341
2399
 
2342
2400
  // src/server/stdio.ts
@@ -2381,7 +2439,7 @@ import { createRequire } from "module";
2381
2439
  import http2 from "http";
2382
2440
  import fs10 from "fs";
2383
2441
  import os6 from "os";
2384
- import path11 from "path";
2442
+ import path12 from "path";
2385
2443
  import { randomUUID as randomUUID2 } from "crypto";
2386
2444
  import { fileURLToPath as fileURLToPath2 } from "url";
2387
2445
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
@@ -2393,9 +2451,9 @@ import { CallToolRequestSchema as CallToolRequestSchema2, ListToolsRequestSchema
2393
2451
  // src/hub/projects.ts
2394
2452
  import fs7 from "fs";
2395
2453
  import os3 from "os";
2396
- import path8 from "path";
2397
- var CONFIG_DIR = path8.join(process.env.XDG_CONFIG_HOME ?? path8.join(os3.homedir(), ".config"), "truecanvas");
2398
- var FILE = path8.join(CONFIG_DIR, "projects.json");
2454
+ import path9 from "path";
2455
+ var CONFIG_DIR = path9.join(process.env.XDG_CONFIG_HOME ?? path9.join(os3.homedir(), ".config"), "truecanvas");
2456
+ var FILE = path9.join(CONFIG_DIR, "projects.json");
2399
2457
  var ProjectStore = class {
2400
2458
  list = [];
2401
2459
  constructor() {
@@ -2413,20 +2471,20 @@ var ProjectStore = class {
2413
2471
  return [...this.list].sort((a, b) => b.lastOpened - a.lastOpened).map((p) => ({ ...p, ...inspect(p.path) }));
2414
2472
  }
2415
2473
  touch(dir) {
2416
- const abs = path8.resolve(dir);
2474
+ const abs = path9.resolve(dir);
2417
2475
  const existing = this.list.find((p) => p.path === abs);
2418
2476
  if (existing) existing.lastOpened = Date.now();
2419
2477
  else this.list.push({ path: abs, name: projectName(abs), lastOpened: Date.now() });
2420
2478
  this.save();
2421
2479
  }
2422
2480
  forget(dir) {
2423
- this.list = this.list.filter((p) => p.path !== path8.resolve(dir));
2481
+ this.list = this.list.filter((p) => p.path !== path9.resolve(dir));
2424
2482
  this.save();
2425
2483
  }
2426
2484
  };
2427
2485
  function readPkg(dir) {
2428
2486
  try {
2429
- return JSON.parse(fs7.readFileSync(path8.join(dir, "package.json"), "utf8"));
2487
+ return JSON.parse(fs7.readFileSync(path9.join(dir, "package.json"), "utf8"));
2430
2488
  } catch {
2431
2489
  return null;
2432
2490
  }
@@ -2434,7 +2492,7 @@ function readPkg(dir) {
2434
2492
  function projectName(dir) {
2435
2493
  const pkg = readPkg(dir);
2436
2494
  const name = typeof pkg?.name === "string" ? pkg.name : "";
2437
- return name && !name.startsWith("@") ? name : path8.basename(dir);
2495
+ return name && !name.startsWith("@") ? name : path9.basename(dir);
2438
2496
  }
2439
2497
  function inspect(dir) {
2440
2498
  const pkg = readPkg(dir);
@@ -2443,7 +2501,7 @@ function inspect(dir) {
2443
2501
  let canvases = 0;
2444
2502
  for (const d of ["canvas", "src/canvas"]) {
2445
2503
  try {
2446
- canvases += fs7.readdirSync(path8.join(dir, d)).filter((f) => f.endsWith(".canvas.tsx")).length;
2504
+ canvases += fs7.readdirSync(path9.join(dir, d)).filter((f) => f.endsWith(".canvas.tsx")).length;
2447
2505
  } catch {
2448
2506
  }
2449
2507
  }
@@ -2451,10 +2509,10 @@ function inspect(dir) {
2451
2509
  }
2452
2510
  function discover() {
2453
2511
  const home = os3.homedir();
2454
- const roots = ["Github", "GitHub", "github", "Projects", "projects", "code", "Code", "dev", "src", "Developer", "Documents", "work"].map((d) => path8.join(home, d)).filter((d) => fs7.existsSync(d));
2512
+ const roots = ["Github", "GitHub", "github", "Projects", "projects", "code", "Code", "dev", "src", "Developer", "Documents", "work"].map((d) => path9.join(home, d)).filter((d) => fs7.existsSync(d));
2455
2513
  const found = [];
2456
2514
  let visited = 0;
2457
- const walk = (dir, depth) => {
2515
+ const walk2 = (dir, depth) => {
2458
2516
  if (depth > 3 || visited > 3e3 || found.length > 60) return;
2459
2517
  visited++;
2460
2518
  let entries;
@@ -2469,15 +2527,15 @@ function discover() {
2469
2527
  }
2470
2528
  for (const e of entries) {
2471
2529
  if (!e.isDirectory() || e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist" || e.name === "build") continue;
2472
- walk(path8.join(dir, e.name), depth + 1);
2530
+ walk2(path9.join(dir, e.name), depth + 1);
2473
2531
  }
2474
2532
  };
2475
- for (const r of roots) walk(r, 0);
2533
+ for (const r of roots) walk2(r, 0);
2476
2534
  return found;
2477
2535
  }
2478
2536
  function listDirs(dir) {
2479
- const abs = path8.resolve(dir.replace(/^~(?=$|[\\/])/, os3.homedir()));
2480
- const dirs = fs7.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => ({ name: e.name, next: inspect(path8.join(abs, e.name)).next })).sort((a, b) => Number(b.next) - Number(a.next) || a.name.localeCompare(b.name));
2537
+ const abs = path9.resolve(dir.replace(/^~(?=$|[\\/])/, os3.homedir()));
2538
+ const dirs = fs7.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => ({ name: e.name, next: inspect(path9.join(abs, e.name)).next })).sort((a, b) => Number(b.next) - Number(a.next) || a.name.localeCompare(b.name));
2481
2539
  return { path: abs, dirs };
2482
2540
  }
2483
2541
 
@@ -2486,14 +2544,14 @@ import { execFile, spawn as spawn2 } from "child_process";
2486
2544
  import fs9 from "fs";
2487
2545
  import net from "net";
2488
2546
  import os5 from "os";
2489
- import path10 from "path";
2547
+ import path11 from "path";
2490
2548
 
2491
2549
  // src/cli/setup.ts
2492
2550
  import { spawn } from "child_process";
2493
2551
  import { createHash as createHash2 } from "crypto";
2494
2552
  import fs8 from "fs";
2495
2553
  import os4 from "os";
2496
- import path9 from "path";
2554
+ import path10 from "path";
2497
2555
  function addDevArgs(pm, spec) {
2498
2556
  return addArgs(pm, [spec], true);
2499
2557
  }
@@ -2507,52 +2565,52 @@ var execInherit = (cmd, args, cwd) => new Promise((resolve, reject) => {
2507
2565
  p.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} exited with ${code}`)));
2508
2566
  });
2509
2567
  function packageDir(cliFile) {
2510
- return path9.resolve(path9.dirname(cliFile), "..");
2568
+ return path10.resolve(path10.dirname(cliFile), "..");
2511
2569
  }
2512
2570
  function packageVersion(pkgDir) {
2513
2571
  try {
2514
- return JSON.parse(fs8.readFileSync(path9.join(pkgDir, "package.json"), "utf8")).version;
2572
+ return JSON.parse(fs8.readFileSync(path10.join(pkgDir, "package.json"), "utf8")).version;
2515
2573
  } catch {
2516
2574
  return "0.0.0";
2517
2575
  }
2518
2576
  }
2519
2577
  function isSourceCheckout(pkgDir) {
2520
- return !pkgDir.split(path9.sep).includes("node_modules") && fs8.existsSync(path9.join(pkgDir, "src", "cli"));
2578
+ return !pkgDir.split(path10.sep).includes("node_modules") && fs8.existsSync(path10.join(pkgDir, "src", "cli"));
2521
2579
  }
2522
2580
  async function packageSpec(pkgDir, exec) {
2523
2581
  if (process.env.TRUECANVAS_PACKAGE) return process.env.TRUECANVAS_PACKAGE;
2524
2582
  const version = packageVersion(pkgDir);
2525
2583
  if (!isSourceCheckout(pkgDir)) return `truecanvas@${version}`;
2526
- const cacheDir = path9.join(os4.homedir(), ".cache", "truecanvas");
2584
+ const cacheDir = path10.join(os4.homedir(), ".cache", "truecanvas");
2527
2585
  fs8.mkdirSync(cacheDir, { recursive: true });
2528
- const file = path9.join(cacheDir, `truecanvas-${version}.tgz`);
2586
+ const file = path10.join(cacheDir, `truecanvas-${version}.tgz`);
2529
2587
  await exec("npm", ["pack", "--silent", "--pack-destination", cacheDir], pkgDir);
2530
2588
  const hash = createHash2("sha256").update(fs8.readFileSync(file)).digest("hex").slice(0, 10);
2531
- const named = path9.join(cacheDir, `truecanvas-${version}-${hash}.tgz`);
2589
+ const named = path10.join(cacheDir, `truecanvas-${version}-${hash}.tgz`);
2532
2590
  fs8.renameSync(file, named);
2533
2591
  return named;
2534
2592
  }
2535
2593
  function installedIn(root) {
2536
2594
  let declared = false;
2537
2595
  try {
2538
- const pkg = JSON.parse(fs8.readFileSync(path9.join(root, "package.json"), "utf8"));
2596
+ const pkg = JSON.parse(fs8.readFileSync(path10.join(root, "package.json"), "utf8"));
2539
2597
  declared = "truecanvas" in { ...pkg.dependencies, ...pkg.devDependencies };
2540
2598
  } catch {
2541
2599
  }
2542
- const file = path9.join(root, "node_modules", "truecanvas", "package.json");
2600
+ const file = path10.join(root, "node_modules", "truecanvas", "package.json");
2543
2601
  const installed = fs8.existsSync(file);
2544
- return { declared, installed, version: installed ? packageVersion(path9.dirname(file)) : null };
2602
+ return { declared, installed, version: installed ? packageVersion(path10.dirname(file)) : null };
2545
2603
  }
2546
2604
  function readPackage(root) {
2547
2605
  try {
2548
- return JSON.parse(fs8.readFileSync(path9.join(root, "package.json"), "utf8"));
2606
+ return JSON.parse(fs8.readFileSync(path10.join(root, "package.json"), "utf8"));
2549
2607
  } catch {
2550
2608
  return null;
2551
2609
  }
2552
2610
  }
2553
2611
  function depVersion(root, name) {
2554
- const file = path9.join(root, "node_modules", name, "package.json");
2555
- if (fs8.existsSync(file)) return packageVersion(path9.dirname(file));
2612
+ const file = path10.join(root, "node_modules", name, "package.json");
2613
+ if (fs8.existsSync(file)) return packageVersion(path10.dirname(file));
2556
2614
  const pkg = readPackage(root);
2557
2615
  return pkg ? { ...pkg.dependencies, ...pkg.devDependencies }[name] ?? null : null;
2558
2616
  }
@@ -2568,7 +2626,7 @@ function mergeJson(file, update) {
2568
2626
  const before = JSON.stringify(data2);
2569
2627
  update(data2);
2570
2628
  if (JSON.stringify(data2) === before) return "unchanged";
2571
- fs8.mkdirSync(path9.dirname(file), { recursive: true });
2629
+ fs8.mkdirSync(path10.dirname(file), { recursive: true });
2572
2630
  fs8.writeFileSync(file, `${JSON.stringify(data2, null, 2)}
2573
2631
  `);
2574
2632
  return "written";
@@ -2576,16 +2634,16 @@ function mergeJson(file, update) {
2576
2634
  function connectAgents(root, port, opts = {}) {
2577
2635
  const url = `http://localhost:${port}/mcp`;
2578
2636
  const out = { written: [], unchanged: [] };
2579
- const add = (rel, update) => out[mergeJson(path9.join(root, rel), update)].push(rel);
2637
+ const add = (rel, update) => out[mergeJson(path10.join(root, rel), update)].push(rel);
2580
2638
  const servers = (d, key) => d[key] ??= {};
2581
2639
  add(".mcp.json", (d) => void (servers(d, "mcpServers").truecanvas = { type: "http", url }));
2582
- if (opts.cursor || fs8.existsSync(path9.join(root, ".cursor"))) add(".cursor/mcp.json", (d) => void (servers(d, "mcpServers").truecanvas = { url }));
2583
- if (opts.vscode || fs8.existsSync(path9.join(root, ".vscode"))) add(".vscode/mcp.json", (d) => void (servers(d, "servers").truecanvas = { type: "http", url }));
2640
+ if (opts.cursor || fs8.existsSync(path10.join(root, ".cursor"))) add(".cursor/mcp.json", (d) => void (servers(d, "mcpServers").truecanvas = { url }));
2641
+ if (opts.vscode || fs8.existsSync(path10.join(root, ".vscode"))) add(".vscode/mcp.json", (d) => void (servers(d, "servers").truecanvas = { type: "http", url }));
2584
2642
  return out;
2585
2643
  }
2586
2644
  function hasAgentConfig(root, port) {
2587
2645
  try {
2588
- const d = JSON.parse(fs8.readFileSync(path9.join(root, ".mcp.json"), "utf8"));
2646
+ const d = JSON.parse(fs8.readFileSync(path10.join(root, ".mcp.json"), "utf8"));
2589
2647
  const url = d?.mcpServers?.truecanvas?.url;
2590
2648
  return typeof url === "string" && (port === void 0 || url.includes(`:${port}/`));
2591
2649
  } catch {
@@ -2638,7 +2696,7 @@ var Runner = class {
2638
2696
  return next;
2639
2697
  }
2640
2698
  async start(dir) {
2641
- if (!fs9.existsSync(path10.join(dir, "package.json"))) throw new Error(`${dir} isn't there anymore (or has no package.json).`);
2699
+ if (!fs9.existsSync(path11.join(dir, "package.json"))) throw new Error(`${dir} isn't there anymore (or has no package.json).`);
2642
2700
  const [editorPort, appPort] = await this.allocatePorts();
2643
2701
  const proc = spawn2(process.execPath, [this.cli, "dev", "--no-open", "--port", String(editorPort), "--app", `http://localhost:${appPort}`], {
2644
2702
  cwd: dir,
@@ -2750,7 +2808,7 @@ async function install(job, cmd, args, cwd) {
2750
2808
  await run2(job, cmd, args, cwd);
2751
2809
  } catch (err) {
2752
2810
  const ignoredBuilds = job.log.some((l) => l.includes("ERR_PNPM_IGNORED_BUILDS"));
2753
- const installed = fs9.existsSync(path10.join(cwd, "node_modules", "truecanvas", "package.json"));
2811
+ const installed = fs9.existsSync(path11.join(cwd, "node_modules", "truecanvas", "package.json"));
2754
2812
  if (!(ignoredBuilds && installed)) throw err;
2755
2813
  job.log.push("(pnpm only complained about other packages' build scripts; Truecanvas is installed, continuing)");
2756
2814
  }
@@ -2791,7 +2849,7 @@ async function preferredPm() {
2791
2849
  return ok ? "pnpm" : "npm";
2792
2850
  }
2793
2851
  function setupJob(dir, pkgDir, cli) {
2794
- return startJob(`Set up ${path10.basename(dir)}`, async (job) => {
2852
+ return startJob(`Set up ${path11.basename(dir)}`, async (job) => {
2795
2853
  const pm = packageManager(dir);
2796
2854
  const source = await packageSpec(pkgDir, (cmd, args, cwd) => run2(job, cmd, args, cwd));
2797
2855
  await install(job, pm.cmd, [...pm.add, source], dir);
@@ -2801,8 +2859,8 @@ function setupJob(dir, pkgDir, cli) {
2801
2859
  }
2802
2860
  function createJob(name, parent, pkgDir, cli) {
2803
2861
  const safe = name.trim().toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "");
2804
- const parentDir = path10.resolve(parent.replace(/^~(?=$|[\\/])/, os5.homedir()));
2805
- const dir = path10.join(parentDir, safe);
2862
+ const parentDir = path11.resolve(parent.replace(/^~(?=$|[\\/])/, os5.homedir()));
2863
+ const dir = path11.join(parentDir, safe);
2806
2864
  return startJob(`Create ${safe}`, async (job) => {
2807
2865
  if (!safe) throw new Error("Pick a project name.");
2808
2866
  if (fs9.existsSync(dir)) throw new Error(`${dir} already exists.`);
@@ -2827,9 +2885,9 @@ function capture(cmd, args, cwd) {
2827
2885
  });
2828
2886
  }
2829
2887
  function cloneJob(repo, parent, pkgDir, cli) {
2830
- const parentDir = path10.resolve(parent.replace(/^~(?=$|[\\/])/, os5.homedir()));
2888
+ const parentDir = path11.resolve(parent.replace(/^~(?=$|[\\/])/, os5.homedir()));
2831
2889
  const name = repo.replace(/\.git$/, "").split(/[/:]/).pop() ?? "";
2832
- const dir = path10.join(parentDir, name);
2890
+ const dir = path11.join(parentDir, name);
2833
2891
  return startJob(`Clone ${name}`, async (job) => {
2834
2892
  if (!name || !/^[\w.-]+$/.test(name)) throw new Error("Pick a repository (owner/name or a git URL).");
2835
2893
  if (fs9.existsSync(dir)) throw new Error(`${dir} already exists. Open it with Open folder instead.`);
@@ -2837,12 +2895,12 @@ function cloneJob(repo, parent, pkgDir, cli) {
2837
2895
  const url = /^(https?:|git@|ssh:|file:)/.test(repo);
2838
2896
  if (url) await run2(job, "git", ["clone", repo, dir], parentDir);
2839
2897
  else await run2(job, "gh", ["repo", "clone", repo, dir], parentDir);
2840
- if (!fs9.existsSync(path10.join(dir, "package.json"))) throw new Error("Cloned, but there's no package.json at the root: Truecanvas needs a Next.js or Vite app.");
2898
+ if (!fs9.existsSync(path11.join(dir, "package.json"))) throw new Error("Cloned, but there's no package.json at the root: Truecanvas needs a Next.js or Vite app.");
2841
2899
  const pm = packageManager(dir);
2842
2900
  await install(job, pm.cmd, ["install"], dir).catch(async (err) => {
2843
- if (!fs9.existsSync(path10.join(dir, "node_modules"))) throw err;
2901
+ if (!fs9.existsSync(path11.join(dir, "node_modules"))) throw err;
2844
2902
  });
2845
- const pkg = JSON.parse(fs9.readFileSync(path10.join(dir, "package.json"), "utf8"));
2903
+ const pkg = JSON.parse(fs9.readFileSync(path11.join(dir, "package.json"), "utf8"));
2846
2904
  if (!("truecanvas" in { ...pkg.dependencies, ...pkg.devDependencies })) {
2847
2905
  const source = await packageSpec(pkgDir, (cmd, args, cwd) => run2(job, cmd, args, cwd));
2848
2906
  await install(job, pm.cmd, [...pm.add, source], dir);
@@ -2867,7 +2925,7 @@ Use list_projects to see them. All other tools are the regular Truecanvas canvas
2867
2925
  async function startHub(opts) {
2868
2926
  const store = new ProjectStore();
2869
2927
  const editorDir = fileURLToPath2(new URL("./editor/", import.meta.url));
2870
- const pkgDir = path11.resolve(path11.dirname(opts.cli), "..");
2928
+ const pkgDir = path12.resolve(path12.dirname(opts.cli), "..");
2871
2929
  const origin = `http://localhost:${opts.port}`;
2872
2930
  const allowedHosts = /* @__PURE__ */ new Set([`localhost:${opts.port}`, `127.0.0.1:${opts.port}`]);
2873
2931
  const clients = /* @__PURE__ */ new Set();
@@ -2972,7 +3030,7 @@ async function startHub(opts) {
2972
3030
  }
2973
3031
  if (req.params.name === "open_project") {
2974
3032
  const q = String(req.params.arguments?.project ?? "").toLowerCase();
2975
- const p = store.all().find((x) => x.name.toLowerCase() === q || x.path.toLowerCase() === q || path11.basename(x.path).toLowerCase() === q);
3033
+ const p = store.all().find((x) => x.name.toLowerCase() === q || x.path.toLowerCase() === q || path12.basename(x.path).toLowerCase() === q);
2976
3034
  if (!p) return text(`No project "${q}". Use list_projects.`, true);
2977
3035
  if (!p.ready) return text(`${p.name} isn't set up for Truecanvas yet. Ask the user to click "Set up" on the Dashboard.`, true);
2978
3036
  store.touch(p.path);
@@ -3027,7 +3085,7 @@ async function startHub(opts) {
3027
3085
  case "GET /api/hub/icon": {
3028
3086
  const icon = findIcon(url.searchParams.get("path") ?? "");
3029
3087
  if (!icon) return json2(res, 404, { error: "No icon" });
3030
- res.writeHead(200, { "content-type": ICON_MIME[path11.extname(icon)] ?? "application/octet-stream", "cache-control": "private, max-age=300" });
3088
+ res.writeHead(200, { "content-type": ICON_MIME[path12.extname(icon)] ?? "application/octet-stream", "cache-control": "private, max-age=300" });
3031
3089
  return fs10.createReadStream(icon).on("error", () => res.destroy()).pipe(res);
3032
3090
  }
3033
3091
  case "GET /api/hub/ls":
@@ -3039,8 +3097,8 @@ async function startHub(opts) {
3039
3097
  req.on("close", () => clients.delete(res));
3040
3098
  return;
3041
3099
  case "POST /api/hub/add": {
3042
- const dir = path11.resolve(body.path.replace(/^~(?=$|[\\/])/, os6.homedir()));
3043
- if (!fs10.existsSync(path11.join(dir, "package.json"))) return json2(res, 400, { error: "No package.json in that folder. Pick the root of a Next.js or Vite app." });
3100
+ const dir = path12.resolve(body.path.replace(/^~(?=$|[\\/])/, os6.homedir()));
3101
+ if (!fs10.existsSync(path12.join(dir, "package.json"))) return json2(res, 400, { error: "No package.json in that folder. Pick the root of a Next.js or Vite app." });
3044
3102
  store.touch(dir);
3045
3103
  broadcast();
3046
3104
  return json2(res, 200, { path: dir, ready: store.all().find((p) => p.path === dir)?.ready ?? false });
@@ -3124,13 +3182,13 @@ async function startHub(opts) {
3124
3182
  return json2(res, 404, { error: `No route ${route}` });
3125
3183
  }
3126
3184
  function serveStatic(res, pathname) {
3127
- let file = path11.join(editorDir, pathname === "/" ? "hub.html" : pathname);
3128
- if (!inside2(editorDir, file) || !fs10.existsSync(file) || fs10.statSync(file).isDirectory()) file = path11.join(editorDir, "hub.html");
3185
+ let file = path12.join(editorDir, pathname === "/" ? "hub.html" : pathname);
3186
+ if (!inside2(editorDir, file) || !fs10.existsSync(file) || fs10.statSync(file).isDirectory()) file = path12.join(editorDir, "hub.html");
3129
3187
  if (!fs10.existsSync(file)) {
3130
3188
  res.writeHead(500, { "content-type": "text/plain" });
3131
3189
  return res.end("Hub bundle missing. Run `pnpm build`.");
3132
3190
  }
3133
- res.writeHead(200, { "content-type": MIME2[path11.extname(file)] ?? "application/octet-stream", "cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache" });
3191
+ res.writeHead(200, { "content-type": MIME2[path12.extname(file)] ?? "application/octet-stream", "cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache" });
3134
3192
  fs10.createReadStream(file).on("error", () => res.destroy()).pipe(res);
3135
3193
  }
3136
3194
  const server = http2.createServer(async (req, res) => {
@@ -3163,26 +3221,26 @@ async function startHub(opts) {
3163
3221
  }
3164
3222
  var ICON_MIME = { ".ico": "image/x-icon", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
3165
3223
  function findIcon(dir) {
3166
- if (!dir || !fs10.existsSync(path11.join(dir, "package.json"))) return null;
3224
+ if (!dir || !fs10.existsSync(path12.join(dir, "package.json"))) return null;
3167
3225
  const names = ["icon.svg", "icon.png", "favicon.svg", "favicon.ico", "icon.ico", "apple-icon.png", "favicon.png", "logo.svg", "icon.jpg"];
3168
3226
  const roots = ["app", "src/app", "public", "static", "assets"];
3169
3227
  for (const root of roots) {
3170
3228
  for (const name of names) {
3171
- const file = path11.join(dir, root, name);
3229
+ const file = path12.join(dir, root, name);
3172
3230
  if (fs10.existsSync(file) && fs10.statSync(file).size < 2e6) return file;
3173
3231
  }
3174
3232
  }
3175
3233
  for (const root of ["app", "src/app"]) {
3176
3234
  let entries = [];
3177
3235
  try {
3178
- entries = fs10.readdirSync(path11.join(dir, root), { withFileTypes: true });
3236
+ entries = fs10.readdirSync(path12.join(dir, root), { withFileTypes: true });
3179
3237
  } catch {
3180
3238
  continue;
3181
3239
  }
3182
3240
  for (const e of entries) {
3183
3241
  if (!e.isDirectory()) continue;
3184
3242
  for (const name of names.slice(0, 4)) {
3185
- const file = path11.join(dir, root, e.name, name);
3243
+ const file = path12.join(dir, root, e.name, name);
3186
3244
  if (fs10.existsSync(file)) return file;
3187
3245
  }
3188
3246
  }
@@ -3194,14 +3252,14 @@ function json2(res, status, body) {
3194
3252
  res.end(JSON.stringify(body));
3195
3253
  }
3196
3254
  function inside2(dir, file) {
3197
- const rel = path11.relative(dir, file);
3198
- return !!rel && !rel.startsWith("..") && !path11.isAbsolute(rel);
3255
+ const rel = path12.relative(dir, file);
3256
+ return !!rel && !rel.startsWith("..") && !path12.isAbsolute(rel);
3199
3257
  }
3200
3258
 
3201
3259
  // src/hub/desktop.ts
3202
3260
  import fs11 from "fs";
3203
3261
  import os7 from "os";
3204
- import path12 from "path";
3262
+ import path13 from "path";
3205
3263
  var ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
3206
3264
  <defs>
3207
3265
  <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff9466"/><stop offset="1" stop-color="#ea6a3c"/></linearGradient>
@@ -3215,13 +3273,13 @@ var ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
3215
3273
  <path fill="#f6f5f1" fill-rule="evenodd" d="M236 236h552v168H236z M428 316h168v472H428z"/>
3216
3274
  </svg>
3217
3275
  `;
3218
- var data = () => process.env.XDG_DATA_HOME ?? path12.join(os7.homedir(), ".local", "share");
3276
+ var data = () => process.env.XDG_DATA_HOME ?? path13.join(os7.homedir(), ".local", "share");
3219
3277
  function installDesktop(cli, port) {
3220
- const dir = path12.join(data(), "truecanvas");
3278
+ const dir = path13.join(data(), "truecanvas");
3221
3279
  fs11.mkdirSync(dir, { recursive: true });
3222
- const icon = path12.join(dir, "icon.svg");
3280
+ const icon = path13.join(dir, "icon.svg");
3223
3281
  fs11.writeFileSync(icon, ICON);
3224
- const launcher = path12.join(dir, "launch.sh");
3282
+ const launcher = path13.join(dir, "launch.sh");
3225
3283
  const url = `http://localhost:${port}`;
3226
3284
  fs11.writeFileSync(
3227
3285
  launcher,
@@ -3232,7 +3290,7 @@ CLI=${JSON.stringify(cli)}
3232
3290
  URL=${JSON.stringify(url)}
3233
3291
  LOG="\${XDG_STATE_HOME:-$HOME/.local/state}/truecanvas-hub.log"
3234
3292
  # apps started from the desktop don't get your shell's PATH: next, pnpm and npx need it
3235
- export PATH=${JSON.stringify([path12.dirname(process.execPath), ...(process.env.PATH ?? "").split(path12.delimiter)].filter((p, i, a) => p && a.indexOf(p) === i).join(path12.delimiter))}
3293
+ export PATH=${JSON.stringify([path13.dirname(process.execPath), ...(process.env.PATH ?? "").split(path13.delimiter)].filter((p, i, a) => p && a.indexOf(p) === i).join(path13.delimiter))}
3236
3294
  mkdir -p "$(dirname "$LOG")"
3237
3295
 
3238
3296
  if ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; then
@@ -3241,7 +3299,7 @@ if ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; then
3241
3299
  while [ $i -lt 60 ] && ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; do sleep 0.25; i=$((i+1)); done
3242
3300
  fi
3243
3301
 
3244
- PROFILE=${JSON.stringify(path12.join(dir, "window"))}
3302
+ PROFILE=${JSON.stringify(path13.join(dir, "window"))}
3245
3303
  for b in google-chrome google-chrome-stable chromium-browser chromium brave-browser microsoft-edge; do
3246
3304
  if command -v "$b" >/dev/null 2>&1; then
3247
3305
  exec "$b" --app="$URL" --class=Truecanvas --user-data-dir="$PROFILE" --no-first-run --no-default-browser-check
@@ -3251,9 +3309,9 @@ exec xdg-open "$URL"
3251
3309
  `,
3252
3310
  { mode: 493 }
3253
3311
  );
3254
- const apps = path12.join(data(), "applications");
3312
+ const apps = path13.join(data(), "applications");
3255
3313
  fs11.mkdirSync(apps, { recursive: true });
3256
- const desktop = path12.join(apps, "truecanvas.desktop");
3314
+ const desktop = path13.join(apps, "truecanvas.desktop");
3257
3315
  fs11.writeFileSync(
3258
3316
  desktop,
3259
3317
  `[Desktop Entry]
@@ -3270,10 +3328,10 @@ StartupWMClass=Truecanvas
3270
3328
  StartupNotify=true
3271
3329
  `
3272
3330
  );
3273
- let command = path12.join(os7.homedir(), ".local", "bin", "truecanvas");
3331
+ let command = path13.join(os7.homedir(), ".local", "bin", "truecanvas");
3274
3332
  const ours = !fs11.existsSync(command) || fs11.readFileSync(command, "utf8").includes("# truecanvas wrapper");
3275
3333
  if (ours) {
3276
- fs11.mkdirSync(path12.dirname(command), { recursive: true });
3334
+ fs11.mkdirSync(path13.dirname(command), { recursive: true });
3277
3335
  fs11.writeFileSync(command, `#!/bin/sh
3278
3336
  # truecanvas wrapper (installed by \`truecanvas desktop\`)
3279
3337
  exec ${JSON.stringify(process.execPath)} ${JSON.stringify(cli)} "$@"
@@ -3282,17 +3340,17 @@ exec ${JSON.stringify(process.execPath)} ${JSON.stringify(cli)} "$@"
3282
3340
  return { desktop, launcher, icon, command };
3283
3341
  }
3284
3342
  function launcherPath() {
3285
- const p = path12.join(data(), "truecanvas", "launch.sh");
3343
+ const p = path13.join(data(), "truecanvas", "launch.sh");
3286
3344
  return fs11.existsSync(p) ? p : null;
3287
3345
  }
3288
3346
  function uninstallDesktop() {
3289
3347
  const removed = [];
3290
- const cmd = path12.join(os7.homedir(), ".local", "bin", "truecanvas");
3348
+ const cmd = path13.join(os7.homedir(), ".local", "bin", "truecanvas");
3291
3349
  if (fs11.existsSync(cmd) && fs11.readFileSync(cmd, "utf8").includes("# truecanvas wrapper")) {
3292
3350
  fs11.rmSync(cmd);
3293
3351
  removed.push(cmd);
3294
3352
  }
3295
- for (const f of [path12.join(data(), "applications", "truecanvas.desktop"), path12.join(data(), "truecanvas")]) {
3353
+ for (const f of [path13.join(data(), "applications", "truecanvas.desktop"), path13.join(data(), "truecanvas")]) {
3296
3354
  if (fs11.existsSync(f)) {
3297
3355
  fs11.rmSync(f, { recursive: true, force: true });
3298
3356
  removed.push(f);
@@ -3308,7 +3366,7 @@ import { createInterface } from "readline/promises";
3308
3366
  // src/cli/doctor.ts
3309
3367
  import { execFile as execFile2 } from "child_process";
3310
3368
  import fs12 from "fs";
3311
- import path13 from "path";
3369
+ import path14 from "path";
3312
3370
  var major = (v) => v ? Number(/(\d+)(?:\.(\d+))?/.exec(v)?.[1] ?? 0) : 0;
3313
3371
  function has(cmd, args, cwd) {
3314
3372
  return new Promise((resolve) => execFile2(cmd, args, { cwd, timeout: 8e3 }, (err, stdout, stderr) => resolve(err ? null : `${stdout}${stderr}`)));
@@ -3335,7 +3393,7 @@ async function doctor(root, cliVersion) {
3335
3393
  const tw = depVersion(root, "tailwindcss");
3336
3394
  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" });
3337
3395
  const config = loadConfig(root);
3338
- if (config.framework === "next") add(fs12.existsSync(path13.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/)" });
3396
+ if (config.framework === "next") add(fs12.existsSync(path14.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/)" });
3339
3397
  const tc = installedIn(root);
3340
3398
  if (!tc.declared) add({ label: "Truecanvas in the project", status: "fail", detail: "not a dependency", fix: "npx truecanvas (sets it up)" });
3341
3399
  else if (!tc.installed) add({ label: "Truecanvas in the project", status: "fail", detail: "declared but not installed", fix: `${pm} install` });
@@ -3344,15 +3402,15 @@ async function doctor(root, cliVersion) {
3344
3402
  const wrapped = configStatus(root, config.framework);
3345
3403
  const configLabel = config.framework === "vite" ? "vite.config" : "next.config";
3346
3404
  const uses = config.framework === "vite" ? "has the truecanvas() plugin" : "uses withTruecanvas()";
3347
- add(wrapped.wrapped ? { label: configLabel, status: "ok", detail: `${path13.basename(wrapped.file)} ${uses}` } : { label: configLabel, status: "fail", detail: wrapped.file ? "Truecanvas not enabled" : "missing", fix: "npx truecanvas init" });
3405
+ add(wrapped.wrapped ? { label: configLabel, status: "ok", detail: `${path14.basename(wrapped.file)} ${uses}` } : { label: configLabel, status: "fail", detail: wrapped.file ? "Truecanvas not enabled" : "missing", fix: "npx truecanvas init" });
3348
3406
  const scripts = pkg.scripts ?? {};
3349
3407
  const script = Object.entries(scripts).find(([, v]) => v.includes("truecanvas"));
3350
3408
  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")' });
3351
- const ignore = fs12.existsSync(path13.join(root, ".gitignore")) ? fs12.readFileSync(path13.join(root, ".gitignore"), "utf8") : "";
3409
+ const ignore = fs12.existsSync(path14.join(root, ".gitignore")) ? fs12.readFileSync(path14.join(root, ".gitignore"), "utf8") : "";
3352
3410
  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" });
3353
3411
  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)" });
3354
3412
  const chrome = findChromium();
3355
- add(chrome ? { label: "Screenshots", status: "ok", detail: path13.basename(chrome) } : { label: "Screenshots", status: "warn", detail: "no Chromium found", fix: "npx playwright install chromium-headless-shell (or install Chrome)" });
3413
+ add(chrome ? { label: "Screenshots", status: "ok", detail: path14.basename(chrome) } : { label: "Screenshots", status: "warn", detail: "no Chromium found", fix: "npx playwright install chromium-headless-shell (or install Chrome)" });
3356
3414
  const git = await has("git", ["rev-parse", "--is-inside-work-tree"], root);
3357
3415
  add(git ? { label: "Git", status: "ok" } : { label: "Git", status: "warn", detail: "not a repository", fix: "git init (branches, history, comments and compare use git)" });
3358
3416
  const gh = await has("gh", ["auth", "status"], root);
@@ -3378,7 +3436,8 @@ Usage
3378
3436
  truecanvas quit Quit the window and every app it started
3379
3437
  truecanvas desktop Add Truecanvas to your app launcher (Linux); "desktop remove" undoes it
3380
3438
  truecanvas share [canvas] Share a canvas as a link for clients (needs truecanvas running).
3381
- --password <p>, --no-password, --revoke, --restore, --delete, --title <t>, --local (preview only)
3439
+ --password <p>, --no-password, --revoke, --restore, --delete, --title <t>, --local (preview only),
3440
+ --live <url|dir>: a live version clients open with "View live" (where the app runs, or a static build)
3382
3441
  truecanvas share setup Connect your studio's review site (URL + REVIEW_TOKEN)
3383
3442
  truecanvas mcp stdio MCP server for agents without HTTP support
3384
3443
 
@@ -3423,7 +3482,8 @@ async function main() {
3423
3482
  revoke: { type: "boolean" },
3424
3483
  restore: { type: "boolean" },
3425
3484
  delete: { type: "boolean" },
3426
- token: { type: "string" }
3485
+ token: { type: "string" },
3486
+ live: { type: "string" }
3427
3487
  }
3428
3488
  });
3429
3489
  const cli = fileURLToPath3(import.meta.url);
@@ -3444,7 +3504,7 @@ async function main() {
3444
3504
  }
3445
3505
  if (!await ensureSetup(root2, pkgDir, version, yes)) return;
3446
3506
  if (await openInWindow(root2, Number(values.port ?? 4800))) return;
3447
- const local = path14.join(root2, "node_modules", "truecanvas", "dist", "cli.js");
3507
+ const local = path15.join(root2, "node_modules", "truecanvas", "dist", "cli.js");
3448
3508
  if (fs13.existsSync(local) && fs13.realpathSync(local) !== fs13.realpathSync(cli)) {
3449
3509
  const child = spawn3(process.execPath, [local, "dev", ...process.argv.slice(2).filter((a) => a !== "start" && a !== "-y" && a !== "--yes")], { stdio: "inherit" });
3450
3510
  for (const sig of ["SIGINT", "SIGTERM"]) process.on(sig, () => child.kill(sig));
@@ -3527,7 +3587,7 @@ async function main() {
3527
3587
  return;
3528
3588
  }
3529
3589
  const root = process.cwd();
3530
- if (!fs13.existsSync(path14.join(root, "package.json"))) {
3590
+ if (!fs13.existsSync(path15.join(root, "package.json"))) {
3531
3591
  console.error("Run truecanvas from your app's root (no package.json here).");
3532
3592
  process.exit(1);
3533
3593
  }
@@ -3537,7 +3597,7 @@ async function main() {
3537
3597
  const report = initProject(config);
3538
3598
  ws.ensureRoute();
3539
3599
  console.log(`
3540
- ${c.accent("\u25C6")} ${c.bold("Truecanvas")} is set up in ${c.bold(path14.basename(root))}
3600
+ ${c.accent("\u25C6")} ${c.bold("Truecanvas")} is set up in ${c.bold(path15.basename(root))}
3541
3601
  `);
3542
3602
  for (const line of report.done) console.log(` ${c.green("\u2713")} ${line}`);
3543
3603
  console.log(` ${c.green("\u2713")} Canvas folder ${c.bold(config.canvasDir)} and dev route ${c.bold(config.routeDir)}`);
@@ -3569,7 +3629,7 @@ async function main() {
3569
3629
  console.error(site ? "Name the canvas: truecanvas share <canvas> --delete" : `No review site yet: ${c.accent("npx truecanvas share setup")}`);
3570
3630
  process.exit(1);
3571
3631
  }
3572
- const done = await deleteShare(site, path14.basename(root), canvas);
3632
+ const done = await deleteShare(site, path15.basename(root), canvas);
3573
3633
  console.log(done ? ` ${c.green("\u2713")} Deleted the link of ${canvas}, with its versions and comments.` : ` ${canvas} has no link.`);
3574
3634
  return;
3575
3635
  }
@@ -3580,7 +3640,7 @@ async function main() {
3580
3640
  process.exit(1);
3581
3641
  }
3582
3642
  const change = { ...values.revoke ? { revoked: true } : values.restore ? { revoked: false } : {}, ...values["no-password"] ? { password: null } : {} };
3583
- const url = await updateShare(site, path14.basename(root), canvas, change);
3643
+ const url = await updateShare(site, path15.basename(root), canvas, change);
3584
3644
  console.log(` ${c.green("\u2713")} ${values.revoke ? "Link revoked" : values.restore ? "Link restored" : "Password removed"}: ${url}`);
3585
3645
  return;
3586
3646
  }
@@ -3596,7 +3656,16 @@ async function main() {
3596
3656
  const publish = !values.local && !!reviewSite();
3597
3657
  console.log(` ${c.dim(`Rendering ${canvas} through your app${publish ? " and publishing it" : ""}\u2026`)}`);
3598
3658
  const password = values.password;
3599
- const res = await fetch(`${base}/api/share/snapshot`, { method: "POST", headers: { "content-type": "application/json", origin: base }, body: JSON.stringify({ canvas, local: !publish, title: values.title, password }) });
3659
+ const live = values.live ? /^https?:\/\//.test(values.live) ? { url: values.live } : { dir: path15.resolve(values.live) } : void 0;
3660
+ if (live && "dir" in live && !fs13.existsSync(path15.join(live.dir, "index.html"))) {
3661
+ 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.`);
3662
+ process.exit(1);
3663
+ }
3664
+ if (live && !publish) {
3665
+ console.error(` ${c.red("\u2717")} --live needs a review site: ${c.accent("npx truecanvas share setup")}`);
3666
+ process.exit(1);
3667
+ }
3668
+ const res = await fetch(`${base}/api/share/snapshot`, { method: "POST", headers: { "content-type": "application/json", origin: base }, body: JSON.stringify({ canvas, local: !publish, title: values.title, password, live }) });
3600
3669
  const body = await res.json();
3601
3670
  if (!res.ok) {
3602
3671
  console.error(` ${c.red("\u2717")} ${body.error}`);
@@ -3608,8 +3677,9 @@ async function main() {
3608
3677
  if (manifest.missing.length) console.log(` ${c.yellow("!")} ${manifest.missing.length} asset${manifest.missing.length === 1 ? "" : "s"} couldn't be fetched.`);
3609
3678
  if (published?.skipped.length) console.log(` ${c.yellow("!")} Left out (over 4.4 MB): ${published.skipped.join(", ")}`);
3610
3679
  if (published) {
3611
- console.log(` ${c.green("\u2713")} Shared: ${c.bold(published.url)} ${c.dim(`(version ${published.versions}${published.password ? ", password protected" : ""})`)}
3612
- `);
3680
+ console.log(` ${c.green("\u2713")} Shared: ${c.bold(published.url)} ${c.dim(`(version ${published.versions}${published.password ? ", password protected" : ""})`)}`);
3681
+ if (published.live) console.log(` ${c.green("\u2713")} Live: ${published.live} ${c.dim(published.password ? "(opens from the link's View live button)" : "")}`);
3682
+ console.log("");
3613
3683
  if (values.open && !values["no-open"]) openBrowser(published.url);
3614
3684
  } else {
3615
3685
  console.log(` ${c.dim("Preview")} ${c.bold(body.preview)}`);
@@ -3721,7 +3791,7 @@ async function openInWindow(root, port) {
3721
3791
  const post = (route, body) => fetch(`${hub}${route}`, { method: "POST", headers: { "content-type": "application/json", origin: hub }, body: JSON.stringify(body) }).catch(() => null);
3722
3792
  await post("/api/hub/add", { path: root });
3723
3793
  await post("/api/hub/open", { path: root, focus: "1" });
3724
- console.log(`${c.accent("\u25C6")} Opened ${c.bold(path14.basename(root))} in the Truecanvas window ${c.dim(`(${hub})`)}.`);
3794
+ console.log(`${c.accent("\u25C6")} Opened ${c.bold(path15.basename(root))} in the Truecanvas window ${c.dim(`(${hub})`)}.`);
3725
3795
  return true;
3726
3796
  }
3727
3797
  async function prompt(question) {
@@ -3745,7 +3815,7 @@ async function ensureSetup(root, pkgDir, version, yes, force = false) {
3745
3815
  const tc = installedIn(root);
3746
3816
  const wrapped = configStatus(root).wrapped;
3747
3817
  if (tc.declared && tc.installed && wrapped && !force) return true;
3748
- const name = readPackage(root)?.name ?? path14.basename(root);
3818
+ const name = readPackage(root)?.name ?? path15.basename(root);
3749
3819
  if (!yes) {
3750
3820
  if (!process.stdin.isTTY) {
3751
3821
  console.error(`Truecanvas isn't set up in ${name} yet. Run again with --yes to set it up.`);
@@ -3836,11 +3906,11 @@ function startApp(root, appUrl, framework) {
3836
3906
  }
3837
3907
  function binScript(root, pkg) {
3838
3908
  try {
3839
- const req = createRequire(path14.join(root, "package.json"));
3909
+ const req = createRequire(path15.join(root, "package.json"));
3840
3910
  const manifest = req.resolve(`${pkg}/package.json`);
3841
3911
  const { bin } = JSON.parse(fs13.readFileSync(manifest, "utf8"));
3842
3912
  const rel = typeof bin === "string" ? bin : bin?.[pkg];
3843
- return rel ? path14.join(path14.dirname(manifest), rel) : null;
3913
+ return rel ? path15.join(path15.dirname(manifest), rel) : null;
3844
3914
  } catch {
3845
3915
  return null;
3846
3916
  }
@@ -2,7 +2,10 @@
2
2
  * The share viewer: one static page that reads ./manifest.json and lays the
3
3
  * frozen frames out at their canvas positions, with pan and zoom (drag,
4
4
  * wheel, pinch, keys). Frames are sandboxed iframes without scripts, under a
5
- * transparent layer that takes every pointer event. Self-contained: no
5
+ * transparent layer that takes every pointer event. When the version has a
6
+ * live site, each frame gets a "View live" button: the real site in a
7
+ * device-sized window, scripts and animations included, on its own origin.
8
+ * Self-contained: no
6
9
  * build step, no dependencies, so the same file works locally and on a
7
10
  * review site.
8
11
  */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  viewerHtml
3
- } from "../chunk-M7B3W7EP.js";
3
+ } from "../chunk-DV76O5SZ.js";
4
4
  export {
5
5
  viewerHtml
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truecanvas",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Design with your real React components: a Figma-like canvas for Next.js and Vite where layers are your .tsx and agents edit through MCP.",
5
5
  "license": "MIT",
6
6
  "type": "module",