@sanjay5114/cdx 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cdx.js +27 -0
- package/commands/auth.js +197 -0
- package/commands/config.js +165 -0
- package/commands/create.js +474 -0
- package/index.js +530 -0
- package/lib/ai.js +249 -0
- package/lib/auth.js +120 -0
- package/lib/fetch.js +46 -0
- package/lib/scanner.js +351 -0
- package/lib/store.js +83 -0
- package/lib/ui.js +477 -0
- package/package.json +35 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CDX Create Command
|
|
5
|
+
* Full pipeline: scan → detect stack → AI passes → write → push → interactive improvement loop
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require("fs").promises;
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const chalk = require("chalk");
|
|
11
|
+
|
|
12
|
+
const { T, section, ok, warn, err, info, note, spinner, progressBar, table, badge, panel, ICONS, footer, rule, center, W } = require("../lib/ui");
|
|
13
|
+
const { runLocalPipeline, runRemotePipeline, pushToGitHub, extractSection } = require("../lib/scanner");
|
|
14
|
+
const { pass1FileSummaries, pass2SystemOverview, pass3FullDocs, pass4Improve } = require("../lib/ai");
|
|
15
|
+
const store = require("../lib/store");
|
|
16
|
+
|
|
17
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
18
|
+
|
|
19
|
+
// ─── Local fallback (no Gemini key) ──────────────────────────────────────────
|
|
20
|
+
function generateLocalDocs(metas, stackInfo) {
|
|
21
|
+
const topFiles = metas.slice(0, 15);
|
|
22
|
+
const byExt = {};
|
|
23
|
+
for (const m of metas) byExt[m.ext] = (byExt[m.ext] || 0) + 1;
|
|
24
|
+
const langs = Object.entries(byExt).sort((a,b)=>b[1]-a[1]).map(([e])=>e || "misc").join(", ");
|
|
25
|
+
const totalFns = metas.reduce((s,m) => s + m.functions, 0);
|
|
26
|
+
const totalImps = metas.reduce((s,m) => s + m.imports, 0);
|
|
27
|
+
|
|
28
|
+
const tree = topFiles.map(m =>
|
|
29
|
+
`├── ${m.file}${" ".repeat(Math.max(1, 50 - m.file.length))}# ${m.functions} fn · ${m.complexity} branches`
|
|
30
|
+
).join("\n");
|
|
31
|
+
|
|
32
|
+
return `# README.md
|
|
33
|
+
|
|
34
|
+
## Project Overview
|
|
35
|
+
|
|
36
|
+
A ${stackInfo.stack} software project comprising **${metas.length} source files** across ${langs}.
|
|
37
|
+
Analyzed ${totalFns} functions and ${totalImps} import declarations.
|
|
38
|
+
Documentation generated locally via CDX — configure a Gemini API key for AI-enhanced output.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- Command-driven architecture with modular command handlers
|
|
43
|
+
- Automated static metadata analysis across ${metas.length} source files
|
|
44
|
+
- Stack-aware file prioritization (detected: ${stackInfo.stack})
|
|
45
|
+
- Local documentation generation (no API required)
|
|
46
|
+
- GitHub repository integration for remote analysis and push
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
|
|
50
|
+
- Node.js >= 18.0.0
|
|
51
|
+
- npm >= 9.0.0
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
\`\`\`bash
|
|
56
|
+
git clone <repository-url>
|
|
57
|
+
cd <project>
|
|
58
|
+
npm install
|
|
59
|
+
cdx config # configure credentials
|
|
60
|
+
cdx create README.md
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
## Project Structure
|
|
64
|
+
|
|
65
|
+
\`\`\`
|
|
66
|
+
${tree}
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
| Key | Required | Default | Description |
|
|
72
|
+
|------------------|----------|------------|-----------------------------------|
|
|
73
|
+
| geminiApiKey | No | — | Gemini API key for AI-enhanced docs |
|
|
74
|
+
| githubToken | No | — | GitHub PAT for remote repos |
|
|
75
|
+
| githubRepo | No | — | Target repo (owner/repo) |
|
|
76
|
+
| firebaseApiKey | No | — | Firebase Web API key for auth |
|
|
77
|
+
|
|
78
|
+
# Architecture
|
|
79
|
+
|
|
80
|
+
## Overview
|
|
81
|
+
|
|
82
|
+
This project follows a ${stackInfo.stack === "node" || stackInfo.stack === "next" ? "layered command-driven" : "modular"} architecture.
|
|
83
|
+
File analysis indicates ${metas.filter(m=>m.exports>0).length} modules with public exports and
|
|
84
|
+
${metas.filter(m=>m.complexity>10).length} high-complexity files warranting attention.
|
|
85
|
+
|
|
86
|
+
## Module Breakdown
|
|
87
|
+
|
|
88
|
+
${topFiles.map(m => `### \`${m.file}\`\n- **Role**: ${m.exports > 0 ? "Exportable module" : "Internal module"}\n- **Complexity**: ${m.complexity} branching statements\n- **Functions**: ${m.functions}\n- **Imports**: ${m.imports} dependencies\n`).join("\n")}
|
|
89
|
+
|
|
90
|
+
# Onboarding
|
|
91
|
+
|
|
92
|
+
## Environment Setup
|
|
93
|
+
|
|
94
|
+
1. **Clone the repository**
|
|
95
|
+
\`\`\`bash
|
|
96
|
+
git clone <repository-url> && cd <project>
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
2. **Install dependencies**
|
|
100
|
+
\`\`\`bash
|
|
101
|
+
npm install
|
|
102
|
+
\`\`\`
|
|
103
|
+
|
|
104
|
+
3. **Configure CDX**
|
|
105
|
+
\`\`\`bash
|
|
106
|
+
cdx config
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
4. **Authenticate** (optional, requires Firebase API key)
|
|
110
|
+
\`\`\`bash
|
|
111
|
+
cdx auth login
|
|
112
|
+
\`\`\`
|
|
113
|
+
|
|
114
|
+
5. **Generate documentation**
|
|
115
|
+
\`\`\`bash
|
|
116
|
+
cdx create README.md
|
|
117
|
+
\`\`\`
|
|
118
|
+
|
|
119
|
+
## Common Pitfalls
|
|
120
|
+
|
|
121
|
+
- Ensure Node.js >= 18 (uses native \`AbortSignal.timeout\`)
|
|
122
|
+
- GitHub token requires \`repo\` scope for push operations
|
|
123
|
+
- Gemini API key must have Generative Language API enabled
|
|
124
|
+
- \`.env\` files are intentionally excluded from AI payloads for security
|
|
125
|
+
- Large repositories (1500+ files) may require additional processing time
|
|
126
|
+
|
|
127
|
+
# Usage
|
|
128
|
+
|
|
129
|
+
## CLI Reference
|
|
130
|
+
|
|
131
|
+
| Command | Description |
|
|
132
|
+
|----------------------------|------------------------------------------|
|
|
133
|
+
| \`cdx create <file>\` | Generate documentation for current dir |
|
|
134
|
+
| \`cdx create <file> --all\` | Include hidden files |
|
|
135
|
+
| \`cdx config\` | Interactive configuration manager |
|
|
136
|
+
| \`cdx auth login\` | Sign in with email/password |
|
|
137
|
+
| \`cdx auth signup\` | Create a new account |
|
|
138
|
+
| \`cdx auth whoami\` | Display current session info |
|
|
139
|
+
| \`cdx auth logout\` | Sign out and clear session |
|
|
140
|
+
| \`cdx start\` | Launch interactive terminal UI |
|
|
141
|
+
|
|
142
|
+
# Security
|
|
143
|
+
|
|
144
|
+
## Secrets Management
|
|
145
|
+
|
|
146
|
+
- All credentials stored in \`~/.cdx/config.json\` with \`0o600\` permissions (owner read/write only)
|
|
147
|
+
- JWT session token stored separately in \`~/.cdx/.session\` with \`0o600\` permissions
|
|
148
|
+
- Sensitive files (\`.env\`, \`*.pem\`, \`*.key\`, private keys) are explicitly excluded from AI payloads
|
|
149
|
+
- GitHub tokens are never transmitted to the Gemini API
|
|
150
|
+
- Firebase ID tokens are decoded locally — signature verification is handled server-side
|
|
151
|
+
|
|
152
|
+
## Threat Mitigations
|
|
153
|
+
|
|
154
|
+
| Threat | Mitigation |
|
|
155
|
+
|-----------------------------|---------------------------------------------------------|
|
|
156
|
+
| Credential theft via config | 0o600 file permissions; ~/.cdx directory at 0o700 |
|
|
157
|
+
| Token leakage to AI | Sensitive file exclusion list + content sanitization |
|
|
158
|
+
| Session hijacking | Short-lived Firebase ID tokens (1h) + refresh rotation |
|
|
159
|
+
| Supply chain attacks | Lockfile integrity; minimal production dependencies |
|
|
160
|
+
|
|
161
|
+
# Contributing
|
|
162
|
+
|
|
163
|
+
## Workflow
|
|
164
|
+
|
|
165
|
+
1. Fork the repository
|
|
166
|
+
2. Create a feature branch: \`git checkout -b feat/your-feature\`
|
|
167
|
+
3. Commit with conventional commits: \`feat:\`, \`fix:\`, \`docs:\`
|
|
168
|
+
4. Open a pull request with a clear description
|
|
169
|
+
|
|
170
|
+
## Checklist
|
|
171
|
+
|
|
172
|
+
- [ ] Tests added/updated
|
|
173
|
+
- [ ] No new sensitive data exposed
|
|
174
|
+
- [ ] Documentation updated
|
|
175
|
+
- [ ] \`npm audit\` passes
|
|
176
|
+
|
|
177
|
+
# Changelog
|
|
178
|
+
|
|
179
|
+
## [Unreleased]
|
|
180
|
+
|
|
181
|
+
### Added
|
|
182
|
+
- Multi-model AI cascade with automatic fallback
|
|
183
|
+
- Firebase Authentication (email/password)
|
|
184
|
+
- Secure JWT session storage
|
|
185
|
+
- Category-based configuration manager
|
|
186
|
+
- Stack-aware file prioritization
|
|
187
|
+
- Interactive post-processing improvement loop
|
|
188
|
+
- GitHub repository integration (remote analysis + push)
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Section writer ───────────────────────────────────────────────────────────
|
|
193
|
+
async function writeDocFiles(cwd, doc, mainOutPath) {
|
|
194
|
+
const docsDir = path.join(cwd, "docs");
|
|
195
|
+
await fs.mkdir(docsDir, { recursive: true });
|
|
196
|
+
|
|
197
|
+
const SECTIONS = {
|
|
198
|
+
"README.md" : extractSection(doc, "README\\.md") || extractSection(doc, "Project Overview"),
|
|
199
|
+
"docs/architecture.md" : extractSection(doc, "Architecture"),
|
|
200
|
+
"docs/onboarding.md" : extractSection(doc, "Onboarding"),
|
|
201
|
+
"docs/usage.md" : extractSection(doc, "Usage"),
|
|
202
|
+
"docs/security.md" : extractSection(doc, "Security"),
|
|
203
|
+
"docs/api-reference.md" : extractSection(doc, "API Reference"),
|
|
204
|
+
"docs/contributing.md" : extractSection(doc, "Contributing"),
|
|
205
|
+
"docs/changelog.md" : extractSection(doc, "Changelog"),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const written = [];
|
|
209
|
+
const docFiles = {};
|
|
210
|
+
|
|
211
|
+
for (const [rel, content] of Object.entries(SECTIONS)) {
|
|
212
|
+
if (!content || content.trim().length < 30) continue;
|
|
213
|
+
const heading = path.basename(rel, ".md")
|
|
214
|
+
.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
215
|
+
const full = `# ${heading}\n\n${content}\n`;
|
|
216
|
+
const absPath = path.join(cwd, rel);
|
|
217
|
+
try {
|
|
218
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
219
|
+
await fs.writeFile(absPath, full, "utf8");
|
|
220
|
+
written.push(rel);
|
|
221
|
+
docFiles[rel] = full;
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Always write the full master doc
|
|
226
|
+
await fs.writeFile(mainOutPath, doc, "utf8");
|
|
227
|
+
docFiles[path.relative(cwd, mainOutPath)] = doc;
|
|
228
|
+
|
|
229
|
+
return { written, docFiles };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Interactive improvement loop ─────────────────────────────────────────────
|
|
233
|
+
async function improvementLoop(apiKey, doc, cwd, mainOutPath, onProgress) {
|
|
234
|
+
const inquirer = (await import("inquirer").catch(() => null))?.default;
|
|
235
|
+
if (!inquirer) return doc;
|
|
236
|
+
|
|
237
|
+
const IMPROVE_CHOICES = [
|
|
238
|
+
{ name: ` ${T.brand("◈")} ${T.white("Expand a specific section")} ${T.dim("Add more depth to any doc section")}`, value: "expand" },
|
|
239
|
+
{ name: ` ${T.brand("◈")} ${T.white("Add code examples")} ${T.dim("Include realistic usage examples")}`, value: "examples" },
|
|
240
|
+
{ name: ` ${T.brand("◈")} ${T.white("Improve security documentation")} ${T.dim("Deeper threat model and controls")}`, value: "security" },
|
|
241
|
+
{ name: ` ${T.brand("◈")} ${T.white("Add deployment guide")} ${T.dim("Docker, CI/CD, cloud deployment")}`, value: "deployment" },
|
|
242
|
+
{ name: ` ${T.brand("◈")} ${T.white("Custom instruction")} ${T.dim("Type your own improvement request")}`, value: "custom" },
|
|
243
|
+
{ name: ` ${T.dim("◈")} ${T.dim("Done — exit")}`, value: "done" },
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
const CANNED = {
|
|
247
|
+
expand : "Significantly expand all sections that are fewer than 200 words. Add concrete details, specific file references, and technical depth throughout.",
|
|
248
|
+
examples : "Add detailed, realistic code examples to the Usage section. Include at minimum: basic usage, advanced configuration, error handling pattern, and integration example.",
|
|
249
|
+
security : "Substantially expand the Security section. Add a formal threat model table with at least 6 threats, detailed secrets management procedures, OWASP top-10 relevance, and incident response steps.",
|
|
250
|
+
deployment: "Add a comprehensive Deployment section covering: Docker containerization, environment variable management, CI/CD pipeline (GitHub Actions), staging vs production configuration, rollback procedures, and health check endpoints.",
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
let current = doc;
|
|
254
|
+
let iteration = 0;
|
|
255
|
+
|
|
256
|
+
while (true) {
|
|
257
|
+
console.log();
|
|
258
|
+
console.log(rule("─", T.dim));
|
|
259
|
+
console.log();
|
|
260
|
+
console.log(` ${T.accent(ICONS.star)} ${T.whiteBold("Documentation generated successfully!")}`);
|
|
261
|
+
console.log();
|
|
262
|
+
|
|
263
|
+
const { action } = await inquirer.prompt([{
|
|
264
|
+
type : "list",
|
|
265
|
+
name : "action",
|
|
266
|
+
message: T.brand("What would you like to do next?"),
|
|
267
|
+
choices: IMPROVE_CHOICES,
|
|
268
|
+
prefix : ` ${T.accent(ICONS.arrow)}`,
|
|
269
|
+
}]);
|
|
270
|
+
|
|
271
|
+
if (action === "done") break;
|
|
272
|
+
|
|
273
|
+
let instruction = CANNED[action] || "";
|
|
274
|
+
|
|
275
|
+
if (action === "custom") {
|
|
276
|
+
const { text } = await inquirer.prompt([{
|
|
277
|
+
type : "input",
|
|
278
|
+
name : "text",
|
|
279
|
+
message: T.brand("Enter your improvement instruction:"),
|
|
280
|
+
prefix : ` ${T.accent(ICONS.arrow)}`,
|
|
281
|
+
validate: v => v.trim().length > 5 || "Please enter a meaningful instruction.",
|
|
282
|
+
}]);
|
|
283
|
+
instruction = text.trim();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!instruction) continue;
|
|
287
|
+
|
|
288
|
+
console.log();
|
|
289
|
+
const spin = spinner("Applying improvement…").start();
|
|
290
|
+
try {
|
|
291
|
+
current = await pass4Improve(apiKey, current, instruction, { onProgress });
|
|
292
|
+
spin.ok(`Improvement applied (iteration ${++iteration})`);
|
|
293
|
+
|
|
294
|
+
// Re-write files with updated content
|
|
295
|
+
const { written } = await writeDocFiles(cwd, current, mainOutPath);
|
|
296
|
+
written.forEach(f => note(`Updated: ${f}`));
|
|
297
|
+
} catch (e) {
|
|
298
|
+
spin.fail(`Improvement failed: ${e.message}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return current;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── Main export ──────────────────────────────────────────────────────────────
|
|
306
|
+
module.exports = async function createCommand(filename, cmdOpts = {}) {
|
|
307
|
+
const cwd = process.cwd();
|
|
308
|
+
const outName = filename.endsWith(".md") ? filename : `${filename}.md`;
|
|
309
|
+
const outPath = path.resolve(cwd, outName);
|
|
310
|
+
|
|
311
|
+
const log = (msg) => { if (cmdOpts.onProgress) cmdOpts.onProgress(msg); else process.stdout.write(`\r ${T.dim(msg)} \n`); };
|
|
312
|
+
|
|
313
|
+
// ── Load config ─────────────────────────────────────────────────────────────
|
|
314
|
+
const cfg = store.load();
|
|
315
|
+
const apiKey = cfg.geminiApiKey || null;
|
|
316
|
+
const aiModel = cfg.geminiModel || "gemini-2.5-pro-preview-03-25";
|
|
317
|
+
const ghToken = cmdOpts.remoteGithub?.token || cfg.githubToken || null;
|
|
318
|
+
const ghRepoRaw = cmdOpts.remoteGithub
|
|
319
|
+
? `${cmdOpts.remoteGithub.owner}/${cmdOpts.remoteGithub.repo}`
|
|
320
|
+
: cfg.githubRepo || null;
|
|
321
|
+
const autoPush = cmdOpts.push !== false && (cmdOpts.autoPush || cfg.autoPush || false);
|
|
322
|
+
|
|
323
|
+
// Parse owner/repo from "owner/repo" or "https://github.com/owner/repo"
|
|
324
|
+
let ghTarget = null;
|
|
325
|
+
if (ghRepoRaw) {
|
|
326
|
+
const m = ghRepoRaw.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?(?:\/|$)/) ||
|
|
327
|
+
ghRepoRaw.match(/^([^/]+)\/([^/]+)$/);
|
|
328
|
+
if (m) ghTarget = { owner: m[1], repo: m[2] };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!apiKey) {
|
|
332
|
+
warn("Gemini API key not configured — using local documentation fallback.");
|
|
333
|
+
info("Run cdx config to configure your Gemini API key for AI-enhanced output.");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Scan / Metadata phase ────────────────────────────────────────────────────
|
|
337
|
+
section("Scanning Project", cmdOpts.remoteGithub ? `${cmdOpts.remoteGithub.owner}/${cmdOpts.remoteGithub.repo}` : cwd);
|
|
338
|
+
|
|
339
|
+
let scanResult;
|
|
340
|
+
const scanSpin = spinner(cmdOpts.remoteGithub ? "Fetching repository tree via GitHub API…" : "Scanning local file system…").start();
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
if (cmdOpts.remoteGithub) {
|
|
344
|
+
scanResult = await runRemotePipeline(cmdOpts.remoteGithub.owner, cmdOpts.remoteGithub.repo, cmdOpts.remoteGithub.token);
|
|
345
|
+
} else {
|
|
346
|
+
scanResult = await runLocalPipeline(cwd, { includeHidden: !!cmdOpts.all });
|
|
347
|
+
}
|
|
348
|
+
scanSpin.ok(`Scanned ${scanResult.raw.length} files → selected ${scanResult.selected.length} for analysis`);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
scanSpin.fail(`Scan failed: ${e.message}`);
|
|
351
|
+
throw e;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const { raw, selected, stackInfo } = scanResult;
|
|
355
|
+
|
|
356
|
+
// Display scan summary
|
|
357
|
+
console.log();
|
|
358
|
+
table([
|
|
359
|
+
["Total files scanned", String(raw.length)],
|
|
360
|
+
["Files selected for AI", String(selected.length)],
|
|
361
|
+
["Detected stack", badge(stackInfo.stack, "info")],
|
|
362
|
+
["Stack signals", stackInfo.reasons.slice(0,2).join("; ")],
|
|
363
|
+
["Sensitive files", String(raw.filter(m=>m.sensitive).length) + " (excluded from AI)"],
|
|
364
|
+
["Test files", String(raw.filter(m=>m.testFile).length) + " (deprioritized)"],
|
|
365
|
+
["Large files (chunked)", String(selected.filter(m=>m.chunked).length)],
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
// ── AI passes ────────────────────────────────────────────────────────────────
|
|
369
|
+
let doc = "";
|
|
370
|
+
|
|
371
|
+
if (apiKey) {
|
|
372
|
+
section("AI Documentation Generation", `Model: ${aiModel}`);
|
|
373
|
+
|
|
374
|
+
const aiOpts = {
|
|
375
|
+
onProgress: log,
|
|
376
|
+
maxRetries: 7,
|
|
377
|
+
baseDelay : 3500,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
// Pass 1 — per-file summaries
|
|
382
|
+
const p1spin = spinner("Pass 1 — Analyzing individual modules…").start();
|
|
383
|
+
let summaries;
|
|
384
|
+
try {
|
|
385
|
+
summaries = await pass1FileSummaries(apiKey, selected, aiOpts);
|
|
386
|
+
p1spin.ok(`Pass 1 complete — summarized ${summaries.length} modules`);
|
|
387
|
+
} catch (e) {
|
|
388
|
+
p1spin.fail(`Pass 1 failed: ${e.message}`);
|
|
389
|
+
throw e;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Pass 2 — system overview
|
|
393
|
+
const p2spin = spinner("Pass 2 — Synthesizing system architecture…").start();
|
|
394
|
+
let overview;
|
|
395
|
+
try {
|
|
396
|
+
overview = await pass2SystemOverview(apiKey, selected, summaries, stackInfo, aiOpts);
|
|
397
|
+
p2spin.ok("Pass 2 complete — architecture overview generated");
|
|
398
|
+
} catch (e) {
|
|
399
|
+
p2spin.fail(`Pass 2 failed: ${e.message}`);
|
|
400
|
+
throw e;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Pass 3 — full documentation suite
|
|
404
|
+
const p3spin = spinner("Pass 3 — Composing comprehensive documentation suite…").start();
|
|
405
|
+
try {
|
|
406
|
+
doc = await pass3FullDocs(apiKey, overview, summaries, selected, stackInfo, aiOpts);
|
|
407
|
+
p3spin.ok("Pass 3 complete — full documentation suite generated");
|
|
408
|
+
} catch (e) {
|
|
409
|
+
p3spin.fail(`Pass 3 failed: ${e.message}`);
|
|
410
|
+
throw e;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
} catch (e) {
|
|
414
|
+
err(`AI generation failed: ${e.message}`);
|
|
415
|
+
if (e.code === "RATE_LIMIT_EXCEEDED") {
|
|
416
|
+
warn("Rate limit exhausted. Falling back to local documentation generation.");
|
|
417
|
+
} else {
|
|
418
|
+
warn("Falling back to local documentation generation.");
|
|
419
|
+
}
|
|
420
|
+
doc = generateLocalDocs(selected, stackInfo);
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
doc = generateLocalDocs(selected, stackInfo);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Write files ──────────────────────────────────────────────────────────────
|
|
427
|
+
section("Writing Documentation Files");
|
|
428
|
+
|
|
429
|
+
const writeSpin = spinner("Writing documentation files to disk…").start();
|
|
430
|
+
let docFiles = {};
|
|
431
|
+
try {
|
|
432
|
+
const result = await writeDocFiles(cwd, doc, outPath);
|
|
433
|
+
docFiles = result.docFiles;
|
|
434
|
+
writeSpin.ok(`Wrote ${result.written.length + 1} documentation files`);
|
|
435
|
+
result.written.forEach(f => note(` ${f}`));
|
|
436
|
+
note(` ${outName} (master document)`);
|
|
437
|
+
} catch (e) {
|
|
438
|
+
writeSpin.fail(`Write failed: ${e.message}`);
|
|
439
|
+
throw e;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── GitHub push ──────────────────────────────────────────────────────────────
|
|
443
|
+
if (ghToken && ghTarget && autoPush) {
|
|
444
|
+
section("GitHub Push", `${ghTarget.owner}/${ghTarget.repo}`);
|
|
445
|
+
const pushSpin = spinner(`Pushing documentation to ${ghTarget.owner}/${ghTarget.repo}…`).start();
|
|
446
|
+
try {
|
|
447
|
+
const { pushed, failed } = await pushToGitHub(ghToken, ghTarget.owner, ghTarget.repo, docFiles, log);
|
|
448
|
+
pushSpin.ok(`Pushed ${pushed.length} file(s) to GitHub`);
|
|
449
|
+
if (failed.length) {
|
|
450
|
+
failed.forEach(f => warn(`Push failed: ${f.path} — ${f.error.split("\n")[0]}`));
|
|
451
|
+
}
|
|
452
|
+
} catch (e) {
|
|
453
|
+
pushSpin.fail(`GitHub push failed: ${e.message}`);
|
|
454
|
+
}
|
|
455
|
+
} else if (ghToken && ghTarget && !autoPush) {
|
|
456
|
+
note(`GitHub push skipped (auto-push disabled). Run with --push to push now.`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Interactive improvement loop (only in terminal, not in piped/non-TTY mode) ──
|
|
460
|
+
if (apiKey && process.stdout.isTTY && !cmdOpts.noInteractive) {
|
|
461
|
+
await improvementLoop(apiKey, doc, cwd, outPath, log);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── Final summary ────────────────────────────────────────────────────────────
|
|
465
|
+
section("Generation Complete");
|
|
466
|
+
ok(`Master document: ${chalk.cyan(outName)}`);
|
|
467
|
+
if (cfg.generateSubDocs !== false) {
|
|
468
|
+
ok("Sub-documents written to docs/");
|
|
469
|
+
}
|
|
470
|
+
note(`Total files analyzed: ${raw.length}`);
|
|
471
|
+
note(`AI model used: ${apiKey ? aiModel : "local fallback (no API key)"}`);
|
|
472
|
+
|
|
473
|
+
footer();
|
|
474
|
+
};
|