mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
34a9bff2b3
- 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
78 lines
3.2 KiB
TypeScript
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');
|
|
}
|