project-compass 2.8.2 → 2.9.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 +104 -62
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "2.8.2",
3
+ "version": "2.9.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
@@ -78,9 +78,7 @@ function useScanner(rootPath) {
78
78
  }
79
79
 
80
80
  function buildDetailCommands(project, config) {
81
- if (!project) {
82
- return [];
83
- }
81
+ if (!project) return [];
84
82
  const builtins = Object.entries(project.commands || {}).map(([key, command]) => ({
85
83
  label: command.label || key,
86
84
  command: command.command,
@@ -177,6 +175,11 @@ function Compass({rootPath, initialView = 'navigator'}) {
177
175
  const [customMode, setCustomMode] = useState(false);
178
176
  const [customInput, setCustomInput] = useState('');
179
177
  const [customCursor, setCustomCursor] = useState(0);
178
+ const [renameMode, setRenameMode] = useState(false);
179
+ const [renameInput, setRenameInput] = useState('');
180
+ const [renameCursor, setRenameCursor] = useState(0);
181
+ const [quitConfirm, setQuitConfirm] = useState(false);
182
+ const [showArtBoard, setShowArtBoard] = useState(true);
180
183
  const [config, setConfig] = useState(() => loadConfig());
181
184
  const [showHelpCards, setShowHelpCards] = useState(false);
182
185
  const [showStructureGuide, setShowStructureGuide] = useState(false);
@@ -189,6 +192,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
189
192
 
190
193
  const activeTask = useMemo(() => tasks.find(t => t.id === activeTaskId), [tasks, activeTaskId]);
191
194
  const running = activeTask?.status === 'running';
195
+ const hasRunningTasks = useMemo(() => tasks.some(t => t.status === 'running'), [tasks]);
192
196
 
193
197
  const addLogToTask = useCallback((taskId, line) => {
194
198
  setTasks(prev => prev.map(t => {
@@ -200,11 +204,11 @@ function Compass({rootPath, initialView = 'navigator'}) {
200
204
  }));
201
205
  }, []);
202
206
 
203
- const detailCommands = useMemo(() => buildDetailCommands(selectedProject, config), [selectedProject, config]);
204
- const detailedIndexed = useMemo(() => detailCommands.map((command, index) => ({
207
+ const detailedIndexed = useMemo(() => buildDetailCommands(selectedProject, config).map((command, index) => ({
205
208
  ...command,
206
209
  shortcut: `${index + 1}`
207
- })), [detailCommands]);
210
+ })), [selectedProject, config]);
211
+
208
212
  const detailShortcutMap = useMemo(() => {
209
213
  const map = new Map();
210
214
  detailedIndexed.forEach((cmd) => map.set(cmd.shortcut, cmd));
@@ -245,52 +249,27 @@ function Compass({rootPath, initialView = 'navigator'}) {
245
249
  setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'finished'} : t));
246
250
  addLogToTask(taskId, kleur.green(`✓ ${commandLabel} finished`));
247
251
  } catch (error) {
248
- setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'failed'} : t));
249
- addLogToTask(taskId, kleur.red(`✗ ${commandLabel} failed: ${error.shortMessage || error.message}`));
252
+ if (error.isCanceled) {
253
+ setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'killed'} : t));
254
+ addLogToTask(taskId, kleur.yellow(`! Task killed by user`));
255
+ } else {
256
+ setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'failed'} : t));
257
+ addLogToTask(taskId, kleur.red(`✗ ${commandLabel} failed: ${error.shortMessage || error.message}`));
258
+ }
250
259
  } finally {
251
260
  runningProcessMap.current.delete(taskId);
252
261
  }
253
262
  }, [addLogToTask, selectedProject]);
254
263
 
