@mrpatronz/nexusflow 0.1.11 → 0.2.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.
Files changed (80) hide show
  1. package/dist/analyzers/detect-apis.d.ts.map +1 -1
  2. package/dist/analyzers/detect-apis.js +168 -39
  3. package/dist/analyzers/detect-apis.js.map +1 -1
  4. package/dist/analyzers/detect-deps.d.ts +26 -4
  5. package/dist/analyzers/detect-deps.d.ts.map +1 -1
  6. package/dist/analyzers/detect-deps.js +228 -66
  7. package/dist/analyzers/detect-deps.js.map +1 -1
  8. package/dist/analyzers/index.d.ts +1 -1
  9. package/dist/analyzers/index.d.ts.map +1 -1
  10. package/dist/analyzers/index.js +7 -3
  11. package/dist/analyzers/index.js.map +1 -1
  12. package/dist/analyzers/readme-summarizer.d.ts +3 -1
  13. package/dist/analyzers/readme-summarizer.d.ts.map +1 -1
  14. package/dist/analyzers/readme-summarizer.js +43 -19
  15. package/dist/analyzers/readme-summarizer.js.map +1 -1
  16. package/dist/analyzers/tech-stack.d.ts +2 -2
  17. package/dist/analyzers/tech-stack.d.ts.map +1 -1
  18. package/dist/analyzers/tech-stack.js +257 -232
  19. package/dist/analyzers/tech-stack.js.map +1 -1
  20. package/dist/commands/create.d.ts.map +1 -1
  21. package/dist/commands/create.js +15 -8
  22. package/dist/commands/create.js.map +1 -1
  23. package/dist/commands/sync.d.ts.map +1 -1
  24. package/dist/commands/sync.js +31 -0
  25. package/dist/commands/sync.js.map +1 -1
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +25 -0
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/graph.d.ts.map +1 -1
  30. package/dist/core/graph.js +1 -2
  31. package/dist/core/graph.js.map +1 -1
  32. package/dist/core/packer.d.ts +2 -1
  33. package/dist/core/packer.d.ts.map +1 -1
  34. package/dist/core/packer.js +36 -30
  35. package/dist/core/packer.js.map +1 -1
  36. package/dist/core/packer.test.js +4 -5
  37. package/dist/core/packer.test.js.map +1 -1
  38. package/dist/core/workspace.d.ts.map +1 -1
  39. package/dist/core/workspace.js +22 -12
  40. package/dist/core/workspace.js.map +1 -1
  41. package/dist/generators/base.d.ts.map +1 -1
  42. package/dist/generators/base.js +64 -31
  43. package/dist/generators/base.js.map +1 -1
  44. package/dist/generators/index.d.ts.map +1 -1
  45. package/dist/generators/index.js +17 -0
  46. package/dist/generators/index.js.map +1 -1
  47. package/dist/generators/map-generator.d.ts +15 -0
  48. package/dist/generators/map-generator.d.ts.map +1 -0
  49. package/dist/generators/map-generator.js +385 -0
  50. package/dist/generators/map-generator.js.map +1 -0
  51. package/dist/generators/map-generator.test.d.ts +2 -0
  52. package/dist/generators/map-generator.test.d.ts.map +1 -0
  53. package/dist/generators/map-generator.test.js +74 -0
  54. package/dist/generators/map-generator.test.js.map +1 -0
  55. package/dist/generators/plan-generator.d.ts +2 -0
  56. package/dist/generators/plan-generator.d.ts.map +1 -1
  57. package/dist/generators/plan-generator.js +104 -8
  58. package/dist/generators/plan-generator.js.map +1 -1
  59. package/dist/types.d.ts +18 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/package.json +2 -1
  62. package/src/analyzers/detect-apis.ts +192 -40
  63. package/src/analyzers/detect-deps.ts +246 -69
  64. package/src/analyzers/index.ts +7 -3
  65. package/src/analyzers/readme-summarizer.ts +48 -19
  66. package/src/analyzers/tech-stack.ts +222 -194
  67. package/src/commands/create.ts +16 -10
  68. package/src/commands/sync.ts +35 -0
  69. package/src/core/config.ts +25 -0
  70. package/src/core/graph.ts +1 -2
  71. package/src/core/packer.test.ts +4 -6
  72. package/src/core/packer.ts +42 -43
  73. package/src/core/workspace.ts +23 -13
  74. package/src/generators/base.ts +68 -30
  75. package/src/generators/index.ts +17 -0
  76. package/src/generators/map-generator.test.ts +81 -0
  77. package/src/generators/map-generator.ts +405 -0
  78. package/src/generators/plan-generator.ts +117 -7
  79. package/src/types.ts +13 -0
  80. package/vitest.config.ts +13 -0
