safeword 0.6.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/{check-OYYSYHFP.js → check-QGZJ62PY.js} +73 -57
  2. package/dist/check-QGZJ62PY.js.map +1 -0
  3. package/dist/{chunk-ZS3Z3Q37.js → chunk-QPO3C3FP.js} +285 -65
  4. package/dist/chunk-QPO3C3FP.js.map +1 -0
  5. package/dist/{chunk-LNSEDZIW.js → chunk-YMLVQC4V.js} +159 -152
  6. package/dist/chunk-YMLVQC4V.js.map +1 -0
  7. package/dist/cli.js +6 -6
  8. package/dist/{diff-325TIZ63.js → diff-654SSCFQ.js} +51 -53
  9. package/dist/diff-654SSCFQ.js.map +1 -0
  10. package/dist/index.d.ts +1 -0
  11. package/dist/{reset-ZGJIKMUW.js → reset-IU6AIT7C.js} +3 -3
  12. package/dist/{setup-GAMXTFM2.js → setup-5IQ4KV2M.js} +17 -20
  13. package/dist/setup-5IQ4KV2M.js.map +1 -0
  14. package/dist/{sync-BFMXZEHM.js → sync-YBQEISFI.js} +8 -26
  15. package/dist/sync-YBQEISFI.js.map +1 -0
  16. package/dist/{upgrade-X4GREJXN.js → upgrade-RRTWEQIP.js} +3 -3
  17. package/package.json +1 -1
  18. package/templates/SAFEWORD.md +141 -73
  19. package/templates/commands/architecture.md +1 -1
  20. package/templates/commands/lint.md +1 -0
  21. package/templates/commands/quality-review.md +1 -1
  22. package/templates/cursor/rules/safeword-core.mdc +5 -0
  23. package/templates/doc-templates/architecture-template.md +1 -1
  24. package/templates/doc-templates/task-spec-template.md +151 -0
  25. package/templates/doc-templates/ticket-template.md +2 -4
  26. package/templates/guides/architecture-guide.md +2 -2
  27. package/templates/guides/code-philosophy.md +1 -1
  28. package/templates/guides/context-files-guide.md +3 -3
  29. package/templates/guides/design-doc-guide.md +2 -2
  30. package/templates/guides/development-workflow.md +2 -2
  31. package/templates/guides/learning-extraction.md +9 -9
  32. package/templates/guides/tdd-best-practices.md +39 -38
  33. package/templates/guides/test-definitions-guide.md +15 -14
  34. package/templates/hooks/cursor/after-file-edit.sh +66 -0
  35. package/templates/hooks/cursor/stop.sh +50 -0
  36. package/templates/hooks/post-tool-lint.sh +19 -5
  37. package/templates/hooks/prompt-questions.sh +1 -1
  38. package/templates/hooks/prompt-timestamp.sh +8 -1
  39. package/templates/hooks/session-lint-check.sh +1 -1
  40. package/templates/hooks/session-verify-agents.sh +1 -1
  41. package/templates/hooks/session-version.sh +1 -1
  42. package/templates/hooks/stop-quality.sh +2 -2
  43. package/templates/markdownlint-cli2.jsonc +18 -19
  44. package/templates/scripts/bisect-test-pollution.sh +87 -0
  45. package/templates/scripts/bisect-zombie-processes.sh +129 -0
  46. package/templates/scripts/lint-md.sh +16 -0
  47. package/templates/skills/safeword-quality-reviewer/SKILL.md +3 -3
  48. package/templates/skills/safeword-systematic-debugger/SKILL.md +246 -0
  49. package/templates/skills/safeword-tdd-enforcer/SKILL.md +221 -0
  50. package/dist/check-OYYSYHFP.js.map +0 -1
  51. package/dist/chunk-LNSEDZIW.js.map +0 -1
  52. package/dist/chunk-ZS3Z3Q37.js.map +0 -1
  53. package/dist/diff-325TIZ63.js.map +0 -1
  54. package/dist/setup-GAMXTFM2.js.map +0 -1
  55. package/dist/sync-BFMXZEHM.js.map +0 -1
  56. /package/dist/{reset-ZGJIKMUW.js.map → reset-IU6AIT7C.js.map} +0 -0
  57. /package/dist/{upgrade-X4GREJXN.js.map → upgrade-RRTWEQIP.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/fs.ts","../src/utils/project-detector.ts","../src/templates/content.ts","../src/templates/config.ts","../src/utils/boundaries.ts","../src/utils/install.ts","../src/utils/hooks.ts","../src/schema.ts"],"sourcesContent":["/**\n * File system utilities for CLI operations\n */\n\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n rmSync,\n rmdirSync,\n readdirSync,\n chmodSync,\n} from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n// Get the directory of this module (for locating templates)\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Get path to bundled templates directory.\n * Works in both development (src/) and production (dist/) contexts.\n *\n * Note: We check for SAFEWORD.md to distinguish from src/templates/ which\n * contains TypeScript source files (config.ts, content.ts).\n *\n * Path resolution (bundled with tsup):\n * - From dist/chunk-*.js: __dirname = packages/cli/dist/ → ../templates\n */\nexport function getTemplatesDir(): string {\n const knownTemplateFile = 'SAFEWORD.md';\n\n // Try different relative paths - the bundled code ends up in dist/ directly (flat)\n // while source is in src/utils/\n const candidates = [\n join(__dirname, '..', 'templates'), // From dist/ (flat bundled)\n join(__dirname, '..', '..', 'templates'), // From src/utils/ or dist/utils/\n join(__dirname, 'templates'), // Direct sibling (unlikely but safe)\n ];\n\n for (const candidate of candidates) {\n if (existsSync(join(candidate, knownTemplateFile))) {\n return candidate;\n }\n }\n\n throw new Error('Templates directory not found');\n}\n\n/**\n * Check if a path exists\n */\nexport function exists(path: string): boolean {\n return existsSync(path);\n}\n\n/**\n * Create directory recursively\n */\nexport function ensureDir(path: string): void {\n if (!existsSync(path)) {\n mkdirSync(path, { recursive: true });\n }\n}\n\n/**\n * Read file as string\n */\nexport function readFile(path: string): string {\n return readFileSync(path, 'utf-8');\n}\n\n/**\n * Read file as string, return null if not exists\n */\nexport function readFileSafe(path: string): string | null {\n if (!existsSync(path)) return null;\n return readFileSync(path, 'utf-8');\n}\n\n/**\n * Write file, creating parent directories if needed\n */\nexport function writeFile(path: string, content: string): void {\n ensureDir(dirname(path));\n writeFileSync(path, content);\n}\n\n/**\n * Remove file or directory recursively\n */\nexport function remove(path: string): void {\n if (existsSync(path)) {\n rmSync(path, { recursive: true, force: true });\n }\n}\n\n/**\n * Remove directory only if empty, returns true if removed\n */\nexport function removeIfEmpty(path: string): boolean {\n if (!existsSync(path)) return false;\n try {\n rmdirSync(path); // Non-recursive, throws if not empty\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Make all shell scripts in a directory executable\n */\nexport function makeScriptsExecutable(dirPath: string): void {\n if (!existsSync(dirPath)) return;\n for (const file of readdirSync(dirPath)) {\n if (file.endsWith('.sh')) {\n chmodSync(join(dirPath, file), 0o755);\n }\n }\n}\n\n/**\n * Read JSON file\n */\nexport function readJson<T = unknown>(path: string): T | null {\n const content = readFileSafe(path);\n if (!content) return null;\n try {\n return JSON.parse(content) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Write JSON file with formatting\n */\nexport function writeJson(path: string, data: unknown): void {\n writeFile(path, JSON.stringify(data, null, 2) + '\\n');\n}\n","/**\n * Project type detection from package.json\n *\n * Detects frameworks and tools used in the project to configure\n * appropriate linting rules.\n */\n\nimport { readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport interface PackageJson {\n name?: string;\n version?: string;\n private?: boolean;\n main?: string;\n module?: string;\n exports?: unknown;\n types?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\nexport interface ProjectType {\n typescript: boolean;\n react: boolean;\n nextjs: boolean;\n astro: boolean;\n vue: boolean;\n nuxt: boolean;\n svelte: boolean;\n sveltekit: boolean;\n electron: boolean;\n vitest: boolean;\n playwright: boolean;\n tailwind: boolean;\n publishableLibrary: boolean;\n shell: boolean;\n}\n\n/**\n * Checks if a directory contains any .sh files up to specified depth.\n * Excludes node_modules and .git directories.\n */\nexport function hasShellScripts(cwd: string, maxDepth = 4): boolean {\n const excludeDirs = new Set(['node_modules', '.git', '.safeword']);\n\n function scan(dir: string, depth: number): boolean {\n if (depth > maxDepth) return false;\n\n try {\n const entries = readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isFile() && entry.name.endsWith('.sh')) {\n return true;\n }\n if (entry.isDirectory() && !excludeDirs.has(entry.name)) {\n if (scan(join(dir, entry.name), depth + 1)) {\n return true;\n }\n }\n }\n } catch {\n // Ignore permission errors\n }\n return false;\n }\n\n return scan(cwd, 0);\n}\n\n/**\n * Detects project type from package.json contents and optional file scanning\n */\nexport function detectProjectType(packageJson: PackageJson, cwd?: string): ProjectType {\n const deps = packageJson.dependencies || {};\n const devDeps = packageJson.devDependencies || {};\n const allDeps = { ...deps, ...devDeps };\n\n const hasTypescript = 'typescript' in allDeps;\n const hasReact = 'react' in deps || 'react' in devDeps;\n const hasNextJs = 'next' in deps;\n const hasAstro = 'astro' in deps || 'astro' in devDeps;\n const hasVue = 'vue' in deps || 'vue' in devDeps;\n const hasNuxt = 'nuxt' in deps;\n const hasSvelte = 'svelte' in deps || 'svelte' in devDeps;\n const hasSvelteKit = '@sveltejs/kit' in deps || '@sveltejs/kit' in devDeps;\n const hasElectron = 'electron' in deps || 'electron' in devDeps;\n const hasVitest = 'vitest' in devDeps;\n const hasPlaywright = '@playwright/test' in devDeps;\n const hasTailwind = 'tailwindcss' in allDeps;\n\n // Publishable library: has entry points and is not marked private\n const hasEntryPoints = !!(packageJson.main || packageJson.module || packageJson.exports);\n const isPublishable = hasEntryPoints && packageJson.private !== true;\n\n // Shell scripts: detected by scanning for .sh files\n const hasShell = cwd ? hasShellScripts(cwd) : false;\n\n return {\n typescript: hasTypescript,\n react: hasReact || hasNextJs, // Next.js implies React\n nextjs: hasNextJs,\n astro: hasAstro,\n vue: hasVue || hasNuxt, // Nuxt implies Vue\n nuxt: hasNuxt,\n svelte: hasSvelte || hasSvelteKit, // SvelteKit implies Svelte\n sveltekit: hasSvelteKit,\n electron: hasElectron,\n vitest: hasVitest,\n playwright: hasPlaywright,\n tailwind: hasTailwind,\n publishableLibrary: isPublishable,\n shell: hasShell,\n };\n}\n","/**\n * Content templates - static string content\n *\n * Note: Most templates (SAFEWORD.md, hooks, skills, guides, etc.) are now\n * file-based in the templates/ directory. This file contains only small\n * string constants that are used inline.\n */\n\nimport type { ProjectType } from '../utils/project-detector.js';\n\nexport const AGENTS_MD_LINK = `**⚠️ ALWAYS READ FIRST: @./.safeword/SAFEWORD.md**\n\nThe SAFEWORD.md file contains core development patterns, workflows, and conventions.\nRead it BEFORE working on any task in this project.\n\n---`;\n\ninterface PrettierConfig {\n semi: boolean;\n singleQuote: boolean;\n tabWidth: number;\n trailingComma: string;\n printWidth: number;\n endOfLine: string;\n plugins?: string[];\n}\n\n/**\n * Generate .prettierrc content based on project type.\n * Explicitly lists plugins to ensure compatibility with pnpm/Yarn PnP.\n */\nexport function getPrettierConfig(projectType: ProjectType): string {\n const config: PrettierConfig = {\n semi: true,\n singleQuote: true,\n tabWidth: 2,\n trailingComma: 'es5',\n printWidth: 100,\n endOfLine: 'lf',\n };\n\n const plugins: string[] = [];\n\n if (projectType.astro) plugins.push('prettier-plugin-astro');\n if (projectType.svelte) plugins.push('prettier-plugin-svelte');\n if (projectType.shell) plugins.push('prettier-plugin-sh');\n // Tailwind must be last for proper class sorting\n if (projectType.tailwind) plugins.push('prettier-plugin-tailwindcss');\n\n if (plugins.length > 0) {\n config.plugins = plugins;\n }\n\n return JSON.stringify(config, null, 2) + '\\n';\n}\n\n/**\n * Generate lint-staged configuration based on project type.\n * Only includes shell patterns when shell scripts are detected.\n *\n * SYNC: Keep file patterns in sync with post-tool-lint.sh in:\n * packages/cli/templates/hooks/post-tool-lint.sh\n */\nexport function getLintStagedConfig(projectType: ProjectType): Record<string, string[]> {\n const config: Record<string, string[]> = {\n '*.{js,jsx,ts,tsx,mjs,mts,cjs,cts}': ['eslint --fix', 'prettier --write'],\n '*.{vue,svelte,astro}': ['eslint --fix', 'prettier --write'],\n '*.{json,css,scss,html,yaml,yml,graphql}': ['prettier --write'],\n '*.md': ['markdownlint-cli2 --fix', 'prettier --write'],\n };\n\n if (projectType.shell) {\n config['*.sh'] = ['shellcheck', 'prettier --write'];\n }\n\n return config;\n}\n","/**\n * Configuration templates - ESLint config generation and hook settings\n *\n * ESLint flat config (v9+) with:\n * - Dynamic framework detection from package.json at runtime\n * - Static imports for base plugins (always installed by safeword)\n * - Dynamic imports for framework plugins (loaded only if framework detected)\n * - defineConfig helper for validation and type checking\n * - eslint-config-prettier last to avoid conflicts\n *\n * See: https://eslint.org/docs/latest/use/configure/configuration-files\n */\n\n/**\n * Generates a dynamic ESLint config that adapts to project frameworks at runtime.\n *\n * The generated config reads package.json to detect frameworks and dynamically\n * imports the corresponding ESLint plugins. This allows the config to be generated\n * once at setup and automatically adapt when frameworks are added or removed.\n *\n * @param options.boundaries - Whether to include architecture boundaries config\n * @returns ESLint config file content as a string\n */\nexport function getEslintConfig(options: { boundaries?: boolean }): string {\n return `/* eslint-disable import-x/no-unresolved -- dynamic imports for optional framework plugins */\nimport { readFileSync } from \"fs\";\nimport { defineConfig } from \"eslint/config\";\nimport js from \"@eslint/js\";\nimport { importX } from \"eslint-plugin-import-x\";\nimport { createTypeScriptImportResolver } from \"eslint-import-resolver-typescript\";\nimport sonarjs from \"eslint-plugin-sonarjs\";\nimport sdl from \"@microsoft/eslint-plugin-sdl\";\nimport playwright from \"eslint-plugin-playwright\";\nimport unicorn from \"eslint-plugin-unicorn\";\nimport eslintConfigPrettier from \"eslint-config-prettier\";\n${options.boundaries ? 'import boundariesConfig from \"./.safeword/eslint-boundaries.config.mjs\";' : ''}\n\n// Read package.json to detect frameworks at runtime\nconst pkg = JSON.parse(readFileSync(\"./package.json\", \"utf8\"));\nconst deps = { ...pkg.dependencies, ...pkg.devDependencies };\n\n// Build dynamic ignores based on detected frameworks\nconst ignores = [\"**/node_modules/\", \"**/dist/\", \"**/build/\", \"**/coverage/\"];\nif (deps[\"next\"]) ignores.push(\".next/\");\nif (deps[\"astro\"]) ignores.push(\".astro/\");\nif (deps[\"vue\"] || deps[\"nuxt\"]) ignores.push(\".nuxt/\");\nif (deps[\"svelte\"] || deps[\"@sveltejs/kit\"]) ignores.push(\".svelte-kit/\");\n\n// Start with base configs (always loaded)\nconst configs = [\n { ignores },\n js.configs.recommended,\n importX.flatConfigs.recommended,\n {\n settings: {\n \"import-x/resolver-next\": [createTypeScriptImportResolver()],\n },\n },\n sonarjs.configs.recommended,\n ...sdl.configs.recommended,\n unicorn.configs[\"flat/recommended\"],\n {\n // Unicorn overrides for LLM-generated code\n // Keep modern JS enforcement, disable overly pedantic rules\n rules: {\n \"unicorn/prevent-abbreviations\": \"off\", // ctx, dir, pkg, err are standard\n \"unicorn/no-null\": \"off\", // null is valid JS\n \"unicorn/no-process-exit\": \"off\", // CLI apps use process.exit\n \"unicorn/import-style\": \"off\", // Named imports are fine\n \"unicorn/numeric-separators-style\": \"off\", // Style preference\n \"unicorn/text-encoding-identifier-case\": \"off\", // utf-8 vs utf8\n \"unicorn/switch-case-braces\": \"warn\", // Good practice, not critical\n \"unicorn/catch-error-name\": \"warn\", // Reasonable, auto-fixable\n \"unicorn/no-negated-condition\": \"off\", // Sometimes clearer\n \"unicorn/no-array-reduce\": \"off\", // Reduce is fine when readable\n \"unicorn/no-array-for-each\": \"off\", // forEach is fine\n \"unicorn/prefer-module\": \"off\", // CJS still valid\n },\n },\n];\n\n// TypeScript support (detected from package.json)\nif (deps[\"typescript\"] || deps[\"typescript-eslint\"]) {\n const tseslint = await import(\"typescript-eslint\");\n configs.push(importX.flatConfigs.typescript);\n configs.push(...tseslint.default.configs.recommended);\n}\n\n// React/Next.js support\nif (deps[\"react\"] || deps[\"next\"]) {\n const react = await import(\"eslint-plugin-react\");\n const reactHooks = await import(\"eslint-plugin-react-hooks\");\n const jsxA11y = await import(\"eslint-plugin-jsx-a11y\");\n configs.push(react.default.configs.flat.recommended);\n configs.push(react.default.configs.flat[\"jsx-runtime\"]);\n configs.push({\n name: \"react-hooks\",\n plugins: { \"react-hooks\": reactHooks.default },\n rules: reactHooks.default.configs.recommended.rules,\n });\n configs.push(jsxA11y.default.flatConfigs.recommended);\n}\n\n// Next.js plugin\nif (deps[\"next\"]) {\n const nextPlugin = await import(\"@next/eslint-plugin-next\");\n configs.push({\n name: \"nextjs\",\n plugins: { \"@next/next\": nextPlugin.default },\n rules: nextPlugin.default.configs.recommended.rules,\n });\n}\n\n// Astro support\nif (deps[\"astro\"]) {\n const astro = await import(\"eslint-plugin-astro\");\n configs.push(...astro.default.configs.recommended);\n}\n\n// Vue support\nif (deps[\"vue\"] || deps[\"nuxt\"]) {\n const vue = await import(\"eslint-plugin-vue\");\n configs.push(...vue.default.configs[\"flat/recommended\"]);\n}\n\n// Svelte support\nif (deps[\"svelte\"] || deps[\"@sveltejs/kit\"]) {\n const svelte = await import(\"eslint-plugin-svelte\");\n configs.push(...svelte.default.configs.recommended);\n}\n\n// Electron support\nif (deps[\"electron\"]) {\n const electron = await import(\"@electron-toolkit/eslint-config\");\n configs.push(electron.default);\n}\n\n// Vitest support (scoped to test files)\nif (deps[\"vitest\"]) {\n const vitest = await import(\"@vitest/eslint-plugin\");\n configs.push({\n name: \"vitest\",\n files: [\"**/*.test.{js,ts,jsx,tsx}\", \"**/*.spec.{js,ts,jsx,tsx}\", \"**/tests/**\"],\n plugins: { vitest: vitest.default },\n languageOptions: {\n globals: { ...vitest.default.environments.env.globals },\n },\n rules: { ...vitest.default.configs.recommended.rules },\n });\n}\n\n// Playwright for e2e tests (always included - safeword sets up Playwright)\nconfigs.push({\n name: \"playwright\",\n files: [\"**/e2e/**\", \"**/*.e2e.{js,ts,jsx,tsx}\", \"**/playwright/**\"],\n ...playwright.configs[\"flat/recommended\"],\n});\n\n// Architecture boundaries${options.boundaries ? '\\nconfigs.push(boundariesConfig);' : ''}\n\n// eslint-config-prettier must be last to disable conflicting rules\nconfigs.push(eslintConfigPrettier);\n\nexport default defineConfig(configs);\n`;\n}\n\n// Cursor hooks configuration (.cursor/hooks.json format)\n// See: https://cursor.com/docs/agent/hooks\nexport const CURSOR_HOOKS = {\n afterFileEdit: [{ command: './.safeword/hooks/cursor/after-file-edit.sh' }],\n stop: [{ command: './.safeword/hooks/cursor/stop.sh' }],\n};\n\n// Claude Code hooks configuration (.claude/settings.json format)\nexport const SETTINGS_HOOKS = {\n SessionStart: [\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/session-verify-agents.sh',\n },\n ],\n },\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/session-version.sh',\n },\n ],\n },\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/session-lint-check.sh',\n },\n ],\n },\n ],\n UserPromptSubmit: [\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/prompt-timestamp.sh',\n },\n ],\n },\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/prompt-questions.sh',\n },\n ],\n },\n ],\n Stop: [\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/stop-quality.sh',\n },\n ],\n },\n ],\n PostToolUse: [\n {\n matcher: 'Write|Edit|MultiEdit|NotebookEdit',\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/post-tool-lint.sh',\n },\n ],\n },\n ],\n};\n","/**\n * Architecture boundaries detection and config generation\n *\n * Auto-detects common architecture directories and generates\n * eslint-plugin-boundaries config with sensible hierarchy rules.\n *\n * Supports:\n * - Standard projects (src/utils, utils/)\n * - Monorepos (packages/*, apps/*)\n * - Various naming conventions (helpers, shared, core, etc.)\n */\n\nimport { join } from 'node:path';\nimport { readdirSync } from 'node:fs';\nimport { exists } from './fs.js';\n\n/**\n * Architecture layer definitions with alternative names.\n * Each layer maps to equivalent directory names.\n * Order defines hierarchy: earlier = lower layer.\n */\nconst ARCHITECTURE_LAYERS = [\n // Layer 0: Pure types (no imports)\n { layer: 'types', dirs: ['types', 'interfaces', 'schemas'] },\n // Layer 1: Utilities (only types)\n { layer: 'utils', dirs: ['utils', 'helpers', 'shared', 'common', 'core'] },\n // Layer 2: Libraries (types, utils)\n { layer: 'lib', dirs: ['lib', 'libraries'] },\n // Layer 3: State & logic (types, utils, lib)\n { layer: 'hooks', dirs: ['hooks', 'composables'] },\n { layer: 'services', dirs: ['services', 'api', 'stores', 'state'] },\n // Layer 4: UI components (all above)\n { layer: 'components', dirs: ['components', 'ui'] },\n // Layer 5: Features (all above)\n { layer: 'features', dirs: ['features', 'modules', 'domains'] },\n // Layer 6: Entry points (can import everything)\n { layer: 'app', dirs: ['app', 'pages', 'views', 'routes', 'commands'] },\n] as const;\n\ntype Layer = (typeof ARCHITECTURE_LAYERS)[number]['layer'];\n\n/**\n * Hierarchy rules: what each layer can import\n * Lower layers have fewer import permissions\n */\nconst HIERARCHY: Record<Layer, Layer[]> = {\n types: [],\n utils: ['types'],\n lib: ['utils', 'types'],\n hooks: ['lib', 'utils', 'types'],\n services: ['lib', 'utils', 'types'],\n components: ['hooks', 'services', 'lib', 'utils', 'types'],\n features: ['components', 'hooks', 'services', 'lib', 'utils', 'types'],\n app: ['features', 'components', 'hooks', 'services', 'lib', 'utils', 'types'],\n};\n\nexport interface DetectedElement {\n layer: Layer;\n pattern: string; // glob pattern for boundaries config\n location: string; // human-readable location\n}\n\nexport interface DetectedArchitecture {\n elements: DetectedElement[];\n isMonorepo: boolean;\n}\n\n/**\n * Find monorepo package directories\n */\nfunction findMonorepoPackages(projectDir: string): string[] {\n const packages: string[] = [];\n\n // Check common monorepo patterns\n const monorepoRoots = ['packages', 'apps', 'libs', 'modules'];\n\n for (const root of monorepoRoots) {\n const rootPath = join(projectDir, root);\n if (exists(rootPath)) {\n try {\n const entries = readdirSync(rootPath, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n packages.push(join(root, entry.name));\n }\n }\n } catch {\n // Directory not readable, skip\n }\n }\n }\n\n return packages;\n}\n\n/**\n * Check if a layer already exists for this path prefix\n */\nfunction hasLayerForPrefix(elements: DetectedElement[], layer: Layer, pathPrefix: string): boolean {\n return elements.some(e => e.layer === layer && e.pattern.startsWith(pathPrefix));\n}\n\n/**\n * Scan a single search path for architecture layers\n */\nfunction scanSearchPath(\n projectDir: string,\n searchPath: string,\n pathPrefix: string,\n elements: DetectedElement[],\n): void {\n for (const layerDef of ARCHITECTURE_LAYERS) {\n for (const dirName of layerDef.dirs) {\n const fullPath = join(projectDir, searchPath, dirName);\n if (exists(fullPath) && !hasLayerForPrefix(elements, layerDef.layer, pathPrefix)) {\n elements.push({\n layer: layerDef.layer,\n pattern: `${pathPrefix}${dirName}/**`,\n location: `${pathPrefix}${dirName}`,\n });\n }\n }\n }\n}\n\n/**\n * Scan a directory for architecture layers\n */\nfunction scanForLayers(projectDir: string, basePath: string): DetectedElement[] {\n const elements: DetectedElement[] = [];\n const prefix = basePath ? `${basePath}/` : '';\n\n // Check src/ and root level\n scanSearchPath(projectDir, join(basePath, 'src'), `${prefix}src/`, elements);\n scanSearchPath(projectDir, basePath, prefix, elements);\n\n return elements;\n}\n\n/**\n * Detects architecture directories in the project\n * Handles both standard projects and monorepos\n */\nexport function detectArchitecture(projectDir: string): DetectedArchitecture {\n const elements: DetectedElement[] = [];\n\n // First, check for monorepo packages\n const packages = findMonorepoPackages(projectDir);\n const isMonorepo = packages.length > 0;\n\n if (isMonorepo) {\n // Scan each package\n for (const pkg of packages) {\n elements.push(...scanForLayers(projectDir, pkg));\n }\n }\n\n // Also scan root level (works for both monorepo root and standard projects)\n elements.push(...scanForLayers(projectDir, ''));\n\n // Deduplicate by pattern\n const seen = new Set<string>();\n const uniqueElements = elements.filter(e => {\n if (seen.has(e.pattern)) return false;\n seen.add(e.pattern);\n return true;\n });\n\n return { elements: uniqueElements, isMonorepo };\n}\n\n/**\n * Format a single element for the config\n */\nfunction formatElement(el: DetectedElement): string {\n return ` { type: '${el.layer}', pattern: '${el.pattern}', mode: 'full' }`;\n}\n\n/**\n * Format allowed imports for a rule\n */\nfunction formatAllowedImports(allowed: Layer[]): string {\n return allowed.map(d => `'${d}'`).join(', ');\n}\n\n/**\n * Generate a single rule for what a layer can import\n */\nfunction generateRule(layer: Layer, detectedLayers: Set<Layer>): string | null {\n const allowedLayers = HIERARCHY[layer];\n if (allowedLayers.length === 0) return null;\n\n const allowed = allowedLayers.filter(dep => detectedLayers.has(dep));\n if (allowed.length === 0) return null;\n\n return ` { from: ['${layer}'], allow: [${formatAllowedImports(allowed)}] }`;\n}\n\n/**\n * Build description of what was detected\n */\nfunction buildDetectedInfo(arch: DetectedArchitecture): string {\n if (arch.elements.length === 0) {\n return 'No architecture directories detected yet - add types/, utils/, components/, etc.';\n }\n const locations = arch.elements.map(e => e.location).join(', ');\n const monorepoNote = arch.isMonorepo ? ' (monorepo)' : '';\n return `Detected: ${locations}${monorepoNote}`;\n}\n\nexport function generateBoundariesConfig(arch: DetectedArchitecture): string {\n const hasElements = arch.elements.length > 0;\n\n // Generate element definitions\n const elementsContent = arch.elements.map(el => formatElement(el)).join(',\\n');\n\n // Generate rules (what each layer can import)\n const detectedLayers = new Set(arch.elements.map(e => e.layer));\n const rules = [...detectedLayers]\n .map(layer => generateRule(layer, detectedLayers))\n .filter((rule): rule is string => rule !== null);\n const rulesContent = rules.join(',\\n');\n\n const detectedInfo = buildDetectedInfo(arch);\n\n return `/**\n * Architecture Boundaries Configuration (AUTO-GENERATED)\n *\n * ${detectedInfo}\n *\n * This enforces import boundaries between architectural layers:\n * - Lower layers (types, utils) cannot import from higher layers (components, features)\n * - Uses 'warn' severity - informative, not blocking\n *\n * Recognized directories (in hierarchy order):\n * types → utils → lib → hooks/services → components → features/modules → app\n *\n * To customize, override in your eslint.config.mjs:\n * rules: { 'boundaries/element-types': ['error', { ... }] }\n */\n\nimport boundaries from 'eslint-plugin-boundaries';\n\nexport default {\n plugins: { boundaries },\n settings: {\n 'boundaries/elements': [\n${elementsContent}\n ],\n },\n rules: {${\n hasElements\n ? `\n 'boundaries/element-types': ['warn', {\n default: 'disallow',\n rules: [\n${rulesContent}\n ],\n }],`\n : ''\n }\n 'boundaries/no-unknown': 'off', // Allow files outside defined elements\n 'boundaries/no-unknown-files': 'off', // Allow non-matching files\n },\n};\n`;\n}\n","/**\n * Shared installation constants\n *\n * These constants are used by schema.ts to define the single source of truth.\n * All functions have been removed - reconcile.ts now handles all operations.\n */\n\n/**\n * Husky pre-commit hook content - includes safeword sync + lint-staged\n * The sync command keeps ESLint plugins aligned with detected frameworks\n */\nexport const HUSKY_PRE_COMMIT_CONTENT = 'npx safeword sync --quiet --stage\\nnpx lint-staged\\n';\n\n/**\n * MCP servers installed by safeword\n */\nexport const MCP_SERVERS = {\n context7: {\n command: 'npx',\n args: ['-y', '@upstash/context7-mcp@latest'],\n },\n playwright: {\n command: 'npx',\n args: ['@playwright/mcp@latest'],\n },\n} as const;\n\n// NOTE: All other constants and functions were removed in the declarative schema refactor.\n// The single source of truth is now SAFEWORD_SCHEMA in src/schema.ts.\n// Operations are handled by reconcile() in src/reconcile.ts.\n","/**\n * Hook utilities for Claude Code settings\n */\n\ninterface HookCommand {\n type: string;\n command: string;\n}\n\ninterface HookEntry {\n matcher?: string;\n hooks: HookCommand[];\n}\n\n/**\n * Type guard to check if a value is a hook entry with hooks array\n */\nexport function isHookEntry(h: unknown): h is HookEntry {\n return (\n typeof h === 'object' && h !== null && 'hooks' in h && Array.isArray((h as HookEntry).hooks)\n );\n}\n\n/**\n * Check if a hook entry contains a safeword hook (command contains '.safeword')\n */\nexport function isSafewordHook(h: unknown): boolean {\n if (!isHookEntry(h)) return false;\n return h.hooks.some(cmd => typeof cmd.command === 'string' && cmd.command.includes('.safeword'));\n}\n\n/**\n * Filter out safeword hooks from an array of hook entries\n */\nexport function filterOutSafewordHooks(hooks: unknown[]): unknown[] {\n return hooks.filter(h => !isSafewordHook(h));\n}\n","/**\n * SAFEWORD Schema - Single Source of Truth\n *\n * All files, directories, configurations, and packages managed by safeword\n * are defined here. Commands use this schema via the reconciliation engine.\n *\n * Adding a new file? Add it here and it will be handled by setup/upgrade/reset.\n */\n\nimport { VERSION } from './version.js';\nimport { type ProjectType } from './utils/project-detector.js';\nimport { AGENTS_MD_LINK, getPrettierConfig, getLintStagedConfig } from './templates/content.js';\nimport { getEslintConfig, SETTINGS_HOOKS, CURSOR_HOOKS } from './templates/config.js';\nimport { generateBoundariesConfig, detectArchitecture } from './utils/boundaries.js';\nimport { HUSKY_PRE_COMMIT_CONTENT, MCP_SERVERS } from './utils/install.js';\nimport { filterOutSafewordHooks } from './utils/hooks.js';\n\n// ============================================================================\n// Interfaces\n// ============================================================================\n\nexport interface ProjectContext {\n cwd: string;\n projectType: ProjectType;\n devDeps: Record<string, string>;\n isGitRepo: boolean;\n}\n\nexport interface FileDefinition {\n template?: string; // Path in templates/ dir\n content?: string | (() => string); // Static content or factory\n generator?: (ctx: ProjectContext) => string; // Dynamic generator needing context\n}\n\n// managedFiles: created if missing, updated only if content === current template output\nexport type ManagedFileDefinition = FileDefinition;\n\nexport interface JsonMergeDefinition {\n keys: string[]; // Dot-notation keys we manage\n conditionalKeys?: Record<string, string[]>; // Keys added based on project type\n merge: (existing: Record<string, unknown>, ctx: ProjectContext) => Record<string, unknown>;\n unmerge: (existing: Record<string, unknown>) => Record<string, unknown>;\n removeFileIfEmpty?: boolean; // Delete file if our keys were the only content\n}\n\nexport interface TextPatchDefinition {\n operation: 'prepend' | 'append';\n content: string;\n marker: string; // Used to detect if already applied & for removal\n createIfMissing: boolean;\n}\n\nexport interface SafewordSchema {\n version: string;\n ownedDirs: string[]; // Fully owned - create on setup, delete on reset\n sharedDirs: string[]; // We add to but don't own\n preservedDirs: string[]; // Created on setup, NOT deleted on reset (user data)\n ownedFiles: Record<string, FileDefinition>; // Overwrite on upgrade (if changed)\n managedFiles: Record<string, ManagedFileDefinition>; // Create if missing, update if safeword content\n jsonMerges: Record<string, JsonMergeDefinition>;\n textPatches: Record<string, TextPatchDefinition>;\n packages: {\n base: string[];\n conditional: Record<string, string[]>;\n };\n}\n\n// ============================================================================\n// SAFEWORD_SCHEMA - The Single Source of Truth\n// ============================================================================\n\nexport const SAFEWORD_SCHEMA: SafewordSchema = {\n version: VERSION,\n\n // Directories fully owned by safeword (created on setup, deleted on reset)\n ownedDirs: [\n '.safeword',\n '.safeword/hooks',\n '.safeword/hooks/cursor',\n '.safeword/lib',\n '.safeword/guides',\n '.safeword/templates',\n '.safeword/prompts',\n '.safeword/planning',\n '.safeword/planning/specs',\n '.safeword/planning/test-definitions',\n '.safeword/planning/design',\n '.safeword/planning/issues',\n '.safeword/scripts',\n '.husky',\n '.cursor',\n '.cursor/rules',\n '.cursor/commands',\n ],\n\n // Directories we add to but don't own (not deleted on reset)\n sharedDirs: ['.claude', '.claude/skills', '.claude/commands'],\n\n // Created on setup but NOT deleted on reset (preserves user data)\n preservedDirs: ['.safeword/learnings', '.safeword/tickets', '.safeword/tickets/completed'],\n\n // Files owned by safeword (overwritten on upgrade if content changed)\n ownedFiles: {\n // Core files\n '.safeword/SAFEWORD.md': { template: 'SAFEWORD.md' },\n '.safeword/version': { content: () => VERSION },\n '.safeword/eslint-boundaries.config.mjs': {\n generator: ctx => generateBoundariesConfig(detectArchitecture(ctx.cwd)),\n },\n\n // Hooks (7 files)\n '.safeword/hooks/session-verify-agents.sh': { template: 'hooks/session-verify-agents.sh' },\n '.safeword/hooks/session-version.sh': { template: 'hooks/session-version.sh' },\n '.safeword/hooks/session-lint-check.sh': { template: 'hooks/session-lint-check.sh' },\n '.safeword/hooks/prompt-timestamp.sh': { template: 'hooks/prompt-timestamp.sh' },\n '.safeword/hooks/prompt-questions.sh': { template: 'hooks/prompt-questions.sh' },\n '.safeword/hooks/post-tool-lint.sh': { template: 'hooks/post-tool-lint.sh' },\n '.safeword/hooks/stop-quality.sh': { template: 'hooks/stop-quality.sh' },\n\n // Lib (2 files)\n '.safeword/lib/common.sh': { template: 'lib/common.sh' },\n '.safeword/lib/jq-fallback.sh': { template: 'lib/jq-fallback.sh' },\n\n // Guides (13 files)\n '.safeword/guides/architecture-guide.md': { template: 'guides/architecture-guide.md' },\n '.safeword/guides/cli-reference.md': { template: 'guides/cli-reference.md' },\n '.safeword/guides/code-philosophy.md': { template: 'guides/code-philosophy.md' },\n '.safeword/guides/context-files-guide.md': { template: 'guides/context-files-guide.md' },\n '.safeword/guides/data-architecture-guide.md': {\n template: 'guides/data-architecture-guide.md',\n },\n '.safeword/guides/design-doc-guide.md': { template: 'guides/design-doc-guide.md' },\n '.safeword/guides/development-workflow.md': { template: 'guides/development-workflow.md' },\n '.safeword/guides/learning-extraction.md': { template: 'guides/learning-extraction.md' },\n '.safeword/guides/llm-guide.md': { template: 'guides/llm-guide.md' },\n '.safeword/guides/tdd-best-practices.md': { template: 'guides/tdd-best-practices.md' },\n '.safeword/guides/test-definitions-guide.md': { template: 'guides/test-definitions-guide.md' },\n '.safeword/guides/user-story-guide.md': { template: 'guides/user-story-guide.md' },\n '.safeword/guides/zombie-process-cleanup.md': { template: 'guides/zombie-process-cleanup.md' },\n\n // Templates (6 files)\n '.safeword/templates/architecture-template.md': {\n template: 'doc-templates/architecture-template.md',\n },\n '.safeword/templates/design-doc-template.md': {\n template: 'doc-templates/design-doc-template.md',\n },\n '.safeword/templates/task-spec-template.md': {\n template: 'doc-templates/task-spec-template.md',\n },\n '.safeword/templates/test-definitions-feature.md': {\n template: 'doc-templates/test-definitions-feature.md',\n },\n '.safeword/templates/ticket-template.md': { template: 'doc-templates/ticket-template.md' },\n '.safeword/templates/user-stories-template.md': {\n template: 'doc-templates/user-stories-template.md',\n },\n\n // Prompts (2 files)\n '.safeword/prompts/architecture.md': { template: 'prompts/architecture.md' },\n '.safeword/prompts/quality-review.md': { template: 'prompts/quality-review.md' },\n\n // Scripts (3 files)\n '.safeword/scripts/bisect-test-pollution.sh': { template: 'scripts/bisect-test-pollution.sh' },\n '.safeword/scripts/bisect-zombie-processes.sh': {\n template: 'scripts/bisect-zombie-processes.sh',\n },\n '.safeword/scripts/lint-md.sh': { template: 'scripts/lint-md.sh' },\n\n // Claude skills and commands (6 files)\n '.claude/skills/safeword-quality-reviewer/SKILL.md': {\n template: 'skills/safeword-quality-reviewer/SKILL.md',\n },\n '.claude/skills/safeword-systematic-debugger/SKILL.md': {\n template: 'skills/safeword-systematic-debugger/SKILL.md',\n },\n '.claude/skills/safeword-tdd-enforcer/SKILL.md': {\n template: 'skills/safeword-tdd-enforcer/SKILL.md',\n },\n '.claude/commands/architecture.md': { template: 'commands/architecture.md' },\n '.claude/commands/lint.md': { template: 'commands/lint.md' },\n '.claude/commands/quality-review.md': { template: 'commands/quality-review.md' },\n\n // Husky (1 file)\n '.husky/pre-commit': { content: HUSKY_PRE_COMMIT_CONTENT },\n\n // Cursor rules (1 file)\n '.cursor/rules/safeword-core.mdc': { template: 'cursor/rules/safeword-core.mdc' },\n\n // Cursor commands (3 files - same as Claude)\n '.cursor/commands/lint.md': { template: 'commands/lint.md' },\n '.cursor/commands/quality-review.md': { template: 'commands/quality-review.md' },\n '.cursor/commands/architecture.md': { template: 'commands/architecture.md' },\n\n // Cursor hooks adapters (2 files)\n '.safeword/hooks/cursor/after-file-edit.sh': { template: 'hooks/cursor/after-file-edit.sh' },\n '.safeword/hooks/cursor/stop.sh': { template: 'hooks/cursor/stop.sh' },\n },\n\n // Files created if missing, updated only if content matches current template\n managedFiles: {\n 'eslint.config.mjs': {\n generator: () => getEslintConfig({ boundaries: true }),\n },\n '.prettierrc': { generator: ctx => getPrettierConfig(ctx.projectType) },\n '.markdownlint-cli2.jsonc': { template: 'markdownlint-cli2.jsonc' },\n },\n\n // JSON files where we merge specific keys\n jsonMerges: {\n 'package.json': {\n keys: [\n 'scripts.lint',\n 'scripts.lint:md',\n 'scripts.format',\n 'scripts.format:check',\n 'scripts.knip',\n 'scripts.prepare',\n 'lint-staged',\n ],\n conditionalKeys: {\n publishableLibrary: ['scripts.publint'],\n shell: ['scripts.lint:sh'],\n },\n merge: (existing, ctx) => {\n const scripts = (existing.scripts as Record<string, string>) ?? {};\n const result = { ...existing };\n\n // Add scripts if not present\n if (!scripts.lint) scripts.lint = 'eslint .';\n if (!scripts['lint:md']) scripts['lint:md'] = 'markdownlint-cli2 \"**/*.md\" \"#node_modules\"';\n if (!scripts.format) scripts.format = 'prettier --write .';\n if (!scripts['format:check']) scripts['format:check'] = 'prettier --check .';\n if (!scripts.knip) scripts.knip = 'knip';\n if (!scripts.prepare) scripts.prepare = 'husky || true';\n\n // Conditional: publint for publishable libraries\n if (ctx.projectType.publishableLibrary && !scripts.publint) {\n scripts.publint = 'publint';\n }\n\n // Conditional: lint:sh for projects with shell scripts\n if (ctx.projectType.shell && !scripts['lint:sh']) {\n scripts['lint:sh'] = 'shellcheck **/*.sh';\n }\n\n result.scripts = scripts;\n\n // Add lint-staged config\n if (!existing['lint-staged']) {\n result['lint-staged'] = getLintStagedConfig(ctx.projectType);\n }\n\n return result;\n },\n unmerge: existing => {\n const result = { ...existing };\n const scripts = { ...(existing.scripts as Record<string, string>) };\n\n // Remove safeword-specific scripts but preserve lint/format (useful standalone)\n delete scripts['lint:md'];\n delete scripts['lint:sh'];\n delete scripts['format:check'];\n delete scripts.knip;\n delete scripts.prepare;\n delete scripts.publint;\n\n if (Object.keys(scripts).length > 0) {\n result.scripts = scripts;\n } else {\n delete result.scripts;\n }\n\n delete result['lint-staged'];\n\n return result;\n },\n },\n\n '.claude/settings.json': {\n keys: ['hooks'],\n merge: existing => {\n // Preserve non-safeword hooks while adding/updating safeword hooks\n const existingHooks = (existing.hooks as Record<string, unknown[]>) ?? {};\n const mergedHooks: Record<string, unknown[]> = { ...existingHooks };\n\n for (const [event, newHooks] of Object.entries(SETTINGS_HOOKS)) {\n const eventHooks = (mergedHooks[event] as unknown[]) ?? [];\n const nonSafewordHooks = filterOutSafewordHooks(eventHooks);\n mergedHooks[event] = [...nonSafewordHooks, ...newHooks];\n }\n\n return { ...existing, hooks: mergedHooks };\n },\n unmerge: existing => {\n // Remove only safeword hooks, preserve custom hooks\n const existingHooks = (existing.hooks as Record<string, unknown[]>) ?? {};\n const cleanedHooks: Record<string, unknown[]> = {};\n\n for (const [event, eventHooks] of Object.entries(existingHooks)) {\n const nonSafewordHooks = filterOutSafewordHooks(eventHooks as unknown[]);\n if (nonSafewordHooks.length > 0) {\n cleanedHooks[event] = nonSafewordHooks;\n }\n }\n\n const result = { ...existing };\n if (Object.keys(cleanedHooks).length > 0) {\n result.hooks = cleanedHooks;\n } else {\n delete result.hooks;\n }\n return result;\n },\n },\n\n '.mcp.json': {\n keys: ['mcpServers.context7', 'mcpServers.playwright'],\n removeFileIfEmpty: true,\n merge: existing => {\n const mcpServers = (existing.mcpServers as Record<string, unknown>) ?? {};\n return {\n ...existing,\n mcpServers: {\n ...mcpServers,\n context7: MCP_SERVERS.context7,\n playwright: MCP_SERVERS.playwright,\n },\n };\n },\n unmerge: existing => {\n const result = { ...existing };\n const mcpServers = { ...(existing.mcpServers as Record<string, unknown>) };\n\n delete mcpServers.context7;\n delete mcpServers.playwright;\n\n if (Object.keys(mcpServers).length > 0) {\n result.mcpServers = mcpServers;\n } else {\n delete result.mcpServers;\n }\n\n return result;\n },\n },\n\n '.cursor/mcp.json': {\n keys: ['mcpServers.context7', 'mcpServers.playwright'],\n removeFileIfEmpty: true,\n merge: existing => {\n const mcpServers = (existing.mcpServers as Record<string, unknown>) ?? {};\n return {\n ...existing,\n mcpServers: {\n ...mcpServers,\n context7: MCP_SERVERS.context7,\n playwright: MCP_SERVERS.playwright,\n },\n };\n },\n unmerge: existing => {\n const result = { ...existing };\n const mcpServers = { ...(existing.mcpServers as Record<string, unknown>) };\n\n delete mcpServers.context7;\n delete mcpServers.playwright;\n\n if (Object.keys(mcpServers).length > 0) {\n result.mcpServers = mcpServers;\n } else {\n delete result.mcpServers;\n }\n\n return result;\n },\n },\n\n '.cursor/hooks.json': {\n keys: ['version', 'hooks.afterFileEdit', 'hooks.stop'],\n removeFileIfEmpty: true,\n merge: existing => {\n const hooks = (existing.hooks as Record<string, unknown[]>) ?? {};\n return {\n ...existing,\n version: 1, // Required by Cursor\n hooks: {\n ...hooks,\n ...CURSOR_HOOKS,\n },\n };\n },\n unmerge: existing => {\n const result = { ...existing };\n const hooks = { ...(existing.hooks as Record<string, unknown[]>) };\n\n delete hooks.afterFileEdit;\n delete hooks.stop;\n\n if (Object.keys(hooks).length > 0) {\n result.hooks = hooks;\n } else {\n delete result.hooks;\n delete result.version;\n }\n\n return result;\n },\n },\n },\n\n // Text files where we patch specific content\n textPatches: {\n 'AGENTS.md': {\n operation: 'prepend',\n content: AGENTS_MD_LINK,\n marker: '@./.safeword/SAFEWORD.md',\n createIfMissing: true,\n },\n 'CLAUDE.md': {\n operation: 'prepend',\n content: AGENTS_MD_LINK,\n marker: '@./.safeword/SAFEWORD.md',\n createIfMissing: false, // Only patch if exists, don't create (AGENTS.md is primary)\n },\n },\n\n // NPM packages to install\n packages: {\n base: [\n 'eslint',\n 'prettier',\n '@eslint/js',\n 'eslint-plugin-import-x',\n 'eslint-import-resolver-typescript',\n 'eslint-plugin-sonarjs',\n 'eslint-plugin-unicorn',\n 'eslint-plugin-boundaries',\n 'eslint-plugin-playwright',\n '@microsoft/eslint-plugin-sdl',\n 'eslint-config-prettier',\n 'markdownlint-cli2',\n 'knip',\n 'husky',\n 'lint-staged',\n ],\n conditional: {\n typescript: ['typescript-eslint'],\n react: ['eslint-plugin-react', 'eslint-plugin-react-hooks', 'eslint-plugin-jsx-a11y'],\n nextjs: ['@next/eslint-plugin-next'],\n astro: ['eslint-plugin-astro', 'prettier-plugin-astro'],\n vue: ['eslint-plugin-vue'],\n svelte: ['eslint-plugin-svelte', 'prettier-plugin-svelte'],\n electron: ['@electron-toolkit/eslint-config'],\n vitest: ['@vitest/eslint-plugin'],\n tailwind: ['prettier-plugin-tailwindcss'],\n publishableLibrary: ['publint'],\n shell: ['shellcheck', 'prettier-plugin-sh'],\n },\n },\n};\n"],"mappings":"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","names":["readdirSync","join","join","readdirSync","join","readdirSync"]}
