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
154 changes: 22 additions & 132 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
exact = true
# Only install newly resolved package versions published at least 3 days ago.
minimumReleaseAge = 259200
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"]
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@jdxcode/aube-ffi", "@jdxcode/aube-ffi-darwin-arm64", "@jdxcode/aube-ffi-darwin-x64", "@jdxcode/aube-ffi-linux-arm64", "@jdxcode/aube-ffi-linux-arm64-musl", "@jdxcode/aube-ffi-linux-x64", "@jdxcode/aube-ffi-linux-x64-musl", "@jdxcode/aube-ffi-win32-arm64", "@jdxcode/aube-ffi-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"]

[test]
root = "./do-not-run-tests-from-root"
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"@effect/opentelemetry": "4.0.0-beta.83",
"@effect/platform-node": "4.0.0-beta.83",
"@effect/sql-sqlite-bun": "4.0.0-beta.83",
"@npmcli/arborist": "9.4.0",
"@types/bun": "1.3.13",
"@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0",
Expand Down
3 changes: 1 addition & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
"@types/cross-spawn": "catalog:",
"@types/node": "catalog:",
"@types/npm-package-arg": "6.1.4",
"@types/npmcli__arborist": "6.3.3",
"@types/semver": "catalog:",
"@types/turndown": "5.0.5",
"@types/which": "3.0.4",
Expand Down Expand Up @@ -87,7 +86,7 @@
"@effect/sql-sqlite-bun": "catalog:",
"@lydell/node-pty": "catalog:",
"@ff-labs/fff-bun": "0.9.4",
"@npmcli/arborist": "9.4.0",
"@jdxcode/aube-ffi": "1.28.0",
"@npmcli/config": "10.8.1",
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/effect-sqlite-node": "workspace:*",
Expand Down
86 changes: 86 additions & 0 deletions packages/core/src/aube-ffi/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Client for aube's C ABI (@jdxcode/aube-ffi). Each operation gets its own
// worker because aube_wait blocks the thread that calls it; cancellation goes
// through aube_cancel on the main thread using the handle the worker posts
// back before waiting.
import type { FfiOperation, FfiResult, WorkerRequest, WorkerResponse } from "./protocol"

export class AubeFfiError extends Error {
constructor(
readonly code: string,
message: string,
) {
super(message)
this.name = "AubeFfiError"
}
}

const host = { name: "opencode", version: "1.0.0" }

// The Node-API addon maps the project .npmrc's `omit` entries to dependency
// selection itself; the C ABI leaves that to the host, so mirror it here.
export const npmrcDepFlags = async (projectDir: string) => {
const path = await import("path")
const flags: { prodOnly?: boolean; omitOptional?: boolean } = {}
const text = await import("fs/promises")
.then((fsp) => fsp.readFile(path.join(projectDir, ".npmrc"), "utf8"))
.catch(() => "")
for (const line of text.split("\n")) {
const match = line.match(/^\s*omit(\[\])?\s*=\s*(.*)$/)
if (!match) continue
for (const value of match[2].split(/[\s,]+/)) {
if (value === "dev") flags.prodOnly = true
if (value === "optional") flags.omitOptional = true
}
}
return flags
}

let cancelSymbol: ((handle: bigint | number) => number) | undefined

const cancel = async (handle: bigint | number) => {
if (!cancelSymbol) {
const { dlopen } = await import("bun:ffi")
const { libraryPath } = await import("@jdxcode/aube-ffi")
cancelSymbol = dlopen(libraryPath, {
aube_cancel: { args: ["u64"], returns: "i32" },
}).symbols.aube_cancel as (handle: bigint | number) => number
}
cancelSymbol(handle)
}

