nylorun 0.8.0-beta → 0.11.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 +138 -0
- package/README.md +78 -34
- package/dist/cli.js +12 -2
- package/dist/doctor.js +2 -2
- package/dist/project/link.d.ts +21 -7
- package/dist/project/link.js +30 -6
- package/dist/project/seed.d.ts +2 -1
- package/dist/project/seed.js +5 -5
- package/dist/stack/commands.d.ts +39 -12
- package/dist/stack/commands.js +163 -111
- package/dist/stack/compose-file.d.ts +15 -14
- package/dist/stack/compose-file.js +40 -41
- package/dist/stack/env-file.d.ts +3 -17
- package/dist/stack/env-file.js +6 -41
- package/dist/stack/host-files.d.ts +0 -3
- package/dist/stack/host-files.js +2 -3
- package/dist/stack/index.d.ts +6 -0
- package/dist/stack/index.js +12 -1
- package/dist/stack/keys.d.ts +4 -0
- package/dist/stack/keys.js +74 -0
- package/dist/stack/mcp.d.ts +8 -0
- package/dist/stack/mcp.js +114 -0
- package/dist/stack/operator-keys.d.ts +39 -0
- package/dist/stack/operator-keys.js +138 -0
- package/dist/stack/paths.d.ts +18 -12
- package/dist/stack/paths.js +5 -7
- package/dist/stack/ports.d.ts +0 -2
- package/dist/stack/ports.js +0 -2
- package/dist/stack/prepare.d.ts +2 -4
- package/dist/stack/prepare.js +4 -7
- package/dist/stack/sandbox.js +1 -1
- package/package.json +4 -4
- package/dist/project/derived-key.d.ts +0 -8
- package/dist/project/derived-key.js +0 -18
package/dist/stack/commands.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { HostTenant } from "@nylorun/core/contracts";
|
|
2
1
|
import { type ComposeService, type DockerRunner } from "./docker.js";
|
|
3
2
|
import type { HarnessMode, SandboxStackEnv } from "./env-file.js";
|
|
4
3
|
import { type StackPaths } from "./paths.js";
|
|
5
4
|
import type { PortProbe } from "./ports.js";
|
|
6
5
|
import { type FetchLike } from "./studio-login.js";
|
|
6
|
+
import { type OperateEndpoint } from "./operator-keys.js";
|
|
7
7
|
export declare const STACK_SERVICES: readonly ["postgres", "restate", "s2-lite", "rustfs", "gateway", "runtime", "harness", "studio"];
|
|
8
8
|
export declare const stackUsage: string;
|
|
9
9
|
export interface StackDeps {
|
|
@@ -20,7 +20,7 @@ export interface StackDeps {
|
|
|
20
20
|
err(line: string): void;
|
|
21
21
|
/** Ask a yes/no question on the terminal; undefined when not interactive. */
|
|
22
22
|
confirm?(question: string): Promise<boolean>;
|
|
23
|
-
/** A developer is at a terminal
|
|
23
|
+
/** A developer is at a terminal. */
|
|
24
24
|
interactive?: boolean;
|
|
25
25
|
/** Open `url` in a browser; false when no browser could be started. */
|
|
26
26
|
openBrowser(url: string): Promise<boolean>;
|
|
@@ -52,10 +52,20 @@ export declare function parseStackFlags(args: readonly string[], allowed: {
|
|
|
52
52
|
lists?: readonly string[];
|
|
53
53
|
aliases?: Record<string, string>;
|
|
54
54
|
}, usage: string): Flags;
|
|
55
|
-
/** The Host's one Tenant as
|
|
56
|
-
export
|
|
57
|
-
/**
|
|
55
|
+
/** The Host's one Tenant as `nylorun-operate status --json` reports it. */
|
|
56
|
+
export interface StackTenant {
|
|
57
|
+
/** Null until the Tenant row has been read. */
|
|
58
|
+
id: string | null;
|
|
59
|
+
name: string | null;
|
|
60
|
+
state: "open" | "unavailable";
|
|
61
|
+
/** Why it cannot open (`schema-too-new`, `database-layout-old`, …); `message` says more. */
|
|
62
|
+
cause?: string;
|
|
63
|
+
message?: string;
|
|
64
|
+
}
|
|
65
|
+
/** Printed by `start`, which prints Studio's URL and opens no browser. */
|
|
58
66
|
export declare const STUDIO_SIGN_IN_HINT = "To sign a browser in to Studio, run \"npx nylorun studio\".";
|
|
67
|
+
/** Printed last by `start`. */
|
|
68
|
+
export declare const HELP_HINT = "Run \"npx nylorun --help\" to see what else nylorun can do.";
|
|
59
69
|
export interface StackStatus {
|
|
60
70
|
/** The Tenant's name. */
|
|
61
71
|
name: string;
|
|
@@ -66,13 +76,13 @@ export interface StackStatus {
|
|
|
66
76
|
state: "running" | "stopped" | "absent";
|
|
67
77
|
runtime: {
|
|
68
78
|
url?: string;
|
|
69
|
-
/** The Admin API (operator listener), when the Tenant publishes one. */
|
|
70
|
-
adminUrl?: string;
|
|
71
79
|
healthy: boolean;
|
|
80
|
+
/** `/ready`: the Tenant is open and the Runtime's stores answer. */
|
|
81
|
+
ready: boolean;
|
|
72
82
|
version?: string;
|
|
73
83
|
hostId?: string;
|
|
74
84
|
};
|
|
75
|
-
/** The Tenant as
|
|
85
|
+
/** The Tenant as `nylorun-operate status` reports it, while the runtime container runs. */
|
|
76
86
|
tenant?: StackTenant;
|
|
77
87
|
studio: {
|
|
78
88
|
/** `http://localhost:<port>`: also what an embedding app frames (same site as its own localhost). */
|
|
@@ -116,9 +126,8 @@ export interface StackEndpoints {
|
|
|
116
126
|
home: string;
|
|
117
127
|
/** `http://localhost:<port>` */
|
|
118
128
|
runtimeUrl: string;
|
|
119
|
-
/** The operator listener (Admin API). */
|
|
120
|
-
adminUrl: string;
|
|
121
129
|
hostId: string;
|
|
130
|
+
/** The admin key (host-credentials.json): it mints Studio logins. */
|
|
122
131
|
adminKey: string;
|
|
123
132
|
studioPort: number;
|
|
124
133
|
/** `http://localhost:<studio port>` */
|
|
@@ -128,14 +137,32 @@ export interface StackEndpoints {
|
|
|
128
137
|
/** This call started the Runtime (it was not running before). */
|
|
129
138
|
started: boolean;
|
|
130
139
|
}
|
|
131
|
-
/** The running Tenant's
|
|
140
|
+
/** The running Tenant's APIs and their keys (`nylorun sandbox`, `nylorun mcp`). */
|
|
132
141
|
export interface TenantApi {
|
|
133
142
|
/** The Tenant's name. */
|
|
134
143
|
name: string;
|
|
135
144
|
runtimeUrl: string;
|
|
136
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* The linked project's keys (`.nylorun/credentials.json`) when both reach the Tenant, else
|
|
147
|
+
* the keys `cli` and `cli-management` the Host root keeps (`cli-credentials.json`).
|
|
148
|
+
*/
|
|
137
149
|
applicationKey: string;
|
|
150
|
+
managementKey: string;
|
|
138
151
|
}
|
|
152
|
+
/** The selected Tenant's `nylorun-operate` while it runs: `nylorun key` and `runningTenantApi`. */
|
|
153
|
+
export interface RunningOperate extends OperateEndpoint {
|
|
154
|
+
/** The Tenant's name. */
|
|
155
|
+
name: string;
|
|
156
|
+
runtimeUrl: string;
|
|
157
|
+
paths: StackPaths;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The selected Tenant's `nylorun-operate` while it runs; exit 3 when it is not running, 7 when
|
|
161
|
+
* it is not open.
|
|
162
|
+
*/
|
|
163
|
+
export declare function runningOperate(deps: StackDeps, options?: {
|
|
164
|
+
name?: string;
|
|
165
|
+
}): Promise<RunningOperate>;
|
|
139
166
|
/**
|
|
140
167
|
* The selected Tenant's API while it runs: commands that read or change the Tenant's data never
|
|
141
168
|
* start it. Exit 3 when it is not running, 7 when it is not open.
|
package/dist/stack/commands.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { basename, resolve } from "node:path";
|
|
4
|
-
import { compareVersions
|
|
4
|
+
import { compareVersions } from "@nylorun/core/compatibility";
|
|
5
5
|
import { CliError } from "../errors.js";
|
|
6
|
-
import {
|
|
7
|
-
import { readProjectCredentials, readProjectLink, writeProjectCredentials, writeProjectLink, } from "../project/link.js";
|
|
6
|
+
import { credentialsPath, readCredentialsFile, readProjectCredentials, readProjectLink, writeProjectCredentials, writeProjectLink, } from "../project/link.js";
|
|
8
7
|
import { findProjectRoot } from "../project/root.js";
|
|
9
8
|
import { seedTenant } from "../project/seed.js";
|
|
10
9
|
import { readTelemetry, STUDIO_ANALYTICS_ID, TELEMETRY_NOTICE, telemetryDecision, writeTelemetry, } from "../telemetry.js";
|
|
@@ -15,6 +14,7 @@ import { stackPaths } from "./paths.js";
|
|
|
15
14
|
import { prepareStack, readStackEnv } from "./prepare.js";
|
|
16
15
|
import { assertTenantName, chooseTenantName, DEFAULT_TENANT, defaultNylorunRoot, listTenants, moveStackRoots, readTenantRecord, removeOldProxy, sanitizeTenantName, tenantRoot, tenantsDir, writeTenantRecord, } from "./stacks.js";
|
|
17
16
|
import { mintStudioLogin, studioOrigin } from "./studio-login.js";
|
|
17
|
+
import { CLI_KEY_ID, hostKey, keyAuthenticates, PROJECT_KEY_ID, } from "./operator-keys.js";
|
|
18
18
|
export const STACK_SERVICES = [
|
|
19
19
|
"postgres",
|
|
20
20
|
"restate",
|
|
@@ -30,10 +30,10 @@ const CORE_SERVICES = ["postgres", "restate", "s2-lite", "rustfs", "gateway", "r
|
|
|
30
30
|
function coreServices(harness) {
|
|
31
31
|
return CORE_SERVICES.filter((service) => service !== "harness" || harness === "remote");
|
|
32
32
|
}
|
|
33
|
-
export const stackUsage = ` up|start [--tenant <name>] [--no-link] [--no-studio] [--
|
|
33
|
+
export const stackUsage = ` up|start [--tenant <name>] [--no-link] [--no-studio] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset] [--restate-ui]
|
|
34
34
|
start the project's Tenant, creating it and the Project link (.nylorun/link.json,
|
|
35
|
-
credentials.json) on the first run; print the Runtime and Studio URLs
|
|
36
|
-
|
|
35
|
+
credentials.json) on the first run; print the Runtime and Studio URLs (it opens no
|
|
36
|
+
browser). --tenant attaches to (or creates) a named Tenant; --no-link
|
|
37
37
|
starts the default Tenant (or --tenant's) without linking the current directory;
|
|
38
38
|
--restate-ui (or NYLORUN_RESTATE_UI=1) publishes Restate's UI on loopback for debugging
|
|
39
39
|
down|stop [--tenant <name> | --all] stop the Tenant's containers (--all: every Tenant's); keep volumes
|
|
@@ -147,6 +147,13 @@ function composeArgs(ctx, ...args) {
|
|
|
147
147
|
...args,
|
|
148
148
|
];
|
|
149
149
|
}
|
|
150
|
+
/** `nylorun-operate` in the Tenant's runtime container (`docker compose exec`), for its keys. */
|
|
151
|
+
function operateEndpoint(ctx) {
|
|
152
|
+
return {
|
|
153
|
+
fetch: ctx.deps.fetch,
|
|
154
|
+
operate: (args) => ctx.deps.docker.run(composeArgs(ctx, "exec", "-T", "runtime", "nylorun-operate", ...args)),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
150
157
|
function requireStackFiles(ctx) {
|
|
151
158
|
if (!existsSync(ctx.paths.compose) || !existsSync(ctx.paths.env))
|
|
152
159
|
throw new CliError(`No Tenant ${ctx.name} under ${ctx.paths.root}. Run "nylorun start" first.`, 3);
|
|
@@ -221,34 +228,21 @@ async function waitForHealth(ctx, runtimeUrl, hostId) {
|
|
|
221
228
|
await sleep(ctx.deps.pollMs ?? 500);
|
|
222
229
|
}
|
|
223
230
|
}
|
|
224
|
-
/** `/
|
|
225
|
-
async function
|
|
226
|
-
if (!adminKey)
|
|
227
|
-
return undefined;
|
|
231
|
+
/** The Runtime's `/ready` (503 while not ready, same body); undefined when it does not answer. */
|
|
232
|
+
async function fetchReady(deps, runtimeUrl) {
|
|
228
233
|
try {
|
|
229
|
-
const response = await deps.fetch(`${
|
|
230
|
-
headers: {
|
|
231
|
-
authorization: `Bearer ${adminKey}`,
|
|
232
|
-
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
233
|
-
accept: "application/json",
|
|
234
|
-
},
|
|
234
|
+
const response = await deps.fetch(`${runtimeUrl}/ready`, {
|
|
235
|
+
headers: { accept: "application/json" },
|
|
235
236
|
signal: AbortSignal.timeout(3000),
|
|
236
237
|
redirect: "error",
|
|
237
238
|
});
|
|
238
|
-
if (!response.ok)
|
|
239
|
-
return undefined;
|
|
240
239
|
const body = (await response.json());
|
|
241
|
-
|
|
242
|
-
if (!tenant || (tenant.state !== "open" && tenant.state !== "unavailable"))
|
|
240
|
+
if (body.status !== "ready" && body.status !== "not_ready")
|
|
243
241
|
return undefined;
|
|
244
|
-
const harness = body.
|
|
242
|
+
const harness = body.harness;
|
|
245
243
|
return {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
name: typeof tenant.name === "string" ? tenant.name : null,
|
|
249
|
-
state: tenant.state,
|
|
250
|
-
...(tenant.cause ? { cause: tenant.cause } : {}),
|
|
251
|
-
},
|
|
244
|
+
ready: body.status === "ready",
|
|
245
|
+
tenant: body.checks?.tenant === true,
|
|
252
246
|
...((harness?.mode === "remote" || harness?.mode === "in-process") &&
|
|
253
247
|
typeof harness.connected === "number"
|
|
254
248
|
? { harness: { mode: harness.mode, connected: harness.connected } }
|
|
@@ -259,32 +253,59 @@ async function fetchAdminStatus(deps, adminUrl, adminKey) {
|
|
|
259
253
|
return undefined;
|
|
260
254
|
}
|
|
261
255
|
}
|
|
262
|
-
/**
|
|
263
|
-
|
|
264
|
-
|
|
256
|
+
/**
|
|
257
|
+
* `nylorun-operate status --json` in the runtime container (exit 0 open, 2 not open): the
|
|
258
|
+
* Tenant's id and name, and why it cannot open. Undefined when it does not answer.
|
|
259
|
+
*/
|
|
260
|
+
async function operateStatus(ctx) {
|
|
261
|
+
const result = await operateEndpoint(ctx).operate(["status", "--json"]);
|
|
262
|
+
if (result.code !== 0 && result.code !== 2)
|
|
263
|
+
return undefined;
|
|
264
|
+
let tenant;
|
|
265
|
+
try {
|
|
266
|
+
tenant = JSON.parse(result.stdout).tenant;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
if (!tenant || (tenant.state !== "open" && tenant.state !== "unavailable"))
|
|
272
|
+
return undefined;
|
|
273
|
+
return {
|
|
274
|
+
id: typeof tenant.id === "string" ? tenant.id : null,
|
|
275
|
+
name: typeof tenant.name === "string" ? tenant.name : null,
|
|
276
|
+
state: tenant.state,
|
|
277
|
+
...(typeof tenant.cause === "string" ? { cause: tenant.cause } : {}),
|
|
278
|
+
...(typeof tenant.message === "string" ? { message: tenant.message } : {}),
|
|
279
|
+
};
|
|
265
280
|
}
|
|
266
|
-
/** Why the Tenant is unavailable,
|
|
281
|
+
/** Why the Tenant is unavailable, as `nylorun-operate status` names it. */
|
|
267
282
|
function tenantCauseMessage(ctx, tenant) {
|
|
268
|
-
const
|
|
269
|
-
const hint = cause.code === "schema-too-new"
|
|
283
|
+
const hint = tenant.cause === "schema-too-new"
|
|
270
284
|
? ` This nylorun pins Runtime ${ctx.deps.runtimeVersion}, older than the Tenant's database: update nylorun (npx nylorun@latest start).`
|
|
271
285
|
: "";
|
|
272
|
-
|
|
286
|
+
const message = tenant.message ? `: ${tenant.message.replace(/\.?$/, ".")}` : ".";
|
|
287
|
+
return `Tenant ${ctx.name} is unavailable (${tenant.cause})${message}${hint} See "nylorun logs runtime".`;
|
|
273
288
|
}
|
|
274
289
|
/**
|
|
275
|
-
* Wait until the Tenant is open: the Runtime creates it on the
|
|
276
|
-
* it on later ones.
|
|
290
|
+
* Wait until the Tenant is open (`/ready`'s `tenant` check): the Runtime creates it on the
|
|
291
|
+
* first start and opens it on later ones. Then `nylorun-operate status` gives its id. While it
|
|
292
|
+
* is not open, `nylorun-operate status` is asked now and then: it names a cause only when the
|
|
293
|
+
* Tenant cannot open (`schema-too-new`, `database-layout-old`), which ends the wait.
|
|
277
294
|
*/
|
|
278
|
-
async function waitForTenant(ctx,
|
|
295
|
+
async function waitForTenant(ctx, runtimeUrl) {
|
|
279
296
|
const deadline = Date.now() + (ctx.deps.healthTimeoutMs ?? 60_000);
|
|
280
|
-
for (;;) {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
297
|
+
for (let poll = 1;; poll += 1) {
|
|
298
|
+
const open = (await fetchReady(ctx.deps, runtimeUrl))?.tenant === true;
|
|
299
|
+
const late = Date.now() > deadline;
|
|
300
|
+
if (open || late || poll % 10 === 0) {
|
|
301
|
+
const tenant = await operateStatus(ctx);
|
|
302
|
+
if (tenant?.state === "open" && tenant.id)
|
|
303
|
+
return { ...tenant, id: tenant.id };
|
|
304
|
+
if (!open && tenant?.cause)
|
|
305
|
+
throw new CliError(tenantCauseMessage(ctx, tenant), 7);
|
|
306
|
+
if (late)
|
|
307
|
+
throw new CliError(`Tenant ${ctx.name} did not open (${runtimeUrl}/ready: ${open ? "open, but nylorun-operate status did not answer" : "not open"})${tenant?.message ? `: ${tenant.message.replace(/\.$/, "")}` : ""}. See "nylorun status" and "nylorun logs runtime".`, 7);
|
|
308
|
+
}
|
|
288
309
|
await sleep(ctx.deps.pollMs ?? 500);
|
|
289
310
|
}
|
|
290
311
|
}
|
|
@@ -353,7 +374,6 @@ async function tenantPorts(base, except) {
|
|
|
353
374
|
const persisted = await readStackEnv(paths);
|
|
354
375
|
for (const port of [
|
|
355
376
|
persisted?.runtimePort,
|
|
356
|
-
persisted?.adminPort,
|
|
357
377
|
persisted?.studioPort,
|
|
358
378
|
persisted?.restatePort,
|
|
359
379
|
])
|
|
@@ -392,9 +412,6 @@ async function bringUp(ctx, options) {
|
|
|
392
412
|
runtimeVersion: overridden ? undefined : deps.runtimeVersion,
|
|
393
413
|
ports: deps.ports,
|
|
394
414
|
reserved: await reservedPorts(ctx),
|
|
395
|
-
...(deps.env.NYLORUN_DERIVED_PRINCIPALS?.trim()
|
|
396
|
-
? { derivedPrincipals: deps.env.NYLORUN_DERIVED_PRINCIPALS }
|
|
397
|
-
: {}),
|
|
398
415
|
...(options.studioEmbedOrigins ? { studioEmbedOrigins: options.studioEmbedOrigins } : {}),
|
|
399
416
|
...(analytics ? { studioAnalyticsId: STUDIO_ANALYTICS_ID } : {}),
|
|
400
417
|
...(options.sandboxes !== undefined ? { sandboxes: options.sandboxes } : {}),
|
|
@@ -411,14 +428,13 @@ async function bringUp(ctx, options) {
|
|
|
411
428
|
if (prepared.firstRun)
|
|
412
429
|
deps.err(`Created Tenant ${ctx.name} under ${ctx.paths.root} (Runtime port ${prepared.env.runtimePort}, Studio port ${prepared.env.studioPort}).`);
|
|
413
430
|
const runtimeUrl = `http://${STACK_CLIENT_HOST}:${prepared.env.runtimePort}`;
|
|
414
|
-
const adminUrl = `http://${STACK_CLIENT_HOST}:${prepared.env.adminPort}`;
|
|
415
431
|
const up = await deps.docker.stream(
|
|
416
432
|
// --remove-orphans: a service this start's file no longer has (the harness after a rollback
|
|
417
433
|
// to NYLORUN_HARNESS=in-process, sandboxes after disable) is removed, not left running.
|
|
418
434
|
composeArgs(ctx, "up", "--detach", "--wait", "--wait-timeout", "300", "--remove-orphans", ...coreServices(prepared.env.harness)));
|
|
419
435
|
if (up !== 0) {
|
|
420
|
-
// A Runtime whose Tenant cannot open fails readiness;
|
|
421
|
-
const tenant = await
|
|
436
|
+
// A Runtime whose Tenant cannot open fails readiness; nylorun-operate names the cause.
|
|
437
|
+
const tenant = await operateStatus(ctx);
|
|
422
438
|
if (tenant?.cause)
|
|
423
439
|
throw new CliError(tenantCauseMessage(ctx, tenant), 7);
|
|
424
440
|
throw new CliError(`docker compose up failed (exit ${up}). See "nylorun logs runtime", "nylorun logs gateway", "nylorun logs harness" and "nylorun status".`, 7);
|
|
@@ -439,7 +455,6 @@ async function bringUp(ctx, options) {
|
|
|
439
455
|
}
|
|
440
456
|
return {
|
|
441
457
|
runtimeUrl,
|
|
442
|
-
adminUrl,
|
|
443
458
|
hostId: prepared.host.hostId,
|
|
444
459
|
studioPort: prepared.env.studioPort,
|
|
445
460
|
studioUrl: studioOrigin(prepared.env.studioPort),
|
|
@@ -471,12 +486,10 @@ async function tryStudioLogin(ctx, studioPort, adminKey) {
|
|
|
471
486
|
deps.err(`Warning: Studio at ${origin} is not reachable for a login (${lastError}).`);
|
|
472
487
|
return undefined;
|
|
473
488
|
}
|
|
474
|
-
/** Printed by `start
|
|
489
|
+
/** Printed by `start`, which prints Studio's URL and opens no browser. */
|
|
475
490
|
export const STUDIO_SIGN_IN_HINT = 'To sign a browser in to Studio, run "npx nylorun studio".';
|
|
476
|
-
/**
|
|
477
|
-
|
|
478
|
-
return Boolean(deps.interactive) && !deps.env.CI && !flags.booleans.has("--no-open");
|
|
479
|
-
}
|
|
491
|
+
/** Printed last by `start`. */
|
|
492
|
+
export const HELP_HINT = 'Run "npx nylorun --help" to see what else nylorun can do.';
|
|
480
493
|
/**
|
|
481
494
|
* Open a Studio login in the browser, or print it when no browser starts.
|
|
482
495
|
* The login URL carries a single-use token, so it is printed only as a fallback.
|
|
@@ -487,13 +500,18 @@ async function openLogin(ctx, login) {
|
|
|
487
500
|
}
|
|
488
501
|
/**
|
|
489
502
|
* Link the project to the Tenant: `.nylorun/link.json` (format 3) and
|
|
490
|
-
* `.nylorun/credentials.json
|
|
491
|
-
*
|
|
492
|
-
*
|
|
503
|
+
* `.nylorun/credentials.json`. The link is rewritten only when it is older, or the Tenant's
|
|
504
|
+
* name, URL, Host or id changed; a new link seeds the Tenant from the project's `.env`.
|
|
505
|
+
*
|
|
506
|
+
* The credentials file is kept while both its keys still reach the Tenant (one authenticated
|
|
507
|
+
* read each). Otherwise it gets the keys `project` (application) and `project-management`
|
|
508
|
+
* (management): the ones the Host root keeps for the projects linked to this Tenant
|
|
509
|
+
* (`project-credentials.json`), or new ones put through `nylorun-operate`. Every checkout linked
|
|
510
|
+
* to the Tenant shares them, so linking one never rotates another's keys. A file from before
|
|
511
|
+
* management keys keeps its application key and gains the management key.
|
|
493
512
|
*/
|
|
494
513
|
async function linkProject(ctx, started, tenantId) {
|
|
495
514
|
const { deps, projectDir, link } = ctx;
|
|
496
|
-
const applicationKey = deriveTenantKey(started.adminKey, tenantId, PROJECT_PRINCIPAL_ID);
|
|
497
515
|
const fresh = link?.format !== 3 ||
|
|
498
516
|
link.tenant !== ctx.name ||
|
|
499
517
|
link.hostUrl !== started.runtimeUrl ||
|
|
@@ -506,14 +524,28 @@ async function linkProject(ctx, started, tenantId) {
|
|
|
506
524
|
hostId: started.hostId,
|
|
507
525
|
tenantId,
|
|
508
526
|
});
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
527
|
+
const reaches = (key) => keyAuthenticates(deps.fetch, started.runtimeUrl, key);
|
|
528
|
+
const existing = await readProjectCredentials(projectDir);
|
|
529
|
+
const kept = existing && (await reaches(existing.applicationKey)) ? existing : undefined;
|
|
530
|
+
let credentials = kept;
|
|
531
|
+
if (!kept?.management ||
|
|
532
|
+
// A project key from before operator keys becomes the one the Host root keeps.
|
|
533
|
+
(kept.principalId === PROJECT_KEY_ID &&
|
|
534
|
+
(await readCredentialsFile(ctx.paths.projectCredentials))?.applicationKey !== kept.applicationKey) ||
|
|
535
|
+
!(await reaches(kept.management.key))) {
|
|
536
|
+
const keys = await hostKey({
|
|
537
|
+
endpoint: operateEndpoint(ctx),
|
|
538
|
+
runtimeUrl: started.runtimeUrl,
|
|
539
|
+
id: PROJECT_KEY_ID,
|
|
540
|
+
file: ctx.paths.projectCredentials,
|
|
541
|
+
lock: ctx.paths.keysLock,
|
|
542
|
+
...(kept?.principalId === PROJECT_KEY_ID ? { adopt: kept } : {}),
|
|
516
543
|
});
|
|
544
|
+
credentials = kept ? { ...kept, management: keys.management } : keys;
|
|
545
|
+
await writeProjectCredentials(projectDir, credentials);
|
|
546
|
+
if (existing && !kept && !fresh)
|
|
547
|
+
deps.err(`Replaced the project's key, which no longer reaches Tenant ${ctx.name} (.nylorun/credentials.json).`);
|
|
548
|
+
}
|
|
517
549
|
if (!fresh)
|
|
518
550
|
return;
|
|
519
551
|
deps.err(`Linked ${projectDir} to Tenant ${ctx.name} (.nylorun/link.json, .nylorun/credentials.json).`);
|
|
@@ -521,7 +553,7 @@ async function linkProject(ctx, started, tenantId) {
|
|
|
521
553
|
const seeded = await seedTenant({
|
|
522
554
|
fetch: deps.fetch,
|
|
523
555
|
runtimeUrl: started.runtimeUrl,
|
|
524
|
-
|
|
556
|
+
managementKey: credentials.management.key,
|
|
525
557
|
projectRoot: projectDir,
|
|
526
558
|
});
|
|
527
559
|
const set = [...seeded.applied, ...(seeded.model ? [`model ${seeded.model}`] : [])];
|
|
@@ -532,11 +564,12 @@ async function linkProject(ctx, started, tenantId) {
|
|
|
532
564
|
deps.err(`Warning: could not seed the Tenant from .env (${error instanceof Error ? error.message : String(error)}). Set the model in Studio or with "npx @nylorun/cli configure".`);
|
|
533
565
|
}
|
|
534
566
|
}
|
|
535
|
-
const START_USAGE = "nylorun start [--tenant <name>] [--no-link] [--no-studio] [--
|
|
567
|
+
const START_USAGE = "nylorun start [--tenant <name>] [--no-link] [--no-studio] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset] [--restate-ui]";
|
|
536
568
|
async function start(deps, args) {
|
|
537
569
|
const flags = parseStackFlags(args, {
|
|
538
570
|
booleans: [
|
|
539
571
|
"--no-studio",
|
|
572
|
+
// Accepted and ignored: start no longer opens a browser.
|
|
540
573
|
"--no-open",
|
|
541
574
|
"--no-link",
|
|
542
575
|
"--allow-downgrade",
|
|
@@ -564,7 +597,7 @@ async function start(deps, args) {
|
|
|
564
597
|
? { studioEmbedOrigins: { add: embedOrigins, reset: resetEmbed } }
|
|
565
598
|
: {}),
|
|
566
599
|
});
|
|
567
|
-
const tenant = await waitForTenant(ctx, started.
|
|
600
|
+
const tenant = await waitForTenant(ctx, started.runtimeUrl);
|
|
568
601
|
if (ctx.projectDir)
|
|
569
602
|
await linkProject({ ...ctx, projectDir: ctx.projectDir }, started, tenant.id);
|
|
570
603
|
deps.out(`Tenant ${ctx.name} (${tenant.id})`);
|
|
@@ -573,15 +606,10 @@ async function start(deps, args) {
|
|
|
573
606
|
deps.out(`Restate ${started.restateUrl} (UI and admin, unauthenticated; for debugging)`);
|
|
574
607
|
if (started.studioStarted) {
|
|
575
608
|
deps.out(`Studio ${started.studioUrl}`);
|
|
576
|
-
|
|
577
|
-
const login = await tryStudioLogin(ctx, started.studioPort, started.adminKey);
|
|
578
|
-
if (login)
|
|
579
|
-
await openLogin(ctx, withNext(login, tenantStudioPath(tenant.id)));
|
|
580
|
-
}
|
|
581
|
-
else
|
|
582
|
-
deps.err(STUDIO_SIGN_IN_HINT);
|
|
609
|
+
deps.err(STUDIO_SIGN_IN_HINT);
|
|
583
610
|
}
|
|
584
611
|
await alsoRunning(ctx);
|
|
612
|
+
deps.err(HELP_HINT);
|
|
585
613
|
return 0;
|
|
586
614
|
}
|
|
587
615
|
/** After `start`: the machine's other running Tenants and their memory, on stderr. */
|
|
@@ -649,7 +677,7 @@ async function stackStatus(ctx) {
|
|
|
649
677
|
project: ctx.project,
|
|
650
678
|
home: ctx.paths.root,
|
|
651
679
|
state: "absent",
|
|
652
|
-
runtime: { healthy: false },
|
|
680
|
+
runtime: { healthy: false, ready: false },
|
|
653
681
|
studio: { state: "absent" },
|
|
654
682
|
restate: { published: false },
|
|
655
683
|
gateway: { state: "absent", healthy: false },
|
|
@@ -667,14 +695,11 @@ async function stackStatus(ctx) {
|
|
|
667
695
|
const health = runtimeUrl ? await fetchHealth(ctx.deps, runtimeUrl) : undefined;
|
|
668
696
|
const host = await readHostConfig(ctx.paths);
|
|
669
697
|
const healthy = health?.status === "ok" && (host === undefined || health.hostId === host.hostId);
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const answered = healthy && adminUrl
|
|
675
|
-
? await fetchAdminStatus(ctx.deps, adminUrl, await readAdminKey(ctx.paths))
|
|
698
|
+
const ready = healthy && runtimeUrl ? await fetchReady(ctx.deps, runtimeUrl) : undefined;
|
|
699
|
+
// nylorun-operate answers while the container runs, also when the Tenant cannot open.
|
|
700
|
+
const tenant = services.some((s) => s.service === "runtime" && s.state === "running")
|
|
701
|
+
? await operateStatus(ctx)
|
|
676
702
|
: undefined;
|
|
677
|
-
const tenant = answered?.tenant;
|
|
678
703
|
const studio = services.find((s) => s.service === "studio");
|
|
679
704
|
const gateway = services.find((s) => s.service === "gateway");
|
|
680
705
|
const harness = services.find((s) => s.service === "harness");
|
|
@@ -684,8 +709,8 @@ async function stackStatus(ctx) {
|
|
|
684
709
|
state: services.some((s) => s.state === "running") ? "running" : "stopped",
|
|
685
710
|
runtime: {
|
|
686
711
|
...(runtimeUrl ? { url: runtimeUrl } : {}),
|
|
687
|
-
...(persisted.adminPort && adminUrl ? { adminUrl } : {}),
|
|
688
712
|
healthy,
|
|
713
|
+
ready: ready?.ready ?? false,
|
|
689
714
|
...(health?.version ? { version: health.version } : {}),
|
|
690
715
|
...(health?.hostId ? { hostId: health.hostId } : {}),
|
|
691
716
|
},
|
|
@@ -705,10 +730,10 @@ async function stackStatus(ctx) {
|
|
|
705
730
|
healthy: isUp(services, "gateway"),
|
|
706
731
|
},
|
|
707
732
|
harness: {
|
|
708
|
-
mode:
|
|
733
|
+
mode: ready?.harness?.mode ?? harnessMode,
|
|
709
734
|
state: harness ? [harness.state, harness.health].filter(Boolean).join(", ") : "absent",
|
|
710
735
|
healthy: isUp(services, "harness"),
|
|
711
|
-
...(
|
|
736
|
+
...(ready?.harness ? { connected: ready.harness.connected } : {}),
|
|
712
737
|
},
|
|
713
738
|
services,
|
|
714
739
|
};
|
|
@@ -722,9 +747,8 @@ function describeTenant(tenant) {
|
|
|
722
747
|
const who = `${tenant.id ?? "(no id yet)"}${tenant.name ? ` (${tenant.name})` : ""}`;
|
|
723
748
|
if (tenant.state === "open")
|
|
724
749
|
return `${who} open`;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
: `${who} unavailable (opening)`;
|
|
750
|
+
const why = [tenant.cause, tenant.message].filter(Boolean).join(": ");
|
|
751
|
+
return `${who} unavailable${why ? `: ${why}` : " (opening)"}`;
|
|
728
752
|
}
|
|
729
753
|
async function status(deps, args) {
|
|
730
754
|
const usage = "nylorun status [--tenant <name>] [--json]";
|
|
@@ -745,11 +769,9 @@ async function status(deps, args) {
|
|
|
745
769
|
if (result.tenant)
|
|
746
770
|
out(`Tenant id ${describeTenant(result.tenant)}`);
|
|
747
771
|
const runtimeDetail = result.runtime.healthy
|
|
748
|
-
? `healthy, ${result.runtime.version ?? "?"}, ${result.runtime.hostId ?? "?"}`
|
|
772
|
+
? `healthy, ${result.runtime.ready ? "ready" : "not ready"}, ${result.runtime.version ?? "?"}, ${result.runtime.hostId ?? "?"}`
|
|
749
773
|
: "not answering";
|
|
750
774
|
out(`Runtime ${result.runtime.url ?? "?"} ${runtimeDetail}`);
|
|
751
|
-
if (result.runtime.adminUrl)
|
|
752
|
-
out(`Admin API ${result.runtime.adminUrl} (operators only, never proxied)`);
|
|
753
775
|
out(`Studio ${result.studio.url ?? "?"} ${result.studio.state} (log in with "nylorun studio")`);
|
|
754
776
|
if (result.studio.embedOrigins?.length)
|
|
755
777
|
out(`Embeds ${result.studio.embedOrigins.join(" ")} (may show Studio in a frame)`);
|
|
@@ -860,7 +882,6 @@ async function ls(deps, args) {
|
|
|
860
882
|
...(persisted.studioPort ? { studioUrl: studioOrigin(persisted.studioPort) } : {}),
|
|
861
883
|
ports: {
|
|
862
884
|
...(persisted.runtimePort ? { runtime: persisted.runtimePort } : {}),
|
|
863
|
-
...(persisted.adminPort ? { admin: persisted.adminPort } : {}),
|
|
864
885
|
...(persisted.studioPort ? { studio: persisted.studioPort } : {}),
|
|
865
886
|
...(persisted.restatePort ? { restate: persisted.restatePort } : {}),
|
|
866
887
|
},
|
|
@@ -940,9 +961,6 @@ async function runningStack(ctx, options) {
|
|
|
940
961
|
name: ctx.name,
|
|
941
962
|
home: ctx.paths.root,
|
|
942
963
|
runtimeUrl,
|
|
943
|
-
adminUrl: persisted.adminPort
|
|
944
|
-
? `http://${STACK_CLIENT_HOST}:${persisted.adminPort}`
|
|
945
|
-
: runtimeUrl,
|
|
946
964
|
hostId: host.hostId,
|
|
947
965
|
adminKey,
|
|
948
966
|
studioPort: persisted.studioPort,
|
|
@@ -962,7 +980,6 @@ async function ensureSelected(ctx, options) {
|
|
|
962
980
|
name: ctx.name,
|
|
963
981
|
home: ctx.paths.root,
|
|
964
982
|
runtimeUrl: started.runtimeUrl,
|
|
965
|
-
adminUrl: started.adminUrl,
|
|
966
983
|
hostId: started.hostId,
|
|
967
984
|
adminKey: started.adminKey,
|
|
968
985
|
studioPort: started.studioPort,
|
|
@@ -971,25 +988,60 @@ async function ensureSelected(ctx, options) {
|
|
|
971
988
|
started: !runtimeWasUp,
|
|
972
989
|
};
|
|
973
990
|
}
|
|
974
|
-
|
|
975
|
-
* The selected Tenant's API while it runs: commands that read or change the Tenant's data never
|
|
976
|
-
* start it. Exit 3 when it is not running, 7 when it is not open.
|
|
977
|
-
*/
|
|
978
|
-
export async function runningTenantApi(deps, options = {}) {
|
|
991
|
+
async function runningSelected(deps, options) {
|
|
979
992
|
const ctx = await selectStack(deps, options);
|
|
980
993
|
requireStackFiles(ctx);
|
|
981
994
|
await dockerPreflight(deps.docker);
|
|
982
995
|
const running = await runningStack(ctx, { studio: false });
|
|
983
996
|
if (!running)
|
|
984
997
|
throw new CliError(`Tenant ${ctx.name} is not running. Run "nylorun start" first.`, 3);
|
|
985
|
-
|
|
986
|
-
if (!tenant?.id || tenant.state !== "open")
|
|
998
|
+
if ((await fetchReady(deps, running.runtimeUrl))?.tenant !== true)
|
|
987
999
|
throw new CliError(`Tenant ${ctx.name} is not open. See "nylorun status".`, 7);
|
|
988
1000
|
return {
|
|
1001
|
+
ctx,
|
|
1002
|
+
running: {
|
|
1003
|
+
name: ctx.name,
|
|
1004
|
+
runtimeUrl: running.runtimeUrl,
|
|
1005
|
+
paths: ctx.paths,
|
|
1006
|
+
...operateEndpoint(ctx),
|
|
1007
|
+
},
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* The selected Tenant's `nylorun-operate` while it runs; exit 3 when it is not running, 7 when
|
|
1012
|
+
* it is not open.
|
|
1013
|
+
*/
|
|
1014
|
+
export async function runningOperate(deps, options = {}) {
|
|
1015
|
+
return (await runningSelected(deps, options)).running;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* The selected Tenant's API while it runs: commands that read or change the Tenant's data never
|
|
1019
|
+
* start it. Exit 3 when it is not running, 7 when it is not open.
|
|
1020
|
+
*/
|
|
1021
|
+
export async function runningTenantApi(deps, options = {}) {
|
|
1022
|
+
const { ctx, running } = await runningSelected(deps, options);
|
|
1023
|
+
const api = (applicationKey, managementKey) => ({
|
|
989
1024
|
name: ctx.name,
|
|
990
1025
|
runtimeUrl: running.runtimeUrl,
|
|
991
|
-
applicationKey
|
|
992
|
-
|
|
1026
|
+
applicationKey,
|
|
1027
|
+
managementKey,
|
|
1028
|
+
});
|
|
1029
|
+
const reaches = (key) => keyAuthenticates(deps.fetch, running.runtimeUrl, key);
|
|
1030
|
+
if (ctx.projectDir && ctx.link?.format === 3 && ctx.link.tenant === ctx.name) {
|
|
1031
|
+
const project = await readCredentialsFile(credentialsPath(ctx.projectDir));
|
|
1032
|
+
if (project?.management &&
|
|
1033
|
+
(await reaches(project.applicationKey)) &&
|
|
1034
|
+
(await reaches(project.management.key)))
|
|
1035
|
+
return api(project.applicationKey, project.management.key);
|
|
1036
|
+
}
|
|
1037
|
+
const keys = await hostKey({
|
|
1038
|
+
endpoint: running,
|
|
1039
|
+
runtimeUrl: running.runtimeUrl,
|
|
1040
|
+
id: CLI_KEY_ID,
|
|
1041
|
+
file: ctx.paths.cliCredentials,
|
|
1042
|
+
lock: ctx.paths.keysLock,
|
|
1043
|
+
});
|
|
1044
|
+
return api(keys.applicationKey, keys.management.key);
|
|
993
1045
|
}
|
|
994
1046
|
/**
|
|
995
1047
|
* Start the selected Tenant unless it is already running (`nylorun studio`): the
|
|
@@ -1067,7 +1119,7 @@ async function studio(deps, args, options = {}) {
|
|
|
1067
1119
|
// Studio serves the one Tenant; the link's Tenant id stands in while it is opening.
|
|
1068
1120
|
let next = options.next;
|
|
1069
1121
|
if (next === undefined) {
|
|
1070
|
-
const tenant = await
|
|
1122
|
+
const tenant = await operateStatus(ctx);
|
|
1071
1123
|
const linked = ctx.link?.tenant === ctx.name ? ctx.link.tenantId : undefined;
|
|
1072
1124
|
const tenantId = tenant?.id ?? linked;
|
|
1073
1125
|
if (tenantId)
|