safeword 0.12.2 → 0.13.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 (40) hide show
  1. package/dist/{check-3X75X2JL.js → check-X7NR4WAM.js} +5 -4
  2. package/dist/check-X7NR4WAM.js.map +1 -0
  3. package/dist/{chunk-O4LAXZK3.js → chunk-XLOXGDJG.js} +103 -41
  4. package/dist/chunk-XLOXGDJG.js.map +1 -0
  5. package/dist/cli.js +5 -5
  6. package/dist/{diff-6HFT7BLG.js → diff-3USPMFT2.js} +2 -2
  7. package/dist/{reset-XFXLQXOC.js → reset-CM3BNT5S.js} +2 -2
  8. package/dist/{setup-C6NF3YJ5.js → setup-4PQV6EV2.js} +4 -17
  9. package/dist/setup-4PQV6EV2.js.map +1 -0
  10. package/dist/{upgrade-C2I22FAB.js → upgrade-BIMRJENC.js} +12 -6
  11. package/dist/upgrade-BIMRJENC.js.map +1 -0
  12. package/package.json +2 -2
  13. package/templates/hooks/cursor/after-file-edit.ts +47 -0
  14. package/templates/hooks/cursor/stop.ts +73 -0
  15. package/templates/hooks/lib/lint.ts +49 -0
  16. package/templates/hooks/lib/quality.ts +30 -0
  17. package/templates/hooks/post-tool-lint.ts +33 -0
  18. package/templates/hooks/prompt-questions.ts +32 -0
  19. package/templates/hooks/prompt-timestamp.ts +30 -0
  20. package/templates/hooks/session-lint-check.ts +62 -0
  21. package/templates/hooks/session-verify-agents.ts +32 -0
  22. package/templates/hooks/session-version.ts +18 -0
  23. package/templates/hooks/stop-quality.ts +171 -0
  24. package/dist/check-3X75X2JL.js.map +0 -1
  25. package/dist/chunk-O4LAXZK3.js.map +0 -1
  26. package/dist/setup-C6NF3YJ5.js.map +0 -1
  27. package/dist/upgrade-C2I22FAB.js.map +0 -1
  28. package/templates/hooks/cursor/after-file-edit.sh +0 -58
  29. package/templates/hooks/cursor/stop.sh +0 -50
  30. package/templates/hooks/post-tool-lint.sh +0 -51
  31. package/templates/hooks/prompt-questions.sh +0 -27
  32. package/templates/hooks/prompt-timestamp.sh +0 -13
  33. package/templates/hooks/session-lint-check.sh +0 -42
  34. package/templates/hooks/session-verify-agents.sh +0 -31
  35. package/templates/hooks/session-version.sh +0 -17
  36. package/templates/hooks/stop-quality.sh +0 -91
  37. package/templates/lib/common.sh +0 -26
  38. package/templates/lib/jq-fallback.sh +0 -20
  39. /package/dist/{diff-6HFT7BLG.js.map → diff-3USPMFT2.js.map} +0 -0
  40. /package/dist/{reset-XFXLQXOC.js.map → reset-CM3BNT5S.js.map} +0 -0
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Auto Quality Review Stop Hook
3
+ // Triggers quality review when changes are proposed or made
4
+ // Looks for {"proposedChanges": ..., "madeChanges": ..., "askedQuestion": ...} JSON blob
5
+
6
+ import { existsSync } from 'node:fs';
7
+
8
+ import { QUALITY_REVIEW_MESSAGE, QUESTION_RESEARCH_MESSAGE } from './lib/quality.ts';
9
+
10
+ interface HookInput {
11
+ transcript_path?: string;
12
+ }
13
+
14
+ interface TranscriptMessage {
15
+ role: string;
16
+ message?: {
17
+ content?: { type: string; text?: string }[];
18
+ };
19
+ }
20
+
21
+ interface ResponseSummary {
22
+ proposedChanges: boolean;
23
+ madeChanges: boolean;
24
+ askedQuestion: boolean;
25
+ }
26
+
27
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
28
+ const safewordDir = `${projectDir}/.safeword`;
29
+
30
+ // Not a safeword project, skip silently
31
+ if (!existsSync(safewordDir)) {
32
+ process.exit(0);
33
+ }
34
+
35
+ // Read hook input from stdin
36
+ let input: HookInput;
37
+ try {
38
+ input = await Bun.stdin.json();
39
+ } catch (error) {
40
+ if (process.env.DEBUG) console.error('[stop-quality] stdin parse error:', error);
41
+ process.exit(0);
42
+ }
43
+
44
+ const transcriptPath = input.transcript_path;
45
+ if (!transcriptPath) {
46
+ process.exit(0);
47
+ }
48
+
49
+ const transcriptFile = Bun.file(transcriptPath);
50
+ if (!(await transcriptFile.exists())) {
51
+ process.exit(0);
52
+ }
53
+
54
+ // Read transcript (JSONL format)
55
+ const transcriptText = await transcriptFile.text();
56
+ const lines = transcriptText.trim().split('\n');
57
+
58
+ // Find last assistant message with text content (search backwards)
59
+ let messageText: string | null = null;
60
+ for (let i = lines.length - 1; i >= 0; i--) {
61
+ try {
62
+ const message: TranscriptMessage = JSON.parse(lines[i]);
63
+ if (message.role === 'assistant' && message.message?.content) {
64
+ const textContent = message.message.content.find(c => c.type === 'text' && c.text);
65
+ if (textContent?.text) {
66
+ messageText = textContent.text;
67
+ break;
68
+ }
69
+ }
70
+ } catch {
71
+ // Skip invalid JSON lines
72
+ }
73
+ }
74
+
75
+ if (!messageText) {
76
+ process.exit(0);
77
+ }
78
+
79
+ /**
80
+ * Extract all JSON objects from text using brace-balanced scanning.
81
+ * Handles nested objects and braces inside strings correctly.
82
+ */
83
+ function extractJsonObjects(text: string): string[] {
84
+ const results: string[] = [];
85
+ let i = 0;
86
+
87
+ while (i < text.length) {
88
+ if (text[i] === '{') {
89
+ let depth = 0;
90
+ let inString = false;
91
+ let escape = false;
92
+ const start = i;
93
+
94
+ for (let j = i; j < text.length; j++) {
95
+ const char = text[j];
96
+
97
+ if (escape) {
98
+ escape = false;
99
+ continue;
100
+ }
101
+
102
+ if (char === '\\' && inString) {
103
+ escape = true;
104
+ continue;
105
+ }
106
+
107
+ if (char === '"') {
108
+ inString = !inString;
109
+ continue;
110
+ }
111
+
112
+ if (!inString) {
113
+ if (char === '{') depth++;
114
+ if (char === '}') depth--;
115
+
116
+ if (depth === 0) {
117
+ results.push(text.slice(start, j + 1));
118
+ i = j;
119
+ break;
120
+ }
121
+ }
122
+ }
123
+ }
124
+ i++;
125
+ }
126
+
127
+ return results;
128
+ }
129
+
130
+ const candidates = extractJsonObjects(messageText);
131
+
132
+ function isValidSummary(object: unknown): object is ResponseSummary {
133
+ return (
134
+ typeof object === 'object' &&
135
+ object !== null &&
136
+ typeof (object as ResponseSummary).proposedChanges === 'boolean' &&
137
+ typeof (object as ResponseSummary).madeChanges === 'boolean' &&
138
+ typeof (object as ResponseSummary).askedQuestion === 'boolean'
139
+ );
140
+ }
141
+
142
+ let summary: ResponseSummary | null = null;
143
+ for (const candidate of candidates) {
144
+ try {
145
+ const parsed = JSON.parse(candidate);
146
+ if (isValidSummary(parsed)) {
147
+ summary = parsed;
148
+ }
149
+ } catch {
150
+ // Not valid JSON, skip
151
+ }
152
+ }
153
+
154
+ if (!summary) {
155
+ // No valid JSON blob found - remind about required format
156
+ console.error('SAFEWORD: Response missing required JSON summary. Add to end of response:');
157
+ console.error('{"proposedChanges": boolean, "madeChanges": boolean, "askedQuestion": boolean}');
158
+ process.exit(2);
159
+ }
160
+
161
+ // If either proposed or made changes, trigger quality review
162
+ if (summary.proposedChanges || summary.madeChanges) {
163
+ console.error(QUALITY_REVIEW_MESSAGE);
164
+ process.exit(2);
165
+ }
166
+
167
+ // If only asked a question (no changes), trigger research prompt
168
+ if (summary.askedQuestion) {
169
+ console.error(QUESTION_RESEARCH_MESSAGE);
170
+ process.exit(2);
171
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/check.ts"],"sourcesContent":["/**\n * Check command - Verify project health and configuration\n *\n * Uses reconcile() with dryRun to detect missing files and configuration issues.\n */\n\nimport nodePath from 'node:path';\n\nimport { reconcile } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { exists, readFileSafe } from '../utils/fs.js';\nimport { header, info, keyValue, success, warn } from '../utils/output.js';\nimport { isNewerVersion } from '../utils/version.js';\nimport { VERSION } from '../version.js';\n\nexport interface CheckOptions {\n offline?: boolean;\n}\n\n/**\n * Check for missing files from write actions\n * @param cwd\n * @param actions\n */\nfunction findMissingFiles(cwd: string, actions: { type: string; path: string }[]): string[] {\n const issues: string[] = [];\n for (const action of actions) {\n if (action.type === 'write' && !exists(nodePath.join(cwd, action.path))) {\n issues.push(`Missing: ${action.path}`);\n }\n }\n return issues;\n}\n\n/**\n * Check for missing text patch markers\n * @param cwd\n * @param actions\n */\nfunction findMissingPatches(\n cwd: string,\n actions: { type: string; path: string; definition?: { marker: string } }[],\n): string[] {\n const issues: string[] = [];\n for (const action of actions) {\n if (action.type !== 'text-patch') continue;\n\n const fullPath = nodePath.join(cwd, action.path);\n if (exists(fullPath)) {\n const content = readFileSafe(fullPath) ?? '';\n if (action.definition && !content.includes(action.definition.marker)) {\n issues.push(`${action.path} missing safeword link`);\n }\n } else {\n issues.push(`${action.path} file missing`);\n }\n }\n return issues;\n}\n\ninterface HealthStatus {\n configured: boolean;\n projectVersion: string | undefined;\n cliVersion: string;\n updateAvailable: boolean;\n latestVersion: string | undefined;\n issues: string[];\n missingPackages: string[];\n}\n\n/**\n * Check for latest version from npm (with timeout)\n * @param timeout\n */\nasync function checkLatestVersion(timeout = 3000): Promise<string | undefined> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort();\n }, timeout);\n\n const response = await fetch('https://registry.npmjs.org/safeword/latest', {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) return undefined;\n\n const data = (await response.json()) as { version?: string };\n return data.version ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check project configuration health using reconcile dryRun\n * @param cwd\n */\nasync function checkHealth(cwd: string): Promise<HealthStatus> {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDirectory)) {\n return {\n configured: false,\n projectVersion: undefined,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: undefined,\n issues: [],\n missingPackages: [],\n };\n }\n\n // Read project version\n const versionPath = nodePath.join(safewordDirectory, 'version');\n const projectVersion = readFileSafe(versionPath)?.trim() ?? undefined;\n\n // Use reconcile with dryRun to detect issues\n const ctx = createProjectContext(cwd);\n const result = await reconcile(SAFEWORD_SCHEMA, 'upgrade', ctx, { dryRun: true });\n\n // Collect issues from write actions and text patches\n // Filter out chmod (paths[] instead of path) and json-merge/unmerge (incompatible definition)\n const actionsWithPath = result.actions.filter(\n (\n a,\n ): a is Exclude<\n (typeof result.actions)[number],\n { type: 'chmod' } | { type: 'json-merge' } | { type: 'json-unmerge' }\n > => a.type !== 'chmod' && a.type !== 'json-merge' && a.type !== 'json-unmerge',\n );\n const issues: string[] = [\n ...findMissingFiles(cwd, actionsWithPath),\n ...findMissingPatches(cwd, actionsWithPath),\n ];\n\n // Check for missing .claude/settings.json\n if (!exists(nodePath.join(cwd, '.claude', 'settings.json'))) {\n issues.push('Missing: .claude/settings.json');\n }\n\n return {\n configured: true,\n projectVersion,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: undefined,\n issues,\n missingPackages: result.packagesToInstall,\n };\n}\n\n/**\n * Check for CLI updates and report status\n * @param health\n */\nasync function reportUpdateStatus(health: HealthStatus): Promise<void> {\n info('\\nChecking for updates...');\n const latestVersion = await checkLatestVersion();\n\n if (!latestVersion) {\n warn(\"Couldn't check for updates (offline?)\");\n return;\n }\n\n health.latestVersion = latestVersion;\n health.updateAvailable = isNewerVersion(health.cliVersion, latestVersion);\n\n if (health.updateAvailable) {\n warn(`Update available: v${latestVersion}`);\n info('Run `npm install -g safeword` to upgrade');\n } else {\n success('CLI is up to date');\n }\n}\n\n/**\n * Compare project version vs CLI version and report\n * @param health\n */\nfunction reportVersionMismatch(health: HealthStatus): void {\n if (!health.projectVersion) return;\n\n if (isNewerVersion(health.cliVersion, health.projectVersion)) {\n warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);\n info('Consider upgrading the CLI');\n } else if (isNewerVersion(health.projectVersion, health.cliVersion)) {\n info(`\\nUpgrade available for project config`);\n info(\n `Run \\`safeword upgrade\\` to update from v${health.projectVersion} to v${health.cliVersion}`,\n );\n }\n}\n\n/**\n * Report issues or success\n * @param health\n */\nfunction reportHealthSummary(health: HealthStatus): void {\n if (health.issues.length > 0) {\n header('Issues Found');\n for (const issue of health.issues) {\n warn(issue);\n }\n info('\\nRun `safeword upgrade` to repair configuration');\n return;\n }\n\n if (health.missingPackages.length > 0) {\n header('Missing Packages');\n info(`${health.missingPackages.length} linting packages not installed`);\n info('Run `safeword upgrade` to install missing packages');\n return;\n }\n\n success('\\nConfiguration is healthy');\n}\n\n/**\n *\n * @param options\n */\nexport async function check(options: CheckOptions): Promise<void> {\n const cwd = process.cwd();\n\n header('Safeword Health Check');\n\n const health = await checkHealth(cwd);\n\n // Not configured\n if (!health.configured) {\n info('Not configured. Run `safeword setup` to initialize.');\n return;\n }\n\n // Show versions\n keyValue('Safeword CLI', `v${health.cliVersion}`);\n keyValue('Project config', health.projectVersion ? `v${health.projectVersion}` : 'unknown');\n\n // Check for updates (unless offline)\n if (options.offline) {\n info('\\nSkipped update check (offline mode)');\n } else {\n await reportUpdateStatus(health);\n }\n\n reportVersionMismatch(health);\n reportHealthSummary(health);\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/reconcile.ts","../src/templates/config.ts","../src/templates/content.ts","../src/utils/hooks.ts","../src/utils/install.ts","../src/schema.ts","../src/utils/git.ts","../src/utils/context.ts","../src/utils/project-detector.ts"],"sourcesContent":["/**\n * Reconciliation Engine\n *\n * Computes and executes plans based on SAFEWORD_SCHEMA and project state.\n * This is the single source of truth for all file/dir/config operations.\n */\n\nimport nodePath from 'node:path';\n\nimport type {\n FileDefinition,\n JsonMergeDefinition,\n ProjectContext,\n SafewordSchema,\n TextPatchDefinition,\n} from './schema.js';\nimport {\n ensureDirectory,\n exists,\n getTemplatesDirectory,\n makeScriptsExecutable,\n readFile,\n readFileSafe,\n readJson,\n remove,\n removeIfEmpty,\n writeFile,\n writeJson,\n} from './utils/fs.js';\nimport type { ProjectType } from './utils/project-detector.js';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst HUSKY_DIR = '.husky';\n\n/**\n * Prettier-related packages that should be skipped for projects with existing formatter.\n */\nconst PRETTIER_PACKAGES = new Set([\n 'prettier',\n 'prettier-plugin-astro',\n 'prettier-plugin-tailwindcss',\n 'prettier-plugin-sh',\n]);\n\n/**\n * Get conditional packages based on project type.\n * Handles the \"standard\" key and prettier filtering for existing formatters.\n */\nfunction getConditionalPackages(\n conditionalPackages: Record<string, string[]>,\n projectType: ProjectType,\n): string[] {\n const packages: string[] = [];\n\n for (const [key, deps] of Object.entries(conditionalPackages)) {\n // \"standard\" means !existingFormatter - only for projects without existing formatter\n if (key === 'standard') {\n if (!projectType.existingFormatter) {\n packages.push(...deps);\n }\n continue;\n }\n\n // Check if this condition is met\n if (projectType[key as keyof ProjectType]) {\n // For projects with existing formatter, skip prettier-related packages\n if (projectType.existingFormatter) {\n packages.push(...deps.filter(pkg => !PRETTIER_PACKAGES.has(pkg)));\n } else {\n packages.push(...deps);\n }\n }\n }\n\n return packages;\n}\n\n/**\n * Check if path should be skipped in non-git repos (husky files)\n * @param path\n * @param isGitRepo\n */\nfunction shouldSkipForNonGit(path: string, isGitRepo: boolean): boolean {\n return path.startsWith(HUSKY_DIR) && !isGitRepo;\n}\n\n/**\n * Plan mkdir actions for directories that don't exist\n * @param dirs\n * @param cwd\n * @param isGitRepo\n */\nfunction planMissingDirectories(\n directories: string[],\n cwd: string,\n isGitRepo: boolean,\n): { actions: Action[]; created: string[] } {\n const actions: Action[] = [];\n const created: string[] = [];\n for (const dir of directories) {\n if (shouldSkipForNonGit(dir, isGitRepo)) continue;\n if (!exists(nodePath.join(cwd, dir))) {\n actions.push({ type: 'mkdir', path: dir });\n created.push(dir);\n }\n }\n return { actions, created };\n}\n\n/**\n * Plan text-patch actions for files missing the marker\n * @param patches\n * @param cwd\n * @param isGitRepo\n */\nfunction planTextPatches(\n patches: Record<string, TextPatchDefinition>,\n cwd: string,\n isGitRepo: boolean,\n): Action[] {\n const actions: Action[] = [];\n for (const [filePath, definition] of Object.entries(patches)) {\n if (shouldSkipForNonGit(filePath, isGitRepo)) continue;\n const content = readFileSafe(nodePath.join(cwd, filePath)) ?? '';\n if (!content.includes(definition.marker)) {\n actions.push({ type: 'text-patch', path: filePath, definition });\n }\n }\n return actions;\n}\n\nfunction planOwnedFileWrites(\n files: Record<string, FileDefinition>,\n ctx: ProjectContext,\n): { actions: Action[]; created: string[] } {\n const actions: Action[] = [];\n const created: string[] = [];\n for (const [filePath, definition] of Object.entries(files)) {\n if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;\n const content = resolveFileContent(definition, ctx);\n actions.push({ type: 'write', path: filePath, content });\n created.push(filePath);\n }\n return { actions, created };\n}\n\nfunction planManagedFileWrites(\n files: Record<string, FileDefinition>,\n ctx: ProjectContext,\n): { actions: Action[]; created: string[] } {\n const actions: Action[] = [];\n const created: string[] = [];\n for (const [filePath, definition] of Object.entries(files)) {\n if (exists(nodePath.join(ctx.cwd, filePath))) continue;\n const content = resolveFileContent(definition, ctx);\n actions.push({ type: 'write', path: filePath, content });\n created.push(filePath);\n }\n return { actions, created };\n}\n\nfunction planTextPatchesWithCreation(\n patches: Record<string, TextPatchDefinition>,\n ctx: ProjectContext,\n): { actions: Action[]; created: string[] } {\n const actions: Action[] = [];\n const created: string[] = [];\n for (const [filePath, definition] of Object.entries(patches)) {\n if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;\n actions.push({ type: 'text-patch', path: filePath, definition });\n if (definition.createIfMissing && !exists(nodePath.join(ctx.cwd, filePath))) {\n created.push(filePath);\n }\n }\n return { actions, created };\n}\n\n/**\n * Plan rmdir actions for directories that exist\n * @param dirs\n * @param cwd\n */\nfunction planExistingDirectoriesRemoval(\n directories: string[],\n cwd: string,\n): { actions: Action[]; removed: string[] } {\n const actions: Action[] = [];\n const removed: string[] = [];\n for (const dir of directories) {\n if (exists(nodePath.join(cwd, dir))) {\n actions.push({ type: 'rmdir', path: dir });\n removed.push(dir);\n }\n }\n return { actions, removed };\n}\n\n/**\n * Plan rm actions for files that exist\n * @param files\n * @param cwd\n */\nfunction planExistingFilesRemoval(\n files: string[],\n cwd: string,\n): { actions: Action[]; removed: string[] } {\n const actions: Action[] = [];\n const removed: string[] = [];\n for (const filePath of files) {\n if (exists(nodePath.join(cwd, filePath))) {\n actions.push({ type: 'rm', path: filePath });\n removed.push(filePath);\n }\n }\n return { actions, removed };\n}\n\n/**\n * Check if a .claude path needs parent dir cleanup\n * @param filePath\n */\nfunction getClaudeParentDirectoryForCleanup(filePath: string): string | undefined {\n if (!filePath.startsWith('.claude/')) return undefined;\n const parentDirectory = filePath.slice(0, Math.max(0, filePath.lastIndexOf('/')));\n if (\n !parentDirectory ||\n parentDirectory === '.claude' ||\n parentDirectory === '.claude/skills' ||\n parentDirectory === '.claude/commands'\n ) {\n return undefined;\n }\n return parentDirectory;\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type ReconcileMode = 'install' | 'upgrade' | 'uninstall' | 'uninstall-full';\n\nexport type Action =\n | { type: 'mkdir'; path: string }\n | { type: 'rmdir'; path: string }\n | { type: 'write'; path: string; content: string }\n | { type: 'rm'; path: string }\n | { type: 'chmod'; paths: string[] }\n | { type: 'json-merge'; path: string; definition: JsonMergeDefinition }\n | { type: 'json-unmerge'; path: string; definition: JsonMergeDefinition }\n | { type: 'text-patch'; path: string; definition: TextPatchDefinition }\n | { type: 'text-unpatch'; path: string; definition: TextPatchDefinition };\n\nexport interface ReconcileResult {\n actions: Action[];\n applied: boolean;\n created: string[];\n updated: string[];\n removed: string[];\n packagesToInstall: string[];\n packagesToRemove: string[];\n}\n\nexport interface ReconcileOptions {\n dryRun?: boolean;\n}\n\n// ============================================================================\n// Main reconcile function\n// ============================================================================\n\n/**\n *\n * @param schema\n * @param mode\n * @param ctx\n * @param options\n */\nexport async function reconcile(\n schema: SafewordSchema,\n mode: ReconcileMode,\n ctx: ProjectContext,\n options?: ReconcileOptions,\n): Promise<ReconcileResult> {\n const dryRun = options?.dryRun ?? false;\n\n const plan = computePlan(schema, mode, ctx);\n\n if (dryRun) {\n return {\n actions: plan.actions,\n applied: false,\n created: plan.wouldCreate,\n updated: plan.wouldUpdate,\n removed: plan.wouldRemove,\n packagesToInstall: plan.packagesToInstall,\n packagesToRemove: plan.packagesToRemove,\n };\n }\n\n const result = executePlan(plan, ctx);\n\n return {\n actions: plan.actions,\n applied: true,\n created: result.created,\n updated: result.updated,\n removed: result.removed,\n packagesToInstall: plan.packagesToInstall,\n packagesToRemove: plan.packagesToRemove,\n };\n}\n\n// ============================================================================\n// Plan computation\n// ============================================================================\n\ninterface ReconcilePlan {\n actions: Action[];\n wouldCreate: string[];\n wouldUpdate: string[];\n wouldRemove: string[];\n packagesToInstall: string[];\n packagesToRemove: string[];\n}\n\n/**\n *\n * @param deprecatedFiles\n * @param cwd\n */\nfunction planDeprecatedFilesRemoval(\n deprecatedFiles: string[],\n cwd: string,\n): { actions: Action[]; removed: string[] } {\n const actions: Action[] = [];\n const removed: string[] = [];\n for (const filePath of deprecatedFiles) {\n if (exists(nodePath.join(cwd, filePath))) {\n actions.push({ type: 'rm', path: filePath });\n removed.push(filePath);\n }\n }\n return { actions, removed };\n}\n\n/**\n *\n * @param schema\n * @param mode\n * @param ctx\n */\nfunction computePlan(\n schema: SafewordSchema,\n mode: ReconcileMode,\n ctx: ProjectContext,\n): ReconcilePlan {\n switch (mode) {\n case 'install': {\n return computeInstallPlan(schema, ctx);\n }\n case 'upgrade': {\n return computeUpgradePlan(schema, ctx);\n }\n case 'uninstall': {\n return computeUninstallPlan(schema, ctx, false);\n }\n case 'uninstall-full': {\n return computeUninstallPlan(schema, ctx, true);\n }\n default: {\n // Exhaustive check - TypeScript ensures all cases are handled\n const _exhaustiveCheck: never = mode;\n return _exhaustiveCheck;\n }\n }\n}\n\n/**\n *\n * @param schema\n * @param ctx\n */\nfunction computeInstallPlan(schema: SafewordSchema, ctx: ProjectContext): ReconcilePlan {\n const actions: Action[] = [];\n const wouldCreate: string[] = [];\n\n // 1. Create all directories\n const allDirectories = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];\n const directories = planMissingDirectories(allDirectories, ctx.cwd, ctx.isGitRepo);\n actions.push(...directories.actions);\n wouldCreate.push(...directories.created);\n\n // 2. Write owned files\n const owned = planOwnedFileWrites(schema.ownedFiles, ctx);\n actions.push(...owned.actions);\n wouldCreate.push(...owned.created);\n\n // 3. Write managed files (only if missing)\n const managed = planManagedFileWrites(schema.managedFiles, ctx);\n actions.push(...managed.actions);\n wouldCreate.push(...managed.created);\n\n // 4. chmod hook/lib/scripts directories\n const chmodPaths = [\n '.safeword/hooks',\n '.safeword/hooks/cursor',\n '.safeword/lib',\n '.safeword/scripts',\n ];\n if (ctx.isGitRepo) chmodPaths.push(HUSKY_DIR);\n actions.push({ type: 'chmod', paths: chmodPaths });\n\n // 5. JSON merges\n for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {\n actions.push({ type: 'json-merge', path: filePath, definition });\n }\n\n // 6. Text patches\n const patches = planTextPatchesWithCreation(schema.textPatches, ctx);\n actions.push(...patches.actions);\n wouldCreate.push(...patches.created);\n\n // 7. Compute packages to install\n const packagesToInstall = computePackagesToInstall(\n schema,\n ctx.projectType,\n ctx.developmentDeps,\n ctx.isGitRepo,\n );\n\n return {\n actions,\n wouldCreate,\n wouldUpdate: [],\n wouldRemove: [],\n packagesToInstall,\n packagesToRemove: [],\n };\n}\n\n/**\n *\n * @param schema\n * @param ctx\n */\nfunction computeUpgradePlan(schema: SafewordSchema, ctx: ProjectContext): ReconcilePlan {\n const actions: Action[] = [];\n const wouldCreate: string[] = [];\n const wouldUpdate: string[] = [];\n\n // 1. Ensure directories exist (skip .husky if not a git repo)\n const allDirectories = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];\n const missingDirectories = planMissingDirectories(allDirectories, ctx.cwd, ctx.isGitRepo);\n actions.push(...missingDirectories.actions);\n wouldCreate.push(...missingDirectories.created);\n\n // 2. Update owned files if content changed (skip .husky files if not a git repo)\n for (const [filePath, definition] of Object.entries(schema.ownedFiles)) {\n if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;\n\n const fullPath = nodePath.join(ctx.cwd, filePath);\n const newContent = resolveFileContent(definition, ctx);\n\n if (!fileNeedsUpdate(fullPath, newContent)) continue;\n\n actions.push({ type: 'write', path: filePath, content: newContent });\n if (exists(fullPath)) {\n wouldUpdate.push(filePath);\n } else {\n wouldCreate.push(filePath);\n }\n }\n\n // 3. Update managed files only if content matches current template\n for (const [filePath, definition] of Object.entries(schema.managedFiles)) {\n const fullPath = nodePath.join(ctx.cwd, filePath);\n const newContent = resolveFileContent(definition, ctx);\n\n if (!exists(fullPath)) {\n // Missing - create it\n actions.push({ type: 'write', path: filePath, content: newContent });\n wouldCreate.push(filePath);\n }\n // If file exists, don't update during upgrade - user may have customized it\n }\n\n // 4. Remove deprecated files (renamed or removed in newer versions)\n const deprecatedFiles = planDeprecatedFilesRemoval(schema.deprecatedFiles, ctx.cwd);\n actions.push(...deprecatedFiles.actions);\n const wouldRemove = deprecatedFiles.removed;\n\n // 4b. Remove deprecated directories (no longer managed by safeword)\n const deprecatedDirectories = planExistingDirectoriesRemoval(schema.deprecatedDirs, ctx.cwd);\n actions.push(...deprecatedDirectories.actions);\n wouldRemove.push(...deprecatedDirectories.removed);\n\n // 5. chmod\n const chmodPathsUpgrade = [\n '.safeword/hooks',\n '.safeword/hooks/cursor',\n '.safeword/lib',\n '.safeword/scripts',\n ];\n actions.push({ type: 'chmod', paths: chmodPathsUpgrade });\n\n // 6. JSON merges (always apply to ensure keys are present)\n for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {\n actions.push({ type: 'json-merge', path: filePath, definition });\n }\n\n // 7. Text patches (only if marker missing, skip .husky in non-git repos)\n actions.push(...planTextPatches(schema.textPatches, ctx.cwd, ctx.isGitRepo));\n\n // 8. Compute packages to install (husky/lint-staged skipped if no git repo)\n const packagesToInstall = computePackagesToInstall(\n schema,\n ctx.projectType,\n ctx.developmentDeps,\n ctx.isGitRepo,\n );\n\n // 9. Compute deprecated packages to remove (only those actually installed)\n const packagesToRemove = schema.deprecatedPackages.filter(pkg => pkg in ctx.developmentDeps);\n\n return {\n actions,\n wouldCreate,\n wouldUpdate,\n wouldRemove,\n packagesToInstall,\n packagesToRemove,\n };\n}\n\n/**\n *\n * @param schema\n * @param ctx\n * @param full\n */\nfunction computeUninstallPlan(\n schema: SafewordSchema,\n ctx: ProjectContext,\n full: boolean,\n): ReconcilePlan {\n const actions: Action[] = [];\n const wouldRemove: string[] = [];\n\n // 1. Remove all owned files and track parent dirs for cleanup\n const ownedFiles = planExistingFilesRemoval(Object.keys(schema.ownedFiles), ctx.cwd);\n actions.push(...ownedFiles.actions);\n wouldRemove.push(...ownedFiles.removed);\n\n // Collect parent dirs that need cleanup (for .claude/* skill dirs)\n const directoriesToCleanup = new Set<string>();\n for (const filePath of ownedFiles.removed) {\n const parentDirectory = getClaudeParentDirectoryForCleanup(filePath);\n if (parentDirectory) directoriesToCleanup.add(parentDirectory);\n }\n const cleanupDirectories = planExistingDirectoriesRemoval([...directoriesToCleanup], ctx.cwd);\n actions.push(...cleanupDirectories.actions);\n wouldRemove.push(...cleanupDirectories.removed);\n\n // 2. JSON unmerges\n for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {\n actions.push({ type: 'json-unmerge', path: filePath, definition });\n }\n\n // 3. Text unpatches\n for (const [filePath, definition] of Object.entries(schema.textPatches)) {\n const fullPath = nodePath.join(ctx.cwd, filePath);\n if (exists(fullPath)) {\n const content = readFileSafe(fullPath) ?? '';\n if (content.includes(definition.marker)) {\n actions.push({ type: 'text-unpatch', path: filePath, definition });\n }\n }\n }\n\n // 4. Remove preserved directories first (reverse order, only if empty)\n const preserved = planExistingDirectoriesRemoval(schema.preservedDirs.toReversed(), ctx.cwd);\n actions.push(...preserved.actions);\n wouldRemove.push(...preserved.removed);\n\n // 5. Remove owned directories (reverse order ensures children before parents)\n const owned = planExistingDirectoriesRemoval(schema.ownedDirs.toReversed(), ctx.cwd);\n actions.push(...owned.actions);\n wouldRemove.push(...owned.removed);\n\n // 6. Full uninstall: remove managed files\n if (full) {\n const managed = planExistingFilesRemoval(Object.keys(schema.managedFiles), ctx.cwd);\n actions.push(...managed.actions);\n wouldRemove.push(...managed.removed);\n }\n\n // 7. Compute packages to remove (full only)\n const packagesToRemove = full\n ? computePackagesToRemove(schema, ctx.projectType, ctx.developmentDeps)\n : [];\n\n return {\n actions,\n wouldCreate: [],\n wouldUpdate: [],\n wouldRemove,\n packagesToInstall: [],\n packagesToRemove,\n };\n}\n\n// ============================================================================\n// Plan execution\n// ============================================================================\n\ninterface ExecutionResult {\n created: string[];\n updated: string[];\n removed: string[];\n}\n\n/**\n *\n * @param plan\n * @param ctx\n */\nfunction executePlan(plan: ReconcilePlan, ctx: ProjectContext): ExecutionResult {\n const created: string[] = [];\n const updated: string[] = [];\n const removed: string[] = [];\n const result = { created, updated, removed };\n\n for (const action of plan.actions) {\n executeAction(action, ctx, result);\n }\n\n return result;\n}\n\n/**\n *\n * @param action\n * @param ctx\n * @param result\n */\nfunction executeChmod(cwd: string, paths: string[]): void {\n for (const path of paths) {\n const fullPath = nodePath.join(cwd, path);\n if (exists(fullPath)) makeScriptsExecutable(fullPath);\n }\n}\n\nfunction executeRmdir(cwd: string, path: string, result: ExecutionResult): void {\n if (removeIfEmpty(nodePath.join(cwd, path))) result.removed.push(path);\n}\n\nfunction executeAction(action: Action, ctx: ProjectContext, result: ExecutionResult): void {\n switch (action.type) {\n case 'mkdir': {\n ensureDirectory(nodePath.join(ctx.cwd, action.path));\n result.created.push(action.path);\n break;\n }\n case 'rmdir': {\n executeRmdir(ctx.cwd, action.path, result);\n break;\n }\n case 'write': {\n executeWrite(ctx.cwd, action.path, action.content, result);\n break;\n }\n case 'rm': {\n remove(nodePath.join(ctx.cwd, action.path));\n result.removed.push(action.path);\n break;\n }\n case 'chmod': {\n executeChmod(ctx.cwd, action.paths);\n break;\n }\n case 'json-merge': {\n executeJsonMerge(ctx.cwd, action.path, action.definition, ctx);\n break;\n }\n case 'json-unmerge': {\n executeJsonUnmerge(ctx.cwd, action.path, action.definition);\n break;\n }\n case 'text-patch': {\n executeTextPatch(ctx.cwd, action.path, action.definition);\n break;\n }\n case 'text-unpatch': {\n executeTextUnpatch(ctx.cwd, action.path, action.definition);\n break;\n }\n }\n}\n\n/**\n *\n * @param cwd\n * @param path\n * @param content\n * @param result\n */\nfunction executeWrite(cwd: string, path: string, content: string, result: ExecutionResult): void {\n const fullPath = nodePath.join(cwd, path);\n const existed = exists(fullPath);\n writeFile(fullPath, content);\n (existed ? result.updated : result.created).push(path);\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\n/**\n *\n * @param definition\n * @param ctx\n */\nfunction resolveFileContent(definition: FileDefinition, ctx: ProjectContext): string {\n if (definition.template) {\n const templatesDirectory = getTemplatesDirectory();\n return readFile(nodePath.join(templatesDirectory, definition.template));\n }\n\n if (definition.content) {\n return typeof definition.content === 'function' ? definition.content() : definition.content;\n }\n\n if (definition.generator) {\n return definition.generator(ctx);\n }\n\n throw new Error('FileDefinition must have template, content, or generator');\n}\n\n/**\n *\n * @param installedPath\n * @param newContent\n */\nfunction fileNeedsUpdate(installedPath: string, newContent: string): boolean {\n if (!exists(installedPath)) return true;\n const currentContent = readFileSafe(installedPath);\n return currentContent?.trim() !== newContent.trim();\n}\n\n// Packages that require git repo\nconst GIT_ONLY_PACKAGES = new Set(['husky', 'lint-staged']);\n\n/**\n *\n * @param schema\n * @param projectType\n * @param installedDevDeps\n * @param isGitRepo\n */\nexport function computePackagesToInstall(\n schema: SafewordSchema,\n projectType: ProjectType,\n installedDevelopmentDeps: Record<string, string>,\n isGitRepo = true,\n): string[] {\n let needed = [...schema.packages.base];\n\n // Filter out git-only packages when not in a git repo\n if (!isGitRepo) {\n needed = needed.filter(pkg => !GIT_ONLY_PACKAGES.has(pkg));\n }\n\n // Add conditional packages based on project type\n needed.push(...getConditionalPackages(schema.packages.conditional, projectType));\n\n return needed.filter(pkg => !(pkg in installedDevelopmentDeps));\n}\n\n/**\n *\n * @param schema\n * @param projectType\n * @param installedDevDeps\n */\nfunction computePackagesToRemove(\n schema: SafewordSchema,\n projectType: ProjectType,\n installedDevelopmentDeps: Record<string, string>,\n): string[] {\n const safewordPackages = [\n ...schema.packages.base,\n ...getConditionalPackages(schema.packages.conditional, projectType),\n ];\n\n // Only remove packages that are actually installed\n return safewordPackages.filter(pkg => pkg in installedDevelopmentDeps);\n}\n\n/**\n *\n * @param cwd\n * @param path\n * @param definition\n * @param ctx\n */\nfunction executeJsonMerge(\n cwd: string,\n path: string,\n definition: JsonMergeDefinition,\n ctx: ProjectContext,\n): void {\n const fullPath = nodePath.join(cwd, path);\n const rawExisting = readJson(fullPath) as Record<string, unknown> | undefined;\n\n // Skip if file doesn't exist and skipIfMissing is set\n if (!rawExisting && definition.skipIfMissing) return;\n\n const existing = rawExisting ?? {};\n const merged = definition.merge(existing, ctx);\n\n // Skip write if content is unchanged (avoids formatting churn)\n if (JSON.stringify(existing) === JSON.stringify(merged)) return;\n\n writeJson(fullPath, merged);\n}\n\n/**\n *\n * @param cwd\n * @param path\n * @param definition\n */\nfunction executeJsonUnmerge(cwd: string, path: string, definition: JsonMergeDefinition): void {\n const fullPath = nodePath.join(cwd, path);\n if (!exists(fullPath)) return;\n\n const existing = readJson(fullPath) as Record<string, unknown> | undefined;\n if (!existing) return;\n\n const unmerged = definition.unmerge(existing);\n\n // Check if file should be removed\n if (definition.removeFileIfEmpty) {\n const remainingKeys = Object.keys(unmerged).filter(k => unmerged[k] !== undefined);\n if (remainingKeys.length === 0) {\n remove(fullPath);\n return;\n }\n }\n\n writeJson(fullPath, unmerged);\n}\n\n/**\n *\n * @param cwd\n * @param path\n * @param definition\n */\nfunction executeTextPatch(cwd: string, path: string, definition: TextPatchDefinition): void {\n const fullPath = nodePath.join(cwd, path);\n let content = readFileSafe(fullPath) ?? '';\n\n // Check if already patched\n if (content.includes(definition.marker)) return;\n\n // Apply patch\n content =\n definition.operation === 'prepend'\n ? definition.content + content\n : content + definition.content;\n\n writeFile(fullPath, content);\n}\n\n/**\n *\n * @param cwd\n * @param path\n * @param definition\n */\nfunction executeTextUnpatch(cwd: string, path: string, definition: TextPatchDefinition): void {\n const fullPath = nodePath.join(cwd, path);\n const content = readFileSafe(fullPath);\n if (!content) return;\n\n // Remove the patched content\n // First try to remove the full content block\n let unpatched = content.replace(definition.content, '');\n\n // If full content wasn't found but marker exists, remove lines containing the marker\n if (unpatched === content && content.includes(definition.marker)) {\n // Remove lines containing the marker\n const lines = content.split('\\n');\n const filtered = lines.filter(line => !line.includes(definition.marker));\n unpatched = filtered.join('\\n').replace(/^\\n+/, ''); // Remove leading empty lines\n }\n\n writeFile(fullPath, unpatched);\n}\n","/**\n * Configuration templates - ESLint config generation and hook settings\n *\n * ESLint flat config (v9+) using eslint-plugin-safeword for all rules.\n * Framework detection uses safeword.detect utilities at runtime.\n *\n * See: https://eslint.org/docs/latest/use/configure/configuration-files\n */\n\n/**\n * Generates an ESLint config using eslint-plugin-safeword.\n *\n * The generated config uses safeword.detect utilities to detect frameworks\n * and select the appropriate config at lint time.\n * @param hasExistingFormatter - If true, generates a minimal config without Prettier\n * @returns ESLint config file content as a string\n */\nexport function getEslintConfig(hasExistingFormatter = false): string {\n if (hasExistingFormatter) {\n return getFormatterAgnosticEslintConfig();\n }\n return getStandardEslintConfig();\n}\n\n/**\n * Standard ESLint config - full linting with Prettier\n */\nfunction getStandardEslintConfig(): string {\n return `import { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport safeword from \"eslint-plugin-safeword\";\nimport eslintConfigPrettier from \"eslint-config-prettier\";\n\nconst { detect, configs } = safeword;\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst deps = detect.collectAllDeps(__dirname);\nconst framework = detect.detectFramework(deps);\n\n// Map framework to base config\n// Note: Astro config only lints .astro files, so we combine it with TypeScript config\n// to also lint .ts files in Astro projects\nconst baseConfigs = {\n next: configs.recommendedTypeScriptNext,\n react: configs.recommendedTypeScriptReact,\n astro: [...configs.recommendedTypeScript, ...configs.astro],\n typescript: configs.recommendedTypeScript,\n javascript: configs.recommended,\n};\n\nexport default [\n { ignores: detect.getIgnores(deps) },\n ...baseConfigs[framework],\n ...(detect.hasVitest(deps) ? configs.vitest : []),\n ...(detect.hasPlaywright(deps) ? configs.playwright : []),\n ...(detect.hasTailwind(deps) ? configs.tailwind : []),\n ...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),\n eslintConfigPrettier,\n];\n`;\n}\n\n/**\n * Formatter-agnostic ESLint config - minimal config for projects with existing formatter.\n * Used alongside external formatters (Biome, dprint, etc.) that handle formatting.\n * Does not include eslint-config-prettier since another tool handles formatting.\n */\nfunction getFormatterAgnosticEslintConfig(): string {\n return `import { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport safeword from \"eslint-plugin-safeword\";\n\nconst { detect, configs } = safeword;\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst deps = detect.collectAllDeps(__dirname);\nconst framework = detect.detectFramework(deps);\n\n// Map framework to base config\n// Note: Astro config only lints .astro files, so we combine it with TypeScript config\n// to also lint .ts files in Astro projects\nconst baseConfigs = {\n next: configs.recommendedTypeScriptNext,\n react: configs.recommendedTypeScriptReact,\n astro: [...configs.recommendedTypeScript, ...configs.astro],\n typescript: configs.recommendedTypeScript,\n javascript: configs.recommended,\n};\n\nexport default [\n { ignores: detect.getIgnores(deps) },\n ...baseConfigs[framework],\n ...(detect.hasVitest(deps) ? configs.vitest : []),\n ...(detect.hasPlaywright(deps) ? configs.playwright : []),\n ...(detect.hasTailwind(deps) ? configs.tailwind : []),\n ...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),\n];\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 * 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\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","/**\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 * @param h\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 * @param h\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 * @param hooks\n */\nexport function filterOutSafewordHooks(hooks: unknown[]): unknown[] {\n return hooks.filter(h => !isSafewordHook(h));\n}\n","/**\n * Shared installation constants\n *\n * These constants are used by schema.ts to define the single source of truth.\n * Operations are handled by reconcile() in src/reconcile.ts.\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 * 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 { CURSOR_HOOKS, getEslintConfig, SETTINGS_HOOKS } from './templates/config.js';\nimport { AGENTS_MD_LINK } from './templates/content.js';\nimport { filterOutSafewordHooks } from './utils/hooks.js';\nimport { MCP_SERVERS } from './utils/install.js';\nimport { type ProjectType } from './utils/project-detector.js';\nimport { VERSION } from './version.js';\n\n// ============================================================================\n// Interfaces\n// ============================================================================\n\nexport interface ProjectContext {\n cwd: string;\n projectType: ProjectType;\n developmentDeps: 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 skipIfMissing?: boolean; // Don't create file if it doesn't exist (for optional integrations)\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 deprecatedFiles: string[]; // Files to delete on upgrade (renamed or removed)\n deprecatedPackages: string[]; // Packages to uninstall on upgrade (consolidated into safeword plugin)\n deprecatedDirs: string[]; // Directories to delete on upgrade (no longer managed)\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// Shared merge definitions\n// ============================================================================\n\n/**\n * Biome config merge - adds safeword files to excludes list.\n * Biome v2 uses `includes` with `!` prefix for exclusions.\n */\nconst BIOME_JSON_MERGE: JsonMergeDefinition = {\n keys: ['files.includes'],\n skipIfMissing: true, // Only modify if project already uses Biome\n merge: existing => {\n const files = (existing.files as Record<string, unknown>) ?? {};\n const existingIncludes = Array.isArray(files.includes) ? files.includes : [];\n\n // Add safeword exclusions (! prefix) if not already present\n // Note: Biome v2.2.0+ doesn't need /** for folders\n const safewordExcludes = ['!eslint.config.mjs', '!.safeword'];\n const newIncludes = [...existingIncludes];\n for (const exclude of safewordExcludes) {\n if (!newIncludes.includes(exclude)) {\n newIncludes.push(exclude);\n }\n }\n\n return {\n ...existing,\n files: {\n ...files,\n includes: newIncludes,\n },\n };\n },\n unmerge: existing => {\n const result = { ...existing };\n const files = (existing.files as Record<string, unknown>) ?? {};\n const existingIncludes = Array.isArray(files.includes) ? files.includes : [];\n\n // Remove safeword exclusions from includes list\n const safewordExcludes = new Set(['!eslint.config.mjs', '!.safeword', '!.safeword/**']);\n const cleanedIncludes = existingIncludes.filter(\n (entry: string) => !safewordExcludes.has(entry),\n );\n\n if (cleanedIncludes.length > 0) {\n files.includes = cleanedIncludes;\n result.files = files;\n } else {\n delete files.includes;\n if (Object.keys(files).length > 0) {\n result.files = files;\n } else {\n delete result.files;\n }\n }\n\n return result;\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/planning/plans',\n '.safeword/scripts',\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: [\n '.safeword/learnings',\n '.safeword/tickets',\n '.safeword/tickets/completed',\n '.safeword/logs',\n ],\n\n // Files to delete on upgrade (renamed or removed in newer versions)\n deprecatedFiles: [\n '.safeword/templates/user-stories-template.md',\n // Consolidated into planning-guide.md and testing-guide.md (v0.8.0)\n '.safeword/guides/development-workflow.md',\n '.safeword/guides/tdd-best-practices.md',\n '.safeword/guides/user-story-guide.md',\n '.safeword/guides/test-definitions-guide.md',\n // Boundaries config now project-specific (v0.9.0)\n '.safeword/eslint-boundaries.config.mjs',\n // Markdown linting removed (v0.10.0)\n '.markdownlint-cli2.jsonc',\n '.safeword/scripts/lint-md.sh',\n ],\n\n // Packages to uninstall on upgrade (now bundled in eslint-plugin-safeword)\n deprecatedPackages: [\n // Individual ESLint plugins now bundled in eslint-plugin-safeword\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 'eslint-plugin-promise',\n 'eslint-plugin-regexp',\n 'eslint-plugin-jsdoc',\n 'eslint-plugin-simple-import-sort',\n 'eslint-plugin-security',\n // Conditional ESLint plugins now in safeword\n 'typescript-eslint',\n 'eslint-plugin-react',\n 'eslint-plugin-react-hooks',\n 'eslint-plugin-jsx-a11y',\n '@next/eslint-plugin-next',\n 'eslint-plugin-astro',\n '@vitest/eslint-plugin',\n // Pre-commit hooks no longer managed by safeword\n 'husky',\n 'lint-staged',\n ],\n\n // Directories to delete on upgrade (no longer managed by safeword)\n deprecatedDirs: [\n '.husky', // Pre-commit hooks no longer managed by safeword\n ],\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\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 (11 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/learning-extraction.md': { template: 'guides/learning-extraction.md' },\n '.safeword/guides/llm-guide.md': { template: 'guides/llm-guide.md' },\n '.safeword/guides/planning-guide.md': { template: 'guides/planning-guide.md' },\n '.safeword/guides/testing-guide.md': { template: 'guides/testing-guide.md' },\n '.safeword/guides/zombie-process-cleanup.md': { template: 'guides/zombie-process-cleanup.md' },\n\n // Templates (7 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/feature-spec-template.md': {\n template: 'doc-templates/feature-spec-template.md',\n },\n '.safeword/templates/work-log-template.md': { template: 'doc-templates/work-log-template.md' },\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/cleanup-zombies.sh': { template: 'scripts/cleanup-zombies.sh' },\n\n // Claude skills and commands (9 files)\n '.claude/skills/safeword-brainstorming/SKILL.md': {\n template: 'skills/safeword-brainstorming/SKILL.md',\n },\n '.claude/skills/safeword-debugging/SKILL.md': {\n template: 'skills/safeword-debugging/SKILL.md',\n },\n '.claude/skills/safeword-enforcing-tdd/SKILL.md': {\n template: 'skills/safeword-enforcing-tdd/SKILL.md',\n },\n '.claude/skills/safeword-quality-reviewer/SKILL.md': {\n template: 'skills/safeword-quality-reviewer/SKILL.md',\n },\n '.claude/skills/safeword-refactoring/SKILL.md': {\n template: 'skills/safeword-refactoring/SKILL.md',\n },\n '.claude/skills/safeword-writing-plans/SKILL.md': {\n template: 'skills/safeword-writing-plans/SKILL.md',\n },\n '.claude/commands/architecture.md': { template: 'commands/architecture.md' },\n '.claude/commands/audit.md': { template: 'commands/audit.md' },\n '.claude/commands/cleanup-zombies.md': { template: 'commands/cleanup-zombies.md' },\n '.claude/commands/lint.md': { template: 'commands/lint.md' },\n '.claude/commands/quality-review.md': { template: 'commands/quality-review.md' },\n\n // Cursor rules (7 files)\n '.cursor/rules/safeword-core.mdc': { template: 'cursor/rules/safeword-core.mdc' },\n '.cursor/rules/safeword-brainstorming.mdc': {\n template: 'cursor/rules/safeword-brainstorming.mdc',\n },\n '.cursor/rules/safeword-debugging.mdc': {\n template: 'cursor/rules/safeword-debugging.mdc',\n },\n '.cursor/rules/safeword-enforcing-tdd.mdc': {\n template: 'cursor/rules/safeword-enforcing-tdd.mdc',\n },\n '.cursor/rules/safeword-quality-reviewer.mdc': {\n template: 'cursor/rules/safeword-quality-reviewer.mdc',\n },\n '.cursor/rules/safeword-refactoring.mdc': {\n template: 'cursor/rules/safeword-refactoring.mdc',\n },\n '.cursor/rules/safeword-writing-plans.mdc': {\n template: 'cursor/rules/safeword-writing-plans.mdc',\n },\n\n // Cursor commands (5 files - same as Claude)\n '.cursor/commands/architecture.md': { template: 'commands/architecture.md' },\n '.cursor/commands/audit.md': { template: 'commands/audit.md' },\n '.cursor/commands/cleanup-zombies.md': { template: 'commands/cleanup-zombies.md' },\n '.cursor/commands/lint.md': { template: 'commands/lint.md' },\n '.cursor/commands/quality-review.md': { template: 'commands/quality-review.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: ctx => getEslintConfig(ctx.projectType.existingFormatter),\n },\n // Minimal tsconfig for ESLint type-checked linting (only if missing)\n 'tsconfig.json': {\n generator: ctx => {\n // Only create for TypeScript projects\n if (!ctx.developmentDeps.typescript && !ctx.developmentDeps['typescript-eslint']) {\n return ''; // Empty = skip this file\n }\n return JSON.stringify(\n {\n compilerOptions: {\n target: 'ES2022',\n module: 'NodeNext',\n moduleResolution: 'NodeNext',\n strict: true,\n esModuleInterop: true,\n skipLibCheck: true,\n noEmit: true,\n },\n include: ['**/*.ts', '**/*.tsx'],\n exclude: ['node_modules', 'dist', 'build'],\n },\n undefined,\n 2,\n );\n },\n },\n // Knip config for dead code detection (used by /audit)\n 'knip.json': {\n generator: () =>\n JSON.stringify(\n {\n ignore: ['.safeword/**'],\n ignoreDependencies: ['eslint-plugin-safeword'],\n },\n undefined,\n 2,\n ),\n },\n },\n\n // JSON files where we merge specific keys\n jsonMerges: {\n 'package.json': {\n keys: ['scripts.lint', 'scripts.format', 'scripts.format:check', 'scripts.knip'],\n conditionalKeys: {\n existingLinter: ['scripts.lint:eslint'], // Projects with existing linter get separate ESLint script\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 if (ctx.projectType.existingLinter) {\n // Project with existing linter: add lint:eslint for safeword-specific rules\n if (!scripts['lint:eslint']) scripts['lint:eslint'] = 'eslint .';\n // Don't touch their existing lint script\n } else {\n // No existing linter: ESLint is the primary linter\n if (!scripts.lint) scripts.lint = 'eslint .';\n }\n\n if (!ctx.projectType.existingFormatter) {\n // No existing formatter: add Prettier\n if (!scripts.format) scripts.format = 'prettier --write .';\n if (!scripts['format:check']) scripts['format:check'] = 'prettier --check .';\n }\n\n // Always add knip for dead code detection\n if (!scripts.knip) scripts.knip = 'knip';\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 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:eslint']; // Biome hybrid mode\n delete scripts['lint:sh'];\n delete scripts['format:check'];\n delete scripts.knip;\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 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] ?? [];\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);\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 '.prettierrc': {\n keys: ['plugins'],\n merge: (existing, ctx) => {\n const result = { ...existing } as Record<string, unknown>;\n\n // Set defaults for styling options (only if not present)\n // User customizations are preserved\n if (result.semi === undefined) result.semi = true;\n if (result.singleQuote === undefined) result.singleQuote = true;\n if (result.tabWidth === undefined) result.tabWidth = 2;\n if (result.trailingComma === undefined) result.trailingComma = 'all';\n if (result.printWidth === undefined) result.printWidth = 100;\n if (result.endOfLine === undefined) result.endOfLine = 'lf';\n if (result.useTabs === undefined) result.useTabs = false;\n if (result.bracketSpacing === undefined) result.bracketSpacing = true;\n if (result.arrowParens === undefined) result.arrowParens = 'avoid';\n\n // Always update plugins based on project type (safeword owns this)\n const plugins: string[] = [];\n if (ctx.projectType.astro) plugins.push('prettier-plugin-astro');\n if (ctx.projectType.shell) plugins.push('prettier-plugin-sh');\n // Tailwind must be last for proper class sorting\n if (ctx.projectType.tailwind) plugins.push('prettier-plugin-tailwindcss');\n\n if (plugins.length > 0) {\n result.plugins = plugins;\n } else {\n delete result.plugins;\n }\n\n return result;\n },\n unmerge: existing => {\n const result = { ...existing } as Record<string, unknown>;\n // Only remove plugins (safeword-owned), keep user styling preferences\n delete result.plugins;\n return result;\n },\n },\n\n // Biome excludes - add safeword files so they don't get linted by Biome/Ultracite\n // Biome v2 uses `includes` with `!` prefix for exclusions (not a separate `ignore` key)\n // Support both biome.json and biome.jsonc\n 'biome.json': BIOME_JSON_MERGE,\n 'biome.jsonc': BIOME_JSON_MERGE,\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 // Core tools (always needed)\n 'eslint',\n // Safeword plugin (bundles eslint-config-prettier + all ESLint plugins)\n 'eslint-plugin-safeword',\n // Architecture and dead code tools (used by /audit)\n 'dependency-cruiser',\n 'knip',\n ],\n conditional: {\n // Prettier (only for projects without existing formatter)\n standard: ['prettier'], // \"standard\" = !existingFormatter\n // Prettier plugins (only for projects without existing formatter that need them)\n astro: ['prettier-plugin-astro'],\n tailwind: ['prettier-plugin-tailwindcss'],\n shell: ['prettier-plugin-sh'],\n // Non-ESLint tools\n publishableLibrary: ['publint'],\n shellcheck: ['shellcheck'], // Renamed from shell to avoid conflict with prettier-plugin-sh\n },\n },\n};\n","/**\n * Git utilities for CLI operations\n */\n\nimport nodePath from 'node:path';\n\nimport { exists } from './fs.js';\n\n/**\n * Check if directory is a git repository\n * @param cwd\n */\nexport function isGitRepo(cwd: string): boolean {\n return exists(nodePath.join(cwd, '.git'));\n}\n","/**\n * Project Context Utilities\n *\n * Shared helpers for creating ProjectContext objects used by reconcile().\n */\n\nimport nodePath from 'node:path';\n\nimport type { ProjectContext } from '../schema.js';\nimport { readJson } from './fs.js';\nimport { isGitRepo } from './git.js';\nimport { detectProjectType, type PackageJson } from './project-detector.js';\n\n/**\n * Create a ProjectContext from the current working directory.\n *\n * Reads package.json and detects project type for use with reconcile().\n * @param cwd\n */\nexport function createProjectContext(cwd: string): ProjectContext {\n const packageJson = readJson(nodePath.join(cwd, 'package.json')) as PackageJson | undefined;\n\n return {\n cwd,\n projectType: detectProjectType(packageJson ?? {}, cwd),\n developmentDeps: packageJson?.devDependencies ?? {},\n isGitRepo: isGitRepo(cwd),\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 nodePath from 'node:path';\n\nimport { detect } from 'eslint-plugin-safeword';\n\n// Re-export detection constants from eslint-plugin-safeword (single source of truth)\nexport const {\n TAILWIND_PACKAGES,\n TANSTACK_QUERY_PACKAGES,\n PLAYWRIGHT_PACKAGES,\n FORMATTER_CONFIG_FILES,\n hasExistingLinter,\n hasExistingFormatter,\n} = detect;\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 vitest: boolean;\n playwright: boolean;\n tailwind: boolean;\n tanstackQuery: boolean;\n publishableLibrary: boolean;\n shell: boolean;\n /** True if project has existing lint script or linter config */\n existingLinter: boolean;\n /** True if project has existing format script or formatter config */\n existingFormatter: 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 * @param cwd\n * @param maxDepth\n */\nexport function hasShellScripts(cwd: string, maxDepth = 4): boolean {\n const excludeDirectories = new Set(['node_modules', '.git', '.safeword']);\n\n /**\n *\n * @param dir\n * @param depth\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 (\n entry.isDirectory() &&\n !excludeDirectories.has(entry.name) &&\n scan(nodePath.join(dir, entry.name), depth + 1)\n ) {\n return true;\n }\n }\n } catch {\n // Ignore permission errors\n }\n return false;\n }\n\n return scan(cwd, 0);\n}\n\nexport interface PackageJsonWithScripts extends PackageJson {\n scripts?: Record<string, string>;\n}\n\n/**\n * Detects project type from package.json contents and optional file scanning\n * @param packageJson - Package.json contents including scripts\n * @param cwd - Working directory for file-based detection\n */\nexport function detectProjectType(packageJson: PackageJsonWithScripts, cwd?: string): ProjectType {\n const deps = packageJson.dependencies || {};\n const developmentDeps = packageJson.devDependencies || {};\n const allDeps = { ...deps, ...developmentDeps };\n const scripts = packageJson.scripts || {};\n\n const hasTypescript = 'typescript' in allDeps;\n const hasReact = 'react' in deps || 'react' in developmentDeps;\n const hasNextJs = 'next' in deps;\n const hasAstro = 'astro' in deps || 'astro' in developmentDeps;\n const hasVitest = 'vitest' in developmentDeps;\n const hasPlaywright = '@playwright/test' in developmentDeps;\n // Tailwind v4 can be installed via tailwindcss, @tailwindcss/vite, or @tailwindcss/postcss\n const hasTailwind = TAILWIND_PACKAGES.some(pkg => pkg in allDeps);\n\n // TanStack Query detection\n const hasTanstackQuery = TANSTACK_QUERY_PACKAGES.some(pkg => pkg 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 // Generic tooling detection: detect intent, not specific tools\n const hasLinter = hasExistingLinter(scripts);\n const hasFormatter = cwd ? hasExistingFormatter(cwd, scripts) : 'format' in scripts;\n\n return {\n typescript: hasTypescript,\n react: hasReact || hasNextJs, // Next.js implies React\n nextjs: hasNextJs,\n astro: hasAstro,\n vitest: hasVitest,\n playwright: hasPlaywright,\n tailwind: hasTailwind,\n tanstackQuery: hasTanstackQuery,\n publishableLibrary: isPublishable,\n shell: hasShell,\n existingLinter: hasLinter,\n existingFormatter: hasFormatter,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAOA,OAAO,cAAc;AA4BrB,IAAM,YAAY;AAKlB,IAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMD,SAAS,uBACP,qBACA,aACU;AACV,QAAM,WAAqB,CAAC;AAE5B,aAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,mBAAmB,GAAG;AAE7D,QAAI,QAAQ,YAAY;AACtB,UAAI,CAAC,YAAY,mBAAmB;AAClC,iBAAS,KAAK,GAAG,IAAI;AAAA,MACvB;AACA;AAAA,IACF;AAGA,QAAI,YAAY,GAAwB,GAAG;AAEzC,UAAI,YAAY,mBAAmB;AACjC,iBAAS,KAAK,GAAG,KAAK,OAAO,SAAO,CAAC,kBAAkB,IAAI,GAAG,CAAC,CAAC;AAAA,MAClE,OAAO;AACL,iBAAS,KAAK,GAAG,IAAI;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAOA,SAAS,oBAAoB,MAAcA,YAA6B;AACtE,SAAO,KAAK,WAAW,SAAS,KAAK,CAACA;AACxC;AAQA,SAAS,uBACP,aACA,KACAA,YAC0C;AAC1C,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAC3B,aAAW,OAAO,aAAa;AAC7B,QAAI,oBAAoB,KAAKA,UAAS,EAAG;AACzC,QAAI,CAAC,OAAO,SAAS,KAAK,KAAK,GAAG,CAAC,GAAG;AACpC,cAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,IAAI,CAAC;AACzC,cAAQ,KAAK,GAAG;AAAA,IAClB;AAAA,EACF;AACA,SAAO,EAAE,SAAS,QAAQ;AAC5B;AAQA,SAAS,gBACP,SACA,KACAA,YACU;AACV,QAAM,UAAoB,CAAC;AAC3B,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,OAAO,GAAG;AAC5D,QAAI,oBAAoB,UAAUA,UAAS,EAAG;AAC9C,UAAM,UAAU,aAAa,SAAS,KAAK,KAAK,QAAQ,CAAC,KAAK;AAC9D,QAAI,CAAC,QAAQ,SAAS,WAAW,MAAM,GAAG;AACxC,cAAQ,KAAK,EAAE,MAAM,cAAc,MAAM,UAAU,WAAW,CAAC;AAAA,IACjE;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,oBACP,OACA,KAC0C;AAC1C,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAC3B,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC1D,QAAI,oBAAoB,UAAU,IAAI,SAAS,EAAG;AAClD,UAAM,UAAU,mBAAmB,YAAY,GAAG;AAClD,YAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,UAAU,QAAQ,CAAC;AACvD,YAAQ,KAAK,QAAQ;AAAA,EACvB;AACA,SAAO,EAAE,SAAS,QAAQ;AAC5B;AAEA,SAAS,sBACP,OACA,KAC0C;AAC1C,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAC3B,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC1D,QAAI,OAAO,SAAS,KAAK,IAAI,KAAK,QAAQ,CAAC,EAAG;AAC9C,UAAM,UAAU,mBAAmB,YAAY,GAAG;AAClD,YAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,UAAU,QAAQ,CAAC;AACvD,YAAQ,KAAK,QAAQ;AAAA,EACvB;AACA,SAAO,EAAE,SAAS,QAAQ;AAC5B;AAEA,SAAS,4BACP,SACA,KAC0C;AAC1C,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAC3B,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,OAAO,GAAG;AAC5D,QAAI,oBAAoB,UAAU,IAAI,SAAS,EAAG;AAClD,YAAQ,KAAK,EAAE,MAAM,cAAc,MAAM,UAAU,WAAW,CAAC;AAC/D,QAAI,WAAW,mBAAmB,CAAC,OAAO,SAAS,KAAK,IAAI,KAAK,QAAQ,CAAC,GAAG;AAC3E,cAAQ,KAAK,QAAQ;AAAA,IACvB;AAAA,EACF;AACA,SAAO,EAAE,SAAS,QAAQ;AAC5B;AAOA,SAAS,+BACP,aACA,KAC0C;AAC1C,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAC3B,aAAW,OAAO,aAAa;AAC7B,QAAI,OAAO,SAAS,KAAK,KAAK,GAAG,CAAC,GAAG;AACnC,cAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,IAAI,CAAC;AACzC,cAAQ,KAAK,GAAG;AAAA,IAClB;AAAA,EACF;AACA,SAAO,EAAE,SAAS,QAAQ;AAC5B;AAOA,SAAS,yBACP,OACA,KAC0C;AAC1C,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAC3B,aAAW,YAAY,OAAO;AAC5B,QAAI,OAAO,SAAS,KAAK,KAAK,QAAQ,CAAC,GAAG;AACxC,cAAQ,KAAK,EAAE,MAAM,MAAM,MAAM,SAAS,CAAC;AAC3C,cAAQ,KAAK,QAAQ;AAAA,IACvB;AAAA,EACF;AACA,SAAO,EAAE,SAAS,QAAQ;AAC5B;AAMA,SAAS,mCAAmC,UAAsC;AAChF,MAAI,CAAC,SAAS,WAAW,UAAU,EAAG,QAAO;AAC7C,QAAM,kBAAkB,SAAS,MAAM,GAAG,KAAK,IAAI,GAAG,SAAS,YAAY,GAAG,CAAC,CAAC;AAChF,MACE,CAAC,mBACD,oBAAoB,aACpB,oBAAoB,oBACpB,oBAAoB,oBACpB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AA4CA,eAAsB,UACpB,QACA,MACA,KACA,SAC0B;AAC1B,QAAM,SAAS,SAAS,UAAU;AAElC,QAAM,OAAO,YAAY,QAAQ,MAAM,GAAG;AAE1C,MAAI,QAAQ;AACV,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,SAAS;AAAA,MACT,SAAS,KAAK;AAAA,MACd,SAAS,KAAK;AAAA,MACd,SAAS,KAAK;AAAA,MACd,mBAAmB,KAAK;AAAA,MACxB,kBAAkB,KAAK;AAAA,IACzB;AAAA,EACF;AAEA,QAAM,SAAS,YAAY,MAAM,GAAG;AAEpC,SAAO;AAAA,IACL,SAAS,KAAK;AAAA,IACd,SAAS;AAAA,IACT,SAAS,OAAO;AAAA,IAChB,SAAS,OAAO;AAAA,IAChB,SAAS,OAAO;AAAA,IAChB,mBAAmB,KAAK;AAAA,IACxB,kBAAkB,KAAK;AAAA,EACzB;AACF;AAoBA,SAAS,2BACP,iBACA,KAC0C;AAC1C,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAC3B,aAAW,YAAY,iBAAiB;AACtC,QAAI,OAAO,SAAS,KAAK,KAAK,QAAQ,CAAC,GAAG;AACxC,cAAQ,KAAK,EAAE,MAAM,MAAM,MAAM,SAAS,CAAC;AAC3C,cAAQ,KAAK,QAAQ;AAAA,IACvB;AAAA,EACF;AACA,SAAO,EAAE,SAAS,QAAQ;AAC5B;AAQA,SAAS,YACP,QACA,MACA,KACe;AACf,UAAQ,MAAM;AAAA,IACZ,KAAK,WAAW;AACd,aAAO,mBAAmB,QAAQ,GAAG;AAAA,IACvC;AAAA,IACA,KAAK,WAAW;AACd,aAAO,mBAAmB,QAAQ,GAAG;AAAA,IACvC;AAAA,IACA,KAAK,aAAa;AAChB,aAAO,qBAAqB,QAAQ,KAAK,KAAK;AAAA,IAChD;AAAA,IACA,KAAK,kBAAkB;AACrB,aAAO,qBAAqB,QAAQ,KAAK,IAAI;AAAA,IAC/C;AAAA,IACA,SAAS;AAEP,YAAM,mBAA0B;AAChC,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAOA,SAAS,mBAAmB,QAAwB,KAAoC;AACtF,QAAM,UAAoB,CAAC;AAC3B,QAAM,cAAwB,CAAC;AAG/B,QAAM,iBAAiB,CAAC,GAAG,OAAO,WAAW,GAAG,OAAO,YAAY,GAAG,OAAO,aAAa;AAC1F,QAAM,cAAc,uBAAuB,gBAAgB,IAAI,KAAK,IAAI,SAAS;AACjF,UAAQ,KAAK,GAAG,YAAY,OAAO;AACnC,cAAY,KAAK,GAAG,YAAY,OAAO;AAGvC,QAAM,QAAQ,oBAAoB,OAAO,YAAY,GAAG;AACxD,UAAQ,KAAK,GAAG,MAAM,OAAO;AAC7B,cAAY,KAAK,GAAG,MAAM,OAAO;AAGjC,QAAM,UAAU,sBAAsB,OAAO,cAAc,GAAG;AAC9D,UAAQ,KAAK,GAAG,QAAQ,OAAO;AAC/B,cAAY,KAAK,GAAG,QAAQ,OAAO;AAGnC,QAAM,aAAa;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,IAAI,UAAW,YAAW,KAAK,SAAS;AAC5C,UAAQ,KAAK,EAAE,MAAM,SAAS,OAAO,WAAW,CAAC;AAGjD,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,OAAO,UAAU,GAAG;AACtE,YAAQ,KAAK,EAAE,MAAM,cAAc,MAAM,UAAU,WAAW,CAAC;AAAA,EACjE;AAGA,QAAM,UAAU,4BAA4B,OAAO,aAAa,GAAG;AACnE,UAAQ,KAAK,GAAG,QAAQ,OAAO;AAC/B,cAAY,KAAK,GAAG,QAAQ,OAAO;AAGnC,QAAM,oBAAoB;AAAA,IACxB;AAAA,IACA,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,aAAa,CAAC;AAAA,IACd,aAAa,CAAC;AAAA,IACd;AAAA,IACA,kBAAkB,CAAC;AAAA,EACrB;AACF;AAOA,SAAS,mBAAmB,QAAwB,KAAoC;AACtF,QAAM,UAAoB,CAAC;AAC3B,QAAM,cAAwB,CAAC;AAC/B,QAAM,cAAwB,CAAC;AAG/B,QAAM,iBAAiB,CAAC,GAAG,OAAO,WAAW,GAAG,OAAO,YAAY,GAAG,OAAO,aAAa;AAC1F,QAAM,qBAAqB,uBAAuB,gBAAgB,IAAI,KAAK,IAAI,SAAS;AACxF,UAAQ,KAAK,GAAG,mBAAmB,OAAO;AAC1C,cAAY,KAAK,GAAG,mBAAmB,OAAO;AAG9C,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,OAAO,UAAU,GAAG;AACtE,QAAI,oBAAoB,UAAU,IAAI,SAAS,EAAG;AAElD,UAAM,WAAW,SAAS,KAAK,IAAI,KAAK,QAAQ;AAChD,UAAM,aAAa,mBAAmB,YAAY,GAAG;AAErD,QAAI,CAAC,gBAAgB,UAAU,UAAU,EAAG;AAE5C,YAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,UAAU,SAAS,WAAW,CAAC;AACnE,QAAI,OAAO,QAAQ,GAAG;AACpB,kBAAY,KAAK,QAAQ;AAAA,IAC3B,OAAO;AACL,kBAAY,KAAK,QAAQ;AAAA,IAC3B;AAAA,EACF;AAGA,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,OAAO,YAAY,GAAG;AACxE,UAAM,WAAW,SAAS,KAAK,IAAI,KAAK,QAAQ;AAChD,UAAM,aAAa,mBAAmB,YAAY,GAAG;AAErD,QAAI,CAAC,OAAO,QAAQ,GAAG;AAErB,cAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,UAAU,SAAS,WAAW,CAAC;AACnE,kBAAY,KAAK,QAAQ;AAAA,IAC3B;AAAA,EAEF;AAGA,QAAM,kBAAkB,2BAA2B,OAAO,iBAAiB,IAAI,GAAG;AAClF,UAAQ,KAAK,GAAG,gBAAgB,OAAO;AACvC,QAAM,cAAc,gBAAgB;AAGpC,QAAM,wBAAwB,+BAA+B,OAAO,gBAAgB,IAAI,GAAG;AAC3F,UAAQ,KAAK,GAAG,sBAAsB,OAAO;AAC7C,cAAY,KAAK,GAAG,sBAAsB,OAAO;AAGjD,QAAM,oBAAoB;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,UAAQ,KAAK,EAAE,MAAM,SAAS,OAAO,kBAAkB,CAAC;AAGxD,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,OAAO,UAAU,GAAG;AACtE,YAAQ,KAAK,EAAE,MAAM,cAAc,MAAM,UAAU,WAAW,CAAC;AAAA,EACjE;AAGA,UAAQ,KAAK,GAAG,gBAAgB,OAAO,aAAa,IAAI,KAAK,IAAI,SAAS,CAAC;AAG3E,QAAM,oBAAoB;AAAA,IACxB;AAAA,IACA,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAGA,QAAM,mBAAmB,OAAO,mBAAmB,OAAO,SAAO,OAAO,IAAI,eAAe;AAE3F,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAQA,SAAS,qBACP,QACA,KACA,MACe;AACf,QAAM,UAAoB,CAAC;AAC3B,QAAM,cAAwB,CAAC;AAG/B,QAAM,aAAa,yBAAyB,OAAO,KAAK,OAAO,UAAU,GAAG,IAAI,GAAG;AACnF,UAAQ,KAAK,GAAG,WAAW,OAAO;AAClC,cAAY,KAAK,GAAG,WAAW,OAAO;AAGtC,QAAM,uBAAuB,oBAAI,IAAY;AAC7C,aAAW,YAAY,WAAW,SAAS;AACzC,UAAM,kBAAkB,mCAAmC,QAAQ;AACnE,QAAI,gBAAiB,sBAAqB,IAAI,eAAe;AAAA,EAC/D;AACA,QAAM,qBAAqB,+BAA+B,CAAC,GAAG,oBAAoB,GAAG,IAAI,GAAG;AAC5F,UAAQ,KAAK,GAAG,mBAAmB,OAAO;AAC1C,cAAY,KAAK,GAAG,mBAAmB,OAAO;AAG9C,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,OAAO,UAAU,GAAG;AACtE,YAAQ,KAAK,EAAE,MAAM,gBAAgB,MAAM,UAAU,WAAW,CAAC;AAAA,EACnE;AAGA,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,OAAO,WAAW,GAAG;AACvE,UAAM,WAAW,SAAS,KAAK,IAAI,KAAK,QAAQ;AAChD,QAAI,OAAO,QAAQ,GAAG;AACpB,YAAM,UAAU,aAAa,QAAQ,KAAK;AAC1C,UAAI,QAAQ,SAAS,WAAW,MAAM,GAAG;AACvC,gBAAQ,KAAK,EAAE,MAAM,gBAAgB,MAAM,UAAU,WAAW,CAAC;AAAA,MACnE;AAAA,IACF;AAAA,EACF;AAGA,QAAM,YAAY,+BAA+B,OAAO,cAAc,WAAW,GAAG,IAAI,GAAG;AAC3F,UAAQ,KAAK,GAAG,UAAU,OAAO;AACjC,cAAY,KAAK,GAAG,UAAU,OAAO;AAGrC,QAAM,QAAQ,+BAA+B,OAAO,UAAU,WAAW,GAAG,IAAI,GAAG;AACnF,UAAQ,KAAK,GAAG,MAAM,OAAO;AAC7B,cAAY,KAAK,GAAG,MAAM,OAAO;AAGjC,MAAI,MAAM;AACR,UAAM,UAAU,yBAAyB,OAAO,KAAK,OAAO,YAAY,GAAG,IAAI,GAAG;AAClF,YAAQ,KAAK,GAAG,QAAQ,OAAO;AAC/B,gBAAY,KAAK,GAAG,QAAQ,OAAO;AAAA,EACrC;AAGA,QAAM,mBAAmB,OACrB,wBAAwB,QAAQ,IAAI,aAAa,IAAI,eAAe,IACpE,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA,aAAa,CAAC;AAAA,IACd,aAAa,CAAC;AAAA,IACd;AAAA,IACA,mBAAmB,CAAC;AAAA,IACpB;AAAA,EACF;AACF;AAiBA,SAAS,YAAY,MAAqB,KAAsC;AAC9E,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAC3B,QAAM,SAAS,EAAE,SAAS,SAAS,QAAQ;AAE3C,aAAW,UAAU,KAAK,SAAS;AACjC,kBAAc,QAAQ,KAAK,MAAM;AAAA,EACnC;AAEA,SAAO;AACT;AAQA,SAAS,aAAa,KAAa,OAAuB;AACxD,aAAW,QAAQ,OAAO;AACxB,UAAM,WAAW,SAAS,KAAK,KAAK,IAAI;AACxC,QAAI,OAAO,QAAQ,EAAG,uBAAsB,QAAQ;AAAA,EACtD;AACF;AAEA,SAAS,aAAa,KAAa,MAAc,QAA+B;AAC9E,MAAI,cAAc,SAAS,KAAK,KAAK,IAAI,CAAC,EAAG,QAAO,QAAQ,KAAK,IAAI;AACvE;AAEA,SAAS,cAAc,QAAgB,KAAqB,QAA+B;AACzF,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK,SAAS;AACZ,sBAAgB,SAAS,KAAK,IAAI,KAAK,OAAO,IAAI,CAAC;AACnD,aAAO,QAAQ,KAAK,OAAO,IAAI;AAC/B;AAAA,IACF;AAAA,IACA,KAAK,SAAS;AACZ,mBAAa,IAAI,KAAK,OAAO,MAAM,MAAM;AACzC;AAAA,IACF;AAAA,IACA,KAAK,SAAS;AACZ,mBAAa,IAAI,KAAK,OAAO,MAAM,OAAO,SAAS,MAAM;AACzD;AAAA,IACF;AAAA,IACA,KAAK,MAAM;AACT,aAAO,SAAS,KAAK,IAAI,KAAK,OAAO,IAAI,CAAC;AAC1C,aAAO,QAAQ,KAAK,OAAO,IAAI;AAC/B;AAAA,IACF;AAAA,IACA,KAAK,SAAS;AACZ,mBAAa,IAAI,KAAK,OAAO,KAAK;AAClC;AAAA,IACF;AAAA,IACA,KAAK,cAAc;AACjB,uBAAiB,IAAI,KAAK,OAAO,MAAM,OAAO,YAAY,GAAG;AAC7D;AAAA,IACF;AAAA,IACA,KAAK,gBAAgB;AACnB,yBAAmB,IAAI,KAAK,OAAO,MAAM,OAAO,UAAU;AAC1D;AAAA,IACF;AAAA,IACA,KAAK,cAAc;AACjB,uBAAiB,IAAI,KAAK,OAAO,MAAM,OAAO,UAAU;AACxD;AAAA,IACF;AAAA,IACA,KAAK,gBAAgB;AACnB,yBAAmB,IAAI,KAAK,OAAO,MAAM,OAAO,UAAU;AAC1D;AAAA,IACF;AAAA,EACF;AACF;AASA,SAAS,aAAa,KAAa,MAAc,SAAiB,QAA+B;AAC/F,QAAM,WAAW,SAAS,KAAK,KAAK,IAAI;AACxC,QAAM,UAAU,OAAO,QAAQ;AAC/B,YAAU,UAAU,OAAO;AAC3B,GAAC,UAAU,OAAO,UAAU,OAAO,SAAS,KAAK,IAAI;AACvD;AAWA,SAAS,mBAAmB,YAA4B,KAA6B;AACnF,MAAI,WAAW,UAAU;AACvB,UAAM,qBAAqB,sBAAsB;AACjD,WAAO,SAAS,SAAS,KAAK,oBAAoB,WAAW,QAAQ,CAAC;AAAA,EACxE;AAEA,MAAI,WAAW,SAAS;AACtB,WAAO,OAAO,WAAW,YAAY,aAAa,WAAW,QAAQ,IAAI,WAAW;AAAA,EACtF;AAEA,MAAI,WAAW,WAAW;AACxB,WAAO,WAAW,UAAU,GAAG;AAAA,EACjC;AAEA,QAAM,IAAI,MAAM,0DAA0D;AAC5E;AAOA,SAAS,gBAAgB,eAAuB,YAA6B;AAC3E,MAAI,CAAC,OAAO,aAAa,EAAG,QAAO;AACnC,QAAM,iBAAiB,aAAa,aAAa;AACjD,SAAO,gBAAgB,KAAK,MAAM,WAAW,KAAK;AACpD;AAGA,IAAM,oBAAoB,oBAAI,IAAI,CAAC,SAAS,aAAa,CAAC;AASnD,SAAS,yBACd,QACA,aACA,0BACAA,aAAY,MACF;AACV,MAAI,SAAS,CAAC,GAAG,OAAO,SAAS,IAAI;AAGrC,MAAI,CAACA,YAAW;AACd,aAAS,OAAO,OAAO,SAAO,CAAC,kBAAkB,IAAI,GAAG,CAAC;AAAA,EAC3D;AAGA,SAAO,KAAK,GAAG,uBAAuB,OAAO,SAAS,aAAa,WAAW,CAAC;AAE/E,SAAO,OAAO,OAAO,SAAO,EAAE,OAAO,yBAAyB;AAChE;AAQA,SAAS,wBACP,QACA,aACA,0BACU;AACV,QAAM,mBAAmB;AAAA,IACvB,GAAG,OAAO,SAAS;AAAA,IACnB,GAAG,uBAAuB,OAAO,SAAS,aAAa,WAAW;AAAA,EACpE;AAGA,SAAO,iBAAiB,OAAO,SAAO,OAAO,wBAAwB;AACvE;AASA,SAAS,iBACP,KACA,MACA,YACA,KACM;AACN,QAAM,WAAW,SAAS,KAAK,KAAK,IAAI;AACxC,QAAM,cAAc,SAAS,QAAQ;AAGrC,MAAI,CAAC,eAAe,WAAW,cAAe;AAE9C,QAAM,WAAW,eAAe,CAAC;AACjC,QAAM,SAAS,WAAW,MAAM,UAAU,GAAG;AAG7C,MAAI,KAAK,UAAU,QAAQ,MAAM,KAAK,UAAU,MAAM,EAAG;AAEzD,YAAU,UAAU,MAAM;AAC5B;AAQA,SAAS,mBAAmB,KAAa,MAAc,YAAuC;AAC5F,QAAM,WAAW,SAAS,KAAK,KAAK,IAAI;AACxC,MAAI,CAAC,OAAO,QAAQ,EAAG;AAEvB,QAAM,WAAW,SAAS,QAAQ;AAClC,MAAI,CAAC,SAAU;AAEf,QAAM,WAAW,WAAW,QAAQ,QAAQ;AAG5C,MAAI,WAAW,mBAAmB;AAChC,UAAM,gBAAgB,OAAO,KAAK,QAAQ,EAAE,OAAO,OAAK,SAAS,CAAC,MAAM,MAAS;AACjF,QAAI,cAAc,WAAW,GAAG;AAC9B,aAAO,QAAQ;AACf;AAAA,IACF;AAAA,EACF;AAEA,YAAU,UAAU,QAAQ;AAC9B;AAQA,SAAS,iBAAiB,KAAa,MAAc,YAAuC;AAC1F,QAAM,WAAW,SAAS,KAAK,KAAK,IAAI;AACxC,MAAI,UAAU,aAAa,QAAQ,KAAK;AAGxC,MAAI,QAAQ,SAAS,WAAW,MAAM,EAAG;AAGzC,YACE,WAAW,cAAc,YACrB,WAAW,UAAU,UACrB,UAAU,WAAW;AAE3B,YAAU,UAAU,OAAO;AAC7B;AAQA,SAAS,mBAAmB,KAAa,MAAc,YAAuC;AAC5F,QAAM,WAAW,SAAS,KAAK,KAAK,IAAI;AACxC,QAAM,UAAU,aAAa,QAAQ;AACrC,MAAI,CAAC,QAAS;AAId,MAAI,YAAY,QAAQ,QAAQ,WAAW,SAAS,EAAE;AAGtD,MAAI,cAAc,WAAW,QAAQ,SAAS,WAAW,MAAM,GAAG;AAEhE,UAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,UAAM,WAAW,MAAM,OAAO,UAAQ,CAAC,KAAK,SAAS,WAAW,MAAM,CAAC;AACvE,gBAAY,SAAS,KAAK,IAAI,EAAE,QAAQ,QAAQ,EAAE;AAAA,EACpD;AAEA,YAAU,UAAU,SAAS;AAC/B;;;ACt3BO,SAAS,gBAAgBC,wBAAuB,OAAe;AACpE,MAAIA,uBAAsB;AACxB,WAAO,iCAAiC;AAAA,EAC1C;AACA,SAAO,wBAAwB;AACjC;AAKA,SAAS,0BAAkC;AACzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+BT;AAOA,SAAS,mCAA2C;AAClD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6BT;AAIO,IAAM,eAAe;AAAA,EAC1B,eAAe,CAAC,EAAE,SAAS,8CAA8C,CAAC;AAAA,EAC1E,MAAM,CAAC,EAAE,SAAS,mCAAmC,CAAC;AACxD;AAGO,IAAM,iBAAiB;AAAA,EAC5B,cAAc;AAAA,IACZ;AAAA,MACE,OAAO;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,kBAAkB;AAAA,IAChB;AAAA,MACE,OAAO;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE,OAAO;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM;AAAA,IACJ;AAAA,MACE,OAAO;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,aAAa;AAAA,IACX;AAAA,MACE,SAAS;AAAA,MACT,OAAO;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACpKO,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACUvB,SAAS,YAAY,GAA4B;AACtD,SACE,OAAO,MAAM,YAAY,MAAM,QAAQ,WAAW,KAAK,MAAM,QAAS,EAAgB,KAAK;AAE/F;AAMO,SAAS,eAAe,GAAqB;AAClD,MAAI,CAAC,YAAY,CAAC,EAAG,QAAO;AAC5B,SAAO,EAAE,MAAM,KAAK,SAAO,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,SAAS,WAAW,CAAC;AACjG;AAMO,SAAS,uBAAuB,OAA6B;AAClE,SAAO,MAAM,OAAO,OAAK,CAAC,eAAe,CAAC,CAAC;AAC7C;;;AC7BO,IAAM,cAAc;AAAA,EACzB,UAAU;AAAA,IACR,SAAS;AAAA,IACT,MAAM,CAAC,MAAM,8BAA8B;AAAA,EAC7C;AAAA,EACA,YAAY;AAAA,IACV,SAAS;AAAA,IACT,MAAM,CAAC,wBAAwB;AAAA,EACjC;AACF;;;AC2DA,IAAM,mBAAwC;AAAA,EAC5C,MAAM,CAAC,gBAAgB;AAAA,EACvB,eAAe;AAAA;AAAA,EACf,OAAO,cAAY;AACjB,UAAM,QAAS,SAAS,SAAqC,CAAC;AAC9D,UAAM,mBAAmB,MAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,WAAW,CAAC;AAI3E,UAAM,mBAAmB,CAAC,sBAAsB,YAAY;AAC5D,UAAM,cAAc,CAAC,GAAG,gBAAgB;AACxC,eAAW,WAAW,kBAAkB;AACtC,UAAI,CAAC,YAAY,SAAS,OAAO,GAAG;AAClC,oBAAY,KAAK,OAAO;AAAA,MAC1B;AAAA,IACF;AAEA,WAAO;AAAA,MACL,GAAG;AAAA,MACH,OAAO;AAAA,QACL,GAAG;AAAA,QACH,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS,cAAY;AACnB,UAAM,SAAS,EAAE,GAAG,SAAS;AAC7B,UAAM,QAAS,SAAS,SAAqC,CAAC;AAC9D,UAAM,mBAAmB,MAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,WAAW,CAAC;AAG3E,UAAM,mBAAmB,oBAAI,IAAI,CAAC,sBAAsB,cAAc,eAAe,CAAC;AACtF,UAAM,kBAAkB,iBAAiB;AAAA,MACvC,CAAC,UAAkB,CAAC,iBAAiB,IAAI,KAAK;AAAA,IAChD;AAEA,QAAI,gBAAgB,SAAS,GAAG;AAC9B,YAAM,WAAW;AACjB,aAAO,QAAQ;AAAA,IACjB,OAAO;AACL,aAAO,MAAM;AACb,UAAI,OAAO,KAAK,KAAK,EAAE,SAAS,GAAG;AACjC,eAAO,QAAQ;AAAA,MACjB,OAAO;AACL,eAAO,OAAO;AAAA,MAChB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;AAMO,IAAM,kBAAkC;AAAA,EAC7C,SAAS;AAAA;AAAA,EAGT,WAAW;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA;AAAA,EAGA,YAAY,CAAC,WAAW,kBAAkB,kBAAkB;AAAA;AAAA,EAG5D,eAAe;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA;AAAA,EAGA,iBAAiB;AAAA,IACf;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,EACF;AAAA;AAAA,EAGA,oBAAoB;AAAA;AAAA,IAElB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,EACF;AAAA;AAAA,EAGA,gBAAgB;AAAA,IACd;AAAA;AAAA,EACF;AAAA;AAAA,EAGA,YAAY;AAAA;AAAA,IAEV,yBAAyB,EAAE,UAAU,cAAc;AAAA,IACnD,qBAAqB,EAAE,SAAS,MAAM,QAAQ;AAAA;AAAA,IAG9C,4CAA4C,EAAE,UAAU,iCAAiC;AAAA,IACzF,sCAAsC,EAAE,UAAU,2BAA2B;AAAA,IAC7E,yCAAyC,EAAE,UAAU,8BAA8B;AAAA,IACnF,uCAAuC,EAAE,UAAU,4BAA4B;AAAA,IAC/E,uCAAuC,EAAE,UAAU,4BAA4B;AAAA,IAC/E,qCAAqC,EAAE,UAAU,0BAA0B;AAAA,IAC3E,mCAAmC,EAAE,UAAU,wBAAwB;AAAA;AAAA,IAGvE,2BAA2B,EAAE,UAAU,gBAAgB;AAAA,IACvD,gCAAgC,EAAE,UAAU,qBAAqB;AAAA;AAAA,IAGjE,0CAA0C,EAAE,UAAU,+BAA+B;AAAA,IACrF,qCAAqC,EAAE,UAAU,0BAA0B;AAAA,IAC3E,uCAAuC,EAAE,UAAU,4BAA4B;AAAA,IAC/E,2CAA2C,EAAE,UAAU,gCAAgC;AAAA,IACvF,+CAA+C;AAAA,MAC7C,UAAU;AAAA,IACZ;AAAA,IACA,wCAAwC,EAAE,UAAU,6BAA6B;AAAA,IACjF,2CAA2C,EAAE,UAAU,gCAAgC;AAAA,IACvF,iCAAiC,EAAE,UAAU,sBAAsB;AAAA,IACnE,sCAAsC,EAAE,UAAU,2BAA2B;AAAA,IAC7E,qCAAqC,EAAE,UAAU,0BAA0B;AAAA,IAC3E,8CAA8C,EAAE,UAAU,mCAAmC;AAAA;AAAA,IAG7F,gDAAgD;AAAA,MAC9C,UAAU;AAAA,IACZ;AAAA,IACA,8CAA8C;AAAA,MAC5C,UAAU;AAAA,IACZ;AAAA,IACA,6CAA6C;AAAA,MAC3C,UAAU;AAAA,IACZ;AAAA,IACA,mDAAmD;AAAA,MACjD,UAAU;AAAA,IACZ;AAAA,IACA,0CAA0C,EAAE,UAAU,mCAAmC;AAAA,IACzF,gDAAgD;AAAA,MAC9C,UAAU;AAAA,IACZ;AAAA,IACA,4CAA4C,EAAE,UAAU,qCAAqC;AAAA;AAAA,IAG7F,qCAAqC,EAAE,UAAU,0BAA0B;AAAA,IAC3E,uCAAuC,EAAE,UAAU,4BAA4B;AAAA;AAAA,IAG/E,8CAA8C,EAAE,UAAU,mCAAmC;AAAA,IAC7F,gDAAgD;AAAA,MAC9C,UAAU;AAAA,IACZ;AAAA,IACA,wCAAwC,EAAE,UAAU,6BAA6B;AAAA;AAAA,IAGjF,kDAAkD;AAAA,MAChD,UAAU;AAAA,IACZ;AAAA,IACA,8CAA8C;AAAA,MAC5C,UAAU;AAAA,IACZ;AAAA,IACA,kDAAkD;AAAA,MAChD,UAAU;AAAA,IACZ;AAAA,IACA,qDAAqD;AAAA,MACnD,UAAU;AAAA,IACZ;AAAA,IACA,gDAAgD;AAAA,MAC9C,UAAU;AAAA,IACZ;AAAA,IACA,kDAAkD;AAAA,MAChD,UAAU;AAAA,IACZ;AAAA,IACA,oCAAoC,EAAE,UAAU,2BAA2B;AAAA,IAC3E,6BAA6B,EAAE,UAAU,oBAAoB;AAAA,IAC7D,uCAAuC,EAAE,UAAU,8BAA8B;AAAA,IACjF,4BAA4B,EAAE,UAAU,mBAAmB;AAAA,IAC3D,sCAAsC,EAAE,UAAU,6BAA6B;AAAA;AAAA,IAG/E,mCAAmC,EAAE,UAAU,iCAAiC;AAAA,IAChF,4CAA4C;AAAA,MAC1C,UAAU;AAAA,IACZ;AAAA,IACA,wCAAwC;AAAA,MACtC,UAAU;AAAA,IACZ;AAAA,IACA,4CAA4C;AAAA,MAC1C,UAAU;AAAA,IACZ;AAAA,IACA,+CAA+C;AAAA,MAC7C,UAAU;AAAA,IACZ;AAAA,IACA,0CAA0C;AAAA,MACxC,UAAU;AAAA,IACZ;AAAA,IACA,4CAA4C;AAAA,MAC1C,UAAU;AAAA,IACZ;AAAA;AAAA,IAGA,oCAAoC,EAAE,UAAU,2BAA2B;AAAA,IAC3E,6BAA6B,EAAE,UAAU,oBAAoB;AAAA,IAC7D,uCAAuC,EAAE,UAAU,8BAA8B;AAAA,IACjF,4BAA4B,EAAE,UAAU,mBAAmB;AAAA,IAC3D,sCAAsC,EAAE,UAAU,6BAA6B;AAAA;AAAA,IAG/E,6CAA6C,EAAE,UAAU,kCAAkC;AAAA,IAC3F,kCAAkC,EAAE,UAAU,uBAAuB;AAAA,EACvE;AAAA;AAAA,EAGA,cAAc;AAAA,IACZ,qBAAqB;AAAA,MACnB,WAAW,SAAO,gBAAgB,IAAI,YAAY,iBAAiB;AAAA,IACrE;AAAA;AAAA,IAEA,iBAAiB;AAAA,MACf,WAAW,SAAO;AAEhB,YAAI,CAAC,IAAI,gBAAgB,cAAc,CAAC,IAAI,gBAAgB,mBAAmB,GAAG;AAChF,iBAAO;AAAA,QACT;AACA,eAAO,KAAK;AAAA,UACV;AAAA,YACE,iBAAiB;AAAA,cACf,QAAQ;AAAA,cACR,QAAQ;AAAA,cACR,kBAAkB;AAAA,cAClB,QAAQ;AAAA,cACR,iBAAiB;AAAA,cACjB,cAAc;AAAA,cACd,QAAQ;AAAA,YACV;AAAA,YACA,SAAS,CAAC,WAAW,UAAU;AAAA,YAC/B,SAAS,CAAC,gBAAgB,QAAQ,OAAO;AAAA,UAC3C;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA;AAAA,IAEA,aAAa;AAAA,MACX,WAAW,MACT,KAAK;AAAA,QACH;AAAA,UACE,QAAQ,CAAC,cAAc;AAAA,UACvB,oBAAoB,CAAC,wBAAwB;AAAA,QAC/C;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACJ;AAAA,EACF;AAAA;AAAA,EAGA,YAAY;AAAA,IACV,gBAAgB;AAAA,MACd,MAAM,CAAC,gBAAgB,kBAAkB,wBAAwB,cAAc;AAAA,MAC/E,iBAAiB;AAAA,QACf,gBAAgB,CAAC,qBAAqB;AAAA;AAAA,QACtC,oBAAoB,CAAC,iBAAiB;AAAA,QACtC,OAAO,CAAC,iBAAiB;AAAA,MAC3B;AAAA,MACA,OAAO,CAAC,UAAU,QAAQ;AACxB,cAAM,UAAU,EAAE,GAAI,SAAS,QAAmC;AAClE,cAAM,SAAS,EAAE,GAAG,SAAS;AAE7B,YAAI,IAAI,YAAY,gBAAgB;AAElC,cAAI,CAAC,QAAQ,aAAa,EAAG,SAAQ,aAAa,IAAI;AAAA,QAExD,OAAO;AAEL,cAAI,CAAC,QAAQ,KAAM,SAAQ,OAAO;AAAA,QACpC;AAEA,YAAI,CAAC,IAAI,YAAY,mBAAmB;AAEtC,cAAI,CAAC,QAAQ,OAAQ,SAAQ,SAAS;AACtC,cAAI,CAAC,QAAQ,cAAc,EAAG,SAAQ,cAAc,IAAI;AAAA,QAC1D;AAGA,YAAI,CAAC,QAAQ,KAAM,SAAQ,OAAO;AAGlC,YAAI,IAAI,YAAY,sBAAsB,CAAC,QAAQ,SAAS;AAC1D,kBAAQ,UAAU;AAAA,QACpB;AAGA,YAAI,IAAI,YAAY,SAAS,CAAC,QAAQ,SAAS,GAAG;AAChD,kBAAQ,SAAS,IAAI;AAAA,QACvB;AAEA,eAAO,UAAU;AAEjB,eAAO;AAAA,MACT;AAAA,MACA,SAAS,cAAY;AACnB,cAAM,SAAS,EAAE,GAAG,SAAS;AAC7B,cAAM,UAAU,EAAE,GAAI,SAAS,QAAmC;AAGlE,eAAO,QAAQ,aAAa;AAC5B,eAAO,QAAQ,SAAS;AACxB,eAAO,QAAQ,cAAc;AAC7B,eAAO,QAAQ;AACf,eAAO,QAAQ;AAEf,YAAI,OAAO,KAAK,OAAO,EAAE,SAAS,GAAG;AACnC,iBAAO,UAAU;AAAA,QACnB,OAAO;AACL,iBAAO,OAAO;AAAA,QAChB;AAEA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,yBAAyB;AAAA,MACvB,MAAM,CAAC,OAAO;AAAA,MACd,OAAO,cAAY;AAEjB,cAAM,gBAAiB,SAAS,SAAuC,CAAC;AACxE,cAAM,cAAyC,EAAE,GAAG,cAAc;AAElE,mBAAW,CAAC,OAAO,QAAQ,KAAK,OAAO,QAAQ,cAAc,GAAG;AAC9D,gBAAM,aAAa,YAAY,KAAK,KAAK,CAAC;AAC1C,gBAAM,mBAAmB,uBAAuB,UAAU;AAC1D,sBAAY,KAAK,IAAI,CAAC,GAAG,kBAAkB,GAAG,QAAQ;AAAA,QACxD;AAEA,eAAO,EAAE,GAAG,UAAU,OAAO,YAAY;AAAA,MAC3C;AAAA,MACA,SAAS,cAAY;AAEnB,cAAM,gBAAiB,SAAS,SAAuC,CAAC;AACxE,cAAM,eAA0C,CAAC;AAEjD,mBAAW,CAAC,OAAO,UAAU,KAAK,OAAO,QAAQ,aAAa,GAAG;AAC/D,gBAAM,mBAAmB,uBAAuB,UAAU;AAC1D,cAAI,iBAAiB,SAAS,GAAG;AAC/B,yBAAa,KAAK,IAAI;AAAA,UACxB;AAAA,QACF;AAEA,cAAM,SAAS,EAAE,GAAG,SAAS;AAC7B,YAAI,OAAO,KAAK,YAAY,EAAE,SAAS,GAAG;AACxC,iBAAO,QAAQ;AAAA,QACjB,OAAO;AACL,iBAAO,OAAO;AAAA,QAChB;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,aAAa;AAAA,MACX,MAAM,CAAC,uBAAuB,uBAAuB;AAAA,MACrD,mBAAmB;AAAA,MACnB,OAAO,cAAY;AACjB,cAAM,aAAc,SAAS,cAA0C,CAAC;AACxE,eAAO;AAAA,UACL,GAAG;AAAA,UACH,YAAY;AAAA,YACV,GAAG;AAAA,YACH,UAAU,YAAY;AAAA,YACtB,YAAY,YAAY;AAAA,UAC1B;AAAA,QACF;AAAA,MACF;AAAA,MACA,SAAS,cAAY;AACnB,cAAM,SAAS,EAAE,GAAG,SAAS;AAC7B,cAAM,aAAa,EAAE,GAAI,SAAS,WAAuC;AAEzE,eAAO,WAAW;AAClB,eAAO,WAAW;AAElB,YAAI,OAAO,KAAK,UAAU,EAAE,SAAS,GAAG;AACtC,iBAAO,aAAa;AAAA,QACtB,OAAO;AACL,iBAAO,OAAO;AAAA,QAChB;AAEA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,oBAAoB;AAAA,MAClB,MAAM,CAAC,uBAAuB,uBAAuB;AAAA,MACrD,mBAAmB;AAAA,MACnB,OAAO,cAAY;AACjB,cAAM,aAAc,SAAS,cAA0C,CAAC;AACxE,eAAO;AAAA,UACL,GAAG;AAAA,UACH,YAAY;AAAA,YACV,GAAG;AAAA,YACH,UAAU,YAAY;AAAA,YACtB,YAAY,YAAY;AAAA,UAC1B;AAAA,QACF;AAAA,MACF;AAAA,MACA,SAAS,cAAY;AACnB,cAAM,SAAS,EAAE,GAAG,SAAS;AAC7B,cAAM,aAAa,EAAE,GAAI,SAAS,WAAuC;AAEzE,eAAO,WAAW;AAClB,eAAO,WAAW;AAElB,YAAI,OAAO,KAAK,UAAU,EAAE,SAAS,GAAG;AACtC,iBAAO,aAAa;AAAA,QACtB,OAAO;AACL,iBAAO,OAAO;AAAA,QAChB;AAEA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,sBAAsB;AAAA,MACpB,MAAM,CAAC,WAAW,uBAAuB,YAAY;AAAA,MACrD,mBAAmB;AAAA,MACnB,OAAO,cAAY;AACjB,cAAM,QAAS,SAAS,SAAuC,CAAC;AAChE,eAAO;AAAA,UACL,GAAG;AAAA,UACH,SAAS;AAAA;AAAA,UACT,OAAO;AAAA,YACL,GAAG;AAAA,YACH,GAAG;AAAA,UACL;AAAA,QACF;AAAA,MACF;AAAA,MACA,SAAS,cAAY;AACnB,cAAM,SAAS,EAAE,GAAG,SAAS;AAC7B,cAAM,QAAQ,EAAE,GAAI,SAAS,MAAoC;AAEjE,eAAO,MAAM;AACb,eAAO,MAAM;AAEb,YAAI,OAAO,KAAK,KAAK,EAAE,SAAS,GAAG;AACjC,iBAAO,QAAQ;AAAA,QACjB,OAAO;AACL,iBAAO,OAAO;AACd,iBAAO,OAAO;AAAA,QAChB;AAEA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,eAAe;AAAA,MACb,MAAM,CAAC,SAAS;AAAA,MAChB,OAAO,CAAC,UAAU,QAAQ;AACxB,cAAM,SAAS,EAAE,GAAG,SAAS;AAI7B,YAAI,OAAO,SAAS,OAAW,QAAO,OAAO;AAC7C,YAAI,OAAO,gBAAgB,OAAW,QAAO,cAAc;AAC3D,YAAI,OAAO,aAAa,OAAW,QAAO,WAAW;AACrD,YAAI,OAAO,kBAAkB,OAAW,QAAO,gBAAgB;AAC/D,YAAI,OAAO,eAAe,OAAW,QAAO,aAAa;AACzD,YAAI,OAAO,cAAc,OAAW,QAAO,YAAY;AACvD,YAAI,OAAO,YAAY,OAAW,QAAO,UAAU;AACnD,YAAI,OAAO,mBAAmB,OAAW,QAAO,iBAAiB;AACjE,YAAI,OAAO,gBAAgB,OAAW,QAAO,cAAc;AAG3D,cAAM,UAAoB,CAAC;AAC3B,YAAI,IAAI,YAAY,MAAO,SAAQ,KAAK,uBAAuB;AAC/D,YAAI,IAAI,YAAY,MAAO,SAAQ,KAAK,oBAAoB;AAE5D,YAAI,IAAI,YAAY,SAAU,SAAQ,KAAK,6BAA6B;AAExE,YAAI,QAAQ,SAAS,GAAG;AACtB,iBAAO,UAAU;AAAA,QACnB,OAAO;AACL,iBAAO,OAAO;AAAA,QAChB;AAEA,eAAO;AAAA,MACT;AAAA,MACA,SAAS,cAAY;AACnB,cAAM,SAAS,EAAE,GAAG,SAAS;AAE7B,eAAO,OAAO;AACd,eAAO;AAAA,MACT;AAAA,IACF;AAAA;AAAA;AAAA;AAAA,IAKA,cAAc;AAAA,IACd,eAAe;AAAA,EACjB;AAAA;AAAA,EAGA,aAAa;AAAA,IACX,aAAa;AAAA,MACX,WAAW;AAAA,MACX,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,iBAAiB;AAAA,IACnB;AAAA,IACA,aAAa;AAAA,MACX,WAAW;AAAA,MACX,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,iBAAiB;AAAA;AAAA,IACnB;AAAA,EACF;AAAA;AAAA,EAGA,UAAU;AAAA,IACR,MAAM;AAAA;AAAA,MAEJ;AAAA;AAAA,MAEA;AAAA;AAAA,MAEA;AAAA,MACA;AAAA,IACF;AAAA,IACA,aAAa;AAAA;AAAA,MAEX,UAAU,CAAC,UAAU;AAAA;AAAA;AAAA,MAErB,OAAO,CAAC,uBAAuB;AAAA,MAC/B,UAAU,CAAC,6BAA6B;AAAA,MACxC,OAAO,CAAC,oBAAoB;AAAA;AAAA,MAE5B,oBAAoB,CAAC,SAAS;AAAA,MAC9B,YAAY,CAAC,YAAY;AAAA;AAAA,IAC3B;AAAA,EACF;AACF;;;ACvpBA,OAAOC,eAAc;AAQd,SAAS,UAAU,KAAsB;AAC9C,SAAO,OAAOC,UAAS,KAAK,KAAK,MAAM,CAAC;AAC1C;;;ACRA,OAAOC,eAAc;;;ACCrB,SAAS,mBAAmB;AAC5B,OAAOC,eAAc;AAErB,SAAS,cAAc;AAGhB,IAAM;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,IAAI;AAqCG,SAAS,gBAAgB,KAAa,WAAW,GAAY;AAClE,QAAM,qBAAqB,oBAAI,IAAI,CAAC,gBAAgB,QAAQ,WAAW,CAAC;AAOxE,WAAS,KAAK,KAAa,OAAwB;AACjD,QAAI,QAAQ,SAAU,QAAO;AAE7B,QAAI;AACF,YAAM,UAAU,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AACxD,iBAAW,SAAS,SAAS;AAC3B,YAAI,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS,KAAK,GAAG;AAChD,iBAAO;AAAA,QACT;AACA,YACE,MAAM,YAAY,KAClB,CAAC,mBAAmB,IAAI,MAAM,IAAI,KAClC,KAAKA,UAAS,KAAK,KAAK,MAAM,IAAI,GAAG,QAAQ,CAAC,GAC9C;AACA,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,KAAK,CAAC;AACpB;AAWO,SAAS,kBAAkB,aAAqC,KAA2B;AAChG,QAAM,OAAO,YAAY,gBAAgB,CAAC;AAC1C,QAAM,kBAAkB,YAAY,mBAAmB,CAAC;AACxD,QAAM,UAAU,EAAE,GAAG,MAAM,GAAG,gBAAgB;AAC9C,QAAM,UAAU,YAAY,WAAW,CAAC;AAExC,QAAM,gBAAgB,gBAAgB;AACtC,QAAM,WAAW,WAAW,QAAQ,WAAW;AAC/C,QAAM,YAAY,UAAU;AAC5B,QAAM,WAAW,WAAW,QAAQ,WAAW;AAC/C,QAAM,YAAY,YAAY;AAC9B,QAAM,gBAAgB,sBAAsB;AAE5C,QAAM,cAAc,kBAAkB,KAAK,SAAO,OAAO,OAAO;AAGhE,QAAM,mBAAmB,wBAAwB,KAAK,SAAO,OAAO,OAAO;AAG3E,QAAM,iBAAiB,CAAC,EAAE,YAAY,QAAQ,YAAY,UAAU,YAAY;AAChF,QAAM,gBAAgB,kBAAkB,YAAY,YAAY;AAGhE,QAAM,WAAW,MAAM,gBAAgB,GAAG,IAAI;AAG9C,QAAM,YAAY,kBAAkB,OAAO;AAC3C,QAAM,eAAe,MAAM,qBAAqB,KAAK,OAAO,IAAI,YAAY;AAE5E,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,OAAO,YAAY;AAAA;AAAA,IACnB,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,eAAe;AAAA,IACf,oBAAoB;AAAA,IACpB,OAAO;AAAA,IACP,gBAAgB;AAAA,IAChB,mBAAmB;AAAA,EACrB;AACF;;;AD5HO,SAAS,qBAAqB,KAA6B;AAChE,QAAM,cAAc,SAASC,UAAS,KAAK,KAAK,cAAc,CAAC;AAE/D,SAAO;AAAA,IACL;AAAA,IACA,aAAa,kBAAkB,eAAe,CAAC,GAAG,GAAG;AAAA,IACrD,iBAAiB,aAAa,mBAAmB,CAAC;AAAA,IAClD,WAAW,UAAU,GAAG;AAAA,EAC1B;AACF;","names":["isGitRepo","hasExistingFormatter","nodePath","nodePath","nodePath","nodePath","nodePath"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/setup.ts"],"sourcesContent":["/**\n * Setup command - Initialize safeword in a project\n *\n * Uses reconcile() with mode='install' to create all managed files.\n */\n\nimport { execSync } from 'node:child_process';\nimport nodePath from 'node:path';\n\nimport { reconcile, type ReconcileResult } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { exists, writeJson } from '../utils/fs.js';\nimport { isGitRepo } from '../utils/git.js';\nimport { error, header, info, listItem, success, warn } from '../utils/output.js';\nimport { VERSION } from '../version.js';\nimport { buildArchitecture, hasArchitectureDetected, syncConfigCore } from './sync-config.js';\n\nexport interface SetupOptions {\n yes?: boolean;\n}\n\ninterface PackageJson {\n name?: string;\n version?: string;\n scripts?: Record<string, string>;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n 'lint-staged'?: Record<string, string[]>;\n}\n\nfunction ensurePackageJson(cwd: string): boolean {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n if (exists(packageJsonPath)) return false;\n\n const dirName = nodePath.basename(cwd) || 'project';\n const defaultPackageJson: PackageJson = { name: dirName, version: '0.1.0', scripts: {} };\n writeJson(packageJsonPath, defaultPackageJson);\n return true;\n}\n\nfunction installDependencies(cwd: string, packages: string[]): void {\n if (packages.length === 0) return;\n\n info('\\nInstalling linting dependencies...');\n const installCmd = `npm install -D ${packages.join(' ')}`;\n info(`Running: ${installCmd}`);\n\n try {\n execSync(installCmd, { cwd, stdio: 'inherit' });\n success('Installed linting dependencies');\n } catch {\n warn('Failed to install dependencies. Run manually:');\n listItem(installCmd);\n }\n}\n\nfunction printSetupSummary(\n result: ReconcileResult,\n packageJsonCreated: boolean,\n archFiles: string[] = [],\n): void {\n header('Setup Complete');\n\n const allCreated = [...result.created, ...archFiles];\n if (allCreated.length > 0 || packageJsonCreated) {\n info('\\nCreated:');\n if (packageJsonCreated) listItem('package.json');\n for (const file of allCreated) listItem(file);\n }\n\n if (result.updated.length > 0) {\n info('\\nModified:');\n for (const file of result.updated) listItem(file);\n }\n\n info('\\nNext steps:');\n listItem('Run `safeword check` to verify setup');\n listItem('Commit the new files to git');\n\n success(`\\nSafeword ${VERSION} installed successfully!`);\n}\n\nexport async function setup(options: SetupOptions): Promise<void> {\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n if (exists(safewordDirectory)) {\n error('Already configured. Run `safeword upgrade` to update.');\n process.exit(1);\n }\n\n const packageJsonCreated = ensurePackageJson(cwd);\n\n header('Safeword Setup');\n info(`Version: ${VERSION}`);\n if (packageJsonCreated) info('Created package.json (none found)');\n\n try {\n info('\\nCreating safeword configuration...');\n const ctx = createProjectContext(cwd);\n const result = await reconcile(SAFEWORD_SCHEMA, 'install', ctx);\n success('Created .safeword directory and configuration');\n\n // Detect architecture and workspaces, generate depcruise configs if found\n const arch = buildArchitecture(cwd);\n const archFiles: string[] = [];\n\n if (hasArchitectureDetected(arch)) {\n const syncResult = syncConfigCore(cwd, arch);\n if (syncResult.generatedConfig) archFiles.push('.safeword/depcruise-config.js');\n if (syncResult.createdMainConfig) archFiles.push('.dependency-cruiser.js');\n\n const detected: string[] = [];\n if (arch.elements.length > 0) {\n detected.push(arch.elements.map(element => element.location).join(', '));\n }\n if (arch.workspaces && arch.workspaces.length > 0) {\n detected.push(`workspaces: ${arch.workspaces.join(', ')}`);\n }\n info(`\\nArchitecture detected: ${detected.join('; ')}`);\n info('Generated dependency-cruiser config for /audit command');\n }\n\n installDependencies(cwd, result.packagesToInstall);\n\n if (!isGitRepo(cwd)) {\n const isNonInteractive = options.yes || !process.stdin.isTTY;\n warn(\n isNonInteractive\n ? 'Skipped Husky setup (no git repository)'\n : 'Skipped Husky setup (no .git directory)',\n );\n if (!isNonInteractive)\n info('Initialize git and run safeword upgrade to enable pre-commit hooks');\n }\n\n printSetupSummary(result, packageJsonCreated, archFiles);\n } catch (error_) {\n error(`Setup failed: ${error_ instanceof Error ? error_.message : 'Unknown error'}`);\n process.exit(1);\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/upgrade.ts"],"sourcesContent":["/**\n * Upgrade command - Update safeword configuration to latest version\n *\n * Uses reconcile() with mode='upgrade' to update all managed files.\n */\n\nimport nodePath from 'node:path';\n\nimport { reconcile, type ReconcileResult } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { exists, readFileSafe } from '../utils/fs.js';\nimport { error, header, info, listItem, success, warn } from '../utils/output.js';\nimport { compareVersions } from '../utils/version.js';\nimport { VERSION } from '../version.js';\n\nfunction getProjectVersion(safewordDirectory: string): string {\n const versionPath = nodePath.join(safewordDirectory, 'version');\n return readFileSafe(versionPath)?.trim() ?? '0.0.0';\n}\n\nfunction printUpgradeSummary(result: ReconcileResult, projectVersion: string): void {\n header('Upgrade Complete');\n info(`\\nVersion: v${projectVersion} → v${VERSION}`);\n\n if (result.created.length > 0) {\n info('\\nCreated:');\n for (const file of result.created) listItem(file);\n }\n\n if (result.updated.length > 0) {\n info('\\nUpdated:');\n for (const file of result.updated) listItem(file);\n }\n\n if (result.packagesToRemove.length > 0) {\n warn(\n `\\n${result.packagesToRemove.length} package(s) are now bundled in eslint-plugin-safeword:`,\n );\n for (const pkg of result.packagesToRemove) listItem(pkg);\n info(\"\\nIf you don't use these elsewhere, you can remove them:\");\n listItem(`npm uninstall ${result.packagesToRemove.join(' ')}`);\n }\n\n success(`\\nSafeword upgraded to v${VERSION}`);\n}\n\nexport async function upgrade(): Promise<void> {\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n if (!exists(safewordDirectory)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n const projectVersion = getProjectVersion(safewordDirectory);\n\n if (compareVersions(VERSION, projectVersion) < 0) {\n error(`CLI v${VERSION} is older than project v${projectVersion}.`);\n error('Update the CLI first: npm install -g safeword');\n process.exit(1);\n }\n\n header('Safeword Upgrade');\n info(`Upgrading from v${projectVersion} to v${VERSION}`);\n\n try {\n const ctx = createProjectContext(cwd);\n const result = await reconcile(SAFEWORD_SCHEMA, 'upgrade', ctx);\n printUpgradeSummary(result, projectVersion);\n } catch (error_) {\n error(`Upgrade failed: ${error_ instanceof Error ? error_.message : 'Unknown error'}`);\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAMA,OAAO,cAAc;AAUrB,SAAS,kBAAkB,mBAAmC;AAC5D,QAAM,cAAc,SAAS,KAAK,mBAAmB,SAAS;AAC9D,SAAO,aAAa,WAAW,GAAG,KAAK,KAAK;AAC9C;AAEA,SAAS,oBAAoB,QAAyB,gBAA8B;AAClF,SAAO,kBAAkB;AACzB,OAAK;AAAA,YAAe,cAAc,YAAO,OAAO,EAAE;AAElD,MAAI,OAAO,QAAQ,SAAS,GAAG;AAC7B,SAAK,YAAY;AACjB,eAAW,QAAQ,OAAO,QAAS,UAAS,IAAI;AAAA,EAClD;AAEA,MAAI,OAAO,QAAQ,SAAS,GAAG;AAC7B,SAAK,YAAY;AACjB,eAAW,QAAQ,OAAO,QAAS,UAAS,IAAI;AAAA,EAClD;AAEA,MAAI,OAAO,iBAAiB,SAAS,GAAG;AACtC;AAAA,MACE;AAAA,EAAK,OAAO,iBAAiB,MAAM;AAAA,IACrC;AACA,eAAW,OAAO,OAAO,iBAAkB,UAAS,GAAG;AACvD,SAAK,0DAA0D;AAC/D,aAAS,iBAAiB,OAAO,iBAAiB,KAAK,GAAG,CAAC,EAAE;AAAA,EAC/D;AAEA,UAAQ;AAAA,wBAA2B,OAAO,EAAE;AAC9C;AAEA,eAAsB,UAAyB;AAC7C,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,oBAAoB,SAAS,KAAK,KAAK,WAAW;AAExD,MAAI,CAAC,OAAO,iBAAiB,GAAG;AAC9B,UAAM,6CAA6C;AACnD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,iBAAiB,kBAAkB,iBAAiB;AAE1D,MAAI,gBAAgB,SAAS,cAAc,IAAI,GAAG;AAChD,UAAM,QAAQ,OAAO,2BAA2B,cAAc,GAAG;AACjE,UAAM,+CAA+C;AACrD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,SAAO,kBAAkB;AACzB,OAAK,mBAAmB,cAAc,QAAQ,OAAO,EAAE;AAEvD,MAAI;AACF,UAAM,MAAM,qBAAqB,GAAG;AACpC,UAAM,SAAS,MAAM,UAAU,iBAAiB,WAAW,GAAG;AAC9D,wBAAoB,QAAQ,cAAc;AAAA,EAC5C,SAAS,QAAQ;AACf,UAAM,mBAAmB,kBAAkB,QAAQ,OAAO,UAAU,eAAe,EAAE;AACrF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
@@ -1,58 +0,0 @@
1
- #!/bin/bash
2
- # Safeword: Cursor adapter for afterFileEdit
3
- # Auto-lints changed files, only outputs unfixable errors
4
- # Sets marker file for stop hook to trigger quality review
5
-
6
- # Require jq for JSON parsing
7
- command -v jq &> /dev/null || exit 0
8
-
9
- input=$(cat)
10
-
11
- # Get workspace root and file path from Cursor's JSON format
12
- workspace=$(echo "$input" | jq -r '.workspace_roots[0] // empty' 2>/dev/null)
13
- file=$(echo "$input" | jq -r '.file_path // empty' 2>/dev/null)
14
- conv_id=$(echo "$input" | jq -r '.conversation_id // "default"' 2>/dev/null)
15
-
16
- # Exit silently if no file
17
- [ -z "$file" ] || [ ! -f "$file" ] && exit 0
18
-
19
- # Change to workspace directory
20
- [ -n "$workspace" ] && cd "$workspace" || true
21
-
22
- # Check for .safeword directory
23
- if [ ! -d ".safeword" ]; then
24
- exit 0
25
- fi
26
-
27
- # Set marker file for stop hook to know edits were made
28
- touch "/tmp/safeword-cursor-edited-${conv_id}"
29
-
30
- # Determine linter based on file extension
31
- case "$file" in
32
- # JS/TS and framework files - ESLint first (fix code), then Prettier (format)
33
- *.js|*.jsx|*.ts|*.tsx|*.mjs|*.mts|*.cjs|*.cts|*.vue|*.svelte|*.astro)
34
- if ! errors=$(npx eslint --fix "$file" 2>&1); then
35
- [ -n "$errors" ] && echo "$errors"
36
- fi
37
- npx prettier --write "$file" 2>/dev/null
38
- ;;
39
-
40
- # Other supported formats - prettier only
41
- *.md|*.json|*.css|*.scss|*.html|*.yaml|*.yml|*.graphql)
42
- npx prettier --write "$file" 2>/dev/null
43
- ;;
44
-
45
- # Shell scripts - shellcheck (if available), then Prettier (if plugin installed)
46
- *.sh)
47
- if [ -f node_modules/.bin/shellcheck ] || command -v shellcheck &> /dev/null; then
48
- if ! errors=$(npx shellcheck "$file" 2>&1); then
49
- [ -n "$errors" ] && echo "$errors"
50
- fi
51
- fi
52
- if [ -d node_modules/prettier-plugin-sh ]; then
53
- npx prettier --write "$file" 2>/dev/null
54
- fi
55
- ;;
56
- esac
57
-
58
- exit 0
@@ -1,50 +0,0 @@
1
- #!/bin/bash
2
- # Safeword: Cursor adapter for stop hook
3
- # Checks for marker file from afterFileEdit to determine if files were modified
4
- # Uses followup_message to inject quality review prompt into conversation
5
-
6
- # Require jq for JSON parsing
7
- command -v jq &> /dev/null || { echo '{}'; exit 0; }
8
-
9
- input=$(cat)
10
-
11
- # Get workspace root
12
- workspace=$(echo "$input" | jq -r '.workspace_roots[0] // empty' 2>/dev/null)
13
- [ -n "$workspace" ] && cd "$workspace" || true
14
-
15
- # Check for .safeword directory
16
- if [ ! -d ".safeword" ]; then
17
- echo '{}'
18
- exit 0
19
- fi
20
-
21
- # Check status - only proceed on completed (not aborted/error)
22
- status=$(echo "$input" | jq -r '.status // empty' 2>/dev/null)
23
- if [ "$status" != "completed" ]; then
24
- echo '{}'
25
- exit 0
26
- fi
27
-
28
- # Get loop_count to prevent infinite review loops
29
- # When review is triggered, agent runs again with loop_count >= 1
30
- loop_count=$(echo "$input" | jq -r '.loop_count // 0' 2>/dev/null)
31
-
32
- if [ "$loop_count" -ge 1 ]; then
33
- echo '{}'
34
- exit 0
35
- fi
36
-
37
- # Check if any file edits occurred in this session by looking for recent .safeword marker
38
- # This is a heuristic: if afterFileEdit ran recently, work was done
39
- marker_file="/tmp/safeword-cursor-edited-$(echo "$input" | jq -r '.conversation_id // "default"' 2>/dev/null)"
40
-
41
- if [ -f "$marker_file" ]; then
42
- rm -f "$marker_file" # Clean up marker
43
- cat << 'EOF'
44
- {
45
- "followup_message": "SAFEWORD Quality Review:\n\nDouble check and critique your work again just in case.\nAssume you've never seen it before.\n\n- Is it correct?\n- Is it elegant?\n- Does it follow latest docs/best practices?\n- Ask me any non-obvious questions.\n- Avoid bloat."
46
- }
47
- EOF
48
- else
49
- echo '{}'
50
- fi
@@ -1,51 +0,0 @@
1
- #!/bin/bash
2
- # Safeword: Auto-lint changed files (PostToolUse)
3
- # Silently auto-fixes, only outputs unfixable errors
4
- #
5
- # SYNC: Keep file patterns in sync with LINT_STAGED_CONFIG in:
6
- # packages/cli/src/templates/content.ts
7
- #
8
- # This hook is intentionally simple - ESLint's config handles
9
- # framework-specific rules (React, Vue, Svelte, Astro, etc.)
10
-
11
- # Require jq for JSON parsing
12
- command -v jq &> /dev/null || exit 0
13
-
14
- input=$(cat)
15
- file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.notebook_path // empty' 2>/dev/null)
16
-
17
- # Exit silently if no file or file doesn't exist
18
- [ -z "$file" ] || [ ! -f "$file" ] && exit 0
19
-
20
- # Change to project directory
21
- [ -n "$CLAUDE_PROJECT_DIR" ] && cd "$CLAUDE_PROJECT_DIR" || true
22
-
23
- # Determine linter based on file extension
24
- case "$file" in
25
- # JS/TS and framework files - ESLint first (fix code), then Prettier (format)
26
- *.js|*.jsx|*.ts|*.tsx|*.mjs|*.mts|*.cjs|*.cts|*.vue|*.svelte|*.astro)
27
- if ! errors=$(npx eslint --fix "$file" 2>&1); then
28
- [ -n "$errors" ] && echo "$errors"
29
- fi
30
- npx prettier --write "$file" 2>/dev/null
31
- ;;
32
-
33
- # Other supported formats - prettier only
34
- *.md|*.json|*.css|*.scss|*.html|*.yaml|*.yml|*.graphql)
35
- npx prettier --write "$file" 2>/dev/null
36
- ;;
37
-
38
- # Shell scripts - shellcheck (if available), then Prettier (if plugin installed)
39
- *.sh)
40
- if [ -f node_modules/.bin/shellcheck ] || command -v shellcheck &> /dev/null; then
41
- if ! errors=$(npx shellcheck "$file" 2>&1); then
42
- [ -n "$errors" ] && echo "$errors"
43
- fi
44
- fi
45
- if [ -d node_modules/prettier-plugin-sh ]; then
46
- npx prettier --write "$file" 2>/dev/null
47
- fi
48
- ;;
49
- esac
50
-
51
- exit 0