@walkhi/code-relax 0.1.0-beta.79 → 0.1.0-beta.80

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.
@@ -1 +1 @@
1
- {"layout":"npm-platform-packages","version":"0.1.0-beta.79"}
1
+ {"layout":"npm-platform-packages","version":"0.1.0-beta.80"}
@@ -1,9 +1,31 @@
1
- import { managedEndpointUrl } from '../../managed-endpoint.mjs';
2
- // Only the new Desktop process receives the managed connection settings.
3
- // Do not mutate shell profiles or the login session's global environment.
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { managedUserEnvironment, managedEnvironmentName } from '../managed-user-environment.mjs';
6
+ import { installedDesktop } from './desktop-installation.mjs';
7
+
8
+ const exec = promisify(execFile);
9
+ const systemctl = async args => (await exec('/usr/bin/systemctl', ['--user', ...args], { timeout: 5000 })).stdout;
10
+ const config = path.isAbsolute(process.env.XDG_CONFIG_HOME || '') ? process.env.XDG_CONFIG_HOME : path.join(os.homedir(), '.config');
11
+ export const environmentConfig = url => `# Managed by Code Relax\n${managedEnvironmentName}=${url}\n`;
12
+ const adapter = managedUserEnvironment({
13
+ file: path.join(config, 'environment.d/60-code-relax.conf'),
14
+ render: environmentConfig, scope: 'systemd-user',
15
+ read: async () => {
16
+ if (process.getuid() === 0) throw new Error('请以普通 Linux 用户配置内核环境,不使用 sudo。');
17
+ const lines = (await systemctl(['show-environment'])).split('\n');
18
+ const value = name => {
19
+ const raw = lines.find(line => line.startsWith(`${name}=`))?.slice(name.length + 1) || '';
20
+ return raw.startsWith('"') ? JSON.parse(raw) : raw;
21
+ };
22
+ return { value: value(managedEnvironmentName), forceCli: value('CODEX_APP_SERVER_FORCE_CLI') };
23
+ },
24
+ set: url => systemctl(['set-environment', `${managedEnvironmentName}=${url}`]),
25
+ unset: () => systemctl(['unset-environment', managedEnvironmentName]),
26
+ });
4
27
  export async function configureManagedEnvironment(stateRoot, url) {
5
- if (!managedEndpointUrl(url)) throw new Error('未确认 Codex 内核地址。');
6
- return { configured: true, changed: false, owned: false, persistent: false,
7
- scope: 'application-launch', value: url };
28
+ if (!installedDesktop()) return { configured: false, reason: 'desktop-not-installed' };
29
+ return adapter.configureManagedEnvironment(stateRoot, url);
8
30
  }
9
- export async function clearManagedEnvironment() { return { removed: false, preserved: false, reason: 'application-launch-only' }; }
31
+ export const { clearManagedEnvironment } = adapter;
@@ -1,9 +1,31 @@
1
- import { managedEndpointUrl } from '../../managed-endpoint.mjs';
2
- // Launch Services receives these settings for one new application instance.
3
- // Do not mutate shell profiles or the login session's global environment.
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { managedUserEnvironment, managedEnvironmentName } from '../managed-user-environment.mjs';
4
+ import { systemRequest } from './system.mjs';
5
+
6
+ const exec = promisify(execFile);
7
+ const launchctl = async args => (await exec('/bin/launchctl', args, { timeout: 5000 })).stdout.trim();
8
+ const readVariable = async name => {
9
+ try { return await launchctl(['getenv', name]); }
10
+ catch (error) { if (error.code === 1 && !error.stdout && !error.stderr) return ''; throw error; }
11
+ };
12
+ const adapter = managedUserEnvironment({
13
+ // The existing login service restores this only after the kernel is verified.
14
+ // A second static setenv LaunchAgent could race a newly selected dynamic port.
15
+ scope: 'user-launchd',
16
+ read: async () => {
17
+ if (process.getuid() === 0) throw new Error('请以普通 Mac 用户配置内核环境,不使用 sudo。');
18
+ await launchctl(['print-disabled', `gui/${process.getuid()}`]);
19
+ if (await launchctl(['managername']) !== 'Aqua')
20
+ throw new Error('请在当前 Mac 图形登录会话的终端运行 relax setup,以配置 Desktop 的用户环境。');
21
+ return { value: await readVariable(managedEnvironmentName), forceCli: await readVariable('CODEX_APP_SERVER_FORCE_CLI') };
22
+ },
23
+ set: url => launchctl(['setenv', managedEnvironmentName, url]),
24
+ unset: () => launchctl(['unsetenv', managedEnvironmentName]),
25
+ });
4
26
  export async function configureManagedEnvironment(stateRoot, url) {
5
- if (!managedEndpointUrl(url)) throw new Error('未确认 Codex 内核地址。');
6
- return { configured: true, changed: false, owned: false, persistent: false,
7
- scope: 'application-launch', value: url };
27
+ if (!(await systemRequest({ operation: 'desktop-info', url })).desktop)
28
+ return { configured: false, reason: 'desktop-not-installed' };
29
+ return adapter.configureManagedEnvironment(stateRoot, url);
8
30
  }
