@lkangd/cc-env 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +15 -0
  2. package/dist/cli.js +68 -6
  3. package/dist/commands/completion.js +60 -0
  4. package/dist/commands/doctor.js +73 -0
  5. package/dist/commands/preset/edit.js +16 -11
  6. package/dist/commands/preset/rename.js +16 -0
  7. package/dist/commands/run.js +9 -1
  8. package/dist/ink/preset-edit-app.js +112 -0
  9. package/package.json +11 -2
  10. package/.claude/settings.json +0 -6
  11. package/.claude/settings.local.json +0 -8
  12. package/.nvmrc +0 -1
  13. package/CHANGELOG.md +0 -71
  14. package/docs/product-specs/index.draft.md +0 -106
  15. package/docs/product-specs/index.md +0 -911
  16. package/docs/product-specs/optional.md +0 -42
  17. package/docs/references/claude-code-env.md +0 -224
  18. package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +0 -1331
  19. package/docs/superpowers/plans/2026-04-24-cc-env.md +0 -1666
  20. package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +0 -1432
  21. package/docs/superpowers/specs/2026-04-24-cc-env-design.md +0 -438
  22. package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +0 -181
  23. package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +0 -78
  24. package/src/cli.ts +0 -340
  25. package/src/commands/init.ts +0 -139
  26. package/src/commands/preset/create.ts +0 -96
  27. package/src/commands/preset/delete.ts +0 -62
  28. package/src/commands/preset/show.ts +0 -51
  29. package/src/commands/restore.ts +0 -150
  30. package/src/commands/run.ts +0 -158
  31. package/src/core/errors.ts +0 -13
  32. package/src/core/find-claude.ts +0 -70
  33. package/src/core/format.ts +0 -29
  34. package/src/core/fs.ts +0 -18
  35. package/src/core/gitignore.ts +0 -26
  36. package/src/core/logger.ts +0 -11
  37. package/src/core/mask.ts +0 -17
  38. package/src/core/paths.ts +0 -41
  39. package/src/core/process-env.ts +0 -11
  40. package/src/core/schema.ts +0 -55
  41. package/src/core/spawn.ts +0 -36
  42. package/src/flows/init-flow.ts +0 -61
  43. package/src/flows/preset-create-flow.ts +0 -129
  44. package/src/flows/restore-flow.ts +0 -144
  45. package/src/ink/init-app.tsx +0 -110
  46. package/src/ink/preset-create-app.tsx +0 -451
  47. package/src/ink/preset-delete-app.tsx +0 -114
  48. package/src/ink/preset-show-app.tsx +0 -76
  49. package/src/ink/restore-app.tsx +0 -230
  50. package/src/ink/run-preset-select-app.tsx +0 -83
  51. package/src/ink/summary.tsx +0 -91
  52. package/src/services/claude-settings-env-service.ts +0 -72
  53. package/src/services/history-service.ts +0 -48
  54. package/src/services/preset-service.ts +0 -72
  55. package/src/services/project-env-service.ts +0 -128
  56. package/src/services/project-state-service.ts +0 -31
  57. package/src/services/settings-env-service.ts +0 -40
  58. package/src/services/shell-env-service.ts +0 -112
  59. package/src/types.d.ts +0 -19
  60. package/tests/cli/help.test.ts +0 -133
  61. package/tests/cli/init.test.ts +0 -76
  62. package/tests/cli/restore.test.ts +0 -172
  63. package/tests/commands/create.test.ts +0 -263
  64. package/tests/commands/output.test.ts +0 -119
  65. package/tests/commands/run.test.ts +0 -218
  66. package/tests/core/gitignore.test.ts +0 -98
  67. package/tests/core/paths.test.ts +0 -24
  68. package/tests/core/schema-mask.test.ts +0 -182
  69. package/tests/core/spawn.test.ts +0 -47
  70. package/tests/flows/init-flow.test.ts +0 -40
  71. package/tests/flows/preset-create-flow.test.ts +0 -225
  72. package/tests/flows/restore-flow.test.ts +0 -157
  73. package/tests/integration/init-restore.test.ts +0 -406
  74. package/tests/services/claude-shell.test.ts +0 -183
  75. package/tests/services/storage.test.ts +0 -143
  76. package/tsconfig.build.json +0 -9
  77. package/tsconfig.json +0 -22
  78. 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)`.