@nylorun/cli 0.1.1-beta → 0.2.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 CHANGED
@@ -1,5 +1,54 @@
1
1
  # @nylorun/cli
2
2
 
3
+ ## 0.2.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - b8d822a: Remove `nylorun start`. The compiled agent process is now `nylorun serve [entry]`, with the
8
+ same `dist/agents/index.js` default, and generated projects must change `scripts.start` to
9
+ `nylorun serve`. The local Runtime becomes a persistent process of its own under
10
+ `nylorun runtime start|stop|status|logs`, with `nylorun up` and `nylorun down` as aliases.
11
+ `dev` and `serve` attach to that Runtime, start one when nothing is listening, and leave it
12
+ running, so a watch restart re-registers agents and reconnects executors instead of
13
+ destroying in-flight sessions; `--no-autostart` fails instead and is what continuous
14
+ integration should use. Scope is per project by default in `.nylorun/`, created on first use,
15
+ with `--global` and `NYLORUN_HOME` for a shared Runtime in the home directory. Commands
16
+ report a resolved scope in every message, accept `--port` and `--db` alongside the existing
17
+ environment variables, expose `nylorun runtime status --output json`, and use documented exit
18
+ codes for usage errors, port conflicts, version skew, refused autostart and failed starts.
19
+ - b8d822a: Add `sandbox()`: one `.use(sandbox())` gives an agent Runtime-executed `bash`, `read`, `write`, `edit`, `grep` and `glob` tools on an isolated machine with a persistent `/workspace`. The Runtime selects a microsandbox microVM where available, otherwise an in-process virtual shell, enforces deny-by-default egress presets, owns sandbox lifecycle, and reports its choice through `GET /v1/host/sandbox`, the `nylorun dev` banner and `nylorun doctor sandbox`.
20
+
21
+ ### Patch Changes
22
+
23
+ - Pin agents to the tested release.
24
+ - Pin runtime to the tested release.
25
+ - Updated dependencies [b8d822a]
26
+ - Updated dependencies [b8d822a]
27
+ - Updated dependencies [b8d822a]
28
+ - Updated dependencies [b8d822a]
29
+ - Updated dependencies [b8d822a]
30
+ - Updated dependencies [b8d822a]
31
+ - Updated dependencies [b8d822a]
32
+ - Updated dependencies
33
+ - Updated dependencies
34
+ - Updated dependencies
35
+ - @nylorun/agents@0.4.0-beta
36
+ - @nylorun/runtime@0.8.0-beta
37
+
38
+ ## 0.1.2-beta
39
+
40
+ ### Patch Changes
41
+
42
+ - 3a88f51: Ship Agent-Plugins (`plugin()` / `loadPlugin`), Skills (`load_skill` / skill resources), Runtime MCP pool + vault credentials, and manifest v3 capability fields. Validate completed tool `output` against the tool output schema so ordinary tools with `outputSchema` no longer false-fail as `tool.invalid-output`.
43
+ - Pin agents to the tested release.
44
+ - Pin runtime to the tested release.
45
+ - Updated dependencies [3a88f51]
46
+ - Updated dependencies
47
+ - Updated dependencies
48
+ - Updated dependencies
49
+ - @nylorun/agents@0.3.0-beta
50
+ - @nylorun/runtime@0.7.0-beta
51
+
3
52
  ## 0.1.1-beta
4
53
 
5
54
  ### Patch Changes
package/README.md CHANGED
@@ -3,21 +3,56 @@
3
3
  The local `nylorun` executable. Requires Node 24+.
4
4
 
5
5
  ```sh
6
- nylorun configure
7
- nylorun dev
6
+ nylorun up # start the local Runtime (alias for "nylorun runtime start")
7
+ nylorun dev # watch agents/index.ts, open Studio
8
8
  nylorun dev --no-studio
9
- nylorun start
10
- nylorun studio --runtime-url http://127.0.0.1:8787
9
+ nylorun serve [entry] # run the compiled build, default dist/agents/index.js
10
+ nylorun configure
11
+ nylorun studio
12
+ nylorun runtime status
13
+ nylorun down # stop the Runtime, keeping SQLite and credentials
11
14
  ```
