@walkhi/code-relax 0.1.0-beta.2 → 0.1.0-beta.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
@@ -80,4 +80,4 @@ npm uninstall -g @walkhi/code-relax
80
80
 
81
81
  ## 发布边界
82
82
 
83
- 当前包名为 `@walkhi/code-relax@0.1.0-beta.2`,主 CLI 为 `relax`,并保留 `code-relax` 兼容入口。当前已完成 npm scope 发布和本机 registry 在线安装;本版修复了另一台电脑首次安装时的 Windows broker 交接超时,仍需在原电脑复核。详细规则见 [npm 分发](../docs/npm-distribution.md)。
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)。
@@ -25,6 +25,6 @@ export function startWindowsProcess(executable, args, environment, logs = []) {
25
25
  if (!Number.isSafeInteger(pid) || pid <= 0) return reject(new Error('后台启动未返回有效 PID'));
26
26
  resolve(pid);
27
27
  });
28
- helper.stdin.end(JSON.stringify({ executable, args, logs, environment }));
28
+ helper.stdin.end(JSON.stringify({ brokerExecutable: process.execPath, executable, args, logs, environment }));
29
29
  });
30
30
  }
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+
4
+ const [requestPath, readyPath, instance, diagnosticPath] = process.argv.slice(2);
5
+ let result = { instance, brokerPid: process.pid };
6
+
7
+ function diagnostic(message) {
8
+ if (!diagnosticPath) return;
9
+ fs.appendFileSync(diagnosticPath, `${new Date().toISOString()} ${message}\n`, 'utf8');
10
+ }
11
+
12
+ try {
13
+ const spec = JSON.parse(fs.readFileSync(requestPath, 'utf8').replace(/^\uFEFF/u, ''));
14
+ diagnostic('request loaded');
15
+ if (spec.instance !== instance) throw new Error('Windows process broker request identity mismatch.');
16
+ fs.rmSync(requestPath, { force: true });
17
+ const descriptors = (spec.logs || []).map(file => fs.openSync(file, 'a'));
18
+ try {
19
+ const child = spawn(spec.executable, spec.args || [], {
20
+ detached: true,
21
+ windowsHide: true,
22
+ env: spec.environment,
23
+ stdio: ['ignore', descriptors[0] ?? 'ignore', descriptors[1] ?? 'ignore'],
24
+ });
25
+ await new Promise((resolve, reject) => {
26
+ child.once('spawn', resolve);
27
+ child.once('error', reject);
28
+ });
29
+ child.unref();
30
+ result.pid = child.pid;
31
+ diagnostic(`target started (PID ${child.pid})`);
32
+ } finally {
33
+ descriptors.forEach(descriptor => fs.closeSync(descriptor));
34
+ }
35
+ } catch (error) {
36
+ result.error = error?.stack || error?.message || String(error);
37
+ try { diagnostic(`failed: ${result.error}`); } catch {}
38
+ }
39
+
40
+ const temporary = `${readyPath}.tmp`;
41
+ fs.writeFileSync(temporary, JSON.stringify(result), 'utf8');
42
+ fs.renameSync(temporary, readyPath);
@@ -11,29 +11,34 @@ try {
11
11
  $launchRoot = [IsolatedProcess]::ResolvePath($launchRoot)
12
12
  $ready = Join-Path $launchRoot ($instance + '.json')
13
13
  $request = Join-Path $launchRoot ($instance + '.request.json')
14
+ $diagnostic = Join-Path $launchRoot ($instance + '.broker.log')
14
15
  $spec | Add-Member -NotePropertyName instance -NotePropertyValue $instance
16
+ $spec | Add-Member -NotePropertyName diagnostic -NotePropertyValue $diagnostic
15
17
  $requestTemporary = $request + '.tmp'
16
18
  [IO.File]::WriteAllText($requestTemporary, ($spec | ConvertTo-Json -Compress -Depth 10), (New-Object Text.UTF8Encoding($false)))
17
19
  [IO.File]::Move($requestTemporary, $request)
18
- $environment = @(Get-ChildItem Env: | ForEach-Object { $_.Name + '=' + $_.Value })
19
20
  $startup = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property @{
20
- ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010; EnvironmentVariables=[string[]]$environment
21
+ ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010
21
22
  }
22
- $powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
23
- $command = '"' + $powershell + '" -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + (Join-Path $PSScriptRoot 'launch-worker.ps1') + '" -RequestPath "' + $request + '" -ReadyPath "' + $ready + '" -Instance "' + $instance + '"'
23
+ $worker = Join-Path $PSScriptRoot 'launch-worker.mjs'
24
+ $command = '"' + [string]$spec.brokerExecutable + '" "' + $worker + '" "' + $request + '" "' + $ready + '" "' + $instance + '" "' + $diagnostic + '"'
24
25
  $broker = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
25
26
  CommandLine=$command; CurrentDirectory=$env:USERPROFILE; ProcessStartupInformation=$startup
26
27
  }
