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
text/typescript
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);
}