@schalkneethling/toolkit 0.5.0 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/index.mjs.map +1 -1
  2. package/hooks/auto-approve-safe-commands/hook.mjs +5 -1
  3. package/hooks/auto-approve-safe-commands/hook.mts +7 -6
  4. package/hooks/block-dangerous-commands/hook.mjs +3 -3
  5. package/hooks/block-dangerous-commands/hook.mts +10 -22
  6. package/package.json +9 -9
  7. package/skills/css-tokens/SKILL.md +1 -1
  8. package/skills/css-tokens/references/tokens.css +6 -10
  9. package/skills/frontend-security/SKILL.md +3 -0
  10. package/skills/frontend-security/references/csp-configuration.md +68 -51
  11. package/skills/frontend-security/references/csrf-protection.md +74 -70
  12. package/skills/frontend-security/references/dom-security.md +36 -29
  13. package/skills/frontend-security/references/file-upload-security.md +101 -69
  14. package/skills/frontend-security/references/framework-patterns.md +42 -40
  15. package/skills/frontend-security/references/input-validation.md +36 -31
  16. package/skills/frontend-security/references/jwt-security.md +68 -84
  17. package/skills/frontend-security/references/nodejs-npm-security.md +63 -55
  18. package/skills/frontend-security/references/xss-prevention.md +38 -36
  19. package/skills/frontend-testing/SKILL.md +31 -38
  20. package/skills/frontend-testing/references/accessibility-testing.md +56 -62
  21. package/skills/frontend-testing/references/aria-snapshots.md +35 -34
  22. package/skills/frontend-testing/references/locator-strategies.md +37 -40
  23. package/skills/frontend-testing/references/visual-regression.md +29 -23
  24. package/skills/npm-publishing-best-practices/SKILL.md +316 -0
  25. package/skills/semantic-html/SKILL.md +5 -21
  26. package/skills/semantic-html/references/heading-patterns.md +1 -5
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * toolkit — personal CLI for managing Claude Code hooks, skills, and commands.\n *\n * Commands:\n * toolkit add hook <name>\n * toolkit add skill <name> [--link <target>...]\n * toolkit add command <name>\n * toolkit add collections <name>\n * toolkit update [--force]\n * toolkit list hook\n * toolkit list skill\n * toolkit list command\n * toolkit list collections\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createInterface } from \"node:readline/promises\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nconst TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst HOOKS_SRC = join(TOOLKIT_ROOT, \"hooks\");\nconst SKILLS_SRC = join(TOOLKIT_ROOT, \"skills\");\nconst COMMANDS_SRC = join(TOOLKIT_ROOT, \"commands\");\nconst CONFIG_PATH = join(TOOLKIT_ROOT, \"config.json\");\n\nconst PROJECT_ROOT = process.cwd();\nconst CLAUDE_DIR = join(PROJECT_ROOT, \".claude\");\nconst TOOLKIT_DIR = join(PROJECT_ROOT, \".claude-toolkit\");\nconst MANIFEST_PATH = join(CLAUDE_DIR, \"toolkit-manifest.json\");\n\ntype HookEntry = { hash: string; installedAt: string };\ntype SkillEntry = { hash: string; installedAt: string; linkedTo: string[] };\ntype CommandEntry = { hash: string; installedAt: string };\ntype Manifest = {\n commands: Record<string, CommandEntry>;\n hooks: Record<string, HookEntry>;\n skills: Record<string, SkillEntry>;\n};\ntype CollectionItemKind = \"command\" | \"hook\" | \"skill\";\ntype CollectionItemConfig = {\n type: CollectionItemKind | `${CollectionItemKind}s`;\n src: string;\n};\ntype CollectionConfig = {\n name: string;\n items: CollectionItemConfig[];\n};\ntype ResolvedCollectionItem = {\n collection: string;\n sourcePath: string;\n sourceName: string;\n type: CollectionItemKind;\n};\n\n// ---------- helpers ----------\n\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction shortHash(content: string | Buffer): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 7);\n}\n\nfunction readManifest(): Manifest {\n if (!existsSync(MANIFEST_PATH)) {\n return { commands: {}, hooks: {}, skills: {} };\n }\n\n try {\n const parsed = JSON.parse(\n readFileSync(MANIFEST_PATH, \"utf8\"),\n ) as Partial<Manifest>;\n return {\n commands: parsed.commands ?? {},\n hooks: parsed.hooks ?? {},\n skills: parsed.skills ?? {},\n };\n } catch {\n return { commands: {}, hooks: {}, skills: {} };\n }\n}\n\nfunction writeManifest(m: Manifest): void {\n mkdirSync(CLAUDE_DIR, { recursive: true });\n writeFileSync(MANIFEST_PATH, JSON.stringify(m, null, 2) + \"\\n\");\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction deepMerge<T>(target: T, source: T): T {\n if (Array.isArray(target) && Array.isArray(source)) {\n return [...target, ...source] as T;\n }\n if (isPlainObject(target) && isPlainObject(source)) {\n const out: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(source)) {\n out[k] = k in out ? deepMerge(out[k], v) : v;\n }\n return out as T;\n }\n return source;\n}\n\nfunction hashCommandSource(name: string): string {\n const p = join(COMMANDS_SRC, `${name}.md`);\n return shortHash(readFileSync(p));\n}\n\nfunction hashHookSource(name: string): string {\n const p = join(HOOKS_SRC, name, \"hook.mjs\");\n return shortHash(readFileSync(p));\n}\n\nfunction hashSkillSource(name: string): string {\n const dir = join(SKILLS_SRC, name);\n const files = collectFiles(dir).sort();\n const h = createHash(\"sha256\");\n for (const f of files) {\n h.update(relative(dir, f));\n h.update(\"\\0\");\n h.update(readFileSync(f));\n h.update(\"\\0\");\n }\n return h.digest(\"hex\").slice(0, 7);\n}\n\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n if (!existsSync(dir)) {\n return out;\n }\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === \".gitkeep\") {\n continue;\n }\n\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.isFile()) {\n out.push(full);\n }\n }\n return out;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();\n rl.close();\n return answer === \"y\" || answer === \"yes\";\n}\n\nfunction diffLines(oldStr: string, newStr: string): string {\n const a = oldStr.split(\"\\n\");\n const b = newStr.split(\"\\n\");\n const out: string[] = [];\n const max = Math.max(a.length, b.length);\n for (let i = 0; i < max; i++) {\n if (a[i] === b[i]) {\n continue;\n }\n\n if (a[i] !== undefined) {\n out.push(`- ${a[i]}`);\n }\n\n if (b[i] !== undefined) {\n out.push(`+ ${b[i]}`);\n }\n }\n return out.join(\"\\n\");\n}\n\n// ---------- commands ----------\n\nfunction sanitizeName(name: string, kind: string): string {\n name = basename(name);\n if (!name) {\n console.error(`Invalid ${kind} name`);\n process.exit(1);\n }\n return name;\n}\n\nfunction normalizeCollectionItemType(\n type: CollectionItemConfig[\"type\"],\n collectionName: string,\n): CollectionItemKind {\n if (type === \"command\" || type === \"commands\") {\n return \"command\";\n }\n if (type === \"hook\" || type === \"hooks\") {\n return \"hook\";\n }\n if (type === \"skill\" || type === \"skills\") {\n return \"skill\";\n }\n\n throw new Error(\n `Collection \"${collectionName}\" has unsupported item type \"${type}\"`,\n );\n}\n\nfunction resolveSourcePath(src: string, kind: string, collectionName: string): string {\n const sourcePath = resolve(TOOLKIT_ROOT, src);\n if (!sourcePath.startsWith(TOOLKIT_ROOT + sep)) {\n throw new Error(\n `Collection \"${collectionName}\" ${kind} source must stay within the toolkit root: ${src}`,\n );\n }\n return sourcePath;\n}\n\nfunction inferItemNameFromSource(\n type: CollectionItemKind,\n sourcePath: string,\n collectionName: string,\n): string {\n if (type === \"command\") {\n if (\n dirname(sourcePath) !== COMMANDS_SRC ||\n !sourcePath.startsWith(COMMANDS_SRC + sep) ||\n !sourcePath.endsWith(\".md\")\n ) {\n throw new Error(\n `Collection \"${collectionName}\" command source must point to a markdown file directly under commands/: ${relative(TOOLKIT_ROOT, sourcePath)}`,\n );\n }\n return basename(sourcePath, \".md\");\n }\n\n const expectedRoot = type === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (dirname(sourcePath) !== expectedRoot || !sourcePath.startsWith(expectedRoot + sep)) {\n throw new Error(\n `Collection \"${collectionName}\" ${type} source must point to a top-level entry under ${relative(TOOLKIT_ROOT, expectedRoot)}/: ${relative(TOOLKIT_ROOT, sourcePath)}`,\n );\n }\n\n return basename(sourcePath);\n}\n\nfunction readCollectionsConfig(): CollectionConfig[] {\n if (!existsSync(CONFIG_PATH)) {\n throw new Error(\n `Collections config not found: ${relative(TOOLKIT_ROOT, CONFIG_PATH)}`,\n );\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(\n `Collection \"${name}\" item at index ${itemIndex} must be an object`,\n );\n }\n if (typeof item.type !== \"string\" || item.type.trim().length === 0) {\n throw new Error(\n `Collection \"${name}\" item at index ${itemIndex} must have a non-empty type`,\n );\n }\n if (typeof item.src !== \"string\" || item.src.trim().length === 0) {\n throw new Error(\n `Collection \"${name}\" item at index ${itemIndex} must have a non-empty src`,\n );\n }\n\n return {\n type: item.type as CollectionItemConfig[\"type\"],\n src: item.src,\n };\n });\n\n return {\n name,\n items: validatedItems,\n };\n });\n}\n\nfunction resolveCollection(name: string): ResolvedCollectionItem[] {\n const collectionName = sanitizeName(name, \"collection\");\n const collections = readCollectionsConfig();\n const collection = collections.find((entry) => entry.name === collectionName);\n\n if (!collection) {\n throw new Error(`Collection not found: ${collectionName}`);\n }\n\n const deduped = new Map<string, ResolvedCollectionItem>();\n\n for (const item of collection.items) {\n const type = normalizeCollectionItemType(item.type, collection.name);\n const sourcePath = resolveSourcePath(item.src, type, collection.name);\n const sourceName = inferItemNameFromSource(type, sourcePath, collection.name);\n const key = `${type}:${sourceName}`;\n\n if (!deduped.has(key)) {\n deduped.set(key, {\n collection: collection.name,\n sourcePath,\n sourceName,\n type,\n });\n }\n }\n\n return [...deduped.values()];\n}\n\nfunction installCommand(name: string, src: string): void {\n if (!existsSync(src)) {\n console.error(`Command not found: ${name}`);\n process.exit(1);\n }\n\n const commandsDir = join(CLAUDE_DIR, \"commands\");\n mkdirSync(commandsDir, { recursive: true });\n const dest = resolve(commandsDir, `${name}.md`);\n if (!dest.startsWith(commandsDir + sep)) {\n console.error(\"Invalid command name\");\n process.exit(1);\n }\n writeFileSync(dest, readFileSync(src));\n\n const manifest = readManifest();\n manifest.commands[name] = {\n hash: hashCommandSource(name),\n installedAt: today(),\n };\n writeManifest(manifest);\n\n console.log(`Installed command: ${name} → ${relative(PROJECT_ROOT, dest)}`);\n}\n\nfunction addCommand(name: string): void {\n name = sanitizeName(name, \"command\");\n installCommand(name, join(COMMANDS_SRC, `${name}.md`));\n}\n\nfunction installHook(name: string, srcDir: string): void {\n if (!existsSync(srcDir)) {\n console.error(`Hook not found: ${name}`);\n process.exit(1);\n }\n\n const hookSrc = join(srcDir, \"hook.mjs\");\n const fragmentPath = join(srcDir, \"settings-fragment.json\");\n\n const hooksDir = join(CLAUDE_DIR, \"hooks\");\n mkdirSync(hooksDir, { recursive: true });\n const destHook = resolve(hooksDir, `${name}.mjs`);\n if (!destHook.startsWith(hooksDir + sep)) {\n console.error(\"Invalid hook name\");\n process.exit(1);\n }\n writeFileSync(destHook, readFileSync(hookSrc));\n\n if (existsSync(fragmentPath)) {\n const fragment = JSON.parse(readFileSync(fragmentPath, \"utf8\"));\n const settingsPath = join(CLAUDE_DIR, \"settings.json\");\n const current = existsSync(settingsPath)\n ? JSON.parse(readFileSync(settingsPath, \"utf8\"))\n : {};\n const merged = deepMerge(current, fragment);\n writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + \"\\n\");\n }\n\n const manifest = readManifest();\n manifest.hooks[name] = { hash: hashHookSource(name), installedAt: today() };\n writeManifest(manifest);\n\n console.log(`Installed hook: ${name} → ${relative(PROJECT_ROOT, destHook)}`);\n}\n\nfunction addHook(name: string): void {\n name = sanitizeName(name, \"hook\");\n installHook(name, join(HOOKS_SRC, name));\n}\n\nfunction installSkill(name: string, srcDir: string, links: string[]): void {\n if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {\n console.error(`Skill not found: ${name}`);\n process.exit(1);\n }\n\n const destDir = resolve(TOOLKIT_DIR, \"skills\", name);\n if (!destDir.startsWith(join(TOOLKIT_DIR, \"skills\") + sep)) {\n console.error(\"Invalid skill name\");\n process.exit(1);\n }\n mkdirSync(dirname(destDir), { recursive: true });\n cpSync(srcDir, destDir, { recursive: true });\n\n const resolvedLinks = links.length > 0 ? links : [join(\".claude\", \"skills\")];\n for (const link of resolvedLinks) {\n const linkDir = resolve(PROJECT_ROOT, link);\n mkdirSync(linkDir, { recursive: true });\n\n const linkPath = join(linkDir, name);\n if (existsSync(linkPath) || lstatExists(linkPath)) {\n unlinkSync(linkPath);\n }\n\n const relTarget = relative(linkDir, destDir);\n symlinkSync(relTarget, linkPath, \"dir\");\n }\n\n const manifest = readManifest();\n manifest.skills[name] = {\n hash: hashSkillSource(name),\n installedAt: today(),\n linkedTo: resolvedLinks,\n };\n writeManifest(manifest);\n\n console.log(`Installed skill: ${name} → ${relative(PROJECT_ROOT, destDir)}`);\n for (const l of resolvedLinks) {\n console.log(` linked: ${join(l, name)}`);\n }\n}\n\nfunction addSkill(name: string, links: string[]): void {\n name = sanitizeName(name, \"skill\");\n installSkill(name, join(SKILLS_SRC, name), links);\n}\n\nfunction addCollection(name: string): void {\n const items = resolveCollection(name);\n for (const item of items) {\n if (!existsSync(item.sourcePath)) {\n throw new Error(\n `Collection \"${item.collection}\" references missing ${item.type} source: ${relative(TOOLKIT_ROOT, item.sourcePath)}`,\n );\n }\n\n const itemStats = statSync(item.sourcePath);\n const actualKind = itemStats.isFile()\n ? \"file\"\n : itemStats.isDirectory()\n ? \"directory\"\n : \"other\";\n\n if (item.type === \"command\") {\n if (!itemStats.isFile()) {\n throw new Error(\n `Collection \"${item.collection}\" expected command source \"${item.sourcePath}\" to be a file, found ${actualKind}`,\n );\n }\n installCommand(item.sourceName, item.sourcePath);\n continue;\n }\n\n if (item.type === \"hook\") {\n if (!itemStats.isDirectory()) {\n throw new Error(\n `Collection \"${item.collection}\" expected hook source \"${item.sourcePath}\" to be a directory, found ${actualKind}`,\n );\n }\n installHook(item.sourceName, item.sourcePath);\n continue;\n }\n\n if (!itemStats.isDirectory()) {\n throw new Error(\n `Collection \"${item.collection}\" expected skill source \"${item.sourcePath}\" to be a directory, found ${actualKind}`,\n );\n }\n installSkill(item.sourceName, item.sourcePath, []);\n }\n}\n\nfunction lstatExists(p: string): boolean {\n try {\n lstatSync(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function update(force: boolean): Promise<void> {\n const manifest = readManifest();\n let changed = false;\n\n for (const [name, entry] of Object.entries(manifest.hooks)) {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashHookSource(name);\n const installedPath = join(CLAUDE_DIR, \"hooks\", `${name}.mjs`);\n const installedHash = existsSync(installedPath)\n ? shortHash(readFileSync(installedPath))\n : null;\n\n const sourceChanged = sourceHash !== entry.hash;\n const locallyModified =\n 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)\n ? readFileSync(installedPath, \"utf8\")\n : \"\";\n const newSrc = readFileSync(join(srcDir, \"hook.mjs\"), \"utf8\");\n console.log(`\\n~ hook: ${name} (${entry.hash} → ${sourceHash})`);\n console.log(diffLines(oldSrc, newSrc));\n const ok = force || (await confirm(`Update hook \"${name}\"?`));\n\n if (!ok) {\n continue;\n }\n\n writeFileSync(installedPath, newSrc);\n manifest.hooks[name] = { hash: sourceHash, installedAt: today() };\n }\n }\n\n for (const [name, entry] of Object.entries(manifest.skills)) {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashSkillSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ skill: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update skill \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n cpSync(srcDir, destDir, { recursive: true, force: true });\n manifest.skills[name] = {\n hash: sourceHash,\n installedAt: today(),\n linkedTo: entry.linkedTo,\n };\n }\n\n for (const [name, entry] of Object.entries(manifest.commands)) {\n const src = join(COMMANDS_SRC, `${name}.md`);\n if (!existsSync(src)) {\n continue;\n }\n\n const sourceHash = hashCommandSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ command: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update command \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const dest = join(CLAUDE_DIR, \"commands\", `${name}.md`);\n writeFileSync(dest, readFileSync(src));\n manifest.commands[name] = { hash: sourceHash, installedAt: today() };\n }\n\n if (changed) {\n writeManifest(manifest);\n }\n}\n\nfunction list(kind: \"hook\" | \"skill\" | \"command\"): void {\n if (kind === \"command\") {\n if (!existsSync(COMMANDS_SRC)) {\n console.log(\"(no commands available)\");\n return;\n }\n const files = readdirSync(COMMANDS_SRC)\n .filter((f) => f.endsWith(\".md\"))\n .map((f) => f.replace(/\\.md$/, \"\"));\n if (files.length === 0) {\n console.log(\"(no commands available)\");\n return;\n }\n for (const name of files) {\n console.log(`${name} ${hashCommandSource(name)}`);\n }\n return;\n }\n\n const dir = kind === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (!existsSync(dir)) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n const entries = readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() || (kind === \"skill\" && e.isSymbolicLink()))\n .map((e) => e.name);\n\n if (entries.length === 0) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n\n for (const name of entries) {\n const hash = kind === \"hook\" ? hashHookSource(name) : hashSkillSource(name);\n console.log(`${name} ${hash}`);\n }\n}\n\nfunction listCollections(): void {\n const collections = readCollectionsConfig();\n if (collections.length === 0) {\n console.log(\"(no collections available)\");\n return;\n }\n\n for (const collection of collections) {\n console.log(`${collection.name} ${collection.items.length} item(s)`);\n }\n}\n\n// ---------- argv ----------\n\nfunction usage(): never {\n console.error(\n `Usage:\n toolkit add hook <name>\n toolkit add skill <name> [--link <target>]...\n toolkit add command <name>\n toolkit add collections <name>\n toolkit update [--force]\n toolkit list hook\n toolkit list skill\n toolkit list command\n toolkit list collections`,\n );\n process.exit(1);\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n options: {\n force: {\n default: false,\n type: \"boolean\",\n },\n links: {\n multiple: true,\n type: \"string\",\n },\n },\n allowPositionals: true,\n });\n\n const { force, links } = values;\n const [command, resource, name] = positionals;\n\n if (command === \"add\" && resource === \"hook\") {\n if (!name) {\n usage();\n }\n\n addHook(name);\n return;\n }\n\n if (command === \"add\" && resource === \"skill\") {\n if (!name) {\n usage();\n }\n\n addSkill(name, links ? links : []);\n return;\n }\n\n if (command === \"add\" && resource === \"command\") {\n if (!name) {\n usage();\n }\n\n addCommand(name);\n return;\n }\n\n if (\n command === \"add\" &&\n (resource === \"collection\" || resource === \"collections\")\n ) {\n if (!name) {\n usage();\n }\n\n addCollection(name);\n return;\n }\n\n if (command === \"update\") {\n await update(force);\n return;\n }\n\n if (\n command === \"list\" &&\n (resource === \"hook\" || resource === \"skill\" || resource === \"command\")\n ) {\n list(resource as \"hook\" | \"skill\" | \"command\");\n return;\n }\n\n if (\n command === \"list\" &&\n (resource === \"collection\" || resource === \"collections\")\n ) {\n listCollections();\n return;\n }\n\n usage();\n}\n\nmain().catch((err) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * toolkit — personal CLI for managing Claude Code hooks, skills, and commands.\n *\n * Commands:\n * toolkit add hook <name>\n * toolkit add skill <name> [--link <target>...]\n * toolkit add command <name>\n * toolkit add collections <name>\n * toolkit update [--force]\n * toolkit list hook\n * toolkit list skill\n * toolkit list command\n * toolkit list collections\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createInterface } from \"node:readline/promises\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nconst TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst HOOKS_SRC = join(TOOLKIT_ROOT, \"hooks\");\nconst SKILLS_SRC = join(TOOLKIT_ROOT, \"skills\");\nconst COMMANDS_SRC = join(TOOLKIT_ROOT, \"commands\");\nconst CONFIG_PATH = join(TOOLKIT_ROOT, \"config.json\");\n\nconst PROJECT_ROOT = process.cwd();\nconst CLAUDE_DIR = join(PROJECT_ROOT, \".claude\");\nconst TOOLKIT_DIR = join(PROJECT_ROOT, \".claude-toolkit\");\nconst MANIFEST_PATH = join(CLAUDE_DIR, \"toolkit-manifest.json\");\n\ntype HookEntry = { hash: string; installedAt: string };\ntype SkillEntry = { hash: string; installedAt: string; linkedTo: string[] };\ntype CommandEntry = { hash: string; installedAt: string };\ntype Manifest = {\n commands: Record<string, CommandEntry>;\n hooks: Record<string, HookEntry>;\n skills: Record<string, SkillEntry>;\n};\ntype CollectionItemKind = \"command\" | \"hook\" | \"skill\";\ntype CollectionItemConfig = {\n type: CollectionItemKind | `${CollectionItemKind}s`;\n src: string;\n};\ntype CollectionConfig = {\n name: string;\n items: CollectionItemConfig[];\n};\ntype ResolvedCollectionItem = {\n collection: string;\n sourcePath: string;\n sourceName: string;\n type: CollectionItemKind;\n};\n\n// ---------- helpers ----------\n\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction shortHash(content: string | Buffer): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 7);\n}\n\nfunction readManifest(): Manifest {\n if (!existsSync(MANIFEST_PATH)) {\n return { commands: {}, hooks: {}, skills: {} };\n }\n\n try {\n const parsed = JSON.parse(readFileSync(MANIFEST_PATH, \"utf8\")) as Partial<Manifest>;\n return {\n commands: parsed.commands ?? {},\n hooks: parsed.hooks ?? {},\n skills: parsed.skills ?? {},\n };\n } catch {\n return { commands: {}, hooks: {}, skills: {} };\n }\n}\n\nfunction writeManifest(m: Manifest): void {\n mkdirSync(CLAUDE_DIR, { recursive: true });\n writeFileSync(MANIFEST_PATH, JSON.stringify(m, null, 2) + \"\\n\");\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction deepMerge<T>(target: T, source: T): T {\n if (Array.isArray(target) && Array.isArray(source)) {\n return [...target, ...source] as T;\n }\n if (isPlainObject(target) && isPlainObject(source)) {\n const out: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(source)) {\n out[k] = k in out ? deepMerge(out[k], v) : v;\n }\n return out as T;\n }\n return source;\n}\n\nfunction hashCommandSource(name: string): string {\n const p = join(COMMANDS_SRC, `${name}.md`);\n return shortHash(readFileSync(p));\n}\n\nfunction hashHookSource(name: string): string {\n const p = join(HOOKS_SRC, name, \"hook.mjs\");\n return shortHash(readFileSync(p));\n}\n\nfunction hashSkillSource(name: string): string {\n const dir = join(SKILLS_SRC, name);\n const files = collectFiles(dir).sort();\n const h = createHash(\"sha256\");\n for (const f of files) {\n h.update(relative(dir, f));\n h.update(\"\\0\");\n h.update(readFileSync(f));\n h.update(\"\\0\");\n }\n return h.digest(\"hex\").slice(0, 7);\n}\n\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n if (!existsSync(dir)) {\n return out;\n }\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === \".gitkeep\") {\n continue;\n }\n\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.isFile()) {\n out.push(full);\n }\n }\n return out;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();\n rl.close();\n return answer === \"y\" || answer === \"yes\";\n}\n\nfunction diffLines(oldStr: string, newStr: string): string {\n const a = oldStr.split(\"\\n\");\n const b = newStr.split(\"\\n\");\n const out: string[] = [];\n const max = Math.max(a.length, b.length);\n for (let i = 0; i < max; i++) {\n if (a[i] === b[i]) {\n continue;\n }\n\n if (a[i] !== undefined) {\n out.push(`- ${a[i]}`);\n }\n\n if (b[i] !== undefined) {\n out.push(`+ ${b[i]}`);\n }\n }\n return out.join(\"\\n\");\n}\n\n// ---------- commands ----------\n\nfunction sanitizeName(name: string, kind: string): string {\n name = basename(name);\n if (!name) {\n console.error(`Invalid ${kind} name`);\n process.exit(1);\n }\n return name;\n}\n\nfunction normalizeCollectionItemType(\n type: CollectionItemConfig[\"type\"],\n collectionName: string,\n): CollectionItemKind {\n if (type === \"command\" || type === \"commands\") {\n return \"command\";\n }\n if (type === \"hook\" || type === \"hooks\") {\n return \"hook\";\n }\n if (type === \"skill\" || type === \"skills\") {\n return \"skill\";\n }\n\n throw new Error(`Collection \"${collectionName}\" has unsupported item type \"${type}\"`);\n}\n\nfunction resolveSourcePath(src: string, kind: string, collectionName: string): string {\n const sourcePath = resolve(TOOLKIT_ROOT, src);\n if (!sourcePath.startsWith(TOOLKIT_ROOT + sep)) {\n throw new Error(\n `Collection \"${collectionName}\" ${kind} source must stay within the toolkit root: ${src}`,\n );\n }\n return sourcePath;\n}\n\nfunction inferItemNameFromSource(\n type: CollectionItemKind,\n sourcePath: string,\n collectionName: string,\n): string {\n if (type === \"command\") {\n if (\n dirname(sourcePath) !== COMMANDS_SRC ||\n !sourcePath.startsWith(COMMANDS_SRC + sep) ||\n !sourcePath.endsWith(\".md\")\n ) {\n throw new Error(\n `Collection \"${collectionName}\" command source must point to a markdown file directly under commands/: ${relative(TOOLKIT_ROOT, sourcePath)}`,\n );\n }\n return basename(sourcePath, \".md\");\n }\n\n const expectedRoot = type === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (dirname(sourcePath) !== expectedRoot || !sourcePath.startsWith(expectedRoot + sep)) {\n throw new Error(\n `Collection \"${collectionName}\" ${type} source must point to a top-level entry under ${relative(TOOLKIT_ROOT, expectedRoot)}/: ${relative(TOOLKIT_ROOT, sourcePath)}`,\n );\n }\n\n return basename(sourcePath);\n}\n\nfunction readCollectionsConfig(): CollectionConfig[] {\n if (!existsSync(CONFIG_PATH)) {\n throw new Error(`Collections config not found: ${relative(TOOLKIT_ROOT, CONFIG_PATH)}`);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(CONFIG_PATH, \"utf8\"));\n } catch (error) {\n throw new Error(\n `Invalid collections config in ${relative(TOOLKIT_ROOT, CONFIG_PATH)}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n\n if (!Array.isArray(parsed)) {\n throw new Error(\"Collections config must be an array\");\n }\n\n const names = new Set<string>();\n return parsed.map((entry, index) => {\n if (!isPlainObject(entry)) {\n throw new Error(`Collection at index ${index} must be an object`);\n }\n\n const { name, items } = entry;\n if (typeof name !== \"string\" || name.trim().length === 0) {\n throw new Error(`Collection at index ${index} must have a non-empty name`);\n }\n if (names.has(name)) {\n throw new Error(`Duplicate collection name: ${name}`);\n }\n names.add(name);\n\n if (!Array.isArray(items)) {\n throw new Error(`Collection \"${name}\" must have an items array`);\n }\n\n const validatedItems = items.map((item, itemIndex) => {\n if (!isPlainObject(item)) {\n throw new Error(`Collection \"${name}\" item at index ${itemIndex} must be an object`);\n }\n if (typeof item.type !== \"string\" || item.type.trim().length === 0) {\n throw new Error(\n `Collection \"${name}\" item at index ${itemIndex} must have a non-empty type`,\n );\n }\n if (typeof item.src !== \"string\" || item.src.trim().length === 0) {\n throw new Error(\n `Collection \"${name}\" item at index ${itemIndex} must have a non-empty src`,\n );\n }\n\n return {\n type: item.type as CollectionItemConfig[\"type\"],\n src: item.src,\n };\n });\n\n return {\n name,\n items: validatedItems,\n };\n });\n}\n\nfunction resolveCollection(name: string): ResolvedCollectionItem[] {\n const collectionName = sanitizeName(name, \"collection\");\n const collections = readCollectionsConfig();\n const collection = collections.find((entry) => entry.name === collectionName);\n\n if (!collection) {\n throw new Error(`Collection not found: ${collectionName}`);\n }\n\n const deduped = new Map<string, ResolvedCollectionItem>();\n\n for (const item of collection.items) {\n const type = normalizeCollectionItemType(item.type, collection.name);\n const sourcePath = resolveSourcePath(item.src, type, collection.name);\n const sourceName = inferItemNameFromSource(type, sourcePath, collection.name);\n const key = `${type}:${sourceName}`;\n\n if (!deduped.has(key)) {\n deduped.set(key, {\n collection: collection.name,\n sourcePath,\n sourceName,\n type,\n });\n }\n }\n\n return [...deduped.values()];\n}\n\nfunction installCommand(name: string, src: string): void {\n if (!existsSync(src)) {\n console.error(`Command not found: ${name}`);\n process.exit(1);\n }\n\n const commandsDir = join(CLAUDE_DIR, \"commands\");\n mkdirSync(commandsDir, { recursive: true });\n const dest = resolve(commandsDir, `${name}.md`);\n if (!dest.startsWith(commandsDir + sep)) {\n console.error(\"Invalid command name\");\n process.exit(1);\n }\n writeFileSync(dest, readFileSync(src));\n\n const manifest = readManifest();\n manifest.commands[name] = {\n hash: hashCommandSource(name),\n installedAt: today(),\n };\n writeManifest(manifest);\n\n console.log(`Installed command: ${name} → ${relative(PROJECT_ROOT, dest)}`);\n}\n\nfunction addCommand(name: string): void {\n name = sanitizeName(name, \"command\");\n installCommand(name, join(COMMANDS_SRC, `${name}.md`));\n}\n\nfunction installHook(name: string, srcDir: string): void {\n if (!existsSync(srcDir)) {\n console.error(`Hook not found: ${name}`);\n process.exit(1);\n }\n\n const hookSrc = join(srcDir, \"hook.mjs\");\n const fragmentPath = join(srcDir, \"settings-fragment.json\");\n\n const hooksDir = join(CLAUDE_DIR, \"hooks\");\n mkdirSync(hooksDir, { recursive: true });\n const destHook = resolve(hooksDir, `${name}.mjs`);\n if (!destHook.startsWith(hooksDir + sep)) {\n console.error(\"Invalid hook name\");\n process.exit(1);\n }\n writeFileSync(destHook, readFileSync(hookSrc));\n\n if (existsSync(fragmentPath)) {\n const fragment = JSON.parse(readFileSync(fragmentPath, \"utf8\"));\n const settingsPath = join(CLAUDE_DIR, \"settings.json\");\n const current = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, \"utf8\")) : {};\n const merged = deepMerge(current, fragment);\n writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + \"\\n\");\n }\n\n const manifest = readManifest();\n manifest.hooks[name] = { hash: hashHookSource(name), installedAt: today() };\n writeManifest(manifest);\n\n console.log(`Installed hook: ${name} → ${relative(PROJECT_ROOT, destHook)}`);\n}\n\nfunction addHook(name: string): void {\n name = sanitizeName(name, \"hook\");\n installHook(name, join(HOOKS_SRC, name));\n}\n\nfunction installSkill(name: string, srcDir: string, links: string[]): void {\n if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {\n console.error(`Skill not found: ${name}`);\n process.exit(1);\n }\n\n const destDir = resolve(TOOLKIT_DIR, \"skills\", name);\n if (!destDir.startsWith(join(TOOLKIT_DIR, \"skills\") + sep)) {\n console.error(\"Invalid skill name\");\n process.exit(1);\n }\n mkdirSync(dirname(destDir), { recursive: true });\n cpSync(srcDir, destDir, { recursive: true });\n\n const resolvedLinks = links.length > 0 ? links : [join(\".claude\", \"skills\")];\n for (const link of resolvedLinks) {\n const linkDir = resolve(PROJECT_ROOT, link);\n mkdirSync(linkDir, { recursive: true });\n\n const linkPath = join(linkDir, name);\n if (existsSync(linkPath) || lstatExists(linkPath)) {\n unlinkSync(linkPath);\n }\n\n const relTarget = relative(linkDir, destDir);\n symlinkSync(relTarget, linkPath, \"dir\");\n }\n\n const manifest = readManifest();\n manifest.skills[name] = {\n hash: hashSkillSource(name),\n installedAt: today(),\n linkedTo: resolvedLinks,\n };\n writeManifest(manifest);\n\n console.log(`Installed skill: ${name} → ${relative(PROJECT_ROOT, destDir)}`);\n for (const l of resolvedLinks) {\n console.log(` linked: ${join(l, name)}`);\n }\n}\n\nfunction addSkill(name: string, links: string[]): void {\n name = sanitizeName(name, \"skill\");\n installSkill(name, join(SKILLS_SRC, name), links);\n}\n\nfunction addCollection(name: string): void {\n const items = resolveCollection(name);\n for (const item of items) {\n if (!existsSync(item.sourcePath)) {\n throw new Error(\n `Collection \"${item.collection}\" references missing ${item.type} source: ${relative(TOOLKIT_ROOT, item.sourcePath)}`,\n );\n }\n\n const itemStats = statSync(item.sourcePath);\n const actualKind = itemStats.isFile()\n ? \"file\"\n : itemStats.isDirectory()\n ? \"directory\"\n : \"other\";\n\n if (item.type === \"command\") {\n if (!itemStats.isFile()) {\n throw new Error(\n `Collection \"${item.collection}\" expected command source \"${item.sourcePath}\" to be a file, found ${actualKind}`,\n );\n }\n installCommand(item.sourceName, item.sourcePath);\n continue;\n }\n\n if (item.type === \"hook\") {\n if (!itemStats.isDirectory()) {\n throw new Error(\n `Collection \"${item.collection}\" expected hook source \"${item.sourcePath}\" to be a directory, found ${actualKind}`,\n );\n }\n installHook(item.sourceName, item.sourcePath);\n continue;\n }\n\n if (!itemStats.isDirectory()) {\n throw new Error(\n `Collection \"${item.collection}\" expected skill source \"${item.sourcePath}\" to be a directory, found ${actualKind}`,\n );\n }\n installSkill(item.sourceName, item.sourcePath, []);\n }\n}\n\nfunction lstatExists(p: string): boolean {\n try {\n lstatSync(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function update(force: boolean): Promise<void> {\n const manifest = readManifest();\n let changed = false;\n\n for (const [name, entry] of Object.entries(manifest.hooks)) {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashHookSource(name);\n const installedPath = join(CLAUDE_DIR, \"hooks\", `${name}.mjs`);\n const installedHash = existsSync(installedPath) ? shortHash(readFileSync(installedPath)) : null;\n\n const sourceChanged = sourceHash !== entry.hash;\n const locallyModified = installedHash !== null && installedHash !== entry.hash;\n\n if (!sourceChanged && !locallyModified) {\n continue;\n }\n\n changed = true;\n\n if (locallyModified && !force) {\n console.warn(\n `! hook \"${name}\" was modified locally (installed=${installedHash}, manifest=${entry.hash}). Use --force to overwrite.`,\n );\n continue;\n }\n\n if (sourceChanged) {\n const oldSrc = existsSync(installedPath) ? readFileSync(installedPath, \"utf8\") : \"\";\n const newSrc = readFileSync(join(srcDir, \"hook.mjs\"), \"utf8\");\n console.log(`\\n~ hook: ${name} (${entry.hash} → ${sourceHash})`);\n console.log(diffLines(oldSrc, newSrc));\n const ok = force || (await confirm(`Update hook \"${name}\"?`));\n\n if (!ok) {\n continue;\n }\n\n writeFileSync(installedPath, newSrc);\n manifest.hooks[name] = { hash: sourceHash, installedAt: today() };\n }\n }\n\n for (const [name, entry] of Object.entries(manifest.skills)) {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashSkillSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ skill: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update skill \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n cpSync(srcDir, destDir, { recursive: true, force: true });\n manifest.skills[name] = {\n hash: sourceHash,\n installedAt: today(),\n linkedTo: entry.linkedTo,\n };\n }\n\n for (const [name, entry] of Object.entries(manifest.commands)) {\n const src = join(COMMANDS_SRC, `${name}.md`);\n if (!existsSync(src)) {\n continue;\n }\n\n const sourceHash = hashCommandSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ command: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update command \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const dest = join(CLAUDE_DIR, \"commands\", `${name}.md`);\n writeFileSync(dest, readFileSync(src));\n manifest.commands[name] = { hash: sourceHash, installedAt: today() };\n }\n\n if (changed) {\n writeManifest(manifest);\n }\n}\n\nfunction list(kind: \"hook\" | \"skill\" | \"command\"): void {\n if (kind === \"command\") {\n if (!existsSync(COMMANDS_SRC)) {\n console.log(\"(no commands available)\");\n return;\n }\n const files = readdirSync(COMMANDS_SRC)\n .filter((f) => f.endsWith(\".md\"))\n .map((f) => f.replace(/\\.md$/, \"\"));\n if (files.length === 0) {\n console.log(\"(no commands available)\");\n return;\n }\n for (const name of files) {\n console.log(`${name} ${hashCommandSource(name)}`);\n }\n return;\n }\n\n const dir = kind === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (!existsSync(dir)) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n const entries = readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() || (kind === \"skill\" && e.isSymbolicLink()))\n .map((e) => e.name);\n\n if (entries.length === 0) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n\n for (const name of entries) {\n const hash = kind === \"hook\" ? hashHookSource(name) : hashSkillSource(name);\n console.log(`${name} ${hash}`);\n }\n}\n\nfunction listCollections(): void {\n const collections = readCollectionsConfig();\n if (collections.length === 0) {\n console.log(\"(no collections available)\");\n return;\n }\n\n for (const collection of collections) {\n console.log(`${collection.name} ${collection.items.length} item(s)`);\n }\n}\n\n// ---------- argv ----------\n\nfunction usage(): never {\n console.error(\n `Usage:\n toolkit add hook <name>\n toolkit add skill <name> [--link <target>]...\n toolkit add command <name>\n toolkit add collections <name>\n toolkit update [--force]\n toolkit list hook\n toolkit list skill\n toolkit list command\n toolkit list collections`,\n );\n process.exit(1);\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n options: {\n force: {\n default: false,\n type: \"boolean\",\n },\n links: {\n multiple: true,\n type: \"string\",\n },\n },\n allowPositionals: true,\n });\n\n const { force, links } = values;\n const [command, resource, name] = positionals;\n\n if (command === \"add\" && resource === \"hook\") {\n if (!name) {\n usage();\n }\n\n addHook(name);\n return;\n }\n\n if (command === \"add\" && resource === \"skill\") {\n if (!name) {\n usage();\n }\n\n addSkill(name, links ? links : []);\n return;\n }\n\n if (command === \"add\" && resource === \"command\") {\n if (!name) {\n usage();\n }\n\n addCommand(name);\n return;\n }\n\n if (command === \"add\" && (resource === \"collection\" || resource === \"collections\")) {\n if (!name) {\n usage();\n }\n\n addCollection(name);\n return;\n }\n\n if (command === \"update\") {\n await update(force);\n return;\n }\n\n if (\n command === \"list\" &&\n (resource === \"hook\" || resource === \"skill\" || resource === \"command\")\n ) {\n list(resource as \"hook\" | \"skill\" | \"command\");\n return;\n }\n\n if (command === \"list\" && (resource === \"collection\" || resource === \"collections\")) {\n listCollections();\n return;\n }\n\n usage();\n}\n\nmain().catch((err) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
@@ -106,7 +106,7 @@ function main() {
106
106
  input = JSON.parse(raw);
107
107
  }
