docs-agents-md 0.1.0
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/README.md +86 -0
- package/dist/cli.js +1104 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# docs-agents-md
|
|
2
|
+
|
|
3
|
+
**Give your AI coding agent the docs it needs — in one command.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/docs-agents-md)
|
|
6
|
+
[](https://github.com/a-maged/docs-agents-md/blob/main/LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx docs-agents-md
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Downloads docs from any GitHub repo and injects a compact, token-optimized index into your `AGENTS.md` or `CLAUDE.md` — so your AI agent uses **real docs** instead of hallucinating.
|
|
14
|
+
|
|
15
|
+
## Get Started
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Pick from 20+ presets interactively
|
|
19
|
+
npx docs-agents-md
|
|
20
|
+
|
|
21
|
+
# Or one-liner
|
|
22
|
+
npx docs-agents-md --lib nextjs
|
|
23
|
+
|
|
24
|
+
# Any GitHub repo
|
|
25
|
+
npx docs-agents-md --repo vercel/next.js --name nextjs --docs-path docs
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Presets
|
|
29
|
+
|
|
30
|
+
20 frameworks built-in. Run `npx docs-agents-md list` to see all.
|
|
31
|
+
|
|
32
|
+
| | | | |
|
|
33
|
+
| -------------- | -------- | -------- | ---------------- |
|
|
34
|
+
| `nextjs` | `react` | `vue` | `svelte` |
|
|
35
|
+
| `angular` | `nuxt` | `astro` | `tailwindcss` |
|
|
36
|
+
| `drizzle` | `prisma` | `nestjs` | `express` |
|
|
37
|
+
| `fastify` | `hono` | `trpc` | `tanstack-query` |
|
|
38
|
+
| `react-router` | `vite` | `bun` | `zustand` |
|
|
39
|
+
|
|
40
|
+
> Missing one? Use `--repo` for any GitHub repo, or [open an issue](https://github.com/a-maged/docs-agents-md/issues).
|
|
41
|
+
|
|
42
|
+
## What It Does
|
|
43
|
+
|
|
44
|
+
1. **Downloads** only the docs folder via `git sparse-checkout` (fast, minimal)
|
|
45
|
+
2. **Indexes** all doc files into a single-line, pipe-separated format (minimal tokens)
|
|
46
|
+
3. **Injects** into your agent file with namespaced markers (multiple libs coexist)
|
|
47
|
+
4. **Auto-detects** your installed version to match the right docs tag
|
|
48
|
+
5. **Gitignores** the cache directory automatically
|
|
49
|
+
|
|
50
|
+
## Multiple Libraries
|
|
51
|
+
|
|
52
|
+
Each library gets its own marker block — they coexist and update independently:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx docs-agents-md --lib react --output AGENTS.md
|
|
56
|
+
npx docs-agents-md --lib drizzle --output AGENTS.md
|
|
57
|
+
npx docs-agents-md --lib tailwindcss --output AGENTS.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
| Flag | Description | Default |
|
|
63
|
+
| --------------------- | ------------------------------------- | --------------- |
|
|
64
|
+
| `--lib <name>` | Built-in preset | — |
|
|
65
|
+
| `--repo <owner/repo>` | Any GitHub repository | — |
|
|
66
|
+
| `--name <name>` | Library name (required with `--repo`) | — |
|
|
67
|
+
| `--tag <tag>` | Git tag or branch | `main` / preset |
|
|
68
|
+
| `--docs-path <path>` | Docs folder in repo | `docs` |
|
|
69
|
+
| `--output <file>` | Target file | `AGENTS.md` |
|
|
70
|
+
| `--extensions <exts>` | File types to index | `md,mdx` |
|
|
71
|
+
|
|
72
|
+
Set `GITHUB_TOKEN` env var for higher API rate limits (5,000/hr vs 60/hr).
|
|
73
|
+
|
|
74
|
+
## Inspiration
|
|
75
|
+
|
|
76
|
+
Inspired by Vercel's research on [`AGENTS.md`](https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals), which showed that documentation context via `AGENTS.md` significantly outperforms other approaches in agent evaluations.
|
|
77
|
+
|
|
78
|
+
## Contributing
|
|
79
|
+
|
|
80
|
+
1. Add an entry to [`src/lib/registry.ts`](src/lib/registry.ts)
|
|
81
|
+
2. Run `npm test`
|
|
82
|
+
3. Open a PR
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import fs5 from "fs";
|
|
5
|
+
import path6 from "path";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import prompts2 from "prompts";
|
|
9
|
+
import pc2 from "picocolors";
|
|
10
|
+
|
|
11
|
+
// src/lib/git.ts
|
|
12
|
+
import { execFileSync } from "child_process";
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import os from "os";
|
|
16
|
+
|
|
17
|
+
// src/lib/constants.ts
|
|
18
|
+
var DOCS_BASE_DIR = ".agents-docs";
|
|
19
|
+
var DEFAULT_OUTPUT = "AGENTS.md";
|
|
20
|
+
var REPO_REGEX2 = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/;
|
|
21
|
+
var TAG_REGEX = /^[a-zA-Z0-9._\-\/+@]+$/;
|
|
22
|
+
var NAME_REGEX = /^[a-z0-9._-]+$/;
|
|
23
|
+
function formatSize(bytes) {
|
|
24
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
25
|
+
const kb = bytes / 1024;
|
|
26
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
27
|
+
const mb = kb / 1024;
|
|
28
|
+
return `${mb.toFixed(1)} MB`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/lib/git.ts
|
|
32
|
+
function validateRepo(repo) {
|
|
33
|
+
if (!REPO_REGEX2.test(repo)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Invalid repo format: "${repo}". Expected: owner/repo (e.g., vercel/next.js)`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function validateTag(tag) {
|
|
40
|
+
if (!TAG_REGEX.test(tag)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Invalid tag format: "${tag}". Expected a git tag or branch name (e.g., v15.1.0, main)`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function validateDocsPath(docsPath) {
|
|
47
|
+
if (!docsPath || docsPath.includes("..") || path.isAbsolute(docsPath)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Invalid docs path: "${docsPath}". Must be a non-empty relative path without "..".`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function cloneDocs(options) {
|
|
54
|
+
const { repo, tag, docsPath, destDir, timeoutMs = 6e4 } = options;
|
|
55
|
+
validateRepo(repo);
|
|
56
|
+
validateTag(tag);
|
|
57
|
+
validateDocsPath(docsPath);
|
|
58
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "docs-agents-md-"));
|
|
59
|
+
try {
|
|
60
|
+
try {
|
|
61
|
+
execFileSync(
|
|
62
|
+
"git",
|
|
63
|
+
[
|
|
64
|
+
"clone",
|
|
65
|
+
"--depth",
|
|
66
|
+
"1",
|
|
67
|
+
"--filter=blob:none",
|
|
68
|
+
"--sparse",
|
|
69
|
+
"--branch",
|
|
70
|
+
tag,
|
|
71
|
+
`https://github.com/${repo}.git`,
|
|
72
|
+
"."
|
|
73
|
+
],
|
|
74
|
+
{ cwd: tempDir, stdio: "pipe", timeout: timeoutMs }
|
|
75
|
+
);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
+
if (error.killed || error.signal === "SIGTERM") {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Git clone timed out after ${timeoutMs / 1e3}s. Check your network or try again.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (message.includes("not found") || message.includes("did not match")) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Could not find tag/branch "${tag}" in repo "${repo}". Verify it exists on GitHub.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (message.includes("Could not resolve host") || message.includes("unable to access")) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Network error: Could not reach GitHub. Check your internet connection.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
execFileSync("git", ["sparse-checkout", "set", docsPath], {
|
|
96
|
+
cwd: tempDir,
|
|
97
|
+
stdio: "pipe",
|
|
98
|
+
timeout: 1e4
|
|
99
|
+
});
|
|
100
|
+
const sourceDocsDir = path.join(tempDir, docsPath);
|
|
101
|
+
if (!fs.existsSync(sourceDocsDir)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Docs folder "${docsPath}" not found in ${repo}@${tag}. Check the --docs-path value.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (fs.existsSync(destDir)) {
|
|
107
|
+
fs.rmSync(destDir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
110
|
+
fs.cpSync(sourceDocsDir, destDir, { recursive: true });
|
|
111
|
+
return { success: true };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: error instanceof Error ? error.message : String(error)
|
|
116
|
+
};
|
|
117
|
+
} finally {
|
|
118
|
+
if (fs.existsSync(tempDir)) {
|
|
119
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/lib/tree.ts
|
|
125
|
+
import fs2 from "fs";
|
|
126
|
+
import path2 from "path";
|
|
127
|
+
function collectDocFiles(dir, extensions = ["md", "mdx"]) {
|
|
128
|
+
if (!fs2.existsSync(dir)) return [];
|
|
129
|
+
const extSet = new Set(extensions.map((e) => `.${e.toLowerCase()}`));
|
|
130
|
+
return fs2.readdirSync(dir, { recursive: true, encoding: "utf-8" }).map((f) => f.split(path2.sep).join("/")).filter((f) => {
|
|
131
|
+
const ext = f.slice(f.lastIndexOf(".")).toLowerCase();
|
|
132
|
+
if (!extSet.has(ext)) return false;
|
|
133
|
+
const basename = f.split("/").pop() || "";
|
|
134
|
+
if (basename.startsWith("index.")) return false;
|
|
135
|
+
return true;
|
|
136
|
+
}).sort().map((f) => ({ relativePath: f }));
|
|
137
|
+
}
|
|
138
|
+
function buildDocTree(files) {
|
|
139
|
+
const rootFiles = [];
|
|
140
|
+
const root = /* @__PURE__ */ new Map();
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
const parts = file.relativePath.split("/");
|
|
143
|
+
if (parts.length < 2) {
|
|
144
|
+
rootFiles.push(file);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const topDir = parts[0];
|
|
148
|
+
if (!root.has(topDir)) {
|
|
149
|
+
root.set(topDir, { files: [], children: [] });
|
|
150
|
+
}
|
|
151
|
+
const entry = root.get(topDir);
|
|
152
|
+
if (parts.length === 2) {
|
|
153
|
+
entry.files.push(file);
|
|
154
|
+
} else {
|
|
155
|
+
entry.children.push({
|
|
156
|
+
relativePath: parts.slice(1).join("/")
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const sections = [];
|
|
161
|
+
if (rootFiles.length > 0) {
|
|
162
|
+
sections.push({
|
|
163
|
+
name: ".",
|
|
164
|
+
files: rootFiles.sort(
|
|
165
|
+
(a, b) => a.relativePath.localeCompare(b.relativePath)
|
|
166
|
+
),
|
|
167
|
+
subsections: []
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
for (const [name, entry] of root) {
|
|
171
|
+
const subsections = buildDocTree(entry.children);
|
|
172
|
+
restoreFullPaths(subsections, name);
|
|
173
|
+
sections.push({
|
|
174
|
+
name,
|
|
175
|
+
files: entry.files,
|
|
176
|
+
subsections
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
sections.sort((a, b) => {
|
|
180
|
+
if (a.name === ".") return -1;
|
|
181
|
+
if (b.name === ".") return 1;
|
|
182
|
+
return a.name.localeCompare(b.name);
|
|
183
|
+
});
|
|
184
|
+
for (const section of sections) {
|
|
185
|
+
section.files.sort(
|
|
186
|
+
(a, b) => a.relativePath.localeCompare(b.relativePath)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return sections;
|
|
190
|
+
}
|
|
191
|
+
function restoreFullPaths(sections, parentDir) {
|
|
192
|
+
for (const section of sections) {
|
|
193
|
+
section.files = section.files.map((f) => ({
|
|
194
|
+
relativePath: `${parentDir}/${f.relativePath}`
|
|
195
|
+
}));
|
|
196
|
+
restoreFullPaths(section.subsections, `${parentDir}/${section.name}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/lib/index-generator.ts
|
|
201
|
+
function generateIndex(sections, meta) {
|
|
202
|
+
const { name, docsPath, version, outputFile, libKey } = meta;
|
|
203
|
+
const parts = [];
|
|
204
|
+
const versionSuffix = version ? ` v${version}` : "";
|
|
205
|
+
parts.push(`[${name} Docs Index${versionSuffix}]`);
|
|
206
|
+
parts.push(`root: ${docsPath}`);
|
|
207
|
+
parts.push(
|
|
208
|
+
`IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning for any ${name} tasks.`
|
|
209
|
+
);
|
|
210
|
+
const target = outputFile || DEFAULT_OUTPUT;
|
|
211
|
+
let regenCmd = "npx docs-agents-md";
|
|
212
|
+
if (libKey) {
|
|
213
|
+
regenCmd += ` --lib ${libKey}`;
|
|
214
|
+
} else if (meta.repo) {
|
|
215
|
+
regenCmd += ` --repo ${meta.repo} --name ${name.toLowerCase()}`;
|
|
216
|
+
if (meta.repoDocsPath) regenCmd += ` --docs-path ${meta.repoDocsPath}`;
|
|
217
|
+
}
|
|
218
|
+
regenCmd += ` --output ${target}`;
|
|
219
|
+
parts.push(`If docs missing, run: ${regenCmd}`);
|
|
220
|
+
const allFiles = collectAllFiles(sections);
|
|
221
|
+
const grouped = groupByDirectory(allFiles);
|
|
222
|
+
for (const [dir, files] of grouped) {
|
|
223
|
+
const safeFiles = files.map(
|
|
224
|
+
(f) => f.replace(/\|/g, "%7C").replace(/,/g, "%2C").replace(/\{/g, "%7B").replace(/\}/g, "%7D")
|
|
225
|
+
);
|
|
226
|
+
parts.push(`${dir}:{${safeFiles.join(",")}}`);
|
|
227
|
+
}
|
|
228
|
+
return parts.join("|");
|
|
229
|
+
}
|
|
230
|
+
function collectAllFiles(sections) {
|
|
231
|
+
const files = [];
|
|
232
|
+
for (const section of sections) {
|
|
233
|
+
for (const file of section.files) {
|
|
234
|
+
files.push(file.relativePath);
|
|
235
|
+
}
|
|
236
|
+
files.push(...collectAllFiles(section.subsections));
|
|
237
|
+
}
|
|
238
|
+
return files;
|
|
239
|
+
}
|
|
240
|
+
function groupByDirectory(files) {
|
|
241
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
242
|
+
for (const filePath of files) {
|
|
243
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
244
|
+
const dir = lastSlash === -1 ? "." : filePath.slice(0, lastSlash);
|
|
245
|
+
const fileName = lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1);
|
|
246
|
+
const existing = grouped.get(dir);
|
|
247
|
+
if (existing) {
|
|
248
|
+
existing.push(fileName);
|
|
249
|
+
} else {
|
|
250
|
+
grouped.set(dir, [fileName]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return grouped;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/lib/inject.ts
|
|
257
|
+
function startMarker(name) {
|
|
258
|
+
return `<!-- DOCS-AGENTS-MD:${name}-START -->`;
|
|
259
|
+
}
|
|
260
|
+
function endMarker(name) {
|
|
261
|
+
return `<!-- DOCS-AGENTS-MD:${name}-END -->`;
|
|
262
|
+
}
|
|
263
|
+
function hasExistingIndex(content, name) {
|
|
264
|
+
return content.includes(startMarker(name));
|
|
265
|
+
}
|
|
266
|
+
function injectIndex(existingContent, indexContent, name) {
|
|
267
|
+
const start = startMarker(name);
|
|
268
|
+
const end = endMarker(name);
|
|
269
|
+
const wrappedContent = `${start}${indexContent}${end}`;
|
|
270
|
+
if (hasExistingIndex(existingContent, name)) {
|
|
271
|
+
const startIdx = existingContent.indexOf(start);
|
|
272
|
+
const endIdx = existingContent.indexOf(end) + end.length;
|
|
273
|
+
if (existingContent.indexOf(end) === -1) {
|
|
274
|
+
const cleaned = existingContent.replace(start, "");
|
|
275
|
+
return injectIndex(cleaned, indexContent, name);
|
|
276
|
+
}
|
|
277
|
+
if (endIdx <= startIdx) {
|
|
278
|
+
const cleaned = existingContent.replace(start, "").replace(endMarker(name), "");
|
|
279
|
+
return injectIndex(cleaned, indexContent, name);
|
|
280
|
+
}
|
|
281
|
+
return existingContent.slice(0, startIdx) + wrappedContent + existingContent.slice(endIdx);
|
|
282
|
+
}
|
|
283
|
+
if (existingContent === "") {
|
|
284
|
+
return wrappedContent + "\n";
|
|
285
|
+
}
|
|
286
|
+
const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
|
|
287
|
+
return existingContent + separator + wrappedContent + "\n";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/lib/gitignore.ts
|
|
291
|
+
import fs3 from "fs";
|
|
292
|
+
import path3 from "path";
|
|
293
|
+
var GITIGNORE_ENTRY = `${DOCS_BASE_DIR}/`;
|
|
294
|
+
var COMMENT_HEADER = "# docs-agents-md";
|
|
295
|
+
function ensureGitignoreEntry(cwd) {
|
|
296
|
+
const gitignorePath = path3.join(cwd, ".gitignore");
|
|
297
|
+
const entryRegex = new RegExp(`^\\s*\\${DOCS_BASE_DIR}(?:\\/.*)?$`);
|
|
298
|
+
let content = "";
|
|
299
|
+
if (fs3.existsSync(gitignorePath)) {
|
|
300
|
+
content = fs3.readFileSync(gitignorePath, "utf-8");
|
|
301
|
+
}
|
|
302
|
+
const hasEntry = content.split(/\r?\n/).some((line) => entryRegex.test(line));
|
|
303
|
+
if (hasEntry) {
|
|
304
|
+
return { path: gitignorePath, updated: false, alreadyPresent: true };
|
|
305
|
+
}
|
|
306
|
+
const needsNewline = content.length > 0 && !content.endsWith("\n");
|
|
307
|
+
const hasComment = content.includes(COMMENT_HEADER);
|
|
308
|
+
const header = hasComment ? "" : `${COMMENT_HEADER}
|
|
309
|
+
`;
|
|
310
|
+
const newContent = content + (needsNewline ? "\n" : "") + header + `${GITIGNORE_ENTRY}
|
|
311
|
+
`;
|
|
312
|
+
fs3.writeFileSync(gitignorePath, newContent, "utf-8");
|
|
313
|
+
return { path: gitignorePath, updated: true, alreadyPresent: false };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/lib/registry.ts
|
|
317
|
+
var REGISTRY = {
|
|
318
|
+
nextjs: {
|
|
319
|
+
repo: "vercel/next.js",
|
|
320
|
+
docsPath: "docs",
|
|
321
|
+
defaultTag: "canary",
|
|
322
|
+
name: "Next.js",
|
|
323
|
+
packages: ["next"],
|
|
324
|
+
tagPrefix: "v"
|
|
325
|
+
},
|
|
326
|
+
react: {
|
|
327
|
+
repo: "reactjs/react.dev",
|
|
328
|
+
docsPath: "src/content",
|
|
329
|
+
defaultTag: "main",
|
|
330
|
+
name: "React",
|
|
331
|
+
packages: ["react"],
|
|
332
|
+
tagPrefix: null
|
|
333
|
+
},
|
|
334
|
+
vue: {
|
|
335
|
+
repo: "vuejs/docs",
|
|
336
|
+
docsPath: "src",
|
|
337
|
+
defaultTag: "main",
|
|
338
|
+
name: "Vue",
|
|
339
|
+
packages: ["vue"],
|
|
340
|
+
tagPrefix: null
|
|
341
|
+
},
|
|
342
|
+
svelte: {
|
|
343
|
+
repo: "sveltejs/svelte",
|
|
344
|
+
docsPath: "documentation/docs",
|
|
345
|
+
defaultTag: "main",
|
|
346
|
+
name: "Svelte",
|
|
347
|
+
packages: ["svelte"],
|
|
348
|
+
tagPrefix: "svelte@"
|
|
349
|
+
},
|
|
350
|
+
astro: {
|
|
351
|
+
repo: "withastro/docs",
|
|
352
|
+
docsPath: "src/content/docs",
|
|
353
|
+
defaultTag: "main",
|
|
354
|
+
name: "Astro",
|
|
355
|
+
packages: ["astro"],
|
|
356
|
+
tagPrefix: null
|
|
357
|
+
},
|
|
358
|
+
drizzle: {
|
|
359
|
+
repo: "drizzle-team/drizzle-orm-docs",
|
|
360
|
+
docsPath: "src/content",
|
|
361
|
+
defaultTag: "main",
|
|
362
|
+
name: "Drizzle ORM",
|
|
363
|
+
packages: ["drizzle-orm"],
|
|
364
|
+
tagPrefix: null
|
|
365
|
+
},
|
|
366
|
+
hono: {
|
|
367
|
+
repo: "honojs/hono",
|
|
368
|
+
docsPath: "docs",
|
|
369
|
+
defaultTag: "main",
|
|
370
|
+
name: "Hono",
|
|
371
|
+
packages: ["hono"],
|
|
372
|
+
tagPrefix: "v"
|
|
373
|
+
},
|
|
374
|
+
nestjs: {
|
|
375
|
+
repo: "nestjs/docs.nestjs.com",
|
|
376
|
+
docsPath: "content",
|
|
377
|
+
defaultTag: "master",
|
|
378
|
+
name: "NestJS",
|
|
379
|
+
packages: ["@nestjs/core"],
|
|
380
|
+
tagPrefix: null
|
|
381
|
+
},
|
|
382
|
+
angular: {
|
|
383
|
+
repo: "angular/angular",
|
|
384
|
+
docsPath: "adev/src/content",
|
|
385
|
+
defaultTag: "main",
|
|
386
|
+
name: "Angular",
|
|
387
|
+
packages: ["@angular/core"],
|
|
388
|
+
tagPrefix: ""
|
|
389
|
+
},
|
|
390
|
+
nuxt: {
|
|
391
|
+
repo: "nuxt/nuxt",
|
|
392
|
+
docsPath: "docs",
|
|
393
|
+
defaultTag: "main",
|
|
394
|
+
name: "Nuxt",
|
|
395
|
+
packages: ["nuxt"],
|
|
396
|
+
tagPrefix: "v"
|
|
397
|
+
},
|
|
398
|
+
"react-router": {
|
|
399
|
+
repo: "remix-run/react-router",
|
|
400
|
+
docsPath: "docs",
|
|
401
|
+
defaultTag: "main",
|
|
402
|
+
name: "React Router",
|
|
403
|
+
packages: ["react-router"],
|
|
404
|
+
tagPrefix: "react-router@"
|
|
405
|
+
},
|
|
406
|
+
express: {
|
|
407
|
+
repo: "expressjs/expressjs.com",
|
|
408
|
+
docsPath: "en",
|
|
409
|
+
defaultTag: "gh-pages",
|
|
410
|
+
name: "Express",
|
|
411
|
+
packages: ["express"],
|
|
412
|
+
tagPrefix: null
|
|
413
|
+
},
|
|
414
|
+
fastify: {
|
|
415
|
+
repo: "fastify/fastify",
|
|
416
|
+
docsPath: "docs",
|
|
417
|
+
defaultTag: "main",
|
|
418
|
+
name: "Fastify",
|
|
419
|
+
packages: ["fastify"],
|
|
420
|
+
tagPrefix: "v"
|
|
421
|
+
},
|
|
422
|
+
prisma: {
|
|
423
|
+
repo: "prisma/docs",
|
|
424
|
+
docsPath: "content",
|
|
425
|
+
defaultTag: "main",
|
|
426
|
+
name: "Prisma",
|
|
427
|
+
packages: ["prisma"],
|
|
428
|
+
tagPrefix: null
|
|
429
|
+
},
|
|
430
|
+
"tanstack-query": {
|
|
431
|
+
repo: "TanStack/query",
|
|
432
|
+
docsPath: "docs",
|
|
433
|
+
defaultTag: "main",
|
|
434
|
+
name: "TanStack Query",
|
|
435
|
+
packages: ["@tanstack/react-query"],
|
|
436
|
+
tagPrefix: "v"
|
|
437
|
+
},
|
|
438
|
+
vite: {
|
|
439
|
+
repo: "vitejs/vite",
|
|
440
|
+
docsPath: "docs",
|
|
441
|
+
defaultTag: "main",
|
|
442
|
+
name: "Vite",
|
|
443
|
+
packages: ["vite"],
|
|
444
|
+
tagPrefix: "v"
|
|
445
|
+
},
|
|
446
|
+
tailwindcss: {
|
|
447
|
+
repo: "tailwindlabs/tailwindcss.com",
|
|
448
|
+
docsPath: "src/docs",
|
|
449
|
+
defaultTag: "main",
|
|
450
|
+
name: "Tailwind CSS",
|
|
451
|
+
packages: ["tailwindcss"],
|
|
452
|
+
tagPrefix: null
|
|
453
|
+
},
|
|
454
|
+
trpc: {
|
|
455
|
+
repo: "trpc/trpc",
|
|
456
|
+
docsPath: "www/docs",
|
|
457
|
+
defaultTag: "main",
|
|
458
|
+
name: "tRPC",
|
|
459
|
+
packages: ["@trpc/server"],
|
|
460
|
+
tagPrefix: null
|
|
461
|
+
},
|
|
462
|
+
bun: {
|
|
463
|
+
repo: "oven-sh/bun",
|
|
464
|
+
docsPath: "docs",
|
|
465
|
+
defaultTag: "main",
|
|
466
|
+
name: "Bun",
|
|
467
|
+
packages: ["bun-types"],
|
|
468
|
+
tagPrefix: "bun-v"
|
|
469
|
+
},
|
|
470
|
+
zustand: {
|
|
471
|
+
repo: "pmndrs/zustand",
|
|
472
|
+
docsPath: "docs",
|
|
473
|
+
defaultTag: "main",
|
|
474
|
+
name: "Zustand",
|
|
475
|
+
packages: ["zustand"],
|
|
476
|
+
tagPrefix: "v"
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
function getRegistryEntry(key) {
|
|
480
|
+
return REGISTRY[key.toLowerCase()] ?? null;
|
|
481
|
+
}
|
|
482
|
+
function listRegistryKeys() {
|
|
483
|
+
return Object.keys(REGISTRY).sort();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/lib/docs-detector.ts
|
|
487
|
+
import https from "https";
|
|
488
|
+
import path4 from "path";
|
|
489
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
490
|
+
"node_modules",
|
|
491
|
+
".github",
|
|
492
|
+
".git",
|
|
493
|
+
"examples",
|
|
494
|
+
"example",
|
|
495
|
+
"__tests__",
|
|
496
|
+
"test",
|
|
497
|
+
"tests",
|
|
498
|
+
"__mocks__",
|
|
499
|
+
"fixtures",
|
|
500
|
+
".vitepress",
|
|
501
|
+
"public",
|
|
502
|
+
"images",
|
|
503
|
+
"icons",
|
|
504
|
+
"logo",
|
|
505
|
+
"snippets"
|
|
506
|
+
]);
|
|
507
|
+
var DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx"]);
|
|
508
|
+
async function fetchRepoTree(repo, tag) {
|
|
509
|
+
try {
|
|
510
|
+
const commitData = await githubGet(
|
|
511
|
+
`/repos/${repo}/commits/${tag}`
|
|
512
|
+
);
|
|
513
|
+
if (!commitData?.commit?.tree?.sha) return null;
|
|
514
|
+
const treeData = await githubGet(`/repos/${repo}/git/trees/${commitData.commit.tree.sha}?recursive=1`);
|
|
515
|
+
if (!treeData?.tree) return null;
|
|
516
|
+
return treeData.tree.filter((entry) => entry.type === "blob").map((entry) => entry.path);
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function detectDocsPath(paths) {
|
|
522
|
+
const dirCounts = /* @__PURE__ */ new Map();
|
|
523
|
+
for (const filePath of paths) {
|
|
524
|
+
const ext = path4.posix.extname(filePath).toLowerCase();
|
|
525
|
+
if (!DOC_EXTENSIONS.has(ext)) continue;
|
|
526
|
+
const dir = path4.posix.dirname(filePath);
|
|
527
|
+
if (dir === ".") continue;
|
|
528
|
+
const parts = dir.split("/");
|
|
529
|
+
if (parts.some((p) => EXCLUDED_DIRS.has(p))) continue;
|
|
530
|
+
dirCounts.set(dir, (dirCounts.get(dir) || 0) + 1);
|
|
531
|
+
}
|
|
532
|
+
if (dirCounts.size === 0) return null;
|
|
533
|
+
const aggregated = /* @__PURE__ */ new Map();
|
|
534
|
+
for (const [dir, count] of dirCounts) {
|
|
535
|
+
const parts = dir.split("/");
|
|
536
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
537
|
+
const ancestor = parts.slice(0, i).join("/");
|
|
538
|
+
if (EXCLUDED_DIRS.has(parts[i - 1])) continue;
|
|
539
|
+
aggregated.set(ancestor, (aggregated.get(ancestor) || 0) + count);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const candidates = [...aggregated.entries()].filter(([, count]) => count >= 3).map(([dir, count]) => ({
|
|
543
|
+
dir,
|
|
544
|
+
count,
|
|
545
|
+
depth: dir.split("/").length
|
|
546
|
+
}));
|
|
547
|
+
if (candidates.length === 0) return null;
|
|
548
|
+
const DOCS_LIKE_NAMES = /* @__PURE__ */ new Set([
|
|
549
|
+
"docs",
|
|
550
|
+
"doc",
|
|
551
|
+
"documentation",
|
|
552
|
+
"content",
|
|
553
|
+
"guide",
|
|
554
|
+
"guides",
|
|
555
|
+
"wiki",
|
|
556
|
+
"reference",
|
|
557
|
+
"manual",
|
|
558
|
+
"pages"
|
|
559
|
+
]);
|
|
560
|
+
candidates.sort((a, b) => {
|
|
561
|
+
const aLeaf = a.dir.split("/").pop() || "";
|
|
562
|
+
const bLeaf = b.dir.split("/").pop() || "";
|
|
563
|
+
const aBonus = DOCS_LIKE_NAMES.has(aLeaf) ? 1 : 0;
|
|
564
|
+
const bBonus = DOCS_LIKE_NAMES.has(bLeaf) ? 1 : 0;
|
|
565
|
+
if (bBonus !== aBonus) return bBonus - aBonus;
|
|
566
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
567
|
+
return a.depth - b.depth;
|
|
568
|
+
});
|
|
569
|
+
return candidates[0].dir;
|
|
570
|
+
}
|
|
571
|
+
function githubGet(apiPath) {
|
|
572
|
+
return new Promise((resolve) => {
|
|
573
|
+
const options = {
|
|
574
|
+
hostname: "api.github.com",
|
|
575
|
+
path: apiPath,
|
|
576
|
+
headers: {
|
|
577
|
+
"User-Agent": "docs-agents-md",
|
|
578
|
+
Accept: "application/vnd.github.v3+json",
|
|
579
|
+
...process.env.GITHUB_TOKEN && {
|
|
580
|
+
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
timeout: 15e3
|
|
584
|
+
};
|
|
585
|
+
const req = https.get(options, (res) => {
|
|
586
|
+
if (res.statusCode === 403 || res.statusCode === 429) {
|
|
587
|
+
res.resume();
|
|
588
|
+
resolve(null);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (res.statusCode !== 200) {
|
|
592
|
+
res.resume();
|
|
593
|
+
resolve(null);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
let data = "";
|
|
597
|
+
res.on("data", (chunk) => {
|
|
598
|
+
data += chunk.toString();
|
|
599
|
+
});
|
|
600
|
+
res.on("end", () => {
|
|
601
|
+
try {
|
|
602
|
+
resolve(JSON.parse(data));
|
|
603
|
+
} catch {
|
|
604
|
+
resolve(null);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
req.on("error", () => resolve(null));
|
|
609
|
+
req.on("timeout", () => {
|
|
610
|
+
req.destroy();
|
|
611
|
+
resolve(null);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/lib/version-detector.ts
|
|
617
|
+
import fs4 from "fs";
|
|
618
|
+
import path5 from "path";
|
|
619
|
+
function detectVersion(packages, tagPrefix) {
|
|
620
|
+
try {
|
|
621
|
+
const cwd = process.cwd();
|
|
622
|
+
const packageJsonPath = path5.join(cwd, "package.json");
|
|
623
|
+
if (!fs4.existsSync(packageJsonPath)) {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
const raw = fs4.readFileSync(packageJsonPath, "utf-8");
|
|
627
|
+
const packageJson = JSON.parse(raw);
|
|
628
|
+
const dependencies = {
|
|
629
|
+
...packageJson.dependencies,
|
|
630
|
+
...packageJson.devDependencies,
|
|
631
|
+
...packageJson.peerDependencies
|
|
632
|
+
};
|
|
633
|
+
for (const pkgName of packages) {
|
|
634
|
+
const specifier = dependencies[pkgName];
|
|
635
|
+
if (!specifier || typeof specifier !== "string") continue;
|
|
636
|
+
let version = specifier;
|
|
637
|
+
if (version.startsWith("npm:")) {
|
|
638
|
+
const atIdx = version.lastIndexOf("@");
|
|
639
|
+
if (atIdx > 4) {
|
|
640
|
+
version = version.slice(atIdx + 1);
|
|
641
|
+
} else {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
version = version.replace(/^workspace:/, "").replace(/^[~^>=]+/, "");
|
|
646
|
+
if (!/\d/.test(version)) continue;
|
|
647
|
+
if (tagPrefix === null) {
|
|
648
|
+
return { packageName: pkgName, version, gitTag: null };
|
|
649
|
+
}
|
|
650
|
+
const gitTag = tagPrefix + version;
|
|
651
|
+
return { packageName: pkgName, version, gitTag };
|
|
652
|
+
}
|
|
653
|
+
} catch {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/lib/prompt-helpers.ts
|
|
660
|
+
import prompts from "prompts";
|
|
661
|
+
import pc from "picocolors";
|
|
662
|
+
var ON_CANCEL = {
|
|
663
|
+
onCancel: () => {
|
|
664
|
+
process.exit(0);
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
async function promptForOutputFile() {
|
|
668
|
+
const outputResponse = await prompts(
|
|
669
|
+
{
|
|
670
|
+
type: "select",
|
|
671
|
+
name: "output",
|
|
672
|
+
message: "Target file",
|
|
673
|
+
choices: [
|
|
674
|
+
{ title: DEFAULT_OUTPUT, value: DEFAULT_OUTPUT },
|
|
675
|
+
{ title: "CLAUDE.md", value: "CLAUDE.md" },
|
|
676
|
+
{ title: "Custom...", value: "__custom__" }
|
|
677
|
+
]
|
|
678
|
+
},
|
|
679
|
+
ON_CANCEL
|
|
680
|
+
);
|
|
681
|
+
if (outputResponse.output === "__custom__") {
|
|
682
|
+
const customResponse = await prompts(
|
|
683
|
+
{
|
|
684
|
+
type: "text",
|
|
685
|
+
name: "file",
|
|
686
|
+
message: "Enter file path",
|
|
687
|
+
initial: DEFAULT_OUTPUT
|
|
688
|
+
},
|
|
689
|
+
ON_CANCEL
|
|
690
|
+
);
|
|
691
|
+
return customResponse.file;
|
|
692
|
+
}
|
|
693
|
+
return outputResponse.output;
|
|
694
|
+
}
|
|
695
|
+
async function confirmOrAutoAccept(opts) {
|
|
696
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
697
|
+
if (isTTY) {
|
|
698
|
+
const confirm = await prompts(
|
|
699
|
+
{
|
|
700
|
+
type: "confirm",
|
|
701
|
+
name: "ok",
|
|
702
|
+
message: opts.confirmMessage,
|
|
703
|
+
initial: true
|
|
704
|
+
},
|
|
705
|
+
ON_CANCEL
|
|
706
|
+
);
|
|
707
|
+
return confirm.ok;
|
|
708
|
+
}
|
|
709
|
+
console.log(pc.green(opts.autoMessage));
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/cli.ts
|
|
714
|
+
async function resolveOptions(flags) {
|
|
715
|
+
const extensions = flags.extensions ? flags.extensions.split(",").map((e) => e.trim()) : ["md", "mdx"];
|
|
716
|
+
const output = flags.output || DEFAULT_OUTPUT;
|
|
717
|
+
if (flags.docsPath !== void 0 && !flags.docsPath.trim()) {
|
|
718
|
+
console.error(pc2.red("Error: --docs-path cannot be empty."));
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
if (flags.lib && flags.repo) {
|
|
722
|
+
console.error(
|
|
723
|
+
pc2.red(
|
|
724
|
+
"Error: --lib and --repo are mutually exclusive. Use one or the other."
|
|
725
|
+
)
|
|
726
|
+
);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
if (flags.lib) {
|
|
730
|
+
const ignored = [];
|
|
731
|
+
if (flags.name) ignored.push("--name");
|
|
732
|
+
if (flags.docsPath) ignored.push("--docs-path");
|
|
733
|
+
if (ignored.length > 0) {
|
|
734
|
+
console.warn(
|
|
735
|
+
pc2.yellow(
|
|
736
|
+
`Warning: ${ignored.join(", ")} ignored when using --lib (registry values used)`
|
|
737
|
+
)
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (flags.lib) {
|
|
742
|
+
const entry = getRegistryEntry(flags.lib);
|
|
743
|
+
if (!entry) {
|
|
744
|
+
console.error(
|
|
745
|
+
pc2.red(
|
|
746
|
+
`Unknown library: "${flags.lib}". Run "docs-agents-md list" to see available presets.`
|
|
747
|
+
)
|
|
748
|
+
);
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
if (flags.extensions && entry.extensions) {
|
|
752
|
+
console.warn(
|
|
753
|
+
pc2.yellow(
|
|
754
|
+
"Warning: --extensions ignored when using --lib (registry values used)"
|
|
755
|
+
)
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
let tag = flags.tag || entry.defaultTag;
|
|
759
|
+
let detectedVersion;
|
|
760
|
+
if (!flags.tag && entry.packages.length > 0 && entry.tagPrefix != null) {
|
|
761
|
+
const detected = detectVersion(entry.packages, entry.tagPrefix);
|
|
762
|
+
if (detected?.gitTag) {
|
|
763
|
+
const accepted = await confirmOrAutoAccept({
|
|
764
|
+
confirmMessage: `Detected ${entry.name} ${detected.version} \u2192 use tag ${pc2.cyan(detected.gitTag)}?`,
|
|
765
|
+
autoMessage: `Auto-detected ${entry.name} ${detected.version} \u2192 ${detected.gitTag}`
|
|
766
|
+
});
|
|
767
|
+
if (accepted) {
|
|
768
|
+
tag = detected.gitTag;
|
|
769
|
+
detectedVersion = detected.version;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
repo: entry.repo,
|
|
775
|
+
tag,
|
|
776
|
+
docsPath: entry.docsPath,
|
|
777
|
+
name: flags.lib.toLowerCase(),
|
|
778
|
+
displayName: entry.name,
|
|
779
|
+
output,
|
|
780
|
+
extensions: entry.extensions || extensions,
|
|
781
|
+
libKey: flags.lib.toLowerCase(),
|
|
782
|
+
detectedVersion
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
if (flags.repo) {
|
|
786
|
+
try {
|
|
787
|
+
validateRepo(flags.repo);
|
|
788
|
+
} catch (e) {
|
|
789
|
+
console.error(pc2.red(`Error: ${e.message}`));
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
if (!flags.name) {
|
|
793
|
+
console.error(
|
|
794
|
+
pc2.red(
|
|
795
|
+
"Error: --name is required when using --repo.\nExample: npx docs-agents-md --repo owner/repo --name mylib --tag main"
|
|
796
|
+
)
|
|
797
|
+
);
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
const safeName = flags.name.trim().toLowerCase();
|
|
801
|
+
if (!NAME_REGEX.test(safeName)) {
|
|
802
|
+
console.error(
|
|
803
|
+
pc2.red(
|
|
804
|
+
`Error: --name "${flags.name}" contains invalid characters. Use only letters, numbers, dots, hyphens, underscores.`
|
|
805
|
+
)
|
|
806
|
+
);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
let docsPath = flags.docsPath || "";
|
|
810
|
+
const tag = flags.tag || "main";
|
|
811
|
+
if (!flags.docsPath) {
|
|
812
|
+
console.log(pc2.gray("Detecting docs folder via GitHub API..."));
|
|
813
|
+
const treePaths = await fetchRepoTree(flags.repo, tag);
|
|
814
|
+
if (treePaths) {
|
|
815
|
+
const detected = detectDocsPath(treePaths);
|
|
816
|
+
if (detected) {
|
|
817
|
+
const accepted = await confirmOrAutoAccept({
|
|
818
|
+
confirmMessage: `Detected docs at "${detected}". Use this path?`,
|
|
819
|
+
autoMessage: `Auto-detected docs path: "${detected}"`
|
|
820
|
+
});
|
|
821
|
+
docsPath = accepted ? detected : "docs";
|
|
822
|
+
if (!accepted) {
|
|
823
|
+
console.log(pc2.yellow('Using default "docs" path'));
|
|
824
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
console.log(
|
|
827
|
+
pc2.yellow(
|
|
828
|
+
'Could not auto-detect docs folder, using default "docs"'
|
|
829
|
+
)
|
|
830
|
+
);
|
|
831
|
+
docsPath = "docs";
|
|
832
|
+
}
|
|
833
|
+
} else {
|
|
834
|
+
docsPath = "docs";
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
repo: flags.repo,
|
|
839
|
+
tag,
|
|
840
|
+
docsPath,
|
|
841
|
+
name: safeName,
|
|
842
|
+
displayName: flags.name.trim(),
|
|
843
|
+
output,
|
|
844
|
+
extensions
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return await promptForOptions(output, extensions);
|
|
848
|
+
}
|
|
849
|
+
async function promptForOptions(defaultOutput, defaultExtensions) {
|
|
850
|
+
console.log(
|
|
851
|
+
pc2.cyan("\n\u{1F4DA} docs-agents-md \u2014 Documentation Index for AI Agents\n")
|
|
852
|
+
);
|
|
853
|
+
const keys = listRegistryKeys();
|
|
854
|
+
const presetChoices = keys.map((k) => ({
|
|
855
|
+
title: `${REGISTRY[k].name} (${REGISTRY[k].repo})`,
|
|
856
|
+
value: k
|
|
857
|
+
}));
|
|
858
|
+
const modeResponse = await prompts2(
|
|
859
|
+
{
|
|
860
|
+
type: "select",
|
|
861
|
+
name: "mode",
|
|
862
|
+
message: "Choose a library or enter custom repo",
|
|
863
|
+
choices: [
|
|
864
|
+
...presetChoices,
|
|
865
|
+
{ title: "Custom GitHub repo...", value: "__custom__" }
|
|
866
|
+
]
|
|
867
|
+
},
|
|
868
|
+
ON_CANCEL
|
|
869
|
+
);
|
|
870
|
+
if (modeResponse.mode !== "__custom__") {
|
|
871
|
+
const entry = REGISTRY[modeResponse.mode];
|
|
872
|
+
let tagInitial = entry.defaultTag;
|
|
873
|
+
let detectedVersion;
|
|
874
|
+
if (entry.packages.length > 0 && entry.tagPrefix != null) {
|
|
875
|
+
const detected = detectVersion(entry.packages, entry.tagPrefix);
|
|
876
|
+
if (detected?.gitTag) {
|
|
877
|
+
tagInitial = detected.gitTag;
|
|
878
|
+
detectedVersion = detected.version;
|
|
879
|
+
console.log(pc2.green(` Detected ${entry.name} ${detected.version}`));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const tagResponse = await prompts2(
|
|
883
|
+
{
|
|
884
|
+
type: "text",
|
|
885
|
+
name: "tag",
|
|
886
|
+
message: `Git tag/branch for ${entry.name}`,
|
|
887
|
+
initial: tagInitial
|
|
888
|
+
},
|
|
889
|
+
ON_CANCEL
|
|
890
|
+
);
|
|
891
|
+
if (tagResponse.tag !== tagInitial) {
|
|
892
|
+
detectedVersion = void 0;
|
|
893
|
+
}
|
|
894
|
+
const output2 = await promptForOutputFile();
|
|
895
|
+
return {
|
|
896
|
+
repo: entry.repo,
|
|
897
|
+
tag: tagResponse.tag,
|
|
898
|
+
docsPath: entry.docsPath,
|
|
899
|
+
name: modeResponse.mode,
|
|
900
|
+
displayName: entry.name,
|
|
901
|
+
output: output2,
|
|
902
|
+
extensions: entry.extensions || defaultExtensions,
|
|
903
|
+
libKey: modeResponse.mode,
|
|
904
|
+
detectedVersion
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
const customResponse = await prompts2(
|
|
908
|
+
[
|
|
909
|
+
{
|
|
910
|
+
type: "text",
|
|
911
|
+
name: "repo",
|
|
912
|
+
message: "GitHub repo (owner/repo)",
|
|
913
|
+
validate: (v) => REPO_REGEX.test(v.trim()) ? true : "Format: owner/repo"
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
type: "text",
|
|
917
|
+
name: "name",
|
|
918
|
+
message: "Library name (used for markers/directory)",
|
|
919
|
+
validate: (v) => {
|
|
920
|
+
const trimmed = v.trim().toLowerCase();
|
|
921
|
+
if (!trimmed) return "Required";
|
|
922
|
+
if (!NAME_REGEX.test(trimmed))
|
|
923
|
+
return "Use only letters, numbers, dots, hyphens, underscores";
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
type: "text",
|
|
929
|
+
name: "tag",
|
|
930
|
+
message: "Git tag or branch",
|
|
931
|
+
initial: "main",
|
|
932
|
+
validate: (v) => TAG_REGEX.test(v.trim()) ? true : "Invalid characters in tag"
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
type: "text",
|
|
936
|
+
name: "docsPath",
|
|
937
|
+
message: "Path to docs folder in repo",
|
|
938
|
+
initial: "docs",
|
|
939
|
+
validate: (v) => {
|
|
940
|
+
const t = v.trim();
|
|
941
|
+
if (!t) return "Required";
|
|
942
|
+
if (t.includes("..")) return "Path traversal (..) not allowed";
|
|
943
|
+
if (path6.isAbsolute(t)) return "Must be a relative path";
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
type: "select",
|
|
949
|
+
name: "output",
|
|
950
|
+
message: "Target file",
|
|
951
|
+
choices: [
|
|
952
|
+
{ title: DEFAULT_OUTPUT, value: DEFAULT_OUTPUT },
|
|
953
|
+
{ title: "CLAUDE.md", value: "CLAUDE.md" },
|
|
954
|
+
{ title: "Custom...", value: "__custom__" }
|
|
955
|
+
]
|
|
956
|
+
}
|
|
957
|
+
],
|
|
958
|
+
ON_CANCEL
|
|
959
|
+
);
|
|
960
|
+
let output = customResponse.output;
|
|
961
|
+
if (output === "__custom__") {
|
|
962
|
+
const customOutputResponse = await prompts2(
|
|
963
|
+
{
|
|
964
|
+
type: "text",
|
|
965
|
+
name: "file",
|
|
966
|
+
message: "Enter file path",
|
|
967
|
+
initial: DEFAULT_OUTPUT
|
|
968
|
+
},
|
|
969
|
+
ON_CANCEL
|
|
970
|
+
);
|
|
971
|
+
output = customOutputResponse.file;
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
repo: customResponse.repo.trim(),
|
|
975
|
+
tag: customResponse.tag.trim(),
|
|
976
|
+
docsPath: customResponse.docsPath.trim(),
|
|
977
|
+
name: customResponse.name.trim().toLowerCase(),
|
|
978
|
+
displayName: customResponse.name.trim(),
|
|
979
|
+
output,
|
|
980
|
+
extensions: defaultExtensions
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
async function runAdd(flags) {
|
|
984
|
+
const options = await resolveOptions(flags);
|
|
985
|
+
const cwd = process.cwd();
|
|
986
|
+
const docsDir = path6.join(cwd, DOCS_BASE_DIR, options.name);
|
|
987
|
+
const docsLinkPath = `./${DOCS_BASE_DIR}/${options.name}`;
|
|
988
|
+
const targetPath = path6.resolve(cwd, options.output);
|
|
989
|
+
if (!targetPath.startsWith(cwd + path6.sep)) {
|
|
990
|
+
console.error(
|
|
991
|
+
pc2.red(
|
|
992
|
+
`Error: Output path "${options.output}" resolves outside the project directory.`
|
|
993
|
+
)
|
|
994
|
+
);
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
let existingContent = "";
|
|
998
|
+
let sizeBefore = 0;
|
|
999
|
+
let isNewFile = true;
|
|
1000
|
+
if (fs5.existsSync(targetPath)) {
|
|
1001
|
+
existingContent = fs5.readFileSync(targetPath, "utf-8");
|
|
1002
|
+
sizeBefore = Buffer.byteLength(existingContent, "utf-8");
|
|
1003
|
+
isNewFile = false;
|
|
1004
|
+
}
|
|
1005
|
+
console.log(
|
|
1006
|
+
`
|
|
1007
|
+
Downloading ${pc2.cyan(options.displayName)} docs from ${pc2.gray(options.repo)}@${pc2.cyan(options.tag)}...`
|
|
1008
|
+
);
|
|
1009
|
+
const result = cloneDocs({
|
|
1010
|
+
repo: options.repo,
|
|
1011
|
+
tag: options.tag,
|
|
1012
|
+
docsPath: options.docsPath,
|
|
1013
|
+
destDir: docsDir
|
|
1014
|
+
});
|
|
1015
|
+
if (!result.success) {
|
|
1016
|
+
console.error(pc2.red(`
|
|
1017
|
+
\u2717 ${result.error}`));
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
const docFiles = collectDocFiles(docsDir, options.extensions);
|
|
1021
|
+
if (docFiles.length === 0) {
|
|
1022
|
+
console.warn(
|
|
1023
|
+
pc2.yellow(
|
|
1024
|
+
`
|
|
1025
|
+
\u26A0 No doc files found (extensions: ${options.extensions.join(", ")}). Try --extensions to include other file types.`
|
|
1026
|
+
)
|
|
1027
|
+
);
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
const sections = buildDocTree(docFiles);
|
|
1031
|
+
const indexContent = generateIndex(sections, {
|
|
1032
|
+
name: options.displayName,
|
|
1033
|
+
docsPath: docsLinkPath,
|
|
1034
|
+
version: options.detectedVersion || (/^v\d/.test(options.tag) ? options.tag.replace(/^v/, "") : void 0),
|
|
1035
|
+
outputFile: options.output,
|
|
1036
|
+
libKey: options.libKey,
|
|
1037
|
+
repo: options.repo,
|
|
1038
|
+
repoDocsPath: options.docsPath
|
|
1039
|
+
});
|
|
1040
|
+
const newContent = injectIndex(existingContent, indexContent, options.name);
|
|
1041
|
+
fs5.writeFileSync(targetPath, newContent, "utf-8");
|
|
1042
|
+
const sizeAfter = Buffer.byteLength(newContent, "utf-8");
|
|
1043
|
+
const gitignoreResult = ensureGitignoreEntry(cwd);
|
|
1044
|
+
const action = isNewFile ? "Created" : "Updated";
|
|
1045
|
+
const sizeInfo = isNewFile ? formatSize(sizeAfter) : `${formatSize(sizeBefore)} \u2192 ${formatSize(sizeAfter)}`;
|
|
1046
|
+
console.log(
|
|
1047
|
+
`${pc2.green("\u2713")} ${action} ${pc2.bold(options.output)} (${sizeInfo})`
|
|
1048
|
+
);
|
|
1049
|
+
console.log(
|
|
1050
|
+
`${pc2.green("\u2713")} ${docFiles.length} doc files indexed from ${pc2.gray(docsLinkPath)}`
|
|
1051
|
+
);
|
|
1052
|
+
if (gitignoreResult.updated) {
|
|
1053
|
+
console.log(
|
|
1054
|
+
`${pc2.green("\u2713")} Added ${pc2.bold(DOCS_BASE_DIR)} to .gitignore`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
console.log("");
|
|
1058
|
+
}
|
|
1059
|
+
var program = new Command();
|
|
1060
|
+
var require2 = createRequire(import.meta.url);
|
|
1061
|
+
var pkg = require2("../package.json");
|
|
1062
|
+
program.name("docs-agents-md").description(
|
|
1063
|
+
"Download documentation from any GitHub repo and generate a compact index for AI coding agents."
|
|
1064
|
+
).version(pkg.version);
|
|
1065
|
+
program.command("add", { isDefault: true }).description("Add documentation index for a library").option("--repo <owner/repo>", "GitHub repository (e.g., vercel/next.js)").option(
|
|
1066
|
+
"--tag <tag>",
|
|
1067
|
+
"Git tag or branch (default: main for --repo, preset-specific for --lib)"
|
|
1068
|
+
).option("--docs-path <path>", "Path to docs folder in repo (default: docs)").option(
|
|
1069
|
+
"--name <name>",
|
|
1070
|
+
"Library name for markers/directory (required with --repo)"
|
|
1071
|
+
).option(
|
|
1072
|
+
"--lib <name>",
|
|
1073
|
+
"Use a built-in library preset (e.g., nextjs, react, angular, tailwindcss)"
|
|
1074
|
+
).option("--output <file>", "Target agent file (default: AGENTS.md)").option(
|
|
1075
|
+
"--extensions <exts>",
|
|
1076
|
+
"Comma-separated file extensions (default: md,mdx)"
|
|
1077
|
+
).action(async (options) => {
|
|
1078
|
+
try {
|
|
1079
|
+
await runAdd(options);
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
console.error(
|
|
1082
|
+
pc2.red(
|
|
1083
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1084
|
+
)
|
|
1085
|
+
);
|
|
1086
|
+
process.exit(1);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
program.command("list").description("List all available library presets").action(() => {
|
|
1090
|
+
console.log(pc2.cyan("\n\u{1F4DA} Available library presets:\n"));
|
|
1091
|
+
const keys = listRegistryKeys();
|
|
1092
|
+
const maxKeyLen = Math.max(...keys.map((k) => k.length));
|
|
1093
|
+
for (const key of keys) {
|
|
1094
|
+
const entry = REGISTRY[key];
|
|
1095
|
+
const paddedKey = key.padEnd(maxKeyLen);
|
|
1096
|
+
console.log(
|
|
1097
|
+
` ${pc2.bold(paddedKey)} ${pc2.gray(entry.repo)} \u2192 ${pc2.gray(entry.docsPath)} (${entry.defaultTag})`
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
console.log(`
|
|
1101
|
+
Usage: ${pc2.cyan("npx docs-agents-md --lib <name>")}
|
|
1102
|
+
`);
|
|
1103
|
+
});
|
|
1104
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "docs-agents-md",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Download documentation from any GitHub repo and generate a compact index for AI coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"docs-agents-md": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"ai-agents",
|
|
22
|
+
"coding-agents",
|
|
23
|
+
"documentation",
|
|
24
|
+
"docs",
|
|
25
|
+
"claude",
|
|
26
|
+
"cursor",
|
|
27
|
+
"copilot",
|
|
28
|
+
"windsurf",
|
|
29
|
+
"agents-md",
|
|
30
|
+
"claude-md",
|
|
31
|
+
"context",
|
|
32
|
+
"github",
|
|
33
|
+
"npx",
|
|
34
|
+
"cli",
|
|
35
|
+
"markdown",
|
|
36
|
+
"index",
|
|
37
|
+
"retrieval"
|
|
38
|
+
],
|
|
39
|
+
"author": "Abdelrahman Maged",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/a-maged/docs-agents-md.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/a-maged/docs-agents-md#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/a-maged/docs-agents-md/issues"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"README.md"
|
|
52
|
+
],
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"commander": "^12.1.0",
|
|
55
|
+
"picocolors": "^1.1.1",
|
|
56
|
+
"prompts": "^2.4.2"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^20.17.0",
|
|
60
|
+
"@types/prompts": "^2.4.9",
|
|
61
|
+
"tsup": "^8.3.0",
|
|
62
|
+
"typescript": "^5.6.0",
|
|
63
|
+
"vitest": "^2.1.0"
|
|
64
|
+
}
|
|
65
|
+
}
|