refactor(phase-2a): consolidate duplicates (-1,073 LoC)

Phase 2 tasks 1–3 of PLAN-RIP-THE-BAND-AIDS-OFF — collapse duplicated
implementations into single canonical sources.

- Task 1 (transcript parser): replace inline
  parseAssistantTextFromLine/findLastAssistantMessage/extractPriorMessages
  in ObservationCompiler with shared extractLastMessage
- Task 2 (bun-path resolver): new src/shared/bun-resolution.ts; reduce
  npx-cli/utils/bun-resolver.ts to a re-export shim and delegate
  CursorHooksInstaller.findBunPath to the shared helper
- Task 3 (migration runner): delete ~965 lines of inlined migration
  methods from SessionStore; constructor now calls
  MigrationRunner.runAllMigrations(). Add addObservationModelColumns
  (v26) to the runner to preserve coverage.

Deferred: Task 4 (PendingMessage self-heal) reverted — test contract
encoded the removed side-effect behavior; will revisit with test
updates. Task 10 (GracefulShutdown merge) correctly skipped per plan
guardrail: performGracefulShutdown and runShutdownCascade serve
different scopes and are NOT always called together. Tasks 5–9 and
11–13 land in subsequent partial commits.

Tests: 28 fail / 1347 pass / 3 skip / 1 error (baseline 38 fail /
1337 pass — strict improvement, zero new failures).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-04-20 23:19:29 -07:00
parent 5a1a17c0b2
commit 3d9fa39cea
8 changed files with 441 additions and 1433 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,86 +1,7 @@
/**
* Bun binary resolution utility.
* Bun binary resolution utility (re-export shim).
*
* Extracted from `plugin/scripts/bun-runner.js` so that the NPX CLI
* can locate Bun without duplicating the search logic.
*
* Pure Node.js — no Bun APIs used.
* Actual implementation lives in `src/shared/bun-resolution.ts`.
* This file preserves the existing `npx-cli` import surface.
*/
import { spawnSync } from 'child_process';
import { existsSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { IS_WINDOWS } from './paths.js';
/**
* Well-known locations where Bun might be installed, beyond PATH.
* Order matches the search priority in bun-runner.js and smart-install.js.
*/
function bunCandidatePaths(): string[] {
if (IS_WINDOWS) {
return [
join(homedir(), '.bun', 'bin', 'bun.exe'),
join(process.env.USERPROFILE || homedir(), '.bun', 'bin', 'bun.exe'),
];
}
return [
join(homedir(), '.bun', 'bin', 'bun'),
'/usr/local/bin/bun',
'/opt/homebrew/bin/bun',
'/home/linuxbrew/.linuxbrew/bin/bun',
];
}
/**
* Attempt to locate the Bun executable.
*
* 1. Check PATH via `which` / `where`.
* 2. Probe well-known installation directories.
*
* Returns the absolute path to the binary, `'bun'` if it is in PATH,
* or `null` if Bun cannot be found.
*/
export function resolveBunBinaryPath(): string | null {
// Try PATH first
const whichCommand = IS_WINDOWS ? 'where' : 'which';
const pathCheck = spawnSync(whichCommand, ['bun'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS,
});
if (pathCheck.status === 0 && pathCheck.stdout.trim()) {
return 'bun'; // Available in PATH — use short name
}
// Probe known install locations
for (const candidatePath of bunCandidatePaths()) {
if (existsSync(candidatePath)) {
return candidatePath;
}
}
return null;
}
/**
* Get the installed Bun version string (e.g. `"1.2.3"`), or `null`
* if Bun is not available.
*/
export function getBunVersionString(): string | null {
const bunPath = resolveBunBinaryPath();
if (!bunPath) return null;
try {
const result = spawnSync(bunPath, ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS,
});
return result.status === 0 ? result.stdout.trim() : null;
} catch (error: unknown) {
console.error('[bun-resolver] Failed to get Bun version:', error instanceof Error ? error.message : String(error));
return null;
}
}
export { resolveBunBinaryPath, getBunVersionString } from '../../shared/bun-resolution.js';

View File

