Skip to content

Commit 25a9de3

Browse files
committed
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.
1 parent e0d71f1 commit 25a9de3

3 files changed

Lines changed: 60 additions & 72 deletions

File tree

packages/opencode/src/effect/bootstrap-runtime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import { File } from "@/file"
1010
import { Vcs } from "@/project"
1111
import { Snapshot } from "@/snapshot"
1212
import { Bus } from "@/bus"
13+
import { Config } from "@/config"
1314
import * as Observability from "./observability"
1415

1516
export const BootstrapLayer = Layer.mergeAll(
17+
Config.defaultLayer,
1618
Plugin.defaultLayer,
1719
ShareNext.defaultLayer,
1820
Format.defaultLayer,

packages/opencode/src/project/bootstrap.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ import { Log } from "@/util"
1212
import { FileWatcher } from "@/file/watcher"
1313
import { ShareNext } from "@/share"
1414
import * as Effect from "effect/Effect"
15+
import { Config } from "@/config"
1516

1617
export const InstanceBootstrap = Effect.gen(function* () {
1718
Log.Default.info("bootstrapping", { directory: Instance.directory })
19+
// everything depends on config so eager load it for nice traces
20+
yield* Config.Service.use((svc) => svc.get())
21+
// Plugin can mutate config so it has to be initialized before anything else.
1822
yield* Plugin.Service.use((svc) => svc.init())
1923
yield* Effect.all(
2024
[

packages/shared/src/npm.ts

Lines changed: 54 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { EffectFlock } from "./util/effect-flock"
88

99
export namespace Npm {
1010
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
11-
pkg: Schema.String,
11+
add: Schema.Array(Schema.String).pipe(Schema.optional),
12+
dir: Schema.String,
1213
cause: Schema.optional(Schema.Defect),
1314
}) {}
1415

@@ -19,7 +20,10 @@ export namespace Npm {
1920

2021
export interface Interface {
2122
readonly add: (pkg: string) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>
22-
readonly install: (dir: string, input?: { add: string[] }) => Effect.Effect<void, EffectFlock.LockError>
23+
readonly install: (
24+
dir: string,
25+
input?: { add: string[] },
26+
) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
2327
readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect<boolean>
2428
readonly which: (pkg: string) => Effect.Effect<Option.Option<string>>
2529
}
@@ -55,6 +59,37 @@ export namespace Npm {
5559
interface ArboristTree {
5660
edgesOut: Map<string, { to?: ArboristNode }>
5761
}
62+
63+
const reify = (input: { dir: string; add?: string[] }) =>
64+
Effect.gen(function* () {
65+
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
66+
const arborist = new Arborist({
67+
path: input.dir,
68+
binLinks: true,
69+
progress: false,
70+
savePrefix: "",
71+
ignoreScripts: true,
72+
})
73+
return yield* Effect.tryPromise({
74+
try: () =>
75+
arborist.reify({
76+
add: input?.add || [],
77+
save: true,
78+
saveType: "prod",
79+
}),
80+
catch: (cause) =>
81+
new InstallFailedError({
82+
cause,
83+
add: input?.add,
84+
dir: input.dir,
85+
}),
86+
}) as Effect.Effect<ArboristTree, InstallFailedError>
87+
}).pipe(
88+
Effect.withSpan("Npm.reify", {
89+
attributes: input,
90+
}),
91+
)
92+
5893
export const layer = Layer.effect(
5994
Service,
6095
Effect.gen(function* () {
@@ -91,45 +126,12 @@ export namespace Npm {
91126
})
92127

93128
const add = Effect.fn("Npm.add")(function* (pkg: string) {
94-
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
95129
const dir = directory(pkg)
96130
yield* flock.acquire(`npm-install:${dir}`)
97131

98-
const arborist = new Arborist({
99-
path: dir,
100-
binLinks: true,
101-
progress: false,
102-
savePrefix: "",
103-
ignoreScripts: true,
104-
})
105-
106-
const tree = yield* Effect.tryPromise({
107-
try: () => arborist.loadVirtual().catch(() => undefined),
108-
catch: () => undefined,
109-
}).pipe(Effect.orElseSucceed(() => undefined)) as Effect.Effect<ArboristTree | undefined>
110-
111-
if (tree) {
112-
const first = tree.edgesOut.values().next().value?.to
113-
if (first) {
114-
return resolveEntryPoint(first.name, first.path)
115-
}
116-
}
117-
118-
const result = yield* Effect.tryPromise({
119-
try: () =>
120-
arborist.reify({
121-
add: [pkg],
122-
save: true,
123-
saveType: "prod",
124-
}),
125-
catch: (cause) => new InstallFailedError({ pkg, cause }),
126-
}) as Effect.Effect<ArboristTree, InstallFailedError>
127-
128-
const first = result.edgesOut.values().next().value?.to
129-
if (!first) {
130-
return yield* new InstallFailedError({ pkg })
131-
}
132-
132+
const tree = yield* reify({ dir, add: [pkg] })
133+
const first = tree.edgesOut.values().next().value?.to
134+
if (!first) return yield* new InstallFailedError({ add: [pkg], dir })
133135
return resolveEntryPoint(first.name, first.path)
134136
}, Effect.scoped)
135137

@@ -142,41 +144,20 @@ export namespace Npm {
142144

143145
yield* flock.acquire(`npm-install:${dir}`)
144146

145-
const reify = Effect.fn("Npm.reify")(function* () {
146-
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
147-
const arb = new Arborist({
148-
path: dir,
149-
binLinks: true,
150-
progress: false,
151-
savePrefix: "",
152-
ignoreScripts: true,
153-
})
154-
yield* Effect.tryPromise({
155-
try: () =>
156-
arb
157-
.reify({
158-
add: input?.add || [],
159-
save: true,
160-
saveType: "prod",
161-
})
162-
.catch(() => {}),
163-
catch: () => {},
164-
}).pipe(Effect.orElseSucceed(() => {}))
165-
})
166-
167-
const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules"))
168-
if (!nodeModulesExists) {
169-
yield* reify()
170-
return
171-
}
172-
173-
const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({})))
174-
const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({})))
175-
176-
const pkgAny = pkg as any
177-
const lockAny = lock as any
147+
yield* Effect.gen(function* () {
148+
const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules"))
149+
if (!nodeModulesExists) {
150+
yield* reify({ add: input?.add, dir })
151+
return
152+
}
153+
}).pipe(Effect.withSpan("Npm.checkNodeModules"))
178154

179155
yield* Effect.gen(function* () {
156+
const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({})))
157+
const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({})))
158+
159+
const pkgAny = pkg as any
160+
const lockAny = lock as any
180161
const declared = new Set([
181162
...Object.keys(pkgAny?.dependencies || {}),
182163
...Object.keys(pkgAny?.devDependencies || {}),
@@ -195,11 +176,12 @@ export namespace Npm {
195176

196177
for (const name of declared) {
197178
if (!locked.has(name)) {
198-
yield* reify()
179+
yield* reify({ dir, add: input?.add })
199180
return
200181
}
201182
}
202183
}).pipe(Effect.withSpan("Npm.checkDirty"))
184+
203185
return
204186
}, Effect.scoped)
205187

0 commit comments

Comments
 (0)