kodelyth-ecc 2.20.0 → 2.21.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/CHANGELOG.md +82 -0
- package/CLAUDE.md +1 -1
- package/VERSION +1 -1
- package/bin/kodelyth-ecc.js +1 -1
- package/install.ps1 +2 -2
- package/install.sh +37 -5
- package/package.json +1 -1
- package/rules/common/shell-portability.md +1 -0
- package/rules/shell-portability.md +1 -0
- package/scripts/install/register-hooks.js +136 -0
- package/scripts/lib/install/apply.js +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Kodelyth ECC are documented here.
|
|
4
4
|
|
|
5
|
+
## v2.21.0 — the hooks were never actually turned on (September 2026)
|
|
6
|
+
|
|
7
|
+
Every hook ECC ships has been inert since the shell installer was written. Not
|
|
8
|
+
misconfigured — never registered.
|
|
9
|
+
|
|
10
|
+
### What was broken
|
|
11
|
+
|
|
12
|
+
`install_hooks()` in `install.sh` did exactly one thing:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cp "$SCRIPT_DIR/hooks/hooks.json" "$dest/hooks.json"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Claude Code does not read that file. It reads `hooks` out of `settings.json`.
|
|
19
|
+
So the manifest landed on disk and nothing was ever wired up.
|
|
20
|
+
|
|
21
|
+
It also copied only the manifest, not the `hooks/memory/` and `hooks/safety/`
|
|
22
|
+
scripts every command inside it points at. Of the 12 files under `hooks/`, an
|
|
23
|
+
install copied **1**.
|
|
24
|
+
|
|
25
|
+
Measured on a fresh install into a throwaway `HOME`:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
before after
|
|
29
|
+
settings.json events 1 8
|
|
30
|
+
settings.json entries 1 45
|
|
31
|
+
hook scripts on disk 0 9
|
|
32
|
+
entries whose script exists n/a 36 of 36
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That one `PreToolUse` entry was RTK's — its installer writes `settings.json`
|
|
36
|
+
for its own hook. Nothing ECC ships was in there.
|
|
37
|
+
|
|
38
|
+
So none of this had ever run: memory inject on `SessionStart`, memory capture
|
|
39
|
+
and correction encoding on `Stop`, auto-recall on `UserPromptSubmit`, the
|
|
40
|
+
prompt-injection guard, the token-budget enforcer. The compound-memory
|
|
41
|
+
behaviour described in the docs could not have worked, because the hook that
|
|
42
|
+
implements it was never registered.
|
|
43
|
+
|
|
44
|
+
Correct merge logic already existed in `scripts/lib/install/apply.js` — but the
|
|
45
|
+
CLI shells out to `install.sh` and never reaches it. Instrumenting the function
|
|
46
|
+
during a real install confirmed zero calls.
|
|
47
|
+
|
|
48
|
+
### The fix
|
|
49
|
+
|
|
50
|
+
`install_hooks()` now copies the whole `hooks/` tree and calls a new registrar,
|
|
51
|
+
`scripts/install/register-hooks.js`, which merges the manifest into
|
|
52
|
+
`settings.json` through that same tested merge:
|
|
53
|
+
|
|
54
|
+
- **Idempotent** — three consecutive installs give 45, 45, 45 entries.
|
|
55
|
+
- **Non-destructive** — RTK's hook and the user's own survive; verified.
|
|
56
|
+
- **Scoped** — only the `hooks` key changes; `model`, `theme` and `permissions`
|
|
57
|
+
are left alone.
|
|
58
|
+
- **Atomic** — temp file plus rename, so an interrupted install cannot leave a
|
|
59
|
+
truncated `settings.json` and a broken editor.
|
|
60
|
+
- **Permission-preserving** — an existing file keeps its mode; a new one is
|
|
61
|
+
`0600`, because `settings.json` can carry tokens.
|
|
62
|
+
- **Degrades honestly** — no Node, or a corrupt `settings.json`, prints what to
|
|
63
|
+
run rather than failing the install or losing the hooks.
|
|
64
|
+
|
|
65
|
+
`doctor` now reports `8 hook events wired` instead of warning that three are
|
|
66
|
+
missing.
|
|
67
|
+
|
|
68
|
+
### Corrected
|
|
69
|
+
|
|
70
|
+
The installer banner claimed **194 skills, 97 commands, 22+ hooks**. Actual:
|
|
71
|
+
196, 103, and 44 hook entries across 8 events. It is the first thing every user
|
|
72
|
+
sees, and all three numbers were wrong. Fixed in `install.sh`, `install.ps1`
|
|
73
|
+
and the CLI header.
|
|
74
|
+
|
|
75
|
+
One of those tests then broke Windows CI on its own terms: it asserted the
|
|
76
|
+
substituted root by searching `JSON.stringify(settings)` for the path. A Windows
|
|
77
|
+
root is `D:\a\repo`, which stringify escapes to `D:\\a\\repo`, so the match
|
|
78
|
+
failed even though the substitution was correct. It now asserts on the parsed
|
|
79
|
+
command strings. Reproduced locally with a Windows-shaped path before and after:
|
|
80
|
+
old assertion `false`, new assertion `true`.
|
|
81
|
+
|
|
82
|
+
**647 tests passing** — 14 new, covering idempotency, third-party hook
|
|
83
|
+
preservation, unrelated-key preservation, corrupt-config recovery, file modes,
|
|
84
|
+
and two that guard the regression directly: every event in the shipped manifest
|
|
85
|
+
must register, and every script it references must exist in the repo.
|
|
86
|
+
|
|
5
87
|
## v2.20.0 — 2.19.0 shipped 21 MB instead of 5; here is the guard (September 2026)
|
|
6
88
|
|
|
7
89
|
**Upgrade from 2.19.0.** That release is 4× the size it should be and carries
|
package/CLAUDE.md
CHANGED
|
@@ -26,7 +26,7 @@ scripts/ → Node.js utilities: MCP server, dashboard, swarm, replay, router
|
|
|
26
26
|
bundles/ → 3 power bundles (indie-hacker, red-team, enterprise)
|
|
27
27
|
actions/ → GitHub Action (CI/CD integration for PR review)
|
|
28
28
|
docs/ → Feature docs (arena.md, mcp.md, dashboard.md, swarm.md, replay.md, evolve.md, supply-chain.md)
|
|
29
|
-
tests/ →
|
|
29
|
+
tests/ → 647 passing tests across 42 test files
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## Running Tests
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.21.0
|
package/bin/kodelyth-ecc.js
CHANGED
|
@@ -141,7 +141,7 @@ if (args[1] === '--help' || args[1] === '-h') {
|
|
|
141
141
|
|
|
142
142
|
// ── Subcommand: mcp (start MCP server over stdio) ────────────────────────────
|
|
143
143
|
// Usage: npx kodelyth-ecc mcp
|
|
144
|
-
// Exposes 70 agents,
|
|
144
|
+
// Exposes 70 agents, 196 skills, 103 commands, the routing rule, and the local
|
|
145
145
|
// memory store to any MCP-compatible client (Claude Desktop, LangGraph, etc.).
|
|
146
146
|
if (args[0] === 'mcp') {
|
|
147
147
|
// Hand off entirely to the MCP server — it owns stdio from here on.
|
package/install.ps1
CHANGED
|
@@ -61,7 +61,7 @@ if ($Bundle) {
|
|
|
61
61
|
# -- Banner --------------------------------------------------------------------
|
|
62
62
|
Write-Host ""
|
|
63
63
|
Write-Host " Kodelyth ECC -- Production-grade AI coding agent toolkit" -ForegroundColor Cyan
|
|
64
|
-
Write-Host " 70 agents (8 devil-mode) |
|
|
64
|
+
Write-Host " 70 agents (8 devil-mode) | 196 skills | 103 commands | 44 hooks | intent routing | local memory" -ForegroundColor Gray
|
|
65
65
|
Write-Host ""
|
|
66
66
|
|
|
67
67
|
# $PSScriptRoot is always the directory containing this .ps1 file,
|
|
@@ -344,7 +344,7 @@ Powered by Kodelyth ECC -- github.com/sifxprime/kodelyth-ecc
|
|
|
344
344
|
$geminiText = @'
|
|
345
345
|
# Kodelyth ECC -- Gemini CLI context
|
|
346
346
|
|
|
347
|
-
This project uses Kodelyth ECC -- 70 specialist agents,
|
|
347
|
+
This project uses Kodelyth ECC -- 70 specialist agents, 196 skills, 103 commands, intent routing, and self-learning memory.
|
|
348
348
|
|
|
349
349
|
The full toolkit is installed under this directory:
|
|
350
350
|
|
package/install.sh
CHANGED
|
@@ -47,7 +47,7 @@ echo -e "${RED}${BOLD} ║ DANGER LEVEL: GOD TIER · NOT FOR JUNIOR DEV
|
|
|
47
47
|
echo -e "${RED}${BOLD} ╚══════════════════════════════════════════════════════════════╝${RESET}"
|
|
48
48
|
echo ""
|
|
49
49
|
echo -e "${BOLD} Kodelyth ECC — The most dangerous AI coding toolkit on the planet${RESET}"
|
|
50
|
-
echo -e "${CYAN} 70 specialist agents (8 devil-mode) ·
|
|
50
|
+
echo -e "${CYAN} 70 specialist agents (8 devil-mode) · 196 skills · 103 commands · 44 hooks · intent routing · compound memory${RESET}"
|
|
51
51
|
echo -e "${CYAN} Any language · Any framework · Any scale · 300B-level quality${RESET}"
|
|
52
52
|
echo ""
|
|
53
53
|
echo -e " github.com/sifxprime/kodelyth-ecc"
|
|
@@ -435,7 +435,8 @@ install_flat() {
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
install_hooks() {
|
|
438
|
-
local
|
|
438
|
+
local src_dir="$SCRIPT_DIR/hooks"
|
|
439
|
+
local src="$src_dir/hooks.json"
|
|
439
440
|
local dest="$1"
|
|
440
441
|
|
|
441
442
|
if [[ -z "$dest" ]] || [[ ! -f "$src" ]]; then
|
|
@@ -443,8 +444,39 @@ install_hooks() {
|
|
|
443
444
|
fi
|
|
444
445
|
|
|
445
446
|
mkdir -p "$dest"
|
|
446
|
-
|
|
447
|
-
|
|
447
|
+
|
|
448
|
+
# Copy the whole tree, not just hooks.json. Every command inside hooks.json
|
|
449
|
+
# points at ${CLAUDE_PLUGIN_ROOT}/hooks/{memory,safety}/*.js — copying the
|
|
450
|
+
# manifest alone registered hooks whose scripts were never installed.
|
|
451
|
+
cp -r "$src_dir"/. "$dest/"
|
|
452
|
+
local script_count
|
|
453
|
+
script_count=$(find "$dest" -name '*.js' -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
454
|
+
|
|
455
|
+
echo -e " ${GREEN}✓${RESET} Hooks ${BLUE}(→ $dest, $script_count scripts)${RESET}"
|
|
456
|
+
|
|
457
|
+
# Registering into settings.json is the step that actually activates them.
|
|
458
|
+
# Claude Code reads hooks from settings.json; a hooks.json sitting on disk
|
|
459
|
+
# does nothing. Node ships with this package, but install.sh can also be run
|
|
460
|
+
# standalone, so degrade with a clear message rather than failing the install.
|
|
461
|
+
local registrar="$SCRIPT_DIR/scripts/install/register-hooks.js"
|
|
462
|
+
local target_root="${dest%/hooks}"
|
|
463
|
+
|
|
464
|
+
if [[ ! -f "$registrar" ]]; then
|
|
465
|
+
return
|
|
466
|
+
fi
|
|
467
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
468
|
+
echo -e " ${YELLOW}!${RESET} Hooks copied but not registered — node not found"
|
|
469
|
+
echo -e " ${BLUE}Run after installing Node:${RESET} node \"$registrar\" \"$target_root\""
|
|
470
|
+
return
|
|
471
|
+
fi
|
|
472
|
+
|
|
473
|
+
local summary
|
|
474
|
+
if summary=$(node "$registrar" "$target_root" 2>&1); then
|
|
475
|
+
echo -e " ${GREEN}✓${RESET} Hooks registered ${BLUE}($summary)${RESET}"
|
|
476
|
+
else
|
|
477
|
+
echo -e " ${YELLOW}!${RESET} Hook registration failed: $summary"
|
|
478
|
+
echo -e " ${BLUE}Retry with:${RESET} node \"$registrar\" \"$target_root\""
|
|
479
|
+
fi
|
|
448
480
|
}
|
|
449
481
|
|
|
450
482
|
# Generate a single .windsurfrules file from all common rule .md files
|
|
@@ -621,7 +653,7 @@ AIDER_EOF
|
|
|
621
653
|
cat > "$GEMINI_FILE" <<'GEMINI_EOF'
|
|
622
654
|
# Kodelyth ECC — Gemini CLI context
|
|
623
655
|
|
|
624
|
-
This project uses Kodelyth ECC — 70 specialist agents,
|
|
656
|
+
This project uses Kodelyth ECC — 70 specialist agents, 196 skills, 103 commands, intent routing, and self-learning memory.
|
|
625
657
|
|
|
626
658
|
The full toolkit is installed under this directory:
|
|
627
659
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodelyth-ecc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"description": "Production-grade AI coding toolkit — 70 agents (incl. devil-mode adversarial crew), 194 skills, 97 commands, parallel multi-agent commands, semantic intent routing, self-learning memory, and a built-in MCP server (16 tools / 6 prompts / 377 resources) that bridges to Claude Desktop, LangGraph, AutoGen, CrewAI, and OpenAI Agents SDK. Works with Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, RooCode, Aider, Kimi, and Gemini CLI.",
|
|
5
5
|
"author": "Kodelyth <github.com/sifxprime>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,7 @@ Every row below was checked on a stock macOS shell. These are not theoretical.
|
|
|
28
28
|
| `grep -c` under `set -e` | **any OS** — exits 1 on zero matches | `grep -c x f \|\| echo 0` |
|
|
29
29
|
| `for x in $VAR` | **macOS/zsh** — no word splitting, loops once | list items literally, or `for x in "${ARR[@]}"` |
|
|
30
30
|
| `execFileSync('npm', …)` | **Windows** — ENOENT bare (no PATHEXT), EINVAL on `.cmd` since Node 18.20.2 | `execSync` with one literal command string |
|
|
31
|
+
| `JSON.stringify(x).includes(winPath)` | **Windows** — stringify escapes `\\` to `\\\\` | assert on parsed values, never on JSON text |
|
|
31
32
|
|
|
32
33
|
## Measured, not assumed
|
|
33
34
|
|
|
@@ -28,6 +28,7 @@ Every row below was checked on a stock macOS shell. These are not theoretical.
|
|
|
28
28
|
| `grep -c` under `set -e` | **any OS** — exits 1 on zero matches | `grep -c x f \|\| echo 0` |
|
|
29
29
|
| `for x in $VAR` | **macOS/zsh** — no word splitting, loops once | list items literally, or `for x in "${ARR[@]}"` |
|
|
30
30
|
| `execFileSync('npm', …)` | **Windows** — ENOENT bare (no PATHEXT), EINVAL on `.cmd` since Node 18.20.2 | `execSync` with one literal command string |
|
|
31
|
+
| `JSON.stringify(x).includes(winPath)` | **Windows** — stringify escapes `\\` to `\\\\` | assert on parsed values, never on JSON text |
|
|
31
32
|
|
|
32
33
|
## Measured, not assumed
|
|
33
34
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register ECC's hooks into a Claude-compatible settings.json.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists
|
|
7
|
+
* ---------------
|
|
8
|
+
* `install.sh` copied `hooks/hooks.json` into `<target>/hooks/hooks.json` and
|
|
9
|
+
* stopped there. Claude Code does not read that file — it reads `hooks` out of
|
|
10
|
+
* `settings.json`. So every hook ECC ships was placed on disk and never
|
|
11
|
+
* activated: memory inject (SessionStart), memory capture and correction
|
|
12
|
+
* encoding (Stop), the prompt-injection guard, the token-budget enforcer.
|
|
13
|
+
*
|
|
14
|
+
* The only entry that ever appeared in settings.json came from RTK's own
|
|
15
|
+
* installer, which writes that file for its own hook.
|
|
16
|
+
*
|
|
17
|
+
* A correct merge already existed in scripts/lib/install/apply.js, but the CLI
|
|
18
|
+
* shells out to install.sh and never reaches it. This script is the bridge:
|
|
19
|
+
* install.sh calls it, and it uses that same tested merge.
|
|
20
|
+
*
|
|
21
|
+
* Guarantees
|
|
22
|
+
* ----------
|
|
23
|
+
* - Idempotent. Re-running adds nothing; entries dedupe by content signature.
|
|
24
|
+
* - Non-destructive. Hooks already in settings.json are kept, including
|
|
25
|
+
* another tool's (RTK's) and the user's own.
|
|
26
|
+
* - Scoped. Only the `hooks` key is touched; every other setting is preserved.
|
|
27
|
+
* - Atomic. Written via a temp file and rename, so an interrupted install
|
|
28
|
+
* cannot leave a truncated settings.json — which would break the editor.
|
|
29
|
+
*
|
|
30
|
+
* Usage: node scripts/install/register-hooks.js <targetRoot> [hooksJsonPath]
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const fs = require('fs');
|
|
34
|
+
const path = require('path');
|
|
35
|
+
|
|
36
|
+
const { mergeHookEntries } = require('../lib/install/apply.js');
|
|
37
|
+
const { replaceFilePreservingMode } = require('../lib/safe-fs.js');
|
|
38
|
+
|
|
39
|
+
function readJsonOrNull(file) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
42
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} targetRoot e.g. ~/.claude
|
|
50
|
+
* @param {string} [hooksJsonPath] defaults to <targetRoot>/hooks/hooks.json
|
|
51
|
+
* @returns {{status:string, events:number, added:number, total:number, settingsPath:string}}
|
|
52
|
+
*/
|
|
53
|
+
function registerHooks(targetRoot, hooksJsonPath) {
|
|
54
|
+
if (!targetRoot) throw new Error('registerHooks: targetRoot is required');
|
|
55
|
+
|
|
56
|
+
const hooksPath = hooksJsonPath || path.join(targetRoot, 'hooks', 'hooks.json');
|
|
57
|
+
const config = readJsonOrNull(hooksPath);
|
|
58
|
+
if (!config) {
|
|
59
|
+
return { status: 'no-hooks-config', events: 0, added: 0, total: 0, settingsPath: '' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const incoming = config.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks)
|
|
63
|
+
? config.hooks
|
|
64
|
+
: null;
|
|
65
|
+
if (!incoming) {
|
|
66
|
+
return { status: 'invalid-hooks-config', events: 0, added: 0, total: 0, settingsPath: '' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const settingsPath = path.join(targetRoot, 'settings.json');
|
|
70
|
+
const settings = readJsonOrNull(settingsPath) || {};
|
|
71
|
+
const existing = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
72
|
+
? settings.hooks
|
|
73
|
+
: {};
|
|
74
|
+
|
|
75
|
+
const before = countEntries(existing);
|
|
76
|
+
const merged = { ...existing };
|
|
77
|
+
|
|
78
|
+
for (const [event, entries] of Object.entries(incoming)) {
|
|
79
|
+
const current = Array.isArray(existing[event]) ? existing[event] : [];
|
|
80
|
+
const next = Array.isArray(entries) ? entries : [];
|
|
81
|
+
// targetRoot is the plugin root: hooks.json references
|
|
82
|
+
// ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/... which resolves under it.
|
|
83
|
+
merged[event] = mergeHookEntries(current, next, targetRoot);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const after = countEntries(merged);
|
|
87
|
+
const body = JSON.stringify({ ...settings, hooks: merged }, null, 2) + '\n';
|
|
88
|
+
|
|
89
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
90
|
+
// Keeps the file's existing permissions when it is already there — this is a
|
|
91
|
+
// user config and the installer has no business widening or narrowing it.
|
|
92
|
+
// 0o600 for a new one: settings.json can carry tokens.
|
|
93
|
+
replaceFilePreservingMode(settingsPath, body, 0o600);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
status: 'ok',
|
|
97
|
+
events: Object.keys(merged).length,
|
|
98
|
+
added: after - before,
|
|
99
|
+
total: after,
|
|
100
|
+
settingsPath,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function countEntries(hooksObject) {
|
|
105
|
+
let n = 0;
|
|
106
|
+
for (const entries of Object.values(hooksObject || {})) {
|
|
107
|
+
if (Array.isArray(entries)) {
|
|
108
|
+
for (const entry of entries) n += Array.isArray(entry.hooks) ? entry.hooks.length : 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return n;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { registerHooks, countEntries };
|
|
115
|
+
|
|
116
|
+
if (require.main === module) {
|
|
117
|
+
const [targetRoot, hooksJsonPath] = process.argv.slice(2);
|
|
118
|
+
if (!targetRoot) {
|
|
119
|
+
console.error('usage: node scripts/install/register-hooks.js <targetRoot> [hooksJsonPath]');
|
|
120
|
+
process.exit(2);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const r = registerHooks(targetRoot, hooksJsonPath);
|
|
124
|
+
if (r.status !== 'ok') {
|
|
125
|
+
// Not fatal: a target without a hooks config is a valid configuration.
|
|
126
|
+
console.error(`hooks not registered (${r.status})`);
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
console.log(`${r.total} hook entries across ${r.events} events (+${r.added} new)`);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// An install must not die here. Report loudly and let the rest complete —
|
|
132
|
+
// but exit non-zero so a caller that checks can surface it.
|
|
133
|
+
console.error(`hook registration failed: ${err.message}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -261,5 +261,10 @@ function applyInstallPlan(plan) {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
module.exports = {
|
|
264
|
+
// Exported so the shell installer can register hooks through the same tested
|
|
265
|
+
// merge as the JS install path — install.sh copied hooks.json but never wrote
|
|
266
|
+
// settings.json, so none of the hooks were ever active (fixed in 2.21.0).
|
|
267
|
+
mergeHookEntries,
|
|
268
|
+
replacePluginRootPlaceholders,
|
|
264
269
|
applyInstallPlan,
|
|
265
270
|
};
|