project-compass 2.8.1 → 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 +121 -69
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "2.8.1",
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,7 +358,16 @@ 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; }
335
- if (shiftCombo('t')) { setMainView('tasks'); return; }
361
+ if (shiftCombo('b')) { setShowArtBoard(prev => !prev); return; }
362
+
363
+ if (shiftCombo('t')) {
364
+ setMainView((prev) => {
365
+ if (prev === 'tasks') return 'navigator';
366
+ if (tasks.length > 0 && !activeTaskId) setActiveTaskId(tasks[0].id);
367
+ return 'tasks';
368
+ });
369
+ return;
370
+ }
336
371
 
337
372
  const scrollLogs = (delta) => {
338
373
  setLogOffset((prev) => {
@@ -342,6 +377,20 @@ function Compass({rootPath, initialView = 'navigator'}) {
342
377
  });
343
378
  };
344
379
 
380
+ if (mainView === 'tasks') {
381
+ if (tasks.length > 0) {
382
+ if (key.upArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) - 1 + tasks.length) % tasks.length]?.id); return; }
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; }
389
+ }
390
+ if (key.return) { setMainView('navigator'); return; }
391
+ return;
392
+ }
393
+
345
394
  if (running && activeTaskId && runningProcessMap.current.has(activeTaskId)) {
346
395
  const proc = runningProcessMap.current.get(activeTaskId);
347
396
  if (key.ctrl && input === 'c') { proc.kill('SIGINT'); setStdinBuffer(''); setStdinCursor(0); return; }
@@ -368,13 +417,6 @@ function Compass({rootPath, initialView = 'navigator'}) {
368
417
  if (normalizedInput === '?') { setShowHelp((prev) => !prev); return; }
369
418
  if (shiftCombo('l') && lastCommandRef.current) { runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project); return; }
370
419
 
371
- if (mainView === 'tasks') {
372
- if (key.upArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) - 1 + tasks.length) % tasks.length]?.id); return; }
373
- if (key.downArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) + 1) % tasks.length]?.id); return; }
374
- if (key.return || shiftCombo('t')) { setMainView('navigator'); return; }
375
- return;
376
- }
377
-
378
420
  if (key.upArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length); return; }
379
421
  if (key.downArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev + 1) % projects.length); return; }
380
422
  if (key.return) {
@@ -382,7 +424,10 @@ function Compass({rootPath, initialView = 'navigator'}) {
382
424
  setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
383
425
  return;
384
426
  }
385
- if (shiftCombo('q')) { exit(); return; }
427
+ if (shiftCombo('q')) {
428
+ if (hasRunningTasks) setQuitConfirm(true); else exit();
429
+ return;
430
+ }
386
431
  if (shiftCombo('c') && viewMode === 'detail' && selectedProject) { setCustomMode(true); setCustomInput(''); setCustomCursor(0); return; }
387
432
 
388
433
  const actionKey = normalizedInput && ACTION_MAP[normalizedInput];
@@ -398,6 +443,10 @@ function Compass({rootPath, initialView = 'navigator'}) {
398
443
 
399
444
  const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
400
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
+
401
450
  if (mainView === 'studio') return create(Studio);
402
451
 
403
452
  if (mainView === 'tasks') {
@@ -405,14 +454,16 @@ function Compass({rootPath, initialView = 'navigator'}) {
405
454
  Box,
406
455
  {flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1},
407
456
  create(Text, {bold: true, color: 'yellow'}, '🛰️ Task Manager | Background Processes'),
408
- 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'),
409
458
  ...tasks.map(t => create(
410
459
  Box,
411
- {key: t.id, marginBottom: 0},
412
- 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}`)
413
464
  )),
414
465
  !tasks.length && create(Text, {dimColor: true}, 'No active or background tasks.'),
415
- create(Text, {marginTop: 1, dimColor: true}, 'Press Enter/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.')
416
467
  );
417
468
  }
418
469
 
@@ -476,10 +527,6 @@ function Compass({rootPath, initialView = 'navigator'}) {
476
527
  detailContent.push(create(Text, {key: 'e-h', dimColor: true}, 'Press Enter on a project to reveal details.'));
477
528
  }
478
529
 
479
- if (customMode) {
480
- 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})));
481
- }
482
-
483
530
  const artTileNodes = [
484
531
  {label: 'Pulse', detail: projectCountLabel, accent: 'magenta', icon: '●', subtext: `Workspace · ${path.basename(rootPath) || rootPath}`},
485
532
  {label: 'Focus', detail: selectedProject?.name || 'Selection', accent: 'cyan', icon: '◆', subtext: `${selectedProject?.type || 'Stack'}`},
@@ -490,11 +537,11 @@ function Compass({rootPath, initialView = 'navigator'}) {
490
537
  create(Text, {dimColor: true}, tile.subtext)
491
538
  ));
492
539
 
493
- 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},
494
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')),
495
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)))),
496
543
  create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
497
- );
544
+ ) : null;
498
545
 
499
546
  const logs = activeTask?.logs || [];
500
547
  const logWindowStart = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE - logOffset);
@@ -505,7 +552,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
505
552
  const helpCards = [
506
553
  {label: 'Navigation', color: 'magenta', body: ['↑ / ↓ move focus, Enter: details', 'Shift+↑ / ↓ scroll output', 'Shift+H toggle help cards', 'Shift+D detach from task']},
507
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']},
508
- {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']}
509
556
  ];
510
557
 
511
558
  const toggleHint = showHelpCards ? 'Shift+H hide help' : 'Shift+H show help';
@@ -527,7 +574,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
527
574
  ),
528
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))))),
529
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(', ')}`))),
530
- 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.'))
531
578
  );
532
579
  }
533
580
 
@@ -564,10 +611,15 @@ async function main() {
564
611
  console.log(' Shift+A Switch to Omni-Studio (Environment Health)');
565
612
  console.log(' Shift+T Open Orbit Task Manager (Manage background processes)');
566
613
  console.log(' Shift+D Detach from active task (Keep it running in background)');
614
+ console.log(' Shift+B Toggle Art Board visibility');
567
615
  console.log(' Shift+X Clear active task output log');
568
616
  console.log(' Shift+E Export current logs to a .txt file');
569
617
  console.log(' Shift+↑ / ↓ Scroll the output logs');
570
- 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');
571
623
  console.log('');
572
624
  console.log(kleur.bold('Execution shortcuts:'));
573
625
  console.log(' B / T / R Quick run: Build / Test / Run');