UNPKG

rigjs

Version:

A multi-repos dev tool based on yarn and git.Rigjs is intended to be the simplest way to develop,share and deliver codes between different developers or different projects.

180 lines (159 loc) 6.67 kB
import fs from 'fs'; import path from 'path'; import yaml from 'js-yaml'; import { paths, vaultConfigPath } from './paths'; /** * Two-layer config model. * * ~/.rig/config.yml — rig-global preferences (agent / qmd / logRotate). * <vault>/.rig/config.yml — per-vault settings (name, include, exclude, * schedule, ingestRules, optional scan root). * * There is **no global registry**. A vault is discovered by walking up from * the current working directory looking for `.rig/config.yml`. Everything * about a vault — its identity, its scope, its schedule — lives inside the * vault dir itself. */ export interface RigConfig { wiki?: { defaultAgent?: 'claude' | 'codex' | 'pi'; qmd?: { enabled?: 'auto' | 'on' | 'off' }; logRotateMB?: number; }; } /** Per-vault config persisted at `<vault>/.rig/config.yml`. */ export interface VaultConfig { name: string; /** Scan root relative to the vault. Default: `..` (vault's parent dir). */ root?: string; include?: string[]; exclude?: string[]; schedule?: { scan?: string; lint?: string; ingest?: string | null }; ingestRules?: { match: string; mode: 'auto-on-new' | 'propose-only' }[]; } /** Composed view used by all wiki commands. */ export interface WikiEntry { name: string; /** Absolute path to the vault dir. */ path: string; /** Absolute path to the scan root. */ root: string; include: string[]; exclude: string[]; schedule?: { scan?: string; lint?: string; ingest?: string | null }; ingestRules?: { match: string; mode: 'auto-on-new' | 'propose-only' }[]; } const DEFAULT_RIG_CONFIG: RigConfig = { wiki: { defaultAgent: 'claude', qmd: { enabled: 'auto' }, logRotateMB: 50 }, }; const DEFAULT_SCHEDULE = { scan: '0 */6 * * *', lint: '0 3 * * *', ingest: null }; function ensureHomeDir(): void { fs.mkdirSync(paths.home, { recursive: true }); fs.mkdirSync(paths.locks, { recursive: true }); fs.mkdirSync(paths.logs, { recursive: true }); fs.mkdirSync(paths.cache, { recursive: true }); } function readYaml<T>(file: string, fallback: T): T { if (!fs.existsSync(file)) return fallback; try { const parsed = yaml.load(fs.readFileSync(file, 'utf8')); return (parsed ?? fallback) as T; } catch (e: any) { throw new Error(`failed to parse ${file}: ${e.message}`); } } function writeYaml(file: string, data: unknown): void { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, yaml.dump(data, { lineWidth: 100, noRefs: true }), 'utf8'); } // ─────────────────────────── rig-global config ─────────────────────────── export function loadRigConfig(): RigConfig { ensureHomeDir(); return { ...DEFAULT_RIG_CONFIG, ...readYaml<RigConfig>(paths.config, DEFAULT_RIG_CONFIG) }; } export function saveRigConfig(cfg: RigConfig): void { ensureHomeDir(); writeYaml(paths.config, cfg); } // ─────────────────────────── per-vault config ──────────────────────────── export function loadVaultConfig(vaultDir: string): VaultConfig | null { const file = vaultConfigPath(vaultDir); if (!fs.existsSync(file)) return null; return readYaml<VaultConfig | null>(file, null); } export function saveVaultConfig(vaultDir: string, cfg: VaultConfig): void { writeYaml(vaultConfigPath(vaultDir), cfg); } // ─────────────────────────── vault discovery ───────────────────────────── function composeEntry(vaultPath: string, vault: VaultConfig): WikiEntry { const rootRel = vault.root ?? '..'; const root = path.resolve(vaultPath, rootRel); return { name: vault.name || path.basename(vaultPath), path: vaultPath, root, // Default: walk everything visible. Binary extensions, hidden dirs, // and .gitignored content are filtered at walk time by scan.ts / // surveyed by `rig wiki survey`. include/exclude in vault config // remain available as escape hatches for power users. include: vault.include ?? ['**/*'], exclude: vault.exclude ?? [], schedule: vault.schedule ?? DEFAULT_SCHEDULE, ingestRules: vault.ingestRules ?? [{ match: 'raw/**/*.md', mode: 'auto-on-new' }], }; } /** * Walk up from `start` (default: CWD) looking for a vault. At each ancestor * we check two patterns: * * 1. **The dir itself is the vault** — `<dir>/.rig/config.yml` exists. * Triggered when the user is inside the vault or below it. * 2. **An immediate child is the vault** — some `<dir>/<child>/.rig/config.yml` * exists. Triggered when the user is at the project root (the common * case for `cd <project> && rig wiki *` where the vault is * `<project>/rig-wiki/`). If multiple children are vaults, the * lexicographically first wins. * * Returns the composed `WikiEntry`, or `undefined` if no vault is found. * * Callers that need a vault and don't have one must produce a helpful error * themselves — `resolveVault` is a pure lookup and stays quiet. */ export function resolveVault(start?: string): WikiEntry | undefined { let dir = path.resolve(start || process.cwd()); while (true) { // 1. Is `dir` itself a vault? if (fs.existsSync(vaultConfigPath(dir))) { const v = loadVaultConfig(dir); if (v) return composeEntry(dir, v); } // 2. Does any immediate child of `dir` look like a vault? try { const entries = fs.readdirSync(dir, { withFileTypes: true }) .filter(e => e.isDirectory() && !e.name.startsWith('.')) .map(e => e.name) .sort(); for (const name of entries) { const child = path.join(dir, name); if (fs.existsSync(vaultConfigPath(child))) { const v = loadVaultConfig(child); if (v) return composeEntry(child, v); } } } catch { /* unreadable dir — fall through to parent */ } const parent = path.dirname(dir); if (parent === dir) return undefined; dir = parent; } } /** * Same as `resolveVault` but exits the process with a clear error when no * vault is found. Use from CLI commands. */ export function requireVault(): WikiEntry { const v = resolveVault(); if (v) return v; // eslint-disable-next-line no-console console.error('No rig wiki vault found. cd into a vault directory (one that contains .rig/config.yml) or run `rig wiki init <path>` first.'); process.exit(1); }