cliguard 0.3.1 → 0.5.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/README.md CHANGED
@@ -23,7 +23,7 @@ The first line fails your CI. The second one doesn't - `--dry-run` is new and op
23
23
  npm install --save-dev cliguard
24
24
  ```
25
25
 
26
- Your CLI's entry file needs to **export** its Commander `Command` instance instead of calling `.parse()` itself:
26
+ Your CLI's entry file should **export** its Commander `Command` instance instead of calling `.parse()` itself - the cleanest way to adopt cliguard, since it never risks running any of your CLI's real logic:
27
27
 
28
28
  ```js
29
29
  // bin/cli.js
@@ -56,6 +56,36 @@ npx cliguard init ./bin/cli.js --adapter cac
56
56
 
57
57
  `cac` itself is an optional dependency of cliguard - only installed if you actually use `--adapter cac`.
58
58
 
59
+ Built with [Yargs](https://github.com/yargs/yargs) instead? Export the instance the same way and pass `--adapter yargs`:
60
+
61
+ ```js
62
+ // bin/cli.js
63
+ const yargs = require("yargs/yargs");
64
+
65
+ const cli = yargs([])
66
+ .command("build <entry>", "build the project", (y) =>
67
+ y.option("target", { alias: "t", describe: "build target", type: "string" }).demandOption("target"),
68
+ );
69
+
70
+ module.exports = { cli };
71
+ ```
72
+
73
+ ```sh
74
+ npx cliguard init ./bin/cli.js --adapter yargs
75
+ ```
76
+
77
+ Like `cac`, `yargs` itself is an optional dependency of cliguard - only installed if you actually use `--adapter yargs`.
78
+
79
+ ### Entry files that build the CLI lazily
80
+
81
+ Not every real CLI exports its instance - plenty build it inside a function that only runs when something actually calls it, or just never had a reason to export it. Pointing cliguard straight at a file like that would fail with "no instance found" under the rule above alone.
82
+
83
+ So when the direct export lookup finds nothing, cliguard tries one more thing automatically, no flag needed: it patches the exact copy of `commander`/`cac`/`yargs` your entry file will itself `require()`, so any `new Command()` (or CAC's `cac()`, or a `yargs(...)` call) anywhere in your file's own top-level code is captured - even though nothing was ever exported. This covers most real CLIs, since even ones that never bother exporting still build (and often `.parse()`) at the top of their own file as a matter of course.
84
+
85
+ It can't reach an instance built strictly *inside* a function that's only invoked later, never automatically at load time (a `main()` some other file calls, not the file cliguard is pointed at) - there's no safe, generic way for cliguard to know which function to call or with what arguments. For that shape, write a small wrapper file that reaches into the target's own internals to get (or construct) the instance, and point cliguard at the wrapper instead of the original entry file. The exact shape of that wrapper is inherently project-specific - it's standing in for whatever that project's own entry point would otherwise do - but the command stays the same either way: `cliguard init ./your-wrapper.mjs`.
86
+
87
+ Either way, if the target's top-level code has its own real side effects when loaded - a `.parse()` call that matches a real command and runs it, a network request, spawning a process - running cliguard against it (directly or through a wrapper) triggers those too, exactly as `node ./bin/cli.js` would. cliguard neutralizes one specific danger this creates (a target calling `process.exit()` can't kill cliguard's own process or override its exit code), but doesn't sandbox anything else - see [SECURITY.md](./SECURITY.md).
88
+
59
89
  Then:
60
90
 
61
91
  ```sh
@@ -120,9 +150,9 @@ jobs:
120
150
 
121
151
  ## Supported frameworks
122
152
 
123
- [Commander.js](https://github.com/tj/commander.js) (default) and [CAC](https://github.com/cacjs/cac) (`--adapter cac`) today. The core (types + diff engine) is 100% framework-agnostic by design: every framework-specific detail lives behind the `CliAdapter` interface in [`src/adapters/`](src/adapters/), so adding a new adapter never touches the diffing logic. Yargs is the next open gap - see the [good first issue](https://github.com/Bryandero98/cliguard/labels/good%20first%20issue).
153
+ [Commander.js](https://github.com/tj/commander.js) (default), [CAC](https://github.com/cacjs/cac) (`--adapter cac`), and [Yargs](https://github.com/yargs/yargs) (`--adapter yargs`) today. The core (types + diff engine) is 100% framework-agnostic by design: every framework-specific detail lives behind the `CliAdapter` interface in [`src/adapters/`](src/adapters/), so adding a new adapter never touches the diffing logic. Click, Clap, and Cobra are the next open gaps - see the [good first issue](https://github.com/Bryandero98/cliguard/labels/good%20first%20issue).
124
154
 
125
- A couple of `OptionContract`/`ArgumentContract` fields carry real, framework-specific limitations rather than a mapping gap - see [`src/adapters/cac.adapter.ts`](src/adapters/cac.adapter.ts)'s own doc comment for exactly which ones and why (CAC has no declarative "this flag must be passed" concept, and no per-argument description).
155
+ A couple of `OptionContract`/`ArgumentContract` fields carry real, framework-specific limitations rather than a mapping gap - see [`src/adapters/cac.adapter.ts`](src/adapters/cac.adapter.ts)'s own doc comment for exactly which ones and why (CAC has no declarative "this flag must be passed" concept, and no per-argument description). Yargs's own real limitation is the opposite kind - see [`src/adapters/yargs.adapter.ts`](src/adapters/yargs.adapter.ts)'s doc comment for why each command's options are read from a fresh, isolated instance rather than the shared one the target CLI actually built.
126
156
 
127
157
  The current adapter mechanism loads the target CLI's entry file into the Node process (`import()`/`require()`) and reads its object graph directly, so the next targets are other Node frameworks. Cross-language support (Python's Click, Rust's Clap, Go's Cobra) is a real future direction, but needs a different extraction strategy first, since a compiled Clap/Cobra binary can't be `require()`'d into Node the way a JS CLI can - most likely each of those would introspect via a structured `--help` output (some frameworks support a JSON mode) rather than the same in-process approach.
128
158
 
@@ -19,6 +19,8 @@ export declare class CacAdapter implements CliAdapter {
19
19
  readonly id = "cac";
20
20
  extract(entryPath: string): Promise<Contract>;
21
21
  private loadCac;
22
+ /** Among every CAC instance captured during construction, the one that looks most like the real, fully-built root CLI. */
23
+ private pickBestCandidate;
22
24
  /** Handles `export default`, `module.exports = cli`, and named exports. */
23
25
  private findCac;
24
26
  /**
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CacAdapter = void 0;
4
+ const construction_capture_1 = require("./construction-capture");
4
5
  const load_module_1 = require("./load-module");
5
6
  /**
6
7
  * Extracts a Contract from a target file that exports a `cac()` `CAC`
@@ -31,18 +32,39 @@ class CacAdapter {
31
32
  };
32
33
  }
33
34
  async loadCac(entryPath) {
35
+ // See CommanderAdapter.loadCommand's identical block: patched before
36
+ // the target loads, so a `new CAC()` or `cac()` call anywhere in its
37
+ // own top-level code gets captured even if the target never exports
38
+ // the result.
39
+ const captured = (0, construction_capture_1.captureConstructions)("cac", entryPath, "CAC", ["cac"]);
34
40
  const { viaImport, viaRequire } = await (0, load_module_1.loadModule)(entryPath);
35
- const cli = this.findCac(viaImport.moduleExports) ?? this.findCac(viaRequire.moduleExports);
41
+ const cli = this.findCac(viaImport.moduleExports) ??
42
+ this.findCac(viaRequire.moduleExports) ??
43
+ this.pickBestCandidate(captured);
36
44
  if (cli)
37
45
  return cli;
38
46
  // See CommanderAdapter's identical block for why both real errors -
39
47
  // not a swallowed, generic guess - matter here.
40
48
  throw new Error(`cliguard: no CAC instance found in "${entryPath}". ` +
41
49
  "Export it as `export default cli`, `module.exports = cli`, " +
42
- "or a named export (e.g. `export const cli = cac()`).\n" +
50
+ "or a named export (e.g. `export const cli = cac()`). If the file builds its " +
51
+ "CAC instance inside a function that only runs when something calls it (never " +
52
+ "at the top level), point cliguard at a small wrapper file that calls that " +
53
+ "function and exports the result instead - see the README's \"Entry files that " +
54
+ 'build the CLI lazily" section.\n' +
43
55
  ` import() failed: ${viaImport.error ?? "module loaded, but exported no CAC instance"}\n` +
44
56
  ` require() failed: ${viaRequire.error ?? "module loaded, but exported no CAC instance"}`);
45
57
  }
58
+ /** Among every CAC instance captured during construction, the one that looks most like the real, fully-built root CLI. */
59
+ pickBestCandidate(candidates) {
60
+ const valid = candidates.filter((candidate) => this.looksLikeCac(candidate));
61
+ if (valid.length === 0)
62
+ return undefined;
63
+ return valid.reduce((best, candidate) => candidate.commands.length + candidate.globalCommand.options.length >
64
+ best.commands.length + best.globalCommand.options.length
65
+ ? candidate
66
+ : best);
67
+ }
46
68
  /** Handles `export default`, `module.exports = cli`, and named exports. */
47
69
  findCac(moduleExports) {
48
70
  if (this.looksLikeCac(moduleExports)) {
@@ -11,6 +11,8 @@ export declare class CommanderAdapter implements CliAdapter {
11
11
  readonly id = "commander";
12
12
  extract(entryPath: string): Promise<Contract>;
13
13
  private loadCommand;
14
+ /** 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. */
15
+ private pickBestCandidate;
14
16
  /** Handles `export default`, `module.exports = program`, and named exports. */
15
17
  private findCommand;
16
18
  /**
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CommanderAdapter = void 0;
4
+ const construction_capture_1 = require("./construction-capture");
4
5
  const load_module_1 = require("./load-module");
5
6
  /**
6
7
  * Extracts a Contract from a target file that exports a Commander.js
@@ -23,25 +24,49 @@ class CommanderAdapter {
23
24
  };
24
25
  }
25
26
  async loadCommand(entryPath) {
27
+ // Patched *before* the target loads, so a `new Command()` or
28
+ // `createCommand()` call anywhere in its own top-level code gets
29
+ // captured as a side effect of loadModule() below - even if the
30
+ // target never exports the result anywhere. See
31
+ // construction-capture.ts for why this is safe and what it can't
32
+ // reach.
33
+ const captured = (0, construction_capture_1.captureConstructions)("commander", entryPath, "Command", ["createCommand"]);
26
34
  const { viaImport, viaRequire } = await (0, load_module_1.loadModule)(entryPath);
27
- const command = this.findCommand(viaImport.moduleExports) ?? this.findCommand(viaRequire.moduleExports);
35
+ const command = this.findCommand(viaImport.moduleExports) ??
36
+ this.findCommand(viaRequire.moduleExports) ??
37
+ this.pickBestCandidate(captured);
28
38
  if (command)
29
39
  return command;
30
- // Neither attempt's exports contained a Command. One load attempt
31
- // failing on its own is normal and expected (an ESM-only file can't
32
- // require(), a CJS one may reject a bare import() on an older Node)
33
- // - the interesting case is when the file simply never loaded at all
34
- // (a syntax error, a missing dependency inside it), which the
35
- // generic "no Command instance found" message below would otherwise
40
+ // Neither attempt's exports contained a Command, and nothing was
41
+ // captured during construction either. One load attempt failing on
42
+ // its own is normal and expected (an ESM-only file can't require(),
43
+ // a CJS one may reject a bare import() on an older Node) - the
44
+ // interesting case is when the file simply never loaded at all (a
45
+ // syntax error, a missing dependency inside it), which the generic
46
+ // "no Command instance found" message below would otherwise
36
47
  // misrepresent as "loaded fine, wrong export shape." Surface both
37
48
  // real reasons so the actual cause - a broken file vs. a genuinely
38
49
  // missing export - is never a guess.
39
50
  throw new Error(`cliguard: no Commander.js Command instance found in "${entryPath}". ` +
40
51
  "Export it as `export default program`, `module.exports = program`, " +
41
- "or a named export (e.g. `export const program = new Command()`).\n" +
52
+ "or a named export (e.g. `export const program = new Command()`). If the file " +
53
+ "builds its Command inside a function that only runs when something calls it " +
54
+ "(never at the top level), point cliguard at a small wrapper file that calls " +
55
+ "that function and exports the result instead - see the README's " +
56
+ '"Entry files that build the CLI lazily" section.\n' +
42
57
  ` import() failed: ${viaImport.error ?? "module loaded, but exported no Command instance"}\n` +
43
58
  ` require() failed: ${viaRequire.error ?? "module loaded, but exported no Command instance"}`);
44
59
  }
60
+ /** 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. */
61
+ pickBestCandidate(candidates) {
62
+ const valid = candidates.filter((candidate) => this.looksLikeCommand(candidate));
63
+ if (valid.length === 0)
64
+ return undefined;
65
+ return valid.reduce((best, candidate) => candidate.options.length + candidate.commands.length >
66
+ best.options.length + best.commands.length
67
+ ? candidate
68
+ : best);
69
+ }
45
70
  /** Handles `export default`, `module.exports = program`, and named exports. */
46
71
  findCommand(moduleExports) {
47
72
  if (this.looksLikeCommand(moduleExports)) {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Captures whatever instance a target file's own top-level code
3
+ * constructs from `packageName` - even when the target never exports
4
+ * that instance anywhere `module.exports` reaches. Many real CLIs build
5
+ * their Commander/CAC instance inside a function that only runs when
6
+ * something actually calls it (or just never bother exporting it, since
7
+ * they have no reason to) - `findCommand`/`findCac` alone only ever
8
+ * catch the minority that do.
9
+ *
10
+ * How: Node caches a CommonJS module by its resolved absolute path.
11
+ * Resolving `packageName` from the *target file's own directory* (not
12
+ * this package's - a different copy, per commander.adapter.ts's
13
+ * `looksLikeCommand` doc) and mutating the exact object at that cached
14
+ * path means the target's own later `require(packageName)` - resolving
15
+ * to the same path - returns this already-patched object. There's no
16
+ * way for the target to tell the difference: `new Command()` still
17
+ * returns a real `Command` (the patch is a construct-trapping `Proxy`
18
+ * around the real class, transparent to `instanceof` and to every
19
+ * static/instance member), it's just also recorded here as a side
20
+ * effect.
21
+ *
22
+ * Only reaches CommonJS construction - a direct `new ClassExport()`, or
23
+ * a named factory function that closes over the real class and returns
24
+ * `new ClassExport()` internally (commander's `createCommand`, cac's
25
+ * `cac()` - patching only the class export wouldn't catch these, since
26
+ * the factory's own closure still points at the *original* class, not
27
+ * whatever we later put in `moduleExports[classExportName]`). A target
28
+ * that's genuine ESM, reaching the framework via a static `import`
29
+ * rather than `require`, isn't reachable this way - Node's ESM module
30
+ * cache is separate and not patchable from CommonJS. That's a real,
31
+ * documented limit, not a bug: commander and cac both ship CJS-only, so
32
+ * even an ESM target almost always reaches them through interop's own
33
+ * require() underneath - the case this can't reach is the rare one.
34
+ */
35
+ export declare function captureConstructions(packageName: string, entryPath: string, classExportName: string, factoryExportNames: readonly string[]): unknown[];
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.captureConstructions = captureConstructions;
4
+ const path_1 = require("path");
5
+ /**
6
+ * Captures whatever instance a target file's own top-level code
7
+ * constructs from `packageName` - even when the target never exports
8
+ * that instance anywhere `module.exports` reaches. Many real CLIs build
9
+ * their Commander/CAC instance inside a function that only runs when
10
+ * something actually calls it (or just never bother exporting it, since
11
+ * they have no reason to) - `findCommand`/`findCac` alone only ever
12
+ * catch the minority that do.
13
+ *
14
+ * How: Node caches a CommonJS module by its resolved absolute path.
15
+ * Resolving `packageName` from the *target file's own directory* (not
16
+ * this package's - a different copy, per commander.adapter.ts's
17
+ * `looksLikeCommand` doc) and mutating the exact object at that cached
18
+ * path means the target's own later `require(packageName)` - resolving
19
+ * to the same path - returns this already-patched object. There's no
20
+ * way for the target to tell the difference: `new Command()` still
21
+ * returns a real `Command` (the patch is a construct-trapping `Proxy`
22
+ * around the real class, transparent to `instanceof` and to every
23
+ * static/instance member), it's just also recorded here as a side
24
+ * effect.
25
+ *
26
+ * Only reaches CommonJS construction - a direct `new ClassExport()`, or
27
+ * a named factory function that closes over the real class and returns
28
+ * `new ClassExport()` internally (commander's `createCommand`, cac's
29
+ * `cac()` - patching only the class export wouldn't catch these, since
30
+ * the factory's own closure still points at the *original* class, not
31
+ * whatever we later put in `moduleExports[classExportName]`). A target
32
+ * that's genuine ESM, reaching the framework via a static `import`
33
+ * rather than `require`, isn't reachable this way - Node's ESM module
34
+ * cache is separate and not patchable from CommonJS. That's a real,
35
+ * documented limit, not a bug: commander and cac both ship CJS-only, so
36
+ * even an ESM target almost always reaches them through interop's own
37
+ * require() underneath - the case this can't reach is the rare one.
38
+ */
39
+ function captureConstructions(packageName, entryPath, classExportName, factoryExportNames) {
40
+ const captured = [];
41
+ let moduleExports;
42
+ try {
43
+ const targetDir = (0, path_1.dirname)((0, path_1.resolve)(process.cwd(), entryPath));
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- resolving/patching the target's own copy of a CJS package, not a static dependency of this file
45
+ const resolvedPath = require.resolve(packageName, { paths: [targetDir] });
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- see above
47
+ moduleExports = require(resolvedPath);
48
+ }
49
+ catch {
50
+ // The target doesn't have this package resolvable from its own
51
+ // location at all - nothing to patch, nothing to capture. The
52
+ // caller's existing "no instance found" error already covers this.
53
+ return captured;
54
+ }
55
+ const record = (instance) => {
56
+ if (instance && typeof instance === "object")
57
+ captured.push(instance);
58
+ };
59
+ const RealClass = moduleExports[classExportName];
60
+ if (typeof RealClass === "function") {
61
+ moduleExports[classExportName] = new Proxy(RealClass, {
62
+ construct(target, args, newTarget) {
63
+ const instance = Reflect.construct(target, args, newTarget);
64
+ record(instance);
65
+ return instance;
66
+ },
67
+ });
68
+ }
69
+ for (const factoryName of factoryExportNames) {
70
+ const realFactory = moduleExports[factoryName];
71
+ if (typeof realFactory === "function") {
72
+ moduleExports[factoryName] = (...args) => {
73
+ const instance = realFactory(...args);
74
+ record(instance);
75
+ return instance;
76
+ };
77
+ }
78
+ }
79
+ return captured;
80
+ }
@@ -0,0 +1,96 @@
1
+ import type { Contract } from "../core/types";
2
+ import type { CliAdapter } from "./adapter.interface";
3
+ /**
4
+ * Extracts a Contract from a target file that builds a yargs CLI. Never
5
+ * parses --help output - every field comes straight from yargs's own
6
+ * `getOptions()`/`getInternalMethods()` object graph, matching
7
+ * CommanderAdapter/CacAdapter's approach.
8
+ *
9
+ * Yargs has no per-command object tree the way Commander does - every
10
+ * `.option()` call (wherever it happens) mutates one shared options bag on
11
+ * the instance, and a command's own options only get registered when its
12
+ * `builder` function actually runs (normally deferred until that command
13
+ * matches at real parse time). To read a command's options in isolation,
14
+ * this adapter calls each command's `builder` against a **fresh, empty**
15
+ * yargs instance rather than the shared one - the same thing yargs itself
16
+ * does internally via `getInternalMethods().reset()` before running a
17
+ * command's builder (verified in yargs's own `command.js`), just without
18
+ * needing yargs to have actually matched and parsed real argv first.
19
+ */
20
+ export declare class YargsAdapter implements CliAdapter {
21
+ readonly id = "yargs";
22
+ extract(entryPath: string): Promise<Contract>;
23
+ private loadYargs;
24
+ /**
25
+ * Captures every instance produced by calling `require("yargs")` or
26
+ * `require("yargs/yargs")` as a function, from the target's own
27
+ * resolved copy of the package - real CLIs almost always reach yargs
28
+ * through one of these two calls (`yargs(hideBin(process.argv))` or
29
+ * `require("yargs/yargs")(args)`), usually without ever exporting the
30
+ * result.
31
+ *
32
+ * Unlike commander/cac's `captureConstructions` (which patches a
33
+ * *named property* on the required module, since `Command`/`cac` are
34
+ * named exports), yargs's own package export *is itself* the callable
35
+ * factory - `require("yargs")` returns a function directly, not a
36
+ * container object with a factory property on it. So this replaces the
37
+ * entire cached module's `.exports` with a `Proxy` around that
38
+ * function, trapping `apply` instead of `construct`: the proxy is
39
+ * still callable exactly like the original (and forwards every other
40
+ * property read/write straight through, since no `get`/`set` trap is
41
+ * defined - which is what makes `require("yargs")`'s other form, using
42
+ * it directly as a pre-built singleton instance without ever calling
43
+ * it, keep working unmodified). `require("yargs")` and
44
+ * `require("yargs/yargs")` resolve to two different files
45
+ * (`index.cjs` vs `yargs.cjs`), so both are patched independently -
46
+ * whichever the target actually uses is the one that ever fires.
47
+ */
48
+ private captureYargsFactoryCalls;
49
+ /** Among every instance captured during construction, the one that looks most like the real, fully-built root program. */
50
+ private pickBestCandidate;
51
+ private score;
52
+ /** Handles `export default`, `module.exports = cli`, and named exports. */
53
+ private findYargs;
54
+ /**
55
+ * Structural check, not `instanceof` - see CommanderAdapter's
56
+ * identical-purpose `looksLikeCommand` for why (the target's own
57
+ * `yargs` install is almost always a separate copy from cliguard's).
58
+ * `getInternalMethods` is fairly distinctive to yargs among CLI
59
+ * frameworks, so it's included alongside the more generic
60
+ * `command`/`option`/`getOptions` to keep this from ever
61
+ * false-matching a Commander or CAC instance.
62
+ */
63
+ private looksLikeYargs;
64
+ private mapRoot;
65
+ /**
66
+ * Runs `handler.builder` against a fresh, empty yargs instance rather
67
+ * than the shared one the target built - see this class's own doc
68
+ * comment for why. A side effect worth calling out: every fresh
69
+ * instance auto-registers its own `help`/`version` options (yargs's
70
+ * own default, not something this specific command declared), so
71
+ * those two names are always excluded below - matching
72
+ * CommanderAdapter/CacAdapter, neither of which surfaces their
73
+ * framework's built-in help/version as a regular option either.
74
+ */
75
+ private mapCommand;
76
+ private freshInstance;
77
+ private mapArguments;
78
+ /**
79
+ * Yargs stores an option's own name as a real entry in `options.string`
80
+ * / `options.boolean` *and* separately re-registers every alias as its
81
+ * own addressable entry in the same arrays (so `-o` shows up next to
82
+ * `output`, not just inside `output`'s own alias list) - excluded here
83
+ * via `aliasTargets` so each alias surfaces exactly once, nested under
84
+ * its canonical option, matching CommanderAdapter/CacAdapter's shape.
85
+ * Positional names go through this same shared bag too (`.positional()`
86
+ * is implemented in terms of the same option-registration machinery
87
+ * internally) - excluded via `positionalNames` so they surface only in
88
+ * `arguments`, never duplicated into `options`.
89
+ */
90
+ private mapOptions;
91
+ private describe;
92
+ /** `-x` for a single-character name, `--xray` otherwise - yargs's own alias lists carry neither dash. */
93
+ private dashPrefix;
94
+ /** Everything not declared `boolean`/`array`/`number` defaults to yargs's own "string" bucket - collapsed to this Contract's two-value OptionValueType the same way CacAdapter does. */
95
+ private inferValueType;
96
+ }
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.YargsAdapter = void 0;
4
+ const path_1 = require("path");
5
+ const load_module_1 = require("./load-module");
6
+ const YARGS_STRING_MARKER = "__yargsString__:";
7
+ /**
8
+ * Extracts a Contract from a target file that builds a yargs CLI. Never
9
+ * parses --help output - every field comes straight from yargs's own
10
+ * `getOptions()`/`getInternalMethods()` object graph, matching
11
+ * CommanderAdapter/CacAdapter's approach.
12
+ *
13
+ * Yargs has no per-command object tree the way Commander does - every
14
+ * `.option()` call (wherever it happens) mutates one shared options bag on
15
+ * the instance, and a command's own options only get registered when its
16
+ * `builder` function actually runs (normally deferred until that command
17
+ * matches at real parse time). To read a command's options in isolation,
18
+ * this adapter calls each command's `builder` against a **fresh, empty**
19
+ * yargs instance rather than the shared one - the same thing yargs itself
20
+ * does internally via `getInternalMethods().reset()` before running a
21
+ * command's builder (verified in yargs's own `command.js`), just without
22
+ * needing yargs to have actually matched and parsed real argv first.
23
+ */
24
+ class YargsAdapter {
25
+ constructor() {
26
+ this.id = "yargs";
27
+ }
28
+ async extract(entryPath) {
29
+ const cli = await this.loadYargs(entryPath);
30
+ return {
31
+ contractVersion: 1,
32
+ adapter: this.id,
33
+ capturedAt: new Date().toISOString(),
34
+ root: this.mapRoot(cli),
35
+ };
36
+ }
37
+ async loadYargs(entryPath) {
38
+ // Patched *before* the target loads, so a `yargs(args)` or
39
+ // `require("yargs/yargs")(args)` call anywhere in its own top-level
40
+ // code gets captured as a side effect of loadModule() below - even if
41
+ // the target never exports the result anywhere. See
42
+ // captureYargsFactoryCalls's own doc for why this is safe.
43
+ const captured = this.captureYargsFactoryCalls(entryPath);
44
+ const { viaImport, viaRequire } = await (0, load_module_1.loadModule)(entryPath);
45
+ const cli = this.findYargs(viaImport.moduleExports) ??
46
+ this.findYargs(viaRequire.moduleExports) ??
47
+ this.pickBestCandidate(captured);
48
+ if (cli)
49
+ return cli;
50
+ // See CommanderAdapter's identical block for why both real load
51
+ // errors - not a swallowed, generic guess - matter here.
52
+ throw new Error(`cliguard: no yargs instance found in "${entryPath}". ` +
53
+ "Export it as `export default cli`, `module.exports = cli`, " +
54
+ "or a named export (e.g. `export const cli = yargs(hideBin(process.argv))`). If " +
55
+ "the file builds its yargs instance inside a function that only runs when " +
56
+ "something calls it (never at the top level), point cliguard at a small wrapper " +
57
+ "file that calls that function and exports the result instead - see the README's " +
58
+ '"Entry files that build the CLI lazily" section.\n' +
59
+ ` import() failed: ${viaImport.error ?? "module loaded, but exported no yargs instance"}\n` +
60
+ ` require() failed: ${viaRequire.error ?? "module loaded, but exported no yargs instance"}`);
61
+ }
62
+ /**
63
+ * Captures every instance produced by calling `require("yargs")` or
64
+ * `require("yargs/yargs")` as a function, from the target's own
65
+ * resolved copy of the package - real CLIs almost always reach yargs
66
+ * through one of these two calls (`yargs(hideBin(process.argv))` or
67
+ * `require("yargs/yargs")(args)`), usually without ever exporting the
68
+ * result.
69
+ *
70
+ * Unlike commander/cac's `captureConstructions` (which patches a
71
+ * *named property* on the required module, since `Command`/`cac` are
72
+ * named exports), yargs's own package export *is itself* the callable
73
+ * factory - `require("yargs")` returns a function directly, not a
74
+ * container object with a factory property on it. So this replaces the
75
+ * entire cached module's `.exports` with a `Proxy` around that
76
+ * function, trapping `apply` instead of `construct`: the proxy is
77
+ * still callable exactly like the original (and forwards every other
78
+ * property read/write straight through, since no `get`/`set` trap is
79
+ * defined - which is what makes `require("yargs")`'s other form, using
80
+ * it directly as a pre-built singleton instance without ever calling
81
+ * it, keep working unmodified). `require("yargs")` and
82
+ * `require("yargs/yargs")` resolve to two different files
83
+ * (`index.cjs` vs `yargs.cjs`), so both are patched independently -
84
+ * whichever the target actually uses is the one that ever fires.
85
+ */
86
+ captureYargsFactoryCalls(entryPath) {
87
+ const captured = [];
88
+ const record = (instance) => {
89
+ if (instance && typeof instance === "object")
90
+ captured.push(instance);
91
+ };
92
+ const targetDir = (0, path_1.dirname)((0, path_1.resolve)(process.cwd(), entryPath));
93
+ for (const packageName of ["yargs", "yargs/yargs"]) {
94
+ let resolvedPath;
95
+ try {
96
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- resolving the target's own copy of a CJS package, not a static dependency of this file
97
+ resolvedPath = require.resolve(packageName, { paths: [targetDir] });
98
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- ensures the module is loaded into require.cache before this patches it
99
+ require(resolvedPath);
100
+ }
101
+ catch {
102
+ // Not resolvable from the target's own location (e.g. the target
103
+ // only uses one of the two import styles) - nothing to patch.
104
+ continue;
105
+ }
106
+ const cacheEntry = require.cache[resolvedPath];
107
+ const original = cacheEntry?.exports;
108
+ if (!cacheEntry || typeof original !== "function")
109
+ continue;
110
+ cacheEntry.exports = new Proxy(original, {
111
+ apply(target, thisArg, args) {
112
+ const instance = Reflect.apply(target, thisArg, args);
113
+ record(instance);
114
+ return instance;
115
+ },
116
+ });
117
+ }
118
+ return captured;
119
+ }
120
+ /** Among every instance captured during construction, the one that looks most like the real, fully-built root program. */
121
+ pickBestCandidate(candidates) {
122
+ const valid = candidates.filter((candidate) => this.looksLikeYargs(candidate));
123
+ if (valid.length === 0)
124
+ return undefined;
125
+ return valid.reduce((best, candidate) => this.score(candidate) > this.score(best) ? candidate : best);
126
+ }
127
+ score(instance) {
128
+ const options = instance.getOptions();
129
+ const handlerCount = Object.keys(instance.getInternalMethods().getCommandInstance().handlers).length;
130
+ return (handlerCount +
131
+ options.boolean.length +
132
+ options.string.length +
133
+ options.array.length +
134
+ options.number.length);
135
+ }
136
+ /** Handles `export default`, `module.exports = cli`, and named exports. */
137
+ findYargs(moduleExports) {
138
+ if (this.looksLikeYargs(moduleExports)) {
139
+ return moduleExports;
140
+ }
141
+ if (moduleExports && typeof moduleExports === "object") {
142
+ const exportsObject = moduleExports;
143
+ if (this.looksLikeYargs(exportsObject.default)) {
144
+ return exportsObject.default;
145
+ }
146
+ for (const value of Object.values(exportsObject)) {
147
+ if (this.looksLikeYargs(value))
148
+ return value;
149
+ }
150
+ }
151
+ return undefined;
152
+ }
153
+ /**
154
+ * Structural check, not `instanceof` - see CommanderAdapter's
155
+ * identical-purpose `looksLikeCommand` for why (the target's own
156
+ * `yargs` install is almost always a separate copy from cliguard's).
157
+ * `getInternalMethods` is fairly distinctive to yargs among CLI
158
+ * frameworks, so it's included alongside the more generic
159
+ * `command`/`option`/`getOptions` to keep this from ever
160
+ * false-matching a Commander or CAC instance.
161
+ */
162
+ looksLikeYargs(value) {
163
+ if (!value || typeof value !== "object")
164
+ return false;
165
+ const candidate = value;
166
+ return (typeof candidate.command === "function" &&
167
+ typeof candidate.option === "function" &&
168
+ typeof candidate.getOptions === "function" &&
169
+ typeof candidate.getInternalMethods === "function");
170
+ }
171
+ mapRoot(cli) {
172
+ const commandInstance = cli.getInternalMethods().getCommandInstance();
173
+ const options = cli.getOptions();
174
+ const descriptions = cli.getInternalMethods().getUsageInstance().getDescriptions();
175
+ return {
176
+ name: cli.$0,
177
+ description: "",
178
+ aliases: [],
179
+ options: this.mapOptions(options, descriptions, new Set()),
180
+ arguments: [],
181
+ subcommands: Object.entries(commandInstance.handlers).map(([name, handler]) => this.mapCommand(name, handler, commandInstance.aliasMap)),
182
+ };
183
+ }
184
+ /**
185
+ * Runs `handler.builder` against a fresh, empty yargs instance rather
186
+ * than the shared one the target built - see this class's own doc
187
+ * comment for why. A side effect worth calling out: every fresh
188
+ * instance auto-registers its own `help`/`version` options (yargs's
189
+ * own default, not something this specific command declared), so
190
+ * those two names are always excluded below - matching
191
+ * CommanderAdapter/CacAdapter, neither of which surfaces their
192
+ * framework's built-in help/version as a regular option either.
193
+ */
194
+ mapCommand(name, handler, parentAliasMap) {
195
+ const scoped = this.freshInstance();
196
+ if (typeof handler.builder === "function") {
197
+ handler.builder(scoped, false);
198
+ }
199
+ else if (handler.builder && typeof handler.builder === "object") {
200
+ scoped.options(handler.builder);
201
+ }
202
+ const commandInstance = scoped.getInternalMethods().getCommandInstance();
203
+ const options = scoped.getOptions();
204
+ const descriptions = scoped.getInternalMethods().getUsageInstance().getDescriptions();
205
+ const positionals = [...handler.demanded, ...handler.optional];
206
+ const positionalNames = new Set(positionals.map((positional) => positional.cmd[0]));
207
+ return {
208
+ name,
209
+ description: handler.description || "",
210
+ aliases: Object.entries(parentAliasMap)
211
+ .filter(([, canonical]) => canonical === name)
212
+ .map(([alias]) => alias),
213
+ options: this.mapOptions(options, descriptions, positionalNames),
214
+ arguments: this.mapArguments(handler, descriptions),
215
+ subcommands: Object.entries(commandInstance.handlers).map(([subName, subHandler]) => this.mapCommand(subName, subHandler, commandInstance.aliasMap)),
216
+ };
217
+ }
218
+ freshInstance() {
219
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- cliguard's own pinned copy, deliberately separate from the target's - this instance never touches the target's code, it's purely scratch space for reading one command's own builder output in isolation
220
+ const factory = require("yargs/yargs");
221
+ return factory([])
222
+ .exitProcess(false)
223
+ .fail(() => undefined);
224
+ }
225
+ mapArguments(handler, descriptions) {
226
+ const required = handler.demanded.map((positional) => ({ positional, required: true }));
227
+ const optional = handler.optional.map((positional) => ({ positional, required: false }));
228
+ return [...required, ...optional].map(({ positional, required: isRequired }) => ({
229
+ name: positional.cmd[0],
230
+ required: isRequired,
231
+ variadic: positional.variadic,
232
+ description: this.describe(descriptions, positional.cmd[0]),
233
+ }));
234
+ }
235
+ /**
236
+ * Yargs stores an option's own name as a real entry in `options.string`
237
+ * / `options.boolean` *and* separately re-registers every alias as its
238
+ * own addressable entry in the same arrays (so `-o` shows up next to
239
+ * `output`, not just inside `output`'s own alias list) - excluded here
240
+ * via `aliasTargets` so each alias surfaces exactly once, nested under
241
+ * its canonical option, matching CommanderAdapter/CacAdapter's shape.
242
+ * Positional names go through this same shared bag too (`.positional()`
243
+ * is implemented in terms of the same option-registration machinery
244
+ * internally) - excluded via `positionalNames` so they surface only in
245
+ * `arguments`, never duplicated into `options`.
246
+ */
247
+ mapOptions(options, descriptions, positionalNames) {
248
+ const aliasTargets = new Set(Object.values(options.alias).flat());
249
+ const allNames = new Set([
250
+ ...options.boolean,
251
+ ...options.string,
252
+ ...options.array,
253
+ ...options.number,
254
+ ]);
255
+ return [...allNames]
256
+ .filter((name) => name !== "help" && name !== "version")
257
+ .filter((name) => !positionalNames.has(name))
258
+ .filter((name) => !aliasTargets.has(name))
259
+ .map((name) => ({
260
+ flags: [
261
+ `--${name}`,
262
+ ...(options.alias[name] ?? []).map((alias) => this.dashPrefix(alias)),
263
+ ].join(", "),
264
+ name,
265
+ aliases: (options.alias[name] ?? []).map((alias) => this.dashPrefix(alias)),
266
+ description: this.describe(descriptions, name),
267
+ required: name in options.demandedOptions,
268
+ valueType: this.inferValueType(options, name),
269
+ variadic: options.array.includes(name),
270
+ defaultValue: name in options.default ? options.default[name] : null,
271
+ }));
272
+ }
273
+ describe(descriptions, name) {
274
+ const raw = descriptions[name] ?? "";
275
+ return raw.startsWith(YARGS_STRING_MARKER) ? raw.slice(YARGS_STRING_MARKER.length) : raw;
276
+ }
277
+ /** `-x` for a single-character name, `--xray` otherwise - yargs's own alias lists carry neither dash. */
278
+ dashPrefix(name) {
279
+ return name.length === 1 ? `-${name}` : `--${name}`;
280
+ }
281
+ /** Everything not declared `boolean`/`array`/`number` defaults to yargs's own "string" bucket - collapsed to this Contract's two-value OptionValueType the same way CacAdapter does. */
282
+ inferValueType(options, name) {
283
+ return options.boolean.includes(name) ? "boolean" : "string";
284
+ }
285
+ }
286
+ exports.YargsAdapter = YargsAdapter;
package/dist/bin.js CHANGED
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const commander_1 = require("commander");
5
5
  const cac_adapter_1 = require("./adapters/cac.adapter");
6
6
  const commander_adapter_1 = require("./adapters/commander.adapter");
7
+ const yargs_adapter_1 = require("./adapters/yargs.adapter");
7
8
  const diff_engine_1 = require("./core/diff.engine");
8
9
  const storage_1 = require("./core/storage");
9
10
  const types_1 = require("./core/types");
@@ -14,6 +15,7 @@ const types_1 = require("./core/types");
14
15
  const adapters = {
15
16
  commander: new commander_adapter_1.CommanderAdapter(),
16
17
  cac: new cac_adapter_1.CacAdapter(),
18
+ yargs: new yargs_adapter_1.YargsAdapter(),
17
19
  };
18
20
  function resolveAdapter(name) {
19
21
  const adapter = adapters[name];
@@ -45,9 +47,13 @@ program
45
47
  console.warn(`A contract already exists. Run "cliguard update" to overwrite it.`);
46
48
  process.exit(1);
47
49
  }
48
- const contract = await resolveAdapter(options.adapter).extract(entry);
49
- (0, storage_1.writeContract)(contract);
50
- console.log(`✅ CLI contract initialized successfully at ${(0, storage_1.getContractDisplayPath)()}.`);
50
+ const exitCode = await withSuppressedExit(async () => {
51
+ const contract = await resolveAdapter(options.adapter).extract(entry);
52
+ (0, storage_1.writeContract)(contract);
53
+ console.log(`✅ CLI contract initialized successfully at ${(0, storage_1.getContractDisplayPath)()}.`);
54
+ return 0;
55
+ });
56
+ process.exit(exitCode);
51
57
  });
52
58
  program
53
59
  .command("check")
@@ -56,20 +62,23 @@ program
56
62
  .option(...adapterOption)
57
63
  .option("--json", "print a machine-readable JSON result instead of text", false)
58
64
  .action(async (entry, options) => {
59
- const oldContract = (0, storage_1.readContract)();
60
- const newContract = await resolveAdapter(options.adapter).extract(entry);
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
- }
67
- if (diff.length === 0) {
68
- console.log("✅ CLI contract is intact.");
69
- process.exit(0);
70
- }
71
- printDiff(diff);
72
- process.exit(hasBreaking ? 1 : 0);
65
+ const exitCode = await withSuppressedExit(async () => {
66
+ const oldContract = (0, storage_1.readContract)();
67
+ const newContract = await resolveAdapter(options.adapter).extract(entry);
68
+ const diff = diffEngine.compare(oldContract, newContract);
69
+ const hasBreaking = diff.some((entry) => entry.type === types_1.ChangeType.BREAKING);
70
+ if (options.json) {
71
+ console.log(JSON.stringify(toJsonResult(diff), null, 2));
72
+ return hasBreaking ? 1 : 0;
73
+ }
74
+ if (diff.length === 0) {
75
+ console.log("✅ CLI contract is intact.");
76
+ return 0;
77
+ }
78
+ printDiff(diff);
79
+ return hasBreaking ? 1 : 0;
80
+ });
81
+ process.exit(exitCode);
73
82
  });
74
83
  program
75
84
  .command("update")
@@ -77,10 +86,45 @@ program
77
86
  .argument("<entry>", "path to the target CLI's entry file")
78
87
  .option(...adapterOption)
79
88
  .action(async (entry, options) => {
80
- const contract = await resolveAdapter(options.adapter).extract(entry);
81
- (0, storage_1.writeContract)(contract);
82
- console.log("🔄 CLI contract updated successfully.");
89
+ const exitCode = await withSuppressedExit(async () => {
90
+ const contract = await resolveAdapter(options.adapter).extract(entry);
91
+ (0, storage_1.writeContract)(contract);
92
+ console.log("🔄 CLI contract updated successfully.");
93
+ return 0;
94
+ });
95
+ process.exit(exitCode);
83
96
  });
97
+ /**
98
+ * Runs `action` with `process.exit` neutralized, restoring the real one
99
+ * the instant `action` settles - then the caller calls the *real*
100
+ * `process.exit` immediately with cliguard's own, correct code.
101
+ *
102
+ * Why this exists: the fallback in commander.adapter.ts /
103
+ * cac.adapter.ts's construction-capture lets a target CLI's own
104
+ * top-level code run further than a plain export lookup ever did - real
105
+ * targets often call `.parse()`/`.run()` for real as a side effect of
106
+ * being loaded, sometimes asynchronously (an `await` inside their own
107
+ * main function, a dangling `.catch()` continuation still in flight).
108
+ * Node's `process.exit()` is immediate and unconditional - if that
109
+ * dangling target code calls it (even with an unrelated code, even
110
+ * *after* cliguard already computed the right answer), it kills this
111
+ * process with the target's exit code, not cliguard's. Restoring the
112
+ * real `process.exit` and calling it ourselves right away, synchronously,
113
+ * the moment `action` resolves closes the race: Node's exit is immediate
114
+ * and single-threaded, so nothing queued after that point - including
115
+ * whatever the target's own code was about to do - ever runs.
116
+ */
117
+ async function withSuppressedExit(action) {
118
+ const realExit = process.exit.bind(process);
119
+ // eslint-disable-next-line @typescript-eslint/no-empty-function -- deliberately a no-op: see doc comment above
120
+ process.exit = (() => undefined);
121
+ try {
122
+ return await action();
123
+ }
124
+ finally {
125
+ process.exit = realExit;
126
+ }
127
+ }
84
128
  function toJsonResult(diff) {
85
129
  const summary = {
86
130
  breaking: diff.filter((entry) => entry.type === types_1.ChangeType.BREAKING).length,
@@ -17,6 +17,8 @@ export declare class DiffEngine {
17
17
  compare(oldContract: Contract, newContract: Contract): DiffResult[];
18
18
  private compareCommands;
19
19
  private compareSubcommands;
20
+ /** CAC's default command (declared with no leading name, e.g. `cli.command("[...files]", ...)`) has name === "" - a blank path segment reads as a typo, not a real command. */
21
+ private commandLabel;
20
22
  private compareOptions;
21
23
  private compareOption;
22
24
  private compareArguments;
@@ -35,10 +35,10 @@ class DiffEngine {
35
35
  results.push({
36
36
  type: types_1.ChangeType.PATCH,
37
37
  path,
38
- message: `Description changed for command "${oldCmd.name}".`,
38
+ message: `Description changed for command "${this.commandLabel(oldCmd.name)}".`,
39
39
  });
40
40
  }
41
- results.push(...this.compareAliases(oldCmd.aliases, newCmd.aliases, path, `command "${oldCmd.name}"`));
41
+ results.push(...this.compareAliases(oldCmd.aliases, newCmd.aliases, path, `command "${this.commandLabel(oldCmd.name)}"`));
42
42
  results.push(...this.compareOptions(oldCmd.options, newCmd.options, path));
43
43
  results.push(...this.compareArguments(oldCmd.arguments, newCmd.arguments, path));
44
44
  results.push(...this.compareSubcommands(oldCmd.subcommands, newCmd.subcommands, path));
@@ -49,13 +49,13 @@ class DiffEngine {
49
49
  const oldByName = this.indexByName(oldSubs);
50
50
  const newByName = this.indexByName(newSubs);
51
51
  for (const [name, oldSub] of oldByName) {
52
- const childPath = `${path} -> ${name}`;
52
+ const childPath = `${path} -> ${this.commandLabel(name)}`;
53
53
  const newSub = newByName.get(name);
54
54
  if (!newSub) {
55
55
  results.push({
56
56
  type: types_1.ChangeType.BREAKING,
57
57
  path: childPath,
58
- message: `Command "${name}" was removed.`,
58
+ message: `Command "${this.commandLabel(name)}" was removed.`,
59
59
  });
60
60
  continue;
61
61
  }
@@ -66,12 +66,16 @@ class DiffEngine {
66
66
  continue;
67
67
  results.push({
68
68
  type: types_1.ChangeType.ADDITIVE,
69
- path: `${path} -> ${name}`,
70
- message: `Command "${name}" was added.`,
69
+ path: `${path} -> ${this.commandLabel(name)}`,
70
+ message: `Command "${this.commandLabel(name)}" was added.`,
71
71
  });
72
72
  }
73
73
  return results;
74
74
  }
75
+ /** CAC's default command (declared with no leading name, e.g. `cli.command("[...files]", ...)`) has name === "" - a blank path segment reads as a typo, not a real command. */
76
+ commandLabel(name) {
77
+ return name === "" ? "<default>" : name;
78
+ }
75
79
  compareOptions(oldOptions, newOptions, path) {
76
80
  const results = [];
77
81
  const oldByName = this.indexByName(oldOptions);
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "cliguard",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Snapshot-tests your CLI's contract (commands, flags, defaults) so you never ship a breaking change by accident.",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "contract-testing",
8
8
  "snapshot-testing",
9
9
  "commander",
10
+ "yargs",
10
11
  "ci"
11
12
  ],
12
13
  "author": "Bryandero98",
@@ -39,16 +40,21 @@
39
40
  "commander": "^12.1.0"
40
41
  },
41
42
  "peerDependencies": {
42
- "cac": "^6.0.0"
43
+ "cac": "^6.0.0",
44
+ "yargs": "^17.0.0"
43
45
  },
44
46
  "peerDependenciesMeta": {
45
47
  "cac": {
46
48
  "optional": true
49
+ },
50
+ "yargs": {
51
+ "optional": true
47
52
  }
48
53
  },
49
54
  "devDependencies": {
50
55
  "@types/jest": "^29.5.13",
51
56
  "@types/node": "^22.7.4",
57
+ "@types/yargs": "^17.0.33",
52
58
  "@typescript-eslint/eslint-plugin": "^8.8.0",
53
59
  "@typescript-eslint/parser": "^8.8.0",
54
60
  "cac": "^6.7.14",
@@ -57,7 +63,8 @@
57
63
  "jest": "^29.7.0",
58
64
  "prettier": "^3.3.3",
59
65
  "ts-jest": "^29.2.5",
60
- "typescript": "^5.6.2"
66
+ "typescript": "^5.6.2",
67
+ "yargs": "^17.7.2"
61
68
  },
62
69
  "engines": {
63
70
  "node": ">=18"