@pyreon/cli 0.15.0 → 0.18.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 +71 -32
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +1515 -149
- package/lib/types/index.d.ts +185 -10
- package/package.json +3 -2
- package/src/doctor/gates/audit-tests.ts +70 -0
- package/src/doctor/gates/audit-types.ts +146 -0
- package/src/doctor/gates/bundle-budgets.ts +187 -0
- package/src/doctor/gates/distribution.ts +206 -0
- package/src/doctor/gates/doc-claims.ts +240 -0
- package/src/doctor/gates/index.ts +46 -0
- package/src/doctor/gates/islands-audit.ts +66 -0
- package/src/doctor/gates/lint.ts +129 -0
- package/src/doctor/gates/pyreon-patterns.ts +70 -0
- package/src/doctor/gates/react-patterns.ts +113 -0
- package/src/doctor/gates/ssg-audit.ts +57 -0
- package/src/doctor/orchestrator.ts +176 -0
- package/src/doctor/render/ansi.ts +80 -0
- package/src/doctor/render/gha.ts +47 -0
- package/src/doctor/render/index.ts +8 -0
- package/src/doctor/render/json.ts +16 -0
- package/src/doctor/render/text.ts +206 -0
- package/src/doctor/report.ts +61 -0
- package/src/doctor/score.ts +134 -0
- package/src/doctor/types.ts +196 -0
- package/src/doctor/utils/walk.ts +58 -0
- package/src/doctor.ts +84 -251
- package/src/index.ts +83 -14
- package/src/tests/doctor.test.ts +105 -332
- package/src/tests/gate-adapters.test.ts +193 -0
- package/src/tests/gates.test.ts +674 -0
- package/src/tests/orchestrator.test.ts +72 -0
- package/src/tests/render.test.ts +213 -0
- package/src/tests/report.test.ts +99 -0
- package/src/tests/score.test.ts +158 -0
package/src/doctor.ts
CHANGED
|
@@ -1,281 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pyreon doctor — project-wide health
|
|
2
|
+
* `pyreon doctor` — project-wide health audit.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* PR 2 rewrites this entrypoint around the unified gate API
|
|
5
|
+
* (`packages/tools/cli/src/doctor/`). The orchestrator runs every
|
|
6
|
+
* gate in parallel, the aggregator builds a `DoctorReport` with a
|
|
7
|
+
* 0-100 score, the renderer formats it for text / JSON / GHA.
|
|
8
|
+
*
|
|
9
|
+
* The legacy single-purpose flags (`--audit-tests`, `--check-islands`,
|
|
10
|
+
* `--check-ssg`) are interpreted as `--only <gate>` shortcuts so any
|
|
11
|
+
* existing CI script that relied on the old shape keeps working.
|
|
12
|
+
* Without those flags, doctor runs the full fast-gate set + computes
|
|
13
|
+
* a score — the new default behaviour.
|
|
8
14
|
*
|
|
9
15
|
* Output modes:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
16
|
+
* - text (default): big-score banner + per-category bars + top-N findings
|
|
17
|
+
* - --json: full `DoctorReport` as JSON
|
|
18
|
+
* - --gha: GitHub Actions annotation lines (one per finding)
|
|
13
19
|
*
|
|
14
|
-
*
|
|
20
|
+
* Modes:
|
|
21
|
+
* - --full: enable slow gates (audit-types, bundle-budgets)
|
|
22
|
+
* - --only <gates>: run ONLY the listed comma-separated gates
|
|
23
|
+
* - --skip <gates>: exclude these gates
|
|
24
|
+
* - --fix: auto-fix where possible (lint + react-patterns)
|
|
25
|
+
* - --ci: exit non-zero on any error finding
|
|
15
26
|
*/
|
|
16
27
|
|
|
17
|
-
import * as fs from 'node:fs'
|
|
18
|
-
import * as path from 'node:path'
|
|
19
28
|
import {
|
|
20
|
-
|
|
21
|
-
type
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
runDoctor,
|
|
30
|
+
type GateName,
|
|
31
|
+
type OrchestratorOptions,
|
|
32
|
+
} from './doctor/orchestrator'
|
|
33
|
+
import { renderGha, renderJson, renderText } from './doctor/render'
|
|
34
|
+
import type { DoctorReport } from './doctor/types'
|
|
35
|
+
|
|
36
|
+
export type DoctorFormat = 'text' | 'json' | 'gha'
|
|
28
37
|
|
|
29
38
|
export interface DoctorOptions {
|
|
30
39
|
fix: boolean
|
|
40
|
+
/** Legacy boolean — interpreted as `format = 'json'` if true. */
|
|
31
41
|
json: boolean
|
|
32
42
|
ci: boolean
|
|
33
43
|
cwd: string
|
|
44
|
+
/** Explicit format override (wins over `json` boolean). */
|
|
45
|
+
format?: DoctorFormat | undefined
|
|
46
|
+
/** Enable slow gates (audit-types, bundle-budgets). */
|
|
47
|
+
full?: boolean | undefined
|
|
48
|
+
/** Run ONLY these gates. */
|
|
49
|
+
only?: GateName[] | undefined
|
|
50
|
+
/** Skip these gates. */
|
|
51
|
+
skip?: GateName[] | undefined
|
|
52
|
+
|
|
53
|
+
// ── Legacy flags (mapped to --only shortcuts for back-compat) ────
|
|
34
54
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* the React-migration check pipeline, so we gate it to avoid noise
|
|
39
|
-
* in the typical "is my migration done?" call.
|
|
55
|
+
* @deprecated Prefer `--only audit-tests`. Both forms behave
|
|
56
|
+
* identically: include the test-environment audit gate in the
|
|
57
|
+
* report. Kept so existing CI scripts continue to work.
|
|
40
58
|
*/
|
|
41
59
|
auditTests?: boolean | undefined
|
|
42
|
-
/** Minimum risk
|
|
43
|
-
auditMinRisk?:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
diagnostics: ReactDiagnostic[]
|
|
49
|
-
fixed: boolean
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface DoctorResult {
|
|
53
|
-
passed: boolean
|
|
54
|
-
files: FileResult[]
|
|
55
|
-
summary: {
|
|
56
|
-
filesScanned: number
|
|
57
|
-
filesWithIssues: number
|
|
58
|
-
totalErrors: number
|
|
59
|
-
totalFixable: number
|
|
60
|
-
totalFixed: number
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function doctor(options: DoctorOptions): Promise<number> {
|
|
65
|
-
const startTime = performance.now()
|
|
66
|
-
const files = collectSourceFiles(options.cwd)
|
|
67
|
-
const result = runChecks(files, options)
|
|
68
|
-
const elapsed = Math.round(performance.now() - startTime)
|
|
69
|
-
|
|
70
|
-
if (options.json) {
|
|
71
|
-
printJson(result)
|
|
72
|
-
} else {
|
|
73
|
-
printHuman(result, elapsed)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Test-environment audit — optional follow-on pass. We run AFTER the
|
|
77
|
-
// main React-migration output so a migration-focused run isn't
|
|
78
|
-
// contaminated; pass `--audit-tests` to see it. The exit code is
|
|
79
|
-
// unaffected since mock-vnode test risk is a "should review" signal,
|
|
80
|
-
// not a "broken build" signal.
|
|
81
|
-
if (options.auditTests) {
|
|
82
|
-
const auditResult = auditTestEnvironment(options.cwd)
|
|
83
|
-
if (options.json) {
|
|
84
|
-
console.log('')
|
|
85
|
-
console.log(JSON.stringify({ testAudit: auditResult }, null, 2))
|
|
86
|
-
} else {
|
|
87
|
-
console.log('')
|
|
88
|
-
console.log(formatTestAudit(auditResult, { minRisk: options.auditMinRisk ?? 'medium' }))
|
|
89
|
-
console.log('')
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return result.summary.totalErrors
|
|
60
|
+
/** Minimum risk for the test-environment audit. Default 'medium'. */
|
|
61
|
+
auditMinRisk?: 'high' | 'medium' | 'low' | undefined
|
|
62
|
+
/** @deprecated Prefer `--only islands-audit`. */
|
|
63
|
+
checkIslands?: boolean | undefined
|
|
64
|
+
/** @deprecated Prefer `--only ssg-audit`. */
|
|
65
|
+
checkSsg?: boolean | undefined
|
|
94
66
|
}
|
|
95
67
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const sourceExtensions = new Set(['.tsx', '.jsx', '.ts', '.js'])
|
|
101
|
-
const sourceIgnoreDirs = new Set([
|
|
102
|
-
'node_modules',
|
|
103
|
-
'dist',
|
|
104
|
-
'lib',
|
|
105
|
-
'.pyreon',
|
|
106
|
-
'.git',
|
|
107
|
-
'.next',
|
|
108
|
-
'build',
|
|
109
|
-
])
|
|
110
|
-
|
|
111
|
-
function shouldSkipDirEntry(entry: fs.Dirent): boolean {
|
|
112
|
-
if (!entry.isDirectory()) return false
|
|
113
|
-
return entry.name.startsWith('.') || sourceIgnoreDirs.has(entry.name)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function walkSourceFiles(dir: string, results: string[]): void {
|
|
117
|
-
let entries: fs.Dirent[]
|
|
118
|
-
try {
|
|
119
|
-
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
120
|
-
} catch {
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const entry of entries) {
|
|
125
|
-
if (shouldSkipDirEntry(entry)) continue
|
|
126
|
-
|
|
127
|
-
const fullPath = path.join(dir, entry.name)
|
|
128
|
-
if (entry.isDirectory()) {
|
|
129
|
-
walkSourceFiles(fullPath, results)
|
|
130
|
-
} else if (entry.isFile() && sourceExtensions.has(path.extname(entry.name))) {
|
|
131
|
-
results.push(fullPath)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
68
|
+
const resolveFormat = (options: DoctorOptions): DoctorFormat => {
|
|
69
|
+
if (options.format) return options.format
|
|
70
|
+
if (options.json) return 'json'
|
|
71
|
+
return 'text'
|
|
134
72
|
}
|
|
135
73
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
74
|
+
const resolveOnly = (options: DoctorOptions): GateName[] | undefined => {
|
|
75
|
+
if (options.only && options.only.length > 0) return options.only
|
|
76
|
+
// Legacy single-purpose flags → `--only` shortcuts.
|
|
77
|
+
const legacyOnly: GateName[] = []
|
|
78
|
+
if (options.auditTests) legacyOnly.push('audit-tests')
|
|
79
|
+
if (options.checkIslands) legacyOnly.push('islands-audit')
|
|
80
|
+
if (options.checkSsg) legacyOnly.push('ssg-audit')
|
|
81
|
+
return legacyOnly.length > 0 ? legacyOnly : undefined
|
|
140
82
|
}
|
|
141
83
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
let code: string
|
|
151
|
-
try {
|
|
152
|
-
code = fs.readFileSync(file, 'utf-8')
|
|
153
|
-
} catch {
|
|
154
|
-
return { result: null, fixCount: 0 }
|
|
84
|
+
export const doctor = async (options: DoctorOptions): Promise<number> => {
|
|
85
|
+
const orchestratorOpts: OrchestratorOptions = {
|
|
86
|
+
cwd: options.cwd,
|
|
87
|
+
full: options.full,
|
|
88
|
+
only: resolveOnly(options),
|
|
89
|
+
skip: options.skip,
|
|
90
|
+
fix: options.fix,
|
|
91
|
+
auditMinRisk: options.auditMinRisk,
|
|
155
92
|
}
|
|
156
93
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const migrated = migrateReactCode(code, relPath)
|
|
160
|
-
if (migrated.changes.length > 0) {
|
|
161
|
-
fs.writeFileSync(file, migrated.code, 'utf-8')
|
|
162
|
-
}
|
|
163
|
-
const remaining = detectReactPatterns(migrated.code, relPath)
|
|
164
|
-
if (remaining.length > 0 || migrated.changes.length > 0) {
|
|
165
|
-
return {
|
|
166
|
-
result: { file: relPath, diagnostics: remaining, fixed: migrated.changes.length > 0 },
|
|
167
|
-
fixCount: migrated.changes.length,
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return { result: null, fixCount: 0 }
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function checkFileDetectOnly(file: string, relPath: string): FileResult | null {
|
|
174
|
-
let code: string
|
|
175
|
-
try {
|
|
176
|
-
code = fs.readFileSync(file, 'utf-8')
|
|
177
|
-
} catch {
|
|
178
|
-
return null
|
|
179
|
-
}
|
|
94
|
+
const report = await runDoctor(orchestratorOpts)
|
|
95
|
+
const format = resolveFormat(options)
|
|
180
96
|
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return null
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function runChecks(files: string[], options: DoctorOptions): DoctorResult {
|
|
191
|
-
const fileResults: FileResult[] = []
|
|
192
|
-
let totalFixed = 0
|
|
193
|
-
|
|
194
|
-
for (const file of files) {
|
|
195
|
-
const relPath = path.relative(options.cwd, file)
|
|
196
|
-
|
|
197
|
-
if (options.fix) {
|
|
198
|
-
const { result, fixCount } = checkFileWithFix(file, relPath)
|
|
199
|
-
totalFixed += fixCount
|
|
200
|
-
if (result) fileResults.push(result)
|
|
201
|
-
} else {
|
|
202
|
-
const result = checkFileDetectOnly(file, relPath)
|
|
203
|
-
if (result) fileResults.push(result)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const totalErrors = fileResults.reduce((sum, f) => sum + f.diagnostics.length, 0)
|
|
208
|
-
const totalFixable = fileResults.reduce(
|
|
209
|
-
(sum, f) => sum + f.diagnostics.filter((d) => d.fixable).length,
|
|
210
|
-
0,
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
passed: totalErrors === 0,
|
|
215
|
-
files: fileResults,
|
|
216
|
-
summary: {
|
|
217
|
-
filesScanned: files.length,
|
|
218
|
-
filesWithIssues: fileResults.length,
|
|
219
|
-
totalErrors,
|
|
220
|
-
totalFixable,
|
|
221
|
-
totalFixed,
|
|
222
|
-
},
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
227
|
-
// Output formatters
|
|
228
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
229
|
-
|
|
230
|
-
function printJson(result: DoctorResult): void {
|
|
231
|
-
console.log(JSON.stringify(result, null, 2))
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function printFileResult(fileResult: FileResult): void {
|
|
235
|
-
if (fileResult.diagnostics.length === 0) return
|
|
236
|
-
|
|
237
|
-
console.log(` ${fileResult.file}${fileResult.fixed ? ' (partially fixed)' : ''}`)
|
|
238
|
-
|
|
239
|
-
for (const diag of fileResult.diagnostics) {
|
|
240
|
-
const fixTag = diag.fixable ? ' [fixable]' : ''
|
|
241
|
-
console.log(` ${diag.line}:${diag.column} — ${diag.message}${fixTag}`)
|
|
242
|
-
console.log(` Current: ${diag.current}`)
|
|
243
|
-
console.log(` Suggested: ${diag.suggested}`)
|
|
244
|
-
console.log('')
|
|
97
|
+
if (format === 'json') {
|
|
98
|
+
console.log(renderJson(report))
|
|
99
|
+
} else if (format === 'gha') {
|
|
100
|
+
console.log(renderGha(report))
|
|
101
|
+
} else {
|
|
102
|
+
console.log(renderText(report, { cwd: options.cwd }))
|
|
245
103
|
}
|
|
246
|
-
}
|
|
247
104
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
console.log(` ${summary.totalFixable} auto-fixable — run 'pyreon doctor --fix' to apply`)
|
|
254
|
-
}
|
|
255
|
-
console.log('')
|
|
105
|
+
// Exit code: in --ci mode, any error finding fails. Otherwise, only
|
|
106
|
+
// a non-zero is returned when there are findings AT ALL — so
|
|
107
|
+
// `pyreon doctor && echo green` works as a quick gate.
|
|
108
|
+
if (options.ci) return report.totals.errors
|
|
109
|
+
return report.totals.errors + report.totals.warnings + report.totals.infos
|
|
256
110
|
}
|
|
257
111
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
console.log('')
|
|
262
|
-
console.log(` Pyreon Doctor — scanned ${summary.filesScanned} files in ${elapsed}ms`)
|
|
263
|
-
console.log('')
|
|
264
|
-
|
|
265
|
-
if (result.passed && summary.totalFixed === 0) {
|
|
266
|
-
console.log(' ✓ No issues found. Your code is Pyreon-native!')
|
|
267
|
-
console.log('')
|
|
268
|
-
return
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (summary.totalFixed > 0) {
|
|
272
|
-
console.log(` ✓ Auto-fixed ${summary.totalFixed} issue${summary.totalFixed === 1 ? '' : 's'}`)
|
|
273
|
-
console.log('')
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
for (const fileResult of result.files) {
|
|
277
|
-
printFileResult(fileResult)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
printSummary(summary)
|
|
281
|
-
}
|
|
112
|
+
// Re-export the report types for external consumers (CI integrations,
|
|
113
|
+
// AI agents, dashboards).
|
|
114
|
+
export type { DoctorReport, GateName }
|
package/src/index.ts
CHANGED
|
@@ -4,12 +4,26 @@
|
|
|
4
4
|
* @pyreon/cli — Developer tools for Pyreon
|
|
5
5
|
*
|
|
6
6
|
* Commands:
|
|
7
|
-
* pyreon doctor
|
|
8
|
-
* pyreon context
|
|
7
|
+
* pyreon doctor — project-wide health audit (score + per-category bars + findings)
|
|
8
|
+
* pyreon context — generate .pyreon/context.json for AI tools
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { generateContext } from './context'
|
|
12
12
|
import { type DoctorOptions, doctor } from './doctor'
|
|
13
|
+
import type { GateName } from './doctor/orchestrator'
|
|
14
|
+
|
|
15
|
+
const VALID_GATES: GateName[] = [
|
|
16
|
+
'react-patterns',
|
|
17
|
+
'pyreon-patterns',
|
|
18
|
+
'lint',
|
|
19
|
+
'distribution',
|
|
20
|
+
'doc-claims',
|
|
21
|
+
'audit-tests',
|
|
22
|
+
'islands-audit',
|
|
23
|
+
'ssg-audit',
|
|
24
|
+
'audit-types',
|
|
25
|
+
'bundle-budgets',
|
|
26
|
+
]
|
|
13
27
|
|
|
14
28
|
const args = process.argv.slice(2)
|
|
15
29
|
const command = args[0]
|
|
@@ -19,18 +33,69 @@ function printUsage(): void {
|
|
|
19
33
|
pyreon <command> [options]
|
|
20
34
|
|
|
21
35
|
Commands:
|
|
22
|
-
doctor [
|
|
23
|
-
|
|
24
|
-
--audit-tests appends mock-vnode test-audit (PR #197 class).
|
|
25
|
-
--audit-min-risk is high|medium|low (default medium).
|
|
36
|
+
doctor [options] Project-wide health audit with 0-100 score.
|
|
37
|
+
Runs 8 fast gates by default; --full enables 2 slow gates.
|
|
26
38
|
context [--out <path>] Generate .pyreon/context.json for AI tools
|
|
27
39
|
|
|
40
|
+
doctor options:
|
|
41
|
+
--fix Auto-fix what we can (lint + react-patterns).
|
|
42
|
+
--full Include slow gates (audit-types, bundle-budgets).
|
|
43
|
+
--only <gates> Run ONLY these gates (comma-separated).
|
|
44
|
+
--skip <gates> Skip these gates (comma-separated).
|
|
45
|
+
--format text|json|gha Output format (default: text).
|
|
46
|
+
--json Shortcut for --format=json.
|
|
47
|
+
--gha Shortcut for --format=gha (GitHub Actions annotations).
|
|
48
|
+
--ci Exit non-zero on error findings only.
|
|
49
|
+
--audit-min-risk high|medium|low Minimum risk for test-env audit (default: medium).
|
|
50
|
+
|
|
51
|
+
doctor gates:
|
|
52
|
+
Fast: ${VALID_GATES.slice(0, 8).join(', ')}
|
|
53
|
+
Slow: ${VALID_GATES.slice(8).join(', ')} (require --full)
|
|
54
|
+
|
|
55
|
+
Legacy doctor flags (still work — map to --only shortcuts):
|
|
56
|
+
--audit-tests Equivalent to --only audit-tests
|
|
57
|
+
--check-islands Equivalent to --only islands-audit
|
|
58
|
+
--check-ssg Equivalent to --only ssg-audit
|
|
59
|
+
|
|
28
60
|
Options:
|
|
29
61
|
--help Show this help message
|
|
30
62
|
--version Show version
|
|
31
63
|
`)
|
|
32
64
|
}
|
|
33
65
|
|
|
66
|
+
const parseGateList = (raw: string | undefined): GateName[] | undefined => {
|
|
67
|
+
if (!raw) return undefined
|
|
68
|
+
const names = raw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
69
|
+
const invalid = names.filter((n) => !VALID_GATES.includes(n as GateName))
|
|
70
|
+
if (invalid.length > 0) {
|
|
71
|
+
console.error(
|
|
72
|
+
`Unknown gate(s): ${invalid.join(', ')}. Valid: ${VALID_GATES.join(', ')}`,
|
|
73
|
+
)
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
return names as GateName[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const getFlagValue = (flag: string): string | undefined => {
|
|
80
|
+
const idx = args.indexOf(flag)
|
|
81
|
+
if (idx < 0) return undefined
|
|
82
|
+
return args[idx + 1]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const parseFormat = (raw: string | undefined): DoctorOptions['format'] => {
|
|
86
|
+
if (!raw) return undefined
|
|
87
|
+
if (raw === 'text' || raw === 'json' || raw === 'gha') return raw
|
|
88
|
+
console.error(`--format must be text|json|gha, got '${raw}'`)
|
|
89
|
+
process.exit(1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parseMinRisk = (raw: string | undefined): DoctorOptions['auditMinRisk'] => {
|
|
93
|
+
if (!raw) return undefined
|
|
94
|
+
if (raw === 'high' || raw === 'medium' || raw === 'low') return raw
|
|
95
|
+
console.error(`--audit-min-risk must be high|medium|low, got '${raw}'`)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
|
|
34
99
|
async function main(): Promise<void> {
|
|
35
100
|
if (!command || command === '--help' || command === '-h') {
|
|
36
101
|
printUsage()
|
|
@@ -43,19 +108,23 @@ async function main(): Promise<void> {
|
|
|
43
108
|
}
|
|
44
109
|
|
|
45
110
|
if (command === 'doctor') {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
process.exit(1)
|
|
51
|
-
}
|
|
111
|
+
const format = args.includes('--gha')
|
|
112
|
+
? ('gha' as const)
|
|
113
|
+
: parseFormat(getFlagValue('--format'))
|
|
114
|
+
|
|
52
115
|
const options: DoctorOptions = {
|
|
53
116
|
fix: args.includes('--fix'),
|
|
54
117
|
json: args.includes('--json'),
|
|
55
118
|
ci: args.includes('--ci'),
|
|
56
119
|
cwd: process.cwd(),
|
|
120
|
+
format,
|
|
121
|
+
full: args.includes('--full'),
|
|
122
|
+
only: parseGateList(getFlagValue('--only')),
|
|
123
|
+
skip: parseGateList(getFlagValue('--skip')),
|
|
57
124
|
auditTests: args.includes('--audit-tests'),
|
|
58
|
-
auditMinRisk:
|
|
125
|
+
auditMinRisk: parseMinRisk(getFlagValue('--audit-min-risk')),
|
|
126
|
+
checkIslands: args.includes('--check-islands'),
|
|
127
|
+
checkSsg: args.includes('--check-ssg'),
|
|
59
128
|
}
|
|
60
129
|
const exitCode = await doctor(options)
|
|
61
130
|
if (options.ci && exitCode > 0) {
|
|
@@ -82,5 +151,5 @@ main().catch((err) => {
|
|
|
82
151
|
})
|
|
83
152
|
|
|
84
153
|
export type { ContextOptions, ProjectContext } from './context'
|
|
85
|
-
export type { DoctorOptions } from './doctor'
|
|
154
|
+
export type { DoctorOptions, DoctorReport, GateName } from './doctor'
|
|
86
155
|
export { doctor, generateContext }
|