@elench/testkit 0.1.135 → 0.1.137
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/README.md +38 -0
- package/lib/cli/commands/local/down.mjs +37 -0
- package/lib/cli/commands/local/env.mjs +31 -0
- package/lib/cli/commands/local/logs.mjs +35 -0
- package/lib/cli/commands/local/shell.mjs +49 -0
- package/lib/cli/commands/local/status.mjs +34 -0
- package/lib/cli/commands/local/up.mjs +39 -0
- package/lib/cli/entrypoint.mjs +6 -0
- package/lib/cli/renderers/status/text.mjs +14 -0
- package/lib/config/index.mjs +154 -0
- package/lib/config/validation.mjs +9 -0
- package/lib/config-api/index.d.ts +53 -0
- package/lib/config-api/index.mjs +14 -0
- package/lib/database/fingerprint.mjs +13 -33
- package/lib/database/index.mjs +27 -12
- package/lib/database/schema-source.mjs +3 -1
- package/lib/docker-compat/matrix.mjs +135 -0
- package/lib/env/index.d.ts +1 -0
- package/lib/env/index.mjs +5 -1
- package/lib/kiln/client.mjs +100 -0
- package/lib/local/kiln-driver.mjs +544 -0
- package/lib/local/lifecycle.mjs +289 -0
- package/lib/local/orchestrator.mjs +343 -0
- package/lib/repo/fingerprint-policy.mjs +145 -0
- package/lib/repo/state.mjs +46 -44
- package/lib/runner/maintenance.mjs +23 -0
- package/lib/runner/processes.mjs +45 -6
- package/lib/runner/readiness.mjs +12 -1
- package/lib/runner/runtime-preparation.mjs +10 -5
- package/lib/runner/services.mjs +24 -18
- package/lib/runner/status-model.mjs +27 -0
- package/lib/runner/template.mjs +39 -1
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/node_modules/es-toolkit/CHANGELOG.md +801 -0
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/package.json +8 -5
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
2
|
import { resolveServiceCwd } from "../config/paths.mjs";
|
|
3
|
+
import {
|
|
4
|
+
appendFingerprintPathToHash,
|
|
5
|
+
normalizeFingerprintPolicy,
|
|
6
|
+
} from "../repo/fingerprint-policy.mjs";
|
|
5
7
|
import { collectTemplateInputs } from "./template-steps.mjs";
|
|
6
8
|
import { appendSourceSchemaCacheToHash } from "./schema-source.mjs";
|
|
7
9
|
|
|
@@ -11,6 +13,7 @@ const LOCAL_USER = "testkit";
|
|
|
11
13
|
export async function computeTemplateFingerprint(config, options = {}) {
|
|
12
14
|
const hash = crypto.createHash("sha256");
|
|
13
15
|
const db = config.testkit.database;
|
|
16
|
+
const fingerprints = normalizeFingerprintPolicy(config.testkit.fingerprints);
|
|
14
17
|
hash.update(JSON.stringify({
|
|
15
18
|
provider: db.provider,
|
|
16
19
|
selectedBackend: db.selectedBackend,
|
|
@@ -20,48 +23,25 @@ export async function computeTemplateFingerprint(config, options = {}) {
|
|
|
20
23
|
}));
|
|
21
24
|
|
|
22
25
|
for (const envFile of config.testkit.envFiles || []) {
|
|
23
|
-
appendFileToHash(hash, config.productDir, resolveServiceCwd(config.productDir, envFile));
|
|
26
|
+
appendFileToHash(hash, config.productDir, resolveServiceCwd(config.productDir, envFile), fingerprints);
|
|
24
27
|
}
|
|
25
28
|
for (const input of collectTemplateInputs(config.productDir, db.template || {})) {
|
|
26
|
-
appendResolvedInputToHash(hash, config.productDir, input);
|
|
29
|
+
appendResolvedInputToHash(hash, config.productDir, input, fingerprints);
|
|
27
30
|
}
|
|
28
31
|
appendSourceSchemaCacheToHash(hash, config, options.sourceSchemaState || null);
|
|
29
32
|
|
|
30
33
|
return hash.digest("hex");
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
export function appendInputToHash(hash, productDir, input) {
|
|
36
|
+
export function appendInputToHash(hash, productDir, input, fingerprints = {}) {
|
|
34
37
|
const absPath = resolveServiceCwd(productDir, input);
|
|
35
|
-
appendResolvedInputToHash(hash, productDir, absPath);
|
|
38
|
+
appendResolvedInputToHash(hash, productDir, absPath, normalizeFingerprintPolicy(fingerprints));
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
function appendResolvedInputToHash(hash, productDir, absPath) {
|
|
39
|
-
|
|
40
|
-
hash.update(`missing:${path.relative(productDir, absPath)}`);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const stat = fs.statSync(absPath);
|
|
45
|
-
if (stat.isDirectory()) {
|
|
46
|
-
hash.update(`dir:${path.relative(productDir, absPath)}`);
|
|
47
|
-
for (const entry of fs.readdirSync(absPath).sort()) {
|
|
48
|
-
if (entry === ".git" || entry === "node_modules" || entry === ".testkit") continue;
|
|
49
|
-
appendResolvedInputToHash(hash, productDir, path.join(absPath, entry));
|
|
50
|
-
}
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
appendFileToHash(hash, productDir, absPath);
|
|
41
|
+
function appendResolvedInputToHash(hash, productDir, absPath, fingerprints) {
|
|
42
|
+
appendFingerprintPathToHash(hash, productDir, absPath, fingerprints);
|
|
55
43
|
}
|
|
56
44
|
|
|
57
|
-
export function appendFileToHash(hash, productDir, absPath) {
|
|
58
|
-
|
|
59
|
-
hash.update(`missing:${path.relative(productDir, absPath)}`);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const stat = fs.statSync(absPath);
|
|
63
|
-
if (!stat.isFile()) return;
|
|
64
|
-
|
|
65
|
-
hash.update(`file:${path.relative(productDir, absPath)}:${stat.size}:${stat.mtimeMs}`);
|
|
66
|
-
hash.update(fs.readFileSync(absPath));
|
|
45
|
+
export function appendFileToHash(hash, productDir, absPath, fingerprints = {}) {
|
|
46
|
+
appendFingerprintPathToHash(hash, productDir, absPath, normalizeFingerprintPolicy(fingerprints));
|
|
67
47
|
}
|
package/lib/database/index.mjs
CHANGED
|
@@ -2,8 +2,6 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { execa } from "execa";
|
|
4
4
|
import {
|
|
5
|
-
appendFileToHash as appendFileToHashModel,
|
|
6
|
-
appendInputToHash as appendInputToHashModel,
|
|
7
5
|
computeTemplateFingerprint as computeTemplateFingerprintModel,
|
|
8
6
|
} from "./fingerprint.mjs";
|
|
9
7
|
import {
|
|
@@ -44,6 +42,8 @@ const LOCAL_PASSWORD = "testkit";
|
|
|
44
42
|
const LOCAL_ADMIN_DB = "postgres";
|
|
45
43
|
const LOCAL_READY_TIMEOUT_MS = 60_000;
|
|
46
44
|
const LOCAL_POLL_INTERVAL_MS = 1_000;
|
|
45
|
+
const LOCAL_ADMIN_QUERY_RETRY_MS = 15_000;
|
|
46
|
+
const LOCAL_ADMIN_QUERY_RETRY_INTERVAL_MS = 250;
|
|
47
47
|
const LOCAL_DROP_DATABASE_TIMEOUT_MS = 15_000;
|
|
48
48
|
const LOCAL_DROP_DATABASE_POLL_INTERVAL_MS = 250;
|
|
49
49
|
|
|
@@ -603,8 +603,31 @@ async function runAdminQuery(infra, args) {
|
|
|
603
603
|
LOCAL_ADMIN_DB,
|
|
604
604
|
...args,
|
|
605
605
|
];
|
|
606
|
-
const
|
|
607
|
-
|
|
606
|
+
const startedAt = Date.now();
|
|
607
|
+
while (true) {
|
|
608
|
+
try {
|
|
609
|
+
const { stdout } = await execa("docker", commandArgs);
|
|
610
|
+
return stdout;
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (
|
|
613
|
+
Date.now() - startedAt >= LOCAL_ADMIN_QUERY_RETRY_MS ||
|
|
614
|
+
!isTransientAdminQueryConnectionError(error)
|
|
615
|
+
) {
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
await sleep(LOCAL_ADMIN_QUERY_RETRY_INTERVAL_MS);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function isTransientAdminQueryConnectionError(error) {
|
|
624
|
+
const text = `${error?.shortMessage || ""}\n${error?.stderr || ""}\n${error?.stdout || ""}\n${error?.message || ""}`;
|
|
625
|
+
return (
|
|
626
|
+
text.includes('connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed') ||
|
|
627
|
+
text.includes("No such file or directory") ||
|
|
628
|
+
text.includes("the database system is starting up") ||
|
|
629
|
+
text.includes("could not connect to server")
|
|
630
|
+
);
|
|
608
631
|
}
|
|
609
632
|
|
|
610
633
|
async function terminateDatabaseConnections(infra, dbName, runAdminQueryFn) {
|
|
@@ -642,14 +665,6 @@ async function computeTemplateFingerprint(config, options = {}) {
|
|
|
642
665
|
return computeTemplateFingerprintModel(config, options);
|
|
643
666
|
}
|
|
644
667
|
|
|
645
|
-
function appendInputToHash(hash, productDir, input) {
|
|
646
|
-
return appendInputToHashModel(hash, productDir, input);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function appendFileToHash(hash, productDir, absPath) {
|
|
650
|
-
return appendFileToHashModel(hash, productDir, absPath);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
668
|
function buildDatabaseUrl(infra, dbName) {
|
|
654
669
|
return buildDatabaseUrlModel(infra, dbName);
|
|
655
670
|
}
|
|
@@ -29,7 +29,9 @@ export function getSourceSchemaMetadataPath(config, options = {}) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function resolveSourceSchemaCacheLocation(config, options = {}) {
|
|
32
|
-
const repoState = options.repoState || collectRepoState(config.productDir
|
|
32
|
+
const repoState = options.repoState || collectRepoState(config.productDir, {
|
|
33
|
+
fingerprints: config.testkit?.fingerprints,
|
|
34
|
+
});
|
|
33
35
|
const rootDir = getSourceSchemaRootDir(config);
|
|
34
36
|
const cacheDir = path.join(rootDir, ...repoState.cacheKey.split("/").map(sanitizePathSegment));
|
|
35
37
|
return {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export const DEFAULT_DOCKER_COMPAT_MATRIX = Object.freeze([
|
|
2
|
+
dockerCompatEntry({ dockerVersion: "24.0.9", image: "docker:24.0.9-dind" }),
|
|
3
|
+
dockerCompatEntry({ dockerVersion: "25.0.5", image: "docker:25.0.5-dind" }),
|
|
4
|
+
dockerCompatEntry({ dockerVersion: "26.1.4", image: "docker:26.1.4-dind" }),
|
|
5
|
+
dockerCompatEntry({ dockerVersion: "27.5.1", image: "docker:27.5.1-dind" }),
|
|
6
|
+
dockerCompatEntry({ dockerVersion: "28.5.2", image: "docker:28.5.2-dind" }),
|
|
7
|
+
dockerCompatEntry({ dockerVersion: "29.0.0", image: "docker:29.0.0-dind" }),
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export function dockerCompatEntry(entry = {}) {
|
|
11
|
+
const dockerVersion = normalizeNonEmptyString(entry.dockerVersion, "dockerVersion");
|
|
12
|
+
const image = normalizeNonEmptyString(entry.image, "image");
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
dockerVersion,
|
|
15
|
+
image,
|
|
16
|
+
label: entry.label || `Docker ${dockerVersion}`,
|
|
17
|
+
rootfsSizeMB: normalizePositiveInteger(entry.rootfsSizeMB ?? 4096, "rootfsSizeMB"),
|
|
18
|
+
vcpus: normalizePositiveInteger(entry.vcpus ?? 1, "vcpus"),
|
|
19
|
+
memoryMB: normalizePositiveInteger(entry.memoryMB ?? 2048, "memoryMB"),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function selectDockerCompatMatrix(matrix = DEFAULT_DOCKER_COMPAT_MATRIX, selection = []) {
|
|
24
|
+
const requested = normalizeSelection(selection);
|
|
25
|
+
if (requested.length === 0) return [...matrix];
|
|
26
|
+
|
|
27
|
+
const byVersion = new Map(matrix.map((entry) => [entry.dockerVersion, entry]));
|
|
28
|
+
const byLabel = new Map(matrix.map((entry) => [entry.label, entry]));
|
|
29
|
+
const selected = [];
|
|
30
|
+
const unknown = [];
|
|
31
|
+
|
|
32
|
+
for (const key of requested) {
|
|
33
|
+
const entry = byVersion.get(key) || byLabel.get(key);
|
|
34
|
+
if (entry) {
|
|
35
|
+
selected.push(entry);
|
|
36
|
+
} else {
|
|
37
|
+
unknown.push(key);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (unknown.length > 0) {
|
|
42
|
+
throw new Error(`Unknown Docker compatibility matrix selection: ${unknown.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
return selected;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseDockerCompatSelection(value) {
|
|
48
|
+
if (Array.isArray(value)) return normalizeSelection(value);
|
|
49
|
+
return normalizeSelection(String(value || "").split(","));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildDockerCompatDockerfile(entry) {
|
|
53
|
+
const normalized = dockerCompatEntry(entry);
|
|
54
|
+
return [
|
|
55
|
+
`FROM ${normalized.image}`,
|
|
56
|
+
"RUN apk add --no-cache postgresql-client",
|
|
57
|
+
"ENV DOCKER_TLS_CERTDIR=",
|
|
58
|
+
"ENTRYPOINT []",
|
|
59
|
+
"CMD [\"dockerd-entrypoint.sh\"]",
|
|
60
|
+
"",
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildDockerCompatProbeCommand(entry, options = {}) {
|
|
65
|
+
const normalized = dockerCompatEntry(entry);
|
|
66
|
+
const postgresImage = options.postgresImage || "pgvector/pgvector:pg16";
|
|
67
|
+
const containerName = options.containerName || `testkit-docker-compat-${normalized.dockerVersion.replaceAll(".", "-")}`;
|
|
68
|
+
return [
|
|
69
|
+
"set -eu",
|
|
70
|
+
"deadline=$(($(date +%s) + 90))",
|
|
71
|
+
"until docker version >/tmp/testkit-docker-version.txt 2>/tmp/testkit-docker-version.err; do",
|
|
72
|
+
" if [ \"$(date +%s)\" -ge \"$deadline\" ]; then",
|
|
73
|
+
" cat /tmp/testkit-docker-version.err >&2 || true",
|
|
74
|
+
" exit 1",
|
|
75
|
+
" fi",
|
|
76
|
+
" sleep 1",
|
|
77
|
+
"done",
|
|
78
|
+
"actual=$(docker version --format '{{.Server.Version}}')",
|
|
79
|
+
`case "$actual" in ${shellCasePattern(normalized.dockerVersion)}*) ;; *) echo "expected Docker ${normalized.dockerVersion}, got $actual" >&2; exit 1 ;; esac`,
|
|
80
|
+
`docker rm -f ${shellQuote(containerName)} >/dev/null 2>&1 || true`,
|
|
81
|
+
[
|
|
82
|
+
"docker run -d",
|
|
83
|
+
"--name",
|
|
84
|
+
shellQuote(containerName),
|
|
85
|
+
"-e",
|
|
86
|
+
"POSTGRES_USER=testkit",
|
|
87
|
+
"-e",
|
|
88
|
+
"POSTGRES_PASSWORD=testkit",
|
|
89
|
+
"-e",
|
|
90
|
+
"POSTGRES_DB=postgres",
|
|
91
|
+
"-p",
|
|
92
|
+
"127.0.0.1::5432",
|
|
93
|
+
shellQuote(postgresImage),
|
|
94
|
+
].join(" "),
|
|
95
|
+
"ready_deadline=$(($(date +%s) + 120))",
|
|
96
|
+
"until docker exec " + shellQuote(containerName) + " pg_isready -U testkit -d postgres >/dev/null 2>&1; do",
|
|
97
|
+
" if [ \"$(date +%s)\" -ge \"$ready_deadline\" ]; then",
|
|
98
|
+
" docker logs " + shellQuote(containerName) + " >&2 || true",
|
|
99
|
+
" docker rm -f " + shellQuote(containerName) + " >/dev/null 2>&1 || true",
|
|
100
|
+
" exit 1",
|
|
101
|
+
" fi",
|
|
102
|
+
" sleep 1",
|
|
103
|
+
"done",
|
|
104
|
+
"published_port=$(docker inspect --format '{{(index (index .NetworkSettings.Ports \"5432/tcp\") 0).HostPort}}' " + shellQuote(containerName) + ")",
|
|
105
|
+
"test -n \"$published_port\"",
|
|
106
|
+
"docker rm -f " + shellQuote(containerName) + " >/dev/null",
|
|
107
|
+
"echo \"Docker $actual compatibility probe passed\"",
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeSelection(selection) {
|
|
112
|
+
return selection.map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeNonEmptyString(value, name) {
|
|
116
|
+
const normalized = String(value || "").trim();
|
|
117
|
+
if (!normalized) throw new Error(`${name} is required`);
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizePositiveInteger(value, name) {
|
|
122
|
+
const parsed = Number(value);
|
|
123
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
124
|
+
throw new Error(`${name} must be a positive integer`);
|
|
125
|
+
}
|
|
126
|
+
return parsed;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function shellQuote(value) {
|
|
130
|
+
return "'" + String(value).replaceAll("'", "'\"'\"'") + "'";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function shellCasePattern(value) {
|
|
134
|
+
return String(value).replaceAll(".", "\\.");
|
|
135
|
+
}
|
package/lib/env/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface PostgresConnectionEnvValue {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export declare function isManagedRuntime(env?: NodeJS.ProcessEnv): boolean;
|
|
33
|
+
export declare function managedRuntimeMode(env?: NodeJS.ProcessEnv): "test" | "local" | string | null;
|
|
33
34
|
export declare function shouldLoadDotenv(env?: NodeJS.ProcessEnv): boolean;
|
|
34
35
|
export declare function loadDotenvFiles(options?: LoadDotenvFilesOptions): {
|
|
35
36
|
loaded: string[];
|
package/lib/env/index.mjs
CHANGED
|
@@ -9,6 +9,10 @@ export function isManagedRuntime(env = process.env) {
|
|
|
9
9
|
return env?.TESTKIT_ACTIVE === "1";
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export function managedRuntimeMode(env = process.env) {
|
|
13
|
+
return isManagedRuntime(env) ? env?.TESTKIT_MODE || "test" : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
export function shouldLoadDotenv(env = process.env) {
|
|
13
17
|
return env?.NODE_ENV !== "production" && !isManagedRuntime(env);
|
|
14
18
|
}
|
|
@@ -59,7 +63,7 @@ export function assertLocalDatabaseUrl(env = process.env, consumer = "This comma
|
|
|
59
63
|
if (!LOCAL_DATABASE_PROTOCOLS.has(parsed.protocol) || !LOCAL_DATABASE_HOSTS.has(parsed.hostname)) {
|
|
60
64
|
const location = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`;
|
|
61
65
|
throw new Error(
|
|
62
|
-
`${consumer}
|
|
66
|
+
`${consumer} managed Testkit runtime requires a local PostgreSQL DATABASE_URL. Refusing ${location}.`
|
|
63
67
|
);
|
|
64
68
|
}
|
|
65
69
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
export class KilnClient {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
const config = readKilnConfig();
|
|
8
|
+
this.apiUrl = String(options.apiUrl || process.env.KILN_API_URL || config.api_url || "").replace(/\/+$/, "");
|
|
9
|
+
this.token = String(options.token || process.env.KILN_TOKEN || config.token || "");
|
|
10
|
+
if (!this.apiUrl) throw new Error("Kiln API URL is not configured; run kiln login or set KILN_API_URL");
|
|
11
|
+
if (!this.token) throw new Error("Kiln token is not configured; run kiln login or set KILN_TOKEN");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async request(method, requestPath, body = null) {
|
|
15
|
+
const response = await fetch(`${this.apiUrl}${requestPath}`, {
|
|
16
|
+
method,
|
|
17
|
+
headers: {
|
|
18
|
+
authorization: `Bearer ${this.token}`,
|
|
19
|
+
...(body ? { "content-type": "application/json" } : {}),
|
|
20
|
+
},
|
|
21
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
22
|
+
});
|
|
23
|
+
const text = await response.text();
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error(`${method} ${requestPath} returned ${response.status}: ${text.trim()}`);
|
|
26
|
+
}
|
|
27
|
+
if (!text || response.status === 204) return null;
|
|
28
|
+
return JSON.parse(text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getVM(ref) {
|
|
32
|
+
return this.request("GET", `/vms/${encodeURIComponent(ref)}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
listVMs() {
|
|
36
|
+
return this.request("GET", "/vms");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
createVM(req) {
|
|
40
|
+
return this.request("POST", "/vms", req);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
startVM(ref) {
|
|
44
|
+
return this.request("POST", `/vms/${encodeURIComponent(ref)}/start`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
stopVM(ref) {
|
|
48
|
+
return this.request("POST", `/vms/${encodeURIComponent(ref)}/stop`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
deleteVM(ref) {
|
|
52
|
+
return this.request("DELETE", `/vms/${encodeURIComponent(ref)}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
attachVMNetwork(ref, networkId) {
|
|
56
|
+
return this.request("POST", `/vms/${encodeURIComponent(ref)}/network`, { network_id: networkId });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
listNetworks() {
|
|
60
|
+
return this.request("GET", "/networks");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
createNetwork(req) {
|
|
64
|
+
return this.request("POST", "/networks", req);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
deleteNetwork(ref) {
|
|
68
|
+
return this.request("DELETE", `/networks/${encodeURIComponent(ref)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
execVM(ref, command) {
|
|
72
|
+
return this.request("POST", `/vms/${encodeURIComponent(ref)}/exec`, { command });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
sshKey(machineId) {
|
|
76
|
+
return this.requestRaw("GET", `/machines/${encodeURIComponent(machineId)}/ssh-key`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async requestRaw(method, requestPath) {
|
|
80
|
+
const response = await fetch(`${this.apiUrl}${requestPath}`, {
|
|
81
|
+
method,
|
|
82
|
+
headers: { authorization: `Bearer ${this.token}` },
|
|
83
|
+
});
|
|
84
|
+
const text = await response.text();
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`${method} ${requestPath} returned ${response.status}: ${text.trim()}`);
|
|
87
|
+
}
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readKilnConfig() {
|
|
93
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
94
|
+
const configPath = path.join(configHome, "kiln", "config.json");
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|