project-compass 3.0.1 → 3.2.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +64 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "3.0.1",
3
+ "version": "3.2.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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2
+ import React, {useCallback, useEffect, useMemo, useRef, useState, memo} from 'react';
3
3
  import {render, Box, Text, useApp, useInput} from 'ink';
4
4
  import path from 'path';
5
5
  import fs from 'fs';
@@ -90,7 +90,7 @@ function buildDetailCommands(project, config) {
90
90
  return [...builtins, ...custom];
91
91
  }
92
92
 
93
- function Studio() {
93
+ const Studio = memo(() => {
94
94
  const [runtimes, setRuntimes] = useState([]);
95
95
  const [loading, setLoading] = useState(true);
96
96
 
@@ -145,7 +145,7 @@ function Studio() {
145
145
  create(Text, {dimColor: true}, 'Press Shift+A to return to Navigator.')
146
146
  )
147
147
  );
148
- }
148
+ });
149
149
 
150
150
  function CursorText({value, cursorIndex, active = true}) {
151
151
  const before = value.slice(0, cursorIndex);
@@ -161,6 +161,32 @@ function CursorText({value, cursorIndex, active = true}) {
161
161
  );
162
162
  }
163
163
 
164
+ const OutputPanel = memo(({activeTask, logOffset}) => {
165
+ const logs = activeTask?.logs || [];
166
+ const logWindowStart = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE - logOffset);
167
+ const logWindowEnd = Math.max(0, logs.length - logOffset);
168
+ const visibleLogs = logs.slice(logWindowStart, logWindowEnd);
169
+
170
+ const logNodes = visibleLogs.length
171
+ ? visibleLogs.map((line, i) => create(Text, {key: i}, line))
172
+ : [create(Text, {key: 'empty', dimColor: true}, 'Select a task or run a command to see logs.')];
173
+
174
+ return create(
175
+ Box,
176
+ {
177
+ flexDirection: 'column',
178
+ borderStyle: 'round',
179
+ borderColor: 'yellow',
180
+ padding: 1,
181
+ minHeight: OUTPUT_WINDOW_HEIGHT,
182
+ maxHeight: OUTPUT_WINDOW_HEIGHT,
183
+ height: OUTPUT_WINDOW_HEIGHT,
184
+ overflow: 'hidden'
185
+ },
186
+ ...logNodes
187
+ );
188
+ });
189
+
164
190
  function Compass({rootPath, initialView = 'navigator'}) {
165
191
  const {exit} = useApp();
166
192
  const {projects, loading, error} = useScanner(rootPath);
@@ -494,67 +520,66 @@ function Compass({rootPath, initialView = 'navigator'}) {
494
520
  );
495
521
  }
496
522
 
497
- const projectRows = [];
498
- if (loading) projectRows.push(create(Text, {key: 'scanning', dimColor: true}, 'Scanning projects…'));
499
- if (error) projectRows.push(create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`));
500
- if (!loading && !error && projects.length === 0) projectRows.push(create(Text, {key: 'empty', dimColor: true}, 'No recognizable project manifests found.'));
501
-
502
- if (!loading) {
503
- projects.forEach((project, index) => {
523
+ const projectRows = useMemo(() => {
524
+ if (loading) return [create(Text, {key: 'scanning', dimColor: true}, 'Scanning projects…')];
525
+ if (error) return [create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`)];
526
+ if (projects.length === 0) return [create(Text, {key: 'empty', dimColor: true}, 'No recognizable project manifests found.')];
527
+
528
+ return projects.map((project, index) => {
504
529
  const isSelected = index === selectedIndex;
505
530
  const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
506
531
  const hasMissingRuntime = project.missingBinaries && project.missingBinaries.length > 0;
507
- projectRows.push(
532
+ return create(
533
+ Box,
534
+ {key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1},
508
535
  create(
509
536
  Box,
510
- {key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1},
511
- create(
512
- Box,
513
- {flexDirection: 'row'},
514
- create(Text, {color: isSelected ? 'cyan' : 'white', bold: isSelected}, `${project.icon} ${project.name}`),
515
- hasMissingRuntime && create(Text, {color: 'red', bold: true}, ' ⚠️ Runtime missing')
516
- ),
517
- create(Text, {dimColor: true}, ` ${project.type} · ${path.relative(rootPath, project.path) || '.'}`),
518
- frameworkBadges && create(Text, {dimColor: true}, ` ${frameworkBadges}`)
519
- )
537
+ {flexDirection: 'row'},
538
+ create(Text, {color: isSelected ? 'cyan' : 'white', bold: isSelected}, `${project.icon} ${project.name}`),
539
+ hasMissingRuntime && create(Text, {color: 'red', bold: true}, ' ⚠️ Runtime missing')
540
+ ),
541
+ create(Text, {dimColor: true}, ` ${project.type} · ${path.relative(rootPath, project.path) || '.'}`),
542
+ frameworkBadges && create(Text, {dimColor: true}, ` ${frameworkBadges}`)
520
543
  );
521
544
  });
