nylorun 0.13.0-beta → 0.15.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 (76) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +255 -166
  3. package/dist/cli.js +141 -47
  4. package/dist/client/agents.d.ts +6 -0
  5. package/dist/client/agents.js +148 -0
  6. package/dist/client/artifacts.d.ts +4 -0
  7. package/dist/client/artifacts.js +210 -0
  8. package/dist/client/doctor.d.ts +10 -4
  9. package/dist/client/doctor.js +28 -20
  10. package/dist/client/environment-manifests.d.ts +10 -0
  11. package/dist/client/environment-manifests.js +173 -0
  12. package/dist/client/environments.d.ts +4 -0
  13. package/dist/client/environments.js +295 -0
  14. package/dist/client/io.d.ts +31 -0
  15. package/dist/client/io.js +72 -0
  16. package/dist/client/mcp.d.ts +11 -0
  17. package/dist/{stack → client}/mcp.js +40 -26
  18. package/dist/client/model/pi-ai.d.ts +3 -3
  19. package/dist/client/model/pi-ai.js +4 -4
  20. package/dist/client/models.d.ts +13 -0
  21. package/dist/client/models.js +203 -0
  22. package/dist/client/runtime.d.ts +40 -0
  23. package/dist/client/runtime.js +134 -0
  24. package/dist/client/sessions.d.ts +4 -0
  25. package/dist/client/sessions.js +295 -0
  26. package/dist/client/settings.d.ts +12 -0
  27. package/dist/client/settings.js +103 -0
  28. package/dist/client/signing-keys.d.ts +3 -0
  29. package/dist/client/signing-keys.js +32 -0
  30. package/dist/client/target.d.ts +33 -0
  31. package/dist/client/target.js +65 -0
  32. package/dist/client/tenant.d.ts +22 -0
  33. package/dist/client/tenant.js +176 -0
  34. package/dist/doctor.js +3 -3
  35. package/dist/errors.d.ts +1 -1
  36. package/dist/errors.js +1 -1
  37. package/dist/nylo.js +20 -4
  38. package/dist/sandbox/cluster-file.d.ts +1 -1
  39. package/dist/sandbox/commands.d.ts +1 -1
  40. package/dist/sandbox/commands.js +7 -7
  41. package/dist/sandbox/manifests.d.ts +1 -1
  42. package/dist/sandbox/manifests.js +1 -1
  43. package/dist/sandbox/probe.d.ts +1 -1
  44. package/dist/stack/commands.d.ts +9 -4
  45. package/dist/stack/commands.js +22 -26
  46. package/dist/stack/compose-file.d.ts +1 -1
  47. package/dist/stack/compose-file.js +22 -13
  48. package/dist/stack/env-file.d.ts +2 -2
  49. package/dist/stack/env-file.js +2 -2
  50. package/dist/stack/images.d.ts +1 -1
  51. package/dist/stack/images.js +1 -1
  52. package/dist/stack/index.d.ts +1 -10
  53. package/dist/stack/index.js +3 -19
  54. package/dist/stack/keys.d.ts +2 -2
  55. package/dist/stack/keys.js +14 -11
  56. package/dist/stack/paths.d.ts +1 -1
  57. package/dist/stack/prepare.d.ts +1 -1
  58. package/dist/stack/stacks.js +1 -1
  59. package/dist/stack/versions.d.ts +2 -2
  60. package/dist/stack/versions.js +3 -3
  61. package/dist/telemetry.d.ts +2 -2
  62. package/dist/telemetry.js +4 -4
  63. package/package.json +10 -9
  64. package/dist/client/access/commands.d.ts +0 -8
  65. package/dist/client/access/commands.js +0 -33
  66. package/dist/client/cli.d.ts +0 -2
  67. package/dist/client/cli.js +0 -106
  68. package/dist/client/installation/commands.d.ts +0 -7
  69. package/dist/client/installation/commands.js +0 -80
  70. package/dist/client/project/connection.d.ts +0 -36
  71. package/dist/client/project/connection.js +0 -80
  72. package/dist/client/project/env.d.ts +0 -3
  73. package/dist/client/project/env.js +0 -19
  74. package/dist/stack/mcp.d.ts +0 -9
  75. package/dist/stack/sandbox.d.ts +0 -4
  76. package/dist/stack/sandbox.js +0 -106
@@ -1,7 +1,6 @@
1
1
  import { release } from "node:os";
2
- import { findProjectRoot } from "@nylorun/core/project";
3
2
  import { CliError } from "../errors.js";
