project-compass 3.2.0 β 3.3.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/package.json +1 -1
- package/src/cli.js +65 -60
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -147,6 +147,24 @@ const Studio = memo(() => {
|
|
|
147
147
|
);
|
|
148
148
|
});
|
|
149
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: Force Kill, 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
|
+
});
|
|
167
|
+
|
|
150
168
|
function CursorText({value, cursorIndex, active = true}) {
|
|
151
169
|
const before = value.slice(0, cursorIndex);
|
|
152
170
|
const charAt = value[cursorIndex] || ' ';
|
|
@@ -207,22 +225,27 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
207
225
|
const [stdinBuffer, setStdinBuffer] = useState('');
|
|
208
226
|
const [stdinCursor, setStdinCursor] = useState(0);
|
|
209
227
|
const [showHelp, setShowHelp] = useState(false);
|
|
210
|
-
const selectedProject = projects[selectedIndex] || null;
|
|
211
228
|
const runningProcessMap = useRef(new Map());
|
|
212
229
|
const lastCommandRef = useRef(null);
|
|
213
230
|
|
|
214
231
|
const activeTask = useMemo(() => tasks.find(t => t.id === activeTaskId), [tasks, activeTaskId]);
|
|
215
232
|
const running = activeTask?.status === 'running';
|
|
216
233
|
const hasRunningTasks = useMemo(() => tasks.some(t => t.status === 'running'), [tasks]);
|
|
234
|
+
const selectedProject = useMemo(() => projects[selectedIndex] || null, [projects, selectedIndex]);
|
|
217
235
|
|
|
218
236
|
const addLogToTask = useCallback((taskId, line) => {
|
|
219
|
-
setTasks(prev =>
|
|
220
|
-
|
|
237
|
+
setTasks(prev => {
|
|
238
|
+
const idx = prev.findIndex(t => t.id === taskId);
|
|
239
|
+
if (idx === -1) return prev;
|
|
240
|
+
const t = prev[idx];
|
|
221
241
|
const normalized = typeof line === 'string' ? line : JSON.stringify(line);
|
|
222
|
-
const
|
|
223
|
-
const nextLogs = [...t.logs, ...
|
|
224
|
-
|
|
225
|
-
|
|
242
|
+
const newLines = normalized.split(/\r?\n/).filter(l => l.trim().length > 0);
|
|
243
|
+
const nextLogs = [...t.logs, ...newLines];
|
|
244
|
+
const updatedTask = { ...t, logs: nextLogs.length > 500 ? nextLogs.slice(-500) : nextLogs };
|
|
245
|
+
const nextTasks = [...prev];
|
|
246
|
+
nextTasks[idx] = updatedTask;
|
|
247
|
+
return nextTasks;
|
|
248
|
+
});
|
|
226
249
|
}, []);
|
|
227
250
|
|
|
228
251
|
const detailedIndexed = useMemo(() => buildDetailCommands(selectedProject, config).map((command, index) => ({
|
|
@@ -238,7 +261,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
238
261
|
|
|
239
262
|
const killAllTasks = useCallback(() => {
|
|
240
263
|
runningProcessMap.current.forEach((proc) => {
|
|
241
|
-
try { proc.kill('
|
|
264
|
+
try { proc.kill('SIGKILL'); } catch { /* ignore */ }
|
|
242
265
|
});
|
|
243
266
|
runningProcessMap.current.clear();
|
|
244
267
|
}, []);
|
|
@@ -266,7 +289,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
266
289
|
const subprocess = execa(commandMeta.command[0], commandMeta.command.slice(1), {
|
|
267
290
|
cwd: project.path,
|
|
268
291
|
env: process.env,
|
|
269
|
-
stdin: 'pipe'
|
|
292
|
+
stdin: 'pipe',
|
|
293
|
+
cleanup: true
|
|
270
294
|
});
|
|
271
295
|
runningProcessMap.current.set(taskId, subprocess);
|
|
272
296
|
|
|
@@ -277,9 +301,9 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
277
301
|
setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'finished'} : t));
|
|
278
302
|
addLogToTask(taskId, kleur.green(`β ${commandLabel} finished`));
|
|
279
303
|
} catch (error) {
|
|
280
|
-
if (error.isCanceled || error.killed) {
|
|
304
|
+
if (error.isCanceled || error.killed || error.signal === 'SIGKILL' || error.signal === 'SIGINT') {
|
|
281
305
|
setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'killed'} : t));
|
|
282
|
-
addLogToTask(taskId, kleur.yellow(`! Task killed
|
|
306
|
+
addLogToTask(taskId, kleur.yellow(`! Task killed forcefully`));
|
|
283
307
|
} else {
|
|
284
308
|
setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'failed'} : t));
|
|
285
309
|
addLogToTask(taskId, kleur.red(`β ${commandLabel} failed: ${error.shortMessage || error.message}`));
|
|
@@ -292,7 +316,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
292
316
|
const handleKillTask = useCallback((taskId) => {
|
|
293
317
|
const proc = runningProcessMap.current.get(taskId);
|
|
294
318
|
if (proc) {
|
|
295
|
-
proc.kill('
|
|
319
|
+
proc.kill('SIGKILL');
|
|
296
320
|
} else {
|
|
297
321
|
setTasks(prev => prev.filter(t => t.id !== taskId));
|
|
298
322
|
if (activeTaskId === taskId) setActiveTaskId(null);
|
|
@@ -300,15 +324,16 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
300
324
|
}, [activeTaskId]);
|
|
301
325
|
|
|
302
326
|
const exportLogs = useCallback(() => {
|
|
303
|
-
|
|
327
|
+
const taskToExport = tasks.find(t => t.id === activeTaskId);
|
|
328
|
+
if (!taskToExport || !taskToExport.logs.length) return;
|
|
304
329
|
try {
|
|
305
|
-
const exportPath = path.resolve(process.cwd(), `compass-${
|
|
306
|
-
fs.writeFileSync(exportPath,
|
|
330
|
+
const exportPath = path.resolve(process.cwd(), `compass-${taskToExport.id}.txt`);
|
|
331
|
+
fs.writeFileSync(exportPath, taskToExport.logs.join('\n'));
|
|
307
332
|
addLogToTask(activeTaskId, kleur.green(`β Logs exported to ${exportPath}`));
|
|
308
|
-
} catch
|
|
309
|
-
addLogToTask(activeTaskId, kleur.red(
|
|
333
|
+
} catch {
|
|
334
|
+
addLogToTask(activeTaskId, kleur.red('β Export failed'));
|
|
310
335
|
}
|
|
311
|
-
}, [
|
|
336
|
+
}, [tasks, activeTaskId, addLogToTask]);
|
|
312
337
|
|
|
313
338
|
useInput((input, key) => {
|
|
314
339
|
if (quitConfirm) {
|
|
@@ -320,13 +345,14 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
320
345
|
if (customMode) {
|
|
321
346
|
if (key.return) {
|
|
322
347
|
const raw = customInput.trim();
|
|
323
|
-
|
|
348
|
+
const selProj = selectedProject;
|
|
349
|
+
if (selProj && raw) {
|
|
324
350
|
const [labelPart, commandPart] = raw.split('|');
|
|
325
351
|
const commandTokens = (commandPart || labelPart).trim().split(/\s+/).filter(Boolean);
|
|
326
352
|
if (commandTokens.length) {
|
|
327
|
-
const label = commandPart ? labelPart.trim() : `Custom ${
|
|
353
|
+
const label = commandPart ? labelPart.trim() : `Custom ${selProj.name}`;
|
|
328
354
|
setConfig((prev) => {
|
|
329
|
-
const projectKey =
|
|
355
|
+
const projectKey = selProj.path;
|
|
330
356
|
const existing = prev.customCommands?.[projectKey] || [];
|
|
331
357
|
const nextConfig = { ...prev, customCommands: { ...prev.customCommands, [projectKey]: [...existing, {label, command: commandTokens}] } };
|
|
332
358
|
saveConfig(nextConfig);
|
|
@@ -430,11 +456,9 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
430
456
|
if (tasks.length > 0) {
|
|
431
457
|
if (key.upArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) - 1 + tasks.length) % tasks.length]?.id); return; }
|
|
432
458
|
if (key.downArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) + 1) % tasks.length]?.id); return; }
|
|
433
|
-
if (shiftCombo('k') && activeTaskId) {
|
|
434
|
-
handleKillTask(activeTaskId);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
459
|
+
if (shiftCombo('k') && activeTaskId) { handleKillTask(activeTaskId); return; }
|
|
437
460
|
if (shiftCombo('r') && activeTaskId) { setRenameMode(true); setRenameInput(activeTask.name); setRenameCursor(activeTask.name.length); return; }
|
|
461
|
+
if (key.ctrl && input === 'c') { handleKillTask(activeTaskId); return; }
|
|
438
462
|
}
|
|
439
463
|
if (key.return) { setMainView('navigator'); return; }
|
|
440
464
|
return;
|
|
@@ -442,7 +466,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
442
466
|
|
|
443
467
|
if (running && activeTaskId && runningProcessMap.current.has(activeTaskId)) {
|
|
444
468
|
const proc = runningProcessMap.current.get(activeTaskId);
|
|
445
|
-
if (key.ctrl && input === 'c') { proc.kill('
|
|
469
|
+
if (key.ctrl && input === 'c') { proc.kill('SIGKILL'); setStdinBuffer(''); setStdinCursor(0); return; }
|
|
446
470
|
if (key.return) { proc.stdin?.write(stdinBuffer + '\n'); setStdinBuffer(''); setStdinCursor(0); return; }
|
|
447
471
|
if (key.backspace || key.delete) {
|
|
448
472
|
if (stdinCursor > 0) {
|
|
@@ -490,36 +514,12 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
490
514
|
}
|
|
491
515
|
});
|
|
492
516
|
|
|
493
|
-
const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}
|
|
517
|
+
const projectCountLabel = useMemo(() => `${projects.length} project${projects.length === 1 ? '' : 's'}`, [projects.length]);
|
|
494
518
|
const toggleHint = config.showHelpCards ? 'Shift+H hide help' : 'Shift+H show help';
|
|
495
519
|
const statusHint = activeTask ? `[${activeTask.status.toUpperCase()}] ${activeTask.name}` : 'Idle Navigator';
|
|
496
520
|
const orbitHint = mainView === 'tasks' ? 'Tasks View' : `Orbit: ${tasks.length} tasks`;
|
|
497
521
|
const artHint = config.showArtBoard ? 'Shift+B hide art' : 'Shift+B show art';
|
|
498
522
|
|
|
499
|
-
if (quitConfirm) {
|
|
500
|
-
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'));
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (mainView === 'studio') return create(Studio);
|
|
504
|
-
|
|
505
|
-
if (mainView === 'tasks') {
|
|
506
|
-
return create(
|
|
507
|
-
Box,
|
|
508
|
-
{flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1},
|
|
509
|
-
create(Text, {bold: true, color: 'yellow'}, 'π°οΈ Task Manager | Background Processes'),
|
|
510
|
-
create(Text, {dimColor: true, marginBottom: 1}, 'Up/Down: focus, Shift+K: Kill/Delete, Shift+R: Rename'),
|
|
511
|
-
...tasks.map(t => create(
|
|
512
|
-
Box,
|
|
513
|
-
{key: t.id, marginBottom: 0, flexDirection: 'column'},
|
|
514
|
-
t.id === activeTaskId && renameMode
|
|
515
|
-
? create(Box, {flexDirection: 'row'}, create(Text, {color: 'cyan'}, 'β Rename to: '), create(CursorText, {value: renameInput, cursorIndex: renameCursor}))
|
|
516
|
-
: create(Text, {color: t.id === activeTaskId ? 'cyan' : 'white', bold: t.id === activeTaskId}, `${t.id === activeTaskId ? 'β' : ' '} [${t.status.toUpperCase()}] ${t.name}`)
|
|
517
|
-
)),
|
|
518
|
-
!tasks.length && create(Text, {dimColor: true}, 'No active or background tasks.'),
|
|
519
|
-
create(Text, {marginTop: 1, dimColor: true}, 'Press Enter or Shift+T to return to Navigator.')
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
523
|
const projectRows = useMemo(() => {
|
|
524
524
|
if (loading) return [create(Text, {key: 'scanning', dimColor: true}, 'Scanning projectsβ¦')];
|
|
525
525
|
if (error) return [create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`)];
|
|
@@ -582,25 +582,26 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
582
582
|
const artTileNodes = useMemo(() => [
|
|
583
583
|
{label: 'Pulse', detail: projectCountLabel, accent: 'magenta', icon: 'β', subtext: `Workspace Β· ${path.basename(rootPath) || rootPath}`},
|
|
584
584
|
{label: 'Focus', detail: selectedProject?.name || 'Selection', accent: 'cyan', icon: 'β', subtext: `${selectedProject?.type || 'Stack'}`},
|
|
585
|
-
{label: 'Orbit', detail: `${tasks.length}
|
|
585
|
+
{label: 'Orbit', detail: `${tasks.length} tasks`, accent: 'yellow', icon: 'β ', subtext: running ? 'Busy streaming...' : 'Idle'}
|
|
586
586
|
].map(tile => create(Box, {key: tile.label, flexDirection: 'column', padding: 1, marginRight: 1, borderStyle: 'single', borderColor: tile.accent, minWidth: 24},
|
|
587
587
|
create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
|
|
588
588
|
create(Text, {bold: true}, tile.detail),
|
|
589
589
|
create(Text, {dimColor: true}, tile.subtext)
|
|
590
590
|
)), [projectCountLabel, rootPath, selectedProject, tasks.length, running]);
|
|
591
591
|
|
|
592
|
-
const artBoard = config.showArtBoard ? create(Box, {flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
|
|
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')),
|
|
594
|
-
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)))),
|
|
595
|
-
create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
|
|
596
|
-
) : null;
|
|
597
|
-
|
|
598
592
|
const helpCards = [
|
|
599
593
|
{label: 'Navigation', color: 'magenta', body: ['β / β move focus, Enter: details', 'Shift+β / β scroll output', 'Shift+H toggle help cards', 'Shift+D detach from task']},
|
|
600
594
|
{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']},
|
|
601
|
-
{label: 'Orbit & Studio', color: 'yellow', body: ['Shift+T task manager', 'Shift+A studio / Shift+B art board', 'Shift+
|
|
595
|
+
{label: 'Orbit & Studio', color: 'yellow', body: ['Shift+T task manager', 'Shift+A studio / Shift+B art board', 'Shift+S structure / Shift+Q quit']}
|
|
602
596
|
];
|
|
603
597
|
|
|
598
|
+
if (quitConfirm) {
|
|
599
|
+
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'));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (mainView === 'studio') return create(Studio);
|
|
603
|
+
if (mainView === 'tasks') return create(TaskManager, {tasks, activeTaskId, renameMode, renameInput, renameCursor});
|
|
604
|
+
|
|
604
605
|
return create(Box, {flexDirection: 'column', padding: 1},
|
|
605
606
|
create(Box, {justifyContent: 'space-between'},
|
|
606
607
|
create(Box, {flexDirection: 'column'}, create(Text, {color: 'magenta', bold: true}, 'Project Compass'), create(Text, {dimColor: true}, `${projectCountLabel} detected in ${rootPath}`)),
|
|
@@ -609,7 +610,11 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
609
610
|
create(Text, {dimColor: true}, `${toggleHint} Β· ${orbitHint} Β· ${artHint} Β· Shift+Q: Quit`)
|
|
610
611
|
)
|
|
611
612
|
),
|
|
612
|
-
|
|
613
|
+
config.showArtBoard && create(Box, {flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
|
|
614
|
+
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')),
|
|
615
|
+
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)))),
|
|
616
|
+
create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
|
|
617
|
+
),
|
|
613
618
|
create(Box, {marginTop: 1, flexDirection: 'row', alignItems: 'stretch', width: '100%', flexWrap: 'wrap'},
|
|
614
619
|
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)),
|
|
615
620
|
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)
|