@emeryld/manager 1.0.1 → 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.
Files changed (38) hide show
  1. package/README.md +86 -27
  2. package/dist/cli/interactive-settings.js +191 -0
  3. package/dist/create-package/shared.js +3 -1
  4. package/dist/create-package/variants/client/client_vite_r.js +85 -5
  5. package/dist/format-checker/cli/options.js +133 -0
  6. package/dist/format-checker/cli/prompts.js +108 -0
  7. package/dist/format-checker/cli/settings.js +140 -0
  8. package/dist/format-checker/config.js +63 -0
  9. package/dist/format-checker/index.js +48 -0
  10. package/dist/format-checker/report.js +118 -0
  11. package/dist/format-checker/scan/analysis.js +109 -0
  12. package/dist/format-checker/scan/collect.js +33 -0
  13. package/dist/format-checker/scan/constants.js +23 -0
  14. package/dist/format-checker/scan/duplicates.js +116 -0
  15. package/dist/format-checker/scan/functions.js +68 -0
  16. package/dist/format-checker/scan/indentation.js +58 -0
  17. package/dist/format-checker/scan/index.js +2 -0
  18. package/dist/format-checker/scan/types.js +1 -0
  19. package/dist/format-checker/scan/utils.js +68 -0
  20. package/dist/format-checker/scan/variables.js +246 -0
  21. package/dist/format-checker/scan.js +444 -0
  22. package/dist/menu.js +18 -0
  23. package/dist/publish.js +5 -0
  24. package/dist/robot/cli/prompts.js +102 -0
  25. package/dist/robot/cli/settings.js +120 -0
  26. package/dist/robot/config.js +83 -0
  27. package/dist/robot/coordinator.js +98 -0
  28. package/dist/robot/extractors/classes.js +41 -0
  29. package/dist/robot/extractors/components.js +57 -0
  30. package/dist/robot/extractors/constants.js +42 -0
  31. package/dist/robot/extractors/functions.js +40 -0
  32. package/dist/robot/extractors/shared.js +99 -0
  33. package/dist/robot/extractors/types.js +43 -0
  34. package/dist/robot/index.js +1 -0
  35. package/dist/robot/types.js +1 -0
  36. package/dist/utils/export.js +99 -0
  37. package/dist/utils/run.js +3 -0
  38. package/package.json +2 -1
package/README.md CHANGED
@@ -15,38 +15,95 @@ Dev dependency built for Codex agents: scan a pnpm workspace, scaffold RRRoutes
15
15
  - **Action menu** for the selection:
16
16
  - `update dependencies` → runs `pnpm -r update` (or filtered update), stages only dependency files, prompts for a commit message, commits, and pushes.
17
17
  - `test` → `pnpm test` (filtered to the package when possible).
18
+ - `format checker` → prompts for limits, then scans each source file for long functions, deep indentation, too many components/functions, and repeated/similar snippets before reporting offenders.
18
19
  - `build` → `pnpm build` (filtered when a single package is selected).
19
20
  - `publish` → ensures the working tree is committed, checks registry auth, prompts a version strategy, commits the bump, tags, publishes with pnpm, and pushes tags.
20
21
  - `full` → update → test → build → publish in order.
