@prodcycle/prodcycle 0.6.8 → 0.6.10

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/dist/cli.js CHANGED
@@ -558,14 +558,51 @@ async function collectHookFiles(filePath) {
558
558
  const candidate = payload?.tool_input ?? payload;
559
559
  const hookFilePath = candidate?.file_path ?? candidate?.path;
560
560
  const hookContent = candidate?.content ?? candidate?.new_string;
561
- if (hookFilePath && typeof hookContent === 'string') {
562
- return { files: { [hookFilePath]: hookContent }, claudeHook };
561
+ // Coding agents (Claude Code's PostToolUse in particular) pass the file
562
+ // path as an absolute path. The compliance API rejects absolute paths
563
+ // (`File path must be relative`), so we relativise here against cwd's
564
+ // realpath — same code path as `--file <path>`, just driven by the stdin
565
+ // payload. Without this fix every PostToolUse hook call from Claude Code
566
+ // failed with "File path must be relative".
567
+ let hookFileKey = hookFilePath;
568
+ if (hookFilePath && path.isAbsolute(hookFilePath)) {
569
+ try {
570
+ const realpathFile = fs.realpathSync(path.resolve(hookFilePath));
571
+ const realpathCwd = fs.realpathSync(process.cwd());
572
+ const resolved = resolveHookFileKey(hookFilePath, realpathFile, realpathCwd);
573
+ if (!resolved.ok) {
574
+ // The helper's message references `--file`; rewrite for the stdin call site.
575
+ console.error(resolved.error.replace('hook: --file ', 'hook: file_path '));
576
+ process.exit(2);
577
+ }
578
+ hookFileKey = resolved.key;
579
+ }
580
+ catch (err) {
581
+ // File may not exist on disk yet (e.g. a Write event mid-creation) —
582
+ // ONLY ENOENT triggers the lexical fallback. Re-throw permission /
583
+ // symlink-loop / other fs errors so a real problem isn't silently
584
+ // converted into a degraded lexical-only key.
585
+ const code = err?.code;
586
+ if (code !== 'ENOENT') {
587
+ throw err;
588
+ }
589
+ const rel = path.relative(process.cwd(), hookFilePath);
590
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
591
+ console.error(`hook: file_path ${hookFilePath} is outside the current directory ` +
592
+ `(${process.cwd()}). Pass a path relative to the repo root.`);
593
+ process.exit(2);
594
+ }
595
+ hookFileKey = rel;
596
+ }
597
+ }
598
+ if (hookFileKey && typeof hookContent === 'string') {
599
+ return { files: { [hookFileKey]: hookContent }, claudeHook };
563
600
  }
564
- if (hookFilePath && fs.existsSync(hookFilePath)) {
601
+ if (hookFilePath && hookFileKey && fs.existsSync(hookFilePath)) {
565
602
  // Only a path was given — read from disk so post-edit hooks still work
566
603
  // when the agent doesn't ship the content inline.
567
604
  const content = fs.readFileSync(hookFilePath, 'utf8');
568
- return { files: { [hookFilePath]: content }, claudeHook };
605
+ return { files: { [hookFileKey]: content }, claudeHook };
569
606
  }
570
607
  console.error('hook: stdin payload not recognized. Expected one of:\n' +
571
608
  ' {"files": {"path": "content"}}\n' +
package/dist/utils/fs.js CHANGED
@@ -335,7 +335,13 @@ async function collectFiles(baseDir, includePatterns, excludePatterns) {
335
335
  const files = {};
336
336
  const state = { count: 0, limitReached: false };
337
337
  walk(repoRoot, repoRoot, gitignores, prodcycleIgnores, includePatterns, excludePatterns, files, state);
338
- return files;
338
+ // Canonical sort by path. Object iteration order is insertion order, which
339
+ // reflects directory-walk order — that differs between Node's `readdirSync`
340
+ // and Python's `os.walk`, so the two CLIs would otherwise chunk the same
341
+ // file set into different request shapes and the server-side per-chunk
342
+ // evaluation produced subtle finding divergences (CLAUDE.md "Node and
343
+ // Python must stay symmetric"). Sorting here makes the wire shape identical.
344
+ return Object.fromEntries(Object.entries(files).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
339
345
  }
340
346
  function walk(dir, repoRoot, gitignores, prodcycleIgnores, includePatterns, userExcludes, files, state) {
341
347
  if (state.limitReached)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prodcycle/prodcycle",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "Multi-framework policy-as-code compliance scanner for infrastructure and application code.",
5
5
  "homepage": "https://docs.prodcycle.com",
6
6
  "repository": {