@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/README.md CHANGED
@@ -22,22 +22,45 @@ import react from "@vitejs/plugin-react-swc";
22
22
  import { defineConfig } from "vite";
23
23
 
24
24
  export default defineConfig({
25
- plugins: [
26
- spotPatch({
27
- editor: "vscode",
28
- redact: true,
29
- allowLan: false,
30
- locale: "auto",
31
- maxTargets: 8,
32
- }),
33
- react(),
34
- ],
25
+ plugins: [spotPatch(), react()],
35
26
  });
36
27
  ```
37
28
 
38
29
  SpotPatch injects no runtime, source markers, or local API endpoints into a
39
- production build. AI execution is disabled unless a trusted Node-side provider
40
- profile is explicitly configured; API keys must never use a `VITE_` prefix.
30
+ production build. AI stays disabled when no AI environment exists. To enable the
31
+ single-provider setup without changing `vite.config.ts`, add the three required
32
+ values to a Git-ignored `.env.local`:
33
+
34
+ ```dotenv
35
+ SPOTPATCH_AI_BASE_URL=https://relay.example.com/v1
36
+ SPOTPATCH_AI_MODEL=provider-model-name
37
+ SPOTPATCH_AI_API_KEY=<your-key>
38
+ ```
39
+
40
+ `SPOTPATCH_AI_PROTOCOL` optionally selects `chat-completions` (the default) or
41
+ `responses`. `SPOTPATCH_AI_AUTHENTICATION` optionally selects `bearer` (the
42
+ default) or `x-api-key`. Partial environment configuration fails fast without
43
+ printing credential values. API keys must never use a `VITE_` prefix.
44
+
45
+ Source actions auto-detect Cursor or VS Code and open the selected file at its
46
+ exact line and column. Set `editor: "cursor"` or `editor: "vscode"` only when an
47
+ explicit preference is required. The workbench also links to the
48
+ [SpotPatch GitHub repository](https://github.com/huanglvjing/spotpatch) for docs,
49
+ issues, and project updates.
50
+
51
+ Non-secret URL and model values can instead use the concise API:
52
+
53
+ ```ts
54
+ spotPatch({
55
+ ai: {
56
+ baseURL: "https://relay.example.com/v1",
57
+ model: "provider-model-name",
58
+ },
59
+ });
60
+ ```
61
+
62
+ The full provider map remains available for multiple providers, multiple models,
63
+ custom labels, checks, and limits.
41
64
 
42
65
  See the [repository README](https://github.com/huanglvjing/spotpatch#readme) for
43
66
  the complete setup and security model.
package/dist/index.cjs CHANGED
@@ -37,6 +37,71 @@ __export(index_exports, {
37
37
  });
38
38
  module.exports = __toCommonJS(index_exports);
39
39
 
40
+ // src/plugin.ts
41
+ var import_node_path10 = __toESM(require("path"), 1);
42
+ var import_vite2 = require("vite");
43
+
44
+ // src/environment-ai.ts
45
+ var AI_ENVIRONMENT_NAMES = Object.freeze({
46
+ authentication: "SPOTPATCH_AI_AUTHENTICATION",
47
+ baseURL: "SPOTPATCH_AI_BASE_URL",
48
+ credential: "SPOTPATCH_AI_API_KEY",
49
+ model: "SPOTPATCH_AI_MODEL",
50
+ protocol: "SPOTPATCH_AI_PROTOCOL"
51
+ });
52
+ function normalizedValue(environment, name) {
53
+ const value = environment[name];
54
+ if (value === void 0 || value.trim().length === 0) {
55
+ return void 0;
56
+ }
57
+ return value.trim();
58
+ }
59
+ function resolveEnvironmentAiConfiguration(environment) {
60
+ const baseURL = normalizedValue(environment, AI_ENVIRONMENT_NAMES.baseURL);
61
+ const model = normalizedValue(environment, AI_ENVIRONMENT_NAMES.model);
62
+ const credential = normalizedValue(environment, AI_ENVIRONMENT_NAMES.credential);
63
+ const protocol = normalizedValue(environment, AI_ENVIRONMENT_NAMES.protocol);
64
+ const authentication = normalizedValue(
65
+ environment,
66
+ AI_ENVIRONMENT_NAMES.authentication
67
+ );
68
+ const configuredValues = [baseURL, model, credential, protocol, authentication];
69
+ if (configuredValues.every((value) => value === void 0)) {
70
+ return Object.freeze({ ai: false });
71
+ }
72
+ const missing = [
73
+ [AI_ENVIRONMENT_NAMES.baseURL, baseURL],
74
+ [AI_ENVIRONMENT_NAMES.model, model],
75
+ [AI_ENVIRONMENT_NAMES.credential, credential]
76
+ ].filter((entry) => entry[1] === void 0).map(([name]) => name);
77
+ if (missing.length > 0) {
78
+ throw new RangeError(
79
+ `SpotPatch AI environment configuration is incomplete; missing ${missing.join(", ")}.`
80
+ );
81
+ }
82
+ if (baseURL === void 0 || model === void 0 || credential === void 0) {
83
+ throw new RangeError("SpotPatch AI environment configuration is incomplete.");
84
+ }
85
+ if (protocol !== void 0 && protocol !== "responses" && protocol !== "chat-completions") {
86
+ throw new RangeError(
87
+ "SpotPatch SPOTPATCH_AI_PROTOCOL must be responses or chat-completions."
88
+ );
89
+ }
90
+ if (authentication !== void 0 && authentication !== "bearer" && authentication !== "x-api-key") {
91
+ throw new RangeError(
92
+ "SpotPatch SPOTPATCH_AI_AUTHENTICATION must be bearer or x-api-key."
93
+ );
94
+ }
95
+ return Object.freeze({
96
+ ai: Object.freeze({
97
+ baseURL,
98
+ model,
99
+ ...protocol === void 0 ? {} : { protocol },
100
+ ...authentication === void 0 ? {} : { authentication }
101
+ })
102
+ });
103
+ }
104
+
40
105
  // src/options.ts
41
106
  var import_shared = require("@spotpatch/shared");
42
107
  var import_zod = require("zod");
@@ -61,7 +126,7 @@ var DEFAULT_OPTIONS = Object.freeze({
61
126
  enabled: true,
62
127
  include: DEFAULT_INCLUDE,
63
128
  exclude: DEFAULT_EXCLUDE,
64
- editor: "vscode",
129
+ editor: "auto",
65
130
  redact: true,
66
131
  budget: DEFAULT_BUDGET,
67
132
  shortcut: "Mod+Shift+S",
@@ -101,6 +166,7 @@ var aiOptionsSchema = import_zod.z.strictObject({
101
166
  type: import_zod.z.literal("openai-compatible"),
102
167
  label: import_zod.z.string(),
103
168
  protocol: import_zod.z.enum(["responses", "chat-completions"]),
169
+ authentication: import_zod.z.enum(["bearer", "x-api-key"]).optional(),
104
170
  baseURL: import_zod.z.string(),
105
171
  apiKeyEnv: import_zod.z.string(),
106
172
  models: import_zod.z.record(
@@ -118,6 +184,16 @@ var aiOptionsSchema = import_zod.z.strictObject({
118
184
  limits: agentLimitsSchema
119
185
  }).optional()
120
186
  });
187
+ var simpleAiOptionsSchema = import_zod.z.strictObject({
188
+ baseURL: import_zod.z.string(),
189
+ model: import_zod.z.string(),
190
+ apiKeyEnv: import_zod.z.string().optional(),
191
+ protocol: import_zod.z.enum(["responses", "chat-completions"]).optional(),
192
+ authentication: import_zod.z.enum(["bearer", "x-api-key"]).optional(),
193
+ providerLabel: import_zod.z.string().optional(),
194
+ modelLabel: import_zod.z.string().optional(),
195
+ execution: aiOptionsSchema.shape.execution
196
+ });
121
197
  function assertIdentifier(value, label) {
122
198
  if (!PROFILE_ID_PATTERN.test(value)) {
123
199
  throw new RangeError(
@@ -217,6 +293,7 @@ function resolveProviders(providers) {
217
293
  type: provider.type,
218
294
  label: nonEmpty(provider.label, "provider label", 100),
219
295
  protocol: provider.protocol,
296
+ authentication: provider.authentication ?? "bearer",
220
297
  baseURL: normalizeProviderBaseURL(provider.baseURL),
221
298
  apiKeyEnv: provider.apiKeyEnv,
222
299
  models,
@@ -260,7 +337,36 @@ function resolveAiOptions(options) {
260
337
  if (options === void 0 || options === false) {
261
338
  return false;
262
339
  }
263
- const parsed = aiOptionsSchema.safeParse(options);
340
+ const expanded = "providers" in options ? options : (() => {
341
+ const simple = simpleAiOptionsSchema.safeParse(options);
342
+ if (!simple.success) {
343
+ throw new RangeError("SpotPatch AI configuration is invalid.");
344
+ }
345
+ const providerId = "default";
346
+ const modelId = "default";
347
+ return {
348
+ providers: {
349
+ [providerId]: {
350
+ type: "openai-compatible",
351
+ label: simple.data.providerLabel ?? "AI provider",
352
+ protocol: simple.data.protocol ?? "chat-completions",
353
+ authentication: simple.data.authentication ?? "bearer",
354
+ baseURL: simple.data.baseURL,
355
+ apiKeyEnv: simple.data.apiKeyEnv ?? "SPOTPATCH_AI_API_KEY",
356
+ models: {
357
+ [modelId]: {
358
+ label: simple.data.modelLabel ?? "AI model",
359
+ model: simple.data.model
360
+ }
361
+ },
362
+ defaultModel: modelId
363
+ }
364
+ },
365
+ defaultProvider: providerId,
366
+ ...simple.data.execution === void 0 ? {} : { execution: simple.data.execution }
367
+ };
368
+ })();
369
+ const parsed = aiOptionsSchema.safeParse(expanded);
264
370
  if (!parsed.success) {
265
371
  throw new RangeError("SpotPatch AI configuration is invalid.");
266
372
  }
@@ -320,7 +426,7 @@ function assertPositiveBudget(budget) {
320
426
  }
321
427
  }
322
428
  }
323
- function resolveOptions(options = {}) {
429
+ function resolveOptions(options = {}, environmentAi) {
324
430
  const budget = Object.freeze({
325
431
  ...DEFAULT_OPTIONS.budget,
326
432
  ...options.budget
@@ -328,9 +434,13 @@ function resolveOptions(options = {}) {
328
434
  assertPositiveBudget(budget);
329
435
  const maxTargets = options.maxTargets ?? DEFAULT_OPTIONS.maxTargets;
330
436
  const locale = options.locale ?? DEFAULT_OPTIONS.locale;
437
+ const editor = options.editor ?? DEFAULT_OPTIONS.editor;
331
438
  if (!import_shared.SPOTPATCH_LOCALE_PREFERENCES.includes(locale)) {
332
439
  throw new RangeError("SpotPatch locale must be auto, en-US, or zh-CN.");
333
440
  }
441
+ if (!import_shared.SPOTPATCH_EDITOR_PREFERENCES.includes(editor)) {
442
+ throw new RangeError("SpotPatch editor must be auto, vscode, or cursor.");
443
+ }
334
444
  if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets > import_shared.MAX_ANNOTATION_TARGETS) {
335
445
  throw new RangeError(
336
446
  `SpotPatch maxTargets must be an integer between 1 and ${String(import_shared.MAX_ANNOTATION_TARGETS)}.`
@@ -340,7 +450,7 @@ function resolveOptions(options = {}) {
340
450
  enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,
341
451
  include: Object.freeze([...options.include ?? DEFAULT_OPTIONS.include]),
342
452
  exclude: Object.freeze([...options.exclude ?? DEFAULT_OPTIONS.exclude]),
343
- editor: options.editor ?? DEFAULT_OPTIONS.editor,
453
+ editor,
344
454
  redact: options.redact ?? DEFAULT_OPTIONS.redact,
345
455
  budget,
346
456
  shortcut: options.shortcut ?? DEFAULT_OPTIONS.shortcut,
@@ -348,7 +458,7 @@ function resolveOptions(options = {}) {
348
458
  debug: options.debug ?? DEFAULT_OPTIONS.debug,
349
459
  locale,
350
460
  maxTargets,
351
- ai: resolveAiOptions(options.ai)
461
+ ai: resolveAiOptions(options.ai ?? environmentAi)
352
462
  };
353
463
  if (resolved.shortcut.trim().length === 0) {
354
464
  throw new RangeError("SpotPatch shortcut cannot be empty.");
@@ -405,7 +515,7 @@ var import_node_path2 = __toESM(require("path"), 1);
405
515
  // package.json
406
516
  var package_default = {
407
517
  name: "@spotpatch/vite",
408
- version: "1.0.0",
518
+ version: "1.2.0",
409
519
  description: "Vite development plugin for SpotPatch.",
410
520
  license: "MIT",
411
521
  repository: {
@@ -497,15 +607,17 @@ function readConsumerViteVersion(root) {
497
607
  return import_vite.version;
498
608
  }
499
609
  function createClientModule(input, clientBundle, viteVersion) {
610
+ const options = input.context.getOptions();
500
611
  const runtimeConfig = {
501
- ai: createRuntimeAiConfig(input.options.ai),
502
- budget: input.options.budget,
503
- debug: input.options.debug,
504
- locale: input.options.locale,
505
- maxTargets: input.options.maxTargets,
506
- redact: input.options.redact,
612
+ ai: createRuntimeAiConfig(options.ai),
613
+ budget: options.budget,
614
+ debug: options.debug,
615
+ editor: options.editor,
616
+ locale: options.locale,
617
+ maxTargets: options.maxTargets,
618
+ redact: options.redact,
507
619
  sessionToken: input.session.token,
508
- shortcut: input.options.shortcut,
620
+ shortcut: options.shortcut,
509
621
  spotPatchVersion: package_default.version,
510
622
  viteVersion
511
623
  };
@@ -628,7 +740,7 @@ function snapshot(job) {
628
740
  );
629
741
  }
630
742
  function capabilityCacheKey(provider, model) {
631
- const configurationDigest = (0, import_node_crypto2.createHash)("sha256").update(provider.baseURL).update("\0").update(provider.protocol).digest("hex");
743
+ const configurationDigest = (0, import_node_crypto2.createHash)("sha256").update(provider.baseURL).update("\0").update(provider.protocol).update("\0").update(provider.authentication).digest("hex");
632
744
  return `${provider.id}:${model.id}:${configurationDigest}`;
633
745
  }
634
746
  function freezeEvent(event) {
@@ -1553,18 +1665,18 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1553
1665
  "reverted",
1554
1666
  "failed"
1555
1667
  ]);
1556
- function matchAgentRequestPath(path10) {
1557
- if (path10 === import_shared7.SPOTPATCH_ENDPOINTS.agentCapability) {
1668
+ function matchAgentRequestPath(path11) {
1669
+ if (path11 === import_shared7.SPOTPATCH_ENDPOINTS.agentCapability) {
1558
1670
  return Object.freeze({ kind: "capability" });
1559
1671
  }
1560
- if (path10 === import_shared7.SPOTPATCH_ENDPOINTS.agentJobs) {
1672
+ if (path11 === import_shared7.SPOTPATCH_ENDPOINTS.agentJobs) {
1561
1673
  return Object.freeze({ kind: "create-job" });
1562
1674
  }
1563
1675
  const prefix = `${import_shared7.SPOTPATCH_ENDPOINTS.agentJobs}/`;
1564
- if (!path10.startsWith(prefix)) {
1676
+ if (!path11.startsWith(prefix)) {
1565
1677
  return void 0;
1566
1678
  }
1567
- const segments = path10.slice(prefix.length).split("/");
1679
+ const segments = path11.slice(prefix.length).split("/");
1568
1680
  const jobId = segments[0];
1569
1681
  const action = segments[1];
1570
1682
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -1709,9 +1821,36 @@ async function handleAgentRequest(request, response, options, route, writeSucces
1709
1821
 
1710
1822
  // src/server/editor.ts
1711
1823
  var import_launch_editor = __toESM(require("launch-editor"), 1);
1712
- var launchVSCode = (target, onError) => {
1713
- (0, import_launch_editor.default)(target, "code", onError);
1714
- };
1824
+ var EDITOR_STARTUP_GRACE_MS = 150;
1825
+ function editorCommand(editor) {
1826
+ if (editor === "vscode") {
1827
+ return "code";
1828
+ }
1829
+ if (editor === "cursor") {
1830
+ return "cursor";
1831
+ }
1832
+ return void 0;
1833
+ }
1834
+ var launchConfiguredEditor = (target, editor) => new Promise((resolve, reject) => {
1835
+ let settled = false;
1836
+ const startupTimer = setTimeout(() => {
1837
+ settled = true;
1838
+ resolve();
1839
+ }, EDITOR_STARTUP_GRACE_MS);
1840
+ const rejectStartup = () => {
1841
+ if (settled) {
1842
+ return;
1843
+ }
1844
+ settled = true;
1845
+ clearTimeout(startupTimer);
1846
+ reject(new Error("The configured editor could not be started."));
1847
+ };
1848
+ try {
1849
+ (0, import_launch_editor.default)(target, editorCommand(editor), rejectStartup);
1850
+ } catch {
1851
+ rejectStartup();
1852
+ }
1853
+ });
1715
1854
 
1716
1855
  // src/server/request-security.ts
1717
1856
  var import_node_crypto3 = require("crypto");
@@ -1892,23 +2031,24 @@ async function handleOpenEditor(request, options) {
1892
2031
  root: options.root
1893
2032
  });
1894
2033
  const target = `${sourcePath}:${String(body.line)}:${String(body.column)}`;
1895
- const editorLauncher = options.editorLauncher ?? launchVSCode;
2034
+ const editorLauncher = options.editorLauncher ?? launchConfiguredEditor;
1896
2035
  try {
1897
- editorLauncher(target, () => {
1898
- options.logger?.warn("[spotpatch:server] VS Code rejected an editor request.");
1899
- });
2036
+ await editorLauncher(target, options.options.editor);
1900
2037
  } catch (error) {
2038
+ options.logger?.warn(
2039
+ `[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
2040
+ );
1901
2041
  throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.EDITOR_OPEN_FAILED, void 0, {
1902
2042
  cause: error
1903
2043
  });
