@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.
- package/README.md +4 -2
- package/package.json +1 -1
- package/src/cli.mjs +18 -45
- package/src/cli.test.mjs +141 -6
- package/src/command-runner.mjs +71 -0
- package/src/dependency-install.mjs +87 -0
- package/src/generate.mjs +32 -4
- package/src/generate.test.mjs +116 -15
- package/src/package-json.mjs +11 -2
- package/src/package-json.test.mjs +27 -0
- package/src/package-manager.mjs +123 -0
- package/src/package-manager.test.mjs +80 -0
- package/templates/runtime/contracts/component-contracts.test.ts +1 -1
- package/templates/runtime/contracts/component-contracts.ts +1 -1
- package/templates/starter/AGENTS.md +4 -3
- package/templates/starter/docs/toolcraft/README.md +15 -0
- package/templates/starter/docs/toolcraft/acceptance-testing.md +2 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +35 -171
- package/templates/starter/docs/toolcraft/component-rules.md +12 -188
- package/templates/starter/docs/toolcraft/core/control-selection.md +93 -0
- package/templates/starter/docs/toolcraft/core/layout.md +104 -0
- package/templates/starter/docs/toolcraft/core/media-upload.md +85 -0
- package/templates/starter/docs/toolcraft/core/performance.md +83 -0
- package/templates/starter/docs/toolcraft/core/reference-study.md +115 -0
- package/templates/starter/docs/toolcraft/core/runtime-boundary.md +53 -0
- package/templates/starter/docs/toolcraft/core/setup-export.md +86 -0
- package/templates/starter/docs/toolcraft/core/timeline-animation.md +67 -0
- package/templates/starter/docs/toolcraft/custom-controls.md +2 -0
- package/templates/starter/docs/toolcraft/performance.md +2 -0
- package/templates/starter/docs/toolcraft/renderer-technique.md +2 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +117 -367
- package/templates/starter/docs/toolcraft/workflow.md +12 -10
- package/templates/starter/package.json +1 -0
- package/templates/starter/scripts/check-toolcraft-docs.mjs +28 -6
- package/templates/starter/scripts/run-vite-on-free-port.mjs +25 -6
- package/templates/starter/src/app/starter-acceptance.test.ts +24 -15
- package/templates/starter/src/app/starter-performance.test.ts +17 -7
package/src/generate.test.mjs
CHANGED
|
@@ -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"], "
|
|
105
|
-
assert.equal(packageJson.scripts["verify:ui"], "
|
|
106
|
-
assert.equal(packageJson.scripts["verify:perf"], "
|
|
107
|
-
assert.equal(packageJson.scripts["verify:perf:playwright"], "
|
|
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
|
-
"
|
|
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, /
|
|
236
|
-
assert.match(agentsSource, /
|
|
237
|
-
assert.match(agentsSource, /
|
|
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, /
|
|
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(
|
|
280
|
-
assert.match(
|
|
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(
|
|
283
|
-
assert.match(
|
|
284
|
-
assert.match(
|
|
285
|
-
assert.match(
|
|
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);
|
package/src/package-json.mjs
CHANGED
|
@@ -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({
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
28
|
-
14.
|
|
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.
|
|
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
|