devflow-kit 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/cli/agents-view/render.js +9 -42
- package/dist/cli/agents-view/terminal.js +29 -153
- package/dist/cli/commands/agents.js +11 -1
- package/dist/cli/commands/flags.js +501 -82
- package/dist/cli/commands/init-seed.js +60 -42
- package/dist/cli/commands/init.js +36 -79
- package/dist/cli/commands/proxy.js +42 -13
- package/dist/cli/commands/uninstall.js +2 -3
- package/dist/cli/flags-view/index.js +9 -0
- package/dist/cli/flags-view/render.js +271 -0
- package/dist/cli/flags-view/state.js +478 -0
- package/dist/cli/flags-view/terminal.js +63 -0
- package/dist/cli/tui/cells.js +47 -0
- package/dist/cli/tui/terminal.js +329 -0
- package/dist/cli.js +3 -2
- package/dist/core/ansi.js +84 -0
- package/dist/core/flags.js +938 -123
- package/dist/core/manifest.js +97 -13
- package/dist/core/teammate-mode-cleanup.js +2 -2
- package/dist/hud/colors.js +6 -70
- package/package.json +2 -2
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure TUI frame renderer for the devflow flags view.
|
|
3
|
+
*
|
|
4
|
+
* applies ADR-013: CLI-layer view module; zero fs/tty imports.
|
|
5
|
+
* avoids PF-014: pure function, no process.exit(), no I/O.
|
|
6
|
+
*
|
|
7
|
+
* Layout (FIXED_ROWS = 10, viewport = state.viewportHeight — single owner):
|
|
8
|
+
* 1 Title " Devflow Flags"
|
|
9
|
+
* 2 Set / modified summary
|
|
10
|
+
* 3 Column header " FLAG VALUE" (scaled; offsets match data-row label/value)
|
|
11
|
+
* 4 Scroll-up indicator " ↑ N more" (blank if none)
|
|
12
|
+
* 5+ Viewport rows (one per visible flag)
|
|
13
|
+
* -5 Scroll-down indicator " ↓ N more" (blank if none)
|
|
14
|
+
* -4 (blank)
|
|
15
|
+
* -3 Hint line 1 (flag description/hint for selected flag)
|
|
16
|
+
* -2 Hint line 2 (error message while editing, else edit keybindings)
|
|
17
|
+
* -1 Unsaved count line (blank when 0)
|
|
18
|
+
* 0 Keybinding footer
|
|
19
|
+
*
|
|
20
|
+
* Data row columns (chars at the 80-col reference width — total 77):
|
|
21
|
+
* PREFIX : 2 (cursor mark "❯ " or " ")
|
|
22
|
+
* LABEL : 27 (flag label, padded / truncated; scaled by cols/80 at other widths)
|
|
23
|
+
* DIRTY : 2 ("● " when dirty, else " ")
|
|
24
|
+
* VALUE : 16 (formatted value or edit buffer; scaled by cols/80 at other widths)
|
|
25
|
+
* BLURB : 30 (dim per-flag short phrase; scaled by cols/80 at other widths)
|
|
26
|
+
*
|
|
27
|
+
* Column split: VALUE+BLURB = 46, preserving total width from the prior single VALUE column.
|
|
28
|
+
*
|
|
29
|
+
* Edit buffer rendering:
|
|
30
|
+
* Text before caret + inverse(charAtCaret|' ') + text after caret
|
|
31
|
+
* inverse() = ESC[7m ... ESC[0m (reverse video)
|
|
32
|
+
*/
|
|
33
|
+
import { bold, dim, yellow, cyan, gray, green, red, inverse, } from '../../core/ansi.js';
|
|
34
|
+
import { effectiveDisplay } from '../../core/flags.js';
|
|
35
|
+
import { padToVisible, truncateVisible, sanitizeCell } from '../tui/cells.js';
|
|
36
|
+
// ─── Layout constants ─────────────────────────────────────────────────────────
|
|
37
|
+
/** Non-viewport fixed lines in a rendered frame (see layout comment above). */
|
|
38
|
+
export const FIXED_ROWS = 10;
|
|
39
|
+
const MIN_VIEWPORT = 1;
|
|
40
|
+
const COL_LABEL = 27; // flag label
|
|
41
|
+
// D-BLURB: VALUE+BLURB = 46 preserves the prior total; split as 16+30 at 80-col.
|
|
42
|
+
const COL_VALUE = 16; // value or edit buffer
|
|
43
|
+
const COL_BLURB = 30; // per-flag short phrase (dim)
|
|
44
|
+
// ─── computeViewportHeight ────────────────────────────────────────────────────
|
|
45
|
+
/** Return the number of data rows the terminal can display given its height. */
|
|
46
|
+
export function computeViewportHeight(termRows) {
|
|
47
|
+
return Math.max(MIN_VIEWPORT, termRows - FIXED_ROWS);
|
|
48
|
+
}
|
|
49
|
+
// ─── Value formatting ─────────────────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* Format a row's configuredValue for display.
|
|
52
|
+
*
|
|
53
|
+
* Value vocabulary (D-EFFDV — one-definition seam; never shows 'unset'):
|
|
54
|
+
* null (enum neutral) → dim neutralValue text (e.g. dim('default'))
|
|
55
|
+
* null (number) → dim '<effective default> (default)' or dim('—')
|
|
56
|
+
* null (string) → dim('—')
|
|
57
|
+
* boolean true → green 'on'
|
|
58
|
+
* boolean false → yellow 'off'
|
|
59
|
+
* non-boolean at devflow default → plain string
|
|
60
|
+
* non-boolean deviating from devflow default → bold string
|
|
61
|
+
*
|
|
62
|
+
* Colour vocabulary (one colour, one semantic — applies ADR-016's amendment lesson):
|
|
63
|
+
* cyan = focus indicator (chevron wrapper ‹ › on the cursor row only)
|
|
64
|
+
* yellow = dirty indicator (unconditional ●) and boolean 'off'
|
|
65
|
+
* green = boolean 'on'
|
|
66
|
+
* bold = non-boolean value deviating from devflow default
|
|
67
|
+
*
|
|
68
|
+
* disk-sourced values are routed through sanitizeCell to prevent TAB/LF
|
|
69
|
+
* layout breaks inside the fixed-width TUI cell (avoids PF-023).
|
|
70
|
+
*/
|
|
71
|
+
function formatValue(row) {
|
|
72
|
+
const v = row.configuredValue;
|
|
73
|
+
if (v === null) {
|
|
74
|
+
// Non-boolean neutral: show effective default, dimmed.
|
|
75
|
+
// D-EFFDV: delegate to effectiveDisplay — one definition, all sites.
|
|
76
|
+
const { text } = effectiveDisplay(row.def, null);
|
|
77
|
+
// Append ' (default)' for number flags so the value origin is clear.
|
|
78
|
+
// Enum neutral shows its meaningful name (e.g. 'default'); string null shows '—'.
|
|
79
|
+
const display = row.kind === 'number' ? text + ' (default)' : text;
|
|
80
|
+
return dim(display);
|
|
81
|
+
}
|
|
82
|
+
if (typeof v === 'boolean')
|
|
83
|
+
return v ? green('on') : yellow('off');
|
|
84
|
+
// Non-boolean active: sanitize; bold signals deviation (cyan is reserved for focus)
|
|
85
|
+
const str = sanitizeCell(String(v));
|
|
86
|
+
if (!Object.is(v, row.devflowDefault))
|
|
87
|
+
return bold(str);
|
|
88
|
+
return str;
|
|
89
|
+
}
|
|
90
|
+
// ─── Edit buffer rendering ────────────────────────────────────────────────────
|
|
91
|
+
/**
|
|
92
|
+
* Render the edit buffer with an inverse-video caret marker, windowed to budget.
|
|
93
|
+
*
|
|
94
|
+
* Caret semantics: the caret is BETWEEN characters (text cursor position).
|
|
95
|
+
* - caret = 0: inverse on buf[0] (or space for empty buffer)
|
|
96
|
+
* - caret = n < len: inverse on buf[n]
|
|
97
|
+
* - caret = len: inverse on a trailing space (end of string)
|
|
98
|
+
*
|
|
99
|
+
* When the plain buffer length exceeds `budget`, the buffer is windowed so the
|
|
100
|
+
* caret stays at or near the right edge of the visible region. The inverse()
|
|
101
|
+
* marker is inserted AFTER windowing, so it always survives the size constraint.
|
|
102
|
+
*/
|
|
103
|
+
function renderBuffer(buffer, caret, budget) {
|
|
104
|
+
const safe = buffer.replace(/[\x00-\x1f\x7f]/g, ''); // strip control chars from display
|
|
105
|
+
const safeLen = safe.length;
|
|
106
|
+
if (safeLen === 0) {
|
|
107
|
+
// Empty buffer: show inverse on a blank space
|
|
108
|
+
return inverse(' ');
|
|
109
|
+
}
|
|
110
|
+
// Clamp caret to [0, safeLen]; safeLen means "trailing space" (past last char).
|
|
111
|
+
const clampedCaret = Math.max(0, Math.min(caret, safeLen));
|
|
112
|
+
// Window the buffer to fit within budget visible chars, keeping caret visible.
|
|
113
|
+
// The window follows the caret: push it as far right as possible so the caret
|
|
114
|
+
// is at or near the right edge.
|
|
115
|
+
let windowStart = 0;
|
|
116
|
+
if (safeLen > budget) {
|
|
117
|
+
// Position caret at the rightmost slot; clamp so the window stays in bounds.
|
|
118
|
+
windowStart = Math.min(Math.max(0, clampedCaret - budget + 1), Math.max(0, safeLen - budget));
|
|
119
|
+
}
|
|
120
|
+
const windowed = safe.slice(windowStart, windowStart + budget);
|
|
121
|
+
const windowedCaret = clampedCaret - windowStart;
|
|
122
|
+
if (windowedCaret <= 0) {
|
|
123
|
+
return inverse(windowed[0]) + windowed.slice(1);
|
|
124
|
+
}
|
|
125
|
+
if (windowedCaret >= windowed.length) {
|
|
126
|
+
return windowed + inverse(' ');
|
|
127
|
+
}
|
|
128
|
+
return windowed.slice(0, windowedCaret) + inverse(windowed[windowedCaret]) + windowed.slice(windowedCaret + 1);
|
|
129
|
+
}
|
|
130
|
+
// ─── Row renderer ─────────────────────────────────────────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* Render a single data row.
|
|
133
|
+
* Column widths are passed in from renderFrame so the header and rows share one binding.
|
|
134
|
+
*
|
|
135
|
+
* D-BLURB: blurbW is passed alongside valueW; both are scaled by renderFrame.
|
|
136
|
+
*/
|
|
137
|
+
function renderRow(row, isCursor, isEditing, editBuffer, editCaret, labelW, valueW, blurbW) {
|
|
138
|
+
const prefix = isCursor ? '❯ ' : ' ';
|
|
139
|
+
const isDirty = row.configuredValue !== row.originalValue;
|
|
140
|
+
// Dirty dot is yellow unconditionally — dirtiness must be readable on every row,
|
|
141
|
+
// not only the cursor row.
|
|
142
|
+
const dirtyDot = isDirty ? yellow('● ') : ' ';
|
|
143
|
+
// Sanitize label (registry literal; sanitizeCell prevents TAB/LF layout breaks).
|
|
144
|
+
const rawLabel = sanitizeCell(row.label);
|
|
145
|
+
const labelCell = padToVisible(isCursor ? bold(truncateVisible(rawLabel, labelW)) : truncateVisible(rawLabel, labelW), labelW);
|
|
146
|
+
// Chevrons (cyan ‹ ›) mark the focused control / live edit buffer.
|
|
147
|
+
// Colour vocabulary: cyan = focus only; deviation uses bold (see formatValue).
|
|
148
|
+
// The chevrons take 4 visible chars (‹ + space + space + ›); budget accordingly.
|
|
149
|
+
//
|
|
150
|
+
// Composition rule: colour AFTER measuring — each styled segment is self-contained
|
|
151
|
+
// so an inner RESET (e.g. from green('on')) does not kill the outer cyan.
|
|
152
|
+
// cyan('‹ ') + <styled-or-plain content> + cyan(' ›')
|
|
153
|
+
// rather than cyan(`‹ ${content} ›`), which terminates the outer cyan at the
|
|
154
|
+
// inner RESET, leaving the closing chevron unstyled (applies ADR-016 amendment lesson).
|
|
155
|
+
const chevronBudget = valueW - 4;
|
|
156
|
+
let valueCell;
|
|
157
|
+
if (isCursor && isEditing) {
|
|
158
|
+
// Live edit buffer: renderBuffer windows to chevronBudget and inserts the
|
|
159
|
+
// inverse() caret AFTER windowing, so the caret always survives (ARCH-M7b fix).
|
|
160
|
+
const bufStr = renderBuffer(editBuffer, editCaret, chevronBudget);
|
|
161
|
+
valueCell = cyan('‹ ') + bufStr + cyan(' ›');
|
|
162
|
+
}
|
|
163
|
+
else if (isCursor) {
|
|
164
|
+
// Focused control: truncateVisible is safe here — it fires on plain text only
|
|
165
|
+
// when the value exceeds budget; the chevrons are in their own cyan segments.
|
|
166
|
+
const fmtVal = formatValue(row);
|
|
167
|
+
valueCell = padToVisible(cyan('‹ ') + truncateVisible(fmtVal, chevronBudget) + cyan(' ›'), valueW);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const fmtVal = formatValue(row);
|
|
171
|
+
valueCell = padToVisible(truncateVisible(fmtVal, valueW), valueW);
|
|
172
|
+
}
|
|
173
|
+
// D-BLURB: short phrase, dim, truncated to blurbW. row.blurb is sourced from
|
|
174
|
+
// flag.blurb at buildFlagRows — no registry reach-back needed here (ARCH-M4).
|
|
175
|
+
const blurbCell = blurbW > 0
|
|
176
|
+
? ' ' + dim(truncateVisible(sanitizeCell(row.blurb), blurbW - 1))
|
|
177
|
+
: '';
|
|
178
|
+
return `${prefix}${labelCell}${dirtyDot}${valueCell}${blurbCell}`;
|
|
179
|
+
}
|
|
180
|
+
// ─── renderFrame ─────────────────────────────────────────────────────────────
|
|
181
|
+
/**
|
|
182
|
+
* Render a complete flags TUI frame as an array of strings (one per terminal line).
|
|
183
|
+
* No newlines within strings. Safe at any dims (narrows gracefully).
|
|
184
|
+
*/
|
|
185
|
+
export function renderFrame(state, dims) {
|
|
186
|
+
const { rows, cursor, viewportOffset, editing } = state;
|
|
187
|
+
// state.viewportHeight is the single owner — clamped to a MIN so tests that
|
|
188
|
+
// set viewportHeight explicitly render exactly that many data rows.
|
|
189
|
+
const viewportHeight = Math.max(MIN_VIEWPORT, state.viewportHeight);
|
|
190
|
+
const totalRows = rows.length;
|
|
191
|
+
// ── Column widths (hoisted here so header and rows share one binding) ──────
|
|
192
|
+
// D-BLURB: blurbW is scaled alongside labelW/valueW; both VALUE+BLURB columns
|
|
193
|
+
// shrink proportionally so the total width stays at the prior COL_VALUE budget.
|
|
194
|
+
const scale = Math.min(1, dims.cols / 80);
|
|
195
|
+
const labelW = Math.max(8, Math.floor(COL_LABEL * scale));
|
|
196
|
+
const valueW = Math.max(8, Math.floor(COL_VALUE * scale));
|
|
197
|
+
const blurbW = Math.max(0, Math.floor(COL_BLURB * scale));
|
|
198
|
+
// ── Determine visible row range ───────────────────────────────────────────
|
|
199
|
+
const lastVisible = Math.min(totalRows - 1, viewportOffset + viewportHeight - 1);
|
|
200
|
+
const visibleRows = rows.slice(viewportOffset, lastVisible + 1);
|
|
201
|
+
const rowsAbove = viewportOffset;
|
|
202
|
+
const rowsBelow = Math.max(0, totalRows - (lastVisible + 1));
|
|
203
|
+
// ── Title line ────────────────────────────────────────────────────────────
|
|
204
|
+
const titleLine = bold(' Devflow Flags');
|
|
205
|
+
// ── Set / modified summary ────────────────────────────────────────────────
|
|
206
|
+
const totalSet = rows.filter(r => r.configuredValue !== null).length;
|
|
207
|
+
const totalDirty = rows.filter(r => r.configuredValue !== r.originalValue).length;
|
|
208
|
+
let summaryLine = dim(` ${totalSet} active flags`);
|
|
209
|
+
if (totalDirty > 0) {
|
|
210
|
+
summaryLine += dim(` · `) + yellow(`${totalDirty} modified`);
|
|
211
|
+
}
|
|
212
|
+
// ── Column header (uses same labelW/valueW/blurbW as rows so offsets are identical) ──
|
|
213
|
+
// D-BLURB: HINT column header aligns with the blurb column in data rows.
|
|
214
|
+
const colHeader = ' ' +
|
|
215
|
+
padToVisible(gray('FLAG'), labelW) +
|
|
216
|
+
' ' +
|
|
217
|
+
padToVisible(gray('VALUE'), valueW) +
|
|
218
|
+
(blurbW > 0 ? ' ' + gray('HINT') : '');
|
|
219
|
+
// ── Scroll indicators ─────────────────────────────────────────────────────
|
|
220
|
+
const upIndicator = rowsAbove > 0 ? dim(` ↑ ${rowsAbove} more`) : '';
|
|
221
|
+
const downIndicator = rowsBelow > 0 ? dim(` ↓ ${rowsBelow} more`) : '';
|
|
222
|
+
// ── Rendered data rows ────────────────────────────────────────────────────
|
|
223
|
+
const renderedRows = visibleRows.map((row, relIdx) => {
|
|
224
|
+
const absIdx = viewportOffset + relIdx;
|
|
225
|
+
const isCursor = absIdx === cursor;
|
|
226
|
+
const isEditing = isCursor && editing !== null;
|
|
227
|
+
return renderRow(row, isCursor, isEditing, editing?.buffer ?? '', editing?.caret ?? 0, labelW, valueW, blurbW);
|
|
228
|
+
});
|
|
229
|
+
// ── Hint zone ─────────────────────────────────────────────────────────────
|
|
230
|
+
const selectedRow = rows[cursor];
|
|
231
|
+
// row.hint is populated by buildFlagRows from flag.def.hint — no registry reach-back (ARCH-M4).
|
|
232
|
+
const selectedHint = selectedRow ? selectedRow.hint : '';
|
|
233
|
+
const hintLine1 = selectedHint
|
|
234
|
+
? dim(truncateVisible(` ${selectedHint}`, dims.cols))
|
|
235
|
+
: '';
|
|
236
|
+
let hintLine2;
|
|
237
|
+
if (editing !== null) {
|
|
238
|
+
if (editing.error) {
|
|
239
|
+
hintLine2 = red(truncateVisible(` ✕ ${editing.error}`, dims.cols));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
hintLine2 = dim(' enter confirm esc cancel edit backspace delete');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
hintLine2 = dim(' space/←→ cycle e edit d default u unset enter save esc cancel');
|
|
247
|
+
}
|
|
248
|
+
// ── Unsaved changes ───────────────────────────────────────────────────────
|
|
249
|
+
// Reuse totalDirty computed above — avoids a duplicate full-array scan (PERF-L2).
|
|
250
|
+
const unsavedLine = totalDirty > 0
|
|
251
|
+
? ` ${yellow(`${totalDirty} unsaved change${totalDirty === 1 ? '' : 's'}`)}`
|
|
252
|
+
: '';
|
|
253
|
+
// ── Keybinding footer ─────────────────────────────────────────────────────
|
|
254
|
+
const footerText = dim(truncateVisible(' ↑↓/jk move enter save esc/q cancel ctrl-c abort', dims.cols));
|
|
255
|
+
// ── Assemble ──────────────────────────────────────────────────────────────
|
|
256
|
+
const out = [
|
|
257
|
+
titleLine,
|
|
258
|
+
summaryLine,
|
|
259
|
+
colHeader,
|
|
260
|
+
upIndicator,
|
|
261
|
+
...renderedRows,
|
|
262
|
+
downIndicator,
|
|
263
|
+
'',
|
|
264
|
+
hintLine1,
|
|
265
|
+
hintLine2,
|
|
266
|
+
unsavedLine,
|
|
267
|
+
footerText,
|
|
268
|
+
];
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=render.js.map
|