@lovrabet/cli-framework 1.0.1 → 1.0.2

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.
@@ -6,8 +6,8 @@
6
6
  * - `compress` — Single-line JSON envelope (default).
7
7
  * - `pretty` — Human-readable plain-text layout.
8
8
  *
9
- * Supports `--jq` post-filtering in `json` / `compress` modes via the
10
- * bundled `node-jq` executable (or a `jq` binary on PATH).
9
+ * Supports `--jq` post-filtering in `json` / `compress` modes via a jq binary
10
+ * resolved from `JQ_PATH` or the system `PATH`.
11
11
  */
12
12
  import type { CommandResult, OutputFormat, Risk } from "./types.js";
13
13
  /** Shared options passed through every internal print function. */
@@ -6,8 +6,8 @@
6
6
  * - `compress` — Single-line JSON envelope (default).
7
7
  * - `pretty` — Human-readable plain-text layout.
8
8
  *
9
- * Supports `--jq` post-filtering in `json` / `compress` modes via the
10
- * bundled `node-jq` executable (or a `jq` binary on PATH).
9
+ * Supports `--jq` post-filtering in `json` / `compress` modes via a jq binary
10
+ * resolved from `JQ_PATH` or the system `PATH`.
11
11
  */
12
12
  import chalk from "chalk";
13
13
  import { createJqFilter } from "../utils/apply-jq-filter.js";
@@ -7,12 +7,66 @@
7
7
  * or `--format compress`.
8
8
  *
9
9
  * The jq binary is resolved in this order:
10
- * 1. Bundled `jq` shipped with the `node-jq` npm package (platform-specific).
11
- * 2. A `jq` executable found on `PATH`.
10
+ * 1. `JQ_PATH` when explicitly provided.
11
+ * 2. An optional bundled jq candidate path injected by the host CLI.
12
+ * 3. A `jq` executable found on `PATH`.
12
13
  *
13
14
  * @module apply-jq-filter
14
15
  */
15
16
  import type { CliErrorsShape } from "../errors.js";
17
+ /** Source used to resolve the jq binary. */
18
+ export type JqBinarySource = "env" | "bundled" | "path";
19
+ /**
20
+ * Options controlling how a jq executable is discovered.
21
+ */
22
+ export interface JqBinaryResolverOptions {
23
+ /**
24
+ * Explicit bundled jq candidates supplied by the host CLI.
25
+ *
26
+ * The first existing path wins.
27
+ */
28
+ bundledJqPaths?: readonly string[];
29
+ /**
30
+ * Environment map used for resolution.
31
+ *
32
+ * Defaults to `process.env`; exposed for tests.
33
+ */
34
+ env?: NodeJS.ProcessEnv;
35
+ }
36
+ /**
37
+ * Result returned by jq binary resolution.
38
+ */
39
+ export interface ResolvedJqBinary {
40
+ /** Command or absolute file path passed to `execFileSync`. */
41
+ command: string;
42
+ /** Resolution source used for the command. */
43
+ source: JqBinarySource;
44
+ }
45
+ /**
46
+ * Thrown when `JQ_PATH` is present but does not point to an existing file.
47
+ */
48
+ export declare class JqBinaryResolutionError extends Error {
49
+ readonly jqPath: string;
50
+ /** Stable machine-readable error code. */
51
+ code: string;
52
+ /**
53
+ * @param jqPath - Invalid path supplied through `JQ_PATH`.
54
+ */
55
+ constructor(jqPath: string);
56
+ }
57
+ /**
58
+ * Resolves the jq executable for the current process.
59
+ *
60
+ * Resolution order:
61
+ * 1. `JQ_PATH`
62
+ * 2. injected bundled jq candidates
63
+ * 3. `jq` on `PATH`
64
+ *
65
+ * @param options - Resolution options including env overrides and bundled paths.
66
+ * @returns The command/path to execute and its source.
67
+ * @throws {@link JqBinaryResolutionError} if `JQ_PATH` is set but missing.
68
+ */
69
+ export declare function resolveJqBinary(options?: JqBinaryResolverOptions): ResolvedJqBinary;
16
70
  /**
17
71
  * Creates a jq filter function bound to a specific error factory.
18
72
  *
@@ -21,6 +75,7 @@ import type { CliErrorsShape } from "../errors.js";
21
75
  * throws a typed {@link CliError} with code `"validation_error"`.
22
76
  *
23
77
  * @param cliErrors - Error factory providing `validation` for error reporting.
78
+ * @param options - Optional jq binary resolution customizations.
24
79
  * @returns An `applyJqFilter(jsonInput, expr)` function.
25
80
  *
26
81
  * @example
@@ -30,4 +85,4 @@ import type { CliErrorsShape } from "../errors.js";
30
85
  * // filtered === "1\n"
31
86
  * ```
32
87
  */
