@lkangd/cc-env 1.1.0 → 1.1.2
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/LICENSE +15 -0
- package/dist/cli.js +3 -4
- package/package.json +8 -1
- package/.claude/settings.json +0 -6
- package/.claude/settings.local.json +0 -8
- package/.nvmrc +0 -1
- package/CHANGELOG.md +0 -66
- package/docs/product-specs/index.draft.md +0 -106
- package/docs/product-specs/index.md +0 -911
- package/docs/product-specs/optional.md +0 -42
- package/docs/references/claude-code-env.md +0 -224
- package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +0 -1331
- package/docs/superpowers/plans/2026-04-24-cc-env.md +0 -1666
- package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +0 -1432
- package/docs/superpowers/specs/2026-04-24-cc-env-design.md +0 -438
- package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +0 -181
- package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +0 -78
- package/src/cli.ts +0 -339
- package/src/commands/init.ts +0 -139
- package/src/commands/preset/create.ts +0 -96
- package/src/commands/preset/delete.ts +0 -62
- package/src/commands/preset/show.ts +0 -51
- package/src/commands/restore.ts +0 -150
- package/src/commands/run.ts +0 -158
- package/src/core/errors.ts +0 -13
- package/src/core/find-claude.ts +0 -70
- package/src/core/format.ts +0 -29
- package/src/core/fs.ts +0 -18
- package/src/core/gitignore.ts +0 -26
- package/src/core/logger.ts +0 -11
- package/src/core/mask.ts +0 -17
- package/src/core/paths.ts +0 -41
- package/src/core/process-env.ts +0 -11
- package/src/core/schema.ts +0 -55
- package/src/core/spawn.ts +0 -36
- package/src/flows/init-flow.ts +0 -61
- package/src/flows/preset-create-flow.ts +0 -129
- package/src/flows/restore-flow.ts +0 -144
- package/src/ink/init-app.tsx +0 -110
- package/src/ink/preset-create-app.tsx +0 -451
- package/src/ink/preset-delete-app.tsx +0 -114
- package/src/ink/preset-show-app.tsx +0 -76
- package/src/ink/restore-app.tsx +0 -230
- package/src/ink/run-preset-select-app.tsx +0 -83
- package/src/ink/summary.tsx +0 -91
- package/src/services/claude-settings-env-service.ts +0 -72
- package/src/services/history-service.ts +0 -48
- package/src/services/preset-service.ts +0 -72
- package/src/services/project-env-service.ts +0 -128
- package/src/services/project-state-service.ts +0 -31
- package/src/services/settings-env-service.ts +0 -40
- package/src/services/shell-env-service.ts +0 -112
- package/src/types.d.ts +0 -19
- package/tests/cli/help.test.ts +0 -133
- package/tests/cli/init.test.ts +0 -76
- package/tests/cli/restore.test.ts +0 -172
- package/tests/commands/create.test.ts +0 -263
- package/tests/commands/output.test.ts +0 -119
- package/tests/commands/run.test.ts +0 -218
- package/tests/core/gitignore.test.ts +0 -98
- package/tests/core/paths.test.ts +0 -24
- package/tests/core/schema-mask.test.ts +0 -182
- package/tests/core/spawn.test.ts +0 -47
- package/tests/flows/init-flow.test.ts +0 -40
- package/tests/flows/preset-create-flow.test.ts +0 -225
- package/tests/flows/restore-flow.test.ts +0 -157
- package/tests/integration/init-restore.test.ts +0 -406
- package/tests/services/claude-shell.test.ts +0 -183
- package/tests/services/storage.test.ts +0 -143
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -8
|
@@ -1,1331 +0,0 @@
|
|
|
1
|
-
# cc-env Init Shell Migration Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Redesign `cc-env init` and `restore` so selected env keys move between `~/.claude/settings*.json` and managed shell config blocks instead of presets.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Keep the current service-oriented shape: add one service for Claude home settings and one service for managed shell blocks, then rewrite `init`/`restore` to orchestrate those services. Preserve runtime env merge behavior, but expand history to record per-file backups and shell writes so restore can reverse an init migration without guessing.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node.js, TypeScript, Commander, Ink, zod, proper-lockfile, Vitest
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## File Structure
|
|
14
|
-
|
|
15
|
-
### Files to modify
|
|
16
|
-
- Modify: `src/core/schema.ts` — expand history types for init shell migration records
|
|
17
|
-
- Modify: `src/core/paths.ts` — add Claude home and shell config path helpers
|
|
18
|
-
- Modify: `src/commands/init.ts` — replace preset migration with Claude-home-to-shell migration
|
|
19
|
-
- Modify: `src/commands/restore.ts` — restore init records by removing shell keys and restoring both Claude settings files
|
|
20
|
-
- Modify: `src/flows/init-flow.ts` — remove preset target step, add required-key handling
|
|
21
|
-
- Modify: `src/flows/restore-flow.ts` — branch flow by history record action
|
|
22
|
-
- Modify: `src/ink/init-app.tsx` — show required keys and confirm shell migration
|
|
23
|
-
- Modify: `src/ink/restore-app.tsx` — show init restore confirmation path without preset/settings target selection
|
|
24
|
-
- Modify: `src/cli.ts` — wire new services and updated flow contracts
|
|
25
|
-
- Modify: `tests/core/schema-mask.test.ts` — cover new init history shape
|
|
26
|
-
- Modify: `tests/services/storage.test.ts` — persist expanded init history records
|
|
27
|
-
- Modify: `tests/flows/init-flow.test.ts` — cover required-key selection rules
|
|
28
|
-
- Modify: `tests/flows/restore-flow.test.ts` — cover init-record-specific restore flow path
|
|
29
|
-
- Modify: `tests/integration/init-restore.test.ts` — cover dual Claude settings sources and shell restoration
|
|
30
|
-
|
|
31
|
-
### Files to create
|
|
32
|
-
- Create: `tests/core/paths.test.ts` — verify Claude home and shell config path resolution
|
|
33
|
-
- Create: `src/services/claude-settings-env-service.ts` — read/write `~/.claude/settings.json` and `~/.claude/settings.local.json`
|
|
34
|
-
- Create: `src/services/shell-env-service.ts` — manage `cc-env` blocks in zsh/bash/fish config files
|
|
35
|
-
- Create: `tests/services/claude-shell.test.ts` — verify Claude settings service and shell block service behavior
|
|
36
|
-
|
|
37
|
-
### Responsibility boundaries
|
|
38
|
-
- `src/core/schema.ts` owns the persisted record shape. Do not bury history shape in commands.
|
|
39
|
-
- `src/services/claude-settings-env-service.ts` owns only `~/.claude/settings.json` and `~/.claude/settings.local.json`.
|
|
40
|
-
- `src/services/shell-env-service.ts` owns only the `# >>> cc-env >>>` managed blocks in shell config files.
|
|
41
|
-
- `src/commands/init.ts` computes effective values, backups, and call ordering; it does not parse shell files itself.
|
|
42
|
-
- `src/commands/restore.ts` reverses history records; it should not guess original ownership.
|
|
43
|
-
- `src/flows/*` stay pure and encode selection/confirmation rules without filesystem access.
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
### Task 1: Expand history and path primitives for shell migration
|
|
48
|
-
|
|
49
|
-
**Files:**
|
|
50
|
-
- Modify: `src/core/schema.ts`
|
|
51
|
-
- Modify: `src/core/paths.ts`
|
|
52
|
-
- Modify: `tests/core/schema-mask.test.ts`
|
|
53
|
-
- Modify: `tests/services/storage.test.ts`
|
|
54
|
-
- Create: `tests/core/paths.test.ts`
|
|
55
|
-
|
|
56
|
-
- [ ] **Step 1: Write the failing tests for the new init history shape and home/shell paths**
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
import { describe, expect, it } from 'vitest'
|
|
60
|
-
|
|
61
|
-
import { historySchema } from '../../src/core/schema.js'
|
|
62
|
-
|
|
63
|
-
describe('historySchema', () => {
|
|
64
|
-
it('accepts init history with per-file backups and shell writes', () => {
|
|
65
|
-
const result = historySchema.parse({
|
|
66
|
-
timestamp: '2026-04-24T12:00:00.000Z',
|
|
67
|
-
action: 'init',
|
|
68
|
-
migratedKeys: ['ANTHROPIC_AUTH_TOKEN'],
|
|
69
|
-
settingsBackup: {
|
|
70
|
-
ANTHROPIC_BASE_URL: 'https://settings.example.com',
|
|
71
|
-
},
|
|
72
|
-
settingsLocalBackup: {
|
|
73
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
74
|
-
},
|
|
75
|
-
shellWrites: [
|
|
76
|
-
{
|
|
77
|
-
shell: 'zsh',
|
|
78
|
-
filePath: '/Users/test/.zshrc',
|
|
79
|
-
env: {
|
|
80
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
expect(result.action).toBe('init')
|
|
87
|
-
expect(result.shellWrites[0]?.shell).toBe('zsh')
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
import { describe, expect, it } from 'vitest'
|
|
94
|
-
|
|
95
|
-
import {
|
|
96
|
-
resolveClaudeSettingsLocalPath,
|
|
97
|
-
resolveClaudeSettingsPath,
|
|
98
|
-
resolveShellConfigPaths,
|
|
99
|
-
} from '../../src/core/paths.js'
|
|
100
|
-
|
|
101
|
-
describe('Claude home path helpers', () => {
|
|
102
|
-
it('resolves both Claude settings files under the given home directory', () => {
|
|
103
|
-
expect(resolveClaudeSettingsPath('/Users/test')).toBe('/Users/test/.claude/settings.json')
|
|
104
|
-
expect(resolveClaudeSettingsLocalPath('/Users/test')).toBe(
|
|
105
|
-
'/Users/test/.claude/settings.local.json',
|
|
106
|
-
)
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('resolves zsh, bash, and fish config targets', () => {
|
|
110
|
-
expect(resolveShellConfigPaths('/Users/test')).toEqual({
|
|
111
|
-
zsh: '/Users/test/.zshrc',
|
|
112
|
-
bash: '/Users/test/.bashrc',
|
|
113
|
-
fish: '/Users/test/.config/fish/config.fish',
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
it('persists expanded init history records', async () => {
|
|
121
|
-
const service = createHistoryService(root)
|
|
122
|
-
|
|
123
|
-
await service.write({
|
|
124
|
-
timestamp: '2026-04-24T10:00:00.000Z',
|
|
125
|
-
action: 'init',
|
|
126
|
-
migratedKeys: ['ANTHROPIC_AUTH_TOKEN'],
|
|
127
|
-
settingsBackup: {},
|
|
128
|
-
settingsLocalBackup: {
|
|
129
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
130
|
-
},
|
|
131
|
-
shellWrites: [
|
|
132
|
-
{
|
|
133
|
-
shell: 'fish',
|
|
134
|
-
filePath: '/Users/test/.config/fish/config.fish',
|
|
135
|
-
env: {
|
|
136
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
await expect(service.list()).resolves.toMatchObject([
|
|
143
|
-
{
|
|
144
|
-
action: 'init',
|
|
145
|
-
shellWrites: [
|
|
146
|
-
{
|
|
147
|
-
shell: 'fish',
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
},
|
|
151
|
-
])
|
|
152
|
-
})
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
- [ ] **Step 2: Run the targeted tests to verify they fail for the expected reason**
|
|
156
|
-
|
|
157
|
-
Run: `npm test -- tests/core/schema-mask.test.ts tests/core/paths.test.ts tests/services/storage.test.ts`
|
|
158
|
-
Expected: FAIL because `historySchema` still expects `movedKeys` + `backup` + `targetType`, and the new path helpers do not exist yet.
|
|
159
|
-
|
|
160
|
-
- [ ] **Step 3: Implement the minimal schema and path changes**
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
import { z } from 'zod'
|
|
164
|
-
|
|
165
|
-
const envKeySchema = z.string().regex(/^[A-Z0-9_]+$/)
|
|
166
|
-
|
|
167
|
-
export const envMapSchema = z.record(
|
|
168
|
-
envKeySchema,
|
|
169
|
-
z.unknown()
|
|
170
|
-
.refine((value) => value === null || typeof value !== 'object')
|
|
171
|
-
.transform((value) => String(value)),
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
const shellWriteSchema = z.object({
|
|
175
|
-
shell: z.enum(['zsh', 'bash', 'fish']),
|
|
176
|
-
filePath: z.string(),
|
|
177
|
-
env: envMapSchema,
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
const initHistorySchema = z.object({
|
|
181
|
-
timestamp: z.string().datetime({ offset: true }),
|
|
182
|
-
action: z.literal('init'),
|
|
183
|
-
migratedKeys: z.array(envKeySchema),
|
|
184
|
-
settingsBackup: envMapSchema,
|
|
185
|
-
settingsLocalBackup: envMapSchema,
|
|
186
|
-
shellWrites: z.array(shellWriteSchema),
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const restoreHistorySchema = z.object({
|
|
190
|
-
timestamp: z.string().datetime({ offset: true }),
|
|
191
|
-
action: z.literal('restore'),
|
|
192
|
-
backup: envMapSchema,
|
|
193
|
-
targetType: z.enum(['settings', 'preset']),
|
|
194
|
-
targetName: z.string(),
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
export const historySchema = z.discriminatedUnion('action', [
|
|
198
|
-
initHistorySchema,
|
|
199
|
-
restoreHistorySchema,
|
|
200
|
-
])
|
|
201
|
-
|
|
202
|
-
export type InitHistoryRecord = z.infer<typeof initHistorySchema>
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
import { join } from 'node:path'
|
|
207
|
-
|
|
208
|
-
export function resolveGlobalRoot(globalRoot?: string): string {
|
|
209
|
-
return globalRoot ?? join(process.env.HOME ?? process.cwd(), '.cc-env')
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function resolveClaudeSettingsPath(homeDir = process.env.HOME ?? process.cwd()): string {
|
|
213
|
-
return join(homeDir, '.claude', 'settings.json')
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function resolveClaudeSettingsLocalPath(homeDir = process.env.HOME ?? process.cwd()): string {
|
|
217
|
-
return join(homeDir, '.claude', 'settings.local.json')
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function resolveShellConfigPaths(homeDir = process.env.HOME ?? process.cwd()) {
|
|
221
|
-
return {
|
|
222
|
-
zsh: join(homeDir, '.zshrc'),
|
|
223
|
-
bash: join(homeDir, '.bashrc'),
|
|
224
|
-
fish: join(homeDir, '.config', 'fish', 'config.fish'),
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
- [ ] **Step 4: Re-run the targeted tests to verify the new primitives pass**
|
|
230
|
-
|
|
231
|
-
Run: `npm test -- tests/core/schema-mask.test.ts tests/core/paths.test.ts tests/services/storage.test.ts`
|
|
232
|
-
Expected: PASS
|
|
233
|
-
|
|
234
|
-
- [ ] **Step 5: Commit the primitives update**
|
|
235
|
-
|
|
236
|
-
```bash
|
|
237
|
-
git add src/core/schema.ts src/core/paths.ts tests/core/schema-mask.test.ts tests/core/paths.test.ts tests/services/storage.test.ts
|
|
238
|
-
git commit -m "feat: add init shell migration history schema"
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
### Task 2: Add Claude home settings and managed shell block services
|
|
244
|
-
|
|
245
|
-
**Files:**
|
|
246
|
-
- Create: `src/services/claude-settings-env-service.ts`
|
|
247
|
-
- Create: `src/services/shell-env-service.ts`
|
|
248
|
-
- Create: `tests/services/claude-shell.test.ts`
|
|
249
|
-
|
|
250
|
-
- [ ] **Step 1: Write the failing service tests**
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
|
|
254
|
-
import { join } from 'node:path'
|
|
255
|
-
import { tmpdir } from 'node:os'
|
|
256
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
257
|
-
|
|
258
|
-
import { createClaudeSettingsEnvService } from '../../src/services/claude-settings-env-service.js'
|
|
259
|
-
import { createShellEnvService } from '../../src/services/shell-env-service.js'
|
|
260
|
-
|
|
261
|
-
const roots: string[] = []
|
|
262
|
-
|
|
263
|
-
afterEach(async () => {
|
|
264
|
-
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
describe('Claude settings env service', () => {
|
|
268
|
-
it('reads both settings files and keeps them separate', async () => {
|
|
269
|
-
const homeDir = await mkdtemp(join(tmpdir(), 'cc-env-home-'))
|
|
270
|
-
roots.push(homeDir)
|
|
271
|
-
|
|
272
|
-
await writeFile(
|
|
273
|
-
join(homeDir, '.claude', 'settings.json'),
|
|
274
|
-
'{"env":{"ANTHROPIC_BASE_URL":"https://settings.example.com"}}\n',
|
|
275
|
-
'utf8',
|
|
276
|
-
)
|
|
277
|
-
await writeFile(
|
|
278
|
-
join(homeDir, '.claude', 'settings.local.json'),
|
|
279
|
-
'{"env":{"ANTHROPIC_AUTH_TOKEN":"local-token"}}\n',
|
|
280
|
-
'utf8',
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
const service = createClaudeSettingsEnvService({ homeDir })
|
|
284
|
-
|
|
285
|
-
await expect(service.read()).resolves.toMatchObject({
|
|
286
|
-
settings: {
|
|
287
|
-
exists: true,
|
|
288
|
-
env: {
|
|
289
|
-
ANTHROPIC_BASE_URL: 'https://settings.example.com',
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
settingsLocal: {
|
|
293
|
-
exists: true,
|
|
294
|
-
env: {
|
|
295
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
})
|
|
299
|
-
})
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
describe('shell env service', () => {
|
|
303
|
-
it('writes and updates only the managed block in all shell files', async () => {
|
|
304
|
-
const homeDir = await mkdtemp(join(tmpdir(), 'cc-env-shell-'))
|
|
305
|
-
roots.push(homeDir)
|
|
306
|
-
|
|
307
|
-
await writeFile(join(homeDir, '.zshrc'), 'export PATH="/bin"\n', 'utf8')
|
|
308
|
-
|
|
309
|
-
const service = createShellEnvService({ homeDir })
|
|
310
|
-
|
|
311
|
-
await service.write({
|
|
312
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
await expect(readFile(join(homeDir, '.zshrc'), 'utf8')).resolves.toContain(
|
|
316
|
-
'# >>> cc-env >>>',
|
|
317
|
-
)
|
|
318
|
-
await expect(readFile(join(homeDir, '.zshrc'), 'utf8')).resolves.toContain(
|
|
319
|
-
'export ANTHROPIC_AUTH_TOKEN="local-token"',
|
|
320
|
-
)
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
it('removes only the requested keys from a managed block and leaves user content intact', async () => {
|
|
324
|
-
const homeDir = await mkdtemp(join(tmpdir(), 'cc-env-shell-'))
|
|
325
|
-
roots.push(homeDir)
|
|
326
|
-
|
|
327
|
-
const service = createShellEnvService({ homeDir })
|
|
328
|
-
const shellWrites = await service.write({
|
|
329
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
330
|
-
ANTHROPIC_BASE_URL: 'https://local.example.com',
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
await service.removeKeys(shellWrites, ['ANTHROPIC_AUTH_TOKEN'])
|
|
334
|
-
|
|
335
|
-
await expect(readFile(join(homeDir, '.bashrc'), 'utf8')).resolves.not.toContain(
|
|
336
|
-
'ANTHROPIC_AUTH_TOKEN',
|
|
337
|
-
)
|
|
338
|
-
await expect(readFile(join(homeDir, '.bashrc'), 'utf8')).resolves.toContain(
|
|
339
|
-
'ANTHROPIC_BASE_URL',
|
|
340
|
-
)
|
|
341
|
-
})
|
|
342
|
-
})
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
- [ ] **Step 2: Run the service tests to verify they fail**
|
|
346
|
-
|
|
347
|
-
Run: `npm test -- tests/services/claude-shell.test.ts`
|
|
348
|
-
Expected: FAIL with missing module errors for `claude-settings-env-service.ts` and `shell-env-service.ts`.
|
|
349
|
-
|
|
350
|
-
- [ ] **Step 3: Implement the two services with the narrowest useful API**
|
|
351
|
-
|
|
352
|
-
```ts
|
|
353
|
-
import { readFile } from 'node:fs/promises'
|
|
354
|
-
|
|
355
|
-
import { atomicWriteFile } from '../core/fs.js'
|
|
356
|
-
import { envMapSchema, type EnvMap } from '../core/schema.js'
|
|
357
|
-
import {
|
|
358
|
-
resolveClaudeSettingsLocalPath,
|
|
359
|
-
resolveClaudeSettingsPath,
|
|
360
|
-
} from '../core/paths.js'
|
|
361
|
-
|
|
362
|
-
type ClaudeSettingsSource = {
|
|
363
|
-
path: string
|
|
364
|
-
exists: boolean
|
|
365
|
-
env: EnvMap
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export function createClaudeSettingsEnvService({ homeDir }: { homeDir?: string } = {}) {
|
|
369
|
-
const settingsPath = resolveClaudeSettingsPath(homeDir)
|
|
370
|
-
const settingsLocalPath = resolveClaudeSettingsLocalPath(homeDir)
|
|
371
|
-
|
|
372
|
-
async function readOne(path: string): Promise<ClaudeSettingsSource> {
|
|
373
|
-
try {
|
|
374
|
-
const content = await readFile(path, 'utf8')
|
|
375
|
-
const json = JSON.parse(content) as { env?: unknown }
|
|
376
|
-
return {
|
|
377
|
-
path,
|
|
378
|
-
exists: true,
|
|
379
|
-
env: envMapSchema.parse(json.env ?? {}),
|
|
380
|
-
}
|
|
381
|
-
} catch (error) {
|
|
382
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
383
|
-
return {
|
|
384
|
-
path,
|
|
385
|
-
exists: false,
|
|
386
|
-
env: envMapSchema.parse({}),
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
throw error
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return {
|
|
395
|
-
read: async () => ({
|
|
396
|
-
settings: await readOne(settingsPath),
|
|
397
|
-
settingsLocal: await readOne(settingsLocalPath),
|
|
398
|
-
}),
|
|
399
|
-
write: async ({
|
|
400
|
-
settingsEnv,
|
|
401
|
-
settingsLocalEnv,
|
|
402
|
-
}: {
|
|
403
|
-
settingsEnv: EnvMap
|
|
404
|
-
settingsLocalEnv: EnvMap
|
|
405
|
-
}) => {
|
|
406
|
-
await atomicWriteFile(
|
|
407
|
-
settingsPath,
|
|
408
|
-
`${JSON.stringify({ env: envMapSchema.parse(settingsEnv) }, null, 2)}\n`,
|
|
409
|
-
)
|
|
410
|
-
await atomicWriteFile(
|
|
411
|
-
settingsLocalPath,
|
|
412
|
-
`${JSON.stringify({ env: envMapSchema.parse(settingsLocalEnv) }, null, 2)}\n`,
|
|
413
|
-
)
|
|
414
|
-
},
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
```ts
|
|
420
|
-
import { readFile } from 'node:fs/promises'
|
|
421
|
-
|
|
422
|
-
import { atomicWriteFile } from '../core/fs.js'
|
|
423
|
-
import { envMapSchema, type EnvMap } from '../core/schema.js'
|
|
424
|
-
import { resolveShellConfigPaths } from '../core/paths.js'
|
|
425
|
-
|
|
426
|
-
const startMarker = '# >>> cc-env >>>'
|
|
427
|
-
const endMarker = '# <<< cc-env <<<'
|
|
428
|
-
|
|
429
|
-
type ShellName = 'zsh' | 'bash' | 'fish'
|
|
430
|
-
|
|
431
|
-
type ShellWriteRecord = {
|
|
432
|
-
shell: ShellName
|
|
433
|
-
filePath: string
|
|
434
|
-
env: EnvMap
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function parseManagedEnv(content: string): EnvMap {
|
|
438
|
-
const match = content.match(/# >>> cc-env >>>[\s\S]*?# <<< cc-env <<</)
|
|
439
|
-
if (!match) {
|
|
440
|
-
return envMapSchema.parse({})
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const lines = match[0]
|
|
444
|
-
.split('\n')
|
|
445
|
-
.slice(1, -1)
|
|
446
|
-
.filter(Boolean)
|
|
447
|
-
|
|
448
|
-
return envMapSchema.parse(
|
|
449
|
-
Object.fromEntries(
|
|
450
|
-
lines.map((line) => {
|
|
451
|
-
if (line.startsWith('set -gx ')) {
|
|
452
|
-
const [, key, value] = line.match(/^set -gx ([A-Z0-9_]+) "(.*)"$/) ?? []
|
|
453
|
-
return [key, value]
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const [, key, value] = line.match(/^export ([A-Z0-9_]+)="(.*)"$/) ?? []
|
|
457
|
-
return [key, value]
|
|
458
|
-
}),
|
|
459
|
-
),
|
|
460
|
-
)
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function renderBlock(shell: ShellName, env: EnvMap): string {
|
|
464
|
-
const lines = Object.entries(env)
|
|
465
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
466
|
-
.map(([key, value]) =>
|
|
467
|
-
shell === 'fish' ? `set -gx ${key} "${value}"` : `export ${key}="${value}"`,
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
return [startMarker, ...lines, endMarker, ''].join('\n')
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function replaceManagedBlock(content: string, block: string): string {
|
|
474
|
-
const pattern = /# >>> cc-env >>>[\s\S]*?# <<< cc-env <<<\n?/
|
|
475
|
-
if (pattern.test(content)) {
|
|
476
|
-
return content.replace(pattern, block)
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return content.length === 0 ? block : `${content.replace(/\n?$/, '\n')}\n${block}`
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export function createShellEnvService({ homeDir }: { homeDir?: string } = {}) {
|
|
483
|
-
const paths = resolveShellConfigPaths(homeDir)
|
|
484
|
-
|
|
485
|
-
async function readContent(path: string): Promise<string> {
|
|
486
|
-
try {
|
|
487
|
-
return await readFile(path, 'utf8')
|
|
488
|
-
} catch (error) {
|
|
489
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
490
|
-
return ''
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
throw error
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return {
|
|
498
|
-
async write(env: EnvMap): Promise<ShellWriteRecord[]> {
|
|
499
|
-
return Promise.all(
|
|
500
|
-
(Object.entries(paths) as Array<[ShellName, string]>).map(async ([shell, filePath]) => {
|
|
501
|
-
const content = await readContent(filePath)
|
|
502
|
-
const mergedEnv = envMapSchema.parse({
|
|
503
|
-
...parseManagedEnv(content),
|
|
504
|
-
...env,
|
|
505
|
-
})
|
|
506
|
-
await atomicWriteFile(filePath, replaceManagedBlock(content, renderBlock(shell, mergedEnv)))
|
|
507
|
-
return { shell, filePath, env: mergedEnv }
|
|
508
|
-
}),
|
|
509
|
-
)
|
|
510
|
-
},
|
|
511
|
-
async removeKeys(shellWrites: ShellWriteRecord[], keys: string[]): Promise<void> {
|
|
512
|
-
await Promise.all(
|
|
513
|
-
shellWrites.map(async ({ shell, filePath }) => {
|
|
514
|
-
const content = await readContent(filePath)
|
|
515
|
-
const current = parseManagedEnv(content)
|
|
516
|
-
const next = envMapSchema.parse(
|
|
517
|
-
Object.fromEntries(
|
|
518
|
-
Object.entries(current).filter(([key]) => !keys.includes(key)),
|
|
519
|
-
),
|
|
520
|
-
)
|
|
521
|
-
const block = Object.keys(next).length === 0 ? '' : renderBlock(shell, next)
|
|
522
|
-
await atomicWriteFile(filePath, replaceManagedBlock(content, block))
|
|
523
|
-
}),
|
|
524
|
-
)
|
|
525
|
-
},
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
- [ ] **Step 4: Re-run the service tests to verify they pass**
|
|
531
|
-
|
|
532
|
-
Run: `npm test -- tests/services/claude-shell.test.ts`
|
|
533
|
-
Expected: PASS
|
|
534
|
-
|
|
535
|
-
- [ ] **Step 5: Commit the new services**
|
|
536
|
-
|
|
537
|
-
```bash
|
|
538
|
-
git add src/services/claude-settings-env-service.ts src/services/shell-env-service.ts tests/services/claude-shell.test.ts
|
|
539
|
-
git commit -m "feat: add Claude home and shell env services"
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
---
|
|
543
|
-
|
|
544
|
-
### Task 3: Rewrite init flow and command around required keys and shell migration
|
|
545
|
-
|
|
546
|
-
**Files:**
|
|
547
|
-
- Modify: `src/flows/init-flow.ts`
|
|
548
|
-
- Modify: `src/ink/init-app.tsx`
|
|
549
|
-
- Modify: `src/commands/init.ts`
|
|
550
|
-
- Modify: `src/cli.ts`
|
|
551
|
-
- Modify: `tests/flows/init-flow.test.ts`
|
|
552
|
-
- Modify: `tests/integration/init-restore.test.ts`
|
|
553
|
-
|
|
554
|
-
- [ ] **Step 1: Write the failing flow and command tests first**
|
|
555
|
-
|
|
556
|
-
```ts
|
|
557
|
-
import { describe, expect, it } from 'vitest'
|
|
558
|
-
|
|
559
|
-
import {
|
|
560
|
-
advanceInitFlow,
|
|
561
|
-
createInitFlowState,
|
|
562
|
-
} from '../../src/flows/init-flow.js'
|
|
563
|
-
|
|
564
|
-
describe('init flow', () => {
|
|
565
|
-
it('preselects required keys and does not let them be toggled off', () => {
|
|
566
|
-
const state = createInitFlowState(
|
|
567
|
-
['ANTHROPIC_AUTH_TOKEN', 'EXTRA_KEY'],
|
|
568
|
-
['ANTHROPIC_AUTH_TOKEN'],
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
expect(state.selectedKeys).toEqual(['ANTHROPIC_AUTH_TOKEN'])
|
|
572
|
-
|
|
573
|
-
expect(
|
|
574
|
-
advanceInitFlow(state, {
|
|
575
|
-
type: 'toggle-key',
|
|
576
|
-
key: 'ANTHROPIC_AUTH_TOKEN',
|
|
577
|
-
}).selectedKeys,
|
|
578
|
-
).toEqual(['ANTHROPIC_AUTH_TOKEN'])
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
it('moves directly from key selection to confirm', () => {
|
|
582
|
-
const state = createInitFlowState(['ANTHROPIC_AUTH_TOKEN'], ['ANTHROPIC_AUTH_TOKEN'])
|
|
583
|
-
|
|
584
|
-
expect(advanceInitFlow(state, { type: 'continue' }).step).toBe('confirm')
|
|
585
|
-
})
|
|
586
|
-
})
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
```ts
|
|
590
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
591
|
-
|
|
592
|
-
import { CliError } from '../../src/core/errors.js'
|
|
593
|
-
import { createInitCommand } from '../../src/commands/init.js'
|
|
594
|
-
|
|
595
|
-
describe('createInitCommand', () => {
|
|
596
|
-
it('migrates effective env from Claude settings into shell blocks and records per-file backups', async () => {
|
|
597
|
-
const claudeSettingsEnvService = {
|
|
598
|
-
read: vi.fn().mockResolvedValue({
|
|
599
|
-
settings: {
|
|
600
|
-
exists: true,
|
|
601
|
-
env: {
|
|
602
|
-
ANTHROPIC_BASE_URL: 'https://settings.example.com',
|
|
603
|
-
},
|
|
604
|
-
},
|
|
605
|
-
settingsLocal: {
|
|
606
|
-
exists: true,
|
|
607
|
-
env: {
|
|
608
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
609
|
-
ANTHROPIC_BASE_URL: 'https://local.example.com',
|
|
610
|
-
},
|
|
611
|
-
},
|
|
612
|
-
}),
|
|
613
|
-
write: vi.fn().mockResolvedValue(undefined),
|
|
614
|
-
}
|
|
615
|
-
const shellEnvService = {
|
|
616
|
-
write: vi.fn().mockResolvedValue([
|
|
617
|
-
{
|
|
618
|
-
shell: 'zsh',
|
|
619
|
-
filePath: '/Users/test/.zshrc',
|
|
620
|
-
env: {
|
|
621
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
622
|
-
ANTHROPIC_BASE_URL: 'https://local.example.com',
|
|
623
|
-
},
|
|
624
|
-
},
|
|
625
|
-
]),
|
|
626
|
-
}
|
|
627
|
-
const historyService = {
|
|
628
|
-
write: vi.fn().mockResolvedValue(undefined),
|
|
629
|
-
}
|
|
630
|
-
const renderFlow = vi.fn().mockResolvedValue({
|
|
631
|
-
confirmed: true,
|
|
632
|
-
selectedKeys: ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL'],
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
const init = createInitCommand({
|
|
636
|
-
claudeSettingsEnvService,
|
|
637
|
-
shellEnvService,
|
|
638
|
-
historyService,
|
|
639
|
-
renderFlow,
|
|
640
|
-
})
|
|
641
|
-
|
|
642
|
-
await expect(init({ yes: false })).resolves.toBeUndefined()
|
|
643
|
-
|
|
644
|
-
expect(shellEnvService.write).toHaveBeenCalledWith({
|
|
645
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
646
|
-
ANTHROPIC_BASE_URL: 'https://local.example.com',
|
|
647
|
-
})
|
|
648
|
-
expect(historyService.write).toHaveBeenCalledWith({
|
|
649
|
-
timestamp: expect.any(String),
|
|
650
|
-
action: 'init',
|
|
651
|
-
migratedKeys: ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL'],
|
|
652
|
-
settingsBackup: {
|
|
653
|
-
ANTHROPIC_BASE_URL: 'https://settings.example.com',
|
|
654
|
-
},
|
|
655
|
-
settingsLocalBackup: {
|
|
656
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
657
|
-
ANTHROPIC_BASE_URL: 'https://local.example.com',
|
|
658
|
-
},
|
|
659
|
-
shellWrites: [
|
|
660
|
-
{
|
|
661
|
-
shell: 'zsh',
|
|
662
|
-
filePath: '/Users/test/.zshrc',
|
|
663
|
-
env: {
|
|
664
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
665
|
-
ANTHROPIC_BASE_URL: 'https://local.example.com',
|
|
666
|
-
},
|
|
667
|
-
},
|
|
668
|
-
],
|
|
669
|
-
})
|
|
670
|
-
expect(claudeSettingsEnvService.write).toHaveBeenCalledWith({
|
|
671
|
-
settingsEnv: {},
|
|
672
|
-
settingsLocalEnv: {},
|
|
673
|
-
})
|
|
674
|
-
})
|
|
675
|
-
|
|
676
|
-
it('fails when both Claude settings files are missing', async () => {
|
|
677
|
-
const init = createInitCommand({
|
|
678
|
-
claudeSettingsEnvService: {
|
|
679
|
-
read: vi.fn().mockResolvedValue({
|
|
680
|
-
settings: { exists: false, env: {} },
|
|
681
|
-
settingsLocal: { exists: false, env: {} },
|
|
682
|
-
}),
|
|
683
|
-
},
|
|
684
|
-
shellEnvService: { write: vi.fn() },
|
|
685
|
-
historyService: { write: vi.fn() },
|
|
686
|
-
renderFlow: vi.fn(),
|
|
687
|
-
})
|
|
688
|
-
|
|
689
|
-
await expect(init({ yes: false })).rejects.toEqual(
|
|
690
|
-
new CliError('Claude settings.json and settings.local.json were not found'),
|
|
691
|
-
)
|
|
692
|
-
})
|
|
693
|
-
})
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
- [ ] **Step 2: Run the flow and command tests to verify they fail**
|
|
697
|
-
|
|
698
|
-
Run: `npm test -- tests/flows/init-flow.test.ts tests/integration/init-restore.test.ts`
|
|
699
|
-
Expected: FAIL because `init-flow` still has a preset target step and `createInitCommand` still depends on `presetService` + single-file settings.
|
|
700
|
-
|
|
701
|
-
- [ ] **Step 3: Implement the minimal flow and command rewrite**
|
|
702
|
-
|
|
703
|
-
```ts
|
|
704
|
-
export type InitFlowState = {
|
|
705
|
-
step: 'keys' | 'confirm' | 'done'
|
|
706
|
-
availableKeys: string[]
|
|
707
|
-
requiredKeys: string[]
|
|
708
|
-
selectedKeys: string[]
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
export type InitFlowAction =
|
|
712
|
-
| { type: 'toggle-key'; key: string }
|
|
713
|
-
| { type: 'continue' }
|
|
714
|
-
| { type: 'confirm' }
|
|
715
|
-
|
|
716
|
-
export function createInitFlowState(
|
|
717
|
-
availableKeys: string[],
|
|
718
|
-
requiredKeys: string[],
|
|
719
|
-
): InitFlowState {
|
|
720
|
-
return {
|
|
721
|
-
step: 'keys',
|
|
722
|
-
availableKeys,
|
|
723
|
-
requiredKeys,
|
|
724
|
-
selectedKeys: requiredKeys,
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
export function advanceInitFlow(state: InitFlowState, action: InitFlowAction): InitFlowState {
|
|
729
|
-
if (state.step === 'keys' && action.type === 'toggle-key') {
|
|
730
|
-
if (state.requiredKeys.includes(action.key)) {
|
|
731
|
-
return state
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const selectedKeys = state.selectedKeys.includes(action.key)
|
|
735
|
-
? state.selectedKeys.filter((key) => key !== action.key)
|
|
736
|
-
: [...state.selectedKeys, action.key]
|
|
737
|
-
|
|
738
|
-
return {
|
|
739
|
-
...state,
|
|
740
|
-
selectedKeys,
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
if (state.step === 'keys' && action.type === 'continue') {
|
|
745
|
-
return {
|
|
746
|
-
...state,
|
|
747
|
-
step: 'confirm',
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (state.step === 'confirm' && action.type === 'confirm') {
|
|
752
|
-
return {
|
|
753
|
-
...state,
|
|
754
|
-
step: 'done',
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return state
|
|
759
|
-
}
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
```ts
|
|
763
|
-
import { CliError } from '../core/errors.js'
|
|
764
|
-
import { envMapSchema, type EnvMap } from '../core/schema.js'
|
|
765
|
-
|
|
766
|
-
const requiredInitKeys = [
|
|
767
|
-
'ANTHROPIC_AUTH_TOKEN',
|
|
768
|
-
'ANTHROPIC_BASE_URL',
|
|
769
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
770
|
-
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
771
|
-
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
772
|
-
'ANTHROPIC_REASONING_MODEL',
|
|
773
|
-
] as const
|
|
774
|
-
|
|
775
|
-
function omitKeys(env: EnvMap, keys: string[]): EnvMap {
|
|
776
|
-
return envMapSchema.parse(
|
|
777
|
-
Object.fromEntries(Object.entries(env).filter(([key]) => !keys.includes(key))),
|
|
778
|
-
)
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
export function createInitCommand({
|
|
782
|
-
claudeSettingsEnvService,
|
|
783
|
-
shellEnvService,
|
|
784
|
-
historyService,
|
|
785
|
-
renderFlow,
|
|
786
|
-
}: {
|
|
787
|
-
claudeSettingsEnvService: {
|
|
788
|
-
read: () => Promise<{
|
|
789
|
-
settings: { exists: boolean; env: EnvMap }
|
|
790
|
-
settingsLocal: { exists: boolean; env: EnvMap }
|
|
791
|
-
}>
|
|
792
|
-
write: (input: { settingsEnv: EnvMap; settingsLocalEnv: EnvMap }) => Promise<void>
|
|
793
|
-
}
|
|
794
|
-
shellEnvService: {
|
|
795
|
-
write: (env: EnvMap) => Promise<unknown>
|
|
796
|
-
}
|
|
797
|
-
historyService: {
|
|
798
|
-
write: (record: unknown) => Promise<unknown>
|
|
799
|
-
}
|
|
800
|
-
renderFlow: (context: {
|
|
801
|
-
keys: string[]
|
|
802
|
-
requiredKeys: string[]
|
|
803
|
-
yes: boolean
|
|
804
|
-
}) => Promise<{ confirmed?: boolean; selectedKeys: string[] } | void>
|
|
805
|
-
}) {
|
|
806
|
-
return async function init({ yes = false }: { yes?: boolean } = {}): Promise<void> {
|
|
807
|
-
const sources = await claudeSettingsEnvService.read()
|
|
808
|
-
|
|
809
|
-
if (!sources.settings.exists && !sources.settingsLocal.exists) {
|
|
810
|
-
throw new CliError('Claude settings.json and settings.local.json were not found')
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const effectiveEnv = envMapSchema.parse({
|
|
814
|
-
...sources.settings.env,
|
|
815
|
-
...sources.settingsLocal.env,
|
|
816
|
-
})
|
|
817
|
-
const keys = Object.keys(effectiveEnv).sort()
|
|
818
|
-
const requiredKeys = requiredInitKeys.filter((key) => key in effectiveEnv)
|
|
819
|
-
const result = await renderFlow({ keys, requiredKeys, yes })
|
|
820
|
-
|
|
821
|
-
if (!result?.confirmed) {
|
|
822
|
-
return
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const migratedEnv = envMapSchema.parse(
|
|
826
|
-
Object.fromEntries(
|
|
827
|
-
result.selectedKeys
|
|
828
|
-
.filter((key) => key in effectiveEnv)
|
|
829
|
-
.map((key) => [key, effectiveEnv[key]]),
|
|
830
|
-
),
|
|
831
|
-
)
|
|
832
|
-
|
|
833
|
-
if (Object.keys(migratedEnv).length === 0) {
|
|
834
|
-
throw new CliError('No selected env values found to migrate')
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
const settingsBackup = envMapSchema.parse(
|
|
838
|
-
Object.fromEntries(
|
|
839
|
-
result.selectedKeys
|
|
840
|
-
.filter((key) => key in sources.settings.env)
|
|
841
|
-
.map((key) => [key, sources.settings.env[key]]),
|
|
842
|
-
),
|
|
843
|
-
)
|
|
844
|
-
const settingsLocalBackup = envMapSchema.parse(
|
|
845
|
-
Object.fromEntries(
|
|
846
|
-
result.selectedKeys
|
|
847
|
-
.filter((key) => key in sources.settingsLocal.env)
|
|
848
|
-
.map((key) => [key, sources.settingsLocal.env[key]]),
|
|
849
|
-
),
|
|
850
|
-
)
|
|
851
|
-
|
|
852
|
-
const timestamp = new Date().toISOString()
|
|
853
|
-
const shellWrites = await shellEnvService.write(migratedEnv)
|
|
854
|
-
|
|
855
|
-
await historyService.write({
|
|
856
|
-
timestamp,
|
|
857
|
-
action: 'init',
|
|
858
|
-
migratedKeys: result.selectedKeys,
|
|
859
|
-
settingsBackup,
|
|
860
|
-
settingsLocalBackup,
|
|
861
|
-
shellWrites,
|
|
862
|
-
})
|
|
863
|
-
|
|
864
|
-
await claudeSettingsEnvService.write({
|
|
865
|
-
settingsEnv: omitKeys(sources.settings.env, result.selectedKeys),
|
|
866
|
-
settingsLocalEnv: omitKeys(sources.settingsLocal.env, result.selectedKeys),
|
|
867
|
-
})
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
```
|
|
871
|
-
|
|
872
|
-
- [ ] **Step 4: Re-run the init flow and command tests to verify they pass**
|
|
873
|
-
|
|
874
|
-
Run: `npm test -- tests/flows/init-flow.test.ts tests/integration/init-restore.test.ts`
|
|
875
|
-
Expected: PASS
|
|
876
|
-
|
|
877
|
-
- [ ] **Step 5: Commit the init rewrite**
|
|
878
|
-
|
|
879
|
-
```bash
|
|
880
|
-
git add src/flows/init-flow.ts src/ink/init-app.tsx src/commands/init.ts src/cli.ts tests/flows/init-flow.test.ts tests/integration/init-restore.test.ts
|
|
881
|
-
git commit -m "feat: migrate init to Claude shell env flow"
|
|
882
|
-
```
|
|
883
|
-
|
|
884
|
-
---
|
|
885
|
-
|
|
886
|
-
### Task 4: Redesign restore flow and command for init records
|
|
887
|
-
|
|
888
|
-
**Files:**
|
|
889
|
-
- Modify: `src/flows/restore-flow.ts`
|
|
890
|
-
- Modify: `src/ink/restore-app.tsx`
|
|
891
|
-
- Modify: `src/commands/restore.ts`
|
|
892
|
-
- Modify: `src/cli.ts`
|
|
893
|
-
- Modify: `tests/flows/restore-flow.test.ts`
|
|
894
|
-
- Modify: `tests/integration/init-restore.test.ts`
|
|
895
|
-
|
|
896
|
-
- [ ] **Step 1: Write the failing restore tests first**
|
|
897
|
-
|
|
898
|
-
```ts
|
|
899
|
-
import { describe, expect, it } from 'vitest'
|
|
900
|
-
|
|
901
|
-
import {
|
|
902
|
-
advanceRestoreFlow,
|
|
903
|
-
createRestoreFlowState,
|
|
904
|
-
} from '../../src/flows/restore-flow.js'
|
|
905
|
-
|
|
906
|
-
describe('restore flow', () => {
|
|
907
|
-
it('skips target selection for init history entries', () => {
|
|
908
|
-
const state = createRestoreFlowState([
|
|
909
|
-
{
|
|
910
|
-
timestamp: '2026-04-24T00:00:00.000Z',
|
|
911
|
-
action: 'init',
|
|
912
|
-
},
|
|
913
|
-
] as any)
|
|
914
|
-
|
|
915
|
-
expect(
|
|
916
|
-
advanceRestoreFlow(state, {
|
|
917
|
-
type: 'select-record',
|
|
918
|
-
timestamp: '2026-04-24T00:00:00.000Z',
|
|
919
|
-
}).step,
|
|
920
|
-
).toBe('confirm')
|
|
921
|
-
})
|
|
922
|
-
})
|
|
923
|
-
```
|
|
924
|
-
|
|
925
|
-
```ts
|
|
926
|
-
it('restores an init record by removing shell keys and restoring both Claude settings files', async () => {
|
|
927
|
-
const historyService = {
|
|
928
|
-
list: vi.fn().mockResolvedValue([
|
|
929
|
-
{
|
|
930
|
-
timestamp: '2026-04-24T00:00:00.000Z',
|
|
931
|
-
action: 'init',
|
|
932
|
-
migratedKeys: ['ANTHROPIC_AUTH_TOKEN'],
|
|
933
|
-
settingsBackup: {},
|
|
934
|
-
settingsLocalBackup: {
|
|
935
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
936
|
-
},
|
|
937
|
-
shellWrites: [
|
|
938
|
-
{
|
|
939
|
-
shell: 'zsh',
|
|
940
|
-
filePath: '/Users/test/.zshrc',
|
|
941
|
-
env: {
|
|
942
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
943
|
-
},
|
|
944
|
-
},
|
|
945
|
-
],
|
|
946
|
-
},
|
|
947
|
-
]),
|
|
948
|
-
}
|
|
949
|
-
const claudeSettingsEnvService = {
|
|
950
|
-
read: vi.fn().mockResolvedValue({
|
|
951
|
-
settings: { exists: true, env: {} },
|
|
952
|
-
settingsLocal: { exists: true, env: {} },
|
|
953
|
-
}),
|
|
954
|
-
write: vi.fn().mockResolvedValue(undefined),
|
|
955
|
-
}
|
|
956
|
-
const shellEnvService = {
|
|
957
|
-
removeKeys: vi.fn().mockResolvedValue(undefined),
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
const restore = createRestoreCommand({
|
|
961
|
-
historyService,
|
|
962
|
-
claudeSettingsEnvService,
|
|
963
|
-
shellEnvService,
|
|
964
|
-
presetService: {
|
|
965
|
-
read: vi.fn(),
|
|
966
|
-
write: vi.fn(),
|
|
967
|
-
},
|
|
968
|
-
renderFlow: vi.fn().mockResolvedValue({
|
|
969
|
-
confirmed: true,
|
|
970
|
-
timestamp: '2026-04-24T00:00:00.000Z',
|
|
971
|
-
}),
|
|
972
|
-
})
|
|
973
|
-
|
|
974
|
-
await expect(restore({ yes: false })).resolves.toBeUndefined()
|
|
975
|
-
|
|
976
|
-
expect(shellEnvService.removeKeys).toHaveBeenCalledWith(
|
|
977
|
-
[
|
|
978
|
-
{
|
|
979
|
-
shell: 'zsh',
|
|
980
|
-
filePath: '/Users/test/.zshrc',
|
|
981
|
-
env: {
|
|
982
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
983
|
-
},
|
|
984
|
-
},
|
|
985
|
-
],
|
|
986
|
-
['ANTHROPIC_AUTH_TOKEN'],
|
|
987
|
-
)
|
|
988
|
-
expect(claudeSettingsEnvService.write).toHaveBeenCalledWith({
|
|
989
|
-
settingsEnv: {},
|
|
990
|
-
settingsLocalEnv: {
|
|
991
|
-
ANTHROPIC_AUTH_TOKEN: 'local-token',
|
|
992
|
-
},
|
|
993
|
-
})
|
|
994
|
-
})
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
- [ ] **Step 2: Run the restore tests to verify they fail**
|
|
998
|
-
|
|
999
|
-
Run: `npm test -- tests/flows/restore-flow.test.ts tests/integration/init-restore.test.ts`
|
|
1000
|
-
Expected: FAIL because `restore-flow` still assumes every record needs a target step and `createRestoreCommand` still restores init entries into settings or presets.
|
|
1001
|
-
|
|
1002
|
-
- [ ] **Step 3: Implement the restore flow branching and init restore logic**
|
|
1003
|
-
|
|
1004
|
-
```ts
|
|
1005
|
-
import type { HistoryRecord } from '../core/schema.js'
|
|
1006
|
-
|
|
1007
|
-
export type RestoreFlowState = {
|
|
1008
|
-
step: 'record' | 'target' | 'confirm' | 'done'
|
|
1009
|
-
records: HistoryRecord[]
|
|
1010
|
-
selectedTimestamp?: string
|
|
1011
|
-
targetType?: 'settings' | 'preset'
|
|
1012
|
-
targetName?: string
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
export function createRestoreFlowState(records: HistoryRecord[]): RestoreFlowState {
|
|
1016
|
-
return {
|
|
1017
|
-
step: 'record',
|
|
1018
|
-
records,
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
export function advanceRestoreFlow(
|
|
1023
|
-
state: RestoreFlowState,
|
|
1024
|
-
action:
|
|
1025
|
-
| { type: 'select-record'; timestamp: string }
|
|
1026
|
-
| { type: 'select-target'; targetType: 'settings' | 'preset'; targetName?: string }
|
|
1027
|
-
| { type: 'confirm' },
|
|
1028
|
-
): RestoreFlowState {
|
|
1029
|
-
if (state.step === 'record' && action.type === 'select-record') {
|
|
1030
|
-
const selectedRecord = state.records.find((record) => record.timestamp === action.timestamp)
|
|
1031
|
-
if (!selectedRecord) {
|
|
1032
|
-
return state
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (selectedRecord.action === 'init') {
|
|
1036
|
-
return {
|
|
1037
|
-
...state,
|
|
1038
|
-
selectedTimestamp: action.timestamp,
|
|
1039
|
-
step: 'confirm',
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
return {
|
|
1044
|
-
...state,
|
|
1045
|
-
selectedTimestamp: action.timestamp,
|
|
1046
|
-
step: 'target',
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
if (state.step === 'target' && action.type === 'select-target') {
|
|
1051
|
-
return {
|
|
1052
|
-
...state,
|
|
1053
|
-
step: 'confirm',
|
|
1054
|
-
targetType: action.targetType,
|
|
1055
|
-
targetName: action.targetName,
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
if (state.step === 'confirm' && action.type === 'confirm') {
|
|
1060
|
-
return {
|
|
1061
|
-
...state,
|
|
1062
|
-
step: 'done',
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
return state
|
|
1067
|
-
}
|
|
1068
|
-
```
|
|
1069
|
-
|
|
1070
|
-
```ts
|
|
1071
|
-
import { CliError } from '../core/errors.js'
|
|
1072
|
-
import type { EnvMap, HistoryRecord, Preset } from '../core/schema.js'
|
|
1073
|
-
|
|
1074
|
-
export function createRestoreCommand({
|
|
1075
|
-
historyService,
|
|
1076
|
-
claudeSettingsEnvService,
|
|
1077
|
-
shellEnvService,
|
|
1078
|
-
settingsEnvService,
|
|
1079
|
-
presetService,
|
|
1080
|
-
renderFlow,
|
|
1081
|
-
}: {
|
|
1082
|
-
historyService: { list: () => Promise<HistoryRecord[]> }
|
|
1083
|
-
claudeSettingsEnvService: {
|
|
1084
|
-
read: () => Promise<{
|
|
1085
|
-
settings: { env: EnvMap }
|
|
1086
|
-
settingsLocal: { env: EnvMap }
|
|
1087
|
-
}>
|
|
1088
|
-
write: (input: { settingsEnv: EnvMap; settingsLocalEnv: EnvMap }) => Promise<void>
|
|
1089
|
-
}
|
|
1090
|
-
shellEnvService: {
|
|
1091
|
-
removeKeys: (shellWrites: any[], keys: string[]) => Promise<void>
|
|
1092
|
-
}
|
|
1093
|
-
settingsEnvService: {
|
|
1094
|
-
read: () => Promise<EnvMap>
|
|
1095
|
-
write: (env: EnvMap) => Promise<unknown>
|
|
1096
|
-
}
|
|
1097
|
-
presetService: {
|
|
1098
|
-
read: (name: string) => Promise<Preset>
|
|
1099
|
-
write: (preset: Preset) => Promise<unknown>
|
|
1100
|
-
}
|
|
1101
|
-
renderFlow: (context: { records: HistoryRecord[]; yes: boolean }) => Promise<any>
|
|
1102
|
-
}) {
|
|
1103
|
-
return async function restore({ yes = false }: { yes?: boolean } = {}): Promise<void> {
|
|
1104
|
-
const records = await historyService.list()
|
|
1105
|
-
const result = await renderFlow({ records, yes })
|
|
1106
|
-
|
|
1107
|
-
if (!result?.confirmed) {
|
|
1108
|
-
return
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
const record = records.find((entry) => entry.timestamp === result.timestamp)
|
|
1112
|
-
|
|
1113
|
-
if (!record) {
|
|
1114
|
-
throw new CliError('Restore record not found')
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
if (record.action === 'init') {
|
|
1118
|
-
const current = await claudeSettingsEnvService.read()
|
|
1119
|
-
await shellEnvService.removeKeys(record.shellWrites, record.migratedKeys)
|
|
1120
|
-
await claudeSettingsEnvService.write({
|
|
1121
|
-
settingsEnv: {
|
|
1122
|
-
...current.settings.env,
|
|
1123
|
-
...record.settingsBackup,
|
|
1124
|
-
},
|
|
1125
|
-
settingsLocalEnv: {
|
|
1126
|
-
...current.settingsLocal.env,
|
|
1127
|
-
...record.settingsLocalBackup,
|
|
1128
|
-
},
|
|
1129
|
-
})
|
|
1130
|
-
return
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
if (result.targetType === 'settings') {
|
|
1134
|
-
const currentSettings = await settingsEnvService.read()
|
|
1135
|
-
await settingsEnvService.write({
|
|
1136
|
-
...currentSettings,
|
|
1137
|
-
...record.backup,
|
|
1138
|
-
})
|
|
1139
|
-
return
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
const presetName = result.targetName ?? record.targetName
|
|
1143
|
-
const preset = await presetService.read(presetName)
|
|
1144
|
-
|
|
1145
|
-
await presetService.write({
|
|
1146
|
-
...preset,
|
|
1147
|
-
updatedAt: new Date().toISOString(),
|
|
1148
|
-
env: {
|
|
1149
|
-
...preset.env,
|
|
1150
|
-
...record.backup,
|
|
1151
|
-
},
|
|
1152
|
-
})
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
```
|
|
1156
|
-
|
|
1157
|
-
- [ ] **Step 4: Re-run the restore tests to verify they pass**
|
|
1158
|
-
|
|
1159
|
-
Run: `npm test -- tests/flows/restore-flow.test.ts tests/integration/init-restore.test.ts`
|
|
1160
|
-
Expected: PASS
|
|
1161
|
-
|
|
1162
|
-
- [ ] **Step 5: Commit the restore rewrite**
|
|
1163
|
-
|
|
1164
|
-
```bash
|
|
1165
|
-
git add src/flows/restore-flow.ts src/ink/restore-app.tsx src/commands/restore.ts src/cli.ts tests/flows/restore-flow.test.ts tests/integration/init-restore.test.ts
|
|
1166
|
-
git commit -m "feat: restore Claude shell init history"
|
|
1167
|
-
```
|
|
1168
|
-
|
|
1169
|
-
---
|
|
1170
|
-
|
|
1171
|
-
### Task 5: Wire the new services into the CLI and verify the full slice
|
|
1172
|
-
|
|
1173
|
-
**Files:**
|
|
1174
|
-
- Modify: `src/cli.ts`
|
|
1175
|
-
- Modify: `src/ink/init-app.tsx`
|
|
1176
|
-
- Modify: `src/ink/restore-app.tsx`
|
|
1177
|
-
- Test: `tests/cli/help.test.ts`
|
|
1178
|
-
- Test: focused and full suites
|
|
1179
|
-
|
|
1180
|
-
- [ ] **Step 1: Add the real service wiring and `--yes` shortcuts in `src/cli.ts`**
|
|
1181
|
-
|
|
1182
|
-
```ts
|
|
1183
|
-
import { join } from 'node:path'
|
|
1184
|
-
|
|
1185
|
-
import { resolveGlobalRoot } from './core/paths.js'
|
|
1186
|
-
import { createClaudeSettingsEnvService } from './services/claude-settings-env-service.js'
|
|
1187
|
-
import { createShellEnvService } from './services/shell-env-service.js'
|
|
1188
|
-
|
|
1189
|
-
const homeDir = process.env.HOME ?? process.cwd()
|
|
1190
|
-
const cwd = process.cwd()
|
|
1191
|
-
const settingsPath = join(cwd, 'settings.json')
|
|
1192
|
-
const globalRoot = resolveGlobalRoot()
|
|
1193
|
-
|
|
1194
|
-
const claudeSettingsEnvService = createClaudeSettingsEnvService({ homeDir })
|
|
1195
|
-
const shellEnvService = createShellEnvService({ homeDir })
|
|
1196
|
-
```
|
|
1197
|
-
|
|
1198
|
-
```ts
|
|
1199
|
-
program.command('init')
|
|
1200
|
-
.option('-y, --yes')
|
|
1201
|
-
.action((options) =>
|
|
1202
|
-
createInitCommand({
|
|
1203
|
-
claudeSettingsEnvService,
|
|
1204
|
-
shellEnvService,
|
|
1205
|
-
historyService,
|
|
1206
|
-
renderFlow: async (context) => {
|
|
1207
|
-
render(h(InitApp, context))
|
|
1208
|
-
if (context.yes) {
|
|
1209
|
-
return {
|
|
1210
|
-
confirmed: true,
|
|
1211
|
-
selectedKeys: context.requiredKeys,
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
return undefined
|
|
1215
|
-
},
|
|
1216
|
-
})({
|
|
1217
|
-
yes: options.yes,
|
|
1218
|
-
}),
|
|
1219
|
-
)
|
|
1220
|
-
|
|
1221
|
-
program.command('restore')
|
|
1222
|
-
.option('-y, --yes')
|
|
1223
|
-
.action((options) =>
|
|
1224
|
-
createRestoreCommand({
|
|
1225
|
-
historyService,
|
|
1226
|
-
claudeSettingsEnvService,
|
|
1227
|
-
shellEnvService,
|
|
1228
|
-
settingsEnvService,
|
|
1229
|
-
presetService,
|
|
1230
|
-
renderFlow: (context) => runRestoreFlow(context),
|
|
1231
|
-
})({
|
|
1232
|
-
yes: options.yes,
|
|
1233
|
-
}),
|
|
1234
|
-
)
|
|
1235
|
-
```
|
|
1236
|
-
|
|
1237
|
-
```ts
|
|
1238
|
-
function runRestoreFlow(context: {
|
|
1239
|
-
records: Awaited<ReturnType<typeof historyService.list>>
|
|
1240
|
-
yes: boolean
|
|
1241
|
-
}) {
|
|
1242
|
-
const state = createRestoreFlowState(context.records)
|
|
1243
|
-
const firstRecord = context.records[0]
|
|
1244
|
-
|
|
1245
|
-
if (!context.yes || !firstRecord) {
|
|
1246
|
-
render(h(RestoreApp, { state }))
|
|
1247
|
-
return undefined
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
const selectedRecordState = advanceRestoreFlow(state, {
|
|
1251
|
-
type: 'select-record',
|
|
1252
|
-
timestamp: firstRecord.timestamp,
|
|
1253
|
-
})
|
|
1254
|
-
|
|
1255
|
-
if (firstRecord.action === 'init') {
|
|
1256
|
-
const doneState = advanceRestoreFlow(selectedRecordState, { type: 'confirm' })
|
|
1257
|
-
if (doneState.step !== 'done') {
|
|
1258
|
-
return undefined
|
|
1259
|
-
}
|
|
1260
|
-
return {
|
|
1261
|
-
confirmed: true,
|
|
1262
|
-
timestamp: firstRecord.timestamp,
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
const confirmState = advanceRestoreFlow(selectedRecordState, {
|
|
1267
|
-
type: 'select-target',
|
|
1268
|
-
targetType: firstRecord.targetType,
|
|
1269
|
-
...(firstRecord.targetType === 'preset' ? { targetName: firstRecord.targetName } : {}),
|
|
1270
|
-
})
|
|
1271
|
-
|
|
1272
|
-
const doneState = advanceRestoreFlow(confirmState, { type: 'confirm' })
|
|
1273
|
-
if (doneState.step !== 'done') {
|
|
1274
|
-
return undefined
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
return {
|
|
1278
|
-
confirmed: true,
|
|
1279
|
-
timestamp: firstRecord.timestamp,
|
|
1280
|
-
targetType: doneState.targetType,
|
|
1281
|
-
targetName: doneState.targetName,
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
```
|
|
1285
|
-
|
|
1286
|
-
- [ ] **Step 2: Run the focused migration tests**
|
|
1287
|
-
|
|
1288
|
-
Run: `npm test -- tests/core/schema-mask.test.ts tests/core/paths.test.ts tests/services/storage.test.ts tests/services/claude-shell.test.ts tests/flows/init-flow.test.ts tests/flows/restore-flow.test.ts tests/integration/init-restore.test.ts`
|
|
1289
|
-
Expected: PASS
|
|
1290
|
-
|
|
1291
|
-
- [ ] **Step 3: Run the full suite**
|
|
1292
|
-
|
|
1293
|
-
Run: `npm test`
|
|
1294
|
-
Expected: PASS
|
|
1295
|
-
|
|
1296
|
-
- [ ] **Step 4: Build and smoke-test the CLI**
|
|
1297
|
-
|
|
1298
|
-
Run: `npm run build && node dist/cli.js --help`
|
|
1299
|
-
Expected: PASS and help output still includes `run`, `init`, `restore`, `preset`, and `debug`
|
|
1300
|
-
|
|
1301
|
-
- [ ] **Step 5: Commit the integrated redesign**
|
|
1302
|
-
|
|
1303
|
-
```bash
|
|
1304
|
-
git add src/cli.ts src/commands/init.ts src/commands/restore.ts src/flows/init-flow.ts src/flows/restore-flow.ts src/ink/init-app.tsx src/ink/restore-app.tsx src/services/claude-settings-env-service.ts src/services/shell-env-service.ts src/core/schema.ts src/core/paths.ts tests
|
|
1305
|
-
git commit -m "feat: migrate Claude env into managed shell blocks"
|
|
1306
|
-
```
|
|
1307
|
-
|
|
1308
|
-
---
|
|
1309
|
-
|
|
1310
|
-
## Self-Review
|
|
1311
|
-
|
|
1312
|
-
### Spec coverage
|
|
1313
|
-
- Read `~/.claude/settings.json` and `~/.claude/settings.local.json`: Task 2 and Task 3
|
|
1314
|
-
- `settings.local.json` precedence: Task 3 command tests and implementation
|
|
1315
|
-
- Required six keys preselected and non-removable: Task 3 flow tests and flow implementation
|
|
1316
|
-
- No preset creation in `init`: Task 3 command rewrite
|
|
1317
|
-
- Managed zsh/bash/fish blocks: Task 2 service tests and implementation
|
|
1318
|
-
- Per-file init backups and shell writes in history: Task 1 schema/storage changes and Task 3 history write
|
|
1319
|
-
- Dual restore for init records: Task 4
|
|
1320
|
-
- New terminal sessions only, no live shell mutation: Task 2 shell service behavior and Task 5 CLI wiring
|
|
1321
|
-
- Leave non-init restore behavior intact: Task 4 keeps restore target handling for `action: 'restore'`
|
|
1322
|
-
|
|
1323
|
-
### Placeholder scan
|
|
1324
|
-
- No `TODO`, `TBD`, or “implement later” placeholders remain.
|
|
1325
|
-
- Every task has exact files, targeted commands, expected failures, and concrete code snippets.
|
|
1326
|
-
|
|
1327
|
-
### Type consistency
|
|
1328
|
-
- Init history uses `migratedKeys`, `settingsBackup`, `settingsLocalBackup`, and `shellWrites` everywhere.
|
|
1329
|
-
- Non-init restore records keep `backup`, `targetType`, and `targetName`.
|
|
1330
|
-
- `claudeSettingsEnvService.write` consistently takes `{ settingsEnv, settingsLocalEnv }`.
|
|
1331
|
-
- `shellEnvService.removeKeys` consistently takes `(shellWrites, keys)`.
|