@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.
Files changed (72) hide show
  1. package/LICENSE +15 -0
  2. package/dist/cli.js +3 -4
  3. package/package.json +8 -1
  4. package/.claude/settings.json +0 -6
  5. package/.claude/settings.local.json +0 -8
  6. package/.nvmrc +0 -1
  7. package/CHANGELOG.md +0 -66
  8. package/docs/product-specs/index.draft.md +0 -106
  9. package/docs/product-specs/index.md +0 -911
  10. package/docs/product-specs/optional.md +0 -42
  11. package/docs/references/claude-code-env.md +0 -224
  12. package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +0 -1331
  13. package/docs/superpowers/plans/2026-04-24-cc-env.md +0 -1666
  14. package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +0 -1432
  15. package/docs/superpowers/specs/2026-04-24-cc-env-design.md +0 -438
  16. package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +0 -181
  17. package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +0 -78
  18. package/src/cli.ts +0 -339
  19. package/src/commands/init.ts +0 -139
  20. package/src/commands/preset/create.ts +0 -96
  21. package/src/commands/preset/delete.ts +0 -62
  22. package/src/commands/preset/show.ts +0 -51
  23. package/src/commands/restore.ts +0 -150
  24. package/src/commands/run.ts +0 -158
  25. package/src/core/errors.ts +0 -13
  26. package/src/core/find-claude.ts +0 -70
  27. package/src/core/format.ts +0 -29
  28. package/src/core/fs.ts +0 -18
  29. package/src/core/gitignore.ts +0 -26
  30. package/src/core/logger.ts +0 -11
  31. package/src/core/mask.ts +0 -17
  32. package/src/core/paths.ts +0 -41
  33. package/src/core/process-env.ts +0 -11
  34. package/src/core/schema.ts +0 -55
  35. package/src/core/spawn.ts +0 -36
  36. package/src/flows/init-flow.ts +0 -61
  37. package/src/flows/preset-create-flow.ts +0 -129
  38. package/src/flows/restore-flow.ts +0 -144
  39. package/src/ink/init-app.tsx +0 -110
  40. package/src/ink/preset-create-app.tsx +0 -451
  41. package/src/ink/preset-delete-app.tsx +0 -114
  42. package/src/ink/preset-show-app.tsx +0 -76
  43. package/src/ink/restore-app.tsx +0 -230
  44. package/src/ink/run-preset-select-app.tsx +0 -83
  45. package/src/ink/summary.tsx +0 -91
  46. package/src/services/claude-settings-env-service.ts +0 -72
  47. package/src/services/history-service.ts +0 -48
  48. package/src/services/preset-service.ts +0 -72
  49. package/src/services/project-env-service.ts +0 -128
  50. package/src/services/project-state-service.ts +0 -31
  51. package/src/services/settings-env-service.ts +0 -40
  52. package/src/services/shell-env-service.ts +0 -112
  53. package/src/types.d.ts +0 -19
  54. package/tests/cli/help.test.ts +0 -133
  55. package/tests/cli/init.test.ts +0 -76
  56. package/tests/cli/restore.test.ts +0 -172
  57. package/tests/commands/create.test.ts +0 -263
  58. package/tests/commands/output.test.ts +0 -119
  59. package/tests/commands/run.test.ts +0 -218
  60. package/tests/core/gitignore.test.ts +0 -98
  61. package/tests/core/paths.test.ts +0 -24
  62. package/tests/core/schema-mask.test.ts +0 -182
  63. package/tests/core/spawn.test.ts +0 -47
  64. package/tests/flows/init-flow.test.ts +0 -40
  65. package/tests/flows/preset-create-flow.test.ts +0 -225
  66. package/tests/flows/restore-flow.test.ts +0 -157
  67. package/tests/integration/init-restore.test.ts +0 -406
  68. package/tests/services/claude-shell.test.ts +0 -183
  69. package/tests/services/storage.test.ts +0 -143
  70. package/tsconfig.build.json +0 -9
  71. package/tsconfig.json +0 -22
  72. package/vitest.config.ts +0 -8
