From f5dce6d960ea44d3a77cfe868b8209b44a866edb Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Apr 2026 14:36:15 -0400 Subject: core: move npm service to core package for shared dependency management --- bun.lock | 28 +- packages/core/package.json | 9 +- packages/core/src/npm.ts | 347 +++++++++++++++++++++ packages/opencode/package.json | 3 - packages/opencode/src/cli/cmd/tui/config/tui.ts | 2 +- packages/opencode/src/cli/cmd/tui/layer.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 2 +- packages/opencode/src/format/formatter.ts | 2 +- packages/opencode/src/lsp/server.ts | 2 +- packages/opencode/src/npm/config.ts | 0 packages/opencode/src/npm/index.ts | 345 -------------------- packages/opencode/src/npmcli-config.d.ts | 43 --- packages/opencode/src/plugin/shared.ts | 2 +- packages/opencode/src/provider/provider.ts | 2 +- .../test/cli/tui/plugin-loader-entrypoint.test.ts | 2 +- packages/opencode/test/config/config.test.ts | 2 +- packages/opencode/test/npm.test.ts | 2 +- .../opencode/test/plugin/loader-shared.test.ts | 2 +- 19 files changed, 380 insertions(+), 419 deletions(-) create mode 100644 packages/core/src/npm.ts delete mode 100644 packages/opencode/src/npm/config.ts delete mode 100644 packages/opencode/src/npm/index.ts delete mode 100644 packages/opencode/src/npmcli-config.d.ts diff --git a/bun.lock b/bun.lock index 06a414a21..1d2e4462f 100644 --- a/bun.lock +++ b/bun.lock @@ -199,6 +199,8 @@ "dependencies": { "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", + "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", @@ -208,6 +210,8 @@ "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", + "npm-package-arg": "13.0.2", + "semver": "^7.6.3", "xdg-basedir": "5.1.0", "zod": "catalog:", }, @@ -215,6 +219,9 @@ "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@types/cross-spawn": "catalog:", + "@types/npm-package-arg": "6.1.4", + "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:", }, }, "packages/desktop": { @@ -380,8 +387,6 @@ "@hono/zod-validator": "catalog:", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.27.1", - "@npmcli/arborist": "9.4.0", - "@npmcli/config": "10.8.1", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", @@ -470,7 +475,6 @@ "@types/cross-spawn": "catalog:", "@types/mime-types": "3.0.1", "@types/npm-package-arg": "6.1.4", - "@types/npmcli__arborist": "6.3.3", "@types/semver": "^7.5.8", "@types/turndown": "5.0.5", "@types/which": "3.0.4", @@ -2847,7 +2851,7 @@ "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], - "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], @@ -4005,7 +4009,7 @@ "native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="], - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], @@ -5515,8 +5519,6 @@ "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@npmcli/arborist/common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], - "@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], @@ -5709,6 +5711,8 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="], "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], @@ -5739,6 +5743,8 @@ "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], + "astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + "astro/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], "astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], @@ -5893,8 +5899,6 @@ "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], @@ -6881,8 +6885,6 @@ "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], - "@electron/rebuild/node-gyp/make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "@electron/rebuild/node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], "@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], @@ -6947,8 +6949,6 @@ "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -7147,8 +7147,6 @@ "js-beautify/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "opencontrol/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], diff --git a/packages/core/package.json b/packages/core/package.json index bd826de35..62d56908c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,11 +19,16 @@ "devDependencies": { "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", - "@types/cross-spawn": "catalog:" + "@types/cross-spawn": "catalog:", + "@types/npm-package-arg": "6.1.4", + "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:" }, "dependencies": { "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", + "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", @@ -33,6 +38,8 @@ "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", + "npm-package-arg": "13.0.2", + "semver": "^7.6.3", "xdg-basedir": "5.1.0", "zod": "catalog:" }, diff --git a/packages/core/src/npm.ts b/packages/core/src/npm.ts new file mode 100644 index 000000000..a52e0a9a5 --- /dev/null +++ b/packages/core/src/npm.ts @@ -0,0 +1,347 @@ +export * as Npm from "./npm" + +import path from "path" +import { fileURLToPath } from "url" +import npa from "npm-package-arg" +import semver from "semver" +// @ts-expect-error npm does not publish types for this internal config API. +import Config from "@npmcli/config" +// @ts-expect-error npm does not publish types for this internal config API. +import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js" +import { Effect, Schema, Context, Layer, Option, FileSystem, Stream } from "effect" +import { NodeFileSystem } from "@effect/platform-node" +import { AppFileSystem } from "./filesystem" +import { Global } from "./global" +import { EffectFlock } from "./util/effect-flock" +import { makeRuntime } from "./effect/runtime" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" + +import { CrossSpawnSpawner } from "./cross-spawn-spawner" + +export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { + add: Schema.Array(Schema.String).pipe(Schema.optional), + dir: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +export interface EntryPoint { + readonly directory: string + readonly entrypoint: Option.Option +} + +export interface Interface { + readonly add: (pkg: string) => Effect.Effect + readonly install: ( + dir: string, + input?: { + add: { + name: string + version?: string + }[] + }, + ) => Effect.Effect + readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect + readonly which: (pkg: string, bin?: string) => Effect.Effect> +} + +export class Service extends Context.Service()("@opencode/Npm") {} + +const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined +const npmPath = fileURLToPath(new URL("..", import.meta.url)) + +export function sanitize(pkg: string) { + if (!illegal) return pkg + return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") +} + +const loadOptions = (dir: string) => + Effect.tryPromise({ + try: async () => { + const config = new Config({ + npmPath, + cwd: dir, + env: { ...process.env }, + argv: [process.execPath, process.execPath], + execPath: process.execPath, + platform: process.platform, + definitions, + flatten, + nerfDarts, + shorthands, + warn: false, + }) + await config.load() + return config.flat + }, + catch: (cause) => + new InstallFailedError({ + cause, + dir, + }), + }) + +const resolveEntryPoint = (name: string, dir: string): EntryPoint => { + let entrypoint: Option.Option + try { + const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) + entrypoint = Option.some(resolved) + } catch { + entrypoint = Option.none() + } + return { + directory: dir, + entrypoint, + } +} + +interface ArboristNode { + name: string + path: string +} + +interface ArboristTree { + edgesOut: Map +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const afs = yield* AppFileSystem.Service + const global = yield* Global.Service + const fs = yield* FileSystem.FileSystem + const flock = yield* EffectFlock.Service + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) + const runView = Effect.fnUntraced(function* (cmd: string[]) { + const handle = yield* spawner.spawn( + ChildProcess.make(cmd[0], cmd.slice(1), { + extendEnv: true, + }), + ) + const [stdout, stderr] = yield* Effect.all( + [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], + { concurrency: 2 }, + ) + const code = yield* handle.exitCode + if (code !== 0 || !stdout.trim()) { + return yield* Effect.fail(stderr || stdout || `Failed to run ${cmd.join(" ")}`) + } + return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(stdout) + }, Effect.scoped) + const viewLatestVersion = Effect.fnUntraced(function* (pkg: string) { + return yield* runView(["npm", "view", pkg, "dist-tags.latest", "--json"]).pipe( + Effect.catch(() => + runView(["pnpm", "view", pkg, "dist-tags.latest", "--json"]).pipe( + Effect.catch(() => runView(["bun", "pm", "view", pkg, "dist-tags.latest", "--json"])), + ), + ), + ) + }) + const reify = (input: { dir: string; add?: string[] }) => + Effect.gen(function* () { + yield* flock.acquire(`npm-install:${input.dir}`) + const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) + const add = input.add ?? [] + const npmOptions = yield* loadOptions(input.dir) + const arborist = new Arborist({ + ...npmOptions, + path: input.dir, + binLinks: true, + progress: false, + savePrefix: "", + ignoreScripts: true, + }) + return yield* Effect.tryPromise({ + try: () => + arborist.reify({ + ...npmOptions, + add, + save: true, + saveType: "prod", + }), + catch: (cause) => + new InstallFailedError({ + cause, + add, + dir: input.dir, + }), + }) as Effect.Effect + }).pipe( + Effect.withSpan("Npm.reify", { + attributes: input, + }), + ) + + const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) { + const latestVersion = yield* viewLatestVersion(pkg).pipe(Effect.option) + if (Option.isNone(latestVersion)) { + return false + } + + const range = /[\s^~*xX<>|=]/.test(cachedVersion) + if (range) return !semver.satisfies(latestVersion.value, cachedVersion) + + return semver.lt(cachedVersion, latestVersion.value) + }) + + const add = Effect.fn("Npm.add")(function* (pkg: string) { + const dir = directory(pkg) + const name = (() => { + try { + return npa(pkg).name ?? pkg + } catch { + return pkg + } + })() + + if (yield* afs.existsSafe(dir)) { + return resolveEntryPoint(name, path.join(dir, "node_modules", name)) + } + + 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) + + const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) { + const canWrite = yield* afs.access(dir, { writable: true }).pipe( + Effect.as(true), + Effect.orElseSucceed(() => false), + ) + if (!canWrite) return + + const add = input?.add.map((pkg) => [pkg.name, pkg.version].filter(Boolean).join("@")) ?? [] + if ( + yield* Effect.gen(function* () { + const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) + if (!nodeModulesExists) { + yield* reify({ add, dir }) + return true + } + return false + }).pipe(Effect.withSpan("Npm.checkNodeModules")) + ) + return + + 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 || {}), + ...Object.keys(pkgAny?.peerDependencies || {}), + ...Object.keys(pkgAny?.optionalDependencies || {}), + ...(input?.add || []).map((pkg) => pkg.name), + ]) + + const root = lockAny?.packages?.[""] || {} + const locked = new Set([ + ...Object.keys(root?.dependencies || {}), + ...Object.keys(root?.devDependencies || {}), + ...Object.keys(root?.peerDependencies || {}), + ...Object.keys(root?.optionalDependencies || {}), + ]) + + for (const name of declared) { + if (!locked.has(name)) { + yield* reify({ dir, add }) + return + } + } + }).pipe(Effect.withSpan("Npm.checkDirty")) + + return + }, Effect.scoped) + + const which = Effect.fn("Npm.which")(function* (pkg: string, bin?: string) { + const dir = directory(pkg) + const binDir = path.join(dir, "node_modules", ".bin") + + const pick = Effect.fnUntraced(function* () { + const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[]))) + + if (files.length === 0) return Option.none() + // Caller picked a specific bin (e.g. pyright exposes both `pyright` and + // `pyright-langserver`); trust the hint if the package provides it. + if (bin) return files.includes(bin) ? Option.some(bin) : Option.none() + if (files.length === 1) return Option.some(files[0]) + + const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option) + + if (Option.isSome(pkgJson)) { + const parsed = pkgJson.value as { bin?: string | Record } + if (parsed?.bin) { + const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg + const parsedBin = parsed.bin + if (typeof parsedBin === "string") return Option.some(unscoped) + const keys = Object.keys(parsedBin) + if (keys.length === 1) return Option.some(keys[0]) + return parsedBin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]) + } + } + + return Option.some(files[0]) + }) + + return yield* Effect.gen(function* () { + const bin = yield* pick() + if (Option.isSome(bin)) { + return Option.some(path.join(binDir, bin.value)) + } + + yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) + + yield* add(pkg) + + const resolved = yield* pick() + if (Option.isNone(resolved)) return Option.none() + return Option.some(path.join(binDir, resolved.value)) + }).pipe( + Effect.scoped, + Effect.orElseSucceed(() => Option.none()), + ) + }) + + return Service.of({ + add, + install, + outdated, + which, + }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(EffectFlock.layer), + Layer.provide(AppFileSystem.layer), + Layer.provide(Global.layer), + Layer.provide(NodeFileSystem.layer), + Layer.provide(CrossSpawnSpawner.defaultLayer), +) + +const { runPromise } = makeRuntime(Service, defaultLayer) + +export async function install(...args: Parameters) { + return runPromise((svc) => svc.install(...args)) +} + +export async function add(...args: Parameters) { + const entry = await runPromise((svc) => svc.add(...args)) + return { + directory: entry.directory, + entrypoint: Option.getOrUndefined(entry.entrypoint), + } +} + +export async function outdated(...args: Parameters) { + return runPromise((svc) => svc.outdated(...args)) +} + +export async function which(...args: Parameters) { + const resolved = await runPromise((svc) => svc.which(...args)) + return Option.getOrUndefined(resolved) +} diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 1c60e58a8..98a707f4b 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -61,7 +61,6 @@ "@types/cross-spawn": "catalog:", "@types/mime-types": "3.0.1", "@types/npm-package-arg": "6.1.4", - "@types/npmcli__arborist": "6.3.3", "@types/semver": "^7.5.8", "@types/turndown": "5.0.5", "@types/which": "3.0.4", @@ -110,8 +109,6 @@ "@hono/zod-validator": "catalog:", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.27.1", - "@npmcli/arborist": "9.4.0", - "@npmcli/config": "10.8.1", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index b55e5807e..3f99e6d2f 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -18,7 +18,7 @@ import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/instal import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { Filesystem, Log } from "@/util" import { ConfigVariable } from "@/config/variable" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "tui.config" }) diff --git a/packages/opencode/src/cli/cmd/tui/layer.ts b/packages/opencode/src/cli/cmd/tui/layer.ts index 785455334..a0c19e46d 100644 --- a/packages/opencode/src/cli/cmd/tui/layer.ts +++ b/packages/opencode/src/cli/cmd/tui/layer.ts @@ -1,6 +1,6 @@ import { Layer } from "effect" import { TuiConfig } from "./config/tui" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" import { Observability } from "@opencode-ai/core/effect/observability" export const CliLayer = Observability.layer.pipe(Layer.merge(TuiConfig.layer), Layer.provide(Npm.defaultLayer)) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ddd31a3fc..6173b2fb6 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -42,7 +42,7 @@ import { ConfigProvider } from "./provider" import { ConfigServer } from "./server" import { ConfigSkills } from "./skills" import { ConfigVariable } from "./variable" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "config" }) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index b4bdbfca4..fcf64b9d2 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -46,7 +46,7 @@ import { Pty } from "@/pty" import { Installation } from "@/installation" import { ShareNext } from "@/share" import { SessionShare } from "@/share" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" import { memoMap } from "@opencode-ai/core/effect/memo-map" export const AppLayer = Layer.mergeAll( diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index eefafd575..82666b799 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -1,4 +1,4 @@ -import { Npm } from "../npm" +import { Npm } from "@opencode-ai/core/npm" import type { InstanceContext } from "../project/instance" import { Filesystem } from "../util" import { Process } from "../util" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 14b674a98..32a5239be 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -13,7 +13,7 @@ import { Process } from "../util" import { which } from "../util/which" import { Module } from "@opencode-ai/core/util/module" import { spawn } from "./launch" -import { Npm } from "../npm" +import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "lsp.server" }) const pathExists = async (p: string) => diff --git a/packages/opencode/src/npm/config.ts b/packages/opencode/src/npm/config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts deleted file mode 100644 index a7538b9c3..000000000 --- a/packages/opencode/src/npm/index.ts +++ /dev/null @@ -1,345 +0,0 @@ -export * as Npm from "." - -import path from "path" -import { fileURLToPath } from "url" -import npa from "npm-package-arg" -import semver from "semver" -import Config from "@npmcli/config" -import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js" -import { Effect, Schema, Context, Layer, Option, FileSystem, Stream } from "effect" -import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { Global } from "@opencode-ai/core/global" -import { EffectFlock } from "@opencode-ai/core/util/effect-flock" -import { makeRuntime } from "@opencode-ai/core/effect/runtime" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" - -import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" - -export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { - add: Schema.Array(Schema.String).pipe(Schema.optional), - dir: Schema.String, - cause: Schema.optional(Schema.Defect), -}) {} - -export interface EntryPoint { - readonly directory: string - readonly entrypoint: Option.Option -} - -export interface Interface { - readonly add: (pkg: string) => Effect.Effect - readonly install: ( - dir: string, - input?: { - add: { - name: string - version?: string - }[] - }, - ) => Effect.Effect - readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect - readonly which: (pkg: string, bin?: string) => Effect.Effect> -} - -export class Service extends Context.Service()("@opencode/Npm") {} - -const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined -const npmPath = fileURLToPath(new URL("../..", import.meta.url)) - -export function sanitize(pkg: string) { - if (!illegal) return pkg - return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") -} - -const loadOptions = (dir: string) => - Effect.tryPromise({ - try: async () => { - const config = new Config({ - npmPath, - cwd: dir, - env: { ...process.env }, - argv: [process.execPath, process.execPath], - execPath: process.execPath, - platform: process.platform, - definitions, - flatten, - nerfDarts, - shorthands, - warn: false, - }) - await config.load() - return config.flat - }, - catch: (cause) => - new InstallFailedError({ - cause, - dir, - }), - }) - -const resolveEntryPoint = (name: string, dir: string): EntryPoint => { - let entrypoint: Option.Option - try { - const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) - entrypoint = Option.some(resolved) - } catch { - entrypoint = Option.none() - } - return { - directory: dir, - entrypoint, - } -} - -interface ArboristNode { - name: string - path: string -} - -interface ArboristTree { - edgesOut: Map -} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const afs = yield* AppFileSystem.Service - const global = yield* Global.Service - const fs = yield* FileSystem.FileSystem - const flock = yield* EffectFlock.Service - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) - const runView = Effect.fnUntraced(function* (cmd: string[]) { - const handle = yield* spawner.spawn( - ChildProcess.make(cmd[0], cmd.slice(1), { - extendEnv: true, - }), - ) - const [stdout, stderr] = yield* Effect.all( - [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], - { concurrency: 2 }, - ) - const code = yield* handle.exitCode - if (code !== 0 || !stdout.trim()) { - return yield* Effect.fail(stderr || stdout || `Failed to run ${cmd.join(" ")}`) - } - return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(stdout) - }, Effect.scoped) - const viewLatestVersion = Effect.fnUntraced(function* (pkg: string) { - return yield* runView(["npm", "view", pkg, "dist-tags.latest", "--json"]).pipe( - Effect.catch(() => - runView(["pnpm", "view", pkg, "dist-tags.latest", "--json"]).pipe( - Effect.catch(() => runView(["bun", "pm", "view", pkg, "dist-tags.latest", "--json"])), - ), - ), - ) - }) - const reify = (input: { dir: string; add?: string[] }) => - Effect.gen(function* () { - yield* flock.acquire(`npm-install:${input.dir}`) - const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) - const add = input.add ?? [] - const npmOptions = yield* loadOptions(input.dir) - const arborist = new Arborist({ - ...npmOptions, - path: input.dir, - binLinks: true, - progress: false, - savePrefix: "", - ignoreScripts: true, - }) - return yield* Effect.tryPromise({ - try: () => - arborist.reify({ - ...npmOptions, - add, - save: true, - saveType: "prod", - }), - catch: (cause) => - new InstallFailedError({ - cause, - add, - dir: input.dir, - }), - }) as Effect.Effect - }).pipe( - Effect.withSpan("Npm.reify", { - attributes: input, - }), - ) - - const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) { - const latestVersion = yield* viewLatestVersion(pkg).pipe(Effect.option) - if (Option.isNone(latestVersion)) { - return false - } - - const range = /[\s^~*xX<>|=]/.test(cachedVersion) - if (range) return !semver.satisfies(latestVersion.value, cachedVersion) - - return semver.lt(cachedVersion, latestVersion.value) - }) - - const add = Effect.fn("Npm.add")(function* (pkg: string) { - const dir = directory(pkg) - const name = (() => { - try { - return npa(pkg).name ?? pkg - } catch { - return pkg - } - })() - - if (yield* afs.existsSafe(dir)) { - return resolveEntryPoint(name, path.join(dir, "node_modules", name)) - } - - 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) - - const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) { - const canWrite = yield* afs.access(dir, { writable: true }).pipe( - Effect.as(true), - Effect.orElseSucceed(() => false), - ) - if (!canWrite) return - - const add = input?.add.map((pkg) => [pkg.name, pkg.version].filter(Boolean).join("@")) ?? [] - if ( - yield* Effect.gen(function* () { - const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) - if (!nodeModulesExists) { - yield* reify({ add, dir }) - return true - } - return false - }).pipe(Effect.withSpan("Npm.checkNodeModules")) - ) - return - - 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 || {}), - ...Object.keys(pkgAny?.peerDependencies || {}), - ...Object.keys(pkgAny?.optionalDependencies || {}), - ...(input?.add || []).map((pkg) => pkg.name), - ]) - - const root = lockAny?.packages?.[""] || {} - const locked = new Set([ - ...Object.keys(root?.dependencies || {}), - ...Object.keys(root?.devDependencies || {}), - ...Object.keys(root?.peerDependencies || {}), - ...Object.keys(root?.optionalDependencies || {}), - ]) - - for (const name of declared) { - if (!locked.has(name)) { - yield* reify({ dir, add }) - return - } - } - }).pipe(Effect.withSpan("Npm.checkDirty")) - - return - }, Effect.scoped) - - const which = Effect.fn("Npm.which")(function* (pkg: string, bin?: string) { - const dir = directory(pkg) - const binDir = path.join(dir, "node_modules", ".bin") - - const pick = Effect.fnUntraced(function* () { - const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[]))) - - if (files.length === 0) return Option.none() - // Caller picked a specific bin (e.g. pyright exposes both `pyright` and - // `pyright-langserver`); trust the hint if the package provides it. - if (bin) return files.includes(bin) ? Option.some(bin) : Option.none() - if (files.length === 1) return Option.some(files[0]) - - const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option) - - if (Option.isSome(pkgJson)) { - const parsed = pkgJson.value as { bin?: string | Record } - if (parsed?.bin) { - const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg - const parsedBin = parsed.bin - if (typeof parsedBin === "string") return Option.some(unscoped) - const keys = Object.keys(parsedBin) - if (keys.length === 1) return Option.some(keys[0]) - return parsedBin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]) - } - } - - return Option.some(files[0]) - }) - - return yield* Effect.gen(function* () { - const bin = yield* pick() - if (Option.isSome(bin)) { - return Option.some(path.join(binDir, bin.value)) - } - - yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) - - yield* add(pkg) - - const resolved = yield* pick() - if (Option.isNone(resolved)) return Option.none() - return Option.some(path.join(binDir, resolved.value)) - }).pipe( - Effect.scoped, - Effect.orElseSucceed(() => Option.none()), - ) - }) - - return Service.of({ - add, - install, - outdated, - which, - }) - }), -) - -export const defaultLayer = layer.pipe( - Layer.provide(EffectFlock.layer), - Layer.provide(AppFileSystem.layer), - Layer.provide(Global.layer), - Layer.provide(NodeFileSystem.layer), - Layer.provide(CrossSpawnSpawner.defaultLayer), -) - -const { runPromise } = makeRuntime(Service, defaultLayer) - -export async function install(...args: Parameters) { - return runPromise((svc) => svc.install(...args)) -} - -export async function add(...args: Parameters) { - const entry = await runPromise((svc) => svc.add(...args)) - return { - directory: entry.directory, - entrypoint: Option.getOrUndefined(entry.entrypoint), - } -} - -export async function outdated(...args: Parameters) { - return runPromise((svc) => svc.outdated(...args)) -} - -export async function which(...args: Parameters) { - const resolved = await runPromise((svc) => svc.which(...args)) - return Option.getOrUndefined(resolved) -} diff --git a/packages/opencode/src/npmcli-config.d.ts b/packages/opencode/src/npmcli-config.d.ts deleted file mode 100644 index c9b20517a..000000000 --- a/packages/opencode/src/npmcli-config.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -declare module "@npmcli/config" { - type Data = Record - type Where = "default" | "builtin" | "global" | "user" | "project" | "env" | "cli" - - namespace Config { - interface Options { - definitions: Data - shorthands: Record - npmPath: string - flatten?: (input: Data, flat?: Data) => Data - nerfDarts?: string[] - argv?: string[] - cwd?: string - env?: NodeJS.ProcessEnv - execPath?: string - platform?: NodeJS.Platform - warn?: boolean - } - } - - class Config { - constructor(input: Config.Options) - - readonly data: Map - readonly flat: Data - - load(): Promise - } - - export = Config -} - -declare module "@npmcli/config/lib/definitions" { - export const definitions: Record - export const shorthands: Record - export const flatten: (input: Record, flat?: Record) => Record - export const nerfDarts: string[] - export const proxyEnv: string[] -} - -declare module "@npmcli/config/lib/definitions/index.js" { - export * from "@npmcli/config/lib/definitions" -} diff --git a/packages/opencode/src/plugin/shared.ts b/packages/opencode/src/plugin/shared.ts index ca821216d..a930d5b26 100644 --- a/packages/opencode/src/plugin/shared.ts +++ b/packages/opencode/src/plugin/shared.ts @@ -4,7 +4,7 @@ import npa from "npm-package-arg" import semver from "semver" import { Filesystem } from "@/util" import { isRecord } from "@/util/record" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" // Old npm package names for plugins that are now built-in export const DEPRECATED_PLUGIN_PACKAGES = ["opencode-openai-codex-auth", "opencode-copilot-auth"] diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c2439d04f..9aa1b6304 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -4,7 +4,7 @@ import { Config } from "../config" import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" import { Log } from "../util" -import { Npm } from "../npm" +import { Npm } from "@opencode-ai/core/npm" import { Hash } from "@opencode-ai/core/util/hash" import { Plugin } from "../plugin" import { type LanguageModelV3 } from "@ai-sdk/provider" diff --git a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts index 74236afae..66858e2d0 100644 --- a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts @@ -5,7 +5,7 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" -import { Npm } from "../../../src/npm" +import { Npm } from "@opencode-ai/core/npm" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index f6fb04ea0..324914e6d 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -27,7 +27,7 @@ import { Global } from "@opencode-ai/core/global" import { ProjectID } from "../../src/project/schema" import { Filesystem } from "../../src/util" import { ConfigPlugin } from "@/config/plugin" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" const emptyAccount = Layer.mock(Account.Service)({ active: () => Effect.succeed(Option.none()), diff --git a/packages/opencode/test/npm.test.ts b/packages/opencode/test/npm.test.ts index 09fa6b351..d5b93a83c 100644 --- a/packages/opencode/test/npm.test.ts +++ b/packages/opencode/test/npm.test.ts @@ -7,7 +7,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Global } from "@opencode-ai/core/global" import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { Npm } from "../src/npm" +import { Npm } from "@opencode-ai/core/npm" import { tmpdir } from "./fixture/fixture" const win = process.platform === "win32" diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index 83e9d71b4..88b3bf343 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -13,7 +13,7 @@ const { Plugin } = await import("../../src/plugin/index") const { PluginLoader } = await import("../../src/plugin/loader") const { readPackageThemes } = await import("../../src/plugin/shared") const { Instance } = await import("../../src/project/instance") -const { Npm } = await import("../../src/npm") +const { Npm } = await import("@opencode-ai/core/npm") afterAll(() => { if (disableDefault === undefined) { -- cgit v1.2.3