truecanvas 0.3.0 → 0.4.1

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
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ viewerHtml
4
+ } from "./chunk-M7B3W7EP.js";
2
5
  import {
3
6
  DEVICES,
4
7
  EditError,
@@ -29,11 +32,11 @@ import {
29
32
  syncComponentRegistry,
30
33
  toolEnv,
31
34
  writeCompare
32
- } from "./chunk-XWQ5MS24.js";
35
+ } from "./chunk-A4QIAVTG.js";
33
36
  import {
34
37
  loadConfig
35
38
  } from "./chunk-PBKX3SBC.js";
36
- import "./chunk-WU7NBTJH.js";
39
+ import "./chunk-2IIAMOFH.js";
37
40
  import {
38
41
  ICON_LIBRARIES,
39
42
  iconLibraries,
@@ -44,14 +47,14 @@ import {
44
47
 
45
48
  // src/cli/index.ts
46
49
  import { spawn as spawn3 } from "child_process";
47
- import fs11 from "fs";
48
- import path11 from "path";
50
+ import fs13 from "fs";
51
+ import path14 from "path";
49
52
  import { parseArgs } from "util";
50
53
 
51
54
  // src/server/http.ts
52
55
  import http from "http";
53
- import fs4 from "fs";
54
- import path4 from "path";
56
+ import fs6 from "fs";
57
+ import path7 from "path";
55
58
  import { randomUUID } from "crypto";
56
59
  import { fileURLToPath } from "url";
57
60
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -87,6 +90,276 @@ function addShadcnComponents(root, names) {
87
90
  return exclusive(() => shadcnAdd(root, names));
88
91
  }
89
92
 
93
+ // src/share/snapshot.ts
94
+ import crypto from "crypto";
95
+ import fs from "fs";
96
+ import path from "path";
97
+ var MAX_ASSETS_BYTES = 80 * 1024 * 1024;
98
+ function sharesDir(root) {
99
+ return path.join(root, "node_modules", ".cache", "truecanvas", "shares");
100
+ }
101
+ async function createSnapshot(ws, shots, canvas, opts = {}) {
102
+ const doc = ws.doc(canvas);
103
+ const frames = doc.frames.filter((f) => !opts.frames || opts.frames.includes(f.frameName));
104
+ if (!frames.length) throw new Error(`Canvas "${canvas}" has no frames to share.`);
105
+ const id = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[-:T]/g, "").replace(/^(\d{8})/, "$1-");
106
+ const dir = path.join(sharesDir(ws.config.root), canvas, id);
107
+ fs.mkdirSync(path.join(dir, "frames"), { recursive: true });
108
+ fs.mkdirSync(path.join(dir, "assets"), { recursive: true });
109
+ const assets = new Assets(path.join(dir, "assets"));
110
+ const appOrigin = new URL(ws.config.appUrl).origin;
111
+ const out = [];
112
+ const external = /* @__PURE__ */ new Set();
113
+ const used = /* @__PURE__ */ new Set();
114
+ for (const f of frames) {
115
+ const device = findDevice(f.device);
116
+ const raw = await shots.snapshot({ canvas, frame: f.frameName, width: f.width, height: f.height, theme: f.theme ?? "light", mobile: device ? device.kind !== "desktop" : false });
117
+ for (const u of raw.requests) if (new URL(u).origin !== appOrigin) external.add(new URL(u).origin);
118
+ let slug = f.frameName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "frame";
119
+ while (used.has(slug)) slug += "-2";
120
+ used.add(slug);
121
+ fs.writeFileSync(path.join(dir, "frames", `${slug}.html`), await freeze(raw, assets));
122
+ fs.writeFileSync(path.join(dir, "frames", `${slug}.png`), raw.png);
123
+ out.push({ name: f.frameName, x: f.x, y: f.y, width: raw.width, height: f.height ?? raw.height, theme: f.theme, device: f.device, html: `frames/${slug}.html`, image: `frames/${slug}.png` });
124
+ }
125
+ const manifest = {
126
+ format: 1,
127
+ project: path.basename(ws.config.root),
128
+ canvas,
129
+ id,
130
+ createdAt: Date.now(),
131
+ frames: out,
132
+ missing: [...assets.missing],
133
+ external: [...external].filter((o) => !/fonts\.(googleapis|gstatic)\.com$/.test(new URL(o).host))
134
+ };
135
+ fs.writeFileSync(path.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2));
136
+ fs.writeFileSync(path.join(dir, "index.html"), viewerHtml());
137
+ return { dir, manifest };
138
+ }
139
+ async function freeze(raw, assets) {
140
+ let css = "";
141
+ for (const sheet of raw.css) {
142
+ let text = sheet.text;
143
+ if (sheet.href && !text) text = await fetchText(sheet.href) ?? "";
144
+ text = await replaceAsync(text, /url\(\s*(['"]?)([^'")]+)\1\s*\)/g, async (m, _q, u) => {
145
+ const local = await assets.local(u, sheet.base, "../assets/");
146
+ return local ? `url("${local}")` : m;
147
+ });
148
+ css += sheet.media && sheet.media !== "all" ? `@media ${sheet.media} {
149
+ ${text}
150
+ }
151
+ ` : `${text}
152
+ `;
153
+ }
154
+ let html = raw.html;
155
+ html = await replaceAsync(html, /\s(src|poster|href|xlink:href)="([^"]+)"/g, async (m, attr, u) => {
156
+ if (u.startsWith("tc-canvas:")) return m;
157
+ const local = await assets.local(decodeEntities(u), raw.url, "../assets/");
158
+ return local ? ` ${attr}="${local}"` : m;
159
+ });
160
+ html = await replaceAsync(html, /url\(("|['"]?)([^'")&]+)\1\)/g, async (m, q, u) => {
161
+ const local = await assets.local(decodeEntities(u), raw.url, "../assets/");
162
+ return local ? `url(${q}${local}${q})` : m;
163
+ });
164
+ for (const c2 of raw.canvases) {
165
+ const local = assets.store(c2.png, "png");
166
+ html = html.split(`src="tc-canvas:${c2.index}"`).join(`src="../assets/${local}"`);
167
+ }
168
+ html = html.replace(/\ssrc="tc-canvas:\d+"/g, "");
169
+ const head = `<meta name="robots" content="noindex"><style>${css.replace(/<\/style/gi, "<\\/style")}</style>`;
170
+ return html.includes("</head>") ? html.replace("</head>", `${head}</head>`) : head + html;
171
+ }
172
+ var Assets = class {
173
+ constructor(dir) {
174
+ this.dir = dir;
175
+ }
176
+ dir;
177
+ byUrl = /* @__PURE__ */ new Map();
178
+ bytes = 0;
179
+ missing = /* @__PURE__ */ new Set();
180
+ /** The local path (with `prefix`) for an asset URL, or null to leave it as is (data:, #fragment). */
181
+ local(spec, base, prefix) {
182
+ if (/^(data:|#|about:|blob:)/i.test(spec.trim())) return Promise.resolve(null);
183
+ let url;
184
+ try {
185
+ url = new URL(spec.trim(), base).href;
186
+ } catch {
187
+ return Promise.resolve(null);
188
+ }
189
+ if (!/^https?:/.test(url)) return Promise.resolve(null);
190
+ if (!this.byUrl.has(url)) this.byUrl.set(url, this.download(url));
191
+ return this.byUrl.get(url).then((name) => name ? prefix + name : null);
192
+ }
193
+ async download(url) {
194
+ if (this.bytes > MAX_ASSETS_BYTES) {
195
+ this.missing.add(url);
196
+ return null;
197
+ }
198
+ try {
199
+ const res = await fetch(url, { signal: AbortSignal.timeout(2e4) });
200
+ if (!res.ok) throw new Error(String(res.status));
201
+ const buf = Buffer.from(await res.arrayBuffer());
202
+ this.bytes += buf.length;
203
+ return this.store(buf, extension(url, res.headers.get("content-type")));
204
+ } catch {
205
+ this.missing.add(url);
206
+ return null;
207
+ }
208
+ }
209
+ store(buf, ext) {
210
+ const name = `${crypto.createHash("sha256").update(buf).digest("hex").slice(0, 20)}.${ext}`;
211
+ const file = path.join(this.dir, name);
212
+ if (!fs.existsSync(file)) fs.writeFileSync(file, buf);
213
+ return name;
214
+ }
215
+ };
216
+ var TYPES = {
217
+ "image/png": "png",
218
+ "image/jpeg": "jpg",
219
+ "image/webp": "webp",
220
+ "image/avif": "avif",
221
+ "image/gif": "gif",
222
+ "image/svg+xml": "svg",
223
+ "image/x-icon": "ico",
224
+ "image/vnd.microsoft.icon": "ico",
225
+ "font/woff2": "woff2",
226
+ "font/woff": "woff",
227
+ "font/ttf": "ttf",
228
+ "font/otf": "otf",
229
+ "application/font-woff2": "woff2",
230
+ "video/mp4": "mp4",
231
+ "video/webm": "webm"
232
+ };
233
+ function extension(url, type) {
234
+ const fromType = TYPES[(type ?? "").split(";")[0].trim()];
235
+ if (fromType) return fromType;
236
+ const ext = path.extname(new URL(url).pathname).slice(1).toLowerCase();
237
+ return /^[a-z0-9]{1,5}$/.test(ext) ? ext : "bin";
238
+ }
239
+ async function fetchText(url) {
240
+ try {
241
+ const res = await fetch(url, { signal: AbortSignal.timeout(15e3), headers: { "user-agent": "Mozilla/5.0 Chrome/130 Safari/537.36" } });
242
+ return res.ok ? await res.text() : null;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+ var decodeEntities = (s) => s.replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
248
+ async function replaceAsync(input, re, fn) {
249
+ const matches = [...input.matchAll(re)];
250
+ const replaced = await Promise.all(matches.map((m) => fn(...m)));
251
+ let out = "";
252
+ let last = 0;
253
+ matches.forEach((m, i) => {
254
+ out += input.slice(last, m.index) + replaced[i];
255
+ last = m.index + m[0].length;
256
+ });
257
+ return out + input.slice(last);
258
+ }
259
+
260
+ // src/share/publish.ts
261
+ import fs2 from "fs";
262
+ import os from "os";
263
+ import path2 from "path";
264
+ var configFile = () => path2.join(process.env.XDG_CONFIG_HOME ?? path2.join(os.homedir(), ".config"), "truecanvas", "share.json");
265
+ function reviewSite() {
266
+ const { TRUECANVAS_REVIEW_URL: url, TRUECANVAS_REVIEW_TOKEN: token } = process.env;
267
+ if (url && token) return { url: url.replace(/\/+$/, ""), token };
268
+ try {
269
+ const saved = JSON.parse(fs2.readFileSync(configFile(), "utf8"));
270
+ return saved.url && saved.token ? saved : null;
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+ async function connectReviewSite(url, token) {
276
+ const site = { url: url.trim().replace(/\/+$/, ""), token: token.trim() };
277
+ if (!/^https?:\/\//.test(site.url)) throw new Error("The review site URL starts with https://");
278
+ const res = await request(site, "GET", "/api/shares");
279
+ const file = configFile();
280
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
281
+ fs2.writeFileSync(file, JSON.stringify(site, null, 2), { mode: 384 });
282
+ return res;
283
+ }
284
+ var MAX_FILE = 4.4 * 1024 * 1024;
285
+ async function publishSnapshot(site, dir, manifest, opts = {}) {
286
+ const share = await request(site, "POST", "/api/shares", { project: manifest.project, canvas: manifest.canvas, title: opts.title ?? manifest.canvas });
287
+ const base = `/api/shares/${share.slug}`;
288
+ const known = new Set((await request(site, "GET", `${base}/assets`)).assets);
289
+ const files = [];
290
+ for (const name of fs2.readdirSync(path2.join(dir, "assets"))) if (!known.has(name)) files.push({ local: path2.join(dir, "assets", name), remote: `assets/${name}` });
291
+ 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}` });
292
+ const skipped = [];
293
+ let uploaded = 0;
294
+ const queue2 = [...files];
295
+ const worker = async () => {
296
+ for (let f = queue2.shift(); f; f = queue2.shift()) {
297
+ const body = fs2.readFileSync(f.local);
298
+ if (body.length > MAX_FILE) {
299
+ skipped.push(f.remote);
300
+ continue;
301
+ }
302
+ await request(site, "PUT", `${base}/files`, body, { "x-path": f.remote, "content-type": contentType(f.local) });
303
+ uploaded++;
304
+ }
305
+ };
306
+ await Promise.all(Array.from({ length: 4 }, worker));
307
+ 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(", ")}`);
308
+ const change = { ...opts.password !== void 0 ? { password: opts.password } : {}, ...opts.title ? { title: opts.title } : {} };
309
+ 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 };
312
+ }
313
+ async function deleteShare(site, project, canvas) {
314
+ const found = await request(site, "GET", `/api/shares?project=${encodeURIComponent(project)}&canvas=${encodeURIComponent(canvas)}`);
315
+ if (!found.slug) return false;
316
+ await request(site, "DELETE", `/api/shares/${found.slug}`);
317
+ return true;
318
+ }
319
+ async function updateShare(site, project, canvas, change) {
320
+ const share = await request(site, "POST", "/api/shares", { project, canvas });
321
+ await request(site, "PATCH", `/api/shares/${share.slug}`, change);
322
+ return share.url;
323
+ }
324
+ async function request(site, method, route, body, headers = {}) {
325
+ const raw = Buffer.isBuffer(body);
326
+ let res;
327
+ try {
328
+ res = await fetch(site.url + route, {
329
+ method,
330
+ headers: { authorization: `Bearer ${site.token}`, ...body !== void 0 && !raw ? { "content-type": "application/json" } : {}, ...headers },
331
+ body: body === void 0 ? void 0 : raw ? new Uint8Array(body) : JSON.stringify(body),
332
+ signal: AbortSignal.timeout(6e4)
333
+ });
334
+ } catch (err) {
335
+ throw new Error(`Couldn't reach the review site at ${site.url}: ${err.message}`);
336
+ }
337
+ if (res.status === 401) throw new Error("The review site refused the token. Run `truecanvas share setup` again with the REVIEW_TOKEN from the site's environment variables.");
338
+ const data2 = await res.json().catch(() => ({}));
339
+ if (!res.ok) throw new Error(`Review site: ${data2.error ?? `${res.status} ${res.statusText}`}`);
340
+ return data2;
341
+ }
342
+ function contentType(file) {
343
+ const ext = path2.extname(file).slice(1).toLowerCase();
344
+ const types = {
345
+ html: "text/html; charset=utf-8",
346
+ png: "image/png",
347
+ jpg: "image/jpeg",
348
+ webp: "image/webp",
349
+ avif: "image/avif",
350
+ gif: "image/gif",
351
+ svg: "image/svg+xml",
352
+ ico: "image/x-icon",
353
+ woff2: "font/woff2",
354
+ woff: "font/woff",
355
+ ttf: "font/ttf",
356
+ otf: "font/otf",
357
+ mp4: "video/mp4",
358
+ webm: "video/webm"
359
+ };
360
+ return types[ext] ?? "application/octet-stream";
361
+ }
362
+
90
363
  // src/server/mcp.ts
91
364
  var INSTRUCTIONS = `Truecanvas is a design canvas whose layers are the project's real React components.
92
365
  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.
@@ -614,8 +887,9 @@ ${source.slice(node.start, node.end)}
614
887
  else where = `
615
888
  on a ${t.node.name} (it may have moved)`;
616
889
  }
617
- const msgs = t.messages.map((m) => ` ${m.author.name}${m.author.kind === "agent" ? " (agent)" : ""}: ${m.text}`).join("\n");
618
- return `thread ${t.id}${t.resolved ? " [resolved]" : ""} in frame "${t.frame}" at ${t.x},${t.y}${where}
890
+ const msgs = t.messages.map((m) => ` ${m.author.name}${m.author.kind === "agent" ? " (agent)" : m.author.kind === "client" ? " (client)" : ""}: ${m.text}`).join("\n");
891
+ const shared = t.share ? " [from a client on the share link: replies and resolving are sent to the client]" : "";
892
+ return `thread ${t.id}${t.resolved ? " [resolved]" : ""}${shared} in frame "${t.frame}" at ${t.x},${t.y}${where}
619
893
  ${msgs}`;
620
894
  });
621
895
  return text(parts.join("\n\n"));
@@ -655,9 +929,9 @@ ${msgs}`;
655
929
  try {
656
930
  const c2 = resolveCanvas(canvas);
657
931
  const a = actor();
658
- const author = { name: a.kind === "agent" ? a.name : "agent", kind: "agent" };
659
- if (note?.trim()) ws.comments.reply(c2, thread, note, author);
660
- const t = ws.comments.resolve(c2, thread, true, author);
932
+ const author2 = { name: a.kind === "agent" ? a.name : "agent", kind: "agent" };
933
+ if (note?.trim()) ws.comments.reply(c2, thread, note, author2);
934
+ const t = ws.comments.resolve(c2, thread, true, author2);
661
935
  ws.commentsChanged(c2);
662
936
  presence(c2, "editing", "Resolved a comment", [], t.frame);
663
937
  return text(`\u2713 Resolved thread ${t.id}.`);
@@ -703,6 +977,36 @@ ${msgs}`;
703
977
  }
704
978
  }
