@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.
- package/INSTALLATION.md +47 -32
- package/bin/musketeer.js +168 -79
- package/manifest.json +2 -0
- package/package.json +46 -46
- package/src/dotnet-scaffold-copier.js +72 -72
- package/src/provisioner/detect.js +113 -93
- package/src/self-update.js +77 -73
- package/template/.claude/hooks/block-unsafe-adr-title.cjs +72 -70
- package/template/.claude/hooks/init-adr-dir.cjs +153 -110
- package/template/.claude/hooks/inject-adr-env.cjs +83 -82
- package/template/.claude/hooks/lib/adr/command-scan.cjs +107 -0
- package/template/.claude/hooks/sync-adr-toc.cjs +111 -111
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.AssemblyInfo.cs +1 -1
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.AssemblyInfoInputs.cache +1 -1
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/Debug/net10.0/__SolutionName__.Api.assets.cache +0 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.dgspec.json +0 -1536
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.g.props +0 -16
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/__SolutionName__.Api.csproj.nuget.g.targets +0 -2
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/project.assets.json +0 -559
- package/template/dotnet-scaffold/src/__SolutionName__.Api/obj/project.nuget.cache +0 -8
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/Debug/net10.0/__SolutionName__.Application.assets.cache +0 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.dgspec.json +0 -690
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.g.props +0 -16
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/__SolutionName__.Application.csproj.nuget.g.targets +0 -2
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/project.assets.json +0 -376
- package/template/dotnet-scaffold/src/__SolutionName__.Application/obj/project.nuget.cache +0 -8
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/Debug/net10.0/__SolutionName__.Domain.assets.cache +0 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.dgspec.json +0 -347
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.g.props +0 -16
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/__SolutionName__.Domain.csproj.nuget.g.targets +0 -2
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/project.assets.json +0 -353
- package/template/dotnet-scaffold/src/__SolutionName__.Domain/obj/project.nuget.cache +0 -8
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.assets.cache +0 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/Debug/net10.0/__SolutionName__.Infrastructure.csproj.AssemblyReference.cache +0 -0
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.dgspec.json +0 -1047
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.g.props +0 -16
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/__SolutionName__.Infrastructure.csproj.nuget.g.targets +0 -7
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/project.assets.json +0 -858
- package/template/dotnet-scaffold/src/__SolutionName__.Infrastructure/obj/project.nuget.cache +0 -17
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Api.Tests +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.assets.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/Debug/net10.0/__SolutionName__.Api.Tests.csproj.AssemblyReference.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.dgspec.json +0 -1908
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.g.props +0 -27
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/__SolutionName__.Api.Tests.csproj.nuget.g.targets +0 -16
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/project.assets.json +0 -3283
- package/template/dotnet-scaffold/tests/__SolutionName__.Api.Tests/obj/project.nuget.cache +0 -60
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Application.Tests +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.assets.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/Debug/net10.0/__SolutionName__.Application.Tests.csproj.AssemblyReference.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.dgspec.json +0 -1055
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.g.props +0 -26
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/__SolutionName__.Application.Tests.csproj.nuget.g.targets +0 -11
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/project.assets.json +0 -1662
- package/template/dotnet-scaffold/tests/__SolutionName__.Application.Tests/obj/project.nuget.cache +0 -30
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Domain.Tests +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.assets.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/Debug/net10.0/__SolutionName__.Domain.Tests.csproj.AssemblyReference.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.dgspec.json +0 -712
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.g.props +0 -26
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/__SolutionName__.Domain.Tests.csproj.nuget.g.targets +0 -11
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/project.assets.json +0 -1644
- package/template/dotnet-scaffold/tests/__SolutionName__.Domain.Tests/obj/project.nuget.cache +0 -30
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/bin/Debug/net10.0/.msCoverageSourceRootsMapping___SolutionName__.Infrastructure.Tests +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.assets.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/Debug/net10.0/__SolutionName__.Infrastructure.Tests.csproj.AssemblyReference.cache +0 -0
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.dgspec.json +0 -1412
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.g.props +0 -26
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/__SolutionName__.Infrastructure.Tests.csproj.nuget.g.targets +0 -13
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/project.assets.json +0 -2130
- package/template/dotnet-scaffold/tests/__SolutionName__.Infrastructure.Tests/obj/project.nuget.cache +0 -38
|
@@ -1,110 +1,153 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// PreToolUse guard: set up `.adr-dir` before the first `adr new` in a project,
|
|
3
|
-
// so the agent never has to pick between `adr init` and a hand-written file.
|
|
4
|
-
//
|
|
5
|
-
// Two setup paths, and picking the wrong one is destructive:
|
|
6
|
-
// - fresh (no `.adr-dir`, no numbered ADRs yet): `adr init docs/adr` is safe —
|
|
7
|
-
// it creates docs/adr/, writes `.adr-dir`, and adds a baseline ADR.
|
|
8
|
-
// - migration (numbered ADRs already exist, but no `.adr-dir`): `adr init`
|
|
9
|
-
// would ALSO add that baseline ADR, burning the next real number. The fix
|
|
10
|
-
// is a plain `.adr-dir` file with no tool call: `printf 'docs/adr\n' > .adr-dir`.
|
|
11
|
-
// Already initialized: nothing to do, both paths are a no-op.
|
|
12
|
-
//
|
|
13
|
-
// Runs the setup itself (as a side effect) before allowing `adr new` through,
|
|
14
|
-
// and denies a *direct* `adr init` call whenever running it would be wrong —
|
|
15
|
-
// already initialized, or migration state — since either would burn a number.
|
|
16
|
-
// Anything it does is echoed to stdout so it stays visible in the transcript.
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const { spawnSync } = require('child_process');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
process.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse guard: set up `.adr-dir` before the first `adr new` in a project,
|
|
3
|
+
// so the agent never has to pick between `adr init` and a hand-written file.
|
|
4
|
+
//
|
|
5
|
+
// Two setup paths, and picking the wrong one is destructive:
|
|
6
|
+
// - fresh (no `.adr-dir`, no numbered ADRs yet): `adr init docs/adr` is safe —
|
|
7
|
+
// it creates docs/adr/, writes `.adr-dir`, and adds a baseline ADR.
|
|
8
|
+
// - migration (numbered ADRs already exist, but no `.adr-dir`): `adr init`
|
|
9
|
+
// would ALSO add that baseline ADR, burning the next real number. The fix
|
|
10
|
+
// is a plain `.adr-dir` file with no tool call: `printf 'docs/adr\n' > .adr-dir`.
|
|
11
|
+
// Already initialized: nothing to do, both paths are a no-op.
|
|
12
|
+
//
|
|
13
|
+
// Runs the setup itself (as a side effect) before allowing `adr new` through,
|
|
14
|
+
// and denies a *direct* `adr init` call whenever running it would be wrong —
|
|
15
|
+
// already initialized, or migration state — since either would burn a number.
|
|
16
|
+
// Anything it does is echoed to stdout so it stays visible in the transcript.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { spawnSync } = require('child_process');
|
|
21
|
+
const { invokesAdr } = require('./lib/adr/command-scan.cjs');
|
|
22
|
+
|
|
23
|
+
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
24
|
+
const ADR_DIR = 'docs/adr';
|
|
25
|
+
const ADR_DIR_FILE = path.join(root, '.adr-dir');
|
|
26
|
+
const ADR_FILE = /^\d+-.*\.md$/;
|
|
27
|
+
|
|
28
|
+
function allow(message) {
|
|
29
|
+
if (message) process.stdout.write(message + '\n');
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deny(reason) {
|
|
34
|
+
process.stdout.write(
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
hookSpecificOutput: {
|
|
37
|
+
hookEventName: 'PreToolUse',
|
|
38
|
+
permissionDecision: 'deny',
|
|
39
|
+
permissionDecisionReason: reason,
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readAdrDir() {
|
|
47
|
+
try {
|
|
48
|
+
return fs.readFileSync(ADR_DIR_FILE, 'utf8').trim() || null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isInitialized() {
|
|
55
|
+
try {
|
|
56
|
+
return fs.readFileSync(ADR_DIR_FILE, 'utf8').trim() !== '';
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasNumberedAdrs() {
|
|
63
|
+
try {
|
|
64
|
+
return fs.readdirSync(path.join(root, ADR_DIR)).some((f) => ADR_FILE.test(f));
|
|
65
|
+
} catch {
|
|
66
|
+
return false; // directory doesn't exist yet -> nothing to migrate
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function runAdrInit() {
|
|
71
|
+
const shell = process.env.CLAUDE_CODE_GIT_BASH_PATH || 'bash';
|
|
72
|
+
return spawnSync(shell, ['-c', `adr init ${ADR_DIR}`], { cwd: root, encoding: 'utf8' });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Where adr-tools itself thinks the ADRs go. `_adr_dir` is the resolver every
|
|
76
|
+
// adr subcommand uses, so asking it directly is the only honest answer.
|
|
77
|
+
function resolvedAdrDir() {
|
|
78
|
+
const shell = process.env.CLAUDE_CODE_GIT_BASH_PATH || 'bash';
|
|
79
|
+
const res = spawnSync(shell, ['-c', '_adr_dir'], { cwd: root, encoding: 'utf8' });
|
|
80
|
+
if (res.error || res.status !== 0) return null;
|
|
81
|
+
const out = (res.stdout || '').trim();
|
|
82
|
+
return out || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// adr-tools resolves the directory with an unquoted `[` test, so a path holding
|
|
86
|
+
// a space splits into too many arguments, the search loop is skipped, and the
|
|
87
|
+
// script falls through to its `doc/adr` default. It prints the wrong answer on
|
|
88
|
+
// stdout and exits 0, so the ADR lands in a directory nothing else looks at and
|
|
89
|
+
// nothing reports a problem. Refuse the call instead of losing the record.
|
|
90
|
+
function denyOnDirMismatch() {
|
|
91
|
+
const declared = readAdrDir();
|
|
92
|
+
const resolved = resolvedAdrDir();
|
|
93
|
+
if (!declared || !resolved || declared === resolved) return;
|
|
94
|
+
|
|
95
|
+
deny(
|
|
96
|
+
`adr-tools resolves the ADR directory to "${resolved}", but .adr-dir says "${declared}". ` +
|
|
97
|
+
`The new ADR would be written to "${resolved}", where the index hook never looks. ` +
|
|
98
|
+
(/\s/.test(root)
|
|
99
|
+
? `This project's path contains a space (${root}), which is the known cause: ` +
|
|
100
|
+
"adr-tools' `_adr_dir` leaves its path test unquoted. "
|
|
101
|
+
: '') +
|
|
102
|
+
'Fix the quoting in adr-tools\' `_adr_dir`, or move the project to a path with no spaces.'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function main() {
|
|
107
|
+
const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
108
|
+
const command = payload?.tool_input?.command || '';
|
|
109
|
+
const isInit = invokesAdr(command, 'init');
|
|
110
|
+
const isNew = invokesAdr(command, 'new');
|
|
111
|
+
if (!isInit && !isNew) return allow();
|
|
112
|
+
|
|
113
|
+
if (isInitialized()) {
|
|
114
|
+
if (isInit) {
|
|
115
|
+
return deny(
|
|
116
|
+
'.adr-dir already exists — do not run `adr init` again, it would add a duplicate ' +
|
|
117
|
+
'baseline ADR and burn the next number. Run `adr new` directly.'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
denyOnDirMismatch(); // exits when adr-tools would write somewhere else
|
|
121
|
+
return allow(); // adr new, already set up: nothing to do
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (hasNumberedAdrs()) {
|
|
125
|
+
// Migration: numbered ADRs exist but `.adr-dir` does not. `adr init` would
|
|
126
|
+
// still add a baseline ADR here, so it is never the right command.
|
|
127
|
+
if (isInit) {
|
|
128
|
+
return deny(
|
|
129
|
+
'Numbered ADRs already exist without `.adr-dir` — this is a migration, not a fresh ' +
|
|
130
|
+
'project. `adr init` would add a duplicate baseline ADR and burn the next number. ' +
|
|
131
|
+
'Run `adr new` directly; `.adr-dir` is set up automatically.'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
fs.writeFileSync(ADR_DIR_FILE, `${ADR_DIR}\n`);
|
|
135
|
+
denyOnDirMismatch();
|
|
136
|
+
return allow(`Migration detected: wrote .adr-dir (${ADR_DIR}) by hand, no baseline ADR added.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fresh project.
|
|
140
|
+
if (isInit) return allow(); // the agent's own `adr init` call is correct here — let it run
|
|
141
|
+
const res = runAdrInit();
|
|
142
|
+
if (res.error || res.status !== 0) {
|
|
143
|
+
return allow(`Could not run \`adr init ${ADR_DIR}\` (${res.error?.message || res.stderr}); letting the original command run and fail with its own error.`);
|
|
144
|
+
}
|
|
145
|
+
denyOnDirMismatch();
|
|
146
|
+
return allow(`Fresh project: ran \`adr init ${ADR_DIR}\` (creates the baseline ADR).`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
main();
|
|
151
|
+
} catch {
|
|
152
|
+
allow(); // fail open
|
|
153
|
+
}
|
|
@@ -1,82 +1,83 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// PreToolUse guard: inject the env vars every `adr new` call needs, so the
|
|
3
|
-
// agent can write a bare `adr new -- "Title"` and get a correct command.
|
|
4
|
-
//
|
|
5
|
-
// VISUAL=true EDITOR=true — without them an ambient EDITOR opens an
|
|
6
|
-
// interactive editor and hangs the session (adr-new always shells out to
|
|
7
|
-
// $VISUAL/$EDITOR to "edit" the new file).
|
|
8
|
-
// ADR_TEMPLATE=...adr-template.md — points adr-new at this skill's template
|
|
9
|
-
// instead of adr-tools' own bundled default.
|
|
10
|
-
//
|
|
11
|
-
// Uses PreToolUse's `updatedInput` (not just allow/deny) to rewrite the
|
|
12
|
-
// command before it runs. Only fills in vars that are missing — an assignment
|
|
13
|
-
// the agent already wrote (any value, even a deliberately different template)
|
|
14
|
-
// is left alone. Silent no-op if all three are already present.
|
|
15
|
-
//
|
|
16
|
-
// Safety: a title that happens to contain the literal text "adr new" could
|
|
17
|
-
// make the matching regex fire inside a quoted string. Guarded by counting
|
|
18
|
-
// quote characters before each match — an odd count means "inside an open
|
|
19
|
-
// string", and that occurrence is left untouched rather than risk corrupting
|
|
20
|
-
// the title.
|
|
21
|
-
|
|
22
|
-
const fs = require('fs');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse guard: inject the env vars every `adr new` call needs, so the
|
|
3
|
+
// agent can write a bare `adr new -- "Title"` and get a correct command.
|
|
4
|
+
//
|
|
5
|
+
// VISUAL=true EDITOR=true — without them an ambient EDITOR opens an
|
|
6
|
+
// interactive editor and hangs the session (adr-new always shells out to
|
|
7
|
+
// $VISUAL/$EDITOR to "edit" the new file).
|
|
8
|
+
// ADR_TEMPLATE=...adr-template.md — points adr-new at this skill's template
|
|
9
|
+
// instead of adr-tools' own bundled default.
|
|
10
|
+
//
|
|
11
|
+
// Uses PreToolUse's `updatedInput` (not just allow/deny) to rewrite the
|
|
12
|
+
// command before it runs. Only fills in vars that are missing — an assignment
|
|
13
|
+
// the agent already wrote (any value, even a deliberately different template)
|
|
14
|
+
// is left alone. Silent no-op if all three are already present.
|
|
15
|
+
//
|
|
16
|
+
// Safety: a title that happens to contain the literal text "adr new" could
|
|
17
|
+
// make the matching regex fire inside a quoted string. Guarded by counting
|
|
18
|
+
// quote characters before each match — an odd count means "inside an open
|
|
19
|
+
// string", and that occurrence is left untouched rather than risk corrupting
|
|
20
|
+
// the title.
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const {
|
|
24
|
+
invocationRe,
|
|
25
|
+
isInsideQuotes,
|
|
26
|
+
isInsideHeredoc,
|
|
27
|
+
invokesAdr,
|
|
28
|
+
} = require('./lib/adr/command-scan.cjs');
|
|
29
|
+
|
|
30
|
+
const DEFAULTS = [
|
|
31
|
+
['ADR_TEMPLATE', '.claude/skills/adr-writer/references/adr-template.md'],
|
|
32
|
+
['VISUAL', 'true'],
|
|
33
|
+
['EDITOR', 'true'],
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function existingVars(prefix) {
|
|
37
|
+
const names = new Set();
|
|
38
|
+
const re = /([A-Za-z_][A-Za-z0-9_]*)=/g;
|
|
39
|
+
let m;
|
|
40
|
+
while ((m = re.exec(prefix))) names.add(m[1]);
|
|
41
|
+
return names;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function main() {
|
|
45
|
+
const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
46
|
+
const command = payload?.tool_input?.command || '';
|
|
47
|
+
if (!invokesAdr(command, 'new')) return;
|
|
48
|
+
|
|
49
|
+
let changed = false;
|
|
50
|
+
const updated = command.replace(invocationRe('new'), (whole, boundary, ws, existingPrefix, adrNew, offset) => {
|
|
51
|
+
// Both guards, for the same reason the deny hooks use them: a heredoc body
|
|
52
|
+
// is documentation being written, and injecting env vars into it would
|
|
53
|
+
// rewrite the file's contents rather than the command.
|
|
54
|
+
// `offset` is the boundary character; step past it to the word `adr`.
|
|
55
|
+
const at = offset + boundary.length + ws.length + existingPrefix.length;
|
|
56
|
+
if (isInsideQuotes(command, at) || isInsideHeredoc(command, at)) return whole;
|
|
57
|
+
const present = existingVars(existingPrefix);
|
|
58
|
+
const missing = DEFAULTS.filter(([name]) => !present.has(name));
|
|
59
|
+
if (missing.length === 0) return whole;
|
|
60
|
+
changed = true;
|
|
61
|
+
const injected = missing.map(([name, value]) => `${name}=${value}`).join(' ');
|
|
62
|
+
return `${boundary}${ws}${injected} ${existingPrefix}${adrNew}`;
|
|
63
|
+
});
|
|
64
|
+
if (!changed) return;
|
|
65
|
+
|
|
66
|
+
process.stdout.write(
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
systemMessage: 'Added missing VISUAL/EDITOR/ADR_TEMPLATE env vars before `adr new`.',
|
|
69
|
+
hookSpecificOutput: {
|
|
70
|
+
hookEventName: 'PreToolUse',
|
|
71
|
+
permissionDecision: 'allow',
|
|
72
|
+
updatedInput: { command: updated },
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
main();
|
|
80
|
+
} catch {
|
|
81
|
+
/* fail open: leave the command untouched */
|
|
82
|
+
}
|
|
83
|
+
process.exit(0);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Tells a real `adr` invocation apart from a command that merely mentions one.
|
|
4
|
+
//
|
|
5
|
+
// Three PreToolUse hooks key off `adr new` and `adr init`. Matching those words
|
|
6
|
+
// anywhere in the command string is too loose: a commit message explaining ADR
|
|
7
|
+
// work, a grep for the phrase, or a heredoc of documentation all contain them
|
|
8
|
+
// and none of them run the tool. Denying those commands leaves the user staring
|
|
9
|
+
// at an error about a command they never wrote.
|
|
10
|
+
//
|
|
11
|
+
// Three tests must all pass before an occurrence counts:
|
|
12
|
+
//
|
|
13
|
+
// 1. It sits at a command position — the start of the string, or just after a
|
|
14
|
+
// separator such as `;` `&&` `||` `|` `(` or a newline — allowing for any
|
|
15
|
+
// KEY=value assignments in front of it.
|
|
16
|
+
// 2. It is not inside an open quote.
|
|
17
|
+
// 3. It is not inside a heredoc body, which is data being written rather than
|
|
18
|
+
// a command being run.
|
|
19
|
+
//
|
|
20
|
+
// Where the two disagree with each other, the answer is "not an invocation".
|
|
21
|
+
// A missed invocation makes a hook skip work the user will notice at once; a
|
|
22
|
+
// false one blocks an unrelated command for a reason nobody can guess.
|
|
23
|
+
|
|
24
|
+
// boundary/start, whitespace, any existing KEY=value prefix, then `adr <sub>`.
|
|
25
|
+
function invocationRe(sub) {
|
|
26
|
+
return new RegExp(
|
|
27
|
+
`(^|[;&|\\n(){}])([ \\t]*)((?:[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\\S*)[ \\t]+)*)(adr[ \\t]+${sub}\\b)`,
|
|
28
|
+
'g'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// An odd number of quote characters before this point means the point is inside
|
|
33
|
+
// a string that is still open.
|
|
34
|
+
function isInsideQuotes(command, index) {
|
|
35
|
+
const before = command.slice(0, index);
|
|
36
|
+
const dq = (before.match(/"/g) || []).length;
|
|
37
|
+
const sq = (before.match(/'/g) || []).length;
|
|
38
|
+
return dq % 2 === 1 || sq % 2 === 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
42
|
+
|
|
43
|
+
// `<<TAG`, `<<-TAG`, `<<'TAG'`, `<<"TAG"`. `<<<` is a herestring, not a heredoc,
|
|
44
|
+
// and does not match here because `<` is neither a quote nor a word character.
|
|
45
|
+
const HEREDOC_OPEN = /<<(-?)[ \t]*(?:'([^']+)'|"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))/g;
|
|
46
|
+
|
|
47
|
+
// Offset ranges covering heredoc bodies.
|
|
48
|
+
//
|
|
49
|
+
// A heredoc body is data being written, not commands being run. Writing install
|
|
50
|
+
// docs that show `adr new -- "Use X"` is an ordinary thing to do in this repo,
|
|
51
|
+
// and the line sits at the start of its own line with balanced quotes in front
|
|
52
|
+
// of it, so both of the other tests would wave it through.
|
|
53
|
+
//
|
|
54
|
+
// Known limit: several heredocs opened on one line share the first body start,
|
|
55
|
+
// so the ranges merge. That errs toward treating more text as data, which only
|
|
56
|
+
// ever makes this scanner quieter.
|
|
57
|
+
function heredocRanges(command) {
|
|
58
|
+
const ranges = [];
|
|
59
|
+
const re = new RegExp(HEREDOC_OPEN.source, 'g');
|
|
60
|
+
let m;
|
|
61
|
+
|
|
62
|
+
while ((m = re.exec(command))) {
|
|
63
|
+
const tag = m[2] || m[3] || m[4];
|
|
64
|
+
const newline = command.indexOf('\n', m.index);
|
|
65
|
+
if (newline === -1) continue; // an opener with no body on this command
|
|
66
|
+
|
|
67
|
+
const start = newline + 1;
|
|
68
|
+
// `<<-` lets the terminator be indented with tabs.
|
|
69
|
+
const terminator = new RegExp(`^${m[1] === '-' ? '[ \\t]*' : ''}${escapeRe(tag)}[ \\t]*$`, 'm');
|
|
70
|
+
const hit = command.slice(start).match(terminator);
|
|
71
|
+
ranges.push([start, hit ? start + hit.index : command.length]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return ranges;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isInsideHeredoc(command, index) {
|
|
78
|
+
return heredocRanges(command).some(([start, end]) => index >= start && index < end);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Where the word `adr` itself starts. The match begins at the boundary
|
|
82
|
+
// character before it, which is close enough for counting quotes but one
|
|
83
|
+
// position short of the heredoc body it may sit in.
|
|
84
|
+
function adrOffsetOf(match) {
|
|
85
|
+
return match.index + match[1].length + match[2].length + match[3].length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// True when the command really runs `adr <sub>` at least once.
|
|
89
|
+
function invokesAdr(command, sub) {
|
|
90
|
+
const re = invocationRe(sub);
|
|
91
|
+
const bodies = heredocRanges(command);
|
|
92
|
+
const inBody = (i) => bodies.some(([start, end]) => i >= start && i < end);
|
|
93
|
+
let m;
|
|
94
|
+
while ((m = re.exec(command))) {
|
|
95
|
+
const at = adrOffsetOf(m);
|
|
96
|
+
if (!isInsideQuotes(command, at) && !inBody(at)) return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
invokesAdr,
|
|
103
|
+
isInsideQuotes,
|
|
104
|
+
isInsideHeredoc,
|
|
105
|
+
heredocRanges,
|
|
106
|
+
invocationRe,
|
|
107
|
+
};
|