project-compass 4.3.3 → 4.3.7

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/src/cli.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* global setInterval, setTimeout, clearInterval, clearTimeout */
3
2
  import React, {useCallback, useEffect, useMemo, useRef, useState, memo} from 'react';
4
3
  import {render, Box, Text, useApp, useInput} from 'ink';
5
4
  import path from 'path';
@@ -7,7 +6,7 @@ import {fileURLToPath} from 'url';
7
6
  import fs from 'fs';
8
7
  import kleur from 'kleur';
9
8
  import {execa} from 'execa';
10
- import {discoverProjects} from './projectDetection.js';
9
+ import {discoverProjects, SCHEMA_GUIDE} from './projectDetection.js';
11
10
  import {CONFIG_PATH, ensureConfigDir} from './configPaths.js';
12
11
 
13
12
  // Modular Components
@@ -34,6 +33,12 @@ const ACTION_MAP = {
34
33
  r: 'run',
35
34
  i: 'install'
36
35
  };
36
+ const COMMAND_FALLBACKS = {
37
+ build: ['compile', 'dist', 'bundle', 'package'],
38
+ test: ['check', 'spec', 'unit', 'coverage'],
39
+ run: ['start', 'serve', 'dev'],
40
+ install: ['setup', 'bootstrap', 'fetch']
41
+ };
37
42
 