@@ -6,12 +6,11 @@
6
6
  import * as fs from 'node:fs/promises';
7
7
  import * as path from 'node:path';
8
8
 
9
- /** Maximum chars to extract from README for summary. */
10
- const MAX_SUMMARY_LENGTH = 800;
11
-
12
9
  /**
13
10
  * Reads a repository's README.md and extracts the first meaningful
14
- * section as a summary. Looks for README.md (case-insensitive).
11
+ * prose paragraph as a summary. Looks for README.md (case-insensitive).
12
+ *
13
+ * It automatically strips badges, TOC lists, headers, HTML tags, and redacts ClientIDs/Secrets.
15
14
  *
16
15
  * @param repoPath - Absolute path to the repository root.
17
16
  * @returns The extracted summary text, or null if no README is found.
@@ -35,34 +34,64 @@ export async function extractReadmeSummary(
35
34
 
36
35
  if (!content) return null;
37
36
 
38
- // Strip badges, images, and HTML at the top
37
+ // 1. Strip code blocks to avoid false matching on secrets/prose
38
+ content = content.replace(/```[\s\S]*?```/g, '');
39
+
40
+ // 2. Scrub ClientIDs/Secrets/Credentials
41
+ // Match standard secret/ID patterns: GUIDs, client_id/secret variables, hex keys
42
+ const clientSecretsRegex = /(client_id|client_secret|appid|secret|password|key|token|credential)\s*[:=]\s*["']?[a-zA-Z0-9-_\/\+\.]{16,}["']?/gi;
43
+ const guidRegex = /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/g;
44
+
45
+ content = content
46
+ .replace(clientSecretsRegex, (match, p1) => `${p1}: [REDACTED]`)
47
+ .replace(guidRegex, '[REDACTED_ID]');
48
+
49
+ // 3. Process line-by-line to find the first prose paragraph
39
50
  const lines = content.split('\n');
40
- const meaningfulLines: string[] = [];
41
- let foundContent = false;
51
+ const proseLines: string[] = [];
42
52
 
43
53
  for (const line of lines) {
44
54
  const trimmed = line.trim();
45
55
 
46
- // Skip empty lines before content starts
47
- if (!foundContent && !trimmed) continue;
48
-
49
- // Skip badge lines ([![...](...)]) and image lines (![...](...)
50
- if (trimmed.startsWith('[![') || (trimmed.startsWith('![') && trimmed.includes('http'))) continue;
56
+ // Skip empty lines
57
+ if (!trimmed) {
58
+ if (proseLines.length > 0) {
59
+ // We found a paragraph and hit an empty line. Let's finish!
60
+ break;
61
+ }
62
+ continue;
63
+ }
51
64
 
52
65
  // Skip HTML tags
53
66
  if (trimmed.startsWith('<') && trimmed.endsWith('>')) continue;
54
67
 
68
+ // Skip badges
69
+ if (trimmed.startsWith('[![') || (trimmed.startsWith('![') && trimmed.includes('http'))) continue;
70
+
55
71
  // Skip horizontal rules
56
72
  if (/^(-{3,}|={3,}|\*{3,})$/.test(trimmed)) continue;
57
73
 
58
- foundContent = true;
59
- meaningfulLines.push(line);
74
+ // Skip headers (Markdown # )
75
+ if (trimmed.startsWith('#')) continue;
76
+
77
+ // Detect Table of Contents (TOC) lists
78
+ // Skip lines that look like: - [About](#about) or * 1. [Section](#section)
79
+ if (/^[-*+]\s*(\d+\.)?\s*\[[^\]]+\]\(#[^)]+\)/.test(trimmed)) {
80
+ continue;
81
+ }
82
+
83
+ // Skip standard bullet lists if we are searching for prose (e.g. at the top of README before prose)
84
+ if (proseLines.length === 0 && /^[-*+]\s+/.test(trimmed)) {
85
+ continue;
86
+ }
60
87
 
61
- // Stop after we have enough content
62
- const currentLength = meaningfulLines.join('\n').length;
63
- if (currentLength >= MAX_SUMMARY_LENGTH) break;
88
+ // Accumulate prose lines
89
+ proseLines.push(line);
64
90
  }
65
91
 
66
- const summary = meaningfulLines.join('\n').slice(0, MAX_SUMMARY_LENGTH).trim();
67
- return summary || null;
92
+ const summary = proseLines.join(' ').replace(/\s+/g, ' ').trim();
93
+ if (!summary) return null;
94
+
95
+ // Limit the summary to a concise paragraph (first 250 characters)
96
+ return summary.length > 250 ? summary.slice(0, 250) + '...' : summary;
68
97
  }
@@ -6,19 +6,10 @@
6
6
 
7
7
  import * as fs from 'node:fs/promises';
8
8
  import * as path from 'node:path';
9
+ import { globby } from 'globby';
9
10
 
10
11
  import type { TechStack, Language, Framework, ProjectType } from '../types.js';
11
12
 
12
- /** Check if a file exists in a directory. */
13
- async function fileExists(dir: string, filename: string): Promise<boolean> {
14
- try {
15
- await fs.access(path.join(dir, filename));
16
- return true;
17
- } catch {
18
- return false;
19
- }
20
- }
21
-
22
13
  /** Safely read and parse a JSON file. Returns null on failure. */
