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 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 path = target.replace("git@github.com:", "");
26
- const [owner, repoRaw] = path.split("/", 2);
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
- let owner, repo;
176
- try {
177
- ({ owner, repo } = resolveRepoTarget(repository));
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
- catch (e) {
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
- const languages = await fetchLanguages(owner, repo);
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
- let repoData;
204
- try {
205
- repoData = await fetchRepo(owner, repo);
206
- }
207
- catch (e) {
208
- const message = e instanceof Error ? e.message : String(e);
209
- console.error("Failed to fetch repository data.");
210
- console.error(`error: ${message}`);
211
- console.error("\nfix:");
212
- console.error("- Ensure the repository exists and is public");
213
- console.error("- Or set GITHUB_TOKEN to avoid rate limits");
214
- process.exit(1);
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
- try {
218
- readme = await fetchReadme(owner, repo);
219
- }
220
- catch (e) {
221
- const message = e instanceof Error ? e.message : String(e);
222
- console.warn(`Warning: Could not fetch README: ${message}`);
223
- readme = null;
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
- const prompt = buildQuickPrompt(repoData.full_name, repoData.description, readme);
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 = await safeReadRepoFiles(owner, repo);
235
- const prompt = buildSimplePrompt(repoData.full_name, repoData.description, readme, readResult?.treeText ?? null);
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 = await safeReadRepoFiles(owner, repo);
243
- const prompt = buildPrompt(repoData.full_name, repoData.description, readme, options.detailed || false, readResult?.treeText ?? null, readResult?.filesText ?? null);
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,2 @@
1
+ import { type RepoReadResult } from "./repo_reader.js";
2
+ export declare function readLocalRepoSignalFiles(dirPath: string): RepoReadResult;
@@ -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
+ }
@@ -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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explainthisrepo",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "A CLI developer tool to explain any GitHub repository in plain English",
5
5
  "license": "MIT",
6
6
  "type": "module",