@nylorun/cli 0.2.1-beta → 0.4.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/README.md +66 -42
- package/dist/baseline.d.ts +2 -0
- package/dist/baseline.js +10 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +73 -242
- package/dist/doctor.d.ts +15 -4
- package/dist/doctor.js +44 -46
- package/dist/environment.d.ts +4 -1
- package/dist/environment.js +12 -3
- package/dist/home.d.ts +6 -0
- package/dist/home.js +15 -0
- package/dist/model/configure.d.ts +30 -0
- package/dist/model/configure.js +117 -11
- package/dist/model/host-model.d.ts +5 -3
- package/dist/model/host-model.js +37 -16
- package/dist/project/create-tenant.d.ts +28 -0
- package/dist/project/create-tenant.js +66 -0
- package/dist/project/credentials.d.ts +18 -0
- package/dist/project/credentials.js +64 -0
- package/dist/project/env.d.ts +3 -0
- package/dist/project/env.js +21 -0
- package/dist/project/link.d.ts +20 -0
- package/dist/project/link.js +77 -0
- package/dist/project/root.d.ts +9 -0
- package/dist/project/root.js +52 -0
- package/dist/project/seed.d.ts +25 -0
- package/dist/project/seed.js +96 -0
- package/dist/tenant/commands.d.ts +4 -0
- package/dist/tenant/commands.js +321 -0
- package/package.json +6 -6
- package/dist/dev-entry.d.ts +0 -1
- package/dist/dev-entry.js +0 -6
- package/dist/dev.d.ts +0 -6
- package/dist/dev.js +0 -65
- package/dist/launcher.d.ts +0 -5
- package/dist/launcher.js +0 -4
- package/dist/project-runner.d.ts +0 -20
- package/dist/project-runner.js +0 -141
- package/dist/runtime-host.d.ts +0 -39
- package/dist/runtime-host.js +0 -355
- package/dist/scope.d.ts +0 -65
- package/dist/scope.js +0 -191
|
@@ -0,0 +1,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 nylo tenant create.`, 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,21 @@
|
|
|
1
|
+
import { readCredentials } from "./credentials.js";
|
|
2
|
+
import { readLink } from "./link.js";
|
|
3
|
+
import { findProjectRoot } from "./root.js";
|
|
4
|
+
/** Three `export` lines for `nylo env` (was `nylorun status --env`, F2-7). */
|
|
5
|
+
export async function printLinkedEnvExports(projectRoot = process.cwd()) {
|
|
6
|
+
const root = findProjectRoot(projectRoot) ?? projectRoot;
|
|
7
|
+
const link = await readLink(root);
|
|
8
|
+
const credentials = await readCredentials(root);
|
|
9
|
+
if (!link || !credentials) {
|
|
10
|
+
console.log("# No Project link in this directory. Run nylo tenant create to create one.");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
console.log(`export NYLORUN_RUNTIME_URL=${shellQuote(link.hostUrl)}`);
|
|
14
|
+
console.log(`export NYLORUN_SERVER_KEY=${shellQuote(credentials.applicationKey)}`);
|
|
15
|
+
console.log(`export NYLORUN_TENANT=${shellQuote(link.tenantId)}`);
|
|
16
|
+
}
|
|
17
|
+
export function shellQuote(value) {
|
|
18
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(value))
|
|
19
|
+
return value;
|
|
20
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
21
|
+
}
|
|
@@ -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 nylo tenant create.`, 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,52 @@
|
|
|
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
|
+
// The home directory is never a Project, but a package.json below it is.
|
|
30
|
+
if (directory === stop)
|
|
31
|
+
return fallback;
|
|
32
|
+
if (existsSync(join(directory, ".nylorun")))
|
|
33
|
+
return directory;
|
|
34
|
+
if (!fallback && existsSync(join(directory, "package.json")))
|
|
35
|
+
fallback = directory;
|
|
36
|
+
if (directory === root)
|
|
37
|
+
break;
|
|
38
|
+
const parent = dirname(directory);
|
|
39
|
+
if (parent === directory)
|
|
40
|
+
break;
|
|
41
|
+
directory = parent;
|
|
42
|
+
}
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
/** Require a Project root or throw a usage-oriented error. */
|
|
46
|
+
export function requireProjectRoot(cwd = process.cwd()) {
|
|
47
|
+
const root = findProjectRoot(cwd);
|
|
48
|
+
if (!root) {
|
|
49
|
+
throw new Error("No Project found (looked for package.json or .nylorun/). Run this from a Project directory.");
|
|
50
|
+
}
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
* Seed the Tenant-level fixture model (`fixtureModel: true`, Host feature
|
|
12
|
+
* `tenant-fixture-model`) and skip the Project's model credential.
|
|
13
|
+
*/
|
|
14
|
+
fixtureModel?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Seed non-secret Tenant config via `PUT /v1/tenant/config/seed`.
|
|
18
|
+
* Model secrets go through the model route into the Tenant vault (F3).
|
|
19
|
+
* `.env` is read as a value map only — never written back or applied to process.env.
|
|
20
|
+
*/
|
|
21
|
+
export declare function seedTenantFromProject(options: SeedOptions): Promise<{
|
|
22
|
+
applied: string[];
|
|
23
|
+
kept: string[];
|
|
24
|
+
model?: string;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,96 @@
|
|
|
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" || sandboxRaw === "virtual"
|
|
25
|
+
? { backend: sandboxRaw }
|
|
26
|
+
: undefined;
|
|
27
|
+
let applied = [];
|
|
28
|
+
let kept = [];
|
|
29
|
+
if (sandbox || options.fixtureModel) {
|
|
30
|
+
const response = await fetchImpl(`${options.hostUrl.replace(/\/$/, "")}/v1/tenant/config/seed`, {
|
|
31
|
+
method: "PUT",
|
|
32
|
+
headers: {
|
|
33
|
+
...tenantHeaders(options.applicationKey, options.tenantId),
|
|
34
|
+
"content-type": "application/json",
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
requestId: randomUUID(),
|
|
38
|
+
...(sandbox ? { sandbox } : {}),
|
|
39
|
+
...(options.fixtureModel ? { fixtureModel: true } : {}),
|
|
40
|
+
}),
|
|
41
|
+
signal: options.signal ?? AbortSignal.timeout(15_000),
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new CliError(`Tenant config seed failed (${response.status}): ${await response
|
|
45
|
+
.text()
|
|
46
|
+
.catch(() => "")}`, 1);
|
|
47
|
+
}
|
|
48
|
+
const body = (await response.json());
|
|
49
|
+
applied = body.applied ?? [];
|
|
50
|
+
kept = body.kept ?? [];
|
|
51
|
+
}
|
|
52
|
+
// A fixture-model Tenant never calls a real model: its credential stays out of it.
|
|
53
|
+
let model;
|
|
54
|
+
if (!options.fixtureModel && env.NYLORUN_DEV_MODEL?.trim() !== "fixture") {
|
|
55
|
+
const seeded = modelFromEnv(env, options.projectRoot);
|
|
56
|
+
if (seeded) {
|
|
57
|
+
await putHostModel(options.hostUrl, options.applicationKey, seeded, options.tenantId);
|
|
58
|
+
model = `${seeded.provider}/${seeded.model}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { applied, kept, ...(model ? { model } : {}) };
|
|
62
|
+
}
|
|
63
|
+
function modelFromEnv(env, projectRoot) {
|
|
64
|
+
const provider = env.MODEL_PROVIDER?.trim();
|
|
65
|
+
const model = env.MODEL?.trim();
|
|
66
|
+
if (!provider || !model)
|
|
67
|
+
return undefined;
|
|
68
|
+
const baseUrl = env.MODEL_PROVIDER_BASE_URL?.trim();
|
|
69
|
+
const key = env.MODEL_PROVIDER_API_KEY;
|
|
70
|
+
if (key) {
|
|
71
|
+
return {
|
|
72
|
+
provider,
|
|
73
|
+
model,
|
|
74
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
75
|
+
auth: { type: "api_key", key },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
for (const relative of [".nylorun/auth.json", ".env/auth.json"]) {
|
|
79
|
+
try {
|
|
80
|
+
const all = JSON.parse(readFileSync(join(projectRoot, relative), "utf8"));
|
|
81
|
+
const credential = all[provider];
|
|
82
|
+
if (credential?.type === "api_key" || credential?.type === "oauth") {
|
|
83
|
+
return {
|
|
84
|
+
provider,
|
|
85
|
+
model,
|
|
86
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
87
|
+
auth: credential,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* absent */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type AdminTenant } from "@nylorun/admin";
|
|
2
|
+
/** Exact-and-unique name or id match. */
|
|
3
|
+
export declare function matchTenant(tenants: readonly AdminTenant[], nameOrId: string): AdminTenant;
|
|
4
|
+
export declare function tenantCommand(args: readonly string[]): Promise<void>;
|