trackops 1.0.1 → 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 +326 -270
- package/bin/trackops.js +102 -70
- package/lib/config.js +260 -35
- package/lib/control.js +517 -475
- package/lib/env.js +227 -0
- package/lib/i18n.js +61 -53
- package/lib/init.js +135 -46
- package/lib/locale.js +63 -0
- package/lib/opera-bootstrap.js +523 -0
- package/lib/opera.js +319 -170
- package/lib/registry.js +27 -13
- package/lib/release.js +56 -0
- package/lib/resources.js +42 -0
- package/lib/server.js +907 -554
- package/lib/skills.js +148 -124
- package/lib/workspace.js +260 -0
- package/locales/en.json +331 -139
- package/locales/es.json +331 -139
- package/package.json +7 -9
- package/scripts/skills-marketplace-smoke.js +124 -0
- package/scripts/smoke-tests.js +445 -0
- package/scripts/sync-skill-version.js +21 -0
- package/scripts/validate-skill.js +88 -0
- package/skills/trackops/SKILL.md +64 -0
- package/skills/trackops/agents/openai.yaml +3 -0
- package/skills/trackops/references/activation.md +39 -0
- package/skills/trackops/references/troubleshooting.md +34 -0
- package/skills/trackops/references/workflow.md +20 -0
- package/skills/trackops/scripts/bootstrap-trackops.js +201 -0
- package/skills/trackops/skill.json +29 -0
- package/templates/opera/en/agent.md +26 -0
- package/templates/opera/en/genesis.md +79 -0
- package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
- package/templates/opera/en/references/opera-cycle.md +62 -0
- package/templates/opera/en/registry.md +28 -0
- package/templates/opera/en/router.md +39 -0
- package/templates/opera/genesis.md +79 -94
- package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
- package/templates/skills/commiter/locales/en/SKILL.md +11 -0
- package/templates/skills/project-starter-skill/locales/en/SKILL.md +24 -0
- package/ui/css/panels.css +956 -953
- package/ui/index.html +1 -1
- package/ui/js/api.js +211 -194
- package/ui/js/app.js +200 -199
- package/ui/js/i18n.js +14 -0
- package/ui/js/onboarding.js +439 -437
- package/ui/js/state.js +130 -129
- package/ui/js/utils.js +175 -172
- package/ui/js/views/board.js +255 -254
- package/ui/js/views/execution.js +256 -256
- package/ui/js/views/insights.js +340 -339
- package/ui/js/views/overview.js +365 -364
- package/ui/js/views/settings.js +340 -202
- package/ui/js/views/sidebar.js +131 -132
- package/ui/js/views/skills.js +163 -162
- package/ui/js/views/tasks.js +406 -405
- package/ui/js/views/topbar.js +239 -183
package/ui/js/views/execution.js
CHANGED
|
@@ -1,256 +1,256 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* execution.js — Consola integrada + sesiones de comandos
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { icon } from '../icons.js';
|
|
6
|
-
import * as state from '../state.js';
|
|
7
|
-
import * as api from '../api.js';
|
|
8
|
-
import { flash } from './flash.js';
|
|
9
|
-
import { esc, formatDate } from '../utils.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return `
|
|
25
|
-
<div class="view-enter">
|
|
26
|
-
<div class="section-header">
|
|
27
|
-
<div class="section-header-left">
|
|
28
|
-
<p class="eyebrow"
|
|
29
|
-
<h2
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
|
|
33
|
-
<div style="display:grid;grid-template-columns:300px 1fr;gap:var(--space-4);align-items:start">
|
|
34
|
-
|
|
35
|
-
<!-- Panel izquierdo: input + sesiones -->
|
|
36
|
-
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
|
37
|
-
|
|
38
|
-
<!-- Command input -->
|
|
39
|
-
<div class="panel">
|
|
40
|
-
<div class="panel-header">
|
|
41
|
-
<p class="panel-title"
|
|
42
|
-
</div>
|
|
43
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
44
|
-
<div class="field">
|
|
45
|
-
<label for="cmd-input"
|
|
46
|
-
<textarea id="cmd-input" rows="3"
|
|
47
|
-
placeholder="npx trackops status"
|
|
48
|
-
aria-label="
|
|
49
|
-
style="font-family:var(--font-mono);font-size:var(--text-sm)"></textarea>
|
|
50
|
-
</div>
|
|
51
|
-
<button class="btn btn-primary" type="button" id="run-cmd-btn" aria-label="
|
|
52
|
-
${icon('execution', 15)}
|
|
53
|
-
</button>
|
|
54
|
-
</div>
|
|
55
|
-
<div class="panel-footer">
|
|
56
|
-
<p class="label-sm" style="margin-bottom:var(--space-2)"
|
|
57
|
-
<div class="preset-strip">
|
|
58
|
-
${
|
|
59
|
-
<button class="chip" type="button" data-quick="${esc(c.cmd)}"
|
|
60
|
-
aria-label="
|
|
61
|
-
${esc(c.label)}
|
|
62
|
-
</button>
|
|
63
|
-
`).join('')}
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<!-- Lista de sesiones -->
|
|
69
|
-
<div class="panel">
|
|
70
|
-
<div class="panel-header">
|
|
71
|
-
<p class="panel-title"
|
|
72
|
-
<span class="badge badge-muted">${sessions.length}</span>
|
|
73
|
-
</div>
|
|
74
|
-
<div class="panel-body" style="max-height:360px;overflow-y:auto">
|
|
75
|
-
${sessions.length === 0
|
|
76
|
-
? `<div class="empty-state"
|
|
77
|
-
: `<div class="stack stack-sm">
|
|
78
|
-
${sessions.slice().reverse().map(s => `
|
|
79
|
-
<div class="session-pill ${s.id === state.get('selectedSessionId') ? 'is-selected' : ''}"
|
|
80
|
-
data-session-id="${esc(s.id)}"
|
|
81
|
-
role="button" tabindex="0"
|
|
82
|
-
aria-selected="${s.id === state.get('selectedSessionId')}"
|
|
83
|
-
aria-label="${esc(s.command)}, ${esc(s.status)}">
|
|
84
|
-
<span class="session-pill-cmd">${esc(s.command)}</span>
|
|
85
|
-
<span class="badge session-pill-status ${_sessionBadgeClass(s.status)}">${esc(s.status)}</span>
|
|
86
|
-
</div>
|
|
87
|
-
`).join('')}
|
|
88
|
-
</div>`
|
|
89
|
-
}
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
<!-- Terminal output -->
|
|
96
|
-
<div class="terminal-surface" style="min-height:600px;display:flex;flex-direction:column">
|
|
97
|
-
<div class="terminal-header">
|
|
98
|
-
<div class="terminal-dots" aria-hidden="true">
|
|
99
|
-
<span></span><span></span><span></span>
|
|
100
|
-
</div>
|
|
101
|
-
<p class="terminal-title">${selected ? esc(selected.command) : 'ops@terminal —
|
|
102
|
-
<div style="display:flex;gap:var(--space-2)">
|
|
103
|
-
${selected && selected.status === 'running' ? `
|
|
104
|
-
<button class="btn btn-ghost btn-sm" id="kill-session-btn" type="button" aria-label="
|
|
105
|
-
${icon('stop', 13)}
|
|
106
|
-
</button>
|
|
107
|
-
` : ''}
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
<pre class="terminal-output" id="terminal-output" aria-label="
|
|
111
|
-
${selected ? `<div class="panel-footer" style="display:flex;justify-content:space-between;align-items:center;font-family:var(--font-mono);font-size:var(--text-xs)">
|
|
112
|
-
<span class="text-muted"
|
|
113
|
-
<span class="badge ${_sessionBadgeClass(selected.status)}">${esc(selected.status)}${selected.exitCode != null ? ` (${selected.exitCode})` : ''}</span>
|
|
114
|
-
</div>` : ''}
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function _sessionBadgeClass(status) {
|
|
123
|
-
const map = {
|
|
124
|
-
running: 'badge-info',
|
|
125
|
-
completed: 'badge-success',
|
|
126
|
-
failed: 'badge-danger',
|
|
127
|
-
terminated:'badge-warning',
|
|
128
|
-
};
|
|
129
|
-
return map[status] || 'badge-muted';
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function bindEvents() {
|
|
133
|
-
// Run command button
|
|
134
|
-
document.getElementById('run-cmd-btn')?.addEventListener('click', _runCommand);
|
|
135
|
-
|
|
136
|
-
// Ctrl+Enter en el textarea
|
|
137
|
-
document.getElementById('cmd-input')?.addEventListener('keydown', e => {
|
|
138
|
-
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
139
|
-
e.preventDefault();
|
|
140
|
-
_runCommand();
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Quick commands
|
|
145
|
-
document.querySelectorAll('[data-quick]').forEach(btn => {
|
|
146
|
-
btn.addEventListener('click', () => {
|
|
147
|
-
const input = document.getElementById('cmd-input');
|
|
148
|
-
if (input) {
|
|
149
|
-
input.value = btn.dataset.quick;
|
|
150
|
-
input.focus();
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Seleccionar sesión
|
|
156
|
-
document.querySelectorAll('[data-session-id]').forEach(pill => {
|
|
157
|
-
pill.addEventListener('click', () => {
|
|
158
|
-
const id = pill.dataset.sessionId;
|
|
159
|
-
state.update('selectedSessionId', id);
|
|
160
|
-
import('../router.js').then(r => r.refresh());
|
|
161
|
-
});
|
|
162
|
-
pill.addEventListener('keydown', e => {
|
|
163
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
164
|
-
e.preventDefault();
|
|
165
|
-
pill.click();
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function _runCommand() {
|
|
172
|
-
const input = document.getElementById('cmd-input');
|
|
173
|
-
const cmd = input?.value.trim();
|
|
174
|
-
if (!cmd) return;
|
|
175
|
-
|
|
176
|
-
const btn = document.getElementById('run-cmd-btn');
|
|
177
|
-
if (btn) { btn.disabled = true; btn.innerHTML = `${icon('spinner', 15)}
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
const result = await api.runCommand(cmd);
|
|
181
|
-
const sessionId = result.session?.id || result.sessionId;
|
|
182
|
-
|
|
183
|
-
// Añadir sesión al estado
|
|
184
|
-
const sessions = state.get('sessions');
|
|
185
|
-
const newSession = {
|
|
186
|
-
id: sessionId,
|
|
187
|
-
command: cmd,
|
|
188
|
-
status: 'running',
|
|
189
|
-
exitCode: null,
|
|
190
|
-
output: '',
|
|
191
|
-
startedAt: new Date().toISOString(),
|
|
192
|
-
};
|
|
193
|
-
sessions.push(newSession);
|
|
194
|
-
state.update('sessions', sessions);
|
|
195
|
-
state.update('selectedSessionId', sessionId);
|
|
196
|
-
|
|
197
|
-
// Refresh UI
|
|
198
|
-
import('../router.js').then(r => r.refresh()).then(() => {
|
|
199
|
-
_subscribeToStream(sessionId, cmd);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
if (input) input.value = '';
|
|
203
|
-
} catch (err) {
|
|
204
|
-
flash(err.message, 'error');
|
|
205
|
-
} finally {
|
|
206
|
-
if (btn) { btn.disabled = false; btn.innerHTML = `${icon('execution', 15)}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function _subscribeToStream(sessionId, _cmd) {
|
|
211
|
-
const es = api.streamSession(sessionId);
|
|
212
|
-
const terminal = document.getElementById('terminal-output');
|
|
213
|
-
|
|
214
|
-
es.addEventListener('message', e => {
|
|
215
|
-
try {
|
|
216
|
-
const data = JSON.parse(e.data);
|
|
217
|
-
const sessions = state.get('sessions');
|
|
218
|
-
const session = sessions.find(s => s.id === sessionId);
|
|
219
|
-
|
|
220
|
-
if (data.type === 'stdout' || data.type === 'stderr') {
|
|
221
|
-
if (session) session.output += data.chunk;
|
|
222
|
-
if (terminal && state.get('selectedSessionId') === sessionId) {
|
|
223
|
-
terminal.textContent = session?.output || '';
|
|
224
|
-
terminal.scrollTop = terminal.scrollHeight;
|
|
225
|
-
}
|
|
226
|
-
} else if (data.type === 'done') {
|
|
227
|
-
if (session) {
|
|
228
|
-
session.status = data.status;
|
|
229
|
-
session.exitCode = data.exitCode;
|
|
230
|
-
session.output = data.output;
|
|
231
|
-
}
|
|
232
|
-
state.update('sessions', sessions);
|
|
233
|
-
if (state.get('selectedSessionId') === sessionId) {
|
|
234
|
-
import('../router.js').then(r => r.refresh());
|
|
235
|
-
}
|
|
236
|
-
es.close();
|
|
237
|
-
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
238
|
-
}
|
|
239
|
-
} catch (err) {
|
|
240
|
-
console.error('[execution] Error parseando evento SSE:', err);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
es.onerror = () => {
|
|
245
|
-
const sessions = state.get('sessions');
|
|
246
|
-
const session = sessions.find(s => s.id === sessionId);
|
|
247
|
-
if (session && session.status === 'running') {
|
|
248
|
-
session.status = 'failed';
|
|
249
|
-
state.update('sessions', sessions);
|
|
250
|
-
}
|
|
251
|
-
es.close();
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Vinculación de eventos se llama desde la vista
|
|
256
|
-
export { bindEvents as bind };
|
|
1
|
+
/**
|
|
2
|
+
* execution.js — Consola integrada + sesiones de comandos
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { icon } from '../icons.js';
|
|
6
|
+
import * as state from '../state.js';
|
|
7
|
+
import * as api from '../api.js';
|
|
8
|
+
import { flash } from './flash.js';
|
|
9
|
+
import { esc, formatDate } from '../utils.js';
|
|
10
|
+
import { t } from '../i18n.js';
|
|
11
|
+
|
|
12
|
+
export async function render() {
|
|
13
|
+
const sessions = state.get('sessions');
|
|
14
|
+
const selected = sessions.find(s => s.id === state.get('selectedSessionId'));
|
|
15
|
+
const quickCommands = [
|
|
16
|
+
{ label: t('ui.execution.quick.status', {}, 'status'), cmd: 'npx trackops status' },
|
|
17
|
+
{ label: t('ui.execution.quick.sync', {}, 'sync docs'), cmd: 'npx trackops sync' },
|
|
18
|
+
{ label: t('ui.execution.quick.next', {}, 'next tasks'), cmd: 'npx trackops next' },
|
|
19
|
+
{ label: t('ui.execution.quick.repo', {}, 'refresh repo'), cmd: 'npx trackops refresh-repo' },
|
|
20
|
+
{ label: 'git status', cmd: 'git status --short' },
|
|
21
|
+
{ label: 'git log', cmd: 'git log --oneline -10' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
return `
|
|
25
|
+
<div class="view-enter">
|
|
26
|
+
<div class="section-header">
|
|
27
|
+
<div class="section-header-left">
|
|
28
|
+
<p class="eyebrow">${t('ui.execution.eyebrow', {}, 'Execution')}</p>
|
|
29
|
+
<h2>${t('ui.execution.title', {}, 'Command Console')}</h2>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div style="display:grid;grid-template-columns:300px 1fr;gap:var(--space-4);align-items:start">
|
|
34
|
+
|
|
35
|
+
<!-- Panel izquierdo: input + sesiones -->
|
|
36
|
+
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
|
37
|
+
|
|
38
|
+
<!-- Command input -->
|
|
39
|
+
<div class="panel">
|
|
40
|
+
<div class="panel-header">
|
|
41
|
+
<p class="panel-title">${t('ui.execution.run', {}, 'Run')}</p>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
44
|
+
<div class="field">
|
|
45
|
+
<label for="cmd-input">${t('ui.execution.command', {}, 'Command')}</label>
|
|
46
|
+
<textarea id="cmd-input" rows="3"
|
|
47
|
+
placeholder="npx trackops status"
|
|
48
|
+
aria-label="${t('ui.execution.commandAria', {}, 'Enter the command to run')}"
|
|
49
|
+
style="font-family:var(--font-mono);font-size:var(--text-sm)"></textarea>
|
|
50
|
+
</div>
|
|
51
|
+
<button class="btn btn-primary" type="button" id="run-cmd-btn" aria-label="${t('ui.execution.runButtonAria', {}, 'Run command')}">
|
|
52
|
+
${icon('execution', 15)} ${t('ui.execution.run', {}, 'Run')}
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="panel-footer">
|
|
56
|
+
<p class="label-sm" style="margin-bottom:var(--space-2)">${t('ui.execution.quickTitle', {}, 'Quick commands')}</p>
|
|
57
|
+
<div class="preset-strip">
|
|
58
|
+
${quickCommands.map(c => `
|
|
59
|
+
<button class="chip" type="button" data-quick="${esc(c.cmd)}"
|
|
60
|
+
aria-label="${t('ui.execution.runQuick', { label: c.label }, `Run ${c.label}`)}">
|
|
61
|
+
${esc(c.label)}
|
|
62
|
+
</button>
|
|
63
|
+
`).join('')}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Lista de sesiones -->
|
|
69
|
+
<div class="panel">
|
|
70
|
+
<div class="panel-header">
|
|
71
|
+
<p class="panel-title">${t('ui.execution.sessions', {}, 'Sessions')}</p>
|
|
72
|
+
<span class="badge badge-muted">${sessions.length}</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="panel-body" style="max-height:360px;overflow-y:auto">
|
|
75
|
+
${sessions.length === 0
|
|
76
|
+
? `<div class="empty-state">${t('ui.execution.noSessions', {}, 'No active sessions.')}</div>`
|
|
77
|
+
: `<div class="stack stack-sm">
|
|
78
|
+
${sessions.slice().reverse().map(s => `
|
|
79
|
+
<div class="session-pill ${s.id === state.get('selectedSessionId') ? 'is-selected' : ''}"
|
|
80
|
+
data-session-id="${esc(s.id)}"
|
|
81
|
+
role="button" tabindex="0"
|
|
82
|
+
aria-selected="${s.id === state.get('selectedSessionId')}"
|
|
83
|
+
aria-label="${esc(s.command)}, ${esc(s.status)}">
|
|
84
|
+
<span class="session-pill-cmd">${esc(s.command)}</span>
|
|
85
|
+
<span class="badge session-pill-status ${_sessionBadgeClass(s.status)}">${esc(s.status)}</span>
|
|
86
|
+
</div>
|
|
87
|
+
`).join('')}
|
|
88
|
+
</div>`
|
|
89
|
+
}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Terminal output -->
|
|
96
|
+
<div class="terminal-surface" style="min-height:600px;display:flex;flex-direction:column">
|
|
97
|
+
<div class="terminal-header">
|
|
98
|
+
<div class="terminal-dots" aria-hidden="true">
|
|
99
|
+
<span></span><span></span><span></span>
|
|
100
|
+
</div>
|
|
101
|
+
<p class="terminal-title">${selected ? esc(selected.command) : t('ui.execution.noSessionTitle', {}, 'ops@terminal — no active session')}</p>
|
|
102
|
+
<div style="display:flex;gap:var(--space-2)">
|
|
103
|
+
${selected && selected.status === 'running' ? `
|
|
104
|
+
<button class="btn btn-ghost btn-sm" id="kill-session-btn" type="button" aria-label="${t('ui.execution.stopAria', {}, 'Stop process')}">
|
|
105
|
+
${icon('stop', 13)} ${t('ui.execution.stop', {}, 'Stop')}
|
|
106
|
+
</button>
|
|
107
|
+
` : ''}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<pre class="terminal-output" id="terminal-output" aria-label="${t('ui.execution.output', {}, 'Command output')}" aria-live="polite">${selected ? esc(selected.output) : `${t('ui.execution.outputPlaceholder', {}, '# Run a command to see its output here…')}\n`}</pre>
|
|
111
|
+
${selected ? `<div class="panel-footer" style="display:flex;justify-content:space-between;align-items:center;font-family:var(--font-mono);font-size:var(--text-xs)">
|
|
112
|
+
<span class="text-muted">${t('ui.execution.started', {}, 'Started')}: ${formatDate(selected.startedAt)}</span>
|
|
113
|
+
<span class="badge ${_sessionBadgeClass(selected.status)}">${esc(selected.status)}${selected.exitCode != null ? ` (${selected.exitCode})` : ''}</span>
|
|
114
|
+
</div>` : ''}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _sessionBadgeClass(status) {
|
|
123
|
+
const map = {
|
|
124
|
+
running: 'badge-info',
|
|
125
|
+
completed: 'badge-success',
|
|
126
|
+
failed: 'badge-danger',
|
|
127
|
+
terminated:'badge-warning',
|
|
128
|
+
};
|
|
129
|
+
return map[status] || 'badge-muted';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function bindEvents() {
|
|
133
|
+
// Run command button
|
|
134
|
+
document.getElementById('run-cmd-btn')?.addEventListener('click', _runCommand);
|
|
135
|
+
|
|
136
|
+
// Ctrl+Enter en el textarea
|
|
137
|
+
document.getElementById('cmd-input')?.addEventListener('keydown', e => {
|
|
138
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
_runCommand();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Quick commands
|
|
145
|
+
document.querySelectorAll('[data-quick]').forEach(btn => {
|
|
146
|
+
btn.addEventListener('click', () => {
|
|
147
|
+
const input = document.getElementById('cmd-input');
|
|
148
|
+
if (input) {
|
|
149
|
+
input.value = btn.dataset.quick;
|
|
150
|
+
input.focus();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Seleccionar sesión
|
|
156
|
+
document.querySelectorAll('[data-session-id]').forEach(pill => {
|
|
157
|
+
pill.addEventListener('click', () => {
|
|
158
|
+
const id = pill.dataset.sessionId;
|
|
159
|
+
state.update('selectedSessionId', id);
|
|
160
|
+
import('../router.js').then(r => r.refresh());
|
|
161
|
+
});
|
|
162
|
+
pill.addEventListener('keydown', e => {
|
|
163
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
pill.click();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function _runCommand() {
|
|
172
|
+
const input = document.getElementById('cmd-input');
|
|
173
|
+
const cmd = input?.value.trim();
|
|
174
|
+
if (!cmd) return;
|
|
175
|
+
|
|
176
|
+
const btn = document.getElementById('run-cmd-btn');
|
|
177
|
+
if (btn) { btn.disabled = true; btn.innerHTML = `${icon('spinner', 15)} ${t('ui.execution.running', {}, 'Running…')}`; }
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const result = await api.runCommand(cmd);
|
|
181
|
+
const sessionId = result.session?.id || result.sessionId;
|
|
182
|
+
|
|
183
|
+
// Añadir sesión al estado
|
|
184
|
+
const sessions = state.get('sessions');
|
|
185
|
+
const newSession = {
|
|
186
|
+
id: sessionId,
|
|
187
|
+
command: cmd,
|
|
188
|
+
status: 'running',
|
|
189
|
+
exitCode: null,
|
|
190
|
+
output: '',
|
|
191
|
+
startedAt: new Date().toISOString(),
|
|
192
|
+
};
|
|
193
|
+
sessions.push(newSession);
|
|
194
|
+
state.update('sessions', sessions);
|
|
195
|
+
state.update('selectedSessionId', sessionId);
|
|
196
|
+
|
|
197
|
+
// Refresh UI
|
|
198
|
+
import('../router.js').then(r => r.refresh()).then(() => {
|
|
199
|
+
_subscribeToStream(sessionId, cmd);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (input) input.value = '';
|
|
203
|
+
} catch (err) {
|
|
204
|
+
flash(err.message, 'error');
|
|
205
|
+
} finally {
|
|
206
|
+
if (btn) { btn.disabled = false; btn.innerHTML = `${icon('execution', 15)} ${t('ui.execution.run', {}, 'Run')}`; }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function _subscribeToStream(sessionId, _cmd) {
|
|
211
|
+
const es = api.streamSession(sessionId);
|
|
212
|
+
const terminal = document.getElementById('terminal-output');
|
|
213
|
+
|
|
214
|
+
es.addEventListener('message', e => {
|
|
215
|
+
try {
|
|
216
|
+
const data = JSON.parse(e.data);
|
|
217
|
+
const sessions = state.get('sessions');
|
|
218
|
+
const session = sessions.find(s => s.id === sessionId);
|
|
219
|
+
|
|
220
|
+
if (data.type === 'stdout' || data.type === 'stderr') {
|
|
221
|
+
if (session) session.output += data.chunk;
|
|
222
|
+
if (terminal && state.get('selectedSessionId') === sessionId) {
|
|
223
|
+
terminal.textContent = session?.output || '';
|
|
224
|
+
terminal.scrollTop = terminal.scrollHeight;
|
|
225
|
+
}
|
|
226
|
+
} else if (data.type === 'done') {
|
|
227
|
+
if (session) {
|
|
228
|
+
session.status = data.status;
|
|
229
|
+
session.exitCode = data.exitCode;
|
|
230
|
+
session.output = data.output;
|
|
231
|
+
}
|
|
232
|
+
state.update('sessions', sessions);
|
|
233
|
+
if (state.get('selectedSessionId') === sessionId) {
|
|
234
|
+
import('../router.js').then(r => r.refresh());
|
|
235
|
+
}
|
|
236
|
+
es.close();
|
|
237
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
238
|
+
}
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error('[execution] Error parseando evento SSE:', err);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
es.onerror = () => {
|
|
245
|
+
const sessions = state.get('sessions');
|
|
246
|
+
const session = sessions.find(s => s.id === sessionId);
|
|
247
|
+
if (session && session.status === 'running') {
|
|
248
|
+
session.status = 'failed';
|
|
249
|
+
state.update('sessions', sessions);
|
|
250
|
+
}
|
|
251
|
+
es.close();
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Vinculación de eventos se llama desde la vista
|
|
256
|
+
export { bindEvents as bind };
|