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.
@@ -1,8 +1,18 @@
1
+ import {
2
+ detectFramework,
3
+ idOf,
4
+ isComponentName,
5
+ jsxName,
6
+ parseModule,
7
+ rewriteImportSource,
8
+ walk
9
+ } from "./chunk-PBKX3SBC.js";
1
10
  import {
2
11
  Catalog,
3
12
  importSpecifier,
13
+ posix,
4
14
  resolveAliasPath
5
- } from "./chunk-T2TCBSTG.js";
15
+ } from "./chunk-WU7NBTJH.js";
6
16
  import {
7
17
  ICON_LIBRARIES,
8
18
  iconLibrary,
@@ -10,86 +20,6 @@ import {
10
20
  searchIcons
11
21
  } from "./chunk-C4SATRZT.js";
12
22
 
13
- // src/core/config.ts
14
- import fs from "fs";
15
- import path from "path";
16
- function loadConfig(root, overrides = {}) {
17
- let file = {};
18
- const configPath = path.join(root, "truecanvas.config.json");
19
- if (fs.existsSync(configPath)) file = JSON.parse(fs.readFileSync(configPath, "utf8"));
20
- const appDir = file.appDir ?? (fs.existsSync(path.join(root, "src/app")) ? "src/app" : "app");
21
- const srcPrefix = appDir.startsWith("src/") ? "src/" : "";
22
- let deps = {};
23
- try {
24
- const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
25
- deps = { ...pkg.dependencies, ...pkg.devDependencies };
26
- } catch {
27
- deps = {};
28
- }
29
- const knownLibraries = ["@paper-design/shaders-react"].filter((l) => l in deps);
30
- return {
31
- root,
32
- appUrl: "http://localhost:3000",
33
- port: 4800,
34
- canvasDir: `${srcPrefix}canvas`,
35
- appDir,
36
- components: [`${srcPrefix}components/**/*.tsx`],
37
- darkMode: { strategy: "class", value: "dark" },
38
- ...file,
39
- libraries: [.../* @__PURE__ */ new Set([...knownLibraries, ...file.libraries ?? []])],
40
- ...stripUndefined(overrides)
41
- };
42
- }
43
- function stripUndefined(o) {
44
- return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
45
- }
46
-
47
- // src/core/ast.ts
48
- import { parse } from "@babel/parser";
49
- function parseModule(source) {
50
- return parse(source, {
51
- sourceType: "module",
52
- plugins: ["typescript", "jsx"],
53
- errorRecovery: false
54
- });
55
- }
56
- function idOf(node) {
57
- const loc = node.loc.start;
58
- return `${loc.line}:${loc.column}`;
59
- }
60
- function jsxName(name) {
61
- if (name.type === "JSXIdentifier") return name.name;
62
- if (name.type === "JSXNamespacedName") return `${name.namespace.name}:${name.name.name}`;
63
- return `${jsxName(name.object)}.${name.property.name}`;
64
- }
65
- function walk(node, visit, parent = null) {
66
- if (!node || typeof node !== "object") return;
67
- if (Array.isArray(node)) {
68
- for (const child of node) walk(child, visit, parent);
69
- return;
70
- }
71
- const n = node;
72
- if (typeof n.type !== "string") return;
73
- if (visit(n, parent) === false) return;
74
- for (const key of Object.keys(n)) {
75
- if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments" || key === "extra") continue;
76
- const value = n[key];
77
- if (value && typeof value === "object") walk(value, visit, n);
78
- }
79
- }
80
- function isComponentName(name) {
81
- return /^[A-Z]/.test(name) || name.includes(".");
82
- }
83
- function rewriteImportSource(statement, from, to) {
84
- if (from === to) return statement;
85
- for (const q of ['"', "'"]) {
86
- const needle = `${q}${from}${q}`;
87
- const i = statement.indexOf(needle);
88
- if (i >= 0) return `${statement.slice(0, i)}${q}${to}${q}${statement.slice(i + needle.length)}`;
89
- }
90
- return statement;
91
- }
92
-
93
23
  // src/core/parse.ts
94
24
  function parseCanvas(name, file, source) {
95
25
  let ast;
@@ -118,8 +48,8 @@ function parseCanvas(name, file, source) {
118
48
  let frameIndex = 0;
119
49
  for (const child of canvasNode.children) {
120
50
  if (child.kind !== "component" || child.name !== "Frame") continue;
121
- const path15 = [frameIndex++];
122
- reindex(child, path15);
51
+ const path14 = [frameIndex++];
52
+ reindex(child, path14);
123
53
  frames.push({
124
54
  ...child,
125
55
  frameName: str(child.props.name) ?? `Frame ${frameIndex}`,
@@ -170,9 +100,9 @@ function topLevelNames(ast) {
170
100
  }
171
101
  return out;
172
102
  }
173
- function reindex(node, path15) {
174
- node.path = path15;
175
- node.children.forEach((c, i) => reindex(c, [...path15, i]));
103
+ function reindex(node, path14) {
104
+ node.path = path14;
105
+ node.children.forEach((c, i) => reindex(c, [...path14, i]));
176
106
  }
177
107
  function str(v) {
178
108
  return v?.kind === "string" ? v.value : void 0;
@@ -180,10 +110,10 @@ function str(v) {
180
110
  function num(v) {
181
111
  return v?.kind === "number" ? v.value : void 0;
182
112
  }
183
- function buildNode(el, source, path15) {
113
+ function buildNode(el, source, path14) {
184
114
  const children = [];
185
115
  for (const child of el.children) {
186
- const built = buildChild(child, source, [...path15, children.length]);
116
+ const built = buildChild(child, source, [...path14, children.length]);
187
117
  if (built) children.push(built);
188
118
  }
189
119
  if (el.type === "JSXFragment") {
@@ -198,7 +128,7 @@ function buildNode(el, source, path15) {
198
128
  end: el.end,
199
129
  line: el.loc.start.line,
200
130
  col: el.loc.start.column,
201
- path: path15,
131
+ path: path14,
202
132
  attrs: [],
203
133
  nameEnd: el.openingFragment.end - 1,
204
134
  openEnd: el.openingFragment.end,
@@ -233,7 +163,7 @@ function buildNode(el, source, path15) {
233
163
  end: el.end,
234
164
  line: open.loc.start.line,
235
165
  col: open.loc.start.column,
236
- path: path15,
166
+ path: path14,
237
167
  attrs,
238
168
  // after type arguments: `<Select<Opt> …>`
239
169
  nameEnd: (open.typeParameters ?? open.name).end,
@@ -242,27 +172,27 @@ function buildNode(el, source, path15) {
242
172
  closeStart: el.closingElement ? el.closingElement.start : open.end
243
173
  };
244
174
  }
245
- function buildChild(child, source, path15) {
246
- if (child.type === "JSXElement" || child.type === "JSXFragment") return buildNode(child, source, path15);
175
+ function buildChild(child, source, path14) {
176
+ if (child.type === "JSXElement" || child.type === "JSXFragment") return buildNode(child, source, path14);
247
177
  if (child.type === "JSXText") {
248
178
  const text = child.value.replace(/\s+/g, " ").trim();
249
179
  if (!text) return null;
250
- return leaf("text", text, child, path15, { text });
180
+ return leaf("text", text, child, path14, { text });
251
181
  }
252
182
  if (child.type === "JSXExpressionContainer") {
253
183
  const expr = child.expression;
254
184
  if (expr.type === "JSXEmptyExpression") return null;
255
- if (expr.type === "StringLiteral") return leaf("text", expr.value, child, path15, { text: expr.value });
185
+ if (expr.type === "StringLiteral") return leaf("text", expr.value, child, path14, { text: expr.value });
256
186
  if (expr.type === "TemplateLiteral" && expr.expressions.length === 0) {
257
187
  const text = expr.quasis.map((q) => q.value.cooked ?? "").join("");
258
- return leaf("text", text, child, path15, { text });
188
+ return leaf("text", text, child, path14, { text });
259
189
  }
260
- return leaf("expression", source.slice(expr.start, expr.end), child, path15, {});
190
+ return leaf("expression", source.slice(expr.start, expr.end), child, path14, {});
261
191
  }
262
- if (child.type === "JSXSpreadChild") return leaf("expression", source.slice(child.start, child.end), child, path15, {});
192
+ if (child.type === "JSXSpreadChild") return leaf("expression", source.slice(child.start, child.end), child, path14, {});
263
193
  return null;
264
194
  }
265
- function leaf(kind, name, node, path15, extra) {
195
+ function leaf(kind, name, node, path14, extra) {
266
196
  return {
267
197
  id: `${kind[0]}${idOf(node)}`,
268
198
  kind,
@@ -275,7 +205,7 @@ function leaf(kind, name, node, path15, extra) {
275
205
  end: node.end,
276
206
  line: node.loc.start.line,
277
207
  col: node.loc.start.column,
278
- path: path15,
208
+ path: path14,
279
209
  attrs: [],
280
210
  nameEnd: node.end,
281
211
  openEnd: node.end,
@@ -339,10 +269,10 @@ function findNode(doc, id) {
339
269
  function findFrame(doc, ref) {
340
270
  return doc.frames.find((f) => f.id === ref || f.frameName === ref) ?? null;
341
271
  }
342
- function findByPath(doc, path15) {
343
- if (!path15.length) return null;
344
- let node = doc.frames[path15[0]];
345
- for (const i of path15.slice(1)) node = node?.children[i];
272
+ function findByPath(doc, path14) {
273
+ if (!path14.length) return null;
274
+ let node = doc.frames[path14[0]];
275
+ for (const i of path14.slice(1)) node = node?.children[i];
346
276
  return node ?? null;
347
277
  }
348
278
 
@@ -561,9 +491,9 @@ ${pi}`);
561
491
  this.s.appendLeft(this.contentEnd(node), `
562
492
  ${this.lineIndent(node.start)}${marked}`);
563
493
  if (marked === code.trim()) {
564
- const path15 = [...node.path];
565
- path15[path15.length - 1] += 1;
566
- this.selectPaths.push(path15);
494
+ const path14 = [...node.path];
495
+ path14[path14.length - 1] += 1;
496
+ this.selectPaths.push(path14);
567
497
  }
568
498
  }
569
499
  move(node, target, index) {
@@ -811,23 +741,17 @@ function findDevice(id) {
811
741
  return id ? DEVICES.find((d) => d.id === id) : void 0;
812
742
  }
813
743
 
814
- // src/core/comments.ts
815
- import fs3 from "fs";
816
- import path3 from "path";
817
- import { randomUUID } from "crypto";
818
-
819
744
  // src/core/scaffold.ts
820
- import fs2 from "fs";
821
- import path2 from "path";
745
+ import fs from "fs";
746
+ import path from "path";
822
747
  var CANVAS_SUFFIX = ".canvas.tsx";
823
- var ROUTE = "truecanvas";
824
748
  function canvasFiles(config) {
825
- const dir = path2.join(config.root, config.canvasDir);
826
- if (!fs2.existsSync(dir)) return [];
827
- return fs2.readdirSync(dir).filter((f) => f.endsWith(CANVAS_SUFFIX)).sort((a, b) => canvasNameOf(a).localeCompare(canvasNameOf(b)));
749
+ const dir = path.join(config.root, config.canvasDir);
750
+ if (!fs.existsSync(dir)) return [];
751
+ return fs.readdirSync(dir).filter((f) => f.endsWith(CANVAS_SUFFIX)).sort((a, b) => canvasNameOf(a).localeCompare(canvasNameOf(b)));
828
752
  }
829
753
  function canvasNameOf(file) {
830
- return path2.basename(file).slice(0, -CANVAS_SUFFIX.length);
754
+ return path.basename(file).slice(0, -CANVAS_SUFFIX.length);
831
755
  }
832
756
  function assertCanvasName(name) {
833
757
  if (!/^[a-z0-9][a-z0-9-_]*$/i.test(name)) throw new Error(`Invalid canvas name "${name}". Use letters, digits, - and _.`);
@@ -835,23 +759,25 @@ function assertCanvasName(name) {
835
759
  }
836
760
  function canvasPath(config, name) {
837
761
  assertCanvasName(name);
838
- return path2.join(config.root, config.canvasDir, `${name}${CANVAS_SUFFIX}`);
762
+ return path.join(config.root, config.canvasDir, `${name}${CANVAS_SUFFIX}`);
839
763
  }
840
764
  function writeIfChanged(file, content) {
841
- if (fs2.existsSync(file) && fs2.readFileSync(file, "utf8") === content) return false;
842
- fs2.mkdirSync(path2.dirname(file), { recursive: true });
843
- fs2.writeFileSync(file, content);
765
+ if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === content) return false;
766
+ fs.mkdirSync(path.dirname(file), { recursive: true });
767
+ fs.writeFileSync(file, content);
844
768
  return true;
845
769
  }
770
+ function routeDirOf(config) {
771
+ return path.join(config.root, config.routeDir);
772
+ }
846
773
  function syncRoute(config) {
847
- const routeDir = path2.join(config.root, config.appDir, ROUTE);
848
- const pageFile = path2.join(routeDir, "[canvas]", "page.tsx");
849
- const registryFile = path2.join(routeDir, "canvases.tsx");
850
- const canvasDirAbs = path2.join(config.root, config.canvasDir);
851
- let rel = path2.relative(routeDir, canvasDirAbs);
774
+ const routeDir = routeDirOf(config);
775
+ const registryFile = path.join(routeDir, "canvases.tsx");
776
+ const canvasDirAbs = path.join(config.root, config.canvasDir);
777
+ let rel = posix(path.relative(routeDir, canvasDirAbs));
852
778
  if (!rel.startsWith(".")) rel = `./${rel}`;
853
- const compareDir = path2.join(routeDir, "compare");
854
- const compares = fs2.existsSync(compareDir) ? fs2.readdirSync(compareDir).filter((f) => f.endsWith(CANVAS_SUFFIX)) : [];
779
+ const compareDir = path.join(routeDir, "compare");
780
+ const compares = fs.existsSync(compareDir) ? fs.readdirSync(compareDir).filter((f) => f.endsWith(CANVAS_SUFFIX)) : [];
855
781
  const entries = [
856
782
  ...canvasFiles(config).map((f) => ` ${JSON.stringify(canvasNameOf(f))}: lazy(() => import(${JSON.stringify(`${rel}/${f.replace(/\.tsx$/, "")}`)})),`),
857
783
  // older versions rendered for Compare
@@ -873,6 +799,10 @@ export default function Canvases(props: Omit<HostProps, "canvases" | "components
873
799
  }
874
800
  `;
875
801
  const dm = config.darkMode;
802
+ const componentsFile = path.join(routeDir, "components.tsx");
803
+ if (!fs.existsSync(componentsFile)) writeIfChanged(componentsFile, componentRegistry(config, []));
804
+ const a = writeIfChanged(registryFile, registry);
805
+ if (config.framework === "vite") return writeIfChanged(path.join(routeDir, "entry.tsx"), viteEntry(config)) || a;
876
806
  const page = `/* eslint-disable */
877
807
  // Generated by Truecanvas. Dev-only route that renders canvas frames.
878
808
  import { notFound } from "next/navigation";
@@ -900,11 +830,51 @@ export default async function TruecanvasFrame({
900
830
  );
901
831
  }
902
832
  `;
903
- const componentsFile = path2.join(routeDir, "components.tsx");
904
- if (!fs2.existsSync(componentsFile)) writeIfChanged(componentsFile, componentRegistry(config, []));
905
- const a = writeIfChanged(registryFile, registry);
906
- const b = writeIfChanged(pageFile, page);
907
- return a || b;
833
+ return writeIfChanged(path.join(routeDir, "[canvas]", "page.tsx"), page) || a;
834
+ }
835
+ function viteEntry(config) {
836
+ const css = (config.css ?? viteStylesheets(config.root)).map((f) => f.startsWith("/") ? f : f.startsWith("./") ? f.slice(1) : fs.existsSync(path.join(config.root, f)) ? `/${f}` : f);
837
+ return `/* eslint-disable */
838
+ // Generated by Truecanvas. Dev-only entry that renders canvas frames at /truecanvas/<canvas>.
839
+ ${css.map((f) => `import ${JSON.stringify(f)};`).join("\n")}
840
+ import { createRoot } from "react-dom/client";
841
+ import Canvases from "./canvases";
842
+
843
+ const query = new URLSearchParams(location.search);
844
+ const canvas = decodeURIComponent(location.pathname.split("/")[2] ?? "");
845
+
846
+ createRoot(document.getElementById("tc-root")!).render(
847
+ <Canvases
848
+ canvas={canvas}
849
+ frame={query.get("frame")}
850
+ theme={(query.get("theme") as "light" | "dark" | "system" | null) ?? "system"}
851
+ darkMode={${JSON.stringify(config.darkMode)}}
852
+ />,
853
+ );
854
+ `;
855
+ }
856
+ function viteStylesheets(root) {
857
+ let html = "";
858
+ try {
859
+ html = fs.readFileSync(path.join(root, "index.html"), "utf8");
860
+ } catch {
861
+ return [];
862
+ }
863
+ const src = /<script[^>]*type=["']module["'][^>]*src=["']([^"']+)["']/i.exec(html)?.[1] ?? /<script[^>]*src=["']([^"']+)["'][^>]*type=["']module["']/i.exec(html)?.[1];
864
+ if (!src || /^https?:/.test(src)) return [];
865
+ const entry = path.join(root, src.replace(/^\//, ""));
866
+ let code = "";
867
+ try {
868
+ code = fs.readFileSync(entry, "utf8");
869
+ } catch {
870
+ return [];
871
+ }
872
+ const out = [];
873
+ for (const m of code.matchAll(/^\s*import\s+["']([^"']+\.(?:css|scss|sass|less|styl|pcss))["']/gm)) {
874
+ const spec = m[1];
875
+ out.push(spec.startsWith(".") ? `/${posix(path.relative(root, path.resolve(path.dirname(entry), spec)))}` : spec);
876
+ }
877
+ return out;
908
878
  }
909
879
  var STARTER_CANVAS = `"use client";
910
880
  import { Canvas, Frame } from "truecanvas";
@@ -977,11 +947,11 @@ export default function StarterCanvas() {
977
947
  }
978
948
  `;
979
949
  function componentRegistry(config, specs) {
980
- const routeDir = path2.join(config.root, config.appDir, ROUTE);
950
+ const routeDir = routeDirOf(config);
981
951
  const lines = specs.map((c) => {
982
952
  let from = c.library;
983
953
  if (!from) {
984
- from = path2.relative(routeDir, path2.join(config.root, c.file)).replace(/\.(tsx|ts|jsx|js)$/, "");
954
+ from = posix(path.relative(routeDir, path.join(config.root, c.file))).replace(/\.(tsx|ts|jsx|js)$/, "");
985
955
  if (!from.startsWith(".")) from = `./${from}`;
986
956
  }
987
957
  return ` ${JSON.stringify(c.name)}: lazy(() => import(${JSON.stringify(from)}).then((m) => ({ default: m[${JSON.stringify(c.name)}] }))),`;
@@ -998,22 +968,25 @@ ${lines.join("\n")}
998
968
  `;
999
969
  }
1000
970
  function syncComponentRegistry(config, specs) {
1001
- const file = path2.join(config.root, config.appDir, ROUTE, "components.tsx");
971
+ const file = path.join(routeDirOf(config), "components.tsx");
1002
972
  return writeIfChanged(file, componentRegistry(config, specs));
1003
973
  }
1004
974
 
1005
975
  // src/core/comments.ts
976
+ import fs2 from "fs";
977
+ import path2 from "path";
978
+ import { randomUUID } from "crypto";
1006
979
  var Comments = class {
1007
980
  constructor(config) {
1008
981
  this.config = config;
1009
982
  }
1010
983
  config;
1011
984
  file(canvas) {
1012
- return path3.join(this.config.root, this.config.canvasDir, `${assertCanvasName(canvas)}.comments.json`);
985
+ return path2.join(this.config.root, this.config.canvasDir, `${assertCanvasName(canvas)}.comments.json`);
1013
986
  }
1014
987
  list(canvas) {
1015
988
  try {
1016
- return JSON.parse(fs3.readFileSync(this.file(canvas), "utf8")).threads ?? [];
989
+ return JSON.parse(fs2.readFileSync(this.file(canvas), "utf8")).threads ?? [];
1017
990
  } catch {
1018
991
  return [];
1019
992
  }
@@ -1021,11 +994,11 @@ var Comments = class {
1021
994
  save(canvas, threads) {
1022
995
  const file = this.file(canvas);
1023
996
  if (!threads.length) {
1024
- if (fs3.existsSync(file)) fs3.rmSync(file);
997
+ if (fs2.existsSync(file)) fs2.rmSync(file);
1025
998
  return;
1026
999
  }
1027
- fs3.mkdirSync(path3.dirname(file), { recursive: true });
1028
- fs3.writeFileSync(file, `${JSON.stringify({ version: 1, threads }, null, 2)}
1000
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
1001
+ fs2.writeFileSync(file, `${JSON.stringify({ version: 1, threads }, null, 2)}
1029
1002
  `);
1030
1003
  }
1031
1004
  update(canvas, id, fn) {
@@ -1080,14 +1053,13 @@ var Comments = class {
1080
1053
  }
1081
1054
  renameCanvas(from, to) {
1082
1055
  const a = this.file(from);
1083
- if (fs3.existsSync(a)) fs3.renameSync(a, this.file(to));
1056
+ if (fs2.existsSync(a)) fs2.renameSync(a, this.file(to));
1084
1057
  }
1085
1058
  };
1086
1059
 
1087
1060
  // src/core/git.ts
1088
1061
  import { execFile } from "child_process";
1089
- import fs4 from "fs";
1090
- import path4 from "path";
1062
+ import path3 from "path";
1091
1063
  var GitError = class extends EditError {
1092
1064
  };
1093
1065
  function git(cwd, args, timeout = 3e4, opts = {}) {
@@ -1127,6 +1099,7 @@ var Git = class {
1127
1099
  return { repo: false, root: null, branch: null, unborn: false, upstream: null, ahead: 0, behind: 0, files: [], user: null, defaultBranch: null, remote: null };
1128
1100
  }
1129
1101
  const out = await git(this.cwd, ["status", "--porcelain=v2", "-z", "--branch", "--untracked-files=all", "--", "."]);
1102
+ const prefix = await this.prefix();
1130
1103
  const s = { repo: true, root, branch: null, unborn: false, upstream: null, ahead: 0, behind: 0, files: [], user: null, defaultBranch: null, remote: null };
1131
1104
  const entries = out.split("\0");
1132
1105
  for (let i = 0; i < entries.length; i++) {
@@ -1143,10 +1116,10 @@ var Git = class {
1143
1116
  const file = parts.slice(line.startsWith("2 ") ? 9 : 8).join(" ");
1144
1117
  if (line.startsWith("2 ")) i++;
1145
1118
  const code = xy.replace(".", "")[0] ?? "M";
1146
- s.files.push({ path: this.rel(root, file), status: code === "A" ? "added" : code === "D" ? "deleted" : code === "R" ? "renamed" : "modified", staged: xy[0] !== "." });
1119
+ s.files.push({ path: this.rel(prefix, file), status: code === "A" ? "added" : code === "D" ? "deleted" : code === "R" ? "renamed" : "modified", staged: xy[0] !== "." });
1147
1120
  } else if (line.startsWith("u ")) {
1148
- s.files.push({ path: this.rel(root, line.split(" ").slice(10).join(" ")), status: "conflicted", staged: false });
1149
- } else if (line.startsWith("? ")) s.files.push({ path: this.rel(root, line.slice(2)), status: "untracked", staged: false });
1121
+ s.files.push({ path: this.rel(prefix, line.split(" ").slice(10).join(" ")), status: "conflicted", staged: false });
1122
+ } else if (line.startsWith("? ")) s.files.push({ path: this.rel(prefix, line.slice(2)), status: "untracked", staged: false });
1150
1123
  }
1151
1124
  s.user = (await git(this.cwd, ["config", "user.name"]).catch(() => "")).trim() || null;
1152
1125
  s.remote = (await git(this.cwd, ["remote", "get-url", "origin"]).catch(() => "")).trim() || null;
@@ -1201,8 +1174,8 @@ var Git = class {
1201
1174
  safeRef(branch);
1202
1175
  const tip = (await git(this.cwd, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]).catch(() => "")).trim() || (await git(this.cwd, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}`]).catch(() => "")).trim();
1203
1176
  const os = await import("os");
1204
- const fs15 = await import("fs");
1205
- const index = path4.join(os.tmpdir(), `truecanvas-index-${process.pid}-${Date.now()}`);
1177
+ const fs13 = await import("fs");
1178
+ const index = path3.join(os.tmpdir(), `truecanvas-index-${process.pid}-${Date.now()}`);
1206
1179
  const env = { GIT_INDEX_FILE: index };
1207
1180
  try {
1208
1181
  if (tip) await git(this.cwd, ["read-tree", tip], 3e4, { env });
@@ -1216,7 +1189,7 @@ var Git = class {
1216
1189
  await git(this.cwd, ["update-ref", `refs/heads/${branch}`, commit]);
1217
1190
  await git(this.cwd, ["push", "origin", `refs/heads/${branch}:refs/heads/${branch}`], 12e4);
1218
1191
  } finally {
1219
- fs15.rmSync(index, { force: true });
1192
+ fs13.rmSync(index, { force: true });
1220
1193
  }
1221
1194
  }
1222
1195
  // ---------- GitHub (gh CLI) ----------
@@ -1250,19 +1223,16 @@ var Git = class {
1250
1223
  return (await git(this.cwd, ["remote", "get-url", "origin"])).trim();
1251
1224
  }
1252
1225
  /** git reports paths from the repo root; we show them from the project root. */
1253
- rel(root, file) {
1254
- return path4.relative(this.realCwd(), path4.join(root, file));
1226
+ rel(prefix, file) {
1227
+ return file.startsWith(prefix) ? file.slice(prefix.length) : posix(path3.posix.relative(prefix, file));
1255
1228
  }
1256
1229
  /**
1257
- * The project folder as git sees it: git reports real paths, so a folder
1258
- * reached through a symlink (macOS /var → /private/var) must be resolved too.
1230
+ * The project folder inside the repo ("apps/web/", or "" at the root), as git
1231
+ * itself sees it. Asking git instead of comparing file system paths holds up
1232
+ * through symlinks (macOS /var → /private/var), junctions and Windows short names.
1259
1233
  */
1260
- realCwd() {
1261
- try {
1262
- return fs4.realpathSync(this.cwd);
1263
- } catch {
1264
- return this.cwd;
1265
- }
1234
+ async prefix() {
1235
+ return (await git(this.cwd, ["rev-parse", "--show-prefix"]).catch(() => "")).trim();
1266
1236
  }
1267
1237
  async branches() {
1268
1238
  const out = await git(this.cwd, ["branch", "-a", "--format=%(refname)%09%(HEAD)"]).catch(() => "");
@@ -1290,8 +1260,7 @@ var Git = class {
1290
1260
  }
1291
1261
  /** A file's content at a ref, or null if it doesn't exist there. */
1292
1262
  async show(ref, file) {
1293
- const root = (await git(this.cwd, ["rev-parse", "--show-toplevel"])).trim();
1294
- const inRepo = path4.relative(root, path4.join(this.realCwd(), file)).split(path4.sep).join("/");
1263
+ const inRepo = path3.posix.normalize(`${await this.prefix()}${posix(file)}`);
1295
1264
  return git(this.cwd, ["show", `${safeRef(ref)}:${inRepo}`, "--"]).catch(() => null);
1296
1265
  }
1297
1266
  async commit(message, files) {
@@ -1320,17 +1289,17 @@ var Git = class {
1320
1289
  };
1321
1290
 
1322
1291
  // src/core/workspace.ts
1323
- import fs9 from "fs";
1324
- import path9 from "path";
1292
+ import fs7 from "fs";
1293
+ import path8 from "path";
1325
1294
  import { createRequire } from "module";
1326
1295
 
1327
1296
  // src/core/link.ts
1328
- import fs6 from "fs";
1329
- import path6 from "path";
1297
+ import fs4 from "fs";
1298
+ import path5 from "path";
1330
1299
 
1331
1300
  // src/core/routes.ts
1332
- import fs5 from "fs";
1333
- import path5 from "path";
1301
+ import fs3 from "fs";
1302
+ import path4 from "path";
1334
1303
 
1335
1304
  // src/core/source.ts
1336
1305
  function defaultReturn(source, layout) {
@@ -1542,28 +1511,29 @@ function parseTargetDoc(file, source) {
1542
1511
  var PAGE = /^page\.(tsx|jsx|ts|js)$/;
1543
1512
  var LAYOUT = ["layout.tsx", "layout.jsx", "layout.js", "layout.ts"];
1544
1513
  function listRoutes(config) {
1545
- const appDir = path5.join(config.root, config.appDir);
1514
+ if (config.framework !== "next") return [];
1515
+ const appDir = path4.join(config.root, config.appDir);
1546
1516
  const out = [];
1547
1517
  const walkDir = (dir, segments) => {
1548
1518
  let entries;
1549
1519
  try {
1550
- entries = fs5.readdirSync(dir, { withFileTypes: true });
1520
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
1551
1521
  } catch {
1552
1522
  return;
1553
1523
  }
1554
1524
  for (const e of entries) {
1555
1525
  if (e.isFile() && PAGE.test(e.name)) {
1556
1526
  const visible = segments.filter((s) => !/^\(.*\)$/.test(s) && !s.startsWith("@"));
1557
- out.push({ route: `/${visible.join("/")}`, file: path5.relative(config.root, path5.join(dir, e.name)), dynamic: segments.some((s) => s.startsWith("[")) });
1527
+ out.push({ route: `/${visible.join("/")}`, file: posix(path4.relative(config.root, path4.join(dir, e.name))), dynamic: segments.some((s) => s.startsWith("[")) });
1558
1528
  }
1559
- if (e.isDirectory() && !e.name.startsWith("_") && e.name !== "truecanvas" && e.name !== "api" && e.name !== "node_modules") walkDir(path5.join(dir, e.name), [...segments, e.name]);
1529
+ if (e.isDirectory() && !e.name.startsWith("_") && e.name !== "truecanvas" && e.name !== "api" && e.name !== "node_modules") walkDir(path4.join(dir, e.name), [...segments, e.name]);
1560
1530
  }
1561
1531
  };
1562
1532
  walkDir(appDir, []);
1563
1533
  return out.sort((a, b) => a.route.localeCompare(b.route));
1564
1534
  }
1565
1535
  function extract(file, canvasFile, config, layout) {
1566
- const source = fs5.readFileSync(file, "utf8");
1536
+ const source = fs3.readFileSync(file, "utf8");
1567
1537
  const info = defaultReturn(source, layout);
1568
1538
  if (!info?.pure) return null;
1569
1539
  const imports = [];
@@ -1579,26 +1549,26 @@ function extract(file, canvasFile, config, layout) {
1579
1549
  }
1580
1550
  function repoint(config, spec, fromFile, toFile) {
1581
1551
  if (!spec.startsWith(".")) return spec;
1582
- return importSpecifier(config.root, path5.relative(config.root, toFile), path5.relative(config.root, path5.resolve(path5.dirname(fromFile), spec)));
1552
+ return importSpecifier(config.root, path4.relative(config.root, toFile), path4.relative(config.root, path4.resolve(path4.dirname(fromFile), spec)));
1583
1553
  }
1584
1554
  function layoutFiles(config, pageFile) {
1585
- const appDir = path5.join(config.root, config.appDir);
1555
+ const appDir = path4.join(config.root, config.appDir);
1586
1556
  const out = [];
1587
- for (let dir = path5.dirname(path5.resolve(config.root, pageFile)); dir.startsWith(appDir) && dir !== appDir; dir = path5.dirname(dir)) {
1588
- const file = LAYOUT.map((f) => path5.join(dir, f)).find((f) => fs5.existsSync(f));
1589
- if (file) out.push(path5.relative(config.root, file));
1557
+ for (let dir = path4.dirname(path4.resolve(config.root, pageFile)); dir.startsWith(appDir) && dir !== appDir; dir = path4.dirname(dir)) {
1558
+ const file = LAYOUT.map((f) => path4.join(dir, f)).find((f) => fs3.existsSync(f));
1559
+ if (file) out.push(posix(path4.relative(config.root, file)));
1590
1560
  }
1591
1561
  return out;
1592
1562
  }
1593
1563
  function routeOf(config, pageFile) {
1594
- const rel = path5.relative(path5.join(config.root, config.appDir), path5.dirname(path5.resolve(config.root, pageFile)));
1595
- const visible = rel.split(path5.sep).filter((s) => s && !/^\(.*\)$/.test(s) && !s.startsWith("@"));
1564
+ const rel = path4.relative(path4.join(config.root, config.appDir), path4.dirname(path4.resolve(config.root, pageFile)));
1565
+ const visible = rel.split(path4.sep).filter((s) => s && !/^\(.*\)$/.test(s) && !s.startsWith("@"));
1596
1566
  return `/${visible.join("/")}`;
1597
1567
  }
1598
1568
  function routeFrame(config, pageFile, canvasFile) {
1599
- const appDir = path5.join(config.root, config.appDir);
1600
- const pageAbs = path5.resolve(config.root, pageFile);
1601
- const canvasAbs = path5.resolve(config.root, canvasFile);
1569
+ const appDir = path4.join(config.root, config.appDir);
1570
+ const pageAbs = path4.resolve(config.root, pageFile);
1571
+ const canvasAbs = path4.resolve(config.root, canvasFile);
1602
1572
  const imports = [];
1603
1573
  let mode = "inline";
1604
1574
  const page = extract(pageAbs, canvasAbs, config, false);
@@ -1612,8 +1582,8 @@ function routeFrame(config, pageFile, canvasFile) {
1612
1582
  jsx = "<RoutePage />";
1613
1583
  }
1614
1584
  let i = 0;
1615
- for (let dir = path5.dirname(pageAbs); dir.startsWith(appDir) && dir !== appDir; dir = path5.dirname(dir)) {
1616
- const file = LAYOUT.map((f) => path5.join(dir, f)).find((f) => fs5.existsSync(f));
1585
+ for (let dir = path4.dirname(pageAbs); dir.startsWith(appDir) && dir !== appDir; dir = path4.dirname(dir)) {
1586
+ const file = LAYOUT.map((f) => path4.join(dir, f)).find((f) => fs3.existsSync(f));
1617
1587
  if (!file) continue;
1618
1588
  const layout = extract(file, canvasAbs, config, true);
1619
1589
  const holes = layout ? (layout.jsx.match(/\{\s*children\s*\}/g) ?? []).length : 0;
@@ -1623,7 +1593,7 @@ function routeFrame(config, pageFile, canvasFile) {
1623
1593
  imports.push(...layout.imports);
1624
1594
  } else {
1625
1595
  const local = `RouteLayout${i++ || ""}`;
1626
- imports.push(`import ${local} from ${JSON.stringify(importSpecifier(config.root, canvasFile, path5.relative(config.root, file)))};`);
1596
+ imports.push(`import ${local} from ${JSON.stringify(importSpecifier(config.root, canvasFile, path4.relative(config.root, file)))};`);
1627
1597
  jsx = `<${local}>${jsx}</${local}>`;
1628
1598
  }
1629
1599
  }
@@ -1644,16 +1614,16 @@ function wrapperChain(frame) {
1644
1614
  }
1645
1615
  function readSource(config, rel) {
1646
1616
  try {
1647
- return fs6.readFileSync(path6.join(config.root, rel), "utf8");
1617
+ return fs4.readFileSync(path5.join(config.root, rel), "utf8");
1648
1618
  } catch {
1649
1619
  return null;
1650
1620
  }
1651
1621
  }
1652
1622
  var parseMemo = /* @__PURE__ */ new Map();
1653
- function memoParse(key, source, parse2) {
1623
+ function memoParse(key, source, parse) {
1654
1624
  let hit = parseMemo.get(key);
1655
1625
  if (!hit || hit.source !== source) {
1656
- hit = { source, doc: parse2() };
1626
+ hit = { source, doc: parse() };
1657
1627
  parseMemo.set(key, hit);
1658
1628
  }
1659
1629
  return hit.doc && { ...hit.doc, frames: hit.doc.frames.map(cloneTree) };
@@ -1755,7 +1725,7 @@ function linkedFrameBody(config, doc, canvasRel, page) {
1755
1725
  const pageName = importName(config, doc, canvasRel, page, `${segments.length ? pascal(segments[segments.length - 1]) : "Home"}Page`, taken, imports);
1756
1726
  let jsx = `<${pageName} />`;
1757
1727
  for (const rel of layoutFiles(config, page)) {
1758
- const dir = path6.basename(path6.dirname(rel));
1728
+ const dir = path5.basename(path5.dirname(rel));
1759
1729
  const name = importName(config, doc, canvasRel, rel, `${pascal(dir) || "Route"}Layout`, taken, imports);
1760
1730
  jsx = `<${name}>
1761
1731
  ${indentBlock(jsx)}
@@ -1777,7 +1747,7 @@ function dedent(code) {
1777
1747
  function explorationBody(config, frame, canvasRel) {
1778
1748
  if (frame.component) throw new EditError(`"${frame.frameName}" is a main component: edit it directly, every instance follows.`);
1779
1749
  if (!frame.page) throw new EditError(`"${frame.frameName}" isn't linked to a page.`);
1780
- const pageAbs = path6.join(config.root, frame.page);
1750
+ const pageAbs = path5.join(config.root, frame.page);
1781
1751
  const source = readSource(config, frame.page);
1782
1752
  if (source === null) throw new EditError(`${frame.page} doesn't exist anymore.`);
1783
1753
  const chain = wrapperChain(frame);
@@ -1789,7 +1759,7 @@ function explorationBody(config, frame, canvasRel) {
1789
1759
  for (const stmt of info.ast.program.body) {
1790
1760
  if (stmt.type !== "ImportDeclaration" || stmt.importKind === "type" || /\.(css|scss|sass|less)$/.test(stmt.source.value)) continue;
1791
1761
  const src = stmt.source.value;
1792
- const from = repoint(config, src, pageAbs, path6.join(config.root, canvasRel));
1762
+ const from = repoint(config, src, pageAbs, path5.join(config.root, canvasRel));
1793
1763
  const text = source.slice(stmt.start, stmt.end);
1794
1764
  imports.push(from === src ? text : rewriteImportSource(text, src, from));
1795
1765
  }
@@ -1805,7 +1775,7 @@ ${indentBlock(jsx)}
1805
1775
  function applyPlan(config, canvasSource, doc, frame, canvasRel) {
1806
1776
  const page = frame.from;
1807
1777
  if (!page) throw new EditError(`"${frame.frameName}" isn't an exploration of a page. Explore a copy of a linked frame first.`);
1808
- if (!fs6.existsSync(path6.join(config.root, page))) throw new EditError(`${page} doesn't exist anymore.`);
1778
+ if (!fs4.existsSync(path5.join(config.root, page))) throw new EditError(`${page} doesn't exist anymore.`);
1809
1779
  const layoutSpecs = new Set(doc.imports.filter((i) => /(^|\/)layout(\.\w+)?$/.test(i.source)).flatMap((i) => [...i.names, ...i.defaultName ? [i.defaultName] : []]));
1810
1780
  let content = frame.children;
1811
1781
  for (; ; ) {
@@ -1831,7 +1801,7 @@ ${indentBlock(body)}
1831
1801
  continue;
1832
1802
  }
1833
1803
  const sp = decl.specifiers.find((x) => x.local.name === name);
1834
- const from = repoint(config, decl.source.value, path6.join(config.root, canvasRel), path6.join(config.root, page));
1804
+ const from = repoint(config, decl.source.value, path5.join(config.root, canvasRel), path5.join(config.root, page));
1835
1805
  const what = sp.type === "ImportDefaultSpecifier" ? name : sp.type === "ImportNamespaceSpecifier" ? `* as ${name}` : `{ ${sp.imported.type === "Identifier" && sp.imported.name !== name ? `${sp.imported.name} as ${name}` : name} }`;
1836
1806
  imports.push(`import ${what} from ${JSON.stringify(from)};`);
1837
1807
  }
@@ -1864,8 +1834,8 @@ function pruneImports(source, candidates) {
1864
1834
  }
1865
1835
 
1866
1836
  // src/core/motion.ts
1867
- import fs7 from "fs";
1868
- import path7 from "path";
1837
+ import fs5 from "fs";
1838
+ import path6 from "path";
1869
1839
  var REVEAL = `"use client";
1870
1840
 
1871
1841
  import { Children, cloneElement, isValidElement, useEffect, useRef, useState, type CSSProperties, type ReactElement, type ReactNode } from "react";
@@ -2090,8 +2060,8 @@ export function TextAnimate({ effect = "words", stagger = 0.06, delay = 0, durat
2090
2060
  function motionFiles(config) {
2091
2061
  const glob = config.components[0] ?? "components/**/*.tsx";
2092
2062
  const base = glob.split("/").filter((p) => !p.includes("*")).join("/") || "components";
2093
- const dir = path7.join(base, "motion");
2094
- return { reveal: path7.join(dir, "reveal.tsx"), text: path7.join(dir, "text-animate.tsx") };
2063
+ const dir = `${base}/motion`;
2064
+ return { reveal: `${dir}/reveal.tsx`, text: `${dir}/text-animate.tsx` };
2095
2065
  }
2096
2066
  function ensureMotionComponents(config) {
2097
2067
  const files = motionFiles(config);
@@ -2100,18 +2070,18 @@ function ensureMotionComponents(config) {
2100
2070
  [files.reveal, REVEAL],
2101
2071
  [files.text, TEXT_ANIMATE]
2102
2072
  ]) {
2103
- const abs = path7.join(config.root, rel);
2104
- if (fs7.existsSync(abs)) continue;
2105
- fs7.mkdirSync(path7.dirname(abs), { recursive: true });
2106
- fs7.writeFileSync(abs, content);
2073
+ const abs = path6.join(config.root, rel);
2074
+ if (fs5.existsSync(abs)) continue;
2075
+ fs5.mkdirSync(path6.dirname(abs), { recursive: true });
2076
+ fs5.writeFileSync(abs, content);
2107
2077
  created = true;
2108
2078
  }
2109
2079
  return { ...files, created };
2110
2080
  }
2111
2081
 
2112
2082
  // src/core/components.ts
2113
- import fs8 from "fs";
2114
- import path8 from "path";
2083
+ import fs6 from "fs";
2084
+ import path7 from "path";
2115
2085
  function componentsDir(config) {
2116
2086
  const glob = config.components[0] ?? "components/**/*.tsx";
2117
2087
  return glob.split("/").filter((p) => !p.includes("*")).join("/") || "components";
@@ -2175,11 +2145,11 @@ function importsFor(config, jsx, source, doc, fromRel, toRel) {
2175
2145
  (s) => s.type === "ImportDeclaration" && s.importKind !== "type" && s.specifiers.some((sp2) => sp2.local.name === name)
2176
2146
  );
2177
2147
  if (!decl) {
2178
- if (doc.declared.includes(name)) throw new EditError(`These layers use ${name}, defined in ${path8.basename(fromRel)}. Move it to its own file first, or ask your agent to extract the component.`);
2148
+ if (doc.declared.includes(name)) throw new EditError(`These layers use ${name}, defined in ${path7.basename(fromRel)}. Move it to its own file first, or ask your agent to extract the component.`);
2179
2149
  throw new EditError(`These layers use \`${name}\` from the surrounding code, so they can't become a component automatically. Ask your agent to extract it with props.`);
2180
2150
  }
2181
2151
  const sp = decl.specifiers.find((x) => x.local.name === name);
2182
- const from = repoint(config, decl.source.value, path8.join(config.root, fromRel), path8.join(config.root, toRel));
2152
+ const from = repoint(config, decl.source.value, path7.join(config.root, fromRel), path7.join(config.root, toRel));
2183
2153
  const what = sp.type === "ImportDefaultSpecifier" ? name : sp.type === "ImportNamespaceSpecifier" ? `* as ${name}` : `{ ${sp.imported.type === "Identifier" && sp.imported.name !== name ? `${sp.imported.name} as ${name}` : name} }`;
2184
2154
  out.push(`import ${what} from ${JSON.stringify(from)};`);
2185
2155
  }
@@ -2203,8 +2173,8 @@ ${indentBlock(indentBlock(jsx.trim()))}
2203
2173
  `;
2204
2174
  }
2205
2175
  function newComponentPath(config, name) {
2206
- const rel = path8.join(componentsDir(config), `${kebab(name)}.tsx`);
2207
- if (fs8.existsSync(path8.join(config.root, rel))) throw new EditError(`${rel} already exists. Pick another name.`);
2176
+ const rel = `${componentsDir(config)}/${kebab(name)}.tsx`;
2177
+ if (fs6.existsSync(path7.join(config.root, rel))) throw new EditError(`${rel} already exists. Pick another name.`);
2208
2178
  return rel;
2209
2179
  }
2210
2180
 
@@ -2224,7 +2194,7 @@ var Workspace = class {
2224
2194
  this.catalog = new Catalog(config.root, config.components, config.libraries);
2225
2195
  this.comments = new Comments(config);
2226
2196
  this.git = new Git(config.root);
2227
- for (const c of this.canvases()) this.sources.set(c, fs9.readFileSync(this.file(c), "utf8"));
2197
+ for (const c of this.canvases()) this.sources.set(c, fs7.readFileSync(this.file(c), "utf8"));
2228
2198
  }
2229
2199
  config;
2230
2200
  catalog;
@@ -2255,12 +2225,12 @@ var Workspace = class {
2255
2225
  return canvasPath(this.config, canvas);
2256
2226
  }
2257
2227
  relFile(canvas) {
2258
- return path9.relative(this.config.root, this.file(canvas));
2228
+ return posix(path8.relative(this.config.root, this.file(canvas)));
2259
2229
  }
2260
2230
  read(canvas) {
2261
2231
  const file = this.file(canvas);
2262
- if (!fs9.existsSync(file)) throw new EditError(`Canvas "${canvas}" does not exist. Canvases: ${this.canvases().join(", ") || "none"}`);
2263
- const src = fs9.readFileSync(file, "utf8");
2232
+ if (!fs7.existsSync(file)) throw new EditError(`Canvas "${canvas}" does not exist. Canvases: ${this.canvases().join(", ") || "none"}`);
2233
+ const src = fs7.readFileSync(file, "utf8");
2264
2234
  if (!this.sources.has(canvas)) this.sources.set(canvas, src);
2265
2235
  return src;
2266
2236
  }
@@ -2285,7 +2255,7 @@ var Workspace = class {
2285
2255
  docCache = /* @__PURE__ */ new Map();
2286
2256
  stamp(rel) {
2287
2257
  try {
2288
- const st = fs9.statSync(path9.join(this.config.root, rel));
2258
+ const st = fs7.statSync(path8.join(this.config.root, rel));
2289
2259
  return `${st.mtimeMs}:${st.size}`;
2290
2260
  } catch {
2291
2261
  return "missing";
@@ -2299,8 +2269,8 @@ var Workspace = class {
2299
2269
  const linked = fileOfId(id);
2300
2270
  if (linked) assertShown(doc, linked);
2301
2271
  const rel = linked ?? this.relFile(canvas);
2302
- const abs = path9.join(this.config.root, rel);
2303
- return { node: hit.node, rel, abs, source: fs9.readFileSync(abs, "utf8") };
2272
+ const abs = path8.join(this.config.root, rel);
2273
+ return { node: hit.node, rel, abs, source: fs7.readFileSync(abs, "utf8") };
2304
2274
  }
2305
2275
  /** Page and layout files shown by linked frames, across all canvases (the Git panel counts them as design files). */
2306
2276
  linkedFiles() {
@@ -2319,7 +2289,7 @@ var Workspace = class {
2319
2289
  */
2320
2290
  designFiles() {
2321
2291
  const m = motionFiles(this.config);
2322
- return [...this.linkedFiles(), ...[m.reveal, m.text].filter((f) => fs9.existsSync(path9.join(this.config.root, f)))];
2292
+ return [...this.linkedFiles(), ...[m.reveal, m.text].filter((f) => fs7.existsSync(path8.join(this.config.root, f)))];
2323
2293
  }
2324
2294
  /** Canvases with a linked frame showing this page or layout file. */
2325
2295
  canvasesLinking(rel) {
@@ -2335,7 +2305,7 @@ var Workspace = class {
2335
2305
  linkedFileChanged(rel) {
2336
2306
  let next;
2337
2307
  try {
2338
- next = fs9.readFileSync(path9.join(this.config.root, rel), "utf8");
2308
+ next = fs7.readFileSync(path8.join(this.config.root, rel), "utf8");
2339
2309
  } catch {
2340
2310
  next = "";
2341
2311
  }
@@ -2347,9 +2317,9 @@ var Workspace = class {
2347
2317
  }
2348
2318
  createCanvas(name, actor, content = STARTER_CANVAS) {
2349
2319
  const file = canvasPath(this.config, name);
2350
- if (fs9.existsSync(file)) throw new EditError(`Canvas "${name}" already exists.`);
2351
- fs9.mkdirSync(path9.dirname(file), { recursive: true });
2352
- fs9.writeFileSync(file, content);
2320
+ if (fs7.existsSync(file)) throw new EditError(`Canvas "${name}" already exists.`);
2321
+ fs7.mkdirSync(path8.dirname(file), { recursive: true });
2322
+ fs7.writeFileSync(file, content);
2353
2323
  this.sources.set(name, content);
2354
2324
  this.canvasesChanged();
2355
2325
  this.record(name, `Created canvas ${name}`, actor, "", content, []);
@@ -2384,10 +2354,10 @@ var Workspace = class {
2384
2354
  }
2385
2355
  renameCanvas(from, to) {
2386
2356
  const src = this.file(from);
2387
- if (!fs9.existsSync(src)) throw new EditError(`Canvas "${from}" does not exist.`);
2357
+ if (!fs7.existsSync(src)) throw new EditError(`Canvas "${from}" does not exist.`);
2388
2358
  const dest = canvasPath(this.config, to);
2389
- if (fs9.existsSync(dest)) throw new EditError(`A canvas named "${to}" already exists.`);
2390
- fs9.renameSync(src, dest);
2359
+ if (fs7.existsSync(dest)) throw new EditError(`A canvas named "${to}" already exists.`);
2360
+ fs7.renameSync(src, dest);
2391
2361
  this.comments.renameCanvas(from, to);
2392
2362
  for (const map of [this.sources, this.undoStacks, this.redoStacks]) {
2393
2363
  if (map.has(from)) {
@@ -2404,7 +2374,7 @@ var Workspace = class {
2404
2374
  duplicateCanvas(name) {
2405
2375
  const source = this.read(name);
2406
2376
  const copy = this.uniqueCanvasName(`${name}-copy`);
2407
- fs9.writeFileSync(canvasPath(this.config, copy), source);
2377
+ fs7.writeFileSync(canvasPath(this.config, copy), source);
2408
2378
  this.sources.set(copy, source);
2409
2379
  this.canvasesChanged();
2410
2380
  return copy;
@@ -2413,9 +2383,9 @@ var Workspace = class {
2413
2383
  trash = /* @__PURE__ */ new Map();
2414
2384
  deleteCanvas(name) {
2415
2385
  const file = this.file(name);
2416
- if (!fs9.existsSync(file)) throw new EditError(`Canvas "${name}" does not exist.`);
2417
- this.trash.set(name, fs9.readFileSync(file, "utf8"));
2418
- fs9.unlinkSync(file);
2386
+ if (!fs7.existsSync(file)) throw new EditError(`Canvas "${name}" does not exist.`);
2387
+ this.trash.set(name, fs7.readFileSync(file, "utf8"));
2388
+ fs7.unlinkSync(file);
2419
2389
  this.sources.delete(name);
2420
2390
  this.undoStacks.delete(name);
2421
2391
  this.redoStacks.delete(name);
@@ -2425,7 +2395,7 @@ var Workspace = class {
2425
2395
  const source = this.trash.get(name);
2426
2396
  if (source === void 0) throw new EditError(`Nothing to restore for "${name}".`);
2427
2397
  const target = this.uniqueCanvasName(name);
2428
- fs9.writeFileSync(canvasPath(this.config, target), source);
2398
+ fs7.writeFileSync(canvasPath(this.config, target), source);
2429
2399
  this.sources.set(target, source);
2430
2400
  this.trash.delete(name);
2431
2401
  this.canvasesChanged();
@@ -2436,14 +2406,14 @@ var Workspace = class {
2436
2406
  if (!fileName.endsWith(CANVAS_SUFFIX)) return;
2437
2407
  const canvas = canvasNameOf(fileName);
2438
2408
  const file = this.file(canvas);
2439
- if (!fs9.existsSync(file)) {
2409
+ if (!fs7.existsSync(file)) {
2440
2410
  this.sources.delete(canvas);
2441
2411
  syncRoute(this.config);
2442
2412
  this.emit({ type: "canvases", canvases: this.canvases() });
2443
2413
  return;
2444
2414
  }
2445
2415
  const prev = this.sources.get(canvas);
2446
- const next = fs9.readFileSync(file, "utf8");
2416
+ const next = fs7.readFileSync(file, "utf8");
2447
2417
  if (prev === next) return;
2448
2418
  this.sources.set(canvas, next);
2449
2419
  if (prev === void 0) {
@@ -2462,8 +2432,8 @@ var Workspace = class {
2462
2432
  } catch (err) {
2463
2433
  if (!(err instanceof ConflictError)) throw err;
2464
2434
  if (err.file) this.linkedFileChanged(err.file);
2465
- else this.externalChange(path9.basename(this.file(cmd.canvas)));
2466
- if (nodeRefs(cmd).length || "frame" in cmd && cmd.frame) throw new EditError(`${path9.basename(err.file ?? this.relFile(cmd.canvas))} changed while editing. Re-read the canvas and try again.`);
2435
+ else this.externalChange(path8.basename(this.file(cmd.canvas)));
2436
+ if (nodeRefs(cmd).length || "frame" in cmd && cmd.frame) throw new EditError(`${path8.basename(err.file ?? this.relFile(cmd.canvas))} changed while editing. Re-read the canvas and try again.`);
2467
2437
  return this.exec(cmd, actor);
2468
2438
  }
2469
2439
  });
@@ -2473,7 +2443,7 @@ var Workspace = class {
2473
2443
  async exec(cmd, actor) {
2474
2444
  cmd = { ...cmd };
2475
2445
  let prepared = null;
2476
- if (cmd.op === "add_component_frame" && !fs9.existsSync(this.file(cmd.canvas))) {
2446
+ if (cmd.op === "add_component_frame" && !fs7.existsSync(this.file(cmd.canvas))) {
2477
2447
  await this.catalog.load();
2478
2448
  const spec = this.catalog.get(cmd.component);
2479
2449
  if (!spec) throw new EditError(`Unknown component "${cmd.component}". Use list_components.`);
@@ -2501,12 +2471,12 @@ var Workspace = class {
2501
2471
  prepared = { kind: "apply", frame, ...plan };
2502
2472
  }
2503
2473
  const rel = target ?? this.relFile(cmd.canvas);
2504
- const file = path9.join(this.config.root, rel);
2505
- const original = target ? fs9.readFileSync(file, "utf8") : canvasSource;
2474
+ const file = path8.join(this.config.root, rel);
2475
+ const original = target ? fs7.readFileSync(file, "utf8") : canvasSource;
2506
2476
  const crlf = original.includes("\r\n");
2507
2477
  const before = crlf ? original.replace(/\r\n/g, "\n") : original;
2508
- const parse2 = (src) => target ? parseTargetDoc(target, src) : parseCanvas(cmd.canvas, rel, src);
2509
- const doc = parse2(before);
2478
+ const parse = (src) => target ? parseTargetDoc(target, src) : parseCanvas(cmd.canvas, rel, src);
2479
+ const doc = parse(before);
2510
2480
  if (!doc) throw new EditError(`${rel} has data or logic in its component now, so it can't be edited from the canvas. Explore a copy instead.`);
2511
2481
  if (cmd.op === "insert_icon") {
2512
2482
  const icon = cmd;
@@ -2560,7 +2530,7 @@ var Workspace = class {
2560
2530
  imports = importsFor(this.config, jsx, before, doc, rel, fileRel);
2561
2531
  movedNames = new Set(componentNamesIn(jsx));
2562
2532
  }
2563
- const abs = path9.join(this.config.root, fileRel);
2533
+ const abs = path8.join(this.config.root, fileRel);
2564
2534
  newComponent = { rel: fileRel, content: await this.format(componentFile(name, jsx, imports), abs).catch(() => componentFile(name, jsx, imports)) };
2565
2535
  prepared = { kind: "component", name, file: fileRel };
2566
2536
  }
@@ -2578,8 +2548,8 @@ var Workspace = class {
2578
2548
  let after = await finish(result);
2579
2549
  let ids = [];
2580
2550
  const reparse = (src) => {
2581
- const d = parse2(src);
2582
- if (!d) throw new EditError(`That edit would put logic into ${path9.basename(rel)}. Only plain JSX can be edited from the canvas.`);
2551
+ const d = parse(src);
2552
+ if (!d) throw new EditError(`That edit would put logic into ${path8.basename(rel)}. Only plain JSX can be edited from the canvas.`);
2583
2553
  return d;
2584
2554
  };
2585
2555
  if (after.includes(MARK)) {
@@ -2604,8 +2574,8 @@ var Workspace = class {
2604
2574
  if (cmd.op === "apply_to_page") ids = [findFrame(canvasDoc, cmd.frame).id];
2605
2575
  const writeNewComponent = () => {
2606
2576
  if (!newComponent) return;
2607
- fs9.mkdirSync(path9.dirname(path9.join(this.config.root, newComponent.rel)), { recursive: true });
2608
- fs9.writeFileSync(path9.join(this.config.root, newComponent.rel), newComponent.content, { flag: "wx" });
2577
+ fs7.mkdirSync(path8.dirname(path8.join(this.config.root, newComponent.rel)), { recursive: true });
2578
+ fs7.writeFileSync(path8.join(this.config.root, newComponent.rel), newComponent.content, { flag: "wx" });
2609
2579
  this.catalog.invalidate();
2610
2580
  this.emit({ type: "catalog" });
2611
2581
  };
@@ -2614,11 +2584,11 @@ var Workspace = class {
2614
2584
  writeNewComponent();
2615
2585
  return { doc: target ? this.doc(cmd.canvas) : view, ids, label };
2616
2586
  }
2617
- if (fs9.readFileSync(file, "utf8") !== original) throw new ConflictError(`${path9.basename(rel)} changed during the edit.`, target ?? void 0);
2587
+ if (fs7.readFileSync(file, "utf8") !== original) throw new ConflictError(`${path8.basename(rel)} changed during the edit.`, target ?? void 0);
2618
2588
  writeNewComponent();
2619
2589
  if (target) this.sources.set(`file:${target}`, after);
2620
2590
  else this.sources.set(cmd.canvas, after);
2621
- fs9.writeFileSync(file, after);
2591
+ fs7.writeFileSync(file, after);
2622
2592
  const newDoc = this.doc(cmd.canvas);
2623
2593
  this.record(cmd.canvas, label, actor, original, after, ids, target ?? void 0, touchedFrames(cmd, ids, view, newDoc));
2624
2594
  if (renamedFrame) {
@@ -2644,7 +2614,7 @@ var Workspace = class {
2644
2614
  const f = findFrame(view, cmd.parent);
2645
2615
  if (f?.link) {
2646
2616
  if (!f.link.editable) throw viewOnly(f);
2647
- const src = fs9.readFileSync(path9.join(this.config.root, f.link.page), "utf8");
2617
+ const src = fs7.readFileSync(path8.join(this.config.root, f.link.page), "utf8");
2648
2618
  if (f.link.kind === "component") {
2649
2619
  const own = parseModuleDoc(f.link.page, src)?.frames.find((x) => x.frameName === f.link.route);
2650
2620
  if (!own) throw viewOnly(f);
@@ -2669,7 +2639,7 @@ var Workspace = class {
2669
2639
  const refs = nodeRefs(cmd);
2670
2640
  const files = new Set(refs.map(fileOfId));
2671
2641
  if (files.size > 1) {
2672
- const names = [...files].map((f) => f ? path9.basename(f) : "the canvas");
2642
+ const names = [...files].map((f) => f ? path8.basename(f) : "the canvas");
2673
2643
  throw new EditError(`These layers live in different files (${names.join(", ")}). Edit them one file at a time.`);
2674
2644
  }
2675
2645
  return [...files][0] ?? null;
@@ -2717,12 +2687,12 @@ var Workspace = class {
2717
2687
  const doc = ed.doc;
2718
2688
  const guard = (node, verb) => {
2719
2689
  if (doc.canvasNode) return;
2720
- if (node.path.length <= 2) throw new EditError(`Can't ${verb} the root element of ${path9.basename(rel)}.`);
2721
- if (node.kind === "expression" && node.name.trim() === "children" && /^layout\.[jt]sx?$/.test(path9.basename(rel))) throw new EditError(`Can't ${verb} the {children} slot of ${path9.basename(rel)}: that's where the page renders.`);
2690
+ if (node.path.length <= 2) throw new EditError(`Can't ${verb} the root element of ${path8.basename(rel)}.`);
2691
+ if (node.kind === "expression" && node.name.trim() === "children" && /^layout\.[jt]sx?$/.test(path8.basename(rel))) throw new EditError(`Can't ${verb} the {children} slot of ${path8.basename(rel)}: that's where the page renders.`);
2722
2692
  };
2723
2693
  const literalOnly = (node, key) => {
2724
2694
  const v = node.props[key];
2725
- if (!doc.canvasNode && v?.kind === "expression") throw new EditError(`${key} on ${node.name} is computed in code ({${short(v.code, 50)}}). Edit it in ${path9.basename(rel)}, or ask your agent.`);
2695
+ if (!doc.canvasNode && v?.kind === "expression") throw new EditError(`${key} on ${node.name} is computed in code ({${short(v.code, 50)}}). Edit it in ${path8.basename(rel)}, or ask your agent.`);
2726
2696
  };
2727
2697
  switch (cmd.op) {
2728
2698
  case "set_props": {
@@ -3187,11 +3157,11 @@ ${rows}
3187
3157
  const entry = stack[stack.length - 1];
3188
3158
  if (!entry) throw new EditError(`Nothing to ${dir}.`);
3189
3159
  if (dir === "undo" && entry.before === "") throw new EditError("Cannot undo creating the canvas; delete the file instead.");
3190
- const file = entry.file ? path9.join(this.config.root, entry.file) : this.file(canvas);
3191
- const name = entry.file ? path9.basename(entry.file) : "the file";
3160
+ const file = entry.file ? path8.join(this.config.root, entry.file) : this.file(canvas);
3161
+ const name = entry.file ? path8.basename(entry.file) : "the file";
3192
3162
  let current;
3193
3163
  try {
3194
- current = entry.file ? fs9.readFileSync(file, "utf8") : this.read(canvas);
3164
+ current = entry.file ? fs7.readFileSync(file, "utf8") : this.read(canvas);
3195
3165
  } catch {
3196
3166
  throw new EditError(`Cannot ${dir} "${entry.label}": ${name} can't be read right now.`);
3197
3167
  }
@@ -3200,7 +3170,7 @@ ${rows}
3200
3170
  stack.pop();
3201
3171
  const next = dir === "undo" ? entry.before : entry.after;
3202
3172
  this.sources.set(entry.file ? `file:${entry.file}` : canvas, next);
3203
- fs9.writeFileSync(file, next);
3173
+ fs7.writeFileSync(file, next);
3204
3174
  const list = to.get(canvas) ?? [];
3205
3175
  list.push(entry);
3206
3176
  to.set(canvas, list);
@@ -3241,17 +3211,17 @@ ${rows}
3241
3211
  if (this.agents.delete(session)) this.emit({ type: "agents", agents: [...this.agents.values()] });
3242
3212
  }
3243
3213
  ensureRoute() {
3244
- if (!fs9.existsSync(path9.join(this.config.root, this.config.canvasDir))) {
3245
- fs9.mkdirSync(path9.join(this.config.root, this.config.canvasDir), { recursive: true });
3214
+ if (!fs7.existsSync(path8.join(this.config.root, this.config.canvasDir))) {
3215
+ fs7.mkdirSync(path8.join(this.config.root, this.config.canvasDir), { recursive: true });
3246
3216
  }
3247
3217
  if (!this.canvases().length) {
3248
3218
  const file = canvasPath(this.config, "home");
3249
3219
  const content = this.homeCanvas();
3250
- fs9.writeFileSync(file, content);
3220
+ fs7.writeFileSync(file, content);
3251
3221
  void this.format(content, file).then((pretty) => {
3252
- if (fs9.readFileSync(file, "utf8") !== content) return;
3222
+ if (fs7.readFileSync(file, "utf8") !== content) return;
3253
3223
  this.sources.set("home", pretty);
3254
- fs9.writeFileSync(file, pretty);
3224
+ fs7.writeFileSync(file, pretty);
3255
3225
  }).catch(() => {
3256
3226
  });
3257
3227
  }
@@ -3262,7 +3232,7 @@ ${rows}
3262
3232
  try {
3263
3233
  const home = listRoutes(this.config).find((r) => r.route === "/");
3264
3234
  if (!home) return STARTER_CANVAS;
3265
- const rel = path9.relative(this.config.root, canvasPath(this.config, "home"));
3235
+ const rel = path8.relative(this.config.root, canvasPath(this.config, "home"));
3266
3236
  const empty = parseCanvas("home", rel, `import { Canvas, Frame } from "truecanvas";
3267
3237
  export default function C() { return <Canvas></Canvas>; }
3268
3238
  `);
@@ -3320,7 +3290,7 @@ function publicEntry(e) {
3320
3290
  }
3321
3291
  function assertShown(view, rel) {
3322
3292
  const shown = view.frames.some((f) => f.link?.files.includes(rel));
3323
- if (!shown || path9.isAbsolute(rel) || rel.split(/[\\/]/).includes("..")) throw new EditError(`${rel} isn't shown on this canvas. Re-read the canvas for current ids.`);
3293
+ if (!shown || path8.isAbsolute(rel) || rel.split(/[\\/]/).includes("..")) throw new EditError(`${rel} isn't shown on this canvas. Re-read the canvas for current ids.`);
3324
3294
  }
3325
3295
  function rightEdge(doc) {
3326
3296
  return doc.frames.reduce((m, f) => Math.max(m, f.x + f.width), -80);
@@ -3392,15 +3362,15 @@ function componentJsx(spec, props = {}, text) {
3392
3362
 
3393
3363
  // src/core/pm.ts
3394
3364
  import { spawn } from "child_process";
3395
- import fs10 from "fs";
3396
- import path10 from "path";
3365
+ import fs8 from "fs";
3366
+ import path9 from "path";
3397
3367
  function detectPm(dir) {
3398
- for (let d = dir; ; d = path10.dirname(d)) {
3399
- if (fs10.existsSync(path10.join(d, "pnpm-lock.yaml")) || fs10.existsSync(path10.join(d, "pnpm-workspace.yaml"))) return "pnpm";
3400
- if (fs10.existsSync(path10.join(d, "yarn.lock"))) return "yarn";
3401
- if (fs10.existsSync(path10.join(d, "bun.lock")) || fs10.existsSync(path10.join(d, "bun.lockb"))) return "bun";
3402
- if (fs10.existsSync(path10.join(d, "package-lock.json"))) return "npm";
3403
- if (path10.dirname(d) === d || fs10.existsSync(path10.join(d, ".git"))) break;
3368
+ for (let d = dir; ; d = path9.dirname(d)) {
3369
+ if (fs8.existsSync(path9.join(d, "pnpm-lock.yaml")) || fs8.existsSync(path9.join(d, "pnpm-workspace.yaml"))) return "pnpm";
3370
+ if (fs8.existsSync(path9.join(d, "yarn.lock"))) return "yarn";
3371
+ if (fs8.existsSync(path9.join(d, "bun.lock")) || fs8.existsSync(path9.join(d, "bun.lockb"))) return "bun";
3372
+ if (fs8.existsSync(path9.join(d, "package-lock.json"))) return "npm";
3373
+ if (path9.dirname(d) === d || fs8.existsSync(path9.join(d, ".git"))) break;
3404
3374
  }
3405
3375
  const agent = process.env.npm_config_user_agent ?? "";
3406
3376
  if (agent.startsWith("pnpm")) return "pnpm";
@@ -3420,7 +3390,12 @@ function dlx(pm, pkg, args) {
3420
3390
  return ["npx", ["--yes", pkg, ...args]];
3421
3391
  }
3422
3392
  function toolEnv(extra = {}) {
3423
- return { ...process.env, PATH: `${path10.dirname(process.execPath)}${path10.delimiter}${process.env.PATH ?? ""}`, ...extra };
3393
+ return { ...process.env, PATH: `${path9.dirname(process.execPath)}${path9.delimiter}${process.env.PATH ?? ""}`, ...extra };
3394
+ }
3395
+ function shellSafe(cmd, args) {
3396
+ if (process.platform !== "win32") return { cmd, args, shell: false };
3397
+ const quote = (a) => /[\s"&|<>^()%!]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a;
3398
+ return { cmd: quote(cmd), args: args.map(quote), shell: true };
3424
3399
  }
3425
3400
  function run2(cmd, args, cwd, timeoutMs = 3e5) {
3426
3401
  return new Promise((resolve) => {
@@ -3432,7 +3407,8 @@ function run2(cmd, args, cwd, timeoutMs = 3e5) {
3432
3407
  clearTimeout(timer);
3433
3408
  resolve({ ok, out: (out + extra).slice(-2e4) });
3434
3409
  };
3435
- const p = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: toolEnv({ CI: "1", FORCE_COLOR: "0", NO_COLOR: "1" }), shell: process.platform === "win32" });
3410
+ const safe = shellSafe(cmd, args);
3411
+ const p = spawn(safe.cmd, safe.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: toolEnv({ CI: "1", FORCE_COLOR: "0", NO_COLOR: "1" }), shell: safe.shell });
3436
3412
  const timer = setTimeout(() => {
3437
3413
  p.kill();
3438
3414
  finish(false, `
@@ -3447,24 +3423,24 @@ ${err.message}`));
3447
3423
  }
3448
3424
 
3449
3425
  // src/core/shadcn.ts
3450
- import fs11 from "fs";
3451
- import path11 from "path";
3426
+ import fs9 from "fs";
3427
+ import path10 from "path";
3452
3428
  function shadcnStatus(root) {
3453
- const file = path11.join(root, "components.json");
3454
- if (!fs11.existsSync(file)) return { initialized: false, uiDir: null, installed: [] };
3429
+ const file = path10.join(root, "components.json");
3430
+ if (!fs9.existsSync(file)) return { initialized: false, uiDir: null, installed: [] };
3455
3431
  let ui = "@/components/ui";
3456
3432
  try {
3457
- ui = JSON.parse(fs11.readFileSync(file, "utf8"))?.aliases?.ui ?? ui;
3433
+ ui = JSON.parse(fs9.readFileSync(file, "utf8"))?.aliases?.ui ?? ui;
3458
3434
  } catch {
3459
3435
  }
3460
3436
  const dir = resolveAliasPath(root, ui);
3461
3437
  let installed = [];
3462
3438
  try {
3463
- installed = fs11.readdirSync(dir).filter((f) => /\.(tsx|jsx)$/.test(f)).map((f) => f.replace(/\.(tsx|jsx)$/, "")).sort();
3439
+ installed = fs9.readdirSync(dir).filter((f) => /\.(tsx|jsx)$/.test(f)).map((f) => f.replace(/\.(tsx|jsx)$/, "")).sort();
3464
3440
  } catch {
3465
3441
  installed = [];
3466
3442
  }
3467
- return { initialized: true, uiDir: path11.relative(root, dir), installed };
3443
+ return { initialized: true, uiDir: posix(path10.relative(root, dir)), installed };
3468
3444
  }
3469
3445
  var FALLBACK = "accordion alert alert-dialog aspect-ratio avatar badge breadcrumb button button-group calendar card carousel chart checkbox collapsible combobox command context-menu dialog drawer dropdown-menu empty field form hover-card input input-group input-otp item kbd label menubar navigation-menu pagination popover progress radio-group resizable scroll-area select separator sheet sidebar skeleton slider sonner spinner switch table tabs textarea toggle toggle-group tooltip".split(" ");
3470
3446
  var registryCache = null;
@@ -3596,8 +3572,8 @@ function describeComponent(c, verbose = false) {
3596
3572
  }
3597
3573
 
3598
3574
  // src/core/tokens.ts
3599
- import fs12 from "fs";
3600
- import path12 from "path";
3575
+ import fs10 from "fs";
3576
+ import path11 from "path";
3601
3577
  import { globSync } from "tinyglobby";
3602
3578
  var GROUPS = [
3603
3579
  ["--color-", "Colors", "bg-{name} text-{name} border-{name}"],
@@ -3614,9 +3590,9 @@ function readDesignTokens(root) {
3614
3590
  const sources = [];
3615
3591
  let darkVariant = null;
3616
3592
  for (const file of files) {
3617
- const css = fs12.readFileSync(file, "utf8");
3593
+ const css = fs10.readFileSync(file, "utf8");
3618
3594
  if (!css.includes("@theme") && !css.includes("@custom-variant")) continue;
3619
- sources.push(path12.relative(root, file));
3595
+ sources.push(posix(path11.relative(root, file)));
3620
3596
  const dv = /@custom-variant\s+dark\s*\(([^;]+)\);/.exec(css);
3621
3597
  if (dv) darkVariant = dv[1].trim();
3622
3598
  for (const block of css.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g)) {
@@ -3655,7 +3631,7 @@ function readTokenData(root) {
3655
3631
  for (const m of body.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)) into.set(m[1], m[2].trim());
3656
3632
  };
3657
3633
  for (const file of files) {
3658
- const css = fs12.readFileSync(file, "utf8");
3634
+ const css = fs10.readFileSync(file, "utf8");
3659
3635
  for (const block of css.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g)) decls(block[1], theme);
3660
3636
  for (const block of css.matchAll(/(?:^|\n)\s*:root\s*\{([\s\S]*?)\n\}/g)) decls(block[1], lightVars);
3661
3637
  for (const block of css.matchAll(/(?:^|\n)\s*(?:\.dark|\[data-theme=["']?dark["']?\]|:root\.dark)\s*\{([\s\S]*?)\n\}/g)) decls(block[1], darkVars);
@@ -3687,30 +3663,30 @@ function readTokenData(root) {
3687
3663
  }
3688
3664
 
3689
3665
  // src/core/compare.ts
3690
- import fs13 from "fs";
3691
- import path13 from "path";
3666
+ import fs11 from "fs";
3667
+ import path12 from "path";
3692
3668
  var COMPARE_PREFIX = "__compare__";
3693
3669
  function writeCompare(config, canvas, source, files = {}, as = canvas) {
3694
- const routeDir = path13.join(config.root, config.appDir, "truecanvas");
3695
- const dir = path13.join(routeDir, "compare");
3696
- fs13.mkdirSync(dir, { recursive: true });
3697
- const canvasDir = path13.join(config.root, config.canvasDir);
3698
- const canvasRel = path13.join(config.canvasDir, `${canvas}.canvas.tsx`);
3670
+ const routeDir = routeDirOf(config);
3671
+ const dir = path12.join(routeDir, "compare");
3672
+ fs11.mkdirSync(dir, { recursive: true });
3673
+ const canvasDir = path12.join(config.root, config.canvasDir);
3674
+ const canvasRel = `${config.canvasDir}/${canvas}.canvas.tsx`;
3699
3675
  const swapped = /* @__PURE__ */ new Map();
3700
3676
  for (const [rel, content] of Object.entries(files)) {
3701
- const ext = path13.extname(rel);
3677
+ const ext = path12.extname(rel);
3702
3678
  const name = `${as}--${rel.slice(0, -ext.length).replace(/[^\w.-]+/g, "_")}${ext}`;
3703
- const original = path13.join(config.root, rel);
3704
- fs13.writeFileSync(path13.join(dir, name), rewriteImports(content, (spec) => spec.startsWith(".") ? relativeSpec(dir, path13.resolve(path13.dirname(original), spec)) : spec));
3679
+ const original = path12.join(config.root, rel);
3680
+ fs11.writeFileSync(path12.join(dir, name), rewriteImports(content, (spec) => spec.startsWith(".") ? relativeSpec(dir, path12.resolve(path12.dirname(original), spec)) : spec));
3705
3681
  swapped.set(importSpecifier(config.root, canvasRel, rel), `./${name.slice(0, -ext.length)}`);
3706
3682
  }
3707
- const out = rewriteImports(source, (spec) => swapped.get(spec) ?? (spec.startsWith(".") ? relativeSpec(dir, path13.resolve(canvasDir, spec)) : spec));
3708
- fs13.writeFileSync(path13.join(dir, `${as}.canvas.tsx`), out);
3683
+ const out = rewriteImports(source, (spec) => swapped.get(spec) ?? (spec.startsWith(".") ? relativeSpec(dir, path12.resolve(canvasDir, spec)) : spec));
3684
+ fs11.writeFileSync(path12.join(dir, `${as}.canvas.tsx`), out);
3709
3685
  syncRoute(config);
3710
3686
  return `${COMPARE_PREFIX}${as}`;
3711
3687
  }
3712
3688
  function relativeSpec(fromDir, target) {
3713
- const next = path13.relative(fromDir, target);
3689
+ const next = posix(path12.relative(fromDir, target));
3714
3690
  return next.startsWith(".") ? next : `./${next}`;
3715
3691
  }
3716
3692
  function rewriteImports(source, map) {
@@ -3726,9 +3702,9 @@ function rewriteImports(source, map) {
3726
3702
  }
3727
3703
  function clearCompare(config, canvas) {
3728
3704
  if (canvas) assertCanvasName(canvas);
3729
- const dir = path13.join(config.root, config.appDir, "truecanvas", "compare");
3730
- if (!fs13.existsSync(dir)) return;
3731
- for (const f of fs13.readdirSync(dir)) if (!canvas || f === `${canvas}.canvas.tsx` || f.startsWith(`${canvas}--`)) fs13.rmSync(path13.join(dir, f));
3705
+ const dir = path12.join(routeDirOf(config), "compare");
3706
+ if (!fs11.existsSync(dir)) return;
3707
+ for (const f of fs11.readdirSync(dir)) if (!canvas || f === `${canvas}.canvas.tsx` || f.startsWith(`${canvas}--`)) fs11.rmSync(path12.join(dir, f));
3732
3708
  syncRoute(config);
3733
3709
  }
3734
3710
  function frameChanges(before, after) {
@@ -3745,24 +3721,48 @@ function frameChanges(before, after) {
3745
3721
  }
3746
3722
 
3747
3723
  // src/core/init.ts
3748
- import fs14 from "fs";
3724
+ import fs12 from "fs";
3749
3725
  import MagicString2 from "magic-string";
3750
- import path14 from "path";
3751
- var CONFIG_FILES = ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.mts"];
3752
- function nextConfigStatus(root) {
3753
- for (const name of CONFIG_FILES) {
3754
- const file = path14.join(root, name);
3755
- if (fs14.existsSync(file)) return { file, wrapped: fs14.readFileSync(file, "utf8").includes("withTruecanvas") };
3726
+ import path13 from "path";
3727
+ var CONFIG_FILES = {
3728
+ next: ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.mts"],
3729
+ vite: ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]
3730
+ };
3731
+ function projectFramework(root) {
3732
+ try {
3733
+ const pkg = JSON.parse(fs12.readFileSync(path13.join(root, "package.json"), "utf8"));
3734
+ return detectFramework({ ...pkg.dependencies, ...pkg.devDependencies });
3735
+ } catch {
3736
+ return "next";
3737
+ }
3738
+ }
3739
+ function configStatus(root, framework = projectFramework(root)) {
3740
+ const marker = framework === "vite" ? "truecanvas/vite" : "withTruecanvas";
3741
+ for (const name of CONFIG_FILES[framework]) {
3742
+ const file = path13.join(root, name);
3743
+ if (fs12.existsSync(file)) return { file, wrapped: fs12.readFileSync(file, "utf8").includes(marker), framework };
3756
3744
  }
3757
- return { file: null, wrapped: false };
3745
+ return { file: null, wrapped: false, framework };
3758
3746
  }
3759
3747
  function initProject(config) {
3760
3748
  const { root } = config;
3761
3749
  const report = { done: [], todo: [] };
3762
- const status = nextConfigStatus(root);
3763
- if (!status.file) {
3764
- const file = path14.join(root, "next.config.ts");
3765
- fs14.writeFileSync(file, `import type { NextConfig } from "next";
3750
+ const vite = config.framework === "vite";
3751
+ const status = configStatus(root, config.framework);
3752
+ if (vite) {
3753
+ const name = status.file ? path13.basename(status.file) : "vite.config.ts";
3754
+ if (status.wrapped) report.done.push(`${name} already uses the truecanvas() plugin`);
3755
+ else {
3756
+ const src = status.file ? fs12.readFileSync(status.file, "utf8") : null;
3757
+ const next = src && addVitePlugin(src);
3758
+ if (next) {
3759
+ fs12.writeFileSync(status.file, next);
3760
+ report.done.push(`Added the truecanvas() plugin to ${name}`);
3761
+ } else report.todo.push(`Add truecanvas() from "truecanvas/vite" to the plugins in ${name}`);
3762
+ }
3763
+ } else if (!status.file) {
3764
+ const file = path13.join(root, "next.config.ts");
3765
+ fs12.writeFileSync(file, `import type { NextConfig } from "next";
3766
3766
  import { withTruecanvas } from "truecanvas/next";
3767
3767
 
3768
3768
  const nextConfig: NextConfig = {};
@@ -3771,49 +3771,49 @@ export default withTruecanvas(nextConfig);
3771
3771
  `);
3772
3772
  report.done.push("Created next.config.ts with withTruecanvas()");
3773
3773
  } else if (status.wrapped) {
3774
- report.done.push(`${path14.basename(status.file)} already uses withTruecanvas()`);
3774
+ report.done.push(`${path13.basename(status.file)} already uses withTruecanvas()`);
3775
3775
  } else {
3776
- const src = fs14.readFileSync(status.file, "utf8");
3776
+ const src = fs12.readFileSync(status.file, "utf8");
3777
3777
  const next = wrapConfigExport(src);
3778
3778
  if (next && next !== src && next.includes("withTruecanvas(")) {
3779
- fs14.writeFileSync(status.file, next);
3780
- report.done.push(`Wrapped ${path14.basename(status.file)} with withTruecanvas()`);
3779
+ fs12.writeFileSync(status.file, next);
3780
+ report.done.push(`Wrapped ${path13.basename(status.file)} with withTruecanvas()`);
3781
3781
  } else {
3782
- report.todo.push(`Wrap your ${path14.basename(status.file)} export with withTruecanvas() from "truecanvas/next"`);
3782
+ report.todo.push(`Wrap your ${path13.basename(status.file)} export with withTruecanvas() from "truecanvas/next"`);
3783
3783
  }
3784
3784
  }
3785
- const pkgFile = path14.join(root, "package.json");
3785
+ const pkgFile = path13.join(root, "package.json");
3786
3786
  try {
3787
- const raw = fs14.readFileSync(pkgFile, "utf8");
3787
+ const raw = fs12.readFileSync(pkgFile, "utf8");
3788
3788
  const pkg = JSON.parse(raw);
3789
3789
  pkg.scripts ??= {};
3790
3790
  if (!Object.values(pkg.scripts).some((v) => typeof v === "string" && v.includes("truecanvas"))) {
3791
3791
  pkg.scripts.canvas = "truecanvas dev";
3792
3792
  const indent = /\n(\s+)"/.exec(raw)?.[1] ?? " ";
3793
- fs14.writeFileSync(pkgFile, `${JSON.stringify(pkg, null, indent)}
3793
+ fs12.writeFileSync(pkgFile, `${JSON.stringify(pkg, null, indent)}
3794
3794
  `);
3795
3795
  report.done.push(`Added the "canvas" script (npm run canvas)`);
3796
3796
  }
3797
3797
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3798
3798
  if (!("tailwindcss" in deps)) report.todo.push("Tailwind CSS isn't installed: layout and style controls write Tailwind classes");
3799
- if (!("next" in deps)) report.todo.push("Next.js isn't in package.json: Truecanvas currently needs a Next.js (App Router) app");
3799
+ if (!("next" in deps) && !("vite" in deps)) report.todo.push("Neither Next.js nor Vite is in package.json: Truecanvas needs a Next.js (App Router) or Vite + React app");
3800
3800
  } catch {
3801
3801
  report.todo.push("Couldn't read package.json");
3802
3802
  }
3803
- const ignoreFile = path14.join(root, ".gitignore");
3804
- const entry = `/${config.appDir}/truecanvas/`;
3805
- const ignore = fs14.existsSync(ignoreFile) ? fs14.readFileSync(ignoreFile, "utf8") : "";
3803
+ const ignoreFile = path13.join(root, ".gitignore");
3804
+ const entry = `/${config.routeDir}/`;
3805
+ const ignore = fs12.existsSync(ignoreFile) ? fs12.readFileSync(ignoreFile, "utf8") : "";
3806
3806
  if (!ignore.split("\n").some((l) => l.trim() === entry || l.trim() === entry.slice(1))) {
3807
- fs14.writeFileSync(ignoreFile, `${ignore}${ignore && !ignore.endsWith("\n") ? "\n" : ""}
3807
+ fs12.writeFileSync(ignoreFile, `${ignore}${ignore && !ignore.endsWith("\n") ? "\n" : ""}
3808
3808
  # Truecanvas (generated dev-only route)
3809
3809
  ${entry}
3810
3810
  `);
3811
3811
  report.done.push(`Ignored ${entry} in .gitignore`);
3812
3812
  }
3813
3813
  for (const name of ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs", "tailwind.config.cjs"]) {
3814
- const file = path14.join(root, name);
3815
- if (!fs14.existsSync(file)) continue;
3816
- const src = fs14.readFileSync(file, "utf8");
3814
+ const file = path13.join(root, name);
3815
+ if (!fs12.existsSync(file)) continue;
3816
+ const src = fs12.readFileSync(file, "utf8");
3817
3817
  const glob = `./${config.canvasDir}/**/*.{ts,tsx}`;
3818
3818
  if (src.includes(config.canvasDir)) break;
3819
3819
  const m = /content\s*:\s*\[([ \t]*\n?)([ \t]*)/.exec(src);
@@ -3822,12 +3822,12 @@ ${entry}
3822
3822
  const at = m.index + m[0].length;
3823
3823
  const entry2 = m[1].includes("\n") ? `${JSON.stringify(glob)},
3824
3824
  ${indent}` : `${JSON.stringify(glob)}, `;
3825
- fs14.writeFileSync(file, src.slice(0, at) + entry2 + src.slice(at));
3825
+ fs12.writeFileSync(file, src.slice(0, at) + entry2 + src.slice(at));
3826
3826
  report.done.push(`Added ${config.canvasDir} to Tailwind's content in ${name}`);
3827
3827
  } else report.todo.push(`Add "${glob}" to the content list in ${name} so Tailwind styles your canvases`);
3828
3828
  break;
3829
3829
  }
3830
- if (!fs14.existsSync(path14.join(root, config.appDir))) report.todo.push(`No ${config.appDir}/ folder found: Truecanvas needs the Next.js App Router`);
3830
+ if (!vite && !fs12.existsSync(path13.join(root, config.appDir))) report.todo.push(`No ${config.appDir}/ folder found: Truecanvas needs the Next.js App Router`);
3831
3831
  return report;
3832
3832
  }
3833
3833
  function addImport(src, line) {
@@ -3874,12 +3874,35 @@ ${s.toString()}`;
3874
3874
  }
3875
3875
  return null;
3876
3876
  }
3877
+ function addVitePlugin(src) {
3878
+ let ast;
3879
+ try {
3880
+ ast = parseModule(src);
3881
+ } catch {
3882
+ return null;
3883
+ }
3884
+ let plugins = null;
3885
+ walk(ast, (node) => {
3886
+ if (plugins) return false;
3887
+ if (node.type !== "ObjectProperty" || node.value.type !== "ArrayExpression") return;
3888
+ const key = node.key.type === "Identifier" ? node.key.name : node.key.type === "StringLiteral" ? node.key.value : null;
3889
+ if (key !== "plugins") return;
3890
+ plugins = { start: node.value.start, end: node.value.end, elements: node.value.elements.filter((e) => e !== null).map((e) => ({ start: e.start, end: e.end })) };
3891
+ return false;
3892
+ });
3893
+ if (!plugins) return null;
3894
+ const { start, end, elements } = plugins;
3895
+ const s = new MagicString2(src);
3896
+ if (!elements.length) s.overwrite(start, end, "[truecanvas()]");
3897
+ else {
3898
+ const last = elements[elements.length - 1];
3899
+ const before = src.slice(start + 1, elements[0].start);
3900
+ s.appendLeft(last.end, before.includes("\n") ? `,${before.replace(/^[^\n]*/, "")}truecanvas()` : ", truecanvas()");
3901
+ }
3902
+ return addImport(s.toString(), `import { truecanvas } from "truecanvas/vite";`);
3903
+ }
3877
3904
 
3878
3905
  export {
3879
- loadConfig,
3880
- parseModule,
3881
- idOf,
3882
- walk,
3883
3906
  parseCanvas,
3884
3907
  allNodes,
3885
3908
  findNode,
@@ -3895,6 +3918,8 @@ export {
3895
3918
  DEVICES,
3896
3919
  findDevice,
3897
3920
  assertCanvasName,
3921
+ syncRoute,
3922
+ viteStylesheets,
3898
3923
  syncComponentRegistry,
3899
3924
  Comments,
3900
3925
  Git,
@@ -3903,6 +3928,7 @@ export {
3903
3928
  detectPm,
3904
3929
  addArgs,
3905
3930
  toolEnv,
3931
+ shellSafe,
3906
3932
  run2 as run,
3907
3933
  shadcnStatus,
3908
3934
  shadcnRegistry,
@@ -3915,7 +3941,8 @@ export {
3915
3941
  writeCompare,
3916
3942
  clearCompare,
3917
3943
  frameChanges,
3918
- nextConfigStatus,
3944
+ configStatus,
3919
3945
  initProject,
3920
- wrapConfigExport
3946
+ wrapConfigExport,
3947
+ addVitePlugin
3921
3948
  };