108
108
  catch {
109
- process.stderr.write(`[auto-approve-safe-commands] Failed to parse stdin JSON: ${raw}\n`);
109
+ process.stderr.write(`[auto-approve-safe-commands] Failed to parse stdin JSON\n`);
110
110
  defer();
111
111
  return;
112
112
  }
@@ -115,6 +115,10 @@ function main() {
115
115
  defer();
116
116
  return;
117
117
  }
118
+ if (typeof input.tool_input !== "object" || input.tool_input === null) {
119
+ defer();
120
+ return;
121
+ }
118
122
  const { command } = input.tool_input;
119
123
  if (typeof command !== "string") {
120
124
  defer();
@@ -149,9 +149,7 @@ function main(): void {
149
149
  try {
150
150
  input = JSON.parse(raw) as PermissionRequestInput;
151
151
  } catch {
152
- process.stderr.write(
153
- `[auto-approve-safe-commands] Failed to parse stdin JSON: ${raw}\n`,
154
- );
152
+ process.stderr.write(`[auto-approve-safe-commands] Failed to parse stdin JSON\n`);
155
153
  defer();
156
154
  return;
157
155
  }
@@ -162,6 +160,11 @@ function main(): void {
162
160
  return;
163
161
  }
164
162
 
163
+ if (typeof input.tool_input !== "object" || input.tool_input === null) {
164
+ defer();
165
+ return;
166
+ }
167
+
165
168
  const { command } = input.tool_input as BashToolInput;
166
169
 
167
170
  if (typeof command !== "string") {
@@ -173,9 +176,7 @@ function main(): void {
173
176
 
174
177
  for (const { pattern, label } of SAFE_PATTERNS) {
175
178
  if (pattern.test(trimmed)) {
176
- process.stderr.write(
177
- `[auto-approve-safe-commands] Auto-approved: ${label}\n`,
178
- );
179
+ process.stderr.write(`[auto-approve-safe-commands] Auto-approved: ${label}\n`);
179
180
  approve();
180
181
  return;
181
182
  }
@@ -31,7 +31,8 @@ const rules = [
31
31
  },
32
32
  {
33
33
  id: "chmod-777",
34
- test: (c) => /\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(c) && /-R|--recursive|777/.test(c),
34
+ test: (c) => /\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(c) &&
35
+ /-R|--recursive|777/.test(c),
35
36
  message: "`chmod 777` or recursive world-writable chmod is blocked. Grant the minimum permissions required.",
36
37
  },
37
38
  {
@@ -46,8 +47,7 @@ const rules = [
46
47
  },
47
48
  {
48
49
  id: "fork-bomb",
49
- test: (c) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) ||
50
- /\.\s*\|\s*\.\s*&/.test(c),
50
+ test: (c) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) || /\.\s*\|\s*\.\s*&/.test(c),
51
51
  message: "Fork bomb pattern detected and blocked.",
52
52
  },
53
53
  {
@@ -19,15 +19,13 @@ type Rule = {
19
19
  const rules: Rule[] = [
20
20
  {
21
21
  id: "rm-rf",
22
- test: (c) =>
23
- /\brm\s+(?:-[a-zA-Z]*[rRf][a-zA-Z]*|--recursive|--force)(?:\s|$)/.test(c),
22
+ test: (c) => /\brm\s+(?:-[a-zA-Z]*[rRf][a-zA-Z]*|--recursive|--force)(?:\s|$)/.test(c),
24
23
  message:
25
24
  "`rm -rf` (and flag variants) is blocked. Delete specific paths with a non-recursive `rm`, or move them to a backup location.",
26
25
  },
27
26
  {
28
27
  id: "git-push-force",
29
- test: (c) =>
30
- /\bgit\s+push\b.*\s(?:--force\b|--force-with-lease\b|-f\b)/.test(c),
28
+ test: (c) => /\bgit\s+push\b.*\s(?:--force\b|--force-with-lease\b|-f\b)/.test(c),
31
29
  message:
32
30
  "`git push --force` is blocked. Use `--force-with-lease` only after coordinating with collaborators, or create a new branch.",
33
31
  },
@@ -49,9 +47,8 @@ const rules: Rule[] = [
49
47
  {
50
48
  id: "chmod-777",
51
49
  test: (c) =>
52
- /\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(
53
- c,
54
- ) && /-R|--recursive|777/.test(c),
50
+ /\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(c) &&
51
+ /-R|--recursive|777/.test(c),
55
52
  message:
56
53
  "`chmod 777` or recursive world-writable chmod is blocked. Grant the minimum permissions required.",
57
54
  },
@@ -62,24 +59,19 @@ const rules: Rule[] = [
62
59
  },
63
60
  {
64
61
  id: "system-redirect",
65
- test: (c) =>
66
- /(?:>|>>|tee(?:\s+-[a-zA-Z]*)?)\s+\/(?:etc|boot|usr|bin|sbin)\//.test(c),
62
+ test: (c) => /(?:>|>>|tee(?:\s+-[a-zA-Z]*)?)\s+\/(?:etc|boot|usr|bin|sbin)\//.test(c),
67
63
  message:
68
64
  "Writing into /etc, /boot, /usr, /bin, or /sbin is blocked. These are system directories; use a user-writable path.",
69
65
  },
70
66
  {
71
67
  id: "fork-bomb",
72
- test: (c) =>
73
- /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) ||
74
- /\.\s*\|\s*\.\s*&/.test(c),
68
+ test: (c) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) || /\.\s*\|\s*\.\s*&/.test(c),
75
69
  message: "Fork bomb pattern detected and blocked.",
76
70
  },
77
71
  {
78
72
  id: "curl-pipe-shell",
79
73
  test: (c) =>
80
- /\b(?:curl|wget|fetch)\b[^|;]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh|fish|ksh|dash)\b/.test(
81
- c,
82
- ),
74
+ /\b(?:curl|wget|fetch)\b[^|;]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh|fish|ksh|dash)\b/.test(c),
83
75
  message:
84
76
  "Piping remote content directly into a shell is blocked. Download the script, inspect it, then run it.",
85
77
  },
@@ -92,11 +84,8 @@ const rules: Rule[] = [
92
84
  {
93
85
  id: "kill-9",
94
86
  test: (c) =>
95
- /\bkill\s+(?:-[a-zA-Z]*\s+)*-9\b|\bkill\s+-s\s+(?:9|SIGKILL)\b|\bkill\s+-SIGKILL\b/.test(
96
- c,
97
- ),
98
- message:
99
- "`kill -9` is blocked — it prevents cleanup. Try SIGTERM (default) first.",
87
+ /\bkill\s+(?:-[a-zA-Z]*\s+)*-9\b|\bkill\s+-s\s+(?:9|SIGKILL)\b|\bkill\s+-SIGKILL\b/.test(c),
88
+ message: "`kill -9` is blocked — it prevents cleanup. Try SIGTERM (default) first.",
100
89
  },
101
90
  {
102
91
  id: "npm-publish",
@@ -107,8 +96,7 @@ const rules: Rule[] = [
107
96
  {
108
97
  id: "history-clear",
109
98
  test: (c) => /\bhistory\s+-c\b/.test(c),
110
- message:
111
- "`history -c` is blocked — erasing shell history hides what happened.",
99
+ message: "`history -c` is blocked — erasing shell history hides what happened.",
112
100
  },
113
101
  ];
114
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schalkneethling/toolkit",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
4
4
  "description": "CLI for managing Claude Code hooks and skills across projects.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -20,16 +20,16 @@
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
23
- "dependencies": {
24
- "vite-plus": "^0.1.18"
23
+ "scripts": {
24
+ "toolkit": "tsx src/index.ts",
25
+ "prepare": "vp pack && vp run build:hooks",
26
+ "build:hooks": "tsc --project tsconfig.hooks.json"
25
27
  },
26
28
  "devDependencies": {
27
29
  "@types/node": "^25.6.0",
28
30
  "tsx": "^4.21.0",
29
- "typescript": "^6.0.2"
31
+ "typescript": "^6.0.2",
32
+ "vite-plus": "^0.1.18"
30
33
  },
31
- "scripts": {
32
- "toolkit": "tsx src/index.ts",
33
- "build:hooks": "tsc --project tsconfig.hooks.json"
34
- }
35
- }
34
+ "packageManager": "pnpm@11.1.1"
35
+ }
@@ -94,7 +94,7 @@ To use these tokens in your project, import this file in your main CSS file or H
94
94
  **In CSS:**
95
95
 
96
96
  ```css
97
- @import url('path/to/tokens.css');
97
+ @import url("path/to/tokens.css");
98
98
  ```
99
99
 
100
100
  **In HTML:**
@@ -3,11 +3,11 @@
3
3
  * Typography - Font Families
4
4
  * ======================================== */
5
5
  --font-family-base:
6
- inter, "Source Sans 3", "IBM Plex Sans", -apple-system, blinkmacsystemfont,
7
- "Segoe UI", roboto, helvetica, arial, sans-serif;
6
+ inter, "Source Sans 3", "IBM Plex Sans", -apple-system, blinkmacsystemfont, "Segoe UI", roboto,
7
+ helvetica, arial, sans-serif;
8
8
  --font-family-mono:
9
- ui-monospace, "SF Mono", "Cascadia Code", "Segoe UI Mono",
10
- "Liberation Mono", menlo, monaco, consolas, monospace;
9
+ ui-monospace, "SF Mono", "Cascadia Code", "Segoe UI Mono", "Liberation Mono", menlo, monaco,
10
+ consolas, monospace;
11
11
 
12
12
  /* ========================================
13
13
  * Typography - Font Weights
@@ -85,9 +85,7 @@
85
85
  /* Text */
86
86
  --color-text: oklch(25% 0.02 250deg); /* Dark slate */
87
87
  --color-text-muted: oklch(50% 0.015 250deg); /* Medium gray */
88
- --color-text-inverted: oklch(
89
- 98% 0.005 250deg
90
- ); /* Light for dark backgrounds */
88
+ --color-text-inverted: oklch(98% 0.005 250deg); /* Light for dark backgrounds */
91
89
 
92
90
  /* Logo Colors - derived from brand */
93
91
  --color-logo-maker: oklch(40% 0.025 220deg); /* Dark slate-blue */
@@ -129,9 +127,7 @@
129
127
  /* Text */
130
128
  --color-text: oklch(92% 0.01 250deg); /* Light gray */
131
129
  --color-text-muted: oklch(65% 0.01 250deg); /* Muted gray */
132
- --color-text-inverted: oklch(
133
- 18% 0.015 250deg
134
- ); /* Dark for light backgrounds */
130
+ --color-text-inverted: oklch(18% 0.015 250deg); /* Dark for light backgrounds */
135
131
 
136
132
  /* Logo Colors - brighter for dark mode */
137
133
  --color-logo-maker: oklch(75% 0.02 220deg); /* Lighter slate */
@@ -95,6 +95,7 @@ Load these references based on findings:
95
95
  ## Security Audit Report
96
96
 
97
97
  ### Summary
98
+
98
99
  - Critical: X findings
99
100
  - High: X findings
100
101
  - Medium: X findings
@@ -103,6 +104,7 @@ Load these references based on findings:
103
104
  ### Critical Findings
104
105
 
105
106
  #### [CRITICAL-001] Title
107
+
106
108
  - **Location**: file:line
107
109
  - **Pattern**: Code snippet
108
110
  - **Risk**: Description of the vulnerability
@@ -110,6 +112,7 @@ Load these references based on findings:
110
112
  - **Reference**: OWASP link
111
113
 
112
114
  ### High Findings
115
+
113
116
  [...]
114
117
  ```
115
118
 
@@ -15,12 +15,13 @@ Implementation:
15
15
 
16
16
  ```javascript
17
17
  // Generate unique nonce per request
18
- const crypto = require('crypto');
19
- const nonce = crypto.randomBytes(16).toString('base64');
18
+ const crypto = require("crypto");
19
+ const nonce = crypto.randomBytes(16).toString("base64");
20
20
 
21
21
  // Set header
22
- res.setHeader('Content-Security-Policy',
23
- `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`
22
+ res.setHeader(
23
+ "Content-Security-Policy",
24
+ `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`,
24
25
  );
25
26
 
26
27
  // Include nonce in script tags
@@ -44,45 +45,48 @@ echo -n 'console.log("hello");' | openssl sha256 -binary | openssl base64
44
45
 
45
46
  ## Essential Directives
46
47
 
47
- | Directive | Purpose | Recommended Value |
48
- |-----------|---------|-------------------|
49
- | `default-src` | Fallback for other directives | `'self'` |
50
- | `script-src` | JavaScript sources | `'nonce-{random}' 'strict-dynamic'` |
51
- | `style-src` | CSS sources | `'self' 'unsafe-inline'` (or nonces) |
52
- | `img-src` | Image sources | `'self' data: https:` |
53
- | `font-src` | Font sources | `'self'` |
54
- | `connect-src` | AJAX/WebSocket/Fetch | `'self' https://api.example.com` |
55
- | `frame-src` | iframe sources | `'none'` or specific origins |
56
- | `object-src` | Plugin content | `'none'` |
57
- | `base-uri` | Base URL restrictions | `'none'` |
58
- | `form-action` | Form submission targets | `'self'` |
59
- | `frame-ancestors` | Who can embed page | `'self'` or `'none'` |
48
+ | Directive | Purpose | Recommended Value |
49
+ | ----------------- | ----------------------------- | ------------------------------------ |
50
+ | `default-src` | Fallback for other directives | `'self'` |
51
+ | `script-src` | JavaScript sources | `'nonce-{random}' 'strict-dynamic'` |
52
+ | `style-src` | CSS sources | `'self' 'unsafe-inline'` (or nonces) |
53
+ | `img-src` | Image sources | `'self' data: https:` |
54
+ | `font-src` | Font sources | `'self'` |
55
+ | `connect-src` | AJAX/WebSocket/Fetch | `'self' https://api.example.com` |
56
+ | `frame-src` | iframe sources | `'none'` or specific origins |
57
+ | `object-src` | Plugin content | `'none'` |
58
+ | `base-uri` | Base URL restrictions | `'none'` |
59
+ | `form-action` | Form submission targets | `'self'` |
60
+ | `frame-ancestors` | Who can embed page | `'self'` or `'none'` |
60
61
 
61
62
  ## Framework Integration
62
63
 
63
64
  ### Express.js
64
65
 
65
66
  ```javascript
66
- const helmet = require('helmet');
67
+ const crypto = require("crypto");
68
+ const helmet = require("helmet");
67
69
 
68
70
  // Nonce middleware
69
71
  app.use((req, res, next) => {
70
- res.locals.nonce = crypto.randomBytes(16).toString('base64');
72
+ res.locals.nonce = crypto.randomBytes(16).toString("base64");
71
73
  next();
72
74
  });
73
75
 
74
- app.use(helmet.contentSecurityPolicy({
75
- directives: {
76
- defaultSrc: ["'self'"],
77
- scriptSrc: [(req, res) => `'nonce-${res.locals.nonce}'`, "'strict-dynamic'"],
78
- styleSrc: ["'self'", "'unsafe-inline'"],
79
- imgSrc: ["'self'", "data:", "https:"],
80
- objectSrc: ["'none'"],
81
- baseUri: ["'none'"],
82
- formAction: ["'self'"],
83
- frameAncestors: ["'self'"]
84
- }
85
- }));
76
+ app.use(
77
+ helmet.contentSecurityPolicy({
78
+ directives: {
79
+ defaultSrc: ["'self'"],
80
+ scriptSrc: [(req, res) => `'nonce-${res.locals.nonce}'`, "'strict-dynamic'"],
81
+ styleSrc: ["'self'", "'unsafe-inline'"],
82
+ imgSrc: ["'self'", "data:", "https:"],
83
+ objectSrc: ["'none'"],
84
+ baseUri: ["'none'"],
85
+ formAction: ["'self'"],
86
+ frameAncestors: ["'self'"],
87
+ },
88
+ }),
89
+ );
86
90
  ```
87
91
 
88
92
  ### Astro
@@ -91,26 +95,31 @@ app.use(helmet.contentSecurityPolicy({
91
95
  // astro.config.mjs
92
96
  export default defineConfig({
93
97
  vite: {
94
- plugins: [{
95
- name: 'csp-plugin',
96
- configureServer(server) {
97
- server.middlewares.use((req, res, next) => {
98
- res.setHeader('Content-Security-Policy',
99
- "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
100
- );
101
- next();
102
- });
103
- }
104
- }]
105
- }
98
+ plugins: [
99
+ {
100
+ name: "csp-plugin",
101
+ configureServer(server) {
102
+ server.middlewares.use((req, res, next) => {
103
+ res.setHeader(
104
+ "Content-Security-Policy",
105
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
106
+ );
107
+ next();
108
+ });
109
+ },
110
+ },
111
+ ],
112
+ },
106
113
  });
107
114
  ```
108
115
 
109
116
  ### Meta Tag (Fallback)
110
117
 
111
118
  ```html
112
- <meta http-equiv="Content-Security-Policy"
113
- content="default-src 'self'; script-src 'self' 'nonce-abc123';">
119
+ <meta
120
+ http-equiv="Content-Security-Policy"
121
+ content="default-src 'self'; script-src 'self' 'nonce-abc123';"
122
+ />
114
123
  ```
115
124
 
116
125
  **Note**: Meta tag CSP cannot set `frame-ancestors`, `report-uri`, or `sandbox`.
@@ -129,8 +138,8 @@ Content-Security-Policy-Report-Only:
129
138
  Report endpoint:
130
139
 
131
140
  ```javascript
132
- app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
133
- console.log('CSP Violation:', req.body);
141
+ app.post("/csp-report", express.json({ type: "application/csp-report" }), (req, res) => {
142
+ console.log("CSP Violation:", req.body);
134
143
  res.status(204).end();
135
144
  });
136
145
  ```
@@ -141,10 +150,14 @@ app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req,
141
150
 
142
151
  ```html
143
152
  <!-- Violation: inline script -->
144
- <script>alert('hello');</script>
153
+ <script>
154
+ alert("hello");
155
+ </script>
145
156
 
146
157
  <!-- Fix: add nonce -->
147
- <script nonce="abc123">alert('hello');</script>
158
+ <script nonce="abc123">
159
+ alert("hello");
160
+ </script>
148
161
 
149
162
  <!-- Or: move to external file -->
150
163
  <script src="/js/app.js"></script>
@@ -159,7 +172,7 @@ app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req,
159
172
  <!-- Fix: use addEventListener -->
160
173
  <button id="myBtn">Click</button>
161
174
  <script nonce="abc123">
162
- document.getElementById('myBtn').addEventListener('click', handleClick);
175
+ document.getElementById("myBtn").addEventListener("click", handleClick);
163
176
  </script>
164
177
  ```
165
178
 
@@ -173,7 +186,11 @@ app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req,
173
186
  <div class="text-red">Text</div>
174
187
 
175
188
  <!-- Or: add nonce to style tags -->
176
- <style nonce="abc123">.text-red { color: red; }</style>
189
+ <style nonce="abc123">
190
+ .text-red {
191
+ color: red;
192
+ }
193
+ </style>
177
194
  ```
178
195
 
179
196
  ## Security Headers Companion