@pixel-point/toolcraft 0.0.2 → 0.0.3
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 +15 -2
- package/package.json +6 -1
- package/src/cli.mjs +226 -17
- package/src/cli.test.mjs +127 -2
- package/templates/starter/scripts/check-ai-skills.mjs +39 -4
- package/toolcraft-skills/brainstorming/SKILL.md +28 -0
- package/toolcraft-skills/browser/SKILL.md +25 -0
- package/toolcraft-skills/figma/SKILL.md +24 -0
- package/toolcraft-skills/figma-implement-design/SKILL.md +24 -0
- package/toolcraft-skills/systematic-debugging/SKILL.md +24 -0
- package/toolcraft-skills/writing-plans/SKILL.md +26 -0
package/README.md
CHANGED
|
@@ -8,6 +8,10 @@ npx @pixel-point/toolcraft create
|
|
|
8
8
|
|
|
9
9
|
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 `pnpm install`, then prints the command to start the dev server.
|
|
10
10
|
|
|
11
|
+
After dependencies are installed, Toolcraft installs the required workflow skills
|
|
12
|
+
in a batch through the `skills` CLI. The skill installer uses the same agent,
|
|
13
|
+
scope, and installation prompts as `npx skills add`.
|
|
14
|
+
|
|
11
15
|
## License
|
|
12
16
|
|
|
13
17
|
Toolcraft is distributed under the Toolcraft Designer License in `LICENSE.md`.
|
|
@@ -23,12 +27,21 @@ Scripted usage:
|
|
|
23
27
|
npx @pixel-point/toolcraft create my-toolcraft-app --name my-toolcraft-app --yes --force
|
|
24
28
|
```
|
|
25
29
|
|
|
30
|
+
Install Toolcraft skills to specific agents or locations:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @pixel-point/toolcraft create my-toolcraft-app --agent codex --agent claude-code
|
|
34
|
+
npx @pixel-point/toolcraft create my-toolcraft-app --agent codex --global
|
|
35
|
+
npx @pixel-point/toolcraft create my-toolcraft-app --all
|
|
36
|
+
npx @pixel-point/toolcraft create my-toolcraft-app --no-skills
|
|
37
|
+
```
|
|
38
|
+
|
|
26
39
|
Local source test without publishing:
|
|
27
40
|
|
|
28
41
|
```bash
|
|
29
42
|
mkdir -p /tmp/toolcraft-local-cli-test
|
|
30
43
|
cd /tmp/toolcraft-local-cli-test
|
|
31
|
-
node /Users/alex/Projects/primeui-v2/cli/bin/toolcraft.mjs --name local-cli-test --yes --force --no-install
|
|
44
|
+
node /Users/alex/Projects/primeui-v2/cli/bin/toolcraft.mjs --name local-cli-test --yes --force --no-install --no-skills
|
|
32
45
|
```
|
|
33
46
|
|
|
34
47
|
Local tarball test, matching the published package layout:
|
|
@@ -37,5 +50,5 @@ Local tarball test, matching the published package layout:
|
|
|
37
50
|
cd cli
|
|
38
51
|
npm pack --pack-destination /tmp
|
|
39
52
|
cd ..
|
|
40
|
-
TOOLCRAFT_SKIP_INSTALL=1 npm exec --package /tmp/pixel-point-toolcraft-0.0.
|
|
53
|
+
TOOLCRAFT_SKIP_INSTALL=1 TOOLCRAFT_SKIP_SKILLS=1 npm exec --package /tmp/pixel-point-toolcraft-0.0.2.tgz -- toolcraft create /tmp/toolcraft-pack-exec-test --name pack-exec-test --yes --force
|
|
41
54
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixel-point/toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
"scripts",
|
|
15
15
|
"src",
|
|
16
16
|
"templates",
|
|
17
|
+
"toolcraft-skills",
|
|
17
18
|
"package.json"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"prepack": "node scripts/prepare-pack.mjs",
|
|
21
22
|
"test": "node --test src/*.test.mjs",
|
|
22
23
|
"typecheck": "node --check bin/toolcraft.mjs && node --check bin/create-toolcraft-app.mjs && node --check scripts/*.mjs && node --check src/*.mjs"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@clack/prompts": "^1.6.0",
|
|
27
|
+
"skills": "^1.5.12"
|
|
23
28
|
}
|
|
24
29
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import readline from "node:readline/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
cancel,
|
|
10
|
+
confirm,
|
|
11
|
+
intro,
|
|
12
|
+
isCancel,
|
|
13
|
+
note,
|
|
14
|
+
outro,
|
|
15
|
+
spinner,
|
|
16
|
+
text,
|
|
17
|
+
} from "@clack/prompts";
|
|
5
18
|
|
|
6
19
|
import { pathExists } from "./copy-recursive.mjs";
|
|
7
20
|
import { generateToolcraft } from "./generate.mjs";
|
|
8
21
|
import { sanitizePackageName } from "./package-json.mjs";
|
|
9
22
|
|
|
10
23
|
const DEFAULT_PROJECT_NAME = "my-toolcraft-app";
|
|
24
|
+
const PACKAGE_ROOT = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
11
26
|
|
|
12
27
|
function writeLine(stream, message = "") {
|
|
13
28
|
stream.write(`${message}\n`);
|
|
@@ -44,6 +59,43 @@ function canPrompt(context) {
|
|
|
44
59
|
return Boolean(context.forcePrompts || (stdin.isTTY && stdout.isTTY));
|
|
45
60
|
}
|
|
46
61
|
|
|
62
|
+
function canUseClack(context) {
|
|
63
|
+
return Boolean(
|
|
64
|
+
!context.disableClack &&
|
|
65
|
+
!context.prompts &&
|
|
66
|
+
getStdin(context).isTTY &&
|
|
67
|
+
getStdout(context).isTTY,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleCancel(value) {
|
|
72
|
+
if (!isCancel(value)) {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
cancel("Operation cancelled");
|
|
77
|
+
throw new Error("Operation cancelled.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runWithSpinner(context, startMessage, successMessage, task) {
|
|
81
|
+
if (!canUseClack(context)) {
|
|
82
|
+
writeLine(getStdout(context), startMessage);
|
|
83
|
+
return task();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const taskSpinner = spinner();
|
|
87
|
+
taskSpinner.start(startMessage);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const result = await task();
|
|
91
|
+
taskSpinner.stop(successMessage);
|
|
92
|
+
return result;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
taskSpinner.stop("Failed");
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
47
99
|
function createMissingValueError(valueName) {
|
|
48
100
|
return new Error(`${valueName} is required. Pass it with a flag, or run in an interactive terminal.`);
|
|
49
101
|
}
|
|
@@ -66,6 +118,17 @@ async function promptText(context, label, defaultValue) {
|
|
|
66
118
|
throw createMissingValueError(label);
|
|
67
119
|
}
|
|
68
120
|
|
|
121
|
+
if (canUseClack(context)) {
|
|
122
|
+
const answer = handleCancel(
|
|
123
|
+
await text({
|
|
124
|
+
message: label,
|
|
125
|
+
placeholder: defaultValue,
|
|
126
|
+
defaultValue,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
return String(answer).trim() || defaultValue;
|
|
130
|
+
}
|
|
131
|
+
|
|
69
132
|
const rl = readline.createInterface({
|
|
70
133
|
input: getStdin(context),
|
|
71
134
|
output: getStdout(context),
|
|
@@ -88,6 +151,15 @@ async function promptConfirm(context, label, defaultValue = false) {
|
|
|
88
151
|
return defaultValue;
|
|
89
152
|
}
|
|
90
153
|
|
|
154
|
+
if (canUseClack(context)) {
|
|
155
|
+
return handleCancel(
|
|
156
|
+
await confirm({
|
|
157
|
+
message: label,
|
|
158
|
+
initialValue: defaultValue,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
91
163
|
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
92
164
|
const rl = readline.createInterface({
|
|
93
165
|
input: getStdin(context),
|
|
@@ -131,10 +203,15 @@ function readOptionValue(argv, index, optionName) {
|
|
|
131
203
|
|
|
132
204
|
export function parseCreateArgs(argv) {
|
|
133
205
|
const options = {
|
|
206
|
+
agent: [],
|
|
207
|
+
copy: false,
|
|
134
208
|
force: false,
|
|
209
|
+
global: false,
|
|
135
210
|
help: false,
|
|
136
211
|
install: true,
|
|
137
212
|
name: undefined,
|
|
213
|
+
skills: true,
|
|
214
|
+
skillsAll: false,
|
|
138
215
|
targetDir: undefined,
|
|
139
216
|
yes: false,
|
|
140
217
|
};
|
|
@@ -152,6 +229,21 @@ export function parseCreateArgs(argv) {
|
|
|
152
229
|
continue;
|
|
153
230
|
}
|
|
154
231
|
|
|
232
|
+
if (arg === "--global" || arg === "-g") {
|
|
233
|
+
options.global = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (arg === "--copy") {
|
|
238
|
+
options.copy = true;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (arg === "--all") {
|
|
243
|
+
options.skillsAll = true;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
155
247
|
if (arg === "--yes" || arg === "-y") {
|
|
156
248
|
options.yes = true;
|
|
157
249
|
continue;
|
|
@@ -162,12 +254,31 @@ export function parseCreateArgs(argv) {
|
|
|
162
254
|
continue;
|
|
163
255
|
}
|
|
164
256
|
|
|
257
|
+
if (arg === "--no-skills") {
|
|
258
|
+
options.skills = false;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
165
262
|
if (arg === "--name") {
|
|
166
263
|
options.name = readOptionValue(argv, index, "--name");
|
|
167
264
|
index += 1;
|
|
168
265
|
continue;
|
|
169
266
|
}
|
|
170
267
|
|
|
268
|
+
if (arg === "--agent" || arg === "-a") {
|
|
269
|
+
const startIndex = index;
|
|
270
|
+
while (argv[index + 1] && !argv[index + 1].startsWith("-")) {
|
|
271
|
+
options.agent.push(argv[index + 1]);
|
|
272
|
+
index += 1;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (index === startIndex) {
|
|
276
|
+
throw new Error(`${arg} requires an agent name.`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
171
282
|
if (arg === "--dir") {
|
|
172
283
|
if (options.targetDir) {
|
|
173
284
|
throw new Error("Pass either a positional target directory or --dir, not both.");
|
|
@@ -239,6 +350,7 @@ export async function resolveCreateOptions(parsedOptions, context = {}) {
|
|
|
239
350
|
force,
|
|
240
351
|
install: parsedOptions.install && context.env?.TOOLCRAFT_SKIP_INSTALL !== "1",
|
|
241
352
|
name,
|
|
353
|
+
skills: parsedOptions.skills && context.env?.TOOLCRAFT_SKIP_SKILLS !== "1",
|
|
242
354
|
targetDir,
|
|
243
355
|
};
|
|
244
356
|
}
|
|
@@ -268,17 +380,106 @@ function runCommand(command, args, options = {}, context = {}) {
|
|
|
268
380
|
});
|
|
269
381
|
}
|
|
270
382
|
|
|
383
|
+
function getSkillsCliPath(context = {}) {
|
|
384
|
+
if (context.skillsCliPath) {
|
|
385
|
+
return context.skillsCliPath;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const skillsPackageJsonPath = require.resolve("skills/package.json");
|
|
389
|
+
return path.join(path.dirname(skillsPackageJsonPath), "bin/cli.mjs");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function getToolcraftSkillsSourceDir(context = {}) {
|
|
393
|
+
return context.toolcraftSkillsSourceDir ?? path.join(PACKAGE_ROOT, "toolcraft-skills");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function createSkillsAddArgs(parsedOptions, context = {}) {
|
|
397
|
+
const args = ["add", getToolcraftSkillsSourceDir(context)];
|
|
398
|
+
|
|
399
|
+
if (parsedOptions.skillsAll) {
|
|
400
|
+
args.push("--all");
|
|
401
|
+
} else {
|
|
402
|
+
args.push("--skill", "*");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const agent of parsedOptions.agent) {
|
|
406
|
+
args.push("--agent", agent);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (parsedOptions.global) {
|
|
410
|
+
args.push("--global");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (parsedOptions.copy) {
|
|
414
|
+
args.push("--copy");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (parsedOptions.yes && !parsedOptions.skillsAll) {
|
|
418
|
+
args.push("--yes");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return args;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function installToolcraftSkills(parsedOptions, targetDir, context = {}) {
|
|
425
|
+
const skillsSourceDir = getToolcraftSkillsSourceDir(context);
|
|
426
|
+
const skillsCliPath = getSkillsCliPath(context);
|
|
427
|
+
|
|
428
|
+
const args = createSkillsAddArgs(parsedOptions, {
|
|
429
|
+
...context,
|
|
430
|
+
toolcraftSkillsSourceDir: skillsSourceDir,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
await runCommand(
|
|
434
|
+
process.execPath,
|
|
435
|
+
[skillsCliPath, ...args],
|
|
436
|
+
{
|
|
437
|
+
cwd: targetDir,
|
|
438
|
+
env: {
|
|
439
|
+
...process.env,
|
|
440
|
+
...(context.env ?? {}),
|
|
441
|
+
},
|
|
442
|
+
stdio: "inherit",
|
|
443
|
+
},
|
|
444
|
+
context,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function writeFinalSummary(stdout, result, createOptions) {
|
|
449
|
+
const sameDirectory = result.targetDir === path.resolve(createOptions.cwd);
|
|
450
|
+
|
|
451
|
+
writeLine(stdout, "");
|
|
452
|
+
writeLine(stdout, `Created ${result.packageName} at ${result.targetDir}`);
|
|
453
|
+
writeLine(stdout, "");
|
|
454
|
+
writeLine(stdout, "Next steps:");
|
|
455
|
+
if (!sameDirectory) {
|
|
456
|
+
const displayTargetDir = path.isAbsolute(createOptions.targetDir)
|
|
457
|
+
? result.targetDir
|
|
458
|
+
: result.relativeTargetDir;
|
|
459
|
+
writeLine(stdout, ` cd ${displayTargetDir}`);
|
|
460
|
+
}
|
|
461
|
+
writeLine(stdout, " pnpm dev");
|
|
462
|
+
}
|
|
463
|
+
|
|
271
464
|
export async function runCreateCommand(parsedOptions, context = {}) {
|
|
272
465
|
const cwd = path.resolve(context.cwd ?? process.cwd());
|
|
273
466
|
const stdout = getStdout(context);
|
|
467
|
+
const usingClack = canUseClack(context);
|
|
468
|
+
|
|
469
|
+
if (usingClack) {
|
|
470
|
+
intro("Create Toolcraft app");
|
|
471
|
+
}
|
|
472
|
+
|
|
274
473
|
const createOptions = await resolveCreateOptions(parsedOptions, context);
|
|
275
474
|
|
|
276
|
-
const result = await
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
475
|
+
const result = await runWithSpinner(context, "Creating Toolcraft app...", "Created app", () =>
|
|
476
|
+
generateToolcraft({
|
|
477
|
+
cwd,
|
|
478
|
+
force: createOptions.force,
|
|
479
|
+
name: createOptions.name,
|
|
480
|
+
targetDir: createOptions.targetDir,
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
282
483
|
|
|
283
484
|
if (createOptions.install) {
|
|
284
485
|
writeLine(stdout, "");
|
|
@@ -298,19 +499,22 @@ export async function runCreateCommand(parsedOptions, context = {}) {
|
|
|
298
499
|
);
|
|
299
500
|
}
|
|
300
501
|
|
|
301
|
-
|
|
502
|
+
if (createOptions.skills) {
|
|
503
|
+
writeLine(stdout, "");
|
|
504
|
+
writeLine(stdout, "Installing Toolcraft skills with skills add...");
|
|
505
|
+
await installToolcraftSkills(parsedOptions, result.targetDir, context);
|
|
506
|
+
}
|
|
302
507
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
508
|
+
if (usingClack) {
|
|
509
|
+
const nextSteps = [
|
|
510
|
+
...(result.targetDir === cwd ? [] : [`cd ${result.relativeTargetDir}`]),
|
|
511
|
+
"pnpm dev",
|
|
512
|
+
].join("\n");
|
|
513
|
+
note(nextSteps, "Next steps");
|
|
514
|
+
outro(`Created ${result.packageName}`);
|
|
515
|
+
} else {
|
|
516
|
+
writeFinalSummary(stdout, result, { ...createOptions, cwd });
|
|
312
517
|
}
|
|
313
|
-
writeLine(stdout, " pnpm dev");
|
|
314
518
|
|
|
315
519
|
return result;
|
|
316
520
|
}
|
|
@@ -339,6 +543,11 @@ Options:
|
|
|
339
543
|
--dir <dir> Target directory for the generated app.
|
|
340
544
|
--force, -f Allow writing into a non-empty target folder.
|
|
341
545
|
--yes, -y Use defaults for missing values and skip prompts.
|
|
546
|
+
--agent, -a Install Toolcraft skills to specific agents. Repeat or pass multiple values.
|
|
547
|
+
--global, -g Install Toolcraft skills globally instead of project-level.
|
|
548
|
+
--copy Copy skills instead of using the skills CLI default link behavior.
|
|
549
|
+
--all Install Toolcraft skills to all supported agents without skills prompts.
|
|
550
|
+
--no-skills Skip Toolcraft skills installation.
|
|
342
551
|
--no-install Skip automatic pnpm install. Intended for local CLI tests and automation.
|
|
343
552
|
--help, -h Show this help message.`;
|
|
344
553
|
}
|
package/src/cli.test.mjs
CHANGED
|
@@ -46,10 +46,15 @@ describe("parseCreateArgs", () => {
|
|
|
46
46
|
"--no-install",
|
|
47
47
|
]),
|
|
48
48
|
{
|
|
49
|
+
agent: [],
|
|
50
|
+
copy: false,
|
|
49
51
|
force: true,
|
|
52
|
+
global: false,
|
|
50
53
|
help: false,
|
|
51
54
|
install: false,
|
|
52
55
|
name: "Generated App",
|
|
56
|
+
skills: true,
|
|
57
|
+
skillsAll: false,
|
|
53
58
|
targetDir: "generated-app",
|
|
54
59
|
yes: true,
|
|
55
60
|
},
|
|
@@ -60,6 +65,25 @@ describe("parseCreateArgs", () => {
|
|
|
60
65
|
assert.equal(parseCreateArgs(["--dir", "target-app"]).targetDir, "target-app");
|
|
61
66
|
});
|
|
62
67
|
|
|
68
|
+
it("parses skills add compatible options", () => {
|
|
69
|
+
const options = parseCreateArgs([
|
|
70
|
+
"generated-app",
|
|
71
|
+
"--agent",
|
|
72
|
+
"codex",
|
|
73
|
+
"claude-code",
|
|
74
|
+
"--global",
|
|
75
|
+
"--copy",
|
|
76
|
+
"--all",
|
|
77
|
+
"--no-skills",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(options.agent, ["codex", "claude-code"]);
|
|
81
|
+
assert.equal(options.global, true);
|
|
82
|
+
assert.equal(options.copy, true);
|
|
83
|
+
assert.equal(options.skillsAll, true);
|
|
84
|
+
assert.equal(options.skills, false);
|
|
85
|
+
});
|
|
86
|
+
|
|
63
87
|
it("rejects duplicate target directory values", () => {
|
|
64
88
|
assert.throws(
|
|
65
89
|
() => parseCreateArgs(["target-app", "--dir", "other-app"]),
|
|
@@ -80,6 +104,7 @@ describe("resolveCreateOptions", () => {
|
|
|
80
104
|
force: false,
|
|
81
105
|
install: false,
|
|
82
106
|
name: path.basename(tempRoot).toLowerCase(),
|
|
107
|
+
skills: true,
|
|
83
108
|
targetDir: ".",
|
|
84
109
|
});
|
|
85
110
|
});
|
|
@@ -109,6 +134,7 @@ describe("resolveCreateOptions", () => {
|
|
|
109
134
|
force: false,
|
|
110
135
|
install: true,
|
|
111
136
|
name: "prompted-app",
|
|
137
|
+
skills: true,
|
|
112
138
|
targetDir: "./prompted-app",
|
|
113
139
|
});
|
|
114
140
|
assert.equal(confirmCalled, false);
|
|
@@ -140,6 +166,7 @@ describe("resolveCreateOptions", () => {
|
|
|
140
166
|
force: true,
|
|
141
167
|
install: true,
|
|
142
168
|
name: "prompted-app",
|
|
169
|
+
skills: true,
|
|
143
170
|
targetDir: "./prompted-app",
|
|
144
171
|
});
|
|
145
172
|
});
|
|
@@ -167,7 +194,7 @@ describe("runToolcraftCli", () => {
|
|
|
167
194
|
const stdout = createWritableCapture();
|
|
168
195
|
const stderr = createWritableCapture();
|
|
169
196
|
|
|
170
|
-
const result = await runToolcraftCli(["--no-install"], {
|
|
197
|
+
const result = await runToolcraftCli(["--no-install", "--no-skills"], {
|
|
171
198
|
cwd: tempRoot,
|
|
172
199
|
env: {},
|
|
173
200
|
stderr,
|
|
@@ -197,6 +224,7 @@ describe("runToolcraftCli", () => {
|
|
|
197
224
|
"--yes",
|
|
198
225
|
"--force",
|
|
199
226
|
"--no-install",
|
|
227
|
+
"--no-skills",
|
|
200
228
|
],
|
|
201
229
|
{
|
|
202
230
|
cwd: tempRoot,
|
|
@@ -216,16 +244,20 @@ describe("runToolcraftCli", () => {
|
|
|
216
244
|
assert.equal(stderr.text, "");
|
|
217
245
|
});
|
|
218
246
|
|
|
219
|
-
it("runs pnpm install by default after creating the app", async () => {
|
|
247
|
+
it("runs pnpm install and Toolcraft skills install by default after creating the app", async () => {
|
|
220
248
|
const tempRoot = await createTempRoot();
|
|
221
249
|
const stdout = createWritableCapture();
|
|
222
250
|
const installs = [];
|
|
251
|
+
const skillsCliPath = path.join(tempRoot, "skills-cli.mjs");
|
|
252
|
+
const skillsSourceDir = path.join(tempRoot, "toolcraft-skills");
|
|
223
253
|
|
|
224
254
|
await runToolcraftCli(["create", "install-app", "--name", "Install App", "--yes", "--force"], {
|
|
225
255
|
cwd: tempRoot,
|
|
226
256
|
env: {},
|
|
227
257
|
stdout,
|
|
258
|
+
skillsCliPath,
|
|
228
259
|
throwOnError: true,
|
|
260
|
+
toolcraftSkillsSourceDir: skillsSourceDir,
|
|
229
261
|
async runCommand(command, args, options) {
|
|
230
262
|
installs.push({ args, command, cwd: options.cwd });
|
|
231
263
|
},
|
|
@@ -237,11 +269,100 @@ describe("runToolcraftCli", () => {
|
|
|
237
269
|
command: "pnpm",
|
|
238
270
|
cwd: path.join(tempRoot, "install-app"),
|
|
239
271
|
},
|
|
272
|
+
{
|
|
273
|
+
args: [skillsCliPath, "add", skillsSourceDir, "--skill", "*", "--yes"],
|
|
274
|
+
command: process.execPath,
|
|
275
|
+
cwd: path.join(tempRoot, "install-app"),
|
|
276
|
+
},
|
|
240
277
|
]);
|
|
241
278
|
assert.match(stdout.text, /Installing dependencies with pnpm/);
|
|
279
|
+
assert.match(stdout.text, /Installing Toolcraft skills/);
|
|
242
280
|
assert.match(stdout.text, /pnpm dev/);
|
|
243
281
|
});
|
|
244
282
|
|
|
283
|
+
it("forwards skills add compatible options", async () => {
|
|
284
|
+
const tempRoot = await createTempRoot();
|
|
285
|
+
const skillsCommands = [];
|
|
286
|
+
const skillsCliPath = path.join(tempRoot, "skills-cli.mjs");
|
|
287
|
+
const skillsSourceDir = path.join(tempRoot, "toolcraft-skills");
|
|
288
|
+
|
|
289
|
+
await runToolcraftCli(
|
|
290
|
+
[
|
|
291
|
+
"create",
|
|
292
|
+
"skills-options-app",
|
|
293
|
+
"--name",
|
|
294
|
+
"Skills Options App",
|
|
295
|
+
"--yes",
|
|
296
|
+
"--force",
|
|
297
|
+
"--no-install",
|
|
298
|
+
"--agent",
|
|
299
|
+
"codex",
|
|
300
|
+
"claude-code",
|
|
301
|
+
"--global",
|
|
302
|
+
"--copy",
|
|
303
|
+
],
|
|
304
|
+
{
|
|
305
|
+
cwd: tempRoot,
|
|
306
|
+
env: {},
|
|
307
|
+
skillsCliPath,
|
|
308
|
+
throwOnError: true,
|
|
309
|
+
toolcraftSkillsSourceDir: skillsSourceDir,
|
|
310
|
+
async runCommand(command, args, options) {
|
|
311
|
+
skillsCommands.push({ args, command, cwd: options.cwd });
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
assert.deepEqual(skillsCommands, [
|
|
317
|
+
{
|
|
318
|
+
args: [
|
|
319
|
+
skillsCliPath,
|
|
320
|
+
"add",
|
|
321
|
+
skillsSourceDir,
|
|
322
|
+
"--skill",
|
|
323
|
+
"*",
|
|
324
|
+
"--agent",
|
|
325
|
+
"codex",
|
|
326
|
+
"--agent",
|
|
327
|
+
"claude-code",
|
|
328
|
+
"--global",
|
|
329
|
+
"--copy",
|
|
330
|
+
"--yes",
|
|
331
|
+
],
|
|
332
|
+
command: process.execPath,
|
|
333
|
+
cwd: path.join(tempRoot, "skills-options-app"),
|
|
334
|
+
},
|
|
335
|
+
]);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("skips Toolcraft skills installation with --no-skills", async () => {
|
|
339
|
+
const tempRoot = await createTempRoot();
|
|
340
|
+
const commands = [];
|
|
341
|
+
|
|
342
|
+
await runToolcraftCli(
|
|
343
|
+
[
|
|
344
|
+
"create",
|
|
345
|
+
"no-skills-app",
|
|
346
|
+
"--name",
|
|
347
|
+
"No Skills App",
|
|
348
|
+
"--yes",
|
|
349
|
+
"--force",
|
|
350
|
+
"--no-install",
|
|
351
|
+
"--no-skills",
|
|
352
|
+
],
|
|
353
|
+
{
|
|
354
|
+
cwd: tempRoot,
|
|
355
|
+
env: {},
|
|
356
|
+
throwOnError: true,
|
|
357
|
+
async runCommand(command, args, options) {
|
|
358
|
+
commands.push({ args, command, cwd: options.cwd });
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
assert.deepEqual(commands, []);
|
|
364
|
+
});
|
|
365
|
+
|
|
245
366
|
it("prints create help with the npx command", async () => {
|
|
246
367
|
const stdout = createWritableCapture();
|
|
247
368
|
|
|
@@ -252,6 +373,9 @@ describe("runToolcraftCli", () => {
|
|
|
252
373
|
|
|
253
374
|
assert.equal(result.ok, true);
|
|
254
375
|
assert.match(stdout.text, /npx @pixel-point\/toolcraft create/);
|
|
376
|
+
assert.match(stdout.text, /--agent/);
|
|
377
|
+
assert.match(stdout.text, /--global/);
|
|
378
|
+
assert.match(stdout.text, /--no-skills/);
|
|
255
379
|
assert.match(stdout.text, /--no-install/);
|
|
256
380
|
});
|
|
257
381
|
|
|
@@ -269,6 +393,7 @@ describe("runToolcraftCli", () => {
|
|
|
269
393
|
"--yes",
|
|
270
394
|
"--force",
|
|
271
395
|
"--no-install",
|
|
396
|
+
"--no-skills",
|
|
272
397
|
],
|
|
273
398
|
{
|
|
274
399
|
cwd: tempRoot,
|
|
@@ -3,31 +3,66 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
7
8
|
const codexHome = process.env.CODEX_HOME
|
|
8
9
|
? path.resolve(process.env.CODEX_HOME)
|
|
9
10
|
: path.join(os.homedir(), ".codex");
|
|
11
|
+
const claudeHome = process.env.CLAUDE_HOME
|
|
12
|
+
? path.resolve(process.env.CLAUDE_HOME)
|
|
13
|
+
: path.join(os.homedir(), ".claude");
|
|
14
|
+
const cursorHome = path.join(os.homedir(), ".cursor");
|
|
15
|
+
const configHome = process.env.XDG_CONFIG_HOME
|
|
16
|
+
? path.resolve(process.env.XDG_CONFIG_HOME)
|
|
17
|
+
: path.join(os.homedir(), ".config");
|
|
18
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const projectRoot = path.resolve(scriptDir, "..");
|
|
20
|
+
|
|
21
|
+
const skillRoots = [
|
|
22
|
+
path.join(projectRoot, ".agents/skills"),
|
|
23
|
+
path.join(projectRoot, ".claude/skills"),
|
|
24
|
+
path.join(codexHome, "skills"),
|
|
25
|
+
path.join(claudeHome, "skills"),
|
|
26
|
+
path.join(cursorHome, "skills"),
|
|
27
|
+
path.join(configHome, "opencode/skills"),
|
|
28
|
+
path.join(configHome, "agents/skills"),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function skillPaths(name) {
|
|
32
|
+
return skillRoots.map((root) => path.join(root, name, "SKILL.md"));
|
|
33
|
+
}
|
|
10
34
|
|
|
11
35
|
const requiredSkills = [
|
|
12
36
|
{
|
|
13
37
|
name: "brainstorming",
|
|
14
38
|
purpose: "shape the app behavior, panels, controls, canvas, media, export, and ambiguity before code",
|
|
15
|
-
paths:
|
|
39
|
+
paths: skillPaths("brainstorming"),
|
|
16
40
|
},
|
|
17
41
|
{
|
|
18
42
|
name: "writing-plans",
|
|
19
43
|
purpose: "turn the approved app spec into a deterministic Toolcraft implementation plan",
|
|
20
|
-
paths:
|
|
44
|
+
paths: skillPaths("writing-plans"),
|
|
21
45
|
},
|
|
22
46
|
{
|
|
23
47
|
name: "systematic-debugging",
|
|
24
48
|
purpose: "investigate root cause before fixing broken controls, tests, builds, visual regressions, or runtime bugs",
|
|
25
|
-
paths:
|
|
49
|
+
paths: skillPaths("systematic-debugging"),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "figma",
|
|
53
|
+
purpose: "inspect actual Figma structure before implementing Figma-referenced Toolcraft apps",
|
|
54
|
+
paths: skillPaths("figma"),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "figma-implement-design",
|
|
58
|
+
purpose: "translate inspected Figma structure into Toolcraft schema, renderer, and verification coverage",
|
|
59
|
+
paths: skillPaths("figma-implement-design"),
|
|
26
60
|
},
|
|
27
61
|
{
|
|
28
62
|
name: "browser",
|
|
29
63
|
purpose: "verify the generated UI in a running local browser after implementation",
|
|
30
64
|
paths: [
|
|
65
|
+
...skillPaths("browser"),
|
|
31
66
|
path.join(codexHome, "skills/browser/SKILL.md"),
|
|
32
67
|
path.join(codexHome, "skills/browser/browser/SKILL.md"),
|
|
33
68
|
path.join(codexHome, "plugins/cache/openai-bundled/browser/0.1.0-alpha2/skills/browser/SKILL.md"),
|
|
@@ -90,6 +125,6 @@ for (const skill of missingSkills) {
|
|
|
90
125
|
console.error("");
|
|
91
126
|
console.error("If your AI environment supports Codex skills, install the missing skills before implementation.");
|
|
92
127
|
console.error("If installation is not available, stop and ask the user to install them.");
|
|
93
|
-
console.error(
|
|
128
|
+
console.error("Checked project and global skill locations for Codex, Claude Code, Cursor, and OpenCode-compatible agents.");
|
|
94
129
|
|
|
95
130
|
process.exit(1);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brainstorming
|
|
3
|
+
description: Use before creating Toolcraft app features, changing behavior, or assembling app specs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Toolcraft Brainstorming
|
|
7
|
+
|
|
8
|
+
Use this skill before changing a Toolcraft generated app spec, behavior, controls,
|
|
9
|
+
canvas output, panels, export behavior, renderer technique, timeline, layers, or
|
|
10
|
+
media flow.
|
|
11
|
+
|
|
12
|
+
## Process
|
|
13
|
+
|
|
14
|
+
1. Read the generated app `AGENTS.md` and relevant `docs/toolcraft/*` contract
|
|
15
|
+
files first.
|
|
16
|
+
2. Identify the product goal, visible output, editable entities, required
|
|
17
|
+
controls, export behavior, and verification tier.
|
|
18
|
+
3. Make a Control Section Inventory grouped by product entity or workflow stage,
|
|
19
|
+
not by UI component type.
|
|
20
|
+
4. Decide whether layers, timeline, persistence, settings transfer, custom
|
|
21
|
+
controls, or a custom renderer are actually required.
|
|
22
|
+
5. Record decisions in `docs/toolcraft/agent-worklog.md`.
|
|
23
|
+
|
|
24
|
+
## Toolcraft Rule
|
|
25
|
+
|
|
26
|
+
The local Toolcraft app contract is the source of truth. Do not ask the user to
|
|
27
|
+
confirm decisions already covered by the prompt, `AGENTS.md`, or local
|
|
28
|
+
Toolcraft docs. Continue when the product behavior is clear.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser
|
|
3
|
+
description: Use to verify Toolcraft generated apps in a real local browser after implementation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Toolcraft Browser Verification
|
|
7
|
+
|
|
8
|
+
Use this skill after implementing generated app behavior that affects visible UI,
|
|
9
|
+
canvas output, export behavior, upload/media flow, panels, controls, timeline,
|
|
10
|
+
layers, or viewport interactions.
|
|
11
|
+
|
|
12
|
+
## Process
|
|
13
|
+
|
|
14
|
+
1. Run the app with `pnpm dev`.
|
|
15
|
+
2. Open the local URL in a real browser.
|
|
16
|
+
3. Verify the product output, canvas backing, controls, panel actions, upload or
|
|
17
|
+
export flow, responsive layout, and any timeline or layer behavior touched by
|
|
18
|
+
the change.
|
|
19
|
+
4. For final delivery, run the generated app verification commands required by
|
|
20
|
+
`AGENTS.md`.
|
|
21
|
+
|
|
22
|
+
## Toolcraft Rule
|
|
23
|
+
|
|
24
|
+
Browser verification is part of the Toolcraft contract. A typecheck or build
|
|
25
|
+
alone is not enough for visible app behavior.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma
|
|
3
|
+
description: Use when a Toolcraft task includes a Figma URL, node ID, or design-to-code requirement.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Toolcraft Figma Workflow
|
|
7
|
+
|
|
8
|
+
Use this skill when the user provides a Figma URL, node ID, or asks to implement
|
|
9
|
+
or match a Figma design.
|
|
10
|
+
|
|
11
|
+
## Process
|
|
12
|
+
|
|
13
|
+
1. Inspect the Figma file through the available Figma MCP or design context
|
|
14
|
+
tooling.
|
|
15
|
+
2. Read actual nodes, layers, components, variables, styles, assets, and layout
|
|
16
|
+
structure.
|
|
17
|
+
3. Translate the design into Toolcraft schema controls, product renderer output,
|
|
18
|
+
canvas sizing, export behavior, and verification coverage.
|
|
19
|
+
4. Use screenshots only for final visual QA, not as the source of truth.
|
|
20
|
+
|
|
21
|
+
## Toolcraft Rule
|
|
22
|
+
|
|
23
|
+
Do not implement a Figma task from a screenshot or by eye when Figma context is
|
|
24
|
+
available. The Figma structure is the reference.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma-implement-design
|
|
3
|
+
description: Use to translate inspected Figma structure into a production-ready Toolcraft generated app.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Toolcraft Figma Implementation
|
|
7
|
+
|
|
8
|
+
Use this skill after Figma context has been inspected and the task is to build or
|
|
9
|
+
update a Toolcraft generated app from that design.
|
|
10
|
+
|
|
11
|
+
## Process
|
|
12
|
+
|
|
13
|
+
1. Map Figma sections, components, and visual entities to product output and
|
|
14
|
+
schema controls.
|
|
15
|
+
2. Use built-in Toolcraft controls before custom controls.
|
|
16
|
+
3. Keep app UI out of `canvasContent`; render product output there only.
|
|
17
|
+
4. Preserve the Toolcraft runtime canvas backing.
|
|
18
|
+
5. Add acceptance, browser, and performance coverage for every visible product
|
|
19
|
+
entity.
|
|
20
|
+
|
|
21
|
+
## Toolcraft Rule
|
|
22
|
+
|
|
23
|
+
Build through `defineToolcraft` and `ToolcraftApp`. Do not recreate Toolcraft
|
|
24
|
+
panels, toolbar, controls, canvas, layers, or timeline by hand.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: systematic-debugging
|
|
3
|
+
description: Use before fixing broken Toolcraft controls, tests, builds, visual mismatches, exports, or runtime regressions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Toolcraft Systematic Debugging
|
|
7
|
+
|
|
8
|
+
Use this skill before fixing any Toolcraft generated app failure.
|
|
9
|
+
|
|
10
|
+
## Process
|
|
11
|
+
|
|
12
|
+
1. Reproduce the failure with the smallest command or browser action.
|
|
13
|
+
2. Read the full error, stack trace, failing assertion, or visual mismatch.
|
|
14
|
+
3. Identify whether the root cause is app schema, app renderer, acceptance data,
|
|
15
|
+
browser test setup, copied runtime source, dependencies, or generated docs.
|
|
16
|
+
4. Compare the failure against the local Toolcraft docs and existing passing
|
|
17
|
+
patterns in the generated app.
|
|
18
|
+
5. Make one targeted fix and rerun the relevant verification.
|
|
19
|
+
|
|
20
|
+
## Toolcraft Rule
|
|
21
|
+
|
|
22
|
+
Fix source behavior in the monorepo runtime or starter when the generated
|
|
23
|
+
template is wrong. Do not patch copied `src/toolcraft` files inside one generated
|
|
24
|
+
app unless the user explicitly wants a local fork.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-plans
|
|
3
|
+
description: Use before Toolcraft code changes once the product behavior or approved spec is clear.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Toolcraft Writing Plans
|
|
7
|
+
|
|
8
|
+
Use this skill before editing generated app code from a clear Toolcraft spec or
|
|
9
|
+
user request.
|
|
10
|
+
|
|
11
|
+
## Plan Content
|
|
12
|
+
|
|
13
|
+
Write a concise implementation plan that names:
|
|
14
|
+
|
|
15
|
+
1. Files to change under `src/app`, `src/routes`, docs, tests, or scripts.
|
|
16
|
+
2. Schema controls, sections, panel actions, renderer output, timeline, layers,
|
|
17
|
+
persistence, settings transfer, and export paths affected by the work.
|
|
18
|
+
3. Acceptance, browser, and performance coverage required by the selected
|
|
19
|
+
verification tier.
|
|
20
|
+
4. Commands to run before completion.
|
|
21
|
+
|
|
22
|
+
## Toolcraft Rule
|
|
23
|
+
|
|
24
|
+
Keep implementation plans focused on app behavior and verification. Do not move
|
|
25
|
+
runtime behavior into route-local state, do not patch copied `src/toolcraft`
|
|
26
|
+
internals for one app, and do not replace built-in Toolcraft surfaces.
|