codeproof 1.0.0 → 1.0.1

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/codeproof.js CHANGED
@@ -3,13 +3,14 @@ import { runInit } from "../commands/init.js";
3
3
  import { runCli } from "../commands/run.js";
4
4
  import { runReportDashboard } from "../commands/reportDashboard.js";
5
5
  import { runMoveSecret } from "../commands/moveSecret.js";
6
+ import { runWhoAmI } from "../commands/whoami.js";
6
7
  import { logError, logInfo } from "../utils/logger.js";
7
8
 
8
9
  const [, , command, ...args] = process.argv;
9
10
 
10
11
  async function main() {
11
12
  if (!command || command === "-h" || command === "--help") {
12
- logInfo("Usage: codeproof <command>\n\nCommands:\n init Initialize CodeProof in a Git repository\n run Run CodeProof checks (stub)\n report@dashboard Send latest report and show dashboard link\n move-secret Move high-confidence secrets to .env");
13
+ logInfo("Usage: codeproof <command>\n\nCommands:\n init Initialize CodeProof in a Git repository\n run Run CodeProof checks (stub)\n report@dashboard Send latest report and show dashboard link\n move-secret Move high-confidence secrets to .env\n whoami Show the local CodeProof client ID");
13
14
  process.exit(0);
14
15
  }
15
16
 
@@ -33,6 +34,11 @@ async function main() {
33
34
  return;
34
35
  }
35
36
 
37
+ if (command === "whoami") {
38
+ await runWhoAmI();
39
+ return;
40
+ }
41
+
36
42
  logError(`Unknown command: ${command}`);
37
43
  process.exit(1);
38
44
  }
package/commands/init.js CHANGED
@@ -5,11 +5,15 @@ import { logInfo, logSuccess, logWarn } from "../utils/logger.js";
5
5
  import { detectProjectType } from "../utils/projectType.js";
6
6
  import { installPreCommitHook } from "../hooks/preCommit.js";
7
7
  import { showWelcomeScreen } from "../ui/welcomeScreen.js";
8
+ import { getClientId } from "../core/identity.js";
9
+ import { randomUUID } from "crypto";
8
10
 
9
11
 
10
12
 
