@phuc1403/musketeer 0.2.2 → 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 CHANGED
@@ -12,15 +12,19 @@ npm i -g @phuc1403/musketeer
12
12
  ## Use
13
13
 
14
14
  ```bash
15
- musketeer muster # open the muster — pick your company (multi-select) → reconcile
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` is **idempotent**: the picker pre-checks your current company (from `.musketeer.json`),
22
- and confirming applies the diff install newly-checked, remove newly-unchecked, regenerate
23
- `settings.json`, and auto-install any missing prerequisites.
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 Open the muster pick your company (multi-select), then reconcile
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 hook.",
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.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 view = exec(`npm view ${name} version`);
54
- const latest = (view.stdout || '').trim();
66
+ const latest = fetchLatest(exec, name);
55
67
 
56
- if (view.code !== 0 || !/^\d+\.\d+\.\d+/.test(latest)) {
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} managed, ${plan.remove.length} to remove.`);
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
+ }