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