@batuhanw/api-documenter
Version:
Document your API endpoints with AI.
258 lines (257 loc) • 9.37 kB
JavaScript
import { Anthropic } from '@anthropic-ai/sdk';
import { Args, Command, Flags } from '@oclif/core';
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
import path from 'node:path';
import { pack } from 'repomix';
import { defaultPrompt } from './prompts/default.js';
import { input } from '@inquirer/prompts';
import { glob } from 'glob';
var RepoLanguage;
(function (RepoLanguage) {
RepoLanguage["NODEJS"] = "nodejs";
RepoLanguage["PYTHON"] = "python";
RepoLanguage["JAVA"] = "java";
RepoLanguage["RUBY"] = "ruby";
RepoLanguage["PHP"] = "php";
RepoLanguage["CSHARP"] = "csharp";
RepoLanguage["GO"] = "go";
})(RepoLanguage || (RepoLanguage = {}));
const repoLanguageToIncludedFilesMapping = {
[RepoLanguage.NODEJS]: [
'**/*.entity.ts',
'**/*.controller.ts',
'**/*.dto.ts',
'**/prisma/schema.prisma',
],
[RepoLanguage.PYTHON]: [
'**/models.py',
'**/serializers.py',
'**/schemas.py',
'**/views.py',
'**/routers.py',
'**/urls.py',
],
[RepoLanguage.JAVA]: [
'**/*.entity.java',
'**/*.controller.java',
'**/*.dto.java',
'**/model/**/*.java',
'**/entity/**/*.java',
'**/*Entity.java',
'**/dto/**/*.java',
'**/*DTO.java',
'**/controller/**/*.java',
'**/*Controller.java',
'**/resources/db/changelog/*.sql',
],
[RepoLanguage.RUBY]: [
'schemas/**/*.rb',
'**/db/schema.rb',
'**/db/structure.sql',
'**/models/**/*.rb',
'**/serializers/**/*.rb',
'**/controllers/**/*.rb',
'config/routes.rb',
],
[RepoLanguage.PHP]: [
'**/*.entity.php',
'**/*.controller.php',
'**/*.dto.php',
'**/src/**/*.php',
'app/Models/**/*.php',
'app/Http/Resources/**/*.php',
'app/Http/Controllers/**/*.php',
'routes/api.php',
'src/Entity/**/*.php',
'src/DTO/**/*.php',
'src/Controller/**/*.php',
],
[RepoLanguage.CSHARP]: [
'**/*.entity.cs',
'**/*.controller.cs',
'**/*.dto.cs',
'**/Models/**/*.cs',
'**/Controllers/**/*.cs',
'**/DTOs/**/*.cs',
'**/Entities/**/*.cs',
'**/Repositories/**/*.cs',
'**/Models/**/*.cs',
'**/Entities/**/*.cs',
'**/DTOs/**/*.cs',
'**/*DTO.cs',
'**/Controllers/**/*.cs',
],
[RepoLanguage.GO]: [
'**/*.entity.go',
'**/*.controller.go',
'**/*.dto.go',
'**/model/**/*.go',
'**/models/**/*.go',
'**/dto/**/*.go',
'**/transport/**/*.go',
'**/handler/**/*.go',
'**/handlers/**/*.go',
'**/api/**/*.go',
'**/migrations/*.sql',
'**/db/migrations/*.sql',
],
};
export default class Doc extends Command {
static args = {
path: Args.string({
required: false,
default: process.cwd(),
description: 'path to repository root',
}),
};
static description = 'Run this comment to document your API endpoints.';
static examples = ['<%= config.bin %> <%= command.id %>'];
static flags = {
anthropicApiKey: Flags.string({
default: process.env.ANTHROPIC_API_KEY,
required: false,
description: 'Anthropic API key. Also can be set via ANTHROPIC_API_KEY env variable.',
}),
};
initConfigFolder(mainPath) {
const fullPath = path.join(mainPath, 'api-documenter');
if (!existsSync(fullPath))
mkdirSync(fullPath, { recursive: true });
return fullPath;
}
initClient(apiKey) {
return new Anthropic({ apiKey });
}
inferRepoLanguage(repoPath) {
const packageJsonPath = path.join(repoPath, 'package.json');
const hasPackageJson = existsSync(packageJsonPath);
if (hasPackageJson) {
this.log('Detected Node.js repository.');
return RepoLanguage.NODEJS;
}
const requirementsTxtPath = path.join(repoPath, 'requirements.txt');
const hasRequirementsTxt = existsSync(requirementsTxtPath);
if (hasRequirementsTxt) {
this.log('Detected Python repository.');
return RepoLanguage.PYTHON;
}
const pomXmlPath = path.join(repoPath, 'pom.xml');
const hasPomXml = existsSync(pomXmlPath);
if (hasPomXml) {
this.log('Detected Java repository.');
return RepoLanguage.JAVA;
}
const gemfilePath = path.join(repoPath, 'Gemfile');
const hasGemfile = existsSync(gemfilePath);
if (hasGemfile) {
this.log('Detected Ruby repository.');
return RepoLanguage.RUBY;
}
const composerJsonPath = path.join(repoPath, 'composer.json');
const hasComposerJson = existsSync(composerJsonPath);
if (hasComposerJson) {
this.log('Detected PHP repository.');
return RepoLanguage.PHP;
}
const csprojFiles = glob.sync('**/*.csproj', { cwd: repoPath });
if (csprojFiles.length > 0) {
this.log('Detected CSharp repository.');
return RepoLanguage.CSHARP;
}
const goModPath = path.join(repoPath, 'go.mod');
const goSumPath = path.join(repoPath, 'go.sum');
const hasGoMod = existsSync(goModPath);
const hasGoSum = existsSync(goSumPath);
if (hasGoMod || hasGoSum) {
this.log('Detected Go repository.');
return RepoLanguage.GO;
}
this.error('Unsupported repository language. Please use a supported language.');
}
async run() {
const { args, flags } = await this.parse(Doc);
let anthropicApiKey = flags.anthropicApiKey;
if (!anthropicApiKey) {
anthropicApiKey = await input({
message: 'Please enter your Anthropic API key:',
required: true,
validate: (value) => {
if (value.trim() === '') {
return 'API key cannot be empty.';
}
return true;
},
});
}
const repoLanguage = this.inferRepoLanguage(args.path);
const includedFiles = repoLanguageToIncludedFilesMapping[repoLanguage];
this.log(`Detected repository language: ${repoLanguage}`);
this.log(`Using included files pattern: ${includedFiles.join(', ')}`);
const configPath = this.initConfigFolder(args.path);
const repoName = args.path.split('/')[args.path.split('/').length - 1];
const timestamp = new Date().toISOString().split('.')[0];
const sessionPath = path.join(configPath, repoName, timestamp);
mkdirSync(sessionPath, { recursive: true });
this.log(`Session path: ${sessionPath}`);
this.log('Reading repository files...');
const outputPath = path.join(sessionPath, 'repo.md');
await pack([args.path], {
cwd: process.cwd(),
ignore: {
useDefaultPatterns: true,
useGitignore: true,
customPatterns: [],
},
include: includedFiles,
output: {
compress: false,
git: { sortByChanges: false, sortByChangesMaxCommits: 1 },
filePath: outputPath,
style: 'plain',
parsableStyle: true,
fileSummary: false,
directoryStructure: false,
removeComments: true,
removeEmptyLines: true,
showLineNumbers: false,
topFilesLength: 500,
copyToClipboard: false,
includeEmptyDirectories: false,
},
security: { enableSecurityCheck: false },
tokenCount: { encoding: 'o200k_base' },
});
this.log('Repository files are written.', outputPath);
const files = readFileSync(outputPath, 'utf8').toString();
this.log('Sending repository files to Anthropic...');
const client = this.initClient(anthropicApiKey);
const replacedContent = defaultPrompt.user
.replace('{{files}}', files)
.replace('{{repoLanguage}}', repoLanguage);
const stream = await client.messages.create({
max_tokens: 64000,
messages: [
{
content: replacedContent,
role: 'user',
},
],
model: 'claude-sonnet-4-0',
system: defaultPrompt.system,
stream: true,
});
this.log('Streaming API response...');
let result = '';
for await (const chunk of stream) {
if (chunk.type === 'content_block_delta' &&
chunk.delta.type === 'text_delta') {
result += chunk.delta.text;
process.stdout.write(chunk.delta.text);
}
}
this.log('\nAPI endpoints are documented.');
const endpointsPath = path.join(sessionPath, 'endpoints.md');
writeFileSync(endpointsPath, result);
this.log('Documented endpoints are saved to ', endpointsPath);
}
}