cliguard 0.3.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.
@@ -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, CacClass) ??
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, CacClass) {
67
- if (moduleExports instanceof CacClass) {
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 instanceof CacClass) {
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 instanceof CacClass)
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 instanceof commander_1.Command) {
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 instanceof commander_1.Command) {
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 instanceof commander_1.Command)
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cliguard",
3
- "version": "0.3.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
5
  "keywords": [
6
6
  "cli",