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.
@@ -1,83 +1,85 @@
1
1
  import { existsSync } from "node:fs";
2
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { basename, join, resolve } from "node:path";
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 { assertStackName, chooseStackName, defaultNylorunRoot, LEGACY_ENTRIES, legacyStack, listStacks, readStackRecord, sanitizeStackName, stackRoot, stacksDir, writeStackRecord, } from "./stacks.js";
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 [--name <stack>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset]
20
- start the project's stack, creating it, its one Tenant and the Project link
21
- (.nylorun/link.json, credentials.json) on the first run; print the Runtime and Studio URLs
22
- and open Studio signed in (in a terminal). --name attaches to (or creates) a named stack;
23
- --no-link starts a stack without linking the current directory
24
- down|stop [--name <stack>] stop the stack's containers; keep volumes
25
- status [--name <stack>] [--json] the stack, its Tenant, services, endpoints and Runtime health
26
- logs [service] [--name <stack>] [-f] [--tail <n>]
27
- stack logs (${STACK_SERVICES.join(", ")})
28
- studio [--name <stack>] [--no-open] sign a browser in to Studio on the stack's Tenant; --no-open prints the login URL; start the stack if it is stopped
29
- reset [--name <stack>] [--yes] delete the stack's volumes and its Tenant directory; the next start creates a new Tenant
30
- ls [--json] the stacks on this machine
31
- delete <stack> --yes remove a stack: containers, volumes and its Host root, with the vault key (KEK)
32
- legacy stop|delete [--yes] stop or remove the single stack of older releases (Compose project nylorun)`;
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 stack `name`: `nylorun-<name>`, or `NYLORUN_STACK_PROJECT`. */
35
+ /** The Compose project of Tenant `name`: `nylorun-<name>`, or `NYLORUN_COMPOSE_PROJECT`. */
35
36
  export function stackProject(env, name) {
36
- const override = env.NYLORUN_STACK_PROJECT?.trim();
37
+ const override = env.NYLORUN_COMPOSE_PROJECT?.trim();
37
38
  if (!override)
38
39
  return `nylorun-${name}`;
39
- return assertStackName(override, "NYLORUN_STACK_PROJECT");
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 stackNames(deps) {
45
- const names = (await listStacks(nylorunRoot(deps))).map((stack) => stack.name);
46
- return names.length ? ` Stacks on this machine: ${names.join(", ")}.` : "";
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 stack a command acts on: `--name`, then `NYLORUN_STACK`, then the stack of the
50
- * project's link (format 2); for `start` in a project, then a name from the project
51
- * directory (`chooseStackName`). `NYLORUN_HOME` sets the Host root whatever the name; its
52
- * name otherwise comes from its `stack.json`, the project directory or the Host root's own.
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.NYLORUN_STACK?.trim() || undefined);
61
+ const explicit = options.name ?? (env.NYLORUN_TENANT?.trim() || undefined);
60
62
  if (explicit !== undefined)
61
- assertStackName(explicit, options.name !== undefined ? "--name" : "NYLORUN_STACK");
62
- let name = explicit ?? (link?.format === 2 ? link.stack : undefined);
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 readStackRecord(root))?.name ??
69
- sanitizeStackName(basename(projectDir ?? root));
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 chooseStackName(base, projectDir);
75
+ name = await chooseTenantName(base, projectDir);
76
+ if (name === undefined && !projectDir)
77
+ name = DEFAULT_TENANT;
74
78
  if (name === undefined)
75
- throw usageError(options.start
76
- ? `Not in a project: name the stack with "nylorun start --name <stack>" (or NYLORUN_STACK).${await stackNames(deps)}`
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
- assertStackName(name, "The stack name");
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 Nylorun stack ${ctx.name} under ${ctx.paths.root}. Run "nylorun start" first.`, 3);
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
- /** `--name` when given. */
142
+ /** `--tenant` when given. */
141
143
  function nameOption(flags) {
142
- const name = flags.values.get("--name");
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 stack's Tenant is unavailable, with the repair the Runtime names. */
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 stack's database: update nylorun (npx nylorun@latest start).`
212
+ ? ` This nylorun pins Runtime ${ctx.deps.runtimeVersion}, older than the Tenant's database: update nylorun (npx nylorun@latest start).`
211
213
  : "";
212
- return `The Tenant of stack ${ctx.name} is unavailable (${cause.code}): ${cause.message} ${cause.repair}${hint} See "nylorun logs runtime".`;
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 stack's Tenant is open: the Runtime creates it on the first start and opens
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(`The Tenant of stack ${ctx.name} did not open (${adminUrl}/v1/admin/status: ${tenant ? tenant.state : "no answer"}). See "nylorun status" and "nylorun logs runtime".`, 7);
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 stack's database: the Runtime that last ran the stack
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 stack ${ctx.name}, which Runtime ${host.runtimeVersion} last ran. If that Runtime migrated the database, the Tenant stays unavailable (schema-too-new).`);
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 stack ${ctx.name}: Runtime ${host.runtimeVersion} last ran it (${ctx.paths.config}), and a Runtime older than the stack'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);
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 stacks (and the legacy stack) keep in their `.env`, so a new stack avoids them. */
288
+ /** Ports other Tenants keep in their `.env`, so a new Tenant avoids them. */
287
289
  async function reservedPorts(ctx) {
288
- const base = nylorunRoot(ctx.deps);
289
- const others = (await listStacks(base))
290
- .filter((stack) => resolve(stack.root) !== ctx.paths.root)
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
- const record = await readStackRecord(ctx.paths.root);
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 writeStackRecord(ctx.paths.root, { name: ctx.name, ...(project ? { project } : {}) });
342
+ await writeTenantRecord(ctx.paths.root, { name: ctx.name, ...(project ? { project } : {}) });
339
343
  if (prepared.firstRun)
340
- deps.err(`Created stack ${ctx.name} under ${ctx.paths.root} (Runtime port ${prepared.env.runtimePort}, Studio port ${prepared.env.studioPort}).`);
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 stack: `.nylorun/link.json` (format 2) and
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 stack, its URL, Host or Tenant changed; a new link seeds 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 !== 2 ||
422
- link.stack !== ctx.name ||
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
- stack: ctx.name,
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 stack ${ctx.name} (.nylorun/link.json, .nylorun/credentials.json).`);
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 [--name <stack>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset]";
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: ["--name"],
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(`Stack ${ctx.name} (${ctx.paths.root})`);
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 [--name <stack>]";
510
- const flags = parseStackFlags(args, { values: ["--name"] }, usage);
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 stack ${ctx.name}; volumes are kept.`);
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 stack has only the Runtime port.
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 [--name <stack>] [--json]";
596
- const flags = parseStackFlags(args, { booleans: ["--json"], values: ["--name"] }, usage);
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(`Stack ${result.name} absent (nothing under ${result.home}; run "nylorun start")`);
595
+ out(`Tenant ${result.name} absent (nothing under ${result.home}; run "nylorun start")`);
606
596
  }
607
597
  else {
608
- out(`Stack ${result.name} ${result.state} (Compose project ${result.project})`);
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 ${describeTenant(result.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("|")}] [--name <stack>] [-f] [--tail <n>]`;
636
- const flags = parseStackFlags(args, { booleans: ["--follow"], values: ["--tail", "--name"], aliases: { "-f": "--follow" } }, usage);
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 [--name <stack>] [--yes]";
652
- const flags = parseStackFlags(args, { booleans: ["--yes"], values: ["--name"], aliases: { "-y": "--yes" } }, usage);
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 stack ${ctx.name}'s volumes (Compose project ${ctx.project}) and its Tenant directory ${paths.tenant} and vault key ${paths.vaultKey}? Its Tenant's data is lost and the next start creates a new Tenant. [y/N] `;
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 stack ${ctx.name}'s Tenant and all its data; pass --yes to confirm when not in a terminal.`);
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 stack ${ctx.name}: volumes and the Tenant directory deleted. Run "nylorun start" to start again with a new Tenant (it relinks the project).`);
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 stacks = [];
705
- for (const entry of await listStacks(base)) {
694
+ const tenants = [];
695
+ for (const entry of await listTenants(base)) {
706
696
  const persisted = (await readStackEnv(stackPaths(entry.root))) ?? {};
707
- stacks.push({
697
+ tenants.push({
708
698
  name: entry.name,
709
699
  root: entry.root,
710
- ...(entry.record?.project ? { project: entry.record.project } : {}),
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({ stacks, ...(legacyView ? { legacy: legacyView } : {}) }, null, 2));
715
+ deps.out(JSON.stringify({ tenants }, null, 2));
730
716
  return 0;
731
717
  }
732
- if (stacks.length === 0)
733
- deps.out(`No stacks under ${stacksDir(base)}. Run "npx nylorun start" in a project.`);
734
- else {
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
- if (legacyView)
741
- deps.out(`Legacy stack: Compose project nylorun, Host root ${legacyView.root} (${legacyView.state}); "nylorun legacy stop|delete" handles it.`);
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 <stack> --yes";
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
- assertStackName(name, "The stack name");
734
+ assertTenantName(name, "The Tenant name");
751
735
  const base = nylorunRoot(deps);
752
- const ctx = { deps, project: stackProject(deps.env, name), paths: stackPaths(stackRoot(base, name)) };
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 stack ${name} under ${stacksDir(base)}. See "nylorun ls".`, 3);
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 stack ${name}: its containers, volumes and Host root ${ctx.paths.root}, with the Tenant's vault key (KEK) and all its data. This cannot be undone; pass --yes to confirm.`);
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 stack ${name}: its containers, volumes and Host root (vault key included).`);
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 stack when the Runtime (and Studio, if wanted) already answer; otherwise undefined. */
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 stack unless it is already running (`nylorun studio`): the
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 of the stack's Tenant, as a login `next` path. */
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 stack, landing on `next`.
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, stack, next) {
892
- const login = await tryStudioLogin({ deps }, stack.studioPort, stack.adminKey);
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 [--name <stack>] [--no-open]";
897
- const flags = parseStackFlags(args, { booleans: ["--no-open"], values: ["--name"] }, usage);
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 stack = await ensureSelected(ctx, { studio: true });
902
- if (stack.started)
903
- deps.out(`Runtime ${stack.runtimeUrl}`);
904
- if (!stack.studioUp)
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 stack's one Tenant; the link's Tenant id stands in while it is opening.
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, stack.adminUrl, stack.adminKey);
910
- const linked = ctx.link?.format === 2 && ctx.link.stack === ctx.name ? ctx.link.tenantId : undefined;
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, stack, next);
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(stack.studioPort);
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 stack's Tenant. */
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 stack command (`up`/`start`, `down`/`stop`, `status`, `logs`, `reset`, `studio`, `ls`, `delete`, `legacy`). */
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 stack command ${name}.\n${stackUsage}`);
891
+ throw usageError(`Unknown command ${name}.\n${stackUsage}`);
892
+ await moveStackRoots(nylorunRoot(deps), deps.err);
953
893
  return await command(deps, args);
954
894
  }