From d08edf0cd83ff22d0832a0cddfd47edd323ba78a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=95=85=E7=92=83?= Date: Fri, 17 Jul 2026 16:43:06 +0800 Subject: [PATCH] feat: sync wiki data from oss by fc --- packages/cli/package.json | 9 +- packages/cli/postinstall.js | 160 +++++++++++++ .../src/commands/advisor/recommend.ts | 7 + packages/core/package.json | 2 + packages/core/src/advisor/index.ts | 1 + packages/core/src/advisor/sources/catalog.ts | 41 +--- packages/core/src/advisor/sync.ts | 226 ++++++++++++++++++ pnpm-lock.yaml | 157 ++++++++++++ pnpm-workspace.yaml | 2 + 9 files changed, 571 insertions(+), 34 deletions(-) create mode 100644 packages/cli/postinstall.js create mode 100644 packages/core/src/advisor/sync.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 1e5701e..e7b712c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -25,7 +25,8 @@ }, "files": [ "dist", - "README.zh.md" + "README.zh.md", + "postinstall.js" ], "type": "module", "exports": { @@ -45,12 +46,14 @@ "build": "vp pack", "dev": "tsx src/main.ts", "test": "vp test", - "check": "vp check" + "check": "vp check", + "postinstall": "node postinstall.js" }, "dependencies": { "bailian-cli-commands": "workspace:*", "bailian-cli-core": "workspace:*", - "bailian-cli-runtime": "workspace:*" + "bailian-cli-runtime": "workspace:*", + "tar-stream": "catalog:" }, "devDependencies": { "@clack/prompts": "^0.7.0", diff --git a/packages/cli/postinstall.js b/packages/cli/postinstall.js new file mode 100644 index 0000000..9210efc --- /dev/null +++ b/packages/cli/postinstall.js @@ -0,0 +1,160 @@ +/** + * postinstall.js —— Wiki 数据同步(第一层:npm install 触发) + * + * npm/pnpm 装完 bailian-cli 后自动执行:无条件下载全量 Wiki 数据包并覆盖本地目录, + * 保证用户首次使用 `bl advisor recommend` 时数据已就位。 + * + * 流程: + * 1. fetch FC 签名函数 → 拿 OSS 预签名 URL + * 2. 下载 manifest.json + wiki-doc-full.tar.br(~2.15MB) + * 3. 校验 sha256 + * 4. Node 原生 brotli 解压 + tar-stream 解包到同盘临时目录 + * 5. renameSync 原子替换到 ~/.bailian/skills/bailian-docs-llm-wiki/ + * 6. 写 ~/.bailian/wiki-sync-state.json + * + * 设计约束: + * - 无条件覆盖:每次 install 都全量替换,不比对已有版本 + * - 失败静默:任何一步失败 → console.warn → process.exit(0),绝不阻塞安装 + * - 独立实现:不 import bailian-cli-core,避免打包后 ESM 路径问题 + * - 依赖 Node 原生模块 + tar-stream(与 sync.ts / Crawler oss-upload.mjs 一致) + */ +import { createHash } from "node:crypto"; +import { + createWriteStream, + existsSync, + mkdirSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import { createBrotliDecompress } from "node:zlib"; +import tar from "tar-stream"; + +const FC_SIGN_URL = "https://signature-gmqkxxozrl.cn-hangzhou.fcapp.run"; +const CONFIG_DIR_NAME = ".bailian"; +const SKILL_DIR_NAME = "skills/bailian-docs-llm-wiki"; +const STATE_FILE_NAME = "wiki-sync-state.json"; +const MANIFEST_KEY = "manifest.json"; +const ASSET_KEY = "wiki-doc-full.tar.br"; + +const MANIFEST_TIMEOUT_MS = 3000; +const DOWNLOAD_TIMEOUT_MS = 30000; + +function getConfigDir() { + if (process.env.BAILIAN_CONFIG_DIR) return process.env.BAILIAN_CONFIG_DIR; + return join(homedir(), CONFIG_DIR_NAME); +} + +function getCatalogDir() { + return join(getConfigDir(), SKILL_DIR_NAME); +} + +function getStatePath() { + return join(getConfigDir(), STATE_FILE_NAME); +} + +async function fetchJson(url, timeoutMs) { + const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + +async function downloadBuffer(url) { + const res = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return Buffer.from(await res.arrayBuffer()); +} + +/** brotli 解压 + tar-stream 解包到 destDir(与 Crawler tar.pack() 对称)。 */ +async function extractTarBr(tarBrBuffer, destDir) { + const extract = tar.extract(); + + extract.on("entry", (header, stream, next) => { + const filePath = join(destDir, header.name); + if (header.type === "directory") { + mkdirSync(filePath, { recursive: true }); + stream.resume(); + stream.on("end", next); + return; + } + mkdirSync(dirname(filePath), { recursive: true }); + const ws = createWriteStream(filePath); + stream.pipe(ws); + ws.on("finish", next); + ws.on("error", next); + }); + + await pipeline(Readable.from(tarBrBuffer), createBrotliDecompress(), extract); +} + +/** 原子替换:tmpDir(同盘)→ catalogDir。 */ +function atomicSwap(tmpDir, catalogDir) { + mkdirSync(dirname(catalogDir), { recursive: true }); + const backup = `${catalogDir}.old-${Date.now()}`; + if (existsSync(catalogDir)) renameSync(catalogDir, backup); + try { + renameSync(tmpDir, catalogDir); + } catch (err) { + if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir); + throw err; + } + if (existsSync(backup)) rmSync(backup, { recursive: true, force: true }); +} + +async function main() { + // 1. 拿全部签名 URL + const signResp = await fetchJson(FC_SIGN_URL, MANIFEST_TIMEOUT_MS); + const urls = signResp?.urls; + if (!urls?.[MANIFEST_KEY] || !urls?.[ASSET_KEY]) { + throw new Error("签名函数未返回所需 URL"); + } + + // 2. 下载 manifest + const manifest = await fetchJson(urls[MANIFEST_KEY], MANIFEST_TIMEOUT_MS); + if (!manifest?.version) throw new Error("manifest 无 version"); + + // 3. 下载 tar.br + 校验 + const tarBuf = await downloadBuffer(urls[ASSET_KEY]); + const sha256 = createHash("sha256").update(tarBuf).digest("hex"); + if (manifest.asset?.sha256 && sha256 !== manifest.asset.sha256) { + throw new Error("sha256 校验失败"); + } + + // 4. 解包到同盘临时目录 + 原子替换 + const catalogDir = getCatalogDir(); + const tmpDir = `${catalogDir}.tmp-${process.pid}-${Date.now()}`; + try { + mkdirSync(tmpDir, { recursive: true }); + await extractTarBr(tarBuf, tmpDir); + atomicSwap(tmpDir, catalogDir); + } catch (err) { + if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true }); + throw err; + } + + // 5. 写 state + try { + writeFileSync( + getStatePath(), + JSON.stringify({ lastChecked: Date.now(), version: manifest.version }), + ); + } catch { + /* state 写失败不影响:首次 recommend 会重新检查 */ + } + + process.stdout.write(`bailian-cli: wiki 数据已就绪 (v${manifest.version})\n`); +} + +main().catch((err) => { + // 无条件放行:安装期网络/权限问题不应阻塞 npm install, + // 首次 `bl advisor recommend` 时 sync.ts 会兜底同步。 + const msg = err instanceof Error ? err.message : String(err); + process.stderr.write(`bailian-cli: wiki 数据预下载跳过 (${msg}),首次使用时将自动同步。\n`); + // Force a success exit code so a download failure never fails `npm install`. + // eslint-disable-next-line unicorn/no-process-exit + process.exit(0); +}); diff --git a/packages/commands/src/commands/advisor/recommend.ts b/packages/commands/src/commands/advisor/recommend.ts index c848a51..c328b06 100644 --- a/packages/commands/src/commands/advisor/recommend.ts +++ b/packages/commands/src/commands/advisor/recommend.ts @@ -6,6 +6,7 @@ import { type GetModelsOptions, getModels, type IntentProfile, + maybeSyncWikiData, type PipelineStep, type RecommendedModel, type RecommendResult, @@ -248,6 +249,12 @@ export default defineCommand({ const { settings, flags } = ctx; const userInput = flags.message; const top = 3; + + // Keep the local wiki catalog fresh: throttled (12h) version check against + // the remote manifest, silently replaces data when a newer version exists. + // Never throws — a sync failure must not block recommendation. + await maybeSyncWikiData(); + // Default to JSON for structured output; render boxen cards only when the // user explicitly asked for text output. const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json"; diff --git a/packages/core/package.json b/packages/core/package.json index 85f8b5a..0030cdc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,11 +40,13 @@ "check": "vp check" }, "dependencies": { + "tar-stream": "catalog:", "yaml": "^2.8.3", "yauzl": "catalog:" }, "devDependencies": { "@types/node": "catalog:", + "@types/tar-stream": "catalog:", "@types/yauzl": "catalog:", "@typescript/native-preview": "7.0.0-dev.20260328.1", "typescript": "^6.0.2", diff --git a/packages/core/src/advisor/index.ts b/packages/core/src/advisor/index.ts index 78e85ec..1d78f0b 100644 --- a/packages/core/src/advisor/index.ts +++ b/packages/core/src/advisor/index.ts @@ -7,6 +7,7 @@ export { recallCandidates } from "./recall.ts"; export { recallSemantic, isSemanticAvailable } from "./recall-semantic.ts"; export type { RecommendOptions } from "./recommend.ts"; export { buildDocLink, rankModels } from "./recommend.ts"; +export { maybeSyncWikiData } from "./sync.ts"; export type { ModelSource } from "./sources/types.ts"; export type { Budget, diff --git a/packages/core/src/advisor/sources/catalog.ts b/packages/core/src/advisor/sources/catalog.ts index 7a90fa5..723c757 100644 --- a/packages/core/src/advisor/sources/catalog.ts +++ b/packages/core/src/advisor/sources/catalog.ts @@ -1,6 +1,5 @@ -import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; import { getConfigDir } from "../../config/paths.ts"; import type { ModelPrice, ModelProfile, QpmLimit } from "../types.ts"; import type { ModelSource } from "./types.ts"; @@ -13,12 +12,9 @@ function getCatalogDir(): string { } function getCatalogPath(): string { - return join(getCatalogDir(), MODELS_FILE); -} - -function getMonorepoModelsDir(): string { - const coreDir = dirname(fileURLToPath(import.meta.url)); - return join(coreDir, "../../../../../skills/bailian-docs-llm-wiki/models"); + // Full-package layout keeps the `models/` subdir (raw/, wiki/, models/, …), + // so models.jsonl lives at /models/models.jsonl — not at the skill root. + return join(getCatalogDir(), "models", MODELS_FILE); } function fromJsonlRecord(raw: Record): ModelProfile | null { @@ -62,41 +58,24 @@ function readJsonlModels(filePath: string): ModelProfile[] { return models; } -function installFromMonorepo(): boolean { - const src = getMonorepoModelsDir(); - if (!existsSync(join(src, MODELS_FILE))) return false; - const dest = getCatalogDir(); - try { - mkdirSync(dest, { recursive: true }); - cpSync(src, dest, { recursive: true }); - return true; - } catch { - return false; - } -} - export interface CatalogSourceOptions { onPrepareStart?: () => void; } export class CatalogSource implements ModelSource { readonly name = "catalog"; - private options: CatalogSourceOptions; - constructor(options?: CatalogSourceOptions) { - this.options = options ?? {}; - } + // Options retained for API compatibility. Data is now always provisioned by + // the CLI postinstall hook and refreshed by advisor sync, so the previous + // `onPrepareStart` install callback is obsolete. + constructor(_options?: CatalogSourceOptions) {} available(): boolean { return existsSync(getCatalogPath()); } async load(): Promise { - if (!this.available()) { - this.options.onPrepareStart?.(); - const installed = installFromMonorepo(); - if (!installed) return []; - } + if (!this.available()) return []; return readJsonlModels(getCatalogPath()); } } diff --git a/packages/core/src/advisor/sync.ts b/packages/core/src/advisor/sync.ts new file mode 100644 index 0000000..382b8c9 --- /dev/null +++ b/packages/core/src/advisor/sync.ts @@ -0,0 +1,226 @@ +/** + * sync.ts —— Wiki 数据同步(第二层:recommend 触发) + * + * `bl advisor recommend` 执行时调用 `maybeSyncWikiData()`: + * 1. 12h throttle:距上次检查不足 12h 直接跳过 + * 2. 拉 FC 签名 URL → 下载 manifest.json 比对版本 + * 3. 版本相同 → 仅刷新 lastChecked + * 4. 版本不同 → 下载 tar.br → 校验 sha256 → brotli 解压 + tar-stream 解包 + * 到同盘临时目录 → renameSync 原子替换 → 写 state + * + * 与 postinstall.js(第一层,npm install 无条件覆盖)互补。二者都用 + * Node 原生 brotli + tar-stream extract(),与 Crawler 端 tar.pack() 对称。 + * + * 失败策略:任何一步失败都静默返回且不更新 lastChecked,下次 recommend 立即重试。 + */ +import { createHash } from "node:crypto"; +import { + createWriteStream, + existsSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { dirname, join } from "node:path"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import { createBrotliDecompress } from "node:zlib"; +import tar from "tar-stream"; +import { getConfigDir } from "../config/paths.ts"; + +/** FC 匿名签名函数入口,返回 OSS 预签名 URL(硬编码,不走 env)。 */ +const FC_SIGN_URL = "https://signature-gmqkxxozrl.cn-hangzhou.fcapp.run"; +const SKILL_DIR_NAME = "skills/bailian-docs-llm-wiki"; +const STATE_FILE_NAME = "wiki-sync-state.json"; +const MODELS_FILE = "models.jsonl"; +const MANIFEST_KEY = "manifest.json"; +const ASSET_KEY = "wiki-doc-full.tar.br"; + +const THROTTLE_MS = 12 * 60 * 60 * 1000; // 12h +const MANIFEST_TIMEOUT_MS = 3000; +const DOWNLOAD_TIMEOUT_MS = 30000; + +interface SyncState { + lastChecked: number; + version: string; +} + +interface Manifest { + name: string; + version: string; + publishedAt?: string; + asset: { + name: string; + url?: string; + sha256: string; + size: number; + compression: string; + }; +} + +function getCatalogDir(): string { + return join(getConfigDir(), SKILL_DIR_NAME); +} + +/** + * 本地 Wiki 数据是否已就绪。以 `models.jsonl` 作为存在信号,与 + * `CatalogSource.available()` 判定一致:只要 advisor 真正消费的文件在, + * 就认为数据可用。 + */ +function catalogDataExists(): boolean { + return existsSync(join(getCatalogDir(), "models", MODELS_FILE)); +} + +function getStatePath(): string { + return join(getConfigDir(), STATE_FILE_NAME); +} + +function readState(): SyncState | null { + try { + return JSON.parse(readFileSync(getStatePath(), "utf-8")) as SyncState; + } catch { + return null; + } +} + +function writeState(state: SyncState): void { + try { + writeFileSync(getStatePath(), JSON.stringify(state)); + } catch { + /* 非关键:state 写失败下次会重新检查 */ + } +} + +/** 拉全部 OSS 预签名 URL:`{ urls: { "manifest.json": "...", "wiki-doc-full.tar.br": "..." } }` */ +async function fetchSignedUrls(): Promise | null> { + try { + const res = await fetch(FC_SIGN_URL, { + signal: AbortSignal.timeout(MANIFEST_TIMEOUT_MS), + }); + if (!res.ok) return null; + const data = (await res.json()) as { urls?: Record }; + return data.urls ?? null; + } catch { + return null; + } +} + +async function fetchManifest(url: string): Promise { + try { + const res = await fetch(url, { signal: AbortSignal.timeout(MANIFEST_TIMEOUT_MS) }); + if (!res.ok) return null; + return (await res.json()) as Manifest; + } catch { + return null; + } +} + +async function downloadBuffer(url: string): Promise { + try { + const res = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) }); + if (!res.ok) return null; + return Buffer.from(await res.arrayBuffer()); + } catch { + return null; + } +} + +/** brotli 解压 + tar-stream 解包到 destDir(与 Crawler tar.pack() 对称)。 */ +async function extractTarBr(tarBrBuffer: Buffer, destDir: string): Promise { + const extract = tar.extract(); + + extract.on("entry", (header, stream, next) => { + const filePath = join(destDir, header.name); + if (header.type === "directory") { + mkdirSync(filePath, { recursive: true }); + stream.resume(); + stream.on("end", next); + return; + } + mkdirSync(dirname(filePath), { recursive: true }); + const ws = createWriteStream(filePath); + stream.pipe(ws); + ws.on("finish", next); + ws.on("error", next); + }); + + await pipeline(Readable.from(tarBrBuffer), createBrotliDecompress(), extract); +} + +/** + * 原子替换:把 tmpDir 解包好的内容替换到 catalogDir。 + * tmpDir 必须与 catalogDir 同盘(同一 parent 下),renameSync 才是原子的。 + */ +function atomicSwap(tmpDir: string, catalogDir: string): void { + mkdirSync(dirname(catalogDir), { recursive: true }); + const backup = `${catalogDir}.old-${Date.now()}`; + if (existsSync(catalogDir)) renameSync(catalogDir, backup); + try { + renameSync(tmpDir, catalogDir); + } catch (err) { + // 替换失败 → 回滚旧目录,避免留下空洞 + if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir); + throw err; + } + if (existsSync(backup)) rmSync(backup, { recursive: true, force: true }); +} + +/** + * 检查并同步 Wiki 数据。静默执行,任何异常都不抛出。 + * @returns 是否实际更新了数据(用于测试/调试) + */ +export async function maybeSyncWikiData(): Promise { + const state = readState(); + const now = Date.now(); + + // 1. throttle gate:仅当「在 12h 窗口内」且「本地数据确实存在」时才跳过。 + // 数据缺失(用户手动删除、postinstall 失败但 state 残留等)时无视 throttle, + // 立即走同步补齐,避免 advisor 拿不到数据。 + if (state && now - state.lastChecked < THROTTLE_MS && catalogDataExists()) { + return false; + } + + // 2. 拿签名 URL + manifest + const urls = await fetchSignedUrls(); + if (!urls?.[MANIFEST_KEY]) return false; // 失败不写 lastChecked,下次重试 + + const manifest = await fetchManifest(urls[MANIFEST_KEY]); + if (!manifest?.version) return false; + + // 3. 版本相同且本地数据存在:仅刷新 lastChecked,无需重新下载。 + // 若数据缺失,即便版本相同也要继续走下载补齐(落到第 4 步)。 + if (state && manifest.version === state.version && catalogDataExists()) { + writeState({ lastChecked: now, version: state.version }); + return false; + } + + // 4. 版本不同:下载 + 校验 + 解包 + 原子替换 + const assetUrl = urls[ASSET_KEY]; + if (!assetUrl) return false; + + const tarBuf = await downloadBuffer(assetUrl); + if (!tarBuf) return false; + + // sha256 校验 + const sha256 = createHash("sha256").update(tarBuf).digest("hex"); + if (manifest.asset?.sha256 && sha256 !== manifest.asset.sha256) return false; + + const catalogDir = getCatalogDir(); + // 同盘临时目录:extract 到这里再 rename,跨盘 rename 会 EXDEV + const tmpDir = `${catalogDir}.tmp-${process.pid}-${Date.now()}`; + try { + mkdirSync(tmpDir, { recursive: true }); + await extractTarBr(tarBuf, tmpDir); + atomicSwap(tmpDir, catalogDir); + } catch { + // 解包/替换失败 → 清理临时目录,不动现有数据,不写 state + if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true }); + return false; + } + + // 5. 成功:写 state + writeState({ lastChecked: now, version: manifest.version }); + return true; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a434066..7f40dd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,9 @@ catalogs: '@types/node': specifier: ^24 version: 24.12.2 + '@types/tar-stream': + specifier: ^3.1.4 + version: 3.1.4 '@types/yauzl': specifier: ^3.4.0 version: 3.4.0 @@ -21,6 +24,9 @@ catalogs: chalk: specifier: ^5.6.2 version: 5.6.2 + tar-stream: + specifier: ^3.2.0 + version: 3.2.0 tsx: specifier: ^4.23.0 version: 4.23.0 @@ -63,6 +69,9 @@ importers: bailian-cli-runtime: specifier: workspace:* version: link:../runtime + tar-stream: + specifier: 'catalog:' + version: 3.2.0 devDependencies: '@clack/prompts': specifier: ^0.7.0 @@ -134,6 +143,9 @@ importers: packages/core: dependencies: + tar-stream: + specifier: 'catalog:' + version: 3.2.0 yaml: specifier: ^2.8.3 version: 2.8.3 @@ -144,6 +156,9 @@ importers: '@types/node': specifier: 'catalog:' version: 24.12.2 + '@types/tar-stream': + specifier: 'catalog:' + version: 3.1.4 '@types/yauzl': specifier: 'catalog:' version: 3.4.0 @@ -847,6 +862,9 @@ packages: '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/tar-stream@3.1.4': + resolution: {integrity: sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==} + '@types/yauzl@3.4.0': resolution: {integrity: sha512-NRPn5w6h8dhcnmx3YIRQcqMywY/+nND/uOkJessedcrowO3C0AssHp3tMJpxKAwOhFOo0OV1y9VtsC5hbKKBAw==} @@ -1057,6 +1075,51 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + bare-events@2.9.1: + resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.4: + resolution: {integrity: sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} + + bare-stream@2.13.3: + resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.5: + resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} + boxen@8.0.1: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} @@ -1091,9 +1154,15 @@ packages: engines: {node: '>=18'} hasBin: true + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} @@ -1276,6 +1345,9 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + streamx@2.28.0: + resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1292,6 +1364,15 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1744,6 +1825,10 @@ snapshots: dependencies: undici-types: 7.19.2 + '@types/tar-stream@3.1.4': + dependencies: + '@types/node': 25.6.0 + '@types/yauzl@3.4.0': dependencies: '@types/node': 25.6.0 @@ -1932,6 +2017,37 @@ snapshots: assertion-error@2.0.1: {} + b4a@1.8.1: {} + + bare-events@2.9.1: {} + + bare-fs@4.7.4: + dependencies: + bare-events: 2.9.1 + bare-path: 3.1.1 + bare-stream: 2.13.3(bare-events@2.9.1) + bare-url: 2.4.5 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-path@3.1.1: {} + + bare-stream@2.13.3(bare-events@2.9.1): + dependencies: + b4a: 1.8.1 + streamx: 2.28.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.5: + dependencies: + bare-path: 3.1.1 + boxen@8.0.1: dependencies: ansi-align: 3.0.1 @@ -1986,8 +2102,16 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + events-universal@1.0.1: + dependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - bare-abort-controller + fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-uri@3.1.2: {} fdir@6.5.0(picomatch@4.0.4): @@ -2170,6 +2294,15 @@ snapshots: std-env@4.1.0: {} + streamx@2.28.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -2190,6 +2323,30 @@ snapshots: dependencies: ansi-regex: 6.2.2 + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.7.4 + fast-fifo: 1.3.2 + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + teex@1.0.1: + dependencies: + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + tinybench@2.9.0: {} tinyexec@1.1.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4f3e9e3..8296585 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,10 +4,12 @@ packages: catalog: "@types/node": ^24 + "@types/tar-stream": ^3.1.4 "@types/yauzl": ^3.4.0 ajv: ^8.20.0 boxen: ^8.0.1 chalk: ^5.6.2 + tar-stream: ^3.2.0 tsx: ^4.23.0 typescript: ^5 undici: ^8.4.1