@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.
- package/dist/argv-parse.d.ts +11 -0
- package/dist/argv-parse.d.ts.map +1 -0
- package/dist/argv-parse.js +28 -0
- package/dist/argv-parse.js.map +7 -0
- package/dist/cli-gate.d.ts +11 -0
- package/dist/cli-gate.d.ts.map +1 -0
- package/dist/cli-gate.js +46 -0
- package/dist/cli-gate.js.map +7 -0
- package/dist/command-palette.d.ts +61 -0
- package/dist/command-palette.d.ts.map +1 -0
- package/dist/command-palette.js +352 -0
- package/dist/command-palette.js.map +7 -0
- package/dist/command-surface.d.ts +85 -0
- package/dist/command-surface.d.ts.map +1 -0
- package/dist/command-surface.js +292 -0
- package/dist/command-surface.js.map +7 -0
- package/dist/commands-reload.js +2 -2
- package/dist/commands-reload.js.map +2 -2
- package/dist/commands.d.ts +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +196 -21
- package/dist/commands.js.map +2 -2
- package/dist/debug-command.d.ts +5 -0
- package/dist/debug-command.d.ts.map +1 -0
- package/dist/debug-command.js +118 -0
- package/dist/debug-command.js.map +7 -0
- package/dist/help-text.d.ts +3 -0
- package/dist/help-text.d.ts.map +1 -0
- package/dist/help-text.js +160 -0
- package/dist/help-text.js.map +7 -0
- package/dist/i18n/locales/en.d.ts +103 -89
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +105 -91
- 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 +138 -118
- package/dist/i18n/locales/zh-CN.js.map +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +2 -2
- 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 +69 -0
- package/dist/log-filter.d.ts.map +1 -0
- package/dist/log-filter.js +150 -0
- package/dist/log-filter.js.map +7 -0
- package/dist/logs.d.ts +29 -7
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +164 -72
- package/dist/logs.js.map +3 -3
- package/dist/main.js +55 -46
- package/dist/main.js.map +2 -2
- package/dist/module-commands.d.ts +17 -40
- package/dist/module-commands.d.ts.map +1 -1
- package/dist/module-commands.js +129 -85
- package/dist/module-commands.js.map +2 -2
- package/dist/module-pack-build.d.ts +10 -0
- package/dist/module-pack-build.d.ts.map +1 -0
- package/dist/module-pack-build.js +434 -0
- package/dist/module-pack-build.js.map +7 -0
- package/dist/module-toggle.d.ts +33 -0
- package/dist/module-toggle.d.ts.map +1 -0
- package/dist/module-toggle.js +28 -0
- package/dist/module-toggle.js.map +7 -0
- 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 +1 -1
- package/dist/pack-lifecycle.d.ts.map +1 -1
- package/dist/pack-lifecycle.js +14 -4
- package/dist/pack-lifecycle.js.map +2 -2
- package/dist/pack-update/index.d.ts +18 -0
- package/dist/pack-update/index.d.ts.map +1 -0
- package/dist/pack-update/index.js +113 -0
- package/dist/pack-update/index.js.map +7 -0
- package/dist/registry.d.ts +7 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +15 -3
- package/dist/registry.js.map +2 -2
- package/dist/repl-windows/format-display.d.ts +10 -0
- package/dist/repl-windows/format-display.d.ts.map +1 -0
- package/dist/repl-windows/format-display.js +57 -0
- package/dist/repl-windows/format-display.js.map +7 -0
- package/dist/repl-windows/host.d.ts +60 -0
- package/dist/repl-windows/host.d.ts.map +1 -0
- package/dist/repl-windows/host.js +179 -0
- package/dist/repl-windows/host.js.map +7 -0
- package/dist/repl-windows/index.d.ts +9 -0
- package/dist/repl-windows/index.d.ts.map +1 -0
- package/dist/repl-windows/index.js +13 -0
- package/dist/repl-windows/index.js.map +7 -0
- package/dist/repl-windows/logs-filter-window.d.ts +15 -0
- package/dist/repl-windows/logs-filter-window.d.ts.map +1 -0
- package/dist/repl-windows/logs-filter-window.js +77 -0
- package/dist/repl-windows/logs-filter-window.js.map +7 -0
- package/dist/repl-windows/service-window.d.ts +4 -0
- package/dist/repl-windows/service-window.d.ts.map +1 -0
- package/dist/repl-windows/service-window.js +35 -0
- package/dist/repl-windows/service-window.js.map +7 -0
- package/dist/repl-windows/types.d.ts +56 -0
- package/dist/repl-windows/types.d.ts.map +1 -0
- package/dist/repl-windows/types.js +1 -0
- package/dist/repl-windows/types.js.map +7 -0
- package/dist/repl.d.ts +6 -9
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +698 -427
- package/dist/repl.js.map +3 -3
- package/dist/runtime.d.ts +6 -13
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +23 -28
- package/dist/runtime.js.map +2 -2
- package/dist/send-target.d.ts +11 -0
- package/dist/send-target.d.ts.map +1 -0
- package/dist/send-target.js +42 -0
- package/dist/send-target.js.map +7 -0
- package/dist/services.d.ts +50 -4
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +244 -20
- package/dist/services.js.map +2 -2
- package/dist/theme.js +1 -1
- package/dist/theme.js.map +2 -2
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +76 -16
- package/dist/wizard.js.map +2 -2
- package/dist/world-packs.d.ts +15 -0
- package/dist/world-packs.d.ts.map +1 -1
- package/dist/world-packs.js +90 -87
- package/dist/world-packs.js.map +2 -2
- package/package.json +24 -21
- package/scripts/module-install/check-modules.mjs +204 -0
- package/scripts/module-install/fetch-module.mjs +1007 -0
- package/scripts/module-install/install-resolver.test.mjs +48 -0
- package/scripts/module-install/lib/catalog.mjs +96 -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 +142 -0
- package/scripts/module-install/lib/paths.mjs +48 -0
- package/scripts/module-install/lib/registry-index.mjs +50 -0
- package/scripts/module-install/link-local.test.mjs +51 -0
- package/scripts/module-install/local-resolver.test.mjs +51 -0
- package/scripts/module-install/registry-index.test.mjs +28 -0
- package/dist/module-wizard.d.ts +0 -14
- package/dist/module-wizard.d.ts.map +0 -1
- package/dist/module-wizard.js +0 -313
- package/dist/module-wizard.js.map +0 -7
- package/dist/pack-update/bindings.d.ts +0 -12
- package/dist/pack-update/bindings.d.ts.map +0 -1
- package/dist/pack-update/bindings.js +0 -64
- package/dist/pack-update/bindings.js.map +0 -7
- package/dist/pack-update/config.d.ts +0 -22
- package/dist/pack-update/config.d.ts.map +0 -1
- package/dist/pack-update/config.js +0 -141
- package/dist/pack-update/config.js.map +0 -7
- package/dist/pack-update/providers/curseforge.d.ts +0 -23
- package/dist/pack-update/providers/curseforge.d.ts.map +0 -1
- package/dist/pack-update/providers/curseforge.js +0 -209
- package/dist/pack-update/providers/curseforge.js.map +0 -7
- package/dist/pack-update/providers/index.d.ts +0 -16
- package/dist/pack-update/providers/index.d.ts.map +0 -1
- package/dist/pack-update/providers/index.js +0 -37
- package/dist/pack-update/providers/index.js.map +0 -7
- package/dist/pack-update/service.d.ts +0 -21
- package/dist/pack-update/service.d.ts.map +0 -1
- package/dist/pack-update/service.js +0 -567
- package/dist/pack-update/service.js.map +0 -7
- package/dist/pack-update/types.d.ts +0 -126
- package/dist/pack-update/types.d.ts.map +0 -1
- package/dist/pack-update/types.js +0 -8
- package/dist/pack-update/types.js.map +0 -7
- package/dist/pack-update/version-policy.d.ts +0 -80
- package/dist/pack-update/version-policy.d.ts.map +0 -1
- package/dist/pack-update/version-policy.js +0 -216
- package/dist/pack-update/version-policy.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 -196
- 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
|
+
});
|