explainthisrepo 0.5.0 → 0.6.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # ExplainThisRepo
2
2
 
3
- ExplainThisRepo is a CLI that generates plain-English explanations of public GitHub repositories by analyzing repository structure, README content, and selected high signal files.
3
+ ExplainThisRepo is a CLI that generates plain-English explanations of any codebase (GitHub repositories and local directories) by analyzing project structure, README content, and high signal files.
4
4
 
5
- It's helps developers understand unfamiliar repositories does by generating a structured `EXPLAIN.md` from real
5
+ It helps developers quickly understand unfamiliar codebases by deriving architectural explanations from real project structure and code signals, producing a clear, structured `EXPLAIN.md`.
6
6
 
7
7
  [![PyPI Version](https://img.shields.io/pypi/v/explainthisrepo?color=blue)](https://pypi.org/project/explainthisrepo/)
8
8
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/explainthisrepo?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/explainthisrepo)
@@ -19,15 +19,15 @@ It's helps developers understand unfamiliar repositories does by generating a st
19
19
 
20
20
  ## Key Features
21
21
 
22
- - Understand unfamiliar repositories instantly through structural and architechural summaries by turning structure and code signals into a readable architectural summary
23
- - Fetches public GitHub repositories automatically
24
- - Analyzes real repository data including file tree, configs, entrypoints, and high signal source files
22
+ - Generates architectural summaries from repository structure and code signals
23
+ - Fetches public repositories by GitHub URLs (with or without https), `owner/repo` format, issue links, query strings, and SSH clone links
24
+ - Analyzes repository data including file tree, configs, entrypoints, and high signal source files
25
25
  - Extracts repo signals from key files (package.json, pyproject.toml, config files, entrypoints)
26
26
  - Builds a file tree summary to understand project architecture
27
- - Detects programming languages via the GitHub API
28
- - Accepts repositories via owner/repo, GitHub URLs (with or without https), issue links, query strings, and SSH clone links
27
+ - Detects programming languages with the GitHub API
28
+ - Analyzes local project directories using the same pipeline as GitHub repositories
29
29
  - Generates a structured plain English explanation grounded in actual project files
30
- - Outputs an EXPLAIN.md file in your current directory (default mode)
30
+ - Outputs the explanation to an `EXPLAIN.md` file in your current directory or print it directly in the terminal
31
31
  - Multi mode command-line interface
32
32
 
33
33
  ---
@@ -38,13 +38,13 @@ It's helps developers understand unfamiliar repositories does by generating a st
38
38
 
39
39
  - `--quick` → One-sentence summary
40
40
 
41
- - `--simple` → Short, easy explanation
41
+ - `--simple` → Short, simplified explanation
42
42
 
43
43
  - `--detailed` → Deeper explanation including structure and architecture
44
44
 
45
45
  - `--stack` → Tech stack breakdown from repo signals
46
46
 
47
- - `--version` → Show CLI version
47
+ - `--version` → Check installed CLI version
48
48
 
49
49
  - `--help` → Show usage guide
50
50
 
@@ -56,9 +56,9 @@ It's helps developers understand unfamiliar repositories does by generating a st
56
56
 
57
57
  ExplainThisRepo uses Gemini models for code analysis.
58
58
 
59
- Set your API key as an environment variable.
59
+ Set your Google Gemini API key as an environment variable.
60
60
 
61
- macOS / Linux
61
+ Linux / macOS
62
62
 
63
63
  ```bash
64
64
  export GEMINI_API_KEY="your_api_key_here"
@@ -74,7 +74,7 @@ Restart your terminal after setting the key.
74
74
 
75
75
  ## Installation
76
76
 
77
- ### Option 1: install via pip (recommended):
77
+ ### Option 1: install with pip (recommended):
78
78
 
79
79
  Requirements: Python 3.9+
80
80
 
@@ -99,29 +99,32 @@ explainthisrepo owner/repo
99
99
  # or: npx explainthisrepo owner/repo
100
100
  ```
101
101
 
102
+ Replace `owner/repo` with the GitHub repository identifier (e.g., `facebook/react`).
103
+
102
104
  ---
103
105
 
104
- ## Flexible Repository Input
106
+ ## Flexible Repository and Local Directory Input
105
107
 
106
- You don’t need to reformat links anymore.
108
+ Accepts various formats for repository input, full GitHub URLs, issue links, and SSH clone links.
107
109
 
108
- ExplainThisRepo accepts GitHub repositories the way you actually copy them.
109
110
  ```bash
110
111
  explainthisrepo https://github.com/owner/repo
111
112
  explainthisrepo github.com/owner/repo
112
113
  explainthisrepo https://github.com/owner/repo/issues/123
113
114
  explainthisrepo https://github.com/owner/repo?tab=readme
114
115
  explainthisrepo git@github.com:owner/repo.git
116
+ explainthisrepo .
117
+ explainthisrepo ./path/to/directory
115
118
  ```
116
119
 
117
- All inputs are normalized internally to owner/repo.
120
+ All inputs are normalized internally to `owner/repo`.
118
121
 
119
122
  ---
120
123
 
121
124
  ## Usage
122
125
 
123
126
  ### Basic
124
- Generate a full explanation and saves it to `EXPLAIN.md`:
127
+ Writes a full explanation to `EXPLAIN.md`:
125
128
 
126
129
  ```bash
127
130
  explainthisrepo owner/repo
@@ -134,7 +137,8 @@ explainthisrepo facebook/react
134
137
 
135
138
  ### Quick mode
136
139
 
137
- Get a one-sentence definition (prints only, no file created):
140
+ Prints a one-sentence summary to stdout:
141
+
138
142
  ```bash
139
143
  explainthisrepo owner/repo --quick
140
144
  ```
@@ -148,7 +152,8 @@ explainthisrepo facebook/react --quick
148
152
 
149
153
  ### Detailed mode
150
154
 
151
- Generate a more detailed explanation (includes architecture / folder structure):
155
+ Writes a more detailed explanation of repository structure and architecture:
156
+
152
157
  ```bash
153
158
  explainthisrepo owner/repo --detailed
154
159
  ```
@@ -159,7 +164,8 @@ explainthisrepo owner/repo --detailed
159
164
 
160
165
  ### Simple mode
161
166
 
162
- Prints only the simple output (no EXPLAIN.md)
167
+ Prints a short, simplified explanation to stdout. No files are written.
168
+
163
169
  ```bash
164
170
  explainthisrepo owner/repo --simple
165
171
  ```
@@ -170,15 +176,43 @@ explainthisrepo owner/repo --simple
170
176
 
171
177
  ### Stack detector
172
178
 
173
- Get a tech stack breakdown detected from repo signals. No AI explanation. Prints only.
179
+ Tech stack breakdown detected from repo signals. No LLM calls are made.
180
+
174
181
  ```bash
175
182
  explainthisrepo owner/repo --stack
176
183
  ```
177
184
  ![Stack detector Output](assets/stack-command-output.png)
178
185
 
186
+ ### Local Directory Analysis
187
+
188
+ ExplainThisRepo can analyze local directories directly in the terminal, using the same modes and output formats as GitHub repositories
189
+
190
+ ```bash
191
+ explainthisrepo .
192
+ explainthisrepo ./path/to/directory
193
+ ```
194
+
195
+ This works with all existing modes:
196
+
197
+ ```bash
198
+ explainthisrepo . --quick
199
+ explainthisrepo . --simple
200
+ explainthisrepo . --detailed
201
+ explainthisrepo . --stack
202
+ ```
203
+
204
+ When analyzing a local directory:
205
+ - Repository structure is derived from the filesystem
206
+ - Key files (README, configs, entrypoints) are extracted locally
207
+ - No GitHub APIs calls are made
208
+ - All prompts and outputs remain identical
209
+
210
+ This allows analysis of projects directly from the local filesystem, without requiring a GitHub repository.
211
+
179
212
  ### Version
180
213
 
181
- Print the installed version:
214
+ Print the installed CLI version:
215
+
182
216
  ```bash
183
217
  explainthisrepo --version
184
218
  ```
@@ -187,12 +221,13 @@ explainthisrepo --version
187
221
 
188
222
  ### Doctor
189
223
 
190
- Check environment + connectivity (useful for debugging):
224
+ Check environment and connectivity (useful for debugging):
225
+
191
226
  ```bash
192
227
  explainthisrepo --doctor
193
228
  ```
194
229
 
195
- ## Termux (Android) install notes
230
+ ### Termux (Android) install notes
196
231
 
197
232
  Termux has some environment limitations that can make `pip install explainthisrepo` fail to create the `explainthisrepo` command in `$PREFIX/bin`.
198
233
 
@@ -216,7 +251,7 @@ If you do not want to modify PATH, you can run ExplainThisRepo as a module:
216
251
  python -m explain_this_repo owner/repo
217
252
  ```
218
253
 
219
- Gemini support on Termux (Optional)
254
+ ### Gemini support on Termux (Optional)
220
255
 
221
256
  Installing Gemini support may require building Rust-based dependencies on Android, which can take time on first install:
222
257
 
@@ -247,4 +282,4 @@ Caleb Wodi
247
282
 
248
283
  - Email: caleb@explainthisrepo.com
249
284
  - Twitter: [@calchiwo](https://x.com/calchiwo)
250
- - LinkedIn: [@calchiwo](https://linkedin.com/in/calchiwo)
285
+ - LinkedIn: [@calchiwo](https://linkedin.com/in/calchiwo)
package/dist/cli.js CHANGED
@@ -6,6 +6,8 @@ import { readFileSync } from "node:fs";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { Command } from "commander";
9
+ import ora from "ora";
10
+ import { runInit } from "./init.js";
9
11
  import { fetchRepo, fetchReadme } from "./github.js";
10
12
  import { buildPrompt, buildQuickPrompt, buildSimplePrompt } from "./prompt.js";
11
13
  import { generateExplanation } from "./generate.js";
@@ -132,9 +134,9 @@ async function main() {
132
134
  const program = new Command();
133
135
  program
134
136
  .name("explainthisrepo")
135
- .description("Explain GitHub repositories in plain English")
137
+ .description("CLI that generates plain English explanations of any codebase")
136
138
  .version(getPkgVersion(), "-v, --version", "Show version")
137
- .argument("[repository]", "GitHub repository (owner/repo or URL) or local path")
139
+ .argument("[repository]", "GitHub repository (owner/repo or URL) or local directories")
138
140
  .option("--doctor", "Run diagnostics")
139
141
  .option("--quick", "Quick summary mode")
140
142
  .option("--simple", "Simple summary mode")
@@ -154,7 +156,16 @@ Examples:
154
156
  $ explainthisrepo ./path/to/directory
155
157
  $ explainthisrepo . --stack
156
158
  $ explainthisrepo --doctor`);
159
+ program
160
+ .command("init")
161
+ .description("Initialize configuration with Gemini API key")
162
+ .action(async () => {
163
+ await runInit();
164
+ });
157
165
  program.parse(process.argv);
166
+ if (process.argv[2] === "init") {
167
+ return;
168
+ }
158
169
  const options = program.opts();
159
170
  const repository = program.args[0];
160
171
  if (options.doctor) {
@@ -172,7 +183,7 @@ Examples:
172
183
  process.exit(1);
173
184
  }
174
185
  if (!repository) {
175
- program.error("repository argument required");
186
+ program.error("repository argument required (or use `init` to set up API key)");
176
187
  }
177
188
  const local = fs.existsSync(repository);
178
189
  let owner = "";
@@ -191,20 +202,32 @@ Examples:
191
202
  console.error(`error: ${message}`);
192
203
  process.exit(1);
193
204
  }
194
- console.log(`Fetching ${owner}/${repo}...`);
195
205
  }
196
206
  if (options.stack) {
197
207
  let read;
198
208
  let languages = {};
199
209
  if (local) {
200
- read = readLocalRepoSignalFiles(localPath);
210
+ const spinner = ora("Reading repository files…").start();
211
+ try {
212
+ read = readLocalRepoSignalFiles(localPath);
213
+ spinner.stop();
214
+ }
215
+ catch (e) {
216
+ spinner.stop();
217
+ const message = e instanceof Error ? e.message : String(e);
218
+ console.error(`error: ${message}`);
219
+ process.exit(1);
220
+ }
201
221
  }
202
222
  else {
223
+ const spinner = ora(`Fetching ${owner}/${repo}…`).start();
203
224
  try {
204
225
  languages = await fetchLanguages(owner, repo);
205
226
  read = await readRepoSignalFiles(owner, repo);
227
+ spinner.stop();
206
228
  }
207
229
  catch (e) {
230
+ spinner.stop();
208
231
  const message = e instanceof Error ? e.message : String(e);
209
232
  console.error(`error: ${message}`);
210
233
  process.exit(1);
@@ -223,10 +246,14 @@ Examples:
223
246
  let repoData = null;
224
247
  let readme = null;
225
248
  if (!local) {
249
+ const spinner = ora(`Fetching ${owner}/${repo}…`).start();
226
250
  try {
227
251
  repoData = await fetchRepo(owner, repo);
252
+ readme = await fetchReadme(owner, repo);
253
+ spinner.stop();
228
254
  }
229
255
  catch (e) {
256
+ spinner.stop();
230
257
  const message = e instanceof Error ? e.message : String(e);
231
258
  console.error("Failed to fetch repository data.");
232
259
  console.error(`error: ${message}`);
@@ -235,48 +262,66 @@ Examples:
235
262
  console.error("- Or set GITHUB_TOKEN to avoid rate limits");
236
263
  process.exit(1);
237
264
  }
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
- }
246
265
  }
247
266
  if (options.quick) {
248
267
  let quickReadme = readme;
249
268
  const repoName = local ? localPath : (repoData?.full_name ?? "");
250
269
  const description = local ? null : (repoData?.description ?? null);
251
270
  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;
271
+ const spinner = ora("Reading repository files…").start();
272
+ try {
273
+ const read = readLocalRepoSignalFiles(localPath);
274
+ spinner.stop();
275
+ const readmeKey = Object.keys(read.keyFiles).find((k) => k.toLowerCase().startsWith("readme"));
276
+ quickReadme = readmeKey !== undefined ? read.keyFiles[readmeKey] : null;
277
+ }
278
+ catch (e) {
279
+ spinner.stop();
280
+ throw e;
281
+ }
255
282
  }
256
283
  const prompt = buildQuickPrompt(repoName, description, quickReadme);
257
- console.log("Generating explanation...");
258
- const output = await generateWithExit(prompt);
284
+ const spinner = ora("Generating explanation").start();
285
+ const output = await generateWithExit(prompt).finally(() => spinner.stop());
259
286
  console.log("Quick summary 🎉");
260
287
  console.log(output.trim());
261
288
  return;
262
289
  }
263
290
  if (options.simple) {
264
- const readResult = local
265
- ? readLocalRepoSignalFiles(localPath)
266
- : await safeReadRepoFiles(owner, repo);
291
+ let readResult;
292
+ const spinner = ora("Reading repository files…").start();
293
+ try {
294
+ readResult = local
295
+ ? readLocalRepoSignalFiles(localPath)
296
+ : await safeReadRepoFiles(owner, repo);
297
+ spinner.stop();
298
+ }
299
+ catch (e) {
300
+ spinner.stop();
301
+ throw e;
302
+ }
267
303
  const prompt = buildSimplePrompt(local ? localPath : (repoData?.full_name ?? ""), local ? null : (repoData?.description ?? null), local ? null : readme, readResult?.treeText ?? null);
268
- console.log("Generating explanation...");
269
- const output = await generateWithExit(prompt);
304
+ const genSpinner = ora("Generating explanation").start();
305
+ const output = await generateWithExit(prompt).finally(() => genSpinner.stop());
270
306
  console.log("Simple summary 🎉");
271
307
  console.log(output.trim());
272
308
  return;
273
309
  }
274
- const readResult = local
275
- ? readLocalRepoSignalFiles(localPath)
276
- : await safeReadRepoFiles(owner, repo);
310
+ let readResult;
311
+ const readSpinner = ora("Reading repository files…").start();
312
+ try {
313
+ readResult = local
314
+ ? readLocalRepoSignalFiles(localPath)
315
+ : await safeReadRepoFiles(owner, repo);
316
+ readSpinner.stop();
317
+ }
318
+ catch (e) {
319
+ readSpinner.stop();
320
+ throw e;
321
+ }
277
322
  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);
278
- console.log("Generating explanation...");
279
- const output = await generateWithExit(prompt);
323
+ const genSpinner = ora("Generating explanation").start();
324
+ const output = await generateWithExit(prompt).finally(() => genSpinner.stop());
280
325
  console.log("Writing EXPLAIN.md...");
281
326
  writeOutput(output);
282
327
  const wordCount = output.split(/\s+/).filter(Boolean).length;
@@ -0,0 +1,4 @@
1
+ export declare function getConfigPath(): string;
2
+ export declare function ensureConfigDir(): string;
3
+ export declare function writeConfig(contents: string): void;
4
+ export declare function readConfig(): string | null;
package/dist/config.js ADDED
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const CONFIG_DIR_NAME = "ExplainThisRepo";
5
+ const CONFIG_FILE_NAME = "config.toml";
6
+ export function getConfigPath() {
7
+ if (process.platform === "win32") {
8
+ const appdata = process.env.APPDATA;
9
+ if (!appdata) {
10
+ throw new Error("APPDATA environment variable is not set");
11
+ }
12
+ return path.join(appdata, CONFIG_DIR_NAME, CONFIG_FILE_NAME);
13
+ }
14
+ const xdg = process.env.XDG_CONFIG_HOME;
15
+ const base = xdg ?? path.join(os.homedir(), ".config");
16
+ return path.join(base, "explainthisrepo", CONFIG_FILE_NAME);
17
+ }
18
+ export function ensureConfigDir() {
19
+ const configPath = getConfigPath();
20
+ const dir = path.dirname(configPath);
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ return configPath;
23
+ }
24
+ export function writeConfig(contents) {
25
+ const path = ensureConfigDir();
26
+ fs.writeFileSync(path, contents, { encoding: "utf-8" });
27
+ }
28
+ export function readConfig() {
29
+ const path = getConfigPath();
30
+ if (!fs.existsSync(path))
31
+ return null;
32
+ return fs.readFileSync(path, "utf-8");
33
+ }
package/dist/init.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function runInit(): Promise<void>;
package/dist/init.js ADDED
@@ -0,0 +1,41 @@
1
+ import readline from "node:readline";
2
+ import process from "node:process";
3
+ import { stdin as input, stderr as output } from "node:process";
4
+ import chalk from "chalk";
5
+ import { writeConfig } from "./config.js";
6
+ const CONFIG_TEMPLATE = `[llm]
7
+ provider = "gemini"
8
+ api_key = "{api_key}"
9
+ `;
10
+ export async function runInit() {
11
+ output.write(chalk.yellow("WARNING: input is hidden. Paste your GEMINI_API_KEY and press Enter.\n"));
12
+ try {
13
+ const apiKey = (await promptHidden("Gemini API key: ")).trim();
14
+ if (!apiKey) {
15
+ output.write(chalk.red("error: API key cannot be empty\n"));
16
+ process.exit(1);
17
+ }
18
+ writeConfig(CONFIG_TEMPLATE.replace("{api_key}", apiKey));
19
+ output.write(chalk.green("Configuration written.\n"));
20
+ process.exit(0);
21
+ }
22
+ catch {
23
+ output.write(chalk.red("\nInterrupted.\n"));
24
+ process.exit(130);
25
+ }
26
+ }
27
+ function promptHidden(prompt) {
28
+ return new Promise((resolve) => {
29
+ const rl = readline.createInterface({
30
+ input,
31
+ output,
32
+ terminal: true,
33
+ });
34
+ rl._writeToOutput = () => { };
35
+ rl.question(prompt, (answer) => {
36
+ rl.close();
37
+ output.write("\n");
38
+ resolve(answer);
39
+ });
40
+ });
41
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "explainthisrepo",
3
- "version": "0.5.0",
4
- "description": "A CLI developer tool to explain any GitHub repository in plain English",
3
+ "version": "0.6.0",
4
+ "description": "CLI that generates plain English explanations of any codebase",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "author": "Caleb Wodi <calebwodi33@gmail.com>",
@@ -47,7 +47,8 @@
47
47
  "@google/generative-ai": "^0.24.1",
48
48
  "axios": "^1.13.2",
49
49
  "commander": "^14.0.3",
50
- "dotenv": "^17.2.3"
50
+ "dotenv": "^17.2.3",
51
+ "ora": "^9.3.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@types/node": "^22.0.0",