xclaude-launcher 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/README.zh-CN.md +117 -0
- package/dist/adapters/file-config-repo.js +31 -0
- package/dist/adapters/process-runner.js +15 -0
- package/dist/commands/config.js +107 -0
- package/dist/commands/run-claude.js +19 -0
- package/dist/index.js +50 -0
- package/dist/models/config.js +1 -0
- package/dist/services/claude-launcher.js +21 -0
- package/dist/services/config-service.js +99 -0
- package/dist/services/prompt-service.js +238 -0
- package/dist/utils/env.js +45 -0
- package/dist/utils/errors.js +6 -0
- package/dist/utils/paths.js +8 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 placeholder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Claude Launcher
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | [中文](./README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
`xclaude` solves the problem of switching Claude Code between multiple environment sources.
|
|
6
|
+
|
|
7
|
+
If you work with more than one Claude setup, such as:
|
|
8
|
+
|
|
9
|
+
- the default official Claude service
|
|
10
|
+
- a proxy or gateway endpoint
|
|
11
|
+
- different API base URLs for work and personal use
|
|
12
|
+
- different model defaults for different tasks
|
|
13
|
+
- different auth tokens across environments
|
|
14
|
+
|
|
15
|
+
then repeatedly exporting environment variables by hand is tedious and error-prone.
|
|
16
|
+
|
|
17
|
+
`xclaude` lets you save each setup as a reusable profile, then launch `claude` with the right environment in one step.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g xclaude-launcher
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
`xclaude` launches the local `claude` command, so Claude Code must already be installed and available in your `PATH`.
|
|
28
|
+
|
|
29
|
+
## QuickStart
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# 1. Add your first profile
|
|
33
|
+
xclaude config add
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
When creating a profile, you will be prompted for:
|
|
37
|
+
|
|
38
|
+
- `Profile name`: the label shown in the launcher
|
|
39
|
+
- `ANTHROPIC_AUTH_TOKEN`: your Claude auth token
|
|
40
|
+
- `ANTHROPIC_BASE_URL`: optional custom API base URL
|
|
41
|
+
- `ANTHROPIC_MODEL`: optional default model
|
|
42
|
+
- `ANTHROPIC_DEFAULT_HAIKU_MODEL`: optional Haiku override
|
|
43
|
+
- `ANTHROPIC_DEFAULT_SONNET_MODEL`: optional Sonnet override
|
|
44
|
+
- `ANTHROPIC_DEFAULT_OPUS_MODEL`: optional Opus override
|
|
45
|
+
- `CLAUDE_CODE_SUBAGENT_MODEL`: optional subagent model override
|
|
46
|
+
- Additional custom ENV vars if needed
|
|
47
|
+
|
|
48
|
+
You can leave optional fields empty and only set the values you actually use.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 2. Launch with the interactive picker
|
|
52
|
+
xclaude
|
|
53
|
+
|
|
54
|
+
# 3. Or launch a specific profile directly
|
|
55
|
+
xclaude --profile my-profile
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If you have not created any profiles yet, start with `xclaude config add`.
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
xclaude
|
|
64
|
+
xclaude --profile my-profile
|
|
65
|
+
xclaude --list
|
|
66
|
+
xclaude config
|
|
67
|
+
xclaude config path
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## What it does
|
|
71
|
+
|
|
72
|
+
- `xclaude` opens an interactive profile picker and launches `claude` with the selected profile environment.
|
|
73
|
+
- `xclaude config` opens the interactive profile manager.
|
|
74
|
+
|
|
75
|
+
## Config file
|
|
76
|
+
|
|
77
|
+
Profiles are stored in:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
~/.claude-launcher/config.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Built-in environment variables
|
|
84
|
+
|
|
85
|
+
`xclaude config` prompts for these first-class variables:
|
|
86
|
+
|
|
87
|
+
- `ANTHROPIC_AUTH_TOKEN`
|
|
88
|
+
- `ANTHROPIC_BASE_URL`
|
|
89
|
+
- `ANTHROPIC_MODEL`
|
|
90
|
+
- `ANTHROPIC_DEFAULT_HAIKU_MODEL`
|
|
91
|
+
- `ANTHROPIC_DEFAULT_SONNET_MODEL`
|
|
92
|
+
- `ANTHROPIC_DEFAULT_OPUS_MODEL`
|
|
93
|
+
- `CLAUDE_CODE_SUBAGENT_MODEL`
|
|
94
|
+
|
|
95
|
+
You can also add any number of custom environment variables to each profile.
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install
|
|
101
|
+
npm run build
|
|
102
|
+
npm run start -- --help
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Links
|
|
106
|
+
|
|
107
|
+
- Repository: https://github.com/placeholder/xclaude
|
|
108
|
+
- Issues: https://github.com/placeholder/xclaude/issues
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Claude Launcher
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | [中文](./README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
`xclaude` 用来解决 Claude Code 在多个环境源之间切换麻烦的问题。
|
|
6
|
+
|
|
7
|
+
如果你平时会在多个 Claude 配置之间来回切换,比如:
|
|
8
|
+
|
|
9
|
+
- 官方默认 Claude 服务
|
|
10
|
+
- 公司或团队内部代理 / 网关
|
|
11
|
+
- 不同的 `ANTHROPIC_BASE_URL`
|
|
12
|
+
- 不同环境下使用不同的 token
|
|
13
|
+
- 不同场景下使用不同的默认模型
|
|
14
|
+
|
|
15
|
+
那么每次手动切换环境变量会很繁琐,也很容易出错。
|
|
16
|
+
|
|
17
|
+
`xclaude` 可以把这些配置保存成可复用的 profile,然后在启动 `claude` 时一键带上对应环境变量。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g xclaude-launcher
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 前置要求
|
|
26
|
+
|
|
27
|
+
`xclaude` 本身不会替代 Claude Code,它只是负责按指定 profile 启动本地的 `claude` 命令。
|
|
28
|
+
|
|
29
|
+
所以在使用前,需要先确保:
|
|
30
|
+
|
|
31
|
+
- 已经安装 Claude Code
|
|
32
|
+
- `claude` 命令已经在你的 `PATH` 中可用
|
|
33
|
+
|
|
34
|
+
## QuickStart
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 1. 新增第一个 profile
|
|
38
|
+
xclaude config add
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
创建 profile 时,会依次提示你填写这些信息:
|
|
42
|
+
|
|
43
|
+
- `Profile name`:这个 profile 在启动器里的显示名称
|
|
44
|
+
- `ANTHROPIC_AUTH_TOKEN`:你的 Claude 鉴权 token
|
|
45
|
+
- `ANTHROPIC_BASE_URL`:可选,自定义 API 地址
|
|
46
|
+
- `ANTHROPIC_MODEL`:可选,默认模型
|
|
47
|
+
- `ANTHROPIC_DEFAULT_HAIKU_MODEL`:可选,Haiku 默认模型覆盖
|
|
48
|
+
- `ANTHROPIC_DEFAULT_SONNET_MODEL`:可选,Sonnet 默认模型覆盖
|
|
49
|
+
- `ANTHROPIC_DEFAULT_OPUS_MODEL`:可选,Opus 默认模型覆盖
|
|
50
|
+
- `CLAUDE_CODE_SUBAGENT_MODEL`:可选,subagent 默认模型覆盖
|
|
51
|
+
- 其他自定义 ENV 变量:按需添加
|
|
52
|
+
|
|
53
|
+
如果某些字段你暂时不需要,可以直接留空。
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# 2. 使用交互方式选择 profile 并启动
|
|
57
|
+
xclaude
|
|
58
|
+
|
|
59
|
+
# 3. 或者直接指定 profile 启动
|
|
60
|
+
xclaude --profile my-profile
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
如果你还没有创建任何 profile,建议先执行:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
xclaude config add
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 用法
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
xclaude
|
|
73
|
+
xclaude --profile my-profile
|
|
74
|
+
xclaude --list
|
|
75
|
+
xclaude config
|
|
76
|
+
xclaude config path
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 这个工具会做什么
|
|
80
|
+
|
|
81
|
+
- `xclaude` 会打开一个交互式 profile 选择器,并使用所选 profile 的环境变量启动 `claude`
|
|
82
|
+
- `xclaude config` 会打开交互式 profile 管理界面
|
|
83
|
+
|
|
84
|
+
## 配置文件位置
|
|
85
|
+
|
|
86
|
+
profile 保存在:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
~/.claude-launcher/config.json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 内置支持的环境变量
|
|
93
|
+
|
|
94
|
+
执行 `xclaude config` 时,会优先提示这些一等字段:
|
|
95
|
+
|
|
96
|
+
- `ANTHROPIC_AUTH_TOKEN`
|
|
97
|
+
- `ANTHROPIC_BASE_URL`
|
|
98
|
+
- `ANTHROPIC_MODEL`
|
|
99
|
+
- `ANTHROPIC_DEFAULT_HAIKU_MODEL`
|
|
100
|
+
- `ANTHROPIC_DEFAULT_SONNET_MODEL`
|
|
101
|
+
- `ANTHROPIC_DEFAULT_OPUS_MODEL`
|
|
102
|
+
- `CLAUDE_CODE_SUBAGENT_MODEL`
|
|
103
|
+
|
|
104
|
+
除此之外,你也可以为每个 profile 添加任意数量的自定义环境变量。
|
|
105
|
+
|
|
106
|
+
## 开发
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm install
|
|
110
|
+
npm run build
|
|
111
|
+
npm run start -- --help
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 链接
|
|
115
|
+
|
|
116
|
+
- Repository: https://github.com/placeholder/xclaude
|
|
117
|
+
- Issues: https://github.com/placeholder/xclaude/issues
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { CliError } from '../utils/errors.js';
|
|
3
|
+
import { getConfigDir, getConfigPath } from '../utils/paths.js';
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
version: 1,
|
|
6
|
+
profiles: [],
|
|
7
|
+
};
|
|
8
|
+
export class FileConfigRepository {
|
|
9
|
+
async load() {
|
|
10
|
+
const filePath = getConfigPath();
|
|
11
|
+
try {
|
|
12
|
+
const content = await readFile(filePath, 'utf8');
|
|
13
|
+
const parsed = JSON.parse(content);
|
|
14
|
+
return {
|
|
15
|
+
version: parsed.version ?? 1,
|
|
16
|
+
profiles: parsed.profiles ?? [],
|
|
17
|
+
lastUsedProfileId: parsed.lastUsedProfileId,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code === 'ENOENT') {
|
|
22
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
23
|
+
}
|
|
24
|
+
throw new CliError(`Failed to read config: ${filePath}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async save(config) {
|
|
28
|
+
await mkdir(getConfigDir(), { recursive: true });
|
|
29
|
+
await writeFile(getConfigPath(), `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export class ProcessRunner {
|
|
3
|
+
run(command, args, env) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(command, args, {
|
|
6
|
+
env,
|
|
7
|
+
stdio: 'inherit',
|
|
8
|
+
});
|
|
9
|
+
child.on('error', reject);
|
|
10
|
+
child.on('exit', (code) => {
|
|
11
|
+
resolve(code ?? 0);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ConfigService } from '../services/config-service.js';
|
|
2
|
+
import { PromptService } from '../services/prompt-service.js';
|
|
3
|
+
import { getConfigPath } from '../utils/paths.js';
|
|
4
|
+
import { CliError } from '../utils/errors.js';
|
|
5
|
+
export async function runConfigCommand(action) {
|
|
6
|
+
const configService = new ConfigService();
|
|
7
|
+
const promptService = new PromptService();
|
|
8
|
+
while (true) {
|
|
9
|
+
const nextAction = normalizeAction(action) ?? (await promptService.chooseConfigAction());
|
|
10
|
+
action = undefined;
|
|
11
|
+
if (nextAction === 'path') {
|
|
12
|
+
console.log(getConfigPath());
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (nextAction === 'exit') {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
if (nextAction === 'list') {
|
|
19
|
+
while (true) {
|
|
20
|
+
const config = await configService.getConfig();
|
|
21
|
+
promptService.printProfiles(config.profiles, config.lastUsedProfileId);
|
|
22
|
+
if (config.profiles.length === 0) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
const profile = await promptService.chooseProfileOrBackOrExit(config.profiles, config.lastUsedProfileId);
|
|
26
|
+
if (profile === 'exit') {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (profile === 'back') {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
const profileAction = await promptService.chooseProfileAction();
|
|
33
|
+
if (profileAction === 'exit') {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (profileAction === 'back') {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (profileAction === 'edit') {
|
|
40
|
+
const input = await promptService.promptProfileInput(profile);
|
|
41
|
+
if (input === 'exit') {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
if (input === 'back') {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const updated = await configService.updateProfile(profile.id, input);
|
|
48
|
+
console.log(`Updated profile: ${updated.name}`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (await promptService.confirmRemoveProfile(profile)) {
|
|
52
|
+
const removed = await configService.removeProfile(profile.id);
|
|
53
|
+
console.log(`Removed profile: ${removed.name}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (nextAction === 'add') {
|
|
59
|
+
while (true) {
|
|
60
|
+
const input = await promptService.promptProfileInput();
|
|
61
|
+
if (input === 'exit') {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if (input === 'back') {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
const profile = await configService.addProfile(input);
|
|
68
|
+
console.log(`Added profile: ${profile.name}`);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (nextAction === 'edit') {
|
|
74
|
+
while (true) {
|
|
75
|
+
const config = await configService.getConfig();
|
|
76
|
+
const profile = await promptService.chooseProfileOrBackOrExit(config.profiles, config.lastUsedProfileId);
|
|
77
|
+
if (profile === 'exit') {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
if (profile === 'back') {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const input = await promptService.promptProfileInput(profile);
|
|
84
|
+
if (input === 'exit') {
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
if (input === 'back') {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const updated = await configService.updateProfile(profile.id, input);
|
|
91
|
+
console.log(`Updated profile: ${updated.name}`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw new CliError(`Unsupported config action: ${action}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function normalizeAction(action) {
|
|
100
|
+
if (!action) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
if (action === 'list' || action === 'add' || action === 'edit' || action === 'path') {
|
|
104
|
+
return action;
|
|
105
|
+
}
|
|
106
|
+
throw new CliError(`Unsupported config action: ${action}`);
|
|
107
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ConfigService } from '../services/config-service.js';
|
|
2
|
+
import { PromptService } from '../services/prompt-service.js';
|
|
3
|
+
export async function runClaudeCommand(options) {
|
|
4
|
+
const configService = new ConfigService();
|
|
5
|
+
const promptService = new PromptService();
|
|
6
|
+
const config = await configService.getConfig();
|
|
7
|
+
const sortedProfiles = configService.sortProfilesByLastUsed(config.profiles, config.lastUsedProfileId);
|
|
8
|
+
if (options.list) {
|
|
9
|
+
promptService.printProfiles(sortedProfiles, config.lastUsedProfileId);
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
const profile = options.profile
|
|
13
|
+
? await configService.getProfileByIdOrName(options.profile)
|
|
14
|
+
: await promptService.chooseProfileForLaunch(sortedProfiles, config.lastUsedProfileId);
|
|
15
|
+
await configService.markLastUsed(profile.id);
|
|
16
|
+
const { ClaudeLauncherService } = await import('../services/claude-launcher.js');
|
|
17
|
+
const launcher = new ClaudeLauncherService();
|
|
18
|
+
return launcher.launch(profile, options.claudeArgs);
|
|
19
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runClaudeCommand } from './commands/run-claude.js';
|
|
3
|
+
import { runConfigCommand } from './commands/config.js';
|
|
4
|
+
import { CliError } from './utils/errors.js';
|
|
5
|
+
async function main() {
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const [command, ...rest] = args;
|
|
8
|
+
if (command === '--help' || command === '-h') {
|
|
9
|
+
printHelp();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (command === 'config') {
|
|
13
|
+
const code = await runConfigCommand(rest[0]);
|
|
14
|
+
process.exitCode = code;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const code = await runClaudeCommand(parseRunClaudeOptions(args));
|
|
18
|
+
process.exitCode = code;
|
|
19
|
+
}
|
|
20
|
+
function parseRunClaudeOptions(args) {
|
|
21
|
+
const options = {
|
|
22
|
+
claudeArgs: [],
|
|
23
|
+
};
|
|
24
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
25
|
+
const current = args[index];
|
|
26
|
+
if (current === '--profile') {
|
|
27
|
+
const value = args[index + 1];
|
|
28
|
+
if (!value) {
|
|
29
|
+
throw new CliError('--profile requires a value');
|
|
30
|
+
}
|
|
31
|
+
options.profile = value;
|
|
32
|
+
index += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (current === '--list') {
|
|
36
|
+
options.list = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
options.claudeArgs.push(current);
|
|
40
|
+
}
|
|
41
|
+
return options;
|
|
42
|
+
}
|
|
43
|
+
function printHelp() {
|
|
44
|
+
console.log(`xclaude - Claude launcher\n\nUsage:\n xclaude [--profile <name-or-id>] [--list] [claude args...]\n xclaude config [list|add|edit]\n`);
|
|
45
|
+
}
|
|
46
|
+
main().catch((error) => {
|
|
47
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
48
|
+
console.error(message);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ProcessRunner } from '../adapters/process-runner.js';
|
|
2
|
+
import { mergeEnv } from '../utils/env.js';
|
|
3
|
+
import { CliError } from '../utils/errors.js';
|
|
4
|
+
export class ClaudeLauncherService {
|
|
5
|
+
processRunner;
|
|
6
|
+
constructor(processRunner = new ProcessRunner()) {
|
|
7
|
+
this.processRunner = processRunner;
|
|
8
|
+
}
|
|
9
|
+
async launch(profile, extraArgs = []) {
|
|
10
|
+
try {
|
|
11
|
+
return await this.processRunner.run(profile.command, [...profile.args, ...extraArgs], mergeEnv(profile.env));
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const errno = error;
|
|
15
|
+
if (errno.code === 'ENOENT') {
|
|
16
|
+
throw new CliError('Could not find the claude command. Make sure it is installed and available in PATH');
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { FileConfigRepository } from '../adapters/file-config-repo.js';
|
|
2
|
+
import { buildProfileEnv } from '../utils/env.js';
|
|
3
|
+
import { CliError } from '../utils/errors.js';
|
|
4
|
+
export class ConfigService {
|
|
5
|
+
repository;
|
|
6
|
+
constructor(repository = new FileConfigRepository()) {
|
|
7
|
+
this.repository = repository;
|
|
8
|
+
}
|
|
9
|
+
async getConfig() {
|
|
10
|
+
return this.repository.load();
|
|
11
|
+
}
|
|
12
|
+
async listProfiles() {
|
|
13
|
+
const config = await this.repository.load();
|
|
14
|
+
return config.profiles;
|
|
15
|
+
}
|
|
16
|
+
sortProfilesByLastUsed(profiles, lastUsedProfileId) {
|
|
17
|
+
if (!lastUsedProfileId) {
|
|
18
|
+
return [...profiles];
|
|
19
|
+
}
|
|
20
|
+
const sortedProfiles = [...profiles];
|
|
21
|
+
const index = sortedProfiles.findIndex((profile) => profile.id === lastUsedProfileId);
|
|
22
|
+
if (index <= 0) {
|
|
23
|
+
return sortedProfiles;
|
|
24
|
+
}
|
|
25
|
+
const [lastUsedProfile] = sortedProfiles.splice(index, 1);
|
|
26
|
+
sortedProfiles.unshift(lastUsedProfile);
|
|
27
|
+
return sortedProfiles;
|
|
28
|
+
}
|
|
29
|
+
async getProfileByIdOrName(idOrName) {
|
|
30
|
+
const config = await this.repository.load();
|
|
31
|
+
const profile = config.profiles.find((item) => item.id === idOrName || item.name === idOrName);
|
|
32
|
+
if (!profile) {
|
|
33
|
+
throw new CliError(`Profile not found: ${idOrName}`);
|
|
34
|
+
}
|
|
35
|
+
return profile;
|
|
36
|
+
}
|
|
37
|
+
async addProfile(input) {
|
|
38
|
+
const config = await this.repository.load();
|
|
39
|
+
const id = this.createProfileId(input.name);
|
|
40
|
+
if (config.profiles.some((profile) => profile.id === id || profile.name === input.name)) {
|
|
41
|
+
throw new CliError(`Profile already exists: ${input.name}`);
|
|
42
|
+
}
|
|
43
|
+
const profile = {
|
|
44
|
+
id,
|
|
45
|
+
name: input.name,
|
|
46
|
+
command: 'claude',
|
|
47
|
+
args: [],
|
|
48
|
+
env: buildProfileEnv(input),
|
|
49
|
+
};
|
|
50
|
+
config.profiles.push(profile);
|
|
51
|
+
await this.repository.save(config);
|
|
52
|
+
return profile;
|
|
53
|
+
}
|
|
54
|
+
async updateProfile(profileId, input) {
|
|
55
|
+
const config = await this.repository.load();
|
|
56
|
+
const profile = config.profiles.find((item) => item.id === profileId);
|
|
57
|
+
if (!profile) {
|
|
58
|
+
throw new CliError(`Profile not found: ${profileId}`);
|
|
59
|
+
}
|
|
60
|
+
const nextId = this.createProfileId(input.name);
|
|
61
|
+
const duplicate = config.profiles.find((item) => item.id !== profileId && (item.id === nextId || item.name === input.name));
|
|
62
|
+
if (duplicate) {
|
|
63
|
+
throw new CliError(`Profile name conflict: ${input.name}`);
|
|
64
|
+
}
|
|
65
|
+
profile.id = nextId;
|
|
66
|
+
profile.name = input.name;
|
|
67
|
+
profile.env = buildProfileEnv(input);
|
|
68
|
+
if (config.lastUsedProfileId === profileId) {
|
|
69
|
+
config.lastUsedProfileId = nextId;
|
|
70
|
+
}
|
|
71
|
+
await this.repository.save(config);
|
|
72
|
+
return profile;
|
|
73
|
+
}
|
|
74
|
+
async markLastUsed(profileId) {
|
|
75
|
+
const config = await this.repository.load();
|
|
76
|
+
config.lastUsedProfileId = profileId;
|
|
77
|
+
await this.repository.save(config);
|
|
78
|
+
}
|
|
79
|
+
async removeProfile(profileId) {
|
|
80
|
+
const config = await this.repository.load();
|
|
81
|
+
const index = config.profiles.findIndex((item) => item.id === profileId);
|
|
82
|
+
if (index === -1) {
|
|
83
|
+
throw new CliError(`Profile not found: ${profileId}`);
|
|
84
|
+
}
|
|
85
|
+
const [removedProfile] = config.profiles.splice(index, 1);
|
|
86
|
+
if (config.lastUsedProfileId === profileId) {
|
|
87
|
+
config.lastUsedProfileId = config.profiles[0]?.id;
|
|
88
|
+
}
|
|
89
|
+
await this.repository.save(config);
|
|
90
|
+
return removedProfile;
|
|
91
|
+
}
|
|
92
|
+
createProfileId(name) {
|
|
93
|
+
return name
|
|
94
|
+
.trim()
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
97
|
+
.replace(/(^-|-$)/g, '');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { confirm, input, select } from '@inquirer/prompts';
|
|
2
|
+
import { maskEnvValue, validateEnvKey } from '../utils/env.js';
|
|
3
|
+
import { CliError } from '../utils/errors.js';
|
|
4
|
+
export class PromptService {
|
|
5
|
+
async chooseProfile(profiles, lastUsedProfileId) {
|
|
6
|
+
if (profiles.length === 0) {
|
|
7
|
+
throw new CliError('No profiles available yet. Run xclaude config add first');
|
|
8
|
+
}
|
|
9
|
+
const answer = await select({
|
|
10
|
+
message: 'Choose a profile',
|
|
11
|
+
choices: profiles.map((profile) => ({
|
|
12
|
+
name: this.formatProfileLabel(profile, lastUsedProfileId),
|
|
13
|
+
value: profile.id,
|
|
14
|
+
description: this.describeProfile(profile),
|
|
15
|
+
})),
|
|
16
|
+
});
|
|
17
|
+
const profile = profiles.find((item) => item.id === answer);
|
|
18
|
+
if (!profile) {
|
|
19
|
+
throw new CliError('Selected profile does not exist');
|
|
20
|
+
}
|
|
21
|
+
return profile;
|
|
22
|
+
}
|
|
23
|
+
async chooseProfileForLaunch(profiles, lastUsedProfileId) {
|
|
24
|
+
if (profiles.length === 0) {
|
|
25
|
+
throw new CliError('No profiles available yet. Run xclaude config add first');
|
|
26
|
+
}
|
|
27
|
+
const answer = await select({
|
|
28
|
+
message: 'Launch profile',
|
|
29
|
+
choices: [
|
|
30
|
+
...profiles.map((profile) => ({
|
|
31
|
+
name: this.formatProfileLabel(profile, lastUsedProfileId),
|
|
32
|
+
value: profile.id,
|
|
33
|
+
})),
|
|
34
|
+
{
|
|
35
|
+
name: 'Exit',
|
|
36
|
+
value: '__exit__',
|
|
37
|
+
description: 'Exit without launching Claude',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
if (answer === '__exit__') {
|
|
42
|
+
throw new CliError('Launch cancelled');
|
|
43
|
+
}
|
|
44
|
+
const profile = profiles.find((item) => item.id === answer);
|
|
45
|
+
if (!profile) {
|
|
46
|
+
throw new CliError('Selected profile does not exist');
|
|
47
|
+
}
|
|
48
|
+
return profile;
|
|
49
|
+
}
|
|
50
|
+
async chooseConfigAction() {
|
|
51
|
+
return select({
|
|
52
|
+
message: 'Choose a config action',
|
|
53
|
+
choices: [
|
|
54
|
+
{ name: 'List profiles', value: 'list' },
|
|
55
|
+
{ name: 'Add profile', value: 'add' },
|
|
56
|
+
{ name: 'Edit profile', value: 'edit' },
|
|
57
|
+
{ name: 'Show config path', value: 'path' },
|
|
58
|
+
{ name: 'Exit', value: 'exit' },
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async confirmLaunchProfile(profile, extraArgs = []) {
|
|
63
|
+
console.log(`About to launch with profile: ${profile.name} (${profile.id})`);
|
|
64
|
+
for (const [key, value] of Object.entries(profile.env)) {
|
|
65
|
+
console.log(` ${key}=${maskEnvValue(key, value)}`);
|
|
66
|
+
}
|
|
67
|
+
console.log(` Claude args=${extraArgs.length > 0 ? extraArgs.join(' ') : '(none)'}`);
|
|
68
|
+
return confirm({
|
|
69
|
+
message: 'Launch Claude?',
|
|
70
|
+
default: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async chooseProfileOrBackOrExit(profiles, lastUsedProfileId) {
|
|
74
|
+
if (profiles.length === 0) {
|
|
75
|
+
return 'back';
|
|
76
|
+
}
|
|
77
|
+
const answer = await select({
|
|
78
|
+
message: 'Choose a profile',
|
|
79
|
+
choices: [
|
|
80
|
+
...profiles.map((profile) => ({
|
|
81
|
+
name: this.formatProfileLabel(profile, lastUsedProfileId),
|
|
82
|
+
value: profile.id,
|
|
83
|
+
description: this.describeProfile(profile),
|
|
84
|
+
})),
|
|
85
|
+
{
|
|
86
|
+
name: 'Back',
|
|
87
|
+
value: '__back__',
|
|
88
|
+
description: 'Return to the previous menu',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'Exit',
|
|
92
|
+
value: '__exit__',
|
|
93
|
+
description: 'Exit config',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
if (answer === '__back__') {
|
|
98
|
+
return 'back';
|
|
99
|
+
}
|
|
100
|
+
if (answer === '__exit__') {
|
|
101
|
+
return 'exit';
|
|
102
|
+
}
|
|
103
|
+
return profiles.find((item) => item.id === answer) ?? 'back';
|
|
104
|
+
}
|
|
105
|
+
async chooseProfileAction() {
|
|
106
|
+
return select({
|
|
107
|
+
message: 'Choose an action',
|
|
108
|
+
choices: [
|
|
109
|
+
{ name: 'Edit', value: 'edit' },
|
|
110
|
+
{ name: 'Remove', value: 'remove' },
|
|
111
|
+
{ name: 'Back', value: 'back' },
|
|
112
|
+
{ name: 'Exit', value: 'exit' },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async confirmRemoveProfile(profile) {
|
|
117
|
+
return confirm({
|
|
118
|
+
message: `Remove profile ${profile.name}?`,
|
|
119
|
+
default: false,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async promptProfileInput(existing) {
|
|
123
|
+
const name = await input({
|
|
124
|
+
message: 'Profile name',
|
|
125
|
+
default: existing?.name,
|
|
126
|
+
validate: (value) => (value.trim() ? true : 'Profile name is required'),
|
|
127
|
+
});
|
|
128
|
+
const anthropicAuthToken = await input({
|
|
129
|
+
message: 'ANTHROPIC_AUTH_TOKEN',
|
|
130
|
+
default: existing?.env.ANTHROPIC_AUTH_TOKEN,
|
|
131
|
+
});
|
|
132
|
+
const anthropicBaseUrl = await input({
|
|
133
|
+
message: 'ANTHROPIC_BASE_URL',
|
|
134
|
+
default: existing?.env.ANTHROPIC_BASE_URL,
|
|
135
|
+
});
|
|
136
|
+
const anthropicModel = await input({
|
|
137
|
+
message: 'ANTHROPIC_MODEL',
|
|
138
|
+
default: existing?.env.ANTHROPIC_MODEL,
|
|
139
|
+
});
|
|
140
|
+
const anthropicDefaultHaikuModel = await input({
|
|
141
|
+
message: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
142
|
+
default: existing?.env.ANTHROPIC_DEFAULT_HAIKU_MODEL,
|
|
143
|
+
});
|
|
144
|
+
const anthropicDefaultSonnetModel = await input({
|
|
145
|
+
message: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
146
|
+
default: existing?.env.ANTHROPIC_DEFAULT_SONNET_MODEL,
|
|
147
|
+
});
|
|
148
|
+
const anthropicDefaultOpusModel = await input({
|
|
149
|
+
message: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
150
|
+
default: existing?.env.ANTHROPIC_DEFAULT_OPUS_MODEL,
|
|
151
|
+
});
|
|
152
|
+
const claudeCodeSubagentModel = await input({
|
|
153
|
+
message: 'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
154
|
+
default: existing?.env.CLAUDE_CODE_SUBAGENT_MODEL,
|
|
155
|
+
});
|
|
156
|
+
const extraEnv = await this.promptExtraEnv(existing?.env ?? {});
|
|
157
|
+
return {
|
|
158
|
+
name: name.trim(),
|
|
159
|
+
anthropicAuthToken: anthropicAuthToken.trim() || undefined,
|
|
160
|
+
anthropicBaseUrl: anthropicBaseUrl.trim() || undefined,
|
|
161
|
+
anthropicModel: anthropicModel.trim() || undefined,
|
|
162
|
+
anthropicDefaultHaikuModel: anthropicDefaultHaikuModel.trim() || undefined,
|
|
163
|
+
anthropicDefaultSonnetModel: anthropicDefaultSonnetModel.trim() || undefined,
|
|
164
|
+
anthropicDefaultOpusModel: anthropicDefaultOpusModel.trim() || undefined,
|
|
165
|
+
claudeCodeSubagentModel: claudeCodeSubagentModel.trim() || undefined,
|
|
166
|
+
extraEnv,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
printProfiles(profiles, lastUsedProfileId) {
|
|
170
|
+
if (profiles.length === 0) {
|
|
171
|
+
console.log('No profiles');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const profile of profiles) {
|
|
175
|
+
const markers = [];
|
|
176
|
+
if (profile.id === lastUsedProfileId) {
|
|
177
|
+
markers.push('recent');
|
|
178
|
+
}
|
|
179
|
+
const marker = markers.length > 0 ? ` [${markers.join(' / ')}]` : '';
|
|
180
|
+
console.log(`- ${profile.name} (${profile.id})${marker}`);
|
|
181
|
+
for (const [key, value] of Object.entries(profile.env)) {
|
|
182
|
+
console.log(` ${key}=${maskEnvValue(key, value)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async promptExtraEnv(existingEnv) {
|
|
187
|
+
const extraEnv = {};
|
|
188
|
+
const presetEntries = Object.entries(existingEnv).filter(([key]) => key !== 'ANTHROPIC_AUTH_TOKEN' &&
|
|
189
|
+
key !== 'ANTHROPIC_BASE_URL' &&
|
|
190
|
+
key !== 'ANTHROPIC_MODEL' &&
|
|
191
|
+
key !== 'ANTHROPIC_DEFAULT_HAIKU_MODEL' &&
|
|
192
|
+
key !== 'ANTHROPIC_DEFAULT_SONNET_MODEL' &&
|
|
193
|
+
key !== 'ANTHROPIC_DEFAULT_OPUS_MODEL' &&
|
|
194
|
+
key !== 'CLAUDE_CODE_SUBAGENT_MODEL');
|
|
195
|
+
for (const [key, value] of presetEntries) {
|
|
196
|
+
const keep = await confirm({
|
|
197
|
+
message: `Keep ${key}=${maskEnvValue(key, value)}?`,
|
|
198
|
+
default: true,
|
|
199
|
+
});
|
|
200
|
+
if (keep) {
|
|
201
|
+
extraEnv[key] = value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
while (await confirm({
|
|
205
|
+
message: 'Add a custom ENV?',
|
|
206
|
+
default: false,
|
|
207
|
+
})) {
|
|
208
|
+
const key = (await input({
|
|
209
|
+
message: 'ENV name',
|
|
210
|
+
validate: (value) => {
|
|
211
|
+
try {
|
|
212
|
+
validateEnvKey(value.trim());
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
return error.message;
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
})).trim();
|
|
220
|
+
const value = await input({
|
|
221
|
+
message: `ENV value (${key})`,
|
|
222
|
+
});
|
|
223
|
+
extraEnv[key] = value;
|
|
224
|
+
}
|
|
225
|
+
return extraEnv;
|
|
226
|
+
}
|
|
227
|
+
formatProfileLabel(profile, lastUsedProfileId) {
|
|
228
|
+
const markers = [];
|
|
229
|
+
if (profile.id === lastUsedProfileId) {
|
|
230
|
+
markers.push('recent');
|
|
231
|
+
}
|
|
232
|
+
return markers.length > 0 ? `${profile.name} (${markers.join(', ')})` : profile.name;
|
|
233
|
+
}
|
|
234
|
+
describeProfile(profile) {
|
|
235
|
+
const pairs = Object.entries(profile.env).map(([key, value]) => `${key}=${maskEnvValue(key, value)}`);
|
|
236
|
+
return pairs.length > 0 ? pairs.join('\n') : 'No ENV';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CliError } from './errors.js';
|
|
2
|
+
const ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
3
|
+
export function validateEnvKey(key) {
|
|
4
|
+
if (!ENV_KEY_PATTERN.test(key)) {
|
|
5
|
+
throw new CliError(`Invalid ENV name: ${key}`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function mergeEnv(profileEnv) {
|
|
9
|
+
return {
|
|
10
|
+
...process.env,
|
|
11
|
+
...profileEnv,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function maskEnvValue(_key, value) {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
export function buildProfileEnv(input) {
|
|
18
|
+
const env = {};
|
|
19
|
+
if (input.anthropicAuthToken) {
|
|
20
|
+
env.ANTHROPIC_AUTH_TOKEN = input.anthropicAuthToken;
|
|
21
|
+
}
|
|
22
|
+
if (input.anthropicBaseUrl) {
|
|
23
|
+
env.ANTHROPIC_BASE_URL = input.anthropicBaseUrl;
|
|
24
|
+
}
|
|
25
|
+
if (input.anthropicModel) {
|
|
26
|
+
env.ANTHROPIC_MODEL = input.anthropicModel;
|
|
27
|
+
}
|
|
28
|
+
if (input.anthropicDefaultHaikuModel) {
|
|
29
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = input.anthropicDefaultHaikuModel;
|
|
30
|
+
}
|
|
31
|
+
if (input.anthropicDefaultSonnetModel) {
|
|
32
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = input.anthropicDefaultSonnetModel;
|
|
33
|
+
}
|
|
34
|
+
if (input.anthropicDefaultOpusModel) {
|
|
35
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = input.anthropicDefaultOpusModel;
|
|
36
|
+
}
|
|
37
|
+
if (input.claudeCodeSubagentModel) {
|
|
38
|
+
env.CLAUDE_CODE_SUBAGENT_MODEL = input.claudeCodeSubagentModel;
|
|
39
|
+
}
|
|
40
|
+
for (const [key, value] of Object.entries(input.extraEnv)) {
|
|
41
|
+
validateEnvKey(key);
|
|
42
|
+
env[key] = value;
|
|
43
|
+
}
|
|
44
|
+
return env;
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xclaude-launcher",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Launch Claude Code with reusable environment profiles.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/Jango26/claude-launcher.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/Jango26/claude-launcher",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Jango26/claude-launcher/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"cli",
|
|
18
|
+
"launcher",
|
|
19
|
+
"profiles"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"xclaude": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/index.js",
|
|
32
|
+
"dev": "tsc --watch",
|
|
33
|
+
"start": "node dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@inquirer/prompts": "^7.5.3"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^24.0.1",
|
|
40
|
+
"tsx": "^4.19.4",
|
|
41
|
+
"typescript": "^5.8.3"
|
|
42
|
+
}
|
|
43
|
+
}
|