@nylorun/cli 0.2.0-beta → 0.3.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +87 -40
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/cli.js +132 -220
  5. package/dist/dev-entry.d.ts +5 -1
  6. package/dist/dev-entry.js +7 -6
  7. package/dist/dev.d.ts +45 -4
  8. package/dist/dev.js +245 -32
  9. package/dist/doctor.d.ts +25 -3
  10. package/dist/doctor.js +104 -16
  11. package/dist/environment.d.ts +4 -1
  12. package/dist/environment.js +12 -3
  13. package/dist/launcher.d.ts +5 -5
  14. package/dist/launcher.js +6 -3
  15. package/dist/model/configure.d.ts +30 -0
  16. package/dist/model/configure.js +117 -11
  17. package/dist/model/host-model.d.ts +5 -3
  18. package/dist/model/host-model.js +36 -15
  19. package/dist/project/attach.d.ts +32 -0
  20. package/dist/project/attach.js +241 -0
  21. package/dist/project/create-tenant.d.ts +26 -0
  22. package/dist/project/create-tenant.js +65 -0
  23. package/dist/project/credentials.d.ts +18 -0
  24. package/dist/project/credentials.js +64 -0
  25. package/dist/project/link.d.ts +20 -0
  26. package/dist/project/link.js +77 -0
  27. package/dist/project/root.d.ts +9 -0
  28. package/dist/project/root.js +51 -0
  29. package/dist/project/seed.d.ts +19 -0
  30. package/dist/project/seed.js +94 -0
  31. package/dist/runtime/baseline.d.ts +2 -0
  32. package/dist/runtime/baseline.js +11 -0
  33. package/dist/runtime/commands.d.ts +16 -0
  34. package/dist/runtime/commands.js +292 -0
  35. package/dist/runtime/ephemeral.d.ts +37 -0
  36. package/dist/runtime/ephemeral.js +84 -0
  37. package/dist/runtime/exit-codes.d.ts +9 -0
  38. package/dist/runtime/exit-codes.js +43 -0
  39. package/dist/runtime/index.d.ts +8 -0
  40. package/dist/runtime/index.js +6 -0
  41. package/dist/runtime/launcher.d.ts +81 -0
  42. package/dist/runtime/launcher.js +243 -0
  43. package/dist/runtime/progress.d.ts +10 -0
  44. package/dist/runtime/progress.js +8 -0
  45. package/dist/runtime/version.d.ts +6 -0
  46. package/dist/runtime/version.js +27 -0
  47. package/dist/tenant/commands.d.ts +4 -0
  48. package/dist/tenant/commands.js +262 -0
  49. package/package.json +6 -3
  50. package/dist/project-runner.d.ts +0 -20
  51. package/dist/project-runner.js +0 -141
  52. package/dist/runtime-host.d.ts +0 -39
  53. package/dist/runtime-host.js +0 -355
  54. package/dist/scope.d.ts +0 -65
  55. package/dist/scope.js +0 -191
@@ -1,5 +1,7 @@
1
1
  import { createInterface } from "node:readline/promises";
2
- import { modelsFor } from "@nylorun/runtime/configuration";
2
+ import { builtinModels } from "@earendil-works/pi-ai/providers/all";
3
+ import { createProvider, } from "@earendil-works/pi-ai";
4
+ import { stream, streamSimple, } from "@earendil-works/pi-ai/api/openai-completions";
3
5
  export class ConfigurationCancelled extends Error {
4
6
  signal;
5
7
  exitCode;
@@ -9,6 +11,112 @@ export class ConfigurationCancelled extends Error {
9
11
  this.exitCode = signal === "SIGINT" ? 130 : 143;
10
12
  }
11
13
  }
