toolcraft-openapi 0.0.38 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/bin/generate.d.ts +9 -0
- package/dist/bin/generate.js +122 -6
- package/dist/config.d.ts +53 -0
- package/dist/config.js +266 -0
- package/dist/define-client.js +4 -0
- package/dist/diagnose.d.ts +4 -0
- package/dist/diagnose.js +93 -0
- package/dist/diagnostics.d.ts +19 -0
- package/dist/diagnostics.js +19 -0
- package/dist/generate.d.ts +13 -1
- package/dist/generate.js +165 -12
- package/dist/http.d.ts +30 -25
- package/dist/http.js +184 -42
- package/dist/index.d.ts +7 -2
- package/dist/index.js +5 -2
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +30 -5
- package/node_modules/@poe-code/frontmatter/README.md +35 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +8 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.js +72 -0
- package/node_modules/@poe-code/frontmatter/dist/index.d.ts +3 -0
- package/node_modules/@poe-code/frontmatter/dist/index.js +3 -0
- package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +20 -0
- package/node_modules/@poe-code/frontmatter/dist/parse.js +137 -0
- package/node_modules/@poe-code/frontmatter/dist/stringify.d.ts +1 -0
- package/node_modules/@poe-code/frontmatter/dist/stringify.js +35 -0
- package/node_modules/@poe-code/frontmatter/package.json +25 -0
- package/node_modules/toolcraft-design/README.md +12 -0
- package/node_modules/toolcraft-design/dist/prompts/index.d.ts +39 -14
- package/node_modules/toolcraft-design/dist/prompts/index.js +10 -6
- package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.js +4 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.d.ts +9 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.js +47 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +55 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +274 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.d.ts +20 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.js +53 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/index.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/index.js +6 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/keys.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/keys.js +28 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.d.ts +13 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.js +131 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.js +52 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/password.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/password.js +55 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/select.d.ts +18 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/select.js +89 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.d.ts +21 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.js +32 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/text.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/text.js +60 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.js +18 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.js +1 -1
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/frontmatter.js +20 -453
- package/node_modules/toolcraft-design/package.json +6 -3
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -24,6 +24,10 @@ command files.
|
|
|
24
24
|
- `--input <path-or-url>` — OpenAPI document to read. Defaults to `openapi.json`.
|
|
25
25
|
- `--output <dir>` — directory for generated files. Defaults to `src/generated`.
|
|
26
26
|
- `--check` — exits non-zero when generated files would change.
|
|
27
|
+
- `--diff` — prints the generated file changes without writing them.
|
|
28
|
+
|
|
29
|
+
When `toolcraft.yml` is present next to the input file, the generator validates it, prints
|
|
30
|
+
diagnostics, and uses it to shape generated command names for mapped resources.
|
|
27
31
|
|
|
28
32
|
### CI drift check
|
|
29
33
|
|
|
@@ -36,6 +40,14 @@ toolcraft-openapi-generate --check
|
|
|
36
40
|
- `bearerTokenAuth(opts)`
|
|
37
41
|
- `requestJson(options)`
|
|
38
42
|
- `HttpError`
|
|
43
|
+
- `readToolcraftConfig(path)`
|
|
44
|
+
- `validateToolcraftConfig(value)`
|
|
45
|
+
- `mergeToolcraftConfig(base, override)`
|
|
46
|
+
- `diagnose(config, document)`
|
|
47
|
+
- `formatDiagnostic(diagnostic)`
|
|
48
|
+
- `formatDiagnostics(diagnostics)`
|
|
49
|
+
- `resolveOpenApiBaseUrl(options)`
|
|
50
|
+
- `DIAGNOSTIC_CODES`
|
|
39
51
|
- `TokenSource`
|
|
40
52
|
- `CommandContributor`
|
|
41
53
|
- `AuthProvider`
|
|
@@ -44,9 +56,64 @@ toolcraft-openapi-generate --check
|
|
|
44
56
|
|
|
45
57
|
- `<envVar passed to bearerTokenAuth>` — bearer token override; wins over stored credentials.
|
|
46
58
|
- `AUTH_BACKEND` — forwarded to `auth-store` to select `file` or `keychain` storage.
|
|
59
|
+
- `TOOLCRAFT_OPENAPI_ENV` — selects a configured OpenAPI environment when using
|
|
60
|
+
`resolveOpenApiBaseUrl`.
|
|
47
61
|
|
|
48
62
|
## Configuration
|
|
49
63
|
|
|
64
|
+
### `toolcraft.yml`
|
|
65
|
+
|
|
66
|
+
`toolcraft.yml` lives next to `openapi.json`. Without it, OpenAPI-only generation remains the
|
|
67
|
+
default.
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
edition: 2026-05-16
|
|
71
|
+
|
|
72
|
+
client_settings:
|
|
73
|
+
idempotency_header: Idempotency-Key
|
|
74
|
+
|
|
75
|
+
pagination:
|
|
76
|
+
cursor:
|
|
77
|
+
request: { cursor: cursor, limit: limit }
|
|
78
|
+
response: { items: data, next_cursor: meta.next_cursor }
|
|
79
|
+
|
|
80
|
+
resources:
|
|
81
|
+
messages:
|
|
82
|
+
methods:
|
|
83
|
+
list: get /messages { pagination: cursor }
|
|
84
|
+
create: post /messages { idempotent: true }
|
|
85
|
+
|
|
86
|
+
unspecified_endpoints:
|
|
87
|
+
- get /internal/health
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Supported keys:
|
|
91
|
+
|
|
92
|
+
- `edition` — required when `toolcraft.yml` exists. Currently `2026-05-16`.
|
|
93
|
+
- `environments` — named base URLs for `resolveOpenApiBaseUrl`.
|
|
94
|
+
- `client_settings.idempotency_header` — header used for generated idempotency keys.
|
|
95
|
+
- `client_settings.auth.*.env` — declarative auth environment metadata.
|
|
96
|
+
- `pagination` — named request/response field mappings. Schemes are validated today; iterator
|
|
97
|
+
generation is not implemented yet.
|
|
98
|
+
- `resources` — resource/method mapping. Mapped methods shape generated names and file paths.
|
|
99
|
+
- `readme.examples` — examples emitted into generated command help and MCP descriptions.
|
|
100
|
+
- `unspecified_endpoints` — endpoints intentionally omitted from `resources`.
|
|
101
|
+
|
|
102
|
+
Mapped idempotent methods with `client_settings.idempotency_header` get an optional
|
|
103
|
+
`idempotencyKey` param across CLI, MCP, and SDK surfaces. Mapped methods also get a CLI/SDK
|
|
104
|
+
`rawResponse` param that returns `{ data, response }`; the CLI accepts both `--raw-response` and
|
|
105
|
+
`--raw`.
|
|
106
|
+
|
|
107
|
+
Diagnostics use stable codes:
|
|
108
|
+
|
|
109
|
+
- `TOOLCRAFT_OPENAPI_001` — endpoint is not mapped or listed in `unspecified_endpoints`.
|
|
110
|
+
- `TOOLCRAFT_OPENAPI_002` — duplicate configured method path.
|
|
111
|
+
- `TOOLCRAFT_OPENAPI_003` — unknown pagination scheme.
|
|
112
|
+
- `TOOLCRAFT_OPENAPI_004` — reserved for spec drift.
|
|
113
|
+
- `TOOLCRAFT_OPENAPI_005` — reserved method name.
|
|
114
|
+
- `TOOLCRAFT_OPENAPI_006` — missing or unsupported edition.
|
|
115
|
+
- `TOOLCRAFT_OPENAPI_007` — invalid config shape.
|
|
116
|
+
|
|
50
117
|
### `bearerTokenAuth(opts)`
|
|
51
118
|
|
|
52
119
|
- `serviceName: string` — auth-store service name and file-store key.
|
package/dist/bin/generate.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type OutputFormat } from "toolcraft-design";
|
|
3
|
+
import { type Diagnostic } from "../diagnostics.js";
|
|
4
|
+
import { type GeneratedFile } from "../generate.js";
|
|
3
5
|
interface GenerateCliFileSystem {
|
|
4
6
|
lstat(targetPath: string): Promise<{
|
|
5
7
|
isDirectory(): boolean;
|
|
@@ -33,6 +35,7 @@ interface GenerateCliServices {
|
|
|
33
35
|
}
|
|
34
36
|
interface GenerateCliOptions {
|
|
35
37
|
check: boolean;
|
|
38
|
+
diff: boolean;
|
|
36
39
|
input: string;
|
|
37
40
|
inspect: boolean;
|
|
38
41
|
outputFormat: OutputFormat;
|
|
@@ -40,10 +43,16 @@ interface GenerateCliOptions {
|
|
|
40
43
|
}
|
|
41
44
|
interface SyncGeneratedClientResult {
|
|
42
45
|
deletedFileCount: number;
|
|
46
|
+
deletedFiles: string[];
|
|
47
|
+
diagnostics: Diagnostic[];
|
|
43
48
|
drifted: boolean;
|
|
44
49
|
specSha: string;
|
|
50
|
+
updatedFiles: UpdatedGeneratedFile[];
|
|
45
51
|
updatedFileCount: number;
|
|
46
52
|
}
|
|
53
|
+
interface UpdatedGeneratedFile extends GeneratedFile {
|
|
54
|
+
previousContents?: string;
|
|
55
|
+
}
|
|
47
56
|
export declare function runGenerateCli(argv?: string[], services?: GenerateCliServices): Promise<number>;
|
|
48
57
|
export declare function syncGeneratedClient(options: GenerateCliOptions, services: Pick<GenerateCliServices, "cwd" | "fetch" | "fs">): Promise<SyncGeneratedClientResult>;
|
|
49
58
|
export {};
|
package/dist/bin/generate.js
CHANGED
|
@@ -7,12 +7,16 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { UserError } from "toolcraft";
|
|
8
8
|
import { hasOwnErrorCode } from "../error-codes.js";
|
|
9
9
|
import { withOutputFormat } from "toolcraft-design";
|
|
10
|
+
import { readToolcraftConfig } from "../config.js";
|
|
11
|
+
import { diagnose } from "../diagnose.js";
|
|
12
|
+
import { formatDiagnostics } from "../diagnostics.js";
|
|
10
13
|
import { generate } from "../generate.js";
|
|
11
14
|
import { inspectOpenApiSource } from "../inspect-source.js";
|
|
12
15
|
import { renderOpenApiInspection } from "../render-inspection.js";
|
|
13
16
|
import { parseOpenApiDocument, readOpenApiSourceText } from "../spec-source.js";
|
|
14
17
|
const DEFAULT_OPTIONS = {
|
|
15
18
|
check: false,
|
|
19
|
+
diff: false,
|
|
16
20
|
input: "openapi.json",
|
|
17
21
|
inspect: false,
|
|
18
22
|
outputFormat: "terminal",
|
|
@@ -24,6 +28,7 @@ Options:
|
|
|
24
28
|
--input <path-or-url> OpenAPI document to read (default: openapi.json)
|
|
25
29
|
--output <dir> Directory for generated command files (default: src/generated)
|
|
26
30
|
--check Exit non-zero if generated output would change
|
|
31
|
+
--diff Print a diff of generated changes without writing files
|
|
27
32
|
--inspect Inspect route compatibility without writing files
|
|
28
33
|
--output-format <fmt> Inspection output: terminal, markdown, or json (default: terminal)
|
|
29
34
|
-h, --help Show this help text
|
|
@@ -47,11 +52,26 @@ export async function runGenerateCli(argv = process.argv, services = {
|
|
|
47
52
|
return 0;
|
|
48
53
|
}
|
|
49
54
|
const result = await syncGeneratedClient(parsed, services);
|
|
55
|
+
if (result.diagnostics.length > 0) {
|
|
56
|
+
services.stderr.write(formatDiagnostics(result.diagnostics));
|
|
57
|
+
}
|
|
58
|
+
if (parsed.diff) {
|
|
59
|
+
if (!result.drifted) {
|
|
60
|
+
services.stdout.write(`OpenAPI output is up to date (${result.specSha}).\n`);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
services.stdout.write(renderGeneratedDiff(result, services.cwd));
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
50
66
|
if (parsed.check) {
|
|
51
67
|
if (result.drifted) {
|
|
52
68
|
services.stderr.write(`OpenAPI output is out of date for ${parsed.outputDir}. Run the generator without --check to update it.\n`);
|
|
53
69
|
return 1;
|
|
54
70
|
}
|
|
71
|
+
if (hasErrorDiagnostics(result.diagnostics)) {
|
|
72
|
+
services.stderr.write("OpenAPI diagnostics failed for toolcraft.yml.\n");
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
55
75
|
services.stdout.write(`OpenAPI output is up to date (${result.specSha}).\n`);
|
|
56
76
|
return 0;
|
|
57
77
|
}
|
|
@@ -81,7 +101,12 @@ export async function syncGeneratedClient(options, services) {
|
|
|
81
101
|
const sourceText = await readOpenApiSourceText(options.input, services);
|
|
82
102
|
const specSha = createSpecSha(sourceText);
|
|
83
103
|
const document = parseOpenApiDocument(sourceText, options.input);
|
|
84
|
-
const
|
|
104
|
+
const configResult = await readAdjacentToolcraftConfig(options.input, services);
|
|
105
|
+
const diagnostics = configResult.config === undefined
|
|
106
|
+
? configResult.diagnostics
|
|
107
|
+
: [...configResult.diagnostics, ...diagnose(configResult.config, document)];
|
|
108
|
+
const effectiveConfig = hasErrorDiagnostics(diagnostics) ? undefined : configResult.config;
|
|
109
|
+
const generatedFiles = generate(document, { specSha, config: effectiveConfig });
|
|
85
110
|
const outputDir = path.resolve(services.cwd, options.outputDir);
|
|
86
111
|
const currentFiles = await readGeneratedFiles(services.fs, outputDir);
|
|
87
112
|
const desiredFiles = new Map([
|
|
@@ -91,7 +116,7 @@ export async function syncGeneratedClient(options, services) {
|
|
|
91
116
|
const updatedFiles = collectUpdatedFiles(currentFiles, desiredFiles);
|
|
92
117
|
const deletedFiles = collectDeletedFiles(currentFiles, desiredFiles);
|
|
93
118
|
const drifted = updatedFiles.length > 0 || deletedFiles.length > 0;
|
|
94
|
-
if (!options.check && drifted) {
|
|
119
|
+
if (!options.check && !options.diff && drifted) {
|
|
95
120
|
try {
|
|
96
121
|
await writeGeneratedFiles(services.fs, outputDir, updatedFiles);
|
|
97
122
|
await deleteGeneratedFiles(services.fs, outputDir, deletedFiles);
|
|
@@ -102,12 +127,98 @@ export async function syncGeneratedClient(options, services) {
|
|
|
102
127
|
}
|
|
103
128
|
}
|
|
104
129
|
return {
|
|
130
|
+
deletedFiles,
|
|
131
|
+
deletedFileCount: deletedFiles.length,
|
|
132
|
+
diagnostics,
|
|
105
133
|
drifted,
|
|
106
134
|
specSha,
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
updatedFiles,
|
|
136
|
+
updatedFileCount: updatedFiles.length
|
|
109
137
|
};
|
|
110
138
|
}
|
|
139
|
+
async function readAdjacentToolcraftConfig(input, services) {
|
|
140
|
+
const configPath = resolveAdjacentConfigPath(input, services.cwd);
|
|
141
|
+
if (configPath === undefined) {
|
|
142
|
+
return { diagnostics: [] };
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
return await readToolcraftConfig(configPath, { fs: services.fs });
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (isNotFoundError(error)) {
|
|
149
|
+
return { diagnostics: [] };
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function resolveAdjacentConfigPath(input, cwd) {
|
|
155
|
+
const inputUrl = tryParseUrl(input);
|
|
156
|
+
if (inputUrl !== null && (inputUrl.protocol === "http:" || inputUrl.protocol === "https:")) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
return path.resolve(cwd, path.dirname(input), "toolcraft.yml");
|
|
160
|
+
}
|
|
161
|
+
function hasErrorDiagnostics(diagnostics) {
|
|
162
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
163
|
+
}
|
|
164
|
+
function renderGeneratedDiff(result, outputDir) {
|
|
165
|
+
const sections = [];
|
|
166
|
+
for (const file of result.updatedFiles) {
|
|
167
|
+
const relativePath = path.relative(outputDir, file.path);
|
|
168
|
+
sections.push(renderFileDiff(relativePath, file.previousContents ?? "", file.contents));
|
|
169
|
+
}
|
|
170
|
+
for (const filePath of result.deletedFiles) {
|
|
171
|
+
const relativePath = path.relative(outputDir, filePath);
|
|
172
|
+
sections.push([`--- ${relativePath}`, `+++ /dev/null`, "-<deleted>"].join("\n"));
|
|
173
|
+
}
|
|
174
|
+
return `${sections.join("\n")}\n`;
|
|
175
|
+
}
|
|
176
|
+
function renderFileDiff(relativePath, previousContents, nextContents) {
|
|
177
|
+
const previousLines = previousContents.split("\n");
|
|
178
|
+
const nextLines = nextContents.split("\n");
|
|
179
|
+
return [
|
|
180
|
+
`--- ${relativePath}`,
|
|
181
|
+
`+++ ${relativePath}`,
|
|
182
|
+
...collectChangedDiffLines(previousLines, nextLines)
|
|
183
|
+
].join("\n");
|
|
184
|
+
}
|
|
185
|
+
function collectChangedDiffLines(previousLines, nextLines) {
|
|
186
|
+
const lengths = Array.from({ length: previousLines.length + 1 }, () => Array(nextLines.length + 1).fill(0));
|
|
187
|
+
for (let leftIndex = previousLines.length - 1; leftIndex >= 0; leftIndex -= 1) {
|
|
188
|
+
for (let rightIndex = nextLines.length - 1; rightIndex >= 0; rightIndex -= 1) {
|
|
189
|
+
lengths[leftIndex][rightIndex] =
|
|
190
|
+
previousLines[leftIndex] === nextLines[rightIndex]
|
|
191
|
+
? lengths[leftIndex + 1][rightIndex + 1] + 1
|
|
192
|
+
: Math.max(lengths[leftIndex + 1][rightIndex], lengths[leftIndex][rightIndex + 1]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const lines = [];
|
|
196
|
+
let leftIndex = 0;
|
|
197
|
+
let rightIndex = 0;
|
|
198
|
+
while (leftIndex < previousLines.length && rightIndex < nextLines.length) {
|
|
199
|
+
if (previousLines[leftIndex] === nextLines[rightIndex]) {
|
|
200
|
+
leftIndex += 1;
|
|
201
|
+
rightIndex += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (lengths[leftIndex + 1][rightIndex] >= lengths[leftIndex][rightIndex + 1]) {
|
|
205
|
+
lines.push(`-${previousLines[leftIndex]}`);
|
|
206
|
+
leftIndex += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
lines.push(`+${nextLines[rightIndex]}`);
|
|
210
|
+
rightIndex += 1;
|
|
211
|
+
}
|
|
212
|
+
while (leftIndex < previousLines.length) {
|
|
213
|
+
lines.push(`-${previousLines[leftIndex]}`);
|
|
214
|
+
leftIndex += 1;
|
|
215
|
+
}
|
|
216
|
+
while (rightIndex < nextLines.length) {
|
|
217
|
+
lines.push(`+${nextLines[rightIndex]}`);
|
|
218
|
+
rightIndex += 1;
|
|
219
|
+
}
|
|
220
|
+
return lines;
|
|
221
|
+
}
|
|
111
222
|
function parseGenerateCliArgs(argv) {
|
|
112
223
|
const options = { ...DEFAULT_OPTIONS };
|
|
113
224
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -119,6 +230,10 @@ function parseGenerateCliArgs(argv) {
|
|
|
119
230
|
options.check = true;
|
|
120
231
|
continue;
|
|
121
232
|
}
|
|
233
|
+
if (argument === "--diff") {
|
|
234
|
+
options.diff = true;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
122
237
|
if (argument === "--inspect") {
|
|
123
238
|
options.inspect = true;
|
|
124
239
|
continue;
|
|
@@ -228,10 +343,11 @@ async function readGeneratedFiles(fs, directoryPath) {
|
|
|
228
343
|
function collectUpdatedFiles(currentFiles, desiredFiles) {
|
|
229
344
|
const updatedFiles = [];
|
|
230
345
|
for (const [filePath, contents] of desiredFiles) {
|
|
231
|
-
|
|
346
|
+
const previousContents = currentFiles.get(filePath);
|
|
347
|
+
if (previousContents === contents) {
|
|
232
348
|
continue;
|
|
233
349
|
}
|
|
234
|
-
updatedFiles.push({ path: filePath, contents });
|
|
350
|
+
updatedFiles.push({ path: filePath, contents, previousContents });
|
|
235
351
|
}
|
|
236
352
|
return updatedFiles;
|
|
237
353
|
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Diagnostic } from "./diagnostics.js";
|
|
2
|
+
export declare const SUPPORTED_TOOLCRAFT_EDITION = "2026-05-16";
|
|
3
|
+
export type ToolcraftHttpMethod = "get" | "post" | "put" | "patch" | "delete" | "head" | "options";
|
|
4
|
+
export interface ToolcraftMethodConfig {
|
|
5
|
+
method: ToolcraftHttpMethod;
|
|
6
|
+
path: string;
|
|
7
|
+
pagination?: string;
|
|
8
|
+
idempotent?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolcraftResourceConfig {
|
|
11
|
+
methods?: Record<string, ToolcraftMethodConfig>;
|
|
12
|
+
subresources?: Record<string, ToolcraftResourceConfig>;
|
|
13
|
+
}
|
|
14
|
+
export interface ToolcraftConfig {
|
|
15
|
+
edition: string;
|
|
16
|
+
environments?: Record<string, string>;
|
|
17
|
+
client_settings?: {
|
|
18
|
+
idempotency_header?: string;
|
|
19
|
+
auth?: Record<string, {
|
|
20
|
+
env?: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
pagination?: Record<string, {
|
|
24
|
+
request: Record<string, string>;
|
|
25
|
+
response: Record<string, string>;
|
|
26
|
+
}>;
|
|
27
|
+
retries?: {
|
|
28
|
+
max?: number;
|
|
29
|
+
backoff?: "exponential";
|
|
30
|
+
retry_on?: number[];
|
|
31
|
+
};
|
|
32
|
+
resources?: Record<string, ToolcraftResourceConfig>;
|
|
33
|
+
readme?: {
|
|
34
|
+
examples?: Record<string, Array<{
|
|
35
|
+
title: string;
|
|
36
|
+
params: Record<string, unknown>;
|
|
37
|
+
}>>;
|
|
38
|
+
};
|
|
39
|
+
unspecified_endpoints?: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface ReadToolcraftConfigOptions {
|
|
42
|
+
cwd?: string;
|
|
43
|
+
fs?: {
|
|
44
|
+
readFile(filePath: string, encoding: BufferEncoding): Promise<string>;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export interface ToolcraftConfigResult {
|
|
48
|
+
config?: ToolcraftConfig;
|
|
49
|
+
diagnostics: Diagnostic[];
|
|
50
|
+
}
|
|
51
|
+
export declare function readToolcraftConfig(filePath: string, options?: ReadToolcraftConfigOptions): Promise<ToolcraftConfigResult>;
|
|
52
|
+
export declare function validateToolcraftConfig(value: unknown): ToolcraftConfigResult;
|
|
53
|
+
export declare function mergeToolcraftConfig(base: Partial<ToolcraftConfig>, override: Partial<ToolcraftConfig>): Partial<ToolcraftConfig>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parse } from "yaml";
|
|
3
|
+
import { S, validate } from "toolcraft-schema";
|
|
4
|
+
import { DIAGNOSTIC_CODES, createDiagnostic } from "./diagnostics.js";
|
|
5
|
+
export const SUPPORTED_TOOLCRAFT_EDITION = "2026-05-16";
|
|
6
|
+
const configSchema = S.Object({
|
|
7
|
+
edition: S.Optional(S.String()),
|
|
8
|
+
environments: S.Optional(S.Record(S.String())),
|
|
9
|
+
client_settings: S.Optional(S.Object({
|
|
10
|
+
idempotency_header: S.Optional(S.String()),
|
|
11
|
+
auth: S.Optional(S.Record(S.Object({
|
|
12
|
+
env: S.Optional(S.String())
|
|
13
|
+
}, { additionalProperties: false })))
|
|
14
|
+
}, { additionalProperties: false })),
|
|
15
|
+
pagination: S.Optional(S.Record(S.Object({
|
|
16
|
+
request: S.Record(S.String()),
|
|
17
|
+
response: S.Record(S.String())
|
|
18
|
+
}, { additionalProperties: false }))),
|
|
19
|
+
retries: S.Optional(S.Object({
|
|
20
|
+
max: S.Optional(S.Number({ jsonType: "integer", minimum: 0 })),
|
|
21
|
+
backoff: S.Optional(S.Enum(["exponential"])),
|
|
22
|
+
retry_on: S.Optional(S.Array(S.Number({ jsonType: "integer", minimum: 100, maximum: 599 })))
|
|
23
|
+
}, { additionalProperties: false })),
|
|
24
|
+
resources: S.Optional(S.Json()),
|
|
25
|
+
readme: S.Optional(S.Json()),
|
|
26
|
+
unspecified_endpoints: S.Optional(S.Array(S.String()))
|
|
27
|
+
}, { additionalProperties: false });
|
|
28
|
+
export async function readToolcraftConfig(filePath, options = {}) {
|
|
29
|
+
const fs = options.fs ?? (await import("node:fs/promises"));
|
|
30
|
+
const resolvedPath = path.resolve(options.cwd ?? process.cwd(), filePath);
|
|
31
|
+
const source = await fs.readFile(resolvedPath, "utf8");
|
|
32
|
+
const parsed = parseToolcraftYaml(source);
|
|
33
|
+
return validateToolcraftConfig(parsed);
|
|
34
|
+
}
|
|
35
|
+
function parseToolcraftYaml(source) {
|
|
36
|
+
try {
|
|
37
|
+
return parse(source);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return parse(quoteMethodShorthandValues(source));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function quoteMethodShorthandValues(source) {
|
|
44
|
+
let methodsIndent;
|
|
45
|
+
return source
|
|
46
|
+
.split("\n")
|
|
47
|
+
.map((line) => {
|
|
48
|
+
const trimmed = line.trim();
|
|
49
|
+
if (trimmed.length === 0) {
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
52
|
+
const indent = countLeadingSpaces(line);
|
|
53
|
+
if (methodsIndent !== undefined && indent <= methodsIndent) {
|
|
54
|
+
methodsIndent = undefined;
|
|
55
|
+
}
|
|
56
|
+
if (trimmed === "methods:") {
|
|
57
|
+
methodsIndent = indent;
|
|
58
|
+
return line;
|
|
59
|
+
}
|
|
60
|
+
if (methodsIndent === undefined) {
|
|
61
|
+
return line;
|
|
62
|
+
}
|
|
63
|
+
const separatorIndex = line.indexOf(":");
|
|
64
|
+
if (separatorIndex < 0) {
|
|
65
|
+
return line;
|
|
66
|
+
}
|
|
67
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
68
|
+
if (!value.includes("{") ||
|
|
69
|
+
value.startsWith("{") ||
|
|
70
|
+
value.startsWith("\"") ||
|
|
71
|
+
value.startsWith("'")) {
|
|
72
|
+
return line;
|
|
73
|
+
}
|
|
74
|
+
return `${line.slice(0, separatorIndex + 1)} ${JSON.stringify(value)}`;
|
|
75
|
+
})
|
|
76
|
+
.join("\n");
|
|
77
|
+
}
|
|
78
|
+
function countLeadingSpaces(value) {
|
|
79
|
+
let count = 0;
|
|
80
|
+
for (const character of value) {
|
|
81
|
+
if (character !== " ") {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
count += 1;
|
|
85
|
+
}
|
|
86
|
+
return count;
|
|
87
|
+
}
|
|
88
|
+
export function validateToolcraftConfig(value) {
|
|
89
|
+
const result = validate(configSchema, value);
|
|
90
|
+
const diagnostics = [];
|
|
91
|
+
if (!isPlainObject(value) || typeof value.edition !== "string") {
|
|
92
|
+
diagnostics.push(createDiagnostic({
|
|
93
|
+
code: DIAGNOSTIC_CODES.invalidEdition,
|
|
94
|
+
severity: "error",
|
|
95
|
+
location: "edition",
|
|
96
|
+
message: `toolcraft.yml must set edition: ${SUPPORTED_TOOLCRAFT_EDITION}.`
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
else if (value.edition !== SUPPORTED_TOOLCRAFT_EDITION) {
|
|
100
|
+
diagnostics.push(createDiagnostic({
|
|
101
|
+
code: DIAGNOSTIC_CODES.invalidEdition,
|
|
102
|
+
severity: "error",
|
|
103
|
+
location: "edition",
|
|
104
|
+
message: `Unsupported edition ${JSON.stringify(value.edition)}. Expected ${SUPPORTED_TOOLCRAFT_EDITION}.`
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
if (!result.ok) {
|
|
108
|
+
diagnostics.push(...result.issues.map((issue) => createDiagnostic({
|
|
109
|
+
code: DIAGNOSTIC_CODES.invalidConfig,
|
|
110
|
+
severity: "error",
|
|
111
|
+
location: issue.path.join("."),
|
|
112
|
+
message: issue.message
|
|
113
|
+
})));
|
|
114
|
+
}
|
|
115
|
+
if (diagnostics.length > 0 || !result.ok) {
|
|
116
|
+
return { diagnostics };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
config: normalizeToolcraftConfig(result.value),
|
|
120
|
+
diagnostics
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function mergeToolcraftConfig(base, override) {
|
|
124
|
+
return deepMerge(base, override);
|
|
125
|
+
}
|
|
126
|
+
function normalizeToolcraftConfig(value) {
|
|
127
|
+
return {
|
|
128
|
+
...value,
|
|
129
|
+
resources: normalizeResources(value.resources),
|
|
130
|
+
readme: normalizeReadme(value.readme)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function normalizeResources(value) {
|
|
134
|
+
if (!isPlainObject(value)) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return Object.fromEntries(Object.entries(value).map(([name, resource]) => [name, normalizeResource(resource)]));
|
|
138
|
+
}
|
|
139
|
+
function normalizeResource(value) {
|
|
140
|
+
if (!isPlainObject(value)) {
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
methods: normalizeMethods(value.methods),
|
|
145
|
+
subresources: normalizeResources(value.subresources)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function normalizeMethods(value) {
|
|
149
|
+
if (!isPlainObject(value)) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
return Object.fromEntries(Object.entries(value)
|
|
153
|
+
.map(([name, method]) => [name, normalizeMethod(method)])
|
|
154
|
+
.filter((entry) => entry[1] !== undefined));
|
|
155
|
+
}
|
|
156
|
+
function normalizeMethod(value) {
|
|
157
|
+
if (typeof value === "string") {
|
|
158
|
+
return parseMethodShorthand(value);
|
|
159
|
+
}
|
|
160
|
+
if (!isPlainObject(value)) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
const method = value.method;
|
|
164
|
+
const requestPath = value.path;
|
|
165
|
+
if (!isHttpMethod(method) || typeof requestPath !== "string") {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
method,
|
|
170
|
+
path: requestPath,
|
|
171
|
+
...(typeof value.pagination === "string" ? { pagination: value.pagination } : {}),
|
|
172
|
+
...(typeof value.idempotent === "boolean" ? { idempotent: value.idempotent } : {})
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function parseMethodShorthand(value) {
|
|
176
|
+
const parts = value.trim().split(" ").filter((part) => part.length > 0);
|
|
177
|
+
const method = parts[0];
|
|
178
|
+
const requestPath = parts[1];
|
|
179
|
+
if (!isHttpMethod(method) || requestPath === undefined) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const optionsText = parts.slice(2).join(" ");
|
|
183
|
+
const options = parseInlineOptions(optionsText);
|
|
184
|
+
return {
|
|
185
|
+
method,
|
|
186
|
+
path: requestPath,
|
|
187
|
+
...(typeof options.pagination === "string" ? { pagination: options.pagination } : {}),
|
|
188
|
+
...(typeof options.idempotent === "boolean" ? { idempotent: options.idempotent } : {})
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function parseInlineOptions(value) {
|
|
192
|
+
const trimmed = value.trim();
|
|
193
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
const body = trimmed.slice(1, -1).trim();
|
|
197
|
+
if (body.length === 0) {
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
const entries = [];
|
|
201
|
+
for (const pair of body.split(",")) {
|
|
202
|
+
const separatorIndex = pair.indexOf(":");
|
|
203
|
+
if (separatorIndex < 0) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const key = pair.slice(0, separatorIndex).trim();
|
|
207
|
+
const rawValue = pair.slice(separatorIndex + 1).trim();
|
|
208
|
+
entries.push([key, parseInlineScalar(rawValue)]);
|
|
209
|
+
}
|
|
210
|
+
return Object.fromEntries(entries);
|
|
211
|
+
}
|
|
212
|
+
function parseInlineScalar(value) {
|
|
213
|
+
if (value === "true")
|
|
214
|
+
return true;
|
|
215
|
+
if (value === "false")
|
|
216
|
+
return false;
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
function normalizeReadme(value) {
|
|
220
|
+
if (!isPlainObject(value) || !isPlainObject(value.examples)) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
const examples = Object.fromEntries(Object.entries(value.examples)
|
|
224
|
+
.filter((entry) => Array.isArray(entry[1]))
|
|
225
|
+
.map(([key, entries]) => [
|
|
226
|
+
key,
|
|
227
|
+
entries
|
|
228
|
+
.filter(isPlainObject)
|
|
229
|
+
.map((entry) => ({
|
|
230
|
+
title: typeof entry.title === "string" ? entry.title : "Example",
|
|
231
|
+
params: isPlainObject(entry.params) ? entry.params : {}
|
|
232
|
+
}))
|
|
233
|
+
]));
|
|
234
|
+
return { examples };
|
|
235
|
+
}
|
|
236
|
+
function isHttpMethod(value) {
|
|
237
|
+
return (value === "get" ||
|
|
238
|
+
value === "post" ||
|
|
239
|
+
value === "put" ||
|
|
240
|
+
value === "patch" ||
|
|
241
|
+
value === "delete" ||
|
|
242
|
+
value === "head" ||
|
|
243
|
+
value === "options");
|
|
244
|
+
}
|
|
245
|
+
function deepMerge(left, right) {
|
|
246
|
+
if (!isPlainObject(left) || !isPlainObject(right)) {
|
|
247
|
+
return cloneMergeValue(right);
|
|
248
|
+
}
|
|
249
|
+
const merged = { ...left };
|
|
250
|
+
for (const [key, value] of Object.entries(right)) {
|
|
251
|
+
merged[key] = key in merged ? deepMerge(merged[key], value) : cloneMergeValue(value);
|
|
252
|
+
}
|
|
253
|
+
return merged;
|
|
254
|
+
}
|
|
255
|
+
function cloneMergeValue(value) {
|
|
256
|
+
if (Array.isArray(value)) {
|
|
257
|
+
return [...value];
|
|
258
|
+
}
|
|
259
|
+
if (isPlainObject(value)) {
|
|
260
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneMergeValue(entry)]));
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
function isPlainObject(value) {
|
|
265
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
266
|
+
}
|
package/dist/define-client.js
CHANGED
|
@@ -78,6 +78,10 @@ function cloneCommand(command, scopeOverride) {
|
|
|
78
78
|
return defineCommand({
|
|
79
79
|
name: command.name,
|
|
80
80
|
description: command.description,
|
|
81
|
+
examples: command.examples.map((example) => ({
|
|
82
|
+
title: example.title,
|
|
83
|
+
params: { ...example.params }
|
|
84
|
+
})),
|
|
81
85
|
aliases: [...command.aliases],
|
|
82
86
|
positional: [...command.positional],
|
|
83
87
|
params: command.params,
|