12
15
 
13
- The CLI launches `@nylorun/runtime/server` in a child process, registers the
14
- project's exported agents with the SDK, connects scoped customer executors and
15
- optionally opens a project-installed `@nylorun/studio`. Runtime and executor
16
- credentials remain separate. Configuration lives in the existing project files;
17
- the SQLite database defaults to `.nylorun/runtime.sqlite`.
16
+ ## The Runtime is a separate, persistent process
17
+
18
+ `nylorun up` starts `@nylorun/runtime/server` in the background and returns the
19
+ terminal. `dev` and `serve` attach to it, registering the project's exported
20
+ agents and connecting scoped executors through `PUT /v1/executors`. They start
21
+ the Runtime themselves when nothing is listening, and they leave it running on
22
+ exit, so a watch restart re-registers agents without losing sessions. Pass
23
+ `--no-autostart` to fail instead, which is what CI should do. `--foreground`
24
+ keeps `runtime start` attached for logs.
25
+
26
+ ## Scope
27
+
28
+ A project is the default scope: the Runtime's SQLite, credentials, log and pid
29
+ live in `.nylorun/` beside the nearest `package.json`, created on first use and
30
+ added to an existing `.gitignore`. `--global` (and running outside a project)
31
+ uses `~/.nylorun` instead, relocatable with `NYLORUN_HOME`. Both default to
32
+ `127.0.0.1:8787`; `--port` beats `NYLORUN_PORT`, then `PORT`, then the port
33
+ recorded for a running Runtime. `--db` or `NYLORUN_SQLITE_PATH` moves the
34
+ database. `nylorun runtime status --output json` prints the resolved scope for
35
+ scripts, and `nylorun logs [-f]` tails the Runtime's own output.
36
+
37
+ Runtime and executor credentials remain separate. On first `configure`, `dev` or
38
+ `serve`, an interactive terminal saves the model provider credential into the
39
+ Runtime vault; `configure` replaces it while the Runtime is already running.
40
+
41
+ ## Exit codes
42
+
43
+ | Code | Meaning |
44
+ | --- | --- |
45
+ | 0 | Success, including `down` when nothing was running |
46
+ | 1 | Generic failure |
47
+ | 2 | Usage error |
48
+ | 3 | `runtime status` or `logs`: not running |
49
+ | 4 | Port held by another process or another scope |
50
+ | 5 | The running Runtime does not match this CLI |
51
+ | 6 | `--no-autostart` and nothing is listening |
52
+ | 7 | The Runtime did not become healthy |
53
+ | 130 / 143 | SIGINT / SIGTERM |
18
54
 
19
55
  `dev` loads `agents/index.ts` using project-installed `tsx` and watches changes.
20
- `start [entry]` defaults to `dist/agents/index.js` without Studio or watching.
21
56
  Studio and `tsx` are optional development dependencies; the CLI is a production
22
57
  dependency when the application's `start` script uses it.
23
58
 
package/dist/cli.js CHANGED
@@ -3,14 +3,83 @@ import { join } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { createRequire } from "node:module";
5
5
  import { ConfigurationCancelled, configureProvider, } from "./model/configure.js";
