@pixel-point/toolcraft 0.0.2

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.
Files changed (257) hide show
  1. package/LICENSE.md +98 -0
  2. package/README.md +41 -0
  3. package/bin/create-toolcraft-app.mjs +8 -0
  4. package/bin/toolcraft.mjs +8 -0
  5. package/package.json +24 -0
  6. package/scripts/prepare-pack.mjs +29 -0
  7. package/src/cli.mjs +392 -0
  8. package/src/cli.test.mjs +284 -0
  9. package/src/copy-recursive.mjs +86 -0
  10. package/src/generate.mjs +212 -0
  11. package/src/generate.test.mjs +322 -0
  12. package/src/import-map.mjs +14 -0
  13. package/src/package-json.mjs +80 -0
  14. package/src/package-json.test.mjs +67 -0
  15. package/src/rewrite-imports.mjs +85 -0
  16. package/src/rewrite-imports.test.mjs +58 -0
  17. package/templates/runtime/contracts/component-contracts.test.ts +1165 -0
  18. package/templates/runtime/contracts/component-contracts.ts +1340 -0
  19. package/templates/runtime/contracts/decision-contracts.test.ts +206 -0
  20. package/templates/runtime/contracts/decision-contracts.ts +283 -0
  21. package/templates/runtime/contracts/index.test.ts +14 -0
  22. package/templates/runtime/contracts/index.ts +3 -0
  23. package/templates/runtime/contracts/types.ts +56 -0
  24. package/templates/runtime/export/export.test.ts +203 -0
  25. package/templates/runtime/export/export.ts +132 -0
  26. package/templates/runtime/export/index.ts +1 -0
  27. package/templates/runtime/index.ts +14 -0
  28. package/templates/runtime/react/canvas-shell.test.tsx +424 -0
  29. package/templates/runtime/react/canvas-shell.tsx +408 -0
  30. package/templates/runtime/react/control-renderers.ts +31 -0
  31. package/templates/runtime/react/controls-panel.test.tsx +3736 -0
  32. package/templates/runtime/react/controls-panel.tsx +2327 -0
  33. package/templates/runtime/react/curve-geometry.test.ts +70 -0
  34. package/templates/runtime/react/index.ts +15 -0
  35. package/templates/runtime/react/layer-tree.ts +96 -0
  36. package/templates/runtime/react/layers-panel.test.tsx +487 -0
  37. package/templates/runtime/react/layers-panel.tsx +1348 -0
  38. package/templates/runtime/react/media-file.ts +82 -0
  39. package/templates/runtime/react/panel-host-config.ts +80 -0
  40. package/templates/runtime/react/panel-host-geometry.test.ts +66 -0
  41. package/templates/runtime/react/panel-host-geometry.ts +109 -0
  42. package/templates/runtime/react/panel-host-types.ts +74 -0
  43. package/templates/runtime/react/panel-host.test.tsx +102 -0
  44. package/templates/runtime/react/panel-host.tsx +353 -0
  45. package/templates/runtime/react/runtime-public-api.test.tsx +132 -0
  46. package/templates/runtime/react/settings-transfer.test.ts +150 -0
  47. package/templates/runtime/react/settings-transfer.ts +279 -0
  48. package/templates/runtime/react/storage-key-migration.ts +48 -0
  49. package/templates/runtime/react/theme-runtime.tsx +177 -0
  50. package/templates/runtime/react/timeline-panel.test.tsx +668 -0
  51. package/templates/runtime/react/timeline-panel.tsx +2953 -0
  52. package/templates/runtime/react/toolbar-panel.test.tsx +212 -0
  53. package/templates/runtime/react/toolbar-panel.tsx +205 -0
  54. package/templates/runtime/react/toolcraft-app.integration.test.tsx +350 -0
  55. package/templates/runtime/react/toolcraft-app.test.tsx +339 -0
  56. package/templates/runtime/react/toolcraft-app.tsx +81 -0
  57. package/templates/runtime/react/toolcraft-root.test.tsx +347 -0
  58. package/templates/runtime/react/toolcraft-root.tsx +203 -0
  59. package/templates/runtime/react/use-toolcraft.ts +41 -0
  60. package/templates/runtime/schema/define-toolcraft.test.ts +1524 -0
  61. package/templates/runtime/schema/define-toolcraft.ts +1442 -0
  62. package/templates/runtime/schema/keyframe-capability.test.ts +90 -0
  63. package/templates/runtime/schema/keyframe-capability.ts +51 -0
  64. package/templates/runtime/schema/runtime-targets.ts +40 -0
  65. package/templates/runtime/schema/types.ts +370 -0
  66. package/templates/runtime/state/canvas-zoom.ts +8 -0
  67. package/templates/runtime/state/create-template-state.test.ts +242 -0
  68. package/templates/runtime/state/create-template-state.ts +95 -0
  69. package/templates/runtime/state/keyframe-evaluation.test.ts +141 -0
  70. package/templates/runtime/state/keyframe-evaluation.ts +203 -0
  71. package/templates/runtime/state/persistence.test.ts +217 -0
  72. package/templates/runtime/state/persistence.ts +511 -0
  73. package/templates/runtime/state/reducer.test.ts +937 -0
  74. package/templates/runtime/state/reducer.ts +1212 -0
  75. package/templates/runtime/state/timeline-readiness.ts +43 -0
  76. package/templates/runtime/state/types.ts +242 -0
  77. package/templates/runtime/styles.css +125 -0
  78. package/templates/runtime/testing/performance.test.ts +1058 -0
  79. package/templates/runtime/testing/performance.ts +1078 -0
  80. package/templates/starter/AGENTS.md +186 -0
  81. package/templates/starter/LICENSE.md +98 -0
  82. package/templates/starter/NOTICE.md +8 -0
  83. package/templates/starter/docs/toolcraft/README.md +41 -0
  84. package/templates/starter/docs/toolcraft/acceptance-testing.md +205 -0
  85. package/templates/starter/docs/toolcraft/agent-worklog.md +81 -0
  86. package/templates/starter/docs/toolcraft/assembly-workflow.md +206 -0
  87. package/templates/starter/docs/toolcraft/component-rules.md +299 -0
  88. package/templates/starter/docs/toolcraft/custom-controls.md +71 -0
  89. package/templates/starter/docs/toolcraft/decision-contract.md +71 -0
  90. package/templates/starter/docs/toolcraft/performance.md +112 -0
  91. package/templates/starter/docs/toolcraft/renderer-technique.md +48 -0
  92. package/templates/starter/docs/toolcraft/schema-reference.md +265 -0
  93. package/templates/starter/docs/toolcraft/workflow.md +87 -0
  94. package/templates/starter/e2e/app-browser-acceptance.spec.ts +785 -0
  95. package/templates/starter/e2e/app-controls.spec.ts +41 -0
  96. package/templates/starter/e2e/app-performance.spec.ts +326 -0
  97. package/templates/starter/e2e/canvas-handle-helpers.ts +244 -0
  98. package/templates/starter/e2e/performance-helpers.ts +612 -0
  99. package/templates/starter/e2e/product-observable-helpers.ts +170 -0
  100. package/templates/starter/index.html +12 -0
  101. package/templates/starter/package.json +52 -0
  102. package/templates/starter/playwright.config.ts +43 -0
  103. package/templates/starter/scripts/check-ai-skills.mjs +95 -0
  104. package/templates/starter/scripts/check-toolcraft-docs.mjs +159 -0
  105. package/templates/starter/scripts/check-toolcraft-integrity.mjs +232 -0
  106. package/templates/starter/scripts/run-vite-on-free-port.mjs +48 -0
  107. package/templates/starter/scripts/toolcraft-port.mjs +54 -0
  108. package/templates/starter/scripts/toolcraft-port.test.mjs +73 -0
  109. package/templates/starter/src/app/starter-acceptance.test.ts +5959 -0
  110. package/templates/starter/src/app/starter-acceptance.ts +2646 -0
  111. package/templates/starter/src/app/starter-performance.test.ts +1390 -0
  112. package/templates/starter/src/app/starter-performance.ts +12 -0
  113. package/templates/starter/src/app/starter-schema.test.ts +70 -0
  114. package/templates/starter/src/app/starter-schema.ts +15 -0
  115. package/templates/starter/src/main.tsx +18 -0
  116. package/templates/starter/src/router.tsx +16 -0
  117. package/templates/starter/src/routes/index.tsx +7 -0
  118. package/templates/starter/src/routes/root.tsx +19 -0
  119. package/templates/starter/src/styles.css +120 -0
  120. package/templates/starter/tsconfig.json +11 -0
  121. package/templates/starter/vite.config.ts +13 -0
  122. package/templates/ui/components/composites/accordion.tsx +73 -0
  123. package/templates/ui/components/composites/alert-dialog.tsx +190 -0
  124. package/templates/ui/components/composites/alert.tsx +74 -0
  125. package/templates/ui/components/composites/aspect-ratio.tsx +22 -0
  126. package/templates/ui/components/composites/avatar.tsx +98 -0
  127. package/templates/ui/components/composites/badge.tsx +69 -0
  128. package/templates/ui/components/composites/breadcrumb.tsx +106 -0
  129. package/templates/ui/components/composites/card.tsx +91 -0
  130. package/templates/ui/components/composites/combobox.tsx +486 -0
  131. package/templates/ui/components/composites/command.tsx +296 -0
  132. package/templates/ui/components/composites/context-menu.tsx +247 -0
  133. package/templates/ui/components/composites/dialog.tsx +282 -0
  134. package/templates/ui/components/composites/dropdown-menu.tsx +299 -0
  135. package/templates/ui/components/composites/empty.tsx +110 -0
  136. package/templates/ui/components/composites/hover-card.tsx +44 -0
  137. package/templates/ui/components/composites/index.ts +30 -0
  138. package/templates/ui/components/composites/menubar.tsx +214 -0
  139. package/templates/ui/components/composites/navigation-menu.tsx +167 -0
  140. package/templates/ui/components/composites/pagination.tsx +131 -0
  141. package/templates/ui/components/composites/progress.tsx +72 -0
  142. package/templates/ui/components/composites/radio-group.tsx +84 -0
  143. package/templates/ui/components/composites/resizable.tsx +42 -0
  144. package/templates/ui/components/composites/sheet.tsx +153 -0
  145. package/templates/ui/components/composites/sidebar-structural.tsx +310 -0
  146. package/templates/ui/components/composites/sidebar.tsx +431 -0
  147. package/templates/ui/components/composites/sonner.tsx +35 -0
  148. package/templates/ui/components/composites/spinner.tsx +43 -0
  149. package/templates/ui/components/composites/table.tsx +108 -0
  150. package/templates/ui/components/composites/tabs.tsx +83 -0
  151. package/templates/ui/components/control-layout/index.tsx +437 -0
  152. package/templates/ui/components/controls/actions/actions-control.tsx +139 -0
  153. package/templates/ui/components/controls/actions/index.ts +9 -0
  154. package/templates/ui/components/controls/anchor-grid/anchor-grid-control.tsx +107 -0
  155. package/templates/ui/components/controls/anchor-grid/index.ts +4 -0
  156. package/templates/ui/components/controls/boolean/boolean-controls.tsx +79 -0
  157. package/templates/ui/components/controls/boolean/index.ts +4 -0
  158. package/templates/ui/components/controls/channel-mixer/channel-mixer-control.tsx +95 -0
  159. package/templates/ui/components/controls/channel-mixer/index.ts +4 -0
  160. package/templates/ui/components/controls/channel-tabs/channel-tabs.tsx +42 -0
  161. package/templates/ui/components/controls/channel-tabs/index.ts +6 -0
  162. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +90 -0
  163. package/templates/ui/components/controls/code-textarea/index.ts +4 -0
  164. package/templates/ui/components/controls/color/color-control.tsx +571 -0
  165. package/templates/ui/components/controls/color/color-picker-popover.tsx +104 -0
  166. package/templates/ui/components/controls/color/index.ts +41 -0
  167. package/templates/ui/components/controls/color/palette-control-data.ts +436 -0
  168. package/templates/ui/components/controls/color/palette-control.tsx +535 -0
  169. package/templates/ui/components/controls/color/style-guide-color-picker-channel-utils.ts +162 -0
  170. package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +190 -0
  171. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +485 -0
  172. package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +710 -0
  173. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +503 -0
  174. package/templates/ui/components/controls/control-types.ts +43 -0
  175. package/templates/ui/components/controls/curves/curve-geometry.ts +355 -0
  176. package/templates/ui/components/controls/curves/curve-graph.tsx +390 -0
  177. package/templates/ui/components/controls/curves/curves-control.tsx +445 -0
  178. package/templates/ui/components/controls/curves/index.ts +6 -0
  179. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +191 -0
  180. package/templates/ui/components/controls/file-drop/index.ts +5 -0
  181. package/templates/ui/components/controls/font-picker/font-catalog.json +15360 -0
  182. package/templates/ui/components/controls/font-picker/font-catalog.ts +116 -0
  183. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1202 -0
  184. package/templates/ui/components/controls/font-picker/font-preview-loader.ts +336 -0
  185. package/templates/ui/components/controls/font-picker/index.ts +24 -0
  186. package/templates/ui/components/controls/font-picker/use-hover-intent.ts +46 -0
  187. package/templates/ui/components/controls/gradient/gradient-control-utils.ts +190 -0
  188. package/templates/ui/components/controls/gradient/gradient-control.tsx +612 -0
  189. package/templates/ui/components/controls/gradient/gradient-stop-list.tsx +400 -0
  190. package/templates/ui/components/controls/gradient/gradient-toolbar.tsx +152 -0
  191. package/templates/ui/components/controls/gradient/index.ts +4 -0
  192. package/templates/ui/components/controls/image-picker/image-picker-control.tsx +139 -0
  193. package/templates/ui/components/controls/image-picker/index.ts +7 -0
  194. package/templates/ui/components/controls/index.ts +192 -0
  195. package/templates/ui/components/controls/range-input/index.ts +4 -0
  196. package/templates/ui/components/controls/range-input/range-input-control.tsx +173 -0
  197. package/templates/ui/components/controls/range-slider/index.ts +4 -0
  198. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +122 -0
  199. package/templates/ui/components/controls/range-slider/range-slider-value.ts +61 -0
  200. package/templates/ui/components/controls/segmented/index.ts +8 -0
  201. package/templates/ui/components/controls/segmented/segmented-control.tsx +94 -0
  202. package/templates/ui/components/controls/select/index.ts +4 -0
  203. package/templates/ui/components/controls/select/select-control.tsx +223 -0
  204. package/templates/ui/components/controls/slider/index.ts +4 -0
  205. package/templates/ui/components/controls/slider/slider-control.tsx +150 -0
  206. package/templates/ui/components/controls/slider/slider-value.ts +56 -0
  207. package/templates/ui/components/controls/text-input/index.ts +4 -0
  208. package/templates/ui/components/controls/text-input/text-input-control.tsx +158 -0
  209. package/templates/ui/components/controls/use-measured-element-width.ts +42 -0
  210. package/templates/ui/components/controls/vector/index.ts +8 -0
  211. package/templates/ui/components/controls/vector/vector-control.tsx +401 -0
  212. package/templates/ui/components/panel/index.ts +19 -0
  213. package/templates/ui/components/panel/panel-actions.tsx +165 -0
  214. package/templates/ui/components/panel/panel-header.tsx +61 -0
  215. package/templates/ui/components/panel/panel-icon-button.tsx +96 -0
  216. package/templates/ui/components/panel/panel-section.tsx +168 -0
  217. package/templates/ui/components/panel/panel-surface.tsx +206 -0
  218. package/templates/ui/components/panel/panel.tsx +210 -0
  219. package/templates/ui/components/primitives/animated-loader.tsx +61 -0
  220. package/templates/ui/components/primitives/button-group.tsx +134 -0
  221. package/templates/ui/components/primitives/button.tsx +429 -0
  222. package/templates/ui/components/primitives/checkbox.tsx +62 -0
  223. package/templates/ui/components/primitives/editable-slider-value-label.tsx +337 -0
  224. package/templates/ui/components/primitives/field.tsx +225 -0
  225. package/templates/ui/components/primitives/index.ts +82 -0
  226. package/templates/ui/components/primitives/input-group.tsx +298 -0
  227. package/templates/ui/components/primitives/input.tsx +61 -0
  228. package/templates/ui/components/primitives/internal/button-loading.tsx +178 -0
  229. package/templates/ui/components/primitives/label.tsx +16 -0
  230. package/templates/ui/components/primitives/popover.tsx +126 -0
  231. package/templates/ui/components/primitives/portal-layer-context.tsx +33 -0
  232. package/templates/ui/components/primitives/primitive-arrow-icon.tsx +38 -0
  233. package/templates/ui/components/primitives/scroll-fade-logic.ts +441 -0
  234. package/templates/ui/components/primitives/scroll-fade-render.tsx +75 -0
  235. package/templates/ui/components/primitives/scroll-fade-types.ts +41 -0
  236. package/templates/ui/components/primitives/scroll-fade.tsx +72 -0
  237. package/templates/ui/components/primitives/select.tsx +408 -0
  238. package/templates/ui/components/primitives/selection-state.ts +31 -0
  239. package/templates/ui/components/primitives/separator.tsx +21 -0
  240. package/templates/ui/components/primitives/slider/index.ts +4 -0
  241. package/templates/ui/components/primitives/slider/slider-interaction.tsx +96 -0
  242. package/templates/ui/components/primitives/slider/slider-parts.tsx +303 -0
  243. package/templates/ui/components/primitives/slider/slider-reset.ts +152 -0
  244. package/templates/ui/components/primitives/slider/slider-value.ts +114 -0
  245. package/templates/ui/components/primitives/slider/slider.tsx +511 -0
  246. package/templates/ui/components/primitives/switch.tsx +35 -0
  247. package/templates/ui/components/primitives/textarea.tsx +49 -0
  248. package/templates/ui/components/primitives/toggle-group.tsx +114 -0
  249. package/templates/ui/components/primitives/toggle.tsx +46 -0
  250. package/templates/ui/components/primitives/tooltip.tsx +100 -0
  251. package/templates/ui/hooks/use-mobile.ts +21 -0
  252. package/templates/ui/index.ts +31 -0
  253. package/templates/ui/lib/control-outline.ts +3 -0
  254. package/templates/ui/lib/input-control-style.ts +131 -0
  255. package/templates/ui/lib/style-guide-color-utils.ts +111 -0
  256. package/templates/ui/lib/utils.ts +6 -0
  257. package/templates/ui/styles.css +291 -0
