@lkangd/cc-env 1.3.1 → 1.4.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/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import React from 'react';
3
3
  import { render } from 'ink';
4
+ import { spawn } from 'node:child_process';
4
5
  import { join } from 'node:path';
5
6
  import figlet from 'figlet';
6
7
  import gradient from 'gradient-string';
@@ -8,11 +9,6 @@ import { Command } from 'commander';
8
9
  import packageJson from '../package.json' with { type: 'json' };
9
10
  const h = React.createElement;
10
11
  import { createPresetCreateCommand } from './commands/preset/create.js';
11
- import { createDeletePresetCommand } from './commands/preset/delete.js';
12
- import { createEditPresetCommand } from './commands/preset/edit.js';
13
- import { createRenamePresetCommand } from './commands/preset/rename.js';
14
- import { PresetDeleteApp } from './ink/preset-delete-app.js';
15
- import { PresetEditApp } from './ink/preset-edit-app.js';
16
12
  import { createShowPresetsCommand } from './commands/preset/show.js';
17
13
  import { createRestoreCommand } from './commands/restore.js';
18
14
  import { createRunCommand } from './commands/run.js';
@@ -54,6 +50,19 @@ const projectEnvService = createProjectEnvService({ cwd });
54
50
  const presetService = createPresetService(globalRoot);
55
51
  const historyService = createHistoryService(globalRoot);
56
52
  const projectStateService = createProjectStateService(globalRoot);
