@nylorun/cli 0.2.1-beta → 0.4.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 +83 -0
- package/README.md +66 -42
- package/dist/baseline.d.ts +2 -0
- package/dist/baseline.js +10 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +73 -242
- package/dist/doctor.d.ts +15 -4
- package/dist/doctor.js +44 -46
- package/dist/environment.d.ts +4 -1
- package/dist/environment.js +12 -3
- package/dist/home.d.ts +6 -0
- package/dist/home.js +15 -0
- package/dist/model/configure.d.ts +30 -0
- package/dist/model/configure.js +117 -11
- package/dist/model/host-model.d.ts +5 -3
- package/dist/model/host-model.js +37 -16
- package/dist/project/create-tenant.d.ts +28 -0
- package/dist/project/create-tenant.js +66 -0
- package/dist/project/credentials.d.ts +18 -0
- package/dist/project/credentials.js +64 -0
- package/dist/project/env.d.ts +3 -0
- package/dist/project/env.js +21 -0
- package/dist/project/link.d.ts +20 -0
- package/dist/project/link.js +77 -0
- package/dist/project/root.d.ts +9 -0
- package/dist/project/root.js +52 -0
- package/dist/project/seed.d.ts +25 -0
- package/dist/project/seed.js +96 -0
- package/dist/tenant/commands.d.ts +4 -0
- package/dist/tenant/commands.js +321 -0
- package/package.json +6 -6
- package/dist/dev-entry.d.ts +0 -1
- package/dist/dev-entry.js +0 -6
- package/dist/dev.d.ts +0 -6
- package/dist/dev.js +0 -65
- package/dist/launcher.d.ts +0 -5
- package/dist/launcher.js +0 -4
- package/dist/project-runner.d.ts +0 -20
- package/dist/project-runner.js +0 -141
- package/dist/runtime-host.d.ts +0 -39
- package/dist/runtime-host.js +0 -355
- package/dist/scope.d.ts +0 -65
- package/dist/scope.js +0 -191
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createAdmin } from "@nylorun/admin";
|
|
4
|
+
import { createClient, isTenantId } from "@nylorun/agents";
|
|
5
|
+
import { CliError } from "../errors.js";
|
|
6
|
+
import { resolveHome } from "../home.js";
|
|
7
|
+
import { readCredentials, removeCredentials, } from "../project/credentials.js";
|
|
8
|
+
import { readLink, removeLink, writeLink, } from "../project/link.js";
|
|
9
|
+
import { findProjectRoot, requireProjectRoot } from "../project/root.js";
|
|
10
|
+
import { createProjectTenant, defaultTenantName, } from "../project/create-tenant.js";
|
|
11
|
+
import { seedTenantFromProject } from "../project/seed.js";
|
|
12
|
+
import { loadProjectEnvironment } from "../environment.js";
|
|
13
|
+
import { shellQuote } from "../project/env.js";
|
|
14
|
+
/** Exact-and-unique name or id match. */
|
|
15
|
+
export function matchTenant(tenants, nameOrId) {
|
|
16
|
+
if (isTenantId(nameOrId)) {
|
|
17
|
+
const byId = tenants.find((t) => t.id === nameOrId);
|
|
18
|
+
if (!byId)
|
|
19
|
+
throw new CliError(`No Tenant with id ${nameOrId}.`, 1);
|
|
20
|
+
return byId;
|
|
21
|
+
}
|
|
22
|
+
const matches = tenants.filter((t) => t.name === nameOrId);
|
|
23
|
+
if (matches.length === 0) {
|
|
24
|
+
throw new CliError(`No Tenant named ${JSON.stringify(nameOrId)}.`, 1);
|
|
25
|
+
}
|
|
26
|
+
if (matches.length > 1) {
|
|
27
|
+
throw new CliError(`Tenant name ${JSON.stringify(nameOrId)} is not unique (${matches.length} matches). Use the Tenant id.`, 1);
|
|
28
|
+
}
|
|
29
|
+
return matches[0];
|
|
30
|
+
}
|
|
31
|
+
function localAdmin(home) {
|
|
32
|
+
return createAdmin(home ? { home } : undefined);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `nylo tenant create [name]`. Inside a Project: create the Tenant, write the
|
|
36
|
+
* Project link and credentials, and seed it from the Project's `.env` (sandbox
|
|
37
|
+
* and model provider). Outside one: create the Tenant and print its
|
|
38
|
+
* connection variables once, since the Host keeps only a hash of the key.
|
|
39
|
+
*/
|
|
40
|
+
async function createTenant(options) {
|
|
41
|
+
const admin = localAdmin(options.home);
|
|
42
|
+
let hostId;
|
|
43
|
+
try {
|
|
44
|
+
hostId = (await admin.status()).host?.hostId;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new CliError(`No Runtime answers at ${admin.url} (${error instanceof Error ? error.message : String(error)}). Start the local stack with "npx nylorun up".`, 6);
|
|
48
|
+
}
|
|
49
|
+
if (!hostId)
|
|
50
|
+
throw new CliError(`Host at ${admin.url} did not report hostId in admin status.`, 1);
|
|
51
|
+
const projectRoot = findProjectRoot();
|
|
52
|
+
if (!projectRoot) {
|
|
53
|
+
const created = await admin.createTenant({ name: options.name ?? "tenant" });
|
|
54
|
+
console.log(`Tenant ${created.tenant.name} ${created.tenant.id} (created)`);
|
|
55
|
+
console.log("# Not in a Project: nothing was linked. The key is shown only once.");
|
|
56
|
+
console.log(`export NYLORUN_RUNTIME_URL=${shellQuote(admin.url.replace(/\/$/, ""))}`);
|
|
57
|
+
console.log(`export NYLORUN_SERVER_KEY=${shellQuote(created.applicationKey)}`);
|
|
58
|
+
console.log(`export NYLORUN_TENANT=${shellQuote(created.tenant.id)}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const link = await readLink(projectRoot);
|
|
62
|
+
if (link)
|
|
63
|
+
throw new CliError(`This Project is already linked to Tenant ${link.tenantId}. Use "nylo tenant use <name-or-id>" to switch, or "nylo tenant delete" first.`, 1);
|
|
64
|
+
const created = await createProjectTenant({
|
|
65
|
+
admin,
|
|
66
|
+
hostId,
|
|
67
|
+
projectRoot,
|
|
68
|
+
name: options.name ?? (await defaultTenantName(projectRoot)),
|
|
69
|
+
});
|
|
70
|
+
console.log(`Tenant ${created.envelope.name} ${created.envelope.id} (created)`);
|
|
71
|
+
console.log(`Linked ${projectRoot}/.nylorun/`);
|
|
72
|
+
const seeded = await seedTenantFromProject({
|
|
73
|
+
hostUrl: created.link.hostUrl,
|
|
74
|
+
tenantId: created.link.tenantId,
|
|
75
|
+
applicationKey: created.credentials.applicationKey,
|
|
76
|
+
projectRoot,
|
|
77
|
+
env: loadProjectEnvironment(projectRoot),
|
|
78
|
+
});
|
|
79
|
+
console.log(seeded.model
|
|
80
|
+
? `Model ${seeded.model} (from .env)`
|
|
81
|
+
: "Model not configured: set it in Studio, or run nylo configure");
|
|
82
|
+
}
|
|
83
|
+
export async function tenantCommand(args) {
|
|
84
|
+
const [verb, ...rest] = args;
|
|
85
|
+
if (!verb || verb === "--help" || verb === "-h") {
|
|
86
|
+
console.log(`nylo tenant create [name]|current|list [--json]|use <name-or-id>|status [--json]|reset [--sessions|--sandboxes|--all] [--yes]|delete <name-or-id> [--yes]`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const home = resolveHome();
|
|
90
|
+
if (verb === "create") {
|
|
91
|
+
if (rest.length > 1 || rest.some((arg) => arg.startsWith("-")))
|
|
92
|
+
throw new CliError("Usage: nylo tenant create [name]", 2);
|
|
93
|
+
await createTenant({ home, ...(rest[0] ? { name: rest[0] } : {}) });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (verb === "current") {
|
|
97
|
+
const root = requireProjectRoot();
|
|
98
|
+
const link = await readLink(root);
|
|
99
|
+
if (!link) {
|
|
100
|
+
console.log("No Project link. Run nylo tenant create to create one.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const admin = localAdmin(home);
|
|
104
|
+
const tenants = await admin.listTenants();
|
|
105
|
+
const match = tenants.find((t) => t.id === link.tenantId);
|
|
106
|
+
const name = match?.name ?? match?.envelope?.name ?? "(unknown)";
|
|
107
|
+
console.log(`${name} ${link.tenantId}`);
|
|
108
|
+
console.log(`Host ${link.hostUrl} ${link.hostId}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (verb === "list") {
|
|
112
|
+
const json = rest.includes("--json");
|
|
113
|
+
if (rest.some((a) => a !== "--json")) {
|
|
114
|
+
throw new CliError("Usage: nylo tenant list [--json]", 2);
|
|
115
|
+
}
|
|
116
|
+
const admin = localAdmin(home);
|
|
117
|
+
const tenants = await admin.listTenants();
|
|
118
|
+
if (json) {
|
|
119
|
+
console.log(JSON.stringify(tenants, null, 2));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (tenants.length === 0) {
|
|
123
|
+
console.log("No Tenants on this Host.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const tenant of tenants) {
|
|
127
|
+
const name = tenant.name ?? "(unreadable)";
|
|
128
|
+
console.log(`${name} ${tenant.id} ${tenant.state}`);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (verb === "use") {
|
|
133
|
+
const nameOrId = rest[0];
|
|
134
|
+
if (!nameOrId || rest.length > 1) {
|
|
135
|
+
throw new CliError("Usage: nylo tenant use <name-or-id>", 2);
|
|
136
|
+
}
|
|
137
|
+
const root = requireProjectRoot();
|
|
138
|
+
const admin = localAdmin(home);
|
|
139
|
+
const status = await admin.status();
|
|
140
|
+
const hostId = status.host?.hostId;
|
|
141
|
+
if (!hostId) {
|
|
142
|
+
throw new CliError(`Host at ${admin.url} did not report hostId in admin status.`, 1);
|
|
143
|
+
}
|
|
144
|
+
const tenants = await admin.listTenants();
|
|
145
|
+
const selected = matchTenant(tenants, nameOrId);
|
|
146
|
+
console.warn("Sharing a Tenant shares executor registrations across Projects that link to it.");
|
|
147
|
+
const credentials = await readCredentials(root);
|
|
148
|
+
if (!credentials) {
|
|
149
|
+
throw new CliError("No Project credentials. Run nylo tenant create to create a Tenant first, or copy credentials for the target Tenant.", 1);
|
|
150
|
+
}
|
|
151
|
+
const client = createClient({
|
|
152
|
+
url: admin.url,
|
|
153
|
+
key: credentials.applicationKey,
|
|
154
|
+
tenant: selected.id,
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
await client.transport.json("/v1/tenant", "GET");
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
throw new CliError(`Credentials do not authorize Tenant ${selected.id}. Create a new Tenant with nylo tenant create instead of reusing another Project's Tenant without its key.`, 1);
|
|
161
|
+
}
|
|
162
|
+
await writeLink(root, {
|
|
163
|
+
format: 1,
|
|
164
|
+
hostUrl: admin.url,
|
|
165
|
+
hostId,
|
|
166
|
+
tenantId: selected.id,
|
|
167
|
+
});
|
|
168
|
+
console.log(`Linked to ${selected.name ?? selected.id} ${selected.id}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (verb === "status") {
|
|
172
|
+
const json = rest.includes("--json");
|
|
173
|
+
if (rest.some((a) => a !== "--json")) {
|
|
174
|
+
throw new CliError("Usage: nylo tenant status [--json]", 2);
|
|
175
|
+
}
|
|
176
|
+
const root = requireProjectRoot();
|
|
177
|
+
const link = await readLink(root);
|
|
178
|
+
const credentials = await readCredentials(root);
|
|
179
|
+
if (!link || !credentials) {
|
|
180
|
+
throw new CliError("No Project link. Run nylo tenant create to create one.", 1);
|
|
181
|
+
}
|
|
182
|
+
const client = createClient({
|
|
183
|
+
url: link.hostUrl,
|
|
184
|
+
key: credentials.applicationKey,
|
|
185
|
+
tenant: link.tenantId,
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
const body = await client.transport.json("/v1/tenant", "GET");
|
|
189
|
+
if (json) {
|
|
190
|
+
console.log(JSON.stringify(body, null, 2));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
console.log(`${body.tenant.name} ${body.tenant.id}`);
|
|
194
|
+
console.log(`path ${body.path}`);
|
|
195
|
+
console.log(`checks ${Object.entries(body.checks)
|
|
196
|
+
.map(([k, v]) => `${k}=${v ? "ok" : "fail"}`)
|
|
197
|
+
.join(" ")}`);
|
|
198
|
+
console.log(`counts sessions=${body.counts.sessions} running=${body.counts.runningSessions} pending=${body.counts.pendingActions}`);
|
|
199
|
+
console.log(`sandbox ${body.sandbox.backend ?? "none"}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Quarantined / opaque — fall back to admin status.
|
|
204
|
+
}
|
|
205
|
+
const admin = localAdmin(home);
|
|
206
|
+
try {
|
|
207
|
+
const body = await admin.getTenant(link.tenantId);
|
|
208
|
+
if (json) {
|
|
209
|
+
console.log(JSON.stringify(body, null, 2));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
console.log(`${body.name ?? "(unreadable)"} ${body.id} ${body.state}`);
|
|
213
|
+
if (body.quarantine) {
|
|
214
|
+
console.log(`reason ${body.quarantine.code}: ${body.quarantine.message}`);
|
|
215
|
+
console.log(`repair ${body.quarantine.repair}`);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
throw new CliError(error instanceof Error
|
|
221
|
+
? error.message
|
|
222
|
+
: `Tenant ${link.tenantId} is not reachable.`, 1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (verb === "reset") {
|
|
226
|
+
const yes = rest.includes("--yes");
|
|
227
|
+
const flags = rest.filter((a) => a !== "--yes");
|
|
228
|
+
let scope = "sessions";
|
|
229
|
+
if (flags.includes("--all"))
|
|
230
|
+
scope = "all";
|
|
231
|
+
else if (flags.includes("--sandboxes"))
|
|
232
|
+
scope = "sandboxes";
|
|
233
|
+
else if (flags.includes("--sessions") || flags.length === 0)
|
|
234
|
+
scope = "sessions";
|
|
235
|
+
else
|
|
236
|
+
throw new CliError("Usage: nylo tenant reset [--sessions|--sandboxes|--all] [--yes]", 2);
|
|
237
|
+
if (flags.filter((f) => ["--sessions", "--sandboxes", "--all"].includes(f)).length > 1) {
|
|
238
|
+
throw new CliError("Pass only one of --sessions, --sandboxes, --all.", 2);
|
|
239
|
+
}
|
|
240
|
+
const root = requireProjectRoot();
|
|
241
|
+
const link = await readLink(root);
|
|
242
|
+
const credentials = await readCredentials(root);
|
|
243
|
+
if (!link || !credentials) {
|
|
244
|
+
throw new CliError("No Project link. Run nylo tenant create first.", 1);
|
|
245
|
+
}
|
|
246
|
+
const admin = localAdmin(home);
|
|
247
|
+
const tenants = await admin.listTenants();
|
|
248
|
+
const match = tenants.find((t) => t.id === link.tenantId);
|
|
249
|
+
const name = match?.name ?? link.tenantId;
|
|
250
|
+
if (scope === "all" && !yes) {
|
|
251
|
+
await confirmOrThrow(`Reset ALL data for Tenant ${name} (${link.tenantId})? Link and credentials are kept. [y/N] `);
|
|
252
|
+
}
|
|
253
|
+
const client = createClient({
|
|
254
|
+
url: link.hostUrl,
|
|
255
|
+
key: credentials.applicationKey,
|
|
256
|
+
tenant: link.tenantId,
|
|
257
|
+
});
|
|
258
|
+
try {
|
|
259
|
+
await client.transport.json("/v1/tenant/reset", "POST", {
|
|
260
|
+
requestId: randomUUID(),
|
|
261
|
+
scope,
|
|
262
|
+
activeWork: "drain",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
throw new CliError(error instanceof Error
|
|
267
|
+
? error.message
|
|
268
|
+
: `Tenant reset failed: ${String(error)}`, 1);
|
|
269
|
+
}
|
|
270
|
+
console.log(`Reset Tenant ${name} (${scope}).`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (verb === "delete") {
|
|
274
|
+
const yes = rest.includes("--yes");
|
|
275
|
+
const nameOrId = rest.find((a) => a !== "--yes");
|
|
276
|
+
if (!nameOrId || rest.filter((a) => a !== "--yes").length !== 1) {
|
|
277
|
+
throw new CliError("Usage: nylo tenant delete <name-or-id> [--yes]", 2);
|
|
278
|
+
}
|
|
279
|
+
const root = requireProjectRoot();
|
|
280
|
+
const admin = localAdmin(home);
|
|
281
|
+
const tenants = await admin.listTenants();
|
|
282
|
+
const selected = matchTenant(tenants, nameOrId);
|
|
283
|
+
if (!yes) {
|
|
284
|
+
await confirmOrThrow(`Delete Tenant ${selected.name ?? selected.id} (${selected.id})?\nThe KEK moves to trash with the Tenant directory. [y/N] `);
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
await admin.deleteTenant(selected.id, { activeWork: "refuse" });
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
291
|
+
if (/active_work|409|live work/i.test(message)) {
|
|
292
|
+
throw new CliError(`Tenant ${selected.id} has live work. Retry with the Host idle, or drain sessions first.`, 1);
|
|
293
|
+
}
|
|
294
|
+
throw new CliError(message, 1);
|
|
295
|
+
}
|
|
296
|
+
const link = await readLink(root);
|
|
297
|
+
if (link?.tenantId === selected.id) {
|
|
298
|
+
await removeLink(root);
|
|
299
|
+
await removeCredentials(root);
|
|
300
|
+
console.log("Removed Project link and credentials for the deleted Tenant.");
|
|
301
|
+
}
|
|
302
|
+
console.log(`Deleted Tenant ${selected.name ?? selected.id}.`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
throw new CliError(`Unknown tenant command ${verb}.\nUsage: nylo tenant current|list|use|status|reset|delete`, 2);
|
|
306
|
+
}
|
|
307
|
+
async function confirmOrThrow(prompt) {
|
|
308
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
309
|
+
throw new CliError("Confirmation required; pass --yes for non-interactive use.", 2);
|
|
310
|
+
}
|
|
311
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
312
|
+
try {
|
|
313
|
+
const answer = (await rl.question(prompt)).trim().toLowerCase();
|
|
314
|
+
if (answer !== "y" && answer !== "yes") {
|
|
315
|
+
throw new CliError("Cancelled.", 1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
rl.close();
|
|
320
|
+
}
|
|
321
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-beta",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"description": "Nylorun
|
|
6
|
+
"description": "Nylorun Runtime client: Tenants, Project links and model providers",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/nylorun/harness.git",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"prepack": "npm run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@nylorun/agents": "0.
|
|
34
|
-
"@nylorun/
|
|
35
|
-
"@earendil-works/pi-ai": "0.
|
|
33
|
+
"@nylorun/agents": "0.7.0-beta",
|
|
34
|
+
"@nylorun/admin": "0.3.0-beta",
|
|
35
|
+
"@earendil-works/pi-ai": "0.87.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
@@ -40,6 +40,6 @@
|
|
|
40
40
|
"vitest": "^5.0.1"
|
|
41
41
|
},
|
|
42
42
|
"bin": {
|
|
43
|
-
"
|
|
43
|
+
"nylo": "dist/cli.js"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/dist/dev-entry.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/dev-entry.js
DELETED
package/dist/dev.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Everything that can be checked without contacting the host, so a project that cannot run
|
|
3
|
-
* never causes a Runtime to be started on its behalf.
|
|
4
|
-
*/
|
|
5
|
-
export declare function developmentPreflight(args: readonly string[]): string;
|
|
6
|
-
export declare function develop(args: readonly string[]): Promise<number>;
|
package/dist/dev.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
/**
|
|
6
|
-
* Everything that can be checked without contacting the host, so a project that cannot run
|
|
7
|
-
* never causes a Runtime to be started on its behalf.
|
|
8
|
-
*/
|
|
9
|
-
export function developmentPreflight(args) {
|
|
10
|
-
if (new Set(args).size !== args.length ||
|
|
11
|
-
args.some((arg) => !["--no-studio", "--no-open", "--no-autostart"].includes(arg)))
|
|
12
|
-
throw new Error("Usage: nylorun dev [--no-studio] [--no-open] [--no-autostart]");
|
|
13
|
-
const require = createRequire(join(process.cwd(), "package.json"));
|
|
14
|
-
let tsx;
|
|
15
|
-
try {
|
|
16
|
-
tsx = require.resolve("tsx/cli");
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
throw new Error("Install tsx to use nylorun dev");
|
|
20
|
-
}
|
|
21
|
-
if (!args.includes("--no-studio"))
|
|
22
|
-
try {
|
|
23
|
-
require.resolve("@nylorun/studio");
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
throw new Error("Install @nylorun/studio or use --no-studio");
|
|
27
|
-
}
|
|
28
|
-
return tsx;
|
|
29
|
-
}
|
|
30
|
-
export async function develop(args) {
|
|
31
|
-
const tsx = developmentPreflight(args);
|
|
32
|
-
const child = spawn(process.execPath, [
|
|
33
|
-
tsx,
|
|
34
|
-
"watch",
|
|
35
|
-
"--clear-screen=false",
|
|
36
|
-
fileURLToPath(new URL("./dev-entry.js", import.meta.url)),
|
|
37
|
-
...args,
|
|
38
|
-
],
|
|
39
|
-
// Deliver terminal shutdown once, through the supervisor, instead of twice through the terminal group.
|
|
40
|
-
{
|
|
41
|
-
stdio: "inherit",
|
|
42
|
-
env: process.env,
|
|
43
|
-
detached: process.platform !== "win32",
|
|
44
|
-
});
|
|
45
|
-
let stopping = false;
|
|
46
|
-
const stop = (signal) => {
|
|
47
|
-
if (stopping)
|
|
48
|
-
return;
|
|
49
|
-
stopping = true;
|
|
50
|
-
child.kill(signal);
|
|
51
|
-
};
|
|
52
|
-
const interrupt = () => stop("SIGINT"), terminate = () => stop("SIGTERM");
|
|
53
|
-
process.on("SIGINT", interrupt);
|
|
54
|
-
process.on("SIGTERM", terminate);
|
|
55
|
-
try {
|
|
56
|
-
return await new Promise((resolve, reject) => {
|
|
57
|
-
child.once("error", reject);
|
|
58
|
-
child.once("exit", (code, signal) => resolve(code ?? (signal === "SIGINT" ? 130 : 143)));
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
process.removeListener("SIGINT", interrupt);
|
|
63
|
-
process.removeListener("SIGTERM", terminate);
|
|
64
|
-
}
|
|
65
|
-
}
|
package/dist/launcher.d.ts
DELETED
package/dist/launcher.js
DELETED
package/dist/project-runner.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { type Scope } from "./scope.js";
|
|
2
|
-
export interface LocalCredentials {
|
|
3
|
-
serverKey: string;
|
|
4
|
-
executors: Record<string, string>;
|
|
5
|
-
}
|
|
6
|
-
export declare function localCredentials(path: string): Promise<LocalCredentials>;
|
|
7
|
-
export interface ExecutorScopeToken {
|
|
8
|
-
token: string;
|
|
9
|
-
agentId: string;
|
|
10
|
-
implementationVersion: string;
|
|
11
|
-
}
|
|
12
|
-
/** Registers or rotates this project's executor credentials on the running host. */
|
|
13
|
-
export declare function putExecutors(url: string, serverKey: string, executors: readonly ExecutorScopeToken[]): Promise<void>;
|
|
14
|
-
export interface RunProjectOptions {
|
|
15
|
-
studio?: boolean;
|
|
16
|
-
open?: boolean;
|
|
17
|
-
autostart?: boolean;
|
|
18
|
-
scope?: Scope;
|
|
19
|
-
}
|
|
20
|
-
export declare function runProject(entry: string, options?: RunProjectOptions): Promise<void>;
|
package/dist/project-runner.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { mkdir, readFile, writeFile, chmod } from "node:fs/promises";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
-
import { createClient, connectAgents, } from "@nylorun/agents";
|
|
7
|
-
import { ensureHostModel } from "./model/host-model.js";
|
|
8
|
-
import { CliError } from "./errors.js";
|
|
9
|
-
import { ensureDataDir, loadScopeEnvironment, probeHealth, resolveScope, } from "./scope.js";
|
|
10
|
-
import { ensureRuntime } from "./runtime-host.js";
|
|
11
|
-
export async function localCredentials(path) {
|
|
12
|
-
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
13
|
-
try {
|
|
14
|
-
const value = JSON.parse(await readFile(path, "utf8"));
|
|
15
|
-
if (typeof value.serverKey !== "string" || !value.executors)
|
|
16
|
-
throw new Error("Invalid local credentials");
|
|
17
|
-
await chmod(path, 0o600);
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
if (error.code !== "ENOENT")
|
|
22
|
-
throw error;
|
|
23
|
-
const value = { serverKey: randomBytes(32).toString("hex"), executors: {} };
|
|
24
|
-
await writeFile(path, JSON.stringify(value), { mode: 0o600, flag: "wx" });
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
/** Registers or rotates this project's executor credentials on the running host. */
|
|
29
|
-
export async function putExecutors(url, serverKey, executors) {
|
|
30
|
-
const response = await fetch(`${url}/v1/executors`, {
|
|
31
|
-
method: "PUT",
|
|
32
|
-
headers: {
|
|
33
|
-
authorization: `Bearer ${serverKey}`,
|
|
34
|
-
"content-type": "application/json",
|
|
35
|
-
},
|
|
36
|
-
body: JSON.stringify({ executors }),
|
|
37
|
-
});
|
|
38
|
-
if (!response.ok)
|
|
39
|
-
throw new CliError(`The Runtime at ${url} rejected this project's executors (${response.status}): ${await response
|
|
40
|
-
.text()
|
|
41
|
-
.catch(() => "")}`);
|
|
42
|
-
}
|
|
43
|
-
export async function runProject(entry, options = {}) {
|
|
44
|
-
const scope = options.scope ?? resolveScope();
|
|
45
|
-
loadScopeEnvironment(scope);
|
|
46
|
-
const registry = await import(pathToFileURL(resolve(entry)).href);
|
|
47
|
-
if (!Array.isArray(registry.agents) || !registry.agents.length)
|
|
48
|
-
throw new Error(`${entry} must export a non-empty agents array`);
|
|
49
|
-
const agents = registry.agents;
|
|
50
|
-
await ensureDataDir(scope);
|
|
51
|
-
const credentials = await localCredentials(scope.credentialsPath);
|
|
52
|
-
const version = process.env.NYLORUN_IMPLEMENTATION_VERSION ?? "dev";
|
|
53
|
-
const scopes = agents.map((agent) => {
|
|
54
|
-
const token = (credentials.executors[agent.id] ??=
|
|
55
|
-
randomBytes(32).toString("hex"));
|
|
56
|
-
return { token, agentId: agent.id, implementationVersion: version };
|
|
57
|
-
});
|
|
58
|
-
await writeFile(scope.credentialsPath, JSON.stringify(credentials), {
|
|
59
|
-
mode: 0o600,
|
|
60
|
-
});
|
|
61
|
-
const url = scope.url;
|
|
62
|
-
let studio;
|
|
63
|
-
const connections = [];
|
|
64
|
-
let watchdog;
|
|
65
|
-
let stopping;
|
|
66
|
-
const close = () => (stopping ??= (async () => {
|
|
67
|
-
if (watchdog)
|
|
68
|
-
clearInterval(watchdog);
|
|
69
|
-
await Promise.allSettled(connections.map((c) => c.close()));
|
|
70
|
-
await studio?.close().catch(() => { });
|
|
71
|
-
for (const agent of agents)
|
|
72
|
-
await agent.close?.();
|
|
73
|
-
})());
|
|
74
|
-
// The Runtime is a separate, persistent process now, so shutting this one down leaves it up.
|
|
75
|
-
const stop = () => void close().finally(() => (process.exitCode = 0));
|
|
76
|
-
process.on("SIGINT", stop);
|
|
77
|
-
process.on("SIGTERM", stop);
|
|
78
|
-
try {
|
|
79
|
-
await ensureRuntime(scope, { autostart: options.autostart !== false });
|
|
80
|
-
await putExecutors(url, credentials.serverKey, scopes);
|
|
81
|
-
if (process.env.NYLORUN_DEV_MODEL !== "fixture")
|
|
82
|
-
await ensureHostModel({ runtimeUrl: url, serverKey: credentials.serverKey });
|
|
83
|
-
const client = createClient({ url, key: credentials.serverKey });
|
|
84
|
-
for (let index = 0; index < agents.length; index++) {
|
|
85
|
-
await client.saveAgent(agents[index], {
|
|
86
|
-
implementationVersion: version,
|
|
87
|
-
});
|
|
88
|
-
const connection = connectAgents({
|
|
89
|
-
agents: [agents[index]],
|
|
90
|
-
implementationVersion: version,
|
|
91
|
-
runtime: { url, key: scopes[index].token },
|
|
92
|
-
onError: (error) => console.error("Executor:", error instanceof Error ? error.message : error),
|
|
93
|
-
});
|
|
94
|
-
connections.push(connection);
|
|
95
|
-
await connection.ready;
|
|
96
|
-
}
|
|
97
|
-
if (options.studio) {
|
|
98
|
-
const modulePath = createRequire(join(process.cwd(), "package.json")).resolve("@nylorun/studio");
|
|
99
|
-
const module = await import(pathToFileURL(modulePath).href);
|
|
100
|
-
studio = await module.startStudio({
|
|
101
|
-
runtimeUrl: url,
|
|
102
|
-
serverKey: credentials.serverKey,
|
|
103
|
-
open: options.open !== false,
|
|
104
|
-
});
|
|
105
|
-
console.log(`Studio on ${studio.address}`);
|
|
106
|
-
}
|
|
107
|
-
console.log(`Local project ready at ${url}; ${agents.length} connected agent(s).`);
|
|
108
|
-
const sandbox = await (await import("./doctor.js")).sandboxBanner(url, credentials.serverKey, agents.map((agent) => agent.manifest));
|
|
109
|
-
if (sandbox)
|
|
110
|
-
console.log(sandbox);
|
|
111
|
-
await new Promise((settle) => {
|
|
112
|
-
const finish = () => {
|
|
113
|
-
if (!stopping)
|
|
114
|
-
void close();
|
|
115
|
-
settle();
|
|
116
|
-
};
|
|
117
|
-
process.once("SIGINT", finish);
|
|
118
|
-
process.once("SIGTERM", finish);
|
|
119
|
-
// Without a child to watch, a Runtime that dies would leave this process connected to
|
|
120
|
-
// nothing; three consecutive failed probes end the run with a pointer to the log.
|
|
121
|
-
let failures = 0;
|
|
122
|
-
watchdog = setInterval(async () => {
|
|
123
|
-
if (stopping)
|
|
124
|
-
return;
|
|
125
|
-
if ((await probeHealth(url, 2000)).health)
|
|
126
|
-
return void (failures = 0);
|
|
127
|
-
if (++failures < 3)
|
|
128
|
-
return;
|
|
129
|
-
console.error('Runtime stopped unexpectedly; see "nylorun logs".');
|
|
130
|
-
process.exitCode = 1;
|
|
131
|
-
finish();
|
|
132
|
-
}, 5000);
|
|
133
|
-
watchdog.unref();
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
finally {
|
|
137
|
-
process.removeListener("SIGINT", stop);
|
|
138
|
-
process.removeListener("SIGTERM", stop);
|
|
139
|
-
await close();
|
|
140
|
-
}
|
|
141
|
-
}
|
package/dist/runtime-host.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type RuntimeState, type Scope } from "./scope.js";
|
|
2
|
-
export type HostState = "running" | "stopped" | "stale" | "unhealthy" | "pid-recycled" | "port-conflict";
|
|
3
|
-
export interface Status {
|
|
4
|
-
state: HostState;
|
|
5
|
-
scope: Scope;
|
|
6
|
-
runtime?: RuntimeState;
|
|
7
|
-
version?: string;
|
|
8
|
-
scopeId?: string;
|
|
9
|
-
pid?: number;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Derived from the resolved server entry rather than a "./package.json" export, so the version
|
|
13
|
-
* is readable for every installed Runtime — including one too old to declare that export,
|
|
14
|
-
* which is exactly when skew detection matters.
|
|
15
|
-
*/
|
|
16
|
-
export declare const cliRuntimeVersion: () => string | undefined;
|
|
17
|
-
export declare function hostStatus(scope: Scope): Promise<Status>;
|
|
18
|
-
export declare function startHost(scope: Scope, options?: {
|
|
19
|
-
foreground?: boolean;
|
|
20
|
-
restart?: boolean;
|
|
21
|
-
}): Promise<Status>;
|
|
22
|
-
export declare function stopHost(scope: Scope, options?: {
|
|
23
|
-
timeoutMs?: number;
|
|
24
|
-
}): Promise<"stopped" | "not-running">;
|
|
25
|
-
export interface EnsureOptions {
|
|
26
|
-
autostart: boolean;
|
|
27
|
-
quiet?: boolean;
|
|
28
|
-
}
|
|
29
|
-
export declare function ensureRuntime(scope: Scope, options: EnsureOptions): Promise<Status>;
|
|
30
|
-
/**
|
|
31
|
-
* A daemon from an older install would reject executor registration with a bare 404, so the
|
|
32
|
-
* commands that depend on that route fail early with the remedy instead.
|
|
33
|
-
*/
|
|
34
|
-
export declare function assertCompatible(scope: Scope, status: Status): void;
|
|
35
|
-
export declare function warnIncompatible(scope: Scope, status: Status): void;
|
|
36
|
-
export declare function tailLog(scope: Scope, options: {
|
|
37
|
-
follow: boolean;
|
|
38
|
-
lines: number;
|
|
39
|
-
}): Promise<void>;
|