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

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 (183) hide show
  1. package/dist/argv-parse.d.ts +11 -0
  2. package/dist/argv-parse.d.ts.map +1 -0
  3. package/dist/argv-parse.js +28 -0
  4. package/dist/argv-parse.js.map +7 -0
  5. package/dist/cli-gate.d.ts +11 -0
  6. package/dist/cli-gate.d.ts.map +1 -0
  7. package/dist/cli-gate.js +46 -0
  8. package/dist/cli-gate.js.map +7 -0
  9. package/dist/command-palette.d.ts +61 -0
  10. package/dist/command-palette.d.ts.map +1 -0
  11. package/dist/command-palette.js +352 -0
  12. package/dist/command-palette.js.map +7 -0
  13. package/dist/command-surface.d.ts +85 -0
  14. package/dist/command-surface.d.ts.map +1 -0
  15. package/dist/command-surface.js +292 -0
  16. package/dist/command-surface.js.map +7 -0
  17. package/dist/commands-reload.js +2 -2
  18. package/dist/commands-reload.js.map +2 -2
  19. package/dist/commands.d.ts +1 -1
  20. package/dist/commands.d.ts.map +1 -1
  21. package/dist/commands.js +196 -21
  22. package/dist/commands.js.map +2 -2
  23. package/dist/debug-command.d.ts +5 -0
  24. package/dist/debug-command.d.ts.map +1 -0
  25. package/dist/debug-command.js +118 -0
  26. package/dist/debug-command.js.map +7 -0
  27. package/dist/help-text.d.ts +3 -0
  28. package/dist/help-text.d.ts.map +1 -0
  29. package/dist/help-text.js +160 -0
  30. package/dist/help-text.js.map +7 -0
  31. package/dist/i18n/locales/en.d.ts +103 -89
  32. package/dist/i18n/locales/en.d.ts.map +1 -1
  33. package/dist/i18n/locales/en.js +105 -91
  34. package/dist/i18n/locales/en.js.map +2 -2
  35. package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
  36. package/dist/i18n/locales/zh-CN.js +138 -118
  37. package/dist/i18n/locales/zh-CN.js.map +2 -2
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +9 -4
  41. package/dist/index.js.map +2 -2
  42. package/dist/llbot-launch.d.ts +20 -0
  43. package/dist/llbot-launch.d.ts.map +1 -0
  44. package/dist/llbot-launch.js +40 -0
  45. package/dist/llbot-launch.js.map +7 -0
  46. package/dist/log-filter.d.ts +69 -0
  47. package/dist/log-filter.d.ts.map +1 -0
  48. package/dist/log-filter.js +150 -0
  49. package/dist/log-filter.js.map +7 -0
  50. package/dist/logs.d.ts +29 -7
  51. package/dist/logs.d.ts.map +1 -1
  52. package/dist/logs.js +164 -72
  53. package/dist/logs.js.map +3 -3
  54. package/dist/main.js +55 -46
  55. package/dist/main.js.map +2 -2
  56. package/dist/module-commands.d.ts +17 -40
  57. package/dist/module-commands.d.ts.map +1 -1
  58. package/dist/module-commands.js +129 -85
  59. package/dist/module-commands.js.map +2 -2
  60. package/dist/module-pack-build.d.ts +10 -0
  61. package/dist/module-pack-build.d.ts.map +1 -0
  62. package/dist/module-pack-build.js +434 -0
  63. package/dist/module-pack-build.js.map +7 -0
  64. package/dist/module-toggle.d.ts +33 -0
  65. package/dist/module-toggle.d.ts.map +1 -0
  66. package/dist/module-toggle.js +28 -0
  67. package/dist/module-toggle.js.map +7 -0
  68. package/dist/node-service-probe.d.ts +18 -0
  69. package/dist/node-service-probe.d.ts.map +1 -0
  70. package/dist/node-service-probe.js +97 -0
  71. package/dist/node-service-probe.js.map +7 -0
  72. package/dist/pack-lifecycle.d.ts +1 -1
  73. package/dist/pack-lifecycle.d.ts.map +1 -1
  74. package/dist/pack-lifecycle.js +14 -4
  75. package/dist/pack-lifecycle.js.map +2 -2
  76. package/dist/pack-update/index.d.ts +18 -0
  77. package/dist/pack-update/index.d.ts.map +1 -0
  78. package/dist/pack-update/index.js +113 -0
  79. package/dist/pack-update/index.js.map +7 -0
  80. package/dist/registry.d.ts +7 -4
  81. package/dist/registry.d.ts.map +1 -1
  82. package/dist/registry.js +15 -3
  83. package/dist/registry.js.map +2 -2
  84. package/dist/repl-windows/format-display.d.ts +10 -0
  85. package/dist/repl-windows/format-display.d.ts.map +1 -0
  86. package/dist/repl-windows/format-display.js +57 -0
  87. package/dist/repl-windows/format-display.js.map +7 -0
  88. package/dist/repl-windows/host.d.ts +60 -0
  89. package/dist/repl-windows/host.d.ts.map +1 -0
  90. package/dist/repl-windows/host.js +179 -0
  91. package/dist/repl-windows/host.js.map +7 -0
  92. package/dist/repl-windows/index.d.ts +9 -0
  93. package/dist/repl-windows/index.d.ts.map +1 -0
  94. package/dist/repl-windows/index.js +13 -0
  95. package/dist/repl-windows/index.js.map +7 -0
  96. package/dist/repl-windows/logs-filter-window.d.ts +15 -0
  97. package/dist/repl-windows/logs-filter-window.d.ts.map +1 -0
  98. package/dist/repl-windows/logs-filter-window.js +77 -0
  99. package/dist/repl-windows/logs-filter-window.js.map +7 -0
  100. package/dist/repl-windows/service-window.d.ts +4 -0
  101. package/dist/repl-windows/service-window.d.ts.map +1 -0
  102. package/dist/repl-windows/service-window.js +35 -0
  103. package/dist/repl-windows/service-window.js.map +7 -0
  104. package/dist/repl-windows/types.d.ts +56 -0
  105. package/dist/repl-windows/types.d.ts.map +1 -0
  106. package/dist/repl-windows/types.js +1 -0
  107. package/dist/repl-windows/types.js.map +7 -0
  108. package/dist/repl.d.ts +6 -9
  109. package/dist/repl.d.ts.map +1 -1
  110. package/dist/repl.js +698 -427
  111. package/dist/repl.js.map +3 -3
  112. package/dist/runtime.d.ts +6 -13
  113. package/dist/runtime.d.ts.map +1 -1
  114. package/dist/runtime.js +23 -28
  115. package/dist/runtime.js.map +2 -2
  116. package/dist/send-target.d.ts +11 -0
  117. package/dist/send-target.d.ts.map +1 -0
  118. package/dist/send-target.js +42 -0
  119. package/dist/send-target.js.map +7 -0
  120. package/dist/services.d.ts +50 -4
  121. package/dist/services.d.ts.map +1 -1
  122. package/dist/services.js +244 -20
  123. package/dist/services.js.map +2 -2
  124. package/dist/theme.js +1 -1
  125. package/dist/theme.js.map +2 -2
  126. package/dist/wizard.d.ts.map +1 -1
  127. package/dist/wizard.js +76 -16
  128. package/dist/wizard.js.map +2 -2
  129. package/dist/world-packs.d.ts +15 -0
  130. package/dist/world-packs.d.ts.map +1 -1
  131. package/dist/world-packs.js +90 -87
  132. package/dist/world-packs.js.map +2 -2
  133. package/package.json +24 -21
  134. package/scripts/module-install/check-modules.mjs +204 -0
  135. package/scripts/module-install/fetch-module.mjs +1007 -0
  136. package/scripts/module-install/install-resolver.test.mjs +48 -0
  137. package/scripts/module-install/lib/catalog.mjs +96 -0
  138. package/scripts/module-install/lib/io.mjs +37 -0
  139. package/scripts/module-install/lib/link-from.mjs +80 -0
  140. package/scripts/module-install/lib/lock.mjs +78 -0
  141. package/scripts/module-install/lib/npm-resolver.mjs +55 -0
  142. package/scripts/module-install/lib/packages.mjs +142 -0
  143. package/scripts/module-install/lib/paths.mjs +48 -0
  144. package/scripts/module-install/lib/registry-index.mjs +50 -0
  145. package/scripts/module-install/link-local.test.mjs +51 -0
  146. package/scripts/module-install/local-resolver.test.mjs +51 -0
  147. package/scripts/module-install/registry-index.test.mjs +28 -0
  148. package/dist/module-wizard.d.ts +0 -14
  149. package/dist/module-wizard.d.ts.map +0 -1
  150. package/dist/module-wizard.js +0 -313
  151. package/dist/module-wizard.js.map +0 -7
  152. package/dist/pack-update/bindings.d.ts +0 -12
  153. package/dist/pack-update/bindings.d.ts.map +0 -1
  154. package/dist/pack-update/bindings.js +0 -64
  155. package/dist/pack-update/bindings.js.map +0 -7
  156. package/dist/pack-update/config.d.ts +0 -22
  157. package/dist/pack-update/config.d.ts.map +0 -1
  158. package/dist/pack-update/config.js +0 -141
  159. package/dist/pack-update/config.js.map +0 -7
  160. package/dist/pack-update/providers/curseforge.d.ts +0 -23
  161. package/dist/pack-update/providers/curseforge.d.ts.map +0 -1
  162. package/dist/pack-update/providers/curseforge.js +0 -209
  163. package/dist/pack-update/providers/curseforge.js.map +0 -7
  164. package/dist/pack-update/providers/index.d.ts +0 -16
  165. package/dist/pack-update/providers/index.d.ts.map +0 -1
  166. package/dist/pack-update/providers/index.js +0 -37
  167. package/dist/pack-update/providers/index.js.map +0 -7
  168. package/dist/pack-update/service.d.ts +0 -21
  169. package/dist/pack-update/service.d.ts.map +0 -1
  170. package/dist/pack-update/service.js +0 -567
  171. package/dist/pack-update/service.js.map +0 -7
  172. package/dist/pack-update/types.d.ts +0 -126
  173. package/dist/pack-update/types.d.ts.map +0 -1
  174. package/dist/pack-update/types.js +0 -8
  175. package/dist/pack-update/types.js.map +0 -7
  176. package/dist/pack-update/version-policy.d.ts +0 -80
  177. package/dist/pack-update/version-policy.d.ts.map +0 -1
  178. package/dist/pack-update/version-policy.js +0 -216
  179. package/dist/pack-update/version-policy.js.map +0 -7
  180. package/dist/remote-agent.d.ts +0 -18
  181. package/dist/remote-agent.d.ts.map +0 -1
  182. package/dist/remote-agent.js +0 -196
  183. package/dist/remote-agent.js.map +0 -7
