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.
Files changed (57) hide show
  1. package/README.md +326 -270
  2. package/bin/trackops.js +102 -70
  3. package/lib/config.js +260 -35
  4. package/lib/control.js +517 -475
  5. package/lib/env.js +227 -0
  6. package/lib/i18n.js +61 -53
  7. package/lib/init.js +135 -46
  8. package/lib/locale.js +63 -0
  9. package/lib/opera-bootstrap.js +523 -0
  10. package/lib/opera.js +319 -170
  11. package/lib/registry.js +27 -13
  12. package/lib/release.js +56 -0
  13. package/lib/resources.js +42 -0
  14. package/lib/server.js +907 -554
  15. package/lib/skills.js +148 -124
  16. package/lib/workspace.js +260 -0
  17. package/locales/en.json +331 -139
  18. package/locales/es.json +331 -139
  19. package/package.json +7 -9
  20. package/scripts/skills-marketplace-smoke.js +124 -0
  21. package/scripts/smoke-tests.js +445 -0
  22. package/scripts/sync-skill-version.js +21 -0
  23. package/scripts/validate-skill.js +88 -0
  24. package/skills/trackops/SKILL.md +64 -0
  25. package/skills/trackops/agents/openai.yaml +3 -0
  26. package/skills/trackops/references/activation.md +39 -0
  27. package/skills/trackops/references/troubleshooting.md +34 -0
  28. package/skills/trackops/references/workflow.md +20 -0
  29. package/skills/trackops/scripts/bootstrap-trackops.js +201 -0
  30. package/skills/trackops/skill.json +29 -0
  31. package/templates/opera/en/agent.md +26 -0
  32. package/templates/opera/en/genesis.md +79 -0
  33. package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
  34. package/templates/opera/en/references/opera-cycle.md +62 -0
  35. package/templates/opera/en/registry.md +28 -0
  36. package/templates/opera/en/router.md +39 -0
  37. package/templates/opera/genesis.md +79 -94
  38. package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
  39. package/templates/skills/commiter/locales/en/SKILL.md +11 -0
  40. package/templates/skills/project-starter-skill/locales/en/SKILL.md +24 -0
  41. package/ui/css/panels.css +956 -953
  42. package/ui/index.html +1 -1
  43. package/ui/js/api.js +211 -194
  44. package/ui/js/app.js +200 -199
  45. package/ui/js/i18n.js +14 -0
  46. package/ui/js/onboarding.js +439 -437
  47. package/ui/js/state.js +130 -129
  48. package/ui/js/utils.js +175 -172
  49. package/ui/js/views/board.js +255 -254
  50. package/ui/js/views/execution.js +256 -256
  51. package/ui/js/views/insights.js +340 -339
  52. package/ui/js/views/overview.js +365 -364
  53. package/ui/js/views/settings.js +340 -202
  54. package/ui/js/views/sidebar.js +131 -132
  55. package/ui/js/views/skills.js +163 -162
  56. package/ui/js/views/tasks.js +406 -405
  57. package/ui/js/views/topbar.js +239 -183
