mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
80eef46b4e
* refactor: move adapters from src/clis/ to root clis/ for monorepo separation Separates CLI adapters from the core runtime to prepare for independent adapter distribution via postinstall fetch. Key changes: - Move src/clis/ → clis/ (adapters at repo root) - Change tsconfig rootDir from "src" to "." so tsc compiles both - Create root-level shim files (registry.ts, errors.ts, etc.) so adapter relative imports (../../registry.js) resolve correctly - Update build-manifest.ts, main.ts paths for new dist/src/ structure - Expand ensureUserCliCompatShims() to cover all adapter import targets (types, utils, logger, launcher, browser/*, download/*, pipeline/*) - Add scripts/fetch-adapters.js postinstall for ~/.opencli/clis/ sync - Update vitest.config.ts adapter test paths - Add package.json files field to exclude adapters from npm package Official adapter files are unconditionally overwritten on update; user-created files not in the manifest are preserved. * fix: add dist/clis/ and cli-manifest.json to npm files, harden fetch-adapters - Add dist/clis/ and dist/cli-manifest.json to package.json files field so built-in adapters and manifest ship with the npm package - Replace execSync with execFileSync to prevent command injection - Add version check to skip redundant adapter fetches - Track tmpRoot explicitly for reliable cleanup * fix: address review blockers — manifest-based updates, global-only fetch, first-run fallback 1. Manifest-based update strategy: - Read old manifest to identify previously-official files - Clean up files removed upstream (in old manifest but not new) - User-created files (never in any manifest) remain untouched 2. Only run fetch-adapters on global install (npm_config_global=true) or explicit OPENCLI_FETCH=1, preventing heavy side effects for local/dev installs 3. First-run fallback in discovery.ts: - ensureUserAdapters() checks for adapter-manifest.json - If missing and ~/.opencli/clis/ is empty, spawns fetch-adapters.js - Guarantees adapters are available even with --ignore-scripts * fix: remove OPENCLI_FETCH env var, use internal _OPENCLI_FIRST_RUN instead * feat: also support OPENCLI_FETCH=1 for explicit adapter fetch trigger * simplify: replace git clone with local copy from dist/clis/ Adapters already ship in the npm package (dist/clis/), so there's no need to clone from GitHub. Copy directly from the installed package: - Eliminates git, curl, tar dependencies - No network calls in postinstall - No timeout/offline issues - Version always matches the installed CLI - ~65 lines of clone/download code replaced by one cpSync loop
91 lines
3.5 KiB
TypeScript
91 lines
3.5 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||
|
||
const SAMPLE_RSS = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0"><channel><title>36氪</title>
|
||
<item>
|
||
<title>红杉中国领投AI公司「示例」,金额近2亿元</title>
|
||
<link><![CDATA[https://36kr.com/p/1111111111111111?f=rss]]></link>
|
||
<pubDate>2026-03-26 10:00:00 +0800</pubDate>
|
||
</item>
|
||
<item>
|
||
<title>马斯克旗下xAI估值突破1000亿美元</title>
|
||
<link><![CDATA[https://36kr.com/p/2222222222222222?f=rss]]></link>
|
||
<pubDate>2026-03-26 09:00:00 +0800</pubDate>
|
||
</item>
|
||
<item>
|
||
<title>OpenAI发布GPT-5,多模态能力大幅提升</title>
|
||
<link><![CDATA[https://36kr.com/p/3333333333333333?f=rss]]></link>
|
||
<pubDate>2026-03-25 20:00:00 +0800</pubDate>
|
||
</item>
|
||
</channel></rss>`;
|
||
|
||
afterEach(() => {
|
||
vi.restoreAllMocks();
|
||
});
|
||
|
||
describe('36kr/news RSS parsing', () => {
|
||
it('parses RSS feed into ranked news items', async () => {
|
||
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||
ok: true,
|
||
text: async () => SAMPLE_RSS,
|
||
} as Response);
|
||
|
||
// Direct RSS parse test using the same regex logic as news.ts
|
||
const xml = SAMPLE_RSS;
|
||
const items: { rank: number; title: string; date: string; url: string }[] = [];
|
||
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
||
let match;
|
||
while ((match = itemRegex.exec(xml)) && items.length < 10) {
|
||
const block = match[1];
|
||
const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? '';
|
||
const url =
|
||
block.match(/<link><!\[CDATA\[(.*?)\]\]>/)?.[1] ??
|
||
block.match(/<link>(.*?)<\/link>/)?.[1] ??
|
||
'';
|
||
const pubDate = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim() ?? '';
|
||
const date = pubDate.slice(0, 10);
|
||
if (title) items.push({ rank: items.length + 1, title, date, url: url.trim() });
|
||
}
|
||
|
||
expect(items).toHaveLength(3);
|
||
expect(items[0].rank).toBe(1);
|
||
expect(items[0].title).toBe('红杉中国领投AI公司「示例」,金额近2亿元');
|
||
expect(items[0].date).toBe('2026-03-26');
|
||
expect(items[0].url).toBe('https://36kr.com/p/1111111111111111?f=rss');
|
||
});
|
||
|
||
it('respects limit — returns at most N items', async () => {
|
||
const xml = SAMPLE_RSS;
|
||
const limit = 2;
|
||
const items: { rank: number; title: string; date: string; url: string }[] = [];
|
||
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
||
let match;
|
||
while ((match = itemRegex.exec(xml)) && items.length < limit) {
|
||
const block = match[1];
|
||
const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? '';
|
||
const url = block.match(/<link><!\[CDATA\[(.*?)\]\]>/)?.[1] ?? '';
|
||
const pubDate = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim() ?? '';
|
||
const date = pubDate.slice(0, 10);
|
||
if (title) items.push({ rank: items.length + 1, title, date, url: url.trim() });
|
||
}
|
||
expect(items).toHaveLength(2);
|
||
});
|
||
|
||
it('skips items with empty title', async () => {
|
||
const xml = `<rss><channel>
|
||
<item><title></title><link>https://36kr.com/p/0</link><pubDate>2026-01-01</pubDate></item>
|
||
<item><title>有标题的文章</title><link>https://36kr.com/p/1</link><pubDate>2026-01-01</pubDate></item>
|
||
</channel></rss>`;
|
||
const items: any[] = [];
|
||
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
||
let match;
|
||
while ((match = itemRegex.exec(xml))) {
|
||
const block = match[1];
|
||
const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? '';
|
||
if (title) items.push({ title });
|
||
}
|
||
expect(items).toHaveLength(1);
|
||
expect(items[0].title).toBe('有标题的文章');
|
||
});
|
||
});
|