diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts index 89cc071561..62b71e58b1 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 e506d2feda..a7c071a9f8 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 e4f42227de..865e827b31 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()("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 - readonly install: (dir: string, input?: { add: string[] }) => Effect.Effect + readonly install: ( + dir: string, + input?: { add: string[] }, + ) => Effect.Effect readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect readonly which: (pkg: string) => Effect.Effect> } @@ -55,6 +59,37 @@ export namespace Npm { interface ArboristTree { edgesOut: Map } + + 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 + }).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 - - 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 - - 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)