mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat: sync wiki data from oss by fc
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Generated
+157
@@ -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: {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user