@lkangd/cc-env 1.1.1 → 1.2.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/LICENSE +15 -0
- package/dist/cli.js +68 -6
- package/dist/commands/completion.js +60 -0
- package/dist/commands/doctor.js +73 -0
- package/dist/commands/preset/edit.js +16 -11
- package/dist/commands/preset/rename.js +16 -0
- package/dist/commands/run.js +9 -1
- package/dist/ink/preset-edit-app.js +112 -0
- package/package.json +11 -2
- package/.claude/settings.json +0 -6
- package/.claude/settings.local.json +0 -8
- package/.nvmrc +0 -1
- package/CHANGELOG.md +0 -71
- package/docs/product-specs/index.draft.md +0 -106
- package/docs/product-specs/index.md +0 -911
- package/docs/product-specs/optional.md +0 -42
- package/docs/references/claude-code-env.md +0 -224
- package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +0 -1331
- package/docs/superpowers/plans/2026-04-24-cc-env.md +0 -1666
- package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +0 -1432
- package/docs/superpowers/specs/2026-04-24-cc-env-design.md +0 -438
- package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +0 -181
- package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +0 -78
- package/src/cli.ts +0 -340
- package/src/commands/init.ts +0 -139
- package/src/commands/preset/create.ts +0 -96
- package/src/commands/preset/delete.ts +0 -62
- package/src/commands/preset/show.ts +0 -51
- package/src/commands/restore.ts +0 -150
- package/src/commands/run.ts +0 -158
- package/src/core/errors.ts +0 -13
- package/src/core/find-claude.ts +0 -70
- package/src/core/format.ts +0 -29
- package/src/core/fs.ts +0 -18
- package/src/core/gitignore.ts +0 -26
- package/src/core/logger.ts +0 -11
- package/src/core/mask.ts +0 -17
- package/src/core/paths.ts +0 -41
- package/src/core/process-env.ts +0 -11
- package/src/core/schema.ts +0 -55
- package/src/core/spawn.ts +0 -36
- package/src/flows/init-flow.ts +0 -61
- package/src/flows/preset-create-flow.ts +0 -129
- package/src/flows/restore-flow.ts +0 -144
- package/src/ink/init-app.tsx +0 -110
- package/src/ink/preset-create-app.tsx +0 -451
- package/src/ink/preset-delete-app.tsx +0 -114
- package/src/ink/preset-show-app.tsx +0 -76
- package/src/ink/restore-app.tsx +0 -230
- package/src/ink/run-preset-select-app.tsx +0 -83
- package/src/ink/summary.tsx +0 -91
- package/src/services/claude-settings-env-service.ts +0 -72
- package/src/services/history-service.ts +0 -48
- package/src/services/preset-service.ts +0 -72
- package/src/services/project-env-service.ts +0 -128
- package/src/services/project-state-service.ts +0 -31
- package/src/services/settings-env-service.ts +0 -40
- package/src/services/shell-env-service.ts +0 -112
- package/src/types.d.ts +0 -19
- package/tests/cli/help.test.ts +0 -133
- package/tests/cli/init.test.ts +0 -76
- package/tests/cli/restore.test.ts +0 -172
- package/tests/commands/create.test.ts +0 -263
- package/tests/commands/output.test.ts +0 -119
- package/tests/commands/run.test.ts +0 -218
- package/tests/core/gitignore.test.ts +0 -98
- package/tests/core/paths.test.ts +0 -24
- package/tests/core/schema-mask.test.ts +0 -182
- package/tests/core/spawn.test.ts +0 -47
- package/tests/flows/init-flow.test.ts +0 -40
- package/tests/flows/preset-create-flow.test.ts +0 -225
- package/tests/flows/restore-flow.test.ts +0 -157
- package/tests/integration/init-restore.test.ts +0 -406
- package/tests/services/claude-shell.test.ts +0 -183
- package/tests/services/storage.test.ts +0 -143
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, lkangd
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/dist/cli.js
CHANGED
|
@@ -5,14 +5,19 @@ import { join } from 'node:path';
|
|
|
5
5
|
import figlet from 'figlet';
|
|
6
6
|
import gradient from 'gradient-string';
|
|
7
7
|
import { Command } from 'commander';
|
|
8
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
8
9
|
const h = React.createElement;
|
|
9
10
|
import { createInitCommand } from './commands/init.js';
|
|
10
11
|
import { createPresetCreateCommand } from './commands/preset/create.js';
|
|
11
12
|
import { createDeletePresetCommand } from './commands/preset/delete.js';
|
|
13
|
+
import { createEditPresetCommand } from './commands/preset/edit.js';
|
|
14
|
+
import { createRenamePresetCommand } from './commands/preset/rename.js';
|
|
12
15
|
import { PresetDeleteApp } from './ink/preset-delete-app.js';
|
|
16
|
+
import { PresetEditApp } from './ink/preset-edit-app.js';
|
|
13
17
|
import { createShowPresetsCommand } from './commands/preset/show.js';
|
|
14
18
|
import { createRestoreCommand } from './commands/restore.js';
|
|
15
19
|
import { createRunCommand } from './commands/run.js';
|
|
20
|
+
import { runDoctorCommand } from './commands/doctor.js';
|
|
16
21
|
import { findClaudeExecutable } from './core/find-claude.js';
|
|
17
22
|
import { InitApp } from './ink/init-app.js';
|
|
18
23
|
import { renderEnvSummary } from './ink/summary.js';
|
|
@@ -32,7 +37,13 @@ import { createProjectStateService } from './services/project-state-service.js';
|
|
|
32
37
|
import { createSettingsEnvService } from './services/settings-env-service.js';
|
|
33
38
|
import { createShellEnvService } from './services/shell-env-service.js';
|
|
34
39
|
const program = new Command();
|
|
35
|
-
program
|
|
40
|
+
program
|
|
41
|
+
.name('cc-env')
|
|
42
|
+
.description('Manage runtime environment variables for Claude Code')
|
|
43
|
+
.version(packageJson.version)
|
|
44
|
+
.option('--verbose', 'Enable verbose output')
|
|
45
|
+
.option('--quiet', 'Suppress non-essential output')
|
|
46
|
+
.option('--no-interactive', 'Disable interactive prompts (equivalent to -y)');
|
|
36
47
|
const homeDir = process.env.HOME ?? process.cwd();
|
|
37
48
|
const cwd = process.cwd();
|
|
38
49
|
const settingsPath = join(cwd, 'settings.json');
|
|
@@ -111,6 +122,7 @@ program
|
|
|
111
122
|
.description('Run claude with merged environment variables')
|
|
112
123
|
.option('--dry-run', 'Preview the merged env without executing')
|
|
113
124
|
.option('-y, --yes', 'Auto-select the default preset without interactive prompts')
|
|
125
|
+
.option('--json', 'Output as JSON (only with --dry-run)')
|
|
114
126
|
.action((args, options) => {
|
|
115
127
|
const rawArgs = args ?? [];
|
|
116
128
|
return createRunCommand({
|
|
@@ -136,6 +148,7 @@ program
|
|
|
136
148
|
args: rawArgs,
|
|
137
149
|
dryRun: options.dryRun ?? false,
|
|
138
150
|
yes: options.yes ?? false,
|
|
151
|
+
json: options.json ?? false,
|
|
139
152
|
cwd
|
|
140
153
|
});
|
|
141
154
|
});
|
|
@@ -186,14 +199,19 @@ program
|
|
|
186
199
|
program
|
|
187
200
|
.command('show')
|
|
188
201
|
.description('List and view all presets')
|
|
189
|
-
.
|
|
202
|
+
.option('--json', 'Output as JSON')
|
|
203
|
+
.action((options) => createShowPresetsCommand({
|
|
190
204
|
presetService,
|
|
191
205
|
projectEnvService,
|
|
192
206
|
renderShow: async (presets) => {
|
|
207
|
+
if (options.json) {
|
|
208
|
+
process.stdout.write(JSON.stringify(presets, null, 2) + '\n');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
193
211
|
const app = render(h(PresetShowApp, { presets }));
|
|
194
212
|
await app.waitUntilExit();
|
|
195
213
|
}
|
|
196
|
-
}));
|
|
214
|
+
})());
|
|
197
215
|
program
|
|
198
216
|
.command('delete')
|
|
199
217
|
.description('Delete a saved preset')
|
|
@@ -235,14 +253,58 @@ program
|
|
|
235
253
|
return result;
|
|
236
254
|
}
|
|
237
255
|
})({ cwd }));
|
|
256
|
+
program
|
|
257
|
+
.command('doctor')
|
|
258
|
+
.description('Check system health and configuration')
|
|
259
|
+
.option('--json', 'Output as JSON')
|
|
260
|
+
.action((options) => runDoctorCommand({ cwd, json: options.json }));
|
|
261
|
+
program
|
|
262
|
+
.command('edit <name>')
|
|
263
|
+
.description('Edit an existing preset')
|
|
264
|
+
.action((name) => createEditPresetCommand({
|
|
265
|
+
presetService,
|
|
266
|
+
renderEdit: async (preset) => {
|
|
267
|
+
let result;
|
|
268
|
+
const app = render(h(PresetEditApp, {
|
|
269
|
+
name: preset.name,
|
|
270
|
+
env: preset.env,
|
|
271
|
+
onSubmit: (value) => {
|
|
272
|
+
result = value;
|
|
273
|
+
}
|
|
274
|
+
}));
|
|
275
|
+
await app.waitUntilExit();
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
})({ name }));
|
|
279
|
+
program
|
|
280
|
+
.command('rename <from> <to>')
|
|
281
|
+
.description('Rename a preset')
|
|
282
|
+
.action((from, to) => createRenamePresetCommand({ presetService })({ from, to }));
|
|
283
|
+
program
|
|
284
|
+
.command('completion')
|
|
285
|
+
.description('Generate shell completion script')
|
|
286
|
+
.option('--shell <shell>', 'Shell type (bash, zsh, fish)', 'bash')
|
|
287
|
+
.action(async (options) => {
|
|
288
|
+
const { generateCompletion } = await import('./commands/completion.js');
|
|
289
|
+
process.stdout.write(generateCompletion(options.shell));
|
|
290
|
+
});
|
|
238
291
|
function printBanner() {
|
|
239
292
|
const banner = figlet.textSync('CC ENV', { font: 'ANSI Shadow' });
|
|
240
293
|
const line = '─'.repeat(48);
|
|
241
294
|
const styled = gradient(['#00d2ff', '#7b2ff7', '#ff0080'])(banner);
|
|
242
295
|
process.stderr.write(`\n${styled}\x1b[2m\n${line}\x1b[0m\n\n`);
|
|
243
296
|
}
|
|
244
|
-
program.hook('preAction', () => {
|
|
245
|
-
|
|
297
|
+
program.hook('preAction', (thisCommand) => {
|
|
298
|
+
const opts = program.opts();
|
|
299
|
+
if (!opts.quiet)
|
|
300
|
+
printBanner();
|
|
301
|
+
// propagate --no-interactive as -y to subcommands
|
|
302
|
+
const globalOpts = program.opts();
|
|
303
|
+
if (globalOpts.interactive === false) {
|
|
304
|
+
const subOpts = thisCommand.opts();
|
|
305
|
+
if ('yes' in subOpts)
|
|
306
|
+
thisCommand.setOptionValue('yes', true);
|
|
307
|
+
}
|
|
246
308
|
});
|
|
247
309
|
program.parseAsync(process.argv).catch((error) => {
|
|
248
310
|
if (error instanceof CliError) {
|
|
@@ -252,7 +314,7 @@ program.parseAsync(process.argv).catch((error) => {
|
|
|
252
314
|
}
|
|
253
315
|
if (error && typeof error === 'object' && 'code' in error) {
|
|
254
316
|
const { code, message } = error;
|
|
255
|
-
if (code === 'commander.helpDisplayed') {
|
|
317
|
+
if (code === 'commander.helpDisplayed' || code === 'commander.version') {
|
|
256
318
|
process.exitCode = 0;
|
|
257
319
|
return;
|
|
258
320
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const COMMANDS = ['run', 'init', 'restore', 'show', 'delete', 'create', 'doctor', 'completion', '--help', '--version'];
|
|
2
|
+
export function generateCompletion(shell) {
|
|
3
|
+
switch (shell) {
|
|
4
|
+
case 'zsh':
|
|
5
|
+
return generateZsh();
|
|
6
|
+
case 'fish':
|
|
7
|
+
return generateFish();
|
|
8
|
+
default:
|
|
9
|
+
return generateBash();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function generateBash() {
|
|
13
|
+
return `# cc-env bash completion
|
|
14
|
+
# Add to ~/.bashrc: eval "$(cc-env completion --shell bash)"
|
|
15
|
+
_cc_env_completions() {
|
|
16
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
17
|
+
local commands="${COMMANDS.join(' ')}"
|
|
18
|
+
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
19
|
+
}
|
|
20
|
+
complete -F _cc_env_completions cc-env
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
function generateZsh() {
|
|
24
|
+
const cmds = COMMANDS.filter((c) => !c.startsWith('-'));
|
|
25
|
+
const cmdList = cmds.map((c) => ` '${c}'`).join('\n');
|
|
26
|
+
return `# cc-env zsh completion
|
|
27
|
+
# Add to ~/.zshrc: eval "$(cc-env completion --shell zsh)"
|
|
28
|
+
_cc_env() {
|
|
29
|
+
local -a commands
|
|
30
|
+
commands=(
|
|
31
|
+
${cmdList}
|
|
32
|
+
)
|
|
33
|
+
_describe 'command' commands
|
|
34
|
+
}
|
|
35
|
+
compdef _cc_env cc-env
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
function generateFish() {
|
|
39
|
+
const cmds = [
|
|
40
|
+
['run', 'Run claude with merged environment variables'],
|
|
41
|
+
['init', 'Initialize cc-env for the current project'],
|
|
42
|
+
['restore', 'Restore environment variables from a previous snapshot'],
|
|
43
|
+
['show', 'List and view all presets'],
|
|
44
|
+
['delete', 'Delete a saved preset'],
|
|
45
|
+
['create', 'Create a new environment preset'],
|
|
46
|
+
['doctor', 'Check system health and configuration'],
|
|
47
|
+
['completion', 'Generate shell completion script'],
|
|
48
|
+
];
|
|
49
|
+
const lines = cmds.map(([cmd, desc]) => `complete -c cc-env -f -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'`);
|
|
50
|
+
return `# cc-env fish completion
|
|
51
|
+
# Add to fish config: cc-env completion --shell fish | source
|
|
52
|
+
${lines.join('\n')}
|
|
53
|
+
complete -c cc-env -l help -d 'Show help'
|
|
54
|
+
complete -c cc-env -l version -d 'Show version'
|
|
55
|
+
complete -c cc-env -l json -d 'Output as JSON'
|
|
56
|
+
complete -c cc-env -l quiet -d 'Suppress non-essential output'
|
|
57
|
+
complete -c cc-env -l verbose -d 'Enable verbose output'
|
|
58
|
+
complete -c cc-env -l no-interactive -d 'Disable interactive prompts'
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { access, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { findClaudeExecutable } from '../core/find-claude.js';
|
|
4
|
+
import { resolveGlobalRoot } from '../core/paths.js';
|
|
5
|
+
async function exists(p) {
|
|
6
|
+
try {
|
|
7
|
+
await access(p);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function checkGlobalRoot(globalRoot) {
|
|
15
|
+
const ok = await exists(globalRoot);
|
|
16
|
+
return { label: 'Global root (~/.cc-env)', ok, detail: globalRoot };
|
|
17
|
+
}
|
|
18
|
+
async function checkPresetsDir(globalRoot) {
|
|
19
|
+
const dir = join(globalRoot, 'presets');
|
|
20
|
+
const ok = await exists(dir);
|
|
21
|
+
if (!ok)
|
|
22
|
+
return { label: 'Presets directory', ok: false, detail: dir };
|
|
23
|
+
const entries = await readdir(dir).catch(() => []);
|
|
24
|
+
const count = entries.filter((e) => e.endsWith('.json')).length;
|
|
25
|
+
return { label: 'Presets directory', ok: true, detail: `${dir} (${count} preset${count === 1 ? '' : 's'})` };
|
|
26
|
+
}
|
|
27
|
+
async function checkClaudeExecutable() {
|
|
28
|
+
try {
|
|
29
|
+
const path = findClaudeExecutable();
|
|
30
|
+
return { label: 'Claude executable', ok: true, detail: path };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { label: 'Claude executable', ok: false, detail: 'not found — run: npm install -g @anthropic-ai/claude-code' };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function checkProjectEnv(cwd) {
|
|
37
|
+
const path = join(cwd, '.cc-env', 'env.json');
|
|
38
|
+
const ok = await exists(path);
|
|
39
|
+
return { label: 'Project env (.cc-env/env.json)', ok, detail: ok ? path : 'not initialized — run: cc-env init' };
|
|
40
|
+
}
|
|
41
|
+
function renderCheck(result, json) {
|
|
42
|
+
if (json)
|
|
43
|
+
return '';
|
|
44
|
+
const icon = result.ok ? '\x1b[32m✔\x1b[0m' : '\x1b[31m✘\x1b[0m';
|
|
45
|
+
const detail = result.detail ? `\x1b[2m ${result.detail}\x1b[0m` : '';
|
|
46
|
+
return ` ${icon} ${result.label}${detail ? '\n' + detail : ''}`;
|
|
47
|
+
}
|
|
48
|
+
export async function runDoctorCommand({ cwd, json = false, stdout = process.stdout, }) {
|
|
49
|
+
const globalRoot = resolveGlobalRoot();
|
|
50
|
+
const checks = await Promise.all([
|
|
51
|
+
checkGlobalRoot(globalRoot),
|
|
52
|
+
checkPresetsDir(globalRoot),
|
|
53
|
+
checkClaudeExecutable(),
|
|
54
|
+
checkProjectEnv(cwd),
|
|
55
|
+
]);
|
|
56
|
+
if (json) {
|
|
57
|
+
stdout.write(JSON.stringify(checks, null, 2) + '\n');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
stdout.write('\n');
|
|
61
|
+
for (const check of checks) {
|
|
62
|
+
stdout.write(renderCheck(check, false) + '\n');
|
|
63
|
+
}
|
|
64
|
+
stdout.write('\n');
|
|
65
|
+
const failed = checks.filter((c) => !c.ok);
|
|
66
|
+
if (failed.length === 0) {
|
|
67
|
+
stdout.write(' \x1b[32mAll checks passed.\x1b[0m\n\n');
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
stdout.write(` \x1b[33m${failed.length} check${failed.length > 1 ? 's' : ''} failed.\x1b[0m\n\n`);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { spawnSync as defaultSpawnSync } from 'node:child_process';
|
|
2
1
|
import { CliError } from '../../core/errors.js';
|
|
3
|
-
export function createEditPresetCommand({ presetService,
|
|
4
|
-
return async function editPreset(name) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
throw new CliError(`Editor exited with code ${result.status ?? 1}`);
|
|
2
|
+
export function createEditPresetCommand({ presetService, renderEdit, }) {
|
|
3
|
+
return async function editPreset({ name }) {
|
|
4
|
+
if (!name)
|
|
5
|
+
throw new CliError('Usage: cc-env edit <preset-name>');
|
|
6
|
+
const existing = await presetService.read(name);
|
|
7
|
+
const result = await renderEdit({ name, env: existing.env });
|
|
8
|
+
if (!result?.confirmed) {
|
|
9
|
+
process.stdout.write('Edit cancelled.\n');
|
|
10
|
+
return;
|
|
13
11
|
}
|
|
12
|
+
await presetService.write({
|
|
13
|
+
name,
|
|
14
|
+
env: result.env,
|
|
15
|
+
createdAt: existing.createdAt,
|
|
16
|
+
updatedAt: new Date().toISOString(),
|
|
17
|
+
});
|
|
18
|
+
process.stdout.write(`Updated preset "${name}"\n`);
|
|
14
19
|
};
|
|
15
20
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CliError } from '../../core/errors.js';
|
|
2
|
+
export function createRenamePresetCommand({ presetService }) {
|
|
3
|
+
return async function renamePreset({ from, to }) {
|
|
4
|
+
if (!from || !to)
|
|
5
|
+
throw new CliError('Usage: cc-env rename <from> <to>');
|
|
6
|
+
if (from === to)
|
|
7
|
+
throw new CliError('New name must be different from the current name');
|
|
8
|
+
const existing = await presetService.read(from);
|
|
9
|
+
const names = await presetService.listNames();
|
|
10
|
+
if (names.includes(to))
|
|
11
|
+
throw new CliError(`Preset "${to}" already exists`);
|
|
12
|
+
await presetService.write({ ...existing, name: to, updatedAt: new Date().toISOString() });
|
|
13
|
+
await presetService.remove(from);
|
|
14
|
+
process.stdout.write(`Renamed preset "${from}" → "${to}"\n`);
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -9,7 +9,7 @@ const requiredInitKeys = [
|
|
|
9
9
|
'ANTHROPIC_REASONING_MODEL',
|
|
10
10
|
];
|
|
11
11
|
export function createRunCommand({ claudeSettingsEnvService, presetService, projectEnvService, projectStateService, findClaude, renderPresetSelect, spawnCommand, stdout = process.stdout, }) {
|
|
12
|
-
return async function run({ args = [], dryRun = false, yes = false, cwd, }) {
|
|
12
|
+
return async function run({ args = [], dryRun = false, yes = false, json = false, cwd, }) {
|
|
13
13
|
// Step 0: Check settings files for init-managed keys
|
|
14
14
|
const sources = await claudeSettingsEnvService.read();
|
|
15
15
|
const mergedSettingsEnv = sources.reduce((acc, s) => ({ ...acc, ...s.env }), {});
|
|
@@ -66,6 +66,14 @@ export function createRunCommand({ claudeSettingsEnvService, presetService, proj
|
|
|
66
66
|
claudeArgs = args;
|
|
67
67
|
}
|
|
68
68
|
// Step 6: Print env vars
|
|
69
|
+
if (json && dryRun) {
|
|
70
|
+
stdout.write(JSON.stringify({
|
|
71
|
+
preset: { name: selected.name, source: selected.source },
|
|
72
|
+
command: [command, ...claudeArgs],
|
|
73
|
+
env: selected.env
|
|
74
|
+
}, null, 2) + '\n');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
69
77
|
const presetKeys = new Set(Object.keys(selected.env));
|
|
70
78
|
const envBlock = formatRunEnvBlock(selected.env, presetKeys);
|
|
71
79
|
stdout.write(`Using preset: ${selected.name} (${selected.source})\n${envBlock}\n\n`);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
|
|
5
|
+
const { exit } = useApp();
|
|
6
|
+
const [entries, setEntries] = useState(Object.entries(initialEnv));
|
|
7
|
+
const [cursor, setCursor] = useState(0);
|
|
8
|
+
const [editing, setEditing] = useState(null);
|
|
9
|
+
const [textInput, setTextInput] = useState('');
|
|
10
|
+
const [error, setError] = useState();
|
|
11
|
+
const [step, setStep] = useState('list');
|
|
12
|
+
useInput((input, key) => {
|
|
13
|
+
if (key.escape || input === 'q') {
|
|
14
|
+
if (editing !== null) {
|
|
15
|
+
setEditing(null);
|
|
16
|
+
setTextInput('');
|
|
17
|
+
setError(undefined);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
onSubmit({ env: initialEnv, confirmed: false });
|
|
21
|
+
exit();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (step === 'list' && editing === null) {
|
|
25
|
+
if (key.upArrow || input === 'k') {
|
|
26
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (key.downArrow || input === 'j') {
|
|
30
|
+
setCursor((c) => Math.min(entries.length - 1, c + 1));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (key.return && entries.length > 0) {
|
|
34
|
+
const entry = entries[cursor];
|
|
35
|
+
if (entry) {
|
|
36
|
+
setTextInput(`${entry[0]}=${entry[1]}`);
|
|
37
|
+
setEditing(cursor);
|
|
38
|
+
setError(undefined);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (input === 'd' && entries.length > 0) {
|
|
43
|
+
setEntries((prev) => prev.filter((_, i) => i !== cursor));
|
|
44
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (input === 'a') {
|
|
48
|
+
setTextInput('');
|
|
49
|
+
setEditing(entries.length);
|
|
50
|
+
setError(undefined);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (input === 's') {
|
|
54
|
+
setStep('confirm');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (editing !== null) {
|
|
59
|
+
if (key.backspace || key.delete) {
|
|
60
|
+
setTextInput((v) => v.slice(0, -1));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (key.return) {
|
|
64
|
+
const sep = textInput.indexOf('=');
|
|
65
|
+
if (sep <= 0) {
|
|
66
|
+
setError('Format must be KEY=VALUE');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const k = textInput.slice(0, sep);
|
|
70
|
+
const v = textInput.slice(sep + 1);
|
|
71
|
+
if (!/^[A-Z0-9_]+$/.test(k)) {
|
|
72
|
+
setError('Key must match [A-Z0-9_]+');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setEntries((prev) => {
|
|
76
|
+
const next = [...prev];
|
|
77
|
+
if (editing < prev.length) {
|
|
78
|
+
next[editing] = [k, v];
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
next.push([k, v]);
|
|
82
|
+
}
|
|
83
|
+
return next;
|
|
84
|
+
});
|
|
85
|
+
setEditing(null);
|
|
86
|
+
setTextInput('');
|
|
87
|
+
setError(undefined);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (input && !key.ctrl && !key.meta) {
|
|
91
|
+
setTextInput((v) => v + input);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (step === 'confirm') {
|
|
96
|
+
if (key.return) {
|
|
97
|
+
const env = Object.fromEntries(entries);
|
|
98
|
+
onSubmit({ env, confirmed: true });
|
|
99
|
+
exit();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (input === 'q') {
|
|
103
|
+
setStep('list');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
if (step === 'confirm') {
|
|
109
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Save changes to preset \"", name, "\"?"] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: entries.map(([k, v]) => (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u2022 " }), _jsx(Text, { color: "magenta", children: k }), _jsx(Text, { dimColor: true, children: "=" }), _jsx(Text, { children: v })] }, k))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press enter to confirm \u00B7 q to go back" }) })] }));
|
|
110
|
+
}
|
|
111
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Editing preset: ", name] }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter edit \u00B7 d delete \u00B7 a add \u00B7 s save \u00B7 q cancel" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [entries.map(([k, v], i) => (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { color: "magenta", children: k }), _jsx(Text, { dimColor: true, children: "=" }), _jsx(Text, { children: v })] }, k))), entries.length === 0 && _jsx(Text, { dimColor: true, children: "No entries. Press a to add." })] }), editing !== null && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: editing < entries.length ? 'Edit entry' : 'Add entry' }), _jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ['>', " "] }), _jsx(Text, { color: "cyan", children: textInput }), _jsx(Text, { dimColor: true, children: "\u2588" })] }), error ? _jsx(Text, { color: "red", children: error }) : null] }))] }));
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lkangd/cc-env",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"homepage": "https://github.com/lkangd/cc-env#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -22,10 +22,18 @@
|
|
|
22
22
|
"bin": {
|
|
23
23
|
"cc-env": "dist/cli.js"
|
|
24
24
|
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"package.json",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
25
31
|
"scripts": {
|
|
26
32
|
"build": "tsc -p tsconfig.build.json",
|
|
33
|
+
"prepack": "npm run build",
|
|
27
34
|
"dev": "tsx src/cli.ts",
|
|
28
|
-
"test": "vitest run"
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:coverage": "vitest run --coverage"
|
|
29
37
|
},
|
|
30
38
|
"dependencies": {
|
|
31
39
|
"@inkjs/ui": "^2.0.0",
|
|
@@ -46,6 +54,7 @@
|
|
|
46
54
|
"@types/gradient-string": "^1.1.6",
|
|
47
55
|
"@types/node": "^20.19.0",
|
|
48
56
|
"@types/react": "^19.1.10",
|
|
57
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
49
58
|
"execa": "^9.6.0",
|
|
50
59
|
"react-test-renderer": "^19.2.5",
|
|
51
60
|
"tsx": "^4.20.3",
|
package/.claude/settings.json
DELETED
package/.nvmrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
20.19.2
|
package/CHANGELOG.md
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.1.1 (2026-04-27)
|
|
4
|
-
|
|
5
|
-
### Code Refactoring
|
|
6
|
-
* flatten preset subcommands to top-level CLI commands
|
|
7
|
-
|
|
8
|
-
## 1.1.0 (2026-04-27)
|
|
9
|
-
|
|
10
|
-
### Features
|
|
11
|
-
* add CLI descriptions and improve error formatting
|
|
12
|
-
* add env source and merge services
|
|
13
|
-
* add env validation and masking helpers
|
|
14
|
-
* add gradient ASCII art banner to CLI startup
|
|
15
|
-
* add initial project structure with .gitignore, package.json, and documentation
|
|
16
|
-
* add interactive preset creation flow
|
|
17
|
-
* add interactive preset delete with confirmation flow
|
|
18
|
-
* add interactive preset list UI with project/global source display
|
|
19
|
-
* add non-interactive preset creation
|
|
20
|
-
* add output and preset inspection commands
|
|
21
|
-
* add preset and history storage services
|
|
22
|
-
* add restore flow and command
|
|
23
|
-
* add runtime execution and dry-run
|
|
24
|
-
* add settings migration flow
|
|
25
|
-
* add shebang to CLI entry point
|
|
26
|
-
* auto-add .cc-env to .gitignore on project preset create and remove config service
|
|
27
|
-
* complete cc-env v1 command wiring
|
|
28
|
-
* migrate Claude env into managed shell blocks
|
|
29
|
-
* redesign run command as Claude launcher with interactive preset selection
|
|
30
|
-
* scope package as @lkangd/cc-env for public npm publish
|
|
31
|
-
* support project-level Claude settings in init and restore
|
|
32
|
-
|
|
33
|
-
### Bug Fixes
|
|
34
|
-
* align preset create step progression
|
|
35
|
-
* complete interactive init and restore flows
|
|
36
|
-
* harden interactive preset create flow
|
|
37
|
-
* harden project env first-write handling
|
|
38
|
-
* harden restore flow selection
|
|
39
|
-
* harden run command validation and preview
|
|
40
|
-
* harden storage writes and preset deletion
|
|
41
|
-
* normalize preset create input errors
|
|
42
|
-
* resolve TypeScript exactOptionalPropertyTypes errors in preset create
|
|
43
|
-
* simplify interactive preset create flow
|
|
44
|
-
* support top-level run flags
|
|
45
|
-
* wire preset management commands and outputs
|
|
46
|
-
|
|
47
|
-
### Code Refactoring
|
|
48
|
-
* align persisted history records with schema
|
|
49
|
-
* extract shared EnvSummary component and replace stdout writes with ink rendering
|
|
50
|
-
* merge preset list and show into single interactive show command
|
|
51
|
-
* remove debug command and runtime env service
|
|
52
|
-
* remove preset edit command and add .cc-env/ to gitignore
|
|
53
|
-
* remove proper-lockfile in favor of atomic writes
|
|
54
|
-
* reorder merge params to match priority and use ink in debug
|
|
55
|
-
* rewrite preset-create-app with full interactive wizard UI
|
|
56
|
-
* rewrite preset-create-flow state machine for full interactive wizard
|
|
57
|
-
* simplify preset create command to thin renderFlow wrapper
|
|
58
|
-
* use sources array in history schema and improve interactive UI
|
|
59
|
-
|
|
60
|
-
### Documentation
|
|
61
|
-
* add preset create interactive refactor design spec
|
|
62
|
-
* add preset create interactive refactor implementation plan
|
|
63
|
-
|
|
64
|
-
### Other Changes
|
|
65
|
-
* merge: integrate Claude shell env migration
|
|
66
|
-
* fix restore typing against persisted history schema
|
|
67
|
-
* fix signal exits and history record validation
|
|
68
|
-
* fix restore flow state invariants and CLI wiring
|
|
69
|
-
* fix interactive preset create flow wiring
|
|
70
|
-
* fix schema timestamp validation and secret masking
|
|
71
|
-
* fix package dependency versions for task 1 compliance
|