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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "4.2.0",
4
- "description": "Futuristic project navigator and runner for Node, Python, Rust, and Go",
3
+ "version": "4.3.0",
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": {
7
7
  "project-compass": "src/cli.js"
@@ -17,10 +17,35 @@
17
17
  "navigator",
18
18
  "ink",
19
19
  "runner",
20
- "projects"
20
+ "projects",
21
+ "tui",
22
+ "terminal",
23
+ "workspace",
24
+ "ai",
25
+ "detect",
26
+ "python",
27
+ "rust",
28
+ "node",
29
+ "java",
30
+ "php",
31
+ "ruby",
32
+ "dotnet",
33
+ "uv",
34
+ "fastapi",
35
+ "django",
36
+ "nextjs",
37
+ "react"
21
38
  ],
22
39
  "author": "Satyaa & Clawdy",
23
40
  "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/CrimsonDevil333333/project-compass"
44
+ },
45
+ "homepage": "https://github.com/CrimsonDevil333333/project-compass",
46
+ "bugs": {
47
+ "url": "https://github.com/CrimsonDevil333333/project-compass/issues"
48
+ },
24
49
  "dependencies": {
25
50
  "execa": "^9.5.2",
26
51
  "fast-glob": "^3.3.3",
package/src/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ /* global setInterval, setTimeout, clearInterval, clearTimeout */
2
3
  import React, {useCallback, useEffect, useMemo, useRef, useState, memo} from 'react';
3
4
  import {render, Box, Text, useApp, useInput} from 'ink';
4
5
  import path from 'path';
@@ -123,22 +124,38 @@ const OutputPanel = memo(({activeTask, logOffset}) => {
123
124
  const logWindowEnd = Math.max(0, logs.length - logOffset);
124
125
  const visibleLogs = logs.slice(logWindowStart, logWindowEnd);
125
126
 
127
+ const statusColor = activeTask?.status === 'running' ? 'green' :
128
+ activeTask?.status === 'failed' ? 'red' :
129
+ activeTask?.status === 'killed' ? 'yellow' : 'cyan';
130
+
131
+ const scrollIndicator = logs.length > OUTPUT_WINDOW_SIZE
132
+ ? ` โ†• ${Math.min(logs.length - logOffset, logs.length)}/${logs.length}`
133
+ : '';
134
+
126
135
  const logNodes = visibleLogs.length
127
- ? visibleLogs.map((line, i) => create(Text, {key: i}, line))
128
- : [create(Text, {key: 'empty', dimColor: true}, 'Select a task or run a command to see logs.')];
136
+ ? visibleLogs.map((line, i) => create(Text, {key: i, wrap: 'truncate'}, line))
137
+ : [create(Text, {key: 'empty', dimColor: true}, 'No logs yet. Run a command to see output here.')];
129
138
 
130
139
  return create(
131
140
  Box,
132
141
  {
133
142
  flexDirection: 'column',
134
- borderStyle: 'round',
135
- borderColor: 'yellow',
136
- padding: 1,
143
+ borderStyle: activeTask?.status === 'running' ? 'double' : 'round',
144
+ borderColor: statusColor,
145
+ paddingX: 1,
146
+ paddingY: 0,
137
147
  minHeight: OUTPUT_WINDOW_HEIGHT,
138
148
  maxHeight: OUTPUT_WINDOW_HEIGHT,
139
149
  height: OUTPUT_WINDOW_HEIGHT,
140
150
  overflow: 'hidden'
141
151
  },
152
+ // Header with log count
153
+ logs.length > 0 && create(
154
+ Box,
155
+ { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 0 },
156
+ create(Text, { dimColor: true, bold: true }, ` ${activeTask?.status?.toUpperCase() || 'IDLE'} `),
157
+ create(Text, { dimColor: true }, `Lines: ${logs.length}${scrollIndicator}`)
158
+ ),
142
159
  ...logNodes
143
160
  );
144
161
  });
@@ -164,9 +181,20 @@ function Compass({rootPath, initialView = 'navigator'}) {
164
181
  const [stdinBuffer, setStdinBuffer] = useState('');
165
182
  const [stdinCursor, setStdinCursor] = useState(0);
166
183
  const [showHelp, setShowHelp] = useState(false);
184
+ const [startup, setStartup] = useState(true);
185
+ const [startupTick, setStartupTick] = useState(0);
167
186
  const runningProcessMap = useRef(new Map());
168
187
  const lastCommandRef = useRef(null);
169
188
 
189
+ useEffect(() => {
190
+ if (!startup) return;
191
+ const timer = setInterval(() => {
192
+ setStartupTick(t => t + 1);
193
+ }, 80);
194
+ const hideTimer = setTimeout(() => setStartup(false), 2400);
195
+ return () => { clearInterval(timer); clearTimeout(hideTimer); };
196
+ }, [startup]);
197
+
170
198
  const activeTask = useMemo(() => tasks.find(t => t.id === activeTaskId), [tasks, activeTaskId]);
171
199
  const running = activeTask?.status === 'running';
172
200
  const hasRunningTasks = useMemo(() => tasks.some(t => t.status === 'running'), [tasks]);
@@ -306,7 +334,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
306
334
  }
307
335
 
308
336
  if (quitConfirm) {
309
- if (input?.toLowerCase() === 'y') { killAllTasks(); process.stdout.write('\x1b[2J\x1b[0;0H'); exit(); return; }
337
+ if (input?.toLowerCase() === 'y') { killAllTasks(); exit(); return; }
310
338
  if (input?.toLowerCase() === 'n' || key.escape) { setQuitConfirm(false); return; }
311
339
  return;
312
340
  }
@@ -408,26 +436,24 @@ function Compass({rootPath, initialView = 'navigator'}) {
408
436
  const shiftCombo = (char) => key.shift && normalizedInput === char;
409
437
 
410
438
  const clearAndSwitch = (view) => {
411
- console.clear();
412
439
  setMainView(view);
413
440
  setViewMode('list');
414
441
  setShowHelp(false);
415
442
  };
416
443
 
417
- if (shiftCombo('h')) { console.clear(); setConfig(prev => { const next = {...prev, showHelpCards: !prev.showHelpCards}; saveConfig(next); return next; }); return; }
418
- if (shiftCombo('s')) { console.clear(); setConfig(prev => { const next = {...prev, showStructureGuide: !prev.showStructureGuide}; saveConfig(next); return next; }); return; }
444
+ if (shiftCombo('h')) { setConfig(prev => { const next = {...prev, showHelpCards: !prev.showHelpCards}; saveConfig(next); return next; }); return; }
445
+ if (shiftCombo('s')) { setConfig(prev => { const next = {...prev, showStructureGuide: !prev.showStructureGuide}; saveConfig(next); return next; }); return; }
419
446
  if (shiftCombo('a')) { clearAndSwitch(mainView === 'navigator' ? 'studio' : 'navigator'); return; }
420
447
  if (shiftCombo('p')) { clearAndSwitch(mainView === 'navigator' ? 'registry' : 'navigator'); return; }
421
448
  if (shiftCombo('n')) { clearAndSwitch(mainView === 'navigator' ? 'architect' : 'navigator'); return; }
422
449
  if (shiftCombo('o')) { clearAndSwitch(mainView === 'navigator' ? 'ai' : 'navigator'); return; }
423
- if (shiftCombo('x')) { console.clear(); setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, logs: []} : t)); setLogOffset(0); return; }
450
+ if (shiftCombo('x')) { setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, logs: []} : t)); setLogOffset(0); return; }
424
451
  if (shiftCombo('e')) { exportLogs(); return; }
