Explorar el Código

core: add documentation comments to plugin configuration merge logic

Adds explanatory comments to config.ts and plugin.ts clarifying:

- How plugin specs are stored and normalized during config loading

- Why plugin_origins tracks provenance for location-sensitive decisions

- Why path-like specs are resolved early to prevent reinterpretation during merges

- How plugin deduplication works while keeping origin metadata for writes and diagnostics
Dax Raad hace 4 meses
padre
commit
8b1f0e2d90
Se han modificado 2 ficheros con 28 adiciones y 6 borrados
  1. 18 5
      packages/opencode/src/config/config.ts
  2. 10 1
      packages/opencode/src/config/plugin.ts

+ 18 - 5
packages/opencode/src/config/config.ts

@@ -93,6 +93,7 @@ export const Info = z
       .describe(
         "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
       ),
+    // User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
     plugin: ConfigPlugin.Spec.array().optional(),
     share: z
       .enum(["manual", "auto", "disabled"])
@@ -267,6 +268,8 @@ export const Info = z
   })
 
 export type Info = z.output<typeof Info> & {
+  // plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
+  // with the file and scope it came from so later runtime code can make location-sensitive decisions.
   plugin_origins?: ConfigPlugin.Origin[]
 }
 
@@ -420,6 +423,8 @@ export const layer = Layer.effect(
         if (data.plugin && isFile) {
           const list = data.plugin
           for (let i = 0; i < list.length; i++) {
+            // Normalize path-like plugin specs while we still know which config file declared them.
+            // This prevents `./plugin.ts` from being reinterpreted relative to some later merge location.
             list[i] = yield* Effect.promise(() => ConfigPlugin.resolvePluginSpec(list[i], options.path))
           }
         }
@@ -505,20 +510,26 @@ export const layer = Layer.effect(
       const consoleManagedProviders = new Set<string>()
       let activeOrgName: string | undefined
 
-      const scope = Effect.fnUntraced(function* (source: string) {
+      const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
         if (source.startsWith("http://") || source.startsWith("https://")) return "global"
         if (source === "OPENCODE_CONFIG_CONTENT") return "local"
         if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
         return "global"
       })
 
-      const track = Effect.fnUntraced(function* (
+      const mergePluginOrigins = Effect.fnUntraced(function* (
         source: string,
+        // mergePluginOrigins receives raw Specs from one config source, before provenance for this merge step
+        // is attached.
         list: ConfigPlugin.Spec[] | undefined,
+        // Scope can be inferred from the source path, but some callers already know whether the config should
+        // behave as global or local and can pass that explicitly.
         kind?: ConfigPlugin.Scope,
       ) {
         if (!list?.length) return
-        const hit = kind ?? (yield* scope(source))
+        const hit = kind ?? (yield* pluginScopeForSource(source))
+        // Merge newly seen plugin origins with previously collected ones, then dedupe by plugin identity while
+        // keeping the winning source/scope metadata for downstream installs, writes, and diagnostics.
         const plugins = ConfigPlugin.deduplicatePluginOrigins([
           ...(result.plugin_origins ?? []),
           ...list.map((spec) => ({ spec, source, scope: hit })),
@@ -529,7 +540,7 @@ export const layer = Layer.effect(
 
       const merge = (source: string, next: Info, kind?: ConfigPlugin.Scope) => {
         result = mergeConfigConcatArrays(result, next)
-        return track(source, next.plugin, kind)
+        return mergePluginOrigins(source, next.plugin, kind)
       }
 
       for (const [key, value] of Object.entries(auth)) {
@@ -617,8 +628,10 @@ export const layer = Layer.effect(
         result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir)))
         result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir)))
         result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir)))
+        // Auto-discovered plugins under `.opencode/plugin(s)` are already local files, so ConfigPlugin.load
+        // returns normalized Specs and we only need to attach origin metadata here.
         const list = yield* Effect.promise(() => ConfigPlugin.load(dir))
-        yield* track(dir, list)
+        yield* mergePluginOrigins(dir, list)
       }
 
       if (process.env.OPENCODE_CONFIG_CONTENT) {

+ 10 - 1
packages/opencode/src/config/plugin.ts

@@ -8,11 +8,16 @@ export namespace ConfigPlugin {
   const Options = z.record(z.string(), z.unknown())
   export type Options = z.infer<typeof Options>
 
+  // Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options.
+  // It answers "what should we load?" but says nothing about where that value came from.
   export const Spec = z.union([z.string(), z.tuple([z.string(), Options])])
   export type Spec = z.infer<typeof Spec>
 
   export type Scope = "global" | "local"
 
+  // Origin keeps the original config provenance attached to a spec.
+  // After multiple config files are merged, callers still need to know which file declared the plugin
+  // and whether it should behave like a global or project-local plugin.
   export type Origin = {
     spec: Spec
     source: string
@@ -33,7 +38,7 @@ export namespace ConfigPlugin {
     return plugins
   }
 
-  export function pluginSpecifier(plugin: ConfigPlugin.Spec): string {
+  export function pluginSpecifier(plugin: Spec): string {
     return Array.isArray(plugin) ? plugin[0] : plugin
   }
 
@@ -41,6 +46,8 @@ export namespace ConfigPlugin {
     return Array.isArray(plugin) ? plugin[1] : undefined
   }
 
+  // Path-like specs are resolved relative to the config file that declared them so merges later on do not
+  // accidentally reinterpret `./plugin.ts` relative to some other directory.
   export async function resolvePluginSpec(plugin: Spec, configFilepath: string): Promise<Spec> {
     const spec = pluginSpecifier(plugin)
     if (!isPathPluginSpec(spec)) return plugin
@@ -58,6 +65,8 @@ export namespace ConfigPlugin {
     return resolved
   }
 
+  // Dedupe on the load identity (package name for npm specs, exact file URL for local specs), but keep the
+  // full Origin so downstream code still knows which config file won and where follow-up writes should go.
   export function deduplicatePluginOrigins(plugins: Origin[]): Origin[] {
     const seen = new Set<string>()
     const list: Origin[] = []