@nylorun/cli 0.2.0-beta → 0.3.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +87 -40
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +132 -220
- package/dist/dev-entry.d.ts +5 -1
- package/dist/dev-entry.js +7 -6
- package/dist/dev.d.ts +45 -4
- package/dist/dev.js +245 -32
- package/dist/doctor.d.ts +25 -3
- package/dist/doctor.js +104 -16
- package/dist/environment.d.ts +4 -1
- package/dist/environment.js +12 -3
- package/dist/launcher.d.ts +5 -5
- package/dist/launcher.js +6 -3
- 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 +36 -15
- package/dist/project/attach.d.ts +32 -0
- package/dist/project/attach.js +241 -0
- package/dist/project/create-tenant.d.ts +26 -0
- package/dist/project/create-tenant.js +65 -0
- package/dist/project/credentials.d.ts +18 -0
- package/dist/project/credentials.js +64 -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 +51 -0
- package/dist/project/seed.d.ts +19 -0
- package/dist/project/seed.js +94 -0
- package/dist/runtime/baseline.d.ts +2 -0
- package/dist/runtime/baseline.js +11 -0
- package/dist/runtime/commands.d.ts +16 -0
- package/dist/runtime/commands.js +292 -0
- package/dist/runtime/ephemeral.d.ts +37 -0
- package/dist/runtime/ephemeral.js +84 -0
- package/dist/runtime/exit-codes.d.ts +9 -0
- package/dist/runtime/exit-codes.js +43 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.js +6 -0
- package/dist/runtime/launcher.d.ts +81 -0
- package/dist/runtime/launcher.js +243 -0
- package/dist/runtime/progress.d.ts +10 -0
- package/dist/runtime/progress.js +8 -0
- package/dist/runtime/version.d.ts +6 -0
- package/dist/runtime/version.js +27 -0
- package/dist/tenant/commands.d.ts +4 -0
- package/dist/tenant/commands.js +262 -0
- package/package.json +6 -3
- 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,64 @@
|
|
|
1
|
+
import { chmod, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { CliError } from "../errors.js";
|
|
4
|
+
import { credentialsPath, ensureProjectNylorunDir, } from "./link.js";
|
|
5
|
+
export async function readCredentials(projectRoot) {
|
|
6
|
+
const path = credentialsPath(projectRoot);
|
|
7
|
+
try {
|
|
8
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
9
|
+
if (typeof value?.applicationKey !== "string" ||
|
|
10
|
+
!/^[0-9a-f]{64}$/.test(value.applicationKey) ||
|
|
11
|
+
typeof value?.principalId !== "string" ||
|
|
12
|
+
value.principalId.length === 0) {
|
|
13
|
+
throw new CliError(`Invalid Project credentials at ${path}. Remove .nylorun/credentials.json and run nylorun dev.`, 1);
|
|
14
|
+
}
|
|
15
|
+
const format = value.format === undefined || value.format === 0
|
|
16
|
+
? 0
|
|
17
|
+
: value.format === 1
|
|
18
|
+
? 1
|
|
19
|
+
: undefined;
|
|
20
|
+
if (format === undefined) {
|
|
21
|
+
throw new CliError(`Project credentials at ${path} have an unsupported format. Upgrade the CLI.`, 1);
|
|
22
|
+
}
|
|
23
|
+
await chmod(path, 0o600);
|
|
24
|
+
const executors = format === 0 &&
|
|
25
|
+
value.executors &&
|
|
26
|
+
typeof value.executors === "object" &&
|
|
27
|
+
!Array.isArray(value.executors)
|
|
28
|
+
? value.executors
|
|
29
|
+
: undefined;
|
|
30
|
+
return {
|
|
31
|
+
format,
|
|
32
|
+
applicationKey: value.applicationKey,
|
|
33
|
+
principalId: value.principalId,
|
|
34
|
+
...(executors ? { executors } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code === "ENOENT")
|
|
39
|
+
return undefined;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function writeCredentials(projectRoot, credentials) {
|
|
44
|
+
await ensureProjectNylorunDir(projectRoot);
|
|
45
|
+
const path = credentialsPath(projectRoot);
|
|
46
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
47
|
+
// Version 1 writes format 1 without executors (D§3.7).
|
|
48
|
+
const body = `${JSON.stringify({
|
|
49
|
+
format: credentials.format ?? 1,
|
|
50
|
+
applicationKey: credentials.applicationKey,
|
|
51
|
+
principalId: credentials.principalId,
|
|
52
|
+
}, null, 2)}\n`;
|
|
53
|
+
try {
|
|
54
|
+
await writeFile(temporary, body, { mode: 0o600 });
|
|
55
|
+
await rename(temporary, path);
|
|
56
|
+
await chmod(path, 0o600);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await rm(temporary, { force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function removeCredentials(projectRoot) {
|
|
63
|
+
await rm(credentialsPath(projectRoot), { force: true });
|
|
64
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Project-local link to a Runtime Host Tenant (D§3.7, format 0|1). */
|
|
2
|
+
export interface ProjectLink {
|
|
3
|
+
format: 0 | 1;
|
|
4
|
+
hostUrl: string;
|
|
5
|
+
hostId: string;
|
|
6
|
+
tenantId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function nylorunDir(projectRoot: string): string;
|
|
9
|
+
export declare function linkPath(projectRoot: string): string;
|
|
10
|
+
export declare function credentialsPath(projectRoot: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Create `.nylorun/` mode 0700 with a private `.gitignore` containing `*`.
|
|
13
|
+
* The Project's own `.gitignore` is never edited.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ensureProjectNylorunDir(projectRoot: string): Promise<string>;
|
|
16
|
+
export declare function readLink(projectRoot: string): Promise<ProjectLink | undefined>;
|
|
17
|
+
export declare function writeLink(projectRoot: string, link: Omit<ProjectLink, "format"> & {
|
|
18
|
+
format?: 0 | 1;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
export declare function removeLink(projectRoot: string): Promise<void>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { isTenantId } from "@nylorun/agents";
|
|
5
|
+
import { CliError } from "../errors.js";
|
|
6
|
+
export function nylorunDir(projectRoot) {
|
|
7
|
+
return join(projectRoot, ".nylorun");
|
|
8
|
+
}
|
|
9
|
+
export function linkPath(projectRoot) {
|
|
10
|
+
return join(nylorunDir(projectRoot), "link.json");
|
|
11
|
+
}
|
|
12
|
+
export function credentialsPath(projectRoot) {
|
|
13
|
+
return join(nylorunDir(projectRoot), "credentials.json");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create `.nylorun/` mode 0700 with a private `.gitignore` containing `*`.
|
|
17
|
+
* The Project's own `.gitignore` is never edited.
|
|
18
|
+
*/
|
|
19
|
+
export async function ensureProjectNylorunDir(projectRoot) {
|
|
20
|
+
const dir = nylorunDir(projectRoot);
|
|
21
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
22
|
+
await writeFile(join(dir, ".gitignore"), "*\n", { mode: 0o600 }).catch(async () => {
|
|
23
|
+
await writeFile(join(dir, ".gitignore"), "*\n", { mode: 0o600 });
|
|
24
|
+
});
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
export async function readLink(projectRoot) {
|
|
28
|
+
try {
|
|
29
|
+
const value = JSON.parse(await readFile(linkPath(projectRoot), "utf8"));
|
|
30
|
+
if (typeof value?.hostUrl !== "string" ||
|
|
31
|
+
typeof value?.hostId !== "string" ||
|
|
32
|
+
typeof value?.tenantId !== "string" ||
|
|
33
|
+
!isTenantId(value.tenantId)) {
|
|
34
|
+
throw new CliError(`Invalid Project link at ${linkPath(projectRoot)}. Remove .nylorun/link.json and run nylorun dev.`, 1);
|
|
35
|
+
}
|
|
36
|
+
const format = value.format === undefined || value.format === 0
|
|
37
|
+
? 0
|
|
38
|
+
: value.format === 1
|
|
39
|
+
? 1
|
|
40
|
+
: undefined;
|
|
41
|
+
if (format === undefined) {
|
|
42
|
+
throw new CliError(`Project link at ${linkPath(projectRoot)} has an unsupported format. Upgrade the CLI.`, 1);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
format,
|
|
46
|
+
hostUrl: value.hostUrl.replace(/\/$/, ""),
|
|
47
|
+
hostId: value.hostId,
|
|
48
|
+
tenantId: value.tenantId,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code === "ENOENT")
|
|
53
|
+
return undefined;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function writeLink(projectRoot, link) {
|
|
58
|
+
await ensureProjectNylorunDir(projectRoot);
|
|
59
|
+
const path = linkPath(projectRoot);
|
|
60
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
61
|
+
const body = `${JSON.stringify({
|
|
62
|
+
format: link.format ?? 1,
|
|
63
|
+
hostUrl: link.hostUrl.replace(/\/$/, ""),
|
|
64
|
+
hostId: link.hostId,
|
|
65
|
+
tenantId: link.tenantId,
|
|
66
|
+
}, null, 2)}\n`;
|
|
67
|
+
try {
|
|
68
|
+
await writeFile(temporary, body, { mode: 0o600 });
|
|
69
|
+
await rename(temporary, path);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await rm(temporary, { force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export async function removeLink(projectRoot) {
|
|
76
|
+
await rm(linkPath(projectRoot), { force: true });
|
|
77
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The nearest `.nylorun` wins so an initialised Project keeps working from a
|
|
3
|
+
* subdirectory; otherwise the nearest package.json defines the Project. The
|
|
4
|
+
* walk stops at the home directory because `~/.nylorun` is the Host root, never
|
|
5
|
+
* a Project root.
|
|
6
|
+
*/
|
|
7
|
+
export declare function findProjectRoot(cwd?: string): string | undefined;
|
|
8
|
+
/** Require a Project root or throw a usage-oriented error. */
|
|
9
|
+
export declare function requireProjectRoot(cwd?: string): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* The nearest `.nylorun` wins so an initialised Project keeps working from a
|
|
6
|
+
* subdirectory; otherwise the nearest package.json defines the Project. The
|
|
7
|
+
* walk stops at the home directory because `~/.nylorun` is the Host root, never
|
|
8
|
+
* a Project root.
|
|
9
|
+
*/
|
|
10
|
+
export function findProjectRoot(cwd = process.cwd()) {
|
|
11
|
+
const stop = (() => {
|
|
12
|
+
try {
|
|
13
|
+
return realpathSync(homedir());
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return homedir();
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
let directory = resolve(cwd);
|
|
20
|
+
try {
|
|
21
|
+
directory = realpathSync(directory);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* unresolvable cwd falls through */
|
|
25
|
+
}
|
|
26
|
+
const root = parse(directory).root;
|
|
27
|
+
let fallback;
|
|
28
|
+
for (;;) {
|
|
29
|
+
if (directory === stop)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (existsSync(join(directory, ".nylorun")))
|
|
32
|
+
return directory;
|
|
33
|
+
if (!fallback && existsSync(join(directory, "package.json")))
|
|
34
|
+
fallback = directory;
|
|
35
|
+
if (directory === root)
|
|
36
|
+
break;
|
|
37
|
+
const parent = dirname(directory);
|
|
38
|
+
if (parent === directory)
|
|
39
|
+
break;
|
|
40
|
+
directory = parent;
|
|
41
|
+
}
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
/** Require a Project root or throw a usage-oriented error. */
|
|
45
|
+
export function requireProjectRoot(cwd = process.cwd()) {
|
|
46
|
+
const root = findProjectRoot(cwd);
|
|
47
|
+
if (!root) {
|
|
48
|
+
throw new Error("No Project found (looked for package.json or .nylorun/). Run this from a Project directory.");
|
|
49
|
+
}
|
|
50
|
+
return root;
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SeedOptions {
|
|
2
|
+
hostUrl: string;
|
|
3
|
+
tenantId: string;
|
|
4
|
+
applicationKey: string;
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
/** Pre-loaded env map; when omitted, read `.env` without mutating process.env. */
|
|
7
|
+
env?: Readonly<Record<string, string>>;
|
|
8
|
+
fetchImpl?: typeof fetch;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Seed non-secret Tenant config via `PUT /v1/tenant/config/seed`.
|
|
13
|
+
* Model secrets go through the model route into the Tenant vault (F3).
|
|
14
|
+
* `.env` is read as a value map only — never written back or applied to process.env.
|
|
15
|
+
*/
|
|
16
|
+
export declare function seedTenantFromProject(options: SeedOptions): Promise<{
|
|
17
|
+
applied: string[];
|
|
18
|
+
kept: string[];
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { PROTOCOL_HEADER, PROTOCOL_VERSION, TENANT_HEADER, } from "@nylorun/agents";
|
|
5
|
+
import { CliError } from "../errors.js";
|
|
6
|
+
import { putHostModel } from "../model/host-model.js";
|
|
7
|
+
import { loadProjectEnvironment } from "../environment.js";
|
|
8
|
+
function tenantHeaders(applicationKey, tenantId) {
|
|
9
|
+
return {
|
|
10
|
+
authorization: `Bearer ${applicationKey}`,
|
|
11
|
+
[TENANT_HEADER]: tenantId,
|
|
12
|
+
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Seed non-secret Tenant config via `PUT /v1/tenant/config/seed`.
|
|
17
|
+
* Model secrets go through the model route into the Tenant vault (F3).
|
|
18
|
+
* `.env` is read as a value map only — never written back or applied to process.env.
|
|
19
|
+
*/
|
|
20
|
+
export async function seedTenantFromProject(options) {
|
|
21
|
+
const env = options.env ?? loadProjectEnvironment(options.projectRoot);
|
|
22
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
23
|
+
const sandboxRaw = env.NYLORUN_SANDBOX?.trim();
|
|
24
|
+
const sandbox = sandboxRaw === "auto" ||
|
|
25
|
+
sandboxRaw === "microsandbox" ||
|
|
26
|
+
sandboxRaw === "virtual"
|
|
27
|
+
? { backend: sandboxRaw }
|
|
28
|
+
: undefined;
|
|
29
|
+
let applied = [];
|
|
30
|
+
let kept = [];
|
|
31
|
+
if (sandbox) {
|
|
32
|
+
const response = await fetchImpl(`${options.hostUrl.replace(/\/$/, "")}/v1/tenant/config/seed`, {
|
|
33
|
+
method: "PUT",
|
|
34
|
+
headers: {
|
|
35
|
+
...tenantHeaders(options.applicationKey, options.tenantId),
|
|
36
|
+
"content-type": "application/json",
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
requestId: randomUUID(),
|
|
40
|
+
sandbox,
|
|
41
|
+
}),
|
|
42
|
+
signal: options.signal ?? AbortSignal.timeout(15_000),
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new CliError(`Tenant config seed failed (${response.status}): ${await response
|
|
46
|
+
.text()
|
|
47
|
+
.catch(() => "")}`, 1);
|
|
48
|
+
}
|
|
49
|
+
const body = (await response.json());
|
|
50
|
+
applied = body.applied ?? [];
|
|
51
|
+
kept = body.kept ?? [];
|
|
52
|
+
}
|
|
53
|
+
if (env.NYLORUN_DEV_MODEL?.trim() !== "fixture") {
|
|
54
|
+
const seeded = modelFromEnv(env, options.projectRoot);
|
|
55
|
+
if (seeded) {
|
|
56
|
+
await putHostModel(options.hostUrl, options.applicationKey, seeded, options.tenantId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { applied, kept };
|
|
60
|
+
}
|
|
61
|
+
function modelFromEnv(env, projectRoot) {
|
|
62
|
+
const provider = env.MODEL_PROVIDER?.trim();
|
|
63
|
+
const model = env.MODEL?.trim();
|
|
64
|
+
if (!provider || !model)
|
|
65
|
+
return undefined;
|
|
66
|
+
const baseUrl = env.MODEL_PROVIDER_BASE_URL?.trim();
|
|
67
|
+
const key = env.MODEL_PROVIDER_API_KEY;
|
|
68
|
+
if (key) {
|
|
69
|
+
return {
|
|
70
|
+
provider,
|
|
71
|
+
model,
|
|
72
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
73
|
+
auth: { type: "api_key", key },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
for (const relative of [".nylorun/auth.json", ".env/auth.json"]) {
|
|
77
|
+
try {
|
|
78
|
+
const all = JSON.parse(readFileSync(join(projectRoot, relative), "utf8"));
|
|
79
|
+
const credential = all[provider];
|
|
80
|
+
if (credential?.type === "api_key" || credential?.type === "oauth") {
|
|
81
|
+
return {
|
|
82
|
+
provider,
|
|
83
|
+
model,
|
|
84
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
85
|
+
auth: credential,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* absent */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* // CLIENTS-CCR: owned by WS-F1 (moves from cli/src/host/baseline.ts).
|
|
3
|
+
* Snapshot of the process environment at CLI module load.
|
|
4
|
+
*/
|
|
5
|
+
const snapshot = Object.freeze({
|
|
6
|
+
...process.env,
|
|
7
|
+
});
|
|
8
|
+
/** Frozen map captured when this module first loads. */
|
|
9
|
+
export function baselineEnv() {
|
|
10
|
+
return snapshot;
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type LauncherHandle, type LauncherOptions } from "./launcher.js";
|
|
2
|
+
export declare const runtimeUsage = " runtime up [--port <n>]\n runtime down [--wait] [--force] [--timeout <seconds>]\n runtime status [--json] [--env]\n runtime logs [-f|--follow] [-n <lines>] [--tenant <id>|--all]\n runtime restart [--port <n>] [--allow-downgrade] [--wait|--force]\n runtime run [--port <n>]\n up alias for \"runtime up\"\n down alias for \"runtime down\"\n logs alias for \"runtime logs\"";
|
|
3
|
+
export interface RuntimeCommandOptions extends LauncherOptions {
|
|
4
|
+
home?: string;
|
|
5
|
+
/** Injected launcher handle (tests). */
|
|
6
|
+
handle?: LauncherHandle;
|
|
7
|
+
/** Hook for `status --env` (F2 wires Project link exports). */
|
|
8
|
+
envHook?: () => void | Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `nylorun runtime …` and the `up` / `down` / `logs` aliases.
|
|
12
|
+
* Dispatch from `cli.ts` is owned by WS-F2 (one-line import change).
|
|
13
|
+
*/
|
|
14
|
+
export declare function runtimeCommand(args: readonly string[], options?: RuntimeCommandOptions): Promise<void>;
|
|
15
|
+
export { runtimeVersion } from "./version.js";
|
|
16
|
+
export { launcher } from "./launcher.js";
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { CliError } from "../errors.js";
|
|
3
|
+
import { exitCodeForStatusState, } from "./exit-codes.js";
|
|
4
|
+
import { launcher, resolveHome, throwOnLauncherFailure, } from "./launcher.js";
|
|
5
|
+
import { runtimeVersion } from "./version.js";
|
|
6
|
+
export const runtimeUsage = ` runtime up [--port <n>]
|
|
7
|
+
runtime down [--wait] [--force] [--timeout <seconds>]
|
|
8
|
+
runtime status [--json] [--env]
|
|
9
|
+
runtime logs [-f|--follow] [-n <lines>] [--tenant <id>|--all]
|
|
10
|
+
runtime restart [--port <n>] [--allow-downgrade] [--wait|--force]
|
|
11
|
+
runtime run [--port <n>]
|
|
12
|
+
up alias for "runtime up"
|
|
13
|
+
down alias for "runtime down"
|
|
14
|
+
logs alias for "runtime logs"`;
|
|
15
|
+
const usageError = (message) => new CliError(message, 2);
|
|
16
|
+
function parseRuntimeFlags(args, allowed = {}) {
|
|
17
|
+
const booleans = new Set();
|
|
18
|
+
const values = new Map();
|
|
19
|
+
const rest = [];
|
|
20
|
+
const booleanNames = new Set(allowed.booleans ?? []);
|
|
21
|
+
const valueNames = new Set(allowed.values ?? []);
|
|
22
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
23
|
+
const arg = args[index];
|
|
24
|
+
if (!arg.startsWith("-")) {
|
|
25
|
+
rest.push(arg);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (booleanNames.has(arg)) {
|
|
29
|
+
if (booleans.has(arg))
|
|
30
|
+
throw usageError(`${arg} may only be supplied once.`);
|
|
31
|
+
booleans.add(arg);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (valueNames.has(arg)) {
|
|
35
|
+
if (values.has(arg))
|
|
36
|
+
throw usageError(`${arg} may only be supplied once.`);
|
|
37
|
+
const value = args[++index];
|
|
38
|
+
if (!value || value.startsWith("-"))
|
|
39
|
+
throw usageError(`${arg} requires a value.`);
|
|
40
|
+
values.set(arg, value);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
throw usageError(`Unknown flag ${arg}.\n${runtimeUsage}`);
|
|
44
|
+
}
|
|
45
|
+
return { rest, booleans, values };
|
|
46
|
+
}
|
|
47
|
+
function parsePort(value) {
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
const port = Number(value);
|
|
51
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
52
|
+
throw usageError("--port must be an integer between 0 and 65535.");
|
|
53
|
+
return port;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `nylorun runtime …` and the `up` / `down` / `logs` aliases.
|
|
57
|
+
* Dispatch from `cli.ts` is owned by WS-F2 (one-line import change).
|
|
58
|
+
*/
|
|
59
|
+
export async function runtimeCommand(args, options = {}) {
|
|
60
|
+
const home = options.home ??
|
|
61
|
+
(options.env?.NYLORUN_HOME
|
|
62
|
+
? resolve(options.env.NYLORUN_HOME)
|
|
63
|
+
: resolveHome());
|
|
64
|
+
const handle = options.handle ??
|
|
65
|
+
(await launcher(home, { env: options.env }));
|
|
66
|
+
const [verb, ...rest] = args;
|
|
67
|
+
if (verb === "up" || verb === "start") {
|
|
68
|
+
const flags = parseRuntimeFlags(rest, {
|
|
69
|
+
values: ["--port"],
|
|
70
|
+
});
|
|
71
|
+
if (flags.rest.length)
|
|
72
|
+
throw usageError(runtimeUsage);
|
|
73
|
+
const port = parsePort(flags.values.get("--port"));
|
|
74
|
+
const invokeArgs = ["up"];
|
|
75
|
+
if (port !== undefined)
|
|
76
|
+
invokeArgs.push("--port", String(port));
|
|
77
|
+
const outcome = await handle.invoke(invokeArgs);
|
|
78
|
+
throwOnLauncherFailure(outcome);
|
|
79
|
+
const result = outcome.result ?? {};
|
|
80
|
+
const url = typeof result.url === "string" ? result.url : undefined;
|
|
81
|
+
const started = result.started === true;
|
|
82
|
+
if (url && started) {
|
|
83
|
+
console.log(`Started the Runtime Host at ${url}${result.pid ? ` (pid ${result.pid})` : ""}. It stays running; stop it with "nylorun runtime down".`);
|
|
84
|
+
}
|
|
85
|
+
printUpResult(result);
|
|
86
|
+
console.log(`\n→ npx nylorun dev`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (verb === "down" || verb === "stop") {
|
|
90
|
+
const flags = parseRuntimeFlags(rest, {
|
|
91
|
+
booleans: ["--wait", "--force"],
|
|
92
|
+
values: ["--timeout"],
|
|
93
|
+
});
|
|
94
|
+
if (flags.rest.length)
|
|
95
|
+
throw usageError(runtimeUsage);
|
|
96
|
+
if (flags.booleans.has("--wait") && flags.booleans.has("--force")) {
|
|
97
|
+
throw usageError("Use either --wait or --force, not both.");
|
|
98
|
+
}
|
|
99
|
+
const invokeArgs = ["down"];
|
|
100
|
+
if (flags.booleans.has("--wait"))
|
|
101
|
+
invokeArgs.push("--wait");
|
|
102
|
+
if (flags.booleans.has("--force"))
|
|
103
|
+
invokeArgs.push("--force");
|
|
104
|
+
if (flags.values.has("--timeout")) {
|
|
105
|
+
const seconds = Number(flags.values.get("--timeout"));
|
|
106
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
107
|
+
throw usageError("--timeout must be a positive number of seconds.");
|
|
108
|
+
invokeArgs.push("--timeout", String(seconds));
|
|
109
|
+
}
|
|
110
|
+
const outcome = await handle.invoke(invokeArgs);
|
|
111
|
+
throwOnLauncherFailure(outcome);
|
|
112
|
+
const stopped = outcome.result?.stopped === true;
|
|
113
|
+
console.log(stopped
|
|
114
|
+
? `Stopped the Runtime Host. host.json and Tenants are kept under the Host root.`
|
|
115
|
+
: `No Runtime Host is running.`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (verb === "status") {
|
|
119
|
+
const flags = parseRuntimeFlags(rest, {
|
|
120
|
+
booleans: ["--json", "--env"],
|
|
121
|
+
values: ["--output"],
|
|
122
|
+
});
|
|
123
|
+
if (flags.rest.length)
|
|
124
|
+
throw usageError(runtimeUsage);
|
|
125
|
+
const json = flags.booleans.has("--json") || flags.values.get("--output") === "json";
|
|
126
|
+
if (flags.values.has("--output") &&
|
|
127
|
+
flags.values.get("--output") !== "json" &&
|
|
128
|
+
flags.values.get("--output") !== "text") {
|
|
129
|
+
throw usageError("--output must be text or json.");
|
|
130
|
+
}
|
|
131
|
+
if (flags.booleans.has("--env")) {
|
|
132
|
+
if (options.envHook)
|
|
133
|
+
await options.envHook();
|
|
134
|
+
else {
|
|
135
|
+
console.log("# runtime status --env requires a Project link (nylorun dev). Coming in a later release.");
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const outcome = await handle.invoke(["status"]);
|
|
140
|
+
throwOnLauncherFailure(outcome);
|
|
141
|
+
const result = outcome.result ?? {};
|
|
142
|
+
if (json) {
|
|
143
|
+
console.log(JSON.stringify({
|
|
144
|
+
...result,
|
|
145
|
+
node: handle.runtime.node,
|
|
146
|
+
recommendedRuntimeVersion: runtimeVersion(),
|
|
147
|
+
}, null, 2));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
printStatus(result, handle);
|
|
151
|
+
}
|
|
152
|
+
const state = typeof result.state === "string" ? result.state : undefined;
|
|
153
|
+
const code = exitCodeForStatusState(state);
|
|
154
|
+
if (code !== undefined)
|
|
155
|
+
process.exitCode = code;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (verb === "logs") {
|
|
159
|
+
const flags = parseRuntimeFlags(rest, {
|
|
160
|
+
booleans: ["-f", "--follow", "--all"],
|
|
161
|
+
values: ["-n", "--lines", "--tenant"],
|
|
162
|
+
});
|
|
163
|
+
if (flags.rest.length)
|
|
164
|
+
throw usageError(runtimeUsage);
|
|
165
|
+
if (flags.booleans.has("--all") && flags.values.has("--tenant")) {
|
|
166
|
+
throw usageError("Use either --tenant or --all, not both.");
|
|
167
|
+
}
|
|
168
|
+
const linesRaw = flags.values.get("-n") ?? flags.values.get("--lines");
|
|
169
|
+
const lines = linesRaw !== undefined ? Number(linesRaw) : 200;
|
|
170
|
+
if (!Number.isInteger(lines) || lines < 1)
|
|
171
|
+
throw usageError("-n must be a positive integer.");
|
|
172
|
+
const follow = flags.booleans.has("-f") || flags.booleans.has("--follow");
|
|
173
|
+
const invokeArgs = ["logs", "--lines", String(lines)];
|
|
174
|
+
if (follow)
|
|
175
|
+
invokeArgs.push("--follow");
|
|
176
|
+
if (flags.booleans.has("--all"))
|
|
177
|
+
invokeArgs.push("--all");
|
|
178
|
+
if (flags.values.has("--tenant")) {
|
|
179
|
+
invokeArgs.push("--tenant", flags.values.get("--tenant"));
|
|
180
|
+
}
|
|
181
|
+
const outcome = follow
|
|
182
|
+
? await handle.invokeStreaming(invokeArgs)
|
|
183
|
+
: await handle.invoke(invokeArgs);
|
|
184
|
+
if (outcome.exitCode !== 0) {
|
|
185
|
+
if (outcome.error?.code === "not_found" ||
|
|
186
|
+
/no .*log/i.test(outcome.error?.message ?? outcome.stderr)) {
|
|
187
|
+
throw new CliError(`No Runtime Host log under ${home}. Start one with "nylorun runtime up".`, 3);
|
|
188
|
+
}
|
|
189
|
+
throwOnLauncherFailure(outcome);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (verb === "restart") {
|
|
194
|
+
const flags = parseRuntimeFlags(rest, {
|
|
195
|
+
booleans: ["--allow-downgrade", "--wait", "--force"],
|
|
196
|
+
values: ["--port"],
|
|
197
|
+
});
|
|
198
|
+
if (flags.rest.length)
|
|
199
|
+
throw usageError(runtimeUsage);
|
|
200
|
+
if (flags.booleans.has("--wait") && flags.booleans.has("--force")) {
|
|
201
|
+
throw usageError("Use either --wait or --force, not both.");
|
|
202
|
+
}
|
|
203
|
+
const port = parsePort(flags.values.get("--port"));
|
|
204
|
+
const invokeArgs = ["restart"];
|
|
205
|
+
if (port !== undefined)
|
|
206
|
+
invokeArgs.push("--port", String(port));
|
|
207
|
+
if (flags.booleans.has("--allow-downgrade"))
|
|
208
|
+
invokeArgs.push("--allow-downgrade");
|
|
209
|
+
if (flags.booleans.has("--wait"))
|
|
210
|
+
invokeArgs.push("--wait");
|
|
211
|
+
if (flags.booleans.has("--force"))
|
|
212
|
+
invokeArgs.push("--force");
|
|
213
|
+
const outcome = await handle.invoke(invokeArgs);
|
|
214
|
+
throwOnLauncherFailure(outcome);
|
|
215
|
+
printUpResult(outcome.result ?? {});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (verb === "run") {
|
|
219
|
+
const flags = parseRuntimeFlags(rest, {
|
|
220
|
+
values: ["--port"],
|
|
221
|
+
});
|
|
222
|
+
if (flags.rest.length)
|
|
223
|
+
throw usageError(runtimeUsage);
|
|
224
|
+
const port = parsePort(flags.values.get("--port"));
|
|
225
|
+
const invokeArgs = ["run"];
|
|
226
|
+
if (port !== undefined)
|
|
227
|
+
invokeArgs.push("--port", String(port));
|
|
228
|
+
const outcome = await handle.invokeStreaming(invokeArgs);
|
|
229
|
+
throwOnLauncherFailure(outcome);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
throw usageError(runtimeUsage);
|
|
233
|
+
}
|
|
234
|
+
function printUpResult(result) {
|
|
235
|
+
const lines = [
|
|
236
|
+
["Host", typeof result.hostId === "string" ? result.hostId : undefined],
|
|
237
|
+
["URL", typeof result.url === "string" ? result.url : undefined],
|
|
238
|
+
[
|
|
239
|
+
"PID",
|
|
240
|
+
typeof result.pid === "number" ? String(result.pid) : undefined,
|
|
241
|
+
],
|
|
242
|
+
[
|
|
243
|
+
"Version",
|
|
244
|
+
typeof result.version === "string" ? result.version : undefined,
|
|
245
|
+
],
|
|
246
|
+
[
|
|
247
|
+
"Started",
|
|
248
|
+
result.started === true
|
|
249
|
+
? "yes"
|
|
250
|
+
: result.started === false
|
|
251
|
+
? "already running"
|
|
252
|
+
: undefined,
|
|
253
|
+
],
|
|
254
|
+
];
|
|
255
|
+
const width = Math.max(...lines.map(([label]) => label.length));
|
|
256
|
+
for (const [label, value] of lines) {
|
|
257
|
+
if (value !== undefined)
|
|
258
|
+
console.log(`${label.padEnd(width)} ${value}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function printStatus(result, handle) {
|
|
262
|
+
const lines = [
|
|
263
|
+
["Home", typeof result.home === "string" ? result.home : undefined],
|
|
264
|
+
["State", typeof result.state === "string" ? result.state : undefined],
|
|
265
|
+
["Host", typeof result.hostId === "string" ? result.hostId : undefined],
|
|
266
|
+
["URL", typeof result.url === "string" ? result.url : undefined],
|
|
267
|
+
[
|
|
268
|
+
"PID",
|
|
269
|
+
typeof result.pid === "number" ? String(result.pid) : undefined,
|
|
270
|
+
],
|
|
271
|
+
[
|
|
272
|
+
"Version",
|
|
273
|
+
typeof result.version === "string" ? result.version : undefined,
|
|
274
|
+
],
|
|
275
|
+
[
|
|
276
|
+
"Last started",
|
|
277
|
+
typeof result.runtimeVersion === "string"
|
|
278
|
+
? result.runtimeVersion
|
|
279
|
+
: undefined,
|
|
280
|
+
],
|
|
281
|
+
["Installed", `${handle.runtime.version} (${handle.runtime.bin})`],
|
|
282
|
+
["Node", handle.runtime.node],
|
|
283
|
+
["Recommended", runtimeVersion()],
|
|
284
|
+
];
|
|
285
|
+
const width = Math.max(...lines.map(([label]) => label.length));
|
|
286
|
+
for (const [label, value] of lines) {
|
|
287
|
+
if (value !== undefined)
|
|
288
|
+
console.log(`${label.padEnd(width)} ${value}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
export { runtimeVersion } from "./version.js";
|
|
292
|
+
export { launcher } from "./launcher.js";
|