@nylorun/runtime 0.8.0-beta → 0.10.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +96 -25
  3. package/dist/adapters/media.d.ts +2 -2
  4. package/dist/adapters/media.js +2 -2
  5. package/dist/adapters/observe.d.ts +2 -1
  6. package/dist/adapters/observe.js +2 -3
  7. package/dist/assets.d.ts +2 -2
  8. package/dist/assets.js +5 -3
  9. package/dist/core/executors.d.ts +11 -2
  10. package/dist/core/executors.js +20 -4
  11. package/dist/core/flow-host.d.ts +264 -0
  12. package/dist/core/flow-host.js +783 -0
  13. package/dist/core/limits.d.ts +40 -0
  14. package/dist/core/limits.js +76 -0
  15. package/dist/core/runtime.d.ts +5 -90
  16. package/dist/core/runtime.js +3 -1418
  17. package/dist/core/sandbox-routes.d.ts +35 -0
  18. package/dist/core/sandbox-routes.js +106 -0
  19. package/dist/core/store.d.ts +5 -2
  20. package/dist/core/store.js +20 -72
  21. package/dist/core/turn-manifest.d.ts +50 -0
  22. package/dist/core/turn-manifest.js +66 -0
  23. package/dist/host/config-for.d.ts +16 -0
  24. package/dist/host/config-for.js +45 -0
  25. package/dist/host/config.d.ts +24 -0
  26. package/dist/host/create-host.d.ts +21 -0
  27. package/dist/host/create-host.js +396 -0
  28. package/dist/host/environment.d.ts +13 -0
  29. package/dist/host/environment.js +53 -0
  30. package/dist/host/http.d.ts +35 -0
  31. package/dist/host/http.js +136 -0
  32. package/dist/host/index.d.ts +6 -0
  33. package/dist/host/index.js +5 -0
  34. package/dist/host/logger.d.ts +3 -0
  35. package/dist/host/logger.js +18 -0
  36. package/dist/host/main.d.ts +3 -0
  37. package/dist/host/main.js +142 -0
  38. package/dist/index.d.ts +7 -2
  39. package/dist/index.js +4 -2
  40. package/dist/launcher/commands.d.ts +24 -0
  41. package/dist/launcher/commands.js +260 -0
  42. package/dist/launcher/ensure-config.d.ts +16 -0
  43. package/dist/launcher/ensure-config.js +79 -0
  44. package/dist/launcher/errors.d.ts +12 -0
  45. package/dist/launcher/errors.js +22 -0
  46. package/dist/launcher/host-config.d.ts +17 -0
  47. package/dist/launcher/host-config.js +64 -0
  48. package/dist/launcher/host-state.d.ts +17 -0
  49. package/dist/launcher/host-state.js +61 -0
  50. package/dist/launcher/lifecycle.d.ts +66 -0
  51. package/dist/launcher/lifecycle.js +407 -0
  52. package/dist/launcher/locks.d.ts +26 -0
  53. package/dist/launcher/locks.js +117 -0
  54. package/dist/launcher/logs.d.ts +18 -0
  55. package/dist/launcher/logs.js +117 -0
  56. package/dist/launcher/main.d.ts +2 -0
  57. package/dist/launcher/main.js +66 -0
  58. package/dist/launcher/output.d.ts +11 -0
  59. package/dist/launcher/output.js +68 -0
  60. package/dist/launcher/paths.d.ts +20 -0
  61. package/dist/launcher/paths.js +36 -0
  62. package/dist/launcher/probe.d.ts +23 -0
  63. package/dist/launcher/probe.js +72 -0
  64. package/dist/launcher/protocol.d.ts +62 -0
  65. package/dist/launcher/protocol.js +1 -0
  66. package/dist/launcher/schema-guard.d.ts +6 -0
  67. package/dist/launcher/schema-guard.js +36 -0
  68. package/dist/launcher/spawn.d.ts +22 -0
  69. package/dist/launcher/spawn.js +85 -0
  70. package/dist/launcher/status.d.ts +6 -0
  71. package/dist/launcher/status.js +65 -0
  72. package/dist/mcp/connect.d.ts +1 -0
  73. package/dist/mcp/connect.js +2 -0
  74. package/dist/mcp/pool.d.ts +16 -2
  75. package/dist/mcp/pool.js +54 -19
  76. package/dist/mcp/snapshot.d.ts +4 -0
  77. package/dist/mcp/stdio.d.ts +2 -0
  78. package/dist/mcp/stdio.js +3 -1
  79. package/dist/model/auth-store.d.ts +1 -1
  80. package/dist/model/auth-store.js +2 -2
  81. package/dist/model/models.d.ts +4 -0
  82. package/dist/model/models.js +22 -26
  83. package/dist/model/pi-model.js +1 -1
  84. package/dist/model/settings.d.ts +5 -1
  85. package/dist/model/settings.js +6 -2
  86. package/dist/sandbox/select.d.ts +4 -3
  87. package/dist/sandbox/select.js +15 -9
  88. package/dist/sandbox/share.d.ts +23 -0
  89. package/dist/sandbox/share.js +58 -0
  90. package/dist/tenant/envelope.d.ts +10 -0
  91. package/dist/tenant/envelope.js +54 -0
  92. package/dist/tenant/ephemeral.d.ts +31 -0
  93. package/dist/tenant/ephemeral.js +156 -0
  94. package/dist/tenant/logger.d.ts +9 -0
  95. package/dist/tenant/logger.js +25 -0
  96. package/dist/tenant/migration.d.ts +16 -0
  97. package/dist/tenant/migration.js +116 -0
  98. package/dist/tenant/module.d.ts +17 -0
  99. package/dist/tenant/module.js +315 -0
  100. package/dist/tenant/paths.d.ts +24 -0
  101. package/dist/tenant/paths.js +65 -0
  102. package/dist/tenant/pool.d.ts +10 -0
  103. package/dist/tenant/pool.js +54 -0
  104. package/dist/tenant/principals.d.ts +17 -0
  105. package/dist/tenant/principals.js +44 -0
  106. package/dist/tenant/quarantine-error.d.ts +6 -0
  107. package/dist/tenant/quarantine-error.js +15 -0
  108. package/dist/tenant/quarantine.d.ts +35 -0
  109. package/dist/tenant/quarantine.js +94 -0
  110. package/dist/tenant/reset.d.ts +21 -0
  111. package/dist/tenant/reset.js +125 -0
  112. package/dist/tenant/runtime.d.ts +80 -0
  113. package/dist/tenant/runtime.js +2256 -0
  114. package/dist/tenant/schema.d.ts +23 -0
  115. package/dist/tenant/schema.js +151 -0
  116. package/dist/tenant/status.d.ts +28 -0
  117. package/dist/tenant/status.js +95 -0
  118. package/dist/tenant/store-fs.d.ts +16 -0
  119. package/dist/tenant/store-fs.js +205 -0
  120. package/dist/tenant/store-memory.d.ts +19 -0
  121. package/dist/tenant/store-memory.js +100 -0
  122. package/dist/tenant/types.d.ts +125 -0
  123. package/dist/tenant/types.js +1 -0
  124. package/dist/vault/kek.d.ts +5 -2
  125. package/dist/vault/kek.js +7 -10
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/package.json +9 -6
  129. package/dist/core/main.js +0 -35
  130. /package/dist/{core/main.d.ts → host/config.js} +0 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Runtime Host process entry (`@nylorun/runtime/server`).
