mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
Tighten hook and proxy discovery
AI assistance disclosure: Codex implemented and verified these fixes under maintainer direction.
This commit is contained in:
@@ -1313,13 +1313,11 @@ function hookEnabledAt(root) {
|
||||
|
||||
const STOP_REVIEW_PROVIDERS = new Set(['claude-code', 'codex', 'agents', 'grok']);
|
||||
|
||||
// Harness project settings are discovered by walking up from the active
|
||||
// working directory. Context resolution can intentionally select a nested
|
||||
// product root even when the harness project (and its hook manifest) lives at
|
||||
// the enclosing git root, so checking only projectRoot/repoRoot produces a
|
||||
// false MANUAL_DETECTOR_REQUIRED directive. Mirror the ancestor lookup through
|
||||
// the nearest git boundary, while retaining exact resolved roots for explicit
|
||||
// targets outside the invoking directory.
|
||||
// Harness project settings are discovered by walking up from the resolved
|
||||
// project root. Its hook manifest can live at an enclosing git root, so
|
||||
// checking only projectRoot/repoRoot produces a false
|
||||
// MANUAL_DETECTOR_REQUIRED directive. Starting from projectRoot also prevents
|
||||
// an explicit target from borrowing an unrelated manifest near the caller.
|
||||
function hookManifestSearchRoots(ctx) {
|
||||
const roots = [];
|
||||
const seen = new Set();
|
||||
@@ -1331,7 +1329,7 @@ function hookManifestSearchRoots(ctx) {
|
||||
roots.push(resolved);
|
||||
};
|
||||
|
||||
let current = path.resolve(process.cwd());
|
||||
let current = path.resolve(ctx.projectRoot || process.cwd());
|
||||
const home = path.resolve(os.homedir());
|
||||
while (true) {
|
||||
add(current);
|
||||
|
||||
@@ -78,10 +78,12 @@ const NUXT_ROUTE_RULES_SIGNALS = [
|
||||
/\bscript-src\b/,
|
||||
];
|
||||
|
||||
const NEXT_REQUEST_HOOK_FILES = new Set([
|
||||
const NEXT_MIDDLEWARE_FILES = new Set([
|
||||
'middleware.ts',
|
||||
'middleware.js',
|
||||
'middleware.mjs',
|
||||
]);
|
||||
const NEXT_PROXY_FILES = new Set([
|
||||
'proxy.ts',
|
||||
'proxy.js',
|
||||
'proxy.mjs',
|
||||
@@ -89,6 +91,16 @@ const NEXT_REQUEST_HOOK_FILES = new Set([
|
||||
const MIDDLEWARE_HINT = /headers\.set\(\s*["']Content-Security-Policy["']/i;
|
||||
const META_TAG_HINT = /http-equiv\s*=\s*["']Content-Security-Policy["']/i;
|
||||
|
||||
function isNextRequestHookFile(relPath, base) {
|
||||
if (NEXT_MIDDLEWARE_FILES.has(base)) return true;
|
||||
if (!NEXT_PROXY_FILES.has(base)) return false;
|
||||
const normalized = relPath.split(path.sep).join('/').toLowerCase();
|
||||
// Next.js 16 recognizes proxy at the project root or in the optional src/
|
||||
// directory, alongside app/ or pages/. A same-named helper deeper in the
|
||||
// tree is not the framework request hook.
|
||||
return normalized === base || normalized === `src/${base}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} cwd Project root.
|
||||
* @returns {{ shape: string|null, signals: string[] }}
|
||||
@@ -142,7 +154,7 @@ export function detectCsp(cwd = process.cwd()) {
|
||||
|
||||
// === detect-only shapes ===
|
||||
|
||||
if (NEXT_REQUEST_HOOK_FILES.has(base) && MIDDLEWARE_HINT.test(body)) {
|
||||
if (isNextRequestHookFile(relPath, base) && MIDDLEWARE_HINT.test(body)) {
|
||||
hits.middleware.push(relPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -1221,6 +1221,39 @@ describe('context.mjs CLI', () => {
|
||||
assert.doesNotMatch(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
|
||||
});
|
||||
|
||||
it('does not borrow a hook manifest from the invoking workspace when targeting a sibling', () => {
|
||||
const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
|
||||
stageContextBundle(scripts, { providerId: 'claude-code' });
|
||||
|
||||
const repo = path.join(scratch, 'repo');
|
||||
const caller = path.join(repo, 'apps', 'marketing');
|
||||
const target = path.join(repo, 'apps', 'dashboard');
|
||||
fs.mkdirSync(path.join(repo, '.git'), { recursive: true });
|
||||
fs.mkdirSync(path.join(caller, '.claude'), { recursive: true });
|
||||
fs.mkdirSync(path.join(target, 'src'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo, 'package.json'), JSON.stringify({ private: true, workspaces: ['apps/*'] }));
|
||||
fs.writeFileSync(path.join(repo, 'turbo.json'), JSON.stringify({ tasks: {} }));
|
||||
fs.writeFileSync(path.join(caller, 'package.json'), JSON.stringify({ name: 'marketing' }));
|
||||
fs.writeFileSync(path.join(target, 'package.json'), JSON.stringify({ name: 'dashboard' }));
|
||||
fs.writeFileSync(path.join(target, 'PRODUCT.md'), '# Dashboard\n');
|
||||
fs.writeFileSync(path.join(target, 'src', 'App.jsx'), 'export default function App() { return "dashboard"; }\n');
|
||||
fs.writeFileSync(path.join(caller, '.claude', 'settings.local.json'), JSON.stringify({
|
||||
hooks: { Stop: [{ hooks: [{ command: 'node .claude/skills/impeccable/scripts/hook.mjs' }] }] },
|
||||
}));
|
||||
|
||||
const res = spawnSync(process.execPath, [
|
||||
path.join(scripts, 'context.mjs'),
|
||||
'--target',
|
||||
path.join(target, 'src', 'App.jsx'),
|
||||
], {
|
||||
cwd: caller,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
||||
});
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
assert.match(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
|
||||
});
|
||||
|
||||
it('adds no detector directive when a per-edit-only hook is active', () => {
|
||||
const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
|
||||
stageContextBundle(scripts, { providerId: 'cursor' });
|
||||
|
||||
@@ -284,3 +284,27 @@ for (const name of listFixtures()) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('detectCsp — Next.js proxy placement', () => {
|
||||
it('accepts root and src proxy files but ignores same-named nested helpers', () => {
|
||||
const source = `export function proxy() {
|
||||
const response = new Response();
|
||||
response.headers.set('Content-Security-Policy', "script-src 'self'");
|
||||
return response;
|
||||
}\n`;
|
||||
for (const [relPath, expectedShape] of [
|
||||
['proxy.ts', 'middleware'],
|
||||
['src/proxy.ts', 'middleware'],
|
||||
['lib/network/proxy.ts', null],
|
||||
]) {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'impeccable-proxy-placement-'));
|
||||
try {
|
||||
mkdirSync(dirname(join(tmp, relPath)), { recursive: true });
|
||||
writeFileSync(join(tmp, relPath), source);
|
||||
assert.equal(detectCsp(tmp).shape, expectedShape, relPath);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user