project-compass 2.3.1 → 2.5.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.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
 
@@ -9,6 +9,8 @@ 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.
13
+ - 💎 **Omni-Studio**: A new interactive environment intelligence mode to see all installed runtimes and versions.
12
14
  - 🔌 **Extensible**: Add custom commands with **Shift+C** and frameworks via `plugins.json`.
13
15
 
14
16
  ## Installation
@@ -20,7 +22,7 @@ npm install -g project-compass
20
22
  ## Usage
21
23
 
22
24
  ```bash
23
- project-compass [--dir /path/to/workspace]
25
+ project-compass [--dir /path/to/workspace] [--studio]
24
26
  ```
25
27
 
26
28
  ### Keyboard Guide
@@ -30,7 +32,9 @@ project-compass [--dir /path/to/workspace]
30
32
  | ↑ / ↓ | Move focus, **Enter**: toggle details |
31
33
  | B / T / R | Build / Test / Run |
32
34
  | 1‑9 | Execute numbered detail commands |
35
+ | **Shift+A** | Open **Omni-Studio** (Environment View) |
33
36
  | **Shift+C** | Add a custom command (`label|cmd`) |
37
+ | **Shift+X** | **Clear output logs** |
34
38
  | **Shift ↑ / ↓** | Scroll output buffer |
35
39
  | **Shift+L** | Rerun last command |
36
40
  | **Shift+H** | Toggle help cards |
@@ -39,6 +43,10 @@ project-compass [--dir /path/to/workspace]
39
43
  | ? | Toggle help overlay |
40
44
  | Ctrl+C | Interrupt running command |
41
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
+
42
50
  ## Layout & UX
43
51
 
44
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.3.1",
3
+ "version": "2.5.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
@@ -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) => {
@@ -349,6 +417,11 @@ function Compass({rootPath}) {
349
417
  runProjectCommand(detailShortcutMap.get(normalizedInput), selectedProject);
350
418
  }
351
419
  });
420
+
421
+ if (mainView === 'studio') {
422
+ return create(Studio);
423
+ }
424
+
352
425
  const projectRows = [];
