Unified · Split
| @@ -5,7 +5,7 @@ on: · 4 unmodified lines | |||
| 5 | workflow_dispatch: | 5 | workflow_dispatch: |
| 6 | 6 | ||
| 7 | permissions: | 7 | permissions: |
| 8 | contents: read | 8 | contents: write |
| 9 | id-token: write | 9 | id-token: write |
| 10 | 10 | ||
| 11 | concurrency: | 11 | concurrency: |
| @@ -19,6 +19,8 @@ jobs: · 7 unmodified lines | |||
| 19 | steps: | 19 | steps: |
| 20 | - name: Checkout | 20 | - name: Checkout |
| 21 | uses: actions/checkout@v4 | 21 | uses: actions/checkout@v4 |
| 22 | with: | ||
| 23 | fetch-depth: 0 | ||
| 22 | 24 | ||
| 23 | - name: Require default branch | 25 | - name: Require default branch |
| 24 | if: github.ref_name != github.event.repository.default_branch | 26 | if: github.ref_name != github.event.repository.default_branch |
| @@ -53,15 +55,50 @@ jobs: · 28 unmodified lines | |||
| 53 | fi | 55 | fi |
| 54 | 56 | ||
| 55 | echo "version=${version}" >> "$GITHUB_OUTPUT" | 57 | echo "version=${version}" >> "$GITHUB_OUTPUT" |
| 58 | echo "tag=v${version}" >> "$GITHUB_OUTPUT" | ||
| 56 | echo "Publishing executor@${version}" | 59 | echo "Publishing executor@${version}" |
| 57 | 60 | ||
| 58 | - name: Ensure version is unpublished | 61 | - name: Inspect existing publish state |
| 62 | id: state | ||
| 59 | shell: bash | 63 | shell: bash |
| 64 | env: | ||
| 65 | GH_TOKEN: ${{ github.token }} | ||
| 60 | run: | | 66 | run: | |
| 61 | set -euo pipefail | 67 | set -euo pipefail |
| 68 | version="${{ steps.version.outputs.version }}" | ||
| 69 | tag="${{ steps.version.outputs.tag }}" | ||
| 70 | package_published=false | ||
| 71 | release_exists=false | ||
| 72 | tag_exists=false | ||
| 73 | |||
| 74 | if npm view "executor@${version}" version >/dev/null 2>&1; then | ||
| 75 | package_published=true | ||
| 76 | fi | ||
| 77 | |||
| 78 | if gh release view "${tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then | ||
| 79 | release_exists=true | ||
| 80 | fi | ||
| 81 | |||
| 82 | if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null || git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then | ||
| 83 | tag_exists=true | ||
| 84 | fi | ||
| 85 | |||
| 86 | echo "package_published=${package_published}" >> "$GITHUB_OUTPUT" | ||
| 87 | echo "release_exists=${release_exists}" >> "$GITHUB_OUTPUT" | ||
| 88 | echo "tag_exists=${tag_exists}" >> "$GITHUB_OUTPUT" | ||
| 89 | |||
| 90 | if [[ "${package_published}" == true && "${release_exists}" == true ]]; then | ||
| 91 | echo "executor@${version} is already published and ${tag} already exists" >&2 | ||
| 92 | exit 1 | ||
| 93 | fi | ||
| 62 | 94 | ||
| 63 | if npm view "executor@${{ steps.version.outputs.version }}" version >/dev/null 2>&1; then | 95 | if [[ "${package_published}" == false && "${release_exists}" == true ]]; then |
| 64 | echo "executor@${{ steps.version.outputs.version }} is already published" >&2 | 96 | echo "GitHub release ${tag} exists, but executor@${version} is not published to npm" >&2 |
| 97 | exit 1 | ||
| 98 | fi | ||
| 99 | |||
| 100 | if [[ "${package_published}" == false && "${tag_exists}" == true && "${release_exists}" == false ]]; then | ||
| 101 | echo "Release tag ${tag} already exists without a GitHub release; resolve that before publishing executor@${version}" >&2 | ||
| 65 | exit 1 | 102 | exit 1 |
| 66 | fi | 103 | fi |
| 67 | 104 | ||
| @@ -70,9 +107,38 @@ jobs: · 2 unmodified lines | |||
| 70 | bun run --cwd apps/executor typecheck | 107 | bun run --cwd apps/executor typecheck |
| 71 | bun run --cwd apps/executor test | 108 | bun run --cwd apps/executor test |
| 72 | 109 | ||
| 73 | - name: Build distribution package | 110 | - name: Build release artifacts |
| 74 | run: bun run --cwd apps/executor build:dist | 111 | run: bun run --cwd apps/executor build:release |
| 75 | 112 | ||
| 76 | - name: Publish to npm | 113 | - name: Publish to npm |
| 114 | if: steps.state.outputs.package_published != 'true' | ||
| 77 | working-directory: apps/executor | 115 | working-directory: apps/executor |
| 78 | run: npm publish ./dist/npm --access public --provenance | 116 | run: npm publish ./dist/npm --access public --provenance |
| 117 | |||
| 118 | - name: Create GitHub release | ||
| 119 | if: steps.state.outputs.release_exists != 'true' | ||
| 120 | shell: bash | ||
| 121 | env: | ||
| 122 | GH_TOKEN: ${{ github.token }} | ||
| 123 | run: | | ||
| 124 | set -euo pipefail | ||
| 125 | shopt -s nullglob | ||
| 126 | assets=( | ||
| 127 | apps/executor/dist/release/*.tgz | ||
| 128 | apps/executor/dist/release/checksums.txt | ||
| 129 | apps/executor/dist/release/portable/*.tar.gz | ||
| 130 | apps/executor/dist/release/portable/*.zip | ||
| 131 | ) | ||
| 132 | |||
| 133 | if [[ ${#assets[@]} -lt 3 ]]; then | ||
| 134 | echo "Missing release assets under apps/executor/dist/release" >&2 | ||
| 135 | exit 1 | ||
| 136 | fi | ||
| 137 | |||
| 138 | gh release create "${{ steps.version.outputs.tag }}" \ | ||
| 139 | "${assets[@]}" \ | ||
| 140 | --repo "${GITHUB_REPOSITORY}" \ | ||
| 141 | --target "${GITHUB_SHA}" \ | ||
| 142 | --title "${{ steps.version.outputs.tag }}" \ | ||
| 143 | --generate-notes \ | ||
| 144 | --latest | ||
| @@ -33,7 +33,8 @@ · 32 unmodified lines | |||
| 33 | "pack:dist": "bun src/distribution/build-package.ts && npm pack ./dist/npm", | 33 | "pack:dist": "bun src/distribution/build-package.ts && npm pack ./dist/npm", |
| 34 | "publish:dist:dry-run": "bun src/distribution/build-package.ts && npm publish ./dist/npm --dry-run", | 34 | "publish:dist:dry-run": "bun src/distribution/build-package.ts && npm publish ./dist/npm --dry-run", |
| 35 | "publish:dist": "bun src/distribution/build-package.ts && npm publish ./dist/npm", | 35 | "publish:dist": "bun src/distribution/build-package.ts && npm publish ./dist/npm", |
| 36 | "build:portable": "bun src/distribution/build-portable.ts" | 36 | "build:portable": "bun src/distribution/build-portable.ts", |
| 37 | "build:release": "bun src/distribution/build-release.ts" | ||
| 37 | }, | 38 | }, |
| 38 | "dependencies": { | 39 | "dependencies": { |
| 39 | "@effect/cli": "catalog:", | 40 | "@effect/cli": "catalog:", |
| @@ -0,0 +1,137 @@ | |||
| 1 | import { createHash } from "node:crypto"; | ||
| 2 | import { spawn } from "node:child_process"; | ||
| 3 | import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; | ||
| 4 | import { join, relative, resolve } from "node:path"; | ||
| 5 | |||
| 6 | import { buildDistributionPackage } from "./artifact"; | ||
| 7 | import { repoRoot } from "./metadata"; | ||
| 8 | import { buildPortableDistribution, portableTargetIds } from "./portable"; | ||
| 9 | |||
| 10 | const defaultReleaseDir = resolve(repoRoot, "apps/executor/dist/release"); | ||
| 11 | |||
| 12 | type CommandInput = { | ||
| 13 | command: string; | ||
| 14 | args: ReadonlyArray<string>; | ||
| 15 | cwd: string; | ||
| 16 | }; | ||
| 17 | |||
| 18 | type CommandOutput = { | ||
| 19 | stdout: string; | ||
| 20 | stderr: string; | ||
| 21 | }; | ||
| 22 | |||
| 23 | type PackResult = { | ||
| 24 | filename?: string; | ||
| 25 | }; | ||
| 26 | |||
| 27 | const runCommand = async (input: CommandInput): Promise<CommandOutput> => { | ||
| 28 | const child = spawn(input.command, [...input.args], { | ||
| 29 | cwd: input.cwd, | ||
| 30 | env: process.env, | ||
| 31 | stdio: ["ignore", "pipe", "pipe"], | ||
| 32 | }); | ||
| 33 | |||
| 34 | let stdout = ""; | ||
| 35 | let stderr = ""; | ||
| 36 | |||
| 37 | child.stdout?.setEncoding("utf8"); | ||
| 38 | child.stdout?.on("data", (chunk) => { | ||
| 39 | stdout += chunk; | ||
| 40 | }); | ||
| 41 | |||
| 42 | child.stderr?.setEncoding("utf8"); | ||
| 43 | child.stderr?.on("data", (chunk) => { | ||
| 44 | stderr += chunk; | ||
| 45 | }); | ||
| 46 | |||
| 47 | const exitCode = await new Promise<number>((resolveExitCode, reject) => { | ||
| 48 | child.once("error", reject); | ||
| 49 | child.once("close", (code) => { | ||
| 50 | resolveExitCode(code ?? -1); | ||
| 51 | }); | ||
| 52 | }); | ||
| 53 | |||
| 54 | if (exitCode !== 0) { | ||
| 55 | throw new Error( | ||
| 56 | [ | ||
| 57 | `${input.command} ${input.args.join(" ")} exited with code ${exitCode}`, | ||
| 58 | stdout.trim().length > 0 ? `stdout:\n${stdout.trim()}` : null, | ||
| 59 | stderr.trim().length > 0 ? `stderr:\n${stderr.trim()}` : null, | ||
| 60 | ] | ||
| 61 | .filter((part) => part !== null) | ||
| 62 | .join("\n\n"), | ||
| 63 | ); | ||
| 64 | } | ||
| 65 | |||
| 66 | return { | ||
| 67 | stdout: stdout.trim(), | ||
| 68 | stderr: stderr.trim(), | ||
| 69 | }; | ||
| 70 | }; | ||
| 71 | |||
| 72 | const computeSha256 = (contents: Uint8Array): string => | ||
| 73 | createHash("sha256").update(contents).digest("hex"); | ||
| 74 | |||
| 75 | const createChecksumsFile = async ( | ||
| 76 | releaseDir: string, | ||
| 77 | assetPaths: ReadonlyArray<string>, | ||
| 78 | ): Promise<string> => { | ||
| 79 | const entries = await Promise.all( | ||
| 80 | [...assetPaths] | ||
| 81 | .sort((left, right) => left.localeCompare(right)) | ||
| 82 | .map(async (assetPath) => { | ||
| 83 | const contents = await readFile(assetPath); | ||
| 84 | const relativePath = relative(releaseDir, assetPath).replaceAll("\\", "/"); | ||
| 85 | return `${computeSha256(contents)} ${relativePath}`; | ||
| 86 | }), | ||
| 87 | ); | ||
| 88 | |||
| 89 | const checksumsPath = join(releaseDir, "checksums.txt"); | ||
| 90 | await writeFile(checksumsPath, `${entries.join("\n")}\n`); | ||
| 91 | return checksumsPath; | ||
| 92 | }; | ||
| 93 | |||
| 94 | const packDistributionPackage = async (releaseDir: string): Promise<string> => { | ||
| 95 | const output = await runCommand({ | ||
| 96 | command: "npm", | ||
| 97 | args: ["pack", "./apps/executor/dist/npm", "--pack-destination", releaseDir, "--json"], | ||
| 98 | cwd: repoRoot, | ||
| 99 | }); | ||
| 100 | const [result] = JSON.parse(output.stdout) as ReadonlyArray<PackResult>; | ||
| 101 | const filename = result?.filename; | ||
| 102 | |||
| 103 | if (!filename) { | ||
| 104 | throw new Error(`npm pack did not report an output filename. stdout:\n${output.stdout}`); | ||
| 105 | } | ||
| 106 | |||
| 107 | return join(releaseDir, filename); | ||
| 108 | }; | ||
| 109 | |||
| 110 | const main = async () => { | ||
| 111 | const releaseDir = defaultReleaseDir; | ||
| 112 | const portableDir = join(releaseDir, "portable"); | ||
| 113 | |||
| 114 | await rm(releaseDir, { recursive: true, force: true }); | ||
| 115 | await mkdir(releaseDir, { recursive: true }); | ||
| 116 | |||
| 117 | await buildDistributionPackage(); | ||
| 118 | const packageArchivePath = await packDistributionPackage(releaseDir); | ||
| 119 | const portableArtifact = await buildPortableDistribution({ | ||
| 120 | outputDir: portableDir, | ||
| 121 | buildWeb: false, | ||
| 122 | targets: portableTargetIds, | ||
| 123 | }); | ||
| 124 | const portableArchivePaths = portableArtifact.artifacts | ||
| 125 | .map((artifact) => artifact.archivePath) | ||
| 126 | .filter((archivePath): archivePath is string => archivePath !== null); | ||
| 127 | const checksumsPath = await createChecksumsFile(releaseDir, [ | ||
| 128 | packageArchivePath, | ||
| 129 | ...portableArchivePaths, | ||
| 130 | ]); | ||
| 131 | |||
| 132 | for (const assetPath of [packageArchivePath, ...portableArchivePaths, checksumsPath]) { | ||
| 133 | process.stdout.write(`${assetPath}\n`); | ||
| 134 | } | ||
| 135 | }; | ||
| 136 | |||
| 137 | await main(); | ||
| @@ -74,6 +74,8 @@ const portableTargets: Record<string, PortableTarget> = { · 73 unmodified lines | |||
| 74 | }, | 74 | }, |
| 75 | }; | 75 | }; |
| 76 | 76 | ||
| 77 | export const portableTargetIds = Object.freeze(Object.keys(portableTargets)); | ||
| 78 | |||
| 77 | export type BuildPortableDistributionOptions = { | 79 | export type BuildPortableDistributionOptions = { |
| 78 | outputDir?: string; | 80 | outputDir?: string; |
| 79 | packageName?: string; | 81 | packageName?: string; |
| @@ -23,6 +23,17 @@ const defaultLinuxStateHome = trim(process.env.XDG_STATE_HOME) ?? join(home, ".l · 22 unmodified lines | |||
| 23 | const defaultWindowsHome = join(trim(process.env.LOCALAPPDATA) ?? join(home, "AppData", "Local"), "Executor"); | 23 | const defaultWindowsHome = join(trim(process.env.LOCALAPPDATA) ?? join(home, "AppData", "Local"), "Executor"); |
| 24 | const defaultMacHome = join(home, "Library", "Application Support", "Executor"); | 24 | const defaultMacHome = join(home, "Library", "Application Support", "Executor"); |
| 25 | const defaultLinuxHome = join(defaultLinuxDataHome, "executor"); | 25 | const defaultLinuxHome = join(defaultLinuxDataHome, "executor"); |
| 26 | const legacyLinuxHome = join(defaultLinuxDataHome, "executor-v3"); | ||
| 27 | const legacyWindowsHome = join( | ||
| 28 | trim(process.env.LOCALAPPDATA) ?? join(home, "AppData", "Local"), | ||
| 29 | "Executor-v3", | ||
| 30 | ); | ||
| 31 | const legacyMacHome = join( | ||
| 32 | home, | ||
| 33 | "Library", | ||
| 34 | "Application Support", | ||
| 35 | "Executor-v3", | ||
| 36 | ); | ||
| 26 | 37 | ||
| 27 | export const DEFAULT_SERVER_PORT = 8788; | 38 | export const DEFAULT_SERVER_PORT = 8788; |
| 28 | export const DEFAULT_SERVER_HOST = "127.0.0.1"; | 39 | export const DEFAULT_SERVER_HOST = "127.0.0.1"; |
| @@ -43,6 +54,14 @@ export const DEFAULT_LOCAL_DATA_DIR = · 14 unmodified lines | |||
| 43 | trim(process.env[EXECUTOR_LOCAL_DATA_DIR_ENV]) | 54 | trim(process.env[EXECUTOR_LOCAL_DATA_DIR_ENV]) |
| 44 | ?? join(DEFAULT_EXECUTOR_DATA_DIR, "control-plane"); | 55 | ?? join(DEFAULT_EXECUTOR_DATA_DIR, "control-plane"); |
| 45 | 56 | ||
| 57 | export const DEFAULT_LEGACY_LOCAL_DATA_DIRS = explicitExecutorHome | ||
| 58 | ? [] | ||
| 59 | : [ | ||
| 60 | isWindows ? join(legacyWindowsHome, "control-plane") : null, | ||
| 61 | isMac ? join(legacyMacHome, "control-plane") : null, | ||
| 62 | !isWindows && !isMac ? join(legacyLinuxHome, "control-plane") : null, | ||
| 63 | ].filter((value): value is string => value !== null); | ||
| 64 | |||
| 46 | export const DEFAULT_SERVER_PID_FILE = | 65 | export const DEFAULT_SERVER_PID_FILE = |
| 47 | trim(process.env[EXECUTOR_SERVER_PID_FILE_ENV]) | 66 | trim(process.env[EXECUTOR_SERVER_PID_FILE_ENV]) |
| 48 | ?? join(DEFAULT_EXECUTOR_RUN_DIR, "server.pid"); | 67 | ?? join(DEFAULT_EXECUTOR_RUN_DIR, "server.pid"); |
| @@ -1,13 +1,13 @@ | |||
| 1 | import { existsSync } from "node:fs"; | ||
| 2 | import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"; | ||
| 1 | import { | 3 | import { |
| 2 | createServer as createNodeServer, | 4 | createServer as createNodeServer, |
| 3 | type IncomingMessage, | 5 | type IncomingMessage, |
| 4 | type ServerResponse, | 6 | type ServerResponse, |
| 5 | } from "node:http"; | 7 | } from "node:http"; |
| 6 | import type { AddressInfo } from "node:net"; | 8 | import type { AddressInfo } from "node:net"; |
| 7 | import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; | ||
| 8 | import { Readable } from "node:stream"; | ||
| 9 | import { dirname, extname, resolve } from "node:path"; | 9 | import { dirname, extname, resolve } from "node:path"; |
| 10 | 10 | import { Readable } from "node:stream"; | |
| 11 | import { HttpApiBuilder, HttpServer } from "@effect/platform"; | 11 | import { HttpApiBuilder, HttpServer } from "@effect/platform"; |
| 12 | import { | 12 | import { |
| 13 | createControlPlaneApiLayer, | 13 | createControlPlaneApiLayer, |
| @@ -17,11 +17,14 @@ import { · 3 unmodified lines | |||
| 17 | type SqlControlPlaneRuntime, | 17 | type SqlControlPlaneRuntime, |
| 18 | } from "@executor/control-plane"; | 18 | } from "@executor/control-plane"; |
| 19 | import { createExecutorMcpRequestHandler } from "@executor/executor-mcp"; | 19 | import { createExecutorMcpRequestHandler } from "@executor/executor-mcp"; |
| 20 | import * as Cause from "effect/Cause"; | ||
| 20 | import * as Effect from "effect/Effect"; | 21 | import * as Effect from "effect/Effect"; |
| 22 | import * as Exit from "effect/Exit"; | ||
| 21 | import * as Layer from "effect/Layer"; | 23 | import * as Layer from "effect/Layer"; |
| 22 | import type * as Scope from "effect/Scope"; | 24 | import type * as Scope from "effect/Scope"; |
| 23 | 25 | ||
| 24 | import { | 26 | import { |
| 27 | DEFAULT_LEGACY_LOCAL_DATA_DIRS, | ||
| 25 | DEFAULT_LOCAL_DATA_DIR, | 28 | DEFAULT_LOCAL_DATA_DIR, |
| 26 | DEFAULT_SERVER_LOG_FILE, | 29 | DEFAULT_SERVER_LOG_FILE, |
| 27 | DEFAULT_SERVER_PID_FILE, | 30 | DEFAULT_SERVER_PID_FILE, |
| @@ -79,6 +82,9 @@ export type LocalExecutorRequestHandler = { · 51 unmodified lines | |||
| 79 | readonly setBaseUrl: (baseUrl: string) => void; | 82 | readonly setBaseUrl: (baseUrl: string) => void; |
| 80 | }; | 83 | }; |
| 81 | 84 | ||
| 85 | type ControlPlaneWebHandler = ReturnType<typeof HttpApiBuilder.toWebHandler>; | ||
| 86 | type ExecutorMcpHandler = ReturnType<typeof createExecutorMcpRequestHandler>; | ||
| 87 | |||
| 82 | const disposeRuntime = (runtime: SqlControlPlaneRuntime) => | 88 | const disposeRuntime = (runtime: SqlControlPlaneRuntime) => |
| 83 | Effect.tryPromise({ | 89 | Effect.tryPromise({ |
| 84 | try: () => runtime.close(), | 90 | try: () => runtime.close(), |
| @@ -86,6 +92,121 @@ const disposeRuntime = (runtime: SqlControlPlaneRuntime) => · 1 unmodified line | |||
| 86 | cause instanceof Error ? cause : new Error(String(cause ?? "runtime close failed")), | 92 | cause instanceof Error ? cause : new Error(String(cause ?? "runtime close failed")), |
| 87 | }).pipe(Effect.orDie); | 93 | }).pipe(Effect.orDie); |
| 88 | 94 | ||
| 95 | const createRuntime = ( | ||
| 96 | localDataDir: string, | ||
| 97 | getLocalServerBaseUrl: () => string | undefined, | ||
| 98 | options: StartLocalExecutorServerOptions, | ||
| 99 | ) => | ||
| 100 | createSqlControlPlaneRuntime({ | ||
| 101 | localDataDir, | ||
| 102 | migrationsFolder: options.migrationsFolder, | ||
| 103 | executionResolver: options.executionResolver, | ||
| 104 | resolveSecretMaterial: options.resolveSecretMaterial, | ||
| 105 | getLocalServerBaseUrl, | ||
| 106 | }).pipe( | ||
| 107 | Effect.mapError((cause) => | ||
| 108 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 109 | ), | ||
| 110 | ); | ||
| 111 | |||
| 112 | const moveLegacyLocalDataDir = ( | ||
| 113 | legacyLocalDataDir: string, | ||
| 114 | requestedLocalDataDir: string, | ||
| 115 | ) => | ||
| 116 | Effect.tryPromise({ | ||
| 117 | try: async () => { | ||
| 118 | await mkdir(dirname(requestedLocalDataDir), { recursive: true }); | ||
| 119 | if (existsSync(requestedLocalDataDir)) { | ||
| 120 | const backupPath = `${requestedLocalDataDir}.backup-${Date.now()}`; | ||
| 121 | await rename(requestedLocalDataDir, backupPath); | ||
| 122 | console.warn( | ||
| 123 | `[executor] Backed up unreadable local data dir to: ${backupPath}`, | ||
| 124 | ); | ||
| 125 | } | ||
| 126 | await rename(legacyLocalDataDir, requestedLocalDataDir); | ||
| 127 | console.warn( | ||
| 128 | `[executor] Moved legacy local data dir to: ${requestedLocalDataDir}`, | ||
| 129 | ); | ||
| 130 | }, | ||
| 131 | catch: (cause) => | ||
| 132 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 133 | }); | ||
| 134 | |||
| 135 | const createRuntimeWithLegacyMigration = ( | ||
| 136 | requestedLocalDataDir: string, | ||
| 137 | legacyLocalDataDirs: ReadonlyArray<string>, | ||
| 138 | getLocalServerBaseUrl: () => string | undefined, | ||
| 139 | options: StartLocalExecutorServerOptions, | ||
| 140 | ) => | ||
| 141 | Effect.gen(function* () { | ||
| 142 | if ( | ||
| 143 | requestedLocalDataDir !== ":memory:" | ||
| 144 | && !existsSync(requestedLocalDataDir) | ||
| 145 | && legacyLocalDataDirs.length > 0 | ||
| 146 | ) { | ||
| 147 | for (const legacyLocalDataDir of legacyLocalDataDirs) { | ||
| 148 | const legacyExit = yield* Effect.exit( | ||
| 149 | createRuntime(legacyLocalDataDir, getLocalServerBaseUrl, options), | ||
| 150 | ); | ||
| 151 | if (Exit.isFailure(legacyExit)) { | ||
| 152 | continue; | ||
| 153 | } | ||
| 154 | yield* disposeRuntime(legacyExit.value); | ||
| 155 | const migrationExit = yield* Effect.exit( | ||
| 156 | moveLegacyLocalDataDir(legacyLocalDataDir, requestedLocalDataDir), | ||
| 157 | ); | ||
| 158 | if (Exit.isFailure(migrationExit)) { | ||
| 159 | continue; | ||
| 160 | } | ||
| 161 | const migratedExit = yield* Effect.exit( | ||
| 162 | createRuntime(requestedLocalDataDir, getLocalServerBaseUrl, options), | ||
| 163 | ); | ||
| 164 | if (Exit.isSuccess(migratedExit)) { | ||
| 165 | return migratedExit.value; | ||
| 166 | } | ||
| 167 | } | ||
| 168 | } | ||
| 169 | const primaryExit = yield* Effect.exit( | ||
| 170 | createRuntime(requestedLocalDataDir, getLocalServerBaseUrl, options), | ||
| 171 | ); | ||
| 172 | if (Exit.isSuccess(primaryExit)) { | ||
| 173 | return primaryExit.value; | ||
| 174 | } | ||
| 175 | const primaryError = Cause.squash(primaryExit.cause); | ||
| 176 | if (legacyLocalDataDirs.length > 0) { | ||
| 177 | yield* Effect.sync(() => { | ||
| 178 | console.warn( | ||
| 179 | `[executor] Failed to open default local data dir: ${requestedLocalDataDir}`, | ||
| 180 | primaryError instanceof Error ? primaryError.message : String(primaryError), | ||
| 181 | ); | ||
| 182 | }); | ||
| 183 | } | ||
| 184 | for (const legacyLocalDataDir of legacyLocalDataDirs) { | ||
| 185 | const legacyExit = yield* Effect.exit( | ||
| 186 | createRuntime(legacyLocalDataDir, getLocalServerBaseUrl, options), | ||
| 187 | ); | ||
| 188 | if (Exit.isFailure(legacyExit)) { | ||
| 189 | continue; | ||
| 190 | } | ||
| 191 | yield* disposeRuntime(legacyExit.value); | ||
| 192 | const migrationExit = yield* Effect.exit( | ||
| 193 | moveLegacyLocalDataDir(legacyLocalDataDir, requestedLocalDataDir), | ||
| 194 | ); | ||
| 195 | if (Exit.isFailure(migrationExit)) { | ||
| 196 | continue; | ||
| 197 | } | ||
| 198 | const migratedExit = yield* Effect.exit( | ||
| 199 | createRuntime(requestedLocalDataDir, getLocalServerBaseUrl, options), | ||
| 200 | ); | ||
| 201 | if (Exit.isSuccess(migratedExit)) { | ||
| 202 | return migratedExit.value; | ||
| 203 | } | ||
| 204 | } | ||
| 205 | return yield* Effect.fail( | ||
| 206 | primaryError instanceof Error ? primaryError : new Error(String(primaryError)), | ||
| 207 | ); | ||
| 208 | }); | ||
| 209 | |||
| 89 | const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) => | 210 | const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) => |
| 90 | Effect.acquireRelease( | 211 | Effect.acquireRelease( |
| 91 | Effect.sync(() => | 212 | Effect.sync(() => |
| @@ -96,7 +217,7 @@ const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) => · 4 unmodified lines | |||
| 96 | ), | 217 | ), |
| 97 | ), | 218 | ), |
| 98 | ), | 219 | ), |
| 99 | (handler) => Effect.tryPromise({ try: () => handler.dispose(), catch: (cause) => cause instanceof Error ? cause : new Error(String(cause ?? "web handler dispose failed")) }).pipe(Effect.orDie), | 220 | (handler: ControlPlaneWebHandler) => Effect.tryPromise({ try: () => handler.dispose(), catch: (cause) => cause instanceof Error ? cause : new Error(String(cause ?? "web handler dispose failed")) }).pipe(Effect.orDie), |
| 100 | ); | 221 | ); |
| 101 | 222 | ||
| 102 | const safeFilePath = (assetsDir: string, pathname: string): string | null => { | 223 | const safeFilePath = (assetsDir: string, pathname: string): string | null => { |
| @@ -255,11 +376,15 @@ export const createLocalExecutorRequestHandler = ( · 152 unmodified lines | |||
| 255 | options: StartLocalExecutorServerOptions = {}, | 376 | options: StartLocalExecutorServerOptions = {}, |
| 256 | ): Effect.Effect<LocalExecutorRequestHandler, Error, Scope.Scope> => | 377 | ): Effect.Effect<LocalExecutorRequestHandler, Error, Scope.Scope> => |
| 257 | Effect.gen(function* () { | 378 | Effect.gen(function* () { |
| 258 | const localDataDir = options.localDataDir ?? DEFAULT_LOCAL_DATA_DIR; | 379 | const requestedLocalDataDir = options.localDataDir ?? DEFAULT_LOCAL_DATA_DIR; |
| 380 | const legacyLocalDataDirs = | ||
| 381 | options.localDataDir === undefined | ||
| 382 | ? DEFAULT_LEGACY_LOCAL_DATA_DIRS.filter((candidate) => existsSync(candidate)) | ||
| 383 | : []; | ||
| 259 | 384 | ||
| 260 | if (localDataDir !== ":memory:") { | 385 | if (requestedLocalDataDir !== ":memory:") { |
| 261 | yield* Effect.tryPromise({ | 386 | yield* Effect.tryPromise({ |
| 262 | try: () => mkdir(dirname(localDataDir), { recursive: true }), | 387 | try: () => mkdir(dirname(requestedLocalDataDir), { recursive: true }), |
| 263 | catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), | 388 | catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), |
| 264 | }); | 389 | }); |
| 265 | } | 390 | } |
| @@ -267,16 +392,11 @@ export const createLocalExecutorRequestHandler = ( · 1 unmodified line | |||
| 267 | let baseUrlRef: string | undefined; | 392 | let baseUrlRef: string | undefined; |
| 268 | 393 | ||
| 269 | const runtime = yield* Effect.acquireRelease( | 394 | const runtime = yield* Effect.acquireRelease( |
| 270 | createSqlControlPlaneRuntime({ | 395 | createRuntimeWithLegacyMigration( |
| 271 | localDataDir, | 396 | requestedLocalDataDir, |
| 272 | migrationsFolder: options.migrationsFolder, | 397 | legacyLocalDataDirs, |
| 273 | executionResolver: options.executionResolver, | 398 | () => baseUrlRef, |
| 274 | resolveSecretMaterial: options.resolveSecretMaterial, | 399 | options, |
| 275 | getLocalServerBaseUrl: () => baseUrlRef, | ||
| 276 | }).pipe( | ||
| 277 | Effect.mapError((cause) => | ||
| 278 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 279 | ), | ||
| 280 | ), | 400 | ), |
| 281 | disposeRuntime, | 401 | disposeRuntime, |
| 282 | ); | 402 | ); |
| @@ -284,7 +404,14 @@ export const createLocalExecutorRequestHandler = ( · 1 unmodified line | |||
| 284 | const apiHandler = yield* createControlPlaneWebHandler(runtime); | 404 | const apiHandler = yield* createControlPlaneWebHandler(runtime); |
| 285 | const mcpHandler = yield* Effect.acquireRelease( | 405 | const mcpHandler = yield* Effect.acquireRelease( |
| 286 | Effect.sync(() => createExecutorMcpRequestHandler(runtime)), | 406 | Effect.sync(() => createExecutorMcpRequestHandler(runtime)), |
| 287 | (handler) => Effect.tryPromise({ try: () => handler.close(), catch: (cause) => cause instanceof Error ? cause : new Error(String(cause ?? "mcp handler close failed")) }).pipe(Effect.orDie), | 407 | (handler: ExecutorMcpHandler) => |
| 408 | Effect.tryPromise({ | ||
| 409 | try: () => handler.close(), | ||
| 410 | catch: (cause) => | ||
| 411 | cause instanceof Error | ||
| 412 | ? cause | ||
| 413 | : new Error(String(cause ?? "mcp handler close failed")), | ||
| 414 | }).pipe(Effect.orDie), | ||
| 288 | ); | 415 | ); |
| 289 | 416 | ||
| 290 | return { | 417 | return { |