truecanvas 0.2.0 → 0.4.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,34 +968,46 @@ ${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
- list(canvas) {
987
+ read(canvas) {
1015
988
  try {
1016
- return JSON.parse(fs3.readFileSync(this.file(canvas), "utf8")).threads ?? [];
989
+ const f = JSON.parse(fs2.readFileSync(this.file(canvas), "utf8"));
990
+ return { version: 1, threads: f.threads ?? [], dismissed: f.dismissed };
1017
991
  } catch {
1018
- return [];
992
+ return { version: 1, threads: [] };
1019
993
  }
1020
994
  }
1021
- save(canvas, threads) {
995
+ list(canvas) {
996
+ return this.read(canvas).threads;
997
+ }
998
+ /** Ids of client threads deleted here. */
999
+ dismissed(canvas) {
1000
+ return new Set(this.read(canvas).dismissed ?? []);
1001
+ }
1002
+ save(canvas, threads, dismissed = this.read(canvas).dismissed) {
1022
1003
  const file = this.file(canvas);
1023
- if (!threads.length) {
1024
- if (fs3.existsSync(file)) fs3.rmSync(file);
1004
+ if (!threads.length && !dismissed?.length) {
1005
+ if (fs2.existsSync(file)) fs2.rmSync(file);
1025
1006
  return;
1026
1007
  }
1027
- fs3.mkdirSync(path3.dirname(file), { recursive: true });
1028
- fs3.writeFileSync(file, `${JSON.stringify({ version: 1, threads }, null, 2)}
1008
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
1009
+ const data = { version: 1, threads, ...dismissed?.length ? { dismissed } : {} };
1010
+ fs2.writeFileSync(file, `${JSON.stringify(data, null, 2)}
1029
1011
  `);
1030
1012
  }
1031
1013
  update(canvas, id, fn) {
@@ -1063,12 +1045,23 @@ var Comments = class {
1063
1045
  return this.update(canvas, id, (t) => {
1064
1046
  t.resolved = resolved;
1065
1047
  t.resolvedBy = resolved ? author : void 0;
1048
+ t.resolvedAt = Date.now();
1066
1049
  });
1067
1050
  }
1051
+ /** Applies synced threads (from a share link) in one write; returns whether anything changed. */
1052
+ merge(canvas, fn) {
1053
+ const threads = this.list(canvas);
1054
+ if (!fn(threads)) return false;
1055
+ this.save(canvas, threads);
1056
+ return true;
1057
+ }
1068
1058
  remove(canvas, id) {
1059
+ const { threads, dismissed = [] } = this.read(canvas);
1060
+ const gone = threads.find((t) => t.id === id);
1069
1061
  this.save(
1070
1062
  canvas,
1071
- this.list(canvas).filter((t) => t.id !== id)
1063
+ threads.filter((t) => t.id !== id),
1064
+ gone?.share ? [.../* @__PURE__ */ new Set([...dismissed, id])] : dismissed
1072
1065
  );
1073
1066
  }
1074
1067
  /** Frames keep their comments when renamed. */
@@ -1080,14 +1073,13 @@ var Comments = class {
1080
1073
  }
1081
1074
  renameCanvas(from, to) {
1082
1075
  const a = this.file(from);
1083
- if (fs3.existsSync(a)) fs3.renameSync(a, this.file(to));
1076
+ if (fs2.existsSync(a)) fs2.renameSync(a, this.file(to));
1084
1077
  }
1085
1078
  };
1086
1079
 
1087
1080
  // src/core/git.ts
1088
1081
  import { execFile } from "child_process";
1089
- import fs4 from "fs";
1090
- import path4 from "path";
1082
+ import path3 from "path";
1091
1083
  var GitError = class extends EditError {
1092
1084
  };
1093
1085
  function git(cwd, args, timeout = 3e4, opts = {}) {
@@ -1127,6 +1119,7 @@ var Git = class {
1127
1119
  return { repo: false, root: null, branch: null, unborn: false, upstream: null, ahead: 0, behind: 0, files: [], user: null, defaultBranch: null, remote: null };
1128
1120
  }
1129
1121
  const out = await git(this.cwd, ["status", "--porcelain=v2", "-z", "--branch", "--untracked-files=all", "--", "."]);
1122
+ const prefix = await this.prefix();
1130
1123
  const s = { repo: true, root, branch: null, unborn: false, upstream: null, ahead: 0, behind: 0, files: [], user: null, defaultBranch: null, remote: null };
1131
1124
  const entries = out.split("\0");
1132
1125
  for (let i = 0; i < entries.length; i++) {
@@ -1143,10 +1136,10 @@ var Git = class {
1143
1136
  const file = parts.slice(line.startsWith("2 ") ? 9 : 8).join(" ");
1144
1137
  if (line.startsWith("2 ")) i++;
1145
1138
  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] !== "." });
1139
+ s.files.push({ path: this.rel(prefix, file), status: code === "A" ? "added" : code === "D" ? "deleted" : code === "R" ? "renamed" : "modified", staged: xy[0] !== "." });
1147
1140
  } 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 });
1141
+ s.files.push({ path: this.rel(prefix, line.split(" ").slice(10).join(" ")), status: "conflicted", staged: false });
1142
+ } else if (line.startsWith("? ")) s.files.push({ path: this.rel(prefix, line.slice(2)), status: "untracked", staged: false });
1150
1143
  }
1151
1144
  s.user = (await git(this.cwd, ["config", "user.name"]).catch(() => "")).trim() || null;
1152
1145
  s.remote = (await git(this.cwd, ["remote", "get-url", "origin"]).catch(() => "")).trim() || null;
@@ -1201,8 +1194,8 @@ var Git = class {
1201
1194
  safeRef(branch);
1202
1195
  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
1196
  const os = await import("os");
1204
- const fs15 = await import("fs");
1205
- const index = path4.join(os.tmpdir(), `truecanvas-index-${process.pid}-${Date.now()}`);
1197
+ const fs13 = await import("fs");
1198
+ const index = path3.join(os.tmpdir(), `truecanvas-index-${process.pid}-${Date.now()}`);
1206
1199
  const env = { GIT_INDEX_FILE: index };
1207
1200
  try {
1208
1201
  if (tip) await git(this.cwd, ["read-tree", tip], 3e4, { env });
@@ -1216,7 +1209,7 @@ var Git = class {
1216
1209
  await git(this.cwd, ["update-ref", `refs/heads/${branch}`, commit]);
1217
1210
  await git(this.cwd, ["push", "origin", `refs/heads/${branch}:refs/heads/${branch}`], 12e4);
1218
1211
  } finally {
1219
- fs15.rmSync(index, { force: true });
1212
+ fs13.rmSync(index, { force: true });
1220
1213
  }
1221
1214
  }
1222
1215
  // ---------- GitHub (gh CLI) ----------
@@ -1250,19 +1243,16 @@ var Git = class {
1250
1243
  return (await git(this.cwd, ["remote", "get-url", "origin"])).trim();
1251
1244
  }
1252
1245
  /** 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));
1246
+ rel(prefix, file) {
1247
+ return file.startsWith(prefix) ? file.slice(prefix.length) : posix(path3.posix.relative(prefix, file));
1255
1248
  }
1256
1249
  /**
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.
1250
+ * The project folder inside the repo ("apps/web/", or "" at the root), as git
1251
+ * itself sees it. Asking git instead of comparing file system paths holds up
1252
+ * through symlinks (macOS /var → /private/var), junctions and Windows short names.
1259
1253
  */
1260
- realCwd() {
1261
- try {
1262
- return fs4.realpathSync(this.cwd);
1263
- } catch {
1264
- return this.cwd;
1265
- }
1254
+ async prefix() {
1255
+ return (await git(this.cwd, ["rev-parse", "--show-prefix"]).catch(() => "")).trim();
1266
1256
  }
1267
1257
  async branches() {
1268
1258
  const out = await git(this.cwd, ["branch", "-a", "--format=%(refname)%09%(HEAD)"]).catch(() => "");
@@ -1290,8 +1280,7 @@ var Git = class {
1290
1280
  }
1291
1281
  /** A file's content at a ref, or null if it doesn't exist there. */
1292
1282
  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("/");
1283
+ const inRepo = path3.posix.normalize(`${await this.prefix()}${posix(file)}`);
1295
1284
  return git(this.cwd, ["show", `${safeRef(ref)}:${inRepo}`, "--"]).catch(() => null);
1296
1285
  }
1297
1286
  async commit(message, files) {
@@ -1320,17 +1309,17 @@ var Git = class {
1320
1309
  };
1321
1310
 
1322
1311
  // src/core/workspace.ts
1323
- import fs9 from "fs";
1324
- import path9 from "path";
1312
+ import fs7 from "fs";
1313
+ import path8 from "path";
1325
1314
  import { createRequire } from "module";
1326
1315
 
1327
1316
  // src/core/link.ts
1328
- import fs6 from "fs";
1329
- import path6 from "path";
1317
+ import fs4 from "fs";
1318
+ import path5 from "path";
1330
1319
 
1331
1320
  // src/core/routes.ts
1332
- import fs5 from "fs";
1333
- import path5 from "path";
1321
+ import fs3 from "fs";
1322
+ import path4 from "path";
1334
1323
 
1335
1324
  // src/core/source.ts
1336
1325
  function defaultReturn(source, layout) {
@@ -1542,28 +1531,29 @@ function parseTargetDoc(file, source) {
1542
1531
  var PAGE = /^page\.(tsx|jsx|ts|js)$/;
1543
1532
  var LAYOUT = ["layout.tsx", "layout.jsx", "layout.js", "layout.ts"];
1544
1533
  function listRoutes(config) {
1545
- const appDir = path5.join(config.root, config.appDir);
1534
+ if (config.framework !== "next") return [];
1535
+ const appDir = path4.join(config.root, config.appDir);
1546
1536
  const out = [];
1547
1537
  const walkDir = (dir, segments) => {
1548
1538
  let entries;
1549
1539
  try {
1550
- entries = fs5.readdirSync(dir, { withFileTypes: true });
1540
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
1551
1541
  } catch {
1552
1542
  return;
1553
1543
  }
1554
1544
  for (const e of entries) {
1555
1545
  if (e.isFile() && PAGE.test(e.name)) {
1556
1546
  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("[")) });
1547
+ out.push({ route: `/${visible.join("/")}`, file: posix(path4.relative(config.root, path4.join(dir, e.name))), dynamic: segments.some((s) => s.startsWith("[")) });
1558
1548
  }
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]);
1549
+ 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
1550
  }
1561
1551
  };
1562
1552
  walkDir(appDir, []);
1563
1553
  return out.sort((a, b) => a.route.localeCompare(b.route));
1564
1554
  }
1565
1555
  function extract(file, canvasFile, config, layout) {
1566
- const source = fs5.readFileSync(file, "utf8");
1556
+ const source = fs3.readFileSync(file, "utf8");
1567
1557
  const info = defaultReturn(source, layout);
1568
1558
  if (!info?.pure) return null;
1569
1559
  const imports = [];
@@ -1579,26 +1569,26 @@ function extract(file, canvasFile, config, layout) {
1579
1569
  }
1580
1570
  function repoint(config, spec, fromFile, toFile) {
1581
1571
  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)));
1572
+ return importSpecifier(config.root, path4.relative(config.root, toFile), path4.relative(config.root, path4.resolve(path4.dirname(fromFile), spec)));
1583
1573
  }
1584
1574
  function layoutFiles(config, pageFile) {
1585
- const appDir = path5.join(config.root, config.appDir);
1575
+ const appDir = path4.join(config.root, config.appDir);
1586
1576
  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));
1577
+ for (let dir = path4.dirname(path4.resolve(config.root, pageFile)); dir.startsWith(appDir) && dir !== appDir; dir = path4.dirname(dir)) {
1578
+ const file = LAYOUT.map((f) => path4.join(dir, f)).find((f) => fs3.existsSync(f));
1579
+ if (file) out.push(posix(path4.relative(config.root, file)));
1590
1580
  }
1591
1581
  return out;
1592
1582
  }
1593
1583
  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("@"));
1584
+ const rel = path4.relative(path4.join(config.root, config.appDir), path4.dirname(path4.resolve(config.root, pageFile)));
1585
+ const visible = rel.split(path4.sep).filter((s) => s && !/^\(.*\)$/.test(s) && !s.startsWith("@"));
1596
1586
  return `/${visible.join("/")}`;
1597
1587
  }
1598
1588
  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);
1589
+ const appDir = path4.join(config.root, config.appDir);
1590
+ const pageAbs = path4.resolve(config.root, pageFile);
1591
+ const canvasAbs = path4.resolve(config.root, canvasFile);
1602
1592
  const imports = [];
1603
1593
  let mode = "inline";
1604
1594
  const page = extract(pageAbs, canvasAbs, config, false);
@@ -1612,8 +1602,8 @@ function routeFrame(config, pageFile, canvasFile) {
1612
1602
  jsx = "<RoutePage />";
1613
1603
  }
1614
1604
  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));
1605
+ for (let dir = path4.dirname(pageAbs); dir.startsWith(appDir) && dir !== appDir; dir = path4.dirname(dir)) {
1606
+ const file = LAYOUT.map((f) => path4.join(dir, f)).find((f) => fs3.existsSync(f));
1617
1607
  if (!file) continue;
1618
1608
  const layout = extract(file, canvasAbs, config, true);
1619
1609
  const holes = layout ? (layout.jsx.match(/\{\s*children\s*\}/g) ?? []).length : 0;
@@ -1623,7 +1613,7 @@ function routeFrame(config, pageFile, canvasFile) {
1623
1613
  imports.push(...layout.imports);
1624
1614
  } else {
1625
1615
  const local = `RouteLayout${i++ || ""}`;
1626
- imports.push(`import ${local} from ${JSON.stringify(importSpecifier(config.root, canvasFile, path5.relative(config.root, file)))};`);
1616
+ imports.push(`import ${local} from ${JSON.stringify(importSpecifier(config.root, canvasFile, path4.relative(config.root, file)))};`);
1627
1617
  jsx = `<${local}>${jsx}</${local}>`;
1628
1618
  }
1629
1619
  }
@@ -1644,16 +1634,16 @@ function wrapperChain(frame) {
1644
1634
  }
1645
1635
  function readSource(config, rel) {
1646
1636
  try {
1647
- return fs6.readFileSync(path6.join(config.root, rel), "utf8");
1637
+ return fs4.readFileSync(path5.join(config.root, rel), "utf8");
1648
1638
  } catch {
1649
1639
  return null;
1650
1640
  }
1651
1641
  }
1652
1642
  var parseMemo = /* @__PURE__ */ new Map();
1653
- function memoParse(key, source, parse2) {
1643
+ function memoParse(key, source, parse) {
1654
1644
  let hit = parseMemo.get(key);
1655
1645
  if (!hit || hit.source !== source) {
1656
- hit = { source, doc: parse2() };
1646
+ hit = { source, doc: parse() };
1657
1647
  parseMemo.set(key, hit);
1658
1648
  }
1659
1649
  return hit.doc && { ...hit.doc, frames: hit.doc.frames.map(cloneTree) };
@@ -1755,7 +1745,7 @@ function linkedFrameBody(config, doc, canvasRel, page) {
1755
1745
  const pageName = importName(config, doc, canvasRel, page, `${segments.length ? pascal(segments[segments.length - 1]) : "Home"}Page`, taken, imports);
1756
1746
  let jsx = `<${pageName} />`;
1757
1747
  for (const rel of layoutFiles(config, page)) {
1758
- const dir = path6.basename(path6.dirname(rel));
1748
+ const dir = path5.basename(path5.dirname(rel));
1759
1749
  const name = importName(config, doc, canvasRel, rel, `${pascal(dir) || "Route"}Layout`, taken, imports);
1760
1750
  jsx = `<${name}>
1761
1751
  ${indentBlock(jsx)}
@@ -1777,7 +1767,7 @@ function dedent(code) {
1777
1767
  function explorationBody(config, frame, canvasRel) {
1778
1768
  if (frame.component) throw new EditError(`"${frame.frameName}" is a main component: edit it directly, every instance follows.`);
1779
1769
  if (!frame.page) throw new EditError(`"${frame.frameName}" isn't linked to a page.`);
1780
- const pageAbs = path6.join(config.root, frame.page);
1770
+ const pageAbs = path5.join(config.root, frame.page);
1781
1771
  const source = readSource(config, frame.page);
1782
1772
  if (source === null) throw new EditError(`${frame.page} doesn't exist anymore.`);
1783
1773
  const chain = wrapperChain(frame);
@@ -1789,7 +1779,7 @@ function explorationBody(config, frame, canvasRel) {
1789
1779
  for (const stmt of info.ast.program.body) {
1790
1780
  if (stmt.type !== "ImportDeclaration" || stmt.importKind === "type" || /\.(css|scss|sass|less)$/.test(stmt.source.value)) continue;
1791
1781
  const src = stmt.source.value;
1792
- const from = repoint(config, src, pageAbs, path6.join(config.root, canvasRel));
1782
+ const from = repoint(config, src, pageAbs, path5.join(config.root, canvasRel));
1793
1783
  const text = source.slice(stmt.start, stmt.end);
1794
1784
  imports.push(from === src ? text : rewriteImportSource(text, src, from));
1795
1785
  }
@@ -1805,7 +1795,7 @@ ${indentBlock(jsx)}
1805
1795
  function applyPlan(config, canvasSource, doc, frame, canvasRel) {
1806
1796
  const page = frame.from;
1807
1797
  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.`);
1798
+ if (!fs4.existsSync(path5.join(config.root, page))) throw new EditError(`${page} doesn't exist anymore.`);
1809
1799
  const layoutSpecs = new Set(doc.imports.filter((i) => /(^|\/)layout(\.\w+)?$/.test(i.source)).flatMap((i) => [...i.names, ...i.defaultName ? [i.defaultName] : []]));
1810
1800
  let content = frame.children;
1811
1801
  for (; ; ) {
@@ -1831,7 +1821,7 @@ ${indentBlock(body)}
1831
1821
  continue;
1832
1822
  }
1833
1823
  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));
1824
+ const from = repoint(config, decl.source.value, path5.join(config.root, canvasRel), path5.join(config.root, page));
1835
1825
  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
1826
  imports.push(`import ${what} from ${JSON.stringify(from)};`);
1837
1827
  }
@@ -1864,8 +1854,8 @@ function pruneImports(source, candidates) {
1864
1854
  }
1865
1855
 
1866
1856
  // src/core/motion.ts
1867
- import fs7 from "fs";
1868
- import path7 from "path";
1857
+ import fs5 from "fs";
1858
+ import path6 from "path";
1869
1859
  var REVEAL = `"use client";
1870
1860
 
1871
1861
  import { Children, cloneElement, isValidElement, useEffect, useRef, useState, type CSSProperties, type ReactElement, type ReactNode } from "react";
@@ -2090,8 +2080,8 @@ export function TextAnimate({ effect = "words", stagger = 0.06, delay = 0, durat
2090
2080
  function motionFiles(config) {
2091
2081
  const glob = config.components[0] ?? "components/**/*.tsx";
2092
2082
  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") };
2083
+ const dir = `${base}/motion`;
2084
+ return { reveal: `${dir}/reveal.tsx`, text: `${dir}/text-animate.tsx` };
2095
2085
  }
2096
2086
  function ensureMotionComponents(config) {
2097
2087
  const files = motionFiles(config);
@@ -2100,18 +2090,18 @@ function ensureMotionComponents(config) {
2100
2090
  [files.reveal, REVEAL],
2101
2091
  [files.text, TEXT_ANIMATE]
2102
2092
  ]) {
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);
2093
+ const abs = path6.join(config.root, rel);
2094
+ if (fs5.existsSync(abs)) continue;
2095
+ fs5.mkdirSync(path6.dirname(abs), { recursive: true });
2096
+ fs5.writeFileSync(abs, content);
2107
2097
  created = true;
2108
2098
  }
2109
2099
  return { ...files, created };
2110
2100
  }
2111
2101
 
2112
2102
  // src/core/components.ts
2113
- import fs8 from "fs";
2114
- import path8 from "path";
2103
+ import fs6 from "fs";
2104
+ import path7 from "path";
2115
2105
  function componentsDir(config) {
2116
2106
  const glob = config.components[0] ?? "components/**/*.tsx";
2117
2107
  return glob.split("/").filter((p) => !p.includes("*")).join("/") || "components";
@@ -2175,11 +2165,11 @@ function importsFor(config, jsx, source, doc, fromRel, toRel) {
2175
2165
  (s) => s.type === "ImportDeclaration" && s.importKind !== "type" && s.specifiers.some((sp2) => sp2.local.name === name)
2176
2166
  );
2177
2167
  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.`);
2168
+ 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
2169
  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
2170
  }
2181
2171
  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));
2172
+ const from = repoint(config, decl.source.value, path7.join(config.root, fromRel), path7.join(config.root, toRel));
2183
2173
  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
2174
  out.push(`import ${what} from ${JSON.stringify(from)};`);
2185
2175
  }
@@ -2203,8 +2193,8 @@ ${indentBlock(indentBlock(jsx.trim()))}
2203
2193
  `;
2204
2194
  }
2205
2195
  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.`);
2196
+ const rel = `${componentsDir(config)}/${kebab(name)}.tsx`;
2197
+ if (fs6.existsSync(path7.join(config.root, rel))) throw new EditError(`${rel} already exists. Pick another name.`);
2208
2198
  return rel;
2209
2199
  }
