sequant 1.12.0 → 1.13.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/README.md +10 -8
- package/dist/bin/cli.js +19 -9
- package/dist/src/commands/doctor.js +42 -20
- package/dist/src/commands/init.js +152 -65
- package/dist/src/commands/logs.js +7 -6
- package/dist/src/commands/run.d.ts +13 -1
- package/dist/src/commands/run.js +122 -32
- package/dist/src/commands/stats.js +67 -48
- package/dist/src/commands/status.js +30 -12
- package/dist/src/commands/sync.d.ts +28 -0
- package/dist/src/commands/sync.js +102 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +4 -0
- package/dist/src/lib/cli-ui.d.ts +196 -0
- package/dist/src/lib/cli-ui.js +544 -0
- package/dist/src/lib/content-analyzer.d.ts +89 -0
- package/dist/src/lib/content-analyzer.js +437 -0
- package/dist/src/lib/phase-signal.d.ts +94 -0
- package/dist/src/lib/phase-signal.js +171 -0
- package/dist/src/lib/phase-spinner.d.ts +146 -0
- package/dist/src/lib/phase-spinner.js +255 -0
- package/dist/src/lib/solve-comment-parser.d.ts +84 -0
- package/dist/src/lib/solve-comment-parser.js +200 -0
- package/dist/src/lib/stack-config.d.ts +51 -0
- package/dist/src/lib/stack-config.js +77 -0
- package/dist/src/lib/stacks.d.ts +52 -0
- package/dist/src/lib/stacks.js +173 -0
- package/dist/src/lib/templates.d.ts +2 -0
- package/dist/src/lib/templates.js +9 -2
- package/dist/src/lib/upstream/assessment.d.ts +70 -0
- package/dist/src/lib/upstream/assessment.js +385 -0
- package/dist/src/lib/upstream/index.d.ts +11 -0
- package/dist/src/lib/upstream/index.js +14 -0
- package/dist/src/lib/upstream/issues.d.ts +38 -0
- package/dist/src/lib/upstream/issues.js +267 -0
- package/dist/src/lib/upstream/relevance.d.ts +50 -0
- package/dist/src/lib/upstream/relevance.js +209 -0
- package/dist/src/lib/upstream/report.d.ts +29 -0
- package/dist/src/lib/upstream/report.js +391 -0
- package/dist/src/lib/upstream/types.d.ts +207 -0
- package/dist/src/lib/upstream/types.js +5 -0
- package/dist/src/lib/workflow/log-writer.d.ts +1 -1
- package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
- package/dist/src/lib/workflow/qa-cache.d.ts +199 -0
- package/dist/src/lib/workflow/qa-cache.js +440 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +34 -6
- package/dist/src/lib/workflow/run-log-schema.js +12 -1
- package/dist/src/lib/workflow/state-schema.d.ts +4 -4
- package/dist/src/lib/workflow/types.d.ts +4 -0
- package/package.json +6 -1
- package/templates/skills/qa/scripts/quality-checks.sh +509 -53
- package/templates/skills/solve/SKILL.md +375 -83
- package/templates/skills/spec/SKILL.md +107 -5
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized CLI UI module for Sequant
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent styling, spinners, boxes, tables, and branding
|
|
5
|
+
* with graceful fallbacks for non-TTY, CI, and legacy terminals.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import boxen from "boxen";
|
|
10
|
+
// cli-table3 uses CommonJS `export =` syntax, default import works with esModuleInterop
|
|
11
|
+
import Table from "cli-table3";
|
|
12
|
+
import gradient from "gradient-string";
|
|
13
|
+
import { isCI, isStdoutTTY } from "./tty.js";
|
|
14
|
+
/**
|
|
15
|
+
* Global UI configuration state
|
|
16
|
+
*/
|
|
17
|
+
let config = {
|
|
18
|
+
noColor: false,
|
|
19
|
+
jsonMode: false,
|
|
20
|
+
verbose: false,
|
|
21
|
+
isTTY: isStdoutTTY(),
|
|
22
|
+
isCI: isCI(),
|
|
23
|
+
minimal: false,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Configure UI settings
|
|
27
|
+
*
|
|
28
|
+
* Call this early in CLI startup to set global options.
|
|
29
|
+
*/
|
|
30
|
+
export function configureUI(options) {
|
|
31
|
+
config = { ...config, ...options };
|
|
32
|
+
// Check environment variables
|
|
33
|
+
if (process.env.NO_COLOR || process.env.SEQUANT_MINIMAL === "1") {
|
|
34
|
+
config.noColor = !!process.env.NO_COLOR;
|
|
35
|
+
config.minimal = process.env.SEQUANT_MINIMAL === "1";
|
|
36
|
+
}
|
|
37
|
+
// Auto-configure for CI
|
|
38
|
+
if (config.isCI && !options.minimal) {
|
|
39
|
+
config.minimal = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get current UI configuration
|
|
44
|
+
*/
|
|
45
|
+
export function getUIConfig() {
|
|
46
|
+
return { ...config };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if decorative output should be shown
|
|
50
|
+
*/
|
|
51
|
+
function shouldShowDecorative() {
|
|
52
|
+
return !config.jsonMode && !config.minimal && config.isTTY;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if animations should be used
|
|
56
|
+
*/
|
|
57
|
+
function shouldAnimate() {
|
|
58
|
+
return (!config.jsonMode &&
|
|
59
|
+
!config.verbose &&
|
|
60
|
+
!config.isCI &&
|
|
61
|
+
config.isTTY &&
|
|
62
|
+
!config.noColor);
|
|
63
|
+
}
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Color Palette
|
|
66
|
+
// ============================================================================
|
|
67
|
+
/**
|
|
68
|
+
* Standardized color palette for consistent styling
|
|
69
|
+
*/
|
|
70
|
+
export const colors = {
|
|
71
|
+
// Semantic colors
|
|
72
|
+
success: config.noColor ? (s) => s : chalk.green,
|
|
73
|
+
error: config.noColor ? (s) => s : chalk.red,
|
|
74
|
+
warning: config.noColor ? (s) => s : chalk.yellow,
|
|
75
|
+
info: config.noColor ? (s) => s : chalk.blue,
|
|
76
|
+
muted: config.noColor ? (s) => s : chalk.gray,
|
|
77
|
+
// UI elements
|
|
78
|
+
header: config.noColor ? (s) => s : chalk.blue.bold,
|
|
79
|
+
label: config.noColor ? (s) => s : chalk.cyan,
|
|
80
|
+
value: config.noColor ? (s) => s : chalk.white,
|
|
81
|
+
accent: config.noColor ? (s) => s : chalk.cyan,
|
|
82
|
+
bold: config.noColor ? (s) => s : chalk.bold,
|
|
83
|
+
// Status colors
|
|
84
|
+
pending: config.noColor ? (s) => s : chalk.gray,
|
|
85
|
+
running: config.noColor ? (s) => s : chalk.cyan,
|
|
86
|
+
completed: config.noColor ? (s) => s : chalk.green,
|
|
87
|
+
failed: config.noColor ? (s) => s : chalk.red,
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Get color function respecting current config
|
|
91
|
+
*/
|
|
92
|
+
function getColor(colorFn) {
|
|
93
|
+
return config.noColor ? (s) => s : colorFn;
|
|
94
|
+
}
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Windows Compatibility
|
|
97
|
+
// ============================================================================
|
|
98
|
+
/**
|
|
99
|
+
* Detect if running on legacy Windows terminal
|
|
100
|
+
*/
|
|
101
|
+
function isLegacyWindows() {
|
|
102
|
+
return (process.platform === "win32" &&
|
|
103
|
+
!process.env.WT_SESSION && // Not Windows Terminal
|
|
104
|
+
!process.env.TERM_PROGRAM // Not VS Code terminal
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// ASCII Logo (Static - No figlet)
|
|
109
|
+
// ============================================================================
|
|
110
|
+
/**
|
|
111
|
+
* Static ASCII logo for SEQUANT branding
|
|
112
|
+
* Pre-generated to avoid ~1MB figlet font bundle
|
|
113
|
+
*/
|
|
114
|
+
const ASCII_LOGO = `
|
|
115
|
+
_____ _____ _____ _ _ _____ _ _ _____
|
|
116
|
+
/ ___/ ___| _ | | | | _ | \\ | |_ _|
|
|
117
|
+
\\ \`--.\\ \`--.| | | | | | | | | | \\| | | |
|
|
118
|
+
\`--. \\\`--. \\ \\_/ / | | \\_| |_| . \` | | |
|
|
119
|
+
/\\__/ /\\__/ /\\___/\\ |_| |\\___/\\_|\\_/ \\_/
|
|
120
|
+
\\____/\\____/ \\___/
|
|
121
|
+
`.trimStart();
|
|
122
|
+
/**
|
|
123
|
+
* Get the ASCII logo with optional gradient
|
|
124
|
+
*/
|
|
125
|
+
export function logo() {
|
|
126
|
+
if (config.jsonMode || config.minimal)
|
|
127
|
+
return "";
|
|
128
|
+
if (config.noColor || !config.isTTY)
|
|
129
|
+
return ASCII_LOGO;
|
|
130
|
+
// Apply gradient for color terminals
|
|
131
|
+
const sequantGradient = gradient(["#00D4FF", "#7B68EE", "#FF6B9D"]);
|
|
132
|
+
return sequantGradient(ASCII_LOGO);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get the banner (logo + tagline)
|
|
136
|
+
*/
|
|
137
|
+
export function banner() {
|
|
138
|
+
if (config.jsonMode || config.minimal)
|
|
139
|
+
return "";
|
|
140
|
+
const logoText = logo();
|
|
141
|
+
const tagline = config.noColor
|
|
142
|
+
? " Quantize your development workflow"
|
|
143
|
+
: chalk.gray(" Quantize your development workflow");
|
|
144
|
+
return `${logoText}\n${tagline}\n`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Text-only spinner for verbose mode / non-TTY
|
|
148
|
+
*/
|
|
149
|
+
class TextSpinner {
|
|
150
|
+
text;
|
|
151
|
+
isSpinning = false;
|
|
152
|
+
constructor(initialText) {
|
|
153
|
+
this.text = initialText;
|
|
154
|
+
}
|
|
155
|
+
start(text) {
|
|
156
|
+
if (text)
|
|
157
|
+
this.text = text;
|
|
158
|
+
this.isSpinning = true;
|
|
159
|
+
if (!config.jsonMode) {
|
|
160
|
+
console.log(getColor(chalk.cyan)(`\u23F3 ${this.text}`));
|
|
161
|
+
}
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
succeed(text) {
|
|
165
|
+
if (text)
|
|
166
|
+
this.text = text;
|
|
167
|
+
this.isSpinning = false;
|
|
168
|
+
if (!config.jsonMode) {
|
|
169
|
+
console.log(getColor(chalk.green)(`\u2713 ${this.text}`));
|
|
170
|
+
}
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
fail(text) {
|
|
174
|
+
if (text)
|
|
175
|
+
this.text = text;
|
|
176
|
+
this.isSpinning = false;
|
|
177
|
+
if (!config.jsonMode) {
|
|
178
|
+
console.log(getColor(chalk.red)(`\u2717 ${this.text}`));
|
|
179
|
+
}
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
warn(text) {
|
|
183
|
+
if (text)
|
|
184
|
+
this.text = text;
|
|
185
|
+
this.isSpinning = false;
|
|
186
|
+
if (!config.jsonMode) {
|
|
187
|
+
console.log(getColor(chalk.yellow)(`\u26A0 ${this.text}`));
|
|
188
|
+
}
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
stop() {
|
|
192
|
+
this.isSpinning = false;
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Animated spinner wrapper
|
|
198
|
+
*/
|
|
199
|
+
class AnimatedSpinner {
|
|
200
|
+
spinner;
|
|
201
|
+
constructor(initialText) {
|
|
202
|
+
this.spinner = ora({
|
|
203
|
+
text: initialText,
|
|
204
|
+
color: "cyan",
|
|
205
|
+
spinner: "dots",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
get text() {
|
|
209
|
+
return this.spinner.text;
|
|
210
|
+
}
|
|
211
|
+
set text(value) {
|
|
212
|
+
this.spinner.text = value;
|
|
213
|
+
}
|
|
214
|
+
get isSpinning() {
|
|
215
|
+
return this.spinner.isSpinning;
|
|
216
|
+
}
|
|
217
|
+
start(text) {
|
|
218
|
+
if (text)
|
|
219
|
+
this.spinner.text = text;
|
|
220
|
+
this.spinner.start();
|
|
221
|
+
return this;
|
|
222
|
+
}
|
|
223
|
+
succeed(text) {
|
|
224
|
+
this.spinner.succeed(text);
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
fail(text) {
|
|
228
|
+
this.spinner.fail(text);
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
warn(text) {
|
|
232
|
+
this.spinner.warn(text);
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
stop() {
|
|
236
|
+
this.spinner.stop();
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Create a spinner with automatic fallback
|
|
242
|
+
*
|
|
243
|
+
* Returns an animated spinner for TTY, text-only for non-TTY/verbose/CI.
|
|
244
|
+
*/
|
|
245
|
+
export function spinner(text) {
|
|
246
|
+
if (shouldAnimate()) {
|
|
247
|
+
return new AnimatedSpinner(text);
|
|
248
|
+
}
|
|
249
|
+
return new TextSpinner(text);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get boxen options for a given style
|
|
253
|
+
*/
|
|
254
|
+
function getBoxOptions(style) {
|
|
255
|
+
const baseOptions = {
|
|
256
|
+
padding: 1,
|
|
257
|
+
borderStyle: isLegacyWindows() ? "classic" : "round",
|
|
258
|
+
};
|
|
259
|
+
switch (style) {
|
|
260
|
+
case "success":
|
|
261
|
+
return {
|
|
262
|
+
...baseOptions,
|
|
263
|
+
borderColor: config.noColor ? undefined : "green",
|
|
264
|
+
};
|
|
265
|
+
case "error":
|
|
266
|
+
return {
|
|
267
|
+
...baseOptions,
|
|
268
|
+
borderColor: config.noColor ? undefined : "red",
|
|
269
|
+
};
|
|
270
|
+
case "warning":
|
|
271
|
+
return {
|
|
272
|
+
...baseOptions,
|
|
273
|
+
borderColor: config.noColor ? undefined : "yellow",
|
|
274
|
+
};
|
|
275
|
+
case "info":
|
|
276
|
+
return {
|
|
277
|
+
...baseOptions,
|
|
278
|
+
borderColor: config.noColor ? undefined : "blue",
|
|
279
|
+
};
|
|
280
|
+
case "header":
|
|
281
|
+
return {
|
|
282
|
+
...baseOptions,
|
|
283
|
+
borderColor: config.noColor ? undefined : "cyan",
|
|
284
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
285
|
+
};
|
|
286
|
+
default:
|
|
287
|
+
return baseOptions;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Create a boxed message
|
|
292
|
+
*/
|
|
293
|
+
export function box(content, style = "default") {
|
|
294
|
+
if (config.jsonMode)
|
|
295
|
+
return "";
|
|
296
|
+
if (!shouldShowDecorative()) {
|
|
297
|
+
// Fallback to simple bordered output
|
|
298
|
+
return content;
|
|
299
|
+
}
|
|
300
|
+
return boxen(content, getBoxOptions(style));
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Create a success box with title and message
|
|
304
|
+
*/
|
|
305
|
+
export function successBox(title, message) {
|
|
306
|
+
const content = `${getColor(chalk.green.bold)(`\u2705 ${title}`)}\n\n${message}`;
|
|
307
|
+
return box(content, "success");
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Create an error box with title and message
|
|
311
|
+
*/
|
|
312
|
+
export function errorBox(title, message) {
|
|
313
|
+
const content = `${getColor(chalk.red.bold)(`\u274C ${title}`)}\n\n${message}`;
|
|
314
|
+
return box(content, "error");
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Create a warning box with title and message
|
|
318
|
+
*/
|
|
319
|
+
export function warningBox(title, message) {
|
|
320
|
+
const content = `${getColor(chalk.yellow.bold)(`\u26A0\uFE0F ${title}`)}\n\n${message}`;
|
|
321
|
+
return box(content, "warning");
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Create a header box
|
|
325
|
+
*/
|
|
326
|
+
export function headerBox(title) {
|
|
327
|
+
const content = getColor(chalk.bold)(title);
|
|
328
|
+
return box(content, "header");
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Create a formatted table
|
|
332
|
+
*/
|
|
333
|
+
export function table(rows, options) {
|
|
334
|
+
if (config.jsonMode)
|
|
335
|
+
return "";
|
|
336
|
+
const tableInstance = new Table({
|
|
337
|
+
head: options.columns.map((col) => config.noColor ? col.header : chalk.cyan.bold(col.header)),
|
|
338
|
+
colWidths: options.columns.map((col) => col.width ?? null),
|
|
339
|
+
colAligns: options.columns.map((col) => col.align || "left"),
|
|
340
|
+
style: {
|
|
341
|
+
head: [],
|
|
342
|
+
border: config.noColor ? [] : ["gray"],
|
|
343
|
+
},
|
|
344
|
+
chars: isLegacyWindows()
|
|
345
|
+
? {
|
|
346
|
+
top: "-",
|
|
347
|
+
"top-mid": "+",
|
|
348
|
+
"top-left": "+",
|
|
349
|
+
"top-right": "+",
|
|
350
|
+
bottom: "-",
|
|
351
|
+
"bottom-mid": "+",
|
|
352
|
+
"bottom-left": "+",
|
|
353
|
+
"bottom-right": "+",
|
|
354
|
+
left: "|",
|
|
355
|
+
"left-mid": "+",
|
|
356
|
+
mid: "-",
|
|
357
|
+
"mid-mid": "+",
|
|
358
|
+
right: "|",
|
|
359
|
+
"right-mid": "+",
|
|
360
|
+
middle: "|",
|
|
361
|
+
}
|
|
362
|
+
: undefined,
|
|
363
|
+
});
|
|
364
|
+
for (const row of rows) {
|
|
365
|
+
tableInstance.push(row.map(String));
|
|
366
|
+
}
|
|
367
|
+
return tableInstance.toString();
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Create a simple key-value table
|
|
371
|
+
*/
|
|
372
|
+
export function keyValueTable(data) {
|
|
373
|
+
if (config.jsonMode)
|
|
374
|
+
return "";
|
|
375
|
+
const rows = Object.entries(data).map(([key, value]) => [
|
|
376
|
+
config.noColor ? key : chalk.cyan(key),
|
|
377
|
+
String(value),
|
|
378
|
+
]);
|
|
379
|
+
const tableInstance = new Table({
|
|
380
|
+
style: {
|
|
381
|
+
head: [],
|
|
382
|
+
border: config.noColor ? [] : ["gray"],
|
|
383
|
+
},
|
|
384
|
+
chars: isLegacyWindows()
|
|
385
|
+
? {
|
|
386
|
+
top: "-",
|
|
387
|
+
"top-mid": "+",
|
|
388
|
+
"top-left": "+",
|
|
389
|
+
"top-right": "+",
|
|
390
|
+
bottom: "-",
|
|
391
|
+
"bottom-mid": "+",
|
|
392
|
+
"bottom-left": "+",
|
|
393
|
+
"bottom-right": "+",
|
|
394
|
+
left: "|",
|
|
395
|
+
"left-mid": "+",
|
|
396
|
+
mid: "-",
|
|
397
|
+
"mid-mid": "+",
|
|
398
|
+
right: "|",
|
|
399
|
+
"right-mid": "+",
|
|
400
|
+
middle: "|",
|
|
401
|
+
}
|
|
402
|
+
: undefined,
|
|
403
|
+
});
|
|
404
|
+
for (const row of rows) {
|
|
405
|
+
tableInstance.push(row);
|
|
406
|
+
}
|
|
407
|
+
return tableInstance.toString();
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Get a status icon
|
|
411
|
+
*/
|
|
412
|
+
export function statusIcon(type) {
|
|
413
|
+
if (config.noColor) {
|
|
414
|
+
switch (type) {
|
|
415
|
+
case "success":
|
|
416
|
+
return "[OK]";
|
|
417
|
+
case "error":
|
|
418
|
+
return "[FAIL]";
|
|
419
|
+
case "warning":
|
|
420
|
+
return "[WARN]";
|
|
421
|
+
case "pending":
|
|
422
|
+
return "[ ]";
|
|
423
|
+
case "running":
|
|
424
|
+
return "[..]";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
switch (type) {
|
|
428
|
+
case "success":
|
|
429
|
+
return chalk.green("\u2713");
|
|
430
|
+
case "error":
|
|
431
|
+
return chalk.red("\u2717");
|
|
432
|
+
case "warning":
|
|
433
|
+
return chalk.yellow("\u26A0");
|
|
434
|
+
case "pending":
|
|
435
|
+
return chalk.gray("\u25CB");
|
|
436
|
+
case "running":
|
|
437
|
+
return chalk.cyan("\u25D0");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Print a status message with icon
|
|
442
|
+
*/
|
|
443
|
+
export function printStatus(type, message) {
|
|
444
|
+
if (config.jsonMode)
|
|
445
|
+
return;
|
|
446
|
+
console.log(`${statusIcon(type)} ${message}`);
|
|
447
|
+
}
|
|
448
|
+
// ============================================================================
|
|
449
|
+
// Layout Utilities
|
|
450
|
+
// ============================================================================
|
|
451
|
+
/**
|
|
452
|
+
* Create a horizontal divider
|
|
453
|
+
*/
|
|
454
|
+
export function divider(width = 50) {
|
|
455
|
+
if (config.jsonMode)
|
|
456
|
+
return "";
|
|
457
|
+
const char = isLegacyWindows() ? "-" : "\u2501";
|
|
458
|
+
const line = char.repeat(width);
|
|
459
|
+
return config.noColor ? line : chalk.gray(line);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Create a section header
|
|
463
|
+
*/
|
|
464
|
+
export function sectionHeader(title) {
|
|
465
|
+
if (config.jsonMode)
|
|
466
|
+
return "";
|
|
467
|
+
const formattedTitle = config.noColor ? title : chalk.blue.bold(title);
|
|
468
|
+
return `\n${formattedTitle}\n${divider(title.length + 4)}\n`;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Display phase progress as a visual indicator
|
|
472
|
+
*/
|
|
473
|
+
export function phaseProgress(phases) {
|
|
474
|
+
if (config.jsonMode)
|
|
475
|
+
return "";
|
|
476
|
+
const icons = phases.map((phase) => {
|
|
477
|
+
switch (phase.status) {
|
|
478
|
+
case "success":
|
|
479
|
+
return config.noColor ? "[OK]" : chalk.green("\u25CF");
|
|
480
|
+
case "failure":
|
|
481
|
+
return config.noColor ? "[X]" : chalk.red("\u2717");
|
|
482
|
+
case "running":
|
|
483
|
+
return config.noColor ? "[..]" : chalk.cyan("\u25D0");
|
|
484
|
+
case "skipped":
|
|
485
|
+
return config.noColor ? "[-]" : chalk.gray("-");
|
|
486
|
+
default:
|
|
487
|
+
return config.noColor ? "[ ]" : chalk.gray("\u25CB");
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
const labels = phases.map((phase) => phase.name.charAt(0).toUpperCase());
|
|
491
|
+
return ` Phases: ${icons.join(" ")}\n ${labels.join(" ")}`;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Create a progress bar
|
|
495
|
+
*/
|
|
496
|
+
export function progressBar(current, total, width = 20) {
|
|
497
|
+
if (config.jsonMode)
|
|
498
|
+
return "";
|
|
499
|
+
const percentage = total > 0 ? current / total : 0;
|
|
500
|
+
const filled = Math.round(percentage * width);
|
|
501
|
+
const empty = width - filled;
|
|
502
|
+
const filledChar = config.noColor || isLegacyWindows() ? "#" : "\u2588";
|
|
503
|
+
const emptyChar = config.noColor || isLegacyWindows() ? "-" : "\u2591";
|
|
504
|
+
const bar = filledChar.repeat(filled) + emptyChar.repeat(empty);
|
|
505
|
+
return config.noColor
|
|
506
|
+
? bar
|
|
507
|
+
: chalk.green(filledChar.repeat(filled)) +
|
|
508
|
+
chalk.gray(emptyChar.repeat(empty));
|
|
509
|
+
}
|
|
510
|
+
// ============================================================================
|
|
511
|
+
// Unified UI Namespace
|
|
512
|
+
// ============================================================================
|
|
513
|
+
/**
|
|
514
|
+
* Unified UI object for convenient access to all utilities
|
|
515
|
+
*/
|
|
516
|
+
export const ui = {
|
|
517
|
+
// Config
|
|
518
|
+
configure: configureUI,
|
|
519
|
+
getConfig: getUIConfig,
|
|
520
|
+
// Branding
|
|
521
|
+
logo,
|
|
522
|
+
banner,
|
|
523
|
+
// Spinners
|
|
524
|
+
spinner,
|
|
525
|
+
// Boxes
|
|
526
|
+
box,
|
|
527
|
+
successBox,
|
|
528
|
+
errorBox,
|
|
529
|
+
warningBox,
|
|
530
|
+
headerBox,
|
|
531
|
+
// Tables
|
|
532
|
+
table,
|
|
533
|
+
keyValueTable,
|
|
534
|
+
// Status
|
|
535
|
+
statusIcon,
|
|
536
|
+
printStatus,
|
|
537
|
+
// Layout
|
|
538
|
+
divider,
|
|
539
|
+
sectionHeader,
|
|
540
|
+
// Progress
|
|
541
|
+
phaseProgress,
|
|
542
|
+
progressBar,
|
|
543
|
+
};
|
|
544
|
+
export default ui;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Analyzer for Phase Detection
|
|
3
|
+
*
|
|
4
|
+
* Analyzes issue title and body content to detect phase-relevant keywords
|
|
5
|
+
* and patterns. This supplements (not replaces) label-based detection.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { analyzeTitleForPhases, analyzeBodyForPhases, analyzeContentForPhases } from './content-analyzer';
|
|
10
|
+
*
|
|
11
|
+
* const title = "Extract header component from main layout";
|
|
12
|
+
* const body = "Refactor the header.tsx file to create a reusable component...";
|
|
13
|
+
*
|
|
14
|
+
* const signals = analyzeContentForPhases(title, body);
|
|
15
|
+
* // Returns: { phases: ['test'], signals: [...], source: 'content' }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { Phase } from "./workflow/types.js";
|
|
19
|
+
/**
|
|
20
|
+
* A signal detected from content analysis
|
|
21
|
+
*/
|
|
22
|
+
export interface ContentSignal {
|
|
23
|
+
/** The phase this signal suggests */
|
|
24
|
+
phase: Phase | "quality-loop";
|
|
25
|
+
/** Where the signal was detected */
|
|
26
|
+
source: "title" | "body";
|
|
27
|
+
/** The pattern or keyword that matched */
|
|
28
|
+
pattern: string;
|
|
29
|
+
/** The actual text that matched */
|
|
30
|
+
match: string;
|
|
31
|
+
/** Confidence level of the signal */
|
|
32
|
+
confidence: "high" | "medium" | "low";
|
|
33
|
+
/** Human-readable reason for this signal */
|
|
34
|
+
reason: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Result of content analysis
|
|
38
|
+
*/
|
|
39
|
+
export interface ContentAnalysisResult {
|
|
40
|
+
/** Phases suggested by content analysis */
|
|
41
|
+
phases: Phase[];
|
|
42
|
+
/** Whether quality loop should be enabled */
|
|
43
|
+
qualityLoop: boolean;
|
|
44
|
+
/** Individual signals detected */
|
|
45
|
+
signals: ContentSignal[];
|
|
46
|
+
/** Notes about the analysis */
|
|
47
|
+
notes: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Analyze issue title for phase-relevant keywords
|
|
51
|
+
*
|
|
52
|
+
* @param title - The issue title
|
|
53
|
+
* @returns Array of detected signals
|
|
54
|
+
*/
|
|
55
|
+
export declare function analyzeTitleForPhases(title: string): ContentSignal[];
|
|
56
|
+
/**
|
|
57
|
+
* Analyze issue body for phase-relevant patterns
|
|
58
|
+
*
|
|
59
|
+
* @param body - The issue body
|
|
60
|
+
* @returns Array of detected signals
|
|
61
|
+
*/
|
|
62
|
+
export declare function analyzeBodyForPhases(body: string): ContentSignal[];
|
|
63
|
+
/**
|
|
64
|
+
* Check if content indicates trivial work
|
|
65
|
+
*
|
|
66
|
+
* @param title - The issue title
|
|
67
|
+
* @param body - The issue body
|
|
68
|
+
* @returns True if the work appears trivial
|
|
69
|
+
*/
|
|
70
|
+
export declare function isTrivialWork(title: string, body: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Analyze issue content (title + body) for phase recommendations
|
|
73
|
+
*
|
|
74
|
+
* This is the main entry point for content analysis.
|
|
75
|
+
* It analyzes both title and body, deduplicates signals,
|
|
76
|
+
* and returns a consolidated result.
|
|
77
|
+
*
|
|
78
|
+
* @param title - The issue title
|
|
79
|
+
* @param body - The issue body
|
|
80
|
+
* @returns Consolidated analysis result with phases and signals
|
|
81
|
+
*/
|
|
82
|
+
export declare function analyzeContentForPhases(title: string, body: string): ContentAnalysisResult;
|
|
83
|
+
/**
|
|
84
|
+
* Format content analysis result for display
|
|
85
|
+
*
|
|
86
|
+
* @param result - The analysis result
|
|
87
|
+
* @returns Formatted markdown string
|
|
88
|
+
*/
|
|
89
|
+
export declare function formatContentAnalysis(result: ContentAnalysisResult): string;
|