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/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-ZPARY6OD.js";
33
- import "./chunk-T2TCBSTG.js";
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)}). Next.js picks the new route up in a second.`);
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 ["chrome-headless-shell-linux64/chrome-headless-shell", "chrome-linux/headless_shell", "chrome-headless-shell-mac-arm64/chrome-headless-shell", "chrome-headless-shell-mac-x64/chrome-headless-shell"]) {
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 = path3.join(ws.config.canvasDir, `${canvas}.comments.json`);
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), path3.join(ws.config.canvasDir, `${canvas}.comments.json`)];
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(/^~(?=$|\/)/, os2.homedir()));
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 p = spawn(cmd, args, { cwd, stdio: "inherit", env: toolEnv() });
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 !== "win32" && proc.pid) process.kill(-proc.pid, signal);
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 p = spawn2(cmd, args, { cwd, env: toolEnv({ CI: "1", FORCE_COLOR: "0" }), stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
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("\n")) if (line.trim()) job.log.push(line.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, ""));
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(/^~(?=$|\/)/, os4.homedir()));
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(/^~(?=$|\/)/, os4.homedir()));
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(/^~(?=$|\/)/, process.env.HOME ?? "~"));
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) >= 20 ? { label: "Node.js", status: "ok", detail: node } : { label: "Node.js", status: "fail", detail: node, fix: "Install Node.js 20 or newer (https://nodejs.org)" });
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
- if (!next) add({ label: "Next.js", status: "fail", detail: "not a dependency", fix: "Truecanvas works with Next.js (App Router) apps for now. Create one: npm create truecanvas" });
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: "Truecanvas needs React 19 (comes with Next.js 15+)" });
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 = nextConfigStatus(root);
2727
- add(wrapped.wrapped ? { label: "next.config", status: "ok", detail: `${path10.basename(wrapped.file)} uses withTruecanvas()` } : { label: "next.config", status: "fail", detail: wrapped.file ? "not wrapped" : "missing", fix: "npx truecanvas init" });
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(`${config.appDir}/truecanvas`) ? { label: "Generated route ignored", status: "ok" } : { label: "Generated route ignored", status: "warn", detail: `${config.appDir}/truecanvas/ isn't in .gitignore`, fix: "npx truecanvas init" });
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.config, script, canvas folder)
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 Next.js dev URL (default http://localhost:3000)
2766
- --no-next Don't start \`next dev\` (by default it starts when the app isn't running)
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
- if (!readPackage(root2) || !("next" in { ...readPackage(root2).dependencies, ...readPackage(root2).devDependencies })) {
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 Next.js project root (no package.json here).");
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(`${config.appDir}/truecanvas`)}`);
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 = nextConfigStatus(root);
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
- if (!appUp && values.next && !values["no-next"]) next = startNext(root, config.appUrl);
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 ? "(starting next dev\u2026)" : "(not running)")}
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("next dev stopped (see above). Start your app, the editor connects as soon as it's up.")}
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("next dev is taking a while. The editor will connect when it's up.")}
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?.kill("SIGINT");
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.kill("SIGKILL");
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(`next dev exited (${code ?? "signal"}).`)}`);
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 = nextConfigStatus(root).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 wrapper in next.config,`);
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 startNext(root, appUrl) {
3102
- const port = new URL(appUrl).port || "3000";
3103
- const bin = path11.join(root, "node_modules", ".bin", "next");
3104
- const devArgs = ["dev", "-p", port, "-H", "127.0.0.1"];
3105
- const [cmd, args] = fs11.existsSync(bin) ? [bin, devArgs] : ["npx", ["next", ...devArgs]];
3106
- const child = spawn3(cmd, args, { cwd: root, env: toolEnv(process.env.NO_COLOR ? {} : { FORCE_COLOR: "1" }), stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
3107
- child.on("error", (err) => console.error(` ${c.dim("next \u2502")} couldn't start next dev: ${err.message}`));
3108
- const prefix = c.dim("next \u2502 ");
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("\n");
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 {
@@ -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
- /** Where your Next.js dev server runs. */
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 as git sees it: git reports real paths, so a folder
434
- * reached through a symlink (macOS /var → /private/var) must be resolved too.
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 realCwd;
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 Next can
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: wraps next.config, adds a `canvas` script and
945
- * ignores the generated route. Every step is idempotent and reported.
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 };