@schalkneethling/toolkit 0.6.0 → 1.0.0
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 +8 -27
- package/dist/index.mjs +2 -80
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -7
- package/skills/code-review/SKILL.md +81 -0
- package/skills/code-review/agents/openai.yaml +6 -0
- package/skills/more-secure-dependabot-config/SKILL.md +1 -3
- package/skills/npm-publishing-best-practices/SKILL.md +92 -10
- package/skills/npm-publishing-best-practices/agents/openai.yaml +3 -0
- package/skills/npm-trusted-publishing-github-workflow/SKILL.md +294 -0
- package/skills/refined-plan-mode/SKILL.md +137 -11
- package/commands/rpm-advance.md +0 -13
- package/commands/rpm-checkpoint.md +0 -13
- package/commands/rpm-feedback.md +0 -15
- package/commands/rpm-handoff.md +0 -14
- package/commands/rpm-review.md +0 -13
- package/commands/rpm-start.md +0 -13
package/README.md
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
# claude-toolkit
|
|
2
2
|
|
|
3
|
-
CLI for managing [Claude Code](https://claude.com/claude-code) hooks, skills,
|
|
3
|
+
CLI for managing [Claude Code](https://claude.com/claude-code) hooks, skills, and collections across projects. Hooks are copied into a project's `.claude/` directory; skills are copied into `.claude-toolkit/skills/` and symlinked into wherever Claude Code expects to find them; collections install any combination of those resources from a bundled root config.
|
|
4
4
|
|
|
5
5
|
## Repo layout
|
|
6
6
|
|
|
7
7
|
```plaintext
|
|
8
8
|
.
|
|
9
|
-
├── commands/ # Claude Code custom slash commands (*.md)
|
|
10
|
-
│ ├── rpm-start.md
|
|
11
|
-
│ ├── rpm-advance.md
|
|
12
|
-
│ └── ...
|
|
13
9
|
├── config.json # bundled collection definitions
|
|
14
10
|
├── hooks/
|
|
15
11
|
│ ├── auto-approve-safe-commands/
|
|
@@ -33,7 +29,7 @@ CLI for managing [Claude Code](https://claude.com/claude-code) hooks, skills, co
|
|
|
33
29
|
- Node.js 22+
|
|
34
30
|
- `tsx` (installed as a devDependency)
|
|
35
31
|
|
|
36
|
-
From inside a consuming project, run the CLI with `tsx /path/to/claude-toolkit/
|
|
32
|
+
From inside a consuming project, run the CLI with `tsx /path/to/claude-toolkit/src/index.ts <command>`, or link it as `toolkit` on your `PATH`.
|
|
37
33
|
|
|
38
34
|
## Commands
|
|
39
35
|
|
|
@@ -49,17 +45,9 @@ Copies `skills/<name>/` into `<project>/.claude-toolkit/skills/<name>/` and crea
|
|
|
49
45
|
toolkit add skill css-shared-first --link .claude/skills --link docs/skills
|
|
50
46
|
```
|
|
51
47
|
|
|
52
|
-
### `toolkit add command <name>`
|
|
53
|
-
|
|
54
|
-
Copies `commands/<name>.md` into `<project>/.claude/commands/<name>.md`. Records the source hash in `.claude/toolkit-manifest.json`.
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
toolkit add command rpm-start
|
|
58
|
-
```
|
|
59
|
-
|
|
60
48
|
### `toolkit add collections <name>`
|
|
61
49
|
|
|
62
|
-
Reads the root `config.json`, resolves the named collection, and installs each referenced hook
|
|
50
|
+
Reads the root `config.json`, resolves the named collection, and installs each referenced hook and skill using the same underlying logic as the individual `add` commands.
|
|
63
51
|
|
|
64
52
|
```bash
|
|
65
53
|
toolkit add collections web
|
|
@@ -73,9 +61,9 @@ For every entry in `.claude/toolkit-manifest.json`, compares the current source
|
|
|
73
61
|
- If the installed file was modified locally (its hash differs from the one recorded in the manifest), warns and skips unless `--force` is passed.
|
|
74
62
|
- Silent if everything is current.
|
|
75
63
|
|
|
76
|
-
### `toolkit list hook` / `toolkit list skill`
|
|
64
|
+
### `toolkit list hook` / `toolkit list skill`
|
|
77
65
|
|
|
78
|
-
Lists available hooks
|
|
66
|
+
Lists available hooks or skills shipped by this repo, with the current source hash.
|
|
79
67
|
|
|
80
68
|
### `toolkit list collections`
|
|
81
69
|
|
|
@@ -109,13 +97,12 @@ Collections are defined in the repo root `config.json` as an array:
|
|
|
109
97
|
```
|
|
110
98
|
|
|
111
99
|
- `name` must be unique.
|
|
112
|
-
- `items` may contain `skill
|
|
113
|
-
- `src` must point to a top-level entry under `skills
|
|
114
|
-
- Plural `type` values such as `
|
|
100
|
+
- `items` may contain `skill` or `hook` entries.
|
|
101
|
+
- `src` must point to a top-level entry under `skills/` or `hooks/`.
|
|
102
|
+
- Plural `type` values such as `skills` are also accepted for compatibility.
|
|
115
103
|
|
|
116
104
|
## Versioning
|
|
117
105
|
|
|
118
|
-
- Each command is hashed over its `.md` file only.
|
|
119
106
|
- Each hook is hashed over `hook.mjs` only (not the README or `settings-fragment.json`).
|
|
120
107
|
- Each skill is hashed over every file in the skill directory (sorted by path).
|
|
121
108
|
- SHA-256, truncated to the first 7 hex characters.
|
|
@@ -126,12 +113,6 @@ The CLI writes `<project>/.claude/toolkit-manifest.json`:
|
|
|
126
113
|
|
|
127
114
|
```json
|
|
128
115
|
{
|
|
129
|
-
"commands": {
|
|
130
|
-
"rpm-start": {
|
|
131
|
-
"hash": "b8e2a1f",
|
|
132
|
-
"installedAt": "2026-04-18"
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
116
|
"hooks": {
|
|
136
117
|
"block-dangerous-commands": {
|
|
137
118
|
"hash": "a3f9c2d",
|
package/dist/index.mjs
CHANGED
|
@@ -7,23 +7,20 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
//#region src/index.ts
|
|
9
9
|
/**
|
|
10
|
-
* toolkit — personal CLI for managing Claude Code hooks
|
|
10
|
+
* toolkit — personal CLI for managing Claude Code hooks and skills.
|
|
11
11
|
*
|
|
12
12
|
* Commands:
|
|
13
13
|
* toolkit add hook <name>
|
|
14
14
|
* toolkit add skill <name> [--link <target>...]
|
|
15
|
-
* toolkit add command <name>
|
|
16
15
|
* toolkit add collections <name>
|
|
17
16
|
* toolkit update [--force]
|
|
18
17
|
* toolkit list hook
|
|
19
18
|
* toolkit list skill
|
|
20
|
-
* toolkit list command
|
|
21
19
|
* toolkit list collections
|
|
22
20
|
*/
|
|
23
21
|
const TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
24
22
|
const HOOKS_SRC = join(TOOLKIT_ROOT, "hooks");
|
|
25
23
|
const SKILLS_SRC = join(TOOLKIT_ROOT, "skills");
|
|
26
|
-
const COMMANDS_SRC = join(TOOLKIT_ROOT, "commands");
|
|
27
24
|
const CONFIG_PATH = join(TOOLKIT_ROOT, "config.json");
|
|
28
25
|
const PROJECT_ROOT = process.cwd();
|
|
29
26
|
const CLAUDE_DIR = join(PROJECT_ROOT, ".claude");
|
|
@@ -37,20 +34,17 @@ function shortHash(content) {
|
|
|
37
34
|
}
|
|
38
35
|
function readManifest() {
|
|
39
36
|
if (!existsSync(MANIFEST_PATH)) return {
|
|
40
|
-
commands: {},
|
|
41
37
|
hooks: {},
|
|
42
38
|
skills: {}
|
|
43
39
|
};
|
|
44
40
|
try {
|
|
45
41
|
const parsed = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
46
42
|
return {
|
|
47
|
-
commands: parsed.commands ?? {},
|
|
48
43
|
hooks: parsed.hooks ?? {},
|
|
49
44
|
skills: parsed.skills ?? {}
|
|
50
45
|
};
|
|
51
46
|
} catch {
|
|
52
47
|
return {
|
|
53
|
-
commands: {},
|
|
54
48
|
hooks: {},
|
|
55
49
|
skills: {}
|
|
56
50
|
};
|
|
@@ -72,9 +66,6 @@ function deepMerge(target, source) {
|
|
|
72
66
|
}
|
|
73
67
|
return source;
|
|
74
68
|
}
|
|
75
|
-
function hashCommandSource(name) {
|
|
76
|
-
return shortHash(readFileSync(join(COMMANDS_SRC, `${name}.md`)));
|
|
77
|
-
}
|
|
78
69
|
function hashHookSource(name) {
|
|
79
70
|
return shortHash(readFileSync(join(HOOKS_SRC, name, "hook.mjs")));
|
|
80
71
|
}
|
|
@@ -131,7 +122,6 @@ function sanitizeName(name, kind) {
|
|
|
131
122
|
return name;
|
|
132
123
|
}
|
|
133
124
|
function normalizeCollectionItemType(type, collectionName) {
|
|
134
|
-
if (type === "command" || type === "commands") return "command";
|
|
135
125
|
if (type === "hook" || type === "hooks") return "hook";
|
|
136
126
|
if (type === "skill" || type === "skills") return "skill";
|
|
137
127
|
throw new Error(`Collection "${collectionName}" has unsupported item type "${type}"`);
|
|
@@ -142,10 +132,6 @@ function resolveSourcePath(src, kind, collectionName) {
|
|
|
142
132
|
return sourcePath;
|
|
143
133
|
}
|
|
144
134
|
function inferItemNameFromSource(type, sourcePath, collectionName) {
|
|
145
|
-
if (type === "command") {
|
|
146
|
-
if (dirname(sourcePath) !== COMMANDS_SRC || !sourcePath.startsWith(COMMANDS_SRC + sep) || !sourcePath.endsWith(".md")) throw new Error(`Collection "${collectionName}" command source must point to a markdown file directly under commands/: ${relative(TOOLKIT_ROOT, sourcePath)}`);
|
|
147
|
-
return basename(sourcePath, ".md");
|
|
148
|
-
}
|
|
149
135
|
const expectedRoot = type === "hook" ? HOOKS_SRC : SKILLS_SRC;
|
|
150
136
|
if (dirname(sourcePath) !== expectedRoot || !sourcePath.startsWith(expectedRoot + sep)) throw new Error(`Collection "${collectionName}" ${type} source must point to a top-level entry under ${relative(TOOLKIT_ROOT, expectedRoot)}/: ${relative(TOOLKIT_ROOT, sourcePath)}`);
|
|
151
137
|
return basename(sourcePath);
|
|
@@ -200,31 +186,6 @@ function resolveCollection(name) {
|
|
|
200
186
|
}
|
|
201
187
|
return [...deduped.values()];
|
|
202
188
|
}
|
|
203
|
-
function installCommand(name, src) {
|
|
204
|
-
if (!existsSync(src)) {
|
|
205
|
-
console.error(`Command not found: ${name}`);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
const commandsDir = join(CLAUDE_DIR, "commands");
|
|
209
|
-
mkdirSync(commandsDir, { recursive: true });
|
|
210
|
-
const dest = resolve(commandsDir, `${name}.md`);
|
|
211
|
-
if (!dest.startsWith(commandsDir + sep)) {
|
|
212
|
-
console.error("Invalid command name");
|
|
213
|
-
process.exit(1);
|
|
214
|
-
}
|
|
215
|
-
writeFileSync(dest, readFileSync(src));
|
|
216
|
-
const manifest = readManifest();
|
|
217
|
-
manifest.commands[name] = {
|
|
218
|
-
hash: hashCommandSource(name),
|
|
219
|
-
installedAt: today()
|
|
220
|
-
};
|
|
221
|
-
writeManifest(manifest);
|
|
222
|
-
console.log(`Installed command: ${name} → ${relative(PROJECT_ROOT, dest)}`);
|
|
223
|
-
}
|
|
224
|
-
function addCommand(name) {
|
|
225
|
-
name = sanitizeName(name, "command");
|
|
226
|
-
installCommand(name, join(COMMANDS_SRC, `${name}.md`));
|
|
227
|
-
}
|
|
228
189
|
function installHook(name, srcDir) {
|
|
229
190
|
if (!existsSync(srcDir)) {
|
|
230
191
|
console.error(`Hook not found: ${name}`);
|
|
@@ -298,11 +259,6 @@ function addCollection(name) {
|
|
|
298
259
|
if (!existsSync(item.sourcePath)) throw new Error(`Collection "${item.collection}" references missing ${item.type} source: ${relative(TOOLKIT_ROOT, item.sourcePath)}`);
|
|
299
260
|
const itemStats = statSync(item.sourcePath);
|
|
300
261
|
const actualKind = itemStats.isFile() ? "file" : itemStats.isDirectory() ? "directory" : "other";
|
|
301
|
-
if (item.type === "command") {
|
|
302
|
-
if (!itemStats.isFile()) throw new Error(`Collection "${item.collection}" expected command source "${item.sourcePath}" to be a file, found ${actualKind}`);
|
|
303
|
-
installCommand(item.sourceName, item.sourcePath);
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
262
|
if (item.type === "hook") {
|
|
307
263
|
if (!itemStats.isDirectory()) throw new Error(`Collection "${item.collection}" expected hook source "${item.sourcePath}" to be a directory, found ${actualKind}`);
|
|
308
264
|
installHook(item.sourceName, item.sourcePath);
|
|
@@ -368,36 +324,9 @@ async function update(force) {
|
|
|
368
324
|
linkedTo: entry.linkedTo
|
|
369
325
|
};
|
|
370
326
|
}
|
|
371
|
-
for (const [name, entry] of Object.entries(manifest.commands)) {
|
|
372
|
-
const src = join(COMMANDS_SRC, `${name}.md`);
|
|
373
|
-
if (!existsSync(src)) continue;
|
|
374
|
-
const sourceHash = hashCommandSource(name);
|
|
375
|
-
if (sourceHash === entry.hash) continue;
|
|
376
|
-
changed = true;
|
|
377
|
-
console.log(`\n~ command: ${name} (${entry.hash} → ${sourceHash})`);
|
|
378
|
-
if (!(force || await confirm(`Update command "${name}"?`))) continue;
|
|
379
|
-
writeFileSync(join(CLAUDE_DIR, "commands", `${name}.md`), readFileSync(src));
|
|
380
|
-
manifest.commands[name] = {
|
|
381
|
-
hash: sourceHash,
|
|
382
|
-
installedAt: today()
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
327
|
if (changed) writeManifest(manifest);
|
|
386
328
|
}
|
|
387
329
|
function list(kind) {
|
|
388
|
-
if (kind === "command") {
|
|
389
|
-
if (!existsSync(COMMANDS_SRC)) {
|
|
390
|
-
console.log("(no commands available)");
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
const files = readdirSync(COMMANDS_SRC).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
|
|
394
|
-
if (files.length === 0) {
|
|
395
|
-
console.log("(no commands available)");
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
for (const name of files) console.log(`${name} ${hashCommandSource(name)}`);
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
330
|
const dir = kind === "hook" ? HOOKS_SRC : SKILLS_SRC;
|
|
402
331
|
if (!existsSync(dir)) {
|
|
403
332
|
console.log(`(no ${kind}s available)`);
|
|
@@ -425,12 +354,10 @@ function usage() {
|
|
|
425
354
|
console.error(`Usage:
|
|
426
355
|
toolkit add hook <name>
|
|
427
356
|
toolkit add skill <name> [--link <target>]...
|
|
428
|
-
toolkit add command <name>
|
|
429
357
|
toolkit add collections <name>
|
|
430
358
|
toolkit update [--force]
|
|
431
359
|
toolkit list hook
|
|
432
360
|
toolkit list skill
|
|
433
|
-
toolkit list command
|
|
434
361
|
toolkit list collections`);
|
|
435
362
|
process.exit(1);
|
|
436
363
|
}
|
|
@@ -460,11 +387,6 @@ async function main() {
|
|
|
460
387
|
addSkill(name, links ? links : []);
|
|
461
388
|
return;
|
|
462
389
|
}
|
|
463
|
-
if (command === "add" && resource === "command") {
|
|
464
|
-
if (!name) usage();
|
|
465
|
-
addCommand(name);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
390
|
if (command === "add" && (resource === "collection" || resource === "collections")) {
|
|
469
391
|
if (!name) usage();
|
|
470
392
|
addCollection(name);
|
|
@@ -474,7 +396,7 @@ async function main() {
|
|
|
474
396
|
await update(force);
|
|
475
397
|
return;
|
|
476
398
|
}
|
|
477
|
-
if (command === "list" && (resource === "hook" || resource === "skill"
|
|
399
|
+
if (command === "list" && (resource === "hook" || resource === "skill")) {
|
|
478
400
|
list(resource);
|
|
479
401
|
return;
|
|
480
402
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * toolkit — personal CLI for managing Claude Code hooks, skills, and commands.\n *\n * Commands:\n * toolkit add hook <name>\n * toolkit add skill <name> [--link <target>...]\n * toolkit add command <name>\n * toolkit add collections <name>\n * toolkit update [--force]\n * toolkit list hook\n * toolkit list skill\n * toolkit list command\n * toolkit list collections\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createInterface } from \"node:readline/promises\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nconst TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst HOOKS_SRC = join(TOOLKIT_ROOT, \"hooks\");\nconst SKILLS_SRC = join(TOOLKIT_ROOT, \"skills\");\nconst COMMANDS_SRC = join(TOOLKIT_ROOT, \"commands\");\nconst CONFIG_PATH = join(TOOLKIT_ROOT, \"config.json\");\n\nconst PROJECT_ROOT = process.cwd();\nconst CLAUDE_DIR = join(PROJECT_ROOT, \".claude\");\nconst TOOLKIT_DIR = join(PROJECT_ROOT, \".claude-toolkit\");\nconst MANIFEST_PATH = join(CLAUDE_DIR, \"toolkit-manifest.json\");\n\ntype HookEntry = { hash: string; installedAt: string };\ntype SkillEntry = { hash: string; installedAt: string; linkedTo: string[] };\ntype CommandEntry = { hash: string; installedAt: string };\ntype Manifest = {\n commands: Record<string, CommandEntry>;\n hooks: Record<string, HookEntry>;\n skills: Record<string, SkillEntry>;\n};\ntype CollectionItemKind = \"command\" | \"hook\" | \"skill\";\ntype CollectionItemConfig = {\n type: CollectionItemKind | `${CollectionItemKind}s`;\n src: string;\n};\ntype CollectionConfig = {\n name: string;\n items: CollectionItemConfig[];\n};\ntype ResolvedCollectionItem = {\n collection: string;\n sourcePath: string;\n sourceName: string;\n type: CollectionItemKind;\n};\n\n// ---------- helpers ----------\n\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction shortHash(content: string | Buffer): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 7);\n}\n\nfunction readManifest(): Manifest {\n if (!existsSync(MANIFEST_PATH)) {\n return { commands: {}, hooks: {}, skills: {} };\n }\n\n try {\n const parsed = JSON.parse(readFileSync(MANIFEST_PATH, \"utf8\")) as Partial<Manifest>;\n return {\n commands: parsed.commands ?? {},\n hooks: parsed.hooks ?? {},\n skills: parsed.skills ?? {},\n };\n } catch {\n return { commands: {}, hooks: {}, skills: {} };\n }\n}\n\nfunction writeManifest(m: Manifest): void {\n mkdirSync(CLAUDE_DIR, { recursive: true });\n writeFileSync(MANIFEST_PATH, JSON.stringify(m, null, 2) + \"\\n\");\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction deepMerge<T>(target: T, source: T): T {\n if (Array.isArray(target) && Array.isArray(source)) {\n return [...target, ...source] as T;\n }\n if (isPlainObject(target) && isPlainObject(source)) {\n const out: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(source)) {\n out[k] = k in out ? deepMerge(out[k], v) : v;\n }\n return out as T;\n }\n return source;\n}\n\nfunction hashCommandSource(name: string): string {\n const p = join(COMMANDS_SRC, `${name}.md`);\n return shortHash(readFileSync(p));\n}\n\nfunction hashHookSource(name: string): string {\n const p = join(HOOKS_SRC, name, \"hook.mjs\");\n return shortHash(readFileSync(p));\n}\n\nfunction hashSkillSource(name: string): string {\n const dir = join(SKILLS_SRC, name);\n const files = collectFiles(dir).sort();\n const h = createHash(\"sha256\");\n for (const f of files) {\n h.update(relative(dir, f));\n h.update(\"\\0\");\n h.update(readFileSync(f));\n h.update(\"\\0\");\n }\n return h.digest(\"hex\").slice(0, 7);\n}\n\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n if (!existsSync(dir)) {\n return out;\n }\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === \".gitkeep\") {\n continue;\n }\n\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.isFile()) {\n out.push(full);\n }\n }\n return out;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();\n rl.close();\n return answer === \"y\" || answer === \"yes\";\n}\n\nfunction diffLines(oldStr: string, newStr: string): string {\n const a = oldStr.split(\"\\n\");\n const b = newStr.split(\"\\n\");\n const out: string[] = [];\n const max = Math.max(a.length, b.length);\n for (let i = 0; i < max; i++) {\n if (a[i] === b[i]) {\n continue;\n }\n\n if (a[i] !== undefined) {\n out.push(`- ${a[i]}`);\n }\n\n if (b[i] !== undefined) {\n out.push(`+ ${b[i]}`);\n }\n }\n return out.join(\"\\n\");\n}\n\n// ---------- commands ----------\n\nfunction sanitizeName(name: string, kind: string): string {\n name = basename(name);\n if (!name) {\n console.error(`Invalid ${kind} name`);\n process.exit(1);\n }\n return name;\n}\n\nfunction normalizeCollectionItemType(\n type: CollectionItemConfig[\"type\"],\n collectionName: string,\n): CollectionItemKind {\n if (type === \"command\" || type === \"commands\") {\n return \"command\";\n }\n if (type === \"hook\" || type === \"hooks\") {\n return \"hook\";\n }\n if (type === \"skill\" || type === \"skills\") {\n return \"skill\";\n }\n\n throw new Error(`Collection \"${collectionName}\" has unsupported item type \"${type}\"`);\n}\n\nfunction resolveSourcePath(src: string, kind: string, collectionName: string): string {\n const sourcePath = resolve(TOOLKIT_ROOT, src);\n if (!sourcePath.startsWith(TOOLKIT_ROOT + sep)) {\n throw new Error(\n `Collection \"${collectionName}\" ${kind} source must stay within the toolkit root: ${src}`,\n );\n }\n return sourcePath;\n}\n\nfunction inferItemNameFromSource(\n type: CollectionItemKind,\n sourcePath: string,\n collectionName: string,\n): string {\n if (type === \"command\") {\n if (\n dirname(sourcePath) !== COMMANDS_SRC ||\n !sourcePath.startsWith(COMMANDS_SRC + sep) ||\n !sourcePath.endsWith(\".md\")\n ) {\n throw new Error(\n `Collection \"${collectionName}\" command source must point to a markdown file directly under commands/: ${relative(TOOLKIT_ROOT, sourcePath)}`,\n );\n }\n return basename(sourcePath, \".md\");\n }\n\n const expectedRoot = type === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (dirname(sourcePath) !== expectedRoot || !sourcePath.startsWith(expectedRoot + sep)) {\n throw new Error(\n `Collection \"${collectionName}\" ${type} source must point to a top-level entry under ${relative(TOOLKIT_ROOT, expectedRoot)}/: ${relative(TOOLKIT_ROOT, sourcePath)}`,\n );\n }\n\n return basename(sourcePath);\n}\n\nfunction readCollectionsConfig(): CollectionConfig[] {\n if (!existsSync(CONFIG_PATH)) {\n throw new Error(`Collections config not found: ${relative(TOOLKIT_ROOT, CONFIG_PATH)}`);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(CONFIG_PATH, \"utf8\"));\n } catch (error) {\n throw new Error(\n `Invalid collections config in ${relative(TOOLKIT_ROOT, CONFIG_PATH)}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n\n if (!Array.isArray(parsed)) {\n throw new Error(\"Collections config must be an array\");\n }\n\n const names = new Set<string>();\n return parsed.map((entry, index) => {\n if (!isPlainObject(entry)) {\n throw new Error(`Collection at index ${index} must be an object`);\n }\n\n const { name, items } = entry;\n if (typeof name !== \"string\" || name.trim().length === 0) {\n throw new Error(`Collection at index ${index} must have a non-empty name`);\n }\n if (names.has(name)) {\n throw new Error(`Duplicate collection name: ${name}`);\n }\n names.add(name);\n\n if (!Array.isArray(items)) {\n throw new Error(`Collection \"${name}\" must have an items array`);\n }\n\n const validatedItems = items.map((item, itemIndex) => {\n if (!isPlainObject(item)) {\n throw new Error(`Collection \"${name}\" item at index ${itemIndex} must be an object`);\n }\n if (typeof item.type !== \"string\" || item.type.trim().length === 0) {\n throw new Error(\n `Collection \"${name}\" item at index ${itemIndex} must have a non-empty type`,\n );\n }\n if (typeof item.src !== \"string\" || item.src.trim().length === 0) {\n throw new Error(\n `Collection \"${name}\" item at index ${itemIndex} must have a non-empty src`,\n );\n }\n\n return {\n type: item.type as CollectionItemConfig[\"type\"],\n src: item.src,\n };\n });\n\n return {\n name,\n items: validatedItems,\n };\n });\n}\n\nfunction resolveCollection(name: string): ResolvedCollectionItem[] {\n const collectionName = sanitizeName(name, \"collection\");\n const collections = readCollectionsConfig();\n const collection = collections.find((entry) => entry.name === collectionName);\n\n if (!collection) {\n throw new Error(`Collection not found: ${collectionName}`);\n }\n\n const deduped = new Map<string, ResolvedCollectionItem>();\n\n for (const item of collection.items) {\n const type = normalizeCollectionItemType(item.type, collection.name);\n const sourcePath = resolveSourcePath(item.src, type, collection.name);\n const sourceName = inferItemNameFromSource(type, sourcePath, collection.name);\n const key = `${type}:${sourceName}`;\n\n if (!deduped.has(key)) {\n deduped.set(key, {\n collection: collection.name,\n sourcePath,\n sourceName,\n type,\n });\n }\n }\n\n return [...deduped.values()];\n}\n\nfunction installCommand(name: string, src: string): void {\n if (!existsSync(src)) {\n console.error(`Command not found: ${name}`);\n process.exit(1);\n }\n\n const commandsDir = join(CLAUDE_DIR, \"commands\");\n mkdirSync(commandsDir, { recursive: true });\n const dest = resolve(commandsDir, `${name}.md`);\n if (!dest.startsWith(commandsDir + sep)) {\n console.error(\"Invalid command name\");\n process.exit(1);\n }\n writeFileSync(dest, readFileSync(src));\n\n const manifest = readManifest();\n manifest.commands[name] = {\n hash: hashCommandSource(name),\n installedAt: today(),\n };\n writeManifest(manifest);\n\n console.log(`Installed command: ${name} → ${relative(PROJECT_ROOT, dest)}`);\n}\n\nfunction addCommand(name: string): void {\n name = sanitizeName(name, \"command\");\n installCommand(name, join(COMMANDS_SRC, `${name}.md`));\n}\n\nfunction installHook(name: string, srcDir: string): void {\n if (!existsSync(srcDir)) {\n console.error(`Hook not found: ${name}`);\n process.exit(1);\n }\n\n const hookSrc = join(srcDir, \"hook.mjs\");\n const fragmentPath = join(srcDir, \"settings-fragment.json\");\n\n const hooksDir = join(CLAUDE_DIR, \"hooks\");\n mkdirSync(hooksDir, { recursive: true });\n const destHook = resolve(hooksDir, `${name}.mjs`);\n if (!destHook.startsWith(hooksDir + sep)) {\n console.error(\"Invalid hook name\");\n process.exit(1);\n }\n writeFileSync(destHook, readFileSync(hookSrc));\n\n if (existsSync(fragmentPath)) {\n const fragment = JSON.parse(readFileSync(fragmentPath, \"utf8\"));\n const settingsPath = join(CLAUDE_DIR, \"settings.json\");\n const current = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, \"utf8\")) : {};\n const merged = deepMerge(current, fragment);\n writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + \"\\n\");\n }\n\n const manifest = readManifest();\n manifest.hooks[name] = { hash: hashHookSource(name), installedAt: today() };\n writeManifest(manifest);\n\n console.log(`Installed hook: ${name} → ${relative(PROJECT_ROOT, destHook)}`);\n}\n\nfunction addHook(name: string): void {\n name = sanitizeName(name, \"hook\");\n installHook(name, join(HOOKS_SRC, name));\n}\n\nfunction installSkill(name: string, srcDir: string, links: string[]): void {\n if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {\n console.error(`Skill not found: ${name}`);\n process.exit(1);\n }\n\n const destDir = resolve(TOOLKIT_DIR, \"skills\", name);\n if (!destDir.startsWith(join(TOOLKIT_DIR, \"skills\") + sep)) {\n console.error(\"Invalid skill name\");\n process.exit(1);\n }\n mkdirSync(dirname(destDir), { recursive: true });\n cpSync(srcDir, destDir, { recursive: true });\n\n const resolvedLinks = links.length > 0 ? links : [join(\".claude\", \"skills\")];\n for (const link of resolvedLinks) {\n const linkDir = resolve(PROJECT_ROOT, link);\n mkdirSync(linkDir, { recursive: true });\n\n const linkPath = join(linkDir, name);\n if (existsSync(linkPath) || lstatExists(linkPath)) {\n unlinkSync(linkPath);\n }\n\n const relTarget = relative(linkDir, destDir);\n symlinkSync(relTarget, linkPath, \"dir\");\n }\n\n const manifest = readManifest();\n manifest.skills[name] = {\n hash: hashSkillSource(name),\n installedAt: today(),\n linkedTo: resolvedLinks,\n };\n writeManifest(manifest);\n\n console.log(`Installed skill: ${name} → ${relative(PROJECT_ROOT, destDir)}`);\n for (const l of resolvedLinks) {\n console.log(` linked: ${join(l, name)}`);\n }\n}\n\nfunction addSkill(name: string, links: string[]): void {\n name = sanitizeName(name, \"skill\");\n installSkill(name, join(SKILLS_SRC, name), links);\n}\n\nfunction addCollection(name: string): void {\n const items = resolveCollection(name);\n for (const item of items) {\n if (!existsSync(item.sourcePath)) {\n throw new Error(\n `Collection \"${item.collection}\" references missing ${item.type} source: ${relative(TOOLKIT_ROOT, item.sourcePath)}`,\n );\n }\n\n const itemStats = statSync(item.sourcePath);\n const actualKind = itemStats.isFile()\n ? \"file\"\n : itemStats.isDirectory()\n ? \"directory\"\n : \"other\";\n\n if (item.type === \"command\") {\n if (!itemStats.isFile()) {\n throw new Error(\n `Collection \"${item.collection}\" expected command source \"${item.sourcePath}\" to be a file, found ${actualKind}`,\n );\n }\n installCommand(item.sourceName, item.sourcePath);\n continue;\n }\n\n if (item.type === \"hook\") {\n if (!itemStats.isDirectory()) {\n throw new Error(\n `Collection \"${item.collection}\" expected hook source \"${item.sourcePath}\" to be a directory, found ${actualKind}`,\n );\n }\n installHook(item.sourceName, item.sourcePath);\n continue;\n }\n\n if (!itemStats.isDirectory()) {\n throw new Error(\n `Collection \"${item.collection}\" expected skill source \"${item.sourcePath}\" to be a directory, found ${actualKind}`,\n );\n }\n installSkill(item.sourceName, item.sourcePath, []);\n }\n}\n\nfunction lstatExists(p: string): boolean {\n try {\n lstatSync(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function update(force: boolean): Promise<void> {\n const manifest = readManifest();\n let changed = false;\n\n for (const [name, entry] of Object.entries(manifest.hooks)) {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashHookSource(name);\n const installedPath = join(CLAUDE_DIR, \"hooks\", `${name}.mjs`);\n const installedHash = existsSync(installedPath) ? shortHash(readFileSync(installedPath)) : null;\n\n const sourceChanged = sourceHash !== entry.hash;\n const locallyModified = installedHash !== null && installedHash !== entry.hash;\n\n if (!sourceChanged && !locallyModified) {\n continue;\n }\n\n changed = true;\n\n if (locallyModified && !force) {\n console.warn(\n `! hook \"${name}\" was modified locally (installed=${installedHash}, manifest=${entry.hash}). Use --force to overwrite.`,\n );\n continue;\n }\n\n if (sourceChanged) {\n const oldSrc = existsSync(installedPath) ? readFileSync(installedPath, \"utf8\") : \"\";\n const newSrc = readFileSync(join(srcDir, \"hook.mjs\"), \"utf8\");\n console.log(`\\n~ hook: ${name} (${entry.hash} → ${sourceHash})`);\n console.log(diffLines(oldSrc, newSrc));\n const ok = force || (await confirm(`Update hook \"${name}\"?`));\n\n if (!ok) {\n continue;\n }\n\n writeFileSync(installedPath, newSrc);\n manifest.hooks[name] = { hash: sourceHash, installedAt: today() };\n }\n }\n\n for (const [name, entry] of Object.entries(manifest.skills)) {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashSkillSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ skill: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update skill \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n cpSync(srcDir, destDir, { recursive: true, force: true });\n manifest.skills[name] = {\n hash: sourceHash,\n installedAt: today(),\n linkedTo: entry.linkedTo,\n };\n }\n\n for (const [name, entry] of Object.entries(manifest.commands)) {\n const src = join(COMMANDS_SRC, `${name}.md`);\n if (!existsSync(src)) {\n continue;\n }\n\n const sourceHash = hashCommandSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ command: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update command \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const dest = join(CLAUDE_DIR, \"commands\", `${name}.md`);\n writeFileSync(dest, readFileSync(src));\n manifest.commands[name] = { hash: sourceHash, installedAt: today() };\n }\n\n if (changed) {\n writeManifest(manifest);\n }\n}\n\nfunction list(kind: \"hook\" | \"skill\" | \"command\"): void {\n if (kind === \"command\") {\n if (!existsSync(COMMANDS_SRC)) {\n console.log(\"(no commands available)\");\n return;\n }\n const files = readdirSync(COMMANDS_SRC)\n .filter((f) => f.endsWith(\".md\"))\n .map((f) => f.replace(/\\.md$/, \"\"));\n if (files.length === 0) {\n console.log(\"(no commands available)\");\n return;\n }\n for (const name of files) {\n console.log(`${name} ${hashCommandSource(name)}`);\n }\n return;\n }\n\n const dir = kind === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (!existsSync(dir)) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n const entries = readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() || (kind === \"skill\" && e.isSymbolicLink()))\n .map((e) => e.name);\n\n if (entries.length === 0) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n\n for (const name of entries) {\n const hash = kind === \"hook\" ? hashHookSource(name) : hashSkillSource(name);\n console.log(`${name} ${hash}`);\n }\n}\n\nfunction listCollections(): void {\n const collections = readCollectionsConfig();\n if (collections.length === 0) {\n console.log(\"(no collections available)\");\n return;\n }\n\n for (const collection of collections) {\n console.log(`${collection.name} ${collection.items.length} item(s)`);\n }\n}\n\n// ---------- argv ----------\n\nfunction usage(): never {\n console.error(\n `Usage:\n toolkit add hook <name>\n toolkit add skill <name> [--link <target>]...\n toolkit add command <name>\n toolkit add collections <name>\n toolkit update [--force]\n toolkit list hook\n toolkit list skill\n toolkit list command\n toolkit list collections`,\n );\n process.exit(1);\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n options: {\n force: {\n default: false,\n type: \"boolean\",\n },\n links: {\n multiple: true,\n type: \"string\",\n },\n },\n allowPositionals: true,\n });\n\n const { force, links } = values;\n const [command, resource, name] = positionals;\n\n if (command === \"add\" && resource === \"hook\") {\n if (!name) {\n usage();\n }\n\n addHook(name);\n return;\n }\n\n if (command === \"add\" && resource === \"skill\") {\n if (!name) {\n usage();\n }\n\n addSkill(name, links ? links : []);\n return;\n }\n\n if (command === \"add\" && resource === \"command\") {\n if (!name) {\n usage();\n }\n\n addCommand(name);\n return;\n }\n\n if (command === \"add\" && (resource === \"collection\" || resource === \"collections\")) {\n if (!name) {\n usage();\n }\n\n addCollection(name);\n return;\n }\n\n if (command === \"update\") {\n await update(force);\n return;\n }\n\n if (\n command === \"list\" &&\n (resource === \"hook\" || resource === \"skill\" || resource === \"command\")\n ) {\n list(resource as \"hook\" | \"skill\" | \"command\");\n return;\n }\n\n if (command === \"list\" && (resource === \"collection\" || resource === \"collections\")) {\n listCollections();\n return;\n }\n\n usage();\n}\n\nmain().catch((err) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * toolkit — personal CLI for managing Claude Code hooks and skills.\n *\n * Commands:\n * toolkit add hook <name>\n * toolkit add skill <name> [--link <target>...]\n * toolkit add collections <name>\n * toolkit update [--force]\n * toolkit list hook\n * toolkit list skill\n * toolkit list collections\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createInterface } from \"node:readline/promises\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nconst TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst HOOKS_SRC = join(TOOLKIT_ROOT, \"hooks\");\nconst SKILLS_SRC = join(TOOLKIT_ROOT, \"skills\");\nconst CONFIG_PATH = join(TOOLKIT_ROOT, \"config.json\");\n\nconst PROJECT_ROOT = process.cwd();\nconst CLAUDE_DIR = join(PROJECT_ROOT, \".claude\");\nconst TOOLKIT_DIR = join(PROJECT_ROOT, \".claude-toolkit\");\nconst MANIFEST_PATH = join(CLAUDE_DIR, \"toolkit-manifest.json\");\n\ntype HookEntry = { hash: string; installedAt: string };\ntype SkillEntry = { hash: string; installedAt: string; linkedTo: string[] };\ntype Manifest = {\n hooks: Record<string, HookEntry>;\n skills: Record<string, SkillEntry>;\n};\ntype CollectionItemKind = \"hook\" | \"skill\";\ntype CollectionItemConfig = {\n type: CollectionItemKind | `${CollectionItemKind}s`;\n src: string;\n};\ntype CollectionConfig = {\n name: string;\n items: CollectionItemConfig[];\n};\ntype ResolvedCollectionItem = {\n collection: string;\n sourcePath: string;\n sourceName: string;\n type: CollectionItemKind;\n};\n\n// ---------- helpers ----------\n\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction shortHash(content: string | Buffer): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 7);\n}\n\nfunction readManifest(): Manifest {\n if (!existsSync(MANIFEST_PATH)) {\n return { hooks: {}, skills: {} };\n }\n\n try {\n const parsed = JSON.parse(readFileSync(MANIFEST_PATH, \"utf8\")) as Partial<Manifest>;\n return {\n hooks: parsed.hooks ?? {},\n skills: parsed.skills ?? {},\n };\n } catch {\n return { hooks: {}, skills: {} };\n }\n}\n\nfunction writeManifest(m: Manifest): void {\n mkdirSync(CLAUDE_DIR, { recursive: true });\n writeFileSync(MANIFEST_PATH, JSON.stringify(m, null, 2) + \"\\n\");\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction deepMerge<T>(target: T, source: T): T {\n if (Array.isArray(target) && Array.isArray(source)) {\n return [...target, ...source] as T;\n }\n if (isPlainObject(target) && isPlainObject(source)) {\n const out: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(source)) {\n out[k] = k in out ? deepMerge(out[k], v) : v;\n }\n return out as T;\n }\n return source;\n}\n\nfunction hashHookSource(name: string): string {\n const p = join(HOOKS_SRC, name, \"hook.mjs\");\n return shortHash(readFileSync(p));\n}\n\nfunction hashSkillSource(name: string): string {\n const dir = join(SKILLS_SRC, name);\n const files = collectFiles(dir).sort();\n const h = createHash(\"sha256\");\n for (const f of files) {\n h.update(relative(dir, f));\n h.update(\"\\0\");\n h.update(readFileSync(f));\n h.update(\"\\0\");\n }\n return h.digest(\"hex\").slice(0, 7);\n}\n\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n if (!existsSync(dir)) {\n return out;\n }\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === \".gitkeep\") {\n continue;\n }\n\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.isFile()) {\n out.push(full);\n }\n }\n return out;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();\n rl.close();\n return answer === \"y\" || answer === \"yes\";\n}\n\nfunction diffLines(oldStr: string, newStr: string): string {\n const a = oldStr.split(\"\\n\");\n const b = newStr.split(\"\\n\");\n const out: string[] = [];\n const max = Math.max(a.length, b.length);\n for (let i = 0; i < max; i++) {\n if (a[i] === b[i]) {\n continue;\n }\n\n if (a[i] !== undefined) {\n out.push(`- ${a[i]}`);\n }\n\n if (b[i] !== undefined) {\n out.push(`+ ${b[i]}`);\n }\n }\n return out.join(\"\\n\");\n}\n\n// ---------- resources ----------\n\nfunction sanitizeName(name: string, kind: string): string {\n name = basename(name);\n if (!name) {\n console.error(`Invalid ${kind} name`);\n process.exit(1);\n }\n return name;\n}\n\nfunction normalizeCollectionItemType(\n type: CollectionItemConfig[\"type\"],\n collectionName: string,\n): CollectionItemKind {\n if (type === \"hook\" || type === \"hooks\") {\n return \"hook\";\n }\n if (type === \"skill\" || type === \"skills\") {\n return \"skill\";\n }\n\n throw new Error(`Collection \"${collectionName}\" has unsupported item type \"${type}\"`);\n}\n\nfunction resolveSourcePath(src: string, kind: string, collectionName: string): string {\n const sourcePath = resolve(TOOLKIT_ROOT, src);\n if (!sourcePath.startsWith(TOOLKIT_ROOT + sep)) {\n throw new Error(\n `Collection \"${collectionName}\" ${kind} source must stay within the toolkit root: ${src}`,\n );\n }\n return sourcePath;\n}\n\nfunction inferItemNameFromSource(\n type: CollectionItemKind,\n sourcePath: string,\n collectionName: string,\n): string {\n const expectedRoot = type === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (dirname(sourcePath) !== expectedRoot || !sourcePath.startsWith(expectedRoot + sep)) {\n throw new Error(\n `Collection \"${collectionName}\" ${type} source must point to a top-level entry under ${relative(TOOLKIT_ROOT, expectedRoot)}/: ${relative(TOOLKIT_ROOT, sourcePath)}`,\n );\n }\n\n return basename(sourcePath);\n}\n\nfunction readCollectionsConfig(): CollectionConfig[] {\n if (!existsSync(CONFIG_PATH)) {\n throw new Error(`Collections config not found: ${relative(TOOLKIT_ROOT, CONFIG_PATH)}`);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(CONFIG_PATH, \"utf8\"));\n } catch (error) {\n throw new Error(\n `Invalid collections config in ${relative(TOOLKIT_ROOT, CONFIG_PATH)}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n\n if (!Array.isArray(parsed)) {\n throw new Error(\"Collections config must be an array\");\n }\n\n const names = new Set<string>();\n return parsed.map((entry, index) => {\n if (!isPlainObject(entry)) {\n throw new Error(`Collection at index ${index} must be an object`);\n }\n\n const { name, items } = entry;\n if (typeof name !== \"string\" || name.trim().length === 0) {\n throw new Error(`Collection at index ${index} must have a non-empty name`);\n }\n if (names.has(name)) {\n throw new Error(`Duplicate collection name: ${name}`);\n }\n names.add(name);\n\n if (!Array.isArray(items)) {\n throw new Error(`Collection \"${name}\" must have an items array`);\n }\n\n const validatedItems = items.map((item, itemIndex) => {\n if (!isPlainObject(item)) {\n throw new Error(`Collection \"${name}\" item at index ${itemIndex} must be an object`);\n }\n if (typeof item.type !== \"string\" || item.type.trim().length === 0) {\n throw new Error(\n `Collection \"${name}\" item at index ${itemIndex} must have a non-empty type`,\n );\n }\n if (typeof item.src !== \"string\" || item.src.trim().length === 0) {\n throw new Error(\n `Collection \"${name}\" item at index ${itemIndex} must have a non-empty src`,\n );\n }\n\n return {\n type: item.type as CollectionItemConfig[\"type\"],\n src: item.src,\n };\n });\n\n return {\n name,\n items: validatedItems,\n };\n });\n}\n\nfunction resolveCollection(name: string): ResolvedCollectionItem[] {\n const collectionName = sanitizeName(name, \"collection\");\n const collections = readCollectionsConfig();\n const collection = collections.find((entry) => entry.name === collectionName);\n\n if (!collection) {\n throw new Error(`Collection not found: ${collectionName}`);\n }\n\n const deduped = new Map<string, ResolvedCollectionItem>();\n\n for (const item of collection.items) {\n const type = normalizeCollectionItemType(item.type, collection.name);\n const sourcePath = resolveSourcePath(item.src, type, collection.name);\n const sourceName = inferItemNameFromSource(type, sourcePath, collection.name);\n const key = `${type}:${sourceName}`;\n\n if (!deduped.has(key)) {\n deduped.set(key, {\n collection: collection.name,\n sourcePath,\n sourceName,\n type,\n });\n }\n }\n\n return [...deduped.values()];\n}\n\nfunction installHook(name: string, srcDir: string): void {\n if (!existsSync(srcDir)) {\n console.error(`Hook not found: ${name}`);\n process.exit(1);\n }\n\n const hookSrc = join(srcDir, \"hook.mjs\");\n const fragmentPath = join(srcDir, \"settings-fragment.json\");\n\n const hooksDir = join(CLAUDE_DIR, \"hooks\");\n mkdirSync(hooksDir, { recursive: true });\n const destHook = resolve(hooksDir, `${name}.mjs`);\n if (!destHook.startsWith(hooksDir + sep)) {\n console.error(\"Invalid hook name\");\n process.exit(1);\n }\n writeFileSync(destHook, readFileSync(hookSrc));\n\n if (existsSync(fragmentPath)) {\n const fragment = JSON.parse(readFileSync(fragmentPath, \"utf8\"));\n const settingsPath = join(CLAUDE_DIR, \"settings.json\");\n const current = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, \"utf8\")) : {};\n const merged = deepMerge(current, fragment);\n writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + \"\\n\");\n }\n\n const manifest = readManifest();\n manifest.hooks[name] = { hash: hashHookSource(name), installedAt: today() };\n writeManifest(manifest);\n\n console.log(`Installed hook: ${name} → ${relative(PROJECT_ROOT, destHook)}`);\n}\n\nfunction addHook(name: string): void {\n name = sanitizeName(name, \"hook\");\n installHook(name, join(HOOKS_SRC, name));\n}\n\nfunction installSkill(name: string, srcDir: string, links: string[]): void {\n if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {\n console.error(`Skill not found: ${name}`);\n process.exit(1);\n }\n\n const destDir = resolve(TOOLKIT_DIR, \"skills\", name);\n if (!destDir.startsWith(join(TOOLKIT_DIR, \"skills\") + sep)) {\n console.error(\"Invalid skill name\");\n process.exit(1);\n }\n mkdirSync(dirname(destDir), { recursive: true });\n cpSync(srcDir, destDir, { recursive: true });\n\n const resolvedLinks = links.length > 0 ? links : [join(\".claude\", \"skills\")];\n for (const link of resolvedLinks) {\n const linkDir = resolve(PROJECT_ROOT, link);\n mkdirSync(linkDir, { recursive: true });\n\n const linkPath = join(linkDir, name);\n if (existsSync(linkPath) || lstatExists(linkPath)) {\n unlinkSync(linkPath);\n }\n\n const relTarget = relative(linkDir, destDir);\n symlinkSync(relTarget, linkPath, \"dir\");\n }\n\n const manifest = readManifest();\n manifest.skills[name] = {\n hash: hashSkillSource(name),\n installedAt: today(),\n linkedTo: resolvedLinks,\n };\n writeManifest(manifest);\n\n console.log(`Installed skill: ${name} → ${relative(PROJECT_ROOT, destDir)}`);\n for (const l of resolvedLinks) {\n console.log(` linked: ${join(l, name)}`);\n }\n}\n\nfunction addSkill(name: string, links: string[]): void {\n name = sanitizeName(name, \"skill\");\n installSkill(name, join(SKILLS_SRC, name), links);\n}\n\nfunction addCollection(name: string): void {\n const items = resolveCollection(name);\n for (const item of items) {\n if (!existsSync(item.sourcePath)) {\n throw new Error(\n `Collection \"${item.collection}\" references missing ${item.type} source: ${relative(TOOLKIT_ROOT, item.sourcePath)}`,\n );\n }\n\n const itemStats = statSync(item.sourcePath);\n const actualKind = itemStats.isFile()\n ? \"file\"\n : itemStats.isDirectory()\n ? \"directory\"\n : \"other\";\n\n if (item.type === \"hook\") {\n if (!itemStats.isDirectory()) {\n throw new Error(\n `Collection \"${item.collection}\" expected hook source \"${item.sourcePath}\" to be a directory, found ${actualKind}`,\n );\n }\n installHook(item.sourceName, item.sourcePath);\n continue;\n }\n\n if (!itemStats.isDirectory()) {\n throw new Error(\n `Collection \"${item.collection}\" expected skill source \"${item.sourcePath}\" to be a directory, found ${actualKind}`,\n );\n }\n installSkill(item.sourceName, item.sourcePath, []);\n }\n}\n\nfunction lstatExists(p: string): boolean {\n try {\n lstatSync(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function update(force: boolean): Promise<void> {\n const manifest = readManifest();\n let changed = false;\n\n for (const [name, entry] of Object.entries(manifest.hooks)) {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashHookSource(name);\n const installedPath = join(CLAUDE_DIR, \"hooks\", `${name}.mjs`);\n const installedHash = existsSync(installedPath) ? shortHash(readFileSync(installedPath)) : null;\n\n const sourceChanged = sourceHash !== entry.hash;\n const locallyModified = installedHash !== null && installedHash !== entry.hash;\n\n if (!sourceChanged && !locallyModified) {\n continue;\n }\n\n changed = true;\n\n if (locallyModified && !force) {\n console.warn(\n `! hook \"${name}\" was modified locally (installed=${installedHash}, manifest=${entry.hash}). Use --force to overwrite.`,\n );\n continue;\n }\n\n if (sourceChanged) {\n const oldSrc = existsSync(installedPath) ? readFileSync(installedPath, \"utf8\") : \"\";\n const newSrc = readFileSync(join(srcDir, \"hook.mjs\"), \"utf8\");\n console.log(`\\n~ hook: ${name} (${entry.hash} → ${sourceHash})`);\n console.log(diffLines(oldSrc, newSrc));\n const ok = force || (await confirm(`Update hook \"${name}\"?`));\n\n if (!ok) {\n continue;\n }\n\n writeFileSync(installedPath, newSrc);\n manifest.hooks[name] = { hash: sourceHash, installedAt: today() };\n }\n }\n\n for (const [name, entry] of Object.entries(manifest.skills)) {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashSkillSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ skill: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update skill \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n cpSync(srcDir, destDir, { recursive: true, force: true });\n manifest.skills[name] = {\n hash: sourceHash,\n installedAt: today(),\n linkedTo: entry.linkedTo,\n };\n }\n\n if (changed) {\n writeManifest(manifest);\n }\n}\n\nfunction list(kind: \"hook\" | \"skill\"): void {\n const dir = kind === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (!existsSync(dir)) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n const entries = readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() || (kind === \"skill\" && e.isSymbolicLink()))\n .map((e) => e.name);\n\n if (entries.length === 0) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n\n for (const name of entries) {\n const hash = kind === \"hook\" ? hashHookSource(name) : hashSkillSource(name);\n console.log(`${name} ${hash}`);\n }\n}\n\nfunction listCollections(): void {\n const collections = readCollectionsConfig();\n if (collections.length === 0) {\n console.log(\"(no collections available)\");\n return;\n }\n\n for (const collection of collections) {\n console.log(`${collection.name} ${collection.items.length} item(s)`);\n }\n}\n\n// ---------- argv ----------\n\nfunction usage(): never {\n console.error(\n `Usage:\n toolkit add hook <name>\n toolkit add skill <name> [--link <target>]...\n toolkit add collections <name>\n toolkit update [--force]\n toolkit list hook\n toolkit list skill\n toolkit list collections`,\n );\n process.exit(1);\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n options: {\n force: {\n default: false,\n type: \"boolean\",\n },\n links: {\n multiple: true,\n type: \"string\",\n },\n },\n allowPositionals: true,\n });\n\n const { force, links } = values;\n const [command, resource, name] = positionals;\n\n if (command === \"add\" && resource === \"hook\") {\n if (!name) {\n usage();\n }\n\n addHook(name);\n return;\n }\n\n if (command === \"add\" && resource === \"skill\") {\n if (!name) {\n usage();\n }\n\n addSkill(name, links ? links : []);\n return;\n }\n\n if (command === \"add\" && (resource === \"collection\" || resource === \"collections\")) {\n if (!name) {\n usage();\n }\n\n addCollection(name);\n return;\n }\n\n if (command === \"update\") {\n await update(force);\n return;\n }\n\n if (command === \"list\" && (resource === \"hook\" || resource === \"skill\")) {\n list(resource as \"hook\" | \"skill\");\n return;\n }\n\n if (command === \"list\" && (resource === \"collection\" || resource === \"collections\")) {\n listCollections();\n return;\n }\n\n usage();\n}\n\nmain().catch((err) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schalkneethling/toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "CLI for managing Claude Code hooks and skills across projects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
7
|
"toolkit": "./dist/index.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"commands",
|
|
11
10
|
"config.json",
|
|
12
11
|
"dist",
|
|
13
12
|
"hooks",
|
|
@@ -20,14 +19,16 @@
|
|
|
20
19
|
"publishConfig": {
|
|
21
20
|
"access": "public"
|
|
22
21
|
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"toolkit": "tsx src/index.ts",
|
|
24
|
+
"prepare": "vp pack && vp run build:hooks",
|
|
25
|
+
"build:hooks": "tsc --project tsconfig.hooks.json"
|
|
26
|
+
},
|
|
23
27
|
"devDependencies": {
|
|
24
28
|
"@types/node": "^25.6.0",
|
|
25
29
|
"tsx": "^4.21.0",
|
|
26
30
|
"typescript": "^6.0.2",
|
|
27
31
|
"vite-plus": "^0.1.18"
|
|
28
32
|
},
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
"build:hooks": "tsc --project tsconfig.hooks.json"
|
|
32
|
-
}
|
|
33
|
-
}
|
|
33
|
+
"packageManager": "pnpm@11.1.1"
|
|
34
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-review
|
|
3
|
+
description: Review code changes for correctness, security, performance, accessibility, maintainability, tests, dependencies, design-system adherence, and localization. Use when the user asks for a code review, PR review, review of local changes, risk assessment, code quality feedback, or actionable findings before merge.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Code Review
|
|
8
|
+
|
|
9
|
+
Review code to catch issues the original engineer may have missed and to improve the codebase without creating unnecessary friction. Prioritize real risks over style noise.
|
|
10
|
+
|
|
11
|
+
## Review Workflow
|
|
12
|
+
|
|
13
|
+
1. Determine the review scope:
|
|
14
|
+
- Inspect `git status`, the relevant diff, and any user-specified files or PR context.
|
|
15
|
+
- Preserve unrelated user changes. Do not modify code unless the user asks for fixes.
|
|
16
|
+
- Identify whether the change is frontend, backend, library, CLI, infrastructure, docs, or mixed.
|
|
17
|
+
2. Check project context before judging:
|
|
18
|
+
- Frameworks and languages.
|
|
19
|
+
- Test tooling and CI coverage.
|
|
20
|
+
- Linting, formatting, type checking, and build scripts.
|
|
21
|
+
- Dependency management and lockfiles.
|
|
22
|
+
- Accessibility, localization, and design-system tooling when UI code is involved.
|
|
23
|
+
3. Review the change for material issues:
|
|
24
|
+
- Correctness: broken behavior, edge cases, data loss, error handling, and regressions.
|
|
25
|
+
- Security: injection risks, unsafe auth, secret handling, dependency risk, and permission scope.
|
|
26
|
+
- Performance and resources: unnecessary CPU, memory, network use, leaks, race conditions, deadlocks, async coordination, and missing back-pressure or throttling.
|
|
27
|
+
- Accessibility: semantic HTML, labels, keyboard support, focus management, ARIA misuse, and contrast.
|
|
28
|
+
- Maintainability: naming, structure, type safety, readability, duplication, and fit with local patterns.
|
|
29
|
+
- Tests and docs: missing coverage for new behavior, insufficient regression tests, and stale public docs.
|
|
30
|
+
- Dependencies: added packages, bundle/runtime impact, maintenance state, security posture, and whether built-in or existing project utilities would be enough.
|
|
31
|
+
- Design systems and branding: token usage, component reuse, theme consistency, and justified deviations.
|
|
32
|
+
- Localization: hard-coded UI strings, date/number formatting, translation keys, and future translation workflow.
|
|
33
|
+
4. Produce review feedback:
|
|
34
|
+
- Lead with findings, ordered by severity.
|
|
35
|
+
- Include file and line references for repo-local issues.
|
|
36
|
+
- Explain impact and give a concrete fix.
|
|
37
|
+
- Group repeated instances when one root cause explains them.
|
|
38
|
+
- Mark non-blocking refactors as follow-up suggestions, not merge blockers.
|
|
39
|
+
- If no issues are found, say so clearly and mention any residual test or tooling gaps.
|
|
40
|
+
|
|
41
|
+
## Setup Deficiencies
|
|
42
|
+
|
|
43
|
+
If essential review infrastructure is missing, call it out early before deep findings:
|
|
44
|
+
|
|
45
|
+
- No runnable tests or tests absent from CI.
|
|
46
|
+
- No linting, formatting, type checking, or build validation for the changed area.
|
|
47
|
+
- Missing lockfile or unpinned dependencies.
|
|
48
|
+
- No dependency or supply-chain scanning for publishable/server code.
|
|
49
|
+
- No accessibility checks for UI-heavy changes.
|
|
50
|
+
- No i18n framework or translation process for localized UI.
|
|
51
|
+
- Missing design-system tokens/components when the project clearly depends on them.
|
|
52
|
+
|
|
53
|
+
When a setup deficiency would make the review noisy or unreliable, report the deficiency as the primary finding and then provide only the highest-confidence code findings.
|
|
54
|
+
|
|
55
|
+
## Feedback Standards
|
|
56
|
+
|
|
57
|
+
- Be direct, specific, and respectful. Focus on code and impact, never the author.
|
|
58
|
+
- Avoid nitpicks that an existing formatter or linter should handle.
|
|
59
|
+
- Prefer established local helpers, components, patterns, and style systems over new abstractions.
|
|
60
|
+
- Flag redundant implementations when the project already has an equivalent helper, component, CSS pattern, or service.
|
|
61
|
+
- Challenge clever but opaque code. Prefer readable control flow and well-named helpers.
|
|
62
|
+
- Encourage comments only for non-obvious decisions, tradeoffs, constraints, or nuanced behavior. Discourage comments that restate the code.
|
|
63
|
+
- Include positive feedback after findings when something is genuinely strong, such as clean tests, simple abstractions, or thoughtful design.
|
|
64
|
+
|
|
65
|
+
## Severity Guide
|
|
66
|
+
|
|
67
|
+
- `P0`: Must fix immediately. Security exploit, data loss, severe outage, or merge-blocking broken core behavior.
|
|
68
|
+
- `P1`: Should fix before merge. Likely bug, serious regression, accessibility blocker, unsafe dependency/auth pattern, or missing critical test.
|
|
69
|
+
- `P2`: Important but may be follow-up. Maintainability issue, incomplete edge coverage, performance concern, duplicate implementation, or design-system drift.
|
|
70
|
+
- `P3`: Optional improvement. Clarity, small refactor, documentation polish, or non-blocking suggestion.
|
|
71
|
+
|
|
72
|
+
## Output Shape
|
|
73
|
+
|
|
74
|
+
Use this order for review responses:
|
|
75
|
+
|
|
76
|
+
1. Findings, ordered by severity, with `file:line`.
|
|
77
|
+
2. Open questions or assumptions.
|
|
78
|
+
3. Brief positive notes, if useful.
|
|
79
|
+
4. Validation performed or not performed.
|
|
80
|
+
|
|
81
|
+
Keep summaries short. The findings are the review.
|
|
@@ -43,9 +43,6 @@ updates:
|
|
|
43
43
|
interval: "daily"
|
|
44
44
|
cooldown:
|
|
45
45
|
default-days: 7
|
|
46
|
-
semver-major-days: 7
|
|
47
|
-
semver-minor-days: 3
|
|
48
|
-
semver-patch-days: 2
|
|
49
46
|
include:
|
|
50
47
|
- "*"
|
|
51
48
|
```
|
|
@@ -104,6 +101,7 @@ without repeating every field verbatim.
|
|
|
104
101
|
## Constraints
|
|
105
102
|
|
|
106
103
|
- **Never omit the `cooldown` block** from any ecosystem entry.
|
|
104
|
+
- **`semver-*` cooldown fields are only valid for package managers that use semver** (e.g. `npm`, `pip`, `cargo`). Do not include `semver-major-days`, `semver-minor-days`, or `semver-patch-days` for `github-actions` or any other non-semver ecosystem — Dependabot will reject the config.
|
|
107
105
|
- **Never change the canonical values** unless the user explicitly requests it and
|
|
108
106
|
provides a reason (e.g. a monorepo with a stricter release cadence).
|
|
109
107
|
- **Always include `github-actions`** as an ecosystem, even if the user only asked
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: npm-package-publishing
|
|
3
3
|
description: >
|
|
4
|
-
|
|
5
|
-
publishing
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
Audit and improve the security posture of npm package publishing: account security, npm trusted
|
|
5
|
+
publishing strategy, GitHub repository hardening, token removal, release governance, dependency
|
|
6
|
+
update policy, provenance, and supply-chain risk. Use when the user asks about npm publishing best
|
|
7
|
+
practices, publishing security, OIDC/trusted publishing strategy, npm token hygiene, release
|
|
8
|
+
automation posture, Changesets versus changelog strategies, publint, provenance, or auditing an
|
|
9
|
+
existing publishing pipeline. For writing or debugging the concrete GitHub Actions publish
|
|
10
|
+
workflow file, use the npm-trusted-publishing-github-workflow skill instead.
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# npm Package Publishing — Best Practices
|
|
@@ -14,8 +15,66 @@ description: >
|
|
|
14
15
|
Based on the [e18e publishing guide](https://e18e.dev/docs/publishing.html). Reference it for
|
|
15
16
|
the canonical source; this skill distils the actionable steps.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
## Agent Workflow
|
|
19
|
+
|
|
20
|
+
1. Inspect the repository before recommending changes:
|
|
21
|
+
- `package.json`
|
|
22
|
+
- lockfiles and `packageManager`
|
|
23
|
+
- `.npmrc`, `.yarnrc.yml`, `.github/workflows/*`
|
|
24
|
+
- release tooling such as Changesets, changelogithub, semantic-release, or release-it
|
|
25
|
+
- existing npm/GitHub tokens or `NODE_AUTH_TOKEN` usage in workflows
|
|
26
|
+
2. Classify the request:
|
|
27
|
+
- **Audit**: report risks by severity, with file and line references where possible.
|
|
28
|
+
- **Implementation**: make scoped repository changes, then run relevant validation.
|
|
29
|
+
- **Strategy**: explain tradeoffs and identify user-only settings.
|
|
30
|
+
3. Separate agent-doable work from user-only UI work. Do not claim account, npmjs.com, or GitHub
|
|
31
|
+
settings are configured unless verified by authenticated tool/API access.
|
|
32
|
+
4. For concrete GitHub Actions publish workflow creation or CI failure debugging, hand off to the
|
|
33
|
+
`npm-trusted-publishing-github-workflow` skill.
|
|
34
|
+
|
|
35
|
+
## Freshness Rule
|
|
36
|
+
|
|
37
|
+
Before changing trusted publishing requirements, supported CI providers, npm CLI minimums, Node.js
|
|
38
|
+
minimums, provenance behavior, or GitHub release/security settings, verify the current official npm
|
|
39
|
+
and GitHub documentation when browsing is available. These requirements change over time.
|
|
40
|
+
|
|
41
|
+
## Responsibility Split
|
|
42
|
+
|
|
43
|
+
Agent can usually:
|
|
44
|
+
|
|
45
|
+
- Edit `.github/workflows/*`.
|
|
46
|
+
- Add or update repository package-manager config such as `.npmrc` or `.yarnrc.yml`.
|
|
47
|
+
- Remove `NODE_AUTH_TOKEN` usage from publish steps when trusted publishing is used.
|
|
48
|
+
If private dependencies require registry auth during install, keep that token scoped to read-only
|
|
49
|
+
install steps only.
|
|
50
|
+
- Add Dependabot/Renovate config.
|
|
51
|
+
- Run `publint`, workflow linting, tests, package builds, and other local validation.
|
|
52
|
+
- Report exact user steps for npm/GitHub settings.
|
|
53
|
+
|
|
54
|
+
User or authenticated UI/API access required:
|
|
55
|
+
|
|
56
|
+
- Enable npm/GitHub 2FA.
|
|
57
|
+
- Configure npm trusted publisher settings on npmjs.com.
|
|
58
|
+
- Set package publishing access to require 2FA and disallow tokens.
|
|
59
|
+
- Enable GitHub repository/org Actions restrictions.
|
|
60
|
+
- Configure branch/tag rulesets and immutable releases.
|
|
61
|
+
- Remove repository/org secrets when the agent lacks GitHub settings access.
|
|
62
|
+
|
|
63
|
+
## Non-Negotiables
|
|
64
|
+
|
|
65
|
+
- Do not add `NODE_AUTH_TOKEN` to publish steps when trusted publishing is available.
|
|
66
|
+
Use OIDC for publishing; if private dependencies require install auth, use a read-only token only
|
|
67
|
+
for the install step.
|
|
68
|
+
- Do not store npm publish tokens in GitHub Actions secrets for OIDC-capable publishing.
|
|
69
|
+
- Do not run install lifecycle scripts in release workflows unless the user explicitly accepts the
|
|
70
|
+
risk.
|
|
71
|
+
- Do not show tag-pinned actions as compliant with SHA pinning; resolve actions to full commit SHAs.
|
|
72
|
+
- Do not say npm/GitHub account settings are complete unless they were actually checked.
|
|
73
|
+
- Do not use self-hosted runners for npm trusted publishing unless official npm docs currently
|
|
74
|
+
support them.
|
|
75
|
+
|
|
76
|
+
> **Package manager note.** The example install and configuration commands in this skill use `npm`
|
|
77
|
+
> to match the e18e source material, but should be adapted to whichever package manager the project
|
|
19
78
|
> already uses — `pnpm`, `yarn`, `bun`, etc. Adapt commands accordingly:
|
|
20
79
|
>
|
|
21
80
|
> | npm | pnpm | yarn |
|
|
@@ -27,6 +86,10 @@ the canonical source; this skill distils the actionable steps.
|
|
|
27
86
|
> Detect the project's package manager by checking for a lockfile (`pnpm-lock.yaml`,
|
|
28
87
|
> `yarn.lock`, `bun.lockb`) or a `packageManager` field in `package.json` before
|
|
29
88
|
> generating any commands or workflow steps.
|
|
89
|
+
>
|
|
90
|
+
> Section 2.2's trusted-publishing requirement is npm-specific: the publish step must run with a
|
|
91
|
+
> supported Node.js version and npm CLI version even when the rest of the workflow uses another
|
|
92
|
+
> package manager.
|
|
30
93
|
|
|
31
94
|
---
|
|
32
95
|
|
|
@@ -92,7 +155,8 @@ ever touches the repository.
|
|
|
92
155
|
### 2.2 · npm CLI version requirement
|
|
93
156
|
|
|
94
157
|
The publish step **must** use npm CLI ≥ 11.5.1 for automatic OIDC trusted publishing.
|
|
95
|
-
Node.js
|
|
158
|
+
Node.js 22.14.0 or newer can use trusted publishing when the workflow installs npm CLI 11.5.1 or
|
|
159
|
+
newer before publishing:
|
|
96
160
|
|
|
97
161
|
```yaml
|
|
98
162
|
- run: npm i -g npm
|
|
@@ -277,6 +341,24 @@ all other security recommendations in this document regardless.
|
|
|
277
341
|
|
|
278
342
|
---
|
|
279
343
|
|
|
344
|
+
## Audit Output Format
|
|
345
|
+
|
|
346
|
+
For audits, lead with findings:
|
|
347
|
+
|
|
348
|
+
- **P0/P1/P2/P3** severity.
|
|
349
|
+
- File and line when repo-local.
|
|
350
|
+
- Risk.
|
|
351
|
+
- Recommended fix.
|
|
352
|
+
- Whether the agent can implement it now or the user must configure it externally.
|
|
353
|
+
|
|
354
|
+
Then include:
|
|
355
|
+
|
|
356
|
+
- Validation run.
|
|
357
|
+
- Remaining user-only checklist.
|
|
358
|
+
- Suggested next change.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
280
362
|
## Quick Reference Checklist
|
|
281
363
|
|
|
282
364
|
Use this when setting up a new package or auditing an existing one.
|
|
@@ -299,7 +381,7 @@ Use this when setting up a new package or auditing an existing one.
|
|
|
299
381
|
|
|
300
382
|
- [ ] OIDC trusted publisher configured on npmjs.com
|
|
301
383
|
- [ ] "Require 2FA, disallow tokens" enabled on npm
|
|
302
|
-
- [ ] Publish step uses Node.js
|
|
384
|
+
- [ ] Publish step uses npm CLI ≥ 11.5.1 and a Node.js version supported by official npm docs
|
|
303
385
|
- [ ] GitHub environment (`publish`) configured with branch restrictions
|
|
304
386
|
|
|
305
387
|
### Workflow hygiene
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: npm-trusted-publishing-github-workflow
|
|
3
|
+
description: >
|
|
4
|
+
Generate, repair, or debug the GitHub Actions workflow FILE that performs an OIDC
|
|
5
|
+
trusted publish of a pnpm package — the concrete publish.yml, its test → build →
|
|
6
|
+
publish job shape, the package tarball artifact handoff, Node-version inference from
|
|
7
|
+
package.json, pnpm setup via pnpm/action-setup, the npm-CLI-version upgrade step, and
|
|
8
|
+
repository.url/Sigstore provenance matching. Use when the user wants the actual
|
|
9
|
+
workflow written or fixed, or is debugging a specific CI failure: npm publish
|
|
10
|
+
E404/E403/422, NODE_AUTH_TOKEN appearing unexpectedly, provenance or id-token errors,
|
|
11
|
+
pnpm/action-setup version resolution, or actions/setup-node node-version-file problems.
|
|
12
|
+
For the broader publishing SECURITY POSTURE — account 2FA, repository and branch
|
|
13
|
+
hardening, GitHub environments, changesets versus changelogithub, sole-maintainer risk,
|
|
14
|
+
or auditing an existing pipeline — use the npm-package-publishing skill instead.
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# NPM Trusted Publish
|
|
18
|
+
|
|
19
|
+
## Goal
|
|
20
|
+
|
|
21
|
+
Implement the same hardened npm trusted publishing pattern every time, without rediscovering the details from CI logs.
|
|
22
|
+
|
|
23
|
+
## Related skills
|
|
24
|
+
|
|
25
|
+
This skill generates and debugs the publish workflow file. For the surrounding security posture — account and repository 2FA, branch protection, GitHub publish environments, release-strategy choice, and sole-maintainer risk — use the `npm-package-publishing` skill. The two are complementary: `npm-package-publishing` decides how publishing should be set up, this skill writes and fixes the YAML that does it.
|
|
26
|
+
|
|
27
|
+
One number to keep consistent between the two: both skills use Node 24.8.0 or higher as the publish-step floor. Node 24.8.0 bundles npm 11.6.0, which already exceeds the npm CLI 11.5.1 minimum that trusted publishing requires, so on that floor no manual npm upgrade is needed. If a project must publish on an older Node, it has to upgrade npm to 11.5.1 or later first — the publish job retains a guard step for exactly that case.
|
|
28
|
+
|
|
29
|
+
## Workflow
|
|
30
|
+
|
|
31
|
+
1. Inspect `package.json`, `.npmrc`, lockfiles, and existing `.github/workflows/*.yml`.
|
|
32
|
+
2. Resolve every workflow dependency to its latest stable version at the moment the file is created, and pin each to the full-length commit SHA of that version. The SHAs in this skill's template are placeholders that will be out of date; never copy them verbatim. See "Pinning actions to current SHAs" below for the procedure.
|
|
33
|
+
3. Preserve pinned action SHAs when they already exist; annotate each with a version comment so Dependabot can bump it.
|
|
34
|
+
4. Drive the test and build jobs' Node version from the project's **existing** target, not from a number invented for this workflow. Read it from the repo's current `.nvmrc`, `.node-version`, `volta.node`, or CI config; if none exists, ask the developer rather than guessing. Use `node-version-file: .nvmrc` for these jobs (do not point them at `package.json`, which falls through to the `engines.node` range — an unbounded range like `>=20` resolves to the newest Node release, so CI silently floats away from the version developers actually run). `.nvmrc` is a development and CI file only; npm never reads it during a consumer install, so it does not constrain consumers.
|
|
35
|
+
5. Never raise the project's Node version, create a new `.nvmrc`, or overwrite an existing one to "match" the publish step. The publish step's Node 24.8.0 (step 11) is an isolated requirement of the publish action and must not propagate to `.nvmrc`, to `engines.node`, or to the test and build jobs. A project that targets Node 22 keeps testing and building on Node 22; only the final `npm publish` invocation runs on 24.8.0, and it does not rebuild the artifact. Conflating these two numbers is the most likely way this skill is misapplied — do not do it.
|
|
36
|
+
6. Ensure every job that reads the repo (including any reading `.nvmrc`) runs `actions/checkout` first.
|
|
37
|
+
7. Install pnpm with `pnpm/action-setup`, omitting the `version` input so the version is read from the `packageManager` field. Do not use Corepack: it is still marked experimental and downloads the package manager from the network on first use, which is an avoidable failure surface in a release pipeline.
|
|
38
|
+
8. `pnpm/action-setup` does not install Node.js, so always run `actions/setup-node` as a separate step.
|
|
39
|
+
9. Disable setup-node package-manager caching for release/publish workflows with `package-manager-cache: false`.
|
|
40
|
+
10. Set `persist-credentials: false` on every `actions/checkout` step unless a later step must push to git.
|
|
41
|
+
11. Target Node 24.8.0 or higher in the publish step. That floor bundles npm 11.6.0, which already exceeds the npm CLI 11.5.1 minimum trusted publishing requires, so no manual npm upgrade is needed there. Keep a guard step that upgrades npm only when the resolved Node ships an npm below 11.5.1, so the workflow stays correct if a project pins an older Node. An npm that is too old silently falls back to token auth or fails to attempt OIDC at all.
|
|
42
|
+
12. Pack into a dedicated artifact directory, usually `package/*.tgz`.
|
|
43
|
+
13. In the publish job, download the artifact to `package`, find the `.tgz`, and publish its resolved path.
|
|
44
|
+
14. Use GitHub OIDC trusted publishing, not npm tokens. Provenance is generated automatically under trusted publishing, so the `--provenance` flag is not required.
|
|
45
|
+
15. Add a `concurrency` group keyed on the release so two tag pushes cannot race into overlapping publishes.
|
|
46
|
+
|
|
47
|
+
## Package Metadata
|
|
48
|
+
|
|
49
|
+
Three different Node versions live in three different places, and keeping them separate is deliberate — conflating them is the main way this workflow goes wrong. `engines.node` in `package.json` is the _consumer_ floor: the only one that constrains people who install the package, and it should reflect what the package actually supports (npm warns, but does not hard-fail, when a consumer is outside it). The test and build jobs run on the project's _own_ target version, read from the existing `.nvmrc` (or `.node-version`/`volta.node`); this is never read during a consumer install, so it does not leak into the consumer contract. The publish step pins Node 24.8.0 or higher independently, purely because that floor bundles an npm new enough for OIDC. These three are not meant to agree: a repo can develop and test on Node 22, keep `engines.node` at its true support range, and still publish on Node 24 — all without affecting consumers, and without changing what the project builds and tests against.
|
|
50
|
+
|
|
51
|
+
The publish-step version must never be copied into the other two. Do not raise `engines.node` to 24.8.0, and do not set or bump `.nvmrc` to 24, to "make things consistent". Doing so would move the test and build jobs onto Node 24, so the package would be validated against a version above its actual target and a Node-22 incompatibility could ship uncaught. The publish job runs `npm publish` on the already-built tarball with scripts ignored, so its Node version never rebuilds or retests the code; it is inert with respect to the artifact.
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=20"
|
|
57
|
+
},
|
|
58
|
+
"packageManager": "pnpm@10.0.0",
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/OWNER/REPO.git"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The `engines.node` value above is the _consumer_ floor and should reflect what the package actually supports; `>=20` is only an example, and a bounded upper limit is sensible if the package genuinely needs one. Do not raise it to 24.8.0 to satisfy CI — the publish step pins its own Node version, and the test and build jobs read theirs from `.nvmrc`, so the trusted-publishing requirement never leaks into the consumer contract.
|
|
67
|
+
|
|
68
|
+
Because the test and build jobs read `.nvmrc`, that file must exist in the repository root with a single version line matching the project's target (for example `22`). If the repo already has one, use it as-is and do not change it. If it has none, derive the value from the project's existing Node target (`.node-version`, `volta.node`, the previous CI config, or by asking the developer) before creating it — do not default to the publish step's 24.8.0. Alternatively, point those jobs' `node-version` at the project's explicit version instead of using a file.
|
|
69
|
+
|
|
70
|
+
The `repository.url` field is not cosmetic. Provenance verification runs through Sigstore, which compares the repository in the OIDC token against `package.json`. A mismatch fails the publish with a 422 error that the user-facing npm docs do not explain. Make sure the owner/name in `repository.url` matches the repository actually running the workflow.
|
|
71
|
+
|
|
72
|
+
Do not add npm auth tokens for trusted publishing.
|
|
73
|
+
|
|
74
|
+
## Workflow Template
|
|
75
|
+
|
|
76
|
+
Use this shape for pnpm packages, adapting only names, test commands, and existing pinned action SHAs. The `@<sha>` values below are **placeholders**: before writing the file, resolve each action to its latest stable release and replace the placeholder with that release's full-length commit SHA, keeping the `# vX.Y.Z` comment accurate. Do not copy the example SHAs — see "Pinning actions to current SHAs".
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
# NOTE: every action SHA below is a PLACEHOLDER and is almost certainly out of date.
|
|
80
|
+
# Re-resolve each action to its latest stable release and pin to that SHA before use.
|
|
81
|
+
# See "Pinning actions to current SHAs".
|
|
82
|
+
name: Publish
|
|
83
|
+
|
|
84
|
+
on:
|
|
85
|
+
release:
|
|
86
|
+
types: [published]
|
|
87
|
+
|
|
88
|
+
permissions:
|
|
89
|
+
contents: read
|
|
90
|
+
|
|
91
|
+
concurrency:
|
|
92
|
+
group: publish-${{ github.event.release.tag_name }}
|
|
93
|
+
cancel-in-progress: false
|
|
94
|
+
|
|
95
|
+
jobs:
|
|
96
|
+
test:
|
|
97
|
+
name: Test
|
|
98
|
+
runs-on: ubuntu-latest
|
|
99
|
+
timeout-minutes: 60
|
|
100
|
+
permissions:
|
|
101
|
+
contents: read
|
|
102
|
+
steps:
|
|
103
|
+
- name: Checkout
|
|
104
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 — PLACEHOLDER SHA, re-resolve before use
|
|
105
|
+
with:
|
|
106
|
+
persist-credentials: false
|
|
107
|
+
|
|
108
|
+
- name: Install pnpm
|
|
109
|
+
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 — PLACEHOLDER SHA, re-resolve before use
|
|
110
|
+
# version is read from the packageManager field in package.json
|
|
111
|
+
|
|
112
|
+
- name: Setup Node.js
|
|
113
|
+
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 — PLACEHOLDER SHA, re-resolve before use
|
|
114
|
+
with:
|
|
115
|
+
node-version-file: .nvmrc # exact dev/CI version; decoupled from engines.node
|
|
116
|
+
package-manager-cache: false
|
|
117
|
+
|
|
118
|
+
- name: Install dependencies
|
|
119
|
+
run: pnpm install --frozen-lockfile --ignore-scripts
|
|
120
|
+
|
|
121
|
+
- name: Check package
|
|
122
|
+
run: pnpm run package:check
|
|
123
|
+
|
|
124
|
+
- name: Run tests
|
|
125
|
+
run: pnpm test
|
|
126
|
+
|
|
127
|
+
build:
|
|
128
|
+
name: Pack package
|
|
129
|
+
needs: test
|
|
130
|
+
runs-on: ubuntu-latest
|
|
131
|
+
timeout-minutes: 10
|
|
132
|
+
permissions:
|
|
133
|
+
contents: read
|
|
134
|
+
steps:
|
|
135
|
+
- name: Checkout
|
|
136
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 — PLACEHOLDER SHA, re-resolve before use
|
|
137
|
+
with:
|
|
138
|
+
persist-credentials: false
|
|
139
|
+
|
|
140
|
+
- name: Install pnpm
|
|
141
|
+
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 — PLACEHOLDER SHA, re-resolve before use
|
|
142
|
+
|
|
143
|
+
- name: Setup Node.js
|
|
144
|
+
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 — PLACEHOLDER SHA, re-resolve before use
|
|
145
|
+
with:
|
|
146
|
+
node-version-file: .nvmrc # exact dev/CI version; decoupled from engines.node
|
|
147
|
+
package-manager-cache: false
|
|
148
|
+
|
|
149
|
+
- name: Install dependencies
|
|
150
|
+
run: pnpm install --frozen-lockfile --ignore-scripts
|
|
151
|
+
|
|
152
|
+
- name: Create package directory
|
|
153
|
+
run: mkdir package
|
|
154
|
+
|
|
155
|
+
- name: Create package tarball
|
|
156
|
+
run: pnpm pack --pack-destination package
|
|
157
|
+
|
|
158
|
+
- name: Upload package tarball
|
|
159
|
+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 — PLACEHOLDER SHA, re-resolve before use
|
|
160
|
+
with:
|
|
161
|
+
name: npm-package
|
|
162
|
+
path: package/*.tgz
|
|
163
|
+
if-no-files-found: error
|
|
164
|
+
retention-days: 7
|
|
165
|
+
|
|
166
|
+
publish:
|
|
167
|
+
name: Publish to npm
|
|
168
|
+
needs: build
|
|
169
|
+
runs-on: ubuntu-latest
|
|
170
|
+
timeout-minutes: 10
|
|
171
|
+
environment: publish
|
|
172
|
+
permissions:
|
|
173
|
+
contents: read
|
|
174
|
+
id-token: write
|
|
175
|
+
steps:
|
|
176
|
+
- name: Checkout
|
|
177
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 — PLACEHOLDER SHA, re-resolve before use
|
|
178
|
+
with:
|
|
179
|
+
persist-credentials: false
|
|
180
|
+
|
|
181
|
+
- name: Setup Node.js
|
|
182
|
+
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 — PLACEHOLDER SHA, re-resolve before use
|
|
183
|
+
with:
|
|
184
|
+
# Pinned for the publish step only. 24.8.0 bundles npm 11.6.0, new enough
|
|
185
|
+
# for OIDC; this is independent of engines.node, the consumer floor.
|
|
186
|
+
node-version: 24.8.0
|
|
187
|
+
package-manager-cache: false
|
|
188
|
+
registry-url: https://registry.npmjs.org
|
|
189
|
+
|
|
190
|
+
- name: Ensure npm is new enough for trusted publishing
|
|
191
|
+
# No-op on Node >= 24.8.0; the guard only matters if Node is pinned lower.
|
|
192
|
+
run: |
|
|
193
|
+
required="11.5.1"
|
|
194
|
+
current="$(npm --version)"
|
|
195
|
+
if npx -y semver -r "<$required" --include-prerelease "$current" > /dev/null 2>&1; then
|
|
196
|
+
echo "npm $current is below $required; upgrading."
|
|
197
|
+
npm install -g npm@latest
|
|
198
|
+
fi
|
|
199
|
+
npm --version
|
|
200
|
+
|
|
201
|
+
- name: Download package tarball
|
|
202
|
+
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 — PLACEHOLDER SHA, re-resolve before use
|
|
203
|
+
with:
|
|
204
|
+
name: npm-package
|
|
205
|
+
path: package
|
|
206
|
+
|
|
207
|
+
- name: Publish to npm
|
|
208
|
+
run: |
|
|
209
|
+
tarball="$(find package -type f -name '*.tgz' -print -quit)"
|
|
210
|
+
|
|
211
|
+
if [ -z "$tarball" ]; then
|
|
212
|
+
echo "No package tarball found in downloaded artifact."
|
|
213
|
+
find package -maxdepth 3 -type f -print
|
|
214
|
+
exit 1
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
npm publish "$(realpath "$tarball")" --ignore-scripts --access public
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Pinning actions to current SHAs
|
|
221
|
+
|
|
222
|
+
The template's SHAs are stale by design. Action versions and their commit SHAs change over time, so resolve them fresh whenever a `publish.yml` is created or reviewed. Pin to the full-length commit SHA, never a tag or branch, because a tag can be moved to point at malicious code after you have reviewed it.
|
|
223
|
+
|
|
224
|
+
There are two reliable ways to produce current pins.
|
|
225
|
+
|
|
226
|
+
The preferred approach is to let tooling resolve and pin for you. Write the workflow first using human-readable tags (for example `actions/checkout@v4`), then run `npx actions-up` in the repository to rewrite every `uses:` reference to the latest stable release pinned to its commit SHA, with a version comment appended. This is the same tool the `npm-package-publishing` skill recommends, and it removes the chance of a hand-typed SHA being wrong. After it runs, confirm each line carries a `@<40-hex-sha> # vX.Y.Z` form.
|
|
227
|
+
|
|
228
|
+
If resolving manually, for each action find the latest stable release tag, then read the exact commit that tag points to and pin that commit:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# Latest stable release tag for an action (skips pre-releases)
|
|
232
|
+
gh release view --repo actions/checkout --json tagName --jq .tagName
|
|
233
|
+
|
|
234
|
+
# The commit SHA that the tag resolves to — pin THIS value
|
|
235
|
+
gh api repos/actions/checkout/git/refs/tags/v4.2.2 --jq .object.sha
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
For an annotated tag the first lookup may return a tag object rather than a commit; dereference it with `gh api repos/<owner>/<repo>/git/tags/<sha> --jq .object.sha` to reach the underlying commit. Pin the commit SHA, not the tag SHA.
|
|
239
|
+
|
|
240
|
+
Keep the pins current after creation by letting Dependabot manage action updates. This is why every `uses:` line carries a `# vX.Y.Z` comment: Dependabot reads the comment to know which version a SHA represents and to raise update PRs. The companion Dependabot configuration should include a `github-actions` ecosystem entry pointing at `/` so the publish workflow is covered. Periodically re-running `npx actions-up` is a reasonable backstop if Dependabot is not enabled.
|
|
241
|
+
|
|
242
|
+
## Checks
|
|
243
|
+
|
|
244
|
+
After edits:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
ruby -e 'require "yaml"; YAML.load_file(".github/workflows/publish.yml"); puts "YAML ok"'
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
If the project uses pnpm, validate packing without publishing:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
pack_dir="$(mktemp -d)"
|
|
254
|
+
pnpm pack --pack-destination "$pack_dir"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Confirm no placeholder markers survived into the generated file, and that every action is pinned to a 40-character SHA rather than a tag:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
# Must print nothing
|
|
261
|
+
grep -n "PLACEHOLDER" .github/workflows/publish.yml
|
|
262
|
+
|
|
263
|
+
# Every uses: line must reference a 40-hex SHA, not a tag
|
|
264
|
+
if ! grep -qE "uses: [^@]+@" .github/workflows/publish.yml; then
|
|
265
|
+
echo "No uses lines found"
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
grep -nE "uses: [^@]+@[^ ]+" .github/workflows/publish.yml \
|
|
270
|
+
| grep -vE "@[0-9a-f]{40} " && echo "Unpinned action found" || echo "All actions SHA-pinned"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Failure Clues
|
|
274
|
+
|
|
275
|
+
- `NODE_AUTH_TOKEN: ***` appears in the publish log: token auth is being used or injected. Trusted publishing should not need it.
|
|
276
|
+
- `E404 Not Found - PUT ... could not be found or you do not have permission`: often an auth/scope permission problem, especially if local manual publish works.
|
|
277
|
+
- `422 Unprocessable Entity` during publish with provenance: the repository in the OIDC token does not match `package.json`. Check `repository.url` first.
|
|
278
|
+
- npm silently publishing with a token despite trusted-publisher config: the runner's npm CLI is older than 11.5.1. This should not happen on the pinned Node 24.8.0 (which bundles npm 11.6.0); if the publish step was moved to an older Node, confirm the guard step actually upgraded npm and reported a version at or above 11.5.1.
|
|
279
|
+
- Tests or build now run on a newer Node than the project targets (for example Node 24 when the project is on 22): `.nvmrc` was created or bumped to match the publish step. Reset it to the project's actual target; the publish step's 24.8.0 must stay confined to the publish job.
|
|
280
|
+
- `package.json does not exist` from `setup-node`: the job uses `node-version-file` before checkout, or the publish job only downloaded an artifact.
|
|
281
|
+
- `pnpm/action-setup` cannot resolve a version: the `packageManager` field is missing, or the v6 bug in [pnpm/action-setup#227](https://github.com/pnpm/action-setup/issues/227) occasionally fails to read `packageManager` from `package.json` when `package_json_file` is set, causing version resolution to fail. Pin `pnpm/action-setup` to a known-good SHA and, if needed, set the `version` input explicitly as a fallback.
|
|
282
|
+
- Publishing an already-published version will fail even after the workflow is fixed.
|
|
283
|
+
|
|
284
|
+
## External Setup Reminder
|
|
285
|
+
|
|
286
|
+
Repo changes cannot create npm's trusted publisher entry. Remind the user to verify npm package settings:
|
|
287
|
+
|
|
288
|
+
- provider: GitHub Actions
|
|
289
|
+
- repository owner/name matches the repo
|
|
290
|
+
- workflow filename matches `.github/workflows/publish.yml`
|
|
291
|
+
- publish environment matches the workflow if npm is configured with one
|
|
292
|
+
- at least one allowed action is selected: configurations created after 20 May 2026 require explicitly selecting an allowed action (for example, allow `npm publish`), or the publish will be rejected
|
|
293
|
+
|
|
294
|
+
The first version of a brand-new package cannot be published via OIDC, because npm requires the package to exist before its trusted-publisher settings can be edited. Publish the initial version manually or with a token, then configure trusted publishing for subsequent releases.
|
|
@@ -1,20 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: refined-plan-mode
|
|
3
|
+
description: Use this skill when the user asks to plan, review, revise, continue, checkpoint, handoff, reset, or execute work using Refined Plan Mode. Also use it for legacy /rpm:start, /rpm:advance, /rpm:review, /rpm:feedback, /rpm:checkpoint, and /rpm:handoff prompts.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Refined Plan Mode
|
|
2
7
|
|
|
3
|
-
Use this skill when the user
|
|
8
|
+
Use this skill when the user asks to plan, review, revise, continue, checkpoint, handoff, reset, or execute work using Refined Plan Mode.
|
|
4
9
|
|
|
5
|
-
This skill is additive to the agent's current
|
|
10
|
+
This skill is additive to the agent's current planning guidance. It turns a plan into a versioned Markdown artifact that can be reviewed with line, range, and text-selection comments. The agent remains responsible for reading feedback, revising the plan, and moving only when the user has approved the plan or explicitly asks to proceed.
|
|
6
11
|
|
|
7
12
|
## Core Protocol
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
Before deciding what to do, inspect the local `.plan-review` state:
|
|
15
|
+
|
|
16
|
+
- `.plan-review/.current-version`
|
|
17
|
+
- `.plan-review/approved-plan.md`
|
|
18
|
+
- `.plan-review/plans/`
|
|
19
|
+
- `.plan-review/feedback/`
|
|
20
|
+
|
|
21
|
+
Then choose the next state transition:
|
|
22
|
+
|
|
23
|
+
1. If `.plan-review/approved-plan.md` exists, read it and execute the approved plan carefully.
|
|
24
|
+
2. If a current version exists and `.plan-review/feedback/plan-vN-feedback.json` exists for it, read the current plan and feedback, address every feedback item in a revised next plan version, update `.plan-review/.current-version`, and stop for review.
|
|
25
|
+
3. If a current version exists with no feedback and no approval, report that the plan is awaiting review and include the reviewer launch command.
|
|
26
|
+
4. If no current plan exists, clarify only what is necessary, inspect the repository enough to produce a useful plan, write `.plan-review/plans/plan-v1.md`, write `v1` to `.plan-review/.current-version`, and stop for review.
|
|
27
|
+
|
|
28
|
+
Users can ask for checkpoint, handoff, or reset in natural language:
|
|
29
|
+
|
|
30
|
+
- For a checkpoint, report the current version, latest plan path, feedback status, approval status, and recommended next action.
|
|
31
|
+
- For a handoff, summarize the goal, current plan, feedback status, approval status, important assumptions, unresolved decisions, and recommended next action.
|
|
32
|
+
- For a reset, first require explicit confirmation by asking the user to type `RESET` or by accepting an explicit `--force` request. After confirmation, empty only the contents of `.plan-review` while keeping the `.plan-review` directory itself. Do not remove source files or any other workspace files.
|
|
33
|
+
|
|
34
|
+
## Task Modes
|
|
35
|
+
|
|
36
|
+
Treat these legacy `/rpm:*` prompts as natural-language requests for this skill:
|
|
37
|
+
|
|
38
|
+
### `/rpm:start`
|
|
39
|
+
|
|
40
|
+
Start a plan review loop for the user's current task.
|
|
41
|
+
|
|
42
|
+
1. Inspect the repository enough to understand the task and relevant constraints.
|
|
43
|
+
2. Ask only blocking clarification questions. If reasonable assumptions are available, state them in the plan instead of stopping.
|
|
44
|
+
3. Create `.plan-review/plans/plan-v1.md` with the complete plan.
|
|
45
|
+
4. Create or update `.plan-review/.current-version` with `v1`.
|
|
46
|
+
5. Reply with a concise summary and tell the user the plan is ready for review in Refined Plan Mode.
|
|
47
|
+
|
|
48
|
+
Do not implement the plan yet unless the user explicitly asks you to proceed without review.
|
|
49
|
+
|
|
50
|
+
### `/rpm:advance`
|
|
51
|
+
|
|
52
|
+
Continue the loop from the current state.
|
|
53
|
+
|
|
54
|
+
1. Inspect `.plan-review/.current-version`, `.plan-review/approved-plan.md`, available plan files, and available feedback files.
|
|
55
|
+
2. If an approved plan exists, execute that plan.
|
|
56
|
+
3. If feedback exists for the current plan version, incorporate it into the next plan version.
|
|
57
|
+
4. If there is a current plan but no feedback or approval, remind the user that the plan is awaiting review.
|
|
58
|
+
5. If no plan exists, start with `/rpm:start` behavior.
|
|
59
|
+
|
|
60
|
+
Keep the response focused on the next state transition.
|
|
61
|
+
|
|
62
|
+
### `/rpm:review`
|
|
63
|
+
|
|
64
|
+
Audit the latest plan before the user reviews it.
|
|
65
|
+
|
|
66
|
+
1. Read the current plan version.
|
|
67
|
+
2. Review the plan for missing context, vague steps, untested assumptions, risky sequencing, and weak validation.
|
|
68
|
+
3. If improvements are needed, write a revised next version and update `.plan-review/.current-version`.
|
|
69
|
+
4. If the plan is already review-ready, leave files unchanged.
|
|
70
|
+
5. Reply with either the new plan version written or a short explanation that the current plan is ready for review.
|
|
71
|
+
|
|
72
|
+
This mode reviews plan quality. It does not implement the plan.
|
|
73
|
+
|
|
74
|
+
### `/rpm:feedback`
|
|
75
|
+
|
|
76
|
+
Incorporate submitted feedback into the next plan version.
|
|
77
|
+
|
|
78
|
+
1. Read `.plan-review/.current-version` to find the current version.
|
|
79
|
+
2. Read `.plan-review/feedback/plan-vN-feedback.json` for that version.
|
|
80
|
+
3. Read `.plan-review/plans/plan-vN.md`.
|
|
81
|
+
4. Address every feedback item in a revised plan, adding a `Feedback Addressed` section that maps comments to changes made.
|
|
82
|
+
5. Write the revision to `.plan-review/plans/plan-vN+1.md`.
|
|
83
|
+
6. Update `.plan-review/.current-version` to the new version.
|
|
84
|
+
7. Reply with a short note naming the feedback file read and the new plan file written.
|
|
85
|
+
|
|
86
|
+
If the feedback file is missing, report the exact path expected and stop.
|
|
87
|
+
|
|
88
|
+
### `/rpm:checkpoint`
|
|
89
|
+
|
|
90
|
+
Summarize the current review-loop state.
|
|
91
|
+
|
|
92
|
+
Report:
|
|
93
|
+
|
|
94
|
+
- Current plan version from `.plan-review/.current-version`, if present.
|
|
95
|
+
- Latest plan file path.
|
|
96
|
+
- Whether feedback exists for the current version.
|
|
97
|
+
- Whether `.plan-review/approved-plan.md` exists.
|
|
98
|
+
- The recommended next action.
|
|
99
|
+
|
|
100
|
+
Do not modify files unless the user also asks you to advance or revise the plan.
|
|
101
|
+
|
|
102
|
+
### `/rpm:handoff`
|
|
103
|
+
|
|
104
|
+
Prepare a compact continuation summary for another agent or a future session.
|
|
105
|
+
|
|
106
|
+
Include:
|
|
107
|
+
|
|
108
|
+
- Goal.
|
|
109
|
+
- Current plan version and file path.
|
|
110
|
+
- Feedback status.
|
|
111
|
+
- Approval status.
|
|
112
|
+
- Important assumptions or unresolved decisions.
|
|
113
|
+
- Recommended next action.
|
|
114
|
+
|
|
115
|
+
Prefer reading the current plan and feedback files directly instead of relying on chat history.
|
|
116
|
+
|
|
117
|
+
### Reset
|
|
118
|
+
|
|
119
|
+
Reset the review-loop state only after explicit confirmation:
|
|
120
|
+
|
|
121
|
+
1. If the user did not provide an explicit `--force` request, ask them to type `RESET`.
|
|
122
|
+
2. Proceed only when the user confirms exactly.
|
|
123
|
+
3. Empty the contents of `.plan-review`, preserving the `.plan-review` directory itself.
|
|
124
|
+
4. Do not remove source files or any workspace files outside `.plan-review`.
|
|
18
125
|
|
|
19
126
|
## File Convention
|
|
20
127
|
|
|
@@ -58,6 +165,24 @@ Prefer this structure unless the task clearly calls for something else:
|
|
|
58
165
|
|
|
59
166
|
Keep the plan practical. Include file paths, commands, and decision points when known. Call out assumptions explicitly instead of hiding uncertainty inside confident prose.
|
|
60
167
|
|
|
168
|
+
## Reviewer Launch Command
|
|
169
|
+
|
|
170
|
+
Whenever you write or advance to a plan version that is ready for user review, include a command the user can run from the Refined Plan Mode project root.
|
|
171
|
+
|
|
172
|
+
Prefer an absolute path to the target project's `.plan-review` directory when you know it:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
PLAN_REVIEW_DIR=/absolute/path/to/project/.plan-review vp dev --host 127.0.0.1 --port 5173
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
If only a home-relative path is known, shell expansion is acceptable:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
PLAN_REVIEW_DIR=~/dev/target-project/.plan-review vp dev --host 127.0.0.1 --port 5173
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Use the toolchain command documented by the Refined Plan Mode project. For the Vite+ project, use `vp dev` rather than invoking the package manager directly.
|
|
185
|
+
|
|
61
186
|
## Feedback Handling
|
|
62
187
|
|
|
63
188
|
When feedback exists:
|
|
@@ -78,6 +203,7 @@ In conversation, keep updates brief:
|
|
|
78
203
|
|
|
79
204
|
- Say which plan version was written.
|
|
80
205
|
- Say where feedback should be submitted.
|
|
206
|
+
- Include the reviewer launch command when a plan is ready for review.
|
|
81
207
|
- Say which feedback file was read when revising.
|
|
82
208
|
- Say when the plan is approved and execution is beginning.
|
|
83
209
|
|
package/commands/rpm-advance.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# /rpm:advance
|
|
2
|
-
|
|
3
|
-
Use the Refined Plan Mode skill to continue the loop from the current state.
|
|
4
|
-
|
|
5
|
-
Steps:
|
|
6
|
-
|
|
7
|
-
1. Inspect `.plan-review/.current-version`, `.plan-review/approved-plan.md`, available plan files, and available feedback files.
|
|
8
|
-
2. If an approved plan exists, execute that plan.
|
|
9
|
-
3. If feedback exists for the current plan version, incorporate it into the next plan version.
|
|
10
|
-
4. If there is a current plan but no feedback or approval, remind the user that the plan is awaiting review.
|
|
11
|
-
5. If no plan exists, start with `/rpm:start` behavior.
|
|
12
|
-
|
|
13
|
-
Keep the response focused on the next state transition.
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# /rpm:checkpoint
|
|
2
|
-
|
|
3
|
-
Use the Refined Plan Mode skill to summarize the current review-loop state.
|
|
4
|
-
|
|
5
|
-
Report:
|
|
6
|
-
|
|
7
|
-
- Current plan version from `.plan-review/.current-version`, if present.
|
|
8
|
-
- Latest plan file path.
|
|
9
|
-
- Whether feedback exists for the current version.
|
|
10
|
-
- Whether `.plan-review/approved-plan.md` exists.
|
|
11
|
-
- The recommended next action.
|
|
12
|
-
|
|
13
|
-
Do not modify files unless the user also asks you to advance or revise the plan.
|
package/commands/rpm-feedback.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# /rpm:feedback
|
|
2
|
-
|
|
3
|
-
Use the Refined Plan Mode skill to incorporate submitted feedback into the next plan version.
|
|
4
|
-
|
|
5
|
-
Steps:
|
|
6
|
-
|
|
7
|
-
1. Read `.plan-review/.current-version` to find the current version.
|
|
8
|
-
2. Read `.plan-review/feedback/plan-vN-feedback.json` for that version.
|
|
9
|
-
3. Read `.plan-review/plans/plan-vN.md`.
|
|
10
|
-
4. Address every feedback item in a revised plan, adding a "Feedback Addressed" section that maps comments to changes made.
|
|
11
|
-
5. Write the revision to `.plan-review/plans/plan-vN+1.md`.
|
|
12
|
-
6. Update `.plan-review/.current-version` to the new version.
|
|
13
|
-
7. Reply with a short note naming the feedback file read and the new plan file written.
|
|
14
|
-
|
|
15
|
-
If the feedback file is missing, report the exact path expected and stop.
|
package/commands/rpm-handoff.md
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# /rpm:handoff
|
|
2
|
-
|
|
3
|
-
Use the Refined Plan Mode skill to prepare a compact continuation summary for another agent or a future session.
|
|
4
|
-
|
|
5
|
-
Include:
|
|
6
|
-
|
|
7
|
-
- Goal.
|
|
8
|
-
- Current plan version and file path.
|
|
9
|
-
- Feedback status.
|
|
10
|
-
- Approval status.
|
|
11
|
-
- Important assumptions or unresolved decisions.
|
|
12
|
-
- Recommended next command or action.
|
|
13
|
-
|
|
14
|
-
Prefer reading the current plan and feedback files directly instead of relying on chat history.
|
package/commands/rpm-review.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# /rpm:review
|
|
2
|
-
|
|
3
|
-
Use the Refined Plan Mode skill to audit the latest plan before the user reviews it.
|
|
4
|
-
|
|
5
|
-
Steps:
|
|
6
|
-
|
|
7
|
-
1. Read the current plan version.
|
|
8
|
-
2. Review the plan for missing context, vague steps, untested assumptions, risky sequencing, and weak validation.
|
|
9
|
-
3. If improvements are needed, write a revised next version and update `.plan-review/.current-version`.
|
|
10
|
-
4. If the plan is already review-ready, leave files unchanged.
|
|
11
|
-
5. Reply with either the new plan version written or a short explanation that the current plan is ready for review.
|
|
12
|
-
|
|
13
|
-
This command reviews plan quality. It does not implement the plan.
|
package/commands/rpm-start.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# /rpm:start
|
|
2
|
-
|
|
3
|
-
Use the Refined Plan Mode skill to start a plan review loop for the user's current task.
|
|
4
|
-
|
|
5
|
-
Steps:
|
|
6
|
-
|
|
7
|
-
1. Inspect the repository enough to understand the task and relevant constraints.
|
|
8
|
-
2. Ask only blocking clarification questions. If reasonable assumptions are available, state them in the plan instead of stopping.
|
|
9
|
-
3. Create `.plan-review/plans/plan-v1.md` with the complete plan.
|
|
10
|
-
4. Create or update `.plan-review/.current-version` with `v1`.
|
|
11
|
-
5. Reply with a concise summary and tell the user the plan is ready for review in Refined Plan Mode.
|
|
12
|
-
|
|
13
|
-
Do not implement the plan yet unless the user explicitly asks you to proceed without review.
|