project-compass 2.5.0 → 2.6.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.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/package.json +1 -1
  3. package/src/cli.js +85 -44
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Project Compass (v2.5.0)
1
+ # Project Compass (v2.6.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
 
@@ -11,6 +11,7 @@ Project Compass is a futuristic CLI navigator built with [Ink](https://github.co
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
13
  - 💎 **Omni-Studio**: A new interactive environment intelligence mode to see all installed runtimes and versions.
14
+ - 📂 **Log Management**: Clear output with **Shift+X** or export logs to a text file with **Shift+E**.
14
15
  - 🔌 **Extensible**: Add custom commands with **Shift+C** and frameworks via `plugins.json`.
15
16
 
16
17
  ## Installation
@@ -35,7 +36,8 @@ project-compass [--dir /path/to/workspace] [--studio]
35
36
  | **Shift+A** | Open **Omni-Studio** (Environment View) |
36
37
  | **Shift+C** | Add a custom command (`label|cmd`) |
37
38
  | **Shift+X** | **Clear output logs** |
38
- | **Shift / ↓** | Scroll output buffer |
39
+ | **Shift+E** | **Export logs to .txt** |
40
+ | **Shift ↑ / ↓** | Scroll output buffer (Intuitive Direction) |
39
41
  | **Shift+L** | Rerun last command |
40
42
  | **Shift+H** | Toggle help cards |
41
43
  | **Shift+S** | Toggle structure guide |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "2.5.0",
3
+ "version": "2.6.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
@@ -177,9 +177,9 @@ function Compass({rootPath, initialView = 'navigator'}) {
177
177
  const addLog = useCallback((line) => {
178
178
  setLogLines((prev) => {
179
179
  const normalized = typeof line === 'string' ? line : JSON.stringify(line);
180
- const appended = [...prev, normalized];
181
- const next = appended.length > 500 ? appended.slice(appended.length - 500) : appended;
182
- return next;
180
+ const lines = normalized.split(/\r?\n/).filter(l => l.trim().length > 0);
181
+ const appended = [...prev, ...lines];
182
+ return appended.length > 500 ? appended.slice(appended.length - 500) : appended;
183
183
  });
184
184
  }, []);
185
185
 
@@ -228,10 +228,10 @@ function Compass({rootPath, initialView = 'navigator'}) {
228
228
  runningProcessRef.current = subprocess;
229
229
 
230
230
  subprocess.stdout?.on('data', (chunk) => {
231
- addLog(chunk.toString().trimEnd());
231
+ addLog(chunk.toString());
232
232
  });
233
233
  subprocess.stderr?.on('data', (chunk) => {
234
- addLog(kleur.red(chunk.toString().trimEnd()));
234
+ addLog(kleur.red(chunk.toString()));
235
235
  });
236
236
 
237
237
  await subprocess;
@@ -292,6 +292,19 @@ function Compass({rootPath, initialView = 'navigator'}) {
292
292
  setCustomInput('');
293
293
  }, [customInput, selectedProject, handleAddCustomCommand, addLog]);
294
294
 
295
+ const exportLogs = useCallback(() => {
296
+ if (!logLines.length) {
297
+ return;
298
+ }
299
+ try {
300
+ const exportPath = path.resolve(process.cwd(), `compass-logs-${Date.now()}.txt`);
301
+ fs.writeFileSync(exportPath, logLines.join('\n'));
302
+ addLog(kleur.green(`✓ Logs exported to ${exportPath}`));
303
+ } catch (err) {
304
+ addLog(kleur.red(`✗ Export failed: ${err.message}`));
305
+ }
306
+ }, [logLines, addLog]);
307
+
295
308
  useInput((input, key) => {
296
309
  if (customMode) {
297
310
  if (key.return) {
@@ -334,6 +347,10 @@ function Compass({rootPath, initialView = 'navigator'}) {
334
347
  setLogOffset(0);
335
348
  return;
336
349
  }
350
+ if (shiftCombo('e')) {
351
+ exportLogs();
352
+ return;
353
+ }
337
354
 
338
355
  const scrollLogs = (delta) => {
339
356
  setLogOffset((prev) => {
@@ -366,11 +383,11 @@ function Compass({rootPath, initialView = 'navigator'}) {
366
383
  }
367
384
 
368
385
  if (key.shift && key.upArrow) {
369
- scrollLogs(-1);
386
+ scrollLogs(1);
370
387
  return;
371
388
  }
372
389
  if (key.shift && key.downArrow) {
373
- scrollLogs(1);
390
+ scrollLogs(-1);
374
391
  return;
375
392
  }
376
393
 
@@ -418,21 +435,18 @@ function Compass({rootPath, initialView = 'navigator'}) {
418
435
  }
419
436
  });
420
437
 
421
- if (mainView === 'studio') {
422
- return create(Studio);
423
- }
424
-
425
- const projectRows = [];
438
+ const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
439
+ const projectRows = [];
426
440
  if (loading) {
427
- projectRows.push(create(Text, {dimColor: true}, 'Scanning projects…'));
441
+ projectRows.push(create(Text, {key: 'scanning', dimColor: true}, 'Scanning projects…'));
428
442
  }
429
443
  if (error) {
430
- projectRows.push(create(Text, {color: 'red'}, `Unable to scan: ${error}`));
444
+ projectRows.push(create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`));
431
445
  }
432
446
  if (!loading && !error && projects.length === 0) {
433
- projectRows.push(create(Text, {dimColor: true}, 'No recognizable project manifests found.'));
447
+ projectRows.push(create(Text, {key: 'empty', dimColor: true}, 'No recognizable project manifests found.'));
434
448
  }
435
- if (!loading) {
449
+ if (!loading && mainView === 'navigator') {
436
450
  projects.forEach((project, index) => {
437
451
  const isSelected = index === selectedIndex;
438
452
  const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
@@ -466,59 +480,58 @@ const projectRows = [];
466
480
  detailContent.push(
467
481
  create(
468
482
  Box,
469
- {flexDirection: 'row'},
483
+ {key: 'title-row', flexDirection: 'row'},
470
484
  create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
471
485
  selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' ⚠️ MISSING RUNTIME')
472
486
  ),
473
- create(Text, {dimColor: true}, `${selectedProject.type} · ${selectedProject.manifest || 'detected manifest'}`),
474
- create(Text, {dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
487
+ create(Text, {key: 'manifest', dimColor: true}, `${selectedProject.type} · ${selectedProject.manifest || 'detected manifest'}`),
488
+ create(Text, {key: 'loc', dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
475
489
  );
476
490
  if (selectedProject.description) {
477
- detailContent.push(create(Text, null, selectedProject.description));
491
+ detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
478
492
  }
479
493
  const frameworks = (selectedProject.frameworks || []).map((lib) => `${lib.icon} ${lib.name}`).join(', ');
480
494
  if (frameworks) {
481
- detailContent.push(create(Text, {dimColor: true}, `Frameworks: ${frameworks}`));
495
+ detailContent.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
482
496
  }
483
497
  if (selectedProject.extra?.scripts && selectedProject.extra.scripts.length) {
484
- detailContent.push(create(Text, {dimColor: true}, `Scripts: ${selectedProject.extra.scripts.join(', ')}`));
498
+ detailContent.push(create(Text, {key: 'scripts', dimColor: true}, `Scripts: ${selectedProject.extra.scripts.join(', ')}`));
485
499
  }
486
500
 
487
501
  if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
488
502
  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.')
503
+ create(Text, {key: 'missing-title', color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
504
+ create(Text, {key: 'missing-list', color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`),
505
+ create(Text, {key: 'missing-hint', dimColor: true}, 'Project commands may fail until these are in your PATH.')
492
506
  );
493
507
  }
494
508
 
495
- detailContent.push(create(Text, {dimColor: true, marginTop: 1}, `Custom commands stored in ${CONFIG_PATH}`));
496
- detailContent.push(create(Text, {dimColor: true, marginBottom: 1}, `Extend frameworks via ${PLUGIN_FILE}`));
497
- detailContent.push(create(Text, {bold: true, marginTop: 1}, 'Commands'));
509
+ detailContent.push(create(Text, {key: 'config-path', dimColor: true, marginTop: 1}, `Custom commands stored in ${CONFIG_PATH}`));
510
+ detailContent.push(create(Text, {key: 'plugin-path', dimColor: true, marginBottom: 1}, `Extend frameworks via ${PLUGIN_FILE}`));
511
+ detailContent.push(create(Text, {key: 'cmd-header', bold: true, marginTop: 1}, 'Commands'));
498
512
  detailedIndexed.forEach((command) => {
499
513
  detailContent.push(
500
514
  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)') : ''}`)
501
515
  );
502
- detailContent.push(create(Text, {dimColor: true}, ` ↳ ${command.command.join(' ')}`));
516
+ detailContent.push(create(Text, {key: `detail-line-${command.shortcut}-${command.label}`, dimColor: true}, ` ↳ ${command.command.join(' ')}`));
503
517
  });
504
518
  if (!detailedIndexed.length) {
505
- detailContent.push(create(Text, {dimColor: true}, 'No built-in commands yet. Add a custom command with Shift+C.'));
519
+ detailContent.push(create(Text, {key: 'no-cmds', dimColor: true}, 'No built-in commands yet. Add a custom command with Shift+C.'));
506
520
  }
507
521
  const setupHints = selectedProject.extra?.setupHints || [];
508
522
  if (setupHints.length) {
509
- detailContent.push(create(Text, {dimColor: true, marginTop: 1}, 'Setup hints:'));
510
- setupHints.forEach((hint) => detailContent.push(create(Text, {dimColor: true}, ` • ${hint}`)));
523
+ detailContent.push(create(Text, {key: 'setup-header', dimColor: true, marginTop: 1}, 'Setup hints:'));
524
+ setupHints.forEach((hint, hidx) => detailContent.push(create(Text, {key: `hint-${hidx}`, dimColor: true}, ` • ${hint}`)));
511
525
  }
512
- detailContent.push(create(Text, {dimColor: true}, 'Press Shift+C → label|cmd to save custom actions, Enter to close detail view.'));
526
+ detailContent.push(create(Text, {key: 'hint-line', dimColor: true}, 'Press Shift+C → label|cmd to save custom actions, Enter to close detail view.'));
513
527
  } else {
514
- detailContent.push(create(Text, {dimColor: true}, 'Press Enter on a project to reveal details (icons, commands, frameworks, custom actions).'));
528
+ detailContent.push(create(Text, {key: 'enter-hint', dimColor: true}, 'Press Enter on a project to reveal details (icons, commands, frameworks, custom actions).'));
515
529
  }
516
530
 
517
531
  if (customMode) {
518
- detailContent.push(create(Text, {color: 'cyan'}, `Type label|cmd (Enter to save, Esc to cancel): ${customInput}`));
532
+ detailContent.push(create(Text, {key: 'custom-input', color: 'cyan'}, `Type label|cmd (Enter to save, Esc to cancel): ${customInput}`));
519
533
  }
520
534
 
521
- const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
522
535
  const artTileNodes = useMemo(() => {
523
536
  const selectedName = selectedProject?.name || 'Awaiting selection';
524
537
  const selectedType = selectedProject?.type || 'Unknown stack';
@@ -602,7 +615,7 @@ const projectRows = [];
602
615
  const visibleLogs = logLines.slice(logWindowStart, logWindowEnd);
603
616
  const logNodes = visibleLogs.length
604
617
  ? visibleLogs.map((line, index) => create(Text, {key: index}, line))
605
- : [create(Text, {dimColor: true}, 'Logs will appear here once you run a command.')];
618
+ : [create(Text, {key: 'no-logs', dimColor: true}, 'Logs will appear here once you run a command.')];
606
619
 
607
620
  const helpCards = [
608
621
  {
@@ -622,7 +635,7 @@ const projectRows = [];
622
635
  'B / T / R build/test/run',
623
636
  '1-9 run detail commands',
624
637
  'Shift+L rerun last command',
625
- 'Shift+X clear output logs'
638
+ 'Shift+X clear / Shift+E export'
626
639
  ]
627
640
  },
628
641
  {
@@ -662,7 +675,7 @@ const projectRows = [];
662
675
  )
663
676
  )
664
677
  )
665
- : create(Text, {dimColor: true, marginTop: 1}, 'Help cards hidden · press Shift+H to show navigation, command flow, and recent runs.');
678
+ : create(Text, {key: 'help-hint', dimColor: true, marginTop: 1}, 'Help cards hidden · press Shift+H to show navigation, command flow, and recent runs.');
666
679
 
667
680
  const structureGuide = showStructureGuide
668
681
  ? create(
@@ -693,14 +706,18 @@ const projectRows = [];
693
706
  padding: 1
694
707
  },
695
708
  create(Text, {color: 'cyan', bold: true}, 'Help overlay · press ? to hide'),
696
- create(Text, null, 'Shift+↑/↓ scrolls the log buffer; Shift+X clears logs; Shift+A opens Omni-Studio.'),
697
- create(Text, null, 'B/T/R run build/test/run; 1-9 executes detail commands; Shift+L reruns the previous command.'),
698
- create(Text, null, 'Shift+H toggles help cards, Shift+S structure guide, ? overlay, Shift+Q quits.'),
709
+ create(Text, null, 'Shift+↑/↓ scrolls logs; Shift+X clears; Shift+E exports to file; Shift+A Omni-Studio.'),
710
+ create(Text, null, 'B/T/R run build/test/run; 1-9 detail commands; Shift+L reruns previous command.'),
711
+ create(Text, null, 'Shift+H help cards, Shift+S structure guide, ? overlay, Shift+Q quits.'),
699
712
  create(Text, null, 'Projects + Details stay paired while Output keeps its own full-width band.'),
700
713
  create(Text, null, 'Structure guide lists the manifests that trigger each language detection.')
701
714
  )
702
715
  : null;
703
716
 
717
+ if (mainView === 'studio') {
718
+ return create(Studio);
719
+ }
720
+
704
721
  const toggleHint = showHelpCards ? 'Shift+H hides the help cards' : 'Shift+H shows the help cards';
705
722
  const headerHint = viewMode === 'detail'
706
723
  ? `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`
@@ -834,8 +851,32 @@ function parseArgs() {
834
851
  async function main() {
835
852
  const args = parseArgs();
836
853
  if (args.help) {
837
- console.log('Project Compass · Ink project runner');
838
- console.log('Usage: project-compass [--dir <path>] [--mode test] [--studio]');
854
+ console.log(kleur.cyan('Project Compass · Ink project navigator/runner'));
855
+ console.log('');
856
+ console.log(kleur.bold('Usage:'));
857
+ console.log(' project-compass [--dir <path>] [--studio]');
858
+ console.log('');
859
+ console.log(kleur.bold('Arguments:'));
860
+ console.log(' --dir, --path <path> Specify root workspace directory to scan');
861
+ console.log(' --studio Launch directly into Omni-Studio mode');
862
+ console.log(' --help, -h Show this help menu');
863
+ console.log('');
864
+ console.log(kleur.bold('Core Keybinds:'));
865
+ console.log(' ↑ / ↓ Move project focus');
866
+ console.log(' Enter Toggle detail view for selected project');
867
+ console.log(' Shift+A Switch to Omni-Studio (Environment Health)');
868
+ console.log(' Shift+X Clear the output log buffer');
869
+ console.log(' Shift+E Export current logs to a .txt file');
870
+ console.log(' Shift+↑ / ↓ Scroll the output logs back/forward');
871
+ console.log(' Shift+Q Quit application');
872
+ console.log('');
873
+ console.log(kleur.bold('Execution shortcuts:'));
874
+ console.log(' B / T / R Quick run: Build / Test / Run');
875
+ console.log(' 1-9 Run numbered commands in detail view');
876
+ console.log(' Shift+L Rerun the last executed command');
877
+ console.log(' Shift+C Add a custom command (in detail view)');
878
+ console.log('');
879
+ console.log(kleur.dim('Documentation: https://github.com/CrimsonDevil333333/project-compass'));
839
880
  return;
840
881
  }
841
882
  const rootPath = args.root ? path.resolve(args.root) : process.cwd();