@phuc1403/musketeer 0.2.1 → 0.2.3
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 +8 -4
- package/bin/musketeer.js +11 -2
- package/manifest.json +10 -2
- package/package.json +1 -1
- package/src/promote.js +16 -4
- package/src/reconcile.js +1 -1
- package/src/self-update.js +73 -0
- package/template/.claude/hooks/inject-context.cjs +52 -0
- package/template/.claude/skills/knowledge-crunching/SKILL.md +2 -2
- package/template/.claude/skills/knowledge-crunching/assets/context.template.md +2 -2
package/README.md
CHANGED
|
@@ -12,15 +12,19 @@ npm i -g @phuc1403/musketeer
|
|
|
12
12
|
## Use
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
musketeer muster #
|
|
15
|
+
musketeer muster # self-update → pick company (multi-select) → refresh all selected skills to latest
|
|
16
16
|
musketeer promote # update the musketeer CLI itself to the latest published version
|
|
17
17
|
musketeer # show usage
|
|
18
18
|
musketeer --help # show usage
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
`musketeer muster`
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
`musketeer muster` **self-updates first**: it pulls the latest published version (re-execing when behind),
|
|
22
|
+
so the refresh always applies the newest template. Set `MUSKETEER_NO_SELF_UPDATE=1` to skip (offline / pinned).
|
|
23
|
+
|
|
24
|
+
It is also **idempotent** and **force-updating**: the picker pre-checks your current company (from
|
|
25
|
+
`.musketeer.json`); on confirm it **re-writes every selected musketeer's files to the latest template**
|
|
26
|
+
(overwriting any local edits), removes newly-unchecked ones, regenerates `settings.json`, and auto-installs
|
|
27
|
+
any missing prerequisites.
|
|
24
28
|
|
|
25
29
|
`musketeer promote` upgrades the **CLI tool** itself (not your project's company): it checks the npm
|
|
26
30
|
registry, and if a newer version exists, runs `npm i -g @phuc1403/musketeer@latest`. _muster = which
|
package/bin/musketeer.js
CHANGED
|
@@ -6,16 +6,17 @@ const { parseArgs } = require('node:util');
|
|
|
6
6
|
const USAGE = `musketeer — scaffold a curated company of Claude Code musketeers into ./.claude
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
|
-
musketeer muster
|
|
9
|
+
musketeer muster Self-update to latest, then pick your company and refresh all selected skills
|
|
10
10
|
musketeer promote Update the musketeer CLI itself to the latest published version
|
|
11
11
|
musketeer Show this help
|
|
12
12
|
musketeer --help Show this help
|
|
13
13
|
|
|
14
14
|
Notes:
|
|
15
15
|
* "muster" changes which musketeers are in THIS project; "promote" upgrades the CLI itself.
|
|
16
|
+
* On confirm, muster REFRESHES every selected musketeer's files to the latest template (overwrites local edits).
|
|
17
|
+
* muster auto-updates the CLI to the latest published version first (set MUSKETEER_NO_SELF_UPDATE=1 to skip).
|
|
16
18
|
* "core" is always installed and locked — it cannot be removed.
|
|
17
19
|
* The muster is multi-select: check every musketeer you want, then confirm.
|
|
18
|
-
* Re-running is idempotent: the picker pre-checks your current company; confirm applies the diff.
|
|
19
20
|
* settings.json is GENERATED per-selection; missing prereqs are auto-installed (announced).`;
|
|
20
21
|
|
|
21
22
|
function parse(argv) {
|
|
@@ -48,6 +49,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
if (command === 'muster') {
|
|
52
|
+
// Pull the latest published version first, so the picker refreshes installed
|
|
53
|
+
// skills to the newest template (not a stale package's). Re-execs when behind.
|
|
54
|
+
const { ensureLatest } = require('../src/self-update');
|
|
55
|
+
const reexecCode = ensureLatest();
|
|
56
|
+
if (reexecCode !== null) {
|
|
57
|
+
process.exitCode = reexecCode;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
51
60
|
// Lazy-require so help never pays the cost of loading the engine/manifest.
|
|
52
61
|
const reconcile = require('../src/reconcile');
|
|
53
62
|
await reconcile.run({ desiredIds: null, interactive: true });
|
package/manifest.json
CHANGED
|
@@ -95,13 +95,14 @@
|
|
|
95
95
|
},
|
|
96
96
|
"dotnet": {
|
|
97
97
|
"label": "dotnet",
|
|
98
|
-
"description": ".NET extras: tdd, knowledge-crunching + EF migration-guard
|
|
98
|
+
"description": ".NET extras: tdd, knowledge-crunching + EF migration-guard & CONTEXT auto-load hooks.",
|
|
99
99
|
"locked": false,
|
|
100
100
|
"deps": [],
|
|
101
101
|
"files": [
|
|
102
102
|
"skills/tdd/**",
|
|
103
103
|
"skills/knowledge-crunching/**",
|
|
104
|
-
"hooks/block-migration-edits.cjs"
|
|
104
|
+
"hooks/block-migration-edits.cjs",
|
|
105
|
+
"hooks/inject-context.cjs"
|
|
105
106
|
],
|
|
106
107
|
"settings": [
|
|
107
108
|
{
|
|
@@ -110,6 +111,13 @@
|
|
|
110
111
|
"command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/block-migration-edits.cjs\"",
|
|
111
112
|
"order": 1,
|
|
112
113
|
"statusMessage": "Checking for EF migration edits"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"event": "SessionStart",
|
|
117
|
+
"matcher": "startup|resume|clear|compact",
|
|
118
|
+
"command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/inject-context.cjs\"",
|
|
119
|
+
"order": 1,
|
|
120
|
+
"statusMessage": "Loading domain CONTEXT"
|
|
113
121
|
}
|
|
114
122
|
],
|
|
115
123
|
"prereqs": []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phuc1403/musketeer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Distributable custom Claude Code harness — one declarative command scaffolds a curated company of musketeers (skills/agents/hooks) into any project's .claude/.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
package/src/promote.js
CHANGED
|
@@ -23,6 +23,19 @@ function defaultRun(cmd) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Look up a package's published `latest` version from the registry.
|
|
28
|
+
* @param {(cmd:string)=>{code:number,stdout:string,stderr:string}} exec
|
|
29
|
+
* @param {string} name
|
|
30
|
+
* @returns {?string} the version, or null if unreachable / unparseable
|
|
31
|
+
*/
|
|
32
|
+
function fetchLatest(exec, name) {
|
|
33
|
+
const view = exec(`npm view ${name} version`);
|
|
34
|
+
const latest = (view.stdout || '').trim();
|
|
35
|
+
if (view.code !== 0 || !/^\d+\.\d+\.\d+/.test(latest)) return null;
|
|
36
|
+
return latest;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
/** Compare dotted numeric versions: 1 if a>b, -1 if a<b, 0 if equal. */
|
|
27
40
|
function compareVersions(a, b) {
|
|
28
41
|
const pa = String(a).split('.').map(Number);
|
|
@@ -50,10 +63,9 @@ async function run(overrides = {}) {
|
|
|
50
63
|
const current = overrides.current || pkg.version;
|
|
51
64
|
|
|
52
65
|
log(`Checking rank… installed ${current} (${name})`);
|
|
53
|
-
const
|
|
54
|
-
const latest = (view.stdout || '').trim();
|
|
66
|
+
const latest = fetchLatest(exec, name);
|
|
55
67
|
|
|
56
|
-
if (
|
|
68
|
+
if (!latest) {
|
|
57
69
|
log('Could not reach the npm registry to check for a newer version.');
|
|
58
70
|
log(`Try manually: npm i -g ${name}@latest`);
|
|
59
71
|
return { status: 'unknown', from: current, to: null };
|
|
@@ -78,4 +90,4 @@ async function run(overrides = {}) {
|
|
|
78
90
|
return { status: 'promoted', from: current, to: latest };
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
module.exports = { run, compareVersions, defaultRun };
|
|
93
|
+
module.exports = { run, fetchLatest, compareVersions, defaultRun };
|
package/src/reconcile.js
CHANGED
|
@@ -50,7 +50,7 @@ async function run(opts, overrides = {}) {
|
|
|
50
50
|
const plan = copier.planFiles(prev.files, desired.files);
|
|
51
51
|
|
|
52
52
|
d.log(`Company: ${desired.ids.join(', ')}`);
|
|
53
|
-
d.log(`Files: ${plan.copy.length}
|
|
53
|
+
d.log(`Files: ${plan.copy.length} refreshed to latest, ${plan.remove.length} removed.`);
|
|
54
54
|
|
|
55
55
|
copier.apply(plan, d.templateDir, projectDir);
|
|
56
56
|
d.settingsMerger.generate(desired, projectDir);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Self-update gate for `musketeer muster`. Before the picker applies the bundled
|
|
4
|
+
// template, make sure the CLI is at the latest PUBLISHED version — otherwise
|
|
5
|
+
// "muster" would refresh installed skills to a stale package's template.
|
|
6
|
+
//
|
|
7
|
+
// When behind: promote (npm i -g @latest), then re-exec `musketeer muster` so the
|
|
8
|
+
// picker runs under the freshly-installed template. A running Node process can't
|
|
9
|
+
// reliably hot-swap its own package files mid-run, so re-exec is the safe path.
|
|
10
|
+
//
|
|
11
|
+
// Guards:
|
|
12
|
+
// MUSKETEER_REEXEC=1 set on the child so it never re-checks (no loop)
|
|
13
|
+
// MUSKETEER_NO_SELF_UPDATE=1 user opt-out (offline / pinned version)
|
|
14
|
+
|
|
15
|
+
const { spawnSync } = require('child_process');
|
|
16
|
+
const { fetchLatest, compareVersions, defaultRun } = require('./promote');
|
|
17
|
+
const pkg = require('../package.json');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Decide whether muster should self-update first. Pure — injectable exec/env.
|
|
21
|
+
* @param {{exec?:Function, current?:string, name?:string, env?:object}} [o]
|
|
22
|
+
* @returns {{action:'proceed'|'update', latest:?string}}
|
|
23
|
+
*/
|
|
24
|
+
function decide(o = {}) {
|
|
25
|
+
const exec = o.exec || defaultRun;
|
|
26
|
+
const current = o.current || pkg.version;
|
|
27
|
+
const name = o.name || pkg.name;
|
|
28
|
+
const env = o.env || process.env;
|
|
29
|
+
|
|
30
|
+
if (env.MUSKETEER_REEXEC === '1' || env.MUSKETEER_NO_SELF_UPDATE === '1') {
|
|
31
|
+
return { action: 'proceed', latest: null };
|
|
32
|
+
}
|
|
33
|
+
const latest = fetchLatest(exec, name);
|
|
34
|
+
if (!latest) return { action: 'proceed', latest: null }; // offline / unknown — proceed as-is
|
|
35
|
+
if (compareVersions(latest, current) <= 0) return { action: 'proceed', latest }; // already latest
|
|
36
|
+
return { action: 'update', latest };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ensure the CLI is latest before muster applies the template.
|
|
41
|
+
* @param {object} [io] injectable { log, exec, spawn, env, name, current } for tests
|
|
42
|
+
* @returns {?number} an exit code if it re-exec'd a fresh muster (caller should
|
|
43
|
+
* exit with it); null to mean "proceed in-process".
|
|
44
|
+
*/
|
|
45
|
+
function ensureLatest(io = {}) {
|
|
46
|
+
const log = io.log || ((m) => process.stdout.write(m + '\n'));
|
|
47
|
+
const exec = io.exec || defaultRun;
|
|
48
|
+
const spawn = io.spawn || spawnSync;
|
|
49
|
+
const env = io.env || process.env;
|
|
50
|
+
const name = io.name || pkg.name;
|
|
51
|
+
const current = io.current || pkg.version;
|
|
52
|
+
|
|
53
|
+
const d = decide({ exec, current, name, env });
|
|
54
|
+
if (d.action === 'proceed') return null;
|
|
55
|
+
|
|
56
|
+
log(`Updating musketeer ${current} → ${d.latest} before muster…`);
|
|
57
|
+
const ins = exec(`npm i -g ${name}@latest`);
|
|
58
|
+
if (ins.code !== 0) {
|
|
59
|
+
log('Self-update failed — continuing with the current version.');
|
|
60
|
+
if (ins.stderr) log(ins.stderr.trim());
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Re-exec the now-latest binary's muster, inheriting the TTY for the picker.
|
|
65
|
+
const r = spawn('musketeer', ['muster'], {
|
|
66
|
+
stdio: 'inherit',
|
|
67
|
+
shell: true,
|
|
68
|
+
env: { ...env, MUSKETEER_REEXEC: '1' },
|
|
69
|
+
});
|
|
70
|
+
return typeof r.status === 'number' ? r.status : 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { decide, ensureLatest };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SessionStart hook (dotnet company): auto-load the repo-root `CONTEXT.md` — the
|
|
3
|
+
// bounded-context model produced by the knowledge-crunching skill — into every
|
|
4
|
+
// session, so the domain's ubiquitous language and invariants "lead the code".
|
|
5
|
+
//
|
|
6
|
+
// If the root CONTEXT.md is missing, ALERT the user (systemMessage) so they
|
|
7
|
+
// create one. Any other error fails open (emits nothing, exit 0) so it can never
|
|
8
|
+
// block a session.
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
|
|
12
|
+
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
13
|
+
const file = path.join(root, "CONTEXT.md");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
let content;
|
|
17
|
+
try {
|
|
18
|
+
content = fs.readFileSync(file, "utf-8");
|
|
19
|
+
} catch {
|
|
20
|
+
// Not found — surface a visible warning to the user, inject nothing.
|
|
21
|
+
process.stdout.write(
|
|
22
|
+
JSON.stringify({
|
|
23
|
+
systemMessage:
|
|
24
|
+
"musketeer: no CONTEXT.md at the repo root — the bounded-context model is missing. " +
|
|
25
|
+
"Run the knowledge-crunching skill (/knowledge-crunching) to create one.",
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const additionalContext =
|
|
32
|
+
"Bounded-context model (root CONTEXT.md) — injected every session. It is the " +
|
|
33
|
+
"ubiquitous language and model rules of this bounded context, kept vendor- and " +
|
|
34
|
+
"decision-neutral. Treat it as canonical for domain naming, concepts, and invariants: " +
|
|
35
|
+
"name new code after it, and when a concept is renamed update CONTEXT.md and the code in " +
|
|
36
|
+
"the same turn. It bounds what the DOMAIN model sees, not what infrastructure may do " +
|
|
37
|
+
"(an ACL can legitimately key on more).\n\n" +
|
|
38
|
+
"===== CONTEXT.md =====\n" +
|
|
39
|
+
content.trimEnd();
|
|
40
|
+
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
hookSpecificOutput: {
|
|
44
|
+
hookEventName: "SessionStart",
|
|
45
|
+
additionalContext,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
} catch {
|
|
51
|
+
process.exit(0); // fail open
|
|
52
|
+
}
|
|
@@ -42,7 +42,7 @@ it's out of scope and offer to hand off afterward.
|
|
|
42
42
|
|
|
43
43
|
### Step 1 — Open the session and name the flow
|
|
44
44
|
|
|
45
|
-
**First read any prior crunching output for that context** —
|
|
45
|
+
**First read any prior crunching output for that context** — the root-level `CONTEXT.md` and read the domain's code; in DDD, document = code. While reading the code, **note the language, idioms, and test framework already in use** — you'll model in those. If the module is a bare scaffold with no language chosen yet, ask the user which language and test framework the context should be modeled in before proposing any code.
|
|
46
46
|
|
|
47
47
|
### Step 2 — Find the first model elements (the "glimmer")
|
|
48
48
|
Listen past the user's solution framing for the **domain nouns and verbs that keep recurring** — Name 2–4 candidate
|
|
@@ -105,7 +105,7 @@ that embodies it), then `_Avoid_:` rejected synonyms — grouped under a `###` s
|
|
|
105
105
|
`CONTEXT.md` identical: rename in code, rename here in the same turn. Distilled-out concepts go under
|
|
106
106
|
`## Deferred`, unresolved ones under `## Flagged ambiguities`.
|
|
107
107
|
|
|
108
|
-
Edit the context's existing `
|
|
108
|
+
Edit the context's existing root-level `CONTEXT.md` directly — it is canonical, and is
|
|
109
109
|
also your seed (Step 1). If the module is new and has no `CONTEXT.md` yet, create one from
|
|
110
110
|
`assets/context.template.md`.
|
|
111
111
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
<One line: the slice of the domain this context covers — the flow you crunched, in the expert's words.>
|
|
4
4
|
|
|
5
|
-
> Starter for a context that has **no `CONTEXT.md` yet**. It lives at
|
|
6
|
-
> `
|
|
5
|
+
> Starter for a context that has **no `CONTEXT.md` yet**. It lives at the
|
|
6
|
+
> root folder as `CONTEXT.md`. If the context already has one, edit that — never a second.
|
|
7
7
|
|
|
8
8
|
## Language
|
|
9
9
|
|