@emeryld/manager 1.1.0 → 1.2.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/README.md CHANGED
@@ -39,6 +39,7 @@ Dev dependency built for Codex agents: scan a pnpm workspace, scaffold RRRoutes
39
39
 
40
40
  Each run also prompts for overrides, so you can tweak targets without editing the file.
41
41
  The override prompt now lists every limit at once; you can move with ↑/↓, type to replace the highlighted value, Backspace to erase characters, and press Enter when the values validate before confirming.
42
+ You can also decide where the report lands: the manager will ask whether to stream the violations to the console or dump them into a temporary file that is opened in your editor (no repo files are modified).
42
43
 
43
44
  ## Format checker scan CLI
44
45
  - **Usage**: `pnpm manager-cli scan [flags]` scans the workspace without interactive prompts while honoring defaults in `.vscode/settings.json` under `manager.formatChecker`.
@@ -56,9 +57,16 @@ The override prompt now lists every limit at once; you can move with ↑/↓, ty
56
57
  | `--indentation-width <number>` | Number of spaces counted per tab when measuring indentation depth. |
57
58
  | `--export-only <true|false>` | When `true`, component/function counts only consider exported symbols. |
58
59
  | `--reporting-mode <group|file>` | Choose `group` (summary by violation type) or `file` (per-file entries). |
60
+ | `--export-mode <console|editor>` | Choose `console` to print violations in the terminal or `editor` to write them to a temporary file and open it in your default editor. Defaults to `console`. |
59
61
  - **Purpose**: give Codex agents, CI pipelines, and automation deterministic scans that print the same violation summaries as the interactive action.
60
62
  - **Output**: prints `Format checker violations:` followed by grouped or per-file buckets (with severity, snippets, and location references); when no violations occur you get `Format checker found no violations.` and `Workspace meets the configured format limits.` Colors mirror the interactive report to keep results easy to scan.
61
63
 
64
+ ## Robot metadata
65
+
66
+ The `robot metadata` action now starts with the same interactive settings screen as the format checker: pick which kinds of symbols to include, decide whether to limit the scan to exported declarations, and adjust the column width. Use ↑/↓ to move between rows, type new values (comma-separated lists for the kinds), and press Enter once the validation message disappears.
67
+
68
+ Before the extraction runs you can also choose how to consume the results. The default `console` stream prints the JSON into your terminal, while `editor` writes the report to a temporary file (non-saved) and tries to open it in your editor via your configured `$EDITOR`, `code`, or the OS `open/xdg-open` helpers.
69
+
62
70
  ## Non-interactive release (Codex/CI)
63
71
  - **Syntax**: `pnpm manager-cli <pkg|all> --non-interactive [publish flags]`
64
72
  - **Requirements**: provide the selection (`<pkg>` or `all`) and one of `--bump <type>`, `--sync <version>`, or `--noop` (skip the version change but still tag/publish). Use `--non-interactive`, `--ci`, `--yes`, or `-y` interchangeably to answer every prompt in the affirmative.
