Unified · Split
| @@ -10,6 +10,9 @@ · 9 unmodified lines | |||
| 10 | "typescript": "^5.9.3", | 10 | "typescript": "^5.9.3", |
| 11 | }, | 11 | }, |
| 12 | }, | 12 | }, |
| 13 | "apps/docs": { | ||
| 14 | "name": "@executor/docs", | ||
| 15 | }, | ||
| 13 | "apps/executor": { | 16 | "apps/executor": { |
| 14 | "name": "executor", | 17 | "name": "executor", |
| 15 | "version": "1.1.9", | 18 | "version": "1.1.9", |
| @@ -79,9 +82,6 @@ · 63 unmodified lines | |||
| 79 | "vite": "^7.3.1", | 82 | "vite": "^7.3.1", |
| 80 | }, | 83 | }, |
| 81 | }, | 84 | }, |
| 82 | "docs": { | ||
| 83 | "name": "@executor/docs", | ||
| 84 | }, | ||
| 85 | "packages/ai-sdk-adapter": { | 85 | "packages/ai-sdk-adapter": { |
| 86 | "name": "@executor/ai-sdk-adapter", | 86 | "name": "@executor/ai-sdk-adapter", |
| 87 | "dependencies": { | 87 | "dependencies": { |
| @@ -171,6 +171,7 @@ · 83 unmodified lines | |||
| 171 | "drizzle-orm": "1.0.0-beta.15-859cf75", | 171 | "drizzle-orm": "1.0.0-beta.15-859cf75", |
| 172 | "effect": "catalog:", | 172 | "effect": "catalog:", |
| 173 | "graphql": "^16.12.0", | 173 | "graphql": "^16.12.0", |
| 174 | "jsonc-parser": "3.3.1", | ||
| 174 | "postgres": "^3.4.7", | 175 | "postgres": "^3.4.7", |
| 175 | "prettier": "^3.8.1", | 176 | "prettier": "^3.8.1", |
| 176 | }, | 177 | }, |
| @@ -496,7 +497,7 @@ · 319 unmodified lines | |||
| 496 | 497 | ||
| 497 | "@executor/control-plane": ["@executor/control-plane@workspace:packages/control-plane"], | 498 | "@executor/control-plane": ["@executor/control-plane@workspace:packages/control-plane"], |
| 498 | 499 | ||
| 499 | "@executor/docs": ["@executor/docs@workspace:docs"], | 500 | "@executor/docs": ["@executor/docs@workspace:apps/docs"], |
| 500 | 501 | ||
| 501 | "@executor/executor-mcp": ["@executor/executor-mcp@workspace:packages/executor-mcp"], | 502 | "@executor/executor-mcp": ["@executor/executor-mcp@workspace:packages/executor-mcp"], |
| 502 | 503 | ||
| @@ -34,6 +34,7 @@ · 33 unmodified lines | |||
| 34 | "drizzle-orm": "1.0.0-beta.15-859cf75", | 34 | "drizzle-orm": "1.0.0-beta.15-859cf75", |
| 35 | "effect": "catalog:", | 35 | "effect": "catalog:", |
| 36 | "graphql": "^16.12.0", | 36 | "graphql": "^16.12.0", |
| 37 | "jsonc-parser": "3.3.1", | ||
| 37 | "postgres": "^3.4.7", | 38 | "postgres": "^3.4.7", |
| 38 | "prettier": "^3.8.1" | 39 | "prettier": "^3.8.1" |
| 39 | }, | 40 | }, |
| @@ -0,0 +1,96 @@ | |||
| 1 | import { mkdtempSync } from "node:fs"; | ||
| 2 | import { mkdir, writeFile } from "node:fs/promises"; | ||
| 3 | import { tmpdir } from "node:os"; | ||
| 4 | import { join } from "node:path"; | ||
| 5 | import { describe, expect, it } from "@effect/vitest"; | ||
| 6 | import * as Effect from "effect/Effect"; | ||
| 7 | |||
| 8 | import { | ||
| 9 | loadLocalExecutorConfig, | ||
| 10 | resolveLocalWorkspaceContext, | ||
| 11 | } from "./local-config"; | ||
| 12 | |||
| 13 | const makeWorkspaceRoot = () => | ||
| 14 | mkdtempSync(join(tmpdir(), "executor-local-config-")); | ||
| 15 | |||
| 16 | describe("local-config", () => { | ||
| 17 | it.effect("parses jsonc project config with comments and trailing commas", () => | ||
| 18 | Effect.gen(function* () { | ||
| 19 | const workspaceRoot = makeWorkspaceRoot(); | ||
| 20 | const configDirectory = join(workspaceRoot, ".executor"); | ||
| 21 | yield* Effect.promise(() => mkdir(configDirectory, { recursive: true })); | ||
| 22 | yield* Effect.promise(() => | ||
| 23 | writeFile( | ||
| 24 | join(configDirectory, "executor.jsonc"), | ||
| 25 | `{ | ||
| 26 | // local workspace config | ||
| 27 | "sources": { | ||
| 28 | "github": { | ||
| 29 | "kind": "openapi", | ||
| 30 | "connection": { | ||
| 31 | "endpoint": "https://api.github.com", | ||
| 32 | }, | ||
| 33 | "binding": { | ||
| 34 | "specUrl": "https://example.com/openapi.json", | ||
| 35 | }, | ||
| 36 | }, | ||
| 37 | }, | ||
| 38 | } | ||
| 39 | `, | ||
| 40 | "utf8", | ||
| 41 | ), | ||
| 42 | ); | ||
| 43 | |||
| 44 | const context = yield* Effect.promise(() => | ||
| 45 | resolveLocalWorkspaceContext({ workspaceRoot }), | ||
| 46 | ); | ||
| 47 | const loaded = yield* Effect.promise(() => | ||
| 48 | loadLocalExecutorConfig(context), | ||
| 49 | ); | ||
| 50 | |||
| 51 | expect(loaded.config?.sources?.github?.kind).toBe("openapi"); | ||
| 52 | expect(loaded.config?.sources?.github?.connection.endpoint).toBe( | ||
| 53 | "https://api.github.com", | ||
| 54 | ); | ||
| 55 | }), | ||
| 56 | ); | ||
| 57 | |||
| 58 | it.effect("reports jsonc syntax errors with line and column details", () => | ||
| 59 | Effect.gen(function* () { | ||
| 60 | const workspaceRoot = makeWorkspaceRoot(); | ||
| 61 | const configDirectory = join(workspaceRoot, ".executor"); | ||
| 62 | yield* Effect.promise(() => mkdir(configDirectory, { recursive: true })); | ||
| 63 | yield* Effect.promise(() => | ||
| 64 | writeFile( | ||
| 65 | join(configDirectory, "executor.jsonc"), | ||
| 66 | `{ | ||
| 67 | "sources": { | ||
| 68 | "github": { | ||
| 69 | "kind": "openapi" | ||
| 70 | "connection": { | ||
| 71 | "endpoint": "https://api.github.com" | ||
| 72 | } | ||
| 73 | } | ||
| 74 | } | ||
| 75 | } | ||
| 76 | `, | ||
| 77 | "utf8", | ||
| 78 | ), | ||
| 79 | ); | ||
| 80 | |||
| 81 | const context = yield* Effect.promise(() => | ||
| 82 | resolveLocalWorkspaceContext({ workspaceRoot }), | ||
| 83 | ); | ||
| 84 | const failure = yield* Effect.flip( | ||
| 85 | Effect.tryPromise({ | ||
| 86 | try: () => loadLocalExecutorConfig(context), | ||
| 87 | catch: (cause) => | ||
| 88 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 89 | }), | ||
| 90 | ); | ||
| 91 | |||
| 92 | expect(failure.message).toContain("Invalid executor config"); | ||
| 93 | expect(failure.message).toContain("line 5, column 7"); | ||
| 94 | }), | ||
| 95 | ); | ||
| 96 | }); | ||
| @@ -2,6 +2,11 @@ import { createHash } from "node:crypto"; · 1 unmodified line | |||
| 2 | import { homedir } from "node:os"; | 2 | import { homedir } from "node:os"; |
| 3 | import { basename, dirname, isAbsolute, join, resolve } from "node:path"; | 3 | import { basename, dirname, isAbsolute, join, resolve } from "node:path"; |
| 4 | import { promises as fs } from "node:fs"; | 4 | import { promises as fs } from "node:fs"; |
| 5 | import { | ||
| 6 | type ParseError as JsoncParseError, | ||
| 7 | parse as parseJsoncDocument, | ||
| 8 | printParseErrorCode, | ||
| 9 | } from "jsonc-parser"; | ||
| 5 | 10 | ||
| 6 | import { | 11 | import { |
| 7 | LocalExecutorConfigSchema, | 12 | LocalExecutorConfigSchema, |
| @@ -49,74 +54,38 @@ const trimOrUndefined = (value: string | undefined | null): string | undefined = · 41 unmodified lines | |||
| 49 | return trimmed && trimmed.length > 0 ? trimmed : undefined; | 54 | return trimmed && trimmed.length > 0 ? trimmed : undefined; |
| 50 | }; | 55 | }; |
| 51 | 56 | ||
| 52 | const stripJsonComments = (input: string): string => { | 57 | const formatJsoncParseErrors = (content: string, errors: readonly JsoncParseError[]): string => { |
| 53 | let output = ""; | 58 | const lines = content.split("\n"); |
| 54 | let inString = false; | ||
| 55 | let stringChar = '"'; | ||
| 56 | let escapeNext = false; | ||
| 57 | let inLineComment = false; | ||
| 58 | let inBlockComment = false; | ||
| 59 | |||
| 60 | for (let index = 0; index < input.length; index += 1) { | ||
| 61 | const char = input[index]!; | ||
| 62 | const next = input[index + 1]; | ||
| 63 | |||
| 64 | if (inLineComment) { | ||
| 65 | if (char === "\n") { | ||
| 66 | inLineComment = false; | ||
| 67 | output += char; | ||
| 68 | } | ||
| 69 | continue; | ||
| 70 | } | ||
| 71 | 59 | ||
| 72 | if (inBlockComment) { | 60 | return errors |
| 73 | if (char === "*" && next === "/") { | 61 | .map((error) => { |
| 74 | inBlockComment = false; | 62 | const beforeOffset = content.slice(0, error.offset).split("\n"); |
| 75 | index += 1; | 63 | const line = beforeOffset.length; |
| 76 | } | 64 | const column = beforeOffset[beforeOffset.length - 1]?.length ?? 0; |
| 77 | continue; | 65 | const lineText = lines[line - 1]; |
| 78 | } | 66 | const location = `line ${line}, column ${column + 1}`; |
| 67 | const detail = printParseErrorCode(error.error); | ||
| 79 | 68 | ||
| 80 | if (inString) { | 69 | if (!lineText) { |
| 81 | output += char; | 70 | return `${detail} at ${location}`; |
| 82 | if (escapeNext) { | ||
| 83 | escapeNext = false; | ||
| 84 | } else if (char === "\\") { | ||
| 85 | escapeNext = true; | ||
| 86 | } else if (char === stringChar) { | ||
| 87 | inString = false; | ||
| 88 | } | 71 | } |
| 89 | continue; | ||
| 90 | } | ||
| 91 | |||
| 92 | if ((char === '"' || char === "'") && !inString) { | ||
| 93 | inString = true; | ||
| 94 | stringChar = char; | ||
| 95 | output += char; | ||
| 96 | continue; | ||
| 97 | } | ||
| 98 | 72 | ||
| 99 | if (char === "/" && next === "/") { | 73 | return `${detail} at ${location}\n${lineText}`; |
| 100 | inLineComment = true; | 74 | }) |
| 101 | index += 1; | 75 | .join("\n"); |
| 102 | continue; | ||
| 103 | } | ||
| 104 | |||
| 105 | if (char === "/" && next === "*") { | ||
| 106 | inBlockComment = true; | ||
| 107 | index += 1; | ||
| 108 | continue; | ||
| 109 | } | ||
| 110 | |||
| 111 | output += char; | ||
| 112 | } | ||
| 113 | |||
| 114 | return output; | ||
| 115 | }; | 76 | }; |
| 116 | 77 | ||
| 117 | const parseJsonc = (input: { path: string; content: string }): LocalExecutorConfig => { | 78 | const parseJsonc = (input: { path: string; content: string }): LocalExecutorConfig => { |
| 79 | const errors: JsoncParseError[] = []; | ||
| 80 | |||
| 118 | try { | 81 | try { |
| 119 | const parsed = JSON.parse(stripJsonComments(input.content)) as unknown; | 82 | const parsed = parseJsoncDocument(input.content, errors, { |
| 83 | allowTrailingComma: true, | ||
| 84 | }); | ||
| 85 | if (errors.length > 0) { | ||
| 86 | throw new Error(formatJsoncParseErrors(input.content, errors)); | ||
| 87 | } | ||
| 88 | |||
| 120 | return decodeLocalExecutorConfig(parsed); | 89 | return decodeLocalExecutorConfig(parsed); |
| 121 | } catch (cause) { | 90 | } catch (cause) { |
| 122 | const message = cause instanceof Error ? cause.message : String(cause); | 91 | const message = cause instanceof Error ? cause.message : String(cause); |