project-compass 2.7.1 โ 2.8.1
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 +9 -16
- package/package.json +1 -1
- package/src/cli.js +167 -526
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Project Compass (v2.
|
|
1
|
+
# Project Compass (v2.8.0)
|
|
2
2
|
|
|
3
3
|
Project Compass is a futuristic CLI navigator built with [Ink](https://github.com/vadimdemedes/ink) that scans your current folder tree for familiar code projects and gives you one-keystroke access to build, test, or run them.
|
|
4
4
|
|
|
@@ -9,6 +9,7 @@ Project Compass is a futuristic CLI navigator built with [Ink](https://github.co
|
|
|
9
9
|
- ๐ **New Keyboard-Centric UX**: Shortcuts now use **Shift** instead of Ctrl to avoid terminal interference.
|
|
10
10
|
- ๐ก **Refined Output**: Improved stdin buffer with proper spacing and reliable scrolling (Shift+โ/โ).
|
|
11
11
|
- ๐ง **Smart Detection**: Support for 20+ frameworks including **Spring Boot** (Maven/Gradle), **ASP.NET Core**, **Rocket/Actix** (Rust), **Laravel** (PHP), **Vite**, **Prisma**, and more.
|
|
12
|
+
- ๐ฐ๏ธ **Orbit Task Manager**: Run commands and **Detach** (**Shift+D**) them to the background. Manage multiple processes via **Shift+T**.
|
|
12
13
|
- โ ๏ธ **Runtime Health**: Automatically checks if the required language/runtime (e.g., `node`, `python`, `cargo`) is installed and warns you if it's missing.
|
|
13
14
|
- ๐ **Omni-Studio**: A new interactive environment intelligence mode to see all installed runtimes and versions.
|
|
14
15
|
- ๐ **Log Management**: Clear output with **Shift+X** or export logs to a text file with **Shift+E**.
|
|
@@ -33,11 +34,13 @@ project-compass [--dir /path/to/workspace] [--studio]
|
|
|
33
34
|
| โ / โ | Move focus, **Enter**: toggle details |
|
|
34
35
|
| B / T / R | Build / Test / Run |
|
|
35
36
|
| 1โ9 | Execute numbered detail commands |
|
|
37
|
+
| **Shift+T** | Open **Orbit Task Manager** |
|
|
38
|
+
| **Shift+D** | **Detach** from active task (runs in background) |
|
|
36
39
|
| **Shift+A** | Open **Omni-Studio** (Environment View) |
|
|
37
40
|
| **Shift+C** | Add a custom command (`label|cmd`) |
|
|
38
41
|
| **Shift+X** | **Clear output logs** |
|
|
39
42
|
| **Shift+E** | **Export logs to .txt** |
|
|
40
|
-
| **Shift โ / โ** | Scroll output buffer
|
|
43
|
+
| **Shift โ / โ** | Scroll output buffer |
|
|
41
44
|
| **Shift+L** | Rerun last command |
|
|
42
45
|
| **Shift+H** | Toggle help cards |
|
|
43
46
|
| **Shift+S** | Toggle structure guide |
|
|
@@ -45,23 +48,13 @@ project-compass [--dir /path/to/workspace] [--studio]
|
|
|
45
48
|
| ? | Toggle help overlay |
|
|
46
49
|
| Ctrl+C | Interrupt running command |
|
|
47
50
|
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
Launch with `project-compass --studio` or press **Shift+A** inside the app. Omni-Studio provides real-time intelligence on your installed development environments, checking versions for Node, Python, Rust, Go, Java, and more.
|
|
51
|
+
## Orbit Task Manager
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
Project Compass v2.8 introduces background task management. You can start a build, press **Shift+D** to detach and return to the navigator, then start another task. Switch between them or view logs via the Task Manager (**Shift+T**).
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
## Frameworks
|
|
55
|
+
## Omni-Studio
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
- **Node.js**: Next.js, React, Vue, NestJS, Vite, Prisma, Tailwind
|
|
60
|
-
- **Python**: FastAPI, Django, Flask
|
|
61
|
-
- **Java/Kotlin**: Spring Boot (Maven & Gradle)
|
|
62
|
-
- **Rust**: Rocket, Actix Web
|
|
63
|
-
- **.NET**: ASP.NET Core
|
|
64
|
-
- **PHP**: Laravel
|
|
57
|
+
Launch with `project-compass --studio` or press **Shift+A** inside the app. Omni-Studio provides real-time intelligence on your installed development environments, checking versions for Node, Python, Rust, Go, Java, and more.
|
|
65
58
|
|
|
66
59
|
## License
|
|
67
60
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import fs from 'fs';
|
|
|
6
6
|
import kleur from 'kleur';
|
|
7
7
|
import {execa} from 'execa';
|
|
8
8
|
import {discoverProjects, SCHEMA_GUIDE, checkBinary} from './projectDetection.js';
|
|
9
|
-
import {CONFIG_PATH,
|
|
9
|
+
import {CONFIG_PATH, ensureConfigDir} from './configPaths.js';
|
|
10
10
|
|
|
11
11
|
const create = React.createElement;
|
|
12
12
|
const DEFAULT_CONFIG = {customCommands: {}};
|
|
@@ -17,7 +17,6 @@ const OUTPUT_WINDOW_HEIGHT = OUTPUT_WINDOW_SIZE + 2;
|
|
|
17
17
|
const PROJECTS_MIN_WIDTH = 32;
|
|
18
18
|
const DETAILS_MIN_WIDTH = 44;
|
|
19
19
|
const HELP_CARD_MIN_WIDTH = 28;
|
|
20
|
-
const RECENT_RUN_LIMIT = 5;
|
|
21
20
|
const ACTION_MAP = {
|
|
22
21
|
b: 'build',
|
|
23
22
|
t: 'test',
|
|
@@ -172,10 +171,9 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
172
171
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
173
172
|
const [viewMode, setViewMode] = useState('list');
|
|
174
173
|
const [mainView, setMainView] = useState(initialView);
|
|
175
|
-
const [
|
|
174
|
+
const [tasks, setTasks] = useState([]);
|
|
175
|
+
const [activeTaskId, setActiveTaskId] = useState(null);
|
|
176
176
|
const [logOffset, setLogOffset] = useState(0);
|
|
177
|
-
const [running, setRunning] = useState(false);
|
|
178
|
-
const [lastAction, setLastAction] = useState(null);
|
|
179
177
|
const [customMode, setCustomMode] = useState(false);
|
|
180
178
|
const [customInput, setCustomInput] = useState('');
|
|
181
179
|
const [customCursor, setCustomCursor] = useState(0);
|
|
@@ -185,18 +183,21 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
185
183
|
const [stdinBuffer, setStdinBuffer] = useState('');
|
|
186
184
|
const [stdinCursor, setStdinCursor] = useState(0);
|
|
187
185
|
const [showHelp, setShowHelp] = useState(false);
|
|
188
|
-
const [recentRuns, setRecentRuns] = useState([]);
|
|
189
186
|
const selectedProject = projects[selectedIndex] || null;
|
|
190
|
-
const
|
|
187
|
+
const runningProcessMap = useRef(new Map());
|
|
191
188
|
const lastCommandRef = useRef(null);
|
|
192
189
|
|
|
193
|
-
const
|
|
194
|
-
|
|
190
|
+
const activeTask = useMemo(() => tasks.find(t => t.id === activeTaskId), [tasks, activeTaskId]);
|
|
191
|
+
const running = activeTask?.status === 'running';
|
|
192
|
+
|
|
193
|
+
const addLogToTask = useCallback((taskId, line) => {
|
|
194
|
+
setTasks(prev => prev.map(t => {
|
|
195
|
+
if (t.id !== taskId) return t;
|
|
195
196
|
const normalized = typeof line === 'string' ? line : JSON.stringify(line);
|
|
196
197
|
const lines = normalized.split(/\r?\n/).filter(l => l.trim().length > 0);
|
|
197
|
-
const
|
|
198
|
-
return
|
|
199
|
-
});
|
|
198
|
+
const nextLogs = [...t.logs, ...lines];
|
|
199
|
+
return { ...t, logs: nextLogs.length > 500 ? nextLogs.slice(-500) : nextLogs };
|
|
200
|
+
}));
|
|
200
201
|
}, []);
|
|
201
202
|
|
|
202
203
|
const detailCommands = useMemo(() => buildDetailCommands(selectedProject, config), [selectedProject, config]);
|
|
@@ -212,85 +213,65 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
212
213
|
|
|
213
214
|
const runProjectCommand = useCallback(async (commandMeta, targetProject = selectedProject) => {
|
|
214
215
|
const project = targetProject || selectedProject;
|
|
215
|
-
if (!project)
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) {
|
|
219
|
-
addLog(kleur.gray('(no command configured)'));
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (running) {
|
|
223
|
-
addLog(kleur.yellow('โ Wait for the current task to finish.'));
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
216
|
+
if (!project) return;
|
|
217
|
+
if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) return;
|
|
226
218
|
|
|
227
219
|
const commandLabel = commandMeta.label || commandMeta.command.join(' ');
|
|
220
|
+
const taskId = `task-${Date.now()}`;
|
|
221
|
+
const newTask = {
|
|
222
|
+
id: taskId,
|
|
223
|
+
name: `${project.name} ยท ${commandLabel}`,
|
|
224
|
+
status: 'running',
|
|
225
|
+
logs: [kleur.cyan(`> ${commandMeta.command.join(' ')}`)],
|
|
226
|
+
project: project.name
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
setTasks(prev => [...prev, newTask]);
|
|
230
|
+
setActiveTaskId(taskId);
|
|
228
231
|
lastCommandRef.current = {project, commandMeta};
|
|
229
|
-
setRunning(true);
|
|
230
|
-
setLastAction(`${project.name} ยท ${commandLabel}`);
|
|
231
|
-
const fullCmd = commandMeta.command;
|
|
232
|
-
addLog(kleur.cyan(`> ${fullCmd.join(' ')}`));
|
|
233
|
-
setRecentRuns((prev) => {
|
|
234
|
-
const entry = {project: project.name, command: commandLabel, time: new Date().toLocaleTimeString()};
|
|
235
|
-
return [entry, ...prev].slice(0, RECENT_RUN_LIMIT);
|
|
236
|
-
});
|
|
237
232
|
|
|
238
233
|
try {
|
|
239
|
-
const subprocess = execa(
|
|
234
|
+
const subprocess = execa(commandMeta.command[0], commandMeta.command.slice(1), {
|
|
240
235
|
cwd: project.path,
|
|
241
236
|
env: process.env,
|
|
242
237
|
stdin: 'pipe'
|
|
243
238
|
});
|
|
244
|
-
|
|
239
|
+
runningProcessMap.current.set(taskId, subprocess);
|
|
245
240
|
|
|
246
|
-
subprocess.stdout?.on('data', (chunk) =>
|
|
247
|
-
|
|
248
|
-
});
|
|
249
|
-
subprocess.stderr?.on('data', (chunk) => {
|
|
250
|
-
addLog(kleur.red(chunk.toString()));
|
|
251
|
-
});
|
|
241
|
+
subprocess.stdout?.on('data', (chunk) => addLogToTask(taskId, chunk.toString()));
|
|
242
|
+
subprocess.stderr?.on('data', (chunk) => addLogToTask(taskId, kleur.red(chunk.toString())));
|
|
252
243
|
|
|
253
244
|
await subprocess;
|
|
254
|
-
|
|
245
|
+
setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'finished'} : t));
|
|
246
|
+
addLogToTask(taskId, kleur.green(`โ ${commandLabel} finished`));
|
|
255
247
|
} catch (error) {
|
|
256
|
-
|
|
248
|
+
setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'failed'} : t));
|
|
249
|
+
addLogToTask(taskId, kleur.red(`โ ${commandLabel} failed: ${error.shortMessage || error.message}`));
|
|
257
250
|
} finally {
|
|
258
|
-
|
|
259
|
-
setStdinBuffer('');
|
|
260
|
-
setStdinCursor(0);
|
|
261
|
-
runningProcessRef.current = null;
|
|
251
|
+
runningProcessMap.current.delete(taskId);
|
|
262
252
|
}
|
|
263
|
-
}, [
|
|
253
|
+
}, [addLogToTask, selectedProject]);
|
|
264
254
|
|
|
265
255
|
const handleAddCustomCommand = useCallback((label, commandTokens) => {
|
|
266
|
-
if (!selectedProject)
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
256
|
+
if (!selectedProject) return;
|
|
269
257
|
setConfig((prev) => {
|
|
270
258
|
const projectKey = selectedProject.path;
|
|
271
259
|
const existing = prev.customCommands?.[projectKey] || [];
|
|
272
|
-
const nextCustom = [...existing, {label, command: commandTokens}];
|
|
273
260
|
const nextConfig = {
|
|
274
261
|
...prev,
|
|
275
262
|
customCommands: {
|
|
276
263
|
...prev.customCommands,
|
|
277
|
-
[projectKey]:
|
|
264
|
+
[projectKey]: [...existing, {label, command: commandTokens}]
|
|
278
265
|
}
|
|
279
266
|
};
|
|
280
267
|
saveConfig(nextConfig);
|
|
281
268
|
return nextConfig;
|
|
282
269
|
});
|
|
283
|
-
|
|
284
|
-
}, [selectedProject, addLog]);
|
|
270
|
+
}, [selectedProject]);
|
|
285
271
|
|
|
286
272
|
const handleCustomSubmit = useCallback(() => {
|
|
287
273
|
const raw = customInput.trim();
|
|
288
|
-
if (!selectedProject) {
|
|
289
|
-
setCustomMode(false);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (!raw) {
|
|
293
|
-
addLog(kleur.gray('Canceled custom command (empty).'));
|
|
274
|
+
if (!selectedProject || !raw) {
|
|
294
275
|
setCustomMode(false);
|
|
295
276
|
setCustomInput('');
|
|
296
277
|
setCustomCursor(0);
|
|
@@ -299,44 +280,33 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
299
280
|
const [labelPart, commandPart] = raw.split('|');
|
|
300
281
|
const commandTokens = (commandPart || labelPart).trim().split(/\s+/).filter(Boolean);
|
|
301
282
|
if (!commandTokens.length) {
|
|
302
|
-
addLog(kleur.red('Custom command needs at least one token.'));
|
|
303
283
|
setCustomMode(false);
|
|
304
284
|
setCustomInput('');
|
|
305
285
|
setCustomCursor(0);
|
|
306
286
|
return;
|
|
307
287
|
}
|
|
308
288
|
const label = commandPart ? labelPart.trim() : `Custom ${selectedProject.name}`;
|
|
309
|
-
handleAddCustomCommand(label
|
|
289
|
+
handleAddCustomCommand(label, commandTokens);
|
|
310
290
|
setCustomMode(false);
|
|
311
291
|
setCustomInput('');
|
|
312
292
|
setCustomCursor(0);
|
|
313
|
-
}, [customInput, selectedProject, handleAddCustomCommand
|
|
293
|
+
}, [customInput, selectedProject, handleAddCustomCommand]);
|
|
314
294
|
|
|
315
295
|
const exportLogs = useCallback(() => {
|
|
316
|
-
if (!
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
296
|
+
if (!activeTask || !activeTask.logs.length) return;
|
|
319
297
|
try {
|
|
320
|
-
const exportPath = path.resolve(process.cwd(), `compass
|
|
321
|
-
fs.writeFileSync(exportPath,
|
|
322
|
-
|
|
298
|
+
const exportPath = path.resolve(process.cwd(), `compass-${activeTask.id}.txt`);
|
|
299
|
+
fs.writeFileSync(exportPath, activeTask.logs.join('\n'));
|
|
300
|
+
addLogToTask(activeTaskId, kleur.green(`โ Logs exported to ${exportPath}`));
|
|
323
301
|
} catch (err) {
|
|
324
|
-
|
|
302
|
+
addLogToTask(activeTaskId, kleur.red(`โ Export failed: ${err.message}`));
|
|
325
303
|
}
|
|
326
|
-
}, [
|
|
304
|
+
}, [activeTask, activeTaskId, addLogToTask]);
|
|
327
305
|
|
|
328
306
|
useInput((input, key) => {
|
|
329
307
|
if (customMode) {
|
|
330
|
-
if (key.return) {
|
|
331
|
-
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
if (key.escape) {
|
|
335
|
-
setCustomMode(false);
|
|
336
|
-
setCustomInput('');
|
|
337
|
-
setCustomCursor(0);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
308
|
+
if (key.return) { handleCustomSubmit(); return; }
|
|
309
|
+
if (key.escape) { setCustomMode(false); setCustomInput(''); setCustomCursor(0); return; }
|
|
340
310
|
if (key.backspace || key.delete) {
|
|
341
311
|
if (customCursor > 0) {
|
|
342
312
|
setCustomInput((prev) => prev.slice(0, customCursor - 1) + prev.slice(customCursor));
|
|
@@ -344,14 +314,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
344
314
|
}
|
|
345
315
|
return;
|
|
346
316
|
}
|
|
347
|
-
if (key.leftArrow) {
|
|
348
|
-
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
if (key.rightArrow) {
|
|
352
|
-
setCustomCursor(c => Math.min(customInput.length, c + 1));
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
317
|
+
if (key.leftArrow) { setCustomCursor(c => Math.max(0, c - 1)); return; }
|
|
318
|
+
if (key.rightArrow) { setCustomCursor(c => Math.min(customInput.length, c + 1)); return; }
|
|
355
319
|
if (input) {
|
|
356
320
|
setCustomInput((prev) => prev.slice(0, customCursor) + input + prev.slice(customCursor));
|
|
357
321
|
setCustomCursor(c => c + input.length);
|
|
@@ -360,51 +324,28 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
360
324
|
}
|
|
361
325
|
|
|
362
326
|
const normalizedInput = input?.toLowerCase();
|
|
363
|
-
const ctrlCombo = (char) => key.ctrl && normalizedInput === char;
|
|
364
327
|
const shiftCombo = (char) => key.shift && normalizedInput === char;
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
if (toggleShortcut('a')) {
|
|
375
|
-
setMainView((prev) => (prev === 'navigator' ? 'studio' : 'navigator'));
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
if (toggleShortcut('x')) {
|
|
379
|
-
setLogLines([]);
|
|
380
|
-
setLogOffset(0);
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
if (shiftCombo('e')) {
|
|
384
|
-
exportLogs();
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
328
|
+
|
|
329
|
+
if (shiftCombo('h')) { setShowHelpCards((prev) => !prev); return; }
|
|
330
|
+
if (shiftCombo('s')) { setShowStructureGuide((prev) => !prev); return; }
|
|
331
|
+
if (shiftCombo('a')) { setMainView((prev) => (prev === 'navigator' ? 'studio' : 'navigator')); return; }
|
|
332
|
+
if (shiftCombo('x')) { setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, logs: []} : t)); setLogOffset(0); return; }
|
|
333
|
+
if (shiftCombo('e')) { exportLogs(); return; }
|
|
334
|
+
if (shiftCombo('d')) { setActiveTaskId(null); return; }
|
|
335
|
+
if (shiftCombo('t')) { setMainView('tasks'); return; }
|
|
387
336
|
|
|
388
337
|
const scrollLogs = (delta) => {
|
|
389
338
|
setLogOffset((prev) => {
|
|
390
|
-
const
|
|
339
|
+
const logs = activeTask?.logs || [];
|
|
340
|
+
const maxScroll = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE);
|
|
391
341
|
return Math.max(0, Math.min(maxScroll, prev + delta));
|
|
392
342
|
});
|
|
393
343
|
};
|
|
394
344
|
|
|
395
|
-
if (running &&
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
setStdinCursor(0);
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
if (key.return) {
|
|
403
|
-
runningProcessRef.current.stdin?.write(stdinBuffer + '\n');
|
|
404
|
-
setStdinBuffer('');
|
|
405
|
-
setStdinCursor(0);
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
345
|
+
if (running && activeTaskId && runningProcessMap.current.has(activeTaskId)) {
|
|
346
|
+
const proc = runningProcessMap.current.get(activeTaskId);
|
|
347
|
+
if (key.ctrl && input === 'c') { proc.kill('SIGINT'); setStdinBuffer(''); setStdinCursor(0); return; }
|
|
348
|
+
if (key.return) { proc.stdin?.write(stdinBuffer + '\n'); setStdinBuffer(''); setStdinCursor(0); return; }
|
|
408
349
|
if (key.backspace || key.delete) {
|
|
409
350
|
if (stdinCursor > 0) {
|
|
410
351
|
setStdinBuffer(prev => prev.slice(0, stdinCursor - 1) + prev.slice(stdinCursor));
|
|
@@ -412,14 +353,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
412
353
|
}
|
|
413
354
|
return;
|
|
414
355
|
}
|
|
415
|
-
if (key.leftArrow) {
|
|
416
|
-
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
if (key.rightArrow) {
|
|
420
|
-
setStdinCursor(c => Math.min(stdinBuffer.length, c + 1));
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
356
|
+
if (key.leftArrow) { setStdinCursor(c => Math.max(0, c - 1)); return; }
|
|
357
|
+
if (key.rightArrow) { setStdinCursor(c => Math.min(stdinBuffer.length, c + 1)); return; }
|
|
423
358
|
if (input) {
|
|
424
359
|
setStdinBuffer(prev => prev.slice(0, stdinCursor) + input + prev.slice(stdinCursor));
|
|
425
360
|
setStdinCursor(c => c + input.length);
|
|
@@ -427,49 +362,29 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
427
362
|
return;
|
|
428
363
|
}
|
|
429
364
|
|
|
430
|
-
if (key.shift && key.upArrow) {
|
|
431
|
-
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
if (key.shift && key.downArrow) {
|
|
435
|
-
scrollLogs(1);
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
365
|
+
if (key.shift && key.upArrow) { scrollLogs(-1); return; }
|
|
366
|
+
if (key.shift && key.downArrow) { scrollLogs(1); return; }
|
|
438
367
|
|
|
439
|
-
if (normalizedInput === '?') {
|
|
440
|
-
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
if (shiftCombo('l') && lastCommandRef.current) {
|
|
444
|
-
runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
368
|
+
if (normalizedInput === '?') { setShowHelp((prev) => !prev); return; }
|
|
369
|
+
if (shiftCombo('l') && lastCommandRef.current) { runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project); return; }
|
|
447
370
|
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
return;
|
|
451
|
-
|
|
452
|
-
if (key.downArrow && !key.shift && projects.length > 0) {
|
|
453
|
-
setSelectedIndex((prev) => (prev + 1) % projects.length);
|
|
371
|
+
if (mainView === 'tasks') {
|
|
372
|
+
if (key.upArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) - 1 + tasks.length) % tasks.length]?.id); return; }
|
|
373
|
+
if (key.downArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) + 1) % tasks.length]?.id); return; }
|
|
374
|
+
if (key.return || shiftCombo('t')) { setMainView('navigator'); return; }
|
|
454
375
|
return;
|
|
455
376
|
}
|
|
377
|
+
|
|
378
|
+
if (key.upArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length); return; }
|
|
379
|
+
if (key.downArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev + 1) % projects.length); return; }
|
|
456
380
|
if (key.return) {
|
|
457
|
-
if (!selectedProject)
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
381
|
+
if (!selectedProject) return;
|
|
460
382
|
setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
|
|
461
383
|
return;
|
|
462
384
|
}
|
|
463
|
-
if (shiftCombo('q')) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
if (shiftCombo('c') && viewMode === 'detail' && selectedProject) {
|
|
468
|
-
setCustomMode(true);
|
|
469
|
-
setCustomInput('');
|
|
470
|
-
setCustomCursor(0);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
385
|
+
if (shiftCombo('q')) { exit(); return; }
|
|
386
|
+
if (shiftCombo('c') && viewMode === 'detail' && selectedProject) { setCustomMode(true); setCustomInput(''); setCustomCursor(0); return; }
|
|
387
|
+
|
|
473
388
|
const actionKey = normalizedInput && ACTION_MAP[normalizedInput];
|
|
474
389
|
if (actionKey) {
|
|
475
390
|
const commandMeta = selectedProject?.commands?.[actionKey];
|
|
@@ -482,17 +397,31 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
482
397
|
});
|
|
483
398
|
|
|
484
399
|
const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
400
|
+
|
|
401
|
+
if (mainView === 'studio') return create(Studio);
|
|
402
|
+
|
|
403
|
+
if (mainView === 'tasks') {
|
|
404
|
+
return create(
|
|
405
|
+
Box,
|
|
406
|
+
{flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1},
|
|
407
|
+
create(Text, {bold: true, color: 'yellow'}, '๐ฐ๏ธ Task Manager | Background Processes'),
|
|
408
|
+
create(Text, {dimColor: true, marginBottom: 1}, 'Select a task to view its output logs.'),
|
|
409
|
+
...tasks.map(t => create(
|
|
410
|
+
Box,
|
|
411
|
+
{key: t.id, marginBottom: 0},
|
|
412
|
+
create(Text, {color: t.id === activeTaskId ? 'cyan' : 'white', bold: t.id === activeTaskId}, `${t.id === activeTaskId ? 'โ' : ' '} [${t.status.toUpperCase()}] ${t.name}`)
|
|
413
|
+
)),
|
|
414
|
+
!tasks.length && create(Text, {dimColor: true}, 'No active or background tasks.'),
|
|
415
|
+
create(Text, {marginTop: 1, dimColor: true}, 'Press Enter/Shift+T to return to Navigator, Up/Down to switch focus.')
|
|
416
|
+
);
|
|
494
417
|
}
|
|
495
|
-
|
|
418
|
+
|
|
419
|
+
const projectRows = [];
|
|
420
|
+
if (loading) projectRows.push(create(Text, {key: 'scanning', dimColor: true}, 'Scanning projectsโฆ'));
|
|
421
|
+
if (error) projectRows.push(create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`));
|
|
422
|
+
if (!loading && !error && projects.length === 0) projectRows.push(create(Text, {key: 'empty', dimColor: true}, 'No recognizable project manifests found.'));
|
|
423
|
+
|
|
424
|
+
if (!loading) {
|
|
496
425
|
projects.forEach((project, index) => {
|
|
497
426
|
const isSelected = index === selectedIndex;
|
|
498
427
|
const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
|
|
@@ -504,14 +433,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
504
433
|
create(
|
|
505
434
|
Box,
|
|
506
435
|
{flexDirection: 'row'},
|
|
507
|
-
create(
|
|
508
|
-
Text,
|
|
509
|
-
{
|
|
510
|
-
color: isSelected ? 'cyan' : 'white',
|
|
511
|
-
bold: isSelected
|
|
512
|
-
},
|
|
513
|
-
`${project.icon} ${project.name}`
|
|
514
|
-
),
|
|
436
|
+
create(Text, {color: isSelected ? 'cyan' : 'white', bold: isSelected}, `${project.icon} ${project.name}`),
|
|
515
437
|
hasMissingRuntime && create(Text, {color: 'red', bold: true}, ' โ ๏ธ Runtime missing')
|
|
516
438
|
),
|
|
517
439
|
create(Text, {dimColor: true}, ` ${project.type} ยท ${path.relative(rootPath, project.path) || '.'}`),
|
|
@@ -524,359 +446,88 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
524
446
|
const detailContent = [];
|
|
525
447
|
if (viewMode === 'detail' && selectedProject) {
|
|
526
448
|
detailContent.push(
|
|
527
|
-
create(
|
|
528
|
-
Box,
|
|
529
|
-
{key: 'title-row', flexDirection: 'row'},
|
|
449
|
+
create(Box, {key: 'title-row', flexDirection: 'row'},
|
|
530
450
|
create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
|
|
531
451
|
selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' โ ๏ธ MISSING RUNTIME')
|
|
532
452
|
),
|
|
533
453
|
create(Text, {key: 'manifest', dimColor: true}, `${selectedProject.type} ยท ${selectedProject.manifest || 'detected manifest'}`),
|
|
534
454
|
create(Text, {key: 'loc', dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
|
|
535
455
|
);
|
|
536
|
-
if (selectedProject.description) {
|
|
537
|
-
detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
|
|
538
|
-
}
|
|
456
|
+
if (selectedProject.description) detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
|
|
539
457
|
const frameworks = (selectedProject.frameworks || []).map((lib) => `${lib.icon} ${lib.name}`).join(', ');
|
|
540
|
-
if (frameworks) {
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
if (selectedProject.extra?.scripts && selectedProject.extra.scripts.length) {
|
|
544
|
-
detailContent.push(create(Text, {key: 'scripts', dimColor: true}, `Scripts: ${selectedProject.extra.scripts.join(', ')}`));
|
|
545
|
-
}
|
|
546
|
-
|
|
458
|
+
if (frameworks) detailContent.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
|
|
459
|
+
|
|
547
460
|
if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
|
|
548
461
|
detailContent.push(
|
|
549
|
-
create(Text, {key: '
|
|
550
|
-
create(Text, {key: '
|
|
551
|
-
create(Text, {key: 'missing-hint', dimColor: true}, 'Project commands may fail until these are in your PATH.')
|
|
462
|
+
create(Text, {key: 'm-t', color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
|
|
463
|
+
create(Text, {key: 'm-l', color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`)
|
|
552
464
|
);
|
|
553
465
|
}
|
|
554
466
|
|
|
555
|
-
detailContent.push(create(Text, {key: 'config-path', dimColor: true, marginTop: 1}, `Custom commands stored in ${CONFIG_PATH}`));
|
|
556
|
-
detailContent.push(create(Text, {key: 'plugin-path', dimColor: true, marginBottom: 1}, `Extend frameworks via ${PLUGIN_FILE}`));
|
|
557
467
|
detailContent.push(create(Text, {key: 'cmd-header', bold: true, marginTop: 1}, 'Commands'));
|
|
558
468
|
detailedIndexed.forEach((command) => {
|
|
559
469
|
detailContent.push(
|
|
560
|
-
create(Text, {key: `
|
|
470
|
+
create(Text, {key: `d-${command.shortcut}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : ''}`),
|
|
471
|
+
create(Text, {key: `dl-${command.shortcut}`, dimColor: true}, ` โณ ${command.command.join(' ')}`)
|
|
561
472
|
);
|
|
562
|
-
detailContent.push(create(Text, {key: `detail-line-${command.shortcut}-${command.label}`, dimColor: true}, ` โณ ${command.command.join(' ')}`));
|
|
563
473
|
});
|
|
564
|
-
|
|
565
|
-
detailContent.push(create(Text, {key: 'no-cmds', dimColor: true}, 'No built-in commands yet. Add a custom command with Shift+C.'));
|
|
566
|
-
}
|
|
567
|
-
const setupHints = selectedProject.extra?.setupHints || [];
|
|
568
|
-
if (setupHints.length) {
|
|
569
|
-
detailContent.push(create(Text, {key: 'setup-header', dimColor: true, marginTop: 1}, 'Setup hints:'));
|
|
570
|
-
setupHints.forEach((hint, hidx) => detailContent.push(create(Text, {key: `hint-${hidx}`, dimColor: true}, ` โข ${hint}`)));
|
|
571
|
-
}
|
|
572
|
-
detailContent.push(create(Text, {key: 'hint-line', dimColor: true}, 'Press Shift+C โ label|cmd to save custom actions, Enter to close detail view.'));
|
|
474
|
+
detailContent.push(create(Text, {key: 'h-l', dimColor: true, marginTop: 1}, 'Press Shift+C โ label|cmd to save custom actions, Enter to close detail view.'));
|
|
573
475
|
} else {
|
|
574
|
-
detailContent.push(create(Text, {key: '
|
|
476
|
+
detailContent.push(create(Text, {key: 'e-h', dimColor: true}, 'Press Enter on a project to reveal details.'));
|
|
575
477
|
}
|
|
576
478
|
|
|
577
479
|
if (customMode) {
|
|
578
|
-
detailContent.push(
|
|
579
|
-
create(
|
|
580
|
-
Box,
|
|
581
|
-
{key: 'custom-input-box', flexDirection: 'row'},
|
|
582
|
-
create(Text, {color: 'cyan'}, 'Type label|cmd (Enter: save, Esc: cancel): '),
|
|
583
|
-
create(CursorText, {value: customInput, cursorIndex: customCursor})
|
|
584
|
-
)
|
|
585
|
-
);
|
|
480
|
+
detailContent.push(create(Box, {key: 'ci-box', flexDirection: 'row'}, create(Text, {color: 'cyan'}, 'Type label|cmd (Enter: save, Esc: cancel): '), create(CursorText, {value: customInput, cursorIndex: customCursor})));
|
|
586
481
|
}
|
|
587
482
|
|
|
588
|
-
const artTileNodes =
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
{
|
|
603
|
-
label: 'Focus',
|
|
604
|
-
detail: selectedName,
|
|
605
|
-
accent: 'cyan',
|
|
606
|
-
icon: 'โ',
|
|
607
|
-
subtext: `${selectedType} ยท ${selectedLocation}`
|
|
608
|
-
},
|
|
609
|
-
{
|
|
610
|
-
label: 'Rhythm',
|
|
611
|
-
detail: `${detailCommands.length} commands`,
|
|
612
|
-
accent: 'yellow',
|
|
613
|
-
icon: 'โ ',
|
|
614
|
-
subtext: statusNarrative
|
|
615
|
-
}
|
|
616
|
-
];
|
|
617
|
-
return tileDefinition.map((tile) =>
|
|
618
|
-
create(
|
|
619
|
-
Box,
|
|
620
|
-
{
|
|
621
|
-
key: tile.label,
|
|
622
|
-
flexDirection: 'column',
|
|
623
|
-
padding: 1,
|
|
624
|
-
marginRight: 1,
|
|
625
|
-
borderStyle: 'single',
|
|
626
|
-
borderColor: tile.accent,
|
|
627
|
-
minWidth: 24
|
|
628
|
-
},
|
|
629
|
-
create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
|
|
630
|
-
create(Text, {bold: true}, tile.detail),
|
|
631
|
-
create(Text, {dimColor: true}, tile.subtext)
|
|
632
|
-
)
|
|
633
|
-
);
|
|
634
|
-
}, [projectCountLabel, rootPath, selectedProject, detailCommands.length, running, lastAction]);
|
|
635
|
-
|
|
636
|
-
const artBoard = create(
|
|
637
|
-
Box,
|
|
638
|
-
{
|
|
639
|
-
flexDirection: 'column',
|
|
640
|
-
marginTop: 1,
|
|
641
|
-
borderStyle: 'round',
|
|
642
|
-
borderColor: 'gray',
|
|
643
|
-
padding: 1
|
|
644
|
-
},
|
|
645
|
-
create(
|
|
646
|
-
Box,
|
|
647
|
-
{flexDirection: 'row', justifyContent: 'space-between'},
|
|
648
|
-
create(Text, {color: 'magenta', bold: true}, 'Art-coded build atlas'),
|
|
649
|
-
create(Text, {dimColor: true}, 'press ? for overlay help')
|
|
650
|
-
),
|
|
651
|
-
create(
|
|
652
|
-
Box,
|
|
653
|
-
{flexDirection: 'row', marginTop: 1},
|
|
654
|
-
...ART_CHARS.map((char, index) =>
|
|
655
|
-
create(Text, {key: `art-${index}`, color: ART_COLORS[index % ART_COLORS.length]}, char.repeat(2))
|
|
656
|
-
)
|
|
657
|
-
),
|
|
658
|
-
create(
|
|
659
|
-
Box,
|
|
660
|
-
{flexDirection: 'row', marginTop: 1},
|
|
661
|
-
...artTileNodes
|
|
662
|
-
),
|
|
663
|
-
create(Text, {dimColor: true, marginTop: 1}, kleur.italic('The art board now follows your layout and stays inside the window.'))
|
|
483
|
+
const artTileNodes = [
|
|
484
|
+
{label: 'Pulse', detail: projectCountLabel, accent: 'magenta', icon: 'โ', subtext: `Workspace ยท ${path.basename(rootPath) || rootPath}`},
|
|
485
|
+
{label: 'Focus', detail: selectedProject?.name || 'Selection', accent: 'cyan', icon: 'โ', subtext: `${selectedProject?.type || 'Stack'}`},
|
|
486
|
+
{label: 'Orbit', detail: `${tasks.length} active tasks`, accent: 'yellow', icon: 'โ ', subtext: running ? 'Busy streaming...' : 'Idle'}
|
|
487
|
+
].map(tile => create(Box, {key: tile.label, flexDirection: 'column', padding: 1, marginRight: 1, borderStyle: 'single', borderColor: tile.accent, minWidth: 24},
|
|
488
|
+
create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
|
|
489
|
+
create(Text, {bold: true}, tile.detail),
|
|
490
|
+
create(Text, {dimColor: true}, tile.subtext)
|
|
491
|
+
));
|
|
492
|
+
|
|
493
|
+
const artBoard = create(Box, {flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
|
|
494
|
+
create(Box, {flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {color: 'magenta', bold: true}, 'Art-coded build atlas'), create(Text, {dimColor: true}, 'press ? for overlay help')),
|
|
495
|
+
create(Box, {flexDirection: 'row', marginTop: 1}, ...ART_CHARS.map((char, i) => create(Text, {key: i, color: ART_COLORS[i % ART_COLORS.length]}, char.repeat(2)))),
|
|
496
|
+
create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
|
|
664
497
|
);
|
|
665
498
|
|
|
666
|
-
const
|
|
667
|
-
const
|
|
668
|
-
const
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
: [create(Text, {key: 'no-logs', dimColor: true}, 'Logs will appear here once you run a command.')];
|
|
499
|
+
const logs = activeTask?.logs || [];
|
|
500
|
+
const logWindowStart = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE - logOffset);
|
|
501
|
+
const logWindowEnd = Math.max(0, logs.length - logOffset);
|
|
502
|
+
const visibleLogs = logs.slice(logWindowStart, logWindowEnd);
|
|
503
|
+
const logNodes = visibleLogs.length ? visibleLogs.map((line, i) => create(Text, {key: i}, line)) : [create(Text, {dimColor: true}, 'Select a task or run a command to see logs.')];
|
|
672
504
|
|
|
673
505
|
const helpCards = [
|
|
674
|
-
{
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
body: [
|
|
678
|
-
'โ / โ move focus, Enter: details',
|
|
679
|
-
'Shift+โ / โ scroll output',
|
|
680
|
-
'Shift+H toggle help cards',
|
|
681
|
-
'? opens the overlay help'
|
|
682
|
-
]
|
|
683
|
-
},
|
|
684
|
-
{
|
|
685
|
-
label: 'Command flow',
|
|
686
|
-
color: 'cyan',
|
|
687
|
-
body: [
|
|
688
|
-
'B / T / R build/test/run',
|
|
689
|
-
'1-9 run detail commands',
|
|
690
|
-
'Shift+L rerun last command',
|
|
691
|
-
'Shift+X clear / Shift+E export'
|
|
692
|
-
]
|
|
693
|
-
},
|
|
694
|
-
{
|
|
695
|
-
label: 'System & Studio',
|
|
696
|
-
color: 'yellow',
|
|
697
|
-
body: [
|
|
698
|
-
'Shift+A open Omni-Studio',
|
|
699
|
-
'Shift+S toggle structure guide',
|
|
700
|
-
'Shift+C save custom action',
|
|
701
|
-
'Shift+Q quit application'
|
|
702
|
-
]
|
|
703
|
-
}
|
|
506
|
+
{label: 'Navigation', color: 'magenta', body: ['โ / โ move focus, Enter: details', 'Shift+โ / โ scroll output', 'Shift+H toggle help cards', 'Shift+D detach from task']},
|
|
507
|
+
{label: 'Commands', color: 'cyan', body: ['B / T / R build/test/run', '1-9 run detail commands', 'Shift+L rerun last command', 'Shift+X clear / Shift+E export']},
|
|
508
|
+
{label: 'Orbit & Studio', color: 'yellow', body: ['Shift+T task manager', 'Shift+A open Omni-Studio', 'Shift+C save custom action', 'Shift+Q quit application']}
|
|
704
509
|
];
|
|
705
|
-
const helpSection = showHelpCards
|
|
706
|
-
? create(
|
|
707
|
-
Box,
|
|
708
|
-
{marginTop: 1, flexDirection: 'row', justifyContent: 'space-between', flexWrap: 'wrap'},
|
|
709
|
-
...helpCards.map((card, index) =>
|
|
710
|
-
create(
|
|
711
|
-
Box,
|
|
712
|
-
{
|
|
713
|
-
key: card.label,
|
|
714
|
-
flexGrow: 1,
|
|
715
|
-
flexBasis: 0,
|
|
716
|
-
minWidth: HELP_CARD_MIN_WIDTH,
|
|
717
|
-
marginRight: index < helpCards.length - 1 ? 1 : 0,
|
|
718
|
-
marginBottom: 1,
|
|
719
|
-
borderStyle: 'round',
|
|
720
|
-
borderColor: card.color,
|
|
721
|
-
padding: 1,
|
|
722
|
-
flexDirection: 'column'
|
|
723
|
-
},
|
|
724
|
-
create(Text, {color: card.color, bold: true, marginBottom: 1}, card.label),
|
|
725
|
-
...card.body.map((line, lineIndex) =>
|
|
726
|
-
create(Text, {key: `${card.label}-${lineIndex}`, dimColor: card.color === 'yellow'}, line)
|
|
727
|
-
)
|
|
728
|
-
)
|
|
729
|
-
)
|
|
730
|
-
)
|
|
731
|
-
: create(Text, {key: 'help-hint', dimColor: true, marginTop: 1}, 'Help cards hidden ยท press Shift+H to show navigation, command flow, and recent runs.');
|
|
732
|
-
|
|
733
|
-
const structureGuide = showStructureGuide
|
|
734
|
-
? create(
|
|
735
|
-
Box,
|
|
736
|
-
{
|
|
737
|
-
flexDirection: 'column',
|
|
738
|
-
borderStyle: 'round',
|
|
739
|
-
borderColor: 'blue',
|
|
740
|
-
marginTop: 1,
|
|
741
|
-
padding: 1
|
|
742
|
-
},
|
|
743
|
-
create(Text, {color: 'cyan', bold: true}, 'Project structure guide ยท press Shift+S to hide'),
|
|
744
|
-
...SCHEMA_GUIDE.map((entry) =>
|
|
745
|
-
create(Text, {key: entry.type, dimColor: true}, `โข ${entry.icon} ${entry.label}: ${entry.files.join(', ')}`)
|
|
746
|
-
),
|
|
747
|
-
create(Text, {dimColor: true, marginTop: 1}, 'SCHEMAS describe the manifests that trigger each language type.')
|
|
748
|
-
)
|
|
749
|
-
: null;
|
|
750
|
-
|
|
751
|
-
const helpOverlay = showHelp
|
|
752
|
-
? create(
|
|
753
|
-
Box,
|
|
754
|
-
{
|
|
755
|
-
flexDirection: 'column',
|
|
756
|
-
borderStyle: 'double',
|
|
757
|
-
borderColor: 'cyan',
|
|
758
|
-
marginTop: 1,
|
|
759
|
-
padding: 1
|
|
760
|
-
},
|
|
761
|
-
create(Text, {color: 'cyan', bold: true}, 'Help overlay ยท press ? to hide'),
|
|
762
|
-
create(Text, null, 'Shift+โ/โ scrolls logs; Shift+X clears; Shift+E exports to file; Shift+A Omni-Studio.'),
|
|
763
|
-
create(Text, null, 'B/T/R run build/test/run; 1-9 detail commands; Shift+L reruns previous command.'),
|
|
764
|
-
create(Text, null, 'Shift+H help cards, Shift+S structure guide, ? overlay, Shift+Q quits.'),
|
|
765
|
-
create(Text, null, 'Projects + Details stay paired while Output keeps its own full-width band.'),
|
|
766
|
-
create(Text, null, 'Structure guide lists the manifests that trigger each language detection.')
|
|
767
|
-
)
|
|
768
|
-
: null;
|
|
769
|
-
|
|
770
|
-
if (mainView === 'studio') {
|
|
771
|
-
return create(Studio);
|
|
772
|
-
}
|
|
773
510
|
|
|
774
|
-
const toggleHint = showHelpCards ? 'Shift+H
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
return create(
|
|
780
|
-
Box,
|
|
781
|
-
{flexDirection: 'column', padding: 1},
|
|
782
|
-
create(
|
|
783
|
-
Box,
|
|
784
|
-
{justifyContent: 'space-between'},
|
|
785
|
-
create(
|
|
786
|
-
Box,
|
|
787
|
-
{flexDirection: 'column'},
|
|
788
|
-
create(Text, {color: 'magenta', bold: true}, 'Project Compass'),
|
|
789
|
-
create(Text, {dimColor: true}, loading ? 'Scanning workspacesโฆ' : `${projectCountLabel} detected in ${rootPath}`),
|
|
790
|
-
create(Text, {dimColor: true}, 'Use keyboard arrows to navigate, ? for help.')
|
|
791
|
-
),
|
|
792
|
-
create(
|
|
793
|
-
Box,
|
|
794
|
-
{flexDirection: 'column', alignItems: 'flex-end'},
|
|
795
|
-
create(Text, {color: running ? 'yellow' : 'green'}, running ? 'Busy streaming...' : lastAction ? `Last action ยท ${lastAction}` : 'Idle'),
|
|
796
|
-
create(Text, {dimColor: true}, headerHint)
|
|
797
|
-
)
|
|
511
|
+
const toggleHint = showHelpCards ? 'Shift+H hide help' : 'Shift+H show help';
|
|
512
|
+
return create(Box, {flexDirection: 'column', padding: 1},
|
|
513
|
+
create(Box, {justifyContent: 'space-between'},
|
|
514
|
+
create(Box, {flexDirection: 'column'}, create(Text, {color: 'magenta', bold: true}, 'Project Compass'), create(Text, {dimColor: true}, `${projectCountLabel} detected in ${rootPath}`)),
|
|
515
|
+
create(Box, {flexDirection: 'column', alignItems: 'flex-end'}, create(Text, {color: running ? 'yellow' : 'green'}, activeTask ? `[${activeTask.status.toUpperCase()}] ${activeTask.name}` : 'Idle Navigator'), create(Text, {dimColor: true}, `${toggleHint}, Shift+T: Tasks, Shift+Q: Quit`))
|
|
798
516
|
),
|
|
799
517
|
artBoard,
|
|
800
|
-
create(
|
|
801
|
-
Box,
|
|
802
|
-
{
|
|
803
|
-
create(
|
|
804
|
-
Box,
|
|
805
|
-
{
|
|
806
|
-
flexGrow: 1,
|
|
807
|
-
flexBasis: 0,
|
|
808
|
-
flexShrink: 1,
|
|
809
|
-
minWidth: PROJECTS_MIN_WIDTH,
|
|
810
|
-
marginRight: 1,
|
|
811
|
-
borderStyle: 'round',
|
|
812
|
-
borderColor: 'magenta',
|
|
813
|
-
padding: 1
|
|
814
|
-
},
|
|
815
|
-
create(Text, {bold: true, color: 'magenta'}, 'Projects'),
|
|
816
|
-
create(Box, {flexDirection: 'column', marginTop: 1}, ...projectRows)
|
|
817
|
-
),
|
|
818
|
-
create(
|
|
819
|
-
Box,
|
|
820
|
-
{
|
|
821
|
-
flexGrow: 1.3,
|
|
822
|
-
flexBasis: 0,
|
|
823
|
-
flexShrink: 1,
|
|
824
|
-
minWidth: DETAILS_MIN_WIDTH,
|
|
825
|
-
borderStyle: 'round',
|
|
826
|
-
borderColor: 'cyan',
|
|
827
|
-
padding: 1,
|
|
828
|
-
flexDirection: 'column'
|
|
829
|
-
},
|
|
830
|
-
create(Text, {bold: true, color: 'cyan'}, 'Details'),
|
|
831
|
-
...detailContent
|
|
832
|
-
)
|
|
518
|
+
create(Box, {marginTop: 1, flexDirection: 'row', alignItems: 'stretch', width: '100%', flexWrap: 'wrap'},
|
|
519
|
+
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)),
|
|
520
|
+
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)
|
|
833
521
|
),
|
|
834
|
-
create(
|
|
835
|
-
Box,
|
|
836
|
-
{
|
|
837
|
-
create(
|
|
838
|
-
|
|
839
|
-
{flexDirection: 'row', justifyContent: 'space-between'},
|
|
840
|
-
create(Text, {bold: true, color: 'yellow'}, `Output ${running ? 'ยท Running' : ''}`),
|
|
841
|
-
create(Text, {dimColor: true}, logOffset ? `Scrolled ${logOffset} lines` : 'Live log view')
|
|
842
|
-
),
|
|
843
|
-
create(
|
|
844
|
-
Box,
|
|
845
|
-
{
|
|
846
|
-
flexDirection: 'column',
|
|
847
|
-
borderStyle: 'round',
|
|
848
|
-
borderColor: 'yellow',
|
|
849
|
-
padding: 1,
|
|
850
|
-
minHeight: OUTPUT_WINDOW_HEIGHT,
|
|
851
|
-
maxHeight: OUTPUT_WINDOW_HEIGHT,
|
|
852
|
-
height: OUTPUT_WINDOW_HEIGHT,
|
|
853
|
-
overflow: 'hidden',
|
|
854
|
-
flexShrink: 0
|
|
855
|
-
},
|
|
856
|
-
...logNodes
|
|
857
|
-
),
|
|
858
|
-
create(
|
|
859
|
-
Box,
|
|
860
|
-
{marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'},
|
|
861
|
-
create(Text, {dimColor: true}, running ? 'Type to feed stdin; Enter submits, Ctrl+C aborts.' : 'Run a command or press ? for extra help.'),
|
|
862
|
-
create(Text, {dimColor: true}, `${toggleHint}, Shift+S toggles the structure guide`)
|
|
863
|
-
),
|
|
864
|
-
create(
|
|
865
|
-
Box,
|
|
866
|
-
{
|
|
867
|
-
marginTop: 1,
|
|
868
|
-
flexDirection: 'row',
|
|
869
|
-
borderStyle: 'round',
|
|
870
|
-
borderColor: running ? 'green' : 'gray',
|
|
871
|
-
paddingX: 1
|
|
872
|
-
},
|
|
873
|
-
create(Text, {bold: true, color: running ? 'green' : 'white'}, running ? ' Stdin buffer ' : ' Input ready '),
|
|
874
|
-
create(Box, {marginLeft: 1}, create(CursorText, {value: stdinBuffer || (running ? '' : 'Start a command to feed stdin'), cursorIndex: stdinCursor, active: running}))
|
|
875
|
-
)
|
|
522
|
+
create(Box, {marginTop: 1, flexDirection: 'column'},
|
|
523
|
+
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')),
|
|
524
|
+
create(Box, {flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1, minHeight: OUTPUT_WINDOW_HEIGHT, maxHeight: OUTPUT_WINDOW_HEIGHT, height: OUTPUT_WINDOW_HEIGHT, overflow: 'hidden'}, ...logNodes),
|
|
525
|
+
create(Box, {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {dimColor: true}, running ? 'Type to feed stdin; Enter: submit, Ctrl+C: abort.' : 'Run a command or press Shift+T to switch tasks.'), create(Text, {dimColor: true}, `${toggleHint}, Shift+S: Structure Guide`)),
|
|
526
|
+
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})))
|
|
876
527
|
),
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
528
|
+
showHelpCards && create(Box, {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between', flexWrap: 'wrap'}, ...helpCards.map((card, idx) => create(Box, {key: card.label, flexGrow: 1, flexBasis: 0, minWidth: HELP_CARD_MIN_WIDTH, marginRight: idx < 2 ? 1 : 0, marginBottom: 1, borderStyle: 'round', borderColor: card.color, padding: 1, flexDirection: 'column'}, create(Text, {color: card.color, bold: true, marginBottom: 1}, card.label), ...card.body.map((line, lidx) => create(Text, {key: lidx, dimColor: card.color === 'yellow'}, line))))),
|
|
529
|
+
showStructureGuide && create(Box, {flexDirection: 'column', borderStyle: 'round', borderColor: 'blue', marginTop: 1, padding: 1}, create(Text, {color: 'cyan', bold: true}, 'Structure guide ยท press Shift+S to hide'), ...SCHEMA_GUIDE.map(e => create(Text, {key: e.type, dimColor: true}, `โข ${e.icon} ${e.label}: ${e.files.join(', ')}`))),
|
|
530
|
+
showHelp && create(Box, {flexDirection: 'column', borderStyle: 'double', borderColor: 'cyan', marginTop: 1, padding: 1}, create(Text, {color: 'cyan', bold: true}, 'Help overlay'), create(Text, null, 'Shift+โ/โ scrolls logs; Shift+X clears; Shift+E exports; Shift+A Studio; Shift+T Tasks; Shift+D Detach.'))
|
|
880
531
|
);
|
|
881
532
|
}
|
|
882
533
|
|
|
@@ -886,17 +537,10 @@ function parseArgs() {
|
|
|
886
537
|
const tokens = process.argv.slice(2);
|
|
887
538
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
888
539
|
const token = tokens[i];
|
|
889
|
-
if ((token === '--dir' || token === '--path') && tokens[i + 1]) {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
args.mode = tokens[i + 1];
|
|
894
|
-
i += 1;
|
|
895
|
-
} else if (token === '--help' || token === '-h') {
|
|
896
|
-
args.help = true;
|
|
897
|
-
} else if (token === '--studio') {
|
|
898
|
-
args.view = 'studio';
|
|
899
|
-
}
|
|
540
|
+
if ((token === '--dir' || token === '--path') && tokens[i + 1]) { args.root = tokens[i + 1]; i += 1; }
|
|
541
|
+
else if (token === '--mode' && tokens[i + 1]) { args.mode = tokens[i + 1]; i += 1; }
|
|
542
|
+
else if (token === '--help' || token === '-h') args.help = true;
|
|
543
|
+
else if (token === '--studio') args.view = 'studio';
|
|
900
544
|
}
|
|
901
545
|
return args;
|
|
902
546
|
}
|
|
@@ -918,9 +562,11 @@ async function main() {
|
|
|
918
562
|
console.log(' โ / โ Move project focus');
|
|
919
563
|
console.log(' Enter Toggle detail view for selected project');
|
|
920
564
|
console.log(' Shift+A Switch to Omni-Studio (Environment Health)');
|
|
921
|
-
console.log(' Shift+
|
|
565
|
+
console.log(' Shift+T Open Orbit Task Manager (Manage background processes)');
|
|
566
|
+
console.log(' Shift+D Detach from active task (Keep it running in background)');
|
|
567
|
+
console.log(' Shift+X Clear active task output log');
|
|
922
568
|
console.log(' Shift+E Export current logs to a .txt file');
|
|
923
|
-
console.log(' Shift+โ / โ Scroll the output logs
|
|
569
|
+
console.log(' Shift+โ / โ Scroll the output logs');
|
|
924
570
|
console.log(' Shift+Q Quit application');
|
|
925
571
|
console.log('');
|
|
926
572
|
console.log(kleur.bold('Execution shortcuts:'));
|
|
@@ -936,16 +582,11 @@ async function main() {
|
|
|
936
582
|
if (args.mode === 'test') {
|
|
937
583
|
const projects = await discoverProjects(rootPath);
|
|
938
584
|
console.log(`Detected ${projects.length} project(s) under ${rootPath}`);
|
|
939
|
-
projects.forEach((project) => {
|
|
940
|
-
console.log(` โข [${project.type}] ${project.name} (${project.path})`);
|
|
941
|
-
});
|
|
585
|
+
projects.forEach((project) => { console.log(` โข [${project.type}] ${project.name} (${project.path})`); });
|
|
942
586
|
return;
|
|
943
587
|
}
|
|
944
588
|
|
|
945
589
|
render(create(Compass, {rootPath, initialView: args.view || 'navigator'}));
|
|
946
590
|
}
|
|
947
591
|
|
|
948
|
-
main().catch((error) => {
|
|
949
|
-
console.error(error);
|
|
950
|
-
process.exit(1);
|
|
951
|
-
});
|
|
592
|
+
main().catch((error) => { console.error(error); process.exit(1); });
|