@pixel-point/toolcraft 0.0.12 → 0.0.14

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 (37) hide show
  1. package/README.md +4 -2
  2. package/package.json +1 -1
  3. package/src/cli.mjs +18 -45
  4. package/src/cli.test.mjs +141 -6
  5. package/src/command-runner.mjs +71 -0
  6. package/src/dependency-install.mjs +87 -0
  7. package/src/generate.mjs +32 -4
  8. package/src/generate.test.mjs +116 -15
  9. package/src/package-json.mjs +11 -2
  10. package/src/package-json.test.mjs +27 -0
  11. package/src/package-manager.mjs +123 -0
  12. package/src/package-manager.test.mjs +80 -0
  13. package/templates/runtime/contracts/component-contracts.test.ts +1 -1
  14. package/templates/runtime/contracts/component-contracts.ts +1 -1
  15. package/templates/starter/AGENTS.md +4 -3
  16. package/templates/starter/docs/toolcraft/README.md +15 -0
  17. package/templates/starter/docs/toolcraft/acceptance-testing.md +2 -0
  18. package/templates/starter/docs/toolcraft/assembly-workflow.md +35 -171
  19. package/templates/starter/docs/toolcraft/component-rules.md +12 -188
  20. package/templates/starter/docs/toolcraft/core/control-selection.md +93 -0
  21. package/templates/starter/docs/toolcraft/core/layout.md +104 -0
  22. package/templates/starter/docs/toolcraft/core/media-upload.md +85 -0
  23. package/templates/starter/docs/toolcraft/core/performance.md +83 -0
  24. package/templates/starter/docs/toolcraft/core/reference-study.md +115 -0
  25. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +53 -0
  26. package/templates/starter/docs/toolcraft/core/setup-export.md +86 -0
  27. package/templates/starter/docs/toolcraft/core/timeline-animation.md +67 -0
  28. package/templates/starter/docs/toolcraft/custom-controls.md +2 -0
  29. package/templates/starter/docs/toolcraft/performance.md +2 -0
  30. package/templates/starter/docs/toolcraft/renderer-technique.md +2 -0
  31. package/templates/starter/docs/toolcraft/schema-reference.md +117 -367
  32. package/templates/starter/docs/toolcraft/workflow.md +12 -10
  33. package/templates/starter/package.json +1 -0
  34. package/templates/starter/scripts/check-toolcraft-docs.mjs +28 -6
  35. package/templates/starter/scripts/run-vite-on-free-port.mjs +25 -6
  36. package/templates/starter/src/app/starter-acceptance.test.ts +24 -15
  37. package/templates/starter/src/app/starter-performance.test.ts +17 -7
