Unified · Split
| @@ -83,7 +83,7 @@ executor web · 82 unmodified lines | ||
| 83 | 83 | Uninstall: |
| 84 | 84 | |
| 85 | 85 | ```bash |
| 86 | −bash executor/uninstall --yes | |
| 86 | +executor uninstall --yes | |
| 87 | 87 | ``` |
| 88 | 88 | |
| 89 | 89 | Default managed-runtime ports: |
| @@ -164,7 +164,7 @@ export function ToolsView({ · 163 unmodified lines | ||
| 164 | 164 | {debug ? ( |
| 165 | 165 | <div className="space-y-0.5"> |
| 166 | 166 | <p className="text-[10px] font-mono text-muted-foreground/90"> |
| 167 | − debug mode={debug.mode} · cacheHit={String(debug.cacheHit)} · cacheFresh={String(debug.cacheFresh)} · duration={debug.durationMs}ms · sources={debug.normalizedSourceCount}/{debug.sourceCount} | |
| 167 | + debug mode={debug.mode} · cacheHit={String(debug.cacheHit)} · cacheFresh={String(debug.cacheFresh)} · skipCacheRead={String(debug.skipCacheRead)} · duration={debug.durationMs}ms · sources={debug.normalizedSourceCount}/{debug.sourceCount} | |
| 168 | 168 | {debug.timedOutSources.length > 0 ? ` · timedOut=${debug.timedOutSources.join(",")}` : " · timedOut=none"} |
| 169 | 169 | </p> |
| 170 | 170 | <p className="text-[10px] font-mono text-muted-foreground/80 truncate" title={debug.trace.join(" | ")}> |
| @@ -23,6 +23,7 @@ interface WorkspaceToolsQueryResult { · 22 unmodified lines | ||
| 23 | 23 | mode: "cache-fresh" | "cache-stale" | "rebuild"; |
| 24 | 24 | includeDts: boolean; |
| 25 | 25 | sourceTimeoutMs: number | null; |
| 26 | + skipCacheRead: boolean; | |
| 26 | 27 | sourceCount: number; |
| 27 | 28 | normalizedSourceCount: number; |
| 28 | 29 | cacheHit: boolean; |
| @@ -96,6 +97,7 @@ export function useWorkspaceTools(context: WorkspaceContext | null) { · 67 unmodified lines | ||
| 96 | 97 | durationMs: inventoryData.debug.durationMs, |
| 97 | 98 | cacheHit: inventoryData.debug.cacheHit, |
| 98 | 99 | cacheFresh: inventoryData.debug.cacheFresh, |
| 100 | + skipCacheRead: inventoryData.debug.skipCacheRead, | |
| 99 | 101 | sourceCount: inventoryData.debug.sourceCount, |
| 100 | 102 | normalizedSourceCount: inventoryData.debug.normalizedSourceCount, |
| 101 | 103 | timedOutSources: inventoryData.debug.timedOutSources, |
| @@ -1,6 +1,8 @@ | ||
| 1 | 1 | #!/usr/bin/env bun |
| 2 | 2 | |
| 3 | 3 | import fs from "node:fs/promises"; |
| 4 | +import os from "node:os"; | |
| 5 | +import path from "node:path"; | |
| 4 | 6 | import { managedRuntimeDiagnostics, runManagedBackend, runManagedWeb } from "./packages/core/src/managed-runtime"; |
| 5 | 7 | |
| 6 | 8 | function printHelp(): void { |
| @@ -11,12 +13,14 @@ Usage: · 4 unmodified lines | ||
| 11 | 13 | executor up [backend-args] |
| 12 | 14 | executor backend <args> |
| 13 | 15 | executor web [--port <number>] |
| 16 | + executor uninstall [--yes] | |
| 14 | 17 | |
| 15 | 18 | Commands: |
| 16 | 19 | doctor Bootstrap and verify managed Convex backend runtime |
| 17 | 20 | up Run managed backend and auto-bootstrap Convex functions |
| 18 | 21 | backend Pass through arguments to managed convex-local-backend binary |
| 19 | 22 | web Run packaged web UI (default port: 5312) |
| 23 | + uninstall Remove local managed runtime install | |
| 20 | 24 | `); |
| 21 | 25 | } |
| 22 | 26 | |
| @@ -61,6 +65,73 @@ async function checkHttp(url: string): Promise<boolean> { · 38 unmodified lines | ||
| 61 | 65 | } |
| 62 | 66 | } |
| 63 | 67 | |
| 68 | +async function runUninstall(args: string[]): Promise<number> { | |
| 69 | + let assumeYes = false; | |
| 70 | + let index = 0; | |
| 71 | + | |
| 72 | + while (index < args.length) { | |
| 73 | + const arg = args[index]; | |
| 74 | + if (arg === "-y" || arg === "--yes") { | |
| 75 | + assumeYes = true; | |
| 76 | + index += 1; | |
| 77 | + continue; | |
| 78 | + } | |
| 79 | + | |
| 80 | + if (arg === "-h" || arg === "--help") { | |
| 81 | + console.log(`Usage: | |
| 82 | + executor uninstall [--yes] | |
| 83 | + | |
| 84 | +Options: | |
| 85 | + -y, --yes Skip confirmation prompt | |
| 86 | + -h, --help Show this help`); | |
| 87 | + return 0; | |
| 88 | + } | |
| 89 | + | |
| 90 | + console.log(`Unknown option: ${arg}`); | |
| 91 | + return 1; | |
| 92 | + } | |
| 93 | + | |
| 94 | + const installDir = Bun.env.EXECUTOR_INSTALL_DIR ?? path.join(os.homedir(), ".executor", "bin"); | |
| 95 | + const runtimeDir = Bun.env.EXECUTOR_RUNTIME_DIR ?? path.join(os.homedir(), ".executor", "runtime"); | |
| 96 | + const homeDir = Bun.env.EXECUTOR_HOME_DIR ?? path.join(os.homedir(), ".executor"); | |
| 97 | + | |
| 98 | + if (!assumeYes) { | |
| 99 | + console.log("This will remove:"); | |
| 100 | + console.log(` - ${installDir}/executor`); | |
| 101 | + console.log(` - ${runtimeDir}`); | |
| 102 | + const response = prompt("Continue? [y/N] "); | |
| 103 | + if (response === null || response.toLowerCase() !== "y") { | |
| 104 | + console.log("Cancelled."); | |
| 105 | + return 0; | |
| 106 | + } | |
| 107 | + } | |
| 108 | + | |
| 109 | + await fs.rm(path.join(installDir, "executor"), { force: true }); | |
| 110 | + await fs.rm(runtimeDir, { recursive: true, force: true }); | |
| 111 | + | |
| 112 | + if (await pathExists(installDir)) { | |
| 113 | + try { | |
| 114 | + await fs.rmdir(installDir); | |
| 115 | + } catch { | |
| 116 | + // keep if it is not empty | |
| 117 | + } | |
| 118 | + } | |
| 119 | + | |
| 120 | + if (await pathExists(homeDir)) { | |
| 121 | + try { | |
| 122 | + await fs.rmdir(homeDir); | |
| 123 | + } catch { | |
| 124 | + // keep if other files remain | |
| 125 | + } | |
| 126 | + } | |
| 127 | + | |
| 128 | + console.log("Executor uninstall complete."); | |
| 129 | + console.log(""); | |
| 130 | + console.log("If you previously added PATH manually, remove this line from your shell rc:"); | |
| 131 | + console.log(` export PATH=${installDir}:$PATH`); | |
| 132 | + return 0; | |
| 133 | +} | |
| 134 | + | |
| 64 | 135 | async function run(): Promise<void> { |
| 65 | 136 | const [command, ...rest] = process.argv.slice(2); |
| 66 | 137 | |
| @@ -110,6 +181,11 @@ async function run(): Promise<void> { · 43 unmodified lines | ||
| 110 | 181 | process.exit(exitCode); |
| 111 | 182 | } |
| 112 | 183 | |
| 184 | + if (command === "uninstall") { | |
| 185 | + const exitCode = await runUninstall(rest); | |
| 186 | + process.exit(exitCode); | |
| 187 | + } | |
| 188 | + | |
| 113 | 189 | throw new Error(`Unknown command: ${command}`); |
| 114 | 190 | } |
| 115 | 191 | |
| @@ -269,6 +269,7 @@ echo " executor up" · 268 unmodified lines | ||
| 269 | 269 | echo " executor web # defaults to http://localhost:5312" |
| 270 | 270 | echo "" |
| 271 | 271 | echo "To uninstall later:" |
| 272 | +echo " executor uninstall --yes" | |
| 272 | 273 | echo " bash executor/uninstall --yes" |
| 273 | 274 | echo "" |
| 274 | 275 | show_star_prompt |
| @@ -48,7 +48,7 @@ export const listToolsWithWarnings = action({ · 47 unmodified lines | ||
| 48 | 48 | workspaceId: args.workspaceId, |
| 49 | 49 | actorId: canonicalActorId, |
| 50 | 50 | clientId: args.clientId, |
| 51 | − }, { includeDts: false, sourceTimeoutMs: 2_500, allowStaleOnMismatch: true }); | |
| 51 | + }, { includeDts: false, sourceTimeoutMs: 2_500, allowStaleOnMismatch: true, skipCacheRead: true }); | |
| 52 | 52 | |
| 53 | 53 | if (inventory.warnings.some((warning) => warning.includes("showing previous results while refreshing"))) { |
| 54 | 54 | try { |
| @@ -39,6 +39,7 @@ export interface WorkspaceToolsDebug { · 38 unmodified lines | ||
| 39 | 39 | mode: "cache-fresh" | "cache-stale" | "rebuild"; |
| 40 | 40 | includeDts: boolean; |
| 41 | 41 | sourceTimeoutMs: number | null; |
| 42 | + skipCacheRead: boolean; | |
| 42 | 43 | sourceCount: number; |
| 43 | 44 | normalizedSourceCount: number; |
| 44 | 45 | cacheHit: boolean; |
| @@ -52,6 +53,7 @@ interface GetWorkspaceToolsOptions { · 7 unmodified lines | ||
| 52 | 53 | includeDts?: boolean; |
| 53 | 54 | sourceTimeoutMs?: number; |
| 54 | 55 | allowStaleOnMismatch?: boolean; |
| 56 | + skipCacheRead?: boolean; | |
| 55 | 57 | } |
| 56 | 58 | |
| 57 | 59 | export interface WorkspaceToolInventory { |
| @@ -133,6 +135,7 @@ export async function getWorkspaceTools( · 75 unmodified lines | ||
| 133 | 135 | const includeDts = options.includeDts ?? false; |
| 134 | 136 | const sourceTimeoutMs = options.sourceTimeoutMs; |
| 135 | 137 | const allowStaleOnMismatch = options.allowStaleOnMismatch ?? false; |
| 138 | + const skipCacheRead = options.skipCacheRead ?? false; | |
| 136 | 139 | const sources = (await ctx.runQuery(internal.database.listToolSources, { workspaceId })) |
| 137 | 140 | .filter((source: { enabled: boolean }) => source.enabled); |
| 138 | 141 | traceStep("listToolSources", listSourcesStartedAt); |
| @@ -141,10 +144,12 @@ export async function getWorkspaceTools( · 2 unmodified lines | ||
| 141 | 144 | const debugBase: Omit<WorkspaceToolsDebug, "mode" | "normalizedSourceCount" | "cacheHit" | "cacheFresh" | "timedOutSources" | "durationMs" | "trace"> = { |
| 142 | 145 | includeDts, |
| 143 | 146 | sourceTimeoutMs: sourceTimeoutMs ?? null, |
| 147 | + skipCacheRead, | |
| 144 | 148 | sourceCount: sources.length, |
| 145 | 149 | }; |
| 146 | 150 | |
| 147 | − try { | |
| 151 | + if (!skipCacheRead) { | |
| 152 | + try { | |
| 148 | 153 | const cacheReadStartedAt = Date.now(); |
| 149 | 154 | const cacheEntry = await ctx.runQuery(internal.workspaceToolCache.getEntry, { |
| 150 | 155 | workspaceId, |
| @@ -202,9 +207,12 @@ export async function getWorkspaceTools( · 51 unmodified lines | ||
| 202 | 207 | } |
| 203 | 208 | } |
| 204 | 209 | } |
| 205 | − } catch (error) { | |
| 206 | − const msg = error instanceof Error ? error.message : String(error); | |
| 207 | − console.warn(`[executor] workspace tool cache read failed for '${workspaceId}': ${msg}`); | |
| 210 | + } catch (error) { | |
| 211 | + const msg = error instanceof Error ? error.message : String(error); | |
| 212 | + console.warn(`[executor] workspace tool cache read failed for '${workspaceId}': ${msg}`); | |
| 213 | + } | |
| 214 | + } else { | |
| 215 | + trace.push("cacheEntryLookup=skipped"); | |
| 208 | 216 | } |
| 209 | 217 | |
| 210 | 218 | const configs: ExternalToolSourceConfig[] = []; |
| @@ -364,13 +372,24 @@ export async function getWorkspaceTools( · 153 unmodified lines | ||
| 364 | 372 | export async function loadWorkspaceToolInventoryForContext( |
| 365 | 373 | ctx: ActionCtx, |
| 366 | 374 | context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }, |
| 367 | − options: { includeDts?: boolean; sourceTimeoutMs?: number; allowStaleOnMismatch?: boolean } = {}, | |
| 375 | + options: { | |
| 376 | + includeDts?: boolean; | |
| 377 | + sourceTimeoutMs?: number; | |
| 378 | + allowStaleOnMismatch?: boolean; | |
| 379 | + skipCacheRead?: boolean; | |
| 380 | + } = {}, | |
| 368 | 381 | ): Promise<WorkspaceToolInventory> { |
| 369 | 382 | const includeDts = options.includeDts ?? false; |
| 370 | 383 | const sourceTimeoutMs = options.sourceTimeoutMs; |
| 371 | 384 | const allowStaleOnMismatch = options.allowStaleOnMismatch; |
| 385 | + const skipCacheRead = options.skipCacheRead; | |
| 372 | 386 | const [result, policies] = await Promise.all([ |
| 373 | − getWorkspaceTools(ctx, context.workspaceId, { includeDts, sourceTimeoutMs, allowStaleOnMismatch }), | |
| 387 | + getWorkspaceTools(ctx, context.workspaceId, { | |
| 388 | + includeDts, | |
| 389 | + sourceTimeoutMs, | |
| 390 | + allowStaleOnMismatch, | |
| 391 | + skipCacheRead, | |
| 392 | + }), | |
| 374 | 393 | ctx.runQuery(internal.database.listAccessPolicies, { workspaceId: context.workspaceId }), |
| 375 | 394 | ]); |
| 376 | 395 | const typedPolicies = policies as AccessPolicyRecord[]; |
| @@ -405,7 +424,12 @@ export async function loadWorkspaceToolInventoryForContext( · 28 unmodified lines | ||
| 405 | 424 | export async function listToolsForContext( |
| 406 | 425 | ctx: ActionCtx, |
| 407 | 426 | context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }, |
| 408 | − options: { includeDts?: boolean; sourceTimeoutMs?: number; allowStaleOnMismatch?: boolean } = {}, | |
| 427 | + options: { | |
| 428 | + includeDts?: boolean; | |
| 429 | + sourceTimeoutMs?: number; | |
| 430 | + allowStaleOnMismatch?: boolean; | |
| 431 | + skipCacheRead?: boolean; | |
| 432 | + } = {}, | |
| 409 | 433 | ): Promise<ToolDescriptor[]> { |
| 410 | 434 | const inventory = await loadWorkspaceToolInventoryForContext(ctx, context, options); |
| 411 | 435 | return inventory.tools; |
| @@ -414,7 +438,12 @@ export async function listToolsForContext( · 2 unmodified lines | ||
| 414 | 438 | export async function listToolsWithWarningsForContext( |
| 415 | 439 | ctx: ActionCtx, |
| 416 | 440 | context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }, |
| 417 | − options: { includeDts?: boolean; sourceTimeoutMs?: number; allowStaleOnMismatch?: boolean } = {}, | |
| 441 | + options: { | |
| 442 | + includeDts?: boolean; | |
| 443 | + sourceTimeoutMs?: number; | |
| 444 | + allowStaleOnMismatch?: boolean; | |
| 445 | + skipCacheRead?: boolean; | |
| 446 | + } = {}, | |
| 418 | 447 | ): Promise<{ |
| 419 | 448 | tools: ToolDescriptor[]; |
| 420 | 449 | warnings: string[]; |
| @@ -33,6 +33,67 @@ async function generateSelfHostedAdminKey(info: ManagedRuntimeInfo): Promise<str · 32 unmodified lines | ||
| 33 | 33 | return parsed.adminKey; |
| 34 | 34 | } |
| 35 | 35 | |
| 36 | +async function hasAnyPath(paths: string[]): Promise<boolean> { | |
| 37 | + for (const candidate of paths) { | |
| 38 | + if (await pathExists(candidate)) { | |
| 39 | + return true; | |
| 40 | + } | |
| 41 | + } | |
| 42 | + return false; | |
| 43 | +} | |
| 44 | + | |
| 45 | +function getConfigCandidates(baseDir: string): string[] { | |
| 46 | + return [ | |
| 47 | + path.join(baseDir, "convex.config.ts"), | |
| 48 | + path.join(baseDir, "convex.config.js"), | |
| 49 | + path.join(baseDir, "convex.config.mts"), | |
| 50 | + path.join(baseDir, "convex.config.mjs"), | |
| 51 | + path.join(baseDir, "convex.config.cts"), | |
| 52 | + path.join(baseDir, "convex.config.cjs"), | |
| 53 | + ]; | |
| 54 | +} | |
| 55 | + | |
| 56 | +async function getConfiguredFunctionsPath(candidate: string): Promise<string | null> { | |
| 57 | + try { | |
| 58 | + const raw = await fs.readFile(path.join(candidate, "convex.json"), "utf8"); | |
| 59 | + const parsed = JSON.parse(raw) as { functions?: unknown }; | |
| 60 | + if (typeof parsed.functions !== "string") { | |
| 61 | + return null; | |
| 62 | + } | |
| 63 | + const value = parsed.functions.trim(); | |
| 64 | + return value.length > 0 ? value : null; | |
| 65 | + } catch { | |
| 66 | + return null; | |
| 67 | + } | |
| 68 | +} | |
| 69 | + | |
| 70 | +async function hasConvexProjectConfig(candidate: string): Promise<boolean> { | |
| 71 | + const convexJson = path.join(candidate, "convex.json"); | |
| 72 | + if (!(await pathExists(convexJson))) { | |
| 73 | + return false; | |
| 74 | + } | |
| 75 | + | |
| 76 | + const legacyDir = path.join(candidate, "convex"); | |
| 77 | + const legacyMatches = await hasAnyPath(getConfigCandidates(legacyDir)); | |
| 78 | + if (legacyMatches) { | |
| 79 | + return true; | |
| 80 | + } | |
| 81 | + | |
| 82 | + if (await hasAnyPath(getConfigCandidates(candidate))) { | |
| 83 | + return true; | |
| 84 | + } | |
| 85 | + | |
| 86 | + const configuredFunctionsPath = await getConfiguredFunctionsPath(candidate); | |
| 87 | + if (!configuredFunctionsPath) { | |
| 88 | + return false; | |
| 89 | + } | |
| 90 | + | |
| 91 | + const resolvedFunctionsPath = path.isAbsolute(configuredFunctionsPath) | |
| 92 | + ? configuredFunctionsPath | |
| 93 | + : path.resolve(candidate, configuredFunctionsPath); | |
| 94 | + return await hasAnyPath(getConfigCandidates(resolvedFunctionsPath)); | |
| 95 | +} | |
| 96 | + | |
| 36 | 97 | async function findProjectDir(): Promise<string | null> { |
| 37 | 98 | const roots = [ |
| 38 | 99 | Bun.env.EXECUTOR_PROJECT_DIR, |
| @@ -47,10 +108,7 @@ async function findProjectDir(): Promise<string | null> { · 8 unmodified lines | ||
| 47 | 108 | } |
| 48 | 109 | |
| 49 | 110 | for (const candidate of candidates) { |
| 50 | − const convexDir = path.join(candidate, "convex"); | |
| 51 | − const convexConfig = path.join(convexDir, "convex.config.ts"); | |
| 52 | − const convexJson = path.join(candidate, "convex.json"); | |
| 53 | − if ((await pathExists(convexDir)) && (await pathExists(convexConfig)) && (await pathExists(convexJson))) { | |
| 111 | + if (await hasConvexProjectConfig(candidate)) { | |
| 54 | 112 | return candidate; |
| 55 | 113 | } |
| 56 | 114 | } |
| @@ -74,11 +74,45 @@ export async function runManagedBackend(args: string[]): Promise<number> { · 73 unmodified lines | ||
| 74 | 74 | |
| 75 | 75 | export async function runManagedWeb(options: { port?: number } = {}): Promise<number> { |
| 76 | 76 | const info = await ensureManagedRuntime(); |
| 77 | + let managedBackend: ReturnType<typeof Bun.spawn> | null = null; | |
| 78 | + let startedManagedBackend = false; | |
| 79 | + | |
| 77 | 80 | try { |
| 78 | − await waitForBackendReady(info, 3_000); | |
| 79 | − await ensureProjectBootstrapped(info); | |
| 81 | + try { | |
| 82 | + await waitForBackendReady(info, 3_000); | |
| 83 | + } catch { | |
| 84 | + const backendEnv = { | |
| 85 | + ...process.env, | |
| 86 | + PATH: `${path.dirname(info.nodeBin)}:${process.env.PATH ?? ""}`, | |
| 87 | + }; | |
| 88 | + | |
| 89 | + try { | |
| 90 | + managedBackend = Bun.spawn([info.backendBinary, ...backendArgs(info, [])], { | |
| 91 | + env: backendEnv, | |
| 92 | + stdin: "ignore", | |
| 93 | + stdout: "inherit", | |
| 94 | + stderr: "inherit", | |
| 95 | + }); | |
| 96 | + startedManagedBackend = true; | |
| 97 | + await waitForBackendReady(info); | |
| 98 | + } catch (error) { | |
| 99 | + const message = error instanceof Error ? error.message : String(error); | |
| 100 | + console.warn(`[executor] could not auto-start managed backend: ${message}`); | |
| 101 | + } | |
| 102 | + } | |
| 103 | + | |
| 104 | + if (startedManagedBackend) { | |
| 105 | + console.log("[executor] started managed Convex backend for web UI"); | |
| 106 | + } | |
| 107 | + | |
| 108 | + try { | |
| 109 | + await ensureProjectBootstrapped(info); | |
| 110 | + } catch (error) { | |
| 111 | + const message = error instanceof Error ? error.message : String(error); | |
| 112 | + console.warn(`[executor] failed to auto-bootstrap Convex functions: ${message}`); | |
| 113 | + } | |
| 80 | 114 | } catch { |
| 81 | − // Backend not running yet; web can still start and retry queries later. | |
| 115 | + // Backend unavailable for now; web can still start and retry queries later. | |
| 82 | 116 | } |
| 83 | 117 | await ensureNodeRuntime(info); |
| 84 | 118 | await ensureWebBundle(info); |
| @@ -99,6 +133,12 @@ export async function runManagedWeb(options: { port?: number } = {}): Promise<nu · 14 unmodified lines | ||
| 99 | 133 | cwd: info.webDir, |
| 100 | 134 | env, |
| 101 | 135 | }); |
| 136 | + | |
| 137 | + if (managedBackend && startedManagedBackend) { | |
| 138 | + managedBackend.kill(); | |
| 139 | + await managedBackend.exited; | |
| 140 | + } | |
| 141 | + | |
| 102 | 142 | return proc.exitCode; |
| 103 | 143 | } |
| 104 | 144 | |