@rulemetric/hooks 0.7.28 → 0.7.30
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/package.json +1 -1
- package/scripts/_config.sh +9 -0
- package/scripts/_log.sh +9 -0
- package/scripts/_normalize.sh +9 -0
- package/scripts/jq +103 -3
package/package.json
CHANGED
package/scripts/_config.sh
CHANGED
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
# discoverable on hosts that do not have jq installed system-wide.
|
|
14
14
|
if ! command -v jq >/dev/null 2>&1; then
|
|
15
15
|
_rulemetric_scripts_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
# npm normalises tarball file modes to 0644, so the bundled shim arrives
|
|
17
|
+
# NON-EXECUTABLE from a published install — `command -v jq` then never finds
|
|
18
|
+
# it on PATH and the fallback silently does not exist at all. Measured on
|
|
19
|
+
# @rulemetric/hooks@0.7.29: scripts/jq shipped -rw-r--r--. Restore the bit
|
|
20
|
+
# here; best effort, because a read-only install must stay on the safe
|
|
21
|
+
# clean-skip path rather than fail the hook.
|
|
22
|
+
if [ -f "$_rulemetric_scripts_dir/jq" ] && [ ! -x "$_rulemetric_scripts_dir/jq" ]; then
|
|
23
|
+
chmod +x "$_rulemetric_scripts_dir/jq" 2>/dev/null || true
|
|
24
|
+
fi
|
|
16
25
|
if [ -x "$_rulemetric_scripts_dir/jq" ]; then
|
|
17
26
|
PATH="$_rulemetric_scripts_dir:$PATH"
|
|
18
27
|
export PATH
|
package/scripts/_log.sh
CHANGED
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
if ! command -v jq >/dev/null 2>&1; then
|
|
7
7
|
_rulemetric_log_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
# npm normalises tarball file modes to 0644, so the bundled shim arrives
|
|
9
|
+
# NON-EXECUTABLE from a published install — `command -v jq` then never finds
|
|
10
|
+
# it on PATH and the fallback silently does not exist at all. Measured on
|
|
11
|
+
# @rulemetric/hooks@0.7.29: scripts/jq shipped -rw-r--r--. Restore the bit
|
|
12
|
+
# here; best effort, because a read-only install must stay on the safe
|
|
13
|
+
# clean-skip path rather than fail the hook.
|
|
14
|
+
if [ -f "$_rulemetric_log_dir/jq" ] && [ ! -x "$_rulemetric_log_dir/jq" ]; then
|
|
15
|
+
chmod +x "$_rulemetric_log_dir/jq" 2>/dev/null || true
|
|
16
|
+
fi
|
|
8
17
|
if [ -x "$_rulemetric_log_dir/jq" ]; then
|
|
9
18
|
PATH="$_rulemetric_log_dir:$PATH"
|
|
10
19
|
export PATH
|
package/scripts/_normalize.sh
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
if ! command -v jq >/dev/null 2>&1; then
|
|
4
4
|
_rulemetric_normalize_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
# npm normalises tarball file modes to 0644, so the bundled shim arrives
|
|
6
|
+
# NON-EXECUTABLE from a published install — `command -v jq` then never finds
|
|
7
|
+
# it on PATH and the fallback silently does not exist at all. Measured on
|
|
8
|
+
# @rulemetric/hooks@0.7.29: scripts/jq shipped -rw-r--r--. Restore the bit
|
|
9
|
+
# here; best effort, because a read-only install must stay on the safe
|
|
10
|
+
# clean-skip path rather than fail the hook.
|
|
11
|
+
if [ -f "$_rulemetric_normalize_dir/jq" ] && [ ! -x "$_rulemetric_normalize_dir/jq" ]; then
|
|
12
|
+
chmod +x "$_rulemetric_normalize_dir/jq" 2>/dev/null || true
|
|
13
|
+
fi
|
|
5
14
|
if [ -x "$_rulemetric_normalize_dir/jq" ]; then
|
|
6
15
|
PATH="$_rulemetric_normalize_dir:$PATH"
|
|
7
16
|
export PATH
|
package/scripts/jq
CHANGED
|
@@ -48,9 +48,22 @@ def parse_args(argv):
|
|
|
48
48
|
return flags, vars_, filter_ or ".", files
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
class Unsupported(Exception):
|
|
52
|
+
"""
|
|
53
|
+
A filter this shim does not implement.
|
|
54
|
+
|
|
55
|
+
Raised rather than returning EMPTY because EMPTY prints nothing and exits 0,
|
|
56
|
+
and every hook caller guards on the EXIT CODE (`... || return 0`). So an
|
|
57
|
+
unsupported filter looked exactly like a successful empty result and the
|
|
58
|
+
guards never fired — which is how `.memories | length` and
|
|
59
|
+
`.suggestions | length` both stayed silently dead. Failing loudly turns the
|
|
60
|
+
next unsupported filter into a clean skip instead of wrong data.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
51
64
|
def path_get(obj, expr):
|
|
52
65
|
if not expr.startswith("."):
|
|
53
|
-
|
|
66
|
+
raise Unsupported(expr)
|
|
54
67
|
cur = obj
|
|
55
68
|
rest = expr[1:]
|
|
56
69
|
if rest == "":
|
|
@@ -109,7 +122,14 @@ def eval_simple(expr, data, vars_):
|
|
|
109
122
|
if m:
|
|
110
123
|
return isinstance(data, dict) and m.group(1) in data
|
|
111
124
|
|
|
112
|
-
|
|
125
|
+
# NOT r"(.+)\\|\\s*length" — in a RAW string that is a literal backslash,
|
|
126
|
+
# so the pattern read as "(.+)\" OR "\s*length" and never matched a real
|
|
127
|
+
# `.foo | length`. It silently fell through to path_get(".memories | length"),
|
|
128
|
+
# which returns EMPTY, which prints nothing with exit 0 — so
|
|
129
|
+
# `count=$(... | jq -r '.memories | length')` came back empty and every
|
|
130
|
+
# caller's `|| return 0` guard saw success. That is the whole reason memory
|
|
131
|
+
# injection was a silent no-op on hosts without real jq.
|
|
132
|
+
m = re.fullmatch(r"(.+)\|\s*length", expr)
|
|
113
133
|
if m:
|
|
114
134
|
value = eval_filter(m.group(1), data, vars_)
|
|
115
135
|
if value is EMPTY or value is None:
|
|
@@ -129,6 +149,19 @@ def eval_simple(expr, data, vars_):
|
|
|
129
149
|
if expr.startswith("{") and expr.endswith("}"):
|
|
130
150
|
return eval_object_literal(expr, data, vars_, drop_null=False)
|
|
131
151
|
|
|
152
|
+
# JSON scalar literals. Without this, a bare `5` in an object literal
|
|
153
|
+
# (`{..., limit: 5}`) fell through to path_get, which returns None for
|
|
154
|
+
# anything not starting with ".", so the request body went out carrying
|
|
155
|
+
# "limit": null — valid JSON with a wrong value, the worst failure shape.
|
|
156
|
+
if re.fullmatch(r"-?\d+", expr):
|
|
157
|
+
return int(expr)
|
|
158
|
+
if re.fullmatch(r"-?\d*\.\d+([eE][-+]?\d+)?|-?\d+[eE][-+]?\d+", expr):
|
|
159
|
+
return float(expr)
|
|
160
|
+
if expr == "true":
|
|
161
|
+
return True
|
|
162
|
+
if expr == "false":
|
|
163
|
+
return False
|
|
164
|
+
|
|
132
165
|
return path_get(data, expr)
|
|
133
166
|
|
|
134
167
|
|
|
@@ -270,13 +303,55 @@ def build_known_n_filter(filter_, vars_):
|
|
|
270
303
|
return eval_filter(filter_, {}, vars_)
|
|
271
304
|
|
|
272
305
|
|
|
306
|
+
def eval_inputs_filter(filter_, stdin, raw_input=False):
|
|
307
|
+
"""
|
|
308
|
+
`inputs` — consume the remaining input stream. Supports exactly the shape the
|
|
309
|
+
hook scripts use, `[inputs | select(length>0)]` under -Rn, which collects the
|
|
310
|
+
non-empty lines of a file into a JSON array (the seen-ids list).
|
|
311
|
+
|
|
312
|
+
Previously unsupported entirely: with -n set, main() went straight to
|
|
313
|
+
build_known_n_filter, nothing matched, and path_get returned EMPTY — printing
|
|
314
|
+
nothing with exit 0, so the caller read an empty seen-list and could not tell
|
|
315
|
+
that from "no ids seen yet".
|
|
316
|
+
"""
|
|
317
|
+
lines = stdin.split("\n")
|
|
318
|
+
# A trailing newline is a terminator, not an empty final record — real jq
|
|
319
|
+
# yields 3 inputs for "a\n\nb\n", not 4.
|
|
320
|
+
if lines and lines[-1] == "":
|
|
321
|
+
lines.pop()
|
|
322
|
+
|
|
323
|
+
if raw_input:
|
|
324
|
+
values = lines
|
|
325
|
+
else:
|
|
326
|
+
values = []
|
|
327
|
+
for line in lines:
|
|
328
|
+
if not line.strip():
|
|
329
|
+
continue
|
|
330
|
+
try:
|
|
331
|
+
values.append(json.loads(line))
|
|
332
|
+
except Exception:
|
|
333
|
+
return EMPTY
|
|
334
|
+
|
|
335
|
+
compact = " ".join(filter_.strip().split())
|
|
336
|
+
if "select(length>0)" in compact.replace(" ", ""):
|
|
337
|
+
values = [v for v in values if v is not None and len(v) > 0]
|
|
338
|
+
|
|
339
|
+
# Wrapped in [...] collects into one array; bare `inputs` streams each value.
|
|
340
|
+
if compact.startswith("[") and compact.endswith("]"):
|
|
341
|
+
return values
|
|
342
|
+
return values
|
|
343
|
+
|
|
344
|
+
|
|
273
345
|
def emit(value, raw=False, compact=False):
|
|
274
346
|
if value is EMPTY:
|
|
275
347
|
return
|
|
276
348
|
if raw and isinstance(value, str):
|
|
277
349
|
sys.stdout.write(value + "\n")
|
|
278
350
|
elif raw and value is None:
|
|
279
|
-
|
|
351
|
+
# `jq -r` prints the four characters "null" for a null result; only
|
|
352
|
+
# `empty` produces no output. Writing a bare newline here diverged from
|
|
353
|
+
# real jq, and invisible divergence is what made this shim dangerous.
|
|
354
|
+
sys.stdout.write("null\n")
|
|
280
355
|
elif raw and isinstance(value, bool):
|
|
281
356
|
sys.stdout.write(("true" if value else "false") + "\n")
|
|
282
357
|
else:
|
|
@@ -284,6 +359,21 @@ def emit(value, raw=False, compact=False):
|
|
|
284
359
|
|
|
285
360
|
|
|
286
361
|
def main():
|
|
362
|
+
try:
|
|
363
|
+
run()
|
|
364
|
+
except Unsupported as err:
|
|
365
|
+
# Exit 3 = jq's own "compile error" code, i.e. "this program is not one I
|
|
366
|
+
# can run" — distinct from 4 (bad input) and from 0 with empty output.
|
|
367
|
+
sys.stderr.write(
|
|
368
|
+
"rulemetric jq shim: unsupported filter %r. This is a narrow fallback, "
|
|
369
|
+
"not a jq implementation — install real jq, or add support in "
|
|
370
|
+
"packages/hooks/scripts/jq and cover it in test/jq-fallback.test.ts.\n"
|
|
371
|
+
% (str(err),)
|
|
372
|
+
)
|
|
373
|
+
sys.exit(3)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def run():
|
|
287
377
|
flags, vars_, filter_, files = parse_args(sys.argv[1:])
|
|
288
378
|
if files:
|
|
289
379
|
with open(files[0], "r", encoding="utf-8") as handle:
|
|
@@ -295,6 +385,16 @@ def main():
|
|
|
295
385
|
emit(stdin, raw=False, compact="c" in flags)
|
|
296
386
|
return
|
|
297
387
|
|
|
388
|
+
# `inputs` reads the stream even under -n, so it must be handled before the
|
|
389
|
+
# -n fast path (which assumes the filter is built purely from --arg vars).
|
|
390
|
+
if "n" in flags and re.search(r"\binputs\b", filter_):
|
|
391
|
+
emit(
|
|
392
|
+
eval_inputs_filter(filter_, stdin, raw_input="R" in flags),
|
|
393
|
+
raw="r" in flags,
|
|
394
|
+
compact="c" in flags,
|
|
395
|
+
)
|
|
396
|
+
return
|
|
397
|
+
|
|
298
398
|
if "n" in flags:
|
|
299
399
|
emit(build_known_n_filter(filter_, vars_), raw="r" in flags, compact="c" in flags)
|
|
300
400
|
return
|