cliguard 0.2.0 → 0.3.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 +26 -0
- package/dist/adapters/cac.adapter.d.ts +17 -0
- package/dist/adapters/cac.adapter.js +31 -24
- package/dist/adapters/commander.adapter.d.ts +15 -0
- package/dist/adapters/commander.adapter.js +27 -4
- package/dist/bin.js +25 -5
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -71,6 +71,25 @@ npx cliguard update ./bin/cli.js
|
|
|
71
71
|
|
|
72
72
|
`cliguard check` exits `1` if it finds even one `BREAKING` change, and `0` otherwise - safe to drop straight into any CI pipeline.
|
|
73
73
|
|
|
74
|
+
Pass `--json` to `check` for a machine-readable result instead of the emoji lines above - useful for a bot that comments on the PR, a dashboard, or any other script consuming the result instead of a human reading it:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npx cliguard check ./bin/cli.js --json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"ok": false,
|
|
83
|
+
"changes": [
|
|
84
|
+
{ "type": "BREAKING", "path": "root -> build -> option[--target]", "message": "Option \"--target\" was removed." }
|
|
85
|
+
],
|
|
86
|
+
"summary": { "breaking": 1, "additive": 0, "patch": 0 },
|
|
87
|
+
"suggestedBump": "major"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`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.
|
|
92
|
+
|
|
74
93
|
## How changes get classified
|
|
75
94
|
|
|
76
95
|
| | Removed | Added | Required flipped | Value type / default changed |
|
|
@@ -115,6 +134,13 @@ Extracting a contract runs the target entry file's own top-level code, the same
|
|
|
115
134
|
|
|
116
135
|
The CLI and core diffing engine are, and will stay, free and open-source. Planned next: a hosted add-on for teams that want more than a CI exit code - a dashboard with the history of contract changes across releases, and Slack/webhook alerts the moment a breaking change lands. See [issue: Webhook reporter for SaaS integration](https://github.com/Bryandero98/cliguard/issues) for the first building block.
|
|
117
136
|
|
|
137
|
+
## Support this project
|
|
138
|
+
|
|
139
|
+
cliguard is free and will stay free. If it's saving you from a broken release, a small tip helps keep it going:
|
|
140
|
+
|
|
141
|
+
- **Ko-fi:** [ko-fi.com/bryandero98](https://ko-fi.com/bryandero98)
|
|
142
|
+
- **USDT (TRC20):** `TEG4Kk2qXYMQ4mHNd7dPhSPRyT14CGr2or` — double-check the network is set to **TRC20** before sending; a transfer on the wrong network can't be recovered.
|
|
143
|
+
|
|
118
144
|
## Contributing
|
|
119
145
|
|
|
120
146
|
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
@@ -21,6 +21,23 @@ export declare class CacAdapter implements CliAdapter {
|
|
|
21
21
|
private loadCac;
|
|
22
22
|
/** Handles `export default`, `module.exports = cli`, and named exports. */
|
|
23
23
|
private findCac;
|
|
24
|
+
/**
|
|
25
|
+
* Structural check, not `instanceof CAC` - see CommanderAdapter's
|
|
26
|
+
* identical-purpose `looksLikeCommand` for why: the target project's
|
|
27
|
+
* own `cac` install is almost always a separate copy from any `cac`
|
|
28
|
+
* cliguard itself could resolve, even at the identical version, so
|
|
29
|
+
* `instanceof` fails by construction. This also removes the only
|
|
30
|
+
* reason this adapter ever needed `cac` installed in cliguard's own
|
|
31
|
+
* environment - `require("cac")` from cliguard's own (often
|
|
32
|
+
* `npx`-isolated) location previously gated every use of this adapter
|
|
33
|
+
* behind a package cliguard could rarely actually see, even when the
|
|
34
|
+
* target project had it. The target file's own `require("cac")` /
|
|
35
|
+
* `import("cac")`, resolved from *its* location by `loadModule`, is
|
|
36
|
+
* the only place `cac` needs to be installed now - and if it isn't,
|
|
37
|
+
* that failure surfaces below via the real load error, same as any
|
|
38
|
+
* other missing dependency.
|
|
39
|
+
*/
|
|
40
|
+
private looksLikeCac;
|
|
24
41
|
/**
|
|
25
42
|
* CAC's root instance carries global options (`cli.option(...)`,
|
|
26
43
|
* exposed via `globalCommand`) but no description of its own and no
|
|
@@ -2,23 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CacAdapter = void 0;
|
|
4
4
|
const load_module_1 = require("./load-module");
|
|
5
|
-
/**
|
|
6
|
-
* `cac` is an optional peer dependency (see package.json) - a project
|
|
7
|
-
* whose target CLI is built with Commander has no reason to install it.
|
|
8
|
-
* Loaded lazily, only when this adapter actually runs, so importing this
|
|
9
|
-
* file (which `bin.ts` does unconditionally, to register every adapter)
|
|
10
|
-
* never requires `cac` to be present.
|
|
11
|
-
*/
|
|
12
|
-
function loadCacClass() {
|
|
13
|
-
try {
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports -- deliberate lazy load of an optional peer dependency
|
|
15
|
-
const cacModule = require("cac");
|
|
16
|
-
return cacModule.CAC;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
throw new Error("cliguard: the CAC adapter needs the `cac` package. Run `npm install --save-dev cac`.");
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
5
|
/**
|
|
23
6
|
* Extracts a Contract from a target file that exports a `cac()` `CAC`
|
|
24
7
|
* instance. Never parses --help output - every field comes straight from
|
|
@@ -48,10 +31,8 @@ class CacAdapter {
|
|
|
48
31
|
};
|
|
49
32
|
}
|
|
50
33
|
async loadCac(entryPath) {
|
|
51
|
-
const CacClass = loadCacClass();
|
|
52
34
|
const { viaImport, viaRequire } = await (0, load_module_1.loadModule)(entryPath);
|
|
53
|
-
const cli = this.findCac(viaImport.moduleExports
|
|
54
|
-
this.findCac(viaRequire.moduleExports, CacClass);
|
|
35
|
+
const cli = this.findCac(viaImport.moduleExports) ?? this.findCac(viaRequire.moduleExports);
|
|
55
36
|
if (cli)
|
|
56
37
|
return cli;
|
|
57
38
|
// See CommanderAdapter's identical block for why both real errors -
|
|
@@ -63,22 +44,48 @@ class CacAdapter {
|
|
|
63
44
|
` require() failed: ${viaRequire.error ?? "module loaded, but exported no CAC instance"}`);
|
|
64
45
|
}
|
|
65
46
|
/** Handles `export default`, `module.exports = cli`, and named exports. */
|
|
66
|
-
findCac(moduleExports
|
|
67
|
-
if (moduleExports
|
|
47
|
+
findCac(moduleExports) {
|
|
48
|
+
if (this.looksLikeCac(moduleExports)) {
|
|
68
49
|
return moduleExports;
|
|
69
50
|
}
|
|
70
51
|
if (moduleExports && typeof moduleExports === "object") {
|
|
71
52
|
const exportsObject = moduleExports;
|
|
72
|
-
if (exportsObject.default
|
|
53
|
+
if (this.looksLikeCac(exportsObject.default)) {
|
|
73
54
|
return exportsObject.default;
|
|
74
55
|
}
|
|
75
56
|
for (const value of Object.values(exportsObject)) {
|
|
76
|
-
if (value
|
|
57
|
+
if (this.looksLikeCac(value))
|
|
77
58
|
return value;
|
|
78
59
|
}
|
|
79
60
|
}
|
|
80
61
|
return undefined;
|
|
81
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Structural check, not `instanceof CAC` - see CommanderAdapter's
|
|
65
|
+
* identical-purpose `looksLikeCommand` for why: the target project's
|
|
66
|
+
* own `cac` install is almost always a separate copy from any `cac`
|
|
67
|
+
* cliguard itself could resolve, even at the identical version, so
|
|
68
|
+
* `instanceof` fails by construction. This also removes the only
|
|
69
|
+
* reason this adapter ever needed `cac` installed in cliguard's own
|
|
70
|
+
* environment - `require("cac")` from cliguard's own (often
|
|
71
|
+
* `npx`-isolated) location previously gated every use of this adapter
|
|
72
|
+
* behind a package cliguard could rarely actually see, even when the
|
|
73
|
+
* target project had it. The target file's own `require("cac")` /
|
|
74
|
+
* `import("cac")`, resolved from *its* location by `loadModule`, is
|
|
75
|
+
* the only place `cac` needs to be installed now - and if it isn't,
|
|
76
|
+
* that failure surfaces below via the real load error, same as any
|
|
77
|
+
* other missing dependency.
|
|
78
|
+
*/
|
|
79
|
+
looksLikeCac(value) {
|
|
80
|
+
if (!value || typeof value !== "object")
|
|
81
|
+
return false;
|
|
82
|
+
const candidate = value;
|
|
83
|
+
return (Array.isArray(candidate.commands) &&
|
|
84
|
+
typeof candidate.globalCommand === "object" &&
|
|
85
|
+
candidate.globalCommand !== null &&
|
|
86
|
+
typeof candidate.command === "function" &&
|
|
87
|
+
typeof candidate.parse === "function");
|
|
88
|
+
}
|
|
82
89
|
/**
|
|
83
90
|
* CAC's root instance carries global options (`cli.option(...)`,
|
|
84
91
|
* exposed via `globalCommand`) but no description of its own and no
|
|
@@ -13,6 +13,21 @@ export declare class CommanderAdapter implements CliAdapter {
|
|
|
13
13
|
private loadCommand;
|
|
14
14
|
/** Handles `export default`, `module.exports = program`, and named exports. */
|
|
15
15
|
private findCommand;
|
|
16
|
+
/**
|
|
17
|
+
* Structural check, not `instanceof Command`. The target CLI almost
|
|
18
|
+
* always has its own separate install of `commander` - a different
|
|
19
|
+
* copy than the one this adapter imports, even at the identical
|
|
20
|
+
* version - because `npx cliguard` installs cliguard (and its pinned
|
|
21
|
+
* `commander`) into its own isolated location, unrelated to the target
|
|
22
|
+
* project's `node_modules`. Node gives every resolved copy of a
|
|
23
|
+
* package its own class identity ("dual package hazard"), so
|
|
24
|
+
* `instanceof` fails by construction in that - extremely common - case.
|
|
25
|
+
* Verified against a real external consumer project via `npx cliguard`
|
|
26
|
+
* with its own separate `commander` install, both at a different major
|
|
27
|
+
* version and at the identical version to this package's own
|
|
28
|
+
* `^12.1.0` - `instanceof` failed in both; this doesn't.
|
|
29
|
+
*/
|
|
30
|
+
private looksLikeCommand;
|
|
16
31
|
/** Recurses into `command.commands` so root and every subcommand at any depth go through the same mapping. */
|
|
17
32
|
private mapCommand;
|
|
18
33
|
private mapOption;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CommanderAdapter = void 0;
|
|
4
|
-
const commander_1 = require("commander");
|
|
5
4
|
const load_module_1 = require("./load-module");
|
|
6
5
|
/**
|
|
7
6
|
* Extracts a Contract from a target file that exports a Commander.js
|
|
@@ -45,21 +44,45 @@ class CommanderAdapter {
|
|
|
45
44
|
}
|
|
46
45
|
/** Handles `export default`, `module.exports = program`, and named exports. */
|
|
47
46
|
findCommand(moduleExports) {
|
|
48
|
-
if (moduleExports
|
|
47
|
+
if (this.looksLikeCommand(moduleExports)) {
|
|
49
48
|
return moduleExports;
|
|
50
49
|
}
|
|
51
50
|
if (moduleExports && typeof moduleExports === "object") {
|
|
52
51
|
const exportsObject = moduleExports;
|
|
53
|
-
if (exportsObject.default
|
|
52
|
+
if (this.looksLikeCommand(exportsObject.default)) {
|
|
54
53
|
return exportsObject.default;
|
|
55
54
|
}
|
|
56
55
|
for (const value of Object.values(exportsObject)) {
|
|
57
|
-
if (value
|
|
56
|
+
if (this.looksLikeCommand(value))
|
|
58
57
|
return value;
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
return undefined;
|
|
62
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Structural check, not `instanceof Command`. The target CLI almost
|
|
64
|
+
* always has its own separate install of `commander` - a different
|
|
65
|
+
* copy than the one this adapter imports, even at the identical
|
|
66
|
+
* version - because `npx cliguard` installs cliguard (and its pinned
|
|
67
|
+
* `commander`) into its own isolated location, unrelated to the target
|
|
68
|
+
* project's `node_modules`. Node gives every resolved copy of a
|
|
69
|
+
* package its own class identity ("dual package hazard"), so
|
|
70
|
+
* `instanceof` fails by construction in that - extremely common - case.
|
|
71
|
+
* Verified against a real external consumer project via `npx cliguard`
|
|
72
|
+
* with its own separate `commander` install, both at a different major
|
|
73
|
+
* version and at the identical version to this package's own
|
|
74
|
+
* `^12.1.0` - `instanceof` failed in both; this doesn't.
|
|
75
|
+
*/
|
|
76
|
+
looksLikeCommand(value) {
|
|
77
|
+
if (!value || typeof value !== "object")
|
|
78
|
+
return false;
|
|
79
|
+
const candidate = value;
|
|
80
|
+
return (Array.isArray(candidate.options) &&
|
|
81
|
+
Array.isArray(candidate.commands) &&
|
|
82
|
+
typeof candidate.name === "function" &&
|
|
83
|
+
typeof candidate.action === "function" &&
|
|
84
|
+
typeof candidate.opts === "function");
|
|
85
|
+
}
|
|
63
86
|
/** Recurses into `command.commands` so root and every subcommand at any depth go through the same mapping. */
|
|
64
87
|
mapCommand(command) {
|
|
65
88
|
return {
|
package/dist/bin.js
CHANGED
|
@@ -42,28 +42,33 @@ program
|
|
|
42
42
|
.option(...adapterOption)
|
|
43
43
|
.action(async (entry, options) => {
|
|
44
44
|
if ((0, storage_1.contractExists)()) {
|
|
45
|
-
console.warn(`
|
|
45
|
+
console.warn(`A contract already exists. Run "cliguard update" to overwrite it.`);
|
|
46
46
|
process.exit(1);
|
|
47
47
|
}
|
|
48
48
|
const contract = await resolveAdapter(options.adapter).extract(entry);
|
|
49
49
|
(0, storage_1.writeContract)(contract);
|
|
50
|
-
console.log(`✅
|
|
50
|
+
console.log(`✅ CLI contract initialized successfully at ${(0, storage_1.getContractDisplayPath)()}.`);
|
|
51
51
|
});
|
|
52
52
|
program
|
|
53
53
|
.command("check")
|
|
54
54
|
.description("Compare the current CLI surface against the committed contract")
|
|
55
55
|
.argument("<entry>", "path to the target CLI's entry file")
|
|
56
56
|
.option(...adapterOption)
|
|
57
|
+
.option("--json", "print a machine-readable JSON result instead of text", false)
|
|
57
58
|
.action(async (entry, options) => {
|
|
58
59
|
const oldContract = (0, storage_1.readContract)();
|
|
59
60
|
const newContract = await resolveAdapter(options.adapter).extract(entry);
|
|
60
61
|
const diff = diffEngine.compare(oldContract, newContract);
|
|
62
|
+
const hasBreaking = diff.some((entry) => entry.type === types_1.ChangeType.BREAKING);
|
|
63
|
+
if (options.json) {
|
|
64
|
+
console.log(JSON.stringify(toJsonResult(diff), null, 2));
|
|
65
|
+
process.exit(hasBreaking ? 1 : 0);
|
|
66
|
+
}
|
|
61
67
|
if (diff.length === 0) {
|
|
62
|
-
console.log("✅
|
|
68
|
+
console.log("✅ CLI contract is intact.");
|
|
63
69
|
process.exit(0);
|
|
64
70
|
}
|
|
65
71
|
printDiff(diff);
|
|
66
|
-
const hasBreaking = diff.some((entry) => entry.type === types_1.ChangeType.BREAKING);
|
|
67
72
|
process.exit(hasBreaking ? 1 : 0);
|
|
68
73
|
});
|
|
69
74
|
program
|
|
@@ -74,8 +79,23 @@ program
|
|
|
74
79
|
.action(async (entry, options) => {
|
|
75
80
|
const contract = await resolveAdapter(options.adapter).extract(entry);
|
|
76
81
|
(0, storage_1.writeContract)(contract);
|
|
77
|
-
console.log("🔄
|
|
82
|
+
console.log("🔄 CLI contract updated successfully.");
|
|
78
83
|
});
|
|
84
|
+
function toJsonResult(diff) {
|
|
85
|
+
const summary = {
|
|
86
|
+
breaking: diff.filter((entry) => entry.type === types_1.ChangeType.BREAKING).length,
|
|
87
|
+
additive: diff.filter((entry) => entry.type === types_1.ChangeType.ADDITIVE).length,
|
|
88
|
+
patch: diff.filter((entry) => entry.type === types_1.ChangeType.PATCH).length,
|
|
89
|
+
};
|
|
90
|
+
const suggestedBump = summary.breaking > 0
|
|
91
|
+
? "major"
|
|
92
|
+
: summary.additive > 0
|
|
93
|
+
? "minor"
|
|
94
|
+
: summary.patch > 0
|
|
95
|
+
? "patch"
|
|
96
|
+
: null;
|
|
97
|
+
return { ok: summary.breaking === 0, changes: diff, summary, suggestedBump };
|
|
98
|
+
}
|
|
79
99
|
function printDiff(diff) {
|
|
80
100
|
for (const entry of diff) {
|
|
81
101
|
console.log(`${emojiFor(entry.type)} [${entry.path}] ${entry.message}`);
|
package/package.json
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cliguard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Snapshot-tests your CLI's contract (commands, flags, defaults) so you never ship a breaking change by accident.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"contract-testing",
|
|
8
|
+
"snapshot-testing",
|
|
9
|
+
"commander",
|
|
10
|
+
"ci"
|
|
11
|
+
],
|
|
6
12
|
"author": "Bryandero98",
|
|
7
13
|
"license": "MIT",
|
|
8
14
|
"homepage": "https://github.com/Bryandero98/cliguard",
|
|
@@ -13,10 +19,13 @@
|
|
|
13
19
|
"bugs": {
|
|
14
20
|
"url": "https://github.com/Bryandero98/cliguard/issues"
|
|
15
21
|
},
|
|
22
|
+
"funding": "https://ko-fi.com/bryandero98",
|
|
16
23
|
"bin": {
|
|
17
24
|
"cliguard": "dist/bin.js"
|
|
18
25
|
},
|
|
19
|
-
"files": [
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
20
29
|
"scripts": {
|
|
21
30
|
"build": "tsc",
|
|
22
31
|
"lint": "eslint src --ext .ts",
|