project-compass 4.2.1 → 4.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.
@@ -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 = ['main.py', 'app.py', 'src/main.py', 'src/app.py'];
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 set = new Set();
13
- const addFromFile = (filePath) => {
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
- const token = clean.split(/[>=<=~!]/)[0].trim().toLowerCase();
22
- if (token) {
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
- addFromFile(path.join(projectPath, 'requirements.txt'));
29
- addFromFile(path.join(projectPath, 'Pipfile'));
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').toLowerCase();
33
- const matches = content.match(/\b[a-z0-9-_/.]+\b/g);
34
- (matches || []).forEach((match) => {
35
- if (match) {
36
- set.add(match);
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
- return Array.from(set);
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: [process.platform === 'win32' ? 'python' : 'python3', 'pip'],
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
- if (hasProjectFile(projectPath, 'requirements.txt')) {
54
- commands.install = { label: 'Pip Install', command: ['pip', 'install', '-r', 'requirements.txt'] };
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
- if (hasProjectFile(projectPath, 'pyproject.toml')) {
57
- commands.test = { label: 'Pytest', command: ['pytest'] };
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
- commands.run = { label: 'Run', command: ['python', entry] };
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: gatherPythonDependencies(projectPath)
164
+ dependencies: allDeps,
165
+ packageManager: pkgManager,
166
+ frameworks: detectedFrameworks
69
167
  };
70
168
 
71
169
  const setupHints = [];
72
- if (hasProjectFile(projectPath, 'requirements.txt')) {
73
- setupHints.push('pip install -r requirements.txt');
74
- }
75
- if (hasProjectFile(projectPath, 'Pipfile')) {
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
  }
@@ -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', label: 'Ruby', icon: '💎', priority: 65, files: ['Gemfile'], binaries: ['ruby', 'bundle'],
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`, path: projectPath, name: path.basename(projectPath), type: 'Ruby', icon: '💎',
9
- priority: this.priority, commands: { install: { label: 'Bundle install', command: ['bundle', 'install'] } },
10
- metadata: {}, manifest: path.basename(manifest), description: '', missingBinaries, extra: {}
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
  };
@@ -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
- install: { label: 'Cargo fetch', command: ['cargo', 'fetch'] },
22
- build: { label: 'Cargo build', command: ['cargo', 'build'] },
23
- test: { label: 'Cargo test', command: ['cargo', 'test'] },
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: ['cargo fetch', 'Run cargo build before releasing']
131
+ setupHints,
132
+ binaries: metadata.binaries
32
133
  }
33
134
  };
34
135
  }
@@ -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 getPackageManager(projectPath) {
20
- if (hasProjectFile(projectPath, 'bun.lockb') || hasProjectFile(projectPath, 'bun.lock')) return 'bun';
21
- if (hasProjectFile(projectPath, 'pnpm-lock.yaml')) return 'pnpm';
22
- if (hasProjectFile(projectPath, 'yarn.lock')) return 'yarn';
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 || 'npm';
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) => dep.toLowerCase());
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) => value === target || value.startsWith(`${target}@`) || value.includes(`/${target}`));
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
+ }