project-compass 3.6.4 โ†’ 3.6.6

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/README.md CHANGED
@@ -28,6 +28,7 @@ npm install -g project-compass
28
28
  - **Background Orchestration**: Keep tasks running while you navigate the rest of your workspace.
29
29
  - **Process Management**: Monitor status (Running, Finished, Failed, Killed) and forcefully terminate processes with `Shift+K`.
30
30
  - **Task Identity**: Rename tasks on the fly with `Shift+R` to keep your workspace organized.
31
+ - **Cross-Platform Safety**: Automatically handles process signaling for both Unix (SIGKILL) and Windows (taskkill) to ensure clean exits.
31
32
 
32
33
  ![Task Manager](https://raw.githubusercontent.com/CrimsonDevil333333/project-compass/master/assets/screenshots/taskmanager.jpg)
33
34
 
@@ -76,7 +77,8 @@ npm install -g project-compass
76
77
 
77
78
  Project Compass is designed to be personalized. All settings and custom commands are stored in a local JSON file.
78
79
 
79
- - **Location**: `~/.project-compass/config.json` (Automatically created on first run)
80
+ - **Linux/macOS**: `~/.project-compass/config.json`
81
+ - **Windows**: `C:\Users\<YourUser>\.project-compass\config.json`
80
82
 
81
83
  ### ๐Ÿ› ๏ธ Adding Custom Commands
82
84
  You can add your own project-specific commands directly within the app by pressing **Shift+C** in the Detail View. Use the format `Label|Command` (e.g., `Deploy|npm run deploy`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "3.6.4",
3
+ "version": "3.6.6",
4
4
  "description": "Futuristic project navigator and runner for Node, Python, Rust, and Go",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -630,26 +630,26 @@ async function main() {
630
630
  }
631
631
  if (args.help) {
632
632
  console.clear();
633
- console.log(kleur.magenta.bold('๐Ÿงญ Project Compass ยท Premium Developer Cockpit'));
633
+ console.log(kleur.bold(kleur.magenta('๐Ÿงญ Project Compass ยท Premium Developer Cockpit')));
634
634
  console.log(kleur.dim('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
635
635
  console.log('');
636
636
  console.log(kleur.bold('Usage:'));
637
637
  console.log(' project-compass [--dir <path>] [--studio]');
638
638
  console.log('');
639
- console.log(kleur.cyan.bold('๐ŸŒŒ Core Views:'));
639
+ console.log(kleur.bold(kleur.cyan('๐ŸŒŒ Core Views:')));
640
640
  console.log(' Shift+T ' + kleur.bold('Orbit Task Manager') + ' - Manage background processes & stream logs');
641
641
  console.log(' Shift+P ' + kleur.bold('Package Registry') + ' - Direct dependency management (add/remove)');
642
642
  console.log(' Shift+N ' + kleur.bold('Project Architect') + ' - Scaffold new projects from templates');
643
643
  console.log(' Shift+A ' + kleur.bold('Omni-Studio') + ' - Environment & runtime health audit');
644
644
  console.log('');
645
- console.log(kleur.yellow.bold('๐ŸŽฎ Navigation & Details:'));
645
+ console.log(kleur.bold(kleur.yellow('๐ŸŽฎ Navigation & Details:')));
646
646
  console.log(' โ†‘ / โ†“ Move focus through discovered projects');
647
647
  console.log(' Enter Toggle deep detail view (manifests, scripts, frameworks)');
648
648
  console.log(' Shift+C Add a persistent custom command to the focused project');
649
649
  console.log(' 1-9 Quick-run numbered scripts in detail view');
650
650
  console.log(' B/T/R Macro run: Build / Test / Run');
651
651
  console.log('');
652
- console.log(kleur.green.bold('๐Ÿ› ๏ธ Workspace Tools:'));
652
+ console.log(kleur.bold(kleur.green('๐Ÿ› ๏ธ Workspace Tools:')));
653
653
  console.log(' Shift+B Toggle Art-coded Build Atlas');
654
654
  console.log(' Shift+S Toggle Directory Structure Guide');
655
655
  console.log(' Shift+H Toggle Navigation Help Cards');
@@ -657,7 +657,7 @@ async function main() {
657
657
  console.log(' Shift+X Clear active log buffer');
658
658
  console.log(' Shift+E Export current session logs to .txt');
659
659
  console.log('');
660
- console.log(kleur.red.bold('๐Ÿšช System:'));
660
+ console.log(kleur.bold(kleur.red('๐Ÿšช System:')));
661
661
  console.log(' Shift+Q Quit application (triggers confirmation if tasks are running)');
662
662
  console.log(' Esc Global "Back" key to return to Main Navigator');
663
663
  console.log('');
package/src/cli.js.bak ADDED
@@ -0,0 +1,679 @@
1
+ #!/usr/bin/env node
2
+ import React, {useCallback, useEffect, useMemo, useRef, useState, memo} from 'react';
3
+ import {render, Box, Text, useApp, useInput} from 'ink';
4
+ import path from 'path';
5
+ import {fileURLToPath} from 'url';
6
+ import fs from 'fs';
7
+ import kleur from 'kleur';
8
+ import {execa} from 'execa';
9
+ import {discoverProjects, SCHEMA_GUIDE} from './projectDetection.js';
10
+ import {CONFIG_PATH, ensureConfigDir} from './configPaths.js';
11
+
12
+ // Modular Components
13
+ import Studio from './components/Studio.js';
14
+ import TaskManager from './components/TaskManager.js';
15
+ import PackageRegistry from './components/PackageRegistry.js';
16
+ import ProjectArchitect from './components/ProjectArchitect.js';
17
+
18
+ const create = React.createElement;
19
+ const ART_CHARS = ['โ–', 'โ–ƒ', 'โ–„', 'โ–…', 'โ–‡'];
20
+ const ART_COLORS = ['magenta', 'blue', 'cyan', 'yellow', 'red'];
21
+ const OUTPUT_WINDOW_SIZE = 8;
22
+ const OUTPUT_WINDOW_HEIGHT = OUTPUT_WINDOW_SIZE + 2;
23
+ const PROJECTS_MIN_WIDTH = 32;
24
+ const DETAILS_MIN_WIDTH = 44;
25
+ const HELP_CARD_MIN_WIDTH = 28;
26
+ const ACTION_MAP = {
27
+ b: 'build',
28
+ t: 'test',
29
+ r: 'run'
30
+ };
31
+
32
+ function saveConfig(config) {
33
+ try {
34
+ ensureConfigDir();
35
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
36
+ } catch (error) {
37
+ console.error(`Unable to persist config: ${error.message}`);
38
+ }
39
+ }
40
+
41
+ function loadConfig() {
42
+ try {
43
+ if (fs.existsSync(CONFIG_PATH)) {
44
+ const payload = fs.readFileSync(CONFIG_PATH, 'utf-8');
45
+ const parsed = JSON.parse(payload || '{}');
46
+ return {
47
+ customCommands: {},
48
+ showArtBoard: true,
49
+ showHelpCards: false,
50
+ showStructureGuide: false,
51
+ ...parsed,
52
+ };
53
+ }
54
+ } catch (error) {
55
+ console.error(`Ignoring corrupt config: ${error.message}`);
56
+ }
57
+ return {customCommands: {}, showArtBoard: true, showHelpCards: false, showStructureGuide: false};
58
+ }
59
+
60
+ function useScanner(rootPath) {
61
+ const [state, setState] = useState({projects: [], loading: true, error: null});
62
+
63
+ useEffect(() => {
64
+ let cancelled = false;
65
+ (async () => {
66
+ try {
67
+ const projects = await discoverProjects(rootPath);
68
+ if (!cancelled) {
69
+ setState({projects, loading: false, error: null});
70
+ }
71
+ } catch (error) {
72
+ if (!cancelled) {
73
+ setState({projects: [], loading: false, error: error.message});
74
+ }
75
+ }
76
+ })();
77
+ return () => {
78
+ cancelled = true;
79
+ };
80
+ }, [rootPath]);
81
+
82
+ return state;
83
+ }
84
+
85
+ function buildDetailCommands(project, config) {
86
+ if (!project) return [];
87
+ const builtins = Object.entries(project.commands || {}).map(([key, command]) => ({
88
+ label: command.label || key,
89
+ command: command.command,
90
+ source: command.source || 'builtin'
91
+ }));
92
+ const custom = (config.customCommands?.[project.path] || []).map((entry) => ({
93
+ label: entry.label,
94
+ command: entry.command,
95
+ source: 'custom'
96
+ }));
97
+ return [...builtins, ...custom];
98
+ }
99
+
100
+ function CursorText({value, cursorIndex, active = true}) {
101
+ const before = value.slice(0, cursorIndex);
102
+ const charAt = value[cursorIndex] || ' ';
103
+ const after = value.slice(cursorIndex + 1);
104
+
105
+ return create(
106
+ Text,
107
+ null,
108
+ before,
109
+ active ? create(Text, {backgroundColor: 'white', color: 'black'}, charAt) : charAt,
110
+ after
111
+ );
112
+ }
113
+
114
+ const OutputPanel = memo(({activeTask, logOffset}) => {
115
+ const logs = activeTask?.logs || [];
116
+ const logWindowStart = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE - logOffset);
117
+ const logWindowEnd = Math.max(0, logs.length - logOffset);
118
+ const visibleLogs = logs.slice(logWindowStart, logWindowEnd);
119
+
120
+ const logNodes = visibleLogs.length
121
+ ? visibleLogs.map((line, i) => create(Text, {key: i}, line))
122
+ : [create(Text, {key: 'empty', dimColor: true}, 'Select a task or run a command to see logs.')];
123
+
124
+ return create(
125
+ Box,
126
+ {
127
+ flexDirection: 'column',
128
+ borderStyle: 'round',
129
+ borderColor: 'yellow',
130
+ padding: 1,
131
+ minHeight: OUTPUT_WINDOW_HEIGHT,
132
+ maxHeight: OUTPUT_WINDOW_HEIGHT,
133
+ height: OUTPUT_WINDOW_HEIGHT,
134
+ overflow: 'hidden'
135
+ },
136
+ ...logNodes
137
+ );
138
+ });
139
+
140
+ function Compass({rootPath, initialView = 'navigator'}) {
141
+ const {exit} = useApp();
142
+ const {projects, loading, error} = useScanner(rootPath);
143
+ const [selectedIndex, setSelectedIndex] = useState(0);
144
+ const [viewMode, setViewMode] = useState('list');
145
+ const [mainView, setMainView] = useState(initialView);
146
+ const [tasks, setTasks] = useState([]);
147
+ const [activeTaskId, setActiveTaskId] = useState(null);
148
+ const [logOffset, setLogOffset] = useState(0);
149
+ const [customMode, setCustomMode] = useState(false);
150
+ const [customInput, setCustomInput] = useState('');
151
+ const [customCursor, setCustomCursor] = useState(0);
152
+ const [renameMode, setRenameMode] = useState(false);
153
+ const [renameInput, setRenameInput] = useState('');
154
+ const [renameCursor, setRenameCursor] = useState(0);
155
+ const [quitConfirm, setQuitConfirm] = useState(false);
156
+ const [config, setConfig] = useState(() => loadConfig());
157
+ const [stdinBuffer, setStdinBuffer] = useState('');
158
+ const [stdinCursor, setStdinCursor] = useState(0);
159
+ const [showHelp, setShowHelp] = useState(false);
160
+ const runningProcessMap = useRef(new Map());
161
+ const lastCommandRef = useRef(null);
162
+
163
+ const activeTask = useMemo(() => tasks.find(t => t.id === activeTaskId), [tasks, activeTaskId]);
164
+ const running = activeTask?.status === 'running';
165
+ const hasRunningTasks = useMemo(() => tasks.some(t => t.status === 'running'), [tasks]);
166
+ const selectedProject = useMemo(() => projects[selectedIndex] || null, [projects, selectedIndex]);
167
+
168
+ const addLogToTask = useCallback((taskId, line) => {
169
+ setTasks(prev => {
170
+ const idx = prev.findIndex(t => t.id === taskId);
171
+ if (idx === -1) return prev;
172
+ const t = prev[idx];
173
+ const normalized = typeof line === 'string' ? line : JSON.stringify(line);
174
+ const newLines = normalized.split(/\r?\n/).filter(l => l.trim().length > 0);
175
+ const nextLogs = [...t.logs, ...newLines];
176
+ const updatedTask = { ...t, logs: nextLogs.length > 500 ? nextLogs.slice(-500) : nextLogs };
177
+ const nextTasks = [...prev];
178
+ nextTasks[idx] = updatedTask;
179
+ return nextTasks;
180
+ });
181
+ }, []);
182
+
183
+ const detailedIndexed = useMemo(() => buildDetailCommands(selectedProject, config).map((command, index) => {
184
+ const isOver9 = index >= 9;
185
+ const shortcut = isOver9 ? `S+${String.fromCharCode(65 + index - 9)}` : `${index + 1}`;
186
+ return { ...command, shortcut };
187
+ }), [selectedProject, config]);
188
+
189
+ const detailShortcutMap = useMemo(() => {
190
+ const map = new Map();
191
+ detailedIndexed.forEach((cmd) => {
192
+ if (cmd.shortcut.startsWith('S+')) {
193
+ map.set(cmd.shortcut.slice(2).toLowerCase(), cmd);
194
+ } else {
195
+ map.set(cmd.shortcut, cmd);
196
+ }
197
+ });
198
+ return map;
199
+ }, [detailedIndexed]);
200
+
201
+ const handleKillTask = useCallback((taskId) => {
202
+ const proc = runningProcessMap.current.get(taskId);
203
+ if (proc) {
204
+ addLogToTask(taskId, kleur.yellow('! Triggering emergency kill sequence...'));
205
+ try {
206
+ if (process.platform === 'win32') {
207
+ execa('taskkill', ['/pid', proc.pid, '/f', '/t']);
208
+ } else if (proc.pid) {
209
+ process.kill(-proc.pid, 'SIGKILL');
210
+ } else {
211
+ proc.kill('SIGKILL');
212
+ }
213
+ } catch (e) {
214
+ addLogToTask(taskId, kleur.red(`โœ— Kill failed: ${e.message}`));
215
+ }
216
+ } else {
217
+ setTasks(prev => prev.filter(t => t.id !== taskId));
218
+ if (activeTaskId === taskId) setActiveTaskId(null);
219
+ }
220
+ }, [activeTaskId, addLogToTask]);
221
+
222
+ const killAllTasks = useCallback(() => {
223
+ runningProcessMap.current.forEach((proc, tid) => {
224
+ handleKillTask(tid);
225
+ });
226
+ runningProcessMap.current.clear();
227
+ }, [handleKillTask]);
228
+
229
+ const runProjectCommand = useCallback(async (commandMeta, targetProject = selectedProject) => {
230
+ const project = targetProject || selectedProject;
231
+ if (!project) return;
232
+ if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) return;
233
+
234
+ const commandLabel = commandMeta.label || commandMeta.command.join(' ');
235
+ const taskId = `task-${Date.now()}`;
236
+ const newTask = {
237
+ id: taskId,
238
+ name: `${project.name} ยท ${commandLabel}`,
239
+ status: 'running',
240
+ logs: [kleur.cyan(`> ${commandMeta.command.join(' ')}`)],
241
+ project: project.name
242
+ };
243
+
244
+ setTasks(prev => [...prev, newTask]);
245
+ setActiveTaskId(taskId);
246
+ lastCommandRef.current = {project, commandMeta};
247
+
248
+ try {
249
+ const subprocess = execa(commandMeta.command[0], commandMeta.command.slice(1), {
250
+ cwd: project.path,
251
+ env: process.env,
252
+ stdin: 'pipe',
253
+ detached: process.platform !== 'win32',
254
+ cleanup: true
255
+ });
256
+ runningProcessMap.current.set(taskId, subprocess);
257
+
258
+ subprocess.stdout?.on('data', (chunk) => addLogToTask(taskId, chunk.toString()));
259
+ subprocess.stderr?.on('data', (chunk) => addLogToTask(taskId, kleur.red(chunk.toString())));
260
+
261
+ await subprocess;
262
+ setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'finished'} : t));
263
+ addLogToTask(taskId, kleur.green(`โœ“ ${commandLabel} finished`));
264
+ } catch (error) {
265
+ if (error.isCanceled || error.killed || error.signal === 'SIGKILL' || error.signal === 'SIGINT') {
266
+ setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'killed'} : t));
267
+ addLogToTask(taskId, kleur.yellow(`! Task killed forcefully`));
268
+ } else {
269
+ setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'failed'} : t));
270
+ addLogToTask(taskId, kleur.red(`โœ— ${commandLabel} failed: ${error.shortMessage || error.message}`));
271
+ }
272
+ } finally {
273
+ runningProcessMap.current.delete(taskId);
274
+ }
275
+ }, [addLogToTask, selectedProject]);
276
+
277
+ const exportLogs = useCallback(() => {
278
+ const taskToExport = tasks.find(t => t.id === activeTaskId);
279
+ if (!taskToExport || !taskToExport.logs.length) return;
280
+ try {
281
+ const exportPath = path.resolve(process.cwd(), `compass-${taskToExport.id}.txt`);
282
+ fs.writeFileSync(exportPath, taskToExport.logs.join('\n'));
283
+ addLogToTask(activeTaskId, kleur.green(`โœ“ Logs exported to ${exportPath}`));
284
+ } catch {
285
+ addLogToTask(activeTaskId, kleur.red('โœ— Export failed'));
286
+ }
287
+ }, [tasks, activeTaskId, addLogToTask]);
288
+
289
+ useInput((input, key) => {
290
+ if (quitConfirm) {
291
+ if (input?.toLowerCase() === 'y') { killAllTasks(); exit(); return; }
292
+ if (input?.toLowerCase() === 'n' || key.escape) { setQuitConfirm(false); return; }
293
+ return;
294
+ }
295
+
296
+ const isCtrlC = (key.ctrl && input === 'c') || input === '\u0003';
297
+
298
+ if (customMode) {
299
+ if (key.return) {
300
+ const raw = customInput.trim();
301
+ const selProj = selectedProject;
302
+ if (selProj && raw) {
303
+ const [labelPart, commandPart] = raw.split('|');
304
+ const commandTokens = (commandPart || labelPart).trim().split(/\s+/).filter(Boolean);
305
+ if (commandTokens.length) {
306
+ const label = commandPart ? labelPart.trim() : `Custom ${selProj.name}`;
307
+ setConfig((prev) => {
308
+ const projectKey = selProj.path;
309
+ const existing = prev.customCommands?.[projectKey] || [];
310
+ const nextConfig = { ...prev, customCommands: { ...prev.customCommands, [projectKey]: [...existing, {label, command: commandTokens}] } };
311
+ saveConfig(nextConfig);
312
+ return nextConfig;
313
+ });
314
+ }
315
+ }
316
+ setCustomMode(false); setCustomInput(''); setCustomCursor(0);
317
+ return;
318
+ }
319
+ if (key.escape) { setCustomMode(false); setCustomInput(''); setCustomCursor(0); return; }
320
+ if (key.backspace || key.delete) {
321
+ if (customCursor > 0) {
322
+ setCustomInput((prev) => prev.slice(0, customCursor - 1) + prev.slice(customCursor));
323
+ setCustomCursor(c => Math.max(0, c - 1));
324
+ }
325
+ return;
326
+ }
327
+ if (key.leftArrow) { setCustomCursor(c => Math.max(0, c - 1)); return; }
328
+ if (key.rightArrow) { setCustomCursor(c => Math.min(customInput.length, c + 1)); return; }
329
+ if (input) {
330
+ setCustomInput((prev) => prev.slice(0, customCursor) + input + prev.slice(customCursor));
331
+ setCustomCursor(c => c + input.length);
332
+ }
333
+ return;
334
+ }
335
+
336
+ if (renameMode) {
337
+ if (key.return) {
338
+ setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, name: renameInput} : t));
339
+ setRenameMode(false); setRenameInput(''); setRenameCursor(0);
340
+ return;
341
+ }
342
+ if (key.escape) { setRenameMode(false); setRenameInput(''); setRenameCursor(0); return; }
343
+ if (key.backspace || key.delete) {
344
+ if (renameCursor > 0) {
345
+ setRenameInput((prev) => prev.slice(0, renameCursor - 1) + prev.slice(renameCursor));
346
+ setRenameCursor(c => Math.max(0, c - 1));
347
+ }
348
+ return;
349
+ }
350
+ if (key.leftArrow) { setRenameCursor(c => Math.max(0, c - 1)); return; }
351
+ if (key.rightArrow) { setRenameCursor(c => Math.min(renameInput.length, c + 1)); return; }
352
+ if (input) {
353
+ setRenameInput((prev) => prev.slice(0, renameCursor) + input + prev.slice(renameCursor));
354
+ setRenameCursor(c => c + input.length);
355
+ }
356
+ return;
357
+ }
358
+
359
+ const normalizedInput = input?.toLowerCase();
360
+ const shiftCombo = (char) => key.shift && normalizedInput === char;
361
+
362
+ const clearAndSwitch = (view) => {
363
+ console.clear();
364
+ setMainView(view);
365
+ setViewMode('list');
366
+ setShowHelp(false);
367
+ };
368
+
369
+ if (shiftCombo('h')) { console.clear(); setConfig(prev => { const next = {...prev, showHelpCards: !prev.showHelpCards}; saveConfig(next); return next; }); return; }
370
+ if (shiftCombo('s')) { console.clear(); setConfig(prev => { const next = {...prev, showStructureGuide: !prev.showStructureGuide}; saveConfig(next); return next; }); return; }
371
+ if (shiftCombo('a')) { clearAndSwitch(mainView === 'navigator' ? 'studio' : 'navigator'); return; }
372
+ if (shiftCombo('p')) { clearAndSwitch(mainView === 'navigator' ? 'registry' : 'navigator'); return; }
373
+ if (shiftCombo('n')) { clearAndSwitch(mainView === 'navigator' ? 'architect' : 'navigator'); return; }
374
+ if (shiftCombo('x')) { console.clear(); setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, logs: []} : t)); setLogOffset(0); return; }
375
+ if (shiftCombo('e')) { exportLogs(); return; }
376
+ if (shiftCombo('d')) { console.clear(); setActiveTaskId(null); return; }
377
+ if (shiftCombo('b')) { console.clear(); setConfig(prev => { const next = {...prev, showArtBoard: !prev.showArtBoard}; saveConfig(next); return next; }); return; }
378
+
379
+ if (shiftCombo('t')) {
380
+ setMainView((prev) => {
381
+ console.clear();
382
+ if (prev === 'tasks') return 'navigator';
383
+ if (tasks.length > 0 && !activeTaskId) setActiveTaskId(tasks[0].id);
384
+ return 'tasks';
385
+ });
386
+ setViewMode('list');
387
+ setShowHelp(false);
388
+ return;
389
+ }
390
+
391
+ if (key.escape) {
392
+ if (mainView !== 'navigator') {
393
+ clearAndSwitch('navigator');
394
+ return;
395
+ }
396
+ }
397
+
398
+ const scrollLogs = (delta) => {
399
+ setLogOffset((prev) => {
400
+ const logs = activeTask?.logs || [];
401
+ const maxScroll = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE);
402
+ return Math.max(0, Math.min(maxScroll, prev + delta));
403
+ });
404
+ };
405
+
406
+ if (mainView === 'tasks') {
407
+ if (tasks.length > 0) {
408
+ if (key.upArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) - 1 + tasks.length) % tasks.length]?.id); return; }
409
+ if (key.downArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) + 1) % tasks.length]?.id); return; }
410
+ if (shiftCombo('k') && activeTaskId) { handleKillTask(activeTaskId); return; }
411
+ if (shiftCombo('r') && activeTaskId) { setRenameMode(true); setRenameInput(activeTask.name); setRenameCursor(activeTask.name.length); return; }
412
+ if (isCtrlC) { handleKillTask(activeTaskId); return; }
413
+ }
414
+ if (key.return) { setMainView('navigator'); return; }
415
+ return;
416
+ }
417
+
418
+ if (mainView === 'registry' || mainView === 'architect') {
419
+ return;
420
+ }
421
+
422
+ if (running && activeTaskId && runningProcessMap.current.has(activeTaskId)) {
423
+ if (isCtrlC) { handleKillTask(activeTaskId); setStdinBuffer(''); setStdinCursor(0); return; }
424
+ if (key.return) {
425
+ const proc = runningProcessMap.current.get(activeTaskId);
426
+ proc?.stdin?.write(stdinBuffer + '\n'); setStdinBuffer(''); setStdinCursor(0); return;
427
+ }
428
+ if (key.backspace || key.delete) {
429
+ if (stdinCursor > 0) {
430
+ setStdinBuffer(prev => prev.slice(0, stdinCursor - 1) + prev.slice(stdinCursor));
431
+ setStdinCursor(c => Math.max(0, c - 1));
432
+ }
433
+ return;
434
+ }
435
+ if (key.leftArrow) { setStdinCursor(c => Math.max(0, c - 1)); return; }
436
+ if (key.rightArrow) { setStdinCursor(c => Math.min(stdinBuffer.length, c + 1)); return; }
437
+ if (input) {
438
+ setStdinBuffer(prev => prev.slice(0, stdinCursor) + input + prev.slice(stdinCursor));
439
+ setStdinCursor(c => c + input.length);
440
+ }
441
+ return;
442
+ }
443
+
444
+ if (key.shift && key.upArrow) { scrollLogs(1); return; }
445
+ if (key.shift && key.downArrow) { scrollLogs(-1); return; }
446
+
447
+ if (normalizedInput === '?') { console.clear(); setShowHelp((prev) => !prev); return; }
448
+ if (shiftCombo('l') && lastCommandRef.current) { runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project); return; }
449
+
450
+ if (key.upArrow && !key.shift && projects.length > 0) { console.clear(); setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length); return; }
451
+ if (key.downArrow && !key.shift && projects.length > 0) { console.clear(); setSelectedIndex((prev) => (prev + 1) % projects.length); return; }
452
+ if (key.return) {
453
+ if (!selectedProject) return;
454
+ console.clear();
455
+ setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
456
+ return;
457
+ }
458
+ if (shiftCombo('q') || isCtrlC) {
459
+ if (hasRunningTasks) setQuitConfirm(true); else exit();
460
+ return;
461
+ }
462
+ if (shiftCombo('c') && viewMode === 'detail' && selectedProject) { setCustomMode(true); setCustomInput(''); setCustomCursor(0); return; }
463
+
464
+ const actionKey = normalizedInput && ACTION_MAP[normalizedInput];
465
+ if (actionKey) {
466
+ const commandMeta = selectedProject?.commands?.[actionKey];
467
+ runProjectCommand(commandMeta, selectedProject);
468
+ return;
469
+ }
470
+ if (viewMode === 'detail' && normalizedInput && detailShortcutMap.has(normalizedInput)) {
471
+ if (!isNaN(parseInt(normalizedInput))) {
472
+ runProjectCommand(detailShortcutMap.get(normalizedInput), selectedProject);
473
+ return;
474
+ }
475
+ const reserved = ['a', 'p', 'n', 'x', 'e', 'd', 'b', 't', 'q', 'h', 's', 'l', 'c'];
476
+ if (key.shift && !reserved.includes(normalizedInput)) {
477
+ runProjectCommand(detailShortcutMap.get(normalizedInput), selectedProject);
478
+ return;
479
+ }
480
+ }
481
+ });
482
+
483
+ const projectCountLabel = useMemo(() => `${projects.length} project${projects.length === 1 ? '' : 's'}`, [projects.length]);
484
+ const toggleHint = config.showHelpCards ? 'Shift+H hide help' : 'Shift+H show help';
485
+ const statusHint = activeTask ? `[${activeTask.status.toUpperCase()}] ${activeTask.name}` : 'Idle Navigator';
486
+ const orbitHint = mainView === 'tasks' ? 'Tasks View' : `Orbit: ${tasks.length} tasks`;
487
+ const artHint = config.showArtBoard ? 'Shift+B hide art' : 'Shift+B show art';
488
+
489
+ const projectRows = useMemo(() => {
490
+ if (loading) return [create(Text, {key: 'scanning', dimColor: true}, 'Scanning projectsโ€ฆ')];
491
+ if (error) return [create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`)];
492
+ if (projects.length === 0) return [create(Text, {key: 'empty', dimColor: true}, 'No recognizable project manifests found.')];
493
+
494
+ return projects.map((project, index) => {
495
+ const isSelected = index === selectedIndex;
496
+ const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
497
+ const hasMissingRuntime = project.missingBinaries && project.missingBinaries.length > 0;
498
+ return create(
499
+ Box,
500
+ {key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1},
501
+ create(
502
+ Box,
503
+ {flexDirection: 'row'},
504
+ create(Text, {color: isSelected ? 'cyan' : 'white', bold: isSelected}, `${project.icon} ${project.name}`),
505
+ hasMissingRuntime && create(Text, {color: 'red', bold: true}, ' โš ๏ธ Runtime missing')
506
+ ),
507
+ create(Text, {dimColor: true}, ` ${project.type} ยท ${path.relative(rootPath, project.path) || '.'}`),
508
+ frameworkBadges && create(Text, {dimColor: true}, ` ${frameworkBadges}`)
509
+ );
510
+ });
511
+ }, [loading, error, projects, selectedIndex, rootPath]);
512
+
513
+ const detailContent = useMemo(() => {
514
+ if (viewMode !== 'detail' || !selectedProject) {
515
+ return [create(Text, {key: 'e-h', dimColor: true}, 'Press Enter on a project to reveal details.')];
516
+ }
517
+
518
+ const content = [
519
+ create(Box, {key: 'title-row', flexDirection: 'row'},
520
+ create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
521
+ selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' โš ๏ธ MISSING RUNTIME')
522
+ ),
523
+ create(Text, {key: 'manifest', dimColor: true}, `${selectedProject.type} ยท ${selectedProject.manifest || 'detected manifest'}`),
524
+ create(Text, {key: 'loc', dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
525
+ ];
526
+ if (selectedProject.description) content.push(create(Text, {key: 'desc'}, selectedProject.description));
527
+ const frameworks = (selectedProject.frameworks || []).map((lib) => `${lib.icon} ${lib.name}`).join(', ');
528
+ if (frameworks) content.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
529
+
530
+ if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
531
+ content.push(
532
+ create(Text, {key: 'm-t', color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
533
+ create(Text, {key: 'm-l', color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`)
534
+ );
535
+ }
536
+
537
+ content.push(create(Text, {key: 'cmd-header', bold: true, marginTop: 1}, 'Commands'));
538
+ detailedIndexed.forEach((command) => {
539
+ content.push(
540
+ create(Text, {key: `d-${command.shortcut}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : ''}`),
541
+ create(Text, {key: `dl-${command.shortcut}`, dimColor: true}, ` โ†ณ ${command.command.join(' ')}`)
542
+ );
543
+ });
544
+ 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.'));
545
+ return content;
546
+ }, [viewMode, selectedProject, rootPath, detailedIndexed]);
547
+
548
+ const artTileNodes = useMemo(() => [
549
+ {label: 'Pulse', detail: projectCountLabel, accent: 'magenta', icon: 'โ—', subtext: `Workspace ยท ${path.basename(rootPath) || rootPath}`},
550
+ {label: 'Focus', detail: selectedProject?.name || 'Selection', accent: 'cyan', icon: 'โ—†', subtext: `${selectedProject?.type || 'Stack'}`},
551
+ {label: 'Orbit', detail: `${tasks.length} tasks`, accent: 'yellow', icon: 'โ– ', subtext: running ? 'Busy streaming...' : 'Idle'}
552
+ ].map(tile => create(Box, {key: tile.label, flexDirection: 'column', padding: 1, marginRight: 1, borderStyle: 'single', borderColor: tile.accent, minWidth: 24},
553
+ create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
554
+ create(Text, {bold: true}, tile.detail),
555
+ create(Text, {dimColor: true}, tile.subtext)
556
+ )), [projectCountLabel, rootPath, selectedProject, tasks.length, running]);
557
+
558
+ if (quitConfirm) {
559
+ 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'));
560
+ }
561
+
562
+ const renderView = () => {
563
+ switch (mainView) {
564
+ case 'studio': return create(Studio);
565
+ case 'tasks': return create(TaskManager, {tasks, activeTaskId, renameMode, renameInput, renameCursor, CursorText});
566
+ case 'registry': return create(PackageRegistry, {selectedProject, projects, onRunCommand: runProjectCommand, CursorText, onSelectProject: (idx) => setSelectedIndex(idx)});
567
+ case 'architect': return create(ProjectArchitect, {rootPath, onRunCommand: runProjectCommand, CursorText, onReturn: () => setMainView('navigator')});
568
+ default: {
569
+ const navigatorBody = [
570
+ create(Box, {key: 'header', justifyContent: 'space-between'},
571
+ create(Box, {flexDirection: 'column'}, create(Text, {color: 'magenta', bold: true}, 'Project Compass'), create(Text, {dimColor: true}, `${projectCountLabel} detected in ${rootPath}`)),
572
+ create(Box, {flexDirection: 'column', alignItems: 'flex-end'},
573
+ create(Text, {color: running ? 'yellow' : 'green'}, statusHint),
574
+ create(Text, {dimColor: true}, `${toggleHint} ยท ${orbitHint} ยท ${artHint} ยท Shift+Q: Quit`)
575
+ )
576
+ ),
577
+ config.showArtBoard && create(Box, {key: 'artboard', flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
578
+ 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')),
579
+ 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)))),
580
+ create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
581
+ ),
582
+ create(Box, {key: 'projects-row', marginTop: 1, flexDirection: 'row', alignItems: 'stretch', width: '100%', flexWrap: 'wrap'},
583
+ 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)),
584
+ 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)
585
+ ),
586
+ create(Box, {key: 'output-row', marginTop: 1, flexDirection: 'column'},
587
+ 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')),
588
+ create(OutputPanel, {activeTask, logOffset}),
589
+ create(Box, {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {dimColor: true}, running ? 'Type to feed stdin; Enter: submit.' : 'Run a command or press Shift+T to switch tasks.'), create(Text, {dimColor: true}, `${toggleHint}, Shift+S: Structure Guide`)),
590
+ 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})))
591
+ ),
592
+ config.showHelpCards && create(Box, {key: 'help-cards', marginTop: 1, flexDirection: 'row', justifyContent: 'space-between', flexWrap: 'wrap'}, [
593
+ {label: 'Navigation', color: 'magenta', body: ['โ†‘ / โ†“ move focus, Enter: details', 'Shift+โ†‘ / โ†“ scroll output', 'Shift+H toggle help cards', 'Shift+D detach from task']},
594
+ {label: 'Management', color: 'cyan', body: ['Shift+P Package Registry', 'Shift+N Project Architect', 'Shift+X clear / Shift+E export']},
595
+ {label: 'Orbit & Studio', color: 'yellow', body: ['Shift+T task manager', 'Shift+A studio / Shift+B art board', 'Shift+S structure / Shift+Q quit']}
596
+ ].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))))),
597
+ 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(', ')}`))),
598
+ 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.'))
599
+ ];
600
+ return create(Box, {flexDirection: 'column'}, ...navigatorBody);
601
+ }
602
+ }
603
+ };
604
+
605
+ return create(Box, {flexDirection: 'column', padding: 1, width: '100%'}, renderView());
606
+ }
607
+
608
+
609
+ function parseArgs() {
610
+ const args = {};
611
+ const tokens = process.argv.slice(2);
612
+ for (let i = 0; i < tokens.length; i += 1) {
613
+ const token = tokens[i];
614
+ if ((token === '--dir' || token === '--path') && tokens[i + 1]) { args.root = tokens[i + 1]; i += 1; }
615
+ else if (token === '--mode' && tokens[i + 1]) { args.mode = tokens[i + 1]; i += 1; }
616
+ else if (token === '--help' || token === '-h') args.help = true;
617
+ else if (token === '--version' || token === '-v') args.version = true;
618
+ else if (token === '--studio') args.view = 'studio';
619
+ }
620
+ return args;
621
+ }
622
+
623
+ async function main() {
624
+ const args = parseArgs();
625
+ if (args.version) {
626
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
627
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
628
+ console.log(`v${pkg.version}`);
629
+ return;
630
+ }
631
+ if (args.help) {
632
+ console.clear();
633
+ console.log(kleur.bold(kleur.magenta('๐Ÿงญ Project Compass ยท Premium Developer Cockpit'));
634
+ console.log(kleur.dim('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
635
+ console.log('');
636
+ console.log(kleur.bold('Usage:'));
637
+ console.log(' project-compass [--dir <path>] [--studio]');
638
+ console.log('');
639
+ console.log(kleur.bold(kleur.cyan('๐ŸŒŒ Core Views:'));
640
+ console.log(' Shift+T ' + kleur.bold('Orbit Task Manager') + ' - Manage background processes & stream logs');
641
+ console.log(' Shift+P ' + kleur.bold('Package Registry') + ' - Direct dependency management (add/remove)');
642
+ console.log(' Shift+N ' + kleur.bold('Project Architect') + ' - Scaffold new projects from templates');
643
+ console.log(' Shift+A ' + kleur.bold('Omni-Studio') + ' - Environment & runtime health audit');
644
+ console.log('');
645
+ console.log(kleur.bold(kleur.yellow('๐ŸŽฎ Navigation & Details:'));
646
+ console.log(' โ†‘ / โ†“ Move focus through discovered projects');
647
+ console.log(' Enter Toggle deep detail view (manifests, scripts, frameworks)');
648
+ console.log(' Shift+C Add a persistent custom command to the focused project');
649
+ console.log(' 1-9 Quick-run numbered scripts in detail view');
650
+ console.log(' B/T/R Macro run: Build / Test / Run');
651
+ console.log('');
652
+ console.log(kleur.green.bold('๐Ÿ› ๏ธ Workspace Tools:'));
653
+ console.log(' Shift+B Toggle Art-coded Build Atlas');
654
+ console.log(' Shift+S Toggle Directory Structure Guide');
655
+ console.log(' Shift+H Toggle Navigation Help Cards');
656
+ console.log(' Shift+โ†‘/โ†“ Scroll the live output log window');
657
+ console.log(' Shift+X Clear active log buffer');
658
+ console.log(' Shift+E Export current session logs to .txt');
659
+ console.log('');
660
+ console.log(kleur.red.bold('๐Ÿšช System:'));
661
+ console.log(' Shift+Q Quit application (triggers confirmation if tasks are running)');
662
+ console.log(' Esc Global "Back" key to return to Main Navigator');
663
+ console.log('');
664
+ console.log(kleur.dim('Documentation: https://github.com/CrimsonDevil333333/project-compass'));
665
+ console.log(kleur.magenta('Crafted for performance by Satyaa & Clawdy'));
666
+ return;
667
+ }
668
+ const rootPath = args.root ? path.resolve(args.root) : process.cwd();
669
+ if (args.mode === 'test') {
670
+ const projects = await discoverProjects(rootPath);
671
+ console.log(`Detected ${projects.length} project(s) under ${rootPath}`);
672
+ projects.forEach((project) => { console.log(` โ€ข [${project.type}] ${project.name} (${project.path})`); });
673
+ return;
674
+ }
675
+
676
+ render(create(Compass, {rootPath, initialView: args.view || 'navigator'}));
677
+ }
678
+
679
+ main().catch((error) => { console.error(error); process.exit(1); });
Binary file