@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,438 @@
|
|
|
1
|
+
# cc-env Design Spec
|
|
2
|
+
|
|
3
|
+
Date: 2026-04-24
|
|
4
|
+
Status: Approved for implementation
|
|
5
|
+
Scope: Full v1
|
|
6
|
+
|
|
7
|
+
## 1. Objective
|
|
8
|
+
|
|
9
|
+
Build a Node.js CLI named `cc-env` that injects a deterministic set of environment variables into a child process at runtime, primarily for Claude Code CLI usage, without relying on persistent shell mutation.
|
|
10
|
+
|
|
11
|
+
The tool must support:
|
|
12
|
+
- global presets for provider switching
|
|
13
|
+
- project-level env overrides
|
|
14
|
+
- migration from `~/.claude/settings.json`
|
|
15
|
+
- history and restore flows
|
|
16
|
+
- masked output for sensitive values
|
|
17
|
+
- deterministic merge behavior
|
|
18
|
+
|
|
19
|
+
## 2. Product Decisions Confirmed
|
|
20
|
+
|
|
21
|
+
The following decisions were confirmed during design:
|
|
22
|
+
|
|
23
|
+
- Full v1 scope is in scope, not MVP-only.
|
|
24
|
+
- Destructive or state-changing confirmation flows default to interactive confirmation, with `--yes` available for non-interactive automation.
|
|
25
|
+
- `debug` supports explicit `--preset <name>` and also falls back to a default preset in `~/.cc-env/config.json`.
|
|
26
|
+
- `preset create` interactive variable enumeration defaults to `process.env` and `~/.claude/settings.json.env`, and may additionally import current project env when present.
|
|
27
|
+
- `init` migrates selected keys from `~/.claude/settings.json.env` into a global preset chosen interactively by the user, and also writes a history record.
|
|
28
|
+
- Migrated variables belong to the global configuration domain, not the project override layer.
|
|
29
|
+
- `restore` supports choosing the destination interactively: restore to `settings.json.env` or to a global preset.
|
|
30
|
+
- Preset secrets are allowed to be stored in plaintext on disk in v1; outputs and logs must still mask sensitive values.
|
|
31
|
+
- Project env is part of full v1. It is managed indirectly through `preset create` flows: users can import current project env into a create flow and can choose to save create results back to the current project env file.
|
|
32
|
+
- Action commands use subcommands rather than long flags. Behavior modifiers still use flags.
|
|
33
|
+
- If both `./.cc-env/env.json` and `./.cc-env/env.yaml` exist, the command fails with a human-readable error.
|
|
34
|
+
- The design approach is a layered command-oriented architecture rather than a monolithic CLI or heavyweight domain abstraction.
|
|
35
|
+
|
|
36
|
+
## 3. Command Model
|
|
37
|
+
|
|
38
|
+
Action-style operations are represented as subcommands:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cc-env run [--preset <name>] [--dry-run] <command> [args...]
|
|
42
|
+
cc-env init [--yes]
|
|
43
|
+
cc-env restore [--yes]
|
|
44
|
+
cc-env preset create [--file <path>] [KEY=VALUE ...]
|
|
45
|
+
cc-env preset list
|
|
46
|
+
cc-env preset show <name>
|
|
47
|
+
cc-env preset delete <name> [--yes]
|
|
48
|
+
cc-env preset edit <name>
|
|
49
|
+
cc-env debug [--preset <name>]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3.1 Command semantics
|
|
53
|
+
|
|
54
|
+
#### `cc-env run`
|
|
55
|
+
- Loads env inputs from all supported sources.
|
|
56
|
+
- Resolves the effective preset from `--preset` or the default preset in `~/.cc-env/config.json`.
|
|
57
|
+
- If neither is available, fails with a human-readable preset selection error.
|
|
58
|
+
- Merges environment variables in the deterministic order defined below.
|
|
59
|
+
- Spawns the target child process with `stdio: 'inherit'`.
|
|
60
|
+
- Uses `cross-spawn`, not `exec()`.
|
|
61
|
+
- In `--dry-run`, prints what would run and the masked env values that would be injected, but does not spawn and does not mutate state.
|
|
62
|
+
|
|
63
|
+
#### `cc-env init`
|
|
64
|
+
- Reads `~/.claude/settings.json`.
|
|
65
|
+
- If no `env` field exists, prints `No env field found` and exits successfully without writing state.
|
|
66
|
+
- Uses Ink UI to let the user select which keys to migrate.
|
|
67
|
+
- Uses Ink UI to let the user name or select the destination global preset.
|
|
68
|
+
- Shows a preview of which keys will be written to the preset and removed from `settings.json.env`.
|
|
69
|
+
- Confirms before applying unless `--yes` is provided.
|
|
70
|
+
- On apply:
|
|
71
|
+
- writes selected keys to the destination global preset
|
|
72
|
+
- writes a history record
|
|
73
|
+
- removes the selected keys from `~/.claude/settings.json.env`
|
|
74
|
+
|
|
75
|
+
#### `cc-env restore`
|
|
76
|
+
- Lists available history records in Ink.
|
|
77
|
+
- Lets the user select a history record.
|
|
78
|
+
- Lets the user choose the restore target:
|
|
79
|
+
- `~/.claude/settings.json.env`
|
|
80
|
+
- a global preset
|
|
81
|
+
- Detects key collisions and asks for overwrite confirmation unless `--yes` is provided.
|
|
82
|
+
- Applies the restore and records the result in logs.
|
|
83
|
+
|
|
84
|
+
#### `cc-env preset create`
|
|
85
|
+
Supports three input modes:
|
|
86
|
+
- interactive selection
|
|
87
|
+
- file import with `--file`
|
|
88
|
+
- inline `KEY=VALUE` arguments
|
|
89
|
+
|
|
90
|
+
Interactive mode:
|
|
91
|
+
- lets the user choose variables from:
|
|
92
|
+
- `process.env`
|
|
93
|
+
- `~/.claude/settings.json.env`
|
|
94
|
+
- may additionally import current project env when present
|
|
95
|
+
- then lets the user choose the save target:
|
|
96
|
+
- a global preset
|
|
97
|
+
- the current project env file
|
|
98
|
+
|
|
99
|
+
Project env save behavior:
|
|
100
|
+
- if a project env file already exists, preserve its existing format
|
|
101
|
+
- if neither project env file exists, default to writing `./.cc-env/env.json`
|
|
102
|
+
|
|
103
|
+
#### `cc-env preset list`
|
|
104
|
+
- Prints a compact tabular list of presets with name, updated date, and variable count.
|
|
105
|
+
- No Ink UI.
|
|
106
|
+
|
|
107
|
+
#### `cc-env preset show <name>`
|
|
108
|
+
- Prints the preset content with sensitive values masked.
|
|
109
|
+
- No Ink UI.
|
|
110
|
+
|
|
111
|
+
#### `cc-env preset delete <name>`
|
|
112
|
+
- Requires confirmation unless `--yes` is provided.
|
|
113
|
+
- Deletes the preset file under file lock.
|
|
114
|
+
|
|
115
|
+
#### `cc-env preset edit <name>`
|
|
116
|
+
- Opens the preset in `$EDITOR`.
|
|
117
|
+
- Validation is re-run after save.
|
|
118
|
+
- Invalid edited content is rejected with a human-readable error.
|
|
119
|
+
|
|
120
|
+
#### `cc-env debug`
|
|
121
|
+
- Resolves preset from `--preset` or default config.
|
|
122
|
+
- Loads all env sources.
|
|
123
|
+
- Computes the final merged env.
|
|
124
|
+
- Prints masked effective env and source participation details.
|
|
125
|
+
- No Ink UI.
|
|
126
|
+
|
|
127
|
+
## 4. Storage Layout
|
|
128
|
+
|
|
129
|
+
Global state lives under:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
~/.cc-env/
|
|
133
|
+
config.json
|
|
134
|
+
presets/
|
|
135
|
+
<name>.json
|
|
136
|
+
history/
|
|
137
|
+
<timestamp>.json
|
|
138
|
+
logs/
|
|
139
|
+
cc-env.log
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Project state lives under the working directory:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
./.cc-env/
|
|
146
|
+
env.json
|
|
147
|
+
env.yaml
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Exactly zero or one project env file may exist. If both exist, the command fails.
|
|
151
|
+
|
|
152
|
+
## 5. Data Model
|
|
153
|
+
|
|
154
|
+
## 5.1 Preset schema
|
|
155
|
+
|
|
156
|
+
Each preset file is JSON and validates with zod:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"name": "openai",
|
|
161
|
+
"createdAt": "2026-04-24T10:00:00Z",
|
|
162
|
+
"updatedAt": "2026-04-24T10:00:00Z",
|
|
163
|
+
"env": {
|
|
164
|
+
"ANTHROPIC_BASE_URL": "https://api.openai.com",
|
|
165
|
+
"ANTHROPIC_AUTH_TOKEN": "sk-xxx"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Rules:
|
|
171
|
+
- `env` must be a flat object
|
|
172
|
+
- all values must be strings
|
|
173
|
+
- all keys must match `^[A-Z0-9_]+$`
|
|
174
|
+
|
|
175
|
+
## 5.2 History schema
|
|
176
|
+
|
|
177
|
+
Each history record is JSON and validates with zod:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"timestamp": "2026-04-24T10:00:00Z",
|
|
182
|
+
"action": "init",
|
|
183
|
+
"movedKeys": ["ANTHROPIC_BASE_URL"],
|
|
184
|
+
"backup": {
|
|
185
|
+
"ANTHROPIC_BASE_URL": "https://api.anthropic.com"
|
|
186
|
+
},
|
|
187
|
+
"targetType": "preset",
|
|
188
|
+
"targetName": "openai"
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Required semantics:
|
|
193
|
+
- `timestamp` identifies when the state-changing operation happened
|
|
194
|
+
- `action` identifies the originating action, such as `init` or `restore`
|
|
195
|
+
- `movedKeys` records which keys were affected
|
|
196
|
+
- `backup` stores the source values needed for restore
|
|
197
|
+
- `targetType` is either `settings` or `preset`
|
|
198
|
+
- `targetName` is present when `targetType` is `preset`
|
|
199
|
+
|
|
200
|
+
## 5.3 Config schema
|
|
201
|
+
|
|
202
|
+
`~/.cc-env/config.json` stores stable global tool settings. For v1, it only needs:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"defaultPreset": "openai"
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## 6. Deterministic Merge Rules
|
|
211
|
+
|
|
212
|
+
The effective env must always be computed in the same precedence order:
|
|
213
|
+
|
|
214
|
+
1. `~/.claude/settings.json.env`
|
|
215
|
+
2. `process.env`
|
|
216
|
+
3. selected preset
|
|
217
|
+
4. project env
|
|
218
|
+
|
|
219
|
+
Later sources override earlier sources.
|
|
220
|
+
|
|
221
|
+
This order is implemented once in a dedicated runtime env service and reused by both `run` and `debug`.
|
|
222
|
+
|
|
223
|
+
## 7. Architecture
|
|
224
|
+
|
|
225
|
+
The implementation uses a layered structure:
|
|
226
|
+
|
|
227
|
+
- **CLI layer** — Commander setup and top-level process exit handling
|
|
228
|
+
- **Command layer** — one entry per command or subcommand
|
|
229
|
+
- **Service layer** — business logic for presets, history, settings migration, project env, and runtime merge
|
|
230
|
+
- **Core layer** — schemas, masking, paths, locking, errors, logger, and process spawning helpers
|
|
231
|
+
|
|
232
|
+
Suggested source layout:
|
|
233
|
+
|
|
234
|
+
```text
|
|
235
|
+
src/
|
|
236
|
+
cli.ts
|
|
237
|
+
commands/
|
|
238
|
+
run.ts
|
|
239
|
+
init.ts
|
|
240
|
+
restore.ts
|
|
241
|
+
debug.ts
|
|
242
|
+
preset/
|
|
243
|
+
create.ts
|
|
244
|
+
list.ts
|
|
245
|
+
show.ts
|
|
246
|
+
delete.ts
|
|
247
|
+
edit.ts
|
|
248
|
+
services/
|
|
249
|
+
preset-service.ts
|
|
250
|
+
project-env-service.ts
|
|
251
|
+
settings-env-service.ts
|
|
252
|
+
history-service.ts
|
|
253
|
+
runtime-env-service.ts
|
|
254
|
+
core/
|
|
255
|
+
schema.ts
|
|
256
|
+
mask.ts
|
|
257
|
+
paths.ts
|
|
258
|
+
lock.ts
|
|
259
|
+
errors.ts
|
|
260
|
+
logger.ts
|
|
261
|
+
spawn.ts
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### 7.1 Architectural rule
|
|
265
|
+
|
|
266
|
+
All computation of the final merged env must happen in `runtime-env-service`. No command may reimplement merge precedence independently.
|
|
267
|
+
|
|
268
|
+
## 8. Interaction Design
|
|
269
|
+
|
|
270
|
+
Ink is used only for complex interactive flows:
|
|
271
|
+
- `init`
|
|
272
|
+
- `restore`
|
|
273
|
+
- interactive `preset create`
|
|
274
|
+
|
|
275
|
+
Ink is not used for:
|
|
276
|
+
- `preset list`
|
|
277
|
+
- `preset show`
|
|
278
|
+
- `debug`
|
|
279
|
+
|
|
280
|
+
### 8.1 `init` interaction flow
|
|
281
|
+
1. Load `settings.json.env`
|
|
282
|
+
2. Show selectable keys and masked previews
|
|
283
|
+
3. Ask for destination global preset name
|
|
284
|
+
4. Show operation preview
|
|
285
|
+
5. Confirm or auto-apply with `--yes`
|
|
286
|
+
6. Apply write operations under lock
|
|
287
|
+
|
|
288
|
+
### 8.2 `restore` interaction flow
|
|
289
|
+
1. List history records
|
|
290
|
+
2. Select one record
|
|
291
|
+
3. Select restore target
|
|
292
|
+
4. Detect collisions
|
|
293
|
+
5. Ask overwrite confirmation or auto-overwrite with `--yes`
|
|
294
|
+
6. Apply write operations under lock
|
|
295
|
+
|
|
296
|
+
### 8.3 `preset create` interaction flow
|
|
297
|
+
1. Determine input mode
|
|
298
|
+
2. For interactive mode, select variable source(s)
|
|
299
|
+
3. Select keys and values
|
|
300
|
+
4. Select destination target
|
|
301
|
+
5. Preview result
|
|
302
|
+
6. Apply write operations under lock
|
|
303
|
+
|
|
304
|
+
## 9. Security and Privacy Rules
|
|
305
|
+
|
|
306
|
+
Sensitive values may be stored in preset files in plaintext in v1, but must never be emitted raw in user-facing output or logs when the key name indicates a secret.
|
|
307
|
+
|
|
308
|
+
Masking applies to keys matching at least:
|
|
309
|
+
- `*_TOKEN`
|
|
310
|
+
- `*_KEY`
|
|
311
|
+
- `*_SECRET`
|
|
312
|
+
- `*_PASSWORD`
|
|
313
|
+
|
|
314
|
+
Masked output should preserve enough prefix to identify the value, for example:
|
|
315
|
+
|
|
316
|
+
```text
|
|
317
|
+
sk-123456********
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The logger must never write full secret values.
|
|
321
|
+
|
|
322
|
+
## 10. Concurrency and File Safety
|
|
323
|
+
|
|
324
|
+
Every state-changing write uses file locking via `proper-lockfile`.
|
|
325
|
+
|
|
326
|
+
This includes:
|
|
327
|
+
- writing presets
|
|
328
|
+
- deleting presets
|
|
329
|
+
- editing presets after validation
|
|
330
|
+
- mutating `~/.claude/settings.json`
|
|
331
|
+
- writing history records
|
|
332
|
+
- writing project env files
|
|
333
|
+
- updating `config.json`
|
|
334
|
+
|
|
335
|
+
The system must prefer reversible writes:
|
|
336
|
+
- read current state
|
|
337
|
+
- validate intended new state
|
|
338
|
+
- write atomically where possible
|
|
339
|
+
- record history for destructive migrations and restore operations
|
|
340
|
+
|
|
341
|
+
## 11. Error Handling
|
|
342
|
+
|
|
343
|
+
All user-facing errors are short and human-readable.
|
|
344
|
+
|
|
345
|
+
Rules:
|
|
346
|
+
- no stack traces in normal CLI output
|
|
347
|
+
- non-zero exit codes for failure
|
|
348
|
+
- distinguish argument errors from business-logic failures
|
|
349
|
+
|
|
350
|
+
Suggested exit code policy:
|
|
351
|
+
- `1` — runtime or business error
|
|
352
|
+
- `2` — invalid CLI usage or argument validation
|
|
353
|
+
|
|
354
|
+
Examples:
|
|
355
|
+
- `Preset not found: openai`
|
|
356
|
+
- `Project env conflict: env.json and env.yaml both exist`
|
|
357
|
+
- `No env field found`
|
|
358
|
+
|
|
359
|
+
## 12. Logging
|
|
360
|
+
|
|
361
|
+
Log file path:
|
|
362
|
+
|
|
363
|
+
```text
|
|
364
|
+
~/.cc-env/logs/cc-env.log
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Log entries include:
|
|
368
|
+
- timestamp
|
|
369
|
+
- command
|
|
370
|
+
- result
|
|
371
|
+
- error summary when present
|
|
372
|
+
|
|
373
|
+
Logs must not include raw secret values.
|
|
374
|
+
|
|
375
|
+
## 13. Testing Strategy
|
|
376
|
+
|
|
377
|
+
### 13.1 Unit tests
|
|
378
|
+
Cover:
|
|
379
|
+
- zod schema validation
|
|
380
|
+
- env merge precedence
|
|
381
|
+
- secret masking
|
|
382
|
+
- default preset fallback
|
|
383
|
+
- project env conflict detection
|
|
384
|
+
- project env format preservation
|
|
385
|
+
|
|
386
|
+
### 13.2 Integration tests
|
|
387
|
+
Cover:
|
|
388
|
+
- `run --dry-run`
|
|
389
|
+
- `preset create --file`
|
|
390
|
+
- inline `preset create`
|
|
391
|
+
- `init` migration side effects
|
|
392
|
+
- `restore` side effects
|
|
393
|
+
- `debug` output shape
|
|
394
|
+
|
|
395
|
+
### 13.3 Interaction tests
|
|
396
|
+
For Ink flows, test:
|
|
397
|
+
- key selection
|
|
398
|
+
- target selection
|
|
399
|
+
- overwrite confirmation
|
|
400
|
+
- `--yes` bypass behavior
|
|
401
|
+
|
|
402
|
+
Tests should verify decisions and side effects, not terminal styling.
|
|
403
|
+
|
|
404
|
+
## 14. Implementation Boundaries
|
|
405
|
+
|
|
406
|
+
This v1 does not include:
|
|
407
|
+
- shell mutation
|
|
408
|
+
- token lifecycle management
|
|
409
|
+
- secret encryption or OS keychain integration
|
|
410
|
+
- sandboxing
|
|
411
|
+
- Claude CLI hooking
|
|
412
|
+
- project env dedicated management commands outside the `preset create` flow
|
|
413
|
+
|
|
414
|
+
## 15. Recommended Implementation Sequence
|
|
415
|
+
|
|
416
|
+
1. Initialize TypeScript project structure and build/test tooling
|
|
417
|
+
2. Implement core schema, path, mask, error, and lock utilities
|
|
418
|
+
3. Implement preset storage service and config service
|
|
419
|
+
4. Implement project env and settings env readers/writers
|
|
420
|
+
5. Implement runtime env merge service
|
|
421
|
+
6. Implement `preset list/show/create/delete/edit`
|
|
422
|
+
7. Implement `debug`
|
|
423
|
+
8. Implement `run` with `--dry-run`
|
|
424
|
+
9. Implement `init`
|
|
425
|
+
10. Implement `restore`
|
|
426
|
+
11. Add interaction tests and integration coverage
|
|
427
|
+
|
|
428
|
+
## 16. Success Criteria
|
|
429
|
+
|
|
430
|
+
The design is successful when:
|
|
431
|
+
- the same inputs always produce the same merged env
|
|
432
|
+
- the tool can switch provider settings without shell mutation
|
|
433
|
+
- global presets and project env can be combined predictably
|
|
434
|
+
- migration from `~/.claude/settings.json.env` is reversible
|
|
435
|
+
- restore can target either settings or preset destination
|
|
436
|
+
- all secret-like output is masked
|
|
437
|
+
- all state-changing writes are lock-protected
|
|
438
|
+
- non-interactive automation is possible via `--yes` and `--dry-run`
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# cc-env init shell migration redesign
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Redefine `cc-env init` so it migrates selected env keys out of Claude Code's home-directory settings files and into shell-level global environment configuration. The command must stop creating presets. Its purpose is to remove startup-time env overrides from `~/.claude/settings.json` and `~/.claude/settings.local.json`, then make those values available to new terminal sessions through managed shell config blocks.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
This redesign changes:
|
|
10
|
+
|
|
11
|
+
- `init` input sources and migration target
|
|
12
|
+
- history shape for init migrations
|
|
13
|
+
- `restore` behavior for init history
|
|
14
|
+
- supporting services for Claude settings files and shell config files
|
|
15
|
+
|
|
16
|
+
This redesign does not change:
|
|
17
|
+
|
|
18
|
+
- preset CRUD behavior
|
|
19
|
+
- project env file behavior
|
|
20
|
+
- runtime merge precedence beyond the already-approved `process < settings < project < preset`
|
|
21
|
+
|
|
22
|
+
## Required init behavior
|
|
23
|
+
|
|
24
|
+
### Input sources
|
|
25
|
+
|
|
26
|
+
`init` reads only these two files in the user's home Claude directory:
|
|
27
|
+
|
|
28
|
+
- `~/.claude/settings.json`
|
|
29
|
+
- `~/.claude/settings.local.json`
|
|
30
|
+
|
|
31
|
+
For each file, `init` reads only its `env` field. If both files are missing, `init` exits with an error.
|
|
32
|
+
|
|
33
|
+
If the same env key exists in both files, `settings.local.json` wins for the effective migration value.
|
|
34
|
+
|
|
35
|
+
### Selection behavior
|
|
36
|
+
|
|
37
|
+
`init` builds a candidate view from the union of both `env` maps.
|
|
38
|
+
|
|
39
|
+
These six keys are always selected by default and cannot be deselected:
|
|
40
|
+
|
|
41
|
+
- `ANTHROPIC_AUTH_TOKEN`
|
|
42
|
+
- `ANTHROPIC_BASE_URL`
|
|
43
|
+
- `ANTHROPIC_DEFAULT_HAIKU_MODEL`
|
|
44
|
+
- `ANTHROPIC_DEFAULT_OPUS_MODEL`
|
|
45
|
+
- `ANTHROPIC_DEFAULT_SONNET_MODEL`
|
|
46
|
+
- `ANTHROPIC_REASONING_MODEL`
|
|
47
|
+
|
|
48
|
+
The user may add other discovered keys to the migration set.
|
|
49
|
+
|
|
50
|
+
If no selected key resolves to an effective value after applying `settings.local.json` precedence, `init` exits with an error instead of writing empty shell config.
|
|
51
|
+
|
|
52
|
+
### Migration behavior
|
|
53
|
+
|
|
54
|
+
After confirmation, `init` performs three operations:
|
|
55
|
+
|
|
56
|
+
1. Remove the selected keys from `~/.claude/settings.json` `env`
|
|
57
|
+
2. Remove the selected keys from `~/.claude/settings.local.json` `env`
|
|
58
|
+
3. Write the effective migrated values to managed shell config blocks for:
|
|
59
|
+
- `~/.zshrc`
|
|
60
|
+
- `~/.bashrc`
|
|
61
|
+
- `~/.config/fish/config.fish`
|
|
62
|
+
|
|
63
|
+
Shell writes guarantee the values are available to newly opened terminals. Already-running terminals are not updated in place.
|
|
64
|
+
|
|
65
|
+
`init` no longer creates or updates any preset.
|
|
66
|
+
|
|
67
|
+
## Managed shell block design
|
|
68
|
+
|
|
69
|
+
Each supported shell file gets a `cc-env` managed block. `cc-env` may only create, replace, or remove content inside its own block.
|
|
70
|
+
|
|
71
|
+
### zsh/bash block
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
# >>> cc-env >>>
|
|
75
|
+
export KEY="value"
|
|
76
|
+
# <<< cc-env <<<
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### fish block
|
|
80
|
+
|
|
81
|
+
```fish
|
|
82
|
+
# >>> cc-env >>>
|
|
83
|
+
set -gx KEY "value"
|
|
84
|
+
# <<< cc-env <<<
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Behavior rules:
|
|
88
|
+
|
|
89
|
+
- If the target shell file does not exist, create it and write the managed block
|
|
90
|
+
- If the managed block exists, replace the entire block
|
|
91
|
+
- If the managed block does not exist, append a new block
|
|
92
|
+
- Do not inspect or modify unrelated user content outside the managed block
|
|
93
|
+
|
|
94
|
+
## Service boundaries
|
|
95
|
+
|
|
96
|
+
### Claude settings env service
|
|
97
|
+
|
|
98
|
+
Introduce a dedicated service for Claude home settings files. It is responsible for:
|
|
99
|
+
|
|
100
|
+
- reading `env` from `~/.claude/settings.json`
|
|
101
|
+
- reading `env` from `~/.claude/settings.local.json`
|
|
102
|
+
- writing updated `env` maps back to each file independently
|
|
103
|
+
- treating missing files as missing sources rather than project-local defaults
|
|
104
|
+
|
|
105
|
+
This service should expose per-file data so history and restore can preserve source boundaries.
|
|
106
|
+
|
|
107
|
+
### Shell env service
|
|
108
|
+
|
|
109
|
+
Introduce a dedicated service for shell config files. It is responsible for:
|
|
110
|
+
|
|
111
|
+
- rendering shell-specific export syntax
|
|
112
|
+
- inserting/replacing/removing the `cc-env` managed block
|
|
113
|
+
- reading current managed keys when needed for restore
|
|
114
|
+
- handling zsh, bash, and fish paths independently
|
|
115
|
+
|
|
116
|
+
This service should not parse or rewrite arbitrary user shell config beyond its own block.
|
|
117
|
+
|
|
118
|
+
## History redesign
|
|
119
|
+
|
|
120
|
+
The current init history model is too narrow because it stores a single backup map and assumes restore targets are either `settings` or `preset`.
|
|
121
|
+
|
|
122
|
+
Init history must be expanded so restore can reverse the migration without guessing. An init record must contain at least:
|
|
123
|
+
|
|
124
|
+
- `timestamp`
|
|
125
|
+
- `action: 'init'`
|
|
126
|
+
- `migratedKeys`
|
|
127
|
+
- `settingsBackup` — keys removed from `~/.claude/settings.json`
|
|
128
|
+
- `settingsLocalBackup` — keys removed from `~/.claude/settings.local.json`
|
|
129
|
+
- `shellWrites` — which shell files were written and which effective key/value pairs were placed there
|
|
130
|
+
|
|
131
|
+
Restore logic must rely on this persisted data rather than recomputing source ownership later.
|
|
132
|
+
|
|
133
|
+
## Restore behavior for init history
|
|
134
|
+
|
|
135
|
+
For an init-created history entry, `restore` performs a two-way reversal:
|
|
136
|
+
|
|
137
|
+
1. Remove the migrated keys from the `cc-env` managed blocks in zsh, bash, and fish
|
|
138
|
+
2. Restore the backed-up keys to their original Claude settings source files:
|
|
139
|
+
- `settingsBackup` back to `~/.claude/settings.json`
|
|
140
|
+
- `settingsLocalBackup` back to `~/.claude/settings.local.json`
|
|
141
|
+
|
|
142
|
+
If a shell file was not present when the init record was created, restore should only touch files listed in that record's `shellWrites`.
|
|
143
|
+
|
|
144
|
+
Restore for non-init history should continue using its existing target-driven flow unless deliberately refactored as part of the implementation.
|
|
145
|
+
|
|
146
|
+
## Error handling
|
|
147
|
+
|
|
148
|
+
`init` should fail with a `CliError` when:
|
|
149
|
+
|
|
150
|
+
- both Claude settings files are missing
|
|
151
|
+
- no selected key has an effective value to migrate
|
|
152
|
+
- a required settings file cannot be parsed
|
|
153
|
+
- a shell config file cannot be updated
|
|
154
|
+
|
|
155
|
+
`restore` should fail with a `CliError` when:
|
|
156
|
+
|
|
157
|
+
- the requested history entry does not exist
|
|
158
|
+
- a recorded shell target cannot be updated
|
|
159
|
+
- a recorded settings target cannot be restored
|
|
160
|
+
|
|
161
|
+
## Testing
|
|
162
|
+
|
|
163
|
+
Add or update tests for:
|
|
164
|
+
|
|
165
|
+
- reading env from both Claude settings files
|
|
166
|
+
- `settings.local.json` overriding `settings.json` for effective migrated values
|
|
167
|
+
- the six required keys being preselected and non-removable in init flow state
|
|
168
|
+
- writing zsh/bash/fish managed blocks
|
|
169
|
+
- replacing an existing managed block without touching surrounding content
|
|
170
|
+
- removing managed keys during restore
|
|
171
|
+
- recording per-file Claude settings backups in history
|
|
172
|
+
- restoring those backups to the correct source file
|
|
173
|
+
- erroring when both settings files are absent
|
|
174
|
+
- erroring when no selected key resolves to a value
|
|
175
|
+
|
|
176
|
+
## Implementation notes
|
|
177
|
+
|
|
178
|
+
- Keep the existing codebase pattern of small services plus thin command handlers
|
|
179
|
+
- Prefer extending existing restore flow carefully over broad refactors
|
|
180
|
+
- Do not add preset compatibility shims to init
|
|
181
|
+
- Do not treat current working directory settings files as init inputs anymore
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Preset Create 交互重构设计
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
|
|
5
|
+
将 `preset create` 命令从半交互式(支持 CLI 参数直接执行)改为全交互式,去掉所有 CLI 参数(`-n`、`-f`、`--project`、`[pairs...]`),完全通过 ink UI 引导用户完成 preset 创建。
|
|
6
|
+
|
|
7
|
+
## 交互流程
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
source → filePath? → keys → manualInput? → name → destination → confirm → done
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 步骤说明
|
|
14
|
+
|
|
15
|
+
1. **source** — 选择数据来源:「文件导入(默认)」或「手动输入」,回车确认
|
|
16
|
+
2. **filePath**(仅文件导入)— 文本输入框输入文件路径,回车确认。支持 `.yaml`/`.yml`/`.json` 格式。读取失败时显示红色错误提示,允许重新输入
|
|
17
|
+
3. **keys**(仅文件导入)— 复用 init 的 checkbox 模式(j/k 上下、空格勾选、回车确认),展示从文件解析出的 key 供用户勾选
|
|
18
|
+
4. **manualInput**(仅手动输入)— 文本输入框输入 `KEY=VALUE` 回车添加,已添加的 key-value 实时显示,输入 `q` 结束
|
|
19
|
+
5. **name** — 文本输入框输入 preset 名称,回车确认
|
|
20
|
+
6. **destination** — 选择「全局 preset」或「项目 preset」,回车确认
|
|
21
|
+
7. **confirm** — 复用 `EnvSummary` 组件展示来源文件(如有)、保存路径、key-value 对,回车确认保存,`q` 取消
|
|
22
|
+
|
|
23
|
+
全局按键:`q`/Escape 在任意步骤退出。
|
|
24
|
+
|
|
25
|
+
## 架构变更
|
|
26
|
+
|
|
27
|
+
### Flow 状态机 (`src/flows/preset-create-flow.ts`)
|
|
28
|
+
|
|
29
|
+
从当前 5 步扩展为 7 步条件分支流程。State 新增字段:
|
|
30
|
+
|
|
31
|
+
- `source: 'file' | 'manual'`
|
|
32
|
+
- `filePath: string`
|
|
33
|
+
- `presetName: string`
|
|
34
|
+
- `destination: 'global' | 'project'`
|
|
35
|
+
- `env: EnvMap`(解析后的键值对)
|
|
36
|
+
|
|
37
|
+
路径分支:
|
|
38
|
+
- `source='file'` → `filePath` → `keys` → `name` → `destination` → `confirm` → `done`
|
|
39
|
+
- `source='manual'` → `manualInput` → `name` → `destination` → `confirm` → `done`
|
|
40
|
+
|
|
41
|
+
### Ink 组件 (`src/ink/preset-create-app.tsx`)
|
|
42
|
+
|
|
43
|
+
扩展渲染逻辑覆盖所有 7 个步骤。每步根据 flow state 的当前 step 渲染对应 UI。keys 步骤复用 init-app 的 checkbox 模式(j/k/空格/回车)。
|
|
44
|
+
|
|
45
|
+
### 命令层 (`src/commands/preset/create.ts`)
|
|
46
|
+
|
|
47
|
+
- 移除所有参数(`name`、`file`、`pairs`、`project`),命令函数无参数
|
|
48
|
+
- 不再有"有参数直接执行 vs 无参数交互"的分支
|
|
49
|
+
- 只调用 `renderFlow()` 拿结果,写入对应位置
|
|
50
|
+
- `buildPlaceholderEnv` 删除
|
|
51
|
+
- `readEnvFile` 和 `parseInlinePairs` 保留,由 ink 组件在交互中通过 renderFlow 回调调用
|
|
52
|
+
|
|
53
|
+
### CLI 注册 (`src/cli.ts`)
|
|
54
|
+
|
|
55
|
+
- 移除 `-n`、`-f`、`--project` flag 和 `[pairs...]` 参数
|
|
56
|
+
- 只保留裸命令 `preset create`
|
|
57
|
+
|
|
58
|
+
### 服务接口
|
|
59
|
+
|
|
60
|
+
`presetService.write()` 和 `projectEnvService.write()` 保持现有签名不变。
|
|
61
|
+
|
|
62
|
+
## 文件解析逻辑
|
|
63
|
+
|
|
64
|
+
在 `readEnvFile` 中增加 JSON env 字段处理:如果解析结果是对象且存在 `env` 字段(且 `env` 是对象),则使用 `env` 内的字段;否则使用第一层字段。
|
|
65
|
+
|
|
66
|
+
## 错误处理
|
|
67
|
+
|
|
68
|
+
- 文件不存在 / 无法读取 → 红色提示,停留在 filePath 步骤
|
|
69
|
+
- 文件格式不支持(非 .yaml/.yml/.json)→ 红色提示,重新输入
|
|
70
|
+
- 文件内容解析失败 → 红色提示,重新输入
|
|
71
|
+
- preset 名称为空 → 提示必须输入,停留在 name 步骤
|
|
72
|
+
- preset 名称已存在 → 提示已存在,可选择覆盖或重新输入
|
|
73
|
+
|
|
74
|
+
## 测试
|
|
75
|
+
|
|
76
|
+
- `readEnvFile` 的 JSON env 字段提取逻辑补单测
|
|
77
|
+
- flow 状态机的条件分支路径补单测(file 路径 vs manual 路径)
|
|
78
|
+
- 命令函数验证 renderFlow 结果正确路由到对应 service
|