Files
jackwener__opencli/src/validate.ts
T
jackwener 34a9bff2b3 refactor: eliminate code duplication, improve type safety, add tests
- NEW: src/interceptor.ts — unified XHR/Fetch interceptor (was duplicated 3x)
- NEW: src/version.ts — centralized PKG_VERSION (was duplicated 2x)
- NEW: src/constants.ts — shared VOLATILE_PARAMS, FIELD_ROLES etc.
- NEW: src/engine.test.ts, src/registry.test.ts — 14 new unit tests

- browser.ts: use shared normalizeEval, interceptor, withTimeoutMs, PKG_VERSION
- intercept.ts, tap.ts: use shared interceptor generators
- cascade.ts: extract shared buildFetchProbeJs (90% dedup)
- engine.ts: use InternalCliCommand (no more 'as any' casts)
- executor.ts: remove all 15 'as StepHandler' type casts
- runtime.ts: add withTimeoutMs, IBrowserFactory interface
- registry.ts: add InternalCliCommand type for internal fields
- validate.ts: add pipeline step name validation
- output.ts: remove 'null as any' and '.filter(() => true)' hacks
- explore.ts, synthesize.ts: use shared constants
- docs: fix V2EX commands (3→6), SKILL.md version, verify example

Tests: 88 passed (was 74), tsc --noEmit: 0 errors
2026-03-16 13:22:12 +08:00

78 lines
3.2 KiB
TypeScript

/** Validate CLI definitions. */
import * as fs from 'node:fs';
import * as path from 'node:path';
import yaml from 'js-yaml';
/** All recognized pipeline step names */
const KNOWN_STEP_NAMES = new Set([
'navigate', 'click', 'type', 'wait', 'press', 'snapshot', 'scroll',
'fetch', 'evaluate',
'select', 'map', 'filter', 'sort', 'limit',
'intercept', 'tap',
]);
export function validateClisWithTarget(dirs: string[], target?: string): any {
const results: any[] = [];
let errors = 0; let warnings = 0; let files = 0;
for (const dir of dirs) {
if (!fs.existsSync(dir)) continue;
for (const site of fs.readdirSync(dir)) {
if (target && site !== target && !target.startsWith(site + '/')) continue;
const siteDir = path.join(dir, site);
if (!fs.statSync(siteDir).isDirectory()) continue;
for (const file of fs.readdirSync(siteDir)) {
if (!file.endsWith('.yaml') && !file.endsWith('.yml')) continue;
if (target && target.includes('/') && !target.endsWith(file.replace(/\.(yaml|yml)$/, ''))) continue;
files++;
const filePath = path.join(siteDir, file);
const r = validateYamlFile(filePath);
results.push(r);
errors += r.errors.length;
warnings += r.warnings.length;
}
}
}
return { ok: errors === 0, results, errors, warnings, files };
}
function validateYamlFile(filePath: string): any {
const errors: string[] = []; const warnings: string[] = [];
try {
const raw = fs.readFileSync(filePath, 'utf-8');
const def = yaml.load(raw) as any;
if (!def || typeof def !== 'object') { errors.push('Not a valid YAML object'); return { path: filePath, errors, warnings }; }
if (!def.site) errors.push('Missing "site"');
if (!def.name) errors.push('Missing "name"');
if (def.pipeline && !Array.isArray(def.pipeline)) errors.push('"pipeline" must be an array');
if (def.columns && !Array.isArray(def.columns)) errors.push('"columns" must be an array');
if (def.args && typeof def.args !== 'object') errors.push('"args" must be an object');
// Validate pipeline step names (catch typos like 'navaigate')
if (Array.isArray(def.pipeline)) {
for (let i = 0; i < def.pipeline.length; i++) {
const step = def.pipeline[i];
if (step && typeof step === 'object') {
const stepKeys = Object.keys(step);
for (const key of stepKeys) {
if (!KNOWN_STEP_NAMES.has(key)) {
warnings.push(`Pipeline step ${i}: unknown step name "${key}" (did you mean one of: ${[...KNOWN_STEP_NAMES].join(', ')}?)`);
}
}
}
}
}
} catch (e: any) { errors.push(`YAML parse error: ${e.message}`); }
return { path: filePath, errors, warnings };
}
export function renderValidationReport(report: any): string {
const lines = [`opencli validate: ${report.ok ? 'PASS' : 'FAIL'}`, `Checked ${report.results.length} CLI(s) in ${report.files} file(s)`, `Errors: ${report.errors} Warnings: ${report.warnings}`];
for (const r of report.results) {
if (r.errors.length > 0 || r.warnings.length > 0) {
lines.push(`\n${r.path}:`);
for (const e of r.errors) lines.push(` ❌ ${e}`);
for (const w of r.warnings) lines.push(` ⚠️ ${w}`);
}
}
return lines.join('\n');
}