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.

247 lines (214 loc) 9.89 kB
import fs from 'fs'; import path from 'path'; import os from 'os'; import print from '../print'; import { guardPath, refusalMessage } from './pathGuard'; import { saveVaultConfig, loadVaultConfig, VaultConfig } from './config'; import { vaultConfigPath } from './paths'; /** * `rig wiki init <scope>` * * The user runs this from a project root. The CWD is treated as the project * (the conceptual "vault"); `<scope>` names a data subdir under it that the * wiki should ingest from (e.g. `personal` for `<project>/personal/`). * * Vault metadata always lives at `<CWD>/rig-wiki/` (fixed name). The scope * is recorded in `<CWD>/rig-wiki/.rig/config.yml` as the scan root, so the * user's data dir stays untouched. * * $ cd overmind * $ rig wiki init personal * ⇒ creates overmind/rig-wiki/ with templates + .rig/config.yml * ⇒ config.yml.root = "../personal", config.yml.name = "personal" * * Idempotent for the same scope. Errors if rig-wiki/ already exists with a * different scope (manual `.rig/config.yml` edit required to switch). */ const VAULT_DIRNAME = 'rig-wiki'; const PURPOSE_TMPL = `# Purpose This wiki is <author>'s <scope> (single, do not mix). Key questions it aims to answer: - ... - ... What's in scope: ... What's out of scope: ... Audience: <author> + agents. `; const SCHEMA_TMPL = `# Schema ## Layers - raw/, purpose.md, schema.md: read-only for LLM - index.md, overview.md, log.md, reviews.md, sources/, entities/, concepts/, synthesis/, queries/: LLM is sole author ## Page types - sources/<slug>.md : 1-source summary - entities/<slug>.md : 1 thing with properties - concepts/<slug>.md : 1 abstract idea - synthesis/<slug>.md : cross-source integration - queries/<slug>.md : archived Q&A worth keeping ## Frontmatter (every wiki page MUST have) - type: source | entity | concept | synthesis | query - sources: [<source-slug>, ...] - source-sha: <sha> # source pages only - source-path: raw/... | <relpath> # source pages only - ingested-at: <ISO> - last-updated: <ISO> ## Naming - kebab-case; no spaces; no dates in page filenames - raw/ filenames keep YYYY-MM-DD prefix ## Linking - use [[wikilink]] to other wiki pages by slug - every wiki page must link to >=1 other page or be flagged orphan ## Contradictions - flag inline: > Contradiction: A vs B (see [[source-A]], [[source-B]]) - never silently merge; lint surfaces them for human resolution ## Hard rules - never edit raw/, purpose.md, schema.md - raw/ file sha drift = error, not a re-ingest trigger - living-doc paths (in include[]) sha drift = MODIFIED, propose re-ingest ## Ingestion policy This section is consumed by \`rig wiki survey\` to decide which files under the scan root are eligible to ingest. Edit the "Custom rules" subsection to add wiki-specific filters; the default rules below cover the obvious cases. ### Default — INGESTIBLE - markdown / plain text: \`.md\` \`.markdown\` \`.txt\` \`.rst\` - documents: \`.pdf\` (Claude reads natively) - images of receipts / whiteboards / diagrams: \`.png\` \`.jpg\` \`.jpeg\` \`.webp\` \`.gif\` - structured text: \`.csv\` \`.tsv\` \`.json\` \`.yaml\` \`.yml\` \`.toml\` \`.html\` \`.xml\` ### Default — NOT INGESTIBLE - compressed archives: \`.zip\` \`.tar\` \`.tar.gz\` \`.tgz\` \`.gz\` \`.bz2\` \`.xz\` \`.7z\` \`.rar\` \`.dmg\` \`.iso\` - binaries / native: \`.exe\` \`.dll\` \`.so\` \`.dylib\` \`.bin\` \`.o\` \`.a\` \`.lib\` \`.class\` \`.jar\` \`.pyc\` \`.node\` \`.wasm\` - AV: \`.mp4\` \`.mov\` \`.mkv\` \`.avi\` \`.webm\` \`.mp3\` \`.wav\` \`.flac\` \`.aac\` \`.ogg\` - design / proprietary: \`.psd\` \`.ai\` \`.fig\` \`.sketch\` \`.fla\` \`.indd\` - lockfiles + build artifacts: \`yarn.lock\` \`package-lock.json\` \`pnpm-lock.yaml\` \`*.lock\` \`*.min.js\` \`*.map\` - model weights / embeddings: \`.gguf\` \`.safetensors\` \`.bin\` \`.pt\` \`.onnx\` \`.h5\` \`.pkl\` - anything in hidden dirs (segment starts with \`.\`) or .gitignored — refused by the path guard ### Custom rules (edit me) - (e.g.) skip files in \`personal/work/archive/\` - (e.g.) only English-language content - (e.g.) skip files larger than 5MB `; const SUBDIRS = ['sources', 'entities', 'concepts', 'synthesis', 'queries']; const GITIGNORE_TMPL = `# rig wiki — local-only artifacts (do not commit) # (vector cache lives outside the vault at ~/.rig/<project>/wiki/) # auto-generated reports lint-report-*.md # daemon proposal queue (per-machine) proposals/ # editor scratch .DS_Store *.swp `; /** * Defaults for a freshly-scoped vault. The user can edit * `<vault>/.rig/config.yml` afterwards. * * `include` defaults to `**` (everything) — rig wiki is multimodal: Claude * Read tool handles markdown / code / json natively, images and PDFs are * read as visual / document inputs. The user can tighten this per-vault. * * `exclude` defaults to common binary-archive extensions whose contents * can't be ingested without unpacking. Hidden directories (segments starting * with `.`) and `.gitignore`'d files are skipped automatically by the * scanner — no need to list them. */ function defaultVaultConfig(scope: string, rootRel: string): VaultConfig { return { name: scope, root: rootRel, include: ['**'], exclude: [ '*.zip', '**/*.zip', '*.tar', '**/*.tar', '*.tar.gz', '**/*.tar.gz', '*.tgz', '**/*.tgz', '*.7z', '**/*.7z', '*.rar', '**/*.rar', ], schedule: { scan: '0 */6 * * *', lint: '0 3 * * *', ingest: null }, ingestRules: [{ match: 'raw/**/*.*', mode: 'auto-on-new' }], }; } export default function wikiInit(scope?: string): void { const cwd = process.cwd(); const vaultDir = path.join(cwd, VAULT_DIRNAME); // No-arg form: scope = the whole project (CWD). Vault scans up one level // from `<CWD>/rig-wiki/` to reach CWD. Wiki name defaults to basename(CWD). // Hidden segments, binary extensions, and .gitignored files are filtered // by scan/survey at walk time — no scope restriction needed. const hasScope = !!(scope && scope.trim()); const scopeAbs = hasScope ? path.resolve(cwd, scope!) : cwd; const scopeName = hasScope ? scope!.trim() : path.basename(cwd); if (hasScope) { // The explicit scope must already exist — pointing the wiki at a missing // dir would hide what is almost certainly a typo. if (!fs.existsSync(scopeAbs) || !fs.statSync(scopeAbs).isDirectory()) { print.error(`scope dir not found: ${scope}`); print.info(`expected an existing data subdir at ${shortPath(scopeAbs)}`); process.exit(1); } // The scope can't be (or contain) the vault dir itself. if (scopeAbs === vaultDir || vaultDir.startsWith(scopeAbs + path.sep)) { print.error(`scope cannot be or contain the vault dir (${VAULT_DIRNAME}/).`); process.exit(1); } } const guard = guardPath(vaultDir, cwd); if (!guard.ok) { print.error(`refusing to initialize the vault at a hidden or gitignored path.`); // eslint-disable-next-line no-console console.error(refusalMessage(vaultDir, guard)); process.exit(1); } // If the vault already has a config, it must already be scoped to the // same data dir — otherwise the user is trying to re-target an existing // vault, which we won't do silently. Manual config edit only. // No-arg init resolves to the CWD itself, which equals `path.dirname(vaultDir)`. const cfgFile = vaultConfigPath(vaultDir); if (fs.existsSync(cfgFile)) { const existing = loadVaultConfig(vaultDir); const existingRootAbs = existing?.root ? path.resolve(vaultDir, existing.root) : path.dirname(vaultDir); if (existingRootAbs !== scopeAbs) { print.error(`vault already initialized at ${shortPath(vaultDir)} for scope "${existing?.name ?? '?'}" (root: ${existing?.root ?? '..'}).`); print.info(`to switch scopes, edit ${shortPath(cfgFile)} (name + root) by hand.`); process.exit(1); } } fs.mkdirSync(vaultDir, { recursive: true }); writeIfMissing(path.join(vaultDir, 'purpose.md'), PURPOSE_TMPL); writeIfMissing(path.join(vaultDir, 'schema.md'), SCHEMA_TMPL); writeIfMissing(path.join(vaultDir, 'index.md'), '# Index\n'); writeIfMissing(path.join(vaultDir, 'overview.md'), '# Overview\n'); writeIfMissing(path.join(vaultDir, 'log.md'), '# Log\n'); writeIfMissing(path.join(vaultDir, 'reviews.md'), '# Reviews\n'); writeIfMissing(path.join(vaultDir, '.gitignore'), GITIGNORE_TMPL); fs.mkdirSync(path.join(vaultDir, 'raw'), { recursive: true }); writeIfMissing(path.join(vaultDir, 'raw', '.gitkeep'), ''); for (const sub of SUBDIRS) { const d = path.join(vaultDir, sub); fs.mkdirSync(d, { recursive: true }); writeIfMissing(path.join(d, '.gitkeep'), ''); } if (!fs.existsSync(cfgFile)) { const rootRel = path.relative(vaultDir, scopeAbs); saveVaultConfig(vaultDir, defaultVaultConfig(scopeName, rootRel)); } const scopeLabel = hasScope ? `scope: ${scope}` : `scope: <project-wide, name "${scopeName}">`; print.succeed(`vault initialized at ${shortPath(vaultDir)} (${scopeLabel})`); print.info(`next: edit ${shortPath(path.join(vaultDir, 'purpose.md'))} to describe what this wiki is for.`); print.info(`then run \`rig wiki sync\` from anywhere inside ${shortPath(cwd)} to ingest, update, and prune in one shot.`); if (!fs.existsSync(path.join(scopeAbs, '.wikiignore'))) { print.info(`tip: drop a \`.wikiignore\` (gitignore syntax) at ${shortPath(scopeAbs)} for paths the wiki should skip even though git tracks them (e.g. \`keychain/\`, \`secrets/\`).`); } } function writeIfMissing(file: string, content: string) { if (fs.existsSync(file)) return; fs.writeFileSync(file, content, 'utf8'); } function shortPath(p: string): string { const home = os.homedir(); if (p.startsWith(home + path.sep)) return '~' + p.slice(home.length); return p; }