mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
Adds Orca's experimental plugin system behind a settings flag: a supervised kernel, declarative content packs (VM recipes, commands and keybindings, language packs), sandboxed iframe panels, forked worker hosts, and a Git-backed marketplace v0 with consent, provenance and kill-list enforcement. Theme, icon-theme and terminal-theme contributions are deferred to a follow-up pass.
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
import { createReadStream } from 'node:fs'
|
|
import { randomUUID } from 'node:crypto'
|
|
import { mkdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import {
|
|
emptyPluginLockfile,
|
|
parsePluginLockfile,
|
|
serializePluginLockfile,
|
|
type PluginLockfile
|
|
} from '../../shared/plugins/plugin-install-lockfile'
|
|
import { recoverPluginLockfile } from './plugin-install-provenance'
|
|
|
|
export const PLUGIN_LOCKFILE_MAX_BYTES = 5 * 1024 * 1024
|
|
const lockfileAccessChains = new Map<string, Promise<void>>()
|
|
|
|
export function pluginLockfilePath(pluginsDir: string): string {
|
|
return join(pluginsDir, 'plugins.lock.json')
|
|
}
|
|
|
|
async function serializeLockfileAccess<T>(
|
|
pluginsDir: string,
|
|
operation: () => Promise<T>
|
|
): Promise<T> {
|
|
const previous = lockfileAccessChains.get(pluginsDir) ?? Promise.resolve()
|
|
const run = previous.catch(() => undefined).then(operation)
|
|
const settled = run.then(
|
|
() => undefined,
|
|
() => undefined
|
|
)
|
|
lockfileAccessChains.set(pluginsDir, settled)
|
|
try {
|
|
return await run
|
|
} finally {
|
|
if (lockfileAccessChains.get(pluginsDir) === settled) {
|
|
lockfileAccessChains.delete(pluginsDir)
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Reads the install index through a byte cap so a corrupt local file cannot
|
|
* turn every plugin-list refresh into an unbounded main-process allocation. */
|
|
export async function readPluginLockfile(pluginsDir: string): Promise<PluginLockfile> {
|
|
return serializeLockfileAccess(pluginsDir, () => readPluginLockfileUnserialized(pluginsDir))
|
|
}
|
|
|
|
async function readPluginLockfileUnserialized(pluginsDir: string): Promise<PluginLockfile> {
|
|
let lock = emptyPluginLockfile()
|
|
try {
|
|
const chunks: Buffer[] = []
|
|
let totalBytes = 0
|
|
for await (const chunk of createReadStream(pluginLockfilePath(pluginsDir))) {
|
|
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
totalBytes += bytes.byteLength
|
|
if (totalBytes > PLUGIN_LOCKFILE_MAX_BYTES) {
|
|
throw new Error('plugin lockfile exceeds its size limit')
|
|
}
|
|
chunks.push(bytes)
|
|
}
|
|
lock = parsePluginLockfile(JSON.parse(Buffer.concat(chunks, totalBytes).toString('utf8')))
|
|
} catch {
|
|
// Missing/corrupt global indexes can be reconstructed from current-version provenance.
|
|
}
|
|
const recovered = await recoverPluginLockfile(pluginsDir, lock)
|
|
if (recovered.changed) {
|
|
await writePluginLockfileUnserialized(pluginsDir, recovered.lock).catch(() => undefined)
|
|
}
|
|
return recovered.lock
|
|
}
|
|
|
|
export async function writePluginLockfile(pluginsDir: string, lock: PluginLockfile): Promise<void> {
|
|
await serializeLockfileAccess(pluginsDir, () => writePluginLockfileUnserialized(pluginsDir, lock))
|
|
}
|
|
|
|
async function writePluginLockfileUnserialized(
|
|
pluginsDir: string,
|
|
lock: PluginLockfile
|
|
): Promise<void> {
|
|
await mkdir(pluginsDir, { recursive: true })
|
|
const target = pluginLockfilePath(pluginsDir)
|
|
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`
|
|
try {
|
|
await writeFile(temporary, JSON.stringify(serializePluginLockfile(lock), null, 2), 'utf8')
|
|
await rename(temporary, target)
|
|
} finally {
|
|
await rm(temporary, { force: true })
|
|
}
|
|
}
|