cliguard 0.6.0 → 0.7.1
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 +148 -0
- package/dist/adapters/adapter.interface.d.ts +10 -0
- package/dist/adapters/cac.adapter.d.ts +1 -0
- package/dist/adapters/cac.adapter.js +5 -0
- package/dist/adapters/commander.adapter.d.ts +2 -0
- package/dist/adapters/commander.adapter.js +2 -0
- package/dist/adapters/registry.d.ts +3 -0
- package/dist/adapters/registry.js +26 -0
- package/dist/adapters/yargs.adapter.d.ts +1 -0
- package/dist/adapters/yargs.adapter.js +3 -0
- package/dist/bin.js +285 -35
- package/dist/core/config.d.ts +24 -0
- package/dist/core/config.js +106 -0
- package/dist/core/diff.engine.d.ts +56 -1
- package/dist/core/diff.engine.js +123 -8
- package/dist/core/report-formats.d.ts +44 -0
- package/dist/core/report-formats.js +103 -0
- package/dist/core/storage.d.ts +25 -1
- package/dist/core/storage.js +121 -0
- package/dist/core/types.d.ts +19 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +66 -0
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -97,6 +97,9 @@ Then:
|
|
|
97
97
|
# Capture the current contract - commit .cliguard/contract.json
|
|
98
98
|
npx cliguard init ./bin/cli.js
|
|
99
99
|
|
|
100
|
+
# Same, plus scaffold .github/workflows/cliguard.yml so CI is wired up too
|
|
101
|
+
npx cliguard init ./bin/cli.js --with-ci
|
|
102
|
+
|
|
100
103
|
# In CI: fail the build on any breaking change
|
|
101
104
|
npx cliguard check ./bin/cli.js
|
|
102
105
|
|
|
@@ -125,6 +128,43 @@ npx cliguard check ./bin/cli.js --json
|
|
|
125
128
|
|
|
126
129
|
`suggestedBump` is the semver bump this diff implies (`"major"`, `"minor"`, `"patch"`, or `null` if nothing changed) - a direct read of the same BREAKING/ADDITIVE/PATCH classification the emoji output already uses, so a release script never has to re-derive it.
|
|
127
130
|
|
|
131
|
+
### Marking something unstable right where it's declared
|
|
132
|
+
|
|
133
|
+
`cliguard.config.js` is a separate file - useful for a blanket rule, but one more place to keep in sync as flags get renamed or removed. For a single command/option/argument that isn't stable yet, mark it in its own description instead:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
program.option("--fast", "skip checks [unstable]");
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Any BREAKING change to a path whose own description contains `[unstable]` reports as PATCH instead - the marker travels with the code, so it can't silently point at a flag that no longer exists the way an external ignore list can.
|
|
140
|
+
|
|
141
|
+
### Project-wide policy: ignoring or downgrading a whole class of change
|
|
142
|
+
|
|
143
|
+
`accept`/`deprecate` handle one breaking change at a time. For a rule that applies to a whole class of changes - "alias changes are never breaking for us," "ignore everything under the `debug` subcommand" - write `cliguard.config.js` (or `.cjs`) instead:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
// cliguard.config.js
|
|
147
|
+
module.exports = {
|
|
148
|
+
// Dropped from the report entirely - never shown, never fails the build.
|
|
149
|
+
ignore: ["root -> debug -> *"],
|
|
150
|
+
|
|
151
|
+
// Reclassified, not dropped - still visible, just not BREAKING anymore.
|
|
152
|
+
severityOverrides: [{ pattern: /alias/, severity: "PATCH" }],
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`pattern` in either field is a `RegExp` or a glob string (`*` matches any run of characters) matched against a change's path (the same string `check`'s own output shows, e.g. `"root -> build -> option[--target]"`). Applied before `accept`/`deprecate` ever run, so a change this config already downgraded has nothing left for either of those to act on. No `cliguard.config.js` present is a no-op - every project behaves exactly as it always has.
|
|
157
|
+
|
|
158
|
+
### Comparing against a git ref instead of a local file
|
|
159
|
+
|
|
160
|
+
`check` normally diffs against `.cliguard/contract.json` on disk, but a CI runner checking out a PR branch often doesn't have a freshly-updated one - `--against <ref>` reads the contract straight out of git instead, no local file required:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
npx cliguard check ./bin/cli.js --against origin/main
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Works with any ref `git show` understands - a branch, a tag, a commit sha. Combine with `--json` the same way as the file-based path.
|
|
167
|
+
|
|
128
168
|
### Accepting an intentional breaking change
|
|
129
169
|
|
|
130
170
|
Sometimes a `BREAKING` change is exactly what you meant to ship - a flag genuinely needed to go away in a major version. Running `cliguard update` after a real, intentional break re-baselines the *entire* contract silently; it doesn't leave a record of what changed or why. `cliguard accept` does:
|
|
@@ -135,6 +175,20 @@ npx cliguard accept ./bin/cli.js "root -> build -> option[--target]" --reason "r
|
|
|
135
175
|
|
|
136
176
|
This only works against a change `check` would currently report as `BREAKING` - it reads the exact `path` from your own `check` output (text or `--json`), so there's nothing to guess. It writes `.cliguard/accepted-breaks.json` (commit this file); from then on, `check` still shows that change - now as a 🟣 acknowledged line with the reason attached - but stops counting it toward the `BREAKING` total that fails your build. Any *other*, un-accepted breaking change still fails CI as normal. Once you're done, `cliguard update` still re-baselines the contract to match reality, same as always.
|
|
137
177
|
|
|
178
|
+
### Deprecating something ahead of its removal
|
|
179
|
+
|
|
180
|
+
`accept` forgives a break that already happened. `deprecate` is the other half - announce a removal *before* it happens, so when it eventually does, it's a PATCH instead of a BREAKING change:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
# The option still exists today - deprecate marks it for a future removal
|
|
184
|
+
npx cliguard deprecate ./bin/cli.js "root -> build -> option[--target]" \
|
|
185
|
+
--remove-by 2.0.0 --reason "replaced by --targets"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
This only works against a path that currently exists (it reads the same `path` shape `check`/`accept` use) - it writes `.cliguard/deprecations.json` (commit this file). From then on, whenever that command/option/argument actually gets removed, `check` reports it as PATCH, with the deprecation's reason and `--remove-by` folded into the message, instead of failing the build. Removing anything that was never deprecated first still fails exactly as before - deprecation has to be announced ahead of the break, not applied retroactively.
|
|
189
|
+
|
|
190
|
+
`--remove-by` is informational only (a version or a date, whichever fits your release process) - cliguard never checks it against the clock or your `package.json` version, it's just carried through into the message so whoever's reading a changelog or a PR comment knows the plan.
|
|
191
|
+
|
|
138
192
|
### Comparing two contracts directly
|
|
139
193
|
|
|
140
194
|
`cliguard diff <old.json> <new.json>` runs the same comparison as `check`, but reads both sides straight off disk instead of running any CLI - useful for comparing two tags' committed contracts (`git show v1.0.0:.cliguard/contract.json > old.json`), or reviewing a contract change in a PR without a working copy of the target CLI at all:
|
|
@@ -145,6 +199,77 @@ npx cliguard diff old-contract.json new-contract.json --json
|
|
|
145
199
|
|
|
146
200
|
It respects `.cliguard/accepted-breaks.json` the same way `check` does, and exits `1` on an un-acknowledged `BREAKING` change.
|
|
147
201
|
|
|
202
|
+
### Previewing a contract without committing it
|
|
203
|
+
|
|
204
|
+
`cliguard preview <entry>` runs the same extraction `init` would, but prints the contract to stdout instead of writing `.cliguard/contract.json` - useful for sanity-checking what a new adapter or a lazily-built target CLI actually captures before you commit to it as the baseline:
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
npx cliguard preview ./bin/cli.js --adapter yargs
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Checking an adapter's real limitations, or sanity-checking one against your CLI
|
|
211
|
+
|
|
212
|
+
Every adapter has a couple of real, framework-shape gaps (see "Supported frameworks" below) - `cliguard doctor` surfaces them directly instead of leaving them to a code comment only a maintainer would read:
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
npx cliguard doctor
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Pass an entry file to also run a real extraction against it and get a quick structural summary (or the real failure, if extraction doesn't work) instead of a full contract dump:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
npx cliguard doctor ./bin/cli.js --adapter yargs
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Catching a breaking change before it reaches CI
|
|
225
|
+
|
|
226
|
+
`cliguard install-hook <entry>` installs a git hook (`pre-push` by default) that runs `cliguard check` automatically, so a breaking change is caught locally instead of waiting for CI to say so:
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
npx cliguard install-hook ./bin/cli.js
|
|
230
|
+
# or, to gate every commit instead of every push:
|
|
231
|
+
npx cliguard install-hook ./bin/cli.js --hook pre-commit
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Never overwrites a hook that's already there - if you're already using [husky](https://typicode.github.io/husky/) or a similar tool, add the same `npx cliguard check ...` line to your existing hook instead.
|
|
235
|
+
|
|
236
|
+
### `--strict`: catching changes the default rules can't see
|
|
237
|
+
|
|
238
|
+
The default rules match commands/options/arguments by name, so a change that keeps every name the same is invisible to them - even when it can still break a caller. `--strict` adds rules for exactly that gap. Today, one: a pure reorder of a command's positional arguments.
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
// before
|
|
242
|
+
program.command("copy").argument("<src>").argument("<dest>");
|
|
243
|
+
// after - same two arguments, swapped order
|
|
244
|
+
program.command("copy").argument("<dest>").argument("<src>");
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The default rules see no change at all here (`<src>` still exists, `<dest>` still exists). But `cli copy a.txt b.txt` now copies `b.txt` over `a.txt`, not the reverse - a real, silent break for anyone calling it positionally:
|
|
248
|
+
|
|
249
|
+
```sh
|
|
250
|
+
npx cliguard check ./bin/cli.js --strict
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Off by default so it never changes behavior for an existing CI config - opt in per project.
|
|
254
|
+
|
|
255
|
+
A `--strict`-only change is real BREAKING output, so `cliguard accept` needs the same flag to find it - `cliguard accept ./bin/cli.js "root -> copy" --strict --reason "..."` - without it, `accept` compares in default (non-strict) mode and won't see the change at all.
|
|
256
|
+
|
|
257
|
+
## Programmatic API
|
|
258
|
+
|
|
259
|
+
Everything above is the CLI. The same extraction and diff logic is also available as a library, for a custom build script, monorepo tool, or bot that wants to embed a contract check without spawning `npx cliguard` as a subprocess:
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
const { extractContract, compareContracts, ChangeType } = require("cliguard");
|
|
263
|
+
|
|
264
|
+
const oldContract = await extractContract("./bin/cli.js"); // or read one off disk yourself
|
|
265
|
+
const newContract = await extractContract("./bin/cli.js");
|
|
266
|
+
const diff = compareContracts(oldContract, newContract, { strict: true });
|
|
267
|
+
|
|
268
|
+
const breaking = diff.filter((change) => change.type === ChangeType.BREAKING);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`listAdapters()` returns every name `extractContract`'s second argument accepts. `DiffEngine`, every adapter class (`CommanderAdapter`/`CacAdapter`/`YargsAdapter`), and the `toJUnitXml`/`toGitLabCodeQuality`/`toRdjsonl` formatters are all exported too, for anything more custom than the two convenience functions cover.
|
|
272
|
+
|
|
148
273
|
## How changes get classified
|
|
149
274
|
|
|
150
275
|
| | Removed | Added | Required flipped | Value type / default changed |
|
|
@@ -156,8 +281,30 @@ It respects `.cliguard/accepted-breaks.json` the same way `check` does, and exit
|
|
|
156
281
|
|
|
157
282
|
Full rules live in [`src/core/diff.engine.ts`](src/core/diff.engine.ts) - it's the one file worth reading if you want to know exactly why something was flagged.
|
|
158
283
|
|
|
284
|
+
### Reports for non-GitHub CI
|
|
285
|
+
|
|
286
|
+
The bundled GitHub Action is the recommended path on GitHub, but `check`/`diff` can also emit two other formats directly, no Action or bespoke reporter needed:
|
|
287
|
+
|
|
288
|
+
```sh
|
|
289
|
+
# JUnit XML - understood natively by Jenkins, CircleCI, Azure DevOps, and GitLab's own JUnit widget
|
|
290
|
+
npx cliguard check ./bin/cli.js --format junit > cliguard-report.xml
|
|
291
|
+
|
|
292
|
+
# GitLab Code Quality JSON - surfaced as inline annotations on a GitLab merge request
|
|
293
|
+
npx cliguard check ./bin/cli.js --format gitlab-codequality > gl-code-quality-report.json
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
A third format, `--format rdjsonl`, emits [reviewdog](https://github.com/reviewdog/reviewdog)'s own Diagnostic Format instead of a report cliguard renders itself - hand it off to whichever platform reviewdog already has a reporter for:
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
npx cliguard check ./bin/cli.js --format rdjsonl | reviewdog -f=rdjsonl -reporter=github-pr-review
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Same exit code either way - `1` on an unacknowledged BREAKING change, `0` otherwise - so any of the three drops straight into a CI job that already fails the build on a non-zero exit.
|
|
303
|
+
|
|
159
304
|
## CI integration
|
|
160
305
|
|
|
306
|
+
`cliguard init --with-ci` scaffolds the workflow below for you - `git add .github/workflows/cliguard.yml` and you're done. Prefer to see it first, or wire it up by hand? Read on.
|
|
307
|
+
|
|
161
308
|
The bundled GitHub Action (`Bryandero98/cliguard@v1`) is the recommended way to run this in CI: on top of the same exit-code gate as `npx cliguard check`, it posts the diff as a PR comment - updated in place on every push, not a new one each time - so a reviewer sees exactly what changed without opening the CI log:
|
|
162
309
|
|
|
163
310
|
```yaml
|
|
@@ -179,6 +326,7 @@ jobs:
|
|
|
179
326
|
entry: ./bin/cli.js
|
|
180
327
|
# adapter: yargs # default: commander
|
|
181
328
|
# comment-on-pr: false # default: true
|
|
329
|
+
# strict: true # default: false
|
|
182
330
|
```
|
|
183
331
|
|
|
184
332
|
Set `comment-on-pr: false` to keep the exit-code gate without the comment, or use the raw CLI directly for a non-GitHub CI provider:
|
|
@@ -8,6 +8,16 @@ import type { Contract } from "../core/types";
|
|
|
8
8
|
export interface CliAdapter {
|
|
9
9
|
/** Adapter identifier stored in `Contract.adapter`, e.g. "commander". */
|
|
10
10
|
readonly id: string;
|
|
11
|
+
/**
|
|
12
|
+
* Real, framework-shape limitations on what this adapter can populate in
|
|
13
|
+
* a Contract - not bugs, just things the target framework itself has no
|
|
14
|
+
* concept of (documented in more depth in each adapter's own doc
|
|
15
|
+
* comment). Empty for an adapter with no such gaps. Surfaced by
|
|
16
|
+
* `cliguard doctor` so a user hits this in a one-line summary instead of
|
|
17
|
+
* only discovering it by reading adapter source or being surprised by a
|
|
18
|
+
* `required` that's silently always `false`.
|
|
19
|
+
*/
|
|
20
|
+
readonly limitations: readonly string[];
|
|
11
21
|
/** Loads `entryPath` and extracts its full command surface as a Contract. */
|
|
12
22
|
extract(entryPath: string): Promise<Contract>;
|
|
13
23
|
}
|
|
@@ -17,6 +17,7 @@ import type { CliAdapter } from "./adapter.interface";
|
|
|
17
17
|
*/
|
|
18
18
|
export declare class CacAdapter implements CliAdapter {
|
|
19
19
|
readonly id = "cac";
|
|
20
|
+
readonly limitations: readonly string[];
|
|
20
21
|
extract(entryPath: string): Promise<Contract>;
|
|
21
22
|
private loadCac;
|
|
22
23
|
/** Among every CAC instance captured during construction, the one that looks most like the real, fully-built root CLI. */
|
|
@@ -21,6 +21,11 @@ const load_module_1 = require("./load-module");
|
|
|
21
21
|
class CacAdapter {
|
|
22
22
|
constructor() {
|
|
23
23
|
this.id = "cac";
|
|
24
|
+
this.limitations = [
|
|
25
|
+
'OptionContract.required is always false - CAC has no declarative "this option must be passed" concept.',
|
|
26
|
+
"CommandContract.subcommands is always [] - CAC's commands are a flat list, not a tree.",
|
|
27
|
+
'ArgumentContract.description is always "" - CAC\'s positional args carry no description field.',
|
|
28
|
+
];
|
|
24
29
|
}
|
|
25
30
|
async extract(entryPath) {
|
|
26
31
|
const cli = await this.loadCac(entryPath);
|
|
@@ -9,6 +9,8 @@ import type { CliAdapter } from "./adapter.interface";
|
|
|
9
9
|
*/
|
|
10
10
|
export declare class CommanderAdapter implements CliAdapter {
|
|
11
11
|
readonly id = "commander";
|
|
12
|
+
/** No known gaps - Commander's own object graph exposes everything a Contract needs directly. */
|
|
13
|
+
readonly limitations: readonly string[];
|
|
12
14
|
extract(entryPath: string): Promise<Contract>;
|
|
13
15
|
private loadCommand;
|
|
14
16
|
/** Among every Command captured during construction, the one that looks most like the real, fully-built root program - flags on this file's own top-level code sometimes constructing more than one incidentally. */
|
|
@@ -13,6 +13,8 @@ const load_module_1 = require("./load-module");
|
|
|
13
13
|
class CommanderAdapter {
|
|
14
14
|
constructor() {
|
|
15
15
|
this.id = "commander";
|
|
16
|
+
/** No known gaps - Commander's own object graph exposes everything a Contract needs directly. */
|
|
17
|
+
this.limitations = [];
|
|
16
18
|
}
|
|
17
19
|
async extract(entryPath) {
|
|
18
20
|
const program = await this.loadCommand(entryPath);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.adapters = void 0;
|
|
4
|
+
exports.resolveAdapter = resolveAdapter;
|
|
5
|
+
const cac_adapter_1 = require("./cac.adapter");
|
|
6
|
+
const commander_adapter_1 = require("./commander.adapter");
|
|
7
|
+
const yargs_adapter_1 = require("./yargs.adapter");
|
|
8
|
+
// Constructing an adapter here is cheap (no eager require of its
|
|
9
|
+
// framework - CacAdapter/YargsAdapter only load their framework lazily,
|
|
10
|
+
// inside extract()), so every adapter is always registered regardless of
|
|
11
|
+
// which one a given caller actually uses. Shared by bin.ts (the CLI) and
|
|
12
|
+
// index.ts (the programmatic API) so both agree on exactly the same set
|
|
13
|
+
// of adapters under exactly the same names, rather than two registries
|
|
14
|
+
// that could silently drift apart.
|
|
15
|
+
exports.adapters = {
|
|
16
|
+
commander: new commander_adapter_1.CommanderAdapter(),
|
|
17
|
+
cac: new cac_adapter_1.CacAdapter(),
|
|
18
|
+
yargs: new yargs_adapter_1.YargsAdapter(),
|
|
19
|
+
};
|
|
20
|
+
function resolveAdapter(name) {
|
|
21
|
+
const adapter = exports.adapters[name];
|
|
22
|
+
if (!adapter) {
|
|
23
|
+
throw new Error(`cliguard: unknown adapter "${name}". Available: ${Object.keys(exports.adapters).join(", ")}.`);
|
|
24
|
+
}
|
|
25
|
+
return adapter;
|
|
26
|
+
}
|
|
@@ -19,6 +19,7 @@ import type { CliAdapter } from "./adapter.interface";
|
|
|
19
19
|
*/
|
|
20
20
|
export declare class YargsAdapter implements CliAdapter {
|
|
21
21
|
readonly id = "yargs";
|
|
22
|
+
readonly limitations: readonly string[];
|
|
22
23
|
extract(entryPath: string): Promise<Contract>;
|
|
23
24
|
private loadYargs;
|
|
24
25
|
/**
|
|
@@ -24,6 +24,9 @@ const YARGS_STRING_MARKER = "__yargsString__:";
|
|
|
24
24
|
class YargsAdapter {
|
|
25
25
|
constructor() {
|
|
26
26
|
this.id = "yargs";
|
|
27
|
+
this.limitations = [
|
|
28
|
+
"Each command's options are read from a fresh, isolated yargs instance built by re-running that command's builder, not the shared instance the target CLI actually built - see this class's own doc comment for why.",
|
|
29
|
+
];
|
|
27
30
|
}
|
|
28
31
|
async extract(entryPath) {
|
|
29
32
|
const cli = await this.loadYargs(entryPath);
|