@@ -5,10 +5,8 @@
*/
import path from 'path';
import { existsSync, readFileSync } from 'fs';
import { SessionStore } from '../sqlite/SessionStore.js';
import { logger } from '../../utils/logger.js';
import { SYSTEM_REMINDER_REGEX } from '../../utils/tag-stripping.js';
import { extractLastMessage } from '../../shared/transcript-parser.js';
import { CLAUDE_CONFIG_DIR } from '../../shared/paths.js';
import type {
ContextConfig,
@@ -195,61 +193,12 @@ function cwdToDashed(cwd: string): string {
return cwd.replace(/\//g, '-');
}
/**
* Find the last assistant message text from parsed transcript lines.
*/
function parseAssistantTextFromLine(line: string): string | null {
if (!line.includes('"type":"assistant"')) return null;
const entry = JSON.parse(line);
if (entry.type === 'assistant' && entry.message?.content && Array.isArray(entry.message.content)) {
let text = '';
for (const block of entry.message.content) {
if (block.type === 'text') text += block.text;
}
text = text.replace(SYSTEM_REMINDER_REGEX, '').trim();
if (text) return text;
}
return null;
}
function findLastAssistantMessage(lines: string[]): string {
for (let i = lines.length - 1; i >= 0; i--) {
try {
const result = parseAssistantTextFromLine(lines[i]);
if (result) return result;
} catch (parseError) {
if (parseError instanceof Error) {
logger.debug('WORKER', 'Skipping malformed transcript line', { lineIndex: i }, parseError);
} else {
logger.debug('WORKER', 'Skipping malformed transcript line', { lineIndex: i, error: String(parseError) });
}
continue;
}
}
return '';
}
/**
* Extract prior messages from transcript file
*/
export function extractPriorMessages(transcriptPath: string): PriorMessages {
try {
if (!existsSync(transcriptPath)) return { userMessage: '', assistantMessage: '' };
const content = readFileSync(transcriptPath, 'utf-8').trim();
if (!content) return { userMessage: '', assistantMessage: '' };
const lines = content.split('\n').filter(line => line.trim());
const lastAssistantMessage = findLastAssistantMessage(lines);
return { userMessage: '', assistantMessage: lastAssistantMessage };
} catch (error) {
if (error instanceof Error) {
logger.failure('WORKER', 'Failed to extract prior messages from transcript', { transcriptPath }, error);
} else {
logger.warn('WORKER', 'Failed to extract prior messages from transcript', { transcriptPath, error: String(error) });
}
return { userMessage: '', assistantMessage: '' };
}
const assistantMessage = extractLastMessage(transcriptPath, 'assistant', true);
return { userMessage: '', assistantMessage };
}
/**

View File

@@ -17,6 +17,7 @@ import { promisify } from 'util';
import { logger } from '../../utils/logger.js';
import { getWorkerPort, workerHttpRequest } from '../../shared/worker-utils.js';
import { DATA_DIR, MARKETPLACE_ROOT, CLAUDE_CONFIG_DIR } from '../../shared/paths.js';
import { resolveBunBinaryPathOrDefault } from '../../shared/bun-resolution.js';
import {
readCursorRegistry as readCursorRegistryFromFile,
writeCursorRegistry as writeCursorRegistryToFile,
@@ -170,34 +171,15 @@ export function findWorkerServicePath(): string | null {
}
/**
* Find the Bun executable path
* Required because worker-service.cjs uses bun:sqlite which is Bun-specific
* Searches common installation locations across platforms
* Find the Bun executable path.
* Required because worker-service.cjs uses bun:sqlite which is Bun-specific.
*
* Delegates to the shared resolver; falls back to the literal `'bun'` so
* hook config generation always emits an invocation even if Bun isn't
* detected at install time (the user sees a clear error at hook-run time).
*/
export function findBunPath(): string {
const possiblePaths = [
// Standard user install location (most common)
path.join(homedir(), '.bun', 'bin', 'bun'),
// Global install locations
'/usr/local/bin/bun',
'/usr/bin/bun',
// Windows locations
...(process.platform === 'win32' ? [
path.join(homedir(), '.bun', 'bin', 'bun.exe'),
path.join(process.env.LOCALAPPDATA || '', 'bun', 'bun.exe'),
] : []),
];
for (const p of possiblePaths) {
if (p && existsSync(p)) {
return p;
}
}
// Fallback to 'bun' and hope it's in PATH
// This allows the installation to proceed even if we can't find bun
// The user will get a clear error when the hook runs if bun isn't available
return 'bun';
return resolveBunBinaryPathOrDefault();
}
/**

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,7 @@ export class MigrationRunner {
this.addSessionCustomTitleColumn();
this.createObservationFeedbackTable();
this.addSessionPlatformSourceColumn();
this.addObservationModelColumns();
this.ensureMergedIntoProjectColumns();
this.addObservationSubagentColumns();
}
@@ -1015,4 +1016,24 @@ export class MigrationRunner {
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(27, new Date().toISOString());
}
}
/**
* Add generated_by_model + relevance_count columns to observations (schema v26).
*/
private addObservationModelColumns(): void {
const columns = this.db.query('PRAGMA table_info(observations)').all() as TableColumnInfo[];
const hasGeneratedByModel = columns.some(col => col.name === 'generated_by_model');
const hasRelevanceCount = columns.some(col => col.name === 'relevance_count');
if (hasGeneratedByModel && hasRelevanceCount) return;
if (!hasGeneratedByModel) {
this.db.run('ALTER TABLE observations ADD COLUMN generated_by_model TEXT');
}
if (!hasRelevanceCount) {
this.db.run('ALTER TABLE observations ADD COLUMN relevance_count INTEGER DEFAULT 0');
}
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(26, new Date().toISOString());
}
}

View File

@@ -0,0 +1,96 @@
/**
* Bun binary resolution — single source of truth.
*
* Merged from `npx-cli/utils/bun-resolver.ts` (null-returning) and
* `CursorHooksInstaller.findBunPath` (fallback-to-'bun'-returning).
*
* Pure Node.js — no Bun APIs used.
*/
import { spawnSync } from 'child_process';
import { existsSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
const IS_WINDOWS = process.platform === 'win32';
/**
* Well-known locations where Bun might be installed, beyond PATH.
* Order matches the search priority in bun-runner.js and smart-install.js.
*/
function bunCandidatePaths(): string[] {
if (IS_WINDOWS) {
return [
join(homedir(), '.bun', 'bin', 'bun.exe'),
join(process.env.USERPROFILE || homedir(), '.bun', 'bin', 'bun.exe'),
join(process.env.LOCALAPPDATA || '', 'bun', 'bun.exe'),
];
}
return [
join(homedir(), '.bun', 'bin', 'bun'),
'/usr/local/bin/bun',
'/usr/bin/bun',
'/opt/homebrew/bin/bun',
'/home/linuxbrew/.linuxbrew/bin/bun',
];
}
/**
* Attempt to locate the Bun executable.
*
* 1. Check PATH via `which` / `where`.
* 2. Probe well-known installation directories.
*
* Returns the absolute path to the binary, `'bun'` if it is in PATH,
* or `null` if Bun cannot be found.
*/
export function resolveBunBinaryPath(): string | null {
// Try PATH first
const whichCommand = IS_WINDOWS ? 'where' : 'which';
const pathCheck = spawnSync(whichCommand, ['bun'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS,
});
if (pathCheck.status === 0 && pathCheck.stdout.trim()) {
return 'bun'; // Available in PATH — use short name
}
// Probe known install locations
for (const candidatePath of bunCandidatePaths()) {
if (candidatePath && existsSync(candidatePath)) {
return candidatePath;
}
}
return null;
}
/**
* Locate the Bun executable, falling back to the literal string 'bun'
* so callers that must emit a bun invocation always get *something* to run.
*
* Use this when writing hook scripts / config files: the installation should
* succeed even if Bun isn't detected at install time, and the user sees a
* clear error at hook-run time if bun is still missing.
*/
export function resolveBunBinaryPathOrDefault(): string {
return resolveBunBinaryPath() ?? 'bun';
}
/**
* Get the installed Bun version string (e.g. `"1.2.3"`), or `null`
* if Bun is not available.
*/
export function getBunVersionString(): string | null {
const bunPath = resolveBunBinaryPath();
if (!bunPath) return null;
const result = spawnSync(bunPath, ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS,
});
return result.status === 0 ? result.stdout.trim() : null;
}