refactor: lsp server and core improvements
This commit is contained in:
parent
6b9f8fb9b3
commit
656fa191c1
12 changed files with 1282 additions and 948 deletions
6
.opencode/.gitignore
vendored
6
.opencode/.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
||||||
plans/
|
node_modules
|
||||||
bun.lock
|
|
||||||
package.json
|
package.json
|
||||||
|
bun.lock
|
||||||
|
.gitignore
|
||||||
|
package-lock.json
|
||||||
209
.opencode/plans/1771135996918-mighty-wizard.md
Normal file
209
.opencode/plans/1771135996918-mighty-wizard.md
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
# Plan: Project References Feature
|
||||||
|
|
||||||
|
Add support for referencing external git repositories in `opencode.json` that can be queried via a dedicated subagent.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Users can specify references in config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"references": ["git@github.com:Effect-TS/effect.git", "git@github.com:foo/bar.git#v1.0.0", "/local/path/to/repo"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These get cloned/cached to `~/.cache/opencode/references/` and a `reference` subagent can search across them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files to Create
|
||||||
|
|
||||||
|
### `src/reference/index.ts`
|
||||||
|
|
||||||
|
Core reference management module with:
|
||||||
|
|
||||||
|
- `parse(url: string): Reference` - Parse git URL or local path, extract branch if present (`#branch` suffix)
|
||||||
|
- `cachePath(ref: Reference): string` - SHA-256 hash of URL for deterministic directory names
|
||||||
|
- `isStale(ref: Reference): Promise<boolean>` - Check if `FETCH_HEAD` mtime > 1 hour old
|
||||||
|
- `fetch(ref: Reference): Promise<void>` - Clone with `--depth 1` or `git fetch`, handle branch checkout
|
||||||
|
- `ensureFresh(ref: Reference): Promise<void>` - Lazy fetch wrapper (skip if not stale)
|
||||||
|
- `list(): Promise<Reference[]>` - Get references from config
|
||||||
|
- `directories(): Promise<string[]>` - Get all cached reference paths for permissions
|
||||||
|
|
||||||
|
Types:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Reference {
|
||||||
|
url: string // Original URL/path
|
||||||
|
path: string // Cache directory (for git) or resolved path (for local)
|
||||||
|
branch?: string // Optional branch/tag from URL fragment
|
||||||
|
type: "git" | "local"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
URL parsing supports:
|
||||||
|
|
||||||
|
- `git@github.com:foo/bar.git` → git
|
||||||
|
- `git@github.com:foo/bar.git#main` → git with branch
|
||||||
|
- `https://github.com/foo/bar.git#v1.0.0` → git with tag
|
||||||
|
- `/absolute/path` → local
|
||||||
|
- `~/relative/path` → local (expanded)
|
||||||
|
|
||||||
|
### `src/agent/prompt/reference.txt`
|
||||||
|
|
||||||
|
Prompt for reference subagent:
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a multi-project code search specialist. You search across referenced projects
|
||||||
|
to answer questions about external codebases.
|
||||||
|
|
||||||
|
Available references: {references}
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
- Search across ALL referenced projects unless the user specifies one
|
||||||
|
- Report which project(s) contained relevant findings
|
||||||
|
- Use Glob for file patterns, Grep for content search, Read for specific files
|
||||||
|
- Return absolute paths so users can locate findings
|
||||||
|
- Do not modify any files or run destructive commands
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
### `src/config/config.ts`
|
||||||
|
|
||||||
|
Add `references` field to `Config.Info` schema (around line 1174):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
references: z.array(z.string()).optional().describe(
|
||||||
|
"Git repositories or local paths to reference from subagents"
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/global/index.ts`
|
||||||
|
|
||||||
|
Add reference cache path and create directory:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Add to Path object:
|
||||||
|
reference: path.join(cache, "references"),
|
||||||
|
|
||||||
|
// Add to mkdir promise array:
|
||||||
|
fs.mkdir(path.join(cache, "references"), { recursive: true }),
|
||||||
|
```
|
||||||
|
|
||||||
|
### `src/agent/agent.ts`
|
||||||
|
|
||||||
|
Add `reference` agent to built-in agents (after `explore` around line 155):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
reference: {
|
||||||
|
name: "reference",
|
||||||
|
description: `Search across referenced projects configured in opencode.json under "references". Use this to query code in external repositories.`,
|
||||||
|
permission: PermissionNext.merge(
|
||||||
|
defaults,
|
||||||
|
PermissionNext.fromConfig({
|
||||||
|
"*": "deny",
|
||||||
|
grep: "allow",
|
||||||
|
glob: "allow",
|
||||||
|
list: "allow",
|
||||||
|
bash: "allow",
|
||||||
|
webfetch: "allow",
|
||||||
|
websearch: "allow",
|
||||||
|
codesearch: "allow",
|
||||||
|
read: "allow",
|
||||||
|
lsp: "allow",
|
||||||
|
external_directory: {
|
||||||
|
[Truncate.GLOB]: "allow",
|
||||||
|
// Reference paths added dynamically via Reference.directories()
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
user,
|
||||||
|
),
|
||||||
|
prompt: PROMPT_REFERENCE,
|
||||||
|
options: {},
|
||||||
|
mode: "subagent",
|
||||||
|
native: true,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Need to inject reference paths into `external_directory` permissions. This requires:
|
||||||
|
|
||||||
|
1. Loading references via `Reference.list()`
|
||||||
|
2. Ensuring they're fresh via `Reference.ensureFresh()`
|
||||||
|
3. Adding their paths to `external_directory` permissions
|
||||||
|
|
||||||
|
**Challenge**: The agent definition is loaded once at startup, but references may change. Solution: Inject reference paths dynamically in the Task tool when creating the subagent session.
|
||||||
|
|
||||||
|
### `src/tool/task.ts`
|
||||||
|
|
||||||
|
When creating session for `reference` subagent (around line 72):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Before Session.create:
|
||||||
|
let extraPermissions = []
|
||||||
|
if (params.subagent_type === "reference") {
|
||||||
|
const refs = await Reference.list()
|
||||||
|
await Promise.all(refs.map((r) => Reference.ensureFresh(r)))
|
||||||
|
const paths = refs.map((r) => path.join(r.path, "*"))
|
||||||
|
extraPermissions = paths.map((p) => ({
|
||||||
|
permission: "external_directory" as const,
|
||||||
|
pattern: p,
|
||||||
|
action: "allow" as const,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add extraPermissions to the permission array in Session.create
|
||||||
|
```
|
||||||
|
|
||||||
|
Also need to inject available references into the prompt. Modify `SessionPrompt.resolvePromptParts` or add a way to inject context.
|
||||||
|
|
||||||
|
### `src/session/prompt.ts` (or similar)
|
||||||
|
|
||||||
|
Inject reference list into reference agent's system prompt:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// When agent.name === "reference", prepend reference info to prompt
|
||||||
|
const refs = await Reference.list()
|
||||||
|
const refList = refs.map((r) => `- ${r.url} at ${r.path}`).join("\n")
|
||||||
|
// Inject into prompt or parts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
1. **Clone failures**: Log warning, continue with other references. Don't block subagent.
|
||||||
|
2. **Network failures**: Use cached version if exists, even if stale. Only error if never cloned.
|
||||||
|
3. **Branch not found**: Log error, skip that reference.
|
||||||
|
4. **Local path missing**: Log warning, skip reference.
|
||||||
|
5. **Invalid URL format**: Log warning, skip reference.
|
||||||
|
6. **Shallow clone**: Use `--depth 1` for faster clones. For branch-specific URLs, use `--branch <ref> --depth 1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. Add to `opencode.json`: `{"references": ["git@github.com:effect-ts/effect.git"]}`
|
||||||
|
2. Invoke `@reference` with query like "show me the Effect schema module"
|
||||||
|
3. Verify repo cloned to `~/.cache/opencode/references/<hash>/`
|
||||||
|
4. Verify subagent can search/read files in the cloned repo
|
||||||
|
5. Wait 1+ hour, invoke again, verify fetch happens
|
||||||
|
6. Test with local path: `{"references": ["/path/to/local/repo"]}`
|
||||||
|
7. Test with branch: `{"references": ["git@github.com:foo/bar.git#main"]}`
|
||||||
|
8. Verify error handling with invalid URL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Summary
|
||||||
|
|
||||||
|
| File | Action |
|
||||||
|
| -------------------------------- | ------------------------------------------- |
|
||||||
|
| `src/reference/index.ts` | Create |
|
||||||
|
| `src/agent/prompt/reference.txt` | Create |
|
||||||
|
| `src/config/config.ts` | Add `references` to schema |
|
||||||
|
| `src/global/index.ts` | Add `reference` path |
|
||||||
|
| `src/agent/agent.ts` | Add `reference` agent |
|
||||||
|
| `src/tool/task.ts` | Inject ref permissions + paths into session |
|
||||||
|
| `src/session/prompt.ts` | Inject reference list into system prompt |
|
||||||
|
|
@ -42,10 +42,11 @@
|
||||||
"@types/babel__core": "7.20.5",
|
"@types/babel__core": "7.20.5",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/mime-types": "3.0.1",
|
"@types/mime-types": "3.0.1",
|
||||||
|
"@types/npmcli__arborist": "6.3.3",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/yargs": "17.0.33",
|
|
||||||
"@types/which": "3.0.4",
|
"@types/which": "3.0.4",
|
||||||
|
"@types/yargs": "17.0.33",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
"drizzle-kit": "1.0.0-beta.16-ea816b6",
|
||||||
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
"drizzle-orm": "1.0.0-beta.16-ea816b6",
|
||||||
|
|
@ -84,6 +85,7 @@
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.25.2",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/graphql": "9.0.2",
|
"@octokit/graphql": "9.0.2",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
import z from "zod"
|
|
||||||
import { Global } from "../global"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import path from "path"
|
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
|
||||||
import { text } from "node:stream/consumers"
|
import { text } from "node:stream/consumers"
|
||||||
import { Lock } from "../util/lock"
|
|
||||||
import { PackageRegistry } from "./registry"
|
|
||||||
import { proxied } from "@/util/proxied"
|
|
||||||
import { Process } from "../util/process"
|
import { Process } from "../util/process"
|
||||||
|
|
||||||
export namespace BunProc {
|
export namespace BunProc {
|
||||||
|
|
@ -45,87 +37,4 @@ export namespace BunProc {
|
||||||
export function which() {
|
export function which() {
|
||||||
return process.execPath
|
return process.execPath
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstallFailedError = NamedError.create(
|
|
||||||
"BunInstallFailedError",
|
|
||||||
z.object({
|
|
||||||
pkg: z.string(),
|
|
||||||
version: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export async function install(pkg: string, version = "latest") {
|
|
||||||
// Use lock to ensure only one install at a time
|
|
||||||
using _ = await Lock.write("bun-install")
|
|
||||||
|
|
||||||
const mod = path.join(Global.Path.cache, "node_modules", pkg)
|
|
||||||
const pkgjsonPath = path.join(Global.Path.cache, "package.json")
|
|
||||||
const parsed = await Filesystem.readJson<{ dependencies: Record<string, string> }>(pkgjsonPath).catch(async () => {
|
|
||||||
const result = { dependencies: {} as Record<string, string> }
|
|
||||||
await Filesystem.writeJson(pkgjsonPath, result)
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
if (!parsed.dependencies) parsed.dependencies = {} as Record<string, string>
|
|
||||||
const dependencies = parsed.dependencies
|
|
||||||
const modExists = await Filesystem.exists(mod)
|
|
||||||
const cachedVersion = dependencies[pkg]
|
|
||||||
|
|
||||||
if (!modExists || !cachedVersion) {
|
|
||||||
// continue to install
|
|
||||||
} else if (version !== "latest" && cachedVersion === version) {
|
|
||||||
return mod
|
|
||||||
} else if (version === "latest") {
|
|
||||||
const isOutdated = await PackageRegistry.isOutdated(pkg, cachedVersion, Global.Path.cache)
|
|
||||||
if (!isOutdated) return mod
|
|
||||||
log.info("Cached version is outdated, proceeding with install", { pkg, cachedVersion })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build command arguments
|
|
||||||
const args = [
|
|
||||||
"add",
|
|
||||||
"--force",
|
|
||||||
"--exact",
|
|
||||||
// TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
|
|
||||||
...(proxied() || process.env.CI ? ["--no-cache"] : []),
|
|
||||||
"--cwd",
|
|
||||||
Global.Path.cache,
|
|
||||||
pkg + "@" + version,
|
|
||||||
]
|
|
||||||
|
|
||||||
// Let Bun handle registry resolution:
|
|
||||||
// - If .npmrc files exist, Bun will use them automatically
|
|
||||||
// - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
|
|
||||||
// - No need to pass --registry flag
|
|
||||||
log.info("installing package using Bun's default registry resolution", {
|
|
||||||
pkg,
|
|
||||||
version,
|
|
||||||
})
|
|
||||||
|
|
||||||
await BunProc.run(args, {
|
|
||||||
cwd: Global.Path.cache,
|
|
||||||
}).catch((e) => {
|
|
||||||
throw new InstallFailedError(
|
|
||||||
{ pkg, version },
|
|
||||||
{
|
|
||||||
cause: e,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Resolve actual version from installed package when using "latest"
|
|
||||||
// This ensures subsequent starts use the cached version until explicitly updated
|
|
||||||
let resolvedVersion = version
|
|
||||||
if (version === "latest") {
|
|
||||||
const installedPkg = await Filesystem.readJson<{ version?: string }>(path.join(mod, "package.json")).catch(
|
|
||||||
() => null,
|
|
||||||
)
|
|
||||||
if (installedPkg?.version) {
|
|
||||||
resolvedVersion = installedPkg.version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed.dependencies[pkg] = resolvedVersion
|
|
||||||
await Filesystem.writeJson(pkgjsonPath, parsed)
|
|
||||||
return mod
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { pathToFileURL, fileURLToPath } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { createRequire } from "module"
|
import { createRequire } from "module"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
@ -22,7 +22,6 @@ import {
|
||||||
} from "jsonc-parser"
|
} from "jsonc-parser"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { LSPServer } from "../lsp/server"
|
import { LSPServer } from "../lsp/server"
|
||||||
import { BunProc } from "@/bun"
|
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { ConfigMarkdown } from "./markdown"
|
import { ConfigMarkdown } from "./markdown"
|
||||||
import { constants, existsSync } from "fs"
|
import { constants, existsSync } from "fs"
|
||||||
|
|
@ -31,11 +30,11 @@ import { GlobalBus } from "@/bus/global"
|
||||||
import { Event } from "../server/event"
|
import { Event } from "../server/event"
|
||||||
import { Glob } from "../util/glob"
|
import { Glob } from "../util/glob"
|
||||||
import { PackageRegistry } from "@/bun/registry"
|
import { PackageRegistry } from "@/bun/registry"
|
||||||
import { proxied } from "@/util/proxied"
|
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Account } from "@/account"
|
import { Account } from "@/account"
|
||||||
import { ConfigPaths } from "./paths"
|
import { ConfigPaths } from "./paths"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
|
import { Npm } from "@/npm"
|
||||||
|
|
||||||
export namespace Config {
|
export namespace Config {
|
||||||
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
|
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
|
||||||
|
|
@ -282,20 +281,14 @@ export namespace Config {
|
||||||
await Filesystem.writeJson(pkg, json)
|
await Filesystem.writeJson(pkg, json)
|
||||||
|
|
||||||
const gitignore = path.join(dir, ".gitignore")
|
const gitignore = path.join(dir, ".gitignore")
|
||||||
const hasGitIgnore = await Filesystem.exists(gitignore)
|
await Filesystem.write(
|
||||||
if (!hasGitIgnore)
|
gitignore,
|
||||||
await Filesystem.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
|
["node_modules", "plans", "package.json", "bun.lock", ".gitignore", "package-lock.json"].join("\n"),
|
||||||
|
)
|
||||||
|
|
||||||
// Install any additional dependencies defined in the package.json
|
// Install any additional dependencies defined in the package.json
|
||||||
// This allows local plugins and custom tools to use external packages
|
// This allows local plugins and custom tools to use external packages
|
||||||
await BunProc.run(
|
await Npm.install(dir).catch((err) => {
|
||||||
[
|
|
||||||
"install",
|
|
||||||
// TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
|
|
||||||
...(proxied() || process.env.CI ? ["--no-cache"] : []),
|
|
||||||
],
|
|
||||||
{ cwd: dir },
|
|
||||||
).catch((err) => {
|
|
||||||
log.warn("failed to install dependencies", { dir, error: err })
|
log.warn("failed to install dependencies", { dir, error: err })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export namespace Global {
|
||||||
return process.env.OPENCODE_TEST_HOME || os.homedir()
|
return process.env.OPENCODE_TEST_HOME || os.homedir()
|
||||||
},
|
},
|
||||||
data,
|
data,
|
||||||
bin: path.join(data, "bin"),
|
bin: path.join(cache, "bin"),
|
||||||
log: path.join(data, "log"),
|
log: path.join(data, "log"),
|
||||||
cache,
|
cache,
|
||||||
config,
|
config,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import path from "path"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { BunProc } from "../bun"
|
|
||||||
import { text } from "node:stream/consumers"
|
import { text } from "node:stream/consumers"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
|
@ -13,6 +12,7 @@ import { Archive } from "../util/archive"
|
||||||
import { Process } from "../util/process"
|
import { Process } from "../util/process"
|
||||||
import { which } from "../util/which"
|
import { which } from "../util/which"
|
||||||
import { Module } from "@opencode-ai/util/module"
|
import { Module } from "@opencode-ai/util/module"
|
||||||
|
import { Npm } from "@/npm"
|
||||||
|
|
||||||
export namespace LSPServer {
|
export namespace LSPServer {
|
||||||
const log = Log.create({ service: "lsp.server" })
|
const log = Log.create({ service: "lsp.server" })
|
||||||
|
|
@ -102,7 +102,7 @@ export namespace LSPServer {
|
||||||
const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
|
const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
|
||||||
log.info("typescript server", { tsserver })
|
log.info("typescript server", { tsserver })
|
||||||
if (!tsserver) return
|
if (!tsserver) return
|
||||||
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
|
const proc = spawn(await Npm.which("typescript-language-server"), ["--stdio"], {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
|
@ -128,29 +128,8 @@ export namespace LSPServer {
|
||||||
let binary = which("vue-language-server")
|
let binary = which("vue-language-server")
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
Global.Path.bin,
|
binary = await Npm.which("@vue/language-server")
|
||||||
"node_modules",
|
|
||||||
"@vue",
|
|
||||||
"language-server",
|
|
||||||
"bin",
|
|
||||||
"vue-language-server.js",
|
|
||||||
)
|
|
||||||
if (!(await Filesystem.exists(js))) {
|
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
|
||||||
await Process.spawn([BunProc.which(), "install", "@vue/language-server"], {
|
|
||||||
cwd: Global.Path.bin,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
BUN_BE_BUN: "1",
|
|
||||||
},
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
stdin: "pipe",
|
|
||||||
}).exited
|
|
||||||
}
|
|
||||||
binary = BunProc.which()
|
|
||||||
args.push("run", js)
|
|
||||||
}
|
}
|
||||||
args.push("--stdio")
|
args.push("--stdio")
|
||||||
const proc = spawn(binary, args, {
|
const proc = spawn(binary, args, {
|
||||||
|
|
@ -213,7 +192,7 @@ export namespace LSPServer {
|
||||||
log.info("installed VS Code ESLint server", { serverPath })
|
log.info("installed VS Code ESLint server", { serverPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
const proc = spawn(BunProc.which(), [serverPath, "--stdio"], {
|
const proc = spawn(await Npm.which("tsx"), [serverPath, "--stdio"], {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
|
@ -344,8 +323,8 @@ export namespace LSPServer {
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
const resolved = Module.resolve("biome", root)
|
const resolved = Module.resolve("biome", root)
|
||||||
if (!resolved) return
|
if (!resolved) return
|
||||||
bin = BunProc.which()
|
bin = await Npm.which("biome")
|
||||||
args = ["x", "biome", "lsp-proxy", "--stdio"]
|
args = ["lsp-proxy", "--stdio"]
|
||||||
}
|
}
|
||||||
|
|
||||||
const proc = spawn(bin, args, {
|
const proc = spawn(bin, args, {
|
||||||
|
|
@ -371,9 +350,7 @@ export namespace LSPServer {
|
||||||
},
|
},
|
||||||
extensions: [".go"],
|
extensions: [".go"],
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = which("gopls", {
|
let bin = which("gopls")
|
||||||
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
|
|
||||||
})
|
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
if (!which("go")) return
|
if (!which("go")) return
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
|
|
@ -408,9 +385,7 @@ export namespace LSPServer {
|
||||||
root: NearestRoot(["Gemfile"]),
|
root: NearestRoot(["Gemfile"]),
|
||||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = which("rubocop", {
|
let bin = which("rubocop")
|
||||||
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
|
|
||||||
})
|
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
const ruby = which("ruby")
|
const ruby = which("ruby")
|
||||||
const gem = which("gem")
|
const gem = which("gem")
|
||||||
|
|
@ -515,19 +490,8 @@ export namespace LSPServer {
|
||||||
let binary = which("pyright-langserver")
|
let binary = which("pyright-langserver")
|
||||||
const args = []
|
const args = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
if (!(await Filesystem.exists(js))) {
|
binary = await Npm.which("pyright")
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
|
||||||
await Process.spawn([BunProc.which(), "install", "pyright"], {
|
|
||||||
cwd: Global.Path.bin,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
BUN_BE_BUN: "1",
|
|
||||||
},
|
|
||||||
}).exited
|
|
||||||
}
|
|
||||||
binary = BunProc.which()
|
|
||||||
args.push(...["run", js])
|
|
||||||
}
|
}
|
||||||
args.push("--stdio")
|
args.push("--stdio")
|
||||||
|
|
||||||
|
|
@ -629,9 +593,7 @@ export namespace LSPServer {
|
||||||
extensions: [".zig", ".zon"],
|
extensions: [".zig", ".zon"],
|
||||||
root: NearestRoot(["build.zig"]),
|
root: NearestRoot(["build.zig"]),
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = which("zls", {
|
let bin = which("zls")
|
||||||
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
const zig = which("zig")
|
const zig = which("zig")
|
||||||
|
|
@ -741,9 +703,7 @@ export namespace LSPServer {
|
||||||
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
|
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
|
||||||
extensions: [".cs"],
|
extensions: [".cs"],
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = which("csharp-ls", {
|
let bin = which("csharp-ls")
|
||||||
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
|
|
||||||
})
|
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
if (!which("dotnet")) {
|
if (!which("dotnet")) {
|
||||||
log.error(".NET SDK is required to install csharp-ls")
|
log.error(".NET SDK is required to install csharp-ls")
|
||||||
|
|
@ -780,9 +740,7 @@ export namespace LSPServer {
|
||||||
root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
|
root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
|
||||||
extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
|
extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = which("fsautocomplete", {
|
let bin = which("fsautocomplete")
|
||||||
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
|
|
||||||
})
|
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
if (!which("dotnet")) {
|
if (!which("dotnet")) {
|
||||||
log.error(".NET SDK is required to install fsautocomplete")
|
log.error(".NET SDK is required to install fsautocomplete")
|
||||||
|
|
@ -1048,22 +1006,8 @@ export namespace LSPServer {
|
||||||
let binary = which("svelteserver")
|
let binary = which("svelteserver")
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
if (!(await Filesystem.exists(js))) {
|
binary = await Npm.which("svelte-language-server")
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
|
||||||
await Process.spawn([BunProc.which(), "install", "svelte-language-server"], {
|
|
||||||
cwd: Global.Path.bin,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
BUN_BE_BUN: "1",
|
|
||||||
},
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
stdin: "pipe",
|
|
||||||
}).exited
|
|
||||||
}
|
|
||||||
binary = BunProc.which()
|
|
||||||
args.push("run", js)
|
|
||||||
}
|
}
|
||||||
args.push("--stdio")
|
args.push("--stdio")
|
||||||
const proc = spawn(binary, args, {
|
const proc = spawn(binary, args, {
|
||||||
|
|
@ -1095,22 +1039,8 @@ export namespace LSPServer {
|
||||||
let binary = which("astro-ls")
|
let binary = which("astro-ls")
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
if (!(await Filesystem.exists(js))) {
|
binary = await Npm.which("@astrojs/language-server")
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
|
||||||
await Process.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
|
|
||||||
cwd: Global.Path.bin,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
BUN_BE_BUN: "1",
|
|
||||||
},
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
stdin: "pipe",
|
|
||||||
}).exited
|
|
||||||
}
|
|
||||||
binary = BunProc.which()
|
|
||||||
args.push("run", js)
|
|
||||||
}
|
}
|
||||||
args.push("--stdio")
|
args.push("--stdio")
|
||||||
const proc = spawn(binary, args, {
|
const proc = spawn(binary, args, {
|
||||||
|
|
@ -1359,31 +1289,8 @@ export namespace LSPServer {
|
||||||
let binary = which("yaml-language-server")
|
let binary = which("yaml-language-server")
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
Global.Path.bin,
|
binary = await Npm.which("yaml-language-server")
|
||||||
"node_modules",
|
|
||||||
"yaml-language-server",
|
|
||||||
"out",
|
|
||||||
"server",
|
|
||||||
"src",
|
|
||||||
"server.js",
|
|
||||||
)
|
|
||||||
const exists = await Filesystem.exists(js)
|
|
||||||
if (!exists) {
|
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
|
||||||
await Process.spawn([BunProc.which(), "install", "yaml-language-server"], {
|
|
||||||
cwd: Global.Path.bin,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
BUN_BE_BUN: "1",
|
|
||||||
},
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
stdin: "pipe",
|
|
||||||
}).exited
|
|
||||||
}
|
|
||||||
binary = BunProc.which()
|
|
||||||
args.push("run", js)
|
|
||||||
}
|
}
|
||||||
args.push("--stdio")
|
args.push("--stdio")
|
||||||
const proc = spawn(binary, args, {
|
const proc = spawn(binary, args, {
|
||||||
|
|
@ -1412,9 +1319,7 @@ export namespace LSPServer {
|
||||||
]),
|
]),
|
||||||
extensions: [".lua"],
|
extensions: [".lua"],
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = which("lua-language-server", {
|
let bin = which("lua-language-server")
|
||||||
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
|
|
@ -1550,22 +1455,8 @@ export namespace LSPServer {
|
||||||
let binary = which("intelephense")
|
let binary = which("intelephense")
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
if (!(await Filesystem.exists(js))) {
|
binary = await Npm.which("intelephense")
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
|
||||||
await Process.spawn([BunProc.which(), "install", "intelephense"], {
|
|
||||||
cwd: Global.Path.bin,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
BUN_BE_BUN: "1",
|
|
||||||
},
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
stdin: "pipe",
|
|
||||||
}).exited
|
|
||||||
}
|
|
||||||
binary = BunProc.which()
|
|
||||||
args.push("run", js)
|
|
||||||
}
|
}
|
||||||
args.push("--stdio")
|
args.push("--stdio")
|
||||||
const proc = spawn(binary, args, {
|
const proc = spawn(binary, args, {
|
||||||
|
|
@ -1647,22 +1538,8 @@ export namespace LSPServer {
|
||||||
let binary = which("bash-language-server")
|
let binary = which("bash-language-server")
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
if (!(await Filesystem.exists(js))) {
|
binary = await Npm.which("bash-language-server")
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
|
||||||
await Process.spawn([BunProc.which(), "install", "bash-language-server"], {
|
|
||||||
cwd: Global.Path.bin,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
BUN_BE_BUN: "1",
|
|
||||||
},
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
stdin: "pipe",
|
|
||||||
}).exited
|
|
||||||
}
|
|
||||||
binary = BunProc.which()
|
|
||||||
args.push("run", js)
|
|
||||||
}
|
}
|
||||||
args.push("start")
|
args.push("start")
|
||||||
const proc = spawn(binary, args, {
|
const proc = spawn(binary, args, {
|
||||||
|
|
@ -1683,9 +1560,7 @@ export namespace LSPServer {
|
||||||
extensions: [".tf", ".tfvars"],
|
extensions: [".tf", ".tfvars"],
|
||||||
root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
|
root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = which("terraform-ls", {
|
let bin = which("terraform-ls")
|
||||||
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
|
|
@ -1766,9 +1641,7 @@ export namespace LSPServer {
|
||||||
extensions: [".tex", ".bib"],
|
extensions: [".tex", ".bib"],
|
||||||
root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
|
root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = which("texlab", {
|
let bin = which("texlab")
|
||||||
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
|
|
@ -1859,22 +1732,8 @@ export namespace LSPServer {
|
||||||
let binary = which("docker-langserver")
|
let binary = which("docker-langserver")
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
if (!(await Filesystem.exists(js))) {
|
binary = await Npm.which("dockerfile-language-server-nodejs")
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
|
||||||
await Process.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
|
|
||||||
cwd: Global.Path.bin,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
BUN_BE_BUN: "1",
|
|
||||||
},
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
stdin: "pipe",
|
|
||||||
}).exited
|
|
||||||
}
|
|
||||||
binary = BunProc.which()
|
|
||||||
args.push("run", js)
|
|
||||||
}
|
}
|
||||||
args.push("--stdio")
|
args.push("--stdio")
|
||||||
const proc = spawn(binary, args, {
|
const proc = spawn(binary, args, {
|
||||||
|
|
@ -1965,9 +1824,7 @@ export namespace LSPServer {
|
||||||
extensions: [".typ", ".typc"],
|
extensions: [".typ", ".typc"],
|
||||||
root: NearestRoot(["typst.toml"]),
|
root: NearestRoot(["typst.toml"]),
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = which("tinymist", {
|
let bin = which("tinymist")
|
||||||
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
|
|
|
||||||
84
packages/opencode/src/npm/index.ts
Normal file
84
packages/opencode/src/npm/index.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import z from "zod"
|
||||||
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
|
import { Global } from "../global"
|
||||||
|
import { Lock } from "../util/lock"
|
||||||
|
import { Log } from "../util/log"
|
||||||
|
import path from "path"
|
||||||
|
import { readdir } from "fs/promises"
|
||||||
|
import { Arborist } from "@npmcli/arborist"
|
||||||
|
|
||||||
|
export namespace Npm {
|
||||||
|
const log = Log.create({ service: "npm" })
|
||||||
|
|
||||||
|
export const InstallFailedError = NamedError.create(
|
||||||
|
"NpmInstallFailedError",
|
||||||
|
z.object({
|
||||||
|
pkg: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
function directory(pkg: string) {
|
||||||
|
return path.join(Global.Path.cache, "packages", pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function add(pkg: string) {
|
||||||
|
using _ = await Lock.write("npm-install")
|
||||||
|
log.info("installing package using npm arborist", {
|
||||||
|
pkg,
|
||||||
|
})
|
||||||
|
const hash = pkg
|
||||||
|
const dir = directory(hash)
|
||||||
|
|
||||||
|
const arborist = new Arborist({
|
||||||
|
path: dir,
|
||||||
|
binLinks: true,
|
||||||
|
progress: false,
|
||||||
|
savePrefix: "",
|
||||||
|
})
|
||||||
|
const tree = await arborist.loadVirtual().catch(() => {})
|
||||||
|
if (tree) {
|
||||||
|
const first = tree.edgesOut.values().next().value?.to
|
||||||
|
if (first) return 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 first.path
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function install(dir: string) {
|
||||||
|
console.log(dir)
|
||||||
|
const arb = new Arborist({
|
||||||
|
path: dir,
|
||||||
|
binLinks: true,
|
||||||
|
progress: false,
|
||||||
|
savePrefix: "",
|
||||||
|
})
|
||||||
|
await arb.reify()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function which(pkg: string) {
|
||||||
|
const dir = path.join(directory(pkg), "node_modules", ".bin")
|
||||||
|
const files = await readdir(dir).catch(() => [])
|
||||||
|
if (!files.length) {
|
||||||
|
await add(pkg)
|
||||||
|
return which(pkg)
|
||||||
|
}
|
||||||
|
return path.join(dir, files[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import { Bus } from "../bus"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||||
import { Server } from "../server/server"
|
import { Server } from "../server/server"
|
||||||
import { BunProc } from "../bun"
|
import { Npm } from "../npm"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import { CodexAuthPlugin } from "./codex"
|
import { CodexAuthPlugin } from "./codex"
|
||||||
|
|
@ -59,16 +59,13 @@ export namespace Plugin {
|
||||||
if (plugin.includes("opencode-openai-codex-auth") || plugin.includes("opencode-copilot-auth")) continue
|
if (plugin.includes("opencode-openai-codex-auth") || plugin.includes("opencode-copilot-auth")) continue
|
||||||
log.info("loading plugin", { path: plugin })
|
log.info("loading plugin", { path: plugin })
|
||||||
if (!plugin.startsWith("file://")) {
|
if (!plugin.startsWith("file://")) {
|
||||||
const lastAtIndex = plugin.lastIndexOf("@")
|
plugin = await Npm.add(plugin).catch((err) => {
|
||||||
const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
|
|
||||||
const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
|
|
||||||
plugin = await BunProc.install(pkg, version).catch((err) => {
|
|
||||||
const cause = err instanceof Error ? err.cause : err
|
const cause = err instanceof Error ? err.cause : err
|
||||||
const detail = cause instanceof Error ? cause.message : String(cause ?? err)
|
const detail = cause instanceof Error ? cause.message : String(cause ?? err)
|
||||||
log.error("failed to install plugin", { pkg, version, error: detail })
|
log.error("failed to install plugin", { plugin, error: detail })
|
||||||
Bus.publish(Session.Event.Error, {
|
Bus.publish(Session.Event.Error, {
|
||||||
error: new NamedError.Unknown({
|
error: new NamedError.Unknown({
|
||||||
message: `Failed to install plugin ${pkg}@${version}: ${detail}`,
|
message: `Failed to install plugin ${plugin}: ${detail}`,
|
||||||
}).toObject(),
|
}).toObject(),
|
||||||
})
|
})
|
||||||
return ""
|
return ""
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Config } from "../config/config"
|
||||||
import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
|
import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
|
||||||
import { NoSuchModelError, type Provider as SDK } from "ai"
|
import { NoSuchModelError, type Provider as SDK } from "ai"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { BunProc } from "../bun"
|
import { Npm } from "../npm"
|
||||||
import { Hash } from "../util/hash"
|
import { Hash } from "../util/hash"
|
||||||
import { Plugin } from "../plugin"
|
import { Plugin } from "../plugin"
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
|
|
@ -1201,7 +1201,7 @@ export namespace Provider {
|
||||||
|
|
||||||
let installedPath: string
|
let installedPath: string
|
||||||
if (!model.api.npm.startsWith("file://")) {
|
if (!model.api.npm.startsWith("file://")) {
|
||||||
installedPath = await BunProc.install(model.api.npm, "latest")
|
installedPath = await Npm.add(model.api.npm)
|
||||||
} else {
|
} else {
|
||||||
log.info("loading local provider", { pkg: model.api.npm })
|
log.info("loading local provider", { pkg: model.api.npm })
|
||||||
installedPath = model.api.npm
|
installedPath = model.api.npm
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import whichPkg from "which"
|
import whichPkg from "which"
|
||||||
|
import path from "path"
|
||||||
|
import { Global } from "../global"
|
||||||
|
|
||||||
export function which(cmd: string, env?: NodeJS.ProcessEnv) {
|
export function which(cmd: string, env?: NodeJS.ProcessEnv) {
|
||||||
|
const base = env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path
|
||||||
|
const full = base + path.delimiter + Global.Path.bin
|
||||||
const result = whichPkg.sync(cmd, {
|
const result = whichPkg.sync(cmd, {
|
||||||
nothrow: true,
|
nothrow: true,
|
||||||
path: env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path,
|
path: full,
|
||||||
pathExt: env?.PATHEXT ?? env?.PathExt ?? process.env.PATHEXT ?? process.env.PathExt,
|
pathExt: env?.PATHEXT ?? env?.PathExt ?? process.env.PATHEXT ?? process.env.PathExt,
|
||||||
})
|
})
|
||||||
return typeof result === "string" ? result : null
|
return typeof result === "string" ? result : null
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue