@nylorun/cli 0.2.0-beta → 0.3.0-beta
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/CHANGELOG.md +47 -0
- package/README.md +87 -40
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +132 -220
- package/dist/dev-entry.d.ts +5 -1
- package/dist/dev-entry.js +7 -6
- package/dist/dev.d.ts +45 -4
- package/dist/dev.js +245 -32
- package/dist/doctor.d.ts +25 -3
- package/dist/doctor.js +104 -16
- package/dist/environment.d.ts +4 -1
- package/dist/environment.js +12 -3
- package/dist/launcher.d.ts +5 -5
- package/dist/launcher.js +6 -3
- package/dist/model/configure.d.ts +30 -0
- package/dist/model/configure.js +117 -11
- package/dist/model/host-model.d.ts +5 -3
- package/dist/model/host-model.js +36 -15
- package/dist/project/attach.d.ts +32 -0
- package/dist/project/attach.js +241 -0
- package/dist/project/create-tenant.d.ts +26 -0
- package/dist/project/create-tenant.js +65 -0
- package/dist/project/credentials.d.ts +18 -0
- package/dist/project/credentials.js +64 -0
- package/dist/project/link.d.ts +20 -0
- package/dist/project/link.js +77 -0
- package/dist/project/root.d.ts +9 -0
- package/dist/project/root.js +51 -0
- package/dist/project/seed.d.ts +19 -0
- package/dist/project/seed.js +94 -0
- package/dist/runtime/baseline.d.ts +2 -0
- package/dist/runtime/baseline.js +11 -0
- package/dist/runtime/commands.d.ts +16 -0
- package/dist/runtime/commands.js +292 -0
- package/dist/runtime/ephemeral.d.ts +37 -0
- package/dist/runtime/ephemeral.js +84 -0
- package/dist/runtime/exit-codes.d.ts +9 -0
- package/dist/runtime/exit-codes.js +43 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.js +6 -0
- package/dist/runtime/launcher.d.ts +81 -0
- package/dist/runtime/launcher.js +243 -0
- package/dist/runtime/progress.d.ts +10 -0
- package/dist/runtime/progress.js +8 -0
- package/dist/runtime/version.d.ts +6 -0
- package/dist/runtime/version.js +27 -0
- package/dist/tenant/commands.d.ts +4 -0
- package/dist/tenant/commands.js +262 -0
- package/package.json +6 -3
- package/dist/project-runner.d.ts +0 -20
- package/dist/project-runner.js +0 -141
- package/dist/runtime-host.d.ts +0 -39
- package/dist/runtime-host.js +0 -355
- package/dist/scope.d.ts +0 -65
- package/dist/scope.js +0 -191
package/dist/runtime-host.js
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { createServer } from "node:net";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import { openSync, closeSync, statSync, renameSync, existsSync, readFileSync, } from "node:fs";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { readFile, open } from "node:fs/promises";
|
|
7
|
-
import { clearState, ensureDataDir, probeHealth, readState, writeState, } from "./scope.js";
|
|
8
|
-
import { CliError } from "./errors.js";
|
|
9
|
-
import { localCredentials } from "./project-runner.js";
|
|
10
|
-
const require = createRequire(import.meta.url);
|
|
11
|
-
const READY_TIMEOUT_MS = 20_000;
|
|
12
|
-
const LOG_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
13
|
-
/**
|
|
14
|
-
* Derived from the resolved server entry rather than a "./package.json" export, so the version
|
|
15
|
-
* is readable for every installed Runtime — including one too old to declare that export,
|
|
16
|
-
* which is exactly when skew detection matters.
|
|
17
|
-
*/
|
|
18
|
-
export const cliRuntimeVersion = () => {
|
|
19
|
-
try {
|
|
20
|
-
return require("@nylorun/runtime/package.json").version;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
/* fall through to the entry-relative read */
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
let directory = dirname(require.resolve("@nylorun/runtime/server"));
|
|
27
|
-
for (let depth = 0; depth < 5; depth += 1) {
|
|
28
|
-
const candidate = join(directory, "package.json");
|
|
29
|
-
if (existsSync(candidate)) {
|
|
30
|
-
const pkg = JSON.parse(readFileSync(candidate, "utf8"));
|
|
31
|
-
if (pkg.name === "@nylorun/runtime")
|
|
32
|
-
return pkg.version;
|
|
33
|
-
}
|
|
34
|
-
directory = dirname(directory);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
/* an unresolvable Runtime is reported elsewhere */
|
|
39
|
-
}
|
|
40
|
-
return undefined;
|
|
41
|
-
};
|
|
42
|
-
const alive = (pid) => {
|
|
43
|
-
try {
|
|
44
|
-
process.kill(pid, 0);
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
// EPERM means the pid exists but belongs to another user; only ESRCH proves it is gone.
|
|
49
|
-
return error.code !== "ESRCH";
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
/** A squatter that accepts connections without answering /health is still a port conflict. */
|
|
53
|
-
async function bindable(host, port) {
|
|
54
|
-
const server = createServer();
|
|
55
|
-
return await new Promise((resolve) => {
|
|
56
|
-
server.once("error", () => resolve(false));
|
|
57
|
-
server.listen(port, host, () => server.close(() => resolve(true)));
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
export async function hostStatus(scope) {
|
|
61
|
-
const runtime = await readState(scope);
|
|
62
|
-
const probe = await probeHealth(scope.url);
|
|
63
|
-
const ours = probe.health !== undefined &&
|
|
64
|
-
(runtime?.scopeId === undefined ||
|
|
65
|
-
probe.health.scopeId === undefined ||
|
|
66
|
-
probe.health.scopeId === runtime.scopeId);
|
|
67
|
-
const common = {
|
|
68
|
-
scope,
|
|
69
|
-
...(runtime ? { runtime } : {}),
|
|
70
|
-
...(probe.health?.version ? { version: probe.health.version } : {}),
|
|
71
|
-
...(probe.health?.scopeId ? { scopeId: probe.health.scopeId } : {}),
|
|
72
|
-
...(probe.health?.pid ? { pid: probe.health.pid } : {}),
|
|
73
|
-
};
|
|
74
|
-
if (probe.foreign)
|
|
75
|
-
return { state: "port-conflict", ...common };
|
|
76
|
-
if (probe.health && ours)
|
|
77
|
-
return { state: "running", ...common };
|
|
78
|
-
if (probe.health && !ours)
|
|
79
|
-
return { state: "port-conflict", ...common };
|
|
80
|
-
if (runtime && alive(runtime.pid))
|
|
81
|
-
return { state: "unhealthy", ...common };
|
|
82
|
-
if (!(await bindable(scope.host, scope.port)))
|
|
83
|
-
return { state: "port-conflict", ...common };
|
|
84
|
-
if (!runtime)
|
|
85
|
-
return { state: "stopped", ...common };
|
|
86
|
-
return { state: "stale", ...common };
|
|
87
|
-
}
|
|
88
|
-
function rotateLog(path) {
|
|
89
|
-
try {
|
|
90
|
-
if (statSync(path).size > LOG_LIMIT_BYTES)
|
|
91
|
-
renameSync(path, `${path}.1`);
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
/* no log yet */
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
async function logTail(path, lines = 40) {
|
|
98
|
-
try {
|
|
99
|
-
const text = await readFile(path, "utf8");
|
|
100
|
-
return text.trimEnd().split("\n").slice(-lines).join("\n");
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return "";
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
export async function startHost(scope, options = {}) {
|
|
107
|
-
await ensureDataDir(scope);
|
|
108
|
-
if (options.restart)
|
|
109
|
-
await stopHost(scope);
|
|
110
|
-
const status = await hostStatus(scope);
|
|
111
|
-
if (status.state === "running" && !options.foreground)
|
|
112
|
-
return status;
|
|
113
|
-
if (status.state === "port-conflict")
|
|
114
|
-
throw portConflict(scope, status);
|
|
115
|
-
if (status.state === "unhealthy")
|
|
116
|
-
throw new CliError(`A process for ${scope.label} is running (pid ${status.runtime?.pid}) but is not answering ${scope.url}. Inspect it with "nylorun logs", then stop it with "nylorun down".`, 1);
|
|
117
|
-
if (status.state === "stale")
|
|
118
|
-
await clearState(scope);
|
|
119
|
-
const credentials = await localCredentials(scope.credentialsPath);
|
|
120
|
-
const entry = require.resolve("@nylorun/runtime/server");
|
|
121
|
-
const environment = {
|
|
122
|
-
...process.env,
|
|
123
|
-
HOST: scope.host,
|
|
124
|
-
PORT: String(scope.port),
|
|
125
|
-
NYLORUN_PROJECT_PROVIDER: "1",
|
|
126
|
-
NYLORUN_SERVER_KEY: credentials.serverKey,
|
|
127
|
-
NYLORUN_SQLITE_PATH: scope.sqlitePath,
|
|
128
|
-
};
|
|
129
|
-
// Executor scopes are no longer handed to the process at startup: the host outlives any one
|
|
130
|
-
// project run and learns its executors through PUT /v1/executors.
|
|
131
|
-
delete environment.NYLORUN_EXECUTORS_JSON;
|
|
132
|
-
if (options.foreground) {
|
|
133
|
-
const child = spawn(process.execPath, [entry], {
|
|
134
|
-
env: environment,
|
|
135
|
-
stdio: ["ignore", "inherit", "inherit", "ipc"],
|
|
136
|
-
});
|
|
137
|
-
await awaitReady(child, scope, "");
|
|
138
|
-
const ready = await recordState(scope, child.pid);
|
|
139
|
-
const stop = (signal) => child.kill(signal);
|
|
140
|
-
process.on("SIGINT", () => stop("SIGINT"));
|
|
141
|
-
process.on("SIGTERM", () => stop("SIGTERM"));
|
|
142
|
-
const code = await new Promise((resolve) => child.once("exit", (value, signal) => resolve(value ?? (signal === "SIGINT" ? 130 : 143))));
|
|
143
|
-
await clearState(scope);
|
|
144
|
-
if (code)
|
|
145
|
-
throw new CliError(`The Runtime exited (${code}).`, code);
|
|
146
|
-
return ready;
|
|
147
|
-
}
|
|
148
|
-
rotateLog(scope.logPath);
|
|
149
|
-
const fd = openSync(scope.logPath, "a", 0o600);
|
|
150
|
-
try {
|
|
151
|
-
const child = spawn(process.execPath, [entry], {
|
|
152
|
-
detached: process.platform !== "win32",
|
|
153
|
-
windowsHide: true,
|
|
154
|
-
cwd: scope.dataDir,
|
|
155
|
-
env: environment,
|
|
156
|
-
stdio: ["ignore", fd, fd, "ipc"],
|
|
157
|
-
});
|
|
158
|
-
await awaitReady(child, scope, scope.logPath);
|
|
159
|
-
child.disconnect();
|
|
160
|
-
child.unref();
|
|
161
|
-
return await recordState(scope, child.pid);
|
|
162
|
-
}
|
|
163
|
-
finally {
|
|
164
|
-
closeSync(fd);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
function portConflict(scope, status) {
|
|
168
|
-
if (status.scopeId)
|
|
169
|
-
return new CliError(`Port ${scope.port} is already serving a different nylorun scope (${status.scopeId}). Start this one elsewhere with "nylorun up --port <n>", or stop the other from its project with "nylorun down".`, 4);
|
|
170
|
-
return new CliError(`Port ${scope.port} on ${scope.host} is in use by another process. Start ${scope.label} elsewhere with "nylorun up --port <n>".`, 4);
|
|
171
|
-
}
|
|
172
|
-
async function awaitReady(child, scope, logPath) {
|
|
173
|
-
// The IPC message is the fast path and the only way to see an early crash; /health is the
|
|
174
|
-
// authority, so a host that ever stops sending the message still starts correctly.
|
|
175
|
-
await new Promise((resolve, reject) => {
|
|
176
|
-
const timer = setTimeout(() => {
|
|
177
|
-
cleanup();
|
|
178
|
-
reject(new CliError(`The Runtime for ${scope.label} did not start within 20 seconds. See "nylorun logs".`, 7));
|
|
179
|
-
}, READY_TIMEOUT_MS);
|
|
180
|
-
const onMessage = (message) => {
|
|
181
|
-
if (message?.type !== "ready")
|
|
182
|
-
return;
|
|
183
|
-
cleanup();
|
|
184
|
-
resolve();
|
|
185
|
-
};
|
|
186
|
-
const onExit = async (code) => {
|
|
187
|
-
cleanup();
|
|
188
|
-
const tail = logPath ? await logTail(logPath) : "";
|
|
189
|
-
reject(new CliError(`The Runtime for ${scope.label} exited (${code}).${tail ? `\n${tail}` : ""}`, 7));
|
|
190
|
-
};
|
|
191
|
-
const onError = (error) => {
|
|
192
|
-
cleanup();
|
|
193
|
-
reject(error);
|
|
194
|
-
};
|
|
195
|
-
const cleanup = () => {
|
|
196
|
-
clearTimeout(timer);
|
|
197
|
-
child.off("message", onMessage);
|
|
198
|
-
child.off("exit", onExit);
|
|
199
|
-
child.off("error", onError);
|
|
200
|
-
};
|
|
201
|
-
child.on("message", onMessage);
|
|
202
|
-
child.once("exit", onExit);
|
|
203
|
-
child.once("error", onError);
|
|
204
|
-
});
|
|
205
|
-
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
206
|
-
for (;;) {
|
|
207
|
-
const probe = await probeHealth(scope.url);
|
|
208
|
-
if (probe.health)
|
|
209
|
-
return;
|
|
210
|
-
if (Date.now() > deadline)
|
|
211
|
-
throw new CliError(`The Runtime for ${scope.label} started but did not answer ${scope.url}. See "nylorun logs".`, 7);
|
|
212
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
async function recordState(scope, pid) {
|
|
216
|
-
const probe = await probeHealth(scope.url);
|
|
217
|
-
const state = {
|
|
218
|
-
pid,
|
|
219
|
-
host: scope.host,
|
|
220
|
-
port: scope.port,
|
|
221
|
-
url: scope.url,
|
|
222
|
-
sqlitePath: scope.sqlitePath,
|
|
223
|
-
logPath: scope.logPath,
|
|
224
|
-
startedAt: new Date().toISOString(),
|
|
225
|
-
...(probe.health?.scopeId ? { scopeId: probe.health.scopeId } : {}),
|
|
226
|
-
...(probe.health?.version ? { runtimeVersion: probe.health.version } : {}),
|
|
227
|
-
...(cliRuntimeVersion() ? { cliVersion: cliRuntimeVersion() } : {}),
|
|
228
|
-
};
|
|
229
|
-
await writeState(scope, state);
|
|
230
|
-
return {
|
|
231
|
-
state: "running",
|
|
232
|
-
scope,
|
|
233
|
-
runtime: state,
|
|
234
|
-
pid,
|
|
235
|
-
...(probe.health?.version ? { version: probe.health.version } : {}),
|
|
236
|
-
...(probe.health?.scopeId ? { scopeId: probe.health.scopeId } : {}),
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
export async function stopHost(scope, options = {}) {
|
|
240
|
-
const status = await hostStatus(scope);
|
|
241
|
-
const pid = status.runtime?.pid ?? status.pid;
|
|
242
|
-
if (status.state === "stopped" || status.state === "stale" || !pid) {
|
|
243
|
-
await clearState(scope);
|
|
244
|
-
return "not-running";
|
|
245
|
-
}
|
|
246
|
-
if (status.state === "port-conflict")
|
|
247
|
-
throw portConflict(scope, status);
|
|
248
|
-
// SIGTERM the process rather than the group: the Runtime's own shutdown is what closes
|
|
249
|
-
// SQLite cleanly and tears down MCP children.
|
|
250
|
-
try {
|
|
251
|
-
process.kill(pid, "SIGTERM");
|
|
252
|
-
}
|
|
253
|
-
catch (error) {
|
|
254
|
-
if (error.code !== "ESRCH")
|
|
255
|
-
throw error;
|
|
256
|
-
}
|
|
257
|
-
const deadline = Date.now() + (options.timeoutMs ?? 10_000);
|
|
258
|
-
while (Date.now() < deadline) {
|
|
259
|
-
if (!alive(pid)) {
|
|
260
|
-
await clearState(scope);
|
|
261
|
-
return "stopped";
|
|
262
|
-
}
|
|
263
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
264
|
-
}
|
|
265
|
-
try {
|
|
266
|
-
if (process.platform === "win32")
|
|
267
|
-
process.kill(pid);
|
|
268
|
-
else
|
|
269
|
-
process.kill(-pid, "SIGKILL");
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
try {
|
|
273
|
-
process.kill(pid, "SIGKILL");
|
|
274
|
-
}
|
|
275
|
-
catch {
|
|
276
|
-
/* already gone */
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
await clearState(scope);
|
|
280
|
-
return "stopped";
|
|
281
|
-
}
|
|
282
|
-
export async function ensureRuntime(scope, options) {
|
|
283
|
-
const status = await hostStatus(scope);
|
|
284
|
-
if (status.state === "running") {
|
|
285
|
-
assertCompatible(scope, status);
|
|
286
|
-
return status;
|
|
287
|
-
}
|
|
288
|
-
if (status.state === "port-conflict")
|
|
289
|
-
throw portConflict(scope, status);
|
|
290
|
-
if (status.state === "unhealthy")
|
|
291
|
-
throw new CliError(`A process for ${scope.label} is running (pid ${status.runtime?.pid}) but is not answering ${scope.url}. See "nylorun logs".`, 1);
|
|
292
|
-
if (!options.autostart)
|
|
293
|
-
throw new CliError(`No Runtime is listening at ${scope.url} for ${scope.label}. Start one with "nylorun up", or drop --no-autostart.`, 6);
|
|
294
|
-
const started = await startHost(scope);
|
|
295
|
-
if (!options.quiet)
|
|
296
|
-
console.log(`Started the Runtime for ${scope.label} at ${scope.url} (pid ${started.pid}). It stays running; stop it with "nylorun down".`);
|
|
297
|
-
assertCompatible(scope, started);
|
|
298
|
-
return started;
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* A daemon from an older install would reject executor registration with a bare 404, so the
|
|
302
|
-
* commands that depend on that route fail early with the remedy instead.
|
|
303
|
-
*/
|
|
304
|
-
export function assertCompatible(scope, status) {
|
|
305
|
-
const expected = cliRuntimeVersion();
|
|
306
|
-
if (!expected)
|
|
307
|
-
return;
|
|
308
|
-
if (!status.version)
|
|
309
|
-
throw new CliError(`The Runtime serving ${scope.url} predates executor registration. Restart it with "nylorun runtime start --restart".`, 5);
|
|
310
|
-
if (status.version !== expected)
|
|
311
|
-
throw new CliError(`Runtime ${status.version} is serving ${scope.label}; this CLI ships ${expected}. Restart it with "nylorun runtime start --restart".`, 5);
|
|
312
|
-
}
|
|
313
|
-
export function warnIncompatible(scope, status) {
|
|
314
|
-
try {
|
|
315
|
-
assertCompatible(scope, status);
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
console.warn(error instanceof Error ? error.message : String(error));
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
export async function tailLog(scope, options) {
|
|
322
|
-
let size = 0;
|
|
323
|
-
try {
|
|
324
|
-
size = statSync(scope.logPath).size;
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
throw new CliError(`No Runtime log for ${scope.label}. Start one with "nylorun up".`, 3);
|
|
328
|
-
}
|
|
329
|
-
const text = await readFile(scope.logPath, "utf8");
|
|
330
|
-
process.stdout.write(`${text.trimEnd().split("\n").slice(-options.lines).join("\n")}\n`);
|
|
331
|
-
if (!options.follow)
|
|
332
|
-
return;
|
|
333
|
-
await new Promise((resolve) => {
|
|
334
|
-
process.once("SIGINT", () => resolve());
|
|
335
|
-
process.once("SIGTERM", () => resolve());
|
|
336
|
-
const poll = setInterval(async () => {
|
|
337
|
-
let next = 0;
|
|
338
|
-
try {
|
|
339
|
-
next = statSync(scope.logPath).size;
|
|
340
|
-
}
|
|
341
|
-
catch {
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
if (next <= size)
|
|
345
|
-
return;
|
|
346
|
-
const handle = await open(scope.logPath, "r");
|
|
347
|
-
const buffer = Buffer.alloc(next - size);
|
|
348
|
-
await handle.read(buffer, 0, buffer.length, size);
|
|
349
|
-
await handle.close();
|
|
350
|
-
size = next;
|
|
351
|
-
process.stdout.write(buffer.toString("utf8"));
|
|
352
|
-
}, 500);
|
|
353
|
-
poll.unref();
|
|
354
|
-
});
|
|
355
|
-
}
|
package/dist/scope.d.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
export type ScopeKind = "project" | "global";
|
|
2
|
-
export interface Scope {
|
|
3
|
-
readonly kind: ScopeKind;
|
|
4
|
-
readonly root: string;
|
|
5
|
-
readonly dataDir: string;
|
|
6
|
-
readonly sqlitePath: string;
|
|
7
|
-
readonly credentialsPath: string;
|
|
8
|
-
readonly statePath: string;
|
|
9
|
-
readonly logPath: string;
|
|
10
|
-
readonly pidPath: string;
|
|
11
|
-
readonly host: string;
|
|
12
|
-
readonly port: number;
|
|
13
|
-
readonly url: string;
|
|
14
|
-
readonly label: string;
|
|
15
|
-
}
|
|
16
|
-
export interface RuntimeState {
|
|
17
|
-
pid: number;
|
|
18
|
-
host: string;
|
|
19
|
-
port: number;
|
|
20
|
-
url: string;
|
|
21
|
-
scopeId?: string;
|
|
22
|
-
runtimeVersion?: string;
|
|
23
|
-
cliVersion?: string;
|
|
24
|
-
sqlitePath: string;
|
|
25
|
-
logPath: string;
|
|
26
|
-
startedAt: string;
|
|
27
|
-
}
|
|
28
|
-
export interface ScopeRequest {
|
|
29
|
-
global?: boolean;
|
|
30
|
-
port?: number;
|
|
31
|
-
db?: string;
|
|
32
|
-
cwd?: string;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* The nearest `.nylorun` wins so an initialised project keeps working from a subdirectory;
|
|
36
|
-
* otherwise the nearest package.json defines the project. The walk stops at the home
|
|
37
|
-
* directory because `~/.nylorun` is the global data directory, never a project root.
|
|
38
|
-
*/
|
|
39
|
-
export declare function findProjectRoot(cwd?: string): string | undefined;
|
|
40
|
-
export declare function resolveScope(request?: ScopeRequest): Scope;
|
|
41
|
-
export declare function ensureDataDir(scope: Scope): Promise<void>;
|
|
42
|
-
export declare function readState(scope: Scope): Promise<RuntimeState | undefined>;
|
|
43
|
-
export declare function writeState(scope: Scope, state: RuntimeState): Promise<void>;
|
|
44
|
-
export declare function clearState(scope: Scope): Promise<void>;
|
|
45
|
-
/**
|
|
46
|
-
* The health fields this CLI relies on. Declared here rather than imported, because the CLI
|
|
47
|
-
* may only depend on the agents SDK and the Runtime; version and scopeId are optional so a
|
|
48
|
-
* Runtime that predates them still parses into a usable probe.
|
|
49
|
-
*/
|
|
50
|
-
export interface HealthResponse {
|
|
51
|
-
status: string;
|
|
52
|
-
service: string;
|
|
53
|
-
version?: string;
|
|
54
|
-
scopeId?: string;
|
|
55
|
-
pid?: number;
|
|
56
|
-
}
|
|
57
|
-
export interface HealthProbe {
|
|
58
|
-
reachable: boolean;
|
|
59
|
-
health?: HealthResponse;
|
|
60
|
-
/** True when something answered the port but it is not a Runtime we can parse. */
|
|
61
|
-
foreign?: boolean;
|
|
62
|
-
}
|
|
63
|
-
export declare function probeHealth(url: string, timeoutMs?: number): Promise<HealthProbe>;
|
|
64
|
-
export declare const scopeId: (sqlitePath: string) => string | undefined;
|
|
65
|
-
export declare function loadScopeEnvironment(scope: Scope): void;
|
package/dist/scope.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, writeFile, rename, unlink } from "node:fs/promises";
|
|
3
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { dirname, join, parse, resolve } from "node:path";
|
|
6
|
-
import { loadProjectEnvironment } from "./environment.js";
|
|
7
|
-
/** An environment variable set to the empty string means "unset", not "use an empty path". */
|
|
8
|
-
const setting = (name) => {
|
|
9
|
-
const value = process.env[name];
|
|
10
|
-
return value === undefined || value.trim() === "" ? undefined : value;
|
|
11
|
-
};
|
|
12
|
-
const globalDataDir = () => {
|
|
13
|
-
const home = setting("NYLORUN_HOME");
|
|
14
|
-
return home ? resolve(home) : join(homedir(), ".nylorun");
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* The nearest `.nylorun` wins so an initialised project keeps working from a subdirectory;
|
|
18
|
-
* otherwise the nearest package.json defines the project. The walk stops at the home
|
|
19
|
-
* directory because `~/.nylorun` is the global data directory, never a project root.
|
|
20
|
-
*/
|
|
21
|
-
export function findProjectRoot(cwd = process.cwd()) {
|
|
22
|
-
const stop = (() => {
|
|
23
|
-
try {
|
|
24
|
-
return realpathSync(homedir());
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return homedir();
|
|
28
|
-
}
|
|
29
|
-
})();
|
|
30
|
-
let directory = resolve(cwd);
|
|
31
|
-
try {
|
|
32
|
-
directory = realpathSync(directory);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
/* a cwd that cannot be resolved simply falls through to the global scope */
|
|
36
|
-
}
|
|
37
|
-
const root = parse(directory).root;
|
|
38
|
-
let fallback;
|
|
39
|
-
for (;;) {
|
|
40
|
-
if (directory === stop)
|
|
41
|
-
return undefined;
|
|
42
|
-
if (existsSync(join(directory, ".nylorun")))
|
|
43
|
-
return directory;
|
|
44
|
-
if (!fallback && existsSync(join(directory, "package.json")))
|
|
45
|
-
fallback = directory;
|
|
46
|
-
if (directory === root)
|
|
47
|
-
break;
|
|
48
|
-
const parent = dirname(directory);
|
|
49
|
-
if (parent === directory)
|
|
50
|
-
break;
|
|
51
|
-
directory = parent;
|
|
52
|
-
}
|
|
53
|
-
return fallback;
|
|
54
|
-
}
|
|
55
|
-
function port(request, state) {
|
|
56
|
-
const candidate = request.port ??
|
|
57
|
-
numeric(setting("NYLORUN_PORT")) ??
|
|
58
|
-
numeric(setting("PORT")) ??
|
|
59
|
-
state?.port ??
|
|
60
|
-
8787;
|
|
61
|
-
if (!Number.isInteger(candidate) || candidate < 1 || candidate > 65535)
|
|
62
|
-
throw new Error("The Runtime port must be an integer between 1 and 65535.");
|
|
63
|
-
return candidate;
|
|
64
|
-
}
|
|
65
|
-
const numeric = (value) => {
|
|
66
|
-
if (value === undefined || value.trim() === "")
|
|
67
|
-
return undefined;
|
|
68
|
-
const parsed = Number(value);
|
|
69
|
-
return Number.isInteger(parsed) ? parsed : Number.NaN;
|
|
70
|
-
};
|
|
71
|
-
export function resolveScope(request = {}) {
|
|
72
|
-
const projectRoot = request.global ? undefined : findProjectRoot(request.cwd);
|
|
73
|
-
const kind = projectRoot ? "project" : "global";
|
|
74
|
-
const root = projectRoot ?? globalDataDir();
|
|
75
|
-
const dataDir = projectRoot ? join(projectRoot, ".nylorun") : globalDataDir();
|
|
76
|
-
const statePath = join(dataDir, "runtime.json");
|
|
77
|
-
const state = readStateSync(statePath);
|
|
78
|
-
const resolvedPort = port(request, state);
|
|
79
|
-
const host = setting("HOST") ?? "127.0.0.1";
|
|
80
|
-
return {
|
|
81
|
-
kind,
|
|
82
|
-
root,
|
|
83
|
-
dataDir,
|
|
84
|
-
sqlitePath: request.db ?? setting("NYLORUN_SQLITE_PATH") ?? join(dataDir, "runtime.sqlite"),
|
|
85
|
-
credentialsPath: join(dataDir, "local-credentials.json"),
|
|
86
|
-
statePath,
|
|
87
|
-
logPath: join(dataDir, "runtime.log"),
|
|
88
|
-
pidPath: join(dataDir, "runtime.pid"),
|
|
89
|
-
host,
|
|
90
|
-
port: resolvedPort,
|
|
91
|
-
url: `http://${host}:${resolvedPort}`,
|
|
92
|
-
label: projectRoot ? `project ${projectRoot}` : `global ${dataDir}`,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
export async function ensureDataDir(scope) {
|
|
96
|
-
await mkdir(scope.dataDir, { recursive: true, mode: 0o700 });
|
|
97
|
-
if (scope.kind !== "project")
|
|
98
|
-
return;
|
|
99
|
-
// A project that already keeps a .gitignore should not start committing local credentials;
|
|
100
|
-
// creating one where the author chose not to have it would be a surprise, so we never do.
|
|
101
|
-
const path = join(scope.root, ".gitignore");
|
|
102
|
-
let current;
|
|
103
|
-
try {
|
|
104
|
-
current = await readFile(path, "utf8");
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
if (error.code === "ENOENT")
|
|
108
|
-
return;
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
if (/^\.nylorun\/?$/m.test(current))
|
|
112
|
-
return;
|
|
113
|
-
await writeFile(path, `${current}${current.endsWith("\n") || !current ? "" : "\n"}.nylorun/\n`);
|
|
114
|
-
}
|
|
115
|
-
function readStateSync(path) {
|
|
116
|
-
try {
|
|
117
|
-
// Only the port is read during scope resolution, so a torn or hand-edited file is ignored.
|
|
118
|
-
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
119
|
-
return typeof value?.port === "number" ? value : undefined;
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return undefined;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
export async function readState(scope) {
|
|
126
|
-
try {
|
|
127
|
-
const value = JSON.parse(await readFile(scope.statePath, "utf8"));
|
|
128
|
-
return typeof value?.pid === "number" ? value : undefined;
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
export async function writeState(scope, state) {
|
|
135
|
-
const temporary = `${scope.statePath}.${randomUUID()}.tmp`;
|
|
136
|
-
await writeFile(temporary, JSON.stringify(state), { mode: 0o600 });
|
|
137
|
-
await rename(temporary, scope.statePath);
|
|
138
|
-
await writeFile(scope.pidPath, `${state.pid}\n`, { mode: 0o600 });
|
|
139
|
-
}
|
|
140
|
-
export async function clearState(scope) {
|
|
141
|
-
for (const path of [scope.statePath, scope.pidPath])
|
|
142
|
-
await unlink(path).catch(() => { });
|
|
143
|
-
}
|
|
144
|
-
const asHealth = (value) => {
|
|
145
|
-
const body = value;
|
|
146
|
-
if (!body || body.status !== "ok" || typeof body.service !== "string")
|
|
147
|
-
return undefined;
|
|
148
|
-
return {
|
|
149
|
-
status: body.status,
|
|
150
|
-
service: body.service,
|
|
151
|
-
...(typeof body.version === "string" ? { version: body.version } : {}),
|
|
152
|
-
...(typeof body.scopeId === "string" ? { scopeId: body.scopeId } : {}),
|
|
153
|
-
...(typeof body.pid === "number" ? { pid: body.pid } : {}),
|
|
154
|
-
};
|
|
155
|
-
};
|
|
156
|
-
export async function probeHealth(url, timeoutMs = 2000) {
|
|
157
|
-
const signal = AbortSignal.timeout(timeoutMs);
|
|
158
|
-
let response;
|
|
159
|
-
try {
|
|
160
|
-
response = await fetch(`${url}/health`, { signal });
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
return { reachable: false };
|
|
164
|
-
}
|
|
165
|
-
if (!response.ok)
|
|
166
|
-
return { reachable: true, foreign: true };
|
|
167
|
-
try {
|
|
168
|
-
const health = asHealth(await response.json());
|
|
169
|
-
if (health?.service !== "oss-runtime")
|
|
170
|
-
return { reachable: true, foreign: true };
|
|
171
|
-
return { reachable: true, health };
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
return { reachable: true, foreign: true };
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
export const scopeId = (sqlitePath) => {
|
|
178
|
-
try {
|
|
179
|
-
return createHash("sha256")
|
|
180
|
-
.update(realpathSync(sqlitePath))
|
|
181
|
-
.digest("hex")
|
|
182
|
-
.slice(0, 16);
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
return undefined;
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
export function loadScopeEnvironment(scope) {
|
|
189
|
-
if (scope.kind === "project")
|
|
190
|
-
loadProjectEnvironment(scope.root);
|
|
191
|
-
}
|