@walkhi/code-relax 0.1.0-beta.35 → 0.1.0-beta.36

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.
@@ -265,7 +265,8 @@ async function main() {
265
265
  if (previousInstallation?.nodeExecutable) await removeDesktopShortcut({ serviceRoot, nodeExecutable: previousInstallation.nodeExecutable });
266
266
  const desktopBeforeSetup = desktopHost.desktopIntegration === false ? { running: false, desktop: null } : await desktopHost.inspect();
267
267
  const runtime = await resolveCodexRuntime(physicalStateRoot, { refresh: true });
268
- const desktopNeedsAttach = desktopBeforeSetup.running && !await host.sharedServiceUrl();
268
+ const preserveCodex = options.includes('--preserve-codex');
269
+ const desktopNeedsRestart = desktopBeforeSetup.running && !preserveCodex;
269
270
  const previous = hadInstalledService ? await service.status() : { processRunning: false };
270
271
  const legacyRelaySession = readRecord(path.join(stateRoot, 'self-relay-session.json'));
271
272
  const previousRelay = legacyRelaySession ? await controlRequest(legacyRelaySession) : await selfRelay('relay-status', stateRoot);
@@ -286,7 +287,7 @@ async function main() {
286
287
  let stoppedForUpdate = false;
287
288
  const installed = await installService(packageRoot(path.dirname(fileURLToPath(import.meta.url))), serviceRoot, executable, physicalStateRoot, {
288
289
  beforeActivate: async () => {
289
- if (desktopNeedsAttach) await desktopHost.closeDesktop(desktopBeforeSetup.desktop);
290
+ if (desktopNeedsRestart) await desktopHost.closeDesktop(desktopBeforeSetup.desktop);
290
291
  stoppedForUpdate = resumeAfterUpdate;
291
292
  if (previousRelay.running && legacyRelaySession) {
292
293
  await controlRequest(legacyRelaySession, 'POST');
@@ -315,46 +316,45 @@ async function main() {
315
316
  ? await configureManagedEnvironment(physicalStateRoot) : { configured: false, reason: desktopHost.desktopIntegration === false ? 'desktop-integration-unsupported' : 'desktop-not-installed' };
316
317
  let managedAppServerMigrated = false;
317
318
  const managedAppServerMigration = {
318
- required: bridge.managedAppServer.launcherRestartRequired === true,
319
+ required: bridge.managedAppServer.launcherRestartRequired === true || bridge.managedAppServer.updatePending === true,
320
+ reason: bridge.managedAppServer.updatePending ? 'runtime-update' : bridge.managedAppServer.launcherRestartRequired ? 'launcher-migration' : null,
319
321
  restarted: false,
320
322
  previousPid: bridge.managedAppServer.pid || null,
323
+ previousVersion: bridge.managedAppServer.version || null,
321
324
  };
322
325
  let desktopRestarted = false;
323
- let desktopRestartReason = null;
324
- if (managedAppServerMigration.required) {
325
- const desktopInfo = desktopHost.desktopIntegration === false ? { running: false } : await desktopHost.inspect();
326
- const desktopAttached = desktopInfo.running
327
- && (bridge.desktop?.attachedToManagedServer === true || Boolean(await host.sharedServiceUrl()));
328
- if (desktopAttached) await desktopHost.closeDesktop(desktopInfo.desktop);
329
- await service.restartManagedAppServer();
330
- const migrationDeadline = Date.now() + 40_000;
326
+ if (managedAppServerMigration.required && !preserveCodex) {
327
+ const requested = await service.restartManagedAppServer(true);
328
+ const operationId = requested.restart?.operationId;
329
+ if (!operationId) throw new Error('安装更新未取得 Codex 内核重启操作编号。');
330
+ const complete = () => bridge.running && bridge.managedAppServer?.state === 'ready'
331
+ && bridge.managedAppServer.restart?.operationId === operationId
332
+ && bridge.managedAppServer.restart.status === 'completed'
333
+ && !bridge.managedAppServer.launcherRestartRequired && !bridge.managedAppServer.updatePending;
334
+ const migrationDeadline = Date.now() + 60_000;
331
335
  do {
332
336
  await delay(250);
333
337
  bridge = await service.status();
334
- if (bridge.managedAppServer?.restart?.status === 'failed') {
335
- throw new Error(bridge.managedAppServer.restart.error || 'Codex 内核启动链迁移失败。');
338
+ if (bridge.managedAppServer?.restart?.operationId === operationId && bridge.managedAppServer.restart.status === 'failed') {
339
+ throw new Error(bridge.managedAppServer.restart.error || '安装期间 Codex 内核更新失败。');
336
340
  }
337
- } while (Date.now() < migrationDeadline && (!bridge.running || bridge.managedAppServer?.state !== 'ready' || bridge.managedAppServer.launcherRestartRequired));
338
- if (!bridge.running || bridge.managedAppServer?.state !== 'ready' || bridge.managedAppServer.launcherRestartRequired) {
339
- throw new Error('Codex 内核未在 40 秒内完成启动链迁移。');
340
- }
341
+ } while (Date.now() < migrationDeadline && !complete());
342
+ if (!complete()) throw new Error('安装期间 Codex 内核未在 60 秒内完成更新;未确认新内核就绪。');
341
343
  managedAppServerMigrated = true;
342
344
  managedAppServerMigration.restarted = true;
343
- if (desktopAttached) {
344
- await launchDesktop({ stateRoot: physicalStateRoot, host: desktopHost });
345
- desktopRestarted = true;
346
- desktopRestartReason = 'app-server-migration';
347
- bridge = await service.status();
348
- }
349
345
  }
350
346
  const skill = installSkill(path.join(serviceRoot, 'dist/skills/code-relax/SKILL.md'));
351
- if (desktopNeedsAttach && !desktopRestarted) {
347
+ if (desktopNeedsRestart) {
352
348
  const currentDesktop = await desktopHost.inspect();
353
349
  if (currentDesktop.running) await desktopHost.closeDesktop(currentDesktop.desktop);
354
350
  await launchDesktop({ stateRoot: physicalStateRoot, host: desktopHost });
351
+ if (!await desktopHost.desktopReady()) throw new Error('安装期间 Codex 窗口已启动,但未确认连接到工作站内核。');
355
352
  desktopRestarted = true;
356
- desktopRestartReason = 'attach-managed-server';
357
- bridge = await service.status();
353
+ }
354
+ bridge = await service.status();
355
+ if (!bridge.running || bridge.managedAppServer?.state !== 'ready' || !bridge.execution?.ready
356
+ || (!preserveCodex && (bridge.managedAppServer.updatePending || bridge.managedAppServer.launcherRestartRequired))) {
357
+ throw new Error('安装文件已更新,但 Codex 内核更新或完整控制尚未就绪,安装未完成。');
358
358
  }
359
359
  installed.autostart = autostart;
360
360
  installed.skill = skill;
@@ -366,8 +366,11 @@ async function main() {
366
366
  installed.managedAppServerMigration = { ...managedAppServerMigration,
367
367
  currentPid: bridge.managedAppServer.pid || null,
368
368
  launcherRevision: bridge.managedAppServer.launcherRevision ?? null };
369
+ installed.preserveCodex = preserveCodex;
369
370
  installed.desktopRestarted = desktopRestarted;
370
- installed.desktopRestartReason = desktopRestartReason;
371
+ installed.desktopRestartReason = desktopRestarted ? 'setup-reconnect' : null;
372
+ installed.desktopConnected = desktopRestarted ? true : null;
373
+ installed.executionReady = bridge.execution.ready;
371
374
  if (options.includes('--desktop-helper')) installed.desktopHelper = await installDesktopHelper();
372
375
  const migrationText = managedAppServerMigrated ? 'Codex 内核已更新并重启;请检查安装前正在进行的任务。\n' : '';
373
376
  const displayText = `Code Relax ${installed.version} 安装成功。\n${migrationText}${desktopSetupSteps.join('\n')}`;
@@ -1 +1 @@
1
- {"layout":"npm-platform-packages","version":"0.1.0-beta.35"}
1
+ {"layout":"npm-platform-packages","version":"0.1.0-beta.36"}
@@ -39,6 +39,7 @@ import { desktopDotsProfiles } from './platform/windows/dots-profile.mjs';
39
39
  import { readPlanPricing } from './plan-pricing.mjs';
40
40
  import { requestRecovery, recoveryStatus } from './shared-recovery.mjs';
41
41
  import { requestServiceRestart } from './service-restart.mjs';
42
+ import { checkServiceUpdate, requestServiceUpdate, serviceUpdateStatus } from './service-update.mjs';
42
43
  import { TurnModelHistory } from './turn-model-history.mjs';
43
44
  import { browseCode } from './code-browser.mjs';
44
45
  import { fileRange, fileRevision, FileRangeError } from './file-transfer-range.mjs';
@@ -265,6 +266,12 @@ async function handleRequest(request, response) {
265
266
  if (body.confirmed !== true || typeof body.operationId !== 'string') throw new RequestError(400, '请确认重新打开当前 Codex 并接入 Codex 内核后再继续。');
266
267
  sendJson(response, 202, await requestRecovery(stateRoot, body)); return;
267
268
  }
269
+ if (url.pathname === '/api/service/update' && request.method === 'GET') {
270
+ sendJson(response, 200, await checkServiceUpdate(stateRoot)); return;
271
+ }
272
+ if (url.pathname === '/api/service/update' && request.method === 'POST') {
273
+ sendJson(response, 202, await requestServiceUpdate(stateRoot, await readJsonBody(request))); return;
274
+ }
268
275
  if (request.method === 'POST' && url.pathname === '/api/service/restart') {
269
276
  const body = await readJsonBody(request);
270
277
  if (body.confirmed !== true) throw new RequestError(400, '请确认重启工作站服务后再继续。');
@@ -309,6 +316,7 @@ async function handleRequest(request, response) {
309
316
  sendJson(response, 200, {
310
317
  status: 'ok', resident: true, version: runtimeVersion,
311
318
  keepAwake: powerManager.snapshot(),
319
+ serviceUpdate: serviceUpdateStatus(stateRoot),
312
320
  execution: { ...executionHealth.state, ready: executionReady, canWrite: executionReady,
313
321
  mode: executionMode },
314
322
  managedAppServer: managedAppServer.snapshot(),
@@ -35,7 +35,13 @@ export async function installService(source, target, executable, stateRoot, hook
35
35
  }
36
36
  await exec(executable, [path.join(stage, 'dist/bin/codex-remote.mjs'), '--help'], { windowsHide: true });
37
37
  await exec(executable, ['-e', 'const r=require("node:module").createRequire(process.argv[1]); r("sharp"); r("qrcode"); r("ws"); r("node-datachannel");', path.join(stage, 'package.json')], { windowsHide: true });
38
- writeRecord(path.join(stage, 'installation.json'), { installedAt: new Date().toISOString(), source, nodeExecutable: executable, stateRoot,
38
+ const previousInstallation = readRecord(path.join(target, 'installation.json'));
39
+ const npmCliPath = process.env.npm_execpath || previousInstallation?.npmCliPath;
40
+ const npmPrefix = process.env.npm_config_prefix || previousInstallation?.npmPrefix;
41
+ writeRecord(path.join(stage, 'installation.json'), {
42
+ npmCliPath: npmCliPath && path.isAbsolute(npmCliPath) && fs.existsSync(npmCliPath) ? fs.realpathSync(npmCliPath) : null,
43
+ npmPrefix: npmPrefix && path.isAbsolute(npmPrefix) ? npmPrefix : null,
44
+ installedAt: new Date().toISOString(), source, nodeExecutable: executable, stateRoot,
39
45
  version: readRecord(path.join(source, 'package.json')).version });
40
46
  const previous = fs.existsSync(target);
41
47
  let previousMoved = false, activated = false;
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+
6
+ // Copied outside both npm and service directories before launch. Builtins only.
7
+ const jobFile = process.argv[2];
8
+ const job = JSON.parse(fs.readFileSync(jobFile, 'utf8'));
9
+ process.chdir(path.dirname(jobFile));
10
+ const read = () => JSON.parse(fs.readFileSync(job.statusFile, 'utf8'));
11
+ function update(values) {
12
+ const current = read();
13
+ if (current.operationId !== job.operationId) throw new Error('更新操作已被替换');
14
+ const temporary = `${job.statusFile}.${process.pid}.tmp`;
15
+ fs.writeFileSync(temporary, JSON.stringify({ ...current, ...values }), { mode: 0o600 });
16
+ fs.renameSync(temporary, job.statusFile);
17
+ }
18
+ try {
19
+ const deadline = Date.now() + 30000;
20
+ while (read().owner?.pid !== process.pid) {
21
+ if (Date.now() >= deadline) throw new Error('独立更新进程交接超时');
22
+ await delay(100);
23
+ }
24
+ await delay(1500);
25
+ update({ status: 'running', startedAt: new Date().toISOString() });
26
+ const args = [job.npmCliPath, 'install', '--global', `@walkhi/code-relax@${job.targetVersion}`,
27
+ '--foreground-scripts', '--allow-scripts=@walkhi/code-relax,node-datachannel,sharp', '--registry=https://registry.npmjs.org'];
28
+ if (job.npmPrefix) args.push('--prefix', job.npmPrefix);
29
+ const child = spawn(job.nodeExecutable, args, { cwd: path.dirname(jobFile), windowsHide: true,
30
+ stdio: ['ignore', 'inherit', 'inherit'] });
31
+ const code = await new Promise((resolve, reject) => {
32
+ child.once('error', reject); child.once('exit', (code, signal) => resolve(code ?? `signal ${signal}`));
33
+ });
34
+ if (code !== 0) throw new Error(`npm 安装失败(${code}),请查看工作站更新日志。`);
35
+ const installed = JSON.parse(fs.readFileSync(path.join(job.serviceRoot, 'installation.json'), 'utf8'));
36
+ if (installed.version !== job.targetVersion) throw new Error('安装结束,但工作站版本与目标版本不一致。');
37
+ update({ status: 'completed', finishedAt: new Date().toISOString(), installedVersion: installed.version });
38
+ } catch (error) {
39
+ console.error(error.stack || error.message);
40
+ update({ status: 'failed', finishedAt: new Date().toISOString(), error: error.message });
41
+ process.exitCode = 1;
42
+ }
@@ -0,0 +1,71 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { startBackground } from './background-process.mjs';
5
+ import { readRecord, writeRecord } from './service-lifecycle.mjs';
6
+ import { runtimePaths } from './platform/runtime-paths.mjs';
7
+ import { desktopHost } from './platform/desktop-host.mjs';
8
+
9
+ const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
10
+ const statusPath = root => path.join(root, 'service-update.json');
11
+ const installation = () => readRecord(path.join(runtimePaths().serviceRoot, 'installation.json'));
12
+ export function serviceUpdateStatus(root) {
13
+ const record = readRecord(statusPath(root));
14
+ const installed = installation();
15
+ return { supported: Boolean(installed?.npmCliPath && fs.existsSync(installed.npmCliPath)),
16
+ currentVersion: installed?.version || null,
17
+ operation: record ? { operationId: record.operationId, targetVersion: record.targetVersion,
18
+ status: record.status, error: record.error || null, installedVersion: record.installedVersion || null } : null };
19
+ }
20
+ export async function checkServiceUpdate(root) {
21
+ const result = await fetch('https://registry.npmjs.org/@walkhi%2fcode-relax/latest', { signal: AbortSignal.timeout(12000) });
22
+ if (!result.ok) throw new Error(`查询工作站版本失败:HTTP ${result.status}`);
23
+ const latest = await result.json();
24
+ if (!versionPattern.test(latest.version || '')) throw new Error('npm 返回的版本号无效');
25
+ return { ...serviceUpdateStatus(root), latestVersion: latest.version };
26
+ }
27
+ let scheduling = false;
28
+ export async function requestServiceUpdate(root, body) {
29
+ if (body.confirmed !== true || !/^[0-9a-f-]{36}$/i.test(body.operationId || '') || !versionPattern.test(body.targetVersion || '')) {
30
+ throw new Error('请确认工作站更新及目标版本后再继续。');
31
+ }
32
+ if (scheduling) throw new Error('工作站更新正在安排。');
33
+ scheduling = true;
34
+ try {
35
+ const previous = readRecord(statusPath(root));
36
+ if (previous?.operationId === body.operationId) return serviceUpdateStatus(root);
37
+ if (previous && ['scheduled', 'running'].includes(previous.status)) {
38
+ const actual = previous.owner?.pid ? await desktopHost.process(previous.owner.pid) : null;
39
+ if (actual && actual.created === previous.owner.created && actual.executable === previous.owner.executable) {
40
+ throw new Error('工作站已有更新正在执行。');
41
+ }
42
+ writeRecord(statusPath(root), { ...previous, status: 'failed', error: '更新进程已退出,未确认安装完成。' });
43
+ }
44
+ const latest = await checkServiceUpdate(root);
45
+ if (!latest.supported) throw new Error('当前安装缺少 npm 路径,请先安装支持手机更新的过渡版。');
46
+ if (latest.latestVersion !== body.targetVersion) throw new Error('最新版本已变化,请重新检查后确认。');
47
+ if (latest.currentVersion === body.targetVersion) throw new Error('工作站已是此版本。');
48
+ const installed = installation();
49
+ const directory = path.join(root, 'service-updates', body.operationId);
50
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
51
+ const worker = path.join(directory, 'worker.mjs');
52
+ fs.copyFileSync(fileURLToPath(new URL('./service-update-worker.mjs', import.meta.url)), worker);
53
+ const jobFile = path.join(directory, 'job.json');
54
+ writeRecord(jobFile, { ...body, statusFile: statusPath(root), npmCliPath: installed.npmCliPath,
55
+ npmPrefix: installed.npmPrefix, nodeExecutable: installed.nodeExecutable, serviceRoot: runtimePaths().serviceRoot });
56
+ writeRecord(statusPath(root), { operationId: body.operationId, targetVersion: body.targetVersion,
57
+ status: 'scheduled', requestedAt: new Date().toISOString() });
58
+ try {
59
+ const log = path.join(directory, 'update.log');
60
+ const pid = await startBackground(installed.nodeExecutable, [worker, jobFile], process.env, [log, log]);
61
+ const owner = await desktopHost.process(pid);
62
+ if (!owner) throw new Error('独立更新进程未启动。');
63
+ writeRecord(statusPath(root), { ...readRecord(statusPath(root)), owner: {
64
+ pid: owner.pid, created: owner.created, executable: owner.executable } });
65
+ } catch (error) {
66
+ writeRecord(statusPath(root), { ...readRecord(statusPath(root)), status: 'failed', error: error.message });
67
+ throw error;
68
+ }
69
+ return serviceUpdateStatus(root);
70
+ } finally { scheduling = false; }
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.35",
3
+ "version": "0.1.0-beta.36",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -41,8 +41,8 @@
41
41
  "tag": "beta"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@walkhi/code-relax-native-win32-x64": "0.1.0-beta.35",
45
- "@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.35",
46
- "@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.35"
44
+ "@walkhi/code-relax-native-win32-x64": "0.1.0-beta.36",
45
+ "@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.36",
46
+ "@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.36"
47
47
  }
48
48
  }
@@ -55,7 +55,8 @@ export function formatSetupOutput(output) {
55
55
  const migration = result.managedAppServerMigration || {};
56
56
  return `Code Relax ${result.version || '未知版本'} 服务安装完成。\n`
57
57
  + '工作站服务和 Codex 内核已就绪,已启用登录自启。\n'
58
- + (migration.restarted ? 'Codex 内核已更新并重启;请检查安装前正在进行的任务。\n' : '')
58
+ + (result.preserveCodex ? '过渡版保留当前 Codex 和内核运行状态;待手机发起下一次完整更新。\n' : migration.restarted ? `Codex 内核已更新:${migration.previousVersion || '未知'} → ${result.managedAppServer?.version || '未知'}(PID ${migration.previousPid} → ${migration.currentPid})。\n` : 'Codex 内核已是当前选定版本,无需切换。\n')
59
+ + (result.preserveCodex ? '' : result.desktopRestarted ? '电脑版 Codex 已重启,并确认连接到工作站内核。\n' : '安装前未运行电脑版 Codex,保持关闭。\n')
59
60
  + (result.desktopRestartRecommended ? 'Codex Desktop:请完全退出后重新打开。\n' : '');
60
61
  } catch { return output; }
61
62
  }
@@ -150,7 +151,7 @@ if (invokedDirectly && process.env.npm_config_global === 'true') {
150
151
  const { prepareNativePackage } = await import('../dist/src/platform/native-package.mjs');
151
152
  prepareNativePackage(fileURLToPath(new URL('../', import.meta.url)));
152
153
  setupStarted = true;
153
- runStage(entry, 'setup', ['setup', '--json'], output => formatSetupOutput(output));
154
+ runStage(entry, 'setup', ['setup', '--json', '--preserve-codex'], output => formatSetupOutput(output));
154
155
  try { runStage(entry, 'doctor', ['doctor']); }
155
156
  catch (error) {
156
157
  console.error('服务安装已完成,但安装后健康检查未通过;请查看上方诊断与安装日志。');