nylorun 0.4.0-beta → 0.5.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 +22 -0
- package/README.md +108 -98
- package/dist/cli.js +24 -22
- package/dist/doctor.d.ts +7 -7
- package/dist/doctor.js +36 -36
- package/dist/home.d.ts +1 -1
- package/dist/home.js +1 -1
- package/dist/project/derived-key.d.ts +2 -2
- package/dist/project/derived-key.js +2 -2
- package/dist/project/link.d.ts +7 -7
- package/dist/project/link.js +3 -3
- package/dist/stack/commands.d.ts +14 -14
- package/dist/stack/commands.js +132 -192
- package/dist/stack/compose-file.d.ts +7 -7
- package/dist/stack/compose-file.js +13 -11
- package/dist/stack/docker.js +1 -1
- package/dist/stack/env-file.d.ts +7 -2
- package/dist/stack/env-file.js +8 -3
- package/dist/stack/host-files.d.ts +4 -4
- package/dist/stack/host-files.js +3 -3
- package/dist/stack/images.d.ts +1 -1
- package/dist/stack/images.js +1 -1
- package/dist/stack/index.d.ts +2 -2
- package/dist/stack/index.js +7 -3
- package/dist/stack/paths.d.ts +3 -3
- package/dist/stack/paths.js +1 -1
- package/dist/stack/ports.d.ts +2 -2
- package/dist/stack/ports.js +1 -1
- package/dist/stack/prepare.d.ts +5 -3
- package/dist/stack/prepare.js +3 -2
- package/dist/stack/stacks.d.ts +37 -34
- package/dist/stack/stacks.js +91 -57
- package/dist/stack/versions.d.ts +1 -1
- package/dist/stack/versions.js +1 -1
- package/dist/telemetry.d.ts +31 -0
- package/dist/telemetry.js +79 -0
- package/package.json +5 -5
package/dist/stack/commands.js
CHANGED
|
@@ -1,83 +1,85 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, rm
|
|
3
|
-
import { basename,
|
|
2
|
+
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
4
|
import { compareVersions, PROTOCOL_HEADER, PROTOCOL_VERSION, } from "@nylorun/core/compatibility";
|
|
5
5
|
import { CliError } from "../errors.js";
|
|
6
6
|
import { deriveTenantKey, PROJECT_PRINCIPAL_ID } from "../project/derived-key.js";
|
|
7
7
|
import { readProjectCredentials, readProjectLink, writeProjectCredentials, writeProjectLink, } from "../project/link.js";
|
|
8
8
|
import { findProjectRoot } from "../project/root.js";
|
|
9
9
|
import { seedTenant } from "../project/seed.js";
|
|
10
|
+
import { readTelemetry, STUDIO_ANALYTICS_ID, TELEMETRY_NOTICE, telemetryDecision, writeTelemetry, } from "../telemetry.js";
|
|
10
11
|
import { dockerPreflight, parseComposePs, } from "./docker.js";
|
|
11
12
|
import { readAdminKey, readHostConfig, STACK_CLIENT_HOST } from "./host-files.js";
|
|
12
13
|
import { runtimeImageOverridden, stackImages } from "./images.js";
|
|
13
14
|
import { stackPaths } from "./paths.js";
|
|
14
15
|
import { prepareStack, readStackEnv } from "./prepare.js";
|
|
15
|
-
import {
|
|
16
|
+
import { assertTenantName, chooseTenantName, DEFAULT_TENANT, defaultNylorunRoot, listTenants, moveStackRoots, readTenantRecord, sanitizeTenantName, tenantRoot, tenantsDir, writeTenantRecord, } from "./stacks.js";
|
|
16
17
|
import { mintStudioLogin, studioOrigin } from "./studio-login.js";
|
|
17
18
|
export const STACK_SERVICES = ["postgres", "restate", "s2", "gateway", "runtime", "studio"];
|
|
18
19
|
const CORE_SERVICES = ["postgres", "restate", "s2", "gateway", "runtime"];
|
|
19
|
-
export const stackUsage = ` up|start [--
|
|
20
|
-
start the project's
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
down|stop [--
|
|
25
|
-
status [--
|
|
26
|
-
logs [service] [--
|
|
27
|
-
|
|
28
|
-
studio [--
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
export const stackUsage = ` up|start [--tenant <name>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset]
|
|
21
|
+
start the project's Tenant, creating it and the Project link (.nylorun/link.json,
|
|
22
|
+
credentials.json) on the first run; print the Runtime and Studio URLs and open Studio
|
|
23
|
+
signed in (in a terminal). --tenant attaches to (or creates) a named Tenant; --no-link
|
|
24
|
+
starts the default Tenant (or --tenant's) without linking the current directory
|
|
25
|
+
down|stop [--tenant <name>] stop the Tenant's containers; keep volumes
|
|
26
|
+
status [--tenant <name>] [--json] the Tenant, its id, services, endpoints and Runtime health
|
|
27
|
+
logs [service] [--tenant <name>] [-f] [--tail <n>]
|
|
28
|
+
container logs (${STACK_SERVICES.join(", ")})
|
|
29
|
+
studio [--tenant <name>] [--no-open]
|
|
30
|
+
sign a browser in to Studio on the Tenant; --no-open prints the login URL; start the Tenant if it is stopped
|
|
31
|
+
reset [--tenant <name>] [--yes] delete the Tenant's data (volumes, tenant/ and vault key); the next start creates it anew
|
|
32
|
+
ls [--json] the Tenants on this machine
|
|
33
|
+
delete <tenant> --yes remove a Tenant: containers, volumes and its Host root, with the vault key (KEK)`;
|
|
33
34
|
const usageError = (message) => new CliError(message, 2);
|
|
34
|
-
/** The Compose project of
|
|
35
|
+
/** The Compose project of Tenant `name`: `nylorun-<name>`, or `NYLORUN_COMPOSE_PROJECT`. */
|
|
35
36
|
export function stackProject(env, name) {
|
|
36
|
-
const override = env.
|
|
37
|
+
const override = env.NYLORUN_COMPOSE_PROJECT?.trim();
|
|
37
38
|
if (!override)
|
|
38
39
|
return `nylorun-${name}`;
|
|
39
|
-
return
|
|
40
|
+
return assertTenantName(override, "NYLORUN_COMPOSE_PROJECT");
|
|
40
41
|
}
|
|
41
42
|
function nylorunRoot(deps) {
|
|
42
43
|
return deps.nylorunRoot ?? defaultNylorunRoot();
|
|
43
44
|
}
|
|
44
|
-
async function
|
|
45
|
-
const names = (await
|
|
46
|
-
return names.length ? `
|
|
45
|
+
async function tenantNames(deps) {
|
|
46
|
+
const names = (await listTenants(nylorunRoot(deps))).map((tenant) => tenant.name);
|
|
47
|
+
return names.length ? ` Tenants on this machine: ${names.join(", ")}.` : "";
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
|
-
* Which
|
|
50
|
-
* project's link (format
|
|
51
|
-
* directory (`
|
|
52
|
-
*
|
|
50
|
+
* Which Tenant a command acts on: `--tenant`, then `NYLORUN_TENANT`, then the Tenant of the
|
|
51
|
+
* project's link (format 3; `start` replaces an older one); for `start` in a project, then a
|
|
52
|
+
* name from the project directory (`chooseTenantName`); outside a project (or with
|
|
53
|
+
* --no-link), `default`. `NYLORUN_HOME` sets the Host root whatever the name; its name
|
|
54
|
+
* otherwise comes from its `tenant.json`, the project directory or the Host root's own.
|
|
53
55
|
*/
|
|
54
56
|
async function selectStack(deps, options = {}) {
|
|
55
57
|
const { env } = deps;
|
|
56
58
|
const base = nylorunRoot(deps);
|
|
57
59
|
const projectDir = options.noLink ? undefined : findProjectRoot(deps.cwd ?? process.cwd());
|
|
58
60
|
const link = projectDir ? await readProjectLink(projectDir) : undefined;
|
|
59
|
-
const explicit = options.name ?? (env.
|
|
61
|
+
const explicit = options.name ?? (env.NYLORUN_TENANT?.trim() || undefined);
|
|
60
62
|
if (explicit !== undefined)
|
|
61
|
-
|
|
62
|
-
let name = explicit ??
|
|
63
|
+
assertTenantName(explicit, options.name !== undefined ? "--tenant" : "NYLORUN_TENANT");
|
|
64
|
+
let name = explicit ?? link?.tenant;
|
|
63
65
|
let root;
|
|
64
66
|
const home = env.NYLORUN_HOME?.trim();
|
|
65
67
|
if (home) {
|
|
66
68
|
root = resolve(home);
|
|
67
69
|
name ??=
|
|
68
|
-
(await
|
|
69
|
-
|
|
70
|
+
(await readTenantRecord(root))?.name ??
|
|
71
|
+
sanitizeTenantName(basename(projectDir ?? root));
|
|
70
72
|
}
|
|
71
73
|
else {
|
|
72
74
|
if (name === undefined && options.start && projectDir)
|
|
73
|
-
name = await
|
|
75
|
+
name = await chooseTenantName(base, projectDir);
|
|
76
|
+
if (name === undefined && !projectDir)
|
|
77
|
+
name = DEFAULT_TENANT;
|
|
74
78
|
if (name === undefined)
|
|
75
|
-
throw usageError(
|
|
76
|
-
|
|
77
|
-
: `No stack selected: run this in a project "nylorun start" linked, pass --name <stack>, or set NYLORUN_STACK.${await stackNames(deps)}`);
|
|
78
|
-
root = stackRoot(base, name);
|
|
79
|
+
throw usageError(`No Tenant selected: run "nylorun start" in this project, pass --tenant <name>, or set NYLORUN_TENANT.${await tenantNames(deps)}`);
|
|
80
|
+
root = tenantRoot(base, name);
|
|
79
81
|
}
|
|
80
|
-
|
|
82
|
+
assertTenantName(name, "The Tenant name");
|
|
81
83
|
return {
|
|
82
84
|
deps,
|
|
83
85
|
name,
|
|
@@ -101,7 +103,7 @@ function composeArgs(ctx, ...args) {
|
|
|
101
103
|
}
|
|
102
104
|
function requireStackFiles(ctx) {
|
|
103
105
|
if (!existsSync(ctx.paths.compose) || !existsSync(ctx.paths.env))
|
|
104
|
-
throw new CliError(`No
|
|
106
|
+
throw new CliError(`No Tenant ${ctx.name} under ${ctx.paths.root}. Run "nylorun start" first.`, 3);
|
|
105
107
|
}
|
|
106
108
|
export function parseStackFlags(args, allowed, usage) {
|
|
107
109
|
const booleans = new Set();
|
|
@@ -137,9 +139,9 @@ export function parseStackFlags(args, allowed, usage) {
|
|
|
137
139
|
}
|
|
138
140
|
return { rest, booleans, values, lists };
|
|
139
141
|
}
|
|
140
|
-
/** `--
|
|
142
|
+
/** `--tenant` when given. */
|
|
141
143
|
function nameOption(flags) {
|
|
142
|
-
const name = flags.values.get("--
|
|
144
|
+
const name = flags.values.get("--tenant");
|
|
143
145
|
return name === undefined ? {} : { name };
|
|
144
146
|
}
|
|
145
147
|
async function sleep(ms) {
|
|
@@ -203,16 +205,16 @@ async function fetchTenant(deps, adminUrl, adminKey) {
|
|
|
203
205
|
return undefined;
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
|
-
/** Why the
|
|
208
|
+
/** Why the Tenant is unavailable, with the repair the Runtime names. */
|
|
207
209
|
function tenantCauseMessage(ctx, tenant) {
|
|
208
210
|
const cause = tenant.cause;
|
|
209
211
|
const hint = cause.code === "schema-too-new"
|
|
210
|
-
? ` This nylorun pins Runtime ${ctx.deps.runtimeVersion}, older than the
|
|
212
|
+
? ` This nylorun pins Runtime ${ctx.deps.runtimeVersion}, older than the Tenant's database: update nylorun (npx nylorun@latest start).`
|
|
211
213
|
: "";
|
|
212
|
-
return `
|
|
214
|
+
return `Tenant ${ctx.name} is unavailable (${cause.code}): ${cause.message} ${cause.repair}${hint} See "nylorun logs runtime".`;
|
|
213
215
|
}
|
|
214
216
|
/**
|
|
215
|
-
* Wait until the
|
|
217
|
+
* Wait until the Tenant is open: the Runtime creates it on the first start and opens
|
|
216
218
|
* it on later ones. A Tenant that could not be opened has a cause; report it.
|
|
217
219
|
*/
|
|
218
220
|
async function waitForTenant(ctx, adminUrl, adminKey) {
|
|
@@ -224,7 +226,7 @@ async function waitForTenant(ctx, adminUrl, adminKey) {
|
|
|
224
226
|
if (tenant?.cause)
|
|
225
227
|
throw new CliError(tenantCauseMessage(ctx, tenant), 7);
|
|
226
228
|
if (Date.now() > deadline)
|
|
227
|
-
throw new CliError(`
|
|
229
|
+
throw new CliError(`Tenant ${ctx.name} did not open (${adminUrl}/v1/admin/status: ${tenant ? tenant.state : "no answer"}). See "nylorun status" and "nylorun logs runtime".`, 7);
|
|
228
230
|
await sleep(ctx.deps.pollMs ?? 500);
|
|
229
231
|
}
|
|
230
232
|
}
|
|
@@ -267,7 +269,7 @@ function isOlder(version, than) {
|
|
|
267
269
|
compareVersions(version, than) < 0);
|
|
268
270
|
}
|
|
269
271
|
/**
|
|
270
|
-
* Refuse a Runtime older than the
|
|
272
|
+
* Refuse a Runtime older than the Tenant's database: the Runtime that last ran the Tenant
|
|
271
273
|
* (host.json `runtimeVersion`) migrated it, and an older one cannot open its Tenant
|
|
272
274
|
* (`schema-too-new`). Checked before Compose replaces anything. Skipped when
|
|
273
275
|
* `NYLORUN_RUNTIME_IMAGE` names the image.
|
|
@@ -278,21 +280,16 @@ async function refuseDowngrade(ctx, allowDowngrade) {
|
|
|
278
280
|
if (!isOlder(pinned, host?.runtimeVersion))
|
|
279
281
|
return;
|
|
280
282
|
if (allowDowngrade) {
|
|
281
|
-
ctx.deps.err(`Warning: starting Runtime ${pinned} on
|
|
283
|
+
ctx.deps.err(`Warning: starting Runtime ${pinned} on Tenant ${ctx.name}, which Runtime ${host.runtimeVersion} last ran. If that Runtime migrated the database, the Tenant stays unavailable (schema-too-new).`);
|
|
282
284
|
return;
|
|
283
285
|
}
|
|
284
|
-
throw new CliError(`Refusing to start Runtime ${pinned} on
|
|
286
|
+
throw new CliError(`Refusing to start Runtime ${pinned} on Tenant ${ctx.name}: Runtime ${host.runtimeVersion} last ran it (${ctx.paths.config}), and a Runtime older than the Tenant's database cannot open its Tenant. Update nylorun (npx nylorun@latest start), or run "nylorun start --allow-downgrade" when both Runtimes use the same database schema.`, 5);
|
|
285
287
|
}
|
|
286
|
-
/** Ports other
|
|
288
|
+
/** Ports other Tenants keep in their `.env`, so a new Tenant avoids them. */
|
|
287
289
|
async function reservedPorts(ctx) {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
.
|
|
291
|
-
.map((stack) => stackPaths(stack.root));
|
|
292
|
-
// The legacy stack keeps its .env in stack/, not docker/.
|
|
293
|
-
const legacy = legacyStack(base);
|
|
294
|
-
if (legacy && resolve(base) !== ctx.paths.root)
|
|
295
|
-
others.push(legacy);
|
|
290
|
+
const others = (await listTenants(nylorunRoot(ctx.deps)))
|
|
291
|
+
.filter((tenant) => resolve(tenant.root) !== ctx.paths.root)
|
|
292
|
+
.map((tenant) => stackPaths(tenant.root));
|
|
296
293
|
const reserved = new Set();
|
|
297
294
|
for (const paths of others) {
|
|
298
295
|
const persisted = await readStackEnv(paths);
|
|
@@ -313,6 +310,8 @@ async function bringUp(ctx, options) {
|
|
|
313
310
|
const overridden = runtimeImageOverridden(deps.env);
|
|
314
311
|
if (!overridden)
|
|
315
312
|
await refuseDowngrade(ctx, options.allowDowngrade ?? false);
|
|
313
|
+
const telemetry = await readTelemetry(nylorunRoot(deps));
|
|
314
|
+
const analytics = telemetryDecision(deps.env, telemetry).enabled;
|
|
316
315
|
const prepared = await prepareStack({
|
|
317
316
|
paths: ctx.paths,
|
|
318
317
|
name: ctx.name,
|
|
@@ -331,13 +330,18 @@ async function bringUp(ctx, options) {
|
|
|
331
330
|
? { derivedPrincipals: deps.env.NYLORUN_DERIVED_PRINCIPALS }
|
|
332
331
|
: {}),
|
|
333
332
|
...(options.studioEmbedOrigins ? { studioEmbedOrigins: options.studioEmbedOrigins } : {}),
|
|
333
|
+
...(analytics ? { studioAnalyticsId: STUDIO_ANALYTICS_ID } : {}),
|
|
334
334
|
});
|
|
335
|
-
|
|
335
|
+
if (analytics && options.studio && telemetry.noticeShown === undefined) {
|
|
336
|
+
deps.err(TELEMETRY_NOTICE);
|
|
337
|
+
await writeTelemetry(nylorunRoot(deps), { ...telemetry, noticeShown: new Date().toISOString() });
|
|
338
|
+
}
|
|
339
|
+
const record = await readTenantRecord(ctx.paths.root);
|
|
336
340
|
const project = record?.project ?? ctx.projectDir;
|
|
337
341
|
if (record?.name !== ctx.name || record.project !== project)
|
|
338
|
-
await
|
|
342
|
+
await writeTenantRecord(ctx.paths.root, { name: ctx.name, ...(project ? { project } : {}) });
|
|
339
343
|
if (prepared.firstRun)
|
|
340
|
-
deps.err(`Created
|
|
344
|
+
deps.err(`Created Tenant ${ctx.name} under ${ctx.paths.root} (Runtime port ${prepared.env.runtimePort}, Studio port ${prepared.env.studioPort}).`);
|
|
341
345
|
const runtimeUrl = `http://${STACK_CLIENT_HOST}:${prepared.env.runtimePort}`;
|
|
342
346
|
const adminUrl = `http://${STACK_CLIENT_HOST}:${prepared.env.adminPort}`;
|
|
343
347
|
const up = await deps.docker.stream(composeArgs(ctx, "up", "--detach", "--wait", "--wait-timeout", "300", ...CORE_SERVICES));
|
|
@@ -396,36 +400,23 @@ async function openLogin(ctx, login) {
|
|
|
396
400
|
if (!(await ctx.deps.openBrowser(login)))
|
|
397
401
|
ctx.deps.out(`Sign in ${login}`);
|
|
398
402
|
}
|
|
399
|
-
/** The legacy stack is mentioned once per machine (a marker under `stacks/`). */
|
|
400
|
-
async function noteLegacy(deps) {
|
|
401
|
-
const base = nylorunRoot(deps);
|
|
402
|
-
const legacy = legacyStack(base);
|
|
403
|
-
const marker = join(stacksDir(base), ".legacy-noted");
|
|
404
|
-
if (!legacy || existsSync(marker))
|
|
405
|
-
return;
|
|
406
|
-
deps.err(`Note: the single stack of an older release is still on this machine (Compose project nylorun, Host root ${legacy.root}). Stacks are now one per project, and the old one is left as it is: stop it with "npx nylorun legacy stop", or remove it and its Tenants with "npx nylorun legacy delete --yes".`);
|
|
407
|
-
await mkdir(stacksDir(base), { recursive: true, mode: 0o700 });
|
|
408
|
-
await writeFile(marker, "", { mode: 0o600 });
|
|
409
|
-
}
|
|
410
403
|
/**
|
|
411
|
-
* Link the project to the
|
|
404
|
+
* Link the project to the Tenant: `.nylorun/link.json` (format 3) and
|
|
412
405
|
* `.nylorun/credentials.json` with the key of the derived principal `project`. The link is
|
|
413
|
-
* rewritten only when the
|
|
414
|
-
* Tenant from the project's `.env`.
|
|
406
|
+
* rewritten only when it is older, or the Tenant's name, URL, Host or id changed; a new link
|
|
407
|
+
* seeds the Tenant from the project's `.env`.
|
|
415
408
|
*/
|
|
416
409
|
async function linkProject(ctx, started, tenantId) {
|
|
417
410
|
const { deps, projectDir, link } = ctx;
|
|
418
|
-
if (link && link.format < 2)
|
|
419
|
-
deps.err(`This project was linked to a Tenant on ${link.hostUrl}, a stack of an older Runtime. It gets its own stack, ${ctx.name}, and a new link; model credentials come from .env (or Studio), and agents register again (npm run dev).`);
|
|
420
411
|
const applicationKey = deriveTenantKey(started.adminKey, tenantId, PROJECT_PRINCIPAL_ID);
|
|
421
|
-
const fresh = link?.format !==
|
|
422
|
-
link.
|
|
412
|
+
const fresh = link?.format !== 3 ||
|
|
413
|
+
link.tenant !== ctx.name ||
|
|
423
414
|
link.hostUrl !== started.runtimeUrl ||
|
|
424
415
|
link.hostId !== started.hostId ||
|
|
425
416
|
link.tenantId !== tenantId;
|
|
426
417
|
if (fresh)
|
|
427
418
|
await writeProjectLink(projectDir, {
|
|
428
|
-
|
|
419
|
+
tenant: ctx.name,
|
|
429
420
|
hostUrl: started.runtimeUrl,
|
|
430
421
|
hostId: started.hostId,
|
|
431
422
|
tenantId,
|
|
@@ -440,7 +431,7 @@ async function linkProject(ctx, started, tenantId) {
|
|
|
440
431
|
});
|
|
441
432
|
if (!fresh)
|
|
442
433
|
return;
|
|
443
|
-
deps.err(`Linked ${projectDir} to
|
|
434
|
+
deps.err(`Linked ${projectDir} to Tenant ${ctx.name} (.nylorun/link.json, .nylorun/credentials.json).`);
|
|
444
435
|
try {
|
|
445
436
|
const seeded = await seedTenant({
|
|
446
437
|
fetch: deps.fetch,
|
|
@@ -456,7 +447,7 @@ async function linkProject(ctx, started, tenantId) {
|
|
|
456
447
|
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".`);
|
|
457
448
|
}
|
|
458
449
|
}
|
|
459
|
-
const START_USAGE = "nylorun start [--
|
|
450
|
+
const START_USAGE = "nylorun start [--tenant <name>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset]";
|
|
460
451
|
async function start(deps, args) {
|
|
461
452
|
const flags = parseStackFlags(args, {
|
|
462
453
|
booleans: [
|
|
@@ -466,7 +457,7 @@ async function start(deps, args) {
|
|
|
466
457
|
"--allow-downgrade",
|
|
467
458
|
"--studio-embed-origin-reset",
|
|
468
459
|
],
|
|
469
|
-
values: ["--
|
|
460
|
+
values: ["--tenant"],
|
|
470
461
|
lists: ["--studio-embed-origin"],
|
|
471
462
|
}, START_USAGE);
|
|
472
463
|
if (flags.rest.length)
|
|
@@ -477,7 +468,6 @@ async function start(deps, args) {
|
|
|
477
468
|
noLink: flags.booleans.has("--no-link"),
|
|
478
469
|
});
|
|
479
470
|
await dockerPreflight(deps.docker);
|
|
480
|
-
await noteLegacy(deps);
|
|
481
471
|
const embedOrigins = flags.lists.get("--studio-embed-origin") ?? [];
|
|
482
472
|
const resetEmbed = flags.booleans.has("--studio-embed-origin-reset");
|
|
483
473
|
const started = await bringUp(ctx, {
|
|
@@ -490,8 +480,7 @@ async function start(deps, args) {
|
|
|
490
480
|
const tenant = await waitForTenant(ctx, started.adminUrl, started.adminKey);
|
|
491
481
|
if (ctx.projectDir)
|
|
492
482
|
await linkProject({ ...ctx, projectDir: ctx.projectDir }, started, tenant.id);
|
|
493
|
-
deps.out(`
|
|
494
|
-
deps.out(`Tenant ${tenant.id}${tenant.name ? ` (${tenant.name})` : ""}`);
|
|
483
|
+
deps.out(`Tenant ${ctx.name} (${tenant.id})`);
|
|
495
484
|
deps.out(`Runtime ${started.runtimeUrl}`);
|
|
496
485
|
if (started.studioStarted) {
|
|
497
486
|
deps.out(`Studio ${studioOrigin(started.studioPort)}`);
|
|
@@ -506,8 +495,8 @@ async function start(deps, args) {
|
|
|
506
495
|
return 0;
|
|
507
496
|
}
|
|
508
497
|
async function stop(deps, args) {
|
|
509
|
-
const usage = "nylorun stop [--
|
|
510
|
-
const flags = parseStackFlags(args, { values: ["--
|
|
498
|
+
const usage = "nylorun stop [--tenant <name>]";
|
|
499
|
+
const flags = parseStackFlags(args, { values: ["--tenant"] }, usage);
|
|
511
500
|
if (flags.rest.length)
|
|
512
501
|
throw usageError(`Usage: ${usage}`);
|
|
513
502
|
const ctx = await selectStack(deps, nameOption(flags));
|
|
@@ -516,7 +505,7 @@ async function stop(deps, args) {
|
|
|
516
505
|
const code = await deps.docker.stream(composeArgs(ctx, "stop"));
|
|
517
506
|
if (code !== 0)
|
|
518
507
|
throw new CliError(`docker compose stop failed (exit ${code}).`, 1);
|
|
519
|
-
deps.out(`Stopped
|
|
508
|
+
deps.out(`Stopped Tenant ${ctx.name}; its data is kept.`);
|
|
520
509
|
return 0;
|
|
521
510
|
}
|
|
522
511
|
async function stackStatus(ctx) {
|
|
@@ -542,7 +531,7 @@ async function stackStatus(ctx) {
|
|
|
542
531
|
const health = runtimeUrl ? await fetchHealth(ctx.deps, runtimeUrl) : undefined;
|
|
543
532
|
const host = await readHostConfig(ctx.paths);
|
|
544
533
|
const healthy = health?.status === "ok" && (host === undefined || health.hostId === host.hostId);
|
|
545
|
-
// The Admin API answers on the operator port; an older
|
|
534
|
+
// The Admin API answers on the operator port; an older Host root has only the Runtime port.
|
|
546
535
|
const adminUrl = persisted.adminPort
|
|
547
536
|
? `http://${STACK_CLIENT_HOST}:${persisted.adminPort}`
|
|
548
537
|
: runtimeUrl;
|
|
@@ -581,6 +570,7 @@ async function stackStatus(ctx) {
|
|
|
581
570
|
}
|
|
582
571
|
/** What `nylorun status` reports, as data (`nylorun doctor`). */
|
|
583
572
|
export async function readStackStatus(deps, options = {}) {
|
|
573
|
+
await moveStackRoots(nylorunRoot(deps), deps.err);
|
|
584
574
|
return await stackStatus(await selectStack(deps, options));
|
|
585
575
|
}
|
|
586
576
|
function describeTenant(tenant) {
|
|
@@ -592,8 +582,8 @@ function describeTenant(tenant) {
|
|
|
592
582
|
: `${who} unavailable (opening)`;
|
|
593
583
|
}
|
|
594
584
|
async function status(deps, args) {
|
|
595
|
-
const usage = "nylorun status [--
|
|
596
|
-
const flags = parseStackFlags(args, { booleans: ["--json"], values: ["--
|
|
585
|
+
const usage = "nylorun status [--tenant <name>] [--json]";
|
|
586
|
+
const flags = parseStackFlags(args, { booleans: ["--json"], values: ["--tenant"] }, usage);
|
|
597
587
|
if (flags.rest.length)
|
|
598
588
|
throw usageError(`Usage: ${usage}`);
|
|
599
589
|
const result = await stackStatus(await selectStack(deps, nameOption(flags)));
|
|
@@ -602,13 +592,13 @@ async function status(deps, args) {
|
|
|
602
592
|
out(JSON.stringify(result, null, 2));
|
|
603
593
|
}
|
|
604
594
|
else if (result.state === "absent") {
|
|
605
|
-
out(`
|
|
595
|
+
out(`Tenant ${result.name} absent (nothing under ${result.home}; run "nylorun start")`);
|
|
606
596
|
}
|
|
607
597
|
else {
|
|
608
|
-
out(`
|
|
598
|
+
out(`Tenant ${result.name} ${result.state} (Compose project ${result.project})`);
|
|
609
599
|
out(`Host root ${result.home} (admin key in host-credentials.json, mode 0600)`);
|
|
610
600
|
if (result.tenant)
|
|
611
|
-
out(`Tenant
|
|
601
|
+
out(`Tenant id ${describeTenant(result.tenant)}`);
|
|
612
602
|
const runtimeDetail = result.runtime.healthy
|
|
613
603
|
? `healthy, ${result.runtime.version ?? "?"}, ${result.runtime.hostId ?? "?"}`
|
|
614
604
|
: "not answering";
|
|
@@ -632,8 +622,8 @@ async function status(deps, args) {
|
|
|
632
622
|
return result.runtime.healthy ? 0 : 3;
|
|
633
623
|
}
|
|
634
624
|
async function logs(deps, args) {
|
|
635
|
-
const usage = `nylorun logs [${STACK_SERVICES.join("|")}] [--
|
|
636
|
-
const flags = parseStackFlags(args, { booleans: ["--follow"], values: ["--tail", "--
|
|
625
|
+
const usage = `nylorun logs [${STACK_SERVICES.join("|")}] [--tenant <name>] [-f] [--tail <n>]`;
|
|
626
|
+
const flags = parseStackFlags(args, { booleans: ["--follow"], values: ["--tail", "--tenant"], aliases: { "-f": "--follow" } }, usage);
|
|
637
627
|
if (flags.rest.length > 1)
|
|
638
628
|
throw usageError(`Usage: ${usage}`);
|
|
639
629
|
const service = flags.rest[0];
|
|
@@ -648,16 +638,16 @@ async function logs(deps, args) {
|
|
|
648
638
|
return await deps.docker.stream(composeArgs(ctx, "logs", ...(flags.booleans.has("--follow") ? ["--follow"] : []), ...(tail !== undefined ? ["--tail", tail] : []), ...(service ? [service] : [])));
|
|
649
639
|
}
|
|
650
640
|
async function reset(deps, args) {
|
|
651
|
-
const usage = "nylorun reset [--
|
|
652
|
-
const flags = parseStackFlags(args, { booleans: ["--yes"], values: ["--
|
|
641
|
+
const usage = "nylorun reset [--tenant <name>] [--yes]";
|
|
642
|
+
const flags = parseStackFlags(args, { booleans: ["--yes"], values: ["--tenant"], aliases: { "-y": "--yes" } }, usage);
|
|
653
643
|
if (flags.rest.length)
|
|
654
644
|
throw usageError(`Usage: ${usage}`);
|
|
655
645
|
const ctx = await selectStack(deps, nameOption(flags));
|
|
656
646
|
const { paths } = ctx;
|
|
657
647
|
if (!flags.booleans.has("--yes")) {
|
|
658
|
-
const question = `Delete
|
|
648
|
+
const question = `Delete Tenant ${ctx.name}'s volumes (Compose project ${ctx.project}), its Tenant directory ${paths.tenant} and its vault key ${paths.vaultKey}? Its data is lost, and the next start creates the Tenant anew, with a new id. [y/N] `;
|
|
659
649
|
if (!deps.confirm)
|
|
660
|
-
throw usageError(`nylorun reset deletes
|
|
650
|
+
throw usageError(`nylorun reset deletes all of Tenant ${ctx.name}'s data; pass --yes to confirm when not in a terminal.`);
|
|
661
651
|
if (!(await deps.confirm(question))) {
|
|
662
652
|
deps.err("Reset cancelled.");
|
|
663
653
|
return 1;
|
|
@@ -673,7 +663,7 @@ async function reset(deps, args) {
|
|
|
673
663
|
await mkdir(paths.tenant, { recursive: true, mode: 0o700 });
|
|
674
664
|
// The vault key goes with the Tenant's data; the next start writes a new one.
|
|
675
665
|
await rm(paths.keys, { recursive: true, force: true });
|
|
676
|
-
deps.out(`Reset
|
|
666
|
+
deps.out(`Reset Tenant ${ctx.name}: its volumes, Tenant directory and vault key deleted. Run "nylorun start" to create it anew (it relinks the project).`);
|
|
677
667
|
return 0;
|
|
678
668
|
}
|
|
679
669
|
/** Compose projects on this Docker engine and whether any container runs; undefined without Docker. */
|
|
@@ -701,13 +691,13 @@ async function ls(deps, args) {
|
|
|
701
691
|
const base = nylorunRoot(deps);
|
|
702
692
|
const projects = await composeProjects(deps.docker);
|
|
703
693
|
const state = (project) => (projects ? (projects.get(project) ?? "stopped") : "unknown");
|
|
704
|
-
const
|
|
705
|
-
for (const entry of await
|
|
694
|
+
const tenants = [];
|
|
695
|
+
for (const entry of await listTenants(base)) {
|
|
706
696
|
const persisted = (await readStackEnv(stackPaths(entry.root))) ?? {};
|
|
707
|
-
|
|
697
|
+
tenants.push({
|
|
708
698
|
name: entry.name,
|
|
709
699
|
root: entry.root,
|
|
710
|
-
...(entry.record
|
|
700
|
+
...(entry.record.project ? { project: entry.record.project } : {}),
|
|
711
701
|
state: state(`nylorun-${entry.name}`),
|
|
712
702
|
...(persisted.runtimePort
|
|
713
703
|
? { runtimeUrl: `http://${STACK_CLIENT_HOST}:${persisted.runtimePort}` }
|
|
@@ -721,39 +711,33 @@ async function ls(deps, args) {
|
|
|
721
711
|
},
|
|
722
712
|
});
|
|
723
713
|
}
|
|
724
|
-
const legacy = legacyStack(base);
|
|
725
|
-
const legacyView = legacy
|
|
726
|
-
? { root: legacy.root, project: legacy.project, state: state(legacy.project) }
|
|
727
|
-
: undefined;
|
|
728
714
|
if (flags.booleans.has("--json")) {
|
|
729
|
-
deps.out(JSON.stringify({
|
|
715
|
+
deps.out(JSON.stringify({ tenants }, null, 2));
|
|
730
716
|
return 0;
|
|
731
717
|
}
|
|
732
|
-
if (
|
|
733
|
-
deps.out(
|
|
734
|
-
|
|
735
|
-
const width = Math.max(4, ...stacks.map((stack) => stack.name.length)) + 2;
|
|
736
|
-
deps.out(`${"NAME".padEnd(width)}${"STATE".padEnd(9)}${"RUNTIME".padEnd(24)}${"STUDIO".padEnd(24)}PROJECT`);
|
|
737
|
-
for (const stack of stacks)
|
|
738
|
-
deps.out(`${stack.name.padEnd(width)}${stack.state.padEnd(9)}${(stack.runtimeUrl ?? "-").padEnd(24)}${(stack.studioUrl ?? "-").padEnd(24)}${stack.project ?? "-"}`);
|
|
718
|
+
if (tenants.length === 0) {
|
|
719
|
+
deps.out('No Tenants on this machine. Run "npx nylorun start" in a project (or anywhere, for the default Tenant).');
|
|
720
|
+
return 0;
|
|
739
721
|
}
|
|
740
|
-
|
|
741
|
-
|
|
722
|
+
const width = Math.max(6, ...tenants.map((tenant) => tenant.name.length)) + 2;
|
|
723
|
+
deps.out(`${"TENANT".padEnd(width)}${"STATE".padEnd(9)}${"RUNTIME".padEnd(24)}${"STUDIO".padEnd(24)}PROJECT`);
|
|
724
|
+
for (const tenant of tenants)
|
|
725
|
+
deps.out(`${tenant.name.padEnd(width)}${tenant.state.padEnd(9)}${(tenant.runtimeUrl ?? "-").padEnd(24)}${(tenant.studioUrl ?? "-").padEnd(24)}${tenant.project ?? "-"}`);
|
|
742
726
|
return 0;
|
|
743
727
|
}
|
|
744
728
|
async function deleteStack(deps, args) {
|
|
745
|
-
const usage = "nylorun delete <
|
|
729
|
+
const usage = "nylorun delete <tenant> --yes";
|
|
746
730
|
const flags = parseStackFlags(args, { booleans: ["--yes"], aliases: { "-y": "--yes" } }, usage);
|
|
747
731
|
const [name, ...extra] = flags.rest;
|
|
748
732
|
if (name === undefined || extra.length)
|
|
749
733
|
throw usageError(`Usage: ${usage}`);
|
|
750
|
-
|
|
734
|
+
assertTenantName(name, "The Tenant name");
|
|
751
735
|
const base = nylorunRoot(deps);
|
|
752
|
-
const ctx = { deps, project: stackProject(deps.env, name), paths: stackPaths(
|
|
736
|
+
const ctx = { deps, project: stackProject(deps.env, name), paths: stackPaths(tenantRoot(base, name)) };
|
|
753
737
|
if (!existsSync(ctx.paths.root))
|
|
754
|
-
throw new CliError(`No
|
|
738
|
+
throw new CliError(`No Tenant ${name} under ${tenantsDir(base)}. See "nylorun ls".`, 3);
|
|
755
739
|
if (!flags.booleans.has("--yes"))
|
|
756
|
-
throw usageError(`nylorun delete removes
|
|
740
|
+
throw usageError(`nylorun delete removes Tenant ${name}: its containers, volumes and Host root ${ctx.paths.root}, with its vault key (KEK) and all its data. This cannot be undone; pass --yes to confirm.`);
|
|
757
741
|
await dockerPreflight(deps.docker);
|
|
758
742
|
const files = existsSync(ctx.paths.compose) && existsSync(ctx.paths.env);
|
|
759
743
|
const code = await deps.docker.stream(files
|
|
@@ -762,55 +746,10 @@ async function deleteStack(deps, args) {
|
|
|
762
746
|
if (code !== 0)
|
|
763
747
|
throw new CliError(`docker compose down failed (exit ${code}).`, 1);
|
|
764
748
|
await rm(ctx.paths.root, { recursive: true, force: true });
|
|
765
|
-
deps.out(`Deleted
|
|
766
|
-
return 0;
|
|
767
|
-
}
|
|
768
|
-
function legacyCompose(legacy, ...args) {
|
|
769
|
-
return [
|
|
770
|
-
"compose",
|
|
771
|
-
"--project-name",
|
|
772
|
-
legacy.project,
|
|
773
|
-
"--file",
|
|
774
|
-
legacy.compose,
|
|
775
|
-
"--env-file",
|
|
776
|
-
legacy.env,
|
|
777
|
-
...args,
|
|
778
|
-
];
|
|
779
|
-
}
|
|
780
|
-
async function legacy(deps, args) {
|
|
781
|
-
const usage = "nylorun legacy stop|delete [--yes]";
|
|
782
|
-
const flags = parseStackFlags(args, { booleans: ["--yes"], aliases: { "-y": "--yes" } }, usage);
|
|
783
|
-
const [action, ...extra] = flags.rest;
|
|
784
|
-
if ((action !== "stop" && action !== "delete") || extra.length)
|
|
785
|
-
throw usageError(`Usage: ${usage}`);
|
|
786
|
-
if (action === "stop" && flags.booleans.has("--yes"))
|
|
787
|
-
throw usageError(`Usage: ${usage}`);
|
|
788
|
-
const base = nylorunRoot(deps);
|
|
789
|
-
const stack = legacyStack(base);
|
|
790
|
-
if (!stack) {
|
|
791
|
-
deps.out(`No legacy stack under ${base} (no stack/compose.yaml).`);
|
|
792
|
-
return 0;
|
|
793
|
-
}
|
|
794
|
-
if (action === "stop") {
|
|
795
|
-
await dockerPreflight(deps.docker);
|
|
796
|
-
const code = await deps.docker.stream(legacyCompose(stack, "stop"));
|
|
797
|
-
if (code !== 0)
|
|
798
|
-
throw new CliError(`docker compose stop failed (exit ${code}).`, 1);
|
|
799
|
-
deps.out("Stopped the legacy stack (Compose project nylorun); its volumes are kept.");
|
|
800
|
-
return 0;
|
|
801
|
-
}
|
|
802
|
-
if (!flags.booleans.has("--yes"))
|
|
803
|
-
throw usageError(`nylorun legacy delete removes the legacy stack (Compose project nylorun): its containers, volumes, every Tenant on it with their vault keys, and its files under ${base} (${LEGACY_ENTRIES.join(", ")}). ${stacksDir(base)} is kept. Pass --yes to confirm.`);
|
|
804
|
-
await dockerPreflight(deps.docker);
|
|
805
|
-
const code = await deps.docker.stream(legacyCompose(stack, "down", "--volumes", "--remove-orphans"));
|
|
806
|
-
if (code !== 0)
|
|
807
|
-
throw new CliError(`docker compose down failed (exit ${code}).`, 1);
|
|
808
|
-
for (const entry of LEGACY_ENTRIES)
|
|
809
|
-
await rm(join(base, entry), { recursive: true, force: true });
|
|
810
|
-
deps.out(`Deleted the legacy stack: its containers, volumes and files under ${base}; ${stacksDir(base)} is kept.`);
|
|
749
|
+
deps.out(`Deleted Tenant ${name}: its containers, volumes and Host root (vault key included).`);
|
|
811
750
|
return 0;
|
|
812
751
|
}
|
|
813
|
-
/** The
|
|
752
|
+
/** The Tenant when the Runtime (and Studio, if wanted) already answer; otherwise undefined. */
|
|
814
753
|
async function runningStack(ctx, options) {
|
|
815
754
|
if (!existsSync(ctx.paths.compose) || !existsSync(ctx.paths.env))
|
|
816
755
|
return undefined;
|
|
@@ -865,7 +804,7 @@ async function ensureSelected(ctx, options) {
|
|
|
865
804
|
};
|
|
866
805
|
}
|
|
867
806
|
/**
|
|
868
|
-
* Start the selected
|
|
807
|
+
* Start the selected Tenant unless it is already running (`nylorun studio`): the
|
|
869
808
|
* `start` code path without its output, its Tenant wait or the Project link. Compose
|
|
870
809
|
* progress still streams, since the first run pulls images.
|
|
871
810
|
*/
|
|
@@ -880,52 +819,53 @@ export function withNext(loginUrl, next) {
|
|
|
880
819
|
url.searchParams.set("next", next);
|
|
881
820
|
return url.toString();
|
|
882
821
|
}
|
|
883
|
-
/** The Studio page
|
|
822
|
+
/** The Tenant's Studio page, as a login `next` path. */
|
|
884
823
|
export function tenantStudioPath(tenantId) {
|
|
885
824
|
return `/tenants/${encodeURIComponent(tenantId)}`;
|
|
886
825
|
}
|
|
887
826
|
/**
|
|
888
|
-
* Mint a fresh Studio login URL on a running
|
|
827
|
+
* Mint a fresh Studio login URL on a running Tenant, landing on `next`.
|
|
889
828
|
* Undefined (after a warning) when Studio does not answer.
|
|
890
829
|
*/
|
|
891
|
-
export async function studioLoginUrl(deps,
|
|
892
|
-
const login = await tryStudioLogin({ deps },
|
|
830
|
+
export async function studioLoginUrl(deps, running, next) {
|
|
831
|
+
const login = await tryStudioLogin({ deps }, running.studioPort, running.adminKey);
|
|
893
832
|
return login === undefined ? undefined : withNext(login, next);
|
|
894
833
|
}
|
|
895
834
|
async function studio(deps, args, options = {}) {
|
|
896
|
-
const usage = "nylorun studio [--
|
|
897
|
-
const flags = parseStackFlags(args, { booleans: ["--no-open"], values: ["--
|
|
835
|
+
const usage = "nylorun studio [--tenant <name>] [--no-open]";
|
|
836
|
+
const flags = parseStackFlags(args, { booleans: ["--no-open"], values: ["--tenant"] }, usage);
|
|
898
837
|
if (flags.rest.length)
|
|
899
838
|
throw usageError(`Usage: ${usage}`);
|
|
900
839
|
const ctx = await selectStack(deps, nameOption(flags));
|
|
901
|
-
const
|
|
902
|
-
if (
|
|
903
|
-
deps.out(`Runtime ${
|
|
904
|
-
if (!
|
|
840
|
+
const running = await ensureSelected(ctx, { studio: true });
|
|
841
|
+
if (running.started)
|
|
842
|
+
deps.out(`Runtime ${running.runtimeUrl}`);
|
|
843
|
+
if (!running.studioUp)
|
|
905
844
|
throw new CliError(`Studio did not start. See "nylorun logs studio".`, 7);
|
|
906
|
-
// Studio serves the
|
|
845
|
+
// Studio serves the one Tenant; the link's Tenant id stands in while it is opening.
|
|
907
846
|
let next = options.next;
|
|
908
847
|
if (next === undefined) {
|
|
909
|
-
const tenant = await fetchTenant(deps,
|
|
910
|
-
const linked = ctx.link?.
|
|
848
|
+
const tenant = await fetchTenant(deps, running.adminUrl, running.adminKey);
|
|
849
|
+
const linked = ctx.link?.tenant === ctx.name ? ctx.link.tenantId : undefined;
|
|
911
850
|
const tenantId = tenant?.id ?? linked;
|
|
912
851
|
if (tenantId)
|
|
913
852
|
next = tenantStudioPath(tenantId);
|
|
914
853
|
}
|
|
915
|
-
const login = await studioLoginUrl(deps,
|
|
854
|
+
const login = await studioLoginUrl(deps, running, next);
|
|
916
855
|
if (!login)
|
|
917
856
|
return 1;
|
|
918
857
|
if (flags.booleans.has("--no-open")) {
|
|
919
858
|
deps.out(`Studio ${login}`);
|
|
920
859
|
return 0;
|
|
921
860
|
}
|
|
922
|
-
const origin = studioOrigin(
|
|
861
|
+
const origin = studioOrigin(running.studioPort);
|
|
923
862
|
deps.out(`Studio ${next ? new URL(next, origin).toString() : origin}`);
|
|
924
863
|
await openLogin({ deps }, login);
|
|
925
864
|
return 0;
|
|
926
865
|
}
|
|
927
|
-
/** `nylorun studio`, landing on `next` when given, else on the
|
|
866
|
+
/** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
|
|
928
867
|
export async function runStudioCommand(args, deps, options = {}) {
|
|
868
|
+
await moveStackRoots(nylorunRoot(deps), deps.err);
|
|
929
869
|
return await studio(deps, args, options);
|
|
930
870
|
}
|
|
931
871
|
const COMMANDS = {
|
|
@@ -940,15 +880,15 @@ const COMMANDS = {
|
|
|
940
880
|
studio: (deps, args) => studio(deps, args),
|
|
941
881
|
ls,
|
|
942
882
|
delete: deleteStack,
|
|
943
|
-
legacy,
|
|
944
883
|
};
|
|
945
884
|
export function isStackCommand(name) {
|
|
946
885
|
return name !== undefined && Object.hasOwn(COMMANDS, name);
|
|
947
886
|
}
|
|
948
|
-
/** Run one
|
|
887
|
+
/** Run one Tenant command (`up`/`start`, `down`/`stop`, `status`, `logs`, `reset`, `studio`, `ls`, `delete`). */
|
|
949
888
|
export async function runStackCommand(name, args, deps) {
|
|
950
889
|
const command = COMMANDS[name];
|
|
951
890
|
if (!command)
|
|
952
|
-
throw usageError(`Unknown
|
|
891
|
+
throw usageError(`Unknown command ${name}.\n${stackUsage}`);
|
|
892
|
+
await moveStackRoots(nylorunRoot(deps), deps.err);
|
|
953
893
|
return await command(deps, args);
|
|
954
894
|
}
|