nylorun 0.6.0-beta → 0.8.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 +61 -0
- package/README.md +77 -73
- package/dist/cli.js +7 -2
- package/dist/doctor.d.ts +2 -4
- package/dist/doctor.js +12 -19
- package/dist/sandbox/cluster-file.d.ts +47 -0
- package/dist/sandbox/cluster-file.js +55 -0
- package/dist/sandbox/commands.d.ts +41 -0
- package/dist/sandbox/commands.js +281 -0
- package/dist/sandbox/kubectl.d.ts +39 -0
- package/dist/sandbox/kubectl.js +70 -0
- package/dist/sandbox/manifests.d.ts +41 -0
- package/dist/sandbox/manifests.js +138 -0
- package/dist/sandbox/probe.d.ts +44 -0
- package/dist/sandbox/probe.js +137 -0
- package/dist/stack/commands.d.ts +56 -9
- package/dist/stack/commands.js +167 -123
- package/dist/stack/compose-file.d.ts +42 -5
- package/dist/stack/compose-file.js +208 -21
- package/dist/stack/env-file.d.ts +48 -6
- package/dist/stack/env-file.js +81 -9
- package/dist/stack/host-files.js +11 -1
- package/dist/stack/images.d.ts +8 -4
- package/dist/stack/images.js +8 -3
- package/dist/stack/index.d.ts +4 -2
- package/dist/stack/index.js +7 -3
- package/dist/stack/paths.d.ts +15 -0
- package/dist/stack/paths.js +7 -0
- package/dist/stack/ports.d.ts +0 -2
- package/dist/stack/ports.js +0 -2
- package/dist/stack/prepare.d.ts +10 -4
- package/dist/stack/prepare.js +20 -3
- package/dist/stack/sandbox.d.ts +4 -0
- package/dist/stack/sandbox.js +106 -0
- package/dist/stack/stacks.d.ts +8 -0
- package/dist/stack/stacks.js +19 -0
- package/dist/stack/studio-login.d.ts +2 -2
- package/dist/stack/studio-login.js +2 -2
- package/package.json +5 -5
- package/dist/stack/proxy.d.ts +0 -78
- package/dist/stack/proxy.js +0 -224
package/dist/stack/images.js
CHANGED
|
@@ -2,14 +2,17 @@ import { pinnedVersion } from "./versions.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Images a local Tenant runs. The Runtime and Studio tags are pinned by this
|
|
4
4
|
* CLI release; `NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them
|
|
5
|
-
* (local builds, CI images).
|
|
6
|
-
*
|
|
5
|
+
* (local builds, CI images). The sandboxes service (`nylorun sandbox enable`) is
|
|
6
|
+
* versioned with the Runtime: `ghcr.io/nylorun/sandboxes:<runtime pin>`, or
|
|
7
|
+
* `NYLORUN_SANDBOXES_IMAGE`. Postgres, Restate, s2-lite and RustFS are the
|
|
8
|
+
* official images, pinned here; RustFS (the Object store, D35) by digest as well, so
|
|
9
|
+
* it is upgraded only on purpose.
|
|
7
10
|
*/
|
|
8
11
|
export const PINNED_IMAGES = {
|
|
9
12
|
postgres: "postgres:17.11",
|
|
10
13
|
restate: "docker.restate.dev/restatedev/restate:1.7.12",
|
|
11
14
|
s2: "ghcr.io/s2-streamstore/s2:0.43.0",
|
|
12
|
-
|
|
15
|
+
rustfs: "rustfs/rustfs:1.0.1@sha256:1803faef57627e2d9c2e7d89d655d712ddded5389040054987163043fecb6a3c",
|
|
13
16
|
};
|
|
14
17
|
function override(env, name) {
|
|
15
18
|
const value = env[name]?.trim();
|
|
@@ -28,6 +31,8 @@ export function stackImages(env, versions = {
|
|
|
28
31
|
`ghcr.io/nylorun/runtime:${versions.runtime}`,
|
|
29
32
|
studio: override(env, "NYLORUN_STUDIO_IMAGE") ??
|
|
30
33
|
`ghcr.io/nylorun/studio:${versions.studio}`,
|
|
34
|
+
sandboxes: override(env, "NYLORUN_SANDBOXES_IMAGE") ??
|
|
35
|
+
`ghcr.io/nylorun/sandboxes:${versions.runtime}`,
|
|
31
36
|
...PINNED_IMAGES,
|
|
32
37
|
};
|
|
33
38
|
}
|
package/dist/stack/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { type StackDeps } from "./commands.js";
|
|
2
|
-
export { ensureStack, isStackCommand,
|
|
2
|
+
export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
|
|
3
3
|
export type { StackDeps, StackEndpoints, StackStatus, StackTenant } from "./commands.js";
|
|
4
4
|
export { checkDocker } from "./docker.js";
|
|
5
5
|
export type { Check, DockerChecks } from "./docker.js";
|
|
6
|
-
export type { ProxyStatus } from "./proxy.js";
|
|
7
6
|
/** Real dependencies: docker on PATH, global fetch, the terminal. */
|
|
8
7
|
export declare function defaultStackDeps(env: Readonly<Record<string, string | undefined>>): StackDeps;
|
|
9
8
|
/** Entry for `nylorun start|stop|status|logs|reset|studio|ls|delete`. */
|
|
10
9
|
export declare function stackCommand(name: string, args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
|
|
10
|
+
/** Entry for `nylorun sandbox ls|rm`. */
|
|
11
|
+
export declare function sandboxCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
|
|
12
|
+
export { sandboxUsage } from "./sandbox.js";
|
|
11
13
|
/** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
|
|
12
14
|
export declare function studioCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>, options?: {
|
|
13
15
|
next?: string;
|
package/dist/stack/index.js
CHANGED
|
@@ -2,9 +2,10 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { runStackCommand, runStudioCommand } from "./commands.js";
|
|
4
4
|
import { spawnDocker } from "./docker.js";
|
|
5
|
+
import { sandboxCommand as runSandboxCommand } from "./sandbox.js";
|
|
5
6
|
import { pinnedVersion } from "./versions.js";
|
|
6
7
|
import { loopbackPorts } from "./ports.js";
|
|
7
|
-
export { ensureStack, isStackCommand,
|
|
8
|
+
export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
|
|
8
9
|
export { checkDocker } from "./docker.js";
|
|
9
10
|
function browserCommand(env) {
|
|
10
11
|
if (process.platform === "darwin")
|
|
@@ -21,8 +22,6 @@ const STACK_ENV_OWNED = [
|
|
|
21
22
|
"NYLORUN_TENANT_NAME",
|
|
22
23
|
"NYLORUN_DERIVED_PRINCIPALS",
|
|
23
24
|
"NYLORUN_STUDIO_ANALYTICS_ID",
|
|
24
|
-
"NYLORUN_STUDIO_PUBLIC_ORIGINS",
|
|
25
|
-
"NYLORUN_PROXY_PORT",
|
|
26
25
|
];
|
|
27
26
|
/** Real dependencies: docker on PATH, global fetch, the terminal. */
|
|
28
27
|
export function defaultStackDeps(env) {
|
|
@@ -81,6 +80,11 @@ export function defaultStackDeps(env) {
|
|
|
81
80
|
export async function stackCommand(name, args, env) {
|
|
82
81
|
return await runStackCommand(name, args, defaultStackDeps(env));
|
|
83
82
|
}
|
|
83
|
+
/** Entry for `nylorun sandbox ls|rm`. */
|
|
84
|
+
export async function sandboxCommand(args, env) {
|
|
85
|
+
return await runSandboxCommand(defaultStackDeps(env), args);
|
|
86
|
+
}
|
|
87
|
+
export { sandboxUsage } from "./sandbox.js";
|
|
84
88
|
/** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
|
|
85
89
|
export async function studioCommand(args, env, options = {}) {
|
|
86
90
|
return await runStudioCommand(args, defaultStackDeps(env), options);
|
package/dist/stack/paths.d.ts
CHANGED
|
@@ -12,6 +12,21 @@ export interface StackPaths {
|
|
|
12
12
|
record: string;
|
|
13
13
|
/** The Tenant directory: homes, sandboxes, plugin data, logs. */
|
|
14
14
|
tenant: string;
|
|
15
|
+
/**
|
|
16
|
+
* The Tenant directory's parts the harness container mounts (F6.2): workspaces, plugin data,
|
|
17
|
+
* and the home and temporary directory of MCP stdio servers.
|
|
18
|
+
*/
|
|
19
|
+
harness: {
|
|
20
|
+
sandboxes: string;
|
|
21
|
+
pluginData: string;
|
|
22
|
+
home: string;
|
|
23
|
+
tmp: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Plugin roots on this machine (`plugins/`), mounted read-only at the same path into the
|
|
27
|
+
* runtime and harness containers, so a stdio MCP server's plugin root resolves there.
|
|
28
|
+
*/
|
|
29
|
+
plugins: string;
|
|
15
30
|
/** The keys directory, mounted only into the gateway (F4.2). */
|
|
16
31
|
keys: string;
|
|
17
32
|
/** The Tenant's vault key (KEK), `keys/vault-kek`. */
|
package/dist/stack/paths.js
CHANGED
|
@@ -9,6 +9,13 @@ export function stackPaths(hostRoot) {
|
|
|
9
9
|
state: join(root, "host-state.json"),
|
|
10
10
|
record: join(root, "tenant.json"),
|
|
11
11
|
tenant: join(root, "tenant"),
|
|
12
|
+
harness: {
|
|
13
|
+
sandboxes: join(root, "tenant", "sandboxes"),
|
|
14
|
+
pluginData: join(root, "tenant", "plugin-data"),
|
|
15
|
+
home: join(root, "tenant", "home"),
|
|
16
|
+
tmp: join(root, "tenant", "tmp"),
|
|
17
|
+
},
|
|
18
|
+
plugins: join(root, "plugins"),
|
|
12
19
|
keys: join(root, "keys"),
|
|
13
20
|
vaultKey: join(root, "keys", "vault-kek"),
|
|
14
21
|
home: join(root, "home"),
|
package/dist/stack/ports.d.ts
CHANGED
package/dist/stack/ports.js
CHANGED
package/dist/stack/prepare.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type StackEnv } from "./env-file.js";
|
|
1
|
+
import { type SandboxStackEnv, type StackEnv } from "./env-file.js";
|
|
2
2
|
import { type HostConfigFile } from "./host-files.js";
|
|
3
3
|
import type { StackImages } from "./images.js";
|
|
4
4
|
import type { StackPaths } from "./paths.js";
|
|
@@ -16,7 +16,8 @@ export declare function readStackEnv(paths: Pick<StackPaths, "env">): Promise<im
|
|
|
16
16
|
* Write everything `docker compose up` needs under the Host root:
|
|
17
17
|
* host.json, host-credentials.json (0600), docker/compose.yaml,
|
|
18
18
|
* docker/.env (0600) and docker/restate-identity.pem (0600). Ports and the
|
|
19
|
-
* Postgres password persist in .env; the identity key persists in its PEM.
|
|
19
|
+
* Postgres password persist in .env; the identity key persists in its PEM. The harness token
|
|
20
|
+
* and mode persist too; whether Restate's UI is published is decided on every start.
|
|
20
21
|
*/
|
|
21
22
|
export declare function prepareStack(input: {
|
|
22
23
|
paths: StackPaths;
|
|
@@ -43,6 +44,11 @@ export declare function prepareStack(input: {
|
|
|
43
44
|
};
|
|
44
45
|
/** The measurement id Studio reports page views to; absent when telemetry is off. */
|
|
45
46
|
studioAnalyticsId?: string;
|
|
46
|
-
/**
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The sandboxes settings (`nylorun sandbox enable`); null removes them (`disable`).
|
|
49
|
+
* Undefined keeps the persisted ones while `sandboxes/cluster.json` exists.
|
|
50
|
+
*/
|
|
51
|
+
sandboxes?: SandboxStackEnv | null;
|
|
52
|
+
/** Publish Restate's UI and admin port (`start --restate-ui`); undefined keeps the last start's choice. */
|
|
53
|
+
restateUi?: boolean;
|
|
48
54
|
}): Promise<PreparedStack>;
|
package/dist/stack/prepare.js
CHANGED
|
@@ -4,6 +4,7 @@ import { renderComposeFile } from "./compose-file.js";
|
|
|
4
4
|
import { parseFrameAncestors } from "@nylorun/core/contracts";
|
|
5
5
|
import { CliError } from "../errors.js";
|
|
6
6
|
import { DEFAULT_STUDIO_FRAME_ANCESTORS, parseDerivedPrincipals, parsePersisted, renderEnvFile, } from "./env-file.js";
|
|
7
|
+
import { sandboxesEnabled } from "../sandbox/cluster-file.js";
|
|
7
8
|
import { ensureHostCredentials, ensureVaultKey, ensureHostLayout, writeStackHostConfig, } from "./host-files.js";
|
|
8
9
|
import { choosePort, DEFAULT_PORTS } from "./ports.js";
|
|
9
10
|
import { ensureIdentityKey } from "./restate-identity.js";
|
|
@@ -30,7 +31,8 @@ export async function readStackEnv(paths) {
|
|
|
30
31
|
* Write everything `docker compose up` needs under the Host root:
|
|
31
32
|
* host.json, host-credentials.json (0600), docker/compose.yaml,
|
|
32
33
|
* docker/.env (0600) and docker/restate-identity.pem (0600). Ports and the
|
|
33
|
-
* Postgres password persist in .env; the identity key persists in its PEM.
|
|
34
|
+
* Postgres password persist in .env; the identity key persists in its PEM. The harness token
|
|
35
|
+
* and mode persist too; whether Restate's UI is published is decided on every start.
|
|
34
36
|
*/
|
|
35
37
|
export async function prepareStack(input) {
|
|
36
38
|
const { paths } = input;
|
|
@@ -69,6 +71,12 @@ export async function prepareStack(input) {
|
|
|
69
71
|
postgresPassword: persisted.postgresPassword ?? randomBytes(24).toString("hex"),
|
|
70
72
|
// Kept across starts: a new token would recreate the runtime and gateway containers.
|
|
71
73
|
gatesToken: persisted.gatesToken ?? randomBytes(32).toString("hex"),
|
|
74
|
+
// Kept across starts: a new token would recreate the runtime and harness containers.
|
|
75
|
+
harnessToken: persisted.harnessToken ?? randomBytes(32).toString("hex"),
|
|
76
|
+
harness: persisted.harness ?? "remote",
|
|
77
|
+
restateUi: input.restateUi ?? persisted.restateUi ?? false,
|
|
78
|
+
// Kept across starts: a new key would recreate the runtime, gateway and rustfs containers.
|
|
79
|
+
objectStoreSecretKey: persisted.objectStoreSecretKey ?? randomBytes(32).toString("hex"),
|
|
72
80
|
restateIdentityKey: identity.publicKey,
|
|
73
81
|
uid: input.uid,
|
|
74
82
|
gid: input.gid,
|
|
@@ -77,12 +85,17 @@ export async function prepareStack(input) {
|
|
|
77
85
|
studioImage: input.images.studio,
|
|
78
86
|
studioFrameAncestors,
|
|
79
87
|
studioAnalyticsId: input.studioAnalyticsId ?? "",
|
|
80
|
-
studioPublicOrigins: input.studioPublicOrigin ?? "",
|
|
81
88
|
tenantName: input.name,
|
|
82
89
|
derivedPrincipals: (input.derivedPrincipals?.trim()
|
|
83
90
|
? parseDerivedPrincipals(input.derivedPrincipals, "NYLORUN_DERIVED_PRINCIPALS")
|
|
84
91
|
: (persisted.derivedPrincipals ?? ["project"])).join(","),
|
|
85
92
|
};
|
|
93
|
+
const sandboxes = input.sandboxes === null
|
|
94
|
+
? undefined
|
|
95
|
+
: (input.sandboxes ?? (sandboxesEnabled(paths.root) ? persisted.sandboxes : undefined));
|
|
96
|
+
// The sandboxes image follows the Runtime's, like the runtime image, on every start.
|
|
97
|
+
if (sandboxes)
|
|
98
|
+
env.sandboxes = { ...sandboxes, image: input.images.sandboxes };
|
|
86
99
|
const { adminKey } = await ensureHostCredentials(paths);
|
|
87
100
|
await ensureVaultKey(paths);
|
|
88
101
|
const host = await writeStackHostConfig(paths, {
|
|
@@ -91,6 +104,10 @@ export async function prepareStack(input) {
|
|
|
91
104
|
runtimeVersion: input.runtimeVersion,
|
|
92
105
|
});
|
|
93
106
|
await writeFileMode(paths.env, renderEnvFile(env), 0o600);
|
|
94
|
-
await writeFileMode(paths.compose, renderComposeFile(input.project, input.name
|
|
107
|
+
await writeFileMode(paths.compose, renderComposeFile(input.project, input.name, {
|
|
108
|
+
harness: env.harness,
|
|
109
|
+
...(env.sandboxes ? { sandboxes: true } : {}),
|
|
110
|
+
...(env.restateUi ? { restateUi: true } : {}),
|
|
111
|
+
}), 0o644);
|
|
95
112
|
return { env, host, adminKey, firstRun };
|
|
96
113
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type StackDeps } from "./commands.js";
|
|
2
|
+
export declare const sandboxUsage = " sandbox ls [--tenant <name>] [--label <key=value>]... [--json]\n the Tenant's sandboxes: id, kind, state, attached sessions and labels\n sandbox rm <id> [--tenant <name>] delete a sandbox and its files (refused while a turn runs in it)\n sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--no-pull]\n run the Tenant's sandboxes as pods on that kubeconfig context (agent-sandbox v1.0.5)\n sandbox disable [--tenant <name>] [--delete-namespace]\n stop the sandboxes service and forget the cluster\n sandbox status [--tenant <name>] [--json]\n the cluster, the sandboxes service's readiness and what it reports";
|
|
3
|
+
/** `nylorun sandbox <ls|rm|enable|disable|status> ...` */
|
|
4
|
+
export declare function sandboxCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nylorun sandbox ls | rm`: the running local Tenant's sandbox resources (Host feature
|
|
3
|
+
* `sandboxes`), through its Tenant API as the Project's derived principal. Neither starts the
|
|
4
|
+
* Tenant. `nylorun sandbox enable|disable|status` (sandbox pods on a cluster, F7.2) live in
|
|
5
|
+
* `../sandbox/commands.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
|
|
8
|
+
import { CliError } from "../errors.js";
|
|
9
|
+
import { runSandboxCommand, sandboxUsage as clusterUsage } from "../sandbox/commands.js";
|
|
10
|
+
import { spawnKubectl } from "../sandbox/kubectl.js";
|
|
11
|
+
import { parseStackFlags, runningTenantApi } from "./commands.js";
|
|
12
|
+
export const sandboxUsage = ` sandbox ls [--tenant <name>] [--label <key=value>]... [--json]
|
|
13
|
+
the Tenant's sandboxes: id, kind, state, attached sessions and labels
|
|
14
|
+
sandbox rm <id> [--tenant <name>] delete a sandbox and its files (refused while a turn runs in it)
|
|
15
|
+
${clusterUsage}`;
|
|
16
|
+
const usageError = (message) => new CliError(message, 2);
|
|
17
|
+
async function call(deps, api, method, path) {
|
|
18
|
+
const response = await deps.fetch(`${api.runtimeUrl}${path}`, {
|
|
19
|
+
method,
|
|
20
|
+
headers: {
|
|
21
|
+
authorization: `Bearer ${api.applicationKey}`,
|
|
22
|
+
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
23
|
+
accept: "application/json",
|
|
24
|
+
},
|
|
25
|
+
redirect: "error",
|
|
26
|
+
signal: AbortSignal.timeout(15_000),
|
|
27
|
+
});
|
|
28
|
+
const body = (await response.json().catch(() => ({})));
|
|
29
|
+
if (response.status === 404 && path === "/v1/sandboxes")
|
|
30
|
+
throw new CliError(`Tenant ${api.name} runs a Runtime without sandboxes. Update it: npx nylorun@latest start.`, 1);
|
|
31
|
+
if (!response.ok && response.status !== 404)
|
|
32
|
+
throw new CliError(`${method} ${path} returned ${response.status}${typeof body.message === "string" ? `: ${body.message}` : ""}`, 1);
|
|
33
|
+
return { status: response.status, body };
|
|
34
|
+
}
|
|
35
|
+
function labelsText(labels) {
|
|
36
|
+
const entries = Object.entries(labels);
|
|
37
|
+
return entries.length === 0 ? "-" : entries.map(([key, value]) => `${key}=${value}`).join(",");
|
|
38
|
+
}
|
|
39
|
+
async function ls(deps, args) {
|
|
40
|
+
const usage = "nylorun sandbox ls [--tenant <name>] [--label <key=value>]... [--json]";
|
|
41
|
+
const flags = parseStackFlags(args, { booleans: ["--json"], values: ["--tenant"], lists: ["--label"] }, usage);
|
|
42
|
+
if (flags.rest.length)
|
|
43
|
+
throw usageError(`Usage: ${usage}`);
|
|
44
|
+
const query = new URLSearchParams();
|
|
45
|
+
for (const label of flags.lists.get("--label") ?? []) {
|
|
46
|
+
if (label.indexOf("=") <= 0)
|
|
47
|
+
throw usageError(`--label must be key=value, not ${label}`);
|
|
48
|
+
query.append("label", label);
|
|
49
|
+
}
|
|
50
|
+
const name = flags.values.get("--tenant");
|
|
51
|
+
const api = await runningTenantApi(deps, name === undefined ? {} : { name });
|
|
52
|
+
const search = query.toString();
|
|
53
|
+
const { body } = await call(deps, api, "GET", `/v1/sandboxes${search ? `?${search}` : ""}`);
|
|
54
|
+
const sandboxes = (body.sandboxes ?? []);
|
|
55
|
+
if (flags.booleans.has("--json")) {
|
|
56
|
+
deps.out(JSON.stringify(sandboxes, null, 2));
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
if (sandboxes.length === 0) {
|
|
60
|
+
deps.out(`No sandboxes on Tenant ${api.name}.`);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
const rows = [
|
|
64
|
+
["ID", "KIND", "STATE", "SESSIONS", "LABELS"],
|
|
65
|
+
...sandboxes.map((sandbox) => [
|
|
66
|
+
sandbox.id,
|
|
67
|
+
sandbox.kind,
|
|
68
|
+
sandbox.state,
|
|
69
|
+
String(sandbox.sessions.length),
|
|
70
|
+
labelsText(sandbox.labels),
|
|
71
|
+
]),
|
|
72
|
+
];
|
|
73
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
|
|
74
|
+
for (const row of rows)
|
|
75
|
+
deps.out(row
|
|
76
|
+
.map((cell, column) => (column === row.length - 1 ? cell : cell.padEnd(widths[column])))
|
|
77
|
+
.join(" "));
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
async function rm(deps, args) {
|
|
81
|
+
const usage = "nylorun sandbox rm <id> [--tenant <name>]";
|
|
82
|
+
const flags = parseStackFlags(args, { values: ["--tenant"] }, usage);
|
|
83
|
+
if (flags.rest.length !== 1)
|
|
84
|
+
throw usageError(`Usage: ${usage}`);
|
|
85
|
+
const id = flags.rest[0];
|
|
86
|
+
const name = flags.values.get("--tenant");
|
|
87
|
+
const api = await runningTenantApi(deps, name === undefined ? {} : { name });
|
|
88
|
+
const { status, body } = await call(deps, api, "DELETE", `/v1/sandboxes/${encodeURIComponent(id)}`);
|
|
89
|
+
if (status === 404 || body.deleted !== true) {
|
|
90
|
+
deps.err(`No sandbox ${id} on Tenant ${api.name}.`);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
deps.out(`Deleted sandbox ${id} and its files.`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
/** `nylorun sandbox <ls|rm|enable|disable|status> ...` */
|
|
97
|
+
export async function sandboxCommand(deps, args) {
|
|
98
|
+
const [sub, ...rest] = args;
|
|
99
|
+
if (sub === "ls" || sub === "list")
|
|
100
|
+
return ls(deps, rest);
|
|
101
|
+
if (sub === "rm" || sub === "delete")
|
|
102
|
+
return rm(deps, rest);
|
|
103
|
+
if (sub === "enable" || sub === "disable" || sub === "status")
|
|
104
|
+
return runSandboxCommand(args, { stack: deps, kubectl: spawnKubectl(deps.env) });
|
|
105
|
+
throw usageError(`Usage:\n${sandboxUsage}`);
|
|
106
|
+
}
|
package/dist/stack/stacks.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DockerRunner } from "./docker.js";
|
|
1
2
|
/**
|
|
2
3
|
* The machine's local Tenants (tenancy.md §6): one installation per Tenant, each with its own
|
|
3
4
|
* Host root `~/.nylorun/tenants/<name>/`, Compose project `nylorun-<name>`, ports and volumes.
|
|
@@ -48,3 +49,10 @@ export declare function chooseTenantName(base: string, projectDir: string): Prom
|
|
|
48
49
|
* volumes with new keys.
|
|
49
50
|
*/
|
|
50
51
|
export declare function moveStackRoots(base: string, err: (line: string) => void): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Remove the Studio proxy of nylorun 0.6 (a Caddy container `nylorun-proxy` on its own network,
|
|
54
|
+
* files in `~/.nylorun/proxy/`). Removing the container also detaches it from the Tenants'
|
|
55
|
+
* networks, so their `docker compose down` works again. The directory goes last, once Docker
|
|
56
|
+
* removed both (or never had them), so a Docker that does not answer is retried next time.
|
|
57
|
+
*/
|
|
58
|
+
export declare function removeOldProxy(docker: DockerRunner, base: string, err: (line: string) => void): Promise<void>;
|
package/dist/stack/stacks.js
CHANGED
|
@@ -148,3 +148,22 @@ export async function moveStackRoots(base, err) {
|
|
|
148
148
|
err(`Moved Tenants ${moved.join(", ")} from ${tilde(stacks)} to ${tilde(tenantsDir(base))}.`);
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Remove the Studio proxy of nylorun 0.6 (a Caddy container `nylorun-proxy` on its own network,
|
|
153
|
+
* files in `~/.nylorun/proxy/`). Removing the container also detaches it from the Tenants'
|
|
154
|
+
* networks, so their `docker compose down` works again. The directory goes last, once Docker
|
|
155
|
+
* removed both (or never had them), so a Docker that does not answer is retried next time.
|
|
156
|
+
*/
|
|
157
|
+
export async function removeOldProxy(docker, base, err) {
|
|
158
|
+
const dir = join(base, "proxy");
|
|
159
|
+
if (!existsSync(dir))
|
|
160
|
+
return;
|
|
161
|
+
const container = await docker.run(["rm", "--force", "nylorun-proxy"]);
|
|
162
|
+
if (container.code !== 0 && !/no such container/i.test(container.stderr))
|
|
163
|
+
return;
|
|
164
|
+
const network = await docker.run(["network", "rm", "nylorun-proxy"]);
|
|
165
|
+
if (network.code !== 0 && !/not found|no such network/i.test(network.stderr))
|
|
166
|
+
return;
|
|
167
|
+
await rm(dir, { recursive: true, force: true });
|
|
168
|
+
err("Removed the Studio proxy of nylorun 0.6 (nylorun-proxy); each Studio is on its own localhost port again (\"nylorun studio\" opens it).");
|
|
169
|
+
}
|
|
@@ -3,8 +3,8 @@ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>
|
|
|
3
3
|
export declare function studioOrigin(port: number): string;
|
|
4
4
|
/**
|
|
5
5
|
* Ask the Studio container for a single-use login token with the admin key
|
|
6
|
-
* (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port
|
|
7
|
-
*
|
|
6
|
+
* (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port)
|
|
7
|
+
* and return the URL to open there.
|
|
8
8
|
* Accepts `{ loginUrl }`, `{ url }` or `{ token }` in the response.
|
|
9
9
|
*/
|
|
10
10
|
export declare function mintStudioLogin(input: {
|
|
@@ -5,8 +5,8 @@ export function studioOrigin(port) {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Ask the Studio container for a single-use login token with the admin key
|
|
8
|
-
* (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port
|
|
9
|
-
*
|
|
8
|
+
* (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port)
|
|
9
|
+
* and return the URL to open there.
|
|
10
10
|
* Accepts `{ loginUrl }`, `{ url }` or `{ token }` in the response.
|
|
11
11
|
*/
|
|
12
12
|
export async function mintStudioLogin(input) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nylorun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-beta",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Set up and run local Nylorun Tenants (Runtime and Studio) with Docker Compose",
|
|
@@ -30,18 +30,18 @@
|
|
|
30
30
|
"prepack": "npm run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@nylorun/core": "0.
|
|
33
|
+
"@nylorun/core": "0.12.0-beta"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
37
37
|
"@types/node": "^26.5.0",
|
|
38
|
-
"vitest": "^5.0.
|
|
38
|
+
"vitest": "^5.0.2"
|
|
39
39
|
},
|
|
40
40
|
"bin": {
|
|
41
41
|
"nylorun": "dist/cli.js"
|
|
42
42
|
},
|
|
43
43
|
"nylorun": {
|
|
44
|
-
"runtime": "0.
|
|
45
|
-
"studio": "0.
|
|
44
|
+
"runtime": "0.16.0-beta",
|
|
45
|
+
"studio": "0.19.0-beta"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/dist/stack/proxy.d.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type { DockerRunner } from "./docker.js";
|
|
2
|
-
import { type PortProbe } from "./ports.js";
|
|
3
|
-
/**
|
|
4
|
-
* The Studio proxy: one small Caddy container per machine that gives each local Tenant's
|
|
5
|
-
* Studio the address `http://<name>.localhost:<port>`. Browsers keep cookies per host, not
|
|
6
|
-
* per port, so separate hosts keep the Studios' sessions apart. It routes browsers to Studio
|
|
7
|
-
* only (programs keep the Runtime's `http://localhost:<port>`: Node's resolver need not
|
|
8
|
-
* resolve `*.localhost`) and holds no Tenant data: its files are `~/.nylorun/proxy/`
|
|
9
|
-
* (`compose.yaml`, `Caddyfile`, `.env`), and Caddy's own state is tmpfs.
|
|
10
|
-
*
|
|
11
|
-
* The proxy joins each Tenant's network (named after its Compose project) and reaches Studio
|
|
12
|
-
* by container name, `<project>-studio:3000`. Docker cannot remove a network the proxy is
|
|
13
|
-
* attached to, so `reset` and `delete` disconnect it first.
|
|
14
|
-
*/
|
|
15
|
-
export declare const PROXY_PROJECT = "nylorun-proxy";
|
|
16
|
-
export declare const PROXY_CONTAINER = "nylorun-proxy";
|
|
17
|
-
export interface ProxyPaths {
|
|
18
|
-
dir: string;
|
|
19
|
-
compose: string;
|
|
20
|
-
caddyfile: string;
|
|
21
|
-
/** The port, and whether Docker took the `::1` binding. */
|
|
22
|
-
env: string;
|
|
23
|
-
}
|
|
24
|
-
export declare function proxyPaths(base: string): ProxyPaths;
|
|
25
|
-
/** `NYLORUN_PROXY_DISABLED=1`: no proxy; Studio is `http://localhost:<port>`. */
|
|
26
|
-
export declare function proxyDisabled(env: Readonly<Record<string, string | undefined>>): boolean;
|
|
27
|
-
export declare function proxyOrigin(name: string, port: number): string;
|
|
28
|
-
export interface ProxySettings {
|
|
29
|
-
port: number;
|
|
30
|
-
/** Also published on `[::1]` (false once Docker refused it). */
|
|
31
|
-
ipv6: boolean;
|
|
32
|
-
}
|
|
33
|
-
export declare function readProxySettings(base: string): Promise<ProxySettings | undefined>;
|
|
34
|
-
/**
|
|
35
|
-
* The proxy's settings. On first use its port is 4160 when free, else a free one outside
|
|
36
|
-
* `taken`; it is kept after that.
|
|
37
|
-
*/
|
|
38
|
-
export declare function ensureProxySettings(base: string, ports: PortProbe, taken: ReadonlySet<number>): Promise<ProxySettings>;
|
|
39
|
-
/** A Tenant the proxy routes to: its name and Compose project (also its network's name). */
|
|
40
|
-
export interface ProxyRoute {
|
|
41
|
-
name: string;
|
|
42
|
-
project: string;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* One site per Tenant. Site addresses carry no port: Caddy listens on 80 in the container,
|
|
46
|
-
* and its host match ignores the published port in the browser's Host. The browser's Host
|
|
47
|
-
* (`<name>.localhost:<port>`) reaches Studio unchanged, which Studio checks; a Tenant that is
|
|
48
|
-
* not running answers 502, and an unknown host 404.
|
|
49
|
-
*/
|
|
50
|
-
export declare function renderCaddyfile(routes: readonly ProxyRoute[]): string;
|
|
51
|
-
export declare function renderProxyCompose(ipv6: boolean): string;
|
|
52
|
-
/** Detach the proxy from a Tenant's network, so Compose can remove it; never fails. */
|
|
53
|
-
export declare function disconnectProxy(docker: DockerRunner, network: string): Promise<void>;
|
|
54
|
-
/**
|
|
55
|
-
* Write the routes, bring the proxy up (Compose recreates it when its file changed), attach
|
|
56
|
-
* it to every routed Tenant's network and reload Caddy. Never throws: a problem is warned
|
|
57
|
-
* once and the caller shows Studio's own port. When Docker refuses `[::1]` (IPv6 off), the
|
|
58
|
-
* proxy listens on 127.0.0.1 only, and says so once.
|
|
59
|
-
*/
|
|
60
|
-
export declare function startProxy(input: {
|
|
61
|
-
docker: DockerRunner;
|
|
62
|
-
base: string;
|
|
63
|
-
settings: ProxySettings;
|
|
64
|
-
routes: readonly ProxyRoute[];
|
|
65
|
-
err(line: string): void;
|
|
66
|
-
pollMs?: number;
|
|
67
|
-
}): Promise<boolean>;
|
|
68
|
-
/** Rewrite the routes of an existing proxy and reload it (`delete`); never fails. */
|
|
69
|
-
export declare function refreshProxyRoutes(docker: DockerRunner, base: string, routes: readonly ProxyRoute[]): Promise<void>;
|
|
70
|
-
/** Stop the proxy (`stop --all`); true when it stopped. */
|
|
71
|
-
export declare function stopProxy(docker: DockerRunner): Promise<boolean>;
|
|
72
|
-
export interface ProxyStatus {
|
|
73
|
-
state: "running" | "stopped" | "absent" | "disabled";
|
|
74
|
-
port?: number;
|
|
75
|
-
ipv6?: boolean;
|
|
76
|
-
}
|
|
77
|
-
/** The proxy as `nylorun doctor` reports it. */
|
|
78
|
-
export declare function readProxyStatus(docker: DockerRunner, base: string, env: Readonly<Record<string, string | undefined>>): Promise<ProxyStatus>;
|