project-compass 4.2.1 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +28 -3
- package/src/cli.js +91 -21
- package/src/components/Footer.js +64 -8
- package/src/components/Header.js +47 -8
- package/src/components/Navigator.js +69 -15
- package/src/detectors/dotnet.js +110 -5
- package/src/detectors/frameworks.js +692 -42
- package/src/detectors/go.js +111 -10
- package/src/detectors/java.js +129 -14
- package/src/detectors/node.js +69 -11
- package/src/detectors/php.js +98 -5
- package/src/detectors/python.js +137 -39
- package/src/detectors/ruby.js +105 -5
- package/src/detectors/rust.js +111 -10
- package/src/detectors/utils.js +95 -7
package/src/detectors/python.js
CHANGED
|
@@ -2,42 +2,96 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { checkBinary, hasProjectFile } from './utils.js';
|
|
4
4
|
|
|
5
|
-
const PYTHON_ENTRY_FILES = [
|
|
5
|
+
const PYTHON_ENTRY_FILES = [
|
|
6
|
+
'main.py', 'app.py', 'src/main.py', 'src/app.py',
|
|
7
|
+
'manage.py', 'wsgi.py', 'asgi.py', 'api.py', 'run.py'
|
|
8
|
+
];
|
|
6
9
|
|
|
7
10
|
function findPythonEntry(projectPath) {
|
|
8
11
|
return PYTHON_ENTRY_FILES.find((file) => hasProjectFile(projectPath, file)) || null;
|
|
9
12
|
}
|
|
10
13
|
|
|
14
|
+
function getPythonPackageManager(projectPath) {
|
|
15
|
+
if (hasProjectFile(projectPath, 'uv.lock') || hasProjectFile(projectPath, 'pyproject.toml')) {
|
|
16
|
+
if (checkBinary('uv')) return 'uv';
|
|
17
|
+
}
|
|
18
|
+
if (hasProjectFile(projectPath, 'Pipfile.lock')) return 'pipenv';
|
|
19
|
+
if (hasProjectFile(projectPath, 'poetry.lock')) return 'poetry';
|
|
20
|
+
if (hasProjectFile(projectPath, 'requirements.txt')) return 'pip';
|
|
21
|
+
return 'pip';
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
function gatherPythonDependencies(projectPath) {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
if (!fs.existsSync(filePath))
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
25
|
+
const deps = new Set();
|
|
26
|
+
const addFromRequirements = (filePath) => {
|
|
27
|
+
if (!fs.existsSync(filePath)) return;
|
|
17
28
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
18
29
|
raw.split(/\r?\n/).forEach((line) => {
|
|
19
30
|
const clean = line.trim().split('#')[0].trim();
|
|
20
|
-
if (clean)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
set.add(token);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
31
|
+
if (!clean || clean.startsWith('-') || clean.startsWith('"') || clean.startsWith("'")) return;
|
|
32
|
+
const match = clean.match(/^([a-zA-Z0-9_.-]+)/);
|
|
33
|
+
if (match) deps.add(match[1].toLowerCase());
|
|
26
34
|
});
|
|
27
35
|
};
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
|
|
37
|
+
addFromRequirements(path.join(projectPath, 'requirements.txt'));
|
|
38
|
+
addFromRequirements(path.join(projectPath, 'requirements-dev.txt'));
|
|
39
|
+
|
|
30
40
|
const pyproject = path.join(projectPath, 'pyproject.toml');
|
|
31
41
|
if (fs.existsSync(pyproject)) {
|
|
32
|
-
const content = fs.readFileSync(pyproject, 'utf-8')
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
const content = fs.readFileSync(pyproject, 'utf-8');
|
|
43
|
+
const depSection = content.match(/(?:dependencies|requires)\s*=\s*\[([^\]]+)\]/g);
|
|
44
|
+
if (depSection) {
|
|
45
|
+
depSection.forEach((section) => {
|
|
46
|
+
const matches = section.match(/["']([^"']+)["']/g);
|
|
47
|
+
if (matches) {
|
|
48
|
+
matches.forEach((m) => {
|
|
49
|
+
const dep = m.replace(/["']/g, '').split(/[>=<=~!]/)[0].trim();
|
|
50
|
+
if (dep) deps.add(dep.toLowerCase());
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const pipfile = path.join(projectPath, 'Pipfile');
|
|
58
|
+
if (fs.existsSync(pipfile)) {
|
|
59
|
+
const content = fs.readFileSync(pipfile, 'utf-8');
|
|
60
|
+
const matches = content.match(/["']([^"']+)["']/g);
|
|
61
|
+
if (matches) {
|
|
62
|
+
matches.forEach((m) => {
|
|
63
|
+
const dep = m.replace(/["']/g, '').split(/[>=<=~!]/)[0].trim();
|
|
64
|
+
if (dep) deps.add(dep.toLowerCase());
|
|
65
|
+
});
|
|
66
|
+
}
|
|
39
67
|
}
|
|
40
|
-
|
|
68
|
+
|
|
69
|
+
return Array.from(deps);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectPythonFramework(deps) {
|
|
73
|
+
const frameworks = [];
|
|
74
|
+
const depStr = deps.join(' ').toLowerCase();
|
|
75
|
+
|
|
76
|
+
if (depStr.includes('fastapi')) frameworks.push({ name: 'FastAPI', icon: '⚡' });
|
|
77
|
+
if (depStr.includes('flask')) frameworks.push({ name: 'Flask', icon: '🌶️' });
|
|
78
|
+
if (depStr.includes('django')) frameworks.push({ name: 'Django', icon: '🌿' });
|
|
79
|
+
if (depStr.includes('tornado')) frameworks.push({ name: 'Tornado', icon: '🌪️' });
|
|
80
|
+
if (depStr.includes('aiohttp')) frameworks.push({ name: 'AioHTTP', icon: '🔄' });
|
|
81
|
+
if (depStr.includes('sanic')) frameworks.push({ name: 'Sanic', icon: '🚀' });
|
|
82
|
+
if (depStr.includes('pyramid')) frameworks.push({ name: 'Pyramid', icon: '🔺' });
|
|
83
|
+
if (depStr.includes('falcon')) frameworks.push({ name: 'Falcon', icon: '🦅' });
|
|
84
|
+
if (depStr.includes('starlette')) frameworks.push({ name: 'Starlette', icon: '⭐' });
|
|
85
|
+
if (depStr.includes('pandas')) frameworks.push({ name: 'Pandas', icon: '🐼' });
|
|
86
|
+
if (depStr.includes('numpy')) frameworks.push({ name: 'NumPy', icon: '🔢' });
|
|
87
|
+
if (depStr.includes('scipy')) frameworks.push({ name: 'SciPy', icon: '🔬' });
|
|
88
|
+
if (depStr.includes('torch') || depStr.includes('pytorch')) frameworks.push({ name: 'PyTorch', icon: '🔥' });
|
|
89
|
+
if (depStr.includes('tensorflow')) frameworks.push({ name: 'TensorFlow', icon: '🧠' });
|
|
90
|
+
if (depStr.includes('sqlalchemy')) frameworks.push({ name: 'SQLAlchemy', icon: '🗄️' });
|
|
91
|
+
if (depStr.includes('pytest')) frameworks.push({ name: 'Pytest', icon: '✅' });
|
|
92
|
+
if (depStr.includes('celery')) frameworks.push({ name: 'Celery', icon: '🥬' });
|
|
93
|
+
|
|
94
|
+
return frameworks;
|
|
41
95
|
}
|
|
42
96
|
|
|
43
97
|
export default {
|
|
@@ -45,36 +99,78 @@ export default {
|
|
|
45
99
|
label: 'Python',
|
|
46
100
|
icon: '🐍',
|
|
47
101
|
priority: 95,
|
|
48
|
-
files: ['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile'],
|
|
49
|
-
binaries: [
|
|
102
|
+
files: ['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile', 'manage.py'],
|
|
103
|
+
binaries: ['python3', 'python', 'uv'].filter(Boolean),
|
|
50
104
|
async build(projectPath, manifest) {
|
|
51
105
|
const missingBinaries = this.binaries.filter(b => !checkBinary(b));
|
|
106
|
+
const pkgManager = getPythonPackageManager(projectPath);
|
|
107
|
+
const isUV = pkgManager === 'uv';
|
|
108
|
+
const isPoetry = pkgManager === 'poetry';
|
|
109
|
+
const isPipenv = pkgManager === 'pipenv';
|
|
110
|
+
|
|
52
111
|
const commands = {};
|
|
53
|
-
|
|
54
|
-
|
|
112
|
+
|
|
113
|
+
if (isUV) {
|
|
114
|
+
commands.install = { label: 'UV Sync', command: ['uv', 'sync'], source: 'builtin' };
|
|
115
|
+
commands.add = { label: 'UV Add', command: ['uv', 'add'], source: 'builtin' };
|
|
116
|
+
commands.run = { label: 'UV Run', command: ['uv', 'run', 'python'], source: 'builtin' };
|
|
117
|
+
} else if (isPoetry) {
|
|
118
|
+
commands.install = { label: 'Poetry Install', command: ['poetry', 'install'], source: 'builtin' };
|
|
119
|
+
commands.add = { label: 'Poetry Add', command: ['poetry', 'add'], source: 'builtin' };
|
|
120
|
+
commands.run = { label: 'Poetry Run', command: ['poetry', 'run', 'python'], source: 'builtin' };
|
|
121
|
+
} else if (isPipenv) {
|
|
122
|
+
commands.install = { label: 'Pipenv Install', command: ['pipenv', 'install'], source: 'builtin' };
|
|
123
|
+
commands.run = { label: 'Pipenv Run', command: ['pipenv', 'run', 'python'], source: 'builtin' };
|
|
124
|
+
} else {
|
|
125
|
+
commands.install = { label: 'Pip Install', command: ['pip', 'install', '-r', 'requirements.txt'], source: 'builtin' };
|
|
55
126
|
}
|
|
56
|
-
|
|
57
|
-
|
|
127
|
+
|
|
128
|
+
if (hasProjectFile(projectPath, 'pyproject.toml') || hasProjectFile(projectPath, 'setup.py')) {
|
|
129
|
+
commands.test = { label: 'Pytest', command: [isUV ? 'uv' : 'python', ...(isUV ? ['run'] : []), 'pytest'], source: 'builtin' };
|
|
58
130
|
} else {
|
|
59
|
-
commands.test = { label: 'Unittest', command: ['python', '-m', 'unittest', 'discover'] };
|
|
131
|
+
commands.test = { label: 'Unittest', command: ['python', '-m', 'unittest', 'discover'], source: 'builtin' };
|
|
60
132
|
}
|
|
61
133
|
|
|
62
134
|
const entry = findPythonEntry(projectPath);
|
|
63
135
|
if (entry) {
|
|
64
|
-
|
|
136
|
+
const runCmd = isUV ? ['uv', 'run', 'python', entry] :
|
|
137
|
+
isPoetry ? ['poetry', 'run', 'python', entry] :
|
|
138
|
+
isPipenv ? ['pipenv', 'run', 'python', entry] :
|
|
139
|
+
['python', entry];
|
|
140
|
+
commands.run = { label: 'Run', command: runCmd, source: 'builtin' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (hasProjectFile(projectPath, 'manage.py')) {
|
|
144
|
+
const djangoCmd = isUV ? ['uv', 'run', 'python', 'manage.py'] :
|
|
145
|
+
isPoetry ? ['poetry', 'run', 'python', 'manage.py'] :
|
|
146
|
+
['python', 'manage.py'];
|
|
147
|
+
commands['runserver'] = { label: 'Django Runserver', command: [...djangoCmd, 'runserver'], source: 'builtin' };
|
|
148
|
+
commands['migrate'] = { label: 'Django Migrate', command: [...djangoCmd, 'migrate'], source: 'builtin' };
|
|
149
|
+
commands['test'] = { label: 'Django Test', command: [...djangoCmd, 'test'], source: 'builtin' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (hasProjectFile(projectPath, 'fastapi') || hasProjectFile(projectPath, 'main.py')) {
|
|
153
|
+
if (hasProjectFile(projectPath, 'main.py')) {
|
|
154
|
+
const fastapiCmd = isUV ? ['uv', 'run', 'uvicorn', 'main:app', '--reload'] :
|
|
155
|
+
['uvicorn', 'main:app', '--reload'];
|
|
156
|
+
commands['dev'] = { label: 'FastAPI Dev', command: fastapiCmd, source: 'builtin' };
|
|
157
|
+
}
|
|
65
158
|
}
|
|
66
159
|
|
|
160
|
+
const allDeps = gatherPythonDependencies(projectPath);
|
|
161
|
+
const detectedFrameworks = detectPythonFramework(allDeps);
|
|
162
|
+
|
|
67
163
|
const metadata = {
|
|
68
|
-
dependencies:
|
|
164
|
+
dependencies: allDeps,
|
|
165
|
+
packageManager: pkgManager,
|
|
166
|
+
frameworks: detectedFrameworks
|
|
69
167
|
};
|
|
70
168
|
|
|
71
169
|
const setupHints = [];
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (hasProjectFile(projectPath, '
|
|
76
|
-
setupHints.push('Use pipenv install --dev or poetry install');
|
|
77
|
-
}
|
|
170
|
+
if (isUV) setupHints.push('uv sync');
|
|
171
|
+
else if (isPoetry) setupHints.push('poetry install');
|
|
172
|
+
else if (isPipenv) setupHints.push('pipenv install');
|
|
173
|
+
else if (hasProjectFile(projectPath, 'requirements.txt')) setupHints.push('pip install -r requirements.txt');
|
|
78
174
|
|
|
79
175
|
return {
|
|
80
176
|
id: `${projectPath}::python`,
|
|
@@ -86,11 +182,13 @@ export default {
|
|
|
86
182
|
commands,
|
|
87
183
|
metadata,
|
|
88
184
|
manifest: path.basename(manifest),
|
|
89
|
-
description: '',
|
|
185
|
+
description: detectedFrameworks.map(f => f.name).join(', '),
|
|
90
186
|
missingBinaries,
|
|
187
|
+
frameworks: detectedFrameworks,
|
|
91
188
|
extra: {
|
|
92
189
|
entry,
|
|
93
|
-
setupHints
|
|
190
|
+
setupHints,
|
|
191
|
+
packageManager: pkgManager
|
|
94
192
|
}
|
|
95
193
|
};
|
|
96
194
|
}
|
package/src/detectors/ruby.js
CHANGED
|
@@ -1,13 +1,113 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
1
2
|
import path from 'path';
|
|
2
|
-
import { checkBinary } from './utils.js';
|
|
3
|
+
import { checkBinary, hasProjectFile } from './utils.js';
|
|
4
|
+
|
|
5
|
+
function parseGemfile(content) {
|
|
6
|
+
const gems = [];
|
|
7
|
+
const lines = content.split('\n');
|
|
8
|
+
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
const trimmed = line.trim();
|
|
11
|
+
if (trimmed.startsWith('gem ') || trimmed.startsWith('gem(')) {
|
|
12
|
+
const match = trimmed.match(/gem\s+['"]([^'"]+)['"]/);
|
|
13
|
+
if (match) gems.push(match[1]);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return gems;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function detectRubyFrameworks(gems) {
|
|
21
|
+
const frameworks = [];
|
|
22
|
+
const gemStr = gems.join(' ').toLowerCase();
|
|
23
|
+
|
|
24
|
+
if (gemStr.includes('rails')) frameworks.push({ name: 'Ruby on Rails', icon: '🛤️' });
|
|
25
|
+
if (gemStr.includes('sinatra')) frameworks.push({ name: 'Sinatra', icon: '🎷' });
|
|
26
|
+
if (gemStr.includes('padrino')) frameworks.push({ name: 'Padrino', icon: '🎩' });
|
|
27
|
+
if (gemStr.includes('hanami') || gemStr.includes('lotus')) frameworks.push({ name: 'Hanami', icon: '🌸' });
|
|
28
|
+
if (gemStr.includes('grape')) frameworks.push({ name: 'Grape', icon: '🍇' });
|
|
29
|
+
if (gemStr.includes('roda')) frameworks.push({ name: 'Roda', icon: '🎣' });
|
|
30
|
+
if (gemStr.includes('cuba')) frameworks.push({ name: 'Cuba', icon: '🎵' });
|
|
31
|
+
if (gemStr.includes('rspec')) frameworks.push({ name: 'RSpec', icon: '✅' });
|
|
32
|
+
if (gemStr.includes('minitest')) frameworks.push({ name: 'MiniTest', icon: '🔬' });
|
|
33
|
+
if (gemStr.includes('sidekiq')) frameworks.push({ name: 'Sidekiq', icon: '🥬' });
|
|
34
|
+
if (gemStr.includes('activerecord')) frameworks.push({ name: 'ActiveRecord', icon: '🗄️' });
|
|
35
|
+
|
|
36
|
+
return frameworks;
|
|
37
|
+
}
|
|
38
|
+
|
|
3
39
|
export default {
|
|
4
|
-
type: 'ruby',
|
|
40
|
+
type: 'ruby',
|
|
41
|
+
label: 'Ruby',
|
|
42
|
+
icon: '💎',
|
|
43
|
+
priority: 65,
|
|
44
|
+
files: ['Gemfile', 'Gemfile.lock'],
|
|
45
|
+
binaries: ['ruby', 'bundle'],
|
|
5
46
|
async build(projectPath, manifest) {
|
|
6
47
|
const missingBinaries = this.binaries.filter(b => !checkBinary(b));
|
|
48
|
+
let gems = [];
|
|
49
|
+
let frameworks = [];
|
|
50
|
+
|
|
51
|
+
const gemfilePath = path.join(projectPath, 'Gemfile');
|
|
52
|
+
if (fs.existsSync(gemfilePath)) {
|
|
53
|
+
const content = fs.readFileSync(gemfilePath, 'utf-8');
|
|
54
|
+
gems = parseGemfile(content);
|
|
55
|
+
frameworks = detectRubyFrameworks(gems);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const commands = {
|
|
59
|
+
install: { label: 'Bundle install', command: ['bundle', 'install'], source: 'builtin' },
|
|
60
|
+
update: { label: 'Bundle update', command: ['bundle', 'update'], source: 'builtin' },
|
|
61
|
+
console: { label: 'Ruby console', command: ['ruby', '-e', 'puts "IRB"'], source: 'builtin' }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (hasProjectFile(projectPath, 'bin/rails')) {
|
|
65
|
+
commands.run = { label: 'Rails server', command: ['bin/rails', 'server'], source: 'builtin' };
|
|
66
|
+
commands.test = { label: 'Rails test', command: ['bin/rails', 'test'], source: 'builtin' };
|
|
67
|
+
commands.migrate = { label: 'Rails migrate', command: ['bin/rails', 'db:migrate'], source: 'builtin' };
|
|
68
|
+
commands.console = { label: 'Rails console', command: ['bin/rails', 'console'], source: 'builtin' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (hasProjectFile(projectPath, 'config.ru') && !hasProjectFile(projectPath, 'bin/rails')) {
|
|
72
|
+
commands.run = { label: 'Rackup', command: ['rackup'], source: 'builtin' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (gems.includes('rspec')) {
|
|
76
|
+
commands.test = { label: 'RSpec', command: ['bundle', 'exec', 'rspec'], source: 'builtin' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const setupHints = [];
|
|
80
|
+
if (missingBinaries.includes('bundle')) {
|
|
81
|
+
setupHints.push('Install Bundler: gem install bundler');
|
|
82
|
+
}
|
|
83
|
+
if (gems.length > 0) {
|
|
84
|
+
setupHints.push('Run bundle install to fetch dependencies');
|
|
85
|
+
}
|
|
86
|
+
if (hasProjectFile(projectPath, '.ruby-version')) {
|
|
87
|
+
const version = fs.readFileSync(path.join(projectPath, '.ruby-version'), 'utf-8').trim();
|
|
88
|
+
setupHints.push(`Requires Ruby ${version}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
7
91
|
return {
|
|
8
|
-
id: `${projectPath}::ruby`,
|
|
9
|
-
|
|
10
|
-
|
|
92
|
+
id: `${projectPath}::ruby`,
|
|
93
|
+
path: projectPath,
|
|
94
|
+
name: path.basename(projectPath),
|
|
95
|
+
type: 'Ruby',
|
|
96
|
+
icon: '💎',
|
|
97
|
+
priority: this.priority,
|
|
98
|
+
commands,
|
|
99
|
+
metadata: {
|
|
100
|
+
dependencies: gems,
|
|
101
|
+
packageManager: 'bundler'
|
|
102
|
+
},
|
|
103
|
+
manifest: path.basename(manifest),
|
|
104
|
+
description: frameworks.map(f => f.name).join(', '),
|
|
105
|
+
missingBinaries,
|
|
106
|
+
frameworks,
|
|
107
|
+
extra: {
|
|
108
|
+
setupHints,
|
|
109
|
+
gems
|
|
110
|
+
}
|
|
11
111
|
};
|
|
12
112
|
}
|
|
13
113
|
};
|
package/src/detectors/rust.js
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
1
2
|
import path from 'path';
|
|
2
|
-
import { checkBinary } from './utils.js';
|
|
3
|
+
import { checkBinary, hasProjectFile } from './utils.js';
|
|
4
|
+
|
|
5
|
+
function parseCargoToml(content) {
|
|
6
|
+
const metadata = {
|
|
7
|
+
name: '',
|
|
8
|
+
version: '',
|
|
9
|
+
description: '',
|
|
10
|
+
dependencies: [],
|
|
11
|
+
binaries: []
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const lines = content.split('\n');
|
|
15
|
+
let inDependencies = false;
|
|
16
|
+
let inBin = false;
|
|
17
|
+
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
|
|
21
|
+
if (trimmed.startsWith('name') && !inDependencies && !inBin) {
|
|
22
|
+
metadata.name = trimmed.split('=')[1]?.replace(/"/g, '').trim() || '';
|
|
23
|
+
}
|
|
24
|
+
if (trimmed.startsWith('version') && !inDependencies && !inBin) {
|
|
25
|
+
metadata.version = trimmed.split('=')[1]?.replace(/"/g, '').trim() || '';
|
|
26
|
+
}
|
|
27
|
+
if (trimmed.startsWith('description') && !inDependencies && !inBin) {
|
|
28
|
+
metadata.description = trimmed.split('=')[1]?.replace(/"/g, '').trim() || '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (trimmed === '[dependencies]') {
|
|
32
|
+
inDependencies = true;
|
|
33
|
+
inBin = false;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (trimmed.startsWith('[')) {
|
|
37
|
+
inDependencies = false;
|
|
38
|
+
inBin = trimmed === '[bin]';
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (inDependencies && trimmed && !trimmed.startsWith('#')) {
|
|
43
|
+
const depName = trimmed.split('=')[0]?.trim() || trimmed.split('{')[0]?.trim();
|
|
44
|
+
if (depName) metadata.dependencies.push(depName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (inBin && trimmed.startsWith('name')) {
|
|
48
|
+
const binName = trimmed.split('=')[1]?.replace(/"/g, '').trim();
|
|
49
|
+
if (binName) metadata.binaries.push(binName);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return metadata;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function detectRustFrameworks(deps) {
|
|
57
|
+
const frameworks = [];
|
|
58
|
+
const depStr = deps.join(' ').toLowerCase();
|
|
59
|
+
|
|
60
|
+
if (depStr.includes('actix-web')) frameworks.push({ name: 'Actix Web', icon: '🎭' });
|
|
61
|
+
if (depStr.includes('rocket')) frameworks.push({ name: 'Rocket', icon: '🚀' });
|
|
62
|
+
if (depStr.includes('axum')) frameworks.push({ name: 'Axum', icon: '🗡️' });
|
|
63
|
+
if (depStr.includes('warp')) frameworks.push({ name: 'Warp', icon: '🌀' });
|
|
64
|
+
if (depStr.includes('tokio')) frameworks.push({ name: 'Tokio', icon: '⚡' });
|
|
65
|
+
if (depStr.includes('serde')) frameworks.push({ name: 'Serde', icon: '🔄' });
|
|
66
|
+
if (depStr.includes('sqlx')) frameworks.push({ name: 'SQLx', icon: '🗄️' });
|
|
67
|
+
if (depStr.includes('diesel')) frameworks.push({ name: 'Diesel', icon: '🛢️' });
|
|
68
|
+
if (depStr.includes('tonic')) frameworks.push({ name: 'Tonic', icon: '🎵' });
|
|
69
|
+
if (depStr.includes('tower')) frameworks.push({ name: 'Tower', icon: '🏰' });
|
|
70
|
+
|
|
71
|
+
return frameworks;
|
|
72
|
+
}
|
|
3
73
|
|
|
4
74
|
export default {
|
|
5
75
|
type: 'rust',
|
|
@@ -10,25 +80,56 @@ export default {
|
|
|
10
80
|
binaries: ['cargo', 'rustc'],
|
|
11
81
|
async build(projectPath, manifest) {
|
|
12
82
|
const missingBinaries = this.binaries.filter(b => !checkBinary(b));
|
|
83
|
+
const cargoPath = path.join(projectPath, 'Cargo.toml');
|
|
84
|
+
let metadata = { name: '', version: '', description: '', dependencies: [], binaries: [] };
|
|
85
|
+
let frameworks = [];
|
|
86
|
+
|
|
87
|
+
if (fs.existsSync(cargoPath)) {
|
|
88
|
+
const content = fs.readFileSync(cargoPath, 'utf-8');
|
|
89
|
+
metadata = parseCargoToml(content);
|
|
90
|
+
frameworks = detectRustFrameworks(metadata.dependencies);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const commands = {
|
|
94
|
+
install: { label: 'Cargo fetch', command: ['cargo', 'fetch'], source: 'builtin' },
|
|
95
|
+
build: { label: 'Cargo build', command: ['cargo', 'build'], source: 'builtin' },
|
|
96
|
+
test: { label: 'Cargo test', command: ['cargo', 'test'], source: 'builtin' },
|
|
97
|
+
run: { label: 'Cargo run', command: ['cargo', 'run'], source: 'builtin' },
|
|
98
|
+
check: { label: 'Cargo check', command: ['cargo', 'check'], source: 'builtin' },
|
|
99
|
+
doc: { label: 'Cargo doc', command: ['cargo', 'doc', '--open'], source: 'builtin' }
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (hasProjectFile(projectPath, 'Cargo.toml')) {
|
|
103
|
+
commands.update = { label: 'Cargo update', command: ['cargo', 'update'], source: 'builtin' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const setupHints = [];
|
|
107
|
+
if (missingBinaries.length > 0) {
|
|
108
|
+
setupHints.push('Install Rust: curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh');
|
|
109
|
+
}
|
|
110
|
+
if (metadata.dependencies.length > 0) {
|
|
111
|
+
setupHints.push('Run cargo fetch to download dependencies');
|
|
112
|
+
}
|
|
113
|
+
|
|
13
114
|
return {
|
|
14
115
|
id: `${projectPath}::rust`,
|
|
15
116
|
path: projectPath,
|
|
16
|
-
name: path.basename(projectPath),
|
|
117
|
+
name: metadata.name || path.basename(projectPath),
|
|
17
118
|
type: 'Rust',
|
|
18
119
|
icon: '🦀',
|
|
19
120
|
priority: this.priority,
|
|
20
|
-
commands
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
run: { label: 'Cargo run', command: ['cargo', 'run'] }
|
|
121
|
+
commands,
|
|
122
|
+
metadata: {
|
|
123
|
+
...metadata,
|
|
124
|
+
packageManager: 'cargo'
|
|
25
125
|
},
|
|
26
|
-
metadata: {},
|
|
27
126
|
manifest: path.basename(manifest),
|
|
28
|
-
description: '',
|
|
127
|
+
description: metadata.description || frameworks.map(f => f.name).join(', '),
|
|
29
128
|
missingBinaries,
|
|
129
|
+
frameworks,
|
|
30
130
|
extra: {
|
|
31
|
-
setupHints
|
|
131
|
+
setupHints,
|
|
132
|
+
binaries: metadata.binaries
|
|
32
133
|
}
|
|
33
134
|
};
|
|
34
135
|
}
|
package/src/detectors/utils.js
CHANGED
|
@@ -16,16 +16,72 @@ export function hasProjectFile(projectPath, file) {
|
|
|
16
16
|
return fs.existsSync(path.join(projectPath, file));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
export function hasProjectFilePattern(projectPath, pattern) {
|
|
20
|
+
try {
|
|
21
|
+
const files = fs.readdirSync(projectPath);
|
|
22
|
+
return files.some(f => new RegExp(pattern).test(f));
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getPackageManager(projectPath, language = 'node') {
|
|
29
|
+
// Node.js package managers
|
|
30
|
+
if (language === 'node' || language === 'Node.js') {
|
|
31
|
+
if (hasProjectFile(projectPath, 'bun.lockb') || hasProjectFile(projectPath, 'bun.lock')) return 'bun';
|
|
32
|
+
if (hasProjectFile(projectPath, 'pnpm-lock.yaml')) return 'pnpm';
|
|
33
|
+
if (hasProjectFile(projectPath, 'yarn.lock')) return 'yarn';
|
|
34
|
+
if (hasProjectFile(projectPath, 'package-lock.json')) return 'npm';
|
|
35
|
+
return 'npm';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Python package managers
|
|
39
|
+
if (language === 'python' || language === 'Python') {
|
|
40
|
+
if (hasProjectFile(projectPath, 'uv.lock') && checkBinary('uv')) return 'uv';
|
|
41
|
+
if (hasProjectFile(projectPath, 'poetry.lock')) return 'poetry';
|
|
42
|
+
if (hasProjectFile(projectPath, 'Pipfile.lock')) return 'pipenv';
|
|
43
|
+
if (hasProjectFile(projectPath, 'requirements.txt')) return 'pip';
|
|
44
|
+
return 'pip';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Go - uses go modules
|
|
48
|
+
if (language === 'go' || language === 'Go') {
|
|
49
|
+
return 'go';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Rust - uses cargo
|
|
53
|
+
if (language === 'rust' || language === 'Rust') {
|
|
54
|
+
return 'cargo';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Java - maven or gradle
|
|
58
|
+
if (language === 'java' || language === 'Java') {
|
|
59
|
+
if (hasProjectFile(projectPath, 'pom.xml')) return 'maven';
|
|
60
|
+
if (hasProjectFile(projectPath, 'build.gradle') || hasProjectFile(projectPath, 'build.gradle.kts')) return 'gradle';
|
|
61
|
+
return 'maven';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// PHP - uses composer
|
|
65
|
+
if (language === 'php' || language === 'PHP') {
|
|
66
|
+
return 'composer';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Ruby - uses bundler
|
|
70
|
+
if (language === 'ruby' || language === 'Ruby') {
|
|
71
|
+
return 'bundler';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// .NET - uses dotnet
|
|
75
|
+
if (language === '.net' || language === '.NET') {
|
|
76
|
+
return 'dotnet';
|
|
77
|
+
}
|
|
78
|
+
|
|
23
79
|
return 'npm';
|
|
24
80
|
}
|
|
25
81
|
|
|
26
82
|
export function resolveScriptCommand(project, scriptName, fallback = null) {
|
|
27
83
|
const scripts = project.metadata?.scripts || {};
|
|
28
|
-
const pm = project.metadata?.packageManager ||
|
|
84
|
+
const pm = project.metadata?.packageManager || getPackageManager(project.path, project.type);
|
|
29
85
|
if (Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
30
86
|
return [pm, 'run', scriptName];
|
|
31
87
|
}
|
|
@@ -36,9 +92,19 @@ export function resolveScriptCommand(project, scriptName, fallback = null) {
|
|
|
36
92
|
}
|
|
37
93
|
|
|
38
94
|
export function dependencyMatches(project, needle) {
|
|
39
|
-
const dependencies = (project.metadata?.dependencies || []).map((dep) =>
|
|
95
|
+
const dependencies = (project.metadata?.dependencies || []).map((dep) => {
|
|
96
|
+
if (typeof dep === 'string') return dep.toLowerCase();
|
|
97
|
+
if (dep && typeof dep === 'object' && dep.name) return dep.name.toLowerCase();
|
|
98
|
+
return '';
|
|
99
|
+
}).filter(Boolean);
|
|
40
100
|
const target = needle.toLowerCase();
|
|
41
|
-
return dependencies.some((value) =>
|
|
101
|
+
return dependencies.some((value) =>
|
|
102
|
+
value === target ||
|
|
103
|
+
value.startsWith(`${target}@`) ||
|
|
104
|
+
value.includes(`/${target}`) ||
|
|
105
|
+
value.startsWith(`${target}/`) ||
|
|
106
|
+
value.endsWith(`/${target}`)
|
|
107
|
+
);
|
|
42
108
|
}
|
|
43
109
|
|
|
44
110
|
export function parseCommandTokens(value) {
|
|
@@ -58,3 +124,25 @@ export function parseCommandTokens(value) {
|
|
|
58
124
|
}
|
|
59
125
|
return [];
|
|
60
126
|
}
|
|
127
|
+
|
|
128
|
+
export function getLockfileInfo(projectPath) {
|
|
129
|
+
const lockfiles = [
|
|
130
|
+
{ file: 'package-lock.json', pm: 'npm' },
|
|
131
|
+
{ file: 'yarn.lock', pm: 'yarn' },
|
|
132
|
+
{ file: 'pnpm-lock.yaml', pm: 'pnpm' },
|
|
133
|
+
{ file: 'bun.lockb', pm: 'bun' },
|
|
134
|
+
{ file: 'uv.lock', pm: 'uv' },
|
|
135
|
+
{ file: 'poetry.lock', pm: 'poetry' },
|
|
136
|
+
{ file: 'Pipfile.lock', pm: 'pipenv' },
|
|
137
|
+
{ file: 'composer.lock', pm: 'composer' },
|
|
138
|
+
{ file: 'Cargo.lock', pm: 'cargo' },
|
|
139
|
+
{ file: 'go.sum', pm: 'go' }
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
for (const { file, pm } of lockfiles) {
|
|
143
|
+
if (hasProjectFile(projectPath, file)) {
|
|
144
|
+
return { lockfile: file, packageManager: pm };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { lockfile: null, packageManager: null };
|
|
148
|
+
}
|