255
- const handleAddCustomCommand = useCallback((label, commandTokens) => {
256
- if (!selectedProject) return;
257
- setConfig((prev) => {
258
- const projectKey = selectedProject.path;
259
- const existing = prev.customCommands?.[projectKey] || [];
260
- const nextConfig = {
261
- ...prev,
262
- customCommands: {
263
- ...prev.customCommands,
264
- [projectKey]: [...existing, {label, command: commandTokens}]
265
- }
266
- };
267
- saveConfig(nextConfig);
268
- return nextConfig;
269
- });
270
- }, [selectedProject]);
271
-
272
- const handleCustomSubmit = useCallback(() => {
273
- const raw = customInput.trim();
274
- if (!selectedProject || !raw) {
275
- setCustomMode(false);
276
- setCustomInput('');
277
- setCustomCursor(0);
278
- return;
279
- }
280
- const [labelPart, commandPart] = raw.split('|');
281
- const commandTokens = (commandPart || labelPart).trim().split(/\s+/).filter(Boolean);
282
- if (!commandTokens.length) {
283
- setCustomMode(false);
284
- setCustomInput('');
285
- setCustomCursor(0);
286
- return;
264
+ const handleKillTask = useCallback((taskId) => {
265
+ const proc = runningProcessMap.current.get(taskId);
266
+ if (proc) {
267
+ proc.kill('SIGINT');
268
+ } else {
269
+ setTasks(prev => prev.filter(t => t.id !== taskId));
270
+ if (activeTaskId === taskId) setActiveTaskId(null);
287
271
  }
288
- const label = commandPart ? labelPart.trim() : `Custom ${selectedProject.name}`;
289
- handleAddCustomCommand(label, commandTokens);
290
- setCustomMode(false);
291
- setCustomInput('');
292
- setCustomCursor(0);
293
- }, [customInput, selectedProject, handleAddCustomCommand]);
272
+ }, [activeTaskId]);
294
273
 
295
274
  const exportLogs = useCallback(() => {
296
275
  if (!activeTask || !activeTask.logs.length) return;
@@ -304,8 +283,32 @@ function Compass({rootPath, initialView = 'navigator'}) {
304
283
  }, [activeTask, activeTaskId, addLogToTask]);
305
284
 
306
285
  useInput((input, key) => {
286
+ if (quitConfirm) {
287
+ if (input?.toLowerCase() === 'y') exit();
288
+ if (input?.toLowerCase() === 'n' || key.escape) setQuitConfirm(false);
289
+ return;
290
+ }
291
+
307
292
  if (customMode) {
308
- if (key.return) { handleCustomSubmit(); return; }
293
+ if (key.return) {
294
+ const raw = customInput.trim();
295
+ if (selectedProject && raw) {
296
+ const [labelPart, commandPart] = raw.split('|');
297
+ const commandTokens = (commandPart || labelPart).trim().split(/\s+/).filter(Boolean);
298
+ if (commandTokens.length) {
299
+ const label = commandPart ? labelPart.trim() : `Custom ${selectedProject.name}`;
300
+ setConfig((prev) => {
301
+ const projectKey = selectedProject.path;
302
+ const existing = prev.customCommands?.[projectKey] || [];
303
+ const nextConfig = { ...prev, customCommands: { ...prev.customCommands, [projectKey]: [...existing, {label, command: commandTokens}] } };
304
+ saveConfig(nextConfig);
305
+ return nextConfig;
306
+ });
307
+ }
308
+ }
309
+ setCustomMode(false); setCustomInput(''); setCustomCursor(0);
310
+ return;
311
+ }
309
312
  if (key.escape) { setCustomMode(false); setCustomInput(''); setCustomCursor(0); return; }
310
313
  if (key.backspace || key.delete) {
311
314
  if (customCursor > 0) {
@@ -323,6 +326,29 @@ function Compass({rootPath, initialView = 'navigator'}) {
323
326
  return;
324
327
  }
325
328
 
329
+ if (renameMode) {
330
+ if (key.return) {
331
+ setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, name: renameInput} : t));
332
+ setRenameMode(false); setRenameInput(''); setRenameCursor(0);
333
+ return;
334
+ }
335
+ if (key.escape) { setRenameMode(false); setRenameInput(''); setRenameCursor(0); return; }
336
+ if (key.backspace || key.delete) {
337
+ if (renameCursor > 0) {
338
+ setRenameInput((prev) => prev.slice(0, renameCursor - 1) + prev.slice(renameCursor));
339
+ setRenameCursor(c => Math.max(0, c - 1));
340
+ }
341
+ return;
342
+ }
343
+ if (key.leftArrow) { setRenameCursor(c => Math.max(0, c - 1)); return; }
344
+ if (key.rightArrow) { setRenameCursor(c => Math.min(renameInput.length, c + 1)); return; }
345
+ if (input) {
346
+ setRenameInput((prev) => prev.slice(0, renameCursor) + input + prev.slice(renameCursor));
347
+ setRenameCursor(c => c + input.length);
348
+ }
349
+ return;
350
+ }
351
+
326
352
  const normalizedInput = input?.toLowerCase();
327
353
  const shiftCombo = (char) => key.shift && normalizedInput === char;
328
354
 
@@ -332,6 +358,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
332
358
  if (shiftCombo('x')) { setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, logs: []} : t)); setLogOffset(0); return; }
333
359
  if (shiftCombo('e')) { exportLogs(); return; }
334
360
  if (shiftCombo('d')) { setActiveTaskId(null); return; }
361
+ if (shiftCombo('b')) { setShowArtBoard(prev => !prev); return; }
335
362
 
336
363
  if (shiftCombo('t')) {
337
364
  setMainView((prev) => {
@@ -354,6 +381,11 @@ function Compass({rootPath, initialView = 'navigator'}) {
354
381
  if (tasks.length > 0) {
355
382
  if (key.upArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) - 1 + tasks.length) % tasks.length]?.id); return; }
356
383
  if (key.downArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) + 1) % tasks.length]?.id); return; }
