@walkhi/code-relax 0.1.0-beta.32 → 0.1.0-beta.34

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.
@@ -23,6 +23,7 @@ import { controlRequest } from '../src/service-control.mjs';
23
23
  import { processAlive } from '../src/background-process.mjs';
24
24
  import { prepareNativePackage } from '../src/platform/native-package.mjs';
25
25
  import { managedAppServerUrl } from '../src/managed-app-server.mjs';
26
+ import { powerConfigPath, powerEnabled } from '../src/service-power.mjs';
26
27
  import { readDotsTasks } from '../src/dots-task-catalog.mjs';
27
28
  import { installDesktopHelper, desktopHelperStatus, removeDesktopHelper } from '../src/platform/windows/desktop-helper.mjs';
28
29
 
@@ -45,6 +46,8 @@ const help = `Code Relax(Windows / macOS / Linux)
45
46
  desktop-helper-remove (Windows)移除桌面操作辅助服务,需要工作站 UAC 授权
46
47
  doctor 简单检查工作站是否正常
47
48
  diag 输出详细诊断报告,供客服或技术人员排查
49
+ keep-awake 默认保持电脑唤醒;查看状态
50
+ keep-awake on|off 开启或关闭保持唤醒,立即生效并记住选择;允许屏幕熄灭
48
51
  kernel-restart --confirm 重启 Codex 内核并启用已选更新;等待新内核就绪
49
52
  desktop-recover --confirm 重新打开 Desktop 并接入 Codex 内核;不强制重启已有内核
50
53
  uninstall 停止并移除固定目录服务和本包 Skill,清理自有 Desktop 环境变量,保留其他配置与附件
@@ -71,8 +74,10 @@ async function main() {
71
74
  const longPair = command === 'pair' && options.length === 2 && options[0] === '--expires';
72
75
  const revokeInvite = command === 'pair-revoke' && options.length === 1 && /^[\w-]{16,128}$/.test(options[0]);
73
76
  const revokeDevice = command === 'device-revoke' && options.length === 2 && /^[\w-]{16,128}$/.test(options[0]) && options[1] === '--confirm';
74
- if (!['kernel-restart', 'desktop-recover', 'setup', 'doctor', 'diag', 'uninstall', 'start', 'pair', 'pair-list', 'pair-revoke', 'devices', 'device-revoke', 'dots-tasks', 'desktop-helper-install', 'desktop-helper-status', 'desktop-helper-remove'].includes(command)
75
- || (!longPair && !revokeInvite && !revokeDevice && (['pair-revoke', 'device-revoke'].includes(command) || options.some(flag => !((['kernel-restart', 'desktop-recover'].includes(command) && flag === '--confirm') || (command === 'setup' && flag === '--desktop-helper')))))) {
77
+ const powerCommand = command === 'keep-awake';
78
+ if (powerCommand && !(options.length === 0 || options.length === 1 && ['on', 'off'].includes(options[0]))) throw new Error('用法:relax keep-awake [on|off]');
79
+ if (!['keep-awake', 'kernel-restart', 'desktop-recover', 'setup', 'doctor', 'diag', 'uninstall', 'start', 'pair', 'pair-list', 'pair-revoke', 'devices', 'device-revoke', 'dots-tasks', 'desktop-helper-install', 'desktop-helper-status', 'desktop-helper-remove'].includes(command)
80
+ || (!powerCommand && !longPair && !revokeInvite && !revokeDevice && (['pair-revoke', 'device-revoke'].includes(command) || options.some(flag => !((['kernel-restart', 'desktop-recover'].includes(command) && flag === '--confirm') || (command === 'setup' && flag === '--desktop-helper')))))) {
76
81
  throw new Error('无效命令或参数。使用 --help 查看用法。');
77
82
  }
78
83
  const inviteExpires = longPair ? Date.parse(options[1]) : undefined;
@@ -100,6 +105,25 @@ async function main() {
100
105
  }
101
106
  const paths = runtimePaths();
102
107
  const { stateRoot, serviceRoot } = paths;
108
+ if (powerCommand) {
109
+ const session = readRecord(path.join(stateRoot, 'bridge-session.json'));
110
+ let live = false;
111
+ if (session?.controlPort) {
112
+ try { await controlRequest(session); live = true; }
113
+ catch (error) { if (error.code !== 'ECONNREFUSED') throw error; }
114
+ }
115
+ let result;
116
+ if (live) {
117
+ const action = options[0] === 'on' ? 'enable' : options[0] === 'off' ? 'disable' : 'status';
118
+ result = await controlRequest(session, 'POST', `keep-awake-${action}`);
119
+ } else {
120
+ if (options.length) { fs.mkdirSync(stateRoot, { recursive: true }); writeRecord(powerConfigPath(stateRoot), { enabled: options[0] === 'on' }); }
121
+ result = { enabled: powerEnabled(stateRoot), active: false, error: '', serviceStopped: true };
122
+ }
123
+ console.log(json ? JSON.stringify(result) : `保持唤醒:${result.enabled ? '已开启' : '已关闭'};${result.active ? '正在生效,允许屏幕熄灭' : result.serviceStopped ? '服务未运行,下次启动应用此设置' : result.error || '未生效'}`);
124
+ if (result.error) process.exitCode = 1;
125
+ return;
126
+ }
103
127
  const installedEntry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
104
128
  const installationPath = path.join(serviceRoot, 'installation.json');
105
129
  const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
@@ -1 +1 @@
1
- {"layout":"npm-platform-packages","version":"0.1.0-beta.32"}
1
+ {"layout":"npm-platform-packages","version":"0.1.0-beta.34"}
@@ -0,0 +1,22 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { spawn } from 'node:child_process';
4
+
5
+ // stdin is a lifetime lease: even an abrupt service exit releases the assertion.
6
+ export function launchPowerInhibitor() {
7
+ const lease = "process.stdin.resume();process.stdin.on('end',()=>process.exit(0));console.log('ready')";
8
+ let executable, args;
9
+ if (process.platform === 'darwin') {
10
+ executable = '/usr/bin/caffeinate';
11
+ args = ['-i', process.execPath, '-e', lease];
12
+ } else if (process.platform === 'win32') {
13
+ executable = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
14
+ args = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
15
+ fileURLToPath(new URL('./windows/keep-awake.ps1', import.meta.url))];
16
+ } else if (process.platform === 'linux') {
17
+ executable = '/usr/bin/systemd-inhibit';
18
+ args = ['--what=idle', '--mode=block', '--who=Code Relax', '--why=Workstation remote access',
19
+ process.execPath, '-e', lease];
20
+ } else throw new Error(`保持唤醒不支持当前系统:${process.platform}`);
21
+ return spawn(executable, args, { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
22
+ }
@@ -0,0 +1,20 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ Import-Module "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1"
3
+ Add-Type -TypeDefinition @'
4
+ using System;
5
+ using System.Runtime.InteropServices;
6
+ public static class CodeRelaxPowerLease {
7
+ [DllImport("kernel32.dll", SetLastError = true)]
8
+ static extern uint SetThreadExecutionState(uint flags);
9
+ public static void Hold() {
10
+ if (SetThreadExecutionState(0x80000001u) == 0)
11
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
12
+ try {
13
+ Console.WriteLine("ready");
14
+ Console.Out.Flush();
15
+ Console.In.ReadLine();
16
+ } finally { SetThreadExecutionState(0x80000000u); }
17
+ }
18
+ }
19
+ '@
20
+ [CodeRelaxPowerLease]::Hold()
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
2
2
  import { RustDeskVideoProbe } from './platform/windows/rustdesk-video-probe.mjs';
3
3
  import fs from 'node:fs';
4
4
  import http from 'node:http';
5
+ import { createPowerManager } from './service-power.mjs';
5
6
  import path from 'node:path';
6
7
  import os from 'node:os';
7
8
  import { fileURLToPath } from 'node:url';
@@ -307,6 +308,7 @@ async function handleRequest(request, response) {
307
308
  const executionReady = executionHealth.state.ready && sharedServer?.ready === true;
308
309
  sendJson(response, 200, {
309
310
  status: 'ok', resident: true, version: runtimeVersion,
311
+ keepAwake: powerManager.snapshot(),
310
312
  execution: { ...executionHealth.state, ready: executionReady, canWrite: executionReady,
311
313
  mode: executionMode },
312
314
  managedAppServer: managedAppServer.snapshot(),
@@ -975,6 +977,7 @@ const lanSocket = attachLanSocket(server, { token: options.token, dispatch: hand
975
977
  });
976
978
 
977
979
  let control;
980
+ const powerManager = createPowerManager(stateRoot);
978
981
  void managedAppServer.start();
979
982
  executionHealth.start();
980
983
  runtimeDiscovery.start();
@@ -984,10 +987,14 @@ server.on('error', error => {
984
987
  });
985
988
  server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
986
989
  try {
990
+ await powerManager.start();
987
991
  if (process.env.CODEX_REMOTE_INSTANCE && process.env.CODEX_REMOTE_CONTROL_SECRET && process.env.CODEX_REMOTE_READY_PATH) {
988
992
  control = await openControl({ instanceId: process.env.CODEX_REMOTE_INSTANCE,
989
993
  secret: process.env.CODEX_REMOTE_CONTROL_SECRET, shutdown: shutDown, actions: {
990
994
  'refresh-execution': refreshExecution,
995
+ 'keep-awake-status': () => powerManager.snapshot(),
996
+ 'keep-awake-enable': () => powerManager.setEnabled(true),
997
+ 'keep-awake-disable': () => powerManager.setEnabled(false),
991
998
  'relay-start': () => relayManager.run('relay-start'),
992
999
  'relay-status': () => relayManager.run('relay-status'),
993
1000
  'relay-stop': () => relayManager.run('relay-stop'),
@@ -1021,6 +1028,7 @@ let shuttingDown = false;
1021
1028
  async function shutDown() {
1022
1029
  if (shuttingDown) return;
1023
1030
  shuttingDown = true;
1031
+ await powerManager.close();
1024
1032
  desktopHelper?.release();
1025
1033
  lanSocket.close();
1026
1034
  server.close();
@@ -38,6 +38,12 @@ export async function doctor({ paths, host, service, skillTarget, ensureAutostar
38
38
  await check('service', async () => {
39
39
  const state = await service.status();
40
40
  serviceState = state;
41
+ if (state.keepAwake) {
42
+ const power = state.keepAwake;
43
+ add('keep-awake', power.enabled && !power.active ? 'warning' : 'ok',
44
+ power.active ? '保持唤醒已生效,允许屏幕熄灭。' : !power.enabled ? '保持唤醒已由 CLI 关闭。'
45
+ : `保持唤醒未生效:${power.error || '未知原因'};可执行 relax keep-awake on 重新申请。`);
46
+ }
41
47
  add('service', state.running ? 'ok' : 'error', state.running
42
48
  ? `本机服务正常:${state.mode},端口 ${state.port},运行版本:${state.version || '未知'}` : '服务未就绪;安装后执行 relax start。');
43
49
  if (state.running && installedVersion && state.version !== installedVersion) add('service-version', 'warning', state.version
@@ -78,7 +84,7 @@ export async function doctor({ paths, host, service, skillTarget, ensureAutostar
78
84
  }
79
85
 
80
86
  export function formatDoctorOutput(result, detail = false) {
81
- const labels = { installation: '安装文件', 'bridge-runtime': '运行环境', network: '局域网',
87
+ const labels = { 'keep-awake': '保持唤醒', installation: '安装文件', 'bridge-runtime': '运行环境', network: '局域网',
82
88
  autostart: '登录自启动', service: '工作站服务', 'managed-app-server': 'Codex 内核',
83
89
  'service-version': '服务版本', 'managed-app-server-update': 'Codex 更新',
84
90
  'managed-app-server-launcher': '工作站更新', skill: 'Codex Skill' };
@@ -57,6 +57,7 @@ export function lifecycle({ stateRoot, host }) {
57
57
  return { running: running && result?.status === 'ok', processRunning: running, pid: session?.pid,
58
58
  port: session?.port, mode: result?.transportMode, version: result?.version, execution: result?.execution,
59
59
  managedAppServer: result?.managedAppServer, desktop: result?.desktop,
60
+ keepAwake: result?.keepAwake,
60
61
  desktopRestartRecommended: result?.desktopRestartRecommended === true,
61
62
  resident: result?.resident === true };
62
63
  }
@@ -0,0 +1,58 @@
1
+ import path from 'node:path';
2
+ import { readRecord, writeRecord } from './service-lifecycle.mjs';
3
+ import { launchPowerInhibitor } from './platform/power-inhibitor.mjs';
4
+
5
+ export const powerConfigPath = stateRoot => path.join(stateRoot, 'keep-awake.json');
6
+ export const powerEnabled = stateRoot => readRecord(powerConfigPath(stateRoot))?.enabled !== false;
7
+
8
+ export function createPowerManager(stateRoot, { launch = launchPowerInhibitor, report = console.error } = {}) {
9
+ let enabled = powerEnabled(stateRoot), active = false, error = '', child = null, closed, stopping = false;
10
+ let queue = Promise.resolve();
11
+ const snapshot = () => ({ enabled, active, error });
12
+ async function release() {
13
+ if (!child) return;
14
+ stopping = true;
15
+ child.stdin.end();
16
+ await closed;
17
+ }
18
+ async function acquire() {
19
+ if (child || !enabled) return;
20
+ error = '';
21
+ try {
22
+ const process = launch();
23
+ stopping = false;
24
+ child = process;
25
+ let stderr = '', output = '';
26
+ process.stderr.on('data', data => { stderr = (stderr + data).slice(-2048); });
27
+ process.stdin.on('error', cause => { if (cause.code !== 'EPIPE') report(`保持唤醒通道异常:${cause.message}`); });
28
+ closed = new Promise(resolve => process.once('close', code => {
29
+ if (active && !stopping) { error = stderr.trim() || `保持唤醒进程意外退出(${code})`; report(error); }
30
+ child = null; active = false; resolve();
31
+ }));
32
+ await new Promise((resolve, reject) => {
33
+ const timer = setTimeout(() => { process.kill(); reject(new Error('保持唤醒申请超时')); }, 8000);
34
+ const finish = cause => { clearTimeout(timer); cause ? reject(cause) : resolve(); };
35
+ process.once('error', finish);
36
+ process.once('close', code => {
37
+ finish(new Error(stderr.trim() || `保持唤醒进程已退出(${code})`));
38
+ });
39
+ process.stdout.on('data', data => {
40
+ output += data;
41
+ if (output.includes('ready\n') || output.includes('ready\r\n')) { active = true; finish(); }
42
+ });
43
+ });
44
+ } catch (cause) { error = cause.message; report(`保持唤醒未生效:${error}`); }
45
+ }
46
+ const serial = task => { const result = queue.then(task); queue = result.catch(() => {}); return result; };
47
+ return {
48
+ snapshot,
49
+ start: () => serial(async () => { await acquire(); return snapshot(); }),
50
+ setEnabled: value => serial(async () => {
51
+ writeRecord(powerConfigPath(stateRoot), { enabled: value });
52
+ enabled = value;
53
+ if (enabled) await acquire(); else { await release(); error = ''; }
54
+ return snapshot();
55
+ }),
56
+ close: () => serial(async () => { enabled = false; await release(); }),
57
+ };
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.32",
3
+ "version": "0.1.0-beta.34",
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.32",
45
- "@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.32",
46
- "@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.32"
44
+ "@walkhi/code-relax-native-win32-x64": "0.1.0-beta.34",
45
+ "@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.34",
46
+ "@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.34"
47
47
  }
48
48
  }
@@ -53,14 +53,52 @@ export function formatSetupOutput(output) {
53
53
  try {
54
54
  const result = JSON.parse(output);
55
55
  const migration = result.managedAppServerMigration || {};
56
- return `Code Relax ${result.version || '未知版本'} 安装成功。\n`
56
+ return `Code Relax ${result.version || '未知版本'} 服务安装完成。\n`
57
57
  + '工作站服务和 Codex 内核已就绪,已启用登录自启。\n'
58
58
  + (migration.restarted ? 'Codex 内核已更新并重启;请检查安装前正在进行的任务。\n' : '')
59
- + (result.desktopRestartRecommended ? 'Codex Desktop:请完全退出后重新打开。\n' : '')
60
- + '生成配对二维码:relax pair\n';
59
+ + (result.desktopRestartRecommended ? 'Codex Desktop:请完全退出后重新打开。\n' : '');
61
60
  } catch { return output; }
62
61
  }
63
62
 
63
+ // npm owns command links. Inspect them without changing the user's shell configuration.
64
+ export function commandDiagnostics(entry, { prefix, env = process.env, platform = process.platform } = {}) {
65
+ const windows = platform === 'win32';
66
+ const bin = windows ? prefix : path.join(prefix, 'bin');
67
+ const command = path.join(bin, windows ? 'relax.cmd' : 'relax');
68
+ const lines = [`命令入口:${command}`];
69
+ let linkReady = false;
70
+ try {
71
+ if (windows) {
72
+ const shim = fs.readFileSync(command, 'utf8');
73
+ linkReady = shim.replaceAll('\\', '/').includes('node_modules/@walkhi/code-relax/dist/bin/codex-remote.mjs') && fs.existsSync(entry);
74
+ } else {
75
+ linkReady = fs.realpathSync(command) === fs.realpathSync(entry);
76
+ fs.accessSync(command, fs.constants.X_OK);
77
+ }
78
+ lines.push(linkReady ? '命令链接:正常' : '命令链接:目标与本次安装不一致');
79
+ } catch (error) {
80
+ linkReady = false;
81
+ lines.push(`命令链接:不可用(${error.code || error.message})`);
82
+ }
83
+ const normalize = value => {
84
+ let resolved;
85
+ try { resolved = fs.realpathSync(value); } catch { resolved = path.resolve(value); }
86
+ return windows ? resolved.toLowerCase() : resolved;
87
+ };
88
+ const pathValue = Object.entries(env).find(([key]) => key.toLowerCase() === 'path')?.[1] || '';
89
+ const onPath = pathValue.split(windows ? ';' : ':').filter(Boolean)
90
+ .some(value => normalize(value.replace(/^"|"$/g, '')) === normalize(bin));
91
+ lines.push(`安装进程 PATH:${onPath ? '包含' : '未包含'}全局命令目录`);
92
+ lines.push('注:npm 会调整安装进程 PATH;当前终端能否直接找到命令仍以终端为准。');
93
+ if (!linkReady) lines.push('CLI 入口异常;服务状态请以上方健康检查为准,请保留本次诊断输出。');
94
+ else if (!onPath) lines.push('全局命令目录未在 PATH 中,可先使用下方完整路径命令。');
95
+ const quote = value => "'" + value.replaceAll("'", windows ? "''" : "'\"'\"'") + "'";
96
+ const direct = `${windows ? '& ' : ''}${quote(process.execPath)} ${quote(entry)}`;
97
+ lines.push(`无需依赖 relax 命令的检查入口${windows ? '(PowerShell)' : ''}:`, `${direct} doctor`);
98
+ lines.push('生成配对二维码:', `${direct} pair`);
99
+ return lines.join('\n') + '\n';
100
+ }
101
+
64
102
  function installationSnapshot() {
65
103
  const recordPath = path.join(serviceRoot, 'installation.json');
66
104
  if (!fs.existsSync(serviceRoot)) return { exists: false, valid: false };
@@ -113,8 +151,13 @@ if (invokedDirectly && process.env.npm_config_global === 'true') {
113
151
  prepareNativePackage(fileURLToPath(new URL('../', import.meta.url)));
114
152
  setupStarted = true;
115
153
  runStage(entry, 'setup', ['setup', '--json'], output => formatSetupOutput(output));
154
+ try { runStage(entry, 'doctor', ['doctor']); }
155
+ catch (error) {
156
+ console.error('服务安装已完成,但安装后健康检查未通过;请查看上方诊断与安装日志。');
157
+ process.exitCode = 1;
158
+ }
116
159
  console.log(`安装日志:${installLog}`);
117
- appendLog('[postinstall] success');
160
+ appendLog(process.exitCode ? '[postinstall] failed stage=doctor' : '[postinstall] success');
118
161
  } catch (error) {
119
162
  const current = installationSnapshot();
120
163
  if (!setupStarted) console.error(`平台组件检查失败:${error.message}`);
@@ -123,5 +166,16 @@ if (invokedDirectly && process.env.npm_config_global === 'true') {
123
166
  console.error(`安装日志:${installLog}`);
124
167
  appendLog('[postinstall] failed stage=setup');
125
168
  process.exitCode = Number.isInteger(error?.status) && error.status > 0 ? error.status : 1;
169
+ } finally {
170
+ try {
171
+ const prefix = execFileSync(process.execPath, [process.env.npm_execpath, 'prefix', '--global'],
172
+ { encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }).trim();
173
+ const output = commandDiagnostics(entry, { prefix });
174
+ process.stdout.write(output);
175
+ appendLog(`[command-diagnostics]\n${output}`);
176
+ } catch (error) {
177
+ console.error(`命令入口诊断失败:${error.message}`);
178
+ appendLog(`[command-diagnostics] failed: ${error.message}`);
179
+ }
126
180
  }
127
181
  }