cc-env-checker 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 +413 -0
- package/package.json +35 -0
- package/src/checks/artifacts.js +318 -0
- package/src/checks/config.js +174 -0
- package/src/checks/fingerprint.js +192 -0
- package/src/checks/helpers.js +326 -0
- package/src/checks/install.js +72 -0
- package/src/checks/network.js +443 -0
- package/src/checks/runtime.js +106 -0
- package/src/checks/static-install.js +307 -0
- package/src/cli-app.js +108 -0
- package/src/cli.js +50 -0
- package/src/doctor.js +67 -0
- package/src/i18n.js +405 -0
- package/src/modules.js +25 -0
- package/src/render.js +189 -0
- package/src/report.js +165 -0
- package/src/types.js +18 -0
package/src/i18n.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
const dictionaries = {
|
|
2
|
+
'zh-CN': {
|
|
3
|
+
'ui.overallRisk': '整体环境风险',
|
|
4
|
+
'ui.generatedAt': '生成时间',
|
|
5
|
+
'ui.modulesChecked': '已检查模块: {moduleCount} | 检查项: {checkCount} | 高风险: {highRiskCount} | 中风险: {mediumRiskCount}',
|
|
6
|
+
'ui.topFindings': '重点发现',
|
|
7
|
+
'ui.noFindings': '暂无明显问题。',
|
|
8
|
+
'ui.coverageMatrix': '覆盖矩阵',
|
|
9
|
+
'ui.evidenceSummary': '依据摘要',
|
|
10
|
+
'ui.evidenceSources': '依据来源',
|
|
11
|
+
'ui.evidenceConfidence': '依据置信度',
|
|
12
|
+
'coverage.persistent-identifiers.title': '持久化标识',
|
|
13
|
+
'coverage.message-request-fingerprinting.title': '消息与请求指纹',
|
|
14
|
+
'coverage.environment-fingerprinting.title': '环境指纹采集',
|
|
15
|
+
'coverage.telemetry-event-logging.title': '遥测与事件日志',
|
|
16
|
+
'coverage.third-party-analytics-channels.title': '第三方分析通道',
|
|
17
|
+
'coverage.remote-policy-settings-control.title': '远程策略与设置控制',
|
|
18
|
+
'coverage.identity-linking-risk.title': '身份关联风险',
|
|
19
|
+
'coverage.rate-limit-abuse-signals.title': '速率限制滥用信号',
|
|
20
|
+
'coverage.automation-abuse-signals.title': '自动化滥用信号',
|
|
21
|
+
'coverage.client-tampering-signals.title': '客户端篡改信号',
|
|
22
|
+
'coverage.state.covered': '已覆盖',
|
|
23
|
+
'coverage.state.partial': '部分覆盖',
|
|
24
|
+
'coverage.state.unverifiable': '无法本地验证',
|
|
25
|
+
'coverage.state.not_covered': '未覆盖',
|
|
26
|
+
'ui.recommendedActions': '建议动作',
|
|
27
|
+
'ui.progress.runningChecks': '正在检查 {moduleId} 模块...',
|
|
28
|
+
'cli.description.doctor': '执行完整环境风险检测',
|
|
29
|
+
'cli.description.network': '仅执行网络相关检测',
|
|
30
|
+
'cli.description.runtime': '仅执行运行时相关检测',
|
|
31
|
+
'cli.description.install': '仅执行安装相关检测',
|
|
32
|
+
'cli.description.config': '仅执行配置相关检测',
|
|
33
|
+
'cli.description.report': '输出完整报告',
|
|
34
|
+
'cli.option.verbose': '显示详细依据和原始观测值',
|
|
35
|
+
'cli.option.json': '输出机器可读的 JSON',
|
|
36
|
+
'cli.option.lang': '指定输出语言,例如 zh-CN 或 en',
|
|
37
|
+
'cli.option.noRemote': '关闭真实联网探测',
|
|
38
|
+
'cli.option.timeout': '设置联网探测超时时间(毫秒)',
|
|
39
|
+
'ui.detail.risk': '风险',
|
|
40
|
+
'ui.detail.evidence': '依据',
|
|
41
|
+
'ui.detail.source': '来源',
|
|
42
|
+
'ui.detail.suggestion': '建议',
|
|
43
|
+
'ui.priority.fix_now': '立即处理',
|
|
44
|
+
'ui.priority.review_soon': '尽快复查',
|
|
45
|
+
'ui.priority.optional': '可选',
|
|
46
|
+
'risk.low': '低',
|
|
47
|
+
'risk.medium': '中',
|
|
48
|
+
'risk.high': '高',
|
|
49
|
+
'risk.unknown': '未知',
|
|
50
|
+
'module.network': '网络',
|
|
51
|
+
'module.runtime': '运行时',
|
|
52
|
+
'module.install': '安装',
|
|
53
|
+
'module.config': '配置',
|
|
54
|
+
'module.artifacts': '工件',
|
|
55
|
+
'module.fingerprint': '指纹',
|
|
56
|
+
'module.staticInstall': '静态安装',
|
|
57
|
+
'check.artifacts.identity.title': 'Claude 身份摘要',
|
|
58
|
+
'check.artifacts.identity.summary.pass': 'Claude 身份信息已记录,并以掩码形式展示',
|
|
59
|
+
'check.artifacts.identity.summary.warn': '未能读取 Claude 身份配置',
|
|
60
|
+
'check.artifacts.identity.suggestion.warn': '确认 .claude.json 可读,并避免在输出中暴露原始账号信息。',
|
|
61
|
+
'check.artifacts.telemetry.title': '遥测缓存',
|
|
62
|
+
'check.artifacts.telemetry.summary.pass': '遥测缓存目录可用',
|
|
63
|
+
'check.artifacts.telemetry.summary.warn': '遥测缓存目录不可用',
|
|
64
|
+
'check.artifacts.telemetry.suggestion.review': '检查缓存目录中的历史记录是否符合预期。',
|
|
65
|
+
'check.artifacts.projects.title': '项目历史',
|
|
66
|
+
'check.artifacts.projects.summary.pass': '项目历史目录可用',
|
|
67
|
+
'check.artifacts.projects.summary.skip': '未找到项目历史目录',
|
|
68
|
+
'check.artifacts.projects.summary.warn': '项目历史目录存在访问异常',
|
|
69
|
+
'check.artifacts.projects.suggestion.review': '确认是否需要保留项目历史数据。',
|
|
70
|
+
'check.artifacts.credentials.title': '凭据文件',
|
|
71
|
+
'check.artifacts.credentials.summary.warn': '检测到本地凭据文件',
|
|
72
|
+
'check.artifacts.credentials.summary.pass': '未发现本地凭据文件',
|
|
73
|
+
'check.artifacts.credentials.suggestion.warn': '检查凭据文件是否应继续保留在本机。',
|
|
74
|
+
'check.fingerprint.environment.title': '本地可观测信号',
|
|
75
|
+
'check.fingerprint.environment.summary.pass': '未发现明显的自动化或远程会话信号',
|
|
76
|
+
'check.fingerprint.environment.summary.warn': '检测到自动化或远程会话信号',
|
|
77
|
+
'check.fingerprint.environment.suggestion.review': '确认这些本地信号是否符合当前会话预期。',
|
|
78
|
+
'check.fingerprint.runtime.title': '运行时清单',
|
|
79
|
+
'check.fingerprint.runtime.summary.pass': '已收集本地运行时清单',
|
|
80
|
+
'check.fingerprint.runtime.suggestion.review': '确认这些运行时可用性是否符合当前环境。',
|
|
81
|
+
'check.runtime.node.title': 'Node.js 运行时',
|
|
82
|
+
'check.runtime.node.summary.fail': '需要 Node.js 20 或更高版本',
|
|
83
|
+
'check.runtime.node.summary.pass': 'Node.js 版本兼容',
|
|
84
|
+
'check.runtime.node.suggestion.fail': '请升级到 Node.js 20 或更高版本。',
|
|
85
|
+
'check.common.noAction': '无需处理。',
|
|
86
|
+
'check.runtime.npm.title': 'npm 可用性',
|
|
87
|
+
'check.runtime.npm.summary.fail': '当前 shell 中无法使用 npm',
|
|
88
|
+
'check.runtime.npm.summary.pass': 'npm 可用',
|
|
89
|
+
'check.runtime.npm.suggestion.fail': '请安装 npm,或确认它已在 PATH 中。',
|
|
90
|
+
'check.runtime.path.title': 'PATH 一致性',
|
|
91
|
+
'check.runtime.path.summary.warn': 'PATH 中存在重复条目',
|
|
92
|
+
'check.runtime.path.summary.pass': 'PATH 未发现明显重复',
|
|
93
|
+
'check.runtime.path.suggestion.warn': '清理重复 PATH 条目,减少命令解析歧义。',
|
|
94
|
+
'check.install.binary.title': 'Claude Code 二进制',
|
|
95
|
+
'check.install.binary.summary.fail': '当前 shell 中找不到 Claude Code CLI',
|
|
96
|
+
'check.install.binary.summary.pass': 'Claude Code CLI 可用',
|
|
97
|
+
'check.install.binary.suggestion.fail': '请安装 Claude Code CLI,并确保 `claude` 命令在 PATH 中。',
|
|
98
|
+
'check.install.help.title': 'Claude Code 基本执行',
|
|
99
|
+
'check.install.help.summary.warn': 'Claude Code CLI 存在,但 `--help` 执行失败',
|
|
100
|
+
'check.install.help.summary.pass': 'Claude Code CLI 可响应基础命令',
|
|
101
|
+
'check.install.help.suggestion.warn': '请重新安装 Claude Code CLI,或排查当前安装是否损坏。',
|
|
102
|
+
'check.staticInstall.summary.skip': '未找到 Claude Code 可执行文件,无法进行静态安装分析',
|
|
103
|
+
'check.staticInstall.summary.warn': '无法读取本地安装文本,静态安装分析不完整',
|
|
104
|
+
'check.staticInstall.suggestion.skip': '确认 `claude` 命令可用后,再运行静态安装分析。',
|
|
105
|
+
'check.staticInstall.suggestion.warn': '检查 Claude Code 安装文件是否可读,再重新运行分析。',
|
|
106
|
+
'check.staticInstall.endpoint.title': '可观测端点摘要',
|
|
107
|
+
'check.staticInstall.endpoint.summary.pass': '已从本地安装文本中归类出可观测端点',
|
|
108
|
+
'check.staticInstall.endpoint.summary.warn': '未从本地安装文本中发现已知端点类别',
|
|
109
|
+
'check.staticInstall.endpoint.suggestion.review': '复查这些端点类别是否符合你对当前安装行为的预期。',
|
|
110
|
+
'check.staticInstall.header.title': '可观测请求头摘要',
|
|
111
|
+
'check.staticInstall.header.summary.pass': '已从本地安装文本中归类出可观测请求头',
|
|
112
|
+
'check.staticInstall.header.summary.warn': '未从本地安装文本中发现已知请求头类别',
|
|
113
|
+
'check.staticInstall.header.suggestion.review': '复查这些请求头类别是否符合你对当前安装行为的预期。',
|
|
114
|
+
'check.staticInstall.event.title': '可观测事件摘要',
|
|
115
|
+
'check.staticInstall.event.summary.pass': '已从本地安装文本中归类出可观测事件名称',
|
|
116
|
+
'check.staticInstall.event.summary.warn': '未从本地安装文本中发现已知事件类别',
|
|
117
|
+
'check.staticInstall.event.suggestion.review': '复查这些事件类别是否符合你对当前安装行为的预期。',
|
|
118
|
+
'check.install.npx.title': 'NPX 兜底执行',
|
|
119
|
+
'check.install.npx.summary.warn': '当前无法通过 NPX 调起 Claude Code CLI',
|
|
120
|
+
'check.install.npx.summary.pass': 'NPX 可以解析 Claude Code CLI',
|
|
121
|
+
'check.install.npx.suggestion.warn': '如果你依赖 npx 方式运行,请确认 npm 安装和包解析状态正常。',
|
|
122
|
+
'check.config.json.title': 'Claude 配置文件',
|
|
123
|
+
'check.config.json.summary.pass': 'Claude 配置文件存在且 JSON 合法',
|
|
124
|
+
'check.config.json.summary.missing': '未找到 Claude 配置文件',
|
|
125
|
+
'check.config.json.summary.fail': 'Claude 配置文件存在但无法解析',
|
|
126
|
+
'check.config.json.suggestion.missing': '先运行一次 Claude Code 生成配置,或确认当前用户目录是否正确。',
|
|
127
|
+
'check.config.json.suggestion.fail': '请修复或重建配置文件,再继续使用当前环境。',
|
|
128
|
+
'check.config.dir.title': 'Claude 数据目录',
|
|
129
|
+
'check.config.dir.summary.invalid': 'Claude 数据路径存在,但不是目录',
|
|
130
|
+
'check.config.dir.summary.pass': 'Claude 数据目录存在',
|
|
131
|
+
'check.config.dir.summary.missing': '未找到 Claude 数据目录',
|
|
132
|
+
'check.config.dir.summary.fail': '无法检查 Claude 数据目录',
|
|
133
|
+
'check.config.dir.suggestion.invalid': '移除无效路径,让 Claude Code 重新初始化数据目录。',
|
|
134
|
+
'check.config.dir.suggestion.missing': '至少运行一次 Claude Code 以初始化用户数据。',
|
|
135
|
+
'check.config.dir.suggestion.fail': '请检查权限和用户目录状态。',
|
|
136
|
+
'check.config.noProxy.title': 'NO_PROXY 配置',
|
|
137
|
+
'check.config.noProxy.summary.unset': 'NO_PROXY 未设置',
|
|
138
|
+
'check.config.noProxy.summary.empty': 'NO_PROXY 已设置,但解析后为空',
|
|
139
|
+
'check.config.noProxy.summary.pass': 'NO_PROXY 包含明确的绕过规则',
|
|
140
|
+
'check.config.noProxy.suggestion.unset': '如果你的环境不需要代理绕过规则,则无需处理。',
|
|
141
|
+
'check.config.noProxy.suggestion.empty': '清理 NO_PROXY,避免代理绕过行为含糊不清。',
|
|
142
|
+
'check.config.noProxy.suggestion.pass': '确认当前绕过列表仍符合你的网络设计。',
|
|
143
|
+
'check.network.remoteDisabled': '已关闭联网探测',
|
|
144
|
+
'check.network.connectivity.title': '网络连通性',
|
|
145
|
+
'check.network.connectivity.summary.fail': '无法完成外网连通性探测',
|
|
146
|
+
'check.network.connectivity.summary.pass': '外网连通性探测成功',
|
|
147
|
+
'check.network.connectivity.suggestion.disabled': '去掉 `--no-remote` 可启用真实联网检测。',
|
|
148
|
+
'check.network.connectivity.suggestion.fail': '请检查外网连接、代理设置和 DNS 解析。',
|
|
149
|
+
'check.network.egress.title': '出口画像',
|
|
150
|
+
'check.network.egress.summary.fail': '由于 IP 探测失败,无法确定出口画像',
|
|
151
|
+
'check.network.egress.summary.partial': '已检测到公网 IP,但无法获取地区或 ASN 信息',
|
|
152
|
+
'check.network.egress.summary.pass': '已成功获取公网 IP、地区和 ASN 提示',
|
|
153
|
+
'check.network.egress.suggestion.disabled': '去掉 `--no-remote` 可启用地区和 ASN 提示。',
|
|
154
|
+
'check.network.egress.suggestion.fail': '先恢复外网连通性,再参考地区和 ASN 信息。',
|
|
155
|
+
'check.network.egress.suggestion.partial': '重试出口画像检查,确认地区和 ASN 元数据。',
|
|
156
|
+
'check.network.egress.suggestion.pass': '检查暴露的国家和 ASN 是否符合预期。',
|
|
157
|
+
'check.network.externalIpSignal.title': '外部 IP 风险信号',
|
|
158
|
+
'check.network.externalIpSignal.summary.unavailable': '未拿到足够的外部 IP 风险元数据',
|
|
159
|
+
'check.network.externalIpSignal.summary.low': '外部风险元数据显示当前出口暂无明显高风险信号',
|
|
160
|
+
'check.network.externalIpSignal.summary.medium': '外部风险元数据显示当前出口存在可疑风险信号',
|
|
161
|
+
'check.network.externalIpSignal.summary.high': '外部风险元数据显示当前出口存在强风险信号',
|
|
162
|
+
'check.network.externalIpSignal.suggestion.disabled': '去掉 `--no-remote` 可启用外部 IP 风险信号探测。',
|
|
163
|
+
'check.network.externalIpSignal.suggestion.unavailable': '重试外部 IP 风险探测,确认是否能拿到稳定元数据。',
|
|
164
|
+
'check.network.externalIpSignal.suggestion.low': '继续关注出口类型和组织信息是否符合预期。',
|
|
165
|
+
'check.network.externalIpSignal.suggestion.review': '复查当前出口 IP 类型、代理/VPN/机房标签,以及 ASN/组织信息。',
|
|
166
|
+
'check.network.dnsBaseline.title': 'DNS 基线',
|
|
167
|
+
'check.network.dnsBaseline.summary.fail': '一个或多个目标域名无法解析',
|
|
168
|
+
'check.network.dnsBaseline.summary.pass': '目标域名解析正常',
|
|
169
|
+
'check.network.dnsBaseline.suggestion.fail': '请检查 DNS 服务器、代理 DNS 行为和 IPv4/IPv6 可用性。',
|
|
170
|
+
'check.network.dnsFamily.title': '地址族一致性',
|
|
171
|
+
'check.network.dnsFamily.summary.skip': '由于 DNS 基线解析失败,无法判断 IPv4/IPv6 一致性',
|
|
172
|
+
'check.network.dnsFamily.summary.warn': '目标域名可解析,但 IPv4/IPv6 地址族不一致',
|
|
173
|
+
'check.network.dnsFamily.summary.pass': '目标域名在 IPv4 和 IPv6 上都可解析',
|
|
174
|
+
'check.network.dnsFamily.suggestion.skip': '先恢复 DNS 解析,再判断 IPv4 或 IPv6 一致性。',
|
|
175
|
+
'check.network.dnsFamily.suggestion.warn': '请检查 IPv4 和 IPv6 路由,避免可达性不一致。',
|
|
176
|
+
'check.network.proxy.title': '代理环境',
|
|
177
|
+
'check.network.proxy.summary.unset': '未设置代理环境变量',
|
|
178
|
+
'check.network.proxy.summary.warn': 'HTTP 和 HTTPS 代理变量不一致',
|
|
179
|
+
'check.network.proxy.summary.pass': '代理环境变量存在且一致',
|
|
180
|
+
'check.network.proxy.suggestion.unset': '如果预期直连网络,则无需处理。',
|
|
181
|
+
'check.network.proxy.suggestion.warn': '请同时设置或同时清理 HTTP_PROXY 和 HTTPS_PROXY。',
|
|
182
|
+
'check.network.proxy.suggestion.pass': '确认当前代理地址是你预期使用的那一组。',
|
|
183
|
+
},
|
|
184
|
+
en: {
|
|
185
|
+
'ui.overallRisk': 'Overall environment risk',
|
|
186
|
+
'ui.generatedAt': 'Generated at',
|
|
187
|
+
'ui.modulesChecked': 'Modules checked: {moduleCount} | Checks: {checkCount} | High: {highRiskCount} | Medium: {mediumRiskCount}',
|
|
188
|
+
'ui.topFindings': 'Top findings',
|
|
189
|
+
'ui.noFindings': 'No significant findings.',
|
|
190
|
+
'ui.coverageMatrix': 'Coverage matrix',
|
|
191
|
+
'ui.evidenceSummary': 'Evidence summary',
|
|
192
|
+
'ui.evidenceSources': 'sources',
|
|
193
|
+
'ui.evidenceConfidence': 'confidence',
|
|
194
|
+
'coverage.persistent-identifiers.title': 'Persistent identifiers',
|
|
195
|
+
'coverage.message-request-fingerprinting.title': 'Message and request fingerprinting',
|
|
196
|
+
'coverage.environment-fingerprinting.title': 'Environment fingerprint collection',
|
|
197
|
+
'coverage.telemetry-event-logging.title': 'Telemetry and event logging',
|
|
198
|
+
'coverage.third-party-analytics-channels.title': 'Third-party analytics channels',
|
|
199
|
+
'coverage.remote-policy-settings-control.title': 'Remote policy and settings control',
|
|
200
|
+
'coverage.identity-linking-risk.title': 'Identity-linking risk',
|
|
201
|
+
'coverage.rate-limit-abuse-signals.title': 'Rate-limit abuse signals',
|
|
202
|
+
'coverage.automation-abuse-signals.title': 'Automation-abuse signals',
|
|
203
|
+
'coverage.client-tampering-signals.title': 'Client-tampering signals',
|
|
204
|
+
'coverage.state.covered': 'covered',
|
|
205
|
+
'coverage.state.partial': 'partial',
|
|
206
|
+
'coverage.state.unverifiable': 'unverifiable',
|
|
207
|
+
'coverage.state.not_covered': 'not covered',
|
|
208
|
+
'ui.recommendedActions': 'Recommended actions',
|
|
209
|
+
'ui.progress.runningChecks': 'Running {moduleId} checks...',
|
|
210
|
+
'cli.description.doctor': 'run the full environment risk check',
|
|
211
|
+
'cli.description.network': 'run only network checks',
|
|
212
|
+
'cli.description.runtime': 'run only runtime checks',
|
|
213
|
+
'cli.description.install': 'run only installation checks',
|
|
214
|
+
'cli.description.config': 'run only configuration checks',
|
|
215
|
+
'cli.description.report': 'print the full report',
|
|
216
|
+
'cli.option.verbose': 'show detailed evidence and raw observations',
|
|
217
|
+
'cli.option.json': 'print machine-readable JSON output',
|
|
218
|
+
'cli.option.lang': 'override output language, e.g. zh-CN or en',
|
|
219
|
+
'cli.option.noRemote': 'disable live network probes',
|
|
220
|
+
'cli.option.timeout': 'remote check timeout in milliseconds',
|
|
221
|
+
'ui.detail.risk': 'risk',
|
|
222
|
+
'ui.detail.evidence': 'evidence',
|
|
223
|
+
'ui.detail.source': 'source',
|
|
224
|
+
'ui.detail.suggestion': 'suggestion',
|
|
225
|
+
'ui.priority.fix_now': 'fix now',
|
|
226
|
+
'ui.priority.review_soon': 'review soon',
|
|
227
|
+
'ui.priority.optional': 'optional',
|
|
228
|
+
'risk.low': 'low',
|
|
229
|
+
'risk.medium': 'medium',
|
|
230
|
+
'risk.high': 'high',
|
|
231
|
+
'risk.unknown': 'unknown',
|
|
232
|
+
'module.network': 'Network',
|
|
233
|
+
'module.runtime': 'Runtime',
|
|
234
|
+
'module.install': 'Install',
|
|
235
|
+
'module.config': 'Config',
|
|
236
|
+
'module.artifacts': 'Artifacts',
|
|
237
|
+
'module.fingerprint': 'Fingerprint',
|
|
238
|
+
'module.staticInstall': 'Static install',
|
|
239
|
+
'check.artifacts.identity.title': 'Claude identity summary',
|
|
240
|
+
'check.artifacts.identity.summary.pass': 'Claude identity information is recorded and shown in masked form',
|
|
241
|
+
'check.artifacts.identity.summary.warn': 'Could not read the Claude identity config',
|
|
242
|
+
'check.artifacts.identity.suggestion.warn': 'Verify that .claude.json is readable and avoid exposing raw account data in output.',
|
|
243
|
+
'check.artifacts.telemetry.title': 'Telemetry cache',
|
|
244
|
+
'check.artifacts.telemetry.summary.pass': 'Telemetry cache directory is available',
|
|
245
|
+
'check.artifacts.telemetry.summary.warn': 'Telemetry cache directory is unavailable',
|
|
246
|
+
'check.artifacts.telemetry.suggestion.review': 'Review the cache directory history to confirm it matches expectations.',
|
|
247
|
+
'check.artifacts.projects.title': 'Projects history',
|
|
248
|
+
'check.artifacts.projects.summary.pass': 'Projects history directory is available',
|
|
249
|
+
'check.artifacts.projects.summary.skip': 'Projects history directory was not found',
|
|
250
|
+
'check.artifacts.projects.summary.warn': 'Projects history directory has an access issue',
|
|
251
|
+
'check.artifacts.projects.suggestion.review': 'Confirm whether project history data should be retained locally.',
|
|
252
|
+
'check.artifacts.credentials.title': 'Credentials file',
|
|
253
|
+
'check.artifacts.credentials.summary.warn': 'A local credentials file was detected',
|
|
254
|
+
'check.artifacts.credentials.summary.pass': 'No local credentials file was found',
|
|
255
|
+
'check.artifacts.credentials.suggestion.warn': 'Check whether the credentials file should continue to live on this machine.',
|
|
256
|
+
'check.fingerprint.environment.title': 'Locally observable signals',
|
|
257
|
+
'check.fingerprint.environment.summary.pass': 'No obvious automation or remote-session signals were observed',
|
|
258
|
+
'check.fingerprint.environment.summary.warn': 'Automation or remote-session signals were observed',
|
|
259
|
+
'check.fingerprint.environment.suggestion.review': 'Confirm whether these local signals are expected for this session.',
|
|
260
|
+
'check.fingerprint.runtime.title': 'Runtime inventory',
|
|
261
|
+
'check.fingerprint.runtime.summary.pass': 'Local runtime inventory was collected',
|
|
262
|
+
'check.fingerprint.runtime.suggestion.review': 'Confirm whether the available runtimes match this environment.',
|
|
263
|
+
'check.runtime.node.title': 'Node.js runtime',
|
|
264
|
+
'check.runtime.node.summary.fail': 'Node.js 20 or newer is required',
|
|
265
|
+
'check.runtime.node.summary.pass': 'Node.js version is compatible',
|
|
266
|
+
'check.runtime.node.suggestion.fail': 'Upgrade Node.js to version 20 or newer.',
|
|
267
|
+
'check.common.noAction': 'No action needed.',
|
|
268
|
+
'check.runtime.npm.title': 'npm availability',
|
|
269
|
+
'check.runtime.npm.summary.fail': 'npm is not available in the current shell',
|
|
270
|
+
'check.runtime.npm.summary.pass': 'npm is available',
|
|
271
|
+
'check.runtime.npm.suggestion.fail': 'Install npm or ensure it is available on PATH.',
|
|
272
|
+
'check.runtime.path.title': 'PATH consistency',
|
|
273
|
+
'check.runtime.path.summary.warn': 'PATH contains duplicated entries',
|
|
274
|
+
'check.runtime.path.summary.pass': 'PATH does not contain obvious duplication',
|
|
275
|
+
'check.runtime.path.suggestion.warn': 'Clean up duplicated PATH entries to reduce command resolution ambiguity.',
|
|
276
|
+
'check.install.binary.title': 'Claude Code binary',
|
|
277
|
+
'check.install.binary.summary.fail': 'Claude Code CLI is not available from the current shell',
|
|
278
|
+
'check.install.binary.summary.pass': 'Claude Code CLI is available',
|
|
279
|
+
'check.install.binary.suggestion.fail': 'Install Claude Code CLI and ensure the `claude` command is on PATH.',
|
|
280
|
+
'check.install.help.title': 'Claude Code execution',
|
|
281
|
+
'check.install.help.summary.warn': 'Claude Code CLI exists but help output failed',
|
|
282
|
+
'check.install.help.summary.pass': 'Claude Code CLI responds to basic execution',
|
|
283
|
+
'check.install.help.suggestion.warn': 'Reinstall Claude Code CLI or inspect the current installation for corruption.',
|
|
284
|
+
'check.staticInstall.summary.skip': 'Claude Code executable was not found, so static install analysis could not run',
|
|
285
|
+
'check.staticInstall.summary.warn': 'Local install text could not be read, so static install analysis is incomplete',
|
|
286
|
+
'check.staticInstall.suggestion.skip': 'Ensure the `claude` command is available before rerunning static install analysis.',
|
|
287
|
+
'check.staticInstall.suggestion.warn': 'Check whether the Claude Code install files are readable, then rerun the analysis.',
|
|
288
|
+
'check.staticInstall.endpoint.title': 'Observable endpoint summary',
|
|
289
|
+
'check.staticInstall.endpoint.summary.pass': 'Observable endpoint categories were identified from local install text',
|
|
290
|
+
'check.staticInstall.endpoint.summary.warn': 'No known endpoint categories were identified from local install text',
|
|
291
|
+
'check.staticInstall.endpoint.suggestion.review': 'Review whether these endpoint categories match the installation behavior you expect.',
|
|
292
|
+
'check.staticInstall.header.title': 'Observable header summary',
|
|
293
|
+
'check.staticInstall.header.summary.pass': 'Observable header categories were identified from local install text',
|
|
294
|
+
'check.staticInstall.header.summary.warn': 'No known header categories were identified from local install text',
|
|
295
|
+
'check.staticInstall.header.suggestion.review': 'Review whether these header categories match the installation behavior you expect.',
|
|
296
|
+
'check.staticInstall.event.title': 'Observable event summary',
|
|
297
|
+
'check.staticInstall.event.summary.pass': 'Observable event-name categories were identified from local install text',
|
|
298
|
+
'check.staticInstall.event.summary.warn': 'No known event categories were identified from local install text',
|
|
299
|
+
'check.staticInstall.event.suggestion.review': 'Review whether these event categories match the installation behavior you expect.',
|
|
300
|
+
'check.install.npx.title': 'NPX fallback',
|
|
301
|
+
'check.install.npx.summary.warn': 'NPX fallback is not currently usable',
|
|
302
|
+
'check.install.npx.summary.pass': 'NPX fallback can resolve Claude Code CLI',
|
|
303
|
+
'check.install.npx.suggestion.warn': 'If you rely on npx-based execution, verify your npm installation and package availability.',
|
|
304
|
+
'check.config.json.title': 'Claude config file',
|
|
305
|
+
'check.config.json.summary.pass': 'Claude config file exists and is valid JSON',
|
|
306
|
+
'check.config.json.summary.missing': 'Claude config file was not found',
|
|
307
|
+
'check.config.json.summary.fail': 'Claude config file exists but cannot be parsed',
|
|
308
|
+
'check.config.json.suggestion.missing': 'Run Claude Code once to generate config, or verify you are using the expected user profile.',
|
|
309
|
+
'check.config.json.suggestion.fail': 'Repair or recreate the config file before relying on this environment.',
|
|
310
|
+
'check.config.dir.title': 'Claude data directory',
|
|
311
|
+
'check.config.dir.summary.invalid': 'The Claude data path exists but is not a directory',
|
|
312
|
+
'check.config.dir.summary.pass': 'Claude data directory is present',
|
|
313
|
+
'check.config.dir.summary.missing': 'Claude data directory was not found',
|
|
314
|
+
'check.config.dir.summary.fail': 'Claude data directory could not be inspected',
|
|
315
|
+
'check.config.dir.suggestion.invalid': 'Remove the invalid path and allow Claude Code to recreate its data directory.',
|
|
316
|
+
'check.config.dir.suggestion.missing': 'Run Claude Code at least once to initialize user data.',
|
|
317
|
+
'check.config.dir.suggestion.fail': 'Review permissions and user directory health.',
|
|
318
|
+
'check.config.noProxy.title': 'NO_PROXY configuration',
|
|
319
|
+
'check.config.noProxy.summary.unset': 'NO_PROXY is not set',
|
|
320
|
+
'check.config.noProxy.summary.empty': 'NO_PROXY is set but empty after parsing',
|
|
321
|
+
'check.config.noProxy.summary.pass': 'NO_PROXY contains explicit bypass rules',
|
|
322
|
+
'check.config.noProxy.suggestion.unset': 'No action needed unless your environment requires proxy bypass rules.',
|
|
323
|
+
'check.config.noProxy.suggestion.empty': 'Clean up NO_PROXY to avoid ambiguous proxy bypass behavior.',
|
|
324
|
+
'check.config.noProxy.suggestion.pass': 'Verify the bypass list still matches your network design.',
|
|
325
|
+
'check.network.remoteDisabled': 'Remote checks are disabled',
|
|
326
|
+
'check.network.connectivity.title': 'Network connectivity',
|
|
327
|
+
'check.network.connectivity.summary.fail': 'Unable to complete the outbound connectivity probe',
|
|
328
|
+
'check.network.connectivity.summary.pass': 'Outbound connectivity probe completed successfully',
|
|
329
|
+
'check.network.connectivity.suggestion.disabled': 'Run without --no-remote to include live network checks.',
|
|
330
|
+
'check.network.connectivity.suggestion.fail': 'Check your outbound connectivity, proxy settings, and DNS resolution.',
|
|
331
|
+
'check.network.egress.title': 'Egress profile',
|
|
332
|
+
'check.network.egress.summary.fail': 'Egress profile could not be determined because the IP probe failed',
|
|
333
|
+
'check.network.egress.summary.partial': 'Public IP was detected, but geographic and ASN hints were unavailable',
|
|
334
|
+
'check.network.egress.summary.pass': 'Public IP, region, and ASN hints were collected successfully',
|
|
335
|
+
'check.network.egress.suggestion.disabled': 'Run without --no-remote to include region and ASN hints.',
|
|
336
|
+
'check.network.egress.suggestion.fail': 'Restore outbound connectivity before relying on region or ASN hints.',
|
|
337
|
+
'check.network.egress.suggestion.partial': 'Retry the egress profile check to confirm region and ASN metadata.',
|
|
338
|
+
'check.network.egress.suggestion.pass': 'Review the exposed country and ASN values to confirm they match your expectation.',
|
|
339
|
+
'check.network.externalIpSignal.title': 'External IP risk signal',
|
|
340
|
+
'check.network.externalIpSignal.summary.unavailable': 'Not enough external IP risk metadata was available',
|
|
341
|
+
'check.network.externalIpSignal.summary.low': 'External metadata does not show obvious high-risk signals for this egress IP',
|
|
342
|
+
'check.network.externalIpSignal.summary.medium': 'External metadata shows suspicious risk signals for this egress IP',
|
|
343
|
+
'check.network.externalIpSignal.summary.high': 'External metadata shows strong risk signals for this egress IP',
|
|
344
|
+
'check.network.externalIpSignal.suggestion.disabled': 'Run without --no-remote to include external IP risk signals.',
|
|
345
|
+
'check.network.externalIpSignal.suggestion.unavailable': 'Retry the external IP risk probe and confirm whether stable metadata is available.',
|
|
346
|
+
'check.network.externalIpSignal.suggestion.low': 'Keep monitoring whether the exposed IP type and organization still match your expectation.',
|
|
347
|
+
'check.network.externalIpSignal.suggestion.review': 'Review the current IP type, proxy/VPN/datacenter labels, and ASN/organization metadata.',
|
|
348
|
+
'check.network.dnsBaseline.title': 'DNS baseline',
|
|
349
|
+
'check.network.dnsBaseline.summary.fail': 'One or more target hosts could not be resolved',
|
|
350
|
+
'check.network.dnsBaseline.summary.pass': 'Target hosts resolve successfully',
|
|
351
|
+
'check.network.dnsBaseline.suggestion.fail': 'Check your DNS servers, proxy DNS behavior, and IPv4/IPv6 network availability.',
|
|
352
|
+
'check.network.dnsFamily.title': 'Address family consistency',
|
|
353
|
+
'check.network.dnsFamily.summary.skip': 'Address family consistency could not be assessed because DNS baseline resolution failed',
|
|
354
|
+
'check.network.dnsFamily.summary.warn': 'Target hosts resolve, but address families are inconsistent',
|
|
355
|
+
'check.network.dnsFamily.summary.pass': 'Target hosts resolve on both IPv4 and IPv6',
|
|
356
|
+
'check.network.dnsFamily.suggestion.skip': 'Restore DNS resolution before relying on IPv4 or IPv6 consistency hints.',
|
|
357
|
+
'check.network.dnsFamily.suggestion.warn': 'Review IPv4 and IPv6 routing to avoid inconsistent reachability.',
|
|
358
|
+
'check.network.proxy.title': 'Proxy environment',
|
|
359
|
+
'check.network.proxy.summary.unset': 'No proxy environment variables are set',
|
|
360
|
+
'check.network.proxy.summary.warn': 'HTTP and HTTPS proxy variables are not aligned',
|
|
361
|
+
'check.network.proxy.summary.pass': 'Proxy environment variables are present and aligned',
|
|
362
|
+
'check.network.proxy.suggestion.unset': 'No action needed if you expect direct network access.',
|
|
363
|
+
'check.network.proxy.suggestion.warn': 'Set both HTTP_PROXY and HTTPS_PROXY consistently, or clear both if not required.',
|
|
364
|
+
'check.network.proxy.suggestion.pass': 'Confirm the proxy endpoints are the ones you intend to use.',
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
export function normalizeLocale(input) {
|
|
369
|
+
if (!input) return null;
|
|
370
|
+
const value = input.toLowerCase();
|
|
371
|
+
if (value.startsWith('zh')) return 'zh-CN';
|
|
372
|
+
if (value.startsWith('en')) return 'en';
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function detectSystemLocale() {
|
|
377
|
+
return normalizeLocale(Intl.DateTimeFormat().resolvedOptions().locale) ?? 'zh-CN';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function resolveLocale(requestedLocale) {
|
|
381
|
+
if (requestedLocale) {
|
|
382
|
+
return normalizeLocale(requestedLocale) ?? 'zh-CN';
|
|
383
|
+
}
|
|
384
|
+
return detectSystemLocale() ?? 'zh-CN';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function formatTemplate(template, params = {}) {
|
|
388
|
+
return template.replaceAll(/\{([^}]+)\}/g, (_, key) => String(params[key] ?? ''));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function createTranslator(locale) {
|
|
392
|
+
const resolved = resolveLocale(locale);
|
|
393
|
+
const dict = dictionaries[resolved] ?? dictionaries['zh-CN'];
|
|
394
|
+
const fallback = dictionaries['zh-CN'];
|
|
395
|
+
|
|
396
|
+
function t(key, params = {}) {
|
|
397
|
+
const template = dict[key] ?? fallback[key] ?? key;
|
|
398
|
+
return formatTemplate(template, params);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
locale: resolved,
|
|
403
|
+
t,
|
|
404
|
+
};
|
|
405
|
+
}
|
package/src/modules.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { artifactsModule } from './checks/artifacts.js';
|
|
2
|
+
import { configModule } from './checks/config.js';
|
|
3
|
+
import { fingerprintModule } from './checks/fingerprint.js';
|
|
4
|
+
import { installModule } from './checks/install.js';
|
|
5
|
+
import { networkModule } from './checks/network.js';
|
|
6
|
+
import { runtimeModule } from './checks/runtime.js';
|
|
7
|
+
import { staticInstallModule } from './checks/static-install.js';
|
|
8
|
+
|
|
9
|
+
export const defaultModules = [
|
|
10
|
+
networkModule,
|
|
11
|
+
runtimeModule,
|
|
12
|
+
installModule,
|
|
13
|
+
configModule,
|
|
14
|
+
artifactsModule,
|
|
15
|
+
fingerprintModule,
|
|
16
|
+
staticInstallModule,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function selectModules(modules, targetId) {
|
|
20
|
+
if (!targetId || targetId === 'doctor') {
|
|
21
|
+
return modules;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return modules.filter((module) => module.id === targetId);
|
|
25
|
+
}
|
package/src/render.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
function line(text = '') {
|
|
2
|
+
return `${text}\n`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const ANSI = {
|
|
6
|
+
reset: '\u001b[0m',
|
|
7
|
+
red: '\u001b[31m',
|
|
8
|
+
yellow: '\u001b[33m',
|
|
9
|
+
green: '\u001b[32m',
|
|
10
|
+
gray: '\u001b[90m',
|
|
11
|
+
bold: '\u001b[1m',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function colorize(text, color) {
|
|
15
|
+
return `${color}${text}${ANSI.reset}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatGeneratedAt(timestamp, locale) {
|
|
19
|
+
const date = new Date(timestamp);
|
|
20
|
+
if (Number.isNaN(date.getTime())) {
|
|
21
|
+
return timestamp;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return new Intl.DateTimeFormat(locale, {
|
|
25
|
+
year: 'numeric',
|
|
26
|
+
month: 'numeric',
|
|
27
|
+
day: 'numeric',
|
|
28
|
+
hour: '2-digit',
|
|
29
|
+
minute: '2-digit',
|
|
30
|
+
second: '2-digit',
|
|
31
|
+
hour12: false,
|
|
32
|
+
}).format(date);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatPriority(priority, t) {
|
|
36
|
+
return t(`ui.priority.${priority}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatStatusSymbol(status) {
|
|
40
|
+
switch (status) {
|
|
41
|
+
case 'fail':
|
|
42
|
+
return colorize('✖', ANSI.red);
|
|
43
|
+
case 'warn':
|
|
44
|
+
return colorize('▲', ANSI.yellow);
|
|
45
|
+
case 'skip':
|
|
46
|
+
return colorize('•', ANSI.gray);
|
|
47
|
+
default:
|
|
48
|
+
return colorize('✔', ANSI.green);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatRiskLabel(riskLevel, t) {
|
|
53
|
+
const label = t(`risk.${riskLevel}`);
|
|
54
|
+
switch (riskLevel) {
|
|
55
|
+
case 'high':
|
|
56
|
+
return colorize(label, ANSI.red);
|
|
57
|
+
case 'medium':
|
|
58
|
+
return colorize(label, ANSI.yellow);
|
|
59
|
+
case 'unknown':
|
|
60
|
+
return colorize(label, ANSI.gray);
|
|
61
|
+
default:
|
|
62
|
+
return colorize(label, ANSI.green);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function localizeMessage(key, fallback, params, t) {
|
|
67
|
+
if (key) {
|
|
68
|
+
return t(key, params);
|
|
69
|
+
}
|
|
70
|
+
return fallback ?? '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function localizeCheck(check, t) {
|
|
74
|
+
return {
|
|
75
|
+
...check,
|
|
76
|
+
title: localizeMessage(check.titleKey, check.title, check.messageParams, t),
|
|
77
|
+
summary: localizeMessage(check.summaryKey, check.summary, check.messageParams, t),
|
|
78
|
+
suggestion: localizeMessage(check.suggestionKey, check.suggestion, check.messageParams, t),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function localizeReport(report, t) {
|
|
83
|
+
return {
|
|
84
|
+
...report,
|
|
85
|
+
coverageMatrix: (report.coverageMatrix ?? []).map((row) => ({
|
|
86
|
+
...row,
|
|
87
|
+
title: localizeMessage(row.titleKey ?? `coverage.${row.id}.title`, row.title, {}, t),
|
|
88
|
+
state: localizeMessage(row.stateKey ?? `coverage.state.${row.state}`, row.state, {}, t),
|
|
89
|
+
})),
|
|
90
|
+
modules: report.modules.map((module) => ({
|
|
91
|
+
...module,
|
|
92
|
+
title: localizeMessage(module.titleKey, module.title, {}, t),
|
|
93
|
+
checks: module.checks.map((check) => localizeCheck(check, t)),
|
|
94
|
+
})),
|
|
95
|
+
topFindings: report.topFindings.map((check) => localizeCheck(check, t)),
|
|
96
|
+
recommendedActions: report.recommendedActions.map((action) => ({
|
|
97
|
+
...action,
|
|
98
|
+
suggestion: localizeMessage(action.suggestionKey, action.suggestion, action.messageParams, t),
|
|
99
|
+
})),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatSummaryEntries(entries) {
|
|
104
|
+
const pairs = Object.entries(entries ?? {});
|
|
105
|
+
if (!pairs.length) {
|
|
106
|
+
return 'none';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return pairs.map(([label, count]) => `${label}: ${count}`).join(', ');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatCheck(check, verbose, t) {
|
|
113
|
+
const prefix = `${formatStatusSymbol(check.status)} ${check.title}`;
|
|
114
|
+
let output = line(`${prefix}: ${check.summary}`);
|
|
115
|
+
|
|
116
|
+
if (verbose) {
|
|
117
|
+
output += line(` ${t('ui.detail.risk')}: ${formatRiskLabel(check.riskLevel, t)}`);
|
|
118
|
+
output += line(` ${t('ui.detail.evidence')}: ${check.evidenceType}`);
|
|
119
|
+
output += line(` ${t('ui.detail.source')}: ${check.source}`);
|
|
120
|
+
for (const detail of check.details) {
|
|
121
|
+
output += line(` - ${detail}`);
|
|
122
|
+
}
|
|
123
|
+
if (check.suggestion) {
|
|
124
|
+
output += line(` ${t('ui.detail.suggestion')}: ${check.suggestion}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return output;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function renderHumanReport(report, options = {}) {
|
|
132
|
+
const verbose = Boolean(options.verbose);
|
|
133
|
+
const t = options.t ?? ((key) => key);
|
|
134
|
+
const locale = options.lang ?? 'zh-CN';
|
|
135
|
+
const localized = localizeReport(report, t);
|
|
136
|
+
let output = '';
|
|
137
|
+
|
|
138
|
+
output += line(`${ANSI.bold}${t('ui.overallRisk')}:${ANSI.reset} ${formatRiskLabel(localized.overallRiskLevel, t)}`);
|
|
139
|
+
output += line(`${colorize(`${t('ui.generatedAt')}:`, ANSI.gray)} ${formatGeneratedAt(localized.generatedAt, locale)}`);
|
|
140
|
+
output += line(t('ui.modulesChecked', localized.summary));
|
|
141
|
+
output += line();
|
|
142
|
+
|
|
143
|
+
output += line(`${ANSI.bold}${t('ui.topFindings')}:${ANSI.reset}`);
|
|
144
|
+
if (localized.topFindings.length) {
|
|
145
|
+
for (const finding of localized.topFindings.slice(0, 3)) {
|
|
146
|
+
output += line(`- ${finding.title}: ${finding.summary}`);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
output += line(`- ${t('ui.noFindings')}`);
|
|
150
|
+
}
|
|
151
|
+
output += line();
|
|
152
|
+
|
|
153
|
+
if (localized.coverageMatrix?.length) {
|
|
154
|
+
output += line(`${ANSI.bold}${t('ui.coverageMatrix')}:${ANSI.reset}`);
|
|
155
|
+
for (const row of localized.coverageMatrix.slice(0, 5)) {
|
|
156
|
+
output += line(`- ${row.title}: ${row.state}`);
|
|
157
|
+
}
|
|
158
|
+
output += line();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (localized.evidenceSummary) {
|
|
162
|
+
output += line(`${ANSI.bold}${t('ui.evidenceSummary')}:${ANSI.reset}`);
|
|
163
|
+
output += line(`- ${t('ui.evidenceSources')}: ${formatSummaryEntries(localized.evidenceSummary.sources)}`);
|
|
164
|
+
output += line(`- ${t('ui.evidenceConfidence')}: ${formatSummaryEntries(localized.evidenceSummary.confidence)}`);
|
|
165
|
+
output += line();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const module of localized.modules) {
|
|
169
|
+
output += line(`${ANSI.bold}${module.title}${ANSI.reset} (${formatRiskLabel(module.riskLevel, t)})`);
|
|
170
|
+
for (const check of module.checks) {
|
|
171
|
+
output += formatCheck(check, verbose, t);
|
|
172
|
+
}
|
|
173
|
+
output += line();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (localized.recommendedActions.length) {
|
|
177
|
+
output += line(`${ANSI.bold}${t('ui.recommendedActions')}:${ANSI.reset}`);
|
|
178
|
+
for (const action of localized.recommendedActions) {
|
|
179
|
+
output += line(`- [${formatPriority(action.priority, t)}] ${action.suggestion}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return output;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function renderJsonReport(report, options = {}) {
|
|
187
|
+
const t = options.t ?? ((key) => key);
|
|
188
|
+
return JSON.stringify(localizeReport(report, t), null, 2);
|
|
189
|
+
}
|