@nakedved/ano
Version:
Claude Code plugin for collaborative annotation and review
193 lines (192 loc) • 6.04 kB
JavaScript
/**
* Team Configuration Management
*
* Handles reading/writing team config from .ano/config.json
* Advisory enforcement - shows warnings but doesn't block.
*/
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
// Default config file location
const CONFIG_DIR = '.ano';
const CONFIG_FILE = 'config.json';
/**
* Get the path to the team config file
*/
export function getConfigPath(projectRoot = process.cwd()) {
return join(projectRoot, CONFIG_DIR, CONFIG_FILE);
}
/**
* Get the .ano directory path
*/
export function getConfigDir(projectRoot = process.cwd()) {
return join(projectRoot, CONFIG_DIR);
}
/**
* Default team configuration
*/
export function createDefaultConfig(projectName) {
return {
version: '1.0',
projectName,
members: [],
roles: {
lead: { canOverride: true, weight: 2 },
reviewer: { canOverride: false, weight: 1 },
},
requirements: {
minApprovals: 1,
},
};
}
/**
* Read team config from disk
* Returns null if no config exists
*/
export async function readTeamConfig(projectRoot = process.cwd()) {
const configPath = getConfigPath(projectRoot);
if (!existsSync(configPath)) {
return null;
}
try {
const content = await readFile(configPath, 'utf-8');
return JSON.parse(content);
}
catch {
return null;
}
}
/**
* Write team config to disk
*/
export async function writeTeamConfig(config, projectRoot = process.cwd()) {
const configDir = getConfigDir(projectRoot);
const configPath = getConfigPath(projectRoot);
// Ensure .ano directory exists
if (!existsSync(configDir)) {
await mkdir(configDir, { recursive: true });
}
await writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
}
/**
* Initialize team config (create if doesn't exist)
* Returns true if created, false if already exists
*/
export async function initTeamConfig(projectName, projectRoot = process.cwd()) {
const existing = await readTeamConfig(projectRoot);
if (existing) {
return false; // Already exists
}
const config = createDefaultConfig(projectName);
await writeTeamConfig(config, projectRoot);
return true;
}
/**
* Add a member to the team
*/
export async function addTeamMember(member, projectRoot = process.cwd()) {
let config = await readTeamConfig(projectRoot);
if (!config) {
// Auto-init if no config
config = createDefaultConfig();
}
// Check if member already exists (by email)
const existing = config.members.find((m) => m.email.toLowerCase() === member.email.toLowerCase());
if (existing) {
return { added: false, reason: 'Member with this email already exists' };
}
// Validate role exists
if (!config.roles[member.role]) {
return {
added: false,
reason: `Unknown role: ${member.role}. Available: ${Object.keys(config.roles).join(', ')}`,
};
}
config.members.push(member);
await writeTeamConfig(config, projectRoot);
return { added: true };
}
/**
* Remove a member from the team (by email)
*/
export async function removeTeamMember(email, projectRoot = process.cwd()) {
const config = await readTeamConfig(projectRoot);
if (!config) {
return { removed: false, reason: 'No team config found. Run: ano team init' };
}
const index = config.members.findIndex((m) => m.email.toLowerCase() === email.toLowerCase());
if (index === -1) {
return { removed: false, reason: 'Member not found' };
}
config.members.splice(index, 1);
await writeTeamConfig(config, projectRoot);
return { removed: true };
}
/**
* Find a team member by email
*/
export async function findTeamMember(email, projectRoot = process.cwd()) {
const config = await readTeamConfig(projectRoot);
if (!config) {
return null;
}
return (config.members.find((m) => m.email.toLowerCase() === email.toLowerCase()) || null);
}
/**
* Check if an email belongs to a team member
* Returns member info and their role permissions
*/
export async function checkTeamMembership(email, projectRoot = process.cwd()) {
const config = await readTeamConfig(projectRoot);
if (!config) {
return { isMember: false };
}
const member = config.members.find((m) => m.email.toLowerCase() === email.toLowerCase());
if (!member) {
return { isMember: false };
}
const role = config.roles[member.role];
return {
isMember: true,
member,
role,
};
}
/**
* Add a new role to the config
*/
export async function addTeamRole(roleName, role, projectRoot = process.cwd()) {
const config = await readTeamConfig(projectRoot);
if (!config) {
return { added: false, reason: 'No team config found. Run: ano team init' };
}
if (config.roles[roleName]) {
return { added: false, reason: 'Role already exists' };
}
config.roles[roleName] = role;
await writeTeamConfig(config, projectRoot);
return { added: true };
}
/**
* Update approval requirements
*/
export async function setRequirements(requirements, projectRoot = process.cwd()) {
const config = await readTeamConfig(projectRoot);
if (!config) {
throw new Error('No team config found. Run: ano team init');
}
config.requirements = { ...config.requirements, ...requirements };
await writeTeamConfig(config, projectRoot);
}
/**
* Get formatted team member status for an approver
* Used in check/approve commands for advisory display
*/
export async function getApproverStatus(email, projectRoot = process.cwd()) {
const { isMember, member, role } = await checkTeamMembership(email, projectRoot);
if (!isMember) {
return '(not in team)';
}
const roleInfo = role?.canOverride ? `${member.role}, can override` : member.role;
return `(${roleInfo})`;
}