6
- import { loadProjectEnvironment } from "./environment.js";
7
- import { develop } from "./dev.js";
8
- const usage = `nylorun <configure|dev|start|studio>
9
- dev [--no-studio] [--no-open]
10
- start [entry]
11
- configure
12
- studio --runtime-url <http(s)-url> [--port <n>] [--no-open]`;
13
- async function startStudio(agentServerUrl, open, port) {
6
+ import { putHostModel } from "./model/host-model.js";
7
+ import { develop, developmentPreflight } from "./dev.js";
8
+ import { CliError } from "./errors.js";
9
+ import { loadScopeEnvironment, resolveScope, } from "./scope.js";
10
+ import { cliRuntimeVersion, ensureRuntime, hostStatus, startHost, stopHost, tailLog, warnIncompatible, } from "./runtime-host.js";
11
+ import { localCredentials } from "./project-runner.js";
12
+ const usage = `nylorun <runtime|up|down|logs|dev|serve|configure|studio|doctor>
13
+
14
+ runtime start [--foreground] [--restart] [--port <n>] [--db <path>] [--global]
15
+ runtime stop [--global] [--timeout <seconds>]
16
+ runtime status [--output json] [--global]
17
+ runtime logs [-f] [-n <lines>] [--global]
18
+ up alias for "runtime start"
19
+ down alias for "runtime stop"
20
+ logs alias for "runtime logs"
21
+
22
+ dev [--no-studio] [--no-open] [--no-autostart] [--port <n>]
23
+ serve [entry] [--no-autostart] [--port <n>]
24
+ configure [--global]
25
+ studio [--runtime-url <http(s)-url>] [--port <n>] [--no-open]
26
+ doctor sandbox [--json] show which sandbox backend this machine offers`;
27
+ /**
28
+ * Flags are parsed before the scope is resolved so --port can win over a PORT in .env, which
29
+ * is only loaded once the project root is known.
30
+ */
31
+ function parseFlags(args, allowed = {}) {
32
+ const booleans = new Set();
33
+ const values = new Map();
34
+ const rest = [];
35
+ const scope = {};
36
+ const booleanNames = new Set(["--global", ...(allowed.booleans ?? [])]);
37
+ const valueNames = new Set(["--port", "--db", ...(allowed.values ?? [])]);
38
+ for (let index = 0; index < args.length; index += 1) {
39
+ const arg = args[index];
40
+ if (!arg.startsWith("-")) {
41
+ rest.push(arg);
42
+ continue;
43
+ }
44
+ if (booleanNames.has(arg)) {
45
+ if (booleans.has(arg))
46
+ throw usageError(`${arg} may only be supplied once.`);
47
+ booleans.add(arg);
48
+ continue;
49
+ }
50
+ if (valueNames.has(arg)) {
51
+ if (values.has(arg))
52
+ throw usageError(`${arg} may only be supplied once.`);
53
+ const value = args[++index];
54
+ if (!value || value.startsWith("-"))
55
+ throw usageError(`${arg} requires a value.`);
56
+ values.set(arg, value);
57
+ continue;
58
+ }
59
+ throw usageError(usage);
60
+ }
61
+ if (booleans.has("--global"))
62
+ scope.global = true;
63
+ if (values.has("--port"))
64
+ scope.port = parsePort(values.get("--port"));
65
+ if (values.has("--db"))
66
+ scope.db = values.get("--db");
67
+ return { scope, rest, booleans, values };
68
+ }
69
+ const usageError = (message) => new CliError(message, 2);
70
+ function parsePort(value) {
71
+ if (value === undefined)
72
+ return undefined;
73
+ const port = Number(value);
74
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
75
+ throw usageError("--port must be an integer between 1 and 65535.");
76
+ return port;
77
+ }
78
+ async function serverKeyFor(scope) {
79
+ return (process.env.NYLORUN_SERVER_KEY ??
80
+ (await localCredentials(scope.credentialsPath)).serverKey);
81
+ }
82
+ async function startStudio(agentServerUrl, serverKey, open, port) {
14
83
  let entry;
15
84
  try {
16
85
  entry = createRequire(join(process.cwd(), "package.json")).resolve("@nylorun/studio");
@@ -21,74 +90,203 @@ async function startStudio(agentServerUrl, open, port) {
21
90
  const studio = await import(pathToFileURL(entry).href);
22
91
  return studio.startStudio({
23
92
  runtimeUrl: agentServerUrl,
24
- serverKey: process.env.NYLORUN_SERVER_KEY ??
25
- (await (await import("./project-runner.js")).localCredentials())
26
- .serverKey,
93
+ serverKey,
27
94
  open,
28
95
  ...(port === undefined ? {} : { port }),
29
96
  });
30
97
  }
31
- function parsePort(value) {
32
- if (value === undefined)
33
- return undefined;
34
- const port = Number(value);
35
- if (!Number.isInteger(port) || port < 1 || port > 65535)
36
- throw new Error("--port must be an integer between 1 and 65535.");
37
- return port;
98
+ function printStatus(status) {
99
+ const { scope, runtime } = status;
100
+ const lines = [
101
+ ["Scope", `${scope.kind} (${scope.kind === "project" ? scope.root : scope.dataDir})`],
102
+ ["State", status.state],
103
+ ["URL", scope.url],
104
+ ["PID", runtime?.pid ? String(runtime.pid) : status.pid ? String(status.pid) : undefined],
105
+ ["Runtime", status.version],
106
+ ["CLI expects", cliRuntimeVersion()],
107
+ ["Database", scope.sqlitePath],
108
+ ["Log", scope.logPath],
109
+ ["Started", runtime?.startedAt],
110
+ ];
111
+ const width = Math.max(...lines.map(([label]) => label.length));
112
+ for (const [label, value] of lines)
113
+ if (value !== undefined)
114
+ console.log(`${label.padEnd(width)} ${value}`);
115
+ }
116
+ async function runtimeCommand(args) {
117
+ const [verb, ...rest] = args;
118
+ if (verb === "start") {
119
+ const flags = parseFlags(rest, { booleans: ["--foreground", "--restart"] });
120
+ if (flags.rest.length)
121
+ throw usageError(usage);
122
+ const scope = resolveScope(flags.scope);
123
+ loadScopeEnvironment(scope);
124
+ const before = await hostStatus(scope);
125
+ if (before.state === "running" && !flags.booleans.has("--restart")) {
126
+ console.log(`Runtime already running for ${scope.label} at ${scope.url} (pid ${before.runtime?.pid ?? before.pid}).`);
127
+ warnIncompatible(scope, before);
128
+ return;
129
+ }
130
+ const status = await startHost(scope, {
131
+ foreground: flags.booleans.has("--foreground"),
132
+ restart: flags.booleans.has("--restart"),
133
+ });
134
+ printStatus(status);
135
+ console.log(`\n→ npx nylorun dev`);
136
+ return;
137
+ }
138
+ if (verb === "stop") {
139
+ const flags = parseFlags(rest, { values: ["--timeout"] });
140
+ if (flags.rest.length)
141
+ throw usageError(usage);
142
+ const scope = resolveScope(flags.scope);
143
+ const seconds = flags.values.has("--timeout")
144
+ ? Number(flags.values.get("--timeout"))
145
+ : undefined;
146
+ if (seconds !== undefined && (!Number.isFinite(seconds) || seconds <= 0))
147
+ throw usageError("--timeout must be a positive number of seconds.");
148
+ const result = await stopHost(scope, {
149
+ ...(seconds === undefined ? {} : { timeoutMs: seconds * 1000 }),
150
+ });
151
+ console.log(result === "stopped"
152
+ ? `Stopped the Runtime for ${scope.label}. SQLite and credentials are kept in ${scope.dataDir}.`
153
+ : `No Runtime is running for ${scope.label}.`);
154
+ return;
155
+ }
156
+ if (verb === "status") {
157
+ const flags = parseFlags(rest, { values: ["--output"] });
158
+ if (flags.rest.length)
159
+ throw usageError(usage);
160
+ const output = flags.values.get("--output") ?? "text";
161
+ if (output !== "text" && output !== "json")
162
+ throw usageError("--output must be text or json.");
163
+ const scope = resolveScope(flags.scope);
164
+ const status = await hostStatus(scope);
165
+ if (output === "json")
166
+ console.log(JSON.stringify({
167
+ scope: scope.kind,
168
+ root: scope.kind === "project" ? scope.root : scope.dataDir,
169
+ url: scope.url,
170
+ state: status.state,
171
+ pid: status.runtime?.pid ?? status.pid ?? null,
172
+ runtimeVersion: status.version ?? null,
173
+ cliVersion: cliRuntimeVersion() ?? null,
174
+ scopeId: status.scopeId ?? null,
175
+ sqlitePath: scope.sqlitePath,
176
+ logPath: scope.logPath,
177
+ startedAt: status.runtime?.startedAt ?? null,
178
+ }, null, 2));
179
+ else
180
+ printStatus(status);
181
+ if (status.state !== "running")
182
+ process.exitCode = status.state === "port-conflict" ? 4 : 3;
183
+ return;
184
+ }
185
+ if (verb === "logs") {
186
+ const flags = parseFlags(rest, { booleans: ["-f"], values: ["-n"] });
187
+ if (flags.rest.length)
188
+ throw usageError(usage);
189
+ const lines = flags.values.has("-n") ? Number(flags.values.get("-n")) : 200;
190
+ if (!Number.isInteger(lines) || lines < 1)
191
+ throw usageError("-n must be a positive integer.");
192
+ await tailLog(resolveScope(flags.scope), {
193
+ follow: flags.booleans.has("-f"),
194
+ lines,
195
+ });
196
+ return;
197
+ }
198
+ throw usageError(usage);
38
199
  }
39
200
  async function main() {
40
- const [command, ...args] = process.argv.slice(2);
41
- if (!command || command === "--help" || command === "-h")
201
+ const [rawCommand, ...rawArgs] = process.argv.slice(2);
202
+ if (!rawCommand || rawCommand === "--help" || rawCommand === "-h")
42
203
  return void console.log(usage);
43
- if (["configure", "dev", "start"].includes(command))
44
- loadProjectEnvironment();
45
- if (command === "start") {
46
- if (args.length > 1 || args[0]?.startsWith("--"))
47
- throw new Error(usage);
48
- await (await import("./launcher.js")).start(args[0]);
204
+ const aliases = {
205
+ up: "start",
206
+ down: "stop",
207
+ logs: "logs",
208
+ };
209
+ const command = rawCommand in aliases ? "runtime" : rawCommand;
210
+ const args = rawCommand in aliases ? [aliases[rawCommand], ...rawArgs] : rawArgs;
211
+ if (command === "runtime")
212
+ return await runtimeCommand(args);
213
+ if (command === "doctor") {
214
+ const [topic, ...options] = args;
215
+ if (topic !== "sandbox" || options.some((option) => option !== "--json"))
216
+ throw usageError("Usage: nylorun doctor sandbox [--json]");
217
+ const { doctorSandbox } = await import("./doctor.js");
218
+ await doctorSandbox({ json: options.includes("--json") });
49
219
  return;
50
220
  }
51
221
  if (command === "dev") {
52
- process.exitCode = await develop(args);
222
+ const flags = parseFlags(args, {
223
+ booleans: ["--no-studio", "--no-open", "--no-autostart"],
224
+ });
225
+ if (flags.rest.length)
226
+ throw usageError(usage);
227
+ const scope = resolveScope(flags.scope);
228
+ loadScopeEnvironment(scope);
229
+ // The port is re-resolved after .env so a project file can set PORT, while --port wins.
230
+ const resolved = resolveScope(flags.scope);
231
+ process.env.PORT = String(resolved.port);
232
+ developmentPreflight([...flags.booleans]);
233
+ // The supervisor checks the host before spawning tsx so port conflicts, version skew and
234
+ // --no-autostart surface with their own exit codes instead of a generic watcher failure.
235
+ await ensureRuntime(resolved, {
236
+ autostart: !flags.booleans.has("--no-autostart"),
237
+ });
238
+ process.exitCode = await develop([...flags.booleans]);
239
+ return;
240
+ }
241
+ if (command === "serve") {
242
+ const flags = parseFlags(args, { booleans: ["--no-autostart"] });
243
+ if (flags.rest.length > 1)
244
+ throw usageError(usage);
245
+ const scope = resolveScope(flags.scope);
246
+ loadScopeEnvironment(scope);
247
+ const resolved = resolveScope(flags.scope);
248
+ // serve runs the project in this process, so runProject's own errors carry their exit
249
+ // codes out; it validates the agent registry before it touches the host.
250
+ await (await import("./launcher.js")).serve(flags.rest[0], {
251
+ autostart: !flags.booleans.has("--no-autostart"),
252
+ scope: resolved,
253
+ });
53
254
  return;
54
255
  }
55
256
  if (command === "configure") {
56
- if (args.length)
57
- throw new Error(usage);
257
+ const flags = parseFlags(args);
258
+ if (flags.rest.length)
259
+ throw usageError(usage);
260
+ const scope = resolveScope(flags.scope);
261
+ loadScopeEnvironment(scope);
58
262
  const controller = new AbortController();
59
263
  const cancel = (signal) => controller.abort(new ConfigurationCancelled(signal));
60
264
  process.once("SIGINT", () => cancel("SIGINT"));
61
265
  process.once("SIGTERM", () => cancel("SIGTERM"));
62
- await configureProvider({ signal: controller.signal });
266
+ const serverKey = await serverKeyFor(scope);
267
+ const health = await fetch(`${scope.url}/health`).catch(() => undefined);
268
+ if (!health?.ok)
269
+ throw new CliError(`No Runtime is listening at ${scope.url} for ${scope.label}. Start one with "nylorun up".`, 6);
270
+ const prompted = await configureProvider({ signal: controller.signal });
271
+ await putHostModel(scope.url, serverKey, prompted);
63
272
  return;
64
273
  }
65
274
  if (command !== "studio")
66
- throw new Error(usage);
67
- let agentUrl;
68
- let port;
69
- let open = true;
70
- for (let index = 0; index < args.length; index += 1) {
71
- const arg = args[index];
72
- if (arg === "--runtime-url") {
73
- if (agentUrl !== undefined)
74
- throw new Error("--runtime-url may only be supplied once.");
75
- agentUrl = args[++index];
76
- if (!agentUrl || agentUrl.startsWith("--"))
77
- throw new Error("--runtime-url requires a value.");
78
- }
79
- else if (arg === "--port") {
80
- if (port !== undefined)
81
- throw new Error("--port may only be supplied once.");
82
- port = parsePort(args[++index]);
83
- }
84
- else if (arg === "--no-open" && open)
85
- open = false;
86
- else
87
- throw new Error(usage);
88
- }
89
- if (!agentUrl)
90
- throw new Error("--runtime-url is required.");
91
- const dashboard = await startStudio(agentUrl, open, port);
275
+ throw usageError(usage);
276
+ const flags = parseFlags(args, {
277
+ booleans: ["--no-open"],
278
+ values: ["--runtime-url", "--studio-port"],
279
+ });
280
+ if (flags.rest.length)
281
+ throw usageError(usage);
282
+ const scope = resolveScope(flags.scope);
283
+ const runtimeUrl = flags.values.get("--runtime-url") ?? scope.url;
284
+ const studioPort = flags.values.has("--studio-port")
285
+ ? parsePort(flags.values.get("--studio-port"))
286
+ : flags.scope.port !== undefined && flags.values.has("--runtime-url")
287
+ ? flags.scope.port
288
+ : undefined;
289
+ const dashboard = await startStudio(runtimeUrl, await serverKeyFor(scope), !flags.booleans.has("--no-open"), studioPort);
92
290
  console.log(`Studio on ${dashboard.address}`);
93
291
  await new Promise((resolve, reject) => {
94
292
  const close = () => void dashboard.close().then(resolve, reject);
@@ -96,8 +294,19 @@ async function main() {
96
294
  process.once("SIGTERM", close);
97
295
  });
98
296
  }
99
- void main().catch((error) => {
297
+ /**
298
+ * Probing a port that answers but is not a Runtime can leave a pooled connection holding the
299
+ * event loop open, so a finished command exits explicitly once its output has drained.
300
+ */
301
+ async function finish(code) {
302
+ process.exitCode = code;
303
+ for (const stream of [process.stdout, process.stderr])
304
+ await new Promise((resolve) => stream.write("", () => resolve()));
305
+ process.exit(code);
306
+ }
307
+ void main().then(() => finish(process.exitCode === undefined ? 0 : Number(process.exitCode)), (error) => {
100
308
  console.error(error instanceof Error ? error.message : String(error));
101
- process.exitCode =
102
- error instanceof ConfigurationCancelled ? error.exitCode : 1;
309
+ return finish(error instanceof ConfigurationCancelled || error instanceof CliError
310
+ ? error.exitCode
311
+ : 1);
103
312
  });
package/dist/dev-entry.js CHANGED
@@ -2,4 +2,5 @@ import { runProject } from "./project-runner.js";
2
2
  await runProject("agents/index.ts", {
3
3
  studio: !process.argv.includes("--no-studio"),
4
4
  open: !process.argv.includes("--no-open"),
5
+ autostart: !process.argv.includes("--no-autostart"),
5
6
  });
package/dist/dev.d.ts CHANGED
@@ -1 +1,6 @@
1
+ /**
2
+ * Everything that can be checked without contacting the host, so a project that cannot run
3
+ * never causes a Runtime to be started on its behalf.
4
+ */
5
+ export declare function developmentPreflight(args: readonly string[]): string;
1
6
  export declare function develop(args: readonly string[]): Promise<number>;
package/dist/dev.js CHANGED
@@ -2,13 +2,14 @@ import { spawn } from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- export async function develop(args) {
5
+ /**
6
+ * Everything that can be checked without contacting the host, so a project that cannot run
7
+ * never causes a Runtime to be started on its behalf.
8
+ */
9
+ export function developmentPreflight(args) {
6
10
  if (new Set(args).size !== args.length ||
7
- args.some((arg) => !["--no-studio", "--no-open"].includes(arg)))
8
- throw new Error("Usage: nylorun dev [--no-studio] [--no-open]");
9
- const port = Number(process.env.PORT ?? 8787);
10
- if (!Number.isInteger(port) || port < 1 || port > 65535)
11
- throw new Error("PORT must be between 1 and 65535");
11
+ args.some((arg) => !["--no-studio", "--no-open", "--no-autostart"].includes(arg)))
12
+ throw new Error("Usage: nylorun dev [--no-studio] [--no-open] [--no-autostart]");
12
13
  const require = createRequire(join(process.cwd(), "package.json"));
13
14
  let tsx;
14
15
  try {
@@ -24,6 +25,10 @@ export async function develop(args) {
24
25
  catch {
25
26
  throw new Error("Install @nylorun/studio or use --no-studio");
26
27
  }
28
+ return tsx;
29
+ }
30
+ export async function develop(args) {
31
+ const tsx = developmentPreflight(args);
27
32
  const child = spawn(process.execPath, [
28
33
  tsx,
29
34
  "watch",
@@ -0,0 +1,7 @@
1
+ import type { AgentManifest } from "@nylorun/agents";
2
+ /** `nylorun doctor sandbox`: what this machine offers and what a Runtime here would pick. */
3
+ export declare function doctorSandbox(options: {
4
+ json: boolean;
5
+ }): Promise<void>;
6
+ /** One line for the dev banner, or undefined when no connected agent declares a sandbox. */
7
+ export declare function sandboxBanner(runtimeUrl: string, serverKey: string, manifests: readonly AgentManifest[]): Promise<string | undefined>;
package/dist/doctor.js ADDED
@@ -0,0 +1,61 @@
1
+ import { release } from "node:os";
2
+ import { probeSandboxBackends, } from "@nylorun/runtime/node";
3
+ const LABEL = {
4
+ microsandbox: "microsandbox VM",
5
+ virtual: "virtual shell",
6
+ };
7
+ /** `nylorun doctor sandbox`: what this machine offers and what a Runtime here would pick. */
8
+ export async function doctorSandbox(options) {
9
+ const report = await probeSandboxBackends();
10
+ if (options.json) {
11
+ console.log(JSON.stringify({ platform: platform(), ...report }, null, 2));
12
+ return;
13
+ }
14
+ const rows = [["platform", platform()]];
15
+ for (const probe of report.probes)
16
+ rows.push([
17
+ probe.name,
18
+ `${probe.available ? "✓" : "✗"} ${probe.reason ?? ""}${probe.version ? ` · ${probe.version}` : ""}`,
19
+ ]);
20
+ rows.push(["preference", report.preference === "auto" ? "auto (set NYLORUN_SANDBOX to force one)" : report.preference]);
21
+ rows.push(["selected", report.backend ? `${report.backend} (${report.isolation} isolation)` : `none: ${report.reason}`]);
22
+ const width = Math.max(...rows.map(([key]) => key.length)) + 2;
23
+ for (const [key, value] of rows)
24
+ console.log(` ${key.padEnd(width)}${value}`);
25
+ if (report.backend === "virtual")
26
+ console.log("\n The virtual shell emulates bash in the Runtime process; it is not a VM boundary.\n" +
27
+ " For hardware isolation use macOS on Apple Silicon or Linux with KVM.");
28
+ }
29
+ /** One line for the dev banner, or undefined when no connected agent declares a sandbox. */
30
+ export async function sandboxBanner(runtimeUrl, serverKey, manifests) {
31
+ const capability = manifests
32
+ .flatMap((manifest) => manifest.capabilities)
33
+ .find((item) => item.sandbox);
34
+ if (!capability)
35
+ return undefined;
36
+ let report;
37
+ try {
38
+ const response = await fetch(`${runtimeUrl}/v1/host/sandbox`, {
39
+ headers: { authorization: `Bearer ${serverKey}` },
40
+ signal: AbortSignal.timeout(10_000),
41
+ });
42
+ if (response.ok)
43
+ report = await response.json();
44
+ }
45
+ catch { }
46
+ if (!report)
47
+ return undefined;
48
+ const doctor = "run `npx nylorun doctor sandbox` for options";
49
+ if (!report.backend)
50
+ return `sandbox: unavailable (${report.reason}) · ${doctor}`;
51
+ const image = capability.sandbox?.image ?? report.defaultImage;
52
+ const network = capability.sandbox?.network?.preset ?? "dev";
53
+ const fellBack = report.preference === "auto" && report.backend !== report.probes[0]?.name;
54
+ return fellBack
55
+ ? `sandbox: ${LABEL[report.backend] ?? report.backend} (${report.reason}) · ${doctor}`
56
+ : `sandbox: ${LABEL[report.backend] ?? report.backend}${report.backend === "microsandbox" && image ? ` · image ${image}` : ""} · network: ${network}`;
57
+ }
58
+ function platform() {
59
+ const os = process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : process.platform;
60
+ return `${os} (kernel ${release()}) · ${process.arch}`;
61
+ }
@@ -0,0 +1,5 @@
1
+ /** Exit codes are part of the CLI contract; see cli/README.md. */
2
+ export declare class CliError extends Error {
3
+ readonly exitCode: number;
4
+ constructor(message: string, exitCode?: number);
5
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Exit codes are part of the CLI contract; see cli/README.md. */
2
+ export class CliError extends Error {
3
+ exitCode;
4
+ constructor(message, exitCode = 1) {
5
+ super(message);
6
+ this.exitCode = exitCode;
7
+ this.name = "CliError";
8
+ }
9
+ }
@@ -1 +1,5 @@
1
- export declare function start(entry?: string): Promise<void>;
1
+ import type { Scope } from "./scope.js";
2
+ export declare function serve(entry?: string, options?: {
3
+ autostart?: boolean;
4
+ scope?: Scope;
5
+ }): Promise<void>;
package/dist/launcher.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { runProject } from "./project-runner.js";
2
- export async function start(entry = "dist/agents/index.js") {
3
- await runProject(entry);
2
+ export async function serve(entry = "dist/agents/index.js", options = {}) {
3
+ await runProject(entry, options);
4
4
  }