@wtdlee/repomap 0.3.0 → 0.3.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.
Files changed (71) hide show
  1. package/dist/analyzers/index.d.ts +69 -5
  2. package/dist/analyzers/index.js +1 -5
  3. package/dist/chunk-3PWXDB7B.js +153 -0
  4. package/dist/{generators/page-map-generator.js → chunk-3YFXZAP7.js} +322 -358
  5. package/dist/chunk-6F4PWJZI.js +1 -0
  6. package/dist/{generators/rails-map-generator.js → chunk-E4WRODSI.js} +86 -94
  7. package/dist/chunk-GNBMJMET.js +2519 -0
  8. package/dist/{server/doc-server.js → chunk-M6YNU536.js} +702 -303
  9. package/dist/chunk-OWM6WNLE.js +2610 -0
  10. package/dist/chunk-SSU6QFTX.js +1058 -0
  11. package/dist/cli.d.ts +0 -1
  12. package/dist/cli.js +348 -452
  13. package/dist/dataflow-analyzer-BfAiqVUp.d.ts +180 -0
  14. package/dist/env-detector-EEMVUEIA.js +1 -0
  15. package/dist/generators/index.d.ts +431 -3
  16. package/dist/generators/index.js +2 -3
  17. package/dist/index.d.ts +53 -10
  18. package/dist/index.js +8 -11
  19. package/dist/page-map-generator-6MJGPBVA.js +1 -0
  20. package/dist/rails-UWSDRS33.js +1 -0
  21. package/dist/rails-map-generator-D2URLMVJ.js +2 -0
  22. package/dist/server/index.d.ts +33 -1
  23. package/dist/server/index.js +7 -1
  24. package/dist/types.d.ts +39 -37
  25. package/dist/types.js +1 -5
  26. package/package.json +4 -2
  27. package/dist/analyzers/base-analyzer.d.ts +0 -45
  28. package/dist/analyzers/base-analyzer.js +0 -47
  29. package/dist/analyzers/dataflow-analyzer.d.ts +0 -29
  30. package/dist/analyzers/dataflow-analyzer.js +0 -425
  31. package/dist/analyzers/graphql-analyzer.d.ts +0 -22
  32. package/dist/analyzers/graphql-analyzer.js +0 -386
  33. package/dist/analyzers/pages-analyzer.d.ts +0 -84
  34. package/dist/analyzers/pages-analyzer.js +0 -1695
  35. package/dist/analyzers/rails/index.d.ts +0 -46
  36. package/dist/analyzers/rails/index.js +0 -145
  37. package/dist/analyzers/rails/rails-controller-analyzer.d.ts +0 -82
  38. package/dist/analyzers/rails/rails-controller-analyzer.js +0 -478
  39. package/dist/analyzers/rails/rails-grpc-analyzer.d.ts +0 -44
  40. package/dist/analyzers/rails/rails-grpc-analyzer.js +0 -262
  41. package/dist/analyzers/rails/rails-model-analyzer.d.ts +0 -88
  42. package/dist/analyzers/rails/rails-model-analyzer.js +0 -493
  43. package/dist/analyzers/rails/rails-react-analyzer.d.ts +0 -41
  44. package/dist/analyzers/rails/rails-react-analyzer.js +0 -529
  45. package/dist/analyzers/rails/rails-routes-analyzer.d.ts +0 -62
  46. package/dist/analyzers/rails/rails-routes-analyzer.js +0 -540
  47. package/dist/analyzers/rails/rails-view-analyzer.d.ts +0 -49
  48. package/dist/analyzers/rails/rails-view-analyzer.js +0 -386
  49. package/dist/analyzers/rails/ruby-parser.d.ts +0 -63
  50. package/dist/analyzers/rails/ruby-parser.js +0 -212
  51. package/dist/analyzers/rest-api-analyzer.d.ts +0 -65
  52. package/dist/analyzers/rest-api-analyzer.js +0 -479
  53. package/dist/core/cache.d.ts +0 -47
  54. package/dist/core/cache.js +0 -151
  55. package/dist/core/engine.d.ts +0 -46
  56. package/dist/core/engine.js +0 -319
  57. package/dist/core/index.d.ts +0 -2
  58. package/dist/core/index.js +0 -2
  59. package/dist/generators/markdown-generator.d.ts +0 -25
  60. package/dist/generators/markdown-generator.js +0 -782
  61. package/dist/generators/mermaid-generator.d.ts +0 -35
  62. package/dist/generators/mermaid-generator.js +0 -364
  63. package/dist/generators/page-map-generator.d.ts +0 -22
  64. package/dist/generators/rails-map-generator.d.ts +0 -21
  65. package/dist/server/doc-server.d.ts +0 -30
  66. package/dist/utils/env-detector.d.ts +0 -31
  67. package/dist/utils/env-detector.js +0 -188
  68. package/dist/utils/parallel.d.ts +0 -23
  69. package/dist/utils/parallel.js +0 -70
  70. package/dist/utils/port.d.ts +0 -15
  71. package/dist/utils/port.js +0 -41
@@ -1,201 +1,609 @@
1
+ import { RailsMapGenerator } from './chunk-E4WRODSI.js';
2
+ import { detectEnvironments } from './chunk-3PWXDB7B.js';
3
+ import { RestApiAnalyzer, DataFlowAnalyzer, GraphQLAnalyzer, PagesAnalyzer } from './chunk-GNBMJMET.js';
4
+ import { MermaidGenerator, MarkdownGenerator } from './chunk-SSU6QFTX.js';
5
+ import { PageMapGenerator } from './chunk-3YFXZAP7.js';
6
+ import { analyzeRailsApp } from './chunk-OWM6WNLE.js';
7
+ import { simpleGit } from 'simple-git';
8
+ import * as fs from 'fs/promises';
9
+ import * as path2 from 'path';
10
+ import fg from 'fast-glob';
11
+ import * as crypto from 'crypto';
1
12
  import express from 'express';
2
13
  import { Server } from 'socket.io';
3
14
  import * as http from 'http';
4
- import * as fs from 'fs/promises';
5
- import * as path from 'path';
6
15
  import { marked } from 'marked';
