mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: add parent/child directory lookup for ccw cli output
- Implement findProjectWithExecution() to search upward through parent directories - Add automatic project path discovery in outputAction - Support explicit --project parameter for manual path specification - Improve error messages with search scope indication - Display project path in formatted output - Enable cross-directory execution without working directory dependency
This commit is contained in:
@@ -28,7 +28,7 @@ import {
|
|||||||
projectExists,
|
projectExists,
|
||||||
getStorageLocationInstructions
|
getStorageLocationInstructions
|
||||||
} from '../tools/storage-manager.js';
|
} from '../tools/storage-manager.js';
|
||||||
import { getHistoryStore } from '../tools/cli-history-store.js';
|
import { getHistoryStore, findProjectWithExecution } from '../tools/cli-history-store.js';
|
||||||
import { createSpinner } from '../utils/ui.js';
|
import { createSpinner } from '../utils/ui.js';
|
||||||
import { loadClaudeCliSettings } from '../tools/claude-cli-tools.js';
|
import { loadClaudeCliSettings } from '../tools/claude-cli-tools.js';
|
||||||
|
|
||||||
@@ -163,6 +163,7 @@ interface OutputViewOptions {
|
|||||||
turn?: string;
|
turn?: string;
|
||||||
raw?: boolean;
|
raw?: boolean;
|
||||||
final?: boolean; // Only output final result with usage hint
|
final?: boolean; // Only output final result with usage hint
|
||||||
|
project?: string; // Optional project path for lookup
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -355,16 +356,21 @@ function showStorageHelp(): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Show cached output for a conversation with pagination
|
* Show cached output for a conversation with pagination
|
||||||
|
* Supports automatic discovery of project path from current directory or parents
|
||||||
*/
|
*/
|
||||||
async function outputAction(conversationId: string | undefined, options: OutputViewOptions): Promise<void> {
|
async function outputAction(conversationId: string | undefined, options: OutputViewOptions): Promise<void> {
|
||||||
if (!conversationId) {
|
if (!conversationId) {
|
||||||
console.error(chalk.red('Error: Conversation ID is required'));
|
console.error(chalk.red('Error: Conversation ID is required'));
|
||||||
console.error(chalk.gray('Usage: ccw cli output <conversation-id> [--offset N] [--limit N]'));
|
console.error(chalk.gray('Usage: ccw cli output <conversation-id> [--offset N] [--limit N] [--project <path>]'));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = getHistoryStore(process.cwd());
|
// Determine project path to use
|
||||||
const result = store.getCachedOutput(
|
let projectPath = options.project || process.cwd();
|
||||||
|
let store = getHistoryStore(projectPath);
|
||||||
|
|
||||||
|
// Try to get result from specified/current directory
|
||||||
|
let result = store.getCachedOutput(
|
||||||
conversationId,
|
conversationId,
|
||||||
options.turn ? parseInt(options.turn) : undefined,
|
options.turn ? parseInt(options.turn) : undefined,
|
||||||
{
|
{
|
||||||
@@ -374,8 +380,31 @@ async function outputAction(conversationId: string | undefined, options: OutputV
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If not found and no explicit project specified, try to find it
|
||||||
|
if (!result && !options.project) {
|
||||||
|
const found = findProjectWithExecution(conversationId, process.cwd());
|
||||||
|
if (found) {
|
||||||
|
projectPath = found.projectPath;
|
||||||
|
store = getHistoryStore(projectPath);
|
||||||
|
result = store.getCachedOutput(
|
||||||
|
conversationId,
|
||||||
|
options.turn ? parseInt(options.turn) : undefined,
|
||||||
|
{
|
||||||
|
offset: parseInt(options.offset || '0'),
|
||||||
|
limit: parseInt(options.limit || '10000'),
|
||||||
|
outputType: options.outputType || 'both'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
const hint = options.project
|
||||||
|
? `in project: ${options.project}`
|
||||||
|
: 'in current directory or parent directories';
|
||||||
console.error(chalk.red(`Error: Execution not found: ${conversationId}`));
|
console.error(chalk.red(`Error: Execution not found: ${conversationId}`));
|
||||||
|
console.error(chalk.gray(` Searched ${hint}`));
|
||||||
|
console.error(chalk.gray('Usage: ccw cli output <conversation-id> [--project <path>]'));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,9 +424,10 @@ async function outputAction(conversationId: string | undefined, options: OutputV
|
|||||||
console.log();
|
console.log();
|
||||||
console.log(chalk.gray('─'.repeat(60)));
|
console.log(chalk.gray('─'.repeat(60)));
|
||||||
console.log(chalk.dim(`Usage: ccw cli output ${conversationId} [options]`));
|
console.log(chalk.dim(`Usage: ccw cli output ${conversationId} [options]`));
|
||||||
console.log(chalk.dim(' --raw Raw output (no hint)'));
|
console.log(chalk.dim(' --raw Raw output (no formatting)'));
|
||||||
console.log(chalk.dim(' --offset <n> Start from byte offset'));
|
console.log(chalk.dim(' --offset <n> Start from byte offset'));
|
||||||
console.log(chalk.dim(' --limit <n> Limit output bytes'));
|
console.log(chalk.dim(' --limit <n> Limit output bytes'));
|
||||||
|
console.log(chalk.dim(' --project <p> Specify project path explicitly'));
|
||||||
console.log(chalk.dim(` --resume ccw cli -p "..." --resume ${conversationId}`));
|
console.log(chalk.dim(` --resume ccw cli -p "..." --resume ${conversationId}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -409,6 +439,7 @@ async function outputAction(conversationId: string | undefined, options: OutputV
|
|||||||
console.log(` ${chalk.gray('Cached:')} ${result.cached ? chalk.green('Yes') : chalk.yellow('No')}`);
|
console.log(` ${chalk.gray('Cached:')} ${result.cached ? chalk.green('Yes') : chalk.yellow('No')}`);
|
||||||
console.log(` ${chalk.gray('Status:')} ${result.status}`);
|
console.log(` ${chalk.gray('Status:')} ${result.status}`);
|
||||||
console.log(` ${chalk.gray('Time:')} ${result.timestamp}`);
|
console.log(` ${chalk.gray('Time:')} ${result.timestamp}`);
|
||||||
|
console.log(` ${chalk.gray('Project:')} ${chalk.cyan(projectPath)}`);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
if (result.stdout) {
|
if (result.stdout) {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, rmdirSync } from 'fs';
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, rmdirSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join, dirname, resolve } from 'path';
|
||||||
import { parseSessionFile, formatConversation, extractConversationPairs, type ParsedSession, type ParsedTurn } from './session-content-parser.js';
|
import { parseSessionFile, formatConversation, extractConversationPairs, type ParsedSession, type ParsedTurn } from './session-content-parser.js';
|
||||||
import { StoragePaths, ensureStorageDir, getProjectId } from '../config/storage-paths.js';
|
import { StoragePaths, ensureStorageDir, getProjectId, getCCWHome } from '../config/storage-paths.js';
|
||||||
import type { CliOutputUnit } from './cli-output-converter.js';
|
import type { CliOutputUnit } from './cli-output-converter.js';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@@ -1404,5 +1404,95 @@ export function closeAllStores(): void {
|
|||||||
storeCache.clear();
|
storeCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find project path that contains the given execution
|
||||||
|
* Searches upward through parent directories and all registered projects
|
||||||
|
* @param conversationId - Execution ID to search for
|
||||||
|
* @param startDir - Starting directory (default: process.cwd())
|
||||||
|
* @returns Object with projectPath and projectId if found, null otherwise
|
||||||
|
*/
|
||||||
|
export function findProjectWithExecution(
|
||||||
|
conversationId: string,
|
||||||
|
startDir: string = process.cwd()
|
||||||
|
): { projectPath: string; projectId: string } | null {
|
||||||
|
// Strategy 1: Search upward in parent directories
|
||||||
|
let currentPath = resolve(startDir);
|
||||||
|
const visited = new Set<string>();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Avoid infinite loops
|
||||||
|
if (visited.has(currentPath)) break;
|
||||||
|
visited.add(currentPath);
|
||||||
|
|
||||||
|
const projectId = getProjectId(currentPath);
|
||||||
|
const paths = StoragePaths.project(currentPath);
|
||||||
|
|
||||||
|
// Check if database exists for this path
|
||||||
|
if (existsSync(paths.historyDb)) {
|
||||||
|
try {
|
||||||
|
const store = getHistoryStore(currentPath);
|
||||||
|
const result = store.getCachedOutput(conversationId);
|
||||||
|
if (result) {
|
||||||
|
return { projectPath: currentPath, projectId };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Database might be locked or corrupted, continue searching
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to parent directory
|
||||||
|
const parentPath = dirname(currentPath);
|
||||||
|
if (parentPath === currentPath) {
|
||||||
|
// Reached filesystem root
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentPath = parentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Search in all registered projects (global search)
|
||||||
|
// This covers cases where execution might be in a completely different project tree
|
||||||
|
const projectsDir = join(getCCWHome(), 'projects');
|
||||||
|
if (existsSync(projectsDir)) {
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(projectsDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
|
||||||
|
const projectId = entry.name;
|
||||||
|
const historyDb = join(projectsDir, projectId, 'cli-history', 'history.db');
|
||||||
|
|
||||||
|
if (!existsSync(historyDb)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Open and query this database directly
|
||||||
|
const db = new Database(historyDb, { readonly: true });
|
||||||
|
const turn = db.prepare(`
|
||||||
|
SELECT * FROM turns
|
||||||
|
WHERE conversation_id = ?
|
||||||
|
ORDER BY turn_number DESC
|
||||||
|
LIMIT 1
|
||||||
|
`).get(conversationId);
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
if (turn) {
|
||||||
|
// Found in this project - return the projectId
|
||||||
|
// Note: projectPath is set to projectId since we don't have the original path stored
|
||||||
|
return { projectPath: projectId, projectId };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip this database (might be corrupted or locked)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Failed to read projects directory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Re-export types from session-content-parser
|
// Re-export types from session-content-parser
|
||||||
export type { ParsedSession, ParsedTurn } from './session-content-parser.js';
|
export type { ParsedSession, ParsedTurn } from './session-content-parser.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user