project-compass 2.3.1 โ†’ 2.4.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Project Compass (v2.3.0)
1
+ # Project Compass (v2.4.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
 
@@ -9,6 +9,7 @@ Project Compass is a futuristic CLI navigator built with [Ink](https://github.co
9
9
  - ๐Ÿš€ **New Keyboard-Centric UX**: Shortcuts now use **Shift** instead of Ctrl to avoid terminal interference.
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
+ - โš ๏ธ **Runtime Health**: Automatically checks if the required language/runtime (e.g., `node`, `python`, `cargo`) is installed and warns you if it's missing.
12
13
  - ๐Ÿ”Œ **Extensible**: Add custom commands with **Shift+C** and frameworks via `plugins.json`.
13
14
 
14
15
  ## Installation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
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
@@ -363,17 +363,23 @@ const projectRows = [];
363
363
  projects.forEach((project, index) => {
364
364
  const isSelected = index === selectedIndex;
365
365
  const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
366
+ const hasMissingRuntime = project.missingBinaries && project.missingBinaries.length > 0;
366
367
  projectRows.push(
367
368
  create(
368
369
  Box,
369
370
  {key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1},
370
371
  create(
371
- Text,
372
- {
373
- color: isSelected ? 'cyan' : 'white',
374
- bold: isSelected
375
- },
376
- `${project.icon} ${project.name}`
372
+ Box,
373
+ {flexDirection: 'row'},
374
+ create(
375
+ Text,
376
+ {
377
+ color: isSelected ? 'cyan' : 'white',
378
+ bold: isSelected
379
+ },
380
+ `${project.icon} ${project.name}`
381
+ ),
382
+ hasMissingRuntime && create(Text, {color: 'red', bold: true}, ' โš ๏ธ Runtime missing')
377
383
  ),
378
384
  create(Text, {dimColor: true}, ` ${project.type} ยท ${path.relative(rootPath, project.path) || '.'}`),
379
385
  frameworkBadges && create(Text, {dimColor: true}, ` ${frameworkBadges}`)
@@ -385,7 +391,12 @@ const projectRows = [];
385
391
  const detailContent = [];
386
392
  if (viewMode === 'detail' && selectedProject) {
387
393
  detailContent.push(
388
- create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
394
+ create(
395
+ Box,
396
+ {flexDirection: 'row'},
397
+ create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
398
+ selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' โš ๏ธ MISSING RUNTIME')
399
+ ),
389
400
  create(Text, {dimColor: true}, `${selectedProject.type} ยท ${selectedProject.manifest || 'detected manifest'}`),
390
401
  create(Text, {dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
391
402
  );
@@ -399,7 +410,16 @@ const projectRows = [];
399
410
  if (selectedProject.extra?.scripts && selectedProject.extra.scripts.length) {
400
411
  detailContent.push(create(Text, {dimColor: true}, `Scripts: ${selectedProject.extra.scripts.join(', ')}`));
401
412
  }
402
- detailContent.push(create(Text, {dimColor: true}, `Custom commands stored in ${CONFIG_PATH}`));
413
+
414
+ if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
415
+ 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.')
419
+ );
420
+ }
421
+
422
+ detailContent.push(create(Text, {dimColor: true, marginTop: 1}, `Custom commands stored in ${CONFIG_PATH}`));
403
423
  detailContent.push(create(Text, {dimColor: true, marginBottom: 1}, `Extend frameworks via ${PLUGIN_FILE}`));
404
424
  detailContent.push(create(Text, {bold: true, marginTop: 1}, 'Commands'));
405
425
  detailedIndexed.forEach((command) => {
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import {execSync} from 'child_process';
3
4
  import fastGlob from 'fast-glob';
4
5
  import {ensureConfigDir, PLUGIN_FILE} from './configPaths.js';
5
6
 
@@ -7,6 +8,16 @@ const IGNORE_PATTERNS = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/b
7
8
 
8
9
  const PYTHON_ENTRY_FILES = ['main.py', 'app.py', 'src/main.py', 'src/app.py'];
9
10
 
11
+ function checkBinary(name) {
12
+ try {
13
+ const cmd = process.platform === 'win32' ? `where ${name}` : `which ${name}`;
14
+ execSync(cmd, {stdio: 'ignore'});
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
10
21
  function findPythonEntry(projectPath) {
11
22
  return PYTHON_ENTRY_FILES.find((file) => hasProjectFile(projectPath, file)) || null;
12
23
  }
@@ -538,7 +549,9 @@ class SchemaRegistry {
538
549
  icon: '๐ŸŸข',
539
550
  priority: 100,
540
551
  files: ['package.json'],
552
+ binaries: ['node', 'npm'],
541
553
  async build(projectPath, manifest) {
554
+ const missingBinaries = this.binaries.filter(b => !checkBinary(b));
542
555
  const pkgPath = path.join(projectPath, 'package.json');
543
556
  if (!fs.existsSync(pkgPath)) {
544
557
  return null;
@@ -587,6 +600,7 @@ class SchemaRegistry {
587
600
  metadata,
588
601
  manifest: path.basename(manifest),
589
602
  description: pkg.description || '',
603
+ missingBinaries,
590
604
  extra: {
591
605
  scripts: Object.keys(scripts),
592
606
  setupHints
@@ -600,7 +614,9 @@ class SchemaRegistry {
600
614
  icon: '๐Ÿ',
601
615
  priority: 95,
602
616
  files: ['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile'],
617
+ binaries: [process.platform === 'win32' ? 'python' : 'python3', 'pip'],
603
618
  async build(projectPath, manifest) {
619
+ const missingBinaries = this.binaries.filter(b => !checkBinary(b));
604
620
  const commands = {};
605
621
  if (hasProjectFile(projectPath, 'pyproject.toml')) {
606
622
  commands.test = {label: 'Pytest', command: ['pytest']};
@@ -636,6 +652,7 @@ class SchemaRegistry {
636
652
  metadata,
637
653
  manifest: path.basename(manifest),
638
654
  description: '',
655
+ missingBinaries,
639
656
  extra: {
640
657
  entry,
641
658
  setupHints
@@ -649,7 +666,9 @@ class SchemaRegistry {
649
666
  icon: '๐Ÿฆ€',
650
667
  priority: 90,
651
668
  files: ['Cargo.toml'],
669
+ binaries: ['cargo', 'rustc'],
652
670
  async build(projectPath, manifest) {
671
+ const missingBinaries = this.binaries.filter(b => !checkBinary(b));
653
672
  return {
654
673
  id: `${projectPath}::rust`,
655
674
  path: projectPath,
@@ -665,6 +684,7 @@ class SchemaRegistry {
665
684
  metadata: {},
666
685
  manifest: path.basename(manifest),
667
686
  description: '',
687
+ missingBinaries,
668
688
  extra: {
669
689
  setupHints: ['cargo fetch', 'Run cargo build before releasing']
670
690
  }
@@ -677,7 +697,9 @@ class SchemaRegistry {
677
697
  icon: '๐Ÿน',
678
698
  priority: 85,
679
699
  files: ['go.mod'],
700
+ binaries: ['go'],
680
701
  async build(projectPath, manifest) {
702
+ const missingBinaries = this.binaries.filter(b => !checkBinary(b));
681
703
  return {
682
704
  id: `${projectPath}::go`,
683
705
  path: projectPath,
@@ -693,6 +715,7 @@ class SchemaRegistry {
693
715
  metadata: {},
694
716
  manifest: path.basename(manifest),
695
717
  description: '',
718
+ missingBinaries,
696
719
  extra: {
697
720
  setupHints: ['go mod tidy', 'Ensure Go toolchain is installed']
698
721
  }
@@ -705,7 +728,9 @@ class SchemaRegistry {
705
728
  icon: 'โ˜•๏ธ',
706
729
  priority: 80,
707
730
  files: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
731
+ binaries: ['java', 'javac'],
708
732
  async build(projectPath, manifest) {
733
+ const missingBinaries = this.binaries.filter(b => !checkBinary(b));
709
734
  const hasMvnw = hasProjectFile(projectPath, 'mvnw');
710
735
  const hasGradlew = hasProjectFile(projectPath, 'gradlew');
711
736
  const commands = {};
@@ -731,6 +756,7 @@ class SchemaRegistry {
731
756
  metadata: {},
732
757
  manifest: path.basename(manifest),
733
758
  description: '',
759
+ missingBinaries,
734
760
  extra: {
735
761
  setupHints: ['Install JDK 17+ and run ./mvnw install or ./gradlew build']
736
762
  }
@@ -743,7 +769,9 @@ class SchemaRegistry {
743
769
  icon: '๐Ÿ”ต',
744
770
  priority: 70,
745
771
  files: ['build.sbt'],
772
+ binaries: ['sbt', 'scala'],
746
773
  async build(projectPath, manifest) {
774
+ const missingBinaries = this.binaries.filter(b => !checkBinary(b));
747
775
  return {
748
776
  id: `${projectPath}::scala`,
749
777
  path: projectPath,
@@ -759,6 +787,7 @@ class SchemaRegistry {
759
787
  metadata: {},
760
788
  manifest: path.basename(manifest),
761
789
  description: '',
790
+ missingBinaries,
762
791
  extra: {
763
792
  setupHints: ['Ensure sbt is installed', 'Run sbt compile before running your app']
764
793
  }
@@ -771,7 +800,9 @@ class SchemaRegistry {
771
800
  icon: '๐Ÿ˜',
772
801
  priority: 65,
773
802
  files: ['composer.json'],
803
+ binaries: ['php', 'composer'],
774
804
  async build(projectPath, manifest) {
805
+ const missingBinaries = this.binaries.filter(b => !checkBinary(b));
775
806
  return {
776
807
  id: `${projectPath}::php`,
777
808
  path: projectPath,
@@ -785,6 +816,7 @@ class SchemaRegistry {
785
816
  metadata: {},
786
817
  manifest: path.basename(manifest),
787
818
  description: '',
819
+ missingBinaries,
788
820
  extra: {
789
821
  setupHints: ['composer install to install dependencies']
790
822
  }
@@ -797,7 +829,9 @@ class SchemaRegistry {
797
829
  icon: '๐Ÿ’Ž',
798
830
  priority: 65,
799
831
  files: ['Gemfile'],
832
+ binaries: ['ruby', 'bundle'],
800
833
  async build(projectPath, manifest) {
834
+ const missingBinaries = this.binaries.filter(b => !checkBinary(b));
801
835
  return {
802
836
  id: `${projectPath}::ruby`,
803
837
  path: projectPath,
@@ -812,6 +846,7 @@ class SchemaRegistry {
812
846
  metadata: {},
813
847
  manifest: path.basename(manifest),
814
848
  description: '',
849
+ missingBinaries,
815
850
  extra: {
816
851
  setupHints: ['bundle install to ensure gems are present']
817
852
  }
@@ -824,7 +859,9 @@ class SchemaRegistry {
824
859
  icon: '๐Ÿ”ท',
825
860
  priority: 65,
826
861
  files: ['*.csproj'],
862
+ binaries: ['dotnet'],
827
863
  async build(projectPath, manifest) {
864
+ const missingBinaries = this.binaries.filter(b => !checkBinary(b));
828
865
  return {
829
866
  id: `${projectPath}::dotnet`,
830
867
  path: projectPath,
@@ -840,6 +877,7 @@ class SchemaRegistry {
840
877
  metadata: {},
841
878
  manifest: path.basename(manifest),
842
879
  description: '',
880
+ missingBinaries,
843
881
  extra: {
844
882
  setupHints: ['Install .NET SDK 8+', 'dotnet restore before running']
845
883
  }
@@ -852,7 +890,9 @@ class SchemaRegistry {
852
890
  icon: '๐Ÿš',
853
891
  priority: 50,
854
892
  files: ['Makefile', 'build.sh'],
893
+ binaries: ['make', 'sh'],
855
894
  async build(projectPath, manifest) {
895
+ const missingBinaries = this.binaries.filter(b => !checkBinary(b));
856
896
  return {
857
897
  id: `${projectPath}::shell`,
858
898
  path: projectPath,
@@ -867,6 +907,7 @@ class SchemaRegistry {
867
907
  metadata: {},
868
908
  manifest: path.basename(manifest),
869
909
  description: '',
910
+ missingBinaries,
870
911
  extra: {
871
912
  setupHints: ['Run make install if available', 'Ensure shell scripts are executable']
872
913
  }
@@ -879,6 +920,7 @@ class SchemaRegistry {
879
920
  icon: '๐Ÿงฐ',
880
921
  priority: 10,
881
922
  files: ['README.md'],
923
+ binaries: [],
882
924
  async build(projectPath, manifest) {
883
925
  return {
884
926
  id: `${projectPath}::generic`,
@@ -891,6 +933,7 @@ class SchemaRegistry {
891
933
  metadata: {},
892
934
  manifest: path.basename(manifest),
893
935
  description: 'Detected via README or Makefile layout.',
936
+ missingBinaries: [],
894
937
  extra: {
895
938
  setupHints: ['Read the README for custom build instructions']
896
939
  }