33
- export declare function createJqFilter(cliErrors: Pick<CliErrorsShape, "validation">): (jsonInput: string, expr: string) => string;
88
+ export declare function createJqFilter(cliErrors: Pick<CliErrorsShape, "validation">, options?: JqBinaryResolverOptions): (jsonInput: string, expr: string) => string;
@@ -7,17 +7,62 @@
7
7
  * or `--format compress`.
8
8
  *
9
9
  * The jq binary is resolved in this order:
10
- * 1. Bundled `jq` shipped with the `node-jq` npm package (platform-specific).
11
- * 2. A `jq` executable found on `PATH`.
10
+ * 1. `JQ_PATH` when explicitly provided.
11
+ * 2. An optional bundled jq candidate path injected by the host CLI.
12
+ * 3. A `jq` executable found on `PATH`.
12
13
  *
13
14
  * @module apply-jq-filter
14
15
  */
15
16
  import { execFileSync } from "node:child_process";
16
17
  import { existsSync } from "node:fs";
17
- import { createRequire } from "node:module";
18
- import path from "node:path";
19
18
  /** Maximum stdout buffer for the jq process (50 MB). */
20
19
  const MAX_BUFFER = 50 * 1024 * 1024;
20
+ /** Error code used when `JQ_PATH` points to a missing executable. */
21
+ const INVALID_JQ_PATH_CODE = "jq_path_invalid";
22
+ /**
23
+ * Thrown when `JQ_PATH` is present but does not point to an existing file.
24
+ */
25
+ export class JqBinaryResolutionError extends Error {
26
+ jqPath;
27
+ /** Stable machine-readable error code. */
28
+ code = INVALID_JQ_PATH_CODE;
29
+ /**
30
+ * @param jqPath - Invalid path supplied through `JQ_PATH`.
31
+ */
32
+ constructor(jqPath) {
33
+ super(`JQ_PATH points to a missing jq executable: ${jqPath}`);
34
+ this.jqPath = jqPath;
35
+ this.name = "JqBinaryResolutionError";
36
+ }
37
+ }
38
+ /**
39
+ * Resolves the jq executable for the current process.
40
+ *
41
+ * Resolution order:
42
+ * 1. `JQ_PATH`
43
+ * 2. injected bundled jq candidates
44
+ * 3. `jq` on `PATH`
45
+ *
46
+ * @param options - Resolution options including env overrides and bundled paths.
47
+ * @returns The command/path to execute and its source.
48
+ * @throws {@link JqBinaryResolutionError} if `JQ_PATH` is set but missing.
49
+ */
50
+ export function resolveJqBinary(options = {}) {
51
+ const env = options.env ?? process.env;
52
+ const jqPath = readJqPath(env);
53
+ if (jqPath) {
54
+ if (!existsSync(jqPath)) {
55
+ throw new JqBinaryResolutionError(jqPath);
56
+ }
57
+ return { command: jqPath, source: "env" };
58
+ }
59
+ for (const candidate of options.bundledJqPaths ?? []) {
60
+ if (candidate && existsSync(candidate)) {
61
+ return { command: candidate, source: "bundled" };
62
+ }
63
+ }
64
+ return { command: "jq", source: "path" };
65
+ }
21
66
  /**
22
67
  * Creates a jq filter function bound to a specific error factory.
23
68
  *
@@ -26,6 +71,7 @@ const MAX_BUFFER = 50 * 1024 * 1024;
26
71
  * throws a typed {@link CliError} with code `"validation_error"`.
27
72
  *
28
73
  * @param cliErrors - Error factory providing `validation` for error reporting.
74
+ * @param options - Optional jq binary resolution customizations.
29
75
  * @returns An `applyJqFilter(jsonInput, expr)` function.
30
76
  *
31
77
  * @example
@@ -35,7 +81,7 @@ const MAX_BUFFER = 50 * 1024 * 1024;
35
81
  * // filtered === "1\n"
36
82
  * ```
37
83
  */
