UNPKG

@batuhanw/api-documenter

Version:
258 lines (257 loc) 9.37 kB
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); } }