@wirecat/cli-base 0.61.3 → 0.63.0
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/CHANGELOG.md +30 -0
- package/dist/codegen/discovery.d.ts +52 -0
- package/dist/codegen/discovery.js +288 -0
- package/dist/codegen/index.d.ts +3 -2
- package/dist/codegen/index.js +3 -2
- package/dist/codegen/model.d.ts +2 -1
- package/dist/codegen/model.js +2 -1
- package/dist/commands/contracts.js +26 -23
- package/dist/commands/registry.d.ts +5 -0
- package/dist/commands/registry.js +14 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,36 @@ Versions before 0.8.0 are in the [git tags](https://github.com/WireCatLabs/cli-b
|
|
|
8
8
|
Every entry says what changed as a caller sees it, why, and what to watch for — the rules are
|
|
9
9
|
[`docs/dev/CONVENTIONS.md`](docs/dev/CONVENTIONS.md#the-changelog).
|
|
10
10
|
|
|
11
|
+
## 0.63.0 — 27.10.2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`@wirecat/cli-base/codegen` reads Google Discovery documents: `adaptDiscovery`, `syncDiscovery` and
|
|
16
|
+
`readDiscovery`.** A Google CLI no longer keeps its own copy of the Discovery adapter and the `spec:sync` script.
|
|
17
|
+
`adaptDiscovery` takes `exclude` (method id → reason) for methods a CLI cannot serve, such as a `watch` that needs a
|
|
18
|
+
public webhook, and returns each service's `rootUrl` and `servicePath` for a CLI that calls Google with `fetch`.
|
|
19
|
+
Effects come from the HTTP verb only: correct a POST that reads, or a method that deletes, with `generate`'s
|
|
20
|
+
`overrides`. `syncDiscovery` writes the downloaded bytes unchanged and every provenance field, including
|
|
21
|
+
`sourceSha256` and `normalization`.
|
|
22
|
+
|
|
23
|
+
## 0.62.0 — 26.10.2026
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`describeAlone(command, known)` describes a command as if it had no parent, reusing what `known` already holds.**
|
|
28
|
+
The shared-command check uses it to describe each command once per pass. Keep `known` for one pass only: a map kept
|
|
29
|
+
longer returns descriptions of commands that have since changed.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **The shared-command check describes each command once per pass, not once for itself and again for every group
|
|
34
|
+
above it.** It runs when a shared command is built and before each command runs, so every command of every CLI paid
|
|
35
|
+
for it: about 60% of tg's setup, even for `tg --version`. On tg's real program one check now takes about 5 ms
|
|
36
|
+
instead of 11, building the program about 35 ms instead of 46, and a warm `run(["--version"])` about 30 ms instead
|
|
37
|
+
of 47 ([measurement](https://github.com/WireCatLabs/cli-testing/blob/main/performance/cli/shared-contract-check/README.md)).
|
|
38
|
+
The check still covers every protected subcommand, with the same errors and overrides, and `sharedContracts` returns
|
|
39
|
+
the same contracts as before.
|
|
40
|
+
|
|
11
41
|
## 0.61.3 — 26.10.2026
|
|
12
42
|
|
|
13
43
|
### Fixed
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ApiModel } from "./model.js";
|
|
2
|
+
export interface DiscoveryMetadata {
|
|
3
|
+
/** OAuth scopes per method id, as the document lists them. */
|
|
4
|
+
scopes: Record<string, string[]>;
|
|
5
|
+
/** What the model leaves out, one line each, for a reviewer. */
|
|
6
|
+
omissions: string[];
|
|
7
|
+
}
|
|
8
|
+
/** Where a service's paths are relative to: a request goes to `rootUrl + servicePath + path`. */
|
|
9
|
+
export interface DiscoveryService {
|
|
10
|
+
rootUrl: string;
|
|
11
|
+
servicePath: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DiscoveryOptions {
|
|
14
|
+
sourceUrl?: string;
|
|
15
|
+
sourceRevision?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Method ids left out of the model entirely, each with the reason, as for an override. An id that matches
|
|
18
|
+
* no method stops generation. For a method a CLI cannot serve, such as a `watch` that needs a public webhook.
|
|
19
|
+
*/
|
|
20
|
+
exclude?: Readonly<Record<string, string>>;
|
|
21
|
+
}
|
|
22
|
+
export interface DiscoveryResult {
|
|
23
|
+
model: ApiModel;
|
|
24
|
+
metadata: DiscoveryMetadata;
|
|
25
|
+
services: Record<string, DiscoveryService>;
|
|
26
|
+
}
|
|
27
|
+
/** `documents` maps each service name to its parsed Discovery document; schema ids get the service as a prefix. */
|
|
28
|
+
export declare const adaptDiscovery: (documents: Readonly<Record<string, unknown>>, options?: DiscoveryOptions) => DiscoveryResult;
|
|
29
|
+
/** One service's entry in `provenance.json`. */
|
|
30
|
+
export interface DiscoveryProvenance {
|
|
31
|
+
source: string;
|
|
32
|
+
revision: string;
|
|
33
|
+
version: string;
|
|
34
|
+
/** Of the committed file; `readDiscovery` refuses a snapshot that no longer matches it. */
|
|
35
|
+
sha256: string;
|
|
36
|
+
/** Of the bytes Google served. Equal to `sha256` unless the file was normalized after download. */
|
|
37
|
+
sourceSha256: string;
|
|
38
|
+
normalization: string;
|
|
39
|
+
}
|
|
40
|
+
export interface SyncDiscoveryOptions {
|
|
41
|
+
/** Where `<service>.json` and `provenance.json` are written. */
|
|
42
|
+
dir: string;
|
|
43
|
+
fetch?: typeof globalThis.fetch;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Downloads each Discovery document byte for byte and records where it came from. Run by hand, then review
|
|
47
|
+
* the diff: the build never fetches. `provenance.json` keeps the order of `sources`, and that order is
|
|
48
|
+
* the order of the generated schemas.
|
|
49
|
+
*/
|
|
50
|
+
export declare const syncDiscovery: (sources: Readonly<Record<string, string>>, options: SyncDiscoveryOptions) => Promise<Record<string, DiscoveryProvenance>>;
|
|
51
|
+
/** The snapshots `syncDiscovery` wrote, parsed, in `provenance.json` order. Throws if a file's hash changed. */
|
|
52
|
+
export declare const readDiscovery: (dir: string) => Record<string, unknown>;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Discovery documents, already parsed → the format-neutral `ApiModel`, plus the offline
|
|
3
|
+
* snapshot those documents come from: `syncDiscovery` downloads them with their provenance, and
|
|
4
|
+
* `readDiscovery` loads them back only when every hash still matches.
|
|
5
|
+
*
|
|
6
|
+
* Strict like `adaptOpenApi`: an unknown schema keyword, format or parameter location stops generation.
|
|
7
|
+
* Effects come from the HTTP verb; a method the verb misjudges is corrected with `generate`'s `overrides`.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { CodegenError } from "./pipeline.js";
|
|
13
|
+
const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
/** Discovery links point at developers.google.com without the host, which breaks them in generated Markdown. */
|
|
15
|
+
const description = (value) => String(value ?? "").replace(/\]\((\/workspace\/[^)]+)\)/g, "](https://developers.google.com$1)");
|
|
16
|
+
const DESCRIPTIVE = new Set([
|
|
17
|
+
"id",
|
|
18
|
+
"description",
|
|
19
|
+
"format",
|
|
20
|
+
"enumDescriptions",
|
|
21
|
+
"enumDeprecated",
|
|
22
|
+
"readOnly",
|
|
23
|
+
"deprecated",
|
|
24
|
+
"annotations",
|
|
25
|
+
"location",
|
|
26
|
+
"required",
|
|
27
|
+
"repeated",
|
|
28
|
+
"parameterName",
|
|
29
|
+
]);
|
|
30
|
+
const CONSTRAINTS = new Set([
|
|
31
|
+
"type",
|
|
32
|
+
"$ref",
|
|
33
|
+
"properties",
|
|
34
|
+
"items",
|
|
35
|
+
"additionalProperties",
|
|
36
|
+
"enum",
|
|
37
|
+
"default",
|
|
38
|
+
"minimum",
|
|
39
|
+
"maximum",
|
|
40
|
+
"pattern",
|
|
41
|
+
"minLength",
|
|
42
|
+
"maxLength",
|
|
43
|
+
"minItems",
|
|
44
|
+
"maxItems",
|
|
45
|
+
"nullable",
|
|
46
|
+
]);
|
|
47
|
+
const FORMATS = new Set([
|
|
48
|
+
"int32",
|
|
49
|
+
"int64",
|
|
50
|
+
"float",
|
|
51
|
+
"double",
|
|
52
|
+
"byte",
|
|
53
|
+
"date",
|
|
54
|
+
"date-time",
|
|
55
|
+
"google-datetime",
|
|
56
|
+
"google-fieldmask",
|
|
57
|
+
]);
|
|
58
|
+
const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
|
|
59
|
+
/** What the HTTP client sets itself; as flags they would only invite a second, wrong value. */
|
|
60
|
+
const TRANSPORT_PARAMETERS = new Set(["key", "oauth_token", "access_token", "upload_protocol", "uploadType"]);
|
|
61
|
+
/** `documents` maps each service name to its parsed Discovery document; schema ids get the service as a prefix. */
|
|
62
|
+
export const adaptDiscovery = (documents, options = {}) => {
|
|
63
|
+
const exclude = options.exclude ?? {};
|
|
64
|
+
const excluded = new Set();
|
|
65
|
+
const schemas = [];
|
|
66
|
+
const operations = [];
|
|
67
|
+
const metadata = { scopes: {}, omissions: [] };
|
|
68
|
+
const services = {};
|
|
69
|
+
const fail = (problem) => {
|
|
70
|
+
throw new CodegenError([problem]);
|
|
71
|
+
};
|
|
72
|
+
const node = (value, service, where) => {
|
|
73
|
+
if (!object(value))
|
|
74
|
+
return fail(`${where}: expected a Discovery schema object`);
|
|
75
|
+
for (const key of Object.keys(value))
|
|
76
|
+
if (!DESCRIPTIVE.has(key) && !CONSTRAINTS.has(key))
|
|
77
|
+
fail(`${where}: unsupported schema keyword ${key}`);
|
|
78
|
+
if (value.format !== undefined && !FORMATS.has(String(value.format)))
|
|
79
|
+
fail(`${where}: unsupported format`);
|
|
80
|
+
const common = {
|
|
81
|
+
...(typeof value.description === "string" ? { description: description(value.description) } : {}),
|
|
82
|
+
...(value.nullable === true ? { nullable: true } : {}),
|
|
83
|
+
...(value.deprecated === true ? { deprecated: true } : {}),
|
|
84
|
+
...(value.default !== undefined ? { default: value.default } : {}),
|
|
85
|
+
};
|
|
86
|
+
if (typeof value.$ref === "string")
|
|
87
|
+
return { ...common, type: "ref", ref: `${service}_${value.$ref}` };
|
|
88
|
+
const bound = (key) => {
|
|
89
|
+
if (value[key] === undefined)
|
|
90
|
+
return {};
|
|
91
|
+
const n = Number(value[key]);
|
|
92
|
+
if (!Number.isFinite(n))
|
|
93
|
+
fail(`${where}: invalid numeric bound`);
|
|
94
|
+
return { [key]: n };
|
|
95
|
+
};
|
|
96
|
+
switch (value.type) {
|
|
97
|
+
case "any":
|
|
98
|
+
return { ...common, type: "unknown" };
|
|
99
|
+
case "string":
|
|
100
|
+
return {
|
|
101
|
+
...common,
|
|
102
|
+
type: "string",
|
|
103
|
+
...(Array.isArray(value.enum) ? { enum: value.enum.map(String) } : {}),
|
|
104
|
+
...(typeof value.pattern === "string"
|
|
105
|
+
? { pattern: value.pattern }
|
|
106
|
+
: value.format === "int64"
|
|
107
|
+
? { pattern: "^-?\\d+$" }
|
|
108
|
+
: {}),
|
|
109
|
+
...bound("minLength"),
|
|
110
|
+
...bound("maxLength"),
|
|
111
|
+
};
|
|
112
|
+
case "integer":
|
|
113
|
+
return {
|
|
114
|
+
...common,
|
|
115
|
+
type: "integer",
|
|
116
|
+
...(value.format === "int64" ? { format: "int64" } : { format: "int32" }),
|
|
117
|
+
...bound("minimum"),
|
|
118
|
+
...bound("maximum"),
|
|
119
|
+
...(Array.isArray(value.enum) ? { enum: value.enum.map(Number) } : {}),
|
|
120
|
+
};
|
|
121
|
+
case "number":
|
|
122
|
+
return { ...common, type: "number", ...bound("minimum"), ...bound("maximum") };
|
|
123
|
+
case "boolean":
|
|
124
|
+
return { ...common, type: "boolean" };
|
|
125
|
+
case "array":
|
|
126
|
+
return {
|
|
127
|
+
...common,
|
|
128
|
+
type: "array",
|
|
129
|
+
items: node(value.items, service, `${where}/items`),
|
|
130
|
+
...bound("minItems"),
|
|
131
|
+
...bound("maxItems"),
|
|
132
|
+
};
|
|
133
|
+
case "object": {
|
|
134
|
+
const properties = object(value.properties) ? value.properties : {};
|
|
135
|
+
return {
|
|
136
|
+
...common,
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: Object.fromEntries(Object.entries(properties).map(([key, item]) => [key, node(item, service, `${where}/properties/${key}`)])),
|
|
139
|
+
required: Array.isArray(value.required) ? value.required.map(String) : [],
|
|
140
|
+
...(object(value.additionalProperties)
|
|
141
|
+
? { additionalProperties: node(value.additionalProperties, service, `${where}/additionalProperties`) }
|
|
142
|
+
: {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
default:
|
|
146
|
+
return fail(`${where}: unsupported schema type`);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
for (const [service, source] of Object.entries(documents)) {
|
|
150
|
+
if (!object(source) || source.name !== service || !object(source.schemas) || !object(source.resources))
|
|
151
|
+
fail(`invalid Discovery document: ${service}`);
|
|
152
|
+
const document = source;
|
|
153
|
+
if (typeof document.rootUrl !== "string" || typeof document.servicePath !== "string")
|
|
154
|
+
fail(`invalid Discovery document: ${service} has no rootUrl or servicePath`);
|
|
155
|
+
services[service] = { rootUrl: String(document.rootUrl), servicePath: String(document.servicePath) };
|
|
156
|
+
for (const [id, value] of Object.entries(document.schemas))
|
|
157
|
+
schemas.push({ id: `${service}_${id}`, schema: node(value, service, `/schemas/${id}`) });
|
|
158
|
+
const walk = (resources, path) => {
|
|
159
|
+
for (const [resource, value] of Object.entries(resources)) {
|
|
160
|
+
if (!object(value))
|
|
161
|
+
fail(`invalid resource: ${resource}`);
|
|
162
|
+
const resourceValue = value;
|
|
163
|
+
const location = `${path}/${resource}`;
|
|
164
|
+
if (object(resourceValue.methods))
|
|
165
|
+
for (const [name, raw] of Object.entries(resourceValue.methods)) {
|
|
166
|
+
if (!object(raw) || typeof raw.id !== "string" || typeof raw.path !== "string")
|
|
167
|
+
return fail(`invalid method at ${location}`);
|
|
168
|
+
if (exclude[raw.id] !== undefined) {
|
|
169
|
+
excluded.add(raw.id);
|
|
170
|
+
metadata.omissions.push(`${raw.id}: excluded — ${exclude[raw.id]}`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const method = String(raw.httpMethod);
|
|
174
|
+
if (!METHODS.has(method))
|
|
175
|
+
fail(`unsupported HTTP method: ${raw.id}`);
|
|
176
|
+
const effect = method === "GET" || method === "HEAD" ? "read" : method === "DELETE" ? "destructive" : "write";
|
|
177
|
+
metadata.scopes[raw.id] = Array.isArray(raw.scopes) ? raw.scopes.map(String) : [];
|
|
178
|
+
const common = object(document.parameters) ? document.parameters : {};
|
|
179
|
+
const parameters = { ...common, ...(object(raw.parameters) ? raw.parameters : {}) };
|
|
180
|
+
const safe = Object.entries(parameters).filter(([key]) => !TRANSPORT_PARAMETERS.has(key));
|
|
181
|
+
const operation = {
|
|
182
|
+
id: raw.id,
|
|
183
|
+
binding: { kind: "http", method, path: raw.path },
|
|
184
|
+
effect,
|
|
185
|
+
summary: description(raw.description ?? raw.id).split("\n")[0],
|
|
186
|
+
description: description(raw.description),
|
|
187
|
+
tags: [service],
|
|
188
|
+
parameters: safe.map(([key, value]) => {
|
|
189
|
+
if (!object(value) || !["path", "query"].includes(String(value.location)))
|
|
190
|
+
return fail(`unsupported parameter: ${raw.id}/${key}`);
|
|
191
|
+
const schema = node(value, service, `${location}/methods/${name}/parameters/${key}`);
|
|
192
|
+
return {
|
|
193
|
+
name: key,
|
|
194
|
+
in: value.location,
|
|
195
|
+
required: value.required === true,
|
|
196
|
+
...(typeof value.description === "string" ? { description: description(value.description) } : {}),
|
|
197
|
+
schema: value.repeated === true ? { type: "array", items: schema } : schema,
|
|
198
|
+
};
|
|
199
|
+
}),
|
|
200
|
+
source: { location: `${service}${location}/methods/${name}` },
|
|
201
|
+
...(raw.deprecated === true ? { deprecated: true } : {}),
|
|
202
|
+
};
|
|
203
|
+
if (object(raw.request))
|
|
204
|
+
operation.requestBody = {
|
|
205
|
+
confidence: "contract",
|
|
206
|
+
required: raw.request.required === true,
|
|
207
|
+
schema: node(raw.request, service, `${location}/methods/${name}/request`),
|
|
208
|
+
};
|
|
209
|
+
if (object(raw.response))
|
|
210
|
+
operation.response = {
|
|
211
|
+
confidence: "contract",
|
|
212
|
+
schema: node(raw.response, service, `${location}/methods/${name}/response`),
|
|
213
|
+
required: false,
|
|
214
|
+
};
|
|
215
|
+
if (raw.supportsMediaUpload || raw.supportsMediaDownload)
|
|
216
|
+
metadata.omissions.push(`${raw.id}: JSON endpoint only; media transport is not generated`);
|
|
217
|
+
operations.push(operation);
|
|
218
|
+
}
|
|
219
|
+
if (object(resourceValue.resources))
|
|
220
|
+
walk(resourceValue.resources, `${location}/resources`);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
walk(document.resources, "/resources");
|
|
224
|
+
}
|
|
225
|
+
const stale = Object.keys(exclude).filter((id) => !excluded.has(id));
|
|
226
|
+
if (stale.length > 0)
|
|
227
|
+
throw new CodegenError(stale.map((id) => `exclusion "${id}" matches no method — stale, or the id changed`));
|
|
228
|
+
return {
|
|
229
|
+
model: {
|
|
230
|
+
source: {
|
|
231
|
+
kind: "other",
|
|
232
|
+
sourceUrl: options.sourceUrl ?? "https://developers.google.com/discovery",
|
|
233
|
+
...(options.sourceRevision !== undefined ? { sourceRevision: options.sourceRevision } : {}),
|
|
234
|
+
},
|
|
235
|
+
operations: operations.sort((a, b) => a.id.localeCompare(b.id)),
|
|
236
|
+
schemas,
|
|
237
|
+
},
|
|
238
|
+
metadata,
|
|
239
|
+
services,
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
243
|
+
/**
|
|
244
|
+
* Downloads each Discovery document byte for byte and records where it came from. Run by hand, then review
|
|
245
|
+
* the diff: the build never fetches. `provenance.json` keeps the order of `sources`, and that order is
|
|
246
|
+
* the order of the generated schemas.
|
|
247
|
+
*/
|
|
248
|
+
export const syncDiscovery = async (sources, options) => {
|
|
249
|
+
const fetch = options.fetch ?? globalThis.fetch;
|
|
250
|
+
const provenance = {};
|
|
251
|
+
const downloaded = {};
|
|
252
|
+
for (const [service, source] of Object.entries(sources)) {
|
|
253
|
+
const response = await fetch(source, { signal: AbortSignal.timeout(30_000), redirect: "error" });
|
|
254
|
+
if (!response.ok)
|
|
255
|
+
throw new Error(`Google Discovery fetch failed for ${service}: HTTP ${response.status}`);
|
|
256
|
+
const raw = await response.text();
|
|
257
|
+
const document = JSON.parse(raw);
|
|
258
|
+
if (!object(document) || document.name !== service)
|
|
259
|
+
throw new Error(`${source} is not the Discovery document of ${service}`);
|
|
260
|
+
const hash = sha256(raw);
|
|
261
|
+
downloaded[service] = raw;
|
|
262
|
+
provenance[service] = {
|
|
263
|
+
source,
|
|
264
|
+
revision: String(document.revision),
|
|
265
|
+
version: String(document.version),
|
|
266
|
+
sha256: hash,
|
|
267
|
+
sourceSha256: hash,
|
|
268
|
+
normalization: "none",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
mkdirSync(options.dir, { recursive: true });
|
|
272
|
+
for (const [service, raw] of Object.entries(downloaded))
|
|
273
|
+
writeFileSync(join(options.dir, `${service}.json`), raw);
|
|
274
|
+
writeFileSync(join(options.dir, "provenance.json"), `${JSON.stringify(provenance, null, 2)}\n`);
|
|
275
|
+
return provenance;
|
|
276
|
+
};
|
|
277
|
+
/** The snapshots `syncDiscovery` wrote, parsed, in `provenance.json` order. Throws if a file's hash changed. */
|
|
278
|
+
export const readDiscovery = (dir) => {
|
|
279
|
+
const provenance = JSON.parse(readFileSync(join(dir, "provenance.json"), "utf8"));
|
|
280
|
+
const documents = {};
|
|
281
|
+
for (const [service, entry] of Object.entries(provenance)) {
|
|
282
|
+
const raw = readFileSync(join(dir, `${service}.json`));
|
|
283
|
+
if (sha256(raw) !== entry.sha256)
|
|
284
|
+
throw new CodegenError([`Discovery hash mismatch: ${service}`]);
|
|
285
|
+
documents[service] = JSON.parse(raw.toString("utf8"));
|
|
286
|
+
}
|
|
287
|
+
return documents;
|
|
288
|
+
};
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turns an official API description into committed code: types, Valibot schemas, an operation
|
|
3
3
|
* manifest and a coverage page. Build-time only, and format-neutral: it parses no file format and
|
|
4
|
-
* adds no dependency. `adaptOpenApi` reads an OpenAPI document
|
|
5
|
-
* Postman or anything else lives in the consumer.
|
|
4
|
+
* adds no dependency. `adaptOpenApi` reads an OpenAPI document and `adaptDiscovery` Google Discovery
|
|
5
|
+
* documents the consumer has parsed; an adapter from Postman or anything else lives in the consumer.
|
|
6
6
|
*/
|
|
7
|
+
export { adaptDiscovery, type DiscoveryMetadata, type DiscoveryOptions, type DiscoveryProvenance, type DiscoveryResult, type DiscoveryService, readDiscovery, type SyncDiscoveryOptions, syncDiscovery, } from "./discovery.js";
|
|
7
8
|
export { type CoverageOptions, coverageGenerator, definitionsGenerator, type ManifestOperation, manifestGenerator, } from "./manifest.js";
|
|
8
9
|
export type { ApiBody, ApiModel, ApiOperation, ApiParameter, ApiSchema, Binding, CodeGenerator, Confidence, Discriminator, Effect, GeneratedArtifact, HttpBinding, HttpMethod, Override, RpcBinding, SchemaNode, SourceInfo, SourceReference, } from "./model.js";
|
|
9
10
|
export { identifier, kebab } from "./names.js";
|
package/dist/codegen/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turns an official API description into committed code: types, Valibot schemas, an operation
|
|
3
3
|
* manifest and a coverage page. Build-time only, and format-neutral: it parses no file format and
|
|
4
|
-
* adds no dependency. `adaptOpenApi` reads an OpenAPI document
|
|
5
|
-
* Postman or anything else lives in the consumer.
|
|
4
|
+
* adds no dependency. `adaptOpenApi` reads an OpenAPI document and `adaptDiscovery` Google Discovery
|
|
5
|
+
* documents the consumer has parsed; an adapter from Postman or anything else lives in the consumer.
|
|
6
6
|
*/
|
|
7
|
+
export { adaptDiscovery, readDiscovery, syncDiscovery, } from "./discovery.js";
|
|
7
8
|
export { coverageGenerator, definitionsGenerator, manifestGenerator, } from "./manifest.js";
|
|
8
9
|
export { identifier, kebab } from "./names.js";
|
|
9
10
|
export { adaptOpenApi, OpenApiError } from "./openapi.js";
|
package/dist/codegen/model.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The model every generator reads, whatever the source was: an OpenAPI file, a Postman collection,
|
|
3
3
|
* a scraped reference page. An adapter produces this from a document already parsed into plain
|
|
4
|
-
* values: `adaptOpenApi` for OpenAPI here, the rest in the
|
|
4
|
+
* values: `adaptOpenApi` for OpenAPI and `adaptDiscovery` for Google Discovery here, the rest in the
|
|
5
|
+
* consumer. Nothing here reads YAML or a file.
|
|
5
6
|
*/
|
|
6
7
|
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
|
7
8
|
/** How much the source promises. A Postman example is not a contract and must never become one. */
|
package/dist/codegen/model.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The model every generator reads, whatever the source was: an OpenAPI file, a Postman collection,
|
|
3
3
|
* a scraped reference page. An adapter produces this from a document already parsed into plain
|
|
4
|
-
* values: `adaptOpenApi` for OpenAPI here, the rest in the
|
|
4
|
+
* values: `adaptOpenApi` for OpenAPI and `adaptDiscovery` for Google Discovery here, the rest in the
|
|
5
|
+
* consumer. Nothing here reads YAML or a file.
|
|
5
6
|
*/
|
|
6
7
|
export {};
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { CliError } from "../errors.js";
|
|
4
|
-
import {
|
|
4
|
+
import { describeAlone } from "./registry.js";
|
|
5
5
|
const protectedCommands = new WeakMap();
|
|
6
6
|
export const hasSharedContract = (command) => protectedCommands.has(command);
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const described = describeProgram(root)[0];
|
|
12
|
-
command.parent = parent;
|
|
13
|
-
if (!described)
|
|
7
|
+
const hidden = (command) => Boolean(command._hidden);
|
|
8
|
+
/** Shares arrays with the live command and with `known`: clone it before keeping it past the pass. */
|
|
9
|
+
const snapshot = (command, known = new Map(), children) => {
|
|
10
|
+
if (hidden(command))
|
|
14
11
|
throw new Error("a hidden command cannot own a shared contract");
|
|
15
|
-
const { commands: _, usage: __, path: ___, ...contract } =
|
|
16
|
-
return
|
|
12
|
+
const { commands: _, usage: __, path: ___, ...contract } = describeAlone(command, known);
|
|
13
|
+
return {
|
|
17
14
|
...contract,
|
|
18
15
|
aliases: [...command.aliases()],
|
|
19
16
|
children: (children ?? protectedCommands.get(command)?.originalChildren ?? command.commands)
|
|
20
|
-
.filter((child) => command.commands.includes(child) && !child
|
|
17
|
+
.filter((child) => command.commands.includes(child) && !hidden(child))
|
|
21
18
|
.map((child) => child.name()),
|
|
22
|
-
}
|
|
19
|
+
};
|
|
23
20
|
};
|
|
24
21
|
const same = isDeepStrictEqual;
|
|
25
22
|
const visit = (root, fn) => {
|
|
@@ -36,11 +33,12 @@ const pathOf = (command) => {
|
|
|
36
33
|
const differences = (before, after) => [...new Set([...Object.keys(before), ...Object.keys(after)])].filter((field) => !same(before[field], after[field]));
|
|
37
34
|
export const assertSharedContracts = (root) => {
|
|
38
35
|
const changed = [];
|
|
36
|
+
const known = new Map();
|
|
39
37
|
visit(root, (command) => {
|
|
40
38
|
const protectedContract = protectedCommands.get(command);
|
|
41
39
|
if (!protectedContract)
|
|
42
40
|
return;
|
|
43
|
-
const fields = differences(protectedContract.contract, snapshot(command));
|
|
41
|
+
const fields = differences(protectedContract.contract, snapshot(command, known));
|
|
44
42
|
if (fields.length)
|
|
45
43
|
changed.push({ command: pathOf(command).join(" "), fields });
|
|
46
44
|
});
|
|
@@ -55,9 +53,9 @@ export const overrideSharedContract = (command, reason, change) => {
|
|
|
55
53
|
if (!protectedContract)
|
|
56
54
|
throw new Error("only a core-owned command contract can be overridden");
|
|
57
55
|
assertSharedContracts(command);
|
|
58
|
-
const before = snapshot(command);
|
|
56
|
+
const before = structuredClone(snapshot(command));
|
|
59
57
|
change(command);
|
|
60
|
-
const after = snapshot(command);
|
|
58
|
+
const after = structuredClone(snapshot(command));
|
|
61
59
|
const fields = differences(before, after);
|
|
62
60
|
if (fields.length === 0)
|
|
63
61
|
throw new Error("a shared contract override must change its named command");
|
|
@@ -119,14 +117,18 @@ export const sharedCommand = (factory) => (...args) => {
|
|
|
119
117
|
if (!command.summary() && first.length > 80)
|
|
120
118
|
command.summary(`${first.slice(0, 77).replace(/\s+\S*$/, "")}…`);
|
|
121
119
|
});
|
|
120
|
+
const known = new Map();
|
|
122
121
|
visit(shared, (command) => {
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
if (hidden(command) || protectedCommands.has(command))
|
|
123
|
+
return;
|
|
124
|
+
// One object for both: `contract` is only ever replaced, and `reference` is cloned on the way out.
|
|
125
|
+
const contract = structuredClone(snapshot(command, known));
|
|
126
|
+
protectedCommands.set(command, {
|
|
127
|
+
contract,
|
|
128
|
+
reference: contract,
|
|
129
|
+
overrides: [],
|
|
130
|
+
originalChildren: [...command.commands],
|
|
131
|
+
});
|
|
130
132
|
});
|
|
131
133
|
shared.hook("preAction", () => {
|
|
132
134
|
let root = shared;
|
|
@@ -147,6 +149,7 @@ export const customizeSharedCommand = (reference, reason, build, expectedOverrid
|
|
|
147
149
|
const command = build();
|
|
148
150
|
assertSharedContracts(command);
|
|
149
151
|
const pending = [];
|
|
152
|
+
const known = new Map();
|
|
150
153
|
const edits = [];
|
|
151
154
|
const bindingPath = (own) => {
|
|
152
155
|
const path = [own.name()];
|
|
@@ -163,7 +166,7 @@ export const customizeSharedCommand = (reference, reason, build, expectedOverrid
|
|
|
163
166
|
return;
|
|
164
167
|
if (!protectedCommands.has(own)) {
|
|
165
168
|
const originalChildren = own.commands.filter((child) => inherited.reference.children.includes(child.name()));
|
|
166
|
-
const actual = snapshot(own, originalChildren);
|
|
169
|
+
const actual = structuredClone(snapshot(own, known, originalChildren));
|
|
167
170
|
const overrides = differences(inherited.contract, actual).map((field) => ({
|
|
168
171
|
command: bindingPath(own),
|
|
169
172
|
field,
|
|
@@ -119,3 +119,8 @@ export declare const flatten: (commands: readonly CommandInfo[]) => CommandInfo[
|
|
|
119
119
|
* in snake_case. Hand it to `stepOf` and `errorObjectOf` as `targets`.
|
|
120
120
|
*/
|
|
121
121
|
export declare const stepTargets: (root: Command) => ((command: string) => StepTarget | undefined);
|
|
122
|
+
/**
|
|
123
|
+
* The command as if it had no parent, so it inherits nothing, and its subcommands the same way. `known` holds what one
|
|
124
|
+
* pass has described already, so a whole tree costs one description per command; keep it no longer than that pass.
|
|
125
|
+
*/
|
|
126
|
+
export declare const describeAlone: (command: Command, known: Map<Command, CommandInfo>) => CommandInfo;
|
|
@@ -40,7 +40,19 @@ export const stepTargets = (root) => {
|
|
|
40
40
|
visit(child, [], root.options);
|
|
41
41
|
return (command) => found.get(command);
|
|
42
42
|
};
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The command as if it had no parent, so it inherits nothing, and its subcommands the same way. `known` holds what one
|
|
45
|
+
* pass has described already, so a whole tree costs one description per command; keep it no longer than that pass.
|
|
46
|
+
*/
|
|
47
|
+
export const describeAlone = (command, known) => {
|
|
48
|
+
let found = known.get(command);
|
|
49
|
+
if (!found) {
|
|
50
|
+
found = describe(command, "", [], { offMcp: false, noOffline: false }, known);
|
|
51
|
+
known.set(command, found);
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
};
|
|
55
|
+
const describe = (command, cli, parents, above, known) => {
|
|
44
56
|
const path = [...parents, command.name()];
|
|
45
57
|
const args = command.registeredArguments.map(describeArgument);
|
|
46
58
|
const options = describeOptions(command);
|
|
@@ -68,7 +80,7 @@ const describe = (command, cli, parents, above) => {
|
|
|
68
80
|
...(visible(command).length > 0 && hasAction(command) ? { runnable: true } : {}),
|
|
69
81
|
arguments: args,
|
|
70
82
|
options,
|
|
71
|
-
commands: visible(command).map((child) => describe(child, cli, path, here)),
|
|
83
|
+
commands: visible(command).map((child) => (known ? describeAlone(child, known) : describe(child, cli, path, here))),
|
|
72
84
|
};
|
|
73
85
|
};
|
|
74
86
|
// Commander has no public getter for the action either; without one, a group prints its help.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"description": "The parts every command line tool needs: output modes, terminal rendering, error model, exit codes, credentials, clocks",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|