353
426
  if (loading) {
354
427
  projectRows.push(create(Text, {dimColor: true}, 'Scanning projects…'));
@@ -363,17 +436,23 @@ const projectRows = [];
363
436
  projects.forEach((project, index) => {
364
437
  const isSelected = index === selectedIndex;
365
438
  const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
439
+ const hasMissingRuntime = project.missingBinaries && project.missingBinaries.length > 0;
366
440
  projectRows.push(
367
441
  create(
368
442
  Box,
369
443
  {key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1},
370
444
  create(
371
- Text,
372
- {
373
- color: isSelected ? 'cyan' : 'white',
374
- bold: isSelected
375
- },
376
- `${project.icon} ${project.name}`
445
+ Box,
446
+ {flexDirection: 'row'},
447
+ create(
448
+ Text,
449
+ {
450
+ color: isSelected ? 'cyan' : 'white',
451
+ bold: isSelected
452
+ },
453
+ `${project.icon} ${project.name}`
454
+ ),
455
+ hasMissingRuntime && create(Text, {color: 'red', bold: true}, ' ⚠️ Runtime missing')
377
456
  ),
378
457
  create(Text, {dimColor: true}, ` ${project.type} · ${path.relative(rootPath, project.path) || '.'}`),
379
458
  frameworkBadges && create(Text, {dimColor: true}, ` ${frameworkBadges}`)
@@ -385,7 +464,12 @@ const projectRows = [];
385
464
  const detailContent = [];
386
465
  if (viewMode === 'detail' && selectedProject) {
387
466
  detailContent.push(
388
- create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
467
+ create(
468
+ Box,
469
+ {flexDirection: 'row'},
470
+ create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
471
+ selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' ⚠️ MISSING RUNTIME')
472
+ ),
389
473
  create(Text, {dimColor: true}, `${selectedProject.type} · ${selectedProject.manifest || 'detected manifest'}`),
390
474
  create(Text, {dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
391
475
  );
@@ -399,7 +483,16 @@ const projectRows = [];
399
483
  if (selectedProject.extra?.scripts && selectedProject.extra.scripts.length) {
400
484
  detailContent.push(create(Text, {dimColor: true}, `Scripts: ${selectedProject.extra.scripts.join(', ')}`));
401
485
  }
402
- detailContent.push(create(Text, {dimColor: true}, `Custom commands stored in ${CONFIG_PATH}`));
486
+
487
+ if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
488
+ detailContent.push(
489
+ create(Text, {color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
490
+ create(Text, {color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`),
491
+ create(Text, {dimColor: true}, 'Project commands may fail until these are in your PATH.')
492
+ );
493
+ }
494
+
495
+ detailContent.push(create(Text, {dimColor: true, marginTop: 1}, `Custom commands stored in ${CONFIG_PATH}`));
403
496
  detailContent.push(create(Text, {dimColor: true, marginBottom: 1}, `Extend frameworks via ${PLUGIN_FILE}`));
404
497
  detailContent.push(create(Text, {bold: true, marginTop: 1}, 'Commands'));
405
498
  detailedIndexed.forEach((command) => {
@@ -529,14 +622,14 @@ const projectRows = [];
529
622
  'B / T / R build/test/run',
530
623
  '1-9 run detail commands',
531
624
  'Shift+L rerun last command',
532
- 'Ctrl+C abort; type feeds stdin'
625
+ 'Shift+X clear output logs'
533
626
  ]
534
627
  },
535
628
  {
536
- label: 'Recent runs',
629
+ label: 'System & Studio',
537
630
  color: 'yellow',
538
631
  body: [
539
- recentRuns.length ? `${recentRuns.length} runs recorded` : 'No runs yet · start with B/T/R',
632
+ 'Shift+A open Omni-Studio',
540
633
  'Shift+S toggle structure guide',
541
634
  'Shift+C save custom action',
542
635
  'Shift+Q quit application'
@@ -600,11 +693,11 @@ const projectRows = [];
600
693
  padding: 1
601
694
  },
602
695
  create(Text, {color: 'cyan', bold: true}, 'Help overlay · press ? to hide'),
603
- create(Text, null, 'Shift+↑/↓ scrolls the log buffer while commands stream; type to feed stdin (Enter submits, Ctrl+C aborts).'),
696
+ create(Text, null, 'Shift+↑/↓ scrolls the log buffer; Shift+X clears logs; Shift+A opens Omni-Studio.'),
604
697
  create(Text, null, 'B/T/R run build/test/run; 1-9 executes detail commands; Shift+L reruns the previous command.'),
605
- create(Text, null, 'Shift+H toggles these help cards, Shift+S toggles the structure guide, ? toggles this overlay, Shift+Q quits.'),
698
+ create(Text, null, 'Shift+H toggles help cards, Shift+S structure guide, ? overlay, Shift+Q quits.'),
606
699
  create(Text, null, 'Projects + Details stay paired while Output keeps its own full-width band.'),
607
- create(Text, null, 'Structure guide lists the manifests that trigger each language detection (Shift+S to toggle).')
700
+ create(Text, null, 'Structure guide lists the manifests that trigger each language detection.')
608
701
  )
609
702
  : null;
610
703
 
@@ -731,6 +824,8 @@ function parseArgs() {
731
824
  i += 1;
732
825
  } else if (token === '--help' || token === '-h') {
733
826
  args.help = true;
827
+ } else if (token === '--studio') {
828
+ args.view = 'studio';
734
829
  }
735
830
  }
736
831
  return args;
@@ -740,7 +835,7 @@ async function main() {
740
835
  const args = parseArgs();
741
836
  if (args.help) {
742
837
  console.log('Project Compass · Ink project runner');
743
- console.log('Usage: project-compass [--dir <path>] [--mode test]');
838
+ console.log('Usage: project-compass [--dir <path>] [--mode test] [--studio]');
744
839
  return;
745
840
  }
746
841
  const rootPath = args.root ? path.resolve(args.root) : process.cwd();
@@ -753,7 +848,7 @@ async function main() {
753
848
  return;
754
849
  }
755
850
 
756
- render(create(Compass, {rootPath}));
851
+ render(create(Compass, {rootPath, initialView: args.view || 'navigator'}));
757
852
  }
758
853
 
759
854
  main().catch((error) => {
@@ -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
  }
@@ -1055,4 +1098,4 @@ const SCHEMA_GUIDE = schemaRegistry.getSchemas().map((schema) => ({
1055
1098
  files: schema.files
1056
1099
  }));
1057
1100
 
1058
- export {discoverProjects, SCHEMA_GUIDE};
1101
+ export {discoverProjects, SCHEMA_GUIDE, checkBinary};