@latent-space-labs/open-auto-doc 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +381 -155
- package/dist/site-template/README.md +3 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import net from "net";
|
|
|
10
10
|
import path10 from "path";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
12
|
import { spawn } from "child_process";
|
|
13
|
+
import { Octokit as Octokit4 } from "@octokit/rest";
|
|
13
14
|
|
|
14
15
|
// src/auth/device-flow.ts
|
|
15
16
|
import * as p from "@clack/prompts";
|
|
@@ -258,48 +259,58 @@ function getGitHubUsername(octokit) {
|
|
|
258
259
|
return octokit.rest.users.getAuthenticated().then((res) => res.data.login);
|
|
259
260
|
}
|
|
260
261
|
async function createAndPushDocsRepo(params) {
|
|
261
|
-
const { token, docsDir, config } = params;
|
|
262
|
+
const { token, docsDir, config, preCollected } = params;
|
|
262
263
|
const octokit = new Octokit2({ auth: token });
|
|
263
264
|
const username = await getGitHubUsername(octokit);
|
|
264
|
-
let
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
265
|
+
let owner;
|
|
266
|
+
let repoName;
|
|
267
|
+
let visibility;
|
|
268
|
+
if (preCollected) {
|
|
269
|
+
owner = preCollected.owner;
|
|
270
|
+
repoName = preCollected.repoName;
|
|
271
|
+
visibility = preCollected.visibility;
|
|
272
|
+
} else {
|
|
273
|
+
let orgs = [];
|
|
274
|
+
try {
|
|
275
|
+
const { data } = await octokit.rest.orgs.listForAuthenticatedUser({ per_page: 100 });
|
|
276
|
+
orgs = data;
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
const ownerOptions = [
|
|
280
|
+
{ value: username, label: username, hint: "Personal account" },
|
|
281
|
+
...orgs.map((org) => ({ value: org.login, label: org.login, hint: "Organization" }))
|
|
282
|
+
];
|
|
283
|
+
owner = username;
|
|
284
|
+
if (ownerOptions.length > 1) {
|
|
285
|
+
const selected = await p3.select({
|
|
286
|
+
message: "Where should the docs repo be created?",
|
|
287
|
+
options: ownerOptions
|
|
288
|
+
});
|
|
289
|
+
if (p3.isCancel(selected)) return null;
|
|
290
|
+
owner = selected;
|
|
291
|
+
}
|
|
292
|
+
const isOrg2 = owner !== username;
|
|
293
|
+
const slug = config?.projectName ? config.projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") : config?.repos?.[0]?.name;
|
|
294
|
+
const defaultName = slug ? `${slug}-docs` : "my-project-docs";
|
|
295
|
+
repoName = await p3.text({
|
|
296
|
+
message: "Name for the docs GitHub repo:",
|
|
297
|
+
initialValue: defaultName,
|
|
298
|
+
validate: (v) => {
|
|
299
|
+
if (!v || v.length === 0) return "Repo name is required";
|
|
300
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(v)) return "Invalid repo name";
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
if (p3.isCancel(repoName)) return null;
|
|
304
|
+
visibility = await p3.select({
|
|
305
|
+
message: "Repository visibility:",
|
|
306
|
+
options: [
|
|
307
|
+
{ value: "public", label: "Public" },
|
|
308
|
+
{ value: "private", label: "Private" }
|
|
309
|
+
]
|
|
279
310
|
});
|
|
280
|
-
if (p3.isCancel(
|
|
281
|
-
owner = selected;
|
|
311
|
+
if (p3.isCancel(visibility)) return null;
|
|
282
312
|
}
|
|
283
313
|
const isOrg = owner !== username;
|
|
284
|
-
const slug = config?.projectName ? config.projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") : config?.repos?.[0]?.name;
|
|
285
|
-
const defaultName = slug ? `${slug}-docs` : "my-project-docs";
|
|
286
|
-
const repoName = await p3.text({
|
|
287
|
-
message: "Name for the docs GitHub repo:",
|
|
288
|
-
initialValue: defaultName,
|
|
289
|
-
validate: (v) => {
|
|
290
|
-
if (!v || v.length === 0) return "Repo name is required";
|
|
291
|
-
if (!/^[a-zA-Z0-9._-]+$/.test(v)) return "Invalid repo name";
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
if (p3.isCancel(repoName)) return null;
|
|
295
|
-
const visibility = await p3.select({
|
|
296
|
-
message: "Repository visibility:",
|
|
297
|
-
options: [
|
|
298
|
-
{ value: "public", label: "Public" },
|
|
299
|
-
{ value: "private", label: "Private" }
|
|
300
|
-
]
|
|
301
|
-
});
|
|
302
|
-
if (p3.isCancel(visibility)) return null;
|
|
303
314
|
const spinner9 = p3.spinner();
|
|
304
315
|
spinner9.start(`Creating GitHub repo ${owner}/${repoName}...`);
|
|
305
316
|
let repoUrl;
|
|
@@ -309,7 +320,8 @@ async function createAndPushDocsRepo(params) {
|
|
|
309
320
|
org: owner,
|
|
310
321
|
name: repoName,
|
|
311
322
|
private: visibility === "private",
|
|
312
|
-
description: "
|
|
323
|
+
description: "Documentation auto-generated by https://www.openautodoc.com",
|
|
324
|
+
homepage: "https://www.openautodoc.com",
|
|
313
325
|
auto_init: false
|
|
314
326
|
});
|
|
315
327
|
repoUrl = data.clone_url;
|
|
@@ -318,7 +330,8 @@ async function createAndPushDocsRepo(params) {
|
|
|
318
330
|
const { data } = await octokit.rest.repos.createForAuthenticatedUser({
|
|
319
331
|
name: repoName,
|
|
320
332
|
private: visibility === "private",
|
|
321
|
-
description: "
|
|
333
|
+
description: "Documentation auto-generated by https://www.openautodoc.com",
|
|
334
|
+
homepage: "https://www.openautodoc.com",
|
|
322
335
|
auto_init: false
|
|
323
336
|
});
|
|
324
337
|
repoUrl = data.clone_url;
|
|
@@ -438,8 +451,18 @@ async function authenticateVercel() {
|
|
|
438
451
|
}
|
|
439
452
|
p4.log.warn("Saved Vercel token is invalid.");
|
|
440
453
|
}
|
|
441
|
-
p4.
|
|
442
|
-
|
|
454
|
+
p4.note(
|
|
455
|
+
[
|
|
456
|
+
"To create a Vercel access token:",
|
|
457
|
+
"",
|
|
458
|
+
" 1. Go to https://vercel.com/account/settings/tokens",
|
|
459
|
+
" 2. Click 'Create Token'",
|
|
460
|
+
" 3. Enter a name (e.g., 'open-auto-doc')",
|
|
461
|
+
" 4. Select the scope (your personal account or a team)",
|
|
462
|
+
" 5. Set an expiration (or no expiration for convenience)",
|
|
463
|
+
" 6. Click 'Create Token' and copy the value"
|
|
464
|
+
].join("\n"),
|
|
465
|
+
"Vercel Token Required"
|
|
443
466
|
);
|
|
444
467
|
const tokenInput = await p4.text({
|
|
445
468
|
message: "Enter your Vercel token",
|
|
@@ -469,29 +492,32 @@ async function authenticateVercel() {
|
|
|
469
492
|
}
|
|
470
493
|
async function deployToVercel(params) {
|
|
471
494
|
const { token, githubOwner, githubRepo, docsDir, config } = params;
|
|
472
|
-
const { data: userData } = await vercelFetch(
|
|
473
|
-
"/v2/user",
|
|
474
|
-
token
|
|
475
|
-
);
|
|
476
|
-
const userId = userData.user?.id;
|
|
477
|
-
const { data: teamsData } = await vercelFetch(
|
|
478
|
-
"/v2/teams",
|
|
479
|
-
token
|
|
480
|
-
);
|
|
481
|
-
const teams = teamsData.teams ?? [];
|
|
482
495
|
let teamId;
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
496
|
+
if (params.scope) {
|
|
497
|
+
teamId = params.scope.teamId;
|
|
498
|
+
} else {
|
|
499
|
+
const { data: userData } = await vercelFetch(
|
|
500
|
+
"/v2/user",
|
|
501
|
+
token
|
|
502
|
+
);
|
|
503
|
+
const { data: teamsData } = await vercelFetch(
|
|
504
|
+
"/v2/teams",
|
|
505
|
+
token
|
|
506
|
+
);
|
|
507
|
+
const teams = teamsData.teams ?? [];
|
|
508
|
+
if (teams.length > 0) {
|
|
509
|
+
const scopeOptions = [
|
|
510
|
+
{ value: "__personal__", label: userData.user?.username ?? "Personal", hint: "Personal account" },
|
|
511
|
+
...teams.map((t) => ({ value: t.id, label: t.name || t.slug, hint: "Team" }))
|
|
512
|
+
];
|
|
513
|
+
const selectedScope = await p4.select({
|
|
514
|
+
message: "Which Vercel scope should own this project?",
|
|
515
|
+
options: scopeOptions
|
|
516
|
+
});
|
|
517
|
+
if (p4.isCancel(selectedScope)) return null;
|
|
518
|
+
if (selectedScope !== "__personal__") {
|
|
519
|
+
teamId = selectedScope;
|
|
520
|
+
}
|
|
495
521
|
}
|
|
496
522
|
}
|
|
497
523
|
const spinner9 = p4.spinner();
|
|
@@ -543,6 +569,29 @@ async function deployToVercel(params) {
|
|
|
543
569
|
const projectId = project.id;
|
|
544
570
|
const projectName = project.name;
|
|
545
571
|
spinner9.stop(`Created Vercel project: ${projectName}`);
|
|
572
|
+
spinner9.start("Triggering first deployment...");
|
|
573
|
+
const { ok: deployOk } = await vercelFetch(
|
|
574
|
+
"/v13/deployments",
|
|
575
|
+
token,
|
|
576
|
+
{
|
|
577
|
+
method: "POST",
|
|
578
|
+
teamId,
|
|
579
|
+
body: {
|
|
580
|
+
name: projectName,
|
|
581
|
+
project: projectId,
|
|
582
|
+
target: "production",
|
|
583
|
+
gitSource: {
|
|
584
|
+
type: "github",
|
|
585
|
+
org: githubOwner,
|
|
586
|
+
repo: githubRepo,
|
|
587
|
+
ref: "main"
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
);
|
|
592
|
+
if (!deployOk) {
|
|
593
|
+
spinner9.message("Waiting for deployment...");
|
|
594
|
+
}
|
|
546
595
|
spinner9.start("Waiting for first deployment...");
|
|
547
596
|
const maxWaitMs = 5 * 60 * 1e3;
|
|
548
597
|
const pollIntervalMs = 5e3;
|
|
@@ -2802,18 +2851,24 @@ async function createCiWorkflow(params) {
|
|
|
2802
2851
|
return createCiWorkflowsMultiRepo({
|
|
2803
2852
|
token,
|
|
2804
2853
|
config,
|
|
2805
|
-
docsRepoUrl
|
|
2854
|
+
docsRepoUrl,
|
|
2855
|
+
branch: params.branch
|
|
2806
2856
|
});
|
|
2807
2857
|
}
|
|
2808
2858
|
const relativeOutputDir = path7.relative(gitRoot, path7.resolve(outputDir));
|
|
2809
2859
|
p6.log.info(`Docs repo: ${docsRepoUrl}`);
|
|
2810
2860
|
p6.log.info(`Output directory: ${relativeOutputDir}`);
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2861
|
+
let branch;
|
|
2862
|
+
if (params.branch) {
|
|
2863
|
+
branch = params.branch;
|
|
2864
|
+
} else {
|
|
2865
|
+
branch = await p6.text({
|
|
2866
|
+
message: "Which branch should trigger doc updates?",
|
|
2867
|
+
initialValue: "main",
|
|
2868
|
+
validate: (v) => v.length === 0 ? "Branch name is required" : void 0
|
|
2869
|
+
});
|
|
2870
|
+
if (p6.isCancel(branch)) return null;
|
|
2871
|
+
}
|
|
2817
2872
|
const workflowDir = path7.join(gitRoot, ".github", "workflows");
|
|
2818
2873
|
const workflowPath = path7.join(workflowDir, "update-docs.yml");
|
|
2819
2874
|
fs7.mkdirSync(workflowDir, { recursive: true });
|
|
@@ -2850,12 +2905,17 @@ async function createCiWorkflowsMultiRepo(params) {
|
|
|
2850
2905
|
const octokit = new Octokit3({ auth: token });
|
|
2851
2906
|
p6.log.info(`Setting up CI for ${config.repos.length} repositories`);
|
|
2852
2907
|
p6.log.info(`Docs repo: ${docsRepoUrl}`);
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2908
|
+
let branch;
|
|
2909
|
+
if (params.branch) {
|
|
2910
|
+
branch = params.branch;
|
|
2911
|
+
} else {
|
|
2912
|
+
branch = await p6.text({
|
|
2913
|
+
message: "Which branch should trigger doc updates?",
|
|
2914
|
+
initialValue: "main",
|
|
2915
|
+
validate: (v) => v.length === 0 ? "Branch name is required" : void 0
|
|
2916
|
+
});
|
|
2917
|
+
if (p6.isCancel(branch)) return null;
|
|
2918
|
+
}
|
|
2859
2919
|
const createdRepos = [];
|
|
2860
2920
|
const workflowPath = ".github/workflows/update-docs.yml";
|
|
2861
2921
|
for (const repo of config.repos) {
|
|
@@ -3011,19 +3071,18 @@ import * as p7 from "@clack/prompts";
|
|
|
3011
3071
|
import fs8 from "fs";
|
|
3012
3072
|
import path8 from "path";
|
|
3013
3073
|
var MCP_SERVER_KEY = "project-docs";
|
|
3014
|
-
function getMcpConfig() {
|
|
3074
|
+
function getMcpConfig(docsDir) {
|
|
3015
3075
|
return {
|
|
3016
3076
|
command: "npx",
|
|
3017
|
-
args: ["-y", "@latent-space-labs/open-auto-doc-mcp", "--project-dir",
|
|
3077
|
+
args: ["-y", "@latent-space-labs/open-auto-doc-mcp", "--project-dir", docsDir]
|
|
3018
3078
|
};
|
|
3019
3079
|
}
|
|
3020
3080
|
async function setupMcpConfig(opts) {
|
|
3021
|
-
const
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
writeMcpJson(process.cwd());
|
|
3081
|
+
const absOutputDir = path8.resolve(opts.outputDir);
|
|
3082
|
+
writeMcpJson(process.cwd(), absOutputDir);
|
|
3083
|
+
writeMcpJson(absOutputDir, absOutputDir);
|
|
3084
|
+
addMcpScript(absOutputDir);
|
|
3085
|
+
showMcpInstructions(absOutputDir);
|
|
3027
3086
|
}
|
|
3028
3087
|
async function setupMcpCommand() {
|
|
3029
3088
|
p7.intro("open-auto-doc \u2014 MCP Server Setup");
|
|
@@ -3047,11 +3106,48 @@ Run \`open-auto-doc init\` or \`open-auto-doc generate\` first.`
|
|
|
3047
3106
|
p7.log.error("Cache directory exists but contains no analysis files.");
|
|
3048
3107
|
process.exit(1);
|
|
3049
3108
|
}
|
|
3050
|
-
|
|
3109
|
+
const absOutputDir = path8.resolve(config.outputDir);
|
|
3110
|
+
writeMcpJson(process.cwd(), absOutputDir);
|
|
3111
|
+
writeMcpJson(absOutputDir, absOutputDir);
|
|
3112
|
+
addMcpScript(absOutputDir);
|
|
3113
|
+
showMcpInstructions(absOutputDir);
|
|
3114
|
+
p7.outro("Open Claude Code in this project to start using the tools.");
|
|
3115
|
+
}
|
|
3116
|
+
function writeMcpJson(projectRoot, docsDir) {
|
|
3117
|
+
const mcpPath = path8.join(projectRoot, ".mcp.json");
|
|
3118
|
+
const relDocsDir = path8.relative(projectRoot, docsDir);
|
|
3119
|
+
const isSubdir = !relDocsDir.startsWith("..") && !path8.isAbsolute(relDocsDir);
|
|
3120
|
+
const configDocsDir = isSubdir ? `./${relDocsDir}` : docsDir;
|
|
3121
|
+
let existing = {};
|
|
3122
|
+
if (fs8.existsSync(mcpPath)) {
|
|
3123
|
+
try {
|
|
3124
|
+
existing = JSON.parse(fs8.readFileSync(mcpPath, "utf-8"));
|
|
3125
|
+
} catch {
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
const mcpServers = existing.mcpServers ?? {};
|
|
3129
|
+
mcpServers[MCP_SERVER_KEY] = getMcpConfig(configDocsDir);
|
|
3130
|
+
const merged = { ...existing, mcpServers };
|
|
3131
|
+
fs8.writeFileSync(mcpPath, JSON.stringify(merged, null, 2) + "\n");
|
|
3132
|
+
p7.log.step(`Wrote ${mcpPath}`);
|
|
3133
|
+
}
|
|
3134
|
+
function addMcpScript(docsDir) {
|
|
3135
|
+
const pkgPath = path8.join(docsDir, "package.json");
|
|
3136
|
+
if (!fs8.existsSync(pkgPath)) return;
|
|
3137
|
+
try {
|
|
3138
|
+
const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
|
|
3139
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
3140
|
+
pkg.scripts.mcp = "npx -y @latent-space-labs/open-auto-doc-mcp --project-dir .";
|
|
3141
|
+
fs8.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
3142
|
+
p7.log.step(`Added 'mcp' script to ${pkgPath}`);
|
|
3143
|
+
} catch {
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
function showMcpInstructions(docsDir) {
|
|
3051
3147
|
p7.log.success("MCP server configured!");
|
|
3052
3148
|
p7.note(
|
|
3053
3149
|
[
|
|
3054
|
-
"The following tools are now available
|
|
3150
|
+
"The following tools are now available:",
|
|
3055
3151
|
"",
|
|
3056
3152
|
" get_project_overview \u2014 Project summary and tech stack",
|
|
3057
3153
|
" search_documentation \u2014 Full-text search across all docs",
|
|
@@ -3062,24 +3158,31 @@ Run \`open-auto-doc init\` or \`open-auto-doc generate\` first.`
|
|
|
3062
3158
|
" get_diagram \u2014 Mermaid diagrams",
|
|
3063
3159
|
" get_business_rules \u2014 Domain concepts and workflows"
|
|
3064
3160
|
].join("\n"),
|
|
3065
|
-
"Available MCP
|
|
3161
|
+
"Available MCP Tools"
|
|
3162
|
+
);
|
|
3163
|
+
p7.note(
|
|
3164
|
+
[
|
|
3165
|
+
"A .mcp.json file has been written to your project.",
|
|
3166
|
+
"Claude Code will automatically detect it.",
|
|
3167
|
+
"",
|
|
3168
|
+
"To use from another project, add this to its .mcp.json:",
|
|
3169
|
+
"",
|
|
3170
|
+
` {`,
|
|
3171
|
+
` "mcpServers": {`,
|
|
3172
|
+
` "project-docs": {`,
|
|
3173
|
+
` "command": "npx",`,
|
|
3174
|
+
` "args": ["-y", "@latent-space-labs/open-auto-doc-mcp",`,
|
|
3175
|
+
` "--project-dir", "${docsDir}"]`,
|
|
3176
|
+
` }`,
|
|
3177
|
+
` }`,
|
|
3178
|
+
` }`,
|
|
3179
|
+
"",
|
|
3180
|
+
"Or run the MCP server directly from the docs directory:",
|
|
3181
|
+
"",
|
|
3182
|
+
` cd ${docsDir} && npm run mcp`
|
|
3183
|
+
].join("\n"),
|
|
3184
|
+
"How to Connect"
|
|
3066
3185
|
);
|
|
3067
|
-
p7.outro("Open Claude Code in this project to start using the tools.");
|
|
3068
|
-
}
|
|
3069
|
-
function writeMcpJson(projectRoot) {
|
|
3070
|
-
const mcpPath = path8.join(projectRoot, ".mcp.json");
|
|
3071
|
-
let existing = {};
|
|
3072
|
-
if (fs8.existsSync(mcpPath)) {
|
|
3073
|
-
try {
|
|
3074
|
-
existing = JSON.parse(fs8.readFileSync(mcpPath, "utf-8"));
|
|
3075
|
-
} catch {
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
const mcpServers = existing.mcpServers ?? {};
|
|
3079
|
-
mcpServers[MCP_SERVER_KEY] = getMcpConfig();
|
|
3080
|
-
const merged = { ...existing, mcpServers };
|
|
3081
|
-
fs8.writeFileSync(mcpPath, JSON.stringify(merged, null, 2) + "\n");
|
|
3082
|
-
p7.log.step(`Wrote ${path8.relative(projectRoot, mcpPath)}`);
|
|
3083
3186
|
}
|
|
3084
3187
|
|
|
3085
3188
|
// ../generator/dist/index.js
|
|
@@ -3961,6 +4064,146 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
|
|
|
3961
4064
|
process.exit(0);
|
|
3962
4065
|
}
|
|
3963
4066
|
p8.log.info(`Using ${model}`);
|
|
4067
|
+
const shouldSetupMcp = await p8.confirm({
|
|
4068
|
+
message: "Set up MCP server so Claude Code can query your docs?"
|
|
4069
|
+
});
|
|
4070
|
+
const wantsMcp = !p8.isCancel(shouldSetupMcp) && shouldSetupMcp;
|
|
4071
|
+
const shouldDeploy = await p8.confirm({
|
|
4072
|
+
message: "Would you like to deploy your docs to GitHub?"
|
|
4073
|
+
});
|
|
4074
|
+
const wantsDeploy = !p8.isCancel(shouldDeploy) && shouldDeploy;
|
|
4075
|
+
let deployConfig;
|
|
4076
|
+
let vercelToken = null;
|
|
4077
|
+
let vercelScope;
|
|
4078
|
+
let wantsVercel = false;
|
|
4079
|
+
let wantsCi = false;
|
|
4080
|
+
let ciBranch;
|
|
4081
|
+
if (wantsDeploy) {
|
|
4082
|
+
const octokit = new Octokit4({ auth: token });
|
|
4083
|
+
let username;
|
|
4084
|
+
try {
|
|
4085
|
+
const { data } = await octokit.rest.users.getAuthenticated();
|
|
4086
|
+
username = data.login;
|
|
4087
|
+
} catch {
|
|
4088
|
+
p8.log.error("Failed to fetch GitHub user info.");
|
|
4089
|
+
process.exit(1);
|
|
4090
|
+
}
|
|
4091
|
+
let orgs = [];
|
|
4092
|
+
try {
|
|
4093
|
+
const { data } = await octokit.rest.orgs.listForAuthenticatedUser({ per_page: 100 });
|
|
4094
|
+
orgs = data;
|
|
4095
|
+
} catch {
|
|
4096
|
+
}
|
|
4097
|
+
const ownerOptions = [
|
|
4098
|
+
{ value: username, label: username, hint: "Personal account" },
|
|
4099
|
+
...orgs.map((org) => ({ value: org.login, label: org.login, hint: "Organization" }))
|
|
4100
|
+
];
|
|
4101
|
+
let owner = username;
|
|
4102
|
+
if (ownerOptions.length > 1) {
|
|
4103
|
+
const selected = await p8.select({
|
|
4104
|
+
message: "Where should the docs repo be created?",
|
|
4105
|
+
options: ownerOptions
|
|
4106
|
+
});
|
|
4107
|
+
if (p8.isCancel(selected)) {
|
|
4108
|
+
p8.cancel("Operation cancelled");
|
|
4109
|
+
process.exit(0);
|
|
4110
|
+
}
|
|
4111
|
+
owner = selected;
|
|
4112
|
+
}
|
|
4113
|
+
const slug = projectName ? projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") : repos[0]?.name;
|
|
4114
|
+
const defaultName = slug ? `${slug}-docs` : "my-project-docs";
|
|
4115
|
+
const repoNameInput = await p8.text({
|
|
4116
|
+
message: "Name for the docs GitHub repo:",
|
|
4117
|
+
initialValue: defaultName,
|
|
4118
|
+
validate: (v) => {
|
|
4119
|
+
if (!v || v.length === 0) return "Repo name is required";
|
|
4120
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(v)) return "Invalid repo name";
|
|
4121
|
+
}
|
|
4122
|
+
});
|
|
4123
|
+
if (p8.isCancel(repoNameInput)) {
|
|
4124
|
+
p8.cancel("Operation cancelled");
|
|
4125
|
+
process.exit(0);
|
|
4126
|
+
}
|
|
4127
|
+
const visibilityInput = await p8.select({
|
|
4128
|
+
message: "Repository visibility:",
|
|
4129
|
+
options: [
|
|
4130
|
+
{ value: "public", label: "Public" },
|
|
4131
|
+
{ value: "private", label: "Private" }
|
|
4132
|
+
]
|
|
4133
|
+
});
|
|
4134
|
+
if (p8.isCancel(visibilityInput)) {
|
|
4135
|
+
p8.cancel("Operation cancelled");
|
|
4136
|
+
process.exit(0);
|
|
4137
|
+
}
|
|
4138
|
+
deployConfig = {
|
|
4139
|
+
owner,
|
|
4140
|
+
repoName: repoNameInput,
|
|
4141
|
+
visibility: visibilityInput
|
|
4142
|
+
};
|
|
4143
|
+
const shouldDeployVercel = await p8.confirm({
|
|
4144
|
+
message: "Would you like to deploy to Vercel? (auto-deploys on every push)"
|
|
4145
|
+
});
|
|
4146
|
+
wantsVercel = !p8.isCancel(shouldDeployVercel) && shouldDeployVercel;
|
|
4147
|
+
if (wantsVercel) {
|
|
4148
|
+
vercelToken = await authenticateVercel();
|
|
4149
|
+
if (vercelToken) {
|
|
4150
|
+
try {
|
|
4151
|
+
const teamsRes = await fetch("https://api.vercel.com/v2/teams", {
|
|
4152
|
+
headers: { Authorization: `Bearer ${vercelToken}` }
|
|
4153
|
+
});
|
|
4154
|
+
const teamsData = await teamsRes.json();
|
|
4155
|
+
const teams = teamsData?.teams ?? [];
|
|
4156
|
+
if (teams.length > 0) {
|
|
4157
|
+
const userRes = await fetch("https://api.vercel.com/v2/user", {
|
|
4158
|
+
headers: { Authorization: `Bearer ${vercelToken}` }
|
|
4159
|
+
});
|
|
4160
|
+
const userData = await userRes.json();
|
|
4161
|
+
const vercelUsername = userData?.user?.username ?? "Personal";
|
|
4162
|
+
const scopeOptions = [
|
|
4163
|
+
{ value: "__personal__", label: vercelUsername, hint: "Personal account" },
|
|
4164
|
+
...teams.map((t) => ({ value: t.id, label: t.name || t.slug, hint: "Team" }))
|
|
4165
|
+
];
|
|
4166
|
+
const selectedScope = await p8.select({
|
|
4167
|
+
message: "Which Vercel scope should own this project?",
|
|
4168
|
+
options: scopeOptions
|
|
4169
|
+
});
|
|
4170
|
+
if (p8.isCancel(selectedScope)) {
|
|
4171
|
+
p8.cancel("Operation cancelled");
|
|
4172
|
+
process.exit(0);
|
|
4173
|
+
}
|
|
4174
|
+
vercelScope = {
|
|
4175
|
+
teamId: selectedScope === "__personal__" ? void 0 : selectedScope
|
|
4176
|
+
};
|
|
4177
|
+
} else {
|
|
4178
|
+
vercelScope = { teamId: void 0 };
|
|
4179
|
+
}
|
|
4180
|
+
} catch {
|
|
4181
|
+
vercelScope = { teamId: void 0 };
|
|
4182
|
+
}
|
|
4183
|
+
} else {
|
|
4184
|
+
wantsVercel = false;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
const shouldSetupCi = await p8.confirm({
|
|
4188
|
+
message: "Would you like to set up CI to auto-update docs on every push?"
|
|
4189
|
+
});
|
|
4190
|
+
wantsCi = !p8.isCancel(shouldSetupCi) && shouldSetupCi;
|
|
4191
|
+
if (wantsCi) {
|
|
4192
|
+
const branchInput = await p8.text({
|
|
4193
|
+
message: "Which branch should trigger doc updates?",
|
|
4194
|
+
initialValue: "main",
|
|
4195
|
+
validate: (v) => v.length === 0 ? "Branch name is required" : void 0
|
|
4196
|
+
});
|
|
4197
|
+
if (p8.isCancel(branchInput)) {
|
|
4198
|
+
p8.cancel("Operation cancelled");
|
|
4199
|
+
process.exit(0);
|
|
4200
|
+
}
|
|
4201
|
+
ciBranch = branchInput;
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
p8.log.step("All configured! Starting analysis and generation...");
|
|
4205
|
+
const outputDir = path10.resolve(options.output || "docs-site");
|
|
4206
|
+
const cacheDir = path10.join(outputDir, ".autodoc-cache");
|
|
3964
4207
|
const cloneSpinner = p8.spinner();
|
|
3965
4208
|
cloneSpinner.start(`Cloning ${repos.length} repositories...`);
|
|
3966
4209
|
const clones = [];
|
|
@@ -3998,6 +4241,11 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
|
|
|
3998
4241
|
progressTable.update(repoName, { activity: formatToolActivity(event) });
|
|
3999
4242
|
}
|
|
4000
4243
|
});
|
|
4244
|
+
try {
|
|
4245
|
+
const headSha = getHeadSha(cloned.localPath);
|
|
4246
|
+
saveCache(cacheDir, repoName, headSha, result);
|
|
4247
|
+
} catch {
|
|
4248
|
+
}
|
|
4001
4249
|
progressTable.update(repoName, { status: "done", summary: buildRepoSummary(result) });
|
|
4002
4250
|
return { repo: repoName, result };
|
|
4003
4251
|
} catch (err) {
|
|
@@ -4033,7 +4281,6 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
|
|
|
4033
4281
|
p8.log.warn(`Cross-repo error: ${err instanceof Error ? err.message : err}`);
|
|
4034
4282
|
}
|
|
4035
4283
|
}
|
|
4036
|
-
const outputDir = path10.resolve(options.output || "docs-site");
|
|
4037
4284
|
if (!projectName) {
|
|
4038
4285
|
projectName = results.length === 1 ? results[0].repoName : "My Project";
|
|
4039
4286
|
}
|
|
@@ -4081,10 +4328,7 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
|
|
|
4081
4328
|
p8.log.warn(`Build check skipped: ${err instanceof Error ? err.message : err}`);
|
|
4082
4329
|
}
|
|
4083
4330
|
p8.log.success("Documentation generated successfully!");
|
|
4084
|
-
|
|
4085
|
-
message: "Set up MCP server so Claude Code can query your docs?"
|
|
4086
|
-
});
|
|
4087
|
-
if (!p8.isCancel(shouldSetupMcp) && shouldSetupMcp) {
|
|
4331
|
+
if (wantsMcp) {
|
|
4088
4332
|
await setupMcpConfig({ outputDir });
|
|
4089
4333
|
}
|
|
4090
4334
|
let devServer;
|
|
@@ -4097,10 +4341,7 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
|
|
|
4097
4341
|
p8.log.warn("Could not start preview server. You can run it manually:");
|
|
4098
4342
|
p8.log.info(` cd ${path10.relative(process.cwd(), outputDir)} && npm run dev`);
|
|
4099
4343
|
}
|
|
4100
|
-
|
|
4101
|
-
message: "Would you like to deploy your docs to GitHub?"
|
|
4102
|
-
});
|
|
4103
|
-
if (p8.isCancel(shouldDeploy) || !shouldDeploy) {
|
|
4344
|
+
if (!wantsDeploy || !deployConfig) {
|
|
4104
4345
|
if (devServer) {
|
|
4105
4346
|
killDevServer(devServer);
|
|
4106
4347
|
}
|
|
@@ -4117,7 +4358,8 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
|
|
|
4117
4358
|
const deployResult = await createAndPushDocsRepo({
|
|
4118
4359
|
token,
|
|
4119
4360
|
docsDir: outputDir,
|
|
4120
|
-
config
|
|
4361
|
+
config,
|
|
4362
|
+
preCollected: deployConfig
|
|
4121
4363
|
});
|
|
4122
4364
|
if (!deployResult) {
|
|
4123
4365
|
p8.note(
|
|
@@ -4128,51 +4370,35 @@ Try reinstalling: npm install -g @latent-space-labs/open-auto-doc`
|
|
|
4128
4370
|
return;
|
|
4129
4371
|
}
|
|
4130
4372
|
let vercelDeployed = false;
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
});
|
|
4144
|
-
if (vercelResult) {
|
|
4145
|
-
p8.log.success(`Live at: ${vercelResult.deploymentUrl}`);
|
|
4146
|
-
vercelDeployed = true;
|
|
4147
|
-
}
|
|
4373
|
+
if (wantsVercel && vercelToken) {
|
|
4374
|
+
const vercelResult = await deployToVercel({
|
|
4375
|
+
token: vercelToken,
|
|
4376
|
+
githubOwner: deployResult.owner,
|
|
4377
|
+
githubRepo: deployResult.repoName,
|
|
4378
|
+
docsDir: outputDir,
|
|
4379
|
+
config,
|
|
4380
|
+
scope: vercelScope
|
|
4381
|
+
});
|
|
4382
|
+
if (vercelResult) {
|
|
4383
|
+
p8.log.success(`Live at: ${vercelResult.deploymentUrl}`);
|
|
4384
|
+
vercelDeployed = true;
|
|
4148
4385
|
}
|
|
4149
4386
|
}
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
if (!vercelDeployed) {
|
|
4164
|
-
showVercelInstructions(deployResult.owner, deployResult.repoName);
|
|
4387
|
+
if (wantsCi) {
|
|
4388
|
+
const gitRoot = getGitRoot();
|
|
4389
|
+
if (!gitRoot) {
|
|
4390
|
+
p8.log.warn("Not in a git repository \u2014 skipping CI setup. Run `open-auto-doc setup-ci` from your project root later.");
|
|
4391
|
+
} else {
|
|
4392
|
+
await createCiWorkflow({
|
|
4393
|
+
gitRoot,
|
|
4394
|
+
docsRepoUrl: deployResult.repoUrl,
|
|
4395
|
+
outputDir,
|
|
4396
|
+
token,
|
|
4397
|
+
config,
|
|
4398
|
+
branch: ciBranch
|
|
4399
|
+
});
|
|
4165
4400
|
}
|
|
4166
|
-
p8.outro(`Docs repo: https://github.com/${deployResult.owner}/${deployResult.repoName}`);
|
|
4167
|
-
return;
|
|
4168
4401
|
}
|
|
4169
|
-
const ciResult = await createCiWorkflow({
|
|
4170
|
-
gitRoot,
|
|
4171
|
-
docsRepoUrl: deployResult.repoUrl,
|
|
4172
|
-
outputDir,
|
|
4173
|
-
token,
|
|
4174
|
-
config
|
|
4175
|
-
});
|
|
4176
4402
|
if (!vercelDeployed) {
|
|
4177
4403
|
showVercelInstructions(deployResult.owner, deployResult.repoName);
|
|
4178
4404
|
}
|
|
@@ -4579,7 +4805,7 @@ async function logoutCommand() {
|
|
|
4579
4805
|
|
|
4580
4806
|
// src/index.ts
|
|
4581
4807
|
var program = new Command();
|
|
4582
|
-
program.name("open-auto-doc").description("Auto-generate beautiful documentation websites from GitHub repositories using AI").version("0.5.
|
|
4808
|
+
program.name("open-auto-doc").description("Auto-generate beautiful documentation websites from GitHub repositories using AI").version("0.5.4");
|
|
4583
4809
|
program.command("init", { isDefault: true }).description("Initialize and generate documentation for your repositories").option("-o, --output <dir>", "Output directory", "docs-site").action(initCommand);
|
|
4584
4810
|
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);
|
|
4585
4811
|
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);
|