@lkangd/cc-env 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/dist/cli.js +3 -4
- package/package.json +8 -1
- package/.claude/settings.json +0 -6
- package/.claude/settings.local.json +0 -8
- package/.nvmrc +0 -1
- package/CHANGELOG.md +0 -66
- package/docs/product-specs/index.draft.md +0 -106
- package/docs/product-specs/index.md +0 -911
- package/docs/product-specs/optional.md +0 -42
- package/docs/references/claude-code-env.md +0 -224
- package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +0 -1331
- package/docs/superpowers/plans/2026-04-24-cc-env.md +0 -1666
- package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +0 -1432
- package/docs/superpowers/specs/2026-04-24-cc-env-design.md +0 -438
- package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +0 -181
- package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +0 -78
- package/src/cli.ts +0 -339
- package/src/commands/init.ts +0 -139
- package/src/commands/preset/create.ts +0 -96
- package/src/commands/preset/delete.ts +0 -62
- package/src/commands/preset/show.ts +0 -51
- package/src/commands/restore.ts +0 -150
- package/src/commands/run.ts +0 -158
- package/src/core/errors.ts +0 -13
- package/src/core/find-claude.ts +0 -70
- package/src/core/format.ts +0 -29
- package/src/core/fs.ts +0 -18
- package/src/core/gitignore.ts +0 -26
- package/src/core/logger.ts +0 -11
- package/src/core/mask.ts +0 -17
- package/src/core/paths.ts +0 -41
- package/src/core/process-env.ts +0 -11
- package/src/core/schema.ts +0 -55
- package/src/core/spawn.ts +0 -36
- package/src/flows/init-flow.ts +0 -61
- package/src/flows/preset-create-flow.ts +0 -129
- package/src/flows/restore-flow.ts +0 -144
- package/src/ink/init-app.tsx +0 -110
- package/src/ink/preset-create-app.tsx +0 -451
- package/src/ink/preset-delete-app.tsx +0 -114
- package/src/ink/preset-show-app.tsx +0 -76
- package/src/ink/restore-app.tsx +0 -230
- package/src/ink/run-preset-select-app.tsx +0 -83
- package/src/ink/summary.tsx +0 -91
- package/src/services/claude-settings-env-service.ts +0 -72
- package/src/services/history-service.ts +0 -48
- package/src/services/preset-service.ts +0 -72
- package/src/services/project-env-service.ts +0 -128
- package/src/services/project-state-service.ts +0 -31
- package/src/services/settings-env-service.ts +0 -40
- package/src/services/shell-env-service.ts +0 -112
- package/src/types.d.ts +0 -19
- package/tests/cli/help.test.ts +0 -133
- package/tests/cli/init.test.ts +0 -76
- package/tests/cli/restore.test.ts +0 -172
- package/tests/commands/create.test.ts +0 -263
- package/tests/commands/output.test.ts +0 -119
- package/tests/commands/run.test.ts +0 -218
- package/tests/core/gitignore.test.ts +0 -98
- package/tests/core/paths.test.ts +0 -24
- package/tests/core/schema-mask.test.ts +0 -182
- package/tests/core/spawn.test.ts +0 -47
- package/tests/flows/init-flow.test.ts +0 -40
- package/tests/flows/preset-create-flow.test.ts +0 -225
- package/tests/flows/restore-flow.test.ts +0 -157
- package/tests/integration/init-restore.test.ts +0 -406
- package/tests/services/claude-shell.test.ts +0 -183
- package/tests/services/storage.test.ts +0 -143
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -8
|
@@ -1,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**
|