explainthisrepo 0.4.4 → 0.5.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/dist/cli.js +84 -50
- package/dist/local_reader.d.ts +2 -0
- package/dist/local_reader.js +77 -0
- package/dist/repo_reader.d.ts +1 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
import fs from "node:fs";
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,6 +11,7 @@ import { buildPrompt, buildQuickPrompt, buildSimplePrompt } from "./prompt.js";
|
|
|
10
11
|
import { generateExplanation } from "./generate.js";
|
|
11
12
|
import { writeOutput } from "./writer.js";
|
|
12
13
|
import { readRepoSignalFiles } from "./repo_reader.js";
|
|
14
|
+
import { readLocalRepoSignalFiles } from "./local_reader.js";
|
|
13
15
|
import { fetchLanguages } from "./github.js";
|
|
14
16
|
import { detectStack } from "./stack-detector.js";
|
|
15
17
|
import { printStack } from "./stack_printer.js";
|
|
@@ -22,8 +24,8 @@ function resolveRepoTarget(target) {
|
|
|
22
24
|
target = target.replace("http//", "http://");
|
|
23
25
|
}
|
|
24
26
|
if (target.startsWith("git@github.com:")) {
|
|
25
|
-
const
|
|
26
|
-
const [owner, repoRaw] =
|
|
27
|
+
const p = target.replace("git@github.com:", "");
|
|
28
|
+
const [owner, repoRaw] = p.split("/", 2);
|
|
27
29
|
if (!owner || !repoRaw)
|
|
28
30
|
throw new Error("Invalid GitHub SSH URL");
|
|
29
31
|
return { owner, repo: repoRaw.replace(/\.git$/, "") };
|
|
@@ -83,10 +85,7 @@ async function checkUrl(url, timeoutMs = 6000) {
|
|
|
83
85
|
clearTimeout(t);
|
|
84
86
|
const message = e instanceof Error ? e.message : String(e);
|
|
85
87
|
const name = e instanceof Error ? e.name : "Error";
|
|
86
|
-
return {
|
|
87
|
-
ok: false,
|
|
88
|
-
msg: `failed (${name}: ${message})`,
|
|
89
|
-
};
|
|
88
|
+
return { ok: false, msg: `failed (${name}: ${message})` };
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
async function runDoctor() {
|
|
@@ -135,7 +134,7 @@ async function main() {
|
|
|
135
134
|
.name("explainthisrepo")
|
|
136
135
|
.description("Explain GitHub repositories in plain English")
|
|
137
136
|
.version(getPkgVersion(), "-v, --version", "Show version")
|
|
138
|
-
.argument("[repository]", "GitHub repository (owner/repo or URL)")
|
|
137
|
+
.argument("[repository]", "GitHub repository (owner/repo or URL) or local path")
|
|
139
138
|
.option("--doctor", "Run diagnostics")
|
|
140
139
|
.option("--quick", "Quick summary mode")
|
|
141
140
|
.option("--simple", "Simple summary mode")
|
|
@@ -151,6 +150,9 @@ Examples:
|
|
|
151
150
|
$ explainthisrepo owner/repo --quick
|
|
152
151
|
$ explainthisrepo owner/repo --simple
|
|
153
152
|
$ explainthisrepo owner/repo --stack
|
|
153
|
+
$ explainthisrepo .
|
|
154
|
+
$ explainthisrepo ./path/to/directory
|
|
155
|
+
$ explainthisrepo . --stack
|
|
154
156
|
$ explainthisrepo --doctor`);
|
|
155
157
|
program.parse(process.argv);
|
|
156
158
|
const options = program.opts();
|
|
@@ -172,58 +174,86 @@ Examples:
|
|
|
172
174
|
if (!repository) {
|
|
173
175
|
program.error("repository argument required");
|
|
174
176
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
const local = fs.existsSync(repository);
|
|
178
|
+
let owner = "";
|
|
179
|
+
let repo = "";
|
|
180
|
+
let localPath = "";
|
|
181
|
+
if (local) {
|
|
182
|
+
localPath = path.resolve(repository);
|
|
183
|
+
console.log(`Analyzing local directory: ${repository}`);
|
|
178
184
|
}
|
|
179
|
-
|
|
180
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
181
|
-
console.error(`error: ${message}`);
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
console.log(`Fetching ${owner}/${repo}...`);
|
|
185
|
-
if (options.stack) {
|
|
185
|
+
else {
|
|
186
186
|
try {
|
|
187
|
-
|
|
188
|
-
const read = await readRepoSignalFiles(owner, repo);
|
|
189
|
-
const report = detectStack({
|
|
190
|
-
languages,
|
|
191
|
-
tree: read.tree,
|
|
192
|
-
keyFiles: read.keyFiles,
|
|
193
|
-
});
|
|
194
|
-
printStack(report, owner, repo);
|
|
195
|
-
return;
|
|
187
|
+
({ owner, repo } = resolveRepoTarget(repository));
|
|
196
188
|
}
|
|
197
189
|
catch (e) {
|
|
198
190
|
const message = e instanceof Error ? e.message : String(e);
|
|
199
191
|
console.error(`error: ${message}`);
|
|
200
192
|
process.exit(1);
|
|
201
193
|
}
|
|
194
|
+
console.log(`Fetching ${owner}/${repo}...`);
|
|
202
195
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
196
|
+
if (options.stack) {
|
|
197
|
+
let read;
|
|
198
|
+
let languages = {};
|
|
199
|
+
if (local) {
|
|
200
|
+
read = readLocalRepoSignalFiles(localPath);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
try {
|
|
204
|
+
languages = await fetchLanguages(owner, repo);
|
|
205
|
+
read = await readRepoSignalFiles(owner, repo);
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
209
|
+
console.error(`error: ${message}`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const report = detectStack({
|
|
214
|
+
languages,
|
|
215
|
+
tree: read.tree,
|
|
216
|
+
keyFiles: read.keyFiles,
|
|
217
|
+
});
|
|
218
|
+
const label = local ? repository : owner;
|
|
219
|
+
const sublabel = local ? "" : repo;
|
|
220
|
+
printStack(report, label, sublabel);
|
|
221
|
+
return;
|
|
215
222
|
}
|
|
223
|
+
let repoData = null;
|
|
216
224
|
let readme = null;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
if (!local) {
|
|
226
|
+
try {
|
|
227
|
+
repoData = await fetchRepo(owner, repo);
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
231
|
+
console.error("Failed to fetch repository data.");
|
|
232
|
+
console.error(`error: ${message}`);
|
|
233
|
+
console.error("\nfix:");
|
|
234
|
+
console.error("- Ensure the repository exists and is public");
|
|
235
|
+
console.error("- Or set GITHUB_TOKEN to avoid rate limits");
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
readme = await fetchReadme(owner, repo);
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
243
|
+
console.warn(`Warning: Could not fetch README: ${message}`);
|
|
244
|
+
readme = null;
|
|
245
|
+
}
|
|
224
246
|
}
|
|
225
247
|
if (options.quick) {
|
|
226
|
-
|
|
248
|
+
let quickReadme = readme;
|
|
249
|
+
const repoName = local ? localPath : (repoData?.full_name ?? "");
|
|
250
|
+
const description = local ? null : (repoData?.description ?? null);
|
|
251
|
+
if (local) {
|
|
252
|
+
const read = readLocalRepoSignalFiles(localPath);
|
|
253
|
+
const readmeKey = Object.keys(read.keyFiles).find((k) => k.toLowerCase().startsWith("readme"));
|
|
254
|
+
quickReadme = readmeKey !== undefined ? read.keyFiles[readmeKey] : null;
|
|
255
|
+
}
|
|
256
|
+
const prompt = buildQuickPrompt(repoName, description, quickReadme);
|
|
227
257
|
console.log("Generating explanation...");
|
|
228
258
|
const output = await generateWithExit(prompt);
|
|
229
259
|
console.log("Quick summary 🎉");
|
|
@@ -231,16 +261,20 @@ Examples:
|
|
|
231
261
|
return;
|
|
232
262
|
}
|
|
233
263
|
if (options.simple) {
|
|
234
|
-
const readResult =
|
|
235
|
-
|
|
264
|
+
const readResult = local
|
|
265
|
+
? readLocalRepoSignalFiles(localPath)
|
|
266
|
+
: await safeReadRepoFiles(owner, repo);
|
|
267
|
+
const prompt = buildSimplePrompt(local ? localPath : (repoData?.full_name ?? ""), local ? null : (repoData?.description ?? null), local ? null : readme, readResult?.treeText ?? null);
|
|
236
268
|
console.log("Generating explanation...");
|
|
237
269
|
const output = await generateWithExit(prompt);
|
|
238
270
|
console.log("Simple summary 🎉");
|
|
239
271
|
console.log(output.trim());
|
|
240
272
|
return;
|
|
241
273
|
}
|
|
242
|
-
const readResult =
|
|
243
|
-
|
|
274
|
+
const readResult = local
|
|
275
|
+
? readLocalRepoSignalFiles(localPath)
|
|
276
|
+
: await safeReadRepoFiles(owner, repo);
|
|
277
|
+
const prompt = buildPrompt(local ? localPath : (repoData?.full_name ?? ""), local ? null : (repoData?.description ?? null), local ? null : readme, options.detailed || false, readResult?.treeText ?? null, readResult?.filesText ?? null);
|
|
244
278
|
console.log("Generating explanation...");
|
|
245
279
|
const output = await generateWithExit(prompt);
|
|
246
280
|
console.log("Writing EXPLAIN.md...");
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const KEY_FILENAMES = new Set([
|
|
4
|
+
"readme.md", "readme.txt", "readme.rst", "readme",
|
|
5
|
+
"package.json", "pyproject.toml", "setup.py", "setup.cfg",
|
|
6
|
+
"requirements.txt", "cargo.toml", "go.mod", "pom.xml",
|
|
7
|
+
"build.gradle", "composer.json", "gemfile", "makefile",
|
|
8
|
+
"dockerfile", "docker-compose.yml", "docker-compose.yaml",
|
|
9
|
+
".env.example", "tsconfig.json", "angular.json", "next.config.js",
|
|
10
|
+
"vite.config.js", "vite.config.ts", "webpack.config.js",
|
|
11
|
+
]);
|
|
12
|
+
const SKIP_DIRS = new Set([
|
|
13
|
+
".git", ".hg", ".svn", "node_modules", "__pycache__",
|
|
14
|
+
".venv", "venv", "env", ".env", "dist", "build",
|
|
15
|
+
".idea", ".vscode", ".mypy_cache", ".pytest_cache",
|
|
16
|
+
"coverage", ".coverage", "htmlcov",
|
|
17
|
+
]);
|
|
18
|
+
const MAX_FILE_BYTES = 32_000;
|
|
19
|
+
const MAX_KEY_FILES = 12;
|
|
20
|
+
function walkDir(root, dir, tree, keyFiles) {
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const dirs = [];
|
|
29
|
+
const files = [];
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
32
|
+
dirs.push(entry);
|
|
33
|
+
}
|
|
34
|
+
else if (entry.isFile()) {
|
|
35
|
+
files.push(entry);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
dirs.sort((a, b) => a.name.localeCompare(b.name));
|
|
39
|
+
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const absPath = path.join(dir, file.name);
|
|
42
|
+
const relPath = path.relative(root, absPath).replace(/\\/g, "/");
|
|
43
|
+
tree.push({ path: relPath, type: "blob" });
|
|
44
|
+
if (KEY_FILENAMES.has(file.name.toLowerCase()) &&
|
|
45
|
+
Object.keys(keyFiles).length < MAX_KEY_FILES) {
|
|
46
|
+
try {
|
|
47
|
+
const fd = fs.openSync(absPath, "r");
|
|
48
|
+
const buf = Buffer.alloc(MAX_FILE_BYTES);
|
|
49
|
+
const bytesRead = fs.readSync(fd, buf, 0, MAX_FILE_BYTES, 0);
|
|
50
|
+
fs.closeSync(fd);
|
|
51
|
+
keyFiles[relPath] = buf.subarray(0, bytesRead).toString("utf8");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const subdir of dirs) {
|
|
58
|
+
walkDir(root, path.join(dir, subdir.name), tree, keyFiles);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function readLocalRepoSignalFiles(dirPath) {
|
|
62
|
+
const root = path.resolve(dirPath);
|
|
63
|
+
const tree = [];
|
|
64
|
+
const keyFiles = {};
|
|
65
|
+
walkDir(root, root, tree, keyFiles);
|
|
66
|
+
const treeText = tree.map((item) => item.path).join("\n");
|
|
67
|
+
const filesText = Object.entries(keyFiles)
|
|
68
|
+
.map(([relPath, content]) => `### ${relPath}\n${content}`)
|
|
69
|
+
.join("\n\n");
|
|
70
|
+
return {
|
|
71
|
+
tree,
|
|
72
|
+
treeText,
|
|
73
|
+
keyFiles,
|
|
74
|
+
filesText,
|
|
75
|
+
selectedFiles: Object.keys(keyFiles),
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/repo_reader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type TreeItem = {
|
|
1
|
+
export type TreeItem = {
|
|
2
2
|
path: string;
|
|
3
3
|
type: "blob" | "tree";
|
|
4
4
|
size?: number;
|
|
@@ -11,4 +11,3 @@ export type RepoReadResult = {
|
|
|
11
11
|
keyFiles: Record<string, string>;
|
|
12
12
|
};
|
|
13
13
|
export declare function readRepoSignalFiles(owner: string, repo: string): Promise<RepoReadResult>;
|
|
14
|
-
export {};
|