@sfmc-bds/cli 0.1.0 → 0.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +7 -1
  2. package/dist/cli-result.d.ts +12 -0
  3. package/dist/cli-result.d.ts.map +1 -0
  4. package/dist/cli-result.js +15 -0
  5. package/dist/cli-result.js.map +7 -0
  6. package/dist/commands-behavior-pack.d.ts +11 -42
  7. package/dist/commands-behavior-pack.d.ts.map +1 -1
  8. package/dist/commands-behavior-pack.js +40 -159
  9. package/dist/commands-behavior-pack.js.map +7 -1
  10. package/dist/commands-reload.d.ts +9 -0
  11. package/dist/commands-reload.d.ts.map +1 -0
  12. package/dist/commands-reload.js +30 -0
  13. package/dist/commands-reload.js.map +7 -0
  14. package/dist/commands.d.ts +7 -0
  15. package/dist/commands.d.ts.map +1 -1
  16. package/dist/commands.js +182 -172
  17. package/dist/commands.js.map +7 -1
  18. package/dist/i18n/index.d.ts +26 -0
  19. package/dist/i18n/index.d.ts.map +1 -0
  20. package/dist/i18n/index.js +107 -0
  21. package/dist/i18n/index.js.map +7 -0
  22. package/dist/i18n/locales/en.d.ts +381 -0
  23. package/dist/i18n/locales/en.d.ts.map +1 -0
  24. package/dist/i18n/locales/en.js +393 -0
  25. package/dist/i18n/locales/en.js.map +7 -0
  26. package/dist/i18n/locales/zh-CN.d.ts +4 -0
  27. package/dist/i18n/locales/zh-CN.d.ts.map +1 -0
  28. package/dist/i18n/locales/zh-CN.js +379 -0
  29. package/dist/i18n/locales/zh-CN.js.map +7 -0
  30. package/dist/i18n/types.d.ts +7 -0
  31. package/dist/i18n/types.d.ts.map +1 -0
  32. package/dist/i18n/types.js +17 -0
  33. package/dist/i18n/types.js.map +7 -0
  34. package/dist/index.d.ts +5 -11
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +51 -43
  37. package/dist/index.js.map +7 -1
  38. package/dist/interactive-prompts.d.ts +8 -0
  39. package/dist/interactive-prompts.d.ts.map +1 -0
  40. package/dist/interactive-prompts.js +57 -0
  41. package/dist/interactive-prompts.js.map +7 -0
  42. package/dist/locale-command.d.ts +3 -0
  43. package/dist/locale-command.d.ts.map +1 -0
  44. package/dist/locale-command.js +19 -0
  45. package/dist/locale-command.js.map +7 -0
  46. package/dist/logs.d.ts +22 -4
  47. package/dist/logs.d.ts.map +1 -1
  48. package/dist/logs.js +208 -193
  49. package/dist/logs.js.map +7 -1
  50. package/dist/main.js +135 -173
  51. package/dist/main.js.map +7 -1
  52. package/dist/module-commands.d.ts +43 -4
  53. package/dist/module-commands.d.ts.map +1 -1
  54. package/dist/module-commands.js +440 -327
  55. package/dist/module-commands.js.map +7 -1
  56. package/dist/module-fingerprint.d.ts +7 -0
  57. package/dist/module-fingerprint.d.ts.map +1 -0
  58. package/dist/module-fingerprint.js +31 -0
  59. package/dist/module-fingerprint.js.map +7 -0
  60. package/dist/module-wizard.d.ts +14 -0
  61. package/dist/module-wizard.d.ts.map +1 -0
  62. package/dist/module-wizard.js +313 -0
  63. package/dist/module-wizard.js.map +7 -0
  64. package/dist/pack-lifecycle.d.ts +95 -0
  65. package/dist/pack-lifecycle.d.ts.map +1 -0
  66. package/dist/pack-lifecycle.js +634 -0
  67. package/dist/pack-lifecycle.js.map +7 -0
  68. package/dist/pack-update/bindings.d.ts +12 -0
  69. package/dist/pack-update/bindings.d.ts.map +1 -0
  70. package/dist/pack-update/bindings.js +64 -0
  71. package/dist/pack-update/bindings.js.map +7 -0
  72. package/dist/pack-update/config.d.ts +22 -0
  73. package/dist/pack-update/config.d.ts.map +1 -0
  74. package/dist/pack-update/config.js +141 -0
  75. package/dist/pack-update/config.js.map +7 -0
  76. package/dist/pack-update/providers/curseforge.d.ts +23 -0
  77. package/dist/pack-update/providers/curseforge.d.ts.map +1 -0
  78. package/dist/pack-update/providers/curseforge.js +209 -0
  79. package/dist/pack-update/providers/curseforge.js.map +7 -0
  80. package/dist/pack-update/providers/index.d.ts +16 -0
  81. package/dist/pack-update/providers/index.d.ts.map +1 -0
  82. package/dist/pack-update/providers/index.js +37 -0
  83. package/dist/pack-update/providers/index.js.map +7 -0
  84. package/dist/pack-update/service.d.ts +21 -0
  85. package/dist/pack-update/service.d.ts.map +1 -0
  86. package/dist/pack-update/service.js +567 -0
  87. package/dist/pack-update/service.js.map +7 -0
  88. package/dist/pack-update/types.d.ts +126 -0
  89. package/dist/pack-update/types.d.ts.map +1 -0
  90. package/dist/pack-update/types.js +8 -0
  91. package/dist/pack-update/types.js.map +7 -0
  92. package/dist/pack-update/version-policy.d.ts +80 -0
  93. package/dist/pack-update/version-policy.d.ts.map +1 -0
  94. package/dist/pack-update/version-policy.js +216 -0
  95. package/dist/pack-update/version-policy.js.map +7 -0
  96. package/dist/registry.d.ts +12 -4
  97. package/dist/registry.d.ts.map +1 -1
  98. package/dist/registry.js +73 -106
  99. package/dist/registry.js.map +7 -1
  100. package/dist/remote-agent.d.ts.map +1 -1
  101. package/dist/remote-agent.js +160 -176
  102. package/dist/remote-agent.js.map +7 -1
  103. package/dist/repl.d.ts +4 -0
  104. package/dist/repl.d.ts.map +1 -1
  105. package/dist/repl.js +683 -605
  106. package/dist/repl.js.map +7 -1
  107. package/dist/runtime.d.ts +40 -14
  108. package/dist/runtime.d.ts.map +1 -1
  109. package/dist/runtime.js +185 -62
  110. package/dist/runtime.js.map +7 -1
  111. package/dist/services.d.ts +2 -0
  112. package/dist/services.d.ts.map +1 -1
  113. package/dist/services.js +251 -238
  114. package/dist/services.js.map +7 -1
  115. package/dist/sfmc-modules-root.d.ts +24 -0
  116. package/dist/sfmc-modules-root.d.ts.map +1 -0
  117. package/dist/sfmc-modules-root.js +70 -0
  118. package/dist/sfmc-modules-root.js.map +7 -0
  119. package/dist/theme.d.ts +4 -0
  120. package/dist/theme.d.ts.map +1 -1
  121. package/dist/theme.js +134 -48
  122. package/dist/theme.js.map +7 -1
  123. package/dist/wizard.d.ts.map +1 -1
  124. package/dist/wizard.js +363 -438
  125. package/dist/wizard.js.map +7 -1
  126. package/dist/world-packs.d.ts +22 -0
  127. package/dist/world-packs.d.ts.map +1 -0
  128. package/dist/world-packs.js +872 -0
  129. package/dist/world-packs.js.map +7 -0
  130. package/package.json +10 -10
  131. package/dist/dispatcher.d.ts +0 -13
  132. package/dist/dispatcher.d.ts.map +0 -1
  133. package/dist/dispatcher.js +0 -23
  134. package/dist/dispatcher.js.map +0 -1
  135. package/dist/terminal.d.ts +0 -3
  136. package/dist/terminal.d.ts.map +0 -1
  137. package/dist/terminal.js +0 -178
  138. package/dist/terminal.js.map +0 -1
