Skip to content
Open
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
167 changes: 143 additions & 24 deletions packages/opencode/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,39 +32,158 @@ export namespace Auth {
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).meta({ ref: "Auth" })
export type Info = z.infer<typeof Info>

const filepath = path.join(Global.Path.data, "auth.json")
export const ProviderAuth = z
.object({
accounts: z.record(z.string(), Info),
active: z.string(),
})
.meta({ ref: "ProviderAuth" })
export type ProviderAuth = z.infer<typeof ProviderAuth>

export async function get(providerID: string) {
const auth = await all()
return auth[providerID]
}
export const AccountInfo = z
.object({
name: z.string(),
type: z.enum(["oauth", "api", "wellknown"]),
active: z.boolean(),
})
.meta({ ref: "AccountInfo" })
export type AccountInfo = z.infer<typeof AccountInfo>

export async function all(): Promise<Record<string, Info>> {
const filepath = path.join(Global.Path.data, "auth.json")

async function readRaw(): Promise<Record<string, ProviderAuth>> {
const file = Bun.file(filepath)
const data = await file.json().catch(() => ({}) as Record<string, unknown>)
return Object.entries(data).reduce(
(acc, [key, value]) => {
const parsed = Info.safeParse(value)
if (!parsed.success) return acc
acc[key] = parsed.data
return acc
},
{} as Record<string, Info>,
)
}

export async function set(key: string, info: Info) {
const file = Bun.file(filepath)
const data = await all()
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2))
await fs.chmod(file.name!, 0o600)
const result: Record<string, ProviderAuth> = {}

for (const [providerID, value] of Object.entries(data)) {
const providerAuth = ProviderAuth.safeParse(value)
if (providerAuth.success) {
result[providerID] = providerAuth.data
continue
}

const legacyAuth = Info.safeParse(value)
if (legacyAuth.success) {
result[providerID] = {
accounts: { default: legacyAuth.data },
active: "default",
}
continue
}
}

return result
}

export async function remove(key: string) {
async function writeRaw(data: Record<string, ProviderAuth>) {
const file = Bun.file(filepath)
const data = await all()
delete data[key]
await Bun.write(file, JSON.stringify(data, null, 2))
await fs.chmod(file.name!, 0o600)
}

export async function get(providerID: string): Promise<Info | undefined> {
const data = await readRaw()
const provider = data[providerID]
if (!provider) return undefined
return provider.accounts[provider.active]
}

export async function getAccount(providerID: string, accountName: string): Promise<Info | undefined> {
const data = await readRaw()
const provider = data[providerID]
if (!provider) return undefined
return provider.accounts[accountName]
}

export async function listAccounts(providerID: string): Promise<AccountInfo[]> {
const data = await readRaw()
const provider = data[providerID]
if (!provider) return []
return Object.entries(provider.accounts).map(([name, auth]) => ({
name,
type: auth.type,
active: name === provider.active,
}))
}

export async function all(): Promise<Record<string, Info>> {
const data = await readRaw()
const result: Record<string, Info> = {}
for (const [providerID, provider] of Object.entries(data)) {
const activeAuth = provider.accounts[provider.active]
if (activeAuth) result[providerID] = activeAuth
}
return result
}

export async function allProviders(): Promise<Record<string, ProviderAuth>> {
return readRaw()
}

export async function setAccount(providerID: string, accountName: string, info: Info) {
const data = await readRaw()
const existing = data[providerID]
if (existing) {
existing.accounts[accountName] = info
if (Object.keys(existing.accounts).length === 1) {
existing.active = accountName
}
} else {
data[providerID] = {
accounts: { [accountName]: info },
active: accountName,
}
}
await writeRaw(data)
}

export async function setActive(providerID: string, accountName: string): Promise<boolean> {
const data = await readRaw()
const provider = data[providerID]
if (!provider || !provider.accounts[accountName]) return false
provider.active = accountName
await writeRaw(data)
return true
}

export async function removeAccount(providerID: string, accountName: string): Promise<boolean> {
const data = await readRaw()
const provider = data[providerID]
if (!provider || !provider.accounts[accountName]) return false

delete provider.accounts[accountName]

if (Object.keys(provider.accounts).length === 0) {
delete data[providerID]
} else if (provider.active === accountName) {
provider.active = Object.keys(provider.accounts)[0]
}

await writeRaw(data)
return true
}

export async function set(providerID: string, info: Info) {
await setAccount(providerID, "default", info)
}

export async function remove(providerID: string) {
const data = await readRaw()
delete data[providerID]
await writeRaw(data)
}

export async function getActive(providerID: string): Promise<string | undefined> {
const data = await readRaw()
const provider = data[providerID]
return provider?.active
}

export async function hasAccounts(providerID: string): Promise<boolean> {
const data = await readRaw()
const provider = data[providerID]
return provider ? Object.keys(provider.accounts).length > 0 : false
}
}
22 changes: 15 additions & 7 deletions packages/opencode/src/bun/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,25 @@ export namespace BunProc {
// 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)
// Check if this is a git dependency (github:, git://, git+https://, etc.)
const isGitDep = pkg.startsWith("github:") || pkg.startsWith("git://") || pkg.startsWith("git+")

// For git deps, extract the package name from the URL for the module path
// e.g., "github:user/repo#branch" -> "repo"
const pkgName = isGitDep ? (pkg.split("/").pop()?.split("#")[0] ?? pkg) : pkg
const mod = path.join(Global.Path.cache, "node_modules", pkgName)

const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
const parsed = await pkgjson.json().catch(async () => {
const result = { dependencies: {} }
await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
return result
})
if (parsed.dependencies[pkg] === version) return mod
if (parsed.dependencies[pkgName] === pkg) return mod

// Build command arguments
const args = ["add", "--force", "--exact", "--cwd", Global.Path.cache, pkg + "@" + version]
// Build command arguments - don't append @version for git dependencies
const pkgSpec = isGitDep ? pkg : pkg + "@" + version
const args = ["add", "--force", "--exact", "--cwd", Global.Path.cache, pkgSpec]

// Let Bun handle registry resolution:
// - If .npmrc files exist, Bun will use them automatically
Expand All @@ -98,16 +106,16 @@ export namespace BunProc {

// 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") {
let resolvedVersion: string = isGitDep ? pkg : version
if (!isGitDep && version === "latest") {
const installedPkgJson = Bun.file(path.join(mod, "package.json"))
const installedPkg = await installedPkgJson.json().catch(() => null)
if (installedPkg?.version) {
resolvedVersion = installedPkg.version
}
}

parsed.dependencies[pkg] = resolvedVersion
parsed.dependencies[pkgName] = resolvedVersion
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
return mod
}
Expand Down
Loading