21
- - `back` → return to package selection. When a single package is chosen, its `package.json` scripts also appear as runnable entries.
22
+ - `back` → return to package selection. When a single package is chosen, its `package.json` scripts also appear as runnable entries.
23
+
24
+ ## Format checker configuration
25
+
26
+ `manager-cli` ships with a format checker action that respects defaults in `.vscode/settings.json` under the `manager.formatChecker` key. Define:
27
+
28
+ | setting | description |
29
+ | --- | --- |
30
+ | `maxFunctionLength` | allowed lines per function |
31
+ | `maxIndentationDepth` | indentation spaces before warning |
32
+ | `maxFunctionsPerFile` | functions allowed in a file |
33
+ | `maxComponentsPerFile` | component abstractions allowed |
34
+ | `maxFileLength` | total lines per file |
35
+ | `maxDuplicateLineOccurrences` | times a similar line can repeat |
36
+ | `minDuplicateLines` | minimum consecutive lines required for repetition warnings |
37
+ | `exportOnly` | limit function/component checks to exported symbols |
38
+ | `indentationWidth` | spaces to count per tab when evaluating indentation depth |
39
+
40
+ Each run also prompts for overrides, so you can tweak targets without editing the file.
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).
43
+
44
+ ## Format checker scan CLI
45
+ - **Usage**: `pnpm manager-cli scan [flags]` scans the workspace without interactive prompts while honoring defaults in `.vscode/settings.json` under `manager.formatChecker`.
46
+ - **Flags**: override any limit via the table below or run `pnpm manager-cli scan --help`/`-h` to print the same guidance.
47
+
48
+ | Flag | Description |
49
+ | --- | --- |
50
+ | `--max-function-length <number>` | Maximum lines per function before a violation is reported. |
51
+ | `--max-indentation-depth <number>` | Maximum indentation depth (spaces) that the scanner tolerates. |
52
+ | `--max-functions-per-file <number>` | Maximum number of functions allowed in a source file. |
53
+ | `--max-components-per-file <number>` | Maximum component abstractions permitted per file. |
54
+ | `--max-file-length <number>` | Maximum total lines in a file before the file-length rule triggers. |
55
+ | `--max-duplicate-line-occurrences <number>` | Maximum times a similar line may repeat before it counts as duplication. |
56
+ | `--min-duplicate-lines <number>` | Minimum consecutive duplicate lines needed to trigger the duplicate check (set to `0` to skip the requirement). |
57
+ | `--indentation-width <number>` | Number of spaces counted per tab when measuring indentation depth. |
58
+ | `--export-only <true|false>` | When `true`, component/function counts only consider exported symbols. |
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`. |
61
+ - **Purpose**: give Codex agents, CI pipelines, and automation deterministic scans that print the same violation summaries as the interactive action.
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.
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.
22
69
 
23
70
  ## Non-interactive release (Codex/CI)
24
- - Syntax: `pnpm manager-cli <pkg|all> --non-interactive [publish flags]`
25
- - Required: the selection (`<pkg>` or `all`) **and** one of `--bump <type>`, `--sync <version>`, or `--noop` (skip version change but still publish/tag).
26
- - Flags:
27
- - `--non-interactive`, `--ci`, `--yes`, `-y` → auto-confirm prompts.
28
- - `--bump <patch|minor|major|prepatch|preminor|premajor|prerelease>` → bump versions (pre* keeps/previews prerelease ids).
29
- - `--sync <version>` set all selected packages to an exact version.
30
- - `--tag <dist-tag>` publish with a custom npm dist-tag (default inferred from prerelease suffix).
31
- - `--dry-run` pass through to `pnpm publish --dry-run`.
32
- - `--provenance` enable provenance on publish.
33
- - `--noop` skip version change but still tag/publish current versions.
34
- - Behavior: commits any existing changes before running (prompting for a message), requires registry auth (`pnpm whoami`), and tags/pushes after publish.
35
-
36
- ## Scaffolding packages
37
- - Interactive: `pnpm manager-cli create` prompts for template, path, package name, contract (for server/client), and client kind (for client/fullstack). Uses arrow keys/digits when the terminal supports raw mode.
38
- - Headless (Codex/CI) flags:
39
- - `--list`, `-l` show available templates.
40
- - `--describe`, `-d <variant>` → print scripts/files/notes for a template.
41
- - `--variant`, `-v <id>` → pick a template without a prompt (ids below).
42
- - `--dir`, `--path`, `-p <path>` target directory.
43
- - `--name`, `-n <pkg>` package name (defaults to basename of `--dir`).
44
- - `--contract <pkg>` → contract import for server/client templates.
45
- - `--client-kind <vite-react|expo-react-native>` choose client stack (client/fullstack).
46
- - `--reset` delete the target directory before scaffolding.
47
- - `--skip-install` / `--skip-build` skip post-create install/build.
48
- - `--help`, `-h` print help.
49
- - After effects: ensures the target directory exists (warns if non-empty, or wipes it when `--reset`), writes shared tooling files (tsconfig, workspace helpers), scaffolds the template, runs `pnpm install`, then builds the new package and its workspace deps (fallbacks: full workspace build, then package-only build). Each scaffolded package includes its own README with variant-specific scripts.
71
+ - **Syntax**: `pnpm manager-cli <pkg|all> --non-interactive [publish flags]`
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.
73
+ - **Behavior**: commits any existing changes (prompting for a message), checks registry auth (`pnpm whoami`), runs `pnpm publish` (filtered when possible), tags, and pushes after a successful release.
74
+ - **Flags**:
75
+
76
+ | Flag | Description |
77
+ | --- | --- |
78
+ | `--non-interactive`, `--ci`, `--yes`, `-y` | Auto-respond “yes” to every prompt. |
79
+ | `--bump <patch|minor|major|prepatch|preminor|premajor|prerelease>` | Bump versions (pre* keeps or previews prerelease tags). |
80
+ | `--sync <version>` | Set all selected packages to the exact semver you pass. |
81
+ | `--noop` | Skip the version bump but still tag/publish the current versions. |
82
+ | `--tag <dist-tag>` | Publish with a custom npm dist-tag (defaults to prerelease suffix inference). |
83
+ | `--dry-run` | Forward `--dry-run` to `pnpm publish`. |
84
+ | `--provenance` | Enable npm provenance metadata when publishing. |
85
+
86
+ `--bump`, `--sync`, or `--noop` are mandatory in non-interactive mode. Optional flags like `--tag`, `--dry-run`, and `--provenance` layer additional behavior on top of the release pipeline.
87
+
88
+ ## Create package CLI
89
+ - Interactive: `pnpm manager-cli create` prompts for template, path, package name, contract (for server/client), and client kind (when applicable). The menu accepts ↑/↓/j/k navigation, digits, and arrow keys when raw mode is available.
90
+ - Headless usage: `pnpm manager-cli templates` is a shorthand for `create --list`. Any other flag combination described below will skip prompts and scaffold directly.
91
+
92
+ | Flag | Description |
93
+ | --- | --- |
94
+ | `--list`, `-l` | List every available template (`rrr-contract`, `rrr-server`, etc.) with their default directories and summaries. |
95
+ | `--describe <variant>`, `-d` | Print the scripts, key files, and notes for a specific template before scaffolding. |
96
+ | `--variant <id>`, `-v` | Choose a template by ID, partial label, or friendly name without hitting the prompts. |
97
+ | `--dir`, `--path`, `-p <path>` | Target directory for the new package (defaults to the variant’s default). |
98
+ | `--name`, `-n <pkg>` | Package name inside `package.json`; defaults to the basename of `--dir`. |
99
+ | `--contract <pkg>` | Contract import to wire into server/client templates. |
100
+ | `--client-kind <vite-react|expo-react-native>` | Pick the client stack for client-friendly templates. |
101
+ | `--reset` | Delete the existing target directory before scaffolding (refuses to reset the workspace root). |
102
+ | `--skip-install` | Do not run `pnpm install` after scaffolding. |
103
+ | `--skip-build` | Skip the post-scaffold build step (only meaningful when not skipping install). |
104
+ | `--help`, `-h` | Show the help text above. |
105
+
106
+ After scaffolding, helper tooling (tsconfig, workspace helpers, etc.) is ensured, the template is written, and a build/install attempt runs unless skipped. Each generated package ships with its own README explaining variant-specific scripts.
50
107
 
51
108
  ## Templates at a glance
52
109
  | id | default dir | summary | key files |
@@ -69,6 +126,8 @@ Use `pnpm manager-cli create --describe <variant>` to print the scripts, key fil
69
126
  `pnpm manager-cli @scope/api --non-interactive --bump minor --tag next`
70
127
  - Publish everything with a synced version:
71
128
  `pnpm manager-cli all --non-interactive --sync 1.2.3 --provenance`
129
+ - Run the format checker scan headlessly:
130
+ `pnpm manager-cli scan --max-function-length 90 --reporting-mode file`
72
131
 
73
132
  ## Notes
74
133
  - Helper scripts register the bundled `ts-node/esm` loader so the CLI works even when dependencies are hoisted.
@@ -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,133 @@
1
+ import { colors } from '../../utils/log.js';
2
+ import { EXPORT_MODES } from '../../utils/export.js';
3
+ import { parseBooleanInput, parsePositiveInteger, parseReportingModeInput, } from './settings.js';
4
+ export const SCAN_FLAG_DEFINITIONS = [
5
+ {
6
+ flag: '--max-function-length',
7
+ description: 'Maximum function length (lines).',
8
+ key: 'maxFunctionLength',
9
+ parser: (value) => parseNumberFlag(value, '--max-function-length'),
10
+ },
11
+ {
12
+ flag: '--max-indentation-depth',
13
+ description: 'Maximum indentation depth (spaces).',
14
+ key: 'maxIndentationDepth',
15
+ parser: (value) => parseNumberFlag(value, '--max-indentation-depth'),
16
+ },
17
+ {
18
+ flag: '--max-functions-per-file',
19
+ description: 'Maximum functions per file.',
20
+ key: 'maxFunctionsPerFile',
21
+ parser: (value) => parseNumberFlag(value, '--max-functions-per-file'),
22
+ },
23
+ {
24
+ flag: '--max-components-per-file',
25
+ description: 'Maximum components per file.',
26
+ key: 'maxComponentsPerFile',
27
+ parser: (value) => parseNumberFlag(value, '--max-components-per-file'),
28
+ },
29
+ {
30
+ flag: '--max-file-length',
31
+ description: 'Maximum total lines per file.',
32
+ key: 'maxFileLength',
33
+ parser: (value) => parseNumberFlag(value, '--max-file-length'),
34
+ },
35
+ {
36
+ flag: '--max-duplicate-line-occurrences',
37
+ description: 'Allowed repeated/similar line occurrences.',
38
+ key: 'maxDuplicateLineOccurrences',
39
+ parser: (value) => parseNumberFlag(value, '--max-duplicate-line-occurrences'),
40
+ },
41
+ {
42
+ flag: '--min-duplicate-lines',
43
+ description: 'Minimum repeated lines (0 disables minimum).',
44
+ key: 'minDuplicateLines',
45
+ parser: (value) => parseNumberFlag(value, '--min-duplicate-lines', { allowZero: true }),
46
+ },
47
+ {
48
+ flag: '--export-only',
49
+ description: 'Limit checks to exported symbols (true|false).',
50
+ key: 'exportOnly',
51
+ parser: (value) => parseBooleanFlag(value),
52
+ },
53
+ {
54
+ flag: '--indentation-width',
55
+ description: 'Spaces counted per tab when measuring indentation.',
56
+ key: 'indentationWidth',
57
+ parser: (value) => parseNumberFlag(value, '--indentation-width'),
58
+ },
59
+ {
60
+ flag: '--reporting-mode',
61
+ description: 'How violations are grouped (group|file).',
62
+ key: 'reportingMode',
63
+ parser: (value) => parseReportingModeFlag(value),
64
+ },
65
+ ];
66
+ export function parseScanCliArgs(argv) {
67
+ const entries = [];
68
+ let exportMode = 'console';
69
+ for (let i = 0; i < argv.length; i++) {
70
+ const token = argv[i];
71
+ if (token === '--help' || token === '-h') {
72
+ const overrides = Object.fromEntries(entries);
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;
86
+ }
87
+ const definition = SCAN_FLAG_DEFINITIONS.find((def) => def.flag === token);
88
+ if (!definition) {
89
+ throw new Error(`Unknown scan option "${token}". Use "pnpm manager-cli scan --help".`);
90
+ }
91
+ const rawValue = argv[++i];
92
+ if (!rawValue) {
93
+ throw new Error(`Flag "${token}" expects a value.`);
94
+ }
95
+ entries.push([
96
+ definition.key,
97
+ definition.parser(rawValue),
98
+ ]);
99
+ }
100
+ const overrides = Object.fromEntries(entries);
101
+ return { overrides, help: false, exportMode };
102
+ }
103
+ export function printScanUsage() {
104
+ console.log(colors.bold('Usage: pnpm manager-cli scan [flags]'));
105
+ console.log(colors.dim('Run the format checker scan without interactive prompts. Defaults respect .vscode/settings.json (manager.formatChecker).'));
106
+ console.log('');
107
+ console.log(`${colors.cyan('--export-mode <console|editor>')}\tWhere to print scan results. Defaults to console.`);
108
+ console.log('');
109
+ SCAN_FLAG_DEFINITIONS.forEach((definition) => {
110
+ console.log(`${colors.cyan(definition.flag)}\t${definition.description}`);
111
+ });
112
+ console.log('');
113
+ console.log(colors.dim('Use --help or -h to show this message.'));
114
+ }
115
+ function parseNumberFlag(value, flag, options) {
116
+ const parsed = parsePositiveInteger(value, options);
117
+ if (parsed === undefined) {
118
+ throw new Error(`Flag "${flag}" expects a ${options?.allowZero ? 'non-negative' : 'positive'} integer.`);
119
+ }
120
+ return parsed;
121
+ }
122
+ function parseBooleanFlag(raw) {
123
+ const parsed = parseBooleanInput(raw);
124
+ if (parsed.error)
125
+ throw new Error(parsed.error);
126
+ return parsed.value;
127
+ }
128
+ function parseReportingModeFlag(raw) {
129
+ const result = parseReportingModeInput(raw);
130
+ if (result.error)
131
+ throw new Error(result.error);
132
+ return result.value;
133
+ }