522
- }
545
+ }, [loading, error, projects, selectedIndex, rootPath]);
523
546
 
524
- const detailContent = [];
525
- if (viewMode === 'detail' && selectedProject) {
526
- detailContent.push(
547
+ const detailContent = useMemo(() => {
548
+ if (viewMode !== 'detail' || !selectedProject) {
549
+ return [create(Text, {key: 'e-h', dimColor: true}, 'Press Enter on a project to reveal details.')];
550
+ }
551
+
552
+ const content = [
527
553
  create(Box, {key: 'title-row', flexDirection: 'row'},
528
554
  create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
529
555
  selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' ⚠️ MISSING RUNTIME')
530
556
  ),
531
557
  create(Text, {key: 'manifest', dimColor: true}, `${selectedProject.type} · ${selectedProject.manifest || 'detected manifest'}`),
532
558
  create(Text, {key: 'loc', dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
533
- );
534
- if (selectedProject.description) detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
559
+ ];
560
+ if (selectedProject.description) content.push(create(Text, {key: 'desc'}, selectedProject.description));
535
561
  const frameworks = (selectedProject.frameworks || []).map((lib) => `${lib.icon} ${lib.name}`).join(', ');
536
- if (frameworks) detailContent.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
562
+ if (frameworks) content.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
537
563
 
538
564
  if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
539
- detailContent.push(
565
+ content.push(
540
566
  create(Text, {key: 'm-t', color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
541
567
  create(Text, {key: 'm-l', color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`)
542
568
  );
543
569
  }
544
570
 
545
- detailContent.push(create(Text, {key: 'cmd-header', bold: true, marginTop: 1}, 'Commands'));
571
+ content.push(create(Text, {key: 'cmd-header', bold: true, marginTop: 1}, 'Commands'));
546
572
  detailedIndexed.forEach((command) => {
547
- detailContent.push(
573
+ content.push(
548
574
  create(Text, {key: `d-${command.shortcut}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : ''}`),
549
575
  create(Text, {key: `dl-${command.shortcut}`, dimColor: true}, ` ↳ ${command.command.join(' ')}`)
550
576
  );
551
577
  });
552
- detailContent.push(create(Text, {key: 'h-l', dimColor: true, marginTop: 1}, 'Press Shift+C → label|cmd to save custom actions, Enter to close detail view.'));
553
- } else {
554
- detailContent.push(create(Text, {key: 'e-h', dimColor: true}, 'Press Enter on a project to reveal details.'));
555
- }
578
+ content.push(create(Text, {key: 'h-l', dimColor: true, marginTop: 1}, 'Press Shift+C → label|cmd to save custom actions, Enter to close detail view.'));
579
+ return content;
580
+ }, [viewMode, selectedProject, rootPath, detailedIndexed]);
556
581
 
557
- const artTileNodes = [
582
+ const artTileNodes = useMemo(() => [
558
583
  {label: 'Pulse', detail: projectCountLabel, accent: 'magenta', icon: '●', subtext: `Workspace · ${path.basename(rootPath) || rootPath}`},
559
584
  {label: 'Focus', detail: selectedProject?.name || 'Selection', accent: 'cyan', icon: '◆', subtext: `${selectedProject?.type || 'Stack'}`},
560
585
  {label: 'Orbit', detail: `${tasks.length} active tasks`, accent: 'yellow', icon: '■', subtext: running ? 'Busy streaming...' : 'Idle'}
@@ -562,7 +587,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
562
587
  create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
563
588
  create(Text, {bold: true}, tile.detail),
564
589
  create(Text, {dimColor: true}, tile.subtext)
565
- ));
590
+ )), [projectCountLabel, rootPath, selectedProject, tasks.length, running]);
566
591
 
567
592
  const artBoard = config.showArtBoard ? create(Box, {flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
568
593
  create(Box, {flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {color: 'magenta', bold: true}, 'Art-coded build atlas'), create(Text, {dimColor: true}, 'press ? for overlay help')),
@@ -570,16 +595,10 @@ function Compass({rootPath, initialView = 'navigator'}) {
570
595
  create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
571
596
  ) : null;
572
597
 
573
- const logs = activeTask?.logs || [];
574
- const logWindowStart = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE - logOffset);
575
- const logWindowEnd = Math.max(0, logs.length - logOffset);
576
- const visibleLogs = logs.slice(logWindowStart, logWindowEnd);
577
- const logNodes = visibleLogs.length ? visibleLogs.map((line, i) => create(Text, {key: i}, line)) : [create(Text, {dimColor: true}, 'Select a task or run a command to see logs.')];
578
-
579
598
  const helpCards = [
580
599
  {label: 'Navigation', color: 'magenta', body: ['↑ / ↓ move focus, Enter: details', 'Shift+↑ / ↓ scroll output', 'Shift+H toggle help cards', 'Shift+D detach from task']},
581
600
  {label: 'Commands', color: 'cyan', body: ['B / T / R build/test/run', '1-9 run detail commands', 'Shift+L rerun last command', 'Shift+X clear / Shift+E export']},
582
- {label: 'Orbit & Studio', color: 'yellow', body: ['Shift+T task manager', 'Shift+A studio / Shift+B art board', 'Shift+S structure / Shift+Q quit']}
601
+ {label: 'Orbit & Studio', color: 'yellow', body: ['Shift+T task manager', 'Shift+A studio / Shift+B art board', 'Shift+C custom / Shift+Q quit']}
583
602
  ];
584
603
 
585
604
  return create(Box, {flexDirection: 'column', padding: 1},
@@ -597,7 +616,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
597
616
  ),
598
617
  create(Box, {marginTop: 1, flexDirection: 'column'},
599
618
  create(Box, {flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {bold: true, color: 'yellow'}, `Output: ${activeTask?.name || 'None'}`), create(Text, {dimColor: true}, logOffset ? `Scrolled ${logOffset} lines` : 'Live log view')),
600
- create(Box, {flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1, minHeight: OUTPUT_WINDOW_HEIGHT, maxHeight: OUTPUT_WINDOW_HEIGHT, height: OUTPUT_WINDOW_HEIGHT, overflow: 'hidden'}, ...logNodes),
619
+ create(OutputPanel, {activeTask, logOffset}),
601
620
  create(Box, {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {dimColor: true}, running ? 'Type to feed stdin; Enter: submit, Ctrl+C: abort.' : 'Run a command or press Shift+T to switch tasks.'), create(Text, {dimColor: true}, `${toggleHint}, Shift+S: Structure Guide`)),
602
621
  create(Box, {marginTop: 1, flexDirection: 'row', borderStyle: 'round', borderColor: running ? 'green' : 'gray', paddingX: 1}, create(Text, {bold: true, color: running ? 'green' : 'white'}, running ? ' Stdin buffer ' : ' Input ready '), create(Box, {marginLeft: 1}, create(CursorText, {value: stdinBuffer || (running ? '' : 'Start a command to feed stdin'), cursorIndex: stdinCursor, active: running})))
603
622
  ),