@phuc1403/musketeer 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
@@ -12,16 +12,20 @@ npm i -g @phuc1403/musketeer
12
12
  ## Use
13
13
 
14
14
  ```bash
15
- musketeer # open the muster (interactive picker) → reconcile your company
16
- musketeer --with a,b,c # non-interactive: set company to EXACTLY {core,a,b,c}
17
- musketeer --list # print current company + available musketeers
18
- musketeer --help # usage
15
+ musketeer muster # open the muster — pick your company (multi-select) → reconcile
16
+ musketeer promote # update the musketeer CLI itself to the latest published version
17
+ musketeer # show usage
18
+ musketeer --help # show usage
19
19
  ```
20
20
 
21
- `musketeer` is **idempotent**: the picker pre-checks your current company (from `.musketeer.json`),
21
+ `musketeer muster` is **idempotent**: the picker pre-checks your current company (from `.musketeer.json`),
22
22
  and confirming applies the diff — install newly-checked, remove newly-unchecked, regenerate
23
23
  `settings.json`, and auto-install any missing prerequisites.
24
24
 
25
+ `musketeer promote` upgrades the **CLI tool** itself (not your project's company): it checks the npm
26
+ registry, and if a newer version exists, runs `npm i -g @phuc1403/musketeer@latest`. _muster = which
27
+ musketeers are in this project; promote = upgrade the binary._
28
+
25
29
  ## Musketeers (v1)
26
30
 
27
31
  | Musketeer | Default | What it adds |
@@ -30,8 +34,8 @@ and confirming applies the diff — install newly-checked, remove newly-unchecke
30
34
  | **architecture** | checked | adr-writer, architecture-characteristic-writer, context-map · CML validation hook |
31
35
  | **hallmark** | checked | hallmark, hallmark-explore, hallmark-loop · auditor/explorer agents |
32
36
  | **code-review** | checked | code-review skill + code-reviewer agent |
33
- | **dotnet** | opt-in | tdd, knowledge-crunching · EF migration guard hook |
34
- | **design-docs** | opt-in | inject-design-docs SessionStart hook |
37
+ | **dotnet** | checked | tdd, knowledge-crunching · EF migration guard hook |
38
+ | **design-docs** | checked | inject-design-docs SessionStart hook |
35
39
 
36
40
  Vocabulary: **musketeer** = a selectable unit · **muster** = the picker · **company** = your recruited set.
37
41
 
package/bin/musketeer.js CHANGED
@@ -6,25 +6,25 @@ 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 Open the muster (interactive picker), reconcile your company
10
- musketeer --with a,b,c Non-interactive: set company to EXACTLY {core,a,b,c} (CI/scriptable)
11
- musketeer --list Print current company + all available musketeers
12
- musketeer --help Show this help
9
+ musketeer muster Open the muster — pick your company (multi-select), then reconcile
10
+ musketeer promote Update the musketeer CLI itself to the latest published version
11
+ musketeer Show this help
12
+ musketeer --help Show this help
13
13
 
14
14
  Notes:
15
+ * "muster" changes which musketeers are in THIS project; "promote" upgrades the CLI itself.
15
16
  * "core" is always installed and locked — it cannot be removed.
16
- * Re-running is idempotent: the picker shows your current state, confirm applies the diff.
17
+ * 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.
17
19
  * settings.json is GENERATED per-selection; missing prereqs are auto-installed (announced).`;
18
20
 
19
21
  function parse(argv) {
20
22
  return parseArgs({
21
23
  args: argv,
22
24
  options: {
23
- with: { type: 'string' },
24
- list: { type: 'boolean', default: false },
25
25
  help: { type: 'boolean', short: 'h', default: false },
26
26
  },
27
- allowPositionals: false,
27
+ allowPositionals: true,
28
28
  });
29
29
  }
30
30
 
@@ -38,32 +38,30 @@ async function main(argv = process.argv.slice(2)) {
38
38
  return;
39
39
  }
40
40
 
41
- const { values } = parsed;
41
+ const { values, positionals } = parsed;
42
+ const command = positionals[0];
42
43
 
43
- if (values.help) {
44
+ // `musketeer` alone (or --help) → usage.
45
+ if (values.help || !command) {
44
46
  process.stdout.write(`${USAGE}\n`);
45
47
  return;
46
48
  }
47
49
 
48
- // Lazy-require so --help never pays the cost of loading the engine/manifest.
49
- const reconcile = require('../src/reconcile');
50
-
51
- if (values.list) {
52
- await reconcile.list();
50
+ if (command === 'muster') {
51
+ // Lazy-require so help never pays the cost of loading the engine/manifest.
52
+ const reconcile = require('../src/reconcile');
53
+ await reconcile.run({ desiredIds: null, interactive: true });
53
54
  return;
54
55
  }
55
56
 
56
- if (typeof values.with === 'string') {
57
- const ids = values.with
58
- .split(',')
59
- .map((s) => s.trim())
60
- .filter(Boolean);
61
- await reconcile.run({ desiredIds: ids, interactive: false });
57
+ if (command === 'promote') {
58
+ const promote = require('../src/promote');
59
+ await promote.run();
62
60
  return;
63
61
  }
64
62
 
65
- // No flags → open the muster (interactive).
66
- await reconcile.run({ desiredIds: null, interactive: true });
63
+ process.stderr.write(`Unknown command "${command}".\n\n${USAGE}\n`);
64
+ process.exitCode = 2;
67
65
  }
68
66
 
69
67
  main().catch((err) => {
package/manifest.json CHANGED
@@ -99,9 +99,9 @@
99
99
  },
100
100
  "dotnet": {
101
101
  "label": "dotnet",
102
- "description": "Opt-in .NET extras: tdd, knowledge-crunching + EF migration-guard hook.",
102
+ "description": ".NET extras: tdd, knowledge-crunching + EF migration-guard hook.",
103
103
  "locked": false,
104
- "defaultChecked": false,
104
+ "defaultChecked": true,
105
105
  "deps": [],
106
106
  "files": [
107
107
  "skills/tdd/**",
@@ -121,9 +121,9 @@
121
121
  },
122
122
  "design-docs": {
123
123
  "label": "design-docs",
124
- "description": "Opt-in SessionStart hook that injects architecture/context-map/adr docs/ outputs into context.",
124
+ "description": "SessionStart hook that injects architecture/context-map/adr docs/ outputs into context.",
125
125
  "locked": false,
126
- "defaultChecked": false,
126
+ "defaultChecked": true,
127
127
  "deps": [],
128
128
  "files": [
129
129
  "hooks/inject-design-docs.cjs"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phuc1403/musketeer",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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/muster.js CHANGED
@@ -28,7 +28,7 @@ async function pick(manifest, prev) {
28
28
 
29
29
  clack.intro('musketeer — muster');
30
30
  const selected = await clack.multiselect({
31
- message: 'Select your company (core is always installed):',
31
+ message: 'Pick your company — space toggles, enter confirms (core is always included):',
32
32
  options,
33
33
  initialValues,
34
34
  required: false,
package/src/promote.js ADDED
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ // `musketeer promote` — advance the installed CLI to the latest published rank
4
+ // (version). Detect-first: query the npm registry, compare, and only install when
5
+ // the running copy is behind. Distinct from `muster` (which picks the company in a
6
+ // project); promote upgrades the musketeer binary itself.
7
+
8
+ const { spawnSync } = require('child_process');
9
+ const pkg = require('../package.json');
10
+
11
+ /**
12
+ * Default command runner — same shape/semantics as the provisioner's runner.
13
+ * Captures stdout+stderr and never throws; tests inject their own `run`.
14
+ * @param {string} cmd
15
+ * @returns {{code:number, stdout:string, stderr:string}}
16
+ */
17
+ function defaultRun(cmd) {
18
+ const r = spawnSync(cmd, { shell: true, encoding: 'utf8', windowsHide: true });
19
+ return {
20
+ code: typeof r.status === 'number' ? r.status : 1,
21
+ stdout: r.stdout || '',
22
+ stderr: r.stderr || '',
23
+ };
24
+ }
25
+
26
+ /** Compare dotted numeric versions: 1 if a>b, -1 if a<b, 0 if equal. */
27
+ function compareVersions(a, b) {
28
+ const pa = String(a).split('.').map(Number);
29
+ const pb = String(b).split('.').map(Number);
30
+ for (let i = 0; i < 3; i++) {
31
+ const x = pa[i] || 0;
32
+ const y = pb[i] || 0;
33
+ if (x > y) return 1;
34
+ if (x < y) return -1;
35
+ }
36
+ return 0;
37
+ }
38
+
39
+ /**
40
+ * Run the self-update. Detect latest from the registry, compare to the running
41
+ * version, install only when behind. Partial-failure tolerant — prints a manual
42
+ * fallback rather than throwing.
43
+ * @param {object} [overrides] { run, log, name, current } for hermetic tests
44
+ * @returns {Promise<{status:'up-to-date'|'promoted'|'failed'|'unknown', from:string, to:?string}>}
45
+ */
46
+ async function run(overrides = {}) {
47
+ const exec = overrides.run || defaultRun;
48
+ const log = overrides.log || ((m) => process.stdout.write(m + '\n'));
49
+ const name = overrides.name || pkg.name;
50
+ const current = overrides.current || pkg.version;
51
+
52
+ log(`Checking rank… installed ${current} (${name})`);
53
+ const view = exec(`npm view ${name} version`);
54
+ const latest = (view.stdout || '').trim();
55
+
56
+ if (view.code !== 0 || !/^\d+\.\d+\.\d+/.test(latest)) {
57
+ log('Could not reach the npm registry to check for a newer version.');
58
+ log(`Try manually: npm i -g ${name}@latest`);
59
+ return { status: 'unknown', from: current, to: null };
60
+ }
61
+
62
+ log(` installed ${current} · latest ${latest}`);
63
+ if (compareVersions(latest, current) <= 0) {
64
+ log('Already at the latest rank — no promotion needed.');
65
+ return { status: 'up-to-date', from: current, to: latest };
66
+ }
67
+
68
+ log(`→ npm i -g ${name}@latest`);
69
+ const ins = exec(`npm i -g ${name}@latest`);
70
+ if (ins.code !== 0) {
71
+ log('Promotion failed. Try manually (you may need elevated permissions):');
72
+ log(` npm i -g ${name}@latest`);
73
+ if (ins.stderr) log(ins.stderr.trim());
74
+ return { status: 'failed', from: current, to: latest };
75
+ }
76
+
77
+ log(`Promoted: now at ${latest}.`);
78
+ return { status: 'promoted', from: current, to: latest };
79
+ }
80
+
81
+ module.exports = { run, compareVersions, defaultRun };
package/src/reconcile.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { loadManifest, templateClaudeDir } = require('./manifest-loader');
4
- const { resolveClosure, CORE_ID } = require('./resolver');
4
+ const { resolveClosure } = require('./resolver');
5
5
  const { readLockfile, writeLockfile } = require('./lockfile');
6
6
  const copier = require('./copier');
7
7
  const settingsMerger = require('./settings-merger');
@@ -73,22 +73,4 @@ async function run(opts, overrides = {}) {
73
73
  d.log('Done. Your .claude/ company is reconciled.');
74
74
  }
75
75
 
76
- /**
77
- * Print current company + all available musketeers (no UI).
78
- */
79
- async function list(opts = {}, overrides = {}) {
80
- const projectDir = opts.projectDir || process.cwd();
81
- const d = { ...defaultDeps(), ...overrides };
82
- const prev = readLockfile(projectDir);
83
- const company = new Set(prev.musketeers.length ? prev.musketeers : []);
84
-
85
- d.log(`Current company: ${prev.musketeers.length ? prev.musketeers.join(', ') : '(none installed yet)'}`);
86
- d.log('\nAvailable musketeers:');
87
- for (const [id, m] of Object.entries(d.manifest.musketeers)) {
88
- const mark = company.has(id) || id === CORE_ID ? '[x]' : '[ ]';
89
- const lock = m.locked ? ' (locked)' : m.defaultChecked ? '' : ' (opt-in)';
90
- d.log(` ${mark} ${id}${lock} — ${m.description}`);
91
- }
92
- }
93
-
94
- module.exports = { run, list, defaultDeps };
76
+ module.exports = { run, defaultDeps };