@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.
@@ -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
+ };