From b4e60a08e5f188d3ef5b04f408e15fbdcfdffb14 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Sat, 13 Dec 2025 11:16:24 -0500 Subject: [PATCH] Add optional update: command support to collections YAML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collections can now specify an optional 'update:' key with a bash command that will be executed during 'qmd update' before indexing files. Features: - Runs command in collection directory (cwd) - Uses /usr/bin/env bash -c "..." - Always prints stdout/stderr output - Stops with exit code if command fails (!= 0) - Legacy --pull flag still works for git repos without update command Example YAML: ```yaml collections: archive: path: /path/to/repo pattern: "**/*.md" update: git pull ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/collections.ts | 14 ++++++++++++-- src/qmd.ts | 46 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/collections.ts b/src/collections.ts index 6305688..bcf1900 100644 --- a/src/collections.ts +++ b/src/collections.ts @@ -28,6 +28,7 @@ export interface Collection { path: string; // Absolute path to index pattern: string; // Glob pattern (e.g., "**/*.md") context?: ContextMap; // Optional context definitions + update?: string; // Optional bash command to run during qmd update } /** @@ -49,8 +50,17 @@ export interface NamedCollection extends Collection { // Configuration paths // ============================================================================ -const CONFIG_DIR = join(homedir(), ".config", "qmd"); -const CONFIG_PATH = join(CONFIG_DIR, "index.yml"); +function getConfigDir(): string { + // Allow override via QMD_CONFIG_DIR for testing + if (process.env.QMD_CONFIG_DIR) { + return process.env.QMD_CONFIG_DIR; + } + return join(homedir(), ".config", "qmd"); +} + +function getConfigFilePath(): string { + return join(getConfigDir(), "index.yml"); +} /** * Ensure config directory exists diff --git a/src/qmd.ts b/src/qmd.ts index 21f987c..204a6aa 100755 --- a/src/qmd.ts +++ b/src/qmd.ts @@ -83,6 +83,7 @@ import { escapeCSV, type OutputFormat, } from "./formatter.js"; +import { getCollection as getCollectionFromYaml } from "./collections.js"; // Chunking: ~2000 tokens per chunk, ~3 bytes/token = 6KB const CHUNK_BYTE_SIZE = 6 * 1024; @@ -528,10 +529,10 @@ async function updateCollections(pullFirst: boolean = false): Promise { // Clear Ollama cache on update clearCache(db); - const collections = db.prepare(`SELECT id, pwd, glob_pattern FROM collections`).all() as { id: number; pwd: string; glob_pattern: string }[]; + const collections = listCollections(db); if (collections.length === 0) { - console.log(`${c.dim}No collections found. Run 'qmd add .' to index markdown files.${c.reset}`); + console.log(`${c.dim}No collections found. Run 'qmd collection add .' to index markdown files.${c.reset}`); closeDb(); return; } @@ -541,10 +542,43 @@ async function updateCollections(pullFirst: boolean = false): Promise { for (let i = 0; i < collections.length; i++) { const col = collections[i]; - console.log(`${c.cyan}[${i + 1}/${collections.length}]${c.reset} ${c.bold}${col.pwd}${c.reset}`); + console.log(`${c.cyan}[${i + 1}/${collections.length}]${c.reset} ${c.bold}${col.name}${c.reset}`); + console.log(`${c.dim} Path: ${col.pwd}${c.reset}`); console.log(`${c.dim} Pattern: ${col.glob_pattern}${c.reset}`); - // Check if this is a git repository + // Execute custom update command if specified in YAML + const yamlCol = getCollectionFromYaml(col.name); + if (yamlCol?.update) { + console.log(`${c.dim} Running update command: ${yamlCol.update}${c.reset}`); + try { + const proc = Bun.spawn(["/usr/bin/env", "bash", "-c", yamlCol.update], { + cwd: col.pwd, + stdout: "pipe", + stderr: "pipe", + }); + + const output = await new Response(proc.stdout).text(); + const errorOutput = await new Response(proc.stderr).text(); + const exitCode = await proc.exited; + + if (output.trim()) { + console.log(output.trim().split('\n').map(l => ` ${l}`).join('\n')); + } + if (errorOutput.trim()) { + console.log(errorOutput.trim().split('\n').map(l => ` ${l}`).join('\n')); + } + + if (exitCode !== 0) { + console.log(`${c.yellow}✗ Update command failed with exit code ${exitCode}${c.reset}`); + process.exit(exitCode); + } + } catch (err) { + console.log(`${c.yellow}✗ Update command failed: ${err}${c.reset}`); + process.exit(1); + } + } + + // Check if this is a git repository (for legacy support) const gitDir = `${col.pwd}/.git`; let isGitRepo = false; @@ -559,8 +593,8 @@ async function updateCollections(pullFirst: boolean = false): Promise { if (isGitRepo) { console.log(`${c.dim} Git repository detected${c.reset}`); - // Execute git pull if requested - if (pullFirst) { + // Execute git pull if requested via --pull flag + if (pullFirst && !yamlCol?.update) { console.log(`${c.dim} Running git pull...${c.reset}`); try { const result = await $`cd ${col.pwd} && git pull`.quiet();