27
28
  if ($broker.ReturnValue -ne 0) { throw ('Windows process broker failed: ' + $broker.ReturnValue) }
28
29
  $deadline = [DateTime]::UtcNow.AddSeconds(30)
29
30
  while (-not (Test-Path -LiteralPath $ready)) {
30
- if ([DateTime]::UtcNow -gt $deadline) { throw ('Windows process broker handoff timed out (broker PID ' + $broker.ProcessId + ').') }
31
+ if ([DateTime]::UtcNow -gt $deadline) {
32
+ $detail = if (Test-Path -LiteralPath $diagnostic) { (Get-Content -LiteralPath $diagnostic -Raw -Encoding UTF8).Trim() } else { 'broker did not create a diagnostic log' }
33
+ throw ('Windows process broker handoff timed out (broker PID ' + $broker.ProcessId + '): ' + $detail)
34
+ }
31
35
  Start-Sleep -Milliseconds 50
32
36
  }
33
37
  $result = Get-Content -LiteralPath $ready -Raw -Encoding UTF8 | ConvertFrom-Json
34
38
  Remove-Item -LiteralPath $ready
35
39
  if ($result.instance -ne $instance -or $result.brokerPid -ne $broker.ProcessId) { throw 'Windows process broker identity mismatch.' }
36
40
  if ($result.error) { throw [string]$result.error }
41
+ if (Test-Path -LiteralPath $diagnostic) { Remove-Item -LiteralPath $diagnostic -Force }
37
42
  [Console]::WriteLine([int]$result.pid)
