@webpieces/ai-hook-rules 0.3.191 → 0.3.192
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/bin/wp-ai-guards-hook.js +16 -0
- package/bin/{wp-setup-global-ai-hooks.js → wp-ai-rules-hook.js} +1 -1
- package/package.json +6 -3
- package/src/adapters/claude-code-hook.js +8 -117
- package/src/adapters/claude-code-hook.js.map +1 -1
- package/src/adapters/guards-hook.js +13 -0
- package/src/adapters/guards-hook.js.map +1 -0
- package/src/adapters/hook-core.d.ts +7 -0
- package/src/adapters/hook-core.js +140 -0
- package/src/adapters/hook-core.js.map +1 -0
- package/src/adapters/rules-hook.d.ts +1 -0
- package/src/adapters/rules-hook.js +13 -0
- package/src/adapters/rules-hook.js.map +1 -0
- package/src/bin/setup.d.ts +52 -1
- package/src/bin/setup.js +259 -87
- package/src/bin/setup.js.map +1 -1
- package/src/core/rules/merge-in-progress-guard.d.ts +3 -2
- package/src/core/rules/merge-in-progress-guard.js +18 -11
- package/src/core/rules/merge-in-progress-guard.js.map +1 -1
- package/templates/claude-settings-hook.json +12 -2
- package/src/adapters/global-hook.d.ts +0 -1
- package/src/adapters/global-hook.js +0 -114
- package/src/adapters/global-hook.js.map +0 -1
- package/src/bin/global-setup.js +0 -85
- package/src/bin/global-setup.js.map +0 -1
- /package/src/{bin/global-setup.d.ts → adapters/guards-hook.d.ts} +0 -0
package/src/bin/setup.js
CHANGED
|
@@ -1,118 +1,290 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GUARDS_HOOK = exports.RULES_HOOK = void 0;
|
|
4
|
+
exports.installTargets = installTargets;
|
|
5
|
+
exports.migrate = migrate;
|
|
6
|
+
exports.readSettings = readSettings;
|
|
7
|
+
exports.hasHook = hasHook;
|
|
8
|
+
exports.applyHook = applyHook;
|
|
3
9
|
exports.main = main;
|
|
4
10
|
const tslib_1 = require("tslib");
|
|
5
11
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
12
|
const path = tslib_1.__importStar(require("path"));
|
|
7
|
-
const
|
|
8
|
-
const
|
|
13
|
+
const os_1 = require("os");
|
|
14
|
+
const readline_1 = require("readline");
|
|
15
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
16
|
+
const to_error_1 = require("../core/to-error");
|
|
9
17
|
const CONFIG_FILENAME = 'webpieces.config.json';
|
|
10
|
-
|
|
18
|
+
const DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';
|
|
19
|
+
const DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';
|
|
20
|
+
const DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// The two independently-installable hooks. Each can land in a different settings
|
|
23
|
+
// file (see InstallTarget) so a team can ship the guards while a developer keeps
|
|
24
|
+
// the code-style rules local while iterating.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
class HookSpec {
|
|
27
|
+
key;
|
|
28
|
+
label;
|
|
29
|
+
matcher;
|
|
30
|
+
bin;
|
|
31
|
+
constructor(key, label, matcher, bin) {
|
|
32
|
+
this.key = key;
|
|
33
|
+
this.label = label;
|
|
34
|
+
this.matcher = matcher;
|
|
35
|
+
this.bin = bin;
|
|
36
|
+
}
|
|
37
|
+
// Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.
|
|
38
|
+
commandFor(target, projectRoot) {
|
|
39
|
+
if (target.absolute) {
|
|
40
|
+
return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;
|
|
41
|
+
}
|
|
42
|
+
return `./node_modules/.bin/${this.bin}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
class InstallTarget {
|
|
46
|
+
choice;
|
|
47
|
+
label;
|
|
48
|
+
settingsPath;
|
|
49
|
+
absolute;
|
|
50
|
+
constructor(choice, label, settingsPath, absolute) {
|
|
51
|
+
this.choice = choice;
|
|
52
|
+
this.label = label;
|
|
53
|
+
this.settingsPath = settingsPath;
|
|
54
|
+
this.absolute = absolute;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', 'Write|Edit|MultiEdit', 'wp-ai-rules-hook');
|
|
58
|
+
exports.GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', 'Bash', 'wp-ai-guards-hook');
|
|
59
|
+
// `homeDir` is injectable so tests can point the global target at a temp dir instead of the real
|
|
60
|
+
// ~/.claude/settings.json (a unit test must never write the user's actual global settings).
|
|
61
|
+
function installTargets(projectRoot, homeDir = (0, os_1.homedir)()) {
|
|
62
|
+
return [
|
|
63
|
+
new InstallTarget('1', 'project (.claude/settings.json — committed, for the team)', path.join(projectRoot, '.claude', 'settings.json'), false),
|
|
64
|
+
new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)', path.join(projectRoot, '.claude', 'settings.local.json'), false),
|
|
65
|
+
new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)', path.join(homeDir, '.claude', 'settings.json'), true),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
11
68
|
function seedRule() {
|
|
12
69
|
return { mode: 'OFF', ignoreModifiedUntilEpoch: 0 };
|
|
13
70
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
71
|
+
function seedCommands() {
|
|
72
|
+
return {
|
|
73
|
+
'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },
|
|
74
|
+
upsertPr: DEFAULT_UPSERT_PR,
|
|
75
|
+
mergeComplete: DEFAULT_MERGE_COMPLETE,
|
|
76
|
+
};
|
|
18
77
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (!fs.existsSync(claudeDir)) {
|
|
28
|
-
console.log(' [ai-hook-rules] No .claude/ directory found — add the hook manually:');
|
|
29
|
-
console.log(' In .claude/settings.json under hooks.PreToolUse, add:');
|
|
30
|
-
console.log(` { "matcher": "Write|Edit|MultiEdit|Bash", "hooks": [{ "type": "command", "command": "${HOOK_COMMAND}" }] }`);
|
|
31
|
-
return;
|
|
78
|
+
function buildSeedConfig() {
|
|
79
|
+
const rules = {};
|
|
80
|
+
const hookGuards = {};
|
|
81
|
+
for (const name of (0, rules_config_1.allRuleNames)()) {
|
|
82
|
+
if ((0, rules_config_1.sectionForRule)(name) === 'hookGuards')
|
|
83
|
+
hookGuards[name] = seedRule();
|
|
84
|
+
else
|
|
85
|
+
rules[name] = seedRule();
|
|
32
86
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
87
|
+
return { rules, hookGuards, commands: seedCommands(), rulesDir: [] };
|
|
88
|
+
}
|
|
89
|
+
function writeConfig(configPath, config) {
|
|
90
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\n');
|
|
91
|
+
}
|
|
92
|
+
function readConfig(configPath) {
|
|
93
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
94
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(raw);
|
|
37
97
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
98
|
+
catch (err) {
|
|
99
|
+
const error = (0, to_error_1.toError)(err);
|
|
100
|
+
throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });
|
|
41
101
|
}
|
|
42
|
-
if (!settings.hooks)
|
|
43
|
-
settings.hooks = {};
|
|
44
|
-
if (!Array.isArray(settings.hooks.PreToolUse))
|
|
45
|
-
settings.hooks.PreToolUse = [];
|
|
46
|
-
settings.hooks.PreToolUse.push({
|
|
47
|
-
matcher: 'Write|Edit|MultiEdit|Bash',
|
|
48
|
-
hooks: [{ type: 'command', command: HOOK_COMMAND }],
|
|
49
|
-
});
|
|
50
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
51
|
-
console.log(` [ai-hook-rules] Wired ${HOOK_COMMAND} into .claude/settings.json`);
|
|
52
102
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
103
|
+
function asSection(value) {
|
|
104
|
+
return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? value : {};
|
|
105
|
+
}
|
|
106
|
+
// Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.
|
|
107
|
+
// Returns a human-readable list of what changed (empty = already up to date).
|
|
108
|
+
function migrate(existing) {
|
|
109
|
+
const changes = [];
|
|
110
|
+
const rules = asSection(existing['rules']);
|
|
111
|
+
const hookGuards = asSection(existing['hookGuards']);
|
|
112
|
+
const commands = (typeof existing['commands'] === 'object' && existing['commands'] !== null)
|
|
113
|
+
? existing['commands'] : {};
|
|
114
|
+
// Move a deprecated top-level pr-gate block under commands.
|
|
115
|
+
if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {
|
|
116
|
+
commands['pr-gate'] = existing['pr-gate'];
|
|
117
|
+
changes.push('moved top-level "pr-gate" → commands["pr-gate"]');
|
|
58
118
|
}
|
|
59
|
-
|
|
60
|
-
for (const name of
|
|
61
|
-
|
|
119
|
+
// Move guards mistakenly left in rules into hookGuards.
|
|
120
|
+
for (const name of Object.keys(rules)) {
|
|
121
|
+
if ((0, rules_config_1.isHookGuard)(name)) {
|
|
122
|
+
hookGuards[name] = rules[name];
|
|
123
|
+
delete rules[name];
|
|
124
|
+
changes.push(`moved "${name}" from rules → hookGuards`);
|
|
125
|
+
}
|
|
62
126
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
127
|
+
// Move code rules mistakenly placed in hookGuards back into rules.
|
|
128
|
+
for (const name of Object.keys(hookGuards)) {
|
|
129
|
+
if (!(0, rules_config_1.isHookGuard)(name) && (0, rules_config_1.allRuleNames)().includes(name)) {
|
|
130
|
+
rules[name] = hookGuards[name];
|
|
131
|
+
delete hookGuards[name];
|
|
132
|
+
changes.push(`moved "${name}" from hookGuards → rules`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Add any missing built-in into its correct section (OFF).
|
|
136
|
+
for (const name of (0, rules_config_1.allRuleNames)()) {
|
|
137
|
+
const target = (0, rules_config_1.sectionForRule)(name) === 'hookGuards' ? hookGuards : rules;
|
|
138
|
+
if (!(name in target)) {
|
|
139
|
+
target[name] = seedRule();
|
|
140
|
+
changes.push(`added "${name}" (OFF) to ${(0, rules_config_1.sectionForRule)(name)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Fill command defaults.
|
|
144
|
+
if (commands['pr-gate'] === undefined) {
|
|
145
|
+
commands['pr-gate'] = { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] };
|
|
146
|
+
changes.push('added commands["pr-gate"] (OFF)');
|
|
147
|
+
}
|
|
148
|
+
if (commands['upsertPr'] === undefined) {
|
|
149
|
+
commands['upsertPr'] = DEFAULT_UPSERT_PR;
|
|
150
|
+
changes.push('added commands.upsertPr');
|
|
151
|
+
}
|
|
152
|
+
if (commands['mergeComplete'] === undefined) {
|
|
153
|
+
commands['mergeComplete'] = DEFAULT_MERGE_COMPLETE;
|
|
154
|
+
changes.push('added commands.mergeComplete');
|
|
155
|
+
}
|
|
156
|
+
const rulesDir = Array.isArray(existing['rulesDir']) ? existing['rulesDir'] : [];
|
|
157
|
+
const config = { rules, hookGuards, commands, rulesDir };
|
|
158
|
+
if (typeof existing['extends'] === 'string')
|
|
159
|
+
config.extends = existing['extends'];
|
|
160
|
+
return { config, changes };
|
|
67
161
|
}
|
|
68
|
-
function
|
|
162
|
+
function seedOrSyncConfig(projectRoot, syncOnly) {
|
|
69
163
|
const configPath = path.join(projectRoot, CONFIG_FILENAME);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!config.rules)
|
|
75
|
-
config.rules = {};
|
|
76
|
-
const added = [];
|
|
77
|
-
for (const name of index_1.builtInRuleNames) {
|
|
78
|
-
if (!Object.prototype.hasOwnProperty.call(config.rules, name)) {
|
|
79
|
-
config.rules[name] = seedRule();
|
|
80
|
-
added.push(name);
|
|
164
|
+
if (!fs.existsSync(configPath)) {
|
|
165
|
+
if (syncOnly) {
|
|
166
|
+
console.log(` [ai-hooks] No ${CONFIG_FILENAME} found — nothing to sync.`);
|
|
167
|
+
return;
|
|
81
168
|
}
|
|
169
|
+
writeConfig(configPath, buildSeedConfig());
|
|
170
|
+
console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands), all rules OFF.`);
|
|
171
|
+
console.log(' Enable the ones you want by changing "mode".');
|
|
172
|
+
return;
|
|
82
173
|
}
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
addedPrGate = true;
|
|
87
|
-
}
|
|
88
|
-
if (added.length === 0 && !addedPrGate) {
|
|
89
|
-
console.log(` [ai-hook-rules] ${CONFIG_FILENAME} is already up to date — no new rules to add.`);
|
|
174
|
+
const result = migrate(readConfig(configPath));
|
|
175
|
+
if (result.changes.length === 0) {
|
|
176
|
+
console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);
|
|
90
177
|
return;
|
|
91
178
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
179
|
+
writeConfig(configPath, result.config);
|
|
180
|
+
console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);
|
|
181
|
+
for (const change of result.changes)
|
|
182
|
+
console.log(` - ${change}`);
|
|
183
|
+
}
|
|
184
|
+
function readSettings(settingsPath) {
|
|
185
|
+
if (!fs.existsSync(settingsPath))
|
|
186
|
+
return {};
|
|
187
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
188
|
+
if (raw.trim() === '')
|
|
189
|
+
return {};
|
|
190
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(raw);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
const error = (0, to_error_1.toError)(err);
|
|
196
|
+
throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });
|
|
98
197
|
}
|
|
99
|
-
console.log(' Review each new rule and set "mode" to ON/MODIFIED_CODE/etc. as desired.');
|
|
100
198
|
}
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
199
|
+
function writeSettings(settingsPath, settings) {
|
|
200
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
201
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
202
|
+
}
|
|
203
|
+
function hasHook(settings, bin) {
|
|
204
|
+
const entries = settings.hooks?.PreToolUse ?? [];
|
|
205
|
+
return entries.some((e) => e.hooks.some((h) => h.command.includes(bin)));
|
|
206
|
+
}
|
|
207
|
+
// Drop every PreToolUse command referencing `bin`; returns true if anything was removed.
|
|
208
|
+
function removeHook(settings, bin) {
|
|
209
|
+
const entries = settings.hooks?.PreToolUse;
|
|
210
|
+
if (!entries)
|
|
211
|
+
return false;
|
|
212
|
+
let changed = false;
|
|
213
|
+
const kept = [];
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
const hooks = entry.hooks.filter((h) => !h.command.includes(bin));
|
|
216
|
+
if (hooks.length !== entry.hooks.length)
|
|
217
|
+
changed = true;
|
|
218
|
+
if (hooks.length > 0)
|
|
219
|
+
kept.push({ matcher: entry.matcher, hooks });
|
|
107
220
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
221
|
+
if (changed)
|
|
222
|
+
settings.hooks.PreToolUse = kept;
|
|
223
|
+
return changed;
|
|
224
|
+
}
|
|
225
|
+
function addHook(settings, matcher, command) {
|
|
226
|
+
if (!settings.hooks)
|
|
227
|
+
settings.hooks = {};
|
|
228
|
+
if (!Array.isArray(settings.hooks.PreToolUse))
|
|
229
|
+
settings.hooks.PreToolUse = [];
|
|
230
|
+
settings.hooks.PreToolUse.push({ matcher, hooks: [{ type: 'command', command }] });
|
|
231
|
+
}
|
|
232
|
+
// Apply the chosen install for one hook: remove it from every target file, then add it back to the
|
|
233
|
+
// chosen one (or nowhere, for uninstall). Writes only the files that changed.
|
|
234
|
+
function applyHook(hook, chosen, targets, projectRoot) {
|
|
235
|
+
for (const target of targets) {
|
|
236
|
+
const settings = readSettings(target.settingsPath);
|
|
237
|
+
const removed = removeHook(settings, hook.bin);
|
|
238
|
+
const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;
|
|
239
|
+
if (isChosen) {
|
|
240
|
+
addHook(settings, hook.matcher, hook.commandFor(target, projectRoot));
|
|
241
|
+
writeSettings(target.settingsPath, settings);
|
|
242
|
+
console.log(` ✅ ${hook.label} → ${target.label}`);
|
|
243
|
+
}
|
|
244
|
+
else if (removed) {
|
|
245
|
+
writeSettings(target.settingsPath, settings);
|
|
246
|
+
}
|
|
113
247
|
}
|
|
248
|
+
if (chosen === null)
|
|
249
|
+
console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);
|
|
250
|
+
}
|
|
251
|
+
function currentLocation(hook, targets) {
|
|
252
|
+
const here = targets.filter((t) => hasHook(readSettings(t.settingsPath), hook.bin));
|
|
253
|
+
return here.length === 0 ? 'none' : here.map((t) => t.label.split(' (')[0]).join(', ');
|
|
254
|
+
}
|
|
255
|
+
function prompt(question) {
|
|
256
|
+
return new Promise((resolve) => {
|
|
257
|
+
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
258
|
+
rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async function wireHook(hook, targets, projectRoot) {
|
|
262
|
+
console.log('');
|
|
263
|
+
console.log(`${hook.label} [matcher: ${hook.matcher}]`);
|
|
264
|
+
console.log(` currently installed in: ${currentLocation(hook, targets)}`);
|
|
265
|
+
for (const target of targets)
|
|
266
|
+
console.log(` ${target.choice}) ${target.label}`);
|
|
267
|
+
console.log(' 4) none / uninstall');
|
|
268
|
+
const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');
|
|
269
|
+
const chosen = targets.find((t) => t.choice === answer) ?? null;
|
|
270
|
+
applyHook(hook, chosen, targets, projectRoot);
|
|
271
|
+
}
|
|
272
|
+
async function main() {
|
|
273
|
+
const args = process.argv.slice(2);
|
|
274
|
+
const syncOnly = args.includes('--sync');
|
|
275
|
+
const projectRoot = process.cwd();
|
|
276
|
+
seedOrSyncConfig(projectRoot, syncOnly);
|
|
277
|
+
if (syncOnly)
|
|
278
|
+
return;
|
|
279
|
+
const targets = installTargets(projectRoot);
|
|
280
|
+
console.log('');
|
|
281
|
+
console.log('Two webpieces hooks can be installed independently — choose a location for each:');
|
|
282
|
+
await wireHook(exports.RULES_HOOK, targets, projectRoot);
|
|
283
|
+
await wireHook(exports.GUARDS_HOOK, targets, projectRoot);
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log('Done. Re-run wp-setup-ai-hooks anytime to move or uninstall a hook.');
|
|
114
286
|
}
|
|
115
287
|
if (require.main === module) {
|
|
116
|
-
main();
|
|
288
|
+
void main();
|
|
117
289
|
}
|
|
118
290
|
//# sourceMappingURL=setup.js.map
|
package/src/bin/setup.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { builtInRuleNames } from '../core/rules/index';\n\nconst HOOK_COMMAND = './node_modules/.bin/wp-ai-hook';\nconst CONFIG_FILENAME = 'webpieces.config.json';\n\ninterface HookEntry {\n matcher: string;\n hooks: Array<{ type: string; command: string }>;\n}\n\ninterface ClaudeSettings {\n hooks?: {\n PreToolUse?: HookEntry[];\n };\n // webpieces-disable no-any-unknown -- opaque settings bag allows arbitrary key access\n [key: string]: unknown;\n}\n\ninterface RulesConfig {\n rules: Record<string, object>;\n rulesDir?: string[];\n 'pr-gate'?: object;\n}\n\n// ignoreModifiedUntilEpoch is required on every rule (0 = active). A freshly seeded rule is OFF.\nfunction seedRule(): object {\n return { mode: 'OFF', ignoreModifiedUntilEpoch: 0 };\n}\n\n// pr-gate is a required top-level block; seed it OFF (opt-out) with a ready-to-use buildCommand so\n// flipping it ON is a one-line edit.\nfunction seedPrGate(): object {\n return { mode: 'OFF', buildCommand: 'pnpm nx affected --target=ci --base=origin/main', gates: [] };\n}\n\nfunction settingsAlreadyHasHook(settingsPath: string): boolean {\n if (!fs.existsSync(settingsPath)) return false;\n const content = fs.readFileSync(settingsPath, 'utf8');\n return content.includes('wp-ai-hook');\n}\n\nfunction wireSettings(projectRoot: string): void {\n const claudeDir = path.join(projectRoot, '.claude');\n if (!fs.existsSync(claudeDir)) {\n console.log(' [ai-hook-rules] No .claude/ directory found — add the hook manually:');\n console.log(' In .claude/settings.json under hooks.PreToolUse, add:');\n console.log(` { \"matcher\": \"Write|Edit|MultiEdit|Bash\", \"hooks\": [{ \"type\": \"command\", \"command\": \"${HOOK_COMMAND}\" }] }`);\n return;\n }\n\n const settingsPath = path.join(claudeDir, 'settings.json');\n\n if (settingsAlreadyHasHook(settingsPath)) {\n console.log(' [ai-hook-rules] .claude/settings.json already has the hook — skipping.');\n return;\n }\n\n let settings: ClaudeSettings = {};\n if (fs.existsSync(settingsPath)) {\n settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as ClaudeSettings;\n }\n\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n\n settings.hooks.PreToolUse.push({\n matcher: 'Write|Edit|MultiEdit|Bash',\n hooks: [{ type: 'command', command: HOOK_COMMAND }],\n });\n\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n console.log(` [ai-hook-rules] Wired ${HOOK_COMMAND} into .claude/settings.json`);\n}\n\nfunction seedConfig(projectRoot: string): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (fs.existsSync(configPath)) {\n console.log(` [ai-hook-rules] ${CONFIG_FILENAME} already exists — run with --sync to add missing rules.`);\n return;\n }\n\n const rules: Record<string, object> = {};\n for (const name of builtInRuleNames) {\n rules[name] = seedRule();\n }\n\n const config: RulesConfig = { rules, rulesDir: [], 'pr-gate': seedPrGate() };\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n console.log(` [ai-hook-rules] Created ${CONFIG_FILENAME} with all rules set to OFF.`);\n console.log(' Review and enable the rules you want by changing \"mode\" to \"ON\" or \"MODIFIED_CODE\" etc.');\n}\n\nfunction syncConfig(projectRoot: string): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n\n let config: RulesConfig = { rules: {}, rulesDir: [] };\n if (fs.existsSync(configPath)) {\n config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as RulesConfig;\n }\n if (!config.rules) config.rules = {};\n\n const added: string[] = [];\n for (const name of builtInRuleNames) {\n if (!Object.prototype.hasOwnProperty.call(config.rules, name)) {\n config.rules[name] = seedRule();\n added.push(name);\n }\n }\n\n let addedPrGate = false;\n if (!Object.prototype.hasOwnProperty.call(config, 'pr-gate')) {\n config['pr-gate'] = seedPrGate();\n addedPrGate = true;\n }\n\n if (added.length === 0 && !addedPrGate) {\n console.log(` [ai-hook-rules] ${CONFIG_FILENAME} is already up to date — no new rules to add.`);\n return;\n }\n\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n if (addedPrGate) console.log(` [ai-hook-rules] Added the required \"pr-gate\" block (mode OFF) to ${CONFIG_FILENAME}.`);\n console.log(` [ai-hook-rules] Added ${added.length} new rule(s) to ${CONFIG_FILENAME} (set to OFF):`);\n for (const name of added) {\n console.log(` - ${name}`);\n }\n console.log(' Review each new rule and set \"mode\" to ON/MODIFIED_CODE/etc. as desired.');\n}\n\nexport function main(): void {\n const args = process.argv.slice(2);\n const isSync = args.includes('--sync');\n\n const projectRoot = process.cwd();\n\n if (isSync) {\n syncConfig(projectRoot);\n } else {\n seedConfig(projectRoot);\n console.log('');\n console.log(' To install the global Claude Code hook (one-time, per machine):');\n console.log(' ./node_modules/.bin/wp-setup-global-ai-hooks');\n }\n}\n\nif (require.main === module) {\n main();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\nimport { createInterface } from 'readline';\n\nimport { allRuleNames, sectionForRule, isHookGuard } from '@webpieces/rules-config';\n\nimport { toError } from '../core/to-error';\n\nconst CONFIG_FILENAME = 'webpieces.config.json';\nconst DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';\nconst DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';\nconst DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';\n\n// ---------------------------------------------------------------------------\n// The two independently-installable hooks. Each can land in a different settings\n// file (see InstallTarget) so a team can ship the guards while a developer keeps\n// the code-style rules local while iterating.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly matcher: string,\n readonly bin: string,\n ) {}\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n commandFor(target: InstallTarget, projectRoot: string): string {\n if (target.absolute) {\n return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;\n }\n return `./node_modules/.bin/${this.bin}`;\n }\n}\n\nclass InstallTarget {\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n ) {}\n}\n\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', 'Write|Edit|MultiEdit', 'wp-ai-rules-hook');\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', 'Bash', 'wp-ai-guards-hook');\n\n// `homeDir` is injectable so tests can point the global target at a temp dir instead of the real\n// ~/.claude/settings.json (a unit test must never write the user's actual global settings).\nexport function installTargets(projectRoot: string, homeDir: string = homedir()): InstallTarget[] {\n return [\n new InstallTarget('1', 'project (.claude/settings.json — committed, for the team)',\n path.join(projectRoot, '.claude', 'settings.json'), false),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// webpieces.config.json seeding + migration to the rules / hookGuards / commands layout.\n// ---------------------------------------------------------------------------\n// webpieces-disable no-any-unknown -- webpieces.config.json / settings.json are opaque consumer JSON\ntype Json = Record<string, unknown>;\ntype RuleEntry = Json;\ntype Section = Record<string, RuleEntry>;\n\ninterface ConfigFile {\n extends?: string;\n rules: Section;\n hookGuards: Section;\n commands: Json;\n rulesDir: string[];\n}\n\ninterface MigrateResult {\n config: ConfigFile;\n changes: string[];\n}\n\nfunction seedRule(): RuleEntry {\n return { mode: 'OFF', ignoreModifiedUntilEpoch: 0 };\n}\n\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n upsertPr: DEFAULT_UPSERT_PR,\n mergeComplete: DEFAULT_MERGE_COMPLETE,\n };\n}\n\nfunction buildSeedConfig(): ConfigFile {\n const rules: Section = {};\n const hookGuards: Section = {};\n for (const name of allRuleNames()) {\n if (sectionForRule(name) === 'hookGuards') hookGuards[name] = seedRule();\n else rules[name] = seedRule();\n }\n return { rules, hookGuards, commands: seedCommands(), rulesDir: [] };\n}\n\nfunction writeConfig(configPath: string, config: ConfigFile): void {\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n}\n\nfunction readConfig(configPath: string): Json {\n const raw = fs.readFileSync(configPath, 'utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as Json;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction asSection(value: Json[string]): Section {\n return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? (value as Section) : {};\n}\n\n// Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.\n// Returns a human-readable list of what changed (empty = already up to date).\nexport function migrate(existing: Json): MigrateResult {\n const changes: string[] = [];\n const rules: Section = asSection(existing['rules']);\n const hookGuards: Section = asSection(existing['hookGuards']);\n const commands: Json = (typeof existing['commands'] === 'object' && existing['commands'] !== null)\n ? (existing['commands'] as Json) : {};\n\n // Move a deprecated top-level pr-gate block under commands.\n if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {\n commands['pr-gate'] = existing['pr-gate'];\n changes.push('moved top-level \"pr-gate\" → commands[\"pr-gate\"]');\n }\n // Move guards mistakenly left in rules into hookGuards.\n for (const name of Object.keys(rules)) {\n if (isHookGuard(name)) {\n hookGuards[name] = rules[name];\n delete rules[name];\n changes.push(`moved \"${name}\" from rules → hookGuards`);\n }\n }\n // Move code rules mistakenly placed in hookGuards back into rules.\n for (const name of Object.keys(hookGuards)) {\n if (!isHookGuard(name) && allRuleNames().includes(name)) {\n rules[name] = hookGuards[name];\n delete hookGuards[name];\n changes.push(`moved \"${name}\" from hookGuards → rules`);\n }\n }\n // Add any missing built-in into its correct section (OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n target[name] = seedRule();\n changes.push(`added \"${name}\" (OFF) to ${sectionForRule(name)}`);\n }\n }\n // Fill command defaults.\n if (commands['pr-gate'] === undefined) {\n commands['pr-gate'] = { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] };\n changes.push('added commands[\"pr-gate\"] (OFF)');\n }\n if (commands['upsertPr'] === undefined) { commands['upsertPr'] = DEFAULT_UPSERT_PR; changes.push('added commands.upsertPr'); }\n if (commands['mergeComplete'] === undefined) { commands['mergeComplete'] = DEFAULT_MERGE_COMPLETE; changes.push('added commands.mergeComplete'); }\n\n const rulesDir: string[] = Array.isArray(existing['rulesDir']) ? (existing['rulesDir'] as string[]) : [];\n const config: ConfigFile = { rules, hookGuards, commands, rulesDir };\n if (typeof existing['extends'] === 'string') config.extends = existing['extends'];\n return { config, changes };\n}\n\nfunction seedOrSyncConfig(projectRoot: string, syncOnly: boolean): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n if (syncOnly) {\n console.log(` [ai-hooks] No ${CONFIG_FILENAME} found — nothing to sync.`);\n return;\n }\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands), all rules OFF.`);\n console.log(' Enable the ones you want by changing \"mode\".');\n return;\n }\n const result = migrate(readConfig(configPath));\n if (result.changes.length === 0) {\n console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);\n return;\n }\n writeConfig(configPath, result.config);\n console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);\n for (const change of result.changes) console.log(` - ${change}`);\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code settings.json hook wiring.\n// ---------------------------------------------------------------------------\ninterface HookCommand { type: string; command: string; }\ninterface HookEntry { matcher: string; hooks: HookCommand[]; }\ninterface ClaudeSettings {\n hooks?: { PreToolUse?: HookEntry[] };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\nexport function hasHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(bin)));\n}\n\n// Drop every PreToolUse command referencing `bin`; returns true if anything was removed.\nfunction removeHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand) => !h.command.includes(bin));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\nfunction addHook(settings: ClaudeSettings, matcher: string, command: string): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher, hooks: [{ type: 'command', command }] });\n}\n\n// Apply the chosen install for one hook: remove it from every target file, then add it back to the\n// chosen one (or nowhere, for uninstall). Writes only the files that changed.\nexport function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void {\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n const removed = removeHook(settings, hook.bin);\n const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;\n if (isChosen) {\n addHook(settings, hook.matcher, hook.commandFor(target, projectRoot));\n writeSettings(target.settingsPath, settings);\n console.log(` ✅ ${hook.label} → ${target.label}`);\n } else if (removed) {\n writeSettings(target.settingsPath, settings);\n }\n }\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\n}\n\nfunction currentLocation(hook: HookSpec, targets: InstallTarget[]): string {\n const here = targets.filter((t: InstallTarget) => hasHook(readSettings(t.settingsPath), hook.bin));\n return here.length === 0 ? 'none' : here.map((t: InstallTarget) => t.label.split(' (')[0]).join(', ');\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve: (answer: string) => void) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer: string) => { rl.close(); resolve(answer.trim()); });\n });\n}\n\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label} [matcher: ${hook.matcher}]`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n for (const target of targets) console.log(` ${target.choice}) ${target.label}`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n const chosen = targets.find((t: InstallTarget) => t.choice === answer) ?? null;\n applyHook(hook, chosen, targets, projectRoot);\n}\n\nexport async function main(): Promise<void> {\n const args = process.argv.slice(2);\n const syncOnly = args.includes('--sync');\n const projectRoot = process.cwd();\n\n seedOrSyncConfig(projectRoot, syncOnly);\n if (syncOnly) return;\n\n const targets = installTargets(projectRoot);\n console.log('');\n console.log('Two webpieces hooks can be installed independently — choose a location for each:');\n await wireHook(RULES_HOOK, targets, projectRoot);\n await wireHook(GUARDS_HOOK, targets, projectRoot);\n console.log('');\n console.log('Done. Re-run wp-setup-ai-hooks anytime to move or uninstall a hook.');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
|
|
@@ -2,8 +2,9 @@ import { MergeInProgressGuardConfig } from '@webpieces/rules-config';
|
|
|
2
2
|
import type { BashContext, Violation } from '../types';
|
|
3
3
|
import { BashRuleBase } from '../rule-base';
|
|
4
4
|
export declare class MergeInProgressGuardRule extends BashRuleBase<MergeInProgressGuardConfig> {
|
|
5
|
+
private readonly mergeCompleteCommand;
|
|
5
6
|
constructor(config: MergeInProgressGuardConfig);
|
|
6
|
-
readonly description = "Block commit/push/merge/PR while a 3-point merge marker is unvalidated, forcing
|
|
7
|
-
|
|
7
|
+
readonly description = "Block commit/push/merge/PR while a 3-point merge marker is unvalidated, forcing the merge-complete command.";
|
|
8
|
+
get fixHint(): readonly string[];
|
|
8
9
|
check(ctx: BashContext): readonly Violation[];
|
|
9
10
|
}
|
|
@@ -7,13 +7,16 @@ const path = tslib_1.__importStar(require("path"));
|
|
|
7
7
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
8
8
|
const types_1 = require("../types");
|
|
9
9
|
const rule_base_1 = require("../rule-base");
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const DEFAULT_MERGE_COMPLETE_COMMAND = 'pnpm wp-finish-upsert-pr';
|
|
11
|
+
function fixHintFor(mergeCompleteCommand) {
|
|
12
|
+
return [
|
|
13
|
+
'A 3-point merge is in progress and not yet validated.',
|
|
14
|
+
'Resolve the remaining conflicts in the working tree, then run:',
|
|
15
|
+
` ${mergeCompleteCommand}`,
|
|
16
|
+
'That scans for leftover conflict markers and runs the build; only when green does it commit,',
|
|
17
|
+
'unblock commit/push/PR, render the dashboard, and create/update the PR.',
|
|
18
|
+
];
|
|
19
|
+
}
|
|
17
20
|
// Returns the path of the first UNVALIDATED merge marker found, or null. We detect validation
|
|
18
21
|
// by a raw substring (no JSON.parse) so a malformed marker can never crash the guard.
|
|
19
22
|
function findUnvalidatedMerge(workspaceRoot) {
|
|
@@ -45,9 +48,13 @@ function truncate(s) {
|
|
|
45
48
|
return s.length <= MAX ? s : s.slice(0, MAX) + '…';
|
|
46
49
|
}
|
|
47
50
|
class MergeInProgressGuardRule extends rule_base_1.BashRuleBase {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
mergeCompleteCommand;
|
|
52
|
+
constructor(config) {
|
|
53
|
+
super(config, 'merge-in-progress-guard');
|
|
54
|
+
this.mergeCompleteCommand = config.mergeCompleteCommand ?? DEFAULT_MERGE_COMPLETE_COMMAND;
|
|
55
|
+
}
|
|
56
|
+
description = 'Block commit/push/merge/PR while a 3-point merge marker is unvalidated, forcing the merge-complete command.';
|
|
57
|
+
get fixHint() { return fixHintFor(this.mergeCompleteCommand); }
|
|
51
58
|
check(ctx) {
|
|
52
59
|
if (!isBlockedDuringMerge(ctx.command))
|
|
53
60
|
return [];
|
|
@@ -57,7 +64,7 @@ class MergeInProgressGuardRule extends rule_base_1.BashRuleBase {
|
|
|
57
64
|
return [new types_1.Violation(1, truncate(ctx.command), [
|
|
58
65
|
'A merge is in progress and not yet validated — this command is blocked.',
|
|
59
66
|
`Marker: ${marker}`,
|
|
60
|
-
|
|
67
|
+
`Finish resolving conflicts, then run: ${this.mergeCompleteCommand}`,
|
|
61
68
|
].join('\n'))];
|
|
62
69
|
}
|
|
63
70
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-in-progress-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/merge-in-progress-guard.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAkI;AAGlI,oCAA0C;AAC1C,4CAA4C;AAE5C,MAAM,
|
|
1
|
+
{"version":3,"file":"merge-in-progress-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/merge-in-progress-guard.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAkI;AAGlI,oCAA0C;AAC1C,4CAA4C;AAE5C,MAAM,8BAA8B,GAAG,0BAA0B,CAAC;AAElE,SAAS,UAAU,CAAC,oBAA4B;IAC5C,OAAO;QACH,uDAAuD;QACvD,gEAAgE;QAChE,KAAK,oBAAoB,EAAE;QAC3B,8FAA8F;QAC9F,yEAAyE;KAC5E,CAAC;AACN,CAAC;AAED,8FAA8F;AAC9F,sFAAsF;AACtF,SAAS,oBAAoB,CAAC,aAAqB;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,gCAAiB,CAAC,CAAC;IAC3D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,+BAAgB,CAAC;YAAE,SAAS;QAClD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,qCAAsB,CAAC,CAAC;QAChE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,SAAS;QACrC,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC5C,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,MAAM,CAAC;IAC3D,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,kEAAkE;AAClE,SAAS,oBAAoB,CAAC,GAAW;IACrC,OAAO,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC;WAC5B,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC;WAC1B,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC;WAC3B,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC;WAC5B,mCAAmC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,MAAa,wBAAyB,SAAQ,wBAAwC;IACjE,oBAAoB,CAAS;IAE9C,YAAY,MAAkC;QAC1C,KAAK,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;QACzC,IAAI,CAAC,oBAAoB,GAAG,MAAM,CAAC,oBAAoB,IAAI,8BAA8B,CAAC;IAC9F,CAAC;IAEQ,WAAW,GAAG,6GAA6G,CAAC;IACrI,IAAI,OAAO,KAAwB,OAAO,UAAU,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC;IAElF,KAAK,CAAC,GAAgB;QAClB,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC;QAClD,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,CAAC;QACvB,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB;gBACI,yEAAyE;gBACzE,WAAW,MAAM,EAAE;gBACnB,0CAA0C,IAAI,CAAC,oBAAoB,EAAE;aACxE,CAAC,IAAI,CAAC,IAAI,CAAC,CACf,CAAC,CAAC;IACP,CAAC;CACJ;AAzBD,4DAyBC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { WEBPIECES_TMP_DIR, MERGE_DIR_PREFIX, MERGE_IN_PROGRESS_FILE, MergeInProgressGuardConfig } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\n\nconst DEFAULT_MERGE_COMPLETE_COMMAND = 'pnpm wp-finish-upsert-pr';\n\nfunction fixHintFor(mergeCompleteCommand: string): readonly string[] {\n return [\n 'A 3-point merge is in progress and not yet validated.',\n 'Resolve the remaining conflicts in the working tree, then run:',\n ` ${mergeCompleteCommand}`,\n 'That scans for leftover conflict markers and runs the build; only when green does it commit,',\n 'unblock commit/push/PR, render the dashboard, and create/update the PR.',\n ];\n}\n\n// Returns the path of the first UNVALIDATED merge marker found, or null. We detect validation\n// by a raw substring (no JSON.parse) so a malformed marker can never crash the guard.\nfunction findUnvalidatedMerge(workspaceRoot: string): string | null {\n const tmpDir = path.join(workspaceRoot, WEBPIECES_TMP_DIR);\n if (!fs.existsSync(tmpDir)) return null;\n for (const entry of fs.readdirSync(tmpDir)) {\n if (!entry.startsWith(MERGE_DIR_PREFIX)) continue;\n const marker = path.join(tmpDir, entry, MERGE_IN_PROGRESS_FILE);\n if (!fs.existsSync(marker)) continue;\n const raw = fs.readFileSync(marker, 'utf8');\n if (!/\"validated\"\\s*:\\s*true/.test(raw)) return marker;\n }\n return null;\n}\n\n// Operations that would let an agent route around the merge gate.\nfunction isBlockedDuringMerge(cmd: string): boolean {\n return /\\bgit\\s+commit\\b/.test(cmd)\n || /\\bgit\\s+push\\b/.test(cmd)\n || /\\bgit\\s+merge\\b/.test(cmd)\n || /\\bgit\\s+rebase\\b/.test(cmd)\n || /\\bgh\\s+pr\\s+(create|edit|merge)\\b/.test(cmd);\n}\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class MergeInProgressGuardRule extends BashRuleBase<MergeInProgressGuardConfig> {\n private readonly mergeCompleteCommand: string;\n\n constructor(config: MergeInProgressGuardConfig) {\n super(config, 'merge-in-progress-guard');\n this.mergeCompleteCommand = config.mergeCompleteCommand ?? DEFAULT_MERGE_COMPLETE_COMMAND;\n }\n\n readonly description = 'Block commit/push/merge/PR while a 3-point merge marker is unvalidated, forcing the merge-complete command.';\n get fixHint(): readonly string[] { return fixHintFor(this.mergeCompleteCommand); }\n\n check(ctx: BashContext): readonly Violation[] {\n if (!isBlockedDuringMerge(ctx.command)) return [];\n const marker = findUnvalidatedMerge(ctx.workspaceRoot);\n if (!marker) return [];\n return [new V(\n 1,\n truncate(ctx.command),\n [\n 'A merge is in progress and not yet validated — this command is blocked.',\n `Marker: ${marker}`,\n `Finish resolving conflicts, then run: ${this.mergeCompleteCommand}`,\n ].join('\\n'),\n )];\n }\n}\n"]}
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
+
"_comment": "Two independently-installable webpieces PreToolUse hooks. The Rules hook validates code-style on Write/Edit/MultiEdit; the Guards hook protects git/PR/branch on Bash. Install them with `wp-setup-ai-hooks` (which lets you pick a location per hook); this file just shows the wiring. Use `./node_modules/.bin/<bin>` for project installs, or `node <repoRoot>/node_modules/.bin/<bin>` for a global ~/.claude/settings.json install.",
|
|
2
3
|
"hooks": {
|
|
3
4
|
"PreToolUse": [
|
|
4
5
|
{
|
|
5
|
-
"matcher": "Write|Edit|MultiEdit
|
|
6
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
6
7
|
"hooks": [
|
|
7
8
|
{
|
|
8
9
|
"type": "command",
|
|
9
|
-
"command": "./node_modules/.bin/wp-ai-hook"
|
|
10
|
+
"command": "./node_modules/.bin/wp-ai-rules-hook"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Bash",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "./node_modules/.bin/wp-ai-guards-hook"
|
|
10
20
|
}
|
|
11
21
|
]
|
|
12
22
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|