@znmin/claude-remote-cli 0.3.2 → 0.4.1

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.
@@ -1,98 +1,98 @@
1
- import * as fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import os from 'node:os';
4
- import { spawn } from 'node:child_process';
5
- import { setTimeout as delay } from 'node:timers/promises';
6
- import { execute, exists, trackChild } from '../system.js';
7
- import { sshBase } from '../ssh.js';
8
-
9
- const ALLOCATED = /^Allocated port (\d+) for remote forward\b/;
10
-
11
- // Windows OpenSSH has no connection multiplexing (ControlMaster), so the
12
- // reverse forward runs in its own connection and the server-chosen port is
13
- // read from ssh's "Allocated port" message. Other commands then open their own
14
- // connections, which works because key login is set up before this point.
15
- export async function openDirectTunnel(config, localPort, { env = process.env, spawnProcess = spawn, timeoutMs = 120000 } = {}) {
16
- const child = trackChild(spawnProcess('ssh', [...sshBase(config), '-o', 'ExitOnForwardFailure=yes', '-o', 'LogLevel=INFO',
17
- '-N', '-T', '-R', `127.0.0.1:0:127.0.0.1:${localPort}`, config.host],
18
- { env, stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true }));
19
- let closed = false, errorText = '', spawnError, port, pending = '', announce;
20
- const allocated = new Promise(resolve => { announce = resolve; });
21
- const done = new Promise(resolve => {
22
- child.once('error', error => { spawnError = error; closed = true; resolve(); announce(); });
23
- child.once('close', () => { closed = true; resolve(); announce(); });
24
- });
25
- child.stderr.setEncoding('utf8');
26
- child.stderr.on('data', chunk => {
27
- const lines = (pending + chunk).split(/\r?\n/);
28
- pending = lines.pop();
29
- for (const line of lines) {
30
- const match = line.match(ALLOCATED);
31
- if (match && port === undefined) { port = Number(match[1]); announce(); continue; }
32
- errorText = (errorText + line + '\n').slice(-8000);
33
- process.stderr.write(line + '\n');
34
- }
35
- });
36
- const stop = async () => {
37
- if (closed) return;
38
- child.kill('SIGTERM');
39
- await Promise.race([done, delay(1000)]);
40
- if (!closed) { child.kill('SIGKILL'); await done; }
41
- };
42
- try {
43
- let timer;
44
- const expired = new Promise(resolve => { timer = setTimeout(resolve, timeoutMs); });
45
- await Promise.race([allocated, expired]);
46
- clearTimeout(timer);
47
- if (port === undefined) {
48
- if (closed) throw new Error('SSH 工具连接失败:' + (spawnError?.message || errorText.trim() || '连接已关闭'));
49
- throw new Error('无法获取 SSH 反向隧道端口。请检查服务器是否允许 TCP 转发。');
50
- }
51
- if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('无法获取 SSH 反向隧道端口。请检查服务器是否允许 TCP 转发。');
52
- return { controlPath: undefined, port, close: stop };
53
- } catch (error) { await stop(); throw error; }
54
- }
55
-
56
- export async function openTunnel(config, localPort, { env = process.env, run = execute, platform = process.platform } = {}) {
57
- if (platform === 'win32') return openDirectTunnel(config, localPort, { env });
58
- // Keep the control socket below the short Unix-domain path limit on macOS.
59
- const base = os.tmpdir().length > 55 ? '/tmp' : os.tmpdir();
60
- const directory = await fs.mkdtemp(path.join(base, 'ccr-'));
61
- const controlPath = path.join(directory, 'ssh');
62
- const master = trackChild(spawn('ssh', [...sshBase(config), '-M', '-S', controlPath, '-o', 'ControlPersist=no',
63
- '-o', 'ExitOnForwardFailure=yes', '-N', '-T', config.host], { env, stdio: ['ignore', 'ignore', 'pipe'] }));
64
- let closed = false, errorText = '', spawnError;
65
- const done = new Promise(resolve => {
66
- master.once('error', error => { spawnError = error; closed = true; resolve(); });
67
- master.once('close', () => { closed = true; resolve(); });
68
- });
69
- master.stderr.setEncoding('utf8');
70
- master.stderr.on('data', chunk => { errorText = (errorText + chunk).slice(-8000); process.stderr.write(chunk); });
71
- const stop = async () => {
72
- if (!closed) {
73
- await run('ssh', [...sshBase(config), '-S', controlPath, '-O', 'exit', config.host],
74
- { env, capture: true, timeout: 5000, check: false }).catch(() => {});
75
- master.kill('SIGTERM');
76
- await Promise.race([done, delay(1000)]);
77
- if (!closed) { master.kill('SIGKILL'); await done; }
78
- }
79
- await fs.rm(directory, { recursive: true, force: true });
80
- };
81
- const interrupt = () => master.kill('SIGINT');
82
- process.on('SIGINT', interrupt);
83
- try {
84
- const deadline = Date.now() + 120000;
85
- while (!await exists(controlPath)) {
86
- if (closed) throw new Error('SSH 工具连接失败:' + (spawnError?.message || errorText.trim() || '连接已关闭'));
87
- if (Date.now() > deadline) throw new Error('等待 SSH 登录超时。');
88
- await delay(100);
89
- }
90
- const result = await run('ssh', [...sshBase(config), '-S', controlPath, '-O', 'forward',
91
- '-o', 'ExitOnForwardFailure=yes', '-R', `127.0.0.1:0:127.0.0.1:${localPort}`, config.host],
92
- { env, capture: true, timeout: 15000 });
93
- const port = Number(result.stdout.trim());
94
- if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('无法获取 SSH 反向隧道端口。请检查服务器是否允许 TCP 转发。');
95
- return { controlPath, port, close: stop };
96
- } catch (error) { await stop(); throw error; }
97
- finally { process.off('SIGINT', interrupt); }
98
- }
1
+ import * as fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { spawn } from 'node:child_process';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { execute, exists, trackChild } from '../system.js';
7
+ import { sshBase } from '../ssh.js';
8
+
9
+ const ALLOCATED = /^Allocated port (\d+) for remote forward\b/;
10
+
11
+ // Windows OpenSSH has no connection multiplexing (ControlMaster), so the
12
+ // reverse forward runs in its own connection and the server-chosen port is
13
+ // read from ssh's "Allocated port" message. Other commands then open their own
14
+ // connections, which works because key login is set up before this point.
15
+ export async function openDirectTunnel(config, localPort, { env = process.env, spawnProcess = spawn, timeoutMs = 120000 } = {}) {
16
+ const child = trackChild(spawnProcess('ssh', [...sshBase(config), '-o', 'ExitOnForwardFailure=yes', '-o', 'LogLevel=INFO',
17
+ '-N', '-T', '-R', `127.0.0.1:0:127.0.0.1:${localPort}`, config.host],
18
+ { env, stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true }));
19
+ let closed = false, errorText = '', spawnError, port, pending = '', announce;
20
+ const allocated = new Promise(resolve => { announce = resolve; });
21
+ const done = new Promise(resolve => {
22
+ child.once('error', error => { spawnError = error; closed = true; resolve(); announce(); });
23
+ child.once('close', () => { closed = true; resolve(); announce(); });
24
+ });
25
+ child.stderr.setEncoding('utf8');
26
+ child.stderr.on('data', chunk => {
27
+ const lines = (pending + chunk).split(/\r?\n/);
28
+ pending = lines.pop();
29
+ for (const line of lines) {
30
+ const match = line.match(ALLOCATED);
31
+ if (match && port === undefined) { port = Number(match[1]); announce(); continue; }
32
+ errorText = (errorText + line + '\n').slice(-8000);
33
+ process.stderr.write(line + '\n');
34
+ }
35
+ });
36
+ const stop = async () => {
37
+ if (closed) return;
38
+ child.kill('SIGTERM');
39
+ await Promise.race([done, delay(1000)]);
40
+ if (!closed) { child.kill('SIGKILL'); await done; }
41
+ };
42
+ try {
43
+ let timer;
44
+ const expired = new Promise(resolve => { timer = setTimeout(resolve, timeoutMs); });
45
+ await Promise.race([allocated, expired]);
46
+ clearTimeout(timer);
47
+ if (port === undefined) {
48
+ if (closed) throw new Error('SSH 工具连接失败:' + (spawnError?.message || errorText.trim() || '连接已关闭'));
49
+ throw new Error('无法获取 SSH 反向隧道端口。请检查服务器是否允许 TCP 转发。');
50
+ }
51
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('无法获取 SSH 反向隧道端口。请检查服务器是否允许 TCP 转发。');
52
+ return { controlPath: undefined, port, close: stop };
53
+ } catch (error) { await stop(); throw error; }
54
+ }
55
+
56
+ export async function openTunnel(config, localPort, { env = process.env, run = execute, platform = process.platform } = {}) {
57
+ if (platform === 'win32') return openDirectTunnel(config, localPort, { env });
58
+ // Keep the control socket below the short Unix-domain path limit on macOS.
59
+ const base = os.tmpdir().length > 55 ? '/tmp' : os.tmpdir();
60
+ const directory = await fs.mkdtemp(path.join(base, 'ccr-'));
61
+ const controlPath = path.join(directory, 'ssh');
62
+ const master = trackChild(spawn('ssh', [...sshBase(config), '-M', '-S', controlPath, '-o', 'ControlPersist=no',
63
+ '-o', 'ExitOnForwardFailure=yes', '-N', '-T', config.host], { env, stdio: ['ignore', 'ignore', 'pipe'] }));
64
+ let closed = false, errorText = '', spawnError;
65
+ const done = new Promise(resolve => {
66
+ master.once('error', error => { spawnError = error; closed = true; resolve(); });
67
+ master.once('close', () => { closed = true; resolve(); });
68
+ });
69
+ master.stderr.setEncoding('utf8');
70
+ master.stderr.on('data', chunk => { errorText = (errorText + chunk).slice(-8000); process.stderr.write(chunk); });
71
+ const stop = async () => {
72
+ if (!closed) {
73
+ await run('ssh', [...sshBase(config), '-S', controlPath, '-O', 'exit', config.host],
74
+ { env, capture: true, timeout: 5000, check: false }).catch(() => {});
75
+ master.kill('SIGTERM');
76
+ await Promise.race([done, delay(1000)]);
77
+ if (!closed) { master.kill('SIGKILL'); await done; }
78
+ }
79
+ await fs.rm(directory, { recursive: true, force: true });
80
+ };
81
+ const interrupt = () => master.kill('SIGINT');
82
+ process.on('SIGINT', interrupt);
83
+ try {
84
+ const deadline = Date.now() + 120000;
85
+ while (!await exists(controlPath)) {
86
+ if (closed) throw new Error('SSH 工具连接失败:' + (spawnError?.message || errorText.trim() || '连接已关闭'));
87
+ if (Date.now() > deadline) throw new Error('等待 SSH 登录超时。');
88
+ await delay(100);
89
+ }
90
+ const result = await run('ssh', [...sshBase(config), '-S', controlPath, '-O', 'forward',
91
+ '-o', 'ExitOnForwardFailure=yes', '-R', `127.0.0.1:0:127.0.0.1:${localPort}`, config.host],
92
+ { env, capture: true, timeout: 15000 });
93
+ const port = Number(result.stdout.trim());
94
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('无法获取 SSH 反向隧道端口。请检查服务器是否允许 TCP 转发。');
95
+ return { controlPath, port, close: stop };
96
+ } catch (error) { await stop(); throw error; }
97
+ finally { process.off('SIGINT', interrupt); }
98
+ }