@wirecat/cli-base 0.48.0 → 0.50.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 +42 -0
- package/README.md +1 -0
- package/config/tsconfig.scripts.json +1 -1
- package/dist/codegen/index.d.ts +3 -1
- package/dist/codegen/index.js +3 -1
- package/dist/codegen/manifest.d.ts +1 -0
- package/dist/codegen/manifest.js +1 -0
- package/dist/codegen/model.d.ts +4 -2
- package/dist/codegen/model.js +2 -2
- package/dist/codegen/openapi.d.ts +16 -0
- package/dist/codegen/openapi.js +446 -0
- package/dist/codegen/tree.d.ts +7 -5
- package/dist/codegen/tree.js +10 -4
- package/dist/codegen/typescript.js +3 -1
- package/dist/codegen/valibot.js +7 -2
- package/dist/query/parser.d.ts +3 -1
- package/dist/query/parser.js +8 -1
- package/dist/query/types.d.ts +2 -0
- package/dist/query/types.js +1 -0
- package/docs/standards/accounts.md +3 -2
- package/docs/standards/ci.md +4 -3
- package/docs/standards/mcp.md +4 -3
- package/docs/standards/output.md +3 -3
- package/docs/standards/permissions.md +4 -2
- package/docs/standards/releases.md +5 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,48 @@ 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.50.0 — 22.10.2026
|
|
12
|
+
|
|
13
|
+
### Changed — may break callers
|
|
14
|
+
|
|
15
|
+
- **The shared scripts TypeScript configuration checks `vitest.config.ts`.** Consumers extending it now
|
|
16
|
+
catch type errors in their test-runner configuration alongside scripts; fix newly reported errors before upgrading.
|
|
17
|
+
|
|
18
|
+
- **The handbook clarifies JSON previews and local release validation.** A dry-run preview uses the item
|
|
19
|
+
envelope; local releases must run coverage, engine, contract and security checks, with the npm token
|
|
20
|
+
available only to publishing. Command discovery remains a bare document.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`adaptOpenApi` reads an OpenAPI 3.0 or 3.1 document into the codegen model.** It moved here from max-cli because
|
|
25
|
+
whatsapp-cli needs it too for Meta's Cloud API file. Beyond what max-cli's copy did, it resolves `$ref` parameters,
|
|
26
|
+
drops the `Authorization`, `User-Agent`, `Content-Type` and `Accept` header parameters (the
|
|
27
|
+
HTTP client sets them), reads `oneOf` as a union, `const` and a typeless `enum` as their type, `type: [T, "null"]`
|
|
28
|
+
as nullable, and an array with no `items` as any item. A body that is multipart or a form keeps that in the new
|
|
29
|
+
`ApiBody.contentType` and manifest `request.contentType`; a multipart body with a file wins over a JSON one. A body
|
|
30
|
+
typed only from sample values gets `confidence: "example"`. A path with no `operationId` is skipped instead of
|
|
31
|
+
refused, since Meta uses one to describe its webhook payload. It still stops on anything else it does not
|
|
32
|
+
understand, now under the name `OpenApiError`. Parse YAML yourself and hand it the plain object.
|
|
33
|
+
- **One subtype may belong to two discriminated schemas, or to one schema under two values.** The generated type and
|
|
34
|
+
schema accept each value that selects it (`"unsupported" | "unknown"`); before, generation stopped. Meta's sent and
|
|
35
|
+
received message unions share `TextMessage` this way.
|
|
36
|
+
|
|
37
|
+
- **Release checks can audit Go dependencies for reachable vulnerabilities.** Set `govulncheck: true` and
|
|
38
|
+
`go-version-file` to the module's `go.mod` path. The audit uses that module's Go version and runs in its
|
|
39
|
+
directory; it stays off for existing callers.
|
|
40
|
+
- **The weekly cross-CLI audit includes WhatsApp.** Its latest published command tree joins the checks for
|
|
41
|
+
required commands, shared option meanings, answer shapes and error reasons.
|
|
42
|
+
|
|
43
|
+
## 0.49.0 — 21.10.2026
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **`parseLucene` can read a phrase's distance.** With `{ proximity: true }`, `"a b"~3` parses as a phrase predicate
|
|
48
|
+
with `slop: 3`, and `queryNodeSchema` accepts `slop`. Meeting search needs it for words spread over neighbouring
|
|
49
|
+
transcript lines, and a caller that turns it on says what the number counts. Without the option `~` is still
|
|
50
|
+
`query.unsupported-operator`, so messages and lists see no change. A distance that is not a whole number is
|
|
51
|
+
`query.invalid-syntax`; `word~2` (fuzzy) stays unsupported either way.
|
|
52
|
+
|
|
11
53
|
## 0.48.0 — 21.10.2026
|
|
12
54
|
|
|
13
55
|
### Changed — may break callers
|
package/README.md
CHANGED
|
@@ -171,6 +171,7 @@ unless you pass `isolated: true`; and a locked Linux keyring can block on a prom
|
|
|
171
171
|
### Generated API clients that never guess
|
|
172
172
|
|
|
173
173
|
`/codegen` turns an API description into TypeScript types, Valibot schemas, an operation manifest and a coverage page.
|
|
174
|
+
`adaptOpenApi` reads an OpenAPI 3.0 or 3.1 document; another format needs an adapter in the caller.
|
|
174
175
|
Every operation is classified as a read, a write or destructive, with how far the source can be trusted. Generation
|
|
175
176
|
stops, naming the problem, on a collision, an unclassified operation or a dangling reference — it never drops an
|
|
176
177
|
operation and never falls back to `any`. 64-bit integers stay exact.
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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.
|
|
4
|
+
* adds no dependency. `adaptOpenApi` reads an OpenAPI document the consumer has parsed; an adapter from
|
|
5
|
+
* Postman or anything else lives in the consumer.
|
|
5
6
|
*/
|
|
6
7
|
export { type CoverageOptions, coverageGenerator, definitionsGenerator, type ManifestOperation, manifestGenerator, } from "./manifest.js";
|
|
7
8
|
export type { ApiBody, ApiModel, ApiOperation, ApiParameter, ApiSchema, Binding, CodeGenerator, Confidence, Discriminator, Effect, GeneratedArtifact, HttpBinding, HttpMethod, Override, RpcBinding, SchemaNode, SourceInfo, SourceReference, } from "./model.js";
|
|
8
9
|
export { identifier, kebab } from "./names.js";
|
|
10
|
+
export { adaptOpenApi, OpenApiError } from "./openapi.js";
|
|
9
11
|
export { applyOverrides, CodegenError, type GenerateOptions, generate, validateModel, type WriteOptions, writeArtifacts, } from "./pipeline.js";
|
|
10
12
|
export { typesGenerator } from "./typescript.js";
|
|
11
13
|
export { type ValibotOptions, valibotGenerator } from "./valibot.js";
|
package/dist/codegen/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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.
|
|
4
|
+
* adds no dependency. `adaptOpenApi` reads an OpenAPI document the consumer has parsed; an adapter from
|
|
5
|
+
* Postman or anything else lives in the consumer.
|
|
5
6
|
*/
|
|
6
7
|
export { coverageGenerator, definitionsGenerator, manifestGenerator, } from "./manifest.js";
|
|
7
8
|
export { identifier, kebab } from "./names.js";
|
|
9
|
+
export { adaptOpenApi, OpenApiError } from "./openapi.js";
|
|
8
10
|
export { applyOverrides, CodegenError, generate, validateModel, writeArtifacts, } from "./pipeline.js";
|
|
9
11
|
export { typesGenerator } from "./typescript.js";
|
|
10
12
|
export { valibotGenerator } from "./valibot.js";
|
package/dist/codegen/manifest.js
CHANGED
|
@@ -16,6 +16,7 @@ const toManifest = (operation, tree) => withoutUndefined({
|
|
|
16
16
|
required: operation.requestBody.required,
|
|
17
17
|
confidence: operation.requestBody.confidence,
|
|
18
18
|
schema: tree.requestName(operation.id),
|
|
19
|
+
contentType: operation.requestBody.contentType,
|
|
19
20
|
})
|
|
20
21
|
: undefined,
|
|
21
22
|
response: operation.response
|
package/dist/codegen/model.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The model every generator reads, whatever the source was: an OpenAPI file, a Postman collection,
|
|
3
|
-
* a scraped reference page.
|
|
4
|
-
*
|
|
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 consumer. Nothing here reads YAML or a file.
|
|
5
5
|
*/
|
|
6
6
|
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
|
7
7
|
/** How much the source promises. A Postman example is not a contract and must never become one. */
|
|
@@ -100,6 +100,8 @@ export interface ApiParameter {
|
|
|
100
100
|
export interface ApiBody {
|
|
101
101
|
confidence: Confidence;
|
|
102
102
|
required: boolean;
|
|
103
|
+
/** How the body travels when it is not JSON: `multipart/form-data` for a file upload, or a form. */
|
|
104
|
+
contentType?: string;
|
|
103
105
|
schema?: SchemaNode;
|
|
104
106
|
example?: unknown;
|
|
105
107
|
}
|
package/dist/codegen/model.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The model every generator reads, whatever the source was: an OpenAPI file, a Postman collection,
|
|
3
|
-
* a scraped reference page.
|
|
4
|
-
*
|
|
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 consumer. Nothing here reads YAML or a file.
|
|
5
5
|
*/
|
|
6
6
|
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An OpenAPI 3.0 or 3.1 document, already parsed from YAML or JSON → the format-neutral `ApiModel`.
|
|
3
|
+
*
|
|
4
|
+
* Strict on purpose: a keyword this does not understand stops generation and names the place,
|
|
5
|
+
* because a constraint dropped here is a request the server refuses at run time.
|
|
6
|
+
*/
|
|
7
|
+
import type { ApiModel } from "./model.js";
|
|
8
|
+
export declare class OpenApiError extends Error {
|
|
9
|
+
readonly problems: readonly string[];
|
|
10
|
+
constructor(problems: readonly string[]);
|
|
11
|
+
}
|
|
12
|
+
/** Throws `OpenApiError` with every problem at once, so one run shows all a spec update broke. */
|
|
13
|
+
export declare const adaptOpenApi: (document: unknown, source?: {
|
|
14
|
+
sourceUrl?: string;
|
|
15
|
+
sourceRevision?: string;
|
|
16
|
+
}) => ApiModel;
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
const METHODS = ["get", "post", "put", "patch", "delete", "head"];
|
|
2
|
+
/**
|
|
3
|
+
* Carried by the model, or harmless to lose. `not` only narrows what a payload may hold; losing it
|
|
4
|
+
* accepts more, never refuses a valid request.
|
|
5
|
+
*/
|
|
6
|
+
const IGNORED = new Set(["readOnly", "example", "examples", "externalDocs", "title", "xml", "writeOnly", "not"]);
|
|
7
|
+
const KNOWN = new Set([
|
|
8
|
+
...IGNORED,
|
|
9
|
+
"type",
|
|
10
|
+
"format",
|
|
11
|
+
"description",
|
|
12
|
+
"nullable",
|
|
13
|
+
"deprecated",
|
|
14
|
+
"default",
|
|
15
|
+
"enum",
|
|
16
|
+
"const",
|
|
17
|
+
"minLength",
|
|
18
|
+
"maxLength",
|
|
19
|
+
"pattern",
|
|
20
|
+
"minimum",
|
|
21
|
+
"maximum",
|
|
22
|
+
"items",
|
|
23
|
+
"minItems",
|
|
24
|
+
"maxItems",
|
|
25
|
+
"uniqueItems",
|
|
26
|
+
"properties",
|
|
27
|
+
"required",
|
|
28
|
+
"additionalProperties",
|
|
29
|
+
"allOf",
|
|
30
|
+
"oneOf",
|
|
31
|
+
"$ref",
|
|
32
|
+
"discriminator",
|
|
33
|
+
]);
|
|
34
|
+
const PRIMITIVE_CONSTRAINTS = ["pattern", "minLength", "maxLength", "minimum", "maximum", "enum"];
|
|
35
|
+
/** What the HTTP client sets itself; as flags they would only invite a second, wrong value. */
|
|
36
|
+
const TRANSPORT_HEADERS = new Set(["authorization", "user-agent", "content-type", "accept"]);
|
|
37
|
+
/** What a body may be sent as, in the order an operation offering several is read. */
|
|
38
|
+
const JSON_TYPE = "application/json";
|
|
39
|
+
const MULTIPART = "multipart/form-data";
|
|
40
|
+
const FORM = "application/x-www-form-urlencoded";
|
|
41
|
+
/** Any of these anywhere in a body schema means someone wrote a contract, not a sample. */
|
|
42
|
+
const CONTRACT_MARKS = [
|
|
43
|
+
"$ref",
|
|
44
|
+
"description",
|
|
45
|
+
"required",
|
|
46
|
+
"enum",
|
|
47
|
+
"const",
|
|
48
|
+
"pattern",
|
|
49
|
+
"minLength",
|
|
50
|
+
"maxLength",
|
|
51
|
+
"allOf",
|
|
52
|
+
"oneOf",
|
|
53
|
+
];
|
|
54
|
+
export class OpenApiError extends Error {
|
|
55
|
+
problems;
|
|
56
|
+
constructor(problems) {
|
|
57
|
+
super(`the OpenAPI document cannot be adapted:\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
58
|
+
this.name = "OpenApiError";
|
|
59
|
+
this.problems = problems;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
63
|
+
const EFFECT_BY_METHOD = {
|
|
64
|
+
GET: "read",
|
|
65
|
+
HEAD: "read",
|
|
66
|
+
POST: "write",
|
|
67
|
+
PUT: "write",
|
|
68
|
+
PATCH: "write",
|
|
69
|
+
DELETE: "destructive",
|
|
70
|
+
};
|
|
71
|
+
const pick = (source, keys) => Object.fromEntries(keys.map((key) => [key, source[key]]));
|
|
72
|
+
const typeOfConst = (value) => typeof value === "string"
|
|
73
|
+
? "string"
|
|
74
|
+
: typeof value === "boolean"
|
|
75
|
+
? "boolean"
|
|
76
|
+
: Number.isInteger(value)
|
|
77
|
+
? "integer"
|
|
78
|
+
: typeof value === "number"
|
|
79
|
+
? "number"
|
|
80
|
+
: undefined;
|
|
81
|
+
/**
|
|
82
|
+
* A body typed from a sample request: only `type` and `example` all the way down. Its field types
|
|
83
|
+
* are what one example happened to hold, so the model must never treat it as a contract.
|
|
84
|
+
*/
|
|
85
|
+
const typedFromExample = (schema) => {
|
|
86
|
+
const marked = (node) => {
|
|
87
|
+
if (Array.isArray(node))
|
|
88
|
+
return node.some(marked);
|
|
89
|
+
if (!isObject(node))
|
|
90
|
+
return false;
|
|
91
|
+
if (CONTRACT_MARKS.some((mark) => node[mark] !== undefined))
|
|
92
|
+
return true;
|
|
93
|
+
return Object.entries(node).some(([key, value]) => key !== "example" && marked(value));
|
|
94
|
+
};
|
|
95
|
+
return isObject(schema) && !marked(schema);
|
|
96
|
+
};
|
|
97
|
+
class Adapter {
|
|
98
|
+
problems = [];
|
|
99
|
+
document;
|
|
100
|
+
#components;
|
|
101
|
+
#parameters;
|
|
102
|
+
constructor(document) {
|
|
103
|
+
this.document = document;
|
|
104
|
+
const components = isObject(document.components) ? document.components : {};
|
|
105
|
+
this.#components = isObject(components.schemas) ? components.schemas : {};
|
|
106
|
+
this.#parameters = isObject(components.parameters) ? components.parameters : {};
|
|
107
|
+
}
|
|
108
|
+
refId(ref, where) {
|
|
109
|
+
const prefix = "#/components/schemas/";
|
|
110
|
+
if (typeof ref === "string" && ref.startsWith(prefix))
|
|
111
|
+
return ref.slice(prefix.length);
|
|
112
|
+
this.problems.push(`${where}: unsupported reference ${JSON.stringify(ref)}`);
|
|
113
|
+
return "Unknown";
|
|
114
|
+
}
|
|
115
|
+
/** A parameter given as a `$ref` into `components/parameters`, followed once. */
|
|
116
|
+
#parameterSource(source, where) {
|
|
117
|
+
if (!isObject(source) || source.$ref === undefined)
|
|
118
|
+
return source;
|
|
119
|
+
const prefix = "#/components/parameters/";
|
|
120
|
+
const ref = source.$ref;
|
|
121
|
+
if (typeof ref === "string" && ref.startsWith(prefix) && isObject(this.#parameters[ref.slice(prefix.length)]))
|
|
122
|
+
return this.#parameters[ref.slice(prefix.length)];
|
|
123
|
+
this.problems.push(`${where}: unsupported reference ${JSON.stringify(ref)}`);
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
#common(source) {
|
|
127
|
+
const common = {};
|
|
128
|
+
if (typeof source.description === "string" && source.description.trim())
|
|
129
|
+
common.description = source.description;
|
|
130
|
+
if (source.nullable === true)
|
|
131
|
+
common.nullable = true;
|
|
132
|
+
if (source.deprecated === true)
|
|
133
|
+
common.deprecated = true;
|
|
134
|
+
if (source.default !== undefined)
|
|
135
|
+
common.default = source.default;
|
|
136
|
+
return common;
|
|
137
|
+
}
|
|
138
|
+
/** OpenAPI 3.1 writes `nullable` as `type: [T, "null"]`. */
|
|
139
|
+
#type(source, common, where) {
|
|
140
|
+
if (Array.isArray(source.type)) {
|
|
141
|
+
const types = source.type.filter((type) => type !== "null");
|
|
142
|
+
if (types.length !== source.type.length)
|
|
143
|
+
common.nullable = true;
|
|
144
|
+
if (types.length === 1)
|
|
145
|
+
return types[0];
|
|
146
|
+
this.problems.push(`${where}: a schema of several types ${JSON.stringify(source.type)}`);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (source.type !== undefined)
|
|
150
|
+
return source.type;
|
|
151
|
+
if (source.const !== undefined)
|
|
152
|
+
return typeOfConst(source.const);
|
|
153
|
+
if (Array.isArray(source.enum)) {
|
|
154
|
+
const types = new Set(source.enum.map(typeOfConst));
|
|
155
|
+
if (types.size === 1)
|
|
156
|
+
return [...types][0];
|
|
157
|
+
}
|
|
158
|
+
if (source.items !== undefined)
|
|
159
|
+
return "array";
|
|
160
|
+
if (source.properties !== undefined || source.required !== undefined || source.additionalProperties !== undefined)
|
|
161
|
+
return "object";
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
schema(source, where) {
|
|
165
|
+
if (!isObject(source)) {
|
|
166
|
+
this.problems.push(`${where}: a schema must be an object`);
|
|
167
|
+
return { type: "unknown" };
|
|
168
|
+
}
|
|
169
|
+
for (const keyword of Object.keys(source)) {
|
|
170
|
+
if (!KNOWN.has(keyword) && !keyword.startsWith("x-")) {
|
|
171
|
+
this.problems.push(`${where}: unsupported keyword "${keyword}"`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const common = this.#common(source);
|
|
175
|
+
if (source.$ref !== undefined)
|
|
176
|
+
return { ...common, type: "ref", ref: this.refId(source.$ref, where) };
|
|
177
|
+
if (Array.isArray(source.allOf))
|
|
178
|
+
return this.#allOf(source, source.allOf, common, where);
|
|
179
|
+
if (Array.isArray(source.oneOf)) {
|
|
180
|
+
const of = source.oneOf.map((member, index) => this.schema(member, `${where}.oneOf[${index}]`));
|
|
181
|
+
return { ...common, type: "union", of };
|
|
182
|
+
}
|
|
183
|
+
const type = this.#type(source, common, where);
|
|
184
|
+
const values = source.const !== undefined ? [source.const] : Array.isArray(source.enum) ? source.enum : undefined;
|
|
185
|
+
switch (type) {
|
|
186
|
+
case "string": {
|
|
187
|
+
const node = { ...common, type: "string" };
|
|
188
|
+
if (values)
|
|
189
|
+
node.enum = values.map(String);
|
|
190
|
+
if (source.format === "binary")
|
|
191
|
+
node.format = "binary";
|
|
192
|
+
if (typeof source.minLength === "number")
|
|
193
|
+
node.minLength = source.minLength;
|
|
194
|
+
if (typeof source.maxLength === "number")
|
|
195
|
+
node.maxLength = source.maxLength;
|
|
196
|
+
if (typeof source.pattern === "string")
|
|
197
|
+
node.pattern = source.pattern;
|
|
198
|
+
return node;
|
|
199
|
+
}
|
|
200
|
+
case "integer": {
|
|
201
|
+
const node = { ...common, type: "integer" };
|
|
202
|
+
if (source.format === "int32" || source.format === "int64")
|
|
203
|
+
node.format = source.format;
|
|
204
|
+
else if (source.format !== undefined)
|
|
205
|
+
this.problems.push(`${where}: unsupported integer format "${source.format}"`);
|
|
206
|
+
if (typeof source.minimum === "number")
|
|
207
|
+
node.minimum = source.minimum;
|
|
208
|
+
if (typeof source.maximum === "number")
|
|
209
|
+
node.maximum = source.maximum;
|
|
210
|
+
if (values)
|
|
211
|
+
node.enum = values.map(Number);
|
|
212
|
+
return node;
|
|
213
|
+
}
|
|
214
|
+
case "number": {
|
|
215
|
+
const node = { ...common, type: "number" };
|
|
216
|
+
if (typeof source.minimum === "number")
|
|
217
|
+
node.minimum = source.minimum;
|
|
218
|
+
if (typeof source.maximum === "number")
|
|
219
|
+
node.maximum = source.maximum;
|
|
220
|
+
return node;
|
|
221
|
+
}
|
|
222
|
+
case "boolean": {
|
|
223
|
+
const node = { ...common, type: "boolean" };
|
|
224
|
+
if (values)
|
|
225
|
+
node.enum = values.map((value) => value === true);
|
|
226
|
+
return node;
|
|
227
|
+
}
|
|
228
|
+
case "array": {
|
|
229
|
+
// JSON Schema reads a missing `items` as "any item".
|
|
230
|
+
const items = source.items === undefined ? { type: "unknown" } : this.schema(source.items, `${where}[]`);
|
|
231
|
+
const node = { ...common, type: "array", items };
|
|
232
|
+
if (typeof source.minItems === "number")
|
|
233
|
+
node.minItems = source.minItems;
|
|
234
|
+
if (typeof source.maxItems === "number")
|
|
235
|
+
node.maxItems = source.maxItems;
|
|
236
|
+
if (source.uniqueItems === true)
|
|
237
|
+
node.uniqueItems = true;
|
|
238
|
+
return node;
|
|
239
|
+
}
|
|
240
|
+
case "object":
|
|
241
|
+
return this.#object(source, common, where);
|
|
242
|
+
case null:
|
|
243
|
+
return { type: "unknown" };
|
|
244
|
+
case undefined:
|
|
245
|
+
if (Object.keys(source).every((keyword) => IGNORED.has(keyword) || keyword === "description")) {
|
|
246
|
+
return { ...common, type: "unknown" };
|
|
247
|
+
}
|
|
248
|
+
this.problems.push(`${where}: a schema with no type`);
|
|
249
|
+
return { type: "unknown" };
|
|
250
|
+
default:
|
|
251
|
+
this.problems.push(`${where}: unsupported type ${JSON.stringify(type)}`);
|
|
252
|
+
return { type: "unknown" };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
#object(source, common, where) {
|
|
256
|
+
const properties = {};
|
|
257
|
+
for (const [name, property] of Object.entries(isObject(source.properties) ? source.properties : {})) {
|
|
258
|
+
properties[name] = this.schema(property, `${where}.${name}`);
|
|
259
|
+
}
|
|
260
|
+
const node = {
|
|
261
|
+
...common,
|
|
262
|
+
type: "object",
|
|
263
|
+
properties,
|
|
264
|
+
required: Array.isArray(source.required) ? source.required.map(String) : [],
|
|
265
|
+
};
|
|
266
|
+
// `true` and `{}` say "anything else may appear", which a loose object already allows.
|
|
267
|
+
if (isObject(source.additionalProperties) && Object.keys(source.additionalProperties).length > 0) {
|
|
268
|
+
node.additionalProperties = this.schema(source.additionalProperties, `${where}.*`);
|
|
269
|
+
}
|
|
270
|
+
return node;
|
|
271
|
+
}
|
|
272
|
+
#allOf(source, members, common, where) {
|
|
273
|
+
const of = members.map((member, index) => this.schema(member, `${where}.allOf[${index}]`));
|
|
274
|
+
const [only] = of;
|
|
275
|
+
// `type: object` beside a one-member allOf is inheritance with no fields of its own; without it,
|
|
276
|
+
// the allOf only attaches a description or `nullable` to a reference.
|
|
277
|
+
if (of.length !== 1 || !only || source.type === "object")
|
|
278
|
+
return { ...common, type: "allOf", of };
|
|
279
|
+
const constraints = PRIMITIVE_CONSTRAINTS.filter((keyword) => source[keyword] !== undefined);
|
|
280
|
+
if (constraints.length === 0)
|
|
281
|
+
return { ...only, ...common };
|
|
282
|
+
if (only.type !== "ref") {
|
|
283
|
+
this.problems.push(`${where}: constraints beside a non-reference allOf member`);
|
|
284
|
+
return only;
|
|
285
|
+
}
|
|
286
|
+
const target = this.#components[only.ref];
|
|
287
|
+
const inlined = this.schema({ ...(isObject(target) ? target : {}), ...pick(source, constraints) }, where);
|
|
288
|
+
if (inlined.type === "object" || inlined.type === "allOf") {
|
|
289
|
+
this.problems.push(`${where}: constraints ${constraints.join(", ")} beside a reference to an object`);
|
|
290
|
+
}
|
|
291
|
+
return { ...inlined, ...common };
|
|
292
|
+
}
|
|
293
|
+
components() {
|
|
294
|
+
return Object.entries(this.#components).map(([id, source]) => {
|
|
295
|
+
const where = `#/components/schemas/${id}`;
|
|
296
|
+
const schema = { id, schema: this.schema(source, where) };
|
|
297
|
+
if (isObject(source) && isObject(source.discriminator)) {
|
|
298
|
+
const { propertyName, mapping } = source.discriminator;
|
|
299
|
+
if (typeof propertyName !== "string" || !isObject(mapping)) {
|
|
300
|
+
this.problems.push(`${where}: a discriminator needs propertyName and an explicit mapping`);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
schema.discriminator = {
|
|
304
|
+
property: propertyName,
|
|
305
|
+
mapping: Object.fromEntries(Object.entries(mapping).map(([value, ref]) => [value, this.refId(ref, `${where}.discriminator`)])),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return schema;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
#parameter(reference, where) {
|
|
313
|
+
const source = this.#parameterSource(reference, where);
|
|
314
|
+
if (source === undefined)
|
|
315
|
+
return undefined;
|
|
316
|
+
if (!isObject(source) || typeof source.name !== "string") {
|
|
317
|
+
this.problems.push(`${where}: a parameter needs a name`);
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
if (source.in === "header" && TRANSPORT_HEADERS.has(source.name.toLowerCase()))
|
|
321
|
+
return undefined;
|
|
322
|
+
if (source.in !== "path" && source.in !== "query" && source.in !== "header") {
|
|
323
|
+
this.problems.push(`${where}: parameter "${source.name}" is in ${JSON.stringify(source.in)}`);
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
const parameter = {
|
|
327
|
+
name: source.name,
|
|
328
|
+
in: source.in,
|
|
329
|
+
required: source.required === true || source.in === "path",
|
|
330
|
+
schema: this.schema(source.schema, `${where}.${source.name}`),
|
|
331
|
+
};
|
|
332
|
+
if (typeof source.description === "string" && source.description.trim())
|
|
333
|
+
parameter.description = source.description;
|
|
334
|
+
return parameter;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* JSON when offered, unless a multipart body carries a file, which JSON cannot; then multipart,
|
|
338
|
+
* then a form. A content type outside these three refuses the operation.
|
|
339
|
+
*/
|
|
340
|
+
#requestBody(source, where) {
|
|
341
|
+
const content = isObject(source.content) ? source.content : {};
|
|
342
|
+
const schemaOf = (type) => (isObject(content[type]) ? content[type].schema : undefined);
|
|
343
|
+
const carriesFile = JSON.stringify(schemaOf(MULTIPART) ?? {}).includes('"format":"binary"');
|
|
344
|
+
const offered = Object.keys(content);
|
|
345
|
+
const type = carriesFile ? MULTIPART : [JSON_TYPE, MULTIPART, FORM].find((candidate) => offered.includes(candidate));
|
|
346
|
+
const body = { confidence: "contract", required: source.required === true };
|
|
347
|
+
if (offered.length === 0)
|
|
348
|
+
return body;
|
|
349
|
+
if (!type) {
|
|
350
|
+
this.problems.push(`${where}: a body must be JSON, multipart or a form, found ${offered.join(", ")}`);
|
|
351
|
+
return body;
|
|
352
|
+
}
|
|
353
|
+
if (type !== JSON_TYPE)
|
|
354
|
+
body.contentType = type;
|
|
355
|
+
const schema = schemaOf(type);
|
|
356
|
+
if (schema === undefined)
|
|
357
|
+
return body;
|
|
358
|
+
if (typedFromExample(schema))
|
|
359
|
+
body.confidence = "example";
|
|
360
|
+
body.schema = this.schema(schema, `${where}.requestBody`);
|
|
361
|
+
return body;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Only an inline JSON answer has a shape to check; a file, plain text or a `$ref` to a shared response
|
|
365
|
+
* is passed through as it is. Nothing at run time validates an answer yet.
|
|
366
|
+
*/
|
|
367
|
+
#response(source, where) {
|
|
368
|
+
const ok = isObject(source.responses) ? source.responses["200"] : undefined;
|
|
369
|
+
if (!isObject(ok) || !isObject(ok.content))
|
|
370
|
+
return undefined;
|
|
371
|
+
const json = ok.content[JSON_TYPE];
|
|
372
|
+
if (!isObject(json) || json.schema === undefined)
|
|
373
|
+
return undefined;
|
|
374
|
+
return this.schema(json.schema, `${where}.responses.200`);
|
|
375
|
+
}
|
|
376
|
+
operations() {
|
|
377
|
+
const operations = [];
|
|
378
|
+
const paths = isObject(this.document.paths) ? this.document.paths : {};
|
|
379
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
380
|
+
if (!isObject(item))
|
|
381
|
+
continue;
|
|
382
|
+
const shared = Array.isArray(item.parameters) ? item.parameters : [];
|
|
383
|
+
for (const method of METHODS) {
|
|
384
|
+
const source = item[method];
|
|
385
|
+
if (!isObject(source))
|
|
386
|
+
continue;
|
|
387
|
+
const pointer = `#/paths/${path.replaceAll("~", "~0").replaceAll("/", "~1")}/${method}`;
|
|
388
|
+
// A path without an id is a description, not a call: Meta models its webhook payload this way.
|
|
389
|
+
if (typeof source.operationId !== "string")
|
|
390
|
+
continue;
|
|
391
|
+
const verb = method.toUpperCase();
|
|
392
|
+
const own = (Array.isArray(source.parameters) ? source.parameters : [])
|
|
393
|
+
.map((parameter, index) => this.#parameter(parameter, `${pointer}.parameters[${index}]`))
|
|
394
|
+
.filter((parameter) => parameter !== undefined);
|
|
395
|
+
const inherited = shared
|
|
396
|
+
.map((parameter, index) => this.#parameter(parameter, `${pointer.replace(/\/[a-z]+$/, "")}.parameters[${index}]`))
|
|
397
|
+
.filter((parameter) => parameter !== undefined && !own.some((mine) => mine.name === parameter.name && mine.in === parameter.in));
|
|
398
|
+
const parameters = [...inherited, ...own];
|
|
399
|
+
for (const [, name] of path.matchAll(/\{([^}]+)\}/g)) {
|
|
400
|
+
if (!parameters.some((parameter) => parameter.in === "path" && parameter.name === name))
|
|
401
|
+
this.problems.push(`${pointer}: path parameter "${name}" is not declared`);
|
|
402
|
+
}
|
|
403
|
+
const operation = {
|
|
404
|
+
id: source.operationId,
|
|
405
|
+
binding: { kind: "http", method: verb, path },
|
|
406
|
+
tags: Array.isArray(source.tags) ? source.tags.map(String) : [],
|
|
407
|
+
parameters,
|
|
408
|
+
effect: EFFECT_BY_METHOD[verb],
|
|
409
|
+
source: { location: pointer },
|
|
410
|
+
};
|
|
411
|
+
if (typeof source.summary === "string")
|
|
412
|
+
operation.summary = source.summary;
|
|
413
|
+
if (typeof source.description === "string")
|
|
414
|
+
operation.description = source.description;
|
|
415
|
+
if (source.deprecated === true)
|
|
416
|
+
operation.deprecated = true;
|
|
417
|
+
if (isObject(source.requestBody))
|
|
418
|
+
operation.requestBody = this.#requestBody(source.requestBody, pointer);
|
|
419
|
+
const response = this.#response(source, pointer);
|
|
420
|
+
if (response)
|
|
421
|
+
operation.response = { confidence: "contract", required: true, schema: response };
|
|
422
|
+
operations.push(operation);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return operations;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/** Throws `OpenApiError` with every problem at once, so one run shows all a spec update broke. */
|
|
429
|
+
export const adaptOpenApi = (document, source = {}) => {
|
|
430
|
+
if (!isObject(document) || typeof document.openapi !== "string" || !document.openapi.startsWith("3.")) {
|
|
431
|
+
throw new OpenApiError(["not an OpenAPI 3.x document"]);
|
|
432
|
+
}
|
|
433
|
+
const adapter = new Adapter(document);
|
|
434
|
+
const schemas = adapter.components();
|
|
435
|
+
const operations = adapter.operations();
|
|
436
|
+
if (operations.length === 0)
|
|
437
|
+
adapter.problems.push("the document has no operations");
|
|
438
|
+
if (adapter.problems.length > 0)
|
|
439
|
+
throw new OpenApiError(adapter.problems);
|
|
440
|
+
const info = isObject(document.info) ? document.info : {};
|
|
441
|
+
return {
|
|
442
|
+
source: { kind: "openapi", ...(typeof info.version === "string" ? { version: info.version } : {}), ...source },
|
|
443
|
+
schemas,
|
|
444
|
+
operations,
|
|
445
|
+
};
|
|
446
|
+
};
|
package/dist/codegen/tree.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { ApiModel, ApiSchema, SchemaNode } from "./model.js";
|
|
|
2
2
|
type ObjectNode = Extract<SchemaNode, {
|
|
3
3
|
type: "object";
|
|
4
4
|
}>;
|
|
5
|
+
/** The values a subtype's discriminator property may hold: one per mapping entry that selects it. */
|
|
6
|
+
export interface Literal {
|
|
7
|
+
property: string;
|
|
8
|
+
values: readonly string[];
|
|
9
|
+
}
|
|
5
10
|
export interface NamedSchema {
|
|
6
11
|
name: string;
|
|
7
12
|
schema: ApiSchema;
|
|
@@ -17,11 +22,8 @@ export declare class SchemaTree {
|
|
|
17
22
|
schema(id: string): ApiSchema;
|
|
18
23
|
requestName(operationId: string): string | undefined;
|
|
19
24
|
responseName(operationId: string): string | undefined;
|
|
20
|
-
/** The
|
|
21
|
-
literalFor(id: string):
|
|
22
|
-
property: string;
|
|
23
|
-
value: string;
|
|
24
|
-
} | undefined;
|
|
25
|
+
/** The values a subtype's discriminator property may hold, if the schema is a subtype. */
|
|
26
|
+
literalFor(id: string): Literal | undefined;
|
|
25
27
|
/** Merges inheritance into one object. A member that is not an object stops generation. */
|
|
26
28
|
flatten(node: SchemaNode, where: string): ObjectNode;
|
|
27
29
|
}
|
package/dist/codegen/tree.js
CHANGED
|
@@ -36,10 +36,16 @@ export class SchemaTree {
|
|
|
36
36
|
}
|
|
37
37
|
for (const schema of model.schemas) {
|
|
38
38
|
for (const [value, target] of Object.entries(schema.discriminator?.mapping ?? {})) {
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const property = schema.discriminator?.property ?? "";
|
|
40
|
+
const known = this.#literals.get(target);
|
|
41
|
+
// Two bases may share a subtype when both choose it by the same property, as Meta's sent and received messages do.
|
|
42
|
+
if (known && known.property !== property) {
|
|
43
|
+
throw new CodegenError([
|
|
44
|
+
`schema "${target}" is a subtype chosen by both "${known.property}" and "${property}"`,
|
|
45
|
+
]);
|
|
41
46
|
}
|
|
42
|
-
|
|
47
|
+
const values = known?.values ?? [];
|
|
48
|
+
this.#literals.set(target, { property, values: values.includes(value) ? values : [...values, value] });
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
}
|
|
@@ -55,7 +61,7 @@ export class SchemaTree {
|
|
|
55
61
|
responseName(operationId) {
|
|
56
62
|
return this.#responses.get(operationId);
|
|
57
63
|
}
|
|
58
|
-
/** The
|
|
64
|
+
/** The values a subtype's discriminator property may hold, if the schema is a subtype. */
|
|
59
65
|
literalFor(id) {
|
|
60
66
|
return this.#literals.get(id);
|
|
61
67
|
}
|
|
@@ -55,7 +55,9 @@ class TypeWriter {
|
|
|
55
55
|
const fields = names.map((name) => {
|
|
56
56
|
const property = node.properties[name] ?? { type: "string" };
|
|
57
57
|
const optional = node.required.includes(name) || literal?.property === name ? "" : "?";
|
|
58
|
-
const type = literal?.property === name
|
|
58
|
+
const type = literal?.property === name
|
|
59
|
+
? literal.values.map((value) => JSON.stringify(value)).join(" | ")
|
|
60
|
+
: this.node(property, `${where}.${name}`);
|
|
59
61
|
return `${doc(property.description, " ")} ${key(name)}${optional}: ${type}`;
|
|
60
62
|
});
|
|
61
63
|
const body = `{\n${fields.join("\n")}\n}`;
|
package/dist/codegen/valibot.js
CHANGED
|
@@ -97,8 +97,13 @@ class SchemaWriter {
|
|
|
97
97
|
if (names.length === 0)
|
|
98
98
|
return rest ? `v.record(v.string(), ${rest})` : "v.looseObject({})";
|
|
99
99
|
const entries = names.map((name) => {
|
|
100
|
-
if (literal?.property === name)
|
|
101
|
-
|
|
100
|
+
if (literal?.property === name) {
|
|
101
|
+
const [only] = literal.values;
|
|
102
|
+
const exact = literal.values.length === 1
|
|
103
|
+
? `v.literal(${JSON.stringify(only)})`
|
|
104
|
+
: `v.picklist(${JSON.stringify(literal.values)})`;
|
|
105
|
+
return ` ${key(name)}: ${exact},`;
|
|
106
|
+
}
|
|
102
107
|
const property = node.properties[name];
|
|
103
108
|
const schema = this.node(property, `${where}.${name}`);
|
|
104
109
|
return ` ${key(name)}: ${node.required.includes(name) ? schema : `v.optional(${schema})`},`;
|
package/dist/query/parser.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { type QueryAst, type QueryContext } from "./types.js";
|
|
|
2
2
|
export interface ParseOptions extends QueryContext {
|
|
3
3
|
/** The field a bare word searches; `none` for a list, where a bare word is `query.bare-word`. */
|
|
4
4
|
defaultField?: string;
|
|
5
|
+
/** Read `"a b"~N` as a phrase with `slop: N`; a caller that turns it on says what N counts. */
|
|
6
|
+
proximity?: boolean;
|
|
5
7
|
}
|
|
6
|
-
export declare const parseLucene: (text: string, { defaultField, ...context }?: ParseOptions) => QueryAst;
|
|
8
|
+
export declare const parseLucene: (text: string, { defaultField, proximity, ...context }?: ParseOptions) => QueryAst;
|
|
7
9
|
/** Bare terms eligible for forgiving retrieval; explicit syntax keeps strict semantics. */
|
|
8
10
|
export declare const implicitTextTerms: (text: string) => Set<number>;
|
package/dist/query/parser.js
CHANGED
|
@@ -141,7 +141,7 @@ const regexHint = (text, at) => {
|
|
|
141
141
|
const word = `${/[^\s()"]*$/u.exec(text.slice(0, at))?.[0] ?? ""}${/^[^\s()"]*/u.exec(text.slice(at))?.[0] ?? ""}`;
|
|
142
142
|
return quoteHint(text, at) ?? `/ starts a regular expression; put the value in quotes: "${word}"`;
|
|
143
143
|
};
|
|
144
|
-
export const parseLucene = (text, { defaultField = "text", ...context } = {}) => {
|
|
144
|
+
export const parseLucene = (text, { defaultField = "text", proximity = false, ...context } = {}) => {
|
|
145
145
|
const fail = (reason, span, alternative) => queryError(reason, span, alternative ?? quoteHint(text, span.start), context);
|
|
146
146
|
if (text.length > QUERY_LIMITS.bytes || new TextEncoder().encode(text).length > QUERY_LIMITS.bytes)
|
|
147
147
|
fail("query.too-large", { start: 0, end: text.length });
|
|
@@ -290,6 +290,13 @@ export const parseLucene = (text, { defaultField = "text", ...context } = {}) =>
|
|
|
290
290
|
...(implicit ? { implicit: true } : {}),
|
|
291
291
|
});
|
|
292
292
|
}
|
|
293
|
+
if (proximity && peek().kind === "~" && node.kind === "predicate" && node.operator === "phrase") {
|
|
294
|
+
take();
|
|
295
|
+
const distance = take();
|
|
296
|
+
if (distance.kind !== "number" || !/^\d+$/u.test(distance.raw))
|
|
297
|
+
fail("query.invalid-syntax", distance.span, 'write the distance as a whole number: "a b"~3');
|
|
298
|
+
node = { ...node, slop: Number(distance.raw), span: { start: node.span.start, end: distance.span.end } };
|
|
299
|
+
}
|
|
293
300
|
if (peek().kind === "~")
|
|
294
301
|
fail("query.unsupported-operator", peek().span, node.kind === "predicate" && node.operator === "phrase"
|
|
295
302
|
? "proximity (~) is not supported — drop the ~ for the exact phrase, or join the words with AND"
|
package/dist/query/types.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface Predicate {
|
|
|
21
21
|
span: Span;
|
|
22
22
|
/** A bare word, under no field. */
|
|
23
23
|
implicit?: true;
|
|
24
|
+
/** `"a b"~N`, read only when the parser is asked to: how far apart the phrase's words may be. */
|
|
25
|
+
slop?: number;
|
|
24
26
|
}
|
|
25
27
|
export type QueryNode = Predicate | {
|
|
26
28
|
kind: "boolean";
|
package/dist/query/types.js
CHANGED
|
@@ -16,6 +16,7 @@ const predicateSchema = v.object({
|
|
|
16
16
|
upper_inclusive: v.optional(v.boolean()),
|
|
17
17
|
span: spanSchema,
|
|
18
18
|
implicit: v.optional(v.literal(true)),
|
|
19
|
+
slop: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
19
20
|
});
|
|
20
21
|
export const queryNodeSchema = v.variant("kind", [
|
|
21
22
|
predicateSchema,
|
|
@@ -15,8 +15,9 @@ Rule ids on this page are `WC-ACCT-<n>`. How to read a rule, how ids work and wh
|
|
|
15
15
|
|
|
16
16
|
cli-core resolves the profile for tg, max and wa. WC-ACCT-1 needs cli-core 0.319.0 or newer, and all three pin
|
|
17
17
|
0.321.0. memo has no messenger profile: its shared commands run without one, and they pick accounts with `--provider`
|
|
18
|
-
and `--account` (WC-ACCT-7).
|
|
19
|
-
|
|
18
|
+
and `--account` (WC-ACCT-7). zoom resolves its own profile word by the same rules (`zm <profile> …`, `ZM_PROFILE`,
|
|
19
|
+
`ZM_PROFILE_LOCK`) from zoom-cli 0.35.0, with cli-core's `liftProfile` and `usableProfileName`. drive still has its own
|
|
20
|
+
profile code: it registers its own profiles. That is a known gap, not an exception.
|
|
20
21
|
|
|
21
22
|
## Rules
|
|
22
23
|
|
package/docs/standards/ci.md
CHANGED
|
@@ -96,7 +96,7 @@ cli-testing, cli-docs, plus community itself.
|
|
|
96
96
|
cli-docs validates its site before each deployment instead, and community has no build. Third-party security apps
|
|
97
97
|
may post their own checks.
|
|
98
98
|
- **Applies to:** the CLIs, cli-base, cli-core and cli-testing. cli-base, whatsapp-cli, tg-cli, max-cli, cli-core
|
|
99
|
-
|
|
99
|
+
cli-memo and zoom-cli have `pr.yml`; drive-cli and cli-testing do not have it yet. **Since:**
|
|
100
100
|
cli-base 0.41.0 (pull request validation since cli-base 0.44.6). **Rulings:** none.
|
|
101
101
|
|
|
102
102
|
### WC-CI-6 · Run the changelog guard on every pull request
|
|
@@ -169,8 +169,9 @@ cli-testing, cli-docs, plus community itself.
|
|
|
169
169
|
- **Enforced by:** `whatsapp-cli: main ruleset required checks` · `cli-core: main ruleset required checks` ·
|
|
170
170
|
`tg-cli: main ruleset required checks` · `max-cli: main ruleset required checks` ·
|
|
171
171
|
`cli-memo: main ruleset required checks`, plus the main ruleset of cli-base itself. In each repository with pr.yml,
|
|
172
|
-
the ruleset requires the pull request validation jobs besides the standards job. drive-cli,
|
|
173
|
-
|
|
172
|
+
the ruleset requires the pull request validation jobs besides the standards job. drive-cli, cli-testing and cli-docs
|
|
173
|
+
have no pr.yml, so their rulesets require only the standards job, and review covers the rest. zoom-cli has pr.yml,
|
|
174
|
+
but its ruleset still requires only the standards job.
|
|
174
175
|
- **Example:** `gh pr checks <number>` shows every line as pass before `gh pr merge`. Never merge with `--admin`. A
|
|
175
176
|
release changes `main` the same way
|
|
176
177
|
([WC-REL-2](releases.md#wc-rel-2--main-changes-only-through-a-pull-request-also-during-a-release)).
|
package/docs/standards/mcp.md
CHANGED
|
@@ -20,9 +20,10 @@ Rule ids on this page are `WC-MCP-<n>`. How to read a rule, how ids work and whi
|
|
|
20
20
|
### Who follows these rules today
|
|
21
21
|
|
|
22
22
|
tg, max and wa serve cli-core's personal server (`<cli> <profile> mcp`) and bot server (`<cli> <profile> bot mcp`).
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
zoom serves the three shared tools over its own commands from zoom-cli 0.35.0 (`zm <profile> mcp`, cli-core's
|
|
24
|
+
`toolsFromCommands` and `runCommandForAgent`). memo and drive have their own servers: memo has hand-written tools, and
|
|
25
|
+
drive builds one tool per command from its command tree and skips commands through its own list. Those two are known
|
|
26
|
+
gaps against these rules.
|
|
26
27
|
|
|
27
28
|
WC-MCP-2 and WC-MCP-8 need cli-core 0.321.0, which tg, max and wa pin.
|
|
28
29
|
|
package/docs/standards/output.md
CHANGED
|
@@ -52,9 +52,9 @@ JSON Schema 2020-12.
|
|
|
52
52
|
- **Enforced by:** `cli-core: src/cli/envelope.test.ts` · `cli-core: src/mcp/output-schema.test.ts`.
|
|
53
53
|
- **Example:** a list is `{ items, page, next?, missing?, meta }`. One thing is `{ item, next?, missing?, meta }`.
|
|
54
54
|
cli-core builds them with `listOf` and `itemOf`, and its `renderAnswer` refuses any other shape. A list never
|
|
55
|
-
answers a bare array.
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
answers a bare array. A `--dry-run` preview is one thing: `{ item: <preview>, meta }`, with the
|
|
56
|
+
preview's parsed arguments and declared effects inside `item`.
|
|
57
|
+
- **Exceptions:** command discovery (`commands --json`) is printed whole, with no envelope.
|
|
58
58
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-358 (its older list shape is replaced by
|
|
59
59
|
this one), NEED-431.
|
|
60
60
|
|
|
@@ -18,8 +18,10 @@ Rule ids on this page are `WC-PERM-<n>`. How to read a rule, how ids work and wh
|
|
|
18
18
|
### Who follows these rules today
|
|
19
19
|
|
|
20
20
|
tg, max and wa use cli-core's permissions and write guard in full. memo uses them for the shared commands only. drive
|
|
21
|
-
has its own policy and guard, with its own `--read-only` flag
|
|
22
|
-
writes
|
|
21
|
+
has its own policy and guard, with its own `--read-only` flag. zoom has per-profile `permissions` from zoom-cli 0.35.0,
|
|
22
|
+
read with cli-core's `levelFor`; its writes change Zoom meetings, its built-in default is `remote-meetings: ask`, and
|
|
23
|
+
it has no send journal or recipient list, since it sends no messages. Where this page says "every CLI that writes",
|
|
24
|
+
drive is a known gap.
|
|
23
25
|
|
|
24
26
|
WC-PERM-10 and WC-PERM-11 need cli-core 0.321.0, which tg, max and wa pin. WC-PERM-12 as written needs cli-core
|
|
25
27
|
0.337.0; before it, the lock let through some local writes that overwrite what the owner saved. A command other
|
|
@@ -161,7 +161,11 @@ on the [CI](ci.md) page.
|
|
|
161
161
|
- **Enforced by:** `tg-cli: bin/release` · `max-cli: bin/release` · `whatsapp-cli: bin/release` ·
|
|
162
162
|
`cli-memo: bin/release`.
|
|
163
163
|
- **Example:** `bin/release --local` runs the same checks on this machine and reads the npm token from the keyring
|
|
164
|
-
into the publish command's environment.
|
|
164
|
+
into the publish command's environment. The checks include the test suite with its coverage floor,
|
|
165
|
+
any shipped non-JavaScript engine's tests, offline contracts, leak and secret scans, and dependency
|
|
166
|
+
vulnerability audits for each shipped language. Build and validate before supplying the token to the
|
|
167
|
+
publish process; publishing must not run unchecked lifecycle scripts with it. Neither success nor an
|
|
168
|
+
error prints the token.
|
|
165
169
|
- **Exceptions:** none. drive's and zoom's `bin/release` have no `--local` yet.
|
|
166
170
|
- **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
167
171
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.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",
|
|
@@ -139,7 +139,8 @@
|
|
|
139
139
|
"lefthook": "2.1.16",
|
|
140
140
|
"markdownlint-cli2": "0.23.3",
|
|
141
141
|
"typescript": "^7.0.2",
|
|
142
|
-
"vitest": "^5.0.2"
|
|
142
|
+
"vitest": "^5.0.2",
|
|
143
|
+
"yaml": "^2.9.1"
|
|
143
144
|
},
|
|
144
145
|
"publishConfig": {
|
|
145
146
|
"access": "public"
|