* Add console support for custom app tools * Only claim unambiguous git URLs in connect-dialog detection * Add custom tools console recording * Omit the required key when schema projection empties it * Assert projection output stays JSON-encodable without JSON.parse * Derive app tool schemas via Standard Schema * Cover zod schemas in custom tools e2e * Pass the required target option to Standard Schema converters * Make app sources top-level integrations * Limit local directory app sources to local host * Add local directory picker endpoint * Update local app dependency lockfile * Cover local directory picker in local e2e * Fix CLI node ambient types * Cover custom tools on cloud target * Complete local custom tools picker e2e * Require auth in custom tools git e2e * Exercise custom tools run tab * Cover custom tools collect failure recovery * Handle cloud auth status in custom tools e2e * Show local app source shape in directory picker * Tighten directory picker e2e tag assertion * Remove implicit GitHub connection token from git sources * Package workerd for local binaries * Trust bundled root certificates in workerd outbound TLS workerd's built-in internet service cannot read the Windows system trust store, so npm installs inside the publish worker failed TLS verification. Declare the internet service explicitly with the runtime's bundled Mozilla roots, surface workerd stderr in startup failures, and fail the bundle when a dependency install warning is present instead of publishing a broken bundle. * Add packed-binary custom tools e2e scenarios The new CLI VM scenario proves the compiled executor binary can publish and invoke local-directory custom tools inside macOS, Linux, and Windows guests. The desktop VM scenario proves the packaged sidecar daemon does the same on desktop bundles, and these manual-run VM projects remain outside the default CI chain by design.
Showing the changes to apps/cli/src (2 of 64 files). Show all
Unified · Split
| @@ -2,7 +2,7 @@ import { cp, mkdir, rm, writeFile, chmod } from "node:fs/promises"; · 1 unmodified line | |||
| 2 | import { existsSync } from "node:fs"; | 2 | import { existsSync } from "node:fs"; |
| 3 | import { createRequire } from "node:module"; | 3 | import { createRequire } from "node:module"; |
| 4 | import { resolve, join, dirname } from "node:path"; | 4 | import { resolve, join, dirname } from "node:path"; |
| 5 | import { fileURLToPath } from "node:url"; | 5 | import { fileURLToPath, pathToFileURL } from "node:url"; |
| 6 | import { parseArgs } from "node:util"; | 6 | import { parseArgs } from "node:util"; |
| 7 | import { $ } from "bun"; | 7 | import { $ } from "bun"; |
| 8 | 8 | ||
| @@ -11,6 +11,8 @@ const cliRoot = resolve(repoRoot, "apps/cli"); · 2 unmodified lines | |||
| 11 | const webRoot = resolve(repoRoot, "apps/local"); | 11 | const webRoot = resolve(repoRoot, "apps/local"); |
| 12 | const distDir = resolve(cliRoot, "dist"); | 12 | const distDir = resolve(cliRoot, "dist"); |
| 13 | const ONEPASSWORD_CORE_WASM_FILENAME = "onepassword-core_bg.wasm"; | 13 | const ONEPASSWORD_CORE_WASM_FILENAME = "onepassword-core_bg.wasm"; |
| 14 | const WORKERD_VERSION = "1.20260708.1"; | ||
| 15 | const WORKER_BUNDLER_DIRNAME = "worker-bundler"; | ||
| 14 | 16 | ||
| 15 | const resolveQuickJsWasmPath = (): string => { | 17 | const resolveQuickJsWasmPath = (): string => { |
| 16 | const req = createRequire(join(repoRoot, "packages/kernel/runtime-quickjs/package.json")); | 18 | const req = createRequire(join(repoRoot, "packages/kernel/runtime-quickjs/package.json")); |
| @@ -33,6 +35,34 @@ const resolveOnePasswordCoreWasmPath = (): string => { · 16 unmodified lines | |||
| 33 | return wasmPath; | 35 | return wasmPath; |
| 34 | }; | 36 | }; |
| 35 | 37 | ||
| 38 | const resolveWorkerBundlerDistPath = (): string => { | ||
| 39 | const req = createRequire(join(repoRoot, "packages/plugins/apps/package.json")); | ||
| 40 | const pkgJson = req.resolve("@cloudflare/worker-bundler/package.json"); | ||
| 41 | const distPath = join(dirname(pkgJson), "dist"); | ||
| 42 | if (!existsSync(join(distPath, "index.js")) || !existsSync(join(distPath, "esbuild.wasm"))) { | ||
| 43 | throw new Error(`@cloudflare/worker-bundler dist files not found at ${distPath}`); | ||
| 44 | } | ||
| 45 | return distPath; | ||
| 46 | }; | ||
| 47 | |||
| 48 | const createPackedWorkerBundlerSource = async (distPath: string): Promise<string> => { | ||
| 49 | const esbuildPath = join(repoRoot, "node_modules/.bun/node_modules/esbuild/lib/main.js"); | ||
| 50 | if (!existsSync(esbuildPath)) throw new Error(`esbuild not found at ${esbuildPath}`); | ||
| 51 | const { build } = await import(pathToFileURL(esbuildPath).href); | ||
| 52 | const result = await build({ | ||
| 53 | entryPoints: [join(distPath, "index.js")], | ||
| 54 | bundle: true, | ||
| 55 | format: "esm", | ||
| 56 | platform: "browser", | ||
| 57 | external: ["./esbuild.wasm"], | ||
| 58 | logLevel: "silent", | ||
| 59 | write: false, | ||
| 60 | }); | ||
| 61 | const source = result.outputFiles[0]?.text; | ||
| 62 | if (source === undefined) throw new Error("failed to bundle @cloudflare/worker-bundler"); | ||
| 63 | return source; | ||
| 64 | }; | ||
| 65 | |||
| 36 | // --------------------------------------------------------------------------- | 66 | // --------------------------------------------------------------------------- |
| 37 | // Metadata | 67 | // Metadata |
| 38 | // --------------------------------------------------------------------------- | 68 | // --------------------------------------------------------------------------- |
| @@ -119,6 +149,7 @@ const bunTarget = (t: Target): Bun.Build.CompileTarget => { · 80 unmodified lines | |||
| 119 | }; | 149 | }; |
| 120 | 150 | ||
| 121 | const binaryName = (t: Target) => (t.os === "win32" ? "executor.exe" : "executor"); | 151 | const binaryName = (t: Target) => (t.os === "win32" ? "executor.exe" : "executor"); |
| 152 | const workerdBinaryName = (t: Target) => (t.os === "win32" ? "workerd.exe" : "workerd"); | ||
| 122 | 153 | ||
| 123 | const isCurrentPlatform = (t: Target) => | 154 | const isCurrentPlatform = (t: Target) => |
| 124 | t.os === process.platform && t.arch === process.arch && !t.abi; | 155 | t.os === process.platform && t.arch === process.arch && !t.abi; |
| @@ -211,6 +242,41 @@ const resolveLibsqlNative = (t: Target): string | null => { · 86 unmodified lines | |||
| 211 | } | 242 | } |
| 212 | }; | 243 | }; |
| 213 | 244 | ||
| 245 | const resolveWorkerdBinary = (t: Target): string | null => { | ||
| 246 | const platformMap: Record<string, string> = { | ||
| 247 | "darwin-arm64": "@cloudflare/workerd-darwin-arm64", | ||
| 248 | "darwin-x64": "@cloudflare/workerd-darwin-64", | ||
| 249 | "linux-arm64": "@cloudflare/workerd-linux-arm64", | ||
| 250 | "linux-x64": "@cloudflare/workerd-linux-64", | ||
| 251 | "win32-x64": "@cloudflare/workerd-windows-64", | ||
| 252 | }; | ||
| 253 | const key = [t.os, t.arch, t.abi].filter(Boolean).join("-"); | ||
| 254 | const pkg = platformMap[key]; | ||
| 255 | if (!pkg) return null; | ||
| 256 | const binary = workerdBinaryName(t); | ||
| 257 | try { | ||
| 258 | const req = createRequire( | ||
| 259 | join(repoRoot, "packages/kernel/runtime-workerd-subprocess/package.json"), | ||
| 260 | ); | ||
| 261 | const pkgJson = req.resolve(`${pkg}/package.json`); | ||
| 262 | for (const candidate of [ | ||
| 263 | join(dirname(pkgJson), "bin", binary), | ||
| 264 | join(dirname(pkgJson), binary), | ||
| 265 | ]) { | ||
| 266 | if (existsSync(candidate)) return candidate; | ||
| 267 | } | ||
| 268 | } catch { | ||
| 269 | const packageRoot = join( | ||
| 270 | repoRoot, | ||
| 271 | `node_modules/.bun/${pkg.replace("/", "+")}@${WORKERD_VERSION}/node_modules/${pkg}`, | ||
| 272 | ); | ||
| 273 | for (const candidate of [join(packageRoot, "bin", binary), join(packageRoot, binary)]) { | ||
| 274 | if (existsSync(candidate)) return candidate; | ||
| 275 | } | ||
| 276 | } | ||
| 277 | return null; | ||
| 278 | }; | ||
| 279 | |||
| 214 | // --------------------------------------------------------------------------- | 280 | // --------------------------------------------------------------------------- |
| 215 | // Build mode | 281 | // Build mode |
| 216 | // --------------------------------------------------------------------------- | 282 | // --------------------------------------------------------------------------- |
| @@ -333,6 +399,8 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { · 116 unmodified lines | |||
| 333 | 399 | ||
| 334 | const quickJsWasmPath = resolveQuickJsWasmPath(); | 400 | const quickJsWasmPath = resolveQuickJsWasmPath(); |
| 335 | const onePasswordCoreWasmPath = resolveOnePasswordCoreWasmPath(); | 401 | const onePasswordCoreWasmPath = resolveOnePasswordCoreWasmPath(); |
| 402 | const workerBundlerDistPath = resolveWorkerBundlerDistPath(); | ||
| 403 | const packedWorkerBundlerSource = await createPackedWorkerBundlerSource(workerBundlerDistPath); | ||
| 336 | 404 | ||
| 337 | try { | 405 | try { |
| 338 | for (const target of targets) { | 406 | for (const target of targets) { |
| @@ -375,12 +443,35 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { · 36 unmodified lines | |||
| 375 | await cp(libsqlNative, join(binDir, "libsql.node")); | 443 | await cp(libsqlNative, join(binDir, "libsql.node")); |
| 376 | } | 444 | } |
| 377 | 445 | ||
| 446 | const workerdBinary = resolveWorkerdBinary(target); | ||
| 447 | if (workerdBinary && existsSync(workerdBinary)) { | ||
| 448 | await cp(workerdBinary, join(binDir, workerdBinaryName(target))); | ||
| 449 | if (target.os !== "win32") await chmod(join(binDir, workerdBinaryName(target)), 0o755); | ||
| 450 | } | ||
| 451 | |||
| 452 | await cp(workerBundlerDistPath, join(binDir, WORKER_BUNDLER_DIRNAME, "dist"), { | ||
| 453 | recursive: true, | ||
| 454 | }); | ||
| 455 | await writeFile( | ||
| 456 | join(binDir, WORKER_BUNDLER_DIRNAME, "dist", "index.bundled.js"), | ||
| 457 | packedWorkerBundlerSource, | ||
| 458 | ); | ||
| 459 | |||
| 378 | // Smoke test on current platform | 460 | // Smoke test on current platform |
| 379 | if (isCurrentPlatform(target)) { | 461 | if (isCurrentPlatform(target)) { |
| 380 | const bin = join(binDir, binaryName(target)); | 462 | const bin = join(binDir, binaryName(target)); |
| 381 | console.log(` Smoke test: ${bin} --version`); | 463 | console.log(` Smoke test: ${bin} --version`); |
| 382 | const version = await $`${bin} --version`.text(); | 464 | const version = await $`${bin} --version`.text(); |
| 383 | console.log(` OK: ${version.trim()}`); | 465 | console.log(` OK: ${version.trim()}`); |
| 466 | console.log(` Smoke test: packed apps source sync and invoke`); | ||
| 467 | const smoke = Bun.spawn( | ||
| 468 | ["bun", "run", join(cliRoot, "scripts/smoke-packed-apps.ts"), bin], | ||
| 469 | { | ||
| 470 | cwd: repoRoot, | ||
| 471 | stdio: ["ignore", "inherit", "inherit"], | ||
| 472 | }, | ||
| 473 | ); | ||
| 474 | if ((await smoke.exited) !== 0) throw new Error("packed apps smoke failed"); | ||
| 384 | } | 475 | } |
| 385 | 476 | ||
| 386 | // Variant package.json. All variants publish to the SAME npm package | 477 | // Variant package.json. All variants publish to the SAME npm package |
| @@ -1,11 +1,10 @@ | |||
| 1 | // --------------------------------------------------------------------------- | 1 | // --------------------------------------------------------------------------- |
| 2 | // Native-binding bootstrap for the `bun build --compile` binary. | 2 | // Native-binding bootstrap for the `bun build --compile` binary. |
| 3 | // | 3 | // |
| 4 | // `bun --compile` bundles JS into bunfs but does NOT include `.node` native | 4 | // `bun --compile` bundles JS into bunfs but does NOT include native binaries, |
| 5 | // addons, so a dynamic `require('@libsql/<platform>')` / keyring walk inside | 5 | // wasm sidecars, or runtime-read package files. build.ts copies those artifacts |
| 6 | // the binary fails. build.ts copies each platform's `.node` next to the | 6 | // next to the executable; here we publish their on-disk paths via env vars the |
| 7 | // executable (`libsql.node`, `keyring.node`); here we publish their on-disk | 7 | // loaders read. |
| 8 | // paths via env vars the loaders read. | ||
| 9 | // | 8 | // |
| 10 | // This MUST be the FIRST import in main.ts. ES modules evaluate every import | 9 | // This MUST be the FIRST import in main.ts. ES modules evaluate every import |
| 11 | // before the importer's own body, and libSQL resolves its native addon EAGERLY | 10 | // before the importer's own body, and libSQL resolves its native addon EAGERLY |
| @@ -43,3 +42,18 @@ if ( · 31 unmodified lines | |||
| 43 | ) { | 42 | ) { |
| 44 | process.env.EXECUTOR_KEYRING_NATIVE_PATH = keyringNodeOnDisk; | 43 | process.env.EXECUTOR_KEYRING_NATIVE_PATH = keyringNodeOnDisk; |
| 45 | } | 44 | } |
| 45 | |||
| 46 | const workerdOnDisk = join(execDir, process.platform === "win32" ? "workerd.exe" : "workerd"); | ||
| 47 | if (typeof Bun !== "undefined" && !process.env.EXECUTOR_WORKERD_BIN && existsSync(workerdOnDisk)) { | ||
| 48 | process.env.EXECUTOR_WORKERD_BIN = workerdOnDisk; | ||
| 49 | } | ||
| 50 | |||
| 51 | const workerBundlerOnDisk = join(execDir, "worker-bundler"); | ||
| 52 | if ( | ||
| 53 | typeof Bun !== "undefined" && | ||
| 54 | !process.env.EXECUTOR_WORKER_BUNDLER_DIR && | ||
| 55 | existsSync(join(workerBundlerOnDisk, "dist", "index.js")) && | ||
| 56 | existsSync(join(workerBundlerOnDisk, "dist", "esbuild.wasm")) | ||
| 57 | ) { | ||
| 58 | process.env.EXECUTOR_WORKER_BUNDLER_DIR = workerBundlerOnDisk; | ||
| 59 | } | ||