(opts)
| 468 | // -- Commands ----------------------------------------------------------------- |
| 469 | |
| 470 | function runScan(opts) { |
| 471 | const currentHead = getCurrentHead(); |
| 472 | |
| 473 | if (!currentHead) { |
| 474 | console.error('[watch] Not a git repository or git not available.'); |
| 475 | process.exit(1); |
| 476 | } |
| 477 | |
| 478 | if (!acquireLock()) { |
| 479 | // Another process holds the lock. If it recorded a scan start within the |
| 480 | // overlap window, this is a concurrent scan: skip cleanly. |
| 481 | const lockedState = readState(); |
| 482 | const startedAt = lockedState.scanStartedAt |
| 483 | ? Date.parse(lockedState.scanStartedAt) |
| 484 | : NaN; |
| 485 | if (Number.isFinite(startedAt) && Date.now() - startedAt < SCAN_OVERLAP_MS) { |
| 486 | const ageSec = Math.max(0, Math.round((Date.now() - startedAt) / 1000)); |
| 487 | console.log( |
| 488 | `[watch] Scan skipped: another scan (pid ${lockedState.scanPid ?? 'unknown'}) started ${ageSec}s ago and still holds the lock.` |
| 489 | ); |
| 490 | process.exit(0); |
| 491 | } |
| 492 | console.error( |
| 493 | `[watch] Could not acquire state lock after ${LOCK_RETRIES} attempts. Remove ${path.relative(ROOT, LOCK_PATH)} if no scan is running.` |
| 494 | ); |
| 495 | process.exit(1); |
| 496 | } |
| 497 | |
| 498 | // process.exit() skips finally blocks, so all exits below happen only after |
| 499 | // this try/finally has released the lock. |
| 500 | let outputText; |
| 501 | try { |
| 502 | const state = readState(); |
| 503 | state.scanStartedAt = new Date().toISOString(); |
| 504 | state.scanPid = process.pid; |
| 505 | writeState(state); |
| 506 | |
| 507 | const changedFiles = getChangedFiles(state.lastScanCommit); |
| 508 | |
| 509 | // Scan for markers in changed files that exist on disk |
| 510 | const allMarkers = []; |
| 511 | for (const relFile of changedFiles) { |
| 512 | const fullPath = path.join(ROOT, relFile); |
| 513 | if (fs.existsSync(fullPath)) { |
| 514 | const markers = scanFileForMarkers(fullPath, relFile); |
| 515 | allMarkers.push(...markers); |
| 516 | } |
| 517 | } |
| 518 | |
| 519 | const categories = categorizeFiles(changedFiles); |
| 520 | |
| 521 | // Dedup markers by hash; write intake items only for never-seen markers |
| 522 | const intakeFiles = []; |
| 523 | const existingIntakeHashes = |
| 524 | opts.intake && allMarkers.length > 0 ? readExistingIntakeHashes() : new Set(); |
| 525 | const now = new Date().toISOString(); |
| 526 | |
| 527 | for (const marker of allMarkers) { |
no test coverage detected