Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"@effect/platform-bun": "catalog:",
"@executor/api": "workspace:*",
"@executor/local": "workspace:*",
"@executor/plugin-launchd": "workspace:*",
"@executor/runtime-quickjs": "workspace:*",
"@executor/supervisor": "workspace:*",
"@jitl/quickjs-wasmfile-release-sync": "catalog:",
"effect": "catalog:",
"quickjs-emscripten": "catalog:"
Expand Down
9 changes: 9 additions & 0 deletions apps/cli/src/build-globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Build-time global injected by `Bun.build({ define: { BUILD_PLATFORM: ... } })`
* in `apps/cli/src/build.ts`. When absent (e.g. during `bun run dev`), the
* consuming code must fall back to `process.platform` via a `typeof` check.
*
* The value is one of `"darwin"`, `"linux"`, or `"win32"` — matching
* `target.os` in the compile script.
*/
declare const BUILD_PLATFORM: "darwin" | "linux" | "win32" | undefined;
15 changes: 14 additions & 1 deletion apps/cli/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,20 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => {

await Bun.build({
entrypoints: [join(cliRoot, "src/main.ts")],
minify: mode === "production",
// `syntax: true` is unconditional because the platform-gated plugin
// switch in `main.ts` and `apps/local/src/server/executor.ts` relies
// on Bun's DCE to fold the ternary and tree-shake the unused plugin.
// Identifier/whitespace minification stays gated on production.
minify: {
syntax: true,
whitespace: mode === "production",
identifiers: mode === "production",
},
// Inject the compile target's OS as `BUILD_PLATFORM` so the ternary
// in the platform switch folds to a single branch at bundle time.
define: {
BUILD_PLATFORM: JSON.stringify(target.os),
},
compile: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Bun compile target string is dynamically constructed
target: bunTarget(target) as any,
Expand Down
49 changes: 37 additions & 12 deletions apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ if (typeof Bun !== "undefined" && (await Bun.file(wasmOnDisk).exists())) {
type: "sync" as const,
importFFI: () =>
import("@jitl/quickjs-wasmfile-release-sync/ffi").then(
(m: Record<string, unknown>) => m.QuickJSFFI,
(m: unknown) => (m as { readonly QuickJSFFI: unknown }).QuickJSFFI,
),
importModuleLoader: () =>
import("@jitl/quickjs-wasmfile-release-sync/emscripten-module").then(
(m: Record<string, unknown>) => {
const original = m.default as (...args: unknown[]) => unknown;
return (moduleArg: Record<string, unknown> = {}) =>
original({ ...moduleArg, wasmBinary });
},
),
import("@jitl/quickjs-wasmfile-release-sync/emscripten-module").then((m: unknown) => {
const original = (
m as {
readonly default: (moduleArg?: Readonly<Record<string, unknown>>) => unknown;
}
).default;
return (moduleArg: Readonly<Record<string, unknown>> = {}) =>
original({ ...moduleArg, wasmBinary });
}),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- quickjs-emscripten variant type is not publicly exported
const mod = await newQuickJSWASMModule(variant as any);
const mod = await newQuickJSWASMModule(
variant as unknown as Parameters<typeof newQuickJSWASMModule>[0],
);
setQuickJSModule(mod);
}

Expand All @@ -41,6 +44,8 @@ import * as Cause from "effect/Cause";

import { ExecutorApi } from "@executor/api";
import { startServer, runMcpStdioServer, getExecutor } from "@executor/local";
import { makeServiceCommand, makeUnsupportedPlatformSupervisor } from "@executor/supervisor";
import { makeLaunchdSupervisorLayer } from "@executor/plugin-launchd";

// Embedded web UI — baked into compiled binaries via `with { type: "file" }`
import embeddedWebUI from "./embedded-web-ui.gen";
Expand Down Expand Up @@ -206,7 +211,9 @@ const callCommand = Command.make(
}
} else {
console.log(result.text);
const executionId = (result.structured as Record<string, unknown> | undefined)?.executionId;
const structured = result.structured as { readonly executionId?: unknown } | undefined;
const executionId =
typeof structured?.executionId === "string" ? structured.executionId : undefined;
if (executionId) {
console.log(
`\nTo resume:\n ${cliPrefix} resume --execution-id ${executionId} --action accept`,
Expand Down Expand Up @@ -282,8 +289,25 @@ const mcpCommand = Command.make("mcp", { scope }, ({ scope }) =>
// Root command
// ---------------------------------------------------------------------------

// Platform-gated PlatformSupervisor layer. The ternary discriminator must use
// `BUILD_PLATFORM` directly (not via an intermediate const) so Bun's DCE can
// fold the switch and tree-shake the unused plugin import at build time.
const platformSupervisorLayer =
(typeof BUILD_PLATFORM !== "undefined" ? BUILD_PLATFORM : process.platform) === "darwin"
? makeLaunchdSupervisorLayer()
: makeUnsupportedPlatformSupervisor({
platform:
typeof BUILD_PLATFORM !== "undefined" ? BUILD_PLATFORM : (process.platform as string),
});

const root = Command.make("executor").pipe(
Command.withSubcommands([callCommand, resumeCommand, webCommand, mcpCommand] as const),
Command.withSubcommands([
callCommand,
resumeCommand,
webCommand,
mcpCommand,
makeServiceCommand(),
] as const),
Command.withDescription("Executor local CLI"),
);

Expand All @@ -303,6 +327,7 @@ if (process.argv.includes("-v")) {
}

const program = runCli(process.argv).pipe(
Effect.provide(platformSupervisorLayer),
Effect.catchAllCause((cause) =>
Effect.sync(() => {
console.error(Cause.pretty(cause));
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
"typecheck": "tsgo --noEmit",
"typecheck:slow": "tsc --noEmit"
},
"dependencies": {},
"dependencies": {
"@executor/supervisor": "workspace:*",
"effect": "catalog:"
},
"devDependencies": {
"@types/node": "catalog:",
"electron": "35.7.5",
Expand Down
65 changes: 28 additions & 37 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
appendFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { Effect } from "effect";
import { gracefulStopPid, pollReadiness } from "@executor/supervisor";

// ---------------------------------------------------------------------------
// Constants
Expand Down Expand Up @@ -193,37 +195,22 @@ const resolveServerCommand = (): { command: string; args: string[] } => {
throw new Error(`Sidecar binary not found at ${sidecar}`);
};

const isServerReady = async (port: number): Promise<boolean> => {
try {
const res = await fetch(`http://127.0.0.1:${port}/docs`, {
signal: AbortSignal.timeout(2000),
});
return res.ok;
} catch {
return false;
}
};

const stopServer = (): Promise<void> => {
return new Promise((resolve) => {
if (!serverProcess) {
resolve();
return;
}
const proc = serverProcess;
const stopServer = async (): Promise<void> => {
const proc = serverProcess;
if (!proc?.pid) {
serverProcess = null;
return;
}

proc.once("exit", () => resolve());
proc.kill("SIGTERM");

// Force kill after 5s
setTimeout(() => {
try {
proc.kill("SIGKILL");
} catch {}
resolve();
}, 5000);
});
const pid = proc.pid;
serverProcess = null;
await Effect.runPromise(
gracefulStopPid(pid, {
signals: ["SIGTERM"],
signalDelayMs: 5_000,
killAfterMs: 5_000,
}),
);
};

const startServer = async (scopePath: string, port: number): Promise<void> => {
Expand Down Expand Up @@ -261,14 +248,18 @@ const startServer = async (scopePath: string, port: number): Promise<void> => {
app.quit();
});

// Wait for server to become ready
const deadline = Date.now() + SERVER_STARTUP_TIMEOUT_MS;
while (Date.now() < deadline) {
if (await isServerReady(port)) return;
await new Promise((r) => setTimeout(r, 200));
}

throw new Error(`Server failed to start within ${SERVER_STARTUP_TIMEOUT_MS / 1000}s`);
await Effect.runPromise(
pollReadiness(`http://127.0.0.1:${port}/docs`, {
timeoutMs: SERVER_STARTUP_TIMEOUT_MS,
intervalMs: 200,
}).pipe(
Effect.catchTag("ReadinessTimeout", () =>
Effect.fail(
new Error(`Server failed to start within ${SERVER_STARTUP_TIMEOUT_MS / 1000}s`),
),
),
),
);
};

// ---------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions apps/local/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
"@executor/plugin-google-discovery": "workspace:*",
"@executor/plugin-graphql": "workspace:*",
"@executor/plugin-keychain": "workspace:*",
"@executor/plugin-launchd": "workspace:*",
"@executor/plugin-mcp": "workspace:*",
"@executor/plugin-onepassword": "workspace:*",
"@executor/plugin-openapi": "workspace:*",
"@executor/react": "workspace:*",
"@executor/sdk": "workspace:*",
"@executor/storage-file": "workspace:*",
"@executor/supervisor": "workspace:*",
"@modelcontextprotocol/sdk": "^1.12.1",
"@tanstack/react-router": "catalog:",
"effect": "catalog:",
Expand Down
20 changes: 20 additions & 0 deletions apps/local/src/server/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@ import {
withConfigFile as withGraphqlConfigFile,
} from "@executor/plugin-graphql";
import { keychainPlugin } from "@executor/plugin-keychain";
import { makeLaunchdSupervisorLayer } from "@executor/plugin-launchd";
import { fileSecretsPlugin } from "@executor/plugin-file-secrets";
import { onepasswordPlugin } from "@executor/plugin-onepassword";
import { makeServiceToolsPlugin } from "@executor/supervisor";

// Build-time platform discriminator injected by `Bun.build({ define: ... })`
// in apps/cli/src/build.ts. Falls back to process.platform in dev mode so
// local `bun run dev` works without any build setup.
declare const BUILD_PLATFORM: "darwin" | "linux" | "win32" | undefined;

// ---------------------------------------------------------------------------
// Data directory
Expand Down Expand Up @@ -76,6 +83,19 @@ const createLocalPlugins = (
onepasswordPlugin({
kv: scopeKv(scopedKv, "onepassword"),
}),
// Platform-gated service management plugin. The ternary pattern — with
// `BUILD_PLATFORM` used directly at the branch discriminator, not through
// an intermediate const — is what Bun's DCE needs to fold the switch and
// tree-shake the unused plugin import at build time. Do not refactor this
// into `const p = ...; p === "darwin" ? ...` — that pattern does NOT fold.
...((typeof BUILD_PLATFORM !== "undefined" ? BUILD_PLATFORM : process.platform) === "darwin"
? ([
makeServiceToolsPlugin(makeLaunchdSupervisorLayer(), {
displayName: "macOS launchd",
backendKind: "launchd",
}),
] as const)
: ([] as const)),
] as const;

// Full typed executor — inferred from plugin list
Expand Down
44 changes: 44 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading