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.
@@ -0,0 +1,326 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import { constants as fsConstants } from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import dns from 'node:dns/promises';
7
+ import { execFile } from 'node:child_process';
8
+ import { promisify } from 'node:util';
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ export function createCheckResult({
13
+ id,
14
+ title,
15
+ titleKey,
16
+ status,
17
+ riskLevel,
18
+ evidenceType,
19
+ source,
20
+ summary,
21
+ summaryKey,
22
+ details = [],
23
+ suggestion = '',
24
+ suggestionKey,
25
+ messageParams = {},
26
+ }) {
27
+ return {
28
+ id,
29
+ title,
30
+ titleKey,
31
+ status,
32
+ riskLevel,
33
+ evidenceType,
34
+ source,
35
+ summary,
36
+ summaryKey,
37
+ details,
38
+ suggestion,
39
+ suggestionKey,
40
+ messageParams,
41
+ };
42
+ }
43
+
44
+ export async function tryExec(command, args = []) {
45
+ try {
46
+ const result = await execFileAsync(command, args, {
47
+ timeout: 5_000,
48
+ env: process.env,
49
+ });
50
+
51
+ return {
52
+ ok: true,
53
+ stdout: result.stdout.trim(),
54
+ stderr: result.stderr.trim(),
55
+ };
56
+ } catch (error) {
57
+ return {
58
+ ok: false,
59
+ code: error.code,
60
+ stdout: String(error.stdout ?? '').trim(),
61
+ stderr: String(error.stderr ?? '').trim(),
62
+ message: error.message,
63
+ };
64
+ }
65
+ }
66
+
67
+ export async function safeReadJson(filePath) {
68
+ try {
69
+ const content = await fs.readFile(filePath, 'utf8');
70
+ return {
71
+ ok: true,
72
+ data: JSON.parse(content),
73
+ };
74
+ } catch (error) {
75
+ return {
76
+ ok: false,
77
+ code: error.code,
78
+ message: error.message,
79
+ };
80
+ }
81
+ }
82
+
83
+ export function maskValue(value, { start = 4, end = 4 } = {}) {
84
+ const text = String(value ?? '');
85
+
86
+ if (!text) {
87
+ return 'masked:empty';
88
+ }
89
+
90
+ if (text.length <= start + end) {
91
+ return `${text.slice(0, 1)}***`;
92
+ }
93
+
94
+ return `${text.slice(0, start)}***${text.slice(-end)}`;
95
+ }
96
+
97
+ export async function summarizeDirectory(dirPath) {
98
+ try {
99
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
100
+ const names = entries.map((entry) => entry.name).sort();
101
+
102
+ return {
103
+ ok: true,
104
+ entryCount: entries.length,
105
+ sample: names.slice(0, 5),
106
+ };
107
+ } catch (error) {
108
+ return {
109
+ ok: false,
110
+ code: error.code,
111
+ message: error.message,
112
+ };
113
+ }
114
+ }
115
+
116
+ export function sha256Prefix(value, length = 16) {
117
+ return crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, length);
118
+ }
119
+
120
+ function getPlatformPath(platform) {
121
+ return platform === 'win32' ? path.win32 : path.posix;
122
+ }
123
+
124
+ function getPathExts(platform, env = process.env) {
125
+ if (platform !== 'win32') {
126
+ return [''];
127
+ }
128
+
129
+ return (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
130
+ .split(';')
131
+ .map((ext) => ext.trim())
132
+ .filter(Boolean);
133
+ }
134
+
135
+ function uniquePush(values, candidate) {
136
+ if (candidate && !values.includes(candidate)) {
137
+ values.push(candidate);
138
+ }
139
+ }
140
+
141
+ function buildExecutableCandidates(command, { platform, env } = {}) {
142
+ const pathApi = getPlatformPath(platform);
143
+ const executable = String(command ?? '').trim();
144
+ const exts = getPathExts(platform, env);
145
+ const candidates = [];
146
+ const hasExtension = pathApi.extname(executable) !== '';
147
+ const hasSeparator = executable.includes('/') || executable.includes('\\');
148
+ const isAbsolute = path.posix.isAbsolute(executable) || path.win32.isAbsolute(executable);
149
+ const pathDirs = String(env.PATH ?? env.Path ?? '')
150
+ .split(pathApi.delimiter)
151
+ .filter(Boolean);
152
+
153
+ if (hasSeparator || isAbsolute) {
154
+ uniquePush(candidates, executable);
155
+ if (platform === 'win32' && !hasExtension) {
156
+ for (const ext of exts) {
157
+ uniquePush(candidates, `${executable}${ext.toLowerCase()}`);
158
+ }
159
+ }
160
+ return candidates;
161
+ }
162
+
163
+ for (const dir of pathDirs) {
164
+ if (platform === 'win32') {
165
+ uniquePush(candidates, pathApi.join(dir, executable));
166
+ if (!hasExtension) {
167
+ for (const ext of exts) {
168
+ uniquePush(candidates, pathApi.join(dir, `${executable}${ext.toLowerCase()}`));
169
+ }
170
+ }
171
+ continue;
172
+ }
173
+
174
+ uniquePush(candidates, pathApi.join(dir, executable));
175
+ }
176
+
177
+ return candidates;
178
+ }
179
+
180
+ export async function resolveExecutable(command, { access = fs.access, platform = process.platform, env = process.env } = {}) {
181
+ const executable = String(command ?? '').trim();
182
+
183
+ if (!executable) {
184
+ return { ok: false, message: 'command is required' };
185
+ }
186
+
187
+ for (const candidate of buildExecutableCandidates(executable, { platform, env })) {
188
+ try {
189
+ const mode = platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK;
190
+ await access(candidate, mode);
191
+ return { ok: true, path: candidate };
192
+ } catch {
193
+ continue;
194
+ }
195
+ }
196
+
197
+ return { ok: false, message: `${executable} not found` };
198
+ }
199
+
200
+ export async function readTextIfExists(filePath) {
201
+ try {
202
+ const text = await fs.readFile(filePath, 'utf8');
203
+ return {
204
+ ok: true,
205
+ text,
206
+ };
207
+ } catch (error) {
208
+ return {
209
+ ok: false,
210
+ code: error.code,
211
+ message: error.message,
212
+ };
213
+ }
214
+ }
215
+
216
+ export function extractMatches(text, patterns) {
217
+ const matches = new Set();
218
+ const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
219
+
220
+ for (const pattern of patterns) {
221
+ if (typeof pattern === 'string' && pattern.length === 0) {
222
+ continue;
223
+ }
224
+
225
+ const regex = pattern instanceof RegExp
226
+ ? new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)
227
+ : new RegExp(escapeRegExp(pattern), 'g');
228
+
229
+ for (const match of String(text).matchAll(regex)) {
230
+ matches.add(match[0]);
231
+ }
232
+ }
233
+
234
+ return [...matches].sort();
235
+ }
236
+
237
+ export function proxyEnvSnapshot(env = process.env) {
238
+ return ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY']
239
+ .filter((key) => env[key])
240
+ .map((key) => `${key}=${env[key]}`);
241
+ }
242
+
243
+ export function runtimeSnapshot() {
244
+ return [
245
+ `platform=${process.platform}`,
246
+ `arch=${process.arch}`,
247
+ `node=${process.version}`,
248
+ `hostname=${os.hostname()}`,
249
+ ];
250
+ }
251
+
252
+ export async function resolveHost(hostname) {
253
+ try {
254
+ const [v4, v6] = await Promise.allSettled([
255
+ dns.resolve4(hostname),
256
+ dns.resolve6(hostname),
257
+ ]);
258
+
259
+ return {
260
+ ok: true,
261
+ v4: v4.status === 'fulfilled' ? v4.value : [],
262
+ v6: v6.status === 'fulfilled' ? v6.value : [],
263
+ };
264
+ } catch (error) {
265
+ return {
266
+ ok: false,
267
+ error: error.message,
268
+ v4: [],
269
+ v6: [],
270
+ };
271
+ }
272
+ }
273
+
274
+ export async function fetchJson(url, timeoutMs) {
275
+ const controller = new AbortController();
276
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
277
+
278
+ try {
279
+ const response = await fetch(url, {
280
+ signal: controller.signal,
281
+ headers: {
282
+ 'user-agent': 'cc-env-checker/0.1.0',
283
+ },
284
+ });
285
+
286
+ return {
287
+ ok: response.ok,
288
+ status: response.status,
289
+ data: await response.json(),
290
+ };
291
+ } catch (error) {
292
+ return {
293
+ ok: false,
294
+ error: error.name === 'AbortError' ? 'request timed out' : error.message,
295
+ };
296
+ } finally {
297
+ clearTimeout(timer);
298
+ }
299
+ }
300
+
301
+ export async function fetchText(url, timeoutMs) {
302
+ const controller = new AbortController();
303
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
304
+
305
+ try {
306
+ const response = await fetch(url, {
307
+ signal: controller.signal,
308
+ headers: {
309
+ 'user-agent': 'cc-env-checker/0.1.0',
310
+ },
311
+ });
312
+
313
+ return {
314
+ ok: response.ok,
315
+ status: response.status,
316
+ text: await response.text(),
317
+ };
318
+ } catch (error) {
319
+ return {
320
+ ok: false,
321
+ error: error.name === 'AbortError' ? 'request timed out' : error.message,
322
+ };
323
+ } finally {
324
+ clearTimeout(timer);
325
+ }
326
+ }
@@ -0,0 +1,72 @@
1
+ import { createCheckResult, tryExec } from './helpers.js';
2
+
3
+ async function checkClaudeBinary() {
4
+ const versionResult = await tryExec('claude', ['--version']);
5
+
6
+ if (!versionResult.ok) {
7
+ return createCheckResult({
8
+ id: 'install.claude-binary',
9
+ titleKey: 'check.install.binary.title',
10
+ status: 'fail',
11
+ riskLevel: 'high',
12
+ evidenceType: 'observed',
13
+ source: 'local',
14
+ summaryKey: 'check.install.binary.summary.fail',
15
+ details: [versionResult.message],
16
+ suggestionKey: 'check.install.binary.suggestion.fail',
17
+ });
18
+ }
19
+
20
+ return createCheckResult({
21
+ id: 'install.claude-binary',
22
+ titleKey: 'check.install.binary.title',
23
+ status: 'pass',
24
+ riskLevel: 'low',
25
+ evidenceType: 'observed',
26
+ source: 'local',
27
+ summaryKey: 'check.install.binary.summary.pass',
28
+ details: [`version=${versionResult.stdout || 'unknown'}`],
29
+ suggestionKey: 'check.common.noAction',
30
+ });
31
+ }
32
+
33
+ async function checkClaudeHelp() {
34
+ const helpResult = await tryExec('claude', ['--help']);
35
+
36
+ if (!helpResult.ok) {
37
+ return createCheckResult({
38
+ id: 'install.claude-help',
39
+ titleKey: 'check.install.help.title',
40
+ status: 'warn',
41
+ riskLevel: 'medium',
42
+ evidenceType: 'observed',
43
+ source: 'local',
44
+ summaryKey: 'check.install.help.summary.warn',
45
+ details: [helpResult.message],
46
+ suggestionKey: 'check.install.help.suggestion.warn',
47
+ });
48
+ }
49
+
50
+ return createCheckResult({
51
+ id: 'install.claude-help',
52
+ titleKey: 'check.install.help.title',
53
+ status: 'pass',
54
+ riskLevel: 'low',
55
+ evidenceType: 'observed',
56
+ source: 'local',
57
+ summaryKey: 'check.install.help.summary.pass',
58
+ details: [`output-bytes=${helpResult.stdout.length}`],
59
+ suggestionKey: 'check.common.noAction',
60
+ });
61
+ }
62
+
63
+ export const installModule = {
64
+ id: 'install',
65
+ titleKey: 'module.install',
66
+ async run() {
67
+ return [
68
+ await checkClaudeBinary(),
69
+ await checkClaudeHelp(),
70
+ ];
71
+ },
72
+ };