codmir 0.3.2 → 0.4.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.
@@ -0,0 +1,756 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-EBO3CZXG.mjs";
4
+
5
+ // src/cli/commands/analyze.ts
6
+ import chalk from "chalk";
7
+ import ora from "ora";
8
+ import prompts from "prompts";
9
+
10
+ // src/cli/utils/config.ts
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import os from "os";
14
+ var CONFIG_DIR = path.join(os.homedir(), ".codmir");
15
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
16
+ var PROJECT_CONFIG_FILE = ".codmir.json";
17
+ function ensureConfigDir() {
18
+ if (!fs.existsSync(CONFIG_DIR)) {
19
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
20
+ }
21
+ }
22
+ function readConfig() {
23
+ ensureConfigDir();
24
+ if (!fs.existsSync(CONFIG_FILE)) {
25
+ return {};
26
+ }
27
+ try {
28
+ const data = fs.readFileSync(CONFIG_FILE, "utf-8");
29
+ return JSON.parse(data);
30
+ } catch (error) {
31
+ console.error("Error reading config:", error);
32
+ return {};
33
+ }
34
+ }
35
+ function writeConfig(config) {
36
+ ensureConfigDir();
37
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
38
+ }
39
+ function clearConfig() {
40
+ if (fs.existsSync(CONFIG_FILE)) {
41
+ fs.unlinkSync(CONFIG_FILE);
42
+ }
43
+ }
44
+ function readProjectConfig(cwd = process.cwd()) {
45
+ const configPath = path.join(cwd, PROJECT_CONFIG_FILE);
46
+ if (!fs.existsSync(configPath)) {
47
+ return null;
48
+ }
49
+ try {
50
+ const data = fs.readFileSync(configPath, "utf-8");
51
+ return JSON.parse(data);
52
+ } catch (error) {
53
+ console.error("Error reading project config:", error);
54
+ return null;
55
+ }
56
+ }
57
+ function writeProjectConfig(config, cwd = process.cwd()) {
58
+ const configPath = path.join(cwd, PROJECT_CONFIG_FILE);
59
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
60
+ const gitignorePath = path.join(cwd, ".gitignore");
61
+ if (fs.existsSync(gitignorePath)) {
62
+ const gitignore = fs.readFileSync(gitignorePath, "utf-8");
63
+ if (!gitignore.includes(PROJECT_CONFIG_FILE)) {
64
+ fs.appendFileSync(gitignorePath, `
65
+ # codmir
66
+ ${PROJECT_CONFIG_FILE}
67
+ `);
68
+ }
69
+ }
70
+ }
71
+ function getToken() {
72
+ const config = readConfig();
73
+ return config.token || null;
74
+ }
75
+ function isAuthenticated() {
76
+ const token = getToken();
77
+ return !!token;
78
+ }
79
+ function getBaseUrl() {
80
+ if (process.env.CODMIR_API_URL) {
81
+ return process.env.CODMIR_API_URL.replace(/\/$/, "");
82
+ }
83
+ const config = readConfig();
84
+ if (config.baseUrl) {
85
+ return config.baseUrl.replace(/\/$/, "");
86
+ }
87
+ return "https://codmir.com";
88
+ }
89
+ function getProjectConfig() {
90
+ return readProjectConfig();
91
+ }
92
+ function getExecutionContext() {
93
+ const projectConfig = readProjectConfig();
94
+ const isLinkedProject = projectConfig !== null;
95
+ return {
96
+ mode: isLinkedProject ? "local" : "global",
97
+ isLinkedProject,
98
+ projectConfig: projectConfig || void 0,
99
+ workingDirectory: process.cwd()
100
+ };
101
+ }
102
+
103
+ // src/cli/utils/codebase-analyzer/local.ts
104
+ import fs2 from "fs";
105
+ import path2 from "path";
106
+ import { glob } from "glob";
107
+ var IGNORE_PATTERNS = [
108
+ "**/node_modules/**",
109
+ "**/.git/**",
110
+ "**/dist/**",
111
+ "**/build/**",
112
+ "**/.next/**",
113
+ "**/coverage/**",
114
+ "**/.cache/**",
115
+ "**/tmp/**",
116
+ "**/.turbo/**",
117
+ "**/.vercel/**"
118
+ ];
119
+ var LANGUAGE_EXTENSIONS = {
120
+ ".ts": "TypeScript",
121
+ ".tsx": "TypeScript",
122
+ ".js": "JavaScript",
123
+ ".jsx": "JavaScript",
124
+ ".py": "Python",
125
+ ".rb": "Ruby",
126
+ ".go": "Go",
127
+ ".rs": "Rust",
128
+ ".java": "Java",
129
+ ".kt": "Kotlin",
130
+ ".swift": "Swift",
131
+ ".php": "PHP",
132
+ ".cs": "C#",
133
+ ".cpp": "C++",
134
+ ".c": "C",
135
+ ".css": "CSS",
136
+ ".scss": "SCSS",
137
+ ".sass": "Sass",
138
+ ".html": "HTML",
139
+ ".vue": "Vue",
140
+ ".svelte": "Svelte",
141
+ ".sql": "SQL",
142
+ ".sh": "Shell",
143
+ ".yaml": "YAML",
144
+ ".yml": "YAML",
145
+ ".json": "JSON",
146
+ ".md": "Markdown"
147
+ };
148
+ async function analyzeLocal(projectPath) {
149
+ const startTime = Date.now();
150
+ const [structure, techStack, entryPoints, documentation, metadata] = await Promise.all([
151
+ buildFileTree(projectPath),
152
+ detectTechStack(projectPath),
153
+ findEntryPoints(projectPath),
154
+ extractDocumentation(projectPath),
155
+ collectMetadata(projectPath)
156
+ ]);
157
+ const analysis = {
158
+ structure,
159
+ techStack,
160
+ entryPoints,
161
+ documentation,
162
+ metadata,
163
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
164
+ };
165
+ const duration = Date.now() - startTime;
166
+ console.log(`Analysis complete in ${duration}ms`);
167
+ return analysis;
168
+ }
169
+ async function buildFileTree(projectPath) {
170
+ const stats = fs2.statSync(projectPath);
171
+ const name = path2.basename(projectPath);
172
+ if (!stats.isDirectory()) {
173
+ return {
174
+ name,
175
+ path: projectPath,
176
+ type: "file",
177
+ size: stats.size,
178
+ extension: path2.extname(projectPath)
179
+ };
180
+ }
181
+ const children = [];
182
+ const entries = fs2.readdirSync(projectPath);
183
+ for (const entry of entries) {
184
+ if (shouldIgnore(entry)) continue;
185
+ const fullPath = path2.join(projectPath, entry);
186
+ try {
187
+ const childNode = await buildFileTree(fullPath);
188
+ children.push(childNode);
189
+ } catch (error) {
190
+ continue;
191
+ }
192
+ }
193
+ return {
194
+ name,
195
+ path: projectPath,
196
+ type: "directory",
197
+ children: children.sort((a, b) => {
198
+ if (a.type !== b.type) {
199
+ return a.type === "directory" ? -1 : 1;
200
+ }
201
+ return a.name.localeCompare(b.name);
202
+ })
203
+ };
204
+ }
205
+ async function detectTechStack(projectPath) {
206
+ const techStack = {
207
+ languages: {},
208
+ frameworks: [],
209
+ libraries: [],
210
+ tools: [],
211
+ packageManager: null
212
+ };
213
+ if (fs2.existsSync(path2.join(projectPath, "pnpm-lock.yaml"))) {
214
+ techStack.packageManager = "pnpm";
215
+ } else if (fs2.existsSync(path2.join(projectPath, "yarn.lock"))) {
216
+ techStack.packageManager = "yarn";
217
+ } else if (fs2.existsSync(path2.join(projectPath, "package-lock.json"))) {
218
+ techStack.packageManager = "npm";
219
+ } else if (fs2.existsSync(path2.join(projectPath, "bun.lockb"))) {
220
+ techStack.packageManager = "bun";
221
+ }
222
+ const files = await glob("**/*", {
223
+ cwd: projectPath,
224
+ ignore: IGNORE_PATTERNS,
225
+ nodir: true
226
+ });
227
+ const languageCounts = {};
228
+ let totalCodeFiles = 0;
229
+ for (const file of files) {
230
+ const ext = path2.extname(file);
231
+ const language = LANGUAGE_EXTENSIONS[ext];
232
+ if (language) {
233
+ languageCounts[language] = (languageCounts[language] || 0) + 1;
234
+ totalCodeFiles++;
235
+ }
236
+ }
237
+ for (const [language, count] of Object.entries(languageCounts)) {
238
+ techStack.languages[language] = parseFloat(
239
+ (count / totalCodeFiles * 100).toFixed(1)
240
+ );
241
+ }
242
+ const packageJsonPath = path2.join(projectPath, "package.json");
243
+ if (fs2.existsSync(packageJsonPath)) {
244
+ const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
245
+ const dependencies = {
246
+ ...packageJson.dependencies,
247
+ ...packageJson.devDependencies
248
+ };
249
+ if (dependencies["next"]) techStack.frameworks.push("Next.js");
250
+ if (dependencies["react"]) techStack.frameworks.push("React");
251
+ if (dependencies["vue"]) techStack.frameworks.push("Vue");
252
+ if (dependencies["svelte"]) techStack.frameworks.push("Svelte");
253
+ if (dependencies["express"]) techStack.frameworks.push("Express");
254
+ if (dependencies["fastify"]) techStack.frameworks.push("Fastify");
255
+ if (dependencies["nestjs"]) techStack.frameworks.push("NestJS");
256
+ if (dependencies["@remix-run/react"]) techStack.frameworks.push("Remix");
257
+ if (dependencies["gatsby"]) techStack.frameworks.push("Gatsby");
258
+ if (dependencies["nuxt"]) techStack.frameworks.push("Nuxt");
259
+ if (dependencies["prisma"]) techStack.libraries.push("Prisma");
260
+ if (dependencies["@tanstack/react-query"]) techStack.libraries.push("TanStack Query");
261
+ if (dependencies["tailwindcss"]) techStack.libraries.push("Tailwind CSS");
262
+ if (dependencies["shadcn/ui"]) techStack.libraries.push("shadcn/ui");
263
+ if (dependencies["zod"]) techStack.libraries.push("Zod");
264
+ if (dependencies["react-hook-form"]) techStack.libraries.push("React Hook Form");
265
+ if (dependencies["typescript"]) techStack.tools.push("TypeScript");
266
+ if (dependencies["eslint"]) techStack.tools.push("ESLint");
267
+ if (dependencies["prettier"]) techStack.tools.push("Prettier");
268
+ if (dependencies["jest"]) techStack.tools.push("Jest");
269
+ if (dependencies["vitest"]) techStack.tools.push("Vitest");
270
+ if (dependencies["playwright"]) techStack.tools.push("Playwright");
271
+ }
272
+ return techStack;
273
+ }
274
+ async function findEntryPoints(projectPath) {
275
+ const entryPoints = [];
276
+ const packageJsonPath = path2.join(projectPath, "package.json");
277
+ if (fs2.existsSync(packageJsonPath)) {
278
+ const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
279
+ if (packageJson.scripts) {
280
+ for (const [name, script] of Object.entries(packageJson.scripts)) {
281
+ entryPoints.push({
282
+ type: "script",
283
+ name,
284
+ path: "package.json",
285
+ description: script
286
+ });
287
+ }
288
+ }
289
+ if (packageJson.main) {
290
+ entryPoints.push({
291
+ type: "main",
292
+ name: "main",
293
+ path: packageJson.main,
294
+ description: "Main entry point"
295
+ });
296
+ }
297
+ }
298
+ const commonEntries = [
299
+ "src/index.ts",
300
+ "src/index.js",
301
+ "src/main.ts",
302
+ "src/main.js",
303
+ "src/app/page.tsx",
304
+ "src/app/layout.tsx",
305
+ "app/page.tsx",
306
+ "app/layout.tsx",
307
+ "pages/index.tsx",
308
+ "pages/index.js",
309
+ "pages/_app.tsx",
310
+ "pages/_app.js"
311
+ ];
312
+ for (const entry of commonEntries) {
313
+ const fullPath = path2.join(projectPath, entry);
314
+ if (fs2.existsSync(fullPath)) {
315
+ entryPoints.push({
316
+ type: entry.includes("page") ? "page" : "main",
317
+ name: path2.basename(entry),
318
+ path: entry,
319
+ description: getEntryDescription(entry)
320
+ });
321
+ }
322
+ }
323
+ return entryPoints;
324
+ }
325
+ async function extractDocumentation(projectPath) {
326
+ const documentation = [];
327
+ const docFiles = [
328
+ { pattern: "README.md", type: "readme" },
329
+ { pattern: "CHANGELOG.md", type: "changelog" },
330
+ { pattern: "CONTRIBUTING.md", type: "contributing" },
331
+ { pattern: "LICENSE", type: "license" },
332
+ { pattern: "LICENSE.md", type: "license" },
333
+ { pattern: "docs/**/*.md", type: "other" }
334
+ ];
335
+ for (const { pattern, type } of docFiles) {
336
+ const files = await glob(pattern, {
337
+ cwd: projectPath,
338
+ nodir: true
339
+ });
340
+ for (const file of files) {
341
+ const fullPath = path2.join(projectPath, file);
342
+ const content = fs2.readFileSync(fullPath, "utf-8");
343
+ documentation.push({
344
+ type,
345
+ path: file,
346
+ title: extractTitle(content),
347
+ content: content.substring(0, 1e3)
348
+ // First 1000 chars
349
+ });
350
+ }
351
+ }
352
+ return documentation;
353
+ }
354
+ async function collectMetadata(projectPath) {
355
+ const metadata = {
356
+ name: path2.basename(projectPath),
357
+ totalFiles: 0,
358
+ totalLines: 0
359
+ };
360
+ const packageJsonPath = path2.join(projectPath, "package.json");
361
+ if (fs2.existsSync(packageJsonPath)) {
362
+ const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
363
+ metadata.name = packageJson.name || metadata.name;
364
+ metadata.version = packageJson.version;
365
+ metadata.description = packageJson.description;
366
+ metadata.author = packageJson.author;
367
+ metadata.license = packageJson.license;
368
+ metadata.scripts = packageJson.scripts;
369
+ if (packageJson.repository) {
370
+ metadata.repository = typeof packageJson.repository === "string" ? packageJson.repository : packageJson.repository.url;
371
+ }
372
+ }
373
+ const files = await glob("**/*", {
374
+ cwd: projectPath,
375
+ ignore: IGNORE_PATTERNS,
376
+ nodir: true
377
+ });
378
+ metadata.totalFiles = files.length;
379
+ for (const file of files) {
380
+ const ext = path2.extname(file);
381
+ if (LANGUAGE_EXTENSIONS[ext]) {
382
+ const fullPath = path2.join(projectPath, file);
383
+ const content = fs2.readFileSync(fullPath, "utf-8");
384
+ metadata.totalLines += content.split("\n").length;
385
+ }
386
+ }
387
+ const gitPath = path2.join(projectPath, ".git");
388
+ if (fs2.existsSync(gitPath)) {
389
+ try {
390
+ const headPath = path2.join(gitPath, "HEAD");
391
+ const head = fs2.readFileSync(headPath, "utf-8").trim();
392
+ metadata.gitBranch = head.replace("ref: refs/heads/", "");
393
+ } catch {
394
+ }
395
+ }
396
+ return metadata;
397
+ }
398
+ function shouldIgnore(name) {
399
+ const ignoreList = [
400
+ "node_modules",
401
+ ".git",
402
+ "dist",
403
+ "build",
404
+ ".next",
405
+ "coverage",
406
+ ".cache",
407
+ "tmp",
408
+ ".turbo",
409
+ ".vercel",
410
+ ".DS_Store"
411
+ ];
412
+ return ignoreList.includes(name) || name.startsWith(".");
413
+ }
414
+ function getEntryDescription(entryPath) {
415
+ if (entryPath.includes("layout")) return "Root layout component";
416
+ if (entryPath.includes("page")) return "Page component";
417
+ if (entryPath.includes("_app")) return "App component";
418
+ if (entryPath.includes("index")) return "Main entry point";
419
+ return "Entry file";
420
+ }
421
+ function extractTitle(content) {
422
+ const lines = content.split("\n");
423
+ for (const line of lines) {
424
+ if (line.startsWith("# ")) {
425
+ return line.substring(2).trim();
426
+ }
427
+ }
428
+ return void 0;
429
+ }
430
+
431
+ // src/cli/utils/knowledge-base/storage.ts
432
+ import fs3 from "fs";
433
+ import path3 from "path";
434
+ var KNOWLEDGE_BASE_DIR = ".codmir/knowledge";
435
+ var INDEX_FILE = "index.json";
436
+ function getKnowledgeBaseDir(projectPath) {
437
+ return path3.join(projectPath, KNOWLEDGE_BASE_DIR);
438
+ }
439
+ function ensureKnowledgeBaseDir(projectPath) {
440
+ const kbDir = getKnowledgeBaseDir(projectPath);
441
+ if (!fs3.existsSync(kbDir)) {
442
+ fs3.mkdirSync(kbDir, { recursive: true });
443
+ }
444
+ }
445
+ function saveKnowledgeBase(projectPath, knowledgeBase) {
446
+ ensureKnowledgeBaseDir(projectPath);
447
+ const kbDir = getKnowledgeBaseDir(projectPath);
448
+ const indexPath = path3.join(kbDir, INDEX_FILE);
449
+ fs3.writeFileSync(indexPath, JSON.stringify(knowledgeBase, null, 2));
450
+ if (knowledgeBase.localAnalysis) {
451
+ saveComponent(kbDir, "local-analysis.json", knowledgeBase.localAnalysis);
452
+ }
453
+ }
454
+ function loadKnowledgeBase(projectPath) {
455
+ const indexPath = path3.join(projectPath, KNOWLEDGE_BASE_DIR, INDEX_FILE);
456
+ if (!fs3.existsSync(indexPath)) {
457
+ return null;
458
+ }
459
+ try {
460
+ const content = fs3.readFileSync(indexPath, "utf-8");
461
+ const knowledgeBase = JSON.parse(content);
462
+ const kbDir = getKnowledgeBaseDir(projectPath);
463
+ if (knowledgeBase.analysisType === "local") {
464
+ const localAnalysisPath = path3.join(kbDir, "local-analysis.json");
465
+ if (fs3.existsSync(localAnalysisPath)) {
466
+ knowledgeBase.localAnalysis = JSON.parse(
467
+ fs3.readFileSync(localAnalysisPath, "utf-8")
468
+ );
469
+ }
470
+ }
471
+ return knowledgeBase;
472
+ } catch (error) {
473
+ console.error("Error loading knowledge base:", error);
474
+ return null;
475
+ }
476
+ }
477
+ function hasKnowledgeBase(projectPath) {
478
+ const indexPath = path3.join(projectPath, KNOWLEDGE_BASE_DIR, INDEX_FILE);
479
+ return fs3.existsSync(indexPath);
480
+ }
481
+ function getKnowledgeBaseStats(projectPath) {
482
+ const kb = loadKnowledgeBase(projectPath);
483
+ if (!kb) {
484
+ return null;
485
+ }
486
+ return {
487
+ version: kb.version,
488
+ analyzedAt: kb.analyzedAt,
489
+ analysisType: kb.analysisType,
490
+ totalFiles: kb.localAnalysis?.metadata.totalFiles || 0,
491
+ totalLines: kb.localAnalysis?.metadata.totalLines || 0,
492
+ languages: kb.localAnalysis?.techStack.languages || {},
493
+ frameworks: kb.localAnalysis?.techStack.frameworks || []
494
+ };
495
+ }
496
+ function saveComponent(kbDir, filename, data) {
497
+ const filePath = path3.join(kbDir, filename);
498
+ fs3.writeFileSync(filePath, JSON.stringify(data, null, 2));
499
+ }
500
+ function initializeKnowledgeBase(projectPath, projectId, projectName, localAnalysis) {
501
+ const knowledgeBase = {
502
+ version: "1.0.0",
503
+ projectId,
504
+ projectName,
505
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString(),
506
+ analysisType: "local",
507
+ localAnalysis,
508
+ queries: {}
509
+ };
510
+ saveKnowledgeBase(projectPath, knowledgeBase);
511
+ return knowledgeBase;
512
+ }
513
+
514
+ // src/cli/commands/analyze.ts
515
+ async function analyzeCommand(options = {}) {
516
+ console.log(chalk.bold("\n\u{1F50D} Codebase Analysis\n"));
517
+ const projectConfig = getProjectConfig();
518
+ if (!projectConfig) {
519
+ console.error(chalk.red("\u274C Project not linked"));
520
+ console.log(chalk.dim(" Run"), chalk.cyan("codmir link"), chalk.dim("first"));
521
+ process.exit(1);
522
+ }
523
+ const projectPath = process.cwd();
524
+ const existingKB = hasKnowledgeBase(projectPath);
525
+ if (existingKB && !options.force) {
526
+ const stats = getKnowledgeBaseStats(projectPath);
527
+ console.log(chalk.yellow("\u26A0\uFE0F Knowledge base already exists"));
528
+ console.log(chalk.dim(` Last analyzed: ${new Date(stats.analyzedAt).toLocaleString()}`));
529
+ console.log(chalk.dim(` Files: ${stats.totalFiles}, Lines: ${stats.totalLines}`));
530
+ const { reanalyze } = await prompts({
531
+ type: "confirm",
532
+ name: "reanalyze",
533
+ message: "Re-analyze codebase?",
534
+ initial: false
535
+ });
536
+ if (!reanalyze) {
537
+ console.log(chalk.dim("\n Use"), chalk.cyan("--force"), chalk.dim("to force re-analysis"));
538
+ return;
539
+ }
540
+ }
541
+ let analysisMode = options.mode || "local";
542
+ if (!options.mode) {
543
+ const { mode } = await prompts({
544
+ type: "select",
545
+ name: "mode",
546
+ message: "Choose analysis mode",
547
+ choices: [
548
+ {
549
+ title: "\u{1F4BB} Local (Quick)",
550
+ value: "local",
551
+ description: "Fast structural analysis (1-5 seconds)"
552
+ },
553
+ {
554
+ title: "\u{1F682} Railway (Deep)",
555
+ value: "railway",
556
+ description: "Comprehensive analysis with embeddings (~2 minutes)"
557
+ }
558
+ ],
559
+ initial: 0
560
+ });
561
+ analysisMode = mode;
562
+ }
563
+ if (analysisMode === "local") {
564
+ await runLocalAnalysis(projectPath, projectConfig.projectId, projectConfig.projectName);
565
+ } else {
566
+ await runRailwayAnalysis(projectPath, projectConfig.projectId);
567
+ }
568
+ }
569
+ async function runLocalAnalysis(projectPath, projectId, projectName) {
570
+ const spinner = ora({
571
+ text: chalk.dim("Analyzing project structure..."),
572
+ color: "cyan"
573
+ }).start();
574
+ try {
575
+ const analysis = await analyzeLocal(projectPath);
576
+ spinner.text = chalk.dim("Saving knowledge base...");
577
+ initializeKnowledgeBase(projectPath, projectId, projectName, analysis);
578
+ spinner.succeed(chalk.green("Analysis complete!"));
579
+ console.log();
580
+ console.log(chalk.bold("\u{1F4CA} Results:"));
581
+ console.log();
582
+ console.log(chalk.dim(" Project:"), chalk.cyan(projectName));
583
+ console.log(chalk.dim(" Files:"), chalk.white(analysis.metadata.totalFiles));
584
+ console.log(chalk.dim(" Lines:"), chalk.white(analysis.metadata.totalLines));
585
+ console.log();
586
+ if (Object.keys(analysis.techStack.languages).length > 0) {
587
+ console.log(chalk.bold("\u{1F5E3}\uFE0F Languages:"));
588
+ for (const [lang, percent] of Object.entries(analysis.techStack.languages)) {
589
+ const bar = createProgressBar(percent, 20);
590
+ console.log(` ${chalk.cyan(lang.padEnd(15))} ${bar} ${percent}%`);
591
+ }
592
+ console.log();
593
+ }
594
+ if (analysis.techStack.frameworks.length > 0) {
595
+ console.log(chalk.bold("\u26A1 Frameworks:"));
596
+ analysis.techStack.frameworks.forEach((fw) => {
597
+ console.log(` \u2022 ${chalk.green(fw)}`);
598
+ });
599
+ console.log();
600
+ }
601
+ if (analysis.entryPoints.length > 0) {
602
+ console.log(chalk.bold("\u{1F680} Entry Points:"));
603
+ analysis.entryPoints.slice(0, 5).forEach((ep) => {
604
+ console.log(` \u2022 ${chalk.yellow(ep.name)} ${chalk.dim(`(${ep.path})`)}`);
605
+ });
606
+ if (analysis.entryPoints.length > 5) {
607
+ console.log(chalk.dim(` ... and ${analysis.entryPoints.length - 5} more`));
608
+ }
609
+ console.log();
610
+ }
611
+ if (analysis.documentation.length > 0) {
612
+ console.log(chalk.bold("\u{1F4DA} Documentation:"));
613
+ analysis.documentation.forEach((doc) => {
614
+ const icon = getDocIcon(doc.type);
615
+ console.log(` ${icon} ${chalk.cyan(doc.path)}`);
616
+ });
617
+ console.log();
618
+ }
619
+ console.log(chalk.dim(" Knowledge base saved to:"), chalk.cyan(".codmir/knowledge/"));
620
+ console.log();
621
+ console.log(chalk.dim(" Try:"), chalk.cyan('codmir ai --context "Explain the project structure"'));
622
+ console.log();
623
+ } catch (error) {
624
+ spinner.fail(chalk.red("Analysis failed"));
625
+ console.error(error);
626
+ process.exit(1);
627
+ }
628
+ }
629
+ async function runRailwayAnalysis(projectPath, projectId) {
630
+ const token = getToken();
631
+ if (!token) {
632
+ console.error(chalk.red("\u274C Not authenticated"));
633
+ console.log(chalk.dim(" Run"), chalk.cyan("codmir login"), chalk.dim("first"));
634
+ process.exit(1);
635
+ }
636
+ const baseUrl = getBaseUrl();
637
+ const gitRemote = await getGitRemote(projectPath);
638
+ if (!gitRemote) {
639
+ console.error(chalk.red("\u274C No git remote found"));
640
+ console.log(chalk.dim(" Project must be a git repository with remote"));
641
+ process.exit(1);
642
+ }
643
+ console.log(chalk.dim(" Repository:"), chalk.cyan(gitRemote));
644
+ console.log();
645
+ const spinner = ora({
646
+ text: chalk.dim("Starting Railway analysis..."),
647
+ color: "cyan"
648
+ }).start();
649
+ try {
650
+ const response = await fetch(`${baseUrl}/api/analysis/deep`, {
651
+ method: "POST",
652
+ headers: {
653
+ "Content-Type": "application/json",
654
+ "X-API-Key": token
655
+ },
656
+ body: JSON.stringify({
657
+ projectId,
658
+ repoUrl: gitRemote,
659
+ branch: await getGitBranch(projectPath)
660
+ })
661
+ });
662
+ if (!response.ok) {
663
+ throw new Error(`API error: ${response.status}`);
664
+ }
665
+ const { jobId, estimatedTime } = await response.json();
666
+ spinner.text = chalk.dim(`Analysis in progress (${estimatedTime})...`);
667
+ await pollAnalysisStatus(baseUrl, token, jobId, spinner);
668
+ spinner.succeed(chalk.green("Deep analysis complete!"));
669
+ console.log();
670
+ console.log(chalk.dim(" Knowledge base synced from Railway"));
671
+ console.log(chalk.dim(" Includes:"));
672
+ console.log(chalk.dim(" \u2022 Full AST parsing"));
673
+ console.log(chalk.dim(" \u2022 Code embeddings"));
674
+ console.log(chalk.dim(" \u2022 Best practices report"));
675
+ console.log(chalk.dim(" \u2022 Generated documentation"));
676
+ console.log();
677
+ } catch (error) {
678
+ spinner.fail(chalk.red("Railway analysis failed"));
679
+ console.error(error);
680
+ process.exit(1);
681
+ }
682
+ }
683
+ async function pollAnalysisStatus(baseUrl, token, jobId, spinner) {
684
+ const maxAttempts = 120;
685
+ let attempts = 0;
686
+ while (attempts < maxAttempts) {
687
+ await new Promise((resolve) => setTimeout(resolve, 5e3));
688
+ const response = await fetch(`${baseUrl}/api/analysis/deep/${jobId}`, {
689
+ headers: { "X-API-Key": token }
690
+ });
691
+ const { status, progress } = await response.json();
692
+ spinner.text = chalk.dim(`${status}... ${progress || ""}%`);
693
+ if (status === "complete") {
694
+ return;
695
+ }
696
+ if (status === "failed") {
697
+ throw new Error("Analysis failed on Railway");
698
+ }
699
+ attempts++;
700
+ }
701
+ throw new Error("Analysis timeout");
702
+ }
703
+ async function getGitRemote(projectPath) {
704
+ try {
705
+ const { execSync } = __require("child_process");
706
+ const remote = execSync("git config --get remote.origin.url", {
707
+ cwd: projectPath,
708
+ encoding: "utf-8"
709
+ }).trim();
710
+ return remote;
711
+ } catch {
712
+ return null;
713
+ }
714
+ }
715
+ async function getGitBranch(projectPath) {
716
+ try {
717
+ const { execSync } = __require("child_process");
718
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
719
+ cwd: projectPath,
720
+ encoding: "utf-8"
721
+ }).trim();
722
+ return branch;
723
+ } catch {
724
+ return "main";
725
+ }
726
+ }
727
+ function createProgressBar(percent, width) {
728
+ const filled = Math.round(percent / 100 * width);
729
+ const empty = width - filled;
730
+ return chalk.cyan("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty));
731
+ }
732
+ function getDocIcon(type) {
733
+ const icons = {
734
+ readme: "\u{1F4D6}",
735
+ changelog: "\u{1F4DD}",
736
+ contributing: "\u{1F91D}",
737
+ license: "\u2696\uFE0F",
738
+ other: "\u{1F4C4}"
739
+ };
740
+ return icons[type] || "\u{1F4C4}";
741
+ }
742
+
743
+ export {
744
+ ensureConfigDir,
745
+ readConfig,
746
+ writeConfig,
747
+ clearConfig,
748
+ readProjectConfig,
749
+ writeProjectConfig,
750
+ getToken,
751
+ isAuthenticated,
752
+ getBaseUrl,
753
+ getProjectConfig,
754
+ getExecutionContext,
755
+ analyzeCommand
756
+ };