@@ -1,1432 +0,0 @@
1
- # Preset Create Interactive Refactor 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:** Refactor `preset create` from a semi-interactive command with CLI flags to a fully interactive ink UI wizard.
6
-
7
- **Architecture:** Extend the existing `preset-create-flow.ts` state machine with new conditional steps (filePath, manualInput, name). Rewrite `preset-create-app.tsx` to render each step with appropriate ink UI. Simplify `commands/preset/create.ts` to a thin wrapper that calls `renderFlow` and writes the result. Update `cli.ts` to remove all flags.
8
-
9
- **Tech Stack:** TypeScript, React 19, ink v6, zod v4, yaml
10
-
11
- ---
12
-
13
- ## File Structure
14
-
15
- | Action | File | Responsibility |
16
- |--------|------|----------------|
17
- | Rewrite | `src/flows/preset-create-flow.ts` | 7-step state machine with conditional paths |
18
- | Rewrite | `src/ink/preset-create-app.tsx` | Multi-step ink UI for all 7 steps |
19
- | Rewrite | `src/commands/preset/create.ts` | Thin command that calls renderFlow + writes result |
20
- | Modify | `src/cli.ts:235-266` | Remove flags, update renderFlow wiring |
21
- | Rewrite | `tests/flows/preset-create-flow.test.ts` | Tests for new flow paths |
22
- | Rewrite | `tests/commands/create.test.ts` | Tests for new command function |
23
-
24
- ---
25
-
26
- ### Task 1: Rewrite the flow state machine
27
-
28
- **Files:**
29
- - Rewrite: `src/flows/preset-create-flow.ts`
30
- - Rewrite: `tests/flows/preset-create-flow.test.ts`
31
-
32
- - [ ] **Step 1: Write the new flow types and state factory**
33
-
34
- Replace the entire content of `src/flows/preset-create-flow.ts`:
35
-
36
- ```typescript
37
- import type { EnvMap } from '../core/schema.js'
38
-
39
- export type PresetCreateSource = 'file' | 'manual'
40
- export type PresetCreateDestination = 'global' | 'project'
41
-
42
- export type PresetCreateStep =
43
- | 'source'
44
- | 'filePath'
45
- | 'keys'
46
- | 'manualInput'
47
- | 'name'
48
- | 'destination'
49
- | 'confirm'
50
- | 'done'
51
-
52
- export type PresetCreateFlowState = {
53
- step: PresetCreateStep
54
- source?: PresetCreateSource
55
- filePath?: string
56
- env: EnvMap
57
- allKeys: string[]
58
- selectedKeys: string[]
59
- presetName: string
60
- destination?: PresetCreateDestination
61
- error?: string
62
- }
63
-
64
- export type PresetCreateFlowResult = Pick<
65
- PresetCreateFlowState,
66
- 'source' | 'filePath' | 'env' | 'selectedKeys' | 'presetName' | 'destination'
67
- >
68
-
69
- export type PresetCreateFlowAction =
70
- | { type: 'select-source'; source: PresetCreateSource }
71
- | { type: 'set-file-path'; filePath: string }
72
- | { type: 'set-error'; error: string }
73
- | { type: 'select-keys'; keys: string[]; env: EnvMap }
74
- | { type: 'add-manual-pair'; key: string; value: string }
75
- | { type: 'finish-manual-input' }
76
- | { type: 'set-name'; name: string }
77
- | { type: 'select-destination'; destination: PresetCreateDestination }
78
- | { type: 'confirm' }
79
-
80
- export function createPresetCreateFlowState(): PresetCreateFlowState {
81
- return {
82
- step: 'source',
83
- env: {},
84
- allKeys: [],
85
- selectedKeys: [],
86
- presetName: '',
87
- }
88
- }
89
-
90
- export function advancePresetCreateFlow(
91
- state: PresetCreateFlowState,
92
- action: PresetCreateFlowAction,
93
- ): PresetCreateFlowState {
94
- switch (state.step) {
95
- case 'source':
96
- if (action.type !== 'select-source') return state
97
- return {
98
- ...state,
99
- step: action.source === 'file' ? 'filePath' : 'manualInput',
100
- source: action.source,
101
- }
102
-
103
- case 'filePath':
104
- if (action.type === 'set-error') {
105
- return { ...state, error: action.error }
106
- }
107
- if (action.type !== 'set-file-path') return state
108
- return {
109
- ...state,
110
- step: 'keys',
111
- filePath: action.filePath,
112
- error: undefined,
113
- }
114
-
115
- case 'keys':
116
- if (action.type !== 'select-keys') return state
117
- return {
118
- ...state,
119
- step: 'name',
120
- selectedKeys: action.keys,
121
- env: action.env,
122
- }
123
-
124
- case 'manualInput':
125
- if (action.type === 'add-manual-pair') {
126
- return {
127
- ...state,
128
- env: { ...state.env, [action.key]: action.value },
129
- selectedKeys: [...state.selectedKeys, action.key],
130
- error: undefined,
131
- }
132
- }
133
- if (action.type === 'set-error') {
134
- return { ...state, error: action.error }
135
- }
136
- if (action.type !== 'finish-manual-input') return state
137
- return { ...state, step: 'name' }
138
-
139
- case 'name':
140
- if (action.type !== 'set-name') return state
141
- return {
142
- ...state,
143
- step: 'destination',
144
- presetName: action.name,
145
- }
146
-
147
- case 'destination':
148
- if (action.type !== 'select-destination') return state
149
- return {
150
- ...state,
151
- step: 'confirm',
152
- destination: action.destination,
153
- }
154
-
155
- case 'confirm':
156
- if (action.type !== 'confirm') return state
157
- return { ...state, step: 'done' }
158
-
159
- case 'done':
160
- return state
161
- }
162
- }
163
- ```
164
-
165
- - [ ] **Step 2: Write flow tests**
166
-
167
- Replace the entire content of `tests/flows/preset-create-flow.test.ts`:
168
-
169
- ```typescript
170
- import { describe, expect, it } from 'vitest'
171
-
172
- import {
173
- advancePresetCreateFlow,
174
- createPresetCreateFlowState,
175
- type PresetCreateFlowState,
176
- } from '../../src/flows/preset-create-flow.js'
177
-
178
- describe('preset create flow', () => {
179
- it("starts at step 'source' with empty defaults", () => {
180
- expect(createPresetCreateFlowState()).toEqual({
181
- step: 'source',
182
- env: {},
183
- allKeys: [],
184
- selectedKeys: [],
185
- presetName: '',
186
- })
187
- })
188
-
189
- describe('file path', () => {
190
- function goToFilePath(): PresetCreateFlowState {
191
- return advancePresetCreateFlow(createPresetCreateFlowState(), {
192
- type: 'select-source',
193
- source: 'file',
194
- })
195
- }
196
-
197
- it('source=file advances to filePath', () => {
198
- expect(goToFilePath().step).toBe('filePath')
199
- })
200
-
201
- it('set-file-path advances to keys', () => {
202
- const state = advancePresetCreateFlow(goToFilePath(), {
203
- type: 'set-file-path',
204
- filePath: '/path/to/env.json',
205
- })
206
- expect(state.step).toBe('keys')
207
- expect(state.filePath).toBe('/path/to/env.json')
208
- })
209
-
210
- it('set-error stays on filePath with error message', () => {
211
- const state = advancePresetCreateFlow(goToFilePath(), {
212
- type: 'set-error',
213
- error: 'File not found',
214
- })
215
- expect(state.step).toBe('filePath')
216
- expect(state.error).toBe('File not found')
217
- })
218
- })
219
-
220
- describe('manual input path', () => {
221
- function goToManualInput(): PresetCreateFlowState {
222
- return advancePresetCreateFlow(createPresetCreateFlowState(), {
223
- type: 'select-source',
224
- source: 'manual',
225
- })
226
- }
227
-
228
- it('source=manual advances to manualInput', () => {
229
- expect(goToManualInput().step).toBe('manualInput')
230
- })
231
-
232
- it('add-manual-pair accumulates pairs', () => {
233
- const state = advancePresetCreateFlow(goToManualInput(), {
234
- type: 'add-manual-pair',
235
- key: 'FOO',
236
- value: 'bar',
237
- })
238
- expect(state.env).toEqual({ FOO: 'bar' })
239
- expect(state.selectedKeys).toEqual(['FOO'])
240
- expect(state.step).toBe('manualInput')
241
- })
242
-
243
- it('add-manual-pair overwrites existing key', () => {
244
- const first = advancePresetCreateFlow(goToManualInput(), {
245
- type: 'add-manual-pair',
246
- key: 'FOO',
247
- value: 'bar',
248
- })
249
- const second = advancePresetCreateFlow(first, {
250
- type: 'add-manual-pair',
251
- key: 'FOO',
252
- value: 'updated',
253
- })
254
- expect(second.env.FOO).toBe('updated')
255
- expect(second.selectedKeys).toEqual(['FOO'])
256
- })
257
-
258
- it('set-error on manualInput sets error', () => {
259
- const state = advancePresetCreateFlow(goToManualInput(), {
260
- type: 'set-error',
261
- error: 'Invalid format',
262
- })
263
- expect(state.error).toBe('Invalid format')
264
- })
265
-
266
- it('finish-manual-input advances to name', () => {
267
- const state = advancePresetCreateFlow(goToManualInput(), {
268
- type: 'finish-manual-input',
269
- })
270
- expect(state.step).toBe('name')
271
- })
272
- })
273
-
274
- describe('shared path after source input', () => {
275
- function goToNameViaFile(): PresetCreateFlowState {
276
- const filePath = advancePresetCreateFlow(createPresetCreateFlowState(), {
277
- type: 'select-source',
278
- source: 'file',
279
- })
280
- const keys = advancePresetCreateFlow(filePath, {
281
- type: 'set-file-path',
282
- filePath: '/env.json',
283
- })
284
- return advancePresetCreateFlow(keys, {
285
- type: 'select-keys',
286
- keys: ['API_KEY'],
287
- env: { API_KEY: 'secret' },
288
- })
289
- }
290
-
291
- function goToNameViaManual(): PresetCreateFlowState {
292
- const manual = advancePresetCreateFlow(createPresetCreateFlowState(), {
293
- type: 'select-source',
294
- source: 'manual',
295
- })
296
- return advancePresetCreateFlow(manual, {
297
- type: 'finish-manual-input',
298
- })
299
- }
300
-
301
- it('set-name advances to destination', () => {
302
- const state = advancePresetCreateFlow(goToNameViaFile(), {
303
- type: 'set-name',
304
- name: 'my-preset',
305
- })
306
- expect(state.step).toBe('destination')
307
- expect(state.presetName).toBe('my-preset')
308
- })
309
-
310
- it('select-destination advances to confirm', () => {
311
- const name = advancePresetCreateFlow(goToNameViaFile(), {
312
- type: 'set-name',
313
- name: 'my-preset',
314
- })
315
- const dest = advancePresetCreateFlow(name, {
316
- type: 'select-destination',
317
- destination: 'global',
318
- })
319
- expect(dest.step).toBe('confirm')
320
- expect(dest.destination).toBe('global')
321
- })
322
-
323
- it('confirm advances to done', () => {
324
- const name = advancePresetCreateFlow(goToNameViaFile(), {
325
- type: 'set-name',
326
- name: 'my-preset',
327
- })
328
- const dest = advancePresetCreateFlow(name, {
329
- type: 'select-destination',
330
- destination: 'project',
331
- })
332
- const done = advancePresetCreateFlow(dest, { type: 'confirm' })
333
- expect(done.step).toBe('done')
334
- })
335
-
336
- it('manual path reaches done through name→destination→confirm', () => {
337
- const name = advancePresetCreateFlow(goToNameViaManual(), {
338
- type: 'set-name',
339
- name: 'manual-preset',
340
- })
341
- const dest = advancePresetCreateFlow(name, {
342
- type: 'select-destination',
343
- destination: 'global',
344
- })
345
- const done = advancePresetCreateFlow(dest, { type: 'confirm' })
346
- expect(done.step).toBe('done')
347
- expect(done.presetName).toBe('manual-preset')
348
- })
349
- })
350
-
351
- it('ignores invalid transitions without mutating state', () => {
352
- const state = createPresetCreateFlowState()
353
-
354
- expect(
355
- advancePresetCreateFlow(state, {
356
- type: 'select-keys',
357
- keys: ['FOO'],
358
- env: { FOO: 'bar' },
359
- }),
360
- ).toEqual(state)
361
-
362
- expect(
363
- advancePresetCreateFlow(state, {
364
- type: 'confirm',
365
- }),
366
- ).toEqual(state)
367
- })
368
-
369
- it('ignores changes after the flow is done', () => {
370
- const source = advancePresetCreateFlow(createPresetCreateFlowState(), {
371
- type: 'select-source',
372
- source: 'manual',
373
- })
374
- const name = advancePresetCreateFlow(source, {
375
- type: 'finish-manual-input',
376
- })
377
- const dest = advancePresetCreateFlow(name, {
378
- type: 'set-name',
379
- name: 'test',
380
- })
381
- const confirm = advancePresetCreateFlow(dest, {
382
- type: 'select-destination',
383
- destination: 'global',
384
- })
385
- const done = advancePresetCreateFlow(confirm, { type: 'confirm' })
386
-
387
- expect(
388
- advancePresetCreateFlow(done, {
389
- type: 'select-source',
390
- source: 'file',
391
- }),
392
- ).toEqual(done)
393
- })
394
- })
395
- ```
396
-
397
- - [ ] **Step 3: Run the tests**
398
-
399
- Run: `npx vitest run tests/flows/preset-create-flow.test.ts`
400
- Expected: All tests PASS.
401
-
402
- - [ ] **Step 4: Commit**
403
-
404
- ```bash
405
- git add src/flows/preset-create-flow.ts tests/flows/preset-create-flow.test.ts
406
- git commit -m "refactor: rewrite preset-create-flow state machine for full interactive wizard"
407
- ```
408
-
409
- ---
410
-
411
- ### Task 2: Update readEnvFile to handle JSON env field
412
-
413
- **Files:**
414
- - Modify: `src/commands/preset/create.ts`
415
-
416
- - [ ] **Step 1: Add tests for readEnvFile JSON env field extraction**
417
-
418
- Create a new test section in `tests/commands/create.test.ts` (we'll rewrite the full file later, but add the readEnvFile tests now). Actually, since `readEnvFile` will become a shared utility used by the ink component via a callback, extract it first.
419
-
420
- Instead, we'll keep `readEnvFile` in `create.ts` and add a test for the JSON env field logic. Add to `tests/commands/create.test.ts`:
421
-
422
- Actually, let's keep `readEnvFile` and `parseInlinePairs` as exported functions from `create.ts` (they already are). We'll test them inline. Since we'll fully rewrite the command tests later, add just the new readEnvFile behavior test now.
423
-
424
- For now, just update the `readEnvFile` function in `src/commands/preset/create.ts`. The current function at line 56-68:
425
-
426
- ```typescript
427
- async function readEnvFile(filePath: string): Promise<EnvMap> {
428
- try {
429
- const content = await readFile(filePath, 'utf8')
430
- const extension = extname(filePath).toLowerCase()
431
- const parsed = extension === '.yaml' || extension === '.yml'
432
- ? parseYaml(content)
433
- : JSON.parse(content)
434
-
435
- return toProcessEnvMap((parsed ?? {}) as Record<string, unknown>)
436
- } catch {
437
- throw new CliError(`Failed to read env file: ${filePath}`, 2)
438
- }
439
- }
440
- ```
441
-
442
- Replace it with:
443
-
444
- ```typescript
445
- export async function readEnvFile(filePath: string): Promise<{ allKeys: string[]; env: EnvMap }> {
446
- try {
447
- const content = await readFile(filePath, 'utf8')
448
- const extension = extname(filePath).toLowerCase()
449
-
450
- if (extension !== '.yaml' && extension !== '.yml' && extension !== '.json') {
451
- throw new CliError(`Unsupported file format: ${extension}`, 2)
452
- }
453
-
454
- const parsed = extension === '.yaml' || extension === '.yml'
455
- ? parseYaml(content)
456
- : JSON.parse(content)
457
-
458
- const raw = (parsed ?? {}) as Record<string, unknown>
459
- const source = extension === '.json'
460
- && raw
461
- && typeof raw === 'object'
462
- && 'env' in raw
463
- && raw.env
464
- && typeof raw.env === 'object'
465
- && !Array.isArray(raw.env)
466
- ? raw.env as Record<string, unknown>
467
- : raw
468
-
469
- const env = toProcessEnvMap(source)
470
- return {
471
- allKeys: Object.keys(env),
472
- env,
473
- }
474
- } catch (error) {
475
- if (error instanceof CliError) throw error
476
- throw new CliError(`Failed to read env file: ${filePath}`, 2)
477
- }
478
- }
479
- ```
480
-
481
- - [ ] **Step 2: Update readEnvFile in src/commands/preset/create.ts**
482
-
483
- Replace the existing `readEnvFile` function (lines 56-68) with the new version above. Also remove the `buildPlaceholderEnv` function (lines 70-82) entirely.
484
-
485
- - [ ] **Step 3: Run existing tests to see what breaks**
486
-
487
- Run: `npx vitest run tests/commands/create.test.ts`
488
- Expected: Some tests fail because `readEnvFile` now returns `{ allKeys, env }` instead of just `EnvMap`, and the command function signature has changed. This is expected — we fix the command and tests in later tasks.
489
-
490
- - [ ] **Step 4: Commit**
491
-
492
- ```bash
493
- git add src/commands/preset/create.ts
494
- git commit -m "refactor: update readEnvFile to extract JSON env field and return allKeys"
495
- ```
496
-
497
- ---
498
-
499
- ### Task 3: Rewrite the ink component
500
-
501
- **Files:**
502
- - Rewrite: `src/ink/preset-create-app.tsx`
503
-
504
- - [ ] **Step 1: Write the new PresetCreateApp component**
505
-
506
- Replace the entire content of `src/ink/preset-create-app.tsx`:
507
-
508
- ```tsx
509
- import React, { useState } from 'react'
510
- import { Box, Text, useApp, useInput } from 'ink'
511
-
512
- import {
513
- advancePresetCreateFlow,
514
- createPresetCreateFlowState,
515
- type PresetCreateDestination,
516
- type PresetCreateFlowResult,
517
- type PresetCreateSource,
518
- } from '../flows/preset-create-flow.js'
519
- import type { EnvMap } from '../core/schema.js'
520
- import { EnvSummary } from './summary.js'
521
-
522
- export type PresetCreateAppResult = PresetCreateFlowResult & {
523
- destination: PresetCreateDestination
524
- }
525
-
526
- type PresetCreateAppProps = {
527
- onSubmit: (result: PresetCreateAppResult) => Promise<void> | void
528
- readFile: (filePath: string) => Promise<{ allKeys: string[]; env: EnvMap }>
529
- globalPresetPath: (name: string) => string
530
- projectEnvPath: string
531
- }
532
-
533
- function SourceStep({
534
- cursor,
535
- }: {
536
- cursor: number
537
- }) {
538
- const options: { label: string; value: PresetCreateSource }[] = [
539
- { label: 'File import', value: 'file' },
540
- { label: 'Manual input', value: 'manual' },
541
- ]
542
- return (
543
- <Box flexDirection="column">
544
- <Text bold>Select env source</Text>
545
- <Text dimColor>↑/k ↓/j navigate · enter confirm</Text>
546
- <Box flexDirection="column" marginTop={1}>
547
- {options.map((opt, i) => (
548
- <Box key={opt.value}>
549
- <Text>{i === cursor ? '❯ ' : ' '}</Text>
550
- <Text color={i === cursor ? 'cyan' : undefined}>{opt.label}</Text>
551
- </Box>
552
- ))}
553
- </Box>
554
- </Box>
555
- )
556
- }
557
-
558
- function FilePathStep({
559
- value,
560
- error,
561
- }: {
562
- value: string
563
- error?: string
564
- }) {
565
- return (
566
- <Box flexDirection="column">
567
- <Text bold>Enter file path (.yaml/.yml/.json)</Text>
568
- <Box marginTop={1}>
569
- <Text dimColor>{'>'} </Text>
570
- <Text color="cyan">{value}</Text>
571
- <Text dimColor>█</Text>
572
- </Box>
573
- {error ? <Text color="red">{error}</Text> : null}
574
- </Box>
575
- )
576
- }
577
-
578
- function KeysStep({
579
- keys,
580
- selectedKeys,
581
- cursor,
582
- }: {
583
- keys: string[]
584
- selectedKeys: string[]
585
- cursor: number
586
- }) {
587
- return (
588
- <Box flexDirection="column">
589
- <Text bold>Select env keys to import</Text>
590
- <Text dimColor>↑/k ↓/j navigate · space toggle · enter confirm</Text>
591
- <Box flexDirection="column" marginTop={1}>
592
- {keys.map((key, i) => {
593
- const isSelected = selectedKeys.includes(key)
594
- return (
595
- <Box key={key}>
596
- <Text>{i === cursor ? '❯ ' : ' '}</Text>
597
- <Text color={isSelected ? 'green' : ''}>{isSelected ? '[x]' : '[ ]'}</Text>
598
- <Text> {key}</Text>
599
- </Box>
600
- )
601
- })}
602
- </Box>
603
- <Box marginTop={1}>
604
- <Text dimColor>{selectedKeys.length} of {keys.length} selected</Text>
605
- </Box>
606
- </Box>
607
- )
608
- }
609
-
610
- function ManualInputStep({
611
- entries,
612
- value,
613
- error,
614
- }: {
615
- entries: [string, string][]
616
- value: string
617
- error?: string
618
- }) {
619
- return (
620
- <Box flexDirection="column">
621
- <Text bold>Enter KEY=VALUE pairs (press q when done)</Text>
622
- {entries.length > 0 ? (
623
- <Box flexDirection="column" marginBottom={1}>
624
- {entries.map(([key, val]) => (
625
- <Box key={key}>
626
- <Text color="yellow">• </Text>
627
- <Text color="magenta">{key}</Text>
628
- <Text dimColor>=</Text>
629
- <Text>{val}</Text>
630
- </Box>
631
- ))}
632
- </Box>
633
- ) : null}
634
- <Box>
635
- <Text dimColor>{'>'} </Text>
636
- <Text color="cyan">{value}</Text>
637
- <Text dimColor>█</Text>
638
- </Box>
639
- {error ? <Text color="red">{error}</Text> : null}
640
- </Box>
641
- )
642
- }
643
-
644
- function NameStep({
645
- value,
646
- }: {
647
- value: string
648
- }) {
649
- return (
650
- <Box flexDirection="column">
651
- <Text bold>Enter preset name</Text>
652
- <Box marginTop={1}>
653
- <Text dimColor>{'>'} </Text>
654
- <Text color="cyan">{value}</Text>
655
- <Text dimColor>█</Text>
656
- </Box>
657
- </Box>
658
- )
659
- }
660
-
661
- function DestinationStep({
662
- cursor,
663
- }: {
664
- cursor: number
665
- }) {
666
- const options: { label: string; value: PresetCreateDestination }[] = [
667
- { label: 'Global preset', value: 'global' },
668
- { label: 'Project preset', value: 'project' },
669
- ]
670
- return (
671
- <Box flexDirection="column">
672
- <Text bold>Select save destination</Text>
673
- <Text dimColor>↑/k ↓/j navigate · enter confirm</Text>
674
- <Box flexDirection="column" marginTop={1}>
675
- {options.map((opt, i) => (
676
- <Box key={opt.value}>
677
- <Text>{i === cursor ? '❯ ' : ' '}</Text>
678
- <Text color={i === cursor ? 'cyan' : undefined}>{opt.label}</Text>
679
- </Box>
680
- ))}
681
- </Box>
682
- </Box>
683
- )
684
- }
685
-
686
- export function PresetCreateApp({
687
- onSubmit,
688
- readFile,
689
- globalPresetPath,
690
- projectEnvPath,
691
- }: PresetCreateAppProps) {
692
- const { exit } = useApp()
693
- const [state, setState] = useState(createPresetCreateFlowState)
694
- const [textInput, setTextInput] = useState('')
695
- const [listCursor, setListCursor] = useState(0)
696
- const [allKeys, setAllKeys] = useState<string[]>([])
697
- const [fileEnv, setFileEnv] = useState<EnvMap>({})
698
-
699
- useInput((input, key) => {
700
- if (key.escape) {
701
- exit()
702
- return
703
- }
704
-
705
- // source step
706
- if (state.step === 'source') {
707
- if (input === 'q') {
708
- exit()
709
- return
710
- }
711
- if (key.upArrow || input === 'k') {
712
- setListCursor((c) => Math.max(0, c - 1))
713
- return
714
- }
715
- if (key.downArrow || input === 'j') {
716
- setListCursor((c) => Math.min(1, c + 1))
717
- return
718
- }
719
- if (key.return) {
720
- const source: PresetCreateSource = listCursor === 0 ? 'file' : 'manual'
721
- setState((s) => advancePresetCreateFlow(s, { type: 'select-source', source }))
722
- setListCursor(0)
723
- setTextInput('')
724
- return
725
- }
726
- }
727
-
728
- // filePath step
729
- if (state.step === 'filePath') {
730
- if (input === 'q') {
731
- exit()
732
- return
733
- }
734
- if (key.backspace || key.delete) {
735
- setTextInput((v) => v.slice(0, -1))
736
- return
737
- }
738
- if (key.return) {
739
- void (async () => {
740
- try {
741
- const result = await readFile(textInput)
742
- if (result.allKeys.length === 0) {
743
- setState((s) => advancePresetCreateFlow(s, {
744
- type: 'set-error',
745
- error: 'No valid env keys found in file',
746
- }))
747
- return
748
- }
749
- setAllKeys(result.allKeys)
750
- setFileEnv(result.env)
751
- setState((s) => advancePresetCreateFlow(s, {
752
- type: 'set-file-path',
753
- filePath: textInput,
754
- }))
755
- setListCursor(0)
756
- } catch (err) {
757
- const message = err instanceof Error ? err.message : 'Failed to read file'
758
- setState((s) => advancePresetCreateFlow(s, {
759
- type: 'set-error',
760
- error: message,
761
- }))
762
- }
763
- })()
764
- return
765
- }
766
- if (input && !key.ctrl && !key.meta) {
767
- setTextInput((v) => v + input)
768
- return
769
- }
770
- }
771
-
772
- // keys step
773
- if (state.step === 'keys') {
774
- if (input === 'q') {
775
- exit()
776
- return
777
- }
778
- if (key.upArrow || input === 'k') {
779
- setListCursor((c) => Math.max(0, c - 1))
780
- return
781
- }
782
- if (key.downArrow || input === 'j') {
783
- setListCursor((c) => Math.min(allKeys.length - 1, c + 1))
784
- return
785
- }
786
- if (input === ' ') {
787
- const targetKey = allKeys[listCursor]
788
- if (targetKey) {
789
- const newSelected = state.selectedKeys.includes(targetKey)
790
- ? state.selectedKeys.filter((k) => k !== targetKey)
791
- : [...state.selectedKeys, targetKey]
792
- setState((s) => ({ ...s, selectedKeys: newSelected }))
793
- }
794
- return
795
- }
796
- if (key.return && state.selectedKeys.length > 0) {
797
- const selectedEnv: EnvMap = {}
798
- for (const k of state.selectedKeys) {
799
- selectedEnv[k] = fileEnv[k] ?? ''
800
- }
801
- setState((s) => advancePresetCreateFlow(s, {
802
- type: 'select-keys',
803
- keys: state.selectedKeys,
804
- env: selectedEnv,
805
- }))
806
- setTextInput('')
807
- return
808
- }
809
- }
810
-
811
- // manualInput step
812
- if (state.step === 'manualInput') {
813
- if (input === 'q' && textInput === '') {
814
- if (state.selectedKeys.length === 0) {
815
- setState((s) => advancePresetCreateFlow(s, {
816
- type: 'set-error',
817
- error: 'Add at least one KEY=VALUE pair',
818
- }))
819
- return
820
- }
821
- setState((s) => advancePresetCreateFlow(s, { type: 'finish-manual-input' }))
822
- setTextInput('')
823
- return
824
- }
825
- if (key.backspace || key.delete) {
826
- setTextInput((v) => v.slice(0, -1))
827
- return
828
- }
829
- if (key.return) {
830
- const separatorIndex = textInput.indexOf('=')
831
- if (separatorIndex <= 0) {
832
- setState((s) => advancePresetCreateFlow(s, {
833
- type: 'set-error',
834
- error: 'Format must be KEY=VALUE',
835
- }))
836
- return
837
- }
838
- const k = textInput.slice(0, separatorIndex)
839
- const v = textInput.slice(separatorIndex + 1)
840
- if (!/^[A-Z0-9_]+$/.test(k)) {
841
- setState((s) => advancePresetCreateFlow(s, {
842
- type: 'set-error',
843
- error: 'Key must match [A-Z0-9_]+',
844
- }))
845
- return
846
- }
847
- setState((s) => advancePresetCreateFlow(s, {
848
- type: 'add-manual-pair',
849
- key: k,
850
- value: v,
851
- }))
852
- setTextInput('')
853
- return
854
- }
855
- if (input && !key.ctrl && !key.meta) {
856
- setTextInput((v) => v + input)
857
- return
858
- }
859
- }
860
-
861
- // name step
862
- if (state.step === 'name') {
863
- if (input === 'q') {
864
- exit()
865
- return
866
- }
867
- if (key.backspace || key.delete) {
868
- setTextInput((v) => v.slice(0, -1))
869
- return
870
- }
871
- if (key.return && textInput.trim().length > 0) {
872
- setState((s) => advancePresetCreateFlow(s, {
873
- type: 'set-name',
874
- name: textInput.trim(),
875
- }))
876
- setListCursor(0)
877
- return
878
- }
879
- if (key.return && textInput.trim().length === 0) {
880
- return
881
- }
882
- if (input && !key.ctrl && !key.meta) {
883
- setTextInput((v) => v + input)
884
- return
885
- }
886
- }
887
-
888
- // destination step
889
- if (state.step === 'destination') {
890
- if (input === 'q') {
891
- exit()
892
- return
893
- }
894
- if (key.upArrow || input === 'k') {
895
- setListCursor((c) => Math.max(0, c - 1))
896
- return
897
- }
898
- if (key.downArrow || input === 'j') {
899
- setListCursor((c) => Math.min(1, c + 1))
900
- return
901
- }
902
- if (key.return) {
903
- const destination: PresetCreateDestination = listCursor === 0 ? 'global' : 'project'
904
- setState((s) => advancePresetCreateFlow(s, {
905
- type: 'select-destination',
906
- destination,
907
- }))
908
- return
909
- }
910
- }
911
-
912
- // confirm step
913
- if (state.step === 'confirm') {
914
- if (input === 'q') {
915
- exit()
916
- return
917
- }
918
- if (key.return && state.destination && state.presetName) {
919
- const doneState = advancePresetCreateFlow(state, { type: 'confirm' })
920
- setState(doneState)
921
- void Promise.resolve(
922
- onSubmit({
923
- source: state.source,
924
- filePath: state.filePath,
925
- env: state.env,
926
- selectedKeys: state.selectedKeys,
927
- presetName: state.presetName,
928
- destination: state.destination,
929
- }),
930
- ).finally(() => {
931
- exit()
932
- })
933
- }
934
- }
935
- })
936
-
937
- if (state.step === 'done') {
938
- return (
939
- <Box flexDirection="column">
940
- <Text color="green">Done</Text>
941
- </Box>
942
- )
943
- }
944
-
945
- return (
946
- <Box flexDirection="column">
947
- {state.step === 'source' && <SourceStep cursor={listCursor} />}
948
- {state.step === 'filePath' && (
949
- <FilePathStep value={textInput} error={state.error} />
950
- )}
951
- {state.step === 'keys' && (
952
- <KeysStep keys={allKeys} selectedKeys={state.selectedKeys} cursor={listCursor} />
953
- )}
954
- {state.step === 'manualInput' && (
955
- <ManualInputStep
956
- entries={state.selectedKeys.map((k) => [k, state.env[k] ?? ''] as [string, string])}
957
- value={textInput}
958
- error={state.error}
959
- />
960
- )}
961
- {state.step === 'name' && <NameStep value={textInput} />}
962
- {state.step === 'destination' && <DestinationStep cursor={listCursor} />}
963
- {state.step === 'confirm' && state.destination ? (
964
- <Box flexDirection="column">
965
- <EnvSummary
966
- title={`Preset: ${state.presetName}`}
967
- entries={
968
- Object.entries(state.env)
969
- .filter(([k]) => state.selectedKeys.includes(k))
970
- .sort(([a], [b]) => a.localeCompare(b)) as [string, string][]
971
- }
972
- mask
973
- fromFiles={state.filePath ? [state.filePath] : undefined}
974
- toFiles={[
975
- state.destination === 'global'
976
- ? globalPresetPath(state.presetName)
977
- : projectEnvPath,
978
- ]}
979
- />
980
- <Box marginTop={1}>
981
- <Text dimColor>Press enter to confirm · q to cancel</Text>
982
- </Box>
983
- </Box>
984
- ) : null}
985
- </Box>
986
- )
987
- }
988
- ```
989
-
990
- - [ ] **Step 2: Commit**
991
-
992
- ```bash
993
- git add src/ink/preset-create-app.tsx
994
- git commit -m "refactor: rewrite preset-create-app with full interactive wizard UI"
995
- ```
996
-
997
- ---
998
-
999
- ### Task 4: Rewrite the command function
1000
-
1001
- **Files:**
1002
- - Rewrite: `src/commands/preset/create.ts`
1003
- - Rewrite: `tests/commands/create.test.ts`
1004
-
1005
- - [ ] **Step 1: Rewrite the command function**
1006
-
1007
- Replace the entire content of `src/commands/preset/create.ts`:
1008
-
1009
- ```typescript
1010
- import { readFile } from 'node:fs/promises'
1011
- import { extname } from 'node:path'
1012
-
1013
- import { parse as parseYaml } from 'yaml'
1014
-
1015
- import { CliError } from '../../core/errors.js'
1016
- import { envMapSchema, type EnvMap } from '../../core/schema.js'
1017
- import { toProcessEnvMap } from '../../core/process-env.js'
1018
- import type { PresetCreateAppResult } from '../../ink/preset-create-app.js'
1019
-
1020
- type PresetService = {
1021
- write: (preset: {
1022
- name: string
1023
- createdAt: string
1024
- updatedAt: string
1025
- env: EnvMap
1026
- }) => Promise<unknown>
1027
- }
1028
-
1029
- type ProjectEnvService = {
1030
- write: (env: EnvMap) => Promise<unknown>
1031
- }
1032
-
1033
- export async function readEnvFile(filePath: string): Promise<{ allKeys: string[]; env: EnvMap }> {
1034
- try {
1035
- const content = await readFile(filePath, 'utf8')
1036
- const extension = extname(filePath).toLowerCase()
1037
-
1038
- if (extension !== '.yaml' && extension !== '.yml' && extension !== '.json') {
1039
- throw new CliError(`Unsupported file format: ${extension}`, 2)
1040
- }
1041
-
1042
- const parsed = extension === '.yaml' || extension === '.yml'
1043
- ? parseYaml(content)
1044
- : JSON.parse(content)
1045
-
1046
- const raw = (parsed ?? {}) as Record<string, unknown>
1047
- const source = extension === '.json'
1048
- && raw
1049
- && typeof raw === 'object'
1050
- && 'env' in raw
1051
- && raw.env
1052
- && typeof raw.env === 'object'
1053
- && !Array.isArray(raw.env)
1054
- ? raw.env as Record<string, unknown>
1055
- : raw
1056
-
1057
- const env = toProcessEnvMap(source)
1058
- return {
1059
- allKeys: Object.keys(env),
1060
- env,
1061
- }
1062
- } catch (error) {
1063
- if (error instanceof CliError) throw error
1064
- throw new CliError(`Failed to read env file: ${filePath}`, 2)
1065
- }
1066
- }
1067
-
1068
- export function createPresetCreateCommand({
1069
- presetService,
1070
- projectEnvService,
1071
- renderFlow,
1072
- }: {
1073
- presetService: PresetService
1074
- projectEnvService: ProjectEnvService
1075
- renderFlow: () => Promise<PresetCreateAppResult | void>
1076
- }) {
1077
- return async function createPreset(): Promise<void> {
1078
- const result = await renderFlow()
1079
-
1080
- if (!result) return
1081
-
1082
- const selectedEnv: EnvMap = {}
1083
- for (const key of result.selectedKeys) {
1084
- selectedEnv[key] = result.env[key] ?? ''
1085
- }
1086
-
1087
- const timestamp = new Date().toISOString()
1088
-
1089
- if (result.destination === 'project') {
1090
- await projectEnvService.write(selectedEnv)
1091
- return
1092
- }
1093
-
1094
- await presetService.write({
1095
- name: result.presetName,
1096
- createdAt: timestamp,
1097
- updatedAt: timestamp,
1098
- env: selectedEnv,
1099
- })
1100
- }
1101
- }
1102
- ```
1103
-
1104
- - [ ] **Step 2: Rewrite the command tests**
1105
-
1106
- Replace the entire content of `tests/commands/create.test.ts`:
1107
-
1108
- ```typescript
1109
- import { mkdtemp, rm, writeFile } from 'node:fs/promises'
1110
- import { tmpdir } from 'node:os'
1111
- import { join } from 'node:path'
1112
-
1113
- import { describe, expect, it, vi, afterEach } from 'vitest'
1114
-
1115
- import { createPresetCreateCommand, readEnvFile } from '../../src/commands/preset/create.js'
1116
- import { CliError } from '../../src/core/errors.js'
1117
-
1118
- const tempRoots: string[] = []
1119
-
1120
- async function createTempRoot() {
1121
- const root = await mkdtemp(join(tmpdir(), 'cc-env-create-'))
1122
- tempRoots.push(root)
1123
- return root
1124
- }
1125
-
1126
- afterEach(async () => {
1127
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
1128
- })
1129
-
1130
- describe('readEnvFile', () => {
1131
- it('reads a flat JSON file', async () => {
1132
- const root = await createTempRoot()
1133
- const file = join(root, 'env.json')
1134
- await writeFile(file, JSON.stringify({ API_KEY: 'secret', PORT: '3000' }))
1135
-
1136
- const result = await readEnvFile(file)
1137
- expect(result.allKeys).toEqual(['API_KEY', 'PORT'])
1138
- expect(result.env).toEqual({ API_KEY: 'secret', PORT: '3000' })
1139
- })
1140
-
1141
- it('extracts from nested env field in JSON', async () => {
1142
- const root = await createTempRoot()
1143
- const file = join(root, 'env.json')
1144
- await writeFile(file, JSON.stringify({ env: { API_KEY: 'secret' }, other: true }))
1145
-
1146
- const result = await readEnvFile(file)
1147
- expect(result.allKeys).toEqual(['API_KEY'])
1148
- expect(result.env).toEqual({ API_KEY: 'secret' })
1149
- })
1150
-
1151
- it('falls back to top-level when env is not an object', async () => {
1152
- const root = await createTempRoot()
1153
- const file = join(root, 'env.json')
1154
- await writeFile(file, JSON.stringify({ env: 'not-an-object', API_KEY: 'secret' }))
1155
-
1156
- const result = await readEnvFile(file)
1157
- expect(result.env).toEqual({ API_KEY: 'secret' })
1158
- })
1159
-
1160
- it('reads a YAML file', async () => {
1161
- const root = await createTempRoot()
1162
- const file = join(root, 'env.yaml')
1163
- await writeFile(file, 'API_KEY: secret\nPORT: "3000"\n')
1164
-
1165
- const result = await readEnvFile(file)
1166
- expect(result.allKeys).toEqual(['API_KEY', 'PORT'])
1167
- expect(result.env).toEqual({ API_KEY: 'secret', PORT: '3000' })
1168
- })
1169
-
1170
- it('throws for unsupported file extensions', async () => {
1171
- const root = await createTempRoot()
1172
- const file = join(root, 'env.toml')
1173
- await writeFile(file, 'content')
1174
-
1175
- await expect(readEnvFile(file)).rejects.toThrowError(
1176
- new CliError('Unsupported file format: .toml', 2),
1177
- )
1178
- })
1179
-
1180
- it('throws CliError for unreadable files', async () => {
1181
- await expect(readEnvFile('/nonexistent/file.json')).rejects.toThrowError(
1182
- expect.any(CliError),
1183
- )
1184
- })
1185
-
1186
- it('throws CliError for invalid JSON content', async () => {
1187
- const root = await createTempRoot()
1188
- const file = join(root, 'env.json')
1189
- await writeFile(file, '{invalid')
1190
-
1191
- await expect(readEnvFile(file)).rejects.toThrowError(
1192
- expect.any(CliError),
1193
- )
1194
- })
1195
- })
1196
-
1197
- describe('createPresetCreateCommand', () => {
1198
- it('writes to presetService when destination is global', async () => {
1199
- const presetService = {
1200
- write: vi.fn().mockResolvedValue(undefined),
1201
- }
1202
- const projectEnvService = {
1203
- write: vi.fn().mockResolvedValue(undefined),
1204
- }
1205
- const renderFlow = vi.fn().mockResolvedValue({
1206
- source: 'manual',
1207
- env: { API_KEY: 'secret' },
1208
- selectedKeys: ['API_KEY'],
1209
- presetName: 'my-preset',
1210
- destination: 'global',
1211
- })
1212
-
1213
- const createPreset = createPresetCreateCommand({
1214
- presetService,
1215
- projectEnvService,
1216
- renderFlow,
1217
- })
1218
-
1219
- await createPreset()
1220
-
1221
- expect(presetService.write).toHaveBeenCalledWith({
1222
- name: 'my-preset',
1223
- createdAt: expect.any(String),
1224
- updatedAt: expect.any(String),
1225
- env: { API_KEY: 'secret' },
1226
- })
1227
- expect(projectEnvService.write).not.toHaveBeenCalled()
1228
- })
1229
-
1230
- it('writes to projectEnvService when destination is project', async () => {
1231
- const presetService = {
1232
- write: vi.fn().mockResolvedValue(undefined),
1233
- }
1234
- const projectEnvService = {
1235
- write: vi.fn().mockResolvedValue(undefined),
1236
- }
1237
- const renderFlow = vi.fn().mockResolvedValue({
1238
- source: 'file',
1239
- filePath: '/path/to/env.json',
1240
- env: { API_KEY: 'secret', OTHER: 'value' },
1241
- selectedKeys: ['API_KEY'],
1242
- presetName: 'proj',
1243
- destination: 'project',
1244
- })
1245
-
1246
- const createPreset = createPresetCreateCommand({
1247
- presetService,
1248
- projectEnvService,
1249
- renderFlow,
1250
- })
1251
-
1252
- await createPreset()
1253
-
1254
- expect(projectEnvService.write).toHaveBeenCalledWith({ API_KEY: 'secret' })
1255
- expect(presetService.write).not.toHaveBeenCalled()
1256
- })
1257
-
1258
- it('does nothing when renderFlow returns undefined', async () => {
1259
- const presetService = {
1260
- write: vi.fn().mockResolvedValue(undefined),
1261
- }
1262
- const projectEnvService = {
1263
- write: vi.fn().mockResolvedValue(undefined),
1264
- }
1265
- const renderFlow = vi.fn().mockResolvedValue(undefined)
1266
-
1267
- const createPreset = createPresetCreateCommand({
1268
- presetService,
1269
- projectEnvService,
1270
- renderFlow,
1271
- })
1272
-
1273
- await createPreset()
1274
-
1275
- expect(presetService.write).not.toHaveBeenCalled()
1276
- expect(projectEnvService.write).not.toHaveBeenCalled()
1277
- })
1278
-
1279
- it('only includes selected keys in the written env', async () => {
1280
- const presetService = {
1281
- write: vi.fn().mockResolvedValue(undefined),
1282
- }
1283
- const projectEnvService = {
1284
- write: vi.fn().mockResolvedValue(undefined),
1285
- }
1286
- const renderFlow = vi.fn().mockResolvedValue({
1287
- source: 'file',
1288
- filePath: '/env.json',
1289
- env: { A: '1', B: '2', C: '3' },
1290
- selectedKeys: ['A', 'C'],
1291
- presetName: 'partial',
1292
- destination: 'global',
1293
- })
1294
-
1295
- const createPreset = createPresetCreateCommand({
1296
- presetService,
1297
- projectEnvService,
1298
- renderFlow,
1299
- })
1300
-
1301
- await createPreset()
1302
-
1303
- expect(presetService.write).toHaveBeenCalledWith({
1304
- name: 'partial',
1305
- createdAt: expect.any(String),
1306
- updatedAt: expect.any(String),
1307
- env: { A: '1', C: '3' },
1308
- })
1309
- })
1310
- })
1311
- ```
1312
-
1313
- - [ ] **Step 3: Run all tests**
1314
-
1315
- Run: `npx vitest run tests/commands/create.test.ts`
1316
- Expected: All tests PASS.
1317
-
1318
- - [ ] **Step 4: Commit**
1319
-
1320
- ```bash
1321
- git add src/commands/preset/create.ts tests/commands/create.test.ts
1322
- git commit -m "refactor: simplify preset create command to thin renderFlow wrapper"
1323
- ```
1324
-
1325
- ---
1326
-
1327
- ### Task 5: Update CLI registration
1328
-
1329
- **Files:**
1330
- - Modify: `src/cli.ts:235-266`
1331
-
1332
- - [ ] **Step 1: Update the preset create command registration in cli.ts**
1333
-
1334
- Find the current block (lines 235-266):
1335
-
1336
- ```typescript
1337
- presetCommand.command('create [pairs...]')
1338
- .option('-n, --name <name>')
1339
- .option('-f, --file <path>')
1340
- .option('--project')
1341
- .action((pairs, options) =>
1342
- createPresetCreateCommand({
1343
- presetService,
1344
- projectEnvService,
1345
- renderFlow: async (context) => {
1346
- let result: React.ComponentProps<typeof PresetCreateApp>['onSubmit'] extends (
1347
- result: infer TResult,
1348
- ) => unknown
1349
- ? TResult | undefined
1350
- : undefined
1351
- const app = render(
1352
- h(PresetCreateApp, {
1353
- onSubmit: (value) => {
1354
- result = value
1355
- },
1356
- }),
1357
- )
1358
-
1359
- await app.waitUntilExit()
1360
- return result
1361
- },
1362
- })({
1363
- name: options.name,
1364
- file: options.file,
1365
- pairs,
1366
- project: options.project,
1367
- }),
1368
- )
1369
- ```
1370
-
1371
- Replace with:
1372
-
1373
- ```typescript
1374
- presetCommand.command('create')
1375
- .action(() =>
1376
- createPresetCreateCommand({
1377
- presetService,
1378
- projectEnvService,
1379
- renderFlow: async () => {
1380
- let result: React.ComponentProps<typeof PresetCreateApp>['onSubmit'] extends (
1381
- result: infer TResult,
1382
- ) => unknown
1383
- ? TResult | undefined
1384
- : undefined
1385
- const app = render(
1386
- h(PresetCreateApp, {
1387
- onSubmit: (value) => {
1388
- result = value
1389
- },
1390
- readFile: async (filePath) => {
1391
- const { readEnvFile } = await import('./commands/preset/create.js')
1392
- return readEnvFile(filePath)
1393
- },
1394
- globalPresetPath: (name) => presetService.getPath(name),
1395
- projectEnvPath: join(cwd, '.cc-env', 'env.json'),
1396
- }),
1397
- )
1398
-
1399
- await app.waitUntilExit()
1400
- return result
1401
- },
1402
- })(),
1403
- )
1404
- ```
1405
-
1406
- - [ ] **Step 2: Run all tests**
1407
-
1408
- Run: `npx vitest run`
1409
- Expected: All tests PASS.
1410
-
1411
- - [ ] **Step 3: Commit**
1412
-
1413
- ```bash
1414
- git add src/cli.ts
1415
- git commit -m "refactor: remove preset create CLI flags, wire up full interactive flow"
1416
- ```
1417
-
1418
- ---
1419
-
1420
- ### Task 6: Final verification
1421
-
1422
- - [ ] **Step 1: Run full test suite**
1423
-
1424
- Run: `npx vitest run`
1425
- Expected: All tests PASS.
1426
-
1427
- - [ ] **Step 2: Run TypeScript type check**
1428
-
1429
- Run: `npx tsc --noEmit`
1430
- Expected: No errors.
1431
-
1432
- - [ ] **Step 3: Commit if any fixups were needed**