summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2026-04-16 16:21:47 -0400
committerDax Raad <[email protected]>2026-04-16 16:23:19 -0400
commit25a9de301ad83ac7f6c8ec5ed67d81ee4d2a0221 (patch)
tree1247f0c39d68822a8c8f671981af404b4b4e1a5e
parente0d71f124ef52f557387753ee19abe0f04f0faeb (diff)
downloadopencode-25a9de301ad83ac7f6c8ec5ed67d81ee4d2a0221.tar.gz
opencode-25a9de301ad83ac7f6c8ec5ed67d81ee4d2a0221.zip
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.
-rw-r--r--packages/opencode/src/effect/bootstrap-runtime.ts2
-rw-r--r--packages/opencode/src/project/bootstrap.ts4
-rw-r--r--packages/shared/src/npm.ts126
3 files changed, 60 insertions, 72 deletions
diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts
index 89cc07156..62b71e58b 100644
--- a/packages/opencode/src/effect/bootstrap-runtime.ts
+++ b/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,
diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts
index e506d2fed..a7c071a9f 100644
--- a/packages/opencode/src/project/bootstrap.ts
+++ b/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(
[
diff --git a/packages/shared/src/npm.ts b/packages/shared/src/npm.ts
index e4f42227d..865e827b3 100644
--- a/packages/shared/src/npm.ts
+++ b/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)