7
- import { DocGeneratorEngine } from '../core/engine.js';
8
- import { PageMapGenerator } from '../generators/page-map-generator.js';
9
- import { RailsMapGenerator } from '../generators/rails-map-generator.js';
10
- import { detectEnvironments } from '../utils/env-detector.js';
11
- import { analyzeRailsApp } from '../analyzers/rails/index.js';
12
- import { findAvailablePort } from '../utils/port.js';
13
- /**
14
- * Documentation server with live reload
15
- * ライブリロード機能付きドキュメントサーバー
16
- */
17
- export class DocServer {
18
- config;
19
- port;
20
- app;
21
- server;
22
- io;
23
- engine;
24
- currentReport = null;
25
- envResult = null;
26
- railsAnalysis = null;
27
- constructor(config, port = 3030, options) {
28
- this.config = config;
29
- this.port = port;
30
- this.app = express();
31
- this.server = http.createServer(this.app);
32
- this.io = new Server(this.server);
33
- this.engine = new DocGeneratorEngine(config, { noCache: options?.noCache });
34
- this.setupRoutes();
35
- this.setupSocketIO();
16
+ import * as net from 'net';
17
+
18
+ var CACHE_VERSION = "1.1";
19
+ var AnalysisCache = class {
20
+ cacheDir;
21
+ manifest;
22
+ manifestPath;
23
+ dirty = false;
24
+ constructor(repoPath) {
25
+ this.cacheDir = path2.join(repoPath, ".repomap-cache");
26
+ this.manifestPath = path2.join(this.cacheDir, "manifest.json");
27
+ this.manifest = { version: CACHE_VERSION, entries: {} };
28
+ }
29
+ /**
30
+ * Initialize cache directory and load manifest
31
+ */
32
+ async init() {
33
+ try {
34
+ await fs.mkdir(this.cacheDir, { recursive: true });
35
+ } catch (err) {
36
+ console.warn(` Warning: Could not create cache directory: ${err.message}`);
37
+ return;
36
38
  }
37
- setupRoutes() {
38
- // Serve static assets
39
- this.app.use('/assets', express.static(path.join(this.config.outputDir, 'assets')));
40
- // Serve CSS files from generators/assets
41
- const cssFiles = ['common.css', 'page-map.css', 'docs.css', 'rails-map.css'];
42
- cssFiles.forEach((file) => {
43
- this.app.get(`/${file}`, async (req, res) => {
44
- try {
45
- const cssPath = new URL(`../generators/assets/${file}`, import.meta.url);
46
- const css = await fs.readFile(cssPath, 'utf-8');
47
- res.type('text/css').send(css);
48
- }
49
- catch {
50
- res.status(404).send('CSS not found');
51
- }
52
- });
53
- });
54
- // Main page - redirect to page-map
55
- this.app.get('/', (req, res) => {
56
- res.redirect('/page-map');
57
- });
58
- // Interactive page map (main view) - now with environment awareness
59
- this.app.get('/page-map', (req, res) => {
60
- if (!this.currentReport) {
61
- res.status(503).send('Documentation not ready yet');
62
- return;
63
- }
64
- const generator = new PageMapGenerator();
65
- res.send(generator.generatePageMapHtml(this.currentReport, {
66
- envResult: this.envResult,
67
- railsAnalysis: this.railsAnalysis,
68
- }));
69
- });
70
- // Rails map (standalone view)
71
- this.app.get('/rails-map', (req, res) => {
72
- if (!this.railsAnalysis) {
73
- res.status(404).send('No Rails environment detected');
74
- return;
75
- }
76
- const generator = new RailsMapGenerator();
77
- res.send(generator.generateFromResult(this.railsAnalysis));
78
- });
79
- // Markdown pages - index
80
- this.app.get('/docs', async (req, res) => {
81
- res.send(await this.renderPage('index'));
82
- });
83
- // Markdown pages - specific path
84
- this.app.get('/docs/*', async (req, res) => {
85
- const pagePath = req.params[0] || 'index';
86
- res.send(await this.renderPage(pagePath));
87
- });
88
- // API endpoints
89
- this.app.get('/api/report', (req, res) => {
90
- res.json(this.currentReport);
91
- });
92
- // Environment detection result
93
- this.app.get('/api/env', (req, res) => {
94
- res.json(this.envResult);
95
- });
96
- // Rails analysis result
97
- this.app.get('/api/rails', (req, res) => {
98
- if (this.railsAnalysis) {
99
- res.json(this.railsAnalysis);
100
- }
101
- else {
102
- res.status(404).json({ error: 'No Rails analysis available' });
103
- }
104
- });
105
- this.app.get('/api/diagram/:name', (req, res) => {
106
- const diagram = this.currentReport?.diagrams.find((d) => d.title.toLowerCase().replace(/\s+/g, '-') === req.params.name);
107
- if (diagram) {
108
- res.json(diagram);
109
- }
110
- else {
111
- res.status(404).json({ error: 'Diagram not found' });
112
- }
113
- });
114
- // Regenerate endpoint
115
- this.app.post('/api/regenerate', async (req, res) => {
116
- try {
117
- await this.regenerate();
118
- res.json({ success: true });
119
- }
120
- catch (error) {
121
- res.status(500).json({ error: error.message });
122
- }
123
- });
39
+ try {
40
+ const data = await fs.readFile(this.manifestPath, "utf-8");
41
+ const loaded = JSON.parse(data);
42
+ if (loaded.version === CACHE_VERSION) {
43
+ this.manifest = loaded;
44
+ } else {
45
+ console.log(" Cache version mismatch, clearing cache...");
46
+ await this.clear();
47
+ }
48
+ } catch {
124
49
  }
125
- setupSocketIO() {
126
- this.io.on('connection', (socket) => {
127
- console.log('Client connected');
128
- socket.on('disconnect', () => {
129
- console.log('Client disconnected');
130
- });
131
- });
50
+ }
51
+ /**
52
+ * Compute hash for a file
53
+ */
54
+ async computeFileHash(filePath) {
55
+ try {
56
+ const content = await fs.readFile(filePath, "utf-8");
57
+ return crypto.createHash("md5").update(content).digest("hex");
58
+ } catch {
59
+ return "";
132
60
  }
133
- async renderPage(pagePath) {
134
- // Remove .md extension if present
135
- const cleanPath = pagePath.replace(/\.md$/, '');
136
- const mdPath = path.join(this.config.outputDir, `${cleanPath}.md`);
137
- let content = '';
138
- try {
139
- const markdown = await fs.readFile(mdPath, 'utf-8');
140
- // Parse markdown to HTML
141
- let html = await marked.parse(markdown);
142
- // Convert mermaid code blocks to mermaid divs
143
- // marked renders: <pre><code class="language-mermaid">...</code></pre>
144
- // mermaid expects: <div class="mermaid">...</div>
145
- html = html.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, '<div class="mermaid">$1</div>');
146
- // Wrap tables for horizontal scroll
147
- html = html.replace(/<table>/g, '<div class="table-wrapper"><table>');
148
- html = html.replace(/<\/table>/g, '</table></div>');
149
- content = html;
150
- }
151
- catch (e) {
152
- console.error(`Failed to render page: ${mdPath}`, e);
153
- content = `<h1>Page not found</h1><p>Path: ${cleanPath}</p>`;
61
+ }
62
+ /**
63
+ * Compute hash for multiple files
64
+ * Sort files first for consistent ordering, then use a sample of files
65
+ */
66
+ async computeFilesHash(filePaths) {
67
+ const sortedFiles = [...filePaths].sort();
68
+ const sampleSize = 50;
69
+ let sampleFiles;
70
+ if (sortedFiles.length <= sampleSize * 2) {
71
+ sampleFiles = sortedFiles;
72
+ } else {
73
+ sampleFiles = [...sortedFiles.slice(0, sampleSize), ...sortedFiles.slice(-sampleSize)];
74
+ }
75
+ const hashes = await Promise.all(sampleFiles.map((f) => this.computeFileHash(f)));
76
+ const countHash = crypto.createHash("md5").update(String(sortedFiles.length)).digest("hex");
77
+ return crypto.createHash("md5").update(hashes.join("") + countHash).digest("hex");
78
+ }
79
+ /**
80
+ * Get cached data if valid
81
+ */
82
+ get(key, currentHash) {
83
+ const entry = this.manifest.entries[key];
84
+ if (entry && entry.hash === currentHash) {
85
+ return entry.data;
86
+ }
87
+ return null;
88
+ }
89
+ /**
90
+ * Store data in cache
91
+ */
92
+ set(key, hash, data) {
93
+ this.manifest.entries[key] = {
94
+ hash,
95
+ timestamp: Date.now(),
96
+ data
97
+ };
98
+ this.dirty = true;
99
+ }
100
+ /**
101
+ * Save manifest to disk
102
+ */
103
+ async save() {
104
+ if (!this.dirty) return;
105
+ try {
106
+ await fs.mkdir(this.cacheDir, { recursive: true });
107
+ await fs.writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2));
108
+ this.dirty = false;
109
+ } catch (error) {
110
+ console.warn(" Warning: Failed to save cache:", error.message);
111
+ }
112
+ }
113
+ /**
114
+ * Clear all cache
115
+ */
116
+ async clear() {
117
+ this.manifest = { version: CACHE_VERSION, entries: {} };
118
+ this.dirty = true;
119
+ try {
120
+ await fs.rm(this.cacheDir, { recursive: true, force: true });
121
+ await fs.mkdir(this.cacheDir, { recursive: true });
122
+ } catch {
123
+ }
124
+ }
125
+ /**
126
+ * Get cache statistics
127
+ */
128
+ getStats() {
129
+ const entries = Object.keys(this.manifest.entries).length;
130
+ const size = JSON.stringify(this.manifest).length;
131
+ return {
132
+ entries,
133
+ size: size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`
134
+ };
135
+ }
136
+ };
137
+
138
+ // src/core/engine.ts
139
+ var DocGeneratorEngine = class {
140
+ config;
141
+ mermaidGenerator;
142
+ markdownGenerator;
143
+ noCache;
144
+ constructor(config, options) {
145
+ this.config = config;
146
+ this.mermaidGenerator = new MermaidGenerator();
147
+ this.markdownGenerator = new MarkdownGenerator();
148
+ this.noCache = options?.noCache ?? false;
149
+ }
150
+ /**
151
+ * Run documentation generation for all configured repositories
152
+ */
153
+ async generate() {
154
+ console.log("\u{1F680} Starting documentation generation...\n");
155
+ const repositoryReports = [];
156
+ for (const repoConfig of this.config.repositories) {
157
+ try {
158
+ console.log(`
159
+ \u{1F4E6} Analyzing ${repoConfig.displayName}...`);
160
+ const report2 = await this.analyzeRepository(repoConfig);
161
+ repositoryReports.push(report2);
162
+ console.log(`\u2705 Completed ${repoConfig.displayName}`);
163
+ } catch (error) {
164
+ console.error(`\u274C Failed to analyze ${repoConfig.name}:`, error.message);
165
+ }
166
+ }
167
+ console.log("\n\u{1F517} Running cross-repository analysis...");
168
+ const crossRepoAnalysis = this.analyzeCrossRepo(repositoryReports);
169
+ console.log("\n\u{1F4CA} Generating diagrams...");
170
+ const results = repositoryReports.map((r) => r.analysis);
171
+ const crossRepoLinks = this.extractCrossRepoLinks(results);
172
+ const diagrams = this.mermaidGenerator.generateAll(results, crossRepoLinks);
173
+ const report = {
174
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
175
+ repositories: repositoryReports,
176
+ crossRepoAnalysis,
177
+ diagrams
178
+ };
179
+ console.log("\n\u{1F4DD} Writing documentation...");
180
+ await this.writeDocumentation(report);
181
+ console.log("\n\u2728 Documentation generation complete!");
182
+ console.log(`\u{1F4C1} Output: ${this.config.outputDir}`);
183
+ return report;
184
+ }
185
+ /**
186
+ * Analyze a single repository (with caching)
187
+ */
188
+ async analyzeRepository(repoConfig) {
189
+ const cache = new AnalysisCache(repoConfig.path);
190
+ await cache.init();
191
+ const { version, commitHash } = await this.getRepoInfo(repoConfig);
192
+ const sourceFiles = await fg(["**/*.{ts,tsx,graphql}"], {
193
+ cwd: repoConfig.path,
194
+ ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"],
195
+ absolute: true
196
+ });
197
+ const contentHash = await cache.computeFilesHash(sourceFiles);
198
+ const cacheKey = `analysis_v${version}_${repoConfig.name}_${commitHash}`;
199
+ if (this.noCache) {
200
+ console.log(` \u{1F504} Cache disabled, analyzing from scratch...`);
201
+ }
202
+ const cachedResult = this.noCache ? null : cache.get(cacheKey, contentHash);
203
+ if (cachedResult) {
204
+ console.log(` \u26A1 Using cached analysis (hash: ${contentHash.slice(0, 8)}...)`);
205
+ const summary2 = {
206
+ totalPages: cachedResult.pages.length,
207
+ totalComponents: cachedResult.components.length,
208
+ totalGraphQLOperations: cachedResult.graphqlOperations.length,
209
+ totalDataFlows: cachedResult.dataFlows.length,
210
+ authRequiredPages: cachedResult.pages.filter((p) => p.authentication.required).length,
211
+ publicPages: cachedResult.pages.filter((p) => !p.authentication.required).length
212
+ };
213
+ return {
214
+ name: repoConfig.name,
215
+ displayName: repoConfig.displayName,
216
+ version,
217
+ commitHash,
218
+ analysis: cachedResult,
219
+ summary: summary2
220
+ };
221
+ }
222
+ const analyzers = repoConfig.analyzers.map((analyzerType) => this.createAnalyzer(analyzerType, repoConfig)).filter((a) => a !== null);
223
+ console.log(` Running ${analyzers.length} analyzers in parallel...`);
224
+ const startTime = Date.now();
225
+ const analysisResults = await Promise.all(analyzers.map((analyzer) => analyzer.analyze()));
226
+ console.log(` Analysis completed in ${((Date.now() - startTime) / 1e3).toFixed(1)}s`);
227
+ const analysis = this.mergeAnalysisResults(
228
+ analysisResults,
229
+ repoConfig.name,
230
+ version,
231
+ commitHash
232
+ );
233
+ cache.set(cacheKey, contentHash, analysis);
234
+ await cache.save();
235
+ console.log(` \u{1F4BE} Analysis cached (hash: ${contentHash.slice(0, 8)}...)`);
236
+ const summary = {
237
+ totalPages: analysis.pages.length,
238
+ totalComponents: analysis.components.length,
239
+ totalGraphQLOperations: analysis.graphqlOperations.length,
240
+ totalDataFlows: analysis.dataFlows.length,
241
+ authRequiredPages: analysis.pages.filter((p) => p.authentication.required).length,
242
+ publicPages: analysis.pages.filter((p) => !p.authentication.required).length
243
+ };
244
+ return {
245
+ name: repoConfig.name,
246
+ displayName: repoConfig.displayName,
247
+ version,
248
+ commitHash,
249
+ analysis,
250
+ summary
251
+ };
252
+ }
253
+ /**
254
+ * Get repository version and commit info
255
+ */
256
+ async getRepoInfo(repoConfig) {
257
+ try {
258
+ const git = simpleGit(repoConfig.path);
259
+ const log = await git.log({ n: 1 });
260
+ const commitHash = log.latest?.hash || "unknown";
261
+ let version = "unknown";
262
+ try {
263
+ const packageJsonPath = path2.join(repoConfig.path, "package.json");
264
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
265
+ version = packageJson.version || "unknown";
266
+ } catch {
267
+ }
268
+ return { version, commitHash };
269
+ } catch {
270
+ return { version: "unknown", commitHash: "unknown" };
271
+ }
272
+ }
273
+ /**
274
+ * Create analyzer instance based on type
275
+ */
276
+ createAnalyzer(type, config) {
277
+ switch (type) {
278
+ case "pages":
279
+ if (config.type === "nextjs" || config.type === "rails" || config.type === "generic") {
280
+ return new PagesAnalyzer(config);
154
281
  }
155
- return this.getHtmlTemplate(content);
282
+ break;
283
+ case "graphql":
284
+ return new GraphQLAnalyzer(config);
285
+ case "dataflow":
286
+ case "components":
287
+ return new DataFlowAnalyzer(config);
288
+ case "rest-api":
289
+ case "api":
290
+ return new RestApiAnalyzer(config);
156
291
  }
157
- getGraphQLData() {
158
- if (!this.currentReport)
159
- return '[]';
160
- const ops = [];
161
- for (const repo of this.currentReport.repositories) {
162
- for (const op of repo.analysis?.graphqlOperations || []) {
163
- ops.push({
164
- name: op.name,
165
- type: op.type,
166
- returnType: op.returnType,
167
- variables: op.variables,
168
- fields: op.fields,
169
- usedIn: op.usedIn,
170
- });
171
- }
292
+ return null;
293
+ }
294
+ /**
295
+ * Merge multiple analysis results
296
+ */
297
+ mergeAnalysisResults(results, repository, version, commitHash) {
298
+ const merged = {
299
+ repository,
300
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
301
+ version,
302
+ commitHash,
303
+ pages: [],
304
+ graphqlOperations: [],
305
+ apiCalls: [],
306
+ components: [],
307
+ dataFlows: [],
308
+ apiEndpoints: [],
309
+ models: [],
310
+ crossRepoLinks: []
311
+ };
312
+ for (const result of results) {
313
+ if (result.pages) merged.pages.push(...result.pages);
314
+ if (result.graphqlOperations) merged.graphqlOperations.push(...result.graphqlOperations);
315
+ if (result.apiCalls) merged.apiCalls.push(...result.apiCalls);
316
+ if (result.components) merged.components.push(...result.components);
317
+ if (result.dataFlows) merged.dataFlows.push(...result.dataFlows);
318
+ if (result.apiEndpoints) merged.apiEndpoints.push(...result.apiEndpoints);
319
+ if (result.models) merged.models.push(...result.models);
320
+ if (result.crossRepoLinks) merged.crossRepoLinks.push(...result.crossRepoLinks);
321
+ }
322
+ return merged;
323
+ }
324
+ /**
325
+ * Analyze cross-repository relationships
326
+ */
327
+ analyzeCrossRepo(reports) {
328
+ const sharedTypes = [];
329
+ const apiConnections = [];
330
+ const navigationFlows = [];
331
+ const dataFlowAcrossRepos = [];
332
+ const operationsByName = /* @__PURE__ */ new Map();
333
+ for (const report of reports) {
334
+ for (const op of report.analysis.graphqlOperations) {
335
+ const repos = operationsByName.get(op.name) || [];
336
+ repos.push(report.name);
337
+ operationsByName.set(op.name, repos);
338
+ }
339
+ }
340
+ for (const [name, repos] of operationsByName) {
341
+ if (repos.length > 1) {
342
+ sharedTypes.push(name);
343
+ }
344
+ }
345
+ const frontendRepos = reports.filter((r) => r.analysis.pages.length > 0);
346
+ const backendRepos = reports.filter((r) => r.analysis.apiEndpoints.length > 0);
347
+ for (const frontend of frontendRepos) {
348
+ for (const backend of backendRepos) {
349
+ for (const endpoint of backend.analysis.apiEndpoints) {
350
+ apiConnections.push({
351
+ frontend: frontend.name,
352
+ backend: backend.name,
353
+ endpoint: endpoint.path,
354
+ operations: frontend.analysis.graphqlOperations.filter((op) => op.usedIn.length > 0).map((op) => op.name)
355
+ });
172
356
  }
173
- return JSON.stringify(ops);
357
+ }
174
358
  }
175
- getApiCallsData() {
176
- if (!this.currentReport)
177
- return '[]';
178
- const calls = [];
179
- for (const repo of this.currentReport.repositories) {
180
- for (const call of repo.analysis?.apiCalls || []) {
181
- calls.push({
182
- id: call.id,
183
- method: call.method,
184
- url: call.url,
185
- callType: call.callType,
186
- filePath: call.filePath,
187
- line: call.line,
188
- containingFunction: call.containingFunction,
189
- requiresAuth: call.requiresAuth,
190
- });
191
- }
359
+ return {
360
+ sharedTypes,
361
+ apiConnections,
362
+ navigationFlows,
363
+ dataFlowAcrossRepos
364
+ };
365
+ }
366
+ /**
367
+ * Extract cross-repository links
368
+ */
369
+ extractCrossRepoLinks(results) {
370
+ const links = [];
371
+ const operationsByName = /* @__PURE__ */ new Map();
372
+ for (const result of results) {
373
+ for (const op of result.graphqlOperations) {
374
+ const existing = operationsByName.get(op.name) || [];
375
+ existing.push(result);
376
+ operationsByName.set(op.name, existing);
377
+ }
378
+ }
379
+ for (const [name, repos] of operationsByName) {
380
+ if (repos.length > 1) {
381
+ links.push({
382
+ sourceRepo: repos[0].repository,
383
+ sourcePath: `graphql/${name}`,
384
+ targetRepo: repos[1].repository,
385
+ targetPath: `graphql/${name}`,
386
+ linkType: "graphql-operation",
387
+ description: `Shared GraphQL operation: ${name}`
388
+ });
389
+ }
390
+ }
391
+ return links;
392
+ }
393
+ /**
394
+ * Write documentation to output directory
395
+ */
396
+ async writeDocumentation(report) {
397
+ const outputDir = this.config.outputDir;
398
+ await fs.mkdir(outputDir, { recursive: true });
399
+ const docs = this.markdownGenerator.generateDocumentation(report);
400
+ for (const [filePath, content] of docs) {
401
+ const fullPath = path2.join(outputDir, filePath);
402
+ const dir = path2.dirname(fullPath);
403
+ await fs.mkdir(dir, { recursive: true });
404
+ await fs.writeFile(fullPath, content, "utf-8");
405
+ console.log(` \u{1F4C4} ${filePath}`);
406
+ }
407
+ const jsonPath = path2.join(outputDir, "report.json");
408
+ await fs.writeFile(jsonPath, JSON.stringify(report, null, 2), "utf-8");
409
+ console.log(` \u{1F4CB} report.json`);
410
+ }
411
+ };
412
+ function isPortAvailable(port) {
413
+ return new Promise((resolve) => {
414
+ const server = net.createServer();
415
+ server.once("error", (err) => {
416
+ if (err.code === "EADDRINUSE") {
417
+ resolve(false);
418
+ } else {
419
+ resolve(false);
420
+ }
421
+ });
422
+ server.once("listening", () => {
423
+ server.close();
424
+ resolve(true);
425
+ });
426
+ server.listen(port);
427
+ });
428
+ }
429
+ async function findAvailablePort(startPort, maxAttempts = 10) {
430
+ for (let i = 0; i < maxAttempts; i++) {
431
+ const port = startPort + i;
432
+ if (await isPortAvailable(port)) {
433
+ return port;
434
+ }
435
+ }
436
+ throw new Error(
437
+ `No available port found between ${startPort} and ${startPort + maxAttempts - 1}`
438
+ );
439
+ }
440
+
441
+ // src/server/doc-server.ts
442
+ var DocServer = class {
443
+ config;
444
+ port;
445
+ app;
446
+ server;
447
+ io;
448
+ engine;
449
+ currentReport = null;
450
+ envResult = null;
451
+ railsAnalysis = null;
452
+ constructor(config, port = 3030, options) {
453
+ this.config = config;
454
+ this.port = port;
455
+ this.app = express();
456
+ this.server = http.createServer(this.app);
457
+ this.io = new Server(this.server);
458
+ this.engine = new DocGeneratorEngine(config, { noCache: options?.noCache });
459
+ this.setupRoutes();
460
+ this.setupSocketIO();
461
+ }
462
+ setupRoutes() {
463
+ this.app.use("/assets", express.static(path2.join(this.config.outputDir, "assets")));
464
+ const cssFiles = ["common.css", "page-map.css", "docs.css", "rails-map.css"];
465
+ cssFiles.forEach((file) => {
466
+ this.app.get(`/${file}`, async (req, res) => {
467
+ try {
468
+ const cssPath = new URL(`../generators/assets/${file}`, import.meta.url);
469
+ const css = await fs.readFile(cssPath, "utf-8");
470
+ res.type("text/css").send(css);
471
+ } catch {
472
+ res.status(404).send("CSS not found");
192
473
  }
193
- return JSON.stringify(calls);
474
+ });
475
+ });
476
+ this.app.get("/", (req, res) => {
477
+ res.redirect("/page-map");
478
+ });
479
+ this.app.get("/page-map", (req, res) => {
480
+ if (!this.currentReport) {
481
+ res.status(503).send("Documentation not ready yet");
482
+ return;
483
+ }
484
+ const generator = new PageMapGenerator();
485
+ res.send(
486
+ generator.generatePageMapHtml(this.currentReport, {
487
+ envResult: this.envResult,
488
+ railsAnalysis: this.railsAnalysis
489
+ })
490
+ );
491
+ });
492
+ this.app.get("/rails-map", (req, res) => {
493
+ if (!this.railsAnalysis) {
494
+ res.status(404).send("No Rails environment detected");
495
+ return;
496
+ }
497
+ const generator = new RailsMapGenerator();
498
+ res.send(generator.generateFromResult(this.railsAnalysis));
499
+ });
500
+ this.app.get("/docs", async (req, res) => {
501
+ res.send(await this.renderPage("index"));
502
+ });
503
+ this.app.get("/docs/*", async (req, res) => {
504
+ const pagePath = req.params[0] || "index";
505
+ res.send(await this.renderPage(pagePath));
506
+ });
507
+ this.app.get("/api/report", (req, res) => {
508
+ res.json(this.currentReport);
509
+ });
510
+ this.app.get("/api/env", (req, res) => {
511
+ res.json(this.envResult);
512
+ });
513
+ this.app.get("/api/rails", (req, res) => {
514
+ if (this.railsAnalysis) {
515
+ res.json(this.railsAnalysis);
516
+ } else {
517
+ res.status(404).json({ error: "No Rails analysis available" });
518
+ }
519
+ });
520
+ this.app.get("/api/diagram/:name", (req, res) => {
521
+ const diagram = this.currentReport?.diagrams.find(
522
+ (d) => d.title.toLowerCase().replace(/\s+/g, "-") === req.params.name
523
+ );
524
+ if (diagram) {
525
+ res.json(diagram);
526
+ } else {
527
+ res.status(404).json({ error: "Diagram not found" });
528
+ }
529
+ });
530
+ this.app.post("/api/regenerate", async (req, res) => {
531
+ try {
532
+ await this.regenerate();
533
+ res.json({ success: true });
534
+ } catch (error) {
535
+ res.status(500).json({ error: error.message });
536
+ }
537
+ });
538
+ }
539
+ setupSocketIO() {
540
+ this.io.on("connection", (socket) => {
541
+ console.log("Client connected");
542
+ socket.on("disconnect", () => {
543
+ console.log("Client disconnected");
544
+ });
545
+ });
546
+ }
547
+ async renderPage(pagePath) {
548
+ const cleanPath = pagePath.replace(/\.md$/, "");
549
+ const mdPath = path2.join(this.config.outputDir, `${cleanPath}.md`);
550
+ let content = "";
551
+ try {
552
+ const markdown = await fs.readFile(mdPath, "utf-8");
553
+ let html = await marked.parse(markdown);
554
+ html = html.replace(
555
+ /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
556
+ '<div class="mermaid">$1</div>'
557
+ );
558
+ html = html.replace(/<table>/g, '<div class="table-wrapper"><table>');
559
+ html = html.replace(/<\/table>/g, "</table></div>");
560
+ content = html;
561
+ } catch (e) {
562
+ console.error(`Failed to render page: ${mdPath}`, e);
563
+ content = `<h1>Page not found</h1><p>Path: ${cleanPath}</p>`;
194
564
  }
195
- getHtmlTemplate(content) {
196
- const graphqlData = this.getGraphQLData();
197
- const apiCallsData = this.getApiCallsData();
198
- return `<!DOCTYPE html>
565
+ return this.getHtmlTemplate(content);
566
+ }
567
+ getGraphQLData() {
568
+ if (!this.currentReport) return "[]";
569
+ const ops = [];
570
+ for (const repo of this.currentReport.repositories) {
571
+ for (const op of repo.analysis?.graphqlOperations || []) {
572
+ ops.push({
573
+ name: op.name,
574
+ type: op.type,
575
+ returnType: op.returnType,
576
+ variables: op.variables,
577
+ fields: op.fields,
578
+ usedIn: op.usedIn
579
+ });
580
+ }
581
+ }
582
+ return JSON.stringify(ops);
583
+ }
584
+ getApiCallsData() {
585
+ if (!this.currentReport) return "[]";
586
+ const calls = [];
587
+ for (const repo of this.currentReport.repositories) {
588
+ for (const call of repo.analysis?.apiCalls || []) {
589
+ calls.push({
590
+ id: call.id,
591
+ method: call.method,
592
+ url: call.url,
593
+ callType: call.callType,
594
+ filePath: call.filePath,
595
+ line: call.line,
596
+ containingFunction: call.containingFunction,
597
+ requiresAuth: call.requiresAuth
598
+ });
599
+ }
600
+ }
601
+ return JSON.stringify(calls);
602
+ }
603
+ getHtmlTemplate(content) {
604
+ const graphqlData = this.getGraphQLData();
605
+ const apiCallsData = this.getApiCallsData();
606
+ return `<!DOCTYPE html>
199
607
  <html lang="ja">
200
608
  <head>
201
609
  <meta charset="UTF-8">
@@ -273,10 +681,10 @@ export class DocServer {
273
681
  <body>
274
682
  <header class="header">
275
683
  <div style="display:flex;align-items:center;gap:24px">
276
- <h1 style="cursor:pointer" onclick="location.href='/'">📊 ${this.config.repositories[0]?.displayName || this.config.repositories[0]?.name || 'Repository'}</h1>
684
+ <h1 style="cursor:pointer" onclick="location.href='/'">\u{1F4CA} ${this.config.repositories[0]?.displayName || this.config.repositories[0]?.name || "Repository"}</h1>
277
685
  <nav style="display:flex;gap:4px">
278
686
  <a href="/page-map" class="nav-link">Page Map</a>
279
- ${this.railsAnalysis ? '<a href="/rails-map" class="nav-link">Rails Map</a>' : ''}
687
+ ${this.railsAnalysis ? '<a href="/rails-map" class="nav-link">Rails Map</a>' : ""}
280
688
  <a href="/docs" class="nav-link active">Docs</a>
281
689
  <a href="/api/report" class="nav-link" target="_blank">API</a>
282
690
  </nav>
@@ -288,14 +696,14 @@ export class DocServer {
288
696
  <div class="nav-group">
289
697
  <span class="nav-group-title">Documentation</span>
290
698
  <div class="nav-subitems">
291
- ${this.config.repositories
292
- .map((repo) => `
699
+ ${this.config.repositories.map(
700
+ (repo) => `
293
701
  <a href="/docs/repos/${repo.name}/pages">Pages</a>
294
702
  <a href="/docs/repos/${repo.name}/components">Components</a>
295
703
  <a href="/docs/repos/${repo.name}/graphql">GraphQL</a>
296
704
  <a href="/docs/repos/${repo.name}/dataflow">Data Flow</a>
297
- `)
298
- .join('')}
705
+ `
706
+ ).join("")}
299
707
  </div>
300
708
  </div>
301
709
  <div class="nav-group">
@@ -321,10 +729,10 @@ export class DocServer {
321
729
  <div class="detail-modal-content">
322
730
  <div class="detail-modal-header">
323
731
  <div style="display:flex;align-items:center;gap:8px">
324
- <button id="modalBackBtn" onclick="modalBack()" style="display:none;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:4px 8px;cursor:pointer;font-size:14px">← Back</button>
732
+ <button id="modalBackBtn" onclick="modalBack()" style="display:none;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:4px 8px;cursor:pointer;font-size:14px">\u2190 Back</button>
325
733
  <h3 id="modalTitle">Details</h3>
326
734
  </div>
327
- <button class="detail-modal-close" onclick="closeModal()">×</button>
735
+ <button class="detail-modal-close" onclick="closeModal()">\xD7</button>
328
736
  </div>
329
737
  <div id="modalBody"></div>
330
738
  </div>
@@ -350,10 +758,10 @@ export class DocServer {
350
758
  container.className = 'mermaid-container';
351
759
  container.innerHTML = \`
352
760
  <div class="mermaid-controls">
353
- <button onclick="zoomDiagram(\${idx}, 0.8)" title="縮小">➖</button>
354
- <button onclick="zoomDiagram(\${idx}, 1.25)" title="拡大">➕</button>
355
- <button onclick="zoomDiagram(\${idx}, 'reset')" title="リセット">🔄</button>
356
- <button onclick="toggleFullscreen(\${idx})" title="全画面">⛶</button>
761
+ <button onclick="zoomDiagram(\${idx}, 0.8)" title="\u7E2E\u5C0F">\u2796</button>
762
+ <button onclick="zoomDiagram(\${idx}, 1.25)" title="\u62E1\u5927">\u2795</button>
763
+ <button onclick="zoomDiagram(\${idx}, 'reset')" title="\u30EA\u30BB\u30C3\u30C8">\u{1F504}</button>
764
+ <button onclick="toggleFullscreen(\${idx})" title="\u5168\u753B\u9762">\u26F6</button>
357
765
  </div>
358
766
  <div class="mermaid-wrapper" id="wrapper-\${idx}">
359
767
  <div class="mermaid-inner" id="inner-\${idx}"></div>
@@ -481,17 +889,17 @@ export class DocServer {
481
889
  modalHistory = [];
482
890
 
483
891
  // Clean name: remove icons and extract operation name from patterns like "GraphQL: OPERATION_NAME"
484
- let cleanName = text.replace(/[\u{1F512}\u{1F4E1}\u{270F}\u{FE0F}\u{1F504}]/gu, '').trim();
892
+ let cleanName = text.replace(/[\u{1F512}\u{1F4E1}\u270F\uFE0F\u{1F504}]/gu, '').trim();
485
893
  // Handle "GraphQL: OPERATION_NAME" pattern
486
894
  if (cleanName.includes('GraphQL:')) {
487
- cleanName = cleanName.replace(/^.*GraphQL:\s*/, '').trim();
895
+ cleanName = cleanName.replace(/^.*GraphQL:s*/, '').trim();
488
896
  }
489
897
  // Handle "API: OPERATION_NAME" pattern
490
898
  if (cleanName.includes('API:')) {
491
- cleanName = cleanName.replace(/^.*API:\s*/, '').trim();
899
+ cleanName = cleanName.replace(/^.*API:s*/, '').trim();
492
900
  }
493
901
  // Remove any remaining prefixes like "Query:", "Mutation:"
494
- cleanName = cleanName.replace(/^(Query|Mutation|Fragment):\s*/i, '').trim();
902
+ cleanName = cleanName.replace(/^(Query|Mutation|Fragment):s*/i, '').trim();
495
903
 
496
904
  const op = window.findGraphQLOp?.(cleanName);
497
905
 
@@ -1118,7 +1526,7 @@ export class DocServer {
1118
1526
  async function regenerate() {
1119
1527
  try {
1120
1528
  const btn = document.querySelector('.regenerate-btn');
1121
- btn.textContent = ' 生成中...';
1529
+ btn.textContent = '\u23F3 \u751F\u6210\u4E2D...';
1122
1530
  btn.disabled = true;
1123
1531
 
1124
1532
  const res = await fetch('/api/regenerate', { method: 'POST' });
@@ -1127,119 +1535,110 @@ export class DocServer {
1127
1535
  if (data.success) {
1128
1536
  window.location.reload();
1129
1537
  } else {
1130
- alert('生成に失敗しました: ' + data.error);
1538
+ alert('\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ' + data.error);
1131
1539
  }
1132
1540
  } catch (e) {
1133
- alert('エラー: ' + e.message);
1541
+ alert('\u30A8\u30E9\u30FC: ' + e.message);
1134
1542
  } finally {
1135
1543
  const btn = document.querySelector('.regenerate-btn');
1136
- btn.textContent = '🔄 再生成';
1544
+ btn.textContent = '\u{1F504} \u518D\u751F\u6210';
1137
1545
  btn.disabled = false;
1138
1546
  }
1139
1547
  }
1140
1548
  </script>
1141
1549
  </body>
1142
1550
  </html>`;
1143
- }
1144
- async start(openBrowser = true) {
1145
- // Detect environments first
1146
- const rootPath = this.config.repositories[0]?.path || process.cwd();
1147
- console.log('🔍 Detecting project environments...');
1148
- this.envResult = await detectEnvironments(rootPath);
1149
- if (this.envResult.environments.length > 0) {
1150
- console.log(` Found: ${this.envResult.environments.map((e) => e.type).join(', ')}`);
1151
- for (const env of this.envResult.environments) {
1152
- if (env.features.length > 0) {
1153
- console.log(` ${env.type} features: ${env.features.join(', ')}`);
1154
- }
1155
- }
1156
- }
1157
- // Generate initial documentation for frontend
1158
- console.log('\n📚 Generating documentation...');
1159
- this.currentReport = await this.engine.generate();
1160
- // If Rails is detected, also analyze Rails
1161
- if (this.envResult.hasRails) {
1162
- console.log('\n🛤️ Analyzing Rails application...');
1163
- try {
1164
- this.railsAnalysis = await analyzeRailsApp(rootPath);
1165
- console.log(` ✅ Rails analysis complete`);
1166
- }
1167
- catch (error) {
1168
- console.error(` ⚠️ Rails analysis failed:`, error.message);
1169
- }
1170
- }
1171
- // Find available port (auto-detect if requested port is in use)
1172
- try {
1173
- const availablePort = await findAvailablePort(this.port);
1174
- if (availablePort !== this.port) {
1175
- console.log(`\n⚠️ Port ${this.port} is in use, using port ${availablePort} instead`);
1176
- }
1177
- this.port = availablePort;
1178
- }
1179
- catch (error) {
1180
- console.error(`\n❌ Failed to find available port: ${error.message}`);
1181
- process.exit(1);
1182
- }
1183
- // Start server
1184
- this.server.listen(this.port, () => {
1185
- console.log(`\n🌐 Documentation server running at http://localhost:${this.port}`);
1186
- if (this.envResult?.hasRails && this.envResult?.hasNextjs) {
1187
- console.log(' 📊 Multiple environments detected - use tabs to switch views');
1188
- }
1189
- console.log(' Press Ctrl+C to stop\n');
1190
- });
1191
- // Open browser
1192
- if (openBrowser) {
1193
- const open = (await import('open')).default;
1194
- await open(`http://localhost:${this.port}`);
1195
- }
1196
- // Watch for changes
1197
- if (this.config.watch.enabled) {
1198
- this.watchForChanges();
1551
+ }
1552
+ async start(openBrowser = true) {
1553
+ const rootPath = this.config.repositories[0]?.path || process.cwd();
1554
+ console.log("\u{1F50D} Detecting project environments...");
1555
+ this.envResult = await detectEnvironments(rootPath);
1556
+ if (this.envResult.environments.length > 0) {
1557
+ console.log(` Found: ${this.envResult.environments.map((e) => e.type).join(", ")}`);
1558
+ for (const env of this.envResult.environments) {
1559
+ if (env.features.length > 0) {
1560
+ console.log(` ${env.type} features: ${env.features.join(", ")}`);
1199
1561
  }
1562
+ }
1200
1563
  }
1201
- async regenerate() {
1202
- console.log('\n🔄 Regenerating documentation...');
1203
- this.currentReport = await this.engine.generate();
1204
- // Re-analyze Rails if detected
1205
- if (this.envResult?.hasRails) {
1206
- const rootPath = this.config.repositories[0]?.path || process.cwd();
1207
- try {
1208
- this.railsAnalysis = await analyzeRailsApp(rootPath);
1209
- }
1210
- catch (error) {
1211
- console.error(`⚠️ Rails re-analysis failed:`, error.message);
1212
- }
1213
- }
1214
- this.io.emit('reload');
1215
- console.log('✅ Documentation regenerated');
1564
+ console.log("\n\u{1F4DA} Generating documentation...");
1565
+ this.currentReport = await this.engine.generate();
1566
+ if (this.envResult.hasRails) {
1567
+ console.log("\n\u{1F6E4}\uFE0F Analyzing Rails application...");
1568
+ try {
1569
+ this.railsAnalysis = await analyzeRailsApp(rootPath);
1570
+ console.log(` \u2705 Rails analysis complete`);
1571
+ } catch (error) {
1572
+ console.error(` \u26A0\uFE0F Rails analysis failed:`, error.message);
1573
+ }
1216
1574
  }
1217
- async watchForChanges() {
1218
- const watchDirs = this.config.repositories.map((r) => r.path);
1219
- let timeout = null;
1220
- for (const dir of watchDirs) {
1221
- try {
1222
- const watcher = fs.watch(dir, { recursive: true });
1223
- (async () => {
1224
- for await (const event of watcher) {
1225
- if (event.filename &&
1226
- (event.filename.endsWith('.ts') || event.filename.endsWith('.tsx'))) {
1227
- if (timeout)
1228
- clearTimeout(timeout);
1229
- timeout = setTimeout(async () => {
1230
- await this.regenerate();
1231
- }, this.config.watch.debounce);
1232
- }
1233
- }
1234
- })();
1235
- }
1236
- catch (error) {
1237
- console.warn(`Warning: Could not watch directory ${dir}:`, error.message);
1238
- }
1239
- }
1575
+ try {
1576
+ const availablePort = await findAvailablePort(this.port);
1577
+ if (availablePort !== this.port) {
1578
+ console.log(`
1579
+ \u26A0\uFE0F Port ${this.port} is in use, using port ${availablePort} instead`);
1580
+ }
1581
+ this.port = availablePort;
1582
+ } catch (error) {
1583
+ console.error(`
1584
+ \u274C Failed to find available port: ${error.message}`);
1585
+ process.exit(1);
1240
1586
  }
1241
- stop() {
1242
- this.server.close();
1243
- console.log('\n👋 Server stopped');
1587
+ this.server.listen(this.port, () => {
1588
+ console.log(`
1589
+ \u{1F310} Documentation server running at http://localhost:${this.port}`);
1590
+ if (this.envResult?.hasRails && this.envResult?.hasNextjs) {
1591
+ console.log(" \u{1F4CA} Multiple environments detected - use tabs to switch views");
1592
+ }
1593
+ console.log(" Press Ctrl+C to stop\n");
1594
+ });
1595
+ if (openBrowser) {
1596
+ const open = (await import('open')).default;
1597
+ await open(`http://localhost:${this.port}`);
1244
1598
  }
1245
- }
1599
+ if (this.config.watch.enabled) {
1600
+ this.watchForChanges();
1601
+ }
1602
+ }
1603
+ async regenerate() {
1604
+ console.log("\n\u{1F504} Regenerating documentation...");
1605
+ this.currentReport = await this.engine.generate();
1606
+ if (this.envResult?.hasRails) {
1607
+ const rootPath = this.config.repositories[0]?.path || process.cwd();
1608
+ try {
1609
+ this.railsAnalysis = await analyzeRailsApp(rootPath);
1610
+ } catch (error) {
1611
+ console.error(`\u26A0\uFE0F Rails re-analysis failed:`, error.message);
1612
+ }
1613
+ }
1614
+ this.io.emit("reload");
1615
+ console.log("\u2705 Documentation regenerated");
1616
+ }
1617
+ async watchForChanges() {
1618
+ const watchDirs = this.config.repositories.map((r) => r.path);
1619
+ let timeout = null;
1620
+ for (const dir of watchDirs) {
1621
+ try {
1622
+ const watcher = fs.watch(dir, { recursive: true });
1623
+ (async () => {
1624
+ for await (const event of watcher) {
1625
+ if (event.filename && (event.filename.endsWith(".ts") || event.filename.endsWith(".tsx"))) {
1626
+ if (timeout) clearTimeout(timeout);
1627
+ timeout = setTimeout(async () => {
1628
+ await this.regenerate();
1629
+ }, this.config.watch.debounce);
1630
+ }
1631
+ }
1632
+ })();
1633
+ } catch (error) {
1634
+ console.warn(`Warning: Could not watch directory ${dir}:`, error.message);
1635
+ }
1636
+ }
1637
+ }
1638
+ stop() {
1639
+ this.server.close();
1640
+ console.log("\n\u{1F44B} Server stopped");
1641
+ }
1642
+ };
1643
+
1644
+ export { DocGeneratorEngine, DocServer };