@spotpatch/vite 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,13 +1,24 @@
1
1
  import { Plugin } from 'vite';
2
- import { ContextBudget, SpotPatchLocalePreference, ResolvedAiOptions, AiOptions } from '@spotpatch/shared';
3
- export { AgentApplyMode, AgentCheckDefinition, AgentLimits, AiExecutionOptions, AiModelProfile, AiOptions, AiProviderProtocol, ContextBudget, DEFAULT_AGENT_LIMITS, OpenAICompatibleProviderOptions } from '@spotpatch/shared';
2
+ import { SpotPatchEditorPreference, ContextBudget, SpotPatchLocalePreference, ResolvedAiOptions, AiProviderProtocol, AiProviderAuthentication, AiExecutionOptions, AiOptions } from '@spotpatch/shared';
3
+ export { AgentApplyMode, AgentCheckDefinition, AgentLimits, AiExecutionOptions, AiModelProfile, AiOptions, AiProviderAuthentication, AiProviderProtocol, ContextBudget, DEFAULT_AGENT_LIMITS, OpenAICompatibleProviderOptions, SpotPatchEditorPreference } from '@spotpatch/shared';
4
4
 
5
5
  type FilterEntry = string | RegExp;
6
+ interface SimpleAiOptions {
7
+ readonly baseURL: string;
8
+ readonly model: string;
9
+ readonly apiKeyEnv?: string;
10
+ readonly protocol?: AiProviderProtocol;
11
+ readonly authentication?: AiProviderAuthentication;
12
+ readonly providerLabel?: string;
13
+ readonly modelLabel?: string;
14
+ readonly execution?: AiExecutionOptions;
15
+ }
16
+ type SpotPatchAiOptions = false | SimpleAiOptions | AiOptions;
6
17
  interface SpotPatchOptions {
7
18
  readonly enabled?: boolean;
8
19
  readonly include?: readonly FilterEntry[];
9
20
  readonly exclude?: readonly FilterEntry[];
10
- readonly editor?: "vscode";
21
+ readonly editor?: SpotPatchEditorPreference;
11
22
  readonly redact?: boolean;
12
23
  readonly budget?: Partial<ContextBudget>;
13
24
  readonly shortcut?: string;
@@ -15,13 +26,13 @@ interface SpotPatchOptions {
15
26
  readonly debug?: boolean;
16
27
  readonly locale?: SpotPatchLocalePreference;
17
28
  readonly maxTargets?: number;
18
- readonly ai?: false | AiOptions;
29
+ readonly ai?: SpotPatchAiOptions;
19
30
  }
20
31
  interface ResolvedSpotPatchOptions {
21
32
  readonly enabled: boolean;
22
33
  readonly include: readonly FilterEntry[];
23
34
  readonly exclude: readonly FilterEntry[];
24
- readonly editor: "vscode";
35
+ readonly editor: SpotPatchEditorPreference;
25
36
  readonly redact: boolean;
26
37
  readonly budget: Readonly<ContextBudget>;
27
38
  readonly shortcut: string;
@@ -35,7 +46,7 @@ declare const DEFAULT_OPTIONS: Readonly<{
35
46
  enabled: true;
36
47
  include: readonly RegExp[];
37
48
  exclude: readonly RegExp[];
38
- editor: "vscode";
49
+ editor: "auto";
39
50
  redact: true;
40
51
  budget: Readonly<{
41
52
  totalCharacters: number;
@@ -52,8 +63,8 @@ declare const DEFAULT_OPTIONS: Readonly<{
52
63
  maxTargets: number;
53
64
  ai: false;
54
65
  }>;
55
- declare function resolveOptions(options?: SpotPatchOptions): ResolvedSpotPatchOptions;
66
+ declare function resolveOptions(options?: SpotPatchOptions, environmentAi?: false | SimpleAiOptions): ResolvedSpotPatchOptions;
56
67
 
57
68
  declare function spotPatch(userOptions?: SpotPatchOptions): Plugin[];
58
69
 
59
- export { DEFAULT_OPTIONS, type ResolvedSpotPatchOptions, type SpotPatchOptions, resolveOptions, spotPatch };
70
+ export { DEFAULT_OPTIONS, type ResolvedSpotPatchOptions, type SimpleAiOptions, type SpotPatchAiOptions, type SpotPatchOptions, resolveOptions, spotPatch };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,24 @@
1
1
  import { Plugin } from 'vite';
2
- import { ContextBudget, SpotPatchLocalePreference, ResolvedAiOptions, AiOptions } from '@spotpatch/shared';
3
- export { AgentApplyMode, AgentCheckDefinition, AgentLimits, AiExecutionOptions, AiModelProfile, AiOptions, AiProviderProtocol, ContextBudget, DEFAULT_AGENT_LIMITS, OpenAICompatibleProviderOptions } from '@spotpatch/shared';
2
+ import { SpotPatchEditorPreference, ContextBudget, SpotPatchLocalePreference, ResolvedAiOptions, AiProviderProtocol, AiProviderAuthentication, AiExecutionOptions, AiOptions } from '@spotpatch/shared';
3
+ export { AgentApplyMode, AgentCheckDefinition, AgentLimits, AiExecutionOptions, AiModelProfile, AiOptions, AiProviderAuthentication, AiProviderProtocol, ContextBudget, DEFAULT_AGENT_LIMITS, OpenAICompatibleProviderOptions, SpotPatchEditorPreference } from '@spotpatch/shared';
4
4
 
5
5
  type FilterEntry = string | RegExp;
6
+ interface SimpleAiOptions {
7
+ readonly baseURL: string;
8
+ readonly model: string;
9
+ readonly apiKeyEnv?: string;
10
+ readonly protocol?: AiProviderProtocol;
11
+ readonly authentication?: AiProviderAuthentication;
12
+ readonly providerLabel?: string;
13
+ readonly modelLabel?: string;
14
+ readonly execution?: AiExecutionOptions;
15
+ }
16
+ type SpotPatchAiOptions = false | SimpleAiOptions | AiOptions;
6
17
  interface SpotPatchOptions {
7
18
  readonly enabled?: boolean;
8
19
  readonly include?: readonly FilterEntry[];
9
20
  readonly exclude?: readonly FilterEntry[];
10
- readonly editor?: "vscode";
21
+ readonly editor?: SpotPatchEditorPreference;
11
22
  readonly redact?: boolean;
12
23
  readonly budget?: Partial<ContextBudget>;
13
24
  readonly shortcut?: string;
@@ -15,13 +26,13 @@ interface SpotPatchOptions {
15
26
  readonly debug?: boolean;
16
27
  readonly locale?: SpotPatchLocalePreference;
17
28
  readonly maxTargets?: number;
18
- readonly ai?: false | AiOptions;
29
+ readonly ai?: SpotPatchAiOptions;
19
30
  }
20
31
  interface ResolvedSpotPatchOptions {
21
32
  readonly enabled: boolean;
22
33
  readonly include: readonly FilterEntry[];
23
34
  readonly exclude: readonly FilterEntry[];
24
- readonly editor: "vscode";
35
+ readonly editor: SpotPatchEditorPreference;
25
36
  readonly redact: boolean;
26
37
  readonly budget: Readonly<ContextBudget>;
27
38
  readonly shortcut: string;
@@ -35,7 +46,7 @@ declare const DEFAULT_OPTIONS: Readonly<{
35
46
  enabled: true;
36
47
  include: readonly RegExp[];
37
48
  exclude: readonly RegExp[];
38
- editor: "vscode";
49
+ editor: "auto";
39
50
  redact: true;
40
51
  budget: Readonly<{
41
52
  totalCharacters: number;
@@ -52,8 +63,8 @@ declare const DEFAULT_OPTIONS: Readonly<{
52
63
  maxTargets: number;
53
64
  ai: false;
54
65
  }>;
55
- declare function resolveOptions(options?: SpotPatchOptions): ResolvedSpotPatchOptions;
66
+ declare function resolveOptions(options?: SpotPatchOptions, environmentAi?: false | SimpleAiOptions): ResolvedSpotPatchOptions;
56
67
 
57
68
  declare function spotPatch(userOptions?: SpotPatchOptions): Plugin[];
58
69
 
59
- export { DEFAULT_OPTIONS, type ResolvedSpotPatchOptions, type SpotPatchOptions, resolveOptions, spotPatch };
70
+ export { DEFAULT_OPTIONS, type ResolvedSpotPatchOptions, type SimpleAiOptions, type SpotPatchAiOptions, type SpotPatchOptions, resolveOptions, spotPatch };
package/dist/index.js CHANGED
@@ -1,7 +1,73 @@
1
+ // src/plugin.ts
2
+ import path10 from "path";
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
+
1
66
  // src/options.ts
2
67
  import {
3
68
  DEFAULT_AGENT_LIMITS,
4
69
  MAX_ANNOTATION_TARGETS,
70
+ SPOTPATCH_EDITOR_PREFERENCES,
5
71
  SPOTPATCH_LOCALE_PREFERENCES
6
72
  } from "@spotpatch/shared";
7
73
  import { z } from "zod";
@@ -26,7 +92,7 @@ var DEFAULT_OPTIONS = Object.freeze({
26
92
  enabled: true,
27
93
  include: DEFAULT_INCLUDE,
28
94
  exclude: DEFAULT_EXCLUDE,
29
- editor: "vscode",
95
+ editor: "auto",
30
96
  redact: true,
31
97
  budget: DEFAULT_BUDGET,
32
98
  shortcut: "Mod+Shift+S",
@@ -66,6 +132,7 @@ var aiOptionsSchema = z.strictObject({
66
132
  type: z.literal("openai-compatible"),
67
133
  label: z.string(),
68
134
  protocol: z.enum(["responses", "chat-completions"]),
135
+ authentication: z.enum(["bearer", "x-api-key"]).optional(),
69
136
  baseURL: z.string(),
70
137
  apiKeyEnv: z.string(),
71
138
  models: z.record(
@@ -83,6 +150,16 @@ var aiOptionsSchema = z.strictObject({
83
150
  limits: agentLimitsSchema
84
151
  }).optional()
85
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
+ });
86
163
  function assertIdentifier(value, label) {
87
164
  if (!PROFILE_ID_PATTERN.test(value)) {
88
165
  throw new RangeError(
@@ -182,6 +259,7 @@ function resolveProviders(providers) {
182
259
  type: provider.type,
183
260
  label: nonEmpty(provider.label, "provider label", 100),
184
261
  protocol: provider.protocol,
262
+ authentication: provider.authentication ?? "bearer",
185
263
  baseURL: normalizeProviderBaseURL(provider.baseURL),
186
264
  apiKeyEnv: provider.apiKeyEnv,
187
265
  models,
@@ -225,7 +303,36 @@ function resolveAiOptions(options) {
225
303
  if (options === void 0 || options === false) {
226
304
  return false;
227
305
  }
228
- const parsed = aiOptionsSchema.safeParse(options);
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);
229
336
  if (!parsed.success) {
230
337
  throw new RangeError("SpotPatch AI configuration is invalid.");
231
338
  }
@@ -285,7 +392,7 @@ function assertPositiveBudget(budget) {
285
392
  }
286
393
  }
287
394
  }
288
- function resolveOptions(options = {}) {
395
+ function resolveOptions(options = {}, environmentAi) {
289
396
  const budget = Object.freeze({
290
397
  ...DEFAULT_OPTIONS.budget,
291
398
  ...options.budget
@@ -293,9 +400,13 @@ function resolveOptions(options = {}) {
293
400
  assertPositiveBudget(budget);
294
401
  const maxTargets = options.maxTargets ?? DEFAULT_OPTIONS.maxTargets;
295
402
  const locale = options.locale ?? DEFAULT_OPTIONS.locale;
403
+ const editor = options.editor ?? DEFAULT_OPTIONS.editor;
296
404
  if (!SPOTPATCH_LOCALE_PREFERENCES.includes(locale)) {
297
405
  throw new RangeError("SpotPatch locale must be auto, en-US, or zh-CN.");
298
406
  }
407
+ if (!SPOTPATCH_EDITOR_PREFERENCES.includes(editor)) {
408
+ throw new RangeError("SpotPatch editor must be auto, vscode, or cursor.");
409
+ }
299
410
  if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets > MAX_ANNOTATION_TARGETS) {
300
411
  throw new RangeError(
301
412
  `SpotPatch maxTargets must be an integer between 1 and ${String(MAX_ANNOTATION_TARGETS)}.`
@@ -305,7 +416,7 @@ function resolveOptions(options = {}) {
305
416
  enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,
306
417
  include: Object.freeze([...options.include ?? DEFAULT_OPTIONS.include]),
307
418
  exclude: Object.freeze([...options.exclude ?? DEFAULT_OPTIONS.exclude]),
308
- editor: options.editor ?? DEFAULT_OPTIONS.editor,
419
+ editor,
309
420
  redact: options.redact ?? DEFAULT_OPTIONS.redact,
310
421
  budget,
311
422
  shortcut: options.shortcut ?? DEFAULT_OPTIONS.shortcut,
@@ -313,7 +424,7 @@ function resolveOptions(options = {}) {
313
424
  debug: options.debug ?? DEFAULT_OPTIONS.debug,
314
425
  locale,
315
426
  maxTargets,
316
- ai: resolveAiOptions(options.ai)
427
+ ai: resolveAiOptions(options.ai ?? environmentAi)
317
428
  };
318
429
  if (resolved.shortcut.trim().length === 0) {
319
430
  throw new RangeError("SpotPatch shortcut cannot be empty.");
@@ -370,7 +481,7 @@ import path2 from "path";
370
481
  // package.json
371
482
  var package_default = {
372
483
  name: "@spotpatch/vite",
373
- version: "1.0.0",
484
+ version: "1.2.0",
374
485
  description: "Vite development plugin for SpotPatch.",
375
486
  license: "MIT",
376
487
  repository: {
@@ -462,15 +573,17 @@ function readConsumerViteVersion(root) {
462
573
  return VITE_VERSION;
463
574
  }
464
575
  function createClientModule(input, clientBundle, viteVersion) {
576
+ const options = input.context.getOptions();
465
577
  const runtimeConfig = {
466
- ai: createRuntimeAiConfig(input.options.ai),
467
- budget: input.options.budget,
468
- debug: input.options.debug,
469
- locale: input.options.locale,
470
- maxTargets: input.options.maxTargets,
471
- redact: input.options.redact,
578
+ ai: createRuntimeAiConfig(options.ai),
579
+ budget: options.budget,
580
+ debug: options.debug,
581
+ editor: options.editor,
582
+ locale: options.locale,
583
+ maxTargets: options.maxTargets,
584
+ redact: options.redact,
472
585
  sessionToken: input.session.token,
473
- shortcut: input.options.shortcut,
586
+ shortcut: options.shortcut,
474
587
  spotPatchVersion: package_default.version,
475
588
  viteVersion
476
589
  };
@@ -602,7 +715,7 @@ function snapshot(job) {
602
715
  );
603
716
  }
604
717
  function capabilityCacheKey(provider, model) {
605
- const configurationDigest = createHash("sha256").update(provider.baseURL).update("\0").update(provider.protocol).digest("hex");
718
+ const configurationDigest = createHash("sha256").update(provider.baseURL).update("\0").update(provider.protocol).update("\0").update(provider.authentication).digest("hex");
606
719
  return `${provider.id}:${model.id}:${configurationDigest}`;
607
720
  }
608
721
  function freezeEvent(event) {
@@ -1550,18 +1663,18 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1550
1663
  "reverted",
1551
1664
  "failed"
1552
1665
  ]);
1553
- function matchAgentRequestPath(path10) {
1554
- if (path10 === SPOTPATCH_ENDPOINTS.agentCapability) {
1666
+ function matchAgentRequestPath(path11) {
1667
+ if (path11 === SPOTPATCH_ENDPOINTS.agentCapability) {
1555
1668
  return Object.freeze({ kind: "capability" });
1556
1669
  }
1557
- if (path10 === SPOTPATCH_ENDPOINTS.agentJobs) {
1670
+ if (path11 === SPOTPATCH_ENDPOINTS.agentJobs) {
1558
1671
  return Object.freeze({ kind: "create-job" });
1559
1672
  }
1560
1673
  const prefix = `${SPOTPATCH_ENDPOINTS.agentJobs}/`;
1561
- if (!path10.startsWith(prefix)) {
1674
+ if (!path11.startsWith(prefix)) {
1562
1675
  return void 0;
1563
1676
  }
1564
- const segments = path10.slice(prefix.length).split("/");
1677
+ const segments = path11.slice(prefix.length).split("/");
1565
1678
  const jobId = segments[0];
1566
1679
  const action = segments[1];
1567
1680
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -1706,9 +1819,36 @@ async function handleAgentRequest(request, response, options, route, writeSucces
1706
1819
 
1707
1820
  // src/server/editor.ts
1708
1821
  import launchEditor from "launch-editor";
1709
- var launchVSCode = (target, onError) => {
1710
- launchEditor(target, "code", onError);
1711
- };
1822
+ var EDITOR_STARTUP_GRACE_MS = 150;
1823
+ function editorCommand(editor) {
1824
+ if (editor === "vscode") {
1825
+ return "code";
1826
+ }
1827
+ if (editor === "cursor") {
1828
+ return "cursor";
1829
+ }
1830
+ return void 0;
1831
+ }
1832
+ var launchConfiguredEditor = (target, editor) => new Promise((resolve, reject) => {
1833
+ let settled = false;
1834
+ const startupTimer = setTimeout(() => {
1835
+ settled = true;
1836
+ resolve();
1837
+ }, EDITOR_STARTUP_GRACE_MS);
1838
+ const rejectStartup = () => {
1839
+ if (settled) {
1840
+ return;
1841
+ }
1842
+ settled = true;
1843
+ clearTimeout(startupTimer);
1844
+ reject(new Error("The configured editor could not be started."));
1845
+ };
1846
+ try {
1847
+ launchEditor(target, editorCommand(editor), rejectStartup);
1848
+ } catch {
1849
+ rejectStartup();
1850
+ }
1851
+ });
1712
1852
 
1713
1853
  // src/server/request-security.ts
1714
1854
  import { timingSafeEqual } from "crypto";
@@ -1889,23 +2029,24 @@ async function handleOpenEditor(request, options) {
1889
2029
  root: options.root
1890
2030
  });
1891
2031
  const target = `${sourcePath}:${String(body.line)}:${String(body.column)}`;
1892
- const editorLauncher = options.editorLauncher ?? launchVSCode;
2032
+ const editorLauncher = options.editorLauncher ?? launchConfiguredEditor;
1893
2033
  try {
1894
- editorLauncher(target, () => {
1895
- options.logger?.warn("[spotpatch:server] VS Code rejected an editor request.");
1896
- });
2034
+ await editorLauncher(target, options.options.editor);
1897
2035
  } catch (error) {
2036
+ options.logger?.warn(
2037
+ `[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
2038
+ );
1898
2039
  throw new SpotPatchError8(ERROR_CODES8.EDITOR_OPEN_FAILED, void 0, {
1899
2040
  cause: error
1900
2041
  });
1901
2042
  }
1902
- return Object.freeze({});
2043
+ return Object.freeze({ editor: options.options.editor });
1903
2044
  }
1904
2045
  function createSpotPatchMiddleware(options) {
1905
2046
  return (request, response, next) => {
1906
- const path10 = requestPath(request);
1907
- const agentRoute = matchAgentRequestPath(path10);
1908
- if (path10 !== SPOTPATCH_ENDPOINTS2.sourceContext && path10 !== SPOTPATCH_ENDPOINTS2.openEditor && agentRoute === void 0 && !path10.startsWith(`${SPOTPATCH_API_BASE}/`)) {
2047
+ const path11 = requestPath(request);
2048
+ const agentRoute = matchAgentRequestPath(path11);
2049
+ if (path11 !== SPOTPATCH_ENDPOINTS2.sourceContext && path11 !== SPOTPATCH_ENDPOINTS2.openEditor && agentRoute === void 0 && !path11.startsWith(`${SPOTPATCH_API_BASE}/`)) {
1909
2050
  next();
1910
2051
  return;
1911
2052
  }
@@ -1914,7 +2055,7 @@ function createSpotPatchMiddleware(options) {
1914
2055
  allowLan: options.options.allowLan,
1915
2056
  sessionToken: options.session.token
1916
2057
  });
1917
- if (path10 === SPOTPATCH_ENDPOINTS2.sourceContext) {
2058
+ if (path11 === SPOTPATCH_ENDPOINTS2.sourceContext) {
1918
2059
  if (request.method !== "POST") {
1919
2060
  throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
1920
2061
  }
@@ -1922,7 +2063,7 @@ function createSpotPatchMiddleware(options) {
1922
2063
  writeJson(response, 200, { ok: true, data });
1923
2064
  return;
1924
2065
  }
1925
- if (path10 === SPOTPATCH_ENDPOINTS2.openEditor) {
2066
+ if (path11 === SPOTPATCH_ENDPOINTS2.openEditor) {
1926
2067
  if (request.method !== "POST") {
1927
2068
  throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
1928
2069
  }
@@ -1970,11 +2111,16 @@ function createServerPlugin(input) {
1970
2111
  throw new Error("SpotPatch server initialized before Vite config resolution.");
1971
2112
  }
1972
2113
  const root = path6.resolve(config.root);
1973
- agentManager = input.options.ai === false ? void 0 : createAgentJobManager({ ai: input.options.ai, root });
2114
+ const options = input.context.getOptions();
2115
+ agentManager = options.ai === false ? void 0 : createAgentJobManager({
2116
+ ai: options.ai,
2117
+ environment: input.context.getCredentialEnvironment(),
2118
+ root
2119
+ });
1974
2120
  server.middlewares.use(
1975
2121
  createSpotPatchMiddleware({
1976
2122
  ...agentManager === void 0 ? {} : { agentManager },
1977
- options: input.options,
2123
+ options,
1978
2124
  registry: input.registry,
1979
2125
  root,
1980
2126
  session: input.session,
@@ -1985,7 +2131,7 @@ function createServerPlugin(input) {
1985
2131
  void closeResources();
1986
2132
  });
1987
2133
  config.logger.info(
1988
- `[spotpatch:vite] Ready. Toggle picker with ${input.options.shortcut}.`
2134
+ `[spotpatch:vite] Ready. Toggle picker with ${options.shortcut}.`
1989
2135
  );
1990
2136
  },
1991
2137
  async closeBundle() {
@@ -2180,7 +2326,7 @@ function getDisplayPath(root, id) {
2180
2326
  }
2181
2327
  function createTransformPlugin(input) {
2182
2328
  let root = process.cwd();
2183
- let filter = createTransformFilter(root, input.options);
2329
+ let filter = createTransformFilter(root, input.context.getOptions());
2184
2330
  let logger;
2185
2331
  const warnedFiles = /* @__PURE__ */ new Set();
2186
2332
  const cache = /* @__PURE__ */ new Map();
@@ -2188,9 +2334,12 @@ function createTransformPlugin(input) {
2188
2334
  name: "spotpatch:transform",
2189
2335
  apply: "serve",
2190
2336
  enforce: "pre",
2337
+ config(config, environment) {
2338
+ input.configure?.(config, environment);
2339
+ },
2191
2340
  configResolved(config) {
2192
2341
  root = path9.resolve(config.root);
2193
- filter = createTransformFilter(root, input.options);
2342
+ filter = createTransformFilter(root, input.context.getOptions());
2194
2343
  logger = config.logger;
2195
2344
  },
2196
2345
  transform(code, id) {
@@ -2203,6 +2352,7 @@ function createTransformPlugin(input) {
2203
2352
  return cache.get(cacheKey) ?? null;
2204
2353
  }
2205
2354
  const startedAt = performance.now();
2355
+ const options = input.context.getOptions();
2206
2356
  try {
2207
2357
  const result = injectSourceMarkers({
2208
2358
  code,
@@ -2220,7 +2370,7 @@ function createTransformPlugin(input) {
2220
2370
  map: result.map.toString()
2221
2371
  });
2222
2372
  cache.set(cacheKey, output);
2223
- if (input.options.debug) {
2373
+ if (options.debug) {
2224
2374
  const elapsed = performance.now() - startedAt;
2225
2375
  logger?.info(
2226
2376
  `[spotpatch:transform] ${getDisplayPath(root, id)} ${elapsed.toFixed(2)}ms`
@@ -2230,7 +2380,7 @@ function createTransformPlugin(input) {
2230
2380
  } catch (error) {
2231
2381
  if (!warnedFiles.has(cleanId)) {
2232
2382
  warnedFiles.add(cleanId);
2233
- const detail = input.options.debug && error instanceof Error ? `: ${error.message}` : "";
2383
+ const detail = options.debug && error instanceof Error ? `: ${error.message}` : "";
2234
2384
  logger?.warn(
2235
2385
  `[spotpatch:transform] Failed to transform ${getDisplayPath(root, id)}; using original module${detail}`
2236
2386
  );
@@ -2243,16 +2393,46 @@ function createTransformPlugin(input) {
2243
2393
 
2244
2394
  // src/plugin.ts
2245
2395
  function spotPatch(userOptions = {}) {
2246
- const options = resolveOptions(userOptions);
2396
+ let options = resolveOptions(userOptions);
2397
+ let credentialEnvironment = Object.freeze({});
2247
2398
  if (!options.enabled) {
2248
2399
  return [];
2249
2400
  }
2250
2401
  const registry = createSourceRegistry();
2251
2402
  const session = createSession();
2403
+ const context = Object.freeze({
2404
+ getCredentialEnvironment: () => credentialEnvironment,
2405
+ getOptions: () => options
2406
+ });
2407
+ const configure = (config, environment) => {
2408
+ const root = path10.resolve(process.cwd(), config.root ?? ".");
2409
+ const loadedEnvironment = config.envDir === false ? process.env : loadEnv(environment.mode, path10.resolve(root, config.envDir ?? "."), "");
2410
+ const environmentAi = userOptions.ai === void 0 ? resolveEnvironmentAiConfiguration(loadedEnvironment).ai : false;
2411
+ options = resolveOptions(userOptions, environmentAi);
2412
+ if (options.ai === false) {
2413
+ credentialEnvironment = Object.freeze({});
2414
+ return;
2415
+ }
2416
+ const names = new Set(
2417
+ Object.values(options.ai.providers).map((provider) => provider.apiKeyEnv)
2418
+ );
2419
+ const missing = [...names].filter((name) => {
2420
+ const value = loadedEnvironment[name];
2421
+ return value === void 0 || value.trim().length === 0;
2422
+ });
2423
+ if (missing.length > 0) {
2424
+ throw new RangeError(
2425
+ `SpotPatch AI credential environment is missing ${missing.join(", ")}.`
2426
+ );
2427
+ }
2428
+ credentialEnvironment = Object.freeze(
2429
+ Object.fromEntries([...names].map((name) => [name, loadedEnvironment[name]]))
2430
+ );
2431
+ };
2252
2432
  return [
2253
- createTransformPlugin({ options, registry }),
2254
- createRuntimeInjectionPlugin({ options, session }),
2255
- createServerPlugin({ options, registry, session })
2433
+ createTransformPlugin({ configure, context, registry }),
2434
+ createRuntimeInjectionPlugin({ context, session }),
2435
+ createServerPlugin({ context, registry, session })
2256
2436
  ];
2257
2437
  }
2258
2438