38
43
  function saveConfig(config) {
39
44
  try {
@@ -124,38 +129,22 @@ const OutputPanel = memo(({activeTask, logOffset}) => {
124
129
  const logWindowEnd = Math.max(0, logs.length - logOffset);
125
130
  const visibleLogs = logs.slice(logWindowStart, logWindowEnd);
126
131
 
127
- const statusColor = activeTask?.status === 'running' ? 'green' :
128
- activeTask?.status === 'failed' ? 'red' :
129
- activeTask?.status === 'killed' ? 'yellow' : 'cyan';
130
-
131
- const scrollIndicator = logs.length > OUTPUT_WINDOW_SIZE
132
- ? ` ↕ ${Math.min(logs.length - logOffset, logs.length)}/${logs.length}`
133
- : '';
134
-
135
132
  const logNodes = visibleLogs.length
136
- ? visibleLogs.map((line, i) => create(Text, {key: i, wrap: 'truncate'}, line))
137
- : [create(Text, {key: 'empty', dimColor: true}, 'No logs yet. Run a command to see output here.')];
133
+ ? visibleLogs.map((line, i) => create(Text, {key: i}, line))
134
+ : [create(Text, {key: 'empty', dimColor: true}, 'Select a task or run a command to see logs.')];
138
135
 
139
136
  return create(
140
137
  Box,
141
138
  {
142
139
  flexDirection: 'column',
143
- borderStyle: activeTask?.status === 'running' ? 'double' : 'round',
144
- borderColor: statusColor,
145
- paddingX: 1,
146
- paddingY: 0,
140
+ borderStyle: 'round',
141
+ borderColor: 'yellow',
142
+ padding: 1,
147
143
  minHeight: OUTPUT_WINDOW_HEIGHT,
148
144
  maxHeight: OUTPUT_WINDOW_HEIGHT,
149
145
  height: OUTPUT_WINDOW_HEIGHT,
150
146
  overflow: 'hidden'
151
147
  },
152
- // Header with log count
153
- logs.length > 0 && create(
154
- Box,
155
- { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 0 },
156
- create(Text, { dimColor: true, bold: true }, ` ${activeTask?.status?.toUpperCase() || 'IDLE'} `),
157
- create(Text, { dimColor: true }, `Lines: ${logs.length}${scrollIndicator}`)
158
- ),
159
148
  ...logNodes
160
149
  );
161
150
  });
@@ -181,20 +170,9 @@ function Compass({rootPath, initialView = 'navigator'}) {
181
170
  const [stdinBuffer, setStdinBuffer] = useState('');
182
171
  const [stdinCursor, setStdinCursor] = useState(0);
183
172
  const [showHelp, setShowHelp] = useState(false);
184
- const [startup, setStartup] = useState(true);
185
- const [startupTick, setStartupTick] = useState(0);
186
173
  const runningProcessMap = useRef(new Map());
187
174
  const lastCommandRef = useRef(null);
188
175
 
189
- useEffect(() => {
190
- if (!startup) return;
191
- const timer = setInterval(() => {
192
- setStartupTick(t => t + 1);
193
- }, 80);
194
- const hideTimer = setTimeout(() => setStartup(false), 2400);
195
- return () => { clearInterval(timer); clearTimeout(hideTimer); };
196
- }, [startup]);
197
-
198
176
  const activeTask = useMemo(() => tasks.find(t => t.id === activeTaskId), [tasks, activeTaskId]);
199
177
  const running = activeTask?.status === 'running';
200
178
  const hasRunningTasks = useMemo(() => tasks.some(t => t.status === 'running'), [tasks]);
@@ -266,13 +244,63 @@ function Compass({rootPath, initialView = 'navigator'}) {
266
244
  if (!project) return;
267
245
  if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) return;
268
246
 
247
+ let finalCommand = [...commandMeta.command];
248
+ const port = config.projectMeta?.[project.path]?.port || project.metadata?.port;
249
+ let portApplied = false;
250
+ if (port) {
251
+ const cmdStr = finalCommand.join(' ');
252
+ const portStr = String(port);
253
+ const patterns = [
254
+ { match: 'uvicorn', flag: '--port' },
255
+ { match: 'gunicorn', flag: '--bind' },
256
+ { match: 'gunicorn', flag: '-b' },
257
+ { match: 'hypercorn', flag: '-b' },
258
+ { match: 'next dev', flag: '-p' },
259
+ { match: 'vite', flag: '--port' },
260
+ { match: 'flask', flag: '--port' },
261
+ { match: 'webpack', flag: '--port' },
262
+ { match: 'serve', flag: '-l' },
263
+ { match: 'django', flag: '--port' },
264
+ { match: 'react-scripts start', flag: '--port' }
265
+ ];
266
+ for (const { match, flag } of patterns) {
267
+ if (cmdStr.includes(match.toLowerCase())) {
268
+ const flagIdx = finalCommand.indexOf(flag);
269
+ if (flagIdx !== -1 && flagIdx + 1 < finalCommand.length) {
270
+ finalCommand[flagIdx + 1] = portStr;
271
+ portApplied = true;
272
+ } else if (flagIdx === -1) {
273
+ finalCommand.push(flag, portStr);
274
+ portApplied = true;
275
+ }
276
+ break;
277
+ }
278
+ }
279
+ if (!portApplied && cmdStr.includes('runserver')) {
280
+ const hasPort = finalCommand.some(t => /^\d{4,5}$/.test(t));
281
+ if (!hasPort) {
282
+ finalCommand.push(portStr);
283
+ portApplied = true;
284
+ }
285
+ }
286
+ if (!portApplied && (cmdStr.match(/(?:node|bun|deno)\s/))) {
287
+ const hasPortEnv = finalCommand.some(t => t.startsWith('--port=')) || finalCommand.includes('PORT');
288
+ if (!hasPortEnv) {
289
+ finalCommand.unshift(`PORT=${portStr}`);
290
+ portApplied = true;
291
+ }
292
+ }
293
+ }
294
+
269
295
  const commandLabel = commandMeta.label || commandMeta.command.join(' ');
270
296
  const taskId = `task-${Date.now()}`;
297
+ const logLines = [kleur.cyan(`> ${finalCommand.join(' ')}`)];
298
+ if (portApplied) logLines.push(kleur.dim(`Port ${port} applied`));
271
299
  const newTask = {
272
300
  id: taskId,
273
301
  name: `${project.name} · ${commandLabel}`,
274
302
  status: 'running',
275
- logs: [kleur.cyan(`> ${commandMeta.command.join(' ')}`)],
303
+ logs: logLines,
276
304
  project: project.name
277
305
  };
278
306
 
@@ -281,7 +309,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
281
309
  lastCommandRef.current = {project, commandMeta};
282
310
 
283
311
  try {
284
- const subprocess = execa(commandMeta.command[0], commandMeta.command.slice(1), {
312
+ const subprocess = execa(finalCommand[0], finalCommand.slice(1), {
285
313
  cwd: project.path,
286
314
  env: process.env,
287
315
  stdin: 'pipe',
@@ -307,7 +335,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
307
335
  } finally {
308
336
  runningProcessMap.current.delete(taskId);
309
337
  }
310
- }, [addLogToTask, selectedProject]);
338
+ }, [addLogToTask, selectedProject, config]);
311
339
 
312
340
  const exportLogs = useCallback(() => {
313
341
  const taskToExport = tasks.find(t => t.id === activeTaskId);
@@ -323,18 +351,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
323
351
 
324
352
  useInput((input, key) => {
325
353
 
326
- if (key.shift && (input === 'R' || input === 'r')) {
327
- if (viewMode === 'detail' && selectedProject) {
328
- setPortConfigMode(true);
329
- const currentPort = config.projectMeta?.[selectedProject.path]?.port || '3000';
330
- setCustomInput(String(currentPort));
331
- setCustomCursor(String(currentPort).length);
332
- return;
333
- }
334
- }
335
-
336
354
  if (quitConfirm) {
337
- if (input?.toLowerCase() === 'y') { killAllTasks(); exit(); return; }
355
+ if (input?.toLowerCase() === 'y') { killAllTasks(); process.stdout.write('\x1b[2J\x1b[0;0H'); exit(); return; }
338
356
  if (input?.toLowerCase() === 'n' || key.escape) { setQuitConfirm(false); return; }
339
357
  return;
340
358
  }
@@ -436,24 +454,26 @@ function Compass({rootPath, initialView = 'navigator'}) {
436
454
  const shiftCombo = (char) => key.shift && normalizedInput === char;
437
455
 
438
456
  const clearAndSwitch = (view) => {
457
+ console.clear();
439
458
  setMainView(view);
440
459
  setViewMode('list');
441
460
  setShowHelp(false);
442
461
  };
443
462
 
444
- if (shiftCombo('h')) { setConfig(prev => { const next = {...prev, showHelpCards: !prev.showHelpCards}; saveConfig(next); return next; }); return; }
445
- if (shiftCombo('s')) { setConfig(prev => { const next = {...prev, showStructureGuide: !prev.showStructureGuide}; saveConfig(next); return next; }); return; }
463
+ if (shiftCombo('h')) { console.clear(); setConfig(prev => { const next = {...prev, showHelpCards: !prev.showHelpCards}; saveConfig(next); return next; }); return; }
464
+ if (shiftCombo('s')) { console.clear(); setConfig(prev => { const next = {...prev, showStructureGuide: !prev.showStructureGuide}; saveConfig(next); return next; }); return; }
446
465
  if (shiftCombo('a')) { clearAndSwitch(mainView === 'navigator' ? 'studio' : 'navigator'); return; }
447
466
  if (shiftCombo('p')) { clearAndSwitch(mainView === 'navigator' ? 'registry' : 'navigator'); return; }
448
467
  if (shiftCombo('n')) { clearAndSwitch(mainView === 'navigator' ? 'architect' : 'navigator'); return; }
449
468
  if (shiftCombo('o')) { clearAndSwitch(mainView === 'navigator' ? 'ai' : 'navigator'); return; }
450
- if (shiftCombo('x')) { setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, logs: []} : t)); setLogOffset(0); return; }
469
+ if (shiftCombo('x')) { console.clear(); setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, logs: []} : t)); setLogOffset(0); return; }
451
470
  if (shiftCombo('e')) { exportLogs(); return; }
452
- if (shiftCombo('d')) { setActiveTaskId(null); return; }
453
- if (shiftCombo('b')) { setConfig(prev => { const next = {...prev, showArtBoard: !prev.showArtBoard}; saveConfig(next); return next; }); return; }
471
+ if (shiftCombo('d')) { console.clear(); setActiveTaskId(null); return; }
472
+ if (shiftCombo('b')) { console.clear(); setConfig(prev => { const next = {...prev, showArtBoard: !prev.showArtBoard}; saveConfig(next); return next; }); return; }
454
473
 
455
474
  if (shiftCombo('t')) {
456
475
  setMainView((prev) => {
476
+ console.clear();
457
477
  if (prev === 'tasks') return 'navigator';
458
478
  if (tasks.length > 0 && !activeTaskId) setActiveTaskId(tasks[0].id);
459
479
  return 'tasks';
@@ -528,6 +548,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
528
548
  const next = prev - pageLimit;
529
549
  return next < 0 ? 0 : next;
530
550
  });
551
+ console.clear();
531
552
  return;
532
553
  }
533
554
 
@@ -541,22 +562,24 @@ function Compass({rootPath, initialView = 'navigator'}) {
541
562
  }
542
563
  return next;
543
564
  });
