@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.
Files changed (78) hide show
  1. package/LICENSE +15 -0
  2. package/dist/cli.js +68 -6
  3. package/dist/commands/completion.js +60 -0
  4. package/dist/commands/doctor.js +73 -0
  5. package/dist/commands/preset/edit.js +16 -11
  6. package/dist/commands/preset/rename.js +16 -0
  7. package/dist/commands/run.js +9 -1
  8. package/dist/ink/preset-edit-app.js +112 -0
  9. package/package.json +11 -2
  10. package/.claude/settings.json +0 -6
  11. package/.claude/settings.local.json +0 -8
  12. package/.nvmrc +0 -1
  13. package/CHANGELOG.md +0 -71
  14. package/docs/product-specs/index.draft.md +0 -106
  15. package/docs/product-specs/index.md +0 -911
  16. package/docs/product-specs/optional.md +0 -42
  17. package/docs/references/claude-code-env.md +0 -224
  18. package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +0 -1331
  19. package/docs/superpowers/plans/2026-04-24-cc-env.md +0 -1666
  20. package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +0 -1432
  21. package/docs/superpowers/specs/2026-04-24-cc-env-design.md +0 -438
  22. package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +0 -181
  23. package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +0 -78
  24. package/src/cli.ts +0 -340
  25. package/src/commands/init.ts +0 -139
  26. package/src/commands/preset/create.ts +0 -96
  27. package/src/commands/preset/delete.ts +0 -62
  28. package/src/commands/preset/show.ts +0 -51
  29. package/src/commands/restore.ts +0 -150
  30. package/src/commands/run.ts +0 -158
  31. package/src/core/errors.ts +0 -13
  32. package/src/core/find-claude.ts +0 -70
  33. package/src/core/format.ts +0 -29
  34. package/src/core/fs.ts +0 -18
  35. package/src/core/gitignore.ts +0 -26
  36. package/src/core/logger.ts +0 -11
  37. package/src/core/mask.ts +0 -17
  38. package/src/core/paths.ts +0 -41
  39. package/src/core/process-env.ts +0 -11
  40. package/src/core/schema.ts +0 -55
  41. package/src/core/spawn.ts +0 -36
  42. package/src/flows/init-flow.ts +0 -61
  43. package/src/flows/preset-create-flow.ts +0 -129
  44. package/src/flows/restore-flow.ts +0 -144
  45. package/src/ink/init-app.tsx +0 -110
  46. package/src/ink/preset-create-app.tsx +0 -451
  47. package/src/ink/preset-delete-app.tsx +0 -114
  48. package/src/ink/preset-show-app.tsx +0 -76
  49. package/src/ink/restore-app.tsx +0 -230
  50. package/src/ink/run-preset-select-app.tsx +0 -83
  51. package/src/ink/summary.tsx +0 -91
  52. package/src/services/claude-settings-env-service.ts +0 -72
  53. package/src/services/history-service.ts +0 -48
  54. package/src/services/preset-service.ts +0 -72
  55. package/src/services/project-env-service.ts +0 -128
  56. package/src/services/project-state-service.ts +0 -31
  57. package/src/services/settings-env-service.ts +0 -40
  58. package/src/services/shell-env-service.ts +0 -112
  59. package/src/types.d.ts +0 -19
  60. package/tests/cli/help.test.ts +0 -133
  61. package/tests/cli/init.test.ts +0 -76
  62. package/tests/cli/restore.test.ts +0 -172
  63. package/tests/commands/create.test.ts +0 -263
  64. package/tests/commands/output.test.ts +0 -119
  65. package/tests/commands/run.test.ts +0 -218
  66. package/tests/core/gitignore.test.ts +0 -98
  67. package/tests/core/paths.test.ts +0 -24
  68. package/tests/core/schema-mask.test.ts +0 -182
  69. package/tests/core/spawn.test.ts +0 -47
  70. package/tests/flows/init-flow.test.ts +0 -40
  71. package/tests/flows/preset-create-flow.test.ts +0 -225
  72. package/tests/flows/restore-flow.test.ts +0 -157
  73. package/tests/integration/init-restore.test.ts +0 -406
  74. package/tests/services/claude-shell.test.ts +0 -183
  75. package/tests/services/storage.test.ts +0 -143
  76. package/tsconfig.build.json +0 -9
  77. package/tsconfig.json +0 -22
  78. 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.name('cc-env').description('Manage runtime environment variables for Claude Code');
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
- .action(createShowPresetsCommand({
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
- printBanner();
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, processEnv = process.env, spawnSync = defaultSpawnSync, }) {
4
- return async function editPreset(name) {
5
- const editor = processEnv.EDITOR;
6
- if (!editor) {
7
- throw new CliError('EDITOR is required to edit a preset');
8
- }
9
- const preset = await presetService.read(name);
10
- const result = spawnSync(editor, [preset.filePath], { stdio: 'inherit' });
11
- if (result.status !== 0) {
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
+ }
@@ -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.1.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",
@@ -1,6 +0,0 @@
1
- {
2
- "enabledPlugins": {
3
- "superpowers@claude-plugins-official": true
4
- },
5
- "env": {}
6
- }
@@ -1,8 +0,0 @@
1
- {
2
- "env": {},
3
- "permissions": {
4
- "allow": [
5
- "Bash(python3 *)"
6
- ]
7
- }
8
- }
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