project-compass 1.0.0 → 1.0.2
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 +23 -2
- package/eslint.config.cjs +20 -0
- package/package.json +7 -2
- package/project-compass-1.0.2.tgz +0 -0
- package/src/cli.js +293 -53
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@ Project Compass is a futuristic CLI navigator built with [Ink](https://github.co
|
|
|
5
5
|
## Highlights
|
|
6
6
|
|
|
7
7
|
- 🔍 Scans directories for Node.js, Python, Rust, Go, Java, and Scala projects by looking at their manifest files.
|
|
8
|
-
-
|
|
8
|
+
- 🎨 Combines the glyph-based art board with a Projects/Details row that keeps everything inside the viewport, while live stdout/stderr logs stay in their own band below.
|
|
9
9
|
- 🚀 Press **Enter** on any project to open the detail view, where you can inspect the type, manifest, frameworks, commands, and save custom actions.
|
|
10
|
+
- 💡 A dedicated output row + help tiles keep logs in one pane, support smooth refresh (Shift+↑/↓ or mouse wheel to scroll, typing feeds stdin, Ctrl+C aborts, L reruns the last command) and include a `?` overlay for quick navigation tips.
|
|
10
11
|
- 🎯 Built-in shortcuts (B/T/R) run the canonical build/test/run workflow, while numeric hotkeys (1, 2, 3...) execute whichever command is listed in the detail view.
|
|
11
12
|
- 🧠 Add bespoke commands via **C** in detail view and store them globally (`~/.project-compass/config.json`) so every workspace remembers your favorite invocations.
|
|
12
13
|
- 🔌 Extend detection via plugins (JSON specs under `~/.project-compass/plugins.json`) to teach Project Compass about extra frameworks or command sets.
|
|
@@ -33,6 +34,10 @@ project-compass [--dir /path/to/workspace]
|
|
|
33
34
|
| B / T / R | Quick build / test / run actions (when available) |
|
|
34
35
|
| 1‑9 | Execute the numbered command inside the detail view |
|
|
35
36
|
| C | Add a custom command (`label|cmd`) that saves to `~/.project-compass/config.json` |
|
|
37
|
+
| Shift ↑ / ↓ | Scroll the output buffer (Shift+arrows or mouse wheel) |
|
|
38
|
+
| L | Rerun the last executed command |
|
|
39
|
+
| ? | Toggle the help overlay with navigation tips |
|
|
40
|
+
| Ctrl+C | Interrupt a running command (works while streaming output) |
|
|
36
41
|
| Q | Quit |
|
|
37
42
|
|
|
38
43
|
## Framework & plugin support
|
|
@@ -60,6 +65,15 @@ You can teach it new frameworks by adding a `plugins.json` file in your config d
|
|
|
60
65
|
|
|
61
66
|
Each command value can be a string or an array of tokens. When a plugin matches a project, its commands appear in the detail view with a `framework` badge, and the shortcut keys (B/T/R or numeric) can execute them.
|
|
62
67
|
|
|
68
|
+
|
|
69
|
+
## Art board & detail view
|
|
70
|
+
|
|
71
|
+
Project Compass now opens with a rounded art board that shuffles your glyph row (▁▃▄▅▇ with neon accents) and three branded tiles showing workspace pulse, the selected project focus, and the rhythm of commands. The detail view sits beside the project list as a gallery; border colors, badges, and the ambient header hint keep it feeling like a living installation rather than a vanilla CLI.
|
|
72
|
+
|
|
73
|
+
## Layout, output & help
|
|
74
|
+
|
|
75
|
+
Projects and details now occupy the same row, while the output panel takes its own full-width band beneath so long logs no longer stretch the rest of the UI. The output pane scrolls independently (Shift+↑/↓ or mouse wheel), accepts keystrokes to feed stdin while a command runs, and honors Ctrl+C to abort. A trio of help tiles underneath highlights navigation cues, command flow, and recent runs, and pressing `?` opens an overlay with extra tips (for example, left-click cycles the project focus, L reruns the last command, and the help overlay reminds you about the new shortcuts).
|
|
76
|
+
|
|
63
77
|
## Developer notes
|
|
64
78
|
|
|
65
79
|
- `npm start` launches the Ink UI in the current directory.
|
|
@@ -69,4 +83,11 @@ Each command value can be a string or an array of tokens. When a plugin matches
|
|
|
69
83
|
|
|
70
84
|
## License
|
|
71
85
|
|
|
72
|
-
MIT © 2026 Satyaa & Clawdy
|
|
86
|
+
MIT © 2026 Satyaa & Clawdy
|
|
87
|
+
|
|
88
|
+
## Release & packaging
|
|
89
|
+
|
|
90
|
+
- Bump `package.json`/`package-lock.json` versions (e.g., `npm version 1.0.1 --no-git-tag-version`).
|
|
91
|
+
- Run `npm run lint` and `npm run test` to validate the workspace before publishing.
|
|
92
|
+
- Create the release artifact with `npm pack` (produces `project-compass-<version>.tgz` for uploading to GitHub Releases or npm).
|
|
93
|
+
- Tag the repo `git tag v<version>` and push both commits and tags to publish the release.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const {configs} = require('@eslint/js');
|
|
2
|
+
|
|
3
|
+
module.exports = [
|
|
4
|
+
configs.recommended,
|
|
5
|
+
{
|
|
6
|
+
files: ['src/**/*.js'],
|
|
7
|
+
languageOptions: {
|
|
8
|
+
ecmaVersion: 'latest',
|
|
9
|
+
sourceType: 'module',
|
|
10
|
+
globals: {
|
|
11
|
+
process: 'readonly',
|
|
12
|
+
console: 'readonly'
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
rules: {
|
|
16
|
+
'no-console': 'off',
|
|
17
|
+
'no-undef': 'error'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-compass",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node src/cli.js",
|
|
12
|
-
"test": "node src/cli.js --mode test"
|
|
12
|
+
"test": "node src/cli.js --mode test",
|
|
13
|
+
"lint": "eslint src"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
15
16
|
"cli",
|
|
@@ -25,5 +26,9 @@
|
|
|
25
26
|
"fast-glob": "^3.3.3",
|
|
26
27
|
"ink": "^6.6.0",
|
|
27
28
|
"kleur": "^4.1.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@eslint/js": "^10.0.1",
|
|
32
|
+
"eslint": "^10.0.0"
|
|
28
33
|
}
|
|
29
34
|
}
|
|
Binary file
|
package/src/cli.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
|
2
|
+
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|
3
3
|
import {render, Box, Text, useApp, useInput} from 'ink';
|
|
4
4
|
import fastGlob from 'fast-glob';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import os from 'os';
|
|
8
|
-
import {fileURLToPath} from 'url';
|
|
9
8
|
import kleur from 'kleur';
|
|
10
9
|
import {execa} from 'execa';
|
|
11
10
|
|
|
12
11
|
const create = React.createElement;
|
|
13
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
|
|
15
12
|
const CONFIG_DIR = path.join(os.homedir(), '.project-compass');
|
|
16
13
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
17
14
|
const PLUGIN_FILE = path.join(CONFIG_DIR, 'plugins.json');
|
|
18
15
|
const DEFAULT_CONFIG = {customCommands: {}};
|
|
19
|
-
const DEFAULT_PLUGIN_CONFIG = {plugins: []};
|
|
20
|
-
|
|
21
16
|
function ensureConfigDir() {
|
|
22
17
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
23
18
|
fs.mkdirSync(CONFIG_DIR, {recursive: true});
|
|
@@ -774,6 +769,10 @@ const ACTION_MAP = {
|
|
|
774
769
|
t: 'test',
|
|
775
770
|
r: 'run'
|
|
776
771
|
};
|
|
772
|
+
const ART_CHARS = ['▁', '▃', '▄', '▅', '▇'];
|
|
773
|
+
const ART_COLORS = ['magenta', 'blue', 'cyan', 'yellow', 'red'];
|
|
774
|
+
const OUTPUT_WINDOW_SIZE = 10;
|
|
775
|
+
const RECENT_RUN_LIMIT = 5;
|
|
777
776
|
|
|
778
777
|
function useScanner(rootPath) {
|
|
779
778
|
const [state, setState] = useState({projects: [], loading: true, error: null});
|
|
@@ -823,16 +822,25 @@ function Compass({rootPath}) {
|
|
|
823
822
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
824
823
|
const [viewMode, setViewMode] = useState('list');
|
|
825
824
|
const [logLines, setLogLines] = useState([]);
|
|
825
|
+
const [logOffset, setLogOffset] = useState(0);
|
|
826
826
|
const [running, setRunning] = useState(false);
|
|
827
827
|
const [lastAction, setLastAction] = useState(null);
|
|
828
828
|
const [customMode, setCustomMode] = useState(false);
|
|
829
829
|
const [customInput, setCustomInput] = useState('');
|
|
830
830
|
const [config, setConfig] = useState(() => loadConfig());
|
|
831
|
-
|
|
831
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
832
|
+
const [recentRuns, setRecentRuns] = useState([]);
|
|
832
833
|
const selectedProject = projects[selectedIndex] || null;
|
|
834
|
+
const runningProcessRef = useRef(null);
|
|
835
|
+
const lastCommandRef = useRef(null);
|
|
833
836
|
|
|
834
837
|
const addLog = useCallback((line) => {
|
|
835
|
-
setLogLines((prev) =>
|
|
838
|
+
setLogLines((prev) => {
|
|
839
|
+
const normalized = typeof line === 'string' ? line : JSON.stringify(line);
|
|
840
|
+
const next = [...prev, normalized];
|
|
841
|
+
return next.length > 250 ? next.slice(next.length - 250) : next;
|
|
842
|
+
});
|
|
843
|
+
setLogOffset(0);
|
|
836
844
|
}, []);
|
|
837
845
|
|
|
838
846
|
const detailCommands = useMemo(() => buildDetailCommands(selectedProject, config), [selectedProject, config]);
|
|
@@ -846,8 +854,9 @@ function Compass({rootPath}) {
|
|
|
846
854
|
return map;
|
|
847
855
|
}, [detailedIndexed]);
|
|
848
856
|
|
|
849
|
-
const runProjectCommand = useCallback(async (commandMeta) => {
|
|
850
|
-
|
|
857
|
+
const runProjectCommand = useCallback(async (commandMeta, targetProject = selectedProject) => {
|
|
858
|
+
const project = targetProject || selectedProject;
|
|
859
|
+
if (!project) {
|
|
851
860
|
return;
|
|
852
861
|
}
|
|
853
862
|
if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) {
|
|
@@ -859,16 +868,24 @@ function Compass({rootPath}) {
|
|
|
859
868
|
return;
|
|
860
869
|
}
|
|
861
870
|
|
|
871
|
+
const commandLabel = commandMeta.label || commandMeta.command.join(' ');
|
|
872
|
+
lastCommandRef.current = {project, commandMeta};
|
|
862
873
|
setRunning(true);
|
|
863
|
-
setLastAction(`${
|
|
874
|
+
setLastAction(`${project.name} · ${commandLabel}`);
|
|
864
875
|
const fullCmd = commandMeta.command;
|
|
865
876
|
addLog(kleur.cyan(`> ${fullCmd.join(' ')}`));
|
|
877
|
+
setRecentRuns((prev) => {
|
|
878
|
+
const entry = {project: project.name, command: commandLabel, time: new Date().toLocaleTimeString()};
|
|
879
|
+
return [entry, ...prev].slice(0, RECENT_RUN_LIMIT);
|
|
880
|
+
});
|
|
866
881
|
|
|
867
882
|
try {
|
|
868
883
|
const subprocess = execa(fullCmd[0], fullCmd.slice(1), {
|
|
869
|
-
cwd:
|
|
870
|
-
env: process.env
|
|
884
|
+
cwd: project.path,
|
|
885
|
+
env: process.env,
|
|
886
|
+
stdin: 'pipe'
|
|
871
887
|
});
|
|
888
|
+
runningProcessRef.current = subprocess;
|
|
872
889
|
|
|
873
890
|
subprocess.stdout?.on('data', (chunk) => {
|
|
874
891
|
addLog(chunk.toString().trimEnd());
|
|
@@ -878,13 +895,14 @@ function Compass({rootPath}) {
|
|
|
878
895
|
});
|
|
879
896
|
|
|
880
897
|
await subprocess;
|
|
881
|
-
addLog(kleur.green(`✓ ${
|
|
898
|
+
addLog(kleur.green(`✓ ${commandLabel} finished`));
|
|
882
899
|
} catch (error) {
|
|
883
|
-
addLog(kleur.red(`✗ ${
|
|
900
|
+
addLog(kleur.red(`✗ ${commandLabel} failed: ${error.shortMessage || error.message}`));
|
|
884
901
|
} finally {
|
|
885
902
|
setRunning(false);
|
|
903
|
+
runningProcessRef.current = null;
|
|
886
904
|
}
|
|
887
|
-
}, [
|
|
905
|
+
}, [addLog, running, selectedProject]);
|
|
888
906
|
|
|
889
907
|
const handleAddCustomCommand = useCallback((label, commandTokens) => {
|
|
890
908
|
if (!selectedProject) {
|
|
@@ -954,11 +972,57 @@ function Compass({rootPath}) {
|
|
|
954
972
|
return;
|
|
955
973
|
}
|
|
956
974
|
|
|
957
|
-
if (key.
|
|
975
|
+
if (key.mouse) {
|
|
976
|
+
if (key.mouse === 'left' && projects.length > 0) {
|
|
977
|
+
setSelectedIndex((prev) => (prev + 1) % projects.length);
|
|
978
|
+
} else if (key.mouse === 'scrollUp') {
|
|
979
|
+
const maxScroll = Math.max(0, logLines.length - OUTPUT_WINDOW_SIZE);
|
|
980
|
+
setLogOffset((prev) => Math.min(maxScroll, prev + 1));
|
|
981
|
+
} else if (key.mouse === 'scrollDown') {
|
|
982
|
+
setLogOffset((prev) => Math.max(0, prev - 1));
|
|
983
|
+
}
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (running && runningProcessRef.current) {
|
|
988
|
+
if (key.ctrl && input === 'c') {
|
|
989
|
+
runningProcessRef.current.kill('SIGINT');
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (key.return) {
|
|
993
|
+
runningProcessRef.current.stdin?.write('\n');
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (input) {
|
|
997
|
+
runningProcessRef.current.stdin?.write(input);
|
|
998
|
+
}
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (key.shift && key.upArrow) {
|
|
1003
|
+
const maxScroll = Math.max(0, logLines.length - OUTPUT_WINDOW_SIZE);
|
|
1004
|
+
setLogOffset((prev) => Math.min(maxScroll, prev + 1));
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (key.shift && key.downArrow) {
|
|
1008
|
+
setLogOffset((prev) => Math.max(0, prev - 1));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (input === '?') {
|
|
1013
|
+
setShowHelp((prev) => !prev);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (input === 'l' && lastCommandRef.current) {
|
|
1017
|
+
runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (key.upArrow && !key.shift && projects.length > 0) {
|
|
958
1022
|
setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length);
|
|
959
1023
|
return;
|
|
960
1024
|
}
|
|
961
|
-
if (key.downArrow && projects.length > 0) {
|
|
1025
|
+
if (key.downArrow && !key.shift && projects.length > 0) {
|
|
962
1026
|
setSelectedIndex((prev) => (prev + 1) % projects.length);
|
|
963
1027
|
return;
|
|
964
1028
|
}
|
|
@@ -980,17 +1044,17 @@ function Compass({rootPath}) {
|
|
|
980
1044
|
}
|
|
981
1045
|
if (ACTION_MAP[input]) {
|
|
982
1046
|
const commandMeta = selectedProject?.commands?.[ACTION_MAP[input]];
|
|
983
|
-
runProjectCommand(commandMeta);
|
|
1047
|
+
runProjectCommand(commandMeta, selectedProject);
|
|
984
1048
|
return;
|
|
985
1049
|
}
|
|
986
1050
|
if (viewMode === 'detail' && detailShortcutMap.has(input)) {
|
|
987
|
-
runProjectCommand(detailShortcutMap.get(input));
|
|
1051
|
+
runProjectCommand(detailShortcutMap.get(input), selectedProject);
|
|
988
1052
|
}
|
|
989
1053
|
});
|
|
990
1054
|
|
|
991
1055
|
const projectRows = [];
|
|
992
1056
|
if (loading) {
|
|
993
|
-
projectRows.push(create(Text, {dimColor: true}, 'Scanning
|
|
1057
|
+
projectRows.push(create(Text, {dimColor: true}, 'Scanning projects…'));
|
|
994
1058
|
}
|
|
995
1059
|
if (error) {
|
|
996
1060
|
projectRows.push(create(Text, {color: 'red'}, `Unable to scan: ${error}`));
|
|
@@ -1000,24 +1064,21 @@ function Compass({rootPath}) {
|
|
|
1000
1064
|
}
|
|
1001
1065
|
if (!loading) {
|
|
1002
1066
|
projects.forEach((project, index) => {
|
|
1067
|
+
const isSelected = index === selectedIndex;
|
|
1003
1068
|
const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
|
|
1004
1069
|
projectRows.push(
|
|
1005
1070
|
create(
|
|
1006
1071
|
Box,
|
|
1007
|
-
{key: project.id, flexDirection: 'column', marginBottom: 1},
|
|
1072
|
+
{key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1},
|
|
1008
1073
|
create(
|
|
1009
|
-
|
|
1010
|
-
{
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
bold: index === selectedIndex
|
|
1016
|
-
},
|
|
1017
|
-
`${project.icon} ${project.name}`
|
|
1018
|
-
),
|
|
1019
|
-
create(Text, {dimColor: true}, ` ${project.type} · ${path.relative(rootPath, project.path) || '.'}`)
|
|
1074
|
+
Text,
|
|
1075
|
+
{
|
|
1076
|
+
color: isSelected ? 'cyan' : 'white',
|
|
1077
|
+
bold: isSelected
|
|
1078
|
+
},
|
|
1079
|
+
`${project.icon} ${project.name}`
|
|
1020
1080
|
),
|
|
1081
|
+
create(Text, {dimColor: true}, ` ${project.type} · ${path.relative(rootPath, project.path) || '.'}`),
|
|
1021
1082
|
frameworkBadges && create(Text, {dimColor: true}, ` ${frameworkBadges}`)
|
|
1022
1083
|
)
|
|
1023
1084
|
);
|
|
@@ -1027,7 +1088,7 @@ function Compass({rootPath}) {
|
|
|
1027
1088
|
const detailContent = [];
|
|
1028
1089
|
if (viewMode === 'detail' && selectedProject) {
|
|
1029
1090
|
detailContent.push(
|
|
1030
|
-
create(Text, {color: '
|
|
1091
|
+
create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
|
|
1031
1092
|
create(Text, {dimColor: true}, `${selectedProject.type} · ${selectedProject.manifest || 'detected manifest'}`),
|
|
1032
1093
|
create(Text, {dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
|
|
1033
1094
|
);
|
|
@@ -1046,7 +1107,7 @@ function Compass({rootPath}) {
|
|
|
1046
1107
|
detailContent.push(create(Text, {bold: true, marginTop: 1}, 'Commands'));
|
|
1047
1108
|
detailedIndexed.forEach((command) => {
|
|
1048
1109
|
detailContent.push(
|
|
1049
|
-
create(Text, {key:
|
|
1110
|
+
create(Text, {key: `detail-${command.shortcut}-${command.label}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : ''}`)
|
|
1050
1111
|
);
|
|
1051
1112
|
detailContent.push(create(Text, {dimColor: true}, ` ↳ ${command.command.join(' ')}`));
|
|
1052
1113
|
});
|
|
@@ -1062,10 +1123,162 @@ function Compass({rootPath}) {
|
|
|
1062
1123
|
detailContent.push(create(Text, {color: 'cyan'}, `Type label|cmd (Enter to save, Esc to cancel): ${customInput}`));
|
|
1063
1124
|
}
|
|
1064
1125
|
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1126
|
+
const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
|
|
1127
|
+
const artTileNodes = useMemo(() => {
|
|
1128
|
+
const selectedName = selectedProject?.name || 'Awaiting selection';
|
|
1129
|
+
const selectedType = selectedProject?.type || 'Unknown stack';
|
|
1130
|
+
const selectedLocation = selectedProject?.path ? path.relative(rootPath, selectedProject.path) || '.' : '—';
|
|
1131
|
+
const statusNarrative = running ? 'Running commands' : lastAction ? `Last: ${lastAction}` : 'Idle gallery';
|
|
1132
|
+
const workspaceName = path.basename(rootPath) || rootPath;
|
|
1133
|
+
const tileDefinition = [
|
|
1134
|
+
{
|
|
1135
|
+
label: 'Pulse',
|
|
1136
|
+
detail: projectCountLabel,
|
|
1137
|
+
accent: 'magenta',
|
|
1138
|
+
icon: '●',
|
|
1139
|
+
subtext: `Workspace · ${workspaceName}`
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
label: 'Focus',
|
|
1143
|
+
detail: selectedName,
|
|
1144
|
+
accent: 'cyan',
|
|
1145
|
+
icon: '◆',
|
|
1146
|
+
subtext: `${selectedType} · ${selectedLocation}`
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
label: 'Rhythm',
|
|
1150
|
+
detail: `${detailCommands.length} commands`,
|
|
1151
|
+
accent: 'yellow',
|
|
1152
|
+
icon: '■',
|
|
1153
|
+
subtext: statusNarrative
|
|
1154
|
+
}
|
|
1155
|
+
];
|
|
1156
|
+
return tileDefinition.map((tile) =>
|
|
1157
|
+
create(
|
|
1158
|
+
Box,
|
|
1159
|
+
{
|
|
1160
|
+
key: tile.label,
|
|
1161
|
+
flexDirection: 'column',
|
|
1162
|
+
padding: 1,
|
|
1163
|
+
marginRight: 1,
|
|
1164
|
+
borderStyle: 'single',
|
|
1165
|
+
borderColor: tile.accent,
|
|
1166
|
+
minWidth: 24
|
|
1167
|
+
},
|
|
1168
|
+
create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
|
|
1169
|
+
create(Text, {bold: true}, tile.detail),
|
|
1170
|
+
create(Text, {dimColor: true}, tile.subtext)
|
|
1171
|
+
)
|
|
1172
|
+
);
|
|
1173
|
+
}, [projectCountLabel, rootPath, selectedProject, detailCommands.length, running, lastAction]);
|
|
1174
|
+
|
|
1175
|
+
const artBoard = create(
|
|
1176
|
+
Box,
|
|
1177
|
+
{
|
|
1178
|
+
flexDirection: 'column',
|
|
1179
|
+
marginTop: 1,
|
|
1180
|
+
borderStyle: 'round',
|
|
1181
|
+
borderColor: 'gray',
|
|
1182
|
+
padding: 1
|
|
1183
|
+
},
|
|
1184
|
+
create(
|
|
1185
|
+
Box,
|
|
1186
|
+
{flexDirection: 'row', justifyContent: 'space-between'},
|
|
1187
|
+
create(Text, {color: 'magenta', bold: true}, 'Art-coded build atlas'),
|
|
1188
|
+
create(Text, {dimColor: true}, 'press ? for overlay help')
|
|
1189
|
+
),
|
|
1190
|
+
create(
|
|
1191
|
+
Box,
|
|
1192
|
+
{flexDirection: 'row', marginTop: 1},
|
|
1193
|
+
...ART_CHARS.map((char, index) =>
|
|
1194
|
+
create(Text, {key: `art-${index}`, color: ART_COLORS[index % ART_COLORS.length]}, char.repeat(2))
|
|
1195
|
+
)
|
|
1196
|
+
),
|
|
1197
|
+
create(
|
|
1198
|
+
Box,
|
|
1199
|
+
{flexDirection: 'row', marginTop: 1},
|
|
1200
|
+
...artTileNodes
|
|
1201
|
+
),
|
|
1202
|
+
create(Text, {dimColor: true, marginTop: 1}, kleur.italic('The art board now follows your layout and stays inside the window.'))
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
const logWindowStart = Math.max(0, logLines.length - OUTPUT_WINDOW_SIZE - logOffset);
|
|
1206
|
+
const logWindowEnd = Math.max(0, logLines.length - logOffset);
|
|
1207
|
+
const visibleLogs = logLines.slice(logWindowStart, logWindowEnd);
|
|
1208
|
+
const logNodes = visibleLogs.length
|
|
1209
|
+
? visibleLogs.map((line, index) => create(Text, {key: `${logWindowStart + index}-${line}`}, line))
|
|
1067
1210
|
: [create(Text, {dimColor: true}, 'Logs will appear here once you run a command.')];
|
|
1068
1211
|
|
|
1212
|
+
const helpCards = [
|
|
1213
|
+
{
|
|
1214
|
+
label: 'Navigation',
|
|
1215
|
+
color: 'magenta',
|
|
1216
|
+
body: [
|
|
1217
|
+
'↑/↓ select projects, Enter toggles detail view',
|
|
1218
|
+
'Shift+↑/↓ or mouse wheel scrolls logs',
|
|
1219
|
+
'? toggles this overlay, left click cycles the list'
|
|
1220
|
+
]
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
label: 'Command flow',
|
|
1224
|
+
color: 'cyan',
|
|
1225
|
+
body: [
|
|
1226
|
+
'B/T/R run build/test/run for the focused project',
|
|
1227
|
+
'1-9 execute the numbered detail commands',
|
|
1228
|
+
'L reruns the last command, Ctrl+C aborts it',
|
|
1229
|
+
'Type while a command is running to feed stdin'
|
|
1230
|
+
]
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
label: 'Recent runs',
|
|
1234
|
+
color: 'yellow',
|
|
1235
|
+
body: recentRuns.length
|
|
1236
|
+
? recentRuns.map((run) => `${run.time} · ${run.project}: ${run.command}`)
|
|
1237
|
+
: ['No runs yet. Start one with B/T/R.']
|
|
1238
|
+
}
|
|
1239
|
+
];
|
|
1240
|
+
|
|
1241
|
+
const helpSection = create(
|
|
1242
|
+
Box,
|
|
1243
|
+
{marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'},
|
|
1244
|
+
...helpCards.map((card, index) =>
|
|
1245
|
+
create(
|
|
1246
|
+
Box,
|
|
1247
|
+
{
|
|
1248
|
+
key: card.label,
|
|
1249
|
+
flexGrow: 1,
|
|
1250
|
+
flexBasis: 0,
|
|
1251
|
+
marginRight: index < helpCards.length - 1 ? 1 : 0,
|
|
1252
|
+
borderStyle: 'round',
|
|
1253
|
+
borderColor: card.color,
|
|
1254
|
+
padding: 1
|
|
1255
|
+
},
|
|
1256
|
+
create(Text, {color: card.color, bold: true}, card.label),
|
|
1257
|
+
...card.body.map((line, lineIndex) =>
|
|
1258
|
+
create(Text, {key: `${card.label}-${lineIndex}`, dimColor: card.color === 'yellow'}, line)
|
|
1259
|
+
)
|
|
1260
|
+
)
|
|
1261
|
+
)
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
const helpOverlay = showHelp
|
|
1265
|
+
? create(
|
|
1266
|
+
Box,
|
|
1267
|
+
{
|
|
1268
|
+
flexDirection: 'column',
|
|
1269
|
+
borderStyle: 'double',
|
|
1270
|
+
borderColor: 'cyan',
|
|
1271
|
+
marginTop: 1,
|
|
1272
|
+
padding: 1
|
|
1273
|
+
},
|
|
1274
|
+
create(Text, {color: 'cyan', bold: true}, 'Help overlay · press ? to hide'),
|
|
1275
|
+
create(Text, null, 'Clicking cycles selections and scrolling the mouse wheel moves the output view.'),
|
|
1276
|
+
create(Text, null, 'When a command is running you can type to feed stdin, Enter sends newline, Ctrl+C aborts.'),
|
|
1277
|
+
create(Text, null, 'Layout now keeps Projects and Details side-by-side with Output below so nothing stretches.'),
|
|
1278
|
+
create(Text, null, 'Use L to rerun the previous command and Shift + arrows or the mouse wheel to scroll the output window.')
|
|
1279
|
+
)
|
|
1280
|
+
: null;
|
|
1281
|
+
|
|
1069
1282
|
const headerHint = viewMode === 'detail'
|
|
1070
1283
|
? `Detail mode · 1-${Math.max(detailedIndexed.length, 1)} to execute, C: add custom commands, Enter: back to list, q: quit`
|
|
1071
1284
|
: `Quick run · B/T/R to build/test/run, Enter: view details, q: quit`;
|
|
@@ -1079,53 +1292,80 @@ function Compass({rootPath}) {
|
|
|
1079
1292
|
create(
|
|
1080
1293
|
Box,
|
|
1081
1294
|
{flexDirection: 'column'},
|
|
1082
|
-
create(Text, {color: '
|
|
1083
|
-
create(Text,
|
|
1295
|
+
create(Text, {color: 'magenta', bold: true}, 'Project Compass'),
|
|
1296
|
+
create(Text, {dimColor: true}, loading ? 'Scanning workspaces…' : `${projectCountLabel} detected in ${rootPath}`),
|
|
1297
|
+
create(Text, {dimColor: true}, 'Mouse support: click to cycle, wheel to scroll output.')
|
|
1084
1298
|
),
|
|
1085
1299
|
create(
|
|
1086
1300
|
Box,
|
|
1087
1301
|
{flexDirection: 'column', alignItems: 'flex-end'},
|
|
1088
|
-
create(Text,
|
|
1302
|
+
create(Text, {color: running ? 'yellow' : 'green'}, running ? 'Busy streaming...' : lastAction ? `Last action · ${lastAction}` : 'Idle'),
|
|
1089
1303
|
create(Text, {dimColor: true}, headerHint)
|
|
1090
1304
|
)
|
|
1091
1305
|
),
|
|
1306
|
+
artBoard,
|
|
1092
1307
|
create(
|
|
1093
1308
|
Box,
|
|
1094
|
-
{marginTop: 1},
|
|
1309
|
+
{marginTop: 1, flexDirection: 'row', alignItems: 'stretch'},
|
|
1095
1310
|
create(
|
|
1096
1311
|
Box,
|
|
1097
|
-
{
|
|
1098
|
-
|
|
1312
|
+
{
|
|
1313
|
+
flex: 1,
|
|
1314
|
+
marginRight: 1,
|
|
1315
|
+
borderStyle: 'round',
|
|
1316
|
+
borderColor: 'magenta',
|
|
1317
|
+
padding: 1
|
|
1318
|
+
},
|
|
1319
|
+
create(Text, {bold: true, color: 'magenta'}, 'Projects'),
|
|
1099
1320
|
create(Box, {flexDirection: 'column', marginTop: 1}, ...projectRows)
|
|
1100
1321
|
),
|
|
1101
1322
|
create(
|
|
1102
1323
|
Box,
|
|
1103
1324
|
{
|
|
1104
|
-
|
|
1105
|
-
width: 44,
|
|
1106
|
-
marginRight: 2,
|
|
1325
|
+
flex: 1,
|
|
1107
1326
|
borderStyle: 'round',
|
|
1108
|
-
borderColor: '
|
|
1327
|
+
borderColor: 'cyan',
|
|
1109
1328
|
padding: 1
|
|
1110
1329
|
},
|
|
1111
|
-
create(Text, {bold: true}, 'Details'),
|
|
1330
|
+
create(Text, {bold: true, color: 'cyan'}, 'Details'),
|
|
1112
1331
|
...detailContent
|
|
1332
|
+
)
|
|
1333
|
+
),
|
|
1334
|
+
create(
|
|
1335
|
+
Box,
|
|
1336
|
+
{marginTop: 1, flexDirection: 'column'},
|
|
1337
|
+
create(
|
|
1338
|
+
Box,
|
|
1339
|
+
{flexDirection: 'row', justifyContent: 'space-between'},
|
|
1340
|
+
create(Text, {bold: true, color: 'yellow'}, `Output ${running ? '· Running' : ''}`),
|
|
1341
|
+
create(Text, {dimColor: true}, logOffset ? `Scrolled ${logOffset} lines` : 'Live log view')
|
|
1113
1342
|
),
|
|
1114
1343
|
create(
|
|
1115
1344
|
Box,
|
|
1116
1345
|
{
|
|
1117
1346
|
flexDirection: 'column',
|
|
1118
|
-
flexGrow: 1,
|
|
1119
1347
|
borderStyle: 'round',
|
|
1120
|
-
borderColor: '
|
|
1348
|
+
borderColor: 'yellow',
|
|
1349
|
+
padding: 1,
|
|
1350
|
+
minHeight: OUTPUT_WINDOW_SIZE + 2,
|
|
1351
|
+
overflow: 'hidden'
|
|
1121
1352
|
},
|
|
1122
|
-
|
|
1123
|
-
|
|
1353
|
+
...logNodes
|
|
1354
|
+
),
|
|
1355
|
+
create(
|
|
1356
|
+
Text,
|
|
1357
|
+
{dimColor: true, marginTop: 1},
|
|
1358
|
+
running
|
|
1359
|
+
? 'Type to feed stdin, Enter submits, Ctrl+C aborts, Shift+Arrows or mouse wheel scrolls the buffer.'
|
|
1360
|
+
: 'Run a command or press ? for extra help.'
|
|
1124
1361
|
)
|
|
1125
|
-
)
|
|
1362
|
+
),
|
|
1363
|
+
helpSection,
|
|
1364
|
+
helpOverlay
|
|
1126
1365
|
);
|
|
1127
1366
|
}
|
|
1128
1367
|
|
|
1368
|
+
|
|
1129
1369
|
function parseArgs() {
|
|
1130
1370
|
const args = {};
|
|
1131
1371
|
const tokens = process.argv.slice(2);
|