565
+ console.clear();
544
566
  return;
545
567
  }
546
568
 
547
569
 
548
- if (normalizedInput === '?') { setShowHelp((prev) => !prev); return; }
570
+ if (normalizedInput === '?') { console.clear(); setShowHelp((prev) => !prev); return; }
549
571
  if (shiftCombo('l') && lastCommandRef.current) { runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project); return; }
550
572
 
551
- if (key.upArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length); return; }
552
- if (key.downArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev + 1) % projects.length); return; }
573
+ if (key.upArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => Math.max(0, prev - 1)); return; }
574
+ if (key.downArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => Math.min(projects.length - 1, prev + 1)); return; }
553
575
  if (key.return) {
554
576
  if (!selectedProject) return;
577
+ console.clear();
555
578
  setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
556
579
  return;
557
580
  }
558
581
  if (shiftCombo('q') || isCtrlC) {
559
- if (hasRunningTasks) setQuitConfirm(true); else { exit(); }
582
+ if (hasRunningTasks) setQuitConfirm(true); else { process.stdout.write('\x1b[2J\x1b[0;0H'); exit(); }
560
583
  return;
561
584
  }
562
585
 
@@ -566,16 +589,39 @@ function Compass({rootPath, initialView = 'navigator'}) {
566
589
  }
567
590
 
568
591
 
