Sfoglia il codice sorgente

core: eager load config on startup for better traces and refactor npm install for improved error reporting

Config is now loaded eagerly during project bootstrap so users can see config loading in traces during startup. This helps diagnose configuration issues earlier in the initialization flow.

NPM installation logic has been refactored with a unified reify function and improved InstallFailedError that includes both the packages being installed and the target directory. This provides users with complete context when package installations fail, making it easier to identify which dependency or project directory caused the issue.
Dax Raad 1 giorno fa
parent
commit
25a9de301a

+ 2 - 0
packages/opencode/src/effect/bootstrap-runtime.ts

@@ -10,9 +10,11 @@ import { File } from "@/file"
 import { Vcs } from "@/project"
 import { Snapshot } from "@/snapshot"
 import { Bus } from "@/bus"
+import { Config } from "@/config"
 import * as Observability from "./observability"
 
 export const BootstrapLayer = Layer.mergeAll(
+  Config.defaultLayer,
   Plugin.defaultLayer,
   ShareNext.defaultLayer,
   Format.defaultLayer,

+ 4 - 0
packages/opencode/src/project/bootstrap.ts

@@ -12,9 +12,13 @@ import { Log } from "@/util"
 import { FileWatcher } from "@/file/watcher"
 import { ShareNext } from "@/share"
 import * as Effect from "effect/Effect"
+import { Config } from "@/config"
 
 export const InstanceBootstrap = Effect.gen(function* () {
   Log.Default.info("bootstrapping", { directory: Instance.directory })
+  // everything depends on config so eager load it for nice traces
+  yield* Config.Service.use((svc) => svc.get())
+  // Plugin can mutate config so it has to be initialized before anything else.
   yield* Plugin.Service.use((svc) => svc.init())
   yield* Effect.all(
     [

+ 54 - 72
packages/shared/src/npm.ts

@@ -8,7 +8,8 @@ import { EffectFlock } from "./util/effect-flock"
 
 export namespace Npm {
   export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
-    pkg: Schema.String,
+    add: Schema.Array(Schema.String).pipe(Schema.optional),
+    dir: Schema.String,
     cause: Schema.optional(Schema.Defect),
   }) {}
 
@@ -19,7 +20,10 @@ export namespace Npm {
 
   export interface Interface {
     readonly add: (pkg: string) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>
-    readonly install: (dir: string, input?: { add: string[] }) => Effect.Effect<void, EffectFlock.LockError>
+    readonly install: (
+      dir: string,
+      input?: { add: string[] },
+    ) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
     readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect<boolean>
     readonly which: (pkg: string) => Effect.Effect<Option.Option<string>>
   }
@@ -55,6 +59,37 @@ export namespace Npm {
   interface ArboristTree {
     edgesOut: Map<string, { to?: ArboristNode }>
   }
+
+  const reify = (input: { dir: string; add?: string[] }) =>
+    Effect.gen(function* () {
+      const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
+      const arborist = new Arborist({
+        path: input.dir,
+        binLinks: true,
+        progress: false,
+        savePrefix: "",
+        ignoreScripts: true,
+      })
+      return yield* Effect.tryPromise({
+        try: () =>
+          arborist.reify({
+            add: input?.add || [],
+            save: true,
+            saveType: "prod",
+          }),
+        catch: (cause) =>
+          new InstallFailedError({
+            cause,
+            add: input?.add,
+            dir: input.dir,
+          }),
+      }) as Effect.Effect<ArboristTree, InstallFailedError>
+    }).pipe(
+      Effect.withSpan("Npm.reify", {
+        attributes: input,
+      }),
+    )
+
   export const layer = Layer.effect(
     Service,
     Effect.gen(function* () {
@@ -91,45 +126,12 @@ export namespace Npm {
       })
 
       const add = Effect.fn("Npm.add")(function* (pkg: string) {
-        const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
         const dir = directory(pkg)
         yield* flock.acquire(`npm-install:${dir}`)
 
-        const arborist = new Arborist({
-          path: dir,
-          binLinks: true,
-          progress: false,
-          savePrefix: "",
-          ignoreScripts: true,
-        })
-
-        const tree = yield* Effect.tryPromise({
-          try: () => arborist.loadVirtual().catch(() => undefined),
-          catch: () => undefined,
-        }).pipe(Effect.orElseSucceed(() => undefined)) as Effect.Effect<ArboristTree | undefined>
-
-        if (tree) {
-          const first = tree.edgesOut.values().next().value?.to
-          if (first) {
-            return resolveEntryPoint(first.name, first.path)
-          }
-        }
-
-        const result = yield* Effect.tryPromise({
-          try: () =>
-            arborist.reify({
-              add: [pkg],
-              save: true,
-              saveType: "prod",
-            }),
-          catch: (cause) => new InstallFailedError({ pkg, cause }),
-        }) as Effect.Effect<ArboristTree, InstallFailedError>
-
-        const first = result.edgesOut.values().next().value?.to
-        if (!first) {
-          return yield* new InstallFailedError({ pkg })
-        }
-
+        const tree = yield* reify({ dir, add: [pkg] })
+        const first = tree.edgesOut.values().next().value?.to
+        if (!first) return yield* new InstallFailedError({ add: [pkg], dir })
         return resolveEntryPoint(first.name, first.path)
       }, Effect.scoped)
 
@@ -142,41 +144,20 @@ export namespace Npm {
 
         yield* flock.acquire(`npm-install:${dir}`)
 
-        const reify = Effect.fn("Npm.reify")(function* () {
-          const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
-          const arb = new Arborist({
-            path: dir,
-            binLinks: true,
-            progress: false,
-            savePrefix: "",
-            ignoreScripts: true,
-          })
-          yield* Effect.tryPromise({
-            try: () =>
-              arb
-                .reify({
-                  add: input?.add || [],
-                  save: true,
-                  saveType: "prod",
-                })
-                .catch(() => {}),
-            catch: () => {},
-          }).pipe(Effect.orElseSucceed(() => {}))
-        })
-
-        const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules"))
-        if (!nodeModulesExists) {
-          yield* reify()
-          return
-        }
-
-        const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({})))
-        const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({})))
-
-        const pkgAny = pkg as any
-        const lockAny = lock as any
+        yield* Effect.gen(function* () {
+          const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules"))
+          if (!nodeModulesExists) {
+            yield* reify({ add: input?.add, dir })
+            return
+          }
+        }).pipe(Effect.withSpan("Npm.checkNodeModules"))
 
         yield* Effect.gen(function* () {
+          const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({})))
+          const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({})))
+
+          const pkgAny = pkg as any
+          const lockAny = lock as any
           const declared = new Set([
             ...Object.keys(pkgAny?.dependencies || {}),
             ...Object.keys(pkgAny?.devDependencies || {}),
@@ -195,11 +176,12 @@ export namespace Npm {
 
           for (const name of declared) {
             if (!locked.has(name)) {
-              yield* reify()
+              yield* reify({ dir, add: input?.add })
               return
             }
           }
         }).pipe(Effect.withSpan("Npm.checkDirty"))
+
         return
       }, Effect.scoped)