Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from './init-hooks';
import { buildMcpConfig } from './init-mcp';
import { createContainer } from '../infrastructure/di';
import { createStderrProgressHandler, liberateWithProgress } from './progress';
import { createStderrProgressHandler } from './progress';

interface IInitCommandOptions {
yes?: boolean;
Expand Down Expand Up @@ -107,6 +107,7 @@ export function ensureEnvPlaceholder(cwd: string): void {

// ── Main command ─────────────────────────────────────────────────────

/** Run unified project setup: hooks, MCP config, .gitignore, .env, and optional liberate. */
export async function initCommand(options: IInitCommandOptions, logger?: ILogger): Promise<void> {
const log = logger?.child({ command: 'init' });
const cwd = process.cwd();
Expand Down Expand Up @@ -191,21 +192,16 @@ export async function initCommand(options: IInitCommandOptions, logger?: ILogger

if (apiKey) {
process.env.ANTHROPIC_API_KEY = apiKey;
console.log(`Liberating knowledge from ${commitCount} commits with LLM enrichment...`);
console.log(`Extracting knowledge from ${commitCount} commits with LLM enrichment...`);

const container = createContainer({ logger, scope: 'init', enrich: true });
const { liberateService } = container.cradle;

const onProgress = createStderrProgressHandler();

const result = await liberateWithProgress(
() => liberateService.liberate({
maxCommits: commitCount,
enrich: true,
onProgress,
}),
onProgress,
);
const result = await liberateService.liberate({
maxCommits: commitCount,
enrich: true,
onProgress: createStderrProgressHandler(),
});

console.log(
`Commits scanned: ${result.commitsScanned} | ` +
Expand Down
24 changes: 10 additions & 14 deletions src/commands/liberate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { createContainer } from '../infrastructure/di';
import type { ILogger } from '../domain/interfaces/ILogger';
import { createStderrProgressHandler, liberateWithProgress } from './progress';
import { createStderrProgressHandler } from './progress';

interface ILiberateCommandOptions {
since?: string;
Expand All @@ -14,6 +14,7 @@ interface ILiberateCommandOptions {
enrich?: boolean;
}

/** Scan git history, score commits for interest, and extract memories. */
export async function liberateCommand(options: ILiberateCommandOptions, logger?: ILogger): Promise<void> {
const container = createContainer({ logger, scope: 'liberate', enrich: options.enrich });
const { liberateService, llmClient, logger: log } = container.cradle;
Expand Down Expand Up @@ -49,19 +50,14 @@ export async function liberateCommand(options: ILiberateCommandOptions, logger?:
console.log('Dry run — no notes will be written.\n');
}

const onProgress = createStderrProgressHandler();

const result = await liberateWithProgress(
() => liberateService.liberate({
since,
maxCommits,
dryRun: options.dryRun,
threshold,
enrich: options.enrich,
onProgress,
}),
onProgress,
);
const result = await liberateService.liberate({
since,
maxCommits,
dryRun: options.dryRun,
threshold,
enrich: options.enrich,
onProgress: createStderrProgressHandler(),
});

console.log(`Commits scanned: ${result.commitsScanned}`);
console.log(`Commits annotated: ${result.commitsAnnotated}`);
Expand Down
41 changes: 3 additions & 38 deletions src/commands/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,18 @@ import type { ILiberateProgress } from '../application/interfaces/ILiberateServi

/**
* Create a progress callback that writes liberate progress to stderr.
* Uses in-place `\r` updates when stderr is a TTY, newline-based otherwise.
* Each progress update is written on its own line.
*/
export function createStderrProgressHandler(): (p: ILiberateProgress) => void {
const isTTY = process.stderr.isTTY;
let lastLineLength = 0;
let inPlaceStarted = false;

return (p: ILiberateProgress): void => {
if (p.phase === 'triage') {
process.stderr.write(`Found ${p.total} high-interest commits to analyze.\n`);
} else if (p.phase === 'processing') {
const sha = p.sha.slice(0, 7);
const subject = p.subject.length > 60 ? p.subject.slice(0, 57) + '...' : p.subject;
const line = ` [${p.current}/${p.total}] ${sha} ${subject} (${p.factsExtracted} facts)`;

if (isTTY) {
const padded = line.padEnd(lastLineLength, ' ');
lastLineLength = line.length;
inPlaceStarted = true;
process.stderr.write(`\r${padded}`);
} else {
process.stderr.write(`${line}\n`);
}
process.stderr.write(` [${p.current}/${p.total}] ${sha} ${subject} (${p.factsExtracted} facts)\n`);
Comment thread
TonyCasey marked this conversation as resolved.
} else if (p.phase === 'complete') {
if (isTTY && inPlaceStarted) {
process.stderr.write('\n');
inPlaceStarted = false;
}
process.stderr.write(`Done — ${p.factsExtracted} facts extracted from ${p.total} commits.\n`);
}
};
}

/**
* Wrap a liberate call so that if it throws after writing in-place progress,
* a trailing newline is still written to stderr.
*/
export async function liberateWithProgress<T>(
fn: () => Promise<T>,
onProgress: ReturnType<typeof createStderrProgressHandler>,
): Promise<T> {
try {
return await fn();
} finally {
// Ensure terminal is clean if progress was mid-line when error occurred.
// The 'complete' phase handler already writes \n, but if liberate threw
// before emitting 'complete', we need a fallback. We rely on the handler's
// internal state via one final 'complete' call.
onProgress({ phase: 'complete', current: 0, total: 0, sha: '', subject: '', factsExtracted: 0 });
}
}
Loading