@planu/cli 4.1.1 → 4.1.3

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 (62) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/config/license-plans.json +65 -361
  3. package/dist/engine/core-bridge.js +35 -4
  4. package/dist/engine/hooks/file-watcher.d.ts +6 -0
  5. package/dist/engine/hooks/file-watcher.js +69 -16
  6. package/dist/tools/git/hook-ops.js +23 -9
  7. package/dist/tools/tool-registry/group-infra.js +22 -0
  8. package/package.json +7 -7
  9. package/dist/engine/escalator/index.d.ts +0 -5
  10. package/dist/engine/escalator/index.js +0 -5
  11. package/dist/engine/freeze/retro-audit.d.ts +0 -6
  12. package/dist/engine/freeze/retro-audit.js +0 -24
  13. package/dist/engine/heal/backup.d.ts +0 -9
  14. package/dist/engine/heal/backup.js +0 -21
  15. package/dist/engine/idioma-validator/index.d.ts +0 -17
  16. package/dist/engine/idioma-validator/index.js +0 -89
  17. package/dist/engine/saga/index.d.ts +0 -4
  18. package/dist/engine/saga/index.js +0 -4
  19. package/dist/engine/spec-state-machine/index.d.ts +0 -3
  20. package/dist/engine/spec-state-machine/index.js +0 -2
  21. package/dist/engine/spec-summary-html/dashboard-renderer.d.ts +0 -6
  22. package/dist/engine/spec-summary-html/dashboard-renderer.js +0 -333
  23. package/dist/engine/triagier/index.d.ts +0 -5
  24. package/dist/engine/triagier/index.js +0 -5
  25. package/dist/engine/universal-rules/index.d.ts +0 -5
  26. package/dist/engine/universal-rules/index.js +0 -6
  27. package/dist/testing/cassette/index.d.ts +0 -23
  28. package/dist/testing/cassette/index.js +0 -26
  29. package/dist/tools/domain-bundle-handler.d.ts +0 -37
  30. package/dist/tools/domain-bundle-handler.js +0 -71
  31. package/dist/tools/figma/rules-file.d.ts +0 -5
  32. package/dist/tools/figma/rules-file.js +0 -45
  33. package/dist/tools/heal-planu-root.d.ts +0 -8
  34. package/dist/tools/heal-planu-root.js +0 -144
  35. package/dist/tools/opencode-host-adapter.d.ts +0 -3
  36. package/dist/tools/opencode-host-adapter.js +0 -33
  37. package/dist/tools/plan-team-distribution.d.ts +0 -3
  38. package/dist/tools/plan-team-distribution.js +0 -71
  39. package/dist/tools/reconcile-status-json.d.ts +0 -4
  40. package/dist/tools/reconcile-status-json.js +0 -209
  41. package/dist/tools/register-all-tools.d.ts +0 -8
  42. package/dist/tools/register-all-tools.js +0 -239
  43. package/dist/tools/tool-registry/group-analysis-monitoring.d.ts +0 -3
  44. package/dist/tools/tool-registry/group-analysis-monitoring.js +0 -942
  45. package/dist/tools/tool-registry/group-integrations.d.ts +0 -3
  46. package/dist/tools/tool-registry/group-integrations.js +0 -1046
  47. package/dist/tools/tool-registry/group-misc.d.ts +0 -3
  48. package/dist/tools/tool-registry/group-misc.js +0 -1367
  49. package/dist/tools/tool-registry/group-platform.d.ts +0 -3
  50. package/dist/tools/tool-registry/group-platform.js +0 -1681
  51. package/dist/tools/tool-registry/group-session-knowledge.d.ts +0 -3
  52. package/dist/tools/tool-registry/group-session-knowledge.js +0 -1416
  53. package/dist/tools/tool-registry/group-spec-ops.d.ts +0 -3
  54. package/dist/tools/tool-registry/group-spec-ops.js +0 -917
  55. package/dist/tools/workspace-overview.d.ts +0 -4
  56. package/dist/tools/workspace-overview.js +0 -316
  57. package/dist/transports/middleware/index.d.ts +0 -9
  58. package/dist/transports/middleware/index.js +0 -7
  59. package/dist/transports/middleware/with-sandbox.d.ts +0 -21
  60. package/dist/transports/middleware/with-sandbox.js +0 -68
  61. package/dist/types/heal.d.ts +0 -18
  62. package/dist/types/heal.js +0 -3
