@kubb/studio 5.10.2 → 5.10.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.
@@ -0,0 +1,260 @@
1
+ import { D as InstalledExtensions, P as PublishedSnapshot, W as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-CrMJymjT.cjs";
2
+ import { Config, Hookable, KubbHooks } from "@kubb/core";
3
+ //#region src/connect.d.ts
4
+ /**
5
+ * Studio closed with {@link AgentCloseCode.UNAUTHORIZED}. Hosts forget the token and pair again.
6
+ */
7
+ export declare class InvalidAgentTokenError extends Error {
8
+ constructor(studioUrl: string);
9
+ }
10
+ /**
11
+ * Studio closed with {@link AgentCloseCode.INCOMPATIBLE}. Only an upgrade helps.
12
+ */
13
+ export declare class IncompatibleAgentError extends Error {
14
+ constructor({ studioUrl, detail }: {
15
+ studioUrl: string;
16
+ detail?: string;
17
+ });
18
+ }
19
+ type StudioConnectedContext = AgentConnectInfo & {
20
+ url: string;
21
+ /**
22
+ * The Studio, runtime, and host versions, so a host can show a mismatch.
23
+ */
24
+ versions: {
25
+ studio: string;
26
+ kubb: string;
27
+ agent: string;
28
+ };
29
+ };
30
+ declare global {
31
+ namespace Kubb {
32
+ interface KubbHooksRegistry {
33
+ /**
34
+ * Fired after each handshake, including reconnects.
35
+ */
36
+ 'studio:connected': [ctx: StudioConnectedContext];
37
+ 'studio:disconnected': [ctx: {
38
+ code: number;
39
+ reason: string;
40
+ }];
41
+ 'studio:reconnecting': [ctx: {
42
+ delayMs: number;
43
+ }];
44
+ /**
45
+ * A finished command and what it did, such as `applied 2/3 edits to kubb.config.ts`.
46
+ */
47
+ 'studio:command': [ctx: {
48
+ command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput' | 'readDiff';
49
+ info: string;
50
+ }];
51
+ /**
52
+ * A refused or ignored request, with the missing permission so the host can add a remedy.
53
+ */
54
+ 'studio:warn': [ctx: {
55
+ message: string;
56
+ permission?: AgentPermission;
57
+ }];
58
+ 'studio:error': [ctx: {
59
+ error: Error;
60
+ }];
61
+ }
62
+ }
63
+ }
64
+ type ConnectAgentOptions = {
65
+ /**
66
+ * The agent's bearer token, from {@link pairAgent}.
67
+ */
68
+ token: string;
69
+ studioUrl?: string;
70
+ /**
71
+ * The config path shown in Studio, relative to `root` unless absolute.
72
+ */
73
+ configPath: string;
74
+ /**
75
+ * Called for every handshake and every generation, so edits on disk show up.
76
+ */
77
+ loadConfig: () => Promise<Config>;
78
+ /**
79
+ * The host's own version, reported to Studio next to the runtime's.
80
+ */
81
+ version: string;
82
+ /**
83
+ * The project root generation runs against.
84
+ * @default process.cwd()
85
+ */
86
+ root?: string;
87
+ /**
88
+ * What Studio may do in this project. Every permission is off unless granted.
89
+ */
90
+ permissions?: Partial<AgentPermissions>;
91
+ /**
92
+ * Receives the `studio:*` hooks and the `kubb:*` hooks of every generation.
93
+ */
94
+ hooks?: Hookable<KubbHooks>;
95
+ signal?: AbortSignal;
96
+ /**
97
+ * Longest wait between reconnect attempts, in milliseconds.
98
+ * @default 30000
99
+ */
100
+ maxRetryDelay?: number;
101
+ /**
102
+ * Milliseconds between keep-alive pings, at most 60000.
103
+ * @default 30000
104
+ */
105
+ heartbeatInterval?: number;
106
+ };
107
+ /**
108
+ * Connects this project to Kubb Studio and serves its commands until `signal` aborts. Reconnects
109
+ * with jittered backoff, and resolves when another process of the agent takes over. Rejects with
110
+ * {@link InvalidAgentTokenError} so the host can pair again, or {@link IncompatibleAgentError}.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const hooks = new Hookable<KubbHooks>()
115
+ * hooks.hook('studio:connected', ({ agentSlug }) => console.log(`Connected as ${agentSlug}`))
116
+ *
117
+ * await connectAgent({ token, configPath: 'kubb.config.ts', loadConfig, version: '1.0.0', hooks, signal })
118
+ * ```
119
+ */
120
+ export declare function connectAgent(options: ConnectAgentOptions): Promise<void>;
121
+ //#endregion
122
+ //#region src/pair.d.ts
123
+ /**
124
+ * RFC 8628 device-authorization response from Studio's `/api/auth/device/code` endpoint.
125
+ * Field names match the RFC; the CLI polls with `device_code` and shows `user_code` to the user.
126
+ */
127
+ type PairingSession = {
128
+ device_code: string;
129
+ user_code: string;
130
+ verification_uri: string;
131
+ verification_uri_complete: string;
132
+ expires_in: number;
133
+ interval: number;
134
+ };
135
+ type PairingResult = {
136
+ /**
137
+ * Bearer token for this machine. Write credentials with mode 0600 and never log the value.
138
+ */
139
+ token: string;
140
+ agent: {
141
+ /**
142
+ * Stable agent id in Studio.
143
+ */
144
+ id: string;
145
+ /**
146
+ * Short slug used in logs and the UI (for example `brave-otter`).
147
+ */
148
+ slug: string;
149
+ /**
150
+ * Display name chosen at pairing time.
151
+ */
152
+ name: string;
153
+ /**
154
+ * This agent's organization slug, absent for a sandbox or global agent, which has none.
155
+ */
156
+ organizationSlug?: string;
157
+ };
158
+ };
159
+ /**
160
+ * What this machine registers as. Any member may approve `cli` (`kubb studio`); only an admin may
161
+ * approve `user` and `sandbox` (the Docker image). A `ci` agent never pairs.
162
+ */
163
+ type PairingAgentType = 'cli' | 'user' | 'sandbox';
164
+ /**
165
+ * Thrown when a caller aborts `startPairing` or `pollForPairingToken` through their `signal`, such
166
+ * as a `kubb studio` shutdown mid-pairing. Distinct from a denial or an expired code, so a host can
167
+ * exit quietly instead of reporting a pairing failure.
168
+ */
169
+ export declare class PairingCanceledError extends Error {
170
+ constructor();
171
+ }
172
+ /** Thrown when the code expired unapproved. A fresh code can still succeed. */
173
+ export declare class PairingExpiredError extends Error {
174
+ constructor(message?: string);
175
+ }
176
+ /** Thrown when the pairing was denied in the browser, including by the organization's agent limit. */
177
+ export declare class PairingDeniedError extends Error {
178
+ constructor(message?: string);
179
+ }
180
+ type StartPairingOptions = {
181
+ studioUrl?: string;
182
+ type: PairingAgentType;
183
+ /**
184
+ * Display name for the agent, usually the project or machine name.
185
+ */
186
+ name: string;
187
+ hostname: string;
188
+ /**
189
+ * A stable secret the host keeps for this machine. Studio stores its hash against the agent, so
190
+ * the same machine pairing again rotates one agent's token instead of creating a second agent.
191
+ */
192
+ machineId: string;
193
+ /**
194
+ * Aborting this cancels the request in flight and rejects with {@link PairingCanceledError}.
195
+ */
196
+ signal?: AbortSignal;
197
+ };
198
+ type PairAgentOptions = StartPairingOptions & {
199
+ /** Shows the code to whoever approves it. Called again for each fresh code. */
200
+ onCode: (session: PairingSession, attempt: number) => void | Promise<void>;
201
+ /** Called when a poll could not reach Studio. Polling carries on. */
202
+ onRetry?: (error: Error) => void;
203
+ /**
204
+ * Codes to ask for in total when one expires unapproved. A denial or abort ends it at once.
205
+ * @default 1
206
+ */
207
+ maxAttempts?: number;
208
+ };
209
+ /**
210
+ * Pairs this machine with Studio: asks for a code, hands it to the host to show, and waits for approval.
211
+ */
212
+ export declare function pairAgent({ onCode, onRetry, maxAttempts, ...options }: PairAgentOptions): Promise<PairingResult>;
213
+ //#endregion
214
+ //#region src/snapshot.d.ts
215
+ type CreateSnapshotOptions = Omit<SnapshotUploadMetadata, 'hashes' | 'peerDependencies'> & {
216
+ studioUrl?: string;
217
+ /**
218
+ * Organization CI API key, sent as `x-api-key`.
219
+ */
220
+ apiKey: string;
221
+ /**
222
+ * The project's Kubb config, generated like `kubb generate`: formatter, linter, and `output.postGenerate` included.
223
+ */
224
+ config: Config;
225
+ hooks?: Hookable<KubbHooks>;
226
+ signal?: AbortSignal;
227
+ };
228
+ /**
229
+ * Generates the project, packs it as an npm tarball, and publishes it to Studio in one `POST /api/snapshots` request.
230
+ *
231
+ * @example
232
+ * ```ts
233
+ * const { url } = await createSnapshot({ apiKey: process.env.KUBB_TOKEN!, config, id: 'gh:123:pr-4', name: 'acme/api#4', packageName: '@acme/api', version: '1.0.0' })
234
+ * ```
235
+ */
236
+ export declare function createSnapshot({ studioUrl, apiKey, config, hooks, signal, ...metadata }: CreateSnapshotOptions): Promise<PublishedSnapshot>;
237
+ //#endregion
238
+ //#region src/url.d.ts
239
+ /**
240
+ * Hosted Kubb Studio URL, used when a host passes none.
241
+ */
242
+ export declare const defaultStudioUrl = "https://kubb.studio";
243
+ //#endregion
244
+ //#region src/configFile.d.ts
245
+ /**
246
+ * Applies `edits` to the config file at `filePath`, writing it only when something changed.
247
+ * `add-plugin` accepts `@kubb/plugin-*` packages and the plugins in `extensions`.
248
+ */
249
+ export declare function writeConfigEdits({ filePath, edits, extensions }: {
250
+ filePath: string;
251
+ edits: Array<ConfigEdit>;
252
+ extensions?: InstalledExtensions;
253
+ }): Promise<{
254
+ source: string;
255
+ outcomes: Array<ConfigEditOutcome>;
256
+ changed: boolean;
257
+ }>;
258
+ //#endregion
259
+ export type { ConnectAgentOptions, StudioConnectedContext };
260
+ //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
2
- import { E as InstalledExtensions, N as PublishedSnapshot, U as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-DQfI1aLV.js";
1
+ import { D as InstalledExtensions, P as PublishedSnapshot, W as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-CrMJymjT.js";
3
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
4
3
  //#region src/connect.d.ts
5
4
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import "./rolldown-runtime-CRm0XQPb.js";
2
1
  import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, FileSource, MAX_PAGE_LINES, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
3
2
  import { createRequire } from "node:module";
4
3
  import { createHash, hash, randomUUID } from "node:crypto";
@@ -17,6 +16,7 @@ import { stripVTControlCharacters } from "node:util";
17
16
  import { ast } from "@kubb/ast";
18
17
  import { ofetch } from "ofetch";
19
18
  import { gzipSync } from "node:zlib";
19
+ //#endregion
20
20
  //#region ../../internals/utils/src/casing.ts
21
21
  /**
22
22
  * Shared implementation for camelCase and PascalCase conversion.
@@ -154,7 +154,7 @@ function getElapsedMs(hrStart) {
154
154
  }
155
155
  //#endregion
156
156
  //#region package.json
157
- var version = "5.10.2";
157
+ var version = "5.10.3";
158
158
  //#endregion
159
159
  //#region src/resolveConfig.ts
160
160
  /**
@@ -1155,33 +1155,154 @@ async function writeConfigEdits({ filePath, edits, extensions }) {
1155
1155
  //#endregion
1156
1156
  //#region src/diff.ts
1157
1157
  const CONTEXT_LINES = 3;
1158
- const DIFF_TIMEOUT_MS = 1e4;
1159
- function diffHunks(before, after) {
1160
- return new Promise((resolve) => {
1161
- structuredPatch("", "", before, after, void 0, void 0, {
1158
+ const DIFF_TIMEOUT_MS = 1e3;
1159
+ const splitLines = (text) => text === "" ? [] : text.split(/(?<=\n)/);
1160
+ function longestIncreasing(values) {
1161
+ const tails = [];
1162
+ const prev = [];
1163
+ for (const [index, value] of values.entries()) {
1164
+ let low = 0;
1165
+ let high = tails.length;
1166
+ while (low < high) {
1167
+ const mid = low + high >> 1;
1168
+ if (values[tails[mid]] < value) low = mid + 1;
1169
+ else high = mid;
1170
+ }
1171
+ prev[index] = low > 0 ? tails[low - 1] : -1;
1172
+ tails[low] = index;
1173
+ }
1174
+ const result = [];
1175
+ for (let index = tails.at(-1) ?? -1; index !== -1; index = prev[index]) result.push(index);
1176
+ return result.reverse();
1177
+ }
1178
+ function cutPoints(before, after) {
1179
+ const counts = /* @__PURE__ */ new Map();
1180
+ for (const [index, line] of before.entries()) {
1181
+ const count = counts.get(line);
1182
+ if (count) count.before++;
1183
+ else counts.set(line, {
1184
+ before: 1,
1185
+ after: 0,
1186
+ index
1187
+ });
1188
+ }
1189
+ const afterIndex = /* @__PURE__ */ new Map();
1190
+ for (const [index, line] of after.entries()) {
1191
+ const count = counts.get(line);
1192
+ if (!count) continue;
1193
+ count.after++;
1194
+ afterIndex.set(line, index);
1195
+ }
1196
+ const unique = [...counts].filter(([, count]) => count.before === 1 && count.after === 1).map(([line, count]) => [count.index, afterIndex.get(line)]);
1197
+ const anchors = longestIncreasing(unique.map(([, index]) => index)).map((index) => unique[index]);
1198
+ const cuts = [[0, 0]];
1199
+ let run = {
1200
+ before: 0,
1201
+ after: 0,
1202
+ length: 0
1203
+ };
1204
+ const closeRun = () => {
1205
+ if (run.length > 6) cuts.push([run.before + CONTEXT_LINES, run.after + CONTEXT_LINES]);
1206
+ };
1207
+ let i = 0;
1208
+ let j = 0;
1209
+ for (const [anchorBefore, anchorAfter] of anchors) {
1210
+ const unchanged = anchorBefore - i === anchorAfter - j && before.slice(i, anchorBefore).every((line, offset) => line === after[j + offset]);
1211
+ if (unchanged && run.length && run.before + run.length === i) run.length += anchorBefore - i + 1;
1212
+ else {
1213
+ closeRun();
1214
+ run = unchanged ? {
1215
+ before: i,
1216
+ after: j,
1217
+ length: anchorBefore - i + 1
1218
+ } : {
1219
+ before: anchorBefore,
1220
+ after: anchorAfter,
1221
+ length: 1
1222
+ };
1223
+ }
1224
+ i = anchorBefore + 1;
1225
+ j = anchorAfter + 1;
1226
+ }
1227
+ closeRun();
1228
+ cuts.push([before.length, after.length]);
1229
+ return cuts;
1230
+ }
1231
+ function replaceHunk(before, after) {
1232
+ let start = 0;
1233
+ while (start < before.length && start < after.length && before[start] === after[start]) start++;
1234
+ let end = 0;
1235
+ while (end < before.length - start && end < after.length - start && before[before.length - 1 - end] === after[after.length - 1 - end]) end++;
1236
+ const removed = before.slice(start, before.length - end);
1237
+ const added = after.slice(start, after.length - end);
1238
+ if (!removed.length && !added.length) return [];
1239
+ const lines = (prefix, rows) => rows.flatMap((row) => row.endsWith("\n") ? [`${prefix}${row.slice(0, -1)}`] : [`${prefix}${row}`, "\"]);
1240
+ return [{
1241
+ oldStart: start + 1,
1242
+ oldLines: removed.length,
1243
+ newStart: start + 1,
1244
+ newLines: added.length,
1245
+ lines: [...lines("-", removed), ...lines("+", added)]
1246
+ }];
1247
+ }
1248
+ function diffHunks(before, after, timeout = DIFF_TIMEOUT_MS) {
1249
+ const beforeLines = splitLines(before);
1250
+ const afterLines = splitLines(after);
1251
+ const cuts = cutPoints(beforeLines, afterLines);
1252
+ const deadline = Date.now() + timeout;
1253
+ const hunks = [];
1254
+ let coarse = false;
1255
+ for (const [index, [beforeStart, afterStart]] of cuts.slice(0, -1).entries()) {
1256
+ const [beforeEnd, afterEnd] = cuts[index + 1];
1257
+ const segmentBefore = beforeLines.slice(beforeStart, beforeEnd);
1258
+ const segmentAfter = afterLines.slice(afterStart, afterEnd);
1259
+ const patch = structuredPatch("", "", segmentBefore.join(""), segmentAfter.join(""), void 0, void 0, {
1162
1260
  context: CONTEXT_LINES,
1163
- timeout: DIFF_TIMEOUT_MS,
1164
- callback: (patch) => resolve(patch?.hunks)
1261
+ timeout: Math.max(0, deadline - Date.now())
1165
1262
  });
1166
- });
1263
+ if (!patch) coarse = true;
1264
+ for (const hunk of patch?.hunks ?? replaceHunk(segmentBefore, segmentAfter)) hunks.push({
1265
+ ...hunk,
1266
+ oldStart: hunk.oldStart + beforeStart,
1267
+ newStart: hunk.newStart + afterStart
1268
+ });
1269
+ }
1270
+ const lines = hunks.flatMap((hunk) => hunk.lines);
1271
+ return {
1272
+ hunks,
1273
+ stats: {
1274
+ additions: lines.filter((line) => line[0] === "+").length,
1275
+ deletions: lines.filter((line) => line[0] === "-").length,
1276
+ hunks: hunks.length,
1277
+ lines: lines.length,
1278
+ coarse
1279
+ }
1280
+ };
1167
1281
  }
1168
1282
  function pageHunks({ hunks, cursor, limit }) {
1169
1283
  const page = [];
1170
- const end = cursor + limit;
1284
+ let used = 0;
1171
1285
  let start = 0;
1172
1286
  for (const [index, hunk] of hunks.entries()) {
1173
- const next = start + hunk.lines.length;
1174
- if (next > cursor && start < end) page.push({
1287
+ const offset = Math.max(0, cursor - start);
1288
+ start += hunk.lines.length;
1289
+ const remaining = hunk.lines.length - offset;
1290
+ if (remaining <= 0) continue;
1291
+ if (used > 0 && remaining > limit - used) break;
1292
+ let take = Math.min(remaining, limit - used);
1293
+ if (hunk.lines[offset + take]?.startsWith("\\")) take++;
1294
+ page.push({
1175
1295
  index,
1176
- oldStart: hunk.oldStart,
1177
- newStart: hunk.newStart,
1178
- lines: hunk.lines.slice(Math.max(0, cursor - start), end - start)
1296
+ ...hunk,
1297
+ lines: hunk.lines.slice(offset, offset + take)
1179
1298
  });
1180
- start = next;
1299
+ used += take;
1300
+ if (used >= limit) break;
1181
1301
  }
1302
+ const total = hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
1182
1303
  return {
1183
1304
  hunks: page,
1184
- nextCursor: end < start ? end : null
1305
+ nextCursor: cursor + used < total ? cursor + used : null
1185
1306
  };
1186
1307
  }
1187
1308
  //#endregion
@@ -1417,7 +1538,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1417
1538
  //#region src/generationEvents.ts
1418
1539
  const MAX_QUEUED_EVENTS = 1024;
1419
1540
  const RESERVED_EVENTS = 64;
1420
- const isDiscardable = (event) => event.type === "kubb:files:processing:update" || event.type === "kubb:info" || event.type === "kubb:success";
1541
+ const isDiscardable = (type) => type === "kubb:files:processing:update" || type === "kubb:info" || type === "kubb:success";
1421
1542
  /** Forwards selected Kubb lifecycle events to a native Cap'n Web stream. */
1422
1543
  function createGenerationStream(hooks, jobId) {
1423
1544
  const unhooks = [];
@@ -1449,7 +1570,7 @@ function createGenerationStream(hooks, jobId) {
1449
1570
  version: 1,
1450
1571
  timestamp: Date.now()
1451
1572
  };
1452
- if (controller.desiredSize <= RESERVED_EVENTS && isDiscardable(event)) return;
1573
+ if (controller.desiredSize <= RESERVED_EVENTS && isDiscardable(type)) return;
1453
1574
  if (controller.desiredSize <= 0) {
1454
1575
  fail(/* @__PURE__ */ new Error(`Generation event stream exceeded ${MAX_QUEUED_EVENTS} queued events`));
1455
1576
  return;
@@ -1553,7 +1674,10 @@ function createGenerationStream(hooks, jobId) {
1553
1674
  }]
1554
1675
  };
1555
1676
  function forward(type) {
1556
- on(type, (...args) => emitEvent(type, projections[type](...args)));
1677
+ on(type, (...args) => {
1678
+ emitEvent(type, projections[type](...args));
1679
+ if (!isDiscardable(type)) return new Promise((resolve) => setImmediate(resolve));
1680
+ });
1557
1681
  }
1558
1682
  on("kubb:build:start", ({ config }) => {
1559
1683
  root = config.root;
@@ -2063,7 +2187,14 @@ var Agent = class extends RpcTarget {
2063
2187
  if (beforeContent === void 0) return {
2064
2188
  hunks: [],
2065
2189
  nextCursor: null,
2066
- hasBase: false
2190
+ hasBase: false,
2191
+ stats: {
2192
+ additions: 0,
2193
+ deletions: 0,
2194
+ hunks: 0,
2195
+ lines: 0,
2196
+ coarse: false
2197
+ }
2067
2198
  };
2068
2199
  const key = JSON.stringify([
2069
2200
  jobId,
@@ -2071,25 +2202,21 @@ var Agent = class extends RpcTarget {
2071
2202
  baseSource,
2072
2203
  path
2073
2204
  ]);
2074
- let hunks = this.#diffs.get(key);
2075
- if (!hunks) {
2076
- hunks = diffHunks(beforeContent, afterContent ?? "");
2077
- this.#diffs.set(key, hunks);
2205
+ let diff = this.#diffs.get(key);
2206
+ if (!diff) {
2207
+ diff = diffHunks(beforeContent, afterContent ?? "");
2208
+ this.#diffs.set(key, diff);
2078
2209
  if (this.#diffs.size > 4) this.#diffs.delete(this.#diffs.keys().next().value);
2079
2210
  }
2080
- const all = await hunks;
2081
- if (!all) {
2082
- this.#diffs.delete(key);
2083
- return this.#refuse({ message: "The two versions differ too much to diff" });
2084
- }
2085
2211
  await this.#logCommand("readDiff", `read the diff of ${path}`);
2086
2212
  return {
2087
2213
  ...pageHunks({
2088
- hunks: all,
2214
+ hunks: diff.hunks,
2089
2215
  cursor,
2090
2216
  limit
2091
2217
  }),
2092
- hasBase: true
2218
+ hasBase: true,
2219
+ stats: diff.stats
2093
2220
  };
2094
2221
  }
2095
2222
  };