@walkhi/code-relax 0.1.0-beta.4 → 0.1.0-beta.5

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
@@ -80,4 +80,4 @@ npm uninstall -g @walkhi/code-relax
80
80
 
81
81
  ## 发布边界
82
82
 
83
- registry 当前公开包为 `@walkhi/code-relax@0.1.0-beta.3`,主 CLI 为 `relax`,并保留 `code-relax` 兼容入口。该版将 WMI 后的第二层 PowerShell broker 改为 Node worker,并加入 `%LOCALAPPDATA%\CodexRemote\install.log` 持久安装日志;本机启动交接、npm 打包和公开 registry 已验证,仍需在原故障电脑复核。详细规则见 [npm 分发](../docs/npm-distribution.md)。
83
+ registry 当前公开包为 `@walkhi/code-relax@0.1.0-beta.5`,主 CLI 为 `relax`,并保留 `code-relax` 兼容入口。该版让 WMI 所有的 Node broker 在目标进程期间保留隐藏控制台,避免 Codex 调用工具时反复弹出控制台窗口;同时沿用官方中继自助登记和持久安装日志,通信协议仍为 7。详细规则见 [npm 分发](../docs/npm-distribution.md)。
@@ -17,7 +17,6 @@ try {
17
17
  const descriptors = (spec.logs || []).map(file => fs.openSync(file, 'a'));
18
18
  try {
19
19
  const child = spawn(spec.executable, spec.args || [], {
20
- detached: true,
21
20
  windowsHide: true,
22
21
  env: spec.environment,
23
22
  stdio: ['ignore', descriptors[0] ?? 'ignore', descriptors[1] ?? 'ignore'],
@@ -26,7 +25,8 @@ try {
26
25
  child.once('spawn', resolve);
27
26
  child.once('error', reject);
28
27
  });
29
- child.unref();
28
+ // Keep this WMI-owned broker alive with the target so Codex tools inherit
29
+ // its hidden console instead of creating a visible console per command.
30
30
  result.pid = child.pid;
31
31
  diagnostic(`target started (PID ${child.pid})`);
32
32
  } finally {
@@ -1,9 +1,9 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { execFile } from 'node:child_process';
4
- import { setTimeout as delay } from 'node:timers/promises';
5
- import { startBackground, processAlive } from '../background-process.mjs';
6
- import { promisify } from 'node:util';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { startBackground, processAlive } from '../background-process.mjs';
6
+ import { promisify } from 'node:util';
7
7
  import { randomBytes, randomUUID } from 'node:crypto';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { hostname } from 'node:os';
@@ -16,8 +16,8 @@ import { connectBridgeRelay } from './connector.mjs';
16
16
  import { validProbeIceServers } from './p2p-probe.mjs';
17
17
  import { relayAddress, validSecret } from './wire.mjs';
18
18
  import { saveLanAuthorizations } from './lan-authorizations.mjs';
19
- import { lanNetworks } from '../platform/windows/network-info.mjs';
20
-
19
+ import { lanNetworks } from '../platform/windows/network-info.mjs';
20
+
21
21
  const exec = promisify(execFile);
22
22
  const script = fileURLToPath(import.meta.url);
23
23
  const recordPath = root => path.join(root, 'self-relay-session.json');
@@ -32,34 +32,33 @@ async function loadConfig(stateRoot) {
32
32
  if (config.p2pWindowsRouteHelper !== undefined && typeof config.p2pWindowsRouteHelper !== 'boolean') throw new Error('p2pWindowsRouteHelper 必须是布尔值');
33
33
  if (config.p2pWindowsRouteHelper && !/^[A-Za-z0-9_-]{32,128}$/.test(config.p2pWindowsRouteHelperToken || '')) throw new Error('p2pWindowsRouteHelperToken 无效');
34
34
  const url = process.env.RELAY_URL || config.url;
35
- if (!url) throw new Error('请先配置 self-relay-config.json 的 url 和 hostKeyFile(或 sshHost、sshKeyPath),详见自建中继文档。');
35
+ if (!url) throw new Error('请先配置 self-relay-config.json 的 url 和 hostKeyFile(或 sshHost、sshKeyPath),详见自建中继文档。');
36
36
  relayAddress(url);
37
37
  const resolved = relayAddress(url);
38
38
  const pairingUrl = config.pairingUrl || `${resolved.startsWith('wss:') ? 'https:' : 'http:'}//${new URL(resolved).host}/pair`;
39
39
  const pairing = new URL(pairingUrl);
40
40
  if (!['https:', 'http:'].includes(pairing.protocol) || pairing.pathname !== '/pair' || pairing.search || pairing.hash || pairing.username || pairing.password ||
41
41
  (pairing.protocol === 'http:' && pairing.hostname !== '127.0.0.1')) throw new Error('self-relay-config.json 的 pairingUrl 必须是 HTTPS /pair 地址。');
42
- return { ...config, url: resolved, pairingUrl: pairing.href };
43
- }
42
+ return { ...config, url: resolved, pairingUrl: pairing.href };
43
+ }
44
44
  async function registrationKey(config) {
45
45
  if (config.url === officialConfig.url) return undefined;
46
46
  const hostKeyFile = process.env.RELAY_HOST_KEY_FILE || config.hostKeyFile;
47
- let hostKey;
48
- if (hostKeyFile) hostKey = fs.readFileSync(hostKeyFile, 'utf8').trim();
49
- else {
50
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(config.sshHost || '') ||
51
- !/^\/[a-zA-Z0-9/_.-]+$/.test(config.sshKeyPath || '')) throw new Error('中继注册凭据配置不完整或无效。');
52
- try {
53
- const result = await exec('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', config.sshHost,
54
- `sudo -n cat ${config.sshKeyPath}`], { encoding: 'utf8', timeout: 15000, windowsHide: true });
55
- hostKey = result.stdout.trim();
56
- } catch { throw new Error('无法通过已配置 SSH 读取中继注册凭据。'); }
57
- }
58
- if (!validSecret(hostKey)) throw new Error('中继注册凭据格式无效。');
59
- return hostKey;
60
- }
61
-
62
- export async function selfRelay(command, stateRoot, requiredMode = '') {
47
+ let hostKey;
48
+ if (hostKeyFile) hostKey = fs.readFileSync(hostKeyFile, 'utf8').trim();
49
+ else {
50
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(config.sshHost || '') ||
51
+ !/^\/[a-zA-Z0-9/_.-]+$/.test(config.sshKeyPath || '')) throw new Error('中继注册凭据配置不完整或无效。');
52
+ try {
53
+ const result = await exec('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', config.sshHost,
54
+ `sudo -n cat ${config.sshKeyPath}`], { encoding: 'utf8', timeout: 15000, windowsHide: true });
55
+ hostKey = result.stdout.trim();
56
+ } catch { throw new Error('无法通过已配置 SSH 读取中继注册凭据。'); }
57
+ }
58
+ if (!validSecret(hostKey)) throw new Error('中继注册凭据格式无效。');
59
+ return hostKey;
60
+ }
61
+ export async function selfRelay(command, stateRoot, requiredMode = '') {
63
62
  const previous = readRecord(recordPath(stateRoot));
64
63
  let current;
65
64
  if (previous) {
@@ -139,12 +138,12 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
139
138
  fs.rmSync(legacyPath);
140
139
  }
141
140
  let identity = identities.servers[config.url];
142
- if (!identity) {
143
- identity = { id: randomUUID(), secret: randomBytes(32).toString('base64url'), registered: false };
144
- identities.servers[config.url] = identity; await credentialStore('save', identities);
145
- }
146
- const hostKey = identity.registered ? undefined : await registrationKey(config);
147
- let bridgeError = '';
141
+ if (!identity) {
142
+ identity = { id: randomUUID(), secret: randomBytes(32).toString('base64url'), registered: false };
143
+ identities.servers[config.url] = identity; await credentialStore('save', identities);
144
+ }
145
+ const hostKey = identity.registered ? undefined : await registrationKey(config);
146
+ let bridgeError = '';
148
147
  async function refreshHealth() {
149
148
  try { health = await serviceHealth(session()) || { transportMode: health.transportMode, status: 'offline', desktopConnected: false }; bridgeError = ''; }
150
149
  catch (error) { bridgeError = error.message; health = { transportMode: health.transportMode, status: 'offline', desktopConnected: false }; }
@@ -204,7 +203,7 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
204
203
  await refreshHealth();
205
204
  const lanCandidates = await currentLanCandidates();
206
205
  connector?.events.removeAllListeners(); connector?.close();
207
- const current = connector = connectBridgeRelay({ relayUrl: config.url, hostKey: identity.registered ? undefined : hostKey, identity,
206
+ const current = connector = connectBridgeRelay({ relayUrl: config.url, hostKey: identity.registered ? undefined : hostKey, identity,
208
207
  bridgeUrl: `ws://127.0.0.1:${bridge?.port || 45831}/api/socket`, bridgeToken: bridge?.accessUrl ? new URL(bridge.accessUrl).searchParams.get('token') : 'unavailable',
209
208
  bridgeConnection: () => { const latest = session(); return { url: `ws://127.0.0.1:${latest.port}/api/socket`, token: new URL(latest.accessUrl).searchParams.get('token') }; },
210
209
  hostName: hostname(), lanCandidates, p2pProbe: config.p2pProbe === true, iceServers: config.p2pIceServers || [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.4",
3
+ "version": "0.1.0-beta.5",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "scripts": {