@emeryld/manager 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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/format-checker/cli/options.js +18 -2
- package/dist/format-checker/cli/prompts.js +18 -186
- package/dist/format-checker/index.js +15 -5
- 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 +98 -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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { colors } from '../utils/log.js';
|
|
2
|
+
import { captureConsoleOutput, exportReportLines, promptExportMode, } from '../utils/export.js';
|
|
2
3
|
import { rootDir } from '../helper-cli/env.js';
|
|
3
4
|
import { loadFormatLimits, } from './config.js';
|
|
4
5
|
import { collectSourceFiles, analyzeFiles } from './scan/index.js';
|
|
@@ -9,10 +10,11 @@ export async function runFormatChecker() {
|
|
|
9
10
|
console.log(colors.cyan('Gathering defaults from .vscode/settings.json (manager.formatChecker)'));
|
|
10
11
|
const defaults = await loadFormatLimits();
|
|
11
12
|
const limits = await promptLimits(defaults);
|
|
12
|
-
await
|
|
13
|
+
const exportMode = await promptExportMode();
|
|
14
|
+
await executeFormatCheck(limits, exportMode);
|
|
13
15
|
}
|
|
14
16
|
export async function runFormatCheckerScanCli(argv) {
|
|
15
|
-
const { overrides, help } = parseScanCliArgs(argv);
|
|
17
|
+
const { overrides, help, exportMode } = parseScanCliArgs(argv);
|
|
16
18
|
if (help) {
|
|
17
19
|
printScanUsage();
|
|
18
20
|
return;
|
|
@@ -20,9 +22,9 @@ export async function runFormatCheckerScanCli(argv) {
|
|
|
20
22
|
const defaults = await loadFormatLimits();
|
|
21
23
|
const limits = { ...defaults, ...overrides };
|
|
22
24
|
console.log(colors.cyan('Running format checker scan (machine-friendly)'));
|
|
23
|
-
await executeFormatCheck(limits);
|
|
25
|
+
await executeFormatCheck(limits, exportMode);
|
|
24
26
|
}
|
|
25
|
-
async function executeFormatCheck(limits) {
|
|
27
|
+
async function executeFormatCheck(limits, exportMode) {
|
|
26
28
|
console.log(colors.magenta('Scanning workspace for source files...'));
|
|
27
29
|
const files = await collectSourceFiles(rootDir);
|
|
28
30
|
if (files.length === 0) {
|
|
@@ -31,8 +33,16 @@ async function executeFormatCheck(limits) {
|
|
|
31
33
|
}
|
|
32
34
|
console.log(colors.magenta(`Analyzing ${files.length} files`));
|
|
33
35
|
const violations = await analyzeFiles(files, limits);
|
|
34
|
-
const hadViolations =
|
|
36
|
+
const hadViolations = await handleViolations(violations, limits.reportingMode, exportMode);
|
|
35
37
|
if (!hadViolations) {
|
|
36
38
|
console.log(colors.green('Workspace meets the configured format limits.'));
|
|
37
39
|
}
|
|
38
40
|
}
|
|
41
|
+
async function handleViolations(violations, reportingMode, exportMode) {
|
|
42
|
+
if (exportMode === 'console') {
|
|
43
|
+
return reportViolations(violations, reportingMode);
|
|
44
|
+
}
|
|
45
|
+
const { result, lines } = captureConsoleOutput(() => reportViolations(violations, reportingMode));
|
|
46
|
+
await exportReportLines('format-checker', 'txt', lines);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
@@ -109,6 +109,9 @@ function logViolationEntry(entry, messagePrefix, locationDescriptor) {
|
|
|
109
109
|
console.log(` ${colors.yellow(line)}`);
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
|
+
if (entry.requiredVariables?.length) {
|
|
113
|
+
console.log(` ${colors.dim('Required variables:')} ${entry.requiredVariables.join(', ')}`);
|
|
114
|
+
}
|
|
112
115
|
if (locationDescriptor) {
|
|
113
116
|
console.log(` ${colors.dim(locationDescriptor)}`);
|
|
114
117
|
}
|
|
@@ -2,7 +2,9 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { collectDuplicateViolations, recordDuplicateLines, } from './duplicates.js';
|
|
3
3
|
import { countIndentation, groupIndentationViolations, } from './indentation.js';
|
|
4
4
|
import { collectFunctionRecords } from './functions.js';
|
|
5
|
-
import { markImportLines, normalizeLine } from './utils.js';
|
|
5
|
+
import { markImportLines, normalizeLine, resolveScriptKind } from './utils.js';
|
|
6
|
+
import { collectRequiredVariables } from './variables.js';
|
|
7
|
+
import * as ts from 'typescript';
|
|
6
8
|
export async function analyzeFiles(files, limits) {
|
|
7
9
|
const violations = [];
|
|
8
10
|
const duplicateMap = new Map();
|
|
@@ -45,12 +47,19 @@ async function analyzeSingleFile(filePath, limits, duplicates) {
|
|
|
45
47
|
indentationViolations.push({ line: index + 1, indent });
|
|
46
48
|
}
|
|
47
49
|
});
|
|
48
|
-
|
|
50
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.ESNext, true, resolveScriptKind(filePath));
|
|
51
|
+
recordDuplicateLines(normalizedLines, filePath, limits, duplicates, sourceFile, lines);
|
|
49
52
|
const indentationGroups = groupIndentationViolations(indentationViolations, lines, limits.maxIndentationDepth);
|
|
50
53
|
indentationGroups
|
|
51
54
|
.sort((a, b) => b.severity - a.severity)
|
|
52
55
|
.slice(0, 4)
|
|
53
56
|
.forEach((group) => {
|
|
57
|
+
const requiredVariables = collectRequiredVariables(sourceFile, lines, {
|
|
58
|
+
startLine: group.startLine,
|
|
59
|
+
startColumn: group.startColumn,
|
|
60
|
+
endLine: group.endLine,
|
|
61
|
+
endColumn: group.endColumn,
|
|
62
|
+
});
|
|
54
63
|
violations.push({
|
|
55
64
|
type: 'indentation',
|
|
56
65
|
file: filePath,
|
|
@@ -59,9 +68,10 @@ async function analyzeSingleFile(filePath, limits, duplicates) {
|
|
|
59
68
|
message: `Indentation level ${group.maxIndent} exceeds max ${limits.maxIndentationDepth}`,
|
|
60
69
|
snippet: group.snippet,
|
|
61
70
|
detail: `${group.startLine}:${group.startColumn}/${group.endLine}:${group.endColumn}`,
|
|
71
|
+
requiredVariables,
|
|
62
72
|
});
|
|
63
73
|
});
|
|
64
|
-
const allFunctions = collectFunctionRecords(
|
|
74
|
+
const allFunctions = collectFunctionRecords(sourceFile);
|
|
65
75
|
const functions = limits.exportOnly
|
|
66
76
|
? allFunctions.filter((record) => record.isExported)
|
|
67
77
|
: allFunctions;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { getLineColumnRange } from './utils.js';
|
|
3
|
-
|
|
3
|
+
import { collectRequiredVariables } from './variables.js';
|
|
4
|
+
export function recordDuplicateLines(normalizedLines, filePath, limits, duplicates, sourceFile, lines) {
|
|
4
5
|
const minLines = Math.max(1, limits.minDuplicateLines);
|
|
5
6
|
if (limits.minDuplicateLines <= 0 || minLines === 1) {
|
|
6
7
|
normalizedLines.forEach((entry, index) => {
|
|
@@ -12,7 +13,7 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
|
|
|
12
13
|
if (entry.isImport)
|
|
13
14
|
return;
|
|
14
15
|
const { startColumn, endColumn } = getLineColumnRange(entry.raw);
|
|
15
|
-
addDuplicateOccurrence(duplicates, entry.normalized, filePath, index + 1, startColumn, index + 1, endColumn, snippet);
|
|
16
|
+
addDuplicateOccurrence(duplicates, entry.normalized, filePath, index + 1, startColumn, index + 1, endColumn, snippet, sourceFile, lines);
|
|
16
17
|
});
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
@@ -47,10 +48,10 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
|
|
|
47
48
|
const endLine = i + 1 + lastNonEmptyIndex;
|
|
48
49
|
const { startColumn } = getLineColumnRange(slice[firstNonEmptyIndex].raw);
|
|
49
50
|
const { endColumn } = getLineColumnRange(slice[lastNonEmptyIndex].raw);
|
|
50
|
-
addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet);
|
|
51
|
+
addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet, sourceFile, lines);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
function addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet) {
|
|
54
|
+
function addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet, sourceFile, lines) {
|
|
54
55
|
const tracker = duplicates.get(key);
|
|
55
56
|
if (tracker) {
|
|
56
57
|
tracker.count += 1;
|
|
@@ -69,6 +70,12 @@ function addDuplicateOccurrence(duplicates, key, filePath, startLine, startColum
|
|
|
69
70
|
duplicates.set(key, {
|
|
70
71
|
count: 1,
|
|
71
72
|
snippet,
|
|
73
|
+
requiredVariables: collectRequiredVariables(sourceFile, lines, {
|
|
74
|
+
startLine,
|
|
75
|
+
startColumn,
|
|
76
|
+
endLine,
|
|
77
|
+
endColumn,
|
|
78
|
+
}),
|
|
72
79
|
occurrences: [
|
|
73
80
|
{
|
|
74
81
|
file: filePath,
|
|
@@ -102,6 +109,7 @@ export function collectDuplicateViolations(duplicates, limits) {
|
|
|
102
109
|
.join('\n'),
|
|
103
110
|
snippet: tracker.snippet,
|
|
104
111
|
repeatCount: tracker.count,
|
|
112
|
+
requiredVariables: tracker.requiredVariables,
|
|
105
113
|
});
|
|
106
114
|
}
|
|
107
115
|
return violations;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
|
|
3
|
-
export function collectFunctionRecords(content, filePath) {
|
|
4
|
-
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.ESNext, true, resolveScriptKind(filePath));
|
|
2
|
+
export function collectFunctionRecords(sourceFile) {
|
|
5
3
|
const records = [];
|
|
6
4
|
function visit(node) {
|
|
7
5
|
if ((ts.isFunctionDeclaration(node) ||
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
const RANGE_CLAMP_MIN = 0;
|
|
3
|
+
export const DEFAULT_REQUIRED_VARIABLE_EXCLUSIONS = new Set(['Object', 'console', 'Boolean', 'Number', 'String', 'Array', 'Math', 'Date', 'Set', 'Map', 'WeakSet', 'WeakMap', 'Symbol', 'Promise', 'Error', 'RegExp', 'JSON', 'Intl', 'Reflect', 'Proxy', 'globalThis']);
|
|
4
|
+
export function collectRequiredVariables(sourceFile, lines, range, exclusions) {
|
|
5
|
+
if (!lines.length)
|
|
6
|
+
return [];
|
|
7
|
+
const lastLineIndex = Math.max(0, lines.length - 1);
|
|
8
|
+
let startLineIndex = clamp(range.startLine - 1, RANGE_CLAMP_MIN, lastLineIndex);
|
|
9
|
+
let endLineIndex = clamp(range.endLine - 1, RANGE_CLAMP_MIN, lastLineIndex);
|
|
10
|
+
if (startLineIndex > endLineIndex) {
|
|
11
|
+
const temp = startLineIndex;
|
|
12
|
+
startLineIndex = endLineIndex;
|
|
13
|
+
endLineIndex = temp;
|
|
14
|
+
}
|
|
15
|
+
const startLineText = lines[startLineIndex] ?? '';
|
|
16
|
+
const endLineText = lines[endLineIndex] ?? '';
|
|
17
|
+
const startColumnIndex = clamp(range.startColumn - 1, RANGE_CLAMP_MIN, startLineText.length);
|
|
18
|
+
let endColumnIndex = clamp(range.endColumn - 1, RANGE_CLAMP_MIN, endLineText.length);
|
|
19
|
+
if (startLineIndex === endLineIndex &&
|
|
20
|
+
endColumnIndex < startColumnIndex) {
|
|
21
|
+
endColumnIndex = startLineText.length;
|
|
22
|
+
}
|
|
23
|
+
const rangeStart = sourceFile.getPositionOfLineAndCharacter(startLineIndex, startColumnIndex);
|
|
24
|
+
const rangeEnd = sourceFile.getPositionOfLineAndCharacter(endLineIndex, endColumnIndex);
|
|
25
|
+
const declared = new Set();
|
|
26
|
+
const referenced = new Set();
|
|
27
|
+
const exclusionSet = new Set(DEFAULT_REQUIRED_VARIABLE_EXCLUSIONS);
|
|
28
|
+
if (exclusions) {
|
|
29
|
+
for (const value of exclusions) {
|
|
30
|
+
exclusionSet.add(value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function declareSymbol(name) {
|
|
34
|
+
if (!name)
|
|
35
|
+
return;
|
|
36
|
+
declared.add(name);
|
|
37
|
+
referenced.delete(name);
|
|
38
|
+
}
|
|
39
|
+
function isIdentifierLike(node) {
|
|
40
|
+
return ts.isIdentifier(node);
|
|
41
|
+
}
|
|
42
|
+
function addDeclaration(node) {
|
|
43
|
+
if ((ts.isVariableDeclaration(node) || ts.isParameter(node)) && node.name) {
|
|
44
|
+
collectBindingNames(node.name, declareSymbol);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (ts.isBindingElement(node) && node.name) {
|
|
48
|
+
collectBindingNames(node.name, declareSymbol);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if ((ts.isFunctionDeclaration(node) ||
|
|
52
|
+
ts.isFunctionExpression(node) ||
|
|
53
|
+
ts.isArrowFunction(node)) &&
|
|
54
|
+
node.name) {
|
|
55
|
+
declareSymbol(node.name.text);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if ((ts.isClassDeclaration(node) || ts.isClassExpression(node)) && node.name) {
|
|
59
|
+
declareSymbol(node.name.text);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (ts.isEnumDeclaration(node) && node.name) {
|
|
63
|
+
declareSymbol(node.name.text);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
67
|
+
declareSymbol(node.name.text);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (ts.isTypeAliasDeclaration(node) && node.name) {
|
|
71
|
+
declareSymbol(node.name.text);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
75
|
+
declareSymbol(node.name.text);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (ts.isImportClause(node) && node.name) {
|
|
79
|
+
declareSymbol(node.name.text);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (ts.isNamespaceImport(node) && node.name) {
|
|
83
|
+
declareSymbol(node.name.text);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (ts.isImportSpecifier(node) && node.name) {
|
|
87
|
+
declareSymbol(node.name.text);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function collectBindingNames(name, collector) {
|
|
91
|
+
if (ts.isIdentifier(name)) {
|
|
92
|
+
if (name.text) {
|
|
93
|
+
collector(name.text);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (ts.isObjectBindingPattern(name)) {
|
|
98
|
+
for (const element of name.elements) {
|
|
99
|
+
if (element.name) {
|
|
100
|
+
collectBindingNames(element.name, collector);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (ts.isArrayBindingPattern(name)) {
|
|
106
|
+
for (const element of name.elements) {
|
|
107
|
+
if (ts.isBindingElement(element) && element.name) {
|
|
108
|
+
collectBindingNames(element.name, collector);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function getIdentifierText(node) {
|
|
114
|
+
const escaped = node.escapedText;
|
|
115
|
+
if (typeof escaped === 'string')
|
|
116
|
+
return escaped;
|
|
117
|
+
if (escaped !== undefined)
|
|
118
|
+
return String(escaped);
|
|
119
|
+
return node.getText(sourceFile);
|
|
120
|
+
}
|
|
121
|
+
function shouldSkipIdentifier(node) {
|
|
122
|
+
const parent = node.parent;
|
|
123
|
+
if (!parent)
|
|
124
|
+
return true;
|
|
125
|
+
if ((ts.isPropertyAccessExpression(parent) ||
|
|
126
|
+
ts.isPropertyAccessChain(parent)) &&
|
|
127
|
+
parent.name === node) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
if ((ts.isPropertyAssignment(parent) ||
|
|
131
|
+
ts.isPropertySignature(parent) ||
|
|
132
|
+
ts.isPropertyDeclaration(parent) ||
|
|
133
|
+
ts.isMethodDeclaration(parent) ||
|
|
134
|
+
ts.isMethodSignature(parent) ||
|
|
135
|
+
ts.isGetAccessorDeclaration(parent) ||
|
|
136
|
+
ts.isSetAccessorDeclaration(parent)) &&
|
|
137
|
+
parent.name === node) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (ts.isIndexSignatureDeclaration(parent))
|
|
141
|
+
return true;
|
|
142
|
+
if (ts.isEnumMember(parent) && parent.name === node)
|
|
143
|
+
return true;
|
|
144
|
+
if (ts.isBindingElement(parent) && parent.propertyName === node)
|
|
145
|
+
return true;
|
|
146
|
+
if (ts.isTypeReferenceNode(parent))
|
|
147
|
+
return true;
|
|
148
|
+
if (ts.isTypePredicateNode(parent))
|
|
149
|
+
return true;
|
|
150
|
+
if (ts.isTypeQueryNode(parent))
|
|
151
|
+
return true;
|
|
152
|
+
if (ts.isImportClause(parent) && parent.name === node)
|
|
153
|
+
return true;
|
|
154
|
+
if (ts.isNamespaceImport(parent) && parent.name === node)
|
|
155
|
+
return true;
|
|
156
|
+
if (ts.isImportSpecifier(parent)) {
|
|
157
|
+
if (parent.propertyName === node)
|
|
158
|
+
return true;
|
|
159
|
+
if (parent.name === node)
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (ts.isJsxAttribute(parent) && parent.name === node)
|
|
163
|
+
return true;
|
|
164
|
+
if (ts.isJsxNamespacedName(parent) && parent.name === node)
|
|
165
|
+
return true;
|
|
166
|
+
if (ts.isLabeledStatement(parent) && parent.label === node)
|
|
167
|
+
return true;
|
|
168
|
+
if ((ts.isBreakStatement(parent) || ts.isContinueStatement(parent)) &&
|
|
169
|
+
parent.label === node) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
if (ts.isMetaProperty(parent))
|
|
173
|
+
return true;
|
|
174
|
+
if (ts.isTypeAliasDeclaration(parent) && parent.name === node)
|
|
175
|
+
return true;
|
|
176
|
+
if (ts.isInterfaceDeclaration(parent) && parent.name === node)
|
|
177
|
+
return true;
|
|
178
|
+
if (ts.isClassDeclaration(parent) && parent.name === node)
|
|
179
|
+
return true;
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
function addReference(node) {
|
|
183
|
+
const name = getIdentifierText(node);
|
|
184
|
+
if (!name)
|
|
185
|
+
return;
|
|
186
|
+
if (shouldSkipIdentifier(node))
|
|
187
|
+
return;
|
|
188
|
+
if (isDeclarationIdentifier(node))
|
|
189
|
+
return;
|
|
190
|
+
if (declared.has(name))
|
|
191
|
+
return;
|
|
192
|
+
if (exclusionSet.has(name))
|
|
193
|
+
return;
|
|
194
|
+
referenced.add(name);
|
|
195
|
+
}
|
|
196
|
+
function isDeclarationIdentifier(node) {
|
|
197
|
+
const parent = node.parent;
|
|
198
|
+
if (!parent)
|
|
199
|
+
return false;
|
|
200
|
+
if ((ts.isVariableDeclaration(parent) ||
|
|
201
|
+
ts.isParameter(parent) ||
|
|
202
|
+
ts.isBindingElement(parent)) &&
|
|
203
|
+
parent.name === node) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if ((ts.isFunctionDeclaration(parent) ||
|
|
207
|
+
ts.isFunctionExpression(parent) ||
|
|
208
|
+
ts.isArrowFunction(parent) ||
|
|
209
|
+
ts.isClassDeclaration(parent) ||
|
|
210
|
+
ts.isClassExpression(parent) ||
|
|
211
|
+
ts.isEnumDeclaration(parent) ||
|
|
212
|
+
ts.isInterfaceDeclaration(parent) ||
|
|
213
|
+
ts.isTypeAliasDeclaration(parent)) &&
|
|
214
|
+
parent.name === node) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if ((ts.isImportClause(parent) ||
|
|
218
|
+
ts.isNamespaceImport(parent) ||
|
|
219
|
+
ts.isImportSpecifier(parent)) &&
|
|
220
|
+
parent.name === node) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
function overlapsRange(node) {
|
|
226
|
+
const nodeStart = node.getStart(sourceFile);
|
|
227
|
+
const nodeEnd = node.end;
|
|
228
|
+
return nodeEnd >= rangeStart && nodeStart <= rangeEnd;
|
|
229
|
+
}
|
|
230
|
+
function visit(node) {
|
|
231
|
+
if (!overlapsRange(node))
|
|
232
|
+
return;
|
|
233
|
+
addDeclaration(node);
|
|
234
|
+
if (isIdentifierLike(node)) {
|
|
235
|
+
addReference(node);
|
|
236
|
+
}
|
|
237
|
+
ts.forEachChild(node, visit);
|
|
238
|
+
}
|
|
239
|
+
visit(sourceFile);
|
|
240
|
+
return [...referenced].sort();
|
|
241
|
+
}
|
|
242
|
+
function clamp(value, min, max) {
|
|
243
|
+
if (min > max)
|
|
244
|
+
return min;
|
|
245
|
+
return Math.min(Math.max(value, min), max);
|
|
246
|
+
}
|
package/dist/menu.js
CHANGED
|
@@ -10,6 +10,7 @@ import { openDockerHelper } from './docker.js';
|
|
|
10
10
|
import { run } from './utils/run.js';
|
|
11
11
|
import { makeBaseScriptEntries, getPackageMarker, getPackageModule } from './menu/script-helpers.js';
|
|
12
12
|
import { runFormatChecker } from './format-checker/index.js';
|
|
13
|
+
import { runRobot } from './robot/index.js';
|
|
13
14
|
import { describeVersionControlScope, makeVersionControlEntries, } from './version-control.js';
|
|
14
15
|
function formatKindLabel(kind) {
|
|
15
16
|
if (kind === 'cli')
|
|
@@ -77,6 +78,14 @@ function makeManagerStepEntries(targets, packages, state) {
|
|
|
77
78
|
await runFormatChecker();
|
|
78
79
|
},
|
|
79
80
|
},
|
|
81
|
+
{
|
|
82
|
+
name: 'robot metadata',
|
|
83
|
+
emoji: '🤖',
|
|
84
|
+
description: 'Collect functions, components, types, consts, and classes across the workspace',
|
|
85
|
+
handler: async () => {
|
|
86
|
+
await runRobot();
|
|
87
|
+
},
|
|
88
|
+
},
|
|
80
89
|
{
|
|
81
90
|
name: 'build',
|
|
82
91
|
emoji: '🏗️',
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { colors } from '../../utils/log.js';
|
|
2
|
+
import { askLine } from '../../prompts.js';
|
|
3
|
+
import { formatValue, parseInteractiveValue, parseKindsInput, SETTING_DESCRIPTORS, validateRobotSettings, } from './settings.js';
|
|
4
|
+
import { promptInteractiveSettings, } from '../../cli/interactive-settings.js';
|
|
5
|
+
import { stdin as input } from 'node:process';
|
|
6
|
+
const READY_PROMPT = colors.dim('Settings retained. Use the keys above to adjust and confirm again.');
|
|
7
|
+
export async function promptRobotSettings(defaults) {
|
|
8
|
+
const supportsInteractive = typeof input.setRawMode === 'function' && input.isTTY;
|
|
9
|
+
let currentSettings = defaults;
|
|
10
|
+
while (true) {
|
|
11
|
+
const chosen = supportsInteractive
|
|
12
|
+
? await promptRobotSettingsInteractive(currentSettings)
|
|
13
|
+
: await promptRobotSettingsSequential(currentSettings);
|
|
14
|
+
const confirmed = await confirmExecution();
|
|
15
|
+
if (confirmed)
|
|
16
|
+
return chosen;
|
|
17
|
+
currentSettings = chosen;
|
|
18
|
+
console.log(READY_PROMPT);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function confirmExecution() {
|
|
22
|
+
const question = colors.cyan('Run the robot extractor with these settings? (Y/n): ');
|
|
23
|
+
while (true) {
|
|
24
|
+
const answer = (await askLine(question)).trim().toLowerCase();
|
|
25
|
+
if (!answer || answer === 'y' || answer === 'yes')
|
|
26
|
+
return true;
|
|
27
|
+
if (answer === 'n' || answer === 'no')
|
|
28
|
+
return false;
|
|
29
|
+
console.log(colors.yellow('Answer "yes" or "no", or press Enter to proceed.'));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function promptRobotSettingsSequential(defaults) {
|
|
33
|
+
console.log(colors.dim('Enter values to override defaults or press Enter to keep the current setting.'));
|
|
34
|
+
return {
|
|
35
|
+
includeKinds: await promptKinds(defaults.includeKinds),
|
|
36
|
+
exportedOnly: await promptBoolean('Only consider exported symbols', defaults.exportedOnly),
|
|
37
|
+
maxColumns: await promptNumber('Maximum columns', defaults.maxColumns),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function promptKinds(fallback) {
|
|
41
|
+
const descriptor = SETTING_DESCRIPTORS.find((entry) => entry.key === 'includeKinds');
|
|
42
|
+
if (!descriptor)
|
|
43
|
+
return fallback;
|
|
44
|
+
const question = colors.cyan(`Kinds to include [default ${formatValue(fallback, descriptor)}]: `);
|
|
45
|
+
while (true) {
|
|
46
|
+
const answer = await askLine(question);
|
|
47
|
+
if (!answer)
|
|
48
|
+
return fallback;
|
|
49
|
+
const parsed = parseKindsInput(answer);
|
|
50
|
+
if (parsed.error) {
|
|
51
|
+
console.log(colors.yellow(parsed.error));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (parsed.value)
|
|
55
|
+
return parsed.value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function promptBoolean(label, fallback) {
|
|
59
|
+
const question = colors.cyan(`${label} [default ${fallback ? 'yes' : 'no'}]: `);
|
|
60
|
+
while (true) {
|
|
61
|
+
const answer = (await askLine(question)).trim().toLowerCase();
|
|
62
|
+
if (!answer)
|
|
63
|
+
return fallback;
|
|
64
|
+
if (['yes', 'y', 'true', '1'].includes(answer))
|
|
65
|
+
return true;
|
|
66
|
+
if (['no', 'n', 'false', '0'].includes(answer))
|
|
67
|
+
return false;
|
|
68
|
+
console.log(colors.yellow('Please answer "yes" or "no", or leave blank to keep the default.'));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function promptNumber(label, fallback) {
|
|
72
|
+
const question = colors.cyan(`${label} [default ${fallback}]: `);
|
|
73
|
+
while (true) {
|
|
74
|
+
const answer = await askLine(question);
|
|
75
|
+
if (!answer)
|
|
76
|
+
return fallback;
|
|
77
|
+
const parsed = Number(answer);
|
|
78
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
79
|
+
return Math.floor(parsed);
|
|
80
|
+
}
|
|
81
|
+
console.log(colors.yellow('Provide a positive integer or leave blank to keep the default.'));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function promptRobotSettingsInteractive(defaults) {
|
|
85
|
+
const descriptors = SETTING_DESCRIPTORS.map((descriptor) => ({
|
|
86
|
+
key: descriptor.key,
|
|
87
|
+
label: descriptor.label,
|
|
88
|
+
unit: descriptor.unit,
|
|
89
|
+
format: (value) => formatValue(value, descriptor),
|
|
90
|
+
parse: (buffer) => parseInteractiveValue(descriptor, buffer),
|
|
91
|
+
}));
|
|
92
|
+
return promptInteractiveSettings({
|
|
93
|
+
title: 'Robot settings (type to edit values)',
|
|
94
|
+
descriptors,
|
|
95
|
+
initial: defaults,
|
|
96
|
+
instructions: [
|
|
97
|
+
'Use ↑/↓ to change rows, type comma separated values for kinds, Backspace to clear characters, and Enter to validate and confirm the selection.',
|
|
98
|
+
'Press Esc/Ctrl+C to abort, or hit Enter again after reviewing the summary to continue.',
|
|
99
|
+
],
|
|
100
|
+
validate: validateRobotSettings,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ROBOT_KINDS } from '../types.js';
|
|
2
|
+
const KNOWN_KINDS = new Set(ROBOT_KINDS);
|
|
3
|
+
export const SETTING_DESCRIPTORS = [
|
|
4
|
+
{
|
|
5
|
+
key: 'includeKinds',
|
|
6
|
+
label: 'Kinds to include',
|
|
7
|
+
unit: 'comma-separated (function, component, type, const, class)',
|
|
8
|
+
type: 'list',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
key: 'exportedOnly',
|
|
12
|
+
label: 'Only exported symbols',
|
|
13
|
+
type: 'boolean',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: 'maxColumns',
|
|
17
|
+
label: 'Maximum columns',
|
|
18
|
+
unit: 'columns',
|
|
19
|
+
type: 'number',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
export function formatValue(value, descriptor) {
|
|
23
|
+
if (descriptor.type === 'boolean') {
|
|
24
|
+
return value ? 'true' : 'false';
|
|
25
|
+
}
|
|
26
|
+
if (descriptor.type === 'list') {
|
|
27
|
+
const kinds = value;
|
|
28
|
+
if (kinds.length === ROBOT_KINDS.length)
|
|
29
|
+
return 'all';
|
|
30
|
+
return kinds.join(', ');
|
|
31
|
+
}
|
|
32
|
+
return `${value}`;
|
|
33
|
+
}
|
|
34
|
+
export function parseInteractiveValue(descriptor, buffer) {
|
|
35
|
+
const trimmed = buffer.trim();
|
|
36
|
+
if (!trimmed) {
|
|
37
|
+
return { value: undefined };
|
|
38
|
+
}
|
|
39
|
+
if (descriptor.type === 'number') {
|
|
40
|
+
const parsed = parsePositiveInteger(trimmed, { allowZero: descriptor.allowZero });
|
|
41
|
+
if (parsed === undefined) {
|
|
42
|
+
return {
|
|
43
|
+
error: `${descriptor.label} requires a ${descriptor.allowZero ? 'non-negative' : 'positive'} integer.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { value: parsed };
|
|
47
|
+
}
|
|
48
|
+
if (descriptor.type === 'boolean') {
|
|
49
|
+
const parsed = parseBooleanInput(trimmed);
|
|
50
|
+
if (parsed.error)
|
|
51
|
+
return { error: parsed.error };
|
|
52
|
+
return { value: parsed.value };
|
|
53
|
+
}
|
|
54
|
+
if (descriptor.type === 'list') {
|
|
55
|
+
return parseKindsInput(trimmed);
|
|
56
|
+
}
|
|
57
|
+
return { value: undefined };
|
|
58
|
+
}
|
|
59
|
+
export function validateRobotSettings(settings) {
|
|
60
|
+
if (!settings.includeKinds.length) {
|
|
61
|
+
return 'Select at least one kind to include.';
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isFinite(settings.maxColumns) || settings.maxColumns <= 0) {
|
|
64
|
+
return 'Maximum columns must be a positive number.';
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
function parsePositiveInteger(inputValue, options) {
|
|
69
|
+
const num = Number(inputValue);
|
|
70
|
+
if (!Number.isFinite(num))
|
|
71
|
+
return undefined;
|
|
72
|
+
const floored = Math.floor(num);
|
|
73
|
+
const min = options?.allowZero ? 0 : 1;
|
|
74
|
+
if (floored < min)
|
|
75
|
+
return undefined;
|
|
76
|
+
return floored;
|
|
77
|
+
}
|
|
78
|
+
function parseBooleanInput(raw) {
|
|
79
|
+
const normalized = raw.trim().toLowerCase();
|
|
80
|
+
if (['yes', 'y', 'true', '1'].includes(normalized))
|
|
81
|
+
return { value: true };
|
|
82
|
+
if (['no', 'n', 'false', '0'].includes(normalized))
|
|
83
|
+
return { value: false };
|
|
84
|
+
return { error: 'Provide "true" or "false".' };
|
|
85
|
+
}
|
|
86
|
+
export function parseKindsInput(raw) {
|
|
87
|
+
const normalized = raw
|
|
88
|
+
.split(',')
|
|
89
|
+
.map((chunk) => chunk.trim().toLowerCase())
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
if (normalized.length === 0) {
|
|
92
|
+
return { error: 'Enter at least one kind or "all".' };
|
|
93
|
+
}
|
|
94
|
+
if (normalized.length === 1 && normalized[0] === 'all') {
|
|
95
|
+
return { value: [...ROBOT_KINDS] };
|
|
96
|
+
}
|
|
97
|
+
const invalid = [];
|
|
98
|
+
const unique = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
for (const entry of normalized) {
|
|
101
|
+
if (!KNOWN_KINDS.has(entry)) {
|
|
102
|
+
invalid.push(entry);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const kind = entry;
|
|
106
|
+
if (!seen.has(kind)) {
|
|
107
|
+
seen.add(kind);
|
|
108
|
+
unique.push(kind);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (invalid.length > 0) {
|
|
112
|
+
return {
|
|
113
|
+
error: `Unknown kinds: ${invalid.join(', ')}. Valid kinds are ${ROBOT_KINDS.join(', ')}.`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (unique.length === 0) {
|
|
117
|
+
return { error: 'Provide at least one valid kind.' };
|
|
118
|
+
}
|
|
119
|
+
return { value: unique };
|
|
120
|
+
}
|