@webpieces/ai-hook-rules 0.3.190 → 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/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 index_1 = require("../core/rules/index");
8
- const HOOK_COMMAND = './node_modules/.bin/wp-ai-hook';
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
- // ignoreModifiedUntilEpoch is required on every rule (0 = active). A freshly seeded rule is OFF.
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
- // pr-gate is a required top-level block; seed it OFF (opt-out) with a ready-to-use buildCommand so
15
- // flipping it ON is a one-line edit.
16
- function seedPrGate() {
17
- return { mode: 'OFF', buildCommand: 'pnpm nx affected --target=ci --base=origin/main', gates: [] };
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 settingsAlreadyHasHook(settingsPath) {
20
- if (!fs.existsSync(settingsPath))
21
- return false;
22
- const content = fs.readFileSync(settingsPath, 'utf8');
23
- return content.includes('wp-ai-hook');
24
- }
25
- function wireSettings(projectRoot) {
26
- const claudeDir = path.join(projectRoot, '.claude');
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
- const settingsPath = path.join(claudeDir, 'settings.json');
34
- if (settingsAlreadyHasHook(settingsPath)) {
35
- console.log(' [ai-hook-rules] .claude/settings.json already has the hook — skipping.');
36
- return;
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
- let settings = {};
39
- if (fs.existsSync(settingsPath)) {
40
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
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 seedConfig(projectRoot) {
54
- const configPath = path.join(projectRoot, CONFIG_FILENAME);
55
- if (fs.existsSync(configPath)) {
56
- console.log(` [ai-hook-rules] ${CONFIG_FILENAME} already exists run with --sync to add missing rules.`);
57
- return;
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
- const rules = {};
60
- for (const name of index_1.builtInRuleNames) {
61
- rules[name] = seedRule();
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
- const config = { rules, rulesDir: [], 'pr-gate': seedPrGate() };
64
- fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\n');
65
- console.log(` [ai-hook-rules] Created ${CONFIG_FILENAME} with all rules set to OFF.`);
66
- console.log(' Review and enable the rules you want by changing "mode" to "ON" or "MODIFIED_CODE" etc.');
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 syncConfig(projectRoot) {
162
+ function seedOrSyncConfig(projectRoot, syncOnly) {
69
163
  const configPath = path.join(projectRoot, CONFIG_FILENAME);
70
- let config = { rules: {}, rulesDir: [] };
71
- if (fs.existsSync(configPath)) {
72
- config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
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
- let addedPrGate = false;
84
- if (!Object.prototype.hasOwnProperty.call(config, 'pr-gate')) {
85
- config['pr-gate'] = seedPrGate();
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
- fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\n');
93
- if (addedPrGate)
94
- console.log(` [ai-hook-rules] Added the required "pr-gate" block (mode OFF) to ${CONFIG_FILENAME}.`);
95
- console.log(` [ai-hook-rules] Added ${added.length} new rule(s) to ${CONFIG_FILENAME} (set to OFF):`);
96
- for (const name of added) {
97
- console.log(` - ${name}`);
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 main() {
102
- const args = process.argv.slice(2);
103
- const isSync = args.includes('--sync');
104
- const projectRoot = process.cwd();
105
- if (isSync) {
106
- syncConfig(projectRoot);
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
- else {
109
- seedConfig(projectRoot);
110
- console.log('');
111
- console.log(' To install the global Claude Code hook (one-time, per machine):');
112
- console.log(' ./node_modules/.bin/wp-setup-global-ai-hooks');
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
@@ -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 pnpm wp-git-merge-complete.";
7
- readonly fixHint: readonly string[];
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 FIX_HINT = [
11
- 'A 3-point merge is in progress and not yet validated.',
12
- 'Resolve the remaining conflicts in the working tree, then run:',
13
- ' pnpm wp-git-merge-complete',
14
- 'That scans for leftover conflict markers and runs the build; only when green does it',
15
- 'commit and unblock commit/push/PR. Then run: pnpm wp-upsert-pr',
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
- constructor(config) { super(config, 'merge-in-progress-guard'); }
49
- description = 'Block commit/push/merge/PR while a 3-point merge marker is unvalidated, forcing pnpm wp-git-merge-complete.';
50
- fixHint = FIX_HINT;
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
- 'Finish resolving conflicts, then run: pnpm wp-git-merge-complete',
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,QAAQ,GAAsB;IAChC,uDAAuD;IACvD,gEAAgE;IAChE,8BAA8B;IAC9B,sFAAsF;IACtF,gEAAgE;CACnE,CAAC;AAEF,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;IAClF,YAAY,MAAkC,IAAI,KAAK,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC,CAAC,CAAC;IAEpF,WAAW,GAAG,6GAA6G,CAAC;IAC5H,OAAO,GAAG,QAAQ,CAAC;IAE5B,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,mEAAmE;aACtE,CAAC,IAAI,CAAC,IAAI,CAAC,CACf,CAAC,CAAC;IACP,CAAC;CACJ;AApBD,4DAoBC","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 FIX_HINT: readonly string[] = [\n 'A 3-point merge is in progress and not yet validated.',\n 'Resolve the remaining conflicts in the working tree, then run:',\n ' pnpm wp-git-merge-complete',\n 'That scans for leftover conflict markers and runs the build; only when green does it',\n 'commit and unblock commit/push/PR. Then run: pnpm wp-upsert-pr',\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 constructor(config: MergeInProgressGuardConfig) { super(config, 'merge-in-progress-guard'); }\n\n readonly description = 'Block commit/push/merge/PR while a 3-point merge marker is unvalidated, forcing pnpm wp-git-merge-complete.';\n readonly fixHint = FIX_HINT;\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: pnpm wp-git-merge-complete',\n ].join('\\n'),\n )];\n }\n}\n"]}
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"]}
@@ -3,19 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PrCreationGuardRule = void 0;
4
4
  const types_1 = require("../types");
5
5
  const rule_base_1 = require("../rule-base");
6
- const DEFAULT_UPSERT_PR_COMMAND = 'pnpm wp-upsert-pr';
6
+ const DEFAULT_UPSERT_PR_COMMAND = 'pnpm wp-start-upsert-pr';
7
7
  function fixHintFor(upsertPrCommand) {
8
8
  return [
9
- 'Direct PR creation is blocked. Create or update a PR ONLY via the gated command:',
9
+ 'Direct PR creation is blocked. Create or update a PR ONLY via the gated flow:',
10
10
  ` ${upsertPrCommand}`,
11
- 'It updates the branch from main (3-point merge), runs the real build (nx affected), and',
12
- 'assembles the PR dashboard then creates/updates the PR itself. A failing build = no PR.',
13
- 'There is nothing to paste or attest to; the command does the work.',
11
+ 'It updates the branch from main (3-point merge) and runs the real build (nx affected), then',
12
+ 'instructs you to write review.json and run `pnpm wp-finish-upsert-pr`, which assembles the',
13
+ 'dashboard and creates/updates the PR itself. A failing build = no PR.',
14
+ 'There is nothing to paste or attest to; the commands do the work.',
14
15
  ];
15
16
  }
16
- // Detect every way an agent could open/update a PR directly, so the ONLY path left is
17
- // `pnpm wp-upsert-pr` (whose internal `gh pr create` runs as a child process the hook
18
- // never sees). Read-only `gh pr list` / `gh api .../pulls` GET are intentionally allowed.
17
+ // Detect every way an agent could open/update a PR directly, so the ONLY path left is the gated
18
+ // flow (wp-start-upsert-pr → wp-finish-upsert-pr, whose internal `gh pr create` runs as a child
19
+ // process the hook never sees). Read-only `gh pr list` / `gh api .../pulls` GET are intentionally allowed.
19
20
  function isDirectPrCreation(cmd) {
20
21
  if (/\bgh\s+pr\s+(create|edit)\b/.test(cmd))
21
22
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"pr-creation-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-creation-guard.ts"],"names":[],"mappings":";;;AAGA,oCAA0C;AAC1C,4CAA4C;AAE5C,MAAM,yBAAyB,GAAG,mBAAmB,CAAC;AAEtD,SAAS,UAAU,CAAC,eAAuB;IACvC,OAAO;QACH,kFAAkF;QAClF,KAAK,eAAe,EAAE;QACtB,yFAAyF;QACzF,2FAA2F;QAC3F,oEAAoE;KACvE,CAAC;AACN,CAAC;AAED,sFAAsF;AACtF,sFAAsF;AACtF,0FAA0F;AAC1F,SAAS,kBAAkB,CAAC,GAAW;IACnC,IAAI,6BAA6B,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzD,MAAM,UAAU,GAAG,6BAA6B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3D,IAAI,UAAU,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QAC9I,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,MAAM,SAAS,GAAG,+CAA+C,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5E,IAAI,SAAS,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QAC1G,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,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,mBAAoB,SAAQ,wBAAmC;IACvD,eAAe,CAAS;IAEzC,YAAY,MAA6B;QACrC,KAAK,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;QACnC,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,yBAAyB,CAAC;IAC/E,CAAC;IAEQ,WAAW,GAAG,2GAA2G,CAAC;IACnI,IAAI,OAAO,KAAwB,OAAO,UAAU,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAE7E,KAAK,CAAC,GAAgB;QAClB,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC;QAChD,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB;gBACI,uCAAuC;gBACvC,6EAA6E;gBAC7E,KAAK,IAAI,CAAC,eAAe,EAAE;aAC9B,CAAC,IAAI,CAAC,IAAI,CAAC,CACf,CAAC,CAAC;IACP,CAAC;CACJ;AAvBD,kDAuBC","sourcesContent":["import { PrCreationGuardConfig } 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_UPSERT_PR_COMMAND = 'pnpm wp-upsert-pr';\n\nfunction fixHintFor(upsertPrCommand: string): readonly string[] {\n return [\n 'Direct PR creation is blocked. Create or update a PR ONLY via the gated command:',\n ` ${upsertPrCommand}`,\n 'It updates the branch from main (3-point merge), runs the real build (nx affected), and',\n 'assembles the PR dashboard then creates/updates the PR itself. A failing build = no PR.',\n 'There is nothing to paste or attest to; the command does the work.',\n ];\n}\n\n// Detect every way an agent could open/update a PR directly, so the ONLY path left is\n// `pnpm wp-upsert-pr` (whose internal `gh pr create` runs as a child process the hook\n// never sees). Read-only `gh pr list` / `gh api .../pulls` GET are intentionally allowed.\nfunction isDirectPrCreation(cmd: string): boolean {\n if (/\\bgh\\s+pr\\s+(create|edit)\\b/.test(cmd)) return true;\n\n const ghApiPulls = /\\bgh\\s+api\\b[^\\n]*\\/pulls\\b/.test(cmd);\n if (ghApiPulls && (/--method\\s+POST/i.test(cmd) || /-X\\s+POST/i.test(cmd) || /\\s-f\\b/.test(cmd) || /\\s-F\\b/.test(cmd) || /--field\\b/.test(cmd))) {\n return true;\n }\n\n const curlPulls = /\\bcurl\\b[^\\n]*api\\.github\\.com[^\\n]*\\/pulls\\b/.test(cmd);\n if (curlPulls && (/-X\\s*POST/i.test(cmd) || /--request\\s+POST/i.test(cmd) || /(\\s-d\\b|--data\\b)/.test(cmd))) {\n return true;\n }\n return false;\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 PrCreationGuardRule extends BashRuleBase<PrCreationGuardConfig> {\n private readonly upsertPrCommand: string;\n\n constructor(config: PrCreationGuardConfig) {\n super(config, 'pr-creation-guard');\n this.upsertPrCommand = config.upsertPrCommand ?? DEFAULT_UPSERT_PR_COMMAND;\n }\n\n readonly description = 'Block direct PR creation/edit (gh pr / gh api / curl) so PRs go only through the gated upsert-pr command.';\n get fixHint(): readonly string[] { return fixHintFor(this.upsertPrCommand); }\n\n check(ctx: BashContext): readonly Violation[] {\n if (!isDirectPrCreation(ctx.command)) return [];\n return [new V(\n 1,\n truncate(ctx.command),\n [\n 'Direct PR creation/update is blocked.',\n 'Use the gated command instead — it runs the build and builds the dashboard:',\n ` ${this.upsertPrCommand}`,\n ].join('\\n'),\n )];\n }\n}\n"]}
1
+ {"version":3,"file":"pr-creation-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-creation-guard.ts"],"names":[],"mappings":";;;AAGA,oCAA0C;AAC1C,4CAA4C;AAE5C,MAAM,yBAAyB,GAAG,yBAAyB,CAAC;AAE5D,SAAS,UAAU,CAAC,eAAuB;IACvC,OAAO;QACH,+EAA+E;QAC/E,KAAK,eAAe,EAAE;QACtB,6FAA6F;QAC7F,4FAA4F;QAC5F,uEAAuE;QACvE,mEAAmE;KACtE,CAAC;AACN,CAAC;AAED,gGAAgG;AAChG,gGAAgG;AAChG,2GAA2G;AAC3G,SAAS,kBAAkB,CAAC,GAAW;IACnC,IAAI,6BAA6B,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzD,MAAM,UAAU,GAAG,6BAA6B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3D,IAAI,UAAU,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QAC9I,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,MAAM,SAAS,GAAG,+CAA+C,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5E,IAAI,SAAS,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QAC1G,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,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,mBAAoB,SAAQ,wBAAmC;IACvD,eAAe,CAAS;IAEzC,YAAY,MAA6B;QACrC,KAAK,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;QACnC,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,yBAAyB,CAAC;IAC/E,CAAC;IAEQ,WAAW,GAAG,2GAA2G,CAAC;IACnI,IAAI,OAAO,KAAwB,OAAO,UAAU,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAE7E,KAAK,CAAC,GAAgB;QAClB,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC;QAChD,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EACrB;gBACI,uCAAuC;gBACvC,6EAA6E;gBAC7E,KAAK,IAAI,CAAC,eAAe,EAAE;aAC9B,CAAC,IAAI,CAAC,IAAI,CAAC,CACf,CAAC,CAAC;IACP,CAAC;CACJ;AAvBD,kDAuBC","sourcesContent":["import { PrCreationGuardConfig } 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_UPSERT_PR_COMMAND = 'pnpm wp-start-upsert-pr';\n\nfunction fixHintFor(upsertPrCommand: string): readonly string[] {\n return [\n 'Direct PR creation is blocked. Create or update a PR ONLY via the gated flow:',\n ` ${upsertPrCommand}`,\n 'It updates the branch from main (3-point merge) and runs the real build (nx affected), then',\n 'instructs you to write review.json and run `pnpm wp-finish-upsert-pr`, which assembles the',\n 'dashboard and creates/updates the PR itself. A failing build = no PR.',\n 'There is nothing to paste or attest to; the commands do the work.',\n ];\n}\n\n// Detect every way an agent could open/update a PR directly, so the ONLY path left is the gated\n// flow (wp-start-upsert-pr → wp-finish-upsert-pr, whose internal `gh pr create` runs as a child\n// process the hook never sees). Read-only `gh pr list` / `gh api .../pulls` GET are intentionally allowed.\nfunction isDirectPrCreation(cmd: string): boolean {\n if (/\\bgh\\s+pr\\s+(create|edit)\\b/.test(cmd)) return true;\n\n const ghApiPulls = /\\bgh\\s+api\\b[^\\n]*\\/pulls\\b/.test(cmd);\n if (ghApiPulls && (/--method\\s+POST/i.test(cmd) || /-X\\s+POST/i.test(cmd) || /\\s-f\\b/.test(cmd) || /\\s-F\\b/.test(cmd) || /--field\\b/.test(cmd))) {\n return true;\n }\n\n const curlPulls = /\\bcurl\\b[^\\n]*api\\.github\\.com[^\\n]*\\/pulls\\b/.test(cmd);\n if (curlPulls && (/-X\\s*POST/i.test(cmd) || /--request\\s+POST/i.test(cmd) || /(\\s-d\\b|--data\\b)/.test(cmd))) {\n return true;\n }\n return false;\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 PrCreationGuardRule extends BashRuleBase<PrCreationGuardConfig> {\n private readonly upsertPrCommand: string;\n\n constructor(config: PrCreationGuardConfig) {\n super(config, 'pr-creation-guard');\n this.upsertPrCommand = config.upsertPrCommand ?? DEFAULT_UPSERT_PR_COMMAND;\n }\n\n readonly description = 'Block direct PR creation/edit (gh pr / gh api / curl) so PRs go only through the gated upsert-pr command.';\n get fixHint(): readonly string[] { return fixHintFor(this.upsertPrCommand); }\n\n check(ctx: BashContext): readonly Violation[] {\n if (!isDirectPrCreation(ctx.command)) return [];\n return [new V(\n 1,\n truncate(ctx.command),\n [\n 'Direct PR creation/update is blocked.',\n 'Use the gated command instead — it runs the build and builds the dashboard:',\n ` ${this.upsertPrCommand}`,\n ].join('\\n'),\n )];\n }\n}\n"]}