project-compass 4.3.1 โ 4.3.3
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 +2 -2
- package/src/cli.js +28 -2
- package/src/components/AIHorizon.js +246 -54
- package/src/components/TaskManager.js +70 -12
- package/src/detectors/compass-config.js +39 -0
- package/src/detectors/python.js +57 -58
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-compass",
|
|
3
|
-
"version": "4.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.3.3",
|
|
4
|
+
"description": "\ud83e\udded Futuristic TUI workspace navigator & runner - AI-powered project detection for Node, Python, Rust, Go, Java, PHP, Ruby, .NET",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"project-compass": "src/cli.js"
|
package/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import {fileURLToPath} from 'url';
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import kleur from 'kleur';
|
|
9
9
|
import {execa} from 'execa';
|
|
10
|
-
import {discoverProjects
|
|
10
|
+
import {discoverProjects} from './projectDetection.js';
|
|
11
11
|
import {CONFIG_PATH, ensureConfigDir} from './configPaths.js';
|
|
12
12
|
|
|
13
13
|
// Modular Components
|
|
@@ -696,7 +696,33 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
696
696
|
{label: 'Management', color: 'cyan', body: ['Shift+P Package Registry', 'Shift+N Project Architect', 'Shift+X clear / Shift+E export']},
|
|
697
697
|
{label: 'Orbit & AI', color: 'yellow', body: ['Shift+T: Tasks, Shift+O: AI, 0: Analyze', 'Shift+A studio / Shift+O AI Horizon', 'Shift+S structure / Shift+Q quit']}
|
|
698
698
|
].map((card, idx) => create(Box, {key: card.label, flexGrow: 1, flexBasis: 0, minWidth: HELP_CARD_MIN_WIDTH, marginRight: idx < 2 ? 1 : 0, marginBottom: 1, borderStyle: 'round', borderColor: card.color, padding: 1, flexDirection: 'column'}, create(Text, {color: card.color, bold: true, marginBottom: 1}, card.label), ...card.body.map((line, lidx) => create(Text, {key: lidx, dimColor: card.color === 'yellow'}, line))))),
|
|
699
|
-
config.showStructureGuide && create(Box, {key: 'structure', flexDirection: 'column', borderStyle: 'round', borderColor: 'blue', marginTop: 1, padding: 1},
|
|
699
|
+
config.showStructureGuide && create(Box, {key: 'structure', flexDirection: 'column', borderStyle: 'round', borderColor: 'blue', marginTop: 1, padding: 1},
|
|
700
|
+
create(Box, {flexDirection: 'row', justifyContent: 'space-between', marginBottom: 0},
|
|
701
|
+
create(Text, {color: 'cyan', bold: true}, '๐ Structure Guide ยท Press Shift+S to hide'),
|
|
702
|
+
create(Text, {dimColor: true}, `${projects.length} project${projects.length !== 1 ? 's' : ''} detected`)
|
|
703
|
+
),
|
|
704
|
+
projects.length === 0 && create(Text, {dimColor: true, marginTop: 0}, 'No projects detected yet...'),
|
|
705
|
+
projects.length > 0 && projects.map((p, idx) =>
|
|
706
|
+
create(Box, {key: p.id, flexDirection: 'column', marginBottom: 0,
|
|
707
|
+
borderStyle: 'single', borderColor: p.type === 'Node.js' ? 'cyan' :
|
|
708
|
+
p.type === 'Python' ? 'green' :
|
|
709
|
+
p.type === 'Rust' ? 'red' :
|
|
710
|
+
p.type === 'Go' ? 'blue' :
|
|
711
|
+
p.type === 'Java' ? 'yellow' :
|
|
712
|
+
p.type === 'PHP' ? 'purple' :
|
|
713
|
+
p.type === 'Ruby' ? 'red' : 'gray',
|
|
714
|
+
paddingX: 1, paddingY: 0},
|
|
715
|
+
create(Box, {flexDirection: 'row', alignItems: 'center'},
|
|
716
|
+
create(Text, {bold: true, color: selectedIndex === idx ? 'cyan' : 'white'},
|
|
717
|
+
`${selectedIndex === idx ? 'โ ' : ' '}${p.icon} ${p.name}`),
|
|
718
|
+
p.missingBinaries && p.missingBinaries.length > 0 && create(Text, {color: 'red'}, ' โ ')
|
|
719
|
+
),
|
|
720
|
+
create(Text, {dimColor: true}, ` ${p.type} ยท ${path.relative(rootPath, p.path) || '.'}`),
|
|
721
|
+
p.frameworks && p.frameworks.length > 0 && create(Text, {dimColor: true}, ` Frameworks: ${p.frameworks.map(f => `${f.icon} ${f.name}`).join(', ')}`),
|
|
722
|
+
p.description && create(Text, {dimColor: true}, ` ${p.description}`)
|
|
723
|
+
)
|
|
724
|
+
)
|
|
725
|
+
),
|
|
700
726
|
showHelp && create(Box, {key: 'overlay', flexDirection: 'column', borderStyle: 'double', borderColor: 'cyan', marginTop: 1, padding: 1}, create(Text, {color: 'cyan', bold: true}, 'Help overlay'), create(Text, null, 'Shift+โ/โ scrolls logs; Shift+X clears; Shift+E exports; Shift+A Studio; Shift+T Tasks; Shift+D Detach; Shift+B Toggle Art Board; Shift+P Packages; Shift+N Creator; Shift+O AI Horizon.'))
|
|
701
727
|
];
|
|
702
728
|
return create(Box, {flexDirection: 'column'}, ...navigatorBody);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
/* global fetch */
|
|
1
|
+
/* global fetch, setTimeout */
|
|
2
2
|
import React, {useState, memo} from 'react';
|
|
3
3
|
import {Box, Text, useInput} from 'ink';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
4
6
|
|
|
5
7
|
const create = React.createElement;
|
|
6
8
|
|
|
@@ -20,35 +22,99 @@ const AIHorizon = memo(({selectedProject, CursorText, config, setConfig, saveCon
|
|
|
20
22
|
const [status, setStatus] = useState('ready');
|
|
21
23
|
const [error, setError] = useState(null);
|
|
22
24
|
const [suggestions, setSuggestions] = useState([]);
|
|
25
|
+
const [editingIdx, setEditingIdx] = useState(-1);
|
|
26
|
+
const [editInput, setEditInput] = useState('');
|
|
27
|
+
const [editCursor, setEditCursor] = useState(0);
|
|
28
|
+
const [customContext, setCustomContext] = useState('');
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const readProjectFile = (filePath) => {
|
|
32
|
+
try {
|
|
33
|
+
const fullPath = selectedProject?.path ? path.join(selectedProject.path, filePath) : filePath;
|
|
34
|
+
if (fs.existsSync(fullPath)) {
|
|
35
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
36
|
+
return content.slice(0, 2000); // First 2000 chars
|
|
37
|
+
}
|
|
38
|
+
} catch { /* ignore */ }
|
|
39
|
+
return '';
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const buildProjectContext = () => {
|
|
43
|
+
if (!selectedProject) return null;
|
|
44
|
+
const pm = selectedProject.metadata?.packageManager || 'npm';
|
|
45
|
+
const frameworks = (selectedProject.frameworks || []).map(f => f.name).join(', ');
|
|
46
|
+
const scripts = selectedProject.metadata?.scripts || {};
|
|
47
|
+
const deps = (selectedProject.metadata?.dependencies || []).slice(0, 30);
|
|
48
|
+
|
|
49
|
+
// Read actual project files
|
|
50
|
+
const readme = readProjectFile('README.md') || readProjectFile('readme.md') || '';
|
|
51
|
+
const mainFile = readProjectFile('main.py') || readProjectFile('app.py') ||
|
|
52
|
+
readProjectFile('src/main.py') || readProjectFile('index.js') || '';
|
|
53
|
+
const configFile = readProjectFile('pyproject.toml') || readProjectFile('package.json') || '';
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
name: selectedProject.name,
|
|
57
|
+
type: selectedProject.type,
|
|
58
|
+
path: selectedProject.path,
|
|
59
|
+
manifest: selectedProject.manifest,
|
|
60
|
+
packageManager: pm,
|
|
61
|
+
frameworks: frameworks || 'None detected',
|
|
62
|
+
scripts: scripts,
|
|
63
|
+
scriptNames: Object.keys(scripts).join(', ') || 'None',
|
|
64
|
+
dependencies: deps,
|
|
65
|
+
description: selectedProject.description || '',
|
|
66
|
+
hasPort: !!selectedProject.metadata?.port,
|
|
67
|
+
readme: readme.slice(0, 1500),
|
|
68
|
+
mainFile: mainFile.slice(0, 1500),
|
|
69
|
+
configFile: configFile.slice(0, 1500),
|
|
70
|
+
customContext
|
|
71
|
+
};
|
|
72
|
+
};
|
|
23
73
|
|
|
24
74
|
const runRealAnalysis = async () => {
|
|
25
75
|
setStatus('busy');
|
|
26
76
|
setError(null);
|
|
27
77
|
const provider = AI_PROVIDERS[providerIdx];
|
|
78
|
+
const context = buildProjectContext();
|
|
79
|
+
|
|
80
|
+
if (!context) {
|
|
81
|
+
setError('No project selected for analysis');
|
|
82
|
+
setStatus('ready');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
28
85
|
|
|
29
86
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
87
|
+
const prompt = `You are analyzing a ${context.type} project named "${context.name}".
|
|
88
|
+
Project Details:
|
|
89
|
+
- Package Manager: ${context.packageManager}
|
|
90
|
+
- Frameworks: ${context.frameworks}
|
|
91
|
+
- Available Scripts: ${context.scriptNames}
|
|
92
|
+
- Key Dependencies: ${JSON.stringify(context.dependencies)}
|
|
93
|
+
- Description: ${context.description}
|
|
94
|
+
|
|
95
|
+
Based on this REAL project data, suggest the correct CLI commands for:
|
|
96
|
+
1. BUILD (compile/build the project)
|
|
97
|
+
2. RUN (start/run the project in development)
|
|
98
|
+
3. INSTALL (install dependencies)
|
|
99
|
+
4. TEST (run tests)
|
|
37
100
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
101
|
+
CRITICAL RULES:
|
|
102
|
+
- Use the ACTUAL package manager: ${context.packageManager}
|
|
103
|
+
- Use ACTUAL script names from: ${context.scriptNames}
|
|
104
|
+
- For Python: use "uv run" if uv is available, else "python" or "pip"
|
|
105
|
+
- For Node: use "npm run", "yarn", "pnpm" or "bun" based on ${context.packageManager}
|
|
106
|
+
- For Rust: use "cargo" commands
|
|
107
|
+
- For Go: use "go" commands
|
|
108
|
+
- DO NOT suggest generic commands - use the project's actual tools
|
|
43
109
|
|
|
44
|
-
|
|
110
|
+
Return ONLY valid JSON (no markdown, no code blocks):
|
|
111
|
+
{"build": "exact command here", "run": "exact command here", "install": "exact command here", "test": "exact command here"}
|
|
45
112
|
|
|
46
|
-
|
|
47
|
-
Use the project's detected type (${selectedProject.type}) to ensure commands are correct (e.g., npm, pip, cargo).`;
|
|
113
|
+
Example for Node.js with npm: {"build": "npm run build", "run": "npm run dev", "install": "npm install", "test": "npm run test"}`;
|
|
48
114
|
|
|
49
115
|
let response;
|
|
50
116
|
let aiText = '';
|
|
51
|
-
|
|
117
|
+
|
|
52
118
|
if (provider.id === 'openrouter') {
|
|
53
119
|
response = await fetch(provider.endpoint, {
|
|
54
120
|
method: 'POST',
|
|
@@ -103,33 +169,55 @@ Use the project's detected type (${selectedProject.type}) to ensure commands are
|
|
|
103
169
|
if (!response.ok) throw new Error(data.error || 'Ollama Error');
|
|
104
170
|
aiText = data.response;
|
|
105
171
|
}
|
|
106
|
-
|
|
107
|
-
const jsonMatch = aiText.match(/{
|
|
108
|
-
if (!jsonMatch) throw new Error("AI returned invalid
|
|
172
|
+
|
|
173
|
+
const jsonMatch = aiText.match(/{[\s\S]*?}/);
|
|
174
|
+
if (!jsonMatch) throw new Error("AI returned invalid JSON format.");
|
|
109
175
|
|
|
110
176
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
177
|
+
|
|
178
|
+
// Validate commands are arrays
|
|
111
179
|
const mapped = [
|
|
112
|
-
{ label: '
|
|
113
|
-
{ label: '
|
|
114
|
-
{ label: '
|
|
115
|
-
{ label: '
|
|
116
|
-
];
|
|
117
|
-
|
|
180
|
+
{ key: 'build', label: 'Build', command: parsed.build?.split(' ').filter(Boolean) || [] },
|
|
181
|
+
{ key: 'run', label: 'Run', command: parsed.run?.split(' ').filter(Boolean) || [] },
|
|
182
|
+
{ key: 'install', label: 'Install', command: parsed.install?.split(' ').filter(Boolean) || [] },
|
|
183
|
+
{ key: 'test', label: 'Test', command: parsed.test?.split(' ').filter(Boolean) || [] }
|
|
184
|
+
].filter(cmd => cmd.command.length > 0);
|
|
185
|
+
|
|
118
186
|
setSuggestions(mapped);
|
|
119
|
-
|
|
120
|
-
const currentCustom = config.customCommands?.[projectKey] || [];
|
|
121
|
-
const nextConfig = {
|
|
122
|
-
...config,
|
|
123
|
-
customCommands: { ...config.customCommands, [projectKey]: [...currentCustom, ...mapped] }
|
|
124
|
-
};
|
|
125
|
-
setConfig(nextConfig); saveConfig(nextConfig);
|
|
126
|
-
setStatus('done');
|
|
187
|
+
setStatus('review'); // NOW go to review step instead of auto-saving
|
|
127
188
|
} catch (err) {
|
|
128
189
|
setError(err.message);
|
|
129
190
|
setStatus('ready');
|
|
130
191
|
}
|
|
131
192
|
};
|
|
132
|
-
|
|
193
|
+
|
|
194
|
+
const saveSuggestions = () => {
|
|
195
|
+
if (!selectedProject || suggestions.length === 0) return;
|
|
196
|
+
|
|
197
|
+
const projectKey = selectedProject.path;
|
|
198
|
+
const currentCustom = config.customCommands?.[projectKey] || [];
|
|
199
|
+
|
|
200
|
+
// Convert suggestions to proper format and add to custom commands
|
|
201
|
+
const newCommands = suggestions.map(cmd => ({
|
|
202
|
+
label: `AI ${cmd.label}`,
|
|
203
|
+
command: cmd.command,
|
|
204
|
+
source: 'ai'
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
const nextConfig = {
|
|
208
|
+
...config,
|
|
209
|
+
customCommands: {
|
|
210
|
+
...config.customCommands,
|
|
211
|
+
[projectKey]: [...currentCustom, ...newCommands]
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
setConfig(nextConfig);
|
|
215
|
+
saveConfig(nextConfig);
|
|
216
|
+
setStatus('saved');
|
|
217
|
+
|
|
218
|
+
setTimeout(() => setStatus('ready'), 2000);
|
|
219
|
+
};
|
|
220
|
+
|
|
133
221
|
useInput((input, key) => {
|
|
134
222
|
if (step === 'provider') {
|
|
135
223
|
if (key.upArrow) setProviderIdx(p => (p - 1 + AI_PROVIDERS.length) % AI_PROVIDERS.length);
|
|
@@ -167,25 +255,93 @@ Use the project's detected type (${selectedProject.type}) to ensure commands are
|
|
|
167
255
|
if (key.return && status === 'ready' && selectedProject) {
|
|
168
256
|
runRealAnalysis();
|
|
169
257
|
}
|
|
258
|
+
if (key.return && status === 'review') {
|
|
259
|
+
// Toggle edit mode for a suggestion
|
|
260
|
+
if (editingIdx >= 0) {
|
|
261
|
+
const updated = [...suggestions];
|
|
262
|
+
updated[editingIdx] = { ...updated[editingIdx], command: editInput.split(' ').filter(Boolean) };
|
|
263
|
+
setSuggestions(updated);
|
|
264
|
+
setEditingIdx(-1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (input === 's' && status === 'review') {
|
|
268
|
+
saveSuggestions();
|
|
269
|
+
}
|
|
270
|
+
if (input === 'e' && status === 'review' && suggestions.length > 0) {
|
|
271
|
+
setEditingIdx(0);
|
|
272
|
+
setEditInput(suggestions[0].command.join(' '));
|
|
273
|
+
setEditCursor(suggestions[0].command.join(' ').length);
|
|
274
|
+
}
|
|
275
|
+
if (key.upArrow && status === 'review' && editingIdx < 0) {
|
|
276
|
+
setEditingIdx(p => Math.max(0, p - 1));
|
|
277
|
+
if (suggestions[Math.max(0, editingIdx - 1)]) {
|
|
278
|
+
setEditInput(suggestions[Math.max(0, editingIdx - 1)].command.join(' '));
|
|
279
|
+
setEditCursor(suggestions[Math.max(0, editingIdx - 1)].command.join(' ').length);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (key.downArrow && status === 'review' && editingIdx < 0) {
|
|
283
|
+
setEditingIdx(p => Math.min(suggestions.length - 1, p + 1));
|
|
284
|
+
if (suggestions[Math.min(suggestions.length - 1, editingIdx + 1)]) {
|
|
285
|
+
setEditInput(suggestions[Math.min(suggestions.length - 1, editingIdx + 1)].command.join(' '));
|
|
286
|
+
setEditCursor(suggestions[Math.min(suggestions.length - 1, editingIdx + 1)].command.join(' ').length);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (key.escape) {
|
|
290
|
+
if (editingIdx >= 0) {
|
|
291
|
+
setEditingIdx(-1);
|
|
292
|
+
} else if (status === 'review') {
|
|
293
|
+
setStatus('ready');
|
|
294
|
+
setSuggestions([]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
170
297
|
if (input === 'r') {
|
|
171
298
|
const nextConfig = { ...config, aiToken: '' };
|
|
172
299
|
setConfig(nextConfig); saveConfig(nextConfig);
|
|
173
300
|
setStep('provider');
|
|
174
301
|
}
|
|
175
302
|
}
|
|
303
|
+
|
|
304
|
+
// Handle editing input
|
|
305
|
+
if (editingIdx >= 0) {
|
|
306
|
+
if (key.return) {
|
|
307
|
+
const updated = [...suggestions];
|
|
308
|
+
updated[editingIdx] = { ...updated[editingIdx], command: editInput.split(' ').filter(Boolean) };
|
|
309
|
+
setSuggestions(updated);
|
|
310
|
+
setEditingIdx(-1);
|
|
311
|
+
}
|
|
312
|
+
if (key.escape) {
|
|
313
|
+
setEditingIdx(-1);
|
|
314
|
+
}
|
|
315
|
+
if (key.backspace || key.delete) {
|
|
316
|
+
if (editCursor > 0) {
|
|
317
|
+
setEditInput(prev => prev.slice(0, editCursor - 1) + prev.slice(editCursor));
|
|
318
|
+
setEditCursor(c => Math.max(0, c - 1));
|
|
319
|
+
}
|
|
320
|
+
} else if (key.leftArrow) {
|
|
321
|
+
setEditCursor(c => Math.max(0, c - 1));
|
|
322
|
+
} else if (key.rightArrow) {
|
|
323
|
+
setEditCursor(c => Math.min(editInput.length, c + 1));
|
|
324
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
325
|
+
setEditInput(prev => prev.slice(0, editCursor) + input + prev.slice(editCursor));
|
|
326
|
+
setEditCursor(c => c + input.length);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
176
329
|
});
|
|
177
330
|
|
|
331
|
+
const context = selectedProject ? buildProjectContext() : null;
|
|
332
|
+
|
|
178
333
|
return create(
|
|
179
334
|
Box,
|
|
180
335
|
{flexDirection: 'column', borderStyle: 'double', borderColor: 'magenta', padding: 1, width: '100%'},
|
|
181
|
-
create(Text, {bold: true, color: 'magenta'}, '๐ค AI Horizon |
|
|
336
|
+
create(Text, {bold: true, color: 'magenta'}, '๐ค AI Horizon | Project Intelligence'),
|
|
337
|
+
create(Text, {dimColor: true}, 'Real AI analysis with project context\n'),
|
|
182
338
|
|
|
183
339
|
step === 'provider' && create(
|
|
184
340
|
Box,
|
|
185
341
|
{flexDirection: 'column'},
|
|
186
|
-
create(Text, {bold: true, marginBottom: 1}, 'Step 1: Select AI
|
|
342
|
+
create(Text, {bold: true, marginBottom: 1}, 'Step 1: Select AI Provider'),
|
|
187
343
|
...AI_PROVIDERS.map((p, i) => create(Text, {key: p.id, color: i === providerIdx ? 'cyan' : 'white'}, (i === providerIdx ? 'โ ' : ' ') + p.name)),
|
|
188
|
-
create(Text, {dimColor: true, marginTop: 1}, 'Enter:
|
|
344
|
+
create(Text, {dimColor: true, marginTop: 1}, 'โ/โ: Navigate, Enter: Select')
|
|
189
345
|
),
|
|
190
346
|
|
|
191
347
|
step === 'model' && create(
|
|
@@ -193,40 +349,76 @@ Use the project's detected type (${selectedProject.type}) to ensure commands are
|
|
|
193
349
|
{flexDirection: 'column'},
|
|
194
350
|
create(Text, {bold: true, color: 'yellow', marginBottom: 1}, 'Step 2: Model Configuration'),
|
|
195
351
|
create(Box, {flexDirection: 'row'},
|
|
196
|
-
create(Text, null, 'Model
|
|
352
|
+
create(Text, null, 'Model: '),
|
|
197
353
|
create(CursorText, {value: model, cursorIndex: cursor})
|
|
198
354
|
),
|
|
199
|
-
create(Text, {dimColor: true, marginTop: 1}, 'Enter: Save
|
|
355
|
+
create(Text, {dimColor: true, marginTop: 1}, 'Enter: Save, Esc: Back')
|
|
200
356
|
),
|
|
201
357
|
|
|
202
358
|
step === 'token' && create(
|
|
203
359
|
Box,
|
|
204
360
|
{flexDirection: 'column'},
|
|
205
|
-
create(Text, {bold: true, color: 'red', marginBottom: 1}, 'Step 3: API Token
|
|
361
|
+
create(Text, {bold: true, color: 'red', marginBottom: 1}, 'Step 3: API Token'),
|
|
206
362
|
create(Box, {flexDirection: 'row'},
|
|
207
363
|
create(Text, null, 'Token: '),
|
|
208
364
|
create(CursorText, {value: '*'.repeat(token.length), cursorIndex: cursor})
|
|
209
365
|
),
|
|
210
|
-
create(Text, {dimColor: true, marginTop: 1}, 'Enter: Save
|
|
366
|
+
create(Text, {dimColor: true, marginTop: 1}, 'Enter: Save, Esc: Back')
|
|
211
367
|
),
|
|
212
368
|
|
|
213
369
|
step === 'analyze' && create(
|
|
214
370
|
Box,
|
|
215
371
|
{flexDirection: 'column'},
|
|
216
|
-
create(Text, {bold: true, color: 'cyan', marginBottom: 1},
|
|
217
|
-
|
|
372
|
+
create(Text, {bold: true, color: 'cyan', marginBottom: 1},
|
|
373
|
+
'Project: ' + (selectedProject ? selectedProject.name : 'No project selected')),
|
|
218
374
|
|
|
219
|
-
create(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
create(Text, {dimColor: true, marginTop: 1}, 'Return to Navigator to use BRIT shortcuts.')
|
|
226
|
-
),
|
|
227
|
-
error && create(Text, {color: 'red', bold: true, marginTop: 1}, ' โ AI ERROR: ' + error)
|
|
375
|
+
context && create(
|
|
376
|
+
Box,
|
|
377
|
+
{flexDirection: 'column', marginBottom: 1},
|
|
378
|
+
create(Text, {dimColor: true}, `Type: ${context.type} | PM: ${context.packageManager}`),
|
|
379
|
+
create(Text, {dimColor: true}, `Frameworks: ${context.frameworks}`),
|
|
380
|
+
create(Text, {dimColor: true}, `Scripts: ${context.scriptNames}`)
|
|
228
381
|
),
|
|
229
|
-
|
|
382
|
+
|
|
383
|
+
status === 'ready' && create(
|
|
384
|
+
Box,
|
|
385
|
+
{flexDirection: 'column'},
|
|
386
|
+
create(Text, null, 'Press Enter to analyze with AI using real project context'),
|
|
387
|
+
create(Text, {dimColor: true}, 'Provider: ' + config.aiProvider + ' | Model: ' + config.aiModel)
|
|
388
|
+
),
|
|
389
|
+
|
|
390
|
+
status === 'busy' && create(Text, {color: 'yellow', marginTop: 1}, ' โณ Analyzing project with AI...'),
|
|
391
|
+
|
|
392
|
+
status === 'review' && create(
|
|
393
|
+
Box,
|
|
394
|
+
{flexDirection: 'column', marginTop: 1},
|
|
395
|
+
create(Text, {bold: true, color: 'green'}, 'โ
AI Suggestions (Review & Edit):'),
|
|
396
|
+
...suggestions.map((cmd, idx) => {
|
|
397
|
+
const isEditing = editingIdx === idx;
|
|
398
|
+
return create(
|
|
399
|
+
Box,
|
|
400
|
+
{key: cmd.key, flexDirection: 'column', marginBottom: 0,
|
|
401
|
+
borderStyle: isEditing ? 'double' : 'single',
|
|
402
|
+
borderColor: isEditing ? 'yellow' : (idx === editingIdx ? 'cyan' : 'gray'),
|
|
403
|
+
paddingX: 1},
|
|
404
|
+
create(Text, {bold: true, color: idx === editingIdx ? 'cyan' : 'white'},
|
|
405
|
+
(idx === editingIdx ? 'โ ' : ' ') + cmd.label + ':'),
|
|
406
|
+
isEditing
|
|
407
|
+
? create(CursorText, {value: editInput, cursorIndex: editCursor})
|
|
408
|
+
: create(Text, {dimColor: true}, ' ' + cmd.command.join(' ')),
|
|
409
|
+
);
|
|
410
|
+
}),
|
|
411
|
+
create(Box, {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'},
|
|
412
|
+
create(Text, {dimColor: true}, 'โ/โ: Select | E: Edit | S: Save to Config'),
|
|
413
|
+
create(Text, {color: 'green'}, 'Esc: Cancel')
|
|
414
|
+
)
|
|
415
|
+
),
|
|
416
|
+
|
|
417
|
+
status === 'saved' && create(Text, {color: 'green', bold: true, marginTop: 1}, 'โ
Commands saved to config!'),
|
|
418
|
+
|
|
419
|
+
error && create(Text, {color: 'red', bold: true, marginTop: 1}, 'โ Error: ' + error),
|
|
420
|
+
|
|
421
|
+
create(Text, {dimColor: true, marginTop: 1}, 'Esc: Back | R: Reset Auth')
|
|
230
422
|
)
|
|
231
423
|
);
|
|
232
424
|
});
|
|
@@ -2,22 +2,80 @@ import React, {memo} from 'react';
|
|
|
2
2
|
import {Box, Text} from 'ink';
|
|
3
3
|
|
|
4
4
|
const create = React.createElement;
|
|
5
|
+
const STATUC_COLORS = { running: 'green', finished: 'cyan', failed: 'red', killed: 'yellow' };
|
|
5
6
|
|
|
6
7
|
const TaskManager = memo(({tasks, activeTaskId, renameMode, renameInput, renameCursor, CursorText}) => {
|
|
8
|
+
const activeTask = tasks.find(t => t.id === activeTaskId);
|
|
9
|
+
const logLines = activeTask?.logs || [];
|
|
10
|
+
const visibleLogs = logLines.slice(-5);
|
|
11
|
+
|
|
7
12
|
return create(
|
|
8
13
|
Box,
|
|
9
|
-
{flexDirection: 'column', borderStyle: '
|
|
10
|
-
|
|
11
|
-
create(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
{flexDirection: 'column', borderStyle: 'double', borderColor: 'yellow', padding: 1, height: '100%'},
|
|
15
|
+
// Header
|
|
16
|
+
create(Box, {flexDirection: 'row', justifyContent: 'space-between', marginBottom: 1},
|
|
17
|
+
create(Box, {flexDirection: 'row', alignItems: 'center'},
|
|
18
|
+
create(Text, {bold: true, color: 'yellow'}, '๐ฐ๏ธ '),
|
|
19
|
+
create(Text, {bold: true, color: 'yellow'}, 'Orbit Task Manager'),
|
|
20
|
+
create(Text, {dimColor: true}, ` | ${tasks.length} task${tasks.length !== 1 ? 's' : ''}`)
|
|
21
|
+
),
|
|
22
|
+
tasks.length > 0 && create(Text, {color: 'cyan', bold: true},
|
|
23
|
+
`[${tasks.filter(t => t.status === 'running').length} ACTIVE]`)
|
|
24
|
+
),
|
|
25
|
+
|
|
26
|
+
// Task List
|
|
27
|
+
create(Box, {flexDirection: 'column', flexGrow: 1},
|
|
28
|
+
...tasks.map((t, idx) => {
|
|
29
|
+
const isActive = t.id === activeTaskId;
|
|
30
|
+
const color = STATUC_COLORS[t.status] || 'white';
|
|
31
|
+
return create(
|
|
32
|
+
Box,
|
|
33
|
+
{key: t.id, marginBottom: 0,
|
|
34
|
+
borderStyle: isActive ? 'bold' : 'single',
|
|
35
|
+
borderColor: isActive ? color : 'gray',
|
|
36
|
+
paddingX: 1, paddingY: 0},
|
|
37
|
+
create(Box, {flexDirection: 'column'},
|
|
38
|
+
// Task header
|
|
39
|
+
isActive && renameMode
|
|
40
|
+
? create(Box, {flexDirection: 'row'},
|
|
41
|
+
create(Text, {color: 'cyan'}, 'โ Rename: '),
|
|
42
|
+
create(CursorText, {value: renameInput, cursorIndex: renameCursor}))
|
|
43
|
+
: create(Box, {flexDirection: 'row', justifyContent: 'space-between'},
|
|
44
|
+
create(Box, {flexDirection: 'row', alignItems: 'center'},
|
|
45
|
+
create(Text, {color: isActive ? color : 'white', bold: isActive},
|
|
46
|
+
`${isActive ? 'โ ' : ' '}[${t.status.toUpperCase()}] ${t.name}`),
|
|
47
|
+
t.status === 'running' && create(Text, {color: 'green'}, ' โก'),
|
|
48
|
+
t.status === 'failed' && create(Text, {color: 'red'}, ' โ'),
|
|
49
|
+
t.status === 'killed' && create(Text, {color: 'yellow'}, ' โ ')
|
|
50
|
+
),
|
|
51
|
+
create(Text, {dimColor: true}, `#${idx + 1}`)
|
|
52
|
+
),
|
|
53
|
+
// Mini log preview for active task
|
|
54
|
+
isActive && logLines.length > 0 && create(
|
|
55
|
+
Box, {marginTop: 0, paddingLeft: 2},
|
|
56
|
+
...visibleLogs.map((line, i) =>
|
|
57
|
+
create(Text, {key: i, dimColor: true},
|
|
58
|
+
`${i === visibleLogs.length - 1 ? '>' : 'ยท'} ${line.slice(0, 60)}`)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
}),
|
|
64
|
+
!tasks.length && create(
|
|
65
|
+
Box, {alignItems: 'center', justifyContent: 'center', flexGrow: 1},
|
|
66
|
+
create(Text, {dimColor: true, bold: true}, 'No active tasks'),
|
|
67
|
+
create(Text, {dimColor: true}, 'Run a command to see it here!')
|
|
68
|
+
)
|
|
69
|
+
),
|
|
70
|
+
|
|
71
|
+
// Footer
|
|
72
|
+
create(Box, {marginTop: 1, flexDirection: 'column'},
|
|
73
|
+
create(Box, {flexDirection: 'row', justifyContent: 'space-between'},
|
|
74
|
+
create(Text, {dimColor: true}, 'โ/โ: Navigate | Enter: Select'),
|
|
75
|
+
create(Text, {dimColor: true}, 'Shift+K: Kill | Shift+R: Rename')
|
|
76
|
+
),
|
|
77
|
+
create(Text, {dimColor: true}, 'Press Enter or Shift+T to return to Navigator.')
|
|
78
|
+
)
|
|
21
79
|
);
|
|
22
80
|
});
|
|
23
81
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compass Config Loader
|
|
3
|
+
* Loads project-specific config from compass.config.js if it exists
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
export async function loadProjectConfig(projectPath) {
|
|
9
|
+
const configPath = path.join(projectPath, 'compass.config.js');
|
|
10
|
+
if (!fs.existsSync(configPath)) return null;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Dynamic import for ESM
|
|
14
|
+
const config = await import(`file://${configPath}?t=${Date.now()}`);
|
|
15
|
+
return config.default || config;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.error(`Failed to load compass.config.js: ${e.message}`);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function saveProjectConfig(projectPath, config) {
|
|
23
|
+
const configPath = path.join(projectPath, 'compass.config.js');
|
|
24
|
+
const projectName = path.basename(projectPath);
|
|
25
|
+
const content = `/**
|
|
26
|
+
* Compass Configuration for ${projectName}
|
|
27
|
+
* Generated by AI Horizon
|
|
28
|
+
*/
|
|
29
|
+
export default ${JSON.stringify(config, null, 2)};
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
fs.writeFileSync(configPath, content, 'utf-8');
|
|
34
|
+
return true;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(`Failed to save compass.config.js: ${e.message}`);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/detectors/python.js
CHANGED
|
@@ -23,27 +23,38 @@ function getPythonPackageManager(projectPath) {
|
|
|
23
23
|
|
|
24
24
|
function gatherPythonDependencies(projectPath) {
|
|
25
25
|
const deps = new Set();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
|
|
27
|
+
// Only read requirements.txt files - NOT .py files!
|
|
28
|
+
const reqPath = path.join(projectPath, 'requirements.txt');
|
|
29
|
+
if (fs.existsSync(reqPath)) {
|
|
30
|
+
const raw = fs.readFileSync(reqPath, 'utf-8');
|
|
29
31
|
raw.split(/\r?\n/).forEach((line) => {
|
|
30
32
|
const clean = line.trim().split('#')[0].trim();
|
|
31
33
|
if (!clean || clean.startsWith('-') || clean.startsWith('"') || clean.startsWith("'")) return;
|
|
32
34
|
const match = clean.match(/^([a-zA-Z0-9_.-]+)/);
|
|
33
35
|
if (match) deps.add(match[1].toLowerCase());
|
|
34
36
|
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const reqDevPath = path.join(projectPath, 'requirements-dev.txt');
|
|
40
|
+
if (fs.existsSync(reqDevPath)) {
|
|
41
|
+
const raw = fs.readFileSync(reqDevPath, 'utf-8');
|
|
42
|
+
raw.split(/\r?\n/).forEach((line) => {
|
|
43
|
+
const clean = line.trim().split('#')[0].trim();
|
|
44
|
+
if (!clean || clean.startsWith('-') || clean.startsWith('"') || clean.startsWith("'")) return;
|
|
45
|
+
const match = clean.match(/^([a-zA-Z0-9_.-]+)/);
|
|
46
|
+
if (match) deps.add(match[1].toLowerCase());
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Only read pyproject.toml dependencies section - NOT .py files!
|
|
40
51
|
const pyproject = path.join(projectPath, 'pyproject.toml');
|
|
41
52
|
if (fs.existsSync(pyproject)) {
|
|
42
53
|
const content = fs.readFileSync(pyproject, 'utf-8');
|
|
43
54
|
const depSection = content.match(/(?:dependencies|requires)\s*=\s*\[([^\]]+)\]/g);
|
|
44
55
|
if (depSection) {
|
|
45
56
|
depSection.forEach((section) => {
|
|
46
|
-
const matches = section.match(/["']([^"']+)
|
|
57
|
+
const matches = section.match(/["']([^"']+)/g);
|
|
47
58
|
if (matches) {
|
|
48
59
|
matches.forEach((m) => {
|
|
49
60
|
const dep = m.replace(/["']/g, '').split(/[>=<=~!]/)[0].trim();
|
|
@@ -53,43 +64,41 @@ function gatherPythonDependencies(projectPath) {
|
|
|
53
64
|
});
|
|
54
65
|
}
|
|
55
66
|
}
|
|
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
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
67
|
+
|
|
69
68
|
return Array.from(deps);
|
|
70
69
|
}
|
|
71
70
|
|
|
71
|
+
// Uses EXACT matching - NOT substring!
|
|
72
72
|
function detectPythonFramework(deps) {
|
|
73
73
|
const frameworks = [];
|
|
74
|
-
const depStr = deps.join(' ').toLowerCase();
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
if (
|
|
88
|
-
if (
|
|
89
|
-
if (
|
|
90
|
-
if (
|
|
91
|
-
if (
|
|
92
|
-
if (
|
|
75
|
+
const hasDep = (pattern) =>
|
|
76
|
+
deps.some((dep) => {
|
|
77
|
+
const depLower = dep.toLowerCase();
|
|
78
|
+
return depLower === pattern.toLowerCase() ||
|
|
79
|
+
depLower.startsWith(pattern.toLowerCase() + '==') ||
|
|
80
|
+
depLower.startsWith(pattern.toLowerCase() + '>=') ||
|
|
81
|
+
depLower.startsWith(pattern.toLowerCase() + '<=') ||
|
|
82
|
+
depLower.startsWith(pattern.toLowerCase() + '~=');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (hasDep('fastapi')) frameworks.push({ name: 'FastAPI', icon: 'โก' });
|
|
86
|
+
if (hasDep('flask')) frameworks.push({ name: 'Flask', icon: '๐ถ๏ธ' });
|
|
87
|
+
if (hasDep('django')) frameworks.push({ name: 'Django', icon: '๐ฟ' });
|
|
88
|
+
if (hasDep('tornado')) frameworks.push({ name: 'Tornado', icon: '๐ช๏ธ' });
|
|
89
|
+
if (hasDep('aiohttp')) frameworks.push({ name: 'AioHTTP', icon: '๐' });
|
|
90
|
+
if (hasDep('sanic')) frameworks.push({ name: 'Sanic', icon: '๐' });
|
|
91
|
+
if (hasDep('pyramid')) frameworks.push({ name: 'Pyramid', icon: '๐บ' });
|
|
92
|
+
if (hasDep('falcon')) frameworks.push({ name: 'Falcon', icon: '๐ฆ
' });
|
|
93
|
+
if (hasDep('starlette')) frameworks.push({ name: 'Starlette', icon: 'โญ' });
|
|
94
|
+
if (hasDep('pandas')) frameworks.push({ name: 'Pandas', icon: '๐ผ' });
|
|
95
|
+
if (hasDep('numpy')) frameworks.push({ name: 'NumPy', icon: '๐ข' });
|
|
96
|
+
if (hasDep('scipy')) frameworks.push({ name: 'SciPy', icon: '๐ฌ' });
|
|
97
|
+
if (hasDep('torch') || hasDep('pytorch')) frameworks.push({ name: 'PyTorch', icon: '๐ฅ' });
|
|
98
|
+
if (hasDep('tensorflow')) frameworks.push({ name: 'TensorFlow', icon: '๐ง ' });
|
|
99
|
+
if (hasDep('sqlalchemy')) frameworks.push({ name: 'SQLAlchemy', icon: '๐๏ธ' });
|
|
100
|
+
if (hasDep('pytest')) frameworks.push({ name: 'Pytest', icon: 'โ
' });
|
|
101
|
+
if (hasDep('celery')) frameworks.push({ name: 'Celery', icon: '๐ฅฌ' });
|
|
93
102
|
|
|
94
103
|
return frameworks;
|
|
95
104
|
}
|
|
@@ -108,8 +117,10 @@ export default {
|
|
|
108
117
|
const isPoetry = pkgManager === 'poetry';
|
|
109
118
|
const isPipenv = pkgManager === 'pipenv';
|
|
110
119
|
|
|
120
|
+
const allDeps = gatherPythonDependencies(projectPath);
|
|
121
|
+
const detectedFrameworks = detectPythonFramework(allDeps);
|
|
122
|
+
|
|
111
123
|
const commands = {};
|
|
112
|
-
|
|
113
124
|
if (isUV) {
|
|
114
125
|
commands.install = { label: 'UV Sync', command: ['uv', 'sync'], source: 'builtin' };
|
|
115
126
|
commands.add = { label: 'UV Add', command: ['uv', 'add'], source: 'builtin' };
|
|
@@ -125,8 +136,8 @@ export default {
|
|
|
125
136
|
commands.install = { label: 'Pip Install', command: ['pip', 'install', '-r', 'requirements.txt'], source: 'builtin' };
|
|
126
137
|
}
|
|
127
138
|
|
|
128
|
-
if (
|
|
129
|
-
commands.test = { label: 'Pytest', command: [isUV ? 'uv' : '
|
|
139
|
+
if (isUV || isPoetry || isPipenv) {
|
|
140
|
+
commands.test = { label: 'Pytest', command: [isUV ? 'uv' : isPoetry ? 'poetry' : 'pipenv', ...(isUV ? ['run'] : []), 'pytest'], source: 'builtin' };
|
|
130
141
|
} else {
|
|
131
142
|
commands.test = { label: 'Unittest', command: ['python', '-m', 'unittest', 'discover'], source: 'builtin' };
|
|
132
143
|
}
|
|
@@ -142,28 +153,16 @@ export default {
|
|
|
142
153
|
|
|
143
154
|
if (hasProjectFile(projectPath, 'manage.py')) {
|
|
144
155
|
const djangoCmd = isUV ? ['uv', 'run', 'python', 'manage.py'] :
|
|
145
|
-
|
|
146
|
-
['python', 'manage.py'];
|
|
156
|
+
['python', 'manage.py'];
|
|
147
157
|
commands['runserver'] = { label: 'Django Runserver', command: [...djangoCmd, 'runserver'], source: 'builtin' };
|
|
148
158
|
commands['migrate'] = { label: 'Django Migrate', command: [...djangoCmd, 'migrate'], source: 'builtin' };
|
|
149
159
|
commands['test'] = { label: 'Django Test', command: [...djangoCmd, 'test'], source: 'builtin' };
|
|
150
160
|
}
|
|
151
161
|
|
|
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
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const allDeps = gatherPythonDependencies(projectPath);
|
|
161
|
-
const detectedFrameworks = detectPythonFramework(allDeps);
|
|
162
|
-
|
|
163
162
|
const metadata = {
|
|
164
163
|
dependencies: allDeps,
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
frameworks: detectedFrameworks,
|
|
165
|
+
packageManager: pkgManager
|
|
167
166
|
};
|
|
168
167
|
|
|
169
168
|
const setupHints = [];
|