@pzy560117/opentest 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +57 -8
  2. package/assets/manifest.json +84 -27
  3. package/assets/skills/opentest/SKILL.md +44 -44
  4. package/assets/skills/opentest/references/codex-harness-coverage-heuristics.md +55 -55
  5. package/assets/skills/opentest/references/lifecycle.md +1 -1
  6. package/assets/skills/opentest/references/matrix-format.md +14 -14
  7. package/assets/skills/opentest/references/opentest-driven-development.md +32 -32
  8. package/assets/skills/opentest/templates/acceptance-template.md +1 -1
  9. package/assets/skills/opentest/templates/matrix-template.md +3 -3
  10. package/assets/skills/opentest/templates/plan-template.md +17 -17
  11. package/assets/skills/opentest/templates/report-template.md +1 -1
  12. package/assets/skills/opentest-accept/SKILL.md +13 -13
  13. package/assets/skills/opentest-archive/SKILL.md +2 -2
  14. package/assets/skills/opentest-author/SKILL.md +14 -14
  15. package/assets/skills/opentest-heal/SKILL.md +2 -2
  16. package/assets/skills/opentest-plan/SKILL.md +17 -17
  17. package/assets/skills/opentest-run/SKILL.md +16 -16
  18. package/assets/skills/opentest-verify/SKILL.md +11 -11
  19. package/assets/skills-zh/opentest/SKILL.md +93 -0
  20. package/assets/skills-zh/opentest/references/acceptance-evidence.md +27 -0
  21. package/assets/skills-zh/opentest/references/codex-harness-coverage-heuristics.md +83 -0
  22. package/assets/skills-zh/opentest/references/command-routing.md +9 -0
  23. package/assets/skills-zh/opentest/references/lifecycle.md +16 -0
  24. package/assets/skills-zh/opentest/references/matrix-format.md +27 -0
  25. package/assets/skills-zh/opentest/references/opentest-driven-development.md +48 -0
  26. package/assets/skills-zh/opentest/references/quality-gate.md +24 -0
  27. package/assets/skills-zh/opentest/templates/acceptance-template.md +32 -0
  28. package/assets/skills-zh/opentest/templates/archive-layout.md +14 -0
  29. package/assets/skills-zh/opentest/templates/matrix-template.md +6 -0
  30. package/assets/skills-zh/opentest/templates/plan-template.md +28 -0
  31. package/assets/skills-zh/opentest/templates/report-template.md +28 -0
  32. package/assets/skills-zh/opentest-accept/SKILL.md +25 -0
  33. package/assets/skills-zh/opentest-archive/SKILL.md +8 -0
  34. package/assets/skills-zh/opentest-author/SKILL.md +27 -0
  35. package/assets/skills-zh/opentest-heal/SKILL.md +8 -0
  36. package/assets/skills-zh/opentest-plan/SKILL.md +30 -0
  37. package/assets/skills-zh/opentest-run/SKILL.md +28 -0
  38. package/assets/skills-zh/opentest-verify/SKILL.md +24 -0
  39. package/bin/opentest.js +318 -29
  40. package/package.json +1 -1
  41. package/scripts/prepublish-check.js +105 -6
  42. package/scripts/smoke-test.js +493 -23
package/bin/opentest.js CHANGED
@@ -1,25 +1,60 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { cp, mkdir, readdir, rm, stat } from 'fs/promises';
3
+ import { readFileSync } from 'fs';
4
+ import { copyFile, mkdir, readdir, rm, stat } from 'fs/promises';
4
5
  import path from 'path';
6
+ import { createInterface } from 'readline/promises';
7
+ import { stdin as input, stdout as output } from 'process';
5
8
  import { fileURLToPath } from 'url';
6
9
 
7
10
  const __filename = fileURLToPath(import.meta.url);
8
11
  const __dirname = path.dirname(__filename);
9
12
  const packageRoot = path.resolve(__dirname, '..');