@@ -1,333 +0,0 @@
1
- import { buildNavbar } from '../doc-generator/portal/portal-navbar.js';
2
- import { getPortalThemeCSS } from '../doc-generator/portal/portal-theme.js';
3
- import { buildQuickLinksSection } from '../doc-generator/portal/portal-landing-cards.js';
4
- const STATUS_COLORS = {
5
- draft: { bg: '#e5e7eb', fg: '#374151' },
6
- review: { bg: '#dbeafe', fg: '#1d4ed8' },
7
- approved: { bg: '#d1fae5', fg: '#065f46' },
8
- implementing: { bg: '#fef3c7', fg: '#92400e' },
9
- done: { bg: '#d1fae5', fg: '#065f46' },
10
- discarded: { bg: '#f3f4f6', fg: '#9ca3af' },
11
- };
12
- const RISK_ICONS = { low: '🟢', medium: '🟡', high: '🔴' };
13
- function escapeHtml(text) {
14
- return text
15
- .replace(/&/g, '&')
16
- .replace(/</g, '&lt;')
17
- .replace(/>/g, '&gt;')
18
- .replace(/"/g, '&quot;');
19
- }
20
- function statusBadge(status) {
21
- const colors = STATUS_COLORS[status] ?? { bg: '#e5e7eb', fg: '#374151' };
22
- return `<span class="badge" data-i18n-status="${escapeHtml(status)}" style="background:${colors.bg};color:${colors.fg}">${escapeHtml(status)}</span>`;
23
- }
24
- function specNumericId(spec) {
25
- const m = /SPEC-(\d+)/.exec(spec.id);
26
- return m?.[1] ? parseInt(m[1], 10) : 0;
27
- }
28
- function sortSpecs(specs) {
29
- return [...specs].sort((a, b) => specNumericId(a) - specNumericId(b));
30
- }
31
- function specFolder(spec) {
32
- if (!spec.specPath) {
33
- return '';
34
- }
35
- const parts = spec.specPath.split('/');
36
- return parts.length >= 2 ? (parts[parts.length - 2] ?? '') : '';
37
- }
38
- function renderSpecRow(spec) {
39
- const risk = RISK_ICONS[spec.risk] ?? '⚪';
40
- const tags = spec.tags.length > 0
41
- ? spec.tags.map((t) => `<span class="tag">${escapeHtml(t)}</span>`).join('')
42
- : '';
43
- const folder = specFolder(spec);
44
- const titleHtml = folder
45
- ? `<a href="specs/${escapeHtml(folder)}/executive-report.html" class="spec-link">${escapeHtml(spec.id)}: ${escapeHtml(spec.title)}</a>`
46
- : `<span class="spec-title">${escapeHtml(spec.id)}: ${escapeHtml(spec.title)}</span>`;
47
- const reportsHtml = folder
48
- ? `<a href="specs/${escapeHtml(folder)}/executive-report.html" class="report-link" title="Executive Report">📊</a> <a href="specs/${escapeHtml(folder)}/technical-report.html" class="report-link" title="Technical Report">🔧</a>`
49
- : '';
50
- return `<tr data-status="${escapeHtml(spec.status)}" data-search="${escapeHtml(`${spec.id} ${spec.title} ${spec.tags.join(' ')} ${spec.type} ${spec.status}`.toLowerCase())}">
51
- <td>${titleHtml}</td>
52
- <td>${statusBadge(spec.status)}</td>
53
- <td><span class="type-badge">${escapeHtml(spec.type)}</span></td>
54
- <td>${risk} <span data-i18n-risk="${escapeHtml(spec.risk)}">${escapeHtml(spec.risk)}</span></td>
55
- <td class="num">${String(spec.estimation.devHours)}h</td>
56
- <td class="num">$${String(spec.estimation.totalCostUsd)}</td>
57
- <td class="tags-cell">${tags}</td>
58
- <td class="reports-cell">${reportsHtml}</td>
59
- </tr>`;
60
- }
61
- function renderMetricCard(labelEn, labelEs, value, color) {
62
- return `<div class="metric-card">
63
- <div class="metric-value" style="color:${color}">${value}</div>
64
- <div class="metric-label" data-i18n-en="${escapeHtml(labelEn)}" data-i18n-es="${escapeHtml(labelEs)}">${escapeHtml(labelEn)}</div>
65
- </div>`;
66
- }
67
- function renderProgressBar(done, total) {
68
- const pct = total > 0 ? Math.round((done / total) * 100) : 0;
69
- const color = pct >= 80 ? '#22c55e' : pct >= 40 ? '#f59e0b' : '#ef4444';
70
- return `<div class="progress-bar">
71
- <div class="progress-fill" style="width:${String(pct)}%;background:${color}"></div>
72
- <span class="progress-text">${String(pct)}% <span data-i18n-en="complete" data-i18n-es="completado">complete</span></span>
73
- </div>`;
74
- }
75
- export function getInlineCss() {
76
- return `*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
77
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #1f2937; line-height: 1.5; max-width: 1280px; margin: 0 auto; padding: 24px; }
78
- .header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
79
- h1 { font-size: 1.5rem; color: #111827; }
80
- .subtitle { color: #6b7280; font-size: 0.85rem; margin-bottom: 20px; }
81
- .lang-toggle { display: flex; gap: 4px; }
82
- .lang-btn { padding: 4px 10px; border: 1px solid #d1d5db; border-radius: 4px; background: #fff; cursor: pointer; font-size: 0.75rem; font-weight: 600; color: #6b7280; transition: all 0.15s; }
83
- .lang-btn.active { background: #4F46E5; color: #fff; border-color: #4F46E5; }
84
- .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 12px; margin-bottom: 20px; }
85
- .metric-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; text-align: center; }
86
- .metric-value { font-size: 1.6rem; font-weight: 700; line-height: 1.2; }
87
- .metric-label { font-size: 0.72rem; color: #6b7280; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
88
- .progress-bar { background: #e5e7eb; border-radius: 99px; height: 20px; position: relative; overflow: hidden; margin-bottom: 20px; }
89
- .progress-fill { height: 100%; border-radius: 99px; transition: width 0.3s; }
90
- .progress-text { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); font-size: 0.7rem; font-weight: 600; color: #374151; }
91
- .toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
92
- .search-input { flex: 1; min-width: 200px; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.85rem; outline: none; }
93
- .search-input:focus { border-color: #4F46E5; box-shadow: 0 0 0 2px rgba(79,70,229,0.15); }
94
- .filter-btn { padding: 5px 12px; border: 1px solid #d1d5db; border-radius: 6px; background: #fff; cursor: pointer; font-size: 0.78rem; color: #374151; transition: all 0.15s; }
95
- .filter-btn:hover { border-color: #4F46E5; color: #4F46E5; }
96
- .filter-btn.active { background: #4F46E5; color: #fff; border-color: #4F46E5; }
97
- .table-wrap { overflow-x: auto; }
98
- table { width: 100%; border-collapse: collapse; font-size: 0.82rem; table-layout: fixed; }
99
- thead th { text-align: left; padding: 8px 10px; background: #f9fafb; border-bottom: 2px solid #e5e7eb; color: #6b7280; font-weight: 600; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; }
100
- th:nth-child(1) { width: 38%; } th:nth-child(2) { width: 8%; } th:nth-child(3) { width: 7%; }
101
- th:nth-child(4) { width: 8%; } th:nth-child(5) { width: 6%; } th:nth-child(6) { width: 7%; }
102
- th:nth-child(7) { width: 18%; } th:nth-child(8) { width: 8%; }
103
- tbody td { padding: 8px 10px; border-bottom: 1px solid #f3f4f6; vertical-align: middle; overflow: hidden; text-overflow: ellipsis; }
104
- tbody tr:hover { background: #f9fafb; }
105
- tbody tr.hidden { display: none; }
106
- .badge { display: inline-block; padding: 2px 10px; border-radius: 99px; font-size: 0.72rem; font-weight: 600; white-space: nowrap; text-transform: capitalize; }
107
- .type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; background: #f3f4f6; color: #6b7280; font-size: 0.72rem; }
108
- .tag { display: inline-block; padding: 1px 6px; border-radius: 3px; background: #ede9fe; color: #5b21b6; font-size: 0.65rem; margin: 1px 2px; }
109
- .tags-cell { max-width: 200px; overflow: hidden; }
110
- .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
111
- .spec-link { color: #4F46E5; text-decoration: none; font-weight: 500; }
112
- .spec-link:hover { text-decoration: underline; }
113
- .spec-title { font-weight: 500; }
114
- .report-link { text-decoration: none; font-size: 1rem; padding: 2px 3px; border-radius: 4px; transition: background 0.15s; }
115
- .report-link:hover { background: #f3f4f6; }
116
- .reports-cell { white-space: nowrap; text-align: center; }
117
- .footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid #e5e7eb; color: #9ca3af; font-size: 0.72rem; text-align: center; }
118
- .footer a { color: #4F46E5; text-decoration: none; }
119
- .empty { text-align: center; padding: 48px 20px; color: #9ca3af; }
120
- .pagination { display: flex; justify-content: center; align-items: center; gap: 6px; margin-top: 16px; flex-wrap: wrap; }
121
- .page-btn { padding: 5px 12px; border: 1px solid #d1d5db; border-radius: 6px; background: #fff; cursor: pointer; font-size: 0.78rem; color: #374151; transition: all 0.15s; min-width: 36px; text-align: center; }
122
- .page-btn:hover { border-color: #4F46E5; color: #4F46E5; }
123
- .page-btn.active { background: #4F46E5; color: #fff; border-color: #4F46E5; }
124
- .page-btn:disabled { opacity: 0.4; cursor: default; }
125
- .page-info { font-size: 0.78rem; color: #6b7280; margin: 0 8px; }
126
- @media print { body { padding: 0; max-width: 100%; } .toolbar, .lang-toggle { display: none; } }
127
- @media (max-width: 640px) { .metrics { grid-template-columns: repeat(2, 1fr); } .tags-cell { display: none; } th:nth-child(7), td:nth-child(7) { display: none; } }`;
128
- }
129
- // Inline JS script split into named sections to satisfy max-lines-per-function.
130
- const SCRIPT_INIT = `(function() {
131
- var rows = Array.from(document.querySelectorAll('#specs-body tr'));
132
- var search = document.getElementById('search');
133
- var filters = document.querySelectorAll('.filter-btn');
134
- var langBtns = document.querySelectorAll('.lang-btn');
135
- var pagination = document.getElementById('pagination');
136
- var activeFilter = 'all';
137
- var currentPage = 1;
138
- var PAGE_SIZE = 25;
139
- var currentLang = 'en';
140
- var i18n = {
141
- en: {
142
- status: { draft: 'Draft', review: 'Review', approved: 'Approved', implementing: 'Building', done: 'Done', discarded: 'Discarded' },
143
- risk: { low: 'low', medium: 'medium', high: 'high' },
144
- filter: { all: 'All', implementing: 'Building', approved: 'Approved', review: 'Review', draft: 'Draft', done: 'Done', discarded: 'Discarded' },
145
- of: 'of', prev: 'Prev', next: 'Next'
146
- },
147
- es: {
148
- status: { draft: 'Borrador', review: 'Revisión', approved: 'Aprobado', implementing: 'En progreso', done: 'Hecho', discarded: 'Descartado' },
149
- risk: { low: 'bajo', medium: 'medio', high: 'alto' },
150
- filter: { all: 'Todos', implementing: 'En progreso', approved: 'Aprobados', review: 'Revisión', draft: 'Borrador', done: 'Hechos', discarded: 'Descartados' },
151
- of: 'de', prev: 'Ant', next: 'Sig'
152
- }
153
- };
154
- function getVisibleRows() {
155
- var q = (search ? search.value : '').toLowerCase();
156
- return rows.filter(function(row) {
157
- var matchFilter = activeFilter === 'all' || row.dataset.status === activeFilter;
158
- var matchSearch = !q || (row.dataset.search || '').includes(q);
159
- return matchFilter && matchSearch;
160
- });
161
- }`;
162
- const SCRIPT_PAGINATION = `
163
- function renderPagination(visible) {
164
- if (!pagination) return;
165
- var totalPages = Math.max(1, Math.ceil(visible.length / PAGE_SIZE));
166
- if (currentPage > totalPages) currentPage = totalPages;
167
- var dict = i18n[currentLang] || i18n.en;
168
- var start = (currentPage - 1) * PAGE_SIZE + 1;
169
- var end = Math.min(currentPage * PAGE_SIZE, visible.length);
170
- if (visible.length === 0) { pagination.innerHTML = ''; return; }
171
- var html = '';
172
- html += '<button class="page-btn" data-page="prev"' + (currentPage === 1 ? ' disabled' : '') + '>' + dict.prev + '</button>';
173
- var pages = [];
174
- if (totalPages <= 7) {
175
- for (var i = 1; i <= totalPages; i++) pages.push(i);
176
- } else {
177
- pages.push(1);
178
- if (currentPage > 3) pages.push('...');
179
- for (var j = Math.max(2, currentPage - 1); j <= Math.min(totalPages - 1, currentPage + 1); j++) pages.push(j);
180
- if (currentPage < totalPages - 2) pages.push('...');
181
- pages.push(totalPages);
182
- }
183
- pages.forEach(function(p) {
184
- if (p === '...') { html += '<span class="page-info">…</span>'; }
185
- else { html += '<button class="page-btn' + (p === currentPage ? ' active' : '') + '" data-page="' + p + '">' + p + '</button>'; }
186
- });
187
- html += '<button class="page-btn" data-page="next"' + (currentPage === totalPages ? ' disabled' : '') + '>' + dict.next + '</button>';
188
- html += '<span class="page-info">' + start + '–' + end + ' ' + dict.of + ' ' + visible.length + '</span>';
189
- pagination.innerHTML = html;
190
- pagination.querySelectorAll('.page-btn').forEach(function(btn) {
191
- btn.addEventListener('click', function() {
192
- var p = btn.dataset.page;
193
- if (p === 'prev' && currentPage > 1) currentPage--;
194
- else if (p === 'next' && currentPage < totalPages) currentPage++;
195
- else if (p !== 'prev' && p !== 'next') currentPage = parseInt(p, 10);
196
- applyFilters();
197
- });
198
- });
199
- }
200
- function applyFilters() {
201
- var visible = getVisibleRows();
202
- rows.forEach(function(row) { row.classList.add('hidden'); });
203
- var start = (currentPage - 1) * PAGE_SIZE;
204
- var end = start + PAGE_SIZE;
205
- visible.forEach(function(row, idx) { if (idx >= start && idx < end) row.classList.remove('hidden'); });
206
- renderPagination(visible);
207
- }`;
208
- const SCRIPT_LANG_AND_BOOT = `
209
- function setLang(lang) {
210
- currentLang = lang;
211
- var dict = i18n[lang] || i18n.en;
212
- document.querySelectorAll('[data-i18n-status]').forEach(function(el) {
213
- var key = el.dataset.i18nStatus; if (dict.status[key]) el.textContent = dict.status[key];
214
- });
215
- document.querySelectorAll('[data-i18n-risk]').forEach(function(el) {
216
- var key = el.dataset.i18nRisk; if (dict.risk[key]) el.textContent = dict.risk[key];
217
- });
218
- document.querySelectorAll('.filter-btn[data-filter]').forEach(function(el) {
219
- var key = el.dataset.filter; if (dict.filter[key]) el.textContent = dict.filter[key];
220
- });
221
- document.querySelectorAll('[data-i18n-' + lang + ']').forEach(function(el) {
222
- el.textContent = el.getAttribute('data-i18n-' + lang);
223
- });
224
- var thMap = lang === 'es'
225
- ? { Spec: 'Spec', Status: 'Estado', Type: 'Tipo', Risk: 'Riesgo', Hours: 'Horas', Cost: 'Costo', Tags: 'Etiquetas', Reports: 'Informes' }
226
- : { Spec: 'Spec', Estado: 'Status', Tipo: 'Type', Riesgo: 'Risk', Horas: 'Hours', Costo: 'Cost', Etiquetas: 'Tags', Informes: 'Reports' };
227
- document.querySelectorAll('thead th').forEach(function(th) {
228
- if (thMap[th.textContent]) th.textContent = thMap[th.textContent];
229
- });
230
- if (search) search.placeholder = lang === 'es' ? 'Buscar specs\\u2026' : 'Search specs\\u2026';
231
- langBtns.forEach(function(b) { b.classList.toggle('active', b.dataset.lang === lang); });
232
- try { localStorage.setItem('planu-lang', lang); } catch(e) { /* localStorage unavailable in private browsing — ignore */ }
233
- applyFilters();
234
- }
235
- if (search) search.addEventListener('input', function() { currentPage = 1; applyFilters(); });
236
- filters.forEach(function(btn) {
237
- btn.addEventListener('click', function() {
238
- filters.forEach(function(b) { b.classList.remove('active'); });
239
- btn.classList.add('active');
240
- activeFilter = btn.dataset.filter || 'all';
241
- currentPage = 1; applyFilters();
242
- });
243
- });
244
- langBtns.forEach(function(btn) { btn.addEventListener('click', function() { setLang(btn.dataset.lang); }); });
245
- var saved = null;
246
- try { saved = localStorage.getItem('planu-lang'); } catch(e) { /* localStorage unavailable in private browsing — ignore */ }
247
- var autoLang = saved || (navigator.language.startsWith('es') ? 'es' : 'en');
248
- setLang(autoLang);
249
- })();`;
250
- export function getInlineScript() {
251
- return SCRIPT_INIT + SCRIPT_PAGINATION + SCRIPT_LANG_AND_BOOT;
252
- }
253
- function renderBody(specs, generatedAt) {
254
- const statusCounts = {};
255
- for (const s of specs) {
256
- statusCounts[s.status] = (statusCounts[s.status] ?? 0) + 1;
257
- }
258
- const discardedCount = statusCounts.discarded ?? 0;
259
- const activeSpecs = specs.filter((sp) => sp.status !== 'discarded');
260
- const totalSpecs = specs.length;
261
- const doneCount = statusCounts.done ?? 0;
262
- const implementingCount = statusCounts.implementing ?? 0;
263
- const totalDevHours = activeSpecs.reduce((s, sp) => s + sp.estimation.devHours, 0);
264
- const totalCost = activeSpecs.reduce((s, sp) => s + sp.estimation.totalCostUsd, 0);
265
- const sortedSpecs = sortSpecs(specs);
266
- const rows = sortedSpecs.map((s) => renderSpecRow(s)).join('\n');
267
- const metricsHtml = `<div class="metrics">
268
- ${renderMetricCard('Specs', 'Specs', String(totalSpecs), '#4F46E5')}
269
- ${renderMetricCard('Done', 'Hechos', String(doneCount), '#22c55e')}
270
- ${renderMetricCard('Building', 'En progreso', String(implementingCount), '#f59e0b')}
271
- ${discardedCount > 0 ? renderMetricCard('Discarded', 'Descartados', String(discardedCount), '#9ca3af') : ''}
272
- ${renderMetricCard('Dev Hours', 'Horas Dev', String(Math.round(totalDevHours * 10) / 10), '#6366f1')}
273
- ${renderMetricCard('Est. Cost', 'Costo Est.', '$' + String(Math.round(totalCost)), '#4F46E5')}
274
- </div>`;
275
- const activeTotal = totalSpecs - discardedCount;
276
- const tableHtml = totalSpecs === 0
277
- ? '<div class="empty"><p data-i18n-en="No specs yet. Create one with create_spec." data-i18n-es="No hay specs aún. Crea uno con create_spec.">No specs yet. Create one with <code>create_spec</code>.</p></div>'
278
- : `<div class="toolbar">
279
- <input type="text" class="search-input" placeholder="Search specs…" id="search">
280
- <button class="filter-btn active" data-filter="all">All</button>
281
- <button class="filter-btn" data-filter="implementing">Building</button>
282
- <button class="filter-btn" data-filter="approved">Approved</button>
283
- <button class="filter-btn" data-filter="review">Review</button>
284
- <button class="filter-btn" data-filter="draft">Draft</button>
285
- <button class="filter-btn" data-filter="done">Done</button>
286
- <button class="filter-btn" data-filter="discarded">Discarded</button>
287
- </div>
288
- <div class="table-wrap">
289
- <table>
290
- <thead><tr><th>Spec</th><th>Status</th><th>Type</th><th>Risk</th><th>Hours</th><th>Cost</th><th>Tags</th><th>Reports</th></tr></thead>
291
- <tbody id="specs-body">${rows}</tbody>
292
- </table>
293
- </div>
294
- <div class="pagination" id="pagination"></div>`;
295
- return `<div class="header-row">
296
- <h1>📋 Planu — Spec Dashboard</h1>
297
- <div class="lang-toggle">
298
- <button class="lang-btn" data-lang="en">EN</button>
299
- <button class="lang-btn" data-lang="es">ES</button>
300
- </div>
301
- </div>
302
- <p class="subtitle" data-i18n-en="Auto-generated by Planu · ${escapeHtml(generatedAt)}" data-i18n-es="Generado automáticamente por Planu · ${escapeHtml(generatedAt)}">Auto-generated by Planu · ${escapeHtml(generatedAt)}</p>
303
- ${metricsHtml}
304
- ${renderProgressBar(doneCount, activeTotal)}
305
- ${tableHtml}
306
- <div class="footer">Generated by <a href="https://planu.dev">Planu</a> — Spec Driven Development</div>`;
307
- }
308
- /** Generate the full dashboard HTML document. */
309
- export function generateDashboardHtml(specs, availablePages = []) {
310
- const generatedAt = new Date().toLocaleString('en-US', {
311
- dateStyle: 'medium',
312
- timeStyle: 'short',
313
- });
314
- // Dashboard is at planu/ root — basePath is empty string
315
- const navbar = buildNavbar('dashboard', '', availablePages);
316
- const quickLinks = buildQuickLinksSection(availablePages);
317
- return `<!DOCTYPE html>
318
- <html lang="en">
319
- <head>
320
- <meta charset="UTF-8">
321
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
322
- <title>Planu — Spec Dashboard</title>
323
- <style>${getPortalThemeCSS()}${getInlineCss()}</style>
324
- </head>
325
- <body>
326
- ${navbar}
327
- ${quickLinks}
328
- ${renderBody(specs, generatedAt)}
329
- <script>${getInlineScript()}</script>
330
- </body>
331
- </html>`;
332
- }
333
- //# sourceMappingURL=dashboard-renderer.js.map
@@ -1,5 +0,0 @@
1
- export { classifyIntent, heuristicClassify } from './classifier.js';
2
- export { routeToNextStep } from './router.js';
3
- export { runWithGuard } from './cost-guard.js';
4
- export type { CostGuardOpts, FallbackResult } from './cost-guard.js';
5
- //# sourceMappingURL=index.d.ts.map
@@ -1,5 +0,0 @@
1
- // engine/triagier/index.ts — SPEC-726: Public API for the Triagier engine
2
- export { classifyIntent, heuristicClassify } from './classifier.js';
3
- export { routeToNextStep } from './router.js';
4
- export { runWithGuard } from './cost-guard.js';
5
- //# sourceMappingURL=index.js.map
@@ -1,5 +0,0 @@
1
- export { UNIVERSAL_RULES } from './catalog.js';
2
- export { writeRuleForHost } from './host-writer.js';
3
- export { installUniversalRules } from './installer.js';
4
- export { hashContent, readManifest, isUserModified, upsertManifestEntry, } from './user-edit-detector.js';
5
- //# sourceMappingURL=index.d.ts.map
@@ -1,6 +0,0 @@
1
- // engine/universal-rules/index.ts — SPEC-779 public API barrel
2
- export { UNIVERSAL_RULES } from './catalog.js';
3
- export { writeRuleForHost } from './host-writer.js';
4
- export { installUniversalRules } from './installer.js';
5
- export { hashContent, readManifest, isUserModified, upsertManifestEntry, } from './user-edit-detector.js';
6
- //# sourceMappingURL=index.js.map
@@ -1,23 +0,0 @@
1
- export { computeRequestSha } from './signature.js';
2
- export type { AnthropicRequest } from './signature.js';
3
- export { loadCassetteFromDisk as loadCassette, saveCassetteToDisk, upsertCassetteEntry, getCassettePath, type Cassette, type CassetteEntry, } from './store.js';
4
- export { createInterceptor, isRecordingMode, CassetteMissError } from './intercept.js';
5
- export type { MessageCreateFn } from './intercept.js';
6
- import { type CassetteEntry } from './store.js';
7
- /**
8
- * withCassette — Higher-order helper for test cases.
9
- * Runs a test function within a cassette scope.
10
- * Actual interception must be installed by the caller via createInterceptor().
11
- *
12
- * @example
13
- * it('generates intake JSON', () => withCassette('handoff-artifacts/intake', async () => {
14
- * const result = await generateIntakeJson(spec);
15
- * expect(result).toMatchSnapshot();
16
- * }));
17
- */
18
- export declare function withCassette<T>(_cassetteName: string, fn: () => Promise<T>): Promise<T>;
19
- /**
20
- * recordCassette — Write a new entry to a cassette file (record mode only).
21
- */
22
- export declare function recordCassette(name: string, entry: CassetteEntry): void;
23
- //# sourceMappingURL=index.d.ts.map
@@ -1,26 +0,0 @@
1
- // testing/cassette/index.ts — Public API for LLM cassette/mock pattern (SPEC-743)
2
- export { computeRequestSha } from './signature.js';
3
- export { loadCassetteFromDisk as loadCassette, saveCassetteToDisk, upsertCassetteEntry, getCassettePath, } from './store.js';
4
- export { createInterceptor, isRecordingMode, CassetteMissError } from './intercept.js';
5
- import { upsertCassetteEntry } from './store.js';
6
- /**
7
- * withCassette — Higher-order helper for test cases.
8
- * Runs a test function within a cassette scope.
9
- * Actual interception must be installed by the caller via createInterceptor().
10
- *
11
- * @example
12
- * it('generates intake JSON', () => withCassette('handoff-artifacts/intake', async () => {
13
- * const result = await generateIntakeJson(spec);
14
- * expect(result).toMatchSnapshot();
15
- * }));
16
- */
17
- export async function withCassette(_cassetteName, fn) {
18
- return fn();
19
- }
20
- /**
21
- * recordCassette — Write a new entry to a cassette file (record mode only).
22
- */
23
- export function recordCassette(name, entry) {
24
- upsertCassetteEntry(name, entry);
25
- }
26
- //# sourceMappingURL=index.js.map
@@ -1,37 +0,0 @@
1
- import { z } from 'zod';
2
- export declare const ApplyDomainBundleInputSchema: {
3
- bundle: z.ZodEnum<{
4
- "react-native": "react-native";
5
- "rest-api": "rest-api";
6
- "stripe-payments": "stripe-payments";
7
- "auth-supabase": "auth-supabase";
8
- "nextjs-fullstack": "nextjs-fullstack";
9
- }>;
10
- projectPath: z.ZodString;
11
- overwrite: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
12
- };
13
- export declare const ListDomainBundlesInputSchema: {
14
- projectPath: z.ZodOptional<z.ZodString>;
15
- };
16
- interface ApplyDomainBundleArgs {
17
- bundle: string;
18
- projectPath: string;
19
- overwrite?: boolean;
20
- }
21
- interface ListDomainBundlesArgs {
22
- projectPath?: string;
23
- }
24
- export declare function handleApplyDomainBundle(args: ApplyDomainBundleArgs): Promise<{
25
- content: {
26
- type: 'text';
27
- text: string;
28
- }[];
29
- }>;
30
- export declare function handleListDomainBundles(_args: ListDomainBundlesArgs): Promise<{
31
- content: {
32
- type: 'text';
33
- text: string;
34
- }[];
35
- }>;
36
- export {};
37
- //# sourceMappingURL=domain-bundle-handler.d.ts.map
@@ -1,71 +0,0 @@
1
- // Planu — domain-bundle-handler.ts
2
- // Handlers for apply_domain_bundle and list_domain_bundles tools.
3
- import { z } from 'zod';
4
- import { installBundle, listBundlesAsync } from '../engine/bundle-installer.js';
5
- import { compactJson, compactResult } from './output-formatter.js';
6
- // ---------------------------------------------------------------------------
7
- // Zod schemas
8
- // ---------------------------------------------------------------------------
9
- export const ApplyDomainBundleInputSchema = {
10
- bundle: z
11
- .enum(['stripe-payments', 'auth-supabase', 'rest-api', 'nextjs-fullstack', 'react-native'])
12
- .describe('Bundle ID to install. Valid values: stripe-payments, auth-supabase, rest-api, nextjs-fullstack, react-native'),
13
- projectPath: z
14
- .string()
15
- .describe('Absolute path to the project where the bundle will be installed'),
16
- overwrite: z
17
- .boolean()
18
- .optional()
19
- .default(false)
20
- .describe('If true, overwrite existing files. Default: false (skip existing files)'),
21
- };
22
- export const ListDomainBundlesInputSchema = {
23
- projectPath: z
24
- .string()
25
- .optional()
26
- .describe('Optional project path (unused, included for consistency)'),
27
- };
28
- function formatInstallResult(result) {
29
- const lines = [`Bundle "${result.bundleId}" installation complete.`];
30
- if (result.installed.length > 0) {
31
- lines.push(`\nInstalled (${result.installed.length}):`);
32
- for (const path of result.installed) {
33
- lines.push(` + ${path}`);
34
- }
35
- }
36
- if (result.skipped.length > 0) {
37
- lines.push(`\nSkipped — already exist (${result.skipped.length}):`);
38
- for (const path of result.skipped) {
39
- lines.push(` ~ ${path}`);
40
- }
41
- }
42
- if (result.errors.length > 0) {
43
- lines.push(`\nErrors (${result.errors.length}):`);
44
- for (const err of result.errors) {
45
- lines.push(` ! ${err}`);
46
- }
47
- }
48
- return lines.join('\n');
49
- }
50
- export async function handleApplyDomainBundle(args) {
51
- const result = await installBundle(args.bundle, args.projectPath, {
52
- overwrite: args.overwrite ?? false,
53
- });
54
- const summary = formatInstallResult(result);
55
- const humanSummary = result.errors.length > 0
56
- ? `Bundle "${result.bundleId}" installed with ${result.errors.length} error(s). Check errors for details.`
57
- : `Bundle "${result.bundleId}" installed successfully: ${result.installed.length} file(s) added, ${result.skipped.length} skipped.`;
58
- return compactResult(compactJson({ ...result, summary, humanSummary }));
59
- }
60
- export async function handleListDomainBundles(_args) {
61
- const bundles = await listBundlesAsync();
62
- const items = bundles.map((b) => ({
63
- id: b.id,
64
- name: b.name,
65
- description: b.description,
66
- tags: b.tags,
67
- }));
68
- const humanSummary = `${items.length} domain bundles available: ${items.map((b) => b.name).join(', ')}.`;
69
- return compactResult(compactJson({ bundles: items, humanSummary }));
70
- }
71
- //# sourceMappingURL=domain-bundle-handler.js.map
@@ -1,5 +0,0 @@
1
- import type { ToolResult } from '../../types/index.js';
2
- export declare function handleGenerateFigmaRulesFile(input: {
3
- projectPath: string;
4
- }): Promise<ToolResult>;
5
- //# sourceMappingURL=rules-file.d.ts.map
@@ -1,45 +0,0 @@
1
- // tools/figma/rules-file.ts — SPEC-488: generate_figma_rules_file handler
2
- import { writeFile, mkdir } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { getFigmaConfig, getFigmaCodeConnect, getFigmaTokens } from '../../storage/figma-store.js';
5
- import { knowledgeStore } from '../../storage/index.js';
6
- import { hashProjectPath } from '../../storage/base-store.js';
7
- import { buildFigmaRulesFileContent } from '../../engine/figma/implementation-rules.js';
8
- import { FIGMA_NOT_CONNECTED } from './shared.js';
9
- export async function handleGenerateFigmaRulesFile(input) {
10
- const config = await getFigmaConfig(input.projectPath);
11
- if (config === null) {
12
- return FIGMA_NOT_CONNECTED;
13
- }
14
- const projectId = hashProjectPath(input.projectPath);
15
- const knowledge = await knowledgeStore.getKnowledge(projectId);
16
- const [codeConnectEntries, tokens] = await Promise.all([
17
- getFigmaCodeConnect(input.projectPath),
18
- getFigmaTokens(input.projectPath),
19
- ]);
20
- const content = buildFigmaRulesFileContent({
21
- codeConnectEntries,
22
- tokens,
23
- stack: knowledge?.stack ?? [],
24
- framework: knowledge?.framework ?? null,
25
- });
26
- const rulesDir = join(input.projectPath, '.claude', 'rules');
27
- const rulesPath = join(rulesDir, 'figma-design-system.md');
28
- await mkdir(rulesDir, { recursive: true });
29
- await writeFile(rulesPath, content, 'utf-8');
30
- const summary = [
31
- `✓ Created .claude/rules/figma-design-system.md`,
32
- ` ${String(codeConnectEntries.length)} Code Connect components`,
33
- ` ${String(tokens.length)} design tokens`,
34
- ` Stack: ${knowledge?.framework ?? knowledge?.stack.slice(0, 3).join(', ') ?? 'unknown'}`,
35
- ].join('\n');
36
- return {
37
- content: [{ type: 'text', text: summary }],
38
- structuredContent: {
39
- rulesPath,
40
- codeConnectCount: codeConnectEntries.length,
41
- tokenCount: tokens.length,
42
- },
43
- };
44
- }
45
- //# sourceMappingURL=rules-file.js.map
@@ -1,8 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import type { HealRunResult } from '../types/heal.js';
3
- export declare function healPlanuRoot(opts: {
4
- projectRoot: string;
5
- dryRun?: boolean;
6
- }): Promise<HealRunResult>;
7
- export declare function registerHealPlanuRootTool(server: McpServer): void;
8
- //# sourceMappingURL=heal-planu-root.d.ts.map