@lkangd/cc-env 1.3.0 → 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/README.md +4 -0
- package/README.zh.md +4 -0
- package/dist/cli.js +34 -48
- package/dist/commands/completion.js +24 -18
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/preset/show.js +96 -7
- package/dist/core/cli-name.js +8 -0
- package/dist/flows/preset-create-flow.js +1 -1
- package/dist/ink/components/text-input.js +7 -0
- package/dist/ink/hooks/use-text-input.js +71 -0
- package/dist/ink/preset-create-app.js +27 -43
- package/dist/ink/preset-edit-app.js +12 -16
- package/dist/ink/preset-show-app.js +92 -8
- package/package.json +3 -2
- package/dist/commands/preset/delete.js +0 -25
- package/dist/commands/preset/edit.js +0 -20
- package/dist/commands/preset/rename.js +0 -16
- package/dist/ink/preset-delete-app.js +0 -47
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
`cc-env` is a CLI tool that lets you define, switch, and restore environment variable configurations for Claude Code — per project or via reusable presets. No more manually editing `settings.json` or juggling `.env` files across workspaces.
|
|
22
22
|
|
|
23
|
+
> **Alias:** `ccenv` can be used as a shorthand for `cc-env` everywhere (e.g., `ccenv run`, `ccenv create`).
|
|
24
|
+
|
|
23
25
|
## Installation
|
|
24
26
|
|
|
25
27
|
### via npm
|
|
@@ -75,6 +77,8 @@ cc-env run
|
|
|
75
77
|
|
|
76
78
|
## Shell Completion
|
|
77
79
|
|
|
80
|
+
> Both `cc-env` and `ccenv` are supported in completion scripts.
|
|
81
|
+
|
|
78
82
|
```bash
|
|
79
83
|
# bash
|
|
80
84
|
cc-env completion bash >> ~/.bashrc
|
package/README.zh.md
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
`cc-env` 是一个 CLI 工具,让你可以为 Claude Code 定义、切换和恢复环境变量配置——支持按项目配置或使用可复用的预设。不再需要手动编辑 `settings.json` 或在不同工作区之间切换 `.env` 文件。
|
|
22
22
|
|
|
23
|
+
> **别名:** `ccenv` 可以作为 `cc-env` 的简写使用(例如 `ccenv run`、`ccenv create`)。
|
|
24
|
+
|
|
23
25
|
## 安装
|
|
24
26
|
|
|
25
27
|
### 通过 npm
|
|
@@ -75,6 +77,8 @@ cc-env run
|
|
|
75
77
|
|
|
76
78
|
## Shell 补全
|
|
77
79
|
|
|
80
|
+
> 补全脚本同时支持 `cc-env` 和 `ccenv`。
|
|
81
|
+
|
|
78
82
|
```bash
|
|
79
83
|
# bash
|
|
80
84
|
cc-env completion bash >> ~/.bashrc
|
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';
|
|
@@ -24,6 +20,7 @@ import { PresetShowApp } from './ink/preset-show-app.js';
|
|
|
24
20
|
import { RunPresetSelectApp } from './ink/run-preset-select-app.js';
|
|
25
21
|
import { advanceRestoreFlow, createRestoreFlowState } from './flows/restore-flow.js';
|
|
26
22
|
import { RestoreApp } from './ink/restore-app.js';
|
|
23
|
+
import { getCliName } from './core/cli-name.js';
|
|
27
24
|
import { CliError } from './core/errors.js';
|
|
28
25
|
import { resolveGlobalRoot } from './core/paths.js';
|
|
29
26
|
import { spawnCommand } from './core/spawn.js';
|
|
@@ -36,7 +33,7 @@ import { createSettingsEnvService } from './services/settings-env-service.js';
|
|
|
36
33
|
import { createShellEnvService } from './services/shell-env-service.js';
|
|
37
34
|
const program = new Command();
|
|
38
35
|
program
|
|
39
|
-
.name(
|
|
36
|
+
.name(getCliName())
|
|
40
37
|
.description('Manage runtime environment variables for Claude Code')
|
|
41
38
|
.version(packageJson.version)
|
|
42
39
|
.option('--verbose', 'Enable verbose output')
|
|
@@ -53,6 +50,19 @@ const projectEnvService = createProjectEnvService({ cwd });
|
|
|
53
50
|
const presetService = createPresetService(globalRoot);
|
|
54
51
|
const historyService = createHistoryService(globalRoot);
|
|
55
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
|
+
}
|
|
56
66
|
async function runPresetCreateFlow({ detectedEnv, requiredKeys }) {
|
|
57
67
|
let result;
|
|
58
68
|
const app = render(h(PresetCreateApp, {
|
|
@@ -227,33 +237,24 @@ program
|
|
|
227
237
|
.action((options) => createShowPresetsCommand({
|
|
228
238
|
presetService,
|
|
229
239
|
projectEnvService,
|
|
240
|
+
cwd,
|
|
241
|
+
openDirectory,
|
|
230
242
|
renderShow: async (presets) => {
|
|
231
243
|
if (options.json) {
|
|
232
244
|
process.stdout.write(JSON.stringify(presets, null, 2) + '\n');
|
|
233
|
-
return;
|
|
245
|
+
return { type: 'exit' };
|
|
234
246
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
})());
|
|
239
|
-
program
|
|
240
|
-
.command('delete')
|
|
241
|
-
.description('Delete a saved preset')
|
|
242
|
-
.action(createDeletePresetCommand({
|
|
243
|
-
presetService,
|
|
244
|
-
projectEnvService,
|
|
245
|
-
renderDelete: async (presets) => {
|
|
246
|
-
let result;
|
|
247
|
-
const app = render(h(PresetDeleteApp, {
|
|
247
|
+
let result = { type: 'exit' };
|
|
248
|
+
const app = render(h(PresetShowApp, {
|
|
248
249
|
presets,
|
|
249
|
-
onSubmit:
|
|
250
|
-
result =
|
|
251
|
-
}
|
|
250
|
+
onSubmit: action => {
|
|
251
|
+
result = action;
|
|
252
|
+
},
|
|
252
253
|
}));
|
|
253
254
|
await app.waitUntilExit();
|
|
254
255
|
return result;
|
|
255
256
|
}
|
|
256
|
-
}));
|
|
257
|
+
})());
|
|
257
258
|
program
|
|
258
259
|
.command('create')
|
|
259
260
|
.description('Create a new environment preset')
|
|
@@ -271,35 +272,13 @@ program
|
|
|
271
272
|
.description('Check system health and configuration')
|
|
272
273
|
.option('--json', 'Output as JSON')
|
|
273
274
|
.action((options) => runDoctorCommand({ cwd, json: options.json }));
|
|
274
|
-
program
|
|
275
|
-
.command('edit <name>')
|
|
276
|
-
.description('Edit an existing preset')
|
|
277
|
-
.action((name) => createEditPresetCommand({
|
|
278
|
-
presetService,
|
|
279
|
-
renderEdit: async (preset) => {
|
|
280
|
-
let result;
|
|
281
|
-
const app = render(h(PresetEditApp, {
|
|
282
|
-
name: preset.name,
|
|
283
|
-
env: preset.env,
|
|
284
|
-
onSubmit: (value) => {
|
|
285
|
-
result = value;
|
|
286
|
-
}
|
|
287
|
-
}));
|
|
288
|
-
await app.waitUntilExit();
|
|
289
|
-
return result;
|
|
290
|
-
}
|
|
291
|
-
})({ name }));
|
|
292
|
-
program
|
|
293
|
-
.command('rename <from> <to>')
|
|
294
|
-
.description('Rename a preset')
|
|
295
|
-
.action((from, to) => createRenamePresetCommand({ presetService })({ from, to }));
|
|
296
275
|
program
|
|
297
276
|
.command('completion')
|
|
298
277
|
.description('Generate shell completion script')
|
|
299
278
|
.option('--shell <shell>', 'Shell type (bash, zsh, fish)', 'bash')
|
|
300
279
|
.action(async (options) => {
|
|
301
280
|
const { generateCompletion } = await import('./commands/completion.js');
|
|
302
|
-
process.stdout.write(generateCompletion(options.shell));
|
|
281
|
+
process.stdout.write(generateCompletion(options.shell, getCliName()));
|
|
303
282
|
});
|
|
304
283
|
function printBanner() {
|
|
305
284
|
const banner = figlet.textSync('CC ENV', { font: 'ANSI Shadow' });
|
|
@@ -332,6 +311,13 @@ async function main() {
|
|
|
332
311
|
process.exitCode = 0;
|
|
333
312
|
return;
|
|
334
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
|
+
}
|
|
335
321
|
await program.parseAsync(process.argv);
|
|
336
322
|
}
|
|
337
323
|
main().catch((error) => {
|
|
@@ -346,7 +332,7 @@ main().catch((error) => {
|
|
|
346
332
|
process.exitCode = 0;
|
|
347
333
|
return;
|
|
348
334
|
}
|
|
349
|
-
const hint = ` Run "
|
|
335
|
+
const hint = ` Run "${getCliName()} --help" to see available commands and options.\n`;
|
|
350
336
|
const formatted = message?.replace(/^error:\s*/i, '') ?? 'Unknown error';
|
|
351
337
|
process.stderr.write(`\n Error: ${formatted}\n\n${hint}\n`);
|
|
352
338
|
process.exitCode = 1;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
const COMMANDS = ['run', 'init', 'restore', 'show', 'delete', 'create', 'doctor', 'completion', '--help', '--version'];
|
|
2
|
-
export function generateCompletion(shell) {
|
|
2
|
+
export function generateCompletion(shell, cliName = 'cc-env') {
|
|
3
3
|
switch (shell) {
|
|
4
4
|
case 'zsh':
|
|
5
|
-
return generateZsh();
|
|
5
|
+
return generateZsh(cliName);
|
|
6
6
|
case 'fish':
|
|
7
|
-
return generateFish();
|
|
7
|
+
return generateFish(cliName);
|
|
8
8
|
default:
|
|
9
|
-
return generateBash();
|
|
9
|
+
return generateBash(cliName);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
function generateBash() {
|
|
13
|
-
return `#
|
|
12
|
+
function generateBash(cliName) {
|
|
13
|
+
return `# ${cliName} bash completion
|
|
14
14
|
# Add to ~/.bashrc: eval "$(cc-env completion --shell bash)"
|
|
15
15
|
_cc_env_completions() {
|
|
16
16
|
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
@@ -18,12 +18,13 @@ _cc_env_completions() {
|
|
|
18
18
|
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
19
19
|
}
|
|
20
20
|
complete -F _cc_env_completions cc-env
|
|
21
|
+
complete -F _cc_env_completions ccenv
|
|
21
22
|
`;
|
|
22
23
|
}
|
|
23
|
-
function generateZsh() {
|
|
24
|
+
function generateZsh(cliName) {
|
|
24
25
|
const cmds = COMMANDS.filter((c) => !c.startsWith('-'));
|
|
25
26
|
const cmdList = cmds.map((c) => ` '${c}'`).join('\n');
|
|
26
|
-
return `#
|
|
27
|
+
return `# ${cliName} zsh completion
|
|
27
28
|
# Add to ~/.zshrc: eval "$(cc-env completion --shell zsh)"
|
|
28
29
|
_cc_env() {
|
|
29
30
|
local -a commands
|
|
@@ -33,9 +34,10 @@ ${cmdList}
|
|
|
33
34
|
_describe 'command' commands
|
|
34
35
|
}
|
|
35
36
|
compdef _cc_env cc-env
|
|
37
|
+
compdef _cc_env ccenv
|
|
36
38
|
`;
|
|
37
39
|
}
|
|
38
|
-
function generateFish() {
|
|
40
|
+
function generateFish(cliName) {
|
|
39
41
|
const cmds = [
|
|
40
42
|
['run', 'Run claude with merged environment variables'],
|
|
41
43
|
['init', 'Initialize cc-env for the current project'],
|
|
@@ -46,15 +48,19 @@ function generateFish() {
|
|
|
46
48
|
['doctor', 'Check system health and configuration'],
|
|
47
49
|
['completion', 'Generate shell completion script'],
|
|
48
50
|
];
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
+
const names = ['cc-env', 'ccenv'];
|
|
52
|
+
const subcommandLines = names.flatMap(name => cmds.map(([cmd, desc]) => `complete -c ${name} -f -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'`));
|
|
53
|
+
const flagLines = names.flatMap(name => [
|
|
54
|
+
`complete -c ${name} -l help -d 'Show help'`,
|
|
55
|
+
`complete -c ${name} -l version -d 'Show version'`,
|
|
56
|
+
`complete -c ${name} -l json -d 'Output as JSON'`,
|
|
57
|
+
`complete -c ${name} -l quiet -d 'Suppress non-essential output'`,
|
|
58
|
+
`complete -c ${name} -l verbose -d 'Enable verbose output'`,
|
|
59
|
+
`complete -c ${name} -l no-interactive -d 'Disable interactive prompts'`,
|
|
60
|
+
]);
|
|
61
|
+
return `# ${cliName} fish completion
|
|
51
62
|
# Add to fish config: cc-env completion --shell fish | source
|
|
52
|
-
${
|
|
53
|
-
|
|
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'
|
|
63
|
+
${subcommandLines.join('\n')}
|
|
64
|
+
${flagLines.join('\n')}
|
|
59
65
|
`;
|
|
60
66
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, readdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { getCliName } from '../core/cli-name.js';
|
|
3
4
|
import { findClaudeExecutable } from '../core/find-claude.js';
|
|
4
5
|
import { resolveGlobalRoot } from '../core/paths.js';
|
|
5
6
|
async function exists(p) {
|
|
@@ -36,7 +37,7 @@ async function checkClaudeExecutable() {
|
|
|
36
37
|
async function checkProjectEnv(cwd) {
|
|
37
38
|
const path = join(cwd, '.cc-env', 'env.json');
|
|
38
39
|
const ok = await exists(path);
|
|
39
|
-
return { label: 'Project env (.cc-env/env.json)', ok, detail: ok ? path :
|
|
40
|
+
return { label: 'Project env (.cc-env/env.json)', ok, detail: ok ? path : `not initialized — run: ${getCliName()} init` };
|
|
40
41
|
}
|
|
41
42
|
function renderCheck(result, json) {
|
|
42
43
|
if (json)
|
|
@@ -1,16 +1,105 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
}
|
|
@@ -3,7 +3,7 @@ export function createPresetCreateFlowState(input) {
|
|
|
3
3
|
const requiredKeys = input?.requiredKeys ?? [];
|
|
4
4
|
const detectedKeys = Object.keys(detectedEnv).sort();
|
|
5
5
|
const selectedKeys = requiredKeys.filter((key) => key in detectedEnv);
|
|
6
|
-
if (
|
|
6
|
+
if (selectedKeys.length > 0) {
|
|
7
7
|
return {
|
|
8
8
|
step: 'detectedPrompt',
|
|
9
9
|
env: detectedEnv,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function TextInputDisplay({ value, cursorPos }) {
|
|
4
|
+
const before = value.slice(0, cursorPos);
|
|
5
|
+
const after = value.slice(cursorPos);
|
|
6
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ['>', " "] }), _jsx(Text, { color: "cyan", children: before }), _jsx(Text, { dimColor: true, children: "\u2588" }), _jsx(Text, { color: "cyan", children: after })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
export function handleKey(state, input, key, setState) {
|
|
3
|
+
const { value, cursorPos } = state;
|
|
4
|
+
if (key.leftArrow) {
|
|
5
|
+
if (cursorPos > 0)
|
|
6
|
+
setState(value, cursorPos - 1);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (key.rightArrow) {
|
|
10
|
+
if (cursorPos < value.length)
|
|
11
|
+
setState(value, cursorPos + 1);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (key.home) {
|
|
15
|
+
setState(value, 0);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (key.end) {
|
|
19
|
+
setState(value, value.length);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (key.ctrl && input === 'a') {
|
|
23
|
+
setState(value, 0);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (key.ctrl && input === 'e') {
|
|
27
|
+
setState(value, value.length);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (key.ctrl && input === 'u') {
|
|
31
|
+
setState(value.slice(cursorPos), 0);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (key.ctrl && input === 'k') {
|
|
35
|
+
setState(value.slice(0, cursorPos), cursorPos);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if ((key.ctrl && (key.backspace || key.delete)) || (key.meta && (key.backspace || key.delete))) {
|
|
39
|
+
if (cursorPos > 0) {
|
|
40
|
+
setState(value.slice(cursorPos), 0);
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (key.backspace || key.delete) {
|
|
45
|
+
if (cursorPos > 0) {
|
|
46
|
+
setState(value.slice(0, cursorPos - 1) + value.slice(cursorPos), cursorPos - 1);
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (input && !key.ctrl && !key.meta) {
|
|
51
|
+
setState(value.slice(0, cursorPos) + input + value.slice(cursorPos), cursorPos + 1);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
export function useTextInput() {
|
|
57
|
+
const [value, setValue] = useState('');
|
|
58
|
+
const [cursorPos, setCursorPos] = useState(0);
|
|
59
|
+
const setState = (newValue, newCursor) => {
|
|
60
|
+
setValue(newValue);
|
|
61
|
+
setCursorPos(newCursor);
|
|
62
|
+
};
|
|
63
|
+
const onKey = (input, key) => {
|
|
64
|
+
return handleKey({ value, cursorPos }, input, key, setState);
|
|
65
|
+
};
|
|
66
|
+
const reset = (newValue = '') => {
|
|
67
|
+
setValue(newValue);
|
|
68
|
+
setCursorPos(newValue.length);
|
|
69
|
+
};
|
|
70
|
+
return { value, cursorPos, handleKey: onKey, setValue, reset };
|
|
71
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
import { useTextInput } from './hooks/use-text-input.js';
|
|
5
|
+
import { TextInputDisplay } from './components/text-input.js';
|
|
4
6
|
import { advancePresetCreateFlow, createPresetCreateFlowState, } from '../flows/preset-create-flow.js';
|
|
5
7
|
import { EnvSummary } from './summary.js';
|
|
6
8
|
function DetectedPromptStep({ cursor }) {
|
|
@@ -21,8 +23,8 @@ function SourceStep({ cursor }) {
|
|
|
21
23
|
];
|
|
22
24
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select env source" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter confirm" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { ...(i === cursor ? { color: 'cyan' } : {}), children: opt.label })] }, opt.value))) })] }));
|
|
23
25
|
}
|
|
24
|
-
function FilePathStep({ value, error }) {
|
|
25
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter file path (.yaml/.yml/.json)" }),
|
|
26
|
+
function FilePathStep({ value, cursorPos, error }) {
|
|
27
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter file path (.yaml/.yml/.json)" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInputDisplay, { value: value, cursorPos: cursorPos }) }), error ? _jsx(Text, { color: "red", children: error }) : null] }));
|
|
26
28
|
}
|
|
27
29
|
function KeysStep({ keys, selectedKeys, cursor, }) {
|
|
28
30
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select env keys to import" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 space toggle \u00B7 enter confirm" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: keys.map((key, i) => {
|
|
@@ -30,11 +32,11 @@ function KeysStep({ keys, selectedKeys, cursor, }) {
|
|
|
30
32
|
return (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? 'green' : '', children: isSelected ? '[x]' : '[ ]' }), _jsxs(Text, { children: [" ", key] })] }, key));
|
|
31
33
|
}) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [selectedKeys.length, " of ", keys.length, " selected"] }) })] }));
|
|
32
34
|
}
|
|
33
|
-
function ManualInputStep({ entries, value, error, }) {
|
|
34
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter KEY=VALUE pairs (press q when done)" }), entries.length > 0 ? (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: entries.map(([key, val]) => (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u2022 " }), _jsx(Text, { color: "magenta", children: key }), _jsx(Text, { dimColor: true, children: "=" }), _jsx(Text, { children: val })] }, key))) })) : null,
|
|
35
|
+
function ManualInputStep({ entries, value, cursorPos, error, }) {
|
|
36
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter KEY=VALUE pairs (press q when done)" }), entries.length > 0 ? (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: entries.map(([key, val]) => (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u2022 " }), _jsx(Text, { color: "magenta", children: key }), _jsx(Text, { dimColor: true, children: "=" }), _jsx(Text, { children: val })] }, key))) })) : null, _jsx(TextInputDisplay, { value: value, cursorPos: cursorPos }), error ? _jsx(Text, { color: "red", children: error }) : null] }));
|
|
35
37
|
}
|
|
36
|
-
function NameStep({ value }) {
|
|
37
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter preset name" }),
|
|
38
|
+
function NameStep({ value, cursorPos }) {
|
|
39
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Enter preset name" }), _jsx(Box, { marginTop: 1, children: _jsx(TextInputDisplay, { value: value, cursorPos: cursorPos }) })] }));
|
|
38
40
|
}
|
|
39
41
|
function DestinationStep({ cursor }) {
|
|
40
42
|
const options = [
|
|
@@ -48,7 +50,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
48
50
|
const [state, setState] = useState(() => createPresetCreateFlowState(detectedEnv
|
|
49
51
|
? (requiredKeys ? { detectedEnv, requiredKeys } : { detectedEnv })
|
|
50
52
|
: undefined));
|
|
51
|
-
const
|
|
53
|
+
const textInput = useTextInput();
|
|
52
54
|
const [listCursor, setListCursor] = useState(0);
|
|
53
55
|
const [allKeys, setAllKeys] = useState([]);
|
|
54
56
|
const [fileEnv, setFileEnv] = useState({});
|
|
@@ -75,7 +77,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
75
77
|
? { type: 'accept-detected-prompt' }
|
|
76
78
|
: { type: 'reject-detected-prompt' }));
|
|
77
79
|
setListCursor(0);
|
|
78
|
-
|
|
80
|
+
textInput.reset();
|
|
79
81
|
return;
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -105,7 +107,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
105
107
|
if (key.return) {
|
|
106
108
|
setState((s) => advancePresetCreateFlow(s, { type: 'confirm-detected-keys' }));
|
|
107
109
|
setListCursor(0);
|
|
108
|
-
|
|
110
|
+
textInput.reset();
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
113
|
}
|
|
@@ -126,7 +128,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
126
128
|
const source = listCursor === 0 ? 'file' : 'manual';
|
|
127
129
|
setState((s) => advancePresetCreateFlow(s, { type: 'select-source', source }));
|
|
128
130
|
setListCursor(0);
|
|
129
|
-
|
|
131
|
+
textInput.reset();
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
132
134
|
}
|
|
@@ -135,14 +137,10 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
135
137
|
exit();
|
|
136
138
|
return;
|
|
137
139
|
}
|
|
138
|
-
if (key.backspace || key.delete) {
|
|
139
|
-
setTextInput((v) => v.slice(0, -1));
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
140
|
if (key.return) {
|
|
143
141
|
void (async () => {
|
|
144
142
|
try {
|
|
145
|
-
const result = await readFile(textInput);
|
|
143
|
+
const result = await readFile(textInput.value);
|
|
146
144
|
if (result.allKeys.length === 0) {
|
|
147
145
|
setState((s) => advancePresetCreateFlow(s, {
|
|
148
146
|
type: 'set-error',
|
|
@@ -154,7 +152,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
154
152
|
setFileEnv(result.env);
|
|
155
153
|
setState((s) => advancePresetCreateFlow(s, {
|
|
156
154
|
type: 'set-file-path',
|
|
157
|
-
filePath: textInput,
|
|
155
|
+
filePath: textInput.value,
|
|
158
156
|
}));
|
|
159
157
|
setListCursor(0);
|
|
160
158
|
}
|
|
@@ -168,10 +166,8 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
168
166
|
})();
|
|
169
167
|
return;
|
|
170
168
|
}
|
|
171
|
-
if (input
|
|
172
|
-
setTextInput((v) => v + input);
|
|
169
|
+
if (textInput.handleKey(input, key))
|
|
173
170
|
return;
|
|
174
|
-
}
|
|
175
171
|
}
|
|
176
172
|
if (state.step === 'keys') {
|
|
177
173
|
if (input === 'q') {
|
|
@@ -206,12 +202,12 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
206
202
|
keys: state.selectedKeys,
|
|
207
203
|
env: selectedEnv,
|
|
208
204
|
}));
|
|
209
|
-
|
|
205
|
+
textInput.reset();
|
|
210
206
|
return;
|
|
211
207
|
}
|
|
212
208
|
}
|
|
213
209
|
if (state.step === 'manualInput') {
|
|
214
|
-
if (input === 'q' && textInput === '') {
|
|
210
|
+
if (input === 'q' && textInput.value === '') {
|
|
215
211
|
if (state.selectedKeys.length === 0) {
|
|
216
212
|
setState((s) => advancePresetCreateFlow(s, {
|
|
217
213
|
type: 'set-error',
|
|
@@ -220,15 +216,11 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
220
216
|
return;
|
|
221
217
|
}
|
|
222
218
|
setState((s) => advancePresetCreateFlow(s, { type: 'finish-manual-input' }));
|
|
223
|
-
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
if (key.backspace || key.delete) {
|
|
227
|
-
setTextInput((v) => v.slice(0, -1));
|
|
219
|
+
textInput.reset();
|
|
228
220
|
return;
|
|
229
221
|
}
|
|
230
222
|
if (key.return) {
|
|
231
|
-
const separatorIndex = textInput.indexOf('=');
|
|
223
|
+
const separatorIndex = textInput.value.indexOf('=');
|
|
232
224
|
if (separatorIndex <= 0) {
|
|
233
225
|
setState((s) => advancePresetCreateFlow(s, {
|
|
234
226
|
type: 'set-error',
|
|
@@ -236,8 +228,8 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
236
228
|
}));
|
|
237
229
|
return;
|
|
238
230
|
}
|
|
239
|
-
const k = textInput.slice(0, separatorIndex);
|
|
240
|
-
const v = textInput.slice(separatorIndex + 1);
|
|
231
|
+
const k = textInput.value.slice(0, separatorIndex);
|
|
232
|
+
const v = textInput.value.slice(separatorIndex + 1);
|
|
241
233
|
if (!/^[A-Z0-9_]+$/.test(k)) {
|
|
242
234
|
setState((s) => advancePresetCreateFlow(s, {
|
|
243
235
|
type: 'set-error',
|
|
@@ -250,35 +242,27 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
250
242
|
key: k,
|
|
251
243
|
value: v,
|
|
252
244
|
}));
|
|
253
|
-
|
|
245
|
+
textInput.reset();
|
|
254
246
|
return;
|
|
255
247
|
}
|
|
256
|
-
if (input
|
|
257
|
-
setTextInput((v) => v + input);
|
|
248
|
+
if (textInput.handleKey(input, key))
|
|
258
249
|
return;
|
|
259
|
-
}
|
|
260
250
|
}
|
|
261
251
|
if (state.step === 'name') {
|
|
262
252
|
if (input === 'q') {
|
|
263
253
|
exit();
|
|
264
254
|
return;
|
|
265
255
|
}
|
|
266
|
-
if (key.
|
|
267
|
-
setTextInput((v) => v.slice(0, -1));
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (key.return && textInput.trim().length > 0) {
|
|
256
|
+
if (key.return && textInput.value.trim().length > 0) {
|
|
271
257
|
setState((s) => advancePresetCreateFlow(s, {
|
|
272
258
|
type: 'set-name',
|
|
273
|
-
name: textInput.trim(),
|
|
259
|
+
name: textInput.value.trim(),
|
|
274
260
|
}));
|
|
275
261
|
setListCursor(0);
|
|
276
262
|
return;
|
|
277
263
|
}
|
|
278
|
-
if (input
|
|
279
|
-
setTextInput((v) => v + input);
|
|
264
|
+
if (textInput.handleKey(input, key))
|
|
280
265
|
return;
|
|
281
|
-
}
|
|
282
266
|
}
|
|
283
267
|
if (state.step === 'destination') {
|
|
284
268
|
if (input === 'q') {
|
|
@@ -326,7 +310,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
326
310
|
if (state.step === 'done') {
|
|
327
311
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", children: "Done" }) }));
|
|
328
312
|
}
|
|
329
|
-
return (_jsxs(Box, { flexDirection: "column", children: [state.step === 'detectedPrompt' && _jsx(DetectedPromptStep, { cursor: listCursor }), state.step === 'detected' && (_jsx(DetectedKeysStep, { keys: state.allKeys, selectedKeys: state.selectedKeys, requiredKeys: state.requiredKeys, cursor: listCursor })), state.step === 'source' && _jsx(SourceStep, { cursor: listCursor }), state.step === 'filePath' && (_jsx(FilePathStep, { value: textInput, ...(state.error ? { error: state.error } : {}) })), state.step === 'keys' && (_jsx(KeysStep, { keys: allKeys, selectedKeys: state.selectedKeys, cursor: listCursor })), state.step === 'manualInput' && (_jsx(ManualInputStep, { entries: state.selectedKeys.map((k) => [k, state.env[k] ?? '']), value: textInput, ...(state.error ? { error: state.error } : {}) })), state.step === 'name' && _jsx(NameStep, { value: textInput }), state.step === 'destination' && _jsx(DestinationStep, { cursor: listCursor }), state.step === 'confirm' && state.destination ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(EnvSummary, { title: `Preset: ${state.presetName}`, entries: Object.entries(state.env)
|
|
313
|
+
return (_jsxs(Box, { flexDirection: "column", children: [state.step === 'detectedPrompt' && _jsx(DetectedPromptStep, { cursor: listCursor }), state.step === 'detected' && (_jsx(DetectedKeysStep, { keys: state.allKeys, selectedKeys: state.selectedKeys, requiredKeys: state.requiredKeys, cursor: listCursor })), state.step === 'source' && _jsx(SourceStep, { cursor: listCursor }), state.step === 'filePath' && (_jsx(FilePathStep, { value: textInput.value, cursorPos: textInput.cursorPos, ...(state.error ? { error: state.error } : {}) })), state.step === 'keys' && (_jsx(KeysStep, { keys: allKeys, selectedKeys: state.selectedKeys, cursor: listCursor })), state.step === 'manualInput' && (_jsx(ManualInputStep, { entries: state.selectedKeys.map((k) => [k, state.env[k] ?? '']), value: textInput.value, cursorPos: textInput.cursorPos, ...(state.error ? { error: state.error } : {}) })), state.step === 'name' && _jsx(NameStep, { value: textInput.value, cursorPos: textInput.cursorPos }), state.step === 'destination' && _jsx(DestinationStep, { cursor: listCursor }), state.step === 'confirm' && state.destination ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(EnvSummary, { title: `Preset: ${state.presetName}`, entries: Object.entries(state.env)
|
|
330
314
|
.filter(([k]) => state.selectedKeys.includes(k))
|
|
331
315
|
.sort(([a], [b]) => a.localeCompare(b)), mask: true, ...(state.filePath ? { fromFiles: [state.filePath] } : {}), toFiles: [
|
|
332
316
|
state.destination === 'global'
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
import { useTextInput } from './hooks/use-text-input.js';
|
|
5
|
+
import { TextInputDisplay } from './components/text-input.js';
|
|
4
6
|
export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
|
|
5
7
|
const { exit } = useApp();
|
|
6
8
|
const [entries, setEntries] = useState(Object.entries(initialEnv));
|
|
7
9
|
const [cursor, setCursor] = useState(0);
|
|
8
10
|
const [editing, setEditing] = useState(null);
|
|
9
|
-
const
|
|
11
|
+
const textInput = useTextInput();
|
|
10
12
|
const [error, setError] = useState();
|
|
11
13
|
const [step, setStep] = useState('list');
|
|
12
14
|
useInput((input, key) => {
|
|
13
15
|
if (key.escape || input === 'q') {
|
|
14
16
|
if (editing !== null) {
|
|
15
17
|
setEditing(null);
|
|
16
|
-
|
|
18
|
+
textInput.reset();
|
|
17
19
|
setError(undefined);
|
|
18
20
|
return;
|
|
19
21
|
}
|
|
@@ -33,7 +35,7 @@ export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
|
|
|
33
35
|
if (key.return && entries.length > 0) {
|
|
34
36
|
const entry = entries[cursor];
|
|
35
37
|
if (entry) {
|
|
36
|
-
|
|
38
|
+
textInput.reset(`${entry[0]}=${entry[1]}`);
|
|
37
39
|
setEditing(cursor);
|
|
38
40
|
setError(undefined);
|
|
39
41
|
}
|
|
@@ -45,7 +47,7 @@ export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
|
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
47
49
|
if (input === 'a') {
|
|
48
|
-
|
|
50
|
+
textInput.reset();
|
|
49
51
|
setEditing(entries.length);
|
|
50
52
|
setError(undefined);
|
|
51
53
|
return;
|
|
@@ -56,18 +58,14 @@ export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
|
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
if (editing !== null) {
|
|
59
|
-
if (key.backspace || key.delete) {
|
|
60
|
-
setTextInput((v) => v.slice(0, -1));
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
61
|
if (key.return) {
|
|
64
|
-
const sep = textInput.indexOf('=');
|
|
62
|
+
const sep = textInput.value.indexOf('=');
|
|
65
63
|
if (sep <= 0) {
|
|
66
64
|
setError('Format must be KEY=VALUE');
|
|
67
65
|
return;
|
|
68
66
|
}
|
|
69
|
-
const k = textInput.slice(0, sep);
|
|
70
|
-
const v = textInput.slice(sep + 1);
|
|
67
|
+
const k = textInput.value.slice(0, sep);
|
|
68
|
+
const v = textInput.value.slice(sep + 1);
|
|
71
69
|
if (!/^[A-Z0-9_]+$/.test(k)) {
|
|
72
70
|
setError('Key must match [A-Z0-9_]+');
|
|
73
71
|
return;
|
|
@@ -83,14 +81,12 @@ export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
|
|
|
83
81
|
return next;
|
|
84
82
|
});
|
|
85
83
|
setEditing(null);
|
|
86
|
-
|
|
84
|
+
textInput.reset();
|
|
87
85
|
setError(undefined);
|
|
88
86
|
return;
|
|
89
87
|
}
|
|
90
|
-
if (input
|
|
91
|
-
setTextInput((v) => v + input);
|
|
88
|
+
if (textInput.handleKey(input, key))
|
|
92
89
|
return;
|
|
93
|
-
}
|
|
94
90
|
}
|
|
95
91
|
if (step === 'confirm') {
|
|
96
92
|
if (key.return) {
|
|
@@ -108,5 +104,5 @@ export function PresetEditApp({ name, env: initialEnv, onSubmit }) {
|
|
|
108
104
|
if (step === 'confirm') {
|
|
109
105
|
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
106
|
}
|
|
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' }),
|
|
107
|
+
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' }), _jsx(TextInputDisplay, { value: textInput.value, cursorPos: textInput.cursorPos }), error ? _jsx(Text, { color: "red", children: error }) : null] }))] }));
|
|
112
108
|
}
|
|
@@ -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 (
|
|
14
|
-
|
|
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 (
|
|
18
|
-
|
|
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 (
|
|
22
|
-
|
|
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
|
-
|
|
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
|
+
"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": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"node": ">=20.19.2"
|
|
21
21
|
},
|
|
22
22
|
"bin": {
|
|
23
|
-
"cc-env": "dist/cli.js"
|
|
23
|
+
"cc-env": "dist/cli.js",
|
|
24
|
+
"ccenv": "dist/cli.js"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"dist",
|
|
@@ -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
|
-
}
|