38
- export function createJqFilter(cliErrors) {
84
+ export function createJqFilter(cliErrors, options = {}) {
39
85
  /**
40
86
  * Applies a jq expression filter to a JSON input string.
41
87
  *
@@ -48,7 +94,17 @@ export function createJqFilter(cliErrors) {
48
94
  * - The input is not valid JSON.
49
95
  */
50
96
  return function applyJqFilter(jsonInput, expr) {
51
- const jqCmd = resolveJqBinary();
97
+ const env = options.env ?? process.env;
98
+ let jqCmd = "jq";
99
+ try {
100
+ jqCmd = resolveJqBinary(options).command;
101
+ }
102
+ catch (err) {
103
+ if (err instanceof JqBinaryResolutionError) {
104
+ throw cliErrors.validation(err.message, "Point JQ_PATH to a valid jq executable, unset it, or rely on the bundled/system jq.");
105
+ }
106
+ throw err;
107
+ }
52
108
  try {
53
109
  return execFileSync(jqCmd, [expr], {
54
110
  input: jsonInput,
@@ -59,11 +115,11 @@ export function createJqFilter(cliErrors) {
59
115
  }
60
116
  catch (err) {
61
117
  const e = err;
62
- // jq binary not found
63
118
  if (e.code === "ENOENT") {
64
- throw cliErrors.validation("--jq needs a jq binary. Bundled path (from node-jq) was not found and `jq` is not on PATH. " +
65
- "Reinstall dependencies (allow install scripts), set JQ_PATH to a jq executable, " +
66
- "or install jq: https://jqlang.org/", "e.g. `brew install jq` — or `npm install` with scripts enabled so node-jq can download jq");
119
+ throw cliErrors.validation(`--jq needs a jq binary. Checked ${describeResolutionTargets(env, options.bundledJqPaths)}.`, describeMissingBinaryHint(options.bundledJqPaths));
120
+ }
121
+ if (e.code === "EACCES") {
122
+ throw cliErrors.validation(`jq binary is not executable: ${jqCmd}`, "Fix the file permissions, point JQ_PATH to an executable jq binary, or install jq on PATH.");
67
123
  }
68
124
  const stderr = e.stderr?.toString?.().trim() ?? "";
69
125
  const fallback = (e.message ?? String(err)).trim();
@@ -74,26 +130,48 @@ export function createJqFilter(cliErrors) {
74
130
  };
75
131
  }
76
132
  /**
77
- * Resolves the path to the jq executable.
133
+ * Reads a normalized `JQ_PATH` value from the provided environment.
78
134
  *
79
- * Resolution order:
80
- * 1. Bundled `jq` from the `node-jq` npm package (platform-specific binary).
81
- * 2. `jq` / `jq.exe` found on `PATH` via the `which` equivalent.
135
+ * @param env - Environment variables to inspect.
136
+ * @returns Trimmed `JQ_PATH` when set; otherwise `undefined`.
137
+ */
138
+ function readJqPath(env) {
139
+ const raw = env.JQ_PATH;
140
+ if (typeof raw !== "string")
141
+ return undefined;
142
+ const trimmed = raw.trim();
143
+ return trimmed.length > 0 ? trimmed : undefined;
144
+ }
145
+ /**
146
+ * Builds a user-facing description of the jq binary search order.
82
147
  *
83
- * @returns Absolute path to the jq executable.
148
+ * @param env - Environment variables used for this resolution.
149
+ * @param bundledJqPaths - Bundled jq candidates supplied by the host CLI.
150
+ * @returns Human-readable search target description.
84
151
  */
85
- function resolveJqBinary() {
86
- try {
87
- const require = createRequire(import.meta.url);
88
- const pkgDir = path.dirname(require.resolve("node-jq/package.json"));
89
- const binDir = path.join(pkgDir, "bin");
90
- const name = process.platform === "win32" ? "jq.exe" : "jq";
91
- const bundled = path.join(binDir, name);
92
- if (existsSync(bundled))
93
- return bundled;
152
+ function describeResolutionTargets(env, bundledJqPaths) {
153
+ const targets = ["`jq` on PATH"];
154
+ if ((bundledJqPaths?.length ?? 0) > 0) {
155
+ targets.unshift("the bundled jq shipped with this CLI");
94
156
  }
95
- catch {
96
- // node-jq not installed or resolution failed — fall through to PATH
157
+ if (!readJqPath(env)) {
158
+ targets.unshift("`JQ_PATH`");
159
+ }
160
+ if (targets.length === 1)
161
+ return targets[0] ?? "`jq` on PATH";
162
+ if (targets.length === 2)
163
+ return `${targets[0]} and ${targets[1]}`;
164
+ return `${targets.slice(0, -1).join(", ")}, and ${targets.at(-1)}`;
165
+ }
166
+ /**
167
+ * Builds the remediation hint shown when jq cannot be found.
168
+ *
169
+ * @param bundledJqPaths - Bundled jq candidates supplied by the host CLI.
170
+ * @returns Human-readable remediation hint.
171
+ */
172
+ function describeMissingBinaryHint(bundledJqPaths) {
173
+ if ((bundledJqPaths?.length ?? 0) > 0) {
174
+ return "Set JQ_PATH to a valid jq executable, reinstall the CLI if its bundled jq is missing, or install jq on PATH.";
97
175
  }
98
- return "jq";
176
+ return "Set JQ_PATH to a valid jq executable or install jq on PATH.";
99
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovrabet/cli-framework",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -24,12 +24,11 @@
24
24
  "test": "vitest run"
25
25
  },
26
26
  "dependencies": {
27
- "chalk": "^5.6.2",
28
- "node-jq": "^6.3.1"
27
+ "chalk": "^5.6.2"
29
28
  },
30
29
  "devDependencies": {
31
30
  "@types/node": "^24.5.2",
32
31
  "typescript": "latest",
33
32
  "vitest": "^4.1.2"
34
33
  }
35
- }
34
+ }