| @@ -22,6 +22,7 @@ import { · 21 unmodified lines |
| 22 | 22 | type SqlControlPlaneRuntime, |
| 23 | 23 | } from "@executor-v3/control-plane"; |
| 24 | 24 | import type { ToolCatalog } from "@executor-v3/codemode-core"; |
| 25 | +import { isDenoAvailable } from "@executor-v3/runtime-deno-subprocess"; |
| 25 | 26 | import * as Effect from "effect/Effect"; |
| 26 | 27 | import * as Schema from "effect/Schema"; |
| 27 | 28 | import * as Option from "effect/Option"; |
| @@ -583,46 +584,56 @@ const renderStatus = (status: LocalServerStatus): string => · 555 unmodified lines |
| 583 | 584 | |
| 584 | 585 | const getDoctorReport = (baseUrl: string) => |
| 585 | 586 | getServerStatus(baseUrl).pipe( |
| 586 | | − Effect.map((status) => { |
| 587 | | − const checks = { |
| 588 | | − serverReachable: { |
| 589 | | − ok: status.reachable, |
| 590 | | − detail: status.reachable ? `reachable at ${status.baseUrl}` : `not reachable at ${status.baseUrl}`, |
| 591 | | − }, |
| 592 | | − pidFile: { |
| 593 | | − ok: status.pid !== null, |
| 594 | | − detail: status.pid !== null ? `pid ${status.pid}` : `missing pid file at ${status.pidFile}`, |
| 595 | | − }, |
| 596 | | − process: { |
| 597 | | − ok: status.pidRunning, |
| 598 | | − detail: status.pidRunning ? `pid ${status.pid}` : "no live daemon process recorded", |
| 599 | | − }, |
| 600 | | − database: { |
| 601 | | − ok: status.localDataDir.length > 0, |
| 602 | | − detail: status.localDataDir, |
| 603 | | − }, |
| 604 | | − webAssets: { |
| 605 | | − ok: status.webAssetsDir !== null, |
| 606 | | − detail: status.webAssetsDir ?? "missing bundled web assets", |
| 607 | | − }, |
| 608 | | − migrations: { |
| 609 | | − ok: status.migrationsDir !== null, |
| 610 | | − detail: status.migrationsDir ?? "missing migrations directory", |
| 611 | | − }, |
| 612 | | − installation: { |
| 613 | | − ok: status.installation !== null, |
| 614 | | − detail: status.installation |
| 615 | | − ? `workspace ${status.installation.workspaceId}` |
| 616 | | − : "local installation unavailable", |
| 617 | | − }, |
| 618 | | − } as const; |
| 619 | | − |
| 620 | | − return { |
| 621 | | − ok: Object.values(checks).every((check) => check.ok), |
| 622 | | − status, |
| 623 | | − checks, |
| 624 | | − }; |
| 625 | | − }), |
| 587 | + Effect.flatMap((status) => |
| 588 | + getDenoVersion().pipe( |
| 589 | + Effect.map((denoVersion) => { |
| 590 | + const checks = { |
| 591 | + serverReachable: { |
| 592 | + ok: status.reachable, |
| 593 | + detail: status.reachable ? `reachable at ${status.baseUrl}` : `not reachable at ${status.baseUrl}`, |
| 594 | + }, |
| 595 | + pidFile: { |
| 596 | + ok: status.pid !== null, |
| 597 | + detail: status.pid !== null ? `pid ${status.pid}` : `missing pid file at ${status.pidFile}`, |
| 598 | + }, |
| 599 | + process: { |
| 600 | + ok: status.pidRunning, |
| 601 | + detail: status.pidRunning ? `pid ${status.pid}` : "no live daemon process recorded", |
| 602 | + }, |
| 603 | + database: { |
| 604 | + ok: status.localDataDir.length > 0, |
| 605 | + detail: status.localDataDir, |
| 606 | + }, |
| 607 | + webAssets: { |
| 608 | + ok: status.webAssetsDir !== null, |
| 609 | + detail: status.webAssetsDir ?? "missing bundled web assets", |
| 610 | + }, |
| 611 | + migrations: { |
| 612 | + ok: status.migrationsDir !== null, |
| 613 | + detail: status.migrationsDir ?? "missing migrations directory", |
| 614 | + }, |
| 615 | + installation: { |
| 616 | + ok: status.installation !== null, |
| 617 | + detail: status.installation |
| 618 | + ? `workspace ${status.installation.workspaceId}` |
| 619 | + : "local installation unavailable", |
| 620 | + }, |
| 621 | + denoSandbox: { |
| 622 | + ok: denoVersion !== null, |
| 623 | + detail: denoVersion !== null |
| 624 | + ? `deno ${denoVersion}` |
| 625 | + : "deno not found (run `executor sandbox` to install)", |
| 626 | + }, |
| 627 | + } as const; |
| 628 | + |
| 629 | + return { |
| 630 | + ok: Object.values(checks).every((check) => check.ok), |
| 631 | + status, |
| 632 | + checks, |
| 633 | + }; |
| 634 | + }), |
| 635 | + ), |
| 636 | + ), |
| 626 | 637 | ); |
| 627 | 638 | |
| 628 | 639 | const printJson = (value: unknown) => |
| @@ -1143,6 +1154,93 @@ const doctorCommand = Command.make( · 514 unmodified lines |
| 1143 | 1154 | ), |
| 1144 | 1155 | ).pipe(Command.withDescription("Check local executor install and daemon health")); |
| 1145 | 1156 | |
| 1157 | +const getDenoVersion = (): Effect.Effect<string | null, never, never> => |
| 1158 | + Effect.tryPromise({ |
| 1159 | + try: () => |
| 1160 | + new Promise<string | null>((resolve) => { |
| 1161 | + const denoExecutable = process.env.DENO_BIN?.trim() |
| 1162 | + || (process.env.HOME && existsSync(`${process.env.HOME}/.deno/bin/deno`) |
| 1163 | + ? `${process.env.HOME}/.deno/bin/deno` |
| 1164 | + : "deno"); |
| 1165 | + |
| 1166 | + const child = spawn(denoExecutable, ["--version"], { |
| 1167 | + stdio: ["ignore", "pipe", "ignore"], |
| 1168 | + timeout: 5000, |
| 1169 | + }); |
| 1170 | + |
| 1171 | + let stdout = ""; |
| 1172 | + child.stdout?.setEncoding("utf8"); |
| 1173 | + child.stdout?.on("data", (chunk: string) => { |
| 1174 | + stdout += chunk; |
| 1175 | + }); |
| 1176 | + |
| 1177 | + child.once("error", () => resolve(null)); |
| 1178 | + child.once("close", (code) => { |
| 1179 | + if (code !== 0) { |
| 1180 | + resolve(null); |
| 1181 | + return; |
| 1182 | + } |
| 1183 | + |
| 1184 | + const match = /deno\s+(\S+)/i.exec(stdout); |
| 1185 | + resolve(match ? match[1] : null); |
| 1186 | + }); |
| 1187 | + }), |
| 1188 | + catch: () => null, |
| 1189 | + }).pipe(Effect.catchAll(() => Effect.succeed(null))); |
| 1190 | + |
| 1191 | +const installDeno = (): Effect.Effect<boolean, Error, never> => |
| 1192 | + Effect.tryPromise({ |
| 1193 | + try: () => |
| 1194 | + new Promise<boolean>((resolve, reject) => { |
| 1195 | + const child = spawn("sh", ["-c", "curl -fsSL https://deno.land/install.sh | sh"], { |
| 1196 | + stdio: ["ignore", "inherit", "inherit"], |
| 1197 | + }); |
| 1198 | + |
| 1199 | + child.once("error", (err) => reject(err)); |
| 1200 | + child.once("close", (code) => resolve(code === 0)); |
| 1201 | + }), |
| 1202 | + catch: toError, |
| 1203 | + }); |
| 1204 | + |
| 1205 | +const sandboxCommand = Command.make( |
| 1206 | + "sandbox", |
| 1207 | + {}, |
| 1208 | + () => |
| 1209 | + Effect.gen(function* () { |
| 1210 | + yield* printText("Checking Deno sandbox runtime..."); |
| 1211 | + |
| 1212 | + const version = yield* getDenoVersion(); |
| 1213 | + |
| 1214 | + if (version !== null) { |
| 1215 | + yield* printText(`Deno sandbox is ready (deno ${version}).`); |
| 1216 | + return; |
| 1217 | + } |
| 1218 | + |
| 1219 | + if (!isDenoAvailable()) { |
| 1220 | + yield* printText("Deno is not installed. Installing..."); |
| 1221 | + } else { |
| 1222 | + yield* printText("Deno binary found but did not respond. Reinstalling..."); |
| 1223 | + } |
| 1224 | + |
| 1225 | + const installed = yield* installDeno(); |
| 1226 | + if (!installed) { |
| 1227 | + return yield* Effect.fail( |
| 1228 | + new Error("Deno installation failed. Install manually: https://docs.deno.com/runtime/getting_started/installation/"), |
| 1229 | + ); |
| 1230 | + } |
| 1231 | + |
| 1232 | + const postVersion = yield* getDenoVersion(); |
| 1233 | + if (postVersion === null) { |
| 1234 | + return yield* Effect.fail( |
| 1235 | + new Error("Deno was installed but could not be verified. You may need to restart your shell."), |
| 1236 | + ); |
| 1237 | + } |
| 1238 | + |
| 1239 | + yield* printText(`Deno sandbox installed successfully (deno ${postVersion}).`); |
| 1240 | + }), |
| 1241 | +).pipe(Command.withDescription("Check and install the Deno sandbox runtime")); |
| 1242 | + |
| 1243 | + |
| 1146 | 1244 | const callCommand = Command.make( |
| 1147 | 1245 | "call", |
| 1148 | 1246 | { |
| @@ -1287,6 +1385,7 @@ const root = Command.make("executor").pipe( · 138 unmodified lines |
| 1287 | 1385 | downCommand, |
| 1288 | 1386 | statusCommand, |
| 1289 | 1387 | doctorCommand, |
| 1388 | + sandboxCommand, |
| 1290 | 1389 | callCommand, |
| 1291 | 1390 | resumeCommand, |
| 1292 | 1391 | devCommand, |