384
+ if (shiftCombo('k') && activeTaskId) {
385
+ handleKillTask(activeTaskId);
386
+ return;
387
+ }
388
+ if (shiftCombo('r') && activeTaskId) { setRenameMode(true); setRenameInput(activeTask.name); setRenameCursor(activeTask.name.length); return; }
357
389
  }
358
390
  if (key.return) { setMainView('navigator'); return; }
359
391
  return;
@@ -392,7 +424,10 @@ function Compass({rootPath, initialView = 'navigator'}) {
392
424
  setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
393
425
  return;
394
426
  }
395
- if (shiftCombo('q')) { exit(); return; }
427
+ if (shiftCombo('q')) {
428
+ if (hasRunningTasks) setQuitConfirm(true); else exit();
429
+ return;
430
+ }
396
431
  if (shiftCombo('c') && viewMode === 'detail' && selectedProject) { setCustomMode(true); setCustomInput(''); setCustomCursor(0); return; }
397
432
 
398
433
  const actionKey = normalizedInput && ACTION_MAP[normalizedInput];
@@ -408,6 +443,10 @@ function Compass({rootPath, initialView = 'navigator'}) {
408
443
 
409
444
  const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
410
445
 
446
+ if (quitConfirm) {
447
+ 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'));
448
+ }
449
+
411
450
  if (mainView === 'studio') return create(Studio);
412
451
 
413
452
  if (mainView === 'tasks') {
@@ -415,14 +454,16 @@ function Compass({rootPath, initialView = 'navigator'}) {
415
454
  Box,
416
455
  {flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1},
417
456
  create(Text, {bold: true, color: 'yellow'}, '🛰️ Task Manager | Background Processes'),
418
- create(Text, {dimColor: true, marginBottom: 1}, 'Select a task to view its output logs.'),
457
+ create(Text, {dimColor: true, marginBottom: 1}, 'Up/Down: focus, Shift+K: Kill/Delete, Shift+R: Rename'),
419
458
  ...tasks.map(t => create(
420
459
  Box,
421
- {key: t.id, marginBottom: 0},
422
- create(Text, {color: t.id === activeTaskId ? 'cyan' : 'white', bold: t.id === activeTaskId}, `${t.id === activeTaskId ? '→' : ' '} [${t.status.toUpperCase()}] ${t.name}`)
460
+ {key: t.id, marginBottom: 0, flexDirection: 'column'},
461
+ t.id === activeTaskId && renameMode
462
+ ? create(Box, {flexDirection: 'row'}, create(Text, {color: 'cyan'}, '→ Rename to: '), create(CursorText, {value: renameInput, cursorIndex: renameCursor}))
463
+ : create(Text, {color: t.id === activeTaskId ? 'cyan' : 'white', bold: t.id === activeTaskId}, `${t.id === activeTaskId ? '→' : ' '} [${t.status.toUpperCase()}] ${t.name}`)
423
464
  )),
424
465
  !tasks.length && create(Text, {dimColor: true}, 'No active or background tasks.'),
425
- create(Text, {marginTop: 1, dimColor: true}, 'Press Enter or Shift+T to return to Navigator, Up/Down to switch focus.')
466
+ create(Text, {marginTop: 1, dimColor: true}, 'Press Enter or Shift+T to return to Navigator.')
426
467
  );
427
468
  }
428
469
 
@@ -486,10 +527,6 @@ function Compass({rootPath, initialView = 'navigator'}) {
486
527
  detailContent.push(create(Text, {key: 'e-h', dimColor: true}, 'Press Enter on a project to reveal details.'));
487
528
  }
488
529
 
489
- if (customMode) {
490
- detailContent.push(create(Box, {key: 'ci-box', flexDirection: 'row'}, create(Text, {color: 'cyan'}, 'Type label|cmd (Enter: save, Esc: cancel): '), create(CursorText, {value: customInput, cursorIndex: customCursor})));
491
- }
492
-
493
530
  const artTileNodes = [
494
531
  {label: 'Pulse', detail: projectCountLabel, accent: 'magenta', icon: '●', subtext: `Workspace · ${path.basename(rootPath) || rootPath}`},
495
532
  {label: 'Focus', detail: selectedProject?.name || 'Selection', accent: 'cyan', icon: '◆', subtext: `${selectedProject?.type || 'Stack'}`},
@@ -500,11 +537,11 @@ function Compass({rootPath, initialView = 'navigator'}) {
500
537
  create(Text, {dimColor: true}, tile.subtext)
501
538
  ));
502
539
 
503
- const artBoard = create(Box, {flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
540
+ const artBoard = showArtBoard ? create(Box, {flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
504
541
  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')),
505
542
  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)))),
506
543
  create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
507
- );
544
+ ) : null;
508
545
 
