@lkangd/cc-env 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.claude/settings.json +6 -0
  2. package/.claude/settings.local.json +3 -0
  3. package/.nvmrc +1 -0
  4. package/dist/cli.js +266 -0
  5. package/dist/commands/debug.js +17 -0
  6. package/dist/commands/init.js +64 -0
  7. package/dist/commands/preset/create.js +61 -0
  8. package/dist/commands/preset/delete.js +25 -0
  9. package/dist/commands/preset/edit.js +15 -0
  10. package/dist/commands/preset/list.js +16 -0
  11. package/dist/commands/preset/show.js +16 -0
  12. package/dist/commands/restore.js +65 -0
  13. package/dist/commands/run.js +80 -0
  14. package/dist/core/errors.js +11 -0
  15. package/dist/core/find-claude.js +64 -0
  16. package/dist/core/format.js +23 -0
  17. package/dist/core/fs.js +12 -0
  18. package/dist/core/gitignore.js +23 -0
  19. package/dist/core/lock.js +25 -0
  20. package/dist/core/logger.js +8 -0
  21. package/dist/core/mask.js +13 -0
  22. package/dist/core/paths.js +32 -0
  23. package/dist/core/process-env.js +4 -0
  24. package/dist/core/schema.js +38 -0
  25. package/dist/core/spawn.js +26 -0
  26. package/dist/flows/init-flow.js +35 -0
  27. package/dist/flows/preset-create-flow.js +80 -0
  28. package/dist/flows/restore-flow.js +75 -0
  29. package/dist/ink/init-app.js +54 -0
  30. package/dist/ink/preset-create-app.js +271 -0
  31. package/dist/ink/preset-delete-app.js +47 -0
  32. package/dist/ink/preset-list-app.js +27 -0
  33. package/dist/ink/preset-show-app.js +27 -0
  34. package/dist/ink/restore-app.js +102 -0
  35. package/dist/ink/run-preset-select-app.js +31 -0
  36. package/dist/ink/summary.js +28 -0
  37. package/dist/services/claude-settings-env-service.js +55 -0
  38. package/dist/services/config-service.js +26 -0
  39. package/dist/services/history-service.js +39 -0
  40. package/dist/services/preset-service.js +61 -0
  41. package/dist/services/project-env-service.js +90 -0
  42. package/dist/services/project-state-service.js +26 -0
  43. package/dist/services/runtime-env-service.js +13 -0
  44. package/dist/services/settings-env-service.js +36 -0
  45. package/dist/services/shell-env-service.js +77 -0
  46. package/docs/product-specs/index.draft.md +106 -0
  47. package/docs/product-specs/index.md +911 -0
  48. package/docs/product-specs/optional.md +42 -0
  49. package/docs/references/claude-code-env.md +224 -0
  50. package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +1331 -0
  51. package/docs/superpowers/plans/2026-04-24-cc-env.md +1666 -0
  52. package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +1432 -0
  53. package/docs/superpowers/specs/2026-04-24-cc-env-design.md +438 -0
  54. package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +181 -0
  55. package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +78 -0
  56. package/package.json +55 -0
  57. package/src/cli.ts +337 -0
  58. package/src/commands/init.ts +139 -0
  59. package/src/commands/preset/create.ts +96 -0
  60. package/src/commands/preset/delete.ts +62 -0
  61. package/src/commands/preset/show.ts +51 -0
  62. package/src/commands/restore.ts +150 -0
  63. package/src/commands/run.ts +158 -0
  64. package/src/core/errors.ts +13 -0
  65. package/src/core/find-claude.ts +70 -0
  66. package/src/core/format.ts +29 -0
  67. package/src/core/fs.ts +18 -0
  68. package/src/core/gitignore.ts +26 -0
  69. package/src/core/logger.ts +11 -0
  70. package/src/core/mask.ts +17 -0
  71. package/src/core/paths.ts +41 -0
  72. package/src/core/process-env.ts +11 -0
  73. package/src/core/schema.ts +55 -0
  74. package/src/core/spawn.ts +36 -0
  75. package/src/flows/init-flow.ts +61 -0
  76. package/src/flows/preset-create-flow.ts +129 -0
  77. package/src/flows/restore-flow.ts +144 -0
  78. package/src/ink/init-app.tsx +110 -0
  79. package/src/ink/preset-create-app.tsx +451 -0
  80. package/src/ink/preset-delete-app.tsx +114 -0
  81. package/src/ink/preset-show-app.tsx +76 -0
  82. package/src/ink/restore-app.tsx +230 -0
  83. package/src/ink/run-preset-select-app.tsx +83 -0
  84. package/src/ink/summary.tsx +91 -0
  85. package/src/services/claude-settings-env-service.ts +72 -0
  86. package/src/services/history-service.ts +48 -0
  87. package/src/services/preset-service.ts +72 -0
  88. package/src/services/project-env-service.ts +128 -0
  89. package/src/services/project-state-service.ts +31 -0
  90. package/src/services/settings-env-service.ts +40 -0
  91. package/src/services/shell-env-service.ts +112 -0
  92. package/src/types.d.ts +19 -0
  93. package/tests/cli/help.test.ts +133 -0
  94. package/tests/cli/init.test.ts +76 -0
  95. package/tests/cli/restore.test.ts +172 -0
  96. package/tests/commands/create.test.ts +263 -0
  97. package/tests/commands/output.test.ts +119 -0
  98. package/tests/commands/run.test.ts +218 -0
  99. package/tests/core/gitignore.test.ts +98 -0
  100. package/tests/core/paths.test.ts +24 -0
  101. package/tests/core/schema-mask.test.ts +182 -0
  102. package/tests/core/spawn.test.ts +47 -0
  103. package/tests/flows/init-flow.test.ts +40 -0
  104. package/tests/flows/preset-create-flow.test.ts +225 -0
  105. package/tests/flows/restore-flow.test.ts +157 -0
  106. package/tests/integration/init-restore.test.ts +406 -0
  107. package/tests/services/claude-shell.test.ts +183 -0
  108. package/tests/services/storage.test.ts +143 -0
  109. package/tsconfig.build.json +9 -0
  110. package/tsconfig.json +22 -0
  111. package/vitest.config.ts +8 -0
@@ -0,0 +1,451 @@
1
+ import React, { useState } from 'react'
2
+ import { Box, Text, useApp, useInput } from 'ink'
3
+
4
+ import {
5
+ advancePresetCreateFlow,
6
+ createPresetCreateFlowState,
7
+ type PresetCreateDestination,
8
+ type PresetCreateFlowResult,
9
+ type PresetCreateSource,
10
+ } from '../flows/preset-create-flow.js'
11
+ import type { EnvMap } from '../core/schema.js'
12
+ import { EnvSummary } from './summary.js'
13
+
14
+ export type PresetCreateAppResult = PresetCreateFlowResult & {
15
+ destination: PresetCreateDestination
16
+ }
17
+
18
+ type PresetCreateAppProps = {
19
+ onSubmit: (result: PresetCreateAppResult) => Promise<void> | void
20
+ readFile: (filePath: string) => Promise<{ allKeys: string[]; env: EnvMap }>
21
+ globalPresetPath: (name: string) => string
22
+ projectEnvPath: string
23
+ }
24
+
25
+ function SourceStep({ cursor }: { cursor: number }) {
26
+ const options: { label: string; value: PresetCreateSource }[] = [
27
+ { label: 'File import', value: 'file' },
28
+ { label: 'Manual input', value: 'manual' },
29
+ ]
30
+ return (
31
+ <Box flexDirection="column">
32
+ <Text bold>Select env source</Text>
33
+ <Text dimColor>↑/k ↓/j navigate · enter confirm</Text>
34
+ <Box flexDirection="column" marginTop={1}>
35
+ {options.map((opt, i) => (
36
+ <Box key={opt.value}>
37
+ <Text>{i === cursor ? '❯ ' : ' '}</Text>
38
+ <Text {...(i === cursor ? { color: 'cyan' } : {})}>{opt.label}</Text>
39
+ </Box>
40
+ ))}
41
+ </Box>
42
+ </Box>
43
+ )
44
+ }
45
+
46
+ function FilePathStep({ value, error }: { value: string; error?: string }) {
47
+ return (
48
+ <Box flexDirection="column">
49
+ <Text bold>Enter file path (.yaml/.yml/.json)</Text>
50
+ <Box marginTop={1}>
51
+ <Text dimColor>{'>'} </Text>
52
+ <Text color="cyan">{value}</Text>
53
+ <Text dimColor>█</Text>
54
+ </Box>
55
+ {error ? <Text color="red">{error}</Text> : null}
56
+ </Box>
57
+ )
58
+ }
59
+
60
+ function KeysStep({
61
+ keys,
62
+ selectedKeys,
63
+ cursor,
64
+ }: {
65
+ keys: string[]
66
+ selectedKeys: string[]
67
+ cursor: number
68
+ }) {
69
+ return (
70
+ <Box flexDirection="column">
71
+ <Text bold>Select env keys to import</Text>
72
+ <Text dimColor>↑/k ↓/j navigate · space toggle · enter confirm</Text>
73
+ <Box flexDirection="column" marginTop={1}>
74
+ {keys.map((key, i) => {
75
+ const isSelected = selectedKeys.includes(key)
76
+ return (
77
+ <Box key={key}>
78
+ <Text>{i === cursor ? '❯ ' : ' '}</Text>
79
+ <Text color={isSelected ? 'green' : ''}>{isSelected ? '[x]' : '[ ]'}</Text>
80
+ <Text> {key}</Text>
81
+ </Box>
82
+ )
83
+ })}
84
+ </Box>
85
+ <Box marginTop={1}>
86
+ <Text dimColor>{selectedKeys.length} of {keys.length} selected</Text>
87
+ </Box>
88
+ </Box>
89
+ )
90
+ }
91
+
92
+ function ManualInputStep({
93
+ entries,
94
+ value,
95
+ error,
96
+ }: {
97
+ entries: [string, string][]
98
+ value: string
99
+ error?: string
100
+ }) {
101
+ return (
102
+ <Box flexDirection="column">
103
+ <Text bold>Enter KEY=VALUE pairs (press q when done)</Text>
104
+ {entries.length > 0 ? (
105
+ <Box flexDirection="column" marginBottom={1}>
106
+ {entries.map(([key, val]) => (
107
+ <Box key={key}>
108
+ <Text color="yellow">• </Text>
109
+ <Text color="magenta">{key}</Text>
110
+ <Text dimColor>=</Text>
111
+ <Text>{val}</Text>
112
+ </Box>
113
+ ))}
114
+ </Box>
115
+ ) : null}
116
+ <Box>
117
+ <Text dimColor>{'>'} </Text>
118
+ <Text color="cyan">{value}</Text>
119
+ <Text dimColor>█</Text>
120
+ </Box>
121
+ {error ? <Text color="red">{error}</Text> : null}
122
+ </Box>
123
+ )
124
+ }
125
+
126
+ function NameStep({ value }: { value: string }) {
127
+ return (
128
+ <Box flexDirection="column">
129
+ <Text bold>Enter preset name</Text>
130
+ <Box marginTop={1}>
131
+ <Text dimColor>{'>'} </Text>
132
+ <Text color="cyan">{value}</Text>
133
+ <Text dimColor>█</Text>
134
+ </Box>
135
+ </Box>
136
+ )
137
+ }
138
+
139
+ function DestinationStep({ cursor }: { cursor: number }) {
140
+ const options: { label: string; value: PresetCreateDestination }[] = [
141
+ { label: 'Global preset', value: 'global' },
142
+ { label: 'Project preset', value: 'project' },
143
+ ]
144
+ return (
145
+ <Box flexDirection="column">
146
+ <Text bold>Select save destination</Text>
147
+ <Text dimColor>↑/k ↓/j navigate · enter confirm</Text>
148
+ <Box flexDirection="column" marginTop={1}>
149
+ {options.map((opt, i) => (
150
+ <Box key={opt.value}>
151
+ <Text>{i === cursor ? '❯ ' : ' '}</Text>
152
+ <Text {...(i === cursor ? { color: 'cyan' } : {})}>{opt.label}</Text>
153
+ </Box>
154
+ ))}
155
+ </Box>
156
+ </Box>
157
+ )
158
+ }
159
+
160
+ export function PresetCreateApp({
161
+ onSubmit,
162
+ readFile,
163
+ globalPresetPath,
164
+ projectEnvPath,
165
+ }: PresetCreateAppProps) {
166
+ const { exit } = useApp()
167
+ const [state, setState] = useState(createPresetCreateFlowState)
168
+ const [textInput, setTextInput] = useState('')
169
+ const [listCursor, setListCursor] = useState(0)
170
+ const [allKeys, setAllKeys] = useState<string[]>([])
171
+ const [fileEnv, setFileEnv] = useState<EnvMap>({})
172
+
173
+ useInput((input, key) => {
174
+ if (key.escape) {
175
+ exit()
176
+ return
177
+ }
178
+
179
+ if (state.step === 'source') {
180
+ if (input === 'q') {
181
+ exit()
182
+ return
183
+ }
184
+ if (key.upArrow || input === 'k') {
185
+ setListCursor((c) => Math.max(0, c - 1))
186
+ return
187
+ }
188
+ if (key.downArrow || input === 'j') {
189
+ setListCursor((c) => Math.min(1, c + 1))
190
+ return
191
+ }
192
+ if (key.return) {
193
+ const source: PresetCreateSource = listCursor === 0 ? 'file' : 'manual'
194
+ setState((s) => advancePresetCreateFlow(s, { type: 'select-source', source }))
195
+ setListCursor(0)
196
+ setTextInput('')
197
+ return
198
+ }
199
+ }
200
+
201
+ if (state.step === 'filePath') {
202
+ if (input === 'q') {
203
+ exit()
204
+ return
205
+ }
206
+ if (key.backspace || key.delete) {
207
+ setTextInput((v) => v.slice(0, -1))
208
+ return
209
+ }
210
+ if (key.return) {
211
+ void (async () => {
212
+ try {
213
+ const result = await readFile(textInput)
214
+ if (result.allKeys.length === 0) {
215
+ setState((s) => advancePresetCreateFlow(s, {
216
+ type: 'set-error',
217
+ error: 'No valid env keys found in file',
218
+ }))
219
+ return
220
+ }
221
+ setAllKeys(result.allKeys)
222
+ setFileEnv(result.env)
223
+ setState((s) => advancePresetCreateFlow(s, {
224
+ type: 'set-file-path',
225
+ filePath: textInput,
226
+ }))
227
+ setListCursor(0)
228
+ } catch (err) {
229
+ const message = err instanceof Error ? err.message : 'Failed to read file'
230
+ setState((s) => advancePresetCreateFlow(s, {
231
+ type: 'set-error',
232
+ error: message,
233
+ }))
234
+ }
235
+ })()
236
+ return
237
+ }
238
+ if (input && !key.ctrl && !key.meta) {
239
+ setTextInput((v) => v + input)
240
+ return
241
+ }
242
+ }
243
+
244
+ if (state.step === 'keys') {
245
+ if (input === 'q') {
246
+ exit()
247
+ return
248
+ }
249
+ if (key.upArrow || input === 'k') {
250
+ setListCursor((c) => Math.max(0, c - 1))
251
+ return
252
+ }
253
+ if (key.downArrow || input === 'j') {
254
+ setListCursor((c) => Math.min(allKeys.length - 1, c + 1))
255
+ return
256
+ }
257
+ if (input === ' ') {
258
+ const targetKey = allKeys[listCursor]
259
+ if (targetKey) {
260
+ const newSelected = state.selectedKeys.includes(targetKey)
261
+ ? state.selectedKeys.filter((k) => k !== targetKey)
262
+ : [...state.selectedKeys, targetKey]
263
+ setState((s) => ({ ...s, selectedKeys: newSelected }))
264
+ }
265
+ return
266
+ }
267
+ if (key.return && state.selectedKeys.length > 0) {
268
+ const selectedEnv: EnvMap = {}
269
+ for (const k of state.selectedKeys) {
270
+ selectedEnv[k] = fileEnv[k] ?? ''
271
+ }
272
+ setState((s) => advancePresetCreateFlow(s, {
273
+ type: 'select-keys',
274
+ keys: state.selectedKeys,
275
+ env: selectedEnv,
276
+ }))
277
+ setTextInput('')
278
+ return
279
+ }
280
+ }
281
+
282
+ if (state.step === 'manualInput') {
283
+ if (input === 'q' && textInput === '') {
284
+ if (state.selectedKeys.length === 0) {
285
+ setState((s) => advancePresetCreateFlow(s, {
286
+ type: 'set-error',
287
+ error: 'Add at least one KEY=VALUE pair',
288
+ }))
289
+ return
290
+ }
291
+ setState((s) => advancePresetCreateFlow(s, { type: 'finish-manual-input' }))
292
+ setTextInput('')
293
+ return
294
+ }
295
+ if (key.backspace || key.delete) {
296
+ setTextInput((v) => v.slice(0, -1))
297
+ return
298
+ }
299
+ if (key.return) {
300
+ const separatorIndex = textInput.indexOf('=')
301
+ if (separatorIndex <= 0) {
302
+ setState((s) => advancePresetCreateFlow(s, {
303
+ type: 'set-error',
304
+ error: 'Format must be KEY=VALUE',
305
+ }))
306
+ return
307
+ }
308
+ const k = textInput.slice(0, separatorIndex)
309
+ const v = textInput.slice(separatorIndex + 1)
310
+ if (!/^[A-Z0-9_]+$/.test(k)) {
311
+ setState((s) => advancePresetCreateFlow(s, {
312
+ type: 'set-error',
313
+ error: 'Key must match [A-Z0-9_]+',
314
+ }))
315
+ return
316
+ }
317
+ setState((s) => advancePresetCreateFlow(s, {
318
+ type: 'add-manual-pair',
319
+ key: k,
320
+ value: v,
321
+ }))
322
+ setTextInput('')
323
+ return
324
+ }
325
+ if (input && !key.ctrl && !key.meta) {
326
+ setTextInput((v) => v + input)
327
+ return
328
+ }
329
+ }
330
+
331
+ if (state.step === 'name') {
332
+ if (input === 'q') {
333
+ exit()
334
+ return
335
+ }
336
+ if (key.backspace || key.delete) {
337
+ setTextInput((v) => v.slice(0, -1))
338
+ return
339
+ }
340
+ if (key.return && textInput.trim().length > 0) {
341
+ setState((s) => advancePresetCreateFlow(s, {
342
+ type: 'set-name',
343
+ name: textInput.trim(),
344
+ }))
345
+ setListCursor(0)
346
+ return
347
+ }
348
+ if (input && !key.ctrl && !key.meta) {
349
+ setTextInput((v) => v + input)
350
+ return
351
+ }
352
+ }
353
+
354
+ if (state.step === 'destination') {
355
+ if (input === 'q') {
356
+ exit()
357
+ return
358
+ }
359
+ if (key.upArrow || input === 'k') {
360
+ setListCursor((c) => Math.max(0, c - 1))
361
+ return
362
+ }
363
+ if (key.downArrow || input === 'j') {
364
+ setListCursor((c) => Math.min(1, c + 1))
365
+ return
366
+ }
367
+ if (key.return) {
368
+ const destination: PresetCreateDestination = listCursor === 0 ? 'global' : 'project'
369
+ setState((s) => advancePresetCreateFlow(s, {
370
+ type: 'select-destination',
371
+ destination,
372
+ }))
373
+ return
374
+ }
375
+ }
376
+
377
+ if (state.step === 'confirm') {
378
+ if (input === 'q') {
379
+ exit()
380
+ return
381
+ }
382
+ if (key.return && state.destination && state.presetName) {
383
+ const doneState = advancePresetCreateFlow(state, { type: 'confirm' })
384
+ setState(doneState)
385
+ void Promise.resolve(
386
+ onSubmit({
387
+ source: state.source!,
388
+ filePath: state.filePath,
389
+ env: state.env,
390
+ selectedKeys: state.selectedKeys,
391
+ presetName: state.presetName,
392
+ destination: state.destination,
393
+ }),
394
+ ).finally(() => {
395
+ exit()
396
+ })
397
+ }
398
+ }
399
+ })
400
+
401
+ if (state.step === 'done') {
402
+ return (
403
+ <Box flexDirection="column">
404
+ <Text color="green">Done</Text>
405
+ </Box>
406
+ )
407
+ }
408
+
409
+ return (
410
+ <Box flexDirection="column">
411
+ {state.step === 'source' && <SourceStep cursor={listCursor} />}
412
+ {state.step === 'filePath' && (
413
+ <FilePathStep value={textInput} {...(state.error ? { error: state.error } : {})} />
414
+ )}
415
+ {state.step === 'keys' && (
416
+ <KeysStep keys={allKeys} selectedKeys={state.selectedKeys} cursor={listCursor} />
417
+ )}
418
+ {state.step === 'manualInput' && (
419
+ <ManualInputStep
420
+ entries={state.selectedKeys.map((k) => [k, state.env[k] ?? ''] as [string, string])}
421
+ value={textInput}
422
+ {...(state.error ? { error: state.error } : {})}
423
+ />
424
+ )}
425
+ {state.step === 'name' && <NameStep value={textInput} />}
426
+ {state.step === 'destination' && <DestinationStep cursor={listCursor} />}
427
+ {state.step === 'confirm' && state.destination ? (
428
+ <Box flexDirection="column">
429
+ <EnvSummary
430
+ title={`Preset: ${state.presetName}`}
431
+ entries={
432
+ Object.entries(state.env)
433
+ .filter(([k]) => state.selectedKeys.includes(k))
434
+ .sort(([a], [b]) => a.localeCompare(b)) as [string, string][]
435
+ }
436
+ mask
437
+ {...(state.filePath ? { fromFiles: [state.filePath] } : {})}
438
+ toFiles={[
439
+ state.destination === 'global'
440
+ ? globalPresetPath(state.presetName)
441
+ : projectEnvPath,
442
+ ]}
443
+ />
444
+ <Box marginTop={1}>
445
+ <Text dimColor>Press enter to confirm · q to cancel</Text>
446
+ </Box>
447
+ </Box>
448
+ ) : null}
449
+ </Box>
450
+ )
451
+ }
@@ -0,0 +1,114 @@
1
+ import React, { useMemo, useState } from 'react'
2
+ import { Box, Text, useApp, useInput } from 'ink'
3
+
4
+ import type { EnvMap } from '../core/schema.js'
5
+ import { EnvEntries } from './summary.js'
6
+
7
+ type PresetSource = 'global' | 'project'
8
+
9
+ export type PresetDeleteItem = {
10
+ name: string
11
+ env: EnvMap
12
+ source: PresetSource
13
+ }
14
+
15
+ type DeleteStep = 'browsing' | 'confirming'
16
+
17
+ export function PresetDeleteApp({
18
+ presets,
19
+ onSubmit,
20
+ }: {
21
+ presets: Array<PresetDeleteItem>
22
+ onSubmit: (preset: PresetDeleteItem) => void
23
+ }) {
24
+ const { exit } = useApp()
25
+ const [cursor, setCursor] = useState(0)
26
+ const [step, setStep] = useState<DeleteStep>('browsing')
27
+
28
+ const activePreset = presets[cursor]
29
+
30
+ const entries = useMemo(
31
+ () =>
32
+ activePreset
33
+ ? (Object.entries(activePreset.env).sort(([a], [b]) => a.localeCompare(b)) as [string, string][])
34
+ : [],
35
+ [activePreset],
36
+ )
37
+
38
+ useInput((input, key) => {
39
+ if (step === 'browsing') {
40
+ if (key.escape || input.toLowerCase() === 'q') {
41
+ exit()
42
+ return
43
+ }
44
+
45
+ if (key.upArrow || input === 'k') {
46
+ setCursor((c) => Math.max(0, c - 1))
47
+ return
48
+ }
49
+
50
+ if (key.downArrow || input === 'j') {
51
+ setCursor((c) => Math.min(presets.length - 1, c + 1))
52
+ return
53
+ }
54
+
55
+ if (key.return) {
56
+ setStep('confirming')
57
+ return
58
+ }
59
+ }
60
+
61
+ if (step === 'confirming') {
62
+ if (input.toLowerCase() === 'y') {
63
+ onSubmit(activePreset!)
64
+ exit()
65
+ return
66
+ }
67
+
68
+ if (input.toLowerCase() === 'n' || key.escape) {
69
+ setStep('browsing')
70
+ return
71
+ }
72
+ }
73
+ })
74
+
75
+ return (
76
+ <Box flexDirection="column">
77
+ <Text>Preset delete</Text>
78
+ <Text dimColor>
79
+ {step === 'browsing'
80
+ ? '↑/k ↓/j navigate · Enter select · q exit'
81
+ : 'y confirm · n cancel'}
82
+ </Text>
83
+ <Box marginTop={1}>
84
+ <Box flexDirection="column" width={28} marginRight={2}>
85
+ <Text bold color="cyan">Presets</Text>
86
+ <Box flexDirection="column" marginTop={1}>
87
+ {presets.map((preset, index) => (
88
+ <Box key={`${preset.source}:${preset.name}`}>
89
+ <Text>{index === cursor ? '❯ ' : ' '}</Text>
90
+ <Text {...(preset.source === 'project' ? { color: 'yellow' } : {})}>{preset.name}</Text>
91
+ <Text dimColor> ({preset.source})</Text>
92
+ </Box>
93
+ ))}
94
+ </Box>
95
+ </Box>
96
+ <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="red" paddingX={1}>
97
+ <Text bold color="red">{activePreset!.name}</Text>
98
+ <Text dimColor>{activePreset!.source === 'project' ? 'Project preset' : 'Global preset'}</Text>
99
+ <Box flexDirection="column" marginTop={1}>
100
+ <EnvEntries entries={entries} />
101
+ </Box>
102
+ </Box>
103
+ </Box>
104
+ {step === 'confirming' && (
105
+ <Box marginTop={1}>
106
+ <Text color="red">Delete preset </Text>
107
+ <Text bold>{activePreset!.name}</Text>
108
+ <Text color="red">?</Text>
109
+ <Text dimColor> y/n</Text>
110
+ </Box>
111
+ )}
112
+ </Box>
113
+ )
114
+ }
@@ -0,0 +1,76 @@
1
+ import React, { useMemo, useState } from 'react'
2
+ import { Box, Text, useApp, useInput } from 'ink'
3
+
4
+ import type { EnvMap } from '../core/schema.js'
5
+ import { EnvEntries } from './summary.js'
6
+
7
+ export type PresetSource = 'global' | 'project'
8
+
9
+ export type PresetShowItem = {
10
+ name: string
11
+ env: EnvMap
12
+ source: PresetSource
13
+ }
14
+
15
+ export function PresetShowApp({
16
+ presets,
17
+ }: {
18
+ presets: Array<PresetShowItem>
19
+ }) {
20
+ const { exit } = useApp()
21
+ const [cursor, setCursor] = useState(0)
22
+ const activePreset = presets[cursor]
23
+
24
+ const entries = useMemo(
25
+ () =>
26
+ activePreset
27
+ ? (Object.entries(activePreset.env).sort(([a], [b]) => a.localeCompare(b)) as [string, string][])
28
+ : [],
29
+ [activePreset],
30
+ )
31
+
32
+ useInput((input, key) => {
33
+ if (key.escape || input.toLowerCase() === 'q') {
34
+ exit()
35
+ return
36
+ }
37
+
38
+ if (key.upArrow || input === 'k') {
39
+ setCursor((c) => Math.max(0, c - 1))
40
+ return
41
+ }
42
+
43
+ if (key.downArrow || input === 'j') {
44
+ setCursor((c) => Math.min(presets.length - 1, c + 1))
45
+ return
46
+ }
47
+ })
48
+
49
+ return (
50
+ <Box flexDirection="column">
51
+ <Text>Preset show</Text>
52
+ <Text dimColor>↑/k ↓/j navigate · q exit</Text>
53
+ <Box marginTop={1}>
54
+ <Box flexDirection="column" width={28} marginRight={2}>
55
+ <Text bold color="cyan">Presets</Text>
56
+ <Box flexDirection="column" marginTop={1}>
57
+ {presets.map((preset, index) => (
58
+ <Box key={`${preset.source}:${preset.name}`}>
59
+ <Text>{index === cursor ? '❯ ' : ' '}</Text>
60
+ <Text {...(preset.source === 'project' ? { color: 'yellow' } : {})}>{preset.name}</Text>
61
+ <Text dimColor> ({preset.source})</Text>
62
+ </Box>
63
+ ))}
64
+ </Box>
65
+ </Box>
66
+ <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="green" paddingX={1}>
67
+ <Text bold color="green">{activePreset?.name ?? 'Preview'}</Text>
68
+ <Text dimColor>{activePreset?.source === 'project' ? 'Project preset' : 'Global preset'}</Text>
69
+ <Box flexDirection="column" marginTop={1}>
70
+ <EnvEntries entries={entries} />
71
+ </Box>
72
+ </Box>
73
+ </Box>
74
+ </Box>
75
+ )
76
+ }