@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/README.md
CHANGED
|
@@ -19,7 +19,7 @@ tools for client work.
|
|
|
19
19
|
npx @pixel-point/toolcraft create
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
The create command uses the current directory when no target directory is passed, prompts for missing project values in an interactive terminal, generates the app, runs
|
|
22
|
+
The create command uses the current directory when no target directory is passed, prompts for missing project values in an interactive terminal, generates the app, runs dependency installation with the package manager that launched the CLI, then prints the command to start the dev server.
|
|
23
23
|
|
|
24
24
|
After dependencies are installed, Toolcraft installs the required workflow skills
|
|
25
25
|
in a batch through the `skills` CLI. The skill installer uses the same agent,
|
|
@@ -30,7 +30,7 @@ Example:
|
|
|
30
30
|
```bash
|
|
31
31
|
npx @pixel-point/toolcraft create my-ascii-tool
|
|
32
32
|
cd my-ascii-tool
|
|
33
|
-
|
|
33
|
+
npm run dev
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Then open the generated folder in Codex, Claude Code, Cursor, or another AI
|
|
@@ -46,6 +46,8 @@ Scripted usage:
|
|
|
46
46
|
npx @pixel-point/toolcraft create my-toolcraft-app --name my-toolcraft-app --yes --force
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Toolcraft detects `npm` and `pnpm` from the package manager user agent. For example, `npx @pixel-point/toolcraft create` generates npm-flavored setup commands, while `pnpm dlx @pixel-point/toolcraft create` generates pnpm-flavored setup commands.
|
|
50
|
+
|
|
49
51
|
Install Toolcraft skills to specific agents or locations:
|
|
50
52
|
|
|
51
53
|
```bash
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -4,7 +4,6 @@ import path from "node:path";
|
|
|
4
4
|
import readline from "node:readline/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
-
import spawn from "cross-spawn";
|
|
8
7
|
import {
|
|
9
8
|
cancel,
|
|
10
9
|
confirm,
|
|
@@ -16,8 +15,14 @@ import {
|
|
|
16
15
|
text,
|
|
17
16
|
} from "@clack/prompts";
|
|
18
17
|
|
|
18
|
+
import { runCommand } from "./command-runner.mjs";
|
|
19
19
|
import { pathExists } from "./copy-recursive.mjs";
|
|
20
|
+
import { installGeneratedAppDependencies } from "./dependency-install.mjs";
|
|
20
21
|
import { generateToolcraft } from "./generate.mjs";
|
|
22
|
+
import {
|
|
23
|
+
createRunScriptCommand,
|
|
24
|
+
detectPackageManager,
|
|
25
|
+
} from "./package-manager.mjs";
|
|
21
26
|
import { sanitizePackageName } from "./package-json.mjs";
|
|
22
27
|
|
|
23
28
|
const DEFAULT_PROJECT_NAME = "my-toolcraft-app";
|
|
@@ -355,31 +360,6 @@ export async function resolveCreateOptions(parsedOptions, context = {}) {
|
|
|
355
360
|
};
|
|
356
361
|
}
|
|
357
362
|
|
|
358
|
-
function runCommand(command, args, options = {}, context = {}) {
|
|
359
|
-
if (context.runCommand) {
|
|
360
|
-
return context.runCommand(command, args, options);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return new Promise((resolve, reject) => {
|
|
364
|
-
const child = spawn(command, args, {
|
|
365
|
-
cwd: options.cwd,
|
|
366
|
-
env: options.env,
|
|
367
|
-
shell: options.shell ?? false,
|
|
368
|
-
stdio: options.stdio ?? "inherit",
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
child.on("error", reject);
|
|
372
|
-
child.on("exit", (exitCode) => {
|
|
373
|
-
if (exitCode === 0) {
|
|
374
|
-
resolve();
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
reject(new Error(`${command} ${args.join(" ")} failed with exit code ${exitCode}.`));
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
363
|
function getSkillsCliPath(context = {}) {
|
|
384
364
|
if (context.skillsCliPath) {
|
|
385
365
|
return context.skillsCliPath;
|
|
@@ -459,7 +439,7 @@ function writeFinalSummary(stdout, result, createOptions) {
|
|
|
459
439
|
: result.relativeTargetDir;
|
|
460
440
|
writeLine(stdout, ` cd ${displayTargetDir}`);
|
|
461
441
|
}
|
|
462
|
-
writeLine(stdout,
|
|
442
|
+
writeLine(stdout, ` ${createRunScriptCommand(createOptions.packageManager, "dev")}`);
|
|
463
443
|
}
|
|
464
444
|
|
|
465
445
|
export async function runCreateCommand(parsedOptions, context = {}) {
|
|
@@ -471,6 +451,7 @@ export async function runCreateCommand(parsedOptions, context = {}) {
|
|
|
471
451
|
intro("Create Toolcraft app");
|
|
472
452
|
}
|
|
473
453
|
|
|
454
|
+
const packageManager = detectPackageManager(context.env ?? process.env);
|
|
474
455
|
const createOptions = await resolveCreateOptions(parsedOptions, context);
|
|
475
456
|
|
|
476
457
|
const result = await runWithSpinner(context, "Creating Toolcraft app...", "Created app", () =>
|
|
@@ -478,27 +459,19 @@ export async function runCreateCommand(parsedOptions, context = {}) {
|
|
|
478
459
|
cwd,
|
|
479
460
|
force: createOptions.force,
|
|
480
461
|
name: createOptions.name,
|
|
462
|
+
packageManager,
|
|
481
463
|
targetDir: createOptions.targetDir,
|
|
482
464
|
}),
|
|
483
465
|
);
|
|
484
466
|
|
|
485
467
|
if (createOptions.install) {
|
|
486
|
-
|
|
487
|
-
writeLine(stdout, "Installing dependencies with pnpm...");
|
|
488
|
-
await runCommand(
|
|
489
|
-
"pnpm",
|
|
490
|
-
["install"],
|
|
491
|
-
{
|
|
492
|
-
cwd: result.targetDir,
|
|
493
|
-
env: {
|
|
494
|
-
...process.env,
|
|
495
|
-
...(context.env ?? {}),
|
|
496
|
-
},
|
|
497
|
-
shell: false,
|
|
498
|
-
stdio: "inherit",
|
|
499
|
-
},
|
|
468
|
+
await installGeneratedAppDependencies({
|
|
500
469
|
context,
|
|
501
|
-
|
|
470
|
+
packageManager,
|
|
471
|
+
result,
|
|
472
|
+
skillsRequested: createOptions.skills,
|
|
473
|
+
stdout,
|
|
474
|
+
});
|
|
502
475
|
}
|
|
503
476
|
|
|
504
477
|
if (createOptions.skills) {
|
|
@@ -510,12 +483,12 @@ export async function runCreateCommand(parsedOptions, context = {}) {
|
|
|
510
483
|
if (usingClack) {
|
|
511
484
|
const nextSteps = [
|
|
512
485
|
...(result.targetDir === cwd ? [] : [`cd ${result.relativeTargetDir}`]),
|
|
513
|
-
"
|
|
486
|
+
createRunScriptCommand(packageManager, "dev"),
|
|
514
487
|
].join("\n");
|
|
515
488
|
note(nextSteps, "Next steps");
|
|
516
489
|
outro(`Created ${result.packageName}`);
|
|
517
490
|
} else {
|
|
518
|
-
writeFinalSummary(stdout, result, { ...createOptions, cwd });
|
|
491
|
+
writeFinalSummary(stdout, result, { ...createOptions, cwd, packageManager });
|
|
519
492
|
}
|
|
520
493
|
|
|
521
494
|
return result;
|
|
@@ -550,7 +523,7 @@ Options:
|
|
|
550
523
|
--copy Copy skills instead of using the skills CLI default link behavior.
|
|
551
524
|
--all Install Toolcraft skills to all supported agents without skills prompts.
|
|
552
525
|
--no-skills Skip Toolcraft skills installation.
|
|
553
|
-
--no-install Skip automatic
|
|
526
|
+
--no-install Skip automatic dependency installation. Intended for local CLI tests and automation.
|
|
554
527
|
--help, -h Show this help message.`;
|
|
555
528
|
}
|
|
556
529
|
|
package/src/cli.test.mjs
CHANGED
|
@@ -206,7 +206,7 @@ describe("runToolcraftCli", () => {
|
|
|
206
206
|
assert.ok(await fs.stat(path.join(tempRoot, "src/app/app-schema.ts")));
|
|
207
207
|
assert.match(stdout.text, new RegExp(`Created ${path.basename(tempRoot).toLowerCase()}`));
|
|
208
208
|
assert.doesNotMatch(stdout.text, /cd /);
|
|
209
|
-
assert.match(stdout.text, /
|
|
209
|
+
assert.match(stdout.text, /npm run dev/);
|
|
210
210
|
assert.equal(stderr.text, "");
|
|
211
211
|
});
|
|
212
212
|
|
|
@@ -239,12 +239,12 @@ describe("runToolcraftCli", () => {
|
|
|
239
239
|
assert.ok(await fs.stat(path.join(tempRoot, "generated-app/src/app/app-schema.ts")));
|
|
240
240
|
assert.match(stdout.text, /Created generated-app/);
|
|
241
241
|
assert.match(stdout.text, /cd generated-app/);
|
|
242
|
-
assert.match(stdout.text, /
|
|
242
|
+
assert.match(stdout.text, /npm run dev/);
|
|
243
243
|
assert.doesNotMatch(stdout.text, /pnpm verify:final/);
|
|
244
244
|
assert.equal(stderr.text, "");
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
-
it("runs
|
|
247
|
+
it("runs npm install and Toolcraft skills install when launched through npx", async () => {
|
|
248
248
|
const tempRoot = await createTempRoot();
|
|
249
249
|
const stdout = createWritableCapture();
|
|
250
250
|
const installs = [];
|
|
@@ -253,7 +253,9 @@ describe("runToolcraftCli", () => {
|
|
|
253
253
|
|
|
254
254
|
await runToolcraftCli(["create", "install-app", "--name", "Install App", "--yes", "--force"], {
|
|
255
255
|
cwd: tempRoot,
|
|
256
|
-
env: {
|
|
256
|
+
env: {
|
|
257
|
+
npm_config_user_agent: "npm/10.9.4 node/v24.4.1 darwin arm64",
|
|
258
|
+
},
|
|
257
259
|
stdout,
|
|
258
260
|
skillsCliPath,
|
|
259
261
|
throwOnError: true,
|
|
@@ -266,7 +268,7 @@ describe("runToolcraftCli", () => {
|
|
|
266
268
|
assert.deepEqual(installs, [
|
|
267
269
|
{
|
|
268
270
|
args: ["install"],
|
|
269
|
-
command: "
|
|
271
|
+
command: "npm",
|
|
270
272
|
cwd: path.join(tempRoot, "install-app"),
|
|
271
273
|
shell: false,
|
|
272
274
|
},
|
|
@@ -277,11 +279,144 @@ describe("runToolcraftCli", () => {
|
|
|
277
279
|
shell: false,
|
|
278
280
|
},
|
|
279
281
|
]);
|
|
280
|
-
assert.match(stdout.text, /Installing dependencies with
|
|
282
|
+
assert.match(stdout.text, /Installing dependencies with npm/);
|
|
281
283
|
assert.match(stdout.text, /Installing Toolcraft skills/);
|
|
284
|
+
assert.match(stdout.text, /npm run dev/);
|
|
285
|
+
|
|
286
|
+
const packageJson = JSON.parse(
|
|
287
|
+
await fs.readFile(path.join(tempRoot, "install-app/package.json"), "utf8"),
|
|
288
|
+
);
|
|
289
|
+
assert.equal(
|
|
290
|
+
packageJson.scripts["verify:final"],
|
|
291
|
+
"npm run ai:check && npm run test && npm run build && npm run test:browser",
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const playwrightConfigSource = await fs.readFile(
|
|
295
|
+
path.join(tempRoot, "install-app/playwright.config.ts"),
|
|
296
|
+
"utf8",
|
|
297
|
+
);
|
|
298
|
+
assert.match(playwrightConfigSource, /npm exec -- vite dev/);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("runs pnpm install when launched through pnpm", async () => {
|
|
302
|
+
const tempRoot = await createTempRoot();
|
|
303
|
+
const stdout = createWritableCapture();
|
|
304
|
+
const installs = [];
|
|
305
|
+
|
|
306
|
+
await runToolcraftCli(
|
|
307
|
+
["create", "pnpm-app", "--name", "Pnpm App", "--yes", "--force", "--no-skills"],
|
|
308
|
+
{
|
|
309
|
+
cwd: tempRoot,
|
|
310
|
+
env: {
|
|
311
|
+
npm_config_user_agent: "pnpm/10.0.0 npm/? node/v24.4.1 darwin arm64",
|
|
312
|
+
},
|
|
313
|
+
stdout,
|
|
314
|
+
throwOnError: true,
|
|
315
|
+
async runCommand(command, args, options) {
|
|
316
|
+
installs.push({ args, command, cwd: options.cwd, shell: options.shell });
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
assert.deepEqual(installs, [
|
|
322
|
+
{
|
|
323
|
+
args: ["install"],
|
|
324
|
+
command: "pnpm",
|
|
325
|
+
cwd: path.join(tempRoot, "pnpm-app"),
|
|
326
|
+
shell: false,
|
|
327
|
+
},
|
|
328
|
+
]);
|
|
329
|
+
assert.match(stdout.text, /Installing dependencies with pnpm/);
|
|
282
330
|
assert.match(stdout.text, /pnpm dev/);
|
|
283
331
|
});
|
|
284
332
|
|
|
333
|
+
it("reports recovery steps when dependency installation command is missing", async () => {
|
|
334
|
+
const tempRoot = await createTempRoot();
|
|
335
|
+
const stdout = createWritableCapture();
|
|
336
|
+
const stderr = createWritableCapture();
|
|
337
|
+
const commands = [];
|
|
338
|
+
let exitCode;
|
|
339
|
+
|
|
340
|
+
const result = await runToolcraftCli(
|
|
341
|
+
["create", "missing-npm-app", "--name", "Missing Npm App", "--yes", "--force"],
|
|
342
|
+
{
|
|
343
|
+
cwd: tempRoot,
|
|
344
|
+
env: {
|
|
345
|
+
npm_config_user_agent: "npm/10.9.4 node/v24.4.1 darwin arm64",
|
|
346
|
+
},
|
|
347
|
+
stderr,
|
|
348
|
+
stdout,
|
|
349
|
+
setExitCode(value) {
|
|
350
|
+
exitCode = value;
|
|
351
|
+
},
|
|
352
|
+
async runCommand(command, args, options) {
|
|
353
|
+
commands.push({ args, command, cwd: options.cwd });
|
|
354
|
+
const error = new Error("spawn npm ENOENT");
|
|
355
|
+
error.code = "ENOENT";
|
|
356
|
+
throw error;
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
assert.equal(result.ok, false);
|
|
362
|
+
assert.equal(exitCode, 1);
|
|
363
|
+
assert.deepEqual(commands, [
|
|
364
|
+
{
|
|
365
|
+
args: ["install"],
|
|
366
|
+
command: "npm",
|
|
367
|
+
cwd: path.join(tempRoot, "missing-npm-app"),
|
|
368
|
+
},
|
|
369
|
+
]);
|
|
370
|
+
assert.ok(await fs.stat(path.join(tempRoot, "missing-npm-app/src/app/app-schema.ts")));
|
|
371
|
+
assert.match(stderr.text, /Toolcraft app created at .*missing-npm-app, but setup did not finish/);
|
|
372
|
+
assert.match(stderr.text, /npm was not found in PATH/);
|
|
373
|
+
assert.match(stderr.text, /cd .*missing-npm-app/);
|
|
374
|
+
assert.match(stderr.text, /npm install/);
|
|
375
|
+
assert.match(stderr.text, /Toolcraft skills were not installed/);
|
|
376
|
+
assert.match(stderr.text, /npm run dev/);
|
|
377
|
+
assert.doesNotMatch(stdout.text, /Installing Toolcraft skills/);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("does not mention skipped skills when dependency installation fails with --no-skills", async () => {
|
|
381
|
+
const tempRoot = await createTempRoot();
|
|
382
|
+
const stdout = createWritableCapture();
|
|
383
|
+
const stderr = createWritableCapture();
|
|
384
|
+
let exitCode;
|
|
385
|
+
|
|
386
|
+
const result = await runToolcraftCli(
|
|
387
|
+
[
|
|
388
|
+
"create",
|
|
389
|
+
"missing-npm-no-skills-app",
|
|
390
|
+
"--name",
|
|
391
|
+
"Missing Npm No Skills App",
|
|
392
|
+
"--yes",
|
|
393
|
+
"--force",
|
|
394
|
+
"--no-skills",
|
|
395
|
+
],
|
|
396
|
+
{
|
|
397
|
+
cwd: tempRoot,
|
|
398
|
+
env: {
|
|
399
|
+
npm_config_user_agent: "npm/10.9.4 node/v24.4.1 darwin arm64",
|
|
400
|
+
},
|
|
401
|
+
stderr,
|
|
402
|
+
stdout,
|
|
403
|
+
setExitCode(value) {
|
|
404
|
+
exitCode = value;
|
|
405
|
+
},
|
|
406
|
+
async runCommand() {
|
|
407
|
+
const error = new Error("spawn npm ENOENT");
|
|
408
|
+
error.code = "ENOENT";
|
|
409
|
+
throw error;
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
assert.equal(result.ok, false);
|
|
415
|
+
assert.equal(exitCode, 1);
|
|
416
|
+
assert.match(stderr.text, /npm was not found in PATH/);
|
|
417
|
+
assert.doesNotMatch(stderr.text, /Toolcraft skills were not installed/);
|
|
418
|
+
});
|
|
419
|
+
|
|
285
420
|
it("forwards skills add compatible options", async () => {
|
|
286
421
|
const tempRoot = await createTempRoot();
|
|
287
422
|
const skillsCommands = [];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import spawn from "cross-spawn";
|
|
2
|
+
|
|
3
|
+
export class CommandRunError extends Error {
|
|
4
|
+
constructor(command, args, options, details = {}) {
|
|
5
|
+
super(details.message);
|
|
6
|
+
this.name = "CommandRunError";
|
|
7
|
+
this.args = args;
|
|
8
|
+
this.code = details.code;
|
|
9
|
+
this.command = command;
|
|
10
|
+
this.cwd = options.cwd;
|
|
11
|
+
this.exitCode = details.exitCode;
|
|
12
|
+
this.signal = details.signal;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getErrorCode(error) {
|
|
17
|
+
return error && typeof error === "object" ? error.code : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function runCommand(command, args, options = {}, context = {}) {
|
|
21
|
+
if (context.runCommand) {
|
|
22
|
+
return context.runCommand(command, args, options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
let settled = false;
|
|
27
|
+
const settle = (callback) => {
|
|
28
|
+
if (settled) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
settled = true;
|
|
33
|
+
callback();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const child = spawn(command, args, {
|
|
37
|
+
cwd: options.cwd,
|
|
38
|
+
env: options.env,
|
|
39
|
+
shell: options.shell ?? false,
|
|
40
|
+
stdio: options.stdio ?? "inherit",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
child.on("error", (error) => {
|
|
44
|
+
settle(() =>
|
|
45
|
+
reject(
|
|
46
|
+
new CommandRunError(command, args, options, {
|
|
47
|
+
code: error.code,
|
|
48
|
+
message:
|
|
49
|
+
error.code === "ENOENT" ? `Command not found: ${command}` : error.message,
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
child.on("close", (exitCode, signal) => {
|
|
55
|
+
settle(() => {
|
|
56
|
+
if (exitCode === 0) {
|
|
57
|
+
resolve();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
reject(
|
|
62
|
+
new CommandRunError(command, args, options, {
|
|
63
|
+
exitCode,
|
|
64
|
+
message: `${command} ${args.join(" ")} failed with exit code ${exitCode}.`,
|
|
65
|
+
signal,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getErrorCode, runCommand } from "./command-runner.mjs";
|
|
2
|
+
import {
|
|
3
|
+
createInstallCommand,
|
|
4
|
+
createInstallHelp,
|
|
5
|
+
createRunScriptCommand,
|
|
6
|
+
normalizePackageManager,
|
|
7
|
+
} from "./package-manager.mjs";
|
|
8
|
+
|
|
9
|
+
function writeLine(stream, message = "") {
|
|
10
|
+
stream.write(`${message}\n`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createDependencyInstallError(
|
|
14
|
+
error,
|
|
15
|
+
result,
|
|
16
|
+
packageManager,
|
|
17
|
+
{ skillsRequested = true } = {},
|
|
18
|
+
) {
|
|
19
|
+
const normalizedPackageManager = normalizePackageManager(packageManager);
|
|
20
|
+
const installCommand = `${normalizedPackageManager} install`;
|
|
21
|
+
const devCommand = createRunScriptCommand(normalizedPackageManager, "dev");
|
|
22
|
+
const commandMissing = getErrorCode(error) === "ENOENT";
|
|
23
|
+
const lines = [`Toolcraft app created at ${result.targetDir}, but setup did not finish.`, ""];
|
|
24
|
+
|
|
25
|
+
if (commandMissing) {
|
|
26
|
+
lines.push(
|
|
27
|
+
`Dependency installation failed because ${normalizedPackageManager} was not found in PATH.`,
|
|
28
|
+
"",
|
|
29
|
+
`Install ${normalizedPackageManager}:`,
|
|
30
|
+
"",
|
|
31
|
+
...createInstallHelp(normalizedPackageManager),
|
|
32
|
+
);
|
|
33
|
+
} else {
|
|
34
|
+
lines.push(
|
|
35
|
+
"Dependency installation failed while running:",
|
|
36
|
+
"",
|
|
37
|
+
` ${installCommand}`,
|
|
38
|
+
"",
|
|
39
|
+
`Original error: ${error instanceof Error ? error.message : String(error)}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
lines.push("", "Then finish setup:", "", ` cd ${result.targetDir}`, ` ${installCommand}`);
|
|
44
|
+
|
|
45
|
+
if (skillsRequested) {
|
|
46
|
+
lines.push("", "Toolcraft skills were not installed because dependency installation failed.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lines.push("", "After setup, start the app with:", "", ` ${devCommand}`);
|
|
50
|
+
|
|
51
|
+
return new Error(lines.join("\n"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function installGeneratedAppDependencies({
|
|
55
|
+
context = {},
|
|
56
|
+
packageManager,
|
|
57
|
+
result,
|
|
58
|
+
skillsRequested = true,
|
|
59
|
+
stdout,
|
|
60
|
+
}) {
|
|
61
|
+
const normalizedPackageManager = normalizePackageManager(packageManager);
|
|
62
|
+
const installCommand = createInstallCommand(normalizedPackageManager);
|
|
63
|
+
|
|
64
|
+
writeLine(stdout, "");
|
|
65
|
+
writeLine(stdout, `Installing dependencies with ${normalizedPackageManager}...`);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await runCommand(
|
|
69
|
+
installCommand.command,
|
|
70
|
+
installCommand.args,
|
|
71
|
+
{
|
|
72
|
+
cwd: result.targetDir,
|
|
73
|
+
env: {
|
|
74
|
+
...process.env,
|
|
75
|
+
...(context.env ?? {}),
|
|
76
|
+
},
|
|
77
|
+
shell: false,
|
|
78
|
+
stdio: "inherit",
|
|
79
|
+
},
|
|
80
|
+
context,
|
|
81
|
+
);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw createDependencyInstallError(error, result, normalizedPackageManager, {
|
|
84
|
+
skillsRequested,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/generate.mjs
CHANGED
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
pathExists,
|
|
12
12
|
removeDirectory,
|
|
13
13
|
} from "./copy-recursive.mjs";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_PACKAGE_MANAGER,
|
|
16
|
+
normalizePackageManager,
|
|
17
|
+
replaceGeneratedCommandReferences,
|
|
18
|
+
} from "./package-manager.mjs";
|
|
14
19
|
import {
|
|
15
20
|
createGeneratedPackageJson,
|
|
16
21
|
createGeneratedTsConfig,
|
|
@@ -50,8 +55,29 @@ function createRelativePath(fromDir, toDir) {
|
|
|
50
55
|
return relativePath === "" ? "." : relativePath;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
function
|
|
54
|
-
|
|
58
|
+
function shouldReplacePackageManagerCommands(filePath, targetDir) {
|
|
59
|
+
const relativePath = path.relative(targetDir, filePath).split(path.sep).join("/");
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
relativePath === "AGENTS.md" ||
|
|
63
|
+
relativePath === "playwright.config.ts" ||
|
|
64
|
+
relativePath === "scripts/check-toolcraft-docs.mjs" ||
|
|
65
|
+
/^docs\/toolcraft\/.+\.md$/.test(relativePath)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function rewriteGeneratedAppText(
|
|
70
|
+
source,
|
|
71
|
+
filePath,
|
|
72
|
+
{ packageManager = DEFAULT_PACKAGE_MANAGER, targetDir } = {},
|
|
73
|
+
) {
|
|
74
|
+
const rewrittenSource = rewriteGeneratedText(source);
|
|
75
|
+
const commandSource =
|
|
76
|
+
targetDir && shouldReplacePackageManagerCommands(filePath, targetDir)
|
|
77
|
+
? replaceGeneratedCommandReferences(rewrittenSource, packageManager)
|
|
78
|
+
: rewrittenSource;
|
|
79
|
+
|
|
80
|
+
return commandSource
|
|
55
81
|
.replaceAll("starterAcceptance", "appAcceptance")
|
|
56
82
|
.replaceAll("starter-acceptance", "app-acceptance")
|
|
57
83
|
.replaceAll("starterProductReadiness", "appProductReadiness")
|
|
@@ -218,6 +244,7 @@ export function getDefaultSourcePaths(repoRoot = REPO_ROOT) {
|
|
|
218
244
|
export async function generateToolcraft(options = {}) {
|
|
219
245
|
const cwd = options.cwd ?? process.cwd();
|
|
220
246
|
const targetDir = path.resolve(cwd, options.targetDir ?? ".");
|
|
247
|
+
const packageManager = normalizePackageManager(options.packageManager ?? DEFAULT_PACKAGE_MANAGER);
|
|
221
248
|
const sourcePaths = options.sourcePaths ?? getDefaultSourcePaths(options.repoRoot ?? REPO_ROOT);
|
|
222
249
|
|
|
223
250
|
await assertDirectory(sourcePaths.starterDir, "Starter app");
|
|
@@ -226,6 +253,7 @@ export async function generateToolcraft(options = {}) {
|
|
|
226
253
|
const starterPackageJson = await readJson(path.join(sourcePaths.starterDir, "package.json"));
|
|
227
254
|
const packageJson = createGeneratedPackageJson({
|
|
228
255
|
name: options.name ?? path.basename(targetDir),
|
|
256
|
+
packageManager,
|
|
229
257
|
starterPackageJson,
|
|
230
258
|
});
|
|
231
259
|
const projectTitle = normalizeProjectTitle(packageJson.name);
|
|
@@ -243,8 +271,8 @@ export async function generateToolcraft(options = {}) {
|
|
|
243
271
|
await copyDirectory(sourcePaths.toolcraftSrc, path.join(toolcraftRoot, "runtime"));
|
|
244
272
|
await removeToolcraftTestFiles(toolcraftRoot);
|
|
245
273
|
|
|
246
|
-
const changedFiles = await rewriteTextFiles(targetDir, (source) =>
|
|
247
|
-
rewriteGeneratedAppText(source),
|
|
274
|
+
const changedFiles = await rewriteTextFiles(targetDir, (source, filePath) =>
|
|
275
|
+
rewriteGeneratedAppText(source, filePath, { packageManager, targetDir }),
|
|
248
276
|
);
|
|
249
277
|
await writeGeneratedProjectTitle(targetDir, projectTitle);
|
|
250
278
|
await writeToolcraftIntegrityManifest(toolcraftRoot);
|