@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.
- package/dist/index.js +674 -432
- 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
|
|
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
|
|
41
|
-
|
|
40
|
+
const spinner7 = p.spinner();
|
|
41
|
+
spinner7.start("Waiting for GitHub authorization...");
|
|
42
42
|
const token = await pollForToken(deviceData.device_code, deviceData.interval);
|
|
43
|
-
|
|
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
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
208
|
+
// src/config.ts
|
|
209
209
|
import fs3 from "fs";
|
|
210
210
|
import path3 from "path";
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 {
|
|
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 {
|
|
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 =
|
|
246
|
-
const node = { path:
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
690
|
+
const ext = path6.extname(entry.name).slice(1);
|
|
262
691
|
let size;
|
|
263
692
|
try {
|
|
264
|
-
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 =
|
|
286
|
-
if (
|
|
287
|
-
const content =
|
|
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 =
|
|
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((
|
|
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(
|
|
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 =
|
|
930
|
+
const fullPath = path42.join(repoPath, file);
|
|
502
931
|
let content;
|
|
503
932
|
try {
|
|
504
|
-
content =
|
|
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 =
|
|
531
|
-
const resolved =
|
|
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 =
|
|
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 =
|
|
1141
|
-
|
|
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 =
|
|
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((
|
|
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
|
|
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
|
|
2034
|
+
return path62.join(cacheDir, `${slugify(repoSlug)}-analysis.json`);
|
|
1606
2035
|
}
|
|
1607
2036
|
function saveCache(cacheDir, repoSlug, commitSha, result) {
|
|
1608
|
-
|
|
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
|
-
|
|
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 (!
|
|
2048
|
+
if (!fs62.existsSync(filePath)) return null;
|
|
1620
2049
|
try {
|
|
1621
|
-
const raw = JSON.parse(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2568
|
+
p5.intro("open-auto-doc \u2014 AI-powered documentation generator");
|
|
2140
2569
|
let token = getGithubToken();
|
|
2141
2570
|
if (!token) {
|
|
2142
|
-
|
|
2571
|
+
p5.log.info("Let's connect your GitHub account.");
|
|
2143
2572
|
token = await authenticateWithGithub();
|
|
2144
2573
|
setGithubToken(token);
|
|
2145
2574
|
} else {
|
|
2146
|
-
|
|
2575
|
+
p5.log.success("Using saved GitHub credentials.");
|
|
2147
2576
|
}
|
|
2148
2577
|
const repos = await pickRepos(token);
|
|
2149
|
-
|
|
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
|
|
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 (
|
|
2160
|
-
|
|
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
|
|
2593
|
+
const saveKey = await p5.confirm({
|
|
2165
2594
|
message: "Save API key for future use?"
|
|
2166
2595
|
});
|
|
2167
|
-
if (saveKey && !
|
|
2596
|
+
if (saveKey && !p5.isCancel(saveKey)) {
|
|
2168
2597
|
setAnthropicKey(apiKey);
|
|
2169
2598
|
}
|
|
2170
2599
|
} else {
|
|
2171
|
-
|
|
2600
|
+
p5.log.success("Using saved Anthropic API key.");
|
|
2172
2601
|
}
|
|
2173
|
-
const model = await
|
|
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 (
|
|
2182
|
-
|
|
2610
|
+
if (p5.isCancel(model)) {
|
|
2611
|
+
p5.cancel("Operation cancelled");
|
|
2183
2612
|
process.exit(0);
|
|
2184
2613
|
}
|
|
2185
|
-
|
|
2186
|
-
const cloneSpinner =
|
|
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
|
-
|
|
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
|
-
|
|
2629
|
+
p5.log.error("No repositories were cloned.");
|
|
2201
2630
|
process.exit(1);
|
|
2202
2631
|
}
|
|
2203
|
-
const analyzeSpinner =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
2693
|
+
const genSpinner = p5.spinner();
|
|
2265
2694
|
try {
|
|
2266
2695
|
genSpinner.start("Scaffolding documentation site...");
|
|
2267
2696
|
const templateDir = resolveTemplateDir();
|
|
2268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2309
|
-
|
|
2310
|
-
"
|
|
2311
|
-
);
|
|
2312
|
-
|
|
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
|
|
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
|
-
|
|
2338
|
-
|
|
2339
|
-
if (!
|
|
2340
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2366
|
-
|
|
2831
|
+
if (p6.isCancel(model)) {
|
|
2832
|
+
p6.cancel("Cancelled.");
|
|
2367
2833
|
process.exit(0);
|
|
2368
2834
|
}
|
|
2369
|
-
|
|
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
|
|
2373
|
-
|
|
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
|
|
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
|
-
|
|
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}/${
|
|
2881
|
+
cloneSpinner.stop(`Cloned ${clones.length}/${reposToAnalyze.length} ${reposToAnalyze.length === 1 ? "repository" : "repositories"}`);
|
|
2395
2882
|
if (clones.length === 0) {
|
|
2396
|
-
|
|
2883
|
+
p6.log.error("No repositories were cloned.");
|
|
2397
2884
|
process.exit(1);
|
|
2398
2885
|
}
|
|
2399
|
-
const analyzeSpinner =
|
|
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
|
-
|
|
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
|
|
2471
|
-
analyzeSpinner.stop(`Analyzed ${
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2978
|
+
p6.log.success("Documentation regenerated!");
|
|
2491
2979
|
}
|
|
2492
2980
|
for (const clone of clones) {
|
|
2493
2981
|
cleanupClone(clone);
|
|
2494
2982
|
}
|
|
2495
|
-
|
|
2983
|
+
p6.outro("Done!");
|
|
2496
2984
|
}
|
|
2497
2985
|
|
|
2498
2986
|
// src/commands/deploy.ts
|
|
2499
|
-
import * as
|
|
2500
|
-
import
|
|
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 (!
|
|
2534
|
-
|
|
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 &&
|
|
2999
|
+
if (config?.outputDir && fs9.existsSync(path10.resolve(config.outputDir))) {
|
|
2540
3000
|
return path10.resolve(config.outputDir);
|
|
2541
3001
|
}
|
|
2542
|
-
if (
|
|
3002
|
+
if (fs9.existsSync(path10.resolve("docs-site"))) {
|
|
2543
3003
|
return path10.resolve("docs-site");
|
|
2544
3004
|
}
|
|
2545
|
-
|
|
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
|
-
|
|
3011
|
+
p7.intro("open-auto-doc \u2014 Deploy docs to GitHub");
|
|
2558
3012
|
const token = getGithubToken();
|
|
2559
3013
|
if (!token) {
|
|
2560
|
-
|
|
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
|
-
|
|
2566
|
-
const octokit = new Octokit2({ auth: token });
|
|
3019
|
+
p7.log.info(`Docs directory: ${docsDir}`);
|
|
2567
3020
|
if (config?.docsRepo) {
|
|
2568
|
-
|
|
2569
|
-
const
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
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 (
|
|
2617
|
-
|
|
3035
|
+
if (!result) {
|
|
3036
|
+
p7.cancel("Deploy cancelled.");
|
|
2618
3037
|
process.exit(0);
|
|
2619
3038
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
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
|
|
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
|
-
|
|
2787
|
-
const
|
|
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
|
-
|
|
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
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
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 (
|
|
2808
|
-
|
|
3072
|
+
if (!result) {
|
|
3073
|
+
p8.cancel("Setup cancelled.");
|
|
2809
3074
|
process.exit(0);
|
|
2810
3075
|
}
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
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
|
|
3085
|
+
import * as p9 from "@clack/prompts";
|
|
2844
3086
|
async function loginCommand() {
|
|
2845
|
-
|
|
3087
|
+
p9.intro("open-auto-doc \u2014 GitHub Login");
|
|
2846
3088
|
const existing = getGithubToken();
|
|
2847
3089
|
if (existing) {
|
|
2848
|
-
const overwrite = await
|
|
3090
|
+
const overwrite = await p9.confirm({
|
|
2849
3091
|
message: "You're already logged in. Re-authenticate?"
|
|
2850
3092
|
});
|
|
2851
|
-
if (!overwrite ||
|
|
2852
|
-
|
|
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
|
-
|
|
3100
|
+
p9.outro("Logged in successfully!");
|
|
2859
3101
|
}
|
|
2860
3102
|
|
|
2861
3103
|
// src/commands/logout.ts
|
|
2862
|
-
import * as
|
|
3104
|
+
import * as p10 from "@clack/prompts";
|
|
2863
3105
|
async function logoutCommand() {
|
|
2864
|
-
|
|
3106
|
+
p10.intro("open-auto-doc \u2014 Logout");
|
|
2865
3107
|
clearAll();
|
|
2866
|
-
|
|
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.
|
|
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);
|