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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +65 -60
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "3.2.0",
3
+ "version": "3.3.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
@@ -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 => prev.map(t => {
220
- if (t.id !== taskId) return t;
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 lines = normalized.split(/\r?\n/).filter(l => l.trim().length > 0);
223
- const nextLogs = [...t.logs, ...lines];
224
- return { ...t, logs: nextLogs.length > 500 ? nextLogs.slice(-500) : nextLogs };
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('SIGINT'); } catch { /* ignore */ }
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 by user`));
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('SIGINT');
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
- if (!activeTask || !activeTask.logs.length) return;
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-${activeTask.id}.txt`);
306
- fs.writeFileSync(exportPath, activeTask.logs.join('\n'));
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 (err) {
309
- addLogToTask(activeTaskId, kleur.red(`βœ— Export failed: ${err.message}`));
333
+ } catch {
334
+ addLogToTask(activeTaskId, kleur.red('βœ— Export failed'));
310
335
  }
311
- }, [activeTask, activeTaskId, addLogToTask]);
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
- if (selectedProject && raw) {
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 ${selectedProject.name}`;
353
+ const label = commandPart ? labelPart.trim() : `Custom ${selProj.name}`;
328
354
  setConfig((prev) => {
329
- const projectKey = selectedProject.path;
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('SIGINT'); setStdinBuffer(''); setStdinCursor(0); return; }
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} active tasks`, accent: 'yellow', icon: 'β– ', subtext: running ? 'Busy streaming...' : 'Idle'}
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+C custom / Shift+Q quit']}
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
- artBoard,
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)