@emeryld/manager 1.1.0 → 1.3.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),
@@ -0,0 +1,158 @@
1
+ import path from 'node:path';
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import { IGNORED_DIRECTORIES } from './format-checker/scan/constants.js';
4
+ import { normalizeScripts } from './helper-cli/scripts.js';
5
+ import { promptForScript } from './helper-cli/prompts.js';
6
+ import { rootDir } from './helper-cli/env.js';
7
+ import { loadPackages } from './packages.js';
8
+ const ROOT_ENTRY = {
9
+ label: 'Workspace root',
10
+ relativePath: '.',
11
+ description: 'Root directory',
12
+ color: 'brightBlue',
13
+ };
14
+ export async function promptDirectorySelection(options = {}) {
15
+ const candidates = await buildDirectoryCandidates(options.includeWorkspaceRoot ?? true);
16
+ const entries = normalizeScripts(candidates.map((candidate) => ({
17
+ name: candidate.label,
18
+ emoji: '📁',
19
+ color: candidate.color,
20
+ description: candidate.description ?? candidate.relativePath,
21
+ script: candidate.relativePath,
22
+ })));
23
+ const selection = await promptForScript(entries, options.title ?? 'Select a directory');
24
+ if (!selection)
25
+ return undefined;
26
+ const relativePath = selection.script ?? '.';
27
+ const absolutePath = path.resolve(rootDir, relativePath);
28
+ return { label: selection.displayName, relativePath, absolutePath };
29
+ }
30
+ export function describeDirectorySelection(selection) {
31
+ const normalized = selection.relativePath
32
+ .replace(/\\/g, '/')
33
+ .replace(/^\.\/+/, '');
34
+ if (!normalized || normalized === '.') {
35
+ return selection.label;
36
+ }
37
+ return `${selection.label} (${normalized})`;
38
+ }
39
+ async function buildDirectoryCandidates(includeRoot) {
40
+ const directories = await collectDirectories();
41
+ const packages = await loadPackageMetadata();
42
+ const packageMap = new Map(packages.map((pkg) => [pkg.relativeDir, pkg]));
43
+ const candidates = [];
44
+ const filtered = directories.filter((entry) => includeRoot || entry.relativePath !== '.');
45
+ for (const entry of filtered) {
46
+ const pkg = packageMap.get(entry.relativePath);
47
+ if (pkg) {
48
+ candidates.push({
49
+ label: pkg.name ?? pkg.dirName,
50
+ relativePath: entry.relativePath,
51
+ description: pkg.relativeDir,
52
+ color: pkg.color,
53
+ });
54
+ continue;
55
+ }
56
+ if (entry.relativePath === '.') {
57
+ candidates.push({
58
+ label: ROOT_ENTRY.label,
59
+ relativePath: ROOT_ENTRY.relativePath,
60
+ description: ROOT_ENTRY.description,
61
+ color: ROOT_ENTRY.color,
62
+ });
63
+ continue;
64
+ }
65
+ candidates.push({
66
+ label: path.basename(entry.relativePath) || ROOT_ENTRY.label,
67
+ relativePath: entry.relativePath,
68
+ description: entry.relativePath,
69
+ });
70
+ }
71
+ if (candidates.length === 0 && includeRoot) {
72
+ candidates.push(ROOT_ENTRY);
73
+ }
74
+ return candidates;
75
+ }
76
+ async function collectDirectories() {
77
+ const patterns = await collectIgnorePatterns();
78
+ const directories = [];
79
+ async function walk(current) {
80
+ const relativeRaw = path.relative(rootDir, current);
81
+ const relativePath = normalizeRelativePath(relativeRaw);
82
+ if (current !== rootDir && shouldIgnore(relativePath, patterns)) {
83
+ return;
84
+ }
85
+ directories.push({ absolutePath: current, relativePath });
86
+ let entries;
87
+ try {
88
+ entries = await readdir(current, { withFileTypes: true });
89
+ }
90
+ catch {
91
+ return;
92
+ }
93
+ for (const entry of entries) {
94
+ if (!entry.isDirectory() || entry.isSymbolicLink())
95
+ continue;
96
+ await walk(path.join(current, entry.name));
97
+ }
98
+ }
99
+ await walk(rootDir);
100
+ directories.sort((a, b) => {
101
+ if (a.relativePath === '.')
102
+ return -1;
103
+ if (b.relativePath === '.')
104
+ return 1;
105
+ return a.relativePath.localeCompare(b.relativePath);
106
+ });
107
+ return directories;
108
+ }
109
+ async function collectIgnorePatterns() {
110
+ const patterns = new Set();
111
+ for (const entry of IGNORED_DIRECTORIES) {
112
+ patterns.add(normalizeRelativePath(entry));
113
+ }
114
+ try {
115
+ const contents = await readFile(path.join(rootDir, '.gitignore'), 'utf-8');
116
+ for (const rawLine of contents.split(/\r?\n/)) {
117
+ const trimmed = rawLine.trim();
118
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('!'))
119
+ continue;
120
+ patterns.add(normalizeRelativePath(trimmed));
121
+ }
122
+ }
123
+ catch {
124
+ // ignore failures (missing file)
125
+ }
126
+ return Array.from(patterns).filter(Boolean);
127
+ }
128
+ function normalizeRelativePath(value) {
129
+ const normalized = value.replace(/\\/g, '/').replace(/^\/+/, '');
130
+ const trimmed = normalized.replace(/\/+$/, '');
131
+ return trimmed || '.';
132
+ }
133
+ function shouldIgnore(relativePath, patterns) {
134
+ if (relativePath === '.')
135
+ return false;
136
+ const segments = relativePath.split('/');
137
+ for (const pattern of patterns) {
138
+ if (!pattern)
139
+ continue;
140
+ if (relativePath === pattern)
141
+ return true;
142
+ if (relativePath.startsWith(`${pattern}/`))
143
+ return true;
144
+ const patternSegments = pattern.split('/');
145
+ if (patternSegments.length === 1 && segments.includes(pattern)) {
146
+ return true;
147
+ }
148
+ }
149
+ return false;
150
+ }
151
+ async function loadPackageMetadata() {
152
+ try {
153
+ return await loadPackages();
154
+ }
155
+ catch {
156
+ return [];
157
+ }
158
+ }
@@ -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
  });