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
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { createCheckResult, maskValue, safeReadJson, summarizeDirectory } from './helpers.js';
|
|
6
|
+
|
|
7
|
+
async function defaultStatMtime(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
const stat = await fs.stat(filePath);
|
|
10
|
+
return {
|
|
11
|
+
ok: true,
|
|
12
|
+
mtime: stat.mtime.toISOString(),
|
|
13
|
+
};
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
code: error.code,
|
|
18
|
+
message: error.message,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildMtimeDetail(statResult) {
|
|
24
|
+
if (!statResult.ok) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return `mtime=${statResult.mtime}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function summarizeHomeArtifactPath(segment) {
|
|
32
|
+
return `path=${path.posix.join('~', String(segment))}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function summarizeClaudeArtifactPath(...segments) {
|
|
36
|
+
return `path=${path.posix.join('~', '.claude', ...segments.map((segment) => String(segment)))}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isMissingError(result) {
|
|
40
|
+
return Boolean(result && !result.ok && result.code === 'ENOENT');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildIdentityDetails({
|
|
44
|
+
configResult,
|
|
45
|
+
mtimeResult,
|
|
46
|
+
}) {
|
|
47
|
+
if (!configResult.ok) {
|
|
48
|
+
return [
|
|
49
|
+
summarizeHomeArtifactPath('.claude.json'),
|
|
50
|
+
`error=${configResult.code ?? 'unavailable'}`,
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const details = [
|
|
55
|
+
summarizeHomeArtifactPath('.claude.json'),
|
|
56
|
+
`keys=${Object.keys(configResult.data ?? {}).length}`,
|
|
57
|
+
`userID=${configResult.data?.userID ? 'present' : 'absent'}`,
|
|
58
|
+
`accountUuid=${configResult.data?.accountUuid ? maskValue(configResult.data.accountUuid) : 'absent'}`,
|
|
59
|
+
`email=${configResult.data?.email ? maskValue(configResult.data.email, { start: 2, end: 10 }) : 'absent'}`,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const mtimeDetail = buildMtimeDetail(mtimeResult);
|
|
63
|
+
if (mtimeDetail) {
|
|
64
|
+
details.push(mtimeDetail);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return details;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildDirectoryDetails({
|
|
71
|
+
dirLabel,
|
|
72
|
+
summaryResult,
|
|
73
|
+
mtimeResult,
|
|
74
|
+
}) {
|
|
75
|
+
if (!summaryResult.ok) {
|
|
76
|
+
return [
|
|
77
|
+
summarizeClaudeArtifactPath(dirLabel),
|
|
78
|
+
`error=${summaryResult.code ?? 'unavailable'}`,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const details = [
|
|
83
|
+
summarizeClaudeArtifactPath(dirLabel),
|
|
84
|
+
`entries=${summaryResult.entryCount}`,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
if ((summaryResult.sample ?? []).length > 0) {
|
|
88
|
+
details.push(`sampleCount=${summaryResult.sample.length}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mtimeDetail = buildMtimeDetail(mtimeResult);
|
|
92
|
+
if (mtimeDetail) {
|
|
93
|
+
details.push(mtimeDetail);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return details;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hasNonMissingProbeFailure(result) {
|
|
100
|
+
return Boolean(result && !result.ok && result.code && result.code !== 'ENOENT');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function hasMissingProbe(result) {
|
|
104
|
+
return isMissingError(result);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildCredentialsDetails({
|
|
108
|
+
credentialsPath,
|
|
109
|
+
present,
|
|
110
|
+
readResult,
|
|
111
|
+
mtimeResult,
|
|
112
|
+
}) {
|
|
113
|
+
if (!present) {
|
|
114
|
+
const errorDetails = [];
|
|
115
|
+
if (hasNonMissingProbeFailure(readResult)) {
|
|
116
|
+
errorDetails.push(`readError=${readResult.code}`);
|
|
117
|
+
}
|
|
118
|
+
if (hasNonMissingProbeFailure(mtimeResult)) {
|
|
119
|
+
errorDetails.push(`statError=${mtimeResult.code}`);
|
|
120
|
+
}
|
|
121
|
+
if (errorDetails.length) {
|
|
122
|
+
return [credentialsPath, ...errorDetails];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [credentialsPath, 'credentials-file=absent'];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const details = [credentialsPath, 'credentials-file=present'];
|
|
129
|
+
const mtimeDetail = buildMtimeDetail(mtimeResult);
|
|
130
|
+
if (mtimeDetail) {
|
|
131
|
+
details.push(mtimeDetail);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return details;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveDirectoryState(summaryResult) {
|
|
138
|
+
if (summaryResult.ok) {
|
|
139
|
+
return {
|
|
140
|
+
status: 'pass',
|
|
141
|
+
riskLevel: summaryResult.entryCount > 0 ? 'low' : 'low',
|
|
142
|
+
evidenceType: 'observed',
|
|
143
|
+
summaryKey: 'check.artifacts.projects.summary.pass',
|
|
144
|
+
suggestionKey: 'check.artifacts.projects.suggestion.review',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (hasNonMissingProbeFailure(summaryResult)) {
|
|
149
|
+
return {
|
|
150
|
+
status: 'warn',
|
|
151
|
+
riskLevel: 'high',
|
|
152
|
+
evidenceType: 'partial',
|
|
153
|
+
summaryKey: 'check.artifacts.projects.summary.warn',
|
|
154
|
+
suggestionKey: 'check.artifacts.projects.suggestion.review',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
status: 'skip',
|
|
160
|
+
riskLevel: 'unknown',
|
|
161
|
+
evidenceType: 'unverifiable',
|
|
162
|
+
summaryKey: 'check.artifacts.projects.summary.skip',
|
|
163
|
+
suggestionKey: 'check.artifacts.projects.suggestion.review',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resolveCredentialsState({
|
|
168
|
+
readResult,
|
|
169
|
+
mtimeResult,
|
|
170
|
+
}) {
|
|
171
|
+
if (readResult.ok || mtimeResult.ok) {
|
|
172
|
+
return {
|
|
173
|
+
status: 'warn',
|
|
174
|
+
riskLevel: 'high',
|
|
175
|
+
evidenceType: 'observed',
|
|
176
|
+
summaryKey: 'check.artifacts.credentials.summary.warn',
|
|
177
|
+
suggestionKey: 'check.artifacts.credentials.suggestion.warn',
|
|
178
|
+
present: true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (hasNonMissingProbeFailure(readResult) || hasNonMissingProbeFailure(mtimeResult)) {
|
|
183
|
+
return {
|
|
184
|
+
status: 'warn',
|
|
185
|
+
riskLevel: 'high',
|
|
186
|
+
evidenceType: 'partial',
|
|
187
|
+
summaryKey: 'check.artifacts.credentials.summary.warn',
|
|
188
|
+
suggestionKey: 'check.artifacts.credentials.suggestion.warn',
|
|
189
|
+
present: false,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (hasMissingProbe(readResult) && hasMissingProbe(mtimeResult)) {
|
|
194
|
+
return {
|
|
195
|
+
status: 'pass',
|
|
196
|
+
riskLevel: 'low',
|
|
197
|
+
evidenceType: 'partial',
|
|
198
|
+
summaryKey: 'check.artifacts.credentials.summary.pass',
|
|
199
|
+
suggestionKey: 'check.common.noAction',
|
|
200
|
+
present: false,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
status: 'warn',
|
|
206
|
+
riskLevel: 'high',
|
|
207
|
+
evidenceType: 'partial',
|
|
208
|
+
summaryKey: 'check.artifacts.credentials.summary.warn',
|
|
209
|
+
suggestionKey: 'check.artifacts.credentials.suggestion.warn',
|
|
210
|
+
present: false,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function createArtifactsModule({
|
|
215
|
+
homeDir = os.homedir(),
|
|
216
|
+
safeReadJson: readJson = safeReadJson,
|
|
217
|
+
summarizeDirectory: summarizeDir = summarizeDirectory,
|
|
218
|
+
statMtime = defaultStatMtime,
|
|
219
|
+
} = {}) {
|
|
220
|
+
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
221
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
222
|
+
const telemetryDir = path.join(claudeDir, 'telemetry');
|
|
223
|
+
const projectsDir = path.join(claudeDir, 'projects');
|
|
224
|
+
const credentialsPath = path.join(claudeDir, '.credentials.json');
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
id: 'artifacts',
|
|
228
|
+
titleKey: 'module.artifacts',
|
|
229
|
+
async run() {
|
|
230
|
+
const configResult = await readJson(claudeJsonPath);
|
|
231
|
+
const telemetryResult = await summarizeDir(telemetryDir);
|
|
232
|
+
const projectsResult = await summarizeDir(projectsDir);
|
|
233
|
+
const credentialsResult = await readJson(credentialsPath);
|
|
234
|
+
|
|
235
|
+
const [configMtime, telemetryMtime, projectsMtime, credentialsMtime] = await Promise.all([
|
|
236
|
+
statMtime(claudeJsonPath),
|
|
237
|
+
statMtime(telemetryDir),
|
|
238
|
+
statMtime(projectsDir),
|
|
239
|
+
statMtime(credentialsPath),
|
|
240
|
+
]);
|
|
241
|
+
const credentialsState = resolveCredentialsState({
|
|
242
|
+
readResult: credentialsResult,
|
|
243
|
+
mtimeResult: credentialsMtime,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return [
|
|
247
|
+
createCheckResult({
|
|
248
|
+
id: 'artifacts.identity-summary',
|
|
249
|
+
titleKey: 'check.artifacts.identity.title',
|
|
250
|
+
status: configResult.ok ? 'pass' : 'warn',
|
|
251
|
+
riskLevel: configResult.ok ? 'low' : 'medium',
|
|
252
|
+
evidenceType: configResult.ok ? 'observed' : 'partial',
|
|
253
|
+
source: 'local-file',
|
|
254
|
+
summaryKey: configResult.ok
|
|
255
|
+
? 'check.artifacts.identity.summary.pass'
|
|
256
|
+
: 'check.artifacts.identity.summary.warn',
|
|
257
|
+
details: buildIdentityDetails({
|
|
258
|
+
configResult,
|
|
259
|
+
mtimeResult: configMtime,
|
|
260
|
+
}),
|
|
261
|
+
suggestionKey: configResult.ok
|
|
262
|
+
? 'check.common.noAction'
|
|
263
|
+
: 'check.artifacts.identity.suggestion.warn',
|
|
264
|
+
}),
|
|
265
|
+
createCheckResult({
|
|
266
|
+
id: 'artifacts.telemetry-cache',
|
|
267
|
+
titleKey: 'check.artifacts.telemetry.title',
|
|
268
|
+
status: telemetryResult.ok ? 'pass' : 'warn',
|
|
269
|
+
riskLevel: telemetryResult.ok && telemetryResult.entryCount > 0 ? 'medium' : 'low',
|
|
270
|
+
evidenceType: telemetryResult.ok ? 'observed' : 'partial',
|
|
271
|
+
source: 'local-file',
|
|
272
|
+
summaryKey: telemetryResult.ok
|
|
273
|
+
? 'check.artifacts.telemetry.summary.pass'
|
|
274
|
+
: 'check.artifacts.telemetry.summary.warn',
|
|
275
|
+
details: buildDirectoryDetails({
|
|
276
|
+
dirLabel: 'telemetry',
|
|
277
|
+
summaryResult: telemetryResult,
|
|
278
|
+
mtimeResult: telemetryMtime,
|
|
279
|
+
}),
|
|
280
|
+
suggestionKey: 'check.artifacts.telemetry.suggestion.review',
|
|
281
|
+
}),
|
|
282
|
+
createCheckResult({
|
|
283
|
+
id: 'artifacts.projects-history',
|
|
284
|
+
titleKey: 'check.artifacts.projects.title',
|
|
285
|
+
status: resolveDirectoryState(projectsResult).status,
|
|
286
|
+
riskLevel: resolveDirectoryState(projectsResult).riskLevel,
|
|
287
|
+
evidenceType: resolveDirectoryState(projectsResult).evidenceType,
|
|
288
|
+
source: 'local-file',
|
|
289
|
+
summaryKey: resolveDirectoryState(projectsResult).summaryKey,
|
|
290
|
+
details: buildDirectoryDetails({
|
|
291
|
+
dirLabel: 'projects',
|
|
292
|
+
summaryResult: projectsResult,
|
|
293
|
+
mtimeResult: projectsMtime,
|
|
294
|
+
}),
|
|
295
|
+
suggestionKey: resolveDirectoryState(projectsResult).suggestionKey,
|
|
296
|
+
}),
|
|
297
|
+
createCheckResult({
|
|
298
|
+
id: 'artifacts.credentials-file',
|
|
299
|
+
titleKey: 'check.artifacts.credentials.title',
|
|
300
|
+
status: credentialsState.status,
|
|
301
|
+
riskLevel: credentialsState.riskLevel,
|
|
302
|
+
evidenceType: credentialsState.evidenceType,
|
|
303
|
+
source: 'local-file',
|
|
304
|
+
summaryKey: credentialsState.summaryKey,
|
|
305
|
+
details: buildCredentialsDetails({
|
|
306
|
+
credentialsPath: summarizeClaudeArtifactPath('.credentials.json'),
|
|
307
|
+
present: credentialsState.present,
|
|
308
|
+
readResult: credentialsResult,
|
|
309
|
+
mtimeResult: credentialsMtime,
|
|
310
|
+
}),
|
|
311
|
+
suggestionKey: credentialsState.suggestionKey,
|
|
312
|
+
}),
|
|
313
|
+
];
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export const artifactsModule = createArtifactsModule();
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { createCheckResult } from './helpers.js';
|
|
6
|
+
|
|
7
|
+
const CLAUDE_CONFIG_PATH = path.join(os.homedir(), '.claude.json');
|
|
8
|
+
const CLAUDE_DIR_PATH = path.join(os.homedir(), '.claude');
|
|
9
|
+
|
|
10
|
+
async function checkClaudeConfigPresence() {
|
|
11
|
+
try {
|
|
12
|
+
const content = await fs.readFile(CLAUDE_CONFIG_PATH, 'utf8');
|
|
13
|
+
const parsed = JSON.parse(content);
|
|
14
|
+
|
|
15
|
+
const details = [
|
|
16
|
+
`path=${CLAUDE_CONFIG_PATH}`,
|
|
17
|
+
`keys=${Object.keys(parsed).length}`,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
return createCheckResult({
|
|
21
|
+
id: 'config.claude-json',
|
|
22
|
+
titleKey: 'check.config.json.title',
|
|
23
|
+
status: 'pass',
|
|
24
|
+
riskLevel: 'low',
|
|
25
|
+
evidenceType: 'observed',
|
|
26
|
+
source: 'local',
|
|
27
|
+
summaryKey: 'check.config.json.summary.pass',
|
|
28
|
+
details,
|
|
29
|
+
suggestionKey: 'check.common.noAction',
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code === 'ENOENT') {
|
|
33
|
+
return createCheckResult({
|
|
34
|
+
id: 'config.claude-json',
|
|
35
|
+
titleKey: 'check.config.json.title',
|
|
36
|
+
status: 'warn',
|
|
37
|
+
riskLevel: 'medium',
|
|
38
|
+
evidenceType: 'observed',
|
|
39
|
+
source: 'local',
|
|
40
|
+
summaryKey: 'check.config.json.summary.missing',
|
|
41
|
+
details: [`path=${CLAUDE_CONFIG_PATH}`],
|
|
42
|
+
suggestionKey: 'check.config.json.suggestion.missing',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return createCheckResult({
|
|
47
|
+
id: 'config.claude-json',
|
|
48
|
+
titleKey: 'check.config.json.title',
|
|
49
|
+
status: 'fail',
|
|
50
|
+
riskLevel: 'high',
|
|
51
|
+
evidenceType: 'observed',
|
|
52
|
+
source: 'local',
|
|
53
|
+
summaryKey: 'check.config.json.summary.fail',
|
|
54
|
+
details: [error.message],
|
|
55
|
+
suggestionKey: 'check.config.json.suggestion.fail',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function checkClaudeDirectory() {
|
|
61
|
+
try {
|
|
62
|
+
const stat = await fs.stat(CLAUDE_DIR_PATH);
|
|
63
|
+
if (!stat.isDirectory()) {
|
|
64
|
+
return createCheckResult({
|
|
65
|
+
id: 'config.claude-dir',
|
|
66
|
+
titleKey: 'check.config.dir.title',
|
|
67
|
+
status: 'fail',
|
|
68
|
+
riskLevel: 'high',
|
|
69
|
+
evidenceType: 'observed',
|
|
70
|
+
source: 'local',
|
|
71
|
+
summaryKey: 'check.config.dir.summary.invalid',
|
|
72
|
+
details: [`path=${CLAUDE_DIR_PATH}`],
|
|
73
|
+
suggestionKey: 'check.config.dir.suggestion.invalid',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const entries = await fs.readdir(CLAUDE_DIR_PATH);
|
|
78
|
+
return createCheckResult({
|
|
79
|
+
id: 'config.claude-dir',
|
|
80
|
+
titleKey: 'check.config.dir.title',
|
|
81
|
+
status: 'pass',
|
|
82
|
+
riskLevel: 'low',
|
|
83
|
+
evidenceType: 'observed',
|
|
84
|
+
source: 'local',
|
|
85
|
+
summaryKey: 'check.config.dir.summary.pass',
|
|
86
|
+
details: [`path=${CLAUDE_DIR_PATH}`, `entries=${entries.length}`],
|
|
87
|
+
suggestionKey: 'check.common.noAction',
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code === 'ENOENT') {
|
|
91
|
+
return createCheckResult({
|
|
92
|
+
id: 'config.claude-dir',
|
|
93
|
+
titleKey: 'check.config.dir.title',
|
|
94
|
+
status: 'warn',
|
|
95
|
+
riskLevel: 'medium',
|
|
96
|
+
evidenceType: 'observed',
|
|
97
|
+
source: 'local',
|
|
98
|
+
summaryKey: 'check.config.dir.summary.missing',
|
|
99
|
+
details: [`path=${CLAUDE_DIR_PATH}`],
|
|
100
|
+
suggestionKey: 'check.config.dir.suggestion.missing',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return createCheckResult({
|
|
105
|
+
id: 'config.claude-dir',
|
|
106
|
+
titleKey: 'check.config.dir.title',
|
|
107
|
+
status: 'fail',
|
|
108
|
+
riskLevel: 'high',
|
|
109
|
+
evidenceType: 'observed',
|
|
110
|
+
source: 'local',
|
|
111
|
+
summaryKey: 'check.config.dir.summary.fail',
|
|
112
|
+
details: [error.message],
|
|
113
|
+
suggestionKey: 'check.config.dir.suggestion.fail',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function checkProxyNoProxyRelationship() {
|
|
119
|
+
const noProxy = process.env.NO_PROXY;
|
|
120
|
+
|
|
121
|
+
if (!noProxy) {
|
|
122
|
+
return createCheckResult({
|
|
123
|
+
id: 'config.no-proxy',
|
|
124
|
+
titleKey: 'check.config.noProxy.title',
|
|
125
|
+
status: 'pass',
|
|
126
|
+
riskLevel: 'low',
|
|
127
|
+
evidenceType: 'observed',
|
|
128
|
+
source: 'local',
|
|
129
|
+
summaryKey: 'check.config.noProxy.summary.unset',
|
|
130
|
+
details: ['NO_PROXY=unset'],
|
|
131
|
+
suggestionKey: 'check.config.noProxy.suggestion.unset',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const entries = noProxy.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
136
|
+
|
|
137
|
+
if (!entries.length) {
|
|
138
|
+
return createCheckResult({
|
|
139
|
+
id: 'config.no-proxy',
|
|
140
|
+
titleKey: 'check.config.noProxy.title',
|
|
141
|
+
status: 'warn',
|
|
142
|
+
riskLevel: 'medium',
|
|
143
|
+
evidenceType: 'observed',
|
|
144
|
+
source: 'local',
|
|
145
|
+
summaryKey: 'check.config.noProxy.summary.empty',
|
|
146
|
+
details: [`NO_PROXY=${noProxy}`],
|
|
147
|
+
suggestionKey: 'check.config.noProxy.suggestion.empty',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return createCheckResult({
|
|
152
|
+
id: 'config.no-proxy',
|
|
153
|
+
titleKey: 'check.config.noProxy.title',
|
|
154
|
+
status: 'pass',
|
|
155
|
+
riskLevel: 'low',
|
|
156
|
+
evidenceType: 'observed',
|
|
157
|
+
source: 'local',
|
|
158
|
+
summaryKey: 'check.config.noProxy.summary.pass',
|
|
159
|
+
details: entries.slice(0, 5).map((entry) => `entry=${entry}`),
|
|
160
|
+
suggestionKey: 'check.config.noProxy.suggestion.pass',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const configModule = {
|
|
165
|
+
id: 'config',
|
|
166
|
+
titleKey: 'module.config',
|
|
167
|
+
async run() {
|
|
168
|
+
return [
|
|
169
|
+
await checkClaudeConfigPresence(),
|
|
170
|
+
await checkClaudeDirectory(),
|
|
171
|
+
checkProxyNoProxyRelationship(),
|
|
172
|
+
];
|
|
173
|
+
},
|
|
174
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
|
|
3
|
+
import { createCheckResult, runtimeSnapshot, tryExec } from './helpers.js';
|
|
4
|
+
|
|
5
|
+
const INVENTORY_COMMANDS = ['bun', 'deno', 'npm', 'pnpm', 'yarn'];
|
|
6
|
+
const FALSEY_ENV_VALUES = new Set(['false', '0', 'no', 'off']);
|
|
7
|
+
|
|
8
|
+
function isTruthyEnvFlag(value) {
|
|
9
|
+
if (value === undefined || value === null) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const normalized = String(value).trim().toLowerCase();
|
|
14
|
+
if (!normalized) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return !FALSEY_ENV_VALUES.has(normalized);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function detectIde(env) {
|
|
22
|
+
if (isTruthyEnvFlag(env.CURSOR_TRACE_ID)) {
|
|
23
|
+
return 'cursor';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isTruthyEnvFlag(env.VSCODE_GIT_IPC_HANDLE) || isTruthyEnvFlag(env.VSCODE_IPC_HOOK_CLI) || String(env.TERM_PROGRAM ?? '').toLowerCase() === 'vscode') {
|
|
27
|
+
return 'vscode';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isTruthyEnvFlag(env.JETBRAINS_IDE) || isTruthyEnvFlag(env.IDEA_INITIAL_DIRECTORY) || isTruthyEnvFlag(env.PYCHARM_HOSTED)) {
|
|
31
|
+
return 'jetbrains';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return 'unknown';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function summarizeGitRemote(result) {
|
|
38
|
+
if (!result.ok || !result.stdout) {
|
|
39
|
+
return 'unavailable';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const remote = String(result.stdout).trim().toLowerCase();
|
|
43
|
+
if (remote.startsWith('git@') || remote.startsWith('ssh://')) {
|
|
44
|
+
return 'present:ssh';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (remote.startsWith('https://') || remote.startsWith('http://')) {
|
|
48
|
+
return 'present:https';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (remote.startsWith('file://')) {
|
|
52
|
+
return 'present:file';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return 'present:other';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatBooleanDetail(key, value) {
|
|
59
|
+
return `${key}=${Boolean(value)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function summarizeRuntimeSnapshot() {
|
|
63
|
+
return runtimeSnapshot().filter((entry) => !entry.startsWith('hostname='));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function probeRuntimeVersion(command, exec) {
|
|
67
|
+
const result = await exec(command, ['--version']);
|
|
68
|
+
if (!result.ok || !result.stdout) {
|
|
69
|
+
return `${command}=absent`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const firstLine = String(result.stdout).split(/\r?\n/, 1)[0].trim();
|
|
73
|
+
const versionToken = firstLine.match(/v?\d+(?:\.\d+){0,3}(?:[-+][0-9A-Za-z.-]+)?/);
|
|
74
|
+
|
|
75
|
+
if (versionToken) {
|
|
76
|
+
return `${command}=present:${versionToken[0]}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return `${command}=present`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function buildRuntimeInventory(exec) {
|
|
83
|
+
const details = [`node=${process.version}`];
|
|
84
|
+
const probes = await Promise.all(INVENTORY_COMMANDS.map((command) => probeRuntimeVersion(command, exec)));
|
|
85
|
+
details.push(...probes);
|
|
86
|
+
return details;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function buildGitRemoteSummary(exec) {
|
|
90
|
+
const result = await exec('git', ['remote', 'get-url', 'origin']);
|
|
91
|
+
return summarizeGitRemote(result);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildEnvironmentDetails({
|
|
95
|
+
env,
|
|
96
|
+
isTTY,
|
|
97
|
+
gitRemoteSummary,
|
|
98
|
+
}) {
|
|
99
|
+
return [
|
|
100
|
+
...summarizeRuntimeSnapshot(),
|
|
101
|
+
formatBooleanDetail('ci', isTruthyEnvFlag(env.CI)),
|
|
102
|
+
formatBooleanDetail('ssh', isTruthyEnvFlag(env.SSH_CONNECTION) || isTruthyEnvFlag(env.SSH_CLIENT) || isTruthyEnvFlag(env.SSH_TTY)),
|
|
103
|
+
formatBooleanDetail('tmux', isTruthyEnvFlag(env.TMUX)),
|
|
104
|
+
formatBooleanDetail('screen', isTruthyEnvFlag(env.STY)),
|
|
105
|
+
formatBooleanDetail('tty', isTTY),
|
|
106
|
+
`ide=${detectIde(env)}`,
|
|
107
|
+
`gitRemote=${gitRemoteSummary}`,
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasAutomationSignals(env) {
|
|
112
|
+
return isTruthyEnvFlag(env.CI)
|
|
113
|
+
|| isTruthyEnvFlag(env.SSH_CONNECTION)
|
|
114
|
+
|| isTruthyEnvFlag(env.SSH_CLIENT)
|
|
115
|
+
|| isTruthyEnvFlag(env.SSH_TTY)
|
|
116
|
+
|| isTruthyEnvFlag(env.TMUX)
|
|
117
|
+
|| isTruthyEnvFlag(env.STY);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveEnvironmentRisk(env) {
|
|
121
|
+
if (isTruthyEnvFlag(env.SSH_CONNECTION) || isTruthyEnvFlag(env.SSH_CLIENT) || isTruthyEnvFlag(env.SSH_TTY)) {
|
|
122
|
+
return {
|
|
123
|
+
status: 'warn',
|
|
124
|
+
riskLevel: 'high',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (isTruthyEnvFlag(env.CI) || isTruthyEnvFlag(env.TMUX) || isTruthyEnvFlag(env.STY)) {
|
|
129
|
+
return {
|
|
130
|
+
status: 'warn',
|
|
131
|
+
riskLevel: 'medium',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
status: 'pass',
|
|
137
|
+
riskLevel: 'low',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function createFingerprintModule({
|
|
142
|
+
env = process.env,
|
|
143
|
+
isTTY = Boolean(process.stdout.isTTY),
|
|
144
|
+
tryExec: exec = tryExec,
|
|
145
|
+
} = {}) {
|
|
146
|
+
return {
|
|
147
|
+
id: 'fingerprint',
|
|
148
|
+
titleKey: 'module.fingerprint',
|
|
149
|
+
async run() {
|
|
150
|
+
const [runtimeDetails, gitRemoteHash] = await Promise.all([
|
|
151
|
+
buildRuntimeInventory(exec),
|
|
152
|
+
buildGitRemoteSummary(exec),
|
|
153
|
+
]);
|
|
154
|
+
const environmentDetails = buildEnvironmentDetails({
|
|
155
|
+
env,
|
|
156
|
+
isTTY,
|
|
157
|
+
gitRemoteSummary: gitRemoteHash,
|
|
158
|
+
});
|
|
159
|
+
const environmentRisk = resolveEnvironmentRisk(env);
|
|
160
|
+
const environmentSummaryKey = environmentRisk.status === 'warn'
|
|
161
|
+
? 'check.fingerprint.environment.summary.warn'
|
|
162
|
+
: 'check.fingerprint.environment.summary.pass';
|
|
163
|
+
|
|
164
|
+
return [
|
|
165
|
+
createCheckResult({
|
|
166
|
+
id: 'fingerprint.environment-signals',
|
|
167
|
+
titleKey: 'check.fingerprint.environment.title',
|
|
168
|
+
status: environmentRisk.status,
|
|
169
|
+
riskLevel: environmentRisk.riskLevel,
|
|
170
|
+
evidenceType: 'observed',
|
|
171
|
+
source: 'runtime-env',
|
|
172
|
+
summaryKey: environmentSummaryKey,
|
|
173
|
+
details: environmentDetails,
|
|
174
|
+
suggestionKey: 'check.fingerprint.environment.suggestion.review',
|
|
175
|
+
}),
|
|
176
|
+
createCheckResult({
|
|
177
|
+
id: 'fingerprint.runtime-inventory',
|
|
178
|
+
titleKey: 'check.fingerprint.runtime.title',
|
|
179
|
+
status: 'pass',
|
|
180
|
+
riskLevel: 'low',
|
|
181
|
+
evidenceType: 'observed',
|
|
182
|
+
source: 'runtime-env',
|
|
183
|
+
summaryKey: 'check.fingerprint.runtime.summary.pass',
|
|
184
|
+
details: runtimeDetails,
|
|
185
|
+
suggestionKey: hasAutomationSignals(env) ? 'check.fingerprint.runtime.suggestion.review' : 'check.common.noAction',
|
|
186
|
+
}),
|
|
187
|
+
];
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const fingerprintModule = createFingerprintModule();
|