@kubb/studio 5.12.1 → 5.12.3
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 +246 -27
- 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 +246 -27
- package/dist/index.js.map +1 -1
- package/dist/{protocol-CG1bPO_H.d.ts → protocol-WYICLbSg.d.cts} +68 -7
- package/dist/{protocol-CG1bPO_H.d.cts → protocol-WYICLbSg.d.ts} +68 -7
- package/dist/protocol.cjs +17 -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 +17 -1
- 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 { J as SnapshotUploadMetadata, R as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, f as ConfigEditOutcome, j as InstalledExtensions, s as AgentPermission, u as ConfigEdit } from "./protocol-WYICLbSg.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 { J as SnapshotUploadMetadata, R as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, f as ConfigEditOutcome, j as InstalledExtensions, s as AgentPermission, u as ConfigEdit } from "./protocol-WYICLbSg.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,17 +1,17 @@
|
|
|
1
|
-
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
|
|
1
|
+
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, ConfigEditOperation, 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";
|
|
5
|
+
import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
6
|
import { tmpdir } from "node:os";
|
|
5
7
|
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
8
|
import process$1 from "node:process";
|
|
7
9
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
|
-
import {
|
|
9
|
-
import { Readable, Transform } from "node:stream";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
11
|
-
import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { x } from "tinyexec";
|
|
12
11
|
import { Diagnostics, Hookable, cacheStorage, checkoutGitInput, createKubb, fsStorage, getInputKind, isPackageName, isPublicAddress, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
|
|
13
12
|
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
|
|
14
13
|
import { detectCodeFormat, parseExpression, parseModule } from "magicast";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
15
|
import { structuredPatch } from "diff";
|
|
16
16
|
import { stripVTControlCharacters } from "node:util";
|
|
17
17
|
import { ast } from "@kubb/ast";
|
|
@@ -19,6 +19,7 @@ import dns from "node:dns";
|
|
|
19
19
|
import http from "node:http";
|
|
20
20
|
import https from "node:https";
|
|
21
21
|
import net from "node:net";
|
|
22
|
+
import { Readable, Transform } from "node:stream";
|
|
22
23
|
import { ofetch } from "ofetch";
|
|
23
24
|
import { gzipSync } from "node:zlib";
|
|
24
25
|
//#endregion
|
|
@@ -201,7 +202,7 @@ function getElapsedMs(hrStart) {
|
|
|
201
202
|
}
|
|
202
203
|
//#endregion
|
|
203
204
|
//#region package.json
|
|
204
|
-
var version = "5.12.
|
|
205
|
+
var version = "5.12.3";
|
|
205
206
|
//#endregion
|
|
206
207
|
//#region src/resolveConfig.ts
|
|
207
208
|
/**
|
|
@@ -599,13 +600,7 @@ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
|
599
600
|
* line count, so `enable-plugin` restores exactly those lines and not whatever comments follow.
|
|
600
601
|
*/
|
|
601
602
|
const DISABLED_MARKER = /^\/\/ kubb:disabled (.+)\s+(\d+)$/;
|
|
602
|
-
const OPERATIONS =
|
|
603
|
-
"set",
|
|
604
|
-
"remove",
|
|
605
|
-
"add-plugin",
|
|
606
|
-
"disable-plugin",
|
|
607
|
-
"enable-plugin"
|
|
608
|
-
]);
|
|
603
|
+
const OPERATIONS = new Set(Object.values(ConfigEditOperation));
|
|
609
604
|
const KINDS = /* @__PURE__ */ new Set([
|
|
610
605
|
"plugin",
|
|
611
606
|
"adapter",
|
|
@@ -616,13 +611,14 @@ const KINDS = /* @__PURE__ */ new Set([
|
|
|
616
611
|
* The edit crossed the wire, so its declared type is a promise the sender may not have kept.
|
|
617
612
|
*/
|
|
618
613
|
function invalidEdit(edit) {
|
|
619
|
-
const { operation, plugin, config, path, kind } = typeof edit === "object" && edit !== null ? edit : {};
|
|
614
|
+
const { operation, plugin, config, path, kind, input } = typeof edit === "object" && edit !== null ? edit : {};
|
|
620
615
|
if (typeof operation !== "string" || !OPERATIONS.has(operation)) return `unknown operation ${JSON.stringify(operation)}`;
|
|
616
|
+
if (config !== void 0 && typeof config !== "string" && !Number.isSafeInteger(config)) return "config must name an entry or give its index";
|
|
617
|
+
if (operation === ConfigEditOperation.SET_INPUT) return typeof input === "string" && input.trim() ? void 0 : "the edit names no input";
|
|
621
618
|
if (typeof plugin !== "string" || !plugin) return "the edit names no plugin";
|
|
622
619
|
if (kind !== void 0 && (typeof kind !== "string" || !KINDS.has(kind))) return `unknown kind ${JSON.stringify(kind)}`;
|
|
623
|
-
if (kind !== void 0 && operation !==
|
|
624
|
-
if (
|
|
625
|
-
if (operation === "set" || operation === "remove") {
|
|
620
|
+
if (kind !== void 0 && operation !== ConfigEditOperation.SET && operation !== ConfigEditOperation.REMOVE) return `${operation} edits the plugins array, so it takes no kind`;
|
|
621
|
+
if (operation === ConfigEditOperation.SET || operation === ConfigEditOperation.REMOVE) {
|
|
626
622
|
if (!Array.isArray(path) || !path.every((key) => typeof key === "string" && key)) return "path must be a list of option names";
|
|
627
623
|
if (!path.length) return "no option path given";
|
|
628
624
|
const unsafe = path.find((key) => !isSafeKey(key));
|
|
@@ -1063,6 +1059,15 @@ function applySet(source, call, path, value, quote) {
|
|
|
1063
1059
|
}
|
|
1064
1060
|
return { source: splice(source, rangeOf(target.value).start, rangeOf(target.value).end, printValue(value, quote)) };
|
|
1065
1061
|
}
|
|
1062
|
+
/** Writes the config's top-level `input` as a string literal, replacing what it held. */
|
|
1063
|
+
function applySetInput(source, config, input, quote) {
|
|
1064
|
+
const value = printValue(input, quote);
|
|
1065
|
+
const found = findMember(config, "input");
|
|
1066
|
+
if (!found) return { source: insertItem(source, config, `input: ${value}`) };
|
|
1067
|
+
if (found.member.type !== "ObjectProperty") return { reason: "input is not a plain property" };
|
|
1068
|
+
if (isShadowed(config, found.index)) return { reason: "a spread after input could override it" };
|
|
1069
|
+
return { source: splice(source, rangeOf(found.member.value).start, rangeOf(found.member.value).end, value) };
|
|
1070
|
+
}
|
|
1066
1071
|
/**
|
|
1067
1072
|
* Deletes the option at `path`, falling the plugin back to its default for it. An option the file
|
|
1068
1073
|
* does not set already has its default, so removing it changes nothing.
|
|
@@ -1159,7 +1164,8 @@ function applyEdit({ source, edit, quote, extensions }) {
|
|
|
1159
1164
|
const findConfig = (entries) => typeof ref === "string" ? entries.configs.find((entry) => configName(entry) === ref) : entries.configs[ref ?? 0];
|
|
1160
1165
|
const config = findConfig(parsed);
|
|
1161
1166
|
if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
|
|
1162
|
-
if (edit.operation ===
|
|
1167
|
+
if (edit.operation === ConfigEditOperation.SET_INPUT) return applySetInput(source, config, edit.input, quote);
|
|
1168
|
+
if (edit.operation === ConfigEditOperation.DISABLE_PLUGIN) {
|
|
1163
1169
|
const result = disablePlugin(source, parsed.mod, config, edit.plugin);
|
|
1164
1170
|
if (!("reason" in result) || !result.reason.includes("shares a line")) return result;
|
|
1165
1171
|
const expanded = expandPluginsArray(source, config);
|
|
@@ -1167,8 +1173,8 @@ function applyEdit({ source, edit, quote, extensions }) {
|
|
|
1167
1173
|
const reparsedConfig = reparsed && !("reason" in reparsed) ? findConfig(reparsed) : void 0;
|
|
1168
1174
|
return expanded !== void 0 && reparsed && !("reason" in reparsed) && reparsedConfig ? disablePlugin(expanded, reparsed.mod, reparsedConfig, edit.plugin) : result;
|
|
1169
1175
|
}
|
|
1170
|
-
if (edit.operation ===
|
|
1171
|
-
if (edit.operation ===
|
|
1176
|
+
if (edit.operation === ConfigEditOperation.ENABLE_PLUGIN) return enablePlugin(source, config, edit.plugin);
|
|
1177
|
+
if (edit.operation === ConfigEditOperation.ADD_PLUGIN) return applyAddPlugin({
|
|
1172
1178
|
source,
|
|
1173
1179
|
parsed,
|
|
1174
1180
|
config,
|
|
@@ -1184,7 +1190,7 @@ function applyEdit({ source, edit, quote, extensions }) {
|
|
|
1184
1190
|
if (kind === "adapter") return { reason: `${edit.plugin} is not the config's adapter` };
|
|
1185
1191
|
return { reason: kind === "plugin" && disabledBlocks(source, config).some((block) => block.packageName === edit.plugin) ? `${edit.plugin} is disabled, enable it first` : `${edit.plugin} is not in the ${kind}s array` };
|
|
1186
1192
|
}
|
|
1187
|
-
return edit.operation ===
|
|
1193
|
+
return edit.operation === ConfigEditOperation.SET ? applySet(source, call, edit.path, edit.value, quote) : applyRemove(source, call, edit.path);
|
|
1188
1194
|
}
|
|
1189
1195
|
/**
|
|
1190
1196
|
* Applies edits to a `kubb.config.ts` in place. Each edit rewrites only the text it targets, so
|
|
@@ -2013,6 +2019,43 @@ var IncompatibleAgentError = class extends Error {
|
|
|
2013
2019
|
this.name = "IncompatibleAgentError";
|
|
2014
2020
|
}
|
|
2015
2021
|
};
|
|
2022
|
+
const INSTALL_TIMEOUT_MS = 3e5;
|
|
2023
|
+
/** `owner/slug` or `slug`, as `kubb add` takes it. Checked before it becomes an argument. */
|
|
2024
|
+
const REGISTRY_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
|
|
2025
|
+
/** The CLI the project depends on, so `kubb add` installs next to what the config imports. */
|
|
2026
|
+
function findKubbBin(root) {
|
|
2027
|
+
const require = createRequire(path.resolve(root, "package.json"));
|
|
2028
|
+
const names = ["kubb", "@kubb/cli"];
|
|
2029
|
+
return (require.resolve.paths("kubb") ?? []).flatMap((dir) => names.map((name) => path.join(dir, name, "bin", "kubb.js"))).find((bin) => existsSync(bin));
|
|
2030
|
+
}
|
|
2031
|
+
/** Runs a `kubb` command without a shell, and keeps the end of its output for the failure reason. */
|
|
2032
|
+
async function runKubb({ bin, args, cwd, signal }) {
|
|
2033
|
+
try {
|
|
2034
|
+
const { stdout, stderr, exitCode } = await x(process$1.execPath, [bin, ...args], {
|
|
2035
|
+
signal,
|
|
2036
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
2037
|
+
throwOnError: false,
|
|
2038
|
+
nodeOptions: {
|
|
2039
|
+
cwd,
|
|
2040
|
+
env: {
|
|
2041
|
+
...process$1.env,
|
|
2042
|
+
NO_COLOR: "1",
|
|
2043
|
+
FORCE_COLOR: "0"
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
const output = `${stdout}\n${stderr}`.trim().split("\n").slice(-20).join("\n");
|
|
2048
|
+
return {
|
|
2049
|
+
failed: exitCode !== 0,
|
|
2050
|
+
output: output || `kubb ${args[0]} exited with ${exitCode ?? "a signal"}`
|
|
2051
|
+
};
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
return {
|
|
2054
|
+
failed: true,
|
|
2055
|
+
output: `kubb ${args[0]} did not run: ${getErrorMessage(error)}`
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2016
2059
|
function emit(hooks, name, ...args) {
|
|
2017
2060
|
return Promise.resolve(hooks.callHook(name, ...args)).catch(() => {});
|
|
2018
2061
|
}
|
|
@@ -2083,6 +2126,9 @@ var Agent = class extends RpcTarget {
|
|
|
2083
2126
|
...permissions,
|
|
2084
2127
|
allowWrite: false,
|
|
2085
2128
|
allowConfigEdit: false,
|
|
2129
|
+
allowInstall: false,
|
|
2130
|
+
allowUpdate: false,
|
|
2131
|
+
allowSaveInput: false,
|
|
2086
2132
|
allowRead: true,
|
|
2087
2133
|
allowReadAst: true
|
|
2088
2134
|
} : permissions;
|
|
@@ -2108,13 +2154,13 @@ var Agent = class extends RpcTarget {
|
|
|
2108
2154
|
permission
|
|
2109
2155
|
});
|
|
2110
2156
|
}
|
|
2111
|
-
async #refuse({ message, permission }) {
|
|
2157
|
+
async #refuse({ message, permission, code = Diagnostics.code.unknown }) {
|
|
2112
2158
|
await this.#warn({
|
|
2113
2159
|
message,
|
|
2114
2160
|
permission
|
|
2115
2161
|
});
|
|
2116
2162
|
throw new Diagnostics.Error({
|
|
2117
|
-
code
|
|
2163
|
+
code,
|
|
2118
2164
|
severity: "error",
|
|
2119
2165
|
message
|
|
2120
2166
|
});
|
|
@@ -2234,6 +2280,8 @@ var Agent = class extends RpcTarget {
|
|
|
2234
2280
|
message: "Connection is closed"
|
|
2235
2281
|
});
|
|
2236
2282
|
if (this.#session.active) return this.#refuse({ message: AgentMessage.GENERATION_IN_PROGRESS });
|
|
2283
|
+
if (this.#session.install) return this.#refuse({ message: AgentMessage.INSTALL_IN_PROGRESS });
|
|
2284
|
+
if (this.#session.saving) return this.#refuse({ message: AgentMessage.SAVE_IN_PROGRESS });
|
|
2237
2285
|
const settled = Promise.withResolvers();
|
|
2238
2286
|
this.#session.active = {
|
|
2239
2287
|
jobId: input.jobId,
|
|
@@ -2304,7 +2352,13 @@ var Agent = class extends RpcTarget {
|
|
|
2304
2352
|
});
|
|
2305
2353
|
const activePlugins = plugins ?? config.plugins;
|
|
2306
2354
|
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" });
|
|
2307
|
-
const checkout = this.#managed && studioInput !== void 0 && getInputKind(studioInput) === "git" ? await checkoutGitInput(studioInput, signal, {
|
|
2355
|
+
const checkout = this.#managed && studioInput !== void 0 && getInputKind(studioInput) === "git" ? await checkoutGitInput(studioInput, signal, {
|
|
2356
|
+
publicOnly: true,
|
|
2357
|
+
authorization: input.gitAuth?.authorization
|
|
2358
|
+
}).catch((error) => this.#refuse({
|
|
2359
|
+
message: getErrorMessage(error),
|
|
2360
|
+
code: Diagnostics.code.gitInputFailed
|
|
2361
|
+
})) : void 0;
|
|
2308
2362
|
const run = () => generate({
|
|
2309
2363
|
config: {
|
|
2310
2364
|
...config,
|
|
@@ -2374,13 +2428,33 @@ var Agent = class extends RpcTarget {
|
|
|
2374
2428
|
await this.#warn({ message: "Ignored save: editing kubb.config.ts was not granted" });
|
|
2375
2429
|
return refuse("the agent was not granted permission to edit kubb.config.ts");
|
|
2376
2430
|
}
|
|
2377
|
-
|
|
2431
|
+
const busy = this.#busy();
|
|
2432
|
+
if (busy) return refuse(busy);
|
|
2433
|
+
this.#session.saving = true;
|
|
2378
2434
|
try {
|
|
2379
|
-
const
|
|
2435
|
+
const kept = [];
|
|
2436
|
+
for (const edit of edits) kept.push(await this.#keepInput(edit));
|
|
2437
|
+
const written = await writeConfigEdits({
|
|
2380
2438
|
filePath: configFile,
|
|
2381
|
-
edits,
|
|
2439
|
+
edits: kept.flatMap((edit) => "reason" in edit ? [] : [edit]),
|
|
2382
2440
|
extensions: await this.#installedExtensions()
|
|
2383
2441
|
});
|
|
2442
|
+
let next = 0;
|
|
2443
|
+
const outcomes = kept.map((edit, index) => "reason" in edit ? {
|
|
2444
|
+
edit: edits[index],
|
|
2445
|
+
applied: false,
|
|
2446
|
+
reason: edit.reason
|
|
2447
|
+
} : {
|
|
2448
|
+
...written.outcomes[next++],
|
|
2449
|
+
edit: edits[index]
|
|
2450
|
+
});
|
|
2451
|
+
let wroteDocument = false;
|
|
2452
|
+
for (const [index, edit] of kept.entries()) {
|
|
2453
|
+
if (!("document" in edit) || !edit.document || !outcomes[index].applied) continue;
|
|
2454
|
+
await writeFile(path.join(this.#options.root, edit.document.fileName), edit.document.text, "utf-8");
|
|
2455
|
+
wroteDocument = true;
|
|
2456
|
+
}
|
|
2457
|
+
const changed = written.changed || wroteDocument;
|
|
2384
2458
|
const applied = outcomes.filter((outcome) => outcome.applied).length;
|
|
2385
2459
|
await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`, input.requestedBy);
|
|
2386
2460
|
return {
|
|
@@ -2390,6 +2464,137 @@ var Agent = class extends RpcTarget {
|
|
|
2390
2464
|
} catch (error) {
|
|
2391
2465
|
await emit(this.#options.hooks, "studio:error", { error: toError(error) });
|
|
2392
2466
|
return refuse(getErrorMessage(error));
|
|
2467
|
+
} finally {
|
|
2468
|
+
this.#session.saving = false;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Readies a `set-input` edit for the config file: a document is pointed at `openapi.yaml` (`openapi.json`
|
|
2473
|
+
* for JSON) in the project, one file per config entry, for `saveConfig` to write once the edit applies.
|
|
2474
|
+
* A managed agent refuses a file path, as in a generation.
|
|
2475
|
+
*/
|
|
2476
|
+
async #keepInput(edit) {
|
|
2477
|
+
if (edit?.operation !== ConfigEditOperation.SET_INPUT || typeof edit.input !== "string" || !edit.input.trim()) return edit;
|
|
2478
|
+
if (!this.#permissions.allowSaveInput) {
|
|
2479
|
+
await this.#warn({
|
|
2480
|
+
message: "Ignored the spec to save: saving the input was not granted",
|
|
2481
|
+
permission: AgentPermission.SAVE_INPUT
|
|
2482
|
+
});
|
|
2483
|
+
return { reason: "the agent was not granted permission to save the input" };
|
|
2484
|
+
}
|
|
2485
|
+
const kind = getInputKind(edit.input);
|
|
2486
|
+
if (this.#managed && !MANAGED_INPUT_KINDS.has(kind)) return { reason: AgentMessage.MANAGED_INPUT_KIND };
|
|
2487
|
+
if (kind !== "inline") return edit;
|
|
2488
|
+
const fileName = `openapi${edit.config === void 0 || edit.config === 0 ? "" : `-${String(edit.config).replace(/[^\w.-]/g, "-")}`}.${isJson(edit.input) ? "json" : "yaml"}`;
|
|
2489
|
+
return {
|
|
2490
|
+
...edit,
|
|
2491
|
+
input: `./${fileName}`,
|
|
2492
|
+
document: {
|
|
2493
|
+
fileName,
|
|
2494
|
+
text: edit.input
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Installs a registry extension into the project with `kubb add <id> --yes`, which also adds a
|
|
2500
|
+
* plugin to `kubb.config.ts`. Studio calls `connect()` again afterwards to see the result.
|
|
2501
|
+
*/
|
|
2502
|
+
async installExtension(input) {
|
|
2503
|
+
this.#assertConnected();
|
|
2504
|
+
const { root, configFile, kubbBin = findKubbBin(root) } = this.#options;
|
|
2505
|
+
const refuse = async (reason, permission) => {
|
|
2506
|
+
await this.#warn({
|
|
2507
|
+
message: `Ignored install: ${reason}`,
|
|
2508
|
+
permission
|
|
2509
|
+
});
|
|
2510
|
+
return {
|
|
2511
|
+
installed: false,
|
|
2512
|
+
reason
|
|
2513
|
+
};
|
|
2514
|
+
};
|
|
2515
|
+
if (typeof input?.id !== "string" || !REGISTRY_ID.test(input.id)) return refuse("the message carried no registry id");
|
|
2516
|
+
if (!this.#permissions.allowInstall) return refuse("installing extensions was not granted", AgentPermission.INSTALL);
|
|
2517
|
+
if (!this.#permissions.allowConfigEdit) return refuse("editing kubb.config.ts was not granted", AgentPermission.CONFIG_EDIT);
|
|
2518
|
+
const busy = this.#busy();
|
|
2519
|
+
if (busy) return refuse(busy);
|
|
2520
|
+
if (!kubbBin) return refuse("kubb is not installed in the project");
|
|
2521
|
+
const { failed, output } = await this.#runKubb({
|
|
2522
|
+
bin: kubbBin,
|
|
2523
|
+
args: [
|
|
2524
|
+
"add",
|
|
2525
|
+
input.id,
|
|
2526
|
+
"--yes",
|
|
2527
|
+
"--config",
|
|
2528
|
+
configFile
|
|
2529
|
+
]
|
|
2530
|
+
});
|
|
2531
|
+
await this.#logCommand("installExtension", `${failed ? "failed to install" : "installed"} ${input.id}`, input.requestedBy, failed);
|
|
2532
|
+
return failed ? {
|
|
2533
|
+
installed: false,
|
|
2534
|
+
reason: output
|
|
2535
|
+
} : { installed: true };
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Moves installed extensions to the registry's version with `kubb update [ids] --yes`, which
|
|
2539
|
+
* installs packages and leaves `kubb.config.ts` alone. Studio calls `connect()` again afterwards.
|
|
2540
|
+
*/
|
|
2541
|
+
async updateExtensions(input) {
|
|
2542
|
+
this.#assertConnected();
|
|
2543
|
+
const { root, kubbBin = findKubbBin(root) } = this.#options;
|
|
2544
|
+
const refuse = async (reason, permission) => {
|
|
2545
|
+
await this.#warn({
|
|
2546
|
+
message: `Ignored update: ${reason}`,
|
|
2547
|
+
permission
|
|
2548
|
+
});
|
|
2549
|
+
return {
|
|
2550
|
+
updated: false,
|
|
2551
|
+
reason
|
|
2552
|
+
};
|
|
2553
|
+
};
|
|
2554
|
+
const ids = input?.ids ?? [];
|
|
2555
|
+
if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && REGISTRY_ID.test(id))) return refuse("the message carried an invalid registry id");
|
|
2556
|
+
if (!this.#permissions.allowUpdate) return refuse("updating extensions was not granted", AgentPermission.UPDATE);
|
|
2557
|
+
const busy = this.#busy();
|
|
2558
|
+
if (busy) return refuse(busy);
|
|
2559
|
+
if (!kubbBin) return refuse("kubb is not installed in the project");
|
|
2560
|
+
const { failed, output } = await this.#runKubb({
|
|
2561
|
+
bin: kubbBin,
|
|
2562
|
+
args: [
|
|
2563
|
+
"update",
|
|
2564
|
+
...ids,
|
|
2565
|
+
"--yes"
|
|
2566
|
+
]
|
|
2567
|
+
});
|
|
2568
|
+
await this.#logCommand("updateExtensions", `${failed ? "failed to update" : "updated"} ${ids.join(", ") || "all extensions"}`, input?.requestedBy, failed);
|
|
2569
|
+
return failed ? {
|
|
2570
|
+
updated: false,
|
|
2571
|
+
reason: output
|
|
2572
|
+
} : { updated: true };
|
|
2573
|
+
}
|
|
2574
|
+
/** Why a command that touches the project cannot start right now. */
|
|
2575
|
+
#busy() {
|
|
2576
|
+
if (this.#session.active) return "a generation is in progress";
|
|
2577
|
+
if (this.#session.install) return "an install or update is in progress";
|
|
2578
|
+
if (this.#session.saving) return "a save is in progress";
|
|
2579
|
+
}
|
|
2580
|
+
/** Runs `kubb` in the project while holding off saves and generations, and ends it when the socket closes. */
|
|
2581
|
+
async #runKubb({ bin, args }) {
|
|
2582
|
+
const controller = new AbortController();
|
|
2583
|
+
const settled = Promise.withResolvers();
|
|
2584
|
+
this.#session.install = {
|
|
2585
|
+
abort: () => controller.abort(),
|
|
2586
|
+
settled: settled.promise
|
|
2587
|
+
};
|
|
2588
|
+
try {
|
|
2589
|
+
return await runKubb({
|
|
2590
|
+
bin,
|
|
2591
|
+
args,
|
|
2592
|
+
cwd: this.#options.root,
|
|
2593
|
+
signal: controller.signal
|
|
2594
|
+
});
|
|
2595
|
+
} finally {
|
|
2596
|
+
this.#session.install = void 0;
|
|
2597
|
+
settled.resolve();
|
|
2393
2598
|
}
|
|
2394
2599
|
}
|
|
2395
2600
|
/**
|
|
@@ -2574,6 +2779,8 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2574
2779
|
await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
|
|
2575
2780
|
}
|
|
2576
2781
|
await active?.settled;
|
|
2782
|
+
session.install?.abort();
|
|
2783
|
+
await session.install?.settled;
|
|
2577
2784
|
await agent.clear();
|
|
2578
2785
|
return {
|
|
2579
2786
|
...close,
|
|
@@ -2589,6 +2796,15 @@ function pickConfig(configs, ref) {
|
|
|
2589
2796
|
if (ref !== void 0 && !Number.isSafeInteger(ref)) return void 0;
|
|
2590
2797
|
return configs[ref ?? 0];
|
|
2591
2798
|
}
|
|
2799
|
+
/** Whether `text` parses as JSON, so a pasted document is saved under the matching extension. */
|
|
2800
|
+
function isJson(text) {
|
|
2801
|
+
try {
|
|
2802
|
+
JSON.parse(text);
|
|
2803
|
+
return true;
|
|
2804
|
+
} catch {
|
|
2805
|
+
return false;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2592
2808
|
/** The input path or URL Studio may show: no inline document, and no credentials from a URL. */
|
|
2593
2809
|
function reportedInput(input) {
|
|
2594
2810
|
if (typeof input !== "string") return void 0;
|
|
@@ -2642,6 +2858,9 @@ async function runAgent({ options, openRpc }) {
|
|
|
2642
2858
|
permissions: {
|
|
2643
2859
|
allowWrite: false,
|
|
2644
2860
|
allowConfigEdit: false,
|
|
2861
|
+
allowInstall: false,
|
|
2862
|
+
allowUpdate: false,
|
|
2863
|
+
allowSaveInput: false,
|
|
2645
2864
|
allowExec: false,
|
|
2646
2865
|
allowRead: false,
|
|
2647
2866
|
allowReadAst: false,
|