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.

171 lines (155 loc) 6.52 kB
import fs from 'fs'; import path from 'path'; import print from '../print'; import { loadCrewConfig, saveCrewConfig, requireCrew, normalizeCrew, CrewProject, shortPath } from './config'; import { ensureProject, rootPath } from './vault'; import { scanTasks, summarize } from './task'; import { CrewExecutor } from './config'; interface ProjectOpts { crew?: string; path?: string; owner?: string; executor?: string; testCommand?: string; noWrite?: boolean; from?: string; keepMissing?: boolean; archiveMissing?: boolean; } export function projectAdd(name: string, opts: ProjectOpts): void { const cfg = loadCrewConfig(); const crew = requireCrew(opts.crew); if (!opts.path) { print.error('missing --path <path>'); process.exit(1); } const project: CrewProject = { name, path: path.resolve(opts.path), owner: opts.owner || `maintainer:${name}`, defaultExecutor: parseExecutor(opts.executor || crew.defaultExecutor || 'claude'), canWriteCode: !opts.noWrite, defaultTestCommand: opts.testCommand, }; const crewIndex = cfg.crews.findIndex(c => c.name === crew.name); const entry = normalizeCrew(cfg.crews[crewIndex]); const projects = entry.projects || []; const i = projects.findIndex(p => p.name === name); if (i >= 0) projects[i] = project; else projects.push(project); entry.projects = projects; cfg.crews[crewIndex] = entry; saveCrewConfig(cfg); ensureProject(entry, project); print.succeed(`project "${name}" registered: ${shortPath(project.path)}`); } export function projectList(opts: ProjectOpts): void { const crew = requireCrew(opts.crew); const projects = crew.projects || []; if (projects.length === 0) { print.info('no projects registered. Use `rig crew project add <name> --path <path>`.'); return; } // eslint-disable-next-line no-console console.log('NAME OWNER EXECUTOR CAN WRITE TEST COMMAND PATH'); // eslint-disable-next-line no-console console.log('---- ----- -------- --------- ------------ ----'); for (const p of projects) { // eslint-disable-next-line no-console console.log(`${p.name} ${p.owner} ${p.defaultExecutor || crew.defaultExecutor || 'claude'} ${p.canWriteCode === false ? 'no' : 'yes'} ${p.defaultTestCommand || '-'} ${shortPath(p.path)}`); } } export function projectStatus(name: string, opts: ProjectOpts): void { const crew = requireCrew(opts.crew); const project = (crew.projects || []).find(p => p.name === name); if (!project) { print.error(`unknown project: ${name}`); process.exit(1); } const tasks = scanTasks(crew).filter(t => t.scope !== 'pending' && (t.scope === `project:${name}` || t.scope.startsWith(`project:${name}:`) || t.fields.project === name)); const s = summarize(tasks); print.info(`project: ${name} (${project.owner})`); // eslint-disable-next-line no-console console.log(`path: ${shortPath(project.path)}`); // eslint-disable-next-line no-console console.log(`executor: ${project.defaultExecutor || crew.defaultExecutor || 'claude'}`); // eslint-disable-next-line no-console console.log(`tasks: ${s.done}/${s.total} done, ${s.open} open, ${s.blocked} blocked`); } export function projectSync(opts: ProjectOpts): void { const cfg = loadCrewConfig(); const crew = requireCrew(opts.crew); const crewIndex = cfg.crews.findIndex(c => c.name === crew.name); const entry = normalizeCrew(cfg.crews[crewIndex]); const scanRoot = path.resolve(entry.vault, opts.from || 'projects'); if (!fs.existsSync(scanRoot)) { print.error(`projects directory not found: ${shortPath(scanRoot)}`); process.exit(1); } const found = fs.readdirSync(scanRoot, { withFileTypes: true }) .filter(d => d.isDirectory() && !d.name.startsWith('.') && !d.name.startsWith('_')) .map(d => ({ name: d.name, path: path.join(scanRoot, d.name) })) .sort((a, b) => a.name.localeCompare(b.name)); const existing = new Map((entry.projects || []).map(p => [p.name, p])); const seen = new Set(found.map(p => p.name)); const next: CrewProject[] = []; const added: string[] = []; const updated: string[] = []; const removed: CrewProject[] = []; for (const item of found) { const old = existing.get(item.name); if (old) { const project = { ...old, path: item.path }; next.push(project); updated.push(item.name); } else { const project: CrewProject = { name: item.name, path: item.path, owner: `maintainer:${item.name}`, defaultExecutor: parseExecutor(opts.executor || entry.defaultExecutor || 'claude'), canWriteCode: !opts.noWrite, defaultTestCommand: opts.testCommand, }; next.push(project); added.push(item.name); } } for (const project of entry.projects || []) { if (seen.has(project.name)) continue; if (!isInside(project.path, scanRoot)) { next.push(project); continue; } if (opts.keepMissing) next.push(project); else removed.push(project); } entry.projects = next; cfg.crews[crewIndex] = entry; saveCrewConfig(cfg); for (const project of next) ensureProject(entry, project); if (!opts.keepMissing && opts.archiveMissing !== false) { for (const project of removed) archiveProjectFolder(entry, project.name); } print.succeed(`project registry synced from ${shortPath(scanRoot)}`); print.info(`added: ${added.length ? added.join(', ') : '-'}`); print.info(`updated: ${updated.length ? updated.join(', ') : '-'}`); print.info(`removed: ${removed.length ? removed.map(p => p.name).join(', ') : '-'}`); } function parseExecutor(value: string): CrewExecutor { if (value === 'claude' || value === 'codex' || value === 'pi') return value; print.error(`unknown executor: ${value}. Expected claude, codex, or pi.`); process.exit(1); } function isInside(child: string, parent: string): boolean { const rel = path.relative(path.resolve(parent), path.resolve(child)); return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel)); } function archiveProjectFolder(crew: ReturnType<typeof normalizeCrew>, name: string): void { const from = rootPath(crew, path.join('Projects', name)); if (!fs.existsSync(from)) return; const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '').replace('T', '-'); const to = rootPath(crew, path.join('Projects', '_Archived', `${name}-${stamp}`)); fs.mkdirSync(path.dirname(to), { recursive: true }); fs.renameSync(from, to); }