@@ -1,183 +1,239 @@
1
- /**
2
- * topbar.js — Barra superior del dashboard
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, debounce } from '../utils.js';
10
- import * as theme from '../theme.js';
11
-
12
- /** Renderiza el topbar */
13
- export function render() {
14
- const el = document.getElementById('topbar');
15
- if (!el) return;
16
-
17
- const payload = state.getPayload();
18
- const projects = state.get('projects');
19
- const currentId = state.get('currentProjectId');
20
- const runtime = payload?.runtime;
21
-
22
- el.innerHTML = `
23
- <div class="topbar">
24
- <!-- Hamburger (mobile) -->
25
- <button class="topbar-hamburger" type="button" id="sidebar-toggle" aria-label="Abrir menú" aria-expanded="false" aria-controls="sidebar">
26
- <span></span><span></span><span></span>
27
- </button>
28
-
29
- <!-- Search -->
30
- <div class="topbar-search">
31
- <div class="search-wrapper" role="search">
32
- <span class="search-icon" aria-hidden="true">${icon('search', 16)}</span>
33
- <input
34
- type="search"
35
- id="global-search"
36
- aria-label="Buscar tareas"
37
- placeholder="Buscar tareas…"
38
- autocomplete="off"
39
- value="${esc(state.get('searchQuery'))}"
40
- />
41
- <span class="search-kbd" aria-hidden="true">⌘F</span>
42
- </div>
43
- </div>
44
-
45
- <!-- Derecha -->
46
- <div class="topbar-right">
47
-
48
- <!-- Timer activo -->
49
- <div class="topbar-timer" id="topbar-timer" aria-label="Seguimiento de tiempo" aria-live="polite">
50
- <span class="topbar-timer-dot" aria-hidden="true"></span>
51
- <span id="topbar-timer-display">00:00:00</span>
52
- </div>
53
-
54
- <!-- Repo status -->
55
- ${runtime ? _renderRepoBadge(runtime) : ''}
56
-
57
- <!-- Project selector -->
58
- ${projects.length > 0 ? _renderProjectSelector(projects, currentId) : ''}
59
-
60
- <!-- Sync button -->
61
- <button class="btn btn-ghost btn-sm" id="sync-btn" type="button" aria-label="Sincronizar documentación">
62
- ${icon('sync', 16)} Sincronizar
63
- </button>
64
-
65
- <!-- Theme toggle -->
66
- ${theme.renderButton()}
67
-
68
- <!-- Refresh -->
69
- <button class="btn btn-ghost btn-sm btn-icon" id="refresh-btn" type="button" aria-label="Refrescar estado">
70
- ${icon('refresh', 16)}
71
- </button>
72
-
73
- </div>
74
- </div>
75
- `;
76
-
77
- _bindEvents();
78
- }
79
-
80
- function _renderRepoBadge(runtime) {
81
- const isClean = runtime.clean;
82
- const label = isClean ? 'Repo limpio' : `${runtime.staged}s ${runtime.unstaged}u ${runtime.untracked}?`;
83
- return `
84
- <div class="repo-badge ${isClean ? 'clean' : 'dirty'}" title="${esc(runtime.branch || '')}">
85
- <span class="repo-badge-dot" aria-hidden="true"></span>
86
- <span>${icon('gitBranch', 12)} ${esc(runtime.branch || 'sin rama')} · ${label}</span>
87
- </div>
88
- `;
89
- }
90
-
91
- function _renderProjectSelector(projects, currentId) {
92
- const options = projects.map(p =>
93
- `<option value="${esc(p.id)}" ${p.id === currentId ? 'selected' : ''} ${p.available ? '' : 'disabled'}>
94
- ${esc(p.name)}${p.available ? '' : ' (no disponible)'}
95
- </option>`
96
- ).join('');
97
-
98
- return `
99
- <div class="project-select-wrapper" title="Proyecto activo">
100
- <select id="project-select" aria-label="Seleccionar proyecto activo">
101
- ${options}
102
- </select>
103
- </div>
104
- `;
105
- }
106
-
107
- function _bindEvents() {
108
- // Hamburger → sidebar open
109
- document.getElementById('sidebar-toggle')?.addEventListener('click', () => {
110
- const sidebar = document.getElementById('sidebar');
111
- const isOpen = sidebar?.classList.toggle('is-open');
112
- document.getElementById('sidebar-toggle')?.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
113
- });
114
-
115
- // Theme toggle
116
- theme.bindButton();
117
-
118
- // Cerrar sidebar al hacer clic fuera (mobile)
119
- document.addEventListener('click', e => {
120
- const sidebar = document.getElementById('sidebar');
121
- const toggle = document.getElementById('sidebar-toggle');
122
- if (sidebar?.classList.contains('is-open') &&
123
- !sidebar.contains(e.target) && !toggle?.contains(e.target)) {
124
- sidebar.classList.remove('is-open');
125
- toggle?.setAttribute('aria-expanded', 'false');
126
- }
127
- });
128
-
129
- // Project selector
130
- document.getElementById('project-select')?.addEventListener('change', async e => {
131
- const id = e.target.value;
132
- state.update('currentProjectId', id);
133
- localStorage.setItem('ops-dashboard-project', id);
134
- state.update('selectedTaskId', null);
135
- // Trigger refresh global
136
- window.dispatchEvent(new CustomEvent('ops:refresh'));
137
- });
138
-
139
- // Sync button
140
- document.getElementById('sync-btn')?.addEventListener('click', async () => {
141
- const btn = document.getElementById('sync-btn');
142
- if (btn) { btn.disabled = true; btn.innerHTML = `${icon('sync', 16)} Sincronizando…`; }
143
- try {
144
- await api.syncDocs();
145
- flash('Documentación sincronizada.', 'success');
146
- window.dispatchEvent(new CustomEvent('ops:refresh'));
147
- } catch (err) {
148
- flash(err.message, 'error');
149
- } finally {
150
- if (btn) { btn.disabled = false; btn.innerHTML = `${icon('sync', 16)} Sincronizar`; }
151
- }
152
- });
153
-
154
- // Refresh button
155
- document.getElementById('refresh-btn')?.addEventListener('click', () => {
156
- window.dispatchEvent(new CustomEvent('ops:refresh'));
157
- });
158
-
159
- // Búsqueda global (debounced)
160
- const searchInput = document.getElementById('global-search');
161
- if (searchInput) {
162
- const handleSearch = debounce(e => {
163
- state.update('searchQuery', e.target.value);
164
- window.dispatchEvent(new CustomEvent('ops:search', { detail: { query: e.target.value } }));
165
- }, 250);
166
- searchInput.addEventListener('input', handleSearch);
167
-
168
- // Atajo de teclado ⌘/Ctrl+F
169
- document.addEventListener('keydown', e => {
170
- if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
171
- e.preventDefault();
172
- searchInput.focus();
173
- searchInput.select();
174
- }
175
- });
176
- }
177
- }
178
-
179
- /** Actualiza solo el timer del topbar sin re-renderizar */
180
- export function updateTimer(display) {
181
- const el = document.getElementById('topbar-timer-display');
182
- if (el) el.textContent = display;
183
- }
1
+ /**
2
+ * topbar.js — Barra superior del dashboard
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, debounce } from '../utils.js';
10
+ import * as theme from '../theme.js';
11
+ import { t } from '../i18n.js';
12
+
13
+ /** Renderiza el topbar */
14
+ export function render() {
15
+ const el = document.getElementById('topbar');
16
+ if (!el) return;
17
+
18
+ const payload = state.getPayload();
19
+ const projects = state.get('projects');
20
+ const currentId = state.get('currentProjectId');
21
+ const currentLocale = state.get('locale') || payload?.i18n?.locale || 'es';
22
+ const runtime = payload?.runtime;
23
+
24
+ el.innerHTML = `
25
+ <div class="topbar">
26
+ <!-- Hamburger (mobile) -->
27
+ <button class="topbar-hamburger" type="button" id="sidebar-toggle" aria-label="${t('ui.topbar.openMenu', {}, 'Open menu')}" aria-expanded="false" aria-controls="sidebar">
28
+ <span></span><span></span><span></span>
29
+ </button>
30
+
31
+ <!-- Search -->
32
+ <div class="topbar-search">
33
+ <div class="search-wrapper" role="search">
34
+ <span class="search-icon" aria-hidden="true">${icon('search', 16)}</span>
35
+ <input
36
+ type="search"
37
+ id="global-search"
38
+ aria-label="${t('ui.topbar.searchAria', {}, 'Search tasks')}"
39
+ placeholder="${t('ui.topbar.searchPlaceholder', {}, 'Search tasks…')}"
40
+ autocomplete="off"
41
+ value="${esc(state.get('searchQuery'))}"
42
+ />
43
+ <span class="search-kbd" aria-hidden="true">⌘F</span>
44
+ </div>
45
+ </div>
46
+
47
+ <!-- Derecha -->
48
+ <div class="topbar-right">
49
+
50
+ <!-- Timer activo -->
51
+ <div class="topbar-timer" id="topbar-timer" aria-label="${t('ui.topbar.timer', {}, 'Time tracking')}" aria-live="polite">
52
+ <span class="topbar-timer-dot" aria-hidden="true"></span>
53
+ <span id="topbar-timer-display">00:00:00</span>
54
+ </div>
55
+
56
+ <!-- Repo status -->
57
+ ${runtime ? _renderRepoBadge(runtime) : ''}
58
+
59
+ <!-- Project selector -->
60
+ ${projects.length > 0 ? _renderProjectSelector(projects, currentId) : ''}
61
+
62
+ <!-- Locale selector -->
63
+ ${_renderLocaleSelector(currentLocale)}
64
+
65
+ <!-- Sync button -->
66
+ <button class="btn btn-ghost btn-sm" id="sync-btn" type="button" aria-label="${t('ui.topbar.syncAria', {}, 'Sync documentation')}">
67
+ ${icon('sync', 16)} ${t('ui.topbar.sync', {}, 'Sync')}
68
+ </button>
69
+
70
+ <!-- Theme toggle -->
71
+ ${theme.renderButton()}
72
+
73
+ <!-- Refresh -->
74
+ <button class="btn btn-ghost btn-sm btn-icon" id="refresh-btn" type="button" aria-label="${t('ui.topbar.refresh', {}, 'Refresh state')}">
75
+ ${icon('refresh', 16)}
76
+ </button>
77
+
78
+ </div>
79
+ </div>
80
+ `;
81
+
82
+ _bindEvents();
83
+ }
84
+
85
+ function _renderRepoBadge(runtime) {
86
+ const isClean = runtime.clean;
87
+ const label = isClean
88
+ ? t('ui.topbar.repoClean', {}, 'Clean repo')
89
+ : t('ui.topbar.repoDirty', {
90
+ staged: runtime.staged,
91
+ unstaged: runtime.unstaged,
92
+ untracked: runtime.untracked,
93
+ }, `${runtime.staged}s ${runtime.unstaged}u ${runtime.untracked}?`);
94
+ return `
95
+ <div class="repo-badge ${isClean ? 'clean' : 'dirty'}" title="${esc(runtime.branch || '')}">
96
+ <span class="repo-badge-dot" aria-hidden="true"></span>
97
+ <span>${icon('gitBranch', 12)} ${esc(runtime.branch || t('ui.topbar.noBranch', {}, 'no branch'))} · ${label}</span>
98
+ </div>
99
+ `;
100
+ }
101
+
102
+ function _renderProjectSelector(projects, currentId) {
103
+ const options = projects.map(p =>
104
+ `<option value="${esc(p.id)}" ${p.id === currentId ? 'selected' : ''} ${p.available ? '' : 'disabled'}>
105
+ ${esc(p.name)}${p.available ? '' : ` (${t('ui.topbar.unavailable', {}, 'unavailable')})`}
106
+ </option>`
107
+ ).join('');
108
+
109
+ return `
110
+ <div class="project-select-wrapper" title="${t('ui.topbar.activeProject', {}, 'Active project')}">
111
+ <select id="project-select" aria-label="${t('ui.topbar.activeProject', {}, 'Active project')}">
112
+ ${options}
113
+ </select>
114
+ </div>
115
+ `;
116
+ }
117
+
118
+ function _renderLocaleSelector(currentLocale) {
119
+ return `
120
+ <div class="project-select-wrapper locale-select-wrapper" title="${t('ui.topbar.language', {}, 'Language')}">
121
+ <select id="locale-select" aria-label="${t('ui.topbar.languageAria', {}, 'Select dashboard language')}">
122
+ <option value="es" ${currentLocale === 'es' ? 'selected' : ''}>${t('ui.topbar.languageEs', {}, 'ES')}</option>
123
+ <option value="en" ${currentLocale === 'en' ? 'selected' : ''}>${t('ui.topbar.languageEn', {}, 'EN')}</option>
124
+ </select>
125
+ </div>
126
+ `;
127
+ }
128
+
129
+ function _applyLocaleState(payload) {
130
+ if (!payload) return;
131
+ state.update('payload', payload);
132
+ if (payload.i18n) {
133
+ state.update({
134
+ phases: payload.i18n.phases || [],
135
+ statusLabels: payload.i18n.statusLabels || {},
136
+ locale: payload.i18n.locale || 'es',
137
+ messages: payload.i18n.messages || {},
138
+ });
139
+ }
140
+ }
141
+
142
+ function _bindEvents() {
143
+ // Hamburger → sidebar open
144
+ document.getElementById('sidebar-toggle')?.addEventListener('click', () => {
145
+ const sidebar = document.getElementById('sidebar');
146
+ const isOpen = sidebar?.classList.toggle('is-open');
147
+ document.getElementById('sidebar-toggle')?.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
148
+ });
149
+
150
+ // Theme toggle
151
+ theme.bindButton();
152
+
153
+ // Cerrar sidebar al hacer clic fuera (mobile)
154
+ document.addEventListener('click', e => {
155
+ const sidebar = document.getElementById('sidebar');
156
+ const toggle = document.getElementById('sidebar-toggle');
157
+ if (sidebar?.classList.contains('is-open') &&
158
+ !sidebar.contains(e.target) && !toggle?.contains(e.target)) {
159
+ sidebar.classList.remove('is-open');
160
+ toggle?.setAttribute('aria-expanded', 'false');
161
+ }
162
+ });
163
+
164
+ // Project selector
165
+ document.getElementById('project-select')?.addEventListener('change', async e => {
166
+ const id = e.target.value;
167
+ state.update('currentProjectId', id);
168
+ localStorage.setItem('ops-dashboard-project', id);
169
+ state.update('selectedTaskId', null);
170
+ // Trigger refresh global
171
+ window.dispatchEvent(new CustomEvent('ops:refresh'));
172
+ });
173
+
174
+ document.getElementById('locale-select')?.addEventListener('change', async e => {
175
+ const select = e.target;
176
+ const previousLocale = state.get('locale') || 'es';
177
+ const nextLocale = select.value;
178
+
179
+ if (nextLocale === previousLocale) return;
180
+
181
+ select.disabled = true;
182
+ try {
183
+ const result = await api.updateProjectLocale(nextLocale);
184
+ _applyLocaleState(result.state);
185
+ flash(t('ui.topbar.localeUpdated', {}, 'Language updated.'), 'success');
186
+ window.dispatchEvent(new CustomEvent('ops:refresh'));
187
+ } catch (err) {
188
+ select.value = previousLocale;
189
+ flash(err.message, 'error');
190
+ } finally {
191
+ select.disabled = false;
192
+ }
193
+ });
194
+
195
+ // Sync button
196
+ document.getElementById('sync-btn')?.addEventListener('click', async () => {
197
+ const btn = document.getElementById('sync-btn');
198
+ if (btn) { btn.disabled = true; btn.innerHTML = `${icon('sync', 16)} ${t('ui.topbar.syncing', {}, 'Syncing…')}`; }
199
+ try {
200
+ await api.syncDocs();
201
+ flash(t('ui.topbar.synced', {}, 'Documentation synced.'), 'success');
202
+ window.dispatchEvent(new CustomEvent('ops:refresh'));
203
+ } catch (err) {
204
+ flash(err.message, 'error');
205
+ } finally {
206
+ if (btn) { btn.disabled = false; btn.innerHTML = `${icon('sync', 16)} ${t('ui.topbar.sync', {}, 'Sync')}`; }
207
+ }
208
+ });
209
+
210
+ // Refresh button
211
+ document.getElementById('refresh-btn')?.addEventListener('click', () => {
212
+ window.dispatchEvent(new CustomEvent('ops:refresh'));
213
+ });
214
+
215
+ // Búsqueda global (debounced)
216
+ const searchInput = document.getElementById('global-search');
217
+ if (searchInput) {
218
+ const handleSearch = debounce(e => {
219
+ state.update('searchQuery', e.target.value);
220
+ window.dispatchEvent(new CustomEvent('ops:search', { detail: { query: e.target.value } }));
221
+ }, 250);
222
+ searchInput.addEventListener('input', handleSearch);
223
+
224
+ // Atajo de teclado ⌘/Ctrl+F
225
+ document.addEventListener('keydown', e => {
226
+ if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
227
+ e.preventDefault();
228
+ searchInput.focus();
229
+ searchInput.select();
230
+ }
231
+ });
232
+ }
233
+ }
234
+
235
+ /** Actualiza solo el timer del topbar sin re-renderizar */
236
+ export function updateTimer(display) {
237
+ const el = document.getElementById('topbar-timer-display');
238
+ if (el) el.textContent = display;
239
+ }