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