@@ -11,7 +11,7 @@ import {
11
11
  removeIfEmpty,
12
12
  writeFile,
13
13
  writeJson
14
- } from "./chunk-ZS3Z3Q37.js";
14
+ } from "./chunk-QPO3C3FP.js";
15
15
 
16
16
  // src/utils/output.ts
17
17
  function info(message) {
@@ -50,7 +50,7 @@ function createProjectContext(cwd) {
50
50
  const packageJson = readJson(join2(cwd, "package.json"));
51
51
  return {
52
52
  cwd,
53
- projectType: detectProjectType(packageJson ?? {}),
53
+ projectType: detectProjectType(packageJson ?? {}, cwd),
54
54
  devDeps: packageJson?.devDependencies ?? {},
55
55
  isGitRepo: isGitRepo(cwd)
56
56
  };
@@ -59,6 +59,61 @@ function createProjectContext(cwd) {
59
59
  // src/reconcile.ts
60
60
  import { join as join3 } from "path";
61
61
  var HUSKY_DIR = ".husky";
62
+ function shouldSkipForNonGit(path, isGitRepo2) {
63
+ return path.startsWith(HUSKY_DIR) && !isGitRepo2;
64
+ }
65
+ function planMissingDirs(dirs, cwd, isGitRepo2) {
66
+ const actions = [];
67
+ const created = [];
68
+ for (const dir of dirs) {
69
+ if (shouldSkipForNonGit(dir, isGitRepo2)) continue;
70
+ if (!exists(join3(cwd, dir))) {
71
+ actions.push({ type: "mkdir", path: dir });
72
+ created.push(dir);
73
+ }
74
+ }
75
+ return { actions, created };
76
+ }
77
+ function planTextPatches(patches, cwd) {
78
+ const actions = [];
79
+ for (const [filePath, def] of Object.entries(patches)) {
80
+ const content = readFileSafe(join3(cwd, filePath)) ?? "";
81
+ if (!content.includes(def.marker)) {
82
+ actions.push({ type: "text-patch", path: filePath, definition: def });
83
+ }
84
+ }
85
+ return actions;
86
+ }
87
+ function planExistingDirsRemoval(dirs, cwd) {
88
+ const actions = [];
89
+ const removed = [];
90
+ for (const dir of dirs) {
91
+ if (exists(join3(cwd, dir))) {
92
+ actions.push({ type: "rmdir", path: dir });
93
+ removed.push(dir);
94
+ }
95
+ }
96
+ return { actions, removed };
97
+ }
98
+ function planExistingFilesRemoval(files, cwd) {
99
+ const actions = [];
100
+ const removed = [];
101
+ for (const filePath of files) {
102
+ if (exists(join3(cwd, filePath))) {
103
+ actions.push({ type: "rm", path: filePath });
104
+ removed.push(filePath);
105
+ }
106
+ }
107
+ return { actions, removed };
108
+ }
109
+ function getClaudeParentDirForCleanup(filePath) {
110
+ if (!filePath.startsWith(".claude/")) return null;
111
+ const parentDir = filePath.slice(0, Math.max(0, filePath.lastIndexOf("/")));
112
+ if (!parentDir || parentDir === ".claude" || parentDir === ".claude/skills" || parentDir === ".claude/commands") {
113
+ return null;
114
+ }
115
+ return parentDir;
116
+ }
62
117
  async function reconcile(schema, mode, ctx, options) {
63
118
  const dryRun = options?.dryRun ?? false;
64
119
  const plan = computePlan(schema, mode, ctx);
@@ -86,30 +141,29 @@ async function reconcile(schema, mode, ctx, options) {
86
141
  }
87
142
  function computePlan(schema, mode, ctx) {
88
143
  switch (mode) {
89
- case "install":
144
+ case "install": {
90
145
  return computeInstallPlan(schema, ctx);
91
- case "upgrade":
146
+ }
147
+ case "upgrade": {
92
148
  return computeUpgradePlan(schema, ctx);
93
- case "uninstall":
149
+ }
150
+ case "uninstall": {
94
151
  return computeUninstallPlan(schema, ctx, false);
95
- case "uninstall-full":
152
+ }
153
+ case "uninstall-full": {
96
154
  return computeUninstallPlan(schema, ctx, true);
155
+ }
97
156
  }
98
157
  }
99
158
  function computeInstallPlan(schema, ctx) {
100
159
  const actions = [];
101
160
  const wouldCreate = [];
102
161
  const allDirs = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];
103
- for (const dir of allDirs) {
104
- if (dir.startsWith(HUSKY_DIR) && !ctx.isGitRepo) continue;
105
- const fullPath = join3(ctx.cwd, dir);
106
- if (!exists(fullPath)) {
107
- actions.push({ type: "mkdir", path: dir });
108
- wouldCreate.push(dir);
109
- }
110
- }
162
+ const missingDirs = planMissingDirs(allDirs, ctx.cwd, ctx.isGitRepo);
163
+ actions.push(...missingDirs.actions);
164
+ wouldCreate.push(...missingDirs.created);
111
165
  for (const [filePath, def] of Object.entries(schema.ownedFiles)) {
112
- if (filePath.startsWith(HUSKY_DIR) && !ctx.isGitRepo) continue;
166
+ if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
113
167
  const content = resolveFileContent(def, ctx);
114
168
  actions.push({ type: "write", path: filePath, content });
115
169
  wouldCreate.push(filePath);
@@ -122,7 +176,7 @@ function computeInstallPlan(schema, ctx) {
122
176
  wouldCreate.push(filePath);
123
177
  }
124
178
  }
125
- const chmodPaths = [".safeword/hooks", ".safeword/lib"];
179
+ const chmodPaths = [".safeword/hooks", ".safeword/hooks/cursor", ".safeword/lib"];
126
180
  if (ctx.isGitRepo) chmodPaths.push(HUSKY_DIR);
127
181
  actions.push({ type: "chmod", paths: chmodPaths });
128
182
  for (const [filePath, def] of Object.entries(schema.jsonMerges)) {
@@ -154,25 +208,19 @@ function computeUpgradePlan(schema, ctx) {
154
208
  const wouldCreate = [];
155
209
  const wouldUpdate = [];
156
210
  const allDirs = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];
157
- for (const dir of allDirs) {
158
- if (dir.startsWith(HUSKY_DIR) && !ctx.isGitRepo) continue;
159
- const fullPath = join3(ctx.cwd, dir);
160
- if (!exists(fullPath)) {
161
- actions.push({ type: "mkdir", path: dir });
162
- wouldCreate.push(dir);
163
- }
164
- }
211
+ const missingDirs = planMissingDirs(allDirs, ctx.cwd, ctx.isGitRepo);
212
+ actions.push(...missingDirs.actions);
213
+ wouldCreate.push(...missingDirs.created);
165
214
  for (const [filePath, def] of Object.entries(schema.ownedFiles)) {
166
- if (filePath.startsWith(HUSKY_DIR) && !ctx.isGitRepo) continue;
215
+ if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
167
216
  const fullPath = join3(ctx.cwd, filePath);
168
217
  const newContent = resolveFileContent(def, ctx);
169
- if (fileNeedsUpdate(fullPath, newContent)) {
170
- actions.push({ type: "write", path: filePath, content: newContent });
171
- if (exists(fullPath)) {
172
- wouldUpdate.push(filePath);
173
- } else {
174
- wouldCreate.push(filePath);
175
- }
218
+ if (!fileNeedsUpdate(fullPath, newContent)) continue;
219
+ actions.push({ type: "write", path: filePath, content: newContent });
220
+ if (exists(fullPath)) {
221
+ wouldUpdate.push(filePath);
222
+ } else {
223
+ wouldCreate.push(filePath);
176
224
  }
177
225
  }
178
226
  for (const [filePath, def] of Object.entries(schema.managedFiles)) {
@@ -181,26 +229,15 @@ function computeUpgradePlan(schema, ctx) {
181
229
  if (!exists(fullPath)) {
182
230
  actions.push({ type: "write", path: filePath, content: newContent });
183
231
  wouldCreate.push(filePath);
184
- } else {
185
- const currentContent = readFileSafe(fullPath);
186
- if (currentContent?.trim() === newContent.trim()) {
187
- } else {
188
- }
189
232
  }
190
233
  }
191
- const chmodPaths = [".safeword/hooks", ".safeword/lib"];
192
- if (ctx.isGitRepo) chmodPaths.push(HUSKY_DIR);
193
- actions.push({ type: "chmod", paths: chmodPaths });
234
+ const chmodPathsUpgrade = [".safeword/hooks", ".safeword/hooks/cursor", ".safeword/lib"];
235
+ if (ctx.isGitRepo) chmodPathsUpgrade.push(HUSKY_DIR);
236
+ actions.push({ type: "chmod", paths: chmodPathsUpgrade });
194
237
  for (const [filePath, def] of Object.entries(schema.jsonMerges)) {
195
238
  actions.push({ type: "json-merge", path: filePath, definition: def });
196
239
  }
197
- for (const [filePath, def] of Object.entries(schema.textPatches)) {
198
- const fullPath = join3(ctx.cwd, filePath);
199
- const content = readFileSafe(fullPath) ?? "";
200
- if (!content.includes(def.marker)) {
201
- actions.push({ type: "text-patch", path: filePath, definition: def });
202
- }
203
- }
240
+ actions.push(...planTextPatches(schema.textPatches, ctx.cwd));
204
241
  const packagesToInstall = computePackagesToInstall(
205
242
  schema,
206
243
  ctx.projectType,
@@ -219,27 +256,17 @@ function computeUpgradePlan(schema, ctx) {
219
256
  function computeUninstallPlan(schema, ctx, full) {
220
257
  const actions = [];
221
258
  const wouldRemove = [];
259
+ const ownedFiles = planExistingFilesRemoval(Object.keys(schema.ownedFiles), ctx.cwd);
260
+ actions.push(...ownedFiles.actions);
261
+ wouldRemove.push(...ownedFiles.removed);
222
262
  const dirsToCleanup = /* @__PURE__ */ new Set();
223
- for (const filePath of Object.keys(schema.ownedFiles)) {
224
- const fullPath = join3(ctx.cwd, filePath);
225
- if (exists(fullPath)) {
226
- actions.push({ type: "rm", path: filePath });
227
- wouldRemove.push(filePath);
228
- if (filePath.startsWith(".claude/")) {
229
- const parentDir = filePath.substring(0, filePath.lastIndexOf("/"));
230
- if (parentDir && parentDir !== ".claude" && parentDir !== ".claude/skills" && parentDir !== ".claude/commands") {
231
- dirsToCleanup.add(parentDir);
232
- }
233
- }
234
- }
235
- }
236
- for (const dir of dirsToCleanup) {
237
- const fullPath = join3(ctx.cwd, dir);
238
- if (exists(fullPath)) {
239
- actions.push({ type: "rmdir", path: dir });
240
- wouldRemove.push(dir);
241
- }
263
+ for (const filePath of ownedFiles.removed) {
264
+ const parentDir = getClaudeParentDirForCleanup(filePath);
265
+ if (parentDir) dirsToCleanup.add(parentDir);
242
266
  }
267
+ const cleanupDirs = planExistingDirsRemoval([...dirsToCleanup], ctx.cwd);
268
+ actions.push(...cleanupDirs.actions);
269
+ wouldRemove.push(...cleanupDirs.removed);
243
270
  for (const [filePath, def] of Object.entries(schema.jsonMerges)) {
244
271
  actions.push({ type: "json-unmerge", path: filePath, definition: def });
245
272
  }
@@ -252,30 +279,16 @@ function computeUninstallPlan(schema, ctx, full) {
252
279
  }
253
280
  }
254
281
  }
255
- const preservedDirsToRemove = [...schema.preservedDirs].reverse();
256
- for (const dir of preservedDirsToRemove) {
257
- const fullPath = join3(ctx.cwd, dir);
258
- if (exists(fullPath)) {
259
- actions.push({ type: "rmdir", path: dir });
260
- wouldRemove.push(dir);
261
- }
262
- }
263
- const dirsToRemove = [...schema.ownedDirs].reverse();
264
- for (const dir of dirsToRemove) {
265
- const fullPath = join3(ctx.cwd, dir);
266
- if (exists(fullPath)) {
267
- actions.push({ type: "rmdir", path: dir });
268
- wouldRemove.push(dir);
269
- }
270
- }
282
+ const preserved = planExistingDirsRemoval(schema.preservedDirs.toReversed(), ctx.cwd);
283
+ actions.push(...preserved.actions);
284
+ wouldRemove.push(...preserved.removed);
285
+ const owned = planExistingDirsRemoval(schema.ownedDirs.toReversed(), ctx.cwd);
286
+ actions.push(...owned.actions);
287
+ wouldRemove.push(...owned.removed);
271
288
  if (full) {
272
- for (const filePath of Object.keys(schema.managedFiles)) {
273
- const fullPath = join3(ctx.cwd, filePath);
274
- if (exists(fullPath)) {
275
- actions.push({ type: "rm", path: filePath });
276
- wouldRemove.push(filePath);
277
- }
278
- }
289
+ const managed = planExistingFilesRemoval(Object.keys(schema.managedFiles), ctx.cwd);
290
+ actions.push(...managed.actions);
291
+ wouldRemove.push(...managed.removed);
279
292
  }
280
293
  const packagesToRemove = full ? computePackagesToRemove(schema, ctx.projectType, ctx.devDeps) : [];
281
294
  return {
@@ -291,66 +304,64 @@ function executePlan(plan, ctx) {
291
304
  const created = [];
292
305
  const updated = [];
293
306
  const removed = [];
307
+ const result = { created, updated, removed };
294
308
  for (const action of plan.actions) {
295
- switch (action.type) {
296
- case "mkdir": {
297
- const fullPath = join3(ctx.cwd, action.path);
298
- ensureDir(fullPath);
299
- created.push(action.path);
300
- break;
301
- }
302
- case "rmdir": {
303
- const fullPath = join3(ctx.cwd, action.path);
304
- if (removeIfEmpty(fullPath)) {
305
- removed.push(action.path);
306
- }
307
- break;
308
- }
309
- case "write": {
310
- const fullPath = join3(ctx.cwd, action.path);
311
- const existed = exists(fullPath);
312
- writeFile(fullPath, action.content);
313
- if (existed) {
314
- updated.push(action.path);
315
- } else {
316
- created.push(action.path);
317
- }
318
- break;
319
- }
320
- case "rm": {
321
- const fullPath = join3(ctx.cwd, action.path);
322
- remove(fullPath);
323
- removed.push(action.path);
324
- break;
325
- }
326
- case "chmod": {
327
- for (const path of action.paths) {
328
- const fullPath = join3(ctx.cwd, path);
329
- if (exists(fullPath)) {
330
- makeScriptsExecutable(fullPath);
331
- }
332
- }
333
- break;
334
- }
335
- case "json-merge": {
336
- executeJsonMerge(ctx.cwd, action.path, action.definition, ctx);
337
- break;
338
- }
339
- case "json-unmerge": {
340
- executeJsonUnmerge(ctx.cwd, action.path, action.definition);
341
- break;
342
- }
343
- case "text-patch": {
344
- executeTextPatch(ctx.cwd, action.path, action.definition);
345
- break;
309
+ executeAction(action, ctx, result);
310
+ }
311
+ return result;
312
+ }
313
+ function executeAction(action, ctx, result) {
314
+ switch (action.type) {
315
+ case "mkdir": {
316
+ ensureDir(join3(ctx.cwd, action.path));
317
+ result.created.push(action.path);
318
+ break;
319
+ }
320
+ case "rmdir": {
321
+ if (removeIfEmpty(join3(ctx.cwd, action.path))) {
322
+ result.removed.push(action.path);
346
323
  }
347
- case "text-unpatch": {
348
- executeTextUnpatch(ctx.cwd, action.path, action.definition);
349
- break;
324
+ break;
325
+ }
326
+ case "write": {
327
+ executeWrite(ctx.cwd, action.path, action.content, result);
328
+ break;
329
+ }
330
+ case "rm": {
331
+ remove(join3(ctx.cwd, action.path));
332
+ result.removed.push(action.path);
333
+ break;
334
+ }
335
+ case "chmod": {
336
+ for (const path of action.paths) {
337
+ const fullPath = join3(ctx.cwd, path);
338
+ if (exists(fullPath)) makeScriptsExecutable(fullPath);
350
339
  }
340
+ break;
341
+ }
342
+ case "json-merge": {
343
+ executeJsonMerge(ctx.cwd, action.path, action.definition, ctx);
344
+ break;
345
+ }
346
+ case "json-unmerge": {
347
+ executeJsonUnmerge(ctx.cwd, action.path, action.definition);
348
+ break;
349
+ }
350
+ case "text-patch": {
351
+ executeTextPatch(ctx.cwd, action.path, action.definition);
352
+ break;
353
+ }
354
+ case "text-unpatch": {
355
+ executeTextUnpatch(ctx.cwd, action.path, action.definition);
356
+ break;
351
357
  }
352
358
  }
353
- return { created, updated, removed };
359
+ }
360
+ function executeWrite(cwd, path, content, result) {
361
+ const fullPath = join3(cwd, path);
362
+ const existed = exists(fullPath);
363
+ writeFile(fullPath, content);
364
+ (existed ? result.updated : result.created).push(path);
354
365
  }
355
366
  function resolveFileContent(def, ctx) {
356
367
  if (def.template) {
@@ -370,11 +381,11 @@ function fileNeedsUpdate(installedPath, newContent) {
370
381
  const currentContent = readFileSafe(installedPath);
371
382
  return currentContent?.trim() !== newContent.trim();
372
383
  }
373
- var GIT_ONLY_PACKAGES = ["husky", "lint-staged"];
384
+ var GIT_ONLY_PACKAGES = /* @__PURE__ */ new Set(["husky", "lint-staged"]);
374
385
  function computePackagesToInstall(schema, projectType, installedDevDeps, isGitRepo2 = true) {
375
386
  let needed = [...schema.packages.base];
376
387
  if (!isGitRepo2) {
377
- needed = needed.filter((pkg) => !GIT_ONLY_PACKAGES.includes(pkg));
388
+ needed = needed.filter((pkg) => !GIT_ONLY_PACKAGES.has(pkg));
378
389
  }
379
390
  for (const [key, deps] of Object.entries(schema.packages.conditional)) {
380
391
  if (projectType[key]) {
@@ -419,11 +430,7 @@ function executeTextPatch(cwd, path, def) {
419
430
  const fullPath = join3(cwd, path);
420
431
  let content = readFileSafe(fullPath) ?? "";
421
432
  if (content.includes(def.marker)) return;
422
- if (def.operation === "prepend") {
423
- content = def.content + content;
424
- } else {
425
- content = content + def.content;
426
- }
433
+ content = def.operation === "prepend" ? def.content + content : content + def.content;
427
434
  writeFile(fullPath, content);
428
435
  }
429
436
  function executeTextUnpatch(cwd, path, def) {
@@ -451,4 +458,4 @@ export {
451
458
  createProjectContext,
452
459
  reconcile
453
460
  };
454
- //# sourceMappingURL=chunk-LNSEDZIW.js.map
461
+ //# sourceMappingURL=chunk-YMLVQC4V.js.map