10
- const skillsSource = path.join(packageRoot, 'assets', 'skills');
13
+ const manifest = JSON.parse(readFileSync(path.join(packageRoot, 'assets', 'manifest.json'), 'utf8'));
14
+
15
+ function availableIds(entries) {
16
+ return entries.map((entry) => entry.id).join(', ');
17
+ }
18
+
19
+ function resolveLanguage(languageId) {
20
+ const language = manifest.languages.find((entry) => entry.id === languageId);
21
+ if (!language) {
22
+ throw new Error(`Invalid language: ${languageId}. Available languages: ${availableIds(manifest.languages)}`);
23
+ }
24
+ return language;
25
+ }
26
+
27
+ function resolvePlatform(platformId) {
28
+ const platform = manifest.platforms.find((entry) => entry.id === platformId);
29
+ if (!platform) {
30
+ throw new Error(`Invalid platform: ${platformId}. Available platforms: ${availableIds(manifest.platforms)}`);
31
+ }
32
+ return platform;
33
+ }
34
+
35
+ function resolveScope(scope) {
36
+ const scopes = ['project', 'global'];
37
+ if (!scopes.includes(scope)) {
38
+ throw new Error(`Invalid scope: ${scope}. Available scopes: ${scopes.join(', ')}`);
39
+ }
40
+ return scope;
41
+ }
11
42
 