425
- if (shiftCombo('d')) { console.clear(); setActiveTaskId(null); return; }
426
- if (shiftCombo('b')) { console.clear(); setConfig(prev => { const next = {...prev, showArtBoard: !prev.showArtBoard}; saveConfig(next); return next; }); return; }
452
+ if (shiftCombo('d')) { setActiveTaskId(null); return; }
453
+ if (shiftCombo('b')) { setConfig(prev => { const next = {...prev, showArtBoard: !prev.showArtBoard}; saveConfig(next); return next; }); return; }
427
454
 
428
455
  if (shiftCombo('t')) {
429
456
  setMainView((prev) => {
430
- console.clear();
431
457
  if (prev === 'tasks') return 'navigator';
432
458
  if (tasks.length > 0 && !activeTaskId) setActiveTaskId(tasks[0].id);
433
459
  return 'tasks';
@@ -502,7 +528,6 @@ function Compass({rootPath, initialView = 'navigator'}) {
502
528
  const next = prev - pageLimit;
503
529
  return next < 0 ? 0 : next;
504
530
  });
505
- console.clear();
506
531
  return;
507
532
  }
508
533
 
@@ -516,24 +541,22 @@ function Compass({rootPath, initialView = 'navigator'}) {
516
541
  }
517
542
  return next;
518
543
  });
