@latent-space-labs/open-auto-doc 0.3.3 → 0.3.5

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.
Files changed (2) hide show
  1. package/dist/index.js +891 -570
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
- import * as p5 from "@clack/prompts";
8
- import fs8 from "fs";
9
- import path8 from "path";
7
+ import * as p6 from "@clack/prompts";
8
+ import fs9 from "fs";
9
+ import path9 from "path";
10
10
  import { fileURLToPath as fileURLToPath2 } from "url";
11
11
 
12
12
  // src/auth/device-flow.ts
@@ -37,10 +37,10 @@ Opening ${deviceData.verification_uri} in your browser...`,
37
37
  } catch {
38
38
  p.log.warn(`Could not open browser. Please visit: ${deviceData.verification_uri}`);
39
39
  }
40
- const spinner7 = p.spinner();
41
- spinner7.start("Waiting for GitHub authorization...");
40
+ const spinner8 = p.spinner();
41
+ spinner8.start("Waiting for GitHub authorization...");
42
42
  const token = await pollForToken(deviceData.device_code, deviceData.interval);
43
- spinner7.stop("GitHub authentication successful!");
43
+ spinner8.stop("GitHub authentication successful!");
44
44
  return token;
45
45
  }
46
46
  async function pollForToken(deviceCode, interval) {
@@ -162,8 +162,8 @@ import { Octokit } from "@octokit/rest";
162
162
  import * as p2 from "@clack/prompts";
163
163
  async function pickRepos(token) {
164
164
  const octokit = new Octokit({ auth: token });
165
- const spinner7 = p2.spinner();
166
- spinner7.start("Fetching your repositories...");
165
+ const spinner8 = p2.spinner();
166
+ spinner8.start("Fetching your repositories...");
167
167
  const repos = [];
168
168
  let page = 1;
169
169
  while (true) {
@@ -188,7 +188,7 @@ async function pickRepos(token) {
188
188
  if (data.length < 100) break;
189
189
  page++;
190
190
  }
191
- spinner7.stop(`Found ${repos.length} repositories`);
191
+ spinner8.stop(`Found ${repos.length} repositories`);
192
192
  const selected = await p2.multiselect({
193
193
  message: "Select repositories to document",
194
194
  options: repos.slice(0, 50).map((r) => ({
@@ -289,8 +289,8 @@ async function createAndPushDocsRepo(params) {
289
289
  ]
290
290
  });
291
291
  if (p3.isCancel(visibility)) return null;
292
- const spinner7 = p3.spinner();
293
- spinner7.start(`Creating GitHub repo ${owner}/${repoName}...`);
292
+ const spinner8 = p3.spinner();
293
+ spinner8.start(`Creating GitHub repo ${owner}/${repoName}...`);
294
294
  let repoUrl;
295
295
  try {
296
296
  if (isOrg) {
@@ -302,7 +302,7 @@ async function createAndPushDocsRepo(params) {
302
302
  auto_init: false
303
303
  });
304
304
  repoUrl = data.clone_url;
305
- spinner7.stop(`Created ${data.full_name}`);
305
+ spinner8.stop(`Created ${data.full_name}`);
306
306
  } else {
307
307
  const { data } = await octokit.rest.repos.createForAuthenticatedUser({
308
308
  name: repoName,
@@ -311,10 +311,10 @@ async function createAndPushDocsRepo(params) {
311
311
  auto_init: false
312
312
  });
313
313
  repoUrl = data.clone_url;
314
- spinner7.stop(`Created ${data.full_name}`);
314
+ spinner8.stop(`Created ${data.full_name}`);
315
315
  }
316
316
  } catch (err) {
317
- spinner7.stop("Failed to create repo.");
317
+ spinner8.stop("Failed to create repo.");
318
318
  if (err?.status === 422) {
319
319
  p3.log.error(`Repository "${repoName}" already exists. Choose a different name or delete it first.`);
320
320
  } else {
@@ -322,7 +322,7 @@ async function createAndPushDocsRepo(params) {
322
322
  }
323
323
  return null;
324
324
  }
325
- spinner7.start("Pushing docs to GitHub...");
325
+ spinner8.start("Pushing docs to GitHub...");
326
326
  try {
327
327
  const gitignorePath = path4.join(docsDir, ".gitignore");
328
328
  if (!fs4.existsSync(gitignorePath)) {
@@ -341,9 +341,9 @@ async function createAndPushDocsRepo(params) {
341
341
  exec(`git remote add origin ${pushUrl}`, docsDir);
342
342
  exec("git push -u origin main", docsDir);
343
343
  exec("git remote set-url origin " + repoUrl, docsDir);
344
- spinner7.stop("Pushed to GitHub.");
344
+ spinner8.stop("Pushed to GitHub.");
345
345
  } catch (err) {
346
- spinner7.stop("Git push failed.");
346
+ spinner8.stop("Git push failed.");
347
347
  p3.log.error(`${err instanceof Error ? err.message : err}`);
348
348
  return null;
349
349
  }
@@ -354,8 +354,8 @@ async function createAndPushDocsRepo(params) {
354
354
  }
355
355
  async function pushUpdates(params) {
356
356
  const { token, docsDir, docsRepo } = params;
357
- const spinner7 = p3.spinner();
358
- spinner7.start("Pushing updates to docs repo...");
357
+ const spinner8 = p3.spinner();
358
+ spinner8.start("Pushing updates to docs repo...");
359
359
  try {
360
360
  if (!fs4.existsSync(path4.join(docsDir, ".git"))) {
361
361
  exec("git init", docsDir);
@@ -364,16 +364,16 @@ async function pushUpdates(params) {
364
364
  exec("git add -A", docsDir);
365
365
  try {
366
366
  exec("git diff --cached --quiet", docsDir);
367
- spinner7.stop("No changes to push.");
367
+ spinner8.stop("No changes to push.");
368
368
  return false;
369
369
  } catch {
370
370
  }
371
371
  exec('git commit -m "Update documentation"', docsDir);
372
372
  exec("git push -u origin main", docsDir);
373
- spinner7.stop("Pushed updates to docs repo.");
373
+ spinner8.stop("Pushed updates to docs repo.");
374
374
  return true;
375
375
  } catch (err) {
376
- spinner7.stop("Push failed.");
376
+ spinner8.stop("Push failed.");
377
377
  p3.log.error(`${err instanceof Error ? err.message : err}`);
378
378
  return false;
379
379
  }
@@ -394,353 +394,13 @@ function showVercelInstructions(owner, repoName) {
394
394
  );
395
395
  }
396
396
 
397
- // src/actions/setup-ci-action.ts
397
+ // src/actions/build-check.ts
398
398
  import * as p4 from "@clack/prompts";
399
- import { execSync as execSync3 } from "child_process";
400
- import fs5 from "fs";
401
- import path5 from "path";
402
- import { Octokit as Octokit3 } from "@octokit/rest";
403
- function getGitRoot() {
404
- try {
405
- return execSync3("git rev-parse --show-toplevel", {
406
- encoding: "utf-8",
407
- stdio: ["pipe", "pipe", "pipe"]
408
- }).trim();
409
- } catch {
410
- return null;
411
- }
412
- }
413
- function generateWorkflow(branch, docsRepoUrl, outputDir) {
414
- return `name: Update Documentation
415
-
416
- on:
417
- push:
418
- branches: [${branch}]
419
- workflow_dispatch:
420
-
421
- jobs:
422
- update-docs:
423
- runs-on: ubuntu-latest
424
- steps:
425
- - name: Checkout source repo
426
- uses: actions/checkout@v4
427
- with:
428
- fetch-depth: 0
429
-
430
- - name: Setup Node.js
431
- uses: actions/setup-node@v4
432
- with:
433
- node-version: 20
434
-
435
- - name: Cache analysis results
436
- uses: actions/cache@v4
437
- with:
438
- path: .autodoc-cache
439
- key: autodoc-cache-\${{ github.sha }}
440
- restore-keys: |
441
- autodoc-cache-
442
-
443
- - name: Install open-auto-doc
444
- run: npm install -g @latent-space-labs/open-auto-doc
445
-
446
- - name: Generate documentation
447
- env:
448
- ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
449
- GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
450
- run: open-auto-doc generate --incremental
451
-
452
- - name: Clone docs repo
453
- run: |
454
- git clone https://x-access-token:\${{ secrets.DOCS_DEPLOY_TOKEN }}@${docsRepoUrl.replace("https://", "")} docs-repo
455
-
456
- - name: Copy updated content
457
- run: |
458
- # Copy content and any updated config, preserving the docs repo git history
459
- rsync -av --delete \\
460
- --exclude '.git' \\
461
- --exclude 'node_modules' \\
462
- --exclude '.next' \\
463
- --exclude '.source' \\
464
- ${outputDir}/ docs-repo/
465
-
466
- - name: Push to docs repo
467
- run: |
468
- cd docs-repo
469
- git config user.name "github-actions[bot]"
470
- git config user.email "github-actions[bot]@users.noreply.github.com"
471
- git add -A
472
- # Only commit and push if there are changes
473
- if git diff --cached --quiet; then
474
- echo "No documentation changes to push."
475
- else
476
- git commit -m "Update documentation from \${{ github.repository }}@\${{ github.sha }}"
477
- git push
478
- fi
479
- `;
480
- }
481
- function generatePerRepoWorkflow(branch, repoName, docsRepoUrl) {
482
- return `name: Update Documentation
483
-
484
- on:
485
- push:
486
- branches: [${branch}]
487
- workflow_dispatch:
488
-
489
- jobs:
490
- update-docs:
491
- runs-on: ubuntu-latest
492
- steps:
493
- - name: Checkout source repo
494
- uses: actions/checkout@v4
495
- with:
496
- fetch-depth: 0
497
-
498
- - name: Setup Node.js
499
- uses: actions/setup-node@v4
500
- with:
501
- node-version: 20
502
-
503
- - name: Install open-auto-doc
504
- run: npm install -g @latent-space-labs/open-auto-doc
505
-
506
- - name: Clone docs repo
507
- run: |
508
- git clone https://x-access-token:\${{ secrets.DOCS_DEPLOY_TOKEN }}@${docsRepoUrl.replace("https://", "")} docs-site
509
-
510
- - name: Generate documentation
511
- env:
512
- ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
513
- GITHUB_TOKEN: \${{ secrets.DOCS_DEPLOY_TOKEN }}
514
- run: open-auto-doc generate --repo ${repoName} --incremental
515
-
516
- - name: Push to docs repo
517
- run: |
518
- cd docs-site
519
- git config user.name "github-actions[bot]"
520
- git config user.email "github-actions[bot]@users.noreply.github.com"
521
- git add -A
522
- if git diff --cached --quiet; then
523
- echo "No documentation changes to push."
524
- else
525
- git commit -m "Update docs from \${{ github.repository }}@\${{ github.sha }}"
526
- git pull --rebase origin main || true
527
- git push
528
- fi
529
- `;
530
- }
531
- async function createCiWorkflow(params) {
532
- const { gitRoot, docsRepoUrl, outputDir, token, config } = params;
533
- if (config && config.repos.length > 1 && token) {
534
- return createCiWorkflowsMultiRepo({
535
- token,
536
- config,
537
- docsRepoUrl
538
- });
539
- }
540
- const relativeOutputDir = path5.relative(gitRoot, path5.resolve(outputDir));
541
- p4.log.info(`Docs repo: ${docsRepoUrl}`);
542
- p4.log.info(`Output directory: ${relativeOutputDir}`);
543
- const branch = await p4.text({
544
- message: "Which branch should trigger doc updates?",
545
- initialValue: "main",
546
- validate: (v) => v.length === 0 ? "Branch name is required" : void 0
547
- });
548
- if (p4.isCancel(branch)) return null;
549
- const workflowDir = path5.join(gitRoot, ".github", "workflows");
550
- const workflowPath = path5.join(workflowDir, "update-docs.yml");
551
- fs5.mkdirSync(workflowDir, { recursive: true });
552
- fs5.writeFileSync(
553
- workflowPath,
554
- generateWorkflow(branch, docsRepoUrl, relativeOutputDir),
555
- "utf-8"
556
- );
557
- p4.log.success(`Created ${path5.relative(gitRoot, workflowPath)}`);
558
- if (token) {
559
- try {
560
- const origin = execSync3("git remote get-url origin", {
561
- cwd: gitRoot,
562
- encoding: "utf-8",
563
- stdio: ["pipe", "pipe", "pipe"]
564
- }).trim();
565
- const match = origin.match(/github\.com[:/]([^/]+\/[^/.]+?)(?:\.git)?$/);
566
- if (match) {
567
- const octokit = new Octokit3({ auth: token });
568
- await verifySecretsInteractive(octokit, [match[1]]);
569
- } else {
570
- showSecretsInstructions(false);
571
- }
572
- } catch {
573
- showSecretsInstructions(false);
574
- }
575
- } else {
576
- showSecretsInstructions(false);
577
- }
578
- return { workflowPath, branch };
579
- }
580
- async function createCiWorkflowsMultiRepo(params) {
581
- const { token, config, docsRepoUrl } = params;
582
- const octokit = new Octokit3({ auth: token });
583
- p4.log.info(`Setting up CI for ${config.repos.length} repositories`);
584
- p4.log.info(`Docs repo: ${docsRepoUrl}`);
585
- const branch = await p4.text({
586
- message: "Which branch should trigger doc updates?",
587
- initialValue: "main",
588
- validate: (v) => v.length === 0 ? "Branch name is required" : void 0
589
- });
590
- if (p4.isCancel(branch)) return null;
591
- const createdRepos = [];
592
- const workflowPath = ".github/workflows/update-docs.yml";
593
- for (const repo of config.repos) {
594
- const spinner7 = p4.spinner();
595
- spinner7.start(`Pushing workflow to ${repo.fullName}...`);
596
- try {
597
- const [owner, repoName] = repo.fullName.split("/");
598
- const workflowContent = generatePerRepoWorkflow(branch, repo.name, docsRepoUrl);
599
- const contentBase64 = Buffer.from(workflowContent).toString("base64");
600
- let existingSha;
601
- try {
602
- const { data } = await octokit.rest.repos.getContent({
603
- owner,
604
- repo: repoName,
605
- path: workflowPath
606
- });
607
- if (!Array.isArray(data) && data.type === "file") {
608
- existingSha = data.sha;
609
- }
610
- } catch {
611
- }
612
- await octokit.rest.repos.createOrUpdateFileContents({
613
- owner,
614
- repo: repoName,
615
- path: workflowPath,
616
- message: "Add auto-documentation CI workflow",
617
- content: contentBase64,
618
- ...existingSha ? { sha: existingSha } : {}
619
- });
620
- createdRepos.push(repo.fullName);
621
- spinner7.stop(`Created workflow in ${repo.fullName}`);
622
- } catch (err) {
623
- spinner7.stop(`Failed for ${repo.fullName}`);
624
- p4.log.warn(`Could not push workflow to ${repo.fullName}: ${err?.message || err}`);
625
- }
626
- }
627
- if (createdRepos.length === 0) {
628
- p4.log.error("Failed to create workflows in any repository.");
629
- return null;
630
- }
631
- p4.log.success(`Created workflows in ${createdRepos.length}/${config.repos.length} repositories`);
632
- await verifySecretsInteractive(octokit, createdRepos);
633
- return { repos: createdRepos, branch };
634
- }
635
- var REQUIRED_SECRETS = ["ANTHROPIC_API_KEY", "DOCS_DEPLOY_TOKEN"];
636
- async function checkSecret(octokit, owner, repo, secretName) {
637
- try {
638
- await octokit.rest.actions.getRepoSecret({ owner, repo, secret_name: secretName });
639
- return true;
640
- } catch {
641
- return false;
642
- }
643
- }
644
- async function verifySecretsInteractive(octokit, repos) {
645
- const shouldVerify = await p4.confirm({
646
- message: "Would you like to verify secrets now? (you can add them later)",
647
- initialValue: true
648
- });
649
- if (p4.isCancel(shouldVerify) || !shouldVerify) {
650
- showSecretsInstructions(repos.length > 1);
651
- return;
652
- }
653
- p4.note(
654
- [
655
- "Each source repo needs two Actions secrets:",
656
- "",
657
- " ANTHROPIC_API_KEY \u2014 Your Anthropic API key",
658
- " DOCS_DEPLOY_TOKEN \u2014 GitHub PAT with repo scope",
659
- "",
660
- "To create the PAT:",
661
- " 1. Go to https://github.com/settings/tokens",
662
- " 2. Generate new token (classic) with 'repo' scope",
663
- " 3. Copy the token and add it as DOCS_DEPLOY_TOKEN"
664
- ].join("\n"),
665
- "Required GitHub Secrets"
666
- );
667
- const summary = {};
668
- for (const fullName of repos) {
669
- const [owner, repoName] = fullName.split("/");
670
- p4.log.step(`Add secrets to ${fullName}`);
671
- p4.log.message(` https://github.com/${fullName}/settings/secrets/actions`);
672
- let allFound = false;
673
- while (!allFound) {
674
- await p4.text({
675
- message: `Press enter when you've added the secrets to ${fullName}...`,
676
- defaultValue: "",
677
- placeholder: ""
678
- });
679
- const results = {};
680
- for (const secret of REQUIRED_SECRETS) {
681
- results[secret] = await checkSecret(octokit, owner, repoName, secret);
682
- }
683
- for (const secret of REQUIRED_SECRETS) {
684
- if (results[secret]) {
685
- p4.log.success(`${secret} \u2014 found`);
686
- } else {
687
- p4.log.warn(`${secret} \u2014 not found`);
688
- }
689
- }
690
- summary[fullName] = results;
691
- allFound = REQUIRED_SECRETS.every((s) => results[s]);
692
- if (!allFound) {
693
- const retry = await p4.confirm({
694
- message: "Some secrets are missing. Would you like to try again?",
695
- initialValue: true
696
- });
697
- if (p4.isCancel(retry) || !retry) break;
698
- }
699
- }
700
- }
701
- const lines = [];
702
- let allGreen = true;
703
- for (const fullName of repos) {
704
- const parts = REQUIRED_SECRETS.map((s) => {
705
- const ok = summary[fullName]?.[s] ?? false;
706
- if (!ok) allGreen = false;
707
- return ok ? `\u2713 ${s}` : `\u2717 ${s}`;
708
- });
709
- lines.push(`${fullName} \u2014 ${parts.join(" ")}`);
710
- }
711
- if (allGreen) {
712
- p4.note(lines.join("\n"), "All secrets verified!");
713
- } else {
714
- p4.note(lines.join("\n"), "Secret status");
715
- p4.log.warn("Some secrets are still missing \u2014 workflows will fail until they are added.");
716
- }
717
- }
718
- function showSecretsInstructions(multiRepo = false) {
719
- const repoNote = multiRepo ? "Add these secrets to EACH source repository:" : "Add these secrets to your GitHub repository:";
720
- p4.note(
721
- [
722
- repoNote,
723
- "(Settings \u2192 Secrets and variables \u2192 Actions \u2192 New repository secret)",
724
- "",
725
- " ANTHROPIC_API_KEY \u2014 Your Anthropic API key",
726
- " DOCS_DEPLOY_TOKEN \u2014 GitHub PAT with repo scope",
727
- " (needed to push to the docs repo)",
728
- "",
729
- "To create the PAT:",
730
- " 1. Go to https://github.com/settings/tokens",
731
- " 2. Generate new token (classic) with 'repo' scope",
732
- " 3. Copy the token and add it as DOCS_DEPLOY_TOKEN",
733
- "",
734
- "Note: GITHUB_TOKEN is automatically provided by GitHub Actions",
735
- " (used for reading the source repo during analysis)."
736
- ].join("\n"),
737
- "Required GitHub Secrets"
738
- );
739
- }
399
+ import { execSync as execSync4 } from "child_process";
740
400
 
