import path from 'node:path'; import { initProject, loadConfig } from './config.js'; import { detectQmd, installInstructions, runCommand } from './qmd.js'; import { toPosix } from './fs-utils.js'; import { finishBackgroundJob, startBackgroundJob } from './job-state.js'; const OPERATIONS = new Set(['setup', 'update', 'embed']); function collectionNameFor(root, options: any = {}) { return String(options.name || path.basename(root) || 'project').replace(/[^a-zA-Z0-9_.-]+/g, '-'); } function firstMask(config) { return Array.isArray(config.fileGlobs) && config.fileGlobs.length > 0 ? config.fileGlobs[0] : '**/*.md'; } function qmdOperationCommand(operation, root, config, options: any = {}) { if (operation === 'setup') { const targetPath = options.path || '.'; const command = ['collection', 'add', targetPath, '--name', collectionNameFor(root, options)]; const mask = options.mask || firstMask(config); if (mask) command.push('--mask', mask); return command; } if (operation === 'update') return ['update']; if (operation === 'embed') return options.force ? ['embed', '-f'] : ['embed']; throw new Error(`Unknown qmd operation: ${operation}`); } function qmdOperationPlan(operation, options: any = {}, runtime: any = {}) { if (!OPERATIONS.has(operation)) throw new Error(`Unknown qmd operation: ${operation}. Use one of: setup, update, embed`); const root = runtime.root || process.cwd(); initProject(root); const config = runtime.config || loadConfig(root); const qmd = runtime.qmd || detectQmd(config, root); const qmdArgs = qmdOperationCommand(operation, root, config, options); const command = qmd.available ? [...qmd.command, ...qmdArgs] : ['qmd', ...qmdArgs]; const dryRunCommand = `qmd-adaptive-search qmd ${operation} --dry-run`; const confirmCommand = `qmd-adaptive-search qmd ${operation} --yes`; const details = { setup: { target: toPosix(path.resolve(root, options.path || '.')), sideEffects: [ 'qmd collection config is created or updated for this project path', 'qmd may scan files matching the configured mask', 'no embeddings are generated by setup alone' ], estimatedTime: 'usually seconds to a few minutes, depending on file count' }, update: { target: 'all qmd collections unless qmd config narrows the scope', sideEffects: [ 'qmd re-indexes changed files', 'qmd local index metadata may be rewritten', 'new or changed documents may become pending for embedding' ], estimatedTime: 'minutes on medium projects; longer on large document sets' }, embed: { target: options.force ? 'all qmd chunks (force re-embed)' : 'qmd chunks missing embeddings', sideEffects: [ 'qmd downloads or loads embedding model if needed', 'qmd writes vector embeddings to its local index', 'CPU/GPU and disk activity can be high during embedding' ], estimatedTime: 'minutes to much longer, depending on corpus size and model' } }[operation]; return { operation, qmdAvailable: qmd.available, qmdCommand: qmd.command, command, target: details.target, sideEffects: details.sideEffects, estimatedTime: details.estimatedTime, dryRunCommand, confirmCommand, nextCommandOnFailure: 'qmd-adaptive-search status', warnings: qmd.available ? [] : [installInstructions()] }; } function failureHint(operation, errorText) { return [ `qmd ${operation} failed.`, errorText ? `Reason: ${errorText}` : 'Reason: qmd exited with a non-zero status.', `Next: run \`qmd-adaptive-search qmd ${operation} --dry-run\` to review the plan, then retry with \`--yes\` after fixing qmd output.`, 'If qmd is missing, run `qmd-adaptive-search install-qmd` first.' ].join('\n'); } function runQmdOperation(operation, options: any = {}, runtime: any = {}) { const root = runtime.root || process.cwd(); const plan = qmdOperationPlan(operation, options, runtime); if (options.dryRun || options.planOnly) return { ok: true, dryRun: true, plan }; if (!options.yes) return { ok: false, confirmationRequired: true, plan, nextCommand: plan.confirmCommand }; if (!plan.qmdAvailable) return { ok: false, plan, error: 'qmd not found', humanMessage: installInstructions(), nextCommand: 'qmd-adaptive-search install-qmd' }; const startedJob = startBackgroundJob(root, { type: `qmd-${operation}`, input: { operation, command: plan.command }, qmd: { available: plan.qmdAvailable, command: plan.qmdCommand, method: operation } }); const [bin, ...args] = plan.command; const result = runCommand([bin], args, { cwd: root, timeoutMs: options.timeoutMs || 30 * 60 * 1000 }); const finishedAt = new Date().toISOString(); const errorText = (result.stderr || result.error || '').toString().trim(); if (result.status !== 0) { finishBackgroundJob(root, startedJob, { operation, status: 'failed', finishedAt, qmd: { available: plan.qmdAvailable, command: plan.qmdCommand, method: operation }, result: { ok: false, status: result.status }, error: errorText || `qmd ${operation} failed`, recoveryHint: failureHint(operation, errorText) }); return { ok: false, plan, status: result.status, stdout: result.stdout || '', stderr: result.stderr || '', error: errorText || `qmd ${operation} failed`, humanMessage: failureHint(operation, errorText), nextCommand: plan.dryRunCommand }; } finishBackgroundJob(root, startedJob, { operation, status: 'completed', finishedAt, qmd: { available: plan.qmdAvailable, command: plan.qmdCommand, method: operation }, result: { ok: true, status: result.status } }); return { ok: true, plan, status: result.status, stdout: result.stdout || '', stderr: result.stderr || '' }; } function nextQmdOperation(root, config, qmd, jobState) { if (!qmd.available) return { operation: 'install-qmd', reason: 'qmd is not available', command: 'qmd-adaptive-search install-qmd' }; if (!jobState.lastSetupJob) return { operation: 'setup', reason: 'no confirmed qmd collection setup has been recorded', command: 'qmd-adaptive-search qmd setup --dry-run' }; if (!jobState.lastUpdateJob) return { operation: 'update', reason: 'no confirmed qmd index update has been recorded', command: 'qmd-adaptive-search qmd update --dry-run' }; if (!jobState.lastEmbedJob) return { operation: 'embed', reason: 'no confirmed qmd embedding run has been recorded', command: 'qmd-adaptive-search qmd embed --dry-run' }; return null; } export { qmdOperationPlan, runQmdOperation, nextQmdOperation };