remote-codex 0.1.2 → 0.1.3
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/bin/remote-codex.mjs +7 -2
- package/package.json +1 -1
- 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
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -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();
|