705
979
  );
980
+ server.registerTool(
981
+ "share_canvas",
982
+ {
983
+ title: "Share canvas",
984
+ description: "Share a canvas with a client as a link: every frame is rendered by the app and frozen into static HTML (no scripts, no API calls), then published to the studio's review site as a new version of the canvas's link. Without a review site set up, returns a local preview instead. Data the app showed while rendering is in the snapshot: use sample data, never real client data.",
985
+ inputSchema: {
986
+ ...canvasArg,
987
+ 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".')
989
+ }
990
+ },
991
+ async ({ canvas, frames, title }) => {
992
+ try {
993
+ const c2 = resolveCanvas(canvas);
994
+ presence(c2, "looking", "Preparing a share link");
995
+ const { dir, manifest } = await createSnapshot(ws, shots, c2, { frames });
996
+ const preview = `http://localhost:${ws.config.port}/share/${encodeURIComponent(c2)}/${manifest.id}/`;
997
+ const notes = [
998
+ manifest.external.length ? `While rendering, the app called ${manifest.external.join(", ")}: whatever it showed is in the snapshot.` : "",
999
+ manifest.missing.length ? `${manifest.missing.length} asset(s) couldn't be fetched.` : ""
1000
+ ].filter(Boolean);
1001
+ const site = reviewSite();
1002
+ 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"));
1005
+ } catch (e) {
1006
+ return fail(e);
1007
+ }
1008
+ }
1009
+ );
706
1010
  server.registerTool(
707
1011
  "screenshot_frame",
708
1012
  {
@@ -873,14 +1177,14 @@ ${out.out.slice(-1500)}`));
873
1177
  }
874
1178
 
875
1179
  // src/server/thumbnails.ts
876
- import crypto from "crypto";
877
- import fs from "fs";
878
- import path from "path";
1180
+ import crypto2 from "crypto";
1181
+ import fs3 from "fs";
1182
+ import path3 from "path";
879
1183
  var Thumbnails = class _Thumbnails {
880
1184
  constructor(ws, shots) {
881
1185
  this.ws = ws;
882
1186
  this.shots = shots;
883
- this.dir = path.join(ws.config.root, "node_modules", ".cache", "truecanvas", "thumbs");
1187
+ this.dir = path3.join(ws.config.root, "node_modules", ".cache", "truecanvas", "thumbs");
884
1188
  }
885
1189
  ws;
886
1190
  shots;
@@ -906,26 +1210,26 @@ var Thumbnails = class _Thumbnails {
906
1210
  key(spec, props) {
907
1211
  let stamp = "";
908
1212
  try {
909
- const file = spec.library ? path.join(this.ws.config.root, "node_modules", spec.library, "package.json") : path.join(this.ws.config.root, spec.file);
910
- stamp = String(fs.statSync(file).mtimeMs);
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);
1214
+ stamp = String(fs3.statSync(file).mtimeMs);
911
1215
  } catch {
912
1216
  stamp = "0";
913
1217
  }
914
- return crypto.createHash("sha1").update(JSON.stringify({ v: 1, name: spec.name, props, stamp })).digest("hex").slice(0, 16);
1218
+ return crypto2.createHash("sha1").update(JSON.stringify({ v: 1, name: spec.name, props, stamp })).digest("hex").slice(0, 16);
915
1219
  }
916
1220
  async get(name, preset) {
917
1221
  await this.ws.catalog.load();
918
1222
  const spec = this.ws.catalog.get(name);
919
1223
  if (!spec) throw new Error(`Unknown component ${name}`);
920
1224
  const props = _Thumbnails.previewProps(spec, preset);
921
- const file = path.join(this.dir, `${name}-${this.key(spec, props)}.png`);
922
- if (fs.existsSync(file)) return fs.readFileSync(file);
1225
+ const file = path3.join(this.dir, `${name}-${this.key(spec, props)}.png`);
1226
+ if (fs3.existsSync(file)) return fs3.readFileSync(file);
923
1227
  const job = this.queue.then(async () => {
924
- if (fs.existsSync(file)) return fs.readFileSync(file);
1228
+ if (fs3.existsSync(file)) return fs3.readFileSync(file);
925
1229
  const url = `${this.ws.config.appUrl}/truecanvas/__preview?component=${encodeURIComponent(name)}&props=${encodeURIComponent(JSON.stringify(props))}`;
926
1230
  const png = await this.shots.element(url, { width: 640, height: 480 });
927
- fs.mkdirSync(this.dir, { recursive: true });
928
- fs.writeFileSync(file, png);
1231
+ fs3.mkdirSync(this.dir, { recursive: true });
1232
+ fs3.writeFileSync(file, png);
929
1233
  return png;
930
1234
  });
931
1235
  this.queue = job.catch(() => void 0);
@@ -934,14 +1238,14 @@ var Thumbnails = class _Thumbnails {
934
1238
  };
935
1239
 
936
1240
  // src/server/screenshot.ts
937
- import fs2 from "fs";
938
- import os from "os";
939
- import path2 from "path";
1241
+ import fs4 from "fs";
1242
+ import os2 from "os";
1243
+ import path4 from "path";
940
1244
  function findChromium() {
941
1245
  if (process.env.TRUECANVAS_CHROME) return process.env.TRUECANVAS_CHROME;
942
- const cache2 = process.env.PLAYWRIGHT_BROWSERS_PATH || (process.platform === "win32" ? path2.join(process.env.LOCALAPPDATA ?? path2.join(os.homedir(), "AppData", "Local"), "ms-playwright") : process.platform === "darwin" ? path2.join(os.homedir(), "Library", "Caches", "ms-playwright") : path2.join(os.homedir(), ".cache", "ms-playwright"));
943
- if (fs2.existsSync(cache2)) {
944
- const shells = fs2.readdirSync(cache2).filter((d) => d.startsWith("chromium_headless_shell-")).sort().reverse();
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"));
1247
+ if (fs4.existsSync(cache2)) {
1248
+ const shells = fs4.readdirSync(cache2).filter((d) => d.startsWith("chromium_headless_shell-")).sort().reverse();
945
1249
  for (const d of shells) {
946
1250
  for (const sub of [
947
1251
  "chrome-headless-shell-linux64/chrome-headless-shell",
@@ -951,14 +1255,14 @@ function findChromium() {
951
1255
  "chrome-headless-shell-win64/chrome-headless-shell.exe",
952
1256
  "chrome-win/headless_shell.exe"
953
1257
  ]) {
954
- const p = path2.join(cache2, d, sub);
955
- if (fs2.existsSync(p)) return p;
1258
+ const p = path4.join(cache2, d, sub);
1259
+ if (fs4.existsSync(p)) return p;
956
1260
  }
957
1261
  }
958
1262
  }
959
1263
  const programFiles = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(Boolean);
960
1264
  for (const p of [
961
- ...programFiles.flatMap((dir) => [path2.join(dir, "Google", "Chrome", "Application", "chrome.exe"), path2.join(dir, "Microsoft", "Edge", "Application", "msedge.exe")]),
1265
+ ...programFiles.flatMap((dir) => [path4.join(dir, "Google", "Chrome", "Application", "chrome.exe"), path4.join(dir, "Microsoft", "Edge", "Application", "msedge.exe")]),
962
1266
  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
963
1267
  "/usr/bin/chromium-browser",
964
1268
  "/usr/bin/chromium",
@@ -967,7 +1271,7 @@ function findChromium() {
967
1271
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
968
1272
  "/Applications/Chromium.app/Contents/MacOS/Chromium"
969
1273
  ]) {
970
- if (fs2.existsSync(p)) return p;
1274
+ if (fs4.existsSync(p)) return p;
971
1275
  }
972
1276
  return void 0;
973
1277
  }
@@ -1040,7 +1344,14 @@ var Screenshotter = class {
1040
1344
  });
1041
1345
  }
1042
1346
  async frame(opts) {
1043
- const options = {
1347
+ return this.withContext(this.frameOptions(opts), async (ctx) => {
1348
+ const page = await this.openFrame(ctx, opts);
1349
+ const format = opts.jpeg ? { type: "jpeg", quality: 78 } : { type: "png" };
1350
+ return await page.screenshot(format);
1351
+ });
1352
+ }
1353
+ frameOptions(opts) {
1354
+ return {
1044
1355
  viewport: { width: opts.width, height: opts.height ?? 800 },
1045
1356
  deviceScaleFactor: opts.scale ?? 1,
1046
1357
  colorScheme: opts.theme === "dark" ? "dark" : "light",
@@ -1048,28 +1359,135 @@ var Screenshotter = class {
1048
1359
  isMobile: !!opts.mobile,
1049
1360
  hasTouch: !!opts.mobile
1050
1361
  };
1051
- return this.withContext(options, async (ctx) => {
1052
- const page = await ctx.newPage();
1053
- const url = `${this.appUrl}/truecanvas/${encodeURIComponent(opts.canvas)}?frame=${encodeURIComponent(opts.frame)}&theme=${opts.theme}&still=1`;
1054
- await page.goto(url, { waitUntil: "load", timeout: 3e4 });
1055
- await page.waitForSelector("[data-tc-frame]", { timeout: 2e4 });
1056
- if (!opts.height) {
1057
- const h = await page.evaluate(() => Math.ceil(document.querySelector("[data-tc-frame]").getBoundingClientRect().height));
1058
- await page.setViewportSize({ width: opts.width, height: Math.min(Math.max(h, 100), opts.maxHeight ?? 16e3) });
1059
- }
1060
- await page.waitForFunction(() => window.__tcStill === true, void 0, { timeout: 6e3 }).catch(() => {
1362
+ }
1363
+ /** Loads a frame, sizes hug frames to their content and waits until it has settled on its final state. */
1364
+ async openFrame(ctx, opts) {
1365
+ const page = await ctx.newPage();
1366
+ const url = `${this.appUrl}/truecanvas/${encodeURIComponent(opts.canvas)}?frame=${encodeURIComponent(opts.frame)}&theme=${opts.theme}&still=1`;
1367
+ await page.goto(url, { waitUntil: "load", timeout: 3e4 });
1368
+ await page.waitForSelector("[data-tc-frame]", { timeout: 2e4 });
1369
+ if (!opts.height) {
1370
+ const h = await page.evaluate(() => Math.ceil(document.querySelector("[data-tc-frame]").getBoundingClientRect().height));
1371
+ await page.setViewportSize({ width: opts.width, height: Math.min(Math.max(h, 100), opts.maxHeight ?? 16e3) });
1372
+ }
1373
+ await page.waitForFunction(() => window.__tcStill === true, void 0, { timeout: 6e3 }).catch(() => {
1374
+ });
1375
+ await page.evaluate(() => document.fonts.ready.then(() => new Promise((r) => setTimeout(r, 50))));
1376
+ await page.waitForTimeout(150);
1377
+ return page;
1378
+ }
1379
+ /**
1380
+ * A frame frozen into static HTML (share links): the rendered DOM without
1381
+ * scripts, every stylesheet's text, and pixels of the <canvas> elements
1382
+ * (shaders, charts), which only exist at runtime. Assets are left as URLs
1383
+ * for the caller to fetch.
1384
+ */
1385
+ async snapshot(opts) {
1386
+ return this.withContext(this.frameOptions(opts), async (ctx) => {
1387
+ const requests = /* @__PURE__ */ new Set();
1388
+ ctx.on("request", (r) => {
1389
+ if (/^https?:/.test(r.url())) requests.add(r.url());
1061
1390
  });
1062
- await page.evaluate(() => document.fonts.ready.then(() => new Promise((r) => setTimeout(r, 50))));
1063
- await page.waitForTimeout(150);
1064
- const format = opts.jpeg ? { type: "jpeg", quality: 78 } : { type: "png" };
1065
- return await page.screenshot(format);
1391
+ const page = await this.openFrame(ctx, opts);
1392
+ await page.evaluate(() => Promise.all([...document.images].map((img) => img.complete ? null : new Promise((r) => (img.onload = r, img.onerror = r, setTimeout(r, 4e3))))));
1393
+ const raw = await page.evaluate(serializePage);
1394
+ const canvases = [];
1395
+ for (const c2 of raw.canvases) {
1396
+ if (c2.w < 1 || c2.h < 1) continue;
1397
+ const png2 = await page.screenshot({ type: "png", clip: { x: c2.x, y: c2.y, width: c2.w, height: c2.h }, omitBackground: true }).catch(() => null);
1398
+ if (png2) canvases.push({ index: c2.index, png: png2 });
1399
+ }
1400
+ const png = await page.screenshot({ type: "png", fullPage: true });
1401
+ const viewport = page.viewportSize();
1402
+ return { url: raw.url, html: raw.html, css: raw.css, canvases, png, width: viewport.width, height: raw.height, requests: [...requests] };
1066
1403
  });
1067
1404
  }
1068
1405
  };
1406
+ function serializePage() {
1407
+ const abs = (u) => {
1408
+ try {
1409
+ return new URL(u, location.href).href;
1410
+ } catch {
1411
+ return u;
1412
+ }
1413
+ };
1414
+ for (const img of Array.from(document.images)) {
1415
+ if (img.currentSrc) img.setAttribute("src", img.currentSrc);
1416
+ for (const a of ["srcset", "sizes", "loading", "decoding"]) img.removeAttribute(a);
1417
+ }
1418
+ for (const input of Array.from(document.querySelectorAll("input"))) {
1419
+ if (input.type === "checkbox" || input.type === "radio") input.toggleAttribute("checked", input.checked);
1420
+ else if (input.type !== "password" && input.type !== "file") input.setAttribute("value", input.value);
1421
+ }
1422
+ for (const t of Array.from(document.querySelectorAll("textarea"))) t.textContent = t.value;
1423
+ for (const o of Array.from(document.querySelectorAll("option"))) o.toggleAttribute("selected", o.selected);
1424
+ const canvases = Array.from(document.querySelectorAll("canvas")).map((c2, index) => {
1425
+ c2.setAttribute("data-tc-canvas", String(index));
1426
+ const r = c2.getBoundingClientRect();
1427
+ return { index, x: r.left + scrollX, y: r.top + scrollY, w: r.width, h: r.height };
1428
+ });
1429
+ const css = [];
1430
+ const collect = (sheet, media = "") => {
1431
+ let rules;
1432
+ try {
1433
+ rules = sheet.cssRules;
1434
+ } catch {
1435
+ if (sheet.href) css.push({ base: sheet.href, text: "", media: sheet.media?.mediaText || media, href: sheet.href });
1436
+ return;
1437
+ }
1438
+ let text = "";
1439
+ for (const rule of Array.from(rules)) {
1440
+ if (rule instanceof CSSImportRule) {
1441
+ if (rule.styleSheet) collect(rule.styleSheet, rule.media?.mediaText || "");
1442
+ continue;
1443
+ }
1444
+ text += `${rule.cssText}
1445
+ `;
1446
+ }
1447
+ css.push({ base: sheet.href || location.href, text, media: sheet.media?.mediaText || media });
1448
+ };
1449
+ for (const sheet of Array.from(document.styleSheets)) if (!sheet.disabled) collect(sheet);
1450
+ for (const sheet of document.adoptedStyleSheets ?? []) collect(sheet);
1451
+ const root = document.documentElement.cloneNode(true);
1452
+ for (const el of Array.from(root.querySelectorAll("script, noscript, style, link, template, iframe, object, embed, nextjs-portal, [data-nextjs-toast]"))) {
1453
+ el.remove();
1454
+ }
1455
+ for (const c2 of Array.from(root.querySelectorAll("canvas[data-tc-canvas]"))) {
1456
+ const i = Number(c2.getAttribute("data-tc-canvas"));
1457
+ const img = document.createElement("img");
1458
+ for (const a of Array.from(c2.attributes)) if (a.name !== "width" && a.name !== "height") img.setAttribute(a.name, a.value);
1459
+ img.setAttribute("src", `tc-canvas:${i}`);
1460
+ img.setAttribute("alt", "");
1461
+ const r = canvases[i];
1462
+ img.style.width = `${r.w}px`;
1463
+ img.style.height = `${r.h}px`;
1464
+ c2.replaceWith(img);
1465
+ }
1466
+ for (const el of [root, ...Array.from(root.querySelectorAll("*"))]) {
1467
+ for (const a of Array.from(el.attributes)) {
1468
+ const n = a.name;
1469
+ if (n.startsWith("on") || n === "data-tc" || n === "data-tc-canvas" || n === "data-tc-hidden") el.removeAttribute(n);
1470
+ }
1471
+ for (const n of ["src", "poster", "xlink:href"]) {
1472
+ const v = el.getAttribute(n);
1473
+ if (v && !/^(data:|#|tc-canvas:)/.test(v)) el.setAttribute(n, abs(v));
1474
+ }
1475
+ const href = el.getAttribute("href");
1476
+ if (href !== null && el.tagName.toLowerCase() === "a") {
1477
+ el.setAttribute("data-href", href);
1478
+ el.removeAttribute("href");
1479
+ el.style.cursor = "pointer";
1480
+ } else if (href && !href.startsWith("#") && !href.startsWith("data:")) el.setAttribute("href", abs(href));
1481
+ }
1482
+ for (const f of Array.from(root.querySelectorAll("form"))) f.removeAttribute("action");
1483
+ const height = Math.ceil(document.querySelector("[data-tc-frame]")?.getBoundingClientRect().height ?? document.documentElement.scrollHeight);
1484
+ return { url: location.href, html: `<!doctype html>
1485
+ ${root.outerHTML}`, css, canvases, height };
1486
+ }
1069
1487
 
1070
1488
  // src/server/review.ts
1071
- import fs3 from "fs";
1072
- import path3 from "path";
1489
+ import fs5 from "fs";
1490
+ import path5 from "path";
1073
1491
  import { createHash } from "crypto";
1074
1492
  var actorName = (e) => e.actor.kind === "agent" ? e.actor.name : e.actor.kind === "file" ? "code editor" : "you";
1075
1493
  async function versionAt(ws, canvas, ref) {
@@ -1086,14 +1504,14 @@ async function versionAt(ws, canvas, ref) {
1086
1504
  }
1087
1505
  var readNow = (ws, file) => {
1088
1506
  try {
1089
- return fs3.readFileSync(path3.join(ws.config.root, file), "utf8");
1507
+ return fs5.readFileSync(path5.join(ws.config.root, file), "utf8");
1090
1508
  } catch {
1091
1509
  return null;
1092
1510
  }
1093
1511
  };
1094
1512
  async function reviewPage(ws, canvas, ref, since) {
1095
1513
  const rel = ws.relFile(canvas);
1096
- const exists = fs3.existsSync(ws.file(canvas));
1514
+ const exists = fs5.existsSync(ws.file(canvas));
1097
1515
  const old = await versionAt(ws, canvas, ref);
1098
1516
  if (!exists && old.source === null) return null;
1099
1517
  const now = exists ? ws.read(canvas) : null;
@@ -1163,7 +1581,7 @@ function renderFrame(ws, shots, canvas, frame, side, ref) {
1163
1581
  async function render(ws, shots, canvas, frame, side, ref) {
1164
1582
  let source;
1165
1583
  let files = {};
1166
- if (side === "after") source = fs3.existsSync(ws.file(canvas)) ? ws.read(canvas) : null;
1584
+ if (side === "after") source = fs5.existsSync(ws.file(canvas)) ? ws.read(canvas) : null;
1167
1585
  else {
1168
1586
  const old = await versionAt(ws, canvas, ref);
1169
1587
  source = old.source;
@@ -1179,7 +1597,7 @@ async function render(ws, shots, canvas, frame, side, ref) {
1179
1597
  if (hit) return hit;
1180
1598
  let name = canvas;
1181
1599
  const snapshot = `${canvas}.${side === "before" ? "base" : "now"}`;
1182
- if (side === "before" || !fs3.existsSync(ws.file(canvas))) name = writeCompare(ws.config, canvas, source, files, snapshot);
1600
+ if (side === "before" || !fs5.existsSync(ws.file(canvas))) name = writeCompare(ws.config, canvas, source, files, snapshot);
1183
1601
  try {
1184
1602
  const device = findDevice(f.device);
1185
1603
  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 });
@@ -1231,6 +1649,150 @@ async function readJson(req, limit = 8 * 1024 * 1024) {
1231
1649
  }
1232
1650
  }
1233
1651
 
1652
+ // src/share/sync.ts
1653
+ import path6 from "path";
1654
+ var EVERY = 2e4;
1655
+ var LOOKUP_EVERY = 5 * 6e4;
1656
+ var CommentSync = class {
1657
+ constructor(ws) {
1658
+ this.ws = ws;
1659
+ }
1660
+ ws;
1661
+ links = /* @__PURE__ */ new Map();
1662
+ remote = /* @__PURE__ */ new Map();
1663
+ timer = null;
1664
+ kickTimer = null;
1665
+ running = null;
1666
+ off = null;
1667
+ start() {
1668
+ this.timer = setInterval(() => void this.syncAll(), EVERY);
1669
+ this.timer.unref();
1670
+ setTimeout(() => void this.syncAll(), 3e3).unref();
1671
+ this.off = this.ws.on((e) => {
1672
+ if (e.type !== "comments") return;
1673
+ if (this.kickTimer) clearTimeout(this.kickTimer);
1674
+ this.kickTimer = setTimeout(() => void this.syncAll(), 1500);
1675
+ });
1676
+ }
1677
+ stop() {
1678
+ if (this.timer) clearInterval(this.timer);
1679
+ if (this.kickTimer) clearTimeout(this.kickTimer);
1680
+ this.off?.();
1681
+ }
1682
+ /** A canvas was just shared: look its link up again on the next sync. */
1683
+ forget(canvas) {
1684
+ this.links.delete(canvas);
1685
+ }
1686
+ syncAll() {
1687
+ this.running ??= this.pass().finally(() => this.running = null);
1688
+ return this.running;
1689
+ }
1690
+ async pass() {
1691
+ const site = reviewSite();
1692
+ if (!site) return;
1693
+ for (const canvas of this.ws.canvases()) {
1694
+ try {
1695
+ await this.syncCanvas(site, canvas);
1696
+ } catch {
1697
+ }
1698
+ }
1699
+ }
1700
+ async slugOf(site, canvas) {
1701
+ const known = this.links.get(canvas);
1702
+ if (known && (known.slug || Date.now() - known.at < LOOKUP_EVERY)) return known.slug;
1703
+ const project = path6.basename(this.ws.config.root);
1704
+ const res = await call(site, "GET", `/api/shares?project=${encodeURIComponent(project)}&canvas=${encodeURIComponent(canvas)}`);
1705
+ this.links.set(canvas, { slug: res.slug, at: Date.now() });
1706
+ return res.slug;
1707
+ }
1708
+ async syncCanvas(site, canvas) {
1709
+ const slug = await this.slugOf(site, canvas);
1710
+ if (!slug) return;
1711
+ const cached = this.remote.get(slug);
1712
+ const res = await call(site, "GET", `/api/shares/${slug}/comments?since=${cached?.updated ?? 0}`);
1713
+ const remote = res.threads ?? cached?.threads ?? [];
1714
+ this.remote.set(slug, { updated: res.updated, threads: remote });
1715
+ const byId = new Map(remote.map((t) => [t.id, t]));
1716
+ const dismissed = this.ws.comments.dismissed(canvas);
1717
+ this.ws.comments.merge(canvas, (local) => {
1718
+ let changed = false;
1719
+ for (const r of remote) {
1720
+ if (dismissed.has(r.id)) continue;
1721
+ const l = local.find((t) => t.id === r.id);
1722
+ if (!l) {
1723
+ local.push(fromRemote(r, slug));
1724
+ changed = true;
1725
+ continue;
1726
+ }
1727
+ if (!l.share) continue;
1728
+ const have = new Set(l.messages.map((m) => m.id));
1729
+ for (const m of r.messages) {
1730
+ if (have.has(m.id)) continue;
1731
+ l.messages.push(message(m));
1732
+ changed = true;
1733
+ }
1734
+ if (changed) l.messages.sort((a, b) => a.at - b.at);
1735
+ if (r.resolved !== l.resolved && (r.resolvedAt ?? 0) > (l.resolvedAt ?? 0)) {
1736
+ l.resolved = r.resolved;
1737
+ l.resolvedBy = r.resolved && r.resolvedBy ? author(r.resolvedBy) : void 0;
1738
+ l.resolvedAt = r.resolvedAt;
1739
+ changed = true;
1740
+ }
1741
+ }
1742
+ return changed;
1743
+ });
1744
+ let pushed = false;
1745
+ for (const l of this.ws.comments.list(canvas)) {
1746
+ if (l.share?.link !== slug) continue;
1747
+ const r = byId.get(l.id);
1748
+ if (!r) continue;
1749
+ const there = new Set(r.messages.map((m) => m.id));
1750
+ for (const m of l.messages) {
1751
+ if (there.has(m.id) || m.author.kind === "client") continue;
1752
+ await call(site, "POST", `/api/shares/${slug}/comments`, { thread: l.id, message: { id: m.id, name: displayName(m.author), text: m.text, at: m.at } });
1753
+ pushed = true;
1754
+ }
1755
+ if (l.resolved !== r.resolved && (l.resolvedAt ?? 0) > (r.resolvedAt ?? 0)) {
1756
+ await call(site, "POST", `/api/shares/${slug}/comments`, { thread: l.id, resolved: l.resolved, name: l.resolvedBy ? displayName(l.resolvedBy) : "Studio" });
1757
+ pushed = true;
1758
+ }
1759
+ }
1760
+ if (pushed) this.remote.delete(slug);
1761
+ }
1762
+ };
1763
+ function fromRemote(r, slug) {
1764
+ return {
1765
+ id: r.id,
1766
+ frame: r.frame,
1767
+ x: r.x,
1768
+ y: r.y,
1769
+ node: null,
1770
+ resolved: r.resolved,
1771
+ ...r.resolved && r.resolvedBy ? { resolvedBy: author(r.resolvedBy) } : {},
1772
+ ...r.resolvedAt ? { resolvedAt: r.resolvedAt } : {},
1773
+ messages: r.messages.map(message),
1774
+ createdAt: r.createdAt,
1775
+ share: { link: slug, version: r.version }
1776
+ };
1777
+ }
1778
+ var author = (a) => ({ name: a.name, kind: a.kind === "client" ? "client" : "user" });
1779
+ var message = (m) => ({ id: m.id, author: author(m.author), text: m.text, at: m.at });
1780
+ function displayName(a) {
1781
+ if (a.kind !== "agent") return a.name;
1782
+ const base = a.name.replace(/[-_ ]?(code|cli|agent)$/i, "") || a.name;
1783
+ return base.charAt(0).toUpperCase() + base.slice(1);
1784
+ }
1785
+ async function call(site, method, route, body) {
1786
+ const res = await fetch(site.url + route, {
1787
+ method,
1788
+ headers: { authorization: `Bearer ${site.token}`, ...body ? { "content-type": "application/json" } : {} },
1789
+ body: body ? JSON.stringify(body) : void 0,
1790
+ signal: AbortSignal.timeout(2e4)
1791
+ });
1792
+ if (!res.ok) throw new Error(`Review site ${res.status}`);
1793
+ return res.json();
1794
+ }
1795
+
1234
1796
  // src/server/http.ts
1235
1797
  var MIME = {
1236
1798
  ".html": "text/html; charset=utf-8",
@@ -1239,7 +1801,14 @@ var MIME = {
1239
1801
  ".svg": "image/svg+xml",
1240
1802
  ".woff2": "font/woff2",
1241
1803
  ".png": "image/png",
1242
- ".json": "application/json"
1804
+ ".json": "application/json",
1805
+ ".jpg": "image/jpeg",
1806
+ ".webp": "image/webp",
1807
+ ".avif": "image/avif",
1808
+ ".gif": "image/gif",
1809
+ ".woff": "font/woff",
1810
+ ".ttf": "font/ttf",
1811
+ ".ico": "image/x-icon"
1243
1812
  };
1244
1813
  async function startServer(ws) {
1245
1814
  const { config } = ws;
@@ -1247,6 +1816,8 @@ async function startServer(ws) {
1247
1816
  const shots = new Screenshotter(config.appUrl);
1248
1817
  let prCache = null;
1249
1818
  const thumbs = new Thumbnails(ws, shots);
1819
+ const commentSync = new CommentSync(ws);
1820
+ commentSync.start();
1250
1821
  const syncRegistry = () => ws.catalog.load().then((list) => syncComponentRegistry(config, list)).catch(() => {
1251
1822
  });
1252
1823
  void syncRegistry();
@@ -1314,7 +1885,7 @@ async function startServer(ws) {
1314
1885
  appUrl: config.appUrl,
1315
1886
  framework: config.framework,
1316
1887
  root: config.root,
1317
- projectName: path4.basename(config.root),
1888
+ projectName: path7.basename(config.root),
1318
1889
  canvases: ws.canvases(),
1319
1890
  darkMode: config.darkMode,
1320
1891
  agents: [...ws.agents.values()],
@@ -1346,11 +1917,11 @@ async function startServer(ws) {
1346
1917
  await ws.git.fetch();
1347
1918
  return json(res, 200, { ...await ws.git.status(), linked: ws.designFiles() });
1348
1919
  case "POST /api/git/commit": {
1349
- const { message, scope } = await readJson(req);
1920
+ const { message: message2, scope } = await readJson(req);
1350
1921
  const status = await ws.git.status();
1351
1922
  const linked = new Set(ws.designFiles());
1352
1923
  const files = status.files.map((f) => f.path).filter((f) => scope === "all" || f.startsWith(`${config.canvasDir}/`) || linked.has(f));
1353
- const hash = await ws.git.commit(message, files);
1924
+ const hash = await ws.git.commit(message2, files);
1354
1925
  ws.emit({ type: "git" });
1355
1926
  return json(res, 200, { hash });
1356
1927
  }
@@ -1464,7 +2035,7 @@ async function startServer(ws) {
1464
2035
  for (const file of [f.page, ...layoutFiles(config, f.page)]) {
1465
2036
  const old = await ws.git.show(ref, file);
1466
2037
  if (old === null) continue;
1467
- const now = fs4.existsSync(path4.join(config.root, file)) ? fs4.readFileSync(path4.join(config.root, file), "utf8") : null;
2038
+ const now = fs6.existsSync(path7.join(config.root, file)) ? fs6.readFileSync(path7.join(config.root, file), "utf8") : null;
1468
2039
  if (old !== now) {
1469
2040
  files[file] = old;
1470
2041
  touched.add(f.frameName);
@@ -1594,12 +2165,26 @@ async function startServer(ws) {
1594
2165
  }
1595
2166
  case "POST /api/open-file": {
1596
2167
  const { file } = await readJson(req);
1597
- const abs = path4.resolve(config.root, file);
1598
- const rel = path4.relative(config.root, abs);
1599
- if (rel.startsWith("..") || path4.isAbsolute(rel)) return json(res, 400, { error: "Outside project." });
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." });
1600
2171
  launchEditor(abs, config.editor);
1601
2172
  return json(res, 200, { ok: true });
1602
2173
  }
2174
+ case "GET /api/share/status": {
2175
+ const site = reviewSite();
2176
+ return json(res, 200, { site: site ? site.url : null });
2177
+ }
2178
+ case "POST /api/share/snapshot": {
2179
+ const { canvas, frames, local, title, password } = await readJson(req);
2180
+ assertCanvasName(canvas);
2181
+ const { dir, manifest } = await createSnapshot(ws, shots, canvas, { frames });
2182
+ const preview = `${origin}/share/${encodeURIComponent(canvas)}/${manifest.id}/`;
2183
+ const site = local ? null : reviewSite();
2184
+ const published = site ? await publishSnapshot(site, dir, manifest, { title, password }) : null;
2185
+ if (published) commentSync.forget(canvas);
2186
+ return json(res, 200, { manifest, preview, published });
2187
+ }
1603
2188
  case "POST /api/source": {
1604
2189
  const { canvas, id } = await readJson(req);
1605
2190
  const hit = ws.nodeSource(canvas, id);
@@ -1610,23 +2195,32 @@ async function startServer(ws) {
1610
2195
  return json(res, 404, { error: `No route ${route}` });
1611
2196
  }
1612
2197
  function serveStatic(res, pathname) {
1613
- let file = path4.join(editorDir, pathname === "/" ? "index.html" : pathname);
1614
- if (!inside(editorDir, file) || !fs4.existsSync(file) || fs4.statSync(file).isDirectory()) file = path4.join(editorDir, "index.html");
1615
- if (!fs4.existsSync(file)) {
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");
2200
+ if (!fs6.existsSync(file)) {
1616
2201
  res.writeHead(500, { "content-type": "text/plain" });
1617
2202
  return res.end("Editor bundle missing. Run `pnpm build` in packages/truecanvas.");
1618
2203
  }
1619
- const ext = path4.extname(file);
2204
+ const ext = path7.extname(file);
1620
2205
  const immutable = pathname.startsWith("/assets/");
1621
2206
  res.writeHead(200, { "content-type": MIME[ext] ?? "application/octet-stream", "cache-control": immutable ? "public, max-age=31536000, immutable" : "no-cache" });
1622
- fs4.createReadStream(file).on("error", () => res.destroy()).pipe(res);
2207
+ fs6.createReadStream(file).on("error", () => res.destroy()).pipe(res);
2208
+ }
2209
+ function serveShare(res, rest) {
2210
+ const root = sharesDir(config.root);
2211
+ let file = path7.join(root, decodeURIComponent(rest));
2212
+ if (rest.endsWith("/")) file = path7.join(file, "index.html");
2213
+ 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": "*" });
2215
+ fs6.createReadStream(file).on("error", () => res.destroy()).pipe(res);
1623
2216
  }
1624
2217
  const server = http.createServer(async (req, res) => {
1625
2218
  try {
1626
2219
  if (!allowedHosts.has(req.headers.host ?? "")) return json(res, 403, { error: "Forbidden host" });
2220
+ const url = new URL(req.url ?? "/", origin);
2221
+ if (req.method === "GET" && url.pathname.startsWith("/share/")) return serveShare(res, url.pathname.slice("/share/".length));
1627
2222
  const reqOrigin = req.headers.origin;
1628
2223
  if (reqOrigin && !allowedOrigins.has(reqOrigin)) return json(res, 403, { error: "Forbidden origin" });
1629
- const url = new URL(req.url ?? "/", origin);
1630
2224
  if (url.pathname === "/mcp") return await handleMcp(req, res);
1631
2225
  if (url.pathname.startsWith("/api/")) return await handleApi(req, res, url);
1632
2226
  if (req.method === "GET") return serveStatic(res, url.pathname);
@@ -1638,8 +2232,8 @@ async function startServer(ws) {
1638
2232
  else res.end();
1639
2233
  }
1640
2234
  });
1641
- const canvasDir = path4.join(config.root, config.canvasDir);
1642
- fs4.mkdirSync(canvasDir, { recursive: true });
2235
+ const canvasDir = path7.join(config.root, config.canvasDir);
2236
+ fs6.mkdirSync(canvasDir, { recursive: true });
1643
2237
  const pending = /* @__PURE__ */ new Map();
1644
2238
  const onCanvasFile = (filename) => {
1645
2239
  if (!filename) return;
@@ -1664,7 +2258,7 @@ async function startServer(ws) {
1664
2258
  const watchCanvasDir = () => {
1665
2259
  let ino = null;
1666
2260
  try {
1667
- ino = fs4.statSync(canvasDir).ino;
2261
+ ino = fs6.statSync(canvasDir).ino;
1668
2262
  } catch {
1669
2263
  ino = null;
1670
2264
  }
@@ -1673,21 +2267,21 @@ async function startServer(ws) {
1673
2267
  canvasWatcher = null;
1674
2268
  watchedIno = ino;
1675
2269
  if (ino === null) return;
1676
- canvasWatcher = fs4.watch(canvasDir, (_event, filename) => onCanvasFile(filename));
2270
+ canvasWatcher = fs6.watch(canvasDir, (_event, filename) => onCanvasFile(filename));
1677
2271
  canvasWatcher.on("error", () => {
1678
2272
  canvasWatcher?.close();
1679
2273
  canvasWatcher = null;
1680
2274
  });
1681
- for (const f of fs4.readdirSync(canvasDir)) onCanvasFile(f);
2275
+ for (const f of fs6.readdirSync(canvasDir)) onCanvasFile(f);
1682
2276
  };
1683
2277
  watchCanvasDir();
1684
2278
  setInterval(watchCanvasDir, 2e3).unref();
1685
- const appDirAbs = path4.join(config.root, config.appDir);
1686
- if (config.framework === "next" && fs4.existsSync(appDirAbs)) {
2279
+ const appDirAbs = path7.join(config.root, config.appDir);
2280
+ if (config.framework === "next" && fs6.existsSync(appDirAbs)) {
1687
2281
  const appPending = /* @__PURE__ */ new Map();
1688
- const appWatcher = fs4.watch(appDirAbs, { recursive: true }, (_event, filename) => {
2282
+ const appWatcher = fs6.watch(appDirAbs, { recursive: true }, (_event, filename) => {
1689
2283
  if (!filename || !/(^|[\\/])(page|layout)\.[jt]sx$/.test(filename) || /^truecanvas[\\/]/.test(filename)) return;
1690
- const rel = path4.join(config.appDir, filename).split(path4.sep).join("/");
2284
+ const rel = path7.join(config.appDir, filename).split(path7.sep).join("/");
1691
2285
  clearTimeout(appPending.get(rel));
1692
2286
  appPending.set(
1693
2287
  rel,
@@ -1704,9 +2298,9 @@ async function startServer(ws) {
1704
2298
  }
1705
2299
  let catalogTimer;
1706
2300
  for (const dir of componentRoots(config.components)) {
1707
- const abs = path4.join(config.root, dir);
1708
- if (!fs4.existsSync(abs)) continue;
1709
- const componentWatcher = fs4.watch(abs, { recursive: true }, () => {
2301
+ const abs = path7.join(config.root, dir);
2302
+ if (!fs6.existsSync(abs)) continue;
2303
+ const componentWatcher = fs6.watch(abs, { recursive: true }, () => {
1710
2304
  clearTimeout(catalogTimer);
1711
2305
  catalogTimer = setTimeout(() => {
1712
2306
  ws.catalog.invalidate();
@@ -1721,6 +2315,7 @@ async function startServer(ws) {
1721
2315
  server.listen(config.port, "127.0.0.1", resolve);
1722
2316
  });
1723
2317
  const shutdown = async () => {
2318
+ commentSync.stop();
1724
2319
  await shots.close();
1725
2320
  for (const t of transports.values()) await t.close().catch(() => {
1726
2321
  });
@@ -1740,8 +2335,8 @@ function dedent(code, col) {
1740
2335
  return code.split("\n").map((l, i) => i > 0 && l.startsWith(pad) ? l.slice(col) : l).join("\n");
1741
2336
  }
1742
2337
  function inside(dir, file) {
1743
- const rel = path4.relative(dir, file);
1744
- return !!rel && !rel.startsWith("..") && !path4.isAbsolute(rel);
2338
+ const rel = path7.relative(dir, file);
2339
+ return !!rel && !rel.startsWith("..") && !path7.isAbsolute(rel);
1745
2340
  }
1746
2341
 
1747
2342
  // src/server/stdio.ts
@@ -1784,9 +2379,9 @@ import { createRequire } from "module";
1784
2379
 
1785
2380
  // src/hub/server.ts
1786
2381
  import http2 from "http";
1787
- import fs8 from "fs";
1788
- import os5 from "os";
1789
- import path8 from "path";
2382
+ import fs10 from "fs";
2383
+ import os6 from "os";
2384
+ import path11 from "path";
1790
2385
  import { randomUUID as randomUUID2 } from "crypto";
1791
2386
  import { fileURLToPath as fileURLToPath2 } from "url";
1792
2387
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
@@ -1796,42 +2391,42 @@ import { StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from
1796
2391
  import { CallToolRequestSchema as CallToolRequestSchema2, ListToolsRequestSchema as ListToolsRequestSchema2, isInitializeRequest as isInitializeRequest2 } from "@modelcontextprotocol/sdk/types.js";
1797
2392
 
1798
2393
  // src/hub/projects.ts
1799
- import fs5 from "fs";
1800
- import os2 from "os";
1801
- import path5 from "path";
1802
- var CONFIG_DIR = path5.join(process.env.XDG_CONFIG_HOME ?? path5.join(os2.homedir(), ".config"), "truecanvas");
1803
- var FILE = path5.join(CONFIG_DIR, "projects.json");
2394
+ import fs7 from "fs";
2395
+ 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");
1804
2399
  var ProjectStore = class {
1805
2400
  list = [];
1806
2401
  constructor() {
1807
2402
  try {
1808
- this.list = JSON.parse(fs5.readFileSync(FILE, "utf8"));
2403
+ this.list = JSON.parse(fs7.readFileSync(FILE, "utf8"));
1809
2404
  } catch {
1810
2405
  this.list = [];
1811
2406
  }
1812
2407
  }
1813
2408
  save() {
1814
- fs5.mkdirSync(CONFIG_DIR, { recursive: true });
1815
- fs5.writeFileSync(FILE, JSON.stringify(this.list, null, 2));
2409
+ fs7.mkdirSync(CONFIG_DIR, { recursive: true });
2410
+ fs7.writeFileSync(FILE, JSON.stringify(this.list, null, 2));
1816
2411
  }
1817
2412
  all() {
1818
2413
  return [...this.list].sort((a, b) => b.lastOpened - a.lastOpened).map((p) => ({ ...p, ...inspect(p.path) }));
1819
2414
  }
1820
2415
  touch(dir) {
1821
- const abs = path5.resolve(dir);
2416
+ const abs = path8.resolve(dir);
1822
2417
  const existing = this.list.find((p) => p.path === abs);
1823
2418
  if (existing) existing.lastOpened = Date.now();
1824
2419
  else this.list.push({ path: abs, name: projectName(abs), lastOpened: Date.now() });
1825
2420
  this.save();
1826
2421
  }
1827
2422
  forget(dir) {
1828
- this.list = this.list.filter((p) => p.path !== path5.resolve(dir));
2423
+ this.list = this.list.filter((p) => p.path !== path8.resolve(dir));
1829
2424
  this.save();
1830
2425
  }
1831
2426
  };
1832
2427
  function readPkg(dir) {
1833
2428
  try {
1834
- return JSON.parse(fs5.readFileSync(path5.join(dir, "package.json"), "utf8"));
2429
+ return JSON.parse(fs7.readFileSync(path8.join(dir, "package.json"), "utf8"));
1835
2430
  } catch {
1836
2431
  return null;
1837
2432
  }
@@ -1839,24 +2434,24 @@ function readPkg(dir) {
1839
2434
  function projectName(dir) {
1840
2435
  const pkg = readPkg(dir);
1841
2436
  const name = typeof pkg?.name === "string" ? pkg.name : "";
1842
- return name && !name.startsWith("@") ? name : path5.basename(dir);
2437
+ return name && !name.startsWith("@") ? name : path8.basename(dir);
1843
2438
  }
1844
2439
  function inspect(dir) {
1845
2440
  const pkg = readPkg(dir);
1846
- if (!pkg) return { next: false, ready: false, canvases: 0, exists: fs5.existsSync(dir) };
2441
+ if (!pkg) return { next: false, ready: false, canvases: 0, exists: fs7.existsSync(dir) };
1847
2442
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1848
2443
  let canvases = 0;
1849
2444
  for (const d of ["canvas", "src/canvas"]) {
1850
2445
  try {
1851
- canvases += fs5.readdirSync(path5.join(dir, d)).filter((f) => f.endsWith(".canvas.tsx")).length;
2446
+ canvases += fs7.readdirSync(path8.join(dir, d)).filter((f) => f.endsWith(".canvas.tsx")).length;
1852
2447
  } catch {
1853
2448
  }
1854
2449
  }
1855
2450
  return { next: "next" in deps || "vite" in deps && "react" in deps, ready: "truecanvas" in deps, canvases, exists: true };
1856
2451
  }
1857
2452
  function discover() {
1858
- const home = os2.homedir();
1859
- const roots = ["Github", "GitHub", "github", "Projects", "projects", "code", "Code", "dev", "src", "Developer", "Documents", "work"].map((d) => path5.join(home, d)).filter((d) => fs5.existsSync(d));
2453
+ 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));
1860
2455
  const found = [];
1861
2456
  let visited = 0;
1862
2457
  const walk = (dir, depth) => {
@@ -1864,7 +2459,7 @@ function discover() {
1864
2459
  visited++;
1865
2460
  let entries;
1866
2461
  try {
1867
- entries = fs5.readdirSync(dir, { withFileTypes: true });
2462
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
1868
2463
  } catch {
1869
2464
  return;
1870
2465
  }
@@ -1874,31 +2469,31 @@ function discover() {
1874
2469
  }
1875
2470
  for (const e of entries) {
1876
2471
  if (!e.isDirectory() || e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist" || e.name === "build") continue;
1877
- walk(path5.join(dir, e.name), depth + 1);
2472
+ walk(path8.join(dir, e.name), depth + 1);
1878
2473
  }
1879
2474
  };
1880
2475
  for (const r of roots) walk(r, 0);
1881
2476
  return found;
1882
2477
  }
1883
2478
  function listDirs(dir) {
1884
- const abs = path5.resolve(dir.replace(/^~(?=$|[\\/])/, os2.homedir()));
1885
- const dirs = fs5.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => ({ name: e.name, next: inspect(path5.join(abs, e.name)).next })).sort((a, b) => Number(b.next) - Number(a.next) || a.name.localeCompare(b.name));
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));
1886
2481
  return { path: abs, dirs };
1887
2482
  }
1888
2483
 
1889
2484
  // src/hub/runner.ts
1890
2485
  import { execFile, spawn as spawn2 } from "child_process";
1891
- import fs7 from "fs";
2486
+ import fs9 from "fs";
1892
2487
  import net from "net";
1893
- import os4 from "os";
1894
- import path7 from "path";
2488
+ import os5 from "os";
2489
+ import path10 from "path";
1895
2490
 
1896
2491
  // src/cli/setup.ts
1897
2492
  import { spawn } from "child_process";
1898
2493
  import { createHash as createHash2 } from "crypto";
1899
- import fs6 from "fs";
1900
- import os3 from "os";
1901
- import path6 from "path";
2494
+ import fs8 from "fs";
2495
+ import os4 from "os";
2496
+ import path9 from "path";
1902
2497
  function addDevArgs(pm, spec) {
1903
2498
  return addArgs(pm, [spec], true);
1904
2499
  }
@@ -1912,60 +2507,60 @@ var execInherit = (cmd, args, cwd) => new Promise((resolve, reject) => {
1912
2507
  p.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} exited with ${code}`)));
