@znmin/claude-remote-cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -1,157 +1,187 @@
1
- import { parseArgs } from 'node:util';
2
- import { createInterface } from 'node:readline/promises';
3
- import { Client, DEFAULTS } from './client.js';
4
- import { cachedBinary, ensureMutagen, findTar, VERSION } from './installer.js';
5
- import { exists, installSignalHandlers, readJSON, which } from './system.js';
6
- import { readFileSync } from 'node:fs';
7
-
8
- const PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
9
-
10
- const HELP = `${PACKAGE.name} ${PACKAGE.version}(非 Anthropic 官方客户端)
11
-
12
- 安装后使用:
13
- claude 首次引导设置服务器,之后启动远程 Claude Code
14
- claude --continue 将参数传给服务器上的 Claude
15
- claude --local 本次使用本地文件、终端和受控浏览器
16
- claude --remote 本次使用服务器工具与双向同步
17
- claude-remote configure [user@host] [--port 2222] [--claude /路径/claude]
18
- claude-remote mode local|remote 切换默认执行模式
19
- claude-remote status 查看全部同步状态和冲突
20
- claude-remote flush 等待当前目录同步完成
21
- claude-remote stop [--all | --name 名称]
22
- claude-remote doctor 检查本地环境,不连接服务器
23
- claude-remote setup-ssh [--enable-pubkey] 配置免密登录;可显式开启服务器公钥认证
24
- claude-remote install-sync 准备同步工具(通常自动完成)
25
- claude-remote install-browser 准备本地受控浏览器(本地模式自动完成)
26
- claude-remote run [Claude 参数] 启动远程 Claude
27
-
28
- configure 可选:--mode local|remote、--headless/--headed、--root 远程根目录、--ignore 忽略规则(可重复)。
29
- 新配置默认使用本地执行;旧配置保持服务器模式,直到显式切换。
30
- 服务器缺少默认 Claude Code 时会自动安装;首次启动需完成认证。
31
- 此工具不提供服务器或 Claude 账号。
32
- 详细诊断:CLAUDE_REMOTE_DEBUG=1 claude
33
- 仅服务器同步模式会在退出后继续后台同步;停止请运行 claude-remote stop。
34
- `;
35
-
36
- async function configure(client, args) {
37
- const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
38
- port: { type: 'string' }, root: { type: 'string' }, claude: { type: 'string' },
39
- ignore: { type: 'string', multiple: true }, mode: { type: 'string' }, headless: { type: 'boolean' }, headed: { type: 'boolean' }
40
- } });
41
- if (positionals.length > 1) throw new Error('仅接受一个 SSH 别名或 user@host。');
42
- if (values.headless && values.headed) throw new Error('--headless 和 --headed 不能同时使用。');
43
- let host = positionals[0], port = values.port;
44
- if (!host) {
45
- if (!process.stdin.isTTY) throw new Error('请指定服务器:claude-remote configure 用户名@服务器');
46
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
47
- try {
48
- console.log('设置你的 SSH 服务器(不要填写密码或私钥)。');
49
- host = (await prompt.question('SSH 别名或 user@host:')).trim();
50
- if (port === undefined) port = (await prompt.question('端口(留空使用 SSH 默认配置):')).trim() || undefined;
51
- } finally { prompt.close(); }
52
- }
53
- return client.configure({ host, port: port === undefined ? null : Number(port),
54
- root: values.root || DEFAULTS.root, claude: values.claude || DEFAULTS.claude, ignores: values.ignore || [],
55
- mode: values.mode, browserHeadless: values.headed ? false : values.headless });
56
- }
57
-
58
- export function executionOptions(args) {
59
- const forwarded = []; let mode, literal = false;
60
- for (const arg of args) {
61
- if (arg === '--') literal = true;
62
- if (!literal && (arg === '--local' || arg === '--remote')) {
63
- const selected = arg.slice(2);
64
- if (mode && mode !== selected) throw new Error('--local 和 --remote 不能同时使用。');
65
- mode = selected;
66
- } else forwarded.push(arg);
67
- }
68
- return { mode, forwarded };
69
- }
70
-
71
- export async function main(asClaude, args = process.argv.slice(2), client = new Client()) {
72
- if (asClaude) {
73
- const options = executionOptions(args);
74
- args = options.forwarded;
75
- if (args.length === 1 && ['--help', '-h'].includes(args[0])) { console.log(HELP); return 0; }
76
- if (args.length === 1 && ['--version', '-v'].includes(args[0])) { console.log(`${PACKAGE.name} ${PACKAGE.version}`); return 0; }
77
- const saved = await readJSON(client.configFile, {});
78
- if (!saved.host && !client.env.CLAUDE_REMOTE_HOST) {
79
- if (args.includes('--help') || args.includes('-h')) { console.log(HELP); return 0; }
80
- if (args.includes('--version')) { console.log(`${PACKAGE.name} ${PACKAGE.version}`); return 0; }
81
- if (process.stdin.isTTY) await configure(client, []);
82
- }
83
- const config = await client.settings();
84
- return client.launch(options.mode ? { ...config, mode: options.mode } : config, args);
85
- }
86
- const [action, ...rest] = args;
87
- if (!action || ['--help', '-h', 'help'].includes(action)) { console.log(HELP); return 0; }
88
- if (['--version', '-v'].includes(action)) { console.log(PACKAGE.version); return 0; }
89
- if (action === 'configure') { await configure(client, rest); return 0; }
90
- if (action === 'mode') {
91
- if (rest.length !== 1) throw new Error('用法:claude-remote mode local|remote');
92
- await client.setMode(rest[0]); return 0;
93
- }
94
- if (action === 'run') return main(true, rest[0] === '--' ? rest.slice(1) : rest, client);
95
- if (action === 'stop') {
96
- const { values, positionals } = parseArgs({ args: rest, allowPositionals: true,
97
- options: { all: { type: 'boolean' }, name: { type: 'string' } } });
98
- if (positionals.length || values.all && values.name) throw new Error('用法:claude-remote stop [--all | --name 名称]');
99
- await client.stop(values); return 0;
100
- }
101
- if (action === 'setup-ssh') {
102
- const { values, positionals } = parseArgs({ args: rest, allowPositionals: true,
103
- options: { 'enable-pubkey': { type: 'boolean' } } });
104
- if (positionals.length) throw new Error('用法:claude-remote setup-ssh [--enable-pubkey]');
105
- await client.ensureSSH(client, await client.settings(), { enablePubkey: Boolean(values['enable-pubkey']) });
106
- client.log('SSH 免密登录可用。'); return 0;
107
- }
108
- if (rest.length) throw new Error(`${action} 不接受额外参数。`);
109
- if (action === 'status') {
110
- await client.sync(['list', '--long', '--label-selector', 'claude-remote=true'], { capture: false }); return 0;
111
- }
112
- if (action === 'flush') {
113
- await client.flush((await client.project(await client.settings())).name); return 0;
114
- }
115
- if (action === 'install-sync') { console.log('同步工具:' + await ensureMutagen({ env: client.env })); return 0; }
116
- if (action === 'install-browser') {
117
- const { installBrowser } = await import('./local/browser.js');
118
- await installBrowser(client.log, { debug: client.debug }); console.log('浏览器已就绪。'); return 0;
119
- }
120
- if (action === 'doctor') {
121
- const [major, minor] = process.versions.node.split('.').map(Number);
122
- const nodeOk = major > 20 || major === 20 && minor >= 11;
123
- console.log(`Node.js:${process.version}${nodeOk ? '' : '(需要 20.11 或更高版本)'}\n平台:${process.platform}/${process.arch}`);
124
- for (const tool of ['ssh', 'ssh-keygen', 'scp']) console.log(`${tool}:${await which(tool, client.env.PATH) || '未安装'}`);
125
- console.log(`tar:${await findTar(client.env) || '未安装'}`);
126
- if (process.platform === 'win32') {
127
- const { describeShell, findShell } = await import('./local/shell.js');
128
- const shell = await findShell(client.env);
129
- console.log(`本地终端:${describeShell(shell)}(${shell.file})${shell.kind === 'powershell' ? ';安装 Git for Windows 后改用 Git Bash' : ''}`);
130
- const { conflictReport } = await import('./powershell-conflicts.js');
131
- for (const line of await conflictReport(client.env)) console.log(line);
132
- }
133
- try {
134
- const { browserReady } = await import('./local/browser.js');
135
- const { chromium } = await import('playwright');
136
- const ready = await browserReady();
137
- console.log(`本地浏览器:${ready ? chromium.executablePath() : '尚未准备(本地模式首次启动或 claude-remote install-browser 时下载)'}`);
138
- } catch (error) { console.log('本地浏览器:无法检查(' + error.message + ')'); }
139
- const cached = cachedBinary(client.env);
140
- console.log(`同步工具:${client.env.CLAUDE_REMOTE_MUTAGEN || (await exists(cached) ? cached : await which('mutagen', client.env.PATH)) || '首次使用时自动下载'}(目标版本 ${VERSION})`);
141
- const config = await readJSON(client.configFile, {});
142
- console.log(`配置:${client.configFile}\n服务器:${client.env.CLAUDE_REMOTE_HOST || config.host || '尚未配置'}`);
143
- console.log(`默认模式:${client.env.CLAUDE_REMOTE_MODE || config.mode || 'remote(旧配置)'}`);
144
- return 0;
145
- }
146
- throw new Error(`未知命令 ${action}。运行 claude-remote --help 查看用法。`);
147
- }
148
-
149
- export async function entry(asClaude) {
150
- installSignalHandlers();
151
- try { process.exitCode = await main(asClaude); }
152
- catch (error) {
153
- console.error('claude-remote:' + error.message);
154
- if (process.env.CLAUDE_REMOTE_DEBUG === '1') console.error(error.stack);
155
- process.exitCode = 1;
156
- }
157
- }
1
+ import { parseArgs } from 'node:util';
2
+ import { createInterface } from 'node:readline/promises';
3
+ import { Client, DEFAULTS } from './client.js';
4
+ import { cachedBinary, ensureMutagen, findTar, VERSION } from './installer.js';
5
+ import { exists, installSignalHandlers, readJSON, which } from './system.js';
6
+ import { readFileSync } from 'node:fs';
7
+ import { realpath } from 'node:fs/promises';
8
+
9
+ const PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
10
+
11
+ const HELP = `${PACKAGE.name} ${PACKAGE.version}(非 Anthropic 官方客户端)
12
+
13
+ 安装后使用:
14
+ claude 首次引导设置服务器,之后启动远程 Claude Code
15
+ claude --continue 将参数传给服务器上的 Claude
16
+ claude --local 本次使用本地文件、终端和受控浏览器
17
+ claude --remote 本次使用服务器工具与双向同步
18
+ claude-remote configure [user@host] [--port 2222] [--claude /路径/claude]
19
+ claude-remote mode local|remote 切换默认执行模式
20
+ claude-remote status 查看全部同步状态和冲突
21
+ claude-remote flush 等待当前目录同步完成
22
+ claude-remote stop [--all | --name 名称]
23
+ claude-remote doctor 检查本地环境,不连接服务器
24
+ claude-remote setup-ssh [--enable-pubkey] 配置免密登录;可显式开启服务器公钥认证
25
+ claude-remote install-sync 准备同步工具(通常自动完成)
26
+ claude-remote install-browser 准备本地受控浏览器(本地模式自动完成)
27
+ claude-remote mcp list|add|remove 管理本机 MCP(可访问内网),本地模式下提供给服务器上的 Claude
28
+ claude-remote run [Claude 参数] 启动远程 Claude
29
+
30
+ configure 可选:--mode local|remote、--headless/--headed、--root 远程根目录、--ignore 忽略规则(可重复)。
31
+ 新配置默认使用本地执行;旧配置保持服务器模式,直到显式切换。
32
+ 服务器缺少默认 Claude Code 时会自动安装;首次启动需完成认证。
33
+ 此工具不提供服务器或 Claude 账号。
34
+ 详细诊断:CLAUDE_REMOTE_DEBUG=1 claude
35
+ 仅服务器同步模式会在退出后继续后台同步;停止请运行 claude-remote stop。
36
+ `;
37
+
38
+ async function configure(client, args) {
39
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
40
+ port: { type: 'string' }, root: { type: 'string' }, claude: { type: 'string' },
41
+ ignore: { type: 'string', multiple: true }, mode: { type: 'string' }, headless: { type: 'boolean' }, headed: { type: 'boolean' }
42
+ } });
43
+ if (positionals.length > 1) throw new Error('仅接受一个 SSH 别名或 user@host。');
44
+ if (values.headless && values.headed) throw new Error('--headless 和 --headed 不能同时使用。');
45
+ let host = positionals[0], port = values.port;
46
+ if (!host) {
47
+ if (!process.stdin.isTTY) throw new Error('请指定服务器:claude-remote configure 用户名@服务器');
48
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
49
+ try {
50
+ console.log('设置你的 SSH 服务器(不要填写密码或私钥)。');
51
+ host = (await prompt.question('SSH 别名或 user@host:')).trim();
52
+ if (port === undefined) port = (await prompt.question('端口(留空使用 SSH 默认配置):')).trim() || undefined;
53
+ } finally { prompt.close(); }
54
+ }
55
+ return client.configure({ host, port: port === undefined ? null : Number(port),
56
+ root: values.root || DEFAULTS.root, claude: values.claude || DEFAULTS.claude, ignores: values.ignore || [],
57
+ mode: values.mode, browserHeadless: values.headed ? false : values.headless });
58
+ }
59
+
60
+ export function executionOptions(args) {
61
+ const forwarded = []; let mode, literal = false;
62
+ for (const arg of args) {
63
+ if (arg === '--') literal = true;
64
+ if (!literal && (arg === '--local' || arg === '--remote')) {
65
+ const selected = arg.slice(2);
66
+ if (mode && mode !== selected) throw new Error('--local 和 --remote 不能同时使用。');
67
+ mode = selected;
68
+ } else forwarded.push(arg);
69
+ }
70
+ return { mode, forwarded };
71
+ }
72
+
73
+ // Mirrors Client: configure saves new setups as local; a saved server without
74
+ // a mode predates local mode and stays remote.
75
+ export function defaultModeLabel(config, env = {}) {
76
+ if (env.CLAUDE_REMOTE_MODE) return `${env.CLAUDE_REMOTE_MODE}(环境变量 CLAUDE_REMOTE_MODE)`;
77
+ if (config.mode) return config.mode;
78
+ if (config.host) return 'remote(旧配置)';
79
+ if (env.CLAUDE_REMOTE_HOST) return 'remote(未保存配置)';
80
+ return 'local(配置服务器后的默认值)';
81
+ }
82
+
83
+ async function mcp(client, args) {
84
+ const { McpStore, runMcpCommand } = await import('./local/mcp-config.js');
85
+ const root = await realpath(client.cwd);
86
+ return runMcpCommand(new McpStore({ directory: client.directory, lockFile: client.lockFile, root }), args);
87
+ }
88
+
89
+ export async function main(asClaude, args = process.argv.slice(2), client = new Client()) {
90
+ if (asClaude) {
91
+ const options = executionOptions(args);
92
+ args = options.forwarded;
93
+ if (args.length === 1 && ['--help', '-h'].includes(args[0])) { console.log(HELP); return 0; }
94
+ if (args.length === 1 && ['--version', '-v'].includes(args[0])) { console.log(`${PACKAGE.name} ${PACKAGE.version}`); return 0; }
95
+ const saved = await readJSON(client.configFile, {});
96
+ // In local mode MCP servers run on this computer, so `claude mcp` manages them here.
97
+ if (args[0] === 'mcp') {
98
+ const mode = options.mode || client.env.CLAUDE_REMOTE_MODE || saved.mode || (saved.host || client.env.CLAUDE_REMOTE_HOST ? 'remote' : 'local');
99
+ if (mode === 'local') return mcp(client, args.slice(1));
100
+ }
101
+ if (!saved.host && !client.env.CLAUDE_REMOTE_HOST) {
102
+ if (args.includes('--help') || args.includes('-h')) { console.log(HELP); return 0; }
103
+ if (args.includes('--version')) { console.log(`${PACKAGE.name} ${PACKAGE.version}`); return 0; }
104
+ if (process.stdin.isTTY) await configure(client, []);
105
+ }
106
+ const config = await client.settings();
107
+ return client.launch(options.mode ? { ...config, mode: options.mode } : config, args);
108
+ }
109
+ const [action, ...rest] = args;
110
+ if (!action || ['--help', '-h', 'help'].includes(action)) { console.log(HELP); return 0; }
111
+ if (['--version', '-v'].includes(action)) { console.log(PACKAGE.version); return 0; }
112
+ if (action === 'configure') { await configure(client, rest); return 0; }
113
+ if (action === 'mode') {
114
+ if (rest.length !== 1) throw new Error('用法:claude-remote mode local|remote');
115
+ await client.setMode(rest[0]); return 0;
116
+ }
117
+ if (action === 'run') return main(true, rest[0] === '--' ? rest.slice(1) : rest, client);
118
+ if (action === 'mcp') return mcp(client, rest);
119
+ if (action === 'stop') {
120
+ const { values, positionals } = parseArgs({ args: rest, allowPositionals: true,
121
+ options: { all: { type: 'boolean' }, name: { type: 'string' } } });
122
+ if (positionals.length || values.all && values.name) throw new Error('用法:claude-remote stop [--all | --name 名称]');
123
+ await client.stop(values); return 0;
124
+ }
125
+ if (action === 'setup-ssh') {
126
+ const { values, positionals } = parseArgs({ args: rest, allowPositionals: true,
127
+ options: { 'enable-pubkey': { type: 'boolean' } } });
128
+ if (positionals.length) throw new Error('用法:claude-remote setup-ssh [--enable-pubkey]');
129
+ await client.ensureSSH(client, await client.settings(), { enablePubkey: Boolean(values['enable-pubkey']) });
130
+ client.log('SSH 免密登录可用。'); return 0;
131
+ }
132
+ if (rest.length) throw new Error(`${action} 不接受额外参数。`);
133
+ if (action === 'status') {
134
+ await client.sync(['list', '--long', '--label-selector', 'claude-remote=true'], { capture: false }); return 0;
135
+ }
136
+ if (action === 'flush') {
137
+ await client.flush((await client.project(await client.settings())).name); return 0;
138
+ }
139
+ if (action === 'install-sync') { console.log('同步工具:' + await ensureMutagen({ env: client.env })); return 0; }
140
+ if (action === 'install-browser') {
141
+ const { browserName, installBrowser } = await import('./local/browser.js');
142
+ const choice = await installBrowser(client.log, { debug: client.debug, env: client.env });
143
+ console.log(`浏览器已就绪:${browserName(choice)}(${choice.file})${choice.kind === 'chromium' ? '' : ',无需下载'}`); return 0;
144
+ }
145
+ if (action === 'doctor') {
146
+ const [major, minor] = process.versions.node.split('.').map(Number);
147
+ const nodeOk = major > 20 || major === 20 && minor >= 11;
148
+ console.log(`Node.js:${process.version}${nodeOk ? '' : '(需要 20.11 或更高版本)'}\n平台:${process.platform}/${process.arch}`);
149
+ for (const tool of ['ssh', 'ssh-keygen', 'scp']) console.log(`${tool}:${await which(tool, client.env.PATH) || '未安装'}`);
150
+ console.log(`tar:${await findTar(client.env) || '未安装'}`);
151
+ if (process.platform === 'win32') {
152
+ const { describeShell, findShell } = await import('./local/shell.js');
153
+ const shell = await findShell(client.env);
154
+ console.log(`本地终端:${describeShell(shell)}(${shell.file})${shell.kind === 'powershell' ? ';安装 Git for Windows 后改用 Git Bash' : ''}`);
155
+ const { conflictReport } = await import('./powershell-conflicts.js');
156
+ for (const line of await conflictReport(client.env)) console.log(line);
157
+ }
158
+ try {
159
+ const { browserName, browserReady, chooseBrowser } = await import('./local/browser.js');
160
+ const choice = await chooseBrowser(client.env);
161
+ if (choice.kind !== 'chromium') console.log(`本地浏览器:${browserName(choice)}(${choice.file}),无需下载`);
162
+ else {
163
+ const { chromium } = await import('playwright');
164
+ const ready = await browserReady();
165
+ console.log(`本地浏览器:${browserName(choice)},${ready ? chromium.executablePath() : '尚未准备(本地模式首次启动或 claude-remote install-browser 时下载)'}` +
166
+ (choice.explicit ? '' : ';安装 Chrome 或 Edge 后可直接使用'));
167
+ }
168
+ } catch (error) { console.log('本地浏览器:无法检查(' + error.message + ')'); }
169
+ const cached = cachedBinary(client.env);
170
+ console.log(`同步工具:${client.env.CLAUDE_REMOTE_MUTAGEN || (await exists(cached) ? cached : await which('mutagen', client.env.PATH)) || '首次使用时自动下载'}(目标版本 ${VERSION})`);
171
+ const config = await readJSON(client.configFile, {});
172
+ console.log(`配置:${client.configFile}\n服务器:${client.env.CLAUDE_REMOTE_HOST || config.host || '尚未配置'}`);
173
+ console.log(`默认模式:${defaultModeLabel(config, client.env)}`);
174
+ return 0;
175
+ }
176
+ throw new Error(`未知命令 ${action}。运行 claude-remote --help 查看用法。`);
177
+ }
178
+
179
+ export async function entry(asClaude) {
180
+ installSignalHandlers();
181
+ try { process.exitCode = await main(asClaude); }
182
+ catch (error) {
183
+ console.error('claude-remote:' + error.message);
184
+ if (process.env.CLAUDE_REMOTE_DEBUG === '1') console.error(error.stack);
185
+ process.exitCode = 1;
186
+ }
187
+ }