519
- console.clear();
520
544
  return;
521
545
  }
522
546
 
523
547
 
524
- if (normalizedInput === '?') { console.clear(); setShowHelp((prev) => !prev); return; }
548
+ if (normalizedInput === '?') { setShowHelp((prev) => !prev); return; }
525
549
  if (shiftCombo('l') && lastCommandRef.current) { runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project); return; }
526
550
 
527
- if (key.upArrow && !key.shift && projects.length > 0) { console.clear(); setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length); return; }
528
- if (key.downArrow && !key.shift && projects.length > 0) { console.clear(); setSelectedIndex((prev) => (prev + 1) % projects.length); return; }
551
+ if (key.upArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length); return; }
552
+ if (key.downArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev + 1) % projects.length); return; }
529
553
  if (key.return) {
530
554
  if (!selectedProject) return;
531
- console.clear();
532
555
  setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
533
556
  return;
534
557
  }
535
558
  if (shiftCombo('q') || isCtrlC) {
536
- if (hasRunningTasks) setQuitConfirm(true); else { process.stdout.write('\x1b[2J\x1b[0;0H'); exit(); }
559
+ if (hasRunningTasks) setQuitConfirm(true); else { exit(); }
537
560
  return;
538
561
  }
539
562
 
@@ -630,9 +653,27 @@ function Compass({rootPath, initialView = 'navigator'}) {
630
653
  case 'registry': return create(PackageRegistry, {selectedProject, projects, onRunCommand: runProjectCommand, CursorText, onSelectProject: (idx) => setSelectedIndex(idx)});
631
654
  case 'architect': return create(ProjectArchitect, {rootPath, onRunCommand: runProjectCommand, CursorText, onReturn: () => setMainView('navigator')});
632
655
  case 'ai': return create(AIHorizon, {rootPath, selectedProject, onRunCommand: runProjectCommand, CursorText, config, setConfig, saveConfig});
633
- default: {
656
+ default: {
657
+ const quickActions = selectedProject ? [
658
+ { key: 'B', label: 'Build', color: 'magenta' },
659
+ { key: 'T', label: 'Test', color: 'cyan' },
660
+ { key: 'R', label: 'Run', color: 'green' },
661
+ { key: 'I', label: 'Install', color: 'blue' },
662
+ { key: '0', label: 'AI', color: 'yellow' }
663
+ ] : [];
664
+
634
665
  const navigatorBody = [
635
666
  create(Header, {projectCountLabel, rootPath, running, statusHint, toggleHint, orbitHint, artHint}),
667
+ // Quick Actions Bar
668
+ quickActions.length > 0 && create(Box, {key: 'quick-actions', flexDirection: 'row', marginBottom: 0, paddingX: 1, paddingY: 0, borderStyle: 'single', borderColor: 'cyan'},
669
+ create(Text, {dimColor: true}, 'Quick: '),
670
+ ...quickActions.map((action, idx) =>
671
+ create(Text, {key: action.key, color: action.color},
672
+ `${idx > 0 ? ' ยท ' : ''}[${action.key}] ${action.label}`
673
+ )
674
+ ),
675
+ create(Text, {dimColor: true}, ' ยท [?] Help')
676
+ ),
636
677
  config.showArtBoard && create(Box, {key: 'artboard', flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
637
678
  create(Box, {flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {color: 'magenta', bold: true}, 'Art-coded build atlas'), create(Text, {dimColor: true}, 'press ? for overlay help')),
638
679
  create(Box, {flexDirection: 'row', marginTop: 1}, ...ART_CHARS.map((char, i) => create(Text, {key: i, color: ART_COLORS[i % ART_COLORS.length]}, char.repeat(2)))),
@@ -663,6 +704,35 @@ function Compass({rootPath, initialView = 'navigator'}) {
663
704
  }
664
705
  };
665
706
 
707
+ // Startup banner with animation
708
+ if (startup) {
709
+ const frame = startupTick % 240;
710
+ const progress = Math.min(frame / 240 * 100, 100);
711
+ const barLength = 30;
712
+ const filled = Math.floor(barLength * progress / 100);
713
+ const bar = 'โ–ˆ'.repeat(filled) + 'โ–‘'.repeat(barLength - filled);
714
+ const spinner = ['โ ‹', 'โ ™', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ‡', 'โ '][startupTick % 10];
715
+
716
+ return create(Box, {flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%'},
717
+ create(Box, {marginBottom: 1},
718
+ create(Text, {color: 'magenta', bold: true}, '๐Ÿงญ '),
719
+ create(Text, {color: 'cyan', bold: true}, 'PROJECT COMPASS'),
720
+ create(Text, {color: 'magenta', bold: true}, ' ๐Ÿงญ')
721
+ ),
722
+ create(Box, {marginBottom: 1},
723
+ create(Text, {dimColor: true}, 'Initializing command center...')
724
+ ),
725
+ create(Box, {marginBottom: 1},
726
+ create(Text, {color: 'cyan'}, bar),
727
+ create(Text, {dimColor: true}, ` ${Math.floor(progress)}%`)
728
+ ),
729
+ create(Box, {},
730
+ create(Text, {color: 'yellow'}, ` ${spinner} `),
731
+ create(Text, {dimColor: true}, 'Scanning projects...')
732
+ )
733
+ );
734
+ }
735
+
666
736
  return create(Box, {flexDirection: 'column', padding: 1, width: '100%'}, renderView());
667
737
  }
668
738
 
@@ -1,25 +1,81 @@
1
- import React from 'react';
1
+ /* global setInterval, clearInterval */
2
+ import React, { useState, useEffect } from 'react';
2
3
  import { Box, Text } from 'ink';
3
4
 
5
+ const PULSE = ['โ–ฑ', 'โ–ฐ', 'โ–ฑ'];
6
+ const ARROW = ['โ†’', 'โ‡’', 'โ‡จ', 'โ‡’'];
7
+
4
8
  export default function Footer({ toggleHint, running, stdinBuffer, stdinCursor, CursorText }) {
9
+ const [pulse, setPulse] = useState(0);
10
+
11
+ useEffect(() => {
12
+ if (!running) return;
13
+ const timer = setInterval(() => setPulse(p => p + 1), 500);
14
+ return () => clearInterval(timer);
15
+ }, [running]);
16
+
17
+ const pulseChar = running ? PULSE[pulse % PULSE.length] : '';
18
+ const arrow = running ? ARROW[pulse % ARROW.length] : 'โ†’';
19
+
5
20
  return React.createElement(
6
21
  Box,
7
22
  { flexDirection: 'column', marginTop: 1 },
23
+ // Status bar
8
24
  React.createElement(
9
25
  Box,
10
- { flexDirection: 'row', justifyContent: 'space-between' },
11
- React.createElement(Text, { dimColor: true }, running ? 'Type to feed stdin; Enter: submit.' : 'Run a command or press Shift+T to switch tasks.'),
12
- React.createElement(Text, { dimColor: true }, `${toggleHint}, Shift+S: Structure Guide`)
26
+ { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 0 },
27
+ React.createElement(
28
+ Box,
29
+ { flexDirection: 'row', alignItems: 'center' },
30
+ React.createElement(Text, { dimColor: true }, `${arrow} `),
31
+ React.createElement(Text, { dimColor: true },
32
+ running ? 'Type to feed stdin ยท Enter: submit' : 'Run a command or press Shift+T for tasks'
33
+ )
34
+ ),
35
+ React.createElement(
36
+ Box,
37
+ { flexDirection: 'row', alignItems: 'center' },
38
+ React.createElement(Text, { dimColor: true }, `${toggleHint}`),
39
+ React.createElement(Text, { dimColor: true }, ` ยท `),
40
+ React.createElement(Text, { dimColor: true }, `Shift+S: Guide`)
41
+ )
13
42
  ),
43
+ // Input area
14
44
  React.createElement(
15
45
  Box,
16
- { marginTop: 1, flexDirection: 'row', borderStyle: 'round', borderColor: running ? 'green' : 'gray', paddingX: 1 },
17
- React.createElement(Text, { bold: true, color: running ? 'green' : 'white' }, running ? ' Stdin buffer ' : ' Input ready '),
46
+ {
47
+ marginTop: 0,
48
+ flexDirection: 'row',
49
+ borderStyle: running ? 'double' : 'round',
50
+ borderColor: running ? 'green' : 'gray',
51
+ paddingX: 1,
52
+ paddingY: 0
53
+ },
54
+ React.createElement(
55
+ Text,
56
+ { bold: true, color: running ? 'green' : 'white' },
57
+ running ? ` ${pulseChar} Stdin ` : ' โ—‹ Input '
58
+ ),
18
59
  React.createElement(
19
60
  Box,
20
- { marginLeft: 1 },
21
- React.createElement(CursorText, { value: stdinBuffer || (running ? '' : 'Start a command to feed stdin'), cursorIndex: stdinCursor, active: running })
61
+ { marginLeft: 1, flexGrow: 1 },
62
+ React.createElement(CursorText, {
63
+ value: stdinBuffer || (running ? '' : 'Ready for commandsโ€ฆ'),
64
+ cursorIndex: stdinCursor,
65
+ active: running
66
+ })
67
+ ),
68
+ running && React.createElement(
69
+ Text,
70
+ { color: 'green', bold: true },
71
+ ' [Active]'
22
72
  )
73
+ ),
74
+ // Separator
75
+ React.createElement(
76
+ Box,
77
+ { marginTop: 0 },
78
+ React.createElement(Text, { dimColor: true }, 'โ”€'.repeat(50))
23
79
  )
24
80
  );
25
81
  }
@@ -1,21 +1,60 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
+ import path from 'path';
3
4
 
4
- export default function Header({ projectCountLabel, rootPath, running, statusHint, toggleHint, orbitHint, artHint }) {
5
+ const SPARKS = ['โœฆ', 'โœง', 'โœฉ', 'โœช', 'โœซ'];
6
+
7
+ export default function Header({ projectCountLabel, rootPath, running, toggleHint, orbitHint }) {
8
+ const time = new Date().toLocaleTimeString('en-US', { hour12: false });
9
+ const spark = SPARKS[Math.floor(Date.now() / 1000) % SPARKS.length];
10
+
5
11
  return React.createElement(
6
12
  Box,
7
- { justifyContent: 'space-between' },
13
+ { flexDirection: 'column', marginBottom: 1 },
14
+ // Top bar with logo and status
15
+ React.createElement(
16
+ Box,
17
+ { justifyContent: 'space-between', alignItems: 'center' },
18
+ React.createElement(
19
+ Box,
20
+ { flexDirection: 'row', alignItems: 'center' },
21
+ React.createElement(Text, { color: 'magenta', bold: true }, '๐Ÿงญ '),
22
+ React.createElement(Text, { color: 'magenta', bold: true }, 'PROJECT COMPASS'),
23
+ React.createElement(Text, { color: 'cyan' }, ` ${spark}`)
24
+ ),
25
+ React.createElement(
26
+ Box,
27
+ { flexDirection: 'row', alignItems: 'center' },
28
+ React.createElement(Text, {
29
+ color: running ? 'yellow' : 'green',
30
+ bold: true
31
+ }, running ? 'โšก ACTIVE' : 'โœ“ IDLE'),
32
+ React.createElement(Text, { dimColor: true }, ` ${time}`)
33
+ )
34
+ ),
35
+ // Info bar
8
36
  React.createElement(
9
37
  Box,
10
- { flexDirection: 'column' },
11
- React.createElement(Text, { color: 'magenta', bold: true }, 'Project Compass'),
12
- React.createElement(Text, { dimColor: true }, `${projectCountLabel} detected in ${rootPath}`)
38
+ { justifyContent: 'space-between', marginTop: 0 },
39
+ React.createElement(
40
+ Box,
41
+ { flexDirection: 'row' },
42
+ React.createElement(Text, { dimColor: true }, `${spark} `),
43
+ React.createElement(Text, { color: 'cyan' }, projectCountLabel),
44
+ React.createElement(Text, { dimColor: true }, ' in '),
45
+ React.createElement(Text, { color: 'white' }, path.basename(rootPath))
46
+ ),
47
+ React.createElement(
48
+ Box,
49
+ { flexDirection: 'row' },
50
+ React.createElement(Text, { dimColor: true }, `${toggleHint} ยท ${orbitHint}`)
51
+ )
13
52
  ),
53
+ // Separator
14
54
  React.createElement(
15
55
  Box,
16
- { flexDirection: 'column', alignItems: 'flex-end' },
17
- React.createElement(Text, { color: running ? 'yellow' : 'green' }, statusHint),
18
- React.createElement(Text, { dimColor: true }, `${toggleHint} ยท ${orbitHint} ยท ${artHint} ยท Shift+Q: Quit`)
56
+ { marginTop: 0 },
57
+ React.createElement(Text, { dimColor: true }, 'โ”€'.repeat(50))
19
58
  )
20
59
  );
21
60
  }
@@ -1,7 +1,11 @@
1
- import React, { useMemo } from 'react';
1
+ /* global setInterval, clearInterval */
2
+ import React, { useMemo, useState, useEffect } from 'react';
2
3
  import { Box, Text } from 'ink';
3
4
  import path from 'path';
4
5
 
6
+ const SPINNER = ['โ ‹', 'โ ™', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ‡', 'โ '];
7
+ const SELECTION_BAR = 'โ–ˆ';
8
+
5
9
  export default function Navigator({
6
10
  projects,
7
11
  selectedIndex,
@@ -10,39 +14,89 @@ export default function Navigator({
10
14
  error,
11
15
  maxVisibleProjects = 3
12
16
  }) {
17
+ const [tick, setTick] = useState(0);
18
+
19
+ useEffect(() => {
20
+ if (!loading) return;
21
+ const timer = setInterval(() => setTick(t => t + 1), 100);
22
+ return () => clearInterval(timer);
23
+ }, [loading]);
24
+
13
25
  const page = Math.floor(selectedIndex / maxVisibleProjects);
14
26
  const start = page * maxVisibleProjects;
15
27
  const end = start + maxVisibleProjects;
16
28
  const visibleProjects = projects.slice(start, end);
17
-
29
+
18
30
  const projectRows = useMemo(() => {
19
- if (loading) return [React.createElement(Text, { key: 'scanning', dimColor: true }, 'Scanning projectsโ€ฆ')];
20
- if (error) return [React.createElement(Text, { key: 'error', color: 'red' }, `Unable to scan: ${error}`)];
21
- if (projects.length === 0) return [React.createElement(Text, { key: 'empty', dimColor: true }, 'No recognizable project manifests found.')];
31
+ if (loading) {
32
+ const spinner = SPINNER[tick % SPINNER.length];
33
+ return [React.createElement(
34
+ Box,
35
+ { key: 'scanning', flexDirection: 'row', alignItems: 'center' },
36
+ React.createElement(Text, { color: 'cyan' }, ` ${spinner} `),
37
+ React.createElement(Text, { dimColor: true }, 'Scanning projectsโ€ฆ'),
38
+ React.createElement(Text, { color: 'cyan' }, ' โ—'.repeat(3 + (tick % 3)))
39
+ )];
40
+ }
41
+ if (error) return [React.createElement(
42
+ Box,
43
+ { key: 'error', flexDirection: 'row', alignItems: 'center' },
44
+ React.createElement(Text, { color: 'red', bold: true }, ' โœ— '),
45
+ React.createElement(Text, { color: 'red' }, `Unable to scan: ${error}`)
46
+ )];
47
+ if (projects.length === 0) return [React.createElement(
48
+ Box,
49
+ { key: 'empty', flexDirection: 'row', alignItems: 'center' },
50
+ React.createElement(Text, { color: 'yellow' }, ' โš  '),
51
+ React.createElement(Text, { dimColor: true }, 'No recognizable project manifests found.')
52
+ )];
22
53
 
23
54
  return visibleProjects.map((project, index) => {
24
55
  const absoluteIndex = start + index;
25
56
  const isSelected = absoluteIndex === selectedIndex;
26
- const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
57
+ const frameworkBadges = (project.frameworks || []).map((frame) =>
58
+ React.createElement(Text, { key: frame.name, color: 'cyan', dimColor: !isSelected }, ` ${frame.icon}${frame.name}`)
59
+ );
27
60
  const hasMissingRuntime = project.missingBinaries && project.missingBinaries.length > 0;
28
61
 
29
62
  return React.createElement(
30
63
  Box,
31
- { key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1 },
64
+ {
65
+ key: project.id,
66
+ flexDirection: 'column',
67
+ marginBottom: 0,
68
+ borderStyle: isSelected ? 'bold' : 'single',
69
+ borderColor: isSelected ? 'cyan' : 'gray',
70
+ paddingX: 1,
71
+ paddingY: 0
72
+ },
32
73
  React.createElement(
33
74
  Box,
34
- { flexDirection: 'row' },
35
- React.createElement(Text, { color: isSelected ? 'cyan' : 'white', bold: isSelected }, `${project.icon} ${project.name}`),
36
- hasMissingRuntime && React.createElement(Text, { color: 'red', bold: true }, ' โš ๏ธ Runtime missing')
75
+ { flexDirection: 'row', alignItems: 'center' },
76
+ React.createElement(Text, {
77
+ color: isSelected ? 'cyan' : 'white',
78
+ bold: isSelected
79
+ }, `${isSelected ? SELECTION_BAR + ' ' : ' '}${project.icon} ${project.name}`),
80
+ hasMissingRuntime && React.createElement(Text, { color: 'red', bold: true }, ' โš  Runtime missing')
37
81
  ),
38
- React.createElement(Text, { dimColor: true }, ` ${project.type} ยท ${path.relative(rootPath, project.path) || '.'}`),
39
- frameworkBadges && React.createElement(Text, { dimColor: true }, ` ${frameworkBadges}`)
82
+ React.createElement(
83
+ Box,
84
+ { flexDirection: 'row', alignItems: 'center' },
85
+ React.createElement(Text, { dimColor: true }, ` ${project.type}`),
86
+ React.createElement(Text, { dimColor: true }, ` ยท ${path.relative(rootPath, project.path) || '.'}`)
87
+ ),
88
+ frameworkBadges.length > 0 && React.createElement(
89
+ Box,
90
+ { marginTop: 0 },
91
+ React.createElement(Text, { dimColor: true }, ' '),
92
+ ...frameworkBadges
93
+ )
40
94
  );
41
95
  });
42
- }, [loading, error, projects.length, visibleProjects, selectedIndex, start, rootPath]);
96
+ }, [loading, error, projects.length, visibleProjects, selectedIndex, start, rootPath, tick]);
43
97
 
44
98
  const totalPages = Math.ceil(projects.length / maxVisibleProjects);
45
-
99
+
46
100
  return React.createElement(
47
101
  Box,
48
102
  { flexDirection: 'column' },
@@ -50,7 +104,7 @@ export default function Navigator({
50
104
  projects.length > maxVisibleProjects && React.createElement(
51
105
  Box,
52
106
  { marginTop: 1, justifyContent: 'center' },
53
- React.createElement(Text, { dimColor: true }, `Page ${page + 1} of ${totalPages} (Total: ${projects.length})`)
107
+ React.createElement(Text, { dimColor: true }, `${'-'.repeat(3)} Page ${page + 1}/${totalPages} (${projects.length} projects) ${'-'.repeat(3)}`)
54
108
  )
55
109
  );
56
110
  }
@@ -1,13 +1,118 @@
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 parseCsProj(content) {
6
+ const metadata = {
7
+ name: '',
8
+ targetFramework: '',
9
+ dependencies: []
10
+ };
11
+
12
+ const nameMatch = content.match(/<AssemblyName>([^<]+)<\/AssemblyName>/);
13
+ if (nameMatch) metadata.name = nameMatch[1];
14
+
15
+ const frameworkMatch = content.match(/<TargetFramework>([^<]+)<\/TargetFramework>/);
16
+ if (frameworkMatch) metadata.targetFramework = frameworkMatch[1];
17
+
18
+ const packageMatches = content.matchAll(/<PackageReference\s+Include="([^"]+)"[^/]*\/?>/g);
19
+ for (const match of packageMatches) {
20
+ if (match[1]) metadata.dependencies.push(match[1]);
21
+ }
22
+
23
+ return metadata;
24
+ }
25
+
26
+ function detectDotnetFrameworks(deps) {
27
+ const frameworks = [];
28
+ const depStr = deps.join(' ').toLowerCase();
29
+
30
+ if (depStr.includes('microsoft.aspnetcore') || depStr.includes('microsoft.aspnetcore.app')) frameworks.push({ name: 'ASP.NET Core', icon: '๐Ÿ”ท' });
31
+ if (depStr.includes('blazor')) frameworks.push({ name: 'Blazor', icon: '๐ŸŒ€' });
32
+ if (depStr.includes('entityframework') || depStr.includes('efcore')) frameworks.push({ name: 'Entity Framework', icon: '๐Ÿ—„๏ธ' });
33
+ if (depStr.includes('newtonsoft.json')) frameworks.push({ name: 'Newtonsoft.Json', icon: '๐Ÿ“„' });
34
+ if (depStr.includes('xunit')) frameworks.push({ name: 'xUnit', icon: 'โœ…' });
35
+ if (depStr.includes('nunit')) frameworks.push({ name: 'NUnit', icon: '๐Ÿ”ฌ' });
36
+ if (depStr.includes('mstest')) frameworks.push({ name: 'MSTest', icon: '๐Ÿงช' });
37
+ if (depStr.includes('automapper')) frameworks.push({ name: 'AutoMapper', icon: '๐Ÿ”„' });
38
+ if (depStr.includes('mass transit') || depStr.includes('masstransit')) frameworks.push({ name: 'MassTransit', icon: '๐ŸšŒ' });
39
+ if (depStr.includes('grpc')) frameworks.push({ name: 'gRPC', icon: '๐Ÿ”Œ' });
40
+
41
+ return frameworks;
42
+ }
43
+
44
+ function findCsProj(projectPath) {
45
+ try {
46
+ const files = fs.readdirSync(projectPath);
47
+ const csproj = files.find(f => f.endsWith('.csproj') || f.endsWith('.fsproj'));
48
+ if (csproj) return path.join(projectPath, csproj);
49
+ } catch { /* ignore */ }
50
+ return null;
51
+ }
52
+
3
53
  export default {
4
- type: 'dotnet', label: '.NET', icon: '๐ŸŽฏ', priority: 65, files: ['*.csproj', '*.sln'], binaries: ['dotnet'],
54
+ type: 'dotnet',
55
+ label: '.NET',
56
+ icon: '๐ŸŽฏ',
57
+ priority: 65,
58
+ files: ['*.csproj', '*.sln', '*.fsproj'],
59
+ binaries: ['dotnet'],
5
60
  async build(projectPath, manifest) {
6
61
  const missingBinaries = this.binaries.filter(b => !checkBinary(b));
62
+ let metadata = { name: '', targetFramework: '', dependencies: [] };
63
+ let frameworks = [];
64
+
65
+ const csprojPath = findCsProj(projectPath);
66
+ if (csprojPath && fs.existsSync(csprojPath)) {
67
+ const content = fs.readFileSync(csprojPath, 'utf-8');
68
+ metadata = parseCsProj(content);
69
+ frameworks = detectDotnetFrameworks(metadata.dependencies);
70
+ }
71
+
72
+ const commands = {
73
+ install: { label: 'Dotnet restore', command: ['dotnet', 'restore'], source: 'builtin' },
74
+ build: { label: 'Dotnet build', command: ['dotnet', 'build'], source: 'builtin' },
75
+ test: { label: 'Dotnet test', command: ['dotnet', 'test'], source: 'builtin' },
76
+ run: { label: 'Dotnet run', command: ['dotnet', 'run'], source: 'builtin' },
77
+ clean: { label: 'Dotnet clean', command: ['dotnet', 'clean'], source: 'builtin' },
78
+ publish: { label: 'Dotnet publish', command: ['dotnet', 'publish'], source: 'builtin' }
79
+ };
80
+
81
+ if (hasProjectFile(projectPath, '*.sln')) {
82
+ commands['restore-sl'] = { label: 'Restore Solution', command: ['dotnet', 'restore'], source: 'builtin' };
83
+ }
84
+
85
+ const setupHints = [];
86
+ if (missingBinaries.length > 0) {
87
+ setupHints.push('Install .NET SDK: https://dot.net/');
88
+ }
89
+ if (metadata.targetFramework) {
90
+ setupHints.push(`Target Framework: ${metadata.targetFramework}`);
91
+ }
92
+ if (metadata.dependencies.length > 0) {
93
+ setupHints.push('Run dotnet restore to fetch dependencies');
94
+ }
95
+
7
96
  return {
8
- id: `${projectPath}::dotnet`, path: projectPath, name: path.basename(projectPath), type: '.NET', icon: '๐ŸŽฏ',
9
- priority: this.priority, commands: { install: { label: 'dotnet restore', command: ['dotnet', 'restore'] } },
10
- metadata: {}, manifest: path.basename(manifest), description: '', missingBinaries, extra: {}
97
+ id: `${projectPath}::dotnet`,
98
+ path: projectPath,
99
+ name: metadata.name || path.basename(projectPath),
100
+ type: '.NET',
101
+ icon: '๐ŸŽฏ',
102
+ priority: this.priority,
103
+ commands,
104
+ metadata: {
105
+ ...metadata,
106
+ packageManager: 'dotnet'
107
+ },
108
+ manifest: path.basename(manifest),
109
+ description: frameworks.map(f => f.name).join(', ') || metadata.targetFramework,
110
+ missingBinaries,
111
+ frameworks,
112
+ extra: {
113
+ setupHints,
114
+ targetFramework: metadata.targetFramework
115
+ }
11
116
  };
12
117
  }
13
118
  };