1904
2044
  }
1905
- return Object.freeze({});
2045
+ return Object.freeze({ editor: options.options.editor });
1906
2046
  }
1907
2047
  function createSpotPatchMiddleware(options) {
1908
2048
  return (request, response, next) => {
1909
- const path10 = requestPath(request);
1910
- const agentRoute = matchAgentRequestPath(path10);
1911
- if (path10 !== import_shared9.SPOTPATCH_ENDPOINTS.sourceContext && path10 !== import_shared9.SPOTPATCH_ENDPOINTS.openEditor && agentRoute === void 0 && !path10.startsWith(`${import_shared9.SPOTPATCH_API_BASE}/`)) {
2049
+ const path11 = requestPath(request);
2050
+ const agentRoute = matchAgentRequestPath(path11);
2051
+ if (path11 !== import_shared9.SPOTPATCH_ENDPOINTS.sourceContext && path11 !== import_shared9.SPOTPATCH_ENDPOINTS.openEditor && agentRoute === void 0 && !path11.startsWith(`${import_shared9.SPOTPATCH_API_BASE}/`)) {
1912
2052
  next();
1913
2053
  return;
1914
2054
  }
@@ -1917,7 +2057,7 @@ function createSpotPatchMiddleware(options) {
1917
2057
  allowLan: options.options.allowLan,
1918
2058
  sessionToken: options.session.token
1919
2059
  });
1920
- if (path10 === import_shared9.SPOTPATCH_ENDPOINTS.sourceContext) {
2060
+ if (path11 === import_shared9.SPOTPATCH_ENDPOINTS.sourceContext) {
1921
2061
  if (request.method !== "POST") {
1922
2062
  throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.INVALID_REQUEST);
1923
2063
  }
@@ -1925,7 +2065,7 @@ function createSpotPatchMiddleware(options) {
1925
2065
  writeJson(response, 200, { ok: true, data });
1926
2066
  return;
1927
2067
  }
1928
- if (path10 === import_shared9.SPOTPATCH_ENDPOINTS.openEditor) {
2068
+ if (path11 === import_shared9.SPOTPATCH_ENDPOINTS.openEditor) {
1929
2069
  if (request.method !== "POST") {
1930
2070
  throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.INVALID_REQUEST);
1931
2071
  }
@@ -1973,11 +2113,16 @@ function createServerPlugin(input) {
1973
2113
  throw new Error("SpotPatch server initialized before Vite config resolution.");
1974
2114
  }
1975
2115
  const root = import_node_path6.default.resolve(config.root);
1976
- agentManager = input.options.ai === false ? void 0 : createAgentJobManager({ ai: input.options.ai, root });
2116
+ const options = input.context.getOptions();
2117
+ agentManager = options.ai === false ? void 0 : createAgentJobManager({
2118
+ ai: options.ai,
2119
+ environment: input.context.getCredentialEnvironment(),
2120
+ root
2121
+ });
1977
2122
  server.middlewares.use(
1978
2123
  createSpotPatchMiddleware({
1979
2124
  ...agentManager === void 0 ? {} : { agentManager },
1980
- options: input.options,
2125
+ options,
1981
2126
  registry: input.registry,
1982
2127
  root,
1983
2128
  session: input.session,
@@ -1988,7 +2133,7 @@ function createServerPlugin(input) {
1988
2133
  void closeResources();
1989
2134
  });
1990
2135
  config.logger.info(
1991
- `[spotpatch:vite] Ready. Toggle picker with ${input.options.shortcut}.`
2136
+ `[spotpatch:vite] Ready. Toggle picker with ${options.shortcut}.`
1992
2137
  );
1993
2138
  },
1994
2139
  async closeBundle() {
@@ -2180,7 +2325,7 @@ function getDisplayPath(root, id) {
2180
2325
  }
2181
2326
  function createTransformPlugin(input) {
2182
2327
  let root = process.cwd();
2183
- let filter = createTransformFilter(root, input.options);
2328
+ let filter = createTransformFilter(root, input.context.getOptions());
2184
2329
  let logger;
2185
2330
  const warnedFiles = /* @__PURE__ */ new Set();
2186
2331
  const cache = /* @__PURE__ */ new Map();
@@ -2188,9 +2333,12 @@ function createTransformPlugin(input) {
2188
2333
  name: "spotpatch:transform",
2189
2334
  apply: "serve",
2190
2335
  enforce: "pre",
2336
+ config(config, environment) {
2337
+ input.configure?.(config, environment);
2338
+ },
2191
2339
  configResolved(config) {
2192
2340
  root = import_node_path9.default.resolve(config.root);
2193
- filter = createTransformFilter(root, input.options);
2341
+ filter = createTransformFilter(root, input.context.getOptions());
2194
2342
  logger = config.logger;
2195
2343
  },
2196
2344
  transform(code, id) {
@@ -2203,6 +2351,7 @@ function createTransformPlugin(input) {
2203
2351
  return cache.get(cacheKey) ?? null;
2204
2352
  }
2205
2353
  const startedAt = performance.now();
2354
+ const options = input.context.getOptions();
2206
2355
  try {
2207
2356
  const result = injectSourceMarkers({
2208
2357
  code,
@@ -2220,7 +2369,7 @@ function createTransformPlugin(input) {
2220
2369
  map: result.map.toString()
2221
2370
  });
2222
2371
  cache.set(cacheKey, output);
2223
- if (input.options.debug) {
2372
+ if (options.debug) {
2224
2373
  const elapsed = performance.now() - startedAt;
2225
2374
  logger?.info(
2226
2375
  `[spotpatch:transform] ${getDisplayPath(root, id)} ${elapsed.toFixed(2)}ms`
@@ -2230,7 +2379,7 @@ function createTransformPlugin(input) {
2230
2379
  } catch (error) {
2231
2380
  if (!warnedFiles.has(cleanId)) {
2232
2381
  warnedFiles.add(cleanId);
2233
- const detail = input.options.debug && error instanceof Error ? `: ${error.message}` : "";
2382
+ const detail = options.debug && error instanceof Error ? `: ${error.message}` : "";
2234
2383
  logger?.warn(
2235
2384
  `[spotpatch:transform] Failed to transform ${getDisplayPath(root, id)}; using original module${detail}`
2236
2385
  );
@@ -2243,16 +2392,46 @@ function createTransformPlugin(input) {
2243
2392
 
2244
2393
  // src/plugin.ts
2245
2394
  function spotPatch(userOptions = {}) {
2246
- const options = resolveOptions(userOptions);
2395
+ let options = resolveOptions(userOptions);
2396
+ let credentialEnvironment = Object.freeze({});
2247
2397
  if (!options.enabled) {
2248
2398
  return [];
2249
2399
  }
2250
2400
  const registry = createSourceRegistry();
2251
2401
  const session = createSession();
2402
+ const context = Object.freeze({
2403
+ getCredentialEnvironment: () => credentialEnvironment,
2404
+ getOptions: () => options
2405
+ });
2406
+ const configure = (config, environment) => {
2407
+ const root = import_node_path10.default.resolve(process.cwd(), config.root ?? ".");
2408
+ const loadedEnvironment = config.envDir === false ? process.env : (0, import_vite2.loadEnv)(environment.mode, import_node_path10.default.resolve(root, config.envDir ?? "."), "");
2409
+ const environmentAi = userOptions.ai === void 0 ? resolveEnvironmentAiConfiguration(loadedEnvironment).ai : false;
2410
+ options = resolveOptions(userOptions, environmentAi);
2411
+ if (options.ai === false) {
2412
+ credentialEnvironment = Object.freeze({});
2413
+ return;
2414
+ }
2415
+ const names = new Set(
2416
+ Object.values(options.ai.providers).map((provider) => provider.apiKeyEnv)
2417
+ );
2418
+ const missing = [...names].filter((name) => {
2419
+ const value = loadedEnvironment[name];
2420
+ return value === void 0 || value.trim().length === 0;
2421
+ });
2422
+ if (missing.length > 0) {
2423
+ throw new RangeError(
2424
+ `SpotPatch AI credential environment is missing ${missing.join(", ")}.`
2425
+ );
2426
+ }
2427
+ credentialEnvironment = Object.freeze(
2428
+ Object.fromEntries([...names].map((name) => [name, loadedEnvironment[name]]))
2429
+ );
2430
+ };
2252
2431
  return [
2253
- createTransformPlugin({ options, registry }),
2254
- createRuntimeInjectionPlugin({ options, session }),
2255
- createServerPlugin({ options, registry, session })
2432
+ createTransformPlugin({ configure, context, registry }),
2433
+ createRuntimeInjectionPlugin({ context, session }),
2434
+ createServerPlugin({ context, registry, session })
2256
2435
  ];
2257
2436
  }
2258
2437