38
43
  } catch {
39
44
  [Console]::Error.WriteLine($_.Exception.Message)
@@ -18,12 +18,16 @@ import { relayAddress, validSecret } from './wire.mjs';
18
18
  import { saveLanAuthorizations } from './lan-authorizations.mjs';
19
19
  import { lanNetworks } from '../platform/windows/network-info.mjs';
20
20
 
21
- const exec = promisify(execFile);
22
- const script = fileURLToPath(import.meta.url);
23
- const recordPath = root => path.join(root, 'self-relay-session.json');
24
-
25
- async function loadConfig(stateRoot) {
26
- const config = readRecord(path.join(stateRoot, 'self-relay-config.json')) || {};
21
+ const exec = promisify(execFile);
22
+ const script = fileURLToPath(import.meta.url);
23
+ const recordPath = root => path.join(root, 'self-relay-session.json');
24
+ const officialConfig = { url: 'wss://api.uulife.site/relay' };
25
+
26
+ async function loadConfig(stateRoot) {
27
+ const configPath = path.join(stateRoot, 'self-relay-config.json');
28
+ const stored = readRecord(configPath) || {};
29
+ const config = { ...officialConfig, ...stored };
30
+ if (!stored.url) writeRecord(configPath, config);
27
31
  if (config.p2pIceServers !== undefined && !validProbeIceServers(config.p2pIceServers)) throw new Error('p2pIceServers 只接受最多 4 个 stun:主机:端口地址');
28
32
  if (config.p2pWindowsRouteHelper !== undefined && typeof config.p2pWindowsRouteHelper !== 'boolean') throw new Error('p2pWindowsRouteHelper 必须是布尔值');
29
33
  if (config.p2pWindowsRouteHelper && !/^[A-Za-z0-9_-]{32,128}$/.test(config.p2pWindowsRouteHelperToken || '')) throw new Error('p2pWindowsRouteHelperToken 无效');
@@ -37,8 +41,9 @@ async function loadConfig(stateRoot) {
37
41
  (pairing.protocol === 'http:' && pairing.hostname !== '127.0.0.1')) throw new Error('self-relay-config.json 的 pairingUrl 必须是 HTTPS /pair 地址。');
38
42
  return { ...config, url: resolved, pairingUrl: pairing.href };
39
43
  }
40
- async function registrationKey(config) {
41
- const hostKeyFile = process.env.RELAY_HOST_KEY_FILE || config.hostKeyFile;
44
+ async function registrationKey(config) {
45
+ if (config.url === officialConfig.url) return undefined;
46
+ const hostKeyFile = process.env.RELAY_HOST_KEY_FILE || config.hostKeyFile;
42
47
  let hostKey;
43
48
  if (hostKeyFile) hostKey = fs.readFileSync(hostKeyFile, 'utf8').trim();
44
49
  else {
@@ -198,11 +198,13 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
198
198
  hostName = normalizeHostName(message.hostName);
199
199
  }
200
200
  catch { return fail(1008, 'Invalid LAN candidates'); }
201
- room = durable ? rooms.get(message.deviceId) : undefined;
202
- if (room && (room.status !== 'active' || !matches(message.deviceSecret, room.hostHash))) return fail();
203
- if (!room) {
204
- if (!matches(message.key, hostHash)) return fail();
205
- if (rooms.size >= 256) return fail(4429, 'Device limit reached');
201
+ room = durable ? rooms.get(message.deviceId) : undefined;
202
+ if (room && (room.status !== 'active' || !matches(message.deviceSecret, room.hostHash))) return fail();
203
+ if (!room) {
204
+ // Durable computers prove future ownership with their generated device secret.
205
+ // The legacy deployment key remains accepted, but is no longer required for first registration.
206
+ if (!durable && !matches(message.key, hostHash)) return fail();
207
+ if (rooms.size >= 256) return fail(4429, 'Device limit reached');
206
208
  const now = new Date().toISOString();
207
209
  room = { id: durable ? message.deviceId : randomUUID(), durable,
208
210
  revision: 0, status: 'active', clients: [], connections: new Map(), observers: new Map(), createdAt: now, updatedAt: now,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.4",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -6,6 +6,47 @@ import { fileURLToPath } from 'node:url';
6
6
 
7
7
  const serviceRoot = path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
8
8
  'Programs', 'CodexRemote', 'service');
9
+ const stateRoot = path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'CodexRemote');
10
+ const installLog = path.join(stateRoot, 'install.log');
11
+
12
+ function sanitizeLog(value) {
13
+ let result = String(value).replace(/npm_[A-Za-z0-9]+/gu, '[redacted npm token]')
14
+ .replace(/([?&]token=)[^&\s"']+/giu, '$1[redacted]');
15
+ const paths = [['LOCALAPPDATA', process.env.LOCALAPPDATA], ['APPDATA', process.env.APPDATA],
16
+ ['USERPROFILE', process.env.USERPROFILE], ['TEMP', process.env.TEMP], ['TMP', process.env.TMP]]
17
+ .filter(([, item]) => item).sort((left, right) => right[1].length - left[1].length);
18
+ for (const [name, item] of paths) {
19
+ const pattern = item.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
20
+ result = result.replace(new RegExp(pattern, 'giu'), `%${name}%`);
21
+ }
22
+ return result;
23
+ }
24
+
25
+ function appendLog(message) {
26
+ fs.mkdirSync(stateRoot, { recursive: true });
27
+ fs.appendFileSync(installLog, `${new Date().toISOString()} ${sanitizeLog(message)}\n`, 'utf8');
28
+ }
29
+
30
+ function systemSummary() {
31
+ const npm = String(process.env.npm_config_user_agent || 'unknown').replace(/[\r\n]/gu, ' ');
32
+ return `platform=${process.platform} osRelease=${os.release()} osVersion=${os.version()} `
33
+ + `osArch=${os.arch()} processArch=${process.arch} node=${process.version} npmUserAgent=${npm}`;
34
+ }
35
+
36
+ function runStage(entry, stage, args) {
37
+ appendLog(`[${stage}] start`);
38
+ try {
39
+ const output = execFileSync(process.execPath, [entry, ...args], { encoding: 'utf8', windowsHide: true,
40
+ stdio: ['inherit', 'pipe', 'pipe'] });
41
+ if (output) { process.stdout.write(output); appendLog(`[${stage}] stdout\n${output.trimEnd()}`); }
42
+ appendLog(`[${stage}] success`);
43
+ } catch (error) {
44
+ if (error?.stdout) { process.stdout.write(error.stdout); appendLog(`[${stage}] stdout\n${String(error.stdout).trimEnd()}`); }
45
+ if (error?.stderr) { process.stderr.write(error.stderr); appendLog(`[${stage}] stderr\n${String(error.stderr).trimEnd()}`); }
46
+ appendLog(`[${stage}] failed: ${error?.stack || error?.message || String(error)}`);
47
+ throw error;
48
+ }
49
+ }
9
50
 
10
51
  function installationSnapshot() {
11
52
  const recordPath = path.join(serviceRoot, 'installation.json');
@@ -58,16 +99,20 @@ if (process.env.npm_config_global === 'true') {
58
99
  const entry = fileURLToPath(new URL('../dist/bin/codex-remote.mjs', import.meta.url));
59
100
  const previous = installationSnapshot();
60
101
  let stage = 'setup';
102
+ appendLog(`[postinstall] start package=${process.env.npm_package_version || 'unknown'} ${systemSummary()}`);
61
103
  try {
62
- execFileSync(process.execPath, [entry, 'setup', '--json'], { stdio: 'inherit', windowsHide: true });
104
+ runStage(entry, stage, ['setup', '--json']);
63
105
  stage = 'skill-install';
64
- execFileSync(process.execPath, [entry, 'skill-install', '--json'], { stdio: 'inherit', windowsHide: true });
106
+ runStage(entry, stage, ['skill-install', '--json']);
107
+ appendLog('[postinstall] success');
65
108
  } catch (error) {
66
109
  const current = installationSnapshot();
67
110
  console.error(`\nCode Relax 安装未完全完成:\n${stage === 'setup'
68
111
  ? setupFailureGuidance(previous, current)
69
112
  : skillFailureGuidance(previous, current)}`);
70
113
  if (error?.code && error.code !== 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') console.error(`启动安装步骤失败:${error.message}`);
114
+ console.error(`安装日志:${installLog}`);
115
+ appendLog(`[postinstall] failed stage=${stage}`);
71
116
  process.exitCode = Number.isInteger(error?.status) && error.status > 0 ? error.status : 1;
72
117
  }
73
118
  }
@@ -1,20 +0,0 @@
1
- param(
2
- [Parameter(Mandatory=$true)][string]$RequestPath,
3
- [Parameter(Mandatory=$true)][string]$ReadyPath,
4
- [Parameter(Mandatory=$true)][string]$Instance
5
- )
6
- $ErrorActionPreference = 'Stop'
7
- $result = @{instance=$Instance; brokerPid=$PID}
8
- try {
9
- $spec = Get-Content -LiteralPath $RequestPath -Raw -Encoding UTF8 | ConvertFrom-Json
10
- if ($spec.instance -ne $Instance) { throw 'Windows process broker request identity mismatch.' }
11
- Remove-Item -LiteralPath $RequestPath -Force
12
- Add-Type -Path (Join-Path $PSScriptRoot 'IsolatedProcess.cs')
13
- $stdout = if ($spec.logs.Count -gt 0) { [string]$spec.logs[0] } else { 'NUL' }
14
- $stderr = if ($spec.logs.Count -gt 1) { [string]$spec.logs[1] } else { 'NUL' }
15
- $environment = @($spec.environment.PSObject.Properties | ForEach-Object { $_.Name + '=' + [string]$_.Value })
16
- $result.pid = [IsolatedProcess]::Start([string]$spec.executable, [string[]]$spec.args, $stdout, $stderr, [string[]]$environment)
17
- } catch { $result.error = $_.Exception.Message }
18
- $temporary = $ReadyPath + '.tmp'
19
- [IO.File]::WriteAllText($temporary, ($result | ConvertTo-Json -Compress), (New-Object Text.UTF8Encoding($false)))
20
- [IO.File]::Move($temporary, $ReadyPath)