@lovrabet/cli-framework 1.0.1-beta.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
|
|
10
|
-
*
|
|
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. */
|
package/lib/framework/output.js
CHANGED
|
@@ -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
|
|
10
|
-
*
|
|
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.
|
|
11
|
-
* 2.
|
|
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"
|
|
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.
|
|
11
|
-
* 2.
|
|
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
|
|
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(
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
*
|
|
133
|
+
* Reads a normalized `JQ_PATH` value from the provided environment.
|
|
78
134
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
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
|
-
* @
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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.
|
|
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
|
+
}
|