1913
2508
  });
1914
2509
  function packageDir(cliFile) {
1915
- return path6.resolve(path6.dirname(cliFile), "..");
2510
+ return path9.resolve(path9.dirname(cliFile), "..");
1916
2511
  }
1917
2512
  function packageVersion(pkgDir) {
1918
2513
  try {
1919
- return JSON.parse(fs6.readFileSync(path6.join(pkgDir, "package.json"), "utf8")).version;
2514
+ return JSON.parse(fs8.readFileSync(path9.join(pkgDir, "package.json"), "utf8")).version;
1920
2515
  } catch {
1921
2516
  return "0.0.0";
1922
2517
  }
1923
2518
  }
1924
2519
  function isSourceCheckout(pkgDir) {
1925
- return !pkgDir.split(path6.sep).includes("node_modules") && fs6.existsSync(path6.join(pkgDir, "src", "cli"));
2520
+ return !pkgDir.split(path9.sep).includes("node_modules") && fs8.existsSync(path9.join(pkgDir, "src", "cli"));
1926
2521
  }
1927
2522
  async function packageSpec(pkgDir, exec) {
1928
2523
  if (process.env.TRUECANVAS_PACKAGE) return process.env.TRUECANVAS_PACKAGE;
1929
2524
  const version = packageVersion(pkgDir);
1930
2525
  if (!isSourceCheckout(pkgDir)) return `truecanvas@${version}`;
1931
- const cacheDir = path6.join(os3.homedir(), ".cache", "truecanvas");
1932
- fs6.mkdirSync(cacheDir, { recursive: true });
1933
- const file = path6.join(cacheDir, `truecanvas-${version}.tgz`);
2526
+ const cacheDir = path9.join(os4.homedir(), ".cache", "truecanvas");
2527
+ fs8.mkdirSync(cacheDir, { recursive: true });
2528
+ const file = path9.join(cacheDir, `truecanvas-${version}.tgz`);
1934
2529
  await exec("npm", ["pack", "--silent", "--pack-destination", cacheDir], pkgDir);
1935
- const hash = createHash2("sha256").update(fs6.readFileSync(file)).digest("hex").slice(0, 10);
1936
- const named = path6.join(cacheDir, `truecanvas-${version}-${hash}.tgz`);
1937
- fs6.renameSync(file, named);
2530
+ const hash = createHash2("sha256").update(fs8.readFileSync(file)).digest("hex").slice(0, 10);
2531
+ const named = path9.join(cacheDir, `truecanvas-${version}-${hash}.tgz`);
2532
+ fs8.renameSync(file, named);
1938
2533
  return named;
1939
2534
  }
