mirror of
https://github.com/aphrody-labs/bun-distribution.git
synced 2026-10-13 23:56:39 +00:00
Prioritize native video and Bun migration
This commit is contained in:
1 parent
e2f499cbd5
commit
4500d36811
37 files changed
+1414
-169
No files matched your search
@@ -1,8 +1,9 @@
|
||||
# Goal
|
||||
|
||||
Create and publish an Aphrody/Bun monorepo with selected Storytold repositories
|
||||
as open source libraries in Git submodules. Use our Bun 1.4.4 runtime and pinned
|
||||
Bun Tauri v3 integration. Preserve Rust owners and source licenses.
|
||||
Prioritize the FilmCraft source library for Bun video integration. Reuse its
|
||||
Rust project, media and export owners through a bounded typed Bun adapter.
|
||||
Preserve exact timeline integers, native ownership, Aphrody/Bun identity and
|
||||
explicit runtime qualification. Existing source forks remain independent.
|
||||
|
||||
Deliver pinned forks, portable library commands, the Aphrody shell, Bun frontend
|
||||
tooling and explicit build qualification.
|
||||
Use native Bun quality commands, migrate JavaScript with aggressive n2b, and
|
||||
improve its internal Rust owner for bounded large monorepo scans.
|
||||
@@ -5,6 +5,20 @@
|
||||
- Imported sources are libraries. The shell exposes capabilities and headless
|
||||
CLI inspection, without launching upstream branded GUIs.
|
||||
- Runtime: Bun 1.4.4 at 2ae8fc65fba1d05c423a2433b1195835109942b6.
|
||||
- SDK: published Bun commit c209b95eb0dcc964745f4684f127abb83cbe2697.
|
||||
- SDK: published Bun commit 939c835adf6479bc1014da3843cda4ae29851dbd.
|
||||
- Preserve independent Cargo workspaces and dependency revisions.
|
||||
- Provider authentication and raw sessions remain in isolated stores.
|
||||
- Video is the current priority. Reuse the native source CLI and Bun.Video;
|
||||
no codec implementation or FFmpeg subprocess belongs in this adapter.
|
||||
- FilmCraft schema 15 uses signed i64 ticks and unsigned u64 IDs. Preserve
|
||||
integer precision across JSON rather than routing them through unsafe Numbers.
|
||||
- The installed Bun revision currently lacks Bun.Video. The parallel native
|
||||
owner is replacing the optional media provider; this batch leaves it untouched.
|
||||
- Quality gates use native bun check, bun lint and bun fmt. The formatter config
|
||||
is bunfmt.json; external checker/linter/formatter packages are removed.
|
||||
- Aggressive n2b changes require review: embedded host scripts retain their own
|
||||
runtime, and Buffer or callback rewrites must preserve their actual contract.
|
||||
- Media export uses a fresh destination directory because native formats derive
|
||||
sibling and sequence filenames. Failed operations retain structured results.
|
||||
- Paths remain checkout-relative under the selected C:\src workspace. Native
|
||||
owners are resolved from live tools and configuration after workspace changes.
|
||||
@@ -11,3 +11,19 @@
|
||||
- [x] Apply current Linux, Debian, COSMIC and Bun upstream conventions.
|
||||
- [x] Complete scoped type, lint, format and build checks.
|
||||
- [x] Publish the modified library and prepare the verified monorepo branch.
|
||||
|
||||
## Video priority
|
||||
|
||||
- [x] Audit FilmCraft format, media, time, CLI and export contracts.
|
||||
- [x] Coordinate native Bun.Video ownership with the parallel Bun session.
|
||||
- [x] Add bounded native library inspection, probing, editing and export APIs.
|
||||
- [x] Preserve exact i64/u64 JSON and timeline ticks.
|
||||
- [x] Surface video priority and native availability in the Aphrody shell.
|
||||
- [x] Complete scoped gates and publish the integration.
|
||||
|
||||
## Native tooling
|
||||
|
||||
- [x] Replace external quality packages with native Bun commands.
|
||||
- [x] Apply aggressive n2b to authored code and review its recommendations.
|
||||
- [x] Complete aggressive library and SDK migration through their owners.
|
||||
- [x] Patch the internal n2b owner for large monorepos through a subagent.
|
||||
@@ -3,6 +3,9 @@
|
||||
A Bun 1.4.4 monorepo with 16 open source library forks and our pinned Bun Tauri
|
||||
v3 SDK. Aphrody owns the desktop identity; Bun owns JavaScript tooling.
|
||||
|
||||
Video is the current priority. [The video adapter](docs/video.md) reuses native
|
||||
project, timeline, codec and export owners through Bun.
|
||||
|
||||
## Setup
|
||||
|
||||
Requires our `aphrody-labs/bun` runtime, Git and native Rust. Windows uses MSVC
|
||||
@@ -25,6 +28,7 @@ bun run catalog # library inventory
|
||||
bun run status --json # pinned source state
|
||||
bun run dev # local Aphrody shell
|
||||
bun run build # Bun frontend/backend build
|
||||
bun run fmt # native Bun formatting
|
||||
bun run desktop:dev # native Bun Tauri shell
|
||||
bun run desktop:build # native bundle and Bun sidecar
|
||||
bun run workspace run photocraft --help # headless library inspection
|
||||
@@ -54,5 +58,6 @@ The imported React source builds with Bun. Its historical Tauri v2 shell and
|
||||
tests remain source references; our desktop uses Bun Tauri v3.
|
||||
|
||||
[Architecture](docs/architecture.md), [provenance](docs/provenance.md),
|
||||
[style](docs/style.md), [qualification](docs/qualification.md). Source licenses and notices apply.
|
||||
[style](docs/style.md), [migration](docs/migration.md), [qualification](docs/qualification.md).
|
||||
Source licenses and notices apply.
|
||||
JavaScript builds do not qualify native engines, CEF, installers or providers.
|
||||
+23
-22
@@ -12,17 +12,35 @@
|
||||
"source": {
|
||||
"path": "toolchains/bun",
|
||||
"repository": "aphrody-labs/bun",
|
||||
"revision": "c209b95eb0dcc964745f4684f127abb83cbe2697"
|
||||
"revision": "939c835adf6479bc1014da3843cda4ae29851dbd"
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"id": "filmcraft",
|
||||
"path": "libraries/filmcraft",
|
||||
"upstream": "storytold/filmcraft",
|
||||
"fork": "aphrody-labs/filmcraft",
|
||||
"branch": "main",
|
||||
"revision": "87b845138e2cecf43e97e2c2e011b820a6e70ecd",
|
||||
"upstreamRevision": "cee1feb185e675a108f78067f71ad86a640ace3d",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"kind": "library",
|
||||
"description": "Video",
|
||||
"manifestPath": "Cargo.toml",
|
||||
"binaries": ["filmcraft-cli"],
|
||||
"stars": 11833,
|
||||
"binary": "filmcraft-cli",
|
||||
"cargoPackage": "filmcraft-cli",
|
||||
"role": "engine"
|
||||
},
|
||||
{
|
||||
"id": "artcraft",
|
||||
"path": "libraries/artcraft",
|
||||
"upstream": "storytold/artcraft",
|
||||
"fork": "aphrody-labs/artcraft",
|
||||
"branch": "main",
|
||||
"revision": "e8de597d0bb13be2ed12c1085fd84dcc1c913f27",
|
||||
"revision": "f8dae6cec51cc84c8b565132db2c7471fad96148",
|
||||
"upstreamRevision": "d4e4ac69bbd93d237eaf5d0260bc6a5ff3a5e373",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"kind": "library",
|
||||
@@ -68,31 +86,13 @@
|
||||
"cargoPackage": "vectorcraft-cli",
|
||||
"role": "engine"
|
||||
},
|
||||
{
|
||||
"id": "filmcraft",
|
||||
"path": "libraries/filmcraft",
|
||||
"upstream": "storytold/filmcraft",
|
||||
"fork": "aphrody-labs/filmcraft",
|
||||
"branch": "main",
|
||||
"revision": "cee1feb185e675a108f78067f71ad86a640ace3d",
|
||||
"upstreamRevision": "cee1feb185e675a108f78067f71ad86a640ace3d",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"kind": "library",
|
||||
"description": "Video",
|
||||
"manifestPath": "Cargo.toml",
|
||||
"binaries": ["filmcraft-cli"],
|
||||
"stars": 11833,
|
||||
"binary": "filmcraft-cli",
|
||||
"cargoPackage": "filmcraft-cli",
|
||||
"role": "engine"
|
||||
},
|
||||
{
|
||||
"id": "effectcraft",
|
||||
"path": "libraries/effectcraft",
|
||||
"upstream": "storytold/effectcraft",
|
||||
"fork": "aphrody-labs/effectcraft",
|
||||
"branch": "main",
|
||||
"revision": "470ea7a3af82ee1dd1d2f86c777fe6891bd0c37b",
|
||||
"revision": "e5beea940ed245dc8e6c9293d6f7717b6c3f2724",
|
||||
"upstreamRevision": "470ea7a3af82ee1dd1d2f86c777fe6891bd0c37b",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"kind": "library",
|
||||
@@ -296,5 +296,6 @@
|
||||
"stars": 296,
|
||||
"role": "fonts"
|
||||
}
|
||||
]
|
||||
],
|
||||
"priorities": ["filmcraft"]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { join, resolve } from "node:path";
|
||||
import { loadConfig, assertRuntime, repositoryRoot } from "@aphrody/bun-workspace";
|
||||
import { capabilityFor, type Catalog } from "./shared.ts";
|
||||
import { nativeVideoCapabilities } from "@aphrody/bun-video-project/native";
|
||||
|
||||
export const desktopRoot = import.meta.dir;
|
||||
|
||||
@@ -9,10 +10,12 @@ export async function buildDesktop() {
|
||||
assertRuntime(config);
|
||||
const catalog: Catalog = {
|
||||
runtime: { version: config.runtime.version, repository: config.runtime.repository },
|
||||
video: { nativeAvailable: nativeVideoCapabilities().available },
|
||||
libraries: config.modules
|
||||
.filter(module => module.kind === "library")
|
||||
.map(module => ({
|
||||
id: module.id,
|
||||
priority: config.priorities?.includes(module.id) ?? false,
|
||||
...capabilityFor(module.id),
|
||||
license: module.license,
|
||||
initialized: false,
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { buildDesktop } from "./build.ts";
|
||||
|
||||
const output = await buildDesktop();
|
||||
process.env.APHRODY_WEB_ROOT = output + "/web";
|
||||
Bun.env.APHRODY_WEB_ROOT = output + "/web";
|
||||
await import("./server.ts");
|
||||
@@ -12,6 +12,7 @@
|
||||
"desktop:build": "bun native.ts build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aphrody/bun-video-project": "workspace:*",
|
||||
"@aphrody/bun-workspace": "workspace:*",
|
||||
"@aphrody/tauri": "file:../../toolchains/bun/packages/bun-tauri"
|
||||
},
|
||||
|
||||
@@ -2,10 +2,11 @@ import { dirname, join, resolve } from "node:path";
|
||||
import { serveForTauri } from "@aphrody/tauri/server";
|
||||
import { assertRuntime, listModuleStatus, loadConfig, runModule } from "@aphrody/bun-workspace";
|
||||
import { capabilityFor, type Catalog, type Inspection } from "./shared.ts";
|
||||
import { nativeVideoCapabilities } from "@aphrody/bun-video-project/native";
|
||||
|
||||
async function findWorkspace(): Promise<string> {
|
||||
if (process.env.APHRODY_ROOT) {
|
||||
const root = resolve(process.env.APHRODY_ROOT);
|
||||
if (Bun.env.APHRODY_ROOT) {
|
||||
const root = resolve(Bun.env.APHRODY_ROOT);
|
||||
if (!(await Bun.file(join(root, "aphrody.config.json")).exists())) {
|
||||
throw new Error("APHRODY_ROOT must contain aphrody.config.json.");
|
||||
}
|
||||
@@ -28,7 +29,7 @@ const config = await loadConfig(root);
|
||||
assertRuntime(config);
|
||||
const session = crypto.randomUUID();
|
||||
const inspections = new Map<string, Inspection>();
|
||||
const staticRoot = process.env.APHRODY_WEB_ROOT ? resolve(process.env.APHRODY_WEB_ROOT) : join(import.meta.dir, "web");
|
||||
const staticRoot = Bun.env.APHRODY_WEB_ROOT ? resolve(Bun.env.APHRODY_WEB_ROOT) : join(import.meta.dir, "web");
|
||||
const headers = {
|
||||
"Cache-Control": "no-store",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
@@ -45,12 +46,14 @@ async function catalog(): Promise<Catalog> {
|
||||
const statuses = await listModuleStatus(config, root);
|
||||
return {
|
||||
runtime: { version: config.runtime.version, repository: config.runtime.repository },
|
||||
video: { nativeAvailable: nativeVideoCapabilities().available },
|
||||
libraries: config.modules
|
||||
.filter(module => module.kind === "library")
|
||||
.map(module => {
|
||||
const status = statuses.find(item => item.id === module.id);
|
||||
return {
|
||||
id: module.id,
|
||||
priority: config.priorities?.includes(module.id) ?? false,
|
||||
...capabilityFor(module.id),
|
||||
license: module.license,
|
||||
initialized: status?.initialized ?? false,
|
||||
@@ -64,7 +67,7 @@ async function catalog(): Promise<Catalog> {
|
||||
}
|
||||
|
||||
const server = serveForTauri({
|
||||
port: process.env.PORT ? Number(process.env.PORT) : 0,
|
||||
port: Bun.env.PORT ? Number(Bun.env.PORT) : 0,
|
||||
maxRequestBodySize: 1024,
|
||||
async fetch(request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface Library {
|
||||
initialized: boolean;
|
||||
inspectable: boolean;
|
||||
inspectReason?: string;
|
||||
priority?: boolean;
|
||||
}
|
||||
|
||||
export interface Inspection {
|
||||
@@ -21,6 +22,7 @@ export interface Catalog {
|
||||
runtime: { version: string; repository: string };
|
||||
inspections: Inspection[];
|
||||
session?: string;
|
||||
video?: { nativeAvailable: boolean };
|
||||
}
|
||||
|
||||
const capabilities: Record<string, [string, string]> = {
|
||||
|
||||
+10
-9
@@ -127,6 +127,12 @@ function libraryIcon(library: Library): HTMLDivElement {
|
||||
node.append(svg(categories.find(item => item.id === category(library))!.path));
|
||||
return node;
|
||||
}
|
||||
function libraryState(library: Library): string {
|
||||
if (latest(library.id)?.state === "running") return "Inspecting";
|
||||
if (!library.initialized) return "Source unavailable";
|
||||
if (library.priority) return "Priority";
|
||||
return library.inspectable ? "CLI available" : "Library source";
|
||||
}
|
||||
function card(library: Library): HTMLElement {
|
||||
const node = document.createElement("article");
|
||||
node.className = "app-card";
|
||||
@@ -135,14 +141,7 @@ function card(library: Library): HTMLElement {
|
||||
top.append(libraryIcon(library));
|
||||
const status = document.createElement("span");
|
||||
status.className = "app-state";
|
||||
status.textContent =
|
||||
latest(library.id)?.state === "running"
|
||||
? "Inspecting"
|
||||
: !library.initialized
|
||||
? "Source unavailable"
|
||||
: library.inspectable
|
||||
? "CLI available"
|
||||
: "Library source";
|
||||
status.textContent = libraryState(library);
|
||||
top.append(status);
|
||||
const title = document.createElement("h3");
|
||||
title.textContent = library.title;
|
||||
@@ -197,7 +196,9 @@ function render() {
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = "Aphrody";
|
||||
const description = document.createElement("p");
|
||||
description.textContent = "Native libraries. Bun orchestration. One local workspace.";
|
||||
description.textContent = catalog.video?.nativeAvailable
|
||||
? "Video first. Native media and exact timelines."
|
||||
: "Video first. Native Bun video is not available in this binary.";
|
||||
copy.append(title, description);
|
||||
const mark = document.createElement("div");
|
||||
mark.className = "featured-mark";
|
||||
|
||||
@@ -6,19 +6,27 @@
|
||||
"name": "@aphrody/craft",
|
||||
"devDependencies": {
|
||||
"@aphrody/bun-types": "file:toolchains/bun/packages/bun-types",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
||||
"oxfmt": "0.72.0",
|
||||
"oxlint": "1.87.0",
|
||||
},
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "@aphrody/bun-desktop",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@aphrody/bun-video-project": "workspace:*",
|
||||
"@aphrody/bun-workspace": "workspace:*",
|
||||
"@aphrody/tauri": "file:../../toolchains/bun/packages/bun-tauri",
|
||||
},
|
||||
},
|
||||
"packages/video": {
|
||||
"name": "@aphrody/bun-video-project",
|
||||
"version": "0.1.0",
|
||||
"bin": {
|
||||
"aphrody-video": "./src/cli.ts",
|
||||
},
|
||||
"dependencies": {
|
||||
"@aphrody/bun-workspace": "workspace:*",
|
||||
},
|
||||
},
|
||||
"packages/workspace": {
|
||||
"name": "@aphrody/bun-workspace",
|
||||
"version": "0.1.0",
|
||||
@@ -32,108 +40,12 @@
|
||||
|
||||
"@aphrody/bun-types": ["@aphrody/bun-types@file:toolchains/bun/packages/bun-types", { "dependencies": { "@types/node": "*", "undici-types": "*" } }],
|
||||
|
||||
"@aphrody/bun-video-project": ["@aphrody/bun-video-project@workspace:packages/video"],
|
||||
|
||||
"@aphrody/bun-workspace": ["@aphrody/bun-workspace@workspace:packages/workspace"],
|
||||
|
||||
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.72.0", "", { "os": "android", "cpu": "arm" }, "sha512-u4U1uaDOVBuorGNC/07NLm48nECHOiJJka002YGML16Md2DRvf38RGBt01lH60EOGvNlFVmbLoksQedNBoZHKw=="],
|
||||
|
||||
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.72.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GCnM+Ae2mWOKC+92y3FOb6eZJeVnBAVz82e3lITUUg+kxukYKzBNAvJ/hhHIfa2A0md55SYYhdWPddSSy3KPVA=="],
|
||||
|
||||
"@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.72.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LjQ8tlevdtwraFlWC6AnQ1Forzde0clRmnsP3QmK+ydSElDt/J+5Jc+m+//Ly1G97aP7WCNUw110QMIUw2K7xA=="],
|
||||
|
||||
"@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.72.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+ZbcxhB1C34RQR5IldSyY6Z/yalJIxHXhP36PGE4sEq0f/lgL5jCLmOKoDeVm8dqd9gFGDsd1bNWHSGCAwdSkw=="],
|
||||
|
||||
"@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.72.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yGPhA8sePEBFV9iGcprhyOk3Bz3fZshswMH3DpYIPZFLT1DG5LjiZ0j6AJQpSueaL6bUK0av1K+5+fe+ldy5EQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.72.0", "", { "os": "linux", "cpu": "arm" }, "sha512-/g5axuqFMcAtoQrFLR5EJEMDjR5Ea4GuzpdDmMpPWAgrF1DmL4kD1NPNVlpXEOxpFJnD6lPvLKYj1lzdlrfifA=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.72.0", "", { "os": "linux", "cpu": "arm" }, "sha512-InVitdHbS09PM+w+WinIiyX37rC9R3L7lUP9mKy9uKTEDSUFaLjWsLoreRIxKivnv7Ndqc6Trvi+K+b6ZzhiJg=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.72.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKjlyRAxpPjXoqeuwFBim5LscgvY+RNEiZ971C9JlRQZONhJTsXDki/oHFYQE+fzzRpzXh3owZvOdT3kNL2g+g=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.72.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-poafnbsPpJhR+5Mbwpf8CscugTyiRAzjTOxRVn8ra3X2jnURs2xL5Du2xl5g2iW85lICMglpmLf8jDjZEbNgzQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.72.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zNUi0gKRsO7S3IfPzsTbnm88OedPuX32hmZFWZRWHvWJlAoZRwVbeO6Nb3hng4vDgL7qUKp4TGqzPz1mhsycYg=="],
|
||||
|
||||
"@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.72.0", "", { "os": "linux", "cpu": "none" }, "sha512-qTY7z/iyF/rWI2HWwWMevXqZW5PZUOofUy2afK7v6rseXnpld3yjgyIk6pCux3DqWQ0lB+f5atvLJNBwzLvFsw=="],
|
||||
|
||||
"@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.72.0", "", { "os": "linux", "cpu": "none" }, "sha512-w18qfo8vEMD6EKEHtzTvjc6jH8yCyXIT1Bn7PFWJd0PpZyzH/VQDx4xvjCh0LGz6GRzqiezbisTdG9AoXtpkZQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.72.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-grW8HwEir+vK7fCcq1ZgL3NHQCfZ1PidiERE9Hbf+24xGys+DoDaoqTht4ccfn0PcBwbViLkwkda/KNpJu2KRQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.72.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wQJRQfWBRIn88Pkr449EUViwRayrYUScg7ty+Ihiem21mQ9BUA6S9e/4RAgyNk9lMag0KBouyKskjq61rOC0WQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.72.0", "", { "os": "linux", "cpu": "x64" }, "sha512-T1B0uQzafxuGPjjaPrZJ497nxoi1BiI65JfsYc56nNpmev3msfb/iaTmH2kQuNFqa8PKxpMlf3GhWrWBvBPORQ=="],
|
||||
|
||||
"@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.72.0", "", { "os": "none", "cpu": "arm64" }, "sha512-OKxxgTurl7+hypVplO8QRAbbQlwEs0j1Y6FZv+ZN2qNbdxQk2muMlzuJNBGTCWwxQ0rkkOD1uK/DW1fvzoUSJA=="],
|
||||
|
||||
"@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.72.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-L78TKzURqvjuZOCo88ixMbPXf/RP7u31Y0cmjJ/KEmrHmfYhmhEvsDaTYPiyTRne64jUgCbADfY0YeoQk4/Xyw=="],
|
||||
|
||||
"@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.72.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-gQtFW+Ii8BWgtJTAD17TY/WsdNS5gUfAPov8iI1jeq4r9lRRYImmxPL08UU00/RLaCfBJwh2UgINnDY0iOiZ+A=="],
|
||||
|
||||
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.72.0", "", { "os": "win32", "cpu": "x64" }, "sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw=="],
|
||||
|
||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.87.0", "", { "os": "android", "cpu": "arm" }, "sha512-22tVIHlB9898OYsBKmYB6He1MCSqdTmK1BVWuSNUTVrpS0KWNege+U9uEYqGJ+KatQag0r/oU44i3OOs46z7HQ=="],
|
||||
|
||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.87.0", "", { "os": "android", "cpu": "arm64" }, "sha512-JfZQ3MWf74n8ov0DojZpkOduIubYJvZL5tbTMMhDutsal6u1xH1QuNpN+lK+EQwF8AeAfgwTR2fgh5U2br27XA=="],
|
||||
|
||||
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.87.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rkx6Xvvw93z2GImVOyvbZwAZcoY0Z6MJW177XNsSB+i/CBsH7ANyelTp01VZyIkM3iSXtqKVb4+ptBgPHap5pQ=="],
|
||||
|
||||
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.87.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cw5nYw7L/pQ2SgGBgOcg/Qo89llQre3vIc/kuBLhSICzu1r5s8L76H3ug6AIFsD98WpROj8/z5JVzkf4daxHaQ=="],
|
||||
|
||||
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.87.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-x/SuznE6loRZK9gz9AS4ZmzNzSu+5KdeoCS+sVHhnrE8HM6AD8UyTjwldQTTqlXGvkwHASgB0NiTNOc3o3PdAg=="],
|
||||
|
||||
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.87.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+JEDmCAXiiFmKhAjZhgAOwaHWY6WENOQ5QA+lFuY59tgsUCDOTRi46B+lENK6CWvtxsXpwkZXNOgpjAQcpIpnA=="],
|
||||
|
||||
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.87.0", "", { "os": "linux", "cpu": "arm" }, "sha512-/uC9XqXLAC9AHFLNycwmSQEvKqGrOCgU+uXALI7gNW0DG81TJ/3T2eHnKdRvcb5R1gFEfjxDwI4e4zIa29rICQ=="],
|
||||
|
||||
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.87.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-FTRYPsBy/M728vnUv7l+KAwSAa3Qi4BQbhVvy7NYuMTZ0sUa/OWsbMx79HVFw6u/5KWcaYayDsoOiv7xbevzDg=="],
|
||||
|
||||
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.87.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-6sC+N7IY0h1egd8SrYUGNN7CUyKJZ4bIZakKa7V7hfinf4jfi8Kq4+klJvFp5DqXcwMn44I5MeBwmJluO1odFg=="],
|
||||
|
||||
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.87.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-d5Mnn+ICbhoJPi37Ccjy+86T5qfH2+mSTrTMBXNPfB+wqSWOayBBLLIrCXdUy1YssluCVdGkl+DO9w5r5ODmUA=="],
|
||||
|
||||
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.87.0", "", { "os": "linux", "cpu": "none" }, "sha512-sf5YWuUo1Dx9loqWCgF8P278MycfcD4JHjDC/LV5j11L3Lqa4PFiwCjD6B8kP4AjsgZZkLn3xbpJEtZBaccs4Q=="],
|
||||
|
||||
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.87.0", "", { "os": "linux", "cpu": "none" }, "sha512-+aCraYbmK3uU3P0rFojjeaK9pGY6JvBDXh/0CM+gPyWKmNmXmnFe0qj4qj02U4ibFpbd9Pa9CmDvIdjJUd29rA=="],
|
||||
|
||||
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.87.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-7rzbZQIljo6VLWVPqUyLa2P/WV3VFsJB4M52R4NCUJn1A50yjtUsHxiMHFL9opEKGFIiTbZ3VQzmtcJOPUq4NQ=="],
|
||||
|
||||
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.87.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dry46Fa+AXE/xt9J/YriujU1pU9mzfNWHGQwtOYibAFH8HLrKWCi93AhIGnuUMiwn2JbuBkRyz51f50Ly4r4ow=="],
|
||||
|
||||
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.87.0", "", { "os": "linux", "cpu": "x64" }, "sha512-f/HMZez74gJvnRpGz+b5sdGyWrEgRLmFTXg9sNoqjPWogGxYHUhXKTKyMiYsu9rUGR2ciYqL3fk+Ua4JhMJbRA=="],
|
||||
|
||||
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.87.0", "", { "os": "none", "cpu": "arm64" }, "sha512-lEC6ZfPZm/07JlB7FsOWX9o1RiHlXiBW594LSUGuXmekC8LZmvUPP9XzE2WZcNxJi5cHA/Fw56o02c3JxgMA5g=="],
|
||||
|
||||
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.87.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-afBWKOD36dUiA9AenaiWpCg8xfyUVPYznyAAVXu413brK4RA5UI/Xoc4yfeGIT8G1a44MV+JR7hioryerBDzGQ=="],
|
||||
|
||||
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.87.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-daptNsEOuru4/nJTxV4DBHNJ5GepzmaBVQf+jjgKQGi9NRlZKik+RFq4hUL7ewsF1pwjKM+SkYaMbF82UwFGnw=="],
|
||||
|
||||
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.87.0", "", { "os": "win32", "cpu": "x64" }, "sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw=="],
|
||||
|
||||
"@types/node": ["@types/node@26.6.5", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="],
|
||||
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260707.2", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260707.2", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260707.2" }, "bin": { "tsgo": "bin/tsgo" } }, "sha512-oUGp+Rep/hqMhPunyinsALUwSlzHINSxitifPiSaeqoKOKD2OlR9NE3TaPqwsl4NlGslsOSUXI1JotWQzpYCPg=="],
|
||||
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wny2pgKjGbiZtnOIHVa3tXC1UfDqxNEFzyPGmiqybedG8hipG2Nfp0l5UxbaKCjkLacUpH/W5bP2hBOMVhCOzg=="],
|
||||
|
||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Afc7M5zOwo+GpfcYwz5Z8HMB2tPVsui7nNIqEuuFB73MPdVqNn/Wmpe4tP4MRri0AtJnJknoHBaTJ/VDAp/Jhw=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "arm" }, "sha512-hJm/UOqZTr9FHmR7uNm8VGX4oKtfWk0Jem0zPeJFNC8ckGUfSBueyiEYMZB+XmRc1aG4x1E46y3CplP4CLHvGQ=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-iITBa2WjjTI5N9t5l7Z4KoOSI+2zBlhbvFzsD/f8qX8QoKjz/Y4DPyBDgezYi8nkqjjksbgSOJ3/ykzhwrB9cg=="],
|
||||
|
||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "x64" }, "sha512-du0dzi6y97Po5vDNdPJTyyijHCpaS22JLRnKZEJXBDaO9gCIymOv/5QQokFRuOlQm0bWl3i9PF4OVdGP6uAOQA=="],
|
||||
|
||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SsAwfhyHJ1akgBc+99z4+hwdbHsdWaKB8EwCNIMA6JfSLMeUjffrYvxu+vfMyxVtOVOz7RrRXRoiDiu4a2sCtg=="],
|
||||
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2", "", { "os": "win32", "cpu": "x64" }, "sha512-DL4u27stv0fo71sVhOzHSwE+YMZsbBijVI+kg5dLDLilSH79WFTJ8RSQ46vJrCMt+Gjlv/JOZP1PuLJDfioYeQ=="],
|
||||
|
||||
"oxfmt": ["oxfmt@0.72.0", "", { "dependencies": { "tinypool": "2.2.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.72.0", "@oxfmt/binding-android-arm64": "0.72.0", "@oxfmt/binding-darwin-arm64": "0.72.0", "@oxfmt/binding-darwin-x64": "0.72.0", "@oxfmt/binding-freebsd-x64": "0.72.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.72.0", "@oxfmt/binding-linux-arm-musleabihf": "0.72.0", "@oxfmt/binding-linux-arm64-gnu": "0.72.0", "@oxfmt/binding-linux-arm64-musl": "0.72.0", "@oxfmt/binding-linux-ppc64-gnu": "0.72.0", "@oxfmt/binding-linux-riscv64-gnu": "0.72.0", "@oxfmt/binding-linux-riscv64-musl": "0.72.0", "@oxfmt/binding-linux-s390x-gnu": "0.72.0", "@oxfmt/binding-linux-x64-gnu": "0.72.0", "@oxfmt/binding-linux-x64-musl": "0.72.0", "@oxfmt/binding-openharmony-arm64": "0.72.0", "@oxfmt/binding-win32-arm64-msvc": "0.72.0", "@oxfmt/binding-win32-ia32-msvc": "0.72.0", "@oxfmt/binding-win32-x64-msvc": "0.72.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-16OQeL0uhPZbfmT9S9WWM0Nv9+BmVi8Ra1uTYaSKj/qSCs6FB30KOhz9JoNYw6thT+K5bVxsk/tSEOu497i34g=="],
|
||||
|
||||
"oxlint": ["oxlint@1.87.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.87.0", "@oxlint/binding-android-arm64": "1.87.0", "@oxlint/binding-darwin-arm64": "1.87.0", "@oxlint/binding-darwin-x64": "1.87.0", "@oxlint/binding-freebsd-x64": "1.87.0", "@oxlint/binding-linux-arm-gnueabihf": "1.87.0", "@oxlint/binding-linux-arm-musleabihf": "1.87.0", "@oxlint/binding-linux-arm64-gnu": "1.87.0", "@oxlint/binding-linux-arm64-musl": "1.87.0", "@oxlint/binding-linux-ppc64-gnu": "1.87.0", "@oxlint/binding-linux-riscv64-gnu": "1.87.0", "@oxlint/binding-linux-riscv64-musl": "1.87.0", "@oxlint/binding-linux-s390x-gnu": "1.87.0", "@oxlint/binding-linux-x64-gnu": "1.87.0", "@oxlint/binding-linux-x64-musl": "1.87.0", "@oxlint/binding-openharmony-arm64": "1.87.0", "@oxlint/binding-win32-arm64-msvc": "1.87.0", "@oxlint/binding-win32-ia32-msvc": "1.87.0", "@oxlint/binding-win32-x64-msvc": "1.87.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2003", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg=="],
|
||||
|
||||
"tinypool": ["tinypool@2.2.0", "", {}, "sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ=="],
|
||||
|
||||
"undici-types": ["undici-types@8.11.2", "", {}, "sha512-iMVNmWZ0leK/goS6eXMizSzmm9CDWtyphwbaCms3DNLqRxDL+mMoNVcZMTyyVgXP0N+Z8neAMzDoUOUJL8veKg=="],
|
||||
|
||||
"@aphrody/bun-desktop/@aphrody/tauri": ["@aphrody/tauri@file:toolchains/bun/packages/bun-tauri", {}],
|
||||
|
||||
File renamed without changes.
@@ -6,12 +6,16 @@ flowchart TD
|
||||
UI --> Backend[Bun backend]
|
||||
Backend --> Workspace[Native library orchestration]
|
||||
Workspace --> Sources[Pinned library submodules]
|
||||
Backend --> Video[Typed video adapter]
|
||||
Video --> Headless[Native video CLI]
|
||||
Video --> NativeVideo[Bun.Video when available]
|
||||
Sources --> Rust[Existing Rust engines and headless CLIs]
|
||||
Sources --> Shared[Shared libraries and fonts]
|
||||
```
|
||||
|
||||
- `apps/desktop`: Aphrody shell, built and served by Bun.
|
||||
- `packages/workspace`: config, provenance, source state and native execution.
|
||||
- `packages/video`: exact JSON, bounded native operations and runtime availability.
|
||||
- `libraries/*`: 16 independent open source forks.
|
||||
- `toolchains/bun`: pinned SDK and runtime types.
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Native Bun migration
|
||||
|
||||
Quality commands are native `bun check`, `bun lint` and `bun fmt`. Root tooling
|
||||
uses the pinned fork and bunfmt.json. The native checker covers 41 authored
|
||||
files; the imported frontend covers 1,479 files.
|
||||
|
||||
Aggressive n2b ran across the 16 library inputs and the SDK with two workers.
|
||||
Rewrites were reviewed. Browser and embedded host scripts retain their runtime;
|
||||
native Buffer compatibility remains where a replacement cannot preserve its
|
||||
contract. Unsupported mock helpers and invalid callback rewrites were corrected.
|
||||
|
||||
The internal Rust owner is
|
||||
[packages/bun-n2b](https://github.com/aphrody-labs/bun/tree/176fa03201a78cd9f4ac6e839294d81ef4852435/packages/bun-n2b).
|
||||
The patch adds explicit submodule traversal, bounded scan/report retention,
|
||||
accurate clean-file counts, nested package resolution and AST rewrite guards.
|
||||
Directory pruning avoids repeated ancestor checks. Limit failures explicitly
|
||||
report an incomplete scan.
|
||||
|
||||
The native Cargo check passed for the core, CLI and addon on Windows. Rust 1.97
|
||||
remains the declared minimum; the gate used the installed Rust 1.99 toolchain.
|
||||
The installed n2b addon remains 0.7.0. Its fresh artifact has not been built or
|
||||
installed, so new scanner flags are source-qualified only.
|
||||
+1
-1
@@ -34,6 +34,6 @@ source NOTICE, which identifies restricted assets.
|
||||
|
||||
Our runtime is Bun 1.4.4 at `2ae8fc65fba1d05c423a2433b1195835109942b6`.
|
||||
Its declared sourceUrl belongs to our fork; this does not verify an archive hash.
|
||||
The SDK uses published commit `c209b95eb0dcc964745f4684f127abb83cbe2697`.
|
||||
The SDK uses published commit `939c835adf6479bc1014da3843cda4ae29851dbd`.
|
||||
It contains our Tauri v3 alpha.4 fork, versioned 1.4.4. The npm SDK was unavailable,
|
||||
so JS and Rust resolve the pinned source. Native CLI: `aphrody tauri`.
|
||||
+11
-6
@@ -2,23 +2,28 @@
|
||||
|
||||
Native Windows, 2026-10-13. Bun 1.4.4 fork revision
|
||||
`2ae8fc65fba1d05c423a2433b1195835109942b6`; SDK commit
|
||||
`c209b95eb0dcc964745f4684f127abb83cbe2697`.
|
||||
`939c835adf6479bc1014da3843cda4ae29851dbd`.
|
||||
|
||||
Passed:
|
||||
|
||||
- Fork creation and Git submodule initialization: 16 library sources plus SDK.
|
||||
- Runtime version, revision, provenance and external Bun resolution.
|
||||
- TypeScript native compiler, oxlint and oxfmt for authored workspace/desktop code.
|
||||
- Native Bun check, lint and fmt for authored workspace, video and desktop code.
|
||||
- Bun frontend/backend build: 16 capabilities in the Aphrody shell.
|
||||
- Imported React frontend build: 33 artifacts using Bun, PostCSS and existing assets.
|
||||
- Frozen Bun installs and source diff checks.
|
||||
- Offline native Cargo metadata and lockfile generation: 526 packages.
|
||||
- Rust source formatting and native wrapper output forwarding.
|
||||
- Native video CLI release build: locked dependencies, one Cargo job, Windows MSVC.
|
||||
- Video doctor, command discovery and description through the bounded adapter.
|
||||
- Aggressive n2b across all 16 library inputs and the SDK, with reviewed rewrites.
|
||||
- Imported frontend native Bun check: 1,479 files; strict lint and frozen install.
|
||||
- Internal Rust n2b core, CLI and addon Cargo check on Windows; see [migration](migration.md).
|
||||
|
||||
Not qualified: Rust engine compilation, headless CLI execution, native CEF
|
||||
windows, installers, provider access, signing and CI execution. No tests were
|
||||
added or run. Existing upstream launcher tests still expect Vite/HMR; their
|
||||
test configurations retain required dependencies.
|
||||
Not qualified: other Rust engines, media decoding, rendering, export accuracy,
|
||||
native Bun.Video, CEF windows, installers, provider access, signing and CI
|
||||
execution. No tests were added or run. Upstream launcher specifications retain
|
||||
their original frontend expectations.
|
||||
|
||||
The source migration covers JavaScript build and development tooling. Rust
|
||||
engines remain native libraries. The imported historical shell remains Tauri v2;
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ licenses and owner conventions. These references govern authored code.
|
||||
| Sources/builds | Independent dependencies, exact revisions, preserved notices and non-interactive builds | [Debian Policy](https://www.debian.org/doc/debian-policy/ch-source.html) |
|
||||
|
||||
Use guard clauses and shared cleanup. Comments explain ownership constraints.
|
||||
.editorconfig, .oxfmtrc.json and rustfmt enforce formatting.
|
||||
.editorconfig, bunfmt.json and rustfmt enforce formatting. JavaScript gates use
|
||||
`bun check`, `bun lint` and `bun fmt` through the native runtime.
|
||||
|
||||
Reviewed through `aphrody docs`, `aphrody awesome` and scoped remote
|
||||
`aphrody git ingest`. Local ingestion is owned by Bun MCP in this runtime.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Bun video integration
|
||||
|
||||
Video is the current priority. The source library is pinned to FilmCraft
|
||||
`87b845138e2cecf43e97e2c2e011b820a6e70ecd` (0.7.2, MIT OR Apache-2.0).
|
||||
Its Rust workspace remains independent.
|
||||
|
||||
The fork adds Bun-native JavaScript helpers. Rust and Cargo remain identical to
|
||||
the upstream baseline `cee1feb185e675a108f78067f71ad86a640ace3d`, also used by
|
||||
the parallel native codec owner.
|
||||
|
||||
## Owners
|
||||
|
||||
| Capability | Owner |
|
||||
| --- | --- |
|
||||
| Project loading, schemas 1–15 and atomic saves | filmcraft-format |
|
||||
| Exact time and rational frame rates | filmcraft-time |
|
||||
| Container probing and codecs | filmcraft-codecs / filmcraft-media |
|
||||
| Editing and command validation | filmcraft-engine |
|
||||
| Rendering and export | filmcraft-render / filmcraft-export |
|
||||
| Native Bun metadata and frame API | Bun.Video |
|
||||
| Typed process boundary | @aphrody/bun-video-project |
|
||||
|
||||
The adapter calls the existing headless CLI with structured arguments and JSON.
|
||||
It adds output limits, cancellation and typed operations. Rust retains project
|
||||
migrations, editing rules, codecs and export. No FFmpeg subprocess or copied
|
||||
decoder is introduced.
|
||||
|
||||
Failed native operations retain structured results and failure status. Export
|
||||
uses a new destination directory to isolate filenames derived by native formats.
|
||||
|
||||
Tick values use 254,016,000,000 units per second. Ticks and IDs span i64/u64;
|
||||
the JSON boundary preserves large integers as bigint. JSONL serialization emits
|
||||
their exact decimal representation rather than rounded Numbers.
|
||||
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
bun run video doctor
|
||||
bun run video:build
|
||||
bun run video commands
|
||||
bun run video probe /path/to/media.mp4
|
||||
bun run video inspect /path/to/project.fcproj sequence
|
||||
```
|
||||
|
||||
Use `.local/config.json` to select an existing `filmcraft-cli` binary, or build
|
||||
the pinned library through the native workspace owner. API calls do not build
|
||||
the engine implicitly. Save destinations are explicit; protected provider
|
||||
stores, captures and datasets are outside this integration.
|
||||
|
||||
The parallel Bun session owns native codec internalization. The installed
|
||||
`1.4.4` runtime at `2ae8fc65fba1d05c423a2433b1195835109942b6` currently has no
|
||||
Bun.Video API. The adapter reports this and does not substitute an unqualified
|
||||
provider. Native availability alone does not qualify codecs, video encoding or
|
||||
frame accuracy. See [qualification](qualification.md).
|
||||
+1
-1
Submodule libraries/artcraft updated: e8de597d0b...f8dae6cec5.
+1
-1
Submodule libraries/effectcraft updated: 470ea7a3af...e5beea940e.
+1
-1
Submodule libraries/filmcraft updated: cee1feb185...87b845138e.
+10
-9
@@ -5,7 +5,8 @@
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/workspace",
|
||||
"apps/desktop"
|
||||
"apps/desktop",
|
||||
"packages/video"
|
||||
],
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -18,19 +19,19 @@
|
||||
"build": "bun run --cwd apps/desktop build",
|
||||
"desktop:dev": "bun run --cwd apps/desktop desktop:dev",
|
||||
"desktop:build": "bun run --cwd apps/desktop desktop:build",
|
||||
"typecheck": "bun run --bun node_modules/@typescript/native-preview/bin/tsgo --noEmit -p tsconfig.json",
|
||||
"lint": "bun run --bun node_modules/oxlint/bin/oxlint packages/workspace/src apps/desktop scripts",
|
||||
"format:check": "bun run --bun node_modules/oxfmt/bin/oxfmt --check packages/workspace/src apps/desktop scripts package.json aphrody.config.json tsconfig.json",
|
||||
"typecheck": "bun check -p tsconfig.json --threads 2 --no-pretty",
|
||||
"lint": "bun lint --deny-warnings packages/workspace/src packages/video/src apps/desktop scripts",
|
||||
"format:check": "bun fmt --config bunfmt.json --check packages/workspace/src packages/video/src apps/desktop/build.ts apps/desktop/dev.ts apps/desktop/native.ts apps/desktop/server.ts apps/desktop/shared.ts apps/desktop/src/app.ts apps/desktop/package.json scripts/quality.ts package.json aphrody.config.json tsconfig.json bunfmt.json",
|
||||
"check": "bun scripts/quality.ts",
|
||||
"workspace": "bun packages/workspace/src/cli.ts",
|
||||
"frontend:dev": "bun run --cwd libraries/artcraft/frontend dev",
|
||||
"frontend:build": "bun run --cwd libraries/artcraft/frontend build"
|
||||
"frontend:build": "bun run --cwd libraries/artcraft/frontend build",
|
||||
"video": "bun packages/video/src/cli.ts",
|
||||
"video:build": "bun packages/workspace/src/cli.ts build filmcraft",
|
||||
"fmt": "bun fmt --config bunfmt.json --write packages/workspace/src packages/video/src apps/desktop/build.ts apps/desktop/dev.ts apps/desktop/native.ts apps/desktop/server.ts apps/desktop/shared.ts apps/desktop/src/app.ts apps/desktop/package.json scripts/quality.ts package.json aphrody.config.json tsconfig.json bunfmt.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aphrody/bun-types": "file:toolchains/bun/packages/bun-types",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
||||
"oxfmt": "0.72.0",
|
||||
"oxlint": "1.87.0"
|
||||
"@aphrody/bun-types": "file:toolchains/bun/packages/bun-types"
|
||||
},
|
||||
"engines": {
|
||||
"bun": "=1.4.4"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Aphrody / Bun Video
|
||||
|
||||
`@aphrody/bun-video-project` calls the pinned native video library through
|
||||
structured arguments and exact JSON. Rust owns project loading, migrations,
|
||||
editing, codecs, rendering and export.
|
||||
|
||||
```ts
|
||||
import { createVideoLibrary, ticksFromDecimalSeconds } from "@aphrody/bun-video-project";
|
||||
|
||||
const controller = new AbortController();
|
||||
const video = await createVideoLibrary({ signal: controller.signal });
|
||||
const media = await video.probe("media.mp4");
|
||||
const sequence = await video.inspect("cut.fcproj");
|
||||
const time = ticksFromDecimalSeconds("0.1");
|
||||
await video.run("cut.fcproj", [{ id: "timeline.razor", params: { time } }], { saveAs: "edited.fcproj" });
|
||||
```
|
||||
|
||||
APIs: `commands`, `describe`, `inspect`, `probe`, `run`, `render`, `export`.
|
||||
`inspect` defaults to the active sequence. CLI syntax is available through
|
||||
`bun run video help`. Each call opens its own native session.
|
||||
|
||||
Resolution uses `binary`, then `.local/config.json`, then Cargo's offline
|
||||
metadata and an existing release/debug artifact. Paths come from the selected
|
||||
checkout and its configuration. Source pins and native version are checked.
|
||||
API calls never compile the engine. Build it with
|
||||
`bun run workspace build filmcraft`.
|
||||
|
||||
Defaults: 30 seconds, 16 MiB combined stdout/stderr. Options allow at most one
|
||||
hour and 64 MiB; cancellation terminates the owned process tree. Native stderr
|
||||
is discarded. Errors expose a stable code, optional exit status and structured
|
||||
native results. Failed exports retain their receipt and offline media entries;
|
||||
failed command batches retain their completed outcomes. The CLI prints these
|
||||
results while keeping a failure exit status. `run`
|
||||
accepts at most 10000 operations and 16 MiB of JSONL. Project persistence uses
|
||||
an explicit `saveAs`. Render rejects existing aliases to the source project.
|
||||
Export requires a new destination directory inside an existing parent: native
|
||||
formats can derive additional filenames, and that directory isolates all of them.
|
||||
|
||||
Ticks use 254016000000 units/second. Rust integer JSON tokens outside the safe
|
||||
Number range become `bigint`; `stringifyNativeJSON` emits exact integer tokens.
|
||||
Floating point values retain the native float contract. Decimal time input
|
||||
accepts at most 12 fractional digits and rejects values between ticks.
|
||||
`ticksToDecimalSeconds` truncates toward zero at the requested precision.
|
||||
Native render/export seconds use the engine's floating point boundary.
|
||||
|
||||
Export returns native receipts and offline media entries; `allowOffline` is
|
||||
explicit. Render writes PNG and returns `{ output }`. Project migrations and
|
||||
schemas 1–15 remain in the native library. Resources default to
|
||||
`.local/video/presets`; provider stores are outside this package.
|
||||
|
||||
Library input: `storytold/filmcraft`, commit
|
||||
`87b845138e2cecf43e97e2c2e011b820a6e70ecd`, version 0.7.2,
|
||||
MIT OR Apache-2.0. The input is an open source library. `Bun.Video` is exposed
|
||||
separately through `./native`; its availability does not qualify codecs or
|
||||
encoding. No FFmpeg subprocess or copied decoder is introduced.
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@aphrody/bun-video-project",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"description": "Aphrody / Bun native video project bridge",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./native": "./src/native.ts"
|
||||
},
|
||||
"bin": {
|
||||
"aphrody-video": "./src/cli.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aphrody/bun-workspace": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"bun": "=1.4.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env bun
|
||||
import {
|
||||
createVideoLibrary,
|
||||
stringifyNativeJSON,
|
||||
VideoLibraryError,
|
||||
videoLibraryDoctor,
|
||||
type VideoLibraryOptions,
|
||||
} from "./index.ts";
|
||||
|
||||
const help = `Aphrody / Bun Video
|
||||
|
||||
aphrody-video doctor
|
||||
aphrody-video commands [filter]
|
||||
aphrody-video describe <id>
|
||||
aphrody-video inspect <project> [project|sequence]
|
||||
aphrody-video probe <media>
|
||||
aphrody-video render <project> <output.png> --seconds N [--scale N]
|
||||
aphrody-video export <project> <output> [--format F] [--start N] [--end N] [--allow-offline]
|
||||
|
||||
Options: --binary PATH, --data-dir PATH, --timeout-ms N, --max-output-bytes N.
|
||||
Output is exact JSON. Native artifacts are built through the workspace owner.`;
|
||||
|
||||
const valued = new Set([
|
||||
"--binary",
|
||||
"--data-dir",
|
||||
"--timeout-ms",
|
||||
"--max-output-bytes",
|
||||
"--seconds",
|
||||
"--scale",
|
||||
"--format",
|
||||
"--start",
|
||||
"--end",
|
||||
]);
|
||||
const flags = new Set(["--json", "--allow-offline"]);
|
||||
const globalOptions = ["--binary", "--data-dir", "--timeout-ms", "--max-output-bytes", "--json"];
|
||||
|
||||
interface Arguments {
|
||||
positional: string[];
|
||||
values: Map<string, string | true>;
|
||||
}
|
||||
|
||||
function usage(): never {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Invalid video arguments. Run aphrody-video help.");
|
||||
}
|
||||
|
||||
function parse(args: string[]): Arguments {
|
||||
const positional: string[] = [];
|
||||
const values = new Map<string, string | true>();
|
||||
for (let index = 0; index < args.length; index++) {
|
||||
const entry = args[index]!;
|
||||
if (entry === "--") {
|
||||
positional.push(...args.slice(index + 1));
|
||||
break;
|
||||
}
|
||||
if (!entry.startsWith("--")) {
|
||||
positional.push(entry);
|
||||
continue;
|
||||
}
|
||||
const equal = entry.indexOf("=");
|
||||
const name = equal < 0 ? entry : entry.slice(0, equal);
|
||||
if (values.has(name)) usage();
|
||||
if (flags.has(name)) {
|
||||
if (equal >= 0) usage();
|
||||
values.set(name, true);
|
||||
} else if (valued.has(name)) {
|
||||
const value = equal < 0 ? args[++index] : entry.slice(equal + 1);
|
||||
if (value === undefined || !value || value.includes("\0")) usage();
|
||||
values.set(name, value);
|
||||
} else {
|
||||
usage();
|
||||
}
|
||||
}
|
||||
return { positional, values };
|
||||
}
|
||||
|
||||
function stringOption(args: Arguments, key: string): string | undefined {
|
||||
const value = args.values.get(key);
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function integerOption(args: Arguments, key: string): number | undefined {
|
||||
const value = stringOption(args, key);
|
||||
if (value === undefined) return;
|
||||
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value))) usage();
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
function allowed(args: Arguments, extra: string[], minimum: number, maximum = minimum): void {
|
||||
const options = new Set([...globalOptions, ...extra]);
|
||||
if (
|
||||
args.positional.length < minimum ||
|
||||
args.positional.length > maximum ||
|
||||
[...args.values.keys()].some(key => !options.has(key))
|
||||
)
|
||||
usage();
|
||||
}
|
||||
|
||||
async function main(): Promise<number> {
|
||||
const [action = "help", ...rest] = Bun.argv.slice(2);
|
||||
if (["help", "--help", "-h"].includes(action)) {
|
||||
if (rest.length) usage();
|
||||
console.log(help);
|
||||
return 0;
|
||||
}
|
||||
const args = parse(rest);
|
||||
const options: VideoLibraryOptions = {
|
||||
binary: stringOption(args, "--binary"),
|
||||
dataDir: stringOption(args, "--data-dir"),
|
||||
timeoutMs: integerOption(args, "--timeout-ms"),
|
||||
maxOutputBytes: integerOption(args, "--max-output-bytes"),
|
||||
};
|
||||
if (action === "doctor") {
|
||||
allowed(args, [], 0);
|
||||
const report = await videoLibraryDoctor(options);
|
||||
console.log(stringifyNativeJSON(report, 2));
|
||||
return report.engine.available &&
|
||||
report.runtime.recognizedFork &&
|
||||
report.runtime.versionMatches &&
|
||||
report.runtime.revisionMatches &&
|
||||
report.source.initialized &&
|
||||
report.source.pointerMatches &&
|
||||
report.source.revisionMatches
|
||||
? 0
|
||||
: 1;
|
||||
}
|
||||
switch (action) {
|
||||
case "commands":
|
||||
allowed(args, [], 0, 1);
|
||||
break;
|
||||
case "describe":
|
||||
allowed(args, [], 1);
|
||||
break;
|
||||
case "inspect":
|
||||
allowed(args, [], 1, 2);
|
||||
break;
|
||||
case "probe":
|
||||
allowed(args, [], 1);
|
||||
break;
|
||||
case "render":
|
||||
allowed(args, ["--seconds", "--scale"], 2);
|
||||
break;
|
||||
case "export":
|
||||
allowed(args, ["--format", "--start", "--end", "--allow-offline"], 2);
|
||||
break;
|
||||
default:
|
||||
usage();
|
||||
}
|
||||
const library = await createVideoLibrary(options);
|
||||
let result: unknown;
|
||||
switch (action) {
|
||||
case "commands":
|
||||
result = await library.commands(args.positional[0]);
|
||||
break;
|
||||
case "describe":
|
||||
result = await library.describe(args.positional[0]!);
|
||||
break;
|
||||
case "inspect": {
|
||||
const kind = args.positional[1] ?? "sequence";
|
||||
if (kind === "project") result = await library.inspect(args.positional[0]!, "project");
|
||||
else if (kind === "sequence") result = await library.inspect(args.positional[0]!, "sequence");
|
||||
else usage();
|
||||
break;
|
||||
}
|
||||
case "probe":
|
||||
result = await library.probe(args.positional[0]!);
|
||||
break;
|
||||
case "render": {
|
||||
const seconds = stringOption(args, "--seconds");
|
||||
if (seconds === undefined) usage();
|
||||
const scale = stringOption(args, "--scale");
|
||||
result = await library.render(args.positional[0]!, {
|
||||
output: args.positional[1]!,
|
||||
seconds,
|
||||
scale: scale === undefined ? undefined : Number(scale),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "export":
|
||||
result = await library.export(args.positional[0]!, {
|
||||
output: args.positional[1]!,
|
||||
format: stringOption(args, "--format"),
|
||||
start: stringOption(args, "--start"),
|
||||
end: stringOption(args, "--end"),
|
||||
allowOffline: args.values.has("--allow-offline"),
|
||||
});
|
||||
break;
|
||||
}
|
||||
console.log(stringifyNativeJSON(result, 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
process.exitCode = await main();
|
||||
} catch (error) {
|
||||
if (error instanceof VideoLibraryError) {
|
||||
if (error.result !== undefined) console.log(stringifyNativeJSON(error.result, 2));
|
||||
console.error(`Aphrody / Bun: ${error.message} (${error.code})`);
|
||||
process.exitCode = error.code === "INVALID_OPTIONS" ? 2 : 1;
|
||||
} else {
|
||||
console.error("Aphrody / Bun: The video operation failed.");
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { NativeJSON } from "./json.ts";
|
||||
|
||||
export type VideoErrorCode =
|
||||
| "INVALID_OPTIONS"
|
||||
| "RUNTIME_MISMATCH"
|
||||
| "SOURCE_MISMATCH"
|
||||
| "ENGINE_MISSING"
|
||||
| "NATIVE_START_FAILED"
|
||||
| "NATIVE_FAILED"
|
||||
| "NATIVE_TIMEOUT"
|
||||
| "NATIVE_INTERRUPTED"
|
||||
| "OUTPUT_LIMIT"
|
||||
| "INVALID_NATIVE_JSON"
|
||||
| "INVALID_JSON_VALUE"
|
||||
| "INVALID_TIME";
|
||||
|
||||
/** Structured results remain available on failure; native stderr stays private. */
|
||||
export class VideoLibraryError extends Error {
|
||||
readonly code: VideoErrorCode;
|
||||
readonly exitCode?: number;
|
||||
readonly result?: NativeJSON;
|
||||
|
||||
constructor(code: VideoErrorCode, message: string, exitCode?: number, result?: NativeJSON) {
|
||||
super(message);
|
||||
this.name = "VideoLibraryError";
|
||||
this.code = code;
|
||||
this.exitCode = exitCode;
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export { VideoLibraryError, type VideoErrorCode } from "./errors.ts";
|
||||
export {
|
||||
parseNativeJSON,
|
||||
stringifyNativeJSON,
|
||||
type NativeInteger,
|
||||
type NativeJSON,
|
||||
type NativeObject,
|
||||
} from "./json.ts";
|
||||
export {
|
||||
createVideoLibrary,
|
||||
videoLibraryDoctor,
|
||||
type CommandDescription,
|
||||
type CommandOutcome,
|
||||
type ExportOptions,
|
||||
type ExportReceipt,
|
||||
type ProjectInspection,
|
||||
type RenderOptions,
|
||||
type SequenceInspection,
|
||||
type VideoCommand,
|
||||
type VideoLibrary,
|
||||
type VideoLibraryOptions,
|
||||
} from "./library.ts";
|
||||
export { nativeVideoCapabilities, openNativeVideo, type NativeVideo, type NativeVideoMetadata } from "./native.ts";
|
||||
export { TICKS_PER_SECOND, ticksFromDecimalSeconds, ticksToDecimalSeconds } from "./time.ts";
|
||||
@@ -0,0 +1,75 @@
|
||||
import { VideoLibraryError } from "./errors.ts";
|
||||
|
||||
export type NativeInteger = number | bigint;
|
||||
export type NativeJSON = null | boolean | string | number | bigint | NativeJSON[] | NativeObject;
|
||||
export interface NativeObject {
|
||||
[key: string]: NativeJSON;
|
||||
}
|
||||
|
||||
interface JSONSourceContext {
|
||||
source?: string;
|
||||
}
|
||||
|
||||
interface ExactJSON {
|
||||
parse(text: string, reviver: (key: string, value: unknown, context?: JSONSourceContext) => unknown): unknown;
|
||||
rawJSON(text: string): object;
|
||||
}
|
||||
|
||||
const exactJSON = JSON as unknown as ExactJSON;
|
||||
const safeInteger = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
/** Rust emits i64/u64 as decimal integer tokens. Preserve those tokens beyond Number's range. */
|
||||
export function parseNativeJSON<T = NativeJSON>(text: string): T {
|
||||
try {
|
||||
return exactJSON.parse(text, (_key, value, context) => {
|
||||
if (typeof value !== "number") return value;
|
||||
if (!Number.isFinite(value) || !context?.source) {
|
||||
throw new VideoLibraryError("INVALID_NATIVE_JSON", "Native JSON requires finite numbers and source context.");
|
||||
}
|
||||
if (/^-?\d+$/.test(context.source)) {
|
||||
const integer = BigInt(context.source);
|
||||
return integer > safeInteger || integer < -safeInteger ? integer : value;
|
||||
}
|
||||
// Fractional/exponent tokens are native floating point values, rather than Rust integer fields.
|
||||
return value;
|
||||
}) as T;
|
||||
} catch (error) {
|
||||
if (error instanceof VideoLibraryError) throw error;
|
||||
throw new VideoLibraryError("INVALID_NATIVE_JSON", "The video engine returned invalid JSON.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Serialize bigint as an exact JSON integer, retaining the native Rust command contract. */
|
||||
export function stringifyNativeJSON(value: unknown, space?: number): string {
|
||||
if (space !== undefined && (!Number.isInteger(space) || space < 0 || space > 10)) {
|
||||
throw new VideoLibraryError("INVALID_JSON_VALUE", "JSON indentation must be between 0 and 10.");
|
||||
}
|
||||
try {
|
||||
const text = JSON.stringify(
|
||||
value,
|
||||
(_key, entry: unknown) => {
|
||||
if (typeof entry === "bigint") {
|
||||
if (typeof exactJSON.rawJSON !== "function") {
|
||||
throw new VideoLibraryError("INVALID_JSON_VALUE", "Exact JSON integers require the Aphrody Bun runtime.");
|
||||
}
|
||||
return exactJSON.rawJSON(entry.toString());
|
||||
}
|
||||
if (
|
||||
(typeof entry === "number" &&
|
||||
(!Number.isFinite(entry) || (Number.isInteger(entry) && !Number.isSafeInteger(entry)))) ||
|
||||
typeof entry === "function" ||
|
||||
typeof entry === "symbol"
|
||||
) {
|
||||
throw new VideoLibraryError("INVALID_JSON_VALUE", "Use finite JSON values and bigint for large integers.");
|
||||
}
|
||||
return entry;
|
||||
},
|
||||
space,
|
||||
);
|
||||
if (text === undefined) throw new VideoLibraryError("INVALID_JSON_VALUE", "A JSON value is required.");
|
||||
return text;
|
||||
} catch (error) {
|
||||
if (error instanceof VideoLibraryError) throw error;
|
||||
throw new VideoLibraryError("INVALID_JSON_VALUE", "The value cannot be serialized as native JSON.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
import { mkdir, stat } from "node:fs/promises";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import {
|
||||
assertRuntime,
|
||||
findModule,
|
||||
inspectRuntime,
|
||||
listModuleStatus,
|
||||
loadConfig,
|
||||
localBinaries,
|
||||
repositoryRoot,
|
||||
resolveCargoModule,
|
||||
type Config,
|
||||
} from "@aphrody/bun-workspace";
|
||||
import { VideoLibraryError } from "./errors.ts";
|
||||
import {
|
||||
parseNativeJSON,
|
||||
stringifyNativeJSON,
|
||||
type NativeInteger,
|
||||
type NativeJSON,
|
||||
type NativeObject,
|
||||
} from "./json.ts";
|
||||
import { nativeVideoCapabilities } from "./native.ts";
|
||||
import { runBounded, type ProcessLimits, type ProcessResult } from "./process.ts";
|
||||
|
||||
const buildInstruction = "Build the video engine with bun run workspace build filmcraft";
|
||||
const maxInputBytes = 16 * 1024 * 1024;
|
||||
const maxCommands = 10_000;
|
||||
const nativeFormats = new Set([
|
||||
"h264",
|
||||
"hevc",
|
||||
"prores",
|
||||
"dnxhr",
|
||||
"apv",
|
||||
"mjpeg",
|
||||
"mxf-op1a",
|
||||
"mxf-opatom",
|
||||
"png",
|
||||
"tiff",
|
||||
"bmp",
|
||||
"jpg",
|
||||
"tga",
|
||||
"dpx",
|
||||
"exr",
|
||||
"gif",
|
||||
"wav",
|
||||
"aiff",
|
||||
"aac",
|
||||
"flac",
|
||||
]);
|
||||
|
||||
export interface VideoLibraryOptions {
|
||||
binary?: string;
|
||||
timeoutMs?: number;
|
||||
maxOutputBytes?: number;
|
||||
dataDir?: string;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
interface ResolvedOptions extends ProcessLimits {
|
||||
binary?: string;
|
||||
dataDir: string;
|
||||
}
|
||||
|
||||
export interface VideoCommand {
|
||||
id: string;
|
||||
params?: NativeObject;
|
||||
}
|
||||
|
||||
export interface CommandDescription {
|
||||
id: string;
|
||||
label: string;
|
||||
menu: string[];
|
||||
shortcut: string | null;
|
||||
shortcuts: string[];
|
||||
defaultShortcut: string | null;
|
||||
params: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export type ProjectInspection = NativeObject & {
|
||||
name: string;
|
||||
path: string | null;
|
||||
dirty: boolean;
|
||||
revision: NativeInteger;
|
||||
activeSequence: NativeInteger | null;
|
||||
};
|
||||
|
||||
export type SequenceInspection = NativeObject & {
|
||||
id: NativeInteger;
|
||||
name: string | null;
|
||||
settings: NativeObject;
|
||||
duration: NativeInteger;
|
||||
durationFrames: NativeInteger;
|
||||
video: NativeJSON[];
|
||||
audio: NativeJSON[];
|
||||
};
|
||||
|
||||
export type CommandOutcome = NativeObject & {
|
||||
line: number;
|
||||
id: string;
|
||||
ok: boolean;
|
||||
};
|
||||
|
||||
export interface RenderOptions {
|
||||
output: string;
|
||||
seconds: number | string;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export interface ExportOptions {
|
||||
output: string;
|
||||
format?: string;
|
||||
start?: number | string;
|
||||
end?: number | string;
|
||||
allowOffline?: boolean;
|
||||
}
|
||||
|
||||
export type ExportReceipt = NativeObject & {
|
||||
job: NativeInteger;
|
||||
path: string;
|
||||
result: NativeObject;
|
||||
offlineMedia: NativeJSON[];
|
||||
};
|
||||
|
||||
export interface VideoLibrary {
|
||||
commands(filter?: string): Promise<CommandDescription[]>;
|
||||
describe(id: string): Promise<CommandDescription>;
|
||||
inspect(projectPath: string, kind: "project"): Promise<ProjectInspection>;
|
||||
inspect(projectPath: string, kind?: "sequence"): Promise<SequenceInspection>;
|
||||
probe(mediaPath: string): Promise<NativeObject>;
|
||||
run(projectPath: string, commands: VideoCommand[], options?: { saveAs?: string }): Promise<CommandOutcome[]>;
|
||||
render(projectPath: string, options: RenderOptions): Promise<{ output: string }>;
|
||||
export(projectPath: string, options: ExportOptions): Promise<ExportReceipt>;
|
||||
}
|
||||
|
||||
function pathArgument(path: string): string {
|
||||
if (typeof path !== "string" || !path.trim() || path.length > 32_768 || path.includes("\0")) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "A valid file or directory path is required.");
|
||||
}
|
||||
return resolve(path);
|
||||
}
|
||||
|
||||
function optionsFor(options: VideoLibraryOptions): ResolvedOptions {
|
||||
const timeoutMs = options.timeoutMs ?? 30_000;
|
||||
const maxOutputBytes = options.maxOutputBytes ?? 16 * 1024 * 1024;
|
||||
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3_600_000) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "The video deadline must be between 1 ms and one hour.");
|
||||
}
|
||||
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 64 * 1024 * 1024) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "The video output limit must be between 1 byte and 64 MiB.");
|
||||
}
|
||||
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "signal must be an AbortSignal.");
|
||||
}
|
||||
return {
|
||||
timeoutMs,
|
||||
maxOutputBytes,
|
||||
signal: options.signal,
|
||||
binary: options.binary === undefined ? undefined : pathArgument(options.binary),
|
||||
dataDir: pathArgument(options.dataDir ?? join(repositoryRoot, ".local", "video", "presets")),
|
||||
};
|
||||
}
|
||||
|
||||
async function rejectProjectOutput(projectPath: string, output: string): Promise<void> {
|
||||
const source = pathArgument(projectPath);
|
||||
const equal = process.platform === "win32" ? source.toLowerCase() === output.toLowerCase() : source === output;
|
||||
if (equal) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Media output must use a separate path from the source project.");
|
||||
}
|
||||
let sourceFile;
|
||||
try {
|
||||
sourceFile = await stat(source, { bigint: true });
|
||||
} catch {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "The source project is unavailable.");
|
||||
}
|
||||
let outputFile;
|
||||
try {
|
||||
outputFile = await stat(output, { bigint: true });
|
||||
} catch (error) {
|
||||
if ((error as { code?: string }).code === "ENOENT") return;
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "The media destination is unavailable.");
|
||||
}
|
||||
if (sourceFile.dev === outputFile.dev && sourceFile.ino === outputFile.ino) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Media output must use a separate file from the source project.");
|
||||
}
|
||||
}
|
||||
|
||||
function commandId(id: string): string {
|
||||
if (typeof id !== "string" || !/^[A-Za-z][A-Za-z0-9._-]{0,127}$/.test(id)) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "A native command identifier is required.");
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function secondsArgument(value: string | number): string {
|
||||
if (
|
||||
(typeof value !== "number" && typeof value !== "string") ||
|
||||
(typeof value === "string" && (value.length > 64 || !/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)))
|
||||
) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Use nonnegative seconds.");
|
||||
}
|
||||
const seconds = Number(value);
|
||||
if (!Number.isFinite(seconds) || seconds < 0 || seconds > 10_000_000) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Seconds must be between 0 and 10000000.");
|
||||
}
|
||||
return seconds.toString();
|
||||
}
|
||||
|
||||
async function sourceConfig(): Promise<Config> {
|
||||
let config: Config;
|
||||
try {
|
||||
config = await loadConfig();
|
||||
} catch {
|
||||
throw new VideoLibraryError("SOURCE_MISMATCH", "The video workspace configuration is unavailable.");
|
||||
}
|
||||
try {
|
||||
assertRuntime(config);
|
||||
} catch {
|
||||
throw new VideoLibraryError("RUNTIME_MISMATCH", "Use the pinned Aphrody Bun 1.4.4 runtime.");
|
||||
}
|
||||
const source = (await listModuleStatus(config)).find(module => module.id === "filmcraft");
|
||||
if (!source || source.error || !source.initialized || !source.pointerMatches || !source.revisionMatches) {
|
||||
throw new VideoLibraryError("SOURCE_MISMATCH", "The video library differs from its pinned source.");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async function validatedBinary(path: string, options: ResolvedOptions): Promise<string> {
|
||||
const expected = process.platform === "win32" ? "filmcraft-cli.exe" : "filmcraft-cli";
|
||||
if (basename(path).toLowerCase() !== expected.toLowerCase()) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Select the native video library CLI artifact.");
|
||||
}
|
||||
if (!(await Bun.file(path).exists())) throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
|
||||
const version = await runBounded([path, "--version"], repositoryRoot, {
|
||||
timeoutMs: Math.min(options.timeoutMs, 5_000),
|
||||
maxOutputBytes: Math.min(options.maxOutputBytes, 4_096),
|
||||
signal: options.signal,
|
||||
});
|
||||
if (version.code !== 0 || version.stdout.trim() !== "filmcraft-cli 0.7.2") {
|
||||
throw new VideoLibraryError("SOURCE_MISMATCH", "The native video artifact differs from its pinned version.");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
async function resolveBinary(config: Config, options: ResolvedOptions): Promise<string> {
|
||||
if (options.binary) return validatedBinary(options.binary, options);
|
||||
const selected = (await localBinaries(config)).filmcraft;
|
||||
if (selected) return validatedBinary(selected, options);
|
||||
const module = await resolveCargoModule(findModule(config, "filmcraft"));
|
||||
const cargo = Bun.which("cargo");
|
||||
if (!cargo) throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
|
||||
const metadata = await runBounded(
|
||||
[cargo, "metadata", "--offline", "--no-deps", "--format-version", "1", "--manifest-path", module.manifest],
|
||||
module.directory,
|
||||
{ timeoutMs: Math.min(options.timeoutMs, 20_000), maxOutputBytes: options.maxOutputBytes, signal: options.signal },
|
||||
);
|
||||
if (metadata.code !== 0) throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
|
||||
const parsed = parseNativeJSON<NativeObject>(metadata.stdout);
|
||||
if (typeof parsed.target_directory !== "string") {
|
||||
throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
|
||||
}
|
||||
const filename = process.platform === "win32" ? "filmcraft-cli.exe" : "filmcraft-cli";
|
||||
for (const profile of ["release", "debug"]) {
|
||||
const candidate = join(parsed.target_directory, profile, filename);
|
||||
if (await Bun.file(candidate).exists()) return validatedBinary(candidate, options);
|
||||
}
|
||||
throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
|
||||
}
|
||||
|
||||
function nativeResult(result: ProcessResult, failureKind?: "json" | "jsonl" | null): void {
|
||||
if (result.code !== 0) {
|
||||
let receipt: NativeJSON | undefined;
|
||||
if (failureKind && result.stdout.trim()) {
|
||||
try {
|
||||
receipt =
|
||||
failureKind === "json"
|
||||
? parseNativeJSON(result.stdout)
|
||||
: result.stdout
|
||||
.split(/\r?\n/)
|
||||
.filter(line => line.trim())
|
||||
.map(line => parseNativeJSON<NativeJSON>(line));
|
||||
} catch {
|
||||
// Invalid native output does not replace the original failure status.
|
||||
}
|
||||
}
|
||||
throw new VideoLibraryError("NATIVE_FAILED", "The native video operation failed.", result.code, receipt);
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve an existing native artifact. This API never builds or reconstructs project formats. */
|
||||
export async function createVideoLibrary(options: VideoLibraryOptions = {}): Promise<VideoLibrary> {
|
||||
const resolved = optionsFor(options);
|
||||
const config = await sourceConfig();
|
||||
let binary: string;
|
||||
try {
|
||||
binary = await resolveBinary(config, resolved);
|
||||
} catch (error) {
|
||||
if (error instanceof VideoLibraryError) throw error;
|
||||
throw new VideoLibraryError("ENGINE_MISSING", buildInstruction);
|
||||
}
|
||||
|
||||
const invoke = async (
|
||||
args: string[],
|
||||
projectPath?: string,
|
||||
input?: Uint8Array,
|
||||
saveAs?: string,
|
||||
failureKind: "json" | "jsonl" | null = "json",
|
||||
) => {
|
||||
try {
|
||||
await mkdir(resolved.dataDir, { recursive: true });
|
||||
} catch {
|
||||
throw new VideoLibraryError("NATIVE_START_FAILED", "The isolated video resource directory is unavailable.");
|
||||
}
|
||||
const argv = [binary, "--compact", "--data-dir", resolved.dataDir];
|
||||
if (projectPath !== undefined) argv.push("--project", pathArgument(projectPath));
|
||||
if (saveAs !== undefined) argv.push("--save-as", pathArgument(saveAs));
|
||||
const result = await runBounded([...argv, ...args], repositoryRoot, resolved, input);
|
||||
nativeResult(result, failureKind);
|
||||
return result.stdout;
|
||||
};
|
||||
|
||||
const library: VideoLibrary = {
|
||||
async commands(filter) {
|
||||
if (filter !== undefined && (typeof filter !== "string" || filter.length > 128 || filter.includes("\0"))) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "The command filter must be at most 128 characters.");
|
||||
}
|
||||
const args = ["commands", "--json"];
|
||||
if (filter !== undefined) args.push("--", filter);
|
||||
return parseNativeJSON<CommandDescription[]>(await invoke(args));
|
||||
},
|
||||
async describe(id) {
|
||||
return parseNativeJSON<CommandDescription>(await invoke(["describe", "--", commandId(id)]));
|
||||
},
|
||||
async inspect(projectPath: string, kind: "project" | "sequence" = "sequence") {
|
||||
if (kind !== "project" && kind !== "sequence") {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Inspect project or sequence.");
|
||||
}
|
||||
return parseNativeJSON(await invoke(["inspect", kind], projectPath)) as ProjectInspection & SequenceInspection;
|
||||
},
|
||||
async probe(mediaPath) {
|
||||
return parseNativeJSON<NativeObject>(await invoke(["probe", "--", pathArgument(mediaPath)]));
|
||||
},
|
||||
async run(projectPath, commands, runOptions = {}) {
|
||||
if (!Array.isArray(commands) || commands.length < 1 || commands.length > maxCommands) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Supply between 1 and 10000 video commands.");
|
||||
}
|
||||
const encoder = new TextEncoder();
|
||||
const lines: string[] = [];
|
||||
let inputBytes = 0;
|
||||
for (const command of commands) {
|
||||
if (!command || typeof command !== "object") {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Each video command requires an identifier and parameters.");
|
||||
}
|
||||
const id = commandId(command.id);
|
||||
if (id.startsWith("file.save")) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Persist project edits with the saveAs option.");
|
||||
}
|
||||
const params = command.params ?? {};
|
||||
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Command parameters must be a JSON object.");
|
||||
}
|
||||
const line = `${stringifyNativeJSON({ id, params })}\n`;
|
||||
inputBytes += encoder.encode(line).byteLength;
|
||||
if (inputBytes > maxInputBytes) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Video commands must fit within 16 MiB.");
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
const stdout = await invoke(
|
||||
["run", "-"],
|
||||
projectPath,
|
||||
encoder.encode(lines.join("")),
|
||||
runOptions.saveAs,
|
||||
"jsonl",
|
||||
);
|
||||
return stdout
|
||||
.split(/\r?\n/)
|
||||
.filter(line => line.trim())
|
||||
.map(line => parseNativeJSON<CommandOutcome>(line));
|
||||
},
|
||||
async render(projectPath, renderOptions) {
|
||||
const output = pathArgument(renderOptions.output);
|
||||
await rejectProjectOutput(projectPath, output);
|
||||
if (!output.toLowerCase().endsWith(".png")) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Render output must be a PNG path.");
|
||||
}
|
||||
const args = ["render", "--seconds", secondsArgument(renderOptions.seconds), "--out", output];
|
||||
if (renderOptions.scale !== undefined) {
|
||||
if (!Number.isFinite(renderOptions.scale) || renderOptions.scale <= 0 || renderOptions.scale > 1) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Render scale must be greater than 0 and at most 1.");
|
||||
}
|
||||
args.push("--scale", renderOptions.scale.toString());
|
||||
}
|
||||
await invoke(args, projectPath, undefined, undefined, null);
|
||||
return { output };
|
||||
},
|
||||
async export(projectPath, exportOptions) {
|
||||
const output = pathArgument(exportOptions.output);
|
||||
await rejectProjectOutput(projectPath, output);
|
||||
const args = ["export"];
|
||||
if (exportOptions.format !== undefined) {
|
||||
if (!nativeFormats.has(exportOptions.format)) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Select a supported native export format.");
|
||||
}
|
||||
args.push("--format", exportOptions.format);
|
||||
}
|
||||
if (exportOptions.start !== undefined) args.push("--start", secondsArgument(exportOptions.start));
|
||||
if (exportOptions.end !== undefined) args.push("--end", secondsArgument(exportOptions.end));
|
||||
if (
|
||||
exportOptions.start !== undefined &&
|
||||
exportOptions.end !== undefined &&
|
||||
Number(exportOptions.end) <= Number(exportOptions.start)
|
||||
) {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Export end must follow its start.");
|
||||
}
|
||||
if (exportOptions.allowOffline !== undefined && typeof exportOptions.allowOffline !== "boolean") {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "allowOffline must be a boolean.");
|
||||
}
|
||||
if (exportOptions.allowOffline) args.push("--allow-offline");
|
||||
args.push("--", output);
|
||||
// Native formats derive sibling filenames; a fresh directory protects every destination.
|
||||
try {
|
||||
await mkdir(dirname(output));
|
||||
} catch {
|
||||
throw new VideoLibraryError("INVALID_OPTIONS", "Export into a new directory inside an existing parent.");
|
||||
}
|
||||
return parseNativeJSON<ExportReceipt>(await invoke(args, projectPath));
|
||||
},
|
||||
};
|
||||
return library;
|
||||
}
|
||||
|
||||
/** Report source/runtime pins and artifact discovery without starting a media operation. */
|
||||
export async function videoLibraryDoctor(options: VideoLibraryOptions = {}) {
|
||||
const resolved = optionsFor(options);
|
||||
const config = await loadConfig();
|
||||
const runtime = inspectRuntime(config);
|
||||
const source = (await listModuleStatus(config)).find(module => module.id === "filmcraft");
|
||||
let engine: { available: boolean; reason?: string };
|
||||
try {
|
||||
await resolveBinary(config, resolved);
|
||||
engine = { available: true };
|
||||
} catch (error) {
|
||||
engine = { available: false, reason: error instanceof VideoLibraryError ? error.code : "ENGINE_MISSING" };
|
||||
}
|
||||
return {
|
||||
identity: "Aphrody / Bun",
|
||||
runtime: {
|
||||
version: runtime.version,
|
||||
revision: runtime.revision,
|
||||
recognizedFork: runtime.recognizedFork,
|
||||
versionMatches: runtime.versionMatches,
|
||||
revisionMatches: runtime.revisionMatches,
|
||||
},
|
||||
source: {
|
||||
revision: source?.revision ?? null,
|
||||
expectedRevision: findModule(config, "filmcraft").revision,
|
||||
initialized: source?.initialized ?? false,
|
||||
pointerMatches: source?.pointerMatches ?? false,
|
||||
revisionMatches: source?.revisionMatches ?? false,
|
||||
modified: source?.clean === false,
|
||||
},
|
||||
engine,
|
||||
nativeVideo: await nativeVideoCapabilities(),
|
||||
limits: { timeoutMs: resolved.timeoutMs, maxOutputBytes: resolved.maxOutputBytes },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export interface NativeVideoMetadata {
|
||||
width: number;
|
||||
height: number;
|
||||
duration: number | null;
|
||||
codec: string;
|
||||
}
|
||||
|
||||
export interface NativeVideo {
|
||||
frame(seconds: number): NativeVideo;
|
||||
resize(width: number, height?: number): NativeVideo;
|
||||
png(): NativeVideo;
|
||||
bytes(): Promise<Uint8Array>;
|
||||
metadata(): Promise<NativeVideoMetadata>;
|
||||
}
|
||||
|
||||
interface NativeVideoConstructor {
|
||||
new (input: string | ArrayBuffer | ArrayBufferView | Blob, options?: { maxPixels?: number }): NativeVideo;
|
||||
readonly available: boolean;
|
||||
}
|
||||
|
||||
function videoConstructor(): NativeVideoConstructor | undefined {
|
||||
return (Bun as unknown as { Video?: NativeVideoConstructor }).Video;
|
||||
}
|
||||
|
||||
export function nativeVideoCapabilities() {
|
||||
const video = videoConstructor();
|
||||
if (typeof video !== "function") {
|
||||
return { owner: "Bun.Video", available: false, reason: "This Bun binary has no native video API." };
|
||||
}
|
||||
if (video.available !== true) {
|
||||
return { owner: "Bun.Video", available: false, reason: "The native video provider is disabled." };
|
||||
}
|
||||
return { owner: "Bun.Video", available: true };
|
||||
}
|
||||
|
||||
/** Use the runtime owner directly; availability does not qualify codecs or encoding. */
|
||||
export function openNativeVideo(
|
||||
input: string | ArrayBuffer | ArrayBufferView | Blob,
|
||||
options?: { maxPixels?: number },
|
||||
): NativeVideo {
|
||||
const capability = nativeVideoCapabilities();
|
||||
if (!capability.available) throw new Error(capability.reason);
|
||||
return new (videoConstructor()!)(input, options);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { VideoLibraryError, type VideoErrorCode } from "./errors.ts";
|
||||
|
||||
export interface ProcessLimits {
|
||||
timeoutMs: number;
|
||||
maxOutputBytes: number;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
interface OwnedJob {
|
||||
assign(process: { pid: number }): void;
|
||||
terminate(code?: number): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
interface WindowsJobs {
|
||||
isSupported: boolean;
|
||||
Job?: new (options: { killOnClose: boolean }) => OwnedJob;
|
||||
}
|
||||
|
||||
export interface ProcessResult {
|
||||
code: number;
|
||||
stdout: string;
|
||||
}
|
||||
|
||||
async function reap(child: Bun.ReadableSubprocess): Promise<void> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
await Promise.race([
|
||||
child.exited.catch(() => {}),
|
||||
new Promise<void>(resolve => {
|
||||
timer = setTimeout(resolve, 5_000);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function createJob(): Promise<OwnedJob | undefined> {
|
||||
if (process.platform !== "win32") return;
|
||||
try {
|
||||
const windows = (await import("bun:windows")) as unknown as WindowsJobs;
|
||||
if (windows.isSupported && windows.Job) return new windows.Job({ killOnClose: true });
|
||||
} catch {
|
||||
// Hosts without the optional owner API use taskkill for the same owned process tree.
|
||||
}
|
||||
}
|
||||
|
||||
/** Bound both streams, including discarded diagnostics, and own the child's entire lifetime. */
|
||||
export async function runBounded(
|
||||
argv: string[],
|
||||
cwd: string,
|
||||
limits: ProcessLimits,
|
||||
input?: Uint8Array,
|
||||
): Promise<ProcessResult> {
|
||||
if (limits.signal?.aborted) {
|
||||
throw new VideoLibraryError("NATIVE_INTERRUPTED", "The native video operation was interrupted.");
|
||||
}
|
||||
let job = await createJob();
|
||||
if (limits.signal?.aborted) {
|
||||
job?.close();
|
||||
throw new VideoLibraryError("NATIVE_INTERRUPTED", "The native video operation was interrupted.");
|
||||
}
|
||||
const taskkill = process.platform === "win32" ? Bun.which("taskkill") : null;
|
||||
if (process.platform === "win32" && !job && !taskkill) {
|
||||
throw new VideoLibraryError("NATIVE_START_FAILED", "Native process tree cancellation is unavailable.");
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const reads = new AbortController();
|
||||
let child: Bun.ReadableSubprocess;
|
||||
try {
|
||||
child = Bun.spawn(argv, {
|
||||
cwd,
|
||||
stdin: input ?? "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
signal: controller.signal,
|
||||
killSignal: "SIGKILL",
|
||||
windowsHide: true,
|
||||
detached: process.platform !== "win32",
|
||||
});
|
||||
} catch {
|
||||
job?.close();
|
||||
throw new VideoLibraryError("NATIVE_START_FAILED", "The native video process could not start.");
|
||||
}
|
||||
if (job) {
|
||||
try {
|
||||
job.assign(child);
|
||||
} catch {
|
||||
job.close();
|
||||
job = undefined;
|
||||
if (!taskkill) {
|
||||
controller.abort();
|
||||
await Promise.allSettled([reap(child), child.stdout.cancel(), child.stderr.cancel()]);
|
||||
throw new VideoLibraryError("NATIVE_START_FAILED", "The native video process could not be isolated.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let failure: VideoLibraryError | undefined;
|
||||
let stopping: Promise<void> | undefined;
|
||||
let outputBytes = 0;
|
||||
|
||||
const stopTree = (): Promise<void> => {
|
||||
if (stopping) return stopping;
|
||||
stopping = (async () => {
|
||||
reads.abort();
|
||||
try {
|
||||
if (job) {
|
||||
job.terminate();
|
||||
} else if (process.platform !== "win32") {
|
||||
try {
|
||||
process.kill(-child.pid, "SIGKILL");
|
||||
} catch {
|
||||
// The owned process group may already be gone.
|
||||
}
|
||||
} else if (taskkill && child.exitCode === null) {
|
||||
const killer = Bun.spawn([taskkill, "/PID", String(child.pid), "/T", "/F"], {
|
||||
stdin: "ignore",
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
windowsHide: true,
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
killSignal: "SIGKILL",
|
||||
});
|
||||
await killer.exited;
|
||||
}
|
||||
} catch {
|
||||
// Abort still terminates the direct child if the host tree operation fails.
|
||||
} finally {
|
||||
controller.abort();
|
||||
await reap(child);
|
||||
}
|
||||
})();
|
||||
return stopping;
|
||||
};
|
||||
|
||||
const fail = (code: VideoErrorCode, message: string) => {
|
||||
failure ??= new VideoLibraryError(code, message);
|
||||
void stopTree();
|
||||
};
|
||||
const interrupt = () => fail("NATIVE_INTERRUPTED", "The native video operation was interrupted.");
|
||||
const timer = setTimeout(
|
||||
() => fail("NATIVE_TIMEOUT", "The native video operation exceeded its deadline."),
|
||||
limits.timeoutMs,
|
||||
);
|
||||
process.once("SIGINT", interrupt);
|
||||
process.once("SIGTERM", interrupt);
|
||||
limits.signal?.addEventListener("abort", interrupt, { once: true });
|
||||
if (limits.signal?.aborted) interrupt();
|
||||
|
||||
const collect = async (stream: ReadableStream<Uint8Array>, retain: boolean): Promise<string> => {
|
||||
const reader = stream.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
let retainedBytes = 0;
|
||||
try {
|
||||
while (true) {
|
||||
if (reads.signal.aborted) throw failure;
|
||||
let interrupted: (() => void) | undefined;
|
||||
const cancelled = new Promise<never>((_resolve, reject) => {
|
||||
interrupted = () => reject(failure);
|
||||
reads.signal.addEventListener("abort", interrupted, { once: true });
|
||||
});
|
||||
let chunk: Awaited<ReturnType<typeof reader.read>>;
|
||||
try {
|
||||
chunk = await Promise.race([reader.read(), cancelled]);
|
||||
} finally {
|
||||
if (interrupted) reads.signal.removeEventListener("abort", interrupted);
|
||||
}
|
||||
if (chunk.done) break;
|
||||
outputBytes += chunk.value.byteLength;
|
||||
if (outputBytes > limits.maxOutputBytes) {
|
||||
fail("OUTPUT_LIMIT", "The native video operation exceeded its output limit.");
|
||||
throw failure;
|
||||
}
|
||||
if (retain) {
|
||||
chunks.push(chunk.value);
|
||||
retainedBytes += chunk.value.byteLength;
|
||||
}
|
||||
}
|
||||
if (!retain) return "";
|
||||
const bytes = new Uint8Array(retainedBytes);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
bytes.set(chunk, offset);
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
||||
} finally {
|
||||
if (reads.signal.aborted) void reader.cancel().catch(() => {});
|
||||
try {
|
||||
reader.releaseLock();
|
||||
} catch {
|
||||
// A cancelled read can finish after its consumer has stopped.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const [code, stdout] = await Promise.all([
|
||||
child.exited.then(code => {
|
||||
// Closing a Windows job also closes pipes inherited by its remaining children.
|
||||
job?.close();
|
||||
job = undefined;
|
||||
return code;
|
||||
}),
|
||||
collect(child.stdout, true),
|
||||
collect(child.stderr, false),
|
||||
]);
|
||||
if (failure) throw failure;
|
||||
return { code, stdout };
|
||||
} catch (error) {
|
||||
if (!failure) failure = new VideoLibraryError("NATIVE_FAILED", "The native video operation failed.");
|
||||
await stopTree();
|
||||
if (error instanceof VideoLibraryError) throw error;
|
||||
throw failure;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
process.removeListener("SIGINT", interrupt);
|
||||
process.removeListener("SIGTERM", interrupt);
|
||||
limits.signal?.removeEventListener("abort", interrupt);
|
||||
job?.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { VideoLibraryError } from "./errors.ts";
|
||||
|
||||
export const TICKS_PER_SECOND = 254_016_000_000n;
|
||||
const minTick = -(1n << 63n);
|
||||
const maxTick = (1n << 63n) - 1n;
|
||||
|
||||
function checkedTick(ticks: bigint): bigint {
|
||||
if (typeof ticks !== "bigint" || ticks < minTick || ticks > maxTick) {
|
||||
throw new VideoLibraryError("INVALID_TIME", "Ticks must fit a signed 64-bit integer.");
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
|
||||
/** Convert plain decimal seconds exactly. Reject values between native ticks. */
|
||||
export function ticksFromDecimalSeconds(seconds: string): bigint {
|
||||
if (typeof seconds !== "string" || seconds.length > 64 || !/^-?(?:0|[1-9]\d*)(?:\.\d{1,12})?$/.test(seconds)) {
|
||||
throw new VideoLibraryError("INVALID_TIME", "Use plain decimal seconds with at most 12 fractional digits.");
|
||||
}
|
||||
const negative = seconds.startsWith("-");
|
||||
const [whole, fraction = ""] = (negative ? seconds.slice(1) : seconds).split(".");
|
||||
const denominator = 10n ** BigInt(fraction.length);
|
||||
const numerator = BigInt(`${whole}${fraction}`) * TICKS_PER_SECOND;
|
||||
if (numerator % denominator !== 0n) {
|
||||
throw new VideoLibraryError("INVALID_TIME", "The supplied seconds do not fall on an exact native tick.");
|
||||
}
|
||||
return checkedTick((negative ? -numerator : numerator) / denominator);
|
||||
}
|
||||
|
||||
/** Display seconds with a fixed precision, truncated toward zero; this is not a round trip. */
|
||||
export function ticksToDecimalSeconds(ticks: bigint, precision = 12): string {
|
||||
checkedTick(ticks);
|
||||
if (!Number.isInteger(precision) || precision < 0 || precision > 12) {
|
||||
throw new VideoLibraryError("INVALID_TIME", "Decimal precision must be between 0 and 12.");
|
||||
}
|
||||
const scale = 10n ** BigInt(precision);
|
||||
const scaled = (ticks * scale) / TICKS_PER_SECOND;
|
||||
const magnitude = scaled < 0n ? -scaled : scaled;
|
||||
const sign = scaled < 0n ? "-" : "";
|
||||
if (precision === 0) return `${sign}${magnitude}`;
|
||||
return `${sign}${magnitude / scale}.${(magnitude % scale).toString().padStart(precision, "0")}`;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
|
||||
/** Resolve from this checkout; no workstation or host path is embedded. */
|
||||
export const repositoryRoot = Bun.fileURLToPath(new URL("../../../", import.meta.url));
|
||||
export const repositoryRoot = resolve(import.meta.dir, "../../..");
|
||||
|
||||
export interface Module {
|
||||
id: string;
|
||||
@@ -24,6 +24,7 @@ export interface Config {
|
||||
schemaVersion: 1;
|
||||
runtime: { version: string; repository: string; revision: string };
|
||||
modules: Module[];
|
||||
priorities?: string[];
|
||||
desktop: Record<string, unknown> & { source?: Source };
|
||||
}
|
||||
|
||||
@@ -170,6 +171,12 @@ export async function loadConfig(root = repositoryRoot): Promise<Config> {
|
||||
checkoutPath(modulePath, entry.manifestPath);
|
||||
}
|
||||
}
|
||||
if (
|
||||
value.priorities !== undefined &&
|
||||
(!Array.isArray(value.priorities) || value.priorities.some(id => typeof id !== "string" || !ids.has(id)))
|
||||
) {
|
||||
throw new Error("Priorities must reference catalog libraries.");
|
||||
}
|
||||
return value as unknown as Config;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env bun
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const checks = ["doctor", "typecheck", "lint", "format:check"];
|
||||
for (const check of checks) {
|
||||
const controller = new AbortController();
|
||||
const child = Bun.spawn([process.execPath, "run", "--bun", check], {
|
||||
cwd: Bun.fileURLToPath(new URL("../", import.meta.url)),
|
||||
cwd: resolve(import.meta.dir, ".."),
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
|
||||
+1
-1
Submodule toolchains/bun updated: c209b95eb0...939c835adf.
+9
-2
@@ -9,8 +9,15 @@
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
"moduleDetection": "force",
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"include": ["packages/workspace/src/**/*.ts", "apps/desktop/**/*.ts", "scripts/**/*.ts"],
|
||||
"include": [
|
||||
"packages/workspace/src/**/*.ts",
|
||||
"apps/desktop/**/*.ts",
|
||||
"scripts/**/*.ts",
|
||||
"packages/video/src/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "toolchains", "apps/desktop/dist", "apps/desktop/src-tauri"]
|
||||
}
|
||||
Reference in new issue
Block a user