project-compass 2.3.0 โ 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 +2 -1
- package/package.json +1 -1
- package/src/cli.js +42 -30
- package/src/projectDetection.js +43 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Project Compass (v2.
|
|
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
package/src/cli.js
CHANGED
|
@@ -121,13 +121,6 @@ function Compass({rootPath}) {
|
|
|
121
121
|
const normalized = typeof line === 'string' ? line : JSON.stringify(line);
|
|
122
122
|
const appended = [...prev, normalized];
|
|
123
123
|
const next = appended.length > 250 ? appended.slice(appended.length - 250) : appended;
|
|
124
|
-
setLogOffset((prevOffset) => {
|
|
125
|
-
const maxScroll = Math.max(0, next.length - OUTPUT_WINDOW_SIZE);
|
|
126
|
-
if (prevOffset === 0) {
|
|
127
|
-
return 0;
|
|
128
|
-
}
|
|
129
|
-
return Math.min(maxScroll, prevOffset + 1);
|
|
130
|
-
});
|
|
131
124
|
return next;
|
|
132
125
|
});
|
|
133
126
|
}, []);
|
|
@@ -263,7 +256,6 @@ function Compass({rootPath}) {
|
|
|
263
256
|
}
|
|
264
257
|
|
|
265
258
|
const normalizedInput = input?.toLowerCase();
|
|
266
|
-
const ctrlCombo = (char) => key.ctrl && normalizedInput === char;
|
|
267
259
|
const shiftCombo = (char) => key.shift && normalizedInput === char;
|
|
268
260
|
const toggleShortcut = (char) => shiftCombo(char);
|
|
269
261
|
if (toggleShortcut('h')) {
|
|
@@ -306,11 +298,11 @@ function Compass({rootPath}) {
|
|
|
306
298
|
}
|
|
307
299
|
|
|
308
300
|
if (key.shift && key.upArrow) {
|
|
309
|
-
scrollLogs(1);
|
|
301
|
+
scrollLogs(-1);
|
|
310
302
|
return;
|
|
311
303
|
}
|
|
312
304
|
if (key.shift && key.downArrow) {
|
|
313
|
-
scrollLogs(
|
|
305
|
+
scrollLogs(1);
|
|
314
306
|
return;
|
|
315
307
|
}
|
|
316
308
|
|
|
@@ -371,17 +363,23 @@ const projectRows = [];
|
|
|
371
363
|
projects.forEach((project, index) => {
|
|
372
364
|
const isSelected = index === selectedIndex;
|
|
373
365
|
const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
|
|
366
|
+
const hasMissingRuntime = project.missingBinaries && project.missingBinaries.length > 0;
|
|
374
367
|
projectRows.push(
|
|
375
368
|
create(
|
|
376
369
|
Box,
|
|
377
370
|
{key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1},
|
|
378
371
|
create(
|
|
379
|
-
|
|
380
|
-
{
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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')
|
|
385
383
|
),
|
|
386
384
|
create(Text, {dimColor: true}, ` ${project.type} ยท ${path.relative(rootPath, project.path) || '.'}`),
|
|
387
385
|
frameworkBadges && create(Text, {dimColor: true}, ` ${frameworkBadges}`)
|
|
@@ -393,7 +391,12 @@ const projectRows = [];
|
|
|
393
391
|
const detailContent = [];
|
|
394
392
|
if (viewMode === 'detail' && selectedProject) {
|
|
395
393
|
detailContent.push(
|
|
396
|
-
create(
|
|
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
|
+
),
|
|
397
400
|
create(Text, {dimColor: true}, `${selectedProject.type} ยท ${selectedProject.manifest || 'detected manifest'}`),
|
|
398
401
|
create(Text, {dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
|
|
399
402
|
);
|
|
@@ -407,7 +410,16 @@ const projectRows = [];
|
|
|
407
410
|
if (selectedProject.extra?.scripts && selectedProject.extra.scripts.length) {
|
|
408
411
|
detailContent.push(create(Text, {dimColor: true}, `Scripts: ${selectedProject.extra.scripts.join(', ')}`));
|
|
409
412
|
}
|
|
410
|
-
|
|
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}`));
|
|
411
423
|
detailContent.push(create(Text, {dimColor: true, marginBottom: 1}, `Extend frameworks via ${PLUGIN_FILE}`));
|
|
412
424
|
detailContent.push(create(Text, {bold: true, marginTop: 1}, 'Commands'));
|
|
413
425
|
detailedIndexed.forEach((command) => {
|
|
@@ -524,20 +536,20 @@ const projectRows = [];
|
|
|
524
536
|
label: 'Navigation',
|
|
525
537
|
color: 'magenta',
|
|
526
538
|
body: [
|
|
527
|
-
'โ / โ move
|
|
528
|
-
'
|
|
529
|
-
'Shift
|
|
530
|
-
'
|
|
539
|
+
'โ / โ move focus, Enter: details',
|
|
540
|
+
'Shift+โ / โ scroll output',
|
|
541
|
+
'Shift+H toggle help cards',
|
|
542
|
+
'? opens the overlay help'
|
|
531
543
|
]
|
|
532
544
|
},
|
|
533
545
|
{
|
|
534
546
|
label: 'Command flow',
|
|
535
547
|
color: 'cyan',
|
|
536
548
|
body: [
|
|
537
|
-
'B / T / R
|
|
538
|
-
'1-9
|
|
539
|
-
'Shift+L
|
|
540
|
-
'Ctrl+C
|
|
549
|
+
'B / T / R build/test/run',
|
|
550
|
+
'1-9 run detail commands',
|
|
551
|
+
'Shift+L rerun last command',
|
|
552
|
+
'Ctrl+C abort; type feeds stdin'
|
|
541
553
|
]
|
|
542
554
|
},
|
|
543
555
|
{
|
|
@@ -545,7 +557,7 @@ const projectRows = [];
|
|
|
545
557
|
color: 'yellow',
|
|
546
558
|
body: [
|
|
547
559
|
recentRuns.length ? `${recentRuns.length} runs recorded` : 'No runs yet ยท start with B/T/R',
|
|
548
|
-
'Shift+S
|
|
560
|
+
'Shift+S toggle structure guide',
|
|
549
561
|
'Shift+C save custom action',
|
|
550
562
|
'Shift+Q quit application'
|
|
551
563
|
]
|
|
@@ -609,10 +621,10 @@ const projectRows = [];
|
|
|
609
621
|
},
|
|
610
622
|
create(Text, {color: 'cyan', bold: true}, 'Help overlay ยท press ? to hide'),
|
|
611
623
|
create(Text, null, 'Shift+โ/โ scrolls the log buffer while commands stream; type to feed stdin (Enter submits, Ctrl+C aborts).'),
|
|
612
|
-
create(Text, null, 'B/T/R run build/test/run; 1-9 executes detail commands;
|
|
613
|
-
create(Text, null, '
|
|
624
|
+
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.'),
|
|
614
626
|
create(Text, null, 'Projects + Details stay paired while Output keeps its own full-width band.'),
|
|
615
|
-
create(Text, null, 'Structure guide lists the manifests that trigger each language detection (
|
|
627
|
+
create(Text, null, 'Structure guide lists the manifests that trigger each language detection (Shift+S to toggle).')
|
|
616
628
|
)
|
|
617
629
|
: null;
|
|
618
630
|
|
package/src/projectDetection.js
CHANGED
|
@@ -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
|
}
|