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.

178 lines (160 loc) 5.97 kB
import fs from 'fs'; import path from 'path'; import os from 'os'; import print from '../print'; import { paths } from './paths'; const BUNDLED_SKILLS = [ { name: 'rig-wiki', file: 'RIG_WIKI_SKILL.md' }, { name: 'rig-crew', file: 'RIG_CREW_SKILL.md' }, ]; /** Find rig's package root by walking up from built/ or lib/. */ function findRigRoot(): string | undefined { let dir = __dirname; for (let i = 0; i < 10; i++) { const pkg = path.join(dir, 'package.json'); if (fs.existsSync(pkg)) { try { const p = JSON.parse(fs.readFileSync(pkg, 'utf8')); if (p.name === 'rigjs') return dir; } catch { /* keep walking */ } } const parent = path.dirname(dir); if (parent === dir) break; dir = parent; } return undefined; } interface InstallOpts { force?: boolean; project?: boolean; } /** * Install bundled skills (rig-wiki, rig-crew). * * Default (global): symlink into `~/.claude/skills/<name>/SKILL.md`. Affects * every project that uses Claude Code on this machine. Symlink because * `~/.claude/skills/` is not committed; the link should auto-track * whichever rigjs version is installed. * * `--project`: **copy actual file contents** into BOTH * `<cwd>/.claude/skills/<name>/SKILL.md` (Claude Code) AND * `<cwd>/.agents/skills/<name>/SKILL.md` (Codex). Copy because: * 1. these paths get committed to the project's repo and need to work * after a fresh clone on any machine, regardless of where rigjs lives * on that machine's disk; * 2. a symlink targeting `/usr/local/lib/node_modules/rigjs/...` would * be machine-specific and meaningless on CI / colleague's laptop / * a different install prefix. * To refresh project-local copies after a rigjs upgrade, re-run * `rig wiki install-skill --project --force`. */ export default function wikiInstallSkill(opts: InstallOpts): void { const root = findRigRoot(); if (!root) { print.error('could not locate the rigjs install root. Reinstall rigjs?'); process.exit(1); } const targetRoots = resolveTargetRoots(opts); for (const tr of targetRoots) { if (!fs.existsSync(tr.parent)) { // Project-level install creates the dirs on demand (parent is <cwd>). // Global install requires the dir to already exist — that's how we // detect "Claude Code is installed on this machine". if (tr.kind === 'global') { print.error(`${tr.label} dir not found: ${tr.parent}`); print.info(`install ${tr.label} first, then re-run \`rig wiki install-skill\`.`); process.exit(1); } } for (const skill of BUNDLED_SKILLS) { const src = path.join(root, skill.file); if (!fs.existsSync(src)) { print.warn(`skipping ${skill.name}: ${skill.file} not found inside rigjs install`); continue; } linkSkill(skill.name, src, tr.parent, opts); } } if (opts.project) { print.info('project-local skills will override global ones when this project is the cwd.'); } else { print.info('restart Claude Code to pick up new or updated skills.'); } } interface TargetRoot { /** `<parent>/<skill-name>/SKILL.md` is the resulting symlink. */ parent: string; kind: 'global' | 'project'; label: string; } function resolveTargetRoots(opts: InstallOpts): TargetRoot[] { if (opts.project) { const cwd = process.cwd(); return [ { parent: path.join(cwd, '.claude', 'skills'), kind: 'project', label: 'Claude Code project skills' }, { parent: path.join(cwd, '.agents', 'skills'), kind: 'project', label: 'Codex project skills' }, ]; } return [ { parent: paths.claudeSkillsDir, kind: 'global', label: 'Claude Code skills' }, ]; } function linkSkill(name: string, src: string, parentDir: string, opts: InstallOpts): void { const targetDir = path.join(parentDir, name); const target = path.join(targetDir, 'SKILL.md'); fs.mkdirSync(targetDir, { recursive: true }); const useCopy = !!opts.project; if (fs.existsSync(target) || isBrokenSymlink(target)) { const existingLink = safeReadlink(target); if (useCopy) { // Copy mode: same-content existing copy is a no-op; otherwise overwrite // when --force, error when not. if (!existingLink) { try { if (fs.readFileSync(target, 'utf8') === fs.readFileSync(src, 'utf8')) { print.info(`already up to date: ${shortPath(target)}`); return; } } catch { /* fall through to the force-check below */ } } if (!opts.force) { const what = existingLink ? `symlink -> ${existingLink}` : 'a different file'; print.error(`${shortPath(target)} exists as ${what}. Pass --force to replace.`); process.exit(1); } fs.rmSync(target, { force: true }); } else { // Symlink mode (global): same-target existing symlink is a no-op. if (existingLink === src) { print.info(`already linked: ${shortPath(target)}`); return; } if (!opts.force) { const what = existingLink ? `symlink -> ${existingLink}` : 'a regular file'; print.error(`${shortPath(target)} exists as ${what}. Pass --force to replace.`); process.exit(1); } fs.rmSync(target, { force: true }); } } if (useCopy) { fs.copyFileSync(src, target); print.succeed(`wrote ${shortPath(target)}`); } else { fs.symlinkSync(src, target); print.succeed(`linked ${shortPath(target)} -> ${shortPath(src)}`); } } function safeReadlink(p: string): string | null { try { return fs.readlinkSync(p); } catch { return null; } } function isBrokenSymlink(p: string): boolean { try { fs.statSync(p); return false; } catch { try { return Boolean(fs.readlinkSync(p)); } catch { return false; } } } function shortPath(p: string): string { const home = os.homedir(); if (p.startsWith(home + path.sep)) return '~' + p.slice(home.length); return p; }