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.
- 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/memory/2026-03-02.md +0 -28
- package/memory/2026-03-03.md +0 -22
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
|
+
}
|
package/memory/2026-03-02.md
DELETED
|
@@ -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.
|
package/memory/2026-03-03.md
DELETED
|
@@ -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.
|