3.3 API Route Generator (api-routes.ts): - Two modes: 'direct' (Cosmos DB CRUD) and 'proxy' (backend fetch) - Generates route.ts + [id]/route.ts (Next.js App Router named exports) - Direct mode also generates lib/schemas/ + lib/repositories/ files - withErrorHandler HOF wrapper, Zod validation, auth check - Dry-run preview, configurable methods, skip existing files 3.4 AGENTS.md Auto-Generator (agents-md.ts): - Reads shared/product.json for identity, port, domain - Scans repo for backend modules, lib files, test counts - Generates full AGENTS.md with identity, layout, stack, conventions - --update preserves CUSTOM sections - Creates CLAUDE.md, .cursorrules, .windsurfrules symlinks
604 lines
21 KiB
JavaScript
604 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* AGENTS.md Auto-Generator
|
||
*
|
||
* Generates AGENTS.md from product.json + repo directory scan.
|
||
* Also creates/updates symlinks: CLAUDE.md, .cursorrules, .windsurfrules
|
||
*
|
||
* Usage:
|
||
* npx tsx agents-md.ts --repo /path/to/product-repo
|
||
* npx tsx agents-md.ts --repo /path/to/product-repo --dry-run
|
||
* npx tsx agents-md.ts --repo /path/to/product-repo --update # preserves <!-- CUSTOM --> sections
|
||
*
|
||
* @module @bytelyst/create-app/generators/agents-md
|
||
*/
|
||
|
||
import { promises as fs } from 'node:fs';
|
||
import path from 'node:path';
|
||
import { execSync } from 'node:child_process';
|
||
|
||
// ── CLI ──────────────────────────────────────────────────────────────────────
|
||
|
||
interface Options {
|
||
repo: string;
|
||
dryRun: boolean;
|
||
update: boolean;
|
||
}
|
||
|
||
function parseArgs(): Options {
|
||
const args = process.argv.slice(2);
|
||
const options: Options = { repo: '.', dryRun: false, update: false };
|
||
|
||
for (let i = 0; i < args.length; i++) {
|
||
const arg = args[i];
|
||
if (arg === '--repo' || arg === '-r') options.repo = args[++i];
|
||
else if (arg === '--dry-run' || arg === '-d') options.dryRun = true;
|
||
else if (arg === '--update' || arg === '-u') options.update = true;
|
||
else if (arg === '--help' || arg === '-h') {
|
||
showHelp();
|
||
process.exit(0);
|
||
}
|
||
}
|
||
|
||
return options;
|
||
}
|
||
|
||
function showHelp(): void {
|
||
console.log(`
|
||
AGENTS.md Auto-Generator
|
||
|
||
Generates AGENTS.md from product.json + repo scan, plus symlinks for
|
||
CLAUDE.md, .cursorrules, and .windsurfrules.
|
||
|
||
Usage:
|
||
npx tsx agents-md.ts --repo <path> [--dry-run] [--update]
|
||
|
||
Options:
|
||
--repo, -r Path to product repo root (default: ".")
|
||
--dry-run, -d Preview without writing files
|
||
--update, -u Preserve <!-- CUSTOM --> sections in existing AGENTS.md
|
||
--help, -h Show this help
|
||
|
||
Custom Sections:
|
||
Wrap any hand-written content in <!-- CUSTOM:key --> / <!-- /CUSTOM:key -->
|
||
markers. The --update flag preserves these sections when regenerating.
|
||
`);
|
||
}
|
||
|
||
// ── Product.json Loader ──────────────────────────────────────────────────────
|
||
|
||
interface ProductManifest {
|
||
productId: string;
|
||
displayName: string;
|
||
tagline?: string;
|
||
domain?: string;
|
||
backendPort?: number;
|
||
primarySurface?: string;
|
||
mobileCompanion?: boolean;
|
||
bundleIds?: Record<string, string>;
|
||
bundleId?: string;
|
||
version?: string;
|
||
description?: string;
|
||
licensePrefix?: string;
|
||
configDirName?: string;
|
||
envVarPrefix?: string;
|
||
}
|
||
|
||
async function loadProductJson(repoPath: string): Promise<ProductManifest> {
|
||
const candidates = [
|
||
path.join(repoPath, 'shared', 'product.json'),
|
||
path.join(repoPath, 'product.json'),
|
||
];
|
||
|
||
for (const p of candidates) {
|
||
try {
|
||
const raw = await fs.readFile(p, 'utf-8');
|
||
return JSON.parse(raw);
|
||
} catch {
|
||
// try next
|
||
}
|
||
}
|
||
|
||
throw new Error('product.json not found in shared/ or repo root');
|
||
}
|
||
|
||
// ── Repo Scanner ─────────────────────────────────────────────────────────────
|
||
|
||
interface RepoInfo {
|
||
repoName: string;
|
||
hasFastifyBackend: boolean;
|
||
hasNextWeb: boolean;
|
||
hasExpoMobile: boolean;
|
||
hasSwiftIos: boolean;
|
||
hasKotlinAndroid: boolean;
|
||
hasKmpShared: boolean;
|
||
backendModules: string[];
|
||
backendTestCount: number;
|
||
webTestCount: number;
|
||
mobileTestCount: number;
|
||
backendLibFiles: string[];
|
||
webLibFiles: string[];
|
||
cosmosContainers: string[];
|
||
techStack: { layer: string; tech: string }[];
|
||
buildCommands: string[];
|
||
}
|
||
|
||
async function dirExists(p: string): Promise<boolean> {
|
||
try {
|
||
const st = await fs.stat(p);
|
||
return st.isDirectory();
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function fileExists(p: string): Promise<boolean> {
|
||
try {
|
||
await fs.access(p);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function countTestsInFiles(dir: string): number {
|
||
try {
|
||
const output = execSync(
|
||
`grep -r "\\b\\(it\\|test\\)(" "${dir}" --include="*.test.ts" --include="*.test.tsx" --include="*.spec.ts" 2>/dev/null | wc -l`,
|
||
{ encoding: 'utf-8', timeout: 5000 }
|
||
);
|
||
return parseInt(output.trim()) || 0;
|
||
} catch {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
async function listDirs(dir: string): Promise<string[]> {
|
||
try {
|
||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||
return entries.filter(e => e.isDirectory()).map(e => e.name);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function listFiles(dir: string): Promise<string[]> {
|
||
try {
|
||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||
return entries.filter(e => e.isFile()).map(e => e.name);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function scanRepo(repoPath: string, manifest: ProductManifest): Promise<RepoInfo> {
|
||
const repoName = path.basename(repoPath);
|
||
|
||
// Detect surfaces
|
||
const hasFastifyBackend = await dirExists(path.join(repoPath, 'backend', 'src'));
|
||
const hasNextWeb =
|
||
(await fileExists(path.join(repoPath, 'web', 'next.config.ts'))) ||
|
||
(await fileExists(path.join(repoPath, 'web', 'next.config.js'))) ||
|
||
(await fileExists(path.join(repoPath, 'mindlyst-native', 'web', 'next.config.ts')));
|
||
const hasExpoMobile =
|
||
(await fileExists(path.join(repoPath, 'mobile', 'app.json'))) ||
|
||
(await fileExists(path.join(repoPath, 'app.json')));
|
||
const hasSwiftIos = await dirExists(path.join(repoPath, 'ios'));
|
||
const hasKotlinAndroid = await dirExists(path.join(repoPath, 'android'));
|
||
const hasKmpShared = await dirExists(path.join(repoPath, 'shared', 'src'));
|
||
|
||
// Backend modules
|
||
const modulesDir = path.join(repoPath, 'backend', 'src', 'modules');
|
||
const backendModules = await listDirs(modulesDir);
|
||
|
||
// Backend lib files
|
||
const libDir = path.join(repoPath, 'backend', 'src', 'lib');
|
||
const backendLibFiles = (await listFiles(libDir)).filter(
|
||
f => f.endsWith('.ts') && !f.endsWith('.test.ts')
|
||
);
|
||
|
||
// Web lib files
|
||
let webLibDir = path.join(repoPath, 'web', 'src', 'lib');
|
||
if (!(await dirExists(webLibDir))) {
|
||
webLibDir = path.join(repoPath, 'web', 'src', 'components', 'lib');
|
||
}
|
||
const webLibFiles = (await listFiles(webLibDir)).filter(
|
||
f => f.endsWith('.ts') && !f.endsWith('.test.ts')
|
||
);
|
||
|
||
// Test counts
|
||
const backendTestCount = hasFastifyBackend
|
||
? countTestsInFiles(path.join(repoPath, 'backend', 'src'))
|
||
: 0;
|
||
|
||
let webTestDir = path.join(repoPath, 'web', 'src');
|
||
if (!(await dirExists(webTestDir))) {
|
||
webTestDir = path.join(repoPath, 'mindlyst-native', 'web', 'src');
|
||
}
|
||
const webTestCount = hasNextWeb ? countTestsInFiles(webTestDir) : 0;
|
||
|
||
const mobileTestDir = hasExpoMobile
|
||
? (await dirExists(path.join(repoPath, 'mobile')))
|
||
? path.join(repoPath, 'mobile')
|
||
: repoPath
|
||
: '';
|
||
const mobileTestCount = mobileTestDir ? countTestsInFiles(mobileTestDir) : 0;
|
||
|
||
// Cosmos containers — scan backend types files
|
||
const cosmosContainers: string[] = [];
|
||
for (const mod of backendModules) {
|
||
const typesFile = path.join(modulesDir, mod, 'types.ts');
|
||
if (await fileExists(typesFile)) {
|
||
cosmosContainers.push(mod.replace(/-/g, '_'));
|
||
}
|
||
}
|
||
|
||
// Tech stack
|
||
const techStack: { layer: string; tech: string }[] = [];
|
||
if (hasFastifyBackend)
|
||
techStack.push({
|
||
layer: 'Backend',
|
||
tech: `Fastify 5, TypeScript ESM, Zod, jose (JWT), @bytelyst/datastore`,
|
||
});
|
||
if (hasNextWeb)
|
||
techStack.push({ layer: 'Web', tech: 'Next.js 16 (App Router), React 19, TypeScript' });
|
||
if (hasExpoMobile)
|
||
techStack.push({ layer: 'Mobile', tech: 'React Native (Expo), TypeScript, expo-router' });
|
||
if (hasSwiftIos) techStack.push({ layer: 'iOS', tech: 'SwiftUI (iOS 17+)' });
|
||
if (hasKotlinAndroid)
|
||
techStack.push({ layer: 'Android', tech: 'Jetpack Compose, Material 3, Kotlin' });
|
||
if (hasKmpShared) techStack.push({ layer: 'Shared', tech: 'Kotlin Multiplatform (KMP)' });
|
||
techStack.push({
|
||
layer: 'Platform',
|
||
tech: 'platform-service (port 4003) for auth, flags, telemetry, billing',
|
||
});
|
||
techStack.push({
|
||
layer: 'Database',
|
||
tech: `Azure Cosmos DB via @bytelyst/datastore — productId: "${manifest.productId}"`,
|
||
});
|
||
|
||
// Build commands
|
||
const buildCommands: string[] = [];
|
||
if (hasFastifyBackend) {
|
||
const port = manifest.backendPort ?? 4000;
|
||
buildCommands.push(`cd backend && npm run dev # Dev server (port ${port})`);
|
||
buildCommands.push(`cd backend && npm run typecheck # tsc --noEmit`);
|
||
buildCommands.push(`cd backend && npm test # Vitest tests`);
|
||
}
|
||
if (hasNextWeb) {
|
||
buildCommands.push(`cd web && npm run dev # Dev server`);
|
||
buildCommands.push(`cd web && npm run typecheck # tsc --noEmit`);
|
||
buildCommands.push(`cd web && npm run build # Production build`);
|
||
}
|
||
if (hasExpoMobile) {
|
||
buildCommands.push(`cd mobile && npm start # Expo dev server`);
|
||
buildCommands.push(`cd mobile && npm run typecheck # tsc --noEmit`);
|
||
}
|
||
|
||
return {
|
||
repoName,
|
||
hasFastifyBackend,
|
||
hasNextWeb,
|
||
hasExpoMobile,
|
||
hasSwiftIos,
|
||
hasKotlinAndroid,
|
||
hasKmpShared,
|
||
backendModules,
|
||
backendTestCount,
|
||
webTestCount,
|
||
mobileTestCount,
|
||
backendLibFiles,
|
||
webLibFiles,
|
||
cosmosContainers,
|
||
techStack,
|
||
buildCommands,
|
||
};
|
||
}
|
||
|
||
// ── Markdown Generator ───────────────────────────────────────────────────────
|
||
|
||
function generateAgentsMd(manifest: ProductManifest, info: RepoInfo): string {
|
||
const { productId, displayName, domain, tagline } = manifest;
|
||
const repoDesc = tagline ?? `${displayName} product`;
|
||
|
||
const lines: string[] = [];
|
||
|
||
// ── Header
|
||
lines.push(`# AGENTS.md — AI Coding Agent Instructions`);
|
||
lines.push('');
|
||
lines.push(
|
||
`> **For:** Claude Code, OpenAI Codex, Cursor, GitHub Copilot, Windsurf Cascade, and any AI coding agent.`
|
||
);
|
||
lines.push(`> **Repo:** \`${info.repoName}\` — ${repoDesc}.`);
|
||
lines.push('');
|
||
lines.push('---');
|
||
lines.push('');
|
||
|
||
// ── 1. Project Identity
|
||
lines.push('## 1. Project Identity');
|
||
lines.push('');
|
||
lines.push('| Key | Value |');
|
||
lines.push('|-----|-------|');
|
||
lines.push(`| **Product** | ${displayName} |`);
|
||
lines.push(`| **Product ID** | \`${productId}\` |`);
|
||
if (domain) lines.push(`| **Domain** | ${domain} |`);
|
||
lines.push(`| **Repo** | \`${info.repoName}\` |`);
|
||
lines.push(`| **Ecosystem** | ByteLyst (shares platform-service with other ByteLyst products) |`);
|
||
lines.push('');
|
||
|
||
// ── 2. Repo Layout (simplified tree)
|
||
lines.push('## 2. Repo Layout');
|
||
lines.push('');
|
||
lines.push('```');
|
||
lines.push(`${info.repoName}/`);
|
||
if (info.hasFastifyBackend) {
|
||
const port = manifest.backendPort ?? 4000;
|
||
lines.push(
|
||
`├── backend/ # Fastify 5 + TypeScript ESM backend (port ${port})`
|
||
);
|
||
lines.push(`│ ├── src/`);
|
||
if (info.backendLibFiles.length > 0) {
|
||
lines.push(`│ │ ├── lib/ # Shared backend wiring`);
|
||
for (const f of info.backendLibFiles.slice(0, 8)) {
|
||
lines.push(`│ │ │ ├── ${f}`);
|
||
}
|
||
if (info.backendLibFiles.length > 8) {
|
||
lines.push(`│ │ │ └── ... (${info.backendLibFiles.length - 8} more)`);
|
||
}
|
||
}
|
||
if (info.backendModules.length > 0) {
|
||
lines.push(`│ │ ├── modules/`);
|
||
for (const m of info.backendModules) {
|
||
lines.push(`│ │ │ ├── ${m}/`);
|
||
}
|
||
}
|
||
lines.push(`│ │ └── server.ts`);
|
||
lines.push(`│ ├── package.json`);
|
||
lines.push(`│ └── tsconfig.json`);
|
||
lines.push('│');
|
||
}
|
||
if (info.hasNextWeb) {
|
||
lines.push(`├── web/ # Next.js 16 + React 19 (App Router)`);
|
||
lines.push(`│ ├── src/`);
|
||
lines.push(`│ │ ├── app/ # App Router pages`);
|
||
if (info.webLibFiles.length > 0) {
|
||
lines.push(`│ │ └── lib/ # Pure TS clients + config`);
|
||
}
|
||
lines.push(`│ ├── package.json`);
|
||
lines.push(`│ └── tsconfig.json`);
|
||
lines.push('│');
|
||
}
|
||
if (info.hasExpoMobile) {
|
||
lines.push(`├── mobile/ # React Native + Expo`);
|
||
lines.push('│');
|
||
}
|
||
if (info.hasSwiftIos) {
|
||
lines.push(`├── ios/ # SwiftUI native app`);
|
||
lines.push('│');
|
||
}
|
||
if (info.hasKotlinAndroid) {
|
||
lines.push(`├── android/ # Jetpack Compose`);
|
||
lines.push('│');
|
||
}
|
||
lines.push(`├── shared/`);
|
||
lines.push(`│ └── product.json # Canonical product identity`);
|
||
lines.push(`├── AGENTS.md # This file`);
|
||
lines.push(`└── README.md`);
|
||
lines.push('```');
|
||
lines.push('');
|
||
|
||
// ── 3. Tech Stack
|
||
lines.push('## 3. Tech Stack');
|
||
lines.push('');
|
||
lines.push('| Layer | Technology |');
|
||
lines.push('|-------|-----------|');
|
||
for (const { layer, tech } of info.techStack) {
|
||
lines.push(`| **${layer}** | ${tech} |`);
|
||
}
|
||
|
||
// Test counts
|
||
const totalTests = info.backendTestCount + info.webTestCount + info.mobileTestCount;
|
||
if (totalTests > 0) {
|
||
const parts: string[] = [];
|
||
if (info.backendTestCount > 0) parts.push(`${info.backendTestCount} backend`);
|
||
if (info.webTestCount > 0) parts.push(`${info.webTestCount} web`);
|
||
if (info.mobileTestCount > 0) parts.push(`${info.mobileTestCount} mobile`);
|
||
lines.push(`| **Tests** | Vitest — ~${totalTests} tests (${parts.join(' + ')}) |`);
|
||
}
|
||
lines.push('');
|
||
|
||
// ── 4. Coding Conventions
|
||
lines.push('## 4. Coding Conventions');
|
||
lines.push('');
|
||
lines.push('### MUST follow');
|
||
lines.push('');
|
||
lines.push(`- Every Cosmos document MUST include a \`productId: "${productId}"\` field`);
|
||
if (info.hasFastifyBackend) {
|
||
lines.push('- Backend modules follow `types.ts` → `repository.ts` → `routes.ts` pattern');
|
||
lines.push(
|
||
'- All repositories use `@bytelyst/datastore` getCollection() — never direct Cosmos SDK calls'
|
||
);
|
||
}
|
||
if (info.hasNextWeb) {
|
||
lines.push('- Web engine logic in `web/src/lib/` — pure TS, no React imports');
|
||
lines.push('- Web components in `web/src/components/` — React UI only');
|
||
}
|
||
if (info.hasExpoMobile) {
|
||
lines.push('- Mobile engine logic in `mobile/src/lib/` — pure TS, no React Native imports');
|
||
}
|
||
lines.push(
|
||
'- Commit messages: `type(scope): description` — types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`'
|
||
);
|
||
lines.push('');
|
||
lines.push('### MUST NOT do');
|
||
lines.push('');
|
||
lines.push(
|
||
'- Never use `console.log` in production code — use `req.log` or `app.log` in Fastify'
|
||
);
|
||
lines.push('- Never use `any` type — use Zod inference or explicit types');
|
||
lines.push('- Never hardcode colors — use theme tokens');
|
||
lines.push('- Never hardcode API URLs — use env vars or config');
|
||
lines.push(`- Never hardcode product ID — use \`productConfig.productId\``);
|
||
lines.push('- Never modify tests to make them pass — fix the actual code');
|
||
lines.push('- Never delete existing comments or documentation unless explicitly asked');
|
||
lines.push('- Never add emojis to code unless explicitly asked');
|
||
lines.push('');
|
||
|
||
// ── 5. Build & Test Commands
|
||
if (info.buildCommands.length > 0) {
|
||
lines.push('## 5. Build & Test Commands');
|
||
lines.push('');
|
||
lines.push('```bash');
|
||
for (const cmd of info.buildCommands) {
|
||
lines.push(cmd);
|
||
}
|
||
lines.push('```');
|
||
lines.push('');
|
||
}
|
||
|
||
// ── 6. Backend API Modules (if applicable)
|
||
if (info.hasFastifyBackend && info.backendModules.length > 0) {
|
||
lines.push('## 6. Backend Modules');
|
||
lines.push('');
|
||
lines.push('| Module | Container | Description |');
|
||
lines.push('|--------|-----------|-------------|');
|
||
for (const mod of info.backendModules) {
|
||
const container = mod.replace(/-/g, '_');
|
||
lines.push(`| \`${mod}\` | \`${container}\` | ${mod.replace(/-/g, ' ')} |`);
|
||
}
|
||
lines.push('');
|
||
}
|
||
|
||
// ── Custom section placeholder
|
||
lines.push('<!-- CUSTOM:extra -->');
|
||
lines.push('<!-- /CUSTOM:extra -->');
|
||
lines.push('');
|
||
|
||
return lines.join('\n');
|
||
}
|
||
|
||
// ── Custom Section Preservation ──────────────────────────────────────────────
|
||
|
||
function extractCustomSections(content: string): Map<string, string> {
|
||
const sections = new Map<string, string>();
|
||
const regex = /<!-- CUSTOM:(\w+) -->\n([\s\S]*?)<!-- \/CUSTOM:\1 -->/g;
|
||
let match;
|
||
while ((match = regex.exec(content)) !== null) {
|
||
sections.set(match[1], match[2]);
|
||
}
|
||
return sections;
|
||
}
|
||
|
||
function mergeCustomSections(newContent: string, existing: Map<string, string>): string {
|
||
let result = newContent;
|
||
for (const [key, value] of existing) {
|
||
const placeholder = `<!-- CUSTOM:${key} -->\n<!-- /CUSTOM:${key} -->`;
|
||
const replacement = `<!-- CUSTOM:${key} -->\n${value}<!-- /CUSTOM:${key} -->`;
|
||
result = result.replace(placeholder, replacement);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ── Symlink Manager ──────────────────────────────────────────────────────────
|
||
|
||
async function ensureSymlinks(repoPath: string, dryRun: boolean): Promise<void> {
|
||
const targets = ['CLAUDE.md', '.cursorrules', '.windsurfrules'];
|
||
|
||
for (const target of targets) {
|
||
const linkPath = path.join(repoPath, target);
|
||
const exists = await fileExists(linkPath);
|
||
|
||
if (exists) {
|
||
try {
|
||
const stat = await fs.lstat(linkPath);
|
||
if (stat.isSymbolicLink()) {
|
||
const linkTarget = await fs.readlink(linkPath);
|
||
if (linkTarget === 'AGENTS.md') {
|
||
continue; // already correct
|
||
}
|
||
}
|
||
} catch {
|
||
// not a symlink
|
||
}
|
||
}
|
||
|
||
if (dryRun) {
|
||
console.log(` 📝 Would create symlink: ${target} → AGENTS.md`);
|
||
} else {
|
||
try {
|
||
if (exists) await fs.unlink(linkPath);
|
||
await fs.symlink('AGENTS.md', linkPath);
|
||
console.log(` ✅ ${target} → AGENTS.md`);
|
||
} catch (err) {
|
||
console.log(
|
||
` ⚠️ Could not create symlink ${target}: ${err instanceof Error ? err.message : String(err)}`
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Main ─────────────────────────────────────────────────────────────────────
|
||
|
||
async function main(): Promise<void> {
|
||
const { repo, dryRun, update } = parseArgs();
|
||
const repoPath = path.resolve(repo);
|
||
|
||
console.log(`\n📄 AGENTS.md Generator`);
|
||
console.log(` Repo: ${repoPath}`);
|
||
if (dryRun) console.log(' ⚠️ DRY RUN — no files will be written\n');
|
||
if (update) console.log(' 🔄 UPDATE mode — preserving custom sections\n');
|
||
|
||
// Load product.json
|
||
const manifest = await loadProductJson(repoPath);
|
||
console.log(` Product: ${manifest.displayName} (${manifest.productId})`);
|
||
|
||
// Scan repo
|
||
const info = await scanRepo(repoPath, manifest);
|
||
console.log(` Backend modules: ${info.backendModules.length}`);
|
||
console.log(` Tests: ~${info.backendTestCount + info.webTestCount + info.mobileTestCount}`);
|
||
console.log('');
|
||
|
||
// Generate content
|
||
let content = generateAgentsMd(manifest, info);
|
||
|
||
// Merge custom sections if updating
|
||
if (update) {
|
||
const agentsPath = path.join(repoPath, 'AGENTS.md');
|
||
try {
|
||
const existing = await fs.readFile(agentsPath, 'utf-8');
|
||
const customSections = extractCustomSections(existing);
|
||
if (customSections.size > 0) {
|
||
console.log(` 🔄 Preserving ${customSections.size} custom section(s)`);
|
||
content = mergeCustomSections(content, customSections);
|
||
}
|
||
} catch {
|
||
console.log(' ℹ️ No existing AGENTS.md to preserve custom sections from');
|
||
}
|
||
}
|
||
|
||
if (dryRun) {
|
||
console.log('── AGENTS.md ──────────────────────────────────────');
|
||
console.log(content);
|
||
console.log('\n── Symlinks ──────────────────────────────────────');
|
||
await ensureSymlinks(repoPath, true);
|
||
console.log('\n✨ Dry run complete.');
|
||
return;
|
||
}
|
||
|
||
// Write AGENTS.md
|
||
const agentsPath = path.join(repoPath, 'AGENTS.md');
|
||
await fs.writeFile(agentsPath, content, 'utf-8');
|
||
console.log(` ✅ AGENTS.md written`);
|
||
|
||
// Ensure symlinks
|
||
await ensureSymlinks(repoPath, false);
|
||
|
||
console.log(`\n✨ AGENTS.md generated for ${manifest.displayName}.`);
|
||
}
|
||
|
||
main().catch(err => {
|
||
console.error('❌ Error:', err instanceof Error ? err.message : String(err));
|
||
process.exit(1);
|
||
});
|