From a178ff261747273c44c982b99a80005cd132c062 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 16:22:18 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(ci):=20SEA=20Node=20=E6=8A=BD=E5=88=B0?= =?UTF-8?q?=20.node-version-sea=EF=BC=88DRY=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查 #56 合入后:默认 CI 已统一到 .node-version,但 SEA 仍在 release.yml 与 build-sea.mjs 各写一份 node26。抽出 .node-version-sea 作为 SEA 目标唯一权威;release 用 node-version-file,bundle 读取同文件。 Co-authored-by: Shiroha --- .github/workflows/release.yml | 5 +++-- .node-version-sea | 1 + build-sea.mjs | 14 ++++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 .node-version-sea diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fbed6093..b401b21d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,8 +26,9 @@ jobs: - uses: actions/setup-node@v5 with: - # 故意独立于 .node-version:SEA bundle 目标 node26(见 build-sea.mjs target) - node-version: "26" + # 故意独立于 .node-version(烟测 CI);SEA 目标唯一来源:.node-version-sea + # (与 build-sea.mjs target 共用,DRY) + node-version-file: .node-version-sea cache: npm - run: npm ci --no-audit --no-fund diff --git a/.node-version-sea b/.node-version-sea new file mode 100644 index 00000000..6f4247a6 --- /dev/null +++ b/.node-version-sea @@ -0,0 +1 @@ +26 diff --git a/build-sea.mjs b/build-sea.mjs index a15f59b1..28782a3e 100644 --- a/build-sea.mjs +++ b/build-sea.mjs @@ -1,16 +1,26 @@ import * as esbuild from "esbuild"; import fs from "node:fs"; import { mkdir, rm } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; await rm("dist/sea", { recursive: true, force: true }); await mkdir("dist/sea", { recursive: true }); +/* SEA Node 目标唯一来源:仓库根 .node-version-sea(与 release.yml 共用,DRY) */ +const root = path.dirname(fileURLToPath(import.meta.url)); +const seaNodeMajor = fs.readFileSync(path.join(root, ".node-version-sea"), "utf8").trim(); +if (!/^\d+$/.test(seaNodeMajor)) { + throw new Error(`.node-version-sea 应为纯主版本号,当前: ${JSON.stringify(seaNodeMajor)}`); +} +const seaTarget = `node${seaNodeMajor}`; + const result = await esbuild.build({ entryPoints: ["sfmc/src/dispatcher.ts"], metafile: true, bundle: true, platform: "node", - target: "node26", + target: seaTarget, format: "esm", outfile: "dist/sea/dispatcher.mjs", external: ["node:readline", "node:fs", "node:path"], @@ -26,4 +36,4 @@ const result = await esbuild.build({ }, }); fs.writeFileSync("meta.json", JSON.stringify(result.metafile, null, 2)); -console.log("SEA dispatcher bundle -> dist/sea/dispatcher.mjs"); +console.log(`SEA dispatcher bundle -> dist/sea/dispatcher.mjs (target ${seaTarget})`); From 8cf18e060d0b7f9c3e7410d3ac655347ea4dc830 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 16:24:43 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(sfmc):=20pack-lifecycle=20=E7=9B=B4?= =?UTF-8?q?=E8=BF=9E=20pack-manager-lib=EF=BC=88DIP/DRY=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查 #56 合入后收敛长期债:world-packs 已同进程 import 库, pack-lifecycle 却仍 spawn CLI 解析 stdout,形成双路径。 - 导出 @sfmc-bds/bds-tools/pack-manager-lib - 抽出 readLevelNameSync(与 async 同契约) - assemble/deploy/enable/disable/permission/list/has/manifest 全部改库调用;CLI 保留为外部薄适配 Co-authored-by: Shiroha --- bds-tools/pack-manager-lifecycle.test.mjs | 11 + bds-tools/package.json | 1 + bds-tools/src/pack-manager.ts | 29 +- sfmc/src/pack-lifecycle.ts | 315 +++++++++------------- 4 files changed, 163 insertions(+), 193 deletions(-) diff --git a/bds-tools/pack-manager-lifecycle.test.mjs b/bds-tools/pack-manager-lifecycle.test.mjs index 1cb44d2b..7ef5d484 100644 --- a/bds-tools/pack-manager-lifecycle.test.mjs +++ b/bds-tools/pack-manager-lifecycle.test.mjs @@ -145,6 +145,17 @@ describe("pack-manager CLI extensions", () => { assert.deepEqual(readWorldPackList(worldsDir, "L1", "behavior"), listed); }); + it("readLevelNameSync 与 async 同契约(供 sfmc resolveBdsContext)", async () => { + const { readLevelName, readLevelNameSync } = await import("./dist/pack-manager.js"); + const bds = path.join(tmp, "bds-level"); + fs.mkdirSync(bds, { recursive: true }); + assert.equal(readLevelNameSync(bds), "Bedrock level"); + assert.equal(await readLevelName(bds), "Bedrock level"); + fs.writeFileSync(path.join(bds, "server.properties"), "level-name=My World\n", "utf8"); + assert.equal(readLevelNameSync(bds), "My World"); + assert.equal(await readLevelName(bds), "My World"); + }); + it("readWorldPackListResult 区分缺失与 JSON 损坏(doctor parseFail)", async () => { const { readWorldPackListResult } = await import("./dist/pack-manager.js"); const worldsDir = path.join(tmp, "worlds-parse"); diff --git a/bds-tools/package.json b/bds-tools/package.json index 6fc18c33..0502c264 100644 --- a/bds-tools/package.json +++ b/bds-tools/package.json @@ -31,6 +31,7 @@ "./check-update": "./dist/check-update.js", "./bds-manager": "./dist/bds-manager.js", "./pack-manager": "./dist/cli-pack-manager.js", + "./pack-manager-lib": "./dist/pack-manager.js", "./world-packs": "./dist/world-packs.js", "./taskbar": "./dist/taskbar.js", "./update-result": "./dist/update-result.js", diff --git a/bds-tools/src/pack-manager.ts b/bds-tools/src/pack-manager.ts index 70ccbe65..39401024 100644 --- a/bds-tools/src/pack-manager.ts +++ b/bds-tools/src/pack-manager.ts @@ -3,12 +3,10 @@ * * 这是 SEA 与 npm 两条用户路径共用的纯函数库: * - * - SEA:用户在 REPL/wizard 里跑 `sfmc behavior-pack build/deploy` - * → sfmc 通过 spawnService 启 `bds-tools/dist/pack-manager.js`, - * SEA 自己跑 esbuild 把脚本 bundle 写到 /build/sfmc-modules-bp/scripts/main.js, - * 然后调 pack-manager#assembleBehaviorPack 装配。 - * - npm: 用户跑 `sfmc behavior-pack build`, - * 同一 spawnService 路径。或者直接在同进程 import 这个库。 + * - 同进程(推荐): sfmc/pack-lifecycle 经 `@sfmc-bds/bds-tools/pack-manager-lib` + * 直连本模块(DIP);world-packs 等同理。 + * - CLI: `cli-pack-manager.js` 仍暴露相同动词,供外部脚本/手工调试; + * CLI 只是本库的薄适配,不再是 sfmc 主路径。 * * 关键约束:本文件不允许引 esbuild / 任何 npm-only 包 — SEA 要把它打成 SEA * 的一部分内嵌起来,而 SEA 是个单 exe (除 Node 内置 + bds-tools 已有依赖外)。 @@ -185,10 +183,7 @@ export async function assembleResourcePack(opts: AssembleResourcePackOpts): Prom * Falls back to "Bedrock level" (BDS default) when the file is missing or * the key is absent. */ -export async function readLevelName(bdsRoot: string): Promise { - const file = path.join(bdsRoot, "server.properties"); - if (!fs.existsSync(file)) return "Bedrock level"; - const text = await fs.promises.readFile(file, "utf8"); +function parseLevelNameFromProperties(text: string): string { for (const line of text.split(/\r?\n/)) { const m = /^\s*level-name\s*=\s*(.+?)\s*$/.exec(line); if (m && m[1]) return m[1]; @@ -196,6 +191,20 @@ export async function readLevelName(bdsRoot: string): Promise { return "Bedrock level"; } +/** 同步读 level-name(供 sfmc resolveBdsContext 等同进程调用方,DRY)。 */ +export function readLevelNameSync(bdsRoot: string): string { + const file = path.join(bdsRoot, "server.properties"); + if (!fs.existsSync(file)) return "Bedrock level"; + return parseLevelNameFromProperties(fs.readFileSync(file, "utf8")); +} + +export async function readLevelName(bdsRoot: string): Promise { + const file = path.join(bdsRoot, "server.properties"); + if (!fs.existsSync(file)) return "Bedrock level"; + const text = await fs.promises.readFile(file, "utf8"); + return parseLevelNameFromProperties(text); +} + /** * Copy BP/RP source dirs into the world's behavior_packs/ + resource_packs/ * folders, and write a fresh `permissions.json` into the BP target. diff --git a/sfmc/src/pack-lifecycle.ts b/sfmc/src/pack-lifecycle.ts index 64efa666..41cc58d3 100644 --- a/sfmc/src/pack-lifecycle.ts +++ b/sfmc/src/pack-lifecycle.ts @@ -24,7 +24,19 @@ import { pushLog } from "./logs.js"; import { dirFingerprint } from "./module-fingerprint.js"; import { failResult, okResult, type CliResult } from "./cli-result.js"; import { t } from "./i18n/index.js"; -import { ROOT, PACKAGES_DIR, spawnServiceSync, resolveSdkPackageRoot } from "./runtime.js"; +import { + assembleBehaviorPack, + assembleResourcePack, + deployToBDS, + disablePackInWorld, + enablePackInWorld, + ensureConfigPermission, + readLevelNameSync, + readPackManifestHeader, + readWorldPackList, + worldPackListHas as pmWorldPackListHas, +} from "@sfmc-bds/bds-tools/pack-manager-lib"; +import { ROOT, PACKAGES_DIR, resolveSdkPackageRoot } from "./runtime.js"; import { c } from "./theme.js"; export const BP_NAME = "sfmc-modules"; @@ -137,25 +149,14 @@ function catalogPath(): string { return modulePath(path.join(ROOT, "modules"), "catalog.json"); } -function spawnPackManager(args: string[]): { ok: boolean; out: string; err: string } { - const proc = spawnServiceSync("pack-manager", args, { encoding: "utf8" }); - return { - ok: proc.status === 0, - out: (proc.stdout ?? "").toString(), - err: (proc.stderr ?? "").toString(), - }; -} - -/** 读 BDS 路径与 level-name(level 权威来源:pack-manager read-level,DRY/DIP) */ +/** 读 BDS 路径与 level-name(level 权威:pack-manager-lib.readLevelNameSync,DIP) */ export function resolveBdsContext(): { bdsRoot: string; levelName: string } { const cfg = (readJson(configPath(ROOT, "bds_updater.json")) ?? {}) as BdsUpdaterConfig; const bdsRoot = cfg.bds_path; if (!bdsRoot) { throw new Error("bds_path not configured. Run `sfmc init` first."); } - const r = spawnPackManager(["read-level", "--bds-root", bdsRoot]); - const levelName = r.ok && r.out.trim() ? r.out.trim() : "Bedrock level"; - return { bdsRoot, levelName }; + return { bdsRoot, levelName: readLevelNameSync(bdsRoot) }; } export function deployedBpDir(bdsRoot: string, levelName: string): string { @@ -170,20 +171,11 @@ export function deployedCatalogPath(bdsRoot: string, levelName: string): string return path.join(deployedBpDir(bdsRoot, levelName), DEPLOY_CATALOG_NAME); } -/** 读 BP/RP manifest header — 委托 pack-manager(与 readPackManifestHeader 单一权威) */ +/** 读 BP/RP manifest header — 直连 pack-manager-lib(与 CLI read-manifest 同一权威,DRY/DIP) */ function readManifestHeader( packDir: string ): { uuid: string; version: [number, number, number]; moduleUuid?: string } | null { - if (!existsSync(path.join(packDir, "manifest.json"))) return null; - const r = spawnPackManager(["read-manifest", "--pack-dir", packDir]); - if (!r.ok) return null; - const text = r.out.trim(); - if (!text || text === "null") return null; - try { - return JSON.parse(text) as { uuid: string; version: [number, number, number]; moduleUuid?: string }; - } catch { - return null; - } + return readPackManifestHeader(packDir); } /** 解析启用状态:lock 优先,否则 catalog.enabledByDefault(缺省 true↔!==false),未收录模块 false */ @@ -193,30 +185,6 @@ function isModuleEnabled(logicalId: string, lock: ModuleLock, catalogDefaults: M return catalogDefaults.get(logicalId) ?? false; } -/** 世界 enable/disable 清单 — 统一 argv 形状(DRY) */ -function spawnWorldPack( - verb: "enable-pack" | "disable-pack", - worldsDir: string, - levelName: string, - kind: "behavior" | "resource", - packId: string, - version?: [number, number, number] -): { ok: boolean; out: string; err: string } { - const args = [ - verb, - "--worlds-dir", - worldsDir, - "--level", - levelName, - "--kind", - kind, - "--pack-id", - packId, - ]; - if (version) args.push("--version", version.join(",")); - return spawnPackManager(args); -} - /** * 部署前收集世界内已出现的 BP/RP uuid。 * 同时读 deploy-catalog 与磁盘 manifest — catalog 缺失时仍能卸过期清单项(Demeter/完整契约)。 @@ -424,20 +392,9 @@ export function catalogsEqual(a: DeployCatalog, b: DeployCatalog): boolean { return true; } -/** 世界 enable-list 查询 — 委托 pack-manager has-pack(DRY) */ +/** 世界 enable-list 查询 — 直连 pack-manager-lib(DRY/DIP,与 CLI has-pack 同契约) */ function worldPackListHas(bdsRoot: string, levelName: string, kind: "behavior" | "resource", uuid: string): boolean { - const r = spawnPackManager([ - "has-pack", - "--worlds-dir", - path.join(bdsRoot, "worlds"), - "--level", - levelName, - "--kind", - kind, - "--pack-id", - uuid, - ]); - return r.ok && r.out.trim() === "1"; + return pmWorldPackListHas(path.join(bdsRoot, "worlds"), levelName, kind, uuid); } /** esbuild 聚合启用模块 → assemble BP + RP(若有) */ @@ -489,49 +446,36 @@ export async function buildPacks(desired?: DeployCatalog): Promise 0 && catalog.rpUuid) { - /* OCP:显式 --modules-json 交给 pack-manager,不再临时镜像整树 */ - const mapFile = path.join(buildRoot(), "_rp-modules-map.json"); - writeJson(mapFile, rpDirs); - const rpArgs = [ - "assemble-rp", - "--modules-json", - mapFile, - "--out", - rpOut(), - "--name", - RP_NAME, - "--version", - (catalog.rpVersion ?? DEFAULT_PACK_VERSION).join(","), - "--description", - "ScriptsForMinecraftServer aggregated resource pack", - "--uuid", - catalog.rpUuid, - ]; - if (catalog.rpModuleUuid) rpArgs.push("--module-uuid", catalog.rpModuleUuid); - const rpProc = spawnPackManager(rpArgs); - await fs.rm(mapFile, { force: true }); - if (!rpProc.ok) throw new Error(`assemble-rp failed: ${rpProc.err || rpProc.out}`); + /* OCP:显式 module→rpDir map 直传库,不再临时镜像整树 / 落盘 JSON */ + try { + await assembleResourcePack({ + moduleResourceDirs: rpDirs, + outDir: rpOut(), + projectName: RP_NAME, + version: catalog.rpVersion ?? DEFAULT_PACK_VERSION, + description: "ScriptsForMinecraftServer aggregated resource pack", + uuid: catalog.rpUuid, + ...(catalog.rpModuleUuid ? { moduleUuid: catalog.rpModuleUuid } : {}), + }); + } catch (e) { + throw new Error(`assemble-rp failed: ${(e as Error).message}`); + } packLog(`assembled RP uuid=${catalog.rpUuid} (${Object.keys(rpDirs).length} modules)`, "success"); } else { /* 无 RP:清理旧产物,catalog.rpUuid 置空 */ @@ -556,81 +500,98 @@ export async function deployPacks(catalog: DeployCatalog): Promise { /* 必须在 deploy/clear-rp 之前采集 — 清目录后无法再读 manifest */ const previous = collectDeployedPackUuids(bdsRoot, levelName); - const deployArgs = [ - "deploy", - "--bds-root", - bdsRoot, - "--level", - levelName, - "--bp-src", - bpOut(), - "--bp-name", - BP_NAME, - "--rp-name", - RP_NAME, - ]; - if (catalog.rpUuid && existsSync(rpOut())) { - deployArgs.push("--rp-src", rpOut()); - } else { - /* 不再需要 RP:清世界内聚合 RP 目录 */ - deployArgs.push("--clear-rp"); + try { + await deployToBDS({ + bdsRoot, + levelName, + behaviorPackSrc: bpOut(), + bpName: BP_NAME, + rpName: RP_NAME, + ...(catalog.rpUuid && existsSync(rpOut()) + ? { resourcePackSrc: rpOut() } + : { clearResourcePack: true }), + }); + } catch (e) { + throw new Error(`deploy failed: ${(e as Error).message}`); } - const dep = spawnPackManager(deployArgs); - if (!dep.ok) throw new Error(`deploy failed: ${dep.err || dep.out}`); packLog(`deployed to worlds/${levelName}`, "success"); /* 确保 catalog 在部署后的 BP 内(deploy 会拷贝整个目录) */ writeJson(path.join(deployedBpDir(bdsRoot, levelName), DEPLOY_CATALOG_NAME), catalog); const worldsDir = path.join(bdsRoot, "worlds"); - const enBp = spawnWorldPack("enable-pack", worldsDir, levelName, "behavior", catalog.bpUuid, catalog.bpVersion); - if (!enBp.ok) throw new Error(`enable BP failed: ${enBp.err || enBp.out}`); + try { + await enablePackInWorld({ + worldsDir, + levelName, + kind: "behavior", + packUuid: catalog.bpUuid, + version: catalog.bpVersion, + }); + } catch (e) { + throw new Error(`enable BP failed: ${(e as Error).message}`); + } packLog(`enabled behavior pack ${catalog.bpUuid} in world list`, "success"); if (catalog.rpUuid && catalog.rpVersion) { - const enRp = spawnWorldPack( - "enable-pack", - worldsDir, - levelName, - "resource", - catalog.rpUuid, - catalog.rpVersion - ); - if (!enRp.ok) throw new Error(`enable RP failed: ${enRp.err || enRp.out}`); + try { + await enablePackInWorld({ + worldsDir, + levelName, + kind: "resource", + packUuid: catalog.rpUuid, + version: catalog.rpVersion, + }); + } catch (e) { + throw new Error(`enable RP failed: ${(e as Error).message}`); + } packLog(`enabled resource pack ${catalog.rpUuid} in world list`, "success"); } /* 卸掉过期 RP:UUID 轮换 / 不再提供 RP / catalog 缺失但磁盘仍有旧包 */ for (const staleRp of previous.rp) { if (staleRp === (catalog.rpUuid ?? null)) continue; - const dis = spawnWorldPack("disable-pack", worldsDir, levelName, "resource", staleRp); - if (!dis.ok) { - packLog(`disable stale RP ${staleRp} failed: ${dis.err || dis.out}`, "warn"); - } else { + try { + await disablePackInWorld({ + worldsDir, + levelName, + kind: "resource", + packUuid: staleRp, + version: DEFAULT_PACK_VERSION, + }); packLog(`disabled stale resource pack ${staleRp}`, "success"); + } catch (e) { + packLog(`disable stale RP ${staleRp} failed: ${(e as Error).message}`, "warn"); } } /* 卸掉过期 BP(uuid 轮换) */ for (const staleBp of previous.bp) { if (staleBp === catalog.bpUuid) continue; - const disBp = spawnWorldPack("disable-pack", worldsDir, levelName, "behavior", staleBp); - if (!disBp.ok) { - packLog(`disable stale BP ${staleBp} failed: ${disBp.err || disBp.out}`, "warn"); - } else { + try { + await disablePackInWorld({ + worldsDir, + levelName, + kind: "behavior", + packUuid: staleBp, + version: DEFAULT_PACK_VERSION, + }); packLog(`disabled stale behavior pack ${staleBp}`, "success"); + } catch (e) { + packLog(`disable stale BP ${staleBp} failed: ${(e as Error).message}`, "warn"); } } - const perm = spawnPackManager([ - "ensure-permission", - "--bds-root", - bdsRoot, - "--pack-id", - catalog.bpUuid, - ]); - if (!perm.ok) throw new Error(`ensure-permission failed: ${perm.err || perm.out}`); - packLog(perm.out.trim() || `config/${catalog.bpUuid}/permission.json ok`); + try { + const wrote = await ensureConfigPermission(bdsRoot, catalog.bpUuid); + packLog( + wrote + ? `wrote config/${catalog.bpUuid}/permission.json` + : `config/${catalog.bpUuid}/permission.json already exists — skipped` + ); + } catch (e) { + throw new Error(`ensure-permission failed: ${(e as Error).message}`); + } } export function formatPackLoadInfo(catalog: DeployCatalog, rebuilt: boolean): string { @@ -707,15 +668,16 @@ export async function ensurePacksReady(): Promise { } if (!existsSync(path.join(bdsRoot, "config", deployed.bpUuid, "permission.json"))) { /* permission 缺失只补写,不强制整包重编 */ - const perm = spawnPackManager([ - "ensure-permission", - "--bds-root", - bdsRoot, - "--pack-id", - deployed.bpUuid, - ]); - if (!perm.ok) throw new Error(`ensure-permission failed: ${perm.err || perm.out}`); - packLog(perm.out.trim()); + try { + const wrote = await ensureConfigPermission(bdsRoot, deployed.bpUuid); + packLog( + wrote + ? `wrote config/${deployed.bpUuid}/permission.json` + : `config/${deployed.bpUuid}/permission.json already exists — skipped` + ); + } catch (e) { + throw new Error(`ensure-permission failed: ${(e as Error).message}`); + } } } @@ -822,27 +784,10 @@ export async function cmdPackList(_args: string[]): Promise { const worldsDir = path.join(bdsRoot, "worlds"); lines.push(c.bold("\nWorld enable lists")); for (const kind of ["behavior", "resource"] as const) { - const r = spawnPackManager([ - "list-packs", - "--worlds-dir", - worldsDir, - "--level", - levelName, - "--kind", - kind, - ]); - if (!r.ok) { - lines.push(c.yellow(` ${kind}: (list-packs failed)`)); - continue; - } - try { - const arr = JSON.parse(r.out.trim() || "[]") as Array<{ pack_id: string; version: number[] }>; - if (!arr.length) lines.push(c.dim(` ${kind}: (empty)`)); - for (const e of arr) { - lines.push(` ${kind}: ${e.pack_id} v${(e.version ?? []).join(".")}`); - } - } catch { - lines.push(c.yellow(` ${kind}: (corrupt)`)); + const arr = readWorldPackList(worldsDir, levelName, kind); + if (!arr.length) lines.push(c.dim(` ${kind}: (empty)`)); + for (const e of arr) { + lines.push(` ${kind}: ${e.pack_id} v${(e.version ?? []).join(".")}`); } } return lines.join("\n") + "\n"; @@ -861,9 +806,13 @@ export async function cmdPackEnableDisable(action: "enable" | "disable", args: s const version = kind === "behavior" ? deployed.bpVersion : deployed.rpVersion; if (!uuid || !version) return c.red(t("pack.noUuid", { kind })); const worldsDir = path.join(bdsRoot, "worlds"); - const verb = action === "enable" ? "enable-pack" : "disable-pack"; - const r = spawnWorldPack(verb, worldsDir, levelName, kind, uuid, version); - if (!r.ok) return c.red(t("pack.actionFailed", { action, detail: r.err || r.out })); + try { + const opts = { worldsDir, levelName, kind, packUuid: uuid, version }; + if (action === "enable") await enablePackInWorld(opts); + else await disablePackInWorld(opts); + } catch (e) { + return c.red(t("pack.actionFailed", { action, detail: (e as Error).message })); + } packLog(`${action}d ${kind} pack ${uuid}`, "success"); return c.green( action === "enable"