@@ -0,0 +1,872 @@
1
+ import { confirm, isCancel, multiselect } from "@clack/prompts";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import {
6
+ bumpPackPatchVersion,
7
+ discoverPackRoots,
8
+ disableInstalledPack,
9
+ enableInstalledPack,
10
+ extractArchiveToTemp,
11
+ findInstalledPackById,
12
+ installPackDirectory,
13
+ isPackArchive,
14
+ listInstalledWorldPacks,
15
+ listWorldEnableListResult,
16
+ readPackDependencyUuids,
17
+ readPackManifestInfo,
18
+ uninstallInstalledPack,
19
+ worldPackParentDir
20
+ } from "@sfmc-bds/bds-tools/world-packs";
21
+ import { t } from "./i18n/index.js";
22
+ import { BP_NAME, RP_NAME, resolveBdsContext } from "./pack-lifecycle.js";
23
+ import {
24
+ bindPackSource,
25
+ bindingLabelForUuid,
26
+ checkPackUpdates,
27
+ formatSourcesList,
28
+ probeSourceAfterInstall,
29
+ searchRemote
30
+ } from "./pack-update/service.js";
31
+ import { getBinding, removeBinding, packSourcesPath } from "./pack-update/bindings.js";
32
+ import {
33
+ resolvePackTrashDir,
34
+ resolveUninstallTrashDir
35
+ } from "./pack-update/config.js";
36
+ import { ROOT } from "./runtime.js";
37
+ import { c } from "./theme.js";
38
+ const PACKS_CMD_NAMES = ["packs", "addon"];
39
+ const PACKS_SUBCOMMANDS = [
40
+ "list",
41
+ "search",
42
+ "enable",
43
+ "disable",
44
+ "uninstall",
45
+ "bump",
46
+ "install",
47
+ "scan",
48
+ "doctor",
49
+ "path",
50
+ "bind",
51
+ "unbind",
52
+ "sources",
53
+ "check",
54
+ "update"
55
+ ];
56
+ function restartHint() {
57
+ return t("packs.restartHint");
58
+ }
59
+ function rejoinHint() {
60
+ return t("packs.rejoinHint");
61
+ }
62
+ function isPacksCommand(cmd) {
63
+ return !!cmd && PACKS_CMD_NAMES.includes(cmd);
64
+ }
65
+ function packsInboxDir() {
66
+ return path.join(ROOT, "packs");
67
+ }
68
+ function doneDir() {
69
+ return path.join(packsInboxDir(), "_done");
70
+ }
71
+ function failedDir() {
72
+ return path.join(packsInboxDir(), "_failed");
73
+ }
74
+ function statePath() {
75
+ return path.join(packsInboxDir(), "inbox-state.json");
76
+ }
77
+ function ensureInboxLayout() {
78
+ for (const d of [packsInboxDir(), doneDir(), failedDir(), resolvePackTrashDir()]) {
79
+ fs.mkdirSync(d, { recursive: true });
80
+ }
81
+ }
82
+ function readState() {
83
+ const file = statePath();
84
+ if (!fs.existsSync(file)) return { installed: {} };
85
+ try {
86
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
87
+ return { installed: raw.installed ?? {} };
88
+ } catch {
89
+ return { installed: {} };
90
+ }
91
+ }
92
+ function writeState(state) {
93
+ ensureInboxLayout();
94
+ fs.writeFileSync(statePath(), `${JSON.stringify(state, null, 2)}
95
+ `, "utf8");
96
+ }
97
+ function sourceFingerprint(absPath) {
98
+ const st = fs.statSync(absPath);
99
+ const payload = `${absPath}|${st.size}|${st.mtimeMs}`;
100
+ return crypto.createHash("sha1").update(payload).digest("hex");
101
+ }
102
+ function stampName(name) {
103
+ const safe = name.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").slice(0, 80);
104
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
105
+ return `${ts}-${safe || "pack"}`;
106
+ }
107
+ function moveTo(src, destParent, label) {
108
+ fs.mkdirSync(destParent, { recursive: true });
109
+ const dest = path.join(destParent, stampName(label));
110
+ fs.renameSync(src, dest);
111
+ return dest;
112
+ }
113
+ function fmtVer(v) {
114
+ return v.join(".");
115
+ }
116
+ function pruneInboxStateByUuid(uuid) {
117
+ const state = readState();
118
+ let changed = false;
119
+ for (const [fp, meta] of Object.entries(state.installed)) {
120
+ if (meta.uuid.toLowerCase() === uuid.toLowerCase()) {
121
+ delete state.installed[fp];
122
+ changed = true;
123
+ }
124
+ }
125
+ if (changed) writeState(state);
126
+ }
127
+ function isProtectedSfmcPack(pack) {
128
+ const folder = pack.folderName.toLowerCase();
129
+ return folder === BP_NAME.toLowerCase() || folder === RP_NAME.toLowerCase();
130
+ }
131
+ function formatUninstallOutcome(r) {
132
+ if (r.status === "protected") {
133
+ return c.red(t("packs.uninstall.protected", { folder: r.folder }));
134
+ }
135
+ if (r.status === "trashed") {
136
+ return c.green(t("packs.uninstall.trashed", { folder: r.folder, dest: r.dest }));
137
+ }
138
+ if (r.status === "missing") {
139
+ return c.yellow(t("packs.uninstall.dirMissing", { folder: r.folder }));
140
+ }
141
+ return c.green(t("packs.uninstall.deleted", { folder: r.folder }));
142
+ }
143
+ async function uninstallOneInstalledPack(pack, opts) {
144
+ if (isProtectedSfmcPack(pack)) {
145
+ return { status: "protected", folder: pack.folderName };
146
+ }
147
+ const trash = resolveUninstallTrashDir({ purge: opts.purge });
148
+ const r = await uninstallInstalledPack({
149
+ bdsRoot: opts.bdsRoot,
150
+ levelName: opts.levelName,
151
+ pack,
152
+ trashDir: trash
153
+ });
154
+ pruneInboxStateByUuid(pack.uuid);
155
+ if (r.action === "trashed") {
156
+ return { status: "trashed", folder: pack.folderName, dest: r.dest };
157
+ }
158
+ if (r.action === "missing") {
159
+ return { status: "missing", folder: pack.folderName };
160
+ }
161
+ return { status: "deleted", folder: pack.folderName };
162
+ }
163
+ function resolvePairedResource(pack, packs, noPaired) {
164
+ if (pack.kind !== "behavior" || noPaired) return null;
165
+ const binding = getBinding(pack.uuid);
166
+ const pairedUuid = binding?.pairedResourceUuid ?? (fs.existsSync(pack.dir) ? readPackDependencyUuids(pack.dir)[0] : void 0) ?? null;
167
+ if (!pairedUuid) return null;
168
+ return packs.find((p) => p.kind === "resource" && p.uuid.toLowerCase() === pairedUuid.toLowerCase()) ?? null;
169
+ }
170
+ function formatUninstallPickLabel(pack) {
171
+ const kind = pack.kind === "behavior" ? "BP" : "RP";
172
+ const en = pack.enabled ? t("packs.list.on") : t("packs.list.off");
173
+ return `[${kind}] ${pack.name} ${pack.folderName} ${en.trim()} ${pack.uuid.slice(0, 8)}`;
174
+ }
175
+ async function pickPacksForUninstall(packs) {
176
+ const candidates = packs.filter((p) => !isProtectedSfmcPack(p));
177
+ if (candidates.length === 0) return [];
178
+ const bp = candidates.filter((p) => p.kind === "behavior");
179
+ const rp = candidates.filter((p) => p.kind === "resource");
180
+ const options = [
181
+ ...bp.map((p) => ({ value: p.uuid, label: formatUninstallPickLabel(p) })),
182
+ ...rp.map((p) => ({ value: p.uuid, label: formatUninstallPickLabel(p) }))
183
+ ];
184
+ const selected = await multiselect({
185
+ message: t("packs.uninstall.pick"),
186
+ options,
187
+ required: true
188
+ });
189
+ if (isCancel(selected)) return null;
190
+ const set = new Set(selected);
191
+ return candidates.filter((p) => set.has(p.uuid));
192
+ }
193
+ async function uninstallPacksBatch(selected, allPacks, opts) {
194
+ if (selected.length === 0) return c.yellow(t("packs.uninstall.none"));
195
+ const queue = [];
196
+ const seen = /* @__PURE__ */ new Set();
197
+ for (const pack of selected) {
198
+ const key = pack.uuid.toLowerCase();
199
+ if (seen.has(key)) continue;
200
+ seen.add(key);
201
+ queue.push({ pack });
202
+ if (isProtectedSfmcPack(pack)) continue;
203
+ const paired = resolvePairedResource(pack, allPacks, opts.noPaired);
204
+ if (paired) {
205
+ const pk = paired.uuid.toLowerCase();
206
+ if (!seen.has(pk)) {
207
+ seen.add(pk);
208
+ queue.push({ pack: paired, pairedOf: pack.folderName });
209
+ }
210
+ }
211
+ }
212
+ if (opts.askConfirm && process.stdin.isTTY) {
213
+ const preview = queue.map((q) => {
214
+ const kind = q.pack.kind === "behavior" ? "BP" : "RP";
215
+ const extra = q.pairedOf ? ` ${c.dim(t("packs.uninstall.pairedOf", { folder: q.pairedOf }))}` : "";
216
+ return ` [${kind}] ${q.pack.folderName}${extra}`;
217
+ }).join("\n");
218
+ console.log(t("packs.uninstall.summary", { count: queue.length }));
219
+ console.log(preview);
220
+ const ans = await confirm({
221
+ message: t("packs.uninstall.confirm", { count: String(queue.length) }),
222
+ initialValue: false
223
+ });
224
+ if (isCancel(ans) || !ans) return c.dim(t("packs.uninstall.cancelled"));
225
+ }
226
+ const lines = [];
227
+ for (const item of queue) {
228
+ const outcome = await uninstallOneInstalledPack(item.pack, opts);
229
+ lines.push(formatUninstallOutcome(outcome));
230
+ if (item.pack.kind === "behavior" && outcome.status !== "protected") {
231
+ const removed = removeBinding(item.pack.uuid);
232
+ if (removed) {
233
+ lines.push(c.dim(t("packs.uninstall.bindingRemoved", { uuid: item.pack.uuid })));
234
+ }
235
+ }
236
+ }
237
+ lines.push(c.yellow(restartHint()));
238
+ return lines.join("\n");
239
+ }
240
+ function printConflict(existing, incoming) {
241
+ console.log(c.yellow(t("packs.conflict")));
242
+ console.log(
243
+ t("packs.conflict.existing", {
244
+ name: existing.name,
245
+ version: fmtVer(existing.version),
246
+ uuid: existing.uuid,
247
+ dir: existing.dir
248
+ })
249
+ );
250
+ console.log(
251
+ t("packs.conflict.incoming", {
252
+ name: incoming.name,
253
+ version: fmtVer(incoming.version),
254
+ uuid: incoming.uuid
255
+ })
256
+ );
257
+ }
258
+ async function askOverwrite() {
259
+ if (!process.stdin.isTTY) return false;
260
+ const ans = await confirm({
261
+ message: t("packs.overwriteAsk"),
262
+ initialValue: false
263
+ });
264
+ if (isCancel(ans)) return false;
265
+ return !!ans;
266
+ }
267
+ async function installOnePackRoot(opts) {
268
+ const info = readPackManifestInfo(opts.srcDir);
269
+ if (!info) {
270
+ return { ok: false, reason: t("packs.badManifest") };
271
+ }
272
+ const destParent = worldPackParentDir(opts.bdsRoot, opts.levelName, info.kind);
273
+ let force = !!opts.force;
274
+ let result = await installPackDirectory({
275
+ srcDir: opts.srcDir,
276
+ destParent,
277
+ force: false,
278
+ ...opts.folderHint ? { folderName: opts.folderHint } : {}
279
+ });
280
+ if (result.conflict) {
281
+ printConflict(result.conflict.existing, result.conflict.incoming);
282
+ if (force) {
283
+ } else if (opts.interactive && process.stdin.isTTY) {
284
+ force = await askOverwrite();
285
+ if (!force) {
286
+ console.log(c.dim(t("packs.skippedNoOverwrite")));
287
+ return { ok: false, skipped: true, reason: "conflict:skipped", info };
288
+ }
289
+ } else {
290
+ console.log(c.yellow(t("packs.conflictSkip", { name: info.name })));
291
+ return { ok: false, skipped: true, reason: "conflict:noninteractive", info };
292
+ }
293
+ result = await installPackDirectory({
294
+ srcDir: opts.srcDir,
295
+ destParent,
296
+ force: true,
297
+ ...opts.folderHint ? { folderName: opts.folderHint } : {}
298
+ });
299
+ }
300
+ if (!result.ok || !result.info || !result.destDir) {
301
+ return { ok: false, reason: result.reason ?? "install failed", info };
302
+ }
303
+ await enableInstalledPack({
304
+ bdsRoot: opts.bdsRoot,
305
+ levelName: opts.levelName,
306
+ info: result.info
307
+ });
308
+ try {
309
+ await probeSourceAfterInstall({
310
+ info: result.info,
311
+ packDir: result.destDir,
312
+ ...result.folderName ? { folderName: result.folderName } : {},
313
+ ...opts.interactive !== void 0 ? { interactive: opts.interactive } : {}
314
+ });
315
+ } catch (e) {
316
+ console.log(c.yellow(`[packs] probe: ${e.message}`));
317
+ }
318
+ return { ok: true, info: result.info };
319
+ }
320
+ function listInboxCandidates() {
321
+ const inbox = packsInboxDir();
322
+ if (!fs.existsSync(inbox)) return [];
323
+ const out = [];
324
+ for (const e of fs.readdirSync(inbox, { withFileTypes: true })) {
325
+ if (e.name.startsWith("_") || e.name === "inbox-state.json") continue;
326
+ const full = path.join(inbox, e.name);
327
+ if (e.isDirectory()) out.push(full);
328
+ else if (e.isFile() && isPackArchive(full)) out.push(full);
329
+ }
330
+ return out;
331
+ }
332
+ async function scanAndInstallInbox(opts) {
333
+ ensureInboxLayout();
334
+ const candidates = listInboxCandidates();
335
+ if (candidates.length === 0) {
336
+ return { installed: 0, skipped: 0, failed: 0 };
337
+ }
338
+ let bdsRoot;
339
+ let levelName;
340
+ try {
341
+ ({ bdsRoot, levelName } = resolveBdsContext());
342
+ } catch (e) {
343
+ console.log(c.yellow(t("packs.inboxSkip", { message: e.message })));
344
+ return { installed: 0, skipped: 0, failed: candidates.length };
345
+ }
346
+ const interactive = opts?.interactive ?? !!process.stdin.isTTY;
347
+ const force = !!opts?.force;
348
+ const dryRun = !!opts?.dryRun;
349
+ const state = readState();
350
+ let installed = 0;
351
+ let skipped = 0;
352
+ let failed = 0;
353
+ console.log(c.dim(t("packs.inboxFound", { count: candidates.length, level: levelName })));
354
+ for (const src of candidates) {
355
+ const base = path.basename(src);
356
+ const fp = sourceFingerprint(src);
357
+ if (state.installed[fp] && !force) {
358
+ console.log(c.dim(t("packs.alreadyDone", { name: base })));
359
+ skipped++;
360
+ if (!dryRun) {
361
+ try {
362
+ moveTo(src, doneDir(), base);
363
+ } catch {
364
+ }
365
+ }
366
+ continue;
367
+ }
368
+ let tempDir = null;
369
+ try {
370
+ let roots = [];
371
+ if (fs.statSync(src).isDirectory()) {
372
+ roots = discoverPackRoots(src, { maxDepth: 2 });
373
+ } else if (isPackArchive(src)) {
374
+ if (dryRun) {
375
+ console.log(c.dim(t("packs.dryRunExtract", { name: base })));
376
+ skipped++;
377
+ continue;
378
+ }
379
+ tempDir = await extractArchiveToTemp(src);
380
+ roots = discoverPackRoots(tempDir, { maxDepth: 2 });
381
+ } else {
382
+ failed++;
383
+ if (!dryRun) moveTo(src, failedDir(), base);
384
+ continue;
385
+ }
386
+ if (roots.length === 0) {
387
+ console.log(c.red(t("packs.noPackRoot", { name: base })));
388
+ failed++;
389
+ if (!dryRun && !tempDir) moveTo(src, failedDir(), `${base}-no-root`);
390
+ continue;
391
+ }
392
+ if (dryRun) {
393
+ for (const r of roots) {
394
+ const info = readPackManifestInfo(r);
395
+ console.log(
396
+ c.dim(
397
+ `[packs] dry-run: ${base} \u2192 ${info?.kind ?? "?"} ${info?.name ?? path.basename(r)}`
398
+ )
399
+ );
400
+ }
401
+ skipped++;
402
+ continue;
403
+ }
404
+ let okCount = 0;
405
+ let skipCount = 0;
406
+ let failCount = 0;
407
+ let lastUuid = "";
408
+ for (const root of roots) {
409
+ const folderHint = roots.length === 1 && fs.statSync(src).isFile() ? path.basename(src, path.extname(src)) : path.basename(root);
410
+ const r = await installOnePackRoot({
411
+ srcDir: root,
412
+ bdsRoot,
413
+ levelName,
414
+ folderHint,
415
+ force,
416
+ interactive
417
+ });
418
+ if (r.ok && r.info) {
419
+ okCount++;
420
+ lastUuid = r.info.uuid;
421
+ console.log(
422
+ c.green(
423
+ t("packs.installedEnabled", {
424
+ kind: r.info.kind === "resource" ? "RP" : "BP",
425
+ name: r.info.name,
426
+ version: fmtVer(r.info.version)
427
+ })
428
+ )
429
+ );
430
+ } else if (r.skipped) {
431
+ skipCount++;
432
+ } else {
433
+ failCount++;
434
+ console.log(
435
+ c.red(t("packs.installFailed", { name: path.basename(root), reason: r.reason ?? "?" }))
436
+ );
437
+ }
438
+ }
439
+ installed += okCount > 0 ? 1 : 0;
440
+ skipped += skipCount;
441
+ failed += failCount;
442
+ if (okCount > 0) {
443
+ state.installed[fp] = { uuid: lastUuid, at: (/* @__PURE__ */ new Date()).toISOString(), folderName: base };
444
+ writeState(state);
445
+ moveTo(src, doneDir(), base);
446
+ console.log(c.yellow(`[packs] ${restartHint()}`));
447
+ } else if (failCount > 0 && skipCount === 0) {
448
+ moveTo(src, failedDir(), base);
449
+ }
450
+ } catch (e) {
451
+ console.log(c.red(`[packs] ${base}: ${e.message}`));
452
+ failed++;
453
+ if (!dryRun && fs.existsSync(src)) {
454
+ try {
455
+ moveTo(src, failedDir(), base);
456
+ } catch {
457
+ }
458
+ }
459
+ } finally {
460
+ if (tempDir) {
461
+ try {
462
+ fs.rmSync(tempDir, { recursive: true, force: true });
463
+ } catch {
464
+ }
465
+ }
466
+ }
467
+ }
468
+ return { installed, skipped, failed };
469
+ }
470
+ function filterPacks(packs, kind, search) {
471
+ let list = packs;
472
+ if (kind === "bp") list = list.filter((p) => p.kind === "behavior");
473
+ if (kind === "rp") list = list.filter((p) => p.kind === "resource");
474
+ if (search) {
475
+ const q = search.toLowerCase();
476
+ list = list.filter(
477
+ (p) => p.name.toLowerCase().includes(q) || p.folderName.toLowerCase().includes(q) || p.uuid.toLowerCase().includes(q)
478
+ );
479
+ }
480
+ return list;
481
+ }
482
+ function formatPackList(packs) {
483
+ if (packs.length === 0) return c.dim(t("packs.listEmpty"));
484
+ const lines = [];
485
+ const bp = packs.filter((p) => p.kind === "behavior");
486
+ const rp = packs.filter((p) => p.kind === "resource");
487
+ if (bp.length) {
488
+ lines.push(c.bold(t("packs.list.bpHeader")));
489
+ for (const p of bp) {
490
+ const en = p.enabled ? c.green(t("packs.list.on")) : c.dim(t("packs.list.off"));
491
+ const src = bindingLabelForUuid(p.uuid);
492
+ lines.push(
493
+ ` [${en}] ${p.folderName} ${p.name} v${fmtVer(p.version)} ${c.dim(p.uuid)} ${c.dim(src)}`
494
+ );
495
+ }
496
+ }
497
+ if (rp.length) {
498
+ lines.push(c.bold(t("packs.list.rpHeader")));
499
+ for (const p of rp) {
500
+ const en = p.enabled ? c.green(t("packs.list.on")) : c.dim(t("packs.list.off"));
501
+ lines.push(` [${en}] ${p.folderName} ${p.name} v${fmtVer(p.version)} ${c.dim(p.uuid)}`);
502
+ }
503
+ }
504
+ return lines.join("\n");
505
+ }
506
+ function parseListFlags(args) {
507
+ let kind = "all";
508
+ let search;
509
+ for (let i = 0; i < args.length; i++) {
510
+ const a = args[i];
511
+ if (a === "--kind" && args[i + 1]) {
512
+ const k = args[++i].toLowerCase();
513
+ if (k === "bp" || k === "behavior") kind = "bp";
514
+ else if (k === "rp" || k === "resource") kind = "rp";
515
+ else kind = "all";
516
+ } else if (a === "--search" && args[i + 1]) {
517
+ search = args[++i];
518
+ } else if (a && !a.startsWith("-")) {
519
+ search = a;
520
+ }
521
+ }
522
+ return { kind, ...search ? { search } : {} };
523
+ }
524
+ function hasFlag(args, name) {
525
+ return args.includes(name);
526
+ }
527
+ async function cmdDoctor() {
528
+ const { bdsRoot, levelName } = resolveBdsContext();
529
+ const packs = listInstalledWorldPacks(bdsRoot, levelName);
530
+ const issues = [];
531
+ for (const kind of ["behavior", "resource"]) {
532
+ const snap = listWorldEnableListResult(bdsRoot, levelName, kind);
533
+ if (snap.parseFailedFile) {
534
+ issues.push(t("packs.doctor.parseFail", { file: path.basename(snap.parseFailedFile) }));
535
+ }
536
+ const byUuid = new Map(packs.filter((p) => p.kind === kind).map((p) => [p.uuid, p]));
537
+ for (const e of snap.entries) {
538
+ const p = byUuid.get(e.pack_id);
539
+ if (!p) {
540
+ issues.push(t("packs.doctor.missingDir", { kind, uuid: e.pack_id }));
541
+ continue;
542
+ }
543
+ const ev = e.version;
544
+ if (Array.isArray(ev) && ev.length >= 3 && (ev[0] !== p.version[0] || ev[1] !== p.version[1] || ev[2] !== p.version[2])) {
545
+ issues.push(
546
+ t("packs.doctor.versionMismatch", {
547
+ kind,
548
+ folder: p.folderName,
549
+ listVer: ev.join("."),
550
+ diskVer: fmtVer(p.version)
551
+ })
552
+ );
553
+ }
554
+ }
555
+ }
556
+ for (const p of packs) {
557
+ if (!p.enabled) {
558
+ issues.push(
559
+ t("packs.doctor.notEnabled", {
560
+ kind: p.kind === "resource" ? "RP" : "BP",
561
+ folder: p.folderName,
562
+ uuid: p.uuid
563
+ })
564
+ );
565
+ }
566
+ }
567
+ if (issues.length === 0) return c.green(t("packs.doctor.ok"));
568
+ return t("packs.doctor.found", {
569
+ count: issues.length,
570
+ list: issues.map((x) => ` - ${x}`).join("\n")
571
+ });
572
+ }
573
+ async function dispatchPacksCommand(sub, args) {
574
+ const verb = (sub ?? "").toLowerCase();
575
+ if (!verb || verb === "help" || verb === "-h" || verb === "--help") {
576
+ return packsUsage();
577
+ }
578
+ try {
579
+ switch (verb) {
580
+ case "list": {
581
+ const { bdsRoot, levelName } = resolveBdsContext();
582
+ const flags = parseListFlags(args);
583
+ const packs = filterPacks(listInstalledWorldPacks(bdsRoot, levelName), flags.kind, flags.search);
584
+ return formatPackList(packs);
585
+ }
586
+ case "search": {
587
+ const q = args[0];
588
+ if (!q) return c.yellow(t("packs.search.usage"));
589
+ return await searchRemote(q);
590
+ }
591
+ case "bind": {
592
+ const packId = args[0];
593
+ const ref = args[1];
594
+ if (!packId || !ref) return c.yellow(t("packs.bind.usage"));
595
+ return await bindPackSource(packId, ref);
596
+ }
597
+ case "unbind": {
598
+ const packId = args[0];
599
+ if (!packId) return c.yellow(t("packs.unbind.usage"));
600
+ const { bdsRoot, levelName } = resolveBdsContext();
601
+ const packs = listInstalledWorldPacks(bdsRoot, levelName);
602
+ const pack = findInstalledPackById(packs, packId);
603
+ const uuid = pack?.uuid ?? packId;
604
+ const ok = removeBinding(uuid);
605
+ return ok ? c.green(t("packs.unbindOk", { uuid, path: packSourcesPath() })) : c.yellow(t("packs.unbindMiss", { id: packId }));
606
+ }
607
+ case "sources":
608
+ return formatSourcesList();
609
+ case "check": {
610
+ const id = args.find((a) => !a.startsWith("-"));
611
+ return await checkPackUpdates({ ...id ? { packId: id } : {}, apply: false });
612
+ }
613
+ case "update": {
614
+ const all = hasFlag(args, "--all");
615
+ const id = args.find((a) => !a.startsWith("-") && a !== "--all");
616
+ if (!all && !id) return c.yellow(t("packs.update.usage"));
617
+ return await checkPackUpdates({
618
+ ...id ? { packId: id } : {},
619
+ apply: true
620
+ });
621
+ }
622
+ case "enable":
623
+ case "disable": {
624
+ const id = args[0];
625
+ if (!id) return c.yellow(t("packs.toggle.usage", { verb }));
626
+ const { bdsRoot, levelName } = resolveBdsContext();
627
+ const packs = listInstalledWorldPacks(bdsRoot, levelName);
628
+ const pack = findInstalledPackById(packs, id);
629
+ if (!pack) return c.red(t("packs.notFound", { id }));
630
+ if (verb === "enable") {
631
+ await enableInstalledPack({
632
+ bdsRoot,
633
+ levelName,
634
+ info: {
635
+ name: pack.name,
636
+ uuid: pack.uuid,
637
+ version: pack.version,
638
+ kind: pack.kind
639
+ }
640
+ });
641
+ } else {
642
+ await disableInstalledPack({
643
+ bdsRoot,
644
+ levelName,
645
+ kind: pack.kind,
646
+ packUuid: pack.uuid,
647
+ version: pack.version
648
+ });
649
+ }
650
+ return c.green(t("packs.toggleDone", { verb, folder: pack.folderName, hint: restartHint() }));
651
+ }
652
+ case "uninstall": {
653
+ const ids = args.filter((a) => !a.startsWith("-"));
654
+ const purge = hasFlag(args, "--purge");
655
+ const noPaired = hasFlag(args, "--no-paired");
656
+ const { bdsRoot, levelName } = resolveBdsContext();
657
+ const packs = listInstalledWorldPacks(bdsRoot, levelName);
658
+ let selected = [];
659
+ if (ids.length === 0) {
660
+ if (!process.stdin.isTTY) return c.yellow(t("packs.uninstall.usage"));
661
+ const picked = await pickPacksForUninstall(packs);
662
+ if (picked === null) return c.dim(t("packs.uninstall.cancelled"));
663
+ if (picked.length === 0) return c.yellow(t("packs.uninstall.none"));
664
+ selected = picked;
665
+ } else if (ids.length === 1) {
666
+ const pack = findInstalledPackById(packs, ids[0]);
667
+ if (!pack) return c.red(t("packs.notFound", { id: ids[0] }));
668
+ if (isProtectedSfmcPack(pack)) {
669
+ return c.red(t("packs.uninstall.protected", { folder: pack.folderName }));
670
+ }
671
+ selected = [pack];
672
+ } else {
673
+ const missing = [];
674
+ const byUuid = /* @__PURE__ */ new Map();
675
+ for (const id of ids) {
676
+ const pack = findInstalledPackById(packs, id);
677
+ if (!pack) {
678
+ missing.push(id);
679
+ continue;
680
+ }
681
+ byUuid.set(pack.uuid.toLowerCase(), pack);
682
+ }
683
+ if (missing.length) {
684
+ return c.red(t("packs.notFound", { id: missing.join(", ") }));
685
+ }
686
+ selected = [...byUuid.values()];
687
+ }
688
+ return await uninstallPacksBatch(selected, packs, {
689
+ bdsRoot,
690
+ levelName,
691
+ purge,
692
+ noPaired,
693
+ /* 多选或一次卸多个时确认;单 id 保持原「直接卸」手感 */
694
+ askConfirm: ids.length !== 1
695
+ });
696
+ }
697
+ case "bump": {
698
+ const id = args[0];
699
+ if (!id) return c.yellow(t("packs.bump.usage"));
700
+ const { bdsRoot, levelName } = resolveBdsContext();
701
+ const packs = listInstalledWorldPacks(bdsRoot, levelName);
702
+ const pack = findInstalledPackById(packs, id);
703
+ if (!pack) return c.red(t("packs.notFound", { id }));
704
+ if (pack.kind !== "resource") {
705
+ return c.red(t("packs.bump.rpOnly"));
706
+ }
707
+ const next = bumpPackPatchVersion(pack.dir);
708
+ if (pack.enabled) {
709
+ await enableInstalledPack({
710
+ bdsRoot,
711
+ levelName,
712
+ info: {
713
+ name: pack.name,
714
+ uuid: pack.uuid,
715
+ version: next,
716
+ kind: "resource"
717
+ }
718
+ });
719
+ }
720
+ return c.green(
721
+ t("packs.bumped", {
722
+ folder: pack.folderName,
723
+ version: fmtVer(next),
724
+ restart: restartHint(),
725
+ rejoin: rejoinHint()
726
+ })
727
+ );
728
+ }
729
+ case "install": {
730
+ const force = hasFlag(args, "--force");
731
+ const inbox = hasFlag(args, "--inbox") || args.length === 0 || args.every((a) => a.startsWith("-"));
732
+ if (inbox) {
733
+ const r = await scanAndInstallInbox({ force, interactive: true });
734
+ return c.dim(
735
+ t("packs.installInbox", {
736
+ installed: r.installed,
737
+ skipped: r.skipped,
738
+ failed: r.failed
739
+ })
740
+ );
741
+ }
742
+ const target = args.find((a) => !a.startsWith("-"));
743
+ if (!target) return c.yellow(t("packs.install.usage"));
744
+ const abs = path.resolve(target);
745
+ if (!fs.existsSync(abs)) return c.red(t("packs.pathMissing", { path: abs }));
746
+ const { bdsRoot, levelName } = resolveBdsContext();
747
+ let tempDir = null;
748
+ try {
749
+ let roots;
750
+ if (fs.statSync(abs).isDirectory()) {
751
+ roots = discoverPackRoots(abs, { maxDepth: 2 });
752
+ } else if (isPackArchive(abs)) {
753
+ tempDir = await extractArchiveToTemp(abs);
754
+ roots = discoverPackRoots(tempDir, { maxDepth: 2 });
755
+ } else {
756
+ return c.red(t("packs.notArchive"));
757
+ }
758
+ if (roots.length === 0) return c.red(t("packs.noManifestRoot"));
759
+ const lines = [];
760
+ for (const root of roots) {
761
+ const r = await installOnePackRoot({
762
+ srcDir: root,
763
+ bdsRoot,
764
+ levelName,
765
+ folderHint: path.basename(root),
766
+ force,
767
+ interactive: true
768
+ });
769
+ if (r.ok && r.info) {
770
+ lines.push(
771
+ c.green(
772
+ t("packs.installedLine", {
773
+ name: r.info.name,
774
+ version: fmtVer(r.info.version),
775
+ kind: r.info.kind
776
+ })
777
+ )
778
+ );
779
+ } else {
780
+ lines.push(
781
+ c.yellow(
782
+ t("packs.skipFailLine", {
783
+ name: path.basename(root),
784
+ reason: r.reason ?? "?"
785
+ })
786
+ )
787
+ );
788
+ }
789
+ }
790
+ lines.push(c.yellow(restartHint()));
791
+ return lines.join("\n");
792
+ } finally {
793
+ if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
794
+ }
795
+ }
796
+ case "scan": {
797
+ const force = hasFlag(args, "--force");
798
+ const dryRun = hasFlag(args, "--dry-run");
799
+ const r = await scanAndInstallInbox({
800
+ force,
801
+ dryRun,
802
+ interactive: !!process.stdin.isTTY
803
+ });
804
+ if (r.installed === 0 && r.skipped === 0 && r.failed === 0) {
805
+ return c.dim(t("packs.inboxEmpty"));
806
+ }
807
+ return c.dim(
808
+ t("packs.scanResult", {
809
+ installed: r.installed,
810
+ skipped: r.skipped,
811
+ failed: r.failed
812
+ })
813
+ );
814
+ }
815
+ case "doctor":
816
+ return await cmdDoctor();
817
+ case "path": {
818
+ ensureInboxLayout();
819
+ const { bdsRoot, levelName } = resolveBdsContext();
820
+ return [
821
+ `bdsRoot: ${bdsRoot}`,
822
+ `level: ${levelName}`,
823
+ `behavior: ${worldPackParentDir(bdsRoot, levelName, "behavior")}`,
824
+ `resource: ${worldPackParentDir(bdsRoot, levelName, "resource")}`,
825
+ `inbox: ${packsInboxDir()}`,
826
+ `trash: ${resolvePackTrashDir()}`,
827
+ `build: ${path.join(packsInboxDir(), "_build")}`,
828
+ `sources: ${packSourcesPath()}`
829
+ ].join("\n");
830
+ }
831
+ default:
832
+ return c.red(t("packs.unknownSub", { verb })) + packsUsage();
833
+ }
834
+ } catch (e) {
835
+ return c.red(e.message);
836
+ }
837
+ }
838
+ function packsUsage() {
839
+ return `${t("packs.usage.title", { cmd: c.bold("sfmc packs"), alias: c.green("addon") })}
840
+
841
+ ${c.green("packs list")} [--kind bp|rp|all] [--search q]
842
+ ${c.green("packs search")} <q> ${t("packs.usage.cfSearch")}
843
+ ${c.green("packs bind")} <id> <project|slug|url>
844
+ ${c.green("packs unbind")} <id>
845
+ ${c.green("packs sources")}
846
+ ${c.green("packs check")} [id]
847
+ ${c.green("packs update")} <id|--all>
848
+ ${c.green("packs enable|disable")} <uuid|folder>
849
+ ${c.green("packs uninstall")} [id...] [--purge] [--no-paired]
850
+ ${c.green("packs bump")} <id> ${t("packs.usage.bump")}
851
+ ${c.green("packs install")} [path|--inbox] [--force]
852
+ ${c.green("packs scan")} [--force] [--dry-run]
853
+ ${c.green("packs doctor")}
854
+ ${c.green("packs path")}
855
+
856
+ ${t("packs.usage.inbox", { path: packsInboxDir() })}
857
+ ${t("packs.usage.trash", { path: resolvePackTrashDir() })}
858
+ ${t("packs.usage.sources", { path: packSourcesPath() })}
859
+ ${t("packs.usage.enableNote", { hint: restartHint() })}
860
+ ${t("packs.usage.moduleNote", { cmd: c.green("sfmc mod") })}`;
861
+ }
862
+ export {
863
+ PACKS_CMD_NAMES,
864
+ PACKS_SUBCOMMANDS,
865
+ dispatchPacksCommand,
866
+ isPacksCommand,
867
+ isProtectedSfmcPack,
868
+ packsInboxDir,
869
+ packsUsage,
870
+ scanAndInstallInbox
871
+ };
872
+ //# sourceMappingURL=world-packs.js.map