project-compass 4.3.0 โ†’ 4.3.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
4
4
  "description": "๐Ÿงญ 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": {
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, SCHEMA_GUIDE} from './projectDetection.js';
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}, create(Text, {color: 'cyan', bold: true}, 'Structure guide ยท press Shift+S to hide'), ...SCHEMA_GUIDE.map(e => create(Text, {key: e.type, dimColor: true}, `โ€ข ${e.icon} ${e.label}: ${e.files.join(', ')}`))),
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 projectData = JSON.stringify({
31
- name: selectedProject.name,
32
- type: selectedProject.type,
33
- manifest: selectedProject.manifest,
34
- scripts: selectedProject.metadata?.scripts || {},
35
- dependencies: selectedProject.metadata?.dependencies || []
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
- const prompt = `Analyze this project structure and suggest valid CLI commands for:
39
- 1. Build
40
- 2. Run
41
- 3. Install
42
- 4. Test
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
- Project Data: ${projectData}
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
- Return ONLY a JSON object with this structure: {"build": "cmd", "run": "cmd", "install": "cmd", "test": "cmd"}.
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(/{.*?}/s);
108
- if (!jsonMatch) throw new Error("AI returned invalid DNA mapping format.");
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: 'AI Build', command: parsed.build.split(' ') },
113
- { label: 'AI Run', command: parsed.run.split(' ') },
114
- { label: 'AI Install', command: parsed.install.split(' ') },
115
- { label: 'AI Test', command: parsed.test.split(' ') }
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
- const projectKey = selectedProject.path;
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 | Integrated Project Intelligence'),
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 Infrastructure'),
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: Save & Next')
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 ID: '),
352
+ create(Text, null, 'Model: '),
197
353
  create(CursorText, {value: model, cursorIndex: cursor})
198
354
  ),
199
- create(Text, {dimColor: true, marginTop: 1}, 'Enter: Save Model, Esc: Back')
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 Authorization'),
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 Token, Esc: Back')
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}, 'Ready to analyze: ' + (selectedProject ? selectedProject.name : 'No project selected')),
217
- create(Text, {dimColor: true}, 'Active: ' + config.aiProvider + ' (' + config.aiModel + ')'),
372
+ create(Text, {bold: true, color: 'cyan', marginBottom: 1},
373
+ 'Project: ' + (selectedProject ? selectedProject.name : 'No project selected')),
218
374
 
219
- create(Box, {marginTop: 1, flexDirection: 'column'},
220
- status === 'ready' && create(Text, null, 'Press Enter to perform real agentic analysis and auto-configure macros.'),
221
- status === 'busy' && create(Text, {color: 'yellow'}, ' โณ Contacting AI Agent... mapping project structure...'),
222
- status === 'done' && create(Box, {flexDirection: 'column'},
223
- create(Text, {color: 'green', bold: true}, ' โœ… DNA Mapped via AI Agent!'),
224
- create(Text, null, ' Successfully injected ' + suggestions.length + ' optimized commands. AI detected potential port conflicts? Checking...'),
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
- create(Text, {dimColor: true, marginTop: 1}, 'Esc: Return, R: Reset Credentials')
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: 'round', borderColor: 'yellow', padding: 1},
10
- create(Text, {bold: true, color: 'yellow'}, '๐Ÿ›ฐ๏ธ Orbit Task Manager | Background Processes'),
11
- create(Text, {dimColor: true, marginBottom: 1}, 'Up/Down: focus, Shift+K: Force Kill, Shift+R: Rename'),
12
- ...tasks.map(t => create(
13
- Box,
14
- {key: t.id, marginBottom: 0, flexDirection: 'column'},
15
- t.id === activeTaskId && renameMode
16
- ? create(Box, {flexDirection: 'row'}, create(Text, {color: 'cyan'}, 'โ†’ Rename to: '), create(CursorText, {value: renameInput, cursorIndex: renameCursor}))
17
- : create(Text, {color: t.id === activeTaskId ? 'cyan' : 'white', bold: t.id === activeTaskId}, `${t.id === activeTaskId ? 'โ†’' : ' '} [${t.status.toUpperCase()}] ${t.name}`)
18
- )),
19
- !tasks.length && create(Text, {dimColor: true}, 'No active or background tasks.'),
20
- create(Text, {marginTop: 1, dimColor: true}, 'Press Enter or Shift+T to return to Navigator.')
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
+ }
@@ -71,25 +71,34 @@ function gatherPythonDependencies(projectPath) {
71
71
 
72
72
  function detectPythonFramework(deps) {
73
73
  const frameworks = [];
74
- const depStr = deps.join(' ').toLowerCase();
75
74
 
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: '๐Ÿฅฌ' });
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: '\u26A1' });
86
+ if (hasDep('flask')) frameworks.push({ name: 'Flask', icon: '\uD83C\uDF36\uFE0F' });
87
+ if (hasDep('django')) frameworks.push({ name: 'Django', icon: '\uD83C\uDF3F' });
88
+ if (hasDep('tornado')) frameworks.push({ name: 'Tornado', icon: '\uD83C\uDF2A\uFE0F' });
89
+ if (hasDep('aiohttp')) frameworks.push({ name: 'AioHTTP', icon: '\uD83D\uDD04' });
90
+ if (hasDep('sanic')) frameworks.push({ name: 'Sanic', icon: '\uD83D\uDE80' });
91
+ if (hasDep('pyramid')) frameworks.push({ name: 'Pyramid', icon: '\uD83D\uDE3A' });
92
+ if (hasDep('falcon')) frameworks.push({ name: 'Falcon', icon: '\uD83D\uDC05' });
93
+ if (hasDep('starlette')) frameworks.push({ name: 'Starlette', icon: '\u2B50' });
94
+ if (hasDep('pandas')) frameworks.push({ name: 'Pandas', icon: '\uD83D\uDC3C' });
95
+ if (hasDep('numpy')) frameworks.push({ name: 'NumPy', icon: '\uD83D\uDD22' });
96
+ if (hasDep('scipy')) frameworks.push({ name: 'SciPy', icon: '\uD83D\uDD2C' });
97
+ if (hasDep('torch') || hasDep('pytorch')) frameworks.push({ name: 'PyTorch', icon: '\uD83D\uDD25' });
98
+ if (hasDep('tensorflow')) frameworks.push({ name: 'TensorFlow', icon: '\uD83E\uDD20' });
99
+ if (hasDep('sqlalchemy')) frameworks.push({ name: 'SQLAlchemy', icon: '\uD83D\uDCC4\uFE0F' });
100
+ if (hasDep('pytest')) frameworks.push({ name: 'Pytest', icon: '\u2705' });
101
+ if (hasDep('celery')) frameworks.push({ name: 'Celery', icon: '\uD83D\uDE2C' });
93
102
 
94
103
  return frameworks;
95
104
  }