@bfra.me/workspace-analyzer 0.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 +402 -0
- package/lib/chunk-4LSFAAZW.js +1 -0
- package/lib/chunk-JDF7DQ4V.js +27 -0
- package/lib/chunk-WOJ4C7N7.js +7122 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +318 -0
- package/lib/index.d.ts +3701 -0
- package/lib/index.js +1262 -0
- package/lib/types/index.d.ts +146 -0
- package/lib/types/index.js +28 -0
- package/package.json +89 -0
- package/src/analyzers/analyzer.ts +201 -0
- package/src/analyzers/architectural-analyzer.ts +304 -0
- package/src/analyzers/build-config-analyzer.ts +334 -0
- package/src/analyzers/circular-import-analyzer.ts +463 -0
- package/src/analyzers/config-consistency-analyzer.ts +335 -0
- package/src/analyzers/dead-code-analyzer.ts +565 -0
- package/src/analyzers/duplicate-code-analyzer.ts +626 -0
- package/src/analyzers/duplicate-dependency-analyzer.ts +381 -0
- package/src/analyzers/eslint-config-analyzer.ts +281 -0
- package/src/analyzers/exports-field-analyzer.ts +324 -0
- package/src/analyzers/index.ts +388 -0
- package/src/analyzers/large-dependency-analyzer.ts +535 -0
- package/src/analyzers/package-json-analyzer.ts +349 -0
- package/src/analyzers/peer-dependency-analyzer.ts +275 -0
- package/src/analyzers/tree-shaking-analyzer.ts +623 -0
- package/src/analyzers/tsconfig-analyzer.ts +382 -0
- package/src/analyzers/unused-dependency-analyzer.ts +356 -0
- package/src/analyzers/version-alignment-analyzer.ts +308 -0
- package/src/api/analyze-workspace.ts +245 -0
- package/src/api/index.ts +11 -0
- package/src/cache/cache-manager.ts +495 -0
- package/src/cache/cache-schema.ts +247 -0
- package/src/cache/change-detector.ts +169 -0
- package/src/cache/file-hasher.ts +65 -0
- package/src/cache/index.ts +47 -0
- package/src/cli/commands/analyze.ts +240 -0
- package/src/cli/commands/index.ts +5 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/types.ts +65 -0
- package/src/cli/ui.ts +213 -0
- package/src/cli.ts +9 -0
- package/src/config/defaults.ts +183 -0
- package/src/config/index.ts +81 -0
- package/src/config/loader.ts +270 -0
- package/src/config/merger.ts +229 -0
- package/src/config/schema.ts +263 -0
- package/src/core/incremental-analyzer.ts +462 -0
- package/src/core/index.ts +34 -0
- package/src/core/orchestrator.ts +416 -0
- package/src/graph/dependency-graph.ts +408 -0
- package/src/graph/index.ts +19 -0
- package/src/index.ts +417 -0
- package/src/parser/config-parser.ts +491 -0
- package/src/parser/import-extractor.ts +340 -0
- package/src/parser/index.ts +54 -0
- package/src/parser/typescript-parser.ts +95 -0
- package/src/performance/bundle-estimator.ts +444 -0
- package/src/performance/index.ts +27 -0
- package/src/reporters/console-reporter.ts +355 -0
- package/src/reporters/index.ts +49 -0
- package/src/reporters/json-reporter.ts +273 -0
- package/src/reporters/markdown-reporter.ts +349 -0
- package/src/reporters/reporter.ts +399 -0
- package/src/rules/builtin-rules.ts +709 -0
- package/src/rules/index.ts +52 -0
- package/src/rules/rule-engine.ts +409 -0
- package/src/scanner/index.ts +18 -0
- package/src/scanner/workspace-scanner.ts +403 -0
- package/src/types/index.ts +176 -0
- package/src/types/result.ts +19 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/pattern-matcher.ts +48 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze command implementation.
|
|
3
|
+
*
|
|
4
|
+
* Provides the main 'analyze' command for running workspace analysis
|
|
5
|
+
* with interactive and non-interactive modes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {AnalysisResult} from '../../types/index'
|
|
9
|
+
import type {AnalyzeOptions, AnalyzerSelectionOption} from '../types'
|
|
10
|
+
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import process from 'node:process'
|
|
13
|
+
|
|
14
|
+
import {BUILTIN_ANALYZER_IDS} from '../../analyzers/index'
|
|
15
|
+
import {analyzeWorkspace} from '../../api/analyze-workspace'
|
|
16
|
+
import {createConsoleReporter} from '../../reporters/console-reporter'
|
|
17
|
+
import {createJsonReporter} from '../../reporters/json-reporter'
|
|
18
|
+
import {createMarkdownReporter} from '../../reporters/markdown-reporter'
|
|
19
|
+
import {
|
|
20
|
+
createLogger,
|
|
21
|
+
createSpinner,
|
|
22
|
+
formatDuration,
|
|
23
|
+
formatSeveritySummary,
|
|
24
|
+
handleCancel,
|
|
25
|
+
selectAnalyzers,
|
|
26
|
+
showCancel,
|
|
27
|
+
showIntro,
|
|
28
|
+
showOutro,
|
|
29
|
+
} from '../ui'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Metadata for built-in analyzers used in selection UI.
|
|
33
|
+
*/
|
|
34
|
+
const ANALYZER_METADATA: Record<string, {label: string; hint: string}> = {
|
|
35
|
+
[BUILTIN_ANALYZER_IDS.PACKAGE_JSON]: {
|
|
36
|
+
label: 'Package.json Analyzer',
|
|
37
|
+
hint: 'Validates package.json structure and required fields',
|
|
38
|
+
},
|
|
39
|
+
[BUILTIN_ANALYZER_IDS.TSCONFIG]: {
|
|
40
|
+
label: 'TSConfig Analyzer',
|
|
41
|
+
hint: 'Checks TypeScript configuration consistency',
|
|
42
|
+
},
|
|
43
|
+
[BUILTIN_ANALYZER_IDS.ESLINT_CONFIG]: {
|
|
44
|
+
label: 'ESLint Config Analyzer',
|
|
45
|
+
hint: 'Validates ESLint configuration patterns',
|
|
46
|
+
},
|
|
47
|
+
[BUILTIN_ANALYZER_IDS.BUILD_CONFIG]: {
|
|
48
|
+
label: 'Build Config Analyzer',
|
|
49
|
+
hint: 'Analyzes tsup and build configuration',
|
|
50
|
+
},
|
|
51
|
+
[BUILTIN_ANALYZER_IDS.CONFIG_CONSISTENCY]: {
|
|
52
|
+
label: 'Config Consistency',
|
|
53
|
+
hint: 'Cross-validates multiple configuration files',
|
|
54
|
+
},
|
|
55
|
+
[BUILTIN_ANALYZER_IDS.VERSION_ALIGNMENT]: {
|
|
56
|
+
label: 'Version Alignment',
|
|
57
|
+
hint: 'Checks dependency version consistency',
|
|
58
|
+
},
|
|
59
|
+
[BUILTIN_ANALYZER_IDS.EXPORTS_FIELD]: {
|
|
60
|
+
label: 'Exports Field Analyzer',
|
|
61
|
+
hint: 'Validates package.json exports against source files',
|
|
62
|
+
},
|
|
63
|
+
[BUILTIN_ANALYZER_IDS.UNUSED_DEPENDENCY]: {
|
|
64
|
+
label: 'Unused Dependencies',
|
|
65
|
+
hint: 'Detects dependencies not used in source code',
|
|
66
|
+
},
|
|
67
|
+
[BUILTIN_ANALYZER_IDS.CIRCULAR_IMPORT]: {
|
|
68
|
+
label: 'Circular Imports',
|
|
69
|
+
hint: 'Finds circular import dependencies',
|
|
70
|
+
},
|
|
71
|
+
[BUILTIN_ANALYZER_IDS.PEER_DEPENDENCY]: {
|
|
72
|
+
label: 'Peer Dependencies',
|
|
73
|
+
hint: 'Validates peer dependency requirements',
|
|
74
|
+
},
|
|
75
|
+
[BUILTIN_ANALYZER_IDS.DUPLICATE_DEPENDENCY]: {
|
|
76
|
+
label: 'Duplicate Dependencies',
|
|
77
|
+
hint: 'Finds duplicate packages across workspace',
|
|
78
|
+
},
|
|
79
|
+
[BUILTIN_ANALYZER_IDS.ARCHITECTURAL]: {
|
|
80
|
+
label: 'Architecture',
|
|
81
|
+
hint: 'Validates architectural patterns and layers',
|
|
82
|
+
},
|
|
83
|
+
[BUILTIN_ANALYZER_IDS.DEAD_CODE]: {
|
|
84
|
+
label: 'Dead Code',
|
|
85
|
+
hint: 'Detects unreachable or unused exports',
|
|
86
|
+
},
|
|
87
|
+
[BUILTIN_ANALYZER_IDS.DUPLICATE_CODE]: {
|
|
88
|
+
label: 'Duplicate Code',
|
|
89
|
+
hint: 'Finds similar code patterns via AST fingerprinting',
|
|
90
|
+
},
|
|
91
|
+
[BUILTIN_ANALYZER_IDS.LARGE_DEPENDENCY]: {
|
|
92
|
+
label: 'Large Dependencies',
|
|
93
|
+
hint: 'Identifies oversized dependencies',
|
|
94
|
+
},
|
|
95
|
+
[BUILTIN_ANALYZER_IDS.TREE_SHAKING_BLOCKER]: {
|
|
96
|
+
label: 'Tree Shaking Blockers',
|
|
97
|
+
hint: 'Finds patterns that prevent tree shaking',
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Gets available analyzer options for selection UI.
|
|
103
|
+
*/
|
|
104
|
+
function getAnalyzerOptions(): readonly AnalyzerSelectionOption[] {
|
|
105
|
+
return Object.entries(BUILTIN_ANALYZER_IDS).map(([_key, id]) => {
|
|
106
|
+
const metadata = ANALYZER_METADATA[id] ?? {
|
|
107
|
+
label: id,
|
|
108
|
+
hint: 'Analyzer',
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
value: id,
|
|
112
|
+
label: metadata.label,
|
|
113
|
+
hint: metadata.hint,
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Reports analysis results to the console or other formats.
|
|
120
|
+
*/
|
|
121
|
+
function reportResults(result: AnalysisResult, options: AnalyzeOptions): void {
|
|
122
|
+
if (options.json === true) {
|
|
123
|
+
const jsonReporter = createJsonReporter()
|
|
124
|
+
const report = jsonReporter.generate(result)
|
|
125
|
+
console.log(JSON.stringify(report, null, 2))
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (options.markdown === true) {
|
|
130
|
+
const mdReporter = createMarkdownReporter()
|
|
131
|
+
const report = mdReporter.generate(result)
|
|
132
|
+
console.log(report)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const consoleReporter = createConsoleReporter({verbose: options.verbose})
|
|
137
|
+
consoleReporter.generate(result)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Runs the analyze command.
|
|
142
|
+
*/
|
|
143
|
+
export async function runAnalyze(inputPath: string, options: AnalyzeOptions): Promise<void> {
|
|
144
|
+
const logger = createLogger(options)
|
|
145
|
+
const rootDir = path.resolve(options.root ?? inputPath)
|
|
146
|
+
|
|
147
|
+
if (options.quiet !== true) {
|
|
148
|
+
showIntro('🔍 Workspace Analyzer')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let selectedAnalyzers: readonly string[] | undefined
|
|
152
|
+
|
|
153
|
+
if (options.interactive === true) {
|
|
154
|
+
const availableAnalyzers = getAnalyzerOptions()
|
|
155
|
+
const selection = await selectAnalyzers(availableAnalyzers)
|
|
156
|
+
|
|
157
|
+
if (handleCancel(selection)) {
|
|
158
|
+
showCancel()
|
|
159
|
+
process.exit(0)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
selectedAnalyzers = selection
|
|
163
|
+
logger.debug(`Selected ${selectedAnalyzers.length} analyzers: ${selectedAnalyzers.join(', ')}`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (options.dryRun === true) {
|
|
167
|
+
logger.info(`[DRY RUN] Would analyze workspace at: ${rootDir}`)
|
|
168
|
+
if (selectedAnalyzers != null && selectedAnalyzers.length > 0) {
|
|
169
|
+
logger.info(`[DRY RUN] Using analyzers: ${selectedAnalyzers.join(', ')}`)
|
|
170
|
+
} else {
|
|
171
|
+
logger.info('[DRY RUN] Using all analyzers')
|
|
172
|
+
}
|
|
173
|
+
if (options.config != null) {
|
|
174
|
+
logger.info(`[DRY RUN] Using config: ${options.config}`)
|
|
175
|
+
}
|
|
176
|
+
if (options.quiet !== true) {
|
|
177
|
+
showOutro('Dry run complete - no analysis performed')
|
|
178
|
+
}
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (options.fix === true) {
|
|
183
|
+
logger.warn('Auto-fix mode is not yet implemented. Running analysis only.')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const spinner = options.quiet === true ? undefined : createSpinner()
|
|
187
|
+
spinner?.start('Analyzing workspace...')
|
|
188
|
+
|
|
189
|
+
const startTime = Date.now()
|
|
190
|
+
|
|
191
|
+
const result = await analyzeWorkspace(rootDir, {
|
|
192
|
+
configPath: options.config,
|
|
193
|
+
verbose: options.verbose,
|
|
194
|
+
minSeverity: options.minSeverity,
|
|
195
|
+
analyzers:
|
|
196
|
+
selectedAnalyzers != null && selectedAnalyzers.length > 0
|
|
197
|
+
? Object.fromEntries(
|
|
198
|
+
Object.values(BUILTIN_ANALYZER_IDS).map(id => [
|
|
199
|
+
id,
|
|
200
|
+
{enabled: selectedAnalyzers.includes(id)},
|
|
201
|
+
]),
|
|
202
|
+
)
|
|
203
|
+
: undefined,
|
|
204
|
+
onProgress: progress => {
|
|
205
|
+
const totalSuffix = progress.total == null ? '' : `/${progress.total}`
|
|
206
|
+
const currentItem = progress.current ?? ''
|
|
207
|
+
const message = `${progress.phase}: ${currentItem} (${progress.processed}${totalSuffix})`
|
|
208
|
+
spinner?.message(message)
|
|
209
|
+
logger.debug(message)
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const duration = Date.now() - startTime
|
|
214
|
+
spinner?.stop(`Analysis complete in ${formatDuration(duration)}`)
|
|
215
|
+
|
|
216
|
+
if (result.success) {
|
|
217
|
+
const analysisResult = result.data
|
|
218
|
+
|
|
219
|
+
reportResults(analysisResult, options)
|
|
220
|
+
|
|
221
|
+
if (options.quiet !== true) {
|
|
222
|
+
const summary = formatSeveritySummary(analysisResult.summary.bySeverity)
|
|
223
|
+
showOutro(`${summary} (${analysisResult.summary.totalIssues} total issues)`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const hasErrors =
|
|
227
|
+
(analysisResult.summary.bySeverity.error ?? 0) > 0 ||
|
|
228
|
+
(analysisResult.summary.bySeverity.critical ?? 0) > 0
|
|
229
|
+
|
|
230
|
+
if (hasErrors) {
|
|
231
|
+
process.exit(1)
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
logger.error(`Analysis failed: ${result.error.message}`)
|
|
235
|
+
if (options.verbose === true && result.error.cause != null) {
|
|
236
|
+
logger.debug(`Cause: ${String(result.error.cause)}`)
|
|
237
|
+
}
|
|
238
|
+
process.exit(1)
|
|
239
|
+
}
|
|
240
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for workspace-analyzer.
|
|
4
|
+
*
|
|
5
|
+
* Provides interactive workspace analysis with progress reporting using @clack/prompts.
|
|
6
|
+
* Uses cac for argument parsing following doc-sync's CLI patterns.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {AnalyzeOptions} from './types'
|
|
10
|
+
|
|
11
|
+
import process from 'node:process'
|
|
12
|
+
|
|
13
|
+
import {cac} from 'cac'
|
|
14
|
+
import {consola} from 'consola'
|
|
15
|
+
|
|
16
|
+
import {runAnalyze} from './commands/analyze'
|
|
17
|
+
|
|
18
|
+
const cli = cac('workspace-analyzer')
|
|
19
|
+
|
|
20
|
+
function registerAnalyzeCommand(
|
|
21
|
+
command: ReturnType<typeof cli.command>,
|
|
22
|
+
): ReturnType<typeof cli.command> {
|
|
23
|
+
return command
|
|
24
|
+
.option('-c, --config <path>', 'Path to workspace-analyzer.config.ts file')
|
|
25
|
+
.option('-r, --root <dir>', 'Root directory of the workspace', {default: process.cwd()})
|
|
26
|
+
.option('--json', 'Output results as JSON')
|
|
27
|
+
.option('--markdown', 'Output results as Markdown')
|
|
28
|
+
.option('-i, --interactive', 'Use interactive analyzer selection')
|
|
29
|
+
.option('--fix', 'Attempt to auto-fix issues (placeholder for future)')
|
|
30
|
+
.option('-d, --dry-run', 'Preview what would be analyzed without running')
|
|
31
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
32
|
+
.option('-q, --quiet', 'Suppress non-essential output')
|
|
33
|
+
.option(
|
|
34
|
+
'--min-severity <level>',
|
|
35
|
+
'Minimum severity level to report (info, warning, error, critical)',
|
|
36
|
+
)
|
|
37
|
+
.action(async (inputPath = '.', options: AnalyzeOptions) => {
|
|
38
|
+
const workspacePath = String(inputPath)
|
|
39
|
+
|
|
40
|
+
if (options.verbose === true) {
|
|
41
|
+
consola.level = 4
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await runAnalyze(workspacePath, options)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Default command: workspace-analyzer [path]
|
|
49
|
+
registerAnalyzeCommand(cli.command('[path]', 'Analyze a workspace for issues'))
|
|
50
|
+
|
|
51
|
+
// Explicit command: workspace-analyzer analyze [path]
|
|
52
|
+
registerAnalyzeCommand(cli.command('analyze [path]', 'Analyze a workspace for issues'))
|
|
53
|
+
|
|
54
|
+
cli.command('version', 'Show version information').action(() => {
|
|
55
|
+
consola.info('@bfra.me/workspace-analyzer v0.0.0')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
cli.help()
|
|
59
|
+
cli.version('0.0.0')
|
|
60
|
+
|
|
61
|
+
cli.parse()
|
package/src/cli/types.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for CLI options and state.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {Severity} from '../types/index'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Global options available to all CLI commands.
|
|
9
|
+
*/
|
|
10
|
+
export interface GlobalOptions {
|
|
11
|
+
/** Root directory of the workspace (default: current working directory) */
|
|
12
|
+
readonly root: string
|
|
13
|
+
/** Path to configuration file */
|
|
14
|
+
readonly config?: string
|
|
15
|
+
/** Enable verbose output for debugging */
|
|
16
|
+
readonly verbose?: boolean
|
|
17
|
+
/** Suppress non-error output */
|
|
18
|
+
readonly quiet?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for the analyze command.
|
|
23
|
+
*/
|
|
24
|
+
export interface AnalyzeOptions extends GlobalOptions {
|
|
25
|
+
/** Output results as JSON */
|
|
26
|
+
readonly json?: boolean
|
|
27
|
+
/** Output results as Markdown */
|
|
28
|
+
readonly markdown?: boolean
|
|
29
|
+
/** Show what would be analyzed without making changes */
|
|
30
|
+
readonly dryRun?: boolean
|
|
31
|
+
/** Enable interactive mode for analyzer selection */
|
|
32
|
+
readonly interactive?: boolean
|
|
33
|
+
/** Attempt to auto-fix issues where supported */
|
|
34
|
+
readonly fix?: boolean
|
|
35
|
+
/** Minimum severity level to report */
|
|
36
|
+
readonly minSeverity?: Severity
|
|
37
|
+
/** Specific analyzers to run (empty means all) */
|
|
38
|
+
readonly analyzers?: readonly string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Option for analyzer selection in interactive mode.
|
|
43
|
+
*/
|
|
44
|
+
export interface AnalyzerSelectionOption {
|
|
45
|
+
/** Analyzer ID */
|
|
46
|
+
readonly value: string
|
|
47
|
+
/** Display label */
|
|
48
|
+
readonly label: string
|
|
49
|
+
/** Optional hint text */
|
|
50
|
+
readonly hint?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Status information for individual analyzer results.
|
|
55
|
+
*/
|
|
56
|
+
export interface AnalyzerStatus {
|
|
57
|
+
/** Analyzer ID */
|
|
58
|
+
readonly analyzerId: string
|
|
59
|
+
/** Whether the analyzer completed successfully */
|
|
60
|
+
readonly success: boolean
|
|
61
|
+
/** Number of issues found */
|
|
62
|
+
readonly issueCount: number
|
|
63
|
+
/** Duration in milliseconds */
|
|
64
|
+
readonly durationMs: number
|
|
65
|
+
}
|
package/src/cli/ui.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI utilities for CLI using @clack/prompts.
|
|
3
|
+
*
|
|
4
|
+
* Provides a consistent interface for terminal output, progress reporting,
|
|
5
|
+
* and interactive prompts adapted from doc-sync's UI patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {AnalyzerSelectionOption, GlobalOptions} from './types'
|
|
9
|
+
|
|
10
|
+
import * as p from '@clack/prompts'
|
|
11
|
+
import {consola} from 'consola'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Logger interface for consistent output across CLI commands.
|
|
15
|
+
*/
|
|
16
|
+
export interface Logger {
|
|
17
|
+
readonly info: (message: string) => void
|
|
18
|
+
readonly success: (message: string) => void
|
|
19
|
+
readonly warn: (message: string) => void
|
|
20
|
+
readonly error: (message: string) => void
|
|
21
|
+
readonly debug: (message: string) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Options for creating a logger.
|
|
26
|
+
*/
|
|
27
|
+
export interface LoggerOptions {
|
|
28
|
+
readonly verbose?: boolean
|
|
29
|
+
readonly quiet?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a logger that respects verbose/quiet options.
|
|
34
|
+
*/
|
|
35
|
+
export function createLogger(options: LoggerOptions): Logger {
|
|
36
|
+
const {verbose = false, quiet = false} = options
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
info(message: string): void {
|
|
40
|
+
if (!quiet) {
|
|
41
|
+
p.log.info(message)
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
success(message: string): void {
|
|
45
|
+
if (!quiet) {
|
|
46
|
+
p.log.success(message)
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
warn(message: string): void {
|
|
50
|
+
p.log.warn(message)
|
|
51
|
+
},
|
|
52
|
+
error(message: string): void {
|
|
53
|
+
p.log.error(message)
|
|
54
|
+
},
|
|
55
|
+
debug(message: string): void {
|
|
56
|
+
if (verbose) {
|
|
57
|
+
consola.debug(message)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Shows intro banner for the CLI.
|
|
65
|
+
*/
|
|
66
|
+
export function showIntro(title: string): void {
|
|
67
|
+
p.intro(title)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Shows outro message when CLI completes.
|
|
72
|
+
*/
|
|
73
|
+
export function showOutro(message: string): void {
|
|
74
|
+
p.outro(message)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates a spinner for long-running operations.
|
|
79
|
+
*/
|
|
80
|
+
export function createSpinner(): ReturnType<typeof p.spinner> {
|
|
81
|
+
return p.spinner()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Shows cancellation message when user cancels operation.
|
|
86
|
+
*/
|
|
87
|
+
export function showCancel(message = 'Operation cancelled.'): void {
|
|
88
|
+
p.cancel(message)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Checks if a value represents a user cancellation.
|
|
93
|
+
*/
|
|
94
|
+
export function handleCancel(value: unknown): value is symbol {
|
|
95
|
+
return p.isCancel(value)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Prompts user to select analyzers to run.
|
|
100
|
+
*/
|
|
101
|
+
export async function selectAnalyzers(
|
|
102
|
+
availableAnalyzers: readonly AnalyzerSelectionOption[],
|
|
103
|
+
): Promise<readonly string[] | symbol> {
|
|
104
|
+
if (availableAnalyzers.length === 0) {
|
|
105
|
+
return []
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const options = availableAnalyzers.map(analyzer => ({
|
|
109
|
+
value: analyzer.value,
|
|
110
|
+
label: analyzer.label,
|
|
111
|
+
hint: analyzer.hint,
|
|
112
|
+
}))
|
|
113
|
+
|
|
114
|
+
const selected = await p.multiselect({
|
|
115
|
+
message: 'Select analyzers to run',
|
|
116
|
+
options,
|
|
117
|
+
required: false,
|
|
118
|
+
initialValues: options.map(o => o.value),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return selected
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Prompts user to confirm an action.
|
|
126
|
+
*/
|
|
127
|
+
export async function confirmAction(message: string): Promise<boolean | symbol> {
|
|
128
|
+
return p.confirm({message})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Formats a duration in milliseconds to a human-readable string.
|
|
133
|
+
*/
|
|
134
|
+
export function formatDuration(ms: number): string {
|
|
135
|
+
if (ms < 1000) {
|
|
136
|
+
return `${ms}ms`
|
|
137
|
+
}
|
|
138
|
+
if (ms < 60000) {
|
|
139
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
140
|
+
}
|
|
141
|
+
const minutes = Math.floor(ms / 60000)
|
|
142
|
+
const seconds = Math.round((ms % 60000) / 1000)
|
|
143
|
+
return `${minutes}m ${seconds}s`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Formats a list of items for display.
|
|
148
|
+
*/
|
|
149
|
+
export function formatList(items: readonly string[], maxDisplay = 3): string {
|
|
150
|
+
if (items.length === 0) {
|
|
151
|
+
return 'none'
|
|
152
|
+
}
|
|
153
|
+
if (items.length === 1) {
|
|
154
|
+
return items[0] ?? 'unknown'
|
|
155
|
+
}
|
|
156
|
+
if (items.length <= maxDisplay) {
|
|
157
|
+
return items.join(', ')
|
|
158
|
+
}
|
|
159
|
+
return `${items.slice(0, maxDisplay).join(', ')} and ${items.length - maxDisplay} more`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Creates a progress callback suitable for the onProgress option.
|
|
164
|
+
*/
|
|
165
|
+
export function createProgressCallback(options: GlobalOptions): (message: string) => void {
|
|
166
|
+
const logger = createLogger(options)
|
|
167
|
+
return (message: string) => {
|
|
168
|
+
logger.debug(message)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Formats issue count with appropriate color indicator.
|
|
174
|
+
*/
|
|
175
|
+
export function formatIssueCount(
|
|
176
|
+
count: number,
|
|
177
|
+
severity: 'info' | 'warning' | 'error' | 'critical',
|
|
178
|
+
): string {
|
|
179
|
+
const severityIndicators = {
|
|
180
|
+
info: 'ℹ️',
|
|
181
|
+
warning: '⚠️',
|
|
182
|
+
error: '❌',
|
|
183
|
+
critical: '🚨',
|
|
184
|
+
}
|
|
185
|
+
return `${severityIndicators[severity]} ${count} ${severity}${count === 1 ? '' : 's'}`
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Formats a severity-based summary for display.
|
|
190
|
+
*/
|
|
191
|
+
export function formatSeveritySummary(bySeverity: Readonly<Record<string, number>>): string {
|
|
192
|
+
const parts: string[] = []
|
|
193
|
+
|
|
194
|
+
const critical = bySeverity.critical ?? 0
|
|
195
|
+
const error = bySeverity.error ?? 0
|
|
196
|
+
const warning = bySeverity.warning ?? 0
|
|
197
|
+
const info = bySeverity.info ?? 0
|
|
198
|
+
|
|
199
|
+
if (critical > 0) {
|
|
200
|
+
parts.push(formatIssueCount(critical, 'critical'))
|
|
201
|
+
}
|
|
202
|
+
if (error > 0) {
|
|
203
|
+
parts.push(formatIssueCount(error, 'error'))
|
|
204
|
+
}
|
|
205
|
+
if (warning > 0) {
|
|
206
|
+
parts.push(formatIssueCount(warning, 'warning'))
|
|
207
|
+
}
|
|
208
|
+
if (info > 0) {
|
|
209
|
+
parts.push(formatIssueCount(info, 'info'))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return parts.length > 0 ? parts.join(', ') : '✅ No issues'
|
|
213
|
+
}
|