@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 +8 -0
- package/dist/cli/interactive-settings.js +191 -0
- package/dist/create-package/shared.js +3 -1
- package/dist/create-package/variants/client/client_vite_r.js +85 -5
- package/dist/directory-picker.js +158 -0
- package/dist/format-checker/cli/options.js +18 -2
- package/dist/format-checker/cli/prompts.js +27 -193
- package/dist/format-checker/index.js +68 -8
- package/dist/format-checker/report.js +3 -0
- package/dist/format-checker/scan/analysis.js +13 -3
- package/dist/format-checker/scan/duplicates.js +12 -4
- package/dist/format-checker/scan/functions.js +1 -3
- package/dist/format-checker/scan/variables.js +246 -0
- package/dist/menu.js +9 -0
- package/dist/robot/cli/prompts.js +102 -0
- package/dist/robot/cli/settings.js +120 -0
- package/dist/robot/config.js +83 -0
- package/dist/robot/coordinator.js +151 -0
- package/dist/robot/extractors/classes.js +41 -0
- package/dist/robot/extractors/components.js +57 -0
- package/dist/robot/extractors/constants.js +42 -0
- package/dist/robot/extractors/functions.js +40 -0
- package/dist/robot/extractors/shared.js +99 -0
- package/dist/robot/extractors/types.js +43 -0
- package/dist/robot/index.js +1 -0
- package/dist/robot/types.js +1 -0
- package/dist/utils/export.js +99 -0
- package/dist/utils/run.js +3 -0
- package/package.json +1 -1
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':
|
|
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
|
-
<
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
});
|