2210
2200
 
@@ -2224,7 +2214,7 @@ var Workspace = class {
2224
2214
  this.catalog = new Catalog(config.root, config.components, config.libraries);
2225
2215
  this.comments = new Comments(config);
2226
2216
  this.git = new Git(config.root);
2227
- for (const c of this.canvases()) this.sources.set(c, fs9.readFileSync(this.file(c), "utf8"));
2217
+ for (const c of this.canvases()) this.sources.set(c, fs7.readFileSync(this.file(c), "utf8"));
2228
2218
  }
2229
2219
  config;
2230
2220
  catalog;
@@ -2255,12 +2245,12 @@ var Workspace = class {
2255
2245
  return canvasPath(this.config, canvas);
2256
2246
  }
2257
2247
  relFile(canvas) {
2258
- return path9.relative(this.config.root, this.file(canvas));
2248
+ return posix(path8.relative(this.config.root, this.file(canvas)));
2259
2249
  }
2260
2250
  read(canvas) {
2261
2251
  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");
2252
+ if (!fs7.existsSync(file)) throw new EditError(`Canvas "${canvas}" does not exist. Canvases: ${this.canvases().join(", ") || "none"}`);
2253
+ const src = fs7.readFileSync(file, "utf8");
2264
2254
  if (!this.sources.has(canvas)) this.sources.set(canvas, src);
2265
2255
  return src;
2266
2256
  }
