project-compass 3.1.0 → 3.2.1
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/package.json +1 -1
- package/src/cli.js +97 -79
package/package.json
CHANGED
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
|
-
|
|
93
|
+
const Studio = memo(() => {
|
|
94
94
|
const [runtimes, setRuntimes] = useState([]);
|
|
95
95
|
const [loading, setLoading] = useState(true);
|
|
96
96
|
|
|
@@ -145,7 +145,25 @@ function Studio() {
|
|
|
145
145
|
create(Text, {dimColor: true}, 'Press Shift+A to return to Navigator.')
|
|
146
146
|
)
|
|
147
147
|
);
|
|
148
|
-
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const TaskManager = memo(({tasks, activeTaskId, renameMode, renameInput, renameCursor}) => {
|
|
151
|
+
return create(
|
|
152
|
+
Box,
|
|
153
|
+
{flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1},
|
|
154
|
+
create(Text, {bold: true, color: 'yellow'}, '🛰️ Task Manager | Background Processes'),
|
|
155
|
+
create(Text, {dimColor: true, marginBottom: 1}, 'Up/Down: focus, Shift+K: Kill/Delete, Shift+R: Rename'),
|
|
156
|
+
...tasks.map(t => create(
|
|
157
|
+
Box,
|
|
158
|
+
{key: t.id, marginBottom: 0, flexDirection: 'column'},
|
|
159
|
+
t.id === activeTaskId && renameMode
|
|
160
|
+
? create(Box, {flexDirection: 'row'}, create(Text, {color: 'cyan'}, '→ Rename to: '), create(CursorText, {value: renameInput, cursorIndex: renameCursor}))
|
|
161
|
+
: create(Text, {color: t.id === activeTaskId ? 'cyan' : 'white', bold: t.id === activeTaskId}, `${t.id === activeTaskId ? '→' : ' '} [${t.status.toUpperCase()}] ${t.name}`)
|
|
162
|
+
)),
|
|
163
|
+
!tasks.length && create(Text, {dimColor: true}, 'No active or background tasks.'),
|
|
164
|
+
create(Text, {marginTop: 1, dimColor: true}, 'Press Enter or Shift+T to return to Navigator.')
|
|
165
|
+
);
|
|
166
|
+
});
|
|
149
167
|
|
|
150
168
|
function CursorText({value, cursorIndex, active = true}) {
|
|
151
169
|
const before = value.slice(0, cursorIndex);
|
|
@@ -161,6 +179,32 @@ function CursorText({value, cursorIndex, active = true}) {
|
|
|
161
179
|
);
|
|
162
180
|
}
|
|
163
181
|
|
|
182
|
+
const OutputPanel = memo(({activeTask, logOffset}) => {
|
|
183
|
+
const logs = activeTask?.logs || [];
|
|
184
|
+
const logWindowStart = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE - logOffset);
|
|
185
|
+
const logWindowEnd = Math.max(0, logs.length - logOffset);
|
|
186
|
+
const visibleLogs = logs.slice(logWindowStart, logWindowEnd);
|
|
187
|
+
|
|
188
|
+
const logNodes = visibleLogs.length
|
|
189
|
+
? visibleLogs.map((line, i) => create(Text, {key: i}, line))
|
|
190
|
+
: [create(Text, {key: 'empty', dimColor: true}, 'Select a task or run a command to see logs.')];
|
|
191
|
+
|
|
192
|
+
return create(
|
|
193
|
+
Box,
|
|
194
|
+
{
|
|
195
|
+
flexDirection: 'column',
|
|
196
|
+
borderStyle: 'round',
|
|
197
|
+
borderColor: 'yellow',
|
|
198
|
+
padding: 1,
|
|
199
|
+
minHeight: OUTPUT_WINDOW_HEIGHT,
|
|
200
|
+
maxHeight: OUTPUT_WINDOW_HEIGHT,
|
|
201
|
+
height: OUTPUT_WINDOW_HEIGHT,
|
|
202
|
+
overflow: 'hidden'
|
|
203
|
+
},
|
|
204
|
+
...logNodes
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
164
208
|
function Compass({rootPath, initialView = 'navigator'}) {
|
|
165
209
|
const {exit} = useApp();
|
|
166
210
|
const {projects, loading, error} = useScanner(rootPath);
|
|
@@ -279,8 +323,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
279
323
|
const exportPath = path.resolve(process.cwd(), `compass-${activeTask.id}.txt`);
|
|
280
324
|
fs.writeFileSync(exportPath, activeTask.logs.join('\n'));
|
|
281
325
|
addLogToTask(activeTaskId, kleur.green(`✓ Logs exported to ${exportPath}`));
|
|
282
|
-
} catch
|
|
283
|
-
addLogToTask(activeTaskId, kleur.red(
|
|
326
|
+
} catch {
|
|
327
|
+
addLogToTask(activeTaskId, kleur.red('✗ Export failed'));
|
|
284
328
|
}
|
|
285
329
|
}, [activeTask, activeTaskId, addLogToTask]);
|
|
286
330
|
|
|
@@ -464,117 +508,80 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
464
508
|
}
|
|
465
509
|
});
|
|
466
510
|
|
|
467
|
-
const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}
|
|
511
|
+
const projectCountLabel = useMemo(() => `${projects.length} project${projects.length === 1 ? '' : 's'}`, [projects.length]);
|
|
468
512
|
const toggleHint = config.showHelpCards ? 'Shift+H hide help' : 'Shift+H show help';
|
|
469
513
|
const statusHint = activeTask ? `[${activeTask.status.toUpperCase()}] ${activeTask.name}` : 'Idle Navigator';
|
|
470
514
|
const orbitHint = mainView === 'tasks' ? 'Tasks View' : `Orbit: ${tasks.length} tasks`;
|
|
471
515
|
const artHint = config.showArtBoard ? 'Shift+B hide art' : 'Shift+B show art';
|
|
472
516
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (mainView === 'tasks') {
|
|
480
|
-
return create(
|
|
481
|
-
Box,
|
|
482
|
-
{flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1},
|
|
483
|
-
create(Text, {bold: true, color: 'yellow'}, '🛰️ Task Manager | Background Processes'),
|
|
484
|
-
create(Text, {dimColor: true, marginBottom: 1}, 'Up/Down: focus, Shift+K: Kill/Delete, Shift+R: Rename'),
|
|
485
|
-
...tasks.map(t => create(
|
|
486
|
-
Box,
|
|
487
|
-
{key: t.id, marginBottom: 0, flexDirection: 'column'},
|
|
488
|
-
t.id === activeTaskId && renameMode
|
|
489
|
-
? create(Box, {flexDirection: 'row'}, create(Text, {color: 'cyan'}, '→ Rename to: '), create(CursorText, {value: renameInput, cursorIndex: renameCursor}))
|
|
490
|
-
: create(Text, {color: t.id === activeTaskId ? 'cyan' : 'white', bold: t.id === activeTaskId}, `${t.id === activeTaskId ? '→' : ' '} [${t.status.toUpperCase()}] ${t.name}`)
|
|
491
|
-
)),
|
|
492
|
-
!tasks.length && create(Text, {dimColor: true}, 'No active or background tasks.'),
|
|
493
|
-
create(Text, {marginTop: 1, dimColor: true}, 'Press Enter or Shift+T to return to Navigator.')
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
|
|
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) => {
|
|
517
|
+
const projectRows = useMemo(() => {
|
|
518
|
+
if (loading) return [create(Text, {key: 'scanning', dimColor: true}, 'Scanning projects…')];
|
|
519
|
+
if (error) return [create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`)];
|
|
520
|
+
if (projects.length === 0) return [create(Text, {key: 'empty', dimColor: true}, 'No recognizable project manifests found.')];
|
|
521
|
+
|
|
522
|
+
return projects.map((project, index) => {
|
|
504
523
|
const isSelected = index === selectedIndex;
|
|
505
524
|
const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
|
|
506
525
|
const hasMissingRuntime = project.missingBinaries && project.missingBinaries.length > 0;
|
|
507
|
-
|
|
526
|
+
return create(
|
|
527
|
+
Box,
|
|
528
|
+
{key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1},
|
|
508
529
|
create(
|
|
509
530
|
Box,
|
|
510
|
-
{
|
|
511
|
-
create(
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
),
|
|
517
|
-
create(Text, {dimColor: true}, ` ${project.type} · ${path.relative(rootPath, project.path) || '.'}`),
|
|
518
|
-
frameworkBadges && create(Text, {dimColor: true}, ` ${frameworkBadges}`)
|
|
519
|
-
)
|
|
531
|
+
{flexDirection: 'row'},
|
|
532
|
+
create(Text, {color: isSelected ? 'cyan' : 'white', bold: isSelected}, `${project.icon} ${project.name}`),
|
|
533
|
+
hasMissingRuntime && create(Text, {color: 'red', bold: true}, ' ⚠️ Runtime missing')
|
|
534
|
+
),
|
|
535
|
+
create(Text, {dimColor: true}, ` ${project.type} · ${path.relative(rootPath, project.path) || '.'}`),
|
|
536
|
+
frameworkBadges && create(Text, {dimColor: true}, ` ${frameworkBadges}`)
|
|
520
537
|
);
|
|
521
538
|
});
|
|
522
|
-
}
|
|
539
|
+
}, [loading, error, projects, selectedIndex, rootPath]);
|
|
523
540
|
|
|
524
|
-
const detailContent =
|
|
525
|
-
|
|
526
|
-
|
|
541
|
+
const detailContent = useMemo(() => {
|
|
542
|
+
if (viewMode !== 'detail' || !selectedProject) {
|
|
543
|
+
return [create(Text, {key: 'e-h', dimColor: true}, 'Press Enter on a project to reveal details.')];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const content = [
|
|
527
547
|
create(Box, {key: 'title-row', flexDirection: 'row'},
|
|
528
548
|
create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
|
|
529
549
|
selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' ⚠️ MISSING RUNTIME')
|
|
530
550
|
),
|
|
531
551
|
create(Text, {key: 'manifest', dimColor: true}, `${selectedProject.type} · ${selectedProject.manifest || 'detected manifest'}`),
|
|
532
552
|
create(Text, {key: 'loc', dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
|
|
533
|
-
|
|
534
|
-
if (selectedProject.description)
|
|
553
|
+
];
|
|
554
|
+
if (selectedProject.description) content.push(create(Text, {key: 'desc'}, selectedProject.description));
|
|
535
555
|
const frameworks = (selectedProject.frameworks || []).map((lib) => `${lib.icon} ${lib.name}`).join(', ');
|
|
536
|
-
if (frameworks)
|
|
556
|
+
if (frameworks) content.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
|
|
537
557
|
|
|
538
558
|
if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
|
|
539
|
-
|
|
559
|
+
content.push(
|
|
540
560
|
create(Text, {key: 'm-t', color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
|
|
541
561
|
create(Text, {key: 'm-l', color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`)
|
|
542
562
|
);
|
|
543
563
|
}
|
|
544
564
|
|
|
545
|
-
|
|
565
|
+
content.push(create(Text, {key: 'cmd-header', bold: true, marginTop: 1}, 'Commands'));
|
|
546
566
|
detailedIndexed.forEach((command) => {
|
|
547
|
-
|
|
567
|
+
content.push(
|
|
548
568
|
create(Text, {key: `d-${command.shortcut}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : ''}`),
|
|
549
569
|
create(Text, {key: `dl-${command.shortcut}`, dimColor: true}, ` ↳ ${command.command.join(' ')}`)
|
|
550
570
|
);
|
|
551
571
|
});
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
572
|
+
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.'));
|
|
573
|
+
return content;
|
|
574
|
+
}, [viewMode, selectedProject, rootPath, detailedIndexed]);
|
|
556
575
|
|
|
557
|
-
const artTileNodes = [
|
|
576
|
+
const artTileNodes = useMemo(() => [
|
|
558
577
|
{label: 'Pulse', detail: projectCountLabel, accent: 'magenta', icon: '●', subtext: `Workspace · ${path.basename(rootPath) || rootPath}`},
|
|
559
578
|
{label: 'Focus', detail: selectedProject?.name || 'Selection', accent: 'cyan', icon: '◆', subtext: `${selectedProject?.type || 'Stack'}`},
|
|
560
|
-
{label: 'Orbit', detail: `${tasks.length}
|
|
579
|
+
{label: 'Orbit', detail: `${tasks.length} tasks`, accent: 'yellow', icon: '■', subtext: running ? 'Busy streaming...' : 'Idle'}
|
|
561
580
|
].map(tile => create(Box, {key: tile.label, flexDirection: 'column', padding: 1, marginRight: 1, borderStyle: 'single', borderColor: tile.accent, minWidth: 24},
|
|
562
581
|
create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
|
|
563
582
|
create(Text, {bold: true}, tile.detail),
|
|
564
583
|
create(Text, {dimColor: true}, tile.subtext)
|
|
565
|
-
));
|
|
566
|
-
|
|
567
|
-
const artBoard = config.showArtBoard ? create(Box, {flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
|
|
568
|
-
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')),
|
|
569
|
-
create(Box, {flexDirection: 'row', marginTop: 1}, ...ART_CHARS.map((char, i) => create(Text, {key: i, color: ART_COLORS[i % ART_COLORS.length]}, char.repeat(2)))),
|
|
570
|
-
create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
|
|
571
|
-
) : null;
|
|
572
|
-
|
|
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.')];
|
|
584
|
+
)), [projectCountLabel, rootPath, selectedProject, tasks.length, running]);
|
|
578
585
|
|
|
579
586
|
const helpCards = [
|
|
580
587
|
{label: 'Navigation', color: 'magenta', body: ['↑ / ↓ move focus, Enter: details', 'Shift+↑ / ↓ scroll output', 'Shift+H toggle help cards', 'Shift+D detach from task']},
|
|
@@ -582,6 +589,13 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
582
589
|
{label: 'Orbit & Studio', color: 'yellow', body: ['Shift+T task manager', 'Shift+A studio / Shift+B art board', 'Shift+S structure / Shift+Q quit']}
|
|
583
590
|
];
|
|
584
591
|
|
|
592
|
+
if (quitConfirm) {
|
|
593
|
+
return create(Box, {flexDirection: 'column', borderStyle: 'round', borderColor: 'red', padding: 1}, create(Text, {bold: true, color: 'red'}, '⚠️ Confirm Exit'), create(Text, null, `There are ${tasks.filter(t=>t.status==='running').length} tasks still running in the background.`), create(Text, null, 'Are you sure you want to quit and stop all processes?'), create(Text, {marginTop: 1}, kleur.bold('Y') + ' to Quit, ' + kleur.bold('N') + ' to Cancel'));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (mainView === 'studio') return create(Studio);
|
|
597
|
+
if (mainView === 'tasks') return create(TaskManager, {tasks, activeTaskId, renameMode, renameInput, renameCursor});
|
|
598
|
+
|
|
585
599
|
return create(Box, {flexDirection: 'column', padding: 1},
|
|
586
600
|
create(Box, {justifyContent: 'space-between'},
|
|
587
601
|
create(Box, {flexDirection: 'column'}, create(Text, {color: 'magenta', bold: true}, 'Project Compass'), create(Text, {dimColor: true}, `${projectCountLabel} detected in ${rootPath}`)),
|
|
@@ -590,14 +604,18 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
590
604
|
create(Text, {dimColor: true}, `${toggleHint} · ${orbitHint} · ${artHint} · Shift+Q: Quit`)
|
|
591
605
|
)
|
|
592
606
|
),
|
|
593
|
-
|
|
607
|
+
config.showArtBoard && create(Box, {flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
|
|
608
|
+
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')),
|
|
609
|
+
create(Box, {flexDirection: 'row', marginTop: 1}, ...ART_CHARS.map((char, i) => create(Text, {key: i, color: ART_COLORS[i % ART_COLORS.length]}, char.repeat(2)))),
|
|
610
|
+
create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
|
|
611
|
+
),
|
|
594
612
|
create(Box, {marginTop: 1, flexDirection: 'row', alignItems: 'stretch', width: '100%', flexWrap: 'wrap'},
|
|
595
613
|
create(Box, {flexGrow: 1, flexBasis: 0, minWidth: PROJECTS_MIN_WIDTH, marginRight: 1, borderStyle: 'round', borderColor: 'magenta', padding: 1}, create(Text, {bold: true, color: 'magenta'}, 'Projects'), create(Box, {flexDirection: 'column', marginTop: 1}, ...projectRows)),
|
|
596
614
|
create(Box, {flexGrow: 1.3, flexBasis: 0, minWidth: DETAILS_MIN_WIDTH, borderStyle: 'round', borderColor: 'cyan', padding: 1, flexDirection: 'column'}, create(Text, {bold: true, color: 'cyan'}, 'Details'), ...detailContent)
|
|
597
615
|
),
|
|
598
616
|
create(Box, {marginTop: 1, flexDirection: 'column'},
|
|
599
617
|
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(
|
|
618
|
+
create(OutputPanel, {activeTask, logOffset}),
|
|
601
619
|
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
620
|
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
621
|
),
|