@@ -0,0 +1,284 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFile } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { promisify } from "node:util";
8
+ import { after, describe, it } from "node:test";
9
+
10
+ import { parseCreateArgs, resolveCreateOptions, runToolcraftCli } from "./cli.mjs";
11
+
12
+ const execFileAsync = promisify(execFile);
13
+ const packageRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
14
+ const tempRoots = [];
15
+
16
+ after(async () => {
17
+ await Promise.all(
18
+ tempRoots.map((tempRoot) => fs.rm(tempRoot, { force: true, recursive: true })),
19
+ );
20
+ });
21
+
22
+ function createWritableCapture() {
23
+ return {
24
+ text: "",
25
+ write(chunk) {
26
+ this.text += String(chunk);
27
+ },
28
+ };
29
+ }
30
+
31
+ async function createTempRoot() {
32
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-cli-run-"));
33
+ tempRoots.push(tempRoot);
34
+ return tempRoot;
35
+ }
36
+
37
+ describe("parseCreateArgs", () => {
38
+ it("parses create options for scripted usage", () => {
39
+ assert.deepEqual(
40
+ parseCreateArgs([
41
+ "generated-app",
42
+ "--name",
43
+ "Generated App",
44
+ "--force",
45
+ "--yes",
46
+ "--no-install",
47
+ ]),
48
+ {
49
+ force: true,
50
+ help: false,
51
+ install: false,
52
+ name: "Generated App",
53
+ targetDir: "generated-app",
54
+ yes: true,
55
+ },
56
+ );
57
+ });
58
+
59
+ it("parses --dir as the target directory", () => {
60
+ assert.equal(parseCreateArgs(["--dir", "target-app"]).targetDir, "target-app");
61
+ });
62
+
63
+ it("rejects duplicate target directory values", () => {
64
+ assert.throws(
65
+ () => parseCreateArgs(["target-app", "--dir", "other-app"]),
66
+ /Pass either a positional target directory or --dir/,
67
+ );
68
+ });
69
+ });
70
+
71
+ describe("resolveCreateOptions", () => {
72
+ it("defaults missing target directory and package name to the current directory", async () => {
73
+ const tempRoot = await createTempRoot();
74
+ const options = await resolveCreateOptions(parseCreateArgs(["--yes", "--no-install"]), {
75
+ cwd: tempRoot,
76
+ env: {},
77
+ });
78
+
79
+ assert.deepEqual(options, {
80
+ force: false,
81
+ install: false,
82
+ name: path.basename(tempRoot).toLowerCase(),
83
+ targetDir: ".",
84
+ });
85
+ });
86
+
87
+ it("uses the prompted project name as the target folder from a non-empty parent directory", async () => {
88
+ const tempRoot = await createTempRoot();
89
+ await fs.writeFile(path.join(tempRoot, "README.md"), "existing");
90
+ let confirmCalled = false;
91
+
92
+ const options = await resolveCreateOptions(parseCreateArgs([]), {
93
+ cwd: tempRoot,
94
+ env: {},
95
+ prompts: {
96
+ async text({ label, defaultValue }) {
97
+ assert.equal(label, "Project name");
98
+ assert.equal(defaultValue, path.basename(tempRoot).toLowerCase());
99
+ return "Prompted App";
100
+ },
101
+ async confirm({ label, defaultValue }) {
102
+ confirmCalled = true;
103
+ throw new Error(`Unexpected confirmation: ${label} ${defaultValue}`);
104
+ },
105
+ },
106
+ });
107
+
108
+ assert.deepEqual(options, {
109
+ force: false,
110
+ install: true,
111
+ name: "prompted-app",
112
+ targetDir: "./prompted-app",
113
+ });
114
+ assert.equal(confirmCalled, false);
115
+ });
116
+
117
+ it("confirms when the prompted project target folder is non-empty", async () => {
118
+ const tempRoot = await createTempRoot();
119
+ const targetDir = path.join(tempRoot, "prompted-app");
120
+ await fs.mkdir(targetDir);
121
+ await fs.writeFile(path.join(targetDir, "README.md"), "existing");
122
+
123
+ const options = await resolveCreateOptions(parseCreateArgs([]), {
124
+ cwd: tempRoot,
125
+ env: {},
126
+ prompts: {
127
+ async text({ label }) {
128
+ assert.equal(label, "Project name");
129
+ return "Prompted App";
130
+ },
131
+ async confirm({ label, defaultValue }) {
132
+ assert.equal(label, "Directory is not empty. Continue?");
133
+ assert.equal(defaultValue, false);
134
+ return true;
135
+ },
136
+ },
137
+ });
138
+
139
+ assert.deepEqual(options, {
140
+ force: true,
141
+ install: true,
142
+ name: "prompted-app",
143
+ targetDir: "./prompted-app",
144
+ });
145
+ });
146
+
147
+ it("does not force non-empty folders when --yes is used without --force", async () => {
148
+ const tempRoot = await createTempRoot();
149
+ const targetDir = path.join(tempRoot, "existing-app");
150
+ await fs.mkdir(targetDir);
151
+ await fs.writeFile(path.join(targetDir, "README.md"), "existing");
152
+
153
+ await assert.rejects(
154
+ () =>
155
+ resolveCreateOptions(parseCreateArgs(["existing-app", "--yes"]), {
156
+ cwd: tempRoot,
157
+ env: {},
158
+ }),
159
+ /Use --force/,
160
+ );
161
+ });
162
+ });
163
+
164
+ describe("runToolcraftCli", () => {
165
+ it("creates in the current directory when no command or target is passed", async () => {
166
+ const tempRoot = await createTempRoot();
167
+ const stdout = createWritableCapture();
168
+ const stderr = createWritableCapture();
169
+
170
+ const result = await runToolcraftCli(["--no-install"], {
171
+ cwd: tempRoot,
172
+ env: {},
173
+ stderr,
174
+ stdout,
175
+ throwOnError: true,
176
+ });
177
+
178
+ assert.equal(result.ok, true);
179
+ assert.ok(await fs.stat(path.join(tempRoot, "src/app/app-schema.ts")));
180
+ assert.match(stdout.text, new RegExp(`Created ${path.basename(tempRoot).toLowerCase()}`));
181
+ assert.doesNotMatch(stdout.text, /cd /);
182
+ assert.match(stdout.text, /pnpm dev/);
183
+ assert.equal(stderr.text, "");
184
+ });
185
+
186
+ it("creates a standalone app through toolcraft create without installing in tests", async () => {
187
+ const tempRoot = await createTempRoot();
188
+ const stdout = createWritableCapture();
189
+ const stderr = createWritableCapture();
190
+
191
+ const result = await runToolcraftCli(
192
+ [
193
+ "create",
194
+ "generated-app",
195
+ "--name",
196
+ "Generated App",
197
+ "--yes",
198
+ "--force",
199
+ "--no-install",
200
+ ],
201
+ {
202
+ cwd: tempRoot,
203
+ env: {},
204
+ stderr,
205
+ stdout,
206
+ throwOnError: true,
207
+ },
208
+ );
209
+
210
+ assert.equal(result.ok, true);
211
+ assert.ok(await fs.stat(path.join(tempRoot, "generated-app/src/app/app-schema.ts")));
212
+ assert.match(stdout.text, /Created generated-app/);
213
+ assert.match(stdout.text, /cd generated-app/);
214
+ assert.match(stdout.text, /pnpm dev/);
215
+ assert.doesNotMatch(stdout.text, /pnpm verify:final/);
216
+ assert.equal(stderr.text, "");
217
+ });
218
+
219
+ it("runs pnpm install by default after creating the app", async () => {
220
+ const tempRoot = await createTempRoot();
221
+ const stdout = createWritableCapture();
222
+ const installs = [];
223
+
224
+ await runToolcraftCli(["create", "install-app", "--name", "Install App", "--yes", "--force"], {
225
+ cwd: tempRoot,
226
+ env: {},
227
+ stdout,
228
+ throwOnError: true,
229
+ async runCommand(command, args, options) {
230
+ installs.push({ args, command, cwd: options.cwd });
231
+ },
232
+ });
233
+
234
+ assert.deepEqual(installs, [
235
+ {
236
+ args: ["install"],
237
+ command: "pnpm",
238
+ cwd: path.join(tempRoot, "install-app"),
239
+ },
240
+ ]);
241
+ assert.match(stdout.text, /Installing dependencies with pnpm/);
242
+ assert.match(stdout.text, /pnpm dev/);
243
+ });
244
+
245
+ it("prints create help with the npx command", async () => {
246
+ const stdout = createWritableCapture();
247
+
248
+ const result = await runToolcraftCli(["create", "--help"], {
249
+ stdout,
250
+ throwOnError: true,
251
+ });
252
+
253
+ assert.equal(result.ok, true);
254
+ assert.match(stdout.text, /npx @pixel-point\/toolcraft create/);
255
+ assert.match(stdout.text, /--no-install/);
256
+ });
257
+
258
+ it("keeps create-toolcraft-app as a compatibility wrapper", async () => {
259
+ const tempRoot = await createTempRoot();
260
+ const binPath = path.join(packageRoot, "bin/create-toolcraft-app.mjs");
261
+
262
+ const { stdout } = await execFileAsync(
263
+ process.execPath,
264
+ [
265
+ binPath,
266
+ "wrapped-app",
267
+ "--name",
268
+ "Wrapped App",
269
+ "--yes",
270
+ "--force",
271
+ "--no-install",
272
+ ],
273
+ {
274
+ cwd: tempRoot,
275
+ env: {
276
+ ...process.env,
277
+ },
278
+ },
279
+ );
280
+
281
+ assert.match(stdout, /Created wrapped-app/);
282
+ assert.ok(await fs.stat(path.join(tempRoot, "wrapped-app/src/app/app-schema.ts")));
283
+ });
284
+ });
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const DEFAULT_IGNORED_NAMES = new Set([
5
+ ".DS_Store",
6
+ ".git",
7
+ ".turbo",
8
+ "dist",
9
+ "node_modules",
10
+ "playwright-report",
11
+ "test-results",
12
+ ]);
13
+
14
+ export async function pathExists(filePath) {
15
+ try {
16
+ await fs.access(filePath);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ export async function assertDirectory(directoryPath, label) {
24
+ let stats;
25
+
26
+ try {
27
+ stats = await fs.stat(directoryPath);
28
+ } catch {
29
+ throw new Error(`${label} was not found: ${directoryPath}`);
30
+ }
31
+
32
+ if (!stats.isDirectory()) {
33
+ throw new Error(`${label} must be a directory: ${directoryPath}`);
34
+ }
35
+ }
36
+
37
+ export async function ensureWritableTargetDirectory(targetDir, { force = false } = {}) {
38
+ await fs.mkdir(targetDir, { recursive: true });
39
+
40
+ const entries = await fs.readdir(targetDir);
41
+ const meaningfulEntries = entries.filter((entry) => entry !== ".DS_Store");
42
+
43
+ if (meaningfulEntries.length > 0 && !force) {
44
+ throw new Error(
45
+ `Target directory is not empty: ${targetDir}. Use --force to write into it.`,
46
+ );
47
+ }
48
+ }
49
+
50
+ export async function copyDirectory(sourceDir, targetDir, options = {}) {
51
+ const ignoredNames = options.ignoredNames ?? DEFAULT_IGNORED_NAMES;
52
+
53
+ await assertDirectory(sourceDir, "Source directory");
54
+ await fs.mkdir(targetDir, { recursive: true });
55
+
56
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
57
+
58
+ for (const entry of entries) {
59
+ if (ignoredNames.has(entry.name)) {
60
+ continue;
61
+ }
62
+
63
+ const sourcePath = path.join(sourceDir, entry.name);
64
+ const targetPath = path.join(targetDir, entry.name);
65
+
66
+ if (entry.isDirectory()) {
67
+ await copyDirectory(sourcePath, targetPath, { ignoredNames });
68
+ continue;
69
+ }
70
+
71
+ if (entry.isSymbolicLink()) {
72
+ const linkTarget = await fs.readlink(sourcePath);
73
+ await fs.rm(targetPath, { force: true, recursive: true });
74
+ await fs.symlink(linkTarget, targetPath);
75
+ continue;
76
+ }
77
+
78
+ if (entry.isFile()) {
79
+ await fs.copyFile(sourcePath, targetPath);
80
+ }
81
+ }
82
+ }
83
+
84
+ export async function removeDirectory(directoryPath) {
85
+ await fs.rm(directoryPath, { force: true, recursive: true });
86
+ }
@@ -0,0 +1,212 @@
1
+ import crypto from "node:crypto";
2
+ import fsSync from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import {
8
+ assertDirectory,
9
+ copyDirectory,
10
+ ensureWritableTargetDirectory,
11
+ pathExists,
12
+ removeDirectory,
13
+ } from "./copy-recursive.mjs";
14
+ import { createGeneratedPackageJson, createGeneratedTsConfig } from "./package-json.mjs";
15
+ import { rewriteGeneratedText, rewriteTextFiles } from "./rewrite-imports.mjs";
16
+
17
+ const PACKAGE_ROOT = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
18
+ const REPO_ROOT = path.resolve(fileURLToPath(new URL("../..", import.meta.url)));
19
+
20
+ function writeJson(filePath, value) {
21
+ return fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
22
+ }
23
+
24
+ async function readJson(filePath) {
25
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
26
+ }
27
+
28
+ async function hashFile(filePath) {
29
+ const buffer = await fs.readFile(filePath);
30
+ return crypto.createHash("sha256").update(buffer).digest("hex");
31
+ }
32
+
33
+ function createRelativePath(fromDir, toDir) {
34
+ const relativePath = path.relative(fromDir, toDir);
35
+ return relativePath === "" ? "." : relativePath;
36
+ }
37
+
38
+ function rewriteGeneratedAppText(source) {
39
+ return rewriteGeneratedText(source)
40
+ .replaceAll("starterAcceptance", "appAcceptance")
41
+ .replaceAll("starter-acceptance", "app-acceptance")
42
+ .replaceAll("starterProductReadiness", "appProductReadiness")
43
+ .replaceAll("starterTransferMode", "appTransferMode")
44
+ .replaceAll("starterPerformance", "appPerformance")
45
+ .replaceAll("starter-performance", "app-performance")
46
+ .replaceAll("starterSchema", "appSchema")
47
+ .replaceAll("starter-schema", "app-schema")
48
+ .replaceAll("StarterHome", "AppHome")
49
+ .replaceAll("Toolcraft Starter", "Toolcraft App Template");
50
+ }
51
+
52
+ async function renameGeneratedAppFiles(targetDir) {
53
+ const starterSchemaPath = path.join(targetDir, "src/app/starter-schema.ts");
54
+ const appSchemaPath = path.join(targetDir, "src/app/app-schema.ts");
55
+ const starterSchemaTestPath = path.join(targetDir, "src/app/starter-schema.test.ts");
56
+ const appSchemaTestPath = path.join(targetDir, "src/app/app-schema.test.ts");
57
+ const starterAcceptancePath = path.join(targetDir, "src/app/starter-acceptance.ts");
58
+ const appAcceptancePath = path.join(targetDir, "src/app/app-acceptance.ts");
59
+ const starterAcceptanceTestPath = path.join(targetDir, "src/app/starter-acceptance.test.ts");
60
+ const appAcceptanceTestPath = path.join(targetDir, "src/app/app-acceptance.test.ts");
61
+ const starterPerformancePath = path.join(targetDir, "src/app/starter-performance.ts");
62
+ const appPerformancePath = path.join(targetDir, "src/app/app-performance.ts");
63
+ const starterPerformanceTestPath = path.join(targetDir, "src/app/starter-performance.test.ts");
64
+ const appPerformanceTestPath = path.join(targetDir, "src/app/app-performance.test.ts");
65
+
66
+ if (await pathExists(starterSchemaPath)) {
67
+ await fs.rename(starterSchemaPath, appSchemaPath);
68
+ }
69
+
70
+ if (await pathExists(starterSchemaTestPath)) {
71
+ await fs.rename(starterSchemaTestPath, appSchemaTestPath);
72
+ }
73
+
74
+ if (await pathExists(starterAcceptancePath)) {
75
+ await fs.rename(starterAcceptancePath, appAcceptancePath);
76
+ }
77
+
78
+ if (await pathExists(starterAcceptanceTestPath)) {
79
+ await fs.rename(starterAcceptanceTestPath, appAcceptanceTestPath);
80
+ }
81
+
82
+ if (await pathExists(starterPerformancePath)) {
83
+ await fs.rename(starterPerformancePath, appPerformancePath);
84
+ }
85
+
86
+ if (await pathExists(starterPerformanceTestPath)) {
87
+ await fs.rename(starterPerformanceTestPath, appPerformanceTestPath);
88
+ }
89
+ }
90
+
91
+ async function removeToolcraftTestFiles(toolcraftRoot) {
92
+ async function visit(currentDir) {
93
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
94
+
95
+ for (const entry of entries) {
96
+ const filePath = path.join(currentDir, entry.name);
97
+
98
+ if (entry.isDirectory()) {
99
+ await visit(filePath);
100
+ continue;
101
+ }
102
+
103
+ if (entry.isFile() && /\.(test|spec)\.[cm]?[jt]sx?$/.test(entry.name)) {
104
+ await fs.rm(filePath);
105
+ }
106
+ }
107
+ }
108
+
109
+ await visit(toolcraftRoot);
110
+ }
111
+
112
+ async function writeToolcraftIntegrityManifest(toolcraftRoot) {
113
+ const files = {};
114
+
115
+ async function visit(currentDir) {
116
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
117
+
118
+ for (const entry of entries) {
119
+ const filePath = path.join(currentDir, entry.name);
120
+
121
+ if (entry.isDirectory()) {
122
+ await visit(filePath);
123
+ continue;
124
+ }
125
+
126
+ if (entry.isFile() && entry.name !== ".toolcraft-manifest.json") {
127
+ const relativePath = path.relative(toolcraftRoot, filePath).split(path.sep).join("/");
128
+ files[relativePath] = await hashFile(filePath);
129
+ }
130
+ }
131
+ }
132
+
133
+ await visit(toolcraftRoot);
134
+ await writeJson(path.join(toolcraftRoot, ".toolcraft-manifest.json"), {
135
+ description:
136
+ "Integrity manifest for copied Toolcraft sources. Generated apps should not edit src/toolcraft.",
137
+ files: Object.fromEntries(
138
+ Object.entries(files).sort(([left], [right]) => left.localeCompare(right)),
139
+ ),
140
+ version: 1,
141
+ });
142
+ }
143
+
144
+ function allDirectoriesExist(paths) {
145
+ return Object.values(paths).every((directoryPath) => {
146
+ try {
147
+ return fsSync.statSync(directoryPath).isDirectory();
148
+ } catch {
149
+ return false;
150
+ }
151
+ });
152
+ }
153
+
154
+ export function getDefaultSourcePaths(repoRoot = REPO_ROOT) {
155
+ const monorepoPaths = {
156
+ toolcraftSrc: path.join(repoRoot, "packages/toolcraft-runtime/src"),
157
+ starterDir: path.join(repoRoot, "starter"),
158
+ uiSrc: path.join(repoRoot, "packages/ui/src"),
159
+ };
160
+
161
+ if (allDirectoriesExist(monorepoPaths)) {
162
+ return monorepoPaths;
163
+ }
164
+
165
+ return {
166
+ toolcraftSrc: path.join(PACKAGE_ROOT, "templates/runtime"),
167
+ starterDir: path.join(PACKAGE_ROOT, "templates/starter"),
168
+ uiSrc: path.join(PACKAGE_ROOT, "templates/ui"),
169
+ };
170
+ }
171
+
172
+ export async function generateToolcraft(options = {}) {
173
+ const cwd = options.cwd ?? process.cwd();
174
+ const targetDir = path.resolve(cwd, options.targetDir ?? ".");
175
+ const sourcePaths = options.sourcePaths ?? getDefaultSourcePaths(options.repoRoot ?? REPO_ROOT);
176
+
177
+ await assertDirectory(sourcePaths.starterDir, "Starter app");
178
+ await assertDirectory(sourcePaths.uiSrc, "UI package source");
179
+ await assertDirectory(sourcePaths.toolcraftSrc, "Toolcraft template runtime package source");
180
+ const starterPackageJson = await readJson(path.join(sourcePaths.starterDir, "package.json"));
181
+ await ensureWritableTargetDirectory(targetDir, { force: options.force });
182
+
183
+ await copyDirectory(sourcePaths.starterDir, targetDir);
184
+ await renameGeneratedAppFiles(targetDir);
185
+
186
+ const toolcraftRoot = path.join(targetDir, "src/toolcraft");
187
+ await removeDirectory(toolcraftRoot);
188
+ await fs.mkdir(toolcraftRoot, { recursive: true });
189
+ await copyDirectory(sourcePaths.uiSrc, path.join(toolcraftRoot, "ui"));
190
+ await copyDirectory(sourcePaths.toolcraftSrc, path.join(toolcraftRoot, "runtime"));
191
+ await removeToolcraftTestFiles(toolcraftRoot);
192
+
193
+ const changedFiles = await rewriteTextFiles(targetDir, (source) =>
194
+ rewriteGeneratedAppText(source),
195
+ );
196
+ await writeToolcraftIntegrityManifest(toolcraftRoot);
197
+ const packageJson = createGeneratedPackageJson({
198
+ name: options.name ?? path.basename(targetDir),
199
+ starterPackageJson,
200
+ });
201
+
202
+ await writeJson(path.join(targetDir, "package.json"), packageJson);
203
+ await writeJson(path.join(targetDir, "tsconfig.json"), createGeneratedTsConfig());
204
+ await fs.rm(path.join(targetDir, "pnpm-lock.yaml"), { force: true });
205
+
206
+ return {
207
+ changedFiles,
208
+ packageName: packageJson.name,
209
+ relativeTargetDir: createRelativePath(cwd, targetDir),
210
+ targetDir,
211
+ };
212
+ }