remote-codex 0.1.2 → 0.1.4
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 +2 -2
- package/apps/supervisor-api/dist/index.js +12 -6
- package/bin/remote-codex.mjs +7 -2
- package/config/codex-model-pricing.json +57 -0
- package/package.json +3 -1
- package/packages/db/migrations/0000_initial.sql +74 -0
- package/packages/db/migrations/0001_thread_runtime_fields.sql +3 -0
- package/packages/db/migrations/0002_thread_source.sql +1 -0
- package/packages/db/migrations/0003_thread_runtime_settings.sql +2 -0
- package/packages/db/migrations/0004_thread_connection_state.sql +1 -0
- package/packages/db/migrations/0005_thread_turn_metadata.sql +13 -0
- package/packages/db/migrations/0006_thread_sandbox_mode.sql +1 -0
- package/packages/db/migrations/0007_thread_pending_steers.sql +13 -0
- package/packages/db/migrations/0008_thread_fast_mode_and_activity_notes.sql +19 -0
- package/packages/db/migrations/0009_thread_turn_token_usage.sql +2 -0
- package/packages/db/migrations/0010_thread_turn_pricing_snapshot.sql +5 -0
- package/packages/db/migrations/0011_thread_forks.sql +8 -0
- package/packages/db/migrations/0012_thread_goals.sql +17 -0
- package/packages/db/migrations/0013_thread_activity_note_anchor.sql +2 -0
- package/scripts/run-web-service.mjs +7 -2
- package/scripts/service-manager.mjs +59 -4
package/README.md
CHANGED
|
@@ -43,8 +43,8 @@ remote-codex stop
|
|
|
43
43
|
|
|
44
44
|
The global CLI starts the production API and web service:
|
|
45
45
|
|
|
46
|
-
- API: `http://127.0.0.1:
|
|
47
|
-
- Web: `http://127.0.0.1:
|
|
46
|
+
- API: `http://127.0.0.1:45674`
|
|
47
|
+
- Web: `http://127.0.0.1:45673`
|
|
48
48
|
- service logs/state: `~/.remote-codex/service/`
|
|
49
49
|
- production database: `~/.remote-codex/supervisor.sqlite`
|
|
50
50
|
|
|
@@ -6097,7 +6097,10 @@ function getDefaultHostRecord() {
|
|
|
6097
6097
|
// ../../packages/db/src/migrate.ts
|
|
6098
6098
|
import fs2 from "fs";
|
|
6099
6099
|
import path3 from "path";
|
|
6100
|
-
function
|
|
6100
|
+
function resolvePackageRoot(start = process.cwd()) {
|
|
6101
|
+
if (process.env.REMOTE_CODEX_PACKAGE_ROOT) {
|
|
6102
|
+
return path3.resolve(process.env.REMOTE_CODEX_PACKAGE_ROOT);
|
|
6103
|
+
}
|
|
6101
6104
|
let current = path3.resolve(start);
|
|
6102
6105
|
while (current !== path3.dirname(current)) {
|
|
6103
6106
|
if (fs2.existsSync(path3.join(current, "pnpm-workspace.yaml"))) {
|
|
@@ -6105,10 +6108,10 @@ function resolveRepoRoot(start = process.cwd()) {
|
|
|
6105
6108
|
}
|
|
6106
6109
|
current = path3.dirname(current);
|
|
6107
6110
|
}
|
|
6108
|
-
throw new Error("Unable to locate
|
|
6111
|
+
throw new Error("Unable to locate package root from current working directory.");
|
|
6109
6112
|
}
|
|
6110
6113
|
function getMigrationsDir() {
|
|
6111
|
-
return path3.join(
|
|
6114
|
+
return path3.join(resolvePackageRoot(), "packages", "db", "migrations");
|
|
6112
6115
|
}
|
|
6113
6116
|
function runMigrations(databaseUrl) {
|
|
6114
6117
|
const { sqlite } = createDatabase(databaseUrl);
|
|
@@ -8262,7 +8265,10 @@ import fs7 from "fs";
|
|
|
8262
8265
|
import path7 from "path";
|
|
8263
8266
|
var TOKEN_PRICE_DENOMINATOR = 1e6;
|
|
8264
8267
|
var cachedPricingConfig = null;
|
|
8265
|
-
function
|
|
8268
|
+
function resolvePackageRoot2(start = process.cwd()) {
|
|
8269
|
+
if (process.env.REMOTE_CODEX_PACKAGE_ROOT) {
|
|
8270
|
+
return path7.resolve(process.env.REMOTE_CODEX_PACKAGE_ROOT);
|
|
8271
|
+
}
|
|
8266
8272
|
let current = path7.resolve(start);
|
|
8267
8273
|
while (current !== path7.dirname(current)) {
|
|
8268
8274
|
if (fs7.existsSync(path7.join(current, "pnpm-workspace.yaml"))) {
|
|
@@ -8270,10 +8276,10 @@ function resolveRepoRoot2(start = process.cwd()) {
|
|
|
8270
8276
|
}
|
|
8271
8277
|
current = path7.dirname(current);
|
|
8272
8278
|
}
|
|
8273
|
-
throw new Error("Unable to locate
|
|
8279
|
+
throw new Error("Unable to locate package root for Codex pricing config.");
|
|
8274
8280
|
}
|
|
8275
8281
|
function getPricingConfigPath() {
|
|
8276
|
-
return path7.join(
|
|
8282
|
+
return path7.join(resolvePackageRoot2(), "config", "codex-model-pricing.json");
|
|
8277
8283
|
}
|
|
8278
8284
|
function isPositiveNumber(value) {
|
|
8279
8285
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
package/bin/remote-codex.mjs
CHANGED
|
@@ -9,6 +9,11 @@ const binDir = path.dirname(fileURLToPath(import.meta.url));
|
|
|
9
9
|
const packageRoot = path.resolve(binDir, '..');
|
|
10
10
|
const packageJsonPath = path.join(packageRoot, 'package.json');
|
|
11
11
|
const serviceManagerPath = path.join(packageRoot, 'scripts', 'service-manager.mjs');
|
|
12
|
+
const sourceCheckout =
|
|
13
|
+
fs.existsSync(path.join(packageRoot, 'pnpm-workspace.yaml')) &&
|
|
14
|
+
fs.existsSync(path.join(packageRoot, 'scripts', 'service-restart.mjs'));
|
|
15
|
+
const defaultServicePort = sourceCheckout ? 4173 : 45673;
|
|
16
|
+
const defaultApiPort = sourceCheckout ? 8787 : 45674;
|
|
12
17
|
|
|
13
18
|
const aliases = new Map([
|
|
14
19
|
['service:start', 'start'],
|
|
@@ -84,9 +89,9 @@ Usage:
|
|
|
84
89
|
|
|
85
90
|
Environment:
|
|
86
91
|
SERVICE_HOST Web listen host, default 127.0.0.1
|
|
87
|
-
SERVICE_PORT Web listen port, default
|
|
92
|
+
SERVICE_PORT Web listen port, default ${defaultServicePort}
|
|
88
93
|
SERVICE_API_HOST API listen host, default 127.0.0.1
|
|
89
|
-
SERVICE_API_PORT API listen port, default
|
|
94
|
+
SERVICE_API_PORT API listen port, default ${defaultApiPort}
|
|
90
95
|
REMOTE_CODEX_SERVICE_DIR Service state and log directory, default ~/.remote-codex/service
|
|
91
96
|
`);
|
|
92
97
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"currency": "USD",
|
|
3
|
+
"tiers": {
|
|
4
|
+
"standard": {
|
|
5
|
+
"multiplier": 1
|
|
6
|
+
},
|
|
7
|
+
"fast": {
|
|
8
|
+
"multiplier": 2
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"models": {
|
|
12
|
+
"gpt-5.5": {
|
|
13
|
+
"inputUsdPerMillion": 5,
|
|
14
|
+
"cachedInputUsdPerMillion": 0.5,
|
|
15
|
+
"outputUsdPerMillion": 30,
|
|
16
|
+
"supportsFastMode": true,
|
|
17
|
+
"fastMultiplier": 2.5,
|
|
18
|
+
"contextWindowTokens": 272000
|
|
19
|
+
},
|
|
20
|
+
"gpt-5.3-codex": {
|
|
21
|
+
"inputUsdPerMillion": 1.75,
|
|
22
|
+
"cachedInputUsdPerMillion": 0.175,
|
|
23
|
+
"outputUsdPerMillion": 14,
|
|
24
|
+
"supportsFastMode": true
|
|
25
|
+
},
|
|
26
|
+
"gpt-5.4": {
|
|
27
|
+
"inputUsdPerMillion": 2.5,
|
|
28
|
+
"cachedInputUsdPerMillion": 0.25,
|
|
29
|
+
"outputUsdPerMillion": 15,
|
|
30
|
+
"supportsFastMode": true
|
|
31
|
+
},
|
|
32
|
+
"gpt-5.2-codex": {
|
|
33
|
+
"inputUsdPerMillion": 1.75,
|
|
34
|
+
"cachedInputUsdPerMillion": 0.175,
|
|
35
|
+
"outputUsdPerMillion": 14,
|
|
36
|
+
"supportsFastMode": false
|
|
37
|
+
},
|
|
38
|
+
"gpt-5.1-codex-max": {
|
|
39
|
+
"inputUsdPerMillion": 1.25,
|
|
40
|
+
"cachedInputUsdPerMillion": 0.125,
|
|
41
|
+
"outputUsdPerMillion": 10,
|
|
42
|
+
"supportsFastMode": false
|
|
43
|
+
},
|
|
44
|
+
"gpt-5.2": {
|
|
45
|
+
"inputUsdPerMillion": 1.75,
|
|
46
|
+
"cachedInputUsdPerMillion": 0.175,
|
|
47
|
+
"outputUsdPerMillion": 14,
|
|
48
|
+
"supportsFastMode": false
|
|
49
|
+
},
|
|
50
|
+
"gpt-5.1-codex-mini": {
|
|
51
|
+
"inputUsdPerMillion": 0.25,
|
|
52
|
+
"cachedInputUsdPerMillion": 0.025,
|
|
53
|
+
"outputUsdPerMillion": 2,
|
|
54
|
+
"supportsFastMode": false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remote-codex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Local web supervisor for Codex workspaces and threads.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"scripts/run-web-service.mjs",
|
|
14
14
|
"apps/supervisor-api/dist/",
|
|
15
15
|
"apps/supervisor-web/dist/",
|
|
16
|
+
"config/codex-model-pricing.json",
|
|
17
|
+
"packages/db/migrations/*.sql",
|
|
16
18
|
"README.md"
|
|
17
19
|
],
|
|
18
20
|
"packageManager": "pnpm@10.11.1",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS hosts (
|
|
2
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
3
|
+
hostname TEXT NOT NULL,
|
|
4
|
+
platform TEXT NOT NULL,
|
|
5
|
+
tailscale_name TEXT,
|
|
6
|
+
created_at TEXT NOT NULL,
|
|
7
|
+
last_seen_at TEXT NOT NULL
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
11
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
12
|
+
host_id TEXT NOT NULL,
|
|
13
|
+
label TEXT NOT NULL,
|
|
14
|
+
abs_path TEXT NOT NULL UNIQUE,
|
|
15
|
+
is_favorite INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
created_at TEXT NOT NULL,
|
|
17
|
+
last_opened_at TEXT
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
21
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
22
|
+
workspace_id TEXT NOT NULL,
|
|
23
|
+
codex_thread_id TEXT,
|
|
24
|
+
title TEXT NOT NULL,
|
|
25
|
+
model TEXT,
|
|
26
|
+
approval_mode TEXT,
|
|
27
|
+
status TEXT,
|
|
28
|
+
created_at TEXT NOT NULL,
|
|
29
|
+
updated_at TEXT NOT NULL,
|
|
30
|
+
last_turn_started_at TEXT,
|
|
31
|
+
last_turn_completed_at TEXT,
|
|
32
|
+
last_viewed_at TEXT,
|
|
33
|
+
is_pinned INTEGER NOT NULL DEFAULT 0
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS shell_sessions (
|
|
37
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
38
|
+
workspace_id TEXT NOT NULL,
|
|
39
|
+
thread_id TEXT,
|
|
40
|
+
tmux_session_name TEXT,
|
|
41
|
+
cwd TEXT NOT NULL,
|
|
42
|
+
status TEXT,
|
|
43
|
+
created_at TEXT NOT NULL,
|
|
44
|
+
updated_at TEXT NOT NULL,
|
|
45
|
+
last_activity_at TEXT
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS viewer_sessions (
|
|
49
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
50
|
+
thread_id TEXT,
|
|
51
|
+
shell_id TEXT,
|
|
52
|
+
connected_at TEXT NOT NULL,
|
|
53
|
+
last_heartbeat_at TEXT,
|
|
54
|
+
active_tab TEXT
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
58
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
59
|
+
thread_id TEXT,
|
|
60
|
+
kind TEXT NOT NULL,
|
|
61
|
+
severity TEXT NOT NULL,
|
|
62
|
+
title TEXT NOT NULL,
|
|
63
|
+
body TEXT NOT NULL,
|
|
64
|
+
is_read INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
created_at TEXT NOT NULL
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS policies (
|
|
69
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
70
|
+
key TEXT NOT NULL UNIQUE,
|
|
71
|
+
value_json TEXT NOT NULL,
|
|
72
|
+
created_at TEXT NOT NULL,
|
|
73
|
+
updated_at TEXT NOT NULL
|
|
74
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE threads ADD COLUMN source TEXT NOT NULL DEFAULT 'supervisor';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE threads ADD COLUMN is_connected INTEGER NOT NULL DEFAULT 1;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS thread_turn_metadata (
|
|
2
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
3
|
+
thread_id TEXT NOT NULL,
|
|
4
|
+
turn_id TEXT NOT NULL,
|
|
5
|
+
model TEXT,
|
|
6
|
+
reasoning_effort TEXT,
|
|
7
|
+
reasoning_effort_available INTEGER,
|
|
8
|
+
created_at TEXT NOT NULL,
|
|
9
|
+
updated_at TEXT NOT NULL
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE UNIQUE INDEX IF NOT EXISTS thread_turn_metadata_thread_turn_idx
|
|
13
|
+
ON thread_turn_metadata(thread_id, turn_id);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE threads ADD COLUMN sandbox_mode TEXT;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS thread_pending_steers (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
thread_id TEXT NOT NULL,
|
|
4
|
+
turn_id TEXT NOT NULL,
|
|
5
|
+
client_request_id TEXT,
|
|
6
|
+
display_prompt TEXT NOT NULL,
|
|
7
|
+
submitted_prompt TEXT NOT NULL,
|
|
8
|
+
created_at TEXT NOT NULL,
|
|
9
|
+
updated_at TEXT NOT NULL
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS thread_pending_steers_thread_created_idx
|
|
13
|
+
ON thread_pending_steers(thread_id, created_at);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
ALTER TABLE threads
|
|
2
|
+
ADD COLUMN fast_mode INTEGER NOT NULL DEFAULT 0;
|
|
3
|
+
|
|
4
|
+
ALTER TABLE threads
|
|
5
|
+
ADD COLUMN fast_base_model TEXT;
|
|
6
|
+
|
|
7
|
+
ALTER TABLE threads
|
|
8
|
+
ADD COLUMN fast_base_reasoning_effort TEXT;
|
|
9
|
+
|
|
10
|
+
CREATE TABLE IF NOT EXISTS thread_activity_notes (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
thread_id TEXT NOT NULL,
|
|
13
|
+
kind TEXT NOT NULL,
|
|
14
|
+
text TEXT NOT NULL,
|
|
15
|
+
created_at TEXT NOT NULL
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE INDEX IF NOT EXISTS thread_activity_notes_thread_created_idx
|
|
19
|
+
ON thread_activity_notes(thread_id, created_at);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS thread_goals (
|
|
2
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
3
|
+
thread_id TEXT NOT NULL,
|
|
4
|
+
codex_thread_id TEXT NOT NULL,
|
|
5
|
+
objective TEXT NOT NULL,
|
|
6
|
+
status TEXT NOT NULL,
|
|
7
|
+
token_budget INTEGER,
|
|
8
|
+
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
time_used_seconds INTEGER NOT NULL DEFAULT 0,
|
|
10
|
+
started_at TEXT NOT NULL,
|
|
11
|
+
completed_at TEXT,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS thread_goals_thread_updated_idx
|
|
17
|
+
ON thread_goals(thread_id, updated_at);
|
|
@@ -7,11 +7,16 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
|
|
8
8
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const repoRoot = path.resolve(scriptDir, '..');
|
|
10
|
+
const sourceCheckout =
|
|
11
|
+
fs.existsSync(path.join(repoRoot, 'pnpm-workspace.yaml')) &&
|
|
12
|
+
fs.existsSync(path.join(repoRoot, 'scripts', 'service-restart.mjs'));
|
|
13
|
+
const defaultServicePort = sourceCheckout ? 4173 : 45673;
|
|
14
|
+
const defaultApiPort = sourceCheckout ? 8787 : 45674;
|
|
10
15
|
|
|
11
16
|
const serviceHost = process.env.SERVICE_HOST ?? '127.0.0.1';
|
|
12
|
-
const servicePort = parsePort(process.env.SERVICE_PORT,
|
|
17
|
+
const servicePort = parsePort(process.env.SERVICE_PORT, defaultServicePort);
|
|
13
18
|
const apiHost = process.env.SERVICE_API_HOST ?? '127.0.0.1';
|
|
14
|
-
const apiPort = parsePort(process.env.SERVICE_API_PORT,
|
|
19
|
+
const apiPort = parsePort(process.env.SERVICE_API_PORT, defaultApiPort);
|
|
15
20
|
const distDir = path.resolve(
|
|
16
21
|
process.env.SERVICE_WEB_DIST_DIR ?? path.join(repoRoot, 'apps/supervisor-web/dist')
|
|
17
22
|
);
|
|
@@ -4,6 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import net from 'node:net';
|
|
7
8
|
|
|
8
9
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const repoRoot = path.resolve(scriptDir, '..');
|
|
@@ -17,11 +18,13 @@ const webIndex = path.join(repoRoot, 'apps', 'supervisor-web', 'dist', 'index.ht
|
|
|
17
18
|
const supportsSourceRestart =
|
|
18
19
|
fs.existsSync(path.join(repoRoot, 'pnpm-workspace.yaml')) &&
|
|
19
20
|
fs.existsSync(path.join(repoRoot, 'scripts', 'service-restart.mjs'));
|
|
21
|
+
const defaultServicePort = supportsSourceRestart ? 4173 : 45673;
|
|
22
|
+
const defaultApiPort = supportsSourceRestart ? 8787 : 45674;
|
|
20
23
|
|
|
21
24
|
const serviceHost = process.env.SERVICE_HOST ?? '127.0.0.1';
|
|
22
|
-
const servicePort = parsePort(process.env.SERVICE_PORT,
|
|
25
|
+
const servicePort = parsePort(process.env.SERVICE_PORT, defaultServicePort);
|
|
23
26
|
const apiHost = process.env.SERVICE_API_HOST ?? '127.0.0.1';
|
|
24
|
-
const apiPort = parsePort(process.env.SERVICE_API_PORT,
|
|
27
|
+
const apiPort = parsePort(process.env.SERVICE_API_PORT, defaultApiPort);
|
|
25
28
|
|
|
26
29
|
const command = process.argv[2];
|
|
27
30
|
|
|
@@ -58,6 +61,10 @@ async function startService() {
|
|
|
58
61
|
const webLogPath = path.join(serviceDir, 'web.log');
|
|
59
62
|
prepareLogFile(apiLogPath);
|
|
60
63
|
prepareLogFile(webLogPath);
|
|
64
|
+
|
|
65
|
+
await assertTcpPortAvailable(apiHost, apiPort, 'API');
|
|
66
|
+
await assertTcpPortAvailable(serviceHost, servicePort, 'Web');
|
|
67
|
+
|
|
61
68
|
const apiPid = spawnDetached(process.execPath, [apiEntry], apiLogPath, {
|
|
62
69
|
NODE_ENV: 'production',
|
|
63
70
|
HOST: apiHost,
|
|
@@ -73,7 +80,7 @@ async function startService() {
|
|
|
73
80
|
await waitForHttp(`http://${apiHost}:${apiPort}/healthz`, apiPid, 15_000);
|
|
74
81
|
} catch (error) {
|
|
75
82
|
stopPid(apiPid);
|
|
76
|
-
throw error;
|
|
83
|
+
throw appendLogTail(error, apiLogPath, 'API');
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
const webPid = spawnDetached(process.execPath, [webEntry], webLogPath, {
|
|
@@ -89,7 +96,7 @@ async function startService() {
|
|
|
89
96
|
} catch (error) {
|
|
90
97
|
stopPid(webPid);
|
|
91
98
|
stopPid(apiPid);
|
|
92
|
-
throw error;
|
|
99
|
+
throw appendLogTail(error, webLogPath, 'Web');
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
const state = {
|
|
@@ -214,6 +221,54 @@ async function waitForHttp(url, pid, timeoutMs) {
|
|
|
214
221
|
throw new Error(`Timed out waiting for ${url}.`);
|
|
215
222
|
}
|
|
216
223
|
|
|
224
|
+
async function assertTcpPortAvailable(host, port, label) {
|
|
225
|
+
await new Promise((resolve, reject) => {
|
|
226
|
+
const server = net.createServer();
|
|
227
|
+
server.unref();
|
|
228
|
+
|
|
229
|
+
server.once('error', (error) => {
|
|
230
|
+
const code = typeof error === 'object' && error !== null ? error.code : undefined;
|
|
231
|
+
if (code === 'EADDRINUSE') {
|
|
232
|
+
reject(
|
|
233
|
+
new Error(
|
|
234
|
+
`${label} port ${host}:${port} is already in use. Set ${label === 'API' ? 'SERVICE_API_PORT' : 'SERVICE_PORT'} to another port, or stop the process currently using it.`
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
reject(error);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
server.listen(port, host, () => {
|
|
244
|
+
server.close(resolve);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function appendLogTail(error, logPath, label) {
|
|
250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
+
const tail = readLogTail(logPath, 80);
|
|
252
|
+
if (!tail) {
|
|
253
|
+
return new Error(`${message}\n${label} log: ${logPath}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return new Error(`${message}\n${label} log: ${logPath}\n\nLast ${label} log lines:\n${tail}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readLogTail(logPath, maxLines) {
|
|
260
|
+
try {
|
|
261
|
+
const content = fs.readFileSync(logPath, 'utf8').trimEnd();
|
|
262
|
+
if (!content) {
|
|
263
|
+
return '';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return content.split(/\r?\n/).slice(-maxLines).join('\n');
|
|
267
|
+
} catch {
|
|
268
|
+
return '';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
217
272
|
async function probeHttp(url) {
|
|
218
273
|
try {
|
|
219
274
|
const controller = new AbortController();
|