@lkangd/cc-env 1.2.0 → 1.3.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 +110 -0
- package/README.zh.md +110 -0
- package/dist/cli.js +105 -77
- package/dist/commands/init.js +2 -9
- package/dist/commands/preset/create.js +70 -9
- package/dist/commands/restore.js +25 -1
- package/dist/commands/run.js +27 -22
- package/dist/core/claude-required-keys.js +8 -0
- package/dist/core/schema.js +10 -0
- package/dist/flows/preset-create-flow.js +49 -1
- package/dist/flows/restore-flow.js +26 -7
- package/dist/ink/preset-create-app.js +68 -3
- package/dist/ink/restore-app.js +24 -9
- package/package.json +4 -3
- package/dist/commands/debug.js +0 -17
- package/dist/commands/preset/list.js +0 -16
- package/dist/core/lock.js +0 -25
- package/dist/ink/init-app.js +0 -54
- package/dist/ink/preset-list-app.js +0 -27
- package/dist/services/config-service.js +0 -26
- package/dist/services/runtime-env-service.js +0 -13
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./statics/logo.png" alt="cc-env" width="320" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@lkangd/cc-env"><img src="https://img.shields.io/npm/v/@lkangd/cc-env.svg" alt="npm version" /></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/@lkangd/cc-env"><img src="https://img.shields.io/npm/dm/@lkangd/cc-env.svg" alt="npm downloads" /></a>
|
|
8
|
+
<a href="https://github.com/lkangd/cc-env/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@lkangd/cc-env.svg" alt="license" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">Manage runtime environment variables for <a href="https://claude.ai/code">Claude Code</a></p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="./README.md">English</a> | <a href="./README.zh.md">简体中文</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
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
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
### via npm
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g @lkangd/cc-env
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Node.js `>=20.19.2`.
|
|
32
|
+
|
|
33
|
+
### via Homebrew
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
brew tap lkangd/tap
|
|
37
|
+
brew install cc-env
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 1. Initialize cc-env in your project
|
|
44
|
+
cc-env init
|
|
45
|
+
|
|
46
|
+
# 2. Create a preset with your environment variables
|
|
47
|
+
cc-env create
|
|
48
|
+
|
|
49
|
+
# 3. Run Claude Code with the preset applied
|
|
50
|
+
cc-env run
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
| Command | Description |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `cc-env init` | Initialize cc-env for the current project |
|
|
58
|
+
| `cc-env run [args...]` | Run Claude Code with merged environment variables |
|
|
59
|
+
| `cc-env restore` | Restore environment variables from a previous snapshot |
|
|
60
|
+
| `cc-env show` | List and view all saved presets |
|
|
61
|
+
| `cc-env create` | Create a new environment preset |
|
|
62
|
+
| `cc-env edit <name>` | Edit an existing preset |
|
|
63
|
+
| `cc-env rename <from> <to>` | Rename a preset |
|
|
64
|
+
| `cc-env delete` | Delete a saved preset |
|
|
65
|
+
| `cc-env doctor` | Check system health and configuration |
|
|
66
|
+
| `cc-env completion` | Generate shell completion script |
|
|
67
|
+
|
|
68
|
+
## Global Options
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
--verbose Enable verbose output
|
|
72
|
+
--quiet Suppress non-essential output
|
|
73
|
+
--no-interactive Disable interactive prompts (equivalent to -y)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Shell Completion
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# bash
|
|
80
|
+
cc-env completion bash >> ~/.bashrc
|
|
81
|
+
|
|
82
|
+
# zsh
|
|
83
|
+
cc-env completion zsh >> ~/.zshrc
|
|
84
|
+
|
|
85
|
+
# fish
|
|
86
|
+
cc-env completion fish >> ~/.config/fish/completions/cc-env.fish
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Install dependencies
|
|
93
|
+
npm install
|
|
94
|
+
|
|
95
|
+
# Run in dev mode
|
|
96
|
+
npm run dev
|
|
97
|
+
|
|
98
|
+
# Build
|
|
99
|
+
npm run build
|
|
100
|
+
|
|
101
|
+
# Run tests
|
|
102
|
+
npm test
|
|
103
|
+
|
|
104
|
+
# Run tests with coverage
|
|
105
|
+
npm run test:coverage
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
ISC © [lkangd](https://github.com/lkangd)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./statics/logo.png" alt="cc-env" width="320" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@lkangd/cc-env"><img src="https://img.shields.io/npm/v/@lkangd/cc-env.svg" alt="npm version" /></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/@lkangd/cc-env"><img src="https://img.shields.io/npm/dm/@lkangd/cc-env.svg" alt="npm downloads" /></a>
|
|
8
|
+
<a href="https://github.com/lkangd/cc-env/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@lkangd/cc-env.svg" alt="license" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">为 <a href="https://claude.ai/code">Claude Code</a> 管理运行时环境变量</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="./README.md">English</a> | <a href="./README.zh.md">简体中文</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 概述
|
|
20
|
+
|
|
21
|
+
`cc-env` 是一个 CLI 工具,让你可以为 Claude Code 定义、切换和恢复环境变量配置——支持按项目配置或使用可复用的预设。不再需要手动编辑 `settings.json` 或在不同工作区之间切换 `.env` 文件。
|
|
22
|
+
|
|
23
|
+
## 安装
|
|
24
|
+
|
|
25
|
+
### 通过 npm
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g @lkangd/cc-env
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
需要 Node.js `>=20.19.2`。
|
|
32
|
+
|
|
33
|
+
### 通过 Homebrew
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
brew tap lkangd/tap
|
|
37
|
+
brew install cc-env
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 快速开始
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 1. 在项目中初始化 cc-env
|
|
44
|
+
cc-env init
|
|
45
|
+
|
|
46
|
+
# 2. 创建一个包含环境变量的预设
|
|
47
|
+
cc-env create
|
|
48
|
+
|
|
49
|
+
# 3. 使用预设运行 Claude Code
|
|
50
|
+
cc-env run
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 命令
|
|
54
|
+
|
|
55
|
+
| 命令 | 说明 |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `cc-env init` | 为当前项目初始化 cc-env |
|
|
58
|
+
| `cc-env run [args...]` | 使用合并后的环境变量运行 Claude Code |
|
|
59
|
+
| `cc-env restore` | 从之前的快照恢复环境变量 |
|
|
60
|
+
| `cc-env show` | 列出并查看所有保存的预设 |
|
|
61
|
+
| `cc-env create` | 创建新的环境变量预设 |
|
|
62
|
+
| `cc-env edit <name>` | 编辑现有预设 |
|
|
63
|
+
| `cc-env rename <from> <to>` | 重命名预设 |
|
|
64
|
+
| `cc-env delete` | 删除保存的预设 |
|
|
65
|
+
| `cc-env doctor` | 检查系统健康状况和配置 |
|
|
66
|
+
| `cc-env completion` | 生成 shell 补全脚本 |
|
|
67
|
+
|
|
68
|
+
## 全局选项
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
--verbose 启用详细输出
|
|
72
|
+
--quiet 抑制非必要输出
|
|
73
|
+
--no-interactive 禁用交互式提示(等同于 -y)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Shell 补全
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# bash
|
|
80
|
+
cc-env completion bash >> ~/.bashrc
|
|
81
|
+
|
|
82
|
+
# zsh
|
|
83
|
+
cc-env completion zsh >> ~/.zshrc
|
|
84
|
+
|
|
85
|
+
# fish
|
|
86
|
+
cc-env completion fish >> ~/.config/fish/completions/cc-env.fish
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 开发
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# 安装依赖
|
|
93
|
+
npm install
|
|
94
|
+
|
|
95
|
+
# 开发模式运行
|
|
96
|
+
npm run dev
|
|
97
|
+
|
|
98
|
+
# 构建
|
|
99
|
+
npm run build
|
|
100
|
+
|
|
101
|
+
# 运行测试
|
|
102
|
+
npm test
|
|
103
|
+
|
|
104
|
+
# 运行测试并生成覆盖率报告
|
|
105
|
+
npm run test:coverage
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 许可证
|
|
109
|
+
|
|
110
|
+
ISC © [lkangd](https://github.com/lkangd)
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,6 @@ import gradient from 'gradient-string';
|
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import packageJson from '../package.json' with { type: 'json' };
|
|
9
9
|
const h = React.createElement;
|
|
10
|
-
import { createInitCommand } from './commands/init.js';
|
|
11
10
|
import { createPresetCreateCommand } from './commands/preset/create.js';
|
|
12
11
|
import { createDeletePresetCommand } from './commands/preset/delete.js';
|
|
13
12
|
import { createEditPresetCommand } from './commands/preset/edit.js';
|
|
@@ -19,7 +18,6 @@ import { createRestoreCommand } from './commands/restore.js';
|
|
|
19
18
|
import { createRunCommand } from './commands/run.js';
|
|
20
19
|
import { runDoctorCommand } from './commands/doctor.js';
|
|
21
20
|
import { findClaudeExecutable } from './core/find-claude.js';
|
|
22
|
-
import { InitApp } from './ink/init-app.js';
|
|
23
21
|
import { renderEnvSummary } from './ink/summary.js';
|
|
24
22
|
import { PresetCreateApp } from './ink/preset-create-app.js';
|
|
25
23
|
import { PresetShowApp } from './ink/preset-show-app.js';
|
|
@@ -54,8 +52,80 @@ const shellEnvService = createShellEnvService({ homeDir });
|
|
|
54
52
|
const projectEnvService = createProjectEnvService({ cwd });
|
|
55
53
|
const presetService = createPresetService(globalRoot);
|
|
56
54
|
const historyService = createHistoryService(globalRoot);
|
|
55
|
+
const projectStateService = createProjectStateService(globalRoot);
|
|
56
|
+
async function runPresetCreateFlow({ detectedEnv, requiredKeys }) {
|
|
57
|
+
let result;
|
|
58
|
+
const app = render(h(PresetCreateApp, {
|
|
59
|
+
onSubmit: value => {
|
|
60
|
+
result = value;
|
|
61
|
+
},
|
|
62
|
+
readFile: async (filePath) => {
|
|
63
|
+
const { readEnvFile } = await import('./commands/preset/create.js');
|
|
64
|
+
return readEnvFile(filePath);
|
|
65
|
+
},
|
|
66
|
+
globalPresetPath: name => presetService.getPath(name),
|
|
67
|
+
projectEnvPath: join(cwd, '.cc-env', 'env.json'),
|
|
68
|
+
detectedEnv,
|
|
69
|
+
requiredKeys,
|
|
70
|
+
}));
|
|
71
|
+
await app.waitUntilExit();
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
async function runWithBootstrap({ args = [], dryRun = false, yes = false, json = false, skipDetect = false, }) {
|
|
75
|
+
const result = await createRunCommand({
|
|
76
|
+
claudeSettingsEnvService,
|
|
77
|
+
presetService,
|
|
78
|
+
projectEnvService,
|
|
79
|
+
projectStateService,
|
|
80
|
+
findClaude: findClaudeExecutable,
|
|
81
|
+
renderPresetSelect: async ({ presets, defaultIndex }) => {
|
|
82
|
+
let selected;
|
|
83
|
+
const app = render(h(RunPresetSelectApp, {
|
|
84
|
+
presets,
|
|
85
|
+
defaultIndex,
|
|
86
|
+
onSubmit: preset => {
|
|
87
|
+
selected = preset;
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
await app.waitUntilExit();
|
|
91
|
+
return selected;
|
|
92
|
+
},
|
|
93
|
+
spawnCommand
|
|
94
|
+
})({
|
|
95
|
+
args,
|
|
96
|
+
dryRun,
|
|
97
|
+
yes,
|
|
98
|
+
json,
|
|
99
|
+
skipDetect,
|
|
100
|
+
cwd,
|
|
101
|
+
});
|
|
102
|
+
if (!result || result.status === 'executed') {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (Object.keys(result.detectedEnv).length === 0) {
|
|
106
|
+
throw new CliError('No presets found and no migratable Claude settings were detected.');
|
|
107
|
+
}
|
|
108
|
+
const createdPreset = await createPresetCreateCommand({
|
|
109
|
+
presetService,
|
|
110
|
+
projectEnvService,
|
|
111
|
+
claudeSettingsEnvService,
|
|
112
|
+
historyService,
|
|
113
|
+
renderFlow: runPresetCreateFlow,
|
|
114
|
+
})({ cwd });
|
|
115
|
+
if (!createdPreset) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
await projectStateService.saveLastPreset(cwd, createdPreset);
|
|
119
|
+
await runWithBootstrap({
|
|
120
|
+
args,
|
|
121
|
+
dryRun,
|
|
122
|
+
yes: true,
|
|
123
|
+
json,
|
|
124
|
+
skipDetect: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
57
127
|
async function runRestoreFlow(context) {
|
|
58
|
-
const state = createRestoreFlowState(context.records);
|
|
128
|
+
const state = createRestoreFlowState(context.records, cwd);
|
|
59
129
|
const firstRecord = context.records[0];
|
|
60
130
|
if (!firstRecord) {
|
|
61
131
|
render(h(RestoreApp, { state }));
|
|
@@ -66,16 +136,19 @@ async function runRestoreFlow(context) {
|
|
|
66
136
|
type: 'select-record',
|
|
67
137
|
timestamp: firstRecord.timestamp
|
|
68
138
|
});
|
|
69
|
-
if (firstRecord.action === 'init') {
|
|
139
|
+
if (firstRecord.action === 'init' || firstRecord.action === 'preset-create') {
|
|
70
140
|
const doneState = advanceRestoreFlow(selectedRecordState, { type: 'confirm' });
|
|
71
141
|
if (doneState.step !== 'done') {
|
|
72
142
|
return undefined;
|
|
73
143
|
}
|
|
74
144
|
return {
|
|
75
145
|
confirmed: true,
|
|
76
|
-
timestamp: firstRecord.timestamp
|
|
146
|
+
timestamp: firstRecord.timestamp,
|
|
77
147
|
};
|
|
78
148
|
}
|
|
149
|
+
if (firstRecord.action !== 'restore') {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
79
152
|
const confirmState = advanceRestoreFlow(selectedRecordState, {
|
|
80
153
|
type: 'select-target',
|
|
81
154
|
targetType: firstRecord.targetType,
|
|
@@ -94,7 +167,7 @@ async function runRestoreFlow(context) {
|
|
|
94
167
|
return {
|
|
95
168
|
confirmed: true,
|
|
96
169
|
timestamp: doneState.selectedTimestamp,
|
|
97
|
-
targetType: doneState.targetType
|
|
170
|
+
...(doneState.targetType ? { targetType: doneState.targetType } : {}),
|
|
98
171
|
};
|
|
99
172
|
}
|
|
100
173
|
return undefined;
|
|
@@ -125,62 +198,13 @@ program
|
|
|
125
198
|
.option('--json', 'Output as JSON (only with --dry-run)')
|
|
126
199
|
.action((args, options) => {
|
|
127
200
|
const rawArgs = args ?? [];
|
|
128
|
-
return
|
|
129
|
-
claudeSettingsEnvService,
|
|
130
|
-
presetService,
|
|
131
|
-
projectEnvService,
|
|
132
|
-
projectStateService: createProjectStateService(globalRoot),
|
|
133
|
-
findClaude: findClaudeExecutable,
|
|
134
|
-
renderPresetSelect: async ({ presets, defaultIndex }) => {
|
|
135
|
-
let result;
|
|
136
|
-
const app = render(h(RunPresetSelectApp, {
|
|
137
|
-
presets,
|
|
138
|
-
defaultIndex,
|
|
139
|
-
onSubmit: preset => {
|
|
140
|
-
result = preset;
|
|
141
|
-
}
|
|
142
|
-
}));
|
|
143
|
-
await app.waitUntilExit();
|
|
144
|
-
return result;
|
|
145
|
-
},
|
|
146
|
-
spawnCommand
|
|
147
|
-
})({
|
|
201
|
+
return runWithBootstrap({
|
|
148
202
|
args: rawArgs,
|
|
149
203
|
dryRun: options.dryRun ?? false,
|
|
150
204
|
yes: options.yes ?? false,
|
|
151
205
|
json: options.json ?? false,
|
|
152
|
-
cwd
|
|
153
206
|
});
|
|
154
207
|
});
|
|
155
|
-
program
|
|
156
|
-
.command('init')
|
|
157
|
-
.description('Initialize cc-env for the current project')
|
|
158
|
-
.option('-y, --yes', 'Accept all defaults without interactive prompts')
|
|
159
|
-
.action(options => createInitCommand({
|
|
160
|
-
claudeSettingsEnvService,
|
|
161
|
-
shellEnvService,
|
|
162
|
-
historyService,
|
|
163
|
-
renderEnvSummary,
|
|
164
|
-
renderFlow: async (context) => {
|
|
165
|
-
if (context.yes) {
|
|
166
|
-
return {
|
|
167
|
-
selectedKeys: context.requiredKeys,
|
|
168
|
-
confirmed: true
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
let result;
|
|
172
|
-
const app = render(h(InitApp, {
|
|
173
|
-
...context,
|
|
174
|
-
onSubmit: value => {
|
|
175
|
-
result = value;
|
|
176
|
-
}
|
|
177
|
-
}));
|
|
178
|
-
await app.waitUntilExit();
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
})({
|
|
182
|
-
yes: options.yes
|
|
183
|
-
}));
|
|
184
208
|
program
|
|
185
209
|
.command('restore')
|
|
186
210
|
.description('Restore environment variables from a previous snapshot')
|
|
@@ -233,26 +257,15 @@ program
|
|
|
233
257
|
program
|
|
234
258
|
.command('create')
|
|
235
259
|
.description('Create a new environment preset')
|
|
236
|
-
.action(() =>
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
readFile: async (filePath) => {
|
|
246
|
-
const { readEnvFile } = await import('./commands/preset/create.js');
|
|
247
|
-
return readEnvFile(filePath);
|
|
248
|
-
},
|
|
249
|
-
globalPresetPath: name => presetService.getPath(name),
|
|
250
|
-
projectEnvPath: join(cwd, '.cc-env', 'env.json')
|
|
251
|
-
}));
|
|
252
|
-
await app.waitUntilExit();
|
|
253
|
-
return result;
|
|
254
|
-
}
|
|
255
|
-
})({ cwd }));
|
|
260
|
+
.action(async () => {
|
|
261
|
+
await createPresetCreateCommand({
|
|
262
|
+
presetService,
|
|
263
|
+
projectEnvService,
|
|
264
|
+
claudeSettingsEnvService,
|
|
265
|
+
historyService,
|
|
266
|
+
renderFlow: runPresetCreateFlow,
|
|
267
|
+
})({ cwd });
|
|
268
|
+
});
|
|
256
269
|
program
|
|
257
270
|
.command('doctor')
|
|
258
271
|
.description('Check system health and configuration')
|
|
@@ -306,7 +319,22 @@ program.hook('preAction', (thisCommand) => {
|
|
|
306
319
|
thisCommand.setOptionValue('yes', true);
|
|
307
320
|
}
|
|
308
321
|
});
|
|
309
|
-
|
|
322
|
+
async function main() {
|
|
323
|
+
const args = process.argv.slice(2);
|
|
324
|
+
if (args.length === 0) {
|
|
325
|
+
const hasGlobalPreset = (await presetService.listNames()).length > 0;
|
|
326
|
+
const { env: projectEnv } = await projectEnvService.readWithMeta();
|
|
327
|
+
if (hasGlobalPreset || Object.keys(projectEnv).length > 0) {
|
|
328
|
+
await runWithBootstrap({ args: [], yes: !process.stdin.isTTY });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
program.outputHelp();
|
|
332
|
+
process.exitCode = 0;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
await program.parseAsync(process.argv);
|
|
336
|
+
}
|
|
337
|
+
main().catch((error) => {
|
|
310
338
|
if (error instanceof CliError) {
|
|
311
339
|
process.stderr.write(`\n Error: ${error.message}\n\n`);
|
|
312
340
|
process.exitCode = error.exitCode;
|
package/dist/commands/init.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { requiredClaudeKeys } from '../core/claude-required-keys.js';
|
|
3
4
|
import { CliError } from '../core/errors.js';
|
|
4
5
|
import { envMapSchema } from '../core/schema.js';
|
|
5
6
|
const h = React.createElement;
|
|
6
|
-
const requiredInitKeys = [
|
|
7
|
-
'ANTHROPIC_AUTH_TOKEN',
|
|
8
|
-
'ANTHROPIC_BASE_URL',
|
|
9
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
10
|
-
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
11
|
-
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
12
|
-
'ANTHROPIC_REASONING_MODEL',
|
|
13
|
-
];
|
|
14
7
|
function omitKeys(env, keys) {
|
|
15
8
|
return envMapSchema.parse(Object.fromEntries(Object.entries(env).filter(([key]) => !keys.includes(key))));
|
|
16
9
|
}
|
|
@@ -22,7 +15,7 @@ export function createInitCommand({ claudeSettingsEnvService, shellEnvService, h
|
|
|
22
15
|
}
|
|
23
16
|
const effectiveEnv = envMapSchema.parse(sources.reduce((acc, source) => ({ ...acc, ...source.env }), {}));
|
|
24
17
|
const keys = Object.keys(effectiveEnv).sort();
|
|
25
|
-
const requiredKeys =
|
|
18
|
+
const requiredKeys = requiredClaudeKeys.filter((key) => key in effectiveEnv);
|
|
26
19
|
const sourceFiles = sources.map((s) => s.path);
|
|
27
20
|
const result = await renderFlow({ keys, requiredKeys, yes, sourceFiles });
|
|
28
21
|
if (!result?.confirmed) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { extname } from 'node:path';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { requiredClaudeKeys } from '../../core/claude-required-keys.js';
|
|
4
5
|
import { CliError } from '../../core/errors.js';
|
|
5
6
|
import { ensureGitignoreEntry } from '../../core/gitignore.js';
|
|
7
|
+
import { envMapSchema } from '../../core/schema.js';
|
|
6
8
|
import { toProcessEnvMap } from '../../core/process-env.js';
|
|
7
9
|
export async function readEnvFile(filePath) {
|
|
8
10
|
try {
|
|
@@ -36,9 +38,44 @@ export async function readEnvFile(filePath) {
|
|
|
36
38
|
throw new CliError(`Failed to read env file: ${filePath}`, 2);
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
|
-
|
|
41
|
+
function getDetectedEnv(sources) {
|
|
42
|
+
return toProcessEnvMap(sources.reduce((acc, source) => ({ ...acc, ...source.env }), {}));
|
|
43
|
+
}
|
|
44
|
+
function omitKeys(env, keys) {
|
|
45
|
+
return envMapSchema.parse(Object.fromEntries(Object.entries(env).filter(([key]) => !keys.includes(key))));
|
|
46
|
+
}
|
|
47
|
+
function buildSourceBackups(sources, selectedKeys, selectedEnv) {
|
|
48
|
+
const backups = new Map();
|
|
49
|
+
for (const source of sources) {
|
|
50
|
+
backups.set(source.path, envMapSchema.parse({}));
|
|
51
|
+
}
|
|
52
|
+
for (const key of selectedKeys) {
|
|
53
|
+
for (const source of [...sources].reverse()) {
|
|
54
|
+
if (!(key in source.env)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (source.env[key] !== selectedEnv[key]) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const current = backups.get(source.path) ?? envMapSchema.parse({});
|
|
61
|
+
backups.set(source.path, envMapSchema.parse({
|
|
62
|
+
...current,
|
|
63
|
+
[key]: source.env[key],
|
|
64
|
+
}));
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return sources.map((source) => ({
|
|
69
|
+
file: source.path,
|
|
70
|
+
backup: backups.get(source.path) ?? envMapSchema.parse({}),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
export function createPresetCreateCommand({ presetService, projectEnvService, claudeSettingsEnvService, historyService, renderFlow, ensureGitignore = (dir, entry) => ensureGitignoreEntry(dir, entry), }) {
|
|
40
74
|
return async function createPreset({ cwd }) {
|
|
41
|
-
const
|
|
75
|
+
const sources = claudeSettingsEnvService ? await claudeSettingsEnvService.read() : [];
|
|
76
|
+
const detectedEnv = claudeSettingsEnvService ? getDetectedEnv(sources) : {};
|
|
77
|
+
const requiredKeys = requiredClaudeKeys.filter((key) => key in detectedEnv);
|
|
78
|
+
const result = await renderFlow({ detectedEnv, requiredKeys });
|
|
42
79
|
if (!result)
|
|
43
80
|
return;
|
|
44
81
|
const selectedEnv = {};
|
|
@@ -46,16 +83,40 @@ export function createPresetCreateCommand({ presetService, projectEnvService, re
|
|
|
46
83
|
selectedEnv[key] = result.env[key] ?? '';
|
|
47
84
|
}
|
|
48
85
|
const timestamp = new Date().toISOString();
|
|
86
|
+
const selectedKeys = result.selectedKeys;
|
|
87
|
+
const sourceBackups = result.source === 'detected'
|
|
88
|
+
? buildSourceBackups(sources, selectedKeys, selectedEnv)
|
|
89
|
+
: [];
|
|
49
90
|
if (result.destination === 'project') {
|
|
50
91
|
await projectEnvService.write(selectedEnv, { name: result.presetName, createdAt: timestamp, updatedAt: timestamp });
|
|
51
92
|
await ensureGitignore(cwd, '.cc-env');
|
|
52
|
-
return;
|
|
53
93
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
else {
|
|
95
|
+
await presetService.write({
|
|
96
|
+
name: result.presetName,
|
|
97
|
+
createdAt: timestamp,
|
|
98
|
+
updatedAt: timestamp,
|
|
99
|
+
env: selectedEnv,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (result.source === 'detected' && claudeSettingsEnvService && historyService) {
|
|
103
|
+
await historyService.write({
|
|
104
|
+
timestamp,
|
|
105
|
+
action: 'preset-create',
|
|
106
|
+
projectPath: cwd,
|
|
107
|
+
presetName: result.presetName,
|
|
108
|
+
destination: result.destination,
|
|
109
|
+
migratedKeys: selectedKeys,
|
|
110
|
+
sources: sourceBackups,
|
|
111
|
+
});
|
|
112
|
+
await claudeSettingsEnvService.write(sources.map((source) => ({
|
|
113
|
+
path: source.path,
|
|
114
|
+
env: omitKeys(source.env, Object.keys(sourceBackups.find((entry) => entry.file === source.path)?.backup ?? {})),
|
|
115
|
+
})));
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
presetName: result.presetName,
|
|
119
|
+
source: result.destination,
|
|
120
|
+
};
|
|
60
121
|
};
|
|
61
122
|
}
|
package/dist/commands/restore.js
CHANGED
|
@@ -2,9 +2,12 @@ import React from 'react';
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { CliError } from '../core/errors.js';
|
|
4
4
|
const h = React.createElement;
|
|
5
|
+
function isRestorableRecord(record) {
|
|
6
|
+
return record.action === 'init' || record.action === 'restore' || record.action === 'preset-create';
|
|
7
|
+
}
|
|
5
8
|
export function createRestoreCommand({ historyService, claudeSettingsEnvService, shellEnvService, settingsEnvService, presetService, renderFlow, renderEnvSummary, }) {
|
|
6
9
|
return async function restore({ yes = false } = {}) {
|
|
7
|
-
const records = await historyService.list();
|
|
10
|
+
const records = (await historyService.list()).filter(isRestorableRecord);
|
|
8
11
|
const result = await renderFlow({ records, yes });
|
|
9
12
|
if (!result?.confirmed) {
|
|
10
13
|
return;
|
|
@@ -33,6 +36,27 @@ export function createRestoreCommand({ historyService, claudeSettingsEnvService,
|
|
|
33
36
|
});
|
|
34
37
|
return;
|
|
35
38
|
}
|
|
39
|
+
if (record.action === 'preset-create') {
|
|
40
|
+
const mergedBackup = Object.fromEntries(record.sources.flatMap((source) => Object.entries(source.backup)));
|
|
41
|
+
const current = await claudeSettingsEnvService.read();
|
|
42
|
+
await claudeSettingsEnvService.write(current.map((source) => ({
|
|
43
|
+
path: source.path,
|
|
44
|
+
env: {
|
|
45
|
+
...source.env,
|
|
46
|
+
...(record.sources.find((entry) => entry.file === source.path)?.backup ?? {}),
|
|
47
|
+
},
|
|
48
|
+
})));
|
|
49
|
+
await renderEnvSummary({
|
|
50
|
+
title: `Restored from detected preset ${record.presetName}`,
|
|
51
|
+
env: mergedBackup,
|
|
52
|
+
toFiles: record.sources.map((source) => source.file),
|
|
53
|
+
footer: h(Box, { flexDirection: 'column' }, h(Text, { color: 'green' }, 'Restore complete'), h(Text, { bold: true, color: 'green' }, 'Please restart your terminal for the restored environment variables to take effect.')),
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (record.action !== 'restore') {
|
|
58
|
+
throw new CliError('Restore record type is not supported');
|
|
59
|
+
}
|
|
36
60
|
if (result.targetType === 'settings') {
|
|
37
61
|
const currentSettings = await settingsEnvService.read();
|
|
38
62
|
await settingsEnvService.write({
|
package/dist/commands/run.js
CHANGED
|
@@ -1,38 +1,42 @@
|
|
|
1
|
-
import { CliError } from '../core/errors.js';
|
|
2
1
|
import { formatRunEnvBlock } from '../core/format.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
];
|
|
11
|
-
export function createRunCommand({ claudeSettingsEnvService, presetService, projectEnvService, projectStateService, findClaude, renderPresetSelect, spawnCommand, stdout = process.stdout, }) {
|
|
12
|
-
return async function run({ args = [], dryRun = false, yes = false, json = false, cwd, }) {
|
|
13
|
-
// Step 0: Check settings files for init-managed keys
|
|
2
|
+
import { requiredClaudeKeys } from '../core/claude-required-keys.js';
|
|
3
|
+
const detectTriggerKeys = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL'];
|
|
4
|
+
function getDetectedEnv(sources) {
|
|
5
|
+
return sources.reduce((acc, source) => ({ ...acc, ...source.env }), {});
|
|
6
|
+
}
|
|
7
|
+
export function createRunCommand({ claudeSettingsEnvService, presetService, projectEnvService, projectStateService, findClaude, renderPresetSelect, spawnCommand, stdout = process.stdout }) {
|
|
8
|
+
return async function run({ args = [], dryRun = false, yes = false, json = false, skipDetect = false, cwd }) {
|
|
14
9
|
const sources = await claudeSettingsEnvService.read();
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
const detectedEnv = getDetectedEnv(sources);
|
|
11
|
+
const requiredKeys = requiredClaudeKeys.filter((key) => key in detectedEnv);
|
|
12
|
+
const hasDetectTrigger = detectTriggerKeys.some((key) => key in detectedEnv);
|
|
13
|
+
if (!skipDetect && hasDetectTrigger) {
|
|
14
|
+
return {
|
|
15
|
+
status: 'needs-preset',
|
|
16
|
+
detectedEnv,
|
|
17
|
+
requiredKeys,
|
|
18
|
+
};
|
|
19
19
|
}
|
|
20
20
|
// Step 1: Collect all presets (project + global)
|
|
21
21
|
const names = await presetService.listNames();
|
|
22
|
-
const globalPresets = await Promise.all(names.map(
|
|
22
|
+
const globalPresets = await Promise.all(names.map(name => presetService.read(name).then(p => ({ name, env: p.env, source: 'global' }))));
|
|
23
23
|
const { env: projectEnv, name: projectName } = await projectEnvService.readWithMeta();
|
|
24
24
|
const projectPreset = Object.keys(projectEnv).length > 0
|
|
25
25
|
? [{ name: projectName ?? 'project', env: projectEnv, source: 'project' }]
|
|
26
26
|
: [];
|
|
27
27
|
const presets = [...projectPreset, ...globalPresets];
|
|
28
28
|
if (presets.length === 0) {
|
|
29
|
-
|
|
29
|
+
return {
|
|
30
|
+
status: 'needs-preset',
|
|
31
|
+
detectedEnv,
|
|
32
|
+
requiredKeys,
|
|
33
|
+
};
|
|
30
34
|
}
|
|
31
35
|
// Step 2: Determine default selection
|
|
32
36
|
const savedRef = await projectStateService.getLastPreset(cwd);
|
|
33
37
|
let defaultIndex = 0;
|
|
34
38
|
if (savedRef) {
|
|
35
|
-
const idx = presets.findIndex(
|
|
39
|
+
const idx = presets.findIndex(p => p.name === savedRef.presetName && p.source === savedRef.source);
|
|
36
40
|
if (idx >= 0)
|
|
37
41
|
defaultIndex = idx;
|
|
38
42
|
}
|
|
@@ -52,7 +56,7 @@ export function createRunCommand({ claudeSettingsEnvService, presetService, proj
|
|
|
52
56
|
// Step 4: Save selection
|
|
53
57
|
await projectStateService.saveLastPreset(cwd, {
|
|
54
58
|
presetName: selected.name,
|
|
55
|
-
source: selected.source
|
|
59
|
+
source: selected.source
|
|
56
60
|
});
|
|
57
61
|
// Step 5: Resolve claude command
|
|
58
62
|
let command;
|
|
@@ -72,7 +76,7 @@ export function createRunCommand({ claudeSettingsEnvService, presetService, proj
|
|
|
72
76
|
command: [command, ...claudeArgs],
|
|
73
77
|
env: selected.env
|
|
74
78
|
}, null, 2) + '\n');
|
|
75
|
-
return;
|
|
79
|
+
return { status: 'executed' };
|
|
76
80
|
}
|
|
77
81
|
const presetKeys = new Set(Object.keys(selected.env));
|
|
78
82
|
const envBlock = formatRunEnvBlock(selected.env, presetKeys);
|
|
@@ -80,9 +84,10 @@ export function createRunCommand({ claudeSettingsEnvService, presetService, proj
|
|
|
80
84
|
if (dryRun) {
|
|
81
85
|
const preview = [command, ...claudeArgs].join(' ');
|
|
82
86
|
stdout.write(`Would run: ${preview}\n`);
|
|
83
|
-
return;
|
|
87
|
+
return { status: 'executed' };
|
|
84
88
|
}
|
|
85
89
|
// Step 7: Spawn
|
|
86
90
|
await spawnCommand(command, claudeArgs, { ...process.env, ...selected.env });
|
|
91
|
+
return { status: 'executed' };
|
|
87
92
|
};
|
|
88
93
|
}
|
package/dist/core/schema.js
CHANGED
|
@@ -32,7 +32,17 @@ const restoreHistorySchema = z.object({
|
|
|
32
32
|
targetType: z.enum(['settings', 'preset']),
|
|
33
33
|
targetName: z.string(),
|
|
34
34
|
});
|
|
35
|
+
const presetCreateHistorySchema = z.object({
|
|
36
|
+
timestamp: z.string().datetime({ offset: true }),
|
|
37
|
+
action: z.literal('preset-create'),
|
|
38
|
+
projectPath: z.string(),
|
|
39
|
+
presetName: z.string(),
|
|
40
|
+
destination: z.enum(['global', 'project']),
|
|
41
|
+
migratedKeys: z.array(envKeySchema),
|
|
42
|
+
sources: z.array(sourceEntrySchema),
|
|
43
|
+
});
|
|
35
44
|
export const historySchema = z.discriminatedUnion('action', [
|
|
36
45
|
initHistorySchema,
|
|
37
46
|
restoreHistorySchema,
|
|
47
|
+
presetCreateHistorySchema,
|
|
38
48
|
]);
|
|
@@ -1,14 +1,62 @@
|
|
|
1
|
-
export function createPresetCreateFlowState() {
|
|
1
|
+
export function createPresetCreateFlowState(input) {
|
|
2
|
+
const detectedEnv = input?.detectedEnv ?? {};
|
|
3
|
+
const requiredKeys = input?.requiredKeys ?? [];
|
|
4
|
+
const detectedKeys = Object.keys(detectedEnv).sort();
|
|
5
|
+
const selectedKeys = requiredKeys.filter((key) => key in detectedEnv);
|
|
6
|
+
if (detectedKeys.length > 0) {
|
|
7
|
+
return {
|
|
8
|
+
step: 'detectedPrompt',
|
|
9
|
+
env: detectedEnv,
|
|
10
|
+
allKeys: detectedKeys,
|
|
11
|
+
selectedKeys,
|
|
12
|
+
requiredKeys: selectedKeys,
|
|
13
|
+
presetName: '',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
2
16
|
return {
|
|
3
17
|
step: 'source',
|
|
4
18
|
env: {},
|
|
5
19
|
allKeys: [],
|
|
6
20
|
selectedKeys: [],
|
|
21
|
+
requiredKeys: [],
|
|
7
22
|
presetName: '',
|
|
8
23
|
};
|
|
9
24
|
}
|
|
10
25
|
export function advancePresetCreateFlow(state, action) {
|
|
11
26
|
switch (state.step) {
|
|
27
|
+
case 'detectedPrompt':
|
|
28
|
+
if (action.type === 'accept-detected-prompt') {
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
step: 'detected',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (action.type === 'reject-detected-prompt') {
|
|
35
|
+
const { source: _source, ...rest } = state;
|
|
36
|
+
return {
|
|
37
|
+
...rest,
|
|
38
|
+
step: 'source',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return state;
|
|
42
|
+
case 'detected':
|
|
43
|
+
if (action.type === 'toggle-detected-key') {
|
|
44
|
+
if (state.requiredKeys.includes(action.key) || !state.allKeys.includes(action.key)) {
|
|
45
|
+
return state;
|
|
46
|
+
}
|
|
47
|
+
const selectedKeys = state.selectedKeys.includes(action.key)
|
|
48
|
+
? state.selectedKeys.filter((key) => key !== action.key)
|
|
49
|
+
: [...state.selectedKeys, action.key].sort();
|
|
50
|
+
return { ...state, selectedKeys };
|
|
51
|
+
}
|
|
52
|
+
if (action.type === 'confirm-detected-keys') {
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
step: 'name',
|
|
56
|
+
source: 'detected',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return state;
|
|
12
60
|
case 'source':
|
|
13
61
|
if (action.type !== 'select-source')
|
|
14
62
|
return state;
|
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
export function createRestoreFlowState(records) {
|
|
1
|
+
export function createRestoreFlowState(records, cwd) {
|
|
2
|
+
const currentProjectRecords = records
|
|
3
|
+
.filter((record) => 'projectPath' in record && record.projectPath === cwd)
|
|
4
|
+
.sort((left, right) => right.timestamp.localeCompare(left.timestamp));
|
|
5
|
+
const otherHistoryRecords = records
|
|
6
|
+
.filter((record) => !('projectPath' in record) || record.projectPath !== cwd)
|
|
7
|
+
.sort((left, right) => right.timestamp.localeCompare(left.timestamp));
|
|
8
|
+
const orderedRecords = [...currentProjectRecords, ...otherHistoryRecords];
|
|
9
|
+
const groups = [];
|
|
10
|
+
if (currentProjectRecords.length > 0) {
|
|
11
|
+
groups.push({ title: 'Current project', start: 0, end: currentProjectRecords.length });
|
|
12
|
+
}
|
|
13
|
+
if (otherHistoryRecords.length > 0) {
|
|
14
|
+
groups.push({
|
|
15
|
+
title: 'Other history',
|
|
16
|
+
start: currentProjectRecords.length,
|
|
17
|
+
end: orderedRecords.length,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
2
20
|
return {
|
|
3
21
|
step: 'record',
|
|
4
|
-
records,
|
|
22
|
+
records: orderedRecords,
|
|
23
|
+
groups,
|
|
5
24
|
};
|
|
6
25
|
}
|
|
7
26
|
export function advanceRestoreFlow(state, action) {
|
|
@@ -14,7 +33,7 @@ export function advanceRestoreFlow(state, action) {
|
|
|
14
33
|
if (!selectedRecord) {
|
|
15
34
|
return state;
|
|
16
35
|
}
|
|
17
|
-
if (selectedRecord.action === 'init') {
|
|
36
|
+
if (selectedRecord.action === 'init' || selectedRecord.action === 'preset-create') {
|
|
18
37
|
return {
|
|
19
38
|
...state,
|
|
20
39
|
step: 'confirm',
|
|
@@ -41,19 +60,18 @@ export function advanceRestoreFlow(state, action) {
|
|
|
41
60
|
targetType: 'settings',
|
|
42
61
|
};
|
|
43
62
|
}
|
|
44
|
-
const targetName = action.targetName;
|
|
45
63
|
return {
|
|
46
64
|
...state,
|
|
47
65
|
step: 'confirm',
|
|
48
66
|
targetType: 'preset',
|
|
49
|
-
targetName,
|
|
67
|
+
targetName: action.targetName,
|
|
50
68
|
};
|
|
51
|
-
case 'confirm':
|
|
69
|
+
case 'confirm': {
|
|
52
70
|
if (action.type !== 'confirm' || !state.selectedTimestamp) {
|
|
53
71
|
return state;
|
|
54
72
|
}
|
|
55
73
|
const selectedRecord = state.records.find((record) => record.timestamp === state.selectedTimestamp);
|
|
56
|
-
if (selectedRecord?.action === 'init') {
|
|
74
|
+
if (selectedRecord?.action === 'init' || selectedRecord?.action === 'preset-create') {
|
|
57
75
|
return {
|
|
58
76
|
...state,
|
|
59
77
|
step: 'done',
|
|
@@ -69,6 +87,7 @@ export function advanceRestoreFlow(state, action) {
|
|
|
69
87
|
...state,
|
|
70
88
|
step: 'done',
|
|
71
89
|
};
|
|
90
|
+
}
|
|
72
91
|
case 'done':
|
|
73
92
|
return state;
|
|
74
93
|
}
|
|
@@ -3,6 +3,17 @@ import { useState } from 'react';
|
|
|
3
3
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
4
|
import { advancePresetCreateFlow, createPresetCreateFlowState, } from '../flows/preset-create-flow.js';
|
|
5
5
|
import { EnvSummary } from './summary.js';
|
|
6
|
+
function DetectedPromptStep({ cursor }) {
|
|
7
|
+
const options = ['Generate from detected config', 'Choose another source'];
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Detected existing configuration" }), _jsx(Text, { dimColor: true, children: "Use the currently detected settings to generate a preset?" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter confirm \u00B7 q cancel" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((label, i) => (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { ...(i === cursor ? { color: 'cyan' } : {}), children: label })] }, label))) })] }));
|
|
9
|
+
}
|
|
10
|
+
function DetectedKeysStep({ keys, selectedKeys, requiredKeys, cursor, }) {
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select detected env keys to migrate" }), _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 space toggle optional keys \u00B7 enter confirm" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: keys.map((key, i) => {
|
|
12
|
+
const isSelected = selectedKeys.includes(key);
|
|
13
|
+
const isRequired = requiredKeys.includes(key);
|
|
14
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: i === cursor ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? 'green' : '', children: isSelected ? '[x]' : '[ ]' }), _jsx(Text, { children: isRequired ? ' ! ' : ' ' }), _jsxs(Text, { children: [" ", key] })] }, key));
|
|
15
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "! required key \u00B7 q cancel" }) })] }));
|
|
16
|
+
}
|
|
6
17
|
function SourceStep({ cursor }) {
|
|
7
18
|
const options = [
|
|
8
19
|
{ label: 'File import', value: 'file' },
|
|
@@ -32,9 +43,11 @@ function DestinationStep({ cursor }) {
|
|
|
32
43
|
];
|
|
33
44
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select save destination" }), _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))) })] }));
|
|
34
45
|
}
|
|
35
|
-
export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectEnvPath, }) {
|
|
46
|
+
export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectEnvPath, detectedEnv, requiredKeys, }) {
|
|
36
47
|
const { exit } = useApp();
|
|
37
|
-
const [state, setState] = useState(createPresetCreateFlowState
|
|
48
|
+
const [state, setState] = useState(() => createPresetCreateFlowState(detectedEnv
|
|
49
|
+
? (requiredKeys ? { detectedEnv, requiredKeys } : { detectedEnv })
|
|
50
|
+
: undefined));
|
|
38
51
|
const [textInput, setTextInput] = useState('');
|
|
39
52
|
const [listCursor, setListCursor] = useState(0);
|
|
40
53
|
const [allKeys, setAllKeys] = useState([]);
|
|
@@ -44,6 +57,58 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
44
57
|
exit();
|
|
45
58
|
return;
|
|
46
59
|
}
|
|
60
|
+
if (state.step === 'detectedPrompt') {
|
|
61
|
+
if (input === 'q') {
|
|
62
|
+
exit();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (key.upArrow || input === 'k') {
|
|
66
|
+
setListCursor((c) => Math.max(0, c - 1));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (key.downArrow || input === 'j') {
|
|
70
|
+
setListCursor((c) => Math.min(1, c + 1));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (key.return) {
|
|
74
|
+
setState((s) => advancePresetCreateFlow(s, listCursor === 0
|
|
75
|
+
? { type: 'accept-detected-prompt' }
|
|
76
|
+
: { type: 'reject-detected-prompt' }));
|
|
77
|
+
setListCursor(0);
|
|
78
|
+
setTextInput('');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (state.step === 'detected') {
|
|
83
|
+
if (input === 'q') {
|
|
84
|
+
exit();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (key.upArrow || input === 'k') {
|
|
88
|
+
setListCursor((c) => Math.max(0, c - 1));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (key.downArrow || input === 'j') {
|
|
92
|
+
setListCursor((c) => Math.min(state.allKeys.length - 1, c + 1));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (input === ' ') {
|
|
96
|
+
const targetKey = state.allKeys[listCursor];
|
|
97
|
+
if (targetKey) {
|
|
98
|
+
setState((s) => advancePresetCreateFlow(s, {
|
|
99
|
+
type: 'toggle-detected-key',
|
|
100
|
+
key: targetKey,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (key.return) {
|
|
106
|
+
setState((s) => advancePresetCreateFlow(s, { type: 'confirm-detected-keys' }));
|
|
107
|
+
setListCursor(0);
|
|
108
|
+
setTextInput('');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
47
112
|
if (state.step === 'source') {
|
|
48
113
|
if (input === 'q') {
|
|
49
114
|
exit();
|
|
@@ -261,7 +326,7 @@ export function PresetCreateApp({ onSubmit, readFile, globalPresetPath, projectE
|
|
|
261
326
|
if (state.step === 'done') {
|
|
262
327
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", children: "Done" }) }));
|
|
263
328
|
}
|
|
264
|
-
return (_jsxs(Box, { flexDirection: "column", children: [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)
|
|
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)
|
|
265
330
|
.filter(([k]) => state.selectedKeys.includes(k))
|
|
266
331
|
.sort(([a], [b]) => a.localeCompare(b)), mask: true, ...(state.filePath ? { fromFiles: [state.filePath] } : {}), toFiles: [
|
|
267
332
|
state.destination === 'global'
|
package/dist/ink/restore-app.js
CHANGED
|
@@ -3,6 +3,21 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
3
3
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
4
|
import { advanceRestoreFlow } from '../flows/restore-flow.js';
|
|
5
5
|
import { EnvEntries, EnvSummary } from './summary.js';
|
|
6
|
+
export function getRestorePreviewSections(record) {
|
|
7
|
+
if (!record) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
if (record.action === 'init' || record.action === 'preset-create') {
|
|
11
|
+
return record.sources
|
|
12
|
+
.filter((source) => Object.keys(source.backup).length > 0)
|
|
13
|
+
.map((source) => ({
|
|
14
|
+
file: source.file,
|
|
15
|
+
entries: Object.entries(source.backup)
|
|
16
|
+
.sort(([left], [right]) => left.localeCompare(right)),
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
6
21
|
export function RestoreApp({ state, onSubmit, }) {
|
|
7
22
|
const { exit } = useApp();
|
|
8
23
|
const [currentState, setCurrentState] = useState(state);
|
|
@@ -13,9 +28,11 @@ export function RestoreApp({ state, onSubmit, }) {
|
|
|
13
28
|
? recordAtCursor
|
|
14
29
|
: selectedRecord ?? currentState.records[0];
|
|
15
30
|
const restoreEntries = useMemo(() => activeRecord
|
|
16
|
-
? Object.entries(activeRecord.action === 'init'
|
|
31
|
+
? Object.entries(activeRecord.action === 'init' || activeRecord.action === 'preset-create'
|
|
17
32
|
? Object.fromEntries(activeRecord.sources.flatMap((s) => Object.entries(s.backup)))
|
|
18
|
-
: activeRecord.
|
|
33
|
+
: activeRecord.action === 'restore'
|
|
34
|
+
? activeRecord.backup
|
|
35
|
+
: {}).sort(([left], [right]) => left.localeCompare(right))
|
|
19
36
|
: [], [activeRecord]);
|
|
20
37
|
const fromFiles = useMemo(() => {
|
|
21
38
|
if (!activeRecord || activeRecord.action !== 'init') {
|
|
@@ -23,12 +40,7 @@ export function RestoreApp({ state, onSubmit, }) {
|
|
|
23
40
|
}
|
|
24
41
|
return activeRecord.shellWrites.map((sw) => sw.filePath);
|
|
25
42
|
}, [activeRecord]);
|
|
26
|
-
const
|
|
27
|
-
if (!activeRecord || activeRecord.action !== 'init') {
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
return activeRecord.sources.map((s) => s.file);
|
|
31
|
-
}, [activeRecord]);
|
|
43
|
+
const previewSections = useMemo(() => getRestorePreviewSections(activeRecord), [activeRecord]);
|
|
32
44
|
useEffect(() => {
|
|
33
45
|
setCurrentState(state);
|
|
34
46
|
setCursor(0);
|
|
@@ -96,7 +108,10 @@ export function RestoreApp({ state, onSubmit, }) {
|
|
|
96
108
|
exit();
|
|
97
109
|
}
|
|
98
110
|
});
|
|
99
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Restore record" }), currentState.step === 'record' ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter confirm \u00B7 q cancel" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "History" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: currentState.
|
|
111
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Restore record" }), currentState.step === 'record' ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 enter confirm \u00B7 q cancel" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 28, marginRight: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "History" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: currentState.groups.map((group) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: group.title }), currentState.records.slice(group.start, group.end).map((record, index) => {
|
|
112
|
+
const actualIndex = group.start + index;
|
|
113
|
+
return (_jsxs(Text, { children: [actualIndex === cursor ? '❯ ' : ' ', record.timestamp] }, record.timestamp));
|
|
114
|
+
})] }, group.title))) })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Preview" }), activeRecord?.action === 'restore' ? (_jsxs(Text, { dimColor: true, children: ["Restore to ", activeRecord.targetType === 'preset' ? `preset ${activeRecord.targetName}` : activeRecord.targetType] })) : null, previewSections.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", children: section.file }), _jsx(EnvEntries, { entries: section.entries })] }, section.file))), previewSections.length === 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EnvEntries, { entries: restoreEntries }) })) : null] })] })] })) : null, currentState.step === 'target' ? (_jsxs(Text, { children: ["Select target for ", selectedRecord?.timestamp ?? 'record', ": settings or preset"] })) : null, currentState.step === 'confirm' && selectedRecord?.action === 'init' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord.timestamp })] }), _jsx(EnvSummary, { title: "Will restore", entries: restoreEntries, ...(fromFiles.length > 0 ? { fromFiles } : {}), toFiles: previewSections.map((section) => section.file) })] })) : null, currentState.step === 'confirm' && selectedRecord?.action === 'preset-create' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord.timestamp })] }), previewSections.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", children: section.file }), _jsx(EnvEntries, { entries: section.entries })] }, section.file)))] })) : null, currentState.step === 'confirm' && selectedRecord?.action === 'restore' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Confirm restore from ", _jsx(Text, { color: "cyan", children: selectedRecord.timestamp }), " to", ' ', _jsx(Text, { color: "green", children: currentState.targetType === 'preset'
|
|
100
115
|
? `preset ${currentState.targetName}`
|
|
101
116
|
: currentState.targetType ?? 'settings' })] }), _jsx(EnvSummary, { title: "Will restore", entries: restoreEntries })] })) : null, currentState.step === 'done' ? (_jsxs(Text, { color: "green", children: ['\n', "Restore complete"] })) : null, currentState.step !== 'done' ? (_jsx(Text, { children: "Press Enter to confirm or q to cancel" })) : null] }));
|
|
102
117
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lkangd/cc-env",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Manage runtime environment variables for Claude Code",
|
|
5
5
|
"homepage": "https://github.com/lkangd/cc-env#readme",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/lkangd/cc-env/issues"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"type": "module",
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=20.19.2
|
|
20
|
+
"node": ">=20.19.2"
|
|
21
21
|
},
|
|
22
22
|
"bin": {
|
|
23
23
|
"cc-env": "dist/cli.js"
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@types/gradient-string": "^1.1.6",
|
|
55
55
|
"@types/node": "^20.19.0",
|
|
56
56
|
"@types/react": "^19.1.10",
|
|
57
|
+
"@vitest/coverage-istanbul": "^3.2.4",
|
|
57
58
|
"@vitest/coverage-v8": "^3.2.4",
|
|
58
59
|
"execa": "^9.6.0",
|
|
59
60
|
"react-test-renderer": "^19.2.5",
|
package/dist/commands/debug.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { toProcessEnvMap } from '../core/process-env.js';
|
|
2
|
-
import { renderEnvSummary } from '../ink/summary.js';
|
|
3
|
-
export function createDebugCommand({ projectEnvService, processEnv }) {
|
|
4
|
-
return async function debug() {
|
|
5
|
-
const projectEnv = await projectEnvService.read();
|
|
6
|
-
await renderEnvSummary({
|
|
7
|
-
title: 'Process Environment',
|
|
8
|
-
description: 'Current process environment variables',
|
|
9
|
-
env: toProcessEnvMap(processEnv)
|
|
10
|
-
});
|
|
11
|
-
await renderEnvSummary({
|
|
12
|
-
title: 'Project Environment',
|
|
13
|
-
description: 'Environment variables from .cc-env/env.json',
|
|
14
|
-
env: projectEnv
|
|
15
|
-
});
|
|
16
|
-
};
|
|
17
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export function createListPresetsCommand({ presetService, projectEnvService, renderList, }) {
|
|
2
|
-
return async function listPresets() {
|
|
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
|
-
await renderList(presets);
|
|
15
|
-
};
|
|
16
|
-
}
|
package/dist/core/lock.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { open } from 'node:fs/promises';
|
|
2
|
-
import lockfile from 'proper-lockfile';
|
|
3
|
-
import { ensureParentDir } from './fs.js';
|
|
4
|
-
export async function withFileLock(filePath, run) {
|
|
5
|
-
await ensureParentDir(filePath);
|
|
6
|
-
const handle = await open(filePath, 'a+');
|
|
7
|
-
try {
|
|
8
|
-
const release = await lockfile.lock(filePath, {
|
|
9
|
-
realpath: false,
|
|
10
|
-
retries: {
|
|
11
|
-
retries: 3,
|
|
12
|
-
factor: 1,
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
try {
|
|
16
|
-
return await run();
|
|
17
|
-
}
|
|
18
|
-
finally {
|
|
19
|
-
await release();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
finally {
|
|
23
|
-
await handle.close();
|
|
24
|
-
}
|
|
25
|
-
}
|
package/dist/ink/init-app.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
3
|
-
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
-
import { advanceInitFlow, createInitFlowState, } from '../flows/init-flow.js';
|
|
5
|
-
export function InitApp({ keys = [], requiredKeys = [], sourceFiles = [], onSubmit, }) {
|
|
6
|
-
const { exit } = useApp();
|
|
7
|
-
const [cursor, setCursor] = useState(0);
|
|
8
|
-
const [flowState, setFlowState] = useState(() => createInitFlowState(keys, requiredKeys));
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
if (!onSubmit) {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
if (keys.length === 0) {
|
|
14
|
-
onSubmit({ confirmed: false, selectedKeys: [] });
|
|
15
|
-
exit();
|
|
16
|
-
}
|
|
17
|
-
}, [exit, keys.length, onSubmit]);
|
|
18
|
-
useInput((input, key) => {
|
|
19
|
-
if (!onSubmit) {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
if (key.upArrow || input === 'k') {
|
|
23
|
-
setCursor((c) => Math.max(0, c - 1));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (key.downArrow || input === 'j') {
|
|
27
|
-
setCursor((c) => Math.min(keys.length - 1, c + 1));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
if (input === ' ') {
|
|
31
|
-
const targetKey = keys[cursor];
|
|
32
|
-
if (targetKey) {
|
|
33
|
-
setFlowState((prev) => advanceInitFlow(prev, { type: 'toggle-key', key: targetKey }));
|
|
34
|
-
}
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (key.return) {
|
|
38
|
-
onSubmit({ confirmed: true, selectedKeys: flowState.selectedKeys });
|
|
39
|
-
exit();
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (key.escape || input.toLowerCase() === 'q') {
|
|
43
|
-
onSubmit({ confirmed: false, selectedKeys: [] });
|
|
44
|
-
exit();
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select env keys to migrate into managed shell config" }), sourceFiles.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Source:" }), sourceFiles.map((file) => (_jsxs(Text, { color: "cyan", children: [" ", file] }, file)))] })) : null, _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 space toggle \u00B7 enter confirm \u00B7 q cancel" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: keys.map((key, i) => {
|
|
48
|
-
const isRequired = requiredKeys.includes(key);
|
|
49
|
-
const isSelected = flowState.selectedKeys.includes(key);
|
|
50
|
-
const isCursor = i === cursor;
|
|
51
|
-
const checkbox = isSelected ? '[x]' : '[ ]';
|
|
52
|
-
return (_jsxs(Box, { children: [_jsx(Text, { children: isCursor ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? 'green' : '', children: checkbox }), _jsxs(Text, { children: [" ", key] }), isRequired ? _jsx(Text, { dimColor: true, children: " (required)" }) : null] }, key));
|
|
53
|
-
}) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [flowState.selectedKeys.length, " of ", keys.length, " selected"] }) })] }));
|
|
54
|
-
}
|
|
@@ -1,27 +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 PresetListApp({ presets, }) {
|
|
6
|
-
const { exit } = useApp();
|
|
7
|
-
const [cursor, setCursor] = useState(0);
|
|
8
|
-
const activePreset = presets[cursor];
|
|
9
|
-
const entries = useMemo(() => activePreset
|
|
10
|
-
? Object.entries(activePreset.env).sort(([a], [b]) => a.localeCompare(b))
|
|
11
|
-
: [], [activePreset]);
|
|
12
|
-
useInput((input, key) => {
|
|
13
|
-
if (key.escape || input.toLowerCase() === 'q') {
|
|
14
|
-
exit();
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
if (key.upArrow || input === 'k') {
|
|
18
|
-
setCursor((c) => Math.max(0, c - 1));
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
if (key.downArrow || input === 'j') {
|
|
22
|
-
setCursor((c) => Math.min(presets.length - 1, c + 1));
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Preset list" }), _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 }) })] })] })] }));
|
|
27
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { configSchema } from '../core/schema.js';
|
|
3
|
-
import { atomicWriteFile } from '../core/fs.js';
|
|
4
|
-
import { resolveConfigPath } from '../core/paths.js';
|
|
5
|
-
export function createConfigService(globalRoot) {
|
|
6
|
-
const filePath = resolveConfigPath(globalRoot);
|
|
7
|
-
return {
|
|
8
|
-
async read() {
|
|
9
|
-
try {
|
|
10
|
-
const content = await readFile(filePath, 'utf8');
|
|
11
|
-
return configSchema.parse(JSON.parse(content));
|
|
12
|
-
}
|
|
13
|
-
catch (error) {
|
|
14
|
-
if (error.code === 'ENOENT') {
|
|
15
|
-
return configSchema.parse({});
|
|
16
|
-
}
|
|
17
|
-
throw error;
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
async write(config) {
|
|
21
|
-
const parsed = configSchema.parse(config);
|
|
22
|
-
await atomicWriteFile(filePath, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
23
|
-
return parsed;
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { envMapSchema } from '../core/schema.js';
|
|
2
|
-
export function createRuntimeEnvService() {
|
|
3
|
-
return {
|
|
4
|
-
merge({ processEnv, settingsEnv, projectEnv, presetEnv, }) {
|
|
5
|
-
return envMapSchema.parse({
|
|
6
|
-
...processEnv,
|
|
7
|
-
...settingsEnv,
|
|
8
|
-
...projectEnv,
|
|
9
|
-
...presetEnv,
|
|
10
|
-
});
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
|
-
}
|