@@ -0,0 +1,191 @@
1
+ import { stdin as input, stdout as output } from 'node:process';
2
+ import { colors } from '../utils/log.js';
3
+ const DEFAULT_INSTRUCTIONS = [
4
+ 'Use ↑/↓ to change rows, type to replace the highlighted value, Backspace to clear characters, and Enter to validate and confirm the selection.',
5
+ 'Press Esc/Ctrl+C to abort, or hit Enter again after reviewing the summary to continue.',
6
+ ];
7
+ export async function promptInteractiveSettings({ title, descriptors, initial, instructions, validate, }) {
8
+ const supportsInteractive = typeof input.setRawMode === 'function' && input.isTTY;
9
+ if (!supportsInteractive) {
10
+ throw new Error('Interactive mode is not available in this environment.');
11
+ }
12
+ return new Promise((resolve) => {
13
+ const wasRaw = input.isRaw;
14
+ if (!wasRaw) {
15
+ input.setRawMode(true);
16
+ input.resume();
17
+ }
18
+ output.write('\x1b[?25l');
19
+ const state = {
20
+ selectedIndex: 0,
21
+ editingIndex: null,
22
+ editBuffer: '',
23
+ justFocused: false,
24
+ errorMessage: '',
25
+ renderedLines: 0,
26
+ };
27
+ const settings = { ...initial };
28
+ const writableSettings = settings;
29
+ const cleanup = () => {
30
+ if (state.renderedLines > 0) {
31
+ output.write(`\x1b[${state.renderedLines}A`);
32
+ output.write('\x1b[0J');
33
+ state.renderedLines = 0;
34
+ }
35
+ output.write('\x1b[?25h');
36
+ if (!wasRaw) {
37
+ input.setRawMode(false);
38
+ input.pause();
39
+ }
40
+ input.off('data', onData);
41
+ };
42
+ const finalize = () => {
43
+ cleanup();
44
+ console.log();
45
+ resolve(settings);
46
+ };
47
+ const render = () => {
48
+ const lines = buildInteractiveLines(title, descriptors, settings, state, instructions ?? DEFAULT_INSTRUCTIONS);
49
+ if (state.renderedLines > 0) {
50
+ output.write(`\x1b[${state.renderedLines}A`);
51
+ output.write('\x1b[0J');
52
+ }
53
+ lines.forEach((line) => console.log(line));
54
+ state.renderedLines = lines.length;
55
+ };
56
+ const startEditing = () => {
57
+ state.editingIndex = state.selectedIndex;
58
+ state.editBuffer = '';
59
+ state.justFocused = true;
60
+ state.errorMessage = '';
61
+ };
62
+ const moveSelection = (delta) => {
63
+ state.selectedIndex =
64
+ (state.selectedIndex + delta + descriptors.length) % descriptors.length;
65
+ state.editingIndex = null;
66
+ state.editBuffer = '';
67
+ state.justFocused = false;
68
+ state.errorMessage = '';
69
+ render();
70
+ };
71
+ const commitEdit = () => {
72
+ if (state.editingIndex === null)
73
+ return;
74
+ const descriptor = descriptors[state.editingIndex];
75
+ const parsed = descriptor.parse(state.editBuffer);
76
+ if (parsed.error) {
77
+ state.errorMessage = parsed.error;
78
+ process.stdout.write('\x07');
79
+ render();
80
+ return;
81
+ }
82
+ if (parsed.value !== undefined) {
83
+ writableSettings[descriptor.key] = parsed.value;
84
+ }
85
+ state.editingIndex = null;
86
+ state.editBuffer = '';
87
+ state.justFocused = false;
88
+ state.errorMessage = '';
89
+ render();
90
+ };
91
+ const handlePrintable = (typedChar) => {
92
+ if (state.editingIndex !== state.selectedIndex) {
93
+ startEditing();
94
+ }
95
+ if (state.justFocused) {
96
+ state.editBuffer = typedChar;
97
+ state.justFocused = false;
98
+ }
99
+ else {
100
+ state.editBuffer += typedChar;
101
+ }
102
+ render();
103
+ };
104
+ const handleBackspace = () => {
105
+ if (state.editingIndex !== state.selectedIndex) {
106
+ startEditing();
107
+ state.justFocused = false;
108
+ }
109
+ if (state.editBuffer.length > 0) {
110
+ state.editBuffer = state.editBuffer.slice(0, -1);
111
+ render();
112
+ return;
113
+ }
114
+ process.stdout.write('\x07');
115
+ };
116
+ const onData = (buffer) => {
117
+ const ascii = buffer.length === 1 ? buffer[0] : undefined;
118
+ const isPrintable = ascii !== undefined && ascii >= 0x20 && ascii <= 0x7e;
119
+ const typedChar = isPrintable && ascii !== undefined ? String.fromCharCode(ascii) : '';
120
+ const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
121
+ const isArrowDown = buffer.equals(Buffer.from([0x1b, 0x5b, 0x42]));
122
+ const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
123
+ const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
124
+ const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
125
+ const isBackspace = buffer.length === 1 && (buffer[0] === 0x7f || buffer[0] === 0x08);
126
+ if (isCtrlC || isEscape) {
127
+ cleanup();
128
+ process.exit(1);
129
+ }
130
+ if (isArrowUp) {
131
+ moveSelection(-1);
132
+ return;
133
+ }
134
+ if (isArrowDown) {
135
+ moveSelection(1);
136
+ return;
137
+ }
138
+ if (isEnter) {
139
+ if (state.editingIndex === state.selectedIndex) {
140
+ commitEdit();
141
+ return;
142
+ }
143
+ const validation = validate?.(settings);
144
+ if (validation) {
145
+ state.errorMessage = validation;
146
+ process.stdout.write('\x07');
147
+ render();
148
+ return;
149
+ }
150
+ finalize();
151
+ return;
152
+ }
153
+ if (isBackspace) {
154
+ handleBackspace();
155
+ return;
156
+ }
157
+ if (isPrintable && typedChar) {
158
+ handlePrintable(typedChar);
159
+ return;
160
+ }
161
+ process.stdout.write('\x07');
162
+ };
163
+ input.on('data', onData);
164
+ render();
165
+ });
166
+ }
167
+ function buildInteractiveLines(title, descriptors, settings, state, instructions) {
168
+ const heading = colors.bold(title);
169
+ const lines = [heading, ''];
170
+ descriptors.forEach((descriptor, index) => {
171
+ const isSelected = index === state.selectedIndex;
172
+ const pointer = isSelected ? colors.green('➤') : ' ';
173
+ const label = descriptor.unit
174
+ ? `${descriptor.label} (${descriptor.unit})`
175
+ : descriptor.label;
176
+ const isActive = state.editingIndex === index;
177
+ const baseValue = descriptor.format(settings[descriptor.key]);
178
+ const displayValue = isActive
179
+ ? colors.yellow(state.editBuffer.length > 0 ? state.editBuffer : baseValue)
180
+ : colors.magenta(baseValue);
181
+ const labelColor = isSelected ? colors.green : colors.cyan;
182
+ const line = `${pointer} ${labelColor(label)}: ${displayValue}`;
183
+ lines.push(line);
184
+ });
185
+ lines.push('');
186
+ instructions.forEach((instruction) => lines.push(colors.dim(instruction)));
187
+ if (state.errorMessage) {
188
+ lines.push(colors.red(`⚠ ${state.errorMessage}`));
189
+ }
190
+ return lines;
191
+ }
@@ -135,9 +135,11 @@ export function vscodeSettings() {
135
135
  'editor.defaultFormatter': 'esbenp.prettier-vscode',
136
136
  'editor.formatOnSave': true,
137
137
  'editor.codeActionsOnSave': {
138
- 'source.fixAll.eslint': true,
138
+ 'source.fixAll.eslint': 'always',
139
+ 'source.organizeImports': true,
139
140
  },
140
141
  'eslint.useFlatConfig': true,
142
+ 'eslint.format.enable': true,
141
143
  'eslint.validate': ['typescript', 'javascript'],
142
144
  'files.eol': '\n',
143
145
  'prettier.requireConfig': true,
@@ -19,9 +19,13 @@ VITE_SOCKET_PATH=/socket.io
19
19
  function vitePackageJson(name, contractName, includePrepare) {
20
20
  const dependencies = {
21
21
  '@emeryld/rrroutes-client': '^2.5.3',
22
+ '@emotion/react': '^11.12.1',
23
+ '@emotion/styled': '^11.12.1',
24
+ '@mui/material': '^5.17.1',
22
25
  '@tanstack/react-query': '^5.90.12',
23
26
  react: '^18.3.1',
24
27
  'react-dom': '^18.3.1',
28
+ recharts: '^2.7.2',
25
29
  'socket.io-client': '^4.8.3',
26
30
  zod: '^4.2.1',
27
31
  };
@@ -72,21 +76,57 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
72
76
  function appTsx() {
73
77
  return `import React from 'react'
74
78
  import { QueryClientProvider } from '@tanstack/react-query'
79
+ import { ThemeProvider } from '@mui/material/styles'
80
+ import CssBaseline from '@mui/material/CssBaseline'
81
+ import Container from '@mui/material/Container'
75
82
  import { queryClient } from './lib/queryClient'
76
83
  import { AppSocketProvider } from './lib/socket'
77
84
  import { HealthPage } from './pages/HealthPage'
85
+ import theme from './theme'
78
86
 
79
87
  export default function App() {
80
88
  return (
81
- <QueryClientProvider client={queryClient}>
82
- <AppSocketProvider>
83
- <HealthPage />
84
- </AppSocketProvider>
85
- </QueryClientProvider>
89
+ <ThemeProvider theme={theme}>
90
+ <CssBaseline />
91
+ <QueryClientProvider client={queryClient}>
92
+ <AppSocketProvider>
93
+ <Container component="main" maxWidth="lg">
94
+ <HealthPage />
95
+ </Container>
96
+ </AppSocketProvider>
97
+ </QueryClientProvider>
98
+ </ThemeProvider>
86
99
  )
87
100
  }
88
101
  `;
89
102
  }
103
+ function themeFile() {
104
+ return `import { createTheme } from '@mui/material/styles'
105
+
106
+ const theme = createTheme({
107
+ palette: {
108
+ mode: 'light',
109
+ primary: { main: '#2563eb' },
110
+ secondary: { main: '#f97316' },
111
+ background: {
112
+ default: '#f5f7ff',
113
+ paper: '#ffffff',
114
+ },
115
+ },
116
+ typography: {
117
+ fontFamily: ['Inter', 'Segoe UI', 'system-ui', 'sans-serif'].join(', '),
118
+ button: {
119
+ textTransform: 'none',
120
+ },
121
+ },
122
+ shape: {
123
+ borderRadius: 12,
124
+ },
125
+ })
126
+
127
+ export default theme
128
+ `;
129
+ }
90
130
  const STYLES = `:root {
91
131
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
92
132
  color: #0b1021;
@@ -168,6 +208,11 @@ textarea {
168
208
  min-height: 160px;
169
209
  white-space: pre-line;
170
210
  }
211
+
212
+ .chart-card .chart-container {
213
+ height: 220px;
214
+ width: 100%;
215
+ }
171
216
  `;
172
217
  function queryClient(contractImport) {
173
218
  if (contractImport) {
@@ -347,6 +392,14 @@ export function useSocketConnection() {
347
392
  }
348
393
  function healthPage(contractImport) {
349
394
  return `import React from 'react'
395
+ import {
396
+ Bar,
397
+ BarChart,
398
+ ResponsiveContainer,
399
+ Tooltip,
400
+ XAxis,
401
+ YAxis,
402
+ } from 'recharts'
350
403
  import { healthGet, healthPost, hasContract } from '../lib/queryClient'
351
404
  import { roomMeta, useSocketClient, useSocketConnection, socketReady } from '../lib/socket'
352
405
 
@@ -369,6 +422,18 @@ export function HealthPage() {
369
422
  const socket = useSocketClient()
370
423
  const { logs, push, clear } = useLogs()
371
424
 
425
+ const chartData = React.useMemo(
426
+ () => [
427
+ { name: 'Logs', value: logs.length },
428
+ { name: 'Socket ready', value: socketReady ? 1 : 0 },
429
+ {
430
+ name: 'HTTP errors',
431
+ value: Number(Boolean(httpGet.error || httpPost.error)),
432
+ },
433
+ ],
434
+ [logs.length, socketReady, httpGet.error, httpPost.error],
435
+ )
436
+
372
437
  useSocketConnection({
373
438
  event: 'health:connected',
374
439
  rooms: ['health'],
@@ -502,6 +567,20 @@ export function HealthPage() {
502
567
  {logs.length === 0 ? 'No messages yet' : logs.join('\\n')}
503
568
  </div>
504
569
  </div>
570
+
571
+ <div className="card chart-card">
572
+ <h2>Live state</h2>
573
+ <div className="chart-container">
574
+ <ResponsiveContainer width="100%" height="100%">
575
+ <BarChart data={chartData}>
576
+ <XAxis dataKey="name" />
577
+ <YAxis />
578
+ <Tooltip />
579
+ <Bar dataKey="value" fill="#2563eb" radius={[4, 4, 0, 0]} />
580
+ </BarChart>
581
+ </ResponsiveContainer>
582
+ </div>
583
+ </div>
505
584
  </div>
506
585
  </div>
507
586
  )
@@ -542,6 +621,7 @@ export async function scaffoldViteReactClient(ctx) {
542
621
  'vite.config.ts': viteConfig(),
543
622
  'src/main.tsx': MAIN_TSX,
544
623
  'src/App.tsx': appTsx(),
624
+ 'src/theme.ts': themeFile(),
545
625
  'src/lib/queryClient.ts': queryClient(ctx.contractName),
546
626
  'src/lib/socket.tsx': socketProvider(ctx.contractName),
547
627
  'src/pages/HealthPage.tsx': healthPage(ctx.contractName),
@@ -1,4 +1,5 @@
1
1
  import { colors } from '../../utils/log.js';
2
+ import { EXPORT_MODES } from '../../utils/export.js';
2
3
  import { parseBooleanInput, parsePositiveInteger, parseReportingModeInput, } from './settings.js';
3
4
  export const SCAN_FLAG_DEFINITIONS = [
4
5
  {
@@ -64,11 +65,24 @@ export const SCAN_FLAG_DEFINITIONS = [
64
65
  ];
65
66
  export function parseScanCliArgs(argv) {
66
67
  const entries = [];
68
+ let exportMode = 'console';
67
69
  for (let i = 0; i < argv.length; i++) {
68
70
  const token = argv[i];
69
71
  if (token === '--help' || token === '-h') {
70
72
  const overrides = Object.fromEntries(entries);
71
- return { overrides, help: true };
73
+ return { overrides, help: true, exportMode };
74
+ }
75
+ if (token === '--export-mode') {
76
+ const rawValue = argv[++i];
77
+ if (!rawValue) {
78
+ throw new Error('Flag "--export-mode" expects a value.');
79
+ }
80
+ const normalized = rawValue.toLowerCase();
81
+ if (!EXPORT_MODES.includes(normalized)) {
82
+ throw new Error(`Flag "--export-mode" expects one of ${EXPORT_MODES.join(', ')}.`);
83
+ }
84
+ exportMode = normalized;
85
+ continue;
72
86
  }
73
87
  const definition = SCAN_FLAG_DEFINITIONS.find((def) => def.flag === token);
74
88
  if (!definition) {
@@ -84,12 +98,14 @@ export function parseScanCliArgs(argv) {
84
98
  ]);
85
99
  }
86
100
  const overrides = Object.fromEntries(entries);
87
- return { overrides, help: false };
101
+ return { overrides, help: false, exportMode };
88
102
  }
89
103
  export function printScanUsage() {
90
104
  console.log(colors.bold('Usage: pnpm manager-cli scan [flags]'));
91
105
  console.log(colors.dim('Run the format checker scan without interactive prompts. Defaults respect .vscode/settings.json (manager.formatChecker).'));
92
106
  console.log('');
107
+ console.log(`${colors.cyan('--export-mode <console|editor>')}\tWhere to print scan results. Defaults to console.`);
108
+ console.log('');
93
109
  SCAN_FLAG_DEFINITIONS.forEach((definition) => {
94
110
  console.log(`${colors.cyan(definition.flag)}\t${definition.description}`);
95
111
  });
@@ -1,7 +1,8 @@
1
1
  import { colors } from '../../utils/log.js';
2
- import { stdin as input, stdout as output } from 'node:process';
2
+ import { stdin as input } from 'node:process';
3
3
  import { askLine } from '../../prompts.js';
4
4
  import { formatValue, parseInteractiveValue, SETTING_DESCRIPTORS, validateLimits, } from './settings.js';
5
+ import { promptInteractiveSettings, } from '../../cli/interactive-settings.js';
5
6
  const READY_PROMPT = colors.dim('Limits retained. Use the navigation above to adjust and confirm again.');
6
7
  export async function promptLimits(defaults) {
7
8
  const supportsInteractive = typeof input.setRawMode === 'function' && input.isTTY;
@@ -44,193 +45,24 @@ async function promptLimitsSequential(defaults) {
44
45
  };
45
46
  }
46
47
  async function promptLimitsInteractive(defaults) {
47
- if (typeof input.setRawMode !== 'function' || !input.isTTY) {
48
- return promptLimitsSequential(defaults);
49
- }
50
- return new Promise((resolve) => {
51
- const wasRaw = input.isRaw;
52
- if (!wasRaw) {
53
- input.setRawMode(true);
54
- input.resume();
55
- }
56
- output.write('\x1b[?25l');
57
- const state = {
58
- selectedIndex: 0,
59
- editingIndex: null,
60
- editBuffer: '',
61
- justFocused: false,
62
- errorMessage: '',
63
- renderedLines: 0,
64
- };
65
- const limits = { ...defaults };
66
- const writableLimits = limits;
67
- const cleanup = () => {
68
- if (state.renderedLines > 0) {
69
- output.write(`\x1b[${state.renderedLines}A`);
70
- output.write('\x1b[0J');
71
- state.renderedLines = 0;
72
- }
73
- output.write('\x1b[?25h');
74
- if (!wasRaw) {
75
- input.setRawMode(false);
76
- input.pause();
77
- }
78
- input.off('data', onData);
79
- };
80
- const finalize = () => {
81
- cleanup();
82
- console.log();
83
- resolve(limits);
84
- };
85
- const render = () => {
86
- const lines = buildInteractiveLines(limits, state.selectedIndex, state.editingIndex, state.editBuffer, state.errorMessage);
87
- if (state.renderedLines > 0) {
88
- output.write(`\x1b[${state.renderedLines}A`);
89
- output.write('\x1b[0J');
90
- }
91
- lines.forEach((line) => console.log(line));
92
- state.renderedLines = lines.length;
93
- };
94
- const startEditing = () => {
95
- state.editingIndex = state.selectedIndex;
96
- state.editBuffer = '';
97
- state.justFocused = true;
98
- state.errorMessage = '';
99
- };
100
- const moveSelection = (delta) => {
101
- state.selectedIndex =
102
- (state.selectedIndex + delta + SETTING_DESCRIPTORS.length) %
103
- SETTING_DESCRIPTORS.length;
104
- state.editingIndex = null;
105
- state.editBuffer = '';
106
- state.justFocused = false;
107
- state.errorMessage = '';
108
- render();
109
- };
110
- const commitEdit = () => {
111
- if (state.editingIndex === null)
112
- return;
113
- const descriptor = SETTING_DESCRIPTORS[state.editingIndex];
114
- const parsed = parseInteractiveValue(descriptor, state.editBuffer);
115
- if (parsed.error) {
116
- state.errorMessage = parsed.error;
117
- process.stdout.write('\x07');
118
- render();
119
- return;
120
- }
121
- if (parsed.value !== undefined) {
122
- writableLimits[descriptor.key] = parsed.value;
123
- }
124
- state.editingIndex = null;
125
- state.editBuffer = '';
126
- state.justFocused = false;
127
- state.errorMessage = '';
128
- render();
129
- };
130
- const handlePrintable = (typedChar) => {
131
- if (state.editingIndex !== state.selectedIndex) {
132
- startEditing();
133
- }
134
- if (state.justFocused) {
135
- state.editBuffer = typedChar;
136
- state.justFocused = false;
137
- }
138
- else {
139
- state.editBuffer += typedChar;
140
- }
141
- render();
142
- };
143
- const handleBackspace = () => {
144
- if (state.editingIndex !== state.selectedIndex) {
145
- startEditing();
146
- state.justFocused = false;
147
- }
148
- if (state.editBuffer.length > 0) {
149
- state.editBuffer = state.editBuffer.slice(0, -1);
150
- render();
151
- return;
152
- }
153
- process.stdout.write('\x07');
154
- };
155
- const onData = (buffer) => {
156
- const ascii = buffer.length === 1 ? buffer[0] : undefined;
157
- const isPrintable = ascii !== undefined && ascii >= 0x20 && ascii <= 0x7e;
158
- const typedChar = isPrintable && ascii !== undefined ? String.fromCharCode(ascii) : '';
159
- const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
160
- const isArrowDown = buffer.equals(Buffer.from([0x1b, 0x5b, 0x42]));
161
- const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
162
- const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
163
- const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
164
- const isBackspace = buffer.length === 1 && (buffer[0] === 0x7f || buffer[0] === 0x08);
165
- if (isCtrlC || isEscape) {
166
- cleanup();
167
- process.exit(1);
168
- }
169
- if (isArrowUp ||
170
- (buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
171
- moveSelection(-1);
172
- return;
173
- }
174
- if (isArrowDown ||
175
- (buffer.length === 1 && (buffer[0] === 0x6a || buffer[0] === 0x4a))) {
176
- moveSelection(1);
177
- return;
178
- }
179
- if (isEnter) {
180
- if (state.editingIndex === state.selectedIndex) {
181
- commitEdit();
182
- return;
183
- }
184
- const validation = validateLimits(limits);
185
- if (validation) {
186
- state.errorMessage = validation;
187
- process.stdout.write('\x07');
188
- render();
189
- return;
190
- }
191
- finalize();
192
- return;
193
- }
194
- if (isBackspace) {
195
- handleBackspace();
196
- return;
197
- }
198
- if (isPrintable && typedChar) {
199
- handlePrintable(typedChar);
200
- return;
201
- }
202
- process.stdout.write('\x07');
203
- };
204
- input.on('data', onData);
205
- render();
48
+ const descriptors = SETTING_DESCRIPTORS.map((descriptor) => ({
49
+ key: descriptor.key,
50
+ label: descriptor.label,
51
+ unit: descriptor.unit,
52
+ format: (value) => formatValue(value, descriptor),
53
+ parse: (buffer) => parseInteractiveValue(descriptor, buffer),
54
+ }));
55
+ return promptInteractiveSettings({
56
+ title: 'Format checker settings (type to edit values)',
57
+ descriptors,
58
+ initial: defaults,
59
+ instructions: [
60
+ 'Use ↑/↓ to change rows, type to replace the highlighted value, Backspace to clear characters, and Enter to validate and confirm the selection.',
61
+ 'Press Esc/Ctrl+C to abort, or hit Enter again after reviewing the summary to continue.',
62
+ ],
63
+ validate: validateLimits,
206
64
  });
207
65
  }
208
- function buildInteractiveLines(limits, selectedIndex, editingIndex, editBuffer, errorMessage) {
209
- const heading = colors.bold('Format checker settings (type to edit values)');
210
- const lines = [heading, ''];
211
- SETTING_DESCRIPTORS.forEach((descriptor, index) => {
212
- const isSelected = index === selectedIndex;
213
- const pointer = isSelected ? colors.green('➤') : ' ';
214
- const label = descriptor.unit
215
- ? `${descriptor.label} (${descriptor.unit})`
216
- : descriptor.label;
217
- const isActive = editingIndex === index;
218
- const baseValue = formatValue(limits[descriptor.key], descriptor);
219
- const displayValue = isActive
220
- ? colors.yellow(editBuffer.length > 0 ? editBuffer : baseValue)
221
- : colors.magenta(baseValue);
222
- const labelColor = isSelected ? colors.green : colors.cyan;
223
- const line = `${pointer} ${labelColor(label)}: ${displayValue}`;
224
- lines.push(line);
225
- });
226
- lines.push('');
227
- lines.push(colors.dim('Use ↑/↓ to change rows, type to replace the highlighted value, Backspace to clear characters, and Enter to validate and confirm the selection.'));
228
- lines.push(colors.dim('Press Esc/Ctrl+C to abort, or hit Enter again after reviewing the summary to continue.'));
229
- if (errorMessage) {
230
- lines.push(colors.red(`⚠ ${errorMessage}`));
231
- }
232
- return lines;
233
- }
234
66
  async function promptNumber(label, fallback, unit, options) {
235
67
  const question = colors.cyan(`${label} (${unit}) [default ${fallback}]: `);
236
68
  while (true) {