summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-15 22:14:44 -0400
committerGitHub <[email protected]>2026-04-16 02:14:44 +0000
commitd22b5f026d38562550a9394aff9dbe9839b09812 (patch)
treefafd10b16a9f27f7bd1c071a7f78074611e74042 /packages
parent26cdbc20b2f889d27d5e84c6b87774c61ec87f99 (diff)
downloadopencode-d22b5f026d38562550a9394aff9dbe9839b09812.tar.gz
opencode-d22b5f026d38562550a9394aff9dbe9839b09812.zip
feat: unwrap unpm namespace to flat exports + barrel (#22708)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/npm/index.ts189
-rw-r--r--packages/opencode/src/npm/npm.ts186
2 files changed, 187 insertions, 188 deletions
diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts
index e648fd899..856ed2a2c 100644
--- a/packages/opencode/src/npm/index.ts
+++ b/packages/opencode/src/npm/index.ts
@@ -1,188 +1 @@
-import semver from "semver"
-import z from "zod"
-import { NamedError } from "@opencode-ai/shared/util/error"
-import { Global } from "../global"
-import { Log } from "../util/log"
-import path from "path"
-import { readdir, rm } from "fs/promises"
-import { Filesystem } from "@/util/filesystem"
-import { Flock } from "@opencode-ai/shared/util/flock"
-import { Arborist } from "@npmcli/arborist"
-
-export namespace Npm {
- const log = Log.create({ service: "npm" })
- const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
-
- export const InstallFailedError = NamedError.create(
- "NpmInstallFailedError",
- z.object({
- pkg: z.string(),
- }),
- )
-
- export function sanitize(pkg: string) {
- if (!illegal) return pkg
- return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
- }
-
- function directory(pkg: string) {
- return path.join(Global.Path.cache, "packages", sanitize(pkg))
- }
-
- function resolveEntryPoint(name: string, dir: string) {
- let entrypoint: string | undefined
- try {
- entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
- } catch {}
- const result = {
- directory: dir,
- entrypoint,
- }
- return result
- }
-
- export async function outdated(pkg: string, cachedVersion: string): Promise<boolean> {
- const response = await fetch(`https://registry.npmjs.org/${pkg}`)
- if (!response.ok) {
- log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
- return false
- }
-
- const data = (await response.json()) as { "dist-tags"?: { latest?: string } }
- const latestVersion = data?.["dist-tags"]?.latest
- if (!latestVersion) {
- log.warn("No latest version found, using cached", { pkg, cachedVersion })
- return false
- }
-
- const range = /[\s^~*xX<>|=]/.test(cachedVersion)
- if (range) return !semver.satisfies(latestVersion, cachedVersion)
-
- return semver.lt(cachedVersion, latestVersion)
- }
-
- export async function add(pkg: string) {
- const dir = directory(pkg)
- await using _ = await Flock.acquire(`npm-install:${Filesystem.resolve(dir)}`)
- log.info("installing package", {
- pkg,
- })
-
- const arborist = new Arborist({
- path: dir,
- binLinks: true,
- progress: false,
- savePrefix: "",
- ignoreScripts: true,
- })
- const tree = await arborist.loadVirtual().catch(() => {})
- if (tree) {
- const first = tree.edgesOut.values().next().value?.to
- if (first) {
- return resolveEntryPoint(first.name, first.path)
- }
- }
-
- const result = await arborist
- .reify({
- add: [pkg],
- save: true,
- saveType: "prod",
- })
- .catch((cause) => {
- throw new InstallFailedError(
- { pkg },
- {
- cause,
- },
- )
- })
-
- const first = result.edgesOut.values().next().value?.to
- if (!first) throw new InstallFailedError({ pkg })
- return resolveEntryPoint(first.name, first.path)
- }
-
- export async function install(dir: string) {
- await using _ = await Flock.acquire(`npm-install:${dir}`)
- log.info("checking dependencies", { dir })
-
- const reify = async () => {
- const arb = new Arborist({
- path: dir,
- binLinks: true,
- progress: false,
- savePrefix: "",
- ignoreScripts: true,
- })
- await arb.reify().catch(() => {})
- }
-
- if (!(await Filesystem.exists(path.join(dir, "node_modules")))) {
- log.info("node_modules missing, reifying")
- await reify()
- return
- }
-
- const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({}))
- const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({}))
-
- const declared = new Set([
- ...Object.keys(pkg.dependencies || {}),
- ...Object.keys(pkg.devDependencies || {}),
- ...Object.keys(pkg.peerDependencies || {}),
- ...Object.keys(pkg.optionalDependencies || {}),
- ])
-
- const root = lock.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)) {
- log.info("dependency not in lock file, reifying", { name })
- await reify()
- return
- }
- }
-
- log.info("dependencies in sync")
- }
-
- export async function which(pkg: string) {
- const dir = directory(pkg)
- const binDir = path.join(dir, "node_modules", ".bin")
-
- const pick = async () => {
- const files = await readdir(binDir).catch(() => [])
- if (files.length === 0) return undefined
- if (files.length === 1) return files[0]
- // Multiple binaries — resolve from package.json bin field like npx does
- const pkgJson = await Filesystem.readJson<{ bin?: string | Record<string, string> }>(
- path.join(dir, "node_modules", pkg, "package.json"),
- ).catch(() => undefined)
- if (pkgJson?.bin) {
- const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
- const bin = pkgJson.bin
- if (typeof bin === "string") return unscoped
- const keys = Object.keys(bin)
- if (keys.length === 1) return keys[0]
- return bin[unscoped] ? unscoped : keys[0]
- }
- return files[0]
- }
-
- const bin = await pick()
- if (bin) return path.join(binDir, bin)
-
- await rm(path.join(dir, "package-lock.json"), { force: true })
- await add(pkg)
- const resolved = await pick()
- if (!resolved) return
- return path.join(binDir, resolved)
- }
-}
+export * as Npm from "./npm"
diff --git a/packages/opencode/src/npm/npm.ts b/packages/opencode/src/npm/npm.ts
new file mode 100644
index 000000000..f90513071
--- /dev/null
+++ b/packages/opencode/src/npm/npm.ts
@@ -0,0 +1,186 @@
+import semver from "semver"
+import z from "zod"
+import { NamedError } from "@opencode-ai/shared/util/error"
+import { Global } from "../global"
+import { Log } from "../util/log"
+import path from "path"
+import { readdir, rm } from "fs/promises"
+import { Filesystem } from "@/util/filesystem"
+import { Flock } from "@opencode-ai/shared/util/flock"
+import { Arborist } from "@npmcli/arborist"
+
+const log = Log.create({ service: "npm" })
+const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
+
+export const InstallFailedError = NamedError.create(
+ "NpmInstallFailedError",
+ z.object({
+ pkg: z.string(),
+ }),
+)
+
+export function sanitize(pkg: string) {
+ if (!illegal) return pkg
+ return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
+}
+
+function directory(pkg: string) {
+ return path.join(Global.Path.cache, "packages", sanitize(pkg))
+}
+
+function resolveEntryPoint(name: string, dir: string) {
+ let entrypoint: string | undefined
+ try {
+ entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
+ } catch {}
+ const result = {
+ directory: dir,
+ entrypoint,
+ }
+ return result
+}
+
+export async function outdated(pkg: string, cachedVersion: string): Promise<boolean> {
+ const response = await fetch(`https://registry.npmjs.org/${pkg}`)
+ if (!response.ok) {
+ log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
+ return false
+ }
+
+ const data = (await response.json()) as { "dist-tags"?: { latest?: string } }
+ const latestVersion = data?.["dist-tags"]?.latest
+ if (!latestVersion) {
+ log.warn("No latest version found, using cached", { pkg, cachedVersion })
+ return false
+ }
+
+ const range = /[\s^~*xX<>|=]/.test(cachedVersion)
+ if (range) return !semver.satisfies(latestVersion, cachedVersion)
+
+ return semver.lt(cachedVersion, latestVersion)
+}
+
+export async function add(pkg: string) {
+ const dir = directory(pkg)
+ await using _ = await Flock.acquire(`npm-install:${Filesystem.resolve(dir)}`)
+ log.info("installing package", {
+ pkg,
+ })
+
+ const arborist = new Arborist({
+ path: dir,
+ binLinks: true,
+ progress: false,
+ savePrefix: "",
+ ignoreScripts: true,
+ })
+ const tree = await arborist.loadVirtual().catch(() => {})
+ if (tree) {
+ const first = tree.edgesOut.values().next().value?.to
+ if (first) {
+ return resolveEntryPoint(first.name, first.path)
+ }
+ }
+
+ const result = await arborist
+ .reify({
+ add: [pkg],
+ save: true,
+ saveType: "prod",
+ })
+ .catch((cause) => {
+ throw new InstallFailedError(
+ { pkg },
+ {
+ cause,
+ },
+ )
+ })
+
+ const first = result.edgesOut.values().next().value?.to
+ if (!first) throw new InstallFailedError({ pkg })
+ return resolveEntryPoint(first.name, first.path)
+}
+
+export async function install(dir: string) {
+ await using _ = await Flock.acquire(`npm-install:${dir}`)
+ log.info("checking dependencies", { dir })
+
+ const reify = async () => {
+ const arb = new Arborist({
+ path: dir,
+ binLinks: true,
+ progress: false,
+ savePrefix: "",
+ ignoreScripts: true,
+ })
+ await arb.reify().catch(() => {})
+ }
+
+ if (!(await Filesystem.exists(path.join(dir, "node_modules")))) {
+ log.info("node_modules missing, reifying")
+ await reify()
+ return
+ }
+
+ const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({}))
+ const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({}))
+
+ const declared = new Set([
+ ...Object.keys(pkg.dependencies || {}),
+ ...Object.keys(pkg.devDependencies || {}),
+ ...Object.keys(pkg.peerDependencies || {}),
+ ...Object.keys(pkg.optionalDependencies || {}),
+ ])
+
+ const root = lock.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)) {
+ log.info("dependency not in lock file, reifying", { name })
+ await reify()
+ return
+ }
+ }
+
+ log.info("dependencies in sync")
+}
+
+export async function which(pkg: string) {
+ const dir = directory(pkg)
+ const binDir = path.join(dir, "node_modules", ".bin")
+
+ const pick = async () => {
+ const files = await readdir(binDir).catch(() => [])
+ if (files.length === 0) return undefined
+ if (files.length === 1) return files[0]
+ // Multiple binaries — resolve from package.json bin field like npx does
+ const pkgJson = await Filesystem.readJson<{ bin?: string | Record<string, string> }>(
+ path.join(dir, "node_modules", pkg, "package.json"),
+ ).catch(() => undefined)
+ if (pkgJson?.bin) {
+ const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
+ const bin = pkgJson.bin
+ if (typeof bin === "string") return unscoped
+ const keys = Object.keys(bin)
+ if (keys.length === 1) return keys[0]
+ return bin[unscoped] ? unscoped : keys[0]
+ }
+ return files[0]
+ }
+
+ const bin = await pick()
+ if (bin) return path.join(binDir, bin)
+
+ await rm(path.join(dir, "package-lock.json"), { force: true })
+ await add(pkg)
+ const resolved = await pick()
+ if (!resolved) return
+ return path.join(binDir, resolved)
+}