@fcannizzaro/exocommand 1.0.10 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -38
- package/dist/cli.js +29 -0
- package/dist/cli.test.js +45 -0
- package/dist/config.js +8 -1
- package/dist/config.test.js +31 -1
- package/dist/index.js +292 -47
- package/dist/logger.js +29 -16
- package/dist/registry.js +80 -0
- package/dist/registry.test.js +107 -0
- package/dist/server.js +20 -10
- package/dist/task.js +14 -4
- package/dist/tui.js +517 -0
- package/package.json +4 -2
package/dist/tui.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { createCliRenderer, Box, Text, TextAttributes, t, bold, dim, fg, ScrollBoxRenderable, BoxRenderable, TextRenderable, Renderable, } from "@opentui/core";
|
|
2
|
+
// Tokyo Night Dark palette
|
|
3
|
+
const TN_BG = "#1a1b26";
|
|
4
|
+
const TN_BG_DARK = "#16161e";
|
|
5
|
+
const TN_BG_HIGHLIGHT = "#292e42";
|
|
6
|
+
const TN_FG_DARK = "#565f89";
|
|
7
|
+
const TN_CYAN = "#7dcfff";
|
|
8
|
+
const TN_GREEN = "#9ece6a";
|
|
9
|
+
const TN_YELLOW = "#e0af68";
|
|
10
|
+
const TN_RED = "#f7768e";
|
|
11
|
+
const TN_BLUE = "#7aa2f7";
|
|
12
|
+
const TN_MAGENTA = "#bb9af7";
|
|
13
|
+
const TN_ORANGE = "#ff9e64";
|
|
14
|
+
const SPINNER_FRAMES = [
|
|
15
|
+
"\u28CB",
|
|
16
|
+
"\u28D9",
|
|
17
|
+
"\u28F9",
|
|
18
|
+
"\u28F8",
|
|
19
|
+
"\u28FC",
|
|
20
|
+
"\u28F4",
|
|
21
|
+
"\u28E6",
|
|
22
|
+
"\u28E7",
|
|
23
|
+
"\u28C7",
|
|
24
|
+
"\u28CF",
|
|
25
|
+
];
|
|
26
|
+
function formatTime(date) {
|
|
27
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
28
|
+
const m = String(date.getMinutes()).padStart(2, "0");
|
|
29
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
30
|
+
return `${h}:${m}:${s}`;
|
|
31
|
+
}
|
|
32
|
+
function getStatusStyle(status, frame) {
|
|
33
|
+
switch (status) {
|
|
34
|
+
case "running":
|
|
35
|
+
return { color: TN_YELLOW, label: "running", symbol: frame };
|
|
36
|
+
case "success":
|
|
37
|
+
return { color: TN_GREEN, label: "success", symbol: "\u2713" };
|
|
38
|
+
case "error":
|
|
39
|
+
return { color: TN_RED, label: "error", symbol: "\u2717" };
|
|
40
|
+
case "cancelled":
|
|
41
|
+
return { color: TN_ORANGE, label: "cancelled", symbol: "\u25CB" };
|
|
42
|
+
case "timeout":
|
|
43
|
+
return { color: TN_ORANGE, label: "timeout", symbol: "\u25F4" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
class PillTabBar {
|
|
47
|
+
renderer;
|
|
48
|
+
container;
|
|
49
|
+
options = [];
|
|
50
|
+
selectedIndex = -1;
|
|
51
|
+
wrapSelection;
|
|
52
|
+
selectionCallback = null;
|
|
53
|
+
tabElements = [];
|
|
54
|
+
keyListener = null;
|
|
55
|
+
constructor(renderer, opts) {
|
|
56
|
+
this.renderer = renderer;
|
|
57
|
+
this.wrapSelection = opts.wrapSelection ?? true;
|
|
58
|
+
this.container = new BoxRenderable(renderer, {
|
|
59
|
+
id: "project-tabs",
|
|
60
|
+
flexDirection: "row",
|
|
61
|
+
width: "100%",
|
|
62
|
+
flexShrink: 0,
|
|
63
|
+
gap: 1,
|
|
64
|
+
alignItems: "center",
|
|
65
|
+
paddingLeft: 1,
|
|
66
|
+
paddingRight: 1,
|
|
67
|
+
});
|
|
68
|
+
this.container.visible = false;
|
|
69
|
+
// Global key listener — this is the only interactive element in the TUI
|
|
70
|
+
this.keyListener = (key) => this.handleKey(key);
|
|
71
|
+
renderer.keyInput.on("keypress", this.keyListener);
|
|
72
|
+
}
|
|
73
|
+
handleKey(key) {
|
|
74
|
+
if (!this.container.visible || this.options.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
if (key.name === "left" || key.name === "[") {
|
|
77
|
+
this.moveSelection(-1);
|
|
78
|
+
}
|
|
79
|
+
else if (key.name === "right" || key.name === "]") {
|
|
80
|
+
this.moveSelection(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
moveSelection(delta) {
|
|
84
|
+
if (this.options.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
let newIndex = this.selectedIndex + delta;
|
|
87
|
+
if (this.wrapSelection) {
|
|
88
|
+
newIndex =
|
|
89
|
+
((newIndex % this.options.length) + this.options.length) %
|
|
90
|
+
this.options.length;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
newIndex = Math.max(0, Math.min(this.options.length - 1, newIndex));
|
|
94
|
+
}
|
|
95
|
+
this.selectIndex(newIndex);
|
|
96
|
+
}
|
|
97
|
+
selectIndex(index) {
|
|
98
|
+
if (index === this.selectedIndex)
|
|
99
|
+
return;
|
|
100
|
+
this.selectedIndex = index;
|
|
101
|
+
this.rebuild();
|
|
102
|
+
this.selectionCallback?.(index, this.options[index]);
|
|
103
|
+
}
|
|
104
|
+
rebuild() {
|
|
105
|
+
for (const el of this.tabElements) {
|
|
106
|
+
this.container.remove(el.id);
|
|
107
|
+
el.destroy();
|
|
108
|
+
}
|
|
109
|
+
this.tabElements = [];
|
|
110
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
111
|
+
const option = this.options[i];
|
|
112
|
+
const isSelected = i === this.selectedIndex;
|
|
113
|
+
if (isSelected) {
|
|
114
|
+
// Solid background block, no border
|
|
115
|
+
const text = new TextRenderable(this.renderer, {
|
|
116
|
+
id: `tab-text-${i}`,
|
|
117
|
+
content: t ` ${bold(fg(TN_BG)(option.name))} `,
|
|
118
|
+
selectable: false,
|
|
119
|
+
});
|
|
120
|
+
const pill = new BoxRenderable(this.renderer, {
|
|
121
|
+
id: `tab-pill-${i}`,
|
|
122
|
+
backgroundColor: TN_CYAN,
|
|
123
|
+
paddingLeft: 1,
|
|
124
|
+
paddingRight: 1,
|
|
125
|
+
onMouseDown: () => this.selectIndex(i),
|
|
126
|
+
});
|
|
127
|
+
pill.add(text);
|
|
128
|
+
this.container.add(pill);
|
|
129
|
+
this.tabElements.push(pill);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Plain text label with padding
|
|
133
|
+
const label = new TextRenderable(this.renderer, {
|
|
134
|
+
id: `tab-label-${i}`,
|
|
135
|
+
content: t ` ${fg(TN_FG_DARK)(option.name)} `,
|
|
136
|
+
selectable: false,
|
|
137
|
+
onMouseDown: () => this.selectIndex(i),
|
|
138
|
+
});
|
|
139
|
+
this.container.add(label);
|
|
140
|
+
this.tabElements.push(label);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
get visible() {
|
|
145
|
+
return this.container.visible;
|
|
146
|
+
}
|
|
147
|
+
set visible(value) {
|
|
148
|
+
this.container.visible = value;
|
|
149
|
+
}
|
|
150
|
+
get renderable() {
|
|
151
|
+
return this.container;
|
|
152
|
+
}
|
|
153
|
+
setOptions(options) {
|
|
154
|
+
this.options = options;
|
|
155
|
+
if (this.selectedIndex >= options.length) {
|
|
156
|
+
this.selectedIndex = Math.max(0, options.length - 1);
|
|
157
|
+
}
|
|
158
|
+
this.rebuild();
|
|
159
|
+
}
|
|
160
|
+
getSelectedOption() {
|
|
161
|
+
if (this.selectedIndex < 0 || this.selectedIndex >= this.options.length) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return this.options[this.selectedIndex];
|
|
165
|
+
}
|
|
166
|
+
setSelectedIndex(index) {
|
|
167
|
+
if (index < 0 || index >= this.options.length)
|
|
168
|
+
return;
|
|
169
|
+
if (index === this.selectedIndex)
|
|
170
|
+
return;
|
|
171
|
+
this.selectedIndex = index;
|
|
172
|
+
this.rebuild();
|
|
173
|
+
this.selectionCallback?.(index, this.options[index]);
|
|
174
|
+
}
|
|
175
|
+
onChanged(callback) {
|
|
176
|
+
this.selectionCallback = callback;
|
|
177
|
+
}
|
|
178
|
+
destroy() {
|
|
179
|
+
if (this.keyListener) {
|
|
180
|
+
this.renderer.keyInput.off("keypress", this.keyListener);
|
|
181
|
+
this.keyListener = null;
|
|
182
|
+
}
|
|
183
|
+
for (const el of this.tabElements) {
|
|
184
|
+
el.destroy();
|
|
185
|
+
}
|
|
186
|
+
this.tabElements = [];
|
|
187
|
+
this.container.destroy();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export async function createTui(version) {
|
|
191
|
+
const renderer = await createCliRenderer({
|
|
192
|
+
exitOnCtrlC: true,
|
|
193
|
+
useMouse: true,
|
|
194
|
+
targetFps: 30,
|
|
195
|
+
onDestroy: () => {
|
|
196
|
+
process.exit();
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
// Per-project panels and lookup maps
|
|
200
|
+
const panels = new Map();
|
|
201
|
+
const cardToProject = new Map();
|
|
202
|
+
let totalRunningCount = 0;
|
|
203
|
+
let spinnerIndex = 0;
|
|
204
|
+
let spinnerTimer = null;
|
|
205
|
+
// -- Toolbar --
|
|
206
|
+
const toolbar = Box({
|
|
207
|
+
flexDirection: "row",
|
|
208
|
+
width: "100%",
|
|
209
|
+
flexShrink: 0,
|
|
210
|
+
alignItems: "center",
|
|
211
|
+
justifyContent: "space-between",
|
|
212
|
+
backgroundColor: TN_BG_DARK,
|
|
213
|
+
borderStyle: "rounded",
|
|
214
|
+
borderColor: TN_BG_HIGHLIGHT,
|
|
215
|
+
paddingLeft: 2,
|
|
216
|
+
paddingRight: 2,
|
|
217
|
+
}, Box({ flexDirection: "row", gap: 0 }, Text({
|
|
218
|
+
content: "@fcannizzaro/",
|
|
219
|
+
fg: TN_FG_DARK,
|
|
220
|
+
}), Text({
|
|
221
|
+
content: "exocommand",
|
|
222
|
+
fg: TN_CYAN,
|
|
223
|
+
attributes: TextAttributes.BOLD,
|
|
224
|
+
})), Text({
|
|
225
|
+
content: `v${version}`,
|
|
226
|
+
fg: TN_FG_DARK,
|
|
227
|
+
attributes: TextAttributes.DIM,
|
|
228
|
+
}));
|
|
229
|
+
// -- PillTabBar for project switching --
|
|
230
|
+
const tabBar = new PillTabBar(renderer, { wrapSelection: true });
|
|
231
|
+
// Show the correct panel when the user navigates tabs
|
|
232
|
+
tabBar.onChanged((_index, option) => {
|
|
233
|
+
for (const [key, panel] of panels) {
|
|
234
|
+
panel.scrollBox.visible = option.value === key;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
// -- Panel container (holds per-project ScrollBoxes) --
|
|
238
|
+
const panelContainer = new BoxRenderable(renderer, {
|
|
239
|
+
id: "panel-container",
|
|
240
|
+
flexGrow: 1,
|
|
241
|
+
width: "100%",
|
|
242
|
+
flexDirection: "column",
|
|
243
|
+
});
|
|
244
|
+
// -- Status bar --
|
|
245
|
+
const statusMessage = new TextRenderable(renderer, {
|
|
246
|
+
id: "status-message",
|
|
247
|
+
content: "",
|
|
248
|
+
fg: TN_FG_DARK,
|
|
249
|
+
});
|
|
250
|
+
const statusPort = new TextRenderable(renderer, {
|
|
251
|
+
id: "status-port",
|
|
252
|
+
content: "",
|
|
253
|
+
fg: TN_FG_DARK,
|
|
254
|
+
});
|
|
255
|
+
const statusBar = Box({
|
|
256
|
+
width: "100%",
|
|
257
|
+
height: 3,
|
|
258
|
+
backgroundColor: TN_BG_DARK,
|
|
259
|
+
borderStyle: "rounded",
|
|
260
|
+
borderColor: TN_BG_HIGHLIGHT,
|
|
261
|
+
flexDirection: "row",
|
|
262
|
+
alignItems: "center",
|
|
263
|
+
justifyContent: "space-between",
|
|
264
|
+
paddingLeft: 1,
|
|
265
|
+
paddingRight: 1,
|
|
266
|
+
}, statusMessage, statusPort);
|
|
267
|
+
// -- Assemble tree --
|
|
268
|
+
renderer.root.add(Box({
|
|
269
|
+
flexDirection: "column",
|
|
270
|
+
width: "100%",
|
|
271
|
+
height: "100%",
|
|
272
|
+
backgroundColor: TN_BG,
|
|
273
|
+
}, toolbar, tabBar.renderable, panelContainer, statusBar));
|
|
274
|
+
// -- Tab options helper --
|
|
275
|
+
function rebuildTabOptions() {
|
|
276
|
+
const options = [...panels.values()].map((p) => ({
|
|
277
|
+
name: p.label,
|
|
278
|
+
value: p.projectKey,
|
|
279
|
+
}));
|
|
280
|
+
tabBar.setOptions(options);
|
|
281
|
+
}
|
|
282
|
+
// -- Show the panel for the currently selected tab --
|
|
283
|
+
function syncPanelVisibility() {
|
|
284
|
+
const selected = tabBar.getSelectedOption();
|
|
285
|
+
for (const [key, panel] of panels) {
|
|
286
|
+
panel.scrollBox.visible = selected?.value === key;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// -- Spinner control --
|
|
290
|
+
function startSpinner() {
|
|
291
|
+
if (spinnerTimer)
|
|
292
|
+
return;
|
|
293
|
+
renderer.requestLive();
|
|
294
|
+
spinnerTimer = setInterval(() => {
|
|
295
|
+
spinnerIndex = (spinnerIndex + 1) % SPINNER_FRAMES.length;
|
|
296
|
+
const frame = SPINNER_FRAMES[spinnerIndex];
|
|
297
|
+
for (const panel of panels.values()) {
|
|
298
|
+
for (const card of panel.cards.values()) {
|
|
299
|
+
if (card.status === "running") {
|
|
300
|
+
card.statusText.content = t `${fg(TN_YELLOW)(`${frame} running`)}`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}, 80);
|
|
305
|
+
}
|
|
306
|
+
function stopSpinner() {
|
|
307
|
+
if (!spinnerTimer)
|
|
308
|
+
return;
|
|
309
|
+
clearInterval(spinnerTimer);
|
|
310
|
+
spinnerTimer = null;
|
|
311
|
+
renderer.dropLive();
|
|
312
|
+
}
|
|
313
|
+
// -- Card factory --
|
|
314
|
+
function createCard(id, commandName, agentId, startedAt) {
|
|
315
|
+
const statusText = new TextRenderable(renderer, {
|
|
316
|
+
id: `status-text-${id}`,
|
|
317
|
+
content: t `${fg(TN_YELLOW)(`${SPINNER_FRAMES[0]} running`)}`,
|
|
318
|
+
});
|
|
319
|
+
const statusBadge = new BoxRenderable(renderer, {
|
|
320
|
+
id: `status-badge-${id}`,
|
|
321
|
+
borderStyle: "rounded",
|
|
322
|
+
borderColor: TN_YELLOW,
|
|
323
|
+
paddingLeft: 1,
|
|
324
|
+
paddingRight: 1,
|
|
325
|
+
height: 3,
|
|
326
|
+
});
|
|
327
|
+
statusBadge.add(statusText);
|
|
328
|
+
const cardBox = new BoxRenderable(renderer, {
|
|
329
|
+
id: `card-${id}`,
|
|
330
|
+
borderStyle: "rounded",
|
|
331
|
+
borderColor: TN_FG_DARK,
|
|
332
|
+
flexDirection: "row",
|
|
333
|
+
gap: 1,
|
|
334
|
+
width: "100%",
|
|
335
|
+
paddingLeft: 1,
|
|
336
|
+
paddingRight: 1,
|
|
337
|
+
title: ` ${formatTime(startedAt)} `,
|
|
338
|
+
titleAlignment: "left",
|
|
339
|
+
});
|
|
340
|
+
// cmd badge
|
|
341
|
+
const cmdBadge = Box({
|
|
342
|
+
borderStyle: "rounded",
|
|
343
|
+
borderColor: TN_FG_DARK,
|
|
344
|
+
paddingLeft: 1,
|
|
345
|
+
paddingRight: 1,
|
|
346
|
+
height: 3,
|
|
347
|
+
}, Text({ content: t `${dim("cmd")} ${fg(TN_MAGENTA)(commandName)}` }));
|
|
348
|
+
// agent badge
|
|
349
|
+
const agentBadge = Box({
|
|
350
|
+
borderStyle: "rounded",
|
|
351
|
+
borderColor: TN_FG_DARK,
|
|
352
|
+
paddingLeft: 1,
|
|
353
|
+
paddingRight: 1,
|
|
354
|
+
height: 3,
|
|
355
|
+
}, Text({ content: t `${dim("agent")} ${fg(TN_CYAN)(String(agentId))}` }));
|
|
356
|
+
cardBox.add(agentBadge);
|
|
357
|
+
cardBox.add(cmdBadge);
|
|
358
|
+
cardBox.add(Box({ flexGrow: 1 }));
|
|
359
|
+
cardBox.add(statusBadge);
|
|
360
|
+
return {
|
|
361
|
+
id,
|
|
362
|
+
commandName,
|
|
363
|
+
agentId,
|
|
364
|
+
startedAt,
|
|
365
|
+
status: "running",
|
|
366
|
+
cardBox,
|
|
367
|
+
statusText,
|
|
368
|
+
statusBadge,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// -- TuiManager implementation --
|
|
372
|
+
return {
|
|
373
|
+
addProject(projectKey, label) {
|
|
374
|
+
if (panels.has(projectKey))
|
|
375
|
+
return;
|
|
376
|
+
const scrollBox = new ScrollBoxRenderable(renderer, {
|
|
377
|
+
id: `executions-${projectKey}`,
|
|
378
|
+
flexGrow: 1,
|
|
379
|
+
width: "100%",
|
|
380
|
+
stickyScroll: true,
|
|
381
|
+
stickyStart: "bottom",
|
|
382
|
+
viewportCulling: true,
|
|
383
|
+
contentOptions: {
|
|
384
|
+
flexDirection: "column",
|
|
385
|
+
gap: 0,
|
|
386
|
+
padding: 1,
|
|
387
|
+
},
|
|
388
|
+
scrollbarOptions: {
|
|
389
|
+
trackOptions: {
|
|
390
|
+
foregroundColor: TN_BLUE,
|
|
391
|
+
backgroundColor: TN_BG_HIGHLIGHT,
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
const emptyText = new BoxRenderable(renderer, {
|
|
396
|
+
id: `empty-${projectKey}`,
|
|
397
|
+
flexGrow: 1,
|
|
398
|
+
width: "100%",
|
|
399
|
+
alignItems: "center",
|
|
400
|
+
justifyContent: "center",
|
|
401
|
+
});
|
|
402
|
+
emptyText.add(new TextRenderable(renderer, {
|
|
403
|
+
id: `empty-text-${projectKey}`,
|
|
404
|
+
content: t `${dim("No commands executed yet")}`,
|
|
405
|
+
}));
|
|
406
|
+
scrollBox.add(emptyText);
|
|
407
|
+
scrollBox.visible = false;
|
|
408
|
+
panelContainer.add(scrollBox);
|
|
409
|
+
panels.set(projectKey, {
|
|
410
|
+
projectKey,
|
|
411
|
+
label,
|
|
412
|
+
scrollBox,
|
|
413
|
+
cards: new Map(),
|
|
414
|
+
runningCount: 0,
|
|
415
|
+
emptyText,
|
|
416
|
+
});
|
|
417
|
+
rebuildTabOptions();
|
|
418
|
+
// First project: show tabs and select it
|
|
419
|
+
if (panels.size === 1) {
|
|
420
|
+
tabBar.visible = true;
|
|
421
|
+
tabBar.setSelectedIndex(0);
|
|
422
|
+
scrollBox.visible = true;
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
removeProject(projectKey) {
|
|
426
|
+
const panel = panels.get(projectKey);
|
|
427
|
+
if (!panel)
|
|
428
|
+
return;
|
|
429
|
+
const selected = tabBar.getSelectedOption();
|
|
430
|
+
const wasSelected = selected?.value === projectKey;
|
|
431
|
+
// Remove scroll box from container
|
|
432
|
+
panelContainer.remove(panel.scrollBox.id);
|
|
433
|
+
// Clean up card-to-project mappings
|
|
434
|
+
for (const cardId of panel.cards.keys()) {
|
|
435
|
+
cardToProject.delete(cardId);
|
|
436
|
+
}
|
|
437
|
+
totalRunningCount -= panel.runningCount;
|
|
438
|
+
panels.delete(projectKey);
|
|
439
|
+
if (panels.size === 0) {
|
|
440
|
+
tabBar.visible = false;
|
|
441
|
+
if (totalRunningCount <= 0) {
|
|
442
|
+
totalRunningCount = 0;
|
|
443
|
+
stopSpinner();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
rebuildTabOptions();
|
|
448
|
+
if (wasSelected) {
|
|
449
|
+
tabBar.setSelectedIndex(0);
|
|
450
|
+
syncPanelVisibility();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
addExecution(id, commandName, agentId, projectKey) {
|
|
455
|
+
const panel = panels.get(projectKey);
|
|
456
|
+
if (!panel)
|
|
457
|
+
return;
|
|
458
|
+
// Hide empty state placeholder on first execution
|
|
459
|
+
if (panel.cards.size === 0) {
|
|
460
|
+
panel.emptyText.visible = false;
|
|
461
|
+
}
|
|
462
|
+
const startedAt = new Date();
|
|
463
|
+
const card = createCard(id, commandName, agentId, startedAt);
|
|
464
|
+
panel.cards.set(id, card);
|
|
465
|
+
cardToProject.set(id, projectKey);
|
|
466
|
+
panel.scrollBox.add(card.cardBox);
|
|
467
|
+
panel.runningCount++;
|
|
468
|
+
totalRunningCount++;
|
|
469
|
+
startSpinner();
|
|
470
|
+
},
|
|
471
|
+
updateExecution(id, status) {
|
|
472
|
+
const projectKey = cardToProject.get(id);
|
|
473
|
+
if (!projectKey)
|
|
474
|
+
return;
|
|
475
|
+
const panel = panels.get(projectKey);
|
|
476
|
+
if (!panel)
|
|
477
|
+
return;
|
|
478
|
+
const card = panel.cards.get(id);
|
|
479
|
+
if (!card)
|
|
480
|
+
return;
|
|
481
|
+
if (card.status !== "running")
|
|
482
|
+
return; // already in terminal state
|
|
483
|
+
card.status = status;
|
|
484
|
+
const style = getStatusStyle(status, SPINNER_FRAMES[spinnerIndex]);
|
|
485
|
+
card.statusText.content = t `${fg(style.color)(`${style.symbol} ${style.label}`)}`;
|
|
486
|
+
card.statusBadge.borderColor = style.color;
|
|
487
|
+
// update running count for terminal states
|
|
488
|
+
if (status !== "running") {
|
|
489
|
+
panel.runningCount--;
|
|
490
|
+
totalRunningCount--;
|
|
491
|
+
if (totalRunningCount <= 0) {
|
|
492
|
+
totalRunningCount = 0;
|
|
493
|
+
stopSpinner();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
logMessage(level, event, message) {
|
|
498
|
+
const colorMap = {
|
|
499
|
+
info: TN_CYAN,
|
|
500
|
+
success: TN_GREEN,
|
|
501
|
+
warn: TN_YELLOW,
|
|
502
|
+
error: TN_RED,
|
|
503
|
+
};
|
|
504
|
+
const color = colorMap[level] ?? TN_CYAN;
|
|
505
|
+
const ts = formatTime(new Date());
|
|
506
|
+
statusMessage.content = t `${dim(ts)} ${bold(fg(color)(`[${event}]`))} ${message}`;
|
|
507
|
+
},
|
|
508
|
+
setPort(port) {
|
|
509
|
+
statusPort.content = t `${fg(TN_FG_DARK)(`:${port}`)}`;
|
|
510
|
+
},
|
|
511
|
+
destroy() {
|
|
512
|
+
stopSpinner();
|
|
513
|
+
tabBar.destroy();
|
|
514
|
+
renderer.destroy();
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fcannizzaro/exocommand",
|
|
3
3
|
"module": "index.ts",
|
|
4
|
-
"description": "An MCP server that exposes user-defined shell commands as tools for AI coding assistants",
|
|
5
|
-
"version": "1.0
|
|
4
|
+
"description": "An MCP server (with TUI) that exposes user-defined shell commands as tools for AI coding assistants",
|
|
5
|
+
"version": "1.1.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"homepage": "https://github.com/fcannizzaro/exocommand",
|
|
@@ -45,11 +45,13 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@hono/node-server": "^1.19.9",
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
48
|
+
"@opentui/core": "^0.1.81",
|
|
48
49
|
"yaml": "^2.8.2"
|
|
49
50
|
},
|
|
50
51
|
"keywords": [
|
|
51
52
|
"exocommand",
|
|
52
53
|
"mcp",
|
|
54
|
+
"tui",
|
|
53
55
|
"agentic-programming",
|
|
54
56
|
"mcp",
|
|
55
57
|
"model-context-protocol"
|