@nakedved/ano
Version:
Claude Code plugin for collaborative annotation and review
126 lines (125 loc) • 4.45 kB
JavaScript
/**
* ano annotate command
*
* Add an annotation to a specific line in a file.
*
* Usage:
* ano annotate <file>:<line> "comment" [options]
*
* Examples:
* ano annotate plan.md:15 "Is this safe?"
* ano annotate plan.md:15 "Rate limit risk" --type concern
* ano annotate src/index.ts:42-50 "Refactor this" --type suggestion
*/
import { Command } from 'commander';
import chalk from 'chalk';
import { resolve } from 'node:path';
import { access, readFile } from 'node:fs/promises';
import { addAnnotation } from '../../core/annotations.js';
import { getAuthorString } from '../../core/config.js';
/**
* Parse a file:line reference.
* Supports: "file.md:15" or "file.md:15-20" (range)
*/
function parseFileLineRef(ref) {
// Match: file.md:15 or file.md:15-20
const match = ref.match(/^(.+):(\d+)(?:-(\d+))?$/);
if (!match)
return null;
const [, file, lineStr, endLineStr] = match;
return {
file: resolve(file),
line: parseInt(lineStr, 10),
endLine: endLineStr ? parseInt(endLineStr, 10) : undefined,
};
}
/**
* Validate that an annotation type is valid.
*/
function isValidType(type) {
return ['concern', 'question', 'suggestion', 'blocker'].includes(type);
}
// Create the command
export const annotateCommand = new Command('annotate')
.description('Add an annotation to a file')
.argument('<file:line>', 'File and line number (e.g., plan.md:15 or plan.md:15-20)')
.argument('<comment>', 'The annotation comment')
.option('-t, --type <type>', 'Annotation type: concern, question, suggestion, blocker', 'suggestion')
.action(async (fileLineRef, comment, options) => {
try {
// Parse file:line reference
const parsed = parseFileLineRef(fileLineRef);
if (!parsed) {
console.error(chalk.red('Error: Invalid format. Use file:line (e.g., plan.md:15)'));
process.exit(1);
}
const { file, line, endLine } = parsed;
// Check file exists
try {
await access(file);
}
catch {
console.error(chalk.red(`Error: File not found: ${file}`));
process.exit(1);
}
// Validate line number exists in file
const content = await readFile(file, 'utf-8');
const lineCount = content.split('\n').length;
if (line < 1 || line > lineCount) {
console.error(chalk.red(`Error: Line ${line} is out of range. File has ${lineCount} lines.`));
process.exit(1);
}
if (endLine && (endLine < line || endLine > lineCount)) {
console.error(chalk.red(`Error: End line ${endLine} is invalid. Must be >= ${line} and <= ${lineCount}.`));
process.exit(1);
}
// Validate type
if (!isValidType(options.type)) {
console.error(chalk.red(`Error: Invalid type "${options.type}". Use: concern, question, suggestion, blocker`));
process.exit(1);
}
// Get author from git config
const author = await getAuthorString();
// Create the annotation
const annotationOptions = {
file,
line,
endLine,
type: options.type,
author,
content: comment,
};
const annotation = await addAnnotation(annotationOptions);
// Success output
console.log(chalk.green('✓ Annotation added'));
console.log();
console.log(` ${chalk.bold('ID:')} ${chalk.dim(annotation.id.slice(0, 8))}...`);
console.log(` ${chalk.bold('File:')} ${file}`);
console.log(` ${chalk.bold('Line:')} ${line}${endLine ? `-${endLine}` : ''}`);
console.log(` ${chalk.bold('Type:')} ${formatType(options.type)}`);
console.log(` ${chalk.bold('Author:')} ${author}`);
console.log();
console.log(` "${comment}"`);
}
catch (error) {
console.error(chalk.red('Error:'), error.message);
process.exit(1);
}
});
/**
* Format annotation type with color.
*/
function formatType(type) {
switch (type) {
case 'concern':
return chalk.yellow('concern');
case 'question':
return chalk.blue('question');
case 'suggestion':
return chalk.green('suggestion');
case 'blocker':
return chalk.red('blocker');
default:
return type;
}
}