History

bump version

Rhys Sullivan committed 9 months ago

Commit ee5eda11 parent b65ac9e9 files · +224 −18Browse files

Unified · Split

9 files
  1. executor/README.md +1 −1
  2. executor/apps/web/src/components/tools/view.tsx +1 −1
  3. executor/apps/web/src/hooks/use-workspace-tools.ts +2 −0
  4. executor/executor.ts +76 −0
  5. executor/install +1 −0
  6. executor/packages/convex/executorNode.ts +1 −1
  7. executor/packages/convex/runtime/workspace_tools.ts +37 −8
  8. executor/packages/core/src/managed-runtime-bootstrap.ts +62 −4
  9. executor/packages/core/src/managed-runtime.ts +43 −3
executor/README.md+1−1
@@ -83,7 +83,7 @@ executor web · 82 unmodified lines
83Uninstall:83Uninstall:
84 84
85```bash85```bash
86bash executor/uninstall --yes86executor uninstall --yes
87```87```
88 88
89Default managed-runtime ports:89Default managed-runtime ports:
executor/apps/web/src/components/tools/view.tsx+1−1
@@ -164,7 +164,7 @@ export function ToolsView({ · 163 unmodified lines
164 {debug ? (164 {debug ? (
165 <div className="space-y-0.5">165 <div className="space-y-0.5">
166 <p className="text-[10px] font-mono text-muted-foreground/90">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 {debug.timedOutSources.length > 0 ? ` · timedOut=${debug.timedOutSources.join(",")}` : " · timedOut=none"}168 {debug.timedOutSources.length > 0 ? ` · timedOut=${debug.timedOutSources.join(",")}` : " · timedOut=none"}
169 </p>169 </p>
170 <p className="text-[10px] font-mono text-muted-foreground/80 truncate" title={debug.trace.join(" | ")}>170 <p className="text-[10px] font-mono text-muted-foreground/80 truncate" title={debug.trace.join(" | ")}>
executor/apps/web/src/hooks/use-workspace-tools.ts+2−0
@@ -23,6 +23,7 @@ interface WorkspaceToolsQueryResult { · 22 unmodified lines
23 mode: "cache-fresh" | "cache-stale" | "rebuild";23 mode: "cache-fresh" | "cache-stale" | "rebuild";
24 includeDts: boolean;24 includeDts: boolean;
25 sourceTimeoutMs: number | null;25 sourceTimeoutMs: number | null;
26 skipCacheRead: boolean;
26 sourceCount: number;27 sourceCount: number;
27 normalizedSourceCount: number;28 normalizedSourceCount: number;
28 cacheHit: boolean;29 cacheHit: boolean;
@@ -96,6 +97,7 @@ export function useWorkspaceTools(context: WorkspaceContext | null) { · 67 unmodified lines
96 durationMs: inventoryData.debug.durationMs,97 durationMs: inventoryData.debug.durationMs,
97 cacheHit: inventoryData.debug.cacheHit,98 cacheHit: inventoryData.debug.cacheHit,
98 cacheFresh: inventoryData.debug.cacheFresh,99 cacheFresh: inventoryData.debug.cacheFresh,
100 skipCacheRead: inventoryData.debug.skipCacheRead,
99 sourceCount: inventoryData.debug.sourceCount,101 sourceCount: inventoryData.debug.sourceCount,
100 normalizedSourceCount: inventoryData.debug.normalizedSourceCount,102 normalizedSourceCount: inventoryData.debug.normalizedSourceCount,
101 timedOutSources: inventoryData.debug.timedOutSources,103 timedOutSources: inventoryData.debug.timedOutSources,
executor/executor.ts+76−0
@@ -1,6 +1,8 @@
1#!/usr/bin/env bun1#!/usr/bin/env bun
2 2
3import fs from "node:fs/promises";3import fs from "node:fs/promises";
4import os from "node:os";
5import path from "node:path";
4import { managedRuntimeDiagnostics, runManagedBackend, runManagedWeb } from "./packages/core/src/managed-runtime";6import { managedRuntimeDiagnostics, runManagedBackend, runManagedWeb } from "./packages/core/src/managed-runtime";
5 7
6function printHelp(): void {8function printHelp(): void {
@@ -11,12 +13,14 @@ Usage: · 4 unmodified lines
11 executor up [backend-args]13 executor up [backend-args]
12 executor backend <args>14 executor backend <args>
13 executor web [--port <number>]15 executor web [--port <number>]
16 executor uninstall [--yes]
14 17
15Commands:18Commands:
16 doctor Bootstrap and verify managed Convex backend runtime19 doctor Bootstrap and verify managed Convex backend runtime
17 up Run managed backend and auto-bootstrap Convex functions20 up Run managed backend and auto-bootstrap Convex functions
18 backend Pass through arguments to managed convex-local-backend binary21 backend Pass through arguments to managed convex-local-backend binary
19 web Run packaged web UI (default port: 5312)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
68async 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
84Options:
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
64async function run(): Promise<void> {135async function run(): Promise<void> {
65 const [command, ...rest] = process.argv.slice(2);136 const [command, ...rest] = process.argv.slice(2);
66 137
@@ -110,6 +181,11 @@ async function run(): Promise<void> { · 43 unmodified lines
110 process.exit(exitCode);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 throw new Error(`Unknown command: ${command}`);189 throw new Error(`Unknown command: ${command}`);
114}190}
115 191
executor/install+1−0
@@ -269,6 +269,7 @@ echo " executor up" · 268 unmodified lines
269echo " executor web # defaults to http://localhost:5312"269echo " executor web # defaults to http://localhost:5312"
270echo ""270echo ""
271echo "To uninstall later:"271echo "To uninstall later:"
272echo " executor uninstall --yes"
272echo " bash executor/uninstall --yes"273echo " bash executor/uninstall --yes"
273echo ""274echo ""
274show_star_prompt275show_star_prompt
executor/packages/convex/executorNode.ts+1−1
@@ -48,7 +48,7 @@ export const listToolsWithWarnings = action({ · 47 unmodified lines
48 workspaceId: args.workspaceId,48 workspaceId: args.workspaceId,
49 actorId: canonicalActorId,49 actorId: canonicalActorId,
50 clientId: args.clientId,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 if (inventory.warnings.some((warning) => warning.includes("showing previous results while refreshing"))) {53 if (inventory.warnings.some((warning) => warning.includes("showing previous results while refreshing"))) {
54 try {54 try {
executor/packages/convex/runtime/workspace_tools.ts+37−8
@@ -39,6 +39,7 @@ export interface WorkspaceToolsDebug { · 38 unmodified lines
39 mode: "cache-fresh" | "cache-stale" | "rebuild";39 mode: "cache-fresh" | "cache-stale" | "rebuild";
40 includeDts: boolean;40 includeDts: boolean;
41 sourceTimeoutMs: number | null;41 sourceTimeoutMs: number | null;
42 skipCacheRead: boolean;
42 sourceCount: number;43 sourceCount: number;
43 normalizedSourceCount: number;44 normalizedSourceCount: number;
44 cacheHit: boolean;45 cacheHit: boolean;
@@ -52,6 +53,7 @@ interface GetWorkspaceToolsOptions { · 7 unmodified lines
52 includeDts?: boolean;53 includeDts?: boolean;
53 sourceTimeoutMs?: number;54 sourceTimeoutMs?: number;
54 allowStaleOnMismatch?: boolean;55 allowStaleOnMismatch?: boolean;
56 skipCacheRead?: boolean;
55}57}
56 58
57export interface WorkspaceToolInventory {59export interface WorkspaceToolInventory {
@@ -133,6 +135,7 @@ export async function getWorkspaceTools( · 75 unmodified lines
133 const includeDts = options.includeDts ?? false;135 const includeDts = options.includeDts ?? false;
134 const sourceTimeoutMs = options.sourceTimeoutMs;136 const sourceTimeoutMs = options.sourceTimeoutMs;
135 const allowStaleOnMismatch = options.allowStaleOnMismatch ?? false;137 const allowStaleOnMismatch = options.allowStaleOnMismatch ?? false;
138 const skipCacheRead = options.skipCacheRead ?? false;
136 const sources = (await ctx.runQuery(internal.database.listToolSources, { workspaceId }))139 const sources = (await ctx.runQuery(internal.database.listToolSources, { workspaceId }))
137 .filter((source: { enabled: boolean }) => source.enabled);140 .filter((source: { enabled: boolean }) => source.enabled);
138 traceStep("listToolSources", listSourcesStartedAt);141 traceStep("listToolSources", listSourcesStartedAt);
@@ -141,10 +144,12 @@ export async function getWorkspaceTools( · 2 unmodified lines
141 const debugBase: Omit<WorkspaceToolsDebug, "mode" | "normalizedSourceCount" | "cacheHit" | "cacheFresh" | "timedOutSources" | "durationMs" | "trace"> = {144 const debugBase: Omit<WorkspaceToolsDebug, "mode" | "normalizedSourceCount" | "cacheHit" | "cacheFresh" | "timedOutSources" | "durationMs" | "trace"> = {
142 includeDts,145 includeDts,
143 sourceTimeoutMs: sourceTimeoutMs ?? null,146 sourceTimeoutMs: sourceTimeoutMs ?? null,
147 skipCacheRead,
144 sourceCount: sources.length,148 sourceCount: sources.length,
145 };149 };
146 150
147 try {151 if (!skipCacheRead) {
152 try {
148 const cacheReadStartedAt = Date.now();153 const cacheReadStartedAt = Date.now();
149 const cacheEntry = await ctx.runQuery(internal.workspaceToolCache.getEntry, {154 const cacheEntry = await ctx.runQuery(internal.workspaceToolCache.getEntry, {
150 workspaceId,155 workspaceId,
@@ -202,9 +207,12 @@ export async function getWorkspaceTools( · 51 unmodified lines
202 }207 }
203 }208 }
204 }209 }
205 } catch (error) {210 } catch (error) {
206 const msg = error instanceof Error ? error.message : String(error);211 const msg = error instanceof Error ? error.message : String(error);
207 console.warn(`[executor] workspace tool cache read failed for '${workspaceId}': ${msg}`);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 const configs: ExternalToolSourceConfig[] = [];218 const configs: ExternalToolSourceConfig[] = [];
@@ -364,13 +372,24 @@ export async function getWorkspaceTools( · 153 unmodified lines
364export async function loadWorkspaceToolInventoryForContext(372export async function loadWorkspaceToolInventoryForContext(
365 ctx: ActionCtx,373 ctx: ActionCtx,
366 context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string },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): Promise<WorkspaceToolInventory> {381): Promise<WorkspaceToolInventory> {
369 const includeDts = options.includeDts ?? false;382 const includeDts = options.includeDts ?? false;
370 const sourceTimeoutMs = options.sourceTimeoutMs;383 const sourceTimeoutMs = options.sourceTimeoutMs;
371 const allowStaleOnMismatch = options.allowStaleOnMismatch;384 const allowStaleOnMismatch = options.allowStaleOnMismatch;
385 const skipCacheRead = options.skipCacheRead;
372 const [result, policies] = await Promise.all([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 ctx.runQuery(internal.database.listAccessPolicies, { workspaceId: context.workspaceId }),393 ctx.runQuery(internal.database.listAccessPolicies, { workspaceId: context.workspaceId }),
375 ]);394 ]);
376 const typedPolicies = policies as AccessPolicyRecord[];395 const typedPolicies = policies as AccessPolicyRecord[];
@@ -405,7 +424,12 @@ export async function loadWorkspaceToolInventoryForContext( · 28 unmodified lines
405export async function listToolsForContext(424export async function listToolsForContext(
406 ctx: ActionCtx,425 ctx: ActionCtx,
407 context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string },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): Promise<ToolDescriptor[]> {433): Promise<ToolDescriptor[]> {
410 const inventory = await loadWorkspaceToolInventoryForContext(ctx, context, options);434 const inventory = await loadWorkspaceToolInventoryForContext(ctx, context, options);
411 return inventory.tools;435 return inventory.tools;
@@ -414,7 +438,12 @@ export async function listToolsForContext( · 2 unmodified lines
414export async function listToolsWithWarningsForContext(438export async function listToolsWithWarningsForContext(
415 ctx: ActionCtx,439 ctx: ActionCtx,
416 context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string },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): Promise<{447): Promise<{
419 tools: ToolDescriptor[];448 tools: ToolDescriptor[];
420 warnings: string[];449 warnings: string[];
executor/packages/core/src/managed-runtime-bootstrap.ts+62−4
@@ -33,6 +33,67 @@ async function generateSelfHostedAdminKey(info: ManagedRuntimeInfo): Promise<str · 32 unmodified lines
33 return parsed.adminKey;33 return parsed.adminKey;
34}34}
35 35
36async 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
45function 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
56async 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
70async 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
36async function findProjectDir(): Promise<string | null> {97async function findProjectDir(): Promise<string | null> {
37 const roots = [98 const roots = [
38 Bun.env.EXECUTOR_PROJECT_DIR,99 Bun.env.EXECUTOR_PROJECT_DIR,
@@ -47,10 +108,7 @@ async function findProjectDir(): Promise<string | null> { · 8 unmodified lines
47 }108 }
48 109
49 for (const candidate of candidates) {110 for (const candidate of candidates) {
50 const convexDir = path.join(candidate, "convex");111 if (await hasConvexProjectConfig(candidate)) {
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))) {
54 return candidate;112 return candidate;
55 }113 }
56 }114 }
executor/packages/core/src/managed-runtime.ts+43−3
@@ -74,11 +74,45 @@ export async function runManagedBackend(args: string[]): Promise<number> { · 73 unmodified lines
74 74
75export async function runManagedWeb(options: { port?: number } = {}): Promise<number> {75export async function runManagedWeb(options: { port?: number } = {}): Promise<number> {
76 const info = await ensureManagedRuntime();76 const info = await ensureManagedRuntime();
77 let managedBackend: ReturnType<typeof Bun.spawn> | null = null;
78 let startedManagedBackend = false;
79
77 try {80 try {
78 await waitForBackendReady(info, 3_000);81 try {
79 await ensureProjectBootstrapped(info);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 } catch {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 await ensureNodeRuntime(info);117 await ensureNodeRuntime(info);
84 await ensureWebBundle(info);118 await ensureWebBundle(info);
@@ -99,6 +133,12 @@ export async function runManagedWeb(options: { port?: number } = {}): Promise<nu · 14 unmodified lines
99 cwd: info.webDir,133 cwd: info.webDir,
100 env,134 env,
101 });135 });
136
137 if (managedBackend && startedManagedBackend) {
138 managedBackend.kill();
139 await managedBackend.exited;
140 }
141
102 return proc.exitCode;142 return proc.exitCode;
103}143}
104 144