@phuc1403/musketeer 0.5.0 → 0.6.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.
Files changed (85) hide show
  1. package/INSTALLATION.md +47 -32
  2. package/bin/musketeer.js +168 -79
  3. package/manifest.json +2 -0
  4. package/package.json +46 -46
  5. package/src/dotnet-scaffold-copier.js +72 -72
  6. package/src/provisioner/detect.js +113 -93
  7. package/src/self-update.js +77 -73
  8. package/template/.claude/hooks/block-unsafe-adr-title.cjs +72 -70
  9. package/template/.claude/hooks/init-adr-dir.cjs +153 -110
  10. package/template/.claude/hooks/inject-adr-env.cjs +83 -82
  11. package/template/.claude/hooks/lib/adr/command-scan.cjs +107 -0
  12. package/template/.claude/hooks/sync-adr-toc.cjs +111 -111
  13. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.AssemblyInfo.cs +1 -1
  14. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.AssemblyInfoInputs.cache +1 -1
  15. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.AssemblyInfo.cs +1 -1
  16. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.AssemblyInfoInputs.cache +1 -1
  17. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.AssemblyInfo.cs +1 -1
  18. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.AssemblyInfoInputs.cache +1 -1
  19. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.AssemblyInfo.cs +1 -1
  20. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.AssemblyInfoInputs.cache +1 -1
  21. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.AssemblyInfo.cs +1 -1
  22. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.AssemblyInfoInputs.cache +1 -1
  23. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.AssemblyInfo.cs +1 -1
  24. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.AssemblyInfoInputs.cache +1 -1
  25. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.AssemblyInfo.cs +1 -1
  26. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.AssemblyInfoInputs.cache +1 -1
  27. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.AssemblyInfo.cs +1 -1
  28. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.AssemblyInfoInputs.cache +1 -1
  29. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.assets.cache +0 -0
  30. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.dgspec.json +0 -1536
  31. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.g.props +0 -16
  32. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.g.targets +0 -2
  33. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/project.assets.json +0 -559
  34. package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/project.nuget.cache +0 -8
  35. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.assets.cache +0 -0
  36. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.dgspec.json +0 -690
  37. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.g.props +0 -16
  38. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.g.targets +0 -2
  39. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/project.assets.json +0 -376
  40. package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/project.nuget.cache +0 -8
  41. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.assets.cache +0 -0
  42. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.dgspec.json +0 -347
  43. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.g.props +0 -16
  44. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.g.targets +0 -2
  45. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/project.assets.json +0 -353
  46. package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/project.nuget.cache +0 -8
  47. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.assets.cache +0 -0
  48. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.csproj.AssemblyReference.cache +0 -0
  49. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.dgspec.json +0 -1047
  50. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.g.props +0 -16
  51. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.g.targets +0 -7
  52. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/project.assets.json +0 -858
  53. package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/project.nuget.cache +0 -17
  54. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Api.Tests +0 -0
  55. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.assets.cache +0 -0
  56. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.csproj.AssemblyReference.cache +0 -0
  57. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.dgspec.json +0 -1908
  58. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.g.props +0 -27
  59. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.g.targets +0 -16
  60. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/project.assets.json +0 -3283
  61. package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/project.nuget.cache +0 -60
  62. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Application.Tests +0 -0
  63. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.assets.cache +0 -0
  64. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.csproj.AssemblyReference.cache +0 -0
  65. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.dgspec.json +0 -1055
  66. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.g.props +0 -26
  67. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.g.targets +0 -11
  68. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/project.assets.json +0 -1662
  69. package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/project.nuget.cache +0 -30
  70. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Domain.Tests +0 -0
  71. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.assets.cache +0 -0
  72. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.csproj.AssemblyReference.cache +0 -0
  73. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.dgspec.json +0 -712
  74. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.g.props +0 -26
  75. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.g.targets +0 -11
  76. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/project.assets.json +0 -1644
  77. package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/project.nuget.cache +0 -30
  78. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Infrastructure.Tests +0 -0
  79. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.assets.cache +0 -0
  80. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.csproj.AssemblyReference.cache +0 -0
  81. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.dgspec.json +0 -1412
  82. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.g.props +0 -26
  83. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.g.targets +0 -13
  84. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/project.assets.json +0 -2130
  85. package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/project.nuget.cache +0 -38
