Unified · Split
| @@ -11,7 +11,7 @@ import { · 10 unmodified lines | |||
| 11 | makeScopedKv, | 11 | makeScopedKv, |
| 12 | migrate, | 12 | migrate, |
| 13 | } from "@executor/storage-file"; | 13 | } from "@executor/storage-file"; |
| 14 | import { withConfigFile } from "@executor/config"; | 14 | import { withConfigFile, loadConfig, SECRET_REF_PREFIX, type ConfigHeaderValue } from "@executor/config"; |
| 15 | import { | 15 | import { |
| 16 | openApiPlugin, | 16 | openApiPlugin, |
| 17 | makeKvOperationStore, | 17 | makeKvOperationStore, |
| @@ -69,7 +69,7 @@ export class ExecutorService extends Context.Tag("ExecutorService")< · 51 unmodified lines | |||
| 69 | // --------------------------------------------------------------------------- | 69 | // --------------------------------------------------------------------------- |
| 70 | 70 | ||
| 71 | import { homedir } from "node:os"; | 71 | import { homedir } from "node:os"; |
| 72 | import { join } from "node:path"; | 72 | import { dirname, join, resolve } from "node:path"; |
| 73 | 73 | ||
| 74 | const resolveDataDir = (): string => { | 74 | const resolveDataDir = (): string => { |
| 75 | if (process.env.EXECUTOR_DATA_DIR) return process.env.EXECUTOR_DATA_DIR; | 75 | if (process.env.EXECUTOR_DATA_DIR) return process.env.EXECUTOR_DATA_DIR; |
| @@ -82,6 +82,68 @@ fs.mkdirSync(DATA_DIR, { recursive: true }); · 6 unmodified lines | |||
| 82 | 82 | ||
| 83 | const DB_PATH = `${DATA_DIR}/data.db`; | 83 | const DB_PATH = `${DATA_DIR}/data.db`; |
| 84 | 84 | ||
| 85 | const hasWorkspacePackage = (dir: string): boolean => { | ||
| 86 | try { | ||
| 87 | const path = join(dir, "package.json"); | ||
| 88 | if (!fs.existsSync(path)) return false; | ||
| 89 | const parsed = JSON.parse(fs.readFileSync(path, "utf8")) as { workspaces?: unknown }; | ||
| 90 | return Array.isArray(parsed.workspaces); | ||
| 91 | } catch { | ||
| 92 | return false; | ||
| 93 | } | ||
| 94 | }; | ||
| 95 | |||
| 96 | const resolveExecutorRoot = (startDir: string): string => { | ||
| 97 | let current = resolve(startDir); | ||
| 98 | let fallback = current; | ||
| 99 | |||
| 100 | for (;;) { | ||
| 101 | if (fs.existsSync(join(current, "executor.jsonc"))) { | ||
| 102 | return current; | ||
| 103 | } | ||
| 104 | |||
| 105 | if (fs.existsSync(join(current, ".git")) || hasWorkspacePackage(current)) { | ||
| 106 | fallback = current; | ||
| 107 | } | ||
| 108 | |||
| 109 | const parent = dirname(current); | ||
| 110 | if (parent === current) { | ||
| 111 | return fallback; | ||
| 112 | } | ||
| 113 | current = parent; | ||
| 114 | } | ||
| 115 | }; | ||
| 116 | |||
| 117 | const toInvocationHeaders = ( | ||
| 118 | headers: Record<string, ConfigHeaderValue> | undefined, | ||
| 119 | ): Record<string, string | { readonly secretId: string; readonly prefix?: string }> | undefined => { | ||
| 120 | if (!headers) return undefined; | ||
| 121 | |||
| 122 | return Object.fromEntries( | ||
| 123 | Object.entries(headers).map(([key, value]) => { | ||
| 124 | if (typeof value === "string") { | ||
| 125 | return [ | ||
| 126 | key, | ||
| 127 | value.startsWith(SECRET_REF_PREFIX) | ||
| 128 | ? { secretId: value.slice(SECRET_REF_PREFIX.length) } | ||
| 129 | : value, | ||
| 130 | ]; | ||
| 131 | } | ||
| 132 | |||
| 133 | const rawValue = value.value; | ||
| 134 | return [ | ||
| 135 | key, | ||
| 136 | rawValue.startsWith(SECRET_REF_PREFIX) | ||
| 137 | ? { | ||
| 138 | secretId: rawValue.slice(SECRET_REF_PREFIX.length), | ||
| 139 | ...(value.prefix ? { prefix: value.prefix } : {}), | ||
| 140 | } | ||
| 141 | : rawValue, | ||
| 142 | ]; | ||
| 143 | }), | ||
| 144 | ); | ||
| 145 | }; | ||
| 146 | |||
| 85 | // --------------------------------------------------------------------------- | 147 | // --------------------------------------------------------------------------- |
| 86 | // Executor Layer — SQLite-backed, scoped to ManagedRuntime lifetime | 148 | // Executor Layer — SQLite-backed, scoped to ManagedRuntime lifetime |
| 87 | // --------------------------------------------------------------------------- | 149 | // --------------------------------------------------------------------------- |
| @@ -93,15 +155,15 @@ const ExecutorLayer = Layer.effect( · 5 unmodified lines | |||
| 93 | 155 | ||
| 94 | yield* migrate.pipe(Effect.catchAll((e) => Effect.die(e))); | 156 | yield* migrate.pipe(Effect.catchAll((e) => Effect.die(e))); |
| 95 | 157 | ||
| 96 | const cwd = process.cwd(); | 158 | const executorRoot = resolveExecutorRoot(process.cwd()); |
| 97 | const kv = makeSqliteKv(sql); | 159 | const kv = makeSqliteKv(sql); |
| 98 | const config = makeKvConfig(kv, { cwd }); | 160 | const config = makeKvConfig(kv, { cwd: executorRoot }); |
| 99 | const scopedKv = makeScopedKv(kv, cwd); | 161 | const scopedKv = makeScopedKv(kv, executorRoot); |
| 100 | 162 | ||
| 101 | const configPath = join(process.cwd(), "executor.jsonc"); | 163 | const configPath = join(executorRoot, "executor.jsonc"); |
| 102 | const fsLayer = NodeFileSystem.layer; | 164 | const fsLayer = NodeFileSystem.layer; |
| 103 | 165 | ||
| 104 | return yield* createExecutor({ | 166 | const executor = yield* createExecutor({ |
| 105 | ...config, | 167 | ...config, |
| 106 | plugins: [ | 168 | plugins: [ |
| 107 | openApiPlugin({ | 169 | openApiPlugin({ |
| @@ -138,6 +200,65 @@ const ExecutorLayer = Layer.effect( · 30 unmodified lines | |||
| 138 | }), | 200 | }), |
| 139 | ] as const, | 201 | ] as const, |
| 140 | }); | 202 | }); |
| 203 | |||
| 204 | const fileConfig = yield* loadConfig(configPath).pipe( | ||
| 205 | Effect.provide(fsLayer), | ||
| 206 | Effect.catchAll((e) => Effect.die(e)), | ||
| 207 | ); | ||
| 208 | |||
| 209 | if (fileConfig?.sources) { | ||
| 210 | const existingSources = new Set((yield* executor.sources.list()).map((source) => source.id)); | ||
| 211 | |||
| 212 | for (const source of fileConfig.sources) { | ||
| 213 | if (source.namespace && existingSources.has(source.namespace)) { | ||
| 214 | continue; | ||
| 215 | } | ||
| 216 | |||
| 217 | if (source.kind === "openapi") { | ||
| 218 | yield* executor.openapi.addSpec({ | ||
| 219 | spec: source.spec, | ||
| 220 | ...(source.baseUrl ? { baseUrl: source.baseUrl } : {}), | ||
| 221 | ...(source.namespace ? { namespace: source.namespace } : {}), | ||
| 222 | ...(source.headers ? { headers: toInvocationHeaders(source.headers) } : {}), | ||
| 223 | }).pipe(Effect.catchAll((e) => Effect.die(e))); | ||
| 224 | continue; | ||
| 225 | } | ||
| 226 | |||
| 227 | if (source.kind === "graphql") { | ||
| 228 | yield* executor.graphql.addSource({ | ||
| 229 | endpoint: source.endpoint, | ||
| 230 | ...(source.introspectionJson ? { introspectionJson: source.introspectionJson } : {}), | ||
| 231 | ...(source.namespace ? { namespace: source.namespace } : {}), | ||
| 232 | ...(source.headers ? { headers: toInvocationHeaders(source.headers) } : {}), | ||
| 233 | }).pipe(Effect.catchAll((e) => Effect.die(e))); | ||
| 234 | continue; | ||
| 235 | } | ||
| 236 | |||
| 237 | yield* executor.mcp.addSource( | ||
| 238 | source.transport === "stdio" | ||
| 239 | ? { | ||
| 240 | transport: "stdio", | ||
| 241 | name: source.name, | ||
| 242 | command: source.command, | ||
| 243 | ...(source.args ? { args: [...source.args] } : {}), | ||
| 244 | ...(source.env ? { env: source.env } : {}), | ||
| 245 | ...(source.cwd ? { cwd: source.cwd } : {}), | ||
| 246 | ...(source.namespace ? { namespace: source.namespace } : {}), | ||
| 247 | } | ||
| 248 | : { | ||
| 249 | transport: "remote", | ||
| 250 | name: source.name, | ||
| 251 | endpoint: source.endpoint, | ||
| 252 | ...(source.remoteTransport ? { remoteTransport: source.remoteTransport } : {}), | ||
| 253 | ...(source.queryParams ? { queryParams: source.queryParams } : {}), | ||
| 254 | ...(source.headers ? { headers: source.headers } : {}), | ||
| 255 | ...(source.namespace ? { namespace: source.namespace } : {}), | ||
| 256 | }, | ||
| 257 | ).pipe(Effect.catchAll((e) => Effect.die(e))); | ||
| 258 | } | ||
| 259 | } | ||
| 260 | |||
| 261 | return executor; | ||
| 141 | }), | 262 | }), |
| 142 | ).pipe(Layer.provide(SqliteClient.layer({ filename: DB_PATH }))); | 263 | ).pipe(Layer.provide(SqliteClient.layer({ filename: DB_PATH }))); |
| 143 | 264 | ||
File renamed without changes.