@walkhi/code-relax 0.1.0-beta.2 → 0.1.0-beta.3
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 +1 -1
- package/dist/src/platform/windows/background-process.mjs +1 -1
- package/dist/src/platform/windows/launch-worker.mjs +42 -0
- package/dist/src/platform/windows/start-hidden-console.ps1 +10 -5
- package/package.json +1 -1
- package/tools/postinstall.mjs +47 -2
- package/dist/src/platform/windows/launch-worker.ps1 +0 -20
package/README.md
CHANGED
|
@@ -80,4 +80,4 @@ npm uninstall -g @walkhi/code-relax
|
|
|
80
80
|
|
|
81
81
|
## 发布边界
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
registry 当前公开包为 `@walkhi/code-relax@0.1.0-beta.2`,主 CLI 为 `relax`,并保留 `code-relax` 兼容入口。待发布的 `0.1.0-beta.3` 将 WMI 后的第二层 PowerShell broker 改为 Node worker,并加入 `%LOCALAPPDATA%\CodexRemote\install.log` 持久安装日志;本机启动交接和 npm 打包已验证,发布后仍需在原故障电脑复核。详细规则见 [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
|
|
21
|
+
ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010
|
|
21
22
|
}
|
|
22
|
-
$
|
|
23
|
-
$command = '"' + $
|
|
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) {
|
|
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)
|
package/package.json
CHANGED
package/tools/postinstall.mjs
CHANGED
|
@@ -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
|
-
|
|
104
|
+
runStage(entry, stage, ['setup', '--json']);
|
|
63
105
|
stage = 'skill-install';
|
|
64
|
-
|
|
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)
|