@kubb/studio 5.12.0 → 5.12.2
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/dist/index.cjs +283 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +279 -11
- package/dist/index.js.map +1 -1
- package/dist/{protocol-Dn1jdnFB.d.ts → protocol-DhNrwhqp.d.cts} +43 -4
- package/dist/{protocol-Dn1jdnFB.d.cts → protocol-DhNrwhqp.d.ts} +43 -4
- package/dist/protocol.cjs +5 -0
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +2 -2
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +5 -0
- package/dist/protocol.js.map +1 -1
- package/package.json +5 -4
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as InstalledExtensions, L as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, q as SnapshotUploadMetadata, s as AgentPermission, u as ConfigEdit } from "./protocol-DhNrwhqp.cjs";
|
|
2
2
|
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
3
3
|
//#region src/connect.d.ts
|
|
4
4
|
/**
|
|
@@ -45,7 +45,7 @@ declare global {
|
|
|
45
45
|
* A finished command and what it did, such as `applied 2/3 edits to kubb.config.ts`.
|
|
46
46
|
*/
|
|
47
47
|
'studio:command': [ctx: {
|
|
48
|
-
command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput' | 'readDiff';
|
|
48
|
+
command: 'generate' | 'saveConfig' | 'installExtension' | 'updateExtensions' | 'readFiles' | 'readInput' | 'readDiff';
|
|
49
49
|
info: string;
|
|
50
50
|
/**
|
|
51
51
|
* The Studio user who asked for the command, when Studio names one.
|
|
@@ -97,6 +97,8 @@ type ConnectAgentOptions = {
|
|
|
97
97
|
* What Studio may do in this project. Every permission is off unless granted.
|
|
98
98
|
*/
|
|
99
99
|
permissions?: Partial<AgentPermissions>;
|
|
100
|
+
/** The CLI entry `installExtension` runs `kubb add` with. Found through the project's `kubb` or `@kubb/cli` dependency when omitted. */
|
|
101
|
+
kubbBin?: string;
|
|
100
102
|
/**
|
|
101
103
|
* Receives the `studio:*` hooks and the `kubb:*` hooks of every generation.
|
|
102
104
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as InstalledExtensions, L as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, q as SnapshotUploadMetadata, s as AgentPermission, u as ConfigEdit } from "./protocol-DhNrwhqp.js";
|
|
2
2
|
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
3
3
|
//#region src/connect.d.ts
|
|
4
4
|
/**
|
|
@@ -45,7 +45,7 @@ declare global {
|
|
|
45
45
|
* A finished command and what it did, such as `applied 2/3 edits to kubb.config.ts`.
|
|
46
46
|
*/
|
|
47
47
|
'studio:command': [ctx: {
|
|
48
|
-
command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput' | 'readDiff';
|
|
48
|
+
command: 'generate' | 'saveConfig' | 'installExtension' | 'updateExtensions' | 'readFiles' | 'readInput' | 'readDiff';
|
|
49
49
|
info: string;
|
|
50
50
|
/**
|
|
51
51
|
* The Studio user who asked for the command, when Studio names one.
|
|
@@ -97,6 +97,8 @@ type ConnectAgentOptions = {
|
|
|
97
97
|
* What Studio may do in this project. Every permission is off unless granted.
|
|
98
98
|
*/
|
|
99
99
|
permissions?: Partial<AgentPermissions>;
|
|
100
|
+
/** The CLI entry `installExtension` runs `kubb add` with. Found through the project's `kubb` or `@kubb/cli` dependency when omitted. */
|
|
101
|
+
kubbBin?: string;
|
|
100
102
|
/**
|
|
101
103
|
* Receives the `studio:*` hooks and the `kubb:*` hooks of every generation.
|
|
102
104
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { createHash, hash, randomUUID } from "node:crypto";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
7
|
import process$1 from "node:process";
|
|
7
8
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
|
-
import {
|
|
9
|
-
import { existsSync } from "node:fs";
|
|
9
|
+
import { x } from "tinyexec";
|
|
10
10
|
import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
|
-
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, isPackageName, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
|
|
11
|
+
import { Diagnostics, Hookable, cacheStorage, checkoutGitInput, createKubb, fsStorage, getInputKind, isPackageName, isPublicAddress, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
|
|
12
12
|
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
|
|
13
13
|
import { detectCodeFormat, parseExpression, parseModule } from "magicast";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
14
15
|
import { structuredPatch } from "diff";
|
|
15
16
|
import { stripVTControlCharacters } from "node:util";
|
|
16
17
|
import { ast } from "@kubb/ast";
|
|
18
|
+
import dns from "node:dns";
|
|
19
|
+
import http from "node:http";
|
|
20
|
+
import https from "node:https";
|
|
21
|
+
import net from "node:net";
|
|
22
|
+
import { Readable, Transform } from "node:stream";
|
|
17
23
|
import { ofetch } from "ofetch";
|
|
18
24
|
import { gzipSync } from "node:zlib";
|
|
19
25
|
//#endregion
|
|
@@ -196,7 +202,7 @@ function getElapsedMs(hrStart) {
|
|
|
196
202
|
}
|
|
197
203
|
//#endregion
|
|
198
204
|
//#region package.json
|
|
199
|
-
var version = "5.12.
|
|
205
|
+
var version = "5.12.2";
|
|
200
206
|
//#endregion
|
|
201
207
|
//#region src/resolveConfig.ts
|
|
202
208
|
/**
|
|
@@ -1842,6 +1848,106 @@ function createGenerationStream(hooks, jobId) {
|
|
|
1842
1848
|
};
|
|
1843
1849
|
}
|
|
1844
1850
|
//#endregion
|
|
1851
|
+
//#region src/publicFetch.ts
|
|
1852
|
+
const MAX_REDIRECTS = 5;
|
|
1853
|
+
const MAX_BYTES = 52428800;
|
|
1854
|
+
const REDIRECTS = /* @__PURE__ */ new Set([
|
|
1855
|
+
301,
|
|
1856
|
+
302,
|
|
1857
|
+
303,
|
|
1858
|
+
307,
|
|
1859
|
+
308
|
|
1860
|
+
]);
|
|
1861
|
+
const NO_BODY = /* @__PURE__ */ new Set([
|
|
1862
|
+
204,
|
|
1863
|
+
205,
|
|
1864
|
+
304
|
|
1865
|
+
]);
|
|
1866
|
+
function notPublic(host) {
|
|
1867
|
+
return /* @__PURE__ */ new Error(`${host} is not a public address, so a managed agent does not fetch from it`);
|
|
1868
|
+
}
|
|
1869
|
+
function createLookup(blocked) {
|
|
1870
|
+
return (hostname, options, callback) => {
|
|
1871
|
+
dns.lookup(hostname, {
|
|
1872
|
+
...options,
|
|
1873
|
+
all: true
|
|
1874
|
+
}, (error, addresses) => {
|
|
1875
|
+
if (error) return callback(error, "", 0);
|
|
1876
|
+
const found = addresses;
|
|
1877
|
+
if (!found.length || found.some(({ address }) => !isPublicAddress(address, blocked))) return callback(notPublic(hostname), "", 0);
|
|
1878
|
+
return options.all ? callback(null, found) : callback(null, found[0].address, found[0].family);
|
|
1879
|
+
});
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
function request({ url, blocked, signal }) {
|
|
1883
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return Promise.reject(/* @__PURE__ */ new Error(`Only http and https URLs are fetched, not ${url.protocol}`));
|
|
1884
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
1885
|
+
if (net.isIP(host) && !isPublicAddress(host, blocked)) return Promise.reject(notPublic(host));
|
|
1886
|
+
return new Promise((resolve, reject) => {
|
|
1887
|
+
const req = (url.protocol === "https:" ? https.request : http.request)(url, {
|
|
1888
|
+
method: "GET",
|
|
1889
|
+
lookup: createLookup(blocked),
|
|
1890
|
+
signal: signal ?? void 0,
|
|
1891
|
+
headers: { "accept-encoding": "identity" }
|
|
1892
|
+
}, (res) => {
|
|
1893
|
+
const headers = new Headers();
|
|
1894
|
+
for (const [key, value] of Object.entries(res.headers)) for (const item of [value].flat()) if (item !== void 0) headers.append(key, item);
|
|
1895
|
+
let total = 0;
|
|
1896
|
+
const limit = new Transform({ transform(chunk, _encoding, done) {
|
|
1897
|
+
total += chunk.length;
|
|
1898
|
+
done(total > MAX_BYTES ? /* @__PURE__ */ new Error("The response is larger than 50 MB") : null, chunk);
|
|
1899
|
+
} });
|
|
1900
|
+
res.on("error", (error) => limit.destroy(error));
|
|
1901
|
+
res.pipe(limit);
|
|
1902
|
+
const body = NO_BODY.has(res.statusCode ?? 0) ? null : Readable.toWeb(limit);
|
|
1903
|
+
resolve(new Response(body, {
|
|
1904
|
+
status: res.statusCode,
|
|
1905
|
+
statusText: res.statusMessage,
|
|
1906
|
+
headers
|
|
1907
|
+
}));
|
|
1908
|
+
});
|
|
1909
|
+
req.on("error", reject);
|
|
1910
|
+
req.end();
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Builds a `fetch` that only reaches addresses `blocked` does not list. It follows redirects itself
|
|
1915
|
+
* and checks every hop, and takes GET requests only, since that is all a spec load needs.
|
|
1916
|
+
*/
|
|
1917
|
+
function createPublicFetch(blocked) {
|
|
1918
|
+
const publicFetch = async (input, init) => {
|
|
1919
|
+
if (typeof input !== "string" && !(input instanceof URL)) throw new TypeError("A managed agent fetches by URL only");
|
|
1920
|
+
if (init?.method && init.method.toUpperCase() !== "GET") throw new TypeError("A managed agent only sends GET requests");
|
|
1921
|
+
let url = new URL(input);
|
|
1922
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
1923
|
+
const response = await request({
|
|
1924
|
+
url,
|
|
1925
|
+
blocked,
|
|
1926
|
+
signal: init?.signal
|
|
1927
|
+
});
|
|
1928
|
+
const location = response.headers.get("location");
|
|
1929
|
+
if (!REDIRECTS.has(response.status) || !location) return response;
|
|
1930
|
+
await response.body?.cancel();
|
|
1931
|
+
url = new URL(location, url);
|
|
1932
|
+
}
|
|
1933
|
+
throw new Error(`Stopped after ${MAX_REDIRECTS} redirects from ${String(input)}`);
|
|
1934
|
+
};
|
|
1935
|
+
return publicFetch;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Runs `run` with the global `fetch` limited to public addresses, then puts the original back.
|
|
1939
|
+
* A managed agent runs one job at a time, so nothing else shares the window.
|
|
1940
|
+
*/
|
|
1941
|
+
async function withPublicFetch(run) {
|
|
1942
|
+
const original = globalThis.fetch;
|
|
1943
|
+
globalThis.fetch = createPublicFetch();
|
|
1944
|
+
try {
|
|
1945
|
+
return await run();
|
|
1946
|
+
} finally {
|
|
1947
|
+
globalThis.fetch = original;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
//#endregion
|
|
1845
1951
|
//#region src/url.ts
|
|
1846
1952
|
/**
|
|
1847
1953
|
* Hosted Kubb Studio URL, used when a host passes none.
|
|
@@ -1878,6 +1984,11 @@ const OPEN_TIMEOUT_MS = 5e3;
|
|
|
1878
1984
|
const MAX_RETRY_DELAY_MS = 3e4;
|
|
1879
1985
|
const DISK_SNAPSHOT_MAX_FILES = 1e4;
|
|
1880
1986
|
const SANDBOX_GENERATION_TTL_MS = 9e5;
|
|
1987
|
+
const MANAGED_INPUT_KINDS = /* @__PURE__ */ new Set([
|
|
1988
|
+
"inline",
|
|
1989
|
+
"url",
|
|
1990
|
+
"git"
|
|
1991
|
+
]);
|
|
1881
1992
|
/** Extensions `kubb` ships with, so they count as installed without being a direct dependency. */
|
|
1882
1993
|
const BUILT_IN_PACKAGES = [
|
|
1883
1994
|
"@kubb/adapter-oas",
|
|
@@ -1903,6 +2014,43 @@ var IncompatibleAgentError = class extends Error {
|
|
|
1903
2014
|
this.name = "IncompatibleAgentError";
|
|
1904
2015
|
}
|
|
1905
2016
|
};
|
|
2017
|
+
const INSTALL_TIMEOUT_MS = 3e5;
|
|
2018
|
+
/** `owner/slug` or `slug`, as `kubb add` takes it. Checked before it becomes an argument. */
|
|
2019
|
+
const REGISTRY_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
|
|
2020
|
+
/** The CLI the project depends on, so `kubb add` installs next to what the config imports. */
|
|
2021
|
+
function findKubbBin(root) {
|
|
2022
|
+
const require = createRequire(path.resolve(root, "package.json"));
|
|
2023
|
+
const names = ["kubb", "@kubb/cli"];
|
|
2024
|
+
return (require.resolve.paths("kubb") ?? []).flatMap((dir) => names.map((name) => path.join(dir, name, "bin", "kubb.js"))).find((bin) => existsSync(bin));
|
|
2025
|
+
}
|
|
2026
|
+
/** Runs a `kubb` command without a shell, and keeps the end of its output for the failure reason. */
|
|
2027
|
+
async function runKubb({ bin, args, cwd, signal }) {
|
|
2028
|
+
try {
|
|
2029
|
+
const { stdout, stderr, exitCode } = await x(process$1.execPath, [bin, ...args], {
|
|
2030
|
+
signal,
|
|
2031
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
2032
|
+
throwOnError: false,
|
|
2033
|
+
nodeOptions: {
|
|
2034
|
+
cwd,
|
|
2035
|
+
env: {
|
|
2036
|
+
...process$1.env,
|
|
2037
|
+
NO_COLOR: "1",
|
|
2038
|
+
FORCE_COLOR: "0"
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
const output = `${stdout}\n${stderr}`.trim().split("\n").slice(-20).join("\n");
|
|
2043
|
+
return {
|
|
2044
|
+
failed: exitCode !== 0,
|
|
2045
|
+
output: output || `kubb ${args[0]} exited with ${exitCode ?? "a signal"}`
|
|
2046
|
+
};
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
return {
|
|
2049
|
+
failed: true,
|
|
2050
|
+
output: `kubb ${args[0]} did not run: ${getErrorMessage(error)}`
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
1906
2054
|
function emit(hooks, name, ...args) {
|
|
1907
2055
|
return Promise.resolve(hooks.callHook(name, ...args)).catch(() => {});
|
|
1908
2056
|
}
|
|
@@ -1964,12 +2112,17 @@ var Agent = class extends RpcTarget {
|
|
|
1964
2112
|
get #sandbox() {
|
|
1965
2113
|
return this.#session.info?.sandbox === true;
|
|
1966
2114
|
}
|
|
2115
|
+
get #managed() {
|
|
2116
|
+
return this.#session.info?.managed === true;
|
|
2117
|
+
}
|
|
1967
2118
|
get #permissions() {
|
|
1968
2119
|
const { permissions } = this.#options;
|
|
1969
2120
|
return this.#sandbox ? {
|
|
1970
2121
|
...permissions,
|
|
1971
2122
|
allowWrite: false,
|
|
1972
2123
|
allowConfigEdit: false,
|
|
2124
|
+
allowInstall: false,
|
|
2125
|
+
allowUpdate: false,
|
|
1973
2126
|
allowRead: true,
|
|
1974
2127
|
allowReadAst: true
|
|
1975
2128
|
} : permissions;
|
|
@@ -2121,6 +2274,8 @@ var Agent = class extends RpcTarget {
|
|
|
2121
2274
|
message: "Connection is closed"
|
|
2122
2275
|
});
|
|
2123
2276
|
if (this.#session.active) return this.#refuse({ message: AgentMessage.GENERATION_IN_PROGRESS });
|
|
2277
|
+
if (this.#session.install) return this.#refuse({ message: AgentMessage.INSTALL_IN_PROGRESS });
|
|
2278
|
+
if (this.#session.saving) return this.#refuse({ message: AgentMessage.SAVE_IN_PROGRESS });
|
|
2124
2279
|
const settled = Promise.withResolvers();
|
|
2125
2280
|
this.#session.active = {
|
|
2126
2281
|
jobId: input.jobId,
|
|
@@ -2159,8 +2314,9 @@ var Agent = class extends RpcTarget {
|
|
|
2159
2314
|
*/
|
|
2160
2315
|
async #generate({ input, root, outputRoot, signal }) {
|
|
2161
2316
|
const { jobId, config: patch } = input;
|
|
2162
|
-
const studioInput = this.#sandbox ? patch?.input : void 0;
|
|
2317
|
+
const studioInput = this.#sandbox || this.#managed ? patch?.input : void 0;
|
|
2163
2318
|
if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: AgentMessage.SANDBOX_INPUT_ONLY });
|
|
2319
|
+
if (this.#managed && studioInput !== void 0 && !MANAGED_INPUT_KINDS.has(getInputKind(studioInput))) return this.#refuse({ message: AgentMessage.MANAGED_INPUT_KIND });
|
|
2164
2320
|
const [configs, extensions] = await Promise.all([this.#options.loadConfigs(), this.#installedExtensions()]);
|
|
2165
2321
|
const config = pickConfig(configs, patch?.config);
|
|
2166
2322
|
if (!config) return this.#refuse({ message: AgentMessage.CONFIG_ENTRY_NOT_FOUND });
|
|
@@ -2189,12 +2345,13 @@ var Agent = class extends RpcTarget {
|
|
|
2189
2345
|
throw error;
|
|
2190
2346
|
});
|
|
2191
2347
|
const activePlugins = plugins ?? config.plugins;
|
|
2192
|
-
if (patch?.input && !this.#sandbox) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent" });
|
|
2193
|
-
const
|
|
2348
|
+
if (patch?.input && !this.#sandbox && !this.#managed) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox or managed agent" });
|
|
2349
|
+
const checkout = this.#managed && studioInput !== void 0 && getInputKind(studioInput) === "git" ? await checkoutGitInput(studioInput, signal, { publicOnly: true }).catch((error) => this.#refuse({ message: getErrorMessage(error) })) : void 0;
|
|
2350
|
+
const run = () => generate({
|
|
2194
2351
|
config: {
|
|
2195
2352
|
...config,
|
|
2196
2353
|
root,
|
|
2197
|
-
input: studioInput ?? config.input,
|
|
2354
|
+
input: checkout?.path ?? studioInput ?? config.input,
|
|
2198
2355
|
storage: outputRoot ? stagingStorage({
|
|
2199
2356
|
root,
|
|
2200
2357
|
directory: outputRoot
|
|
@@ -2206,11 +2363,12 @@ var Agent = class extends RpcTarget {
|
|
|
2206
2363
|
hooks: this.#options.hooks,
|
|
2207
2364
|
signal,
|
|
2208
2365
|
allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
|
|
2209
|
-
})
|
|
2366
|
+
});
|
|
2367
|
+
const files = await (this.#managed ? withPublicFetch(run) : run()).catch(async (error) => {
|
|
2210
2368
|
await this.#generations.drop(jobId);
|
|
2211
2369
|
await this.#logCommand("generate", getErrorMessage(error), input.requestedBy, true);
|
|
2212
2370
|
throw error;
|
|
2213
|
-
});
|
|
2371
|
+
}).finally(() => checkout?.dispose());
|
|
2214
2372
|
const output = await this.#generations.keep({
|
|
2215
2373
|
jobId,
|
|
2216
2374
|
files,
|
|
@@ -2258,7 +2416,9 @@ var Agent = class extends RpcTarget {
|
|
|
2258
2416
|
await this.#warn({ message: "Ignored save: editing kubb.config.ts was not granted" });
|
|
2259
2417
|
return refuse("the agent was not granted permission to edit kubb.config.ts");
|
|
2260
2418
|
}
|
|
2261
|
-
|
|
2419
|
+
const busy = this.#busy();
|
|
2420
|
+
if (busy) return refuse(busy);
|
|
2421
|
+
this.#session.saving = true;
|
|
2262
2422
|
try {
|
|
2263
2423
|
const { outcomes, changed } = await writeConfigEdits({
|
|
2264
2424
|
filePath: configFile,
|
|
@@ -2274,6 +2434,110 @@ var Agent = class extends RpcTarget {
|
|
|
2274
2434
|
} catch (error) {
|
|
2275
2435
|
await emit(this.#options.hooks, "studio:error", { error: toError(error) });
|
|
2276
2436
|
return refuse(getErrorMessage(error));
|
|
2437
|
+
} finally {
|
|
2438
|
+
this.#session.saving = false;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Installs a registry extension into the project with `kubb add <id> --yes`, which also adds a
|
|
2443
|
+
* plugin to `kubb.config.ts`. Studio calls `connect()` again afterwards to see the result.
|
|
2444
|
+
*/
|
|
2445
|
+
async installExtension(input) {
|
|
2446
|
+
this.#assertConnected();
|
|
2447
|
+
const { root, configFile, kubbBin = findKubbBin(root) } = this.#options;
|
|
2448
|
+
const refuse = async (reason, permission) => {
|
|
2449
|
+
await this.#warn({
|
|
2450
|
+
message: `Ignored install: ${reason}`,
|
|
2451
|
+
permission
|
|
2452
|
+
});
|
|
2453
|
+
return {
|
|
2454
|
+
installed: false,
|
|
2455
|
+
reason
|
|
2456
|
+
};
|
|
2457
|
+
};
|
|
2458
|
+
if (typeof input?.id !== "string" || !REGISTRY_ID.test(input.id)) return refuse("the message carried no registry id");
|
|
2459
|
+
if (!this.#permissions.allowInstall) return refuse("installing extensions was not granted", AgentPermission.INSTALL);
|
|
2460
|
+
if (!this.#permissions.allowConfigEdit) return refuse("editing kubb.config.ts was not granted", AgentPermission.CONFIG_EDIT);
|
|
2461
|
+
const busy = this.#busy();
|
|
2462
|
+
if (busy) return refuse(busy);
|
|
2463
|
+
if (!kubbBin) return refuse("kubb is not installed in the project");
|
|
2464
|
+
const { failed, output } = await this.#runKubb({
|
|
2465
|
+
bin: kubbBin,
|
|
2466
|
+
args: [
|
|
2467
|
+
"add",
|
|
2468
|
+
input.id,
|
|
2469
|
+
"--yes",
|
|
2470
|
+
"--config",
|
|
2471
|
+
configFile
|
|
2472
|
+
]
|
|
2473
|
+
});
|
|
2474
|
+
await this.#logCommand("installExtension", `${failed ? "failed to install" : "installed"} ${input.id}`, input.requestedBy, failed);
|
|
2475
|
+
return failed ? {
|
|
2476
|
+
installed: false,
|
|
2477
|
+
reason: output
|
|
2478
|
+
} : { installed: true };
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Moves installed extensions to the registry's version with `kubb update [ids] --yes`, which
|
|
2482
|
+
* installs packages and leaves `kubb.config.ts` alone. Studio calls `connect()` again afterwards.
|
|
2483
|
+
*/
|
|
2484
|
+
async updateExtensions(input) {
|
|
2485
|
+
this.#assertConnected();
|
|
2486
|
+
const { root, kubbBin = findKubbBin(root) } = this.#options;
|
|
2487
|
+
const refuse = async (reason, permission) => {
|
|
2488
|
+
await this.#warn({
|
|
2489
|
+
message: `Ignored update: ${reason}`,
|
|
2490
|
+
permission
|
|
2491
|
+
});
|
|
2492
|
+
return {
|
|
2493
|
+
updated: false,
|
|
2494
|
+
reason
|
|
2495
|
+
};
|
|
2496
|
+
};
|
|
2497
|
+
const ids = input?.ids ?? [];
|
|
2498
|
+
if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && REGISTRY_ID.test(id))) return refuse("the message carried an invalid registry id");
|
|
2499
|
+
if (!this.#permissions.allowUpdate) return refuse("updating extensions was not granted", AgentPermission.UPDATE);
|
|
2500
|
+
const busy = this.#busy();
|
|
2501
|
+
if (busy) return refuse(busy);
|
|
2502
|
+
if (!kubbBin) return refuse("kubb is not installed in the project");
|
|
2503
|
+
const { failed, output } = await this.#runKubb({
|
|
2504
|
+
bin: kubbBin,
|
|
2505
|
+
args: [
|
|
2506
|
+
"update",
|
|
2507
|
+
...ids,
|
|
2508
|
+
"--yes"
|
|
2509
|
+
]
|
|
2510
|
+
});
|
|
2511
|
+
await this.#logCommand("updateExtensions", `${failed ? "failed to update" : "updated"} ${ids.join(", ") || "all extensions"}`, input?.requestedBy, failed);
|
|
2512
|
+
return failed ? {
|
|
2513
|
+
updated: false,
|
|
2514
|
+
reason: output
|
|
2515
|
+
} : { updated: true };
|
|
2516
|
+
}
|
|
2517
|
+
/** Why a command that touches the project cannot start right now. */
|
|
2518
|
+
#busy() {
|
|
2519
|
+
if (this.#session.active) return "a generation is in progress";
|
|
2520
|
+
if (this.#session.install) return "an install or update is in progress";
|
|
2521
|
+
if (this.#session.saving) return "a save is in progress";
|
|
2522
|
+
}
|
|
2523
|
+
/** Runs `kubb` in the project while holding off saves and generations, and ends it when the socket closes. */
|
|
2524
|
+
async #runKubb({ bin, args }) {
|
|
2525
|
+
const controller = new AbortController();
|
|
2526
|
+
const settled = Promise.withResolvers();
|
|
2527
|
+
this.#session.install = {
|
|
2528
|
+
abort: () => controller.abort(),
|
|
2529
|
+
settled: settled.promise
|
|
2530
|
+
};
|
|
2531
|
+
try {
|
|
2532
|
+
return await runKubb({
|
|
2533
|
+
bin,
|
|
2534
|
+
args,
|
|
2535
|
+
cwd: this.#options.root,
|
|
2536
|
+
signal: controller.signal
|
|
2537
|
+
});
|
|
2538
|
+
} finally {
|
|
2539
|
+
this.#session.install = void 0;
|
|
2540
|
+
settled.resolve();
|
|
2277
2541
|
}
|
|
2278
2542
|
}
|
|
2279
2543
|
/**
|
|
@@ -2458,6 +2722,8 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2458
2722
|
await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
|
|
2459
2723
|
}
|
|
2460
2724
|
await active?.settled;
|
|
2725
|
+
session.install?.abort();
|
|
2726
|
+
await session.install?.settled;
|
|
2461
2727
|
await agent.clear();
|
|
2462
2728
|
return {
|
|
2463
2729
|
...close,
|
|
@@ -2526,6 +2792,8 @@ async function runAgent({ options, openRpc }) {
|
|
|
2526
2792
|
permissions: {
|
|
2527
2793
|
allowWrite: false,
|
|
2528
2794
|
allowConfigEdit: false,
|
|
2795
|
+
allowInstall: false,
|
|
2796
|
+
allowUpdate: false,
|
|
2529
2797
|
allowExec: false,
|
|
2530
2798
|
allowRead: false,
|
|
2531
2799
|
allowReadAst: false,
|