23
14
  async function readJson(filePath: string): Promise<Record<string, unknown> | null> {
24
15
  try {
@@ -29,227 +20,264 @@ async function readJson(filePath: string): Promise<Record<string, unknown> | nul
29
20
  }
30
21
  }
31
22
 
32
- /** Check if any file matching a glob-like pattern exists. */
33
- async function hasFileWithExtension(dir: string, ext: string): Promise<boolean> {
34
- try {
35
- const entries = await fs.readdir(dir);
36
- return entries.some((e) => e.endsWith(ext));
37
- } catch {
38
- return false;
39
- }
40
- }
41
-
42
23
  /**
43
- * Detects the tech stack of a repository by inspecting its root directory
44
- * for manifest files, config files, and project structures.
24
+ * Detects the tech stack of a repository by recursively inspecting its
25
+ * contents for manifest files, config files, and project structures.
45
26
  *
46
27
  * @param repoPath - Absolute path to the repository root.
47
28
  * @returns A {@link TechStack} object describing the detected stack.
48
29
  */
49
30
  export async function detectTechStack(repoPath: string): Promise<TechStack> {
50
- const languages: Language[] = [];
51
- const frameworks: Framework[] = [];
52
- const buildTools: string[] = [];
53
- let projectType: ProjectType = 'other';
54
-
55
- // ── Node.js / JavaScript / TypeScript ──────────────────────────────
56
- const hasPackageJson = await fileExists(repoPath, 'package.json');
57
- if (hasPackageJson) {
58
- const pkg = await readJson(path.join(repoPath, 'package.json'));
59
-
60
- // Detect TypeScript vs JavaScript
61
- const hasTsConfig = await fileExists(repoPath, 'tsconfig.json');
62
- if (hasTsConfig) {
63
- languages.push('typescript');
64
- } else {
65
- languages.push('javascript');
66
- }
31
+ const languages = new Set<Language>();
32
+ const frameworks = new Set<Framework>();
33
+ const buildTools = new Set<string>();
34
+ const projectTypes = new Set<ProjectType>();
67
35
 
68
- if (pkg) {
69
- const allDeps = {
70
- ...(pkg.dependencies as Record<string, string> | undefined),
71
- ...(pkg.devDependencies as Record<string, string> | undefined),
72
- };
73
-
74
- // Frameworks
75
- if (allDeps['next']) { frameworks.push('nextjs'); projectType = 'fullstack'; }
76
- if (allDeps['react'] && !allDeps['next']) { frameworks.push('react'); projectType = 'frontend'; }
77
- if (allDeps['@angular/core']) { frameworks.push('angular'); projectType = 'frontend'; }
78
- if (allDeps['vue']) { frameworks.push('vue'); projectType = 'frontend'; }
79
- if (allDeps['svelte']) { frameworks.push('svelte'); projectType = 'frontend'; }
80
- if (allDeps['express']) { frameworks.push('express'); projectType = 'backend'; }
81
- if (allDeps['@nestjs/core']) { frameworks.push('nestjs'); projectType = 'backend'; }
82
- if (allDeps['fastify']) { frameworks.push('fastify'); projectType = 'backend'; }
83
- if (allDeps['hono']) { frameworks.push('hono'); projectType = 'backend'; }
84
-
85
- // Build tools
86
- if (allDeps['vite'] || allDeps['@vitejs/plugin-react']) buildTools.push('vite');
87
- if (allDeps['webpack']) buildTools.push('webpack');
88
- if (allDeps['esbuild']) buildTools.push('esbuild');
89
- if (allDeps['rollup']) buildTools.push('rollup');
90
- if (allDeps['turbo']) buildTools.push('turborepo');
91
- if (allDeps['nx']) buildTools.push('nx');
92
-
93
- // Project type heuristics
94
- if (pkg.bin) projectType = 'cli';
95
- if (pkg.main && !pkg.bin && frameworks.length === 0) projectType = 'library';
36
+ try {
37
+ const files = await globby([
38
+ '**/package.json',
39
+ '**/tsconfig.json',
40
+ '**/*.csproj',
41
+ '**/*.sln',
42
+ '**/requirements.txt',
43
+ '**/pyproject.toml',
44
+ '**/setup.py',
45
+ '**/go.mod',
46
+ '**/pom.xml',
47
+ '**/build.gradle',
48
+ '**/build.gradle.kts',
49
+ '**/Cargo.toml',
50
+ '**/Gemfile',
51
+ '**/composer.json',
52
+ '**/Dockerfile',
53
+ '**/docker-compose.yml',
54
+ '**/docker-compose.yaml',
55
+ '**/compose.yaml',
56
+ '**/compose.yml',
57
+ ], {
58
+ cwd: repoPath,
59
+ absolute: true,
60
+ ignore: ['**/node_modules/**', '**/bin/**', '**/obj/**', '**/dist/**', '**/out/**', '**/.git/**'],
61
+ });
62
+
63
+ const fileMap = new Map<string, string[]>();
64
+ for (const file of files) {
65
+ const name = path.basename(file);
66
+ let list = fileMap.get(name);
67
+ if (!list) {
68
+ list = [];
69
+ fileMap.set(name, list);
70
+ }
71
+ list.push(file);
96
72
  }
97
- }
98
73
 
99
- // ── .NET / C# ─────────────────────────────────────────────────────
100
- const hasCsproj = await hasFileWithExtension(repoPath, '.csproj');
101
- const hasSln = await hasFileWithExtension(repoPath, '.sln');
102
- if (hasCsproj || hasSln) {
103
- languages.push('csharp');
104
-
105
- // Try to detect ASP.NET or Blazor from csproj content
106
- try {
107
- const entries = await fs.readdir(repoPath);
108
- const csprojFile = entries.find((e) => e.endsWith('.csproj'));
109
- if (csprojFile) {
110
- const content = await fs.readFile(path.join(repoPath, csprojFile), 'utf-8');
111
- if (content.includes('Microsoft.NET.Sdk.Web')) {
112
- frameworks.push('aspnet');
113
- projectType = 'backend';
114
- }
115
- if (content.includes('Microsoft.NET.Sdk.BlazorWebAssembly') || content.includes('Blazor')) {
116
- frameworks.push('blazor');
117
- projectType = 'frontend';
74
+ // ── Node.js / JavaScript / TypeScript ──────────────────────────────
75
+ const packageJsons = fileMap.get('package.json') || [];
76
+ if (packageJsons.length > 0) {
77
+ const tsconfigs = fileMap.get('tsconfig.json') || [];
78
+ if (tsconfigs.length > 0) {
79
+ languages.add('typescript');
80
+ } else {
81
+ languages.add('javascript');
82
+ }
83
+
84
+ for (const pj of packageJsons) {
85
+ const pkg = await readJson(pj);
86
+ if (pkg) {
87
+ const allDeps = {
88
+ ...(pkg.dependencies as Record<string, string> | undefined),
89
+ ...(pkg.devDependencies as Record<string, string> | undefined),
90
+ };
91
+
92
+ // Frameworks
93
+ if (allDeps['next']) { frameworks.add('nextjs'); projectTypes.add('fullstack'); }
94
+ if (allDeps['react'] && !allDeps['next']) { frameworks.add('react'); projectTypes.add('frontend'); }
95
+ if (allDeps['@angular/core']) { frameworks.add('angular'); projectTypes.add('frontend'); }
96
+ if (allDeps['vue']) { frameworks.add('vue'); projectTypes.add('frontend'); }
97
+ if (allDeps['svelte']) { frameworks.add('svelte'); projectTypes.add('frontend'); }
98
+ if (allDeps['express']) { frameworks.add('express'); projectTypes.add('backend'); }
99
+ if (allDeps['@nestjs/core']) { frameworks.add('nestjs'); projectTypes.add('backend'); }
100
+ if (allDeps['fastify']) { frameworks.add('fastify'); projectTypes.add('backend'); }
101
+ if (allDeps['hono']) { frameworks.add('hono'); projectTypes.add('backend'); }
102
+
103
+ // Build tools
104
+ if (allDeps['vite'] || allDeps['@vitejs/plugin-react']) buildTools.add('vite');
105
+ if (allDeps['webpack']) buildTools.add('webpack');
106
+ if (allDeps['esbuild']) buildTools.add('esbuild');
107
+ if (allDeps['rollup']) buildTools.add('rollup');
108
+ if (allDeps['turbo']) buildTools.add('turborepo');
109
+ if (allDeps['nx']) buildTools.add('nx');
110
+
111
+ // Project type heuristics
112
+ if (pkg.bin) projectTypes.add('cli');
113
+ if (pkg.main && !pkg.bin && frameworks.size === 0) projectTypes.add('library');
118
114
  }
119
115
  }
120
- } catch {
121
- // Ignore read errors
122
116
  }
123
117
 
124
- buildTools.push('dotnet');
125
- }
118
+ // ── .NET / C# ─────────────────────────────────────────────────────
119
+ const csprojFiles = files.filter((f) => f.endsWith('.csproj'));
120
+ const slnFiles = files.filter((f) => f.endsWith('.sln'));
121
+ if (csprojFiles.length > 0 || slnFiles.length > 0) {
122
+ languages.add('csharp');
123
+ buildTools.add('dotnet');
126
124
 
127
- // ── Python ────────────────────────────────────────────────────────
128
- const hasPyproject = await fileExists(repoPath, 'pyproject.toml');
129
- const hasRequirements = await fileExists(repoPath, 'requirements.txt');
130
- const hasSetupPy = await fileExists(repoPath, 'setup.py');
131
- if (hasPyproject || hasRequirements || hasSetupPy) {
132
- languages.push('python');
133
-
134
- // Try to detect frameworks from requirements
135
- try {
136
- let content = '';
137
- if (hasRequirements) {
138
- content = await fs.readFile(path.join(repoPath, 'requirements.txt'), 'utf-8');
139
- } else if (hasPyproject) {
140
- content = await fs.readFile(path.join(repoPath, 'pyproject.toml'), 'utf-8');
125
+ for (const csproj of csprojFiles) {
126
+ try {
127
+ const content = await fs.readFile(csproj, 'utf-8');
128
+ if (content.includes('Microsoft.NET.Sdk.Web')) {
129
+ frameworks.add('aspnet');
130
+ projectTypes.add('backend');
131
+ }
132
+ if (content.includes('Microsoft.NET.Sdk.BlazorWebAssembly') || content.includes('Blazor')) {
133
+ frameworks.add('blazor');
134
+ projectTypes.add('frontend');
135
+ }
136
+ } catch {}
141
137
  }
142
- const lower = content.toLowerCase();
143
- if (lower.includes('django')) { frameworks.push('django'); projectType = 'backend'; }
144
- if (lower.includes('flask')) { frameworks.push('flask'); projectType = 'backend'; }
145
- if (lower.includes('fastapi')) { frameworks.push('fastapi'); projectType = 'backend'; }
146
- } catch {
147
- // Ignore
148
138
  }
149
139
 
150
- if (hasPyproject) buildTools.push('pyproject');
151
- if (hasSetupPy) buildTools.push('setuptools');
152
- }
140
+ // ── Python ────────────────────────────────────────────────────────
141
+ const pyprojectTomls = fileMap.get('pyproject.toml') || [];
142
+ const requirementsTxts = fileMap.get('requirements.txt') || [];
143
+ const setupPys = fileMap.get('setup.py') || [];
144
+
145
+ if (pyprojectTomls.length > 0 || requirementsTxts.length > 0 || setupPys.length > 0) {
146
+ languages.add('python');
153
147
 
154
- // ── Go ────────────────────────────────────────────────────────────
155
- const hasGoMod = await fileExists(repoPath, 'go.mod');
156
- if (hasGoMod) {
157
- languages.push('go');
158
- buildTools.push('go');
159
-
160
- try {
161
- const content = await fs.readFile(path.join(repoPath, 'go.mod'), 'utf-8');
162
- if (content.includes('github.com/gin-gonic/gin')) {
163
- frameworks.push('gin');
164
- projectType = 'backend';
148
+ for (const reqFile of [...requirementsTxts, ...pyprojectTomls]) {
149
+ try {
150
+ const content = await fs.readFile(reqFile, 'utf-8');
151
+ const lower = content.toLowerCase();
152
+ if (lower.includes('django')) { frameworks.add('django'); projectTypes.add('backend'); }
153
+ if (lower.includes('flask')) { frameworks.add('flask'); projectTypes.add('backend'); }
154
+ if (lower.includes('fastapi')) { frameworks.add('fastapi'); projectTypes.add('backend'); }
155
+ } catch {}
165
156
  }
166
- } catch {
167
- // Ignore
157
+
158
+ if (pyprojectTomls.length > 0) buildTools.add('pyproject');
159
+ if (setupPys.length > 0) buildTools.add('setuptools');
168
160
  }
169
- }
170
161
 
171
- // ── Java ──────────────────────────────────────────────────────────
172
- const hasPom = await fileExists(repoPath, 'pom.xml');
173
- const hasGradle = await fileExists(repoPath, 'build.gradle');
174
- const hasGradleKts = await fileExists(repoPath, 'build.gradle.kts');
175
- if (hasPom || hasGradle || hasGradleKts) {
176
- languages.push('java');
177
- if (hasPom) buildTools.push('maven');
178
- if (hasGradle || hasGradleKts) buildTools.push('gradle');
179
-
180
- try {
181
- let content = '';
182
- if (hasPom) {
183
- content = await fs.readFile(path.join(repoPath, 'pom.xml'), 'utf-8');
184
- } else if (hasGradle) {
185
- content = await fs.readFile(path.join(repoPath, 'build.gradle'), 'utf-8');
162
+ // ── Go ────────────────────────────────────────────────────────────
163
+ const goMods = fileMap.get('go.mod') || [];
164
+ if (goMods.length > 0) {
165
+ languages.add('go');
166
+ buildTools.add('go');
167
+
168
+ for (const goMod of goMods) {
169
+ try {
170
+ const content = await fs.readFile(goMod, 'utf-8');
171
+ if (content.includes('github.com/gin-gonic/gin')) {
172
+ frameworks.add('gin');
173
+ projectTypes.add('backend');
174
+ }
175
+ } catch {}
186
176
  }
187
- if (content.includes('spring')) {
188
- frameworks.push('spring');
189
- projectType = 'backend';
177
+ }
178
+
179
+ // ── Java ──────────────────────────────────────────────────────────
180
+ const poms = fileMap.get('pom.xml') || [];
181
+ const gradles = fileMap.get('build.gradle') || [];
182
+ const gradleKts = fileMap.get('build.gradle.kts') || [];
183
+
184
+ if (poms.length > 0 || gradles.length > 0 || gradleKts.length > 0) {
185
+ languages.add('java');
186
+ if (poms.length > 0) buildTools.add('maven');
187
+ if (gradles.length > 0 || gradleKts.length > 0) buildTools.add('gradle');
188
+
189
+ for (const f of [...poms, ...gradles, ...gradleKts]) {
190
+ try {
191
+ const content = await fs.readFile(f, 'utf-8');
192
+ if (content.includes('spring')) {
193
+ frameworks.add('spring');
194
+ projectTypes.add('backend');
195
+ }
196
+ } catch {}
190
197
  }
191
- } catch {
192
- // Ignore
193
198
  }
194
- }
195
199
 
196
- // ── Rust ──────────────────────────────────────────────────────────
197
- const hasCargo = await fileExists(repoPath, 'Cargo.toml');
198
- if (hasCargo) {
199
- languages.push('rust');
200
- buildTools.push('cargo');
201
- }
200
+ // ── Rust ──────────────────────────────────────────────────────────
201
+ const cargoTomls = fileMap.get('Cargo.toml') || [];
202
+ if (cargoTomls.length > 0) {
203
+ languages.add('rust');
204
+ buildTools.add('cargo');
205
+ }
206
+
207
+ // ── Ruby ──────────────────────────────────────────────────────────
208
+ const gemfiles = fileMap.get('Gemfile') || [];
209
+ if (gemfiles.length > 0) {
210
+ languages.add('ruby');
211
+ buildTools.add('bundler');
202
212
 
203
- // ── Ruby ──────────────────────────────────────────────────────────
204
- const hasGemfile = await fileExists(repoPath, 'Gemfile');
205
- if (hasGemfile) {
206
- languages.push('ruby');
207
- buildTools.push('bundler');
208
-
209
- try {
210
- const content = await fs.readFile(path.join(repoPath, 'Gemfile'), 'utf-8');
211
- if (content.includes('rails')) {
212
- frameworks.push('rails');
213
- projectType = 'backend';
213
+ for (const gemfile of gemfiles) {
214
+ try {
215
+ const content = await fs.readFile(gemfile, 'utf-8');
216
+ if (content.includes('rails')) {
217
+ frameworks.add('rails');
218
+ projectTypes.add('backend');
219
+ }
220
+ } catch {}
214
221
  }
215
- } catch {
216
- // Ignore
217
222
  }
218
- }
219
223
 
220
- // ── PHP ───────────────────────────────────────────────────────────
221
- const hasComposer = await fileExists(repoPath, 'composer.json');
222
- if (hasComposer) {
223
- languages.push('php');
224
- buildTools.push('composer');
225
-
226
- const composer = await readJson(path.join(repoPath, 'composer.json'));
227
- if (composer) {
228
- const req = composer.require as Record<string, string> | undefined;
229
- if (req?.['laravel/framework']) {
230
- frameworks.push('laravel');
231
- projectType = 'backend';
224
+ // ── PHP ───────────────────────────────────────────────────────────
225
+ const composerJsons = fileMap.get('composer.json') || [];
226
+ if (composerJsons.length > 0) {
227
+ languages.add('php');
228
+ buildTools.add('composer');
229
+
230
+ for (const comp of composerJsons) {
231
+ const composer = await readJson(comp);
232
+ if (composer) {
233
+ const req = composer.require as Record<string, string> | undefined;
234
+ if (req?.['laravel/framework']) {
235
+ frameworks.add('laravel');
236
+ projectTypes.add('backend');
237
+ }
238
+ }
232
239
  }
233
240
  }
234
- }
235
241
 
236
- // ── Docker ────────────────────────────────────────────────────────
237
- if (await fileExists(repoPath, 'Dockerfile')) {
238
- buildTools.push('docker');
242
+ // ── Docker ────────────────────────────────────────────────────────
243
+ const dockerfiles = fileMap.get('Dockerfile') || [];
244
+ if (dockerfiles.length > 0) {
245
+ buildTools.add('docker');
246
+ }
247
+ const hasCompose = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yaml', 'compose.yml'].some(
248
+ (name) => (fileMap.get(name) || []).length > 0
249
+ );
250
+ if (hasCompose) {
251
+ buildTools.add('docker-compose');
252
+ }
253
+
254
+ } catch {
255
+ // Ignore errors during glob/analysis
239
256
  }
240
- if (await fileExists(repoPath, 'docker-compose.yml') || await fileExists(repoPath, 'compose.yaml')) {
241
- buildTools.push('docker-compose');
257
+
258
+ // Fallback language
259
+ if (languages.size === 0) {
260
+ languages.add('other');
242
261
  }
243
262
 
244
- // Fallback
245
- if (languages.length === 0) {
246
- languages.push('other');
263
+ // Heuristic for overall projectType
264
+ let finalProjectType: ProjectType = 'other';
265
+ if (projectTypes.has('fullstack') || (projectTypes.has('frontend') && projectTypes.has('backend'))) {
266
+ finalProjectType = 'fullstack';
267
+ } else if (projectTypes.has('frontend')) {
268
+ finalProjectType = 'frontend';
269
+ } else if (projectTypes.has('backend')) {
270
+ finalProjectType = 'backend';
271
+ } else if (projectTypes.has('library')) {
272
+ finalProjectType = 'library';
273
+ } else if (projectTypes.has('cli')) {
274
+ finalProjectType = 'cli';
247
275
  }
248
276
 
249
277
  return {
250
- languages,
251
- frameworks,
252
- buildTools,
253
- projectType,
278
+ languages: Array.from(languages),
279
+ frameworks: Array.from(frameworks),
280
+ buildTools: Array.from(buildTools),
281
+ projectType: finalProjectType,
254
282
  };
255
283
  }
@@ -88,7 +88,8 @@ export async function createCommand(): Promise<void> {
88
88
  id: branchName,
89
89
  branchName,
90
90
  description,
91
- repos: selectedRepos.map((r) => r.path),
91
+ repos: selectedRepos.map((r) => path.join(workspacePath, r.name)),
92
+ originalRepos: selectedRepos.map((r) => r.path),
92
93
  assistants: selectedAI,
93
94
  workspacePath,
94
95
  createdAt: new Date().toISOString(),
@@ -119,15 +120,20 @@ export async function createCommand(): Promise<void> {
119
120
  await generateContextFiles(ctx, selectedAI, workspacePath);
120
121
 
121
122
  // ── 8.5. Pack codebase context ──────────────────────────────────────
122
- const packSpinner = ora('Packing codebase context with Repomix...').start();
123
- try {
124
- const packResult = await packWorkspace(workspacePath);
125
- packSpinner.succeed(
126
- `Packed codebase context (${packResult.totalFiles} files, ${(packResult.fileSize / 1024).toFixed(2)} KB)`
127
- );
128
- } catch (error) {
129
- packSpinner.fail('Failed to pack codebase context');
130
- console.error(chalk.red(` ${error}`));
123
+ if (config.packContextXml) {
124
+ const packSpinner = ora('Packing codebase context with Repomix...').start();
125
+ try {
126
+ const packResult = await packWorkspace(workspacePath);
127
+ const filesCount = packResult.outputPaths?.length || 1;
128
+ packSpinner.succeed(
129
+ `Packed codebase context into ${filesCount} file(s) (${packResult.totalFiles} files total, ${(packResult.fileSize / 1024).toFixed(2)} KB)`
130
+ );
131
+ } catch (error) {
132
+ packSpinner.fail('Failed to pack codebase context');
133
+ console.error(chalk.red(` ${error}`));
134
+ }
135
+ } else {
136
+ console.log(chalk.gray('\n ○ Skipping codebase context packing (packContextXml is disabled)'));
131
137
  }
132
138
 
133
139
  // ── 8. Open in editor ───────────────────────────────────────────────