@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.
- package/README.md +57 -8
- package/assets/manifest.json +84 -27
- package/assets/skills/opentest/SKILL.md +44 -44
- package/assets/skills/opentest/references/codex-harness-coverage-heuristics.md +55 -55
- package/assets/skills/opentest/references/lifecycle.md +1 -1
- package/assets/skills/opentest/references/matrix-format.md +14 -14
- package/assets/skills/opentest/references/opentest-driven-development.md +32 -32
- package/assets/skills/opentest/templates/acceptance-template.md +1 -1
- package/assets/skills/opentest/templates/matrix-template.md +3 -3
- package/assets/skills/opentest/templates/plan-template.md +17 -17
- package/assets/skills/opentest/templates/report-template.md +1 -1
- package/assets/skills/opentest-accept/SKILL.md +13 -13
- package/assets/skills/opentest-archive/SKILL.md +2 -2
- package/assets/skills/opentest-author/SKILL.md +14 -14
- package/assets/skills/opentest-heal/SKILL.md +2 -2
- package/assets/skills/opentest-plan/SKILL.md +17 -17
- package/assets/skills/opentest-run/SKILL.md +16 -16
- package/assets/skills/opentest-verify/SKILL.md +11 -11
- package/assets/skills-zh/opentest/SKILL.md +93 -0
- package/assets/skills-zh/opentest/references/acceptance-evidence.md +27 -0
- package/assets/skills-zh/opentest/references/codex-harness-coverage-heuristics.md +83 -0
- package/assets/skills-zh/opentest/references/command-routing.md +9 -0
- package/assets/skills-zh/opentest/references/lifecycle.md +16 -0
- package/assets/skills-zh/opentest/references/matrix-format.md +27 -0
- package/assets/skills-zh/opentest/references/opentest-driven-development.md +48 -0
- package/assets/skills-zh/opentest/references/quality-gate.md +24 -0
- package/assets/skills-zh/opentest/templates/acceptance-template.md +32 -0
- package/assets/skills-zh/opentest/templates/archive-layout.md +14 -0
- package/assets/skills-zh/opentest/templates/matrix-template.md +6 -0
- package/assets/skills-zh/opentest/templates/plan-template.md +28 -0
- package/assets/skills-zh/opentest/templates/report-template.md +28 -0
- package/assets/skills-zh/opentest-accept/SKILL.md +25 -0
- package/assets/skills-zh/opentest-archive/SKILL.md +8 -0
- package/assets/skills-zh/opentest-author/SKILL.md +27 -0
- package/assets/skills-zh/opentest-heal/SKILL.md +8 -0
- package/assets/skills-zh/opentest-plan/SKILL.md +30 -0
- package/assets/skills-zh/opentest-run/SKILL.md +28 -0
- package/assets/skills-zh/opentest-verify/SKILL.md +24 -0
- package/bin/opentest.js +318 -29
- package/package.json +1 -1
- package/scripts/prepublish-check.js +105 -6
- 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 {
|
|
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
|
|
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
|
|
17
|
-
opentest
|
|
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
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
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 = {
|
|
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.
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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,
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
78
|
-
const skillDirs = entries.filter((entry) => entry.isDirectory() && entry.name.startsWith('opentest'));
|
|
351
|
+
const skillDirs = managedSkillDirs(currentManifest);
|
|
79
352
|
|
|
80
|
-
for (const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
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 (
|
|
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
|
-
|
|
90
|
-
|
|
362
|
+
if (options.force) {
|
|
363
|
+
for (const skillDir of skillDirs) {
|
|
364
|
+
await rm(path.join(target, skillDir), { recursive: true, force: true });
|
|
91
365
|
}
|
|
92
|
-
|
|
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(
|
|
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
|
-
|
|
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,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([
|
|
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 =
|
|
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)) {
|