741
401
  // ../analyzer/dist/index.js
742
- import fs6 from "fs";
743
- import path6 from "path";
402
+ import fs5 from "fs";
403
+ import path5 from "path";
744
404
  import ignore from "ignore";
745
405
  import fs22 from "fs";
746
406
  import path22 from "path";
@@ -751,9 +411,9 @@ import path42 from "path";
751
411
  import { query } from "@anthropic-ai/claude-agent-sdk";
752
412
  import fs52 from "fs";
753
413
  import path52 from "path";
754
- import { execSync as execSync4 } from "child_process";
755
- import fs62 from "fs";
756
- import path62 from "path";
414
+ import { execSync as execSync3 } from "child_process";
415
+ import fs6 from "fs";
416
+ import path6 from "path";
757
417
  var DEFAULT_IGNORES = [
758
418
  "node_modules",
759
419
  ".git",
@@ -775,26 +435,26 @@ function buildFileTree(rootPath, maxDepth = 6) {
775
435
  const ig = loadGitignore(rootPath);
776
436
  const flatFiles = [];
777
437
  function walk(dirPath, depth) {
778
- const name = path6.basename(dirPath);
779
- const node = { path: path6.relative(rootPath, dirPath) || ".", name, type: "directory", children: [] };
438
+ const name = path5.basename(dirPath);
439
+ const node = { path: path5.relative(rootPath, dirPath) || ".", name, type: "directory", children: [] };
780
440
  if (depth > maxDepth) return node;
781
441
  let entries;
782
442
  try {
783
- entries = fs6.readdirSync(dirPath, { withFileTypes: true });
443
+ entries = fs5.readdirSync(dirPath, { withFileTypes: true });
784
444
  } catch {
785
445
  return node;
786
446
  }
787
447
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
788
- const rel = path6.relative(rootPath, path6.join(dirPath, entry.name));
448
+ const rel = path5.relative(rootPath, path5.join(dirPath, entry.name));
789
449
  if (ig.ignores(rel) || ig.ignores(rel + "/")) continue;
790
450
  if (entry.isDirectory()) {
791
- const child = walk(path6.join(dirPath, entry.name), depth + 1);
451
+ const child = walk(path5.join(dirPath, entry.name), depth + 1);
792
452
  node.children.push(child);
793
453
  } else if (entry.isFile()) {
794
- const ext = path6.extname(entry.name).slice(1);
454
+ const ext = path5.extname(entry.name).slice(1);
795
455
  let size;
796
456
  try {
797
- size = fs6.statSync(path6.join(dirPath, entry.name)).size;
457
+ size = fs5.statSync(path5.join(dirPath, entry.name)).size;
798
458
  } catch {
799
459
  }
800
460
  flatFiles.push(rel);
@@ -815,9 +475,9 @@ function buildFileTree(rootPath, maxDepth = 6) {
815
475
  function loadGitignore(rootPath) {
816
476
  const ig = ignore.default();
817
477
  ig.add(DEFAULT_IGNORES);
818
- const gitignorePath = path6.join(rootPath, ".gitignore");
819
- if (fs6.existsSync(gitignorePath)) {
820
- const content = fs6.readFileSync(gitignorePath, "utf-8");
478
+ const gitignorePath = path5.join(rootPath, ".gitignore");
479
+ if (fs5.existsSync(gitignorePath)) {
480
+ const content = fs5.readFileSync(gitignorePath, "utf-8");
821
481
  ig.add(content);
822
482
  }
823
483
  return ig;
@@ -845,7 +505,7 @@ function detectLanguages(flatFiles) {
845
505
  };
846
506
  const langs = /* @__PURE__ */ new Set();
847
507
  for (const file of flatFiles) {
848
- const ext = path6.extname(file).slice(1);
508
+ const ext = path5.extname(file).slice(1);
849
509
  if (extMap[ext]) langs.add(extMap[ext]);
850
510
  }
851
511
  return Array.from(langs);
@@ -865,7 +525,7 @@ function detectEntryFiles(flatFiles) {
865
525
  /^app\/page\.tsx$/,
866
526
  /^pages\/index\.\w+$/
867
527
  ];
868
- return flatFiles.filter((f) => entryPatterns.some((p11) => p11.test(f)));
528
+ return flatFiles.filter((f) => entryPatterns.some((p12) => p12.test(f)));
869
529
  }
870
530
  var DEP_FILES = [
871
531
  {
@@ -1731,7 +1391,7 @@ var SECTION_PATTERNS = {
1731
1391
  };
1732
1392
  function computeDiff(repoPath, fromSha) {
1733
1393
  try {
1734
- const output = execSync4(`git diff --name-status ${fromSha}..HEAD`, {
1394
+ const output = execSync3(`git diff --name-status ${fromSha}..HEAD`, {
1735
1395
  cwd: repoPath,
1736
1396
  encoding: "utf-8",
1737
1397
  stdio: ["pipe", "pipe", "pipe"]
@@ -1766,7 +1426,7 @@ function classifyChanges(entries, staticAnalysis) {
1766
1426
  const affected = /* @__PURE__ */ new Set();
1767
1427
  for (const entry of entries) {
1768
1428
  for (const [section, patterns] of Object.entries(SECTION_PATTERNS)) {
1769
- if (patterns.some((p11) => p11.test(entry.filePath))) {
1429
+ if (patterns.some((p12) => p12.test(entry.filePath))) {
1770
1430
  affected.add(section);
1771
1431
  }
1772
1432
  }
@@ -1784,7 +1444,7 @@ function classifyChanges(entries, staticAnalysis) {
1784
1444
  };
1785
1445
  }
1786
1446
  function getHeadSha(repoPath) {
1787
- return execSync4("git rev-parse HEAD", {
1447
+ return execSync3("git rev-parse HEAD", {
1788
1448
  cwd: repoPath,
1789
1449
  encoding: "utf-8"
1790
1450
  }).trim();
@@ -2130,40 +1790,503 @@ Do NOT use file tools \u2014 all information is provided above.`,
2130
1790
  maxTurns: 5
2131
1791
  });
2132
1792
  }
1793
+ var fixerOutputSchema = {
1794
+ type: "object",
1795
+ properties: {
1796
+ fixed: { type: "boolean" },
1797
+ filesChanged: {
1798
+ type: "array",
1799
+ items: { type: "string" }
1800
+ },
1801
+ summary: { type: "string" }
1802
+ },
1803
+ required: ["fixed", "filesChanged", "summary"]
1804
+ };
1805
+ var SYSTEM_PROMPT = `You are an MDX build-error fixer for Fumadocs documentation sites.
1806
+
1807
+ You will receive Next.js / Fumadocs build error output. Your job is to find and fix the broken MDX files so the build succeeds.
1808
+
1809
+ ## Common errors you must handle
1810
+
1811
+ 1. **Unescaped JSX characters** \u2014 \`{\`, \`}\`, \`<\`, \`>\` outside of code blocks/fences must be escaped as \`\\{\`, \`\\}\`, \`&lt;\`, \`&gt;\` (or wrapped in backticks).
1812
+ 2. **Invalid Shiki language identifiers** \u2014 Code fences with unsupported language tags (e.g. \`\`\`env\`\`\`, \`\`\`conf\`\`\`, \`\`\`plaintext\`\`\`) should be changed to a supported language or removed (use \`\`\`text\`\`\` or plain \`\`\`\`\`\` as fallback).
1813
+ 3. **Malformed Mermaid diagrams** \u2014 Syntax errors inside \`\`\`mermaid\`\`\` blocks (unclosed quotes, invalid node IDs, missing arrows).
1814
+ 4. **Unclosed code fences** \u2014 Missing closing \`\`\`\`\`\` causing the rest of the file to be parsed as code.
1815
+ 5. **Invalid frontmatter** \u2014 Malformed YAML in \`---\` blocks (bad indentation, unquoted special characters).
1816
+ 6. **HTML comments** \u2014 \`<!-- -->\` is not valid in MDX; remove or convert to JSX comments \`{/* */}\`.
1817
+ 7. **Unescaped pipes in tables** \u2014 Pipes inside table cells that break table parsing.
1818
+ 8. **Import/export statements** \u2014 Invalid or unnecessary import/export statements in MDX files.
1819
+
1820
+ ## Rules
1821
+
1822
+ - ONLY edit files inside \`content/docs/\`. Never touch other project files.
1823
+ - Read the error output carefully to identify the exact file(s) and line(s).
1824
+ - Use Read to examine the broken file, then Edit to fix it.
1825
+ - After fixing, set \`fixed: true\` and list all changed file paths in \`filesChanged\`.
1826
+ - If you cannot identify or fix the error, set \`fixed: false\` and explain why in \`summary\`.
1827
+ - Be surgical: fix only what's broken, don't rewrite entire files.`;
1828
+ async function fixMdxBuildErrors(docsDir, buildErrors, apiKey, model, onAgentMessage) {
1829
+ return runAgent({
1830
+ onAgentMessage,
1831
+ systemPrompt: SYSTEM_PROMPT,
1832
+ prompt: `The following build errors occurred when building this Fumadocs documentation site.
1833
+ Diagnose and fix the MDX files causing these errors.
1834
+
1835
+ ## Build Error Output
1836
+ \`\`\`
1837
+ ${buildErrors}
1838
+ \`\`\`
1839
+
1840
+ Read the failing files, identify the issues, and use Edit to fix them.
1841
+ Only edit files inside content/docs/.`,
1842
+ cwd: docsDir,
1843
+ apiKey,
1844
+ model,
1845
+ outputSchema: fixerOutputSchema,
1846
+ allowedTools: ["Read", "Glob", "Grep", "Edit"],
1847
+ maxTurns: 20,
1848
+ retryOnMaxTurns: false
1849
+ });
1850
+ }
2133
1851
  var CACHE_VERSION = 2;
2134
1852
  function slugify(name) {
2135
1853
  return name.replace(/[^a-zA-Z0-9_-]/g, "_");
2136
1854
  }
2137
- function cacheFilePath(cacheDir, repoSlug) {
2138
- return path62.join(cacheDir, `${slugify(repoSlug)}-analysis.json`);
1855
+ function cacheFilePath(cacheDir, repoSlug) {
1856
+ return path6.join(cacheDir, `${slugify(repoSlug)}-analysis.json`);
1857
+ }
1858
+ function saveCache(cacheDir, repoSlug, commitSha, result) {
1859
+ fs6.mkdirSync(cacheDir, { recursive: true });
1860
+ const cache = {
1861
+ version: CACHE_VERSION,
1862
+ commitSha,
1863
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1864
+ result
1865
+ };
1866
+ fs6.writeFileSync(cacheFilePath(cacheDir, repoSlug), JSON.stringify(cache), "utf-8");
1867
+ }
1868
+ function loadCache(cacheDir, repoSlug) {
1869
+ const filePath = cacheFilePath(cacheDir, repoSlug);
1870
+ if (!fs6.existsSync(filePath)) return null;
1871
+ try {
1872
+ const raw = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
1873
+ if (raw.version !== CACHE_VERSION) return null;
1874
+ if (!raw.commitSha || !raw.result) return null;
1875
+ return raw;
1876
+ } catch {
1877
+ return null;
1878
+ }
1879
+ }
1880
+
1881
+ // src/actions/build-check.ts
1882
+ function runBuild(docsDir) {
1883
+ try {
1884
+ execSync4("npx fumadocs-mdx", { cwd: docsDir, stdio: "pipe", timeout: 6e4 });
1885
+ execSync4("npm run build", { cwd: docsDir, stdio: "pipe", timeout: 3e5 });
1886
+ return { success: true, output: "" };
1887
+ } catch (err) {
1888
+ const error = err;
1889
+ const stdout = error.stdout?.toString() || "";
1890
+ const stderr = error.stderr?.toString() || "";
1891
+ const output = `${stdout}
1892
+ ${stderr}`.trim();
1893
+ return { success: false, output };
1894
+ }
1895
+ }
1896
+ function truncateErrors(output, max = 8e3) {
1897
+ if (output.length <= max) return output;
1898
+ return output.slice(0, max) + "\n\n... (truncated)";
1899
+ }
1900
+ async function runBuildCheck(options) {
1901
+ const { docsDir, apiKey, model, maxAttempts = 3 } = options;
1902
+ const spinner8 = p4.spinner();
1903
+ spinner8.start("Verifying documentation build...");
1904
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1905
+ const { success, output } = runBuild(docsDir);
1906
+ if (success) {
1907
+ spinner8.stop("Documentation build verified");
1908
+ return { success: true, attempts: attempt };
1909
+ }
1910
+ if (attempt >= maxAttempts) {
1911
+ spinner8.stop("Build check failed after all attempts");
1912
+ p4.log.warn(
1913
+ "Documentation build has errors that could not be auto-fixed.\nYour docs site may still work \u2014 some errors are non-fatal.\nYou can fix remaining issues manually and run `npm run build` in the docs directory."
1914
+ );
1915
+ return { success: false, attempts: attempt, lastErrors: output };
1916
+ }
1917
+ spinner8.message(`Build errors detected \u2014 AI is diagnosing and fixing (attempt ${attempt}/${maxAttempts})...`);
1918
+ try {
1919
+ const result = await fixMdxBuildErrors(
1920
+ docsDir,
1921
+ truncateErrors(output),
1922
+ apiKey,
1923
+ model,
1924
+ (text4) => spinner8.message(text4)
1925
+ );
1926
+ if (!result.fixed) {
1927
+ spinner8.stop("AI fixer could not resolve the build errors");
1928
+ p4.log.warn(result.summary);
1929
+ return { success: false, attempts: attempt, lastErrors: output };
1930
+ }
1931
+ p4.log.info(`Fixed ${result.filesChanged.length} file(s): ${result.summary}`);
1932
+ spinner8.start("Re-verifying build...");
1933
+ } catch (err) {
1934
+ spinner8.stop("AI fixer encountered an error");
1935
+ p4.log.warn(`Fixer error: ${err instanceof Error ? err.message : err}`);
1936
+ return { success: false, attempts: attempt, lastErrors: output };
1937
+ }
1938
+ }
1939
+ return { success: false, attempts: maxAttempts };
1940
+ }
1941
+
1942
+ // src/actions/setup-ci-action.ts
1943
+ import * as p5 from "@clack/prompts";
1944
+ import { execSync as execSync5 } from "child_process";
1945
+ import fs7 from "fs";
1946
+ import path7 from "path";
1947
+ import { Octokit as Octokit3 } from "@octokit/rest";
1948
+ function getGitRoot() {
1949
+ try {
1950
+ return execSync5("git rev-parse --show-toplevel", {
1951
+ encoding: "utf-8",
1952
+ stdio: ["pipe", "pipe", "pipe"]
1953
+ }).trim();
1954
+ } catch {
1955
+ return null;
1956
+ }
1957
+ }
1958
+ function generateWorkflow(branch, docsRepoUrl, outputDir) {
1959
+ return `name: Update Documentation
1960
+
1961
+ on:
1962
+ push:
1963
+ branches: [${branch}]
1964
+ workflow_dispatch:
1965
+
1966
+ jobs:
1967
+ update-docs:
1968
+ runs-on: ubuntu-latest
1969
+ steps:
1970
+ - name: Checkout source repo
1971
+ uses: actions/checkout@v4
1972
+ with:
1973
+ fetch-depth: 0
1974
+
1975
+ - name: Setup Node.js
1976
+ uses: actions/setup-node@v4
1977
+ with:
1978
+ node-version: 20
1979
+
1980
+ - name: Cache analysis results
1981
+ uses: actions/cache@v4
1982
+ with:
1983
+ path: .autodoc-cache
1984
+ key: autodoc-cache-\${{ github.sha }}
1985
+ restore-keys: |
1986
+ autodoc-cache-
1987
+
1988
+ - name: Install open-auto-doc
1989
+ run: npm install -g @latent-space-labs/open-auto-doc
1990
+
1991
+ - name: Generate documentation
1992
+ env:
1993
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
1994
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1995
+ run: open-auto-doc generate --incremental
1996
+
1997
+ - name: Clone docs repo
1998
+ run: |
1999
+ git clone https://x-access-token:\${{ secrets.DOCS_DEPLOY_TOKEN }}@${docsRepoUrl.replace("https://", "")} docs-repo
2000
+
2001
+ - name: Copy updated content
2002
+ run: |
2003
+ # Copy content and any updated config, preserving the docs repo git history
2004
+ rsync -av --delete \\
2005
+ --exclude '.git' \\
2006
+ --exclude 'node_modules' \\
2007
+ --exclude '.next' \\
2008
+ --exclude '.source' \\
2009
+ ${outputDir}/ docs-repo/
2010
+
2011
+ - name: Push to docs repo
2012
+ run: |
2013
+ cd docs-repo
2014
+ git config user.name "github-actions[bot]"
2015
+ git config user.email "github-actions[bot]@users.noreply.github.com"
2016
+ git add -A
2017
+ # Only commit and push if there are changes
2018
+ if git diff --cached --quiet; then
2019
+ echo "No documentation changes to push."
2020
+ else
2021
+ git commit -m "Update documentation from \${{ github.repository }}@\${{ github.sha }}"
2022
+ git push
2023
+ fi
2024
+ `;
2025
+ }
2026
+ function generatePerRepoWorkflow(branch, repoName, docsRepoUrl) {
2027
+ return `name: Update Documentation
2028
+
2029
+ on:
2030
+ push:
2031
+ branches: [${branch}]
2032
+ workflow_dispatch:
2033
+
2034
+ jobs:
2035
+ update-docs:
2036
+ runs-on: ubuntu-latest
2037
+ steps:
2038
+ - name: Checkout source repo
2039
+ uses: actions/checkout@v4
2040
+ with:
2041
+ fetch-depth: 0
2042
+
2043
+ - name: Setup Node.js
2044
+ uses: actions/setup-node@v4
2045
+ with:
2046
+ node-version: 20
2047
+
2048
+ - name: Install open-auto-doc
2049
+ run: npm install -g @latent-space-labs/open-auto-doc
2050
+
2051
+ - name: Clone docs repo
2052
+ run: |
2053
+ git clone https://x-access-token:\${{ secrets.DOCS_DEPLOY_TOKEN }}@${docsRepoUrl.replace("https://", "")} docs-site
2054
+
2055
+ - name: Generate documentation
2056
+ env:
2057
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
2058
+ GITHUB_TOKEN: \${{ secrets.DOCS_DEPLOY_TOKEN }}
2059
+ run: open-auto-doc generate --repo ${repoName} --incremental
2060
+
2061
+ - name: Push to docs repo
2062
+ run: |
2063
+ cd docs-site
2064
+ git config user.name "github-actions[bot]"
2065
+ git config user.email "github-actions[bot]@users.noreply.github.com"
2066
+ git add -A
2067
+ if git diff --cached --quiet; then
2068
+ echo "No documentation changes to push."
2069
+ else
2070
+ git commit -m "Update docs from \${{ github.repository }}@\${{ github.sha }}"
2071
+ git pull --rebase origin main || true
2072
+ git push
2073
+ fi
2074
+ `;
2075
+ }
2076
+ async function createCiWorkflow(params) {
2077
+ const { gitRoot, docsRepoUrl, outputDir, token, config } = params;
2078
+ if (config && config.repos.length > 1 && token) {
2079
+ return createCiWorkflowsMultiRepo({
2080
+ token,
2081
+ config,
2082
+ docsRepoUrl
2083
+ });
2084
+ }
2085
+ const relativeOutputDir = path7.relative(gitRoot, path7.resolve(outputDir));
2086
+ p5.log.info(`Docs repo: ${docsRepoUrl}`);
2087
+ p5.log.info(`Output directory: ${relativeOutputDir}`);
2088
+ const branch = await p5.text({
2089
+ message: "Which branch should trigger doc updates?",
2090
+ initialValue: "main",
2091
+ validate: (v) => v.length === 0 ? "Branch name is required" : void 0
2092
+ });
2093
+ if (p5.isCancel(branch)) return null;
2094
+ const workflowDir = path7.join(gitRoot, ".github", "workflows");
2095
+ const workflowPath = path7.join(workflowDir, "update-docs.yml");
2096
+ fs7.mkdirSync(workflowDir, { recursive: true });
2097
+ fs7.writeFileSync(
2098
+ workflowPath,
2099
+ generateWorkflow(branch, docsRepoUrl, relativeOutputDir),
2100
+ "utf-8"
2101
+ );
2102
+ p5.log.success(`Created ${path7.relative(gitRoot, workflowPath)}`);
2103
+ if (token) {
2104
+ try {
2105
+ const origin = execSync5("git remote get-url origin", {
2106
+ cwd: gitRoot,
2107
+ encoding: "utf-8",
2108
+ stdio: ["pipe", "pipe", "pipe"]
2109
+ }).trim();
2110
+ const match = origin.match(/github\.com[:/]([^/]+\/[^/.]+?)(?:\.git)?$/);
2111
+ if (match) {
2112
+ const octokit = new Octokit3({ auth: token });
2113
+ await verifySecretsInteractive(octokit, [match[1]]);
2114
+ } else {
2115
+ showSecretsInstructions(false);
2116
+ }
2117
+ } catch {
2118
+ showSecretsInstructions(false);
2119
+ }
2120
+ } else {
2121
+ showSecretsInstructions(false);
2122
+ }
2123
+ return { workflowPath, branch };
2139
2124
  }
2140
- function saveCache(cacheDir, repoSlug, commitSha, result) {
2141
- fs62.mkdirSync(cacheDir, { recursive: true });
2142
- const cache = {
2143
- version: CACHE_VERSION,
2144
- commitSha,
2145
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2146
- result
2147
- };
2148
- fs62.writeFileSync(cacheFilePath(cacheDir, repoSlug), JSON.stringify(cache), "utf-8");
2125
+ async function createCiWorkflowsMultiRepo(params) {
2126
+ const { token, config, docsRepoUrl } = params;
2127
+ const octokit = new Octokit3({ auth: token });
2128
+ p5.log.info(`Setting up CI for ${config.repos.length} repositories`);
2129
+ p5.log.info(`Docs repo: ${docsRepoUrl}`);
2130
+ const branch = await p5.text({
2131
+ message: "Which branch should trigger doc updates?",
2132
+ initialValue: "main",
2133
+ validate: (v) => v.length === 0 ? "Branch name is required" : void 0
2134
+ });
2135
+ if (p5.isCancel(branch)) return null;
2136
+ const createdRepos = [];
2137
+ const workflowPath = ".github/workflows/update-docs.yml";
2138
+ for (const repo of config.repos) {
2139
+ const spinner8 = p5.spinner();
2140
+ spinner8.start(`Pushing workflow to ${repo.fullName}...`);
2141
+ try {
2142
+ const [owner, repoName] = repo.fullName.split("/");
2143
+ const workflowContent = generatePerRepoWorkflow(branch, repo.name, docsRepoUrl);
2144
+ const contentBase64 = Buffer.from(workflowContent).toString("base64");
2145
+ let existingSha;
2146
+ try {
2147
+ const { data } = await octokit.rest.repos.getContent({
2148
+ owner,
2149
+ repo: repoName,
2150
+ path: workflowPath
2151
+ });
2152
+ if (!Array.isArray(data) && data.type === "file") {
2153
+ existingSha = data.sha;
2154
+ }
2155
+ } catch {
2156
+ }
2157
+ await octokit.rest.repos.createOrUpdateFileContents({
2158
+ owner,
2159
+ repo: repoName,
2160
+ path: workflowPath,
2161
+ message: "Add auto-documentation CI workflow",
2162
+ content: contentBase64,
2163
+ ...existingSha ? { sha: existingSha } : {}
2164
+ });
2165
+ createdRepos.push(repo.fullName);
2166
+ spinner8.stop(`Created workflow in ${repo.fullName}`);
2167
+ } catch (err) {
2168
+ spinner8.stop(`Failed for ${repo.fullName}`);
2169
+ p5.log.warn(`Could not push workflow to ${repo.fullName}: ${err?.message || err}`);
2170
+ }
2171
+ }
2172
+ if (createdRepos.length === 0) {
2173
+ p5.log.error("Failed to create workflows in any repository.");
2174
+ return null;
2175
+ }
2176
+ p5.log.success(`Created workflows in ${createdRepos.length}/${config.repos.length} repositories`);
2177
+ await verifySecretsInteractive(octokit, createdRepos);
2178
+ return { repos: createdRepos, branch };
2149
2179
  }
2150
- function loadCache(cacheDir, repoSlug) {
2151
- const filePath = cacheFilePath(cacheDir, repoSlug);
2152
- if (!fs62.existsSync(filePath)) return null;
2180
+ var REQUIRED_SECRETS = ["ANTHROPIC_API_KEY", "DOCS_DEPLOY_TOKEN"];
2181
+ async function checkSecret(octokit, owner, repo, secretName) {
2153
2182
  try {
2154
- const raw = JSON.parse(fs62.readFileSync(filePath, "utf-8"));
2155
- if (raw.version !== CACHE_VERSION) return null;
2156
- if (!raw.commitSha || !raw.result) return null;
2157
- return raw;
2183
+ await octokit.rest.actions.getRepoSecret({ owner, repo, secret_name: secretName });
2184
+ return true;
2158
2185
  } catch {
2159
- return null;
2186
+ return false;
2187
+ }
2188
+ }
2189
+ async function verifySecretsInteractive(octokit, repos) {
2190
+ const shouldVerify = await p5.confirm({
2191
+ message: "Would you like to verify secrets now? (you can add them later)",
2192
+ initialValue: true
2193
+ });
2194
+ if (p5.isCancel(shouldVerify) || !shouldVerify) {
2195
+ showSecretsInstructions(repos.length > 1);
2196
+ return;
2197
+ }
2198
+ p5.note(
2199
+ [
2200
+ "Each source repo needs two Actions secrets:",
2201
+ "",
2202
+ " ANTHROPIC_API_KEY \u2014 Your Anthropic API key",
2203
+ " DOCS_DEPLOY_TOKEN \u2014 GitHub PAT with repo scope",
2204
+ "",
2205
+ "To create the PAT:",
2206
+ " 1. Go to https://github.com/settings/tokens",
2207
+ " 2. Generate new token (classic) with 'repo' scope",
2208
+ " 3. Copy the token and add it as DOCS_DEPLOY_TOKEN"
2209
+ ].join("\n"),
2210
+ "Required GitHub Secrets"
2211
+ );
2212
+ const summary = {};
2213
+ for (const fullName of repos) {
2214
+ const [owner, repoName] = fullName.split("/");
2215
+ p5.log.step(`Add secrets to ${fullName}`);
2216
+ p5.log.message(` https://github.com/${fullName}/settings/secrets/actions`);
2217
+ let allFound = false;
2218
+ while (!allFound) {
2219
+ await p5.text({
2220
+ message: `Press enter when you've added the secrets to ${fullName}...`,
2221
+ defaultValue: "",
2222
+ placeholder: ""
2223
+ });
2224
+ const results = {};
2225
+ for (const secret of REQUIRED_SECRETS) {
2226
+ results[secret] = await checkSecret(octokit, owner, repoName, secret);
2227
+ }
2228
+ for (const secret of REQUIRED_SECRETS) {
2229
+ if (results[secret]) {
2230
+ p5.log.success(`${secret} \u2014 found`);
2231
+ } else {
2232
+ p5.log.warn(`${secret} \u2014 not found`);
2233
+ }
2234
+ }
2235
+ summary[fullName] = results;
2236
+ allFound = REQUIRED_SECRETS.every((s) => results[s]);
2237
+ if (!allFound) {
2238
+ const retry = await p5.confirm({
2239
+ message: "Some secrets are missing. Would you like to try again?",
2240
+ initialValue: true
2241
+ });
2242
+ if (p5.isCancel(retry) || !retry) break;
2243
+ }
2244
+ }
2245
+ }
2246
+ const lines = [];
2247
+ let allGreen = true;
2248
+ for (const fullName of repos) {
2249
+ const parts = REQUIRED_SECRETS.map((s) => {
2250
+ const ok = summary[fullName]?.[s] ?? false;
2251
+ if (!ok) allGreen = false;
2252
+ return ok ? `\u2713 ${s}` : `\u2717 ${s}`;
2253
+ });
2254
+ lines.push(`${fullName} \u2014 ${parts.join(" ")}`);
2255
+ }
2256
+ if (allGreen) {
2257
+ p5.note(lines.join("\n"), "All secrets verified!");
2258
+ } else {
2259
+ p5.note(lines.join("\n"), "Secret status");
2260
+ p5.log.warn("Some secrets are still missing \u2014 workflows will fail until they are added.");
2160
2261
  }
2161
2262
  }
2263
+ function showSecretsInstructions(multiRepo = false) {
2264
+ const repoNote = multiRepo ? "Add these secrets to EACH source repository:" : "Add these secrets to your GitHub repository:";
2265
+ p5.note(
2266
+ [
2267
+ repoNote,
2268
+ "(Settings \u2192 Secrets and variables \u2192 Actions \u2192 New repository secret)",
2269
+ "",
2270
+ " ANTHROPIC_API_KEY \u2014 Your Anthropic API key",
2271
+ " DOCS_DEPLOY_TOKEN \u2014 GitHub PAT with repo scope",
2272
+ " (needed to push to the docs repo)",
2273
+ "",
2274
+ "To create the PAT:",
2275
+ " 1. Go to https://github.com/settings/tokens",
2276
+ " 2. Generate new token (classic) with 'repo' scope",
2277
+ " 3. Copy the token and add it as DOCS_DEPLOY_TOKEN",
2278
+ "",
2279
+ "Note: GITHUB_TOKEN is automatically provided by GitHub Actions",
2280
+ " (used for reading the source repo during analysis)."
2281
+ ].join("\n"),
2282
+ "Required GitHub Secrets"
2283
+ );
2284
+ }
2162
2285
 
2163
2286
  // ../generator/dist/index.js
2164
- import fs7 from "fs-extra";
2165
- import path7 from "path";
2166
- import { execSync as execSync5 } from "child_process";
2287
+ import fs8 from "fs-extra";
2288
+ import path8 from "path";
2289
+ import { execSync as execSync6 } from "child_process";
2167
2290
  import fs23 from "fs-extra";
2168
2291
  import path23 from "path";
2169
2292
  import Handlebars from "handlebars";
@@ -2171,40 +2294,40 @@ import { fileURLToPath } from "url";
2171
2294
  import fs33 from "fs-extra";
2172
2295
  import path33 from "path";
2173
2296
  async function scaffoldSite(outputDir, projectName, templateDir) {
2174
- await fs7.copy(templateDir, outputDir, {
2297
+ await fs8.copy(templateDir, outputDir, {
2175
2298
  overwrite: true,
2176
2299
  filter: (src) => {
2177
- const basename = path7.basename(src);
2300
+ const basename = path8.basename(src);
2178
2301
  return basename !== "node_modules" && basename !== ".next" && basename !== ".source" && basename !== "dist" && basename !== ".turbo";
2179
2302
  }
2180
2303
  });
2181
2304
  const filesToProcess = await findTextFiles(outputDir);
2182
2305
  for (const filePath of filesToProcess) {
2183
2306
  try {
2184
- let content = await fs7.readFile(filePath, "utf-8");
2307
+ let content = await fs8.readFile(filePath, "utf-8");
2185
2308
  if (content.includes("{{projectName}}")) {
2186
2309
  content = content.replace(/\{\{projectName\}\}/g, projectName);
2187
- await fs7.writeFile(filePath, content, "utf-8");
2310
+ await fs8.writeFile(filePath, content, "utf-8");
2188
2311
  }
2189
2312
  } catch {
2190
2313
  }
2191
2314
  }
2192
- const nodeModulesPath = path7.join(outputDir, "node_modules");
2193
- if (!fs7.existsSync(nodeModulesPath)) {
2315
+ const nodeModulesPath = path8.join(outputDir, "node_modules");
2316
+ if (!fs8.existsSync(nodeModulesPath)) {
2194
2317
  try {
2195
- execSync5("npm install --ignore-scripts", {
2318
+ execSync6("npm install --ignore-scripts", {
2196
2319
  cwd: outputDir,
2197
2320
  stdio: "pipe",
2198
2321
  timeout: 12e4
2199
2322
  });
2200
2323
  } catch (err) {
2201
- const hasNodeModules = fs7.existsSync(nodeModulesPath);
2324
+ const hasNodeModules = fs8.existsSync(nodeModulesPath);
2202
2325
  if (!hasNodeModules) {
2203
2326
  throw new Error(`npm install failed: ${err instanceof Error ? err.message : err}`);
2204
2327
  }
2205
2328
  }
2206
2329
  try {
2207
- execSync5("npx fumadocs-mdx", { cwd: outputDir, stdio: "pipe", timeout: 3e4 });
2330
+ execSync6("npx fumadocs-mdx", { cwd: outputDir, stdio: "pipe", timeout: 3e4 });
2208
2331
  } catch {
2209
2332
  }
2210
2333
  }
@@ -2227,14 +2350,14 @@ async function findTextFiles(dir) {
2227
2350
  ]);
2228
2351
  const results = [];
2229
2352
  async function walk(currentDir) {
2230
- const entries = await fs7.readdir(currentDir, { withFileTypes: true });
2353
+ const entries = await fs8.readdir(currentDir, { withFileTypes: true });
2231
2354
  for (const entry of entries) {
2232
- const fullPath = path7.join(currentDir, entry.name);
2355
+ const fullPath = path8.join(currentDir, entry.name);
2233
2356
  if (entry.isDirectory()) {
2234
2357
  if (entry.name !== "node_modules" && entry.name !== ".next" && entry.name !== ".source") {
2235
2358
  await walk(fullPath);
2236
2359
  }
2237
- } else if (textExtensions.has(path7.extname(entry.name))) {
2360
+ } else if (textExtensions.has(path8.extname(entry.name))) {
2238
2361
  results.push(fullPath);
2239
2362
  }
2240
2363
  }
@@ -2666,13 +2789,215 @@ function slugify22(name) {
2666
2789
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2667
2790
  }
2668
2791
 
2792
+ // src/ui/progress-table.ts
2793
+ function isUnicodeSupported() {
2794
+ if (process.platform === "win32") {
2795
+ return Boolean(process.env.WT_SESSION) || // Windows Terminal
2796
+ Boolean(process.env.TERMINUS_SUBLIME) || process.env.ConEmuTask === "{cmd::Cmder}" || process.env.TERM_PROGRAM === "Terminus-Sublime" || process.env.TERM_PROGRAM === "vscode" || process.env.TERM === "xterm-256color" || process.env.TERM === "alacritty" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
2797
+ }
2798
+ return process.env.TERM !== "linux";
2799
+ }
2800
+ var unicode = isUnicodeSupported();
2801
+ var S_QUEUED = unicode ? "\u25CB" : "o";
2802
+ var S_DONE = unicode ? "\u2713" : "+";
2803
+ var S_FAILED = unicode ? "\u2717" : "x";
2804
+ var S_BAR = unicode ? "\u2502" : "|";
2805
+ var S_BULLET = unicode ? "\u25CF" : "*";
2806
+ var SPINNER_FRAMES = unicode ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["/", "-", "\\", "|"];
2807
+ var ESC = "\x1B[";
2808
+ var reset = `${ESC}0m`;
2809
+ var dim = (s) => `${ESC}2m${s}${reset}`;
2810
+ var green = (s) => `${ESC}32m${s}${reset}`;
2811
+ var red = (s) => `${ESC}31m${s}${reset}`;
2812
+ var magenta = (s) => `${ESC}35m${s}${reset}`;
2813
+ var cyan = (s) => `${ESC}36m${s}${reset}`;
2814
+ var bold = (s) => `${ESC}1m${s}${reset}`;
2815
+ var cursorUp = (n) => n > 0 ? `${ESC}${n}A` : "";
2816
+ var eraseLine = `${ESC}2K`;
2817
+ var hideCursor = `${ESC}?25l`;
2818
+ var showCursor = `${ESC}?25h`;
2819
+ var ANSI_RE = /\x1b\[[0-9;]*m/g;
2820
+ function stripAnsi(s) {
2821
+ return s.replace(ANSI_RE, "");
2822
+ }
2823
+ function visibleLength(s) {
2824
+ return stripAnsi(s).length;
2825
+ }
2826
+ function truncateAnsi(s, max) {
2827
+ if (visibleLength(s) <= max) return s;
2828
+ let visible = 0;
2829
+ let result = "";
2830
+ let i = 0;
2831
+ const raw = s;
2832
+ while (i < raw.length && visible < max - 1) {
2833
+ if (raw[i] === "\x1B" && raw[i + 1] === "[") {
2834
+ const end = raw.indexOf("m", i);
2835
+ if (end !== -1) {
2836
+ result += raw.slice(i, end + 1);
2837
+ i = end + 1;
2838
+ continue;
2839
+ }
2840
+ }
2841
+ result += raw[i];
2842
+ visible++;
2843
+ i++;
2844
+ }
2845
+ return result + reset + dim("\u2026");
2846
+ }
2847
+ var ProgressTable = class {
2848
+ repos;
2849
+ states;
2850
+ maxNameLen;
2851
+ lineCount = 0;
2852
+ interval = null;
2853
+ spinnerIdx = 0;
2854
+ isTTY;
2855
+ exitHandler = null;
2856
+ constructor(options) {
2857
+ this.repos = options.repos;
2858
+ this.states = /* @__PURE__ */ new Map();
2859
+ this.maxNameLen = Math.max(...options.repos.map((r) => r.length), 4);
2860
+ for (const repo of options.repos) {
2861
+ this.states.set(repo, { status: "queued", message: "", summary: "", error: "" });
2862
+ }
2863
+ this.isTTY = process.stdout.isTTY === true;
2864
+ }
2865
+ start() {
2866
+ if (this.isTTY) {
2867
+ process.stdout.write(hideCursor);
2868
+ this.exitHandler = () => process.stdout.write(showCursor);
2869
+ process.on("exit", this.exitHandler);
2870
+ this.render();
2871
+ this.interval = setInterval(() => {
2872
+ this.spinnerIdx = (this.spinnerIdx + 1) % SPINNER_FRAMES.length;
2873
+ this.render();
2874
+ }, 80);
2875
+ } else {
2876
+ const total = this.repos.length;
2877
+ process.stdout.write(`Analyzing ${total} ${total === 1 ? "repository" : "repositories"}...
2878
+ `);
2879
+ }
2880
+ }
2881
+ update(repo, patch) {
2882
+ const state = this.states.get(repo);
2883
+ if (!state) return;
2884
+ const prevStatus = state.status;
2885
+ if (patch.status !== void 0) state.status = patch.status;
2886
+ if (patch.message !== void 0) state.message = patch.message;
2887
+ if (patch.summary !== void 0) state.summary = patch.summary;
2888
+ if (patch.error !== void 0) state.error = patch.error;
2889
+ if (!this.isTTY && patch.status && patch.status !== prevStatus) {
2890
+ if (patch.status === "done") {
2891
+ process.stdout.write(` ${S_DONE} ${repo} ${state.summary}
2892
+ `);
2893
+ } else if (patch.status === "failed") {
2894
+ process.stdout.write(` ${S_FAILED} ${repo} ${state.error}
2895
+ `);
2896
+ } else if (patch.status === "active" && prevStatus === "queued") {
2897
+ process.stdout.write(` ${S_QUEUED} ${repo} Starting...
2898
+ `);
2899
+ }
2900
+ }
2901
+ }
2902
+ stop() {
2903
+ if (this.interval) {
2904
+ clearInterval(this.interval);
2905
+ this.interval = null;
2906
+ }
2907
+ if (this.isTTY) {
2908
+ this.render();
2909
+ process.stdout.write(showCursor);
2910
+ if (this.exitHandler) {
2911
+ process.removeListener("exit", this.exitHandler);
2912
+ this.exitHandler = null;
2913
+ }
2914
+ }
2915
+ }
2916
+ getSummary() {
2917
+ let done = 0;
2918
+ let failed = 0;
2919
+ for (const state of this.states.values()) {
2920
+ if (state.status === "done") done++;
2921
+ else if (state.status === "failed") failed++;
2922
+ }
2923
+ return { done, failed, total: this.repos.length };
2924
+ }
2925
+ // ── Private rendering ────────────────────────────────────────────
2926
+ render() {
2927
+ const cols = process.stdout.columns || 80;
2928
+ const lines = [];
2929
+ const { done, failed, total } = this.getSummary();
2930
+ const completed = done + failed;
2931
+ lines.push(`${dim(S_BAR)}`);
2932
+ lines.push(`${cyan(S_BULLET)} ${bold(`Analyzing ${total} ${total === 1 ? "repository" : "repositories"}`)}`);
2933
+ lines.push(`${dim(S_BAR)} ${completed}/${total} complete`);
2934
+ for (const repo of this.repos) {
2935
+ const state = this.states.get(repo);
2936
+ const paddedName = repo.padEnd(this.maxNameLen);
2937
+ let line;
2938
+ switch (state.status) {
2939
+ case "queued":
2940
+ line = `${dim(S_BAR)} ${dim(S_QUEUED)} ${dim(paddedName)} ${dim("Queued")}`;
2941
+ break;
2942
+ case "active": {
2943
+ const frame = SPINNER_FRAMES[this.spinnerIdx];
2944
+ line = `${dim(S_BAR)} ${magenta(frame)} ${paddedName} ${dim(state.message)}`;
2945
+ break;
2946
+ }
2947
+ case "done":
2948
+ line = `${dim(S_BAR)} ${green(S_DONE)} ${paddedName} ${state.summary}`;
2949
+ break;
2950
+ case "failed":
2951
+ line = `${dim(S_BAR)} ${red(S_FAILED)} ${paddedName} ${red(state.error || "Failed")}`;
2952
+ break;
2953
+ }
2954
+ if (visibleLength(line) > cols) {
2955
+ line = truncateAnsi(line, cols);
2956
+ }
2957
+ lines.push(line);
2958
+ }
2959
+ lines.push(`${dim(S_BAR)}`);
2960
+ let output = "";
2961
+ if (this.lineCount > 0) {
2962
+ output += cursorUp(this.lineCount);
2963
+ }
2964
+ for (const line of lines) {
2965
+ output += eraseLine + line + "\n";
2966
+ }
2967
+ if (this.lineCount > lines.length) {
2968
+ for (let i = 0; i < this.lineCount - lines.length; i++) {
2969
+ output += eraseLine + "\n";
2970
+ }
2971
+ output += cursorUp(this.lineCount - lines.length);
2972
+ }
2973
+ this.lineCount = lines.length;
2974
+ process.stdout.write(output);
2975
+ }
2976
+ };
2977
+ function buildRepoSummary(result) {
2978
+ const parts = [];
2979
+ if (result.apiEndpoints.length > 0) {
2980
+ parts.push(`${result.apiEndpoints.length} endpoint${result.apiEndpoints.length === 1 ? "" : "s"}`);
2981
+ }
2982
+ if (result.components.length > 0) {
2983
+ parts.push(`${result.components.length} component${result.components.length === 1 ? "" : "s"}`);
2984
+ }
2985
+ if (result.diagrams.length > 0) {
2986
+ parts.push(`${result.diagrams.length} diagram${result.diagrams.length === 1 ? "" : "s"}`);
2987
+ }
2988
+ if (result.dataModels.length > 0) {
2989
+ parts.push(`${result.dataModels.length} model${result.dataModels.length === 1 ? "" : "s"}`);
2990
+ }
2991
+ return parts.length > 0 ? parts.join(", ") : "Analysis complete";
2992
+ }
2993
+
2669
2994
  // src/commands/init.ts
2670
- var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
2995
+ var __dirname2 = path9.dirname(fileURLToPath2(import.meta.url));
2671
2996
  async function initCommand(options) {
2672
- p5.intro("open-auto-doc \u2014 AI-powered documentation generator");
2997
+ p6.intro("open-auto-doc \u2014 AI-powered documentation generator");
2673
2998
  const templateDir = resolveTemplateDir();
2674
- if (!fs8.existsSync(path8.join(templateDir, "package.json"))) {
2675
- p5.log.error(
2999
+ if (!fs9.existsSync(path9.join(templateDir, "package.json"))) {
3000
+ p6.log.error(
2676
3001
  `Site template not found at: ${templateDir}
2677
3002
  This usually means the npm package was not built correctly.
2678
3003
  Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
@@ -2681,38 +3006,38 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
2681
3006
  }
2682
3007
  let token = getGithubToken();
2683
3008
  if (!token) {
2684
- p5.log.info("Let's connect your GitHub account.");
3009
+ p6.log.info("Let's connect your GitHub account.");
2685
3010
  token = await authenticateWithGithub();
2686
3011
  setGithubToken(token);
2687
3012
  } else {
2688
- p5.log.success("Using saved GitHub credentials.");
3013
+ p6.log.success("Using saved GitHub credentials.");
2689
3014
  }
2690
3015
  const repos = await pickRepos(token);
2691
- p5.log.info(`Selected ${repos.length} ${repos.length === 1 ? "repository" : "repositories"}`);
3016
+ p6.log.info(`Selected ${repos.length} ${repos.length === 1 ? "repository" : "repositories"}`);
2692
3017
  let apiKey = getAnthropicKey();
2693
3018
  if (!apiKey) {
2694
- const keyInput = await p5.text({
3019
+ const keyInput = await p6.text({
2695
3020
  message: "Enter your Anthropic API key",
2696
3021
  placeholder: "sk-ant-...",
2697
3022
  validate: (v) => {
2698
3023
  if (!v || !v.startsWith("sk-ant-")) return "Please enter a valid Anthropic API key";
2699
3024
  }
2700
3025
  });
2701
- if (p5.isCancel(keyInput)) {
2702
- p5.cancel("Operation cancelled");
3026
+ if (p6.isCancel(keyInput)) {
3027
+ p6.cancel("Operation cancelled");
2703
3028
  process.exit(0);
2704
3029
  }
2705
3030
  apiKey = keyInput;
2706
- const saveKey = await p5.confirm({
3031
+ const saveKey = await p6.confirm({
2707
3032
  message: "Save API key for future use?"
2708
3033
  });
2709
- if (saveKey && !p5.isCancel(saveKey)) {
3034
+ if (saveKey && !p6.isCancel(saveKey)) {
2710
3035
  setAnthropicKey(apiKey);
2711
3036
  }
2712
3037
  } else {
2713
- p5.log.success("Using saved Anthropic API key.");
3038
+ p6.log.success("Using saved Anthropic API key.");
2714
3039
  }
2715
- const model = await p5.select({
3040
+ const model = await p6.select({
2716
3041
  message: "Which model should analyze your repos?",
2717
3042
  options: [
2718
3043
  { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", hint: "Fast & capable (recommended)" },
@@ -2720,12 +3045,12 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
2720
3045
  { value: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Most capable, slowest" }
2721
3046
  ]
2722
3047
  });
2723
- if (p5.isCancel(model)) {
2724
- p5.cancel("Operation cancelled");
3048
+ if (p6.isCancel(model)) {
3049
+ p6.cancel("Operation cancelled");
2725
3050
  process.exit(0);
2726
3051
  }
2727
- p5.log.info(`Using ${model}`);
2728
- const cloneSpinner = p5.spinner();
3052
+ p6.log.info(`Using ${model}`);
3053
+ const cloneSpinner = p6.spinner();
2729
3054
  cloneSpinner.start(`Cloning ${repos.length} repositories...`);
2730
3055
  const clones = [];
2731
3056
  for (const repo of repos) {
@@ -2734,25 +3059,20 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
2734
3059
  const cloned = cloneRepo(repo, token);
2735
3060
  clones.push(cloned);
2736
3061
  } catch (err) {
2737
- p5.log.warn(`Failed to clone ${repo.name}: ${err instanceof Error ? err.message : err}`);
3062
+ p6.log.warn(`Failed to clone ${repo.name}: ${err instanceof Error ? err.message : err}`);
2738
3063
  }
2739
3064
  }
2740
3065
  cloneSpinner.stop(`Cloned ${clones.length}/${repos.length} repositories`);
2741
3066
  if (clones.length === 0) {
2742
- p5.log.error("No repositories were cloned.");
3067
+ p6.log.error("No repositories were cloned.");
2743
3068
  process.exit(1);
2744
3069
  }
2745
- const analyzeSpinner = p5.spinner();
2746
- let completed = 0;
2747
3070
  const total = clones.length;
2748
- const repoStages = {};
2749
- const updateSpinner = () => {
2750
- const lines = Object.entries(repoStages).map(([name, status]) => `[${name}] ${status}`).join(" | ");
2751
- analyzeSpinner.message(`${completed}/${total} done \u2014 ${lines}`);
2752
- };
2753
- analyzeSpinner.start(`Analyzing ${total} ${total === 1 ? "repo" : "repos"} in parallel...`);
3071
+ const progressTable = new ProgressTable({ repos: clones.map((c) => c.info.name) });
3072
+ progressTable.start();
2754
3073
  const analysisPromises = clones.map(async (cloned) => {
2755
3074
  const repoName = cloned.info.name;
3075
+ progressTable.update(repoName, { status: "active", message: "Starting..." });
2756
3076
  try {
2757
3077
  const result = await analyzeRepository({
2758
3078
  repoPath: cloned.localPath,
@@ -2761,35 +3081,33 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
2761
3081
  apiKey,
2762
3082
  model,
2763
3083
  onProgress: (stage, msg) => {
2764
- repoStages[repoName] = `${stage}: ${msg}`;
2765
- updateSpinner();
3084
+ progressTable.update(repoName, { status: "active", message: `${stage}: ${msg}` });
2766
3085
  }
2767
3086
  });
2768
- completed++;
2769
- delete repoStages[repoName];
2770
- updateSpinner();
3087
+ progressTable.update(repoName, { status: "done", summary: buildRepoSummary(result) });
2771
3088
  return { repo: repoName, result };
2772
3089
  } catch (err) {
2773
- completed++;
2774
- delete repoStages[repoName];
2775
- updateSpinner();
2776
- p5.log.warn(`[${repoName}] Analysis failed: ${err instanceof Error ? err.message : err}`);
3090
+ const errMsg = err instanceof Error ? err.message : String(err);
3091
+ progressTable.update(repoName, { status: "failed", error: errMsg });
3092
+ p6.log.warn(`[${repoName}] Analysis failed: ${errMsg}`);
2777
3093
  return { repo: repoName, result: null };
2778
3094
  }
2779
3095
  });
2780
3096
  const settled = await Promise.all(analysisPromises);
3097
+ progressTable.stop();
2781
3098
  const results = settled.filter((s) => s.result !== null).map((s) => s.result);
2782
- analyzeSpinner.stop(
2783
- `Analyzed ${results.length}/${total} repositories` + (results.length > 0 ? ` \u2014 ${results.reduce((n, r) => n + r.apiEndpoints.length, 0)} endpoints, ${results.reduce((n, r) => n + r.components.length, 0)} components, ${results.reduce((n, r) => n + r.diagrams.length, 0)} diagrams` : "")
3099
+ const { done, failed } = progressTable.getSummary();
3100
+ p6.log.step(
3101
+ `Analyzed ${done}/${total} repositories` + (failed > 0 ? ` (${failed} failed)` : "") + (results.length > 0 ? ` \u2014 ${results.reduce((n, r) => n + r.apiEndpoints.length, 0)} endpoints, ${results.reduce((n, r) => n + r.components.length, 0)} components, ${results.reduce((n, r) => n + r.diagrams.length, 0)} diagrams` : "")
2784
3102
  );
2785
3103
  if (results.length === 0) {
2786
- p5.log.error("No repositories were successfully analyzed.");
3104
+ p6.log.error("No repositories were successfully analyzed.");
2787
3105
  cleanup(clones);
2788
3106
  process.exit(1);
2789
3107
  }
2790
3108
  let crossRepo;
2791
3109
  if (results.length > 1) {
2792
- const crossSpinner = p5.spinner();
3110
+ const crossSpinner = p6.spinner();
2793
3111
  crossSpinner.start("Analyzing cross-repository relationships...");
2794
3112
  try {
2795
3113
  crossRepo = await analyzeCrossRepos(results, apiKey, model, (text4) => {
@@ -2798,31 +3116,31 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
2798
3116
  crossSpinner.stop(`Cross-repo analysis complete \u2014 ${crossRepo.repoRelationships.length} relationships found`);
2799
3117
  } catch (err) {
2800
3118
  crossSpinner.stop("Cross-repo analysis failed (non-fatal)");
2801
- p5.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
3119
+ p6.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
2802
3120
  }
2803
3121
  }
2804
- const outputDir = path8.resolve(options.output || "docs-site");
3122
+ const outputDir = path9.resolve(options.output || "docs-site");
2805
3123
  const projectName = results.length === 1 ? results[0].repoName : "My Project";
2806
- const genSpinner = p5.spinner();
3124
+ const genSpinner = p6.spinner();
2807
3125
  try {
2808
3126
  genSpinner.start("Scaffolding documentation site...");
2809
3127
  await scaffoldSite(outputDir, projectName, templateDir);
2810
3128
  genSpinner.stop("Site scaffolded");
2811
3129
  } catch (err) {
2812
3130
  genSpinner.stop("Scaffold failed");
2813
- p5.log.error(`Scaffold error: ${err instanceof Error ? err.stack || err.message : err}`);
3131
+ p6.log.error(`Scaffold error: ${err instanceof Error ? err.stack || err.message : err}`);
2814
3132
  cleanup(clones);
2815
3133
  process.exit(1);
2816
3134
  }
2817
3135
  try {
2818
3136
  genSpinner.start("Writing documentation content...");
2819
- const contentDir = path8.join(outputDir, "content", "docs");
3137
+ const contentDir = path9.join(outputDir, "content", "docs");
2820
3138
  await writeContent(contentDir, results, crossRepo);
2821
3139
  await writeMeta(contentDir, results, crossRepo);
2822
3140
  genSpinner.stop("Documentation content written");
2823
3141
  } catch (err) {
2824
3142
  genSpinner.stop("Content writing failed");
2825
- p5.log.error(`Content error: ${err instanceof Error ? err.stack || err.message : err}`);
3143
+ p6.log.error(`Content error: ${err instanceof Error ? err.stack || err.message : err}`);
2826
3144
  cleanup(clones);
2827
3145
  process.exit(1);
2828
3146
  }
@@ -2840,16 +3158,21 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
2840
3158
  } catch {
2841
3159
  }
2842
3160
  cleanup(clones);
2843
- p5.log.success("Documentation generated successfully!");
2844
- const shouldDeploy = await p5.confirm({
3161
+ try {
3162
+ await runBuildCheck({ docsDir: outputDir, apiKey, model });
3163
+ } catch (err) {
3164
+ p6.log.warn(`Build check skipped: ${err instanceof Error ? err.message : err}`);
3165
+ }
3166
+ p6.log.success("Documentation generated successfully!");
3167
+ const shouldDeploy = await p6.confirm({
2845
3168
  message: "Would you like to deploy your docs to GitHub?"
2846
3169
  });
2847
- if (p5.isCancel(shouldDeploy) || !shouldDeploy) {
2848
- p5.note(
2849
- `cd ${path8.relative(process.cwd(), outputDir)} && npm run dev`,
3170
+ if (p6.isCancel(shouldDeploy) || !shouldDeploy) {
3171
+ p6.note(
3172
+ `cd ${path9.relative(process.cwd(), outputDir)} && npm run dev`,
2850
3173
  "Next steps"
2851
3174
  );
2852
- p5.outro("Done!");
3175
+ p6.outro("Done!");
2853
3176
  return;
2854
3177
  }
2855
3178
  const deployResult = await createAndPushDocsRepo({
@@ -2858,26 +3181,26 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
2858
3181
  config
2859
3182
  });
2860
3183
  if (!deployResult) {
2861
- p5.note(
2862
- `cd ${path8.relative(process.cwd(), outputDir)} && npm run dev`,
3184
+ p6.note(
3185
+ `cd ${path9.relative(process.cwd(), outputDir)} && npm run dev`,
2863
3186
  "Next steps"
2864
3187
  );
2865
- p5.outro("Done!");
3188
+ p6.outro("Done!");
2866
3189
  return;
2867
3190
  }
2868
- const shouldSetupCi = await p5.confirm({
3191
+ const shouldSetupCi = await p6.confirm({
2869
3192
  message: "Would you like to set up CI to auto-update docs on every push?"
2870
3193
  });
2871
- if (p5.isCancel(shouldSetupCi) || !shouldSetupCi) {
3194
+ if (p6.isCancel(shouldSetupCi) || !shouldSetupCi) {
2872
3195
  showVercelInstructions(deployResult.owner, deployResult.repoName);
2873
- p5.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
3196
+ p6.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
2874
3197
  return;
2875
3198
  }
2876
3199
  const gitRoot = getGitRoot();
2877
3200
  if (!gitRoot) {
2878
- p5.log.warn("Not in a git repository \u2014 skipping CI setup. Run `open-auto-doc setup-ci` from your project root later.");
3201
+ p6.log.warn("Not in a git repository \u2014 skipping CI setup. Run `open-auto-doc setup-ci` from your project root later.");
2879
3202
  showVercelInstructions(deployResult.owner, deployResult.repoName);
2880
- p5.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
3203
+ p6.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
2881
3204
  return;
2882
3205
  }
2883
3206
  const ciResult = await createCiWorkflow({
@@ -2888,24 +3211,24 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
2888
3211
  config
2889
3212
  });
2890
3213
  showVercelInstructions(deployResult.owner, deployResult.repoName);
2891
- p5.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
3214
+ p6.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
2892
3215
  }
2893
3216
  function resolveTemplateDir() {
2894
3217
  const candidates = [
2895
- path8.resolve(__dirname2, "site-template"),
3218
+ path9.resolve(__dirname2, "site-template"),
2896
3219
  // dist/site-template (npm global install)
2897
- path8.resolve(__dirname2, "../../site-template"),
3220
+ path9.resolve(__dirname2, "../../site-template"),
2898
3221
  // monorepo: packages/site-template
2899
- path8.resolve(__dirname2, "../../../site-template"),
3222
+ path9.resolve(__dirname2, "../../../site-template"),
2900
3223
  // monorepo alt
2901
- path8.resolve(__dirname2, "../../../../packages/site-template")
3224
+ path9.resolve(__dirname2, "../../../../packages/site-template")
2902
3225
  // monorepo from nested dist
2903
3226
  ];
2904
3227
  for (const candidate of candidates) {
2905
- const pkgPath = path8.join(candidate, "package.json");
2906
- if (fs8.existsSync(pkgPath)) return candidate;
3228
+ const pkgPath = path9.join(candidate, "package.json");
3229
+ if (fs9.existsSync(pkgPath)) return candidate;
2907
3230
  }
2908
- return path8.resolve(__dirname2, "site-template");
3231
+ return path9.resolve(__dirname2, "site-template");
2909
3232
  }
2910
3233
  function cleanup(clones) {
2911
3234
  for (const clone of clones) {
@@ -2914,26 +3237,26 @@ function cleanup(clones) {
2914
3237
  }
2915
3238
 
2916
3239
  // src/commands/generate.ts
2917
- import * as p6 from "@clack/prompts";
2918
- import path9 from "path";
3240
+ import * as p7 from "@clack/prompts";
3241
+ import path10 from "path";
2919
3242
  async function generateCommand(options) {
2920
- p6.intro("open-auto-doc \u2014 Regenerating documentation");
3243
+ p7.intro("open-auto-doc \u2014 Regenerating documentation");
2921
3244
  const config = loadConfig();
2922
3245
  if (!config) {
2923
- p6.log.error("No .autodocrc.json found. Run `open-auto-doc init` first.");
3246
+ p7.log.error("No .autodocrc.json found. Run `open-auto-doc init` first.");
2924
3247
  process.exit(1);
2925
3248
  }
2926
3249
  const token = getGithubToken();
2927
3250
  const apiKey = getAnthropicKey();
2928
3251
  if (!token) {
2929
- p6.log.error("Not authenticated. Run `open-auto-doc login` or set GITHUB_TOKEN env var.");
3252
+ p7.log.error("Not authenticated. Run `open-auto-doc login` or set GITHUB_TOKEN env var.");
2930
3253
  process.exit(1);
2931
3254
  }
2932
3255
  if (!apiKey) {
2933
- p6.log.error("No Anthropic API key found. Run `open-auto-doc init` or set ANTHROPIC_API_KEY env var.");
3256
+ p7.log.error("No Anthropic API key found. Run `open-auto-doc init` or set ANTHROPIC_API_KEY env var.");
2934
3257
  process.exit(1);
2935
3258
  }
2936
- const model = await p6.select({
3259
+ const model = await p7.select({
2937
3260
  message: "Which model should analyze your repos?",
2938
3261
  options: [
2939
3262
  { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", hint: "Fast & capable (recommended)" },
@@ -2941,20 +3264,20 @@ async function generateCommand(options) {
2941
3264
  { value: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Most capable, slowest" }
2942
3265
  ]
2943
3266
  });
2944
- if (p6.isCancel(model)) {
2945
- p6.cancel("Cancelled.");
3267
+ if (p7.isCancel(model)) {
3268
+ p7.cancel("Cancelled.");
2946
3269
  process.exit(0);
2947
3270
  }
2948
- p6.log.info(`Using ${model}`);
3271
+ p7.log.info(`Using ${model}`);
2949
3272
  const incremental = options.incremental && !options.force;
2950
- const cacheDir = path9.join(config.outputDir, ".autodoc-cache");
3273
+ const cacheDir = path10.join(config.outputDir, ".autodoc-cache");
2951
3274
  const targetRepoName = options.repo;
2952
3275
  let reposToAnalyze = config.repos;
2953
3276
  const cachedResults = [];
2954
3277
  if (targetRepoName) {
2955
3278
  const targetRepo = config.repos.find((r) => r.name === targetRepoName);
2956
3279
  if (!targetRepo) {
2957
- p6.log.error(`Repo "${targetRepoName}" not found in config. Available: ${config.repos.map((r) => r.name).join(", ")}`);
3280
+ p7.log.error(`Repo "${targetRepoName}" not found in config. Available: ${config.repos.map((r) => r.name).join(", ")}`);
2958
3281
  process.exit(1);
2959
3282
  }
2960
3283
  reposToAnalyze = [targetRepo];
@@ -2963,13 +3286,13 @@ async function generateCommand(options) {
2963
3286
  const cached = loadCache(cacheDir, repo.name);
2964
3287
  if (cached) {
2965
3288
  cachedResults.push(cached.result);
2966
- p6.log.info(`Using cached analysis for ${repo.name}`);
3289
+ p7.log.info(`Using cached analysis for ${repo.name}`);
2967
3290
  } else {
2968
- p6.log.warn(`No cached analysis for ${repo.name} \u2014 its docs will be stale until it pushes`);
3291
+ p7.log.warn(`No cached analysis for ${repo.name} \u2014 its docs will be stale until it pushes`);
2969
3292
  }
2970
3293
  }
2971
3294
  }
2972
- const cloneSpinner = p6.spinner();
3295
+ const cloneSpinner = p7.spinner();
2973
3296
  cloneSpinner.start(`Cloning ${reposToAnalyze.length} ${reposToAnalyze.length === 1 ? "repository" : "repositories"}...`);
2974
3297
  const clones = [];
2975
3298
  for (const repo of reposToAnalyze) {
@@ -2988,32 +3311,24 @@ async function generateCommand(options) {
2988
3311
  );
2989
3312
  clones.push(cloned);
2990
3313
  } catch (err) {
2991
- p6.log.warn(`Failed to clone ${repo.name}: ${err instanceof Error ? err.message : err}`);
3314
+ p7.log.warn(`Failed to clone ${repo.name}: ${err instanceof Error ? err.message : err}`);
2992
3315
  }
2993
3316
  }
2994
3317
  cloneSpinner.stop(`Cloned ${clones.length}/${reposToAnalyze.length} ${reposToAnalyze.length === 1 ? "repository" : "repositories"}`);
2995
3318
  if (clones.length === 0) {
2996
- p6.log.error("No repositories were cloned.");
3319
+ p7.log.error("No repositories were cloned.");
2997
3320
  process.exit(1);
2998
3321
  }
2999
- const analyzeSpinner = p6.spinner();
3000
- let completed = 0;
3001
3322
  const total = clones.length;
3002
- const repoStages = {};
3003
- const updateSpinner = () => {
3004
- const lines = Object.entries(repoStages).map(([name, status]) => `[${name}] ${status}`).join(" | ");
3005
- analyzeSpinner.message(`${completed}/${total} done \u2014 ${lines}`);
3006
- };
3007
- analyzeSpinner.start(`Analyzing ${total} ${total === 1 ? "repo" : "repos"} in parallel...`);
3323
+ const progressTable = new ProgressTable({ repos: clones.map((c) => c.info.name) });
3324
+ progressTable.start();
3008
3325
  const analysisPromises = clones.map(async (cloned) => {
3009
3326
  const repo = config.repos.find((r) => r.name === cloned.info.name);
3010
3327
  const repoName = repo.name;
3011
- const callbacks = {
3012
- onProgress: (stage, msg) => {
3013
- repoStages[repoName] = `${stage}: ${msg}`;
3014
- updateSpinner();
3015
- }
3328
+ const onProgress = (stage, msg) => {
3329
+ progressTable.update(repoName, { status: "active", message: `${stage}: ${msg}` });
3016
3330
  };
3331
+ progressTable.update(repoName, { status: "active", message: "Starting..." });
3017
3332
  try {
3018
3333
  let result;
3019
3334
  if (incremental) {
@@ -3027,7 +3342,7 @@ async function generateCommand(options) {
3027
3342
  model,
3028
3343
  previousResult: cached.result,
3029
3344
  previousCommitSha: cached.commitSha,
3030
- ...callbacks
3345
+ onProgress
3031
3346
  });
3032
3347
  } else {
3033
3348
  result = await analyzeRepository({
@@ -3036,7 +3351,7 @@ async function generateCommand(options) {
3036
3351
  repoUrl: repo.htmlUrl,
3037
3352
  apiKey,
3038
3353
  model,
3039
- ...callbacks
3354
+ onProgress
3040
3355
  });
3041
3356
  }
3042
3357
  } else {
@@ -3046,7 +3361,7 @@ async function generateCommand(options) {
3046
3361
  repoUrl: repo.htmlUrl,
3047
3362
  apiKey,
3048
3363
  model,
3049
- ...callbacks
3364
+ onProgress
3050
3365
  });
3051
3366
  }
3052
3367
  try {
@@ -3054,26 +3369,27 @@ async function generateCommand(options) {
3054
3369
  saveCache(cacheDir, repo.name, headSha, result);
3055
3370
  } catch {
3056
3371
  }
3057
- completed++;
3058
- delete repoStages[repoName];
3059
- updateSpinner();
3372
+ progressTable.update(repoName, { status: "done", summary: buildRepoSummary(result) });
3060
3373
  return { repo: repoName, result };
3061
3374
  } catch (err) {
3062
- completed++;
3063
- delete repoStages[repoName];
3064
- updateSpinner();
3065
- p6.log.warn(`[${repoName}] Analysis failed: ${err instanceof Error ? err.message : err}`);
3375
+ const errMsg = err instanceof Error ? err.message : String(err);
3376
+ progressTable.update(repoName, { status: "failed", error: errMsg });
3377
+ p7.log.warn(`[${repoName}] Analysis failed: ${errMsg}`);
3066
3378
  return { repo: repoName, result: null };
3067
3379
  }
3068
3380
  });
3069
3381
  const settled = await Promise.all(analysisPromises);
3382
+ progressTable.stop();
3070
3383
  const freshResults = settled.filter((s) => s.result !== null).map((s) => s.result);
3071
- analyzeSpinner.stop(`Analyzed ${freshResults.length}/${total} ${total === 1 ? "repository" : "repositories"}`);
3384
+ const { done: analyzedCount, failed: failedCount } = progressTable.getSummary();
3385
+ p7.log.step(
3386
+ `Analyzed ${analyzedCount}/${total} ${total === 1 ? "repository" : "repositories"}` + (failedCount > 0 ? ` (${failedCount} failed)` : "")
3387
+ );
3072
3388
  const results = [...freshResults, ...cachedResults];
3073
3389
  if (results.length > 0) {
3074
3390
  let crossRepo;
3075
3391
  if (results.length > 1) {
3076
- const crossSpinner = p6.spinner();
3392
+ const crossSpinner = p7.spinner();
3077
3393
  crossSpinner.start("Analyzing cross-repository relationships...");
3078
3394
  try {
3079
3395
  crossRepo = await analyzeCrossRepos(results, apiKey, model, (text4) => {
@@ -3082,61 +3398,66 @@ async function generateCommand(options) {
3082
3398
  crossSpinner.stop(`Cross-repo analysis complete \u2014 ${crossRepo.repoRelationships.length} relationships found`);
3083
3399
  } catch (err) {
3084
3400
  crossSpinner.stop("Cross-repo analysis failed (non-fatal)");
3085
- p6.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
3401
+ p7.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
3086
3402
  }
3087
3403
  }
3088
- const contentDir = path9.join(config.outputDir, "content", "docs");
3404
+ const contentDir = path10.join(config.outputDir, "content", "docs");
3089
3405
  await writeContent(contentDir, results, crossRepo);
3090
3406
  await writeMeta(contentDir, results, crossRepo);
3091
- p6.log.success("Documentation regenerated!");
3407
+ try {
3408
+ await runBuildCheck({ docsDir: config.outputDir, apiKey, model });
3409
+ } catch (err) {
3410
+ p7.log.warn(`Build check skipped: ${err instanceof Error ? err.message : err}`);
3411
+ }
3412
+ p7.log.success("Documentation regenerated!");
3092
3413
  }
3093
3414
  for (const clone of clones) {
3094
3415
  cleanupClone(clone);
3095
3416
  }
3096
- p6.outro("Done!");
3417
+ p7.outro("Done!");
3097
3418
  }
3098
3419
 
3099
3420
  // src/commands/deploy.ts
3100
- import * as p7 from "@clack/prompts";
3101
- import fs9 from "fs";
3102
- import path10 from "path";
3421
+ import * as p8 from "@clack/prompts";
3422
+ import fs10 from "fs";
3423
+ import path11 from "path";
3103
3424
  function resolveDocsDir(config, dirOption) {
3104
3425
  if (dirOption) {
3105
- const resolved = path10.resolve(dirOption);
3106
- if (!fs9.existsSync(resolved)) {
3107
- p7.log.error(`Directory not found: ${resolved}`);
3426
+ const resolved = path11.resolve(dirOption);
3427
+ if (!fs10.existsSync(resolved)) {
3428
+ p8.log.error(`Directory not found: ${resolved}`);
3108
3429
  process.exit(1);
3109
3430
  }
3110
3431
  return resolved;
3111
3432
  }
3112
- if (config?.outputDir && fs9.existsSync(path10.resolve(config.outputDir))) {
3113
- return path10.resolve(config.outputDir);
3433
+ if (config?.outputDir && fs10.existsSync(path11.resolve(config.outputDir))) {
3434
+ return path11.resolve(config.outputDir);
3114
3435
  }
3115
- if (fs9.existsSync(path10.resolve("docs-site"))) {
3116
- return path10.resolve("docs-site");
3436
+ if (fs10.existsSync(path11.resolve("docs-site"))) {
3437
+ return path11.resolve("docs-site");
3117
3438
  }
3118
- p7.log.error(
3439
+ p8.log.error(
3119
3440
  "Could not find docs site directory. Use --dir to specify the path, or run `open-auto-doc init` first."
3120
3441
  );
3121
3442
  process.exit(1);
3122
3443
  }
3123
3444
  async function deployCommand(options) {
3124
- p7.intro("open-auto-doc \u2014 Deploy docs to GitHub");
3445
+ p8.intro("open-auto-doc \u2014 Deploy docs to GitHub");
3125
3446
  const token = getGithubToken();
3126
3447
  if (!token) {
3127
- p7.log.error("Not authenticated. Run `open-auto-doc login` first.");
3448
+ p8.log.error("Not authenticated. Run `open-auto-doc login` first.");
3128
3449
  process.exit(1);
3129
3450
  }
3130
3451
  const config = loadConfig();
3131
3452
  const docsDir = resolveDocsDir(config, options.dir);
3132
- p7.log.info(`Docs directory: ${docsDir}`);
3453
+ p8.log.info(`Docs directory: ${docsDir}`);
3133
3454
  if (config?.docsRepo) {
3134
- p7.log.info(`Docs repo already configured: ${config.docsRepo}`);
3455
+ p8.log.info(`Docs repo already configured: ${config.docsRepo}`);
3135
3456
  const pushed = await pushUpdates({ token, docsDir, docsRepo: config.docsRepo });
3136
3457
  if (pushed) {
3137
- p7.outro("Docs updated! Vercel will auto-deploy from the push.");
3458
+ p8.outro("Docs updated! Vercel will auto-deploy from the push.");
3138
3459
  } else {
3139
- p7.outro("Docs are up to date!");
3460
+ p8.outro("Docs are up to date!");
3140
3461
  }
3141
3462
  return;
3142
3463
  }
@@ -3146,20 +3467,20 @@ async function deployCommand(options) {
3146
3467
  config: config || { repos: [], outputDir: docsDir }
3147
3468
  });
3148
3469
  if (!result) {
3149
- p7.cancel("Deploy cancelled.");
3470
+ p8.cancel("Deploy cancelled.");
3150
3471
  process.exit(0);
3151
3472
  }
3152
3473
  showVercelInstructions(result.owner, result.repoName);
3153
- p7.outro(`Docs repo: https://github.com/${result.owner}/${result.repoName}`);
3474
+ p8.outro(`Docs repo: https://github.com/${result.owner}/${result.repoName}`);
3154
3475
  }
3155
3476
 
3156
3477
  // src/commands/setup-ci.ts
3157
- import * as p8 from "@clack/prompts";
3478
+ import * as p9 from "@clack/prompts";
3158
3479
  async function setupCiCommand() {
3159
- p8.intro("open-auto-doc \u2014 CI/CD Setup");
3480
+ p9.intro("open-auto-doc \u2014 CI/CD Setup");
3160
3481
  const config = loadConfig();
3161
3482
  if (!config?.docsRepo) {
3162
- p8.log.error(
3483
+ p9.log.error(
3163
3484
  "No docs repo configured. Run `open-auto-doc deploy` first to create a docs GitHub repo."
3164
3485
  );
3165
3486
  process.exit(1);
@@ -3167,12 +3488,12 @@ async function setupCiCommand() {
3167
3488
  const token = getGithubToken();
3168
3489
  const isMultiRepo = config.repos.length > 1;
3169
3490
  if (isMultiRepo && !token) {
3170
- p8.log.error("Not authenticated. Run `open-auto-doc login` first (needed to push workflows to source repos).");
3491
+ p9.log.error("Not authenticated. Run `open-auto-doc login` first (needed to push workflows to source repos).");
3171
3492
  process.exit(1);
3172
3493
  }
3173
3494
  const gitRoot = getGitRoot();
3174
3495
  if (!isMultiRepo && !gitRoot) {
3175
- p8.log.error("Not in a git repository. Run this command from your project root.");
3496
+ p9.log.error("Not in a git repository. Run this command from your project root.");
3176
3497
  process.exit(1);
3177
3498
  }
3178
3499
  const result = await createCiWorkflow({
@@ -3183,46 +3504,46 @@ async function setupCiCommand() {
3183
3504
  config
3184
3505
  });
3185
3506
  if (!result) {
3186
- p8.cancel("Setup cancelled.");
3507
+ p9.cancel("Setup cancelled.");
3187
3508
  process.exit(0);
3188
3509
  }
3189
3510
  if ("repos" in result) {
3190
- p8.outro("Per-repo CI workflows created! Add the required secrets to each source repo.");
3511
+ p9.outro("Per-repo CI workflows created! Add the required secrets to each source repo.");
3191
3512
  } else {
3192
- p8.outro("CI/CD workflow is ready! Commit and push to activate.");
3513
+ p9.outro("CI/CD workflow is ready! Commit and push to activate.");
3193
3514
  }
3194
3515
  }
3195
3516
 
3196
3517
  // src/commands/login.ts
3197
- import * as p9 from "@clack/prompts";
3518
+ import * as p10 from "@clack/prompts";
3198
3519
  async function loginCommand() {
3199
- p9.intro("open-auto-doc \u2014 GitHub Login");
3520
+ p10.intro("open-auto-doc \u2014 GitHub Login");
3200
3521
  const existing = getGithubToken();
3201
3522
  if (existing) {
3202
- const overwrite = await p9.confirm({
3523
+ const overwrite = await p10.confirm({
3203
3524
  message: "You're already logged in. Re-authenticate?"
3204
3525
  });
3205
- if (!overwrite || p9.isCancel(overwrite)) {
3206
- p9.cancel("Keeping existing credentials");
3526
+ if (!overwrite || p10.isCancel(overwrite)) {
3527
+ p10.cancel("Keeping existing credentials");
3207
3528
  return;
3208
3529
  }
3209
3530
  }
3210
3531
  const token = await authenticateWithGithub();
3211
3532
  setGithubToken(token);
3212
- p9.outro("Logged in successfully!");
3533
+ p10.outro("Logged in successfully!");
3213
3534
  }
3214
3535
 
3215
3536
  // src/commands/logout.ts
3216
- import * as p10 from "@clack/prompts";
3537
+ import * as p11 from "@clack/prompts";
3217
3538
  async function logoutCommand() {
3218
- p10.intro("open-auto-doc \u2014 Logout");
3539
+ p11.intro("open-auto-doc \u2014 Logout");
3219
3540
  clearAll();
3220
- p10.outro("Credentials cleared. You've been logged out.");
3541
+ p11.outro("Credentials cleared. You've been logged out.");
3221
3542
  }
3222
3543
 
3223
3544
  // src/index.ts
3224
3545
  var program = new Command();
3225
- program.name("open-auto-doc").description("Auto-generate beautiful documentation websites from GitHub repositories using AI").version("0.3.3");
3546
+ program.name("open-auto-doc").description("Auto-generate beautiful documentation websites from GitHub repositories using AI").version("0.3.5");
3226
3547
  program.command("init", { isDefault: true }).description("Initialize and generate documentation for your repositories").option("-o, --output <dir>", "Output directory", "docs-site").action(initCommand);
3227
3548
  program.command("generate").description("Regenerate documentation using existing configuration").option("--incremental", "Only re-analyze changed files (uses cached results)").option("--force", "Force full regeneration (ignore cache)").option("--repo <name>", "Only analyze this repo (uses cache for others)").action(generateCommand);
3228
3549
  program.command("deploy").description("Create a GitHub repo for docs and push (connect to Vercel for auto-deploy)").option("-d, --dir <path>", "Docs site directory").action(deployCommand);