1940
2535
  function installedIn(root) {
1941
2536
  let declared = false;
1942
2537
  try {
1943
- const pkg = JSON.parse(fs6.readFileSync(path6.join(root, "package.json"), "utf8"));
2538
+ const pkg = JSON.parse(fs8.readFileSync(path9.join(root, "package.json"), "utf8"));
1944
2539
  declared = "truecanvas" in { ...pkg.dependencies, ...pkg.devDependencies };
1945
2540
  } catch {
1946
2541
  }
1947
- const file = path6.join(root, "node_modules", "truecanvas", "package.json");
1948
- const installed = fs6.existsSync(file);
1949
- return { declared, installed, version: installed ? packageVersion(path6.dirname(file)) : null };
2542
+ const file = path9.join(root, "node_modules", "truecanvas", "package.json");
2543
+ const installed = fs8.existsSync(file);
2544
+ return { declared, installed, version: installed ? packageVersion(path9.dirname(file)) : null };
1950
2545
  }
1951
2546
  function readPackage(root) {
1952
2547
  try {
1953
- return JSON.parse(fs6.readFileSync(path6.join(root, "package.json"), "utf8"));
2548
+ return JSON.parse(fs8.readFileSync(path9.join(root, "package.json"), "utf8"));
1954
2549
  } catch {
1955
2550
  return null;
1956
2551
  }
1957
2552
  }
