Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 17 additions & 72 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,85 +165,30 @@ export namespace Config {
}
}

// Per-directory install locks to serialize concurrent installs
const installLocks = new Map<string, Promise<void>>()

// Marker file schema for tracking installed dependencies
const DepsInstalledMarker = z.object({
version: z.string(),
timestamp: z.number(),
})

async function installDependencies(dir: string) {
// Wait for any ongoing install in this directory to complete
const existingLock = installLocks.get(dir)
if (existingLock) {
await existingLock
return
}
if (Installation.isLocal()) return

// Create a new lock for this directory
let resolveLock: () => void
const lockPromise = new Promise<void>((resolve) => {
resolveLock = resolve
})
installLocks.set(dir, lockPromise)

try {
const markerPath = path.join(dir, ".deps-installed.json")
const targetVersion = Installation.isLocal() ? "latest" : Installation.BASE_VERSION

// Check if dependencies are already installed with correct version
try {
const markerContent = await Bun.file(markerPath).text()
const marker = DepsInstalledMarker.parse(JSON.parse(markerContent))
if (marker.version === targetVersion) {
log.debug("dependencies already installed", { dir, version: marker.version })
return
}
} catch {
// Marker doesn't exist or is invalid, proceed with install
}
const pkg = path.join(dir, "package.json")

const pkg = path.join(dir, "package.json")

if (!(await Bun.file(pkg).exists())) {
await Bun.write(pkg, "{}")
}
if (!(await Bun.file(pkg).exists())) {
await Bun.write(pkg, "{}")
}

const gitignore = path.join(dir, ".gitignore")
const hasGitIgnore = await Bun.file(gitignore).exists()
if (!hasGitIgnore) {
await Bun.write(
gitignore,
["node_modules", "package.json", "bun.lock", ".gitignore", ".deps-installed.json"].join("\n"),
)
}
const gitignore = path.join(dir, ".gitignore")
const hasGitIgnore = await Bun.file(gitignore).exists()
if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))

// Use BASE_VERSION for @opencode-ai/plugin since it's published by upstream without our -N suffix
await BunProc.run(["add", "@opencode-ai/plugin@" + targetVersion, "--exact"], {
// Use BASE_VERSION for @opencode-ai/plugin since it's published by upstream without our -N suffix
await BunProc.run(
["add", "@opencode-ai/plugin@" + Installation.BASE_VERSION, "--exact"],
{
cwd: dir,
})

// Install any additional dependencies defined in the package.json
// This allows local plugins and custom tools to use external packages
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
},
).catch(() => {})

// Write marker file after successful install
await Bun.write(
markerPath,
JSON.stringify({
version: targetVersion,
timestamp: Date.now(),
}),
)
log.info("dependencies installed", { dir, version: targetVersion })
} catch (err) {
log.error("failed to install dependencies", { dir, error: err })
} finally {
installLocks.delete(dir)
resolveLock!()
}
// Install any additional dependencies defined in the package.json
// This allows local plugins and custom tools to use external packages
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
}

const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md")
Expand Down
33 changes: 1 addition & 32 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,35 +724,4 @@ test("processes .opencode directory without errors in local dev mode", async ()
})
})

test(".opencode directory gets package.json and gitignore created", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
await Config.get()

const opencodeDir = path.join(tmp.path, ".opencode")

// Verify package.json was created
const pkgPath = path.join(opencodeDir, "package.json")
const pkgExists = await Bun.file(pkgPath).exists()
expect(pkgExists).toBe(true)

// Verify .gitignore was created
const gitignorePath = path.join(opencodeDir, ".gitignore")
const gitignoreExists = await Bun.file(gitignorePath).exists()
expect(gitignoreExists).toBe(true)

// Verify .gitignore contains necessary entries
const gitignoreContent = await Bun.file(gitignorePath).text()
expect(gitignoreContent).toContain("node_modules")
expect(gitignoreContent).toContain("package.json")
expect(gitignoreContent).toContain("bun.lock")
},
})
})