@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.
- package/.claude/settings.json +6 -0
- package/.claude/settings.local.json +3 -0
- package/.nvmrc +1 -0
- package/dist/cli.js +266 -0
- package/dist/commands/debug.js +17 -0
- package/dist/commands/init.js +64 -0
- package/dist/commands/preset/create.js +61 -0
- package/dist/commands/preset/delete.js +25 -0
- package/dist/commands/preset/edit.js +15 -0
- package/dist/commands/preset/list.js +16 -0
- package/dist/commands/preset/show.js +16 -0
- package/dist/commands/restore.js +65 -0
- package/dist/commands/run.js +80 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/find-claude.js +64 -0
- package/dist/core/format.js +23 -0
- package/dist/core/fs.js +12 -0
- package/dist/core/gitignore.js +23 -0
- package/dist/core/lock.js +25 -0
- package/dist/core/logger.js +8 -0
- package/dist/core/mask.js +13 -0
- package/dist/core/paths.js +32 -0
- package/dist/core/process-env.js +4 -0
- package/dist/core/schema.js +38 -0
- package/dist/core/spawn.js +26 -0
- package/dist/flows/init-flow.js +35 -0
- package/dist/flows/preset-create-flow.js +80 -0
- package/dist/flows/restore-flow.js +75 -0
- package/dist/ink/init-app.js +54 -0
- package/dist/ink/preset-create-app.js +271 -0
- package/dist/ink/preset-delete-app.js +47 -0
- package/dist/ink/preset-list-app.js +27 -0
- package/dist/ink/preset-show-app.js +27 -0
- package/dist/ink/restore-app.js +102 -0
- package/dist/ink/run-preset-select-app.js +31 -0
- package/dist/ink/summary.js +28 -0
- package/dist/services/claude-settings-env-service.js +55 -0
- package/dist/services/config-service.js +26 -0
- package/dist/services/history-service.js +39 -0
- package/dist/services/preset-service.js +61 -0
- package/dist/services/project-env-service.js +90 -0
- package/dist/services/project-state-service.js +26 -0
- package/dist/services/runtime-env-service.js +13 -0
- package/dist/services/settings-env-service.js +36 -0
- package/dist/services/shell-env-service.js +77 -0
- package/docs/product-specs/index.draft.md +106 -0
- package/docs/product-specs/index.md +911 -0
- package/docs/product-specs/optional.md +42 -0
- package/docs/references/claude-code-env.md +224 -0
- package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +1331 -0
- package/docs/superpowers/plans/2026-04-24-cc-env.md +1666 -0
- package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +1432 -0
- package/docs/superpowers/specs/2026-04-24-cc-env-design.md +438 -0
- package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +181 -0
- package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +78 -0
- package/package.json +55 -0
- package/src/cli.ts +337 -0
- package/src/commands/init.ts +139 -0
- package/src/commands/preset/create.ts +96 -0
- package/src/commands/preset/delete.ts +62 -0
- package/src/commands/preset/show.ts +51 -0
- package/src/commands/restore.ts +150 -0
- package/src/commands/run.ts +158 -0
- package/src/core/errors.ts +13 -0
- package/src/core/find-claude.ts +70 -0
- package/src/core/format.ts +29 -0
- package/src/core/fs.ts +18 -0
- package/src/core/gitignore.ts +26 -0
- package/src/core/logger.ts +11 -0
- package/src/core/mask.ts +17 -0
- package/src/core/paths.ts +41 -0
- package/src/core/process-env.ts +11 -0
- package/src/core/schema.ts +55 -0
- package/src/core/spawn.ts +36 -0
- package/src/flows/init-flow.ts +61 -0
- package/src/flows/preset-create-flow.ts +129 -0
- package/src/flows/restore-flow.ts +144 -0
- package/src/ink/init-app.tsx +110 -0
- package/src/ink/preset-create-app.tsx +451 -0
- package/src/ink/preset-delete-app.tsx +114 -0
- package/src/ink/preset-show-app.tsx +76 -0
- package/src/ink/restore-app.tsx +230 -0
- package/src/ink/run-preset-select-app.tsx +83 -0
- package/src/ink/summary.tsx +91 -0
- package/src/services/claude-settings-env-service.ts +72 -0
- package/src/services/history-service.ts +48 -0
- package/src/services/preset-service.ts +72 -0
- package/src/services/project-env-service.ts +128 -0
- package/src/services/project-state-service.ts +31 -0
- package/src/services/settings-env-service.ts +40 -0
- package/src/services/shell-env-service.ts +112 -0
- package/src/types.d.ts +19 -0
- package/tests/cli/help.test.ts +133 -0
- package/tests/cli/init.test.ts +76 -0
- package/tests/cli/restore.test.ts +172 -0
- package/tests/commands/create.test.ts +263 -0
- package/tests/commands/output.test.ts +119 -0
- package/tests/commands/run.test.ts +218 -0
- package/tests/core/gitignore.test.ts +98 -0
- package/tests/core/paths.test.ts +24 -0
- package/tests/core/schema-mask.test.ts +182 -0
- package/tests/core/spawn.test.ts +47 -0
- package/tests/flows/init-flow.test.ts +40 -0
- package/tests/flows/preset-create-flow.test.ts +225 -0
- package/tests/flows/restore-flow.test.ts +157 -0
- package/tests/integration/init-restore.test.ts +406 -0
- package/tests/services/claude-shell.test.ts +183 -0
- package/tests/services/storage.test.ts +143 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +22 -0
- 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
|
+
}
|