@schalkneethling/toolkit 1.0.0 → 1.2.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 +33 -6
- package/agents/technical-devils-advocate.md +33 -0
- package/config.json +17 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -9
- package/skills/code-review/SKILL.md +9 -0
- package/skills/frontend-testing/references/visual-regression.md +60 -18
- package/skills/github-goal-issue-triage/SKILL.md +119 -0
- package/skills/github-goal-issue-triage/agents/openai.yaml +4 -0
- package/skills/npm-publishing-best-practices/SKILL.md +40 -4
- package/skills/npm-trusted-publishing-github-workflow/SKILL.md +57 -38
- package/skills/project-goal/SKILL.md +94 -0
- package/skills/project-goal/agents/openai.yaml +4 -0
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# claude-toolkit
|
|
2
2
|
|
|
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
|
|
3
|
+
CLI for managing [Claude Code](https://claude.com/claude-code) hooks, skills, agents, and collections across projects. Hooks are copied into a project's `.claude/` directory; skills and agents are copied into `.claude-toolkit/` 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
9
|
├── config.json # bundled collection definitions
|
|
10
|
+
├── agents/ # Claude Code subagent Markdown files
|
|
10
11
|
├── hooks/
|
|
11
12
|
│ ├── auto-approve-safe-commands/
|
|
12
13
|
│ │ ├── hook.mjs # the hook script itself
|
|
@@ -45,9 +46,17 @@ Copies `skills/<name>/` into `<project>/.claude-toolkit/skills/<name>/` and crea
|
|
|
45
46
|
toolkit add skill css-shared-first --link .claude/skills --link docs/skills
|
|
46
47
|
```
|
|
47
48
|
|
|
49
|
+
### `toolkit add agent <name> [--link <target>]...`
|
|
50
|
+
|
|
51
|
+
Copies `agents/<name>.md` into `<project>/.claude-toolkit/agents/<name>.md` and creates a symlink to that file inside each `--link` target. If no `--link` is given, the default is `.claude/agents`. Repeat `--link` to create symlinks in multiple locations.
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
toolkit add agent technical-devils-advocate
|
|
55
|
+
```
|
|
56
|
+
|
|
48
57
|
### `toolkit add collections <name>`
|
|
49
58
|
|
|
50
|
-
Reads the root `config.json`, resolves the named collection, and installs each referenced hook
|
|
59
|
+
Reads the root `config.json`, resolves the named collection, and installs each referenced hook, skill, or agent using the same underlying logic as the individual `add` commands.
|
|
51
60
|
|
|
52
61
|
```bash
|
|
53
62
|
toolkit add collections web
|
|
@@ -61,9 +70,9 @@ For every entry in `.claude/toolkit-manifest.json`, compares the current source
|
|
|
61
70
|
- If the installed file was modified locally (its hash differs from the one recorded in the manifest), warns and skips unless `--force` is passed.
|
|
62
71
|
- Silent if everything is current.
|
|
63
72
|
|
|
64
|
-
### `toolkit list hook` / `toolkit list skill`
|
|
73
|
+
### `toolkit list hook` / `toolkit list skill` / `toolkit list agent`
|
|
65
74
|
|
|
66
|
-
Lists available hooks or
|
|
75
|
+
Lists available hooks, skills, or agents shipped by this repo, with the current source hash.
|
|
67
76
|
|
|
68
77
|
### `toolkit list collections`
|
|
69
78
|
|
|
@@ -90,6 +99,10 @@ Collections are defined in the repo root `config.json` as an array:
|
|
|
90
99
|
{
|
|
91
100
|
"type": "skill",
|
|
92
101
|
"src": "skills/semantic-html"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "agent",
|
|
105
|
+
"src": "agents/technical-devils-advocate.md"
|
|
93
106
|
}
|
|
94
107
|
]
|
|
95
108
|
}
|
|
@@ -97,14 +110,15 @@ Collections are defined in the repo root `config.json` as an array:
|
|
|
97
110
|
```
|
|
98
111
|
|
|
99
112
|
- `name` must be unique.
|
|
100
|
-
- `items` may contain `skill` or `
|
|
101
|
-
- `src` must point to a top-level entry under `skills
|
|
113
|
+
- `items` may contain `skill`, `hook`, or `agent` entries.
|
|
114
|
+
- `src` must point to a top-level entry under `skills/`, `hooks/`, or `agents/`.
|
|
102
115
|
- Plural `type` values such as `skills` are also accepted for compatibility.
|
|
103
116
|
|
|
104
117
|
## Versioning
|
|
105
118
|
|
|
106
119
|
- Each hook is hashed over `hook.mjs` only (not the README or `settings-fragment.json`).
|
|
107
120
|
- Each skill is hashed over every file in the skill directory (sorted by path).
|
|
121
|
+
- Each agent is hashed over its Markdown source file.
|
|
108
122
|
- SHA-256, truncated to the first 7 hex characters.
|
|
109
123
|
|
|
110
124
|
## Manifest format
|
|
@@ -125,10 +139,23 @@ The CLI writes `<project>/.claude/toolkit-manifest.json`:
|
|
|
125
139
|
"installedAt": "2026-04-18",
|
|
126
140
|
"linkedTo": [".claude/skills"]
|
|
127
141
|
}
|
|
142
|
+
},
|
|
143
|
+
"agents": {
|
|
144
|
+
"technical-devils-advocate": {
|
|
145
|
+
"hash": "c110f5e",
|
|
146
|
+
"installedAt": "2026-04-18",
|
|
147
|
+
"linkedTo": [".claude/agents"]
|
|
148
|
+
}
|
|
128
149
|
}
|
|
129
150
|
}
|
|
130
151
|
```
|
|
131
152
|
|
|
153
|
+
## Bundled agents
|
|
154
|
+
|
|
155
|
+
### `technical-devils-advocate`
|
|
156
|
+
|
|
157
|
+
A Claude Code project subagent for challenging feature plans, system designs, and implementation approaches before work begins. It surfaces assumptions, risks, edge cases, alternatives, dependencies, scale concerns, and operability questions, then recommends whether to proceed, pivot, or pause.
|
|
158
|
+
|
|
132
159
|
## Bundled hooks
|
|
133
160
|
|
|
134
161
|
### `block-dangerous-commands`
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: technical-devils-advocate
|
|
3
|
+
model: claude-4.6-opus-high-thinking
|
|
4
|
+
description: Technical devil's advocate that challenges feature plans, strategies, and implementation approaches. Use proactively when planning features, designing systems, or proposing solutions. ALWAYS use for plan mode - asks probing questions to uncover risks, edge cases, and alternative approaches before implementation begins.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are a technical devil's advocate. Your job is to challenge assumptions and stress-test plans before any implementation.
|
|
8
|
+
|
|
9
|
+
When invoked:
|
|
10
|
+
|
|
11
|
+
1. Receive the feature plan, system design, or proposed solution
|
|
12
|
+
2. Identify assumptions and unstated constraints
|
|
13
|
+
3. Ask probing questions that expose weaknesses
|
|
14
|
+
4. Surface risks, edge cases, and failure modes
|
|
15
|
+
5. Suggest alternative approaches and tradeoffs
|
|
16
|
+
|
|
17
|
+
Challenge areas:
|
|
18
|
+
|
|
19
|
+
- **Risks**: What could fail? Failure modes? Blast radius?
|
|
20
|
+
- **Edge cases**: Boundary conditions, empty states, error paths, race conditions
|
|
21
|
+
- **Alternatives**: Why this approach vs others? What did we reject and why?
|
|
22
|
+
- **Dependencies**: What breaks if X changes? External assumptions?
|
|
23
|
+
- **Scale**: How does this behave at 10x, 100x? Latency, memory, throughput
|
|
24
|
+
- **Operability**: Monitoring, debugging, rollback, incident response
|
|
25
|
+
|
|
26
|
+
Output format:
|
|
27
|
+
|
|
28
|
+
- Prioritized list of concerns (critical → minor)
|
|
29
|
+
- Specific probing questions the team should answer
|
|
30
|
+
- Alternative approaches worth considering
|
|
31
|
+
- Recommendations: proceed, pivot, or pause
|
|
32
|
+
|
|
33
|
+
Be constructive: challenge to improve the plan, not to block it. Every question should help the team make a better decision.
|
package/config.json
CHANGED
|
@@ -23,5 +23,22 @@
|
|
|
23
23
|
"src": "skills/frontend-security"
|
|
24
24
|
}
|
|
25
25
|
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "code-review",
|
|
29
|
+
"items": [
|
|
30
|
+
{
|
|
31
|
+
"type": "skill",
|
|
32
|
+
"src": "skills/code-review"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "skill",
|
|
36
|
+
"src": "skills/semantic-html"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"type": "skill",
|
|
40
|
+
"src": "skills/css-coder"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
26
43
|
}
|
|
27
44
|
]
|
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 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":";;;;;;;;;;;;;;;;;;;;AAiCA,MAAM,eAAe,QAAQ,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,KAAK;AAC3E,MAAM,YAAY,KAAK,cAAc,QAAQ;AAC7C,MAAM,aAAa,KAAK,cAAc,SAAS;AAC/C,MAAM,cAAc,KAAK,cAAc,cAAc;AAErD,MAAM,eAAe,QAAQ,KAAK;AAClC,MAAM,aAAa,KAAK,cAAc,UAAU;AAChD,MAAM,cAAc,KAAK,cAAc,kBAAkB;AACzD,MAAM,gBAAgB,KAAK,YAAY,wBAAwB;AA0B/D,SAAS,QAAgB;CACvB,wBAAO,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG;;AAG9C,SAAS,UAAU,SAAkC;CACnD,OAAO,WAAW,SAAS,CAAC,OAAO,QAAQ,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,EAAE;;AAGvE,SAAS,eAAyB;CAChC,IAAI,CAAC,WAAW,cAAc,EAC5B,OAAO;EAAE,OAAO,EAAE;EAAE,QAAQ,EAAE;EAAE;CAGlC,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,aAAa,eAAe,OAAO,CAAC;EAC9D,OAAO;GACL,OAAO,OAAO,SAAS,EAAE;GACzB,QAAQ,OAAO,UAAU,EAAE;GAC5B;SACK;EACN,OAAO;GAAE,OAAO,EAAE;GAAE,QAAQ,EAAE;GAAE;;;AAIpC,SAAS,cAAc,GAAmB;CACxC,UAAU,YAAY,EAAE,WAAW,MAAM,CAAC;CAC1C,cAAc,eAAe,KAAK,UAAU,GAAG,MAAM,EAAE,GAAG,KAAK;;AAGjE,SAAS,cAAc,GAA0C;CAC/D,OAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,EAAE;;AAGjE,SAAS,UAAa,QAAW,QAAc;CAC7C,IAAI,MAAM,QAAQ,OAAO,IAAI,MAAM,QAAQ,OAAO,EAChD,OAAO,CAAC,GAAG,QAAQ,GAAG,OAAO;CAE/B,IAAI,cAAc,OAAO,IAAI,cAAc,OAAO,EAAE;EAClD,MAAM,MAA+B,EAAE,GAAG,QAAQ;EAClD,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,OAAO,EACzC,IAAI,KAAK,KAAK,MAAM,UAAU,IAAI,IAAI,EAAE,GAAG;EAE7C,OAAO;;CAET,OAAO;;AAGT,SAAS,eAAe,MAAsB;CAE5C,OAAO,UAAU,aADP,KAAK,WAAW,MAAM,WACD,CAAC,CAAC;;AAGnC,SAAS,gBAAgB,MAAsB;CAC7C,MAAM,MAAM,KAAK,YAAY,KAAK;CAClC,MAAM,QAAQ,aAAa,IAAI,CAAC,MAAM;CACtC,MAAM,IAAI,WAAW,SAAS;CAC9B,KAAK,MAAM,KAAK,OAAO;EACrB,EAAE,OAAO,SAAS,KAAK,EAAE,CAAC;EAC1B,EAAE,OAAO,KAAK;EACd,EAAE,OAAO,aAAa,EAAE,CAAC;EACzB,EAAE,OAAO,KAAK;;CAEhB,OAAO,EAAE,OAAO,MAAM,CAAC,MAAM,GAAG,EAAE;;AAGpC,SAAS,aAAa,KAAuB;CAC3C,MAAM,MAAgB,EAAE;CACxB,IAAI,CAAC,WAAW,IAAI,EAClB,OAAO;CAGT,KAAK,MAAM,SAAS,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC,EAAE;EAC7D,IAAI,MAAM,SAAS,YACjB;EAGF,MAAM,OAAO,KAAK,KAAK,MAAM,KAAK;EAClC,IAAI,MAAM,aAAa,EACrB,IAAI,KAAK,GAAG,aAAa,KAAK,CAAC;OAC1B,IAAI,MAAM,QAAQ,EACvB,IAAI,KAAK,KAAK;;CAGlB,OAAO;;AAGT,eAAe,QAAQ,UAAoC;CACzD,MAAM,KAAK,gBAAgB;EAAE,OAAO,QAAQ;EAAO,QAAQ,QAAQ;EAAQ,CAAC;CAC5E,MAAM,UAAU,MAAM,GAAG,SAAS,GAAG,SAAS,SAAS,EAAE,MAAM,CAAC,aAAa;CAC7E,GAAG,OAAO;CACV,OAAO,WAAW,OAAO,WAAW;;AAGtC,SAAS,UAAU,QAAgB,QAAwB;CACzD,MAAM,IAAI,OAAO,MAAM,KAAK;CAC5B,MAAM,IAAI,OAAO,MAAM,KAAK;CAC5B,MAAM,MAAgB,EAAE;CACxB,MAAM,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,OAAO;CACxC,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK;EAC5B,IAAI,EAAE,OAAO,EAAE,IACb;EAGF,IAAI,EAAE,OAAO,KAAA,GACX,IAAI,KAAK,KAAK,EAAE,KAAK;EAGvB,IAAI,EAAE,OAAO,KAAA,GACX,IAAI,KAAK,KAAK,EAAE,KAAK;;CAGzB,OAAO,IAAI,KAAK,KAAK;;AAKvB,SAAS,aAAa,MAAc,MAAsB;CACxD,OAAO,SAAS,KAAK;CACrB,IAAI,CAAC,MAAM;EACT,QAAQ,MAAM,WAAW,KAAK,OAAO;EACrC,QAAQ,KAAK,EAAE;;CAEjB,OAAO;;AAGT,SAAS,4BACP,MACA,gBACoB;CACpB,IAAI,SAAS,UAAU,SAAS,SAC9B,OAAO;CAET,IAAI,SAAS,WAAW,SAAS,UAC/B,OAAO;CAGT,MAAM,IAAI,MAAM,eAAe,eAAe,+BAA+B,KAAK,GAAG;;AAGvF,SAAS,kBAAkB,KAAa,MAAc,gBAAgC;CACpF,MAAM,aAAa,QAAQ,cAAc,IAAI;CAC7C,IAAI,CAAC,WAAW,WAAW,eAAe,IAAI,EAC5C,MAAM,IAAI,MACR,eAAe,eAAe,IAAI,KAAK,6CAA6C,MACrF;CAEH,OAAO;;AAGT,SAAS,wBACP,MACA,YACA,gBACQ;CACR,MAAM,eAAe,SAAS,SAAS,YAAY;CACnD,IAAI,QAAQ,WAAW,KAAK,gBAAgB,CAAC,WAAW,WAAW,eAAe,IAAI,EACpF,MAAM,IAAI,MACR,eAAe,eAAe,IAAI,KAAK,gDAAgD,SAAS,cAAc,aAAa,CAAC,KAAK,SAAS,cAAc,WAAW,GACpK;CAGH,OAAO,SAAS,WAAW;;AAG7B,SAAS,wBAA4C;CACnD,IAAI,CAAC,WAAW,YAAY,EAC1B,MAAM,IAAI,MAAM,iCAAiC,SAAS,cAAc,YAAY,GAAG;CAGzF,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,aAAa,aAAa,OAAO,CAAC;UAC/C,OAAO;EACd,MAAM,IAAI,MACR,iCAAiC,SAAS,cAAc,YAAY,CAAC,IACnE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GAEzD;;CAGH,IAAI,CAAC,MAAM,QAAQ,OAAO,EACxB,MAAM,IAAI,MAAM,sCAAsC;CAGxD,MAAM,wBAAQ,IAAI,KAAa;CAC/B,OAAO,OAAO,KAAK,OAAO,UAAU;EAClC,IAAI,CAAC,cAAc,MAAM,EACvB,MAAM,IAAI,MAAM,uBAAuB,MAAM,oBAAoB;EAGnE,MAAM,EAAE,MAAM,UAAU;EACxB,IAAI,OAAO,SAAS,YAAY,KAAK,MAAM,CAAC,WAAW,GACrD,MAAM,IAAI,MAAM,uBAAuB,MAAM,6BAA6B;EAE5E,IAAI,MAAM,IAAI,KAAK,EACjB,MAAM,IAAI,MAAM,8BAA8B,OAAO;EAEvD,MAAM,IAAI,KAAK;EAEf,IAAI,CAAC,MAAM,QAAQ,MAAM,EACvB,MAAM,IAAI,MAAM,eAAe,KAAK,4BAA4B;EAwBlE,OAAO;GACL;GACA,OAvBqB,MAAM,KAAK,MAAM,cAAc;IACpD,IAAI,CAAC,cAAc,KAAK,EACtB,MAAM,IAAI,MAAM,eAAe,KAAK,kBAAkB,UAAU,oBAAoB;IAEtF,IAAI,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,MAAM,CAAC,WAAW,GAC/D,MAAM,IAAI,MACR,eAAe,KAAK,kBAAkB,UAAU,6BACjD;IAEH,IAAI,OAAO,KAAK,QAAQ,YAAY,KAAK,IAAI,MAAM,CAAC,WAAW,GAC7D,MAAM,IAAI,MACR,eAAe,KAAK,kBAAkB,UAAU,4BACjD;IAGH,OAAO;KACL,MAAM,KAAK;KACX,KAAK,KAAK;KACX;KAKoB;GACtB;GACD;;AAGJ,SAAS,kBAAkB,MAAwC;CACjE,MAAM,iBAAiB,aAAa,MAAM,aAAa;CAEvD,MAAM,aADc,uBACU,CAAC,MAAM,UAAU,MAAM,SAAS,eAAe;CAE7E,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,yBAAyB,iBAAiB;CAG5D,MAAM,0BAAU,IAAI,KAAqC;CAEzD,KAAK,MAAM,QAAQ,WAAW,OAAO;EACnC,MAAM,OAAO,4BAA4B,KAAK,MAAM,WAAW,KAAK;EACpE,MAAM,aAAa,kBAAkB,KAAK,KAAK,MAAM,WAAW,KAAK;EACrE,MAAM,aAAa,wBAAwB,MAAM,YAAY,WAAW,KAAK;EAC7E,MAAM,MAAM,GAAG,KAAK,GAAG;EAEvB,IAAI,CAAC,QAAQ,IAAI,IAAI,EACnB,QAAQ,IAAI,KAAK;GACf,YAAY,WAAW;GACvB;GACA;GACA;GACD,CAAC;;CAIN,OAAO,CAAC,GAAG,QAAQ,QAAQ,CAAC;;AAG9B,SAAS,YAAY,MAAc,QAAsB;CACvD,IAAI,CAAC,WAAW,OAAO,EAAE;EACvB,QAAQ,MAAM,mBAAmB,OAAO;EACxC,QAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,KAAK,QAAQ,WAAW;CACxC,MAAM,eAAe,KAAK,QAAQ,yBAAyB;CAE3D,MAAM,WAAW,KAAK,YAAY,QAAQ;CAC1C,UAAU,UAAU,EAAE,WAAW,MAAM,CAAC;CACxC,MAAM,WAAW,QAAQ,UAAU,GAAG,KAAK,MAAM;CACjD,IAAI,CAAC,SAAS,WAAW,WAAW,IAAI,EAAE;EACxC,QAAQ,MAAM,oBAAoB;EAClC,QAAQ,KAAK,EAAE;;CAEjB,cAAc,UAAU,aAAa,QAAQ,CAAC;CAE9C,IAAI,WAAW,aAAa,EAAE;EAC5B,MAAM,WAAW,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;EAC/D,MAAM,eAAe,KAAK,YAAY,gBAAgB;EAEtD,MAAM,SAAS,UADC,WAAW,aAAa,GAAG,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC,GAAG,EAAE,EAC5D,SAAS;EAC3C,cAAc,cAAc,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG,KAAK;;CAGrE,MAAM,WAAW,cAAc;CAC/B,SAAS,MAAM,QAAQ;EAAE,MAAM,eAAe,KAAK;EAAE,aAAa,OAAO;EAAE;CAC3E,cAAc,SAAS;CAEvB,QAAQ,IAAI,mBAAmB,KAAK,KAAK,SAAS,cAAc,SAAS,GAAG;;AAG9E,SAAS,QAAQ,MAAoB;CACnC,OAAO,aAAa,MAAM,OAAO;CACjC,YAAY,MAAM,KAAK,WAAW,KAAK,CAAC;;AAG1C,SAAS,aAAa,MAAc,QAAgB,OAAuB;CACzE,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC,SAAS,OAAO,CAAC,aAAa,EAAE;EAC1D,QAAQ,MAAM,oBAAoB,OAAO;EACzC,QAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,QAAQ,aAAa,UAAU,KAAK;CACpD,IAAI,CAAC,QAAQ,WAAW,KAAK,aAAa,SAAS,GAAG,IAAI,EAAE;EAC1D,QAAQ,MAAM,qBAAqB;EACnC,QAAQ,KAAK,EAAE;;CAEjB,UAAU,QAAQ,QAAQ,EAAE,EAAE,WAAW,MAAM,CAAC;CAChD,OAAO,QAAQ,SAAS,EAAE,WAAW,MAAM,CAAC;CAE5C,MAAM,gBAAgB,MAAM,SAAS,IAAI,QAAQ,CAAC,KAAK,WAAW,SAAS,CAAC;CAC5E,KAAK,MAAM,QAAQ,eAAe;EAChC,MAAM,UAAU,QAAQ,cAAc,KAAK;EAC3C,UAAU,SAAS,EAAE,WAAW,MAAM,CAAC;EAEvC,MAAM,WAAW,KAAK,SAAS,KAAK;EACpC,IAAI,WAAW,SAAS,IAAI,YAAY,SAAS,EAC/C,WAAW,SAAS;EAItB,YADkB,SAAS,SAAS,QACf,EAAE,UAAU,MAAM;;CAGzC,MAAM,WAAW,cAAc;CAC/B,SAAS,OAAO,QAAQ;EACtB,MAAM,gBAAgB,KAAK;EAC3B,aAAa,OAAO;EACpB,UAAU;EACX;CACD,cAAc,SAAS;CAEvB,QAAQ,IAAI,oBAAoB,KAAK,KAAK,SAAS,cAAc,QAAQ,GAAG;CAC5E,KAAK,MAAM,KAAK,eACd,QAAQ,IAAI,aAAa,KAAK,GAAG,KAAK,GAAG;;AAI7C,SAAS,SAAS,MAAc,OAAuB;CACrD,OAAO,aAAa,MAAM,QAAQ;CAClC,aAAa,MAAM,KAAK,YAAY,KAAK,EAAE,MAAM;;AAGnD,SAAS,cAAc,MAAoB;CACzC,MAAM,QAAQ,kBAAkB,KAAK;CACrC,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,CAAC,WAAW,KAAK,WAAW,EAC9B,MAAM,IAAI,MACR,eAAe,KAAK,WAAW,uBAAuB,KAAK,KAAK,WAAW,SAAS,cAAc,KAAK,WAAW,GACnH;EAGH,MAAM,YAAY,SAAS,KAAK,WAAW;EAC3C,MAAM,aAAa,UAAU,QAAQ,GACjC,SACA,UAAU,aAAa,GACrB,cACA;EAEN,IAAI,KAAK,SAAS,QAAQ;GACxB,IAAI,CAAC,UAAU,aAAa,EAC1B,MAAM,IAAI,MACR,eAAe,KAAK,WAAW,0BAA0B,KAAK,WAAW,6BAA6B,aACvG;GAEH,YAAY,KAAK,YAAY,KAAK,WAAW;GAC7C;;EAGF,IAAI,CAAC,UAAU,aAAa,EAC1B,MAAM,IAAI,MACR,eAAe,KAAK,WAAW,2BAA2B,KAAK,WAAW,6BAA6B,aACxG;EAEH,aAAa,KAAK,YAAY,KAAK,YAAY,EAAE,CAAC;;;AAItD,SAAS,YAAY,GAAoB;CACvC,IAAI;EACF,UAAU,EAAE;EACZ,OAAO;SACD;EACN,OAAO;;;AAIX,eAAe,OAAO,OAA+B;CACnD,MAAM,WAAW,cAAc;CAC/B,IAAI,UAAU;CAEd,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,SAAS,MAAM,EAAE;EAC1D,MAAM,SAAS,KAAK,WAAW,KAAK;EACpC,IAAI,CAAC,WAAW,OAAO,EACrB;EAGF,MAAM,aAAa,eAAe,KAAK;EACvC,MAAM,gBAAgB,KAAK,YAAY,SAAS,GAAG,KAAK,MAAM;EAC9D,MAAM,gBAAgB,WAAW,cAAc,GAAG,UAAU,aAAa,cAAc,CAAC,GAAG;EAE3F,MAAM,gBAAgB,eAAe,MAAM;EAC3C,MAAM,kBAAkB,kBAAkB,QAAQ,kBAAkB,MAAM;EAE1E,IAAI,CAAC,iBAAiB,CAAC,iBACrB;EAGF,UAAU;EAEV,IAAI,mBAAmB,CAAC,OAAO;GAC7B,QAAQ,KACN,WAAW,KAAK,oCAAoC,cAAc,aAAa,MAAM,KAAK,8BAC3F;GACD;;EAGF,IAAI,eAAe;GACjB,MAAM,SAAS,WAAW,cAAc,GAAG,aAAa,eAAe,OAAO,GAAG;GACjF,MAAM,SAAS,aAAa,KAAK,QAAQ,WAAW,EAAE,OAAO;GAC7D,QAAQ,IAAI,aAAa,KAAK,IAAI,MAAM,KAAK,KAAK,WAAW,GAAG;GAChE,QAAQ,IAAI,UAAU,QAAQ,OAAO,CAAC;GAGtC,IAAI,EAFO,SAAU,MAAM,QAAQ,gBAAgB,KAAK,IAAI,GAG1D;GAGF,cAAc,eAAe,OAAO;GACpC,SAAS,MAAM,QAAQ;IAAE,MAAM;IAAY,aAAa,OAAO;IAAE;;;CAIrE,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,SAAS,OAAO,EAAE;EAC3D,MAAM,SAAS,KAAK,YAAY,KAAK;EACrC,IAAI,CAAC,WAAW,OAAO,EACrB;EAGF,MAAM,aAAa,gBAAgB,KAAK;EACxC,IAAI,eAAe,MAAM,MACvB;EAGF,UAAU;EACV,QAAQ,IAAI,cAAc,KAAK,IAAI,MAAM,KAAK,KAAK,WAAW,GAAG;EAEjE,IAAI,EADO,SAAU,MAAM,QAAQ,iBAAiB,KAAK,IAAI,GAE3D;EAIF,OAAO,QADS,KAAK,aAAa,UAAU,KACtB,EAAE;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC;EACzD,SAAS,OAAO,QAAQ;GACtB,MAAM;GACN,aAAa,OAAO;GACpB,UAAU,MAAM;GACjB;;CAGH,IAAI,SACF,cAAc,SAAS;;AAI3B,SAAS,KAAK,MAA8B;CAC1C,MAAM,MAAM,SAAS,SAAS,YAAY;CAC1C,IAAI,CAAC,WAAW,IAAI,EAAE;EACpB,QAAQ,IAAI,OAAO,KAAK,cAAc;EACtC;;CAEF,MAAM,UAAU,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC,CACtD,QAAQ,MAAM,EAAE,aAAa,IAAK,SAAS,WAAW,EAAE,gBAAgB,CAAE,CAC1E,KAAK,MAAM,EAAE,KAAK;CAErB,IAAI,QAAQ,WAAW,GAAG;EACxB,QAAQ,IAAI,OAAO,KAAK,cAAc;EACtC;;CAGF,KAAK,MAAM,QAAQ,SAAS;EAC1B,MAAM,OAAO,SAAS,SAAS,eAAe,KAAK,GAAG,gBAAgB,KAAK;EAC3E,QAAQ,IAAI,GAAG,KAAK,IAAI,OAAO;;;AAInC,SAAS,kBAAwB;CAC/B,MAAM,cAAc,uBAAuB;CAC3C,IAAI,YAAY,WAAW,GAAG;EAC5B,QAAQ,IAAI,6BAA6B;EACzC;;CAGF,KAAK,MAAM,cAAc,aACvB,QAAQ,IAAI,GAAG,WAAW,KAAK,IAAI,WAAW,MAAM,OAAO,UAAU;;AAMzE,SAAS,QAAe;CACtB,QAAQ,MACN;;;;;;;4BAQD;CACD,QAAQ,KAAK,EAAE;;AAGjB,eAAe,OAAsB;CACnC,MAAM,EAAE,QAAQ,gBAAgB,UAAU;EACxC,SAAS;GACP,OAAO;IACL,SAAS;IACT,MAAM;IACP;GACD,OAAO;IACL,UAAU;IACV,MAAM;IACP;GACF;EACD,kBAAkB;EACnB,CAAC;CAEF,MAAM,EAAE,OAAO,UAAU;CACzB,MAAM,CAAC,SAAS,UAAU,QAAQ;CAElC,IAAI,YAAY,SAAS,aAAa,QAAQ;EAC5C,IAAI,CAAC,MACH,OAAO;EAGT,QAAQ,KAAK;EACb;;CAGF,IAAI,YAAY,SAAS,aAAa,SAAS;EAC7C,IAAI,CAAC,MACH,OAAO;EAGT,SAAS,MAAM,QAAQ,QAAQ,EAAE,CAAC;EAClC;;CAGF,IAAI,YAAY,UAAU,aAAa,gBAAgB,aAAa,gBAAgB;EAClF,IAAI,CAAC,MACH,OAAO;EAGT,cAAc,KAAK;EACnB;;CAGF,IAAI,YAAY,UAAU;EACxB,MAAM,OAAO,MAAM;EACnB;;CAGF,IAAI,YAAY,WAAW,aAAa,UAAU,aAAa,UAAU;EACvE,KAAK,SAA6B;EAClC;;CAGF,IAAI,YAAY,WAAW,aAAa,gBAAgB,aAAa,gBAAgB;EACnF,iBAAiB;EACjB;;CAGF,OAAO;;AAGT,MAAM,CAAC,OAAO,QAAQ;CACpB,QAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC;CAC/D,QAAQ,KAAK,EAAE;EACf"}
|
|
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,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schalkneethling/toolkit",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI for managing
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "CLI for managing Agent hooks, skills, and agents across projects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
7
|
"toolkit": "./dist/index.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"config.json",
|
|
11
|
+
"agents",
|
|
11
12
|
"dist",
|
|
12
13
|
"hooks",
|
|
13
14
|
"!**/*.ts",
|
|
@@ -19,16 +20,14 @@
|
|
|
19
20
|
"publishConfig": {
|
|
20
21
|
"access": "public"
|
|
21
22
|
},
|
|
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
|
-
},
|
|
27
23
|
"devDependencies": {
|
|
28
24
|
"@types/node": "^25.6.0",
|
|
29
25
|
"tsx": "^4.21.0",
|
|
30
26
|
"typescript": "^6.0.2",
|
|
31
27
|
"vite-plus": "^0.1.18"
|
|
32
28
|
},
|
|
33
|
-
"
|
|
34
|
-
|
|
29
|
+
"scripts": {
|
|
30
|
+
"toolkit": "tsx src/index.ts",
|
|
31
|
+
"build:hooks": "tsc --project tsconfig.hooks.json"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -38,6 +38,15 @@ Review code to catch issues the original engineer may have missed and to improve
|
|
|
38
38
|
- Mark non-blocking refactors as follow-up suggestions, not merge blockers.
|
|
39
39
|
- If no issues are found, say so clearly and mention any residual test or tooling gaps.
|
|
40
40
|
|
|
41
|
+
## Context Skills
|
|
42
|
+
|
|
43
|
+
Load related skills only when the diff makes that domain relevant:
|
|
44
|
+
|
|
45
|
+
- Load `semantic-html` when reviewing markup-producing code: HTML templates, JSX/TSX, Astro/Vue/Svelte components, Lit templates, Twig/ERB/Handlebars/Nunjucks, MDX, or code that materially changes rendered HTML structure.
|
|
46
|
+
- Load `css-coder` when reviewing style-producing code: CSS, Sass/Less, CSS modules, scoped component styles, CSS-in-JS, design tokens, or code that materially changes selectors, layout, cascade, responsive behavior, colors, motion, or focus states.
|
|
47
|
+
|
|
48
|
+
For large or high-risk diffs, consider a focused specialist subagent for the relevant skill. Pass only the user request, relevant diff, surrounding component context, and known project conventions. Reconcile specialist feedback into one prioritized final review rather than forwarding it verbatim.
|
|
49
|
+
|
|
41
50
|
## Setup Deficiencies
|
|
42
51
|
|
|
43
52
|
If essential review infrastructure is missing, call it out early before deep findings:
|
|
@@ -299,18 +299,44 @@ Font rendering varies by OS. Options:
|
|
|
299
299
|
|
|
300
300
|
## CI/CD Configuration
|
|
301
301
|
|
|
302
|
+
When adding GitHub Actions examples or workflow files, declare explicit least-privilege
|
|
303
|
+
`GITHUB_TOKEN` permissions. Use a restrictive top-level default such as `contents: read`, then add
|
|
304
|
+
job-level permissions only for jobs that need to push commits, update pull requests, publish pages,
|
|
305
|
+
or write other repository resources.
|
|
306
|
+
|
|
307
|
+
Pin every third-party or GitHub-owned action to a full-length commit SHA. Do not commit tag-based
|
|
308
|
+
refs such as `@v6` or `@v7`; use those only as temporary input to a pinning tool, then keep the
|
|
309
|
+
version as a comment beside the SHA.
|
|
310
|
+
|
|
311
|
+
When a job checks out the repository, set `persist-credentials: false` on `actions/checkout`.
|
|
312
|
+
Artifact-producing jobs should not keep the repository token persisted for later test, build, or
|
|
313
|
+
upload steps.
|
|
314
|
+
|
|
302
315
|
### GitHub Actions Example
|
|
303
316
|
|
|
304
317
|
```yaml
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
318
|
+
permissions:
|
|
319
|
+
contents: read
|
|
320
|
+
|
|
321
|
+
jobs:
|
|
322
|
+
visual-tests:
|
|
323
|
+
permissions:
|
|
324
|
+
contents: read
|
|
325
|
+
steps:
|
|
326
|
+
- name: Checkout
|
|
327
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 - re-resolve before use
|
|
328
|
+
with:
|
|
329
|
+
persist-credentials: false
|
|
330
|
+
|
|
331
|
+
- name: Run visual tests
|
|
332
|
+
run: npx playwright test --project=chromium
|
|
333
|
+
|
|
334
|
+
- name: Upload diff artifacts on failure
|
|
335
|
+
if: failure()
|
|
336
|
+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 - re-resolve before use
|
|
337
|
+
with:
|
|
338
|
+
name: visual-diff
|
|
339
|
+
path: test-results/
|
|
314
340
|
```
|
|
315
341
|
|
|
316
342
|
### Generate Baselines in CI
|
|
@@ -318,15 +344,31 @@ Font rendering varies by OS. Options:
|
|
|
318
344
|
For consistent baselines, generate in CI:
|
|
319
345
|
|
|
320
346
|
```yaml
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
347
|
+
permissions:
|
|
348
|
+
contents: read
|
|
349
|
+
|
|
350
|
+
jobs:
|
|
351
|
+
update-baselines:
|
|
352
|
+
permissions:
|
|
353
|
+
contents: write
|
|
354
|
+
steps:
|
|
355
|
+
- name: Checkout
|
|
356
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 - re-resolve before use
|
|
357
|
+
with:
|
|
358
|
+
persist-credentials: false
|
|
359
|
+
|
|
360
|
+
- name: Update baselines
|
|
361
|
+
run: npx playwright test --update-snapshots
|
|
362
|
+
|
|
363
|
+
- name: Commit baselines
|
|
364
|
+
env:
|
|
365
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
366
|
+
run: |
|
|
367
|
+
git config user.name "CI Bot"
|
|
368
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
369
|
+
git add "**/*.png"
|
|
370
|
+
git commit -m "Update visual baselines"
|
|
371
|
+
git -c http.https://github.com/.extraheader="AUTHORIZATION: bearer $GITHUB_TOKEN" push origin HEAD:refs/heads/${GITHUB_REF_NAME}
|
|
330
372
|
```
|
|
331
373
|
|
|
332
374
|
## Organizing Screenshots
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: github-goal-issue-triage
|
|
3
|
+
description: Triage open GitHub issues across one or more repositories against each repository's GOAL.md, create missing p0/p1/p2/p3 priority labels, apply exactly one priority label to each open issue, nominate the next issue to work on per repository, and generate a clean HTML report grouped by project. Use when asked to prioritize, label, rank, audit, or report on GitHub issues based on project goals.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GitHub Goal Issue Triage
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Use this skill to turn a list of GitHub repositories into an applied issue-priority pass and a readable HTML report. Treat `GOAL.md` as the project's north star; without it, flag the repository and skip all issue triage for that repository.
|
|
11
|
+
|
|
12
|
+
## Inputs
|
|
13
|
+
|
|
14
|
+
Expect the repository list from one of these sources:
|
|
15
|
+
|
|
16
|
+
- Inline in the user's prompt, separated by spaces, commas, or new lines.
|
|
17
|
+
- A local text, Markdown, JSON, YAML, or CSV file path supplied by the user.
|
|
18
|
+
- Standard input or pasted content from the user.
|
|
19
|
+
- The current GitHub issue, pull request, repository page, or local git remote only when the user explicitly asks to use the current context.
|
|
20
|
+
|
|
21
|
+
Accept repository entries as `owner/name`, `https://github.com/owner/name`, `git@github.com:owner/name.git`, or a JSON/YAML/CSV field clearly named `repo`, `repository`, `repositories`, `url`, or `github_url`. Normalize every entry to `owner/name`, remove duplicates while preserving first-seen order, and ignore blank lines and comments beginning with `#`.
|
|
22
|
+
|
|
23
|
+
If no repository list is provided and no current GitHub context was explicitly requested, ask the user for the repository list before taking action. If the user does not specify an output path, write `github-issue-triage-report.html` in the current working directory.
|
|
24
|
+
|
|
25
|
+
Use the authenticated GitHub connector when available. Otherwise use `gh` or the GitHub REST/GraphQL API. If authentication or write permission is missing, continue in read-only mode and report the labels that would have been applied.
|
|
26
|
+
|
|
27
|
+
## Repository Workflow
|
|
28
|
+
|
|
29
|
+
For each repository, in the user's order:
|
|
30
|
+
|
|
31
|
+
1. Fetch `GOAL.md` from the repository root on the default branch.
|
|
32
|
+
2. If `GOAL.md` is missing, record the repository as skipped, include the reason in the report, and move to the next repository.
|
|
33
|
+
3. Read the project goal and extract the concrete outcomes, audiences, constraints, and near-term signals of progress.
|
|
34
|
+
4. Retrieve all open issues, not pull requests. Use pagination; do not rely on default issue-list limits.
|
|
35
|
+
5. Inspect each issue's title, body, labels, milestone, assignees, comments when needed, age, and recent activity. Avoid changing issue bodies, comments, milestones, state, or assignees unless the user explicitly asks.
|
|
36
|
+
6. Ensure the repository has the labels `p0`, `p1`, `p2`, and `p3`. Create only missing labels and preserve existing label colors/descriptions.
|
|
37
|
+
7. Assign exactly one priority label to each open issue. Remove any other `p0`/`p1`/`p2`/`p3` labels from that issue so priority state is unambiguous. Preserve all non-priority labels.
|
|
38
|
+
8. Nominate one next issue for the repository unless there are no open issues.
|
|
39
|
+
|
|
40
|
+
## Priority Rubric
|
|
41
|
+
|
|
42
|
+
Use goal alignment as the main criterion, then weigh impact, urgency, unblock value, user harm, implementation readiness, and risk.
|
|
43
|
+
|
|
44
|
+
- `p0`: Critical to the project goal now. Blocks core usage, release, trust, security, data integrity, or a prerequisite without which the project cannot make meaningful progress.
|
|
45
|
+
- `p1`: High-impact work that substantially advances the goal, fixes a major user-facing problem, unlocks several other issues, or removes a major adoption or reliability barrier.
|
|
46
|
+
- `p2`: Useful and goal-aligned, but not urgent. Includes medium-impact bugs, incremental improvements, important documentation, and work that helps after higher-priority blockers are handled.
|
|
47
|
+
- `p3`: Low immediate impact, weak goal alignment, polish, speculative ideas, minor cleanup, stale work, or nice-to-have improvements.
|
|
48
|
+
|
|
49
|
+
When evidence is thin, choose the lower priority and explain the uncertainty in the issue rationale.
|
|
50
|
+
|
|
51
|
+
## Next Issue Nomination
|
|
52
|
+
|
|
53
|
+
Nominate the issue that should be worked on next, not necessarily the smallest issue.
|
|
54
|
+
|
|
55
|
+
Prefer the highest-priority issue that:
|
|
56
|
+
|
|
57
|
+
- Most directly advances the stated goal.
|
|
58
|
+
- Unblocks other issues or users.
|
|
59
|
+
- Has enough context to start.
|
|
60
|
+
- Has a reasonable scope for the next focused work session.
|
|
61
|
+
- Carries lower coordination risk than equally important alternatives.
|
|
62
|
+
|
|
63
|
+
If the top issue is too ambiguous, nominate a clearer issue and note what context would be needed before the ambiguous one becomes actionable.
|
|
64
|
+
|
|
65
|
+
## GitHub Label Details
|
|
66
|
+
|
|
67
|
+
Create missing labels with these defaults unless the repository already defines them:
|
|
68
|
+
|
|
69
|
+
| Label | Color | Description |
|
|
70
|
+
| ----- | -------- | ------------------------------------------------------------------ |
|
|
71
|
+
| `p0` | `b60205` | Critical priority: blocking the project goal or urgent user impact |
|
|
72
|
+
| `p1` | `d93f0b` | High priority: major impact or strong goal alignment |
|
|
73
|
+
| `p2` | `fbca04` | Medium priority: useful, goal-aligned, not urgent |
|
|
74
|
+
| `p3` | `0e8a16` | Low priority: polish, speculative, or weak immediate impact |
|
|
75
|
+
|
|
76
|
+
If label creation or issue updates fail, capture the error and include a read-only triage result in the report. Do not claim labels were applied unless the update succeeded.
|
|
77
|
+
|
|
78
|
+
## HTML Report Requirements
|
|
79
|
+
|
|
80
|
+
Generate a self-contained HTML file with readable CSS and no external dependencies. Escape issue titles, labels, body excerpts, and rationale text before writing HTML.
|
|
81
|
+
|
|
82
|
+
The report must include:
|
|
83
|
+
|
|
84
|
+
- Generation timestamp.
|
|
85
|
+
- Input repository list.
|
|
86
|
+
- A summary section with counts by repository: skipped, total open issues, p0, p1, p2, p3, and nominated next issue.
|
|
87
|
+
- One `<details>` element per repository, with a `<summary>` that includes the repository name, status, priority counts, and nominee.
|
|
88
|
+
- For skipped repositories, a short explanation that `GOAL.md` was missing and no issues were triaged.
|
|
89
|
+
- For triaged repositories, the project goal summary, label-update status, next-issue nomination, and an issue table.
|
|
90
|
+
|
|
91
|
+
Issue table columns:
|
|
92
|
+
|
|
93
|
+
- Priority.
|
|
94
|
+
- Issue link with number and title.
|
|
95
|
+
- Rationale tied to `GOAL.md`.
|
|
96
|
+
- Impact.
|
|
97
|
+
- Current labels after update or intended labels in read-only mode.
|
|
98
|
+
- Notes or uncertainty.
|
|
99
|
+
|
|
100
|
+
Sort issues within each repository by priority (`p0` to `p3`), then by goal alignment and impact. Keep repositories in the input order.
|
|
101
|
+
|
|
102
|
+
Use semantic HTML. A compact structure like this is sufficient:
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<details open>
|
|
106
|
+
<summary>owner/repo - 12 open issues - next: #42 Improve release flow</summary>
|
|
107
|
+
<!-- goal, label status, nominee, issue table -->
|
|
108
|
+
</details>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Quality Checks
|
|
112
|
+
|
|
113
|
+
Before finishing:
|
|
114
|
+
|
|
115
|
+
- Verify every input repository has either a skipped reason or complete triage results.
|
|
116
|
+
- Verify every open issue in triaged repositories has exactly one `p0`/`p1`/`p2`/`p3` label applied or listed as intended in read-only mode.
|
|
117
|
+
- Verify the nominee for each repository links to an open issue from that repository.
|
|
118
|
+
- Verify the HTML file opens without missing resources and all issue links are absolute GitHub URLs.
|
|
119
|
+
- Summarize what changed on GitHub and where the HTML report was written.
|
|
@@ -68,7 +68,15 @@ User or authenticated UI/API access required:
|
|
|
68
68
|
- Do not store npm publish tokens in GitHub Actions secrets for OIDC-capable publishing.
|
|
69
69
|
- Do not run install lifecycle scripts in release workflows unless the user explicitly accepts the
|
|
70
70
|
risk.
|
|
71
|
-
- Do not show tag-pinned actions as compliant with SHA pinning; resolve
|
|
71
|
+
- Do not show tag-pinned actions as compliant with SHA pinning; resolve all third-party and
|
|
72
|
+
GitHub-owned actions to full commit SHAs. Tag-based refs such as `@v6` or `@v7` weaken
|
|
73
|
+
supply-chain integrity and violate pinned-action policy.
|
|
74
|
+
- Do not create or edit GitHub Actions workflows without explicit least-privilege `permissions:`
|
|
75
|
+
for `GITHUB_TOKEN`. Set a restrictive top-level default such as `contents: read`, then grant
|
|
76
|
+
elevated scopes only on the jobs that need them.
|
|
77
|
+
- Do not use `actions/checkout` with default credential persistence. Always set
|
|
78
|
+
`persist-credentials: false`; if a job must push to git, provide an explicit, narrowly scoped
|
|
79
|
+
credential only for that push step.
|
|
72
80
|
- Do not say npm/GitHub account settings are complete unless they were actually checked.
|
|
73
81
|
- Do not use self-hosted runners for npm trusted publishing unless official npm docs currently
|
|
74
82
|
support them.
|
|
@@ -90,6 +98,14 @@ User or authenticated UI/API access required:
|
|
|
90
98
|
> Section 2.2's trusted-publishing requirement is npm-specific: the publish step must run with a
|
|
91
99
|
> supported Node.js version and npm CLI version even when the rest of the workflow uses another
|
|
92
100
|
> package manager.
|
|
101
|
+
>
|
|
102
|
+
> For pnpm projects on GitHub Actions, prefer the Marketplace action
|
|
103
|
+
> [`pnpm/setup`](https://github.com/marketplace/actions/setup-pnpm-with-runtime) to set up pnpm and
|
|
104
|
+
> the JavaScript runtime in one step. It installs pnpm from `@pnpm/exe`, can read
|
|
105
|
+
> `devEngines.runtime` from `package.json`, and replaces the separate
|
|
106
|
+
> `pnpm/action-setup` + `actions/setup-node` pattern. In release workflows, pin `pnpm/setup` to a
|
|
107
|
+
> full commit SHA, set `install: false`, and run the explicit hardened install command yourself so
|
|
108
|
+
> flags like `--frozen-lockfile --ignore-scripts` remain visible.
|
|
93
109
|
|
|
94
110
|
---
|
|
95
111
|
|
|
@@ -186,10 +202,27 @@ test → build → publish
|
|
|
186
202
|
|
|
187
203
|
| Constraint | Why |
|
|
188
204
|
| ------------------------------------------------------ | ----------------------------------------------------------- |
|
|
189
|
-
| All actions pinned to full-length commit SHA | Prevents supply-chain attacks via
|
|
205
|
+
| All actions pinned to full-length commit SHA | Prevents supply-chain attacks via mutable tags |
|
|
206
|
+
| Explicit least-privilege `GITHUB_TOKEN` permissions | Avoids inheriting broad repository defaults |
|
|
207
|
+
| `actions/checkout` uses `persist-credentials: false` | Reduces token exposure to later workflow steps |
|
|
190
208
|
| `npm ci --ignore-scripts` (or `--ignore-scripts` flag) | Prevents malicious lifecycle scripts running during install |
|
|
191
209
|
| Build and publish in **separate jobs** | Isolates publish permissions from arbitrary build code |
|
|
192
210
|
|
|
211
|
+
Always include a top-level `permissions:` block with the workflow-wide minimum, usually
|
|
212
|
+
`contents: read`. Add job-level permissions only when a job requires more access; for npm trusted
|
|
213
|
+
publishing, `id-token: write` belongs on the publish job only.
|
|
214
|
+
|
|
215
|
+
Every checkout step must disable persisted credentials:
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
- uses: actions/checkout@<full-length-commit-sha>
|
|
219
|
+
with:
|
|
220
|
+
persist-credentials: false
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This is especially important for jobs that build packages or upload artifacts, because later steps
|
|
224
|
+
should not inherit a repository token unless they explicitly need one.
|
|
225
|
+
|
|
193
226
|
### 3.3 · Suppress lifecycle scripts project-wide
|
|
194
227
|
|
|
195
228
|
Add to `.npmrc` in the repository:
|
|
@@ -245,8 +278,9 @@ addressing security vulnerabilities promptly.
|
|
|
245
278
|
|
|
246
279
|
### 5.2 · Keep GitHub Actions updated
|
|
247
280
|
|
|
248
|
-
All actions must be pinned to a commit SHA
|
|
249
|
-
|
|
281
|
+
All third-party and GitHub-owned actions must be pinned to a full commit SHA, not a tag. Tag refs
|
|
282
|
+
such as `@v6` or `@v7` are mutable and must not be committed to workflow files. To migrate existing
|
|
283
|
+
workflows and keep them current:
|
|
250
284
|
|
|
251
285
|
```bash
|
|
252
286
|
npx actions-up
|
|
@@ -387,6 +421,8 @@ Use this when setting up a new package or auditing an existing one.
|
|
|
387
421
|
### Workflow hygiene
|
|
388
422
|
|
|
389
423
|
- [ ] Build and publish are separate jobs
|
|
424
|
+
- [ ] Workflows declare explicit least-privilege `GITHUB_TOKEN` permissions
|
|
425
|
+
- [ ] Every `actions/checkout` step sets `persist-credentials: false`
|
|
390
426
|
- [ ] `npm ci --ignore-scripts` used in all install steps
|
|
391
427
|
- [ ] `ignore-scripts=true` in `.npmrc`
|
|
392
428
|
- [ ] `zizmor` passes on all workflow files
|
|
@@ -4,11 +4,11 @@ description: >
|
|
|
4
4
|
Generate, repair, or debug the GitHub Actions workflow FILE that performs an OIDC
|
|
5
5
|
trusted publish of a pnpm package — the concrete publish.yml, its test → build →
|
|
6
6
|
publish job shape, the package tarball artifact handoff, Node-version inference from
|
|
7
|
-
package.json, pnpm setup via pnpm/
|
|
7
|
+
package.json, pnpm and runtime setup via pnpm/setup, the npm-CLI-version upgrade step, and
|
|
8
8
|
repository.url/Sigstore provenance matching. Use when the user wants the actual
|
|
9
9
|
workflow written or fixed, or is debugging a specific CI failure: npm publish
|
|
10
10
|
E404/E403/422, NODE_AUTH_TOKEN appearing unexpectedly, provenance or id-token errors,
|
|
11
|
-
pnpm/
|
|
11
|
+
pnpm/setup version resolution, or runtime version problems.
|
|
12
12
|
For the broader publishing SECURITY POSTURE — account 2FA, repository and branch
|
|
13
13
|
hardening, GitHub environments, changesets versus changelogithub, sole-maintainer risk,
|
|
14
14
|
or auditing an existing pipeline — use the npm-package-publishing skill instead.
|
|
@@ -29,33 +29,51 @@ One number to keep consistent between the two: both skills use Node 24.8.0 or hi
|
|
|
29
29
|
## Workflow
|
|
30
30
|
|
|
31
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.
|
|
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. Never leave third-party or GitHub-owned actions pinned to tag-based refs such as `@v4`, `@v6`, or `@v7` in the final workflow; tag refs weaken supply-chain integrity and violate pinned-action policy. 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
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.
|
|
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 `devEngines.runtime`, `.nvmrc`, `.node-version`, `volta.node`, or CI config; if none exists, ask the developer rather than guessing. For pnpm projects, prefer `pnpm/setup` and either let it read `devEngines.runtime` from `package.json` or set its `runtime` input to the resolved existing target, such as `node@22`. Do not point runtime selection at `engines.node`, which is the consumer compatibility range — an unbounded range like `>=20` can float CI away from the version developers actually run.
|
|
35
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
36
|
6. Ensure every job that reads the repo (including any reading `.nvmrc`) runs `actions/checkout` first.
|
|
37
|
-
7.
|
|
38
|
-
8. `pnpm/
|
|
39
|
-
9.
|
|
40
|
-
10. Set `persist-credentials: false` on every `actions/checkout` step
|
|
37
|
+
7. For pnpm workflows, use `pnpm/setup` from the GitHub Marketplace instead of combining `pnpm/action-setup` with `actions/setup-node`. It installs pnpm from `@pnpm/exe` and installs the requested JavaScript runtime through `pnpm runtime set` in one step. Pin it to a full commit SHA like every other action.
|
|
38
|
+
8. Set `install: false` on `pnpm/setup` in release/publish workflows, then run `pnpm install --frozen-lockfile --ignore-scripts` explicitly. The action can auto-install by default, but release workflows should keep install flags visible and hardened.
|
|
39
|
+
9. Do not use Corepack in release workflows: 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.
|
|
40
|
+
10. Set `persist-credentials: false` on every `actions/checkout` step. Never rely on checkout's default credential persistence. If a workflow genuinely must push to git, use an explicit, narrowly scoped credential only for that push step.
|
|
41
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
42
|
12. Pack into a dedicated artifact directory, usually `package/*.tgz`.
|
|
43
43
|
13. In the publish job, download the artifact to `package`, find the `.tgz`, and publish its resolved path.
|
|
44
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
45
|
15. Add a `concurrency` group keyed on the release so two tag pushes cannot race into overlapping publishes.
|
|
46
46
|
|
|
47
|
+
## GitHub Token Permissions
|
|
48
|
+
|
|
49
|
+
Every GitHub Actions workflow this skill creates or edits must declare explicit least-privilege
|
|
50
|
+
`GITHUB_TOKEN` permissions. Add a top-level `permissions:` block that grants the workflow-wide
|
|
51
|
+
minimum, usually `contents: read`, then add job-level `permissions:` only where a job needs more.
|
|
52
|
+
|
|
53
|
+
For trusted npm publishing, only the publish job should receive `id-token: write`; test and build
|
|
54
|
+
jobs should stay at `contents: read`. If a project genuinely needs another scope, grant it only to
|
|
55
|
+
the specific job that requires it and document why in the workflow review notes. Never rely on
|
|
56
|
+
GitHub's repository default token permissions.
|
|
57
|
+
|
|
58
|
+
Every `actions/checkout` step must include `persist-credentials: false`, including jobs that build
|
|
59
|
+
or upload artifacts. Persisted checkout credentials unnecessarily leave `GITHUB_TOKEN` available to
|
|
60
|
+
later build, test, packaging, and artifact steps.
|
|
61
|
+
|
|
47
62
|
## Package Metadata
|
|
48
63
|
|
|
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
|
|
64
|
+
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 `devEngines.runtime` or the existing `.nvmrc`/`.node-version`/`volta.node`/CI config; these are development and CI targets, so they do 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
65
|
|
|
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
|
|
66
|
+
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` or `devEngines.runtime` 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 plays no role in building the artifact.
|
|
52
67
|
|
|
53
68
|
```json
|
|
54
69
|
{
|
|
55
70
|
"engines": {
|
|
56
71
|
"node": ">=20"
|
|
57
72
|
},
|
|
58
|
-
"packageManager": "pnpm@
|
|
73
|
+
"packageManager": "pnpm@11.0.4",
|
|
74
|
+
"devEngines": {
|
|
75
|
+
"runtime": { "name": "node", "version": "^22.0.0", "onFail": "download" }
|
|
76
|
+
},
|
|
59
77
|
"repository": {
|
|
60
78
|
"type": "git",
|
|
61
79
|
"url": "git+https://github.com/OWNER/REPO.git"
|
|
@@ -63,9 +81,13 @@ The publish-step version must never be copied into the other two. Do not raise `
|
|
|
63
81
|
}
|
|
64
82
|
```
|
|
65
83
|
|
|
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
|
|
84
|
+
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 `devEngines.runtime` or another existing project target, so the trusted-publishing requirement never leaks into the consumer contract.
|
|
67
85
|
|
|
68
|
-
|
|
86
|
+
For pnpm projects, prefer declaring the development runtime in `devEngines.runtime` so `pnpm/setup`
|
|
87
|
+
can read the runtime and version from `package.json`. If the repo already uses `.nvmrc`,
|
|
88
|
+
`.node-version`, `volta.node`, or existing CI config instead, keep that source of truth and set
|
|
89
|
+
`pnpm/setup`'s `runtime` input to the same resolved version. Do not default to the publish step's
|
|
90
|
+
24.8.0.
|
|
69
91
|
|
|
70
92
|
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
93
|
|
|
@@ -105,15 +127,13 @@ jobs:
|
|
|
105
127
|
with:
|
|
106
128
|
persist-credentials: false
|
|
107
129
|
|
|
108
|
-
- name:
|
|
109
|
-
uses: pnpm/
|
|
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
|
|
130
|
+
- name: Setup pnpm and Node.js
|
|
131
|
+
uses: pnpm/setup@1111111111111111111111111111111111111111 # v1.0.0 — PLACEHOLDER SHA, re-resolve before use
|
|
114
132
|
with:
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
# Runtime is read from devEngines.runtime when present. If the project uses
|
|
134
|
+
# .nvmrc/.node-version/volta instead, set runtime to that exact target
|
|
135
|
+
# (for example, runtime: node@22). Do not use engines.node here.
|
|
136
|
+
install: false
|
|
117
137
|
|
|
118
138
|
- name: Install dependencies
|
|
119
139
|
run: pnpm install --frozen-lockfile --ignore-scripts
|
|
@@ -137,14 +157,13 @@ jobs:
|
|
|
137
157
|
with:
|
|
138
158
|
persist-credentials: false
|
|
139
159
|
|
|
140
|
-
- name:
|
|
141
|
-
uses: pnpm/
|
|
142
|
-
|
|
143
|
-
- name: Setup Node.js
|
|
144
|
-
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 — PLACEHOLDER SHA, re-resolve before use
|
|
160
|
+
- name: Setup pnpm and Node.js
|
|
161
|
+
uses: pnpm/setup@1111111111111111111111111111111111111111 # v1.0.0 — PLACEHOLDER SHA, re-resolve before use
|
|
145
162
|
with:
|
|
146
|
-
|
|
147
|
-
|
|
163
|
+
# Runtime is read from devEngines.runtime when present. If the project uses
|
|
164
|
+
# .nvmrc/.node-version/volta instead, set runtime to that exact target
|
|
165
|
+
# (for example, runtime: node@22). Do not use engines.node here.
|
|
166
|
+
install: false
|
|
148
167
|
|
|
149
168
|
- name: Install dependencies
|
|
150
169
|
run: pnpm install --frozen-lockfile --ignore-scripts
|
|
@@ -178,14 +197,13 @@ jobs:
|
|
|
178
197
|
with:
|
|
179
198
|
persist-credentials: false
|
|
180
199
|
|
|
181
|
-
- name: Setup Node.js
|
|
182
|
-
uses:
|
|
200
|
+
- name: Setup pnpm and Node.js
|
|
201
|
+
uses: pnpm/setup@1111111111111111111111111111111111111111 # v1.0.0 — PLACEHOLDER SHA, re-resolve before use
|
|
183
202
|
with:
|
|
184
203
|
# Pinned for the publish step only. 24.8.0 bundles npm 11.6.0, new enough
|
|
185
204
|
# for OIDC; this is independent of engines.node, the consumer floor.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
registry-url: https://registry.npmjs.org
|
|
205
|
+
runtime: node@24.8.0
|
|
206
|
+
install: false
|
|
189
207
|
|
|
190
208
|
- name: Ensure npm is new enough for trusted publishing
|
|
191
209
|
# No-op on Node >= 24.8.0; the guard only matters if Node is pinned lower.
|
|
@@ -214,16 +232,16 @@ jobs:
|
|
|
214
232
|
exit 1
|
|
215
233
|
fi
|
|
216
234
|
|
|
217
|
-
npm publish "$(realpath "$tarball")" --ignore-scripts --access public
|
|
235
|
+
npm publish "$(realpath "$tarball")" --ignore-scripts --access public --registry https://registry.npmjs.org
|
|
218
236
|
```
|
|
219
237
|
|
|
220
238
|
## Pinning actions to current SHAs
|
|
221
239
|
|
|
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.
|
|
240
|
+
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. Tag-based refs such as `@v4`, `@v6`, and `@v7` are acceptable only as temporary input to a pinning tool; they must not survive in committed workflow YAML.
|
|
223
241
|
|
|
224
242
|
There are two reliable ways to produce current pins.
|
|
225
243
|
|
|
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.
|
|
244
|
+
The preferred approach is to let tooling resolve and pin for you. Write the workflow first using human-readable tags only in the temporary draft consumed by the tool (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
245
|
|
|
228
246
|
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
247
|
|
|
@@ -277,8 +295,9 @@ grep -nE "uses: [^@]+@[^ ]+" .github/workflows/publish.yml \
|
|
|
277
295
|
- `422 Unprocessable Entity` during publish with provenance: the repository in the OIDC token does not match `package.json`. Check `repository.url` first.
|
|
278
296
|
- 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
297
|
- 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
|
|
281
|
-
- `pnpm/
|
|
298
|
+
- `package.json does not exist` from `pnpm/setup`: the job runs setup before checkout, or `package-json-file` points at the wrong path.
|
|
299
|
+
- `pnpm/setup` cannot resolve a pnpm version: the `packageManager` field is missing. Add the correct `packageManager` field or set the action's `version` input explicitly as a fallback.
|
|
300
|
+
- `pnpm/setup` installs the wrong runtime: `devEngines.runtime` is missing or does not match the project's existing CI target. Add or correct `devEngines.runtime`, or set the action's `runtime` input explicitly.
|
|
282
301
|
- Publishing an already-published version will fail even after the workflow is fixed.
|
|
283
302
|
|
|
284
303
|
## External Setup Reminder
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-goal
|
|
3
|
+
description: Inspect a project, ask targeted user questions, and write a root GOAL.md that clearly defines the project's goals, intended audience, success criteria, constraints, and explicit non-goals. Use when asked to create, draft, update, clarify, document, or recover a project's goal, north star, mission, scope, or GOAL.md from an existing repository.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Project Goal
|
|
7
|
+
|
|
8
|
+
Use this skill to create a clear `GOAL.md` in the project root. The file should help future agents and maintainers understand what the project is trying to accomplish and what it deliberately is not trying to be.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Determine the project root. Prefer the git repository root. If there is no git repository, use the current working directory unless the user named a different root.
|
|
13
|
+
2. Inspect the project before asking questions:
|
|
14
|
+
- Read existing project-level docs such as `README.md`, `GOAL.md`, `CONTRIBUTING.md`, docs indexes, package manifests, config files, and examples.
|
|
15
|
+
- Inspect source layout, tests, CLI or app entry points, public APIs, bundled assets, and install or build scripts.
|
|
16
|
+
- Review issue, roadmap, changelog, or planning files when present.
|
|
17
|
+
3. Form a concise working theory of:
|
|
18
|
+
- The project purpose.
|
|
19
|
+
- Primary users or consumers, limited to the audiences supported by project evidence or user confirmation.
|
|
20
|
+
- Core capabilities.
|
|
21
|
+
- Success signals.
|
|
22
|
+
- Constraints or principles.
|
|
23
|
+
- Explicit non-goals.
|
|
24
|
+
- Unknowns or contradictions.
|
|
25
|
+
4. Ask the user only the questions that materially affect the final `GOAL.md`.
|
|
26
|
+
- Use the available Ask User tool when one exists. If no Ask User tool is available, ask directly in chat.
|
|
27
|
+
- Ask one question at a time by default.
|
|
28
|
+
- Set a sensible upper limit before asking follow-ups, usually no more than 3-5 total questions unless the project evidence is genuinely contradictory.
|
|
29
|
+
- Prefer confirmation questions when the answer can be inferred: "I infer X from Y; should GOAL.md state that?"
|
|
30
|
+
- Ask open questions only for real gaps, conflicts, or values that cannot be discovered from the repository.
|
|
31
|
+
- Stop asking once the document can be accurate enough. Do not turn the process into an interview.
|
|
32
|
+
5. Write or update `<project-root>/GOAL.md`.
|
|
33
|
+
- If `GOAL.md` already exists, preserve accurate useful content and revise it in place.
|
|
34
|
+
- If discovered evidence conflicts with user answers, treat the user as authoritative but mention the conflict in the final response.
|
|
35
|
+
- If the user cannot answer a question, write the best-supported goal and mark unresolved uncertainty in the document.
|
|
36
|
+
6. Report the file path written and summarize the most important assumptions or unresolved items.
|
|
37
|
+
|
|
38
|
+
## GOAL.md Content
|
|
39
|
+
|
|
40
|
+
Use Markdown features that make the project intent easy to scan. A good default structure is:
|
|
41
|
+
|
|
42
|
+
```markdown
|
|
43
|
+
# Project Goal
|
|
44
|
+
|
|
45
|
+
## North Star
|
|
46
|
+
|
|
47
|
+
## Who This Is For
|
|
48
|
+
|
|
49
|
+
## Core Goals
|
|
50
|
+
|
|
51
|
+
## Success Looks Like
|
|
52
|
+
|
|
53
|
+
## Non-Goals
|
|
54
|
+
|
|
55
|
+
## Principles and Constraints
|
|
56
|
+
|
|
57
|
+
## Current Focus
|
|
58
|
+
|
|
59
|
+
## Open Questions
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Adapt the headings to the project. Keep the file practical rather than ceremonial.
|
|
63
|
+
|
|
64
|
+
`GOAL.md` must include:
|
|
65
|
+
|
|
66
|
+
- A short north-star statement.
|
|
67
|
+
- The intended audience or users. If there is only one clear audience, name that single group instead of inventing additional end users or use cases.
|
|
68
|
+
- The concrete outcomes the project exists to create.
|
|
69
|
+
- A prioritized or grouped list of core goals.
|
|
70
|
+
- A `Non-Goals` section that clearly states what the project is not, will not optimize for, or should avoid becoming.
|
|
71
|
+
- Success criteria or observable signs of progress.
|
|
72
|
+
- Any important constraints, tradeoffs, principles, or scope boundaries.
|
|
73
|
+
- Open questions only when they genuinely remain unresolved after discovery and user input.
|
|
74
|
+
|
|
75
|
+
## Discovery Guidance
|
|
76
|
+
|
|
77
|
+
Look for evidence in:
|
|
78
|
+
|
|
79
|
+
- Project name, README, package metadata, CLI help text, app copy, examples, and tests.
|
|
80
|
+
- Dependency choices, framework configuration, deployment files, and build scripts.
|
|
81
|
+
- Directory names such as `src/`, `docs/`, `examples/`, `packages/`, `apps/`, `hooks/`, `skills/`, or `templates/`.
|
|
82
|
+
- Existing planning artifacts, comments, release notes, or issue templates.
|
|
83
|
+
|
|
84
|
+
Avoid overfitting to implementation details. The goal should describe why the project exists and what outcomes matter, not merely list files or current implementation tasks.
|
|
85
|
+
|
|
86
|
+
## Writing Standards
|
|
87
|
+
|
|
88
|
+
- Be specific, plainspoken, and falsifiable where possible.
|
|
89
|
+
- Separate facts discovered from the repo from assumptions confirmed by the user.
|
|
90
|
+
- Do not make up audiences, personas, or use cases to make the project seem broader. Specific and narrow is better than blurry.
|
|
91
|
+
- Prefer durable project intent over short-lived task lists.
|
|
92
|
+
- Make non-goals explicit enough to guide prioritization and issue triage.
|
|
93
|
+
- Keep the document concise enough to be read before working on the project.
|
|
94
|
+
- Do not include private chain-of-thought, exhaustive discovery notes, or a transcript of user questions.
|