14
+ /**
15
+ * Build a pi-ai registry for interactive login. Catalog listing prefers the
16
+ * Tenant API payload when provided (F2-4).
17
+ */
18
+ function modelsFor(selection, credentials, catalog) {
19
+ const models = builtinModels({ credentials });
20
+ if (selection.custom) {
21
+ const model = {
22
+ id: selection.model,
23
+ name: selection.model,
24
+ api: "openai-completions",
25
+ provider: "custom",
26
+ baseUrl: selection.custom.baseUrl,
27
+ reasoning: false,
28
+ input: ["text", "image"],
29
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
30
+ contextWindow: 128000,
31
+ maxTokens: 16384,
32
+ };
33
+ models.setProvider(createProvider({
34
+ id: "custom",
35
+ name: "Custom OpenAI-compatible",
36
+ baseUrl: selection.custom.baseUrl,
37
+ auth: {
38
+ apiKey: {
39
+ label: "Custom API key",
40
+ schemas: [],
41
+ async resolve() {
42
+ const credential = await credentials.read("custom");
43
+ if (!credential || credential.type !== "api_key")
44
+ return undefined;
45
+ const key = credential.key;
46
+ if (!key)
47
+ return undefined;
48
+ return { type: "api_key", auth: { apiKey: key } };
49
+ },
50
+ },
51
+ },
52
+ models: [model],
53
+ api: { stream, streamSimple },
54
+ }));
55
+ }
56
+ return {
57
+ models,
58
+ listProviders() {
59
+ if (catalog?.providers.length) {
60
+ return catalog.providers.map((provider) => {
61
+ const live = models.getProvider(provider.id);
62
+ return {
63
+ id: provider.id,
64
+ name: provider.name,
65
+ auth: {
66
+ ...(live?.auth?.apiKey || provider.auth?.apiKey
67
+ ? { apiKey: live?.auth?.apiKey ?? {} }
68
+ : {}),
69
+ ...(live?.auth?.oauth || provider.auth?.oauth
70
+ ? { oauth: live?.auth?.oauth ?? {} }
71
+ : {}),
72
+ ...(!live?.auth?.apiKey &&
73
+ !live?.auth?.oauth &&
74
+ !provider.auth?.apiKey &&
75
+ !provider.auth?.oauth
76
+ ? { apiKey: {} }
77
+ : {}),
78
+ },
79
+ };
80
+ });
81
+ }
82
+ return models.getProviders().map((provider) => ({
83
+ id: provider.id,
84
+ name: provider.name,
85
+ auth: {
86
+ ...(provider.auth?.apiKey ? { apiKey: provider.auth.apiKey } : {}),
87
+ ...(provider.auth?.oauth ? { oauth: provider.auth.oauth } : {}),
88
+ },
89
+ }));
90
+ },
91
+ listModels(providerId) {
92
+ const fromCatalog = catalog?.providers.find((p) => p.id === providerId);
93
+ if (fromCatalog?.models.length)
94
+ return fromCatalog.models;
95
+ return models.getModels(providerId).map((model) => ({
96
+ id: model.id,
97
+ name: model.name,
98
+ }));
99
+ },
100
+ };
101
+ }
102
+ /** Fetch GET /v1/tenant/models when a connection is available. */
103
+ export async function fetchModelCatalog(options) {
104
+ const fetchImpl = options.fetchImpl ?? fetch;
105
+ const { PROTOCOL_HEADER, PROTOCOL_VERSION, TENANT_HEADER } = await import("@nylorun/agents");
106
+ const response = await fetchImpl(`${options.url.replace(/\/$/, "")}/v1/tenant/models`, {
107
+ headers: {
108
+ authorization: `Bearer ${options.key}`,
109
+ [TENANT_HEADER]: options.tenantId,
110
+ [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
111
+ Accept: "application/json",
112
+ },
113
+ signal: AbortSignal.timeout(10_000),
114
+ });
115
+ if (!response.ok) {
116
+ throw new Error(`Runtime model catalog returned ${response.status}`);
117
+ }
118
+ return (await response.json());
119
+ }
12
120
  // Internal options also allow isolated prompt tests without changing process globals.
13
121
  export async function configureProvider(options = {}) {
14
122
  const output = options.output ?? process.stdout;
@@ -34,10 +142,8 @@ export async function configureProvider(options = {}) {
34
142
  return next ?? captured;
35
143
  },
36
144
  };
37
- const models = modelsFor({ provider: "", model: "" }, store, {
38
- environment: false,
39
- });
40
- const providers = models.getProviders();
145
+ const registry = modelsFor({ provider: "", model: "" }, store, options.catalog);
146
+ const providers = registry.listProviders();
41
147
  const prompt = createInterface({
42
148
  input: options.input ?? process.stdin,
43
149
  output,
@@ -72,21 +178,21 @@ export async function configureProvider(options = {}) {
72
178
  model,
73
179
  custom: { baseUrl },
74
180
  };
75
- const customModels = modelsFor(selection, store, { environment: false });
76
- if (!(await customModels.checkAuth("custom", { signal })))
77
- await customModels.login("custom", "api_key", interaction());
181
+ const custom = modelsFor(selection, store, options.catalog);
182
+ if (!(await custom.models.checkAuth("custom", { signal })))
183
+ await custom.models.login("custom", "api_key", interaction());
78
184
  return prompted(selection);
79
185
  }
80
186
  else {
81
187
  const chosen = providers[choice - 1];
82
188
  if (!chosen)
83
189
  throw new Error("Choose a listed provider.");
84
- const available = models.getModels(chosen.id);
190
+ const available = registry.listModels(chosen.id);
85
191
  available.forEach((model, index) => output.write(`${index + 1}. ${model.name} (${model.id})\n`));
86
192
  const model = available[Number(await question("Choose a model: ")) - 1];
87
193
  if (!model)
88
194
  throw new Error("Choose a listed model.");
89
- if (!(await models.checkAuth(chosen.id, { signal }))) {
195
+ if (!(await registry.models.checkAuth(chosen.id, { signal }))) {
90
196
  let method = chosen.auth.apiKey
91
197
  ? "api_key"
92
198
  : "oauth";
@@ -97,7 +203,7 @@ export async function configureProvider(options = {}) {
97
203
  if (answer === "2")
98
204
  method = "oauth";
99
205
  }
100
- await models.login(chosen.id, method, interaction());
206
+ await registry.models.login(chosen.id, method, interaction());
101
207
  }
102
208
  return prompted({ provider: chosen.id, model: model.id });
103
209
  }
@@ -1,5 +1,5 @@
1
1
  import { type PromptedModel } from "./configure.js";
2
- /** Wire shape of GET/PUT `/v1/host/model` — local copy so CLI stays off `@nylorun/core`. */
2
+ /** Wire shape of GET/PUT `/v1/tenant/model` — local copy so CLI stays off `@nylorun/core`. */
3
3
  type HostModelView = {
4
4
  readonly configured: false;
5
5
  } | {
@@ -9,12 +9,14 @@ type HostModelView = {
9
9
  readonly authType: "api_key" | "oauth";
10
10
  readonly baseUrl?: string;
11
11
  };
12
- export declare function getHostModel(runtimeUrl: string, serverKey: string): Promise<HostModelView>;
13
- export declare function putHostModel(runtimeUrl: string, serverKey: string, model: PromptedModel): Promise<HostModelView>;
12
+ export declare function getHostModel(runtimeUrl: string, serverKey: string, tenantId?: string): Promise<HostModelView>;
13
+ export declare function putHostModel(runtimeUrl: string, serverKey: string, model: PromptedModel, tenantId?: string): Promise<HostModelView>;
14
14
  export declare function ensureHostModel(options: {
15
15
  runtimeUrl: string;
16
16
  serverKey: string;
17
+ tenantId: string;
17
18
  root?: string;
19
+ env?: Readonly<Record<string, string>>;
18
20
  signal?: AbortSignal;
19
21
  }): Promise<void>;
20
22
  export {};
@@ -1,20 +1,38 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { PROTOCOL_HEADER, PROTOCOL_VERSION, TENANT_HEADER, } from "@nylorun/agents";
4
5
  import { configureProvider } from "./configure.js";
5
- export async function getHostModel(runtimeUrl, serverKey) {
6
- const response = await fetch(`${runtimeUrl}/v1/host/model`, {
7
- headers: { authorization: `Bearer ${serverKey}` },
6
+ import { loadProjectEnvironment } from "../environment.js";
7
+ function tenantHeaders(serverKey, tenantId) {
8
+ return {
9
+ authorization: `Bearer ${serverKey}`,
10
+ [TENANT_HEADER]: tenantId,
11
+ [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
12
+ };
13
+ }
14
+ function resolveTenantId(explicit) {
15
+ const tenant = explicit?.trim() || process.env.NYLORUN_TENANT?.trim();
16
+ if (!tenant) {
17
+ throw new Error("Set NYLORUN_TENANT (or pass tenant via Project link) before calling Tenant model routes");
18
+ }
19
+ return tenant;
20
+ }
21
+ export async function getHostModel(runtimeUrl, serverKey, tenantId) {
22
+ const tenant = resolveTenantId(tenantId);
23
+ const response = await fetch(`${runtimeUrl}/v1/tenant/model`, {
24
+ headers: tenantHeaders(serverKey, tenant),
8
25
  });
9
26
  if (!response.ok)
10
27
  throw new Error(`Runtime model status returned ${response.status}`);
11
28
  return response.json();
12
29
  }
13
- export async function putHostModel(runtimeUrl, serverKey, model) {
14
- const response = await fetch(`${runtimeUrl}/v1/host/model`, {
30
+ export async function putHostModel(runtimeUrl, serverKey, model, tenantId) {
31
+ const tenant = resolveTenantId(tenantId);
32
+ const response = await fetch(`${runtimeUrl}/v1/tenant/model`, {
15
33
  method: "PUT",
16
34
  headers: {
17
- authorization: `Bearer ${serverKey}`,
35
+ ...tenantHeaders(serverKey, tenant),
18
36
  "content-type": "application/json",
19
37
  },
20
38
  body: JSON.stringify({
@@ -32,12 +50,15 @@ export async function putHostModel(runtimeUrl, serverKey, model) {
32
50
  return body;
33
51
  }
34
52
  export async function ensureHostModel(options) {
35
- const current = await getHostModel(options.runtimeUrl, options.serverKey);
53
+ const current = await getHostModel(options.runtimeUrl, options.serverKey, options.tenantId);
36
54
  if (current.configured)
37
55
  return;
38
- const seeded = seedFromProject(options.root ?? process.cwd());
56
+ const env = options.env ?? loadProjectEnvironment(options.root ?? process.cwd());
57
+ if (env.NYLORUN_DEV_MODEL?.trim() === "fixture")
58
+ return;
59
+ const seeded = seedFromEnv(env, options.root ?? process.cwd());
39
60
  if (seeded) {
40
- await putHostModel(options.runtimeUrl, options.serverKey, seeded);
61
+ await putHostModel(options.runtimeUrl, options.serverKey, seeded, options.tenantId);
41
62
  return;
42
63
  }
43
64
  if (process.stdin.isTTY && process.stdout.isTTY) {
@@ -45,18 +66,18 @@ export async function ensureHostModel(options) {
45
66
  root: options.root,
46
67
  signal: options.signal,
47
68
  });
48
- await putHostModel(options.runtimeUrl, options.serverKey, prompted);
69
+ await putHostModel(options.runtimeUrl, options.serverKey, prompted, options.tenantId);
49
70
  return;
50
71
  }
51
72
  throw new Error("Model provider is not configured. Run nylorun dev in a terminal to set it up, or open Studio.");
52
73
  }
53
- function seedFromProject(root) {
54
- const provider = process.env.MODEL_PROVIDER?.trim();
55
- const model = process.env.MODEL?.trim();
74
+ function seedFromEnv(env, root) {
75
+ const provider = env.MODEL_PROVIDER?.trim();
76
+ const model = env.MODEL?.trim();
56
77
  if (!provider || !model)
57
78
  return undefined;
58
- const baseUrl = process.env.MODEL_PROVIDER_BASE_URL?.trim();
59
- const key = process.env.MODEL_PROVIDER_API_KEY;
79
+ const baseUrl = env.MODEL_PROVIDER_BASE_URL?.trim();
80
+ const key = env.MODEL_PROVIDER_API_KEY;
60
81
  const auth = key
61
82
  ? { type: "api_key", key }
62
83
  : legacyCredential(root, provider);
@@ -0,0 +1,32 @@
1
+ import { type ProjectCredentials } from "./credentials.js";
2
+ import { type ProjectLink } from "./link.js";
3
+ export interface UpResult {
4
+ url: string;
5
+ hostId: string;
6
+ pid: number;
7
+ version: string;
8
+ started: boolean;
9
+ }
10
+ export interface AttachedProject {
11
+ projectRoot: string;
12
+ link: ProjectLink;
13
+ credentials: ProjectCredentials;
14
+ host: UpResult;
15
+ tenantName: string;
16
+ hostStarted: boolean;
17
+ home: string;
18
+ }
19
+ export interface AttachOptions {
20
+ projectRoot: string;
21
+ home?: string;
22
+ /** Interactive stdin; defaults to process.stdin when TTY. */
23
+ input?: NodeJS.ReadableStream;
24
+ output?: NodeJS.WritableStream;
25
+ fetchImpl?: typeof fetch;
26
+ }
27
+ /**
28
+ * Ensure the local Runtime is up and a Project link authenticates (D§12 step 2).
29
+ */
30
+ export declare function attachProject(options: AttachOptions): Promise<AttachedProject>;
31
+ /** Three `export` lines for `runtime status --env` (F2-7). */
32
+ export declare function printLinkedEnvExports(projectRoot?: string): Promise<void>;
@@ -0,0 +1,241 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { createAdmin } from "@nylorun/admin";
3
+ import { PROTOCOL_HEADER, PROTOCOL_VERSION, TENANT_HEADER, } from "@nylorun/agents";
4
+ import { CliError } from "../errors.js";
5
+ import { launcher, resolveHome, throwOnLauncherFailure, } from "../runtime/launcher.js";
6
+ import { readCredentials, removeCredentials, } from "./credentials.js";
7
+ import { createProjectTenant } from "./create-tenant.js";
8
+ import { readLink, removeLink, writeLink, } from "./link.js";
9
+ function tenantHeaders(key, tenantId) {
10
+ return {
11
+ authorization: `Bearer ${key}`,
12
+ [TENANT_HEADER]: tenantId,
13
+ [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
14
+ };
15
+ }
16
+ async function probeTenant(hostUrl, tenantId, applicationKey, fetchImpl) {
17
+ try {
18
+ const response = await fetchImpl(`${hostUrl}/v1/tenant`, {
19
+ headers: tenantHeaders(applicationKey, tenantId),
20
+ signal: AbortSignal.timeout(5_000),
21
+ });
22
+ return response.ok ? "ok" : "rejected";
23
+ }
24
+ catch {
25
+ return "rejected";
26
+ }
27
+ }
28
+ async function healthHostId(hostUrl, fetchImpl) {
29
+ try {
30
+ const response = await fetchImpl(`${hostUrl}/health`, {
31
+ signal: AbortSignal.timeout(3_000),
32
+ });
33
+ if (!response.ok)
34
+ return undefined;
35
+ const body = (await response.json());
36
+ return typeof body.hostId === "string" ? body.hostId : undefined;
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ /**
43
+ * Ensure the local Runtime is up and a Project link authenticates (D§12 step 2).
44
+ */
45
+ export async function attachProject(options) {
46
+ const fetchImpl = options.fetchImpl ?? fetch;
47
+ const projectRoot = options.projectRoot;
48
+ const home = resolveHome(options.home);
49
+ const handle = await launcher(home);
50
+ const upOutcome = await handle.invoke(["up"]);
51
+ throwOnLauncherFailure(upOutcome);
52
+ const host = upResultFromInvoke(upOutcome);
53
+ const hostUrl = host.url.replace(/\/$/, "");
54
+ const hostId = host.hostId ||
55
+ (await healthHostId(hostUrl, fetchImpl)) ||
56
+ "";
57
+ if (!hostId) {
58
+ throw new CliError(`Runtime Host at ${hostUrl} did not report a hostId. Restart with "nylorun runtime restart".`, 1);
59
+ }
60
+ const admin = createAdmin({ home });
61
+ let link = await readLink(projectRoot);
62
+ let credentials = await readCredentials(projectRoot);
63
+ if (!link || !credentials) {
64
+ const created = await createProjectTenant({
65
+ admin,
66
+ hostId,
67
+ hostUrl,
68
+ projectRoot,
69
+ });
70
+ return {
71
+ projectRoot,
72
+ link: created.link,
73
+ credentials: created.credentials,
74
+ host,
75
+ tenantName: created.envelope.name,
76
+ hostStarted: host.started,
77
+ home,
78
+ };
79
+ }
80
+ if (link.hostUrl.replace(/\/$/, "") !== hostUrl) {
81
+ const liveId = await healthHostId(hostUrl, fetchImpl);
82
+ if (liveId && liveId === link.hostId) {
83
+ link = { ...link, hostUrl, format: 1 };
84
+ await writeLink(projectRoot, link);
85
+ }
86
+ }
87
+ const liveId = await healthHostId(hostUrl, fetchImpl);
88
+ if (liveId && liveId !== link.hostId) {
89
+ throw new CliError(`Project link points at Host ${link.hostId}, but ${hostUrl} is ${liveId}. Relink: remove .nylorun/link.json and run nylorun dev.`, 1);
90
+ }
91
+ const auth = await probeTenant(hostUrl, link.tenantId, credentials.applicationKey, fetchImpl);
92
+ if (auth === "ok") {
93
+ const tenants = await admin.listTenants();
94
+ const match = tenants.find((t) => t.id === link.tenantId);
95
+ return {
96
+ projectRoot,
97
+ link: { ...link, hostUrl, hostId: liveId ?? link.hostId, format: 1 },
98
+ credentials,
99
+ host,
100
+ tenantName: match?.name ?? match?.envelope?.name ?? link.tenantId,
101
+ hostStarted: host.started,
102
+ home,
103
+ };
104
+ }
105
+ const tenants = await admin.listTenants();
106
+ const known = tenants.some((t) => t.id === link.tenantId);
107
+ if (known) {
108
+ throw new CliError(`Application credential rejected for Tenant ${link.tenantId} on Host ${link.hostId}. Relink: remove .nylorun/link.json and .nylorun/credentials.json, then run nylorun dev.`, 1);
109
+ }
110
+ const choice = await chooseUnknownTenantRecovery({
111
+ tenantId: link.tenantId,
112
+ tenants,
113
+ input: options.input,
114
+ output: options.output,
115
+ });
116
+ if (choice === "remove") {
117
+ await removeLink(projectRoot);
118
+ await removeCredentials(projectRoot);
119
+ throw new CliError("Removed Project link. Run nylorun dev again to create a new Tenant.", 1);
120
+ }
121
+ if (choice === "create") {
122
+ await removeLink(projectRoot);
123
+ await removeCredentials(projectRoot);
124
+ const created = await createProjectTenant({
125
+ admin,
126
+ hostId: liveId ?? hostId,
127
+ hostUrl,
128
+ projectRoot,
129
+ });
130
+ return {
131
+ projectRoot,
132
+ link: created.link,
133
+ credentials: created.credentials,
134
+ host,
135
+ tenantName: created.envelope.name,
136
+ hostStarted: host.started,
137
+ home,
138
+ };
139
+ }
140
+ const selected = choice.tenant;
141
+ await writeLink(projectRoot, {
142
+ format: 1,
143
+ hostUrl,
144
+ hostId: liveId ?? hostId,
145
+ tenantId: selected.id,
146
+ });
147
+ const retry = await probeTenant(hostUrl, selected.id, credentials.applicationKey, fetchImpl);
148
+ if (retry !== "ok") {
149
+ throw new CliError(`Credentials do not authorize Tenant ${selected.id}. Remove .nylorun/credentials.json and create a new Tenant with nylorun dev.`, 1);
150
+ }
151
+ return {
152
+ projectRoot,
153
+ link: {
154
+ format: 1,
155
+ hostUrl,
156
+ hostId: liveId ?? hostId,
157
+ tenantId: selected.id,
158
+ },
159
+ credentials,
160
+ host,
161
+ tenantName: selected.name ?? selected.envelope?.name ?? selected.id,
162
+ hostStarted: host.started,
163
+ home,
164
+ };
165
+ }
166
+ async function chooseUnknownTenantRecovery(options) {
167
+ const message = `Linked Tenant ${options.tenantId} is unknown on this Host.`;
168
+ const tty = options.input === undefined
169
+ ? Boolean(process.stdin.isTTY && process.stdout.isTTY)
170
+ : true;
171
+ if (!tty) {
172
+ throw new CliError(`${message} Create a new Tenant (remove .nylorun/link.json), choose another with "nylorun tenant use", or remove the link.`, 1);
173
+ }
174
+ const input = options.input ?? process.stdin;
175
+ const output = options.output ?? process.stdout;
176
+ const rl = createInterface({ input, output });
177
+ try {
178
+ output.write(`${message}\n`);
179
+ output.write(" 1) Create a new Tenant\n");
180
+ output.write(" 2) Choose another Tenant on this Host\n");
181
+ output.write(" 3) Remove the Project link\n");
182
+ const answer = (await rl.question("Choose [1/2/3]: ")).trim();
183
+ if (answer === "1")
184
+ return "create";
185
+ if (answer === "3")
186
+ return "remove";
187
+ if (answer === "2") {
188
+ const open = options.tenants.filter((t) => t.state === "open" && t.name);
189
+ if (open.length === 0) {
190
+ throw new CliError("No open Tenants on this Host to choose. Create a new one instead.", 1);
191
+ }
192
+ for (let i = 0; i < open.length; i += 1) {
193
+ const t = open[i];
194
+ output.write(` ${i + 1}) ${t.name ?? "(unnamed)"} ${t.id}\n`);
195
+ }
196
+ const pick = Number((await rl.question(`Choose Tenant [1-${open.length}]: `)).trim());
197
+ if (!Number.isInteger(pick) || pick < 1 || pick > open.length) {
198
+ throw new CliError("Invalid Tenant choice.", 2);
199
+ }
200
+ return { tenant: open[pick - 1] };
201
+ }
202
+ throw new CliError("Invalid choice.", 2);
203
+ }
204
+ finally {
205
+ rl.close();
206
+ }
207
+ }
208
+ /** Three `export` lines for `runtime status --env` (F2-7). */
209
+ export async function printLinkedEnvExports(projectRoot = process.cwd()) {
210
+ const { findProjectRoot } = await import("./root.js");
211
+ const root = findProjectRoot(projectRoot) ?? projectRoot;
212
+ const link = await readLink(root);
213
+ const credentials = await readCredentials(root);
214
+ if (!link || !credentials) {
215
+ console.log("# No Project link in this directory. Run nylorun dev to create one.");
216
+ return;
217
+ }
218
+ console.log(`export NYLORUN_RUNTIME_URL=${shellQuote(link.hostUrl)}`);
219
+ console.log(`export NYLORUN_SERVER_KEY=${shellQuote(credentials.applicationKey)}`);
220
+ console.log(`export NYLORUN_TENANT=${shellQuote(link.tenantId)}`);
221
+ }
222
+ function upResultFromInvoke(outcome) {
223
+ const result = outcome.result ?? {};
224
+ const url = typeof result.url === "string" ? result.url : "";
225
+ const hostId = typeof result.hostId === "string" ? result.hostId : "";
226
+ if (!url) {
227
+ throw new CliError("Runtime up succeeded without a URL.", 1);
228
+ }
229
+ return {
230
+ url,
231
+ hostId,
232
+ pid: typeof result.pid === "number" ? result.pid : 0,
233
+ version: typeof result.version === "string" ? result.version : "",
234
+ started: Boolean(result.started),
235
+ };
236
+ }
237
+ function shellQuote(value) {
238
+ if (/^[A-Za-z0-9_./:-]+$/.test(value))
239
+ return value;
240
+ return `'${value.replace(/'/g, `'\\''`)}'`;
241
+ }
@@ -0,0 +1,26 @@
1
+ import { type Admin } from "@nylorun/admin";
2
+ import { type ProjectCredentials } from "./credentials.js";
3
+ import { type ProjectLink } from "./link.js";
4
+ export interface CreateTenantOptions {
5
+ /** Prefer an already-resolved Admin (local Host or explicit). */
6
+ admin: Admin;
7
+ hostId: string;
8
+ projectRoot: string;
9
+ name?: string;
10
+ /** Override Host URL written into the link (defaults to admin.url). */
11
+ hostUrl?: string;
12
+ }
13
+ export interface CreateTenantResult {
14
+ link: ProjectLink;
15
+ credentials: ProjectCredentials;
16
+ envelope: {
17
+ id: string;
18
+ name: string;
19
+ };
20
+ }
21
+ /**
22
+ * Create a Tenant via `@nylorun/admin` and write format-1 link + credentials.
23
+ */
24
+ export declare function createProjectTenant(options: CreateTenantOptions): Promise<CreateTenantResult>;
25
+ /** Resolve Admin from the local Host root (after launcher `up`). */
26
+ export declare function localAdmin(home?: string): Admin;
@@ -0,0 +1,65 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ import { createAdmin } from "@nylorun/admin";
4
+ import { newPrincipalId } from "@nylorun/agents";
5
+ import { CliError } from "../errors.js";
6
+ import { writeCredentials, } from "./credentials.js";
7
+ import { writeLink } from "./link.js";
8
+ async function defaultTenantName(projectRoot) {
9
+ try {
10
+ const pkg = JSON.parse(await readFile(join(projectRoot, "package.json"), "utf8"));
11
+ if (typeof pkg.name === "string" && pkg.name.trim())
12
+ return pkg.name.trim();
13
+ }
14
+ catch {
15
+ /* fall through */
16
+ }
17
+ const base = basename(projectRoot);
18
+ return base && base.length > 0 ? base : "project";
19
+ }
20
+ /**
21
+ * Create a Tenant via `@nylorun/admin` and write format-1 link + credentials.
22
+ */
23
+ export async function createProjectTenant(options) {
24
+ const name = options.name ?? (await defaultTenantName(options.projectRoot));
25
+ const admin = options.admin;
26
+ let created;
27
+ try {
28
+ created = await admin.createTenant({ name });
29
+ }
30
+ catch (error) {
31
+ throw new CliError(error instanceof Error
32
+ ? error.message
33
+ : `Could not create Tenant: ${String(error)}`, 1);
34
+ }
35
+ const hostUrl = (options.hostUrl ?? admin.url).replace(/\/$/, "");
36
+ // CLIENTS-CCR: admin.createTenant should also return principalId (D§4.2 /
37
+ // credentials.json). Until then, mint a local id for the credentials file;
38
+ // agents resolveConnection only needs applicationKey.
39
+ const principalId = "principalId" in created &&
40
+ typeof created.principalId === "string"
41
+ ? created.principalId
42
+ : newPrincipalId();
43
+ const credentials = {
44
+ format: 1,
45
+ applicationKey: created.applicationKey,
46
+ principalId,
47
+ };
48
+ const link = {
49
+ format: 1,
50
+ hostUrl,
51
+ hostId: options.hostId,
52
+ tenantId: created.tenant.id,
53
+ };
54
+ await writeCredentials(options.projectRoot, credentials);
55
+ await writeLink(options.projectRoot, link);
56
+ return {
57
+ link,
58
+ credentials,
59
+ envelope: { id: created.tenant.id, name: created.tenant.name },
60
+ };
61
+ }
62
+ /** Resolve Admin from the local Host root (after launcher `up`). */
63
+ export function localAdmin(home) {
64
+ return createAdmin(home ? { home } : undefined);
65
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Project-local credentials (mode 0600). Format 1 drops `executors`.
3
+ * Format 0 may still contain an executors map; readers ignore it (D§3.7).
4
+ */
5
+ export interface ProjectCredentials {
6
+ format: 0 | 1;
7
+ applicationKey: string;
8
+ principalId: string;
9
+ /** Present only on format 0 files; ignored by version 1. */
10
+ executors?: Record<string, string>;
11
+ }
12
+ export declare function readCredentials(projectRoot: string): Promise<ProjectCredentials | undefined>;
13
+ export declare function writeCredentials(projectRoot: string, credentials: {
14
+ applicationKey: string;
15
+ principalId: string;
16
+ format?: 0 | 1;
17
+ }): Promise<void>;
18
+ export declare function removeCredentials(projectRoot: string): Promise<void>;