569
- if (shiftCombo('r') && viewMode === 'detail' && selectedProject) {
570
- setPortConfigMode(true); setCustomInput(selectedProject.metadata?.port || '3000'); setCustomCursor(String(selectedProject.metadata?.port || '3000').length);
592
+ if (shiftCombo('m') && viewMode === 'detail' && selectedProject) {
593
+ setPortConfigMode(true);
594
+ const port = config.projectMeta?.[selectedProject.path]?.port || selectedProject.metadata?.port || '3000';
595
+ setCustomInput(String(port));
596
+ setCustomCursor(String(port).length);
571
597
  return;
572
598
  }
573
599
  if (shiftCombo('c') && viewMode === 'detail' && selectedProject) { setCustomMode(true); setCustomInput(''); setCustomCursor(0); return; }
574
600
 
575
601
  const actionKey = normalizedInput && ACTION_MAP[normalizedInput];
576
- if (actionKey) {
577
- const commandMeta = selectedProject?.commands?.[actionKey];
578
- runProjectCommand(commandMeta, selectedProject);
602
+ if (actionKey && selectedProject) {
603
+ if (!key.shift) {
604
+ return;
605
+ }
606
+ const findCommand = (project, key) => {
607
+ let meta = project.commands?.[key];
608
+ if (meta) return meta;
609
+ const alts = COMMAND_FALLBACKS[key] || [];
610
+ for (const alt of alts) {
611
+ meta = project.commands?.[alt];
612
+ if (meta) return meta;
613
+ }
614
+ return null;
615
+ };
616
+ const commandMeta = findCommand(selectedProject, actionKey);
617
+ if (commandMeta) {
618
+ runProjectCommand(commandMeta, selectedProject);
619
+ } else {
620
+ const msg = kleur.yellow(`! No ${actionKey} command available for ${selectedProject.name}`);
621
+ const taskId = `task-${Date.now()}`;
622
+ setTasks(prev => [...prev, { id: taskId, name: `Notification`, status: 'failed', logs: [msg], project: '' }]);
623
+ globalThis.setTimeout(() => setTasks(prev => prev.filter(t => t.id !== taskId)), 3000);
624
+ }
579
625
  return;
580
626
  }
581
627
  if (viewMode === 'detail' && normalizedInput && detailShortcutMap.has(normalizedInput)) {
@@ -628,7 +674,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
628
674
  create(Text, {key: `dl-${command.shortcut}`, dimColor: true}, ` ↳ ${command.command.join(' ')}`)
629
675
  );
630
676
  });
631
- 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.'));
677
+ content.push(create(Text, {key: 'h-l', dimColor: true, marginTop: 1}, 'Shift+C: custom cmd · Shift+M: port · Enter: close detail view.'));
632
678
  return content;
633
679
  }, [viewMode, selectedProject, rootPath, detailedIndexed]);
634
680
 
@@ -653,27 +699,9 @@ function Compass({rootPath, initialView = 'navigator'}) {
653
699
  case 'registry': return create(PackageRegistry, {selectedProject, projects, onRunCommand: runProjectCommand, CursorText, onSelectProject: (idx) => setSelectedIndex(idx)});
654
700
  case 'architect': return create(ProjectArchitect, {rootPath, onRunCommand: runProjectCommand, CursorText, onReturn: () => setMainView('navigator')});
655
701
  case 'ai': return create(AIHorizon, {rootPath, selectedProject, onRunCommand: runProjectCommand, CursorText, config, setConfig, saveConfig});
656
- default: {
657
- const quickActions = selectedProject ? [
658
- { key: 'B', label: 'Build', color: 'magenta' },
659
- { key: 'T', label: 'Test', color: 'cyan' },
660
- { key: 'R', label: 'Run', color: 'green' },
661
- { key: 'I', label: 'Install', color: 'blue' },
662
- { key: '0', label: 'AI', color: 'yellow' }
663
- ] : [];
664
-
702
+ default: {
665
703
  const navigatorBody = [
666
704
  create(Header, {projectCountLabel, rootPath, running, statusHint, toggleHint, orbitHint, artHint}),
667
- // Quick Actions Bar
668
- quickActions.length > 0 && create(Box, {key: 'quick-actions', flexDirection: 'row', marginBottom: 0, paddingX: 1, paddingY: 0, borderStyle: 'single', borderColor: 'cyan'},
669
- create(Text, {dimColor: true}, 'Quick: '),
670
- ...quickActions.map((action, idx) =>
671
- create(Text, {key: action.key, color: action.color},
672
- `${idx > 0 ? ' · ' : ''}[${action.key}] ${action.label}`
673
- )
674
- ),
675
- create(Text, {dimColor: true}, ' · [?] Help')
676
- ),
677
705
  config.showArtBoard && create(Box, {key: 'artboard', flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
678
706
  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')),
679
707
  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)))),
@@ -696,73 +724,47 @@ function Compass({rootPath, initialView = 'navigator'}) {
696
724
  {label: 'Management', color: 'cyan', body: ['Shift+P Package Registry', 'Shift+N Project Architect', 'Shift+X clear / Shift+E export']},
697
725
  {label: 'Orbit & AI', color: 'yellow', body: ['Shift+T: Tasks, Shift+O: AI, 0: Analyze', 'Shift+A studio / Shift+O AI Horizon', 'Shift+S structure / Shift+Q quit']}
698
726
  ].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))))),