12
43
  function usage(exitCode = 0) {
13
44
  const text = `OpenTest skill installer
14
45
 
15
46
  Usage:
16
- opentest install [--global] [--force] [--project <path>]
17
- opentest list
47
+ opentest init [--scope <project|global>] [--platform <id>] [--language <id>] [--project <path>] [--force]
48
+ opentest install [--scope <project|global>] [--platform <id>] [--language <id>] [--project <path>] [--global] [--force]
49
+ opentest list [--language <id>]
18
50
 
19
51
  Options:
20
- --global Install to ~/.codex/skills instead of ./ .codex/skills
21
- --project PATH Install to PATH/.codex/skills
22
- --force Overwrite existing opentest skill directories
52
+ --scope SCOPE Install scope: project or global (default: project)
53
+ --platform ID Target platform: ${availableIds(manifest.platforms)} (auto-detected when possible)
54
+ --language ID Skill language: ${availableIds(manifest.languages)} (default: locale-aware)
55
+ --project PATH Project root for project scope (default: current directory)
56
+ --global Alias for --scope global --platform codex
57
+ --force Overwrite existing opentest skill directories
23
58
  `;
24
59
  (exitCode === 0 ? console.log : console.error)(text.trim());
25
60
  process.exit(exitCode);
@@ -27,18 +62,49 @@ Options:
27
62
 
28
63
  function parseArgs(argv) {
29
64
  const [command, ...rest] = argv;
30
- const options = { command, global: false, force: false, project: process.cwd() };
65
+ const options = {
66
+ command,
67
+ scope: undefined,
68
+ platform: undefined,
69
+ language: undefined,
70
+ force: false,
71
+ project: process.cwd(),
72
+ explicitTarget: false,
73
+ };
31
74
 
32
75
  for (let i = 0; i < rest.length; i += 1) {
33
76
  const arg = rest[i];
34
77
  if (arg === '--global') {
35
- options.global = true;
78
+ options.scope = 'global';
79
+ options.platform = 'codex';
80
+ options.explicitTarget = true;
36
81
  } else if (arg === '--force') {
37
82
  options.force = true;
83
+ options.explicitTarget = true;
84
+ } else if (arg === '--scope') {
85
+ const value = rest[i + 1];
86
+ if (!value) usage(1);
87
+ options.scope = value;
88
+ options.explicitTarget = true;
89
+ i += 1;
90
+ } else if (arg === '--platform') {
91
+ const value = rest[i + 1];
92
+ if (!value) usage(1);
93
+ options.platform = value;
94
+ options.explicitTarget = true;
95
+ i += 1;
96
+ } else if (arg === '--language') {
97
+ const value = rest[i + 1];
98
+ if (!value) usage(1);
99
+ options.language = value;
100
+ options.explicitTarget = true;
101
+ i += 1;
38
102
  } else if (arg === '--project') {
39
103
  const value = rest[i + 1];
40
104
  if (!value) usage(1);
105
+ options.scope = 'project';
41
106
  options.project = path.resolve(value);
107
+ options.explicitTarget = true;
42
108
  i += 1;
43
109
  } else {
44
110
  usage(1);
@@ -56,44 +122,260 @@ function homeDir() {
56
122
  return home;
57
123
  }
58
124
 
59
- function targetSkillsDir(options) {
60
- if (options.global) {
61
- return path.join(homeDir(), '.codex', 'skills');
125
+ function homeRelativePath(manifestPath) {
126
+ return manifestPath.replace(/^~[\\/]/, '');
127
+ }
128
+
129
+ function platformById(platformId) {
130
+ return manifest.platforms.find((entry) => entry.id === platformId);
131
+ }
132
+
133
+ function languageById(languageId) {
134
+ return manifest.languages.find((entry) => entry.id === languageId);
135
+ }
136
+
137
+ async function detectPlatform(projectRoot) {
138
+ const envHints = [
139
+ ['OPENTEST_PLATFORM', process.env.OPENTEST_PLATFORM],
140
+ ['CLAUDECODE', process.env.CLAUDECODE ? 'claude' : undefined],
141
+ ['CLAUDE_CODE', process.env.CLAUDE_CODE ? 'claude' : undefined],
142
+ ['CODEX', process.env.CODEX ? 'codex' : undefined],
143
+ ];
144
+ for (const [, value] of envHints) {
145
+ if (value && platformById(value)) {
146
+ return value;
147
+ }
148
+ }
149
+
150
+ for (const platform of manifest.platforms) {
151
+ if (await exists(path.join(projectRoot, platform.projectSkillsDir))) {
152
+ return platform.id;
153
+ }
154
+ }
155
+
156
+ return undefined;
157
+ }
158
+
159
+ function detectLanguage() {
160
+ const explicit = process.env.OPENTEST_LANGUAGE;
161
+ if (explicit && languageById(explicit)) {
162
+ return explicit;
163
+ }
164
+
165
+ const localeText = [
166
+ process.env.LC_ALL,
167
+ process.env.LC_MESSAGES,
168
+ process.env.LANG,
169
+ Intl.DateTimeFormat().resolvedOptions().locale,
170
+ ].filter(Boolean).join(' ').toLowerCase();
171
+
172
+ return localeText.includes('zh') ? 'zh' : 'en';
173
+ }
174
+
175
+ async function withInstallDefaults(options) {
176
+ return {
177
+ ...options,
178
+ scope: options.scope ?? 'project',
179
+ platform: options.platform ?? await detectPlatform(options.project) ?? 'codex',
180
+ language: options.language ?? detectLanguage(),
181
+ };
182
+ }
183
+
184
+ function targetSkillsDir(options, platform) {
185
+ if (options.scope === 'global') {
186
+ return path.join(homeDir(), homeRelativePath(platform.globalSkillsDir));
62
187
  }
63
- return path.join(options.project, '.codex', 'skills');
188
+ return path.join(options.project, platform.projectSkillsDir);
64
189
  }
65
190
 
66
- async function listSkills() {
191
+ async function listSkills(language) {
192
+ const skillsSource = path.join(packageRoot, 'assets', language.skillsDir);
67
193
  const entries = await readdir(skillsSource, { withFileTypes: true });
68
194
  for (const entry of entries.filter((item) => item.isDirectory()).map((item) => item.name).sort()) {
69
195
  console.log(entry);
70
196
  }
71
197
  }
72
198
 
73
- async function install(options) {
74
- const target = targetSkillsDir(options);
199
+ function managedSkillDirs(currentManifest) {
200
+ return [...new Set([
201
+ ...(currentManifest.managedFiles?.localized ?? []),
202
+ ...(currentManifest.managedFiles?.shared ?? []),
203
+ ].map((file) => file.split(/[\\/]+/)[0]).filter(Boolean))].sort();
204
+ }
205
+
206
+ function localizedSourceRoot(language) {
207
+ return path.join(packageRoot, 'assets', language.skillsDir);
208
+ }
209
+
210
+ function sharedSourceRoot() {
211
+ return path.join(packageRoot, 'assets', 'skills');
212
+ }
213
+
214
+ async function exists(filePath) {
215
+ return stat(filePath).then(() => true, () => false);
216
+ }
217
+
218
+ function formatChoiceList(entries) {
219
+ return entries.map((entry, index) => `${index + 1}) ${entry.name} [${entry.id}]`).join('\n');
220
+ }
221
+
222
+ function matchChoice(entries, answer) {
223
+ const trimmed = answer.trim().toLowerCase();
224
+ if (!trimmed) return undefined;
225
+
226
+ const numeric = Number.parseInt(trimmed, 10);
227
+ if (Number.isInteger(numeric) && numeric >= 1 && numeric <= entries.length) {
228
+ return entries[numeric - 1].id;
229
+ }
230
+
231
+ return entries.find((entry) => entry.id.toLowerCase() === trimmed)?.id;
232
+ }
233
+
234
+ async function askChoice(rl, label, entries, defaultId) {
235
+ const defaultEntry = entries.find((entry) => entry.id === defaultId) ?? entries[0];
236
+ for (;;) {
237
+ const answer = await rl.question(`${label}\n${formatChoiceList(entries)}\nChoose [${defaultEntry.id}]: `);
238
+ const selected = matchChoice(entries, answer) ?? (answer.trim() ? undefined : defaultEntry.id);
239
+ if (selected) {
240
+ return selected;
241
+ }
242
+ console.log(`Please enter a number or one of: ${availableIds(entries)}`);
243
+ }
244
+ }
245
+
246
+ async function askText(rl, label, defaultValue) {
247
+ const answer = await rl.question(`${label} [${defaultValue}]: `);
248
+ return answer.trim() || defaultValue;
249
+ }
250
+
251
+ async function askYesNo(rl, label, defaultValue = false) {
252
+ const suffix = defaultValue ? 'Y/n' : 'y/N';
253
+ for (;;) {
254
+ const answer = (await rl.question(`${label} [${suffix}]: `)).trim().toLowerCase();
255
+ if (!answer) return defaultValue;
256
+ if (['y', 'yes'].includes(answer)) return true;
257
+ if (['n', 'no'].includes(answer)) return false;
258
+ console.log('Please answer y or n.');
259
+ }
260
+ }
261
+
262
+ async function readPipedInput() {
263
+ return new Promise((resolve, reject) => {
264
+ let text = '';
265
+ input.setEncoding('utf8');
266
+ input.on('data', (chunk) => {
267
+ text += chunk;
268
+ });
269
+ input.on('end', () => resolve(text));
270
+ input.on('error', reject);
271
+ input.resume();
272
+ });
273
+ }
274
+
275
+ async function createPrompter() {
276
+ if (input.isTTY) {
277
+ return createInterface({ input, output });
278
+ }
279
+
280
+ const lines = (await readPipedInput()).split(/\r?\n/);
281
+ let index = 0;
282
+ return {
283
+ async question(prompt) {
284
+ output.write(prompt);
285
+ const answer = lines[index] ?? '';
286
+ index += 1;
287
+ return answer;
288
+ },
289
+ close() {},
290
+ };
291
+ }
292
+
293
+ async function hasManagedInstall(options, currentManifest) {
294
+ const platform = resolvePlatform(options.platform);
295
+ const target = targetSkillsDir(options, platform);
296
+ for (const skillDir of managedSkillDirs(currentManifest)) {
297
+ if (await exists(path.join(target, skillDir))) {
298
+ return true;
299
+ }
300
+ }
301
+ return false;
302
+ }
303
+
304
+ async function promptInstallOptions(options, currentManifest) {
305
+ const defaults = await withInstallDefaults(options);
306
+ const rl = await createPrompter();
307
+
308
+ try {
309
+ console.log('OpenTest init\n');
310
+ defaults.platform = await askChoice(rl, 'Target AI coding tool:', manifest.platforms, defaults.platform);
311
+ defaults.language = await askChoice(rl, 'Skill language:', manifest.languages, defaults.language);
312
+ defaults.scope = await askChoice(rl, 'Install scope:', [
313
+ { id: 'project', name: 'Current project' },
314
+ { id: 'global', name: 'User global' },
315
+ ], defaults.scope);
316
+
317
+ if (defaults.scope === 'project') {
318
+ defaults.project = path.resolve(await askText(rl, 'Project root', defaults.project));
319
+ }
320
+
321
+ if (!defaults.force && await hasManagedInstall(defaults, currentManifest)) {
322
+ defaults.force = await askYesNo(rl, 'OpenTest skills already exist. Overwrite managed OpenTest directories?', false);
323
+ }
324
+
325
+ return defaults;
326
+ } finally {
327
+ rl.close();
328
+ }
329
+ }
330
+
331
+ async function copyManifestFile(sourceRoot, targetRoot, relativePath) {
332
+ const src = path.join(sourceRoot, relativePath);
333
+ const dest = path.join(targetRoot, relativePath);
334
+
335
+ if (!(await exists(src))) {
336
+ throw new Error(`Manifest-managed source file is missing: ${src}`);
337
+ }
338
+
339
+ await mkdir(path.dirname(dest), { recursive: true });
340
+ await copyFile(src, dest);
341
+ }
342
+
343
+ async function install(options, currentManifest) {
344
+ const language = resolveLanguage(options.language);
345
+ const platform = resolvePlatform(options.platform);
346
+ resolveScope(options.scope);
347
+
348
+ const target = targetSkillsDir(options, platform);
75
349
  await mkdir(target, { recursive: true });
76
350
 
77
- const entries = await readdir(skillsSource, { withFileTypes: true });
78
- const skillDirs = entries.filter((entry) => entry.isDirectory() && entry.name.startsWith('opentest'));
351
+ const skillDirs = managedSkillDirs(currentManifest);
79
352
 
80
- for (const entry of skillDirs) {
81
- const src = path.join(skillsSource, entry.name);
82
- const dest = path.join(target, entry.name);
83
- const exists = await stat(dest).then(() => true, () => false);
353
+ for (const skillDir of skillDirs) {
354
+ const dest = path.join(target, skillDir);
355
+ const directoryExists = await exists(dest);
84
356
 
85
- if (exists && !options.force) {
357
+ if (directoryExists && !options.force) {
86
358
  throw new Error(`${dest} already exists. Re-run with --force to overwrite.`);
87
359
  }
360
+ }
88
361
 
89
- if (exists) {
90
- await rm(dest, { recursive: true, force: true });
362
+ if (options.force) {
363
+ for (const skillDir of skillDirs) {
364
+ await rm(path.join(target, skillDir), { recursive: true, force: true });
91
365
  }
92
- await cp(src, dest, { recursive: true });
366
+ }
367
+
368
+ for (const file of currentManifest.managedFiles?.localized ?? []) {
369
+ await copyManifestFile(localizedSourceRoot(language), target, file);
370
+ }
371
+ for (const file of currentManifest.managedFiles?.shared ?? []) {
372
+ await copyManifestFile(sharedSourceRoot(), target, file);
93
373
  }
94
374
 
95
375
  console.log(`Installed ${skillDirs.length} OpenTest skills to ${target}`);
96
- console.log('Restart Codex or open a new session to pick up newly installed skills.');
376
+ console.log(`Platform: ${platform.name}`);
377
+ console.log(`Language: ${language.name}`);
378
+ console.log('Restart your AI coding tool or open a new session to pick up newly installed skills.');
97
379
  }
98
380
 
99
381
  async function main() {
@@ -104,12 +386,19 @@ async function main() {
104
386
  }
105
387
 
106
388
  if (options.command === 'list') {
107
- await listSkills();
389
+ const language = resolveLanguage(options.language ?? detectLanguage());
390
+ await listSkills(language);
391
+ return;
392
+ }
393
+
394
+ if (options.command === 'init' || (options.command === 'install' && !options.explicitTarget && process.stdin.isTTY)) {
395
+ const promptedOptions = await promptInstallOptions(options, manifest);
396
+ await install(promptedOptions, manifest);
108
397
  return;
109
398
  }
110
399
 
111
400
  if (options.command === 'install') {
112
- await install(options);
401
+ await install(await withInstallDefaults(options), manifest);
113
402
  return;
114
403
  }
115
404
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pzy560117/opentest",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "OpenTest quality evidence lifecycle skills for Codex",
5
5
  "keywords": [
6
6
  "opentest",
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync, readdirSync, statSync } from 'fs';
4
- import { extname, join } from 'path';
3
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
4
+ import { basename, extname, join } from 'path';
5
+ import { TextDecoder } from 'util';
5
6
 
6
7
  const SECRET_PATTERNS = [
7
8
  { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][A-Za-z0-9_-]{20,}['"]/i, name: 'API key' },
@@ -14,7 +15,52 @@ const SECRET_PATTERNS = [
14
15
  ];
15
16
 
16
17
  const SKIP_DIRS = new Set(['node_modules', '.git']);
17
- const TEXT_EXTENSIONS = new Set(['.js', '.json', '.md', '.txt', '.yml', '.yaml', '.toml', '.sh']);
18
+ const TEXT_EXTENSIONS = new Set([
19
+ '.js',
20
+ '.json',
21
+ '.md',
22
+ '.txt',
23
+ '.yml',
24
+ '.yaml',
25
+ '.toml',
26
+ '.sh',
27
+ '.env',
28
+ '.pem',
29
+ '.key',
30
+ '.crt',
31
+ '.cer',
32
+ ]);
33
+ const DOT_TEXT_FILENAMES = new Set(['.env']);
34
+ const MAX_TEXT_SCAN_BYTES = 1024 * 1024;
35
+ const MAX_EXTENSIONLESS_TEXT_SCAN_BYTES = 64 * 1024;
36
+ const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
37
+
38
+ function assertManifest(condition, message) {
39
+ if (!condition) {
40
+ console.error(`[MANIFEST] ${message}`);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ function loadManifest() {
46
+ try {
47
+ return JSON.parse(readFileSync('assets/manifest.json', 'utf8'));
48
+ } catch (error) {
49
+ console.error(`[MANIFEST] failed to load assets/manifest.json: ${error.message}`);
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ function validateManifestForPublish(manifest) {
55
+ assertManifest(Array.isArray(manifest.languages), 'languages must be an array');
56
+ assertManifest(
57
+ manifest.languages.some((entry) => entry.id === 'zh'),
58
+ 'languages must include zh',
59
+ );
60
+ assertManifest(Array.isArray(manifest.managedFiles?.localized), 'managedFiles.localized must exist');
61
+ assertManifest(Array.isArray(manifest.managedFiles?.shared), 'managedFiles.shared must exist');
62
+ assertManifest(existsSync('assets/skills-zh') && statSync('assets/skills-zh').isDirectory(), 'assets/skills-zh must exist');
63
+ }
18
64
 
19
65
  function* walkFiles(dir) {
20
66
  for (const entry of readdirSync(dir)) {
@@ -30,17 +76,70 @@ function* walkFiles(dir) {
30
76
  }
31
77
  }
32
78
 
79
+ function scanByteLimit(filePath, fileStat) {
80
+ const filename = basename(filePath).toLowerCase();
81
+ const extension = extname(filename);
82
+
83
+ if (TEXT_EXTENSIONS.has(extension) || DOT_TEXT_FILENAMES.has(filename)) {
84
+ return MAX_TEXT_SCAN_BYTES;
85
+ }
86
+
87
+ if (extension === '' && fileStat.size <= MAX_EXTENSIONLESS_TEXT_SCAN_BYTES) {
88
+ return MAX_EXTENSIONLESS_TEXT_SCAN_BYTES;
89
+ }
90
+
91
+ return 0;
92
+ }
93
+
94
+ function looksLikeText(buffer) {
95
+ if (buffer.length === 0) {
96
+ return true;
97
+ }
98
+
99
+ let controlBytes = 0;
100
+ for (const byte of buffer) {
101
+ if (byte === 0) {
102
+ return false;
103
+ }
104
+ if (byte < 9 || (byte > 13 && byte < 32)) {
105
+ controlBytes += 1;
106
+ }
107
+ }
108
+
109
+ return controlBytes / buffer.length < 0.05;
110
+ }
111
+
112
+ function readScannableText(filePath) {
113
+ const fileStat = statSync(filePath);
114
+ const byteLimit = scanByteLimit(filePath, fileStat);
115
+ if (byteLimit === 0 || fileStat.size > byteLimit) {
116
+ return undefined;
117
+ }
118
+
119
+ const content = readFileSync(filePath);
120
+ if (!looksLikeText(content)) {
121
+ return undefined;
122
+ }
123
+
124
+ try {
125
+ return utf8Decoder.decode(content);
126
+ } catch {
127
+ return undefined;
128
+ }
129
+ }
130
+
131
+ validateManifestForPublish(loadManifest());
132
+
33
133
  let found = 0;
34
134
 
35
135
  for (const filePath of walkFiles('.')) {
36
- if (!TEXT_EXTENSIONS.has(extname(filePath))) continue;
37
-
38
136
  let content;
39
137
  try {
40
- content = readFileSync(filePath, 'utf8');
138
+ content = readScannableText(filePath);
41
139
  } catch {
42
140
  continue;
43
141
  }
142
+ if (content === undefined) continue;
44
143
 
45
144
  for (const { pattern, name } of SECRET_PATTERNS) {
46
145
  if (pattern.test(content)) {