@sfmc-bds/cli 0.2.0-beta.8 → 0.2.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.
- package/LICENSE +663 -0
- package/README.md +4 -5
- package/dist/bds-runtime-version.d.ts +3 -0
- package/dist/bds-runtime-version.d.ts.map +1 -0
- package/dist/bds-runtime-version.js +15 -0
- package/dist/bds-runtime-version.js.map +7 -0
- package/dist/command-surface.js +3 -3
- package/dist/command-surface.js.map +2 -2
- package/dist/commands.d.ts +57 -4
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +184 -68
- package/dist/commands.js.map +2 -2
- package/dist/directory-picker.d.ts +27 -0
- package/dist/directory-picker.d.ts.map +1 -0
- package/dist/directory-picker.js +112 -0
- package/dist/directory-picker.js.map +7 -0
- package/dist/help-text.d.ts.map +1 -1
- package/dist/help-text.js +1 -5
- package/dist/help-text.js.map +2 -2
- package/dist/i18n/locales/en.d.ts +49 -14
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +49 -15
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
- package/dist/i18n/locales/zh-CN.js +50 -16
- package/dist/i18n/locales/zh-CN.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/interactive-prompts.d.ts +1 -2
- package/dist/interactive-prompts.d.ts.map +1 -1
- package/dist/interactive-prompts.js +12 -23
- package/dist/interactive-prompts.js.map +3 -3
- package/dist/llbot-launch.d.ts +20 -0
- package/dist/llbot-launch.d.ts.map +1 -0
- package/dist/llbot-launch.js +40 -0
- package/dist/llbot-launch.js.map +7 -0
- package/dist/log-filter.d.ts +18 -2
- package/dist/log-filter.d.ts.map +1 -1
- package/dist/log-filter.js +54 -4
- package/dist/log-filter.js.map +2 -2
- package/dist/log-translator.d.ts +36 -0
- package/dist/log-translator.d.ts.map +1 -0
- package/dist/log-translator.js +279 -0
- package/dist/log-translator.js.map +7 -0
- package/dist/logs.d.ts +2 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +24 -11
- package/dist/logs.js.map +2 -2
- package/dist/main.js +9 -6
- package/dist/main.js.map +2 -2
- package/dist/module-commands.d.ts.map +1 -1
- package/dist/module-commands.js +22 -8
- package/dist/module-commands.js.map +2 -2
- package/dist/module-fingerprint.d.ts.map +1 -1
- package/dist/module-fingerprint.js +1 -0
- package/dist/module-fingerprint.js.map +2 -2
- package/dist/module-pack-build.d.ts +14 -4
- package/dist/module-pack-build.d.ts.map +1 -1
- package/dist/module-pack-build.js +81 -108
- package/dist/module-pack-build.js.map +2 -2
- package/dist/module-toggle.d.ts +1 -1
- package/dist/module-toggle.d.ts.map +1 -1
- package/dist/module-toggle.js.map +1 -1
- package/dist/node-service-probe.d.ts +18 -0
- package/dist/node-service-probe.d.ts.map +1 -0
- package/dist/node-service-probe.js +97 -0
- package/dist/node-service-probe.js.map +7 -0
- package/dist/pack-lifecycle.d.ts +15 -1
- package/dist/pack-lifecycle.d.ts.map +1 -1
- package/dist/pack-lifecycle.js +249 -16
- package/dist/pack-lifecycle.js.map +2 -2
- package/dist/player-session.d.ts +13 -0
- package/dist/player-session.d.ts.map +1 -0
- package/dist/player-session.js +26 -0
- package/dist/player-session.js.map +7 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -1
- package/dist/registry.js.map +2 -2
- package/dist/repl-windows/format-display.d.ts.map +1 -1
- package/dist/repl-windows/format-display.js +5 -1
- package/dist/repl-windows/format-display.js.map +2 -2
- package/dist/repl-windows/host.d.ts +6 -3
- package/dist/repl-windows/host.d.ts.map +1 -1
- package/dist/repl-windows/host.js +12 -6
- package/dist/repl-windows/host.js.map +2 -2
- package/dist/repl-windows/index.d.ts +1 -0
- package/dist/repl-windows/index.d.ts.map +1 -1
- package/dist/repl-windows/index.js +3 -0
- package/dist/repl-windows/index.js.map +2 -2
- package/dist/repl-windows/service-window.d.ts +1 -1
- package/dist/repl-windows/service-window.d.ts.map +1 -1
- package/dist/repl-windows/service-window.js +9 -1
- package/dist/repl-windows/service-window.js.map +2 -2
- package/dist/repl-windows/sfmc-window.d.ts +7 -0
- package/dist/repl-windows/sfmc-window.d.ts.map +1 -0
- package/dist/repl-windows/sfmc-window.js +28 -0
- package/dist/repl-windows/sfmc-window.js.map +7 -0
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +37 -22
- package/dist/repl.js.map +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +15 -2
- package/dist/runtime.js.map +2 -2
- package/dist/send-target.d.ts +2 -0
- package/dist/send-target.d.ts.map +1 -1
- package/dist/send-target.js +12 -2
- package/dist/send-target.js.map +2 -2
- package/dist/services.d.ts +58 -5
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +273 -50
- package/dist/services.js.map +2 -2
- package/dist/sfmc-modules-root.d.ts.map +1 -1
- package/dist/sfmc-modules-root.js +6 -2
- package/dist/sfmc-modules-root.js.map +2 -2
- package/dist/ui-command.d.ts +6 -0
- package/dist/ui-command.d.ts.map +1 -0
- package/dist/ui-command.js +68 -0
- package/dist/ui-command.js.map +7 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +95 -32
- package/dist/wizard.js.map +2 -2
- package/dist/world-packs.d.ts +2 -0
- package/dist/world-packs.d.ts.map +1 -1
- package/dist/world-packs.js +161 -18
- package/dist/world-packs.js.map +2 -2
- package/package.json +21 -15
- package/scripts/module-install/check-modules.mjs +178 -0
- package/scripts/module-install/fetch-module.mjs +1083 -0
- package/scripts/module-install/lib/catalog.mjs +96 -0
- package/scripts/module-install/lib/config-seeding.mjs +128 -0
- package/scripts/module-install/lib/io.mjs +37 -0
- package/scripts/module-install/lib/link-from.mjs +80 -0
- package/scripts/module-install/lib/lock.mjs +78 -0
- package/scripts/module-install/lib/npm-resolver.mjs +55 -0
- package/scripts/module-install/lib/packages.mjs +136 -0
- package/scripts/module-install/lib/paths.mjs +48 -0
- package/scripts/module-install/lib/registry-index.mjs +50 -0
- package/dist/cmd-remote.d.ts +0 -13
- package/dist/cmd-remote.d.ts.map +0 -1
- package/dist/cmd-remote.js +0 -38
- package/dist/cmd-remote.js.map +0 -7
- package/dist/remote-agent.d.ts +0 -18
- package/dist/remote-agent.d.ts.map +0 -1
- package/dist/remote-agent.js +0 -198
- package/dist/remote-agent.js.map +0 -7
|
@@ -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://raw.githubusercontent.com/DogeLakeDev/ScriptsForMinecraftServer/main/modules/sdk/@sfmc-sdk/schemas/module_catalog.schema.json";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {import("./packages.mjs").CatalogEntry} CatalogEntry
|
|
19
|
+
* @typedef {import("@sfmc-bds/sdk/contracts").ModuleCatalog} 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,128 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** 模块默认配置播种:仅补缺,不覆盖用户已有值。 */
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const CONFIG_KEY_RE = /^[A-Za-z0-9_-]+$/;
|
|
8
|
+
|
|
9
|
+
/** @param {unknown} value */
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
12
|
+
const proto = Object.getPrototypeOf(value);
|
|
13
|
+
return proto === Object.prototype || proto === null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {string} root @param {string} candidate */
|
|
17
|
+
function assertWithin(root, candidate) {
|
|
18
|
+
const absRoot = path.resolve(root);
|
|
19
|
+
const absCandidate = path.resolve(candidate);
|
|
20
|
+
const rel = path.relative(absRoot, absCandidate);
|
|
21
|
+
if (rel === "" || (!rel.startsWith(".." + path.sep) && rel !== ".." && !path.isAbsolute(rel))) {
|
|
22
|
+
return absCandidate;
|
|
23
|
+
}
|
|
24
|
+
throw new Error("配置路径越界: " + absCandidate);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @param {string} file @param {string} label */
|
|
28
|
+
function readObject(file, label) {
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
throw new Error(label + " JSON 解析失败: " + message);
|
|
35
|
+
}
|
|
36
|
+
if (!isPlainObject(parsed)) throw new Error(label + " 顶层必须是 JSON 对象");
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {Record<string, unknown>} current
|
|
42
|
+
* @param {Record<string, unknown>} defaults
|
|
43
|
+
* @param {string} [prefix]
|
|
44
|
+
* @returns {{ value: Record<string, unknown>; addedPaths: string[] }}
|
|
45
|
+
*/
|
|
46
|
+
export function mergeMissing(current, defaults, prefix = "") {
|
|
47
|
+
const value = structuredClone(current);
|
|
48
|
+
/** @type {string[]} */
|
|
49
|
+
const addedPaths = [];
|
|
50
|
+
for (const key of Object.keys(defaults).sort()) {
|
|
51
|
+
const fieldPath = prefix ? prefix + "." + key : key;
|
|
52
|
+
if (!Object.hasOwn(value, key)) {
|
|
53
|
+
value[key] = structuredClone(defaults[key]);
|
|
54
|
+
addedPaths.push(fieldPath);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (isPlainObject(value[key]) && isPlainObject(defaults[key])) {
|
|
58
|
+
const nested = mergeMissing(
|
|
59
|
+
/** @type {Record<string, unknown>} */ (value[key]),
|
|
60
|
+
/** @type {Record<string, unknown>} */ (defaults[key]),
|
|
61
|
+
fieldPath
|
|
62
|
+
);
|
|
63
|
+
value[key] = nested.value;
|
|
64
|
+
addedPaths.push(...nested.addedPaths);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { value, addedPaths: addedPaths.sort() };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {{
|
|
72
|
+
* status: "created" | "updated" | "unchanged" | "missing-defaults";
|
|
73
|
+
* target: string;
|
|
74
|
+
* addedPaths: string[];
|
|
75
|
+
* }} SeedResult
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {{ moduleRoot: string; projectRoot: string; configKey: string }} options
|
|
80
|
+
* @returns {SeedResult}
|
|
81
|
+
*/
|
|
82
|
+
export function seedModuleConfig(options) {
|
|
83
|
+
const { moduleRoot, projectRoot, configKey } = options;
|
|
84
|
+
if (!CONFIG_KEY_RE.test(configKey)) throw new Error('invalid configKey "' + configKey + '"');
|
|
85
|
+
|
|
86
|
+
const safeModuleRoot = path.resolve(moduleRoot);
|
|
87
|
+
const safeProjectRoot = path.resolve(projectRoot);
|
|
88
|
+
const source = assertWithin(safeModuleRoot, path.join(safeModuleRoot, "configs-default", configKey + ".json"));
|
|
89
|
+
const configsDir = assertWithin(safeProjectRoot, path.join(safeProjectRoot, "configs"));
|
|
90
|
+
const target = assertWithin(configsDir, path.join(configsDir, configKey + ".json"));
|
|
91
|
+
if (!fs.existsSync(source)) {
|
|
92
|
+
const defaultsDir = path.dirname(source);
|
|
93
|
+
const jsonFiles = fs.existsSync(defaultsDir)
|
|
94
|
+
? fs.readdirSync(defaultsDir).filter((name) => name.toLowerCase().endsWith(".json"))
|
|
95
|
+
: [];
|
|
96
|
+
if (jsonFiles.length > 0) {
|
|
97
|
+
throw new Error("默认配置文件名必须为 configs-default/" + configKey + ".json,实际为 " + jsonFiles.join(", "));
|
|
98
|
+
}
|
|
99
|
+
return { status: "missing-defaults", target, addedPaths: [] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const defaults = readObject(source, "默认配置 " + source);
|
|
103
|
+
let status = /** @type {SeedResult["status"]} */ ("created");
|
|
104
|
+
let value = defaults;
|
|
105
|
+
let addedPaths = Object.keys(defaults).sort();
|
|
106
|
+
if (fs.existsSync(target)) {
|
|
107
|
+
const current = readObject(target, "用户配置 " + target);
|
|
108
|
+
const merged = mergeMissing(current, defaults);
|
|
109
|
+
value = merged.value;
|
|
110
|
+
addedPaths = merged.addedPaths;
|
|
111
|
+
status = addedPaths.length > 0 ? "updated" : "unchanged";
|
|
112
|
+
}
|
|
113
|
+
if (status === "unchanged") return { status, target, addedPaths };
|
|
114
|
+
|
|
115
|
+
fs.mkdirSync(configsDir, { recursive: true });
|
|
116
|
+
const temp = assertWithin(configsDir, path.join(configsDir, "." + configKey + "." + randomUUID() + ".tmp"));
|
|
117
|
+
try {
|
|
118
|
+
fs.writeFileSync(temp, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", flag: "wx" });
|
|
119
|
+
fs.renameSync(temp, target);
|
|
120
|
+
} finally {
|
|
121
|
+
try {
|
|
122
|
+
fs.rmSync(temp, { force: true });
|
|
123
|
+
} catch {
|
|
124
|
+
/* best-effort */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { status, target, addedPaths };
|
|
128
|
+
}
|
|
@@ -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 {import("@sfmc-bds/sdk/contracts").ModuleRuntimeState} LockState
|
|
12
|
+
* @typedef {import("@sfmc-bds/sdk/contracts").ModuleLock} 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,136 @@
|
|
|
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 {import("@sfmc-bds/sdk/contracts").ModuleCatalogEntry} CatalogEntry
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** @returns {PackageInfo[]} */
|
|
38
|
+
export function scanInstalledPackages() {
|
|
39
|
+
if (!exists(PACKAGES_DIR)) return [];
|
|
40
|
+
/** @type {PackageInfo[]} */
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const ent of fs.readdirSync(PACKAGES_DIR, { withFileTypes: true })) {
|
|
43
|
+
if (ent.name.startsWith(".")) continue;
|
|
44
|
+
const folder = ent.name;
|
|
45
|
+
const abs = path.join(PACKAGES_DIR, folder);
|
|
46
|
+
// 跟随 symlink:Dirent.isDirectory() 对链接目录为 false
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.statSync(abs).isDirectory()) continue;
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const manifestPath = packageManifestPath(folder);
|
|
53
|
+
if (!exists(manifestPath)) continue;
|
|
54
|
+
const manifest = readJson(manifestPath, null);
|
|
55
|
+
if (!manifest || typeof manifest !== "object") continue;
|
|
56
|
+
const entryAbs = packageEntryPath(folder);
|
|
57
|
+
out.push({
|
|
58
|
+
folder,
|
|
59
|
+
manifest,
|
|
60
|
+
manifestPath,
|
|
61
|
+
entryAbs,
|
|
62
|
+
hasEntry: exists(entryAbs),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
out.sort((a, b) => a.folder.localeCompare(b.folder));
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 从 v2 / v3 manifest 及包所在目录名投影生成 catalog 条目。
|
|
71
|
+
* - v2 manifest:投影基础核心字段,不包含 semantic 块;
|
|
72
|
+
* - v3 manifest:在核心字段基础上将 semantic 块深拷贝透传至条目中;
|
|
73
|
+
* - 契约原则:本函数仅做结构投影与深拷贝,不负责校验字段形状合法性;形状校验统一以 SDK 的 `validateManifest` 为准(由 check-modules 离线自检或安装器统一调用)。
|
|
74
|
+
*
|
|
75
|
+
* @param {string} folder 安装目录名。
|
|
76
|
+
* @param {Record<string, unknown>} manifest 原始 manifest 对象。
|
|
77
|
+
* @returns {CatalogEntry} 投影生成的 catalog 条目。
|
|
78
|
+
*/
|
|
79
|
+
export function projectCatalogEntry(folder, manifest) {
|
|
80
|
+
const id = String(manifest.id || "").trim();
|
|
81
|
+
const configKey = String(manifest.configKey || "").trim();
|
|
82
|
+
if (!id) throw new Error(`packages/${folder}: manifest.id 缺失`);
|
|
83
|
+
if (!configKey) throw new Error(`packages/${folder}: manifest.configKey 缺失`);
|
|
84
|
+
|
|
85
|
+
/* 缺省:默认启用、允许禁用;不可禁用须在 manifest 显式 canDisable:false */
|
|
86
|
+
const enabledByDefault = typeof manifest.enabledByDefault === "boolean" ? manifest.enabledByDefault : true;
|
|
87
|
+
const canDisable = typeof manifest.canDisable === "boolean" ? manifest.canDisable : true;
|
|
88
|
+
|
|
89
|
+
/** @type {CatalogEntry} */
|
|
90
|
+
const entry = {
|
|
91
|
+
id,
|
|
92
|
+
configKey,
|
|
93
|
+
name: String(manifest.name || configKey),
|
|
94
|
+
description: String(manifest.description || ""),
|
|
95
|
+
enabledByDefault,
|
|
96
|
+
canDisable,
|
|
97
|
+
requires: Array.isArray(manifest.requires) ? manifest.requires.map(String) : [],
|
|
98
|
+
entry: {
|
|
99
|
+
kind: "sapi",
|
|
100
|
+
path: catalogEntryRelPath(folder),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
/* v3 semantic 语义块投影:
|
|
104
|
+
* 采用 structuredClone 执行只读深拷贝透传,不在此处重复校验字段形状。
|
|
105
|
+
* 契约原则:字段形状与类型的合法性统一以 SDK validateManifest 为唯一权威(由 check-modules 等上层检查脚本统一调用验证)。 */
|
|
106
|
+
if (manifest.schemaVersion === 3 && manifest.semantic && typeof manifest.semantic === "object") {
|
|
107
|
+
entry.semantic = /** @type {SemanticBlock} */ (structuredClone(manifest.semantic));
|
|
108
|
+
}
|
|
109
|
+
return entry;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
/** @param {string} folder @returns {CatalogEntry | null} */
|
|
114
|
+
export function loadPackageCatalogEntry(folder) {
|
|
115
|
+
const manifestPath = packageManifestPath(folder);
|
|
116
|
+
if (!exists(manifestPath)) return null;
|
|
117
|
+
const manifest = readJson(manifestPath, null);
|
|
118
|
+
if (!manifest) return null;
|
|
119
|
+
return projectCatalogEntry(folder, manifest);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @param {string} folder */
|
|
123
|
+
export function packageExists(folder) {
|
|
124
|
+
return exists(packageDir(folder));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 从 entry.path 解析 packages 目录名
|
|
129
|
+
* @param {string} entryPath
|
|
130
|
+
*/
|
|
131
|
+
export function folderFromEntryPath(entryPath) {
|
|
132
|
+
const m = String(entryPath || "")
|
|
133
|
+
.replace(/\\/g, "/")
|
|
134
|
+
.match(/modules\/packages\/([^/]+)\//);
|
|
135
|
+
return m ? m[1] : null;
|
|
136
|
+
}
|
|
@@ -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
|
+
}
|