@walkhi/code-relax 0.1.0-beta.50 → 0.1.0-beta.51
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 +105 -105
- package/dist/bin/cleanup-accounts.mjs +63 -63
- package/dist/bin/codex-remote.mjs +403 -403
- package/dist/bin/publish-android-release.mjs +63 -63
- package/dist/bin/release-upload-server.mjs +190 -190
- package/dist/build-platform.json +1 -1
- package/dist/get-shared-service-url.ps1 +14 -14
- package/dist/shared/code-browser.cjs +159 -159
- package/dist/shared/file-changes.cjs +31 -31
- package/dist/shared/file-citations.cjs +117 -117
- package/dist/shared/file-mentions.cjs +92 -92
- package/dist/shared/message-delivery.cjs +115 -115
- package/dist/shared/p2p-file-channel.cjs +87 -87
- package/dist/shared/server-requests.cjs +311 -311
- package/dist/shared/timeline-sync.cjs +173 -173
- package/dist/skills/code-relax/SKILL.md +22 -22
- package/dist/src/bounded-process-log.mjs +63 -63
- package/dist/src/celsius-client.mjs +107 -107
- package/dist/src/dots-approval-timeline.mjs +99 -99
- package/dist/src/dots-avatar.mjs +42 -42
- package/dist/src/dots-chat-projection.mjs +50 -50
- package/dist/src/dots-chat-server.mjs +483 -483
- package/dist/src/dots-message-images.mjs +319 -319
- package/dist/src/dots-profile.mjs +77 -77
- package/dist/src/dots-room-activity.mjs +172 -172
- package/dist/src/dots-room-client.mjs +135 -135
- package/dist/src/dots-room-projection.mjs +94 -94
- package/dist/src/dots-schedules.mjs +66 -66
- package/dist/src/dots-summary-tasks.mjs +32 -32
- package/dist/src/dots-task-catalog.mjs +430 -430
- package/dist/src/file-transfer-range.mjs +22 -22
- package/dist/src/file-upload-session.mjs +130 -130
- package/dist/src/hosted-history-app-server.mjs +103 -103
- package/dist/src/hosted-history-cache.mjs +113 -113
- package/dist/src/lan-socket-server.mjs +266 -266
- package/dist/src/managed-app-server.mjs +373 -373
- package/dist/src/managed-endpoint.mjs +25 -25
- package/dist/src/message-upload-files.mjs +109 -109
- package/dist/src/onboarding.mjs +12 -12
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/codex-protocol.mjs +46 -46
- package/dist/src/platform/desktop-project-store.mjs +208 -208
- package/dist/src/platform/linux/desktop-host.mjs +87 -87
- package/dist/src/platform/linux/process.mjs +53 -53
- package/dist/src/platform/linux/system.mjs +50 -50
- package/dist/src/platform/linux/user-environment.mjs +9 -9
- package/dist/src/platform/macos/codex-runtime.mjs +125 -125
- package/dist/src/platform/macos/desktop-host.mjs +86 -86
- package/dist/src/platform/macos/user-environment.mjs +9 -9
- package/dist/src/platform/native-package.mjs +58 -58
- package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
- package/dist/src/platform/windows/codex-runtime.mjs +215 -215
- package/dist/src/platform/windows/credential-store.mjs +27 -27
- package/dist/src/platform/windows/credential-store.ps1 +49 -49
- package/dist/src/platform/windows/desktop-host.mjs +65 -65
- package/dist/src/platform/windows/desktop-info.ps1 +38 -38
- package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
- package/dist/src/platform/windows/dots-profile.mjs +49 -49
- package/dist/src/platform/windows/launch-worker.mjs +41 -41
- package/dist/src/platform/windows/project-locations.mjs +16 -16
- package/dist/src/platform/windows/service-host.mjs +35 -35
- package/dist/src/platform/windows/user-environment.mjs +52 -52
- package/dist/src/project-management.mjs +58 -58
- package/dist/src/runtime-discovery.mjs +47 -47
- package/dist/src/self-relay/account-cleanup.mjs +30 -30
- package/dist/src/self-relay/admin-page.mjs +80 -80
- package/dist/src/self-relay/admin-style.mjs +14 -14
- package/dist/src/self-relay/connector.mjs +439 -439
- package/dist/src/self-relay/demo.mjs +31 -31
- package/dist/src/self-relay/diagnostics.mjs +54 -54
- package/dist/src/self-relay/email-accounts.mjs +183 -183
- package/dist/src/self-relay/file-wire.mjs +13 -13
- package/dist/src/self-relay/p2p-probe.mjs +168 -168
- package/dist/src/self-relay/privacy-page.mjs +26 -26
- package/dist/src/self-relay/public-releases.mjs +67 -67
- package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
- package/dist/src/self-relay/usage-meter.mjs +141 -141
- package/dist/src/server.mjs +5 -4
- package/dist/src/service-doctor.mjs +148 -148
- package/dist/src/service-install.mjs +97 -97
- package/dist/src/service-lifecycle.mjs +139 -139
- package/dist/src/shared-app-server.mjs +666 -666
- package/dist/src/shared-catalog.mjs +132 -132
- package/dist/src/shared-thread-stream.mjs +165 -165
- package/dist/src/usage-reset.mjs +17 -17
- package/dist/src/workstation-downloads.mjs +120 -120
- package/package.json +4 -4
- package/tools/postinstall.mjs +186 -186
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
|
|
4
|
-
import { runScript } from './desktop-host.mjs';
|
|
5
|
-
import { managedEndpointUrl } from '../../managed-endpoint.mjs';
|
|
6
|
-
|
|
7
|
-
const ownershipFile = 'managed-environment.json';
|
|
8
|
-
|
|
9
|
-
export async function readManagedEnvironment() {
|
|
10
|
-
return JSON.parse(await runScript('user-environment.ps1', ['-Action', 'Read']));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function configureManagedEnvironment(stateRoot, url) {
|
|
14
|
-
const current = await readManagedEnvironment();
|
|
15
|
-
if (!managedEndpointUrl(url)) throw new Error('未确认 Codex 内核地址,未修改环境变量。');
|
|
16
|
-
const ownershipPath = path.join(stateRoot, ownershipFile);
|
|
17
|
-
const ownership = readRecord(ownershipPath);
|
|
18
|
-
if (current.forceCliUser === '1' || current.forceCliMachine === '1') {
|
|
19
|
-
throw new Error('CODEX_APP_SERVER_FORCE_CLI=1 会让 Codex 忽略当前内核地址;未修改该配置。');
|
|
20
|
-
}
|
|
21
|
-
if (current.userValue && current.userValue !== url && current.userValue !== ownership?.value) {
|
|
22
|
-
throw new Error(`用户已有 CODEX_APP_SERVER_WS_URL=${current.userValue};未覆盖该配置。`);
|
|
23
|
-
}
|
|
24
|
-
if (current.userValue === url) {
|
|
25
|
-
const owned = ownership?.value === url;
|
|
26
|
-
return { configured: true, value: current.userValue, changed: false, owned };
|
|
27
|
-
}
|
|
28
|
-
const applied = JSON.parse(await runScript('user-environment.ps1', ['-Action', 'Set', '-Value', url]));
|
|
29
|
-
if (applied.userValue !== url) throw new Error('CODEX_APP_SERVER_WS_URL 写入后读回不一致。');
|
|
30
|
-
writeRecord(ownershipPath, { name: 'CODEX_APP_SERVER_WS_URL', value: url,
|
|
31
|
-
configuredAt: new Date().toISOString() });
|
|
32
|
-
return { configured: true, value: applied.userValue, changed: true, owned: true };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function clearManagedEnvironment(stateRoot) {
|
|
36
|
-
const ownershipPath = path.join(stateRoot, ownershipFile);
|
|
37
|
-
const ownership = readRecord(ownershipPath);
|
|
38
|
-
const current = await readManagedEnvironment();
|
|
39
|
-
if (!current.userValue) {
|
|
40
|
-
fs.rmSync(ownershipPath, { force: true });
|
|
41
|
-
return { removed: false, preserved: false, reason: 'already-empty' };
|
|
42
|
-
}
|
|
43
|
-
if (!managedEndpointUrl(ownership?.value) || current.userValue !== ownership.value) {
|
|
44
|
-
fs.rmSync(ownershipPath, { force: true });
|
|
45
|
-
return { removed: false, preserved: true, value: current.userValue,
|
|
46
|
-
reason: managedEndpointUrl(ownership?.value) ? 'user-modified' : 'not-owned' };
|
|
47
|
-
}
|
|
48
|
-
const result = JSON.parse(await runScript('user-environment.ps1', ['-Action', 'Clear']));
|
|
49
|
-
if (result.userValue) throw new Error('CODEX_APP_SERVER_WS_URL 清理后读回仍不为空。');
|
|
50
|
-
fs.rmSync(ownershipPath, { force: true });
|
|
51
|
-
return { removed: true, preserved: false, reason: 'removed' };
|
|
52
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
|
|
4
|
+
import { runScript } from './desktop-host.mjs';
|
|
5
|
+
import { managedEndpointUrl } from '../../managed-endpoint.mjs';
|
|
6
|
+
|
|
7
|
+
const ownershipFile = 'managed-environment.json';
|
|
8
|
+
|
|
9
|
+
export async function readManagedEnvironment() {
|
|
10
|
+
return JSON.parse(await runScript('user-environment.ps1', ['-Action', 'Read']));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function configureManagedEnvironment(stateRoot, url) {
|
|
14
|
+
const current = await readManagedEnvironment();
|
|
15
|
+
if (!managedEndpointUrl(url)) throw new Error('未确认 Codex 内核地址,未修改环境变量。');
|
|
16
|
+
const ownershipPath = path.join(stateRoot, ownershipFile);
|
|
17
|
+
const ownership = readRecord(ownershipPath);
|
|
18
|
+
if (current.forceCliUser === '1' || current.forceCliMachine === '1') {
|
|
19
|
+
throw new Error('CODEX_APP_SERVER_FORCE_CLI=1 会让 Codex 忽略当前内核地址;未修改该配置。');
|
|
20
|
+
}
|
|
21
|
+
if (current.userValue && current.userValue !== url && current.userValue !== ownership?.value) {
|
|
22
|
+
throw new Error(`用户已有 CODEX_APP_SERVER_WS_URL=${current.userValue};未覆盖该配置。`);
|
|
23
|
+
}
|
|
24
|
+
if (current.userValue === url) {
|
|
25
|
+
const owned = ownership?.value === url;
|
|
26
|
+
return { configured: true, value: current.userValue, changed: false, owned };
|
|
27
|
+
}
|
|
28
|
+
const applied = JSON.parse(await runScript('user-environment.ps1', ['-Action', 'Set', '-Value', url]));
|
|
29
|
+
if (applied.userValue !== url) throw new Error('CODEX_APP_SERVER_WS_URL 写入后读回不一致。');
|
|
30
|
+
writeRecord(ownershipPath, { name: 'CODEX_APP_SERVER_WS_URL', value: url,
|
|
31
|
+
configuredAt: new Date().toISOString() });
|
|
32
|
+
return { configured: true, value: applied.userValue, changed: true, owned: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function clearManagedEnvironment(stateRoot) {
|
|
36
|
+
const ownershipPath = path.join(stateRoot, ownershipFile);
|
|
37
|
+
const ownership = readRecord(ownershipPath);
|
|
38
|
+
const current = await readManagedEnvironment();
|
|
39
|
+
if (!current.userValue) {
|
|
40
|
+
fs.rmSync(ownershipPath, { force: true });
|
|
41
|
+
return { removed: false, preserved: false, reason: 'already-empty' };
|
|
42
|
+
}
|
|
43
|
+
if (!managedEndpointUrl(ownership?.value) || current.userValue !== ownership.value) {
|
|
44
|
+
fs.rmSync(ownershipPath, { force: true });
|
|
45
|
+
return { removed: false, preserved: true, value: current.userValue,
|
|
46
|
+
reason: managedEndpointUrl(ownership?.value) ? 'user-modified' : 'not-owned' };
|
|
47
|
+
}
|
|
48
|
+
const result = JSON.parse(await runScript('user-environment.ps1', ['-Action', 'Clear']));
|
|
49
|
+
if (result.userValue) throw new Error('CODEX_APP_SERVER_WS_URL 清理后读回仍不为空。');
|
|
50
|
+
fs.rmSync(ownershipPath, { force: true });
|
|
51
|
+
return { removed: true, preserved: false, reason: 'removed' };
|
|
52
|
+
}
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
const fail = (message, statusCode = 400) => { throw Object.assign(new Error(message), { statusCode }); };
|
|
4
|
-
const pathKey = value => process.platform === 'win32' ? value.toLowerCase() : value;
|
|
5
|
-
|
|
6
|
-
function absoluteDirectory(value) {
|
|
7
|
-
if (typeof value !== 'string' || !value.trim() || !path.isAbsolute(value.trim())) fail('请输入电脑上的绝对文件夹路径。');
|
|
8
|
-
return path.normalize(value.trim());
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async function directory(rpc, value) {
|
|
12
|
-
const target = absoluteDirectory(value);
|
|
13
|
-
const stat = await rpc('fs/getMetadata', { path: target });
|
|
14
|
-
if (!stat.isDirectory) fail(`此路径不是文件夹:${target}`);
|
|
15
|
-
return target;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function folderName(value) {
|
|
19
|
-
if (typeof value !== 'string' || !value.trim() || /[<>:"/\\|?*\x00-\x1f]/.test(value) || /[ .]$/.test(value)
|
|
20
|
-
|| /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(value) || ['.', '..'].includes(value)) {
|
|
21
|
-
fail('文件夹名称无效,请勿使用路径分隔符或系统保留名称。');
|
|
22
|
-
}
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function projectRoots(rpc, roots) {
|
|
27
|
-
if (!Array.isArray(roots)) fail('项目文件夹列表无效。');
|
|
28
|
-
const result = [], seen = new Set();
|
|
29
|
-
for (const root of roots) {
|
|
30
|
-
const target = await directory(rpc, root?.path);
|
|
31
|
-
const key = pathKey(target);
|
|
32
|
-
if (seen.has(key)) fail(`重复的项目文件夹:${target}`);
|
|
33
|
-
seen.add(key); result.push({ path: target });
|
|
34
|
-
}
|
|
35
|
-
return result;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function projectSummary(project) {
|
|
39
|
-
return { projectId: project.id, name: project.name, label: project.name, roots: project.roots,
|
|
40
|
-
path: project.roots[0]?.path || '', cwd: project.roots[0]?.path || '', hostId: 'local' };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function browseProjectFolders(rpc, locations, value) {
|
|
44
|
-
const target = absoluteDirectory(value || locations?.home);
|
|
45
|
-
const entries = (await rpc('fs/readDirectory', { path: target })).entries
|
|
46
|
-
.filter(entry => entry.isDirectory).map(entry => ({ name: entry.fileName, path: path.join(target, entry.fileName) }))
|
|
47
|
-
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN', { numeric: true }));
|
|
48
|
-
return { path: target, parent: path.dirname(target) === target ? null : path.dirname(target), entries, ...(locations || {}) };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function createProjectFolder(rpc, body) {
|
|
52
|
-
const parent = await directory(rpc, body.parent);
|
|
53
|
-
const name = folderName(body.name);
|
|
54
|
-
const entries = (await rpc('fs/readDirectory', { path: parent })).entries;
|
|
55
|
-
if (entries.some(entry => pathKey(entry.fileName) === pathKey(name))) fail('同名文件夹已存在,请选择它或使用其他名称。', 409);
|
|
56
|
-
const target = path.join(parent, name);
|
|
57
|
-
await rpc('fs/createDirectory', { path: target, recursive: false });
|
|
58
|
-
return { path: target };
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const fail = (message, statusCode = 400) => { throw Object.assign(new Error(message), { statusCode }); };
|
|
4
|
+
const pathKey = value => process.platform === 'win32' ? value.toLowerCase() : value;
|
|
5
|
+
|
|
6
|
+
function absoluteDirectory(value) {
|
|
7
|
+
if (typeof value !== 'string' || !value.trim() || !path.isAbsolute(value.trim())) fail('请输入电脑上的绝对文件夹路径。');
|
|
8
|
+
return path.normalize(value.trim());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function directory(rpc, value) {
|
|
12
|
+
const target = absoluteDirectory(value);
|
|
13
|
+
const stat = await rpc('fs/getMetadata', { path: target });
|
|
14
|
+
if (!stat.isDirectory) fail(`此路径不是文件夹:${target}`);
|
|
15
|
+
return target;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function folderName(value) {
|
|
19
|
+
if (typeof value !== 'string' || !value.trim() || /[<>:"/\\|?*\x00-\x1f]/.test(value) || /[ .]$/.test(value)
|
|
20
|
+
|| /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(value) || ['.', '..'].includes(value)) {
|
|
21
|
+
fail('文件夹名称无效,请勿使用路径分隔符或系统保留名称。');
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function projectRoots(rpc, roots) {
|
|
27
|
+
if (!Array.isArray(roots)) fail('项目文件夹列表无效。');
|
|
28
|
+
const result = [], seen = new Set();
|
|
29
|
+
for (const root of roots) {
|
|
30
|
+
const target = await directory(rpc, root?.path);
|
|
31
|
+
const key = pathKey(target);
|
|
32
|
+
if (seen.has(key)) fail(`重复的项目文件夹:${target}`);
|
|
33
|
+
seen.add(key); result.push({ path: target });
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function projectSummary(project) {
|
|
39
|
+
return { projectId: project.id, name: project.name, label: project.name, roots: project.roots,
|
|
40
|
+
path: project.roots[0]?.path || '', cwd: project.roots[0]?.path || '', hostId: 'local' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function browseProjectFolders(rpc, locations, value) {
|
|
44
|
+
const target = absoluteDirectory(value || locations?.home);
|
|
45
|
+
const entries = (await rpc('fs/readDirectory', { path: target })).entries
|
|
46
|
+
.filter(entry => entry.isDirectory).map(entry => ({ name: entry.fileName, path: path.join(target, entry.fileName) }))
|
|
47
|
+
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN', { numeric: true }));
|
|
48
|
+
return { path: target, parent: path.dirname(target) === target ? null : path.dirname(target), entries, ...(locations || {}) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function createProjectFolder(rpc, body) {
|
|
52
|
+
const parent = await directory(rpc, body.parent);
|
|
53
|
+
const name = folderName(body.name);
|
|
54
|
+
const entries = (await rpc('fs/readDirectory', { path: parent })).entries;
|
|
55
|
+
if (entries.some(entry => pathKey(entry.fileName) === pathKey(name))) fail('同名文件夹已存在,请选择它或使用其他名称。', 409);
|
|
56
|
+
const target = path.join(parent, name);
|
|
57
|
+
await rpc('fs/createDirectory', { path: target, recursive: false });
|
|
58
|
+
return { path: target };
|
|
59
59
|
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import { discoverCodex, resolveCodexRuntime, runtimeFingerprint } from './platform/codex-runtime.mjs';
|
|
2
|
-
|
|
3
|
-
// Advisory discovery is independent of execution health. Failure here must never
|
|
4
|
-
// disable an already running app-server or cause an automatic process restart.
|
|
5
|
-
export class RuntimeDiscovery {
|
|
6
|
-
constructor({ stateRoot, host, url, interval = 60000 }) {
|
|
7
|
-
Object.assign(this, { stateRoot, host, url, interval });
|
|
8
|
-
this.state = {};
|
|
9
|
-
}
|
|
10
|
-
start() { this.stopped = false; void this.tick(); }
|
|
11
|
-
stop() { this.stopped = true; clearTimeout(this.timer); }
|
|
12
|
-
async sample() {
|
|
13
|
-
const errors = [];
|
|
14
|
-
try {
|
|
15
|
-
const candidates = discoverCodex();
|
|
16
|
-
const signature = JSON.stringify(candidates.map(candidate => {
|
|
17
|
-
try { return [candidate.executable, runtimeFingerprint(candidate.executable)]; }
|
|
18
|
-
catch (error) { return [candidate.executable, error.message]; }
|
|
19
|
-
}));
|
|
20
|
-
if (signature !== this.signature) {
|
|
21
|
-
await resolveCodexRuntime(this.stateRoot, { refresh: true });
|
|
22
|
-
this.signature = signature;
|
|
23
|
-
}
|
|
24
|
-
} catch (error) { errors.push(error.message); }
|
|
25
|
-
try {
|
|
26
|
-
if (this.host.desktopIntegration === false) {
|
|
27
|
-
this.state = { desktop: { state: 'unsupported', integrationSupported: false }, checkedAt: new Date().toISOString() };
|
|
28
|
-
if (errors.length) this.state.discoveryError = errors.join('\n');
|
|
29
|
-
return this.state;
|
|
30
|
-
}
|
|
31
|
-
const desktop = await this.host.inspect();
|
|
32
|
-
const attached = desktop.running && await this.host.sharedServiceUrl() === (typeof this.url === 'function' ? this.url() : this.url);
|
|
33
|
-
this.state = { desktop: { state: !desktop.running ? 'stopped' : attached ? 'managed' : 'ordinary',
|
|
34
|
-
installed: Boolean(desktop.desktop), version: desktop.version || '', attachedToManagedServer: Boolean(attached) },
|
|
35
|
-
desktopRestartRecommended: Boolean(desktop.running && !attached), checkedAt: new Date().toISOString() };
|
|
36
|
-
} catch (error) {
|
|
37
|
-
errors.push(error.message);
|
|
38
|
-
this.state = { checkedAt: new Date().toISOString() };
|
|
39
|
-
}
|
|
40
|
-
if (errors.length) this.state.discoveryError = errors.join('\n');
|
|
41
|
-
return this.state;
|
|
42
|
-
}
|
|
43
|
-
async tick() {
|
|
44
|
-
await this.sample();
|
|
45
|
-
if (!this.stopped) { this.timer = setTimeout(() => this.tick(), this.interval); this.timer.unref?.(); }
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
import { discoverCodex, resolveCodexRuntime, runtimeFingerprint } from './platform/codex-runtime.mjs';
|
|
2
|
+
|
|
3
|
+
// Advisory discovery is independent of execution health. Failure here must never
|
|
4
|
+
// disable an already running app-server or cause an automatic process restart.
|
|
5
|
+
export class RuntimeDiscovery {
|
|
6
|
+
constructor({ stateRoot, host, url, interval = 60000 }) {
|
|
7
|
+
Object.assign(this, { stateRoot, host, url, interval });
|
|
8
|
+
this.state = {};
|
|
9
|
+
}
|
|
10
|
+
start() { this.stopped = false; void this.tick(); }
|
|
11
|
+
stop() { this.stopped = true; clearTimeout(this.timer); }
|
|
12
|
+
async sample() {
|
|
13
|
+
const errors = [];
|
|
14
|
+
try {
|
|
15
|
+
const candidates = discoverCodex();
|
|
16
|
+
const signature = JSON.stringify(candidates.map(candidate => {
|
|
17
|
+
try { return [candidate.executable, runtimeFingerprint(candidate.executable)]; }
|
|
18
|
+
catch (error) { return [candidate.executable, error.message]; }
|
|
19
|
+
}));
|
|
20
|
+
if (signature !== this.signature) {
|
|
21
|
+
await resolveCodexRuntime(this.stateRoot, { refresh: true });
|
|
22
|
+
this.signature = signature;
|
|
23
|
+
}
|
|
24
|
+
} catch (error) { errors.push(error.message); }
|
|
25
|
+
try {
|
|
26
|
+
if (this.host.desktopIntegration === false) {
|
|
27
|
+
this.state = { desktop: { state: 'unsupported', integrationSupported: false }, checkedAt: new Date().toISOString() };
|
|
28
|
+
if (errors.length) this.state.discoveryError = errors.join('\n');
|
|
29
|
+
return this.state;
|
|
30
|
+
}
|
|
31
|
+
const desktop = await this.host.inspect();
|
|
32
|
+
const attached = desktop.running && await this.host.sharedServiceUrl() === (typeof this.url === 'function' ? this.url() : this.url);
|
|
33
|
+
this.state = { desktop: { state: !desktop.running ? 'stopped' : attached ? 'managed' : 'ordinary',
|
|
34
|
+
installed: Boolean(desktop.desktop), version: desktop.version || '', hasWindow: desktop.hasWindow, attachedToManagedServer: Boolean(attached) },
|
|
35
|
+
desktopRestartRecommended: Boolean(desktop.running && !attached), checkedAt: new Date().toISOString() };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
errors.push(error.message);
|
|
38
|
+
this.state = { checkedAt: new Date().toISOString() };
|
|
39
|
+
}
|
|
40
|
+
if (errors.length) this.state.discoveryError = errors.join('\n');
|
|
41
|
+
return this.state;
|
|
42
|
+
}
|
|
43
|
+
async tick() {
|
|
44
|
+
await this.sample();
|
|
45
|
+
if (!this.stopped) { this.timer = setTimeout(() => this.tick(), this.interval); this.timer.unref?.(); }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
// Process one account per transaction; foreign keys keep incomplete deletion atomic.
|
|
2
|
-
export async function purgeCancelledAccounts(pool, scopeHash) {
|
|
3
|
-
let purged = 0;
|
|
4
|
-
for (;;) {
|
|
5
|
-
const db = await pool.getConnection();
|
|
6
|
-
try {
|
|
7
|
-
await db.beginTransaction();
|
|
8
|
-
const [rows] = await db.query(`SELECT id FROM users WHERE status='cancelled'
|
|
9
|
-
AND cancelled_at <= UTC_TIMESTAMP(3) - INTERVAL 1 DAY
|
|
10
|
-
ORDER BY cancelled_at LIMIT 1 FOR UPDATE`);
|
|
11
|
-
if (!rows.length) { await db.commit(); return purged; }
|
|
12
|
-
const id = rows[0].id;
|
|
13
|
-
const [identities] = await db.execute("SELECT normalized_value FROM user_login_identities WHERE user_id=? AND kind='email'", [id]);
|
|
14
|
-
for (const { normalized_value: email } of identities) {
|
|
15
|
-
await db.execute('DELETE FROM email_login_challenges WHERE email=?', [email]);
|
|
16
|
-
await db.execute('DELETE FROM email_send_limits WHERE scope_hash=?', [scopeHash(`email:${email}`)]);
|
|
17
|
-
await db.execute('DELETE FROM relay_admin_emails WHERE email=?', [email]);
|
|
18
|
-
}
|
|
19
|
-
for (const table of ['user_sessions', 'relay_usage_actions', 'relay_usage_sessions',
|
|
20
|
-
'relay_usage_user_days', 'relay_usage_budgets', 'relay_usage_entitlements',
|
|
21
|
-
'relay_usage_totals', 'user_login_identities']) {
|
|
22
|
-
await db.execute(`DELETE FROM ${table} WHERE user_id=?`, [id]);
|
|
23
|
-
}
|
|
24
|
-
await db.execute('UPDATE computers SET owner_user_id=NULL WHERE owner_user_id=?', [id]);
|
|
25
|
-
await db.execute('DELETE FROM users WHERE id=?', [id]);
|
|
26
|
-
await db.commit(); purged++;
|
|
27
|
-
} catch (error) { await db.rollback(); throw error; }
|
|
28
|
-
finally { db.release(); }
|
|
29
|
-
}
|
|
30
|
-
}
|
|
1
|
+
// Process one account per transaction; foreign keys keep incomplete deletion atomic.
|
|
2
|
+
export async function purgeCancelledAccounts(pool, scopeHash) {
|
|
3
|
+
let purged = 0;
|
|
4
|
+
for (;;) {
|
|
5
|
+
const db = await pool.getConnection();
|
|
6
|
+
try {
|
|
7
|
+
await db.beginTransaction();
|
|
8
|
+
const [rows] = await db.query(`SELECT id FROM users WHERE status='cancelled'
|
|
9
|
+
AND cancelled_at <= UTC_TIMESTAMP(3) - INTERVAL 1 DAY
|
|
10
|
+
ORDER BY cancelled_at LIMIT 1 FOR UPDATE`);
|
|
11
|
+
if (!rows.length) { await db.commit(); return purged; }
|
|
12
|
+
const id = rows[0].id;
|
|
13
|
+
const [identities] = await db.execute("SELECT normalized_value FROM user_login_identities WHERE user_id=? AND kind='email'", [id]);
|
|
14
|
+
for (const { normalized_value: email } of identities) {
|
|
15
|
+
await db.execute('DELETE FROM email_login_challenges WHERE email=?', [email]);
|
|
16
|
+
await db.execute('DELETE FROM email_send_limits WHERE scope_hash=?', [scopeHash(`email:${email}`)]);
|
|
17
|
+
await db.execute('DELETE FROM relay_admin_emails WHERE email=?', [email]);
|
|
18
|
+
}
|
|
19
|
+
for (const table of ['user_sessions', 'relay_usage_actions', 'relay_usage_sessions',
|
|
20
|
+
'relay_usage_user_days', 'relay_usage_budgets', 'relay_usage_entitlements',
|
|
21
|
+
'relay_usage_totals', 'user_login_identities']) {
|
|
22
|
+
await db.execute(`DELETE FROM ${table} WHERE user_id=?`, [id]);
|
|
23
|
+
}
|
|
24
|
+
await db.execute('UPDATE computers SET owner_user_id=NULL WHERE owner_user_id=?', [id]);
|
|
25
|
+
await db.execute('DELETE FROM users WHERE id=?', [id]);
|
|
26
|
+
await db.commit(); purged++;
|
|
27
|
+
} catch (error) { await db.rollback(); throw error; }
|
|
28
|
+
finally { db.release(); }
|
|
29
|
+
}
|
|
30
|
+
}
|