@@ -73,6 +73,7 @@ describe("generateToolcraft", () => {
73
73
  assert.equal(packageJson.dependencies["@repo/toolcraft-runtime"], undefined);
74
74
  assert.equal(packageJson.dependencies["@tanstack/react-router"], "1.170.6");
75
75
  assert.equal(packageJson.dependencies.cmdk, "^1.1.1");
76
+ assert.equal(packageJson.dependencies["cross-spawn"], "^7.0.6");
76
77
  assert.equal(packageJson.dependencies["react-resizable-panels"], "^4.10.0");
77
78
  assert.equal(packageJson.dependencies.sonner, "^2.0.7");
78
79
  assert.equal(packageJson.devDependencies["@repo/typescript-config"], undefined);
@@ -101,13 +102,13 @@ describe("generateToolcraft", () => {
101
102
  packageJson.scripts["test:browser:perf"],
102
103
  "playwright install chromium && playwright test --grep \"browser perf:\" --workers=1 --pass-with-no-tests",
103
104
  );
104
- assert.equal(packageJson.scripts["verify:quick"], "pnpm ai:check && pnpm test");
105
- assert.equal(packageJson.scripts["verify:ui"], "pnpm test:browser");
106
- assert.equal(packageJson.scripts["verify:perf"], "pnpm test:browser:perf");
107
- assert.equal(packageJson.scripts["verify:perf:playwright"], "pnpm test:browser:perf");
105
+ assert.equal(packageJson.scripts["verify:quick"], "npm run ai:check && npm run test");
106
+ assert.equal(packageJson.scripts["verify:ui"], "npm run test:browser");
107
+ assert.equal(packageJson.scripts["verify:perf"], "npm run test:browser:perf");
108
+ assert.equal(packageJson.scripts["verify:perf:playwright"], "npm run test:browser:perf");
108
109
  assert.equal(
109
110
  packageJson.scripts["verify:final"],
110
- "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
111
+ "npm run ai:check && npm run test && npm run build && npm run test:browser",
111
112
  );
112
113
 
113
114
  const indexHtmlSource = await fs.readFile(path.join(targetDir, "index.html"), "utf8");
@@ -145,6 +146,15 @@ describe("generateToolcraft", () => {
145
146
  assert.ok(await fs.stat(path.join(targetDir, "scripts/check-toolcraft-docs.mjs")));
146
147
  assert.ok(await fs.stat(path.join(targetDir, "scripts/check-toolcraft-integrity.mjs")));
147
148
  assert.ok(await fs.stat(path.join(targetDir, "scripts/run-vite-on-free-port.mjs")));
149
+ const runViteSource = await fs.readFile(
150
+ path.join(targetDir, "scripts/run-vite-on-free-port.mjs"),
151
+ "utf8",
152
+ );
153
+ assert.match(runViteSource, /node_modules/);
154
+ assert.match(runViteSource, /\.bin/);
155
+ assert.match(runViteSource, /vite\.cmd/);
156
+ assert.match(runViteSource, /from "cross-spawn"/);
157
+ assert.doesNotMatch(runViteSource, /pnpmCommand|pnpm\.cmd|pnpm",/);
148
158
  assert.ok(await fs.stat(path.join(targetDir, "LICENSE.md")));
149
159
  assert.ok(await fs.stat(path.join(targetDir, "NOTICE.md")));
150
160
  assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/README.md")));
@@ -156,6 +166,14 @@ describe("generateToolcraft", () => {
156
166
  assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/renderer-technique.md")));
157
167
  assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/custom-controls.md")));
158
168
  assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/component-rules.md")));
169
+ assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/core/runtime-boundary.md")));
170
+ assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/core/setup-export.md")));
171
+ assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/core/control-selection.md")));
172
+ assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/core/layout.md")));
173
+ assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/core/media-upload.md")));
174
+ assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/core/timeline-animation.md")));
175
+ assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/core/performance.md")));
176
+ assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/core/reference-study.md")));
159
177
  assert.ok(await fs.stat(path.join(targetDir, "src/toolcraft/.toolcraft-manifest.json")));
160
178
 
161
179
  const appPerformanceSource = await fs.readFile(
@@ -214,6 +232,15 @@ describe("generateToolcraft", () => {
214
232
  const licenseSource = await fs.readFile(path.join(targetDir, "LICENSE.md"), "utf8");
215
233
  const noticeSource = await fs.readFile(path.join(targetDir, "NOTICE.md"), "utf8");
216
234
  const localDocPaths = [
235
+ "workflow.md",
236
+ "core/runtime-boundary.md",
237
+ "core/setup-export.md",
238
+ "core/control-selection.md",
239
+ "core/layout.md",
240
+ "core/media-upload.md",
241
+ "core/timeline-animation.md",
242
+ "core/performance.md",
243
+ "core/reference-study.md",
217
244
  "assembly-workflow.md",
218
245
  "decision-contract.md",
219
246
  "schema-reference.md",
@@ -232,14 +259,14 @@ describe("generateToolcraft", () => {
232
259
  assert.match(agentsSource, /Required AI Workflow Skills/);
233
260
  assert.match(agentsSource, /Verification Tier Classifier/);
234
261
  assert.match(agentsSource, /Verification tier: Tier N/);
235
- assert.match(agentsSource, /pnpm verify:quick/);
236
- assert.match(agentsSource, /pnpm verify:final/);
237
- assert.match(agentsSource, /pnpm ai:check/);
262
+ assert.match(agentsSource, /npm run verify:quick/);
263
+ assert.match(agentsSource, /npm run verify:final/);
264
+ assert.match(agentsSource, /npm run ai:check/);
238
265
  assert.match(agentsSource, /use `brainstorming`/);
239
266
  assert.match(agentsSource, /use `writing-plans`/);
240
267
  assert.match(agentsSource, /use `systematic-debugging`/);
241
268
  assert.match(agentsSource, /browser` workflow/);
242
- assert.match(agentsSource, /pnpm test:browser/);
269
+ assert.match(agentsSource, /npm run test:browser/);
243
270
  assert.match(agentsSource, /Do not silently skip required workflow skills/);
244
271
  assert.match(agentsSource, /Toolcraft app contract overrides generic brainstorming approval/);
245
272
  assert.match(agentsSource, /Do not ask the user to confirm decisions already covered/);
@@ -272,17 +299,43 @@ describe("generateToolcraft", () => {
272
299
  path.join(targetDir, "docs/toolcraft/component-rules.md"),
273
300
  "utf8",
274
301
  );
302
+ const coreControlSelectionSource = await fs.readFile(
303
+ path.join(targetDir, "docs/toolcraft/core/control-selection.md"),
304
+ "utf8",
305
+ );
306
+ const coreLayoutSource = await fs.readFile(
307
+ path.join(targetDir, "docs/toolcraft/core/layout.md"),
308
+ "utf8",
309
+ );
310
+ const coreMediaUploadSource = await fs.readFile(
311
+ path.join(targetDir, "docs/toolcraft/core/media-upload.md"),
312
+ "utf8",
313
+ );
314
+ const coreRuntimeBoundarySource = await fs.readFile(
315
+ path.join(targetDir, "docs/toolcraft/core/runtime-boundary.md"),
316
+ "utf8",
317
+ );
318
+ const coreSetupExportSource = await fs.readFile(
319
+ path.join(targetDir, "docs/toolcraft/core/setup-export.md"),
320
+ "utf8",
321
+ );
322
+ const coreTimelineSource = await fs.readFile(
323
+ path.join(targetDir, "docs/toolcraft/core/timeline-animation.md"),
324
+ "utf8",
325
+ );
275
326
  assert.match(componentRulesSource, /Slider `step` means numeric snapping only/);
276
327
  assert.match(componentRulesSource, /variant: "discrete"/);
277
328
  assert.match(componentRulesSource, /Keep large or precision stepped ranges visually continuous/);
278
329
  assert.match(componentRulesSource, /Segmented Controls/);
279
- assert.match(componentRulesSource, /Never generate a section titled `Color` or `Colors`/);
280
- assert.match(componentRulesSource, /Use `fileDrop` for source material uploads/);
330
+ assert.match(coreLayoutSource, /Bad titles: `Controls`, `Settings`, `Options`, `Sliders`, `Inputs`, `Buttons`, `Color`, `Colors`/);
331
+ assert.match(coreMediaUploadSource, /Use `fileDrop` for source material uploads/);
332
+ assert.match(coreControlSelectionSource, /Use `fileDrop` for source material uploads/);
333
+ assert.match(coreRuntimeBoundarySource, /appSchema\.assembly/);
281
334
  assert.match(componentRulesSource, /Do not show Layers for a single-layer app/);
282
- assert.match(componentRulesSource, /Use playback timeline/);
283
- assert.match(componentRulesSource, /Still-output product apps include one primary `Export PNG` action/);
284
- assert.match(componentRulesSource, /Animated product apps include `Export Video`/);
285
- assert.match(componentRulesSource, /Copy never replaces export/);
335
+ assert.match(coreTimelineSource, /Any app with `Export Video` must enable the top Toolcraft timeline/);
336
+ assert.match(coreSetupExportSource, /Still products expose `Export PNG`/);
337
+ assert.match(coreSetupExportSource, /Animated products expose `Export Video` plus `Export PNG`/);
338
+ assert.match(coreSetupExportSource, /Clipboard copy is optional and never replaces export/);
286
339
 
287
340
  const assemblyDocsSource = await fs.readFile(
288
341
  path.join(targetDir, "docs/toolcraft/assembly-workflow.md"),
@@ -346,6 +399,54 @@ describe("generateToolcraft", () => {
346
399
  );
347
400
  });
348
401
 
402
+ it("generates package-manager-specific commands for pnpm apps", async () => {
403
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-cli-"));
404
+ tempRoots.push(tempRoot);
405
+ const targetDir = path.join(tempRoot, "pnpm-app");
406
+
407
+ await generateToolcraft({
408
+ cwd: tempRoot,
409
+ force: true,
410
+ name: "Pnpm App",
411
+ packageManager: "pnpm",
412
+ targetDir: "pnpm-app",
413
+ });
414
+
415
+ const packageJson = JSON.parse(await fs.readFile(path.join(targetDir, "package.json"), "utf8"));
416
+ assert.equal(packageJson.scripts["verify:quick"], "pnpm ai:check && pnpm test");
417
+ assert.equal(packageJson.scripts["verify:ui"], "pnpm test:browser");
418
+ assert.equal(packageJson.scripts["verify:perf"], "pnpm test:browser:perf");
419
+ assert.equal(packageJson.scripts["verify:perf:playwright"], "pnpm test:browser:perf");
420
+ assert.equal(
421
+ packageJson.scripts["verify:final"],
422
+ "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
423
+ );
424
+
425
+ const playwrightConfigSource = await fs.readFile(
426
+ path.join(targetDir, "playwright.config.ts"),
427
+ "utf8",
428
+ );
429
+ assert.match(playwrightConfigSource, /pnpm exec vite dev/);
430
+ assert.doesNotMatch(playwrightConfigSource, /npm exec -- vite/);
431
+
432
+ const agentsSource = await fs.readFile(path.join(targetDir, "AGENTS.md"), "utf8");
433
+ assert.match(agentsSource, /pnpm verify:quick/);
434
+ assert.match(agentsSource, /pnpm verify:final/);
435
+ assert.match(agentsSource, /pnpm test:browser/);
436
+ assert.doesNotMatch(agentsSource, /npm run verify:quick/);
437
+
438
+ const worklogSource = await fs.readFile(
439
+ path.join(targetDir, "docs/toolcraft/agent-worklog.md"),
440
+ "utf8",
441
+ );
442
+ assert.match(worklogSource, /pnpm verify:quick/);
443
+ assert.match(worklogSource, /pnpm verify:final/);
444
+
445
+ await execFileAsync(process.execPath, [path.join(targetDir, "scripts/check-toolcraft-docs.mjs")], {
446
+ cwd: targetDir,
447
+ });
448
+ });
449
+
349
450
  it("writes the app identity marker when the generated title matches the template title", async () => {
350
451
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-cli-"));
351
452
  tempRoots.push(tempRoot);
@@ -1,3 +1,5 @@
1
+ import { DEFAULT_PACKAGE_MANAGER, createVerificationScripts } from "./package-manager.mjs";
2
+
1
3
  export function sanitizePackageName(value) {
2
4
  const sanitized = String(value ?? "")
3
5
  .trim()
@@ -44,7 +46,11 @@ function addDependencyGroup(packageJson, groupName, dependencies) {
44
46
  }
45
47
  }
46
48
 
47
- export function createGeneratedPackageJson({ name, starterPackageJson }) {
49
+ export function createGeneratedPackageJson({
50
+ name,
51
+ packageManager = DEFAULT_PACKAGE_MANAGER,
52
+ starterPackageJson,
53
+ }) {
48
54
  if (!starterPackageJson || typeof starterPackageJson !== "object") {
49
55
  throw new Error("starterPackageJson is required to create a generated package manifest.");
50
56
  }
@@ -54,7 +60,10 @@ export function createGeneratedPackageJson({ name, starterPackageJson }) {
54
60
  private: true,
55
61
  license: starterPackageJson.license,
56
62
  type: starterPackageJson.type ?? "module",
57
- scripts: starterPackageJson.scripts ?? {},
63
+ scripts: {
64
+ ...(starterPackageJson.scripts ?? {}),
65
+ ...createVerificationScripts(packageManager),
66
+ },
58
67
  };
59
68
 
60
69
  addDependencyGroup(packageJson, "dependencies", starterPackageJson.dependencies);
@@ -58,6 +58,12 @@ describe("createGeneratedPackageJson", () => {
58
58
  scripts: {
59
59
  dev: "vite dev",
60
60
  test: "vitest run",
61
+ "verify:final":
62
+ "npm run ai:check && npm run test && npm run build && npm run test:browser",
63
+ "verify:perf": "npm run test:browser:perf",
64
+ "verify:perf:playwright": "npm run test:browser:perf",
65
+ "verify:quick": "npm run ai:check && npm run test",
66
+ "verify:ui": "npm run test:browser",
61
67
  },
62
68
  dependencies: {
63
69
  "@tanstack/react-router": "1.170.6",
@@ -77,4 +83,25 @@ describe("createGeneratedPackageJson", () => {
77
83
  /starterPackageJson is required/,
78
84
  );
79
85
  });
86
+
87
+ it("generates verification scripts for the selected package manager", () => {
88
+ const packageJson = createGeneratedPackageJson({
89
+ name: "Generated App",
90
+ packageManager: "pnpm",
91
+ starterPackageJson: {
92
+ scripts: {
93
+ dev: "vite dev",
94
+ },
95
+ },
96
+ });
97
+
98
+ assert.deepEqual(packageJson.scripts, {
99
+ dev: "vite dev",
100
+ "verify:final": "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
101
+ "verify:perf": "pnpm test:browser:perf",
102
+ "verify:perf:playwright": "pnpm test:browser:perf",
103
+ "verify:quick": "pnpm ai:check && pnpm test",
104
+ "verify:ui": "pnpm test:browser",
105
+ });
106
+ });
80
107
  });
@@ -0,0 +1,123 @@
1
+ export const DEFAULT_PACKAGE_MANAGER = "npm";
2
+
3
+ const PACKAGE_MANAGER_SPECS = {
4
+ npm: {
5
+ installHelp: [" Install Node.js from https://nodejs.org/"],
6
+ installArgs: ["install"],
7
+ runScript(scriptName) {
8
+ return `npm run ${scriptName}`;
9
+ },
10
+ exec(binaryName, args = []) {
11
+ return ["npm", "exec", "--", binaryName, ...args].join(" ");
12
+ },
13
+ },
14
+ pnpm: {
15
+ installHelp: [" corepack enable", " corepack prepare pnpm@latest --activate"],
16
+ installArgs: ["install"],
17
+ runScript(scriptName) {
18
+ return `pnpm ${scriptName}`;
19
+ },
20
+ exec(binaryName, args = []) {
21
+ return ["pnpm", "exec", binaryName, ...args].join(" ");
22
+ },
23
+ },
24
+ };
25
+
26
+ const PACKAGE_MANAGER_NAMES = new Set(Object.keys(PACKAGE_MANAGER_SPECS));
27
+
28
+ const GENERATED_SCRIPT_COMMANDS = [
29
+ "ai:check",
30
+ "build",
31
+ "dev",
32
+ "dev:restart",
33
+ "preview",
34
+ "preview:restart",
35
+ "test",
36
+ "test:browser",
37
+ "test:browser:perf",
38
+ "verify:final",
39
+ "verify:perf",
40
+ "verify:perf:playwright",
41
+ "verify:quick",
42
+ "verify:ui",
43
+ ];
44
+
45
+ function getSpec(packageManager) {
46
+ return PACKAGE_MANAGER_SPECS[normalizePackageManager(packageManager)];
47
+ }
48
+
49
+ function replaceAllExact(source, replacements) {
50
+ let next = String(source);
51
+
52
+ for (const [from, to] of replacements) {
53
+ next = next.split(from).join(to);
54
+ }
55
+
56
+ return next;
57
+ }
58
+
59
+ export function normalizePackageManager(value, fallback = DEFAULT_PACKAGE_MANAGER) {
60
+ return PACKAGE_MANAGER_NAMES.has(value) ? value : fallback;
61
+ }
62
+
63
+ export function detectPackageManager(env = process.env) {
64
+ const userAgent = String(env.npm_config_user_agent ?? "");
65
+ const [packageManager] = userAgent.split("/", 1);
66
+ return normalizePackageManager(packageManager);
67
+ }
68
+
69
+ export function createInstallCommand(packageManager) {
70
+ const spec = getSpec(packageManager);
71
+
72
+ return {
73
+ args: [...spec.installArgs],
74
+ command: normalizePackageManager(packageManager),
75
+ };
76
+ }
77
+
78
+ export function createInstallHelp(packageManager) {
79
+ return [...getSpec(packageManager).installHelp];
80
+ }
81
+
82
+ export function createRunScriptCommand(packageManager, scriptName) {
83
+ return getSpec(packageManager).runScript(scriptName);
84
+ }
85
+
86
+ export function createExecCommand(packageManager, binaryName, args = []) {
87
+ return getSpec(packageManager).exec(binaryName, args);
88
+ }
89
+
90
+ export function createVerificationScripts(packageManager) {
91
+ return {
92
+ "verify:quick": [
93
+ createRunScriptCommand(packageManager, "ai:check"),
94
+ createRunScriptCommand(packageManager, "test"),
95
+ ].join(" && "),
96
+ "verify:ui": createRunScriptCommand(packageManager, "test:browser"),
97
+ "verify:perf": createRunScriptCommand(packageManager, "test:browser:perf"),
98
+ "verify:perf:playwright": createRunScriptCommand(packageManager, "test:browser:perf"),
99
+ "verify:final": [
100
+ createRunScriptCommand(packageManager, "ai:check"),
101
+ createRunScriptCommand(packageManager, "test"),
102
+ createRunScriptCommand(packageManager, "build"),
103
+ createRunScriptCommand(packageManager, "test:browser"),
104
+ ].join(" && "),
105
+ };
106
+ }
107
+
108
+ export function createGeneratedCommandReplacements(packageManager) {
109
+ const replacements = [
110
+ ["pnpm exec vite", createExecCommand(packageManager, "vite")],
111
+ ["pnpm install", `${normalizePackageManager(packageManager)} install`],
112
+ ];
113
+
114
+ for (const scriptName of GENERATED_SCRIPT_COMMANDS) {
115
+ replacements.push([`pnpm ${scriptName}`, createRunScriptCommand(packageManager, scriptName)]);
116
+ }
117
+
118
+ return replacements.sort(([left], [right]) => right.length - left.length);
119
+ }
120
+
121
+ export function replaceGeneratedCommandReferences(source, packageManager) {
122
+ return replaceAllExact(source, createGeneratedCommandReplacements(packageManager));
123
+ }
@@ -0,0 +1,80 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+
4
+ import {
5
+ createExecCommand,
6
+ createInstallCommand,
7
+ createRunScriptCommand,
8
+ createVerificationScripts,
9
+ detectPackageManager,
10
+ replaceGeneratedCommandReferences,
11
+ } from "./package-manager.mjs";
12
+
13
+ describe("detectPackageManager", () => {
14
+ it("uses the npm user agent when the CLI is launched through npx", () => {
15
+ assert.equal(
16
+ detectPackageManager({
17
+ npm_config_user_agent: "npm/10.9.4 node/v24.4.1 darwin arm64 workspaces/false",
18
+ }),
19
+ "npm",
20
+ );
21
+ });
22
+
23
+ it("detects supported package manager launchers", () => {
24
+ assert.equal(detectPackageManager({ npm_config_user_agent: "pnpm/10.12.4 npm/? node/v24.4.1" }), "pnpm");
25
+ });
26
+
27
+ it("falls back to npm when no supported package manager is detected", () => {
28
+ assert.equal(detectPackageManager({}), "npm");
29
+ assert.equal(detectPackageManager({ npm_config_user_agent: "unknown/1.0.0" }), "npm");
30
+ assert.equal(detectPackageManager({ npm_config_user_agent: "yarn/1.22.22 npm/? node/v24.4.1" }), "npm");
31
+ assert.equal(detectPackageManager({ npm_config_user_agent: "bun/1.2.0 npm/? node/v24.4.1" }), "npm");
32
+ });
33
+ });
34
+
35
+ describe("package manager commands", () => {
36
+ it("formats install, run, exec, and verification commands for npm", () => {
37
+ assert.deepEqual(createInstallCommand("npm"), {
38
+ args: ["install"],
39
+ command: "npm",
40
+ });
41
+ assert.equal(createRunScriptCommand("npm", "dev"), "npm run dev");
42
+ assert.equal(createExecCommand("npm", "vite", ["dev"]), "npm exec -- vite dev");
43
+ assert.deepEqual(createVerificationScripts("npm"), {
44
+ "verify:final":
45
+ "npm run ai:check && npm run test && npm run build && npm run test:browser",
46
+ "verify:perf": "npm run test:browser:perf",
47
+ "verify:perf:playwright": "npm run test:browser:perf",
48
+ "verify:quick": "npm run ai:check && npm run test",
49
+ "verify:ui": "npm run test:browser",
50
+ });
51
+ });
52
+
53
+ it("formats run and exec commands for pnpm", () => {
54
+ assert.equal(createRunScriptCommand("pnpm", "dev"), "pnpm dev");
55
+ assert.equal(createExecCommand("pnpm", "vite", ["dev"]), "pnpm exec vite dev");
56
+ });
57
+
58
+ it("replaces known generated command references for npm", () => {
59
+ const source = [
60
+ "pnpm install",
61
+ "pnpm verify:final",
62
+ "pnpm verify:perf",
63
+ "pnpm test:browser",
64
+ "pnpm dev",
65
+ "pnpm exec vite dev --host 127.0.0.1",
66
+ ].join("\n");
67
+
68
+ assert.equal(
69
+ replaceGeneratedCommandReferences(source, "npm"),
70
+ [
71
+ "npm install",
72
+ "npm run verify:final",
73
+ "npm run verify:perf",
74
+ "npm run test:browser",
75
+ "npm run dev",
76
+ "npm exec -- vite dev --host 127.0.0.1",
77
+ ].join("\n"),
78
+ );
79
+ });
80
+ });
@@ -929,7 +929,7 @@ describe("Toolcraft template component contracts", () => {
929
929
  "Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
930
930
  );
931
931
  expect(contract.aiUsageRules).toContain(
932
- "A full performance checkpoint must run only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance; use the agent-controlled browser first and pnpm verify:perf only as the Playwright fallback.",
932
+ "A full performance checkpoint must run only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance; use the agent-controlled browser first and this app's verify:perf script only as the Playwright fallback.",
933
933
  );
934
934
  expect(contract.aiUsageRules).toContain(
935
935
  "Renderer, canvas, animation, export, timeline, layers, canvas.renderScale, bug fixes, and performance-sensitive control changes use targeted functional/browser checks first and targeted performance scenarios only for touched workload, viewport, or export paths.",
@@ -1186,7 +1186,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
1186
1186
  "Expensive renderers must cache decoded media, source pixels, glyph atlases, gradients, and other reusable inputs by media id, canvas size, and stable control keys.",
1187
1187
  "Slider drags and high-frequency controls must debounce or coalesce preview work, cancel stale async renders, and avoid re-decoding media on every control change.",
1188
1188
  "Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
1189
- "A full performance checkpoint must run only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance; use the agent-controlled browser first and pnpm verify:perf only as the Playwright fallback.",
1189
+ "A full performance checkpoint must run only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance; use the agent-controlled browser first and this app's verify:perf script only as the Playwright fallback.",
1190
1190
  "Renderer, canvas, animation, export, timeline, layers, canvas.renderScale, bug fixes, and performance-sensitive control changes use targeted functional/browser checks first and targeted performance scenarios only for touched workload, viewport, or export paths.",
1191
1191
  "Performance fixes must preserve selected output and preview quality; do not reduce image quality, selected renderScale, export resolution, source media fidelity, or canvas backing pixels as the hidden way to pass budgets.",
1192
1192
  "When canvas or slider interactions lag, diagnose where the slowdown comes from before changing output quality: renderer technique, React update frequency, decoded media, shader/program setup, buffer uploads, layout work, async render cancellation, or animation scheduling.",
@@ -24,11 +24,11 @@ Then follow `workflow.md` to choose the required contract docs and verification
24
24
  10. If a Figma URL is provided, inspect the Figma file through MCP and rebuild from its structure; never implement from a screenshot or by eye.
25
25
  11. If a video, GIF, screen recording, contact sheet, or extracted-frame sequence is provided as a reference, write a Video Reference Study before implementation: storyboard frames, frame-to-frame transition analysis, behavior decomposition, and acceptance mapping. Do not implement video references from a single screenshot or high-level summary.
26
26
  12. Choose an explicit persistence policy; use schema `persistence` for user-edited app settings that should survive reload, and test real reload restoration when localStorage is enabled.
27
- 13. Generated apps keep a controls panel so runtime `Setup` is visible from the first run; product sections are added after it. Runtime `Setup` is the first visible headerless controls block, is not collapsible, and always contains `Export Settings` and `Import Settings`; never implement settings import/export through `panelActions` or route-local file inputs, and never gate this block by app complexity. Visible `Aspect ratio`, `Canvas width`, and `Canvas height` controls are owned by `editable-output` canvas sizing and merge into the same Setup block after settings transfer. App-authored sections must not declare runtime Setup targets such as `runtime.settingsTransfer`, `canvas.aspectRatio`, `canvas.size.width`, `canvas.size.height`, `canvas.renderScale`, or `panels.timeline.extended`; those controls never suppress the mandatory runtime Setup controls. Runtime aspect presets apply canonical canvas sizes, with `16:9` equal to `1920x1080`; manual Canvas width/height edits keep the typed dimension, keep the other dimension unchanged, switch Aspect ratio to Custom, and show the reduced current ratio in custom ratio inputs; when no explicit product size is provided, the runtime default canvas size is also `1920x1080`. Product-output, exportable, shader, procedural, and reference-clone apps use `editable-output`; uploaded background/source images inside product canvases also use `editable-output`, keep the current canvas size, and render as cover/crop inside the current canvas bounds. Fixed/reference/base dimensions are initial `canvas.size` values, not reasons to hide `Aspect ratio`, `Canvas width`, or `Canvas height`. Non-vector raster, Canvas 2D, WebGL, and WebGPU previews set `canvas.renderScale: true`; Setup then appends `Resolution scale` after canvas sizing so backing pixels can increase up to scale 2 without changing CSS/output size. Performance fixes must preserve the selected render scale and keep canvas preview responsive to sliders/high-frequency controls at that scale; diagnose the bottleneck before reducing quality. Do not pass budgets by silently downsampling, stretching a lower-resolution backing canvas, blurring output, or clamping `canvas.renderScale` below the user's chosen value. When `panels.timeline` is enabled, runtime appends a `Timeline` switch as the last Setup control; off shows compact Play-only transport, on shows the extended timeline with scrubber, duration, loop, and keyframe UI, and the switch never changes product values, playback, keyframes, export, or Reset controls. When `panels.timeline` is omitted, the Timeline switch must not appear.
28
- 14. Product apps expose a required `Background` section directly before export settings. It contains a Switch labeled `Include` and a background color control with `label: false` in one equal-width inline row; PNG export wires those runtime values into the standard export helper, live preview uses `shouldIncludeToolcraftPreviewBackground(state)` so Include can hide the product background, and video export keeps the background. Every app with `Export PNG` exposes `Image Export` with `export.image.format` and `export.image.resolution` as two `select` controls in one compact two-column inline row, and passes the selected resolution to `createToolcraftPngExportCanvas({ resolution })` so 2K/4K/8K change actual PNG dimensions. Animated apps with video export enable the top Toolcraft timeline and place `Image Export` immediately before `Video Export`.
27
+ 13. Generated apps follow the mandatory runtime Setup, canvas sizing, render scale, Timeline switch, Background, Image Export, Video Export, and sticky action rules in `docs/toolcraft/core/setup-export.md`. Do not duplicate or reinterpret those controls in app-authored sections.
28
+ 14. Media uploads, image/file mode, source images, multi-upload sorting, default assets, and image transform actions follow `docs/toolcraft/core/media-upload.md`.
29
29
  15. Keep `docs/toolcraft/agent-worklog.md` current with a decision trail, product decisions, explicit reference inputs, evidence, verification, and risks. Reference-runtime-clone apps also declare `referenceStudy` plus `referenceFeatureInventory` so every inspected reference feature has feature-level behavior evidence and maps to Toolcraft implementation and acceptance coverage.
30
30
  16. Prove every visible entity through acceptance, browser, and performance coverage.
31
- 17. Workload performance scenarios must declare `stressFixture` for the tested control value; browser perf tests must use `getToolcraftPerformanceStressValue(appPerformance, scenarioId)` so heavy-case tests cannot use toy values. When the tested control is not itself the whole heavy source, declare `workloadFixture` and apply it first with `getToolcraftPerformanceWorkloadValue` or `applyToolcraftPerformanceWorkloadFixture`; this is the app baseline such as large media, long text, many items, or high render scale, and it must be paired with the measured `stressFixture`. Numeric maximums, density, item counts, canvas/media size, and combined heavy states declare `loadProfile` with `hardLimit`, `smoothTarget`, and `smoothTargetRatio`; try the hard limit first, and lower the guaranteed smooth target only in 10 percent steps with failed-measurement and optimization evidence. Ranges above `smoothTarget` are experimental, not silently guaranteed. Media import and image-processing workloads use `kind: "media"` fixtures at least `1920x1080`-equivalent, and heavy pixel/media Canvas 2D must evaluate WebGL/WebGPU with measured evidence before staying on CPU.
31
+ 17. Performance workload, render scale, live slider responsiveness, GPU evaluation, and optimization evidence rules live in `docs/toolcraft/core/performance.md` plus `docs/toolcraft/performance.md`.
32
32
  18. Custom renderer apps declare a Render Pipeline Inventory in typed `rendererPipeline`: render passes, cache keys, execution location, preview/export quality, and interaction invalidation.
33
33
  19. Classify every implementation pass with a verification tier before editing. Use targeted checks for incremental edits and the full final gate only for final delivery, exports, or architecture/runtime/template changes.
34
34
 
@@ -47,6 +47,7 @@ This project includes Toolcraft source code governed by the Toolcraft Designer L
47
47
  Use this `AGENTS.md` as the entry contract. Use local docs for detail; the app must remain buildable without the website.
48
48
 
49
49
  - `docs/toolcraft/workflow.md` — required preflight, task routing, worklog gate, and verification routing.
50
+ - `docs/toolcraft/core/runtime-boundary.md`, `docs/toolcraft/core/setup-export.md`, `docs/toolcraft/core/control-selection.md`, `docs/toolcraft/core/layout.md`, `docs/toolcraft/core/media-upload.md`, `docs/toolcraft/core/timeline-animation.md`, `docs/toolcraft/core/performance.md`, `docs/toolcraft/core/reference-study.md` — focused core modules routed by `workflow.md`.
50
51
  - `docs/toolcraft/assembly-workflow.md` — runtime assembly, canvas output, and reference clone path.
51
52
  - `docs/toolcraft/decision-contract.md` — rule ids, levels, and enforcement expectations.
52
53
  - `docs/toolcraft/schema-reference.md` — schema authoring rules for `src/app/app-schema.ts`.
@@ -6,6 +6,21 @@ Use `../../AGENTS.md` as the entry contract, then read `workflow.md` before plan
6
6
 
7
7
  The starter app itself is intentionally neutral. It should show the Toolcraft canvas/upload/toolbar baseline only until the product schema is authored. Demo controls, prompt inputs, layers, and timeline belong in tests/docs or in a real generated product that needs them.
8
8
 
9
+ ## Core modules
10
+
11
+ `workflow.md` routes agents to these focused modules. Read every module listed for the task type; each module is intended to be small enough to read fully without relying on truncated terminal output.
12
+
13
+ - `core/runtime-boundary.md` — Toolcraft shell, allowed extension points, canvas boundary, and generated-app source boundary.
14
+ - `core/setup-export.md` — required Setup, canvas sizing, render scale, Timeline switch, Background, Image Export, Video Export, and sticky export actions.
15
+ - `core/control-selection.md` — built-in control fit, exact owners, compound controls, actions, collection actions, vector ownership, and custom control gate.
16
+ - `core/layout.md` — sections, dependency cohesion, headers, reset, spacing, dividers, labels, inline rows, actions layout, colors, select, and segmented fit.
17
+ - `core/media-upload.md` — file/image upload, multi-upload, sorting, transform actions, canvas source images, default assets, and source material behavior.
18
+ - `core/timeline-animation.md` — animation intent, timeline requirement, compact/extended timeline, seamless forward loops, duration changes, keyframes, and video timing.
19
+ - `core/performance.md` — verification triggers, workload fixtures, media/pixel workloads, render scale, live slider responsiveness, renderer pipeline inventory, and optimization evidence.
20
+ - `core/reference-study.md` — reference-runtime clone, feature inventory, reference study, Figma source, video references, acceptance mapping, and worklog evidence.
21
+
22
+ The broad docs below remain compatibility and topic references. They do not replace `workflow.md` routing or the `core/*` modules.
23
+
9
24
  1. `workflow.md` — required preflight, task routing, worklog gate, and verification routing.
10
25
  2. `assembly-workflow.md` — how the app must be assembled.
11
26
  3. `decision-contract.md` — hard rules, defaults, heuristics, and escape hatches.
@@ -1,5 +1,7 @@
1
1
  # Acceptance Testing
2
2
 
3
+ > Reading route: start with `workflow.md`. Core generated-app rules live in `core/*`; this file is a focused acceptance reference for the topic below.
4
+
3
5
  Every visible product entity must prove it works. A control is not accepted because it renders; it is accepted only when tests prove user interaction changes runtime state and the final product output, command side effect, timeline frame, layer result, media lifecycle, or canvas viewport.
4
6
 
5
7
  ## Required Files