3
+ *
4
+ * Reads only `NYLORUN_HOME` from the environment (falls back to `~/.nylorun`).
5
+ * Absolute Host root is resolved once and passed down.
6
+ */
7
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
8
+ import { createRequire } from "node:module";
9
+ import { homedir } from "node:os";
10
+ import { resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { hostPaths } from "../tenant/paths.js";
13
+ import { createTenantModule } from "../tenant/module.js";
14
+ import { createFsTenantStore } from "../tenant/store-fs.js";
15
+ import { openTenantRuntime } from "../tenant/runtime.js";
16
+ import { bootstrapPrincipal } from "../tenant/principals.js";
17
+ import { migrateTenantDatabase } from "../tenant/schema.js";
18
+ import { configForFactory } from "./config-for.js";
19
+ import { createHost } from "./create-host.js";
20
+ import { baselineEnvironment, hostProcessEnvironment, } from "./environment.js";
21
+ import { HostListenError, EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK, } from "./http.js";
22
+ import { createHostLogger } from "./logger.js";
23
+ import { RUNTIME_VERSION } from "../version.js";
24
+ const entry = fileURLToPath(import.meta.url);
25
+ const nodeRequire = createRequire(import.meta.url);
26
+ function coreVersion() {
27
+ try {
28
+ return nodeRequire("@nylorun/core/package.json").version;
29
+ }
30
+ catch {
31
+ return "unknown";
32
+ }
33
+ }
34
+ function loadJson(path) {
35
+ return JSON.parse(readFileSync(path, "utf8"));
36
+ }
37
+ function resolveHostRoot() {
38
+ const fromEnv = process.env.NYLORUN_HOME;
39
+ const root = fromEnv && fromEnv.length > 0 ? fromEnv : resolve(homedir(), ".nylorun");
40
+ return resolve(root);
41
+ }
42
+ export async function main() {
43
+ const hostRoot = resolveHostRoot();
44
+ const paths = hostPaths(hostRoot);
45
+ mkdirSync(paths.home, { recursive: true });
46
+ mkdirSync(paths.tmp, { recursive: true });
47
+ mkdirSync(paths.tenants, { recursive: true });
48
+ if (!existsSync(paths.config)) {
49
+ throw new Error(`Missing host.json at ${paths.config}; run \`nylorun runtime up\` first`);
50
+ }
51
+ if (!existsSync(paths.credentials)) {
52
+ throw new Error(`Missing host-credentials.json at ${paths.credentials}; run \`nylorun runtime up\` first`);
53
+ }
54
+ const config = loadJson(paths.config);
55
+ const credentials = loadJson(paths.credentials);
56
+ const logger = createHostLogger();
57
+ const baseline = baselineEnvironment(process.env);
58
+ void hostProcessEnvironment(baseline, config, paths);
59
+ // Credential-free release/dev fixture (create-agent / CI smokes). Requires
60
+ // ephemeral mode so fixture models are allowed (Tenants D10).
61
+ const useFixture = process.env.NYLORUN_DEV_MODEL?.trim() === "fixture";
62
+ const configFor = configForFactory({
63
+ hostRoot,
64
+ hostConfig: config,
65
+ logger,
66
+ baseline,
67
+ ...(useFixture
68
+ ? { mode: "ephemeral", model: { kind: "fixture" } }
69
+ : {}),
70
+ });
71
+ const openRuntime = (tenantConfig) => openTenantRuntime(tenantConfig);
72
+ const store = createFsTenantStore({
73
+ hostRoot,
74
+ openRuntime,
75
+ configFor,
76
+ logger,
77
+ writeBootstrap: (db, bootstrap) => {
78
+ migrateTenantDatabase(db);
79
+ bootstrapPrincipal(db, bootstrap);
80
+ },
81
+ });
82
+ const module = createTenantModule({
83
+ hostRoot,
84
+ store,
85
+ openRuntime,
86
+ configFor,
87
+ logger,
88
+ });
89
+ const options = {
90
+ hostRoot,
91
+ module,
92
+ config,
93
+ credentials,
94
+ logger,
95
+ coreVersion: coreVersion(),
96
+ };
97
+ const host = createHost(options);
98
+ try {
99
+ await host.listen();
100
+ }
101
+ catch (error) {
102
+ if (error instanceof HostListenError) {
103
+ logger.error("listen_failed", {
104
+ message: error.message,
105
+ exitCode: error.exitCode,
106
+ });
107
+ process.exitCode = error.exitCode;
108
+ return;
109
+ }
110
+ throw error;
111
+ }
112
+ const state = {
113
+ pid: process.pid,
114
+ startedAt: new Date().toISOString(),
115
+ version: RUNTIME_VERSION,
116
+ entry,
117
+ url: host.url,
118
+ };
119
+ writeFileSync(paths.state, `${JSON.stringify(state, null, 2)}\n`, {
120
+ mode: 0o600,
121
+ });
122
+ logger.info("host_ready", { url: host.url, hostId: config.hostId });
123
+ process.send?.({ type: "ready", url: host.url });
124
+ let closing;
125
+ const shutdown = () => {
126
+ closing ??= host.close().then(() => {
127
+ process.exit(0);
128
+ });
129
+ };
130
+ for (const signal of ["SIGINT", "SIGTERM"]) {
131
+ process.on(signal, shutdown);
132
+ }
133
+ }
134
+ void main().catch((error) => {
135
+ const message = error instanceof Error ? error.message : String(error);
136
+ process.stderr.write(`${message}\n`);
137
+ if (error instanceof HostListenError) {
138
+ process.exit(error.exitCode);
139
+ }
140
+ process.exit(1);
141
+ });
142
+ export { EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,10 @@
1
- export { CoreRuntime, createRuntime, startRuntime, type RuntimeOptions } from "./core/runtime.js";
1
+ export { openTenantRuntime, type TenantOpenHooks, } from "./tenant/runtime.js";
2
+ export { startEphemeralRuntime, type StartEphemeralRuntimeOptions, type EphemeralRuntime, } from "./tenant/ephemeral.js";
3
+ export type { TenantConfig, TenantHandle, TenantPaths, Logger, OpenTenantRuntime, } from "./tenant/types.js";
2
4
  export { scriptedModel, gatewayModel, type ModelProvider } from "./core/provider.js";
3
5
  export type { RuntimeAgent } from "./contracts.js";
4
- export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
6
+ export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./media.js";
5
7
  export type { RuntimeModelAdapter } from "./contracts.js";
8
+ export { baselineEnvironment, hostProcessEnvironment, tenantChildEnvironment, } from "./host/environment.js";
9
+ export type { HostProcessPathInputs } from "./host/environment.js";
10
+ export type { HostConfigFile, HostCredentialsFile, HostStateFile, } from "./host/config.js";
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
- export { CoreRuntime, createRuntime, startRuntime } from "./core/runtime.js";
1
+ export { openTenantRuntime, } from "./tenant/runtime.js";
2
+ export { startEphemeralRuntime, } from "./tenant/ephemeral.js";
2
3
  export { scriptedModel, gatewayModel } from "./core/provider.js";
3
- export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
4
+ export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./media.js";
5
+ export { baselineEnvironment, hostProcessEnvironment, tenantChildEnvironment, } from "./host/environment.js";
@@ -0,0 +1,24 @@
1
+ import { type LifecycleContext } from "./lifecycle.js";
2
+ import { type OutputSink } from "./output.js";
3
+ export declare const launcherUsage = "Usage: nylorun-runtime [--home <dir>] [--json] <command>\n\nCommands:\n version also --version\n up [--port <n>] [--allow-downgrade]\n down [--wait [--timeout <s>]] [--force]\n restart [--port <n>] [--allow-downgrade] [--wait | --force]\n run [--port <n>] [--allow-downgrade]\n status\n logs [--lines <n>] [--follow] [--tenant <id> | --all]";
4
+ export interface LauncherRuntimeOptions {
5
+ home: string;
6
+ platform: NodeJS.Platform;
7
+ /** Node running this launcher (`process.execPath`); the Host runs on it. */
8
+ nodeBinary: string;
9
+ /** `process.versions.node`; the launcher refuses Node older than 24. */
10
+ nodeVersion: string;
11
+ sink: OutputSink;
12
+ /** Allowlisted ambient baseline from main.ts. */
13
+ baselineEnv: Readonly<Record<string, string | undefined>>;
14
+ fetchImpl?: typeof fetch;
15
+ /** Abort signal for logs --follow (tests). */
16
+ signal?: AbortSignal;
17
+ /** Optional lifecycle overrides for tests. */
18
+ lifecycle?: Partial<Pick<LifecycleContext, "spawnHost" | "onSignal" | "lock" | "fetchImpl" | "hostEntry" | "runtimeVersion" | "tenantSchemaMax">>;
19
+ }
20
+ /**
21
+ * Run launcher commands. Returns a process exit code (0, 1, or 2).
22
+ * Ambient values must already be resolved into `options`.
23
+ */
24
+ export declare function runLauncher(argv: readonly string[], options: LauncherRuntimeOptions): Promise<number>;
@@ -0,0 +1,260 @@
1
+ import { resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { HOST_PROTOCOL, LAUNCHER_PROTOCOL, } from "@nylorun/core/compatibility";
4
+ import { TENANT_SCHEMA_VERSION } from "../tenant/schema.js";
5
+ import { RUNTIME_VERSION } from "../version.js";
6
+ import { LauncherError, UsageError } from "./errors.js";
7
+ import { down, restart, run, up } from "./lifecycle.js";
8
+ import { logs } from "./logs.js";
9
+ import { emitError, emitEvent, emitResult, exitCodeFor, } from "./output.js";
10
+ import { ensureHostLayout, hostPaths } from "./paths.js";
11
+ import { status } from "./status.js";
12
+ export const launcherUsage = `Usage: nylorun-runtime [--home <dir>] [--json] <command>
13
+
14
+ Commands:
15
+ version also --version
16
+ up [--port <n>] [--allow-downgrade]
17
+ down [--wait [--timeout <s>]] [--force]
18
+ restart [--port <n>] [--allow-downgrade] [--wait | --force]
19
+ run [--port <n>] [--allow-downgrade]
20
+ status
21
+ logs [--lines <n>] [--follow] [--tenant <id> | --all]`;
22
+ /** The Runtime needs node:sqlite and Node 24 APIs. */
23
+ const MIN_NODE_MAJOR = 24;
24
+ function usage(message) {
25
+ throw new UsageError(`${message}\n\n${launcherUsage}`);
26
+ }
27
+ function parseGlobal(args) {
28
+ const rest = [];
29
+ let json = false;
30
+ let home;
31
+ for (let index = 0; index < args.length; index += 1) {
32
+ const arg = args[index];
33
+ if (arg === "--json") {
34
+ json = true;
35
+ continue;
36
+ }
37
+ if (arg === "--home") {
38
+ const value = args[++index];
39
+ if (!value || value.startsWith("-"))
40
+ usage("--home requires a directory.");
41
+ home = value;
42
+ continue;
43
+ }
44
+ if (arg === "--help" || arg === "-h") {
45
+ throw new UsageError(launcherUsage, "");
46
+ }
47
+ rest.push(arg === "--version" && rest.length === 0 ? "version" : arg);
48
+ }
49
+ return { rest, booleans: new Set(), values: new Map(), json, home };
50
+ }
51
+ function parseCommandFlags(args, allowed = {}) {
52
+ const booleans = new Set();
53
+ const values = new Map();
54
+ const rest = [];
55
+ const booleanNames = new Set(allowed.booleans ?? []);
56
+ const valueNames = new Set(allowed.values ?? []);
57
+ for (let index = 0; index < args.length; index += 1) {
58
+ const arg = args[index];
59
+ if (!arg.startsWith("-")) {
60
+ rest.push(arg);
61
+ continue;
62
+ }
63
+ if (booleanNames.has(arg)) {
64
+ if (booleans.has(arg))
65
+ usage(`${arg} may only be supplied once.`);
66
+ booleans.add(arg);
67
+ continue;
68
+ }
69
+ if (valueNames.has(arg)) {
70
+ if (values.has(arg))
71
+ usage(`${arg} may only be supplied once.`);
72
+ const value = args[++index];
73
+ if (!value || value.startsWith("-"))
74
+ usage(`${arg} requires a value.`);
75
+ values.set(arg, value);
76
+ continue;
77
+ }
78
+ usage(`Unknown flag ${arg}.`);
79
+ }
80
+ return { rest, booleans, values, json: false };
81
+ }
82
+ function parsePort(value) {
83
+ if (value === undefined)
84
+ return undefined;
85
+ const port = Number(value);
86
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
87
+ usage("--port must be an integer between 0 and 65535.");
88
+ }
89
+ return port;
90
+ }
91
+ function lifecycleContext(paths, options, sink) {
92
+ return {
93
+ paths,
94
+ platform: options.platform,
95
+ nodeBinary: options.nodeBinary,
96
+ hostEntry: fileURLToPath(new URL("../host/main.js", import.meta.url)),
97
+ runtimeVersion: RUNTIME_VERSION,
98
+ tenantSchemaMax: TENANT_SCHEMA_VERSION,
99
+ baselineEnv: options.baselineEnv,
100
+ emit: (event) => emitEvent(sink, event),
101
+ ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
102
+ ...(options.lifecycle ?? {}),
103
+ };
104
+ }
105
+ /**
106
+ * Run launcher commands. Returns a process exit code (0, 1, or 2).
107
+ * Ambient values must already be resolved into `options`.
108
+ */
109
+ export async function runLauncher(argv, options) {
110
+ const sink = options.sink;
111
+ try {
112
+ const global = parseGlobal(argv);
113
+ sink.json = global.json || sink.json;
114
+ const home = resolve(global.home ?? options.home);
115
+ const paths = hostPaths(home);
116
+ const [verb, ...rest] = global.rest;
117
+ if (!verb)
118
+ usage("Missing command.");
119
+ if (verb === "version") {
120
+ const flags = parseCommandFlags(rest);
121
+ if (flags.rest.length)
122
+ usage("version takes no arguments.");
123
+ emitResult(sink, {
124
+ runtimeVersion: options.lifecycle?.runtimeVersion ?? RUNTIME_VERSION,
125
+ launcherProtocol: LAUNCHER_PROTOCOL,
126
+ protocol: HOST_PROTOCOL,
127
+ node: options.nodeVersion,
128
+ });
129
+ return 0;
130
+ }
131
+ const nodeMajor = Number(options.nodeVersion.split(".")[0]);
132
+ if (!(nodeMajor >= MIN_NODE_MAJOR)) {
133
+ throw new LauncherError("platform_unsupported", `Node ${MIN_NODE_MAJOR} or newer is required to run the Nylorun Runtime (found ${options.nodeVersion}).`, `Install Node ${MIN_NODE_MAJOR}, then reinstall: npm install --global @nylorun/runtime`);
134
+ }
135
+ if (verb === "status") {
136
+ const flags = parseCommandFlags(rest);
137
+ if (flags.rest.length)
138
+ usage("status takes no arguments.");
139
+ const result = await status(paths, options.lifecycle?.runtimeVersion ?? RUNTIME_VERSION);
140
+ emitResult(sink, { ...result });
141
+ return 0;
142
+ }
143
+ if (verb === "logs") {
144
+ const flags = parseCommandFlags(rest, {
145
+ booleans: ["--follow", "-f", "--all"],
146
+ values: ["--lines", "-n", "--tenant"],
147
+ });
148
+ if (flags.rest.length)
149
+ usage("logs takes no positional arguments.");
150
+ if (flags.booleans.has("--all") && flags.values.has("--tenant")) {
151
+ usage("Use either --tenant or --all, not both.");
152
+ }
153
+ const linesRaw = flags.values.get("--lines") ?? flags.values.get("-n");
154
+ let lines;
155
+ if (linesRaw !== undefined) {
156
+ lines = Number(linesRaw);
157
+ if (!Number.isInteger(lines) || lines < 0) {
158
+ usage("--lines must be a non-negative integer.");
159
+ }
160
+ }
161
+ await logs(paths, {
162
+ ...(lines !== undefined ? { lines } : {}),
163
+ follow: flags.booleans.has("--follow") || flags.booleans.has("-f"),
164
+ all: flags.booleans.has("--all"),
165
+ ...(flags.values.has("--tenant")
166
+ ? { tenant: flags.values.get("--tenant") }
167
+ : {}),
168
+ ...(options.signal ? { signal: options.signal } : {}),
169
+ emit: (event) => emitEvent(sink, event),
170
+ });
171
+ return 0;
172
+ }
173
+ if (verb === "up") {
174
+ const flags = parseCommandFlags(rest, {
175
+ booleans: ["--allow-downgrade"],
176
+ values: ["--port"],
177
+ });
178
+ if (flags.rest.length)
179
+ usage("up takes no positional arguments.");
180
+ const port = parsePort(flags.values.get("--port"));
181
+ const result = await up(lifecycleContext(paths, options, sink), {
182
+ ...(port !== undefined ? { port } : {}),
183
+ allowDowngrade: flags.booleans.has("--allow-downgrade"),
184
+ });
185
+ emitResult(sink, { ...result });
186
+ return 0;
187
+ }
188
+ if (verb === "down") {
189
+ const flags = parseCommandFlags(rest, {
190
+ booleans: ["--wait", "--force"],
191
+ values: ["--timeout"],
192
+ });
193
+ if (flags.rest.length)
194
+ usage("down takes no positional arguments.");
195
+ let timeoutMs;
196
+ if (flags.values.has("--timeout")) {
197
+ const seconds = Number(flags.values.get("--timeout"));
198
+ if (!Number.isFinite(seconds) || seconds <= 0) {
199
+ usage("--timeout must be a positive number of seconds.");
200
+ }
201
+ timeoutMs = seconds * 1000;
202
+ }
203
+ const result = await down(lifecycleContext(paths, options, sink), {
204
+ wait: flags.booleans.has("--wait"),
205
+ force: flags.booleans.has("--force"),
206
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
207
+ });
208
+ emitResult(sink, { ...result });
209
+ return 0;
210
+ }
211
+ if (verb === "restart") {
212
+ const flags = parseCommandFlags(rest, {
213
+ booleans: ["--allow-downgrade", "--wait", "--force"],
214
+ values: ["--timeout", "--port"],
215
+ });
216
+ if (flags.rest.length)
217
+ usage("restart takes no positional arguments.");
218
+ let timeoutMs;
219
+ if (flags.values.has("--timeout")) {
220
+ const seconds = Number(flags.values.get("--timeout"));
221
+ if (!Number.isFinite(seconds) || seconds <= 0) {
222
+ usage("--timeout must be a positive number of seconds.");
223
+ }
224
+ timeoutMs = seconds * 1000;
225
+ }
226
+ const port = parsePort(flags.values.get("--port"));
227
+ const result = await restart(lifecycleContext(paths, options, sink), {
228
+ allowDowngrade: flags.booleans.has("--allow-downgrade"),
229
+ wait: flags.booleans.has("--wait"),
230
+ force: flags.booleans.has("--force"),
231
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
232
+ ...(port !== undefined ? { port } : {}),
233
+ });
234
+ emitResult(sink, { ...result });
235
+ return 0;
236
+ }
237
+ if (verb === "run") {
238
+ const flags = parseCommandFlags(rest, {
239
+ booleans: ["--allow-downgrade"],
240
+ values: ["--port"],
241
+ });
242
+ if (flags.rest.length)
243
+ usage("run takes no positional arguments.");
244
+ const port = parsePort(flags.values.get("--port"));
245
+ const ctx = lifecycleContext(paths, options, sink);
246
+ const result = await run(ctx, {
247
+ ...(port !== undefined ? { port } : {}),
248
+ allowDowngrade: flags.booleans.has("--allow-downgrade"),
249
+ onReady: (ready) => emitResult(sink, { ...ready }),
250
+ });
251
+ void result;
252
+ return 0;
253
+ }
254
+ usage(`Unknown command ${verb}.`);
255
+ }
256
+ catch (error) {
257
+ emitError(sink, error instanceof Error ? error : new Error(String(error)));
258
+ return exitCodeFor(error);
259
+ }
260
+ }
@@ -0,0 +1,16 @@
1
+ import { type HostConfigFile } from "./host-config.js";
2
+ import { type HostPaths } from "./paths.js";
3
+ export interface EnsureHostConfigOptions {
4
+ /** Explicit `--port`; collision fails (strict). `0` means pick a free port. */
5
+ port?: number;
6
+ host?: string;
7
+ }
8
+ /**
9
+ * Load or create host.json. On first setup prefer 8787; when taken, pick a free
10
+ * loopback port. Explicit `--port` is strict (except `0` = free port).
11
+ */
12
+ export declare function ensureHostConfig(paths: HostPaths, options?: EnsureHostConfigOptions): Promise<{
13
+ config: HostConfigFile;
14
+ created: boolean;
15
+ portSelected: boolean;
16
+ }>;
@@ -0,0 +1,79 @@
1
+ import { createServer } from "node:net";
2
+ import { LauncherError } from "./errors.js";
3
+ import { newHostId, readHostConfig, writeHostConfig, } from "./host-config.js";
4
+ import { hostUrl } from "./paths.js";
5
+ import { portBindable, probeHostHealth } from "./probe.js";
6
+ import { ensureHostLayout } from "./paths.js";
7
+ const DEFAULT_HOST = "127.0.0.1";
8
+ const DEFAULT_PORT = 8787;
9
+ async function findFreeLoopbackPort(host) {
10
+ const server = createServer();
11
+ return await new Promise((resolvePromise, reject) => {
12
+ server.once("error", reject);
13
+ server.listen(0, host, () => {
14
+ const address = server.address();
15
+ if (!address || typeof address === "string") {
16
+ server.close();
17
+ reject(new Error("Could not allocate a free loopback port."));
18
+ return;
19
+ }
20
+ const { port } = address;
21
+ server.close(() => resolvePromise(port));
22
+ });
23
+ });
24
+ }
25
+ /**
26
+ * Load or create host.json. On first setup prefer 8787; when taken, pick a free
27
+ * loopback port. Explicit `--port` is strict (except `0` = free port).
28
+ */
29
+ export async function ensureHostConfig(paths, options = {}) {
30
+ await ensureHostLayout(paths);
31
+ const existing = await readHostConfig(paths);
32
+ if (existing) {
33
+ if (options.port !== undefined && options.port !== 0 && options.port !== existing.port) {
34
+ const updated = {
35
+ ...existing,
36
+ format: 1,
37
+ port: options.port,
38
+ };
39
+ if (!(await portBindable(updated.host, updated.port))) {
40
+ const probe = await probeHostHealth(hostUrl(updated));
41
+ if (!probe.health || probe.health.hostId !== updated.hostId) {
42
+ throw new LauncherError("foreign_port", `Port ${updated.port} on ${updated.host} is in use by another process.`, 'Choose a free port with "nylorun-runtime up --port <n>".');
43
+ }
44
+ }
45
+ await writeHostConfig(paths, updated);
46
+ return { config: updated, created: false, portSelected: false };
47
+ }
48
+ return {
49
+ config: { ...existing, format: 1 },
50
+ created: false,
51
+ portSelected: false,
52
+ };
53
+ }
54
+ const host = options.host ?? DEFAULT_HOST;
55
+ let port = options.port === 0 ? await findFreeLoopbackPort(host) : (options.port ?? DEFAULT_PORT);
56
+ let portSelected = options.port === 0;
57
+ if (options.port !== undefined && options.port !== 0) {
58
+ if (!(await portBindable(host, port))) {
59
+ throw new LauncherError("foreign_port", `Port ${port} on ${host} is in use by another process.`, 'Choose a free port with "nylorun-runtime up --port <n>".');
60
+ }
61
+ }
62
+ else if (options.port === undefined) {
63
+ if (!(await portBindable(host, port))) {
64
+ const probe = await probeHostHealth(hostUrl({ host, port }));
65
+ if (probe.foreign || probe.health || !(await portBindable(host, port))) {
66
+ port = await findFreeLoopbackPort(host);
67
+ portSelected = true;
68
+ }
69
+ }
70
+ }
71
+ const config = {
72
+ format: 1,
73
+ hostId: newHostId(),
74
+ host,
75
+ port,
76
+ };
77
+ await writeHostConfig(paths, config);
78
+ return { config, created: true, portSelected };
79
+ }
@@ -0,0 +1,12 @@
1
+ import type { ErrorCode } from "@nylorun/core/compatibility";
2
+ /** Operational launcher failure (exit 1). Usage errors are separate (exit 2). */
3
+ export declare class LauncherError extends Error {
4
+ readonly code: ErrorCode;
5
+ readonly remedy: string;
6
+ readonly details?: unknown;
7
+ constructor(code: ErrorCode, message: string, remedy: string, details?: unknown);
8
+ }
9
+ export declare class UsageError extends Error {
10
+ readonly remedy: string;
11
+ constructor(message: string, remedy?: string);
12
+ }
@@ -0,0 +1,22 @@
1
+ /** Operational launcher failure (exit 1). Usage errors are separate (exit 2). */
2
+ export class LauncherError extends Error {
3
+ code;
4
+ remedy;
5
+ details;
6
+ constructor(code, message, remedy, details) {
7
+ super(message);
8
+ this.name = "LauncherError";
9
+ this.code = code;
10
+ this.remedy = remedy;
11
+ if (details !== undefined)
12
+ this.details = details;
13
+ }
14
+ }
15
+ export class UsageError extends Error {
16
+ remedy;
17
+ constructor(message, remedy = "See nylorun-runtime --help.") {
18
+ super(message);
19
+ this.name = "UsageError";
20
+ this.remedy = remedy;
21
+ }
22
+ }
@@ -0,0 +1,17 @@
1
+ import type { HostPaths } from "./paths.js";
2
+ import type { HostConfigFileV1 } from "./protocol.js";
3
+ export type HostConfigFile = HostConfigFileV1 & Record<string, unknown>;
4
+ export declare const HOST_ID_PATTERN: RegExp;
5
+ export declare function newHostId(): string;
6
+ export declare function isHostId(value: unknown): value is string;
7
+ /**
8
+ * Read host.json. Missing `format` means Tenants-era format 0.
9
+ * Returns undefined when absent or structurally invalid.
10
+ */
11
+ export declare function readHostConfig(paths: HostPaths): Promise<HostConfigFile | undefined>;
12
+ /**
13
+ * Write host.json at format 1. Upgrades format 0, keeps unknown fields, and
14
+ * refuses files with a newer format (`host_format_newer`).
15
+ */
16
+ export declare function writeHostConfig(paths: HostPaths, config: HostConfigFile): Promise<void>;
17
+ export declare function hostConfigFormat(config: HostConfigFile): number;
@@ -0,0 +1,64 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { readFile, rename, writeFile } from "node:fs/promises";
3
+ import { newTenantId } from "@nylorun/core/compatibility";
4
+ import { LauncherError } from "./errors.js";
5
+ const KNOWN_FORMAT = 1;
6
+ export const HOST_ID_PATTERN = /^host_[0-9a-hjkmnp-tv-z]{26}$/;
7
+ export function newHostId() {
8
+ return `host_${newTenantId().slice(3)}`;
9
+ }
10
+ export function isHostId(value) {
11
+ return typeof value === "string" && HOST_ID_PATTERN.test(value);
12
+ }
13
+ function formatOf(value) {
14
+ if (value.format === undefined)
15
+ return 0;
16
+ if (typeof value.format === "number" && Number.isInteger(value.format)) {
17
+ return value.format;
18
+ }
19
+ return Number.POSITIVE_INFINITY;
20
+ }
21
+ /**
22
+ * Read host.json. Missing `format` means Tenants-era format 0.
23
+ * Returns undefined when absent or structurally invalid.
24
+ */
25
+ export async function readHostConfig(paths) {
26
+ try {
27
+ const value = JSON.parse(await readFile(paths.config, "utf8"));
28
+ if (typeof value.hostId !== "string" ||
29
+ typeof value.host !== "string" ||
30
+ typeof value.port !== "number") {
31
+ return undefined;
32
+ }
33
+ return value;
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ /**
40
+ * Write host.json at format 1. Upgrades format 0, keeps unknown fields, and
41
+ * refuses files with a newer format (`host_format_newer`).
42
+ */
43
+ export async function writeHostConfig(paths, config) {
44
+ const existing = await readHostConfig(paths);
45
+ if (existing) {
46
+ const existingFormat = formatOf(existing);
47
+ if (existingFormat > KNOWN_FORMAT) {
48
+ throw new LauncherError("host_format_newer", `host.json format ${existingFormat} is newer than this launcher supports (${KNOWN_FORMAT}).`, "Upgrade the Runtime (npm install --global @nylorun/runtime@latest), then retry.", { format: existingFormat, known: KNOWN_FORMAT });
49
+ }
50
+ }
51
+ const merged = {
52
+ ...(existing ?? {}),
53
+ ...config,
54
+ format: 1,
55
+ };
56
+ const temporary = `${paths.config}.${randomBytes(8).toString("hex")}.tmp`;
57
+ await writeFile(temporary, `${JSON.stringify(merged, null, 2)}\n`, {
58
+ mode: 0o600,
59
+ });
60
+ await rename(temporary, paths.config);
61
+ }
62
+ export function hostConfigFormat(config) {
63
+ return formatOf(config);
64
+ }