truecanvas 0.2.0 → 0.3.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 +54 -183
- package/dist/catalog-worker.js +1 -1
- package/dist/chunk-PBKX3SBC.js +97 -0
- package/dist/chunk-TSHD2S36.js +28 -0
- package/dist/{chunk-T2TCBSTG.js → chunk-WU7NBTJH.js} +25 -18
- package/dist/{chunk-ZPARY6OD.js → chunk-XWQ5MS24.js} +362 -335
- package/dist/cli.js +105 -59
- package/dist/core/index.d.ts +42 -9
- package/dist/core/index.js +16 -25
- package/dist/editor/assets/hub-UhU8yp5F.js +2 -0
- package/dist/editor/assets/{main-DlADROMw.js → main-UtSaCfnK.js} +2 -2
- package/dist/editor/hub.html +1 -1
- package/dist/editor/index.html +1 -1
- package/dist/next/index.js +3 -2
- package/dist/vite/index.d.ts +33 -0
- package/dist/vite/index.js +81 -0
- package/package.json +15 -6
- package/dist/editor/assets/hub-BnbEAnt6.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
assertCanvasName,
|
|
8
8
|
clearCompare,
|
|
9
9
|
componentsDir,
|
|
10
|
+
configStatus,
|
|
10
11
|
describeComponent,
|
|
11
12
|
detectPm,
|
|
12
13
|
findByPath,
|
|
@@ -16,8 +17,6 @@ import {
|
|
|
16
17
|
initProject,
|
|
17
18
|
layoutFiles,
|
|
18
19
|
listRoutes,
|
|
19
|
-
loadConfig,
|
|
20
|
-
nextConfigStatus,
|
|
21
20
|
outlineDoc,
|
|
22
21
|
parseCanvas,
|
|
23
22
|
readDesignTokens,
|
|
@@ -26,11 +25,15 @@ import {
|
|
|
26
25
|
shadcnAdd,
|
|
27
26
|
shadcnRegistry,
|
|
28
27
|
shadcnStatus,
|
|
28
|
+
shellSafe,
|
|
29
29
|
syncComponentRegistry,
|
|
30
30
|
toolEnv,
|
|
31
31
|
writeCompare
|
|
32
|
-
} from "./chunk-
|
|
33
|
-
import
|
|
32
|
+
} from "./chunk-XWQ5MS24.js";
|
|
33
|
+
import {
|
|
34
|
+
loadConfig
|
|
35
|
+
} from "./chunk-PBKX3SBC.js";
|
|
36
|
+
import "./chunk-WU7NBTJH.js";
|
|
34
37
|
import {
|
|
35
38
|
ICON_LIBRARIES,
|
|
36
39
|
iconLibraries,
|
|
@@ -577,7 +580,7 @@ ${source.slice(hit.node.start, hit.node.end)}
|
|
|
577
580
|
async ({ name }) => {
|
|
578
581
|
try {
|
|
579
582
|
ws.createCanvas(name, actor());
|
|
580
|
-
return text(`\u2713 Created canvas ${name} (${ws.relFile(name)}).
|
|
583
|
+
return text(`\u2713 Created canvas ${name} (${ws.relFile(name)}). The app picks it up in a second.`);
|
|
581
584
|
} catch (e) {
|
|
582
585
|
return fail(e);
|
|
583
586
|
}
|
|
@@ -936,17 +939,27 @@ import os from "os";
|
|
|
936
939
|
import path2 from "path";
|
|
937
940
|
function findChromium() {
|
|
938
941
|
if (process.env.TRUECANVAS_CHROME) return process.env.TRUECANVAS_CHROME;
|
|
939
|
-
const cache2 = path2.join(os.homedir(), ".cache", "ms-playwright");
|
|
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"));
|
|
940
943
|
if (fs2.existsSync(cache2)) {
|
|
941
944
|
const shells = fs2.readdirSync(cache2).filter((d) => d.startsWith("chromium_headless_shell-")).sort().reverse();
|
|
942
945
|
for (const d of shells) {
|
|
943
|
-
for (const sub of [
|
|
946
|
+
for (const sub of [
|
|
947
|
+
"chrome-headless-shell-linux64/chrome-headless-shell",
|
|
948
|
+
"chrome-linux/headless_shell",
|
|
949
|
+
"chrome-headless-shell-mac-arm64/chrome-headless-shell",
|
|
950
|
+
"chrome-headless-shell-mac-x64/chrome-headless-shell",
|
|
951
|
+
"chrome-headless-shell-win64/chrome-headless-shell.exe",
|
|
952
|
+
"chrome-win/headless_shell.exe"
|
|
953
|
+
]) {
|
|
944
954
|
const p = path2.join(cache2, d, sub);
|
|
945
955
|
if (fs2.existsSync(p)) return p;
|
|
946
956
|
}
|
|
947
957
|
}
|
|
948
958
|
}
|
|
959
|
+
const programFiles = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(Boolean);
|
|
949
960
|
for (const p of [
|
|
961
|
+
...programFiles.flatMap((dir) => [path2.join(dir, "Google", "Chrome", "Application", "chrome.exe"), path2.join(dir, "Microsoft", "Edge", "Application", "msedge.exe")]),
|
|
962
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
950
963
|
"/usr/bin/chromium-browser",
|
|
951
964
|
"/usr/bin/chromium",
|
|
952
965
|
"/usr/bin/google-chrome-stable",
|
|
@@ -1104,7 +1117,7 @@ async function reviewPage(ws, canvas, ref, since) {
|
|
|
1104
1117
|
}
|
|
1105
1118
|
}
|
|
1106
1119
|
}
|
|
1107
|
-
const comments =
|
|
1120
|
+
const comments = `${ws.config.canvasDir}/${canvas}.comments.json`;
|
|
1108
1121
|
if (await ws.git.show(ref, comments) !== readNow(ws, comments)) files.push(comments);
|
|
1109
1122
|
if (!files.length) return null;
|
|
1110
1123
|
const history = exists ? ws.changesSince(canvas, since) : [];
|
|
@@ -1132,7 +1145,7 @@ async function reviewAll(ws) {
|
|
|
1132
1145
|
const linked = new Set(ws.linkedFiles());
|
|
1133
1146
|
const out = [];
|
|
1134
1147
|
for (const canvas of names) {
|
|
1135
|
-
const own = [ws.relFile(canvas),
|
|
1148
|
+
const own = [ws.relFile(canvas), `${ws.config.canvasDir}/${canvas}.comments.json`];
|
|
1136
1149
|
const touchesLinked = [...linked].some((f) => changed.has(f));
|
|
1137
1150
|
if (!own.some((f) => changed.has(f)) && !touchesLinked) continue;
|
|
1138
1151
|
const page = await reviewPage(ws, canvas, ref, since).catch(() => null);
|
|
@@ -1299,6 +1312,7 @@ async function startServer(ws) {
|
|
|
1299
1312
|
case "GET /api/state":
|
|
1300
1313
|
return json(res, 200, {
|
|
1301
1314
|
appUrl: config.appUrl,
|
|
1315
|
+
framework: config.framework,
|
|
1302
1316
|
root: config.root,
|
|
1303
1317
|
projectName: path4.basename(config.root),
|
|
1304
1318
|
canvases: ws.canvases(),
|
|
@@ -1669,7 +1683,7 @@ async function startServer(ws) {
|
|
|
1669
1683
|
watchCanvasDir();
|
|
1670
1684
|
setInterval(watchCanvasDir, 2e3).unref();
|
|
1671
1685
|
const appDirAbs = path4.join(config.root, config.appDir);
|
|
1672
|
-
if (fs4.existsSync(appDirAbs)) {
|
|
1686
|
+
if (config.framework === "next" && fs4.existsSync(appDirAbs)) {
|
|
1673
1687
|
const appPending = /* @__PURE__ */ new Map();
|
|
1674
1688
|
const appWatcher = fs4.watch(appDirAbs, { recursive: true }, (_event, filename) => {
|
|
1675
1689
|
if (!filename || !/(^|[\\/])(page|layout)\.[jt]sx$/.test(filename) || /^truecanvas[\\/]/.test(filename)) return;
|
|
@@ -1765,6 +1779,9 @@ async function runStdioBridge(mcpUrl) {
|
|
|
1765
1779
|
await server.connect(new StdioServerTransport());
|
|
1766
1780
|
}
|
|
1767
1781
|
|
|
1782
|
+
// src/cli/index.ts
|
|
1783
|
+
import { createRequire } from "module";
|
|
1784
|
+
|
|
1768
1785
|
// src/hub/server.ts
|
|
1769
1786
|
import http2 from "http";
|
|
1770
1787
|
import fs8 from "fs";
|
|
@@ -1835,7 +1852,7 @@ function inspect(dir) {
|
|
|
1835
1852
|
} catch {
|
|
1836
1853
|
}
|
|
1837
1854
|
}
|
|
1838
|
-
return { next: "next" in deps, ready: "truecanvas" in deps, canvases, exists: true };
|
|
1855
|
+
return { next: "next" in deps || "vite" in deps && "react" in deps, ready: "truecanvas" in deps, canvases, exists: true };
|
|
1839
1856
|
}
|
|
1840
1857
|
function discover() {
|
|
1841
1858
|
const home = os2.homedir();
|
|
@@ -1864,7 +1881,7 @@ function discover() {
|
|
|
1864
1881
|
return found;
|
|
1865
1882
|
}
|
|
1866
1883
|
function listDirs(dir) {
|
|
1867
|
-
const abs = path5.resolve(dir.replace(/^~(
|
|
1884
|
+
const abs = path5.resolve(dir.replace(/^~(?=$|[\\/])/, os2.homedir()));
|
|
1868
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));
|
|
1869
1886
|
return { path: abs, dirs };
|
|
1870
1887
|
}
|
|
@@ -1889,7 +1906,8 @@ function runScript(pm, script) {
|
|
|
1889
1906
|
return pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
|
|
1890
1907
|
}
|
|
1891
1908
|
var execInherit = (cmd, args, cwd) => new Promise((resolve, reject) => {
|
|
1892
|
-
const
|
|
1909
|
+
const safe = shellSafe(cmd, args);
|
|
1910
|
+
const p = spawn(safe.cmd, safe.args, { cwd, stdio: "inherit", env: toolEnv(), shell: safe.shell });
|
|
1893
1911
|
p.on("error", reject);
|
|
1894
1912
|
p.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} exited with ${code}`)));
|
|
1895
1913
|
});
|
|
@@ -2120,7 +2138,8 @@ var Runner = class {
|
|
|
2120
2138
|
};
|
|
2121
2139
|
function killTree(proc, signal) {
|
|
2122
2140
|
try {
|
|
2123
|
-
if (process.platform
|
|
2141
|
+
if (process.platform === "win32" && proc.pid) spawn2("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { stdio: "ignore" }).on("error", () => proc.kill());
|
|
2142
|
+
else if (proc.pid) process.kill(-proc.pid, signal);
|
|
2124
2143
|
else proc.kill(signal);
|
|
2125
2144
|
} catch {
|
|
2126
2145
|
proc.kill(signal);
|
|
@@ -2144,9 +2163,10 @@ async function install(job, cmd, args, cwd) {
|
|
|
2144
2163
|
function run2(job, cmd, args, cwd) {
|
|
2145
2164
|
job.log.push(`$ ${cmd} ${args.join(" ")}`);
|
|
2146
2165
|
return new Promise((resolve, reject) => {
|
|
2147
|
-
const
|
|
2166
|
+
const safe = shellSafe(cmd, args);
|
|
2167
|
+
const p = spawn2(safe.cmd, safe.args, { cwd, env: toolEnv({ CI: "1", FORCE_COLOR: "0" }), stdio: ["ignore", "pipe", "pipe"], shell: safe.shell });
|
|
2148
2168
|
const capture2 = (c2) => {
|
|
2149
|
-
for (const line of c2.toString().split(
|
|
2169
|
+
for (const line of c2.toString().split(/\r?\n/)) if (line.trim()) job.log.push(line.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, ""));
|
|
2150
2170
|
if (job.log.length > 300) job.log.splice(0, job.log.length - 300);
|
|
2151
2171
|
};
|
|
2152
2172
|
p.stdout.on("data", capture2);
|
|
@@ -2186,7 +2206,7 @@ function setupJob(dir, pkgDir, cli) {
|
|
|
2186
2206
|
}
|
|
2187
2207
|
function createJob(name, parent, pkgDir, cli) {
|
|
2188
2208
|
const safe = name.trim().toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2189
|
-
const parentDir = path7.resolve(parent.replace(/^~(
|
|
2209
|
+
const parentDir = path7.resolve(parent.replace(/^~(?=$|[\\/])/, os4.homedir()));
|
|
2190
2210
|
const dir = path7.join(parentDir, safe);
|
|
2191
2211
|
return startJob(`Create ${safe}`, async (job) => {
|
|
2192
2212
|
if (!safe) throw new Error("Pick a project name.");
|
|
@@ -2202,16 +2222,17 @@ function createJob(name, parent, pkgDir, cli) {
|
|
|
2202
2222
|
}
|
|
2203
2223
|
function capture(cmd, args, cwd) {
|
|
2204
2224
|
return new Promise((resolve) => {
|
|
2225
|
+
const safe = shellSafe(cmd, args);
|
|
2205
2226
|
execFile(
|
|
2206
|
-
cmd,
|
|
2207
|
-
args,
|
|
2208
|
-
{ cwd, timeout: 6e4, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, GH_PROMPT_DISABLED: "1", GIT_TERMINAL_PROMPT: "0" } },
|
|
2227
|
+
safe.cmd,
|
|
2228
|
+
safe.args,
|
|
2229
|
+
{ shell: safe.shell, cwd, timeout: 6e4, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, GH_PROMPT_DISABLED: "1", GIT_TERMINAL_PROMPT: "0" } },
|
|
2209
2230
|
(err, stdout, stderr) => resolve({ ok: !err, out: err ? (stderr || err.message).trim() : stdout })
|
|
2210
2231
|
);
|
|
2211
2232
|
});
|
|
2212
2233
|
}
|
|
2213
2234
|
function cloneJob(repo, parent, pkgDir, cli) {
|
|
2214
|
-
const parentDir = path7.resolve(parent.replace(/^~(
|
|
2235
|
+
const parentDir = path7.resolve(parent.replace(/^~(?=$|[\\/])/, os4.homedir()));
|
|
2215
2236
|
const name = repo.replace(/\.git$/, "").split(/[/:]/).pop() ?? "";
|
|
2216
2237
|
const dir = path7.join(parentDir, name);
|
|
2217
2238
|
return startJob(`Clone ${name}`, async (job) => {
|
|
@@ -2221,7 +2242,7 @@ function cloneJob(repo, parent, pkgDir, cli) {
|
|
|
2221
2242
|
const url = /^(https?:|git@|ssh:|file:)/.test(repo);
|
|
2222
2243
|
if (url) await run2(job, "git", ["clone", repo, dir], parentDir);
|
|
2223
2244
|
else await run2(job, "gh", ["repo", "clone", repo, dir], parentDir);
|
|
2224
|
-
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 app.");
|
|
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.");
|
|
2225
2246
|
const pm = packageManager(dir);
|
|
2226
2247
|
await install(job, pm.cmd, ["install"], dir).catch(async (err) => {
|
|
2227
2248
|
if (!fs7.existsSync(path7.join(dir, "node_modules"))) throw err;
|
|
@@ -2245,7 +2266,7 @@ var MIME2 = {
|
|
|
2245
2266
|
".woff2": "font/woff2",
|
|
2246
2267
|
".png": "image/png"
|
|
2247
2268
|
};
|
|
2248
|
-
var HUB_INSTRUCTIONS = `Truecanvas hub: several projects, each a Next.js app with canvases of real React components.
|
|
2269
|
+
var HUB_INSTRUCTIONS = `Truecanvas hub: several projects, each a Next.js or Vite + React app with canvases of real React components.
|
|
2249
2270
|
Your tools act on the current project (the active tab in the Truecanvas window, unless you pick one with open_project).
|
|
2250
2271
|
Use list_projects to see them. All other tools are the regular Truecanvas canvas tools for that project.`;
|
|
2251
2272
|
async function startHub(opts) {
|
|
@@ -2423,8 +2444,8 @@ async function startHub(opts) {
|
|
|
2423
2444
|
req.on("close", () => clients.delete(res));
|
|
2424
2445
|
return;
|
|
2425
2446
|
case "POST /api/hub/add": {
|
|
2426
|
-
const dir = path8.resolve(body.path.replace(/^~(
|
|
2427
|
-
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 app." });
|
|
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." });
|
|
2428
2449
|
store.touch(dir);
|
|
2429
2450
|
broadcast();
|
|
2430
2451
|
return json2(res, 200, { path: dir, ready: store.all().find((p) => p.path === dir)?.ready ?? false });
|
|
@@ -2701,35 +2722,39 @@ async function doctor(root, cliVersion) {
|
|
|
2701
2722
|
const checks = [];
|
|
2702
2723
|
const add = (c2) => checks.push(c2);
|
|
2703
2724
|
const node = process.versions.node;
|
|
2704
|
-
add(major(node) >=
|
|
2725
|
+
add(major(node) >= 22 ? { label: "Node.js", status: "ok", detail: node } : { label: "Node.js", status: "fail", detail: node, fix: "Install Node.js 22 or newer (24 LTS recommended: https://nodejs.org)" });
|
|
2705
2726
|
const pkg = readPackage(root);
|
|
2706
2727
|
if (!pkg) {
|
|
2707
|
-
add({ label: "Project", status: "fail", detail: "no package.json here", fix: "Run truecanvas from the root of your Next.js app, or create one: npm create truecanvas" });
|
|
2728
|
+
add({ label: "Project", status: "fail", detail: "no package.json here", fix: "Run truecanvas from the root of your Next.js or Vite app, or create one: npm create truecanvas" });
|
|
2708
2729
|
return checks;
|
|
2709
2730
|
}
|
|
2710
2731
|
const pm = detectPm(root);
|
|
2711
2732
|
const next = depVersion(root, "next");
|
|
2712
|
-
|
|
2733
|
+
const vite = !next ? depVersion(root, "vite") : null;
|
|
2734
|
+
if (vite) add(major(vite) >= 5 ? { label: "Vite", status: "ok", detail: vite } : { label: "Vite", status: "fail", detail: vite, fix: `Upgrade to Vite 5 or newer: ${pm} ${pm === "npm" ? "install -D" : "add -D"} vite@latest` });
|
|
2735
|
+
else if (!next) add({ label: "Next.js or Vite", status: "fail", detail: "neither is a dependency", fix: "Truecanvas works with Next.js (App Router) and Vite + React apps. Create one: npm create truecanvas" });
|
|
2713
2736
|
else if (major(next) < 15) add({ label: "Next.js", status: "fail", detail: next, fix: `Upgrade to Next.js 15 or newer: ${pm} ${pm === "npm" ? "install" : "add"} next@latest` });
|
|
2714
2737
|
else add({ label: "Next.js", status: "ok", detail: next });
|
|
2715
2738
|
const react = depVersion(root, "react");
|
|
2716
|
-
add(react && major(react) >= 19 ? { label: "React", status: "ok", detail: react } : { label: "React", status: "fail", detail: react ?? "missing", fix:
|
|
2739
|
+
add(react && major(react) >= 19 ? { label: "React", status: "ok", detail: react } : { label: "React", status: "fail", detail: react ?? "missing", fix: `Truecanvas needs React 19: ${pm} ${pm === "npm" ? "install" : "add"} react@latest react-dom@latest` });
|
|
2717
2740
|
const tw = depVersion(root, "tailwindcss");
|
|
2718
2741
|
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" });
|
|
2719
2742
|
const config = loadConfig(root);
|
|
2720
|
-
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/)" });
|
|
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/)" });
|
|
2721
2744
|
const tc = installedIn(root);
|
|
2722
2745
|
if (!tc.declared) add({ label: "Truecanvas in the project", status: "fail", detail: "not a dependency", fix: "npx truecanvas (sets it up)" });
|
|
2723
2746
|
else if (!tc.installed) add({ label: "Truecanvas in the project", status: "fail", detail: "declared but not installed", fix: `${pm} install` });
|
|
2724
2747
|
else if (tc.version !== cliVersion) add({ label: "Truecanvas in the project", status: "warn", detail: `${tc.version} (this CLI is ${cliVersion})`, fix: `Use the project's version: ${runScript(pm, "canvas")}, or update: ${pm} ${pm === "npm" ? "install -D" : "add -D"} truecanvas@latest` });
|
|
2725
2748
|
else add({ label: "Truecanvas in the project", status: "ok", detail: tc.version ?? "" });
|
|
2726
|
-
const wrapped =
|
|
2727
|
-
|
|
2749
|
+
const wrapped = configStatus(root, config.framework);
|
|
2750
|
+
const configLabel = config.framework === "vite" ? "vite.config" : "next.config";
|
|
2751
|
+
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" });
|
|
2728
2753
|
const scripts = pkg.scripts ?? {};
|
|
2729
2754
|
const script = Object.entries(scripts).find(([, v]) => v.includes("truecanvas"));
|
|
2730
2755
|
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")' });
|
|
2731
2756
|
const ignore = fs10.existsSync(path10.join(root, ".gitignore")) ? fs10.readFileSync(path10.join(root, ".gitignore"), "utf8") : "";
|
|
2732
|
-
add(ignore.includes(
|
|
2757
|
+
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" });
|
|
2733
2758
|
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)" });
|
|
2734
2759
|
const chrome = findChromium();
|
|
2735
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)" });
|
|
@@ -2746,14 +2771,14 @@ async function doctor(root, cliVersion) {
|
|
|
2746
2771
|
var HELP = `truecanvas: design with your real React components
|
|
2747
2772
|
|
|
2748
2773
|
Usage
|
|
2749
|
-
truecanvas In a Next.js app: set it up (asks once) and start the editor.
|
|
2774
|
+
truecanvas In a Next.js or Vite app: set it up (asks once) and start the editor.
|
|
2750
2775
|
Elsewhere: open the Truecanvas window.
|
|
2751
2776
|
truecanvas connect Add Truecanvas to this project's .mcp.json (Claude Code, Cursor,
|
|
2752
2777
|
VS Code): commit it and every teammate's agent connects
|
|
2753
2778
|
truecanvas doctor Check this project and machine, with a fix for each problem
|
|
2754
2779
|
truecanvas dev Start the editor + MCP server for this project (no window)
|
|
2755
2780
|
truecanvas setup Install and set up Truecanvas in this project, without starting
|
|
2756
|
-
truecanvas init Only write the config (next
|
|
2781
|
+
truecanvas init Only write the config (next/vite config, script, canvas folder)
|
|
2757
2782
|
truecanvas open Open the Truecanvas window (projects dashboard + tabs)
|
|
2758
2783
|
truecanvas quit Quit the window and every app it started
|
|
2759
2784
|
truecanvas desktop Add Truecanvas to your app launcher (Linux); "desktop remove" undoes it
|
|
@@ -2762,8 +2787,8 @@ Usage
|
|
|
2762
2787
|
Options
|
|
2763
2788
|
-y, --yes Don't ask: set up and connect agents
|
|
2764
2789
|
--port <n> Editor/MCP port (default 4800)
|
|
2765
|
-
--app <url> Your
|
|
2766
|
-
--no-next Don't start
|
|
2790
|
+
--app <url> Your app's dev URL (default http://localhost:3000, Vite :5173)
|
|
2791
|
+
--no-next Don't start the app's dev server (by default it starts when the app isn't running)
|
|
2767
2792
|
--no-open Don't open the editor in your browser
|
|
2768
2793
|
--cursor, --vscode connect: also write .cursor/mcp.json / .vscode/mcp.json
|
|
2769
2794
|
-v, --version Print the version
|
|
@@ -2807,7 +2832,8 @@ async function main() {
|
|
|
2807
2832
|
const yes = !!values.yes;
|
|
2808
2833
|
if (command === "start") {
|
|
2809
2834
|
const root2 = process.cwd();
|
|
2810
|
-
|
|
2835
|
+
const deps = { ...readPackage(root2)?.dependencies, ...readPackage(root2)?.devDependencies };
|
|
2836
|
+
if (!("next" in deps) && !("vite" in deps && "react" in deps)) {
|
|
2811
2837
|
return openWindow(cli, values.port ? Number(values.port) : 4800);
|
|
2812
2838
|
}
|
|
2813
2839
|
if (!await ensureSetup(root2, pkgDir, version, yes)) return;
|
|
@@ -2881,6 +2907,7 @@ async function main() {
|
|
|
2881
2907
|
return console.log(`${c.green("\u2713")} Truecanvas quit. Project apps it started are stopped too.`);
|
|
2882
2908
|
}
|
|
2883
2909
|
if (command === "desktop") {
|
|
2910
|
+
if (process.platform !== "linux") return console.log(`The app launcher entry is Linux-only for now. Run ${c.accent("truecanvas open")} to open the window.`);
|
|
2884
2911
|
if (positionals[1] === "remove") {
|
|
2885
2912
|
const removed = uninstallDesktop();
|
|
2886
2913
|
console.log(removed.length ? `${c.green("\u2713")} Removed the launcher.` : "Nothing to remove.");
|
|
@@ -2895,7 +2922,7 @@ async function main() {
|
|
|
2895
2922
|
}
|
|
2896
2923
|
const root = process.cwd();
|
|
2897
2924
|
if (!fs11.existsSync(path11.join(root, "package.json"))) {
|
|
2898
|
-
console.error("Run truecanvas from your
|
|
2925
|
+
console.error("Run truecanvas from your app's root (no package.json here).");
|
|
2899
2926
|
process.exit(1);
|
|
2900
2927
|
}
|
|
2901
2928
|
const config = loadConfig(root, { port: values.port ? Number(values.port) : void 0, appUrl: values.app });
|
|
@@ -2907,7 +2934,7 @@ async function main() {
|
|
|
2907
2934
|
${c.accent("\u25C6")} ${c.bold("Truecanvas")} is set up in ${c.bold(path11.basename(root))}
|
|
2908
2935
|
`);
|
|
2909
2936
|
for (const line of report.done) console.log(` ${c.green("\u2713")} ${line}`);
|
|
2910
|
-
console.log(` ${c.green("\u2713")} Canvas folder ${c.bold(config.canvasDir)} and dev route ${c.bold(
|
|
2937
|
+
console.log(` ${c.green("\u2713")} Canvas folder ${c.bold(config.canvasDir)} and dev route ${c.bold(config.routeDir)}`);
|
|
2911
2938
|
for (const line of report.todo) console.log(` ${c.dim("!")} ${line}`);
|
|
2912
2939
|
console.log(`
|
|
2913
2940
|
Next: ${c.accent("npm run canvas")} ${c.dim("(starts your app and the editor)")}
|
|
@@ -2934,7 +2961,7 @@ async function main() {
|
|
|
2934
2961
|
process.exit(1);
|
|
2935
2962
|
}
|
|
2936
2963
|
if (command === "dev" && await openInWindow(root, config.port)) return;
|
|
2937
|
-
const status =
|
|
2964
|
+
const status = configStatus(root, config.framework);
|
|
2938
2965
|
if (!status.wrapped) {
|
|
2939
2966
|
const report = initProject(config);
|
|
2940
2967
|
for (const line of report.done) console.log(` ${c.green("\u2713")} ${line}`);
|
|
@@ -2955,13 +2982,14 @@ async function main() {
|
|
|
2955
2982
|
const mcp = `${origin}/mcp`;
|
|
2956
2983
|
let next = null;
|
|
2957
2984
|
const appUp = await reachable(config.appUrl);
|
|
2958
|
-
|
|
2985
|
+
const devCommand = config.framework === "vite" ? "vite" : "next dev";
|
|
2986
|
+
if (!appUp && values.next && !values["no-next"]) next = startApp(root, config.appUrl, config.framework);
|
|
2959
2987
|
console.log(`
|
|
2960
2988
|
${c.accent("\u25C6")} ${c.bold("Truecanvas")} ${c.dim(`v${version}`)}
|
|
2961
2989
|
|
|
2962
2990
|
${c.dim("Editor")} ${c.bold(origin)}
|
|
2963
2991
|
${c.dim("MCP")} ${mcp}
|
|
2964
|
-
${c.dim("App")} ${config.appUrl} ${c.dim(appUp ? "(already running)" : next ?
|
|
2992
|
+
${c.dim("App")} ${config.appUrl} ${c.dim(appUp ? "(already running)" : next ? `(starting ${devCommand}\u2026)` : "(not running)")}
|
|
2965
2993
|
${c.dim("Canvases")} ${ws.canvases().join(", ") || "none"}
|
|
2966
2994
|
|
|
2967
2995
|
${hasAgentConfig(root, config.port) ? ` ${c.dim("Agents")} connected through .mcp.json` : ` ${c.dim("Connect your agent:")} ${c.accent("npx truecanvas connect")} ${c.dim("(writes .mcp.json; commit it for your team)")}`}
|
|
@@ -2971,9 +2999,9 @@ ${hasAgentConfig(root, config.port) ? ` ${c.dim("Agents")} connected through
|
|
|
2971
2999
|
const result = await Promise.race([waitFor(config.appUrl, 9e4), exited]);
|
|
2972
3000
|
if (result === true) console.log(` ${c.green("\u2713")} App ready at ${config.appUrl}
|
|
2973
3001
|
`);
|
|
2974
|
-
else if (result === "exited") console.log(` ${c.dim(
|
|
3002
|
+
else if (result === "exited") console.log(` ${c.dim(`${devCommand} stopped (see above). Start your app, the editor connects as soon as it's up.`)}
|
|
2975
3003
|
`);
|
|
2976
|
-
else console.log(` ${c.dim(
|
|
3004
|
+
else console.log(` ${c.dim(`${devCommand} is taking a while. The editor will connect when it's up.`)}
|
|
2977
3005
|
`);
|
|
2978
3006
|
}
|
|
2979
3007
|
if (values.open && !values["no-open"]) openBrowser(origin);
|
|
@@ -2982,15 +3010,15 @@ ${hasAgentConfig(root, config.port) ? ` ${c.dim("Agents")} connected through
|
|
|
2982
3010
|
if (stopping) return;
|
|
2983
3011
|
stopping = true;
|
|
2984
3012
|
const nextGone = next && next.exitCode === null ? new Promise((r) => next.once("exit", r)) : Promise.resolve();
|
|
2985
|
-
next
|
|
3013
|
+
if (next) stopTree(next, "SIGINT");
|
|
2986
3014
|
await Promise.all([shutdown(), Promise.race([nextGone, new Promise((r) => setTimeout(r, 4e3))])]);
|
|
2987
|
-
if (next && next.exitCode === null) next
|
|
3015
|
+
if (next && next.exitCode === null) stopTree(next, "SIGKILL");
|
|
2988
3016
|
process.exit(0);
|
|
2989
3017
|
};
|
|
2990
3018
|
process.on("SIGINT", stop);
|
|
2991
3019
|
process.on("SIGTERM", stop);
|
|
2992
3020
|
next?.on("exit", (code) => {
|
|
2993
|
-
if (!stopping) console.log(` ${c.dim(
|
|
3021
|
+
if (!stopping) console.log(` ${c.dim(`${devCommand} exited (${code ?? "signal"}).`)}`);
|
|
2994
3022
|
});
|
|
2995
3023
|
}
|
|
2996
3024
|
async function openWindow(cli, port) {
|
|
@@ -3029,7 +3057,7 @@ async function ask(question) {
|
|
|
3029
3057
|
}
|
|
3030
3058
|
async function ensureSetup(root, pkgDir, version, yes, force = false) {
|
|
3031
3059
|
const tc = installedIn(root);
|
|
3032
|
-
const wrapped =
|
|
3060
|
+
const wrapped = configStatus(root).wrapped;
|
|
3033
3061
|
if (tc.declared && tc.installed && wrapped && !force) return true;
|
|
3034
3062
|
const name = readPackage(root)?.name ?? path11.basename(root);
|
|
3035
3063
|
if (!yes) {
|
|
@@ -3040,7 +3068,7 @@ async function ensureSetup(root, pkgDir, version, yes, force = false) {
|
|
|
3040
3068
|
console.log(`
|
|
3041
3069
|
${c.accent("\u25C6")} ${c.bold("Truecanvas")} ${c.dim(`v${version}`)}
|
|
3042
3070
|
`);
|
|
3043
|
-
console.log(` This adds Truecanvas to ${c.bold(name)}: a dev dependency, a
|
|
3071
|
+
console.log(` This adds Truecanvas to ${c.bold(name)}: a dev dependency, a plugin in ${configStatus(root).framework === "vite" ? "vite.config" : "next.config"},`);
|
|
3044
3072
|
console.log(` a ${c.bold("canvas")} script and a canvas with your homepage. Nothing changes in production builds.
|
|
3045
3073
|
`);
|
|
3046
3074
|
if (!await ask("Set it up?")) return false;
|
|
@@ -3098,19 +3126,21 @@ async function waitFor(url, ms) {
|
|
|
3098
3126
|
}
|
|
3099
3127
|
return false;
|
|
3100
3128
|
}
|
|
3101
|
-
function
|
|
3102
|
-
const port = new URL(appUrl).port || "3000";
|
|
3103
|
-
const
|
|
3104
|
-
const
|
|
3105
|
-
const
|
|
3106
|
-
const
|
|
3107
|
-
|
|
3108
|
-
const
|
|
3129
|
+
function startApp(root, appUrl, framework) {
|
|
3130
|
+
const port = new URL(appUrl).port || (framework === "vite" ? "5173" : "3000");
|
|
3131
|
+
const devArgs = framework === "vite" ? ["--port", port, "--strictPort", "--host", "127.0.0.1"] : ["dev", "-p", port, "-H", "127.0.0.1"];
|
|
3132
|
+
const pkg = framework === "vite" ? "vite" : "next";
|
|
3133
|
+
const bin = binScript(root, pkg);
|
|
3134
|
+
const [cmd, args] = bin ? [process.execPath, [bin, ...devArgs]] : [process.platform === "win32" ? "npx.cmd" : "npx", [pkg, ...devArgs]];
|
|
3135
|
+
const label = framework === "vite" ? "vite" : "next";
|
|
3136
|
+
const child = spawn3(cmd, args, { cwd: root, env: toolEnv(process.env.NO_COLOR ? {} : { FORCE_COLOR: "1" }), stdio: ["ignore", "pipe", "pipe"], shell: !bin && process.platform === "win32" });
|
|
3137
|
+
child.on("error", (err) => console.error(` ${c.dim(`${label} \u2502`)} couldn't start ${pkg}: ${err.message}`));
|
|
3138
|
+
const prefix = c.dim(`${label} \u2502 `);
|
|
3109
3139
|
for (const stream of [child.stdout, child.stderr]) {
|
|
3110
3140
|
let buf = "";
|
|
3111
3141
|
stream.on("data", (chunk) => {
|
|
3112
3142
|
buf += chunk.toString();
|
|
3113
|
-
const lines = buf.split(
|
|
3143
|
+
const lines = buf.split(/\r?\n/);
|
|
3114
3144
|
buf = lines.pop();
|
|
3115
3145
|
for (const line of lines) if (line.trim()) process.stdout.write(` ${prefix}${line}
|
|
3116
3146
|
`);
|
|
@@ -3118,6 +3148,22 @@ function startNext(root, appUrl) {
|
|
|
3118
3148
|
}
|
|
3119
3149
|
return child;
|
|
3120
3150
|
}
|
|
3151
|
+
function binScript(root, pkg) {
|
|
3152
|
+
try {
|
|
3153
|
+
const req = createRequire(path11.join(root, "package.json"));
|
|
3154
|
+
const manifest = req.resolve(`${pkg}/package.json`);
|
|
3155
|
+
const { bin } = JSON.parse(fs11.readFileSync(manifest, "utf8"));
|
|
3156
|
+
const rel = typeof bin === "string" ? bin : bin?.[pkg];
|
|
3157
|
+
return rel ? path11.join(path11.dirname(manifest), rel) : null;
|
|
3158
|
+
} catch {
|
|
3159
|
+
return null;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
function stopTree(child, signal) {
|
|
3163
|
+
if (process.platform === "win32" && child.pid) spawn3("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }).on("error", () => {
|
|
3164
|
+
});
|
|
3165
|
+
else child.kill(signal);
|
|
3166
|
+
}
|
|
3121
3167
|
function openBrowser(url) {
|
|
3122
3168
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3123
3169
|
try {
|
package/dist/core/index.d.ts
CHANGED
|
@@ -252,16 +252,23 @@ interface DarkMode {
|
|
|
252
252
|
/** Value written for light mode with the attribute strategy. */
|
|
253
253
|
lightValue?: string;
|
|
254
254
|
}
|
|
255
|
+
type Framework = "next" | "vite";
|
|
255
256
|
interface TruecanvasConfig {
|
|
256
257
|
root: string;
|
|
257
|
-
/**
|
|
258
|
+
/** The app's framework: Next.js (App Router) or Vite + React. Detected from package.json. */
|
|
259
|
+
framework: Framework;
|
|
260
|
+
/** Where your app's dev server runs. */
|
|
258
261
|
appUrl: string;
|
|
259
262
|
/** Port of the Truecanvas editor + MCP server. */
|
|
260
263
|
port: number;
|
|
261
264
|
/** Folder holding *.canvas.tsx files. */
|
|
262
265
|
canvasDir: string;
|
|
263
|
-
/** Next.js app directory ("app" or "src/app"). */
|
|
266
|
+
/** Next.js app directory ("app" or "src/app"); for Vite, the source folder. */
|
|
264
267
|
appDir: string;
|
|
268
|
+
/** Generated, dev-only render route (project-relative, gitignored). */
|
|
269
|
+
routeDir: string;
|
|
270
|
+
/** Vite: stylesheets every frame loads (default: the ones your entry module imports). */
|
|
271
|
+
css?: string[];
|
|
265
272
|
/** Globs of component files shown in the Components panel. */
|
|
266
273
|
components: string[];
|
|
267
274
|
darkMode: DarkMode;
|
|
@@ -430,10 +437,11 @@ declare class Git {
|
|
|
430
437
|
/** git reports paths from the repo root; we show them from the project root. */
|
|
431
438
|
private rel;
|
|
432
439
|
/**
|
|
433
|
-
* The project folder
|
|
434
|
-
*
|
|
440
|
+
* The project folder inside the repo ("apps/web/", or "" at the root), as git
|
|
441
|
+
* itself sees it. Asking git instead of comparing file system paths holds up
|
|
442
|
+
* through symlinks (macOS /var → /private/var), junctions and Windows short names.
|
|
435
443
|
*/
|
|
436
|
-
private
|
|
444
|
+
private prefix;
|
|
437
445
|
branches(): Promise<{
|
|
438
446
|
current: string | null;
|
|
439
447
|
local: string[];
|
|
@@ -864,7 +872,7 @@ declare const DEVICES: Device[];
|
|
|
864
872
|
declare function findDevice(id: string | null | undefined): Device | undefined;
|
|
865
873
|
|
|
866
874
|
/**
|
|
867
|
-
* Writes an older version of a canvas next to the generated route so
|
|
875
|
+
* Writes an older version of a canvas next to the generated route so the app can
|
|
868
876
|
* render it like any canvas ("__compare__<name>"). Relative imports are
|
|
869
877
|
* rewritten for the new location; alias imports (@/…) work unchanged.
|
|
870
878
|
*/
|
|
@@ -940,9 +948,16 @@ interface InitReport {
|
|
|
940
948
|
done: string[];
|
|
941
949
|
todo: string[];
|
|
942
950
|
}
|
|
951
|
+
/** Does the app's next.config / vite.config already enable Truecanvas? */
|
|
952
|
+
declare function configStatus(root: string, framework?: Framework): {
|
|
953
|
+
file: string | null;
|
|
954
|
+
wrapped: boolean;
|
|
955
|
+
framework: Framework;
|
|
956
|
+
};
|
|
943
957
|
/**
|
|
944
|
-
* Sets up a Next.js project:
|
|
945
|
-
* ignores the generated route. Every step is idempotent
|
|
958
|
+
* Sets up a Next.js or Vite project: enables the plugin in its config, adds a
|
|
959
|
+
* `canvas` script and ignores the generated route. Every step is idempotent
|
|
960
|
+
* and reported.
|
|
946
961
|
*/
|
|
947
962
|
declare function initProject(config: TruecanvasConfig): InitReport;
|
|
948
963
|
/**
|
|
@@ -951,5 +966,23 @@ declare function initProject(config: TruecanvasConfig): InitReport;
|
|
|
951
966
|
* export can't be found (the user is told to wrap it by hand).
|
|
952
967
|
*/
|
|
953
968
|
declare function wrapConfigExport(src: string): string | null;
|
|
969
|
+
/**
|
|
970
|
+
* Adds `truecanvas()` to a vite.config's plugins array, touching nothing
|
|
971
|
+
* else. Null when there's no plugins array to add it to.
|
|
972
|
+
*/
|
|
973
|
+
declare function addVitePlugin(src: string): string | null;
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Generates the dev-only render route `/truecanvas/[canvas]` (Next: an App
|
|
977
|
+
* Router page; Vite: an entry module the plugin serves) and a registry that
|
|
978
|
+
* lazily imports each canvas. Idempotent.
|
|
979
|
+
*/
|
|
980
|
+
declare function syncRoute(config: TruecanvasConfig): boolean;
|
|
981
|
+
/**
|
|
982
|
+
* Stylesheets a Vite app loads globally: the ones its entry module (the
|
|
983
|
+
* script in index.html) imports. Root-relative ("/src/index.css"), or the
|
|
984
|
+
* package path for a package's stylesheet.
|
|
985
|
+
*/
|
|
986
|
+
declare function viteStylesheets(root: string): string[];
|
|
954
987
|
|
|
955
|
-
export { type Actor, type AttrRange, type CanvasDoc, CanvasEditor, type CanvasFrame, type CanvasNode, Catalog, type Command, Comments, type ComponentSpec, DEVICES, type DarkMode, type Device, EditError, type FrameLink, Git, type HistoryEntry, ICON_LIBRARIES, type Icon, type IconLibrary, type ImportInfo, type NodeKind, type Preset, type PropSpec, type PropValue, type ShadcnStatus, type TruecanvasConfig, Workspace, type WorkspaceEvent, allNodes, attrCode, componentJsx, describeComponent, findDevice, findFrame, findNode, frameChanges, iconLibraries, iconLibrary, initProject, injectIds, loadConfig, loadIcons, outlineDoc, parseCanvas, readDesignTokens, searchIcons, shadcnAdd, shadcnInit, shadcnRegistry, shadcnStatus, textCode, wrapConfigExport, writeCompare };
|
|
988
|
+
export { type Actor, type AttrRange, type CanvasDoc, CanvasEditor, type CanvasFrame, type CanvasNode, Catalog, type Command, Comments, type ComponentSpec, DEVICES, type DarkMode, type Device, EditError, type FrameLink, Git, type HistoryEntry, ICON_LIBRARIES, type Icon, type IconLibrary, type ImportInfo, type NodeKind, type Preset, type PropSpec, type PropValue, type ShadcnStatus, type TruecanvasConfig, Workspace, type WorkspaceEvent, addVitePlugin, allNodes, attrCode, componentJsx, configStatus, describeComponent, findDevice, findFrame, findNode, frameChanges, iconLibraries, iconLibrary, initProject, injectIds, loadConfig, loadIcons, outlineDoc, parseCanvas, readDesignTokens, searchIcons, shadcnAdd, shadcnInit, shadcnRegistry, shadcnStatus, syncRoute, textCode, viteStylesheets, wrapConfigExport, writeCompare };
|