@znmin/claude-remote-cli 0.2.2 → 0.2.4

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 CHANGED
@@ -25,10 +25,10 @@ npm install -g git+https://github.com/znmin/claude-remote-cli.git
25
25
  也可以从已下载的分发包安装:
26
26
 
27
27
  ```bash
28
- npm install -g ./znmin-claude-remote-cli-0.2.2.tgz
28
+ npm install -g ./znmin-claude-remote-cli-0.2.4.tgz
29
29
  ```
30
30
 
31
- 若系统 npm 全局目录不可写,可使用 `npm install -g --prefix "$HOME/.local" ./znmin-claude-remote-cli-0.2.2.tgz`,并确保 `~/.local/bin` 在 PATH 中;卸载时也使用相同的 `--prefix`。
31
+ 若系统 npm 全局目录不可写,可使用 `npm install -g --prefix "$HOME/.local" ./znmin-claude-remote-cli-0.2.4.tgz`,并确保 `~/.local/bin` 在 PATH 中;卸载时也使用相同的 `--prefix`。
32
32
 
33
33
  npm 中不带作用域的 `claude-remote-cli` 属于其他项目。本项目使用独立的 `@znmin/claude-remote-cli` 作用域包名;请安装完整包名,或使用上述 GitHub 地址及本项目分发的 `.tgz` 文件。
34
34
 
@@ -81,7 +81,17 @@ claude --continue
81
81
  claude-remote setup-ssh