53
+ function openDirectory(directoryPath) {
54
+ return new Promise((resolve, reject) => {
55
+ const child = spawn('open', [directoryPath], { stdio: 'ignore' });
56
+ child.once('error', reject);
57
+ child.once('exit', (code) => {
58
+ if (code === 0) {
59
+ resolve();
60
+ return;
61
+ }
62
+ reject(new Error(`Failed to open directory: ${directoryPath}`));
63
+ });
64
+ });
65
+ }
57
66
  async function runPresetCreateFlow({ detectedEnv, requiredKeys }) {
58
67
  let result;
59
68
  const app = render(h(PresetCreateApp, {
@@ -228,33 +237,24 @@ program
228
237
  .action((options) => createShowPresetsCommand({
229
238
  presetService,
230
239
  projectEnvService,
240
+ cwd,
241
+ openDirectory,
231
242
  renderShow: async (presets) => {
232
243
  if (options.json) {
233
244
  process.stdout.write(JSON.stringify(presets, null, 2) + '\n');
234
- return;
245
+ return { type: 'exit' };
235
246
  }
236
- const app = render(h(PresetShowApp, { presets }));
237
- await app.waitUntilExit();
238
- }
239
- })());
240
- program
241
- .command('delete')
242
- .description('Delete a saved preset')
243
- .action(createDeletePresetCommand({
244
- presetService,
245
- projectEnvService,
246
- renderDelete: async (presets) => {
247
- let result;
248
- const app = render(h(PresetDeleteApp, {
247
+ let result = { type: 'exit' };
248
+ const app = render(h(PresetShowApp, {
249
249
  presets,
250
- onSubmit: preset => {
251
- result = preset;
252
- }
250
+ onSubmit: action => {
251
+ result = action;
252
+ },
253
253
  }));
254
254
  await app.waitUntilExit();
255
255
  return result;
256
256
  }
257
- }));
257
+ })());
258
258
  program
259
259
  .command('create')
260
260
  .description('Create a new environment preset')
@@ -272,28 +272,6 @@ program
272
272
  .description('Check system health and configuration')
273
273
  .option('--json', 'Output as JSON')
274
274
  .action((options) => runDoctorCommand({ cwd, json: options.json }));
275
- program
276
- .command('edit <name>')
277
- .description('Edit an existing preset')
278
- .action((name) => createEditPresetCommand({
279
- presetService,
280
- renderEdit: async (preset) => {
281
- let result;
282
- const app = render(h(PresetEditApp, {
283
- name: preset.name,
284
- env: preset.env,
285
- onSubmit: (value) => {
286
- result = value;
287
- }
288
- }));
289
- await app.waitUntilExit();
290
- return result;
291
- }
292
- })({ name }));
293
- program
294
- .command('rename <from> <to>')
295
- .description('Rename a preset')
296
- .action((from, to) => createRenamePresetCommand({ presetService })({ from, to }));
297
275
  program
298
276
  .command('completion')
299
277
  .description('Generate shell completion script')
@@ -333,6 +311,13 @@ async function main() {
333
311
  process.exitCode = 0;
334
312
  return;
335
313
  }
314
+ if (args[0] === 'claude') {
315
+ const opts = program.opts();
316
+ if (!opts.quiet)
317
+ printBanner();
318
+ await runWithBootstrap({ args, yes: !process.stdin.isTTY });
319
+ return;
320
+ }
336
321
  await program.parseAsync(process.argv);
337
322
  }
338
323
  main().catch((error) => {
@@ -1,16 +1,105 @@
1
- export function createShowPresetsCommand({ presetService, projectEnvService, renderShow, }) {
2
- return async function showPresets() {
1
+ import { join, dirname } from 'node:path';
2
+ import { CliError } from '../../core/errors.js';
3
+ export function createShowPresetsCommand({ presetService, projectEnvService, renderShow, cwd, openDirectory, }) {
4
+ async function loadPresets() {
3
5
  const names = await presetService.listNames();
4
6
  const globalPresets = await Promise.all(names.map((name) => presetService.read(name).then((p) => ({ name, env: p.env, source: 'global' }))));
5
7
  const { env: projectEnv, name: projectName } = await projectEnvService.readWithMeta();
6
8
  const projectPreset = Object.keys(projectEnv).length > 0
7
9
  ? [{ name: projectName ?? 'project', env: projectEnv, source: 'project' }]
8
10
  : [];
9
- const presets = [...projectPreset, ...globalPresets];
10
- if (presets.length === 0) {
11
- console.log('No presets found.');
12
- return;
11
+ return [...projectPreset, ...globalPresets];
12
+ }
13
+ async function validateRename(nextName, current) {
14
+ const trimmed = nextName.trim();
15
+ if (!trimmed)
16
+ throw new CliError('Preset name cannot be empty');
17
+ if (trimmed === current.name)
18
+ throw new CliError('New name must be different from the current name');
19
+ const globalNames = await presetService.listNames();
20
+ const { name: projectName } = await projectEnvService.readWithMeta();
21
+ const takenNames = new Set([
22
+ ...globalNames.filter((name) => !(current.source === 'global' && name === current.name)),
23
+ ...(projectName && !(current.source === 'project' && projectName === current.name) ? [projectName] : []),
24
+ ]);
25
+ if (takenNames.has(trimmed))
26
+ throw new CliError(`Preset "${trimmed}" already exists`);
27
+ return trimmed;
28
+ }
29
+ function resolvePresetDirectory(preset) {
30
+ if (preset.source === 'project') {
31
+ return join(cwd, '.cc-env');
32
+ }
33
+ return dirname(presetService.getPath(preset.name));
34
+ }
35
+ return async function showPresets() {
36
+ while (true) {
37
+ const presets = await loadPresets();
38
+ if (presets.length === 0) {
39
+ console.log('No presets found.');
40
+ return;
41
+ }
42
+ const action = await renderShow(presets);
43
+ if (!action || action.type === 'exit')
44
+ return;
45
+ if (action.type === 'open-directory') {
46
+ await openDirectory(resolvePresetDirectory(action.preset));
47
+ continue;
48
+ }
49
+ if (action.type === 'delete') {
50
+ if (action.preset.source === 'project') {
51
+ await projectEnvService.write({});
52
+ }
53
+ else {
54
+ await presetService.remove(action.preset.name);
55
+ }
56
+ continue;
57
+ }
58
+ if (action.type === 'edit') {
59
+ if (!action.result.confirmed)
60
+ continue;
61
+ const updatedAt = new Date().toISOString();
62
+ if (action.preset.source === 'project') {
63
+ const existing = await projectEnvService.readWithMeta();
64
+ await projectEnvService.write(action.result.env, {
65
+ name: existing.name ?? action.preset.name,
66
+ ...(existing.createdAt ? { createdAt: existing.createdAt } : {}),
67
+ updatedAt,
68
+ });
69
+ }
70
+ else {
71
+ const existing = await presetService.read(action.preset.name);
72
+ await presetService.write({
73
+ name: action.preset.name,
74
+ env: action.result.env,
75
+ createdAt: existing.createdAt,
76
+ updatedAt,
77
+ });
78
+ }
79
+ continue;
80
+ }
81
+ if (!action.confirmed)
82
+ continue;
83
+ const updatedAt = new Date().toISOString();
84
+ const nextName = await validateRename(action.nextName, action.preset);
85
+ if (action.preset.source === 'project') {
86
+ const existing = await projectEnvService.readWithMeta();
87
+ await projectEnvService.write(existing.env, {
88
+ name: nextName,
89
+ ...(existing.createdAt ? { createdAt: existing.createdAt } : {}),
90
+ updatedAt,
91
+ });
92
+ }
93
+ else {
94
+ const existing = await presetService.read(action.preset.name);
95
+ await presetService.write({
96
+ name: nextName,
97
+ env: existing.env,
98
+ createdAt: existing.createdAt,
99
+ updatedAt,
100
+ });
101
+ await presetService.remove(action.preset.name);
102
+ }
13
103
  }
14
- await renderShow(presets);
15
104
  };
16
105
  }
@@ -1,27 +1,111 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from 'react';
3
3
  import { Box, Text, useApp, useInput } from 'ink';
4
+ import { PresetEditApp } from './preset-edit-app.js';
5
+ import { TextInputDisplay } from './components/text-input.js';
6
+ import { useTextInput } from './hooks/use-text-input.js';
4
7
  import { EnvEntries } from './summary.js';
5
- export function PresetShowApp({ presets, }) {
8
+ export function PresetShowApp({ presets, onSubmit, }) {
6
9
  const { exit } = useApp();
7
10
  const [cursor, setCursor] = useState(0);
11
+ const [step, setStep] = useState('list');
12
+ const [renameError, setRenameError] = useState();
13
+ const textInput = useTextInput();
8
14
  const activePreset = presets[cursor];
9
15
  const entries = useMemo(() => activePreset
10
16
  ? Object.entries(activePreset.env).sort(([a], [b]) => a.localeCompare(b))
11
17
  : [], [activePreset]);
12
18
  useInput((input, key) => {
13
- if (key.escape || input.toLowerCase() === 'q') {
14
- exit();
19
+ if (step === 'list') {
20
+ if (key.escape || input.toLowerCase() === 'q') {
21
+ onSubmit({ type: 'exit' });
22
+ exit();
23
+ return;
24
+ }
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(presets.length - 1, c + 1));
31
+ return;
32
+ }
33
+ if (input === 'o' && activePreset) {
34
+ onSubmit({ type: 'open-directory', preset: activePreset });
35
+ exit();
36
+ return;
37
+ }
38
+ if (input === 'd' && activePreset) {
39
+ setStep('confirm-delete');
40
+ return;
41
+ }
42
+ if (input === 'r' && activePreset) {
43
+ textInput.reset(activePreset.name);
44
+ setRenameError(undefined);
45
+ setStep('rename');
46
+ return;
47
+ }
48
+ if (input === 'e' && activePreset) {
49
+ setStep('edit');
50
+ }
15
51
  return;
16
52
  }
17
- if (key.upArrow || input === 'k') {
18
- setCursor((c) => Math.max(0, c - 1));
53
+ if (step === 'rename') {
54
+ if (key.escape || input.toLowerCase() === 'q') {
55
+ setStep('list');
56
+ textInput.reset();
57
+ setRenameError(undefined);
58
+ return;
59
+ }
60
+ if (key.return) {
61
+ const nextName = textInput.value.trim();
62
+ if (!nextName) {
63
+ setRenameError('Name cannot be empty');
64
+ return;
65
+ }
66
+ if (nextName === activePreset?.name) {
67
+ setRenameError('New name must be different from the current name');
68
+ return;
69
+ }
70
+ setRenameError(undefined);
71
+ setStep('confirm-rename');
72
+ return;
73
+ }
74
+ if (textInput.handleKey(input, key))
75
+ return;
19
76
  return;
20
77
  }
21
- if (key.downArrow || input === 'j') {
22
- setCursor((c) => Math.min(presets.length - 1, c + 1));
78
+ if (step === 'confirm-delete') {
79
+ if (input.toLowerCase() === 'y' && activePreset) {
80
+ onSubmit({ type: 'delete', preset: activePreset });
81
+ exit();
82
+ return;
83
+ }
84
+ if (input.toLowerCase() === 'n' || key.escape) {
85
+ setStep('list');
86
+ return;
87
+ }
23
88
  return;
24
89
  }
90
+ if (step === 'confirm-rename') {
91
+ if (input.toLowerCase() === 'y' && activePreset) {
92
+ onSubmit({ type: 'rename', preset: activePreset, nextName: textInput.value.trim(), confirmed: true });
93
+ exit();
94
+ return;
95
+ }
96
+ if (input.toLowerCase() === 'n' || key.escape) {
97
+ setStep('list');
98
+ textInput.reset();
99
+ setRenameError(undefined);
100
+ return;
101
+ }
102
+ }
25
103
  });
26
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Preset show" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 q exit" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "Presets" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: presets.map((preset, index) => (_jsxs(Box, { children: [_jsx(Text, { children: index === cursor ? '' : ' ' }), _jsx(Text, { ...(preset.source === 'project' ? { color: 'yellow' } : {}), children: preset.name }), _jsxs(Text, { dimColor: true, children: [" (", preset.source, ")"] })] }, `${preset.source}:${preset.name}`))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: activePreset?.name ?? 'Preview' }), _jsx(Text, { dimColor: true, children: activePreset?.source === 'project' ? 'Project preset' : 'Global preset' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: entries }) })] })] })] }));
104
+ if (step === 'edit' && activePreset) {
105
+ return (_jsx(PresetEditApp, { name: activePreset.name, env: activePreset.env, onSubmit: (result) => {
106
+ onSubmit({ type: 'edit', preset: activePreset, result });
107
+ exit();
108
+ } }));
109
+ }
110
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Preset show" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 o open \u00B7 e edit \u00B7 r rename \u00B7 d delete \u00B7 q exit" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "Presets" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: presets.map((preset, index) => (_jsxs(Box, { children: [_jsx(Text, { children: index === cursor ? '❯ ' : ' ' }), _jsx(Text, { ...(preset.source === 'project' ? { color: 'yellow' } : {}), children: preset.name }), _jsxs(Text, { dimColor: true, children: [" (", preset.source, ")"] })] }, `${preset.source}:${preset.name}`))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: activePreset?.name ?? 'Preview' }), _jsx(Text, { dimColor: true, children: activePreset?.source === 'project' ? 'Project preset' : 'Global preset' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: entries }) })] })] }), step === 'rename' && activePreset ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, children: ["Rename preset: ", activePreset.name] }), _jsx(TextInputDisplay, { value: textInput.value, cursorPos: textInput.cursorPos }), renameError ? _jsx(Text, { color: "red", children: renameError }) : null, _jsx(Text, { dimColor: true, children: "Press enter to continue \u00B7 q to cancel" })] })) : null, step === 'confirm-delete' && activePreset ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "red", children: "Delete preset " }), _jsx(Text, { bold: true, children: activePreset.name }), _jsxs(Text, { color: "red", children: [" (", activePreset.source, ")?"] }), _jsx(Text, { dimColor: true, children: " y/n" })] })) : null, step === 'confirm-rename' && activePreset ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Rename preset ", _jsx(Text, { bold: true, children: activePreset.name }), " \u2192 ", _jsx(Text, { bold: true, children: textInput.value.trim() })] }), _jsx(Text, { dimColor: true, children: "Press y to confirm \u00B7 n to cancel" })] })) : null] }));
27
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lkangd/cc-env",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Manage runtime environment variables for Claude Code",
5
5
  "homepage": "https://github.com/lkangd/cc-env#readme",