4
- import { linkedConnection, managementClient } from "./project/connection.js";
3
+ import { connect, managementClient } from "./target.js";
5
4
  function platformLine() {
6
5
  const os = process.platform === "darwin"
7
6
  ? "macOS"
@@ -10,24 +9,26 @@ function platformLine() {
10
9
  : process.platform;
11
10
  return `${os} (kernel ${release()}) · ${process.arch}`;
12
11
  }
13
- /** The Tenant's sandbox report (`GET /v1/tenant/sandbox`, Management API). */
14
- async function fetchSandboxReport() {
15
- const connection = await linkedConnection(findProjectRoot() ?? process.cwd());
16
- return (await managementClient(connection).settings.sandbox.get());
17
- }
18
- /** `nylo doctor sandbox`: Tenant sandbox report via the Management API (F2-4). */
19
- export async function doctorSandbox(options) {
12
+ /**
13
+ * `nylorun doctor sandbox`: the Tenant's sandbox report (`GET /v1/tenant/sandbox`, Management API)
14
+ * of the installation `target.ts` picks (F2-4).
15
+ */
16
+ export async function doctorSandbox(deps, options) {
17
+ const log = (line) => deps.out(line);
20
18
  let report;
21
19
  try {
22
- report = await fetchSandboxReport();
20
+ const connection = await connect(deps, "management", options.tenant);
21
+ report = (await managementClient(deps, connection).settings.sandbox.get());
23
22
  }
24
23
  catch (error) {
24
+ if (error instanceof CliError)
25
+ throw error;
25
26
  throw new CliError(error instanceof Error
26
27
  ? error.message
27
28
  : `Could not read Tenant sandbox status: ${String(error)}`, 1);
28
29
  }
29
30
  if (options.json) {
30
- console.log(JSON.stringify({ platform: platformLine(), ...report }, null, 2));
31
+ log(JSON.stringify({ platform: platformLine(), ...report }, null, 2));
31
32
  return;
32
33
  }
33
34
  const rows = [["platform", platformLine()]];
@@ -42,14 +43,21 @@ export async function doctorSandbox(options) {
42
43
  ? "auto (npx nylorun start seeds the Tenant's sandbox.backend from .env NYLORUN_SANDBOX)"
43
44
  : report.preference,
44
45
  ]);
45
- if (report.config)
46
+ rows.push([
47
+ "sessions",
48
+ "get a sandbox only on an Environment (createSession({ environment }) names one)",
49
+ ]);
50
+ const limits = report.config?.limits;
51
+ if (limits)
46
52
  rows.push([
47
- "default",
48
- report.config.default === "none"
49
- ? "none: sessions get a sandbox only when opened with one (set a default with PUT /v1/tenant/sandbox)"
50
- : report.config.default === "virtual"
51
- ? "virtual: sessions that name no sandbox get one"
52
- : "a Tenant sandbox: sessions that name no sandbox get it",
53
+ "limits",
54
+ [
55
+ limits.idle ? `idle ${limits.idle}` : undefined,
56
+ limits.environments === undefined ? undefined : `${limits.environments} environments`,
57
+ limits.network ? `${limits.network.length} allowed hosts` : undefined,
58
+ ]
59
+ .filter((item) => item !== undefined)
60
+ .join(", ") || "-",
53
61
  ]);
54
62
  rows.push([
55
63
  "selected",
@@ -59,7 +67,7 @@ export async function doctorSandbox(options) {
59
67
  ]);
60
68
  const width = Math.max(...rows.map(([key]) => key.length)) + 2;
61
69
  for (const [key, value] of rows)
62
- console.log(` ${key.padEnd(width)}${value}`);
70
+ log(` ${key.padEnd(width)}${value}`);
63
71
  if (report.backend === "virtual")
64
- console.log("\n The virtual shell emulates bash in the Runtime process; it is not a VM boundary.");
72
+ log("\n The virtual shell emulates bash in the Runtime process; it is not a VM boundary.");
65
73
  }
@@ -0,0 +1,10 @@
1
+ import { type EnvironmentManifestInput } from "@nylorun/agents";
2
+ import type { StackDeps } from "../stack/commands.js";
3
+ export declare const manifestsUsage = " manifests list [--json] each environment manifest's latest version\n manifests get <name> [--version <n>]\n a manifest's latest version (or version n), as JSON\n manifests versions <name> its versions, with the Environments made from each\n manifests save <manifest.json>... [--if-match <n>] [--dry-run]\n save each manifest's next version (a files entry with\n \"from\": a folder beside the file, uploaded by hash);\n --dry-run prints the plan and saves nothing\n manifests rm <name> delete every version (refused while an Environment\n was made from one)";
4
+ /** The manifest in `path`, its `from` folders hashed: the body to save and the bytes to upload. */
5
+ export declare function manifestFrom(path: string): Promise<{
6
+ manifest: EnvironmentManifestInput;
7
+ bytes: Record<string, Uint8Array>;
8
+ }>;
9
+ /** `nylorun environments manifests <command> ...` */
10
+ export declare function manifestsCommand(deps: StackDeps, args: readonly string[], usage: string): Promise<number>;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * `nylorun environments manifests`: the Tenant's environment manifests (protocol 14, D63), named
3
+ * and versioned definitions of a kind of Environment, saved like agents. `save` reads each JSON
4
+ * file (its `$schema` is dropped), turns a `files` entry with `from` (a folder, relative to the
5
+ * file) into files by content hash and uploads those the Runtime lacks, then saves the next
6
+ * version under `If-Match` when `--if-match` is given. `--dry-run` prints the plan and exits 1
7
+ * when the Runtime would refuse the save.
8
+ */
9
+ import { readdir, readFile } from "node:fs/promises";
10
+ import { dirname, join, relative, resolve, sep } from "node:path";
11
+ import { environmentFiles } from "@nylorun/agents";
12
+ import { CliError } from "../errors.js";
13
+ import { integerFlag, parseFlags, positional, printJson, printTable, readJson, runtimeCall, runtimeClient, usageError, } from "./runtime.js";
14
+ export const manifestsUsage = ` manifests list [--json] each environment manifest's latest version
15
+ manifests get <name> [--version <n>]
16
+ a manifest's latest version (or version n), as JSON
17
+ manifests versions <name> its versions, with the Environments made from each
18
+ manifests save <manifest.json>... [--if-match <n>] [--dry-run]
19
+ save each manifest's next version (a files entry with
20
+ "from": a folder beside the file, uploaded by hash);
21
+ --dry-run prints the plan and saves nothing
22
+ manifests rm <name> delete every version (refused while an Environment
23
+ was made from one)`;
24
+ /** Each file under `dir`, by its path relative to it (`/`-separated). */
25
+ async function folderOf(dir) {
26
+ const found = {};
27
+ const visit = async (at) => {
28
+ for (const entry of await readdir(at, { withFileTypes: true })) {
29
+ const path = join(at, entry.name);
30
+ if (entry.isDirectory())
31
+ await visit(path);
32
+ else if (entry.isFile())
33
+ found[relative(dir, path).split(sep).join("/")] = new Uint8Array(await readFile(path));
34
+ }
35
+ };
36
+ try {
37
+ await visit(dir);
38
+ }
39
+ catch (error) {
40
+ throw new CliError(`Cannot read ${dir}: ${error.message}`, 1);
41
+ }
42
+ if (Object.keys(found).length === 0)
43
+ throw new CliError(`${dir} holds no files.`, 1);
44
+ return found;
45
+ }
46
+ /** The manifest in `path`, its `from` folders hashed: the body to save and the bytes to upload. */
47
+ export async function manifestFrom(path) {
48
+ const raw = await readJson(path, "The manifest");
49
+ if (typeof raw.name !== "string" || raw.name === "")
50
+ throw usageError(`${path}: the manifest has no name.`);
51
+ const bytes = {};
52
+ const files = Array.isArray(raw.files) ? raw.files : undefined;
53
+ const entries = [];
54
+ for (const entry of files ?? []) {
55
+ if (typeof entry?.from !== "string") {
56
+ entries.push(entry);
57
+ continue;
58
+ }
59
+ const { from, ...rest } = entry;
60
+ if (typeof rest.path !== "string")
61
+ throw usageError(`${path}: a files entry with from needs a path.`);
62
+ const folder = await environmentFiles(rest.path, await folderOf(resolve(dirname(path), from)));
63
+ Object.assign(bytes, folder.bytes);
64
+ entries.push({ ...rest, files: folder.entry.files });
65
+ }
66
+ const { $schema: _schema, ...manifest } = raw;
67
+ return {
68
+ manifest: (files === undefined ? manifest : { ...manifest, files: entries }),
69
+ bytes,
70
+ };
71
+ }
72
+ async function save(deps, args) {
73
+ const usage = "nylorun environments manifests save <manifest.json>... [--if-match <n>] [--dry-run]";
74
+ const flags = parseFlags(args, { booleans: ["--dry-run"], values: ["--if-match"] }, usage);
75
+ if (flags.rest.length === 0)
76
+ throw usageError(`Usage: ${usage}`);
77
+ const ifMatch = integerFlag(flags, "--if-match");
78
+ if (ifMatch !== undefined && flags.rest.length > 1)
79
+ throw usageError("--if-match takes one manifest.");
80
+ const { client, connection } = await runtimeClient(deps, flags);
81
+ let blocked = false;
82
+ for (const path of flags.rest) {
83
+ const { manifest, bytes } = await manifestFrom(path);
84
+ const options = { files: bytes, ...(ifMatch === undefined ? {} : { ifMatch }) };
85
+ if (flags.booleans.has("--dry-run")) {
86
+ const plan = await runtimeCall(() => client.environments.manifests.plan(manifest, options));
87
+ printJson(deps, plan);
88
+ if (plan.errors.length > 0)
89
+ blocked = true;
90
+ continue;
91
+ }
92
+ const saved = await runtimeCall(() => client.environments.manifests.save(manifest, options));
93
+ deps.err(saved.unchanged
94
+ ? `Environment manifest ${saved.manifest.name} is unchanged at version ${saved.manifest.version} on ${connection.label}.`
95
+ : `Saved environment manifest ${saved.manifest.name} version ${saved.manifest.version} on ${connection.label}.`);
96
+ for (const warning of saved.warnings)
97
+ deps.err(` warning: ${warning.message}`);
98
+ printJson(deps, saved);
99
+ }
100
+ return blocked ? 1 : 0;
101
+ }
102
+ async function list(deps, args) {
103
+ const usage = "nylorun environments manifests list [--json]";
104
+ const flags = parseFlags(args, { booleans: ["--json"] }, usage);
105
+ positional(flags, 0, usage);
106
+ const { client, connection } = await runtimeClient(deps, flags);
107
+ const manifests = await runtimeCall(() => client.environments.manifests.list());
108
+ if (flags.booleans.has("--json")) {
109
+ printJson(deps, manifests);
110
+ return 0;
111
+ }
112
+ if (manifests.length === 0)
113
+ deps.out(`No environment manifests on ${connection.label}.`);
114
+ else
115
+ printTable(deps, [
116
+ ["NAME", "VERSION", "KIND", "ENVIRONMENTS", "SAVED BY"],
117
+ ...manifests.map((manifest) => [
118
+ manifest.name,
119
+ String(manifest.version),
120
+ manifest.manifest.kind ?? "virtual",
121
+ String(manifest.environments),
122
+ manifest.savedBy.actor,
123
+ ]),
124
+ ]);
125
+ return 0;
126
+ }
127
+ async function get(deps, args) {
128
+ const usage = "nylorun environments manifests get <name> [--version <n>]";
129
+ const flags = parseFlags(args, { values: ["--version"] }, usage);
130
+ const [name] = positional(flags, 1, usage);
131
+ const version = integerFlag(flags, "--version");
132
+ const { client } = await runtimeClient(deps, flags);
133
+ printJson(deps, await runtimeCall(() => client.environments.manifests.get(name, version === undefined ? {} : { version })));
134
+ return 0;
135
+ }
136
+ async function versions(deps, args) {
137
+ const usage = "nylorun environments manifests versions <name>";
138
+ const flags = parseFlags(args, {}, usage);
139
+ const [name] = positional(flags, 1, usage);
140
+ const { client } = await runtimeClient(deps, flags);
141
+ printJson(deps, await runtimeCall(() => client.environments.manifests.versions(name)));
142
+ return 0;
143
+ }
144
+ async function remove(deps, args) {
145
+ const usage = "nylorun environments manifests rm <name>";
146
+ const flags = parseFlags(args, {}, usage);
147
+ const [name] = positional(flags, 1, usage);
148
+ const { client, connection } = await runtimeClient(deps, flags);
149
+ const deleted = await runtimeCall(() => client.environments.manifests.delete(name));
150
+ if (!deleted.deleted) {
151
+ deps.err(`No environment manifest ${name} on ${connection.label}.`);
152
+ return 1;
153
+ }
154
+ deps.out(`Deleted environment manifest ${name}: every version.`);
155
+ return 0;
156
+ }
157
+ const COMMANDS = {
158
+ list,
159
+ ls: list,
160
+ get,
161
+ versions,
162
+ save,
163
+ rm: remove,
164
+ delete: remove,
165
+ };
166
+ /** `nylorun environments manifests <command> ...` */
167
+ export async function manifestsCommand(deps, args, usage) {
168
+ const [sub, ...rest] = args;
169
+ const command = sub !== undefined && Object.hasOwn(COMMANDS, sub) ? COMMANDS[sub] : undefined;
170
+ if (!command)
171
+ throw usageError(`Usage: nylorun environments manifests <list|get|versions|save|rm> …\n\n${usage}`);
172
+ return command(deps, rest);
173
+ }
@@ -0,0 +1,4 @@
1
+ import type { StackDeps } from "../stack/commands.js";
2
+ export declare const environmentsUsage: string;
3
+ /** `nylorun environments <command> ...` */
4
+ export declare function environmentsCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
@@ -0,0 +1,295 @@
1
+ import { manifestsCommand, manifestsUsage } from "./environment-manifests.js";
2
+ import { readSecret } from "./io.js";
3
+ import { integerFlag, labelsOf, parseFlags, positional, printCursor, printJson, printTable, readJson, runtimeCall, runtimeClient, runtimeFlagsUsage, usageError, } from "./runtime.js";
4
+ export const environmentsUsage = `nylorun environments <command>
5
+
6
+ The Tenant's Environments (one sandbox and one vault each), through the Runtime API with an
7
+ application key (or NYLORUN_TOKEN).
8
+
9
+ list [--label <key=value>]... [--behind] [--limit <n>] [--cursor <c>] [--json]
10
+ id, kind, manifest, state, sessions and labels;
11
+ --behind: those made from an older manifest version
12
+ get <id> the Environment, as JSON
13
+ put <id> --manifest <name[@n]> [--label <key=value>]...
14
+ put <id> [<inline.json|->] [--kind <virtual|pod|none>] [--label <key=value>]...
15
+ create it from a manifest (its latest version, or n)
16
+ or inline fields, or find it and change its labels
17
+ (what it runs never changes: fork it for that)
18
+ fork <id> <new-id> [--manifest <name[@n]>] [--disk <latest|n|empty>]
19
+ [--secrets <copy|none>] [--label <key=value>]... [--dry-run]
20
+ a new Environment from a manifest version (default
21
+ its manifest's latest), with this one's disk and the
22
+ secrets it declares; --dry-run prints the plan
23
+ stop <id> | reset <id> stop its sandbox; reset its disk
24
+ delete <id> delete it: its sandbox, files and vault (refused while a
25
+ turn runs in it)
26
+ snapshot <id> keep a version of its disk
27
+ versions <id> its disk versions
28
+ restore <id> <version|latest> put a version of its disk back
29
+ delete-version <id> <version>
30
+ events <id> [--from <n>] [--types <type,…>] [--since <time>] [--until <time>]
31
+ its events (lifecycle, and environment.egress for every
32
+ connection, naming the turns running), as JSON
33
+ secrets list <id> its secrets' names, types and bindings (never a value)
34
+ secrets get <id> <name>
35
+ secrets put <id> <name> the value of a slot its manifest declares, piped in
36
+ on standard input
37
+ secrets put <id> <name> <secret.json|->
38
+ create or replace a secret: {"type": "bearer", ...}, read
39
+ from a file or standard input
40
+ secrets rm <id> <name>
41
+ ${manifestsUsage}
42
+
43
+ ${runtimeFlagsUsage}`;
44
+ async function list(deps, args) {
45
+ const usage = "nylorun environments list [--label <key=value>]... [--behind] [--limit <n>] [--cursor <c>] [--json]";
46
+ const flags = parseFlags(args, { booleans: ["--json", "--behind"], values: ["--limit", "--cursor"], lists: ["--label"] }, usage);
47
+ positional(flags, 0, usage);
48
+ const labels = labelsOf(flags);
49
+ const limit = integerFlag(flags, "--limit");
50
+ const cursor = flags.values.get("--cursor");
51
+ const { client, connection } = await runtimeClient(deps, flags);
52
+ const paged = limit !== undefined || cursor !== undefined;
53
+ const page = paged
54
+ ? await runtimeCall(() => client.environments.page({
55
+ ...(labels ? { labels } : {}),
56
+ ...(limit === undefined ? {} : { limit }),
57
+ ...(cursor ? { cursor } : {}),
58
+ }))
59
+ : {
60
+ environments: await runtimeCall(() => client.environments.list(labels ? { labels } : {})),
61
+ nextCursor: null,
62
+ };
63
+ let environments = page.environments;
64
+ if (flags.booleans.has("--behind")) {
65
+ // Made from a manifest version older than its latest: fork them to move on (D63).
66
+ const latest = new Map((await runtimeCall(() => client.environments.manifests.list())).map((manifest) => [manifest.name, manifest.version]));
67
+ environments = environments.filter((environment) => environment.manifestName !== undefined &&
68
+ (environment.manifestVersion ?? 0) < (latest.get(environment.manifestName) ?? 0));
69
+ }
70
+ if (flags.booleans.has("--json")) {
71
+ printJson(deps, paged ? page : environments);
72
+ return 0;
73
+ }
74
+ if (environments.length === 0)
75
+ deps.out(`No Environments on ${connection.label}.`);
76
+ else
77
+ printTable(deps, [
78
+ ["ID", "KIND", "MANIFEST", "STATE", "SESSIONS", "LABELS"],
79
+ ...environments.map((environment) => [
80
+ environment.id,
81
+ environment.kind,
82
+ environment.manifestName === undefined ? "-" : `${environment.manifestName}@${environment.manifestVersion}`,
83
+ environment.state,
84
+ String(environment.sessions.length),
85
+ Object.entries(environment.labels)
86
+ .map(([key, value]) => `${key}=${value}`)
87
+ .join(",") || "-",
88
+ ]),
89
+ ]);
90
+ printCursor(deps, page.nextCursor);
91
+ return 0;
92
+ }
93
+ /** A command that takes one Environment id and prints the Runtime's answer. */
94
+ function byId(name, call) {
95
+ return async (deps, args) => {
96
+ const usage = `nylorun environments ${name} <id>`;
97
+ const flags = parseFlags(args, {}, usage);
98
+ const [id] = positional(flags, 1, usage);
99
+ const { client } = await runtimeClient(deps, flags);
100
+ printJson(deps, await runtimeCall(() => call(client, id)));
101
+ return 0;
102
+ };
103
+ }
104
+ async function put(deps, args) {
105
+ const usage = "nylorun environments put <id> [--manifest <name[@n]> | <inline.json|-> [--kind <virtual|pod|none>]] [--label <key=value>]...";
106
+ const flags = parseFlags(args, { values: ["--kind", "--manifest"], lists: ["--label"] }, usage);
107
+ if (flags.rest.length < 1 || flags.rest.length > 2)
108
+ throw usageError(`Usage: ${usage}`);
109
+ const [id, path] = flags.rest;
110
+ const manifest = flags.values.get("--manifest");
111
+ const kind = flags.values.get("--kind");
112
+ if (manifest !== undefined && (path !== undefined || kind !== undefined))
113
+ throw usageError("--manifest makes it from the manifest: give no inline fields or --kind with it.");
114
+ const recipe = path === undefined ? {} : await readJson(path, "The inline fields");
115
+ const labels = labelsOf(flags);
116
+ const body = {
117
+ ...recipe,
118
+ ...(manifest ? { manifest } : {}),
119
+ ...(kind ? { kind } : {}),
120
+ ...(labels ? { labels: { ...recipe.labels, ...labels } } : {}),
121
+ };
122
+ const { client } = await runtimeClient(deps, flags);
123
+ printJson(deps, await runtimeCall(() => client.environments.put(id, body)));
124
+ return 0;
125
+ }
126
+ async function fork(deps, args) {
127
+ const usage = "nylorun environments fork <id> <new-id> [--manifest <name[@n]>] [--disk <latest|n|empty>] [--secrets <copy|none>] [--label <key=value>]... [--dry-run]";
128
+ const flags = parseFlags(args, { booleans: ["--dry-run"], values: ["--manifest", "--disk", "--secrets"], lists: ["--label"] }, usage);
129
+ const [id, next] = positional(flags, 2, usage);
130
+ const disk = flags.values.get("--disk");
131
+ if (disk !== undefined && disk !== "latest" && disk !== "empty" && !/^[1-9]\d*$/.test(disk))
132
+ throw usageError("--disk is latest, empty or a version number.");
133
+ const secrets = flags.values.get("--secrets");
134
+ if (secrets !== undefined && secrets !== "copy" && secrets !== "none")
135
+ throw usageError("--secrets is copy or none.");
136
+ const manifest = flags.values.get("--manifest");
137
+ const labels = labelsOf(flags);
138
+ const options = {
139
+ id: next,
140
+ ...(manifest ? { manifest } : {}),
141
+ ...(disk === undefined ? {} : { disk: disk === "latest" || disk === "empty" ? disk : Number(disk) }),
142
+ ...(secrets ? { secrets: secrets } : {}),
143
+ ...(labels ? { labels } : {}),
144
+ };
145
+ const { client, connection } = await runtimeClient(deps, flags);
146
+ if (flags.booleans.has("--dry-run")) {
147
+ printJson(deps, await runtimeCall(() => client.environments.planFork(id, options)));
148
+ return 0;
149
+ }
150
+ const forked = await runtimeCall(() => client.environments.fork(id, options));
151
+ const { plan } = forked;
152
+ deps.err(`Forked Environment ${id} into ${next} on ${connection.label}: ${plan.manifest ? `${plan.manifest.name}@${plan.manifest.version}` : "its fields"}, ${plan.disk === null ? "an empty disk" : `disk version ${plan.disk}`}, ${plan.secrets.copied.length} secrets copied${plan.setup ? "; its setup runs before the first turn" : ""}.`);
153
+ if (plan.secrets.missing.length > 0)
154
+ deps.err(` set before use: ${plan.secrets.missing.join(", ")} (nylorun environments secrets put ${next} <name>)`);
155
+ printJson(deps, forked);
156
+ return 0;
157
+ }
158
+ async function deleteEnvironment(deps, args) {
159
+ const usage = "nylorun environments delete <id>";
160
+ const flags = parseFlags(args, {}, usage);
161
+ const [id] = positional(flags, 1, usage);
162
+ const { client, connection } = await runtimeClient(deps, flags);
163
+ const deleted = await runtimeCall(async () => {
164
+ try {
165
+ return await client.environments.delete(id);
166
+ }
167
+ catch (error) {
168
+ if (error.status === 404)
169
+ return undefined;
170
+ throw error;
171
+ }
172
+ });
173
+ if (!deleted || deleted.deleted === false) {
174
+ deps.err(`No Environment ${id} on ${connection.label}.`);
175
+ return 1;
176
+ }
177
+ deps.out(`Deleted Environment ${id}: its sandbox, files and vault.`);
178
+ return 0;
179
+ }
180
+ async function restore(deps, args) {
181
+ const usage = "nylorun environments restore <id> <version|latest>";
182
+ const flags = parseFlags(args, {}, usage);
183
+ const [id, version] = positional(flags, 2, usage);
184
+ if (version !== "latest" && !/^[1-9]\d*$/.test(version))
185
+ throw usageError("The version is a positive number or latest.");
186
+ const { client } = await runtimeClient(deps, flags);
187
+ printJson(deps, await runtimeCall(() => client.environments.restore(id, version === "latest" ? "latest" : Number(version))));
188
+ return 0;
189
+ }
190
+ async function deleteVersion(deps, args) {
191
+ const usage = "nylorun environments delete-version <id> <version>";
192
+ const flags = parseFlags(args, {}, usage);
193
+ const [id, version] = positional(flags, 2, usage);
194
+ if (!/^[1-9]\d*$/.test(version))
195
+ throw usageError("The version is a positive number.");
196
+ const { client } = await runtimeClient(deps, flags);
197
+ printJson(deps, await runtimeCall(() => client.environments.deleteVersion(id, Number(version))));
198
+ return 0;
199
+ }
200
+ async function events(deps, args) {
201
+ const usage = "nylorun environments events <id> [--from <n>] [--types <type,…>] [--since <time>] [--until <time>]";
202
+ const flags = parseFlags(args, { values: ["--from", "--types", "--since", "--until"] }, usage);
203
+ const [id] = positional(flags, 1, usage);
204
+ const from = integerFlag(flags, "--from");
205
+ const types = flags.values.get("--types")?.split(",").map((type) => type.trim()).filter(Boolean);
206
+ const since = flags.values.get("--since");
207
+ const until = flags.values.get("--until");
208
+ const { client } = await runtimeClient(deps, flags);
209
+ printJson(deps, await runtimeCall(() => client.environments.events(id, {
210
+ ...(from === undefined ? {} : { from }),
211
+ ...(types === undefined || types.length === 0 ? {} : { types }),
212
+ ...(since === undefined ? {} : { since }),
213
+ ...(until === undefined ? {} : { until }),
214
+ })));
215
+ return 0;
216
+ }
217
+ async function secrets(deps, args) {
218
+ const [verb, ...rest] = args;
219
+ if (verb === "list" || verb === "ls") {
220
+ const usage = "nylorun environments secrets list <id>";
221
+ const flags = parseFlags(rest, {}, usage);
222
+ const [id] = positional(flags, 1, usage);
223
+ const { client } = await runtimeClient(deps, flags);
224
+ printJson(deps, await runtimeCall(() => client.environments.secrets.list(id)));
225
+ return 0;
226
+ }
227
+ if (verb === "get") {
228
+ const usage = "nylorun environments secrets get <id> <name>";
229
+ const flags = parseFlags(rest, {}, usage);
230
+ const [id, name] = positional(flags, 2, usage);
231
+ const { client } = await runtimeClient(deps, flags);
232
+ printJson(deps, await runtimeCall(() => client.environments.secrets.get(id, name)));
233
+ return 0;
234
+ }
235
+ if (verb === "put") {
236
+ const usage = "nylorun environments secrets put <id> <name> [<secret.json|->]";
237
+ const flags = parseFlags(rest, {}, usage);
238
+ if (flags.rest.length < 2 || flags.rest.length > 3)
239
+ throw usageError(`Usage: ${usage}`);
240
+ const [id, name, path] = flags.rest;
241
+ if (path === undefined) {
242
+ // A slot its manifest declares (D64): the value only, piped in; the binding is the slot's.
243
+ const value = await readSecret(deps, `value of ${name}`, `printf %s "$TOKEN" | nylorun environments secrets put ${id} ${name}`);
244
+ const { client } = await runtimeClient(deps, flags);
245
+ printJson(deps, await runtimeCall(() => client.environments.secrets.put(id, name, { value })));
246
+ return 0;
247
+ }
248
+ const secret = await readJson(path, "The secret");
249
+ if (typeof secret.type !== "string")
250
+ throw usageError('The secret needs a "type".');
251
+ const { client } = await runtimeClient(deps, flags);
252
+ printJson(deps, await runtimeCall(() => client.environments.secrets.put(id, name, secret)));
253
+ return 0;
254
+ }
255
+ if (verb === "rm" || verb === "delete") {
256
+ const usage = "nylorun environments secrets rm <id> <name>";
257
+ const flags = parseFlags(rest, {}, usage);
258
+ const [id, name] = positional(flags, 2, usage);
259
+ const { client } = await runtimeClient(deps, flags);
260
+ await runtimeCall(() => client.environments.secrets.delete(id, name));
261
+ deps.out(`Deleted secret ${name} of Environment ${id}.`);
262
+ return 0;
263
+ }
264
+ throw usageError(`Usage: nylorun environments secrets <list|get|put|rm> …\n\n${environmentsUsage}`);
265
+ }
266
+ const COMMANDS = {
267
+ list,
268
+ ls: list,
269
+ get: byId("get", (client, id) => client.environments.get(id)),
270
+ put,
271
+ stop: byId("stop", (client, id) => client.environments.stop(id)),
272
+ reset: byId("reset", (client, id) => client.environments.reset(id)),
273
+ delete: deleteEnvironment,
274
+ rm: deleteEnvironment,
275
+ snapshot: byId("snapshot", (client, id) => client.environments.snapshot(id)),
276
+ versions: byId("versions", (client, id) => client.environments.versions(id)),
277
+ restore,
278
+ "delete-version": deleteVersion,
279
+ events,
280
+ secrets,
281
+ fork,
282
+ manifests: (deps, args) => manifestsCommand(deps, args, manifestsUsage),
283
+ };
284
+ /** `nylorun environments <command> ...` */
285
+ export async function environmentsCommand(deps, args) {
286
+ const [sub, ...rest] = args;
287
+ if (sub === "--help" || sub === "-h") {
288
+ deps.out(environmentsUsage);
289
+ return 0;
290
+ }
291
+ const command = sub !== undefined && Object.hasOwn(COMMANDS, sub) ? COMMANDS[sub] : undefined;
292
+ if (!command)
293
+ throw usageError(sub ? `Unknown environments command ${sub}.\n${environmentsUsage}` : environmentsUsage);
294
+ return command(deps, rest);
295
+ }
@@ -0,0 +1,31 @@
1
+ import type { StackDeps } from "../stack/commands.js";
2
+ /** A request schema (`@nylorun/core/contracts`), as far as reading a body needs it. */
3
+ export interface BodySchema<T> {
4
+ safeParse(value: unknown): {
5
+ success: true;
6
+ data: T;
7
+ } | {
8
+ success: false;
9
+ error: {
10
+ issues: readonly {
11
+ path: readonly PropertyKey[];
12
+ message: string;
13
+ }[];
14
+ };
15
+ };
16
+ }
17
+ /**
18
+ * The JSON body in file `source`, or on standard input for `-`, as `schema` parses it. Fields
19
+ * named in `readOnly` (a view's, so a `get` can be edited and put back) are dropped first. A
20
+ * body that does not parse is a usage error, before any request.
21
+ */
22
+ export declare function readBody<T>(deps: StackDeps, source: string, schema: BodySchema<T>, readOnly?: readonly string[]): Promise<T>;
23
+ /**
24
+ * A secret piped in on standard input (its trailing newline dropped), so it is never in the
25
+ * arguments, the shell's history or the process list. `what` names it in the refusals.
26
+ */
27
+ export declare function readSecret(deps: StackDeps, what: string, example: string): Promise<string>;
28
+ /** `rows` (the first is the header) as columns two spaces apart, with no trailing blanks. */
29
+ export declare function printTable(deps: StackDeps, rows: readonly (readonly string[])[]): void;
30
+ /** `value` as indented JSON on stdout. */
31
+ export declare function printJson(deps: StackDeps, value: unknown): void;