project-compass 2.4.0 โ†’ 2.5.1

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Project Compass (v2.4.0)
1
+ # Project Compass (v2.5.0)
2
2
 
3
3
  Project Compass is a futuristic CLI navigator built with [Ink](https://github.com/vadimdemedes/ink) that scans your current folder tree for familiar code projects and gives you one-keystroke access to build, test, or run them.
4
4
 
@@ -10,6 +10,7 @@ Project Compass is a futuristic CLI navigator built with [Ink](https://github.co
10
10
  - ๐Ÿ’ก **Refined Output**: Improved stdin buffer with proper spacing and reliable scrolling (Shift+โ†‘/โ†“).
11
11
  - ๐Ÿง  **Smart Detection**: Support for 20+ frameworks including **Spring Boot** (Maven/Gradle), **ASP.NET Core**, **Rocket/Actix** (Rust), **Laravel** (PHP), **Vite**, **Prisma**, and more.
12
12
  - โš ๏ธ **Runtime Health**: Automatically checks if the required language/runtime (e.g., `node`, `python`, `cargo`) is installed and warns you if it's missing.
13
+ - ๐Ÿ’Ž **Omni-Studio**: A new interactive environment intelligence mode to see all installed runtimes and versions.
13
14
  - ๐Ÿ”Œ **Extensible**: Add custom commands with **Shift+C** and frameworks via `plugins.json`.
14
15
 
15
16
  ## Installation
@@ -21,7 +22,7 @@ npm install -g project-compass
21
22
  ## Usage
22
23
 
23
24
  ```bash
24
- project-compass [--dir /path/to/workspace]
25
+ project-compass [--dir /path/to/workspace] [--studio]
25
26
  ```
26
27
 
27
28
  ### Keyboard Guide
@@ -31,7 +32,9 @@ project-compass [--dir /path/to/workspace]
31
32
  | โ†‘ / โ†“ | Move focus, **Enter**: toggle details |
32
33
  | B / T / R | Build / Test / Run |
33
34
  | 1โ€‘9 | Execute numbered detail commands |
35
+ | **Shift+A** | Open **Omni-Studio** (Environment View) |
34
36
  | **Shift+C** | Add a custom command (`label|cmd`) |
37
+ | **Shift+X** | **Clear output logs** |
35
38
  | **Shift โ†‘ / โ†“** | Scroll output buffer |
36
39
  | **Shift+L** | Rerun last command |
37
40
  | **Shift+H** | Toggle help cards |
@@ -40,6 +43,10 @@ project-compass [--dir /path/to/workspace]
40
43
  | ? | Toggle help overlay |
41
44
  | Ctrl+C | Interrupt running command |
42
45
 
46
+ ## Omni-Studio
47
+
48
+ Launch with `project-compass --studio` or press **Shift+A** inside the app. Omni-Studio provides real-time intelligence on your installed development environments, checking versions for Node, Python, Rust, Go, Java, and more.
49
+
43
50
  ## Layout & UX
44
51
 
45
52
  Project Compass features a split layout where Projects and Details stay paired while Output takes a full-width band. The stdin buffer (at the bottom) now has a clear distinction between the label and your input for better readability. The help cards (Shift+H) have been refactored for a cleaner, more readable look.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "Ink-based project explorer that detects local repos and lets you build/test/run them without memorizing commands.",
5
5
  "main": "src/cli.js",
6
6
  "type": "module",
package/src/cli.js CHANGED
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import fs from 'fs';
6
6
  import kleur from 'kleur';
7
7
  import {execa} from 'execa';
8
- import {discoverProjects, SCHEMA_GUIDE} from './projectDetection.js';
8
+ import {discoverProjects, SCHEMA_GUIDE, checkBinary} from './projectDetection.js';
9
9
  import {CONFIG_PATH, PLUGIN_FILE, ensureConfigDir} from './configPaths.js';
10
10
 
11
11
  const create = React.createElement;
@@ -95,11 +95,69 @@ function buildDetailCommands(project, config) {
95
95
  return [...builtins, ...custom];
96
96
  }
97
97
 
98
- function Compass({rootPath}) {
98
+ function Studio() {
99
+ const [runtimes, setRuntimes] = useState([]);
100
+ const [loading, setLoading] = useState(true);
101
+
102
+ useEffect(() => {
103
+ const checks = [
104
+ {name: 'Node.js', binary: 'node', versionCmd: ['-v']},
105
+ {name: 'npm', binary: 'npm', versionCmd: ['-v']},
106
+ {name: 'Python', binary: process.platform === 'win32' ? 'python' : 'python3', versionCmd: ['--version']},
107
+ {name: 'Rust (Cargo)', binary: 'cargo', versionCmd: ['--version']},
108
+ {name: 'Go', binary: 'go', versionCmd: ['version']},
109
+ {name: 'Java', binary: 'java', versionCmd: ['-version']},
110
+ {name: 'PHP', binary: 'php', versionCmd: ['-v']},
111
+ {name: 'Ruby', binary: 'ruby', versionCmd: ['-v']},
112
+ {name: '.NET', binary: 'dotnet', versionCmd: ['--version']}
113
+ ];
114
+
115
+ (async () => {
116
+ const results = await Promise.all(checks.map(async (lang) => {
117
+ if (!checkBinary(lang.binary)) {
118
+ return {...lang, status: 'missing', version: 'not installed'};
119
+ }
120
+ try {
121
+ const {stdout, stderr} = await execa(lang.binary, lang.versionCmd);
122
+ const version = (stdout || stderr || '').split('\n')[0].trim();
123
+ return {...lang, status: 'ok', version};
124
+ } catch {
125
+ return {...lang, status: 'error', version: 'failed to check'};
126
+ }
127
+ }));
128
+ setRuntimes(results);
129
+ setLoading(false);
130
+ })();
131
+ }, []);
132
+
133
+ return create(
134
+ Box,
135
+ {flexDirection: 'column', borderStyle: 'double', borderColor: 'blue', padding: 1},
136
+ create(Text, {bold: true, color: 'blue'}, '๐Ÿ’Ž Omni-Studio | Environment Intelligence'),
137
+ create(Text, {dimColor: true, marginBottom: 1}, 'Overview of installed languages and build tools.'),
138
+ loading
139
+ ? create(Text, {dimColor: true}, 'Gathering intelligence...')
140
+ : create(
141
+ Box,
142
+ {flexDirection: 'column'},
143
+ ...runtimes.map(r => create(
144
+ Box,
145
+ {key: r.name, marginBottom: 1},
146
+ create(Text, {width: 15, color: r.status === 'ok' ? 'green' : 'red'}, `${r.status === 'ok' ? 'โœ“' : 'โœ—'} ${r.name}`),
147
+ create(Text, {dimColor: r.status !== 'ok'}, r.version)
148
+ )),
149
+ create(Text, {marginTop: 1, color: 'yellow'}, '๐Ÿ› ๏ธ Interactive Project Creator coming soon in v3.0'),
150
+ create(Text, {dimColor: true}, 'Press Shift+A to return to Navigator.')
151
+ )
152
+ );
153
+ }
154
+
155
+ function Compass({rootPath, initialView = 'navigator'}) {
99
156
  const {exit} = useApp();
100
157
  const {projects, loading, error} = useScanner(rootPath);
101
158
  const [selectedIndex, setSelectedIndex] = useState(0);
102
159
  const [viewMode, setViewMode] = useState('list');
160
+ const [mainView, setMainView] = useState(initialView);
103
161
  const [logLines, setLogLines] = useState([]);
104
162
  const [logOffset, setLogOffset] = useState(0);
105
163
  const [running, setRunning] = useState(false);
@@ -120,7 +178,7 @@ function Compass({rootPath}) {
120
178
  setLogLines((prev) => {
121
179
  const normalized = typeof line === 'string' ? line : JSON.stringify(line);
122
180
  const appended = [...prev, normalized];
123
- const next = appended.length > 250 ? appended.slice(appended.length - 250) : appended;
181
+ const next = appended.length > 500 ? appended.slice(appended.length - 500) : appended;
124
182
  return next;
125
183
  });
126
184
  }, []);
@@ -256,6 +314,7 @@ function Compass({rootPath}) {
256
314
  }
257
315
 
258
316
  const normalizedInput = input?.toLowerCase();
317
+ const ctrlCombo = (char) => key.ctrl && normalizedInput === char;
259
318
  const shiftCombo = (char) => key.shift && normalizedInput === char;
260
319
  const toggleShortcut = (char) => shiftCombo(char);
261
320
  if (toggleShortcut('h')) {
@@ -266,6 +325,15 @@ function Compass({rootPath}) {
266
325
  setShowStructureGuide((prev) => !prev);
267
326
  return;
268
327
  }
328
+ if (toggleShortcut('a')) {
329
+ setMainView((prev) => (prev === 'navigator' ? 'studio' : 'navigator'));
330
+ return;
331
+ }
332
+ if (toggleShortcut('x')) {
333
+ setLogLines([]);
334
+ setLogOffset(0);
335
+ return;
336
+ }
269
337
 
270
338
  const scrollLogs = (delta) => {
271
339
  setLogOffset((prev) => {
@@ -298,11 +366,11 @@ function Compass({rootPath}) {
298
366
  }
299
367
 
300
368
  if (key.shift && key.upArrow) {
301
- scrollLogs(-1);
369
+ scrollLogs(1);
302
370
  return;
303
371
  }
304
372
  if (key.shift && key.downArrow) {
305
- scrollLogs(1);
373
+ scrollLogs(-1);
306
374
  return;
307
375
  }
308
376
 
@@ -349,17 +417,19 @@ function Compass({rootPath}) {
349
417
  runProjectCommand(detailShortcutMap.get(normalizedInput), selectedProject);
350
418
  }
351
419
  });
352
- const projectRows = [];
420
+
421
+ const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
422
+ const projectRows = [];
353
423
  if (loading) {
354
- projectRows.push(create(Text, {dimColor: true}, 'Scanning projectsโ€ฆ'));
424
+ projectRows.push(create(Text, {key: 'scanning', dimColor: true}, 'Scanning projectsโ€ฆ'));
355
425
  }
356
426
  if (error) {
357
- projectRows.push(create(Text, {color: 'red'}, `Unable to scan: ${error}`));
427
+ projectRows.push(create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`));
358
428
  }
359
429
  if (!loading && !error && projects.length === 0) {
360
- projectRows.push(create(Text, {dimColor: true}, 'No recognizable project manifests found.'));
430
+ projectRows.push(create(Text, {key: 'empty', dimColor: true}, 'No recognizable project manifests found.'));
361
431
  }
362
- if (!loading) {
432
+ if (!loading && mainView === 'navigator') {
363
433
  projects.forEach((project, index) => {
364
434
  const isSelected = index === selectedIndex;
365
435
  const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
@@ -393,59 +463,58 @@ const projectRows = [];
393
463
  detailContent.push(
394
464
  create(
395
465
  Box,
396
- {flexDirection: 'row'},
466
+ {key: 'title-row', flexDirection: 'row'},
397
467
  create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
398
468
  selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' โš ๏ธ MISSING RUNTIME')
399
469
  ),
400
- create(Text, {dimColor: true}, `${selectedProject.type} ยท ${selectedProject.manifest || 'detected manifest'}`),
401
- create(Text, {dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
470
+ create(Text, {key: 'manifest', dimColor: true}, `${selectedProject.type} ยท ${selectedProject.manifest || 'detected manifest'}`),
471
+ create(Text, {key: 'loc', dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
402
472
  );
403
473
  if (selectedProject.description) {
404
- detailContent.push(create(Text, null, selectedProject.description));
474
+ detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
405
475
  }
406
476
  const frameworks = (selectedProject.frameworks || []).map((lib) => `${lib.icon} ${lib.name}`).join(', ');
407
477
  if (frameworks) {
408
- detailContent.push(create(Text, {dimColor: true}, `Frameworks: ${frameworks}`));
478
+ detailContent.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
409
479
  }
410
480
  if (selectedProject.extra?.scripts && selectedProject.extra.scripts.length) {
411
- detailContent.push(create(Text, {dimColor: true}, `Scripts: ${selectedProject.extra.scripts.join(', ')}`));
481
+ detailContent.push(create(Text, {key: 'scripts', dimColor: true}, `Scripts: ${selectedProject.extra.scripts.join(', ')}`));
412
482
  }
413
483
 
414
484
  if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
415
485
  detailContent.push(
416
- create(Text, {color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
417
- create(Text, {color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`),
418
- create(Text, {dimColor: true}, 'Project commands may fail until these are in your PATH.')
486
+ create(Text, {key: 'missing-title', color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
487
+ create(Text, {key: 'missing-list', color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`),
488
+ create(Text, {key: 'missing-hint', dimColor: true}, 'Project commands may fail until these are in your PATH.')
419
489
  );
420
490
  }
421
491
 
422
- detailContent.push(create(Text, {dimColor: true, marginTop: 1}, `Custom commands stored in ${CONFIG_PATH}`));
423
- detailContent.push(create(Text, {dimColor: true, marginBottom: 1}, `Extend frameworks via ${PLUGIN_FILE}`));
424
- detailContent.push(create(Text, {bold: true, marginTop: 1}, 'Commands'));
492
+ detailContent.push(create(Text, {key: 'config-path', dimColor: true, marginTop: 1}, `Custom commands stored in ${CONFIG_PATH}`));
493
+ detailContent.push(create(Text, {key: 'plugin-path', dimColor: true, marginBottom: 1}, `Extend frameworks via ${PLUGIN_FILE}`));
494
+ detailContent.push(create(Text, {key: 'cmd-header', bold: true, marginTop: 1}, 'Commands'));
425
495
  detailedIndexed.forEach((command) => {
426
496
  detailContent.push(
427
497
  create(Text, {key: `detail-${command.shortcut}-${command.label}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : command.source === 'plugin' ? kleur.green('(plugin)') : ''}`)
428
498
  );
429
- detailContent.push(create(Text, {dimColor: true}, ` โ†ณ ${command.command.join(' ')}`));
499
+ detailContent.push(create(Text, {key: `detail-line-${command.shortcut}-${command.label}`, dimColor: true}, ` โ†ณ ${command.command.join(' ')}`));
430
500
  });
431
501
  if (!detailedIndexed.length) {
432
- detailContent.push(create(Text, {dimColor: true}, 'No built-in commands yet. Add a custom command with Shift+C.'));
502
+ detailContent.push(create(Text, {key: 'no-cmds', dimColor: true}, 'No built-in commands yet. Add a custom command with Shift+C.'));
433
503
  }
434
504
  const setupHints = selectedProject.extra?.setupHints || [];
435
505
  if (setupHints.length) {
436
- detailContent.push(create(Text, {dimColor: true, marginTop: 1}, 'Setup hints:'));
437
- setupHints.forEach((hint) => detailContent.push(create(Text, {dimColor: true}, ` โ€ข ${hint}`)));
506
+ detailContent.push(create(Text, {key: 'setup-header', dimColor: true, marginTop: 1}, 'Setup hints:'));
507
+ setupHints.forEach((hint, hidx) => detailContent.push(create(Text, {key: `hint-${hidx}`, dimColor: true}, ` โ€ข ${hint}`)));
438
508
  }
439
- detailContent.push(create(Text, {dimColor: true}, 'Press Shift+C โ†’ label|cmd to save custom actions, Enter to close detail view.'));
509
+ detailContent.push(create(Text, {key: 'hint-line', dimColor: true}, 'Press Shift+C โ†’ label|cmd to save custom actions, Enter to close detail view.'));
440
510
  } else {
441
- detailContent.push(create(Text, {dimColor: true}, 'Press Enter on a project to reveal details (icons, commands, frameworks, custom actions).'));
511
+ detailContent.push(create(Text, {key: 'enter-hint', dimColor: true}, 'Press Enter on a project to reveal details (icons, commands, frameworks, custom actions).'));
442
512
  }
443
513
 
444
514
  if (customMode) {
445
- detailContent.push(create(Text, {color: 'cyan'}, `Type label|cmd (Enter to save, Esc to cancel): ${customInput}`));
515
+ detailContent.push(create(Text, {key: 'custom-input', color: 'cyan'}, `Type label|cmd (Enter to save, Esc to cancel): ${customInput}`));
446
516
  }
447
517
 
448
- const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
449
518
  const artTileNodes = useMemo(() => {
450
519
  const selectedName = selectedProject?.name || 'Awaiting selection';
451
520
  const selectedType = selectedProject?.type || 'Unknown stack';
@@ -529,7 +598,7 @@ const projectRows = [];
529
598
  const visibleLogs = logLines.slice(logWindowStart, logWindowEnd);
530
599
  const logNodes = visibleLogs.length
531
600
  ? visibleLogs.map((line, index) => create(Text, {key: index}, line))
532
- : [create(Text, {dimColor: true}, 'Logs will appear here once you run a command.')];
601
+ : [create(Text, {key: 'no-logs', dimColor: true}, 'Logs will appear here once you run a command.')];
533
602
 
534
603
  const helpCards = [
535
604
  {
@@ -549,14 +618,14 @@ const projectRows = [];
549
618
  'B / T / R build/test/run',
550
619
  '1-9 run detail commands',
551
620
  'Shift+L rerun last command',
552
- 'Ctrl+C abort; type feeds stdin'
621
+ 'Shift+X clear output logs'
553
622
  ]
554
623
  },
555
624
  {
556
- label: 'Recent runs',
625
+ label: 'System & Studio',
557
626
  color: 'yellow',
558
627
  body: [
559
- recentRuns.length ? `${recentRuns.length} runs recorded` : 'No runs yet ยท start with B/T/R',
628
+ 'Shift+A open Omni-Studio',
560
629
  'Shift+S toggle structure guide',
561
630
  'Shift+C save custom action',
562
631
  'Shift+Q quit application'
@@ -589,7 +658,7 @@ const projectRows = [];
589
658
  )
590
659
  )
591
660
  )
592
- : create(Text, {dimColor: true, marginTop: 1}, 'Help cards hidden ยท press Shift+H to show navigation, command flow, and recent runs.');
661
+ : create(Text, {key: 'help-hint', dimColor: true, marginTop: 1}, 'Help cards hidden ยท press Shift+H to show navigation, command flow, and recent runs.');
593
662
 
594
663
  const structureGuide = showStructureGuide
595
664
  ? create(
@@ -620,14 +689,18 @@ const projectRows = [];
620
689
  padding: 1
621
690
  },
622
691
  create(Text, {color: 'cyan', bold: true}, 'Help overlay ยท press ? to hide'),
623
- create(Text, null, 'Shift+โ†‘/โ†“ scrolls the log buffer while commands stream; type to feed stdin (Enter submits, Ctrl+C aborts).'),
692
+ create(Text, null, 'Shift+โ†‘/โ†“ scrolls the log buffer; Shift+X clears logs; Shift+A opens Omni-Studio.'),
624
693
  create(Text, null, 'B/T/R run build/test/run; 1-9 executes detail commands; Shift+L reruns the previous command.'),
625
- create(Text, null, 'Shift+H toggles these help cards, Shift+S toggles the structure guide, ? toggles this overlay, Shift+Q quits.'),
694
+ create(Text, null, 'Shift+H toggles help cards, Shift+S structure guide, ? overlay, Shift+Q quits.'),
626
695
  create(Text, null, 'Projects + Details stay paired while Output keeps its own full-width band.'),
627
- create(Text, null, 'Structure guide lists the manifests that trigger each language detection (Shift+S to toggle).')
696
+ create(Text, null, 'Structure guide lists the manifests that trigger each language detection.')
628
697
  )
629
698
  : null;
630
699
 
700
+ if (mainView === 'studio') {
701
+ return create(Studio);
702
+ }
703
+
631
704
  const toggleHint = showHelpCards ? 'Shift+H hides the help cards' : 'Shift+H shows the help cards';
632
705
  const headerHint = viewMode === 'detail'
633
706
  ? `Detail mode ยท 1-${Math.max(detailedIndexed.length, 1)} to execute, Shift+C: add custom commands, Enter: back to list, Shift+Q: quit ยท ${toggleHint}, Shift+S toggles structure guide`
@@ -751,6 +824,8 @@ function parseArgs() {
751
824
  i += 1;
752
825
  } else if (token === '--help' || token === '-h') {
753
826
  args.help = true;
827
+ } else if (token === '--studio') {
828
+ args.view = 'studio';
754
829
  }
755
830
  }
756
831
  return args;
@@ -760,7 +835,7 @@ async function main() {
760
835
  const args = parseArgs();
761
836
  if (args.help) {
762
837
  console.log('Project Compass ยท Ink project runner');
763
- console.log('Usage: project-compass [--dir <path>] [--mode test]');
838
+ console.log('Usage: project-compass [--dir <path>] [--mode test] [--studio]');
764
839
  return;
765
840
  }
766
841
  const rootPath = args.root ? path.resolve(args.root) : process.cwd();
@@ -773,7 +848,7 @@ async function main() {
773
848
  return;
774
849
  }
775
850
 
776
- render(create(Compass, {rootPath}));
851
+ render(create(Compass, {rootPath, initialView: args.view || 'navigator'}));
777
852
  }
778
853
 
779
854
  main().catch((error) => {
@@ -1098,4 +1098,4 @@ const SCHEMA_GUIDE = schemaRegistry.getSchemas().map((schema) => ({
1098
1098
  files: schema.files
1099
1099
  }));
1100
1100
 
1101
- export {discoverProjects, SCHEMA_GUIDE};
1101
+ export {discoverProjects, SCHEMA_GUIDE, checkBinary};