@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.
Files changed (73) hide show
  1. package/README.md +402 -0
  2. package/lib/chunk-4LSFAAZW.js +1 -0
  3. package/lib/chunk-JDF7DQ4V.js +27 -0
  4. package/lib/chunk-WOJ4C7N7.js +7122 -0
  5. package/lib/cli.d.ts +1 -0
  6. package/lib/cli.js +318 -0
  7. package/lib/index.d.ts +3701 -0
  8. package/lib/index.js +1262 -0
  9. package/lib/types/index.d.ts +146 -0
  10. package/lib/types/index.js +28 -0
  11. package/package.json +89 -0
  12. package/src/analyzers/analyzer.ts +201 -0
  13. package/src/analyzers/architectural-analyzer.ts +304 -0
  14. package/src/analyzers/build-config-analyzer.ts +334 -0
  15. package/src/analyzers/circular-import-analyzer.ts +463 -0
  16. package/src/analyzers/config-consistency-analyzer.ts +335 -0
  17. package/src/analyzers/dead-code-analyzer.ts +565 -0
  18. package/src/analyzers/duplicate-code-analyzer.ts +626 -0
  19. package/src/analyzers/duplicate-dependency-analyzer.ts +381 -0
  20. package/src/analyzers/eslint-config-analyzer.ts +281 -0
  21. package/src/analyzers/exports-field-analyzer.ts +324 -0
  22. package/src/analyzers/index.ts +388 -0
  23. package/src/analyzers/large-dependency-analyzer.ts +535 -0
  24. package/src/analyzers/package-json-analyzer.ts +349 -0
  25. package/src/analyzers/peer-dependency-analyzer.ts +275 -0
  26. package/src/analyzers/tree-shaking-analyzer.ts +623 -0
  27. package/src/analyzers/tsconfig-analyzer.ts +382 -0
  28. package/src/analyzers/unused-dependency-analyzer.ts +356 -0
  29. package/src/analyzers/version-alignment-analyzer.ts +308 -0
  30. package/src/api/analyze-workspace.ts +245 -0
  31. package/src/api/index.ts +11 -0
  32. package/src/cache/cache-manager.ts +495 -0
  33. package/src/cache/cache-schema.ts +247 -0
  34. package/src/cache/change-detector.ts +169 -0
  35. package/src/cache/file-hasher.ts +65 -0
  36. package/src/cache/index.ts +47 -0
  37. package/src/cli/commands/analyze.ts +240 -0
  38. package/src/cli/commands/index.ts +5 -0
  39. package/src/cli/index.ts +61 -0
  40. package/src/cli/types.ts +65 -0
  41. package/src/cli/ui.ts +213 -0
  42. package/src/cli.ts +9 -0
  43. package/src/config/defaults.ts +183 -0
  44. package/src/config/index.ts +81 -0
  45. package/src/config/loader.ts +270 -0
  46. package/src/config/merger.ts +229 -0
  47. package/src/config/schema.ts +263 -0
  48. package/src/core/incremental-analyzer.ts +462 -0
  49. package/src/core/index.ts +34 -0
  50. package/src/core/orchestrator.ts +416 -0
  51. package/src/graph/dependency-graph.ts +408 -0
  52. package/src/graph/index.ts +19 -0
  53. package/src/index.ts +417 -0
  54. package/src/parser/config-parser.ts +491 -0
  55. package/src/parser/import-extractor.ts +340 -0
  56. package/src/parser/index.ts +54 -0
  57. package/src/parser/typescript-parser.ts +95 -0
  58. package/src/performance/bundle-estimator.ts +444 -0
  59. package/src/performance/index.ts +27 -0
  60. package/src/reporters/console-reporter.ts +355 -0
  61. package/src/reporters/index.ts +49 -0
  62. package/src/reporters/json-reporter.ts +273 -0
  63. package/src/reporters/markdown-reporter.ts +349 -0
  64. package/src/reporters/reporter.ts +399 -0
  65. package/src/rules/builtin-rules.ts +709 -0
  66. package/src/rules/index.ts +52 -0
  67. package/src/rules/rule-engine.ts +409 -0
  68. package/src/scanner/index.ts +18 -0
  69. package/src/scanner/workspace-scanner.ts +403 -0
  70. package/src/types/index.ts +176 -0
  71. package/src/types/result.ts +19 -0
  72. package/src/utils/index.ts +7 -0
  73. 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
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * CLI commands barrel export.
3
+ */
4
+
5
+ export {runAnalyze} from './analyze'
@@ -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()
@@ -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
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for workspace-analyzer.
4
+ *
5
+ * Re-exports from the structured CLI module for backwards compatibility.
6
+ * The actual CLI implementation is in src/cli/index.ts.
7
+ */
8
+
9
+ import './cli/index'