From 5b2902d3f4fc531e13b571ab09fd51457dbd01bd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 22 Jul 2026 06:42:12 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(ci):=20=E6=B8=85=E7=90=86=E6=82=AC?= =?UTF-8?q?=E7=A9=BA=20workspace=20lock=20=E5=B9=B6=E8=A1=A5=E9=BD=90=20js?= =?UTF-8?q?zip=EF=BC=8C=E6=81=A2=E5=A4=8D=20npm=20install?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ootb 在 Install root deps 因 package-lock 仍链接已删除的 modules/packages/* 而崩溃(extraneous)。同步 afk workspace, 并将 fetch-module 所需的 jszip 写入 devDependencies。 Co-authored-by: Shiroha --- package-lock.json | 48 +++++++++++++++-------------------------------- package.json | 3 ++- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5259e9c..a685f00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,6 @@ "sfmc", "remote-controller" ], - "dependencies": { - "lodash": "^4.18.1" - }, "devDependencies": { "@minecraft/server": "2.10.0-beta.1.26.40-preview.30", "@minecraft/server-net": "^1.0.0-beta.1.26.33-stable", @@ -30,6 +27,8 @@ "@typescript-eslint/parser": "^8.64.0", "esbuild": "^0.28.1", "eslint": "^10.7.0", + "jszip": "^3.10.1", + "lodash": "^4.18.1", "nodemon": "^3.1.14", "npm-run-all2": "^9.0.2", "prettier": "^3.9.5", @@ -62,6 +61,16 @@ "node": ">=22.13.0" } }, + "modules/packages/afk": { + "name": "@sfmc/module-afk", + "version": "0.1.0", + "dependencies": { + "@sfmc/sdk": "*" + }, + "peerDependencies": { + "@minecraft/server": "2.10.0-beta.1.26.40-preview.30" + } + }, "modules/sdk/@sfmc-sdk": { "name": "@sfmc/sdk", "version": "0.1.0", @@ -802,36 +811,8 @@ "integrity": "sha512-YQOEb3+HjjdL527YHmmPq/uqgJd2NO5DrO/SmCMrapDUuxO3LByPhd7IYGQqfy2MByF+AcjZZj4B0Bo27gGaTQ==", "license": "MIT" }, - "node_modules/@sfmc/module-daily-task": { - "resolved": "modules/packages/daily-task", - "link": true - }, - "node_modules/@sfmc/module-data-backup": { - "resolved": "modules/packages/data-backup", - "link": true - }, - "node_modules/@sfmc/module-economy": { - "resolved": "modules/packages/economy", - "link": true - }, - "node_modules/@sfmc/module-feature-chat": { - "resolved": "modules/packages/feature-chat", - "link": true - }, - "node_modules/@sfmc/module-feature-coop": { - "resolved": "modules/packages/feature-coop", - "link": true - }, - "node_modules/@sfmc/module-gui": { - "resolved": "modules/packages/gui", - "link": true - }, - "node_modules/@sfmc/module-land": { - "resolved": "modules/packages/land", - "link": true - }, - "node_modules/@sfmc/module-land-gui": { - "resolved": "modules/packages/land-gui", + "node_modules/@sfmc/module-afk": { + "resolved": "modules/packages/afk", "link": true }, "node_modules/@sfmc/remote-controller": { @@ -2166,6 +2147,7 @@ "version": "4.18.1", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, "license": "MIT" }, "node_modules/lru-cache": { diff --git a/package.json b/package.json index bd3526c..787fe97 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "ts-node": "^10.9.2", "tsx": "^4.19.0", "typescript": "^6.0.3", - "lodash": "^4.18.1" + "lodash": "^4.18.1", + "jszip": "^3.10.1" }, "scripts": { "start": "node sfmc/dist/main.js", From 49a5b19872fee43f30ec1c030c1d473df1197658 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 22 Jul 2026 06:42:12 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(modules):=20=E5=AF=B9=E9=BD=90=20regist?= =?UTF-8?q?ry=20modules=20=E5=B5=8C=E5=A5=97=E5=A5=91=E7=BA=A6=20+=20unzip?= =?UTF-8?q?=20=E8=B7=AF=E5=BE=84=E5=BD=92=E4=B8=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sfmc registry 与 fetch-module 共用 index.modules 解析契约(LSP/DRY) - unzip 归一化 Windows \\,避免 Linux 目录布局错乱(LSP) - catalog/check-modules 复用 folderFromEntryPath(DRY/Demeter) - 修复 module-commands dirSize 未 await 导致体积统计恒为 0 Co-authored-by: Shiroha --- sfmc/src/module-commands.ts | 2 +- sfmc/src/registry.ts | 30 ++++++++++++++++++++++++++---- tools/check-modules.mjs | 6 ++++-- tools/fetch-module.mjs | 21 ++++++--------------- tools/lib/catalog.mjs | 19 ++++++++----------- tools/lib/packages.mjs | 3 --- tools/lib/registry-index.mjs | 36 ++++++++++++++++++++++++++++++++++++ 7 files changed, 81 insertions(+), 36 deletions(-) create mode 100644 tools/lib/registry-index.mjs diff --git a/sfmc/src/module-commands.ts b/sfmc/src/module-commands.ts index e195e22..9ef08d0 100644 --- a/sfmc/src/module-commands.ts +++ b/sfmc/src/module-commands.ts @@ -131,7 +131,7 @@ async function dirSize(dir: string): Promise<{ totalBytes: number; fileCount: nu } } } - walk(dir); + await walk(dir); return { totalBytes, fileCount }; } diff --git a/sfmc/src/registry.ts b/sfmc/src/registry.ts index 2b5d5ab..97aefdc 100644 --- a/sfmc/src/registry.ts +++ b/sfmc/src/registry.ts @@ -54,14 +54,36 @@ function writeCache(cache: RegistryCache): void { } } +/** + * 解析 registry index.json。 + * 契约与 tools/lib/registry-index.mjs#parseRegistryIndex 保持一致: + * `{ modules: { : { repo, tag } } }`。忽略 `_` 前缀元数据键。 + */ +export function parseRegistryIndex(json: unknown): RegistryIndex { + if (typeof json !== "object" || json === null || Array.isArray(json)) { + throw new Error("registry index must be a JSON object with a 'modules' field"); + } + const root = json as Record; + const modules = root.modules; + if (typeof modules !== "object" || modules === null || Array.isArray(modules)) { + throw new Error("registry index must have a 'modules' object mapping id → { repo, tag }"); + } + const filtered: RegistryIndex = {}; + for (const [k, v] of Object.entries(modules as Record)) { + if (k.startsWith("_")) continue; + if (typeof v !== "object" || v === null || Array.isArray(v)) continue; + const entry = v as Record; + if (typeof entry.repo !== "string" || typeof entry.tag !== "string") continue; + filtered[k] = { repo: entry.repo, tag: entry.tag }; + } + return filtered; +} + async function fetchFresh(): Promise { const res = await fetch(INDEX_URL, { headers: { "User-Agent": "sfmc-cli" } }); if (!res.ok) throw new Error(`HTTP ${res.status} for ${INDEX_URL}`); const json = (await res.json()) as unknown; - if (typeof json !== "object" || json === null || Array.isArray(json)) { - throw new Error("registry index must be a JSON object mapping id → { repo, tag }"); - } - return json as RegistryIndex; + return parseRegistryIndex(json); } export interface RegistryResult { diff --git a/tools/check-modules.mjs b/tools/check-modules.mjs index aaefa70..8c6058d 100644 --- a/tools/check-modules.mjs +++ b/tools/check-modules.mjs @@ -14,7 +14,7 @@ import fs from "node:fs"; import path from "node:path"; import { readCatalog, syncCatalogFromPackages } from "./lib/catalog.mjs"; -import { scanInstalledPackages } from "./lib/packages.mjs"; +import { folderFromEntryPath, scanInstalledPackages } from "./lib/packages.mjs"; import { ROOT } from "./lib/paths.mjs"; import { exists } from "./lib/io.mjs"; @@ -62,7 +62,9 @@ function main() { const abs = path.join(ROOT, m.entry.path); if (!exists(abs)) fail(`${m.id}: entry 不存在: ${m.entry.path}`); - const folder = String(m.entry.path).replace(/\\/g, "/").split("/")[2]; + // 单一真相:folder 解析与 catalog/packages 共用 folderFromEntryPath + const folder = folderFromEntryPath(m.entry.path); + if (!folder) fail(`${m.id}: 无法从 entry.path 解析 packages 目录: ${m.entry.path}`); const manifestPath = path.join(ROOT, "modules", "packages", folder, "sapi", "manifest.json"); if (!exists(manifestPath)) fail(`${m.id}: 缺少 packages/${folder}/sapi/manifest.json`); let manifest; diff --git a/tools/fetch-module.mjs b/tools/fetch-module.mjs index 52237d8..3399aff 100644 --- a/tools/fetch-module.mjs +++ b/tools/fetch-module.mjs @@ -26,6 +26,7 @@ import { upsertCatalogEntry, removeCatalogEntry } from "./lib/catalog.mjs"; import { setModuleLockEnabled, removeModuleLock } from "./lib/lock.mjs"; import { PACKAGES_DIR, ROOT } from "./lib/paths.mjs"; import { exists } from "./lib/io.mjs"; +import { parseRegistryIndex } from "./lib/registry-index.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const TARGET = PACKAGES_DIR; @@ -62,20 +63,7 @@ async function fetchRegistryIndexFresh() { const res = await fetch(DEFAULT_REGISTRY_INDEX_URL, { headers: { "User-Agent": "sfmc-fetch-module" } }); if (!res.ok) throw new Error(`HTTP ${res.status} for ${DEFAULT_REGISTRY_INDEX_URL}`); const json = await res.json(); - if (typeof json !== "object" || json === null || Array.isArray(json)) { - throw new Error("registry index must be a JSON object with a 'modules' field"); - } - const modules = json.modules; - if (typeof modules !== "object" || modules === null || Array.isArray(modules)) { - throw new Error("registry index must have a 'modules' object mapping id → { repo, tag }"); - } - /** @type {RegistryIndex} */ - const filtered = {}; - for (const [k, v] of Object.entries(modules)) { - if (k.startsWith("_")) continue; - filtered[k] = v; - } - return filtered; + return parseRegistryIndex(json); } async function resolveRegistryIndex() { @@ -294,7 +282,10 @@ async function unzip(zipPath, dstDir) { const data = await fsp.readFile(zipPath); const zip = await JSZip.loadAsync(data); for (const e of Object.values(zip.files)) { - const out = path.join(dstDir, e.name); + // Windows zip 可能带 `\`;必须归一化,否则 Linux 会写出字面量 `sapi\manifest.json` + const rel = String(e.name).replace(/\\/g, "/"); + if (!rel || rel.includes("..")) continue; + const out = path.join(dstDir, ...rel.split("/").filter(Boolean)); if (e.dir) { await fsp.mkdir(out, { recursive: true }); continue; diff --git a/tools/lib/catalog.mjs b/tools/lib/catalog.mjs index 9dc622b..138bfd7 100644 --- a/tools/lib/catalog.mjs +++ b/tools/lib/catalog.mjs @@ -3,7 +3,12 @@ */ import { CATALOG_PATH } from "./paths.mjs"; import { readJson, writeJson } from "./io.mjs"; -import { loadPackageCatalogEntry, projectCatalogEntry, scanInstalledPackages } from "./packages.mjs"; +import { + folderFromEntryPath, + loadPackageCatalogEntry, + projectCatalogEntry, + scanInstalledPackages, +} from "./packages.mjs"; const DEFAULT_COMMENT = "modules/catalog.json 是本地 mirror;source of truth 为 github:Tanya7z/sfmc-modules/main/index.json。条目由已安装的 modules/packages//sapi/manifest.json 投影生成(fetch-module install / catalog-sync)。空数组表示纯 SDK 仓,合法。"; @@ -65,7 +70,7 @@ export function upsertCatalogEntry(folder) { if (!entry) throw new Error(`packages/${folder}: 无有效 sapi/manifest.json`); const catalog = readCatalog(); const idx = catalog.modules.findIndex( - (m) => m.id === entry.id || (m.entry && folderFromEntry(m.entry.path) === folder) + (m) => m.id === entry.id || (m.entry && folderFromEntryPath(m.entry.path) === folder) ); if (idx >= 0) catalog.modules[idx] = entry; else catalog.modules.push(entry); @@ -83,18 +88,10 @@ export function removeCatalogEntry(folderOrId) { const catalog = readCatalog(); const idx = catalog.modules.findIndex((m) => { if (m.id === folderOrId) return true; - return folderFromEntry(m.entry?.path) === folderOrId; + return folderFromEntryPath(m.entry?.path) === folderOrId; }); if (idx < 0) return null; const [removed] = catalog.modules.splice(idx, 1); writeCatalog(catalog); return removed; } - -/** @param {string | undefined} entryPath */ -function folderFromEntry(entryPath) { - const m = String(entryPath || "") - .replace(/\\/g, "/") - .match(/modules\/packages\/([^/]+)\//); - return m ? m[1] : null; -} diff --git a/tools/lib/packages.mjs b/tools/lib/packages.mjs index 8398149..e0c4f0d 100644 --- a/tools/lib/packages.mjs +++ b/tools/lib/packages.mjs @@ -1,6 +1,3 @@ -/** - * tools/lib/packages.mjs — 扫描 packages//sapi/manifest.json 并投影为 catalog 条目 - */ /** * tools/lib/packages.mjs — 扫描 packages//sapi/manifest.json 并投影为 catalog 条目 */ diff --git a/tools/lib/registry-index.mjs b/tools/lib/registry-index.mjs new file mode 100644 index 0000000..8306371 --- /dev/null +++ b/tools/lib/registry-index.mjs @@ -0,0 +1,36 @@ +/** + * tools/lib/registry-index.mjs — 解析 Tanya7z/sfmc-modules index.json + * + * 契约(与 sfmc/src/registry.ts#parseRegistryIndex 保持一致): + * { modules: { : { repo, tag } } } + * `_` 前缀键视为元数据并忽略。 + */ + +/** + * @typedef {{ repo: string, tag: string }} RegistryEntry + * @typedef {Record} RegistryIndex + */ + +/** + * @param {unknown} json + * @returns {RegistryIndex} + */ +export function parseRegistryIndex(json) { + if (typeof json !== "object" || json === null || Array.isArray(json)) { + throw new Error("registry index must be a JSON object with a 'modules' field"); + } + const modules = /** @type {Record} */ (json).modules; + if (typeof modules !== "object" || modules === null || Array.isArray(modules)) { + throw new Error("registry index must have a 'modules' object mapping id → { repo, tag }"); + } + /** @type {RegistryIndex} */ + const filtered = {}; + for (const [k, v] of Object.entries(modules)) { + if (k.startsWith("_")) continue; + if (typeof v !== "object" || v === null || Array.isArray(v)) continue; + const entry = /** @type {Record} */ (v); + if (typeof entry.repo !== "string" || typeof entry.tag !== "string") continue; + filtered[k] = { repo: entry.repo, tag: entry.tag }; + } + return filtered; +}