export const run = (op: FfiOperation, signal?: AbortSignal): Promise<FfiResult> =>
new Promise<FfiResult>((resolve, reject) => {
const worker = new Worker(new URL("./worker.ts", import.meta.url))
let handle: bigint | number | undefined
let aborted = false

const onAbort = () => {
aborted = true
if (handle !== undefined) void cancel(handle)
}
signal?.addEventListener("abort", onAbort, { once: true })

const finish = (act: () => void) => {
signal?.removeEventListener("abort", onAbort)
worker.terminate()
act()
}

worker.onmessage = (message: MessageEvent<WorkerResponse>) => {
const data = message.data
if (data.kind === "started") {
handle = data.handle
if (aborted) void cancel(handle)
return
}
finish(() => resolve(data.result))
}
worker.onerror = (event) => {
finish(() => reject(new AubeFfiError("ERR_AUBE_FFI_WORKER", event.message ?? "aube ffi worker failed")))
}

worker.postMessage({ host, op } satisfies WorkerRequest)
}).then((result) => {
if (!result.ok) throw new AubeFfiError(result.code ?? "ERR_AUBE_FFI_RUNTIME", result.message ?? "install failed")
return result
})
39 changes: 39 additions & 0 deletions packages/core/src/aube-ffi/protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Message shapes shared between the aube C ABI worker and its client.

export type InstallOptions = {
projectDir: string
ignoreScripts?: boolean
offline?: boolean
prodOnly?: boolean
omitOptional?: boolean
}

export type AddOptions = {
saveExact?: boolean
ignoreScripts?: boolean
offline?: boolean
prodOnly?: boolean
omitOptional?: boolean
}

export type FfiOperation =
| { kind: "install"; options: InstallOptions }
| { kind: "add"; projectDir: string; packages: string[]; options: AddOptions }

export type FfiResult = {
ok: boolean
code?: string
message?: string
}

export type HostProfile = {
name: string
version: string
}

export type WorkerRequest = {
host: HostProfile
op: FfiOperation
}

export type WorkerResponse = { kind: "started"; handle: bigint | number } | { kind: "result"; result: FfiResult }
49 changes: 49 additions & 0 deletions packages/core/src/aube-ffi/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// aube_wait blocks its calling thread until the operation completes, so every
// operation runs in a worker while the main thread keeps its event loop free.
// The operation handle is posted back before waiting so the main thread can
// aube_cancel it.
import { CString, dlopen, ptr } from "bun:ffi"
import { libraryPath } from "@jdxcode/aube-ffi"

import type { FfiOperation, FfiResult, WorkerRequest, WorkerResponse } from "./protocol"

const library = dlopen(libraryPath, {
aube_init: { args: ["ptr"], returns: "i32" },
aube_install: { args: ["ptr", "ptr", "ptr"], returns: "u64" },
aube_add: { args: ["ptr", "ptr", "ptr", "ptr", "ptr"], returns: "u64" },
aube_wait: { args: ["u64"], returns: "ptr" },
aube_string_free: { args: ["ptr"], returns: "void" },
})

const cstr = (value: string) => Buffer.from(`${value}\0`, "utf8")

const start = (op: FfiOperation) => {
if (op.kind === "install") {
return library.symbols.aube_install(ptr(cstr(JSON.stringify(op.options))), null, null)
}
return library.symbols.aube_add(
ptr(cstr(op.projectDir)),
ptr(cstr(JSON.stringify(op.packages))),
ptr(cstr(JSON.stringify(op.options))),
null,
null,
)
}

const wait = (handle: bigint | number): FfiResult => {
const pointer = library.symbols.aube_wait(handle)
if (!pointer) return { ok: false, code: "ERR_AUBE_FFI_UNKNOWN_HANDLE", message: "aube_wait returned null" }
const result = JSON.parse(new CString(pointer).toString()) as FfiResult
library.symbols.aube_string_free(pointer)
return result
}

declare var self: Worker

self.onmessage = (message: MessageEvent<WorkerRequest>) => {
const { host, op } = message.data
library.symbols.aube_init(ptr(cstr(JSON.stringify(host))))
const handle = start(op)
postMessage({ kind: "started", handle } satisfies WorkerResponse)
postMessage({ kind: "result", result: wait(handle) } satisfies WorkerResponse)
}
Loading
Loading