@promovaweb/specsfy 0.7.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +68 -32
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +155 -4
- package/dist/cli.js.map +1 -1
- package/dist/clickupfy.d.ts +16 -0
- package/dist/clickupfy.d.ts.map +1 -0
- package/dist/clickupfy.js +36 -0
- package/dist/clickupfy.js.map +1 -0
- package/dist/installer.d.ts +2 -2
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +18 -42
- package/dist/installer.js.map +1 -1
- package/dist/lifecycle.d.ts +40 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +254 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/milestones.d.ts +31 -0
- package/dist/milestones.d.ts.map +1 -0
- package/dist/milestones.js +148 -0
- package/dist/milestones.js.map +1 -0
- package/dist/prerequisites.d.ts +28 -0
- package/dist/prerequisites.d.ts.map +1 -0
- package/dist/prerequisites.js +112 -0
- package/dist/prerequisites.js.map +1 -0
- package/dist/progress.d.ts +2 -0
- package/dist/progress.d.ts.map +1 -1
- package/dist/progress.js +25 -28
- package/dist/progress.js.map +1 -1
- package/dist/tui.d.ts +29 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +259 -91
- package/dist/tui.js.map +1 -1
- package/dist/updater.d.ts +15 -3
- package/dist/updater.d.ts.map +1 -1
- package/dist/updater.js +147 -35
- package/dist/updater.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -5
package/dist/tui.js
CHANGED
|
@@ -14,7 +14,7 @@ import { resolvePath } from "./filesystem.js";
|
|
|
14
14
|
import { AUXILIARY_SKILLS, DOCUMENTATION_SKILLS, FRAMEWORK_SKILLS, SkillInstaller, } from "./installer.js";
|
|
15
15
|
import { scanSpecs, specsFingerprint, summarizeSpecs, } from "./progress.js";
|
|
16
16
|
import { runProjectTests } from "./project-testing.js";
|
|
17
|
-
import { ensureSkillsLock, skillsLockFingerprint
|
|
17
|
+
import { ensureSkillsLock, skillsLockFingerprint } from "./skill-lock.js";
|
|
18
18
|
import { VERSION } from "./version.js";
|
|
19
19
|
marked.use(markedTerminal({
|
|
20
20
|
reflowText: true,
|
|
@@ -87,6 +87,30 @@ export const TUI_TABS = [
|
|
|
87
87
|
{ id: "skills", label: "Skills", key: "C-k" },
|
|
88
88
|
{ id: "about", label: "Sobre", key: "C-o" },
|
|
89
89
|
];
|
|
90
|
+
/**
|
|
91
|
+
* Paleta semântica da TUI, derivada dos tokens oficiais do dark mode.
|
|
92
|
+
*
|
|
93
|
+
* Os pares de texto, seleção e foco mantêm contraste mensurável sem depender
|
|
94
|
+
* das cores configuráveis de 16 posições do terminal da pessoa usuária.
|
|
95
|
+
*/
|
|
96
|
+
export const TUI_THEME = {
|
|
97
|
+
background: "#000A0E",
|
|
98
|
+
surface: "#001117",
|
|
99
|
+
surfaceRaised: "#03212A",
|
|
100
|
+
text: "#F2F8F9",
|
|
101
|
+
textMuted: "#B2C6CE",
|
|
102
|
+
border: "#5F7D8C",
|
|
103
|
+
accent: "#C4B5FD",
|
|
104
|
+
activeBackground: "#5EEDE1",
|
|
105
|
+
activeText: "#001117",
|
|
106
|
+
selectedBackground: "#6D28D9",
|
|
107
|
+
selectedText: "#F2F8F9",
|
|
108
|
+
primaryBackground: "#15626A",
|
|
109
|
+
primaryText: "#F2F8F9",
|
|
110
|
+
focusBackground: "#5EEDE1",
|
|
111
|
+
focusText: "#001117",
|
|
112
|
+
warning: "#FCD34D",
|
|
113
|
+
};
|
|
90
114
|
/** Dashboard terminal principal. */
|
|
91
115
|
export class SpecsfyTui {
|
|
92
116
|
project;
|
|
@@ -95,6 +119,7 @@ export class SpecsfyTui {
|
|
|
95
119
|
#status;
|
|
96
120
|
#projectInput;
|
|
97
121
|
#modal;
|
|
122
|
+
#modalFocus;
|
|
98
123
|
#lastBackAt = 0;
|
|
99
124
|
#tabButtons = new Map();
|
|
100
125
|
#activeTab = "home";
|
|
@@ -105,6 +130,8 @@ export class SpecsfyTui {
|
|
|
105
130
|
#catalogError = "";
|
|
106
131
|
#installed = new Set();
|
|
107
132
|
#selected = new Set();
|
|
133
|
+
#selectionDirty = false;
|
|
134
|
+
#loaded = false;
|
|
108
135
|
#detected = new Set();
|
|
109
136
|
#skillFilter = "all";
|
|
110
137
|
#skillQuery = "";
|
|
@@ -112,10 +139,13 @@ export class SpecsfyTui {
|
|
|
112
139
|
#suppressSkillToggleUntil = 0;
|
|
113
140
|
#fingerprints = { specs: "", backlogs: "", lock: "" };
|
|
114
141
|
#poller;
|
|
142
|
+
#refreshPromise;
|
|
115
143
|
#testRunning = false;
|
|
116
144
|
#testResultTab = "summary";
|
|
117
145
|
#testSummary = "Nenhuma execução nesta sessão.";
|
|
118
146
|
#testOutput = [];
|
|
147
|
+
#testSummaryPanel;
|
|
148
|
+
#testOutputPanel;
|
|
119
149
|
constructor(project = process.cwd()) {
|
|
120
150
|
this.project = resolvePath(project);
|
|
121
151
|
}
|
|
@@ -134,6 +164,10 @@ export class SpecsfyTui {
|
|
|
134
164
|
});
|
|
135
165
|
await this.start({ screen });
|
|
136
166
|
await new Promise((resolve) => {
|
|
167
|
+
if (screenDestroyed(screen)) {
|
|
168
|
+
resolve();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
137
171
|
screen.once("destroy", () => {
|
|
138
172
|
resolve();
|
|
139
173
|
});
|
|
@@ -146,6 +180,9 @@ export class SpecsfyTui {
|
|
|
146
180
|
* terminais virtuais, sem manter um processo pendurado durante os testes.
|
|
147
181
|
*/
|
|
148
182
|
async start(options = {}) {
|
|
183
|
+
if (this.#screen && !screenDestroyed(this.#screen)) {
|
|
184
|
+
throw new Error("a TUI já foi iniciada nesta instância");
|
|
185
|
+
}
|
|
149
186
|
const screen = options.screen ??
|
|
150
187
|
blessed.screen({
|
|
151
188
|
smartCSR: true,
|
|
@@ -163,13 +200,20 @@ export class SpecsfyTui {
|
|
|
163
200
|
if (this.#poller)
|
|
164
201
|
clearInterval(this.#poller);
|
|
165
202
|
this.#poller = undefined;
|
|
203
|
+
this.#refreshPromise = undefined;
|
|
204
|
+
this.#modal = undefined;
|
|
205
|
+
this.#modalFocus = undefined;
|
|
166
206
|
});
|
|
167
207
|
this.showStatus("Carregando projeto…");
|
|
168
208
|
await this.refreshAll(!options.catalog);
|
|
169
209
|
this.showTab("home");
|
|
170
210
|
if (options.watch !== false) {
|
|
171
211
|
const config = await loadConfig(this.project);
|
|
172
|
-
|
|
212
|
+
if (screenDestroyed(screen))
|
|
213
|
+
return;
|
|
214
|
+
this.#poller = setInterval(() => void this.refreshIfChanged().catch((error) => {
|
|
215
|
+
this.showStatus(`Erro ao atualizar automaticamente: ${errorMessage(error)}`);
|
|
216
|
+
}), config.watch_interval * 1000);
|
|
173
217
|
}
|
|
174
218
|
}
|
|
175
219
|
mountShell(screen) {
|
|
@@ -181,7 +225,11 @@ export class SpecsfyTui {
|
|
|
181
225
|
height: 1,
|
|
182
226
|
align: "center",
|
|
183
227
|
content: "Specsfy — Dashboard de specs e skills",
|
|
184
|
-
style: {
|
|
228
|
+
style: {
|
|
229
|
+
fg: TUI_THEME.text,
|
|
230
|
+
bg: TUI_THEME.selectedBackground,
|
|
231
|
+
bold: true,
|
|
232
|
+
},
|
|
185
233
|
});
|
|
186
234
|
this.#projectInput = blessed.textbox({
|
|
187
235
|
parent: screen,
|
|
@@ -195,10 +243,10 @@ export class SpecsfyTui {
|
|
|
195
243
|
keys: true,
|
|
196
244
|
mouse: true,
|
|
197
245
|
style: {
|
|
198
|
-
fg:
|
|
199
|
-
bg:
|
|
200
|
-
border: { fg:
|
|
201
|
-
focus: { border: { fg:
|
|
246
|
+
fg: TUI_THEME.text,
|
|
247
|
+
bg: TUI_THEME.surface,
|
|
248
|
+
border: { fg: TUI_THEME.border },
|
|
249
|
+
focus: { border: { fg: TUI_THEME.focusBackground } },
|
|
202
250
|
},
|
|
203
251
|
});
|
|
204
252
|
this.#projectInput.on("submit", (value) => {
|
|
@@ -238,10 +286,14 @@ export class SpecsfyTui {
|
|
|
238
286
|
mouse: true,
|
|
239
287
|
keys: true,
|
|
240
288
|
style: {
|
|
241
|
-
fg:
|
|
242
|
-
bg:
|
|
243
|
-
focus: {
|
|
244
|
-
|
|
289
|
+
fg: TUI_THEME.textMuted,
|
|
290
|
+
bg: TUI_THEME.background,
|
|
291
|
+
focus: {
|
|
292
|
+
fg: TUI_THEME.focusText,
|
|
293
|
+
bg: TUI_THEME.focusBackground,
|
|
294
|
+
bold: true,
|
|
295
|
+
},
|
|
296
|
+
hover: { fg: TUI_THEME.focusText, bg: TUI_THEME.focusBackground },
|
|
245
297
|
},
|
|
246
298
|
});
|
|
247
299
|
button.on("press", () => this.showTab(id));
|
|
@@ -254,7 +306,7 @@ export class SpecsfyTui {
|
|
|
254
306
|
left: 0,
|
|
255
307
|
width: "100%",
|
|
256
308
|
orientation: "horizontal",
|
|
257
|
-
style: { fg:
|
|
309
|
+
style: { fg: TUI_THEME.border },
|
|
258
310
|
});
|
|
259
311
|
this.#body = blessed.box({
|
|
260
312
|
parent: screen,
|
|
@@ -262,7 +314,7 @@ export class SpecsfyTui {
|
|
|
262
314
|
left: 1,
|
|
263
315
|
right: 1,
|
|
264
316
|
bottom: 4,
|
|
265
|
-
style: { fg:
|
|
317
|
+
style: { fg: TUI_THEME.text, bg: TUI_THEME.background },
|
|
266
318
|
});
|
|
267
319
|
this.#status = blessed.box({
|
|
268
320
|
parent: screen,
|
|
@@ -271,7 +323,7 @@ export class SpecsfyTui {
|
|
|
271
323
|
width: "100%",
|
|
272
324
|
height: 1,
|
|
273
325
|
padding: { left: 1 },
|
|
274
|
-
style: { fg:
|
|
326
|
+
style: { fg: TUI_THEME.textMuted, bg: TUI_THEME.surface },
|
|
275
327
|
});
|
|
276
328
|
blessed.box({
|
|
277
329
|
parent: screen,
|
|
@@ -281,7 +333,7 @@ export class SpecsfyTui {
|
|
|
281
333
|
height: 1,
|
|
282
334
|
content: " ^ = Ctrl · Tab/Shift+Tab: foco · Setas: navegar · " +
|
|
283
335
|
"Espaço: abrir spec/alternar skill · Esc: voltar · Mouse: disponível",
|
|
284
|
-
style: { fg:
|
|
336
|
+
style: { fg: TUI_THEME.accent, bg: TUI_THEME.background },
|
|
285
337
|
});
|
|
286
338
|
blessed.box({
|
|
287
339
|
parent: screen,
|
|
@@ -290,7 +342,7 @@ export class SpecsfyTui {
|
|
|
290
342
|
width: "100%",
|
|
291
343
|
height: 1,
|
|
292
344
|
content: " ^Q Sair Esc Voltar",
|
|
293
|
-
style: { fg:
|
|
345
|
+
style: { fg: TUI_THEME.text, bg: TUI_THEME.surface, bold: true },
|
|
294
346
|
});
|
|
295
347
|
}
|
|
296
348
|
bindKeys(screen) {
|
|
@@ -339,12 +391,15 @@ export class SpecsfyTui {
|
|
|
339
391
|
if (!body || !this.#screen)
|
|
340
392
|
return;
|
|
341
393
|
for (const [id, button] of this.#tabButtons) {
|
|
342
|
-
button.style.bg =
|
|
343
|
-
|
|
394
|
+
button.style.bg =
|
|
395
|
+
id === tab ? TUI_THEME.activeBackground : TUI_THEME.background;
|
|
396
|
+
button.style.fg = id === tab ? TUI_THEME.activeText : TUI_THEME.textMuted;
|
|
344
397
|
button.style.bold = id === tab;
|
|
345
398
|
}
|
|
346
399
|
for (const child of [...body.children])
|
|
347
400
|
child.destroy();
|
|
401
|
+
this.#testSummaryPanel = undefined;
|
|
402
|
+
this.#testOutputPanel = undefined;
|
|
348
403
|
if (tab === "home")
|
|
349
404
|
this.renderHome(body);
|
|
350
405
|
else if (tab === "backlogs")
|
|
@@ -357,7 +412,24 @@ export class SpecsfyTui {
|
|
|
357
412
|
this.renderSkills(body);
|
|
358
413
|
else
|
|
359
414
|
this.renderAbout(body);
|
|
360
|
-
this
|
|
415
|
+
this.focusActiveTab();
|
|
416
|
+
if (!screenDestroyed(this.#screen))
|
|
417
|
+
this.#screen.render();
|
|
418
|
+
}
|
|
419
|
+
/** Recupera um alvo navegável quando um painel foi recriado ou fechado. */
|
|
420
|
+
focusActiveTab() {
|
|
421
|
+
const screen = this.#screen;
|
|
422
|
+
if (!screen || screenDestroyed(screen))
|
|
423
|
+
return;
|
|
424
|
+
const focused = screen.focused;
|
|
425
|
+
if (focused && !focused.detached && focused.visible)
|
|
426
|
+
return;
|
|
427
|
+
this.#tabButtons.get(this.#activeTab)?.focus();
|
|
428
|
+
}
|
|
429
|
+
/** Renderiza somente enquanto a tela ainda pertence ao ciclo atual. */
|
|
430
|
+
renderScreen() {
|
|
431
|
+
if (this.#screen && !screenDestroyed(this.#screen))
|
|
432
|
+
this.#screen.render();
|
|
361
433
|
}
|
|
362
434
|
/**
|
|
363
435
|
* Retorna um nível por vez: fecha modal, limpa busca e volta ao Home.
|
|
@@ -373,7 +445,13 @@ export class SpecsfyTui {
|
|
|
373
445
|
if (this.#modal) {
|
|
374
446
|
this.#modal.destroy();
|
|
375
447
|
this.#modal = undefined;
|
|
376
|
-
this.#
|
|
448
|
+
const focus = this.#modalFocus;
|
|
449
|
+
this.#modalFocus = undefined;
|
|
450
|
+
if (focus && !focus.detached && focus.visible)
|
|
451
|
+
focus.focus();
|
|
452
|
+
else
|
|
453
|
+
this.focusActiveTab();
|
|
454
|
+
this.renderScreen();
|
|
377
455
|
return;
|
|
378
456
|
}
|
|
379
457
|
if (this.#activeTab === "skills" && this.#skillQuery) {
|
|
@@ -387,24 +465,24 @@ export class SpecsfyTui {
|
|
|
387
465
|
renderHome(body) {
|
|
388
466
|
const summary = summarizeSpecs(this.#specs);
|
|
389
467
|
const cards = [
|
|
390
|
-
["Specs", String(summary.total_specs), "#
|
|
468
|
+
["Specs", String(summary.total_specs), "#021C26", "#5EEDE1"],
|
|
391
469
|
[
|
|
392
470
|
`Tarefas · ${summary.pending_tasks} pendentes`,
|
|
393
471
|
`${summary.completed_tasks}/${summary.total_tasks}`,
|
|
394
|
-
"#
|
|
395
|
-
"#
|
|
472
|
+
"#2E1065",
|
|
473
|
+
"#C4B5FD",
|
|
396
474
|
],
|
|
397
475
|
[
|
|
398
476
|
`Itens · ${summary.pending_items} pendentes`,
|
|
399
477
|
`${summary.completed_items}/${summary.total_items}`,
|
|
400
|
-
"#
|
|
401
|
-
"#
|
|
478
|
+
"#03212A",
|
|
479
|
+
"#37E1D0",
|
|
402
480
|
],
|
|
403
481
|
[
|
|
404
482
|
"",
|
|
405
483
|
`${summary.percent}%\n${progressBar(summary.percent, 16)}`,
|
|
406
|
-
"#
|
|
407
|
-
|
|
484
|
+
"#001117",
|
|
485
|
+
TUI_THEME.warning,
|
|
408
486
|
],
|
|
409
487
|
];
|
|
410
488
|
cards.forEach(([label, value, background, border], index) => {
|
|
@@ -419,7 +497,7 @@ export class SpecsfyTui {
|
|
|
419
497
|
valign: "middle",
|
|
420
498
|
content: `{bold}${value}{/bold}${label ? `\n${label}` : ""}`,
|
|
421
499
|
tags: true,
|
|
422
|
-
style: { fg:
|
|
500
|
+
style: { fg: TUI_THEME.text, bg: background, border: { fg: border } },
|
|
423
501
|
});
|
|
424
502
|
});
|
|
425
503
|
blessed.box({
|
|
@@ -433,7 +511,11 @@ export class SpecsfyTui {
|
|
|
433
511
|
valign: "middle",
|
|
434
512
|
content: "Visão consolidada do andamento das especificações. " +
|
|
435
513
|
"Os números são recalculados quando os arquivos mudam.",
|
|
436
|
-
style: {
|
|
514
|
+
style: {
|
|
515
|
+
fg: TUI_THEME.text,
|
|
516
|
+
bg: TUI_THEME.surface,
|
|
517
|
+
border: { fg: TUI_THEME.border },
|
|
518
|
+
},
|
|
437
519
|
});
|
|
438
520
|
}
|
|
439
521
|
renderBacklogs(body) {
|
|
@@ -451,8 +533,8 @@ export class SpecsfyTui {
|
|
|
451
533
|
keys: true,
|
|
452
534
|
vi: true,
|
|
453
535
|
mouse: true,
|
|
454
|
-
scrollbar:
|
|
455
|
-
style:
|
|
536
|
+
scrollbar: terminalScrollbar(),
|
|
537
|
+
style: focusablePanelStyle(),
|
|
456
538
|
});
|
|
457
539
|
const list = blessed.list({
|
|
458
540
|
parent: body,
|
|
@@ -468,18 +550,22 @@ export class SpecsfyTui {
|
|
|
468
550
|
mouse: true,
|
|
469
551
|
tags: false,
|
|
470
552
|
style: {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
553
|
+
...focusablePanelStyle(),
|
|
554
|
+
selected: {
|
|
555
|
+
fg: TUI_THEME.selectedText,
|
|
556
|
+
bg: TUI_THEME.selectedBackground,
|
|
557
|
+
bold: true,
|
|
558
|
+
},
|
|
559
|
+
item: { fg: TUI_THEME.text, bg: TUI_THEME.background },
|
|
475
560
|
},
|
|
476
561
|
});
|
|
477
562
|
const updatePreview = () => {
|
|
478
563
|
const item = this.#backlogs[selectedIndex(list)];
|
|
479
564
|
preview.setLabel(item ? ` ${item.identifier} · ${item.status} ` : " Nenhum backlog ");
|
|
480
|
-
preview.setContent(item
|
|
481
|
-
|
|
482
|
-
|
|
565
|
+
preview.setContent(item
|
|
566
|
+
? renderMarkdown(formatBacklogPreview(item.content))
|
|
567
|
+
: "Crie arquivos em specs/backlog/<NNNN>-<slug>.md para visualizá-los aqui.");
|
|
568
|
+
this.renderScreen();
|
|
483
569
|
};
|
|
484
570
|
list.on("select item", updatePreview);
|
|
485
571
|
list.on("keypress", updatePreview);
|
|
@@ -495,7 +581,7 @@ export class SpecsfyTui {
|
|
|
495
581
|
width: "100%",
|
|
496
582
|
height: "100%",
|
|
497
583
|
border: "line",
|
|
498
|
-
style: { border: { fg:
|
|
584
|
+
style: { border: { fg: TUI_THEME.border } },
|
|
499
585
|
});
|
|
500
586
|
blessed.box({
|
|
501
587
|
parent: panel,
|
|
@@ -504,7 +590,11 @@ export class SpecsfyTui {
|
|
|
504
590
|
width: "100%-2",
|
|
505
591
|
height: 1,
|
|
506
592
|
content: heading,
|
|
507
|
-
style: {
|
|
593
|
+
style: {
|
|
594
|
+
fg: TUI_THEME.activeText,
|
|
595
|
+
bg: TUI_THEME.activeBackground,
|
|
596
|
+
bold: true,
|
|
597
|
+
},
|
|
508
598
|
});
|
|
509
599
|
const list = blessed.list({
|
|
510
600
|
parent: panel,
|
|
@@ -522,8 +612,12 @@ export class SpecsfyTui {
|
|
|
522
612
|
vi: true,
|
|
523
613
|
mouse: true,
|
|
524
614
|
style: {
|
|
525
|
-
selected: {
|
|
526
|
-
|
|
615
|
+
selected: {
|
|
616
|
+
fg: TUI_THEME.selectedText,
|
|
617
|
+
bg: TUI_THEME.selectedBackground,
|
|
618
|
+
bold: true,
|
|
619
|
+
},
|
|
620
|
+
item: { fg: TUI_THEME.text, bg: TUI_THEME.background },
|
|
527
621
|
},
|
|
528
622
|
});
|
|
529
623
|
const open = () => {
|
|
@@ -558,10 +652,16 @@ export class SpecsfyTui {
|
|
|
558
652
|
vi: true,
|
|
559
653
|
mouse: true,
|
|
560
654
|
padding: { left: 1, right: 1 },
|
|
561
|
-
scrollbar:
|
|
562
|
-
style: {
|
|
655
|
+
scrollbar: terminalScrollbar(),
|
|
656
|
+
style: {
|
|
657
|
+
fg: TUI_THEME.text,
|
|
658
|
+
bg: TUI_THEME.background,
|
|
659
|
+
border: { fg: TUI_THEME.focusBackground },
|
|
660
|
+
},
|
|
563
661
|
});
|
|
662
|
+
this.#modalFocus = screen.focused;
|
|
564
663
|
this.#modal = modal;
|
|
664
|
+
screen.focusPush(modal);
|
|
565
665
|
modal.focus();
|
|
566
666
|
screen.render();
|
|
567
667
|
}
|
|
@@ -576,12 +676,7 @@ export class SpecsfyTui {
|
|
|
576
676
|
align: "center",
|
|
577
677
|
mouse: true,
|
|
578
678
|
keys: true,
|
|
579
|
-
style:
|
|
580
|
-
fg: "white",
|
|
581
|
-
bg: "blue",
|
|
582
|
-
focus: { bg: "cyan", fg: "black" },
|
|
583
|
-
hover: { bg: "cyan", fg: "black" },
|
|
584
|
-
},
|
|
679
|
+
style: buttonStyle(true),
|
|
585
680
|
});
|
|
586
681
|
button.on("press", () => void this.runTests());
|
|
587
682
|
blessed.box({
|
|
@@ -629,7 +724,7 @@ export class SpecsfyTui {
|
|
|
629
724
|
this.showTab("tests");
|
|
630
725
|
});
|
|
631
726
|
if (this.#testResultTab === "summary") {
|
|
632
|
-
blessed.box({
|
|
727
|
+
this.#testSummaryPanel = blessed.box({
|
|
633
728
|
parent: body,
|
|
634
729
|
top: 6,
|
|
635
730
|
left: 1,
|
|
@@ -643,14 +738,14 @@ export class SpecsfyTui {
|
|
|
643
738
|
vi: true,
|
|
644
739
|
mouse: true,
|
|
645
740
|
scrollable: true,
|
|
646
|
-
style:
|
|
741
|
+
style: focusablePanelStyle(),
|
|
647
742
|
});
|
|
648
743
|
}
|
|
649
744
|
else {
|
|
650
745
|
// O blessed.log agenda a rolagem depois de setContent e acessa widgets
|
|
651
746
|
// destruídos quando a saída ao vivo recria a aba. A caixa preserva a
|
|
652
747
|
// rolagem sem deixar callbacks pendentes entre duas renderizações.
|
|
653
|
-
blessed.box({
|
|
748
|
+
this.#testOutputPanel = blessed.box({
|
|
654
749
|
parent: body,
|
|
655
750
|
top: 6,
|
|
656
751
|
left: 1,
|
|
@@ -666,15 +761,15 @@ export class SpecsfyTui {
|
|
|
666
761
|
alwaysScroll: true,
|
|
667
762
|
tags: false,
|
|
668
763
|
content: this.#testOutput.join("\n"),
|
|
669
|
-
style:
|
|
764
|
+
style: focusablePanelStyle(),
|
|
670
765
|
});
|
|
671
766
|
}
|
|
672
767
|
button.focus();
|
|
673
768
|
}
|
|
674
769
|
async runTests() {
|
|
675
|
-
this
|
|
676
|
-
if (this.#testRunning)
|
|
770
|
+
if (this.#modal || this.#testRunning)
|
|
677
771
|
return;
|
|
772
|
+
this.showTab("tests");
|
|
678
773
|
this.#testRunning = true;
|
|
679
774
|
this.#testResultTab = "output";
|
|
680
775
|
this.#testOutput = [];
|
|
@@ -684,27 +779,32 @@ export class SpecsfyTui {
|
|
|
684
779
|
try {
|
|
685
780
|
const result = await runProjectTests(this.project, (line) => {
|
|
686
781
|
this.#testOutput.push(line);
|
|
782
|
+
this.#testOutputPanel?.setContent(this.#testOutput.join("\n"));
|
|
687
783
|
if (this.#activeTab === "tests" && this.#testResultTab === "output") {
|
|
688
|
-
this.
|
|
784
|
+
this.renderScreen();
|
|
689
785
|
}
|
|
690
786
|
});
|
|
691
787
|
this.#testSummary = formatTestRun(result);
|
|
788
|
+
this.#testSummaryPanel?.setContent(this.#testSummary);
|
|
692
789
|
this.#testResultTab = "summary";
|
|
693
790
|
this.showStatus(`${result.exit_code === 0 ? "Testes passaram" : "Testes falharam"} · ` +
|
|
694
791
|
`${result.duration_seconds.toFixed(2)}s · exit code ${result.exit_code}`);
|
|
695
|
-
this
|
|
792
|
+
if (this.#activeTab === "tests")
|
|
793
|
+
this.showTab("tests");
|
|
696
794
|
}
|
|
697
795
|
catch (error) {
|
|
698
796
|
const message = `Erro ao executar testes: ${errorMessage(error)}`;
|
|
699
797
|
this.#testSummary = message;
|
|
798
|
+
this.#testSummaryPanel?.setContent(message);
|
|
700
799
|
this.#testOutput.push(message);
|
|
701
800
|
this.#testResultTab = "summary";
|
|
702
801
|
this.showStatus(message);
|
|
703
|
-
this
|
|
802
|
+
if (this.#activeTab === "tests")
|
|
803
|
+
this.showTab("tests");
|
|
704
804
|
}
|
|
705
805
|
finally {
|
|
706
806
|
this.#testRunning = false;
|
|
707
|
-
this
|
|
807
|
+
this.renderScreen();
|
|
708
808
|
}
|
|
709
809
|
}
|
|
710
810
|
renderSkills(body) {
|
|
@@ -716,8 +816,10 @@ export class SpecsfyTui {
|
|
|
716
816
|
height: 1,
|
|
717
817
|
content: `${this.project}/skills-lock.json · ` +
|
|
718
818
|
`${this.#installed.size} skill(s) Specsfy instalada(s)` +
|
|
719
|
-
(this.#catalogError
|
|
720
|
-
|
|
819
|
+
(this.#catalogError
|
|
820
|
+
? ` · catálogo indisponível: ${this.#catalogError}`
|
|
821
|
+
: ""),
|
|
822
|
+
style: { fg: TUI_THEME.textMuted },
|
|
721
823
|
});
|
|
722
824
|
const search = blessed.textbox({
|
|
723
825
|
parent: body,
|
|
@@ -731,7 +833,7 @@ export class SpecsfyTui {
|
|
|
731
833
|
inputOnFocus: false,
|
|
732
834
|
keys: true,
|
|
733
835
|
mouse: true,
|
|
734
|
-
style:
|
|
836
|
+
style: focusablePanelStyle(),
|
|
735
837
|
});
|
|
736
838
|
search.on("submit", (value) => {
|
|
737
839
|
this.#skillQuery = String(value ?? "");
|
|
@@ -787,7 +889,7 @@ export class SpecsfyTui {
|
|
|
787
889
|
height: 1,
|
|
788
890
|
content: `${this.#selected.size} selecionada(s) · ${visible.length} visível(is) · ` +
|
|
789
891
|
`${toInstall} para instalar · ${toRemove} para remover`,
|
|
790
|
-
style: { fg:
|
|
892
|
+
style: { fg: TUI_THEME.text, bold: true },
|
|
791
893
|
});
|
|
792
894
|
const catalog = blessed.box({
|
|
793
895
|
parent: body,
|
|
@@ -811,10 +913,13 @@ export class SpecsfyTui {
|
|
|
811
913
|
vi: true,
|
|
812
914
|
mouse: true,
|
|
813
915
|
style: {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
916
|
+
...focusablePanelStyle(),
|
|
917
|
+
selected: {
|
|
918
|
+
fg: TUI_THEME.selectedText,
|
|
919
|
+
bg: TUI_THEME.selectedBackground,
|
|
920
|
+
bold: true,
|
|
921
|
+
},
|
|
922
|
+
item: { fg: TUI_THEME.text, bg: TUI_THEME.background },
|
|
818
923
|
},
|
|
819
924
|
});
|
|
820
925
|
const detail = blessed.box({
|
|
@@ -830,7 +935,7 @@ export class SpecsfyTui {
|
|
|
830
935
|
keys: true,
|
|
831
936
|
vi: true,
|
|
832
937
|
mouse: true,
|
|
833
|
-
style:
|
|
938
|
+
style: focusablePanelStyle(),
|
|
834
939
|
});
|
|
835
940
|
blessed.box({
|
|
836
941
|
parent: catalog,
|
|
@@ -839,7 +944,11 @@ export class SpecsfyTui {
|
|
|
839
944
|
width: "64%",
|
|
840
945
|
height: 1,
|
|
841
946
|
content: " PLANO SKILL CATEGORIA ESTADO ",
|
|
842
|
-
style: {
|
|
947
|
+
style: {
|
|
948
|
+
fg: TUI_THEME.activeText,
|
|
949
|
+
bg: TUI_THEME.activeBackground,
|
|
950
|
+
bold: true,
|
|
951
|
+
},
|
|
843
952
|
});
|
|
844
953
|
const updateDetail = () => {
|
|
845
954
|
const option = visible[selectedIndex(list)];
|
|
@@ -847,7 +956,7 @@ export class SpecsfyTui {
|
|
|
847
956
|
detail.setContent(option
|
|
848
957
|
? formatSkillDetail(option, this.#installed, this.#selected, this.#detected)
|
|
849
958
|
: "Nenhuma skill visível.\n\nAjuste a busca ou os filtros para continuar.");
|
|
850
|
-
this
|
|
959
|
+
this.renderScreen();
|
|
851
960
|
};
|
|
852
961
|
list.on("select item", updateDetail);
|
|
853
962
|
list.on("keypress", () => setImmediate(updateDetail));
|
|
@@ -899,7 +1008,18 @@ export class SpecsfyTui {
|
|
|
899
1008
|
"Ctrl+Q encerra a interface.",
|
|
900
1009
|
});
|
|
901
1010
|
}
|
|
902
|
-
|
|
1011
|
+
refreshAll(reloadCatalog) {
|
|
1012
|
+
if (this.#refreshPromise)
|
|
1013
|
+
return this.#refreshPromise;
|
|
1014
|
+
const promise = this.refreshAllInternal(reloadCatalog);
|
|
1015
|
+
const tracked = promise.finally(() => {
|
|
1016
|
+
if (this.#refreshPromise === tracked)
|
|
1017
|
+
this.#refreshPromise = undefined;
|
|
1018
|
+
});
|
|
1019
|
+
this.#refreshPromise = tracked;
|
|
1020
|
+
return tracked;
|
|
1021
|
+
}
|
|
1022
|
+
async refreshAllInternal(reloadCatalog) {
|
|
903
1023
|
try {
|
|
904
1024
|
const [specs, backlogs, lock] = await Promise.all([
|
|
905
1025
|
scanSpecs(this.project),
|
|
@@ -908,8 +1028,8 @@ export class SpecsfyTui {
|
|
|
908
1028
|
]);
|
|
909
1029
|
this.#specs = specs;
|
|
910
1030
|
this.#backlogs = backlogs;
|
|
911
|
-
|
|
912
|
-
this.#
|
|
1031
|
+
const installed = new Set(Object.keys(lock.skills).filter(isSpecsfySkill));
|
|
1032
|
+
this.#installed = installed;
|
|
913
1033
|
if (reloadCatalog || !this.#catalog) {
|
|
914
1034
|
try {
|
|
915
1035
|
this.#catalog = await Catalog.fetch();
|
|
@@ -917,10 +1037,14 @@ export class SpecsfyTui {
|
|
|
917
1037
|
this.#catalogError = "";
|
|
918
1038
|
}
|
|
919
1039
|
catch (error) {
|
|
920
|
-
this.#catalogEntries = [];
|
|
921
1040
|
this.#catalogError = errorMessage(error);
|
|
922
1041
|
}
|
|
923
1042
|
}
|
|
1043
|
+
const available = new Set(buildSkillOptions(this.#catalogEntries, installed).map((option) => option.name));
|
|
1044
|
+
this.#selected = this.#loaded && this.#selectionDirty
|
|
1045
|
+
? new Set([...this.#selected].filter((name) => available.has(name)))
|
|
1046
|
+
: new Set(installed);
|
|
1047
|
+
this.#loaded = true;
|
|
924
1048
|
this.#fingerprints = {
|
|
925
1049
|
specs: await specsFingerprint(this.project),
|
|
926
1050
|
backlogs: await backlogsFingerprint(this.project),
|
|
@@ -929,7 +1053,9 @@ export class SpecsfyTui {
|
|
|
929
1053
|
const summary = summarizeSpecs(specs);
|
|
930
1054
|
this.showStatus(`${this.project} · ${summary.completed_specs}/${summary.total_specs} specs completas · ` +
|
|
931
1055
|
"atualização automática ativa" +
|
|
932
|
-
(this.#catalogError
|
|
1056
|
+
(this.#catalogError
|
|
1057
|
+
? ` · catálogo indisponível: ${this.#catalogError}`
|
|
1058
|
+
: ""));
|
|
933
1059
|
this.showTab(this.#activeTab);
|
|
934
1060
|
}
|
|
935
1061
|
catch (error) {
|
|
@@ -960,20 +1086,26 @@ export class SpecsfyTui {
|
|
|
960
1086
|
}
|
|
961
1087
|
selectFramework() {
|
|
962
1088
|
this.#selected = new Set([...this.#selected, ...FRAMEWORK_SKILLS]);
|
|
1089
|
+
this.#selectionDirty = true;
|
|
963
1090
|
this.showStatus("As skills do framework foram selecionadas. Use ^A para aplicar.");
|
|
964
1091
|
this.showTab("skills");
|
|
965
1092
|
}
|
|
966
1093
|
async detectSkills() {
|
|
1094
|
+
if (this.#modal)
|
|
1095
|
+
return;
|
|
967
1096
|
this.showTab("skills");
|
|
1097
|
+
const startedOnSkills = this.#activeTab === "skills";
|
|
968
1098
|
try {
|
|
969
1099
|
this.#catalog ??= await Catalog.fetch();
|
|
970
1100
|
this.#catalogEntries = this.#catalog.entries;
|
|
971
1101
|
const entries = await this.#catalog.detect(this.project);
|
|
972
1102
|
this.#detected = new Set(entries.map((entry) => entry.name));
|
|
973
1103
|
this.#selected = new Set([...this.#selected, ...this.#detected]);
|
|
1104
|
+
this.#selectionDirty = true;
|
|
974
1105
|
this.#skillFilter = "detected";
|
|
975
1106
|
this.showStatus(`Skills detectadas e selecionadas: ${[...this.#detected].sort().join(", ") || "nenhuma skill"}`);
|
|
976
|
-
this
|
|
1107
|
+
if (startedOnSkills && this.#activeTab === "skills")
|
|
1108
|
+
this.showTab("skills");
|
|
977
1109
|
}
|
|
978
1110
|
catch (error) {
|
|
979
1111
|
this.showStatus(`Erro: ${errorMessage(error)}`);
|
|
@@ -990,21 +1122,26 @@ export class SpecsfyTui {
|
|
|
990
1122
|
else {
|
|
991
1123
|
this.#selected.add(this.#selectedSkill);
|
|
992
1124
|
}
|
|
1125
|
+
this.#selectionDirty = true;
|
|
993
1126
|
this.showTab("skills");
|
|
994
1127
|
}
|
|
995
1128
|
selectVisible() {
|
|
996
1129
|
this.#suppressSkillToggleUntil = Date.now() + 100;
|
|
997
1130
|
for (const option of this.visibleSkills())
|
|
998
1131
|
this.#selected.add(option.name);
|
|
1132
|
+
this.#selectionDirty = true;
|
|
999
1133
|
this.showTab("skills");
|
|
1000
1134
|
}
|
|
1001
1135
|
clearVisible() {
|
|
1002
1136
|
this.#suppressSkillToggleUntil = Date.now() + 100;
|
|
1003
1137
|
for (const option of this.visibleSkills())
|
|
1004
1138
|
this.#selected.delete(option.name);
|
|
1139
|
+
this.#selectionDirty = true;
|
|
1005
1140
|
this.showTab("skills");
|
|
1006
1141
|
}
|
|
1007
1142
|
async applySkills() {
|
|
1143
|
+
if (this.#modal)
|
|
1144
|
+
return;
|
|
1008
1145
|
this.showTab("skills");
|
|
1009
1146
|
try {
|
|
1010
1147
|
const selected = new Set([...this.#selected].filter(isSpecsfySkill));
|
|
@@ -1034,6 +1171,7 @@ export class SpecsfyTui {
|
|
|
1034
1171
|
}
|
|
1035
1172
|
if (toRemove.length)
|
|
1036
1173
|
changed.push(...(await installer.remove(toRemove.sort())));
|
|
1174
|
+
this.#selectionDirty = false;
|
|
1037
1175
|
await this.refreshAll(false);
|
|
1038
1176
|
this.showStatus(changed.length
|
|
1039
1177
|
? `Seleção aplicada: ${changed.length} alteração(ões).`
|
|
@@ -1044,6 +1182,8 @@ export class SpecsfyTui {
|
|
|
1044
1182
|
}
|
|
1045
1183
|
}
|
|
1046
1184
|
async updateSkills() {
|
|
1185
|
+
if (this.#modal)
|
|
1186
|
+
return;
|
|
1047
1187
|
this.showTab("skills");
|
|
1048
1188
|
try {
|
|
1049
1189
|
const changed = await (await SkillInstaller.create(this.project)).updateAll();
|
|
@@ -1058,7 +1198,7 @@ export class SpecsfyTui {
|
|
|
1058
1198
|
}
|
|
1059
1199
|
showStatus(message) {
|
|
1060
1200
|
this.#status?.setContent(message);
|
|
1061
|
-
this
|
|
1201
|
+
this.renderScreen();
|
|
1062
1202
|
}
|
|
1063
1203
|
}
|
|
1064
1204
|
/** Cria as opções base, especialistas e instalações fora do catálogo atual. */
|
|
@@ -1066,7 +1206,8 @@ export function buildSkillOptions(catalogEntries, installed) {
|
|
|
1066
1206
|
const options = FRAMEWORK_SKILLS.map((name) => ({
|
|
1067
1207
|
name,
|
|
1068
1208
|
description: BASE_DESCRIPTIONS[name] ?? "Skill do framework Specsfy.",
|
|
1069
|
-
kind: AUXILIARY_SKILLS.includes(name) ||
|
|
1209
|
+
kind: AUXILIARY_SKILLS.includes(name) ||
|
|
1210
|
+
name === "specsfy-setup"
|
|
1070
1211
|
? "auxiliary"
|
|
1071
1212
|
: DOCUMENTATION_SKILLS.includes(name)
|
|
1072
1213
|
? "documentation"
|
|
@@ -1152,9 +1293,7 @@ function friendlySkillName(name) {
|
|
|
1152
1293
|
};
|
|
1153
1294
|
if (baseNames[name])
|
|
1154
1295
|
return baseNames[name];
|
|
1155
|
-
return titleCase(name
|
|
1156
|
-
.replace(/^specsfy-(?:aux|specialist)-/u, "")
|
|
1157
|
-
.replaceAll("-", " "));
|
|
1296
|
+
return titleCase(name.replace(/^specsfy-(?:aux|specialist)-/u, "").replaceAll("-", " "));
|
|
1158
1297
|
}
|
|
1159
1298
|
function isSpecsfySkill(name) {
|
|
1160
1299
|
return (FRAMEWORK_SKILLS.includes(name) ||
|
|
@@ -1200,20 +1339,49 @@ function renderMarkdown(content) {
|
|
|
1200
1339
|
}
|
|
1201
1340
|
function buttonStyle(primary = false) {
|
|
1202
1341
|
return {
|
|
1203
|
-
fg:
|
|
1204
|
-
bg: primary ?
|
|
1205
|
-
border: { fg: primary ?
|
|
1206
|
-
focus: {
|
|
1207
|
-
|
|
1342
|
+
fg: primary ? TUI_THEME.primaryText : TUI_THEME.text,
|
|
1343
|
+
bg: primary ? TUI_THEME.primaryBackground : TUI_THEME.surface,
|
|
1344
|
+
border: { fg: primary ? TUI_THEME.focusBackground : TUI_THEME.border },
|
|
1345
|
+
focus: {
|
|
1346
|
+
fg: TUI_THEME.focusText,
|
|
1347
|
+
bg: TUI_THEME.focusBackground,
|
|
1348
|
+
border: { fg: TUI_THEME.focusBackground },
|
|
1349
|
+
},
|
|
1350
|
+
hover: {
|
|
1351
|
+
fg: TUI_THEME.focusText,
|
|
1352
|
+
bg: TUI_THEME.focusBackground,
|
|
1353
|
+
border: { fg: TUI_THEME.focusBackground },
|
|
1354
|
+
},
|
|
1208
1355
|
};
|
|
1209
1356
|
}
|
|
1210
1357
|
function tabStyle(active) {
|
|
1211
1358
|
return {
|
|
1212
|
-
fg: active ?
|
|
1213
|
-
bg: active ?
|
|
1359
|
+
fg: active ? TUI_THEME.activeText : TUI_THEME.textMuted,
|
|
1360
|
+
bg: active ? TUI_THEME.activeBackground : TUI_THEME.background,
|
|
1214
1361
|
bold: active,
|
|
1215
|
-
focus: { fg:
|
|
1216
|
-
hover: { fg:
|
|
1362
|
+
focus: { fg: TUI_THEME.focusText, bg: TUI_THEME.focusBackground },
|
|
1363
|
+
hover: { fg: TUI_THEME.focusText, bg: TUI_THEME.focusBackground },
|
|
1217
1364
|
};
|
|
1218
1365
|
}
|
|
1366
|
+
/** Mantém borda e foco legíveis em painéis, inputs e regiões roláveis. */
|
|
1367
|
+
function focusablePanelStyle() {
|
|
1368
|
+
return {
|
|
1369
|
+
fg: TUI_THEME.text,
|
|
1370
|
+
bg: TUI_THEME.background,
|
|
1371
|
+
border: { fg: TUI_THEME.border },
|
|
1372
|
+
focus: { border: { fg: TUI_THEME.focusBackground } },
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
/** Usa trilho discreto e indicador turquesa na rolagem dos painéis. */
|
|
1376
|
+
function terminalScrollbar() {
|
|
1377
|
+
return {
|
|
1378
|
+
ch: " ",
|
|
1379
|
+
track: { bg: TUI_THEME.surfaceRaised },
|
|
1380
|
+
style: { bg: TUI_THEME.focusBackground },
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
/** Detecta o estado encerrado sem depender da tipagem incompleta do blessed. */
|
|
1384
|
+
function screenDestroyed(screen) {
|
|
1385
|
+
return Boolean(screen.destroyed);
|
|
1386
|
+
}
|
|
1219
1387
|
//# sourceMappingURL=tui.js.map
|