6
6
  "bugs": {
@@ -1,25 +0,0 @@
1
- export function createDeletePresetCommand({ presetService, projectEnvService, renderDelete, }) {
2
- return async function deletePreset() {
3
- const names = await presetService.listNames();
4
- const globalPresets = await Promise.all(names.map((name) => presetService.read(name).then((p) => ({ name, env: p.env, source: 'global' }))));
5
- const { env: projectEnv, name: projectName } = await projectEnvService.readWithMeta();
6
- const projectPreset = Object.keys(projectEnv).length > 0
7
- ? [{ name: projectName ?? 'project', env: projectEnv, source: 'project' }]
8
- : [];
9
- const presets = [...projectPreset, ...globalPresets];
10
- if (presets.length === 0) {
11
- console.log('No presets found.');
12
- return;
13
- }
14
- const selected = await renderDelete(presets);
15
- if (!selected)
16
- return;
17
- if (selected.source === 'project') {
18
- await projectEnvService.write({});
19
- }
20
- else {
21
- await presetService.remove(selected.name);
22
- }
23
- console.log(`Deleted preset: ${selected.name}`);
24
- };
25
- }
@@ -1,20 +0,0 @@
1
- import { CliError } from '../../core/errors.js';
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;
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`);
19
- };
20
- }
@@ -1,16 +0,0 @@
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
- }
@@ -1,47 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from 'react';
3
- import { Box, Text, useApp, useInput } from 'ink';
4
- import { EnvEntries } from './summary.js';
5
- export function PresetDeleteApp({ presets, onSubmit, }) {
6
- const { exit } = useApp();
7
- const [cursor, setCursor] = useState(0);
8
- const [step, setStep] = useState('browsing');
9
- const activePreset = presets[cursor];
10
- const entries = useMemo(() => activePreset
11
- ? Object.entries(activePreset.env).sort(([a], [b]) => a.localeCompare(b))
12
- : [], [activePreset]);
13
- useInput((input, key) => {
14
- if (step === 'browsing') {
15
- if (key.escape || input.toLowerCase() === 'q') {
16
- exit();
17
- return;
18
- }
19
- if (key.upArrow || input === 'k') {
20
- setCursor((c) => Math.max(0, c - 1));
21
- return;
22
- }
23
- if (key.downArrow || input === 'j') {
24
- setCursor((c) => Math.min(presets.length - 1, c + 1));
25
- return;
26
- }
27
- if (key.return) {
28
- setStep('confirming');
29
- return;
30
- }
31
- }
32
- if (step === 'confirming') {
33
- if (input.toLowerCase() === 'y') {
34
- onSubmit(activePreset);
35
- exit();
36
- return;
37
- }
38
- if (input.toLowerCase() === 'n' || key.escape) {
39
- setStep('browsing');
40
- return;
41
- }
42
- }
43
- });
44
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Preset delete" }), _jsx(Text, { dimColor: true, children: step === 'browsing'
45
- ? '↑/k ↓/j navigate · Enter select · q exit'
46
- : 'y confirm · n cancel' }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "Presets" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: presets.map((preset, index) => (_jsxs(Box, { children: [_jsx(Text, { children: index === cursor ? '❯ ' : ' ' }), _jsx(Text, { ...(preset.source === 'project' ? { color: 'yellow' } : {}), children: preset.name }), _jsxs(Text, { dimColor: true, children: [" (", preset.source, ")"] })] }, `${preset.source}:${preset.name}`))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: activePreset.name }), _jsx(Text, { dimColor: true, children: activePreset.source === 'project' ? 'Project preset' : 'Global preset' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: entries }) })] })] }), step === 'confirming' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "red", children: "Delete preset " }), _jsx(Text, { bold: true, children: activePreset.name }), _jsx(Text, { color: "red", children: "?" }), _jsx(Text, { dimColor: true, children: " y/n" })] }))] }));
47
- }