1958
2553
  function depVersion(root, name) {
1959
- const file = path6.join(root, "node_modules", name, "package.json");
1960
- if (fs6.existsSync(file)) return packageVersion(path6.dirname(file));
2554
+ const file = path9.join(root, "node_modules", name, "package.json");
2555
+ if (fs8.existsSync(file)) return packageVersion(path9.dirname(file));
1961
2556
  const pkg = readPackage(root);
1962
2557
  return pkg ? { ...pkg.dependencies, ...pkg.devDependencies }[name] ?? null : null;
1963
2558
  }
1964
2559
  function mergeJson(file, update) {
1965
2560
  let data2 = {};
1966
- if (fs6.existsSync(file)) {
2561
+ if (fs8.existsSync(file)) {
1967
2562
  try {
1968
- data2 = JSON.parse(fs6.readFileSync(file, "utf8"));
2563
+ data2 = JSON.parse(fs8.readFileSync(file, "utf8"));
1969
2564
  } catch {
1970
2565
  throw new Error(`${file} isn't valid JSON; fix it or remove it, then run connect again.`);
1971
2566
  }
@@ -1973,24 +2568,24 @@ function mergeJson(file, update) {
1973
2568
  const before = JSON.stringify(data2);
1974
2569
  update(data2);
1975
2570
  if (JSON.stringify(data2) === before) return "unchanged";
1976
- fs6.mkdirSync(path6.dirname(file), { recursive: true });
1977
- fs6.writeFileSync(file, `${JSON.stringify(data2, null, 2)}
2571
+ fs8.mkdirSync(path9.dirname(file), { recursive: true });
2572
+ fs8.writeFileSync(file, `${JSON.stringify(data2, null, 2)}
1978
2573
  `);
1979
2574
  return "written";
1980
2575
  }
1981
2576
  function connectAgents(root, port, opts = {}) {
1982
2577
  const url = `http://localhost:${port}/mcp`;
1983
2578
  const out = { written: [], unchanged: [] };
1984
- const add = (rel, update) => out[mergeJson(path6.join(root, rel), update)].push(rel);
2579
+ const add = (rel, update) => out[mergeJson(path9.join(root, rel), update)].push(rel);
1985
2580
  const servers = (d, key) => d[key] ??= {};
1986
2581
  add(".mcp.json", (d) => void (servers(d, "mcpServers").truecanvas = { type: "http", url }));
1987
- if (opts.cursor || fs6.existsSync(path6.join(root, ".cursor"))) add(".cursor/mcp.json", (d) => void (servers(d, "mcpServers").truecanvas = { url }));
1988
- if (opts.vscode || fs6.existsSync(path6.join(root, ".vscode"))) add(".vscode/mcp.json", (d) => void (servers(d, "servers").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 }));
1989
2584
  return out;
1990
2585
  }
1991
2586
  function hasAgentConfig(root, port) {
1992
2587
  try {
1993
- const d = JSON.parse(fs6.readFileSync(path6.join(root, ".mcp.json"), "utf8"));
2588
+ const d = JSON.parse(fs8.readFileSync(path9.join(root, ".mcp.json"), "utf8"));
1994
2589
  const url = d?.mcpServers?.truecanvas?.url;
1995
2590
  return typeof url === "string" && (port === void 0 || url.includes(`:${port}/`));
1996
2591
  } catch {
@@ -2043,7 +2638,7 @@ var Runner = class {
2043
2638
  return next;
2044
2639
  }
2045
2640
  async start(dir) {
2046
- if (!fs7.existsSync(path7.join(dir, "package.json"))) throw new Error(`${dir} isn't there anymore (or has no package.json).`);
2641
+ if (!fs9.existsSync(path10.join(dir, "package.json"))) throw new Error(`${dir} isn't there anymore (or has no package.json).`);
2047
2642
  const [editorPort, appPort] = await this.allocatePorts();
2048
2643
  const proc = spawn2(process.execPath, [this.cli, "dev", "--no-open", "--port", String(editorPort), "--app", `http://localhost:${appPort}`], {
2049
2644
  cwd: dir,
@@ -2155,7 +2750,7 @@ async function install(job, cmd, args, cwd) {
2155
2750
  await run2(job, cmd, args, cwd);
2156
2751
  } catch (err) {
2157
2752
  const ignoredBuilds = job.log.some((l) => l.includes("ERR_PNPM_IGNORED_BUILDS"));
2158
- const installed = fs7.existsSync(path7.join(cwd, "node_modules", "truecanvas", "package.json"));
2753
+ const installed = fs9.existsSync(path10.join(cwd, "node_modules", "truecanvas", "package.json"));
2159
2754
  if (!(ignoredBuilds && installed)) throw err;
2160
2755
  job.log.push("(pnpm only complained about other packages' build scripts; Truecanvas is installed, continuing)");
2161
2756
  }
@@ -2192,11 +2787,11 @@ function packageManager(dir) {
2192
2787
  return { cmd: pm, add: addArgs(pm, [], true) };
2193
2788
  }
2194
2789
  async function preferredPm() {
2195
- const { ok } = await capture("pnpm", ["--version"], os4.homedir());
2790
+ const { ok } = await capture("pnpm", ["--version"], os5.homedir());
2196
2791
  return ok ? "pnpm" : "npm";
2197
2792
  }
2198
2793
  function setupJob(dir, pkgDir, cli) {
2199
- return startJob(`Set up ${path7.basename(dir)}`, async (job) => {
2794
+ return startJob(`Set up ${path10.basename(dir)}`, async (job) => {
2200
2795
  const pm = packageManager(dir);
2201
2796
  const source = await packageSpec(pkgDir, (cmd, args, cwd) => run2(job, cmd, args, cwd));
2202
2797
  await install(job, pm.cmd, [...pm.add, source], dir);
@@ -2206,12 +2801,12 @@ function setupJob(dir, pkgDir, cli) {
2206
2801
  }
2207
2802
  function createJob(name, parent, pkgDir, cli) {
2208
2803
  const safe = name.trim().toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "");
2209
- const parentDir = path7.resolve(parent.replace(/^~(?=$|[\\/])/, os4.homedir()));
2210
- const dir = path7.join(parentDir, safe);
2804
+ const parentDir = path10.resolve(parent.replace(/^~(?=$|[\\/])/, os5.homedir()));
2805
+ const dir = path10.join(parentDir, safe);
2211
2806
  return startJob(`Create ${safe}`, async (job) => {
2212
2807
  if (!safe) throw new Error("Pick a project name.");
2213
- if (fs7.existsSync(dir)) throw new Error(`${dir} already exists.`);
2214
- fs7.mkdirSync(parentDir, { recursive: true });
2808
+ if (fs9.existsSync(dir)) throw new Error(`${dir} already exists.`);
2809
+ fs9.mkdirSync(parentDir, { recursive: true });
2215
2810
  const pm = await preferredPm();
2216
2811
  await run2(job, "npx", ["--yes", "create-next-app@latest", safe, "--ts", "--tailwind", "--app", "--eslint", "--no-src-dir", "--import-alias", "@/*", `--use-${pm}`, "--turbopack", "--yes"], parentDir);
2217
2812
  const source = await packageSpec(pkgDir, (cmd, args, cwd) => run2(job, cmd, args, cwd));
@@ -2232,22 +2827,22 @@ function capture(cmd, args, cwd) {
2232
2827
  });
2233
2828
  }
2234
2829
  function cloneJob(repo, parent, pkgDir, cli) {
2235
- const parentDir = path7.resolve(parent.replace(/^~(?=$|[\\/])/, os4.homedir()));
2830
+ const parentDir = path10.resolve(parent.replace(/^~(?=$|[\\/])/, os5.homedir()));
2236
2831
  const name = repo.replace(/\.git$/, "").split(/[/:]/).pop() ?? "";
2237
- const dir = path7.join(parentDir, name);
2832
+ const dir = path10.join(parentDir, name);
2238
2833
  return startJob(`Clone ${name}`, async (job) => {
2239
2834
  if (!name || !/^[\w.-]+$/.test(name)) throw new Error("Pick a repository (owner/name or a git URL).");
2240
- if (fs7.existsSync(dir)) throw new Error(`${dir} already exists. Open it with Open folder instead.`);
2241
- fs7.mkdirSync(parentDir, { recursive: true });
2835
+ if (fs9.existsSync(dir)) throw new Error(`${dir} already exists. Open it with Open folder instead.`);
2836
+ fs9.mkdirSync(parentDir, { recursive: true });
2242
2837
  const url = /^(https?:|git@|ssh:|file:)/.test(repo);
2243
2838
  if (url) await run2(job, "git", ["clone", repo, dir], parentDir);
2244
2839
  else await run2(job, "gh", ["repo", "clone", repo, dir], parentDir);
2245
- if (!fs7.existsSync(path7.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.");
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.");
2246
2841
  const pm = packageManager(dir);
2247
2842
  await install(job, pm.cmd, ["install"], dir).catch(async (err) => {
2248
- if (!fs7.existsSync(path7.join(dir, "node_modules"))) throw err;
2843
+ if (!fs9.existsSync(path10.join(dir, "node_modules"))) throw err;
2249
2844
  });
2250
- const pkg = JSON.parse(fs7.readFileSync(path7.join(dir, "package.json"), "utf8"));
2845
+ const pkg = JSON.parse(fs9.readFileSync(path10.join(dir, "package.json"), "utf8"));
2251
2846
  if (!("truecanvas" in { ...pkg.dependencies, ...pkg.devDependencies })) {
2252
2847
  const source = await packageSpec(pkgDir, (cmd, args, cwd) => run2(job, cmd, args, cwd));
2253
2848
  await install(job, pm.cmd, [...pm.add, source], dir);
@@ -2272,7 +2867,7 @@ Use list_projects to see them. All other tools are the regular Truecanvas canvas
2272
2867
  async function startHub(opts) {
2273
2868
  const store = new ProjectStore();
2274
2869
  const editorDir = fileURLToPath2(new URL("./editor/", import.meta.url));
2275
- const pkgDir = path8.resolve(path8.dirname(opts.cli), "..");
2870
+ const pkgDir = path11.resolve(path11.dirname(opts.cli), "..");
2276
2871
  const origin = `http://localhost:${opts.port}`;
2277
2872
  const allowedHosts = /* @__PURE__ */ new Set([`localhost:${opts.port}`, `127.0.0.1:${opts.port}`]);
2278
2873
  const clients = /* @__PURE__ */ new Set();
@@ -2377,7 +2972,7 @@ async function startHub(opts) {
2377
2972
  }
2378
2973
  if (req.params.name === "open_project") {
2379
2974
  const q = String(req.params.arguments?.project ?? "").toLowerCase();
2380
- const p = store.all().find((x) => x.name.toLowerCase() === q || x.path.toLowerCase() === q || path8.basename(x.path).toLowerCase() === q);
2975
+ const p = store.all().find((x) => x.name.toLowerCase() === q || x.path.toLowerCase() === q || path11.basename(x.path).toLowerCase() === q);
2381
2976
  if (!p) return text(`No project "${q}". Use list_projects.`, true);
2382
2977
  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);
2383
2978
  store.touch(p.path);
@@ -2432,8 +3027,8 @@ async function startHub(opts) {
2432
3027
  case "GET /api/hub/icon": {
2433
3028
  const icon = findIcon(url.searchParams.get("path") ?? "");
2434
3029
  if (!icon) return json2(res, 404, { error: "No icon" });
2435
- res.writeHead(200, { "content-type": ICON_MIME[path8.extname(icon)] ?? "application/octet-stream", "cache-control": "private, max-age=300" });
2436
- return fs8.createReadStream(icon).on("error", () => res.destroy()).pipe(res);
3030
+ res.writeHead(200, { "content-type": ICON_MIME[path11.extname(icon)] ?? "application/octet-stream", "cache-control": "private, max-age=300" });
3031
+ return fs10.createReadStream(icon).on("error", () => res.destroy()).pipe(res);
2437
3032
  }
2438
3033
  case "GET /api/hub/ls":
2439
3034
  return json2(res, 200, listDirs(url.searchParams.get("path") || "~"));
@@ -2444,8 +3039,8 @@ async function startHub(opts) {
2444
3039
  req.on("close", () => clients.delete(res));
2445
3040
  return;
2446
3041
  case "POST /api/hub/add": {
2447
- const dir = path8.resolve(body.path.replace(/^~(?=$|[\\/])/, os5.homedir()));
2448
- if (!fs8.existsSync(path8.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." });
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." });
2449
3044
  store.touch(dir);
2450
3045
  broadcast();
2451
3046
  return json2(res, 200, { path: dir, ready: store.all().find((p) => p.path === dir)?.ready ?? false });
@@ -2484,7 +3079,7 @@ async function startHub(opts) {
2484
3079
  return json2(res, 200, createJob(body.name, body.parent, pkgDir, opts.cli));
2485
3080
  // ---------- GitHub ----------
2486
3081
  case "GET /api/hub/github/repos": {
2487
- const r = await capture("gh", ["repo", "list", "--limit", "100", "--json", "nameWithOwner,description,updatedAt,isPrivate"], os5.homedir());
3082
+ const r = await capture("gh", ["repo", "list", "--limit", "100", "--json", "nameWithOwner,description,updatedAt,isPrivate"], os6.homedir());
2488
3083
  if (!r.ok) return json2(res, 200, { gh: false, error: r.out.split("\n")[0], repos: [] });
2489
3084
  const repos = JSON.parse(r.out).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2490
3085
  return json2(res, 200, { gh: true, repos });
@@ -2529,14 +3124,14 @@ async function startHub(opts) {
2529
3124
  return json2(res, 404, { error: `No route ${route}` });
2530
3125
  }
2531
3126
  function serveStatic(res, pathname) {
2532
- let file = path8.join(editorDir, pathname === "/" ? "hub.html" : pathname);
2533
- if (!inside2(editorDir, file) || !fs8.existsSync(file) || fs8.statSync(file).isDirectory()) file = path8.join(editorDir, "hub.html");
2534
- if (!fs8.existsSync(file)) {
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");
3129
+ if (!fs10.existsSync(file)) {
2535
3130
  res.writeHead(500, { "content-type": "text/plain" });
2536
3131
  return res.end("Hub bundle missing. Run `pnpm build`.");
2537
3132
  }
2538
- res.writeHead(200, { "content-type": MIME2[path8.extname(file)] ?? "application/octet-stream", "cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache" });
2539
- fs8.createReadStream(file).on("error", () => res.destroy()).pipe(res);
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" });
3134
+ fs10.createReadStream(file).on("error", () => res.destroy()).pipe(res);
2540
3135
  }
2541
3136
  const server = http2.createServer(async (req, res) => {
2542
3137
  try {
@@ -2568,27 +3163,27 @@ async function startHub(opts) {
2568
3163
  }
2569
3164
  var ICON_MIME = { ".ico": "image/x-icon", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
2570
3165
  function findIcon(dir) {
2571
- if (!dir || !fs8.existsSync(path8.join(dir, "package.json"))) return null;
3166
+ if (!dir || !fs10.existsSync(path11.join(dir, "package.json"))) return null;
2572
3167
  const names = ["icon.svg", "icon.png", "favicon.svg", "favicon.ico", "icon.ico", "apple-icon.png", "favicon.png", "logo.svg", "icon.jpg"];
2573
3168
  const roots = ["app", "src/app", "public", "static", "assets"];
2574
3169
  for (const root of roots) {
2575
3170
  for (const name of names) {
2576
- const file = path8.join(dir, root, name);
2577
- if (fs8.existsSync(file) && fs8.statSync(file).size < 2e6) return file;
3171
+ const file = path11.join(dir, root, name);
3172
+ if (fs10.existsSync(file) && fs10.statSync(file).size < 2e6) return file;
2578
3173
  }
2579
3174
  }
2580
3175
  for (const root of ["app", "src/app"]) {
2581
3176
  let entries = [];
2582
3177
  try {
2583
- entries = fs8.readdirSync(path8.join(dir, root), { withFileTypes: true });
3178
+ entries = fs10.readdirSync(path11.join(dir, root), { withFileTypes: true });
2584
3179
  } catch {
2585
3180
  continue;
2586
3181
  }
2587
3182
  for (const e of entries) {
2588
3183
  if (!e.isDirectory()) continue;
2589
3184
  for (const name of names.slice(0, 4)) {
2590
- const file = path8.join(dir, root, e.name, name);
2591
- if (fs8.existsSync(file)) return file;
3185
+ const file = path11.join(dir, root, e.name, name);
3186
+ if (fs10.existsSync(file)) return file;
2592
3187
  }
2593
3188
  }
2594
3189
  }
@@ -2599,14 +3194,14 @@ function json2(res, status, body) {
2599
3194
  res.end(JSON.stringify(body));
2600
3195
  }
2601
3196
  function inside2(dir, file) {
2602
- const rel = path8.relative(dir, file);
2603
- return !!rel && !rel.startsWith("..") && !path8.isAbsolute(rel);
3197
+ const rel = path11.relative(dir, file);
3198
+ return !!rel && !rel.startsWith("..") && !path11.isAbsolute(rel);
2604
3199
  }
2605
3200
 
2606
3201
  // src/hub/desktop.ts
2607
- import fs9 from "fs";
2608
- import os6 from "os";
2609
- import path9 from "path";
3202
+ import fs11 from "fs";
3203
+ import os7 from "os";
3204
+ import path12 from "path";
2610
3205
  var ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
2611
3206
  <defs>
2612
3207
  <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff9466"/><stop offset="1" stop-color="#ea6a3c"/></linearGradient>
@@ -2620,15 +3215,15 @@ var ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
2620
3215
  <path fill="#f6f5f1" fill-rule="evenodd" d="M236 236h552v168H236z M428 316h168v472H428z"/>
2621
3216
  </svg>
2622
3217
  `;
2623
- var data = () => process.env.XDG_DATA_HOME ?? path9.join(os6.homedir(), ".local", "share");
3218
+ var data = () => process.env.XDG_DATA_HOME ?? path12.join(os7.homedir(), ".local", "share");
2624
3219
  function installDesktop(cli, port) {
2625
- const dir = path9.join(data(), "truecanvas");
2626
- fs9.mkdirSync(dir, { recursive: true });
2627
- const icon = path9.join(dir, "icon.svg");
2628
- fs9.writeFileSync(icon, ICON);
2629
- const launcher = path9.join(dir, "launch.sh");
3220
+ const dir = path12.join(data(), "truecanvas");
3221
+ fs11.mkdirSync(dir, { recursive: true });
3222
+ const icon = path12.join(dir, "icon.svg");
3223
+ fs11.writeFileSync(icon, ICON);
3224
+ const launcher = path12.join(dir, "launch.sh");
2630
3225
  const url = `http://localhost:${port}`;
2631
- fs9.writeFileSync(
3226
+ fs11.writeFileSync(
2632
3227
  launcher,
2633
3228
  `#!/bin/sh
2634
3229
  # Truecanvas launcher: start the hub if needed, then open its window.
@@ -2637,7 +3232,7 @@ CLI=${JSON.stringify(cli)}
2637
3232
  URL=${JSON.stringify(url)}
2638
3233
  LOG="\${XDG_STATE_HOME:-$HOME/.local/state}/truecanvas-hub.log"
2639
3234
  # apps started from the desktop don't get your shell's PATH: next, pnpm and npx need it
2640
- 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))}
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))}
2641
3236
  mkdir -p "$(dirname "$LOG")"
2642
3237
 
2643
3238
  if ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; then
@@ -2646,7 +3241,7 @@ if ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; then
2646
3241
  while [ $i -lt 60 ] && ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; do sleep 0.25; i=$((i+1)); done
2647
3242
  fi
2648
3243
 
2649
- PROFILE=${JSON.stringify(path9.join(dir, "window"))}
3244
+ PROFILE=${JSON.stringify(path12.join(dir, "window"))}
2650
3245
  for b in google-chrome google-chrome-stable chromium-browser chromium brave-browser microsoft-edge; do
2651
3246
  if command -v "$b" >/dev/null 2>&1; then
2652
3247
  exec "$b" --app="$URL" --class=Truecanvas --user-data-dir="$PROFILE" --no-first-run --no-default-browser-check
@@ -2656,10 +3251,10 @@ exec xdg-open "$URL"
2656
3251
  `,
2657
3252
  { mode: 493 }
2658
3253
  );
2659
- const apps = path9.join(data(), "applications");
2660
- fs9.mkdirSync(apps, { recursive: true });
2661
- const desktop = path9.join(apps, "truecanvas.desktop");
2662
- fs9.writeFileSync(
3254
+ const apps = path12.join(data(), "applications");
3255
+ fs11.mkdirSync(apps, { recursive: true });
3256
+ const desktop = path12.join(apps, "truecanvas.desktop");
3257
+ fs11.writeFileSync(
2663
3258
  desktop,
2664
3259
  `[Desktop Entry]
2665
3260
  Type=Application
@@ -2675,11 +3270,11 @@ StartupWMClass=Truecanvas
2675
3270
  StartupNotify=true
2676
3271
  `
2677
3272
  );
2678
- let command = path9.join(os6.homedir(), ".local", "bin", "truecanvas");
2679
- const ours = !fs9.existsSync(command) || fs9.readFileSync(command, "utf8").includes("# truecanvas wrapper");
3273
+ let command = path12.join(os7.homedir(), ".local", "bin", "truecanvas");
3274
+ const ours = !fs11.existsSync(command) || fs11.readFileSync(command, "utf8").includes("# truecanvas wrapper");
2680
3275
  if (ours) {
2681
- fs9.mkdirSync(path9.dirname(command), { recursive: true });
2682
- fs9.writeFileSync(command, `#!/bin/sh
3276
+ fs11.mkdirSync(path12.dirname(command), { recursive: true });
3277
+ fs11.writeFileSync(command, `#!/bin/sh
2683
3278
  # truecanvas wrapper (installed by \`truecanvas desktop\`)
2684
3279
  exec ${JSON.stringify(process.execPath)} ${JSON.stringify(cli)} "$@"
2685
3280
  `, { mode: 493 });
@@ -2687,19 +3282,19 @@ exec ${JSON.stringify(process.execPath)} ${JSON.stringify(cli)} "$@"
2687
3282
  return { desktop, launcher, icon, command };
2688
3283
  }
2689
3284
  function launcherPath() {
2690
- const p = path9.join(data(), "truecanvas", "launch.sh");
2691
- return fs9.existsSync(p) ? p : null;
3285
+ const p = path12.join(data(), "truecanvas", "launch.sh");
3286
+ return fs11.existsSync(p) ? p : null;
2692
3287
  }
2693
3288
  function uninstallDesktop() {
2694
3289
  const removed = [];
2695
- const cmd = path9.join(os6.homedir(), ".local", "bin", "truecanvas");
2696
- if (fs9.existsSync(cmd) && fs9.readFileSync(cmd, "utf8").includes("# truecanvas wrapper")) {
2697
- fs9.rmSync(cmd);
3290
+ const cmd = path12.join(os7.homedir(), ".local", "bin", "truecanvas");
3291
+ if (fs11.existsSync(cmd) && fs11.readFileSync(cmd, "utf8").includes("# truecanvas wrapper")) {
3292
+ fs11.rmSync(cmd);
2698
3293
  removed.push(cmd);
2699
3294
  }
2700
- for (const f of [path9.join(data(), "applications", "truecanvas.desktop"), path9.join(data(), "truecanvas")]) {
2701
- if (fs9.existsSync(f)) {
2702
- fs9.rmSync(f, { recursive: true, force: true });
3295
+ for (const f of [path12.join(data(), "applications", "truecanvas.desktop"), path12.join(data(), "truecanvas")]) {
3296
+ if (fs11.existsSync(f)) {
3297
+ fs11.rmSync(f, { recursive: true, force: true });
2703
3298
  removed.push(f);
2704
3299
  }
2705
3300
  }
@@ -2712,8 +3307,8 @@ import { createInterface } from "readline/promises";
2712
3307
 
2713
3308
  // src/cli/doctor.ts
2714
3309
  import { execFile as execFile2 } from "child_process";
2715
- import fs10 from "fs";
2716
- import path10 from "path";
3310
+ import fs12 from "fs";
3311
+ import path13 from "path";
2717
3312
  var major = (v) => v ? Number(/(\d+)(?:\.(\d+))?/.exec(v)?.[1] ?? 0) : 0;
2718
3313
  function has(cmd, args, cwd) {
2719
3314
  return new Promise((resolve) => execFile2(cmd, args, { cwd, timeout: 8e3 }, (err, stdout, stderr) => resolve(err ? null : `${stdout}${stderr}`)));
@@ -2740,7 +3335,7 @@ async function doctor(root, cliVersion) {
2740
3335
  const tw = depVersion(root, "tailwindcss");
2741
3336
  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" });
2742
3337
  const config = loadConfig(root);
2743
- if (config.framework === "next") add(fs10.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/)" });
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/)" });
2744
3339
  const tc = installedIn(root);
2745
3340
  if (!tc.declared) add({ label: "Truecanvas in the project", status: "fail", detail: "not a dependency", fix: "npx truecanvas (sets it up)" });
2746
3341
  else if (!tc.installed) add({ label: "Truecanvas in the project", status: "fail", detail: "declared but not installed", fix: `${pm} install` });
@@ -2749,15 +3344,15 @@ async function doctor(root, cliVersion) {
2749
3344
  const wrapped = configStatus(root, config.framework);
2750
3345
  const configLabel = config.framework === "vite" ? "vite.config" : "next.config";
2751
3346
  const uses = config.framework === "vite" ? "has the truecanvas() plugin" : "uses withTruecanvas()";
2752
- 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" });
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" });
2753
3348
  const scripts = pkg.scripts ?? {};
2754
3349
  const script = Object.entries(scripts).find(([, v]) => v.includes("truecanvas"));
2755
3350
  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")' });
2756
- const ignore = fs10.existsSync(path10.join(root, ".gitignore")) ? fs10.readFileSync(path10.join(root, ".gitignore"), "utf8") : "";
3351
+ const ignore = fs12.existsSync(path13.join(root, ".gitignore")) ? fs12.readFileSync(path13.join(root, ".gitignore"), "utf8") : "";
2757
3352
  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" });
2758
3353
  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)" });
2759
3354
  const chrome = findChromium();
2760
- 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)" });
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)" });
2761
3356
  const git = await has("git", ["rev-parse", "--is-inside-work-tree"], root);
2762
3357
  add(git ? { label: "Git", status: "ok" } : { label: "Git", status: "warn", detail: "not a repository", fix: "git init (branches, history, comments and compare use git)" });
2763
3358
  const gh = await has("gh", ["auth", "status"], root);
@@ -2782,6 +3377,9 @@ Usage
2782
3377
  truecanvas open Open the Truecanvas window (projects dashboard + tabs)
2783
3378
  truecanvas quit Quit the window and every app it started
2784
3379
  truecanvas desktop Add Truecanvas to your app launcher (Linux); "desktop remove" undoes it
3380
+ 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)
3382
+ truecanvas share setup Connect your studio's review site (URL + REVIEW_TOKEN)
2785
3383
  truecanvas mcp stdio MCP server for agents without HTTP support
2786
3384
 
2787
3385
  Options
@@ -2817,7 +3415,15 @@ async function main() {
2817
3415
  yes: { type: "boolean", short: "y" },
2818
3416
  cursor: { type: "boolean" },
2819
3417
  vscode: { type: "boolean" },
2820
- version: { type: "boolean", short: "v" }
3418
+ version: { type: "boolean", short: "v" },
3419
+ local: { type: "boolean" },
3420
+ title: { type: "string" },
3421
+ password: { type: "string" },
3422
+ "no-password": { type: "boolean" },
3423
+ revoke: { type: "boolean" },
3424
+ restore: { type: "boolean" },
3425
+ delete: { type: "boolean" },
3426
+ token: { type: "string" }
2821
3427
  }
2822
3428
  });
2823
3429
  const cli = fileURLToPath3(import.meta.url);
@@ -2838,8 +3444,8 @@ async function main() {
2838
3444
  }
2839
3445
  if (!await ensureSetup(root2, pkgDir, version, yes)) return;
2840
3446
  if (await openInWindow(root2, Number(values.port ?? 4800))) return;
2841
- const local = path11.join(root2, "node_modules", "truecanvas", "dist", "cli.js");
2842
- if (fs11.existsSync(local) && fs11.realpathSync(local) !== fs11.realpathSync(cli)) {
3447
+ const local = path14.join(root2, "node_modules", "truecanvas", "dist", "cli.js");
3448
+ if (fs13.existsSync(local) && fs13.realpathSync(local) !== fs13.realpathSync(cli)) {
2843
3449
  const child = spawn3(process.execPath, [local, "dev", ...process.argv.slice(2).filter((a) => a !== "start" && a !== "-y" && a !== "--yes")], { stdio: "inherit" });
2844
3450
  for (const sig of ["SIGINT", "SIGTERM"]) process.on(sig, () => child.kill(sig));
2845
3451
  child.on("exit", (code) => process.exit(code ?? 0));
@@ -2921,7 +3527,7 @@ async function main() {
2921
3527
  return;
2922
3528
  }
2923
3529
  const root = process.cwd();
2924
- if (!fs11.existsSync(path11.join(root, "package.json"))) {
3530
+ if (!fs13.existsSync(path14.join(root, "package.json"))) {
2925
3531
  console.error("Run truecanvas from your app's root (no package.json here).");
2926
3532
  process.exit(1);
2927
3533
  }
@@ -2931,7 +3537,7 @@ async function main() {
2931
3537
  const report = initProject(config);
2932
3538
  ws.ensureRoute();
2933
3539
  console.log(`
2934
- ${c.accent("\u25C6")} ${c.bold("Truecanvas")} is set up in ${c.bold(path11.basename(root))}
3540
+ ${c.accent("\u25C6")} ${c.bold("Truecanvas")} is set up in ${c.bold(path14.basename(root))}
2935
3541
  `);
2936
3542
  for (const line of report.done) console.log(` ${c.green("\u2713")} ${line}`);
2937
3543
  console.log(` ${c.green("\u2713")} Canvas folder ${c.bold(config.canvasDir)} and dev route ${c.bold(config.routeDir)}`);
@@ -2941,6 +3547,78 @@ async function main() {
2941
3547
  `);
2942
3548
  return;
2943
3549
  }
3550
+ if (command === "share") {
3551
+ if (positionals[1] === "setup") {
3552
+ const url = positionals[2] ?? await prompt("Review site URL (e.g. https://review.your-studio.com):");
3553
+ const token = values.token ?? process.env.TRUECANVAS_REVIEW_TOKEN ?? await prompt("Its REVIEW_TOKEN:");
3554
+ try {
3555
+ const { brand } = await connectReviewSite(url, token);
3556
+ console.log(` ${c.green("\u2713")} Connected to ${c.bold(brand.name)} (${url}). Share with ${c.accent("npx truecanvas share <canvas>")}.`);
3557
+ } catch (err) {
3558
+ console.error(` ${c.red("\u2717")} ${err.message}`);
3559
+ process.exit(1);
3560
+ }
3561
+ return;
3562
+ }
3563
+ const base = `http://localhost:${config.port}`;
3564
+ const state = await fetch(`${base}/api/state`).then((r) => r.ok ? r.json() : null).catch(() => null);
3565
+ const canvas = positionals[1] ?? (state?.canvases.length === 1 ? state.canvases[0] : null);
3566
+ if (values.delete) {
3567
+ const site = reviewSite();
3568
+ if (!site || !canvas) {
3569
+ console.error(site ? "Name the canvas: truecanvas share <canvas> --delete" : `No review site yet: ${c.accent("npx truecanvas share setup")}`);
3570
+ process.exit(1);
3571
+ }
3572
+ const done = await deleteShare(site, path14.basename(root), canvas);
3573
+ console.log(done ? ` ${c.green("\u2713")} Deleted the link of ${canvas}, with its versions and comments.` : ` ${canvas} has no link.`);
3574
+ return;
3575
+ }
3576
+ if (values.revoke || values.restore || values["no-password"]) {
3577
+ const site = reviewSite();
3578
+ if (!site || !canvas) {
3579
+ console.error(site ? "Name the canvas: truecanvas share <canvas> --revoke" : `No review site yet: ${c.accent("npx truecanvas share setup")}`);
3580
+ process.exit(1);
3581
+ }
3582
+ 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);
3584
+ console.log(` ${c.green("\u2713")} ${values.revoke ? "Link revoked" : values.restore ? "Link restored" : "Password removed"}: ${url}`);
3585
+ return;
3586
+ }
3587
+ if (!state) {
3588
+ console.error(`Truecanvas isn't running for this project. Start it first: ${c.accent("npx truecanvas")}`);
3589
+ process.exit(1);
3590
+ }
3591
+ if (!canvas || !state.canvases.includes(canvas)) {
3592
+ console.error(`Which canvas? ${state.canvases.join(", ") || "(none)"}
3593
+ truecanvas share <canvas>`);
3594
+ process.exit(1);
3595
+ }
3596
+ const publish = !values.local && !!reviewSite();
3597
+ console.log(` ${c.dim(`Rendering ${canvas} through your app${publish ? " and publishing it" : ""}\u2026`)}`);
3598
+ 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 }) });
3600
+ const body = await res.json();
3601
+ if (!res.ok) {
3602
+ console.error(` ${c.red("\u2717")} ${body.error}`);
3603
+ process.exit(1);
3604
+ }
3605
+ const { manifest, published } = body;
3606
+ console.log(` ${c.green("\u2713")} ${manifest.frames.length} frame${manifest.frames.length === 1 ? "" : "s"} frozen: no scripts, no API calls.`);
3607
+ 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.`);
3608
+ if (manifest.missing.length) console.log(` ${c.yellow("!")} ${manifest.missing.length} asset${manifest.missing.length === 1 ? "" : "s"} couldn't be fetched.`);
3609
+ if (published?.skipped.length) console.log(` ${c.yellow("!")} Left out (over 4.4 MB): ${published.skipped.join(", ")}`);
3610
+ if (published) {
3611
+ console.log(` ${c.green("\u2713")} Shared: ${c.bold(published.url)} ${c.dim(`(version ${published.versions}${published.password ? ", password protected" : ""})`)}
3612
+ `);
3613
+ if (values.open && !values["no-open"]) openBrowser(published.url);
3614
+ } else {
3615
+ console.log(` ${c.dim("Preview")} ${c.bold(body.preview)}`);
3616
+ if (!values.local) console.log(` ${c.dim("To send links to clients, connect your review site:")} ${c.accent("npx truecanvas share setup")}`);
3617
+ console.log("");
3618
+ if (values.open && !values["no-open"]) openBrowser(body.preview);
3619
+ }
3620
+ return;
3621
+ }
2944
3622
  if (command === "mcp") {
2945
3623
  console.log = console.error;
2946
3624
  const mcpUrl = `http://localhost:${config.port}/mcp`;
@@ -3043,9 +3721,17 @@ async function openInWindow(root, port) {
3043
3721
  const post = (route, body) => fetch(`${hub}${route}`, { method: "POST", headers: { "content-type": "application/json", origin: hub }, body: JSON.stringify(body) }).catch(() => null);
3044
3722
  await post("/api/hub/add", { path: root });
3045
3723
  await post("/api/hub/open", { path: root, focus: "1" });
3046
- console.log(`${c.accent("\u25C6")} Opened ${c.bold(path11.basename(root))} in the Truecanvas window ${c.dim(`(${hub})`)}.`);
3724
+ console.log(`${c.accent("\u25C6")} Opened ${c.bold(path14.basename(root))} in the Truecanvas window ${c.dim(`(${hub})`)}.`);
3047
3725
  return true;
3048
3726
  }
3727
+ async function prompt(question) {
3728
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3729
+ try {
3730
+ return (await rl.question(` ${question} `)).trim();
3731
+ } finally {
3732
+ rl.close();
3733
+ }
3734
+ }
3049
3735
  async function ask(question) {
3050
3736
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3051
3737
  try {
@@ -3059,7 +3745,7 @@ async function ensureSetup(root, pkgDir, version, yes, force = false) {
3059
3745
  const tc = installedIn(root);
3060
3746
  const wrapped = configStatus(root).wrapped;
3061
3747
  if (tc.declared && tc.installed && wrapped && !force) return true;
3062
- const name = readPackage(root)?.name ?? path11.basename(root);
3748
+ const name = readPackage(root)?.name ?? path14.basename(root);
3063
3749
  if (!yes) {
3064
3750
  if (!process.stdin.isTTY) {
3065
3751
  console.error(`Truecanvas isn't set up in ${name} yet. Run again with --yes to set it up.`);
@@ -3150,11 +3836,11 @@ function startApp(root, appUrl, framework) {
3150
3836
  }
3151
3837
  function binScript(root, pkg) {
3152
3838
  try {
3153
- const req = createRequire(path11.join(root, "package.json"));
3839
+ const req = createRequire(path14.join(root, "package.json"));
3154
3840
  const manifest = req.resolve(`${pkg}/package.json`);
3155
- const { bin } = JSON.parse(fs11.readFileSync(manifest, "utf8"));
3841
+ const { bin } = JSON.parse(fs13.readFileSync(manifest, "utf8"));
3156
3842
  const rel = typeof bin === "string" ? bin : bin?.[pkg];
3157
- return rel ? path11.join(path11.dirname(manifest), rel) : null;
3843
+ return rel ? path14.join(path14.dirname(manifest), rel) : null;
3158
3844
  } catch {
3159
3845
  return null;
3160
3846
  }