11
13
  export async function runInit({ cwd }) {
12
14
  logInfo("Initializing CodeProof...");
15
+
16
+ getClientId();
13
17
 
14
18
  ensureGitRepo(cwd);
15
19
  logSuccess("Git repository detected.");
@@ -23,9 +27,27 @@ export async function runInit({ cwd }) {
23
27
  const configPath = path.join(gitRoot, "codeproof.config.json");
24
28
  // Avoid overwriting user configuration to keep init idempotent.
25
29
  if (fs.existsSync(configPath)) {
26
- logWarn("Config already exists. Skipping creation.");
30
+ let updated = false;
31
+ try {
32
+ const raw = fs.readFileSync(configPath, "utf8");
33
+ const existing = JSON.parse(raw);
34
+ if (!existing.projectId) {
35
+ existing.projectId = randomUUID();
36
+ updated = true;
37
+ fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
38
+ }
39
+ } catch {
40
+ logWarn("Config already exists but could not be updated.");
41
+ }
42
+
43
+ if (updated) {
44
+ logSuccess("Added projectId to codeproof.config.json");
45
+ } else {
46
+ logWarn("Config already exists. Skipping creation.");
47
+ }
27
48
  } else {
28
49
  const config = {
50
+ projectId: randomUUID(),
29
51
  projectType,
30
52
  scanMode: "staged",
31
53
  features: {
@@ -5,6 +5,7 @@ import { logError, logInfo, logWarn } from "../utils/logger.js";
5
5
  import { sendReportToServer } from "../utils/apiClient.js";
6
6
  import { resolveFeatureFlags, isVerbose } from "../core/featureFlags.js";
7
7
  import { reportFeatureDisabled, withFailOpenIntegration } from "../core/safetyGuards.js";
8
+ import { readLatestReport } from "../reporting/reportReader.js";
8
9
 
9
10
  function readConfig(configPath) {
10
11
  if (!fs.existsSync(configPath)) {
@@ -21,24 +22,6 @@ function readConfig(configPath) {
21
22
  }
22
23
  }
23
24
 
24
- function readLatestReport(reportPath) {
25
- if (!fs.existsSync(reportPath)) {
26
- return null;
27
- }
28
-
29
- const content = fs.readFileSync(reportPath, "utf8");
30
- const lines = content.split(/\r?\n/).filter(Boolean);
31
- if (lines.length === 0) {
32
- return null;
33
- }
34
-
35
- try {
36
- return JSON.parse(lines[lines.length - 1]);
37
- } catch {
38
- return null;
39
- }
40
- }
41
-
42
25
  export async function runReportDashboard({ cwd }) {
43
26
  // Boundary: CLI orchestration only. Avoid importing this module in lower layers.
44
27
  ensureGitRepo(cwd);
@@ -49,30 +32,26 @@ export async function runReportDashboard({ cwd }) {
49
32
  const verbose = isVerbose(config);
50
33
 
51
34
  if (features.reporting) {
52
- const reportPath = path.join(gitRoot, "codeproof-report.log");
53
- const latestReport = readLatestReport(reportPath);
35
+ const latestEntry = readLatestReport(gitRoot);
36
+ const latestReport = latestEntry?.report || null;
54
37
 
55
38
  if (!latestReport) {
56
- logWarn("No reports found. Run 'codeproof run' first.");
39
+ logWarn("No reports found. Run `codeproof run` first.");
57
40
  } else {
58
41
  const integration = config?.integration || {};
59
- const integrationEnabled = features.integration && Boolean(integration.enabled);
60
- if (integrationEnabled) {
61
- // Integrations are fail-open: skip silently when disabled, never throw on network errors.
62
- withFailOpenIntegration(() => {
63
- sendReportToServer(latestReport, {
64
- enabled: true,
65
- endpointUrl: integration.endpointUrl
66
- });
42
+ // Integrations are fail-open: never throw on network errors.
43
+ withFailOpenIntegration(() => {
44
+ sendReportToServer(latestReport, {
45
+ enabled: true,
46
+ endpointUrl: integration.endpointUrl
67
47
  });
68
- } else {
69
- reportFeatureDisabled("Integration", verbose, logInfo);
48
+ });
49
+
50
+ if (latestReport?.projectId) {
51
+ logInfo(`View dashboard: https://dashboard.codeproof.dev/project/${latestReport.projectId}`);
70
52
  }
71
53
  }
72
54
  } else {
73
55
  reportFeatureDisabled("Reporting", verbose, logInfo);
74
56
  }
75
-
76
- const projectId = encodeURIComponent(path.basename(gitRoot) || "project");
77
- logInfo(`Dashboard: https://dashboard.codeproof.dev/project/${projectId}`);
78
57
  }
package/commands/run.js CHANGED
@@ -2,15 +2,16 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { ensureGitRepo, getGitRoot, getStagedFiles } from "../utils/git.js";
4
4
  import { logError, logInfo, logSuccess, logWarn } from "../utils/logger.js";
5
- import { getDefaultExcludes, isBinaryFile, listFilesRecursive } from "../utils/files.js";
5
+ import { buildScanTargets } from "../utils/fileScanner.js";
6
6
  import { runRuleEngine } from "../engine/ruleEngine.js";
7
7
  import { analyze } from "../engine/aiAnalyzer.js";
8
8
  import { mergeDecisions } from "../engine/decisionMerger.js";
9
9
  import { buildAiInputs, buildProjectContext } from "../engine/contextBuilder.js";
10
10
  import { buildReport } from "../reporting/reportBuilder.js";
11
- import { appendReport } from "../reporting/reportWriter.js";
11
+ import { writeReport } from "../reporting/reportWriter.js";
12
12
  import { sendReportToServer } from "../utils/apiClient.js";
13
13
  import { resolveFeatureFlags, isVerbose } from "../core/featureFlags.js";
14
+ import { getClientId } from "../core/identity.js";
14
15
  import {
15
16
  reportFeatureDisabled,
16
17
  warnExperimentalOnce,
@@ -18,6 +19,7 @@ import {
18
19
  withFailOpenIntegration,
19
20
  withFailOpenReporting
20
21
  } from "../core/safetyGuards.js";
22
+ import { randomUUID } from "crypto";
21
23
 
22
24
  function readConfig(configPath) {
23
25
  if (!fs.existsSync(configPath)) {
@@ -34,22 +36,6 @@ function readConfig(configPath) {
34
36
  }
35
37
  }
36
38
 
37
- function normalizeScopeFiles(files, gitRoot) {
38
- return files
39
- .map((filePath) => (path.isAbsolute(filePath) ? filePath : path.join(gitRoot, filePath)))
40
- .filter((filePath) => {
41
- try {
42
- return fs.statSync(filePath).isFile();
43
- } catch {
44
- return false;
45
- }
46
- });
47
- }
48
-
49
- function filterBinaryFiles(files) {
50
- return files.filter((filePath) => !isBinaryFile(filePath));
51
- }
52
-
53
39
  export async function runCli({ cwd }) {
54
40
  // Boundary: CLI orchestration only. Avoid importing this module in lower layers.
55
41
  logInfo("CodeProof run started.");
@@ -71,27 +57,30 @@ export async function runCli({ cwd }) {
71
57
 
72
58
  if (scanMode === "staged") {
73
59
  logInfo("Scan mode: staged");
74
- const staged = getStagedFiles(gitRoot);
75
- targets = normalizeScopeFiles(staged, gitRoot);
60
+ targets = buildScanTargets({
61
+ gitRoot,
62
+ scanMode,
63
+ stagedFiles: getStagedFiles(gitRoot)
64
+ });
76
65
  } else if (scanMode === "full") {
77
66
  logInfo("Scan mode: full");
78
- const excludes = getDefaultExcludes();
79
- const allFiles = listFilesRecursive(gitRoot, excludes);
80
- targets = normalizeScopeFiles(allFiles, gitRoot);
67
+ targets = buildScanTargets({
68
+ gitRoot,
69
+ scanMode,
70
+ stagedFiles: []
71
+ });
81
72
  } else {
82
73
  logError("Invalid scanMode. Expected 'staged' or 'full'.");
83
74
  process.exit(1);
84
75
  }
85
76
 
86
- const filtered = filterBinaryFiles(targets);
87
-
88
- if (filtered.length === 0) {
77
+ if (targets.length === 0) {
89
78
  logWarn("No relevant files found. Exiting.");
90
79
  // Exit code 0 allows the Git commit to continue.
91
80
  process.exit(0);
92
81
  }
93
82
 
94
- const { findings, escalations } = runRuleEngine({ files: filtered });
83
+ const { findings, escalations } = runRuleEngine({ files: targets });
95
84
  const projectContext = buildProjectContext({ gitRoot, config });
96
85
  let aiInputs = [];
97
86
  if (features.aiEscalation) {
@@ -116,18 +105,22 @@ export async function runCli({ cwd }) {
116
105
  if (features.reporting) {
117
106
  withFailOpenReporting(() => {
118
107
  const timestamp = new Date().toISOString();
119
- const runId = `${Date.now()}-${process.pid}`;
108
+ const reportId = randomUUID();
109
+ const projectId = config.projectId || "";
110
+ const clientId = getClientId();
120
111
  const report = buildReport({
121
112
  projectRoot: gitRoot,
113
+ projectId,
114
+ clientId,
115
+ reportId,
122
116
  scanMode,
123
- filesScannedCount: filtered.length,
117
+ filesScannedCount: targets.length,
124
118
  baselineFindings: [...findings, ...escalations],
125
119
  aiReviewed,
126
- runId,
127
120
  timestamp
128
121
  });
129
122
  // Reporting is fail-open: never block commits if logging fails.
130
- appendReport({ projectRoot: gitRoot, report });
123
+ writeReport({ projectRoot: gitRoot, report });
131
124
 
132
125
  const integration = config?.integration || {};
133
126
  const integrationEnabled = features.integration && Boolean(integration.enabled);
@@ -0,0 +1,19 @@
1
+ import { readClientId, getGlobalConfigPath } from "../core/identity.js";
2
+ import { logError, logInfo } from "../utils/logger.js";
3
+
4
+ export async function runWhoAmI() {
5
+ const clientId = readClientId();
6
+
7
+ if (!clientId) {
8
+ logError("CodeProof not initialized. Run codeproof init first.");
9
+ process.exit(1);
10
+ }
11
+
12
+ logInfo("CodeProof Client ID:");
13
+ logInfo(clientId);
14
+ logInfo("");
15
+ logInfo("Use this ID to log in at:");
16
+ logInfo("https://your-dashboard-url.com/login");
17
+ logInfo("");
18
+ logInfo(`Config: ${getGlobalConfigPath()}`);
19
+ }
@@ -0,0 +1,78 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { randomUUID } from "crypto";
5
+
6
+ const CONFIG_DIR_NAME = ".codeproof";
7
+ const CONFIG_FILE_NAME = "config.json";
8
+
9
+ function getConfigDir() {
10
+ return path.join(os.homedir(), CONFIG_DIR_NAME);
11
+ }
12
+
13
+ function getConfigPath() {
14
+ return path.join(getConfigDir(), CONFIG_FILE_NAME);
15
+ }
16
+
17
+ function ensureConfigDir() {
18
+ const dir = getConfigDir();
19
+ if (!fs.existsSync(dir)) {
20
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
21
+ }
22
+ try {
23
+ fs.chmodSync(dir, 0o700);
24
+ } catch {
25
+ // Best-effort on platforms that ignore chmod.
26
+ }
27
+ }
28
+
29
+ function writeConfig(config) {
30
+ const filePath = getConfigPath();
31
+ const payload = JSON.stringify(config, null, 2) + "\n";
32
+ fs.writeFileSync(filePath, payload, { encoding: "utf8", mode: 0o600 });
33
+ try {
34
+ fs.chmodSync(filePath, 0o600);
35
+ } catch {
36
+ // Best-effort on platforms that ignore chmod.
37
+ }
38
+ }
39
+
40
+ function readConfig() {
41
+ const filePath = getConfigPath();
42
+ if (!fs.existsSync(filePath)) {
43
+ return null;
44
+ }
45
+
46
+ try {
47
+ const raw = fs.readFileSync(filePath, "utf8");
48
+ return JSON.parse(raw);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function ensureClientId() {
55
+ ensureConfigDir();
56
+
57
+ const existing = readConfig();
58
+ if (existing?.clientId) {
59
+ return existing.clientId;
60
+ }
61
+
62
+ const clientId = randomUUID();
63
+ writeConfig({ clientId });
64
+ return clientId;
65
+ }
66
+
67
+ export function getClientId() {
68
+ return ensureClientId();
69
+ }
70
+
71
+ export function readClientId() {
72
+ const existing = readConfig();
73
+ return existing?.clientId ?? null;
74
+ }
75
+
76
+ export function getGlobalConfigPath() {
77
+ return getConfigPath();
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeproof",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "CodeProof CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,5 +9,8 @@
9
9
  "engines": {
10
10
  "node": ">=18"
11
11
  },
12
- "license": "MIT"
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "uuid": "^13.0.0"
15
+ }
13
16
  }
@@ -45,11 +45,13 @@ function normalizeSeverity(value) {
45
45
 
46
46
  export function buildReport({
47
47
  projectRoot,
48
+ projectId,
49
+ clientId,
50
+ reportId,
48
51
  scanMode,
49
52
  filesScannedCount,
50
53
  baselineFindings,
51
54
  aiReviewed,
52
- runId,
53
55
  timestamp
54
56
  }) {
55
57
  const aiById = new Map(aiReviewed.map((entry) => [entry.finding.findingId, entry.decision]));
@@ -84,9 +86,10 @@ export function buildReport({
84
86
  : "allowed";
85
87
 
86
88
  return {
87
- runId,
89
+ reportId,
88
90
  timestamp,
89
- projectRoot,
91
+ projectId,
92
+ clientId,
90
93
  scanMode,
91
94
  summary: {
92
95
  totalFilesScanned: filesScannedCount,
@@ -0,0 +1,49 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const REPORT_DIR_NAME = "codeproof-reports";
5
+ const REPORT_PATTERN = /^report-(\d+)\.json$/;
6
+
7
+ function getReportDir(projectRoot) {
8
+ return path.join(projectRoot, REPORT_DIR_NAME);
9
+ }
10
+
11
+ function getReportNumbers(files) {
12
+ return files
13
+ .map((file) => {
14
+ const match = REPORT_PATTERN.exec(file);
15
+ return match ? Number.parseInt(match[1], 10) : null;
16
+ })
17
+ .filter((value) => Number.isInteger(value));
18
+ }
19
+
20
+ export function readLatestReport(projectRoot) {
21
+ const reportDir = getReportDir(projectRoot);
22
+ if (!fs.existsSync(reportDir)) {
23
+ return null;
24
+ }
25
+
26
+ let files = [];
27
+ try {
28
+ files = fs.readdirSync(reportDir);
29
+ } catch {
30
+ return null;
31
+ }
32
+
33
+ const numbers = getReportNumbers(files).sort((a, b) => b - a);
34
+ if (numbers.length === 0) {
35
+ return null;
36
+ }
37
+
38
+ for (const number of numbers) {
39
+ const reportPath = path.join(reportDir, `report-${number}.json`);
40
+ try {
41
+ const raw = fs.readFileSync(reportPath, "utf8");
42
+ return { report: JSON.parse(raw), reportPath };
43
+ } catch {
44
+ // Skip unreadable or invalid JSON reports instead of crashing.
45
+ }
46
+ }
47
+
48
+ return null;
49
+ }
@@ -1,19 +1,62 @@
1
1
  import fs from "fs";
2
- import os from "os";
3
2
  import path from "path";
4
3
 
4
+ const REPORT_DIR_NAME = "codeproof-reports";
5
+ const REPORT_PREFIX = "report-";
6
+ const REPORT_SUFFIX = ".json";
7
+ const REPORT_PATTERN = /^report-(\d+)\.json$/;
8
+
5
9
  // Boundary: reporting storage only. Must not import rule logic, AI logic, or integrations.
6
10
 
7
- export function appendReport({ projectRoot, report }) {
8
- const reportPath = path.join(projectRoot, "codeproof-report.log");
9
- const line = JSON.stringify(report) + os.EOL;
11
+ function getReportDir(projectRoot) {
12
+ return path.join(projectRoot, REPORT_DIR_NAME);
13
+ }
14
+
15
+ function ensureReportDir(reportDir) {
16
+ if (!fs.existsSync(reportDir)) {
17
+ fs.mkdirSync(reportDir, { recursive: true });
18
+ }
19
+ }
10
20
 
11
- // Append-only to preserve an immutable audit trail of every run.
12
- const fileHandle = fs.openSync(reportPath, "a");
21
+ function getNextReportNumber(reportDir) {
22
+ let files = [];
13
23
  try {
14
- fs.writeSync(fileHandle, line, null, "utf8");
15
- fs.fsyncSync(fileHandle);
16
- } finally {
17
- fs.closeSync(fileHandle);
24
+ files = fs.readdirSync(reportDir);
25
+ } catch {
26
+ return 1;
27
+ }
28
+
29
+ const numbers = files
30
+ .map((file) => {
31
+ const match = REPORT_PATTERN.exec(file);
32
+ return match ? Number.parseInt(match[1], 10) : null;
33
+ })
34
+ .filter((value) => Number.isInteger(value));
35
+
36
+ if (numbers.length === 0) {
37
+ return 1;
18
38
  }
39
+
40
+ return Math.max(...numbers) + 1;
41
+ }
42
+
43
+ export function writeReport({ projectRoot, report }) {
44
+ const reportDir = getReportDir(projectRoot);
45
+ ensureReportDir(reportDir);
46
+
47
+ // Per-run JSON keeps every audit entry immutable and easy to archive.
48
+ let reportNumber = getNextReportNumber(reportDir);
49
+ let reportPath = path.join(reportDir, `${REPORT_PREFIX}${reportNumber}${REPORT_SUFFIX}`);
50
+ while (fs.existsSync(reportPath)) {
51
+ reportNumber += 1;
52
+ reportPath = path.join(reportDir, `${REPORT_PREFIX}${reportNumber}${REPORT_SUFFIX}`);
53
+ }
54
+
55
+ // Use numeric sequencing over timestamps to avoid collisions in fast CI runs.
56
+ const tempPath = path.join(reportDir, `.tmp-${process.pid}-${Date.now()}.json`);
57
+ const payload = JSON.stringify(report, null, 2) + "\n";
58
+ fs.writeFileSync(tempPath, payload, "utf8");
59
+ fs.renameSync(tempPath, reportPath);
60
+
61
+ return reportPath;
19
62
  }
@@ -0,0 +1,40 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { filterIgnoredFiles } from "./gitIgnore.js";
4
+ import { getDefaultExcludes, isBinaryFile, listFilesRecursive } from "./files.js";
5
+
6
+ function normalizeScopeFiles(files, gitRoot) {
7
+ return files
8
+ .map((filePath) => (path.isAbsolute(filePath) ? filePath : path.join(gitRoot, filePath)))
9
+ .filter((filePath) => {
10
+ try {
11
+ return fs.statSync(filePath).isFile();
12
+ } catch {
13
+ return false;
14
+ }
15
+ });
16
+ }
17
+
18
+ function isExcludedByDefault(filePath, gitRoot, excludes) {
19
+ const relative = path.relative(gitRoot, filePath);
20
+ const segments = relative.split(path.sep);
21
+ return segments.some((segment) => excludes.has(segment));
22
+ }
23
+
24
+ export function buildScanTargets({ gitRoot, scanMode, stagedFiles }) {
25
+ const excludes = getDefaultExcludes();
26
+
27
+ let scopeFiles = [];
28
+ if (scanMode === "staged") {
29
+ const normalized = normalizeScopeFiles(stagedFiles, gitRoot);
30
+ scopeFiles = normalized.filter((filePath) => !isExcludedByDefault(filePath, gitRoot, excludes));
31
+ } else {
32
+ scopeFiles = normalizeScopeFiles(listFilesRecursive(gitRoot, excludes), gitRoot);
33
+ }
34
+
35
+ // Respect .gitignore via `git check-ignore` so scans only touch tracked sources.
36
+ const notIgnored = filterIgnoredFiles({ gitRoot, filePaths: scopeFiles });
37
+
38
+ // Exclude binary files to avoid false positives and wasted I/O.
39
+ return notIgnored.filter((filePath) => !isBinaryFile(filePath));
40
+ }
@@ -0,0 +1,55 @@
1
+ import path from "path";
2
+ import { spawnSync } from "child_process";
3
+
4
+ function normalizePathForGit(filePath, gitRoot) {
5
+ const relative = path.isAbsolute(filePath)
6
+ ? path.relative(gitRoot, filePath)
7
+ : filePath;
8
+
9
+ if (!relative || relative.startsWith("..")) {
10
+ return null;
11
+ }
12
+
13
+ return relative.replace(/\\/g, "/");
14
+ }
15
+
16
+ export function filterIgnoredFiles({ gitRoot, filePaths }) {
17
+ if (!Array.isArray(filePaths) || filePaths.length === 0) {
18
+ return [];
19
+ }
20
+
21
+ const normalized = new Map();
22
+ for (const filePath of filePaths) {
23
+ const relative = normalizePathForGit(filePath, gitRoot);
24
+ if (relative) {
25
+ normalized.set(relative, filePath);
26
+ }
27
+ }
28
+
29
+ if (normalized.size === 0) {
30
+ return [];
31
+ }
32
+
33
+ // Use `git check-ignore` to respect .gitignore rules quickly and accurately.
34
+ const input = `${Array.from(normalized.keys()).join("\u0000")}\u0000`;
35
+ const result = spawnSync("git", ["check-ignore", "--stdin", "-z"], {
36
+ cwd: gitRoot,
37
+ input,
38
+ encoding: "utf8"
39
+ });
40
+
41
+ if (result.error || (result.status && result.status > 1)) {
42
+ return Array.from(normalized.values());
43
+ }
44
+
45
+ const ignored = new Set(
46
+ String(result.stdout)
47
+ .split("\u0000")
48
+ .map((entry) => entry.trim())
49
+ .filter(Boolean)
50
+ );
51
+
52
+ return Array.from(normalized.entries())
53
+ .filter(([relative]) => !ignored.has(relative))
54
+ .map(([, original]) => original);
55
+ }