509
546
  const logs = activeTask?.logs || [];
510
547
  const logWindowStart = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE - logOffset);
@@ -515,7 +552,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
515
552
  const helpCards = [
516
553
  {label: 'Navigation', color: 'magenta', body: ['↑ / ↓ move focus, Enter: details', 'Shift+↑ / ↓ scroll output', 'Shift+H toggle help cards', 'Shift+D detach from task']},
517
554
  {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']},
518
- {label: 'Orbit & Studio', color: 'yellow', body: ['Shift+T task manager', 'Shift+A open Omni-Studio', 'Shift+C save custom action', 'Shift+Q quit application']}
555
+ {label: 'Orbit & Studio', color: 'yellow', body: ['Shift+T task manager', 'Shift+A studio / Shift+B art', 'Shift+C custom / Shift+Q quit']}
519
556
  ];
520
557
 
521
558
  const toggleHint = showHelpCards ? 'Shift+H hide help' : 'Shift+H show help';
@@ -533,11 +570,11 @@ function Compass({rootPath, initialView = 'navigator'}) {
533
570
  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')),
534
571
  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),
535
572
  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`)),
536
- create(Box, {marginTop: 1, flexDirection: 'row', borderStyle: 'round', borderColor: running ? 'green' : 'gray', paddingX: 1}, create(Text, {bold: true, color: 'green'}, running ? ' Stdin buffer ' : ' Input ready '), create(Box, {marginLeft: 1}, create(CursorText, {value: stdinBuffer || (running ? '' : 'Start a command to feed stdin'), cursorIndex: stdinCursor, active: running})))
573
+ 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})))
537
574
  ),
538
575
  showHelpCards && create(Box, {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between', flexWrap: 'wrap'}, ...helpCards.map((card, idx) => create(Box, {key: card.label, flexGrow: 1, flexBasis: 0, minWidth: HELP_CARD_MIN_WIDTH, marginRight: idx < 2 ? 1 : 0, marginBottom: 1, borderStyle: 'round', borderColor: card.color, padding: 1, flexDirection: 'column'}, create(Text, {color: card.color, bold: true, marginBottom: 1}, card.label), ...card.body.map((line, lidx) => create(Text, {key: lidx, dimColor: card.color === 'yellow'}, line))))),
539
576
  showStructureGuide && create(Box, {flexDirection: 'column', borderStyle: 'round', borderColor: 'blue', marginTop: 1, padding: 1}, create(Text, {color: 'cyan', bold: true}, 'Structure guide · press Shift+S to hide'), ...SCHEMA_GUIDE.map(e => create(Text, {key: e.type, dimColor: true}, `• ${e.icon} ${e.label}: ${e.files.join(', ')}`))),
540
- showHelp && create(Box, {flexDirection: 'column', borderStyle: 'double', borderColor: 'cyan', marginTop: 1, padding: 1}, create(Text, {color: 'cyan', bold: true}, 'Help overlay'), create(Text, null, 'Shift+↑/↓ scrolls logs; Shift+X clears; Shift+E exports; Shift+A Studio; Shift+T Tasks; Shift+D Detach.'))
577
+ showHelp && create(Box, {flexDirection: 'column', borderStyle: 'double', borderColor: 'cyan', marginTop: 1, padding: 1}, create(Text, {color: 'cyan', bold: true}, 'Help overlay'), create(Text, null, 'Shift+↑/↓ scrolls logs; Shift+X clears; Shift+E exports; Shift+A Studio; Shift+T Tasks; Shift+D Detach; Shift+B Toggle Art.'))
541
578
  );
542
579
  }
543
580
 
@@ -574,10 +611,15 @@ async function main() {
574
611
  console.log(' Shift+A Switch to Omni-Studio (Environment Health)');
575
612
  console.log(' Shift+T Open Orbit Task Manager (Manage background processes)');
576
613
  console.log(' Shift+D Detach from active task (Keep it running in background)');
614
+ console.log(' Shift+B Toggle Art Board visibility');
577
615
  console.log(' Shift+X Clear active task output log');
578
616
  console.log(' Shift+E Export current logs to a .txt file');
579
617
  console.log(' Shift+↑ / ↓ Scroll the output logs');
580
- console.log(' Shift+Q Quit application');
618
+ console.log(' Shift+Q Quit application (with confirmation if tasks run)');
619
+ console.log('');
620
+ console.log(kleur.bold('Task Manager (Shift+T):'));
621
+ console.log(' Shift+K Kill active/selected task');
622
+ console.log(' Shift+R Rename selected task');
581
623
  console.log('');
582
624
  console.log(kleur.bold('Execution shortcuts:'));
583
625
  console.log(' B / T / R Quick run: Build / Test / Run');