project-compass 4.2.0 → 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.
@@ -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
+ }
@@ -1,28 +0,0 @@
1
- # 2026-03-02 - Project Compass Final Polish
2
-
3
- ## Task: Finalize and Polish project-compass (v3.7.0)
4
- - **Status**: In Progress
5
- - **Location**: /mnt/ramdisk/project-compass/
6
-
7
- ### Work Log:
8
- - Initializing final polish sub-agent.
9
- - Goal: Production-grade refactor, documentation update, and framework support verification.
10
- - **Documentation Update**:
11
- - Revamped `README.md` and `commands.md`.
12
- - Included all new framework support (Next.js Bun, Django, Rust, etc.).
13
- - Documented the new 'Install' (I) macro command.
14
- - **Refactor (src/projectDetection.js)**:
15
- - Updated `getPackageManager` to return 'npm' as default but prioritized `bun.lockb`, `pnpm-lock.yaml`.
16
- - Hardcoded 'npm' replaced with dynamic `pm` everywhere in Node.js framework detection.
17
- - Extended Next.js detection to include `.mjs` and `.ts` config files.
18
- - Updated Vite, Tailwind, and Prisma commands to use project-specific package managers and `npx`-equivalent logic.
19
- - **CLI Enhancement (src/cli.js)**:
20
- - Added 'i' (install) to `ACTION_MAP`.
21
- - Updated help menu and reserved keys to include 'i'.
22
- - **Architect Polish (src/components/ProjectArchitect.js)**:
23
- - Added Next.js (Bun), React (pnpm), and Django templates.
24
- - Ensured robust command generation for scaffolding.
25
- - **Verification**:
26
- - Confirmed `metadata.packageManager` is used in all critical Node.js paths.
27
- - Bumbed version to 3.7.0 in `package.json`.
28
- - **Status**: Production Ready.
@@ -1,22 +0,0 @@
1
- # Changelog - 2026-03-03
2
-
3
- ## [3.8.0] - Architectural Overhaul
4
-
5
- ### Added
6
- - **Paginated Navigator**: Implemented pagination for the project list. Now handles large workspaces (50+ projects) gracefully without breaking the TUI layout.
7
- - **Configurable Limits**: Added `maxVisibleProjects` setting to `config.json` (default: 8).
8
- - **Modular Architecture**:
9
- - Broke down the massive `projectDetection.js` into specialized detectors in `src/detectors/` (Node, Python, Rust, Go, Java, Generic).
10
- - Modularized TUI logic from `cli.js` into standalone React components: `Navigator.js`, `Header.js`, and `Footer.js`.
11
- - **Production Hardening**: Added robust error boundaries and try-catch blocks in the project discovery pipeline to ensure one corrupt manifest doesn't crash the entire CLI.
12
-
13
- ### Refactored
14
- - **src/projectDetection.js**: Now acts as a clean orchestrator for modular detectors.
15
- - **src/cli.js**: Significantly reduced complexity by delegating UI rendering to sub-components.
16
- - **State Management**: Introduced `src/store/` (preparing for full context-based state management).
17
-
18
- ### Technical Details
19
- - New file structure:
20
- - `src/detectors/node.js`, `python.js`, `rust.js`, etc.
21
- - `src/components/Navigator.js`, `Header.js`, `Footer.js`.
22
- - `src/detectors/utils.js` for shared detection logic.