@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.
- package/dist/index.js +891 -570
- 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
|
|
8
|
-
import
|
|
9
|
-
import
|
|
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
|
|
41
|
-
|
|
40
|
+
const spinner8 = p.spinner();
|
|
41
|
+
spinner8.start("Waiting for GitHub authorization...");
|
|
42
42
|
const token = await pollForToken(deviceData.device_code, deviceData.interval);
|
|
43
|
-
|
|
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
|
|
166
|
-
|
|
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
|
-
|
|
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
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
+
spinner8.stop(`Created ${data.full_name}`);
|
|
315
315
|
}
|
|
316
316
|
} catch (err) {
|
|
317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
344
|
+
spinner8.stop("Pushed to GitHub.");
|
|
345
345
|
} catch (err) {
|
|
346
|
-
|
|
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
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
373
|
+
spinner8.stop("Pushed updates to docs repo.");
|
|
374
374
|
return true;
|
|
375
375
|
} catch (err) {
|
|
376
|
-
|
|
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/
|
|
397
|
+
// src/actions/build-check.ts
|
|
398
398
|
import * as p4 from "@clack/prompts";
|
|
399
|
-
import { execSync as
|
|
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
|
|
743
|
-
import
|
|
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
|
|
755
|
-
import
|
|
756
|
-
import
|
|
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 =
|
|
779
|
-
const node = { path:
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
454
|
+
const ext = path5.extname(entry.name).slice(1);
|
|
795
455
|
let size;
|
|
796
456
|
try {
|
|
797
|
-
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 =
|
|
819
|
-
if (
|
|
820
|
-
const content =
|
|
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 =
|
|
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((
|
|
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 =
|
|
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((
|
|
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
|
|
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 \`\\{\`, \`\\}\`, \`<\`, \`>\` (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
|
|
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
|
|
2141
|
-
|
|
2142
|
-
const
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
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
|
-
|
|
2151
|
-
|
|
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
|
-
|
|
2155
|
-
|
|
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
|
|
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
|
|
2165
|
-
import
|
|
2166
|
-
import { execSync as
|
|
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
|
|
2297
|
+
await fs8.copy(templateDir, outputDir, {
|
|
2175
2298
|
overwrite: true,
|
|
2176
2299
|
filter: (src) => {
|
|
2177
|
-
const basename =
|
|
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
|
|
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
|
|
2310
|
+
await fs8.writeFile(filePath, content, "utf-8");
|
|
2188
2311
|
}
|
|
2189
2312
|
} catch {
|
|
2190
2313
|
}
|
|
2191
2314
|
}
|
|
2192
|
-
const nodeModulesPath =
|
|
2193
|
-
if (!
|
|
2315
|
+
const nodeModulesPath = path8.join(outputDir, "node_modules");
|
|
2316
|
+
if (!fs8.existsSync(nodeModulesPath)) {
|
|
2194
2317
|
try {
|
|
2195
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2353
|
+
const entries = await fs8.readdir(currentDir, { withFileTypes: true });
|
|
2231
2354
|
for (const entry of entries) {
|
|
2232
|
-
const fullPath =
|
|
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(
|
|
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 =
|
|
2995
|
+
var __dirname2 = path9.dirname(fileURLToPath2(import.meta.url));
|
|
2671
2996
|
async function initCommand(options) {
|
|
2672
|
-
|
|
2997
|
+
p6.intro("open-auto-doc \u2014 AI-powered documentation generator");
|
|
2673
2998
|
const templateDir = resolveTemplateDir();
|
|
2674
|
-
if (!
|
|
2675
|
-
|
|
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
|
-
|
|
3009
|
+
p6.log.info("Let's connect your GitHub account.");
|
|
2685
3010
|
token = await authenticateWithGithub();
|
|
2686
3011
|
setGithubToken(token);
|
|
2687
3012
|
} else {
|
|
2688
|
-
|
|
3013
|
+
p6.log.success("Using saved GitHub credentials.");
|
|
2689
3014
|
}
|
|
2690
3015
|
const repos = await pickRepos(token);
|
|
2691
|
-
|
|
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
|
|
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 (
|
|
2702
|
-
|
|
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
|
|
3031
|
+
const saveKey = await p6.confirm({
|
|
2707
3032
|
message: "Save API key for future use?"
|
|
2708
3033
|
});
|
|
2709
|
-
if (saveKey && !
|
|
3034
|
+
if (saveKey && !p6.isCancel(saveKey)) {
|
|
2710
3035
|
setAnthropicKey(apiKey);
|
|
2711
3036
|
}
|
|
2712
3037
|
} else {
|
|
2713
|
-
|
|
3038
|
+
p6.log.success("Using saved Anthropic API key.");
|
|
2714
3039
|
}
|
|
2715
|
-
const model = await
|
|
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 (
|
|
2724
|
-
|
|
3048
|
+
if (p6.isCancel(model)) {
|
|
3049
|
+
p6.cancel("Operation cancelled");
|
|
2725
3050
|
process.exit(0);
|
|
2726
3051
|
}
|
|
2727
|
-
|
|
2728
|
-
const cloneSpinner =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2749
|
-
|
|
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
|
-
|
|
2765
|
-
updateSpinner();
|
|
3084
|
+
progressTable.update(repoName, { status: "active", message: `${stage}: ${msg}` });
|
|
2766
3085
|
}
|
|
2767
3086
|
});
|
|
2768
|
-
|
|
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
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
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
|
-
|
|
2783
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
3119
|
+
p6.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
|
|
2802
3120
|
}
|
|
2803
3121
|
}
|
|
2804
|
-
const outputDir =
|
|
3122
|
+
const outputDir = path9.resolve(options.output || "docs-site");
|
|
2805
3123
|
const projectName = results.length === 1 ? results[0].repoName : "My Project";
|
|
2806
|
-
const genSpinner =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2844
|
-
|
|
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 (
|
|
2848
|
-
|
|
2849
|
-
`cd ${
|
|
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
|
-
|
|
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
|
-
|
|
2862
|
-
`cd ${
|
|
3184
|
+
p6.note(
|
|
3185
|
+
`cd ${path9.relative(process.cwd(), outputDir)} && npm run dev`,
|
|
2863
3186
|
"Next steps"
|
|
2864
3187
|
);
|
|
2865
|
-
|
|
3188
|
+
p6.outro("Done!");
|
|
2866
3189
|
return;
|
|
2867
3190
|
}
|
|
2868
|
-
const shouldSetupCi = await
|
|
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 (
|
|
3194
|
+
if (p6.isCancel(shouldSetupCi) || !shouldSetupCi) {
|
|
2872
3195
|
showVercelInstructions(deployResult.owner, deployResult.repoName);
|
|
2873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3214
|
+
p6.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
|
|
2892
3215
|
}
|
|
2893
3216
|
function resolveTemplateDir() {
|
|
2894
3217
|
const candidates = [
|
|
2895
|
-
|
|
3218
|
+
path9.resolve(__dirname2, "site-template"),
|
|
2896
3219
|
// dist/site-template (npm global install)
|
|
2897
|
-
|
|
3220
|
+
path9.resolve(__dirname2, "../../site-template"),
|
|
2898
3221
|
// monorepo: packages/site-template
|
|
2899
|
-
|
|
3222
|
+
path9.resolve(__dirname2, "../../../site-template"),
|
|
2900
3223
|
// monorepo alt
|
|
2901
|
-
|
|
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 =
|
|
2906
|
-
if (
|
|
3228
|
+
const pkgPath = path9.join(candidate, "package.json");
|
|
3229
|
+
if (fs9.existsSync(pkgPath)) return candidate;
|
|
2907
3230
|
}
|
|
2908
|
-
return
|
|
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
|
|
2918
|
-
import
|
|
3240
|
+
import * as p7 from "@clack/prompts";
|
|
3241
|
+
import path10 from "path";
|
|
2919
3242
|
async function generateCommand(options) {
|
|
2920
|
-
|
|
3243
|
+
p7.intro("open-auto-doc \u2014 Regenerating documentation");
|
|
2921
3244
|
const config = loadConfig();
|
|
2922
3245
|
if (!config) {
|
|
2923
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2945
|
-
|
|
3267
|
+
if (p7.isCancel(model)) {
|
|
3268
|
+
p7.cancel("Cancelled.");
|
|
2946
3269
|
process.exit(0);
|
|
2947
3270
|
}
|
|
2948
|
-
|
|
3271
|
+
p7.log.info(`Using ${model}`);
|
|
2949
3272
|
const incremental = options.incremental && !options.force;
|
|
2950
|
-
const cacheDir =
|
|
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
|
-
|
|
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
|
-
|
|
3289
|
+
p7.log.info(`Using cached analysis for ${repo.name}`);
|
|
2967
3290
|
} else {
|
|
2968
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3003
|
-
|
|
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
|
|
3012
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
3401
|
+
p7.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
|
|
3086
3402
|
}
|
|
3087
3403
|
}
|
|
3088
|
-
const contentDir =
|
|
3404
|
+
const contentDir = path10.join(config.outputDir, "content", "docs");
|
|
3089
3405
|
await writeContent(contentDir, results, crossRepo);
|
|
3090
3406
|
await writeMeta(contentDir, results, crossRepo);
|
|
3091
|
-
|
|
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
|
-
|
|
3417
|
+
p7.outro("Done!");
|
|
3097
3418
|
}
|
|
3098
3419
|
|
|
3099
3420
|
// src/commands/deploy.ts
|
|
3100
|
-
import * as
|
|
3101
|
-
import
|
|
3102
|
-
import
|
|
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 =
|
|
3106
|
-
if (!
|
|
3107
|
-
|
|
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 &&
|
|
3113
|
-
return
|
|
3433
|
+
if (config?.outputDir && fs10.existsSync(path11.resolve(config.outputDir))) {
|
|
3434
|
+
return path11.resolve(config.outputDir);
|
|
3114
3435
|
}
|
|
3115
|
-
if (
|
|
3116
|
-
return
|
|
3436
|
+
if (fs10.existsSync(path11.resolve("docs-site"))) {
|
|
3437
|
+
return path11.resolve("docs-site");
|
|
3117
3438
|
}
|
|
3118
|
-
|
|
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
|
-
|
|
3445
|
+
p8.intro("open-auto-doc \u2014 Deploy docs to GitHub");
|
|
3125
3446
|
const token = getGithubToken();
|
|
3126
3447
|
if (!token) {
|
|
3127
|
-
|
|
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
|
-
|
|
3453
|
+
p8.log.info(`Docs directory: ${docsDir}`);
|
|
3133
3454
|
if (config?.docsRepo) {
|
|
3134
|
-
|
|
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
|
-
|
|
3458
|
+
p8.outro("Docs updated! Vercel will auto-deploy from the push.");
|
|
3138
3459
|
} else {
|
|
3139
|
-
|
|
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
|
-
|
|
3470
|
+
p8.cancel("Deploy cancelled.");
|
|
3150
3471
|
process.exit(0);
|
|
3151
3472
|
}
|
|
3152
3473
|
showVercelInstructions(result.owner, result.repoName);
|
|
3153
|
-
|
|
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
|
|
3478
|
+
import * as p9 from "@clack/prompts";
|
|
3158
3479
|
async function setupCiCommand() {
|
|
3159
|
-
|
|
3480
|
+
p9.intro("open-auto-doc \u2014 CI/CD Setup");
|
|
3160
3481
|
const config = loadConfig();
|
|
3161
3482
|
if (!config?.docsRepo) {
|
|
3162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3507
|
+
p9.cancel("Setup cancelled.");
|
|
3187
3508
|
process.exit(0);
|
|
3188
3509
|
}
|
|
3189
3510
|
if ("repos" in result) {
|
|
3190
|
-
|
|
3511
|
+
p9.outro("Per-repo CI workflows created! Add the required secrets to each source repo.");
|
|
3191
3512
|
} else {
|
|
3192
|
-
|
|
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
|
|
3518
|
+
import * as p10 from "@clack/prompts";
|
|
3198
3519
|
async function loginCommand() {
|
|
3199
|
-
|
|
3520
|
+
p10.intro("open-auto-doc \u2014 GitHub Login");
|
|
3200
3521
|
const existing = getGithubToken();
|
|
3201
3522
|
if (existing) {
|
|
3202
|
-
const overwrite = await
|
|
3523
|
+
const overwrite = await p10.confirm({
|
|
3203
3524
|
message: "You're already logged in. Re-authenticate?"
|
|
3204
3525
|
});
|
|
3205
|
-
if (!overwrite ||
|
|
3206
|
-
|
|
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
|
-
|
|
3533
|
+
p10.outro("Logged in successfully!");
|
|
3213
3534
|
}
|
|
3214
3535
|
|
|
3215
3536
|
// src/commands/logout.ts
|
|
3216
|
-
import * as
|
|
3537
|
+
import * as p11 from "@clack/prompts";
|
|
3217
3538
|
async function logoutCommand() {
|
|
3218
|
-
|
|
3539
|
+
p11.intro("open-auto-doc \u2014 Logout");
|
|
3219
3540
|
clearAll();
|
|
3220
|
-
|
|
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.
|
|
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);
|