feat: sync wiki data from oss by fc

This commit is contained in:
故璃
2026-07-17 16:43:06 +08:00
parent 39f92567d3
commit d08edf0cd8
9 changed files with 571 additions and 34 deletions
+6 -3
View File
@@ -25,7 +25,8 @@
}, },
"files": [ "files": [
"dist", "dist",
"README.zh.md" "README.zh.md",
"postinstall.js"
], ],
"type": "module", "type": "module",
"exports": { "exports": {
@@ -45,12 +46,14 @@
"build": "vp pack", "build": "vp pack",
"dev": "tsx src/main.ts", "dev": "tsx src/main.ts",
"test": "vp test", "test": "vp test",
"check": "vp check" "check": "vp check",
"postinstall": "node postinstall.js"
}, },
"dependencies": { "dependencies": {
"bailian-cli-commands": "workspace:*", "bailian-cli-commands": "workspace:*",
"bailian-cli-core": "workspace:*", "bailian-cli-core": "workspace:*",
"bailian-cli-runtime": "workspace:*" "bailian-cli-runtime": "workspace:*",
"tar-stream": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@clack/prompts": "^0.7.0", "@clack/prompts": "^0.7.0",
+160
View File
@@ -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);
});
@@ -6,6 +6,7 @@ import {
type GetModelsOptions, type GetModelsOptions,
getModels, getModels,
type IntentProfile, type IntentProfile,
maybeSyncWikiData,
type PipelineStep, type PipelineStep,
type RecommendedModel, type RecommendedModel,
type RecommendResult, type RecommendResult,
@@ -248,6 +249,12 @@ export default defineCommand({
const { settings, flags } = ctx; const { settings, flags } = ctx;
const userInput = flags.message; const userInput = flags.message;
const top = 3; 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 // Default to JSON for structured output; render boxen cards only when the
// user explicitly asked for text output. // user explicitly asked for text output.
const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json"; const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json";
+2
View File
@@ -40,11 +40,13 @@
"check": "vp check" "check": "vp check"
}, },
"dependencies": { "dependencies": {
"tar-stream": "catalog:",
"yaml": "^2.8.3", "yaml": "^2.8.3",
"yauzl": "catalog:" "yauzl": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/tar-stream": "catalog:",
"@types/yauzl": "catalog:", "@types/yauzl": "catalog:",
"@typescript/native-preview": "7.0.0-dev.20260328.1", "@typescript/native-preview": "7.0.0-dev.20260328.1",
"typescript": "^6.0.2", "typescript": "^6.0.2",
+1
View File
@@ -7,6 +7,7 @@ export { recallCandidates } from "./recall.ts";
export { recallSemantic, isSemanticAvailable } from "./recall-semantic.ts"; export { recallSemantic, isSemanticAvailable } from "./recall-semantic.ts";
export type { RecommendOptions } from "./recommend.ts"; export type { RecommendOptions } from "./recommend.ts";
export { buildDocLink, rankModels } from "./recommend.ts"; export { buildDocLink, rankModels } from "./recommend.ts";
export { maybeSyncWikiData } from "./sync.ts";
export type { ModelSource } from "./sources/types.ts"; export type { ModelSource } from "./sources/types.ts";
export type { export type {
Budget, Budget,
+10 -31
View File
@@ -1,6 +1,5 @@
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path"; import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { getConfigDir } from "../../config/paths.ts"; import { getConfigDir } from "../../config/paths.ts";
import type { ModelPrice, ModelProfile, QpmLimit } from "../types.ts"; import type { ModelPrice, ModelProfile, QpmLimit } from "../types.ts";
import type { ModelSource } from "./types.ts"; import type { ModelSource } from "./types.ts";
@@ -13,12 +12,9 @@ function getCatalogDir(): string {
} }
function getCatalogPath(): string { function getCatalogPath(): string {
return join(getCatalogDir(), MODELS_FILE); // Full-package layout keeps the `models/` subdir (raw/, wiki/, models/, …),
} // so models.jsonl lives at <skill>/models/models.jsonl — not at the skill root.
return join(getCatalogDir(), "models", MODELS_FILE);
function getMonorepoModelsDir(): string {
const coreDir = dirname(fileURLToPath(import.meta.url));
return join(coreDir, "../../../../../skills/bailian-docs-llm-wiki/models");
} }
function fromJsonlRecord(raw: Record<string, unknown>): ModelProfile | null { function fromJsonlRecord(raw: Record<string, unknown>): ModelProfile | null {
@@ -62,41 +58,24 @@ function readJsonlModels(filePath: string): ModelProfile[] {
return models; 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 { export interface CatalogSourceOptions {
onPrepareStart?: () => void; onPrepareStart?: () => void;
} }
export class CatalogSource implements ModelSource { export class CatalogSource implements ModelSource {
readonly name = "catalog"; readonly name = "catalog";
private options: CatalogSourceOptions;
constructor(options?: CatalogSourceOptions) { // Options retained for API compatibility. Data is now always provisioned by
this.options = options ?? {}; // the CLI postinstall hook and refreshed by advisor sync, so the previous
} // `onPrepareStart` install callback is obsolete.
constructor(_options?: CatalogSourceOptions) {}
available(): boolean { available(): boolean {
return existsSync(getCatalogPath()); return existsSync(getCatalogPath());
} }
async load(): Promise<ModelProfile[]> { async load(): Promise<ModelProfile[]> {
if (!this.available()) { if (!this.available()) return [];
this.options.onPrepareStart?.();
const installed = installFromMonorepo();
if (!installed) return [];
}
return readJsonlModels(getCatalogPath()); return readJsonlModels(getCatalogPath());
} }
} }
+226
View File
@@ -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<Record<string, string> | 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<string, string> };
return data.urls ?? null;
} catch {
return null;
}
}
async function fetchManifest(url: string): Promise<Manifest | null> {
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<Buffer | null> {
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<void> {
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<boolean> {
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;
}
+157
View File
@@ -9,6 +9,9 @@ catalogs:
'@types/node': '@types/node':
specifier: ^24 specifier: ^24
version: 24.12.2 version: 24.12.2
'@types/tar-stream':
specifier: ^3.1.4
version: 3.1.4
'@types/yauzl': '@types/yauzl':
specifier: ^3.4.0 specifier: ^3.4.0
version: 3.4.0 version: 3.4.0
@@ -21,6 +24,9 @@ catalogs:
chalk: chalk:
specifier: ^5.6.2 specifier: ^5.6.2
version: 5.6.2 version: 5.6.2
tar-stream:
specifier: ^3.2.0
version: 3.2.0
tsx: tsx:
specifier: ^4.23.0 specifier: ^4.23.0
version: 4.23.0 version: 4.23.0
@@ -63,6 +69,9 @@ importers:
bailian-cli-runtime: bailian-cli-runtime:
specifier: workspace:* specifier: workspace:*
version: link:../runtime version: link:../runtime
tar-stream:
specifier: 'catalog:'
version: 3.2.0
devDependencies: devDependencies:
'@clack/prompts': '@clack/prompts':
specifier: ^0.7.0 specifier: ^0.7.0
@@ -134,6 +143,9 @@ importers:
packages/core: packages/core:
dependencies: dependencies:
tar-stream:
specifier: 'catalog:'
version: 3.2.0
yaml: yaml:
specifier: ^2.8.3 specifier: ^2.8.3
version: 2.8.3 version: 2.8.3
@@ -144,6 +156,9 @@ importers:
'@types/node': '@types/node':
specifier: 'catalog:' specifier: 'catalog:'
version: 24.12.2 version: 24.12.2
'@types/tar-stream':
specifier: 'catalog:'
version: 3.1.4
'@types/yauzl': '@types/yauzl':
specifier: 'catalog:' specifier: 'catalog:'
version: 3.4.0 version: 3.4.0
@@ -847,6 +862,9 @@ packages:
'@types/node@25.6.0': '@types/node@25.6.0':
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
'@types/tar-stream@3.1.4':
resolution: {integrity: sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==}
'@types/yauzl@3.4.0': '@types/yauzl@3.4.0':
resolution: {integrity: sha512-NRPn5w6h8dhcnmx3YIRQcqMywY/+nND/uOkJessedcrowO3C0AssHp3tMJpxKAwOhFOo0OV1y9VtsC5hbKKBAw==} resolution: {integrity: sha512-NRPn5w6h8dhcnmx3YIRQcqMywY/+nND/uOkJessedcrowO3C0AssHp3tMJpxKAwOhFOo0OV1y9VtsC5hbKKBAw==}
@@ -1057,6 +1075,51 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'} 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: boxen@8.0.1:
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1091,9 +1154,15 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
events-universal@1.0.1:
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 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: fast-uri@3.1.2:
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
@@ -1276,6 +1345,9 @@ packages:
std-env@4.1.0: std-env@4.1.0:
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
streamx@2.28.0:
resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==}
string-width@4.2.3: string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1292,6 +1364,15 @@ packages:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'} 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: tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1744,6 +1825,10 @@ snapshots:
dependencies: dependencies:
undici-types: 7.19.2 undici-types: 7.19.2
'@types/tar-stream@3.1.4':
dependencies:
'@types/node': 25.6.0
'@types/yauzl@3.4.0': '@types/yauzl@3.4.0':
dependencies: dependencies:
'@types/node': 25.6.0 '@types/node': 25.6.0
@@ -1932,6 +2017,37 @@ snapshots:
assertion-error@2.0.1: {} 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: boxen@8.0.1:
dependencies: dependencies:
ansi-align: 3.0.1 ansi-align: 3.0.1
@@ -1986,8 +2102,16 @@ snapshots:
'@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 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-deep-equal@3.1.3: {}
fast-fifo@1.3.2: {}
fast-uri@3.1.2: {} fast-uri@3.1.2: {}
fdir@6.5.0(picomatch@4.0.4): fdir@6.5.0(picomatch@4.0.4):
@@ -2170,6 +2294,15 @@ snapshots:
std-env@4.1.0: {} 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: string-width@4.2.3:
dependencies: dependencies:
emoji-regex: 8.0.0 emoji-regex: 8.0.0
@@ -2190,6 +2323,30 @@ snapshots:
dependencies: dependencies:
ansi-regex: 6.2.2 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: {} tinybench@2.9.0: {}
tinyexec@1.1.2: {} tinyexec@1.1.2: {}
+2
View File
@@ -4,10 +4,12 @@ packages:
catalog: catalog:
"@types/node": ^24 "@types/node": ^24
"@types/tar-stream": ^3.1.4
"@types/yauzl": ^3.4.0 "@types/yauzl": ^3.4.0
ajv: ^8.20.0 ajv: ^8.20.0
boxen: ^8.0.1 boxen: ^8.0.1
chalk: ^5.6.2 chalk: ^5.6.2
tar-stream: ^3.2.0
tsx: ^4.23.0 tsx: ^4.23.0
typescript: ^5 typescript: ^5
undici: ^8.4.1 undici: ^8.4.1