fix: harden startup and schema repair contracts

Reliability patch covering startup path resolution, install marker compatibility, export CLI request contracts, schema repair safety, hard-stop retry-loop handling, and the PR babysit status helper.
This commit is contained in:
Alex Newman
2026-05-06 18:29:26 -07:00
committed by GitHub
parent bb3dbfdb5a
commit 65f2fd8cdd
29 changed files with 2167 additions and 578 deletions
+132 -90
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env node
import { writeFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { SettingsDefaultsManager } from '../src/shared/SettingsDefaultsManager';
import { pathToFileURL } from 'url';
import { SettingsDefaultsManager } from '../src/shared/SettingsDefaultsManager.js';
import { resolveDataDir } from '../src/shared/paths.js';
import type {
ObservationRecord,
SdkSessionRecord,
@@ -12,100 +13,141 @@ import type {
ExportData
} from './types/export.js';
async function exportMemories(query: string, outputFile: string, project?: string) {
const WORKER_FETCH_TIMEOUT_MS = 30_000;
function parseWorkerPort(rawPort: unknown): number {
if (typeof rawPort !== 'string' || rawPort.trim() === '') {
throw new Error('Invalid CLAUDE_MEM_WORKER_PORT in settings.json: missing');
}
const normalized = rawPort.trim();
const port = Number.parseInt(normalized, 10);
if (!Number.isInteger(port) || port < 1 || port > 65535 || String(port) !== normalized) {
throw new Error(`Invalid CLAUDE_MEM_WORKER_PORT in settings.json: ${rawPort}`);
}
return port;
}
async function fetchWithTimeout(url: string, init?: RequestInit): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), WORKER_FETCH_TIMEOUT_MS);
try {
const settings = SettingsDefaultsManager.loadFromFile(join(homedir(), '.claude-mem', 'settings.json'));
const port = parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
const baseUrl = `http://localhost:${port}`;
console.log(`🔍 Searching for: "${query}"${project ? ` (project: ${project})` : ' (all projects)'}`);
const params = new URLSearchParams({
query,
format: 'json',
limit: '999999'
return await fetch(url, {
...init,
signal: controller.signal,
});
if (project) params.set('project', project);
console.log('📡 Fetching all memories via hybrid search...');
const searchResponse = await fetch(`${baseUrl}/api/search?${params.toString()}`);
if (!searchResponse.ok) {
throw new Error(`Failed to search: ${searchResponse.status} ${searchResponse.statusText}`);
}
const searchData = await searchResponse.json();
const observations: ObservationRecord[] = searchData.observations || [];
const summaries: SessionSummaryRecord[] = searchData.sessions || [];
const prompts: UserPromptRecord[] = searchData.prompts || [];
console.log(`✅ Found ${observations.length} observations`);
console.log(`✅ Found ${summaries.length} session summaries`);
console.log(`✅ Found ${prompts.length} user prompts`);
const memorySessionIds = new Set<string>();
observations.forEach((o) => {
if (o.memory_session_id) memorySessionIds.add(o.memory_session_id);
});
summaries.forEach((s) => {
if (s.memory_session_id) memorySessionIds.add(s.memory_session_id);
});
console.log('📡 Fetching SDK sessions metadata...');
let sessions: SdkSessionRecord[] = [];
if (memorySessionIds.size > 0) {
const sessionsResponse = await fetch(`${baseUrl}/api/sdk-sessions/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sdkSessionIds: Array.from(memorySessionIds) })
});
if (sessionsResponse.ok) {
sessions = await sessionsResponse.json();
} else {
console.warn(`⚠️ Failed to fetch SDK sessions: ${sessionsResponse.status}`);
}
}
console.log(`✅ Found ${sessions.length} SDK sessions`);
const exportData: ExportData = {
exportedAt: new Date().toISOString(),
exportedAtEpoch: Date.now(),
query,
project,
totalObservations: observations.length,
totalSessions: sessions.length,
totalSummaries: summaries.length,
totalPrompts: prompts.length,
observations,
sessions,
summaries,
prompts
};
writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
console.log(`\n📦 Export complete!`);
console.log(`📄 Output: ${outputFile}`);
console.log(`📊 Stats:`);
console.log(`${exportData.totalObservations} observations`);
console.log(`${exportData.totalSessions} sessions`);
console.log(`${exportData.totalSummaries} summaries`);
console.log(`${exportData.totalPrompts} prompts`);
} catch (error) {
console.error('❌ Export failed:', error);
process.exit(1);
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`Worker request timed out after ${WORKER_FETCH_TIMEOUT_MS}ms: ${url}`);
}
throw error;
} finally {
clearTimeout(timeout);
}
}
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: npx tsx scripts/export-memories.ts <query> <output-file> [--project=name]');
console.error('Example: npx tsx scripts/export-memories.ts "windows" windows-memories.json --project=claude-mem');
console.error(' npx tsx scripts/export-memories.ts "authentication" auth.json');
process.exit(1);
export async function exportMemories(query: string, outputFile: string, project?: string) {
const settings = SettingsDefaultsManager.loadFromFile(join(resolveDataDir(), 'settings.json'));
const port = parseWorkerPort(settings.CLAUDE_MEM_WORKER_PORT);
const baseUrl = `http://localhost:${port}`;
console.log(`🔍 Searching for: "${query}"${project ? ` (project: ${project})` : ' (all projects)'}`);
const params = new URLSearchParams({
query,
format: 'json',
limit: '999999'
});
if (project) params.set('project', project);
console.log('📡 Fetching all memories via hybrid search...');
const searchResponse = await fetchWithTimeout(`${baseUrl}/api/search?${params.toString()}`);
if (!searchResponse.ok) {
throw new Error(`Failed to search: ${searchResponse.status} ${searchResponse.statusText}`);
}
const searchData = await searchResponse.json();
const observations: ObservationRecord[] = searchData.observations || [];
const summaries: SessionSummaryRecord[] = searchData.sessions || [];
const prompts: UserPromptRecord[] = searchData.prompts || [];
console.log(`✅ Found ${observations.length} observations`);
console.log(`✅ Found ${summaries.length} session summaries`);
console.log(`✅ Found ${prompts.length} user prompts`);
const memorySessionIds = new Set<string>();
observations.forEach((o) => {
if (o.memory_session_id) memorySessionIds.add(o.memory_session_id);
});
summaries.forEach((s) => {
if (s.memory_session_id) memorySessionIds.add(s.memory_session_id);
});
console.log('📡 Fetching SDK sessions metadata...');
let sessions: SdkSessionRecord[] = [];
if (memorySessionIds.size > 0) {
const sessionsResponse = await fetchWithTimeout(`${baseUrl}/api/sdk-sessions/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ memorySessionIds: Array.from(memorySessionIds) })
});
if (sessionsResponse.ok) {
sessions = await sessionsResponse.json();
} else {
const body = await sessionsResponse.text();
throw new Error(`Failed to fetch SDK sessions: ${sessionsResponse.status} ${sessionsResponse.statusText} ${body}`.trim());
}
}
console.log(`✅ Found ${sessions.length} SDK sessions`);
const exportData: ExportData = {
exportedAt: new Date().toISOString(),
exportedAtEpoch: Date.now(),
query,
project,
totalObservations: observations.length,
totalSessions: sessions.length,
totalSummaries: summaries.length,
totalPrompts: prompts.length,
observations,
sessions,
summaries,
prompts
};
writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
console.log(`\n📦 Export complete!`);
console.log(`📄 Output: ${outputFile}`);
console.log(`📊 Stats:`);
console.log(`${exportData.totalObservations} observations`);
console.log(`${exportData.totalSessions} sessions`);
console.log(`${exportData.totalSummaries} summaries`);
console.log(`${exportData.totalPrompts} prompts`);
}
const [query, outputFile, ...flags] = args;
const project = flags.find(f => f.startsWith('--project='))?.split('=')[1];
function isDirectRun(): boolean {
if (process.env.CLAUDE_MEM_EXPORT_MEMORIES_NO_MAIN === '1') {
return false;
}
return Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
}
exportMemories(query, outputFile, project);
if (isDirectRun()) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: npx tsx scripts/export-memories.ts <query> <output-file> [--project=name]');
console.error('Example: npx tsx scripts/export-memories.ts "windows" windows-memories.json --project=claude-mem');
console.error(' npx tsx scripts/export-memories.ts "authentication" auth.json');
process.exit(1);
}
const [query, outputFile, ...flags] = args;
const project = flags.find(f => f.startsWith('--project='))?.split('=')[1];
exportMemories(query, outputFile, project).catch((error) => {
console.error('❌ Export failed:', error);
process.exit(1);
});
}
+513
View File
@@ -0,0 +1,513 @@
#!/usr/bin/env bun
import { pathToFileURL } from 'url';
type GhResult = {
stdout: string;
stderr: string;
exitCode: number;
};
type PullRequest = {
number: number;
title: string;
url: string;
headRefOid: string;
baseRefName: string;
state: string;
isDraft: boolean;
mergeable: string;
mergeStateStatus: string;
reviewDecision: string;
};
type RepoInfo = {
nameWithOwner: string;
};
type CheckRun = {
bucket: 'pass' | 'fail' | 'pending' | 'skipping' | 'cancel' | string;
completedAt?: string;
description?: string;
link?: string;
name: string;
startedAt?: string;
state: string;
workflow?: string;
};
type Review = {
id: number;
user?: { login?: string };
state: string;
body?: string | null;
commit_id?: string;
submitted_at?: string;
html_url?: string;
};
type ReviewComment = {
user?: { login?: string };
body?: string | null;
commit_id?: string;
path?: string;
line?: number | null;
original_line?: number | null;
updated_at?: string;
created_at?: string;
html_url?: string;
};
type BranchProtection = {
required_status_checks?: {
strict?: boolean;
contexts?: string[];
checks?: Array<{ context?: string; app_id?: number | null }>;
};
required_pull_request_reviews?: {
dismiss_stale_reviews?: boolean;
require_code_owner_reviews?: boolean;
require_last_push_approval?: boolean;
required_approving_review_count?: number;
};
required_signatures?: { enabled?: boolean };
enforce_admins?: { enabled?: boolean };
required_conversation_resolution?: { enabled?: boolean };
allow_force_pushes?: { enabled?: boolean };
};
type BotHint = {
source: string;
author: string;
when: string;
location?: string;
hints: string[];
};
const GH_PENDING_EXIT_CODE = 8;
const BOT_LOGIN_PATTERN = /(coderabbit|greptile)/i;
function runCommand(cmd: string[]): GhResult {
try {
const result = Bun.spawnSync({
cmd,
stdout: 'pipe',
stderr: 'pipe',
});
return {
stdout: new TextDecoder().decode(result.stdout).trim(),
stderr: new TextDecoder().decode(result.stderr).trim(),
exitCode: result.exitCode,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { stdout: '', stderr: message, exitCode: 127 };
}
}
function runGh(args: string[], options: { allowExitCodes?: number[] } = {}): string {
const result = runCommand(['gh', ...args]);
const allowed = new Set([0, ...(options.allowExitCodes ?? [])]);
if (!allowed.has(result.exitCode)) {
const detail = result.stderr || result.stdout || `exit code ${result.exitCode}`;
throw new Error(`gh ${args.join(' ')} failed: ${detail}`);
}
return result.stdout;
}
function parseJson<T>(raw: string, label: string): T {
try {
return JSON.parse(raw) as T;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Could not parse ${label} JSON: ${message}`);
}
}
function checkPrerequisites() {
const git = runCommand(['git', 'rev-parse', '--is-inside-work-tree']);
if (git.exitCode !== 0 || git.stdout.trim() !== 'true') {
throw new Error('Not in a git repository. Run this from a checked-out repo.');
}
const ghVersion = runCommand(['gh', '--version']);
if (ghVersion.exitCode !== 0) {
throw new Error('GitHub CLI is not available. Install gh and try again.');
}
const auth = runCommand(['gh', 'auth', 'status']);
if (auth.exitCode !== 0) {
throw new Error(`GitHub CLI is not authenticated. Run "gh auth login".\n${auth.stderr || auth.stdout}`.trim());
}
}
function targetArgs(prArg?: string): string[] {
return prArg ? [prArg] : [];
}
function fetchPr(prArg?: string): PullRequest {
const fields = [
'number',
'title',
'url',
'headRefOid',
'baseRefName',
'state',
'isDraft',
'mergeable',
'mergeStateStatus',
'reviewDecision',
].join(',');
return parseJson<PullRequest>(
runGh(['pr', 'view', ...targetArgs(prArg), '--json', fields]),
'pull request',
);
}
function fetchRepo(): RepoInfo {
return parseJson<RepoInfo>(
runGh(['repo', 'view', '--json', 'nameWithOwner']),
'repository',
);
}
function fetchChecks(prArg?: string): CheckRun[] {
const fields = [
'bucket',
'completedAt',
'description',
'link',
'name',
'startedAt',
'state',
'workflow',
].join(',');
const raw = runGh(
['pr', 'checks', ...targetArgs(prArg), '--json', fields],
{ allowExitCodes: [GH_PENDING_EXIT_CODE] },
);
return raw ? parseJson<CheckRun[]>(raw, 'checks') : [];
}
function fetchBranchProtection(repo: RepoInfo, branch: string): BranchProtection | undefined {
const [owner, name] = repo.nameWithOwner.split('/');
const endpoint = `repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/branches/${encodeURIComponent(branch)}/protection`;
const result = runCommand(['gh', 'api', endpoint]);
if (result.exitCode !== 0) {
return undefined;
}
return parseJson<BranchProtection>(result.stdout, 'branch protection');
}
function fetchReviews(repo: RepoInfo, prNumber: number): Review[] {
const raw = runGh([
'api',
`repos/${repo.nameWithOwner}/pulls/${prNumber}/reviews`,
'--paginate',
]);
return raw ? parseJson<Review[]>(raw, 'reviews') : [];
}
function fetchReviewComments(repo: RepoInfo, prNumber: number): ReviewComment[] {
const raw = runGh([
'api',
`repos/${repo.nameWithOwner}/pulls/${prNumber}/comments`,
'--paginate',
]);
return raw ? parseJson<ReviewComment[]>(raw, 'review comments') : [];
}
function shortSha(sha: string): string {
return sha.slice(0, 12);
}
function formatBool(value: boolean | undefined): string {
return value ? 'yes' : 'no';
}
function formatCheck(check: CheckRun): string {
const workflow = check.workflow ? `${check.workflow} / ` : '';
const suffix = check.state ? ` (${check.state})` : '';
return `${workflow}${check.name}${suffix}`;
}
export function groupChecks(checks: CheckRun[]): Record<string, CheckRun[]> {
return checks.reduce<Record<string, CheckRun[]>>((groups, check) => {
const bucket = check.bucket || 'unknown';
groups[bucket] ??= [];
groups[bucket].push(check);
return groups;
}, {});
}
function markdownToText(raw: string): string {
return raw
.replace(/<!--[\s\S]*?-->/g, ' ')
.replace(/<details[\s\S]*?<\/details>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/!\[[^\]]*]\([^)]*\)/g, ' ')
.replace(/\[([^\]]+)]\([^)]*\)/g, '$1')
.replace(/[`*_>#|]/g, '')
.replace(/\s+/g, ' ')
.trim();
}
function withoutDetails(raw: string): string {
return raw.replace(/<details[\s\S]*?<\/details>/gi, ' ');
}
function concise(text: string, maxLength = 140): string {
const normalized = markdownToText(text);
if (normalized.length <= maxLength) return normalized;
return `${normalized.slice(0, maxLength - 1).trimEnd()}...`;
}
function firstMarkdownBold(raw: string): string | undefined {
const match = raw.match(/\*\*([^*\n][\s\S]*?)\*\*/);
return match ? concise(match[1]) : undefined;
}
function firstUsefulLine(raw: string): string | undefined {
for (const line of raw.split(/\r?\n/)) {
const hint = concise(line);
if (!hint) continue;
if (/^(details|summary|blockquote|---)$/i.test(hint)) continue;
if (/auto-generated|review info|run configuration|commits$/i.test(hint)) continue;
if (/^(?:\W+\s*)?Potential issue\b/i.test(hint)) continue;
return hint;
}
return undefined;
}
export function extractActionableHints(rawBody: string | null | undefined): string[] {
if (!rawBody) return [];
const hints: string[] = [];
const actionable = rawBody.match(/\*\*Actionable comments posted:\s*([^*]+)\*\*/i);
if (actionable) {
hints.push(`Actionable comments posted: ${actionable[1].trim()}`);
}
const bulletPattern = /^\s*-\s+(?:Around\s+)?(?:Line\s+)?([^:]{0,80}):\s+(.+)$/gim;
for (const match of rawBody.matchAll(bulletPattern)) {
const location = concise(match[1], 64);
const body = concise(match[2]);
if (/^https?:\/\//i.test(body)) continue;
if (body) hints.push(location ? `${location}: ${body}` : body);
}
const bodyWithoutDetails = withoutDetails(rawBody);
const bold = firstMarkdownBold(bodyWithoutDetails);
if (bold && !/^Actionable comments posted:/i.test(bold)) {
hints.push(bold);
}
const usefulLine = firstUsefulLine(bodyWithoutDetails);
if (usefulLine && !bold && !hints.includes(usefulLine)) {
hints.push(usefulLine);
}
return Array.from(new Set(hints)).slice(0, 4);
}
function isBot(login: string | undefined): boolean {
return Boolean(login && BOT_LOGIN_PATTERN.test(login));
}
function currentHeadReviews(reviews: Review[], headSha: string): Review[] {
return reviews
.filter(review => review.commit_id === headSha)
.sort((a, b) => String(a.submitted_at).localeCompare(String(b.submitted_at)));
}
function botHints(reviews: Review[], comments: ReviewComment[], headSha: string): BotHint[] {
const currentBotReviews = reviews.filter(review => review.commit_id === headSha && isBot(review.user?.login));
const earliestCurrentBotReview = currentBotReviews
.map(review => review.submitted_at ?? '')
.filter(Boolean)
.sort()[0];
const reviewHints: BotHint[] = reviews
.filter(review => review.commit_id === headSha && isBot(review.user?.login))
.map(review => ({
source: 'review',
author: review.user?.login ?? 'unknown',
when: review.submitted_at ?? '',
hints: extractActionableHints(review.body),
}))
.filter(item => item.hints.length > 0);
const commentHints: BotHint[] = comments
.filter(comment => {
if (comment.commit_id !== headSha || !isBot(comment.user?.login)) return false;
if (comment.body?.includes('Addressed in commit')) return false;
const when = comment.updated_at ?? comment.created_at ?? '';
return !earliestCurrentBotReview || when >= earliestCurrentBotReview;
})
.map(comment => {
const line = comment.line ?? comment.original_line ?? undefined;
const location = comment.path ? `${comment.path}${line ? `:${line}` : ''}` : undefined;
return {
source: 'comment',
author: comment.user?.login ?? 'unknown',
when: comment.updated_at ?? comment.created_at ?? '',
location,
hints: extractActionableHints(comment.body),
};
})
.filter(item => item.hints.length > 0);
return [...reviewHints, ...commentHints]
.sort((a, b) => b.when.localeCompare(a.when))
.slice(0, 8);
}
function summarizeRequiredChecks(protection: BranchProtection | undefined): string {
if (!protection) return 'unavailable';
const contexts = protection.required_status_checks?.contexts ?? [];
const checks = protection.required_status_checks?.checks
?.map(check => check.context)
.filter((context): context is string => Boolean(context)) ?? [];
const required = Array.from(new Set([...contexts, ...checks]));
if (required.length === 0) return 'none';
const strict = protection.required_status_checks?.strict ? 'strict' : 'not strict';
return `${required.length} (${strict}): ${required.join(', ')}`;
}
export function summarizeProtection(protection: BranchProtection | undefined): string[] {
if (!protection) return ['Branch protection: unavailable or not accessible'];
const reviews = protection.required_pull_request_reviews;
const approvalCount = reviews?.required_approving_review_count ?? 0;
return [
`Required checks: ${summarizeRequiredChecks(protection)}`,
`Required reviews: ${approvalCount || 'none'}${approvalCount ? ` approval${approvalCount === 1 ? '' : 's'}` : ''}`,
`Dismiss stale reviews: ${formatBool(reviews?.dismiss_stale_reviews)}`,
`Code owner reviews: ${formatBool(reviews?.require_code_owner_reviews)}`,
`Last-push approval: ${formatBool(reviews?.require_last_push_approval)}`,
`Conversation resolution: ${formatBool(protection.required_conversation_resolution?.enabled)}`,
`Signed commits: ${formatBool(protection.required_signatures?.enabled)}`,
`Enforce admins: ${formatBool(protection.enforce_admins?.enabled)}`,
`Allow force pushes: ${formatBool(protection.allow_force_pushes?.enabled)}`,
];
}
function printSection(title: string) {
console.log(`\n${title}`);
}
function printList(items: string[], empty: string) {
if (items.length === 0) {
console.log(` ${empty}`);
return;
}
for (const item of items) {
console.log(` - ${item}`);
}
}
function printChecks(checks: CheckRun[]) {
const groups = groupChecks(checks);
const order = ['fail', 'pending', 'pass', 'skipping', 'cancel'];
for (const bucket of order) {
const items = groups[bucket] ?? [];
console.log(` ${bucket}: ${items.length || 'none'}`);
for (const check of items) {
console.log(` - ${formatCheck(check)}`);
}
}
const known = new Set(order);
for (const bucket of Object.keys(groups).filter(bucket => !known.has(bucket)).sort()) {
console.log(` ${bucket}: ${groups[bucket].length}`);
for (const check of groups[bucket]) {
console.log(` - ${formatCheck(check)}`);
}
}
}
function usage() {
console.log(`
PR Babysit Status
Usage:
bun scripts/pr-babysit-status.ts [pr-number]
Without a PR number, gh resolves the PR for the current branch.
`);
}
export async function main(args = process.argv.slice(2)) {
if (args.includes('--help') || args.includes('-h')) {
usage();
return;
}
const prArg = args[0];
checkPrerequisites();
const pr = fetchPr(prArg);
const repo = fetchRepo();
const [checks, protection, reviews, comments] = await Promise.all([
Promise.resolve(fetchChecks(prArg)),
Promise.resolve(fetchBranchProtection(repo, pr.baseRefName)),
Promise.resolve(fetchReviews(repo, pr.number)),
Promise.resolve(fetchReviewComments(repo, pr.number)),
]);
const headReviews = currentHeadReviews(reviews, pr.headRefOid);
const hints = botHints(reviews, comments, pr.headRefOid);
console.log(`PR #${pr.number}: ${pr.title}`);
console.log(`URL: ${pr.url}`);
console.log(`Head: ${shortSha(pr.headRefOid)} (${pr.headRefOid})`);
console.log(`Base: ${pr.baseRefName}`);
console.log(`State: ${pr.state}; draft=${formatBool(pr.isDraft)}; mergeable=${pr.mergeable}; mergeStateStatus=${pr.mergeStateStatus}; reviewDecision=${pr.reviewDecision}`);
printSection(`Checks (${checks.length} current-head)`);
printChecks(checks);
printSection(`Branch Protection (${pr.baseRefName})`);
for (const line of summarizeProtection(protection)) {
console.log(` ${line}`);
}
printSection('Current-Head Reviews');
printList(
headReviews.map(review => {
const author = review.user?.login ?? 'unknown';
const summary = concise(review.body ?? '', 80);
const suffix = summary ? ` - ${summary}` : '';
return `${review.submitted_at ?? 'unknown time'} ${author}: ${review.state}${suffix}`;
}),
'none',
);
printSection('Actionable Bot Hints');
if (hints.length === 0) {
console.log(' none');
} else {
for (const hint of hints) {
const location = hint.location ? ` ${hint.location}` : '';
console.log(` - ${hint.when} ${hint.author} ${hint.source}${location}`);
for (const item of hint.hints) {
console.log(` ${item}`);
}
}
}
}
function isDirectRun(): boolean {
if (process.env.PR_BABYSIT_STATUS_NO_MAIN === '1') {
return false;
}
return Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
}
if (isDirectRun()) {
main().catch(error => {
const message = error instanceof Error ? error.message : String(error);
console.error(`Error: ${message}`);
process.exit(1);
});
}