@spotpatch/vite 1.0.0 → 1.1.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,39 @@ 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
+ Non-secret URL and model values can instead use the concise API:
46
+
47
+ ```ts
48
+ spotPatch({
49
+ ai: {
50
+ baseURL: "https://relay.example.com/v1",
51
+ model: "provider-model-name",
52
+ },
53
+ });
54
+ ```
55
+
56
+ The full provider map remains available for multiple providers, multiple models,
57
+ custom labels, checks, and limits.
41
58
 
42
59
  See the [repository README](https://github.com/huanglvjing/spotpatch#readme) for
43
60
  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");
@@ -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
@@ -348,7 +454,7 @@ function resolveOptions(options = {}) {
348
454
  debug: options.debug ?? DEFAULT_OPTIONS.debug,
349
455
  locale,
350
456
  maxTargets,
351
- ai: resolveAiOptions(options.ai)
457
+ ai: resolveAiOptions(options.ai ?? environmentAi)
352
458
  };
353
459
  if (resolved.shortcut.trim().length === 0) {
354
460
  throw new RangeError("SpotPatch shortcut cannot be empty.");
@@ -405,7 +511,7 @@ var import_node_path2 = __toESM(require("path"), 1);
405
511
  // package.json
406
512
  var package_default = {
407
513
  name: "@spotpatch/vite",
408
- version: "1.0.0",
514
+ version: "1.1.0",
409
515
  description: "Vite development plugin for SpotPatch.",
410
516
  license: "MIT",
411
517
  repository: {
@@ -497,15 +603,16 @@ function readConsumerViteVersion(root) {
497
603
  return import_vite.version;
498
604
  }
499
605
  function createClientModule(input, clientBundle, viteVersion) {
606
+ const options = input.context.getOptions();
500
607
  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,
608
+ ai: createRuntimeAiConfig(options.ai),
609
+ budget: options.budget,
610
+ debug: options.debug,
611
+ locale: options.locale,
612
+ maxTargets: options.maxTargets,
613
+ redact: options.redact,
507
614
  sessionToken: input.session.token,
508
- shortcut: input.options.shortcut,
615
+ shortcut: options.shortcut,
509
616
  spotPatchVersion: package_default.version,
510
617
  viteVersion
511
618
  };
@@ -628,7 +735,7 @@ function snapshot(job) {
628
735
  );
629
736
  }
630
737
  function capabilityCacheKey(provider, model) {
631
- const configurationDigest = (0, import_node_crypto2.createHash)("sha256").update(provider.baseURL).update("\0").update(provider.protocol).digest("hex");
738
+ const configurationDigest = (0, import_node_crypto2.createHash)("sha256").update(provider.baseURL).update("\0").update(provider.protocol).update("\0").update(provider.authentication).digest("hex");
632
739
  return `${provider.id}:${model.id}:${configurationDigest}`;
633
740
  }
634
741
  function freezeEvent(event) {
@@ -1553,18 +1660,18 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1553
1660
  "reverted",
1554
1661
  "failed"
1555
1662
  ]);
1556
- function matchAgentRequestPath(path10) {
1557
- if (path10 === import_shared7.SPOTPATCH_ENDPOINTS.agentCapability) {
1663
+ function matchAgentRequestPath(path11) {
1664
+ if (path11 === import_shared7.SPOTPATCH_ENDPOINTS.agentCapability) {
1558
1665
  return Object.freeze({ kind: "capability" });
1559
1666
  }
1560
- if (path10 === import_shared7.SPOTPATCH_ENDPOINTS.agentJobs) {
1667
+ if (path11 === import_shared7.SPOTPATCH_ENDPOINTS.agentJobs) {
1561
1668
  return Object.freeze({ kind: "create-job" });
1562
1669
  }
1563
1670
  const prefix = `${import_shared7.SPOTPATCH_ENDPOINTS.agentJobs}/`;
1564
- if (!path10.startsWith(prefix)) {
1671
+ if (!path11.startsWith(prefix)) {
1565
1672
  return void 0;
1566
1673
  }
1567
- const segments = path10.slice(prefix.length).split("/");
1674
+ const segments = path11.slice(prefix.length).split("/");
1568
1675
  const jobId = segments[0];
1569
1676
  const action = segments[1];
1570
1677
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -1906,9 +2013,9 @@ async function handleOpenEditor(request, options) {
1906
2013
  }
1907
2014
  function createSpotPatchMiddleware(options) {
1908
2015
  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}/`)) {
2016
+ const path11 = requestPath(request);
2017
+ const agentRoute = matchAgentRequestPath(path11);
2018
+ if (path11 !== import_shared9.SPOTPATCH_ENDPOINTS.sourceContext && path11 !== import_shared9.SPOTPATCH_ENDPOINTS.openEditor && agentRoute === void 0 && !path11.startsWith(`${import_shared9.SPOTPATCH_API_BASE}/`)) {
1912
2019
  next();
1913
2020
  return;
1914
2021
  }
@@ -1917,7 +2024,7 @@ function createSpotPatchMiddleware(options) {
1917
2024
  allowLan: options.options.allowLan,
1918
2025
  sessionToken: options.session.token
1919
2026
  });
1920
- if (path10 === import_shared9.SPOTPATCH_ENDPOINTS.sourceContext) {
2027
+ if (path11 === import_shared9.SPOTPATCH_ENDPOINTS.sourceContext) {
1921
2028
  if (request.method !== "POST") {
1922
2029
  throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.INVALID_REQUEST);
1923
2030
  }
@@ -1925,7 +2032,7 @@ function createSpotPatchMiddleware(options) {
1925
2032
  writeJson(response, 200, { ok: true, data });
1926
2033
  return;
1927
2034
  }
1928
- if (path10 === import_shared9.SPOTPATCH_ENDPOINTS.openEditor) {
2035
+ if (path11 === import_shared9.SPOTPATCH_ENDPOINTS.openEditor) {
1929
2036
  if (request.method !== "POST") {
1930
2037
  throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.INVALID_REQUEST);
1931
2038
  }
@@ -1973,11 +2080,16 @@ function createServerPlugin(input) {
1973
2080
  throw new Error("SpotPatch server initialized before Vite config resolution.");
1974
2081
  }
1975
2082
  const root = import_node_path6.default.resolve(config.root);
1976
- agentManager = input.options.ai === false ? void 0 : createAgentJobManager({ ai: input.options.ai, root });
2083
+ const options = input.context.getOptions();
2084
+ agentManager = options.ai === false ? void 0 : createAgentJobManager({
2085
+ ai: options.ai,
2086
+ environment: input.context.getCredentialEnvironment(),
2087
+ root
2088
+ });
1977
2089
  server.middlewares.use(
1978
2090
  createSpotPatchMiddleware({
1979
2091
  ...agentManager === void 0 ? {} : { agentManager },
1980
- options: input.options,
2092
+ options,
1981
2093
  registry: input.registry,
1982
2094
  root,
1983
2095
  session: input.session,
@@ -1988,7 +2100,7 @@ function createServerPlugin(input) {
1988
2100
  void closeResources();
1989
2101
  });
1990
2102
  config.logger.info(
1991
- `[spotpatch:vite] Ready. Toggle picker with ${input.options.shortcut}.`
2103
+ `[spotpatch:vite] Ready. Toggle picker with ${options.shortcut}.`
1992
2104
  );
1993
2105
  },
1994
2106
  async closeBundle() {
@@ -2180,7 +2292,7 @@ function getDisplayPath(root, id) {
2180
2292
  }
2181
2293
  function createTransformPlugin(input) {
2182
2294
  let root = process.cwd();
2183
- let filter = createTransformFilter(root, input.options);
2295
+ let filter = createTransformFilter(root, input.context.getOptions());
2184
2296
  let logger;
2185
2297
  const warnedFiles = /* @__PURE__ */ new Set();
2186
2298
  const cache = /* @__PURE__ */ new Map();
@@ -2188,9 +2300,12 @@ function createTransformPlugin(input) {
2188
2300
  name: "spotpatch:transform",
2189
2301
  apply: "serve",
2190
2302
  enforce: "pre",
2303
+ config(config, environment) {
2304
+ input.configure?.(config, environment);
2305
+ },
2191
2306
  configResolved(config) {
2192
2307
  root = import_node_path9.default.resolve(config.root);
2193
- filter = createTransformFilter(root, input.options);
2308
+ filter = createTransformFilter(root, input.context.getOptions());
2194
2309
  logger = config.logger;
2195
2310
  },
2196
2311
  transform(code, id) {
@@ -2203,6 +2318,7 @@ function createTransformPlugin(input) {
2203
2318
  return cache.get(cacheKey) ?? null;
2204
2319
  }
2205
2320
  const startedAt = performance.now();
2321
+ const options = input.context.getOptions();
2206
2322
  try {
2207
2323
  const result = injectSourceMarkers({
2208
2324
  code,
@@ -2220,7 +2336,7 @@ function createTransformPlugin(input) {
2220
2336
  map: result.map.toString()
2221
2337
  });
2222
2338
  cache.set(cacheKey, output);
2223
- if (input.options.debug) {
2339
+ if (options.debug) {
2224
2340
  const elapsed = performance.now() - startedAt;
2225
2341
  logger?.info(
2226
2342
  `[spotpatch:transform] ${getDisplayPath(root, id)} ${elapsed.toFixed(2)}ms`
@@ -2230,7 +2346,7 @@ function createTransformPlugin(input) {
2230
2346
  } catch (error) {
2231
2347
  if (!warnedFiles.has(cleanId)) {
2232
2348
  warnedFiles.add(cleanId);
2233
- const detail = input.options.debug && error instanceof Error ? `: ${error.message}` : "";
2349
+ const detail = options.debug && error instanceof Error ? `: ${error.message}` : "";
2234
2350
  logger?.warn(
2235
2351
  `[spotpatch:transform] Failed to transform ${getDisplayPath(root, id)}; using original module${detail}`
2236
2352
  );
@@ -2243,16 +2359,46 @@ function createTransformPlugin(input) {
2243
2359
 
2244
2360
  // src/plugin.ts
2245
2361
  function spotPatch(userOptions = {}) {
2246
- const options = resolveOptions(userOptions);
2362
+ let options = resolveOptions(userOptions);
2363
+ let credentialEnvironment = Object.freeze({});
2247
2364
  if (!options.enabled) {
2248
2365
  return [];
2249
2366
  }
2250
2367
  const registry = createSourceRegistry();
2251
2368
  const session = createSession();
2369
+ const context = Object.freeze({
2370
+ getCredentialEnvironment: () => credentialEnvironment,
2371
+ getOptions: () => options
2372
+ });
2373
+ const configure = (config, environment) => {
2374
+ const root = import_node_path10.default.resolve(process.cwd(), config.root ?? ".");
2375
+ const loadedEnvironment = config.envDir === false ? process.env : (0, import_vite2.loadEnv)(environment.mode, import_node_path10.default.resolve(root, config.envDir ?? "."), "");
2376
+ const environmentAi = userOptions.ai === void 0 ? resolveEnvironmentAiConfiguration(loadedEnvironment).ai : false;
2377
+ options = resolveOptions(userOptions, environmentAi);
2378
+ if (options.ai === false) {
2379
+ credentialEnvironment = Object.freeze({});
2380
+ return;
2381
+ }
2382
+ const names = new Set(
2383
+ Object.values(options.ai.providers).map((provider) => provider.apiKeyEnv)
2384
+ );
2385
+ const missing = [...names].filter((name) => {
2386
+ const value = loadedEnvironment[name];
2387
+ return value === void 0 || value.trim().length === 0;
2388
+ });
2389
+ if (missing.length > 0) {
2390
+ throw new RangeError(
2391
+ `SpotPatch AI credential environment is missing ${missing.join(", ")}.`
2392
+ );
2393
+ }
2394
+ credentialEnvironment = Object.freeze(
2395
+ Object.fromEntries([...names].map((name) => [name, loadedEnvironment[name]]))
2396
+ );
2397
+ };
2252
2398
  return [
2253
- createTransformPlugin({ options, registry }),
2254
- createRuntimeInjectionPlugin({ options, session }),
2255
- createServerPlugin({ options, registry, session })
2399
+ createTransformPlugin({ configure, context, registry }),
2400
+ createRuntimeInjectionPlugin({ context, session }),
2401
+ createServerPlugin({ context, registry, session })
2256
2402
  ];
2257
2403
  }
2258
2404