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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +674 -432
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
- import * as p3 from "@clack/prompts";
7
+ import * as p5 from "@clack/prompts";
8
8
  import fs8 from "fs";
9
9
  import path8 from "path";
10
10
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -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 spinner6 = p.spinner();
41
- spinner6.start("Waiting for GitHub authorization...");
40
+ const spinner7 = p.spinner();
41
+ spinner7.start("Waiting for GitHub authorization...");
42
42
  const token = await pollForToken(deviceData.device_code, deviceData.interval);
43
- spinner6.stop("GitHub authentication successful!");
43
+ spinner7.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 spinner6 = p2.spinner();
166
- spinner6.start("Fetching your repositories...");
165
+ const spinner7 = p2.spinner();
166
+ spinner7.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
- spinner6.stop(`Found ${repos.length} repositories`);
191
+ spinner7.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) => ({
@@ -205,22 +205,451 @@ async function pickRepos(token) {
205
205
  return repos.filter((r) => selected.includes(r.fullName));
206
206
  }
207
207
 
208
- // ../analyzer/dist/index.js
208
+ // src/config.ts
209
209
  import fs3 from "fs";
210
210
  import path3 from "path";
211
- import ignore from "ignore";
212
- import fs22 from "fs";
213
- import path22 from "path";
214
- import fs32 from "fs";
215
- import path32 from "path";
211
+ function loadConfig() {
212
+ for (const candidate of [
213
+ path3.resolve(".autodocrc.json"),
214
+ path3.resolve("docs-site", ".autodocrc.json")
215
+ ]) {
216
+ if (fs3.existsSync(candidate)) {
217
+ try {
218
+ return JSON.parse(fs3.readFileSync(candidate, "utf-8"));
219
+ } catch {
220
+ }
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+ function saveConfig(config) {
226
+ fs3.writeFileSync(
227
+ path3.resolve(".autodocrc.json"),
228
+ JSON.stringify(config, null, 2)
229
+ );
230
+ if (config.outputDir && fs3.existsSync(config.outputDir)) {
231
+ fs3.writeFileSync(
232
+ path3.join(config.outputDir, ".autodocrc.json"),
233
+ JSON.stringify(config, null, 2)
234
+ );
235
+ }
236
+ }
237
+
238
+ // src/actions/deploy-action.ts
239
+ import * as p3 from "@clack/prompts";
240
+ import { execSync as execSync2 } from "child_process";
216
241
  import fs4 from "fs";
217
242
  import path4 from "path";
218
- import { query } from "@anthropic-ai/claude-agent-sdk";
243
+ import { Octokit as Octokit2 } from "@octokit/rest";
244
+ function exec(cmd, cwd) {
245
+ return execSync2(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
246
+ }
247
+ function getGitHubUsername(octokit) {
248
+ return octokit.rest.users.getAuthenticated().then((res) => res.data.login);
249
+ }
250
+ async function createAndPushDocsRepo(params) {
251
+ const { token, docsDir, config } = params;
252
+ const octokit = new Octokit2({ auth: token });
253
+ const username = await getGitHubUsername(octokit);
254
+ let orgs = [];
255
+ try {
256
+ const { data } = await octokit.rest.orgs.listForAuthenticatedUser({ per_page: 100 });
257
+ orgs = data;
258
+ } catch {
259
+ }
260
+ const ownerOptions = [
261
+ { value: username, label: username, hint: "Personal account" },
262
+ ...orgs.map((org) => ({ value: org.login, label: org.login, hint: "Organization" }))
263
+ ];
264
+ let owner = username;
265
+ if (ownerOptions.length > 1) {
266
+ const selected = await p3.select({
267
+ message: "Where should the docs repo be created?",
268
+ options: ownerOptions
269
+ });
270
+ if (p3.isCancel(selected)) return null;
271
+ owner = selected;
272
+ }
273
+ const isOrg = owner !== username;
274
+ const defaultName = config?.repos?.[0] ? `${config.repos[0].name}-docs` : "my-project-docs";
275
+ const repoName = await p3.text({
276
+ message: "Name for the docs GitHub repo:",
277
+ initialValue: defaultName,
278
+ validate: (v) => {
279
+ if (!v || v.length === 0) return "Repo name is required";
280
+ if (!/^[a-zA-Z0-9._-]+$/.test(v)) return "Invalid repo name";
281
+ }
282
+ });
283
+ if (p3.isCancel(repoName)) return null;
284
+ const visibility = await p3.select({
285
+ message: "Repository visibility:",
286
+ options: [
287
+ { value: "public", label: "Public" },
288
+ { value: "private", label: "Private" }
289
+ ]
290
+ });
291
+ if (p3.isCancel(visibility)) return null;
292
+ const spinner7 = p3.spinner();
293
+ spinner7.start(`Creating GitHub repo ${owner}/${repoName}...`);
294
+ let repoUrl;
295
+ try {
296
+ if (isOrg) {
297
+ const { data } = await octokit.rest.repos.createInOrg({
298
+ org: owner,
299
+ name: repoName,
300
+ private: visibility === "private",
301
+ description: "Auto-generated documentation site",
302
+ auto_init: false
303
+ });
304
+ repoUrl = data.clone_url;
305
+ spinner7.stop(`Created ${data.full_name}`);
306
+ } else {
307
+ const { data } = await octokit.rest.repos.createForAuthenticatedUser({
308
+ name: repoName,
309
+ private: visibility === "private",
310
+ description: "Auto-generated documentation site",
311
+ auto_init: false
312
+ });
313
+ repoUrl = data.clone_url;
314
+ spinner7.stop(`Created ${data.full_name}`);
315
+ }
316
+ } catch (err) {
317
+ spinner7.stop("Failed to create repo.");
318
+ if (err?.status === 422) {
319
+ p3.log.error(`Repository "${repoName}" already exists. Choose a different name or delete it first.`);
320
+ } else {
321
+ p3.log.error(`GitHub API error: ${err?.message || err}`);
322
+ }
323
+ return null;
324
+ }
325
+ spinner7.start("Pushing docs to GitHub...");
326
+ try {
327
+ const gitignorePath = path4.join(docsDir, ".gitignore");
328
+ if (!fs4.existsSync(gitignorePath)) {
329
+ fs4.writeFileSync(gitignorePath, "node_modules/\n.next/\n.source/\n");
330
+ }
331
+ if (!fs4.existsSync(path4.join(docsDir, ".git"))) {
332
+ exec("git init -b main", docsDir);
333
+ }
334
+ exec("git add -A", docsDir);
335
+ exec('git commit -m "Initial documentation site"', docsDir);
336
+ try {
337
+ exec("git remote remove origin", docsDir);
338
+ } catch {
339
+ }
340
+ const pushUrl = repoUrl.replace("https://", `https://${token}@`);
341
+ exec(`git remote add origin ${pushUrl}`, docsDir);
342
+ exec("git push -u origin main", docsDir);
343
+ exec("git remote set-url origin " + repoUrl, docsDir);
344
+ spinner7.stop("Pushed to GitHub.");
345
+ } catch (err) {
346
+ spinner7.stop("Git push failed.");
347
+ p3.log.error(`${err instanceof Error ? err.message : err}`);
348
+ return null;
349
+ }
350
+ const updatedConfig = { ...config };
351
+ updatedConfig.docsRepo = repoUrl;
352
+ saveConfig(updatedConfig);
353
+ return { repoUrl, owner, repoName };
354
+ }
355
+ async function pushUpdates(params) {
356
+ const { token, docsDir, docsRepo } = params;
357
+ const spinner7 = p3.spinner();
358
+ spinner7.start("Pushing updates to docs repo...");
359
+ try {
360
+ if (!fs4.existsSync(path4.join(docsDir, ".git"))) {
361
+ exec("git init", docsDir);
362
+ exec(`git remote add origin ${docsRepo}`, docsDir);
363
+ }
364
+ exec("git add -A", docsDir);
365
+ try {
366
+ exec("git diff --cached --quiet", docsDir);
367
+ spinner7.stop("No changes to push.");
368
+ return false;
369
+ } catch {
370
+ }
371
+ exec('git commit -m "Update documentation"', docsDir);
372
+ exec("git push -u origin main", docsDir);
373
+ spinner7.stop("Pushed updates to docs repo.");
374
+ return true;
375
+ } catch (err) {
376
+ spinner7.stop("Push failed.");
377
+ p3.log.error(`${err instanceof Error ? err.message : err}`);
378
+ return false;
379
+ }
380
+ }
381
+ function showVercelInstructions(owner, repoName) {
382
+ p3.note(
383
+ [
384
+ "Connect your docs repo to Vercel for automatic deployments:",
385
+ "",
386
+ " 1. Go to https://vercel.com/new",
387
+ " 2. Click 'Import Git Repository'",
388
+ ` 3. Select '${owner}/${repoName}'`,
389
+ " 4. Click 'Deploy'",
390
+ "",
391
+ "Once connected, Vercel will auto-deploy on every push to the docs repo."
392
+ ].join("\n"),
393
+ "Vercel Setup"
394
+ );
395
+ }
396
+
397
+ // src/actions/setup-ci-action.ts
398
+ import * as p4 from "@clack/prompts";
399
+ import { execSync as execSync3 } from "child_process";
219
400
  import fs5 from "fs";
220
401
  import path5 from "path";
221
- import { execSync as execSync2 } from "child_process";
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
+ return { workflowPath, branch };
559
+ }
560
+ async function createCiWorkflowsMultiRepo(params) {
561
+ const { token, config, docsRepoUrl } = params;
562
+ const octokit = new Octokit3({ auth: token });
563
+ p4.log.info(`Setting up CI for ${config.repos.length} repositories`);
564
+ p4.log.info(`Docs repo: ${docsRepoUrl}`);
565
+ const branch = await p4.text({
566
+ message: "Which branch should trigger doc updates?",
567
+ initialValue: "main",
568
+ validate: (v) => v.length === 0 ? "Branch name is required" : void 0
569
+ });
570
+ if (p4.isCancel(branch)) return null;
571
+ const createdRepos = [];
572
+ const workflowPath = ".github/workflows/update-docs.yml";
573
+ for (const repo of config.repos) {
574
+ const spinner7 = p4.spinner();
575
+ spinner7.start(`Pushing workflow to ${repo.fullName}...`);
576
+ try {
577
+ const [owner, repoName] = repo.fullName.split("/");
578
+ const workflowContent = generatePerRepoWorkflow(branch, repo.name, docsRepoUrl);
579
+ const contentBase64 = Buffer.from(workflowContent).toString("base64");
580
+ let existingSha;
581
+ try {
582
+ const { data } = await octokit.rest.repos.getContent({
583
+ owner,
584
+ repo: repoName,
585
+ path: workflowPath
586
+ });
587
+ if (!Array.isArray(data) && data.type === "file") {
588
+ existingSha = data.sha;
589
+ }
590
+ } catch {
591
+ }
592
+ await octokit.rest.repos.createOrUpdateFileContents({
593
+ owner,
594
+ repo: repoName,
595
+ path: workflowPath,
596
+ message: "Add auto-documentation CI workflow",
597
+ content: contentBase64,
598
+ ...existingSha ? { sha: existingSha } : {}
599
+ });
600
+ createdRepos.push(repo.fullName);
601
+ spinner7.stop(`Created workflow in ${repo.fullName}`);
602
+ } catch (err) {
603
+ spinner7.stop(`Failed for ${repo.fullName}`);
604
+ p4.log.warn(`Could not push workflow to ${repo.fullName}: ${err?.message || err}`);
605
+ }
606
+ }
607
+ if (createdRepos.length === 0) {
608
+ p4.log.error("Failed to create workflows in any repository.");
609
+ return null;
610
+ }
611
+ p4.log.success(`Created workflows in ${createdRepos.length}/${config.repos.length} repositories`);
612
+ return { repos: createdRepos, branch };
613
+ }
614
+ function showSecretsInstructions(multiRepo = false) {
615
+ const repoNote = multiRepo ? "Add these secrets to EACH source repository:" : "Add these secrets to your GitHub repository:";
616
+ p4.note(
617
+ [
618
+ repoNote,
619
+ "(Settings \u2192 Secrets and variables \u2192 Actions \u2192 New repository secret)",
620
+ "",
621
+ " ANTHROPIC_API_KEY \u2014 Your Anthropic API key",
622
+ " DOCS_DEPLOY_TOKEN \u2014 GitHub PAT with repo scope",
623
+ " (needed to push to the docs repo)",
624
+ "",
625
+ "To create the PAT:",
626
+ " 1. Go to https://github.com/settings/tokens",
627
+ " 2. Generate new token (classic) with 'repo' scope",
628
+ " 3. Copy the token and add it as DOCS_DEPLOY_TOKEN",
629
+ "",
630
+ "Note: GITHUB_TOKEN is automatically provided by GitHub Actions",
631
+ " (used for reading the source repo during analysis)."
632
+ ].join("\n"),
633
+ "Required GitHub Secrets"
634
+ );
635
+ }
636
+
637
+ // ../analyzer/dist/index.js
222
638
  import fs6 from "fs";
223
639
  import path6 from "path";
640
+ import ignore from "ignore";
641
+ import fs22 from "fs";
642
+ import path22 from "path";
643
+ import fs32 from "fs";
644
+ import path32 from "path";
645
+ import fs42 from "fs";
646
+ import path42 from "path";
647
+ import { query } from "@anthropic-ai/claude-agent-sdk";
648
+ import fs52 from "fs";
649
+ import path52 from "path";
650
+ import { execSync as execSync4 } from "child_process";
651
+ import fs62 from "fs";
652
+ import path62 from "path";
224
653
  var DEFAULT_IGNORES = [
225
654
  "node_modules",
226
655
  ".git",
@@ -242,26 +671,26 @@ function buildFileTree(rootPath, maxDepth = 6) {
242
671
  const ig = loadGitignore(rootPath);
243
672
  const flatFiles = [];
244
673
  function walk(dirPath, depth) {
245
- const name = path3.basename(dirPath);
246
- const node = { path: path3.relative(rootPath, dirPath) || ".", name, type: "directory", children: [] };
674
+ const name = path6.basename(dirPath);
675
+ const node = { path: path6.relative(rootPath, dirPath) || ".", name, type: "directory", children: [] };
247
676
  if (depth > maxDepth) return node;
248
677
  let entries;
249
678
  try {
250
- entries = fs3.readdirSync(dirPath, { withFileTypes: true });
679
+ entries = fs6.readdirSync(dirPath, { withFileTypes: true });
251
680
  } catch {
252
681
  return node;
253
682
  }
254
683
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
255
- const rel = path3.relative(rootPath, path3.join(dirPath, entry.name));
684
+ const rel = path6.relative(rootPath, path6.join(dirPath, entry.name));
256
685
  if (ig.ignores(rel) || ig.ignores(rel + "/")) continue;
257
686
  if (entry.isDirectory()) {
258
- const child = walk(path3.join(dirPath, entry.name), depth + 1);
687
+ const child = walk(path6.join(dirPath, entry.name), depth + 1);
259
688
  node.children.push(child);
260
689
  } else if (entry.isFile()) {
261
- const ext = path3.extname(entry.name).slice(1);
690
+ const ext = path6.extname(entry.name).slice(1);
262
691
  let size;
263
692
  try {
264
- size = fs3.statSync(path3.join(dirPath, entry.name)).size;
693
+ size = fs6.statSync(path6.join(dirPath, entry.name)).size;
265
694
  } catch {
266
695
  }
267
696
  flatFiles.push(rel);
@@ -282,9 +711,9 @@ function buildFileTree(rootPath, maxDepth = 6) {
282
711
  function loadGitignore(rootPath) {
283
712
  const ig = ignore.default();
284
713
  ig.add(DEFAULT_IGNORES);
285
- const gitignorePath = path3.join(rootPath, ".gitignore");
286
- if (fs3.existsSync(gitignorePath)) {
287
- const content = fs3.readFileSync(gitignorePath, "utf-8");
714
+ const gitignorePath = path6.join(rootPath, ".gitignore");
715
+ if (fs6.existsSync(gitignorePath)) {
716
+ const content = fs6.readFileSync(gitignorePath, "utf-8");
288
717
  ig.add(content);
289
718
  }
290
719
  return ig;
@@ -312,7 +741,7 @@ function detectLanguages(flatFiles) {
312
741
  };
313
742
  const langs = /* @__PURE__ */ new Set();
314
743
  for (const file of flatFiles) {
315
- const ext = path3.extname(file).slice(1);
744
+ const ext = path6.extname(file).slice(1);
316
745
  if (extMap[ext]) langs.add(extMap[ext]);
317
746
  }
318
747
  return Array.from(langs);
@@ -332,7 +761,7 @@ function detectEntryFiles(flatFiles) {
332
761
  /^app\/page\.tsx$/,
333
762
  /^pages\/index\.\w+$/
334
763
  ];
335
- return flatFiles.filter((f) => entryPatterns.some((p9) => p9.test(f)));
764
+ return flatFiles.filter((f) => entryPatterns.some((p11) => p11.test(f)));
336
765
  }
337
766
  var DEP_FILES = [
338
767
  {
@@ -494,14 +923,14 @@ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
494
923
  ]);
495
924
  var MAX_FILES = 200;
496
925
  function buildImportGraph(repoPath, flatFiles) {
497
- const sourceFiles = flatFiles.filter((f) => SOURCE_EXTENSIONS.has(path4.extname(f))).slice(0, MAX_FILES);
926
+ const sourceFiles = flatFiles.filter((f) => SOURCE_EXTENSIONS.has(path42.extname(f))).slice(0, MAX_FILES);
498
927
  const edges = [];
499
928
  const fileSet = new Set(flatFiles);
500
929
  for (const file of sourceFiles) {
501
- const fullPath = path4.join(repoPath, file);
930
+ const fullPath = path42.join(repoPath, file);
502
931
  let content;
503
932
  try {
504
- content = fs4.readFileSync(fullPath, "utf-8");
933
+ content = fs42.readFileSync(fullPath, "utf-8");
505
934
  } catch {
506
935
  continue;
507
936
  }
@@ -527,14 +956,14 @@ function buildImportGraph(repoPath, flatFiles) {
527
956
  return { edges, moduleClusters };
528
957
  }
529
958
  function resolveImport(repoPath, fromFile, importPath, fileSet) {
530
- const dir = path4.dirname(fromFile);
531
- const resolved = path4.normalize(path4.join(dir, importPath));
959
+ const dir = path42.dirname(fromFile);
960
+ const resolved = path42.normalize(path42.join(dir, importPath));
532
961
  if (fileSet.has(resolved)) return resolved;
533
962
  for (const ext of [".ts", ".tsx", ".js", ".jsx", ".mjs"]) {
534
963
  if (fileSet.has(resolved + ext)) return resolved + ext;
535
964
  }
536
965
  for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
537
- const indexPath = path4.join(resolved, `index${ext}`);
966
+ const indexPath = path42.join(resolved, `index${ext}`);
538
967
  if (fileSet.has(indexPath)) return indexPath;
539
968
  }
540
969
  return null;
@@ -1137,8 +1566,8 @@ Do NOT read more than 10-12 files total.`,
1137
1566
  outputSchema: repoInitSchema,
1138
1567
  maxTurns: 15
1139
1568
  });
1140
- const claudeMdPath = path5.join(repoPath, "CLAUDE.md");
1141
- fs5.writeFileSync(claudeMdPath, result.content, "utf-8");
1569
+ const claudeMdPath = path52.join(repoPath, "CLAUDE.md");
1570
+ fs52.writeFileSync(claudeMdPath, result.content, "utf-8");
1142
1571
  const updatedClaudeMd = readClaudeMd(repoPath);
1143
1572
  return {
1144
1573
  ...staticAnalysis,
@@ -1198,7 +1627,7 @@ var SECTION_PATTERNS = {
1198
1627
  };
1199
1628
  function computeDiff(repoPath, fromSha) {
1200
1629
  try {
1201
- const output = execSync2(`git diff --name-status ${fromSha}..HEAD`, {
1630
+ const output = execSync4(`git diff --name-status ${fromSha}..HEAD`, {
1202
1631
  cwd: repoPath,
1203
1632
  encoding: "utf-8",
1204
1633
  stdio: ["pipe", "pipe", "pipe"]
@@ -1233,7 +1662,7 @@ function classifyChanges(entries, staticAnalysis) {
1233
1662
  const affected = /* @__PURE__ */ new Set();
1234
1663
  for (const entry of entries) {
1235
1664
  for (const [section, patterns] of Object.entries(SECTION_PATTERNS)) {
1236
- if (patterns.some((p9) => p9.test(entry.filePath))) {
1665
+ if (patterns.some((p11) => p11.test(entry.filePath))) {
1237
1666
  affected.add(section);
1238
1667
  }
1239
1668
  }
@@ -1251,7 +1680,7 @@ function classifyChanges(entries, staticAnalysis) {
1251
1680
  };
1252
1681
  }
1253
1682
  function getHeadSha(repoPath) {
1254
- return execSync2("git rev-parse HEAD", {
1683
+ return execSync4("git rev-parse HEAD", {
1255
1684
  cwd: repoPath,
1256
1685
  encoding: "utf-8"
1257
1686
  }).trim();
@@ -1602,23 +2031,23 @@ function slugify(name) {
1602
2031
  return name.replace(/[^a-zA-Z0-9_-]/g, "_");
1603
2032
  }
1604
2033
  function cacheFilePath(cacheDir, repoSlug) {
1605
- return path6.join(cacheDir, `${slugify(repoSlug)}-analysis.json`);
2034
+ return path62.join(cacheDir, `${slugify(repoSlug)}-analysis.json`);
1606
2035
  }
1607
2036
  function saveCache(cacheDir, repoSlug, commitSha, result) {
1608
- fs6.mkdirSync(cacheDir, { recursive: true });
2037
+ fs62.mkdirSync(cacheDir, { recursive: true });
1609
2038
  const cache = {
1610
2039
  version: CACHE_VERSION,
1611
2040
  commitSha,
1612
2041
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1613
2042
  result
1614
2043
  };
1615
- fs6.writeFileSync(cacheFilePath(cacheDir, repoSlug), JSON.stringify(cache), "utf-8");
2044
+ fs62.writeFileSync(cacheFilePath(cacheDir, repoSlug), JSON.stringify(cache), "utf-8");
1616
2045
  }
1617
2046
  function loadCache(cacheDir, repoSlug) {
1618
2047
  const filePath = cacheFilePath(cacheDir, repoSlug);
1619
- if (!fs6.existsSync(filePath)) return null;
2048
+ if (!fs62.existsSync(filePath)) return null;
1620
2049
  try {
1621
- const raw = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
2050
+ const raw = JSON.parse(fs62.readFileSync(filePath, "utf-8"));
1622
2051
  if (raw.version !== CACHE_VERSION) return null;
1623
2052
  if (!raw.commitSha || !raw.result) return null;
1624
2053
  return raw;
@@ -1630,7 +2059,7 @@ function loadCache(cacheDir, repoSlug) {
1630
2059
  // ../generator/dist/index.js
1631
2060
  import fs7 from "fs-extra";
1632
2061
  import path7 from "path";
1633
- import { execSync as execSync3 } from "child_process";
2062
+ import { execSync as execSync5 } from "child_process";
1634
2063
  import fs23 from "fs-extra";
1635
2064
  import path23 from "path";
1636
2065
  import Handlebars from "handlebars";
@@ -1659,7 +2088,7 @@ async function scaffoldSite(outputDir, projectName, templateDir) {
1659
2088
  const nodeModulesPath = path7.join(outputDir, "node_modules");
1660
2089
  if (!fs7.existsSync(nodeModulesPath)) {
1661
2090
  try {
1662
- execSync3("npm install --ignore-scripts", {
2091
+ execSync5("npm install --ignore-scripts", {
1663
2092
  cwd: outputDir,
1664
2093
  stdio: "pipe",
1665
2094
  timeout: 12e4
@@ -1671,7 +2100,7 @@ async function scaffoldSite(outputDir, projectName, templateDir) {
1671
2100
  }
1672
2101
  }
1673
2102
  try {
1674
- execSync3("npx fumadocs-mdx", { cwd: outputDir, stdio: "pipe", timeout: 3e4 });
2103
+ execSync5("npx fumadocs-mdx", { cwd: outputDir, stdio: "pipe", timeout: 3e4 });
1675
2104
  } catch {
1676
2105
  }
1677
2106
  }
@@ -2136,41 +2565,41 @@ function slugify22(name) {
2136
2565
  // src/commands/init.ts
2137
2566
  var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
2138
2567
  async function initCommand(options) {
2139
- p3.intro("open-auto-doc \u2014 AI-powered documentation generator");
2568
+ p5.intro("open-auto-doc \u2014 AI-powered documentation generator");
2140
2569
  let token = getGithubToken();
2141
2570
  if (!token) {
2142
- p3.log.info("Let's connect your GitHub account.");
2571
+ p5.log.info("Let's connect your GitHub account.");
2143
2572
  token = await authenticateWithGithub();
2144
2573
  setGithubToken(token);
2145
2574
  } else {
2146
- p3.log.success("Using saved GitHub credentials.");
2575
+ p5.log.success("Using saved GitHub credentials.");
2147
2576
  }
2148
2577
  const repos = await pickRepos(token);
2149
- p3.log.info(`Selected ${repos.length} ${repos.length === 1 ? "repository" : "repositories"}`);
2578
+ p5.log.info(`Selected ${repos.length} ${repos.length === 1 ? "repository" : "repositories"}`);
2150
2579
  let apiKey = getAnthropicKey();
2151
2580
  if (!apiKey) {
2152
- const keyInput = await p3.text({
2581
+ const keyInput = await p5.text({
2153
2582
  message: "Enter your Anthropic API key",
2154
2583
  placeholder: "sk-ant-...",
2155
2584
  validate: (v) => {
2156
2585
  if (!v || !v.startsWith("sk-ant-")) return "Please enter a valid Anthropic API key";
2157
2586
  }
2158
2587
  });
2159
- if (p3.isCancel(keyInput)) {
2160
- p3.cancel("Operation cancelled");
2588
+ if (p5.isCancel(keyInput)) {
2589
+ p5.cancel("Operation cancelled");
2161
2590
  process.exit(0);
2162
2591
  }
2163
2592
  apiKey = keyInput;
2164
- const saveKey = await p3.confirm({
2593
+ const saveKey = await p5.confirm({
2165
2594
  message: "Save API key for future use?"
2166
2595
  });
2167
- if (saveKey && !p3.isCancel(saveKey)) {
2596
+ if (saveKey && !p5.isCancel(saveKey)) {
2168
2597
  setAnthropicKey(apiKey);
2169
2598
  }
2170
2599
  } else {
2171
- p3.log.success("Using saved Anthropic API key.");
2600
+ p5.log.success("Using saved Anthropic API key.");
2172
2601
  }
2173
- const model = await p3.select({
2602
+ const model = await p5.select({
2174
2603
  message: "Which model should analyze your repos?",
2175
2604
  options: [
2176
2605
  { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", hint: "Fast & capable (recommended)" },
@@ -2178,12 +2607,12 @@ async function initCommand(options) {
2178
2607
  { value: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Most capable, slowest" }
2179
2608
  ]
2180
2609
  });
2181
- if (p3.isCancel(model)) {
2182
- p3.cancel("Operation cancelled");
2610
+ if (p5.isCancel(model)) {
2611
+ p5.cancel("Operation cancelled");
2183
2612
  process.exit(0);
2184
2613
  }
2185
- p3.log.info(`Using ${model}`);
2186
- const cloneSpinner = p3.spinner();
2614
+ p5.log.info(`Using ${model}`);
2615
+ const cloneSpinner = p5.spinner();
2187
2616
  cloneSpinner.start(`Cloning ${repos.length} repositories...`);
2188
2617
  const clones = [];
2189
2618
  for (const repo of repos) {
@@ -2192,15 +2621,15 @@ async function initCommand(options) {
2192
2621
  const cloned = cloneRepo(repo, token);
2193
2622
  clones.push(cloned);
2194
2623
  } catch (err) {
2195
- p3.log.warn(`Failed to clone ${repo.name}: ${err instanceof Error ? err.message : err}`);
2624
+ p5.log.warn(`Failed to clone ${repo.name}: ${err instanceof Error ? err.message : err}`);
2196
2625
  }
2197
2626
  }
2198
2627
  cloneSpinner.stop(`Cloned ${clones.length}/${repos.length} repositories`);
2199
2628
  if (clones.length === 0) {
2200
- p3.log.error("No repositories were cloned.");
2629
+ p5.log.error("No repositories were cloned.");
2201
2630
  process.exit(1);
2202
2631
  }
2203
- const analyzeSpinner = p3.spinner();
2632
+ const analyzeSpinner = p5.spinner();
2204
2633
  let completed = 0;
2205
2634
  const total = clones.length;
2206
2635
  const repoStages = {};
@@ -2231,7 +2660,7 @@ async function initCommand(options) {
2231
2660
  completed++;
2232
2661
  delete repoStages[repoName];
2233
2662
  updateSpinner();
2234
- p3.log.warn(`[${repoName}] Analysis failed: ${err instanceof Error ? err.message : err}`);
2663
+ p5.log.warn(`[${repoName}] Analysis failed: ${err instanceof Error ? err.message : err}`);
2235
2664
  return { repo: repoName, result: null };
2236
2665
  }
2237
2666
  });
@@ -2241,13 +2670,13 @@ async function initCommand(options) {
2241
2670
  `Analyzed ${results.length}/${total} repositories` + (results.length > 0 ? ` \u2014 ${results.reduce((n, r) => n + r.apiEndpoints.length, 0)} endpoints, ${results.reduce((n, r) => n + r.components.length, 0)} components, ${results.reduce((n, r) => n + r.diagrams.length, 0)} diagrams` : "")
2242
2671
  );
2243
2672
  if (results.length === 0) {
2244
- p3.log.error("No repositories were successfully analyzed.");
2673
+ p5.log.error("No repositories were successfully analyzed.");
2245
2674
  cleanup(clones);
2246
2675
  process.exit(1);
2247
2676
  }
2248
2677
  let crossRepo;
2249
2678
  if (results.length > 1) {
2250
- const crossSpinner = p3.spinner();
2679
+ const crossSpinner = p5.spinner();
2251
2680
  crossSpinner.start("Analyzing cross-repository relationships...");
2252
2681
  try {
2253
2682
  crossRepo = await analyzeCrossRepos(results, apiKey, model, (text4) => {
@@ -2256,21 +2685,21 @@ async function initCommand(options) {
2256
2685
  crossSpinner.stop(`Cross-repo analysis complete \u2014 ${crossRepo.repoRelationships.length} relationships found`);
2257
2686
  } catch (err) {
2258
2687
  crossSpinner.stop("Cross-repo analysis failed (non-fatal)");
2259
- p3.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
2688
+ p5.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
2260
2689
  }
2261
2690
  }
2262
2691
  const outputDir = path8.resolve(options.output || "docs-site");
2263
2692
  const projectName = results.length === 1 ? results[0].repoName : "My Project";
2264
- const genSpinner = p3.spinner();
2693
+ const genSpinner = p5.spinner();
2265
2694
  try {
2266
2695
  genSpinner.start("Scaffolding documentation site...");
2267
2696
  const templateDir = resolveTemplateDir();
2268
- p3.log.info(`Using template from: ${templateDir}`);
2697
+ p5.log.info(`Using template from: ${templateDir}`);
2269
2698
  await scaffoldSite(outputDir, projectName, templateDir);
2270
2699
  genSpinner.stop("Site scaffolded");
2271
2700
  } catch (err) {
2272
2701
  genSpinner.stop("Scaffold failed");
2273
- p3.log.error(`Scaffold error: ${err instanceof Error ? err.stack || err.message : err}`);
2702
+ p5.log.error(`Scaffold error: ${err instanceof Error ? err.stack || err.message : err}`);
2274
2703
  cleanup(clones);
2275
2704
  process.exit(1);
2276
2705
  }
@@ -2282,34 +2711,76 @@ async function initCommand(options) {
2282
2711
  genSpinner.stop("Documentation content written");
2283
2712
  } catch (err) {
2284
2713
  genSpinner.stop("Content writing failed");
2285
- p3.log.error(`Content error: ${err instanceof Error ? err.stack || err.message : err}`);
2714
+ p5.log.error(`Content error: ${err instanceof Error ? err.stack || err.message : err}`);
2286
2715
  cleanup(clones);
2287
2716
  process.exit(1);
2288
2717
  }
2718
+ const config = {
2719
+ repos: repos.map((r) => ({
2720
+ name: r.name,
2721
+ fullName: r.fullName,
2722
+ cloneUrl: r.cloneUrl,
2723
+ htmlUrl: r.htmlUrl
2724
+ })),
2725
+ outputDir
2726
+ };
2289
2727
  try {
2290
- const config = {
2291
- repos: repos.map((r) => ({
2292
- name: r.name,
2293
- fullName: r.fullName,
2294
- cloneUrl: r.cloneUrl,
2295
- htmlUrl: r.htmlUrl
2296
- })),
2297
- outputDir
2298
- };
2299
- const configJson = JSON.stringify(config, null, 2);
2300
- fs8.writeFileSync(path8.join(outputDir, ".autodocrc.json"), configJson);
2301
- const cwdConfig = path8.resolve(".autodocrc.json");
2302
- if (cwdConfig !== path8.join(outputDir, ".autodocrc.json")) {
2303
- fs8.writeFileSync(cwdConfig, configJson);
2304
- }
2728
+ saveConfig(config);
2305
2729
  } catch {
2306
2730
  }
2307
2731
  cleanup(clones);
2308
- p3.note(
2309
- `cd ${path8.relative(process.cwd(), outputDir)} && npm run dev`,
2310
- "Next steps"
2311
- );
2312
- p3.outro("Documentation generated successfully!");
2732
+ p5.log.success("Documentation generated successfully!");
2733
+ const shouldDeploy = await p5.confirm({
2734
+ message: "Would you like to deploy your docs to GitHub?"
2735
+ });
2736
+ if (p5.isCancel(shouldDeploy) || !shouldDeploy) {
2737
+ p5.note(
2738
+ `cd ${path8.relative(process.cwd(), outputDir)} && npm run dev`,
2739
+ "Next steps"
2740
+ );
2741
+ p5.outro("Done!");
2742
+ return;
2743
+ }
2744
+ const deployResult = await createAndPushDocsRepo({
2745
+ token,
2746
+ docsDir: outputDir,
2747
+ config
2748
+ });
2749
+ if (!deployResult) {
2750
+ p5.note(
2751
+ `cd ${path8.relative(process.cwd(), outputDir)} && npm run dev`,
2752
+ "Next steps"
2753
+ );
2754
+ p5.outro("Done!");
2755
+ return;
2756
+ }
2757
+ const shouldSetupCi = await p5.confirm({
2758
+ message: "Would you like to set up CI to auto-update docs on every push?"
2759
+ });
2760
+ if (p5.isCancel(shouldSetupCi) || !shouldSetupCi) {
2761
+ showVercelInstructions(deployResult.owner, deployResult.repoName);
2762
+ p5.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
2763
+ return;
2764
+ }
2765
+ const gitRoot = getGitRoot();
2766
+ if (!gitRoot) {
2767
+ p5.log.warn("Not in a git repository \u2014 skipping CI setup. Run `open-auto-doc setup-ci` from your project root later.");
2768
+ showVercelInstructions(deployResult.owner, deployResult.repoName);
2769
+ p5.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
2770
+ return;
2771
+ }
2772
+ const ciResult = await createCiWorkflow({
2773
+ gitRoot,
2774
+ docsRepoUrl: deployResult.repoUrl,
2775
+ outputDir,
2776
+ token,
2777
+ config
2778
+ });
2779
+ if (ciResult) {
2780
+ showSecretsInstructions(repos.length > 1);
2781
+ }
2782
+ showVercelInstructions(deployResult.owner, deployResult.repoName);
2783
+ p5.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
2313
2784
  }
2314
2785
  function resolveTemplateDir() {
2315
2786
  const candidates = [
@@ -2330,31 +2801,26 @@ function cleanup(clones) {
2330
2801
  }
2331
2802
 
2332
2803
  // src/commands/generate.ts
2333
- import * as p4 from "@clack/prompts";
2334
- import fs9 from "fs";
2804
+ import * as p6 from "@clack/prompts";
2335
2805
  import path9 from "path";
2336
2806
  async function generateCommand(options) {
2337
- p4.intro("open-auto-doc \u2014 Regenerating documentation");
2338
- let configPath = path9.resolve(".autodocrc.json");
2339
- if (!fs9.existsSync(configPath)) {
2340
- configPath = path9.resolve("docs-site", ".autodocrc.json");
2341
- }
2342
- if (!fs9.existsSync(configPath)) {
2343
- p4.log.error("No .autodocrc.json found. Run `open-auto-doc init` first.");
2807
+ p6.intro("open-auto-doc \u2014 Regenerating documentation");
2808
+ const config = loadConfig();
2809
+ if (!config) {
2810
+ p6.log.error("No .autodocrc.json found. Run `open-auto-doc init` first.");
2344
2811
  process.exit(1);
2345
2812
  }
2346
- const config = JSON.parse(fs9.readFileSync(configPath, "utf-8"));
2347
2813
  const token = getGithubToken();
2348
2814
  const apiKey = getAnthropicKey();
2349
2815
  if (!token) {
2350
- p4.log.error("Not authenticated. Run `open-auto-doc login` or set GITHUB_TOKEN env var.");
2816
+ p6.log.error("Not authenticated. Run `open-auto-doc login` or set GITHUB_TOKEN env var.");
2351
2817
  process.exit(1);
2352
2818
  }
2353
2819
  if (!apiKey) {
2354
- p4.log.error("No Anthropic API key found. Run `open-auto-doc init` or set ANTHROPIC_API_KEY env var.");
2820
+ p6.log.error("No Anthropic API key found. Run `open-auto-doc init` or set ANTHROPIC_API_KEY env var.");
2355
2821
  process.exit(1);
2356
2822
  }
2357
- const model = await p4.select({
2823
+ const model = await p6.select({
2358
2824
  message: "Which model should analyze your repos?",
2359
2825
  options: [
2360
2826
  { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", hint: "Fast & capable (recommended)" },
@@ -2362,17 +2828,38 @@ async function generateCommand(options) {
2362
2828
  { value: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Most capable, slowest" }
2363
2829
  ]
2364
2830
  });
2365
- if (p4.isCancel(model)) {
2366
- p4.cancel("Cancelled.");
2831
+ if (p6.isCancel(model)) {
2832
+ p6.cancel("Cancelled.");
2367
2833
  process.exit(0);
2368
2834
  }
2369
- p4.log.info(`Using ${model}`);
2835
+ p6.log.info(`Using ${model}`);
2370
2836
  const incremental = options.incremental && !options.force;
2371
2837
  const cacheDir = path9.join(config.outputDir, ".autodoc-cache");
2372
- const cloneSpinner = p4.spinner();
2373
- cloneSpinner.start(`Cloning ${config.repos.length} repositories...`);
2838
+ const targetRepoName = options.repo;
2839
+ let reposToAnalyze = config.repos;
2840
+ const cachedResults = [];
2841
+ if (targetRepoName) {
2842
+ const targetRepo = config.repos.find((r) => r.name === targetRepoName);
2843
+ if (!targetRepo) {
2844
+ p6.log.error(`Repo "${targetRepoName}" not found in config. Available: ${config.repos.map((r) => r.name).join(", ")}`);
2845
+ process.exit(1);
2846
+ }
2847
+ reposToAnalyze = [targetRepo];
2848
+ for (const repo of config.repos) {
2849
+ if (repo.name === targetRepoName) continue;
2850
+ const cached = loadCache(cacheDir, repo.name);
2851
+ if (cached) {
2852
+ cachedResults.push(cached.result);
2853
+ p6.log.info(`Using cached analysis for ${repo.name}`);
2854
+ } else {
2855
+ p6.log.warn(`No cached analysis for ${repo.name} \u2014 its docs will be stale until it pushes`);
2856
+ }
2857
+ }
2858
+ }
2859
+ const cloneSpinner = p6.spinner();
2860
+ cloneSpinner.start(`Cloning ${reposToAnalyze.length} ${reposToAnalyze.length === 1 ? "repository" : "repositories"}...`);
2374
2861
  const clones = [];
2375
- for (const repo of config.repos) {
2862
+ for (const repo of reposToAnalyze) {
2376
2863
  cloneSpinner.message(`Cloning ${repo.name}...`);
2377
2864
  try {
2378
2865
  const cloned = cloneRepo(
@@ -2388,15 +2875,15 @@ async function generateCommand(options) {
2388
2875
  );
2389
2876
  clones.push(cloned);
2390
2877
  } catch (err) {
2391
- p4.log.warn(`Failed to clone ${repo.name}: ${err instanceof Error ? err.message : err}`);
2878
+ p6.log.warn(`Failed to clone ${repo.name}: ${err instanceof Error ? err.message : err}`);
2392
2879
  }
2393
2880
  }
2394
- cloneSpinner.stop(`Cloned ${clones.length}/${config.repos.length} repositories`);
2881
+ cloneSpinner.stop(`Cloned ${clones.length}/${reposToAnalyze.length} ${reposToAnalyze.length === 1 ? "repository" : "repositories"}`);
2395
2882
  if (clones.length === 0) {
2396
- p4.log.error("No repositories were cloned.");
2883
+ p6.log.error("No repositories were cloned.");
2397
2884
  process.exit(1);
2398
2885
  }
2399
- const analyzeSpinner = p4.spinner();
2886
+ const analyzeSpinner = p6.spinner();
2400
2887
  let completed = 0;
2401
2888
  const total = clones.length;
2402
2889
  const repoStages = {};
@@ -2462,17 +2949,18 @@ async function generateCommand(options) {
2462
2949
  completed++;
2463
2950
  delete repoStages[repoName];
2464
2951
  updateSpinner();
2465
- p4.log.warn(`[${repoName}] Analysis failed: ${err instanceof Error ? err.message : err}`);
2952
+ p6.log.warn(`[${repoName}] Analysis failed: ${err instanceof Error ? err.message : err}`);
2466
2953
  return { repo: repoName, result: null };
2467
2954
  }
2468
2955
  });
2469
2956
  const settled = await Promise.all(analysisPromises);
2470
- const results = settled.filter((s) => s.result !== null).map((s) => s.result);
2471
- analyzeSpinner.stop(`Analyzed ${results.length}/${total} repositories`);
2957
+ const freshResults = settled.filter((s) => s.result !== null).map((s) => s.result);
2958
+ analyzeSpinner.stop(`Analyzed ${freshResults.length}/${total} ${total === 1 ? "repository" : "repositories"}`);
2959
+ const results = [...freshResults, ...cachedResults];
2472
2960
  if (results.length > 0) {
2473
2961
  let crossRepo;
2474
2962
  if (results.length > 1) {
2475
- const crossSpinner = p4.spinner();
2963
+ const crossSpinner = p6.spinner();
2476
2964
  crossSpinner.start("Analyzing cross-repository relationships...");
2477
2965
  try {
2478
2966
  crossRepo = await analyzeCrossRepos(results, apiKey, model, (text4) => {
@@ -2481,396 +2969,150 @@ async function generateCommand(options) {
2481
2969
  crossSpinner.stop(`Cross-repo analysis complete \u2014 ${crossRepo.repoRelationships.length} relationships found`);
2482
2970
  } catch (err) {
2483
2971
  crossSpinner.stop("Cross-repo analysis failed (non-fatal)");
2484
- p4.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
2972
+ p6.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
2485
2973
  }
2486
2974
  }
2487
2975
  const contentDir = path9.join(config.outputDir, "content", "docs");
2488
2976
  await writeContent(contentDir, results, crossRepo);
2489
2977
  await writeMeta(contentDir, results, crossRepo);
2490
- p4.log.success("Documentation regenerated!");
2978
+ p6.log.success("Documentation regenerated!");
2491
2979
  }
2492
2980
  for (const clone of clones) {
2493
2981
  cleanupClone(clone);
2494
2982
  }
2495
- p4.outro("Done!");
2983
+ p6.outro("Done!");
2496
2984
  }
2497
2985
 
2498
2986
  // src/commands/deploy.ts
2499
- import * as p5 from "@clack/prompts";
2500
- import { execSync as execSync4 } from "child_process";
2501
- import fs10 from "fs";
2987
+ import * as p7 from "@clack/prompts";
2988
+ import fs9 from "fs";
2502
2989
  import path10 from "path";
2503
- import { Octokit as Octokit2 } from "@octokit/rest";
2504
- function loadConfig() {
2505
- for (const candidate of [
2506
- path10.resolve(".autodocrc.json"),
2507
- path10.resolve("docs-site", ".autodocrc.json")
2508
- ]) {
2509
- if (fs10.existsSync(candidate)) {
2510
- try {
2511
- return JSON.parse(fs10.readFileSync(candidate, "utf-8"));
2512
- } catch {
2513
- }
2514
- }
2515
- }
2516
- return null;
2517
- }
2518
- function saveConfig(config) {
2519
- fs10.writeFileSync(
2520
- path10.resolve(".autodocrc.json"),
2521
- JSON.stringify(config, null, 2)
2522
- );
2523
- if (config.outputDir && fs10.existsSync(config.outputDir)) {
2524
- fs10.writeFileSync(
2525
- path10.join(config.outputDir, ".autodocrc.json"),
2526
- JSON.stringify(config, null, 2)
2527
- );
2528
- }
2529
- }
2530
2990
  function resolveDocsDir(config, dirOption) {
2531
2991
  if (dirOption) {
2532
2992
  const resolved = path10.resolve(dirOption);
2533
- if (!fs10.existsSync(resolved)) {
2534
- p5.log.error(`Directory not found: ${resolved}`);
2993
+ if (!fs9.existsSync(resolved)) {
2994
+ p7.log.error(`Directory not found: ${resolved}`);
2535
2995
  process.exit(1);
2536
2996
  }
2537
2997
  return resolved;
2538
2998
  }
2539
- if (config?.outputDir && fs10.existsSync(path10.resolve(config.outputDir))) {
2999
+ if (config?.outputDir && fs9.existsSync(path10.resolve(config.outputDir))) {
2540
3000
  return path10.resolve(config.outputDir);
2541
3001
  }
2542
- if (fs10.existsSync(path10.resolve("docs-site"))) {
3002
+ if (fs9.existsSync(path10.resolve("docs-site"))) {
2543
3003
  return path10.resolve("docs-site");
2544
3004
  }
2545
- p5.log.error(
3005
+ p7.log.error(
2546
3006
  "Could not find docs site directory. Use --dir to specify the path, or run `open-auto-doc init` first."
2547
3007
  );
2548
3008
  process.exit(1);
2549
3009
  }
2550
- function getGitHubUsername(octokit) {
2551
- return octokit.rest.users.getAuthenticated().then((res) => res.data.login);
2552
- }
2553
- function exec(cmd, cwd) {
2554
- return execSync4(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2555
- }
2556
3010
  async function deployCommand(options) {
2557
- p5.intro("open-auto-doc \u2014 Deploy docs to GitHub");
3011
+ p7.intro("open-auto-doc \u2014 Deploy docs to GitHub");
2558
3012
  const token = getGithubToken();
2559
3013
  if (!token) {
2560
- p5.log.error("Not authenticated. Run `open-auto-doc login` first.");
3014
+ p7.log.error("Not authenticated. Run `open-auto-doc login` first.");
2561
3015
  process.exit(1);
2562
3016
  }
2563
3017
  const config = loadConfig();
2564
3018
  const docsDir = resolveDocsDir(config, options.dir);
2565
- p5.log.info(`Docs directory: ${docsDir}`);
2566
- const octokit = new Octokit2({ auth: token });
3019
+ p7.log.info(`Docs directory: ${docsDir}`);
2567
3020
  if (config?.docsRepo) {
2568
- p5.log.info(`Docs repo already configured: ${config.docsRepo}`);
2569
- const spinner7 = p5.spinner();
2570
- spinner7.start("Pushing updates to docs repo...");
2571
- try {
2572
- if (!fs10.existsSync(path10.join(docsDir, ".git"))) {
2573
- exec("git init", docsDir);
2574
- exec(`git remote add origin ${config.docsRepo}`, docsDir);
2575
- }
2576
- exec("git add -A", docsDir);
2577
- try {
2578
- exec("git diff --cached --quiet", docsDir);
2579
- spinner7.stop("No changes to push.");
2580
- p5.outro("Docs are up to date!");
2581
- return;
2582
- } catch {
2583
- }
2584
- exec('git commit -m "Update documentation"', docsDir);
2585
- exec("git push -u origin main", docsDir);
2586
- spinner7.stop("Pushed updates to docs repo.");
2587
- } catch (err) {
2588
- spinner7.stop("Push failed.");
2589
- p5.log.error(`${err instanceof Error ? err.message : err}`);
2590
- process.exit(1);
3021
+ p7.log.info(`Docs repo already configured: ${config.docsRepo}`);
3022
+ const pushed = await pushUpdates({ token, docsDir, docsRepo: config.docsRepo });
3023
+ if (pushed) {
3024
+ p7.outro("Docs updated! Vercel will auto-deploy from the push.");
3025
+ } else {
3026
+ p7.outro("Docs are up to date!");
2591
3027
  }
2592
- p5.outro("Docs updated! Vercel will auto-deploy from the push.");
2593
3028
  return;
2594
3029
  }
2595
- const username = await getGitHubUsername(octokit);
2596
- const defaultName = config?.repos?.[0] ? `${config.repos[0].name}-docs` : "my-project-docs";
2597
- const repoName = await p5.text({
2598
- message: "Name for the docs GitHub repo:",
2599
- initialValue: defaultName,
2600
- validate: (v) => {
2601
- if (!v || v.length === 0) return "Repo name is required";
2602
- if (!/^[a-zA-Z0-9._-]+$/.test(v)) return "Invalid repo name";
2603
- }
2604
- });
2605
- if (p5.isCancel(repoName)) {
2606
- p5.cancel("Deploy cancelled.");
2607
- process.exit(0);
2608
- }
2609
- const visibility = await p5.select({
2610
- message: "Repository visibility:",
2611
- options: [
2612
- { value: "public", label: "Public" },
2613
- { value: "private", label: "Private" }
2614
- ]
3030
+ const result = await createAndPushDocsRepo({
3031
+ token,
3032
+ docsDir,
3033
+ config: config || { repos: [], outputDir: docsDir }
2615
3034
  });
2616
- if (p5.isCancel(visibility)) {
2617
- p5.cancel("Deploy cancelled.");
3035
+ if (!result) {
3036
+ p7.cancel("Deploy cancelled.");
2618
3037
  process.exit(0);
2619
3038
  }
2620
- const spinner6 = p5.spinner();
2621
- spinner6.start(`Creating GitHub repo ${username}/${repoName}...`);
2622
- let repoUrl;
2623
- try {
2624
- const { data } = await octokit.rest.repos.createForAuthenticatedUser({
2625
- name: repoName,
2626
- private: visibility === "private",
2627
- description: "Auto-generated documentation site",
2628
- auto_init: false
2629
- });
2630
- repoUrl = data.clone_url;
2631
- spinner6.stop(`Created ${data.full_name}`);
2632
- } catch (err) {
2633
- spinner6.stop("Failed to create repo.");
2634
- if (err?.status === 422) {
2635
- p5.log.error(`Repository "${repoName}" already exists. Choose a different name or delete it first.`);
2636
- } else {
2637
- p5.log.error(`GitHub API error: ${err?.message || err}`);
2638
- }
2639
- process.exit(1);
2640
- }
2641
- spinner6.start("Pushing docs to GitHub...");
2642
- try {
2643
- const gitignorePath = path10.join(docsDir, ".gitignore");
2644
- if (!fs10.existsSync(gitignorePath)) {
2645
- fs10.writeFileSync(gitignorePath, "node_modules/\n.next/\n.source/\n");
2646
- }
2647
- if (!fs10.existsSync(path10.join(docsDir, ".git"))) {
2648
- exec("git init -b main", docsDir);
2649
- }
2650
- exec("git add -A", docsDir);
2651
- exec('git commit -m "Initial documentation site"', docsDir);
2652
- try {
2653
- exec("git remote remove origin", docsDir);
2654
- } catch {
2655
- }
2656
- const pushUrl = repoUrl.replace("https://", `https://${token}@`);
2657
- exec(`git remote add origin ${pushUrl}`, docsDir);
2658
- exec("git push -u origin main", docsDir);
2659
- exec("git remote set-url origin " + repoUrl, docsDir);
2660
- spinner6.stop("Pushed to GitHub.");
2661
- } catch (err) {
2662
- spinner6.stop("Git push failed.");
2663
- p5.log.error(`${err instanceof Error ? err.message : err}`);
2664
- process.exit(1);
2665
- }
2666
- const updatedConfig = config || {
2667
- repos: [],
2668
- outputDir: docsDir
2669
- };
2670
- updatedConfig.docsRepo = repoUrl;
2671
- saveConfig(updatedConfig);
2672
- p5.note(
2673
- [
2674
- "Connect your docs repo to Vercel for automatic deployments:",
2675
- "",
2676
- " 1. Go to https://vercel.com/new",
2677
- " 2. Click 'Import Git Repository'",
2678
- ` 3. Select '${username}/${repoName}'`,
2679
- " 4. Click 'Deploy'",
2680
- "",
2681
- "Once connected, Vercel will auto-deploy on every push to the docs repo."
2682
- ].join("\n"),
2683
- "Vercel Setup"
2684
- );
2685
- p5.outro(`Docs repo: https://github.com/${username}/${repoName}`);
3039
+ showVercelInstructions(result.owner, result.repoName);
3040
+ p7.outro(`Docs repo: https://github.com/${result.owner}/${result.repoName}`);
2686
3041
  }
2687
3042
 
2688
3043
  // src/commands/setup-ci.ts
2689
- import * as p6 from "@clack/prompts";
2690
- import { execSync as execSync5 } from "child_process";
2691
- import fs11 from "fs";
2692
- import path11 from "path";
2693
- function getGitRoot() {
2694
- try {
2695
- return execSync5("git rev-parse --show-toplevel", {
2696
- encoding: "utf-8",
2697
- stdio: ["pipe", "pipe", "pipe"]
2698
- }).trim();
2699
- } catch {
2700
- return null;
2701
- }
2702
- }
2703
- function loadConfig2() {
2704
- for (const candidate of [
2705
- path11.resolve(".autodocrc.json"),
2706
- path11.resolve("docs-site", ".autodocrc.json")
2707
- ]) {
2708
- if (fs11.existsSync(candidate)) {
2709
- try {
2710
- return JSON.parse(fs11.readFileSync(candidate, "utf-8"));
2711
- } catch {
2712
- }
2713
- }
2714
- }
2715
- return null;
2716
- }
2717
- function generateWorkflow(branch, docsRepoUrl, outputDir) {
2718
- return `name: Update Documentation
2719
-
2720
- on:
2721
- push:
2722
- branches: [${branch}]
2723
- workflow_dispatch:
2724
-
2725
- jobs:
2726
- update-docs:
2727
- runs-on: ubuntu-latest
2728
- steps:
2729
- - name: Checkout source repo
2730
- uses: actions/checkout@v4
2731
- with:
2732
- fetch-depth: 0
2733
-
2734
- - name: Setup Node.js
2735
- uses: actions/setup-node@v4
2736
- with:
2737
- node-version: 20
2738
-
2739
- - name: Cache analysis results
2740
- uses: actions/cache@v4
2741
- with:
2742
- path: .autodoc-cache
2743
- key: autodoc-cache-\${{ github.sha }}
2744
- restore-keys: |
2745
- autodoc-cache-
2746
-
2747
- - name: Install open-auto-doc
2748
- run: npm install -g @latent-space-labs/open-auto-doc
2749
-
2750
- - name: Generate documentation
2751
- env:
2752
- ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
2753
- GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
2754
- run: open-auto-doc generate --incremental
2755
-
2756
- - name: Clone docs repo
2757
- run: |
2758
- git clone https://x-access-token:\${{ secrets.DOCS_DEPLOY_TOKEN }}@${docsRepoUrl.replace("https://", "")} docs-repo
2759
-
2760
- - name: Copy updated content
2761
- run: |
2762
- # Copy content and any updated config, preserving the docs repo git history
2763
- rsync -av --delete \\
2764
- --exclude '.git' \\
2765
- --exclude 'node_modules' \\
2766
- --exclude '.next' \\
2767
- --exclude '.source' \\
2768
- ${outputDir}/ docs-repo/
2769
-
2770
- - name: Push to docs repo
2771
- run: |
2772
- cd docs-repo
2773
- git config user.name "github-actions[bot]"
2774
- git config user.email "github-actions[bot]@users.noreply.github.com"
2775
- git add -A
2776
- # Only commit and push if there are changes
2777
- if git diff --cached --quiet; then
2778
- echo "No documentation changes to push."
2779
- else
2780
- git commit -m "Update documentation from \${{ github.repository }}@\${{ github.sha }}"
2781
- git push
2782
- fi
2783
- `;
2784
- }
3044
+ import * as p8 from "@clack/prompts";
2785
3045
  async function setupCiCommand() {
2786
- p6.intro("open-auto-doc \u2014 CI/CD Setup");
2787
- const gitRoot = getGitRoot();
2788
- if (!gitRoot) {
2789
- p6.log.error("Not in a git repository. Run this command from your project root.");
2790
- process.exit(1);
2791
- }
2792
- const config = loadConfig2();
3046
+ p8.intro("open-auto-doc \u2014 CI/CD Setup");
3047
+ const config = loadConfig();
2793
3048
  if (!config?.docsRepo) {
2794
- p6.log.error(
3049
+ p8.log.error(
2795
3050
  "No docs repo configured. Run `open-auto-doc deploy` first to create a docs GitHub repo."
2796
3051
  );
2797
3052
  process.exit(1);
2798
3053
  }
2799
- const outputDir = config.outputDir ? path11.relative(gitRoot, path11.resolve(config.outputDir)) : "docs-site";
2800
- p6.log.info(`Docs repo: ${config.docsRepo}`);
2801
- p6.log.info(`Output directory: ${outputDir}`);
2802
- const branch = await p6.text({
2803
- message: "Which branch should trigger doc updates?",
2804
- initialValue: "main",
2805
- validate: (v) => v.length === 0 ? "Branch name is required" : void 0
3054
+ const token = getGithubToken();
3055
+ const isMultiRepo = config.repos.length > 1;
3056
+ if (isMultiRepo && !token) {
3057
+ p8.log.error("Not authenticated. Run `open-auto-doc login` first (needed to push workflows to source repos).");
3058
+ process.exit(1);
3059
+ }
3060
+ const gitRoot = getGitRoot();
3061
+ if (!isMultiRepo && !gitRoot) {
3062
+ p8.log.error("Not in a git repository. Run this command from your project root.");
3063
+ process.exit(1);
3064
+ }
3065
+ const result = await createCiWorkflow({
3066
+ gitRoot: gitRoot || process.cwd(),
3067
+ docsRepoUrl: config.docsRepo,
3068
+ outputDir: config.outputDir || "docs-site",
3069
+ token: token || void 0,
3070
+ config
2806
3071
  });
2807
- if (p6.isCancel(branch)) {
2808
- p6.cancel("Setup cancelled.");
3072
+ if (!result) {
3073
+ p8.cancel("Setup cancelled.");
2809
3074
  process.exit(0);
2810
3075
  }
2811
- const workflowDir = path11.join(gitRoot, ".github", "workflows");
2812
- const workflowPath = path11.join(workflowDir, "update-docs.yml");
2813
- fs11.mkdirSync(workflowDir, { recursive: true });
2814
- fs11.writeFileSync(
2815
- workflowPath,
2816
- generateWorkflow(branch, config.docsRepo, outputDir),
2817
- "utf-8"
2818
- );
2819
- p6.log.success(`Created ${path11.relative(gitRoot, workflowPath)}`);
2820
- p6.note(
2821
- [
2822
- "Add these secrets to your GitHub repository:",
2823
- "(Settings \u2192 Secrets and variables \u2192 Actions \u2192 New repository secret)",
2824
- "",
2825
- " ANTHROPIC_API_KEY \u2014 Your Anthropic API key",
2826
- " DOCS_DEPLOY_TOKEN \u2014 GitHub PAT with repo scope",
2827
- " (needed to push to the docs repo)",
2828
- "",
2829
- "To create the PAT:",
2830
- " 1. Go to https://github.com/settings/tokens",
2831
- " 2. Generate new token (classic) with 'repo' scope",
2832
- " 3. Copy the token and add it as DOCS_DEPLOY_TOKEN",
2833
- "",
2834
- "Note: GITHUB_TOKEN is automatically provided by GitHub Actions",
2835
- " (used for reading the source repo during analysis)."
2836
- ].join("\n"),
2837
- "Required GitHub Secrets"
2838
- );
2839
- p6.outro("CI/CD workflow is ready! Commit and push to activate.");
3076
+ showSecretsInstructions(isMultiRepo);
3077
+ if ("repos" in result) {
3078
+ p8.outro("Per-repo CI workflows created! Add the required secrets to each source repo.");
3079
+ } else {
3080
+ p8.outro("CI/CD workflow is ready! Commit and push to activate.");
3081
+ }
2840
3082
  }
2841
3083
 
2842
3084
  // src/commands/login.ts
2843
- import * as p7 from "@clack/prompts";
3085
+ import * as p9 from "@clack/prompts";
2844
3086
  async function loginCommand() {
2845
- p7.intro("open-auto-doc \u2014 GitHub Login");
3087
+ p9.intro("open-auto-doc \u2014 GitHub Login");
2846
3088
  const existing = getGithubToken();
2847
3089
  if (existing) {
2848
- const overwrite = await p7.confirm({
3090
+ const overwrite = await p9.confirm({
2849
3091
  message: "You're already logged in. Re-authenticate?"
2850
3092
  });
2851
- if (!overwrite || p7.isCancel(overwrite)) {
2852
- p7.cancel("Keeping existing credentials");
3093
+ if (!overwrite || p9.isCancel(overwrite)) {
3094
+ p9.cancel("Keeping existing credentials");
2853
3095
  return;
2854
3096
  }
2855
3097
  }
2856
3098
  const token = await authenticateWithGithub();
2857
3099
  setGithubToken(token);
2858
- p7.outro("Logged in successfully!");
3100
+ p9.outro("Logged in successfully!");
2859
3101
  }
2860
3102
 
2861
3103
  // src/commands/logout.ts
2862
- import * as p8 from "@clack/prompts";
3104
+ import * as p10 from "@clack/prompts";
2863
3105
  async function logoutCommand() {
2864
- p8.intro("open-auto-doc \u2014 Logout");
3106
+ p10.intro("open-auto-doc \u2014 Logout");
2865
3107
  clearAll();
2866
- p8.outro("Credentials cleared. You've been logged out.");
3108
+ p10.outro("Credentials cleared. You've been logged out.");
2867
3109
  }
2868
3110
 
2869
3111
  // src/index.ts
2870
3112
  var program = new Command();
2871
- program.name("open-auto-doc").description("Auto-generate beautiful documentation websites from GitHub repositories using AI").version("0.3.0");
3113
+ program.name("open-auto-doc").description("Auto-generate beautiful documentation websites from GitHub repositories using AI").version("0.3.2");
2872
3114
  program.command("init", { isDefault: true }).description("Initialize and generate documentation for your repositories").option("-o, --output <dir>", "Output directory", "docs-site").action(initCommand);
2873
- 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)").action(generateCommand);
3115
+ 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);
2874
3116
  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);
2875
3117
  program.command("setup-ci").description("Generate a GitHub Actions workflow for auto-updating docs").action(setupCiCommand);
2876
3118
  program.command("login").description("Authenticate with GitHub").action(loginCommand);