@emeryld/manager 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -27
- package/dist/format-checker/cli/options.js +117 -0
- package/dist/format-checker/cli/prompts.js +276 -0
- package/dist/format-checker/cli/settings.js +140 -0
- package/dist/format-checker/config.js +63 -0
- package/dist/format-checker/index.js +38 -0
- package/dist/format-checker/report.js +115 -0
- package/dist/format-checker/scan/analysis.js +99 -0
- package/dist/format-checker/scan/collect.js +33 -0
- package/dist/format-checker/scan/constants.js +23 -0
- package/dist/format-checker/scan/duplicates.js +108 -0
- package/dist/format-checker/scan/functions.js +70 -0
- package/dist/format-checker/scan/indentation.js +58 -0
- package/dist/format-checker/scan/index.js +2 -0
- package/dist/format-checker/scan/types.js +1 -0
- package/dist/format-checker/scan/utils.js +68 -0
- package/dist/format-checker/scan.js +444 -0
- package/dist/menu.js +9 -0
- package/dist/publish.js +85 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -15,38 +15,87 @@ 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
|
-
|
|
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
|
+
|
|
43
|
+
## Format checker scan CLI
|
|
44
|
+
- **Usage**: `pnpm manager-cli scan [flags]` scans the workspace without interactive prompts while honoring defaults in `.vscode/settings.json` under `manager.formatChecker`.
|
|
45
|
+
- **Flags**: override any limit via the table below or run `pnpm manager-cli scan --help`/`-h` to print the same guidance.
|
|
46
|
+
|
|
47
|
+
| Flag | Description |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `--max-function-length <number>` | Maximum lines per function before a violation is reported. |
|
|
50
|
+
| `--max-indentation-depth <number>` | Maximum indentation depth (spaces) that the scanner tolerates. |
|
|
51
|
+
| `--max-functions-per-file <number>` | Maximum number of functions allowed in a source file. |
|
|
52
|
+
| `--max-components-per-file <number>` | Maximum component abstractions permitted per file. |
|
|
53
|
+
| `--max-file-length <number>` | Maximum total lines in a file before the file-length rule triggers. |
|
|
54
|
+
| `--max-duplicate-line-occurrences <number>` | Maximum times a similar line may repeat before it counts as duplication. |
|
|
55
|
+
| `--min-duplicate-lines <number>` | Minimum consecutive duplicate lines needed to trigger the duplicate check (set to `0` to skip the requirement). |
|
|
56
|
+
| `--indentation-width <number>` | Number of spaces counted per tab when measuring indentation depth. |
|
|
57
|
+
| `--export-only <true|false>` | When `true`, component/function counts only consider exported symbols. |
|
|
58
|
+
| `--reporting-mode <group|file>` | Choose `group` (summary by violation type) or `file` (per-file entries). |
|
|
59
|
+
- **Purpose**: give Codex agents, CI pipelines, and automation deterministic scans that print the same violation summaries as the interactive action.
|
|
60
|
+
- **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.
|
|
22
61
|
|
|
23
62
|
## Non-interactive release (Codex/CI)
|
|
24
|
-
- Syntax
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
- **Syntax**: `pnpm manager-cli <pkg|all> --non-interactive [publish flags]`
|
|
64
|
+
- **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.
|
|
65
|
+
- **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.
|
|
66
|
+
- **Flags**:
|
|
67
|
+
|
|
68
|
+
| Flag | Description |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `--non-interactive`, `--ci`, `--yes`, `-y` | Auto-respond “yes” to every prompt. |
|
|
71
|
+
| `--bump <patch|minor|major|prepatch|preminor|premajor|prerelease>` | Bump versions (pre* keeps or previews prerelease tags). |
|
|
72
|
+
| `--sync <version>` | Set all selected packages to the exact semver you pass. |
|
|
73
|
+
| `--noop` | Skip the version bump but still tag/publish the current versions. |
|
|
74
|
+
| `--tag <dist-tag>` | Publish with a custom npm dist-tag (defaults to prerelease suffix inference). |
|
|
75
|
+
| `--dry-run` | Forward `--dry-run` to `pnpm publish`. |
|
|
76
|
+
| `--provenance` | Enable npm provenance metadata when publishing. |
|
|
77
|
+
|
|
78
|
+
`--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.
|
|
79
|
+
|
|
80
|
+
## Create package CLI
|
|
81
|
+
- 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.
|
|
82
|
+
- Headless usage: `pnpm manager-cli templates` is a shorthand for `create --list`. Any other flag combination described below will skip prompts and scaffold directly.
|
|
83
|
+
|
|
84
|
+
| Flag | Description |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `--list`, `-l` | List every available template (`rrr-contract`, `rrr-server`, etc.) with their default directories and summaries. |
|
|
87
|
+
| `--describe <variant>`, `-d` | Print the scripts, key files, and notes for a specific template before scaffolding. |
|
|
88
|
+
| `--variant <id>`, `-v` | Choose a template by ID, partial label, or friendly name without hitting the prompts. |
|
|
89
|
+
| `--dir`, `--path`, `-p <path>` | Target directory for the new package (defaults to the variant’s default). |
|
|
90
|
+
| `--name`, `-n <pkg>` | Package name inside `package.json`; defaults to the basename of `--dir`. |
|
|
91
|
+
| `--contract <pkg>` | Contract import to wire into server/client templates. |
|
|
92
|
+
| `--client-kind <vite-react|expo-react-native>` | Pick the client stack for client-friendly templates. |
|
|
93
|
+
| `--reset` | Delete the existing target directory before scaffolding (refuses to reset the workspace root). |
|
|
94
|
+
| `--skip-install` | Do not run `pnpm install` after scaffolding. |
|
|
95
|
+
| `--skip-build` | Skip the post-scaffold build step (only meaningful when not skipping install). |
|
|
96
|
+
| `--help`, `-h` | Show the help text above. |
|
|
97
|
+
|
|
98
|
+
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
99
|
|
|
51
100
|
## Templates at a glance
|
|
52
101
|
| id | default dir | summary | key files |
|
|
@@ -69,6 +118,8 @@ Use `pnpm manager-cli create --describe <variant>` to print the scripts, key fil
|
|
|
69
118
|
`pnpm manager-cli @scope/api --non-interactive --bump minor --tag next`
|
|
70
119
|
- Publish everything with a synced version:
|
|
71
120
|
`pnpm manager-cli all --non-interactive --sync 1.2.3 --provenance`
|
|
121
|
+
- Run the format checker scan headlessly:
|
|
122
|
+
`pnpm manager-cli scan --max-function-length 90 --reporting-mode file`
|
|
72
123
|
|
|
73
124
|
## Notes
|
|
74
125
|
- Helper scripts register the bundled `ts-node/esm` loader so the CLI works even when dependencies are hoisted.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { colors } from '../../utils/log.js';
|
|
2
|
+
import { parseBooleanInput, parsePositiveInteger, parseReportingModeInput, } from './settings.js';
|
|
3
|
+
export const SCAN_FLAG_DEFINITIONS = [
|
|
4
|
+
{
|
|
5
|
+
flag: '--max-function-length',
|
|
6
|
+
description: 'Maximum function length (lines).',
|
|
7
|
+
key: 'maxFunctionLength',
|
|
8
|
+
parser: (value) => parseNumberFlag(value, '--max-function-length'),
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
flag: '--max-indentation-depth',
|
|
12
|
+
description: 'Maximum indentation depth (spaces).',
|
|
13
|
+
key: 'maxIndentationDepth',
|
|
14
|
+
parser: (value) => parseNumberFlag(value, '--max-indentation-depth'),
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
flag: '--max-functions-per-file',
|
|
18
|
+
description: 'Maximum functions per file.',
|
|
19
|
+
key: 'maxFunctionsPerFile',
|
|
20
|
+
parser: (value) => parseNumberFlag(value, '--max-functions-per-file'),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
flag: '--max-components-per-file',
|
|
24
|
+
description: 'Maximum components per file.',
|
|
25
|
+
key: 'maxComponentsPerFile',
|
|
26
|
+
parser: (value) => parseNumberFlag(value, '--max-components-per-file'),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
flag: '--max-file-length',
|
|
30
|
+
description: 'Maximum total lines per file.',
|
|
31
|
+
key: 'maxFileLength',
|
|
32
|
+
parser: (value) => parseNumberFlag(value, '--max-file-length'),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
flag: '--max-duplicate-line-occurrences',
|
|
36
|
+
description: 'Allowed repeated/similar line occurrences.',
|
|
37
|
+
key: 'maxDuplicateLineOccurrences',
|
|
38
|
+
parser: (value) => parseNumberFlag(value, '--max-duplicate-line-occurrences'),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
flag: '--min-duplicate-lines',
|
|
42
|
+
description: 'Minimum repeated lines (0 disables minimum).',
|
|
43
|
+
key: 'minDuplicateLines',
|
|
44
|
+
parser: (value) => parseNumberFlag(value, '--min-duplicate-lines', { allowZero: true }),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
flag: '--export-only',
|
|
48
|
+
description: 'Limit checks to exported symbols (true|false).',
|
|
49
|
+
key: 'exportOnly',
|
|
50
|
+
parser: (value) => parseBooleanFlag(value),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
flag: '--indentation-width',
|
|
54
|
+
description: 'Spaces counted per tab when measuring indentation.',
|
|
55
|
+
key: 'indentationWidth',
|
|
56
|
+
parser: (value) => parseNumberFlag(value, '--indentation-width'),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
flag: '--reporting-mode',
|
|
60
|
+
description: 'How violations are grouped (group|file).',
|
|
61
|
+
key: 'reportingMode',
|
|
62
|
+
parser: (value) => parseReportingModeFlag(value),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
export function parseScanCliArgs(argv) {
|
|
66
|
+
const entries = [];
|
|
67
|
+
for (let i = 0; i < argv.length; i++) {
|
|
68
|
+
const token = argv[i];
|
|
69
|
+
if (token === '--help' || token === '-h') {
|
|
70
|
+
const overrides = Object.fromEntries(entries);
|
|
71
|
+
return { overrides, help: true };
|
|
72
|
+
}
|
|
73
|
+
const definition = SCAN_FLAG_DEFINITIONS.find((def) => def.flag === token);
|
|
74
|
+
if (!definition) {
|
|
75
|
+
throw new Error(`Unknown scan option "${token}". Use "pnpm manager-cli scan --help".`);
|
|
76
|
+
}
|
|
77
|
+
const rawValue = argv[++i];
|
|
78
|
+
if (!rawValue) {
|
|
79
|
+
throw new Error(`Flag "${token}" expects a value.`);
|
|
80
|
+
}
|
|
81
|
+
entries.push([
|
|
82
|
+
definition.key,
|
|
83
|
+
definition.parser(rawValue),
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
const overrides = Object.fromEntries(entries);
|
|
87
|
+
return { overrides, help: false };
|
|
88
|
+
}
|
|
89
|
+
export function printScanUsage() {
|
|
90
|
+
console.log(colors.bold('Usage: pnpm manager-cli scan [flags]'));
|
|
91
|
+
console.log(colors.dim('Run the format checker scan without interactive prompts. Defaults respect .vscode/settings.json (manager.formatChecker).'));
|
|
92
|
+
console.log('');
|
|
93
|
+
SCAN_FLAG_DEFINITIONS.forEach((definition) => {
|
|
94
|
+
console.log(`${colors.cyan(definition.flag)}\t${definition.description}`);
|
|
95
|
+
});
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(colors.dim('Use --help or -h to show this message.'));
|
|
98
|
+
}
|
|
99
|
+
function parseNumberFlag(value, flag, options) {
|
|
100
|
+
const parsed = parsePositiveInteger(value, options);
|
|
101
|
+
if (parsed === undefined) {
|
|
102
|
+
throw new Error(`Flag "${flag}" expects a ${options?.allowZero ? 'non-negative' : 'positive'} integer.`);
|
|
103
|
+
}
|
|
104
|
+
return parsed;
|
|
105
|
+
}
|
|
106
|
+
function parseBooleanFlag(raw) {
|
|
107
|
+
const parsed = parseBooleanInput(raw);
|
|
108
|
+
if (parsed.error)
|
|
109
|
+
throw new Error(parsed.error);
|
|
110
|
+
return parsed.value;
|
|
111
|
+
}
|
|
112
|
+
function parseReportingModeFlag(raw) {
|
|
113
|
+
const result = parseReportingModeInput(raw);
|
|
114
|
+
if (result.error)
|
|
115
|
+
throw new Error(result.error);
|
|
116
|
+
return result.value;
|
|
117
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { colors } from '../../utils/log.js';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
import { askLine } from '../../prompts.js';
|
|
4
|
+
import { formatValue, parseInteractiveValue, SETTING_DESCRIPTORS, validateLimits, } from './settings.js';
|
|
5
|
+
const READY_PROMPT = colors.dim('Limits retained. Use the navigation above to adjust and confirm again.');
|
|
6
|
+
export async function promptLimits(defaults) {
|
|
7
|
+
const supportsInteractive = typeof input.setRawMode === 'function' && input.isTTY;
|
|
8
|
+
let currentLimits = defaults;
|
|
9
|
+
while (true) {
|
|
10
|
+
const chosen = supportsInteractive
|
|
11
|
+
? await promptLimitsInteractive(currentLimits)
|
|
12
|
+
: await promptLimitsSequential(currentLimits);
|
|
13
|
+
const confirmed = await confirmExecution();
|
|
14
|
+
if (confirmed)
|
|
15
|
+
return chosen;
|
|
16
|
+
currentLimits = chosen;
|
|
17
|
+
console.log(READY_PROMPT);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function confirmExecution() {
|
|
21
|
+
const question = colors.cyan('Run the format checker with these limits? (Y/n): ');
|
|
22
|
+
while (true) {
|
|
23
|
+
const answer = (await askLine(question)).trim().toLowerCase();
|
|
24
|
+
if (!answer || answer === 'y' || answer === 'yes')
|
|
25
|
+
return true;
|
|
26
|
+
if (answer === 'n' || answer === 'no')
|
|
27
|
+
return false;
|
|
28
|
+
console.log(colors.yellow('Answer "yes" or "no", or press Enter to proceed.'));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function promptLimitsSequential(defaults) {
|
|
32
|
+
console.log(colors.dim('Enter a number to override a limit or press Enter to keep the default.'));
|
|
33
|
+
return {
|
|
34
|
+
maxFunctionLength: await promptNumber('Max function length', defaults.maxFunctionLength, 'lines'),
|
|
35
|
+
maxIndentationDepth: await promptNumber('Max indentation depth', defaults.maxIndentationDepth, 'spaces'),
|
|
36
|
+
maxFunctionsPerFile: await promptNumber('Max functions per file', defaults.maxFunctionsPerFile, 'count'),
|
|
37
|
+
maxComponentsPerFile: await promptNumber('Max components per file', defaults.maxComponentsPerFile, 'count'),
|
|
38
|
+
maxFileLength: await promptNumber('Max file length', defaults.maxFileLength, 'lines'),
|
|
39
|
+
maxDuplicateLineOccurrences: await promptNumber('Max repeated/similar lines', defaults.maxDuplicateLineOccurrences, 'occurrences'),
|
|
40
|
+
minDuplicateLines: await promptNumber('Min duplicate lines required', defaults.minDuplicateLines, 'lines (0 = no minimum)', { allowZero: true }),
|
|
41
|
+
exportOnly: await promptBoolean('Export-only function checks', defaults.exportOnly),
|
|
42
|
+
indentationWidth: await promptNumber('Indentation width', defaults.indentationWidth, 'spaces per tab'),
|
|
43
|
+
reportingMode: await promptReportingMode(defaults.reportingMode),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
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();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
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
|
+
async function promptNumber(label, fallback, unit, options) {
|
|
235
|
+
const question = colors.cyan(`${label} (${unit}) [default ${fallback}]: `);
|
|
236
|
+
while (true) {
|
|
237
|
+
const answer = await askLine(question);
|
|
238
|
+
if (!answer)
|
|
239
|
+
return fallback;
|
|
240
|
+
const parsed = Number(answer);
|
|
241
|
+
if (!Number.isNaN(parsed)) {
|
|
242
|
+
if (options?.allowZero ? parsed >= 0 : parsed > 0) {
|
|
243
|
+
return Math.floor(parsed);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const rangeLabel = options?.allowZero ? 'non-negative' : 'positive';
|
|
247
|
+
console.log(colors.yellow(`Provide a ${rangeLabel} number or leave blank to keep the default.`));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function promptBoolean(label, fallback) {
|
|
251
|
+
const question = colors.cyan(`${label} [default ${fallback ? 'yes' : 'no'}]: `);
|
|
252
|
+
while (true) {
|
|
253
|
+
const answer = (await askLine(question)).trim().toLowerCase();
|
|
254
|
+
if (!answer)
|
|
255
|
+
return fallback;
|
|
256
|
+
if (['yes', 'y', 'true', '1'].includes(answer))
|
|
257
|
+
return true;
|
|
258
|
+
if (['no', 'n', 'false', '0'].includes(answer))
|
|
259
|
+
return false;
|
|
260
|
+
console.log(colors.yellow('Please answer "yes" or "no", or leave blank to keep the default.'));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function promptReportingMode(fallback) {
|
|
264
|
+
const optionsLabel = SETTING_DESCRIPTORS.find((descriptor) => descriptor.key === 'reportingMode')?.options?.join('|') ?? '';
|
|
265
|
+
const question = colors.cyan(`Reporting mode (${optionsLabel}) [default ${fallback}]: `);
|
|
266
|
+
while (true) {
|
|
267
|
+
const answer = (await askLine(question)).trim().toLowerCase();
|
|
268
|
+
if (!answer)
|
|
269
|
+
return fallback;
|
|
270
|
+
const descriptor = SETTING_DESCRIPTORS.find((entry) => entry.key === 'reportingMode');
|
|
271
|
+
if (descriptor?.options?.includes(answer)) {
|
|
272
|
+
return answer;
|
|
273
|
+
}
|
|
274
|
+
console.log(colors.yellow(`Enter one of ${optionsLabel} or leave blank to keep the default.`));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { REPORTING_MODES } from '../config.js';
|
|
2
|
+
export const SETTING_DESCRIPTORS = [
|
|
3
|
+
{
|
|
4
|
+
key: 'maxFunctionLength',
|
|
5
|
+
label: 'Max function length',
|
|
6
|
+
unit: 'lines',
|
|
7
|
+
type: 'number',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
key: 'maxIndentationDepth',
|
|
11
|
+
label: 'Max indentation depth',
|
|
12
|
+
unit: 'spaces',
|
|
13
|
+
type: 'number',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: 'maxFunctionsPerFile',
|
|
17
|
+
label: 'Max functions per file',
|
|
18
|
+
unit: 'count',
|
|
19
|
+
type: 'number',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: 'maxComponentsPerFile',
|
|
23
|
+
label: 'Max components per file',
|
|
24
|
+
unit: 'count',
|
|
25
|
+
type: 'number',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: 'maxFileLength',
|
|
29
|
+
label: 'Max file length',
|
|
30
|
+
unit: 'lines',
|
|
31
|
+
type: 'number',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: 'maxDuplicateLineOccurrences',
|
|
35
|
+
label: 'Max repeated/similar lines',
|
|
36
|
+
unit: 'occurrences',
|
|
37
|
+
type: 'number',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: 'minDuplicateLines',
|
|
41
|
+
label: 'Min duplicate lines required',
|
|
42
|
+
unit: 'lines (0 = no minimum)',
|
|
43
|
+
type: 'number',
|
|
44
|
+
allowZero: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'exportOnly',
|
|
48
|
+
label: 'Export-only function checks',
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'indentationWidth',
|
|
53
|
+
label: 'Indentation width',
|
|
54
|
+
unit: 'spaces per tab',
|
|
55
|
+
type: 'number',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: 'reportingMode',
|
|
59
|
+
label: 'Reporting mode',
|
|
60
|
+
type: 'option',
|
|
61
|
+
options: REPORTING_MODES,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
export function formatValue(value, descriptor) {
|
|
65
|
+
if (descriptor.type === 'boolean') {
|
|
66
|
+
return value ? 'true' : 'false';
|
|
67
|
+
}
|
|
68
|
+
if (descriptor.type === 'option') {
|
|
69
|
+
return `${value}`;
|
|
70
|
+
}
|
|
71
|
+
return `${value}`;
|
|
72
|
+
}
|
|
73
|
+
export function parseInteractiveValue(descriptor, buffer) {
|
|
74
|
+
const trimmed = buffer.trim();
|
|
75
|
+
if (trimmed === '')
|
|
76
|
+
return { value: undefined };
|
|
77
|
+
if (descriptor.type === 'number') {
|
|
78
|
+
const parsed = parsePositiveInteger(trimmed, { allowZero: descriptor.allowZero });
|
|
79
|
+
if (parsed === undefined) {
|
|
80
|
+
return {
|
|
81
|
+
error: `${descriptor.label} requires a ${descriptor.allowZero ? 'non-negative' : 'positive'} integer.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { value: parsed };
|
|
85
|
+
}
|
|
86
|
+
if (descriptor.type === 'boolean') {
|
|
87
|
+
const result = parseBooleanInput(trimmed);
|
|
88
|
+
if (result.error)
|
|
89
|
+
return { error: result.error };
|
|
90
|
+
return { value: result.value };
|
|
91
|
+
}
|
|
92
|
+
if (descriptor.type === 'option') {
|
|
93
|
+
const result = parseReportingModeInput(trimmed);
|
|
94
|
+
if (result.error)
|
|
95
|
+
return { error: result.error };
|
|
96
|
+
return { value: result.value };
|
|
97
|
+
}
|
|
98
|
+
return { value: undefined };
|
|
99
|
+
}
|
|
100
|
+
export function validateLimits(limits) {
|
|
101
|
+
for (const descriptor of SETTING_DESCRIPTORS) {
|
|
102
|
+
if (descriptor.type === 'number') {
|
|
103
|
+
const value = limits[descriptor.key];
|
|
104
|
+
const min = descriptor.allowZero ? 0 : 1;
|
|
105
|
+
if (!Number.isFinite(value) || value < min) {
|
|
106
|
+
return `${descriptor.label} must be ${descriptor.allowZero ? 'non-negative' : 'positive'}.`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!REPORTING_MODES.includes(limits.reportingMode)) {
|
|
111
|
+
return `Reporting mode must be one of ${REPORTING_MODES.join(', ')}.`;
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
export function parsePositiveInteger(inputValue, options) {
|
|
116
|
+
const trimmed = inputValue.trim();
|
|
117
|
+
const num = Number(trimmed);
|
|
118
|
+
if (!Number.isFinite(num))
|
|
119
|
+
return undefined;
|
|
120
|
+
const floored = Math.floor(num);
|
|
121
|
+
const min = options?.allowZero ? 0 : 1;
|
|
122
|
+
if (floored < min)
|
|
123
|
+
return undefined;
|
|
124
|
+
return floored;
|
|
125
|
+
}
|
|
126
|
+
export function parseBooleanInput(raw) {
|
|
127
|
+
const normalized = raw.trim().toLowerCase();
|
|
128
|
+
if (['yes', 'y', 'true', '1'].includes(normalized))
|
|
129
|
+
return { value: true };
|
|
130
|
+
if (['no', 'n', 'false', '0'].includes(normalized))
|
|
131
|
+
return { value: false };
|
|
132
|
+
return { error: 'Provide "true" or "false".' };
|
|
133
|
+
}
|
|
134
|
+
export function parseReportingModeInput(raw) {
|
|
135
|
+
const normalized = raw.trim().toLowerCase();
|
|
136
|
+
if (REPORTING_MODES.includes(normalized)) {
|
|
137
|
+
return { value: normalized };
|
|
138
|
+
}
|
|
139
|
+
return { error: `Reporting mode must be one of ${REPORTING_MODES.join(', ')}.` };
|
|
140
|
+
}
|