nylorun 0.3.0-beta → 0.5.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 +64 -0
- package/README.md +154 -68
- package/dist/cli.js +29 -25
- package/dist/doctor.d.ts +7 -5
- package/dist/doctor.js +58 -29
- package/dist/home.d.ts +1 -1
- package/dist/home.js +1 -1
- package/dist/project/derived-key.d.ts +8 -0
- package/dist/project/derived-key.js +18 -0
- package/dist/project/link.d.ts +32 -4
- package/dist/project/link.js +83 -14
- package/dist/project/seed.d.ts +27 -0
- package/dist/project/seed.js +111 -0
- package/dist/stack/commands.d.ts +42 -19
- package/dist/stack/commands.js +465 -152
- package/dist/stack/compose-file.d.ts +30 -11
- package/dist/stack/compose-file.js +88 -18
- package/dist/stack/docker.js +1 -1
- package/dist/stack/env-file.d.ts +24 -3
- package/dist/stack/env-file.js +49 -4
- package/dist/stack/host-files.d.ts +12 -3
- package/dist/stack/host-files.js +26 -3
- package/dist/stack/images.d.ts +1 -1
- package/dist/stack/images.js +1 -1
- package/dist/stack/index.d.ts +3 -3
- package/dist/stack/index.js +12 -3
- package/dist/stack/paths.d.ts +12 -4
- package/dist/stack/paths.js +9 -6
- package/dist/stack/ports.d.ts +2 -2
- package/dist/stack/ports.js +1 -1
- package/dist/stack/prepare.d.ts +13 -4
- package/dist/stack/prepare.js +25 -13
- package/dist/stack/restate-identity.d.ts +1 -1
- package/dist/stack/restate-identity.js +1 -1
- package/dist/stack/stacks.d.ts +50 -0
- package/dist/stack/stacks.js +150 -0
- package/dist/stack/versions.d.ts +1 -1
- package/dist/stack/versions.js +1 -1
- package/dist/telemetry.d.ts +31 -0
- package/dist/telemetry.js +79 -0
- package/package.json +5 -5
package/dist/stack/ports.js
CHANGED
|
@@ -30,7 +30,7 @@ export const loopbackPorts = {
|
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
32
|
* Choose a port: the persisted one if any (kept even when busy, because the
|
|
33
|
-
* running
|
|
33
|
+
* running Tenant may hold it), else the default when free, else a free one.
|
|
34
34
|
* Never returns a port in `taken`.
|
|
35
35
|
*/
|
|
36
36
|
export async function choosePort(probe, preferred, persisted, taken) {
|
package/dist/stack/prepare.d.ts
CHANGED
|
@@ -10,22 +10,29 @@ export interface PreparedStack {
|
|
|
10
10
|
/** True when this run chose the ports (first start). */
|
|
11
11
|
firstRun: boolean;
|
|
12
12
|
}
|
|
13
|
-
/** Read the persisted
|
|
14
|
-
export declare function readStackEnv(paths: StackPaths): Promise<import("./env-file.js").PersistedStackEnv | undefined>;
|
|
13
|
+
/** Read the persisted Compose settings without writing anything. */
|
|
14
|
+
export declare function readStackEnv(paths: Pick<StackPaths, "env">): Promise<import("./env-file.js").PersistedStackEnv | undefined>;
|
|
15
15
|
/**
|
|
16
16
|
* Write everything `docker compose up` needs under the Host root:
|
|
17
|
-
* host.json, host-credentials.json (0600),
|
|
18
|
-
*
|
|
17
|
+
* host.json, host-credentials.json (0600), docker/compose.yaml,
|
|
18
|
+
* docker/.env (0600) and docker/restate-identity.pem (0600). Ports and the
|
|
19
19
|
* Postgres password persist in .env; the identity key persists in its PEM.
|
|
20
20
|
*/
|
|
21
21
|
export declare function prepareStack(input: {
|
|
22
22
|
paths: StackPaths;
|
|
23
|
+
/** The Tenant's name and its Compose project. */
|
|
24
|
+
name: string;
|
|
25
|
+
project: string;
|
|
23
26
|
images: StackImages;
|
|
24
27
|
uid: number;
|
|
25
28
|
gid: number;
|
|
26
29
|
/** Recorded in host.json; undefined keeps the recorded version. */
|
|
27
30
|
runtimeVersion: string | undefined;
|
|
28
31
|
ports: PortProbe;
|
|
32
|
+
/** Ports other Tenants keep in their `.env`: never chosen for a port not chosen yet. */
|
|
33
|
+
reserved?: ReadonlySet<number>;
|
|
34
|
+
/** `NYLORUN_DERIVED_PRINCIPALS` from the environment of `nylorun start`; else kept. */
|
|
35
|
+
derivedPrincipals?: string;
|
|
29
36
|
/**
|
|
30
37
|
* Changes to the origins that may embed Studio: `reset` goes back to the
|
|
31
38
|
* defaults, `add` appends (for example a desktop app's dev server).
|
|
@@ -34,4 +41,6 @@ export declare function prepareStack(input: {
|
|
|
34
41
|
add?: readonly string[];
|
|
35
42
|
reset?: boolean;
|
|
36
43
|
};
|
|
44
|
+
/** The measurement id Studio reports page views to; absent when telemetry is off. */
|
|
45
|
+
studioAnalyticsId?: string;
|
|
37
46
|
}): Promise<PreparedStack>;
|
package/dist/stack/prepare.js
CHANGED
|
@@ -3,8 +3,8 @@ import { chmod, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { renderComposeFile } from "./compose-file.js";
|
|
4
4
|
import { parseFrameAncestors } from "@nylorun/core/contracts";
|
|
5
5
|
import { CliError } from "../errors.js";
|
|
6
|
-
import { DEFAULT_STUDIO_FRAME_ANCESTORS, parsePersisted, renderEnvFile, } from "./env-file.js";
|
|
7
|
-
import { ensureHostCredentials, ensureHostLayout, writeStackHostConfig, } from "./host-files.js";
|
|
6
|
+
import { DEFAULT_STUDIO_FRAME_ANCESTORS, parseDerivedPrincipals, parsePersisted, renderEnvFile, } from "./env-file.js";
|
|
7
|
+
import { ensureHostCredentials, ensureVaultKey, ensureHostLayout, writeStackHostConfig, } from "./host-files.js";
|
|
8
8
|
import { choosePort, DEFAULT_PORTS } from "./ports.js";
|
|
9
9
|
import { ensureIdentityKey } from "./restate-identity.js";
|
|
10
10
|
async function writeFileMode(path, text, mode) {
|
|
@@ -21,15 +21,15 @@ async function readText(path) {
|
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
/** Read the persisted
|
|
24
|
+
/** Read the persisted Compose settings without writing anything. */
|
|
25
25
|
export async function readStackEnv(paths) {
|
|
26
26
|
const text = await readText(paths.env);
|
|
27
27
|
return text === undefined ? undefined : parsePersisted(text);
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Write everything `docker compose up` needs under the Host root:
|
|
31
|
-
* host.json, host-credentials.json (0600),
|
|
32
|
-
*
|
|
31
|
+
* host.json, host-credentials.json (0600), docker/compose.yaml,
|
|
32
|
+
* docker/.env (0600) and docker/restate-identity.pem (0600). Ports and the
|
|
33
33
|
* Postgres password persist in .env; the identity key persists in its PEM.
|
|
34
34
|
*/
|
|
35
35
|
export async function prepareStack(input) {
|
|
@@ -38,13 +38,17 @@ export async function prepareStack(input) {
|
|
|
38
38
|
const persisted = (await readStackEnv(paths)) ?? {};
|
|
39
39
|
const firstRun = persisted.runtimePort === undefined;
|
|
40
40
|
const taken = new Set();
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
const reserved = input.reserved ?? new Set();
|
|
42
|
+
const pick = async (preferred, persistedPort) => {
|
|
43
|
+
const avoid = persistedPort === undefined ? new Set([...taken, ...reserved]) : taken;
|
|
44
|
+
const port = await choosePort(input.ports, preferred, persistedPort, avoid);
|
|
45
|
+
taken.add(port);
|
|
46
|
+
return port;
|
|
47
|
+
};
|
|
48
|
+
const runtimePort = await pick(DEFAULT_PORTS.runtime, persisted.runtimePort);
|
|
49
|
+
const adminPort = await pick(DEFAULT_PORTS.admin, persisted.adminPort);
|
|
50
|
+
const studioPort = await pick(DEFAULT_PORTS.studio, persisted.studioPort);
|
|
51
|
+
const restatePort = await pick(DEFAULT_PORTS.restate, persisted.restatePort);
|
|
48
52
|
const identity = await ensureIdentityKey(paths.restateIdentity, writeFileMode);
|
|
49
53
|
let added;
|
|
50
54
|
try {
|
|
@@ -63,6 +67,8 @@ export async function prepareStack(input) {
|
|
|
63
67
|
studioPort,
|
|
64
68
|
restatePort,
|
|
65
69
|
postgresPassword: persisted.postgresPassword ?? randomBytes(24).toString("hex"),
|
|
70
|
+
// Kept across starts: a new token would recreate the runtime and gateway containers.
|
|
71
|
+
gatesToken: persisted.gatesToken ?? randomBytes(32).toString("hex"),
|
|
66
72
|
restateIdentityKey: identity.publicKey,
|
|
67
73
|
uid: input.uid,
|
|
68
74
|
gid: input.gid,
|
|
@@ -70,14 +76,20 @@ export async function prepareStack(input) {
|
|
|
70
76
|
runtimeImage: input.images.runtime,
|
|
71
77
|
studioImage: input.images.studio,
|
|
72
78
|
studioFrameAncestors,
|
|
79
|
+
studioAnalyticsId: input.studioAnalyticsId ?? "",
|
|
80
|
+
tenantName: input.name,
|
|
81
|
+
derivedPrincipals: (input.derivedPrincipals?.trim()
|
|
82
|
+
? parseDerivedPrincipals(input.derivedPrincipals, "NYLORUN_DERIVED_PRINCIPALS")
|
|
83
|
+
: (persisted.derivedPrincipals ?? ["project"])).join(","),
|
|
73
84
|
};
|
|
74
85
|
const { adminKey } = await ensureHostCredentials(paths);
|
|
86
|
+
await ensureVaultKey(paths);
|
|
75
87
|
const host = await writeStackHostConfig(paths, {
|
|
76
88
|
port: runtimePort,
|
|
77
89
|
adminPort,
|
|
78
90
|
runtimeVersion: input.runtimeVersion,
|
|
79
91
|
});
|
|
80
92
|
await writeFileMode(paths.env, renderEnvFile(env), 0o600);
|
|
81
|
-
await writeFileMode(paths.compose, renderComposeFile(), 0o644);
|
|
93
|
+
await writeFileMode(paths.compose, renderComposeFile(input.project), 0o644);
|
|
82
94
|
return { env, host, adminKey, firstRun };
|
|
83
95
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* every request to the Worker endpoint with an Ed25519 private key; the
|
|
4
4
|
* Runtime accepts only requests that verify against the public key.
|
|
5
5
|
*
|
|
6
|
-
* - The private key is a PKCS#8 PEM at `
|
|
6
|
+
* - The private key is a PKCS#8 PEM at `docker/restate-identity.pem` (mode
|
|
7
7
|
* 0600), mounted read-only into the restate service as
|
|
8
8
|
* `RESTATE_WORKER__INVOKER__REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE`.
|
|
9
9
|
* - The public key goes to the Runtime as `NYLORUN_RESTATE_IDENTITY_KEY`, in
|
|
@@ -6,7 +6,7 @@ import { CliError } from "../errors.js";
|
|
|
6
6
|
* every request to the Worker endpoint with an Ed25519 private key; the
|
|
7
7
|
* Runtime accepts only requests that verify against the public key.
|
|
8
8
|
*
|
|
9
|
-
* - The private key is a PKCS#8 PEM at `
|
|
9
|
+
* - The private key is a PKCS#8 PEM at `docker/restate-identity.pem` (mode
|
|
10
10
|
* 0600), mounted read-only into the restate service as
|
|
11
11
|
* `RESTATE_WORKER__INVOKER__REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE`.
|
|
12
12
|
* - The public key goes to the Runtime as `NYLORUN_RESTATE_IDENTITY_KEY`, in
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The machine's local Tenants (tenancy.md §6): one installation per Tenant, each with its own
|
|
3
|
+
* Host root `~/.nylorun/tenants/<name>/`, Compose project `nylorun-<name>`, ports and volumes.
|
|
4
|
+
*/
|
|
5
|
+
/** The Tenant a command acts on outside a project when nothing names one. */
|
|
6
|
+
export declare const DEFAULT_TENANT = "default";
|
|
7
|
+
/** A Tenant name: also part of its Compose project, so Compose's rules apply. */
|
|
8
|
+
export declare const TENANT_NAME: RegExp;
|
|
9
|
+
/** `~/.nylorun`: the Tenants and the machine's settings. Tests pass another directory. */
|
|
10
|
+
export declare function defaultNylorunRoot(): string;
|
|
11
|
+
export declare function tenantsDir(base: string): string;
|
|
12
|
+
export declare function tenantRoot(base: string, name: string): string;
|
|
13
|
+
export declare function assertTenantName(name: string, source: string): string;
|
|
14
|
+
/** A directory name as a Tenant name: lowercased, other characters as `-`, trimmed. */
|
|
15
|
+
export declare function sanitizeTenantName(raw: string): string;
|
|
16
|
+
/** `tenant.json` in a Host root: the Tenant's name and the project it was created for. */
|
|
17
|
+
export interface TenantRecord {
|
|
18
|
+
format: 1;
|
|
19
|
+
name: string;
|
|
20
|
+
/** The project directory `nylorun start` created the Tenant in; absent outside a project. */
|
|
21
|
+
project?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function tenantRecordPath(root: string): string;
|
|
24
|
+
export declare function readTenantRecord(root: string): Promise<TenantRecord | undefined>;
|
|
25
|
+
export declare function writeTenantRecord(root: string, record: Omit<TenantRecord, "format">): Promise<void>;
|
|
26
|
+
export interface TenantEntry {
|
|
27
|
+
name: string;
|
|
28
|
+
root: string;
|
|
29
|
+
record: TenantRecord;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The Tenants under `<base>/tenants`, by name: directories with a `tenant.json` (releases
|
|
33
|
+
* before 0.4 kept other directories there).
|
|
34
|
+
*/
|
|
35
|
+
export declare function listTenants(base: string): Promise<TenantEntry[]>;
|
|
36
|
+
/**
|
|
37
|
+
* The Tenant name for a project without a link: the directory's name, or the first `-2`,
|
|
38
|
+
* `-3`, … suffix that no Tenant of another project directory holds. A Tenant this project
|
|
39
|
+
* directory created keeps its name.
|
|
40
|
+
*/
|
|
41
|
+
export declare function chooseTenantName(base: string, projectDir: string): Promise<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Move the Host roots of nylorun 0.4 (`~/.nylorun/stacks/<name>/`) to `tenants/<name>/`, when
|
|
44
|
+
* that is free: `stack.json` becomes `tenant.json`, and `NYLORUN_STACK_NAME` becomes
|
|
45
|
+
* `NYLORUN_TENANT_NAME` in `docker/.env` (and `docker/compose.yaml`, until the next start
|
|
46
|
+
* rewrites it). `stacks/` goes when empty. They move rather than start fresh because Compose
|
|
47
|
+
* project names do not change: a fresh Host root under the same name would reuse the old
|
|
48
|
+
* volumes with new keys.
|
|
49
|
+
*/
|
|
50
|
+
export declare function moveStackRoots(base: string, err: (line: string) => void): Promise<void>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readdir, readFile, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, join, resolve, sep } from "node:path";
|
|
6
|
+
import { CliError } from "../errors.js";
|
|
7
|
+
/**
|
|
8
|
+
* The machine's local Tenants (tenancy.md §6): one installation per Tenant, each with its own
|
|
9
|
+
* Host root `~/.nylorun/tenants/<name>/`, Compose project `nylorun-<name>`, ports and volumes.
|
|
10
|
+
*/
|
|
11
|
+
/** The Tenant a command acts on outside a project when nothing names one. */
|
|
12
|
+
export const DEFAULT_TENANT = "default";
|
|
13
|
+
/** A Tenant name: also part of its Compose project, so Compose's rules apply. */
|
|
14
|
+
export const TENANT_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
15
|
+
/** `~/.nylorun`: the Tenants and the machine's settings. Tests pass another directory. */
|
|
16
|
+
export function defaultNylorunRoot() {
|
|
17
|
+
return resolve(join(homedir(), ".nylorun"));
|
|
18
|
+
}
|
|
19
|
+
export function tenantsDir(base) {
|
|
20
|
+
return join(base, "tenants");
|
|
21
|
+
}
|
|
22
|
+
export function tenantRoot(base, name) {
|
|
23
|
+
return join(tenantsDir(base), name);
|
|
24
|
+
}
|
|
25
|
+
export function assertTenantName(name, source) {
|
|
26
|
+
// A Tenant id, such as a NYLORUN_TENANT kept from an older release.
|
|
27
|
+
if (/^tn_/i.test(name))
|
|
28
|
+
throw new CliError(`${source} must be a Tenant's name, not a Tenant id: ${name}. "nylorun ls" lists the Tenants on this machine.`, 2);
|
|
29
|
+
if (!TENANT_NAME.test(name))
|
|
30
|
+
throw new CliError(`${source} must be lowercase letters, digits, "-" or "_", starting with a letter or digit: ${name}`, 2);
|
|
31
|
+
return name;
|
|
32
|
+
}
|
|
33
|
+
/** A directory name as a Tenant name: lowercased, other characters as `-`, trimmed. */
|
|
34
|
+
export function sanitizeTenantName(raw) {
|
|
35
|
+
const name = raw
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
38
|
+
.replace(/-{2,}/g, "-")
|
|
39
|
+
.replace(/^[-_]+|[-_]+$/g, "")
|
|
40
|
+
.replace(/^tn_/, "tn-");
|
|
41
|
+
return name === "" ? "tenant" : name;
|
|
42
|
+
}
|
|
43
|
+
export function tenantRecordPath(root) {
|
|
44
|
+
return join(root, "tenant.json");
|
|
45
|
+
}
|
|
46
|
+
export async function readTenantRecord(root) {
|
|
47
|
+
try {
|
|
48
|
+
const value = JSON.parse(await readFile(tenantRecordPath(root), "utf8"));
|
|
49
|
+
if (typeof value?.name !== "string" || !TENANT_NAME.test(value.name))
|
|
50
|
+
return undefined;
|
|
51
|
+
return {
|
|
52
|
+
format: 1,
|
|
53
|
+
name: value.name,
|
|
54
|
+
...(typeof value.project === "string" ? { project: value.project } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function writeTenantRecord(root, record) {
|
|
62
|
+
const path = tenantRecordPath(root);
|
|
63
|
+
const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
64
|
+
const body = { format: 1, name: record.name, ...(record.project ? { project: record.project } : {}) };
|
|
65
|
+
await writeFile(temporary, `${JSON.stringify(body, null, 2)}\n`, { mode: 0o600 });
|
|
66
|
+
await rename(temporary, path);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The Tenants under `<base>/tenants`, by name: directories with a `tenant.json` (releases
|
|
70
|
+
* before 0.4 kept other directories there).
|
|
71
|
+
*/
|
|
72
|
+
export async function listTenants(base) {
|
|
73
|
+
let entries;
|
|
74
|
+
try {
|
|
75
|
+
entries = await readdir(tenantsDir(base), { withFileTypes: true });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const tenants = [];
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (!entry.isDirectory() || !TENANT_NAME.test(entry.name))
|
|
83
|
+
continue;
|
|
84
|
+
const root = tenantRoot(base, entry.name);
|
|
85
|
+
const record = await readTenantRecord(root);
|
|
86
|
+
if (record)
|
|
87
|
+
tenants.push({ name: entry.name, root, record });
|
|
88
|
+
}
|
|
89
|
+
return tenants.sort((a, b) => a.name.localeCompare(b.name));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The Tenant name for a project without a link: the directory's name, or the first `-2`,
|
|
93
|
+
* `-3`, … suffix that no Tenant of another project directory holds. A Tenant this project
|
|
94
|
+
* directory created keeps its name.
|
|
95
|
+
*/
|
|
96
|
+
export async function chooseTenantName(base, projectDir) {
|
|
97
|
+
const name = sanitizeTenantName(basename(projectDir));
|
|
98
|
+
const tenants = await listTenants(base);
|
|
99
|
+
const candidate = new RegExp(`^${name}(?:-[0-9]+)?$`);
|
|
100
|
+
const own = tenants.find((tenant) => candidate.test(tenant.name) && tenant.record.project === projectDir);
|
|
101
|
+
if (own)
|
|
102
|
+
return own.name;
|
|
103
|
+
for (let suffix = 1;; suffix += 1) {
|
|
104
|
+
const next = suffix === 1 ? name : `${name}-${suffix}`;
|
|
105
|
+
if (!existsSync(tenantRoot(base, next)))
|
|
106
|
+
return next;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Move the Host roots of nylorun 0.4 (`~/.nylorun/stacks/<name>/`) to `tenants/<name>/`, when
|
|
111
|
+
* that is free: `stack.json` becomes `tenant.json`, and `NYLORUN_STACK_NAME` becomes
|
|
112
|
+
* `NYLORUN_TENANT_NAME` in `docker/.env` (and `docker/compose.yaml`, until the next start
|
|
113
|
+
* rewrites it). `stacks/` goes when empty. They move rather than start fresh because Compose
|
|
114
|
+
* project names do not change: a fresh Host root under the same name would reuse the old
|
|
115
|
+
* volumes with new keys.
|
|
116
|
+
*/
|
|
117
|
+
export async function moveStackRoots(base, err) {
|
|
118
|
+
const stacks = join(base, "stacks");
|
|
119
|
+
let names;
|
|
120
|
+
try {
|
|
121
|
+
names = (await readdir(stacks, { withFileTypes: true }))
|
|
122
|
+
.filter((entry) => entry.isDirectory() && TENANT_NAME.test(entry.name))
|
|
123
|
+
.map((entry) => entry.name);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const moved = [];
|
|
129
|
+
for (const name of names.sort()) {
|
|
130
|
+
const root = tenantRoot(base, name);
|
|
131
|
+
if (existsSync(root))
|
|
132
|
+
continue;
|
|
133
|
+
await mkdir(tenantsDir(base), { recursive: true, mode: 0o700 });
|
|
134
|
+
await rename(join(stacks, name), root);
|
|
135
|
+
await rename(join(root, "stack.json"), tenantRecordPath(root)).catch(() => { });
|
|
136
|
+
for (const file of [join(root, "docker", ".env"), join(root, "docker", "compose.yaml")]) {
|
|
137
|
+
const text = await readFile(file, "utf8").catch(() => undefined);
|
|
138
|
+
if (text?.includes("NYLORUN_STACK_NAME"))
|
|
139
|
+
await writeFile(file, text.replaceAll("NYLORUN_STACK_NAME", "NYLORUN_TENANT_NAME"));
|
|
140
|
+
}
|
|
141
|
+
moved.push(name);
|
|
142
|
+
}
|
|
143
|
+
// A marker file of 0.4.
|
|
144
|
+
await rm(join(stacks, ".legacy-noted"), { force: true });
|
|
145
|
+
await rmdir(stacks).catch(() => { });
|
|
146
|
+
if (moved.length) {
|
|
147
|
+
const tilde = (path) => path.startsWith(homedir() + sep) ? `~${path.slice(homedir().length)}` : path;
|
|
148
|
+
err(`Moved Tenants ${moved.join(", ")} from ${tilde(stacks)} to ${tilde(tenantsDir(base))}.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
package/dist/stack/versions.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type PinnedImage = "runtime" | "studio";
|
|
|
3
3
|
/**
|
|
4
4
|
* The version this CLI release pins for an image, from `cli/package.json`
|
|
5
5
|
* `nylorun.runtime` or `nylorun.studio`: the tag of `ghcr.io/nylorun/<name>`
|
|
6
|
-
*
|
|
6
|
+
* a local Tenant runs unless `NYLORUN_RUNTIME_IMAGE` / `NYLORUN_STUDIO_IMAGE`
|
|
7
7
|
* overrides it. The release tooling writes both pins.
|
|
8
8
|
*/
|
|
9
9
|
export declare function pinnedVersion(name: PinnedImage): string;
|
package/dist/stack/versions.js
CHANGED
|
@@ -9,7 +9,7 @@ function cliPackageJsonPath() {
|
|
|
9
9
|
/**
|
|
10
10
|
* The version this CLI release pins for an image, from `cli/package.json`
|
|
11
11
|
* `nylorun.runtime` or `nylorun.studio`: the tag of `ghcr.io/nylorun/<name>`
|
|
12
|
-
*
|
|
12
|
+
* a local Tenant runs unless `NYLORUN_RUNTIME_IMAGE` / `NYLORUN_STUDIO_IMAGE`
|
|
13
13
|
* overrides it. The release tooling writes both pins.
|
|
14
14
|
*/
|
|
15
15
|
export function pinnedVersion(name) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio's anonymous usage analytics. Studio's dashboard reports page views,
|
|
3
|
+
* with every id replaced by `:id`, to this Google Analytics property unless the
|
|
4
|
+
* developer opted out. `nylorun start` decides and passes the id to the
|
|
5
|
+
* Studio container (`NYLORUN_STUDIO_ANALYTICS_ID`); Studio never decides itself.
|
|
6
|
+
*/
|
|
7
|
+
export declare const STUDIO_ANALYTICS_ID = "G-K6RPDFH6Q6";
|
|
8
|
+
/** `~/.nylorun/telemetry.json`: the developer's choice, for every Tenant on the machine. */
|
|
9
|
+
export interface TelemetryFile {
|
|
10
|
+
format: 1;
|
|
11
|
+
/** False after `nylorun telemetry disable`; absent until a choice is made. */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** When `nylorun start` showed the notice (ISO time); it is shown once. */
|
|
14
|
+
noticeShown?: string;
|
|
15
|
+
}
|
|
16
|
+
export type TelemetryDecision = Readonly<{
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
reason: string;
|
|
19
|
+
}>;
|
|
20
|
+
export declare const TELEMETRY_NOTICE: string;
|
|
21
|
+
export declare function telemetryPath(nylorunRoot: string): string;
|
|
22
|
+
export declare function readTelemetry(nylorunRoot: string): Promise<TelemetryFile>;
|
|
23
|
+
export declare function writeTelemetry(nylorunRoot: string, file: TelemetryFile): Promise<void>;
|
|
24
|
+
/** On unless the environment or the developer's choice turns it off. */
|
|
25
|
+
export declare function telemetryDecision(env: Readonly<Record<string, string | undefined>>, file: TelemetryFile): TelemetryDecision;
|
|
26
|
+
/** `nylorun telemetry [status|enable|disable]`. */
|
|
27
|
+
export declare function telemetryCommand(args: readonly string[], context: {
|
|
28
|
+
nylorunRoot: string;
|
|
29
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
30
|
+
out(line: string): void;
|
|
31
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { CliError } from "./errors.js";
|
|
5
|
+
/**
|
|
6
|
+
* Studio's anonymous usage analytics. Studio's dashboard reports page views,
|
|
7
|
+
* with every id replaced by `:id`, to this Google Analytics property unless the
|
|
8
|
+
* developer opted out. `nylorun start` decides and passes the id to the
|
|
9
|
+
* Studio container (`NYLORUN_STUDIO_ANALYTICS_ID`); Studio never decides itself.
|
|
10
|
+
*/
|
|
11
|
+
export const STUDIO_ANALYTICS_ID = "G-K6RPDFH6Q6";
|
|
12
|
+
export const TELEMETRY_NOTICE = [
|
|
13
|
+
"Studio collects anonymous usage data: which pages are opened, with every",
|
|
14
|
+
"Tenant, agent and session id removed. Nothing you send to agents is collected.",
|
|
15
|
+
'Turn it off with "nylorun telemetry disable" or NYLORUN_TELEMETRY_DISABLED=1.',
|
|
16
|
+
"Learn more: https://github.com/nylorun/agents/blob/main/nylorun/README.md#telemetry",
|
|
17
|
+
].join("\n");
|
|
18
|
+
export function telemetryPath(nylorunRoot) {
|
|
19
|
+
return join(nylorunRoot, "telemetry.json");
|
|
20
|
+
}
|
|
21
|
+
export async function readTelemetry(nylorunRoot) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(await readFile(telemetryPath(nylorunRoot), "utf8"));
|
|
24
|
+
if (parsed && typeof parsed === "object") {
|
|
25
|
+
const { enabled, noticeShown } = parsed;
|
|
26
|
+
return {
|
|
27
|
+
format: 1,
|
|
28
|
+
...(typeof enabled === "boolean" ? { enabled } : {}),
|
|
29
|
+
...(typeof noticeShown === "string" ? { noticeShown } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* missing or unreadable: no choice yet */
|
|
35
|
+
}
|
|
36
|
+
return { format: 1 };
|
|
37
|
+
}
|
|
38
|
+
export async function writeTelemetry(nylorunRoot, file) {
|
|
39
|
+
await mkdir(nylorunRoot, { recursive: true });
|
|
40
|
+
const path = telemetryPath(nylorunRoot);
|
|
41
|
+
const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
42
|
+
await writeFile(temporary, `${JSON.stringify(file, null, 2)}\n`);
|
|
43
|
+
await rename(temporary, path);
|
|
44
|
+
}
|
|
45
|
+
/** An environment flag is set unless it is empty, `0`, `false` or `no`. */
|
|
46
|
+
function flag(value) {
|
|
47
|
+
const trimmed = value?.trim() ?? "";
|
|
48
|
+
return trimmed !== "" && !/^(0|false|no)$/i.test(trimmed);
|
|
49
|
+
}
|
|
50
|
+
/** On unless the environment or the developer's choice turns it off. */
|
|
51
|
+
export function telemetryDecision(env, file) {
|
|
52
|
+
if (flag(env.NYLORUN_TELEMETRY_DISABLED))
|
|
53
|
+
return { enabled: false, reason: "NYLORUN_TELEMETRY_DISABLED is set" };
|
|
54
|
+
if (flag(env.DO_NOT_TRACK))
|
|
55
|
+
return { enabled: false, reason: "DO_NOT_TRACK is set" };
|
|
56
|
+
if (flag(env.CI))
|
|
57
|
+
return { enabled: false, reason: "CI is set" };
|
|
58
|
+
if (file.enabled === false)
|
|
59
|
+
return { enabled: false, reason: 'turned off with "nylorun telemetry disable"' };
|
|
60
|
+
return { enabled: true, reason: "on by default" };
|
|
61
|
+
}
|
|
62
|
+
const USAGE = "Usage: nylorun telemetry [status|enable|disable]";
|
|
63
|
+
/** `nylorun telemetry [status|enable|disable]`. */
|
|
64
|
+
export async function telemetryCommand(args, context) {
|
|
65
|
+
const [action = "status", ...rest] = args;
|
|
66
|
+
if (rest.length || !["status", "enable", "disable"].includes(action))
|
|
67
|
+
throw new CliError(USAGE, 2);
|
|
68
|
+
const file = await readTelemetry(context.nylorunRoot);
|
|
69
|
+
if (action !== "status") {
|
|
70
|
+
const enabled = action === "enable";
|
|
71
|
+
await writeTelemetry(context.nylorunRoot, { ...file, enabled });
|
|
72
|
+
context.out(`Studio telemetry ${enabled ? "enabled" : "disabled"}. Running Tenants pick this up on the next "nylorun start".`);
|
|
73
|
+
}
|
|
74
|
+
const decision = telemetryDecision(context.env, await readTelemetry(context.nylorunRoot));
|
|
75
|
+
context.out(`Studio telemetry is ${decision.enabled ? "on" : "off"} (${decision.reason}).`);
|
|
76
|
+
if (action === "status")
|
|
77
|
+
context.out(TELEMETRY_NOTICE);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nylorun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-beta",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"description": "Set up and run
|
|
6
|
+
"description": "Set up and run local Nylorun Tenants (Runtime and Studio) with Docker Compose",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/nylorun/agents.git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"prepack": "npm run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@nylorun/core": "0.
|
|
33
|
+
"@nylorun/core": "0.11.0-beta"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"nylorun": "dist/cli.js"
|
|
42
42
|
},
|
|
43
43
|
"nylorun": {
|
|
44
|
-
"runtime": "0.
|
|
45
|
-
"studio": "0.
|
|
44
|
+
"runtime": "0.15.1-beta",
|
|
45
|
+
"studio": "0.16.0-beta"
|
|
46
46
|
}
|
|
47
47
|
}
|