@latent-space-labs/open-auto-doc 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -555,6 +555,26 @@ async function createCiWorkflow(params) {
555
555
  "utf-8"
556
556
  );
557
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
+ }
558
578
  return { workflowPath, branch };
559
579
  }
560
580
  async function createCiWorkflowsMultiRepo(params) {
@@ -609,8 +629,92 @@ async function createCiWorkflowsMultiRepo(params) {
609
629
  return null;
610
630
  }
611
631
  p4.log.success(`Created workflows in ${createdRepos.length}/${config.repos.length} repositories`);
632
+ await verifySecretsInteractive(octokit, createdRepos);
612
633
  return { repos: createdRepos, branch };
613
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
+ }
614
718
  function showSecretsInstructions(multiRepo = false) {
615
719
  const repoNote = multiRepo ? "Add these secrets to EACH source repository:" : "Add these secrets to your GitHub repository:";
616
720
  p4.note(
@@ -2562,10 +2666,221 @@ function slugify22(name) {
2562
2666
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2563
2667
  }
2564
2668
 
2669
+ // src/ui/progress-table.ts
2670
+ function isUnicodeSupported() {
2671
+ if (process.platform === "win32") {
2672
+ return Boolean(process.env.WT_SESSION) || // Windows Terminal
2673
+ 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";
2674
+ }
2675
+ return process.env.TERM !== "linux";
2676
+ }
2677
+ var unicode = isUnicodeSupported();
2678
+ var S_QUEUED = unicode ? "\u25CB" : "o";
2679
+ var S_DONE = unicode ? "\u2713" : "+";
2680
+ var S_FAILED = unicode ? "\u2717" : "x";
2681
+ var S_BAR = unicode ? "\u2502" : "|";
2682
+ var S_BULLET = unicode ? "\u25CF" : "*";
2683
+ var SPINNER_FRAMES = unicode ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["/", "-", "\\", "|"];
2684
+ var ESC = "\x1B[";
2685
+ var reset = `${ESC}0m`;
2686
+ var dim = (s) => `${ESC}2m${s}${reset}`;
2687
+ var green = (s) => `${ESC}32m${s}${reset}`;
2688
+ var red = (s) => `${ESC}31m${s}${reset}`;
2689
+ var magenta = (s) => `${ESC}35m${s}${reset}`;
2690
+ var cyan = (s) => `${ESC}36m${s}${reset}`;
2691
+ var bold = (s) => `${ESC}1m${s}${reset}`;
2692
+ var cursorUp = (n) => n > 0 ? `${ESC}${n}A` : "";
2693
+ var eraseLine = `${ESC}2K`;
2694
+ var hideCursor = `${ESC}?25l`;
2695
+ var showCursor = `${ESC}?25h`;
2696
+ var ANSI_RE = /\x1b\[[0-9;]*m/g;
2697
+ function stripAnsi(s) {
2698
+ return s.replace(ANSI_RE, "");
2699
+ }
2700
+ function visibleLength(s) {
2701
+ return stripAnsi(s).length;
2702
+ }
2703
+ function truncateAnsi(s, max) {
2704
+ if (visibleLength(s) <= max) return s;
2705
+ let visible = 0;
2706
+ let result = "";
2707
+ let i = 0;
2708
+ const raw = s;
2709
+ while (i < raw.length && visible < max - 1) {
2710
+ if (raw[i] === "\x1B" && raw[i + 1] === "[") {
2711
+ const end = raw.indexOf("m", i);
2712
+ if (end !== -1) {
2713
+ result += raw.slice(i, end + 1);
2714
+ i = end + 1;
2715
+ continue;
2716
+ }
2717
+ }
2718
+ result += raw[i];
2719
+ visible++;
2720
+ i++;
2721
+ }
2722
+ return result + reset + dim("\u2026");
2723
+ }
2724
+ var ProgressTable = class {
2725
+ repos;
2726
+ states;
2727
+ maxNameLen;
2728
+ lineCount = 0;
2729
+ interval = null;
2730
+ spinnerIdx = 0;
2731
+ isTTY;
2732
+ exitHandler = null;
2733
+ constructor(options) {
2734
+ this.repos = options.repos;
2735
+ this.states = /* @__PURE__ */ new Map();
2736
+ this.maxNameLen = Math.max(...options.repos.map((r) => r.length), 4);
2737
+ for (const repo of options.repos) {
2738
+ this.states.set(repo, { status: "queued", message: "", summary: "", error: "" });
2739
+ }
2740
+ this.isTTY = process.stdout.isTTY === true;
2741
+ }
2742
+ start() {
2743
+ if (this.isTTY) {
2744
+ process.stdout.write(hideCursor);
2745
+ this.exitHandler = () => process.stdout.write(showCursor);
2746
+ process.on("exit", this.exitHandler);
2747
+ this.render();
2748
+ this.interval = setInterval(() => {
2749
+ this.spinnerIdx = (this.spinnerIdx + 1) % SPINNER_FRAMES.length;
2750
+ this.render();
2751
+ }, 80);
2752
+ } else {
2753
+ const total = this.repos.length;
2754
+ process.stdout.write(`Analyzing ${total} ${total === 1 ? "repository" : "repositories"}...
2755
+ `);
2756
+ }
2757
+ }
2758
+ update(repo, patch) {
2759
+ const state = this.states.get(repo);
2760
+ if (!state) return;
2761
+ const prevStatus = state.status;
2762
+ if (patch.status !== void 0) state.status = patch.status;
2763
+ if (patch.message !== void 0) state.message = patch.message;
2764
+ if (patch.summary !== void 0) state.summary = patch.summary;
2765
+ if (patch.error !== void 0) state.error = patch.error;
2766
+ if (!this.isTTY && patch.status && patch.status !== prevStatus) {
2767
+ if (patch.status === "done") {
2768
+ process.stdout.write(` ${S_DONE} ${repo} ${state.summary}
2769
+ `);
2770
+ } else if (patch.status === "failed") {
2771
+ process.stdout.write(` ${S_FAILED} ${repo} ${state.error}
2772
+ `);
2773
+ } else if (patch.status === "active" && prevStatus === "queued") {
2774
+ process.stdout.write(` ${S_QUEUED} ${repo} Starting...
2775
+ `);
2776
+ }
2777
+ }
2778
+ }
2779
+ stop() {
2780
+ if (this.interval) {
2781
+ clearInterval(this.interval);
2782
+ this.interval = null;
2783
+ }
2784
+ if (this.isTTY) {
2785
+ this.render();
2786
+ process.stdout.write(showCursor);
2787
+ if (this.exitHandler) {
2788
+ process.removeListener("exit", this.exitHandler);
2789
+ this.exitHandler = null;
2790
+ }
2791
+ }
2792
+ }
2793
+ getSummary() {
2794
+ let done = 0;
2795
+ let failed = 0;
2796
+ for (const state of this.states.values()) {
2797
+ if (state.status === "done") done++;
2798
+ else if (state.status === "failed") failed++;
2799
+ }
2800
+ return { done, failed, total: this.repos.length };
2801
+ }
2802
+ // ── Private rendering ────────────────────────────────────────────
2803
+ render() {
2804
+ const cols = process.stdout.columns || 80;
2805
+ const lines = [];
2806
+ const { done, failed, total } = this.getSummary();
2807
+ const completed = done + failed;
2808
+ lines.push(`${dim(S_BAR)}`);
2809
+ lines.push(`${cyan(S_BULLET)} ${bold(`Analyzing ${total} ${total === 1 ? "repository" : "repositories"}`)}`);
2810
+ lines.push(`${dim(S_BAR)} ${completed}/${total} complete`);
2811
+ for (const repo of this.repos) {
2812
+ const state = this.states.get(repo);
2813
+ const paddedName = repo.padEnd(this.maxNameLen);
2814
+ let line;
2815
+ switch (state.status) {
2816
+ case "queued":
2817
+ line = `${dim(S_BAR)} ${dim(S_QUEUED)} ${dim(paddedName)} ${dim("Queued")}`;
2818
+ break;
2819
+ case "active": {
2820
+ const frame = SPINNER_FRAMES[this.spinnerIdx];
2821
+ line = `${dim(S_BAR)} ${magenta(frame)} ${paddedName} ${dim(state.message)}`;
2822
+ break;
2823
+ }
2824
+ case "done":
2825
+ line = `${dim(S_BAR)} ${green(S_DONE)} ${paddedName} ${state.summary}`;
2826
+ break;
2827
+ case "failed":
2828
+ line = `${dim(S_BAR)} ${red(S_FAILED)} ${paddedName} ${red(state.error || "Failed")}`;
2829
+ break;
2830
+ }
2831
+ if (visibleLength(line) > cols) {
2832
+ line = truncateAnsi(line, cols);
2833
+ }
2834
+ lines.push(line);
2835
+ }
2836
+ lines.push(`${dim(S_BAR)}`);
2837
+ let output = "";
2838
+ if (this.lineCount > 0) {
2839
+ output += cursorUp(this.lineCount);
2840
+ }
2841
+ for (const line of lines) {
2842
+ output += eraseLine + line + "\n";
2843
+ }
2844
+ if (this.lineCount > lines.length) {
2845
+ for (let i = 0; i < this.lineCount - lines.length; i++) {
2846
+ output += eraseLine + "\n";
2847
+ }
2848
+ output += cursorUp(this.lineCount - lines.length);
2849
+ }
2850
+ this.lineCount = lines.length;
2851
+ process.stdout.write(output);
2852
+ }
2853
+ };
2854
+ function buildRepoSummary(result) {
2855
+ const parts = [];
2856
+ if (result.apiEndpoints.length > 0) {
2857
+ parts.push(`${result.apiEndpoints.length} endpoint${result.apiEndpoints.length === 1 ? "" : "s"}`);
2858
+ }
2859
+ if (result.components.length > 0) {
2860
+ parts.push(`${result.components.length} component${result.components.length === 1 ? "" : "s"}`);
2861
+ }
2862
+ if (result.diagrams.length > 0) {
2863
+ parts.push(`${result.diagrams.length} diagram${result.diagrams.length === 1 ? "" : "s"}`);
2864
+ }
2865
+ if (result.dataModels.length > 0) {
2866
+ parts.push(`${result.dataModels.length} model${result.dataModels.length === 1 ? "" : "s"}`);
2867
+ }
2868
+ return parts.length > 0 ? parts.join(", ") : "Analysis complete";
2869
+ }
2870
+
2565
2871
  // src/commands/init.ts
2566
2872
  var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
2567
2873
  async function initCommand(options) {
2568
2874
  p5.intro("open-auto-doc \u2014 AI-powered documentation generator");
2875
+ const templateDir = resolveTemplateDir();
2876
+ if (!fs8.existsSync(path8.join(templateDir, "package.json"))) {
2877
+ p5.log.error(
2878
+ `Site template not found at: ${templateDir}
2879
+ This usually means the npm package was not built correctly.
2880
+ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
2881
+ );
2882
+ process.exit(1);
2883
+ }
2569
2884
  let token = getGithubToken();
2570
2885
  if (!token) {
2571
2886
  p5.log.info("Let's connect your GitHub account.");
@@ -2629,17 +2944,12 @@ async function initCommand(options) {
2629
2944
  p5.log.error("No repositories were cloned.");
2630
2945
  process.exit(1);
2631
2946
  }
2632
- const analyzeSpinner = p5.spinner();
2633
- let completed = 0;
2634
2947
  const total = clones.length;
2635
- const repoStages = {};
2636
- const updateSpinner = () => {
2637
- const lines = Object.entries(repoStages).map(([name, status]) => `[${name}] ${status}`).join(" | ");
2638
- analyzeSpinner.message(`${completed}/${total} done \u2014 ${lines}`);
2639
- };
2640
- analyzeSpinner.start(`Analyzing ${total} ${total === 1 ? "repo" : "repos"} in parallel...`);
2948
+ const progressTable = new ProgressTable({ repos: clones.map((c) => c.info.name) });
2949
+ progressTable.start();
2641
2950
  const analysisPromises = clones.map(async (cloned) => {
2642
2951
  const repoName = cloned.info.name;
2952
+ progressTable.update(repoName, { status: "active", message: "Starting..." });
2643
2953
  try {
2644
2954
  const result = await analyzeRepository({
2645
2955
  repoPath: cloned.localPath,
@@ -2648,26 +2958,24 @@ async function initCommand(options) {
2648
2958
  apiKey,
2649
2959
  model,
2650
2960
  onProgress: (stage, msg) => {
2651
- repoStages[repoName] = `${stage}: ${msg}`;
2652
- updateSpinner();
2961
+ progressTable.update(repoName, { status: "active", message: `${stage}: ${msg}` });
2653
2962
  }
2654
2963
  });
2655
- completed++;
2656
- delete repoStages[repoName];
2657
- updateSpinner();
2964
+ progressTable.update(repoName, { status: "done", summary: buildRepoSummary(result) });
2658
2965
  return { repo: repoName, result };
2659
2966
  } catch (err) {
2660
- completed++;
2661
- delete repoStages[repoName];
2662
- updateSpinner();
2663
- p5.log.warn(`[${repoName}] Analysis failed: ${err instanceof Error ? err.message : err}`);
2967
+ const errMsg = err instanceof Error ? err.message : String(err);
2968
+ progressTable.update(repoName, { status: "failed", error: errMsg });
2969
+ p5.log.warn(`[${repoName}] Analysis failed: ${errMsg}`);
2664
2970
  return { repo: repoName, result: null };
2665
2971
  }
2666
2972
  });
2667
2973
  const settled = await Promise.all(analysisPromises);
2974
+ progressTable.stop();
2668
2975
  const results = settled.filter((s) => s.result !== null).map((s) => s.result);
2669
- analyzeSpinner.stop(
2670
- `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` : "")
2976
+ const { done, failed } = progressTable.getSummary();
2977
+ p5.log.step(
2978
+ `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` : "")
2671
2979
  );
2672
2980
  if (results.length === 0) {
2673
2981
  p5.log.error("No repositories were successfully analyzed.");
@@ -2693,8 +3001,6 @@ async function initCommand(options) {
2693
3001
  const genSpinner = p5.spinner();
2694
3002
  try {
2695
3003
  genSpinner.start("Scaffolding documentation site...");
2696
- const templateDir = resolveTemplateDir();
2697
- p5.log.info(`Using template from: ${templateDir}`);
2698
3004
  await scaffoldSite(outputDir, projectName, templateDir);
2699
3005
  genSpinner.stop("Site scaffolded");
2700
3006
  } catch (err) {
@@ -2776,23 +3082,25 @@ async function initCommand(options) {
2776
3082
  token,
2777
3083
  config
2778
3084
  });
2779
- if (ciResult) {
2780
- showSecretsInstructions(repos.length > 1);
2781
- }
2782
3085
  showVercelInstructions(deployResult.owner, deployResult.repoName);
2783
3086
  p5.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
2784
3087
  }
2785
3088
  function resolveTemplateDir() {
2786
3089
  const candidates = [
3090
+ path8.resolve(__dirname2, "site-template"),
3091
+ // dist/site-template (npm global install)
2787
3092
  path8.resolve(__dirname2, "../../site-template"),
3093
+ // monorepo: packages/site-template
2788
3094
  path8.resolve(__dirname2, "../../../site-template"),
3095
+ // monorepo alt
2789
3096
  path8.resolve(__dirname2, "../../../../packages/site-template")
3097
+ // monorepo from nested dist
2790
3098
  ];
2791
3099
  for (const candidate of candidates) {
2792
3100
  const pkgPath = path8.join(candidate, "package.json");
2793
3101
  if (fs8.existsSync(pkgPath)) return candidate;
2794
3102
  }
2795
- return path8.resolve(__dirname2, "../../site-template");
3103
+ return path8.resolve(__dirname2, "site-template");
2796
3104
  }
2797
3105
  function cleanup(clones) {
2798
3106
  for (const clone of clones) {
@@ -2883,24 +3191,16 @@ async function generateCommand(options) {
2883
3191
  p6.log.error("No repositories were cloned.");
2884
3192
  process.exit(1);
2885
3193
  }
2886
- const analyzeSpinner = p6.spinner();
2887
- let completed = 0;
2888
3194
  const total = clones.length;
2889
- const repoStages = {};
2890
- const updateSpinner = () => {
2891
- const lines = Object.entries(repoStages).map(([name, status]) => `[${name}] ${status}`).join(" | ");
2892
- analyzeSpinner.message(`${completed}/${total} done \u2014 ${lines}`);
2893
- };
2894
- analyzeSpinner.start(`Analyzing ${total} ${total === 1 ? "repo" : "repos"} in parallel...`);
3195
+ const progressTable = new ProgressTable({ repos: clones.map((c) => c.info.name) });
3196
+ progressTable.start();
2895
3197
  const analysisPromises = clones.map(async (cloned) => {
2896
3198
  const repo = config.repos.find((r) => r.name === cloned.info.name);
2897
3199
  const repoName = repo.name;
2898
- const callbacks = {
2899
- onProgress: (stage, msg) => {
2900
- repoStages[repoName] = `${stage}: ${msg}`;
2901
- updateSpinner();
2902
- }
3200
+ const onProgress = (stage, msg) => {
3201
+ progressTable.update(repoName, { status: "active", message: `${stage}: ${msg}` });
2903
3202
  };
3203
+ progressTable.update(repoName, { status: "active", message: "Starting..." });
2904
3204
  try {
2905
3205
  let result;
2906
3206
  if (incremental) {
@@ -2914,7 +3214,7 @@ async function generateCommand(options) {
2914
3214
  model,
2915
3215
  previousResult: cached.result,
2916
3216
  previousCommitSha: cached.commitSha,
2917
- ...callbacks
3217
+ onProgress
2918
3218
  });
2919
3219
  } else {
2920
3220
  result = await analyzeRepository({
@@ -2923,7 +3223,7 @@ async function generateCommand(options) {
2923
3223
  repoUrl: repo.htmlUrl,
2924
3224
  apiKey,
2925
3225
  model,
2926
- ...callbacks
3226
+ onProgress
2927
3227
  });
2928
3228
  }
2929
3229
  } else {
@@ -2933,7 +3233,7 @@ async function generateCommand(options) {
2933
3233
  repoUrl: repo.htmlUrl,
2934
3234
  apiKey,
2935
3235
  model,
2936
- ...callbacks
3236
+ onProgress
2937
3237
  });
2938
3238
  }
2939
3239
  try {
@@ -2941,21 +3241,22 @@ async function generateCommand(options) {
2941
3241
  saveCache(cacheDir, repo.name, headSha, result);
2942
3242
  } catch {
2943
3243
  }
2944
- completed++;
2945
- delete repoStages[repoName];
2946
- updateSpinner();
3244
+ progressTable.update(repoName, { status: "done", summary: buildRepoSummary(result) });
2947
3245
  return { repo: repoName, result };
2948
3246
  } catch (err) {
2949
- completed++;
2950
- delete repoStages[repoName];
2951
- updateSpinner();
2952
- p6.log.warn(`[${repoName}] Analysis failed: ${err instanceof Error ? err.message : err}`);
3247
+ const errMsg = err instanceof Error ? err.message : String(err);
3248
+ progressTable.update(repoName, { status: "failed", error: errMsg });
3249
+ p6.log.warn(`[${repoName}] Analysis failed: ${errMsg}`);
2953
3250
  return { repo: repoName, result: null };
2954
3251
  }
2955
3252
  });
2956
3253
  const settled = await Promise.all(analysisPromises);
3254
+ progressTable.stop();
2957
3255
  const freshResults = settled.filter((s) => s.result !== null).map((s) => s.result);
2958
- analyzeSpinner.stop(`Analyzed ${freshResults.length}/${total} ${total === 1 ? "repository" : "repositories"}`);
3256
+ const { done: analyzedCount, failed: failedCount } = progressTable.getSummary();
3257
+ p6.log.step(
3258
+ `Analyzed ${analyzedCount}/${total} ${total === 1 ? "repository" : "repositories"}` + (failedCount > 0 ? ` (${failedCount} failed)` : "")
3259
+ );
2959
3260
  const results = [...freshResults, ...cachedResults];
2960
3261
  if (results.length > 0) {
2961
3262
  let crossRepo;
@@ -3073,7 +3374,6 @@ async function setupCiCommand() {
3073
3374
  p8.cancel("Setup cancelled.");
3074
3375
  process.exit(0);
3075
3376
  }
3076
- showSecretsInstructions(isMultiRepo);
3077
3377
  if ("repos" in result) {
3078
3378
  p8.outro("Per-repo CI workflows created! Add the required secrets to each source repo.");
3079
3379
  } else {
@@ -3110,7 +3410,7 @@ async function logoutCommand() {
3110
3410
 
3111
3411
  // src/index.ts
3112
3412
  var program = new Command();
3113
- program.name("open-auto-doc").description("Auto-generate beautiful documentation websites from GitHub repositories using AI").version("0.3.2");
3413
+ program.name("open-auto-doc").description("Auto-generate beautiful documentation websites from GitHub repositories using AI").version("0.3.4");
3114
3414
  program.command("init", { isDefault: true }).description("Initialize and generate documentation for your repositories").option("-o, --output <dir>", "Output directory", "docs-site").action(initCommand);
3115
3415
  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);
3116
3416
  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);
@@ -0,0 +1,6 @@
1
+ import { source } from "@/lib/source";
2
+ import { createFromSource } from "fumadocs-core/search/server";
3
+
4
+ export const { GET } = createFromSource(source, {
5
+ language: "english",
6
+ });
@@ -0,0 +1,47 @@
1
+ import { source } from "@/lib/source";
2
+ import {
3
+ DocsBody,
4
+ DocsDescription,
5
+ DocsPage,
6
+ DocsTitle,
7
+ } from "fumadocs-ui/layouts/docs/page";
8
+ import { notFound } from "next/navigation";
9
+ import { getMDXComponents } from "@/mdx-components";
10
+ import type { Metadata } from "next";
11
+
12
+ export default async function Page(props: {
13
+ params: Promise<{ slug?: string[] }>;
14
+ }) {
15
+ const params = await props.params;
16
+ const page = source.getPage(params.slug);
17
+ if (!page) notFound();
18
+
19
+ const MDX = page.data.body;
20
+
21
+ return (
22
+ <DocsPage toc={page.data.toc} full={page.data.full}>
23
+ <DocsTitle>{page.data.title}</DocsTitle>
24
+ <DocsDescription>{page.data.description}</DocsDescription>
25
+ <DocsBody>
26
+ <MDX components={getMDXComponents()} />
27
+ </DocsBody>
28
+ </DocsPage>
29
+ );
30
+ }
31
+
32
+ export async function generateStaticParams() {
33
+ return source.generateParams();
34
+ }
35
+
36
+ export async function generateMetadata(props: {
37
+ params: Promise<{ slug?: string[] }>;
38
+ }): Promise<Metadata> {
39
+ const params = await props.params;
40
+ const page = source.getPage(params.slug);
41
+ if (!page) notFound();
42
+
43
+ return {
44
+ title: page.data.title,
45
+ description: page.data.description,
46
+ };
47
+ }
@@ -0,0 +1,12 @@
1
+ import { source } from "@/lib/source";
2
+ import { DocsLayout } from "fumadocs-ui/layouts/docs";
3
+ import { baseOptions } from "@/lib/layout.shared";
4
+ import type { ReactNode } from "react";
5
+
6
+ export default function Layout({ children }: { children: ReactNode }) {
7
+ return (
8
+ <DocsLayout tree={source.getPageTree()} {...baseOptions()}>
9
+ {children}
10
+ </DocsLayout>
11
+ );
12
+ }
@@ -0,0 +1,3 @@
1
+ @import "tailwindcss";
2
+ @import "fumadocs-ui/css/neutral.css";
3
+ @import "fumadocs-ui/css/preset.css";
@@ -0,0 +1,21 @@
1
+ import { RootProvider } from "fumadocs-ui/provider/next";
2
+ import "./global.css";
3
+ import { Inter } from "next/font/google";
4
+ import type { ReactNode } from "react";
5
+
6
+ const inter = Inter({ subsets: ["latin"] });
7
+
8
+ export const metadata = {
9
+ title: "{{projectName}} Documentation",
10
+ description: "Auto-generated documentation for {{projectName}}",
11
+ };
12
+
13
+ export default function RootLayout({ children }: { children: ReactNode }) {
14
+ return (
15
+ <html lang="en" className={inter.className} suppressHydrationWarning>
16
+ <body className="flex min-h-screen flex-col">
17
+ <RootProvider>{children}</RootProvider>
18
+ </body>
19
+ </html>
20
+ );
21
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ export default function RootPage() {
4
+ redirect("/docs");
5
+ }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+
5
+ export function Mermaid({ code }: { code: string }) {
6
+ const id = useId().replace(/:/g, "m");
7
+ const containerRef = useRef<HTMLDivElement>(null);
8
+ const [svg, setSvg] = useState<string>("");
9
+ const [error, setError] = useState<string>("");
10
+
11
+ useEffect(() => {
12
+ let cancelled = false;
13
+
14
+ async function render() {
15
+ try {
16
+ const mermaid = (await import("mermaid")).default;
17
+ mermaid.initialize({
18
+ startOnLoad: false,
19
+ theme: "neutral",
20
+ securityLevel: "loose",
21
+ });
22
+ const { svg: rendered } = await mermaid.render(
23
+ `mermaid-${id}`,
24
+ code,
25
+ );
26
+ if (!cancelled) setSvg(rendered);
27
+ } catch (err) {
28
+ if (!cancelled)
29
+ setError(err instanceof Error ? err.message : "Diagram error");
30
+ }
31
+ }
32
+
33
+ render();
34
+ return () => {
35
+ cancelled = true;
36
+ };
37
+ }, [code, id]);
38
+
39
+ if (error) {
40
+ return (
41
+ <pre className="rounded-lg border bg-fd-card p-4 text-sm text-fd-muted-foreground overflow-x-auto">
42
+ <code>{code}</code>
43
+ </pre>
44
+ );
45
+ }
46
+
47
+ if (!svg) {
48
+ return (
49
+ <div className="flex items-center justify-center rounded-lg border bg-fd-card p-8 text-sm text-fd-muted-foreground">
50
+ Loading diagram...
51
+ </div>
52
+ );
53
+ }
54
+
55
+ return (
56
+ <div
57
+ ref={containerRef}
58
+ className="my-4 flex justify-center overflow-x-auto rounded-lg border bg-fd-card p-4 [&_svg]:max-w-full"
59
+ dangerouslySetInnerHTML={{ __html: svg }}
60
+ />
61
+ );
62
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ title: Welcome
3
+ description: Auto-generated documentation
4
+ ---
5
+
6
+ # Welcome
7
+
8
+ This documentation was auto-generated by [open-auto-doc](https://github.com/open-auto-doc).
9
+
10
+ Browse the sidebar to explore the documentation.
@@ -0,0 +1,3 @@
1
+ {
2
+ "pages": ["index"]
3
+ }
@@ -0,0 +1,9 @@
1
+ import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
2
+
3
+ export function baseOptions(): BaseLayoutProps {
4
+ return {
5
+ nav: {
6
+ title: "{{projectName}}",
7
+ },
8
+ };
9
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Remark plugin that transforms ```mermaid code blocks into <Mermaid /> JSX components.
3
+ * This runs before Shiki so the mermaid blocks never get syntax-highlighted as code.
4
+ */
5
+ export function remarkMermaid() {
6
+ return (tree: any) => {
7
+ walk(tree);
8
+ };
9
+ }
10
+
11
+ function walk(node: any) {
12
+ if (!node.children) return;
13
+ for (let i = 0; i < node.children.length; i++) {
14
+ const child = node.children[i];
15
+ if (child.type === "code" && child.lang === "mermaid") {
16
+ node.children[i] = {
17
+ type: "mdxJsxFlowElement",
18
+ name: "Mermaid",
19
+ attributes: [
20
+ {
21
+ type: "mdxJsxAttribute",
22
+ name: "code",
23
+ value: child.value,
24
+ },
25
+ ],
26
+ children: [],
27
+ data: { _mdxExplicitJsx: true },
28
+ };
29
+ } else {
30
+ walk(child);
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,7 @@
1
+ import { docs } from "fumadocs-mdx:collections/server";
2
+ import { loader } from "fumadocs-core/source";
3
+
4
+ export const source = loader({
5
+ baseUrl: "/docs",
6
+ source: docs.toFumadocsSource(),
7
+ });
@@ -0,0 +1,11 @@
1
+ import defaultMdxComponents from "fumadocs-ui/mdx";
2
+ import type { MDXComponents } from "mdx/types";
3
+ import { Mermaid } from "@/components/mermaid";
4
+
5
+ export function getMDXComponents(components?: MDXComponents): MDXComponents {
6
+ return {
7
+ ...defaultMdxComponents,
8
+ Mermaid,
9
+ ...components,
10
+ };
11
+ }
@@ -0,0 +1,10 @@
1
+ import { createMDX } from "fumadocs-mdx/next";
2
+
3
+ const withMDX = createMDX();
4
+
5
+ /** @type {import('next').NextConfig} */
6
+ const config = {
7
+ reactStrictMode: true,
8
+ };
9
+
10
+ export default withMDX(config);
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "{{projectName}}-docs",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "postinstall": "fumadocs-mdx"
10
+ },
11
+ "dependencies": {
12
+ "next": "^16.0.0",
13
+ "react": "^19.0.0",
14
+ "react-dom": "^19.0.0",
15
+ "fumadocs-core": "^16.0.0",
16
+ "fumadocs-ui": "^16.0.0",
17
+ "fumadocs-mdx": "^14.0.0",
18
+ "mermaid": "^11.0.0",
19
+ "lucide-react": "^0.475.0",
20
+ "@types/mdx": "^2.0.13"
21
+ },
22
+ "devDependencies": {
23
+ "@tailwindcss/postcss": "^4.0.0",
24
+ "@types/node": "^22.12.0",
25
+ "@types/react": "^19.0.0",
26
+ "@types/react-dom": "^19.0.0",
27
+ "postcss": "^8.5.0",
28
+ "tailwindcss": "^4.0.0",
29
+ "typescript": "^5.7.0"
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,12 @@
1
+ import { defineConfig, defineDocs } from "fumadocs-mdx/config";
2
+ import { remarkMermaid } from "./lib/remark-mermaid";
3
+
4
+ export const docs = defineDocs({
5
+ dir: "content/docs",
6
+ });
7
+
8
+ export default defineConfig({
9
+ mdxOptions: {
10
+ remarkPlugins: [remarkMermaid],
11
+ },
12
+ });
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "target": "ESNext",
5
+ "lib": ["dom", "dom.iterable", "esnext"],
6
+ "allowJs": true,
7
+ "skipLibCheck": true,
8
+ "strict": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "noEmit": true,
11
+ "esModuleInterop": true,
12
+ "module": "esnext",
13
+ "moduleResolution": "bundler",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "jsx": "react-jsx",
17
+ "incremental": true,
18
+ "paths": {
19
+ "@/*": ["./*"],
20
+ "fumadocs-mdx:collections/*": [".source/*"]
21
+ },
22
+ "plugins": [{ "name": "next" }]
23
+ },
24
+ "include": [
25
+ "next-env.d.ts",
26
+ "**/*.ts",
27
+ "**/*.tsx",
28
+ ".next/types/**/*.ts",
29
+ ".next/dev/types/**/*.ts"
30
+ ],
31
+ "exclude": ["node_modules"]
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latent-space-labs/open-auto-doc",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Auto-generate beautiful documentation websites from GitHub repositories using AI",
5
5
  "type": "module",
6
6
  "bin": {