@@ -0,0 +1,48 @@
1
+ // @ts-check
2
+ /**
3
+ * install-resolver.test.mjs — npm 包名解析表驱动
4
+ */
5
+ import assert from "node:assert/strict";
6
+ import test from "node:test";
7
+ import {
8
+ resolveNpmPackageName,
9
+ folderFromNpmPackageName,
10
+ normalizeNpmPackageName,
11
+ } from "./lib/npm-resolver.mjs";
12
+
13
+ test("短 id → @sfmc-bds/module-<id>", () => {
14
+ assert.equal(resolveNpmPackageName("land"), "@sfmc-bds/module-land");
15
+ assert.equal(resolveNpmPackageName("my-mod"), "@sfmc-bds/module-my-mod");
16
+ });
17
+
18
+ test("logical id (feature-* / core-*) → @sfmc-bds/module-<folder>", () => {
19
+ assert.equal(resolveNpmPackageName("feature-land"), "@sfmc-bds/module-land");
20
+ assert.equal(resolveNpmPackageName("core-data-backup"), "@sfmc-bds/module-data-backup");
21
+ });
22
+
23
+ test("已带 scope 原样透传", () => {
24
+ assert.equal(resolveNpmPackageName("@alice/sfmc-module-foo"), "@alice/sfmc-module-foo");
25
+ assert.equal(resolveNpmPackageName("@sfmc-bds/module-land"), "@sfmc-bds/module-land");
26
+ });
27
+
28
+ test("非 kebab 报错", () => {
29
+ assert.throws(() => resolveNpmPackageName("NotKebab"), /invalid module id/);
30
+ assert.throws(() => resolveNpmPackageName("feature-"), /invalid module id/);
31
+ });
32
+
33
+ test("@ 但无 / → invalid scoped package name", () => {
34
+ assert.throws(() => resolveNpmPackageName("@orphan"), /invalid scoped package name/);
35
+ });
36
+
37
+ test("folderFromNpmPackageName 反向解析", () => {
38
+ assert.equal(folderFromNpmPackageName("@sfmc-bds/module-land"), "land");
39
+ assert.equal(folderFromNpmPackageName("@alice/sfmc-module-foo"), "foo");
40
+ assert.equal(folderFromNpmPackageName("@alice/sfmc-module-my-mod"), "my-mod");
41
+ assert.equal(folderFromNpmPackageName("@alice/random"), null);
42
+ });
43
+
44
+ test("normalizeNpmPackageName 兼容两种前缀", () => {
45
+ assert.equal(normalizeNpmPackageName("@sfmc-bds/module-land"), "land");
46
+ assert.equal(normalizeNpmPackageName("@alice/sfmc-module-foo"), "foo");
47
+ assert.equal(normalizeNpmPackageName("@alice/module-bar"), "bar");
48
+ });
@@ -0,0 +1,96 @@
1
+ // @ts-check
2
+ /**
3
+ * modules/catalog.json 读写与同步(权威实现在 @sfmc-bds/cli)
4
+ */
5
+ import { CATALOG_PATH } from "./paths.mjs";
6
+ import { readJson, writeJson } from "./io.mjs";
7
+ import {
8
+ folderFromEntryPath,
9
+ loadPackageCatalogEntry,
10
+ projectCatalogEntry,
11
+ scanInstalledPackages,
12
+ } from "./packages.mjs";
13
+
14
+ const CATALOG_SCHEMA =
15
+ "https://cdn.jsdelivr.net/gh/DogeLakeDev/ScriptsForMinecraftServer@%40sfmc-bds/sdk@0.2.0-beta.6/modules/sdk/%40sfmc-sdk/schemas/module_catalog.schema.json";
16
+
17
+ /**
18
+ * @typedef {import("./packages.mjs").CatalogEntry} CatalogEntry
19
+ * @typedef {{ $schema?: string, version: number, modules: CatalogEntry[] }} ModuleCatalog
20
+ */
21
+
22
+ /** @returns {ModuleCatalog} */
23
+ export function readCatalog() {
24
+ const raw = readJson(CATALOG_PATH, null);
25
+ if (!raw || typeof raw !== "object") {
26
+ return { version: 1, modules: [] };
27
+ }
28
+ return {
29
+ version: typeof raw.version === "number" ? raw.version : 1,
30
+ modules: Array.isArray(raw.modules) ? raw.modules : [],
31
+ };
32
+ }
33
+
34
+ /** @param {ModuleCatalog} catalog */
35
+ export function writeCatalog(catalog) {
36
+ writeJson(CATALOG_PATH, {
37
+ $schema: CATALOG_SCHEMA,
38
+ version: catalog.version || 1,
39
+ modules: catalog.modules || [],
40
+ });
41
+ }
42
+
43
+ /**
44
+ * 扫描 packages 全量重写 catalog.modules
45
+ * @returns {{ catalog: ModuleCatalog, count: number }}
46
+ */
47
+ export function syncCatalogFromPackages() {
48
+ const prev = readCatalog();
49
+ /** @type {CatalogEntry[]} */
50
+ const modules = [];
51
+ for (const pkg of scanInstalledPackages()) {
52
+ modules.push(projectCatalogEntry(pkg.folder, pkg.manifest));
53
+ }
54
+ const catalog = {
55
+ version: prev.version || 1,
56
+ modules,
57
+ };
58
+ writeCatalog(catalog);
59
+ return { catalog, count: modules.length };
60
+ }
61
+
62
+ /**
63
+ * install 后 upsert 单条
64
+ * @param {string} folder
65
+ * @returns {CatalogEntry}
66
+ */
67
+ export function upsertCatalogEntry(folder) {
68
+ const entry = loadPackageCatalogEntry(folder);
69
+ if (!entry) throw new Error(`packages/${folder}: 无有效 sapi/manifest.json`);
70
+ const catalog = readCatalog();
71
+ const idx = catalog.modules.findIndex(
72
+ (m) => m.id === entry.id || (m.entry && folderFromEntryPath(m.entry.path) === folder)
73
+ );
74
+ if (idx >= 0) catalog.modules[idx] = entry;
75
+ else catalog.modules.push(entry);
76
+ catalog.modules.sort((a, b) => a.id.localeCompare(b.id));
77
+ writeCatalog(catalog);
78
+ return entry;
79
+ }
80
+
81
+ /**
82
+ * uninstall 后按 folder / manifest.id 删除
83
+ * @param {string} folderOrId
84
+ * @returns {CatalogEntry | null} 被删条目
85
+ */
86
+ export function removeCatalogEntry(folderOrId) {
87
+ const catalog = readCatalog();
88
+ const idx = catalog.modules.findIndex((m) => {
89
+ if (m.id === folderOrId) return true;
90
+ return folderFromEntryPath(m.entry?.path) === folderOrId;
91
+ });
92
+ if (idx < 0) return null;
93
+ const [removed] = catalog.modules.splice(idx, 1);
94
+ writeCatalog(catalog);
95
+ return removed;
96
+ }
@@ -0,0 +1,37 @@
1
+ // @ts-check
2
+ /**
3
+ * tools/lib/io.mjs — JSON 读写与 die 辅助
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+
8
+ /** @param {string} file @param {unknown} fallback */
9
+ export function readJson(file, fallback = null) {
10
+ try {
11
+ return JSON.parse(fs.readFileSync(file, "utf8"));
12
+ } catch {
13
+ return fallback;
14
+ }
15
+ }
16
+
17
+ /** @param {string} file @param {unknown} data */
18
+ export function writeJson(file, data) {
19
+ fs.mkdirSync(path.dirname(file), { recursive: true });
20
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n", "utf8");
21
+ }
22
+
23
+ /** @param {string} tag @param {string} msg @param {number} [code] */
24
+ export function die(tag, msg, code = 1) {
25
+ console.error(`[${tag}] ${msg}`);
26
+ process.exit(code);
27
+ }
28
+
29
+ /** @param {string} p */
30
+ export function exists(p) {
31
+ try {
32
+ fs.accessSync(p, fs.constants.F_OK);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
@@ -0,0 +1,80 @@
1
+ // @ts-check
2
+ /**
3
+ * --from 规范化(裸路径 / local: / dir:)
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+
8
+ /**
9
+ * 是否为带 scheme 的 --from(非裸文件系统路径)。
10
+ * @param {string} from
11
+ */
12
+ export function isSchemeFrom(from) {
13
+ return (
14
+ from === "local" ||
15
+ from.startsWith("local:") ||
16
+ from.startsWith("dir:") ||
17
+ from.startsWith("npm:") ||
18
+ from.startsWith("tgz:") ||
19
+ from.startsWith("zip:") ||
20
+ from.startsWith("github:")
21
+ );
22
+ }
23
+
24
+ /**
25
+ * 把裸路径规范成 dir:<abs>(不存在则报错)。
26
+ * @param {string} raw
27
+ * @param {string} [cwd]
28
+ * @returns {{ ok: true, from: string } | { ok: false, error: string }}
29
+ */
30
+ export function normalizeBarePathFrom(raw, cwd = process.cwd()) {
31
+ const abs = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(cwd, raw);
32
+ if (!fs.existsSync(abs)) {
33
+ return { ok: false, error: `--from 路径不存在: ${abs}` };
34
+ }
35
+ const st = fs.lstatSync(abs);
36
+ if (!st.isDirectory()) {
37
+ return {
38
+ ok: false,
39
+ error: `--from 裸路径须为目录(文件请用 --from local:<tgz|zip>): ${abs}`,
40
+ };
41
+ }
42
+ return { ok: true, from: `dir:${abs}` };
43
+ }
44
+
45
+ /**
46
+ * @param {string} from
47
+ * @param {string} [cwd]
48
+ * @returns {{ ok: true, from: string } | { ok: false, error: string }}
49
+ */
50
+ export function normalizeLinkFrom(from, cwd = process.cwd()) {
51
+ if (!from || from === "local" || from.startsWith("local:")) {
52
+ const tail = !from || from === "local" ? "" : from.slice("local:".length);
53
+ const abs =
54
+ !tail || tail === "." || tail === "./"
55
+ ? path.resolve(cwd)
56
+ : path.isAbsolute(tail)
57
+ ? path.resolve(tail)
58
+ : path.resolve(cwd, tail);
59
+ if (!fs.existsSync(abs)) {
60
+ return { ok: false, error: `--from local target not found: ${abs}` };
61
+ }
62
+ const st = fs.lstatSync(abs);
63
+ if (st.isDirectory()) return { ok: true, from: `dir:${abs}` };
64
+ return {
65
+ ok: false,
66
+ error: `--link 仅支持目录源;tgz/zip 请去掉 --link 使用 copy 安装`,
67
+ };
68
+ }
69
+ if (from.startsWith("dir:")) return { ok: true, from };
70
+ /* 约定写法:mod install <id> --from <路径> --link */
71
+ if (!isSchemeFrom(from)) {
72
+ const bare = normalizeBarePathFrom(from, cwd);
73
+ if (!bare.ok) return bare;
74
+ return bare;
75
+ }
76
+ return {
77
+ ok: false,
78
+ error: `--link only works with --from <path> / dir:<path> / local[:<dir>] (got ${from})`,
79
+ };
80
+ }
@@ -0,0 +1,78 @@
1
+ // @ts-check
2
+ /**
3
+ * modules/module-lock.json 读写(权威实现在 @sfmc-bds/cli)
4
+ *
5
+ * 服务类条目(service-* 与 tool-*)保留;业务模块按 manifest.id 启停。
6
+ */
7
+ import { MODULE_LOCK_PATH } from "./paths.mjs";
8
+ import { readJson, writeJson } from "./io.mjs";
9
+
10
+ /**
11
+ * @typedef {{ enabled: boolean, updatedAt?: number }} LockState
12
+ * @typedef {{ version: number, modules: Record<string, LockState> }} ModuleLock
13
+ */
14
+
15
+ const SERVICE_PREFIXES = ["service-", "tool-"];
16
+
17
+ /** @param {string} id */
18
+ export function isServiceLockId(id) {
19
+ return SERVICE_PREFIXES.some((p) => id.startsWith(p));
20
+ }
21
+
22
+ /** @returns {ModuleLock} */
23
+ export function readLock() {
24
+ const raw = readJson(MODULE_LOCK_PATH, null);
25
+ if (!raw || typeof raw !== "object") return { version: 1, modules: {} };
26
+ return {
27
+ version: typeof raw.version === "number" ? raw.version : 1,
28
+ modules: raw.modules && typeof raw.modules === "object" ? { ...raw.modules } : {},
29
+ };
30
+ }
31
+
32
+ /** @param {ModuleLock} lock */
33
+ export function writeLock(lock) {
34
+ writeJson(MODULE_LOCK_PATH, {
35
+ version: lock.version || 1,
36
+ modules: lock.modules || {},
37
+ });
38
+ }
39
+
40
+ /**
41
+ * @param {string} moduleId manifest.id
42
+ * @param {boolean} enabled
43
+ */
44
+ export function setModuleLockEnabled(moduleId, enabled) {
45
+ const lock = readLock();
46
+ lock.modules[moduleId] = {
47
+ enabled: !!enabled,
48
+ updatedAt: Date.now(),
49
+ };
50
+ writeLock(lock);
51
+ }
52
+
53
+ /** @param {string} moduleId */
54
+ export function removeModuleLock(moduleId) {
55
+ const lock = readLock();
56
+ if (!(moduleId in lock.modules)) return false;
57
+ delete lock.modules[moduleId];
58
+ writeLock(lock);
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * 清理 lock 中已不在 catalog 的业务模块条目(保留 service/tool)
64
+ * @param {Set<string>} catalogIds
65
+ */
66
+ export function pruneOrphanModuleLocks(catalogIds) {
67
+ const lock = readLock();
68
+ let changed = false;
69
+ for (const id of Object.keys(lock.modules)) {
70
+ if (isServiceLockId(id)) continue;
71
+ if (!catalogIds.has(id)) {
72
+ delete lock.modules[id];
73
+ changed = true;
74
+ }
75
+ }
76
+ if (changed) writeLock(lock);
77
+ return changed;
78
+ }
@@ -0,0 +1,55 @@
1
+ // @ts-check
2
+ /**
3
+ * npm 包名解析(单一权威)
4
+ *
5
+ * 规则(DRY,避免重复解析逻辑):
6
+ * - 已含 scope(@scope/name)→ 原样
7
+ * - 已含 logical id(feature-land / core-data-backup)→ @sfmc-bds/module-<folder>
8
+ * - 短 id(land / my-mod)→ @sfmc-bds/module-<folder>
9
+ *
10
+ * @param {string} idOrName 用户传入的模块 id 或包名
11
+ * @param {string} [scope] 显式 scope,默认 @sfmc-bds
12
+ * @returns {string} npm 包名
13
+ */
14
+ export const DEFAULT_NPM_SCOPE = "@sfmc-bds";
15
+
16
+ export function resolveNpmPackageName(idOrName, scope = DEFAULT_NPM_SCOPE) {
17
+ const raw = String(idOrName ?? "").trim();
18
+ if (!raw) throw new Error("empty module id");
19
+ /* 已含 scope(@scope/name)→ 原样 */
20
+ if (raw.startsWith("@")) {
21
+ const m = raw.match(/^@([^/]+)\/(.+)$/);
22
+ if (!m) throw new Error(`invalid scoped package name: ${raw}`);
23
+ return raw;
24
+ }
25
+ /* feature-* / core-* → folder */
26
+ const folder = raw.replace(/^(feature|core)-/, "");
27
+ if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(folder)) {
28
+ throw new Error(`invalid module id: ${raw}`);
29
+ }
30
+ return `${scope}/module-${folder}`;
31
+ }
32
+
33
+ /**
34
+ * 反向:从 npm 包名解出 folder(packages/<folder> 用)。
35
+ * 接受:
36
+ * @sfmc-bds/module-land → land
37
+ * @<scope>/sfmc-module-land → land
38
+ * @<scope>/sfmc-module-my-mod → my-mod
39
+ */
40
+ export function folderFromNpmPackageName(pkgName) {
41
+ const raw = String(pkgName ?? "").trim();
42
+ const m = raw.match(/^@[^/]+\/(?:module-|sfmc-module-)(.+)$/);
43
+ return m ? m[1] : null;
44
+ }
45
+
46
+ /**
47
+ * 规范化 npm 包名(去掉前缀、保证 kebab)—— 给 tarball 内部读到的 name 用。
48
+ */
49
+ export function normalizeNpmPackageName(pkgName) {
50
+ const raw = String(pkgName ?? "").trim();
51
+ if (!raw) throw new Error("empty npm package name");
52
+ /* @scope/module-foo → foo;@scope/sfmc-module-foo → foo;module-foo → foo */
53
+ const m = raw.match(/^(?:@[^/]+\/)?(?:sfmc-)?module-(.+)$/);
54
+ return m ? m[1] : raw;
55
+ }
@@ -0,0 +1,142 @@
1
+ // @ts-check
2
+ /**
3
+ * 扫描 modules/packages/<id>/sapi/manifest.json 并投影为 catalog 条目
4
+ *
5
+ * v3 兼容:若 manifest.schemaVersion === 3,则把 semantic 块原样投影进 catalog 条目;
6
+ * v2 manifest 不带 semantic,catalog 条目也不带。
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { exists, readJson } from "./io.mjs";
11
+ import { catalogEntryRelPath, packageDir, packageEntryPath, packageManifestPath, PACKAGES_DIR } from "./paths.mjs";
12
+
13
+ /**
14
+ * @typedef {object} PackageInfo
15
+ * @property {string} folder packages 下目录名(registry install id)
16
+ * @property {*} manifest
17
+ * @property {string} manifestPath
18
+ * @property {string} entryAbs
19
+ * @property {boolean} hasEntry
20
+ */
21
+
22
+ /**
23
+ * v3 semantic 块投影后形状(catalog 与 manifest 同源;后续沙箱可直接读)。
24
+ * @typedef {{
25
+ * configKeys?: string[],
26
+ * dependsOn?: string[],
27
+ * events?: { emits?: string[], listens?: string[] },
28
+ * dbTables?: Array<{ name: string, columns?: string[] }>,
29
+ * publicApi?: Array<{ symbol: string, description?: string, params?: Array<{ name: string, type: string, required?: boolean, description?: string }>, returns?: { type: string, description?: string } }>
30
+ * }} SemanticBlock
31
+ */
32
+
33
+ /**
34
+ * @typedef {object} CatalogEntry
35
+ * @property {string} id
36
+ * @property {string} configKey
37
+ * @property {string} name
38
+ * @property {string} type
39
+ * @property {string} description
40
+ * @property {boolean} enabledByDefault
41
+ * @property {boolean} canDisable
42
+ * @property {string[]} requires
43
+ * @property {{ kind: string, path: string }} entry
44
+ * @property {SemanticBlock} [semantic] v3 字段;v2 模块不写
45
+ */
46
+
47
+ /** @returns {PackageInfo[]} */
48
+ export function scanInstalledPackages() {
49
+ if (!exists(PACKAGES_DIR)) return [];
50
+ /** @type {PackageInfo[]} */
51
+ const out = [];
52
+ for (const ent of fs.readdirSync(PACKAGES_DIR, { withFileTypes: true })) {
53
+ if (ent.name.startsWith(".")) continue;
54
+ const folder = ent.name;
55
+ const abs = path.join(PACKAGES_DIR, folder);
56
+ // 跟随 symlink:Dirent.isDirectory() 对链接目录为 false
57
+ try {
58
+ if (!fs.statSync(abs).isDirectory()) continue;
59
+ } catch {
60
+ continue;
61
+ }
62
+ const manifestPath = packageManifestPath(folder);
63
+ if (!exists(manifestPath)) continue;
64
+ const manifest = readJson(manifestPath, null);
65
+ if (!manifest || typeof manifest !== "object") continue;
66
+ const entryAbs = packageEntryPath(folder);
67
+ out.push({
68
+ folder,
69
+ manifest,
70
+ manifestPath,
71
+ entryAbs,
72
+ hasEntry: exists(entryAbs),
73
+ });
74
+ }
75
+ out.sort((a, b) => a.folder.localeCompare(b.folder));
76
+ return out;
77
+ }
78
+
79
+ /**
80
+ * 从 v2 / v3 manifest + 目录名投影 catalog 条目。
81
+ * v2 → 与历史完全相同;v3 → 把 semantic 块原样带进 catalog(沙箱读模块语义镜像)。
82
+ * @param {string} folder
83
+ * @param {Record<string, unknown>} manifest
84
+ * @returns {CatalogEntry}
85
+ */
86
+ export function projectCatalogEntry(folder, manifest) {
87
+ const type = String(manifest.type || "feature");
88
+ const id = String(manifest.id || "").trim();
89
+ const configKey = String(manifest.configKey || "").trim();
90
+ if (!id) throw new Error(`packages/${folder}: manifest.id 缺失`);
91
+ if (!configKey) throw new Error(`packages/${folder}: manifest.configKey 缺失`);
92
+
93
+ const enabledByDefault = typeof manifest.enabledByDefault === "boolean" ? manifest.enabledByDefault : type === "core";
94
+ const canDisable = typeof manifest.canDisable === "boolean" ? manifest.canDisable : type !== "core";
95
+
96
+ /** @type {CatalogEntry} */
97
+ const entry = {
98
+ id,
99
+ configKey,
100
+ name: String(manifest.name || configKey),
101
+ type,
102
+ description: String(manifest.description || ""),
103
+ enabledByDefault,
104
+ canDisable,
105
+ requires: Array.isArray(manifest.requires) ? manifest.requires.map(String) : [],
106
+ entry: {
107
+ kind: "sapi",
108
+ path: catalogEntryRelPath(folder),
109
+ },
110
+ };
111
+ /* v3 semantic 投影:原样复制即可(catalog 与 manifest 同源)。
112
+ * 此处不再做形状校验——check-modules 才是权威校验点。 */
113
+ if (manifest.schemaVersion === 3 && manifest.semantic && typeof manifest.semantic === "object") {
114
+ entry.semantic = /** @type {SemanticBlock} */ (structuredClone(manifest.semantic));
115
+ }
116
+ return entry;
117
+ }
118
+
119
+ /** @param {string} folder @returns {CatalogEntry | null} */
120
+ export function loadPackageCatalogEntry(folder) {
121
+ const manifestPath = packageManifestPath(folder);
122
+ if (!exists(manifestPath)) return null;
123
+ const manifest = readJson(manifestPath, null);
124
+ if (!manifest) return null;
125
+ return projectCatalogEntry(folder, manifest);
126
+ }
127
+
128
+ /** @param {string} folder */
129
+ export function packageExists(folder) {
130
+ return exists(packageDir(folder));
131
+ }
132
+
133
+ /**
134
+ * 从 entry.path 解析 packages 目录名
135
+ * @param {string} entryPath
136
+ */
137
+ export function folderFromEntryPath(entryPath) {
138
+ const m = String(entryPath || "")
139
+ .replace(/\\/g, "/")
140
+ .match(/modules\/packages\/([^/]+)\//);
141
+ return m ? m[1] : null;
142
+ }
@@ -0,0 +1,48 @@
1
+ // @ts-check
2
+ /**
3
+ * CLI module-install 路径 — modules/packages、catalog、lock(相对 SFMC_ROOT)
4
+ */
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { findMonorepoRoot } from "@sfmc-bds/sdk/node/config";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ /** @sfmc-bds/cli 包根(packages/cli 或 node_modules/@sfmc-bds/cli) */
12
+ export const CLI_PKG_DIR = path.resolve(__dirname, "..", "..", "..");
13
+
14
+ /**
15
+ * 工作根目录。
16
+ * 优先 SFMC_ROOT;否则 walk-up 找 sfmc-monorepo;再回退到 cwd。
17
+ */
18
+ export const ROOT = process.env.SFMC_ROOT
19
+ ? path.resolve(process.env.SFMC_ROOT)
20
+ : findMonorepoRoot(__dirname) ?? process.cwd();
21
+
22
+ export const MODULES_DIR = path.join(ROOT, "modules");
23
+ export const PACKAGES_DIR = path.join(MODULES_DIR, "packages");
24
+ export const CATALOG_PATH = path.join(MODULES_DIR, "catalog.json");
25
+ export const MODULE_LOCK_PATH = path.join(MODULES_DIR, "module-lock.json");
26
+
27
+ /** 本包内 fetch-module 入口(npm 安装时勿拼 ROOT) */
28
+ export const FETCH_MODULE = path.join(CLI_PKG_DIR, "scripts", "module-install", "fetch-module.mjs");
29
+
30
+ /** @param {string} folder packages/<folder> */
31
+ export function packageDir(folder) {
32
+ return path.join(PACKAGES_DIR, folder);
33
+ }
34
+
35
+ /** @param {string} folder */
36
+ export function packageManifestPath(folder) {
37
+ return path.join(PACKAGES_DIR, folder, "sapi", "manifest.json");
38
+ }
39
+
40
+ /** @param {string} folder */
41
+ export function packageEntryPath(folder) {
42
+ return path.join(PACKAGES_DIR, folder, "sapi", "src", "index.ts");
43
+ }
44
+
45
+ /** catalog.entry.path 用正斜杠相对仓库根 */
46
+ export function catalogEntryRelPath(folder) {
47
+ return `modules/packages/${folder}/sapi/src/index.ts`;
48
+ }
@@ -0,0 +1,50 @@
1
+ // @ts-check
2
+ /**
3
+ * 解析 Tanya7z/sfmc-modules index.json
4
+ *
5
+ * 契约(与 sfmc/src/registry.ts#parseRegistryIndex 保持一致):
6
+ * { modules: { <id>: { npm?, version?, sdk?, repo?, tag? } } }
7
+ * 条目至少具备 npm 或 repo+tag。`_` 前缀键视为元数据并忽略。
8
+ */
9
+
10
+ /**
11
+ * @typedef {{ npm?: string, version?: string, sdk?: string, repo?: string, tag?: string }} RegistryEntry
12
+ * @typedef {Record<string, RegistryEntry>} RegistryIndex
13
+ */
14
+
15
+ /**
16
+ * @param {unknown} json
17
+ * @returns {RegistryIndex}
18
+ */
19
+ export function parseRegistryIndex(json) {
20
+ if (typeof json !== "object" || json === null || Array.isArray(json)) {
21
+ throw new Error("registry index must be a JSON object with a 'modules' field");
22
+ }
23
+ const modules = /** @type {Record<string, unknown>} */ (json).modules;
24
+ if (typeof modules !== "object" || modules === null || Array.isArray(modules)) {
25
+ throw new Error("registry index must have a 'modules' object mapping id → entry");
26
+ }
27
+ /** @type {RegistryIndex} */
28
+ const filtered = {};
29
+ for (const [k, v] of Object.entries(modules)) {
30
+ if (k.startsWith("_")) continue;
31
+ if (typeof v !== "object" || v === null || Array.isArray(v)) continue;
32
+ const entry = /** @type {Record<string, unknown>} */ (v);
33
+ const hasNpm = typeof entry.npm === "string" && entry.npm.length > 0;
34
+ const hasGh = typeof entry.repo === "string" && typeof entry.tag === "string";
35
+ if (!hasNpm && !hasGh) continue;
36
+ /** @type {RegistryEntry} */
37
+ const out = {};
38
+ if (hasNpm) {
39
+ out.npm = /** @type {string} */ (entry.npm);
40
+ if (typeof entry.version === "string") out.version = entry.version;
41
+ if (typeof entry.sdk === "string") out.sdk = entry.sdk;
42
+ }
43
+ if (hasGh) {
44
+ out.repo = /** @type {string} */ (entry.repo);
45
+ out.tag = /** @type {string} */ (entry.tag);
46
+ }
47
+ filtered[k] = out;
48
+ }
49
+ return filtered;
50
+ }
@@ -0,0 +1,51 @@
1
+ // @ts-check
2
+ /**
3
+ * link-local.test.mjs — normalizeLinkFrom 表驱动
4
+ */
5
+ import assert from "node:assert/strict";
6
+ import test from "node:test";
7
+ import path from "node:path";
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import { normalizeLinkFrom } from "./lib/link-from.mjs";
11
+
12
+ const CWD = process.cwd();
13
+
14
+ test("local 无路径 → dir:cwd", () => {
15
+ const r = normalizeLinkFrom("local", CWD);
16
+ assert.equal(r.ok, true);
17
+ if (r.ok) assert.equal(r.from, `dir:${path.resolve(CWD)}`);
18
+ });
19
+
20
+ test("local:相对目录 → dir:绝对路径", () => {
21
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-link-rel-"));
22
+ const rel = path.relative(CWD, tmp) || ".";
23
+ const r = normalizeLinkFrom(`local:${rel}`, CWD);
24
+ assert.equal(r.ok, true);
25
+ if (r.ok) assert.equal(r.from, `dir:${path.resolve(CWD, rel)}`);
26
+ fs.rmSync(tmp, { recursive: true, force: true });
27
+ });
28
+
29
+ test("local:file.tgz + link → 拒绝", () => {
30
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-link-"));
31
+ const tgz = path.join(tmp, "x.tgz");
32
+ fs.writeFileSync(tgz, "");
33
+ const r = normalizeLinkFrom(`local:${tgz}`, CWD);
34
+ assert.equal(r.ok, false);
35
+ if (!r.ok) assert.match(r.error, /tgz|zip|--link/i);
36
+ fs.rmSync(tmp, { recursive: true, force: true });
37
+ });
38
+
39
+ test("dir: 原样通过", () => {
40
+ const r = normalizeLinkFrom("dir:D:/mods/foo", CWD);
41
+ assert.equal(r.ok, true);
42
+ if (r.ok) assert.equal(r.from, "dir:D:/mods/foo");
43
+ });
44
+
45
+ test("裸路径目录 → dir:绝对路径", () => {
46
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-link-bare-"));
47
+ const r = normalizeLinkFrom(tmp, CWD);
48
+ assert.equal(r.ok, true);
49
+ if (r.ok) assert.equal(r.from, `dir:${path.resolve(tmp)}`);
50
+ fs.rmSync(tmp, { recursive: true, force: true });
51
+ });