@@ -2285,7 +2275,7 @@ var Workspace = class {
2285
2275
  docCache = /* @__PURE__ */ new Map();
2286
2276
  stamp(rel) {
2287
2277
  try {
2288
- const st = fs9.statSync(path9.join(this.config.root, rel));
2278
+ const st = fs7.statSync(path8.join(this.config.root, rel));
2289
2279
  return `${st.mtimeMs}:${st.size}`;
2290
2280
  } catch {
2291
2281
  return "missing";
@@ -2299,8 +2289,8 @@ var Workspace = class {
2299
2289
  const linked = fileOfId(id);
2300
2290
  if (linked) assertShown(doc, linked);
2301
2291
  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") };
2292
+ const abs = path8.join(this.config.root, rel);
2293
+ return { node: hit.node, rel, abs, source: fs7.readFileSync(abs, "utf8") };
2304
2294
  }
2305
2295
  /** Page and layout files shown by linked frames, across all canvases (the Git panel counts them as design files). */
2306
2296
  linkedFiles() {
@@ -2319,7 +2309,7 @@ var Workspace = class {
2319
2309
  */
2320
2310
  designFiles() {
2321
2311
  const m = motionFiles(this.config);
2322
- return [...this.linkedFiles(), ...[m.reveal, m.text].filter((f) => fs9.existsSync(path9.join(this.config.root, f)))];
2312
+ return [...this.linkedFiles(), ...[m.reveal, m.text].filter((f) => fs7.existsSync(path8.join(this.config.root, f)))];
2323
2313
  }
2324
2314
  /** Canvases with a linked frame showing this page or layout file. */
2325
2315
  canvasesLinking(rel) {
@@ -2335,7 +2325,7 @@ var Workspace = class {
2335
2325
  linkedFileChanged(rel) {
2336
2326
  let next;
2337
2327
  try {
2338
- next = fs9.readFileSync(path9.join(this.config.root, rel), "utf8");
2328
+ next = fs7.readFileSync(path8.join(this.config.root, rel), "utf8");
2339
2329
  } catch {
2340
2330
  next = "";
2341
2331
  }
@@ -2347,9 +2337,9 @@ var Workspace = class {
2347
2337
  }
2348
2338
  createCanvas(name, actor, content = STARTER_CANVAS) {
2349
2339
  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);
2340
+ if (fs7.existsSync(file)) throw new EditError(`Canvas "${name}" already exists.`);
2341
+ fs7.mkdirSync(path8.dirname(file), { recursive: true });
2342
+ fs7.writeFileSync(file, content);
2353
2343
  this.sources.set(name, content);
2354
2344
  this.canvasesChanged();
2355
2345
  this.record(name, `Created canvas ${name}`, actor, "", content, []);
@@ -2384,10 +2374,10 @@ var Workspace = class {
2384
2374
  }
2385
2375
  renameCanvas(from, to) {
2386
2376
  const src = this.file(from);
2387
- if (!fs9.existsSync(src)) throw new EditError(`Canvas "${from}" does not exist.`);
2377
+ if (!fs7.existsSync(src)) throw new EditError(`Canvas "${from}" does not exist.`);
2388
2378
  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);
2379
+ if (fs7.existsSync(dest)) throw new EditError(`A canvas named "${to}" already exists.`);
2380
+ fs7.renameSync(src, dest);
2391
2381
  this.comments.renameCanvas(from, to);
2392
2382
  for (const map of [this.sources, this.undoStacks, this.redoStacks]) {
2393
2383
  if (map.has(from)) {
@@ -2404,7 +2394,7 @@ var Workspace = class {
2404
2394
  duplicateCanvas(name) {
2405
2395
  const source = this.read(name);
2406
2396
  const copy = this.uniqueCanvasName(`${name}-copy`);
2407
- fs9.writeFileSync(canvasPath(this.config, copy), source);
2397
+ fs7.writeFileSync(canvasPath(this.config, copy), source);
2408
2398
  this.sources.set(copy, source);
2409
2399
  this.canvasesChanged();
2410
2400
  return copy;
@@ -2413,9 +2403,9 @@ var Workspace = class {
2413
2403
  trash = /* @__PURE__ */ new Map();
2414
2404
  deleteCanvas(name) {
2415
2405
  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);
2406
+ if (!fs7.existsSync(file)) throw new EditError(`Canvas "${name}" does not exist.`);
2407
+ this.trash.set(name, fs7.readFileSync(file, "utf8"));
2408
+ fs7.unlinkSync(file);
2419
2409
  this.sources.delete(name);
2420
2410
  this.undoStacks.delete(name);
2421
2411
  this.redoStacks.delete(name);
@@ -2425,7 +2415,7 @@ var Workspace = class {
2425
2415
  const source = this.trash.get(name);
2426
2416
  if (source === void 0) throw new EditError(`Nothing to restore for "${name}".`);
2427
2417
  const target = this.uniqueCanvasName(name);
2428
- fs9.writeFileSync(canvasPath(this.config, target), source);
2418
+ fs7.writeFileSync(canvasPath(this.config, target), source);
2429
2419
  this.sources.set(target, source);
2430
2420
  this.trash.delete(name);
2431
2421
  this.canvasesChanged();
@@ -2436,14 +2426,14 @@ var Workspace = class {
2436
2426
  if (!fileName.endsWith(CANVAS_SUFFIX)) return;
2437
2427
  const canvas = canvasNameOf(fileName);
2438
2428
  const file = this.file(canvas);
2439
- if (!fs9.existsSync(file)) {
2429
+ if (!fs7.existsSync(file)) {
2440
2430
  this.sources.delete(canvas);
2441
2431
  syncRoute(this.config);
2442
2432
  this.emit({ type: "canvases", canvases: this.canvases() });
2443
2433
  return;
2444
2434
  }
2445
2435
  const prev = this.sources.get(canvas);
2446
- const next = fs9.readFileSync(file, "utf8");
2436
+ const next = fs7.readFileSync(file, "utf8");
2447
2437
  if (prev === next) return;
2448
2438
  this.sources.set(canvas, next);
2449
2439
  if (prev === void 0) {
@@ -2462,8 +2452,8 @@ var Workspace = class {
2462
2452
  } catch (err) {
2463
2453
  if (!(err instanceof ConflictError)) throw err;
2464
2454
  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.`);
2455
+ else this.externalChange(path8.basename(this.file(cmd.canvas)));
2456
+ 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
2457
  return this.exec(cmd, actor);
2468
2458
  }
2469
2459
  });
@@ -2473,7 +2463,7 @@ var Workspace = class {
2473
2463
  async exec(cmd, actor) {
2474
2464
  cmd = { ...cmd };
2475
2465
  let prepared = null;
2476
- if (cmd.op === "add_component_frame" && !fs9.existsSync(this.file(cmd.canvas))) {
2466
+ if (cmd.op === "add_component_frame" && !fs7.existsSync(this.file(cmd.canvas))) {
2477
2467
  await this.catalog.load();
2478
2468
  const spec = this.catalog.get(cmd.component);
2479
2469
  if (!spec) throw new EditError(`Unknown component "${cmd.component}". Use list_components.`);
@@ -2501,12 +2491,12 @@ var Workspace = class {
2501
2491
  prepared = { kind: "apply", frame, ...plan };
2502
2492
  }
2503
2493
  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;
2494
+ const file = path8.join(this.config.root, rel);
2495
+ const original = target ? fs7.readFileSync(file, "utf8") : canvasSource;
2506
2496
  const crlf = original.includes("\r\n");
2507
2497
  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);
2498
+ const parse = (src) => target ? parseTargetDoc(target, src) : parseCanvas(cmd.canvas, rel, src);
2499
+ const doc = parse(before);
2510
2500
  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
2501
  if (cmd.op === "insert_icon") {
2512
2502
  const icon = cmd;
@@ -2560,7 +2550,7 @@ var Workspace = class {
2560
2550
  imports = importsFor(this.config, jsx, before, doc, rel, fileRel);
2561
2551
  movedNames = new Set(componentNamesIn(jsx));
2562
2552
  }
2563
- const abs = path9.join(this.config.root, fileRel);
2553
+ const abs = path8.join(this.config.root, fileRel);
2564
2554
  newComponent = { rel: fileRel, content: await this.format(componentFile(name, jsx, imports), abs).catch(() => componentFile(name, jsx, imports)) };
2565
2555
  prepared = { kind: "component", name, file: fileRel };
2566
2556
  }
@@ -2578,8 +2568,8 @@ var Workspace = class {
2578
2568
  let after = await finish(result);
2579
2569
  let ids = [];
2580
2570
  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.`);
2571
+ const d = parse(src);
2572
+ if (!d) throw new EditError(`That edit would put logic into ${path8.basename(rel)}. Only plain JSX can be edited from the canvas.`);
2583
2573
  return d;
2584
2574
  };
2585
2575
  if (after.includes(MARK)) {
@@ -2604,8 +2594,8 @@ var Workspace = class {
2604
2594
  if (cmd.op === "apply_to_page") ids = [findFrame(canvasDoc, cmd.frame).id];
2605
2595
  const writeNewComponent = () => {
2606
2596
  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" });
2597
+ fs7.mkdirSync(path8.dirname(path8.join(this.config.root, newComponent.rel)), { recursive: true });
2598
+ fs7.writeFileSync(path8.join(this.config.root, newComponent.rel), newComponent.content, { flag: "wx" });
2609
2599
  this.catalog.invalidate();
2610
2600
  this.emit({ type: "catalog" });
2611
2601
  };
@@ -2614,11 +2604,11 @@ var Workspace = class {
2614
2604
  writeNewComponent();
2615
2605
  return { doc: target ? this.doc(cmd.canvas) : view, ids, label };
2616
2606
  }
2617
- if (fs9.readFileSync(file, "utf8") !== original) throw new ConflictError(`${path9.basename(rel)} changed during the edit.`, target ?? void 0);
2607
+ if (fs7.readFileSync(file, "utf8") !== original) throw new ConflictError(`${path8.basename(rel)} changed during the edit.`, target ?? void 0);
2618
2608
  writeNewComponent();
2619
2609
  if (target) this.sources.set(`file:${target}`, after);
2620
2610
  else this.sources.set(cmd.canvas, after);
2621
- fs9.writeFileSync(file, after);
2611
+ fs7.writeFileSync(file, after);
2622
2612
  const newDoc = this.doc(cmd.canvas);
2623
2613
  this.record(cmd.canvas, label, actor, original, after, ids, target ?? void 0, touchedFrames(cmd, ids, view, newDoc));
2624
2614
  if (renamedFrame) {
@@ -2644,7 +2634,7 @@ var Workspace = class {
2644
2634
  const f = findFrame(view, cmd.parent);
2645
2635
  if (f?.link) {
2646
2636
  if (!f.link.editable) throw viewOnly(f);
2647
- const src = fs9.readFileSync(path9.join(this.config.root, f.link.page), "utf8");
2637
+ const src = fs7.readFileSync(path8.join(this.config.root, f.link.page), "utf8");
2648
2638
  if (f.link.kind === "component") {
2649
2639
  const own = parseModuleDoc(f.link.page, src)?.frames.find((x) => x.frameName === f.link.route);
2650
2640
  if (!own) throw viewOnly(f);
@@ -2669,7 +2659,7 @@ var Workspace = class {
2669
2659
  const refs = nodeRefs(cmd);
2670
2660
  const files = new Set(refs.map(fileOfId));
2671
2661
  if (files.size > 1) {
2672
- const names = [...files].map((f) => f ? path9.basename(f) : "the canvas");
2662
+ const names = [...files].map((f) => f ? path8.basename(f) : "the canvas");
2673
2663
  throw new EditError(`These layers live in different files (${names.join(", ")}). Edit them one file at a time.`);
2674
2664
  }
2675
2665
  return [...files][0] ?? null;
@@ -2717,12 +2707,12 @@ var Workspace = class {
2717
2707
  const doc = ed.doc;
2718
2708
  const guard = (node, verb) => {
2719
2709
  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.`);
2710
+ if (node.path.length <= 2) throw new EditError(`Can't ${verb} the root element of ${path8.basename(rel)}.`);
2711
+ 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
2712
  };
2723
2713
  const literalOnly = (node, key) => {
2724
2714
  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.`);
2715
+ 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
2716
  };
2727
2717
  switch (cmd.op) {
2728
2718
  case "set_props": {
@@ -3187,11 +3177,11 @@ ${rows}
3187
3177
  const entry = stack[stack.length - 1];
3188
3178
  if (!entry) throw new EditError(`Nothing to ${dir}.`);
3189
3179
  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";
3180
+ const file = entry.file ? path8.join(this.config.root, entry.file) : this.file(canvas);
3181
+ const name = entry.file ? path8.basename(entry.file) : "the file";
3192
3182
  let current;
3193
3183
  try {
3194
- current = entry.file ? fs9.readFileSync(file, "utf8") : this.read(canvas);
3184
+ current = entry.file ? fs7.readFileSync(file, "utf8") : this.read(canvas);
3195
3185
  } catch {
3196
3186
  throw new EditError(`Cannot ${dir} "${entry.label}": ${name} can't be read right now.`);
3197
3187
  }
@@ -3200,7 +3190,7 @@ ${rows}
3200
3190
  stack.pop();
3201
3191
  const next = dir === "undo" ? entry.before : entry.after;
3202
3192
  this.sources.set(entry.file ? `file:${entry.file}` : canvas, next);
3203
- fs9.writeFileSync(file, next);
3193
+ fs7.writeFileSync(file, next);
3204
3194
  const list = to.get(canvas) ?? [];
3205
3195
  list.push(entry);
3206
3196
  to.set(canvas, list);
@@ -3241,17 +3231,17 @@ ${rows}
3241
3231
  if (this.agents.delete(session)) this.emit({ type: "agents", agents: [...this.agents.values()] });
3242
3232
  }
3243
3233
  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 });
3234
+ if (!fs7.existsSync(path8.join(this.config.root, this.config.canvasDir))) {
3235
+ fs7.mkdirSync(path8.join(this.config.root, this.config.canvasDir), { recursive: true });
3246
3236
  }
3247
3237
  if (!this.canvases().length) {
3248
3238
  const file = canvasPath(this.config, "home");
3249
3239
  const content = this.homeCanvas();
3250
- fs9.writeFileSync(file, content);
3240
+ fs7.writeFileSync(file, content);
3251
3241
  void this.format(content, file).then((pretty) => {
3252
- if (fs9.readFileSync(file, "utf8") !== content) return;
3242
+ if (fs7.readFileSync(file, "utf8") !== content) return;
3253
3243
  this.sources.set("home", pretty);
3254
- fs9.writeFileSync(file, pretty);
3244
+ fs7.writeFileSync(file, pretty);
3255
3245
  }).catch(() => {
3256
3246
  });
3257
3247
  }
@@ -3262,7 +3252,7 @@ ${rows}
3262
3252
  try {
3263
3253
  const home = listRoutes(this.config).find((r) => r.route === "/");
3264
3254
  if (!home) return STARTER_CANVAS;
3265
- const rel = path9.relative(this.config.root, canvasPath(this.config, "home"));
3255
+ const rel = path8.relative(this.config.root, canvasPath(this.config, "home"));
3266
3256
  const empty = parseCanvas("home", rel, `import { Canvas, Frame } from "truecanvas";
3267
3257
  export default function C() { return <Canvas></Canvas>; }
3268
3258
  `);
@@ -3320,7 +3310,7 @@ function publicEntry(e) {
3320
3310
  }
3321
3311
  function assertShown(view, rel) {
3322
3312
  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.`);
3313
+ 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
3314
  }
3325
3315
  function rightEdge(doc) {
3326
3316
  return doc.frames.reduce((m, f) => Math.max(m, f.x + f.width), -80);
@@ -3392,15 +3382,15 @@ function componentJsx(spec, props = {}, text) {
3392
3382
 
3393
3383
  // src/core/pm.ts
3394
3384
  import { spawn } from "child_process";
3395
- import fs10 from "fs";
3396
- import path10 from "path";
3385
+ import fs8 from "fs";
3386
+ import path9 from "path";
3397
3387
  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;
3388
+ for (let d = dir; ; d = path9.dirname(d)) {
3389
+ if (fs8.existsSync(path9.join(d, "pnpm-lock.yaml")) || fs8.existsSync(path9.join(d, "pnpm-workspace.yaml"))) return "pnpm";
3390
+ if (fs8.existsSync(path9.join(d, "yarn.lock"))) return "yarn";
3391
+ if (fs8.existsSync(path9.join(d, "bun.lock")) || fs8.existsSync(path9.join(d, "bun.lockb"))) return "bun";
3392
+ if (fs8.existsSync(path9.join(d, "package-lock.json"))) return "npm";
3393
+ if (path9.dirname(d) === d || fs8.existsSync(path9.join(d, ".git"))) break;
3404
3394
  }
3405
3395
  const agent = process.env.npm_config_user_agent ?? "";
3406
3396
  if (agent.startsWith("pnpm")) return "pnpm";
@@ -3420,7 +3410,12 @@ function dlx(pm, pkg, args) {
3420
3410
  return ["npx", ["--yes", pkg, ...args]];
3421
3411
  }
3422
3412
  function toolEnv(extra = {}) {
3423
- return { ...process.env, PATH: `${path10.dirname(process.execPath)}${path10.delimiter}${process.env.PATH ?? ""}`, ...extra };
3413
+ return { ...process.env, PATH: `${path9.dirname(process.execPath)}${path9.delimiter}${process.env.PATH ?? ""}`, ...extra };
3414
+ }
3415
+ function shellSafe(cmd, args) {
3416
+ if (process.platform !== "win32") return { cmd, args, shell: false };
3417
+ const quote = (a) => /[\s"&|<>^()%!]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a;
3418
+ return { cmd: quote(cmd), args: args.map(quote), shell: true };
3424
3419
  }
3425
3420
  function run2(cmd, args, cwd, timeoutMs = 3e5) {
3426
3421
  return new Promise((resolve) => {
@@ -3432,7 +3427,8 @@ function run2(cmd, args, cwd, timeoutMs = 3e5) {
3432
3427
  clearTimeout(timer);
3433
3428
  resolve({ ok, out: (out + extra).slice(-2e4) });
3434
3429
  };
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" });
3430
+ const safe = shellSafe(cmd, args);
3431
+ 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
3432
  const timer = setTimeout(() => {
3437
3433
  p.kill();
3438
3434
  finish(false, `
@@ -3447,24 +3443,24 @@ ${err.message}`));
3447
3443
  }
3448
3444
 
3449
3445
  // src/core/shadcn.ts
3450
- import fs11 from "fs";
3451
- import path11 from "path";
3446
+ import fs9 from "fs";
3447
+ import path10 from "path";
3452
3448
  function shadcnStatus(root) {
3453
- const file = path11.join(root, "components.json");
3454
- if (!fs11.existsSync(file)) return { initialized: false, uiDir: null, installed: [] };
3449
+ const file = path10.join(root, "components.json");
3450
+ if (!fs9.existsSync(file)) return { initialized: false, uiDir: null, installed: [] };
3455
3451
  let ui = "@/components/ui";
3456
3452
  try {
3457
- ui = JSON.parse(fs11.readFileSync(file, "utf8"))?.aliases?.ui ?? ui;
3453
+ ui = JSON.parse(fs9.readFileSync(file, "utf8"))?.aliases?.ui ?? ui;
3458
3454
  } catch {
3459
3455
  }
3460
3456
  const dir = resolveAliasPath(root, ui);
3461
3457
  let installed = [];
3462
3458
  try {
3463
- installed = fs11.readdirSync(dir).filter((f) => /\.(tsx|jsx)$/.test(f)).map((f) => f.replace(/\.(tsx|jsx)$/, "")).sort();
3459
+ installed = fs9.readdirSync(dir).filter((f) => /\.(tsx|jsx)$/.test(f)).map((f) => f.replace(/\.(tsx|jsx)$/, "")).sort();
3464
3460
  } catch {
3465
3461
  installed = [];
3466
3462
  }
3467
- return { initialized: true, uiDir: path11.relative(root, dir), installed };
3463
+ return { initialized: true, uiDir: posix(path10.relative(root, dir)), installed };
3468
3464
  }
3469
3465
  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
3466
  var registryCache = null;
@@ -3596,8 +3592,8 @@ function describeComponent(c, verbose = false) {
3596
3592
  }
3597
3593
 
3598
3594
  // src/core/tokens.ts
3599
- import fs12 from "fs";
3600
- import path12 from "path";
3595
+ import fs10 from "fs";
3596
+ import path11 from "path";
3601
3597
  import { globSync } from "tinyglobby";
3602
3598
  var GROUPS = [
3603
3599
  ["--color-", "Colors", "bg-{name} text-{name} border-{name}"],
@@ -3614,9 +3610,9 @@ function readDesignTokens(root) {
3614
3610
  const sources = [];
3615
3611
  let darkVariant = null;
3616
3612
  for (const file of files) {
3617
- const css = fs12.readFileSync(file, "utf8");
3613
+ const css = fs10.readFileSync(file, "utf8");
3618
3614
  if (!css.includes("@theme") && !css.includes("@custom-variant")) continue;
3619
- sources.push(path12.relative(root, file));
3615
+ sources.push(posix(path11.relative(root, file)));
3620
3616
  const dv = /@custom-variant\s+dark\s*\(([^;]+)\);/.exec(css);
3621
3617
  if (dv) darkVariant = dv[1].trim();
3622
3618
  for (const block of css.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g)) {
@@ -3655,7 +3651,7 @@ function readTokenData(root) {
3655
3651
  for (const m of body.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)) into.set(m[1], m[2].trim());
3656
3652
  };
3657
3653
  for (const file of files) {
3658
- const css = fs12.readFileSync(file, "utf8");
3654
+ const css = fs10.readFileSync(file, "utf8");
3659
3655
  for (const block of css.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g)) decls(block[1], theme);
3660
3656
  for (const block of css.matchAll(/(?:^|\n)\s*:root\s*\{([\s\S]*?)\n\}/g)) decls(block[1], lightVars);
3661
3657
  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 +3683,30 @@ function readTokenData(root) {
3687
3683
  }
3688
3684
 
3689
3685
  // src/core/compare.ts
3690
- import fs13 from "fs";
3691
- import path13 from "path";
3686
+ import fs11 from "fs";
3687
+ import path12 from "path";
3692
3688
  var COMPARE_PREFIX = "__compare__";
3693
3689
  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`);
3690
+ const routeDir = routeDirOf(config);
3691
+ const dir = path12.join(routeDir, "compare");
3692
+ fs11.mkdirSync(dir, { recursive: true });
3693
+ const canvasDir = path12.join(config.root, config.canvasDir);
3694
+ const canvasRel = `${config.canvasDir}/${canvas}.canvas.tsx`;
3699
3695
  const swapped = /* @__PURE__ */ new Map();
3700
3696
  for (const [rel, content] of Object.entries(files)) {
3701
- const ext = path13.extname(rel);
3697
+ const ext = path12.extname(rel);
3702
3698
  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));
3699
+ const original = path12.join(config.root, rel);
3700
+ fs11.writeFileSync(path12.join(dir, name), rewriteImports(content, (spec) => spec.startsWith(".") ? relativeSpec(dir, path12.resolve(path12.dirname(original), spec)) : spec));
3705
3701
  swapped.set(importSpecifier(config.root, canvasRel, rel), `./${name.slice(0, -ext.length)}`);
3706
3702
  }
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);
3703
+ const out = rewriteImports(source, (spec) => swapped.get(spec) ?? (spec.startsWith(".") ? relativeSpec(dir, path12.resolve(canvasDir, spec)) : spec));
3704
+ fs11.writeFileSync(path12.join(dir, `${as}.canvas.tsx`), out);
3709
3705
  syncRoute(config);
3710
3706
  return `${COMPARE_PREFIX}${as}`;
3711
3707
  }
3712
3708
  function relativeSpec(fromDir, target) {
3713
- const next = path13.relative(fromDir, target);
3709
+ const next = posix(path12.relative(fromDir, target));
3714
3710
  return next.startsWith(".") ? next : `./${next}`;
3715
3711
  }
3716
3712
  function rewriteImports(source, map) {
@@ -3726,9 +3722,9 @@ function rewriteImports(source, map) {
3726
3722
  }
3727
3723
  function clearCompare(config, canvas) {
3728
3724
  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));
3725
+ const dir = path12.join(routeDirOf(config), "compare");
3726
+ if (!fs11.existsSync(dir)) return;
3727
+ for (const f of fs11.readdirSync(dir)) if (!canvas || f === `${canvas}.canvas.tsx` || f.startsWith(`${canvas}--`)) fs11.rmSync(path12.join(dir, f));
3732
3728
  syncRoute(config);
3733
3729
  }
3734
3730
  function frameChanges(before, after) {
@@ -3745,24 +3741,48 @@ function frameChanges(before, after) {
3745
3741
  }
3746
3742
 
3747
3743
  // src/core/init.ts
3748
- import fs14 from "fs";
3744
+ import fs12 from "fs";
3749
3745
  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") };
3746
+ import path13 from "path";
3747
+ var CONFIG_FILES = {
3748
+ next: ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.mts"],
3749
+ vite: ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]
3750
+ };
3751
+ function projectFramework(root) {
3752
+ try {
3753
+ const pkg = JSON.parse(fs12.readFileSync(path13.join(root, "package.json"), "utf8"));
3754
+ return detectFramework({ ...pkg.dependencies, ...pkg.devDependencies });
3755
+ } catch {
3756
+ return "next";
3756
3757
  }
3757
- return { file: null, wrapped: false };
3758
+ }
3759
+ function configStatus(root, framework = projectFramework(root)) {
3760
+ const marker = framework === "vite" ? "truecanvas/vite" : "withTruecanvas";
3761
+ for (const name of CONFIG_FILES[framework]) {
3762
+ const file = path13.join(root, name);
3763
+ if (fs12.existsSync(file)) return { file, wrapped: fs12.readFileSync(file, "utf8").includes(marker), framework };
3764
+ }
3765
+ return { file: null, wrapped: false, framework };
3758
3766
  }
3759
3767
  function initProject(config) {
3760
3768
  const { root } = config;
3761
3769
  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";
3770
+ const vite = config.framework === "vite";
3771
+ const status = configStatus(root, config.framework);
3772
+ if (vite) {
3773
+ const name = status.file ? path13.basename(status.file) : "vite.config.ts";
3774
+ if (status.wrapped) report.done.push(`${name} already uses the truecanvas() plugin`);
3775
+ else {
3776
+ const src = status.file ? fs12.readFileSync(status.file, "utf8") : null;
3777
+ const next = src && addVitePlugin(src);
3778
+ if (next) {
3779
+ fs12.writeFileSync(status.file, next);
3780
+ report.done.push(`Added the truecanvas() plugin to ${name}`);
3781
+ } else report.todo.push(`Add truecanvas() from "truecanvas/vite" to the plugins in ${name}`);
3782
+ }
3783
+ } else if (!status.file) {
3784
+ const file = path13.join(root, "next.config.ts");
3785
+ fs12.writeFileSync(file, `import type { NextConfig } from "next";
3766
3786
  import { withTruecanvas } from "truecanvas/next";
3767
3787
 
3768
3788
  const nextConfig: NextConfig = {};
@@ -3771,49 +3791,49 @@ export default withTruecanvas(nextConfig);
3771
3791
  `);
3772
3792
  report.done.push("Created next.config.ts with withTruecanvas()");
3773
3793
  } else if (status.wrapped) {
3774
- report.done.push(`${path14.basename(status.file)} already uses withTruecanvas()`);
3794
+ report.done.push(`${path13.basename(status.file)} already uses withTruecanvas()`);
3775
3795
  } else {
3776
- const src = fs14.readFileSync(status.file, "utf8");
3796
+ const src = fs12.readFileSync(status.file, "utf8");
3777
3797
  const next = wrapConfigExport(src);
3778
3798
  if (next && next !== src && next.includes("withTruecanvas(")) {
3779
- fs14.writeFileSync(status.file, next);
3780
- report.done.push(`Wrapped ${path14.basename(status.file)} with withTruecanvas()`);
3799
+ fs12.writeFileSync(status.file, next);
3800
+ report.done.push(`Wrapped ${path13.basename(status.file)} with withTruecanvas()`);
3781
3801
  } else {
3782
- report.todo.push(`Wrap your ${path14.basename(status.file)} export with withTruecanvas() from "truecanvas/next"`);
3802
+ report.todo.push(`Wrap your ${path13.basename(status.file)} export with withTruecanvas() from "truecanvas/next"`);
3783
3803
  }
3784
3804
  }
3785
- const pkgFile = path14.join(root, "package.json");
3805
+ const pkgFile = path13.join(root, "package.json");
3786
3806
  try {
3787
- const raw = fs14.readFileSync(pkgFile, "utf8");
3807
+ const raw = fs12.readFileSync(pkgFile, "utf8");
3788
3808
  const pkg = JSON.parse(raw);
3789
3809
  pkg.scripts ??= {};
3790
3810
  if (!Object.values(pkg.scripts).some((v) => typeof v === "string" && v.includes("truecanvas"))) {
3791
3811
  pkg.scripts.canvas = "truecanvas dev";
3792
3812
  const indent = /\n(\s+)"/.exec(raw)?.[1] ?? " ";
3793
- fs14.writeFileSync(pkgFile, `${JSON.stringify(pkg, null, indent)}
3813
+ fs12.writeFileSync(pkgFile, `${JSON.stringify(pkg, null, indent)}
3794
3814
  `);
3795
3815
  report.done.push(`Added the "canvas" script (npm run canvas)`);
3796
3816
  }
3797
3817
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3798
3818
  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");
3819
+ 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
3820
  } catch {
3801
3821
  report.todo.push("Couldn't read package.json");
3802
3822
  }
3803
- const ignoreFile = path14.join(root, ".gitignore");
3804
- const entry = `/${config.appDir}/truecanvas/`;
3805
- const ignore = fs14.existsSync(ignoreFile) ? fs14.readFileSync(ignoreFile, "utf8") : "";
3823
+ const ignoreFile = path13.join(root, ".gitignore");
3824
+ const entry = `/${config.routeDir}/`;
3825
+ const ignore = fs12.existsSync(ignoreFile) ? fs12.readFileSync(ignoreFile, "utf8") : "";
3806
3826
  if (!ignore.split("\n").some((l) => l.trim() === entry || l.trim() === entry.slice(1))) {
3807
- fs14.writeFileSync(ignoreFile, `${ignore}${ignore && !ignore.endsWith("\n") ? "\n" : ""}
3827
+ fs12.writeFileSync(ignoreFile, `${ignore}${ignore && !ignore.endsWith("\n") ? "\n" : ""}
3808
3828
  # Truecanvas (generated dev-only route)
3809
3829
  ${entry}
3810
3830
  `);
3811
3831
  report.done.push(`Ignored ${entry} in .gitignore`);
3812
3832
  }
3813
3833
  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");
3834
+ const file = path13.join(root, name);
3835
+ if (!fs12.existsSync(file)) continue;
3836
+ const src = fs12.readFileSync(file, "utf8");
3817
3837
  const glob = `./${config.canvasDir}/**/*.{ts,tsx}`;
3818
3838
  if (src.includes(config.canvasDir)) break;
3819
3839
  const m = /content\s*:\s*\[([ \t]*\n?)([ \t]*)/.exec(src);
@@ -3822,12 +3842,12 @@ ${entry}
3822
3842
  const at = m.index + m[0].length;
3823
3843
  const entry2 = m[1].includes("\n") ? `${JSON.stringify(glob)},
3824
3844
  ${indent}` : `${JSON.stringify(glob)}, `;
3825
- fs14.writeFileSync(file, src.slice(0, at) + entry2 + src.slice(at));
3845
+ fs12.writeFileSync(file, src.slice(0, at) + entry2 + src.slice(at));
3826
3846
  report.done.push(`Added ${config.canvasDir} to Tailwind's content in ${name}`);
3827
3847
  } else report.todo.push(`Add "${glob}" to the content list in ${name} so Tailwind styles your canvases`);
3828
3848
  break;
3829
3849
  }
3830
- if (!fs14.existsSync(path14.join(root, config.appDir))) report.todo.push(`No ${config.appDir}/ folder found: Truecanvas needs the Next.js App Router`);
3850
+ 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
3851
  return report;
3832
3852
  }
3833
3853
  function addImport(src, line) {
@@ -3874,12 +3894,35 @@ ${s.toString()}`;
3874
3894
  }
3875
3895
  return null;
3876
3896
  }
3897
+ function addVitePlugin(src) {
3898
+ let ast;
3899
+ try {
3900
+ ast = parseModule(src);
3901
+ } catch {
3902
+ return null;
3903
+ }
3904
+ let plugins = null;
3905
+ walk(ast, (node) => {
3906
+ if (plugins) return false;
3907
+ if (node.type !== "ObjectProperty" || node.value.type !== "ArrayExpression") return;
3908
+ const key = node.key.type === "Identifier" ? node.key.name : node.key.type === "StringLiteral" ? node.key.value : null;
3909
+ if (key !== "plugins") return;
3910
+ 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 })) };
3911
+ return false;
3912
+ });
3913
+ if (!plugins) return null;
3914
+ const { start, end, elements } = plugins;
3915
+ const s = new MagicString2(src);
3916
+ if (!elements.length) s.overwrite(start, end, "[truecanvas()]");
3917
+ else {
3918
+ const last = elements[elements.length - 1];
3919
+ const before = src.slice(start + 1, elements[0].start);
3920
+ s.appendLeft(last.end, before.includes("\n") ? `,${before.replace(/^[^\n]*/, "")}truecanvas()` : ", truecanvas()");
3921
+ }
3922
+ return addImport(s.toString(), `import { truecanvas } from "truecanvas/vite";`);
3923
+ }
3877
3924
 
3878
3925
  export {
3879
- loadConfig,
3880
- parseModule,
3881
- idOf,
3882
- walk,
3883
3926
  parseCanvas,
3884
3927
  allNodes,
3885
3928
  findNode,
@@ -3895,6 +3938,8 @@ export {
3895
3938
  DEVICES,
3896
3939
  findDevice,
3897
3940
  assertCanvasName,
3941
+ syncRoute,
3942
+ viteStylesheets,
3898
3943
  syncComponentRegistry,
3899
3944
  Comments,
3900
3945
  Git,
@@ -3903,6 +3948,7 @@ export {
3903
3948
  detectPm,
3904
3949
  addArgs,
3905
3950
  toolEnv,
3951
+ shellSafe,
3906
3952
  run2 as run,
3907
3953
  shadcnStatus,
3908
3954
  shadcnRegistry,
@@ -3915,7 +3961,8 @@ export {
3915
3961
  writeCompare,
3916
3962
  clearCompare,
3917
3963
  frameChanges,
3918
- nextConfigStatus,
3964
+ configStatus,
3919
3965
  initProject,
3920
- wrapConfigExport
3966
+ wrapConfigExport,
3967
+ addVitePlugin
3921
3968
  };