@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 +351 -51
- package/dist/site-template/app/api/search/route.ts +6 -0
- package/dist/site-template/app/docs/[[...slug]]/page.tsx +47 -0
- package/dist/site-template/app/docs/layout.tsx +12 -0
- package/dist/site-template/app/global.css +3 -0
- package/dist/site-template/app/layout.tsx +21 -0
- package/dist/site-template/app/page.tsx +5 -0
- package/dist/site-template/components/mermaid.tsx +62 -0
- package/dist/site-template/content/docs/index.mdx +10 -0
- package/dist/site-template/content/docs/meta.json +3 -0
- package/dist/site-template/lib/layout.shared.ts +9 -0
- package/dist/site-template/lib/remark-mermaid.ts +33 -0
- package/dist/site-template/lib/source.ts +7 -0
- package/dist/site-template/mdx-components.tsx +11 -0
- package/dist/site-template/next.config.mjs +10 -0
- package/dist/site-template/package.json +31 -0
- package/dist/site-template/postcss.config.mjs +7 -0
- package/dist/site-template/source.config.ts +12 -0
- package/dist/site-template/tsconfig.json +32 -0
- package/package.json +1 -1
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
|
|
2636
|
-
|
|
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
|
-
|
|
2652
|
-
updateSpinner();
|
|
2961
|
+
progressTable.update(repoName, { status: "active", message: `${stage}: ${msg}` });
|
|
2653
2962
|
}
|
|
2654
2963
|
});
|
|
2655
|
-
|
|
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
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
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
|
-
|
|
2670
|
-
|
|
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, "
|
|
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
|
|
2890
|
-
|
|
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
|
|
2899
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
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
|
-
|
|
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.
|
|
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,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,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,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,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,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,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,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
|
+
}
|