Unified · Split
| @@ -11,7 +11,7 @@ import { · 10 unmodified lines | ||
| 11 | 11 | makeScopedKv, |
| 12 | 12 | migrate, |
| 13 | 13 | } from "@executor/storage-file"; |
| 14 | −import { withConfigFile } from "@executor/config"; | |
| 14 | +import { withConfigFile, loadConfig, SECRET_REF_PREFIX, type ConfigHeaderValue } from "@executor/config"; | |
| 15 | 15 | import { |
| 16 | 16 | openApiPlugin, |
| 17 | 17 | makeKvOperationStore, |
| @@ -69,7 +69,7 @@ export class ExecutorService extends Context.Tag("ExecutorService")< · 51 unmodified lines | ||
| 69 | 69 | // --------------------------------------------------------------------------- |
| 70 | 70 | |
| 71 | 71 | import { homedir } from "node:os"; |
| 72 | −import { join } from "node:path"; | |
| 72 | +import { dirname, join, resolve } from "node:path"; | |
| 73 | 73 | |
| 74 | 74 | const resolveDataDir = (): string => { |
| 75 | 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 | 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 | 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 | 156 | yield* migrate.pipe(Effect.catchAll((e) => Effect.die(e))); |
| 95 | 157 | |
| 96 | − const cwd = process.cwd(); | |
| 158 | + const executorRoot = resolveExecutorRoot(process.cwd()); | |
| 97 | 159 | const kv = makeSqliteKv(sql); |
| 98 | − const config = makeKvConfig(kv, { cwd }); | |
| 99 | − const scopedKv = makeScopedKv(kv, cwd); | |
| 160 | + const config = makeKvConfig(kv, { cwd: executorRoot }); | |
| 161 | + const scopedKv = makeScopedKv(kv, executorRoot); | |
| 100 | 162 | |
| 101 | − const configPath = join(process.cwd(), "executor.jsonc"); | |
| 163 | + const configPath = join(executorRoot, "executor.jsonc"); | |
| 102 | 164 | const fsLayer = NodeFileSystem.layer; |
| 103 | 165 | |
| 104 | − return yield* createExecutor({ | |
| 166 | + const executor = yield* createExecutor({ | |
| 105 | 167 | ...config, |
| 106 | 168 | plugins: [ |
| 107 | 169 | openApiPlugin({ |
| @@ -138,6 +200,65 @@ const ExecutorLayer = Layer.effect( · 30 unmodified lines | ||
| 138 | 200 | }), |
| 139 | 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 | 263 | ).pipe(Layer.provide(SqliteClient.layer({ filename: DB_PATH }))); |
| 143 | 264 | |
File renamed without changes.