@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
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lkangd/cc-env",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"homepage": "https://github.com/lkangd/cc-env#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/lkangd/cc-env/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/lkangd/cc-env.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20.19.2 <21"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"cc-env": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.build.json",
|
|
27
|
+
"dev": "tsx src/cli.ts",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@inkjs/ui": "^2.0.0",
|
|
32
|
+
"commander": "^14.0.0",
|
|
33
|
+
"cross-spawn": "^7.0.6",
|
|
34
|
+
"figlet": "^1.11.0",
|
|
35
|
+
"fs-extra": "^11.3.2",
|
|
36
|
+
"gradient-string": "^3.0.0",
|
|
37
|
+
"ink": "^6.3.1",
|
|
38
|
+
"pino": "^10.0.0",
|
|
39
|
+
"react": "^19.1.0",
|
|
40
|
+
"yaml": "^2.8.1",
|
|
41
|
+
"zod": "^4.1.5"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/figlet": "^1.7.0",
|
|
45
|
+
"@types/fs-extra": "^11.0.4",
|
|
46
|
+
"@types/gradient-string": "^1.1.6",
|
|
47
|
+
"@types/node": "^20.19.0",
|
|
48
|
+
"@types/react": "^19.1.10",
|
|
49
|
+
"execa": "^9.6.0",
|
|
50
|
+
"react-test-renderer": "^19.2.5",
|
|
51
|
+
"tsx": "^4.20.3",
|
|
52
|
+
"typescript": "^5.9.2",
|
|
53
|
+
"vitest": "^3.2.4"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render } from 'ink'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import figlet from 'figlet'
|
|
5
|
+
import gradient from 'gradient-string'
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander'
|
|
8
|
+
|
|
9
|
+
const h = React.createElement
|
|
10
|
+
|
|
11
|
+
import { createInitCommand } from './commands/init.js'
|
|
12
|
+
import { createPresetCreateCommand } from './commands/preset/create.js'
|
|
13
|
+
import { createDeletePresetCommand } from './commands/preset/delete.js'
|
|
14
|
+
import { PresetDeleteApp } from './ink/preset-delete-app.js'
|
|
15
|
+
import { createShowPresetsCommand } from './commands/preset/show.js'
|
|
16
|
+
import { createRestoreCommand } from './commands/restore.js'
|
|
17
|
+
import { createRunCommand } from './commands/run.js'
|
|
18
|
+
import { findClaudeExecutable } from './core/find-claude.js'
|
|
19
|
+
import { InitApp } from './ink/init-app.js'
|
|
20
|
+
import { renderEnvSummary } from './ink/summary.js'
|
|
21
|
+
import { PresetCreateApp } from './ink/preset-create-app.js'
|
|
22
|
+
import { PresetShowApp } from './ink/preset-show-app.js'
|
|
23
|
+
import { RunPresetSelectApp } from './ink/run-preset-select-app.js'
|
|
24
|
+
import { advanceRestoreFlow, createRestoreFlowState } from './flows/restore-flow.js'
|
|
25
|
+
import { RestoreApp } from './ink/restore-app.js'
|
|
26
|
+
import { CliError } from './core/errors.js'
|
|
27
|
+
import { resolveGlobalRoot } from './core/paths.js'
|
|
28
|
+
import { spawnCommand } from './core/spawn.js'
|
|
29
|
+
import { createClaudeSettingsEnvService } from './services/claude-settings-env-service.js'
|
|
30
|
+
import { createHistoryService } from './services/history-service.js'
|
|
31
|
+
import { createPresetService } from './services/preset-service.js'
|
|
32
|
+
import { createProjectEnvService } from './services/project-env-service.js'
|
|
33
|
+
import { createProjectStateService } from './services/project-state-service.js'
|
|
34
|
+
import { createSettingsEnvService } from './services/settings-env-service.js'
|
|
35
|
+
import { createShellEnvService } from './services/shell-env-service.js'
|
|
36
|
+
|
|
37
|
+
const program = new Command()
|
|
38
|
+
|
|
39
|
+
program.name('cc-env').description('Manage runtime environment variables for Claude Code')
|
|
40
|
+
|
|
41
|
+
const homeDir = process.env.HOME ?? process.cwd()
|
|
42
|
+
const cwd = process.cwd()
|
|
43
|
+
const settingsPath = join(cwd, 'settings.json')
|
|
44
|
+
const globalRoot = resolveGlobalRoot()
|
|
45
|
+
|
|
46
|
+
const claudeSettingsEnvService = createClaudeSettingsEnvService({ homeDir, cwd })
|
|
47
|
+
const settingsEnvService = createSettingsEnvService({ settingsPath })
|
|
48
|
+
const shellEnvService = createShellEnvService({ homeDir })
|
|
49
|
+
const projectEnvService = createProjectEnvService({ cwd })
|
|
50
|
+
const presetService = createPresetService(globalRoot)
|
|
51
|
+
const historyService = createHistoryService(globalRoot)
|
|
52
|
+
|
|
53
|
+
async function runRestoreFlow(context: { records: Awaited<ReturnType<typeof historyService.list>>; yes: boolean }) {
|
|
54
|
+
const state = createRestoreFlowState(context.records)
|
|
55
|
+
const firstRecord = context.records[0]
|
|
56
|
+
|
|
57
|
+
if (!firstRecord) {
|
|
58
|
+
render(h(RestoreApp, { state }))
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (context.yes) {
|
|
63
|
+
const selectedRecordState = advanceRestoreFlow(state, {
|
|
64
|
+
type: 'select-record',
|
|
65
|
+
timestamp: firstRecord.timestamp
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (firstRecord.action === 'init') {
|
|
69
|
+
const doneState = advanceRestoreFlow(selectedRecordState, { type: 'confirm' })
|
|
70
|
+
if (doneState.step !== 'done') {
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
confirmed: true,
|
|
76
|
+
timestamp: firstRecord.timestamp
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const confirmState = advanceRestoreFlow(selectedRecordState, {
|
|
81
|
+
type: 'select-target',
|
|
82
|
+
targetType: firstRecord.targetType,
|
|
83
|
+
...(firstRecord.targetType === 'preset' ? { targetName: firstRecord.targetName } : {})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const doneState = advanceRestoreFlow(confirmState, { type: 'confirm' })
|
|
87
|
+
|
|
88
|
+
if (doneState.step === 'done' && doneState.targetType === 'preset') {
|
|
89
|
+
return {
|
|
90
|
+
confirmed: true,
|
|
91
|
+
timestamp: doneState.selectedTimestamp,
|
|
92
|
+
targetType: doneState.targetType,
|
|
93
|
+
targetName: doneState.targetName
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (doneState.step === 'done') {
|
|
98
|
+
return {
|
|
99
|
+
confirmed: true,
|
|
100
|
+
timestamp: doneState.selectedTimestamp,
|
|
101
|
+
targetType: doneState.targetType
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let result:
|
|
109
|
+
| {
|
|
110
|
+
confirmed: boolean
|
|
111
|
+
timestamp?: string
|
|
112
|
+
targetType?: 'settings' | 'preset'
|
|
113
|
+
targetName?: string
|
|
114
|
+
}
|
|
115
|
+
| undefined
|
|
116
|
+
|
|
117
|
+
const app = render(
|
|
118
|
+
h(RestoreApp, {
|
|
119
|
+
state,
|
|
120
|
+
onSubmit: value => {
|
|
121
|
+
result = value
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
await app.waitUntilExit()
|
|
127
|
+
return result
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
program.exitOverride().configureOutput({
|
|
131
|
+
writeErr: str => {
|
|
132
|
+
if (!str.startsWith('error:')) {
|
|
133
|
+
process.stderr.write(str)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
program
|
|
139
|
+
.command('run [args...]')
|
|
140
|
+
.allowUnknownOption(true)
|
|
141
|
+
.description('Run claude with merged environment variables')
|
|
142
|
+
.option('--dry-run', 'Preview the merged env without executing')
|
|
143
|
+
.option('-y, --yes', 'Auto-select the default preset without interactive prompts')
|
|
144
|
+
.action((args, options) => {
|
|
145
|
+
const rawArgs = args ?? []
|
|
146
|
+
|
|
147
|
+
return createRunCommand({
|
|
148
|
+
claudeSettingsEnvService,
|
|
149
|
+
presetService,
|
|
150
|
+
projectEnvService,
|
|
151
|
+
projectStateService: createProjectStateService(globalRoot),
|
|
152
|
+
findClaude: findClaudeExecutable,
|
|
153
|
+
renderPresetSelect: async ({ presets, defaultIndex }) => {
|
|
154
|
+
let result: (typeof presets)[number] | undefined
|
|
155
|
+
const app = render(
|
|
156
|
+
h(RunPresetSelectApp, {
|
|
157
|
+
presets,
|
|
158
|
+
defaultIndex,
|
|
159
|
+
onSubmit: preset => {
|
|
160
|
+
result = preset
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
)
|
|
164
|
+
await app.waitUntilExit()
|
|
165
|
+
return result
|
|
166
|
+
},
|
|
167
|
+
spawnCommand
|
|
168
|
+
})({
|
|
169
|
+
args: rawArgs,
|
|
170
|
+
dryRun: options.dryRun ?? false,
|
|
171
|
+
yes: options.yes ?? false,
|
|
172
|
+
cwd
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
program
|
|
177
|
+
.command('init')
|
|
178
|
+
.description('Initialize cc-env for the current project')
|
|
179
|
+
.option('-y, --yes', 'Accept all defaults without interactive prompts')
|
|
180
|
+
.action(options =>
|
|
181
|
+
createInitCommand({
|
|
182
|
+
claudeSettingsEnvService,
|
|
183
|
+
shellEnvService,
|
|
184
|
+
historyService,
|
|
185
|
+
renderEnvSummary,
|
|
186
|
+
renderFlow: async context => {
|
|
187
|
+
if (context.yes) {
|
|
188
|
+
return {
|
|
189
|
+
selectedKeys: context.requiredKeys,
|
|
190
|
+
confirmed: true
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let result:
|
|
195
|
+
| {
|
|
196
|
+
selectedKeys: string[]
|
|
197
|
+
confirmed: boolean
|
|
198
|
+
}
|
|
199
|
+
| undefined
|
|
200
|
+
|
|
201
|
+
const app = render(
|
|
202
|
+
h(InitApp, {
|
|
203
|
+
...context,
|
|
204
|
+
onSubmit: value => {
|
|
205
|
+
result = value
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
await app.waitUntilExit()
|
|
211
|
+
return result
|
|
212
|
+
}
|
|
213
|
+
})({
|
|
214
|
+
yes: options.yes
|
|
215
|
+
})
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
program
|
|
219
|
+
.command('restore')
|
|
220
|
+
.description('Restore environment variables from a previous snapshot')
|
|
221
|
+
.option('-y, --yes', 'Accept all defaults without interactive prompts')
|
|
222
|
+
.action(options =>
|
|
223
|
+
createRestoreCommand({
|
|
224
|
+
historyService,
|
|
225
|
+
claudeSettingsEnvService,
|
|
226
|
+
shellEnvService,
|
|
227
|
+
settingsEnvService,
|
|
228
|
+
presetService,
|
|
229
|
+
renderEnvSummary: renderEnvSummary,
|
|
230
|
+
renderFlow: context => runRestoreFlow(context)
|
|
231
|
+
})({
|
|
232
|
+
yes: options.yes
|
|
233
|
+
})
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
const presetCommand = program.command('preset').description('Manage environment presets')
|
|
237
|
+
presetCommand
|
|
238
|
+
.command('show')
|
|
239
|
+
.description('List and view all presets')
|
|
240
|
+
.action(
|
|
241
|
+
createShowPresetsCommand({
|
|
242
|
+
presetService,
|
|
243
|
+
projectEnvService,
|
|
244
|
+
renderShow: async presets => {
|
|
245
|
+
const app = render(h(PresetShowApp, { presets }))
|
|
246
|
+
await app.waitUntilExit()
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
)
|
|
250
|
+
presetCommand
|
|
251
|
+
.command('delete')
|
|
252
|
+
.description('Delete a saved preset')
|
|
253
|
+
.action(
|
|
254
|
+
createDeletePresetCommand({
|
|
255
|
+
presetService,
|
|
256
|
+
projectEnvService,
|
|
257
|
+
renderDelete: async presets => {
|
|
258
|
+
let result: (typeof presets)[number] | undefined
|
|
259
|
+
const app = render(
|
|
260
|
+
h(PresetDeleteApp, {
|
|
261
|
+
presets,
|
|
262
|
+
onSubmit: preset => {
|
|
263
|
+
result = preset
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
)
|
|
267
|
+
await app.waitUntilExit()
|
|
268
|
+
return result
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
)
|
|
272
|
+
presetCommand
|
|
273
|
+
.command('create')
|
|
274
|
+
.description('Create a new environment preset')
|
|
275
|
+
.action(() =>
|
|
276
|
+
createPresetCreateCommand({
|
|
277
|
+
presetService,
|
|
278
|
+
projectEnvService,
|
|
279
|
+
renderFlow: async () => {
|
|
280
|
+
let result: React.ComponentProps<typeof PresetCreateApp>['onSubmit'] extends (result: infer TResult) => unknown
|
|
281
|
+
? TResult | undefined
|
|
282
|
+
: undefined
|
|
283
|
+
const app = render(
|
|
284
|
+
h(PresetCreateApp, {
|
|
285
|
+
onSubmit: value => {
|
|
286
|
+
result = value
|
|
287
|
+
},
|
|
288
|
+
readFile: async filePath => {
|
|
289
|
+
const { readEnvFile } = await import('./commands/preset/create.js')
|
|
290
|
+
return readEnvFile(filePath)
|
|
291
|
+
},
|
|
292
|
+
globalPresetPath: name => presetService.getPath(name),
|
|
293
|
+
projectEnvPath: join(cwd, '.cc-env', 'env.json')
|
|
294
|
+
})
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
await app.waitUntilExit()
|
|
298
|
+
return result
|
|
299
|
+
}
|
|
300
|
+
})({ cwd })
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
function printBanner() {
|
|
304
|
+
const banner = figlet.textSync('CC ENV', { font: 'ANSI Shadow' })
|
|
305
|
+
const line = '─'.repeat(48)
|
|
306
|
+
const styled = gradient(['#00d2ff', '#7b2ff7', '#ff0080'])(banner)
|
|
307
|
+
process.stderr.write(`\n${styled}\x1b[2m\n${line}\x1b[0m\n\n`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
program.hook('preAction', () => {
|
|
311
|
+
printBanner()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
program.parseAsync(process.argv).catch((error: unknown) => {
|
|
315
|
+
if (error instanceof CliError) {
|
|
316
|
+
process.stderr.write(`\n Error: ${error.message}\n\n`)
|
|
317
|
+
process.exitCode = error.exitCode
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
322
|
+
const { code, message } = error as { code?: string; message?: string }
|
|
323
|
+
|
|
324
|
+
if (code === 'commander.helpDisplayed') {
|
|
325
|
+
process.exitCode = 0
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const hint = ` Run "cc-env --help" to see available commands and options.\n`
|
|
330
|
+
const formatted = message?.replace(/^error:\s*/i, '') ?? 'Unknown error'
|
|
331
|
+
process.stderr.write(`\n Error: ${formatted}\n\n${hint}\n`)
|
|
332
|
+
process.exitCode = 1
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
throw error
|
|
337
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
|
|
4
|
+
import { CliError } from '../core/errors.js'
|
|
5
|
+
import { envMapSchema, type EnvMap, type InitHistoryRecord, type SourceEntry } from '../core/schema.js'
|
|
6
|
+
import type { ClaudeSettingsSource } from '../services/claude-settings-env-service.js'
|
|
7
|
+
import type { ShellWriteRecord } from '../services/shell-env-service.js'
|
|
8
|
+
|
|
9
|
+
const h = React.createElement
|
|
10
|
+
|
|
11
|
+
const requiredInitKeys = [
|
|
12
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
13
|
+
'ANTHROPIC_BASE_URL',
|
|
14
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
15
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
16
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
17
|
+
'ANTHROPIC_REASONING_MODEL',
|
|
18
|
+
] as const
|
|
19
|
+
|
|
20
|
+
type ClaudeSettingsEnvService = {
|
|
21
|
+
read: () => Promise<ClaudeSettingsSource[]>
|
|
22
|
+
write: (sources: Array<{ path: string; env: EnvMap }>) => Promise<void>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ShellEnvService = {
|
|
26
|
+
write: (env: EnvMap) => Promise<ShellWriteRecord[]>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type HistoryService = {
|
|
30
|
+
write: (record: InitHistoryRecord) => Promise<unknown>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type InitFlowResult = {
|
|
34
|
+
selectedKeys: string[]
|
|
35
|
+
confirmed?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function omitKeys(env: EnvMap, keys: string[]): EnvMap {
|
|
39
|
+
return envMapSchema.parse(
|
|
40
|
+
Object.fromEntries(Object.entries(env).filter(([key]) => !keys.includes(key))),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createInitCommand({
|
|
45
|
+
claudeSettingsEnvService,
|
|
46
|
+
shellEnvService,
|
|
47
|
+
historyService,
|
|
48
|
+
renderFlow,
|
|
49
|
+
renderEnvSummary,
|
|
50
|
+
}: {
|
|
51
|
+
claudeSettingsEnvService: ClaudeSettingsEnvService
|
|
52
|
+
shellEnvService: ShellEnvService
|
|
53
|
+
historyService: HistoryService
|
|
54
|
+
renderFlow: (context: {
|
|
55
|
+
keys: string[]
|
|
56
|
+
requiredKeys: string[]
|
|
57
|
+
yes: boolean
|
|
58
|
+
sourceFiles: string[]
|
|
59
|
+
}) => Promise<InitFlowResult | void> | InitFlowResult | void
|
|
60
|
+
renderEnvSummary: (props: {
|
|
61
|
+
title: string
|
|
62
|
+
env: EnvMap
|
|
63
|
+
fromFiles?: string[]
|
|
64
|
+
toFiles?: string[]
|
|
65
|
+
footer?: React.ReactNode
|
|
66
|
+
}) => Promise<void>
|
|
67
|
+
}) {
|
|
68
|
+
return async function init({ yes = false }: { yes?: boolean } = {}): Promise<void> {
|
|
69
|
+
const sources = await claudeSettingsEnvService.read()
|
|
70
|
+
|
|
71
|
+
if (sources.every((s) => !s.exists)) {
|
|
72
|
+
throw new CliError('No Claude settings files were found')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const effectiveEnv = envMapSchema.parse(
|
|
76
|
+
sources.reduce<Record<string, unknown>>((acc, source) => ({ ...acc, ...source.env }), {}),
|
|
77
|
+
)
|
|
78
|
+
const keys = Object.keys(effectiveEnv).sort()
|
|
79
|
+
const requiredKeys = requiredInitKeys.filter((key) => key in effectiveEnv)
|
|
80
|
+
const sourceFiles = sources.map((s) => s.path)
|
|
81
|
+
const result = await renderFlow({ keys, requiredKeys, yes, sourceFiles })
|
|
82
|
+
|
|
83
|
+
if (!result?.confirmed) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const migratedEnv = envMapSchema.parse(
|
|
88
|
+
Object.fromEntries(
|
|
89
|
+
result.selectedKeys
|
|
90
|
+
.filter((key) => key in effectiveEnv)
|
|
91
|
+
.map((key) => [key, effectiveEnv[key]]),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if (Object.keys(migratedEnv).length === 0) {
|
|
96
|
+
throw new CliError('No selected env values found to migrate')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const initSources: SourceEntry[] = sources.map((source) => ({
|
|
100
|
+
file: source.path,
|
|
101
|
+
backup: envMapSchema.parse(
|
|
102
|
+
Object.fromEntries(
|
|
103
|
+
result.selectedKeys
|
|
104
|
+
.filter((key) => key in source.env)
|
|
105
|
+
.map((key) => [key, source.env[key]]),
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
}))
|
|
109
|
+
|
|
110
|
+
const timestamp = new Date().toISOString()
|
|
111
|
+
const shellWrites = await shellEnvService.write(migratedEnv)
|
|
112
|
+
|
|
113
|
+
await historyService.write({
|
|
114
|
+
timestamp,
|
|
115
|
+
action: 'init',
|
|
116
|
+
migratedKeys: result.selectedKeys,
|
|
117
|
+
sources: initSources,
|
|
118
|
+
shellWrites,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
await claudeSettingsEnvService.write(
|
|
122
|
+
sources.map((source) => ({
|
|
123
|
+
path: source.path,
|
|
124
|
+
env: omitKeys(source.env, result.selectedKeys),
|
|
125
|
+
})),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
await renderEnvSummary({
|
|
129
|
+
title: 'Migrated',
|
|
130
|
+
env: migratedEnv,
|
|
131
|
+
fromFiles: initSources.map((s) => s.file),
|
|
132
|
+
toFiles: shellWrites.map((sw) => sw.filePath),
|
|
133
|
+
footer: h(Box, { flexDirection: 'column' },
|
|
134
|
+
h(Text, { color: 'green' }, 'Init complete'),
|
|
135
|
+
h(Text, { bold: true, color: 'green' }, 'Please restart your terminal for the migrated environment variables to take effect.'),
|
|
136
|
+
),
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { extname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { parse as parseYaml } from 'yaml'
|
|
5
|
+
|
|
6
|
+
import { CliError } from '../../core/errors.js'
|
|
7
|
+
import { ensureGitignoreEntry } from '../../core/gitignore.js'
|
|
8
|
+
import { type EnvMap } from '../../core/schema.js'
|
|
9
|
+
import { toProcessEnvMap } from '../../core/process-env.js'
|
|
10
|
+
import type { PresetCreateAppResult } from '../../ink/preset-create-app.js'
|
|
11
|
+
|
|
12
|
+
type PresetService = {
|
|
13
|
+
write: (preset: {
|
|
14
|
+
name: string
|
|
15
|
+
createdAt: string
|
|
16
|
+
updatedAt: string
|
|
17
|
+
env: EnvMap
|
|
18
|
+
}) => Promise<unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ProjectEnvService = {
|
|
22
|
+
write: (env: EnvMap, meta?: { name?: string; createdAt?: string; updatedAt?: string }) => Promise<unknown>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function readEnvFile(filePath: string): Promise<{ allKeys: string[]; env: EnvMap }> {
|
|
26
|
+
try {
|
|
27
|
+
const content = await readFile(filePath, 'utf8')
|
|
28
|
+
const extension = extname(filePath).toLowerCase()
|
|
29
|
+
|
|
30
|
+
if (extension !== '.yaml' && extension !== '.yml' && extension !== '.json') {
|
|
31
|
+
throw new CliError(`Unsupported file format: ${extension}`, 2)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parsed = extension === '.yaml' || extension === '.yml'
|
|
35
|
+
? parseYaml(content)
|
|
36
|
+
: JSON.parse(content)
|
|
37
|
+
|
|
38
|
+
const raw = (parsed ?? {}) as Record<string, unknown>
|
|
39
|
+
const source = extension === '.json'
|
|
40
|
+
&& raw
|
|
41
|
+
&& typeof raw === 'object'
|
|
42
|
+
&& 'env' in raw
|
|
43
|
+
&& raw.env
|
|
44
|
+
&& typeof raw.env === 'object'
|
|
45
|
+
&& !Array.isArray(raw.env)
|
|
46
|
+
? raw.env as Record<string, unknown>
|
|
47
|
+
: raw
|
|
48
|
+
|
|
49
|
+
const env = toProcessEnvMap(source)
|
|
50
|
+
return {
|
|
51
|
+
allKeys: Object.keys(env),
|
|
52
|
+
env,
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof CliError) throw error
|
|
56
|
+
throw new CliError(`Failed to read env file: ${filePath}`, 2)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createPresetCreateCommand({
|
|
61
|
+
presetService,
|
|
62
|
+
projectEnvService,
|
|
63
|
+
renderFlow,
|
|
64
|
+
ensureGitignore = (dir, entry) => ensureGitignoreEntry(dir, entry),
|
|
65
|
+
}: {
|
|
66
|
+
presetService: PresetService
|
|
67
|
+
projectEnvService: ProjectEnvService
|
|
68
|
+
renderFlow: () => Promise<PresetCreateAppResult | void>
|
|
69
|
+
ensureGitignore?: (dir: string, entry: string) => Promise<void>
|
|
70
|
+
}) {
|
|
71
|
+
return async function createPreset({ cwd }: { cwd: string }): Promise<void> {
|
|
72
|
+
const result = await renderFlow()
|
|
73
|
+
|
|
74
|
+
if (!result) return
|
|
75
|
+
|
|
76
|
+
const selectedEnv: EnvMap = {}
|
|
77
|
+
for (const key of result.selectedKeys) {
|
|
78
|
+
selectedEnv[key] = result.env[key] ?? ''
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const timestamp = new Date().toISOString()
|
|
82
|
+
|
|
83
|
+
if (result.destination === 'project') {
|
|
84
|
+
await projectEnvService.write(selectedEnv, { name: result.presetName, createdAt: timestamp, updatedAt: timestamp })
|
|
85
|
+
await ensureGitignore(cwd, '.cc-env')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await presetService.write({
|
|
90
|
+
name: result.presetName,
|
|
91
|
+
createdAt: timestamp,
|
|
92
|
+
updatedAt: timestamp,
|
|
93
|
+
env: selectedEnv,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EnvMap } from '../../core/schema.js'
|
|
2
|
+
|
|
3
|
+
export type PresetSource = 'global' | 'project'
|
|
4
|
+
|
|
5
|
+
type PresetService = {
|
|
6
|
+
listNames: () => Promise<string[]>
|
|
7
|
+
read: (name: string) => Promise<{ env: EnvMap }>
|
|
8
|
+
remove: (name: string) => Promise<void>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type ProjectEnvService = {
|
|
12
|
+
readWithMeta: () => Promise<{ env: EnvMap; name?: string | undefined }>
|
|
13
|
+
write: (env: EnvMap) => Promise<EnvMap>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PresetDeleteItem = {
|
|
17
|
+
name: string
|
|
18
|
+
env: EnvMap
|
|
19
|
+
source: PresetSource
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createDeletePresetCommand({
|
|
23
|
+
presetService,
|
|
24
|
+
projectEnvService,
|
|
25
|
+
renderDelete,
|
|
26
|
+
}: {
|
|
27
|
+
presetService: PresetService
|
|
28
|
+
projectEnvService: ProjectEnvService
|
|
29
|
+
renderDelete: (presets: Array<PresetDeleteItem>) => Promise<PresetDeleteItem | undefined>
|
|
30
|
+
}) {
|
|
31
|
+
return async function deletePreset(): Promise<void> {
|
|
32
|
+
const names = await presetService.listNames()
|
|
33
|
+
const globalPresets = await Promise.all(
|
|
34
|
+
names.map((name) =>
|
|
35
|
+
presetService.read(name).then((p) => ({ name, env: p.env, source: 'global' as const })),
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const { env: projectEnv, name: projectName } = await projectEnvService.readWithMeta()
|
|
40
|
+
const projectPreset =
|
|
41
|
+
Object.keys(projectEnv).length > 0
|
|
42
|
+
? [{ name: projectName ?? 'project', env: projectEnv, source: 'project' as const }]
|
|
43
|
+
: []
|
|
44
|
+
|
|
45
|
+
const presets = [...projectPreset, ...globalPresets]
|
|
46
|
+
if (presets.length === 0) {
|
|
47
|
+
console.log('No presets found.')
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const selected = await renderDelete(presets)
|
|
52
|
+
if (!selected) return
|
|
53
|
+
|
|
54
|
+
if (selected.source === 'project') {
|
|
55
|
+
await projectEnvService.write({})
|
|
56
|
+
} else {
|
|
57
|
+
await presetService.remove(selected.name)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`Deleted preset: ${selected.name}`)
|
|
61
|
+
}
|
|
62
|
+
}
|