/** * Shared CLI command utilities. * Common initialization and formatting helpers. * * @module src/cli/commands/shared */ import type { Collection, Config } from "../../config/types"; import type { SyncResult } from "../../ingestion"; import type { SearchResults } from "../../pipeline/types"; import { decorateUriForIndex, getIndexDbPath } from "../../app/constants"; import { getConfigPaths, isInitialized, loadConfig, writeConfigWarningsToStderr, } from "../../config"; import { SqliteAdapter } from "../../store/sqlite/adapter"; /** * Result of CLI store initialization. */ export type InitStoreResult = | { ok: true; store: SqliteAdapter; config: Config; collections: Collection[]; /** Actual config path used (for status reporting) */ actualConfigPath: string; } | { ok: false; error: string }; /** * Options for store initialization. */ export interface InitStoreOptions { /** Override config path */ configPath?: string; /** Index name (defaults to 'default') */ indexName?: string; /** Filter to single collection by name */ collection?: string; /** Sync collections/contexts from config into DB on open */ syncConfig?: boolean; /** Permit management commands that only need an existing index database */ allowEmptyCollections?: boolean; } /** * Initialize store for CLI commands. * Handles: isInitialized check, loadConfig, DB open, syncCollections, syncContexts. * * Caller is responsible for calling store.close() when done. */ export async function initStore( options: InitStoreOptions = {} ): Promise { // Check if initialized const initialized = await isInitialized(options.configPath); if (!initialized) { return { ok: false, error: "GNO not initialized. Run: gno init" }; } // Load config const configResult = await loadConfig(options.configPath); if (!configResult.ok) { return { ok: false, error: configResult.error.message }; } writeConfigWarningsToStderr(configResult.warnings); const config = configResult.value; // Filter to single collection if specified let collections = config.collections; if (options.collection) { collections = collections.filter((c) => c.name === options.collection); if (collections.length === 0) { return { ok: false, error: `Collection not found: ${options.collection}`, }; } } if (collections.length === 0 && !options.allowEmptyCollections) { return { ok: false, error: "No collections configured. Run: gno collection add ", }; } // Ensure data directory exists (may have been deleted by reset) const { ensureDirectories } = await import("../../config"); await ensureDirectories(); // Open database (honor indexName option) const store = new SqliteAdapter(); const dbPath = getIndexDbPath(options.indexName); const paths = getConfigPaths(); // Actual config path used (options.configPath overrides default) const actualConfigPath = options.configPath ?? paths.configFile; // Set configPath for status output store.setConfigPath(actualConfigPath); const openResult = await store.open( dbPath, config.ftsTokenizer, config.busyTimeoutMs ); if (!openResult.ok) { return { ok: false, error: openResult.error.message }; } if (options.syncConfig === false) { return { ok: true, store, config, collections, actualConfigPath }; } // Sync collections from config to DB const syncCollResult = await store.syncCollections(config.collections); if (!syncCollResult.ok) { await store.close(); return { ok: false, error: syncCollResult.error.message }; } // Sync contexts from config to DB const syncCtxResult = await store.syncContexts(config.contexts ?? []); if (!syncCtxResult.ok) { await store.close(); return { ok: false, error: syncCtxResult.error.message }; } return { ok: true, store, config, collections, actualConfigPath }; } export function decorateSearchResultsForIndex( data: SearchResults, indexName?: string ): SearchResults { if (!indexName) { return data; } return { ...data, results: data.results.map((result) => ({ ...result, uri: decorateUriForIndex(result.uri, indexName), })), }; } /** * Format sync result lines (shared between update and index commands). */ export function formatSyncResultLines( syncResult: SyncResult, options: { verbose?: boolean } ): string[] { const lines: string[] = []; if (syncResult.rechunkedMirrors) { lines.push( `Rechunked ${syncResult.rechunkedMirrors} cached mirrors. Run gno embed if embedding was skipped.` ); } for (const c of syncResult.collections) { lines.push(`${c.collection}:`); lines.push( ` ${c.filesAdded} added, ${c.filesUpdated} updated, ${c.filesUnchanged} unchanged` ); if (c.filesErrored > 0) { lines.push(` ${c.filesErrored} errors`); } if (c.filesMarkedInactive > 0) { lines.push(` ${c.filesMarkedInactive} marked inactive`); } for (const file of c.files ?? []) { const receipt = file.recordImport; if ( !receipt || (receipt.failures.length === 0 && receipt.warnings.length === 0) ) continue; const warningCount = receipt.failures.length + receipt.warnings.length; lines.push( ` ${file.relPath}: ${warningCount} record warning${warningCount === 1 ? "" : "s"} (${receipt.snapshotState} snapshot)` ); if (options.verbose) { for (const warning of receipt.warnings) { lines.push( ` [${warning.code}]: ${warning.message} (retryable=${warning.retryable ? "yes" : "no"})` ); } for (const failure of receipt.failures) { const locator = failure.sourceLocator ? ` at ${failure.sourceLocator}` : ""; lines.push( ` [${failure.code}]${locator}: ${failure.message} (retryable=${failure.retryable ? "yes" : "no"})` ); } } } if (options.verbose && c.errors.length > 0) { for (const err of c.errors) { lines.push(` [${err.code}] ${err.relPath}: ${err.message}`); } } if (options.verbose && c.files?.length) { for (const file of c.files) { if (!file.contentType || !file.contentTypeSource) { continue; } lines.push( ` [${file.status}] ${file.relPath}: contentType=${file.contentType} (${file.contentTypeSource})` ); } } } lines.push(""); lines.push( `Total: ${syncResult.totalFilesAdded} added, ${syncResult.totalFilesUpdated} updated` + (syncResult.totalFilesErrored > 0 ? `, ${syncResult.totalFilesErrored} errors` : "") ); lines.push(`Duration: ${syncResult.totalDurationMs}ms`); return lines; }