@@ -1,93 +1,113 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { spawnSync } = require('child_process');
6
-
7
- // Map of secret prereq name → environment variable that satisfies it.
8
- const SECRET_ENV = { 'anthropic-api-key': 'ANTHROPIC_API_KEY' };
9
-
10
- /**
11
- * Default command runner. Captures stdout+stderr (java prints version to stderr)
12
- * and never throws. Tests inject their own `ctx.run` to avoid touching the host.
13
- * @param {string} cmd
14
- * @returns {{code:number, stdout:string, stderr:string}}
15
- */
16
- function defaultRun(cmd) {
17
- const r = spawnSync(cmd, { shell: true, encoding: 'utf8', windowsHide: true });
18
- return {
19
- code: typeof r.status === 'number' ? r.status : 1,
20
- stdout: r.stdout || '',
21
- stderr: r.stderr || '',
22
- };
23
- }
24
-
25
- /** Extract a numeric version tuple from arbitrary command output. */
26
- function parseVersion(text) {
27
- const m = String(text).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
28
- if (!m) return null;
29
- return [Number(m[1] || 0), Number(m[2] || 0), Number(m[3] || 0)];
30
- }
31
-
32
- /** found >= min ? (min may be "18" or "3.8") */
33
- function versionGte(found, min) {
34
- if (!found) return false;
35
- const want = parseVersion(min) || [0, 0, 0];
36
- for (let i = 0; i < 3; i++) {
37
- if (found[i] > want[i]) return true;
38
- if (found[i] < want[i]) return false;
39
- }
40
- return true;
41
- }
42
-
43
- /**
44
- * Detect whether a single prereq is satisfied. Detection NEVER mutates the host.
45
- * @param {string} name prereq id
46
- * @param {object} prereq manifest prereq entry
47
- * @param {object} ctx { run, platform, projectDir }
48
- * @returns {{name:string, present:boolean, version:?string, reason:string}}
49
- */
50
- function detectPrereq(name, prereq, ctx) {
51
- const run = ctx.run || defaultRun;
52
-
53
- switch (prereq.kind) {
54
- case 'secret': {
55
- const envVar = SECRET_ENV[name];
56
- const present = !!(envVar && process.env[envVar]);
57
- return { name, present, version: null, reason: present ? `${envVar} set` : `${envVar} unset` };
58
- }
59
- case 'venv': {
60
- const dir = path.join(ctx.projectDir, '.claude', 'skills', '.venv');
61
- const present = fs.existsSync(dir);
62
- return { name, present, version: null, reason: present ? 'venv exists' : 'venv missing' };
63
- }
64
- case 'mcp': {
65
- const r = run('claude mcp list');
66
- const present = r.code === 0 && /playwright/i.test(r.stdout + r.stderr);
67
- return { name, present, version: null, reason: present ? 'mcp registered' : 'mcp not registered' };
68
- }
69
- case 'note':
70
- // Nothing to probe directly; satisfied iff its `needs` are (handled by caller).
71
- return { name, present: false, version: null, reason: 'note (self-bootstrap)' };
72
- default: {
73
- // package
74
- if (!prereq.detect) return { name, present: false, version: null, reason: 'no detect command' };
75
- let r = run(prereq.detect);
76
- if (r.code !== 0 && name === 'python') r = run('python3 --version'); // unix fallback
77
- const out = r.stdout + r.stderr;
78
- const ver = parseVersion(out);
79
- if (r.code !== 0) return { name, present: false, version: null, reason: 'not found' };
80
- if (prereq.minVersion && !versionGte(ver, prereq.minVersion)) {
81
- return {
82
- name,
83
- present: false,
84
- version: ver ? ver.join('.') : null,
85
- reason: `version ${ver ? ver.join('.') : '?'} < required ${prereq.minVersion}`,
86
- };
87
- }
88
- return { name, present: true, version: ver ? ver.join('.') : null, reason: 'present' };
89
- }
90
- }
91
- }
92
-
93
- module.exports = { detectPrereq, parseVersion, versionGte, defaultRun, SECRET_ENV };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ // Map of secret prereq name → environment variable that satisfies it.
8
+ const SECRET_ENV = { 'anthropic-api-key': 'ANTHROPIC_API_KEY' };
9
+
10
+ /**
11
+ * Default command runner. Captures stdout+stderr (java prints version to stderr)
12
+ * and never throws. Tests inject their own `ctx.run` to avoid touching the host.
13
+ * @param {string} cmd
14
+ * @returns {{code:number, stdout:string, stderr:string}}
15
+ */
16
+ function defaultRun(cmd) {
17
+ // An array is an argv, spawned without a shell. Windows quoting cannot be
18
+ // trusted to survive a nested `bash -lc "..."` inside cmd.exe, so probes that
19
+ // need a shell of their own pass their arguments already separated.
20
+ const r = Array.isArray(cmd)
21
+ ? spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8', windowsHide: true })
22
+ : spawnSync(cmd, { shell: true, encoding: 'utf8', windowsHide: true });
23
+ return {
24
+ code: typeof r.status === 'number' ? r.status : 1,
25
+ stdout: r.stdout || '',
26
+ stderr: r.stderr || '',
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Probe for a prereq whose manifest entry sets `detectShell: "bash"`.
32
+ * adr-tools ships as bash scripts living in Git's own `usr/bin`, which Windows
33
+ * cannot see or execute: `adr config` fails there while working fine in Git
34
+ * Bash. Probing through the same shell the ADR hooks use stops muster reporting
35
+ * a tool that is installed as missing.
36
+ * @param {string} detect
37
+ * @returns {string[]} argv
38
+ */
39
+ function bashProbe(detect) {
40
+ return [process.env.CLAUDE_CODE_GIT_BASH_PATH || 'bash', '-lc', detect];
41
+ }
42
+
43
+ /** Extract a numeric version tuple from arbitrary command output. */
44
+ function parseVersion(text) {
45
+ const m = String(text).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
46
+ if (!m) return null;
47
+ return [Number(m[1] || 0), Number(m[2] || 0), Number(m[3] || 0)];
48
+ }
49
+
50
+ /** found >= min ? (min may be "18" or "3.8") */
51
+ function versionGte(found, min) {
52
+ if (!found) return false;
53
+ const want = parseVersion(min) || [0, 0, 0];
54
+ for (let i = 0; i < 3; i++) {
55
+ if (found[i] > want[i]) return true;
56
+ if (found[i] < want[i]) return false;
57
+ }
58
+ return true;
59
+ }
60
+
61
+ /**
62
+ * Detect whether a single prereq is satisfied. Detection NEVER mutates the host.
63
+ * @param {string} name prereq id
64
+ * @param {object} prereq manifest prereq entry
65
+ * @param {object} ctx { run, platform, projectDir }
66
+ * @returns {{name:string, present:boolean, version:?string, reason:string}}
67
+ */
68
+ function detectPrereq(name, prereq, ctx) {
69
+ const run = ctx.run || defaultRun;
70
+
71
+ switch (prereq.kind) {
72
+ case 'secret': {
73
+ const envVar = SECRET_ENV[name];
74
+ const present = !!(envVar && process.env[envVar]);
75
+ return { name, present, version: null, reason: present ? `${envVar} set` : `${envVar} unset` };
76
+ }
77
+ case 'venv': {
78
+ const dir = path.join(ctx.projectDir, '.claude', 'skills', '.venv');
79
+ const present = fs.existsSync(dir);
80
+ return { name, present, version: null, reason: present ? 'venv exists' : 'venv missing' };
81
+ }
82
+ case 'mcp': {
83
+ const r = run('claude mcp list');
84
+ const present = r.code === 0 && /playwright/i.test(r.stdout + r.stderr);
85
+ return { name, present, version: null, reason: present ? 'mcp registered' : 'mcp not registered' };
86
+ }
87
+ case 'note':
88
+ // Nothing to probe directly; satisfied iff its `needs` are (handled by caller).
89
+ return { name, present: false, version: null, reason: 'note (self-bootstrap)' };
90
+ default: {
91
+ // package
92
+ if (!prereq.detect) return { name, present: false, version: null, reason: 'no detect command' };
93
+ let r = run(prereq.detectShell === 'bash' ? bashProbe(prereq.detect) : prereq.detect);
94
+ // A shell-script tool invisible to Windows still answers through Git Bash.
95
+ if (r.code !== 0 && prereq.detectShell === 'bash') r = run(prereq.detect);
96
+ if (r.code !== 0 && name === 'python') r = run('python3 --version'); // unix fallback
97
+ const out = r.stdout + r.stderr;
98
+ const ver = parseVersion(out);
99
+ if (r.code !== 0) return { name, present: false, version: null, reason: 'not found' };
100
+ if (prereq.minVersion && !versionGte(ver, prereq.minVersion)) {
101
+ return {
102
+ name,
103
+ present: false,
104
+ version: ver ? ver.join('.') : null,
105
+ reason: `version ${ver ? ver.join('.') : '?'} < required ${prereq.minVersion}`,
106
+ };
107
+ }
108
+ return { name, present: true, version: ver ? ver.join('.') : null, reason: 'present' };
109
+ }
110
+ }
111
+ }
112
+
113
+ module.exports = { detectPrereq, parseVersion, versionGte, defaultRun, bashProbe, SECRET_ENV };
@@ -1,73 +1,77 @@
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 };
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
+ // The caller's flags come along: dropping them here would send a scripted
66
+ // `muster --keep` back to the interactive picker, which is the one place it
67
+ // cannot run.
68
+ const args = io.args || [];
69
+ const r = spawn('musketeer', ['muster', ...args], {
70
+ stdio: 'inherit',
71
+ shell: true,
72
+ env: { ...env, MUSKETEER_REEXEC: '1' },
73
+ });
74
+ return typeof r.status === 'number' ? r.status : 0;
75
+ }
76
+
77
+ module.exports = { decide, ensureLatest };
@@ -1,70 +1,72 @@
1
- #!/usr/bin/env node
2
- // PreToolUse guard: block an `adr new` title that adr-tools' own script cannot
3
- // handle safely.
4
- //
5
- // adr-new builds the ADR file with `sed -e "s|TITLE|$title|"`, substituting the
6
- // title in unescaped and using `|` as the delimiter. Two failure modes follow,
7
- // verified directly against /usr/bin/adr-new:
8
- // - `|` in the title breaks sed's own syntax: it exits 1, but only after
9
- // creating a zero-byte ADR file that already burned that number.
10
- // - `&` in a sed replacement means "the whole match" — it does NOT error.
11
- // "Use Kafka & Queues" silently becomes the title "Use Kafka TITLE Queues",
12
- // written straight into the file and the generated index.
13
- // A title starting with `-` is a third case: adr-new parses its own flags with
14
- // getopts, so without a `--` separator the title is read as an unknown option
15
- // and the ADR is silently never created (clean failure, still worth catching
16
- // early with a clearer reason than adr-tools' own "no title given").
17
- //
18
- // Blocking, not warning: `|` wastes an ADR number and `&` corrupts data with no
19
- // error at all, so there is nothing later in the pipeline that will catch it.
20
-
21
- let raw = "";
22
- process.stdin.on("data", (chunk) => (raw += chunk));
23
- process.stdin.on("end", () => {
24
- let input;
25
- try {
26
- input = JSON.parse(raw || "{}");
27
- } catch {
28
- process.exit(0); // unparseable payload — fail open, don't block legit work
29
- }
30
-
31
- const command = (input && input.tool_input && input.tool_input.command) || "";
32
- if (!/\badr\s+new\b/.test(command)) process.exit(0);
33
-
34
- // The skill always places `--` immediately before a quoted title. Extract
35
- // that argument; anything else about the command's shape is not this hook's
36
- // concern.
37
- const afterDashDash = command.match(/--\s+(["'])((?:(?!\1).)*)\1/);
38
- const title = afterDashDash ? afterDashDash[2] : null;
39
-
40
- let reason = null;
41
- if (!afterDashDash) {
42
- reason =
43
- "no `--` before the title (or no quoted title found after it). Without `--`, " +
44
- "adr-new's getopts can read a title starting with `-` as a flag and silently " +
45
- "create nothing. Always: adr new [-s STEM]... -- \"Title\".";
46
- } else if (title.includes("|")) {
47
- reason =
48
- "the title contains `|`. adr-new substitutes it into `sed -e \"s|TITLE|$title|\"`, " +
49
- "using `|` as sed's own delimiter — this breaks sed's syntax and exits non-zero, " +
50
- "but only after creating a zero-byte ADR file that already consumed that number.";
51
- } else if (title.includes("&")) {
52
- reason =
53
- "the title contains `&`. In a sed replacement `&` means \"the whole match\", so " +
54
- "adr-new writes it as literal text into the title (e.g. \"Use Kafka & Queues\" " +
55
- "becomes \"Use Kafka TITLE Queues\") and exits 0 — nothing else will catch this.";
56
- }
57
-
58
- if (reason) {
59
- process.stdout.write(
60
- JSON.stringify({
61
- hookSpecificOutput: {
62
- hookEventName: "PreToolUse",
63
- permissionDecision: "deny",
64
- permissionDecisionReason: `Refusing this \`adr new\` call: ${reason} Rename the title and retry.`,
65
- },
66
- })
67
- );
68
- }
69
- process.exit(0);
70
- });
1
+ #!/usr/bin/env node
2
+ // PreToolUse guard: block an `adr new` title that adr-tools' own script cannot
3
+ // handle safely.
4
+ //
5
+ // adr-new builds the ADR file with `sed -e "s|TITLE|$title|"`, substituting the
6
+ // title in unescaped and using `|` as the delimiter. Two failure modes follow,
7
+ // verified directly against /usr/bin/adr-new:
8
+ // - `|` in the title breaks sed's own syntax: it exits 1, but only after
9
+ // creating a zero-byte ADR file that already burned that number.
10
+ // - `&` in a sed replacement means "the whole match" — it does NOT error.
11
+ // "Use Kafka & Queues" silently becomes the title "Use Kafka TITLE Queues",
12
+ // written straight into the file and the generated index.
13
+ // A title starting with `-` is a third case: adr-new parses its own flags with
14
+ // getopts, so without a `--` separator the title is read as an unknown option
15
+ // and the ADR is silently never created (clean failure, still worth catching
16
+ // early with a clearer reason than adr-tools' own "no title given").
17
+ //
18
+ // Blocking, not warning: `|` wastes an ADR number and `&` corrupts data with no
19
+ // error at all, so there is nothing later in the pipeline that will catch it.
20
+
21
+ const { invokesAdr } = require("./lib/adr/command-scan.cjs");
22
+
23
+ let raw = "";
24
+ process.stdin.on("data", (chunk) => (raw += chunk));
25
+ process.stdin.on("end", () => {
26
+ let input;
27
+ try {
28
+ input = JSON.parse(raw || "{}");
29
+ } catch {
30
+ process.exit(0); // unparseable payload — fail open, don't block legit work
31
+ }
32
+
33
+ const command = (input && input.tool_input && input.tool_input.command) || "";
34
+ if (!invokesAdr(command, "new")) process.exit(0);
35
+
36
+ // The skill always places `--` immediately before a quoted title. Extract
37
+ // that argument; anything else about the command's shape is not this hook's
38
+ // concern.
39
+ const afterDashDash = command.match(/--\s+(["'])((?:(?!\1).)*)\1/);
40
+ const title = afterDashDash ? afterDashDash[2] : null;
41
+
42
+ let reason = null;
43
+ if (!afterDashDash) {
44
+ reason =
45
+ "no `--` before the title (or no quoted title found after it). Without `--`, " +
46
+ "adr-new's getopts can read a title starting with `-` as a flag and silently " +
47
+ "create nothing. Always: adr new [-s STEM]... -- \"Title\".";
48
+ } else if (title.includes("|")) {
49
+ reason =
50
+ "the title contains `|`. adr-new substitutes it into `sed -e \"s|TITLE|$title|\"`, " +
51
+ "using `|` as sed's own delimiter — this breaks sed's syntax and exits non-zero, " +
52
+ "but only after creating a zero-byte ADR file that already consumed that number.";
53
+ } else if (title.includes("&")) {
54
+ reason =
55
+ "the title contains `&`. In a sed replacement `&` means \"the whole match\", so " +
56
+ "adr-new writes it as literal text into the title (e.g. \"Use Kafka & Queues\" " +
57
+ "becomes \"Use Kafka TITLE Queues\") and exits 0 — nothing else will catch this.";
58
+ }
59
+
60
+ if (reason) {
61
+ process.stdout.write(
62
+ JSON.stringify({
63
+ hookSpecificOutput: {
64
+ hookEventName: "PreToolUse",
65
+ permissionDecision: "deny",
66
+ permissionDecisionReason: `Refusing this \`adr new\` call: ${reason} Rename the title and retry.`,
67
+ },
68
+ })
69
+ );
70
+ }
71
+ process.exit(0);
72
+ });