699
- config.showStructureGuide && create(Box, {key: 'structure', flexDirection: 'column', borderStyle: 'round', borderColor: 'blue', marginTop: 1, padding: 1},
700
- create(Box, {flexDirection: 'row', justifyContent: 'space-between', marginBottom: 0},
701
- create(Text, {color: 'cyan', bold: true}, '📁 Structure Guide · Press Shift+S to hide'),
702
- create(Text, {dimColor: true}, `${projects.length} project${projects.length !== 1 ? 's' : ''} detected`)
703
- ),
704
- projects.length === 0 && create(Text, {dimColor: true, marginTop: 0}, 'No projects detected yet...'),
705
- projects.length > 0 && projects.map((p, idx) =>
706
- create(Box, {key: p.id, flexDirection: 'column', marginBottom: 0,
707
- borderStyle: 'single', borderColor: p.type === 'Node.js' ? 'cyan' :
708
- p.type === 'Python' ? 'green' :
709
- p.type === 'Rust' ? 'red' :
710
- p.type === 'Go' ? 'blue' :
711
- p.type === 'Java' ? 'yellow' :
712
- p.type === 'PHP' ? 'purple' :
713
- p.type === 'Ruby' ? 'red' : 'gray',
714
- paddingX: 1, paddingY: 0},
715
- create(Box, {flexDirection: 'row', alignItems: 'center'},
716
- create(Text, {bold: true, color: selectedIndex === idx ? 'cyan' : 'white'},
717
- `${selectedIndex === idx ? '→ ' : ' '}${p.icon} ${p.name}`),
718
- p.missingBinaries && p.missingBinaries.length > 0 && create(Text, {color: 'red'}, ' ⚠')
719
- ),
720
- create(Text, {dimColor: true}, ` ${p.type} · ${path.relative(rootPath, p.path) || '.'}`),
721
- p.frameworks && p.frameworks.length > 0 && create(Text, {dimColor: true}, ` Frameworks: ${p.frameworks.map(f => `${f.icon} ${f.name}`).join(', ')}`),
722
- p.description && create(Text, {dimColor: true}, ` ${p.description}`)
723
- )
724
- )
725
- ),
726
- showHelp && create(Box, {key: 'overlay', 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 Board; Shift+P Packages; Shift+N Creator; Shift+O AI Horizon.'))
727
+ config.showStructureGuide && create(Box, {key: 'structure', 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(', ')}`))),
728
+ showHelp && create(Box, {key: 'overlay', flexDirection: 'column', borderStyle: 'double', borderColor: 'cyan', marginTop: 1, padding: 1},
729
+ create(Text, {color: 'cyan', bold: true}, '📖 Help Overview'),
730
+ create(Text, null, 'Shift+T Tasks · Shift+P Packages · Shift+N Architect · Shift+O AI · Shift+A Studio'),
731
+ create(Text, null, 'Shift+B/T/R/I Build/Test/Run/Install Shift+C custom cmd Shift+M port'),
732
+ create(Text, null, 'Shift+K kill · Shift+R rename · Shift+D detach · Shift+X clear · Shift+E export · Shift+L rerun'),
733
+ create(Text, null, 'Shift+H help cards · Shift+S structure · Shift+B art · Shift+Q quit'),
734
+ create(Text, null, '↑/↓ navigate · Enter detail · Esc back · ? close')
735
+ )
727
736
  ];
728
737
  return create(Box, {flexDirection: 'column'}, ...navigatorBody);
729
738
  }
730
739
  }
731
740
  };
732
741
 
733
- // Startup banner with animation
734
- if (startup) {
735
- const frame = startupTick % 240;
736
- const progress = Math.min(frame / 240 * 100, 100);
737
- const barLength = 30;
738
- const filled = Math.floor(barLength * progress / 100);
739
- const bar = '█'.repeat(filled) + '░'.repeat(barLength - filled);
740
- const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'][startupTick % 10];
741
-
742
- return create(Box, {flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%'},
743
- create(Box, {marginBottom: 1},
744
- create(Text, {color: 'magenta', bold: true}, '🧭 '),
745
- create(Text, {color: 'cyan', bold: true}, 'PROJECT COMPASS'),
746
- create(Text, {color: 'magenta', bold: true}, ' 🧭')
747
- ),
748
- create(Box, {marginBottom: 1},
749
- create(Text, {dimColor: true}, 'Initializing command center...')
750
- ),
751
- create(Box, {marginBottom: 1},
752
- create(Text, {color: 'cyan'}, bar),
753
- create(Text, {dimColor: true}, ` ${Math.floor(progress)}%`)
754
- ),
755
- create(Box, {},
756
- create(Text, {color: 'yellow'}, ` ${spinner} `),
757
- create(Text, {dimColor: true}, 'Scanning projects...')
758
- )
759
- );
760
- }
761
-
762
742
  return create(Box, {flexDirection: 'column', padding: 1, width: '100%'}, renderView());
763
743
  }
764
744
 
765
745
 
746
+ function getAddCmd(project, pkg) {
747
+ if (!project || !pkg) return null;
748
+ const type = project.type;
749
+ if (type === 'Node.js') return [project.metadata?.packageManager || 'npm', 'install', pkg];
750
+ if (type === 'Python') return ['pip', 'install', pkg];
751
+ if (type === 'Rust') return ['cargo', 'add', pkg];
752
+ if (type === '.NET') return ['dotnet', 'add', 'package', pkg];
753
+ if (type === 'PHP') return ['composer', 'require', pkg];
754
+ return null;
755
+ }
756
+
757
+ function getRemoveCmd(project, pkg) {
758
+ if (!project || !pkg) return null;
759
+ const type = project.type;
760
+ if (type === 'Node.js') return [project.metadata?.packageManager || 'npm', 'uninstall', pkg];
761
+ if (type === 'Python') return ['pip', 'uninstall', '-y', pkg];
762
+ if (type === 'Rust') return ['cargo', 'remove', pkg];
763
+ if (type === '.NET') return ['dotnet', 'remove', 'package', pkg];
764
+ if (type === 'PHP') return ['composer', 'remove', pkg];
765
+ return null;
766
+ }
767
+
766
768
  function parseArgs() {
767
769
  const args = {};
768
770
  const tokens = process.argv.slice(2);
@@ -775,6 +777,16 @@ function parseArgs() {
775
777
  else if (token === '--studio') args.view = 'studio';
776
778
  else if (token === '--ai') args.view = 'ai';
777
779
  else if (token === '--task' || token === '--tasks') args.view = 'tasks';
780
+ else if (token === '--list-projects') args.listProjects = true;
781
+ else if (token === '--json') args.json = true;
782
+ else if (token === '--project-info' && tokens[i + 1]) { args.projectInfo = parseInt(tokens[i + 1], 10); i += 1; }
783
+ else if (token === '--run' && tokens[i + 1]) { args.runCommand = tokens[i + 1]; i += 1; }
784
+ else if (token === '--add-pkg' && tokens[i + 1]) { args.addPkg = tokens[i + 1]; i += 1; }
785
+ else if (token === '--remove-pkg' && tokens[i + 1]) { args.removePkg = tokens[i + 1]; i += 1; }
786
+ else if (token === '--scaffold' && tokens[i + 1]) { args.scaffold = tokens[i + 1]; i += 1; }
787
+ else if (token === '--name' && tokens[i + 1]) { args.name = tokens[i + 1]; i += 1; }
788
+ else if (token === '--studio-check') args.studioCheck = true;
789
+ else if (token === '--ai-analyze') args.aiAnalyze = true;
778
790
  }
779
791
  return args;
780
792
  }
@@ -792,45 +804,174 @@ async function main() {
792
804
  console.log(kleur.dim('───────────────────────────────────────────────────'));
793
805
  console.log('');
794
806
  console.log(kleur.bold('Usage:'));
795
- console.log(' project-compass [--dir <path>] [--studio] [--ai] [--task]');
796
- console.log('');
797
- console.log(kleur.bold(kleur.cyan('🌌 Core Views:')));
798
- console.log(' Shift+T ' + kleur.bold('Orbit Task Manager') + ' - Manage background processes & stream logs');
799
- console.log(' Shift+P ' + kleur.bold('Package Registry') + ' - Direct dependency management (add/remove)');
800
- console.log(' Shift+N ' + kleur.bold('Project Architect') + ' - Scaffold new projects from templates');
801
- console.log(' Shift+O ' + kleur.bold('AI Horizon') + ' - Intelligent project analysis & commands');
802
- console.log(' Shift+A ' + kleur.bold('Omni-Studio') + ' - Environment & runtime health audit');
807
+ console.log(' project-compass Launch TUI');
808
+ console.log(' project-compass [--dir <path>] [--studio|--ai|--task]');
809
+ console.log(' project-compass --list-projects [--json] List detected projects');
810
+ console.log(' project-compass --project-info <idx> [--json] Show project details');
811
+ console.log(' project-compass --run "<cmd>" [--dir <path>] Run a command');
812
+ console.log(' project-compass --add-pkg <name> [--dir] Add package');
813
+ console.log(' project-compass --remove-pkg <name> [--dir] Remove package');
814
+ console.log(' project-compass --scaffold <template> --name <n> [--dir] Scaffold project');
815
+ console.log(' project-compass --studio-check Check runtimes');
816
+ console.log(' project-compass --version / --help Version / help');
803
817
  console.log('');
804
- console.log(kleur.bold(kleur.yellow('🎮 Navigation & Details:')));
805
- console.log(' / ↓ Move focus through discovered projects');
806
- console.log(' PgUp/Dn Jump a full page of projects');
807
- console.log(' Enter Toggle deep detail view (manifests, scripts, frameworks)');
808
- console.log(' 0 Quick AI Analysis (inside Detail View)');
809
- console.log(' Shift+C Add a persistent custom command to the focused project');
810
- console.log(' 1-9 Quick-run numbered scripts in detail view');
811
- console.log(' B/T/R/I Macro run: Build / Test / Run / Install');
818
+ console.log(kleur.bold(kleur.cyan('🌌 CLI Arguments:')));
819
+ console.log(' --dir <path> Working directory for scanning');
820
+ console.log(' --studio Launch in Studio view');
821
+ console.log(' --ai Launch in AI Horizon view');
822
+ console.log(' --task Launch in Task Manager view');
823
+ console.log(' --list-projects List all detected projects');
824
+ console.log(' --json JSON output (with --list-projects, --project-info)');
825
+ console.log(' --project-info Project details by index');
826
+ console.log(' --run Execute command in project directory');
827
+ console.log(' --add-pkg Add a package dependency');
828
+ console.log(' --remove-pkg Remove a package dependency');
829
+ console.log(' --scaffold Scaffold from template (' + Object.keys({
830
+ nextjs: 1, 'nextjs-bun': 1, 'react-vite': 1, 'react-vite-npm': 1,
831
+ 'vue-vite': 1, rust: 1, django: 1, 'python-basic': 1, go: 1
832
+ }).join(', ') + ')');
833
+ console.log(' --name Project name (with --scaffold)');
834
+ console.log(' --studio-check Environment runtime audit');
812
835
  console.log('');
813
- console.log(kleur.bold(kleur.green('🛠️ Workspace Tools:')));
814
- console.log(' Shift+B Toggle Art-coded Build Atlas');
815
- console.log(' Shift+S Toggle Directory Structure Guide');
816
- console.log(' Shift+H Toggle Navigation Help Cards');
817
- console.log(' Shift+↑/↓ Scroll the live output log window');
818
- console.log(' Shift+X Clear active log buffer');
819
- console.log(' Shift+E Export current session logs to .txt');
836
+ console.log(kleur.bold(kleur.yellow('🎮 TUI Keyboard Shortcuts:')));
837
+ console.log(' Core Views: Shift+T(asks) Shift+P(ackages) Shift+N(ew)');
838
+ console.log(' Shift+O(AI) Shift+A(Studio)');
839
+ console.log(' Detail View: Shift+B(uild) Shift+T(est) Shift+R(un) Shift+I(nstall)');
840
+ console.log(' Shift+C(custom cmd) Shift+M(port) 0(AI) 1-9(scripts)');
841
+ console.log(' Navigation: ↑/↓ PgUp/Dn Enter(detail) Esc(back) ?(help)');
842
+ console.log(' Workspace: Shift+H(help) Shift+S(structure) Shift+B(art)');
843
+ console.log(' Tasks: Shift+K(kill) Shift+R(rename) Shift+D(etach)');
844
+ console.log(' Shift+X(clear) Shift+E(xport) Shift+L(rerun)');
845
+ console.log(' System: Shift+Q(quit)');
820
846
  console.log('');
821
- console.log(kleur.bold(kleur.red('🚪 System:')));
822
- console.log(' Shift+Q Quit application (triggers confirmation if tasks are running)');
823
- console.log(' Esc Global "Back" key to return to Main Navigator');
847
+ console.log(kleur.bold(kleur.green('📦 Scaffold Templates:')));
848
+ console.log(' nextjs, nextjs-bun, react-vite, react-vite-npm,');
849
+ console.log(' vue-vite, rust, django, python-basic, go');
824
850
  console.log('');
825
851
  console.log(kleur.dim('Documentation: https://github.com/CrimsonDevil333333/project-compass'));
826
- console.log(kleur.magenta('Crafted for performance by Satyaa & Clawdy'));
852
+ console.log(kleur.dim('Crafted for performance by Satyaa & Clawdy'));
827
853
  return;
828
854
  }
829
855
  const rootPath = args.root ? path.resolve(args.root) : process.cwd();
830
- if (args.mode === 'test') {
856
+
857
+ if (args.listProjects || args.mode === 'test') {
858
+ const projects = await discoverProjects(rootPath);
859
+ if (args.json) {
860
+ console.log(JSON.stringify(projects, (key, value) => key === 'commands' ? Object.keys(value) : value, 2));
861
+ } else {
862
+ console.log(`Detected ${projects.length} project(s) under ${rootPath}`);
863
+ projects.forEach((project) => { console.log(` • [${project.type}] ${project.name} (${project.path})`); });
864
+ }
865
+ return;
866
+ }
867
+
868
+ if (args.projectInfo !== undefined) {
869
+ const projects = await discoverProjects(rootPath);
870
+ const project = projects[args.projectInfo];
871
+ if (!project) { console.error(`Project index ${args.projectInfo} not found. Total: ${projects.length}`); process.exit(1); }
872
+ if (args.json) {
873
+ console.log(JSON.stringify(project, null, 2));
874
+ } else {
875
+ console.log(`Name: ${project.name}`);
876
+ console.log(`Type: ${project.type}`);
877
+ console.log(`Path: ${project.path}`);
878
+ console.log(`Frameworks: ${(project.frameworks || []).map(f => f.name).join(', ') || 'none'}`);
879
+ console.log(`Manifest: ${project.manifest}`);
880
+ console.log('Commands:');
881
+ Object.entries(project.commands || {}).forEach(([key, cmd]) => console.log(` ${key}: ${cmd.label || key} → ${cmd.command.join(' ')}`));
882
+ }
883
+ return;
884
+ }
885
+
886
+ if (args.runCommand) {
887
+ const projects = await discoverProjects(rootPath);
888
+ const targetDir = projects.length > 0 ? (args.root ? projects.find(p => p.path.startsWith(rootPath)) || projects[0] : projects[0]).path : rootPath;
889
+ console.log(`Running "${args.runCommand}" in ${targetDir}...`);
890
+ const subprocess = execa(args.runCommand, { cwd: targetDir, shell: true, stdio: 'inherit' });
891
+ await subprocess;
892
+ return;
893
+ }
894
+
895
+ if (args.addPkg) {
896
+ const projects = await discoverProjects(rootPath);
897
+ const target = projects[0];
898
+ if (!target) { console.error('No projects detected'); process.exit(1); }
899
+ const cmd = getAddCmd(target, args.addPkg);
900
+ if (!cmd) { console.error(`Cannot add package: unsupported project type ${target.type}`); process.exit(1); }
901
+ console.log(`Adding ${args.addPkg} to ${target.name}...`);
902
+ const subprocess = execa(cmd[0], cmd.slice(1), { cwd: target.path, stdio: 'inherit' });
903
+ await subprocess;
904
+ return;
905
+ }
906
+
907
+ if (args.removePkg) {
831
908
  const projects = await discoverProjects(rootPath);
832
- console.log(`Detected ${projects.length} project(s) under ${rootPath}`);
833
- projects.forEach((project) => { console.log(` [${project.type}] ${project.name} (${project.path})`); });
909
+ const target = projects[0];
910
+ if (!target) { console.error('No projects detected'); process.exit(1); }
911
+ const cmd = getRemoveCmd(target, args.removePkg);
912
+ if (!cmd) { console.error(`Cannot remove package: unsupported project type ${target.type}`); process.exit(1); }
913
+ console.log(`Removing ${args.removePkg} from ${target.name}...`);
914
+ const subprocess = execa(cmd[0], cmd.slice(1), { cwd: target.path, stdio: 'inherit' });
915
+ await subprocess;
916
+ return;
917
+ }
918
+
919
+ if (args.scaffold) {
920
+ const template = args.scaffold;
921
+ const projectName = args.name || 'my-project';
922
+ const targetPath = args.root ? path.resolve(args.root, projectName) : path.resolve(process.cwd(), projectName);
923
+ const scaffoldCmds = {
924
+ 'nextjs': ['npx', 'create-next-app@latest', targetPath],
925
+ 'nextjs-bun': ['bun', 'create', 'next-app', targetPath],
926
+ 'react-vite': ['pnpm', 'create', 'vite', targetPath, '--template', 'react'],
927
+ 'react-vite-npm': ['npm', 'create', 'vite@latest', targetPath, '--', '--template', 'react'],
928
+ 'vue-vite': ['npm', 'create', 'vite@latest', targetPath, '--', '--template', 'vue'],
929
+ 'rust': ['cargo', 'new', targetPath],
930
+ 'django': ['django-admin', 'startproject', projectName, targetPath],
931
+ 'python-basic': ['mkdir', '-p', targetPath],
932
+ 'go': ['mkdir', '-p', targetPath, '&&', 'cd', targetPath, '&&', 'go', 'mod', 'init', projectName]
933
+ };
934
+ const cmd = scaffoldCmds[template];
935
+ if (!cmd) { console.error(`Unknown template: ${template}. Available: ${Object.keys(scaffoldCmds).join(', ')}`); process.exit(1); }
936
+ console.log(`Scaffolding ${template} at ${targetPath}...`);
937
+ if (template === 'go') {
938
+ await execa('mkdir', ['-p', targetPath]);
939
+ await execa('go', ['mod', 'init', projectName], { cwd: targetPath });
940
+ } else {
941
+ const subprocess = execa(cmd[0], cmd.slice(1), { stdio: 'inherit' });
942
+ await subprocess;
943
+ }
944
+ console.log(`✓ Project created at ${targetPath}`);
945
+ return;
946
+ }
947
+
948
+ if (args.studioCheck) {
949
+ console.log('Environment check running...');
950
+ const checks = [
951
+ {name: 'Node.js', binary: 'node', versionCmd: ['-v']},
952
+ {name: 'npm', binary: 'npm', versionCmd: ['-v']},
953
+ {name: 'Python', binary: process.platform === 'win32' ? 'python' : 'python3', versionCmd: ['--version']},
954
+ {name: 'Rust (Cargo)', binary: 'cargo', versionCmd: ['--version']},
955
+ {name: 'Go', binary: 'go', versionCmd: ['version']},
956
+ {name: 'Java', binary: 'java', versionCmd: ['-version']},
957
+ {name: 'PHP', binary: 'php', versionCmd: ['-v']},
958
+ {name: 'Ruby', binary: 'ruby', versionCmd: ['-v']},
959
+ {name: '.NET', binary: 'dotnet', versionCmd: ['--version']}
960
+ ];
961
+ for (const lang of checks) {
962
+ try {
963
+ const { stdout, stderr } = await execa(lang.binary, lang.versionCmd);
964
+ const version = (stdout || stderr || '').split('\n')[0].trim();
965
+ console.log(` ✓ ${lang.name}: ${version}`);
966
+ } catch {
967
+ console.log(` ✗ ${lang.name}: not installed`);
968
+ }
969
+ }
970
+ return;
971
+ }
972
+
973
+ if (args.aiAnalyze) {
974
+ console.log('AI Analysis is only available in TUI mode. Run: project-compass');
834
975
  return;
835
976
  }
836
977