9
- export async function clearManagedEnvironment() { return { removed: false, preserved: false, reason: 'application-launch-only' }; }
31
+ export const { clearManagedEnvironment } = adapter;
@@ -0,0 +1,78 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { readRecord, writeRecord } from '../service-lifecycle.mjs';
5
+ import { managedEndpointUrl } from '../managed-endpoint.mjs';
6
+ import { assertPlainPath } from '../skill-install.mjs';
7
+
8
+ export const managedEnvironmentName = 'CODEX_APP_SERVER_WS_URL';
9
+ const readText = file => {
10
+ try { return fs.readFileSync(file, 'utf8'); }
11
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
12
+ };
13
+
14
+ // Platform adapters own the session mechanism; ownership and file updates are shared.
15
+ export function managedUserEnvironment({ file = null, render, read, set, unset, scope, environment = process.env }) {
16
+ const values = record => record?.file === file && record?.scope === scope && record?.name === managedEnvironmentName ? record.values || [] : [];
17
+ const ownedValue = (record, value) => !!value && values(record).some(item => managedEndpointUrl(item) === value);
18
+ const ownedFile = (record, text) => !!file && text !== null && values(record)
19
+ .some(value => managedEndpointUrl(value) && text === render(value));
20
+ return {
21
+ async configureManagedEnvironment(stateRoot, url) {
22
+ if (!managedEndpointUrl(url)) throw new Error('未确认 Codex 内核地址,未修改环境变量。');
23
+ const ownershipPath = path.join(stateRoot, 'managed-environment.json');
24
+ if (file) assertPlainPath(file);
25
+ assertPlainPath(ownershipPath);
26
+ const record = readRecord(ownershipPath), text = file ? readText(file) : null, current = await read();
27
+ if (environment.CODEX_APP_SERVER_FORCE_CLI === '1' || current.forceCli === '1')
28
+ throw new Error('CODEX_APP_SERVER_FORCE_CLI=1 会让 Codex 忽略当前内核地址;未修改该配置。');
29
+ if (current.value && (current.value !== url || record) && !ownedValue(record, current.value))
30
+ throw new Error('用户已有其他 CODEX_APP_SERVER_WS_URL;未覆盖该配置。');
31
+ if (text !== null && !ownedFile(record, text))
32
+ throw new Error('同名用户环境文件不属于此安装或已被修改,未覆盖。');
33
+ if (!record && text === null && current.value === url)
34
+ return { configured: true, changed: false, owned: false, persistent: false, scope, value: url };
35
+ const expected = file ? render(url) : null, changed = text !== expected || current.value !== url;
36
+ // Journal owned endpoints before mutation so interrupted updates remain recoverable.
37
+ const ownership = { name: managedEnvironmentName, value: url, file, scope,
38
+ values: [...new Set([url, ...values(record).filter(value => value === current.value || (file && render(value) === text))])],
39
+ configuredAt: new Date().toISOString() };
40
+ writeRecord(ownershipPath, ownership);
41
+ if (text !== expected) {
42
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
43
+ const temporary = `${file}.${randomUUID()}.tmp`;
44
+ try {
45
+ fs.writeFileSync(temporary, expected, { flag: 'wx', mode: 0o600 });
46
+ fs.renameSync(temporary, file);
47
+ } finally { fs.rmSync(temporary, { force: true }); }
48
+ }
49
+ if (current.value !== url) await set(url);
50
+ if ((await read()).value !== url) throw new Error('CODEX_APP_SERVER_WS_URL 写入后读回不一致。');
51
+ ownership.values = [url];
52
+ writeRecord(ownershipPath, ownership);
53
+ return { configured: true, changed, owned: true, persistent: !!file, restoredAtServiceStart: true, scope, value: url };
54
+ },
55
+ async clearManagedEnvironment(stateRoot) {
56
+ const ownershipPath = path.join(stateRoot, 'managed-environment.json');
57
+ if (file) assertPlainPath(file);
58
+ assertPlainPath(ownershipPath);
59
+ const record = readRecord(ownershipPath), text = file ? readText(file) : null;
60
+ if (!record && text === null) return { removed: false, preserved: false, reason: 'not-owned' };
61
+ const current = await read();
62
+ let removed = false, preserved = false;
63
+ if (text !== null) {
64
+ if (ownedFile(record, text)) { fs.unlinkSync(file); removed = true; }
65
+ else preserved = true;
66
+ }
67
+ if (current.value) {
68
+ if (ownedValue(record, current.value)) {
69
+ await unset();
70
+ if ((await read()).value) throw new Error('CODEX_APP_SERVER_WS_URL 清理后读回仍不为空。');
71
+ removed = true;
72
+ } else preserved = true;
73
+ }
74
+ fs.rmSync(ownershipPath, { force: true });
75
+ return { removed, preserved, reason: preserved ? 'user-modified-or-not-owned' : removed ? 'removed' : 'already-empty' };
76
+ },
77
+ };
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.79",
3
+ "version": "0.1.0-beta.80",
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.79",
45
- "@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.79",
46
- "@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.79"
44
+ "@walkhi/code-relax-native-win32-x64": "0.1.0-beta.80",
45
+ "@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.80",
46
+ "@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.80"
47
47
  }
48
48
  }