@spotpatch/vite 1.3.1 → 1.4.1
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 +252 -33
- package/dist/index.cjs +51 -2204
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -65
- package/dist/index.d.ts +5 -65
- package/dist/index.js +54 -2235
- package/dist/index.js.map +1 -1
- package/dist/runtime-client.js +14 -14
- package/package.json +5 -9
package/dist/index.js
CHANGED
|
@@ -1,487 +1,24 @@
|
|
|
1
1
|
// src/plugin.ts
|
|
2
|
-
import
|
|
3
|
-
import { loadEnv } from "vite";
|
|
4
|
-
|
|
5
|
-
// src/environment-ai.ts
|
|
6
|
-
var AI_ENVIRONMENT_NAMES = Object.freeze({
|
|
7
|
-
authentication: "SPOTPATCH_AI_AUTHENTICATION",
|
|
8
|
-
baseURL: "SPOTPATCH_AI_BASE_URL",
|
|
9
|
-
credential: "SPOTPATCH_AI_API_KEY",
|
|
10
|
-
model: "SPOTPATCH_AI_MODEL",
|
|
11
|
-
protocol: "SPOTPATCH_AI_PROTOCOL"
|
|
12
|
-
});
|
|
13
|
-
function normalizedValue(environment, name) {
|
|
14
|
-
const value = environment[name];
|
|
15
|
-
if (value === void 0 || value.trim().length === 0) {
|
|
16
|
-
return void 0;
|
|
17
|
-
}
|
|
18
|
-
return value.trim();
|
|
19
|
-
}
|
|
20
|
-
function resolveEnvironmentAiConfiguration(environment) {
|
|
21
|
-
const baseURL = normalizedValue(environment, AI_ENVIRONMENT_NAMES.baseURL);
|
|
22
|
-
const model = normalizedValue(environment, AI_ENVIRONMENT_NAMES.model);
|
|
23
|
-
const credential = normalizedValue(environment, AI_ENVIRONMENT_NAMES.credential);
|
|
24
|
-
const protocol = normalizedValue(environment, AI_ENVIRONMENT_NAMES.protocol);
|
|
25
|
-
const authentication = normalizedValue(
|
|
26
|
-
environment,
|
|
27
|
-
AI_ENVIRONMENT_NAMES.authentication
|
|
28
|
-
);
|
|
29
|
-
const configuredValues = [baseURL, model, credential, protocol, authentication];
|
|
30
|
-
if (configuredValues.every((value) => value === void 0)) {
|
|
31
|
-
return Object.freeze({ ai: false });
|
|
32
|
-
}
|
|
33
|
-
const missing = [
|
|
34
|
-
[AI_ENVIRONMENT_NAMES.baseURL, baseURL],
|
|
35
|
-
[AI_ENVIRONMENT_NAMES.model, model],
|
|
36
|
-
[AI_ENVIRONMENT_NAMES.credential, credential]
|
|
37
|
-
].filter((entry) => entry[1] === void 0).map(([name]) => name);
|
|
38
|
-
if (missing.length > 0) {
|
|
39
|
-
throw new RangeError(
|
|
40
|
-
`SpotPatch AI environment configuration is incomplete; missing ${missing.join(", ")}.`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
if (baseURL === void 0 || model === void 0 || credential === void 0) {
|
|
44
|
-
throw new RangeError("SpotPatch AI environment configuration is incomplete.");
|
|
45
|
-
}
|
|
46
|
-
if (protocol !== void 0 && protocol !== "responses" && protocol !== "chat-completions") {
|
|
47
|
-
throw new RangeError(
|
|
48
|
-
"SpotPatch SPOTPATCH_AI_PROTOCOL must be responses or chat-completions."
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
if (authentication !== void 0 && authentication !== "bearer" && authentication !== "x-api-key") {
|
|
52
|
-
throw new RangeError(
|
|
53
|
-
"SpotPatch SPOTPATCH_AI_AUTHENTICATION must be bearer or x-api-key."
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
return Object.freeze({
|
|
57
|
-
ai: Object.freeze({
|
|
58
|
-
baseURL,
|
|
59
|
-
model,
|
|
60
|
-
...protocol === void 0 ? {} : { protocol },
|
|
61
|
-
...authentication === void 0 ? {} : { authentication }
|
|
62
|
-
})
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// src/options.ts
|
|
2
|
+
import path5 from "path";
|
|
67
3
|
import {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/node_modules/,
|
|
76
|
-
/\.test\.[jt]sx$/,
|
|
77
|
-
/\.spec\.[jt]sx$/,
|
|
78
|
-
/\.stories\.[jt]sx$/,
|
|
79
|
-
/(?:^|\/)dist(?:\/|$)/,
|
|
80
|
-
/(?:^|\/)coverage(?:\/|$)/
|
|
81
|
-
]);
|
|
82
|
-
var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:jsx|tsx)$/]);
|
|
83
|
-
var DEFAULT_BUDGET = Object.freeze({
|
|
84
|
-
totalCharacters: 16e3,
|
|
85
|
-
domCharacters: 3e3,
|
|
86
|
-
cssCharacters: 4e3,
|
|
87
|
-
codeCharacters: 7e3,
|
|
88
|
-
maxCodeLines: 80,
|
|
89
|
-
maxComponentDepth: 8
|
|
90
|
-
});
|
|
91
|
-
var DEFAULT_OPTIONS = Object.freeze({
|
|
92
|
-
enabled: true,
|
|
93
|
-
include: DEFAULT_INCLUDE,
|
|
94
|
-
exclude: DEFAULT_EXCLUDE,
|
|
95
|
-
editor: "auto",
|
|
96
|
-
redact: true,
|
|
97
|
-
budget: DEFAULT_BUDGET,
|
|
98
|
-
shortcut: "Mod+Shift+S",
|
|
99
|
-
allowLan: false,
|
|
100
|
-
debug: false,
|
|
101
|
-
locale: "auto",
|
|
102
|
-
maxTargets: 8,
|
|
103
|
-
ai: false
|
|
104
|
-
});
|
|
105
|
-
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
106
|
-
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
107
|
-
var agentLimitsSchema = z.strictObject({
|
|
108
|
-
maxTurns: z.number().optional(),
|
|
109
|
-
maxToolCalls: z.number().optional(),
|
|
110
|
-
maxChangedFiles: z.number().optional(),
|
|
111
|
-
maxDiffBytes: z.number().optional(),
|
|
112
|
-
maxReadBytesPerFile: z.number().optional(),
|
|
113
|
-
maxToolOutputCharacters: z.number().optional(),
|
|
114
|
-
maxProviderResponseBytes: z.number().optional(),
|
|
115
|
-
providerConnectTimeoutMs: z.number().optional(),
|
|
116
|
-
providerFirstByteTimeoutMs: z.number().optional(),
|
|
117
|
-
providerIdleTimeoutMs: z.number().optional(),
|
|
118
|
-
checkTimeoutMs: z.number().optional(),
|
|
119
|
-
jobTimeoutMs: z.number().optional()
|
|
120
|
-
}).optional();
|
|
121
|
-
var agentCheckSchema = z.strictObject({
|
|
122
|
-
label: z.string(),
|
|
123
|
-
command: z.string(),
|
|
124
|
-
args: z.array(z.string()).optional(),
|
|
125
|
-
required: z.boolean().optional(),
|
|
126
|
-
timeoutMs: z.number().optional()
|
|
127
|
-
});
|
|
128
|
-
var aiOptionsSchema = z.strictObject({
|
|
129
|
-
providers: z.record(
|
|
130
|
-
z.string(),
|
|
131
|
-
z.strictObject({
|
|
132
|
-
type: z.literal("openai-compatible"),
|
|
133
|
-
label: z.string(),
|
|
134
|
-
protocol: z.enum(["responses", "chat-completions"]),
|
|
135
|
-
authentication: z.enum(["bearer", "x-api-key"]).optional(),
|
|
136
|
-
baseURL: z.string(),
|
|
137
|
-
apiKeyEnv: z.string(),
|
|
138
|
-
models: z.record(
|
|
139
|
-
z.string(),
|
|
140
|
-
z.strictObject({ label: z.string(), model: z.string() })
|
|
141
|
-
),
|
|
142
|
-
defaultModel: z.string()
|
|
143
|
-
})
|
|
144
|
-
),
|
|
145
|
-
defaultProvider: z.string(),
|
|
146
|
-
execution: z.strictObject({
|
|
147
|
-
isolation: z.literal("git-worktree").optional(),
|
|
148
|
-
applyMode: z.enum(["review", "auto"]).optional(),
|
|
149
|
-
checks: z.record(z.string(), agentCheckSchema).optional(),
|
|
150
|
-
limits: agentLimitsSchema
|
|
151
|
-
}).optional()
|
|
152
|
-
});
|
|
153
|
-
var simpleAiOptionsSchema = z.strictObject({
|
|
154
|
-
baseURL: z.string(),
|
|
155
|
-
model: z.string(),
|
|
156
|
-
apiKeyEnv: z.string().optional(),
|
|
157
|
-
protocol: z.enum(["responses", "chat-completions"]).optional(),
|
|
158
|
-
authentication: z.enum(["bearer", "x-api-key"]).optional(),
|
|
159
|
-
providerLabel: z.string().optional(),
|
|
160
|
-
modelLabel: z.string().optional(),
|
|
161
|
-
execution: aiOptionsSchema.shape.execution
|
|
162
|
-
});
|
|
163
|
-
function assertIdentifier(value, label) {
|
|
164
|
-
if (!PROFILE_ID_PATTERN.test(value)) {
|
|
165
|
-
throw new RangeError(
|
|
166
|
-
`SpotPatch ${label} must contain only letters, numbers, dot, underscore, or hyphen.`
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
function nonEmpty(value, label, maximum = 256) {
|
|
171
|
-
const normalized = value.trim();
|
|
172
|
-
if (normalized.length === 0 || normalized.length > maximum || value.includes("\0")) {
|
|
173
|
-
throw new RangeError(`SpotPatch ${label} is invalid.`);
|
|
174
|
-
}
|
|
175
|
-
return normalized;
|
|
176
|
-
}
|
|
177
|
-
function normalizeProviderBaseURL(value) {
|
|
178
|
-
let url;
|
|
179
|
-
try {
|
|
180
|
-
url = new URL(value);
|
|
181
|
-
} catch {
|
|
182
|
-
throw new RangeError("SpotPatch AI provider baseURL must be a valid URL.");
|
|
183
|
-
}
|
|
184
|
-
const loopbackHosts = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
185
|
-
const allowedProtocol = url.protocol === "https:" || url.protocol === "http:" && loopbackHosts.has(url.hostname);
|
|
186
|
-
if (!allowedProtocol || url.username.length > 0 || url.password.length > 0 || url.search.length > 0 || url.hash.length > 0) {
|
|
187
|
-
throw new RangeError("SpotPatch AI provider baseURL violates URL policy.");
|
|
188
|
-
}
|
|
189
|
-
url.pathname = url.pathname.replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
|
190
|
-
return url.toString().replace(/\/$/, "");
|
|
191
|
-
}
|
|
192
|
-
function resolveLimits(limits) {
|
|
193
|
-
const resolved = Object.freeze({
|
|
194
|
-
maxTurns: limits?.maxTurns ?? DEFAULT_AGENT_LIMITS.maxTurns,
|
|
195
|
-
maxToolCalls: limits?.maxToolCalls ?? DEFAULT_AGENT_LIMITS.maxToolCalls,
|
|
196
|
-
maxChangedFiles: limits?.maxChangedFiles ?? DEFAULT_AGENT_LIMITS.maxChangedFiles,
|
|
197
|
-
maxDiffBytes: limits?.maxDiffBytes ?? DEFAULT_AGENT_LIMITS.maxDiffBytes,
|
|
198
|
-
maxReadBytesPerFile: limits?.maxReadBytesPerFile ?? DEFAULT_AGENT_LIMITS.maxReadBytesPerFile,
|
|
199
|
-
maxToolOutputCharacters: limits?.maxToolOutputCharacters ?? DEFAULT_AGENT_LIMITS.maxToolOutputCharacters,
|
|
200
|
-
maxProviderResponseBytes: limits?.maxProviderResponseBytes ?? DEFAULT_AGENT_LIMITS.maxProviderResponseBytes,
|
|
201
|
-
providerConnectTimeoutMs: limits?.providerConnectTimeoutMs ?? DEFAULT_AGENT_LIMITS.providerConnectTimeoutMs,
|
|
202
|
-
providerFirstByteTimeoutMs: limits?.providerFirstByteTimeoutMs ?? DEFAULT_AGENT_LIMITS.providerFirstByteTimeoutMs,
|
|
203
|
-
providerIdleTimeoutMs: limits?.providerIdleTimeoutMs ?? DEFAULT_AGENT_LIMITS.providerIdleTimeoutMs,
|
|
204
|
-
checkTimeoutMs: limits?.checkTimeoutMs ?? DEFAULT_AGENT_LIMITS.checkTimeoutMs,
|
|
205
|
-
jobTimeoutMs: limits?.jobTimeoutMs ?? DEFAULT_AGENT_LIMITS.jobTimeoutMs
|
|
206
|
-
});
|
|
207
|
-
for (const [name, value] of Object.entries(resolved)) {
|
|
208
|
-
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
209
|
-
throw new RangeError(`SpotPatch AI limit ${name} must be a positive integer.`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return resolved;
|
|
213
|
-
}
|
|
214
|
-
function resolveModels(models) {
|
|
215
|
-
const entries = Object.entries(models);
|
|
216
|
-
if (entries.length === 0) {
|
|
217
|
-
throw new RangeError("SpotPatch AI provider must declare at least one model.");
|
|
218
|
-
}
|
|
219
|
-
return Object.freeze(
|
|
220
|
-
Object.fromEntries(
|
|
221
|
-
entries.map(([id, model]) => {
|
|
222
|
-
assertIdentifier(id, "model profile id");
|
|
223
|
-
return [
|
|
224
|
-
id,
|
|
225
|
-
Object.freeze({
|
|
226
|
-
id,
|
|
227
|
-
label: nonEmpty(model.label, "model label", 100),
|
|
228
|
-
model: nonEmpty(model.model, "provider model name")
|
|
229
|
-
})
|
|
230
|
-
];
|
|
231
|
-
})
|
|
232
|
-
)
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
function resolveProviders(providers) {
|
|
236
|
-
const entries = Object.entries(providers);
|
|
237
|
-
if (entries.length === 0) {
|
|
238
|
-
throw new RangeError("SpotPatch AI must declare at least one provider.");
|
|
239
|
-
}
|
|
240
|
-
return Object.freeze(
|
|
241
|
-
Object.fromEntries(
|
|
242
|
-
entries.map(([id, provider]) => {
|
|
243
|
-
assertIdentifier(id, "provider profile id");
|
|
244
|
-
if (!ENV_NAME_PATTERN.test(provider.apiKeyEnv) || provider.apiKeyEnv.startsWith("VITE_")) {
|
|
245
|
-
throw new RangeError(
|
|
246
|
-
"SpotPatch AI apiKeyEnv must be an uppercase non-VITE environment name."
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
const models = resolveModels(provider.models);
|
|
250
|
-
if (!(provider.defaultModel in models)) {
|
|
251
|
-
throw new RangeError(
|
|
252
|
-
"SpotPatch AI provider defaultModel must reference a configured model."
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
return [
|
|
256
|
-
id,
|
|
257
|
-
Object.freeze({
|
|
258
|
-
id,
|
|
259
|
-
type: provider.type,
|
|
260
|
-
label: nonEmpty(provider.label, "provider label", 100),
|
|
261
|
-
protocol: provider.protocol,
|
|
262
|
-
authentication: provider.authentication ?? "bearer",
|
|
263
|
-
baseURL: normalizeProviderBaseURL(provider.baseURL),
|
|
264
|
-
apiKeyEnv: provider.apiKeyEnv,
|
|
265
|
-
models,
|
|
266
|
-
defaultModel: provider.defaultModel
|
|
267
|
-
})
|
|
268
|
-
];
|
|
269
|
-
})
|
|
270
|
-
)
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
function resolveChecks(checks, defaultTimeoutMs) {
|
|
274
|
-
return Object.freeze(
|
|
275
|
-
Object.fromEntries(
|
|
276
|
-
Object.entries(checks ?? {}).map(([id, check]) => {
|
|
277
|
-
assertIdentifier(id, "check id");
|
|
278
|
-
const timeoutMs = check.timeoutMs ?? defaultTimeoutMs;
|
|
279
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
280
|
-
throw new RangeError("SpotPatch AI check timeout must be positive.");
|
|
281
|
-
}
|
|
282
|
-
const args = Object.freeze(
|
|
283
|
-
[...check.args ?? []].map(
|
|
284
|
-
(argument) => nonEmpty(argument, "check argument", 4096)
|
|
285
|
-
)
|
|
286
|
-
);
|
|
287
|
-
return [
|
|
288
|
-
id,
|
|
289
|
-
Object.freeze({
|
|
290
|
-
id,
|
|
291
|
-
label: nonEmpty(check.label, "check label", 100),
|
|
292
|
-
command: nonEmpty(check.command, "check command", 1024),
|
|
293
|
-
args,
|
|
294
|
-
required: check.required ?? true,
|
|
295
|
-
timeoutMs
|
|
296
|
-
})
|
|
297
|
-
];
|
|
298
|
-
})
|
|
299
|
-
)
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
function resolveAiOptions(options) {
|
|
303
|
-
if (options === void 0 || options === false) {
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
const expanded = "providers" in options ? options : (() => {
|
|
307
|
-
const simple = simpleAiOptionsSchema.safeParse(options);
|
|
308
|
-
if (!simple.success) {
|
|
309
|
-
throw new RangeError("SpotPatch AI configuration is invalid.");
|
|
310
|
-
}
|
|
311
|
-
const providerId = "default";
|
|
312
|
-
const modelId = "default";
|
|
313
|
-
return {
|
|
314
|
-
providers: {
|
|
315
|
-
[providerId]: {
|
|
316
|
-
type: "openai-compatible",
|
|
317
|
-
label: simple.data.providerLabel ?? "AI provider",
|
|
318
|
-
protocol: simple.data.protocol ?? "chat-completions",
|
|
319
|
-
authentication: simple.data.authentication ?? "bearer",
|
|
320
|
-
baseURL: simple.data.baseURL,
|
|
321
|
-
apiKeyEnv: simple.data.apiKeyEnv ?? "SPOTPATCH_AI_API_KEY",
|
|
322
|
-
models: {
|
|
323
|
-
[modelId]: {
|
|
324
|
-
label: simple.data.modelLabel ?? "AI model",
|
|
325
|
-
model: simple.data.model
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
defaultModel: modelId
|
|
329
|
-
}
|
|
330
|
-
},
|
|
331
|
-
defaultProvider: providerId,
|
|
332
|
-
...simple.data.execution === void 0 ? {} : { execution: simple.data.execution }
|
|
333
|
-
};
|
|
334
|
-
})();
|
|
335
|
-
const parsed = aiOptionsSchema.safeParse(expanded);
|
|
336
|
-
if (!parsed.success) {
|
|
337
|
-
throw new RangeError("SpotPatch AI configuration is invalid.");
|
|
338
|
-
}
|
|
339
|
-
const validated = parsed.data;
|
|
340
|
-
const limits = resolveLimits(validated.execution?.limits);
|
|
341
|
-
const checks = resolveChecks(validated.execution?.checks, limits.checkTimeoutMs);
|
|
342
|
-
const applyMode = validated.execution?.applyMode ?? "review";
|
|
343
|
-
if (applyMode === "auto" && !Object.values(checks).some((check) => check.required)) {
|
|
344
|
-
throw new RangeError("SpotPatch AI auto mode requires a required check.");
|
|
345
|
-
}
|
|
346
|
-
const providers = resolveProviders(validated.providers);
|
|
347
|
-
if (!(validated.defaultProvider in providers)) {
|
|
348
|
-
throw new RangeError(
|
|
349
|
-
"SpotPatch AI defaultProvider must reference a configured provider."
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
return Object.freeze({
|
|
353
|
-
providers,
|
|
354
|
-
defaultProvider: validated.defaultProvider,
|
|
355
|
-
execution: Object.freeze({
|
|
356
|
-
isolation: "git-worktree",
|
|
357
|
-
applyMode,
|
|
358
|
-
checks,
|
|
359
|
-
limits
|
|
360
|
-
})
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
function createRuntimeAiConfig(options) {
|
|
364
|
-
if (options === false) {
|
|
365
|
-
return Object.freeze({ enabled: false });
|
|
366
|
-
}
|
|
367
|
-
return Object.freeze({
|
|
368
|
-
enabled: true,
|
|
369
|
-
defaultProvider: options.defaultProvider,
|
|
370
|
-
applyMode: options.execution.applyMode,
|
|
371
|
-
providers: Object.freeze(
|
|
372
|
-
Object.values(options.providers).map(
|
|
373
|
-
(provider) => Object.freeze({
|
|
374
|
-
id: provider.id,
|
|
375
|
-
label: provider.label,
|
|
376
|
-
protocol: provider.protocol,
|
|
377
|
-
defaultModel: provider.defaultModel,
|
|
378
|
-
models: Object.freeze(
|
|
379
|
-
Object.values(provider.models).map(
|
|
380
|
-
(model) => Object.freeze({ id: model.id, label: model.label })
|
|
381
|
-
)
|
|
382
|
-
)
|
|
383
|
-
})
|
|
384
|
-
)
|
|
385
|
-
)
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
function assertPositiveBudget(budget) {
|
|
389
|
-
for (const [name, value] of Object.entries(budget)) {
|
|
390
|
-
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
391
|
-
throw new RangeError(`SpotPatch budget ${name} must be a positive integer.`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
function resolveOptions(options = {}, environmentAi) {
|
|
396
|
-
const budget = Object.freeze({
|
|
397
|
-
...DEFAULT_OPTIONS.budget,
|
|
398
|
-
...options.budget
|
|
399
|
-
});
|
|
400
|
-
assertPositiveBudget(budget);
|
|
401
|
-
const maxTargets = options.maxTargets ?? DEFAULT_OPTIONS.maxTargets;
|
|
402
|
-
const locale = options.locale ?? DEFAULT_OPTIONS.locale;
|
|
403
|
-
const editor = options.editor ?? DEFAULT_OPTIONS.editor;
|
|
404
|
-
if (!SPOTPATCH_LOCALE_PREFERENCES.includes(locale)) {
|
|
405
|
-
throw new RangeError("SpotPatch locale must be auto, en-US, or zh-CN.");
|
|
406
|
-
}
|
|
407
|
-
if (!SPOTPATCH_EDITOR_PREFERENCES.includes(editor)) {
|
|
408
|
-
throw new RangeError("SpotPatch editor must be auto, vscode, or cursor.");
|
|
409
|
-
}
|
|
410
|
-
if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets > MAX_ANNOTATION_TARGETS) {
|
|
411
|
-
throw new RangeError(
|
|
412
|
-
`SpotPatch maxTargets must be an integer between 1 and ${String(MAX_ANNOTATION_TARGETS)}.`
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
const resolved = {
|
|
416
|
-
enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,
|
|
417
|
-
include: Object.freeze([...options.include ?? DEFAULT_OPTIONS.include]),
|
|
418
|
-
exclude: Object.freeze([...options.exclude ?? DEFAULT_OPTIONS.exclude]),
|
|
419
|
-
editor,
|
|
420
|
-
redact: options.redact ?? DEFAULT_OPTIONS.redact,
|
|
421
|
-
budget,
|
|
422
|
-
shortcut: options.shortcut ?? DEFAULT_OPTIONS.shortcut,
|
|
423
|
-
allowLan: options.allowLan ?? DEFAULT_OPTIONS.allowLan,
|
|
424
|
-
debug: options.debug ?? DEFAULT_OPTIONS.debug,
|
|
425
|
-
locale,
|
|
426
|
-
maxTargets,
|
|
427
|
-
ai: resolveAiOptions(options.ai ?? environmentAi)
|
|
428
|
-
};
|
|
429
|
-
if (resolved.shortcut.trim().length === 0) {
|
|
430
|
-
throw new RangeError("SpotPatch shortcut cannot be empty.");
|
|
431
|
-
}
|
|
432
|
-
return Object.freeze(resolved);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// src/registry/source-registry.ts
|
|
436
|
-
import path from "path";
|
|
437
|
-
|
|
438
|
-
// src/registry/source-id.ts
|
|
439
|
-
import { randomBytes } from "crypto";
|
|
440
|
-
var SOURCE_ID_BYTES = 8;
|
|
441
|
-
var createRandomSourceId = () => randomBytes(SOURCE_ID_BYTES).toString("base64url");
|
|
442
|
-
|
|
443
|
-
// src/registry/source-registry.ts
|
|
444
|
-
function normalizeAbsolutePath(absolutePath) {
|
|
445
|
-
return path.normalize(path.resolve(absolutePath));
|
|
446
|
-
}
|
|
447
|
-
function createSourceRegistry(options = {}) {
|
|
448
|
-
const createId = options.createId ?? createRandomSourceId;
|
|
449
|
-
const pathToId = /* @__PURE__ */ new Map();
|
|
450
|
-
const idToPath = /* @__PURE__ */ new Map();
|
|
451
|
-
return Object.freeze({
|
|
452
|
-
register(absolutePath) {
|
|
453
|
-
const normalizedPath = normalizeAbsolutePath(absolutePath);
|
|
454
|
-
const existingId = pathToId.get(normalizedPath);
|
|
455
|
-
if (existingId !== void 0) {
|
|
456
|
-
return existingId;
|
|
457
|
-
}
|
|
458
|
-
let fileId = createId();
|
|
459
|
-
while (idToPath.has(fileId)) {
|
|
460
|
-
fileId = createId();
|
|
461
|
-
}
|
|
462
|
-
pathToId.set(normalizedPath, fileId);
|
|
463
|
-
idToPath.set(fileId, normalizedPath);
|
|
464
|
-
return fileId;
|
|
465
|
-
},
|
|
466
|
-
resolve(fileId) {
|
|
467
|
-
return idToPath.get(fileId);
|
|
468
|
-
},
|
|
469
|
-
clear() {
|
|
470
|
-
pathToId.clear();
|
|
471
|
-
idToPath.clear();
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
}
|
|
4
|
+
createSession,
|
|
5
|
+
createSourceRegistry,
|
|
6
|
+
resolveCredentialEnvironment,
|
|
7
|
+
resolveEnvironmentAiConfiguration,
|
|
8
|
+
resolveOptions
|
|
9
|
+
} from "@spotpatch/dev-server";
|
|
10
|
+
import { loadEnv } from "vite";
|
|
475
11
|
|
|
476
12
|
// src/runtime/runtime-injection-plugin.ts
|
|
477
13
|
import { createRequire } from "module";
|
|
478
14
|
import { readFileSync } from "fs";
|
|
479
|
-
import
|
|
15
|
+
import path from "path";
|
|
16
|
+
import { createRuntimeAiConfig } from "@spotpatch/dev-server";
|
|
480
17
|
|
|
481
18
|
// package.json
|
|
482
19
|
var package_default = {
|
|
483
20
|
name: "@spotpatch/vite",
|
|
484
|
-
version: "1.
|
|
21
|
+
version: "1.4.1",
|
|
485
22
|
description: "Vite development plugin for SpotPatch.",
|
|
486
23
|
license: "MIT",
|
|
487
24
|
repository: {
|
|
@@ -529,15 +66,11 @@ var package_default = {
|
|
|
529
66
|
typecheck: "tsc --noEmit -p tsconfig.json"
|
|
530
67
|
},
|
|
531
68
|
dependencies: {
|
|
532
|
-
"@
|
|
533
|
-
"@spotpatch/
|
|
69
|
+
"@spotpatch/compiler": "workspace:^",
|
|
70
|
+
"@spotpatch/dev-server": "workspace:^",
|
|
534
71
|
"@spotpatch/react-adapter": "workspace:^",
|
|
535
72
|
"@spotpatch/runtime": "workspace:^",
|
|
536
|
-
"@spotpatch/shared": "workspace:^"
|
|
537
|
-
"launch-editor": "2.14.1",
|
|
538
|
-
"magic-string": "1.1.0",
|
|
539
|
-
"oxc-parser": "0.143.0",
|
|
540
|
-
zod: "4.4.3"
|
|
73
|
+
"@spotpatch/shared": "workspace:^"
|
|
541
74
|
},
|
|
542
75
|
peerDependencies: {
|
|
543
76
|
vite: "^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
@@ -555,14 +88,14 @@ var RESOLVED_SPOTPATCH_CLIENT_MODULE_ID = `\0${SPOTPATCH_CLIENT_MODULE_ID}`;
|
|
|
555
88
|
var SPOTPATCH_REACT_ADAPTER_MODULE_ID = "virtual:spotpatch/react-adapter";
|
|
556
89
|
var RESOLVED_SPOTPATCH_REACT_ADAPTER_MODULE_ID = `\0${SPOTPATCH_REACT_ADAPTER_MODULE_ID}`;
|
|
557
90
|
function readRuntimeBundle(root, fileName) {
|
|
558
|
-
const resolveFromProject = createRequire(
|
|
91
|
+
const resolveFromProject = createRequire(path.join(root, "package.json"));
|
|
559
92
|
const packageEntry = resolveFromProject.resolve("@spotpatch/vite");
|
|
560
|
-
const bundlePath =
|
|
93
|
+
const bundlePath = path.join(path.dirname(packageEntry), fileName);
|
|
561
94
|
return readFileSync(bundlePath, "utf8");
|
|
562
95
|
}
|
|
563
96
|
function readConsumerViteVersion(root) {
|
|
564
97
|
try {
|
|
565
|
-
const resolveFromProject = createRequire(
|
|
98
|
+
const resolveFromProject = createRequire(path.join(root, "package.json"));
|
|
566
99
|
const manifestPath = resolveFromProject.resolve("vite/package.json");
|
|
567
100
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
568
101
|
if (typeof manifest === "object" && manifest !== null && "version" in manifest && typeof manifest.version === "string") {
|
|
@@ -579,13 +112,14 @@ function createClientModule(input, clientBundle, viteVersion) {
|
|
|
579
112
|
budget: options.budget,
|
|
580
113
|
debug: options.debug,
|
|
581
114
|
editor: options.editor,
|
|
115
|
+
framework: "vite",
|
|
116
|
+
frameworkVersion: viteVersion,
|
|
582
117
|
locale: options.locale,
|
|
583
118
|
maxTargets: options.maxTargets,
|
|
584
119
|
redact: options.redact,
|
|
585
120
|
sessionToken: input.session.token,
|
|
586
121
|
shortcut: options.shortcut,
|
|
587
|
-
spotPatchVersion: package_default.version
|
|
588
|
-
viteVersion
|
|
122
|
+
spotPatchVersion: package_default.version
|
|
589
123
|
};
|
|
590
124
|
return [
|
|
591
125
|
`const __SPOTPATCH_RUNTIME_CONFIG__ = ${JSON.stringify(runtimeConfig)};`,
|
|
@@ -601,7 +135,7 @@ function createRuntimeInjectionPlugin(input) {
|
|
|
601
135
|
apply: "serve",
|
|
602
136
|
enforce: "pre",
|
|
603
137
|
configResolved(config) {
|
|
604
|
-
root =
|
|
138
|
+
root = path.resolve(config.root);
|
|
605
139
|
viteVersion = readConsumerViteVersion(root);
|
|
606
140
|
},
|
|
607
141
|
resolveId(id, importer) {
|
|
@@ -639,1567 +173,11 @@ function createRuntimeInjectionPlugin(input) {
|
|
|
639
173
|
}
|
|
640
174
|
|
|
641
175
|
// src/server/server-plugin.ts
|
|
642
|
-
import
|
|
643
|
-
|
|
644
|
-
// src/agent/job-manager.ts
|
|
645
|
-
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
646
|
-
import {
|
|
647
|
-
applyPreparedAgentChange,
|
|
648
|
-
executeAgentChange,
|
|
649
|
-
inspectAgentWorkspace,
|
|
650
|
-
probeProviderCapability,
|
|
651
|
-
resolveProviderCredential,
|
|
652
|
-
revertPreparedAgentChange
|
|
653
|
-
} from "@spotpatch/agent";
|
|
654
|
-
import {
|
|
655
|
-
ERROR_CODES,
|
|
656
|
-
SpotPatchError
|
|
657
|
-
} from "@spotpatch/shared";
|
|
658
|
-
var MAX_RETAINED_JOBS = 32;
|
|
659
|
-
var MAX_RETAINED_EVENTS = 512;
|
|
660
|
-
var JOB_ID_PATTERN = /^[A-Za-z0-9_-]{22,128}$/;
|
|
661
|
-
var ACTIVE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
662
|
-
"queued",
|
|
663
|
-
"preparing",
|
|
664
|
-
"running",
|
|
665
|
-
"validating",
|
|
666
|
-
"awaiting-review",
|
|
667
|
-
"applying",
|
|
668
|
-
"cancelling",
|
|
669
|
-
"reverting"
|
|
670
|
-
]);
|
|
671
|
-
var CANCELLABLE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
672
|
-
"queued",
|
|
673
|
-
"preparing",
|
|
674
|
-
"running",
|
|
675
|
-
"validating",
|
|
676
|
-
"awaiting-review"
|
|
677
|
-
]);
|
|
678
|
-
var PRUNABLE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
679
|
-
"completed",
|
|
680
|
-
"cancelled",
|
|
681
|
-
"reverted",
|
|
682
|
-
"failed"
|
|
683
|
-
]);
|
|
684
|
-
var DEFAULT_DEPENDENCIES = Object.freeze({
|
|
685
|
-
applyChange: applyPreparedAgentChange,
|
|
686
|
-
createJobId: () => randomBytes2(16).toString("base64url"),
|
|
687
|
-
executeChange: executeAgentChange,
|
|
688
|
-
inspectWorkspace: inspectAgentWorkspace,
|
|
689
|
-
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
690
|
-
probeCapability: probeProviderCapability,
|
|
691
|
-
resolveCredential: resolveProviderCredential,
|
|
692
|
-
revertChange: revertPreparedAgentChange
|
|
693
|
-
});
|
|
694
|
-
function normalizeError(error) {
|
|
695
|
-
return error instanceof SpotPatchError ? error : new SpotPatchError(ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
|
|
696
|
-
}
|
|
697
|
-
function isActive(status) {
|
|
698
|
-
return ACTIVE_JOB_STATUSES.has(status);
|
|
699
|
-
}
|
|
700
|
-
function snapshot(job) {
|
|
701
|
-
const base = {
|
|
702
|
-
jobId: job.id,
|
|
703
|
-
status: job.status,
|
|
704
|
-
providerProfileId: job.provider.id,
|
|
705
|
-
providerLabel: job.provider.label,
|
|
706
|
-
modelProfileId: job.model.id,
|
|
707
|
-
modelLabel: job.model.label,
|
|
708
|
-
phaseMessage: job.phaseMessage,
|
|
709
|
-
createdAt: job.createdAt,
|
|
710
|
-
updatedAt: job.updatedAt,
|
|
711
|
-
canCancel: CANCELLABLE_JOB_STATUSES.has(job.status),
|
|
712
|
-
canApply: job.status === "awaiting-review" && job.preparedChange?.validationPassed === true && job.result !== void 0 && job.result.diff.length > 0,
|
|
713
|
-
canRevert: job.status === "applied"
|
|
714
|
-
};
|
|
715
|
-
return Object.freeze(
|
|
716
|
-
job.errorCode === void 0 ? base : { ...base, errorCode: job.errorCode }
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
function capabilityCacheKey(provider, model) {
|
|
720
|
-
const configurationDigest = createHash("sha256").update(provider.baseURL).update("\0").update(provider.protocol).update("\0").update(provider.authentication).digest("hex");
|
|
721
|
-
return `${provider.id}:${model.id}:${configurationDigest}`;
|
|
722
|
-
}
|
|
723
|
-
function freezeEvent(event) {
|
|
724
|
-
return Object.freeze(event);
|
|
725
|
-
}
|
|
726
|
-
function createAgentJobManager(options) {
|
|
727
|
-
const dependencies = Object.freeze({
|
|
728
|
-
...DEFAULT_DEPENDENCIES,
|
|
729
|
-
...options.dependencies
|
|
730
|
-
});
|
|
731
|
-
const jobs = /* @__PURE__ */ new Map();
|
|
732
|
-
const capabilityCache = /* @__PURE__ */ new Map();
|
|
733
|
-
const providerConsents = /* @__PURE__ */ new Set();
|
|
734
|
-
let closed = false;
|
|
735
|
-
const resolveSelection = (providerProfileId, modelProfileId) => {
|
|
736
|
-
const provider = options.ai.providers[providerProfileId];
|
|
737
|
-
if (provider === void 0) {
|
|
738
|
-
throw new SpotPatchError(ERROR_CODES.PROVIDER_NOT_CONFIGURED);
|
|
739
|
-
}
|
|
740
|
-
const model = provider.models[modelProfileId];
|
|
741
|
-
if (model === void 0) {
|
|
742
|
-
throw new SpotPatchError(ERROR_CODES.MODEL_NOT_ALLOWED);
|
|
743
|
-
}
|
|
744
|
-
const credential = dependencies.resolveCredential(
|
|
745
|
-
provider.apiKeyEnv,
|
|
746
|
-
options.environment
|
|
747
|
-
);
|
|
748
|
-
return Object.freeze({ credential, model, provider });
|
|
749
|
-
};
|
|
750
|
-
const requireJob = (jobId) => {
|
|
751
|
-
if (!JOB_ID_PATTERN.test(jobId)) {
|
|
752
|
-
throw new SpotPatchError(ERROR_CODES.INVALID_REQUEST);
|
|
753
|
-
}
|
|
754
|
-
const job = jobs.get(jobId);
|
|
755
|
-
if (job === void 0) {
|
|
756
|
-
throw new SpotPatchError(ERROR_CODES.INVALID_REQUEST);
|
|
757
|
-
}
|
|
758
|
-
return job;
|
|
759
|
-
};
|
|
760
|
-
const appendEvent = (job, event) => {
|
|
761
|
-
job.events.push(event);
|
|
762
|
-
if (job.events.length > MAX_RETAINED_EVENTS) {
|
|
763
|
-
job.events.splice(0, job.events.length - MAX_RETAINED_EVENTS);
|
|
764
|
-
}
|
|
765
|
-
for (const listener of job.listeners) {
|
|
766
|
-
listener(event);
|
|
767
|
-
}
|
|
768
|
-
};
|
|
769
|
-
const eventBase = (job) => {
|
|
770
|
-
job.sequence += 1;
|
|
771
|
-
return {
|
|
772
|
-
schemaVersion: 1,
|
|
773
|
-
sequence: job.sequence,
|
|
774
|
-
jobId: job.id,
|
|
775
|
-
status: job.status,
|
|
776
|
-
timestamp: dependencies.now()
|
|
777
|
-
};
|
|
778
|
-
};
|
|
779
|
-
const emitSnapshot = (job) => {
|
|
780
|
-
appendEvent(
|
|
781
|
-
job,
|
|
782
|
-
freezeEvent({
|
|
783
|
-
...eventBase(job),
|
|
784
|
-
type: "snapshot",
|
|
785
|
-
data: Object.freeze({ snapshot: snapshot(job) })
|
|
786
|
-
})
|
|
787
|
-
);
|
|
788
|
-
};
|
|
789
|
-
const emitPhase = (job, message) => {
|
|
790
|
-
appendEvent(
|
|
791
|
-
job,
|
|
792
|
-
freezeEvent({
|
|
793
|
-
...eventBase(job),
|
|
794
|
-
type: "phase",
|
|
795
|
-
data: Object.freeze({ message })
|
|
796
|
-
})
|
|
797
|
-
);
|
|
798
|
-
};
|
|
799
|
-
const emitError = (job, code) => {
|
|
800
|
-
appendEvent(
|
|
801
|
-
job,
|
|
802
|
-
freezeEvent({
|
|
803
|
-
...eventBase(job),
|
|
804
|
-
type: "error",
|
|
805
|
-
data: Object.freeze({ code, message: "The Agent job failed." })
|
|
806
|
-
})
|
|
807
|
-
);
|
|
808
|
-
};
|
|
809
|
-
const transition = (job, status, phaseMessage, errorCode) => {
|
|
810
|
-
job.status = status;
|
|
811
|
-
job.phaseMessage = phaseMessage;
|
|
812
|
-
job.errorCode = errorCode;
|
|
813
|
-
job.updatedAt = dependencies.now();
|
|
814
|
-
emitSnapshot(job);
|
|
815
|
-
emitPhase(job, phaseMessage);
|
|
816
|
-
};
|
|
817
|
-
const probeResolved = async (selection, signal) => {
|
|
818
|
-
const key = capabilityCacheKey(selection.provider, selection.model);
|
|
819
|
-
const cached = capabilityCache.get(key);
|
|
820
|
-
if (cached !== void 0) {
|
|
821
|
-
return cached;
|
|
822
|
-
}
|
|
823
|
-
const capability = await dependencies.probeCapability({
|
|
824
|
-
provider: selection.provider,
|
|
825
|
-
modelProfileId: selection.model.id,
|
|
826
|
-
limits: options.ai.execution.limits,
|
|
827
|
-
credential: selection.credential,
|
|
828
|
-
signal,
|
|
829
|
-
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
830
|
-
});
|
|
831
|
-
if (capability.state !== "agent-ready") {
|
|
832
|
-
throw new SpotPatchError(ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED);
|
|
833
|
-
}
|
|
834
|
-
capabilityCache.set(key, capability);
|
|
835
|
-
return capability;
|
|
836
|
-
};
|
|
837
|
-
const finishWithError = (job, error) => {
|
|
838
|
-
const normalized = normalizeError(error);
|
|
839
|
-
const cancelled = job.controller.signal.aborted || normalized.code === ERROR_CODES.AGENT_CANCELLED;
|
|
840
|
-
transition(
|
|
841
|
-
job,
|
|
842
|
-
cancelled ? "cancelled" : "failed",
|
|
843
|
-
cancelled ? "Agent job cancelled." : "Agent job failed.",
|
|
844
|
-
cancelled ? ERROR_CODES.AGENT_CANCELLED : normalized.code
|
|
845
|
-
);
|
|
846
|
-
if (!cancelled) {
|
|
847
|
-
emitError(job, normalized.code);
|
|
848
|
-
}
|
|
849
|
-
};
|
|
850
|
-
const applyChange = async (job, preparedChange) => {
|
|
851
|
-
transition(job, "applying", "Applying validated changes to the project.");
|
|
852
|
-
try {
|
|
853
|
-
await dependencies.applyChange(preparedChange);
|
|
854
|
-
transition(job, "applied", "Changes were applied to local project files.");
|
|
855
|
-
} catch (error) {
|
|
856
|
-
const normalized = normalizeError(error);
|
|
857
|
-
transition(job, "failed", "Agent change could not be applied.", normalized.code);
|
|
858
|
-
emitError(job, normalized.code);
|
|
859
|
-
throw normalized;
|
|
860
|
-
}
|
|
861
|
-
};
|
|
862
|
-
const runJob = async (job) => {
|
|
863
|
-
try {
|
|
864
|
-
transition(job, "preparing", "Verifying provider and model capabilities.");
|
|
865
|
-
await probeResolved(
|
|
866
|
-
Object.freeze({
|
|
867
|
-
credential: job.credential,
|
|
868
|
-
model: job.model,
|
|
869
|
-
provider: job.provider
|
|
870
|
-
}),
|
|
871
|
-
job.controller.signal
|
|
872
|
-
);
|
|
873
|
-
const callbacks = {
|
|
874
|
-
onCheck(result) {
|
|
875
|
-
appendEvent(
|
|
876
|
-
job,
|
|
877
|
-
freezeEvent({
|
|
878
|
-
...eventBase(job),
|
|
879
|
-
type: "check",
|
|
880
|
-
data: Object.freeze({ result })
|
|
881
|
-
})
|
|
882
|
-
);
|
|
883
|
-
},
|
|
884
|
-
onPhase(event) {
|
|
885
|
-
transition(job, event.phase, event.message);
|
|
886
|
-
},
|
|
887
|
-
onTool(event) {
|
|
888
|
-
appendEvent(
|
|
889
|
-
job,
|
|
890
|
-
freezeEvent({
|
|
891
|
-
...eventBase(job),
|
|
892
|
-
type: "tool",
|
|
893
|
-
data: Object.freeze({ ...event })
|
|
894
|
-
})
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
};
|
|
898
|
-
const preparedChange = await dependencies.executeChange({
|
|
899
|
-
annotation: job.annotation,
|
|
900
|
-
callbacks,
|
|
901
|
-
credential: job.credential,
|
|
902
|
-
execution: options.ai.execution,
|
|
903
|
-
jobId: job.id,
|
|
904
|
-
model: job.model,
|
|
905
|
-
provider: job.provider,
|
|
906
|
-
root: options.root,
|
|
907
|
-
signal: job.controller.signal,
|
|
908
|
-
workingTreeMode: job.workingTreeMode,
|
|
909
|
-
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
910
|
-
});
|
|
911
|
-
job.preparedChange = preparedChange;
|
|
912
|
-
job.result = preparedChange.result;
|
|
913
|
-
appendEvent(
|
|
914
|
-
job,
|
|
915
|
-
freezeEvent({
|
|
916
|
-
...eventBase(job),
|
|
917
|
-
type: "result-ready",
|
|
918
|
-
data: Object.freeze({ hasResult: true })
|
|
919
|
-
})
|
|
920
|
-
);
|
|
921
|
-
if (!preparedChange.validationPassed) {
|
|
922
|
-
transition(
|
|
923
|
-
job,
|
|
924
|
-
"failed",
|
|
925
|
-
"Required validation checks failed.",
|
|
926
|
-
ERROR_CODES.VALIDATION_FAILED
|
|
927
|
-
);
|
|
928
|
-
emitError(job, ERROR_CODES.VALIDATION_FAILED);
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
if (preparedChange.result.diff.length === 0) {
|
|
932
|
-
transition(job, "completed", "No source changes were proposed.");
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
if (options.ai.execution.applyMode === "auto" && preparedChange.autoApplyEligible) {
|
|
936
|
-
try {
|
|
937
|
-
await applyChange(job, preparedChange);
|
|
938
|
-
} catch {
|
|
939
|
-
}
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
transition(job, "awaiting-review", "Validated changes are ready for review.");
|
|
943
|
-
} catch (error) {
|
|
944
|
-
finishWithError(job, error);
|
|
945
|
-
}
|
|
946
|
-
};
|
|
947
|
-
const hasActiveJob = (excludedJobId) => [...jobs.values()].some((job) => job.id !== excludedJobId && isActive(job.status));
|
|
948
|
-
const pruneJobs = () => {
|
|
949
|
-
if (jobs.size < MAX_RETAINED_JOBS) {
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
for (const [jobId, job] of jobs) {
|
|
953
|
-
if (PRUNABLE_JOB_STATUSES.has(job.status)) {
|
|
954
|
-
jobs.delete(jobId);
|
|
955
|
-
}
|
|
956
|
-
if (jobs.size < MAX_RETAINED_JOBS) {
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
};
|
|
961
|
-
return Object.freeze({
|
|
962
|
-
async apply(jobId) {
|
|
963
|
-
const job = requireJob(jobId);
|
|
964
|
-
if (job.status !== "awaiting-review" || job.preparedChange === void 0 || !job.preparedChange.validationPassed || job.result?.diff.length === 0) {
|
|
965
|
-
throw new SpotPatchError(ERROR_CODES.PATCH_REJECTED);
|
|
966
|
-
}
|
|
967
|
-
await applyChange(job, job.preparedChange);
|
|
968
|
-
return snapshot(job);
|
|
969
|
-
},
|
|
970
|
-
cancel(jobId) {
|
|
971
|
-
const job = requireJob(jobId);
|
|
972
|
-
if (!CANCELLABLE_JOB_STATUSES.has(job.status)) {
|
|
973
|
-
return snapshot(job);
|
|
974
|
-
}
|
|
975
|
-
if (job.status === "awaiting-review") {
|
|
976
|
-
job.preparedChange = void 0;
|
|
977
|
-
job.controller.abort("agent-review-cancelled");
|
|
978
|
-
transition(
|
|
979
|
-
job,
|
|
980
|
-
"cancelled",
|
|
981
|
-
"Agent review was closed without applying changes.",
|
|
982
|
-
ERROR_CODES.AGENT_CANCELLED
|
|
983
|
-
);
|
|
984
|
-
return snapshot(job);
|
|
985
|
-
}
|
|
986
|
-
transition(job, "cancelling", "Cancelling Agent job.");
|
|
987
|
-
job.controller.abort("agent-job-cancelled");
|
|
988
|
-
return snapshot(job);
|
|
989
|
-
},
|
|
990
|
-
async close() {
|
|
991
|
-
if (closed) {
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
closed = true;
|
|
995
|
-
for (const job of jobs.values()) {
|
|
996
|
-
if (CANCELLABLE_JOB_STATUSES.has(job.status)) {
|
|
997
|
-
job.controller.abort("vite-server-closed");
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
await Promise.allSettled(
|
|
1001
|
-
[...jobs.values()].map((job) => job.runPromise).filter((promise) => promise !== void 0)
|
|
1002
|
-
);
|
|
1003
|
-
capabilityCache.clear();
|
|
1004
|
-
providerConsents.clear();
|
|
1005
|
-
jobs.clear();
|
|
1006
|
-
},
|
|
1007
|
-
create(request) {
|
|
1008
|
-
if (closed) {
|
|
1009
|
-
throw new SpotPatchError(ERROR_CODES.AI_DISABLED);
|
|
1010
|
-
}
|
|
1011
|
-
if (hasActiveJob()) {
|
|
1012
|
-
throw new SpotPatchError(ERROR_CODES.AGENT_BUSY);
|
|
1013
|
-
}
|
|
1014
|
-
pruneJobs();
|
|
1015
|
-
if (jobs.size >= MAX_RETAINED_JOBS) {
|
|
1016
|
-
throw new SpotPatchError(ERROR_CODES.AGENT_BUSY);
|
|
1017
|
-
}
|
|
1018
|
-
const selection = resolveSelection(
|
|
1019
|
-
request.providerProfileId,
|
|
1020
|
-
request.modelProfileId
|
|
1021
|
-
);
|
|
1022
|
-
providerConsents.add(selection.provider.id);
|
|
1023
|
-
const id = dependencies.createJobId();
|
|
1024
|
-
if (!JOB_ID_PATTERN.test(id) || jobs.has(id)) {
|
|
1025
|
-
throw new SpotPatchError(ERROR_CODES.INTERNAL_ERROR);
|
|
1026
|
-
}
|
|
1027
|
-
const timestamp = dependencies.now();
|
|
1028
|
-
const job = {
|
|
1029
|
-
annotation: request.annotation,
|
|
1030
|
-
controller: new AbortController(),
|
|
1031
|
-
createdAt: timestamp,
|
|
1032
|
-
credential: selection.credential,
|
|
1033
|
-
errorCode: void 0,
|
|
1034
|
-
events: [],
|
|
1035
|
-
id,
|
|
1036
|
-
listeners: /* @__PURE__ */ new Set(),
|
|
1037
|
-
model: selection.model,
|
|
1038
|
-
phaseMessage: "Agent job queued.",
|
|
1039
|
-
preparedChange: void 0,
|
|
1040
|
-
provider: selection.provider,
|
|
1041
|
-
result: void 0,
|
|
1042
|
-
runPromise: void 0,
|
|
1043
|
-
sequence: 0,
|
|
1044
|
-
status: "queued",
|
|
1045
|
-
updatedAt: timestamp,
|
|
1046
|
-
workingTreeMode: request.workingTreeMode
|
|
1047
|
-
};
|
|
1048
|
-
jobs.set(id, job);
|
|
1049
|
-
emitSnapshot(job);
|
|
1050
|
-
emitPhase(job, job.phaseMessage);
|
|
1051
|
-
job.runPromise = Promise.resolve().then(async () => runJob(job));
|
|
1052
|
-
return snapshot(job);
|
|
1053
|
-
},
|
|
1054
|
-
events(jobId) {
|
|
1055
|
-
return Object.freeze([...requireJob(jobId).events]);
|
|
1056
|
-
},
|
|
1057
|
-
async probe(request, signal) {
|
|
1058
|
-
if (closed) {
|
|
1059
|
-
throw new SpotPatchError(ERROR_CODES.AI_DISABLED);
|
|
1060
|
-
}
|
|
1061
|
-
return probeResolved(
|
|
1062
|
-
resolveSelection(request.providerProfileId, request.modelProfileId),
|
|
1063
|
-
signal
|
|
1064
|
-
);
|
|
1065
|
-
},
|
|
1066
|
-
result(jobId) {
|
|
1067
|
-
const job = requireJob(jobId);
|
|
1068
|
-
const response = job.result === void 0 ? { snapshot: snapshot(job) } : { snapshot: snapshot(job), result: job.result };
|
|
1069
|
-
return Object.freeze(response);
|
|
1070
|
-
},
|
|
1071
|
-
async revert(jobId) {
|
|
1072
|
-
const job = requireJob(jobId);
|
|
1073
|
-
if (job.status !== "applied" || job.preparedChange === void 0 || hasActiveJob(job.id)) {
|
|
1074
|
-
throw new SpotPatchError(
|
|
1075
|
-
hasActiveJob(job.id) ? ERROR_CODES.AGENT_BUSY : ERROR_CODES.APPLY_CONFLICT
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
1078
|
-
transition(job, "reverting", "Reverting the applied Agent change.");
|
|
1079
|
-
try {
|
|
1080
|
-
await dependencies.revertChange(job.preparedChange);
|
|
1081
|
-
transition(job, "reverted", "The Agent change was safely reverted.");
|
|
1082
|
-
} catch (error) {
|
|
1083
|
-
const normalized = normalizeError(error);
|
|
1084
|
-
transition(
|
|
1085
|
-
job,
|
|
1086
|
-
"applied",
|
|
1087
|
-
"Revert was rejected because project files changed.",
|
|
1088
|
-
normalized.code
|
|
1089
|
-
);
|
|
1090
|
-
emitError(job, normalized.code);
|
|
1091
|
-
throw normalized;
|
|
1092
|
-
}
|
|
1093
|
-
return snapshot(job);
|
|
1094
|
-
},
|
|
1095
|
-
subscribe(jobId, listener) {
|
|
1096
|
-
const job = requireJob(jobId);
|
|
1097
|
-
job.listeners.add(listener);
|
|
1098
|
-
return () => {
|
|
1099
|
-
job.listeners.delete(listener);
|
|
1100
|
-
};
|
|
1101
|
-
},
|
|
1102
|
-
workspaceHealth(signal) {
|
|
1103
|
-
if (closed) {
|
|
1104
|
-
throw new SpotPatchError(ERROR_CODES.AI_DISABLED);
|
|
1105
|
-
}
|
|
1106
|
-
return dependencies.inspectWorkspace(options.root, signal);
|
|
1107
|
-
}
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// src/server/middleware.ts
|
|
1112
|
-
import {
|
|
1113
|
-
ERROR_CODES as ERROR_CODES8,
|
|
1114
|
-
SPOTPATCH_API_BASE,
|
|
1115
|
-
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
|
|
1116
|
-
SpotPatchError as SpotPatchError8,
|
|
1117
|
-
openEditorRequestSchema,
|
|
1118
|
-
sourceContextRequestSchema
|
|
1119
|
-
} from "@spotpatch/shared";
|
|
1120
|
-
|
|
1121
|
-
// src/server/agent-http.ts
|
|
1122
|
-
import {
|
|
1123
|
-
ERROR_CODES as ERROR_CODES6,
|
|
1124
|
-
SPOTPATCH_ENDPOINTS,
|
|
1125
|
-
SpotPatchError as SpotPatchError6,
|
|
1126
|
-
agentCapabilityRequestSchema,
|
|
1127
|
-
agentJobActionRequestSchema,
|
|
1128
|
-
agentJobCreateRequestSchema,
|
|
1129
|
-
agentWorkspaceHealthRequestSchema
|
|
1130
|
-
} from "@spotpatch/shared";
|
|
1131
|
-
|
|
1132
|
-
// src/server/agent-request.ts
|
|
1133
|
-
import { realpath as realpath3 } from "fs/promises";
|
|
1134
|
-
import path5 from "path";
|
|
1135
|
-
import {
|
|
1136
|
-
ERROR_CODES as ERROR_CODES4,
|
|
1137
|
-
SpotPatchError as SpotPatchError4
|
|
1138
|
-
} from "@spotpatch/shared";
|
|
1139
|
-
|
|
1140
|
-
// src/server/source-context.ts
|
|
1141
|
-
import { readFile, realpath as realpath2 } from "fs/promises";
|
|
1142
|
-
import path4 from "path";
|
|
1143
|
-
import {
|
|
1144
|
-
ERROR_CODES as ERROR_CODES3,
|
|
1145
|
-
SpotPatchError as SpotPatchError3
|
|
1146
|
-
} from "@spotpatch/shared";
|
|
1147
|
-
|
|
1148
|
-
// src/server/extract-code-context.ts
|
|
176
|
+
import path2 from "path";
|
|
1149
177
|
import {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
} from "
|
|
1153
|
-
function isComponentName(name) {
|
|
1154
|
-
return /^[A-Z]/u.test(name);
|
|
1155
|
-
}
|
|
1156
|
-
function unwrapTypeExpression(expression) {
|
|
1157
|
-
switch (expression.type) {
|
|
1158
|
-
case "TSAsExpression":
|
|
1159
|
-
case "TSSatisfiesExpression":
|
|
1160
|
-
case "TSTypeAssertion":
|
|
1161
|
-
case "TSNonNullExpression":
|
|
1162
|
-
case "TSInstantiationExpression":
|
|
1163
|
-
return unwrapTypeExpression(expression.expression);
|
|
1164
|
-
default:
|
|
1165
|
-
return expression;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
function calleeName(expression) {
|
|
1169
|
-
const unwrapped = unwrapTypeExpression(expression);
|
|
1170
|
-
if (unwrapped.type === "Identifier") {
|
|
1171
|
-
return unwrapped.name;
|
|
1172
|
-
}
|
|
1173
|
-
if (unwrapped.type === "MemberExpression" && !unwrapped.computed) {
|
|
1174
|
-
return unwrapped.property.type === "Identifier" ? unwrapped.property.name : void 0;
|
|
1175
|
-
}
|
|
1176
|
-
return void 0;
|
|
1177
|
-
}
|
|
1178
|
-
function isFunctionExpression(expression) {
|
|
1179
|
-
const unwrapped = unwrapTypeExpression(expression);
|
|
1180
|
-
return unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "FunctionExpression";
|
|
1181
|
-
}
|
|
1182
|
-
function isSupportedComponentInitializer(expression) {
|
|
1183
|
-
const unwrapped = unwrapTypeExpression(expression);
|
|
1184
|
-
if (isFunctionExpression(unwrapped)) {
|
|
1185
|
-
return true;
|
|
1186
|
-
}
|
|
1187
|
-
if (unwrapped.type !== "CallExpression") {
|
|
1188
|
-
return false;
|
|
1189
|
-
}
|
|
1190
|
-
const name = calleeName(unwrapped.callee);
|
|
1191
|
-
if (name !== "memo" && name !== "forwardRef") {
|
|
1192
|
-
return false;
|
|
1193
|
-
}
|
|
1194
|
-
const firstArgument = unwrapped.arguments[0];
|
|
1195
|
-
return firstArgument !== void 0 && firstArgument.type !== "SpreadElement" && (isFunctionExpression(firstArgument) || isSupportedComponentInitializer(firstArgument));
|
|
1196
|
-
}
|
|
1197
|
-
function variableComponent(node) {
|
|
1198
|
-
if (node.id.type !== "Identifier" || !isComponentName(node.id.name) || node.init === null || !isSupportedComponentInitializer(node.init)) {
|
|
1199
|
-
return void 0;
|
|
1200
|
-
}
|
|
1201
|
-
return Object.freeze({ start: node.start, end: node.end, name: node.id.name });
|
|
1202
|
-
}
|
|
1203
|
-
function functionComponent(node) {
|
|
1204
|
-
return node.id !== null && isComponentName(node.id.name) && node.body !== null ? Object.freeze({ start: node.start, end: node.end, name: node.id.name }) : void 0;
|
|
1205
|
-
}
|
|
1206
|
-
function isReactComponentSuperclass(expression) {
|
|
1207
|
-
if (expression === null) {
|
|
1208
|
-
return false;
|
|
1209
|
-
}
|
|
1210
|
-
const unwrapped = unwrapTypeExpression(expression);
|
|
1211
|
-
if (unwrapped.type === "Identifier") {
|
|
1212
|
-
return unwrapped.name === "Component" || unwrapped.name === "PureComponent";
|
|
1213
|
-
}
|
|
1214
|
-
return unwrapped.type === "MemberExpression" && !unwrapped.computed && unwrapped.object.type === "Identifier" && unwrapped.object.name === "React" && (unwrapped.property.name === "Component" || unwrapped.property.name === "PureComponent");
|
|
1215
|
-
}
|
|
1216
|
-
function classComponent(node) {
|
|
1217
|
-
return node.id !== null && isComponentName(node.id.name) && isReactComponentSuperclass(node.superClass) ? Object.freeze({ start: node.start, end: node.end, name: node.id.name }) : void 0;
|
|
1218
|
-
}
|
|
1219
|
-
function selectedOffset(source, line, column) {
|
|
1220
|
-
const lines = source.split(/\r?\n/u);
|
|
1221
|
-
if (line < 1 || line > lines.length) {
|
|
1222
|
-
return void 0;
|
|
1223
|
-
}
|
|
1224
|
-
const lineStart = lines.slice(0, line - 1).reduce((total, value) => total + value.length + 1, 0);
|
|
1225
|
-
const lineLength = lines[line - 1]?.length ?? 0;
|
|
1226
|
-
return lineStart + Math.min(Math.max(0, column - 1), lineLength);
|
|
1227
|
-
}
|
|
1228
|
-
function findComponentSpan(options) {
|
|
1229
|
-
const offset = selectedOffset(options.source, options.line, options.column);
|
|
1230
|
-
if (offset === void 0) {
|
|
1231
|
-
return void 0;
|
|
1232
|
-
}
|
|
1233
|
-
let parseResult;
|
|
1234
|
-
try {
|
|
1235
|
-
parseResult = parseSync(options.sourcePath, options.source, {
|
|
1236
|
-
sourceType: "module"
|
|
1237
|
-
});
|
|
1238
|
-
} catch {
|
|
1239
|
-
return void 0;
|
|
1240
|
-
}
|
|
1241
|
-
if (parseResult.errors.length > 0) {
|
|
1242
|
-
return void 0;
|
|
1243
|
-
}
|
|
1244
|
-
const jsxNodes = [];
|
|
1245
|
-
const components = [];
|
|
1246
|
-
const visitor = new Visitor({
|
|
1247
|
-
JSXElement(node) {
|
|
1248
|
-
jsxNodes.push(node);
|
|
1249
|
-
},
|
|
1250
|
-
JSXFragment(node) {
|
|
1251
|
-
jsxNodes.push(node);
|
|
1252
|
-
},
|
|
1253
|
-
FunctionDeclaration(node) {
|
|
1254
|
-
const candidate = functionComponent(node);
|
|
1255
|
-
if (candidate !== void 0) {
|
|
1256
|
-
components.push(candidate);
|
|
1257
|
-
}
|
|
1258
|
-
},
|
|
1259
|
-
VariableDeclarator(node) {
|
|
1260
|
-
const candidate = variableComponent(node);
|
|
1261
|
-
if (candidate !== void 0) {
|
|
1262
|
-
components.push(candidate);
|
|
1263
|
-
}
|
|
1264
|
-
},
|
|
1265
|
-
ClassDeclaration(node) {
|
|
1266
|
-
const candidate = classComponent(node);
|
|
1267
|
-
if (candidate !== void 0) {
|
|
1268
|
-
components.push(candidate);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
});
|
|
1272
|
-
visitor.visit(parseResult.program);
|
|
1273
|
-
const selectedJsx = jsxNodes.filter((node) => node.start <= offset && node.end >= offset).sort((left, right) => left.end - left.start - (right.end - right.start))[0];
|
|
1274
|
-
if (selectedJsx === void 0) {
|
|
1275
|
-
return void 0;
|
|
1276
|
-
}
|
|
1277
|
-
return components.filter(
|
|
1278
|
-
(component) => component.start <= selectedJsx.start && component.end >= selectedJsx.end
|
|
1279
|
-
).sort((left, right) => left.end - left.start - (right.end - right.start))[0];
|
|
1280
|
-
}
|
|
1281
|
-
function lineAtOffset(source, offset) {
|
|
1282
|
-
let line = 1;
|
|
1283
|
-
for (let index = 0; index < offset; index += 1) {
|
|
1284
|
-
if (source[index] === "\n") {
|
|
1285
|
-
line += 1;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
return line;
|
|
1289
|
-
}
|
|
1290
|
-
function componentRange(source, component) {
|
|
1291
|
-
return Object.freeze({
|
|
1292
|
-
startLine: lineAtOffset(source, component.start),
|
|
1293
|
-
endLine: lineAtOffset(source, Math.max(component.start, component.end - 1))
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1296
|
-
function truncateSelectedLine(line, column, maxCharacters) {
|
|
1297
|
-
if (line.length <= maxCharacters) {
|
|
1298
|
-
return line;
|
|
1299
|
-
}
|
|
1300
|
-
if (maxCharacters === 1) {
|
|
1301
|
-
return "\u2026";
|
|
1302
|
-
}
|
|
1303
|
-
const contentCharacters = maxCharacters - 2;
|
|
1304
|
-
const desiredStart = Math.max(0, column - 1 - Math.floor(contentCharacters / 2));
|
|
1305
|
-
const start = Math.min(desiredStart, line.length - contentCharacters);
|
|
1306
|
-
const end = start + contentCharacters;
|
|
1307
|
-
return `${start > 0 ? "\u2026" : ""}${line.slice(start, end)}${end < line.length ? "\u2026" : ""}`.slice(
|
|
1308
|
-
0,
|
|
1309
|
-
maxCharacters
|
|
1310
|
-
);
|
|
1311
|
-
}
|
|
1312
|
-
function boundedRange(lines, selectedLine, column, initialStart, initialEnd, maxCharacters) {
|
|
1313
|
-
let startLine = initialStart;
|
|
1314
|
-
let endLine = initialEnd;
|
|
1315
|
-
let excerpt = lines.slice(startLine - 1, endLine).join("\n");
|
|
1316
|
-
while (excerpt.length > maxCharacters && startLine < endLine) {
|
|
1317
|
-
if (endLine - selectedLine >= selectedLine - startLine) {
|
|
1318
|
-
endLine -= 1;
|
|
1319
|
-
} else {
|
|
1320
|
-
startLine += 1;
|
|
1321
|
-
}
|
|
1322
|
-
excerpt = lines.slice(startLine - 1, endLine).join("\n");
|
|
1323
|
-
}
|
|
1324
|
-
if (excerpt.length > maxCharacters) {
|
|
1325
|
-
startLine = selectedLine;
|
|
1326
|
-
endLine = selectedLine;
|
|
1327
|
-
excerpt = truncateSelectedLine(
|
|
1328
|
-
lines[selectedLine - 1] ?? "",
|
|
1329
|
-
column,
|
|
1330
|
-
maxCharacters
|
|
1331
|
-
);
|
|
1332
|
-
}
|
|
1333
|
-
return Object.freeze({ startLine, endLine, excerpt });
|
|
1334
|
-
}
|
|
1335
|
-
function nearbyContext(options) {
|
|
1336
|
-
const lines = options.source.split(/\r?\n/u);
|
|
1337
|
-
const initialStart = Math.max(1, options.line - Math.floor(options.maxLines / 2));
|
|
1338
|
-
const initialEnd = Math.min(lines.length, initialStart + options.maxLines - 1);
|
|
1339
|
-
const startLine = Math.max(1, initialEnd - options.maxLines + 1);
|
|
1340
|
-
const bounded = boundedRange(
|
|
1341
|
-
lines,
|
|
1342
|
-
options.line,
|
|
1343
|
-
options.column,
|
|
1344
|
-
startLine,
|
|
1345
|
-
initialEnd,
|
|
1346
|
-
options.maxCharacters
|
|
1347
|
-
);
|
|
1348
|
-
return Object.freeze({
|
|
1349
|
-
relativePath: options.relativePath,
|
|
1350
|
-
language: options.language,
|
|
1351
|
-
startLine: bounded.startLine,
|
|
1352
|
-
endLine: bounded.endLine,
|
|
1353
|
-
excerpt: bounded.excerpt,
|
|
1354
|
-
boundary: "nearby-lines"
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
function extractCodeContext(options) {
|
|
1358
|
-
const component = findComponentSpan(options);
|
|
1359
|
-
if (component !== void 0) {
|
|
1360
|
-
const range = componentRange(options.source, component);
|
|
1361
|
-
const lineCount = range.endLine - range.startLine + 1;
|
|
1362
|
-
const excerpt = options.source.split(/\r?\n/u).slice(range.startLine - 1, range.endLine).join("\n");
|
|
1363
|
-
if (lineCount <= options.maxLines && excerpt.length <= options.maxCharacters) {
|
|
1364
|
-
return Object.freeze({
|
|
1365
|
-
relativePath: options.relativePath,
|
|
1366
|
-
language: options.language,
|
|
1367
|
-
startLine: range.startLine,
|
|
1368
|
-
endLine: range.endLine,
|
|
1369
|
-
excerpt,
|
|
1370
|
-
boundary: "component"
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
return nearbyContext(options);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// src/server/source-file.ts
|
|
1378
|
-
import { realpath, stat } from "fs/promises";
|
|
1379
|
-
import path3 from "path";
|
|
1380
|
-
import { ERROR_CODES as ERROR_CODES2, SpotPatchError as SpotPatchError2 } from "@spotpatch/shared";
|
|
1381
|
-
|
|
1382
|
-
// src/server/constants.ts
|
|
1383
|
-
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
1384
|
-
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
1385
|
-
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
1386
|
-
|
|
1387
|
-
// src/server/source-file.ts
|
|
1388
|
-
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
1389
|
-
function isMissingFileError(error) {
|
|
1390
|
-
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
1391
|
-
}
|
|
1392
|
-
async function assertInsideRoot(root, candidate) {
|
|
1393
|
-
let realRoot;
|
|
1394
|
-
let realCandidate;
|
|
1395
|
-
try {
|
|
1396
|
-
[realRoot, realCandidate] = await Promise.all([
|
|
1397
|
-
realpath(root),
|
|
1398
|
-
realpath(candidate)
|
|
1399
|
-
]);
|
|
1400
|
-
} catch (error) {
|
|
1401
|
-
if (isMissingFileError(error)) {
|
|
1402
|
-
throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
|
|
1403
|
-
cause: error
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
throw error;
|
|
1407
|
-
}
|
|
1408
|
-
const relative = path3.relative(realRoot, realCandidate);
|
|
1409
|
-
const outside = relative.startsWith(`..${path3.sep}`) || relative === ".." || path3.isAbsolute(relative);
|
|
1410
|
-
if (outside) {
|
|
1411
|
-
throw new SpotPatchError2(ERROR_CODES2.SOURCE_OUTSIDE_ROOT);
|
|
1412
|
-
}
|
|
1413
|
-
return realCandidate;
|
|
1414
|
-
}
|
|
1415
|
-
async function resolveSourceFile(options) {
|
|
1416
|
-
const registeredPath = options.registry.resolve(options.fileId);
|
|
1417
|
-
if (registeredPath === void 0) {
|
|
1418
|
-
throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
|
|
1419
|
-
}
|
|
1420
|
-
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
1421
|
-
if (!ALLOWED_EXTENSIONS.has(path3.extname(sourcePath).toLowerCase())) {
|
|
1422
|
-
throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
|
|
1423
|
-
}
|
|
1424
|
-
let sourceStat;
|
|
1425
|
-
try {
|
|
1426
|
-
sourceStat = await stat(sourcePath);
|
|
1427
|
-
} catch (error) {
|
|
1428
|
-
if (isMissingFileError(error)) {
|
|
1429
|
-
throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
|
|
1430
|
-
cause: error
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
throw error;
|
|
1434
|
-
}
|
|
1435
|
-
if (!sourceStat.isFile()) {
|
|
1436
|
-
throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
|
|
1437
|
-
}
|
|
1438
|
-
if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
|
|
1439
|
-
throw new SpotPatchError2(ERROR_CODES2.SOURCE_TOO_LARGE);
|
|
1440
|
-
}
|
|
1441
|
-
return sourcePath;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
// src/server/source-context.ts
|
|
1445
|
-
function toDisplayPath(root, sourcePath) {
|
|
1446
|
-
return path4.relative(root, sourcePath).split(path4.sep).join("/");
|
|
1447
|
-
}
|
|
1448
|
-
async function readSourceContext(options) {
|
|
1449
|
-
const sourcePath = await resolveSourceFile({
|
|
1450
|
-
fileId: options.request.fileId,
|
|
1451
|
-
registry: options.registry,
|
|
1452
|
-
root: options.root
|
|
1453
|
-
});
|
|
1454
|
-
let source;
|
|
1455
|
-
try {
|
|
1456
|
-
source = await readFile(sourcePath, "utf8");
|
|
1457
|
-
} catch (error) {
|
|
1458
|
-
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1459
|
-
throw new SpotPatchError3(ERROR_CODES3.SOURCE_NOT_FOUND, void 0, {
|
|
1460
|
-
cause: error
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
|
-
throw error;
|
|
1464
|
-
}
|
|
1465
|
-
const lines = source.split(/\r?\n/);
|
|
1466
|
-
if (options.request.line > lines.length) {
|
|
1467
|
-
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
|
|
1468
|
-
}
|
|
1469
|
-
const extension = path4.extname(sourcePath).toLowerCase();
|
|
1470
|
-
return extractCodeContext({
|
|
1471
|
-
source,
|
|
1472
|
-
sourcePath,
|
|
1473
|
-
relativePath: toDisplayPath(await realpath2(options.root), sourcePath),
|
|
1474
|
-
language: extension === ".tsx" ? "tsx" : "jsx",
|
|
1475
|
-
line: options.request.line,
|
|
1476
|
-
column: options.request.column,
|
|
1477
|
-
maxLines: Math.min(options.request.maxLines, options.maxLines),
|
|
1478
|
-
maxCharacters: options.maxCharacters
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
// src/server/agent-request.ts
|
|
1483
|
-
function compactSourceRef(source) {
|
|
1484
|
-
return Object.freeze({
|
|
1485
|
-
origin: source.origin,
|
|
1486
|
-
confidence: source.confidence,
|
|
1487
|
-
...source.fileId === void 0 ? {} : { fileId: source.fileId },
|
|
1488
|
-
...source.relativePath === void 0 ? {} : { relativePath: source.relativePath },
|
|
1489
|
-
...source.line === void 0 ? {} : { line: source.line },
|
|
1490
|
-
...source.column === void 0 ? {} : { column: source.column }
|
|
1491
|
-
});
|
|
1492
|
-
}
|
|
1493
|
-
async function authorizeSourceRef(source, registry, root) {
|
|
1494
|
-
const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
|
|
1495
|
-
if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
|
|
1496
|
-
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
1497
|
-
}
|
|
1498
|
-
if (source.fileId === void 0) {
|
|
1499
|
-
if (source.origin === "none" && source.relativePath !== void 0) {
|
|
1500
|
-
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
1501
|
-
}
|
|
1502
|
-
return compactSourceRef(source);
|
|
1503
|
-
}
|
|
1504
|
-
const sourcePath = await resolveSourceFile({
|
|
1505
|
-
fileId: source.fileId,
|
|
1506
|
-
registry,
|
|
1507
|
-
root
|
|
1508
|
-
});
|
|
1509
|
-
const relativePath = path5.relative(await realpath3(root), sourcePath).split(path5.sep).join("/");
|
|
1510
|
-
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
1511
|
-
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
1512
|
-
}
|
|
1513
|
-
return Object.freeze({
|
|
1514
|
-
...compactSourceRef(source),
|
|
1515
|
-
relativePath
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
1518
|
-
function freezeMatchedRule(rule) {
|
|
1519
|
-
return Object.freeze({
|
|
1520
|
-
selector: rule.selector,
|
|
1521
|
-
declarations: rule.declarations,
|
|
1522
|
-
...rule.source === void 0 ? {} : { source: rule.source },
|
|
1523
|
-
...rule.media === void 0 ? {} : { media: rule.media }
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
function targetIdentity(target) {
|
|
1527
|
-
const source = target.source;
|
|
1528
|
-
if (source.fileId !== void 0 && source.line !== void 0 && source.column !== void 0) {
|
|
1529
|
-
return `source:${source.fileId}:${String(source.line)}:${String(source.column)}`;
|
|
1530
|
-
}
|
|
1531
|
-
return [
|
|
1532
|
-
"element",
|
|
1533
|
-
source.origin,
|
|
1534
|
-
source.relativePath ?? "",
|
|
1535
|
-
target.element.selector,
|
|
1536
|
-
target.element.sanitizedHtml
|
|
1537
|
-
].join("\0");
|
|
1538
|
-
}
|
|
1539
|
-
async function authorizeTarget(target, input) {
|
|
1540
|
-
const source = await authorizeSourceRef(target.source, input.registry, input.root);
|
|
1541
|
-
const reactSourceInput = target.react.source;
|
|
1542
|
-
const reactSource = reactSourceInput === void 0 ? void 0 : await authorizeSourceRef(reactSourceInput, input.registry, input.root);
|
|
1543
|
-
const marker = source.fileId === void 0 || source.line === void 0 || source.column === void 0 ? void 0 : Object.freeze({
|
|
1544
|
-
fileId: source.fileId,
|
|
1545
|
-
line: source.line,
|
|
1546
|
-
column: source.column,
|
|
1547
|
-
maxLines: input.options.budget.maxCodeLines
|
|
1548
|
-
});
|
|
1549
|
-
if (marker === void 0 && target.code !== void 0) {
|
|
1550
|
-
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
1551
|
-
}
|
|
1552
|
-
const code = marker === void 0 ? void 0 : await readSourceContext({
|
|
1553
|
-
request: marker,
|
|
1554
|
-
registry: input.registry,
|
|
1555
|
-
root: input.root,
|
|
1556
|
-
maxCharacters: input.options.budget.codeCharacters,
|
|
1557
|
-
maxLines: input.options.budget.maxCodeLines
|
|
1558
|
-
});
|
|
1559
|
-
if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
|
|
1560
|
-
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
1561
|
-
}
|
|
1562
|
-
return Object.freeze({
|
|
1563
|
-
instruction: target.instruction,
|
|
1564
|
-
source,
|
|
1565
|
-
react: Object.freeze({
|
|
1566
|
-
supported: target.react.supported,
|
|
1567
|
-
...target.react.version === void 0 ? {} : { version: target.react.version },
|
|
1568
|
-
...target.react.componentName === void 0 ? {} : { componentName: target.react.componentName },
|
|
1569
|
-
componentStack: Object.freeze([...target.react.componentStack]),
|
|
1570
|
-
...reactSource === void 0 ? {} : { source: reactSource }
|
|
1571
|
-
}),
|
|
1572
|
-
element: Object.freeze({
|
|
1573
|
-
tagName: target.element.tagName,
|
|
1574
|
-
selector: target.element.selector,
|
|
1575
|
-
sanitizedHtml: target.element.sanitizedHtml,
|
|
1576
|
-
...target.element.textPreview === void 0 ? {} : { textPreview: target.element.textPreview },
|
|
1577
|
-
...target.element.role === void 0 ? {} : { role: target.element.role },
|
|
1578
|
-
rect: Object.freeze({ ...target.element.rect })
|
|
1579
|
-
}),
|
|
1580
|
-
styles: Object.freeze({
|
|
1581
|
-
classNames: Object.freeze([...target.styles.classNames]),
|
|
1582
|
-
...target.styles.inlineStyle === void 0 ? {} : { inlineStyle: target.styles.inlineStyle },
|
|
1583
|
-
matchedRules: Object.freeze(target.styles.matchedRules.map(freezeMatchedRule)),
|
|
1584
|
-
computed: Object.freeze({ ...target.styles.computed }),
|
|
1585
|
-
warnings: Object.freeze([...target.styles.warnings])
|
|
1586
|
-
}),
|
|
1587
|
-
...code === void 0 ? {} : { code: Object.freeze({ ...code }) },
|
|
1588
|
-
warnings: Object.freeze([...target.warnings])
|
|
1589
|
-
});
|
|
1590
|
-
}
|
|
1591
|
-
async function authorizeAgentJobRequest(input) {
|
|
1592
|
-
const requestedTargets = input.request.annotation.targets;
|
|
1593
|
-
if (requestedTargets.length > input.options.maxTargets) {
|
|
1594
|
-
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
1595
|
-
}
|
|
1596
|
-
const identities = requestedTargets.map(targetIdentity);
|
|
1597
|
-
if (new Set(identities).size !== identities.length) {
|
|
1598
|
-
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
1599
|
-
}
|
|
1600
|
-
const targets = Object.freeze(
|
|
1601
|
-
await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
|
|
1602
|
-
);
|
|
1603
|
-
const annotation = Object.freeze({
|
|
1604
|
-
schemaVersion: 3,
|
|
1605
|
-
id: input.request.annotation.id,
|
|
1606
|
-
locale: input.request.annotation.locale,
|
|
1607
|
-
page: Object.freeze({ ...input.request.annotation.page }),
|
|
1608
|
-
targets,
|
|
1609
|
-
createdAt: input.request.annotation.createdAt
|
|
1610
|
-
});
|
|
1611
|
-
return Object.freeze({
|
|
1612
|
-
annotation,
|
|
1613
|
-
providerProfileId: input.request.providerProfileId,
|
|
1614
|
-
modelProfileId: input.request.modelProfileId,
|
|
1615
|
-
providerDataConsent: true,
|
|
1616
|
-
workingTreeMode: input.request.workingTreeMode
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
// src/server/request-body.ts
|
|
1621
|
-
import { ERROR_CODES as ERROR_CODES5, SpotPatchError as SpotPatchError5 } from "@spotpatch/shared";
|
|
1622
|
-
function isJsonContentType(value) {
|
|
1623
|
-
return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
1624
|
-
}
|
|
1625
|
-
async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
|
|
1626
|
-
if (!isJsonContentType(request.headers["content-type"])) {
|
|
1627
|
-
throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
|
|
1628
|
-
}
|
|
1629
|
-
const declaredLength = Number(request.headers["content-length"]);
|
|
1630
|
-
if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
|
|
1631
|
-
throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
|
|
1632
|
-
}
|
|
1633
|
-
const chunks = [];
|
|
1634
|
-
let byteLength = 0;
|
|
1635
|
-
let exceededLimit = false;
|
|
1636
|
-
for await (const rawChunk of request) {
|
|
1637
|
-
const chunk = rawChunk;
|
|
1638
|
-
if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
|
|
1639
|
-
throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
|
|
1640
|
-
}
|
|
1641
|
-
const buffer = Buffer.from(chunk);
|
|
1642
|
-
byteLength += buffer.byteLength;
|
|
1643
|
-
if (byteLength > maximumBytes) {
|
|
1644
|
-
exceededLimit = true;
|
|
1645
|
-
continue;
|
|
1646
|
-
}
|
|
1647
|
-
chunks.push(buffer);
|
|
1648
|
-
}
|
|
1649
|
-
if (exceededLimit || byteLength === 0) {
|
|
1650
|
-
throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
|
|
1651
|
-
}
|
|
1652
|
-
try {
|
|
1653
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1654
|
-
} catch (error) {
|
|
1655
|
-
throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST, void 0, {
|
|
1656
|
-
cause: error
|
|
1657
|
-
});
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
// src/server/agent-http.ts
|
|
1662
|
-
var AGENT_JOB_ID_PATTERN = /^[A-Za-z0-9_-]{22,128}$/;
|
|
1663
|
-
var AGENT_JOB_ACTIONS = /* @__PURE__ */ new Set([
|
|
1664
|
-
"events",
|
|
1665
|
-
"result",
|
|
1666
|
-
"cancel",
|
|
1667
|
-
"apply",
|
|
1668
|
-
"revert"
|
|
1669
|
-
]);
|
|
1670
|
-
var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
1671
|
-
"awaiting-review",
|
|
1672
|
-
"applied",
|
|
1673
|
-
"completed",
|
|
1674
|
-
"cancelled",
|
|
1675
|
-
"reverted",
|
|
1676
|
-
"failed"
|
|
1677
|
-
]);
|
|
1678
|
-
function matchAgentRequestPath(path11) {
|
|
1679
|
-
if (path11 === SPOTPATCH_ENDPOINTS.agentCapability) {
|
|
1680
|
-
return Object.freeze({ kind: "capability" });
|
|
1681
|
-
}
|
|
1682
|
-
if (path11 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
|
|
1683
|
-
return Object.freeze({ kind: "workspace-health" });
|
|
1684
|
-
}
|
|
1685
|
-
if (path11 === SPOTPATCH_ENDPOINTS.agentJobs) {
|
|
1686
|
-
return Object.freeze({ kind: "create-job" });
|
|
1687
|
-
}
|
|
1688
|
-
const prefix = `${SPOTPATCH_ENDPOINTS.agentJobs}/`;
|
|
1689
|
-
if (!path11.startsWith(prefix)) {
|
|
1690
|
-
return void 0;
|
|
1691
|
-
}
|
|
1692
|
-
const segments = path11.slice(prefix.length).split("/");
|
|
1693
|
-
const jobId = segments[0];
|
|
1694
|
-
const action = segments[1];
|
|
1695
|
-
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
1696
|
-
return void 0;
|
|
1697
|
-
}
|
|
1698
|
-
return Object.freeze({
|
|
1699
|
-
kind: "job-action",
|
|
1700
|
-
action,
|
|
1701
|
-
jobId
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
function requireAgentManager(options) {
|
|
1705
|
-
if (options.agentManager === void 0 || options.options.ai === false) {
|
|
1706
|
-
throw new SpotPatchError6(ERROR_CODES6.AI_DISABLED);
|
|
1707
|
-
}
|
|
1708
|
-
return options.agentManager;
|
|
1709
|
-
}
|
|
1710
|
-
function writeNdjsonEvent(response, event) {
|
|
1711
|
-
response.write(`${JSON.stringify(event)}
|
|
1712
|
-
`);
|
|
1713
|
-
}
|
|
1714
|
-
function streamAgentJobEvents(response, manager, jobId) {
|
|
1715
|
-
const events = manager.events(jobId);
|
|
1716
|
-
const current = manager.result(jobId).snapshot;
|
|
1717
|
-
response.statusCode = 200;
|
|
1718
|
-
response.setHeader("Cache-Control", "no-store");
|
|
1719
|
-
response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
1720
|
-
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1721
|
-
for (const event of events) {
|
|
1722
|
-
writeNdjsonEvent(response, event);
|
|
1723
|
-
}
|
|
1724
|
-
if (EVENT_STREAM_END_STATUSES.has(current.status)) {
|
|
1725
|
-
response.end();
|
|
1726
|
-
return;
|
|
1727
|
-
}
|
|
1728
|
-
let settled = false;
|
|
1729
|
-
let unsubscribe = () => void 0;
|
|
1730
|
-
const heartbeat = setInterval(() => {
|
|
1731
|
-
if (!settled) {
|
|
1732
|
-
response.write("\n");
|
|
1733
|
-
}
|
|
1734
|
-
}, 15e3);
|
|
1735
|
-
heartbeat.unref();
|
|
1736
|
-
const cleanup = () => {
|
|
1737
|
-
if (settled) {
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
settled = true;
|
|
1741
|
-
clearInterval(heartbeat);
|
|
1742
|
-
unsubscribe();
|
|
1743
|
-
};
|
|
1744
|
-
unsubscribe = manager.subscribe(jobId, (event) => {
|
|
1745
|
-
if (settled) {
|
|
1746
|
-
return;
|
|
1747
|
-
}
|
|
1748
|
-
writeNdjsonEvent(response, event);
|
|
1749
|
-
if (event.type === "snapshot" && EVENT_STREAM_END_STATUSES.has(event.data.snapshot.status)) {
|
|
1750
|
-
cleanup();
|
|
1751
|
-
response.end();
|
|
1752
|
-
}
|
|
1753
|
-
});
|
|
1754
|
-
response.once("close", cleanup);
|
|
1755
|
-
response.once("error", cleanup);
|
|
1756
|
-
}
|
|
1757
|
-
async function handleCapability(request, response, options, writeSuccess) {
|
|
1758
|
-
if (request.method !== "POST") {
|
|
1759
|
-
throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
|
|
1760
|
-
}
|
|
1761
|
-
const parsed = agentCapabilityRequestSchema.safeParse(
|
|
1762
|
-
await readJsonRequestBody(request)
|
|
1763
|
-
);
|
|
1764
|
-
if (!parsed.success) {
|
|
1765
|
-
throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
|
|
1766
|
-
}
|
|
1767
|
-
const controller = new AbortController();
|
|
1768
|
-
const abort = () => {
|
|
1769
|
-
controller.abort("agent-capability-client-disconnected");
|
|
1770
|
-
};
|
|
1771
|
-
response.once("close", abort);
|
|
1772
|
-
try {
|
|
1773
|
-
const data = await requireAgentManager(options).probe(
|
|
1774
|
-
parsed.data,
|
|
1775
|
-
controller.signal
|
|
1776
|
-
);
|
|
1777
|
-
writeSuccess(response, 200, data);
|
|
1778
|
-
} finally {
|
|
1779
|
-
response.removeListener("close", abort);
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
async function handleCreateJob(request, response, options, writeSuccess) {
|
|
1783
|
-
if (request.method !== "POST") {
|
|
1784
|
-
throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
|
|
1785
|
-
}
|
|
1786
|
-
const parsed = agentJobCreateRequestSchema.safeParse(
|
|
1787
|
-
await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
|
|
1788
|
-
);
|
|
1789
|
-
if (!parsed.success) {
|
|
1790
|
-
throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
|
|
1791
|
-
}
|
|
1792
|
-
const authorizedRequest = await authorizeAgentJobRequest({
|
|
1793
|
-
request: parsed.data,
|
|
1794
|
-
options: options.options,
|
|
1795
|
-
registry: options.registry,
|
|
1796
|
-
root: options.root
|
|
1797
|
-
});
|
|
1798
|
-
const data = requireAgentManager(options).create(authorizedRequest);
|
|
1799
|
-
writeSuccess(response, 202, data);
|
|
1800
|
-
}
|
|
1801
|
-
async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
1802
|
-
if (request.method !== "POST") {
|
|
1803
|
-
throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
|
|
1804
|
-
}
|
|
1805
|
-
const parsed = agentWorkspaceHealthRequestSchema.safeParse(
|
|
1806
|
-
await readJsonRequestBody(request)
|
|
1807
|
-
);
|
|
1808
|
-
if (!parsed.success) {
|
|
1809
|
-
throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
|
|
1810
|
-
}
|
|
1811
|
-
const controller = new AbortController();
|
|
1812
|
-
const abort = () => {
|
|
1813
|
-
controller.abort("agent-workspace-health-client-disconnected");
|
|
1814
|
-
};
|
|
1815
|
-
response.once("close", abort);
|
|
1816
|
-
try {
|
|
1817
|
-
const data = await requireAgentManager(options).workspaceHealth(controller.signal);
|
|
1818
|
-
writeSuccess(response, 200, data);
|
|
1819
|
-
} finally {
|
|
1820
|
-
response.removeListener("close", abort);
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
async function handleJobAction(request, response, options, route, writeSuccess) {
|
|
1824
|
-
const manager = requireAgentManager(options);
|
|
1825
|
-
if (request.method !== "POST") {
|
|
1826
|
-
throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
|
|
1827
|
-
}
|
|
1828
|
-
const parsed = agentJobActionRequestSchema.safeParse(
|
|
1829
|
-
await readJsonRequestBody(request)
|
|
1830
|
-
);
|
|
1831
|
-
if (!parsed.success) {
|
|
1832
|
-
throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
|
|
1833
|
-
}
|
|
1834
|
-
if (route.action === "events") {
|
|
1835
|
-
streamAgentJobEvents(response, manager, route.jobId);
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
if (route.action === "result") {
|
|
1839
|
-
writeSuccess(response, 200, manager.result(route.jobId));
|
|
1840
|
-
return;
|
|
1841
|
-
}
|
|
1842
|
-
const data = route.action === "cancel" ? manager.cancel(route.jobId) : route.action === "apply" ? await manager.apply(route.jobId) : await manager.revert(route.jobId);
|
|
1843
|
-
writeSuccess(response, 200, data);
|
|
1844
|
-
}
|
|
1845
|
-
async function handleAgentRequest(request, response, options, route, writeSuccess) {
|
|
1846
|
-
if (route.kind === "capability") {
|
|
1847
|
-
await handleCapability(request, response, options, writeSuccess);
|
|
1848
|
-
return;
|
|
1849
|
-
}
|
|
1850
|
-
if (route.kind === "create-job") {
|
|
1851
|
-
await handleCreateJob(request, response, options, writeSuccess);
|
|
1852
|
-
return;
|
|
1853
|
-
}
|
|
1854
|
-
if (route.kind === "workspace-health") {
|
|
1855
|
-
await handleWorkspaceHealth(request, response, options, writeSuccess);
|
|
1856
|
-
return;
|
|
1857
|
-
}
|
|
1858
|
-
await handleJobAction(request, response, options, route, writeSuccess);
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
// src/server/editor.ts
|
|
1862
|
-
import { spawn } from "child_process";
|
|
1863
|
-
import launchEditor from "launch-editor";
|
|
1864
|
-
var EDITOR_STARTUP_GRACE_MS = 300;
|
|
1865
|
-
function normalizedEditorEnvironment(environment) {
|
|
1866
|
-
return [
|
|
1867
|
-
environment.TERM_PROGRAM,
|
|
1868
|
-
environment.VSCODE_GIT_ASKPASS_NODE,
|
|
1869
|
-
environment.VSCODE_GIT_ASKPASS_MAIN,
|
|
1870
|
-
environment.GIT_ASKPASS
|
|
1871
|
-
].filter((value) => typeof value === "string").join("\n").replaceAll("\\", "/").toLowerCase();
|
|
1872
|
-
}
|
|
1873
|
-
function detectIntegratedEditor(environment) {
|
|
1874
|
-
const signature = normalizedEditorEnvironment(environment);
|
|
1875
|
-
if (signature === "cursor" || signature.includes("/cursor")) {
|
|
1876
|
-
return "cursor";
|
|
1877
|
-
}
|
|
1878
|
-
if (signature === "vscode" || signature.includes("visual studio code") || /(^|\/)(code|code-insiders)(\.exe)?($|\/)/u.test(signature)) {
|
|
1879
|
-
return "vscode";
|
|
1880
|
-
}
|
|
1881
|
-
return void 0;
|
|
1882
|
-
}
|
|
1883
|
-
function editorCommand(editor) {
|
|
1884
|
-
return editor === "cursor" ? "cursor" : "code";
|
|
1885
|
-
}
|
|
1886
|
-
var DEFAULT_DEPENDENCIES2 = Object.freeze({
|
|
1887
|
-
environment: process.env,
|
|
1888
|
-
fallbackLauncher: launchEditor,
|
|
1889
|
-
processSpawner: (command, arguments_, options) => spawn(command, [...arguments_], options),
|
|
1890
|
-
startupGraceMs: EDITOR_STARTUP_GRACE_MS
|
|
1891
|
-
});
|
|
1892
|
-
function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
1893
|
-
return (target, configuredEditor) => {
|
|
1894
|
-
const integratedEditor = detectIntegratedEditor(dependencies.environment);
|
|
1895
|
-
const resolvedEditor = configuredEditor === "auto" ? integratedEditor : configuredEditor;
|
|
1896
|
-
return new Promise((resolve, reject) => {
|
|
1897
|
-
let settled = false;
|
|
1898
|
-
const settle = (error, editor = resolvedEditor ?? "auto") => {
|
|
1899
|
-
if (settled) {
|
|
1900
|
-
return;
|
|
1901
|
-
}
|
|
1902
|
-
settled = true;
|
|
1903
|
-
clearTimeout(startupTimer);
|
|
1904
|
-
if (error === void 0) {
|
|
1905
|
-
resolve(editor);
|
|
1906
|
-
} else {
|
|
1907
|
-
reject(error);
|
|
1908
|
-
}
|
|
1909
|
-
};
|
|
1910
|
-
const startupTimer = setTimeout(settle, dependencies.startupGraceMs);
|
|
1911
|
-
const rejectStartup = () => {
|
|
1912
|
-
settle(new Error("The configured editor could not be started."));
|
|
1913
|
-
};
|
|
1914
|
-
try {
|
|
1915
|
-
if (resolvedEditor === void 0) {
|
|
1916
|
-
dependencies.fallbackLauncher(target, void 0, rejectStartup);
|
|
1917
|
-
return;
|
|
1918
|
-
}
|
|
1919
|
-
const child = dependencies.processSpawner(
|
|
1920
|
-
editorCommand(resolvedEditor),
|
|
1921
|
-
["--goto", target],
|
|
1922
|
-
{
|
|
1923
|
-
env: dependencies.environment,
|
|
1924
|
-
stdio: "ignore",
|
|
1925
|
-
windowsHide: true
|
|
1926
|
-
}
|
|
1927
|
-
);
|
|
1928
|
-
child.once("error", rejectStartup);
|
|
1929
|
-
child.once("exit", (code, signal) => {
|
|
1930
|
-
if (code === 0) {
|
|
1931
|
-
settle(void 0, resolvedEditor);
|
|
1932
|
-
return;
|
|
1933
|
-
}
|
|
1934
|
-
if (code !== null || signal !== null) {
|
|
1935
|
-
rejectStartup();
|
|
1936
|
-
}
|
|
1937
|
-
});
|
|
1938
|
-
} catch {
|
|
1939
|
-
rejectStartup();
|
|
1940
|
-
}
|
|
1941
|
-
});
|
|
1942
|
-
};
|
|
1943
|
-
}
|
|
1944
|
-
var launchConfiguredEditor = createEditorLauncher();
|
|
1945
|
-
|
|
1946
|
-
// src/server/request-security.ts
|
|
1947
|
-
import { timingSafeEqual } from "crypto";
|
|
1948
|
-
import { isIP } from "net";
|
|
1949
|
-
import { ERROR_CODES as ERROR_CODES7, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
|
|
1950
|
-
function getSingleHeader(request, name) {
|
|
1951
|
-
const value = request.headers[name.toLowerCase()];
|
|
1952
|
-
return Array.isArray(value) ? value[0] : value;
|
|
1953
|
-
}
|
|
1954
|
-
function tokensMatch(actual, expected) {
|
|
1955
|
-
if (actual === void 0) {
|
|
1956
|
-
return false;
|
|
1957
|
-
}
|
|
1958
|
-
const actualBytes = Buffer.from(actual);
|
|
1959
|
-
const expectedBytes = Buffer.from(expected);
|
|
1960
|
-
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual(actualBytes, expectedBytes);
|
|
1961
|
-
}
|
|
1962
|
-
function isLoopbackHostname(hostname) {
|
|
1963
|
-
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1964
|
-
if (normalized === "localhost" || normalized.endsWith(".localhost")) {
|
|
1965
|
-
return true;
|
|
1966
|
-
}
|
|
1967
|
-
if (normalized === "::1") {
|
|
1968
|
-
return true;
|
|
1969
|
-
}
|
|
1970
|
-
if (isIP(normalized) === 4) {
|
|
1971
|
-
return normalized.split(".")[0] === "127";
|
|
1972
|
-
}
|
|
1973
|
-
return normalized.startsWith("::ffff:127.");
|
|
1974
|
-
}
|
|
1975
|
-
function parseHost(value) {
|
|
1976
|
-
try {
|
|
1977
|
-
return new URL(`http://${value}`);
|
|
1978
|
-
} catch {
|
|
1979
|
-
return void 0;
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
function parseOrigin(value) {
|
|
1983
|
-
try {
|
|
1984
|
-
const origin = new URL(value);
|
|
1985
|
-
if (origin.protocol !== "http:" && origin.protocol !== "https:" || origin.username.length > 0 || origin.password.length > 0 || origin.origin === "null") {
|
|
1986
|
-
return void 0;
|
|
1987
|
-
}
|
|
1988
|
-
return origin;
|
|
1989
|
-
} catch {
|
|
1990
|
-
return void 0;
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
function assertRequestAuthorized(request, options) {
|
|
1994
|
-
const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
|
|
1995
|
-
if (!tokensMatch(actualToken, options.sessionToken)) {
|
|
1996
|
-
throw new SpotPatchError7(ERROR_CODES7.INVALID_TOKEN);
|
|
1997
|
-
}
|
|
1998
|
-
const hostHeader = getSingleHeader(request, "host");
|
|
1999
|
-
const originHeader = getSingleHeader(request, "origin");
|
|
2000
|
-
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
2001
|
-
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
2002
|
-
if (host === void 0 || origin === void 0) {
|
|
2003
|
-
throw new SpotPatchError7(ERROR_CODES7.ORIGIN_NOT_ALLOWED);
|
|
2004
|
-
}
|
|
2005
|
-
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
2006
|
-
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
2007
|
-
if (!options.allowLan) {
|
|
2008
|
-
if (!hostIsLoopback || !originIsLoopback) {
|
|
2009
|
-
throw new SpotPatchError7(ERROR_CODES7.ORIGIN_NOT_ALLOWED);
|
|
2010
|
-
}
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
|
-
if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
|
|
2014
|
-
throw new SpotPatchError7(ERROR_CODES7.ORIGIN_NOT_ALLOWED);
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
// src/server/middleware.ts
|
|
2019
|
-
var STATUS_BY_ERROR = Object.freeze({
|
|
2020
|
-
[ERROR_CODES8.INVALID_REQUEST]: 400,
|
|
2021
|
-
[ERROR_CODES8.INVALID_TOKEN]: 401,
|
|
2022
|
-
[ERROR_CODES8.ORIGIN_NOT_ALLOWED]: 403,
|
|
2023
|
-
[ERROR_CODES8.SOURCE_NOT_FOUND]: 404,
|
|
2024
|
-
[ERROR_CODES8.SOURCE_OUTSIDE_ROOT]: 403,
|
|
2025
|
-
[ERROR_CODES8.SOURCE_TOO_LARGE]: 413,
|
|
2026
|
-
[ERROR_CODES8.EDITOR_OPEN_FAILED]: 500,
|
|
2027
|
-
[ERROR_CODES8.AI_DISABLED]: 404,
|
|
2028
|
-
[ERROR_CODES8.PROVIDER_NOT_CONFIGURED]: 503,
|
|
2029
|
-
[ERROR_CODES8.PROVIDER_AUTH_FAILED]: 502,
|
|
2030
|
-
[ERROR_CODES8.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
|
|
2031
|
-
[ERROR_CODES8.MODEL_NOT_ALLOWED]: 400,
|
|
2032
|
-
[ERROR_CODES8.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
|
|
2033
|
-
[ERROR_CODES8.PROVIDER_RATE_LIMITED]: 429,
|
|
2034
|
-
[ERROR_CODES8.AGENT_BUSY]: 409,
|
|
2035
|
-
[ERROR_CODES8.AGENT_LIMIT_EXCEEDED]: 413,
|
|
2036
|
-
[ERROR_CODES8.AGENT_CANCELLED]: 409,
|
|
2037
|
-
[ERROR_CODES8.WORKTREE_DIRTY]: 409,
|
|
2038
|
-
[ERROR_CODES8.WORKTREE_NOT_REPOSITORY]: 409,
|
|
2039
|
-
[ERROR_CODES8.WORKTREE_OPERATION_IN_PROGRESS]: 409,
|
|
2040
|
-
[ERROR_CODES8.WORKTREE_CONFLICTED]: 409,
|
|
2041
|
-
[ERROR_CODES8.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
|
|
2042
|
-
[ERROR_CODES8.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
|
|
2043
|
-
[ERROR_CODES8.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
|
|
2044
|
-
[ERROR_CODES8.TOOL_DENIED]: 403,
|
|
2045
|
-
[ERROR_CODES8.TOOL_INPUT_INVALID]: 422,
|
|
2046
|
-
[ERROR_CODES8.TOOL_PATH_DENIED]: 403,
|
|
2047
|
-
[ERROR_CODES8.PATCH_REJECTED]: 422,
|
|
2048
|
-
[ERROR_CODES8.VALIDATION_FAILED]: 422,
|
|
2049
|
-
[ERROR_CODES8.APPLY_CONFLICT]: 409,
|
|
2050
|
-
[ERROR_CODES8.INTERNAL_ERROR]: 500
|
|
2051
|
-
});
|
|
2052
|
-
var PUBLIC_MESSAGES = Object.freeze({
|
|
2053
|
-
[ERROR_CODES8.INVALID_REQUEST]: "The request is invalid.",
|
|
2054
|
-
[ERROR_CODES8.INVALID_TOKEN]: "The session token is invalid.",
|
|
2055
|
-
[ERROR_CODES8.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
|
|
2056
|
-
[ERROR_CODES8.SOURCE_NOT_FOUND]: "The source file is unavailable.",
|
|
2057
|
-
[ERROR_CODES8.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
|
|
2058
|
-
[ERROR_CODES8.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
|
|
2059
|
-
[ERROR_CODES8.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
|
|
2060
|
-
[ERROR_CODES8.AI_DISABLED]: "AI execution is not enabled.",
|
|
2061
|
-
[ERROR_CODES8.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
|
|
2062
|
-
[ERROR_CODES8.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
|
|
2063
|
-
[ERROR_CODES8.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
|
|
2064
|
-
[ERROR_CODES8.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
|
|
2065
|
-
[ERROR_CODES8.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
|
|
2066
|
-
[ERROR_CODES8.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
|
|
2067
|
-
[ERROR_CODES8.AGENT_BUSY]: "Another Agent job is already running.",
|
|
2068
|
-
[ERROR_CODES8.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
|
|
2069
|
-
[ERROR_CODES8.AGENT_CANCELLED]: "The Agent job was cancelled.",
|
|
2070
|
-
[ERROR_CODES8.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
|
|
2071
|
-
[ERROR_CODES8.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
|
|
2072
|
-
[ERROR_CODES8.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
|
|
2073
|
-
[ERROR_CODES8.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
|
|
2074
|
-
[ERROR_CODES8.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
|
|
2075
|
-
[ERROR_CODES8.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
|
|
2076
|
-
[ERROR_CODES8.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
|
|
2077
|
-
[ERROR_CODES8.TOOL_DENIED]: "The Agent tool request was denied.",
|
|
2078
|
-
[ERROR_CODES8.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
|
|
2079
|
-
[ERROR_CODES8.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
|
|
2080
|
-
[ERROR_CODES8.PATCH_REJECTED]: "The proposed patch was rejected.",
|
|
2081
|
-
[ERROR_CODES8.VALIDATION_FAILED]: "The proposed change failed validation.",
|
|
2082
|
-
[ERROR_CODES8.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
|
|
2083
|
-
[ERROR_CODES8.INTERNAL_ERROR]: "The request could not be completed."
|
|
2084
|
-
});
|
|
2085
|
-
function writeJson(response, status, payload) {
|
|
2086
|
-
response.statusCode = status;
|
|
2087
|
-
response.setHeader("Cache-Control", "no-store");
|
|
2088
|
-
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2089
|
-
response.end(JSON.stringify(payload));
|
|
2090
|
-
}
|
|
2091
|
-
function asSpotPatchError(error) {
|
|
2092
|
-
return error instanceof SpotPatchError8 ? error : new SpotPatchError8(ERROR_CODES8.INTERNAL_ERROR, void 0, { cause: error });
|
|
2093
|
-
}
|
|
2094
|
-
function writeError(response, error, logger) {
|
|
2095
|
-
const normalized = asSpotPatchError(error);
|
|
2096
|
-
if (normalized.code === ERROR_CODES8.INTERNAL_ERROR) {
|
|
2097
|
-
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
2098
|
-
}
|
|
2099
|
-
writeJson(response, STATUS_BY_ERROR[normalized.code], {
|
|
2100
|
-
ok: false,
|
|
2101
|
-
error: {
|
|
2102
|
-
code: normalized.code,
|
|
2103
|
-
message: PUBLIC_MESSAGES[normalized.code]
|
|
2104
|
-
}
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
function requestPath(request) {
|
|
2108
|
-
try {
|
|
2109
|
-
return new URL(request.url ?? "/", "http://spotpatch.invalid").pathname;
|
|
2110
|
-
} catch {
|
|
2111
|
-
return "";
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
async function handleSourceContext(request, options) {
|
|
2115
|
-
const parsed = sourceContextRequestSchema.safeParse(
|
|
2116
|
-
await readJsonRequestBody(request)
|
|
2117
|
-
);
|
|
2118
|
-
if (!parsed.success) {
|
|
2119
|
-
throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
|
|
2120
|
-
}
|
|
2121
|
-
return readSourceContext({
|
|
2122
|
-
request: parsed.data,
|
|
2123
|
-
registry: options.registry,
|
|
2124
|
-
root: options.root,
|
|
2125
|
-
maxCharacters: options.options.budget.codeCharacters,
|
|
2126
|
-
maxLines: options.options.budget.maxCodeLines
|
|
2127
|
-
});
|
|
2128
|
-
}
|
|
2129
|
-
async function handleOpenEditor(request, options) {
|
|
2130
|
-
const parsed = openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
|
|
2131
|
-
if (!parsed.success) {
|
|
2132
|
-
throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
|
|
2133
|
-
}
|
|
2134
|
-
const body = parsed.data;
|
|
2135
|
-
const sourcePath = await resolveSourceFile({
|
|
2136
|
-
fileId: body.fileId,
|
|
2137
|
-
registry: options.registry,
|
|
2138
|
-
root: options.root
|
|
2139
|
-
});
|
|
2140
|
-
const target = `${sourcePath}:${String(body.line)}:${String(body.column)}`;
|
|
2141
|
-
const editorLauncher = options.editorLauncher ?? launchConfiguredEditor;
|
|
2142
|
-
try {
|
|
2143
|
-
const editor = await editorLauncher(target, options.options.editor);
|
|
2144
|
-
return Object.freeze({ editor });
|
|
2145
|
-
} catch (error) {
|
|
2146
|
-
options.logger?.warn(
|
|
2147
|
-
`[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
|
|
2148
|
-
);
|
|
2149
|
-
throw new SpotPatchError8(ERROR_CODES8.EDITOR_OPEN_FAILED, void 0, {
|
|
2150
|
-
cause: error
|
|
2151
|
-
});
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
function createSpotPatchMiddleware(options) {
|
|
2155
|
-
return (request, response, next) => {
|
|
2156
|
-
const path11 = requestPath(request);
|
|
2157
|
-
const agentRoute = matchAgentRequestPath(path11);
|
|
2158
|
-
if (path11 !== SPOTPATCH_ENDPOINTS2.sourceContext && path11 !== SPOTPATCH_ENDPOINTS2.openEditor && agentRoute === void 0 && !path11.startsWith(`${SPOTPATCH_API_BASE}/`)) {
|
|
2159
|
-
next();
|
|
2160
|
-
return;
|
|
2161
|
-
}
|
|
2162
|
-
const handle = async () => {
|
|
2163
|
-
assertRequestAuthorized(request, {
|
|
2164
|
-
allowLan: options.options.allowLan,
|
|
2165
|
-
sessionToken: options.session.token
|
|
2166
|
-
});
|
|
2167
|
-
if (path11 === SPOTPATCH_ENDPOINTS2.sourceContext) {
|
|
2168
|
-
if (request.method !== "POST") {
|
|
2169
|
-
throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
|
|
2170
|
-
}
|
|
2171
|
-
const data = await handleSourceContext(request, options);
|
|
2172
|
-
writeJson(response, 200, { ok: true, data });
|
|
2173
|
-
return;
|
|
2174
|
-
}
|
|
2175
|
-
if (path11 === SPOTPATCH_ENDPOINTS2.openEditor) {
|
|
2176
|
-
if (request.method !== "POST") {
|
|
2177
|
-
throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
|
|
2178
|
-
}
|
|
2179
|
-
const data = await handleOpenEditor(request, options);
|
|
2180
|
-
writeJson(response, 200, { ok: true, data });
|
|
2181
|
-
return;
|
|
2182
|
-
}
|
|
2183
|
-
if (agentRoute === void 0) {
|
|
2184
|
-
throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
|
|
2185
|
-
}
|
|
2186
|
-
await handleAgentRequest(
|
|
2187
|
-
request,
|
|
2188
|
-
response,
|
|
2189
|
-
options,
|
|
2190
|
-
agentRoute,
|
|
2191
|
-
(target, status, data) => {
|
|
2192
|
-
writeJson(target, status, { ok: true, data });
|
|
2193
|
-
}
|
|
2194
|
-
);
|
|
2195
|
-
};
|
|
2196
|
-
void handle().catch((error) => {
|
|
2197
|
-
writeError(response, error, options.logger);
|
|
2198
|
-
});
|
|
2199
|
-
};
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
// src/server/server-plugin.ts
|
|
178
|
+
createAgentJobManager,
|
|
179
|
+
createSpotPatchMiddleware
|
|
180
|
+
} from "@spotpatch/dev-server";
|
|
2203
181
|
function createServerPlugin(input) {
|
|
2204
182
|
let agentManager;
|
|
2205
183
|
let config;
|
|
@@ -2219,7 +197,7 @@ function createServerPlugin(input) {
|
|
|
2219
197
|
if (config === void 0) {
|
|
2220
198
|
throw new Error("SpotPatch server initialized before Vite config resolution.");
|
|
2221
199
|
}
|
|
2222
|
-
const root =
|
|
200
|
+
const root = path2.resolve(config.root);
|
|
2223
201
|
const options = input.context.getOptions();
|
|
2224
202
|
agentManager = options.ai === false ? void 0 : createAgentJobManager({
|
|
2225
203
|
ai: options.ai,
|
|
@@ -2249,189 +227,40 @@ function createServerPlugin(input) {
|
|
|
2249
227
|
};
|
|
2250
228
|
}
|
|
2251
229
|
|
|
2252
|
-
// src/session/session.ts
|
|
2253
|
-
import { randomBytes as randomBytes3 } from "crypto";
|
|
2254
|
-
function createSession() {
|
|
2255
|
-
return Object.freeze({
|
|
2256
|
-
token: randomBytes3(16).toString("base64url")
|
|
2257
|
-
});
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
230
|
// src/transform/transform-plugin.ts
|
|
2261
|
-
import { createHash
|
|
2262
|
-
import
|
|
2263
|
-
|
|
2264
|
-
// src/transform/inject-source-markers.ts
|
|
2265
|
-
import path7 from "path";
|
|
2266
|
-
import {
|
|
2267
|
-
formatSourceMarker,
|
|
2268
|
-
SOURCE_MARKER_ATTRIBUTE as SOURCE_MARKER_ATTRIBUTE2
|
|
2269
|
-
} from "@spotpatch/shared";
|
|
2270
|
-
import MagicString from "magic-string";
|
|
2271
|
-
import { parseSync as parseSync2, Visitor as Visitor2 } from "oxc-parser";
|
|
2272
|
-
|
|
2273
|
-
// src/transform/intrinsic-element.ts
|
|
2274
|
-
import { SOURCE_MARKER_ATTRIBUTE } from "@spotpatch/shared";
|
|
2275
|
-
function isIntrinsicOpeningElement(node) {
|
|
2276
|
-
if (node.name.type !== "JSXIdentifier") {
|
|
2277
|
-
return false;
|
|
2278
|
-
}
|
|
2279
|
-
const { name } = node.name;
|
|
2280
|
-
const firstCharacter = name[0];
|
|
2281
|
-
return name.includes("-") || firstCharacter?.toLowerCase() === firstCharacter;
|
|
2282
|
-
}
|
|
2283
|
-
function hasSourceMarkerAttribute(node) {
|
|
2284
|
-
return node.attributes.some(
|
|
2285
|
-
(attribute) => attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === SOURCE_MARKER_ATTRIBUTE
|
|
2286
|
-
);
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
// src/transform/source-position.ts
|
|
2290
|
-
function createLineStarts(code) {
|
|
2291
|
-
const starts = [0];
|
|
2292
|
-
for (let index = 0; index < code.length; index += 1) {
|
|
2293
|
-
if (code.charCodeAt(index) === 10) {
|
|
2294
|
-
starts.push(index + 1);
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
return starts;
|
|
2298
|
-
}
|
|
2299
|
-
function getSourcePosition(lineStarts, offset) {
|
|
2300
|
-
let lower = 0;
|
|
2301
|
-
let upper = lineStarts.length - 1;
|
|
2302
|
-
while (lower <= upper) {
|
|
2303
|
-
const middle = Math.floor((lower + upper) / 2);
|
|
2304
|
-
const start = lineStarts[middle];
|
|
2305
|
-
if (start === void 0) {
|
|
2306
|
-
break;
|
|
2307
|
-
}
|
|
2308
|
-
if (start <= offset) {
|
|
2309
|
-
lower = middle + 1;
|
|
2310
|
-
} else {
|
|
2311
|
-
upper = middle - 1;
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
const lineIndex = Math.max(0, upper);
|
|
2315
|
-
const lineStart = lineStarts[lineIndex] ?? 0;
|
|
2316
|
-
return Object.freeze({
|
|
2317
|
-
line: lineIndex + 1,
|
|
2318
|
-
column: offset - lineStart + 1
|
|
2319
|
-
});
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
// src/transform/inject-source-markers.ts
|
|
2323
|
-
function findAttributeInsertionOffset(code, node) {
|
|
2324
|
-
let cursor = node.end - 2;
|
|
2325
|
-
while (cursor >= node.start && /\s/u.test(code[cursor] ?? "")) {
|
|
2326
|
-
cursor -= 1;
|
|
2327
|
-
}
|
|
2328
|
-
if (code[cursor] === "/") {
|
|
2329
|
-
cursor -= 1;
|
|
2330
|
-
while (cursor >= node.start && /\s/u.test(code[cursor] ?? "")) {
|
|
2331
|
-
cursor -= 1;
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
return cursor + 1;
|
|
2335
|
-
}
|
|
2336
|
-
function normalizeRelativePath(root, absolutePath) {
|
|
2337
|
-
return path7.relative(root, absolutePath).split(path7.sep).join("/");
|
|
2338
|
-
}
|
|
2339
|
-
function createMarker(fileId, line, column) {
|
|
2340
|
-
return Object.freeze({ fileId, line, column });
|
|
2341
|
-
}
|
|
2342
|
-
function injectSourceMarkers(input) {
|
|
2343
|
-
const parseResult = parseSync2(input.absolutePath, input.code, {
|
|
2344
|
-
sourceType: "module"
|
|
2345
|
-
});
|
|
2346
|
-
const parseError = parseResult.errors[0];
|
|
2347
|
-
if (parseError !== void 0) {
|
|
2348
|
-
throw new SyntaxError(parseError.message);
|
|
2349
|
-
}
|
|
2350
|
-
const magicString = new MagicString(input.code);
|
|
2351
|
-
const lineStarts = createLineStarts(input.code);
|
|
2352
|
-
let markerCount = 0;
|
|
2353
|
-
const visitor = new Visitor2({
|
|
2354
|
-
JSXOpeningElement(node) {
|
|
2355
|
-
if (!isIntrinsicOpeningElement(node)) {
|
|
2356
|
-
return;
|
|
2357
|
-
}
|
|
2358
|
-
const position = getSourcePosition(lineStarts, node.start);
|
|
2359
|
-
if (hasSourceMarkerAttribute(node)) {
|
|
2360
|
-
input.onWarning?.({
|
|
2361
|
-
code: "EXISTING_SOURCE_MARKER",
|
|
2362
|
-
line: position.line,
|
|
2363
|
-
column: position.column
|
|
2364
|
-
});
|
|
2365
|
-
return;
|
|
2366
|
-
}
|
|
2367
|
-
const value = formatSourceMarker(
|
|
2368
|
-
createMarker(input.fileId, position.line, position.column)
|
|
2369
|
-
);
|
|
2370
|
-
const insertionOffset = findAttributeInsertionOffset(input.code, node);
|
|
2371
|
-
magicString.appendLeft(
|
|
2372
|
-
insertionOffset,
|
|
2373
|
-
` ${SOURCE_MARKER_ATTRIBUTE2}=${JSON.stringify(value)}`
|
|
2374
|
-
);
|
|
2375
|
-
markerCount += 1;
|
|
2376
|
-
}
|
|
2377
|
-
});
|
|
2378
|
-
visitor.visit(parseResult.program);
|
|
2379
|
-
if (markerCount === 0) {
|
|
2380
|
-
return void 0;
|
|
2381
|
-
}
|
|
2382
|
-
return Object.freeze({
|
|
2383
|
-
code: magicString.toString(),
|
|
2384
|
-
map: magicString.generateMap({
|
|
2385
|
-
hires: true,
|
|
2386
|
-
includeContent: true,
|
|
2387
|
-
source: normalizeRelativePath(input.root, input.absolutePath)
|
|
2388
|
-
}),
|
|
2389
|
-
markerCount
|
|
2390
|
-
});
|
|
2391
|
-
}
|
|
231
|
+
import { createHash } from "crypto";
|
|
232
|
+
import path4 from "path";
|
|
233
|
+
import { injectSourceMarkers } from "@spotpatch/compiler";
|
|
2392
234
|
|
|
2393
235
|
// src/transform/transform-filter.ts
|
|
2394
|
-
import
|
|
2395
|
-
import {
|
|
2396
|
-
|
|
236
|
+
import path3 from "path";
|
|
237
|
+
import { createSourceFilter } from "@spotpatch/compiler";
|
|
238
|
+
import { isInsideRoot } from "@spotpatch/compiler";
|
|
2397
239
|
function stripViteQuery(id) {
|
|
2398
240
|
const queryIndex = id.indexOf("?");
|
|
2399
241
|
return queryIndex === -1 ? id : id.slice(0, queryIndex);
|
|
2400
242
|
}
|
|
2401
|
-
function isInsideRoot(root, candidate, pathApi = path8) {
|
|
2402
|
-
const relative = pathApi.relative(pathApi.resolve(root), pathApi.resolve(candidate));
|
|
2403
|
-
return relative === "" || !relative.startsWith(`..${pathApi.sep}`) && relative !== ".." && !pathApi.isAbsolute(relative);
|
|
2404
|
-
}
|
|
2405
243
|
function createTransformFilter(root, options) {
|
|
2406
|
-
const
|
|
244
|
+
const sourceFilter = createSourceFilter(root, options);
|
|
2407
245
|
return Object.freeze({
|
|
2408
246
|
shouldTransform(id, code) {
|
|
2409
|
-
if (id.startsWith("\0") || id.includes("virtual:spotpatch")) {
|
|
247
|
+
if (id.startsWith("\0") || id.includes("virtual:spotpatch") || id.includes("/packages/vite/") || id.includes("\\packages\\vite\\")) {
|
|
2410
248
|
return false;
|
|
2411
249
|
}
|
|
2412
250
|
const cleanId = stripViteQuery(id);
|
|
2413
|
-
|
|
2414
|
-
return false;
|
|
2415
|
-
}
|
|
2416
|
-
if (cleanId.includes("/node_modules/") || cleanId.includes("\\node_modules\\") || cleanId.includes("/packages/vite/") || cleanId.includes("\\packages\\vite\\")) {
|
|
2417
|
-
return false;
|
|
2418
|
-
}
|
|
2419
|
-
if (!isInsideRoot(root, cleanId) || !matchesConfiguredFilter(cleanId)) {
|
|
2420
|
-
return false;
|
|
2421
|
-
}
|
|
2422
|
-
return code.includes("<");
|
|
251
|
+
return sourceFilter.shouldTransform(path3.resolve(cleanId), code);
|
|
2423
252
|
}
|
|
2424
253
|
});
|
|
2425
254
|
}
|
|
2426
255
|
|
|
2427
256
|
// src/transform/transform-plugin.ts
|
|
2428
257
|
function createCacheKey(id, code) {
|
|
2429
|
-
const hash =
|
|
258
|
+
const hash = createHash("sha256").update(code).digest("base64url");
|
|
2430
259
|
return `${id}\0${hash}`;
|
|
2431
260
|
}
|
|
2432
261
|
function getDisplayPath(root, id) {
|
|
2433
|
-
const relative =
|
|
2434
|
-
return relative.split(
|
|
262
|
+
const relative = path4.relative(root, stripViteQuery(id));
|
|
263
|
+
return relative.split(path4.sep).join("/");
|
|
2435
264
|
}
|
|
2436
265
|
function createTransformPlugin(input) {
|
|
2437
266
|
let root = process.cwd();
|
|
@@ -2447,7 +276,7 @@ function createTransformPlugin(input) {
|
|
|
2447
276
|
input.configure?.(config, environment);
|
|
2448
277
|
},
|
|
2449
278
|
configResolved(config) {
|
|
2450
|
-
root =
|
|
279
|
+
root = path4.resolve(config.root);
|
|
2451
280
|
filter = createTransformFilter(root, input.context.getOptions());
|
|
2452
281
|
logger = config.logger;
|
|
2453
282
|
},
|
|
@@ -2455,7 +284,7 @@ function createTransformPlugin(input) {
|
|
|
2455
284
|
if (!filter.shouldTransform(id, code)) {
|
|
2456
285
|
return null;
|
|
2457
286
|
}
|
|
2458
|
-
const cleanId =
|
|
287
|
+
const cleanId = path4.resolve(stripViteQuery(id));
|
|
2459
288
|
const cacheKey = createCacheKey(cleanId, code);
|
|
2460
289
|
if (cache.has(cacheKey)) {
|
|
2461
290
|
return cache.get(cacheKey) ?? null;
|
|
@@ -2514,29 +343,11 @@ function spotPatch(userOptions = {}) {
|
|
|
2514
343
|
getOptions: () => options
|
|
2515
344
|
});
|
|
2516
345
|
const configure = (config, environment) => {
|
|
2517
|
-
const root =
|
|
2518
|
-
const loadedEnvironment = config.envDir === false ? process.env : loadEnv(environment.mode,
|
|
346
|
+
const root = path5.resolve(process.cwd(), config.root ?? ".");
|
|
347
|
+
const loadedEnvironment = config.envDir === false ? process.env : loadEnv(environment.mode, path5.resolve(root, config.envDir ?? "."), "");
|
|
2519
348
|
const environmentAi = userOptions.ai === void 0 ? resolveEnvironmentAiConfiguration(loadedEnvironment).ai : false;
|
|
2520
349
|
options = resolveOptions(userOptions, environmentAi);
|
|
2521
|
-
|
|
2522
|
-
credentialEnvironment = Object.freeze({});
|
|
2523
|
-
return;
|
|
2524
|
-
}
|
|
2525
|
-
const names = new Set(
|
|
2526
|
-
Object.values(options.ai.providers).map((provider) => provider.apiKeyEnv)
|
|
2527
|
-
);
|
|
2528
|
-
const missing = [...names].filter((name) => {
|
|
2529
|
-
const value = loadedEnvironment[name];
|
|
2530
|
-
return value === void 0 || value.trim().length === 0;
|
|
2531
|
-
});
|
|
2532
|
-
if (missing.length > 0) {
|
|
2533
|
-
throw new RangeError(
|
|
2534
|
-
`SpotPatch AI credential environment is missing ${missing.join(", ")}.`
|
|
2535
|
-
);
|
|
2536
|
-
}
|
|
2537
|
-
credentialEnvironment = Object.freeze(
|
|
2538
|
-
Object.fromEntries([...names].map((name) => [name, loadedEnvironment[name]]))
|
|
2539
|
-
);
|
|
350
|
+
credentialEnvironment = resolveCredentialEnvironment(options, loadedEnvironment);
|
|
2540
351
|
};
|
|
2541
352
|
return [
|
|
2542
353
|
createTransformPlugin({ configure, context, registry }),
|
|
@@ -2545,14 +356,22 @@ function spotPatch(userOptions = {}) {
|
|
|
2545
356
|
];
|
|
2546
357
|
}
|
|
2547
358
|
|
|
359
|
+
// src/options.ts
|
|
360
|
+
import {
|
|
361
|
+
createRuntimeAiConfig as createRuntimeAiConfig2,
|
|
362
|
+
DEFAULT_EXCLUDE,
|
|
363
|
+
DEFAULT_OPTIONS,
|
|
364
|
+
resolveOptions as resolveOptions2
|
|
365
|
+
} from "@spotpatch/dev-server";
|
|
366
|
+
|
|
2548
367
|
// src/index.ts
|
|
2549
368
|
import {
|
|
2550
|
-
DEFAULT_AGENT_LIMITS
|
|
369
|
+
DEFAULT_AGENT_LIMITS
|
|
2551
370
|
} from "@spotpatch/shared";
|
|
2552
371
|
export {
|
|
2553
|
-
|
|
372
|
+
DEFAULT_AGENT_LIMITS,
|
|
2554
373
|
DEFAULT_OPTIONS,
|
|
2555
|
-
resolveOptions,
|
|
374
|
+
resolveOptions2 as resolveOptions,
|
|
2556
375
|
spotPatch
|
|
2557
376
|
};
|
|
2558
377
|
//# sourceMappingURL=index.js.map
|