@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.
- package/dist/index.mjs.map +1 -1
- package/hooks/auto-approve-safe-commands/hook.mjs +5 -1
- package/hooks/auto-approve-safe-commands/hook.mts +7 -6
- package/hooks/block-dangerous-commands/hook.mjs +3 -3
- package/hooks/block-dangerous-commands/hook.mts +10 -22
- package/package.json +9 -9
- package/skills/css-tokens/SKILL.md +1 -1
- package/skills/css-tokens/references/tokens.css +6 -10
- package/skills/frontend-security/SKILL.md +3 -0
- package/skills/frontend-security/references/csp-configuration.md +68 -51
- package/skills/frontend-security/references/csrf-protection.md +74 -70
- package/skills/frontend-security/references/dom-security.md +36 -29
- package/skills/frontend-security/references/file-upload-security.md +101 -69
- package/skills/frontend-security/references/framework-patterns.md +42 -40
- package/skills/frontend-security/references/input-validation.md +36 -31
- package/skills/frontend-security/references/jwt-security.md +68 -84
- package/skills/frontend-security/references/nodejs-npm-security.md +63 -55
- package/skills/frontend-security/references/xss-prevention.md +38 -36
- package/skills/frontend-testing/SKILL.md +31 -38
- package/skills/frontend-testing/references/accessibility-testing.md +56 -62
- package/skills/frontend-testing/references/aria-snapshots.md +35 -34
- package/skills/frontend-testing/references/locator-strategies.md +37 -40
- package/skills/frontend-testing/references/visual-regression.md +29 -23
- package/skills/npm-publishing-best-practices/SKILL.md +316 -0
- package/skills/semantic-html/SKILL.md +5 -21
- package/skills/semantic-html/references/heading-patterns.md +1 -5
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * toolkit — personal CLI for managing Claude Code hooks, skills, and commands.\n *\n * Commands:\n * toolkit add hook <name>\n * toolkit add skill <name> [--link <target>...]\n * toolkit add command <name>\n * toolkit add collections <name>\n * toolkit update [--force]\n * toolkit list hook\n * toolkit list skill\n * toolkit list command\n * toolkit list collections\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createInterface } from \"node:readline/promises\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nconst TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst HOOKS_SRC = join(TOOLKIT_ROOT, \"hooks\");\nconst SKILLS_SRC = join(TOOLKIT_ROOT, \"skills\");\nconst COMMANDS_SRC = join(TOOLKIT_ROOT, \"commands\");\nconst CONFIG_PATH = join(TOOLKIT_ROOT, \"config.json\");\n\nconst PROJECT_ROOT = process.cwd();\nconst CLAUDE_DIR = join(PROJECT_ROOT, \".claude\");\nconst TOOLKIT_DIR = join(PROJECT_ROOT, \".claude-toolkit\");\nconst MANIFEST_PATH = join(CLAUDE_DIR, \"toolkit-manifest.json\");\n\ntype HookEntry = { hash: string; installedAt: string };\ntype SkillEntry = { hash: string; installedAt: string; linkedTo: string[] };\ntype CommandEntry = { hash: string; installedAt: string };\ntype Manifest = {\n commands: Record<string, CommandEntry>;\n hooks: Record<string, HookEntry>;\n skills: Record<string, SkillEntry>;\n};\ntype CollectionItemKind = \"command\" | \"hook\" | \"skill\";\ntype CollectionItemConfig = {\n type: CollectionItemKind | `${CollectionItemKind}s`;\n src: string;\n};\ntype CollectionConfig = {\n name: string;\n items: CollectionItemConfig[];\n};\ntype ResolvedCollectionItem = {\n collection: string;\n sourcePath: string;\n sourceName: string;\n type: CollectionItemKind;\n};\n\n// ---------- helpers ----------\n\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction shortHash(content: string | Buffer): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 7);\n}\n\nfunction readManifest(): Manifest {\n if (!existsSync(MANIFEST_PATH)) {\n return { commands: {}, hooks: {}, skills: {} };\n }\n\n try {\n const parsed = JSON.parse(\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
|
|
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) &&
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
24
|
-
"
|
|
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
|
-
"
|
|
32
|
-
|
|
33
|
-
"build:hooks": "tsc --project tsconfig.hooks.json"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
34
|
+
"packageManager": "pnpm@11.1.1"
|
|
35
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
19
|
-
const nonce = crypto.randomBytes(16).toString(
|
|
18
|
+
const crypto = require("crypto");
|
|
19
|
+
const nonce = crypto.randomBytes(16).toString("base64");
|
|
20
20
|
|
|
21
21
|
// Set header
|
|
22
|
-
res.setHeader(
|
|
23
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
| `default-src`
|
|
50
|
-
| `script-src`
|
|
51
|
-
| `style-src`
|
|
52
|
-
| `img-src`
|
|
53
|
-
| `font-src`
|
|
54
|
-
| `connect-src`
|
|
55
|
-
| `frame-src`
|
|
56
|
-
| `object-src`
|
|
57
|
-
| `base-uri`
|
|
58
|
-
| `form-action`
|
|
59
|
-
| `frame-ancestors` | Who can embed page
|
|
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
|
|
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(
|
|
72
|
+
res.locals.nonce = crypto.randomBytes(16).toString("base64");
|
|
71
73
|
next();
|
|
72
74
|
});
|
|
73
75
|
|
|
74
|
-
app.use(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
server
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
113
|
-
|
|
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(
|
|
133
|
-
console.log(
|
|
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>
|
|
153
|
+
<script>
|
|
154
|
+
alert("hello");
|
|
155
|
+
</script>
|
|
145
156
|
|
|
146
157
|
<!-- Fix: add nonce -->
|
|
147
|
-
<script nonce="abc123">
|
|
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(
|
|
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"
|
|
189
|
+
<style nonce="abc123">
|
|
190
|
+
.text-red {
|
|
191
|
+
color: red;
|
|
192
|
+
}
|
|
193
|
+
</style>
|
|
177
194
|
```
|
|
178
195
|
|
|
179
196
|
## Security Headers Companion
|