82
82
  ```
83
83
 
84
- 非交互运行且免密连接不可用时会停止并提示先在终端完成设置,不自动修改密钥。服务器必须允许公钥登录,并允许当前账号修改默认 `authorized_keys` 文件;强制额外验证码或采用自定义授权路径的服务器需要管理员配置,程序不会修改 sshd 策略。已有带口令密钥如需免提示,可预先交给本机 SSH Agent 管理。
84
+ 0.2.3 起,首次密码登录后会先读取当前账号和连接对应的有效 sshd 策略。发现 `PubkeyAuthentication no` 或不允许单独使用公钥的 `AuthenticationMethods` 时,在生成/添加密钥之前停止,给出具体原因;无法读取策略时会说明限制并尝试普通公钥授权。
85
+
86
+ 如果服务器关闭了公钥登录,可以显式运行:
87
+
88
+ ```bash
89
+ claude-remote setup-ssh --enable-pubkey
90
+ ```
91
+
92
+ 该选项只支持 root 登录的标准 Linux systemd SSH 服务(`ssh.service` 或 `sshd.service`),使用默认 `/etc/ssh/sshd_config`,不支持自定义 `-f`/`-o` 启动参数。它会备份原配置,只在全局配置最前面添加 `PubkeyAuthentication yes`,不改密码登录、root 登录或多因素策略;通过 `sshd -t` 和当前连接的有效策略校验后重新加载服务,随后继续添加公钥并独立验证登录。校验或加载失败会恢复备份并尝试重新加载原配置;备份路径会打印出来。普通启动不会自动修改服务策略。额外认证、Match 限制、容器/非 systemd 服务和自定义授权路径需要管理员处理。
93
+
94
+ 非交互运行且免密连接不可用时会停止并提示先在终端完成设置,不自动修改密钥。服务器必须允许当前账号修改默认 `authorized_keys` 文件。已有带口令密钥如需免提示,可预先交给本机 SSH Agent 管理。
85
95
 
86
96
  若需要使用特定的 Claude 可执行文件,可指定绝对路径;自定义路径不存在时会提示修正配置,不会自动改用其他程序:
87
97
 
@@ -171,6 +181,16 @@ CLAUDE_REMOTE_HOST=another-server CLAUDE_REMOTE_PORT=2222 claude
171
181
  - 不允许同步整个本机用户目录或磁盘根目录。不会同步用户目录中的 Claude 登录信息;远程使用自己的登录状态和工具配置。
172
182
  - 本版不提供 tmux 会话恢复。网络中断后文件同步会尝试重连,远程 Claude 进程是否继续运行取决于服务器的进程行为。
173
183
 
184
+ ## 运行提示与调试
185
+
186
+ 正常使用时,只显示需要等待或操作的提示,例如首次下载组件、SSH 验证、服务器策略修复结果,以及错误。已配置好的本地模式直接进入 Claude,不重复显示模式说明、项目路径或浏览器缓存检查。服务器同步模式会简短提示“正在同步…”。`configure`、`setup-ssh`、`doctor` 等显式管理命令仍显示操作结果;Claude 自身的界面和 SSH 验证提示不受影响。
187
+
188
+ 需要查看连接目标、项目路径、密钥配置过程、缓存检查、同步工具过程输出和错误堆栈时,临时启用:
189
+
190
+ ```bash
191
+ CLAUDE_REMOTE_DEBUG=1 claude
192
+ ```
193
+
174
194
  ## 高级环境变量
175
195
 
176
196
  | 变量 | 用途 |
@@ -184,7 +204,7 @@ CLAUDE_REMOTE_HOST=another-server CLAUDE_REMOTE_PORT=2222 claude
184
204
  | `CLAUDE_REMOTE_MUTAGEN` | 使用自行准备的 Mutagen 可执行文件绝对路径(应为 0.18.1,并放置配套代理包) |
185
205
  | `CLAUDE_REMOTE_PREPARE_SYNC=1` | npm 安装阶段提前准备同步工具,默认按需准备 |
186
206
  | `CLAUDE_REMOTE_SKIP_DOWNLOAD=1` | 跳过安装阶段的同步工具准备 |
187
- | `CLAUDE_REMOTE_DEBUG=1` | 出错时显示调试信息 |
207
+ | `CLAUDE_REMOTE_DEBUG=1` | 显示详细启动过程和错误堆栈 |
188
208
 
189
209
  `npm install --ignore-scripts` 也受支持,首次使用各模式时准备对应组件。无法访问 GitHub 时可自行准备 Mutagen 并使用 `CLAUDE_REMOTE_MUTAGEN`。浏览器缓存由 Playwright 管理,支持其 `PLAYWRIGHT_BROWSERS_PATH` 环境变量。
190
210
 
@@ -200,7 +220,7 @@ npm test
200
220
  CLAUDE_REMOTE_INTEGRATION=1 CLAUDE_REMOTE_BROWSER_TEST=1 npm test
201
221
 
202
222
  npm pack
203
- npm install -g ./znmin-claude-remote-cli-0.2.2.tgz
223
+ npm install -g ./znmin-claude-remote-cli-0.2.4.tgz
204
224
 
205
225
  # 确定包名、版本和使用条款后,使用有发布权限的 npm 账号发布
206
226
  npm login
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@znmin/claude-remote-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Run Claude Code on an SSH server with local filesystem, terminal and browser tools, or remote bidirectional sync",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -3,10 +3,9 @@ import { ensureMutagen } from '../src/installer.js';
3
3
  if (process.env.CLAUDE_REMOTE_PREPARE_SYNC === '1' && process.env.CLAUDE_REMOTE_SKIP_DOWNLOAD !== '1') {
4
4
  try {
5
5
  await ensureMutagen();
6
- console.log('claude 已安装。运行 claude-remote configure 用户名@服务器,或直接运行 claude 开始配置。');
7
6
  } catch (error) {
8
7
  // Offline installation and npm --ignore-scripts remain supported: retry at first use.
9
8
  console.warn(`CLI 已安装,同步工具准备未完成:${error.message}\n首次连接时会重试。`);
10
9
  }
11
10
  }
12
- console.log('安装完成:运行 claude 配置服务器并启动。新配置默认本地执行;同步工具和浏览器在对应模式首次使用时自动准备。');
11
+ console.log('安装完成,运行 claude 开始。');
package/src/cli.js CHANGED
@@ -20,7 +20,7 @@ const HELP = `${PACKAGE.name} ${PACKAGE.version}(非 Anthropic 官方客户端
20
20
  claude-remote flush 等待当前目录同步完成
21
21
  claude-remote stop [--all | --name 名称]
22
22
  claude-remote doctor 检查本地环境,不连接服务器
23
- claude-remote setup-ssh 配置并验证服务器 SSH 免密登录(启动时自动检查)
23
+ claude-remote setup-ssh [--enable-pubkey] 配置免密登录;可显式开启服务器公钥认证
24
24
  claude-remote install-sync 准备同步工具(通常自动完成)
25
25
  claude-remote install-browser 准备本地受控浏览器(本地模式自动完成)
26
26
  claude-remote run [Claude 参数] 启动远程 Claude
@@ -29,6 +29,7 @@ configure 可选:--mode local|remote、--headless/--headed、--root 远程根
29
29
  新配置默认使用本地执行;旧配置保持服务器模式,直到显式切换。
30
30
  服务器缺少默认 Claude Code 时会自动安装;首次启动需完成认证。
31
31
  此工具不提供服务器或 Claude 账号。
32
+ 详细诊断:CLAUDE_REMOTE_DEBUG=1 claude
32
33
  仅服务器同步模式会在退出后继续后台同步;停止请运行 claude-remote stop。
33
34
  `;
34
35
 
@@ -97,10 +98,14 @@ export async function main(asClaude, args = process.argv.slice(2), client = new
97
98
  if (positionals.length || values.all && values.name) throw new Error('用法:claude-remote stop [--all | --name 名称]');
98
99
  await client.stop(values); return 0;
99
100
  }
100
- if (rest.length) throw new Error(`${action} 不接受额外参数。`);
101
101
  if (action === 'setup-ssh') {
102
- await client.ensureSSH(client, await client.settings()); return 0;
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;
103
107
  }
108
+ if (rest.length) throw new Error(`${action} 不接受额外参数。`);
104
109
  if (action === 'status') {
105
110
  await client.sync(['list', '--long', '--label-selector', 'claude-remote=true'], { capture: false }); return 0;
106
111
  }
@@ -110,7 +115,7 @@ export async function main(asClaude, args = process.argv.slice(2), client = new
110
115
  if (action === 'install-sync') { console.log('同步工具:' + await ensureMutagen({ env: client.env })); return 0; }
111
116
  if (action === 'install-browser') {
112
117
  const { installBrowser } = await import('./local/browser.js');
113
- await installBrowser(client.log); console.log('本地受控浏览器已就绪。'); return 0;
118
+ await installBrowser(client.log, { debug: client.debug }); console.log('浏览器已就绪。'); return 0;
114
119
  }
115
120
  if (action === 'doctor') {
116
121
  console.log(`Node.js:${process.version}\n平台:${process.platform}/${process.arch}`);
package/src/client.js CHANGED
@@ -63,6 +63,9 @@ export class Client {
63
63
  this.ensureMutagen = options.ensureMutagen || ensureMutagen;
64
64
  this.log = options.log || console.error;
65
65
  this.warn = options.warn || console.error;
66
+ this.debug = (...messages) => {
67
+ if (this.env.CLAUDE_REMOTE_DEBUG === '1') this.log(...messages);
68
+ };
66
69
  this.tty = options.tty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
67
70
  }
68
71
 
@@ -74,8 +77,10 @@ export class Client {
74
77
  ignores: [...DEFAULTS.ignores, ...(input.ignores || [])],
75
78
  client_id: old.client_id || randomUUID().replaceAll('-', '').slice(0, 12) });
76
79
  await writeJSON(this.configFile, config);
77
- this.log(`已配置服务器:${config.host}。进入项目目录后运行 claude。`);
78
- this.log(`当前模式:${config.mode === 'local' ? '本地执行' : '服务器执行与双向同步'}。旧后台同步不会自动停止,可用 claude-remote stop --all 停止。`);
80
+ this.log(`已保存:${config.host}(${config.mode === 'local' ? '本地执行' : '远程同步'})。`);
81
+ this.debug(`配置文件:${this.configFile}`);
82
+ if (old.host && (old.host !== config.host || (old.mode || 'remote') !== config.mode))
83
+ this.log('停止旧同步:claude-remote stop --all');
79
84
  return config;
80
85
  });
81
86
  }
@@ -150,6 +155,7 @@ export class Client {
150
155
  }
151
156
 
152
157
  async launch(config, extra) {
158
+ this.debug(`连接:${config.host}${config.port ? ':' + config.port : ''},模式:${config.mode || 'remote'}`);
153
159
  await this.ensureSSH(this, config);
154
160
  if (config.mode === 'local') {
155
161
  const { launchLocal } = await import('./local-mode.js');
@@ -178,21 +184,22 @@ export class Client {
178
184
  await this.remote(config, ensureRemoteClaude(config.claude) +
179
185
  'mkdir -p -- ' + remotePath(item.remote), false, { timeout: 600000 });
180
186
  if (existing.some(session => session.name === item.name)) {
181
- await this.sync(['resume', item.name], { timeout: 60000, capture: !this.tty });
187
+ await this.sync(['resume', item.name], { timeout: 60000, capture: this.env.CLAUDE_REMOTE_DEBUG !== '1' || !this.tty });
182
188
  } else {
183
189
  const endpoint = config.host + ':' + (config.port != null ? config.port + ':' : '') +
184
190
  (item.remote.startsWith('/') ? item.remote : '~/' + item.remote);
185
191
  const args = ['create', '--name', item.name, '--label', 'claude-remote=true', '--mode', 'two-way-safe',
186
192
  '--no-global-configuration', '--no-ignore-vcs', '--watch-polling-interval', '2'];
187
193
  for (const pattern of config.ignores) args.push('--ignore', pattern);
188
- await this.sync([...args, item.local, endpoint], { timeout: 180000, capture: !this.tty });
194
+ await this.sync([...args, item.local, endpoint], { timeout: 180000, capture: this.env.CLAUDE_REMOTE_DEBUG !== '1' || !this.tty });
189
195
  }
190
196
  registry[item.name] = item;
191
197
  await writeJSON(this.registryFile, registry);
192
198
  });
193
- this.log(`本地:${item.local}\n远程:${config.host}:${item.remote}\n正在完成双向同步……`);
199
+ this.debug(`本地:${item.local}\n远程:${config.host}:${item.remote}`);
200
+ this.log('正在同步…');
194
201
  await this.flush(item.name);
195
- this.log('开始远程 Claude Code。退出后继续后台同步;停止请运行 claude-remote stop。');
202
+ this.debug('同步完成,启动远程 Claude Code。退出后继续后台同步;停止请运行 claude-remote stop。');
196
203
  let code = 1;
197
204
  try {
198
205
  code = (await this.remote(config, prefix + 'cd -- ' + remotePath(item.remote) +
package/src/installer.js CHANGED
@@ -64,7 +64,7 @@ export async function ensureMutagen({ env = process.env, forceDownload = false }
64
64
  const temporary = await fs.mkdtemp(path.join(path.dirname(destination), '.mutagen-install-'));
65
65
  try {
66
66
  const archive = path.join(temporary, 'download.tar.gz');
67
- console.error(`正在下载同步工具 Mutagen ${VERSION}(首次使用,可能需要几分钟)……`);
67
+ console.error('正在下载同步工具…');
68
68
  await download(`https://github.com/mutagen-io/mutagen/releases/download/v${VERSION}/${archiveName}`, archive);
69
69
  await verifyChecksum(archive, checksum);
70
70
  // Extract only the two required files from the authenticated archive.
@@ -8,13 +8,14 @@ import { execute, exists, withLock } from '../system.js';
8
8
  const require = createRequire(import.meta.url);
9
9
  let preparation;
10
10
 
11
- export async function installBrowser(log = console.error) {
11
+ export async function installBrowser(log = console.error, { debug = () => {} } = {}) {
12
12
  if (!preparation) preparation = (async () => {
13
13
  const cli = path.join(path.dirname(require.resolve('playwright/package.json')), 'cli.js');
14
14
  // Keep the lock outside Playwright's cache: its installer replaces browser directories.
15
15
  const lock = path.join(process.env.CLAUDE_REMOTE_CACHE_DIR || path.join(os.homedir(), '.cache', 'claude-remote'), 'browser-install');
16
16
  await withLock(lock, async () => {
17
- log('正在准备本地受控浏览器(首次使用或升级时自动下载)……');
17
+ if (!await exists(chromium.executablePath())) log('正在下载浏览器…');
18
+ else debug('检查浏览器缓存:' + chromium.executablePath());
18
19
  // The CLI checks completion markers and prepares headed AND headless binaries.
19
20
  const result = await execute(process.execPath, [cli, 'install', 'chromium'], { capture: true, timeout: 600000 });
20
21
  if (!await exists(chromium.executablePath())) throw new Error('浏览器安装未完成:' + result.stderr);
package/src/local-mode.js CHANGED
@@ -63,7 +63,7 @@ export async function launchLocal(client, config, extra, dependencies = {}) {
63
63
  const root = await fs.realpath(client.cwd), home = await fs.realpath(client.home);
64
64
  if (root === home || root === path.parse(root).root) throw new Error('请进入具体项目目录,不允许以用户目录或磁盘根目录作为项目。');
65
65
  const prepareBrowser = dependencies.installBrowser || installBrowser;
66
- await prepareBrowser(client.log);
66
+ await prepareBrowser(client.log, { debug: client.debug });
67
67
  const gateway = await (dependencies.startLocalServer || startLocalServer)({ root, env: client.env,
68
68
  headless: client.env.CLAUDE_REMOTE_HEADLESS === undefined ? Boolean(config.browserHeadless) : client.env.CLAUDE_REMOTE_HEADLESS === '1', log: client.log });
69
69
  let tunnel, configFile;
@@ -82,7 +82,7 @@ export async function launchLocal(client, config, extra, dependencies = {}) {
82
82
  headers: { Authorization: 'Bearer ' + gateway.token }, timeout: 180000 } } };
83
83
  await remote('set -e; umask 077; mkdir -p -- ' + remotePath(remoteDirectory) + '; cat > ' + remotePath(configFile),
84
84
  { input: JSON.stringify(mcp) });
85
- client.log(`本地执行模式:${root}\n文件、命令和浏览器在本机运行;项目文件不通过本模式同步。`);
85
+ client.debug(`本地执行模式:${root}\n文件、命令和浏览器在本机运行;项目文件不通过本模式同步。`);
86
86
  const args = localClaudeArguments(path.posix.basename(configFile), root, extra);
87
87
  // Keep the parent shell so EXIT can remove the token-bearing temporary config.
88
88
  const script = remoteEnvironment + 'cd -- ' + remotePath(remoteDirectory) + ' || exit 1; ' +
package/src/ssh-auth.js CHANGED
@@ -4,6 +4,7 @@ import os from 'node:os';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { sshBase } from './ssh.js';
6
6
  import { exists, quote, which, withLock } from './system.js';
7
+ import { parseSSHPolicy, sshPolicyScript } from './ssh-policy.js';
7
8
 
8
9
  // Force a new public-key connection: an existing multiplexed/password session
9
10
  // must never be mistaken for successful key authentication.
@@ -21,7 +22,7 @@ export function authorizeKeyScript(publicKey) {
21
22
  'printf "\\n%s\\n" "$key" >> "$HOME/.ssh/authorized_keys"; fi';
22
23
  }
23
24
 
24
- export async function ensureSSH(client, config) {
25
+ export async function ensureSSH(client, config, { enablePubkey = false } = {}) {
25
26
  if (!await which('ssh', client.env.PATH)) throw new Error('找不到系统 SSH 客户端。');
26
27
  const run = (args, options = {}) => client.execute('ssh', [...sshBase(config), ...args],
27
28
  { env: client.env, timeout: 30000, ...options });
@@ -38,17 +39,33 @@ export async function ensureSSH(client, config) {
38
39
  const temporary = await fs.mkdtemp(path.join(base, 'ccra-'));
39
40
  const socket = path.join(temporary, 'ssh');
40
41
  try {
41
- client.log('正在配置 SSH 免密登录。首次连接请确认服务器指纹,并按 SSH 提示输入服务器密码。');
42
+ client.log('请按提示完成 SSH 验证。');
42
43
  // Authenticate once; subsequent installation reuses this private socket.
43
44
  // Do not capture authentication output or handle passwords in JavaScript.
44
45
  await run(['-M', '-S', socket, '-o', 'ControlPersist=no', '-o', 'ControlMaster=yes',
45
46
  '-f', '-N', config.host], { timeout: 180000, capture: false });
47
+ const inspect = await run(['-S', socket, config.host, 'sh -c ' + quote(sshPolicyScript({ enable: enablePubkey }))],
48
+ { capture: true });
49
+ const policy = parseSSHPolicy(inspect.stdout);
50
+ if (policy.backup) client.log('已开启公钥登录。配置备份:' + policy.backup);
51
+ if (policy.pubkeyauthentication === 'no')
52
+ throw new Error('服务器关闭了公钥登录(PubkeyAuthentication no),尚未生成或添加密钥。' +
53
+ (policy.uid === '0' ? '如需开启,请运行:claude-remote setup-ssh --enable-pubkey(备份配置、校验后重新加载,保留密码登录)。' :
54
+ '请联系管理员开启该账号的公钥登录,再运行 claude-remote setup-ssh。'));
55
+ if (policy.authenticationmethods && policy.authenticationmethods !== 'any' &&
56
+ !policy.authenticationmethods.split(/\s+/).includes('publickey'))
57
+ throw new Error('服务器 AuthenticationMethods=' + policy.authenticationmethods +
58
+ ',不允许仅凭公钥登录;请联系管理员确认认证策略。未修改该策略,尚未生成或添加密钥。');
59
+ if (policy.status !== 'ok') {
60
+ if (enablePubkey) throw new Error('无法读取服务器有效 SSH 配置,未自动修改服务器策略;请联系管理员检查。');
61
+ client.debug('无法读取服务器 SSH 策略,尝试普通公钥授权。');
62
+ }
46
63
  const id = createHash('sha256').update(config.host + '\0' + (config.port ?? '')).digest('hex').slice(0, 20);
47
64
  const key = path.join(directory, 'id_ed25519_' + id);
48
65
  await fs.chmod(directory, 0o700);
49
66
  if (!await exists(key)) {
50
67
  if (await exists(key + '.pub')) throw new Error('专用 SSH 私钥缺失但公钥仍存在,请检查:' + key);
51
- client.log('创建仅供该服务器使用的无口令 SSH 密钥,私钥保存在本机,文件权限设为仅本人可读。');
68
+ client.debug('创建专用 SSH 密钥:' + key);
52
69
  await client.execute('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', 'claude-remote-cli', '-f', key],
53
70
  { env: client.env, capture: true, timeout: 30000 });
54
71
  }
@@ -74,7 +91,7 @@ export async function ensureSSH(client, config) {
74
91
  });
75
92
  if (!previous.includes(block)) await fs.appendFile(configFile, block, { mode: 0o600 });
76
93
  if (!await probe()) throw new Error('密钥已授权,但默认 SSH 配置验证失败。请检查 ~/.ssh/config 的 IdentityFile 和 IdentitiesOnly 设置。');
77
- client.log('SSH 免密登录已配置并验证成功,后续连接无需输入服务器密码。');
94
+ client.debug('SSH 免密登录已配置并验证成功。');
78
95
  } finally {
79
96
  await run(['-S', socket, '-O', 'exit', config.host], { capture: true, check: false, timeout: 5000 }).catch(() => {});
80
97
  await fs.rm(temporary, { recursive: true, force: true });
@@ -0,0 +1,92 @@
1
+ import { quote } from './system.js';
2
+
3
+ // configFile is injectable only for isolated filesystem tests, never a CLI flag.
4
+ export function sshPolicyScript({ enable = false, configFile = '/etc/ssh/sshd_config', procRoot = '/proc' } = {}) {
5
+ return `# claude-remote-ssh-policy
6
+ set -eu
7
+ sshd=$(command -v sshd || true)
8
+ if [ -z "$sshd" ] && [ -x /usr/sbin/sshd ]; then sshd=/usr/sbin/sshd; fi
9
+ printf 'uid=%s\\n' "$(id -u)"
10
+ if [ -z "$sshd" ]; then printf 'status=unavailable\\n'; exit 0; fi
11
+ set -- \${SSH_CONNECTION:-}
12
+ connection="user=$(id -un)"
13
+ if [ "$#" -eq 4 ]; then connection="$connection,addr=$1,host=$1,laddr=$3,lport=$4"; fi
14
+ policy() { "$sshd" -T -C "$connection"; }
15
+ if ! current=$(policy 2>/dev/null); then printf 'status=unavailable\\n'; exit 0; fi
16
+ ${enable ? `
17
+ if printf '%s\\n' "$current" | grep -qx 'pubkeyauthentication no'; then
18
+ printf '%s\\n' "$current" | grep -Eq '^authenticationmethods (any|([^ ]+ )*publickey)( |$)' || {
19
+ echo '服务器要求额外认证,未修改认证策略。' >&2; exit 1;
20
+ }
21
+ [ "$(id -u)" = 0 ] || { echo '自动开启公钥登录需要 root;请由管理员配置。' >&2; exit 1; }
22
+ [ "$(uname -s)" = Linux ] || { echo '自动开启仅支持 Linux systemd 的标准 SSH 服务。' >&2; exit 1; }
23
+ service=
24
+ for name in ssh.service sshd.service; do
25
+ if systemctl is-active --quiet "$name"; then service=$name; break; fi
26
+ done
27
+ [ -n "$service" ] || { echo '无法确认正在运行的 SSH 服务,未修改配置。' >&2; exit 1; }
28
+ pid=$(systemctl show "$service" --property=MainPID --value)
29
+ case "$pid" in ''|*[!0-9]*|0) echo '无法确认 SSH 服务进程,未修改配置。' >&2; exit 1;; esac
30
+ process_file=${quote(procRoot)}/"$pid"/cmdline
31
+ [ -r "$process_file" ] || { echo '无法读取 SSH 服务参数,未修改配置。' >&2; exit 1; }
32
+ process_args=$(tr '\\000' ' ' < "$process_file")
33
+ invocation=$(systemctl show "$service" --property=ExecStart --property=Environment)
34
+ case "$invocation $process_args" in
35
+ *' -f'*|*' -o'*) echo 'SSH 服务使用自定义参数,请由管理员配置;未修改配置。' >&2; exit 1;;
36
+ esac
37
+ file=${quote(configFile)}
38
+ [ -f "$file" ] && [ ! -L "$file" ] || { echo 'SSH 配置不是普通文件,未修改配置。' >&2; exit 1; }
39
+ backup=$(mktemp "$file.claude-remote.XXXXXX")
40
+ cp -p "$file" "$backup"
41
+ changed=0
42
+ candidate=
43
+ rollback() {
44
+ code=$?
45
+ trap - EXIT HUP INT TERM
46
+ if [ -n "$candidate" ]; then rm -f "$candidate"; fi
47
+ if [ "$changed" = 1 ]; then
48
+ cp -p "$backup" "$file"
49
+ if ! "$sshd" -t || ! systemctl reload "$service"; then
50
+ echo "恢复后的 SSH 配置校验或重新加载失败,请使用备份检查:$backup" >&2
51
+ fi
52
+ echo "开启失败,已恢复原配置;备份:$backup" >&2
53
+ fi
54
+ exit "$code"
55
+ }
56
+ trap rollback EXIT
57
+ trap 'exit 1' HUP INT TERM
58
+ # First global value wins, including over later Include files. Restrictive
59
+ # Match rules must pass the connection-specific check or we roll back.
60
+ candidate=$(mktemp "$file.claude-remote-stage.XXXXXX")
61
+ cp -p "$backup" "$candidate"
62
+ { printf 'PubkeyAuthentication yes\\n'; cat "$backup"; } > "$candidate"
63
+ changed=1
64
+ mv -f "$candidate" "$file"
65
+ "$sshd" -t
66
+ current=$(policy)
67
+ printf '%s\\n' "$current" | grep -qx 'pubkeyauthentication yes' || {
68
+ echo '当前账号的 Match 策略仍禁止公钥登录。' >&2; exit 1;
69
+ }
70
+ systemctl reload "$service"
71
+ changed=0
72
+ trap - EXIT HUP INT TERM
73
+ printf 'backup=%s\\n' "$backup"
74
+ fi
75
+ ` : ''}
76
+ printf 'status=ok\\n'
77
+ printf '%s\\n' "$current" | while IFS=' ' read -r key value; do
78
+ case "$key" in pubkeyauthentication|authenticationmethods|authorizedkeysfile|permitrootlogin)
79
+ printf '%s=%s\\n' "$key" "$value";;
80
+ esac
81
+ done
82
+ `;
83
+ }
84
+
85
+ export function parseSSHPolicy(output) {
86
+ const result = {};
87
+ for (const line of output.split(/\r?\n/)) {
88
+ const index = line.indexOf('=');
89
+ if (index > 0) result[line.slice(0, index)] = line.slice(index + 1);
90
+ }
91
+ return result;
92
+ }