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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ All notable changes to Devflow will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.1.0] - 2026-08-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Typed flag registry**: the flag surface is now a discriminated-union registry of 28 Claude Code flags — `boolean`, `enum`, `number`, and `string` kinds with per-flag validation, defaults, and display metadata. Eight new upstream-verified flags including `max-concurrent-subagents` (devflow default 40; upstream 20), `subagent-spawn-depth`, `workflow-size-guideline`, `goal-checkin-minutes`, `default-model`, `spellcheck`, and `enable-todo-tools`.
|
|
12
|
+
- **Flags editor TUI**: bare `devflow flags` on a TTY opens a full-keyboard settings-page editor rendering inline in the scroll buffer (no alt-screen), with effective-value display and per-flag hint blurbs. The agents view now runs on the same generic TUI shell.
|
|
13
|
+
- **Typed flags CLI**: `devflow flags --list/--status/--enable/--disable/--set/--unset` with input validation; `--enable/--disable` are boolean-only — non-boolean flags route through `--set`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Init seeding**: both init paths apply seeded flag values non-interactively — fresh installs get registry defaults; re-inits preserve existing values and adopt defaults only for newly added flags. The flags editor step was removed from the Advanced wizard.
|
|
17
|
+
- **`viewMode` folded into the registry** as the `view-mode` enum flag (`default|verbose|focus`); the `viewMode` settings key is written only when non-default. Old `flags: string[]` manifests heal in-reader on first read — no migration entry needed.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **viewMode silently lost on re-init**: `resolveExistingViewMode` now runs before `stripFlags` in the settings apply block; the prior order stripped `viewMode` from settings before reading it (PF-015).
|
|
21
|
+
- **Published bin not executable**: `prepublishOnly` now sets the execute bit on `dist/cli.js` — the bin shipped as 0644, breaking npm 6 and constrained environments (Docker non-root users, some CI runners) that do not auto-chmod on install.
|
|
22
|
+
|
|
23
|
+
### Breaking Changes
|
|
24
|
+
- **Bare `devflow flags` on non-TTY** now prints a status table and exits 1 (was: usage line, exit 0). Scripts should call `devflow flags --status`.
|
|
25
|
+
- **Seven settings.json/env keys become Devflow-managed**: `ANTHROPIC_DEFAULT_MODEL`, `CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS`, `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH`, `CLAUDE_CODE_GOAL_CHECKIN_MINUTES`, `CLAUDE_CODE_ENABLE_TODO_TOOLS` (env); `workflowSizeGuideline`, `spellcheck` (settings). Hand-set values are preserved — adopted into the manifest on the next `devflow init` or flags mutation; `max-concurrent-subagents` adopts the devflow default of 40 only when the key is absent. All keys are removed on `devflow uninstall`.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
8
29
|
## [2.0.1] - 2026-08-23
|
|
9
30
|
|
|
10
31
|
### Changed
|
|
@@ -1163,6 +1184,7 @@ devflow init
|
|
|
1163
1184
|
---
|
|
1164
1185
|
|
|
1165
1186
|
[Unreleased]: https://github.com/dean0x/devflow/compare/v2.0.0...HEAD
|
|
1187
|
+
[2.1.0]: https://github.com/dean0x/devflow/compare/v2.0.1...v2.1.0
|
|
1166
1188
|
[2.0.1]: https://github.com/dean0x/devflow/compare/v2.0.0...v2.0.1
|
|
1167
1189
|
[2.0.0]: https://github.com/dean0x/devflow/compare/v1.8.3...v2.0.0
|
|
1168
1190
|
[1.8.3]: https://github.com/dean0x/devflow/compare/v1.8.2...v1.8.3
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* EFFORT : 13
|
|
23
23
|
* STATE : 14
|
|
24
24
|
*/
|
|
25
|
-
import { bold, dim, green, yellow, cyan, gray,
|
|
25
|
+
import { bold, dim, green, yellow, cyan, gray, stripAnsi, } from '../../core/ansi.js';
|
|
26
|
+
import { padToVisible, truncateVisible, sanitizeCell } from '../tui/cells.js';
|
|
26
27
|
import { isDirtyModel, isDirtyEffort, unsavedCount, isOffCycle, rowState, } from './state.js';
|
|
27
28
|
import { AGENT_STATE_LABELS } from '../../core/agent-state.js';
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
@@ -43,7 +44,7 @@ const COL_MODEL = 32;
|
|
|
43
44
|
const COL_EFFORT = 13;
|
|
44
45
|
const COL_STATE = 14;
|
|
45
46
|
// ---------------------------------------------------------------------------
|
|
46
|
-
// Name formatter — TUI only
|
|
47
|
+
// Name formatter — TUI only
|
|
47
48
|
// ---------------------------------------------------------------------------
|
|
48
49
|
/**
|
|
49
50
|
* Title-case each hyphen-separated segment of the agent name for TUI display.
|
|
@@ -63,49 +64,13 @@ export function formatAgentName(name) {
|
|
|
63
64
|
.map(seg => (seg.length === 0 ? seg : seg[0].toUpperCase() + seg.slice(1)))
|
|
64
65
|
.join('-');
|
|
65
66
|
}
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Cell renderers (pure, return styled string)
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
function padToVisible(s, width) {
|
|
70
|
-
// Pad by visible length (strip ANSI, then pad with spaces).
|
|
71
|
-
const visible = stripAnsi(s);
|
|
72
|
-
const padding = Math.max(0, width - visible.length);
|
|
73
|
-
return s + ' '.repeat(padding);
|
|
74
|
-
}
|
|
75
|
-
function truncateVisible(s, maxWidth) {
|
|
76
|
-
const raw = stripAnsi(s);
|
|
77
|
-
if (raw.length <= maxWidth)
|
|
78
|
-
return s;
|
|
79
|
-
// Re-truncate the unstyled version and rebuild — simpler than ANSI-aware slice.
|
|
80
|
-
return truncate(raw, maxWidth);
|
|
81
|
-
}
|
|
82
|
-
/** Layout-breaking whitespace that stripAnsi deliberately preserves. */
|
|
83
|
-
const LAYOUT_BREAKING_WS = /[\t\n]/g;
|
|
84
|
-
/**
|
|
85
|
-
* Sanitize an untrusted string for a fixed-width TUI cell.
|
|
86
|
-
*
|
|
87
|
-
* stripAnsi strips escape sequences and C0 controls but, by contract, KEEPS
|
|
88
|
-
* TAB (\x09) and LF (\x0a) — correct for its own callers, wrong for a cell in
|
|
89
|
-
* a fixed-width frame. Orphan row names are arbitrary JSON keys read from
|
|
90
|
-
* agent-models.json, so neither is hypothetical:
|
|
91
|
-
* - LF emits a newline inside a frame line, breaking renderFrame's
|
|
92
|
-
* one-string-per-terminal-line contract and desyncing terminal.ts's
|
|
93
|
-
* cursor arithmetic (it writes ERASE_EOL + '\n' per returned line).
|
|
94
|
-
* - TAB measures as one character in padToVisible but occupies up to eight
|
|
95
|
-
* terminal columns, so every column to its right is misaligned.
|
|
96
|
-
* Both collapse to a single space; the raw key is untouched, so the save-path
|
|
97
|
-
* merge still targets the real mapping key.
|
|
98
|
-
*/
|
|
99
|
-
function sanitizeCell(s) {
|
|
100
|
-
return stripAnsi(s).replace(LAYOUT_BREAKING_WS, ' ');
|
|
101
|
-
}
|
|
102
67
|
/**
|
|
103
68
|
* Render the model cell for a given row, considering cursor/active/dirty state.
|
|
104
69
|
*
|
|
105
|
-
* Three branches
|
|
70
|
+
* Three branches:
|
|
106
71
|
* 1. configuredModel === 'default' → "default (shippedDefault)" [+ dormant hint]
|
|
107
72
|
* 2. off-cycle pin → "model (unavailable)"
|
|
108
|
-
* 3. in-cycle model → bare name (aliases
|
|
73
|
+
* 3. in-cycle model → bare name (aliases stored as picker names by buildRow)
|
|
109
74
|
*
|
|
110
75
|
* Off-cycle pin (AC-F4): when configuredModel is absent from modelCycle
|
|
111
76
|
* (retired/unavailable model), show "model (unavailable)".
|
|
@@ -208,7 +173,9 @@ function renderStateCell(row, proxyEnabled, maxWidth) {
|
|
|
208
173
|
*/
|
|
209
174
|
export function renderFrame(state, dims) {
|
|
210
175
|
const { rows, cursor, activeField, viewportOffset, proxyEnabled, modelCycle, } = state;
|
|
211
|
-
|
|
176
|
+
// state.viewportHeight is the single owner — clamped to a MIN so tests that
|
|
177
|
+
// set viewportHeight explicitly render exactly that many data rows.
|
|
178
|
+
const viewportHeight = Math.max(MIN_VIEWPORT, state.viewportHeight);
|
|
212
179
|
// Column widths — shrink gracefully at narrow terminals.
|
|
213
180
|
const totalContent = 2 + COL_AGENT + COL_MODEL + COL_EFFORT + COL_STATE; // prefix + 4 cols
|
|
214
181
|
const scale = Math.min(1, dims.cols / Math.max(totalContent, 1));
|
|
@@ -253,7 +220,7 @@ export function renderFrame(state, dims) {
|
|
|
253
220
|
// Sanitize the name — mandatory for orphan rows (arbitrary JSON keys from
|
|
254
221
|
// agent-models.json may contain escape sequences, newlines or tabs injected
|
|
255
222
|
// by a hostile file).
|
|
256
|
-
//
|
|
223
|
+
// formatAgentName is TUI-only; --list uses the raw lowercase name.
|
|
257
224
|
const safeName = formatAgentName(sanitizeCell(row.name));
|
|
258
225
|
const nameCell = padToVisible(isCursor ? bold(truncateVisible(safeName, agentW)) : truncateVisible(safeName, agentW), agentW);
|
|
259
226
|
const modelCell = padToVisible(renderModelCell({
|
|
@@ -1,74 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Thin
|
|
2
|
+
* Thin adapter — devflow agents TUI shell over the generic runTui driver.
|
|
3
3
|
*
|
|
4
4
|
* applies ADR-013: impure I/O shell in CLI layer; pure logic lives in state.ts/render.ts.
|
|
5
5
|
* avoids PF-014: all cleanup wired via Promise resolve — never process.exit() inside
|
|
6
6
|
* a finally-guarded scope. Cleanup is idempotent and runs on save, cancel,
|
|
7
7
|
* SIGINT, SIGTERM, and keypress limit exhaustion.
|
|
8
|
+
* avoids PF-017: this is the thin adapter, not a copy of the generic shell.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* Public API (frozen — agents-terminal.test.ts is the acceptance gate):
|
|
11
|
+
* - runAgentsTui(initialState, io?) → Promise<TuiResult>
|
|
12
|
+
* - MAX_KEYPRESSES (re-exported from shared shell)
|
|
13
|
+
* - TuiIO (re-exported from shared shell)
|
|
14
|
+
* - TuiResult
|
|
10
15
|
*
|
|
11
|
-
*
|
|
12
|
-
* terminal event that terminates the TUI.
|
|
16
|
+
* Bounded: MAX_KEYPRESSES = 50_000 hard limit (re-exported from src/cli/tui/terminal.ts).
|
|
13
17
|
*/
|
|
14
|
-
import * as readline from 'readline';
|
|
15
18
|
import { reduce } from './state.js';
|
|
16
19
|
import { renderFrame, computeViewportHeight } from './render.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
/** Hard upper bound on keypress events — resolves with 'cancel' on exhaustion. */
|
|
21
|
-
export const MAX_KEYPRESSES = 50_000;
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Terminal escape sequences
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
const ESC = '\x1b';
|
|
26
|
-
const ENTER_ALT = `${ESC}[?1049h`;
|
|
27
|
-
const LEAVE_ALT = `${ESC}[?1049l`;
|
|
28
|
-
const HIDE_CURSOR = `${ESC}[?25l`;
|
|
29
|
-
const SHOW_CURSOR = `${ESC}[?25h`;
|
|
30
|
-
/** Move cursor to top-left without clearing (less flicker than full clear). */
|
|
31
|
-
const HOME = `${ESC}[H`;
|
|
32
|
-
/** Erase from cursor to end of line. */
|
|
33
|
-
const ERASE_EOL = `${ESC}[K`;
|
|
34
|
-
function normalizeKey(str, key) {
|
|
35
|
-
if (key?.ctrl && key.name === 'c')
|
|
36
|
-
return 'ctrl-c';
|
|
37
|
-
const name = key?.name ?? '';
|
|
38
|
-
switch (name) {
|
|
39
|
-
case 'up': return 'up';
|
|
40
|
-
case 'down': return 'down';
|
|
41
|
-
case 'left': return 'left';
|
|
42
|
-
case 'right': return 'right';
|
|
43
|
-
case 'tab': return 'tab';
|
|
44
|
-
case 'return': return 'enter';
|
|
45
|
-
case 'escape': return 'escape';
|
|
46
|
-
case 'space': return 'space';
|
|
47
|
-
default:
|
|
48
|
-
return str ?? name;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// Dims / viewport
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
function getDims(stdout) {
|
|
55
|
-
return {
|
|
56
|
-
rows: stdout.rows ?? 24,
|
|
57
|
-
cols: stdout.columns ?? 80,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// Redraw
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
function redraw(state, stdout) {
|
|
64
|
-
const dims = getDims(stdout);
|
|
65
|
-
const lines = renderFrame(state, dims);
|
|
66
|
-
let out = HOME;
|
|
67
|
-
for (const line of lines) {
|
|
68
|
-
out += line + ERASE_EOL + '\n';
|
|
69
|
-
}
|
|
70
|
-
stdout.write(out);
|
|
71
|
-
}
|
|
20
|
+
import { runTui } from '../tui/terminal.js';
|
|
21
|
+
export { MAX_KEYPRESSES } from '../tui/terminal.js';
|
|
72
22
|
// ---------------------------------------------------------------------------
|
|
73
23
|
// runAgentsTui
|
|
74
24
|
// ---------------------------------------------------------------------------
|
|
@@ -81,99 +31,25 @@ function redraw(state, stdout) {
|
|
|
81
31
|
* @returns Promise resolving to { action, state } when the user saves or cancels.
|
|
82
32
|
*/
|
|
83
33
|
export async function runAgentsTui(initialState, io) {
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
let state = initialState;
|
|
100
|
-
let cleaned = false;
|
|
101
|
-
let keypressCount = 0;
|
|
102
|
-
// Initial viewport size
|
|
103
|
-
const dims = getDims(stdout);
|
|
104
|
-
state = { ...state, viewportHeight: computeViewportHeight(dims.rows) };
|
|
105
|
-
redraw(state, stdout);
|
|
106
|
-
// ── Cleanup (idempotent) ───────────────────────────────────────────────
|
|
107
|
-
function cleanup() {
|
|
108
|
-
if (cleaned)
|
|
109
|
-
return;
|
|
110
|
-
cleaned = true;
|
|
111
|
-
stdin.removeListener('keypress', onKeypress);
|
|
112
|
-
process.removeListener('SIGINT', onSigint);
|
|
113
|
-
process.removeListener('SIGTERM', onSigterm);
|
|
114
|
-
stdout.removeListener('resize', onResize);
|
|
115
|
-
if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
|
|
116
|
-
try {
|
|
117
|
-
stdin.setRawMode(false);
|
|
118
|
-
}
|
|
119
|
-
catch { /* ignore */ }
|
|
120
|
-
}
|
|
121
|
-
// Pause stdin to release the ref'd TTY handle — mirrors the stdin.resume()
|
|
122
|
-
// at startup. Without this the resumed stdin keeps the event loop alive and
|
|
123
|
-
// the CLI (which has no forced process.exit) hangs after the TUI resolves.
|
|
124
|
-
stdin.pause();
|
|
125
|
-
stdout.write(LEAVE_ALT + SHOW_CURSOR);
|
|
126
|
-
}
|
|
127
|
-
function settle(result) {
|
|
128
|
-
cleanup();
|
|
129
|
-
resolve(result);
|
|
130
|
-
}
|
|
131
|
-
// ── Resize handler ─────────────────────────────────────────────────────
|
|
132
|
-
function onResize() {
|
|
133
|
-
const d = getDims(stdout);
|
|
134
|
-
state = { ...state, viewportHeight: computeViewportHeight(d.rows) };
|
|
135
|
-
redraw(state, stdout);
|
|
136
|
-
}
|
|
137
|
-
// ── Keypress handler ───────────────────────────────────────────────────
|
|
138
|
-
function onKeypress(str, key) {
|
|
139
|
-
keypressCount++;
|
|
140
|
-
if (keypressCount > MAX_KEYPRESSES) {
|
|
141
|
-
// Hard safety bound — cancel on exhaustion (avoids unbounded event loop)
|
|
142
|
-
settle({ action: 'cancel', state });
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const normalized = normalizeKey(str, key);
|
|
146
|
-
const { state: next, intent } = reduce(state, normalized);
|
|
147
|
-
state = next;
|
|
148
|
-
switch (intent) {
|
|
149
|
-
case 'save':
|
|
150
|
-
settle({ action: 'save', state });
|
|
151
|
-
return;
|
|
152
|
-
case 'cancel':
|
|
153
|
-
settle({ action: 'cancel', state });
|
|
154
|
-
return;
|
|
155
|
-
case 'none':
|
|
156
|
-
redraw(state, stdout);
|
|
157
|
-
return;
|
|
158
|
-
default: {
|
|
159
|
-
const _ = intent;
|
|
160
|
-
void _;
|
|
161
|
-
redraw(state, stdout);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// ── Signal handlers ────────────────────────────────────────────────────
|
|
166
|
-
function onSigint() {
|
|
167
|
-
settle({ action: 'cancel', state });
|
|
168
|
-
}
|
|
169
|
-
function onSigterm() {
|
|
170
|
-
settle({ action: 'cancel', state });
|
|
171
|
-
}
|
|
172
|
-
// Register all listeners
|
|
173
|
-
stdin.on('keypress', onKeypress);
|
|
174
|
-
process.on('SIGINT', onSigint);
|
|
175
|
-
process.on('SIGTERM', onSigterm);
|
|
176
|
-
stdout.on('resize', onResize);
|
|
34
|
+
// C='none' makes runTui return Promise<{ intent: Exclude<Intent,'none'>; state }>.
|
|
35
|
+
// Exclude<Intent,'none'> = 'save' | 'cancel', which matches TuiResult.action exactly —
|
|
36
|
+
// no casts needed, and adding a new Intent member is a compile error here
|
|
37
|
+
// (exhaustiveness enforced at the type level).
|
|
38
|
+
const result = await runTui({
|
|
39
|
+
initialState,
|
|
40
|
+
reduce,
|
|
41
|
+
renderFrame,
|
|
42
|
+
onResize: (state, dims) => ({
|
|
43
|
+
...state,
|
|
44
|
+
viewportHeight: computeViewportHeight(dims.rows),
|
|
45
|
+
}),
|
|
46
|
+
signalAction: 'cancel',
|
|
47
|
+
continueIntent: 'none',
|
|
48
|
+
io,
|
|
177
49
|
});
|
|
50
|
+
return {
|
|
51
|
+
action: result.intent,
|
|
52
|
+
state: result.state,
|
|
53
|
+
};
|
|
178
54
|
}
|
|
179
55
|
//# sourceMappingURL=terminal.js.map
|
|
@@ -563,7 +563,17 @@ export const agentsCommand = new Command('agents')
|
|
|
563
563
|
const tuiState = await buildTuiState(agentNames, mapping, shippedDefaults, proxyEnabled, catalog, installDir);
|
|
564
564
|
// Lazy-import terminal to avoid loading readline/tty in non-TTY paths
|
|
565
565
|
const { runAgentsTui } = await import('../agents-view/terminal.js');
|
|
566
|
-
|
|
566
|
+
// Wrap: runTui rejects on initial-render failure or handler throw.
|
|
567
|
+
// On rejection: log and bail — no partial write (avoids PF-014 process.exit).
|
|
568
|
+
let result;
|
|
569
|
+
try {
|
|
570
|
+
result = await runAgentsTui(tuiState);
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
p.log.error(`Agent editor failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
574
|
+
process.exitCode = 1;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
567
577
|
if (result.action === 'cancel') {
|
|
568
578
|
p.outro(color.dim('No changes made.'));
|
|
569
579
|
return;
|