safeword 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{check-OYYSYHFP.js → check-QGZJ62PY.js} +73 -57
- package/dist/check-QGZJ62PY.js.map +1 -0
- package/dist/{chunk-ZS3Z3Q37.js → chunk-QPO3C3FP.js} +285 -65
- package/dist/chunk-QPO3C3FP.js.map +1 -0
- package/dist/{chunk-LNSEDZIW.js → chunk-YMLVQC4V.js} +159 -152
- package/dist/chunk-YMLVQC4V.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/{diff-325TIZ63.js → diff-654SSCFQ.js} +51 -53
- package/dist/diff-654SSCFQ.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/{reset-ZGJIKMUW.js → reset-IU6AIT7C.js} +3 -3
- package/dist/{setup-GAMXTFM2.js → setup-5IQ4KV2M.js} +17 -20
- package/dist/setup-5IQ4KV2M.js.map +1 -0
- package/dist/{sync-BFMXZEHM.js → sync-YBQEISFI.js} +8 -26
- package/dist/sync-YBQEISFI.js.map +1 -0
- package/dist/{upgrade-X4GREJXN.js → upgrade-RRTWEQIP.js} +3 -3
- package/package.json +1 -1
- package/templates/SAFEWORD.md +141 -73
- package/templates/commands/architecture.md +1 -1
- package/templates/commands/lint.md +1 -0
- package/templates/commands/quality-review.md +1 -1
- package/templates/cursor/rules/safeword-core.mdc +5 -0
- package/templates/doc-templates/architecture-template.md +1 -1
- package/templates/doc-templates/task-spec-template.md +151 -0
- package/templates/doc-templates/ticket-template.md +2 -4
- package/templates/guides/architecture-guide.md +2 -2
- package/templates/guides/code-philosophy.md +1 -1
- package/templates/guides/context-files-guide.md +3 -3
- package/templates/guides/design-doc-guide.md +2 -2
- package/templates/guides/development-workflow.md +2 -2
- package/templates/guides/learning-extraction.md +9 -9
- package/templates/guides/tdd-best-practices.md +39 -38
- package/templates/guides/test-definitions-guide.md +15 -14
- package/templates/hooks/cursor/after-file-edit.sh +66 -0
- package/templates/hooks/cursor/stop.sh +50 -0
- package/templates/hooks/post-tool-lint.sh +19 -5
- package/templates/hooks/prompt-questions.sh +1 -1
- package/templates/hooks/prompt-timestamp.sh +8 -1
- package/templates/hooks/session-lint-check.sh +1 -1
- package/templates/hooks/session-verify-agents.sh +1 -1
- package/templates/hooks/session-version.sh +1 -1
- package/templates/hooks/stop-quality.sh +2 -2
- package/templates/markdownlint-cli2.jsonc +18 -19
- package/templates/scripts/bisect-test-pollution.sh +87 -0
- package/templates/scripts/bisect-zombie-processes.sh +129 -0
- package/templates/scripts/lint-md.sh +16 -0
- package/templates/skills/safeword-quality-reviewer/SKILL.md +3 -3
- package/templates/skills/safeword-systematic-debugger/SKILL.md +246 -0
- package/templates/skills/safeword-tdd-enforcer/SKILL.md +221 -0
- package/dist/check-OYYSYHFP.js.map +0 -1
- package/dist/chunk-LNSEDZIW.js.map +0 -1
- package/dist/chunk-ZS3Z3Q37.js.map +0 -1
- package/dist/diff-325TIZ63.js.map +0 -1
- package/dist/setup-GAMXTFM2.js.map +0 -1
- package/dist/sync-BFMXZEHM.js.map +0 -1
- /package/dist/{reset-ZGJIKMUW.js.map → reset-IU6AIT7C.js.map} +0 -0
- /package/dist/{upgrade-X4GREJXN.js.map → upgrade-RRTWEQIP.js.map} +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tdd-enforcer
|
|
3
|
+
description: Use when implementing features, fixing bugs, or making code changes. Ensures scope is defined before coding, then enforces RED → GREEN → REFACTOR test discipline. Triggers: 'implement', 'add', 'build', 'create', 'fix', 'change', 'feature', 'bug'.
|
|
4
|
+
allowed-tools: '*'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# TDD Enforcer
|
|
8
|
+
|
|
9
|
+
Scope work before coding. Write tests before implementation.
|
|
10
|
+
|
|
11
|
+
**Iron Law:** NO IMPLEMENTATION UNTIL SCOPE IS DEFINED AND TEST FAILS
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
Answer IN ORDER. Stop at first match:
|
|
16
|
+
|
|
17
|
+
1. Implementing new feature? → Use this skill
|
|
18
|
+
2. Fixing bug? → Use this skill
|
|
19
|
+
3. Adding enhancement? → Use this skill
|
|
20
|
+
4. Refactoring? → Use this skill
|
|
21
|
+
5. Research/investigation only? → Skip this skill
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Phase 0: TRIAGE
|
|
26
|
+
|
|
27
|
+
**Purpose:** Determine work level and ensure scope exists.
|
|
28
|
+
|
|
29
|
+
### Step 1: Identify Level
|
|
30
|
+
|
|
31
|
+
Answer IN ORDER. Stop at first match:
|
|
32
|
+
|
|
33
|
+
| Question | If Yes → |
|
|
34
|
+
| ---------------------------------------- | -------------- |
|
|
35
|
+
| User-facing feature with business value? | **L2 Feature** |
|
|
36
|
+
| Bug, improvement, internal, or refactor? | **L1 Task** |
|
|
37
|
+
| Typo, config, or trivial change? | **L0 Micro** |
|
|
38
|
+
|
|
39
|
+
### Step 2: Check/Create Artifacts
|
|
40
|
+
|
|
41
|
+
| Level | Required Artifacts | Test Location |
|
|
42
|
+
| ------ | --------------------------------------------------------------- | ------------------------------- |
|
|
43
|
+
| **L2** | Feature Spec + Test Definitions (+ Design Doc if 3+ components) | `test-definitions/feature-*.md` |
|
|
44
|
+
| **L1** | Task Spec | Inline in spec |
|
|
45
|
+
| **L0** | Task Spec (minimal) | Existing tests |
|
|
46
|
+
|
|
47
|
+
**Locations:**
|
|
48
|
+
|
|
49
|
+
- Specs: `.safeword/planning/specs/`
|
|
50
|
+
- Test definitions: `.safeword/planning/test-definitions/`
|
|
51
|
+
|
|
52
|
+
**Templates:**
|
|
53
|
+
|
|
54
|
+
- L2 Feature: @./.safeword/templates/user-stories-template.md
|
|
55
|
+
- L1/L0 Task: @./.safeword/templates/task-spec-template.md
|
|
56
|
+
- Test Definitions: @./.safeword/templates/test-definitions-feature.md
|
|
57
|
+
|
|
58
|
+
### Exit Criteria
|
|
59
|
+
|
|
60
|
+
- [ ] Level identified (L0/L1/L2)
|
|
61
|
+
- [ ] Spec exists with "Out of Scope" defined
|
|
62
|
+
- [ ] L2: Test definitions file exists
|
|
63
|
+
- [ ] L1: Test scenarios in spec
|
|
64
|
+
- [ ] L0: Existing test coverage confirmed
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Phase 1: RED
|
|
69
|
+
|
|
70
|
+
**Iron Law:** NO IMPLEMENTATION UNTIL TEST FAILS FOR THE RIGHT REASON
|
|
71
|
+
|
|
72
|
+
**Protocol:**
|
|
73
|
+
|
|
74
|
+
1. Pick ONE test from spec (L1) or test definitions (L2)
|
|
75
|
+
2. Write test code
|
|
76
|
+
3. Run test
|
|
77
|
+
4. Verify: fails because behavior missing (not syntax error)
|
|
78
|
+
5. Commit: `test: [behavior]`
|
|
79
|
+
|
|
80
|
+
**For L0:** No new test needed. Confirm existing tests pass, then proceed to Phase 2.
|
|
81
|
+
|
|
82
|
+
**Exit Criteria:**
|
|
83
|
+
|
|
84
|
+
- [ ] Test written and executed
|
|
85
|
+
- [ ] Test fails for RIGHT reason (behavior missing)
|
|
86
|
+
- [ ] Committed: `test: [behavior]`
|
|
87
|
+
|
|
88
|
+
**Red Flags → STOP:**
|
|
89
|
+
|
|
90
|
+
| Flag | Action |
|
|
91
|
+
| ----------------------- | -------------------------------- |
|
|
92
|
+
| Test passes immediately | Rewrite - you're testing nothing |
|
|
93
|
+
| Syntax error | Fix syntax, not behavior |
|
|
94
|
+
| Wrote implementation | Delete it, return to test |
|
|
95
|
+
| Multiple tests | Pick ONE |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Phase 2: GREEN
|
|
100
|
+
|
|
101
|
+
**Iron Law:** ONLY WRITE CODE THE TEST REQUIRES
|
|
102
|
+
|
|
103
|
+
**Protocol:**
|
|
104
|
+
|
|
105
|
+
1. Write minimal code to pass test
|
|
106
|
+
2. Run test → verify pass
|
|
107
|
+
3. Commit: `feat:` or `fix:`
|
|
108
|
+
|
|
109
|
+
**Exit Criteria:**
|
|
110
|
+
|
|
111
|
+
- [ ] Test passes
|
|
112
|
+
- [ ] No extra code
|
|
113
|
+
- [ ] No hardcoded/mock values
|
|
114
|
+
- [ ] Committed
|
|
115
|
+
|
|
116
|
+
**Red Flags → STOP:**
|
|
117
|
+
|
|
118
|
+
| Flag | Action |
|
|
119
|
+
| ------------------- | -------------------------------------- |
|
|
120
|
+
| "Just in case" code | Delete it |
|
|
121
|
+
| Multiple functions | Delete extras |
|
|
122
|
+
| Refactoring | Stop - that's Phase 3 |
|
|
123
|
+
| Test still fails | Debug (→ systematic-debugger if stuck) |
|
|
124
|
+
| Hardcoded value | Implement real logic (see below) |
|
|
125
|
+
|
|
126
|
+
### Anti-Pattern: Mock Implementations
|
|
127
|
+
|
|
128
|
+
LLMs sometimes hardcode values to pass tests. This is not TDD.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// ❌ BAD - Hardcoded to pass test
|
|
132
|
+
function calculateDiscount(amount, tier) {
|
|
133
|
+
return 80; // Passes test but isn't real
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ✅ GOOD - Actual logic
|
|
137
|
+
function calculateDiscount(amount, tier) {
|
|
138
|
+
if (tier === 'VIP') return amount * 0.8;
|
|
139
|
+
return amount;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Fix mocks immediately. The next test cycle will catch them, but they're technical debt.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Phase 3: REFACTOR
|
|
148
|
+
|
|
149
|
+
**Protocol:**
|
|
150
|
+
|
|
151
|
+
1. Tests pass before changes
|
|
152
|
+
2. Improve code (rename, extract, dedupe)
|
|
153
|
+
3. Tests pass after changes
|
|
154
|
+
4. Commit if changed: `refactor: [improvement]`
|
|
155
|
+
|
|
156
|
+
**Exit Criteria:**
|
|
157
|
+
|
|
158
|
+
- [ ] Tests still pass
|
|
159
|
+
- [ ] Code cleaner (or no changes needed)
|
|
160
|
+
- [ ] Committed (if changed)
|
|
161
|
+
|
|
162
|
+
**NOT Allowed:** New behavior, changing assertions, adding tests.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Phase 4: ITERATE
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
More tests in spec/test-definitions?
|
|
170
|
+
├─ Yes → Return to Phase 1
|
|
171
|
+
└─ No → All "Done When" / AC checked?
|
|
172
|
+
├─ Yes → Complete
|
|
173
|
+
└─ No → Update spec, return to Phase 0
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
For L2: Update test definition status (✅/⏭️/❌/🔴) as tests pass.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Quick Reference
|
|
181
|
+
|
|
182
|
+
| Phase | Key Question | Gate |
|
|
183
|
+
| ----------- | -------------------------------- | ----------------------------- |
|
|
184
|
+
| 0. TRIAGE | What level? Is scope defined? | Spec exists with boundaries |
|
|
185
|
+
| 1. RED | Does test fail for right reason? | Test fails (behavior missing) |
|
|
186
|
+
| 2. GREEN | Does minimal code pass? | Test passes, no extras |
|
|
187
|
+
| 3. REFACTOR | Is code clean? | Tests still pass |
|
|
188
|
+
| 4. ITERATE | More tests? | All done → complete |
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Examples
|
|
193
|
+
|
|
194
|
+
**L2 Feature** ("Add VIP discount"):
|
|
195
|
+
Phase 0: L2 → create spec + test defs → Phase 1: write test → FAIL → commit → Phase 2: implement → PASS → commit → Phase 3: clean up → Phase 4: more tests? → repeat
|
|
196
|
+
|
|
197
|
+
**L1 Bug** ("Fix login timeout"):
|
|
198
|
+
Phase 0: L1 → create task spec → Phase 1: write failing test → commit → Phase 2: fix → PASS → commit → Phase 3: clean up if needed → Phase 4: done
|
|
199
|
+
|
|
200
|
+
**L0 Micro** ("Fix typo"):
|
|
201
|
+
Phase 0: L0 → create minimal spec → Phase 1: no new test (existing tests cover) → Phase 2: fix typo → tests PASS → commit → done
|
|
202
|
+
|
|
203
|
+
**Why L0 needs a spec:** "Fix typo" can become "refactor error handling" without explicit "Out of Scope".
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Integration
|
|
208
|
+
|
|
209
|
+
| Scenario | Handoff |
|
|
210
|
+
| ----------------------- | --------------------- |
|
|
211
|
+
| Test fails unexpectedly | → systematic-debugger |
|
|
212
|
+
| Review needed | → quality-reviewer |
|
|
213
|
+
| Scope expanding | → Update spec first |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Related
|
|
218
|
+
|
|
219
|
+
- @./.safeword/guides/test-definitions-guide.md
|
|
220
|
+
- @./.safeword/guides/tdd-best-practices.md
|
|
221
|
+
- @./.safeword/guides/development-workflow.md
|
|
@@ -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 { join } from 'node:path';\nimport { VERSION } from '../version.js';\nimport { exists, readFileSafe } from '../utils/fs.js';\nimport { info, success, warn, header, keyValue } from '../utils/output.js';\nimport { isNewerVersion } from '../utils/version.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { reconcile } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\n\nexport interface CheckOptions {\n offline?: boolean;\n}\n\ninterface HealthStatus {\n configured: boolean;\n projectVersion: string | null;\n cliVersion: string;\n updateAvailable: boolean;\n latestVersion: string | null;\n issues: string[];\n missingPackages: string[];\n}\n\n/**\n * Check for latest version from npm (with timeout)\n */\nasync function checkLatestVersion(timeout = 3000): Promise<string | null> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 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 null;\n\n const data = (await response.json()) as { version?: string };\n return data.version ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Check project configuration health using reconcile dryRun\n */\nasync function checkHealth(cwd: string): Promise<HealthStatus> {\n const safewordDir = join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDir)) {\n return {\n configured: false,\n projectVersion: null,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: null,\n issues: [],\n missingPackages: [],\n };\n }\n\n // Read project version\n const versionPath = join(safewordDir, 'version');\n const projectVersion = readFileSafe(versionPath)?.trim() ?? null;\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 const issues: string[] = [];\n\n // Check for missing owned files (write actions indicate missing/changed files)\n const writeActions = result.actions.filter(a => a.type === 'write');\n for (const action of writeActions) {\n if (action.type === 'write') {\n const fullPath = join(cwd, action.path);\n if (!exists(fullPath)) {\n issues.push(`Missing: ${action.path}`);\n }\n }\n }\n\n // Check for missing text patches (e.g., AGENTS.md link)\n const textPatchActions = result.actions.filter(a => a.type === 'text-patch');\n for (const action of textPatchActions) {\n if (action.type === 'text-patch') {\n const fullPath = join(cwd, action.path);\n if (!exists(fullPath)) {\n issues.push(`${action.path} file missing`);\n } else {\n const content = readFileSafe(fullPath) ?? '';\n if (!content.includes(action.definition.marker)) {\n issues.push(`${action.path} missing safeword link`);\n }\n }\n }\n }\n\n // Check for missing .claude/settings.json\n const settingsPath = join(cwd, '.claude', 'settings.json');\n if (!exists(settingsPath)) {\n issues.push('Missing: .claude/settings.json');\n }\n\n return {\n configured: true,\n projectVersion,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: null,\n issues,\n missingPackages: result.packagesToInstall,\n };\n}\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('\\nChecking for updates...');\n const latestVersion = await checkLatestVersion();\n\n if (latestVersion) {\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 } else {\n warn(\"Couldn't check for updates (offline?)\");\n }\n } else {\n info('\\nSkipped update check (offline mode)');\n }\n\n // Check project version vs CLI version\n if (health.projectVersion && 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 (health.projectVersion && 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 // Show issues\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 } else if (health.missingPackages.length > 0) {\n header('Missing Packages');\n info(`${health.missingPackages.length} linting packages not installed`);\n info('Run `safeword sync` to install missing packages');\n } else {\n success('\\nConfiguration is healthy');\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/output.ts","../src/utils/git.ts","../src/utils/context.ts","../src/reconcile.ts"],"sourcesContent":["/**\n * Console output utilities for consistent CLI messaging\n */\n\n/**\n * Print info message\n */\nexport function info(message: string): void {\n console.log(message);\n}\n\n/**\n * Print success message\n */\nexport function success(message: string): void {\n console.log(`✓ ${message}`);\n}\n\n/**\n * Print warning message\n */\nexport function warn(message: string): void {\n console.warn(`⚠ ${message}`);\n}\n\n/**\n * Print error message to stderr\n */\nexport function error(message: string): void {\n console.error(`✗ ${message}`);\n}\n\n/**\n * Print a blank line\n */\nexport function blank(): void {\n console.log('');\n}\n\n/**\n * Print a section header\n */\nexport function header(title: string): void {\n console.log(`\\n${title}`);\n console.log('─'.repeat(title.length));\n}\n\n/**\n * Print a list item\n */\nexport function listItem(item: string, indent = 2): void {\n console.log(`${' '.repeat(indent)}• ${item}`);\n}\n\n/**\n * Print key-value pair\n */\nexport function keyValue(key: string, value: string): void {\n console.log(` ${key}: ${value}`);\n}\n","/**\n * Git utilities for CLI operations\n */\n\nimport { join } from 'node:path';\nimport { exists } from './fs.js';\n\n/**\n * Check if directory is a git repository\n */\nexport function isGitRepo(cwd: string): boolean {\n return exists(join(cwd, '.git'));\n}\n","/**\n * Project Context Utilities\n *\n * Shared helpers for creating ProjectContext objects used by reconcile().\n */\n\nimport { join } from 'node:path';\nimport { readJson } from './fs.js';\nimport { isGitRepo } from './git.js';\nimport { detectProjectType, type PackageJson } from './project-detector.js';\nimport type { ProjectContext } from '../schema.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 */\nexport function createProjectContext(cwd: string): ProjectContext {\n const packageJson = readJson<PackageJson>(join(cwd, 'package.json'));\n\n return {\n cwd,\n projectType: detectProjectType(packageJson ?? {}),\n devDeps: packageJson?.devDependencies ?? {},\n isGitRepo: isGitRepo(cwd),\n };\n}\n","/**\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 { join } from 'node:path';\nimport {\n exists,\n ensureDir,\n writeFile,\n readFile,\n readFileSafe,\n readJson,\n writeJson,\n remove,\n removeIfEmpty,\n makeScriptsExecutable,\n getTemplatesDir,\n} from './utils/fs.js';\nimport type {\n SafewordSchema,\n ProjectContext,\n FileDefinition,\n JsonMergeDefinition,\n TextPatchDefinition,\n} from './schema.js';\nimport type { ProjectType } from './utils/project-detector.js';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst HUSKY_DIR = '.husky';\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\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\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 case 'upgrade':\n return computeUpgradePlan(schema, ctx);\n case 'uninstall':\n return computeUninstallPlan(schema, ctx, false);\n case 'uninstall-full':\n return computeUninstallPlan(schema, ctx, true);\n }\n}\n\nfunction computeInstallPlan(schema: SafewordSchema, ctx: ProjectContext): ReconcilePlan {\n const actions: Action[] = [];\n const wouldCreate: string[] = [];\n\n // 1. Create all directories (skip .husky if not a git repo)\n const allDirs = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];\n for (const dir of allDirs) {\n // Skip .husky in non-git repos\n if (dir.startsWith(HUSKY_DIR) && !ctx.isGitRepo) continue;\n\n const fullPath = join(ctx.cwd, dir);\n if (!exists(fullPath)) {\n actions.push({ type: 'mkdir', path: dir });\n wouldCreate.push(dir);\n }\n }\n\n // 2. Write all owned files (skip .husky files if not a git repo)\n for (const [filePath, def] of Object.entries(schema.ownedFiles)) {\n // Skip .husky files in non-git repos\n if (filePath.startsWith(HUSKY_DIR) && !ctx.isGitRepo) continue;\n\n const content = resolveFileContent(def, ctx);\n actions.push({ type: 'write', path: filePath, content });\n wouldCreate.push(filePath);\n }\n\n // 3. Write managed files (only if missing)\n for (const [filePath, def] of Object.entries(schema.managedFiles)) {\n const fullPath = join(ctx.cwd, filePath);\n if (!exists(fullPath)) {\n const content = resolveFileContent(def, ctx);\n actions.push({ type: 'write', path: filePath, content });\n wouldCreate.push(filePath);\n }\n }\n\n // 4. chmod hook/lib directories (only .husky if git repo)\n const chmodPaths = ['.safeword/hooks', '.safeword/lib'];\n if (ctx.isGitRepo) chmodPaths.push(HUSKY_DIR);\n actions.push({ type: 'chmod', paths: chmodPaths });\n\n // 5. JSON merges\n for (const [filePath, def] of Object.entries(schema.jsonMerges)) {\n actions.push({ type: 'json-merge', path: filePath, definition: def });\n }\n\n // 6. Text patches\n for (const [filePath, def] of Object.entries(schema.textPatches)) {\n actions.push({ type: 'text-patch', path: filePath, definition: def });\n if (def.createIfMissing && !exists(join(ctx.cwd, filePath))) {\n wouldCreate.push(filePath);\n }\n }\n\n // 7. Compute packages to install (husky/lint-staged skipped if no git repo)\n const packagesToInstall = computePackagesToInstall(\n schema,\n ctx.projectType,\n ctx.devDeps,\n ctx.isGitRepo,\n );\n\n return {\n actions,\n wouldCreate,\n wouldUpdate: [],\n wouldRemove: [],\n packagesToInstall,\n packagesToRemove: [],\n };\n}\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 allDirs = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];\n for (const dir of allDirs) {\n // Skip .husky in non-git repos\n if (dir.startsWith(HUSKY_DIR) && !ctx.isGitRepo) continue;\n\n const fullPath = join(ctx.cwd, dir);\n if (!exists(fullPath)) {\n actions.push({ type: 'mkdir', path: dir });\n wouldCreate.push(dir);\n }\n }\n\n // 2. Update owned files if content changed (skip .husky files if not a git repo)\n for (const [filePath, def] of Object.entries(schema.ownedFiles)) {\n // Skip .husky files in non-git repos\n if (filePath.startsWith(HUSKY_DIR) && !ctx.isGitRepo) continue;\n\n const fullPath = join(ctx.cwd, filePath);\n const newContent = resolveFileContent(def, ctx);\n\n if (fileNeedsUpdate(fullPath, newContent)) {\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\n // 3. Update managed files only if content matches current template\n for (const [filePath, def] of Object.entries(schema.managedFiles)) {\n const fullPath = join(ctx.cwd, filePath);\n const newContent = resolveFileContent(def, ctx);\n\n if (!exists(fullPath)) {\n // Missing - create it\n actions.push({ type: 'write', path: filePath, content: newContent });\n wouldCreate.push(filePath);\n } else {\n // Exists - only update if user hasn't modified it\n // For upgrade, we need to compare against what safeword would generate\n // If the current content matches our template, user hasn't customized it\n const currentContent = readFileSafe(fullPath);\n if (currentContent?.trim() === newContent.trim()) {\n // Content matches - no update needed (already current)\n } else {\n // Content differs - check if it was originally from safeword\n // For now, we don't update managed files in upgrade mode\n // unless they match our previous template exactly\n // This is conservative - user may have customized\n }\n }\n }\n\n // 4. chmod (only .husky if git repo)\n const chmodPaths = ['.safeword/hooks', '.safeword/lib'];\n if (ctx.isGitRepo) chmodPaths.push(HUSKY_DIR);\n actions.push({ type: 'chmod', paths: chmodPaths });\n\n // 5. JSON merges (always apply to ensure keys are present)\n for (const [filePath, def] of Object.entries(schema.jsonMerges)) {\n actions.push({ type: 'json-merge', path: filePath, definition: def });\n }\n\n // 6. Text patches (only if marker missing)\n for (const [filePath, def] of Object.entries(schema.textPatches)) {\n const fullPath = join(ctx.cwd, filePath);\n const content = readFileSafe(fullPath) ?? '';\n if (!content.includes(def.marker)) {\n actions.push({ type: 'text-patch', path: filePath, definition: def });\n }\n }\n\n // 7. Compute packages to install (husky/lint-staged skipped if no git repo)\n const packagesToInstall = computePackagesToInstall(\n schema,\n ctx.projectType,\n ctx.devDeps,\n ctx.isGitRepo,\n );\n\n return {\n actions,\n wouldCreate,\n wouldUpdate,\n wouldRemove: [],\n packagesToInstall,\n packagesToRemove: [],\n };\n}\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\n const dirsToCleanup = new Set<string>();\n for (const filePath of Object.keys(schema.ownedFiles)) {\n const fullPath = join(ctx.cwd, filePath);\n if (exists(fullPath)) {\n actions.push({ type: 'rm', path: filePath });\n wouldRemove.push(filePath);\n // Track parent dir for cleanup (for .claude/* skill dirs)\n if (filePath.startsWith('.claude/')) {\n const parentDir = filePath.substring(0, filePath.lastIndexOf('/'));\n if (\n parentDir &&\n parentDir !== '.claude' &&\n parentDir !== '.claude/skills' &&\n parentDir !== '.claude/commands'\n ) {\n dirsToCleanup.add(parentDir);\n }\n }\n }\n }\n // Clean up empty parent directories (like .claude/skills/safeword-*)\n for (const dir of dirsToCleanup) {\n const fullPath = join(ctx.cwd, dir);\n if (exists(fullPath)) {\n actions.push({ type: 'rmdir', path: dir });\n wouldRemove.push(dir);\n }\n }\n\n // 2. JSON unmerges\n for (const [filePath, def] of Object.entries(schema.jsonMerges)) {\n actions.push({ type: 'json-unmerge', path: filePath, definition: def });\n }\n\n // 3. Text unpatches\n for (const [filePath, def] of Object.entries(schema.textPatches)) {\n const fullPath = join(ctx.cwd, filePath);\n if (exists(fullPath)) {\n const content = readFileSafe(fullPath) ?? '';\n if (content.includes(def.marker)) {\n actions.push({ type: 'text-unpatch', path: filePath, definition: def });\n }\n }\n }\n\n // 4. Remove preserved directories first (reverse order)\n // These will only be removed if empty (no user content)\n const preservedDirsToRemove = [...schema.preservedDirs].reverse();\n for (const dir of preservedDirsToRemove) {\n const fullPath = join(ctx.cwd, dir);\n if (exists(fullPath)) {\n actions.push({ type: 'rmdir', path: dir });\n wouldRemove.push(dir);\n }\n }\n\n // 5. Remove owned directories (reverse order)\n // Reverse order ensures children are removed before parents\n const dirsToRemove = [...schema.ownedDirs].reverse();\n for (const dir of dirsToRemove) {\n const fullPath = join(ctx.cwd, dir);\n if (exists(fullPath)) {\n actions.push({ type: 'rmdir', path: dir });\n wouldRemove.push(dir);\n }\n }\n\n // 6. Full uninstall: remove managed files\n if (full) {\n for (const filePath of Object.keys(schema.managedFiles)) {\n const fullPath = join(ctx.cwd, filePath);\n if (exists(fullPath)) {\n actions.push({ type: 'rm', path: filePath });\n wouldRemove.push(filePath);\n }\n }\n }\n\n // 7. Compute packages to remove (full only)\n const packagesToRemove = full\n ? computePackagesToRemove(schema, ctx.projectType, ctx.devDeps)\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\nfunction executePlan(plan: ReconcilePlan, ctx: ProjectContext): ExecutionResult {\n const created: string[] = [];\n const updated: string[] = [];\n const removed: string[] = [];\n\n for (const action of plan.actions) {\n switch (action.type) {\n case 'mkdir': {\n const fullPath = join(ctx.cwd, action.path);\n ensureDir(fullPath);\n created.push(action.path);\n break;\n }\n\n case 'rmdir': {\n const fullPath = join(ctx.cwd, action.path);\n // Use removeIfEmpty to preserve directories with user content\n // This will only succeed if the directory is empty\n if (removeIfEmpty(fullPath)) {\n removed.push(action.path);\n }\n break;\n }\n\n case 'write': {\n const fullPath = join(ctx.cwd, action.path);\n const existed = exists(fullPath);\n writeFile(fullPath, action.content);\n if (existed) {\n updated.push(action.path);\n } else {\n created.push(action.path);\n }\n break;\n }\n\n case 'rm': {\n const fullPath = join(ctx.cwd, action.path);\n remove(fullPath);\n removed.push(action.path);\n break;\n }\n\n case 'chmod': {\n for (const path of action.paths) {\n const fullPath = join(ctx.cwd, path);\n if (exists(fullPath)) {\n makeScriptsExecutable(fullPath);\n }\n }\n break;\n }\n\n case 'json-merge': {\n executeJsonMerge(ctx.cwd, action.path, action.definition, ctx);\n break;\n }\n\n case 'json-unmerge': {\n executeJsonUnmerge(ctx.cwd, action.path, action.definition);\n break;\n }\n\n case 'text-patch': {\n executeTextPatch(ctx.cwd, action.path, action.definition);\n break;\n }\n\n case 'text-unpatch': {\n executeTextUnpatch(ctx.cwd, action.path, action.definition);\n break;\n }\n }\n }\n\n return { created, updated, removed };\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\nfunction resolveFileContent(def: FileDefinition, ctx: ProjectContext): string {\n if (def.template) {\n const templatesDir = getTemplatesDir();\n return readFile(join(templatesDir, def.template));\n }\n\n if (def.content) {\n return typeof def.content === 'function' ? def.content() : def.content;\n }\n\n if (def.generator) {\n return def.generator(ctx);\n }\n\n throw new Error('FileDefinition must have template, content, or generator');\n}\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 = ['husky', 'lint-staged'];\n\nexport function computePackagesToInstall(\n schema: SafewordSchema,\n projectType: ProjectType,\n installedDevDeps: 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.includes(pkg));\n }\n\n for (const [key, deps] of Object.entries(schema.packages.conditional)) {\n if (projectType[key as keyof ProjectType]) {\n needed.push(...deps);\n }\n }\n\n return needed.filter(pkg => !(pkg in installedDevDeps));\n}\n\nfunction computePackagesToRemove(\n schema: SafewordSchema,\n projectType: ProjectType,\n installedDevDeps: Record<string, string>,\n): string[] {\n const safewordPackages = [...schema.packages.base];\n\n for (const [key, deps] of Object.entries(schema.packages.conditional)) {\n if (projectType[key as keyof ProjectType]) {\n safewordPackages.push(...deps);\n }\n }\n\n // Only remove packages that are actually installed\n return safewordPackages.filter(pkg => pkg in installedDevDeps);\n}\n\nfunction executeJsonMerge(\n cwd: string,\n path: string,\n def: JsonMergeDefinition,\n ctx: ProjectContext,\n): void {\n const fullPath = join(cwd, path);\n const existing = readJson<Record<string, unknown>>(fullPath) ?? {};\n const merged = def.merge(existing, ctx);\n writeJson(fullPath, merged);\n}\n\nfunction executeJsonUnmerge(cwd: string, path: string, def: JsonMergeDefinition): void {\n const fullPath = join(cwd, path);\n if (!exists(fullPath)) return;\n\n const existing = readJson<Record<string, unknown>>(fullPath);\n if (!existing) return;\n\n const unmerged = def.unmerge(existing);\n\n // Check if file should be removed\n if (def.removeFileIfEmpty) {\n const remainingKeys = Object.keys(unmerged).filter(\n k => unmerged[k] !== undefined && unmerged[k] !== null,\n );\n if (remainingKeys.length === 0) {\n remove(fullPath);\n return;\n }\n }\n\n writeJson(fullPath, unmerged);\n}\n\nfunction executeTextPatch(cwd: string, path: string, def: TextPatchDefinition): void {\n const fullPath = join(cwd, path);\n let content = readFileSafe(fullPath) ?? '';\n\n // Check if already patched\n if (content.includes(def.marker)) return;\n\n // Apply patch\n if (def.operation === 'prepend') {\n content = def.content + content;\n } else {\n content = content + def.content;\n }\n\n writeFile(fullPath, content);\n}\n\nfunction executeTextUnpatch(cwd: string, path: string, def: TextPatchDefinition): void {\n const fullPath = 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(def.content, '');\n\n // If full content wasn't found but marker exists, remove lines containing the marker\n if (unpatched === content && content.includes(def.marker)) {\n // Remove lines containing the marker\n const lines = content.split('\\n');\n const filtered = lines.filter(line => !line.includes(def.marker));\n unpatched = filtered.join('\\n').replace(/^\\n+/, ''); // Remove leading empty lines\n }\n\n writeFile(fullPath, unpatched);\n}\n"],"mappings":"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","names":["join","join","join","join","isGitRepo"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/fs.ts","../src/utils/project-detector.ts","../src/templates/content.ts","../src/templates/config.ts","../src/utils/boundaries.ts","../src/utils/install.ts","../src/utils/hooks.ts","../src/schema.ts"],"sourcesContent":["/**\n * File system utilities for CLI operations\n */\n\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n rmSync,\n rmdirSync,\n readdirSync,\n chmodSync,\n} from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n// Get the directory of this module (for locating templates)\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Get path to bundled templates directory.\n * Works in both development (src/) and production (dist/) contexts.\n *\n * Note: We check for SAFEWORD.md to distinguish from src/templates/ which\n * contains TypeScript source files (config.ts, content.ts).\n *\n * Path resolution (bundled with tsup):\n * - From dist/chunk-*.js: __dirname = packages/cli/dist/ → ../templates\n */\nexport function getTemplatesDir(): string {\n const knownTemplateFile = 'SAFEWORD.md';\n\n // Try different relative paths - the bundled code ends up in dist/ directly (flat)\n // while source is in src/utils/\n const candidates = [\n join(__dirname, '..', 'templates'), // From dist/ (flat bundled)\n join(__dirname, '..', '..', 'templates'), // From src/utils/ or dist/utils/\n join(__dirname, 'templates'), // Direct sibling (unlikely but safe)\n ];\n\n for (const candidate of candidates) {\n if (existsSync(join(candidate, knownTemplateFile))) {\n return candidate;\n }\n }\n\n throw new Error('Templates directory not found');\n}\n\n/**\n * Check if a path exists\n */\nexport function exists(path: string): boolean {\n return existsSync(path);\n}\n\n/**\n * Create directory recursively\n */\nexport function ensureDir(path: string): void {\n if (!existsSync(path)) {\n mkdirSync(path, { recursive: true });\n }\n}\n\n/**\n * Read file as string\n */\nexport function readFile(path: string): string {\n return readFileSync(path, 'utf-8');\n}\n\n/**\n * Read file as string, return null if not exists\n */\nexport function readFileSafe(path: string): string | null {\n if (!existsSync(path)) return null;\n return readFileSync(path, 'utf-8');\n}\n\n/**\n * Write file, creating parent directories if needed\n */\nexport function writeFile(path: string, content: string): void {\n ensureDir(dirname(path));\n writeFileSync(path, content);\n}\n\n/**\n * Remove file or directory recursively\n */\nexport function remove(path: string): void {\n if (existsSync(path)) {\n rmSync(path, { recursive: true, force: true });\n }\n}\n\n/**\n * Remove directory only if empty, returns true if removed\n */\nexport function removeIfEmpty(path: string): boolean {\n if (!existsSync(path)) return false;\n try {\n rmdirSync(path); // Non-recursive, throws if not empty\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Make all shell scripts in a directory executable\n */\nexport function makeScriptsExecutable(dirPath: string): void {\n if (!existsSync(dirPath)) return;\n for (const file of readdirSync(dirPath)) {\n if (file.endsWith('.sh')) {\n chmodSync(join(dirPath, file), 0o755);\n }\n }\n}\n\n/**\n * Read JSON file\n */\nexport function readJson<T = unknown>(path: string): T | null {\n const content = readFileSafe(path);\n if (!content) return null;\n try {\n return JSON.parse(content) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Write JSON file with formatting\n */\nexport function writeJson(path: string, data: unknown): void {\n writeFile(path, JSON.stringify(data, null, 2) + '\\n');\n}\n","/**\n * Project type detection from package.json\n *\n * Detects frameworks and tools used in the project to configure\n * appropriate linting rules.\n */\n\nexport interface PackageJson {\n name?: string;\n version?: string;\n private?: boolean;\n main?: string;\n module?: string;\n exports?: unknown;\n types?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\nexport interface ProjectType {\n typescript: boolean;\n react: boolean;\n nextjs: boolean;\n astro: boolean;\n vue: boolean;\n nuxt: boolean;\n svelte: boolean;\n sveltekit: boolean;\n electron: boolean;\n vitest: boolean;\n playwright: boolean;\n tailwind: boolean;\n publishableLibrary: boolean;\n}\n\n/**\n * Detects project type from package.json contents\n */\nexport function detectProjectType(packageJson: PackageJson): ProjectType {\n const deps = packageJson.dependencies || {};\n const devDeps = packageJson.devDependencies || {};\n const allDeps = { ...deps, ...devDeps };\n\n const hasTypescript = 'typescript' in allDeps;\n const hasReact = 'react' in deps || 'react' in devDeps;\n const hasNextJs = 'next' in deps;\n const hasAstro = 'astro' in deps || 'astro' in devDeps;\n const hasVue = 'vue' in deps || 'vue' in devDeps;\n const hasNuxt = 'nuxt' in deps;\n const hasSvelte = 'svelte' in deps || 'svelte' in devDeps;\n const hasSvelteKit = '@sveltejs/kit' in deps || '@sveltejs/kit' in devDeps;\n const hasElectron = 'electron' in deps || 'electron' in devDeps;\n const hasVitest = 'vitest' in devDeps;\n const hasPlaywright = '@playwright/test' in devDeps;\n const hasTailwind = 'tailwindcss' in allDeps;\n\n // Publishable library: has entry points and is not marked private\n const hasEntryPoints = !!(packageJson.main || packageJson.module || packageJson.exports);\n const isPublishable = hasEntryPoints && packageJson.private !== true;\n\n return {\n typescript: hasTypescript,\n react: hasReact || hasNextJs, // Next.js implies React\n nextjs: hasNextJs,\n astro: hasAstro,\n vue: hasVue || hasNuxt, // Nuxt implies Vue\n nuxt: hasNuxt,\n svelte: hasSvelte || hasSvelteKit, // SvelteKit implies Svelte\n sveltekit: hasSvelteKit,\n electron: hasElectron,\n vitest: hasVitest,\n playwright: hasPlaywright,\n tailwind: hasTailwind,\n publishableLibrary: isPublishable,\n };\n}\n","/**\n * Content templates - static string content\n *\n * Note: Most templates (SAFEWORD.md, hooks, skills, guides, etc.) are now\n * file-based in the templates/ directory. This file contains only small\n * string constants that are used inline.\n */\n\nimport type { ProjectType } from '../utils/project-detector.js';\n\nexport const AGENTS_MD_LINK = `**⚠️ ALWAYS READ FIRST: @./.safeword/SAFEWORD.md**\n\nThe SAFEWORD.md file contains core development patterns, workflows, and conventions.\nRead it BEFORE working on any task in this project.\n\n---`;\n\ninterface PrettierConfig {\n semi: boolean;\n singleQuote: boolean;\n tabWidth: number;\n trailingComma: string;\n printWidth: number;\n endOfLine: string;\n plugins?: string[];\n}\n\n/**\n * Generate .prettierrc content based on project type.\n * Explicitly lists plugins to ensure compatibility with pnpm/Yarn PnP.\n */\nexport function getPrettierConfig(projectType: ProjectType): string {\n const config: PrettierConfig = {\n semi: true,\n singleQuote: true,\n tabWidth: 2,\n trailingComma: 'es5',\n printWidth: 100,\n endOfLine: 'lf',\n };\n\n const plugins: string[] = [];\n\n if (projectType.astro) plugins.push('prettier-plugin-astro');\n if (projectType.svelte) plugins.push('prettier-plugin-svelte');\n if (projectType.tailwind) plugins.push('prettier-plugin-tailwindcss');\n\n if (plugins.length > 0) {\n config.plugins = plugins;\n }\n\n return JSON.stringify(config, null, 2) + '\\n';\n}\n\n/**\n * lint-staged configuration for pre-commit hooks\n * Runs linters only on staged files for fast commits\n *\n * SYNC: Keep file patterns in sync with post-tool-lint.sh in:\n * packages/cli/templates/hooks/post-tool-lint.sh\n */\nexport const LINT_STAGED_CONFIG = {\n '*.{js,jsx,ts,tsx,mjs,mts,cjs,cts}': ['eslint --fix', 'prettier --write'],\n '*.{vue,svelte,astro}': ['eslint --fix', 'prettier --write'],\n '*.{json,css,scss,html,yaml,yml,graphql}': ['prettier --write'],\n '*.md': ['markdownlint-cli2 --fix', 'prettier --write'],\n};\n","/**\n * Configuration templates - ESLint config generation and hook settings\n *\n * ESLint flat config (v9+) with:\n * - Dynamic framework detection from package.json at runtime\n * - Static imports for base plugins (always installed by safeword)\n * - Dynamic imports for framework plugins (loaded only if framework detected)\n * - defineConfig helper for validation and type checking\n * - eslint-config-prettier last to avoid conflicts\n *\n * See: https://eslint.org/docs/latest/use/configure/configuration-files\n */\n\n/**\n * Generates a dynamic ESLint config that adapts to project frameworks at runtime.\n *\n * The generated config reads package.json to detect frameworks and dynamically\n * imports the corresponding ESLint plugins. This allows the config to be generated\n * once at setup and automatically adapt when frameworks are added or removed.\n *\n * @param options.boundaries - Whether to include architecture boundaries config\n * @returns ESLint config file content as a string\n */\nexport function getEslintConfig(options: { boundaries?: boolean }): string {\n return `import { readFileSync } from \"fs\";\nimport { defineConfig } from \"eslint/config\";\nimport js from \"@eslint/js\";\nimport { importX } from \"eslint-plugin-import-x\";\nimport sonarjs from \"eslint-plugin-sonarjs\";\nimport sdl from \"@microsoft/eslint-plugin-sdl\";\nimport playwright from \"eslint-plugin-playwright\";\nimport eslintConfigPrettier from \"eslint-config-prettier\";\n${options.boundaries ? 'import boundariesConfig from \"./.safeword/eslint-boundaries.config.mjs\";' : ''}\n\n// Read package.json to detect frameworks at runtime\nconst pkg = JSON.parse(readFileSync(\"./package.json\", \"utf8\"));\nconst deps = { ...pkg.dependencies, ...pkg.devDependencies };\n\n// Build dynamic ignores based on detected frameworks\nconst ignores = [\"node_modules/\", \"dist/\", \"build/\", \"coverage/\"];\nif (deps[\"next\"]) ignores.push(\".next/\");\nif (deps[\"astro\"]) ignores.push(\".astro/\");\nif (deps[\"vue\"] || deps[\"nuxt\"]) ignores.push(\".nuxt/\");\nif (deps[\"svelte\"] || deps[\"@sveltejs/kit\"]) ignores.push(\".svelte-kit/\");\n\n// Start with base configs (always loaded)\nconst configs = [\n { ignores },\n js.configs.recommended,\n importX.flatConfigs.recommended,\n sonarjs.configs.recommended,\n ...sdl.configs.recommended,\n];\n\n// TypeScript support (detected from package.json)\nif (deps[\"typescript\"]) {\n const tseslint = await import(\"typescript-eslint\");\n configs.push(importX.flatConfigs.typescript);\n configs.push(...tseslint.default.configs.recommended);\n}\n\n// React/Next.js support\nif (deps[\"react\"] || deps[\"next\"]) {\n const react = await import(\"eslint-plugin-react\");\n const reactHooks = await import(\"eslint-plugin-react-hooks\");\n const jsxA11y = await import(\"eslint-plugin-jsx-a11y\");\n configs.push(react.default.configs.flat.recommended);\n configs.push(react.default.configs.flat[\"jsx-runtime\"]);\n configs.push({\n name: \"react-hooks\",\n plugins: { \"react-hooks\": reactHooks.default },\n rules: reactHooks.default.configs.recommended.rules,\n });\n configs.push(jsxA11y.default.flatConfigs.recommended);\n}\n\n// Next.js plugin\nif (deps[\"next\"]) {\n const nextPlugin = await import(\"@next/eslint-plugin-next\");\n configs.push({\n name: \"nextjs\",\n plugins: { \"@next/next\": nextPlugin.default },\n rules: nextPlugin.default.configs.recommended.rules,\n });\n}\n\n// Astro support\nif (deps[\"astro\"]) {\n const astro = await import(\"eslint-plugin-astro\");\n configs.push(...astro.default.configs.recommended);\n}\n\n// Vue support\nif (deps[\"vue\"] || deps[\"nuxt\"]) {\n const vue = await import(\"eslint-plugin-vue\");\n configs.push(...vue.default.configs[\"flat/recommended\"]);\n}\n\n// Svelte support\nif (deps[\"svelte\"] || deps[\"@sveltejs/kit\"]) {\n const svelte = await import(\"eslint-plugin-svelte\");\n configs.push(...svelte.default.configs.recommended);\n}\n\n// Electron support\nif (deps[\"electron\"]) {\n const electron = await import(\"@electron-toolkit/eslint-config\");\n configs.push(electron.default);\n}\n\n// Vitest support (scoped to test files)\nif (deps[\"vitest\"]) {\n const vitest = await import(\"@vitest/eslint-plugin\");\n configs.push({\n name: \"vitest\",\n files: [\"**/*.test.{js,ts,jsx,tsx}\", \"**/*.spec.{js,ts,jsx,tsx}\", \"**/tests/**\"],\n plugins: { vitest: vitest.default },\n languageOptions: {\n globals: { ...vitest.default.environments.env.globals },\n },\n rules: { ...vitest.default.configs.recommended.rules },\n });\n}\n\n// Playwright for e2e tests (always included - safeword sets up Playwright)\nconfigs.push({\n name: \"playwright\",\n files: [\"**/e2e/**\", \"**/*.e2e.{js,ts,jsx,tsx}\", \"**/playwright/**\"],\n ...playwright.configs[\"flat/recommended\"],\n});\n\n// Architecture boundaries${options.boundaries ? '\\nconfigs.push(boundariesConfig);' : ''}\n\n// eslint-config-prettier must be last to disable conflicting rules\nconfigs.push(eslintConfigPrettier);\n\nexport default defineConfig(configs);\n`;\n}\n\nexport const SETTINGS_HOOKS = {\n SessionStart: [\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/session-verify-agents.sh',\n },\n ],\n },\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/session-version.sh',\n },\n ],\n },\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/session-lint-check.sh',\n },\n ],\n },\n ],\n UserPromptSubmit: [\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/prompt-timestamp.sh',\n },\n ],\n },\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/prompt-questions.sh',\n },\n ],\n },\n ],\n Stop: [\n {\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/stop-quality.sh',\n },\n ],\n },\n ],\n PostToolUse: [\n {\n matcher: 'Write|Edit|MultiEdit|NotebookEdit',\n hooks: [\n {\n type: 'command',\n command: '\"$CLAUDE_PROJECT_DIR\"/.safeword/hooks/post-tool-lint.sh',\n },\n ],\n },\n ],\n};\n","/**\n * Architecture boundaries detection and config generation\n *\n * Auto-detects common architecture directories and generates\n * eslint-plugin-boundaries config with sensible hierarchy rules.\n */\n\nimport { join } from 'node:path';\nimport { exists } from './fs.js';\n\n/**\n * Architecture directories to detect, ordered from bottom to top of hierarchy.\n * Lower items can be imported by higher items, not vice versa.\n */\nconst ARCHITECTURE_DIRS = [\n 'types', // Bottom: can be imported by everything\n 'utils',\n 'lib',\n 'hooks',\n 'services',\n 'components',\n 'features',\n 'modules',\n 'app', // Top: can import everything\n] as const;\n\ntype ArchDir = (typeof ARCHITECTURE_DIRS)[number];\n\n/**\n * Hierarchy rules: what each layer can import\n * Lower layers have fewer import permissions\n */\nconst HIERARCHY: Record<ArchDir, ArchDir[]> = {\n types: [], // types can't import anything (pure type definitions)\n utils: ['types'],\n lib: ['utils', 'types'],\n hooks: ['lib', 'utils', 'types'],\n services: ['lib', 'utils', 'types'],\n components: ['hooks', 'services', 'lib', 'utils', 'types'],\n features: ['components', 'hooks', 'services', 'lib', 'utils', 'types'],\n modules: ['components', 'hooks', 'services', 'lib', 'utils', 'types'],\n app: ['features', 'modules', 'components', 'hooks', 'services', 'lib', 'utils', 'types'],\n};\n\nexport interface DetectedArchitecture {\n directories: ArchDir[];\n inSrc: boolean; // true if dirs are in src/, false if at root\n}\n\n/**\n * Detects architecture directories in the project\n * Always returns a result (even with 0 directories) - boundaries is always configured\n */\nexport function detectArchitecture(projectDir: string): DetectedArchitecture {\n const foundInSrc: ArchDir[] = [];\n const foundAtRoot: ArchDir[] = [];\n\n for (const dir of ARCHITECTURE_DIRS) {\n if (exists(join(projectDir, 'src', dir))) {\n foundInSrc.push(dir);\n }\n if (exists(join(projectDir, dir))) {\n foundAtRoot.push(dir);\n }\n }\n\n // Prefer src/ location if more dirs found there\n const inSrc = foundInSrc.length >= foundAtRoot.length;\n const found = inSrc ? foundInSrc : foundAtRoot;\n\n return { directories: found, inSrc };\n}\n\n/**\n * Generates the boundaries config file content\n */\nexport function generateBoundariesConfig(arch: DetectedArchitecture): string {\n const prefix = arch.inSrc ? 'src/' : '';\n const hasDirectories = arch.directories.length > 0;\n\n // Generate element definitions with mode: 'full' to match from project root only\n const elements = arch.directories\n .map(dir => ` { type: '${dir}', pattern: '${prefix}${dir}/**', mode: 'full' }`)\n .join(',\\n');\n\n // Generate rules (what each layer can import)\n const rules = arch.directories\n .filter(dir => HIERARCHY[dir].length > 0)\n .map(dir => {\n const allowed = HIERARCHY[dir].filter(dep => arch.directories.includes(dep));\n if (allowed.length === 0) return null;\n return ` { from: ['${dir}'], allow: [${allowed.map(d => `'${d}'`).join(', ')}] }`;\n })\n .filter(Boolean)\n .join(',\\n');\n\n const detectedInfo = hasDirectories\n ? `Detected directories: ${arch.directories.join(', ')} (${arch.inSrc ? 'in src/' : 'at root'})`\n : 'No architecture directories detected yet - add types/, utils/, components/, etc.';\n\n // Build elements array content (empty array if no directories)\n const elementsContent = elements || '';\n const rulesContent = rules || '';\n\n return `/**\n * Architecture Boundaries Configuration (AUTO-GENERATED)\n *\n * ${detectedInfo}\n *\n * This enforces import boundaries between architectural layers:\n * - Lower layers (types, utils) cannot import from higher layers (components, features)\n * - Uses 'warn' severity - informative, not blocking\n *\n * Recognized directories (in hierarchy order):\n * types → utils → lib → hooks/services → components → features/modules → app\n *\n * To customize, override in your eslint.config.mjs:\n * rules: { 'boundaries/element-types': ['error', { ... }] }\n */\n\nimport boundaries from 'eslint-plugin-boundaries';\n\nexport default {\n plugins: { boundaries },\n settings: {\n 'boundaries/elements': [\n${elementsContent}\n ],\n },\n rules: {\n 'boundaries/element-types': ['warn', {\n default: 'disallow',\n rules: [\n${rulesContent}\n ],\n }],\n 'boundaries/no-unknown': 'off', // Allow files outside defined elements\n 'boundaries/no-unknown-files': 'off', // Allow non-matching files\n },\n};\n`;\n}\n","/**\n * Shared installation constants\n *\n * These constants are used by schema.ts to define the single source of truth.\n * All functions have been removed - reconcile.ts now handles all operations.\n */\n\n/**\n * Husky pre-commit hook content - includes safeword sync + lint-staged\n * The sync command keeps ESLint plugins aligned with detected frameworks\n */\nexport const HUSKY_PRE_COMMIT_CONTENT = 'npx safeword sync --quiet --stage\\nnpx lint-staged\\n';\n\n/**\n * MCP servers installed by safeword\n */\nexport const MCP_SERVERS = {\n context7: {\n command: 'npx',\n args: ['-y', '@upstash/context7-mcp@latest'],\n },\n playwright: {\n command: 'npx',\n args: ['@playwright/mcp@latest'],\n },\n} as const;\n\n// NOTE: All other constants and functions were removed in the declarative schema refactor.\n// The single source of truth is now SAFEWORD_SCHEMA in src/schema.ts.\n// Operations are handled by reconcile() in src/reconcile.ts.\n","/**\n * Hook utilities for Claude Code settings\n */\n\ninterface HookCommand {\n type: string;\n command: string;\n}\n\ninterface HookEntry {\n matcher?: string;\n hooks: HookCommand[];\n}\n\n/**\n * Type guard to check if a value is a hook entry with hooks array\n */\nexport function isHookEntry(h: unknown): h is HookEntry {\n return (\n typeof h === 'object' &&\n h !== null &&\n 'hooks' in h &&\n Array.isArray((h as HookEntry).hooks)\n );\n}\n\n/**\n * Check if a hook entry contains a safeword hook (command contains '.safeword')\n */\nexport function isSafewordHook(h: unknown): boolean {\n if (!isHookEntry(h)) return false;\n return h.hooks.some(\n (cmd) => typeof cmd.command === 'string' && cmd.command.includes('.safeword'),\n );\n}\n\n/**\n * Filter out safeword hooks from an array of hook entries\n */\nexport function filterOutSafewordHooks(hooks: unknown[]): unknown[] {\n return hooks.filter((h) => !isSafewordHook(h));\n}\n","/**\n * SAFEWORD Schema - Single Source of Truth\n *\n * All files, directories, configurations, and packages managed by safeword\n * are defined here. Commands use this schema via the reconciliation engine.\n *\n * Adding a new file? Add it here and it will be handled by setup/upgrade/reset.\n */\n\nimport { VERSION } from './version.js';\nimport { type ProjectType } from './utils/project-detector.js';\nimport { AGENTS_MD_LINK, getPrettierConfig, LINT_STAGED_CONFIG } from './templates/content.js';\nimport { getEslintConfig, SETTINGS_HOOKS } from './templates/config.js';\nimport { generateBoundariesConfig, detectArchitecture } from './utils/boundaries.js';\nimport { HUSKY_PRE_COMMIT_CONTENT, MCP_SERVERS } from './utils/install.js';\nimport { filterOutSafewordHooks } from './utils/hooks.js';\n\n// ============================================================================\n// Interfaces\n// ============================================================================\n\nexport interface ProjectContext {\n cwd: string;\n projectType: ProjectType;\n devDeps: Record<string, string>;\n isGitRepo: boolean;\n}\n\nexport interface FileDefinition {\n template?: string; // Path in templates/ dir\n content?: string | (() => string); // Static content or factory\n generator?: (ctx: ProjectContext) => string; // Dynamic generator needing context\n}\n\nexport interface ManagedFileDefinition extends FileDefinition {\n // managedFiles: created if missing, updated only if content === current template output\n}\n\nexport interface JsonMergeDefinition {\n keys: string[]; // Dot-notation keys we manage\n conditionalKeys?: Record<string, string[]>; // Keys added based on project type\n merge: (existing: Record<string, unknown>, ctx: ProjectContext) => Record<string, unknown>;\n unmerge: (existing: Record<string, unknown>) => Record<string, unknown>;\n removeFileIfEmpty?: boolean; // Delete file if our keys were the only content\n}\n\nexport interface TextPatchDefinition {\n operation: 'prepend' | 'append';\n content: string;\n marker: string; // Used to detect if already applied & for removal\n createIfMissing: boolean;\n}\n\nexport interface SafewordSchema {\n version: string;\n ownedDirs: string[]; // Fully owned - create on setup, delete on reset\n sharedDirs: string[]; // We add to but don't own\n preservedDirs: string[]; // Created on setup, NOT deleted on reset (user data)\n ownedFiles: Record<string, FileDefinition>; // Overwrite on upgrade (if changed)\n managedFiles: Record<string, ManagedFileDefinition>; // Create if missing, update if safeword content\n jsonMerges: Record<string, JsonMergeDefinition>;\n textPatches: Record<string, TextPatchDefinition>;\n packages: {\n base: string[];\n conditional: Record<string, string[]>;\n };\n}\n\n// ============================================================================\n// SAFEWORD_SCHEMA - The Single Source of Truth\n// ============================================================================\n\nexport const SAFEWORD_SCHEMA: SafewordSchema = {\n version: VERSION,\n\n // Directories fully owned by safeword (created on setup, deleted on reset)\n ownedDirs: [\n '.safeword',\n '.safeword/hooks',\n '.safeword/lib',\n '.safeword/guides',\n '.safeword/templates',\n '.safeword/prompts',\n '.safeword/planning',\n '.safeword/planning/user-stories',\n '.safeword/planning/test-definitions',\n '.safeword/planning/design',\n '.safeword/planning/issues',\n '.husky',\n ],\n\n // Directories we add to but don't own (not deleted on reset)\n sharedDirs: ['.claude', '.claude/skills', '.claude/commands'],\n\n // Created on setup but NOT deleted on reset (preserves user data)\n preservedDirs: ['.safeword/learnings', '.safeword/tickets', '.safeword/tickets/completed'],\n\n // Files owned by safeword (overwritten on upgrade if content changed)\n ownedFiles: {\n // Core files\n '.safeword/SAFEWORD.md': { template: 'SAFEWORD.md' },\n '.safeword/version': { content: () => VERSION },\n '.safeword/eslint-boundaries.config.mjs': {\n generator: ctx => generateBoundariesConfig(detectArchitecture(ctx.cwd)),\n },\n\n // Hooks (7 files)\n '.safeword/hooks/session-verify-agents.sh': { template: 'hooks/session-verify-agents.sh' },\n '.safeword/hooks/session-version.sh': { template: 'hooks/session-version.sh' },\n '.safeword/hooks/session-lint-check.sh': { template: 'hooks/session-lint-check.sh' },\n '.safeword/hooks/prompt-timestamp.sh': { template: 'hooks/prompt-timestamp.sh' },\n '.safeword/hooks/prompt-questions.sh': { template: 'hooks/prompt-questions.sh' },\n '.safeword/hooks/post-tool-lint.sh': { template: 'hooks/post-tool-lint.sh' },\n '.safeword/hooks/stop-quality.sh': { template: 'hooks/stop-quality.sh' },\n\n // Lib (2 files)\n '.safeword/lib/common.sh': { template: 'lib/common.sh' },\n '.safeword/lib/jq-fallback.sh': { template: 'lib/jq-fallback.sh' },\n\n // Guides (13 files)\n '.safeword/guides/architecture-guide.md': { template: 'guides/architecture-guide.md' },\n '.safeword/guides/cli-reference.md': { template: 'guides/cli-reference.md' },\n '.safeword/guides/code-philosophy.md': { template: 'guides/code-philosophy.md' },\n '.safeword/guides/context-files-guide.md': { template: 'guides/context-files-guide.md' },\n '.safeword/guides/data-architecture-guide.md': {\n template: 'guides/data-architecture-guide.md',\n },\n '.safeword/guides/design-doc-guide.md': { template: 'guides/design-doc-guide.md' },\n '.safeword/guides/development-workflow.md': { template: 'guides/development-workflow.md' },\n '.safeword/guides/learning-extraction.md': { template: 'guides/learning-extraction.md' },\n '.safeword/guides/llm-guide.md': { template: 'guides/llm-guide.md' },\n '.safeword/guides/tdd-best-practices.md': { template: 'guides/tdd-best-practices.md' },\n '.safeword/guides/test-definitions-guide.md': { template: 'guides/test-definitions-guide.md' },\n '.safeword/guides/user-story-guide.md': { template: 'guides/user-story-guide.md' },\n '.safeword/guides/zombie-process-cleanup.md': { template: 'guides/zombie-process-cleanup.md' },\n\n // Templates (5 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/test-definitions-feature.md': {\n template: 'doc-templates/test-definitions-feature.md',\n },\n '.safeword/templates/ticket-template.md': { template: 'doc-templates/ticket-template.md' },\n '.safeword/templates/user-stories-template.md': {\n template: 'doc-templates/user-stories-template.md',\n },\n\n // Prompts (2 files)\n '.safeword/prompts/architecture.md': { template: 'prompts/architecture.md' },\n '.safeword/prompts/quality-review.md': { template: 'prompts/quality-review.md' },\n\n // Claude skills and commands (4 files)\n '.claude/skills/safeword-quality-reviewer/SKILL.md': {\n template: 'skills/safeword-quality-reviewer/SKILL.md',\n },\n '.claude/commands/architecture.md': { template: 'commands/architecture.md' },\n '.claude/commands/lint.md': { template: 'commands/lint.md' },\n '.claude/commands/quality-review.md': { template: 'commands/quality-review.md' },\n\n // Husky (1 file)\n '.husky/pre-commit': { content: HUSKY_PRE_COMMIT_CONTENT },\n },\n\n // Files created if missing, updated only if content matches current template\n managedFiles: {\n 'eslint.config.mjs': {\n generator: () => getEslintConfig({ boundaries: true }),\n },\n '.prettierrc': { generator: ctx => getPrettierConfig(ctx.projectType) },\n '.markdownlint-cli2.jsonc': { template: 'markdownlint-cli2.jsonc' },\n },\n\n // JSON files where we merge specific keys\n jsonMerges: {\n 'package.json': {\n keys: [\n 'scripts.lint',\n 'scripts.lint:md',\n 'scripts.format',\n 'scripts.format:check',\n 'scripts.knip',\n 'scripts.prepare',\n 'lint-staged',\n ],\n conditionalKeys: {\n publishableLibrary: ['scripts.publint'],\n },\n merge: (existing, ctx) => {\n const scripts = (existing.scripts as Record<string, string>) ?? {};\n const result = { ...existing };\n\n // Add scripts if not present\n if (!scripts.lint) scripts.lint = 'eslint .';\n if (!scripts['lint:md']) scripts['lint:md'] = 'markdownlint-cli2 \"**/*.md\" \"#node_modules\"';\n if (!scripts.format) scripts.format = 'prettier --write .';\n if (!scripts['format:check']) scripts['format:check'] = 'prettier --check .';\n if (!scripts.knip) scripts.knip = 'knip';\n if (!scripts.prepare) scripts.prepare = 'husky || true';\n\n // Conditional: publint for publishable libraries\n if (ctx.projectType.publishableLibrary && !scripts.publint) {\n scripts.publint = 'publint';\n }\n\n result.scripts = scripts;\n\n // Add lint-staged config\n if (!existing['lint-staged']) {\n result['lint-staged'] = LINT_STAGED_CONFIG;\n }\n\n return result;\n },\n unmerge: existing => {\n const result = { ...existing };\n const scripts = { ...((existing.scripts as Record<string, string>) ?? {}) };\n\n // Remove safeword-specific scripts but preserve lint/format (useful standalone)\n delete scripts['lint:md'];\n delete scripts['format:check'];\n delete scripts.knip;\n delete scripts.prepare;\n delete scripts.publint;\n\n if (Object.keys(scripts).length > 0) {\n result.scripts = scripts;\n } else {\n delete result.scripts;\n }\n\n delete result['lint-staged'];\n\n return result;\n },\n },\n\n '.claude/settings.json': {\n keys: ['hooks'],\n merge: existing => {\n // Preserve non-safeword hooks while adding/updating safeword hooks\n const existingHooks = (existing.hooks as Record<string, unknown[]>) ?? {};\n const mergedHooks: Record<string, unknown[]> = { ...existingHooks };\n\n for (const [event, newHooks] of Object.entries(SETTINGS_HOOKS)) {\n const eventHooks = (mergedHooks[event] as unknown[]) ?? [];\n const nonSafewordHooks = filterOutSafewordHooks(eventHooks);\n mergedHooks[event] = [...nonSafewordHooks, ...newHooks];\n }\n\n return { ...existing, hooks: mergedHooks };\n },\n unmerge: existing => {\n // Remove only safeword hooks, preserve custom hooks\n const existingHooks = (existing.hooks as Record<string, unknown[]>) ?? {};\n const cleanedHooks: Record<string, unknown[]> = {};\n\n for (const [event, eventHooks] of Object.entries(existingHooks)) {\n const nonSafewordHooks = filterOutSafewordHooks(eventHooks as unknown[]);\n if (nonSafewordHooks.length > 0) {\n cleanedHooks[event] = nonSafewordHooks;\n }\n }\n\n const result = { ...existing };\n if (Object.keys(cleanedHooks).length > 0) {\n result.hooks = cleanedHooks;\n } else {\n delete result.hooks;\n }\n return result;\n },\n },\n\n '.mcp.json': {\n keys: ['mcpServers.context7', 'mcpServers.playwright'],\n removeFileIfEmpty: true,\n merge: existing => {\n const mcpServers = (existing.mcpServers as Record<string, unknown>) ?? {};\n return {\n ...existing,\n mcpServers: {\n ...mcpServers,\n context7: MCP_SERVERS.context7,\n playwright: MCP_SERVERS.playwright,\n },\n };\n },\n unmerge: existing => {\n const result = { ...existing };\n const mcpServers = { ...((existing.mcpServers as Record<string, unknown>) ?? {}) };\n\n delete mcpServers.context7;\n delete mcpServers.playwright;\n\n if (Object.keys(mcpServers).length > 0) {\n result.mcpServers = mcpServers;\n } else {\n delete result.mcpServers;\n }\n\n return result;\n },\n },\n },\n\n // Text files where we patch specific content\n textPatches: {\n 'AGENTS.md': {\n operation: 'prepend',\n content: AGENTS_MD_LINK,\n marker: '@./.safeword/SAFEWORD.md',\n createIfMissing: true,\n },\n 'CLAUDE.md': {\n operation: 'prepend',\n content: AGENTS_MD_LINK,\n marker: '@./.safeword/SAFEWORD.md',\n createIfMissing: false, // Only patch if exists, don't create (AGENTS.md is primary)\n },\n },\n\n // NPM packages to install\n packages: {\n base: [\n 'eslint',\n 'prettier',\n '@eslint/js',\n 'eslint-plugin-import-x',\n 'eslint-plugin-sonarjs',\n 'eslint-plugin-boundaries',\n 'eslint-plugin-playwright',\n '@microsoft/eslint-plugin-sdl',\n 'eslint-config-prettier',\n 'markdownlint-cli2',\n 'knip',\n 'husky',\n 'lint-staged',\n ],\n conditional: {\n typescript: ['typescript-eslint'],\n react: ['eslint-plugin-react', 'eslint-plugin-react-hooks', 'eslint-plugin-jsx-a11y'],\n nextjs: ['@next/eslint-plugin-next'],\n astro: ['eslint-plugin-astro', 'prettier-plugin-astro'],\n vue: ['eslint-plugin-vue'],\n svelte: ['eslint-plugin-svelte', 'prettier-plugin-svelte'],\n electron: ['@electron-toolkit/eslint-config'],\n vitest: ['@vitest/eslint-plugin'],\n tailwind: ['prettier-plugin-tailwindcss'],\n publishableLibrary: ['publint'],\n },\n },\n};\n"],"mappings":"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","names":["join","join"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/diff.ts"],"sourcesContent":["/**\n * Diff command - Preview changes that would be made by upgrade\n *\n * Uses reconcile() with dryRun to compute what would change.\n */\n\nimport { join } from 'node:path';\nimport { VERSION } from '../version.js';\nimport { exists, readFileSafe } from '../utils/fs.js';\nimport { info, success, error, header, listItem } from '../utils/output.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { reconcile, type Action } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\n\nexport interface DiffOptions {\n verbose?: boolean;\n}\n\ninterface FileDiff {\n path: string;\n status: 'added' | 'modified' | 'unchanged';\n currentContent?: string;\n newContent?: string;\n}\n\n/**\n * Create a unified diff between two strings\n */\nfunction createUnifiedDiff(oldContent: string, newContent: string, filename: string): string {\n const oldLines = oldContent.split('\\n');\n const newLines = newContent.split('\\n');\n\n const lines: string[] = [];\n lines.push(`--- a/${filename}`);\n lines.push(`+++ b/${filename}`);\n\n // Simple diff - show all changes\n let hasChanges = false;\n\n const maxLines = Math.max(oldLines.length, newLines.length);\n\n for (let i = 0; i < maxLines; i++) {\n const oldLine = oldLines[i];\n const newLine = newLines[i];\n\n if (oldLine === newLine) {\n lines.push(` ${oldLine ?? ''}`);\n } else {\n hasChanges = true;\n if (oldLine !== undefined) {\n lines.push(`-${oldLine}`);\n }\n if (newLine !== undefined) {\n lines.push(`+${newLine}`);\n }\n }\n }\n\n if (!hasChanges) {\n return '';\n }\n\n // Add context marker\n lines.splice(2, 0, `@@ -1,${oldLines.length} +1,${newLines.length} @@`);\n\n return lines.join('\\n');\n}\n\n/**\n * Convert reconcile actions to file diffs\n */\nfunction actionsToDiffs(actions: Action[], cwd: string): FileDiff[] {\n const diffs: FileDiff[] = [];\n const seenPaths = new Set<string>();\n\n for (const action of actions) {\n if (action.type === 'write') {\n if (seenPaths.has(action.path)) continue;\n seenPaths.add(action.path);\n\n const fullPath = join(cwd, action.path);\n const currentContent = readFileSafe(fullPath);\n\n if (currentContent === null) {\n diffs.push({\n path: action.path,\n status: 'added',\n newContent: action.content,\n });\n } else if (currentContent.trim() !== action.content.trim()) {\n diffs.push({\n path: action.path,\n status: 'modified',\n currentContent,\n newContent: action.content,\n });\n } else {\n diffs.push({\n path: action.path,\n status: 'unchanged',\n currentContent,\n newContent: action.content,\n });\n }\n }\n }\n\n return diffs;\n}\n\nexport async function diff(options: DiffOptions): Promise<void> {\n const cwd = process.cwd();\n const safewordDir = join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDir)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n // Read project version\n const versionPath = join(safewordDir, 'version');\n const projectVersion = readFileSafe(versionPath)?.trim() ?? 'unknown';\n\n header('Safeword Diff');\n info(`Changes from v${projectVersion} → v${VERSION}`);\n\n // Use reconcile with dryRun to compute changes\n const ctx = createProjectContext(cwd);\n const result = await reconcile(SAFEWORD_SCHEMA, 'upgrade', ctx, { dryRun: true });\n\n // Convert actions to file diffs\n const diffs = actionsToDiffs(result.actions, cwd);\n\n const added = diffs.filter(d => d.status === 'added');\n const modified = diffs.filter(d => d.status === 'modified');\n const unchanged = diffs.filter(d => d.status === 'unchanged');\n\n // Summary\n info(\n `\\nSummary: ${added.length} added, ${modified.length} modified, ${unchanged.length} unchanged`,\n );\n\n // Package changes\n if (result.packagesToInstall.length > 0) {\n info(`\\nPackages to install: ${result.packagesToInstall.length}`);\n }\n\n // List by category\n if (added.length > 0) {\n info('\\nAdded:');\n for (const file of added) {\n listItem(file.path);\n }\n }\n\n if (modified.length > 0) {\n info('\\nModified:');\n for (const file of modified) {\n listItem(file.path);\n }\n }\n\n if (unchanged.length > 0) {\n info('\\nUnchanged:');\n for (const file of unchanged) {\n listItem(file.path);\n }\n }\n\n // Verbose output - show actual diffs\n if (options.verbose) {\n header('Detailed Changes');\n\n for (const file of modified) {\n if (file.currentContent && file.newContent) {\n info(`\\n${file.path}:`);\n const diffOutput = createUnifiedDiff(file.currentContent, file.newContent, file.path);\n if (diffOutput) {\n console.log(diffOutput);\n }\n }\n }\n\n for (const file of added) {\n if (file.newContent) {\n info(`\\n${file.path}: (new file)`);\n const lines = file.newContent.split('\\n').slice(0, 10);\n for (const line of lines) {\n console.log(`+${line}`);\n }\n if (file.newContent.split('\\n').length > 10) {\n console.log('... (truncated)');\n }\n }\n }\n\n if (result.packagesToInstall.length > 0) {\n info('\\nPackages to install:');\n for (const pkg of result.packagesToInstall) {\n listItem(pkg);\n }\n }\n }\n\n if (added.length === 0 && modified.length === 0 && result.packagesToInstall.length === 0) {\n success('\\nNo changes needed - configuration is up to date');\n } else {\n info('\\nRun `safeword upgrade` to apply these changes');\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -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 { join, basename } from 'node:path';\nimport { VERSION } from '../version.js';\nimport { exists, writeJson } from '../utils/fs.js';\nimport { info, success, warn, error, header, listItem } from '../utils/output.js';\nimport { isGitRepo } from '../utils/git.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { reconcile } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\nimport { execSync } from 'node:child_process';\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\nexport async function setup(options: SetupOptions): Promise<void> {\n const cwd = process.cwd();\n const safewordDir = join(cwd, '.safeword');\n\n // Check if already configured\n if (exists(safewordDir)) {\n error('Already configured. Run `safeword upgrade` to update.');\n process.exit(1);\n }\n\n // Check for package.json, create if missing\n const packageJsonPath = join(cwd, 'package.json');\n let packageJsonCreated = false;\n if (!exists(packageJsonPath)) {\n const dirName = basename(cwd) || 'project';\n const defaultPackageJson: PackageJson = {\n name: dirName,\n version: '0.1.0',\n scripts: {},\n };\n writeJson(packageJsonPath, defaultPackageJson);\n packageJsonCreated = true;\n }\n\n const isNonInteractive = options.yes || !process.stdin.isTTY;\n\n header('Safeword Setup');\n info(`Version: ${VERSION}`);\n\n if (packageJsonCreated) {\n info('Created package.json (none found)');\n }\n\n try {\n // Use reconcile with mode='install' to create all managed files\n info('\\nCreating safeword configuration...');\n const ctx = createProjectContext(cwd);\n const result = await reconcile(SAFEWORD_SCHEMA, 'install', ctx);\n\n success('Created .safeword directory and configuration');\n\n // Install npm dependencies\n if (result.packagesToInstall.length > 0) {\n info('\\nInstalling linting dependencies...');\n\n try {\n const installCmd = `npm install -D ${result.packagesToInstall.join(' ')}`;\n info(`Running: ${installCmd}`);\n execSync(installCmd, { cwd, stdio: 'inherit' });\n success('Installed linting dependencies');\n } catch {\n warn('Failed to install dependencies. Run manually:');\n listItem(`npm install -D ${result.packagesToInstall.join(' ')}`);\n }\n }\n\n // Report Husky status\n if (!isGitRepo(cwd)) {\n if (isNonInteractive) {\n warn('Skipped Husky setup (no git repository)');\n } else {\n warn('Skipped Husky setup (no .git directory)');\n info('Initialize git and run safeword upgrade to enable pre-commit hooks');\n }\n }\n\n // Print summary\n header('Setup Complete');\n\n if (result.created.length > 0 || packageJsonCreated) {\n info('\\nCreated:');\n if (packageJsonCreated) {\n listItem('package.json');\n }\n for (const file of result.created) {\n listItem(file);\n }\n }\n\n if (result.updated.length > 0) {\n info('\\nModified:');\n for (const file of result.updated) {\n listItem(file);\n }\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 } catch (err) {\n error(`Setup failed: ${err instanceof Error ? err.message : 'Unknown error'}`);\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAMA,SAAS,MAAM,gBAAgB;AAQ/B,SAAS,gBAAgB;AAezB,eAAsB,MAAM,SAAsC;AAChE,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,cAAc,KAAK,KAAK,WAAW;AAGzC,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,uDAAuD;AAC7D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,kBAAkB,KAAK,KAAK,cAAc;AAChD,MAAI,qBAAqB;AACzB,MAAI,CAAC,OAAO,eAAe,GAAG;AAC5B,UAAM,UAAU,SAAS,GAAG,KAAK;AACjC,UAAM,qBAAkC;AAAA,MACtC,MAAM;AAAA,MACN,SAAS;AAAA,MACT,SAAS,CAAC;AAAA,IACZ;AACA,cAAU,iBAAiB,kBAAkB;AAC7C,yBAAqB;AAAA,EACvB;AAEA,QAAM,mBAAmB,QAAQ,OAAO,CAAC,QAAQ,MAAM;AAEvD,SAAO,gBAAgB;AACvB,OAAK,YAAY,OAAO,EAAE;AAE1B,MAAI,oBAAoB;AACtB,SAAK,mCAAmC;AAAA,EAC1C;AAEA,MAAI;AAEF,SAAK,sCAAsC;AAC3C,UAAM,MAAM,qBAAqB,GAAG;AACpC,UAAM,SAAS,MAAM,UAAU,iBAAiB,WAAW,GAAG;AAE9D,YAAQ,+CAA+C;AAGvD,QAAI,OAAO,kBAAkB,SAAS,GAAG;AACvC,WAAK,sCAAsC;AAE3C,UAAI;AACF,cAAM,aAAa,kBAAkB,OAAO,kBAAkB,KAAK,GAAG,CAAC;AACvE,aAAK,YAAY,UAAU,EAAE;AAC7B,iBAAS,YAAY,EAAE,KAAK,OAAO,UAAU,CAAC;AAC9C,gBAAQ,gCAAgC;AAAA,MAC1C,QAAQ;AACN,aAAK,+CAA+C;AACpD,iBAAS,kBAAkB,OAAO,kBAAkB,KAAK,GAAG,CAAC,EAAE;AAAA,MACjE;AAAA,IACF;AAGA,QAAI,CAAC,UAAU,GAAG,GAAG;AACnB,UAAI,kBAAkB;AACpB,aAAK,yCAAyC;AAAA,MAChD,OAAO;AACL,aAAK,yCAAyC;AAC9C,aAAK,oEAAoE;AAAA,MAC3E;AAAA,IACF;AAGA,WAAO,gBAAgB;AAEvB,QAAI,OAAO,QAAQ,SAAS,KAAK,oBAAoB;AACnD,WAAK,YAAY;AACjB,UAAI,oBAAoB;AACtB,iBAAS,cAAc;AAAA,MACzB;AACA,iBAAW,QAAQ,OAAO,SAAS;AACjC,iBAAS,IAAI;AAAA,MACf;AAAA,IACF;AAEA,QAAI,OAAO,QAAQ,SAAS,GAAG;AAC7B,WAAK,aAAa;AAClB,iBAAW,QAAQ,OAAO,SAAS;AACjC,iBAAS,IAAI;AAAA,MACf;AAAA,IACF;AAEA,SAAK,eAAe;AACpB,aAAS,sCAAsC;AAC/C,aAAS,6BAA6B;AAEtC,YAAQ;AAAA,WAAc,OAAO,0BAA0B;AAAA,EACzD,SAAS,KAAK;AACZ,UAAM,iBAAiB,eAAe,QAAQ,IAAI,UAAU,eAAe,EAAE;AAC7E,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/sync.ts"],"sourcesContent":["/**\n * Sync command - Keep linting plugins in sync with project dependencies\n *\n * Detects frameworks in package.json and ensures the corresponding ESLint plugins\n * are installed. Designed to be called from Husky pre-commit hook.\n *\n * Behavior:\n * - Fast exit when nothing needs to change\n * - Installs missing plugins\n * - Optionally stages modified files (--stage flag for pre-commit)\n * - Clear error message if installation fails\n */\n\nimport { join } from 'node:path';\nimport { execSync } from 'node:child_process';\nimport { exists, readJson } from '../utils/fs.js';\nimport {\n detectProjectType,\n type PackageJson,\n type ProjectType,\n} from '../utils/project-detector.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\n\nexport interface SyncOptions {\n quiet?: boolean;\n stage?: boolean;\n}\n\n/**\n * Base ESLint packages always required for linting.\n * Explicit list for clarity - sync only cares about ESLint plugins.\n *\n * NOTE: This is a subset of SAFEWORD_SCHEMA.packages.base (which includes\n * prettier, markdownlint-cli2, husky, lint-staged, knip). When adding new\n * ESLint packages to schema.ts, also add them here if they should be\n * auto-installed on pre-commit.\n */\nconst BASE_ESLINT_PACKAGES = [\n 'eslint',\n '@eslint/js',\n 'eslint-plugin-import-x',\n 'eslint-plugin-sonarjs',\n '@microsoft/eslint-plugin-sdl',\n 'eslint-config-prettier',\n 'eslint-plugin-boundaries',\n 'eslint-plugin-playwright',\n];\n\n/**\n * Get required ESLint packages based on project type.\n * Uses explicit base list + SAFEWORD_SCHEMA.packages.conditional for frameworks.\n */\nfunction getRequiredPlugins(projectType: ProjectType): string[] {\n const plugins: string[] = [...BASE_ESLINT_PACKAGES];\n\n // Add conditional packages from schema based on detected project type\n const { conditional } = SAFEWORD_SCHEMA.packages;\n\n if (projectType.typescript && conditional.typescript) {\n plugins.push(...conditional.typescript);\n }\n if ((projectType.react || projectType.nextjs) && conditional.react) {\n plugins.push(...conditional.react);\n }\n if (projectType.nextjs && conditional.nextjs) {\n plugins.push(...conditional.nextjs);\n }\n if (projectType.astro && conditional.astro) {\n plugins.push(...conditional.astro);\n }\n if (projectType.vue && conditional.vue) {\n plugins.push(...conditional.vue);\n }\n if (projectType.svelte && conditional.svelte) {\n plugins.push(...conditional.svelte);\n }\n if (projectType.electron && conditional.electron) {\n plugins.push(...conditional.electron);\n }\n if (projectType.vitest && conditional.vitest) {\n plugins.push(...conditional.vitest);\n }\n\n return plugins;\n}\n\n/**\n * Check which packages are missing from devDependencies\n */\nfunction getMissingPackages(required: string[], installed: Record<string, string>): string[] {\n return required.filter(pkg => !(pkg in installed));\n}\n\n/**\n * Sync linting configuration with current project dependencies\n */\nexport async function sync(options: SyncOptions = {}): Promise<void> {\n const cwd = process.cwd();\n const safewordDir = join(cwd, '.safeword');\n const packageJsonPath = join(cwd, 'package.json');\n\n // Must be in a safeword project\n if (!exists(safewordDir)) {\n if (!options.quiet) {\n console.error('Not a safeword project. Run `safeword setup` first.');\n }\n process.exit(1);\n }\n\n if (!exists(packageJsonPath)) {\n if (!options.quiet) {\n console.error('No package.json found.');\n }\n process.exit(1);\n }\n\n const packageJson = readJson<PackageJson>(packageJsonPath);\n if (!packageJson) {\n process.exit(1);\n }\n\n // Detect current project type\n const projectType = detectProjectType(packageJson);\n const devDeps = packageJson.devDependencies || {};\n\n // Check for missing plugins\n const requiredPlugins = getRequiredPlugins(projectType);\n const missingPlugins = getMissingPackages(requiredPlugins, devDeps);\n\n // Fast exit if nothing to install\n if (missingPlugins.length === 0) {\n return;\n }\n\n // Install missing plugins\n if (!options.quiet) {\n console.log(`Installing missing ESLint plugins: ${missingPlugins.join(', ')}`);\n }\n\n try {\n execSync(`npm install -D ${missingPlugins.join(' ')}`, {\n cwd,\n stdio: options.quiet ? 'pipe' : 'inherit',\n });\n } catch (error) {\n // Clear error message for network/install failures\n const pluginList = missingPlugins.join(' ');\n console.error(`\\n✗ Failed to install ESLint plugins\\n`);\n console.error(`Your project needs: ${pluginList}`);\n console.error(`\\nRun manually when online:`);\n console.error(` npm install -D ${pluginList}\\n`);\n process.exit(1);\n }\n\n // Stage modified files if --stage flag is set (for pre-commit hook)\n if (options.stage) {\n try {\n execSync('git add package.json package-lock.json', {\n cwd,\n stdio: 'pipe',\n });\n } catch {\n // Not in a git repo or git add failed - ignore\n }\n }\n\n if (!options.quiet) {\n console.log(`✓ Installed ${missingPlugins.length} ESLint plugin(s)`);\n }\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|
|
File without changes
|