@chappibunny/repolens 0.4.1

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/src/init.js ADDED
@@ -0,0 +1,540 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { info, warn } from "./utils/logger.js";
5
+
6
+ const DETECTABLE_ROOTS = [
7
+ "app",
8
+ "src/app",
9
+ "components",
10
+ "src/components",
11
+ "lib",
12
+ "src/lib",
13
+ "hooks",
14
+ "src/hooks",
15
+ "store",
16
+ "src/store",
17
+ "pages",
18
+ "src/pages"
19
+ ];
20
+
21
+ const DEFAULT_GITHUB_WORKFLOW = `name: RepoLens Documentation
22
+
23
+ on:
24
+ push:
25
+ branches:
26
+ - main
27
+ pull_request:
28
+
29
+ permissions:
30
+ contents: read
31
+
32
+ jobs:
33
+ publish:
34
+ runs-on: ubuntu-latest
35
+
36
+ steps:
37
+ - name: Checkout repository
38
+ uses: actions/checkout@v4
39
+
40
+ - name: Setup Node.js
41
+ uses: actions/setup-node@v4
42
+ with:
43
+ node-version: 20
44
+
45
+ - name: Generate and publish documentation
46
+ env:
47
+ NOTION_TOKEN: \${{ secrets.NOTION_TOKEN }}
48
+ NOTION_PARENT_PAGE_ID: \${{ secrets.NOTION_PARENT_PAGE_ID }}
49
+ run: npx repolens@latest publish
50
+ `;
51
+
52
+ const DEFAULT_ENV_EXAMPLE = `# Notion Publishing
53
+ NOTION_TOKEN=
54
+ NOTION_PARENT_PAGE_ID=
55
+ NOTION_VERSION=2022-06-28
56
+
57
+ # AI-Assisted Documentation (Optional)
58
+ # Enable AI features for natural language explanations
59
+ # REPOLENS_AI_ENABLED=true
60
+ # REPOLENS_AI_API_KEY=sk-...
61
+ # REPOLENS_AI_BASE_URL=https://api.openai.com/v1
62
+ # REPOLENS_AI_MODEL=gpt-4-turbo-preview
63
+ # REPOLENS_AI_TEMPERATURE=0.3
64
+ # REPOLENS_AI_MAX_TOKENS=2000
65
+ `;
66
+
67
+ const DEFAULT_REPOLENS_README = `# RepoLens Documentation
68
+
69
+ This repository is configured to use [RepoLens](https://github.com/CHAPIBUNNY/repolens) for automatic architecture documentation.
70
+
71
+ ## šŸ“‹ What RepoLens Created
72
+
73
+ - \`.repolens.yml\` — Configuration file
74
+ - \`.github/workflows/repolens.yml\` — GitHub Actions workflow
75
+ - \`.env.example\` — Environment variables template
76
+ - \`README.repolens.md\` — This guide
77
+
78
+ ## šŸš€ Quick Start
79
+
80
+ ### Local Testing
81
+
82
+ \`\`\`bash
83
+ # Test documentation generation locally
84
+ npx repolens publish
85
+ \`\`\`
86
+
87
+ ### Notion Publishing
88
+
89
+ If you configured Notion credentials during setup, documentation will publish automatically. Otherwise:
90
+
91
+ 1. Copy \`.env.example\` to \`.env\`
92
+ 2. Add your credentials:
93
+ - \`NOTION_TOKEN\` — Get from https://www.notion.so/my-integrations
94
+ - \`NOTION_PARENT_PAGE_ID\` — The page where docs will be published
95
+
96
+ ### GitHub Actions
97
+
98
+ For automated publishing on every push:
99
+
100
+ 1. Go to repository Settings → Secrets → Actions
101
+ 2. Add secrets:
102
+ - \`NOTION_TOKEN\`
103
+ - \`NOTION_PARENT_PAGE_ID\`
104
+ 3. Push to main branch
105
+
106
+ ## šŸ“Š Generated Documentation
107
+
108
+ RepoLens generates documentation in two modes:
109
+
110
+ ### Deterministic Mode (Default - Free)
111
+ - **System Overview** — Technical profile and stats
112
+ - **Module Catalog** — Complete code inventory
113
+ - **API Surface** — Internal endpoints + external integrations
114
+ - **Route Map** — Application routes
115
+ - **System Map** — Unicode architecture diagram
116
+ - **Architecture Diff** — Change tracking
117
+
118
+ ### AI-Enhanced Mode (Optional - Requires API Key)
119
+ Adds 5 natural language documents readable by non-technical audiences:
120
+ - **Executive Summary** — Project overview for leadership
121
+ - **Business Domains** — What the system does by function
122
+ - **Architecture Overview** — Layered technical analysis
123
+ - **Data Flows** — How information moves through system
124
+ - **Developer Onboarding** — Getting started guide
125
+
126
+ ## šŸ¤– Enabling AI Features
127
+
128
+ AI features add natural language explanations for non-technical stakeholders.
129
+
130
+ 1. Get an OpenAI API key from https://platform.openai.com/api-keys
131
+ 2. Add to your \`.env\` file:
132
+ \`\`\`bash
133
+ REPOLENS_AI_ENABLED=true
134
+ REPOLENS_AI_API_KEY=sk-...
135
+ \`\`\`
136
+ 3. (Optional) Configure in \`.repolens.yml\`:
137
+ \`\`\`yaml
138
+ ai:
139
+ enabled: true
140
+ mode: hybrid
141
+ temperature: 0.3
142
+
143
+ features:
144
+ executive_summary: true
145
+ business_domains: true
146
+ architecture_overview: true
147
+ data_flows: true
148
+ developer_onboarding: true
149
+ \`\`\`
150
+
151
+ **Cost estimate**: $0.10-$0.40 per run for typical projects
152
+
153
+ See [AI.md](https://github.com/CHAPIBUNNY/repolens/blob/main/AI.md) for full documentation
154
+ - **Module Catalog** — Detected code modules
155
+ - **API Surface** — REST API endpoints
156
+ - **Route Map** — Application routes
157
+ - **System Map** — Architecture diagram (Unicode ASCII art)
158
+
159
+ ## āš™ļø Configuration
160
+
161
+ Edit \`.repolens.yml\` to customize:
162
+
163
+ - Scan patterns and ignore rules
164
+ - Module detection roots
165
+ - Documentation page titles
166
+ - Publishing targets
167
+
168
+ ## šŸ“š Learn More
169
+
170
+ - [RepoLens GitHub](https://github.com/CHAPIBUNNY/repolens)
171
+ - [Configuration Docs](https://github.com/CHAPIBUNNY/repolens#configuration)
172
+ - [Troubleshooting](https://github.com/CHAPIBUNNY/repolens#troubleshooting)
173
+ `;
174
+
175
+ async function fileExists(filePath) {
176
+ try {
177
+ await fs.access(filePath);
178
+ return true;
179
+ } catch {
180
+ return false;
181
+ }
182
+ }
183
+
184
+ async function dirExists(dirPath) {
185
+ try {
186
+ const stat = await fs.stat(dirPath);
187
+ return stat.isDirectory();
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ async function detectRepoStructure(repoRoot) {
194
+ const detectedRoots = [];
195
+
196
+ for (const relativePath of DETECTABLE_ROOTS) {
197
+ const absolutePath = path.join(repoRoot, relativePath);
198
+ if (await dirExists(absolutePath)) {
199
+ detectedRoots.push(relativePath);
200
+ }
201
+ }
202
+
203
+ return detectedRoots;
204
+ }
205
+
206
+ function buildIncludePatterns(detectedRoots) {
207
+ return detectedRoots.map((root) => `${root}/**/*.{ts,tsx,js,jsx,md}`);
208
+ }
209
+
210
+ function buildIgnorePatterns() {
211
+ return [
212
+ "tools/repolens/**",
213
+ "node_modules/**",
214
+ ".next/**",
215
+ "dist/**",
216
+ "build/**",
217
+ "out/**",
218
+ ".git/**",
219
+ "coverage/**",
220
+ "public/**",
221
+ "**/*.test.*",
222
+ "**/*.spec.*",
223
+ "**/__tests__/**",
224
+ "**/__mocks__/**",
225
+ "**/generated/**",
226
+ "**/dist/**",
227
+ "**/esm/**",
228
+ "**/imp/**",
229
+ "**/types/**",
230
+ "**/load-testing/**"
231
+ ];
232
+ }
233
+
234
+ function buildRepoLensConfig(projectName, detectedRoots) {
235
+ const includePatterns = buildIncludePatterns(detectedRoots);
236
+ const ignorePatterns = buildIgnorePatterns();
237
+
238
+ const lines = [
239
+ `configVersion: 1`,
240
+ ``,
241
+ `project:`,
242
+ ` name: "${projectName}"`,
243
+ ` docs_title_prefix: "RepoLens"`,
244
+ ``,
245
+ `publishers:`,
246
+ ` - markdown # Always generate local Markdown files`,
247
+ ` - notion # Auto-detected: publishes if NOTION_TOKEN is set`,
248
+ ``,
249
+ `# Optional: Configure Notion publishing behavior`,
250
+ `# notion:`,
251
+ `# branches:`,
252
+ `# - main # Only publish from main branch`,
253
+ `# - staging # Or add specific branches`,
254
+ `# includeBranchInTitle: false # Clean titles without [branch-name]`,
255
+ ``,
256
+ `# Optional: GitHub integration for SVG diagram hosting`,
257
+ `# github:`,
258
+ `# owner: "your-username"`,
259
+ `# repo: "your-repo-name"`,
260
+ ``,
261
+ `scan:`,
262
+ ` include:`
263
+ ];
264
+
265
+ if (includePatterns.length) {
266
+ for (const pattern of includePatterns) {
267
+ lines.push(` - "${pattern}"`);
268
+ }
269
+ } else {
270
+ lines.push(` - "src/**/*.{ts,tsx,js,jsx,md}"`);
271
+ lines.push(` - "app/**/*.{ts,tsx,js,jsx,md}"`);
272
+ }
273
+
274
+ lines.push(``);
275
+ lines.push(` ignore:`);
276
+
277
+ for (const pattern of ignorePatterns) {
278
+ lines.push(` - "${pattern}"`);
279
+ }
280
+
281
+ lines.push(``);
282
+ lines.push(`module_roots:`);
283
+
284
+ if (detectedRoots.length) {
285
+ for (const root of detectedRoots) {
286
+ lines.push(` - "${root}"`);
287
+ }
288
+ } else {
289
+ lines.push(` - "app"`);
290
+ lines.push(` - "src/app"`);
291
+ lines.push(` - "components"`);
292
+ lines.push(` - "src/components"`);
293
+ lines.push(` - "lib"`);
294
+ lines.push(` - "src/lib"`);
295
+ }
296
+
297
+ lines.push(``);
298
+ lines.push(`outputs:`);
299
+ lines.push(` pages:`);
300
+ lines.push(` - key: "system_overview"`);
301
+ lines.push(` title: "System Overview"`);
302
+ lines.push(` description: "High-level snapshot of the repo and what RepoLens detected."`);
303
+ lines.push(``);
304
+ lines.push(` - key: "module_catalog"`);
305
+ lines.push(` title: "Module Catalog"`);
306
+ lines.push(` description: "Auto-detected modules with file counts."`);
307
+ lines.push(``);
308
+ lines.push(` - key: "api_surface"`);
309
+ lines.push(` title: "API Surface"`);
310
+ lines.push(` description: "Auto-detected API routes/endpoints."`);
311
+ lines.push(``);
312
+ lines.push(` - key: "arch_diff"`);
313
+ lines.push(` title: "Architecture Diff"`);
314
+ lines.push(` description: "Reserved for PR/merge change summaries."`);
315
+ lines.push(``);
316
+ lines.push(` - key: "route_map"`);
317
+ lines.push(` title: "Route Map"`);
318
+ lines.push(` description: "Detected app routes and API routes."`);
319
+ lines.push(``);
320
+ lines.push(` - key: "system_map"`);
321
+ lines.push(` title: "System Map"`);
322
+ lines.push(` description: "Unicode architecture diagram of detected modules."`);
323
+ lines.push(``);
324
+
325
+ return lines.join("\n");
326
+ }
327
+
328
+ function detectProjectName(repoRoot) {
329
+ return path.basename(repoRoot) || "my-project";
330
+ }
331
+
332
+ async function promptNotionCredentials() {
333
+ // Skip prompts in CI environments or test mode
334
+ const isCI = process.env.CI ||
335
+ process.env.GITHUB_ACTIONS ||
336
+ process.env.GITLAB_CI ||
337
+ process.env.CIRCLECI ||
338
+ process.env.JENKINS_HOME ||
339
+ process.env.CODEBUILD_BUILD_ID;
340
+
341
+ const isTest = process.env.NODE_ENV === 'test' || process.env.VITEST;
342
+
343
+ if (isCI) {
344
+ info("ā­ļø Skipping interactive prompts (CI environment detected)");
345
+ return null;
346
+ }
347
+
348
+ if (isTest) {
349
+ // Skip silently in test mode
350
+ return null;
351
+ }
352
+
353
+ const rl = createInterface({
354
+ input: process.stdin,
355
+ output: process.stdout
356
+ });
357
+
358
+ try {
359
+ info("\nšŸ“ Notion Setup (optional)");
360
+ const useNotion = await rl.question("Would you like to publish to Notion? (Y/n): ");
361
+
362
+ if (useNotion.toLowerCase() === 'n') {
363
+ info("Skipping Notion setup. You can configure it later via environment variables.");
364
+ return null;
365
+ }
366
+
367
+ info("\nTo find your Notion Parent Page ID:");
368
+ info(" 1. Open the Notion page where you want docs published");
369
+ info(" 2. Copy the page URL (looks like: notion.so/workspace/abc123...)");
370
+ info(" 3. The page ID is the 32-character code at the end\n");
371
+
372
+ const parentPageId = await rl.question("NOTION_PARENT_PAGE_ID: ");
373
+
374
+ if (!parentPageId || parentPageId.trim() === '') {
375
+ warn("No parent page ID provided. Skipping Notion configuration.");
376
+ return null;
377
+ }
378
+
379
+ info("\nTo get your Notion Integration Token:");
380
+ info(" 1. Go to https://www.notion.so/my-integrations");
381
+ info(" 2. Create a new integration or use an existing one");
382
+ info(" 3. Copy the 'Internal Integration Token'");
383
+ info(" 4. Share the parent page with your integration\n");
384
+
385
+ const token = await rl.question("NOTION_TOKEN: ");
386
+
387
+ if (!token || token.trim() === '') {
388
+ warn("No token provided. Skipping Notion configuration.");
389
+ return null;
390
+ }
391
+
392
+ return {
393
+ parentPageId: parentPageId.trim(),
394
+ token: token.trim()
395
+ };
396
+ } finally {
397
+ rl.close();
398
+ }
399
+ }
400
+
401
+ async function ensureEnvInGitignore(repoRoot) {
402
+ const gitignorePath = path.join(repoRoot, ".gitignore");
403
+
404
+ try {
405
+ let gitignoreContent = '';
406
+
407
+ if (await fileExists(gitignorePath)) {
408
+ gitignoreContent = await fs.readFile(gitignorePath, "utf8");
409
+ }
410
+
411
+ // Check if .env is already in .gitignore
412
+ const lines = gitignoreContent.split('\n');
413
+ const hasEnvEntry = lines.some(line => line.trim() === '.env');
414
+
415
+ if (!hasEnvEntry) {
416
+ // Add .env to .gitignore
417
+ const newContent = gitignoreContent.trim() + (gitignoreContent.trim() ? '\n' : '') + '.env\n';
418
+ await fs.writeFile(gitignorePath, newContent, "utf8");
419
+ info("Added .env to .gitignore");
420
+ }
421
+ } catch (error) {
422
+ warn(`Could not update .gitignore: ${error.message}`);
423
+ }
424
+ }
425
+
426
+ export async function runInit(targetDir = process.cwd()) {
427
+ const repoRoot = path.resolve(targetDir);
428
+
429
+ // Ensure target directory exists
430
+ await fs.mkdir(repoRoot, { recursive: true });
431
+
432
+ // Prompt for Notion credentials interactively
433
+ const notionCredentials = await promptNotionCredentials();
434
+
435
+ const repolensConfigPath = path.join(repoRoot, ".repolens.yml");
436
+ const workflowDir = path.join(repoRoot, ".github", "workflows");
437
+ const workflowPath = path.join(workflowDir, "repolens.yml");
438
+
439
+ const envExamplePath = path.join(repoRoot, ".env.example");
440
+ const envPath = path.join(repoRoot, ".env");
441
+ const readmePath = path.join(repoRoot, "README.repolens.md");
442
+
443
+ const configExists = await fileExists(repolensConfigPath);
444
+ const workflowExists = await fileExists(workflowPath);
445
+
446
+ const envExampleExists = await fileExists(envExamplePath);
447
+ const envExists = await fileExists(envPath);
448
+ const readmeExists = await fileExists(readmePath);
449
+
450
+ const projectName = detectProjectName(repoRoot);
451
+ const detectedRoots = await detectRepoStructure(repoRoot);
452
+ const configContent = buildRepoLensConfig(projectName, detectedRoots);
453
+
454
+ info(`Detected project name: ${projectName}`);
455
+
456
+ if (detectedRoots.length) {
457
+ info(`Detected module roots:`);
458
+ for (const root of detectedRoots) {
459
+ info(` - ${root}`);
460
+ }
461
+ } else {
462
+ info(`No known roots detected. Falling back to default config.`);
463
+ }
464
+
465
+ if (!configExists) {
466
+ await fs.writeFile(repolensConfigPath, configContent, "utf8");
467
+ info(`Created ${repolensConfigPath}`);
468
+ } else {
469
+ info(`Skipped existing ${repolensConfigPath}`);
470
+ }
471
+
472
+ await fs.mkdir(workflowDir, { recursive: true });
473
+
474
+ if (!workflowExists) {
475
+ await fs.writeFile(workflowPath, DEFAULT_GITHUB_WORKFLOW, "utf8");
476
+ info(`Created ${workflowPath}`);
477
+ } else {
478
+ info(`Skipped existing ${workflowPath}`);
479
+ }
480
+
481
+ if (!envExampleExists) {
482
+ await fs.writeFile(envExamplePath, DEFAULT_ENV_EXAMPLE, "utf8");
483
+ info(`Created ${envExamplePath}`);
484
+ } else {
485
+ info(`Skipped existing ${envExamplePath}`);
486
+ }
487
+
488
+ // Create .env file with collected credentials
489
+ if (notionCredentials && !envExists) {
490
+ const envContent = `NOTION_TOKEN=${notionCredentials.token}
491
+ NOTION_PARENT_PAGE_ID=${notionCredentials.parentPageId}
492
+ NOTION_VERSION=2022-06-28
493
+ `;
494
+ await fs.writeFile(envPath, envContent, "utf8");
495
+ info(`āœ… Created ${envPath} with your Notion credentials`);
496
+
497
+ // Ensure .env is in .gitignore
498
+ await ensureEnvInGitignore(repoRoot);
499
+ } else if (notionCredentials && envExists) {
500
+ warn(`Skipped existing ${envPath} - your credentials were not overwritten`);
501
+ }
502
+
503
+ if (!readmeExists) {
504
+ await fs.writeFile(readmePath, DEFAULT_REPOLENS_README, "utf8");
505
+ info(`Created ${readmePath}`);
506
+ } else {
507
+ info(`Skipped existing ${readmePath}`);
508
+ }
509
+
510
+ info("\n✨ RepoLens initialization complete!\n");
511
+
512
+ if (notionCredentials) {
513
+ info("šŸŽ‰ Notion publishing is ready!");
514
+ info(" Your credentials are stored in .env (gitignored)\n");
515
+ info("Next steps:");
516
+ info(" 1. Review .repolens.yml to customize your documentation");
517
+ info(" 2. Run 'npx repolens publish' to generate your first docs (deterministic mode)");
518
+ info(" 3. (Optional) Enable AI features by adding to .env:");
519
+ info(" REPOLENS_AI_ENABLED=true");
520
+ info(" REPOLENS_AI_API_KEY=sk-...");
521
+ info(" See AI.md for full guide: https://github.com/CHAPIBUNNY/repolens/blob/main/AI.md");
522
+ info(" 4. For GitHub Actions, add these repository secrets:");
523
+ info(" - NOTION_TOKEN");
524
+ info(" - NOTION_PARENT_PAGE_ID");
525
+ info(" - REPOLENS_AI_API_KEY (if using AI features)");
526
+ info(" 5. Commit the generated files (workflow will run automatically)");
527
+ } else {
528
+ info("Next steps:");
529
+ info(" 1. Review .repolens.yml to customize your documentation");
530
+ info(" 2. To enable Notion publishing:");
531
+ info(" - Copy .env.example to .env and add your credentials, OR");
532
+ info(" - Add GitHub secrets: NOTION_TOKEN, NOTION_PARENT_PAGE_ID");
533
+ info(" 3. (Optional) Enable AI features by adding to .env:");
534
+ info(" REPOLENS_AI_ENABLED=true");
535
+ info(" REPOLENS_AI_API_KEY=sk-...");
536
+ info(" See: https://github.com/CHAPIBUNNY/repolens/blob/main/AI.md");
537
+ info(" 4. Run 'npx repolens publish' to test locally");
538
+ info(" 5. Commit the generated files");
539
+ }
540
+ }