From 2d638e3a0af5e995a6d76b017763f182766f80dd Mon Sep 17 00:00:00 2001 From: jackwener Date: Sun, 15 Mar 2026 18:59:15 +0800 Subject: [PATCH] fix(manifest): extract TS adapter args at build time for proper CLI registration The manifest builder now statically parses TS source files to extract args, description, columns, domain, and strategy metadata. Previously TS adapters had empty args in the manifest, causing commander to reject their options (e.g. --keyword) before the lazy module could load. Also includes the xiaohongshu search DOM fallback fix from earlier. Co-authored-by: jack4world --- src/build-manifest.ts | 65 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/src/build-manifest.ts b/src/build-manifest.ts index 3b2e565..edf9e69 100644 --- a/src/build-manifest.ts +++ b/src/build-manifest.ts @@ -87,10 +87,11 @@ function scanYaml(filePath: string, site: string): ManifestEntry | null { function scanTs(filePath: string, site: string): ManifestEntry { // TS adapters self-register via cli() at import time. - // We record their module path for lazy dynamic import. + // We statically parse the source to extract metadata for the manifest stub. const baseName = path.basename(filePath, path.extname(filePath)); const relativePath = `${site}/${baseName}.js`; - return { + + const entry: ManifestEntry = { site, name: baseName, description: '', @@ -100,6 +101,66 @@ function scanTs(filePath: string, site: string): ManifestEntry { type: 'ts', modulePath: relativePath, }; + + try { + const src = fs.readFileSync(filePath, 'utf-8'); + + // Extract description + const descMatch = src.match(/description\s*:\s*['"`]([^'"`]*)['"`]/); + if (descMatch) entry.description = descMatch[1]; + + // Extract domain + const domainMatch = src.match(/domain\s*:\s*['"`]([^'"`]*)['"`]/); + if (domainMatch) entry.domain = domainMatch[1]; + + // Extract strategy + const stratMatch = src.match(/strategy\s*:\s*Strategy\.(\w+)/); + if (stratMatch) entry.strategy = stratMatch[1].toLowerCase(); + + // Extract columns + const colMatch = src.match(/columns\s*:\s*\[([^\]]*)\]/); + if (colMatch) { + entry.columns = colMatch[1].split(',').map(s => s.trim().replace(/^['"`]|['"`]$/g, '')).filter(Boolean); + } + + // Extract args array items: { name: '...', ... } + const argsBlockMatch = src.match(/args\s*:\s*\[([\s\S]*?)\]\s*,/); + if (argsBlockMatch) { + const argsBlock = argsBlockMatch[1]; + const argRegex = /\{\s*name\s*:\s*['"`](\w+)['"`]([^}]*)\}/g; + let m; + while ((m = argRegex.exec(argsBlock)) !== null) { + const argName = m[1]; + const body = m[2]; + const typeMatch = body.match(/type\s*:\s*['"`](\w+)['"`]/); + const defaultMatch = body.match(/default\s*:\s*([^,}]+)/); + const requiredMatch = body.match(/required\s*:\s*(true|false)/); + const helpMatch = body.match(/help\s*:\s*['"`]([^'"`]*)['"`]/); + + let defaultVal: any = undefined; + if (defaultMatch) { + const raw = defaultMatch[1].trim(); + if (raw === 'true') defaultVal = true; + else if (raw === 'false') defaultVal = false; + else if (/^\d+$/.test(raw)) defaultVal = parseInt(raw, 10); + else if (/^\d+\.\d+$/.test(raw)) defaultVal = parseFloat(raw); + else defaultVal = raw.replace(/^['"`]|['"`]$/g, ''); + } + + entry.args.push({ + name: argName, + type: typeMatch?.[1] ?? 'str', + default: defaultVal, + required: requiredMatch?.[1] === 'true', + help: helpMatch?.[1] ?? '', + }); + } + } + } catch { + // If parsing fails, fall back to empty metadata — module will self-register at runtime + } + + return entry; } // Main