@sfmc-bds/devkit 0.1.0-beta.1 → 0.1.0-beta.2

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/README.md CHANGED
@@ -5,3 +5,10 @@
5
5
  ```ts
6
6
  import { startModuleWatch, rebuildAndDeploy, scaffoldModule } from "@sfmc-bds/devkit";
7
7
  ```
8
+
9
+ 脚手架 CLI:
10
+
11
+ ```bash
12
+ npx sfmc-new-module my-mod --name "我的模块"
13
+ # 或:node node_modules/@sfmc-bds/devkit/scripts/new-module.mjs …
14
+ ```
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/paths.ts", "../src/module-root.ts", "../src/sfmc-root.ts", "../src/sfmc-cli.ts", "../src/watch.ts", "../src/scaffold.ts", "../src/rebuild.ts", "../src/module-toggle.ts"],
4
- "sourcesContent": ["import path from \"node:path\";\n\n/** \u89E3\u6790 `--from local[:path]` / `dir:` \u6216 cwd \u4E3A\u6A21\u5757\u6839\u3002 */\nexport function resolveLocalModuleRoot(args: { from?: string | null; cwd: string }): string {\n const raw = (args.from ?? \"\").trim();\n if (!raw) return path.resolve(args.cwd);\n if (raw === \"local\") return path.resolve(args.cwd);\n if (raw.startsWith(\"local:\")) {\n const p = raw.slice(\"local:\".length).trim();\n if (!p) return path.resolve(args.cwd);\n return path.isAbsolute(p) ? path.resolve(p) : path.resolve(args.cwd, p);\n }\n if (raw.startsWith(\"dir:\")) {\n const p = raw.slice(\"dir:\".length).trim();\n return path.isAbsolute(p) ? path.resolve(p) : path.resolve(args.cwd, p);\n }\n return path.resolve(args.cwd);\n}\n", "/**\n * \u6709\u6548\u6A21\u5757\u6839\u5224\u5B9A\uFF08\u6269\u5C55\u4E0E CLI \u5171\u7528\u6743\u5A01\uFF09\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport type ModuleRootInfo = {\n root: string;\n id: string;\n name?: string;\n schemaVersion: number;\n};\n\n/** \u76EE\u5F55\u662F\u5426\u4E3A\u6709\u6548 SFMC \u6A21\u5757\u6839\uFF1Apackage.json + sapi/manifest.json schemaVersion===2 + \u975E\u7A7A id */\nexport function isValidModuleRoot(dir: string): boolean {\n return readModuleRootInfo(dir) !== null;\n}\n\nexport function readModuleRootInfo(dir: string): ModuleRootInfo | null {\n const pkg = path.join(dir, \"package.json\");\n const manifestPath = path.join(dir, \"sapi\", \"manifest.json\");\n if (!fs.existsSync(pkg) || !fs.existsSync(manifestPath)) return null;\n try {\n const m = JSON.parse(fs.readFileSync(manifestPath, \"utf8\")) as {\n schemaVersion?: number;\n id?: string;\n name?: string;\n };\n if (m.schemaVersion !== 2) return null;\n if (typeof m.id !== \"string\" || !m.id.trim()) return null;\n return {\n root: path.resolve(dir),\n id: m.id.trim(),\n name: typeof m.name === \"string\" ? m.name : undefined,\n schemaVersion: 2,\n };\n } catch {\n return null;\n }\n}\n\n/** \u81EA filePath \u5411\u4E0A\u627E\u6700\u8FD1\u6709\u6548\u6A21\u5757\u6839\uFF1B\u627E\u4E0D\u5230\u8FD4\u56DE null */\nexport function findModuleRootFromFile(filePath: string): string | null {\n let cur = path.dirname(path.resolve(filePath));\n const root = path.parse(cur).root;\n while (true) {\n if (isValidModuleRoot(cur)) return cur;\n if (cur === root) return null;\n const parent = path.dirname(cur);\n if (parent === cur) return null;\n cur = parent;\n }\n}\n", "/**\n * SFMC \u5DE5\u4F5C\u76EE\u5F55\u5224\u5B9A\uFF08\u8FD0\u884C\u65F6\u6839\uFF1Aconfigs/\u3001modules/\uFF1B\u4E0D\u662F\u6E90\u7801\u4ED3\u5E93\u6839\uFF09\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\n/** \u76EE\u5F55\u662F\u5426\u4E3A\u6709\u6548 SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF08\u542B configs/ \u4E0E modules/\uFF09\u3002 */\nexport function isValidSfmcRoot(dir: string): boolean {\n if (!dir || !dir.trim()) return false;\n const root = path.resolve(dir);\n try {\n return (\n fs.statSync(path.join(root, \"configs\")).isDirectory() &&\n fs.statSync(path.join(root, \"modules\")).isDirectory()\n );\n } catch {\n return false;\n }\n}\n", "/**\n * \u89E3\u6790\u5E76 spawn sfmc CLI\u3002\n * CLI \u4E0E SFMC \u5DE5\u4F5C\u76EE\u5F55\u65E0\u5173\uFF1A\u4ECE\u672C\u5305/\u6269\u5C55\u5B89\u88C5\u6811\u3001PATH \u6216\u663E\u5F0F\u914D\u7F6E\u89E3\u6790\uFF1B\n * \u5DE5\u4F5C\u76EE\u5F55\u53EA\u4F5C\u4E3A spawn \u7684 cwd \u4E0E SFMC_ROOT\u3002\n */\n\nimport { spawn } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isValidSfmcRoot } from \"./sfmc-root.js\";\n\n/** chalk.hex(\"#e06c75\") \u7684\u771F\u5F69\u5E8F\u5217\uFF1BCLI \u5931\u8D25\u6587\u6848\u7ECF c.red() \u5305\u88C5\u3002 */\nconst CLI_ANSI_RED = \"\\u001b[38;2;224;108;117m\";\n\nexport interface ResolveSfmcCliOptions {\n /** \u663E\u5F0F CLI \u5165\u53E3\uFF08.js/.mjs\uFF09\uFF1B\u4E5F\u53EF\u7528\u73AF\u5883\u53D8\u91CF SFMC_CLI\u3002 */\n cliPath?: string;\n}\n\nfunction tryRequireCli(from: string): string | null {\n try {\n const resolved = createRequire(from).resolve(\"@sfmc-bds/cli/cli\");\n return fs.existsSync(resolved) ? resolved : null;\n } catch {\n return null;\n }\n}\n\n/** \u81EA PATH \u4E0A\u7684 sfmc \u53EF\u6267\u884C\u6587\u4EF6\u65C1\u89E3\u6790 @sfmc-bds/cli\uFF08npm global / \u672C\u5730 .bin\uFF09\u3002 */\nfunction resolveCliFromPathEnv(): string | null {\n const pathEnv = process.env.PATH ?? \"\";\n const names =\n process.platform === \"win32\" ? [\"sfmc.cmd\", \"sfmc.exe\", \"sfmc.ps1\", \"sfmc\"] : [\"sfmc\"];\n for (const dir of pathEnv.split(path.delimiter)) {\n if (!dir) continue;\n let hit = false;\n for (const name of names) {\n if (fs.existsSync(path.join(dir, name))) {\n hit = true;\n break;\n }\n }\n if (!hit) continue;\n const nearCli = path.join(dir, \"..\", \"@sfmc-bds\", \"cli\", \"dist\", \"main.js\");\n if (fs.existsSync(nearCli)) return nearCli;\n const nearMeta = path.join(dir, \"..\", \"@sfmc-bds\", \"sfmc\", \"bin\", \"sfmc.mjs\");\n if (fs.existsSync(nearMeta)) return nearMeta;\n const fromBin = tryRequireCli(path.join(dir, \"..\", \"package.json\"));\n if (fromBin) return fromBin;\n }\n return null;\n}\n\n/**\n * \u89E3\u6790 sfmc CLI \u811A\u672C\u8DEF\u5F84\u3002\n * \u987A\u5E8F\uFF1A\u663E\u5F0F cliPath / SFMC_CLI \u2192 \u672C\u5305\u5B89\u88C5\u6811\uFF08@sfmc-bds/cli\uFF09\u2192 cwd \u7684 node_modules \u2192 PATH\u3002\n */\nexport function resolveSfmcCli(opts: ResolveSfmcCliOptions = {}): string {\n const explicit = (opts.cliPath || process.env.SFMC_CLI || \"\").trim();\n if (explicit) {\n const p = path.resolve(explicit);\n if (!fs.existsSync(p)) {\n throw new Error(`\u6307\u5B9A\u7684 sfmc CLI \u4E0D\u5B58\u5728\uFF1A${p}\uFF08\u68C0\u67E5 SFMC_CLI / sfmc.cliPath\uFF09`);\n }\n return p;\n }\n\n const here = fileURLToPath(import.meta.url);\n const fromInstall = tryRequireCli(here);\n if (fromInstall) return fromInstall;\n\n const fromCwd = tryRequireCli(path.join(process.cwd(), \"package.json\"));\n if (fromCwd) return fromCwd;\n\n const fromPath = resolveCliFromPathEnv();\n if (fromPath) return fromPath;\n\n throw new Error(\n \"\u65E0\u6CD5\u89E3\u6790 sfmc CLI\u3002\u8BF7\u5B89\u88C5 @sfmc-bds/cli\uFF08\u6216\u786E\u4FDD PATH \u4E0A\u6709 sfmc\uFF09\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF SFMC_CLI / \u6269\u5C55\u914D\u7F6E sfmc.cliPath\u3002\"\n );\n}\n\nfunction looksLikeCliFailure(output: string): boolean {\n if (output.includes(CLI_ANSI_RED)) return true;\n if (/\\[pack-manager\\].*required/.test(output)) return true;\n if (/^Error:/m.test(output)) return true;\n return false;\n}\n\nexport interface RunSfmcCliResult {\n ok: boolean;\n output: string;\n}\n\nexport interface RunSfmcCliOptions {\n cliPath?: string;\n}\n\n/** \u5728 SFMC \u5DE5\u4F5C\u76EE\u5F55\u4E0B\u6267\u884C `node <sfmc-cli> ...args`\uFF08env.SFMC_ROOT = \u5DE5\u4F5C\u76EE\u5F55\uFF09\u3002 */\nexport async function runSfmcCli(\n sfmcRoot: string,\n args: string[],\n opts: RunSfmcCliOptions = {}\n): Promise<RunSfmcCliResult> {\n const root = path.resolve(sfmcRoot);\n if (!isValidSfmcRoot(root)) {\n return {\n ok: false,\n output: `\u65E0\u6548\u7684 SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF1A${root}\uFF08\u9700\u8981\u542B configs/ \u4E0E modules/\uFF0C\u4E0D\u5FC5\u542B\u6E90\u7801\u4ED3\u5E93\uFF09`,\n };\n }\n\n let script: string;\n try {\n script = resolveSfmcCli({ cliPath: opts.cliPath });\n } catch (e) {\n return { ok: false, output: e instanceof Error ? e.message : String(e) };\n }\n\n const r = await new Promise<{ code: number | null; output: string }>((resolve) => {\n const proc = spawn(process.execPath, [script, ...args], {\n cwd: root,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n env: { ...process.env, SFMC_ROOT: root },\n });\n let out = \"\";\n proc.stdout?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.stderr?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.on(\"exit\", (code) => resolve({ code, output: out }));\n proc.on(\"error\", (e) => resolve({ code: 1, output: e.message }));\n });\n\n const ok = r.code === 0 && !looksLikeCliFailure(r.output);\n return { ok, output: r.output };\n}\n", "/**\n * \u76D1\u542C\u6A21\u5757 sapi/src\uFF0C\u9632\u6296\u540E\u56DE\u8C03 onRebuild\uFF08\u7531\u8C03\u7528\u65B9\u6CE8\u5165 rebuildAndDeploy / \u5176\u5B83\uFF09\u3002\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport interface ModuleWatchOptions {\n moduleRoot: string;\n debounceMs?: number;\n log?: (line: string) => void;\n onRebuild: () => Promise<{ ok: boolean; message?: string }>;\n}\n\nexport function startModuleWatch(opts: ModuleWatchOptions): { stop: () => void } {\n const log = opts.log ?? ((line: string) => console.log(line));\n const debounceMs = opts.debounceMs ?? 200;\n const sapiDir = path.join(opts.moduleRoot, \"sapi\");\n const srcDir = path.join(sapiDir, \"src\");\n\n if (!fs.existsSync(sapiDir)) {\n log(`[devkit:watch] \u65E0 sapi \u76EE\u5F55: ${sapiDir}`);\n return { stop: () => {} };\n }\n\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let pending = new Set<string>();\n let running = false;\n let stopped = false;\n\n log(`[devkit:watch] \u76D1\u542C ${opts.moduleRoot}`);\n\n const runOnce = async () => {\n if (running || stopped) return;\n running = true;\n const changed = [...pending];\n pending = new Set();\n const t0 = Date.now();\n log(`[devkit:watch] rebuild (${changed.length})\u2026`);\n try {\n const r = await opts.onRebuild();\n const ms = Date.now() - t0;\n if (r.ok) log(`[devkit:watch] ok ${ms}ms`);\n else log(`[devkit:watch] fail ${ms}ms ${r.message ?? \"\"}`);\n } catch (e) {\n log(`[devkit:watch] error ${(e as Error).message}`);\n } finally {\n running = false;\n }\n };\n\n const onSrc = (rel: string) => {\n if (stopped) return;\n if (rel.includes(\"node_modules\") || rel.includes(\"dist\") || rel.endsWith(\".tgz\")) return;\n pending.add(rel);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => void runOnce(), debounceMs);\n };\n\n const watchers: fs.FSWatcher[] = [];\n if (fs.existsSync(srcDir)) {\n try {\n watchers.push(\n fs.watch(srcDir, { recursive: true, persistent: true }, (_e, filename) => {\n if (filename) onSrc(filename.toString());\n })\n );\n } catch {\n watchers.push(\n fs.watch(srcDir, { persistent: true }, (_e, filename) => {\n if (filename) onSrc(filename.toString());\n })\n );\n }\n }\n\n const watchMeta = (file: string, kind: string) => {\n if (!fs.existsSync(file)) return;\n watchers.push(\n fs.watch(file, { persistent: true }, (event) => {\n if (event === \"change\") log(`[devkit:watch] ${kind} \u5DF2\u6539\uFF0C\u9700\u91CD\u542F BDS\uFF08SAPI \u542F\u52A8\u671F\u7F13\u5B58\uFF09`);\n })\n );\n };\n watchMeta(path.join(sapiDir, \"manifest.json\"), \"manifest\");\n watchMeta(path.join(sapiDir, \"tsconfig.json\"), \"tsconfig\");\n\n return {\n stop() {\n stopped = true;\n if (debounceTimer) clearTimeout(debounceTimer);\n for (const w of watchers) {\n try {\n w.close();\n } catch {\n /* ignore */\n }\n }\n log(\"[devkit:watch] stopped\");\n },\n };\n}\n", "/**\n * \u811A\u624B\u67B6\uFF1A\u59D4\u6258 @sfmc-bds/tools/new-module\uFF08\u6216 monorepo packages/tools/new-module.mjs\uFF09\u3002\n */\n\nimport { spawn } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { findMonorepoRoot } from \"@sfmc-bds/sdk/node/config\";\n\nexport interface ScaffoldOptions {\n /** \u76EE\u6807\u76EE\u5F55\uFF08\u987B\u4E3A\u7A7A\u6216\u53EF\u521B\u5EFA\uFF09\u3002 */\n targetDir: string;\n moduleId: string;\n displayName?: string;\n official?: boolean;\n}\n\nfunction resolveNewModuleScript(): string {\n const require = createRequire(import.meta.url);\n try {\n return require.resolve(\"@sfmc-bds/tools/new-module.mjs\");\n } catch {\n /* monorepo \u56DE\u9000 */\n }\n const monoRoot = findMonorepoRoot(process.cwd());\n const mono = monoRoot\n ? path.join(monoRoot, \"packages\", \"tools\", \"new-module.mjs\")\n : path.resolve(process.cwd(), \"packages/tools/new-module.mjs\");\n if (fs.existsSync(mono)) return mono;\n throw new Error(\"\u65E0\u6CD5\u89E3\u6790 new-module.mjs\uFF08\u5B89\u88C5 @sfmc-bds/tools \u6216\u5728 monorepo \u6839\u8FD0\u884C\uFF09\");\n}\n\nexport async function scaffoldModule(opts: ScaffoldOptions): Promise<{ ok: boolean; message: string }> {\n const dir = path.resolve(opts.targetDir);\n fs.mkdirSync(dir, { recursive: true });\n const script = resolveNewModuleScript();\n const args = [script, opts.moduleId];\n if (opts.displayName) args.push(\"--name\", opts.displayName);\n if (opts.official) args.push(\"--official\");\n\n return new Promise((resolve) => {\n const proc = spawn(process.execPath, args, {\n cwd: dir,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n env: process.env,\n });\n let out = \"\";\n proc.stdout?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.stderr?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.on(\"exit\", (code) => {\n resolve({\n ok: code === 0,\n message: out.trim() || (code === 0 ? `scaffolded ${opts.moduleId}` : `exit ${code}`),\n });\n });\n proc.on(\"error\", (e) => resolve({ ok: false, message: e.message }));\n });\n}\n", "/**\n * \u7EC4\u88C5\u5E76\u90E8\u7F72\u884C\u4E3A\u5305\uFF1A\u5728 SFMC_ROOT \u4E0B spawn `sfmc mod reload`\n * \uFF08\u4E0E CLI \u540C\u4E00\u5165\u53E3\uFF0C\u5B8C\u6574\u4F20\u53C2\uFF1B\u52FF\u76F4\u63A5\u88F8\u8C03 pack-manager verb\uFF09\u3002\n */\n\nimport { runSfmcCli } from \"./sfmc-cli.js\";\n\nexport interface RebuildOptions {\n /** SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF08\u542B configs/\u3001modules/\uFF09\u3002 */\n sfmcRoot: string;\n /** \u4EC5\u6784\u5EFA\u90E8\u7F72\uFF0C\u4E0D\u5411 BDS \u53D1 reload\uFF08\u5BF9\u5E94 `mod reload --build-only`\uFF09\u3002 */\n buildOnly?: boolean;\n /** \u53EF\u9009\uFF1A\u663E\u5F0F sfmc CLI \u5165\u53E3\uFF1B\u9ED8\u8BA4\u7531\u5B89\u88C5\u6811 / PATH / SFMC_CLI \u89E3\u6790\u3002 */\n cliPath?: string;\n}\n\nexport interface RebuildResult {\n ok: boolean;\n output: string;\n}\n\nexport async function rebuildAndDeploy(opts: RebuildOptions): Promise<RebuildResult> {\n const args = [\"mod\", \"reload\"];\n if (opts.buildOnly) args.push(\"--build-only\");\n return runSfmcCli(opts.sfmcRoot, args, { cliPath: opts.cliPath });\n}\n", "/**\n * \u6A21\u5757\u542F\u505C\uFF1Aspawn `sfmc mod enable|disable`\uFF08\u5199 module-lock + best-effort \u70ED\u540C\u6B65\uFF09\u3002\n */\n\nimport { runSfmcCli, type RunSfmcCliResult } from \"./sfmc-cli.js\";\n\nexport interface SetModuleEnabledOptions {\n /** SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF08\u542B modules/module-lock.json\uFF09\u3002 */\n sfmcRoot: string;\n /** \u6A21\u5757 logicalId / folderId / configKey\uFF08\u4E0E CLI `mod enable` \u4E00\u81F4\uFF09\u3002 */\n moduleId: string;\n enabled: boolean;\n /** \u53EF\u9009\uFF1A\u663E\u5F0F sfmc CLI \u5165\u53E3\uFF1B\u9ED8\u8BA4\u7531\u5B89\u88C5\u6811 / PATH / SFMC_CLI \u89E3\u6790\u3002 */\n cliPath?: string;\n}\n\nexport type SetModuleEnabledResult = RunSfmcCliResult;\n\n/** \u542F\u7528\u6216\u5173\u95ED\u6A21\u5757\uFF08\u4E0E CLI `mod enable|disable` \u540C\u6E90\uFF09\u3002 */\nexport async function setModuleEnabled(opts: SetModuleEnabledOptions): Promise<SetModuleEnabledResult> {\n const id = String(opts.moduleId || \"\").trim();\n if (!id) {\n return { ok: false, output: \"moduleId \u4E3A\u7A7A\" };\n }\n const action = opts.enabled ? \"enable\" : \"disable\";\n return runSfmcCli(opts.sfmcRoot, [\"mod\", action, id], { cliPath: opts.cliPath });\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import path from \"node:path\";\n\n/** \u89E3\u6790 `--from local[:path]` / `dir:` \u6216 cwd \u4E3A\u6A21\u5757\u6839\u3002 */\nexport function resolveLocalModuleRoot(args: { from?: string | null; cwd: string }): string {\n const raw = (args.from ?? \"\").trim();\n if (!raw) return path.resolve(args.cwd);\n if (raw === \"local\") return path.resolve(args.cwd);\n if (raw.startsWith(\"local:\")) {\n const p = raw.slice(\"local:\".length).trim();\n if (!p) return path.resolve(args.cwd);\n return path.isAbsolute(p) ? path.resolve(p) : path.resolve(args.cwd, p);\n }\n if (raw.startsWith(\"dir:\")) {\n const p = raw.slice(\"dir:\".length).trim();\n return path.isAbsolute(p) ? path.resolve(p) : path.resolve(args.cwd, p);\n }\n return path.resolve(args.cwd);\n}\n", "/**\n * \u6709\u6548\u6A21\u5757\u6839\u5224\u5B9A\uFF08\u6269\u5C55\u4E0E CLI \u5171\u7528\u6743\u5A01\uFF09\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport type ModuleRootInfo = {\n root: string;\n id: string;\n name?: string;\n schemaVersion: number;\n};\n\n/** \u76EE\u5F55\u662F\u5426\u4E3A\u6709\u6548 SFMC \u6A21\u5757\u6839\uFF1Apackage.json + sapi/manifest.json schemaVersion===2 + \u975E\u7A7A id */\nexport function isValidModuleRoot(dir: string): boolean {\n return readModuleRootInfo(dir) !== null;\n}\n\nexport function readModuleRootInfo(dir: string): ModuleRootInfo | null {\n const pkg = path.join(dir, \"package.json\");\n const manifestPath = path.join(dir, \"sapi\", \"manifest.json\");\n if (!fs.existsSync(pkg) || !fs.existsSync(manifestPath)) return null;\n try {\n const m = JSON.parse(fs.readFileSync(manifestPath, \"utf8\")) as {\n schemaVersion?: number;\n id?: string;\n name?: string;\n };\n if (m.schemaVersion !== 2) return null;\n if (typeof m.id !== \"string\" || !m.id.trim()) return null;\n return {\n root: path.resolve(dir),\n id: m.id.trim(),\n name: typeof m.name === \"string\" ? m.name : undefined,\n schemaVersion: 2,\n };\n } catch {\n return null;\n }\n}\n\n/** \u81EA filePath \u5411\u4E0A\u627E\u6700\u8FD1\u6709\u6548\u6A21\u5757\u6839\uFF1B\u627E\u4E0D\u5230\u8FD4\u56DE null */\nexport function findModuleRootFromFile(filePath: string): string | null {\n let cur = path.dirname(path.resolve(filePath));\n const root = path.parse(cur).root;\n while (true) {\n if (isValidModuleRoot(cur)) return cur;\n if (cur === root) return null;\n const parent = path.dirname(cur);\n if (parent === cur) return null;\n cur = parent;\n }\n}\n", "/**\n * SFMC \u5DE5\u4F5C\u76EE\u5F55\u5224\u5B9A\uFF08\u8FD0\u884C\u65F6\u6839\uFF1Aconfigs/\u3001modules/\uFF1B\u4E0D\u662F\u6E90\u7801\u4ED3\u5E93\u6839\uFF09\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\n/** \u76EE\u5F55\u662F\u5426\u4E3A\u6709\u6548 SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF08\u542B configs/ \u4E0E modules/\uFF09\u3002 */\nexport function isValidSfmcRoot(dir: string): boolean {\n if (!dir || !dir.trim()) return false;\n const root = path.resolve(dir);\n try {\n return (\n fs.statSync(path.join(root, \"configs\")).isDirectory() &&\n fs.statSync(path.join(root, \"modules\")).isDirectory()\n );\n } catch {\n return false;\n }\n}\n", "/**\n * \u89E3\u6790\u5E76 spawn sfmc CLI\u3002\n * CLI \u4E0E SFMC \u5DE5\u4F5C\u76EE\u5F55\u65E0\u5173\uFF1A\u4ECE\u672C\u5305/\u6269\u5C55\u5B89\u88C5\u6811\u3001PATH \u6216\u663E\u5F0F\u914D\u7F6E\u89E3\u6790\uFF1B\n * \u5DE5\u4F5C\u76EE\u5F55\u53EA\u4F5C\u4E3A spawn \u7684 cwd \u4E0E SFMC_ROOT\u3002\n */\n\nimport { spawn } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isValidSfmcRoot } from \"./sfmc-root.js\";\n\n/** chalk.hex(\"#e06c75\") \u7684\u771F\u5F69\u5E8F\u5217\uFF1BCLI \u5931\u8D25\u6587\u6848\u7ECF c.red() \u5305\u88C5\u3002 */\nconst CLI_ANSI_RED = \"\\u001b[38;2;224;108;117m\";\n\nexport interface ResolveSfmcCliOptions {\n /** \u663E\u5F0F CLI \u5165\u53E3\uFF08.js/.mjs\uFF09\uFF1B\u4E5F\u53EF\u7528\u73AF\u5883\u53D8\u91CF SFMC_CLI\u3002 */\n cliPath?: string;\n}\n\nfunction tryRequireCli(from: string): string | null {\n try {\n const resolved = createRequire(from).resolve(\"@sfmc-bds/cli/cli\");\n return fs.existsSync(resolved) ? resolved : null;\n } catch {\n return null;\n }\n}\n\n/** \u81EA PATH \u4E0A\u7684 sfmc \u53EF\u6267\u884C\u6587\u4EF6\u65C1\u89E3\u6790 @sfmc-bds/cli\uFF08npm global / \u672C\u5730 .bin\uFF09\u3002 */\nfunction resolveCliFromPathEnv(): string | null {\n const pathEnv = process.env.PATH ?? \"\";\n const names =\n process.platform === \"win32\" ? [\"sfmc.cmd\", \"sfmc.exe\", \"sfmc.ps1\", \"sfmc\"] : [\"sfmc\"];\n for (const dir of pathEnv.split(path.delimiter)) {\n if (!dir) continue;\n let hit = false;\n for (const name of names) {\n if (fs.existsSync(path.join(dir, name))) {\n hit = true;\n break;\n }\n }\n if (!hit) continue;\n const nearCli = path.join(dir, \"..\", \"@sfmc-bds\", \"cli\", \"dist\", \"main.js\");\n if (fs.existsSync(nearCli)) return nearCli;\n const nearMeta = path.join(dir, \"..\", \"@sfmc-bds\", \"sfmc\", \"bin\", \"sfmc.mjs\");\n if (fs.existsSync(nearMeta)) return nearMeta;\n const fromBin = tryRequireCli(path.join(dir, \"..\", \"package.json\"));\n if (fromBin) return fromBin;\n }\n return null;\n}\n\n/**\n * \u89E3\u6790 sfmc CLI \u811A\u672C\u8DEF\u5F84\u3002\n * \u987A\u5E8F\uFF1A\u663E\u5F0F cliPath / SFMC_CLI \u2192 \u672C\u5305\u5B89\u88C5\u6811\uFF08@sfmc-bds/cli\uFF09\u2192 cwd \u7684 node_modules \u2192 PATH\u3002\n */\nexport function resolveSfmcCli(opts: ResolveSfmcCliOptions = {}): string {\n const explicit = (opts.cliPath || process.env.SFMC_CLI || \"\").trim();\n if (explicit) {\n const p = path.resolve(explicit);\n if (!fs.existsSync(p)) {\n throw new Error(`\u6307\u5B9A\u7684 sfmc CLI \u4E0D\u5B58\u5728\uFF1A${p}\uFF08\u68C0\u67E5 SFMC_CLI / sfmc.cliPath\uFF09`);\n }\n return p;\n }\n\n const here = fileURLToPath(import.meta.url);\n const fromInstall = tryRequireCli(here);\n if (fromInstall) return fromInstall;\n\n const fromCwd = tryRequireCli(path.join(process.cwd(), \"package.json\"));\n if (fromCwd) return fromCwd;\n\n const fromPath = resolveCliFromPathEnv();\n if (fromPath) return fromPath;\n\n throw new Error(\n \"\u65E0\u6CD5\u89E3\u6790 sfmc CLI\u3002\u8BF7\u5B89\u88C5 @sfmc-bds/cli\uFF08\u6216\u786E\u4FDD PATH \u4E0A\u6709 sfmc\uFF09\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF SFMC_CLI / \u6269\u5C55\u914D\u7F6E sfmc.cliPath\u3002\"\n );\n}\n\nfunction looksLikeCliFailure(output: string): boolean {\n if (output.includes(CLI_ANSI_RED)) return true;\n if (/\\[pack-manager\\].*required/.test(output)) return true;\n if (/^Error:/m.test(output)) return true;\n return false;\n}\n\nexport interface RunSfmcCliResult {\n ok: boolean;\n output: string;\n}\n\nexport interface RunSfmcCliOptions {\n cliPath?: string;\n}\n\n/** \u5728 SFMC \u5DE5\u4F5C\u76EE\u5F55\u4E0B\u6267\u884C `node <sfmc-cli> ...args`\uFF08env.SFMC_ROOT = \u5DE5\u4F5C\u76EE\u5F55\uFF09\u3002 */\nexport async function runSfmcCli(\n sfmcRoot: string,\n args: string[],\n opts: RunSfmcCliOptions = {}\n): Promise<RunSfmcCliResult> {\n const root = path.resolve(sfmcRoot);\n if (!isValidSfmcRoot(root)) {\n return {\n ok: false,\n output: `\u65E0\u6548\u7684 SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF1A${root}\uFF08\u9700\u8981\u542B configs/ \u4E0E modules/\uFF0C\u4E0D\u5FC5\u542B\u6E90\u7801\u4ED3\u5E93\uFF09`,\n };\n }\n\n let script: string;\n try {\n script = resolveSfmcCli({ cliPath: opts.cliPath });\n } catch (e) {\n return { ok: false, output: e instanceof Error ? e.message : String(e) };\n }\n\n const r = await new Promise<{ code: number | null; output: string }>((resolve) => {\n const proc = spawn(process.execPath, [script, ...args], {\n cwd: root,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n env: { ...process.env, SFMC_ROOT: root },\n });\n let out = \"\";\n proc.stdout?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.stderr?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.on(\"exit\", (code) => resolve({ code, output: out }));\n proc.on(\"error\", (e) => resolve({ code: 1, output: e.message }));\n });\n\n const ok = r.code === 0 && !looksLikeCliFailure(r.output);\n return { ok, output: r.output };\n}\n", "/**\n * \u76D1\u542C\u6A21\u5757 sapi/src\uFF0C\u9632\u6296\u540E\u56DE\u8C03 onRebuild\uFF08\u7531\u8C03\u7528\u65B9\u6CE8\u5165 rebuildAndDeploy / \u5176\u5B83\uFF09\u3002\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport interface ModuleWatchOptions {\n moduleRoot: string;\n debounceMs?: number;\n log?: (line: string) => void;\n onRebuild: () => Promise<{ ok: boolean; message?: string }>;\n}\n\nexport function startModuleWatch(opts: ModuleWatchOptions): { stop: () => void } {\n const log = opts.log ?? ((line: string) => console.log(line));\n const debounceMs = opts.debounceMs ?? 200;\n const sapiDir = path.join(opts.moduleRoot, \"sapi\");\n const srcDir = path.join(sapiDir, \"src\");\n\n if (!fs.existsSync(sapiDir)) {\n log(`[devkit:watch] \u65E0 sapi \u76EE\u5F55: ${sapiDir}`);\n return { stop: () => {} };\n }\n\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let pending = new Set<string>();\n let running = false;\n let stopped = false;\n\n log(`[devkit:watch] \u76D1\u542C ${opts.moduleRoot}`);\n\n const runOnce = async () => {\n if (running || stopped) return;\n running = true;\n const changed = [...pending];\n pending = new Set();\n const t0 = Date.now();\n log(`[devkit:watch] rebuild (${changed.length})\u2026`);\n try {\n const r = await opts.onRebuild();\n const ms = Date.now() - t0;\n if (r.ok) log(`[devkit:watch] ok ${ms}ms`);\n else log(`[devkit:watch] fail ${ms}ms ${r.message ?? \"\"}`);\n } catch (e) {\n log(`[devkit:watch] error ${(e as Error).message}`);\n } finally {\n running = false;\n }\n };\n\n const onSrc = (rel: string) => {\n if (stopped) return;\n if (rel.includes(\"node_modules\") || rel.includes(\"dist\") || rel.endsWith(\".tgz\")) return;\n pending.add(rel);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => void runOnce(), debounceMs);\n };\n\n const watchers: fs.FSWatcher[] = [];\n if (fs.existsSync(srcDir)) {\n try {\n watchers.push(\n fs.watch(srcDir, { recursive: true, persistent: true }, (_e, filename) => {\n if (filename) onSrc(filename.toString());\n })\n );\n } catch {\n watchers.push(\n fs.watch(srcDir, { persistent: true }, (_e, filename) => {\n if (filename) onSrc(filename.toString());\n })\n );\n }\n }\n\n const watchMeta = (file: string, kind: string) => {\n if (!fs.existsSync(file)) return;\n watchers.push(\n fs.watch(file, { persistent: true }, (event) => {\n if (event === \"change\") log(`[devkit:watch] ${kind} \u5DF2\u6539\uFF0C\u9700\u91CD\u542F BDS\uFF08SAPI \u542F\u52A8\u671F\u7F13\u5B58\uFF09`);\n })\n );\n };\n watchMeta(path.join(sapiDir, \"manifest.json\"), \"manifest\");\n watchMeta(path.join(sapiDir, \"tsconfig.json\"), \"tsconfig\");\n\n return {\n stop() {\n stopped = true;\n if (debounceTimer) clearTimeout(debounceTimer);\n for (const w of watchers) {\n try {\n w.close();\n } catch {\n /* ignore */\n }\n }\n log(\"[devkit:watch] stopped\");\n },\n };\n}\n", "/**\n * \u811A\u624B\u67B6\uFF1A\u59D4\u6258 @sfmc-bds/devkit/scripts/new-module.mjs\u3002\n */\n\nimport { spawn } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { findMonorepoRoot } from \"@sfmc-bds/sdk/node/config\";\n\nexport interface ScaffoldOptions {\n /** \u76EE\u6807\u76EE\u5F55\uFF08\u987B\u4E3A\u7A7A\u6216\u53EF\u521B\u5EFA\uFF09\u3002 */\n targetDir: string;\n moduleId: string;\n displayName?: string;\n official?: boolean;\n}\n\nfunction resolveNewModuleScript(): string {\n const require = createRequire(import.meta.url);\n try {\n return require.resolve(\"@sfmc-bds/devkit/new-module.mjs\");\n } catch {\n /* monorepo \u56DE\u9000 */\n }\n const monoRoot = findMonorepoRoot(process.cwd());\n const mono = monoRoot\n ? path.join(monoRoot, \"packages\", \"devkit\", \"scripts\", \"new-module.mjs\")\n : path.resolve(process.cwd(), \"packages/devkit/scripts/new-module.mjs\");\n if (fs.existsSync(mono)) return mono;\n throw new Error(\"\u65E0\u6CD5\u89E3\u6790 new-module.mjs\uFF08\u5B89\u88C5 @sfmc-bds/devkit \u6216\u5728 monorepo \u6839\u8FD0\u884C\uFF09\");\n}\n\nexport async function scaffoldModule(opts: ScaffoldOptions): Promise<{ ok: boolean; message: string }> {\n const dir = path.resolve(opts.targetDir);\n fs.mkdirSync(dir, { recursive: true });\n const script = resolveNewModuleScript();\n const args = [script, opts.moduleId];\n if (opts.displayName) args.push(\"--name\", opts.displayName);\n if (opts.official) args.push(\"--official\");\n\n return new Promise((resolve) => {\n const proc = spawn(process.execPath, args, {\n cwd: dir,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n env: process.env,\n });\n let out = \"\";\n proc.stdout?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.stderr?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.on(\"exit\", (code) => {\n resolve({\n ok: code === 0,\n message: out.trim() || (code === 0 ? `scaffolded ${opts.moduleId}` : `exit ${code}`),\n });\n });\n proc.on(\"error\", (e) => resolve({ ok: false, message: e.message }));\n });\n}\n", "/**\n * \u7EC4\u88C5\u5E76\u90E8\u7F72\u884C\u4E3A\u5305\uFF1A\u5728 SFMC_ROOT \u4E0B spawn `sfmc mod reload`\n * \uFF08\u4E0E CLI \u540C\u4E00\u5165\u53E3\uFF0C\u5B8C\u6574\u4F20\u53C2\uFF1B\u52FF\u76F4\u63A5\u88F8\u8C03 pack-manager verb\uFF09\u3002\n */\n\nimport { runSfmcCli } from \"./sfmc-cli.js\";\n\nexport interface RebuildOptions {\n /** SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF08\u542B configs/\u3001modules/\uFF09\u3002 */\n sfmcRoot: string;\n /** \u4EC5\u6784\u5EFA\u90E8\u7F72\uFF0C\u4E0D\u5411 BDS \u53D1 reload\uFF08\u5BF9\u5E94 `mod reload --build-only`\uFF09\u3002 */\n buildOnly?: boolean;\n /** \u53EF\u9009\uFF1A\u663E\u5F0F sfmc CLI \u5165\u53E3\uFF1B\u9ED8\u8BA4\u7531\u5B89\u88C5\u6811 / PATH / SFMC_CLI \u89E3\u6790\u3002 */\n cliPath?: string;\n}\n\nexport interface RebuildResult {\n ok: boolean;\n output: string;\n}\n\nexport async function rebuildAndDeploy(opts: RebuildOptions): Promise<RebuildResult> {\n const args = [\"mod\", \"reload\"];\n if (opts.buildOnly) args.push(\"--build-only\");\n return runSfmcCli(opts.sfmcRoot, args, { cliPath: opts.cliPath });\n}\n", "/**\n * \u6A21\u5757\u542F\u505C\uFF1Aspawn `sfmc mod enable|disable`\uFF08\u5199 module-lock + best-effort \u70ED\u540C\u6B65\uFF09\u3002\n */\n\nimport { runSfmcCli, type RunSfmcCliResult } from \"./sfmc-cli.js\";\n\nexport interface SetModuleEnabledOptions {\n /** SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF08\u542B modules/module-lock.json\uFF09\u3002 */\n sfmcRoot: string;\n /** \u6A21\u5757 logicalId / folderId / configKey\uFF08\u4E0E CLI `mod enable` \u4E00\u81F4\uFF09\u3002 */\n moduleId: string;\n enabled: boolean;\n /** \u53EF\u9009\uFF1A\u663E\u5F0F sfmc CLI \u5165\u53E3\uFF1B\u9ED8\u8BA4\u7531\u5B89\u88C5\u6811 / PATH / SFMC_CLI \u89E3\u6790\u3002 */\n cliPath?: string;\n}\n\nexport type SetModuleEnabledResult = RunSfmcCliResult;\n\n/** \u542F\u7528\u6216\u5173\u95ED\u6A21\u5757\uFF08\u4E0E CLI `mod enable|disable` \u540C\u6E90\uFF09\u3002 */\nexport async function setModuleEnabled(opts: SetModuleEnabledOptions): Promise<SetModuleEnabledResult> {\n const id = String(opts.moduleId || \"\").trim();\n if (!id) {\n return { ok: false, output: \"moduleId \u4E3A\u7A7A\" };\n }\n const action = opts.enabled ? \"enable\" : \"disable\";\n return runSfmcCli(opts.sfmcRoot, [\"mod\", action, id], { cliPath: opts.cliPath });\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["path", "fs", "path", "fs", "path", "fs", "path", "fs", "path", "spawn", "createRequire", "fs", "path", "require"]
7
7
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 脚手架:委托 @sfmc-bds/tools/new-module(或 monorepo packages/tools/new-module.mjs)。
2
+ * 脚手架:委托 @sfmc-bds/devkit/scripts/new-module.mjs
3
3
  */
4
4
  export interface ScaffoldOptions {
5
5
  /** 目标目录(须为空或可创建)。 */
package/dist/scaffold.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 脚手架:委托 @sfmc-bds/tools/new-module(或 monorepo packages/tools/new-module.mjs)。
2
+ * 脚手架:委托 @sfmc-bds/devkit/scripts/new-module.mjs
3
3
  */
4
4
  import { spawn } from "node:child_process";
5
5
  import { createRequire } from "node:module";
@@ -9,18 +9,18 @@ import { findMonorepoRoot } from "@sfmc-bds/sdk/node/config";
9
9
  function resolveNewModuleScript() {
10
10
  const require = createRequire(import.meta.url);
11
11
  try {
12
- return require.resolve("@sfmc-bds/tools/new-module.mjs");
12
+ return require.resolve("@sfmc-bds/devkit/new-module.mjs");
13
13
  }
14
14
  catch {
15
15
  /* monorepo 回退 */
16
16
  }
17
17
  const monoRoot = findMonorepoRoot(process.cwd());
18
18
  const mono = monoRoot
19
- ? path.join(monoRoot, "packages", "tools", "new-module.mjs")
20
- : path.resolve(process.cwd(), "packages/tools/new-module.mjs");
19
+ ? path.join(monoRoot, "packages", "devkit", "scripts", "new-module.mjs")
20
+ : path.resolve(process.cwd(), "packages/devkit/scripts/new-module.mjs");
21
21
  if (fs.existsSync(mono))
22
22
  return mono;
23
- throw new Error("无法解析 new-module.mjs(安装 @sfmc-bds/tools 或在 monorepo 根运行)");
23
+ throw new Error("无法解析 new-module.mjs(安装 @sfmc-bds/devkit 或在 monorepo 根运行)");
24
24
  }
25
25
  export async function scaffoldModule(opts) {
26
26
  const dir = path.resolve(opts.targetDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sfmc-bds/devkit",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "SFMC module authoring toolkit — watch, scaffold, rebuild (for VS Code extension & tooling)",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -14,10 +14,16 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "import": "./dist/index.js",
16
16
  "default": "./dist/index.js"
17
- }
17
+ },
18
+ "./new-module.mjs": "./scripts/new-module.mjs",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "bin": {
22
+ "sfmc-new-module": "./scripts/new-module.mjs"
18
23
  },
19
24
  "files": [
20
25
  "dist",
26
+ "scripts",
21
27
  "README.md"
22
28
  ],
23
29
  "scripts": {
@@ -26,8 +32,8 @@
26
32
  },
27
33
  "dependencies": {
28
34
  "@sfmc-bds/bds-tools": "^0.2.0-beta.8",
29
- "@sfmc-bds/cli": "^0.2.0-beta.8",
30
- "@sfmc-bds/tools": "^0.2.0-beta.9"
35
+ "@sfmc-bds/cli": "^0.2.0-beta.10",
36
+ "@sfmc-bds/sdk": "^0.2.0-beta.8"
31
37
  },
32
38
  "publishConfig": {
33
39
  "access": "public",
@@ -0,0 +1,659 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ /**
4
+ * @sfmc-bds/devkit — 生成最小模块骨架(单包根)
5
+ *
6
+ * - 写入 **当前工作目录**(须为空)—— 与 Tanya7z/sfmc-module-template 同构
7
+ * - 传入 `--root` / `SFMC_MODULES_ROOT` 会直接退出(sfmc-modules 仅为 index)
8
+ * - `--official` → `@sfmc-bds/module-<id>`;默认 `@CHANGE_ME/sfmc-module-<id>`
9
+ *
10
+ * Usage:
11
+ * mkdir my-mod && cd my-mod
12
+ * node packages/devkit/scripts/new-module.mjs my-mod --name "我的模块"
13
+ * npx sfmc-new-module my-mod --name "我的模块"
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import process from "node:process";
19
+
20
+ /**
21
+ * @param {string} msg
22
+ */
23
+ function die(msg, code = 1) {
24
+ console.error(`[new-module] ${msg}`);
25
+ process.exit(code);
26
+ }
27
+
28
+ /**
29
+ * @param {string | any[]} argv
30
+ */
31
+ function parseArgs(argv) {
32
+ /** @type {{ name: string | null, root: string | null, template: string, listTemplates: boolean, official: boolean }} */
33
+ const flags = { name: null, root: null, template: "minimal", listTemplates: false, official: false };
34
+ /** @type {string[]} */
35
+ const positional = [];
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ if (a === "--name") flags.name = argv[++i] ?? null;
39
+ else if (a === "--root") flags.root = argv[++i] ?? null;
40
+ else if (a === "--template") flags.template = argv[++i] ?? "minimal";
41
+ else if (a === "--list-templates") flags.listTemplates = true;
42
+ else if (a === "--official") flags.official = true;
43
+ else if (a.startsWith("--name=")) flags.name = a.slice("--name=".length);
44
+ else if (a.startsWith("--root=")) flags.root = a.slice("--root=".length);
45
+ else if (a.startsWith("--template=")) flags.template = a.slice("--template=".length);
46
+ else if (a.startsWith("--")) die(`未知参数: ${a}`);
47
+ else positional.push(a);
48
+ }
49
+ return { flags, positional };
50
+ }
51
+
52
+ /**
53
+ * 模板清单 —— 这是 sfmc-modules 可用模板的唯一权威源(OCP)。
54
+ * 加新模板只需:
55
+ * 1) 在下方数组里加一条(配合 buildManifest 分支处理)
56
+ * 2) 在 sfmc CLI 的 i18n 中加 modwiz.tpl.<id> / modwiz.tpl.<id>Hint
57
+ *
58
+ * 注意:不在此处放显示文案;本地化由 sfmc i18n 接管,本工具只暴露机器可读清单。
59
+ *
60
+ * 输出格式:每行 `<id>` 或 `<id>\tdefault` —— 单源、纯文本、无 JSON 依赖。
61
+ */
62
+ const TEMPLATES = [
63
+ { id: "minimal", isDefault: true },
64
+ { id: "db", isDefault: false },
65
+ ];
66
+
67
+ function emitTemplateList() {
68
+ for (const tpl of TEMPLATES) {
69
+ process.stdout.write(`${tpl.id}${tpl.isDefault ? "\tdefault" : ""}\n`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * @param {string} id
75
+ */
76
+ function isValidFolderId(id) {
77
+ return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(id);
78
+ }
79
+
80
+ /**
81
+ * 决定模块骨架落盘位置:写到 cwd(单包根,与 Tanya7z/sfmc-module-template 同构)。
82
+ * 传入 `--root` / `SFMC_MODULES_ROOT` 会退出。
83
+ * @param {{ name?: string | null; root: any; template?: string; listTemplates?: boolean; official?: boolean; }} flags
84
+ */
85
+ function resolveTargetDir(flags) {
86
+ if (flags.root || process.env.SFMC_MODULES_ROOT) {
87
+ die(
88
+ `--root / SFMC_MODULES_ROOT 已移除:sfmc-modules 仅为 index。请在空目录运行本工具生成单包根,或使用 Tanya7z/sfmc-module-template。`
89
+ );
90
+ }
91
+ return path.resolve(process.cwd());
92
+ }
93
+
94
+ /**
95
+ * @param {string} folderId
96
+ * @param {{ official: boolean }} opts
97
+ */
98
+ function buildPackageJson(folderId, opts) {
99
+ const base = {
100
+ name: opts.official ? `@sfmc-bds/module-${folderId}` : `@CHANGE_ME/sfmc-module-${folderId}`,
101
+ version: "0.1.0",
102
+ type: "module",
103
+ description: `SAPI module: ${folderId}`,
104
+ main: "sapi/src/index.ts",
105
+ exports: {
106
+ ".": "./sapi/src/index.ts",
107
+ },
108
+ files: ["sapi", "test", "README.md", "LICENSE"],
109
+ };
110
+ /* 自包含单包根(与 Tanya7z/sfmc-module-template 同构) */
111
+ return {
112
+ ...base,
113
+ scripts: {
114
+ build: "npm run typecheck",
115
+ typecheck: "tsc --noEmit -p sapi/tsconfig.json",
116
+ test: "node --test --import @sfmc-bds/sdk/testing/minecraft-loader --import tsx/esm test/*.test.ts",
117
+ lint: 'eslint "sapi/**/*.ts" "test/**/*.ts"',
118
+ format: "prettier --write .",
119
+ },
120
+ devDependencies: {
121
+ "@minecraft/server": "2.10.0-beta.1.26.40-preview.30",
122
+ "@minecraft/server-net": "1.0.0-beta.11940b24",
123
+ "@minecraft/server-ui": "2.2.0-beta.1.26.40-preview.30",
124
+ "@minecraft/vanilla-data": "1.26.40-preview.30",
125
+ "@sfmc-bds/eslint-plugin": "^0.1.0",
126
+ "@sfmc-bds/sdk": "^0.2.0-beta.7",
127
+ "@types/node": "^22.13.0",
128
+ "@typescript-eslint/eslint-plugin": "^8.64.0",
129
+ "@typescript-eslint/parser": "^8.64.0",
130
+ eslint: "^10.7.0",
131
+ prettier: "^3.9.5",
132
+ "prettier-plugin-organize-imports": "^4.3.0",
133
+ tsx: "^4.19.0",
134
+ typescript: "^5.6.0",
135
+ },
136
+ peerDependencies: { "@sfmc-bds/sdk": ">=0.2.0" },
137
+ engines: { node: ">=22.13.0" },
138
+ };
139
+ }
140
+
141
+ /**
142
+ * @param {string} folderId
143
+ * @param {string} displayName
144
+ * @param {"minimal"|"db"} template
145
+ * @param {string} schemaRel $schema 相对 sapi/ 的路径(由调用方按落盘位置计算)
146
+ */
147
+ function buildManifest(folderId, displayName, template, schemaRel) {
148
+ if (folderId.startsWith("feature-") || folderId.startsWith("core-")) {
149
+ die(`folder 须为短名(不含 feature-/core- 前缀),例如 area 而非 feature-area`);
150
+ }
151
+ const logicalId = `feature-${folderId}`;
152
+ const configKey = folderId.replace(/-/g, "_");
153
+ /** @type {Record<string, unknown>} */
154
+ const base = {
155
+ $schema: schemaRel,
156
+ schemaVersion: 2,
157
+ id: logicalId,
158
+ name: displayName,
159
+ type: "feature",
160
+ configKey,
161
+ requires: [],
162
+ permissions: [`config:read:${configKey}`],
163
+ services: { provides: [], requires: [] },
164
+ notes: `由 @sfmc-bds/devkit new-module 脚手架生成(template=${template})`,
165
+ };
166
+ if (template === "db") {
167
+ base.permissions = [`db:read:sfmc_${configKey}`, `db:write:sfmc_${configKey}`, `config:read:${configKey}`];
168
+ base.notes =
169
+ `由 @sfmc-bds/devkit new-module 脚手架生成(含 db 权限占位)。` +
170
+ `请在 sapi/manifest.json 中补全 routes/migrations,并实现 db 表。`;
171
+ }
172
+ return base;
173
+ }
174
+
175
+ /**
176
+ * 自包含 sapi/tsconfig.json —— 不依赖平台仓 tsconfig.base.json;
177
+ * SDK 类型由 npm 装入 node_modules/@sfmc-bds/sdk 时随附。
178
+ */
179
+ function buildTsConfigStandalone() {
180
+ return {
181
+ compilerOptions: {
182
+ module: "nodenext",
183
+ moduleResolution: "nodenext",
184
+ target: "es2022",
185
+ lib: ["es2022"],
186
+ types: ["node"],
187
+ strict: true,
188
+ esModuleInterop: true,
189
+ skipLibCheck: true,
190
+ noEmit: true,
191
+ rootDir: "./src",
192
+ },
193
+ include: ["src/**/*"],
194
+ };
195
+ }
196
+
197
+ function buildRootTsConfig() {
198
+ return {
199
+ compilerOptions: {
200
+ module: "nodenext",
201
+ moduleResolution: "nodenext",
202
+ target: "es2022",
203
+ lib: ["es2022"],
204
+ types: ["node"],
205
+ strict: true,
206
+ esModuleInterop: true,
207
+ skipLibCheck: true,
208
+ noEmit: true,
209
+ },
210
+ include: ["sapi/**/*", "test/**/*"],
211
+ };
212
+ }
213
+
214
+ /**
215
+ * @param {string} folderId
216
+ * @param {string} displayName
217
+ * @param {string} pkgName
218
+ */
219
+ function buildIndexTs(folderId, displayName, pkgName) {
220
+ const logicalId = `feature-${folderId}`;
221
+ const perm = folderId.replace(/-/g, "_");
222
+ return `/**
223
+ * ${pkgName} — ${displayName}
224
+ * 由 @sfmc-bds/devkit new-module 脚手架生成。
225
+ */
226
+
227
+ import { ModuleRegistry, type ModuleDescriptor } from "@sfmc-bds/sdk/module-loader";
228
+ import { Command, Msg, Permission } from "@sfmc-bds/sdk/sapi/runtime";
229
+
230
+ /** 与 sapi/manifest.json 的 id 一致(逻辑 id,非文件夹短名)。 */
231
+ export const MODULE_ID = "${logicalId}";
232
+
233
+ /** 命令权限名。 */
234
+ export const PERM = "${perm}.use";
235
+
236
+ function registerPermissions(): void {
237
+ Permission.register(PERM, Permission.Any);
238
+ }
239
+
240
+ function registerCommands(): void {
241
+ Command.register(
242
+ "${perm}",
243
+ PERM,
244
+ (player) => {
245
+ if (!player) return;
246
+ Msg.info("模块 ${displayName} 已就绪", player);
247
+ },
248
+ "${displayName}",
249
+ MODULE_ID
250
+ );
251
+ }
252
+
253
+ function registerEvents(): void {
254
+ /* 事件订阅放在本阶段,不要放进 init()。 */
255
+ }
256
+
257
+ function init(): void {
258
+ /* TODO: 读取 configs/${perm}.json、注册 db 表等 */
259
+ }
260
+
261
+ function cleanup(): void {
262
+ /* TODO: 取消事件订阅、关闭 handle、清理定时器。 */
263
+ }
264
+
265
+ export const DESCRIPTOR: ModuleDescriptor = {
266
+ id: MODULE_ID,
267
+ afterWorldLoad: false,
268
+ lifecycle: {
269
+ registerPermissions,
270
+ registerCommands,
271
+ registerEvents,
272
+ init,
273
+ cleanup,
274
+ },
275
+ };
276
+
277
+ ModuleRegistry.register(DESCRIPTOR);
278
+ `;
279
+ }
280
+
281
+ /**
282
+ * @param {string} folderId
283
+ * @param {string} displayName
284
+ */
285
+ function buildExampleTest(folderId, displayName) {
286
+ const logicalId = `feature-${folderId}`;
287
+ const cmdName = folderId.replace(/-/g, "_");
288
+ const readyMsg = `模块 ${displayName} 已就绪`;
289
+ return `/**
290
+ * test/${folderId}.test.ts — 模块 lifecycle + 命令冒烟(假引擎)
291
+ *
292
+ * 跑法:npm test(SDK minecraft-loader + createSandbox)
293
+ */
294
+
295
+ import assert from "node:assert/strict";
296
+ import { readFileSync } from "node:fs";
297
+ import { test } from "node:test";
298
+ import { fileURLToPath } from "node:url";
299
+ import { assertMsg, createSandbox, runCleanup } from "@sfmc-bds/sdk/testing";
300
+
301
+ import { DESCRIPTOR, MODULE_ID, PERM } from "../sapi/src/index.js";
302
+
303
+ const MANIFEST_PATH = fileURLToPath(new URL("../sapi/manifest.json", import.meta.url));
304
+
305
+ function readManifest(): {
306
+ id: string;
307
+ configKey: string;
308
+ permissions?: string[];
309
+ } {
310
+ return JSON.parse(readFileSync(MANIFEST_PATH, "utf8")) as {
311
+ id: string;
312
+ configKey: string;
313
+ permissions?: string[];
314
+ };
315
+ }
316
+
317
+ test("descriptor / MODULE_ID 与 sapi/manifest.json 一致", () => {
318
+ const manifest = readManifest();
319
+ assert.equal(DESCRIPTOR.id, MODULE_ID);
320
+ assert.equal(MODULE_ID, manifest.id, "MODULE_ID 必须等于 manifest.id");
321
+ assert.equal(DESCRIPTOR.id, manifest.id, "DESCRIPTOR.id 必须等于 manifest.id");
322
+ assert.equal(MODULE_ID, "${logicalId}");
323
+ assert.equal(DESCRIPTOR.afterWorldLoad, false);
324
+ assert.equal(typeof DESCRIPTOR.lifecycle.registerPermissions, "function");
325
+ assert.equal(typeof DESCRIPTOR.lifecycle.registerCommands, "function");
326
+ assert.equal(typeof DESCRIPTOR.lifecycle.registerEvents, "function");
327
+ assert.equal(typeof DESCRIPTOR.lifecycle.init, "function");
328
+ assert.equal(typeof DESCRIPTOR.lifecycle.cleanup, "function");
329
+ });
330
+
331
+ test("PERM / 命令名与 manifest.configKey 对齐", () => {
332
+ const manifest = readManifest();
333
+ assert.ok(manifest.configKey, "manifest.configKey 必填");
334
+ assert.equal(PERM, \`\${manifest.configKey}.use\`);
335
+ assert.ok(
336
+ Array.isArray(manifest.permissions) &&
337
+ manifest.permissions.includes(\`config:read:\${manifest.configKey}\`),
338
+ \`manifest.permissions 应含 config:read:\${manifest.configKey}\`
339
+ );
340
+ });
341
+
342
+ test("createSandbox lifecycle 跑通", async (t) => {
343
+ const sb = await createSandbox({ module: DESCRIPTOR });
344
+ t.after(() => sb.dispose());
345
+ assert.ok(sb.world);
346
+ assert.ok(sb.system);
347
+ });
348
+
349
+ test("命令 ${cmdName} 触发后,玩家收到 Msg.info", async (t) => {
350
+ const sb = await createSandbox({ module: DESCRIPTOR });
351
+ t.after(() => sb.dispose());
352
+ const player = sb.addPlayer({ id: "tester-1", name: "tester", op: true });
353
+ await sb.triggerCommand("${cmdName}", player);
354
+ assert.ok(assertMsg(player, "${readyMsg}", "§"), "玩家 log 应含预期文本");
355
+ assert.equal(player.log.length, 1);
356
+ assert.match(player.log[0]!, /^§f\\[\\*\\] /);
357
+ });
358
+
359
+ test("cleanup 不抛错", async () => {
360
+ const r = await runCleanup(DESCRIPTOR);
361
+ assert.equal(r.ok, true, \`cleanup 抛出: \${r.error instanceof Error ? r.error.message : String(r.error)}\`);
362
+ });
363
+
364
+ test("PERM 格式正确", () => {
365
+ assert.match(PERM, /^[a-z][a-z0-9_]*\\.use$/);
366
+ });
367
+ `;
368
+ }
369
+
370
+ /** 与 sfmc-module-template/eslint.config.js 同构 */
371
+ function buildEslintConfigJs() {
372
+ return `// SFMC 模块 ESLint 配置
373
+ import sfmc from "@sfmc-bds/eslint-plugin";
374
+ import tsPlugin from "@typescript-eslint/eslint-plugin";
375
+ import tsParser from "@typescript-eslint/parser";
376
+
377
+ export default [
378
+ {
379
+ ignores: ["**/dist/**", "**/node_modules/**", "**/build/**", "**/*.d.ts"],
380
+ },
381
+ {
382
+ files: ["sapi/**/*.ts", "test/**/*.ts"],
383
+ languageOptions: {
384
+ parser: tsParser,
385
+ parserOptions: {
386
+ ecmaVersion: 2022,
387
+ sourceType: "module",
388
+ },
389
+ },
390
+ plugins: {
391
+ "@typescript-eslint": tsPlugin,
392
+ "@sfmc-bds": sfmc,
393
+ },
394
+ rules: {
395
+ "no-undef": "off",
396
+ "no-unused-vars": "off",
397
+ "@typescript-eslint/no-explicit-any": "warn",
398
+ "@typescript-eslint/no-unused-vars": [
399
+ "warn",
400
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
401
+ ],
402
+ "@typescript-eslint/ban-ts-comment": "warn",
403
+ "@typescript-eslint/no-require-imports": "error",
404
+ ...sfmc.configs.recommended.rules,
405
+ },
406
+ },
407
+ {
408
+ /* 旧版 eslint-plugin 静态白名单可能未含 testing;测试文件允许 SDK testing 入口 */
409
+ files: ["test/**/*.ts"],
410
+ rules: {
411
+ "@sfmc-bds/no-sdk-private-export": "off",
412
+ },
413
+ },
414
+ ];
415
+ `;
416
+ }
417
+
418
+ function buildGitignore() {
419
+ return `node_modules/
420
+ dist/
421
+ *.tgz
422
+ *.log
423
+ .DS_Store
424
+ .idea/
425
+ `;
426
+ }
427
+
428
+ function buildPrettierRc() {
429
+ return {
430
+ trailingComma: "es5",
431
+ tabWidth: 2,
432
+ semi: true,
433
+ singleQuote: false,
434
+ bracketSpacing: true,
435
+ arrowParens: "always",
436
+ printWidth: 120,
437
+ endOfLine: "crlf",
438
+ plugins: ["prettier-plugin-organize-imports"],
439
+ };
440
+ }
441
+
442
+ function buildPrettierIgnore() {
443
+ return `node_modules/
444
+ dist/
445
+ *.tgz
446
+ package-lock.json
447
+ `;
448
+ }
449
+
450
+ function buildLicense() {
451
+ return `ISC License
452
+
453
+ Copyright (c) ${new Date().getFullYear()}, ScriptsForMinecraftServer contributors
454
+
455
+ Permission to use, copy, modify, and/or distribute this software for any
456
+ purpose with or without fee is hereby granted, provided that the above
457
+ copyright notice and this permission notice appear in all copies.
458
+
459
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
460
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
461
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
462
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
463
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
464
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
465
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
466
+ `;
467
+ }
468
+
469
+ /**
470
+ * @param {string} folderId
471
+ * @param {string} displayName
472
+ * @param {string} pkgName
473
+ */
474
+ function buildReadme(folderId, displayName, pkgName) {
475
+ return `# ${folderId}
476
+
477
+ ${displayName}(\`${pkgName}\`)— SFMC SAPI 模块。
478
+
479
+ ## 最短成功路径
480
+
481
+ \`\`\`bash
482
+ npm install
483
+ npm run typecheck
484
+ npm test
485
+ npm run lint
486
+ \`\`\`
487
+
488
+ 用 VS Code / Cursor **单独打开本仓根**。推荐扩展:ESLint、Prettier、SFMC Module、Node.js Test Runner。
489
+
490
+ 1. \`npm test\`(假引擎;不依赖 \`sfmc.root\`)
491
+ 2. 可视化编排:独立应用 **Sapience**
492
+ 3. 真机联调:设 \`sfmc.root\` 为 SFMC **工作目录**(含 \`configs/\`、\`modules/\`),再 Start Watch / Reload to BDS
493
+ 4. link:\`sfmc mod install ${folderId} --from dir:<本仓绝对路径> --link\`
494
+
495
+ | 命令 | 作用 |
496
+ | --- | --- |
497
+ | \`npm run build\` / \`typecheck\` | tsc --noEmit |
498
+ | \`npm test\` | createSandbox + DESCRIPTOR |
499
+ | \`npm run lint\` / \`format\` | ESLint / Prettier |
500
+
501
+ \`DESCRIPTOR.id\` 须与 \`sapi/manifest.json\` 的 \`id\`(\`feature-${folderId}\`)一致。
502
+ `;
503
+ }
504
+
505
+ /**
506
+ * @param {fs.PathOrFileDescriptor} filePath
507
+ * @param {Record<string, unknown>} data
508
+ */
509
+ function writeJson(filePath, data) {
510
+ // @ts-ignore
511
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
512
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
513
+ }
514
+
515
+ /**
516
+ * @param {fs.PathOrFileDescriptor} filePath
517
+ * @param {string | NodeJS.ArrayBufferView<ArrayBufferLike>} content
518
+ */
519
+ function writeText(filePath, content) {
520
+ // @ts-ignore
521
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
522
+ fs.writeFileSync(filePath, content, "utf8");
523
+ }
524
+
525
+ /**
526
+ * @param {string} target
527
+ */
528
+ function writeVscodeWorkspace(target) {
529
+ writeJson(path.join(target, ".vscode", "extensions.json"), {
530
+ recommendations: [
531
+ "dbaeumer.vscode-eslint",
532
+ "esbenp.prettier-vscode",
533
+ "sfmc-bds.sfmc-module",
534
+ "connor4312.nodejs-testing",
535
+ ],
536
+ });
537
+ writeJson(path.join(target, ".vscode", "settings.json"), {
538
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
539
+ "editor.formatOnSave": true,
540
+ "eslint.useFlatConfig": true,
541
+ "eslint.validate": ["javascript", "typescript"],
542
+ "nodejs-testing.include": ["./test"],
543
+ "nodejs-testing.extensions": [
544
+ {
545
+ extensions: ["ts"],
546
+ parameters: ["--import", "@sfmc-bds/sdk/testing/minecraft-loader", "--import", "tsx/esm"],
547
+ },
548
+ ],
549
+ });
550
+ writeJson(path.join(target, ".vscode", "launch.json"), {
551
+ version: "0.2.0",
552
+ configurations: [
553
+ {
554
+ type: "node",
555
+ request: "launch",
556
+ name: "Debug Module Tests",
557
+ runtimeArgs: [
558
+ "--test",
559
+ "--import",
560
+ "@sfmc-bds/sdk/testing/minecraft-loader",
561
+ "--import",
562
+ "tsx/esm",
563
+ ],
564
+ args: ["${workspaceFolder}/test"],
565
+ cwd: "${workspaceFolder}",
566
+ console: "integratedTerminal",
567
+ sourceMaps: true,
568
+ },
569
+ ],
570
+ });
571
+ writeJson(path.join(target, ".vscode", "tasks.json"), {
572
+ version: "2.0.0",
573
+ tasks: [
574
+ {
575
+ type: "npm",
576
+ script: "typecheck",
577
+ group: "build",
578
+ problemMatcher: ["$tsc"],
579
+ label: "npm: typecheck",
580
+ },
581
+ {
582
+ type: "npm",
583
+ script: "test",
584
+ group: { kind: "test", isDefault: true },
585
+ problemMatcher: [],
586
+ label: "npm: test",
587
+ },
588
+ {
589
+ type: "npm",
590
+ script: "lint",
591
+ problemMatcher: ["$eslint-stylish"],
592
+ label: "npm: lint",
593
+ },
594
+ ],
595
+ });
596
+ }
597
+
598
+ function main() {
599
+ const { flags, positional } = parseArgs(process.argv.slice(2));
600
+
601
+ /* 模板清单查询:走 stdout 后立刻退出 —— 主流程不要走到此分支 */
602
+ if (flags.listTemplates) {
603
+ emitTemplateList();
604
+ return;
605
+ }
606
+
607
+ const folderId = positional[0];
608
+ if (!folderId) {
609
+ die(
610
+ "用法: new-module.mjs <id> [--name <名>] [--template minimal|db] [--official]\n" +
611
+ " 在空目录(单包根)运行;与 Tanya7z/sfmc-module-template 同构。"
612
+ );
613
+ }
614
+ if (!isValidFolderId(folderId)) {
615
+ die(`id 须为小写 kebab-case,例如 my-mod(收到: ${folderId})`);
616
+ }
617
+ if (folderId.startsWith("feature-") || folderId.startsWith("core-")) {
618
+ die(`id 须为短名(不含 feature-/core- 前缀),例如 area 而非 feature-area`);
619
+ }
620
+ const target = resolveTargetDir(flags);
621
+ if (fs.existsSync(path.join(target, "sapi")) || fs.existsSync(path.join(target, "package.json"))) {
622
+ die(`目标已含模块骨架: ${target}(请用空目录)`);
623
+ }
624
+
625
+ const displayName = flags.name?.trim() || folderId;
626
+ const template = flags.template === "db" ? "db" : "minimal";
627
+ const official = Boolean(flags.official);
628
+ const pkgName = official ? `@sfmc-bds/module-${folderId}` : `@CHANGE_ME/sfmc-module-${folderId}`;
629
+
630
+ const sapiDir = path.join(target, "sapi");
631
+ const schemaRel =
632
+ "https://cdn.jsdelivr.net/gh/DogeLakeDev/ScriptsForMinecraftServer@latest/modules/sdk/%40sfmc-sdk/schemas/sapi-manifest.v2.schema.json";
633
+
634
+ writeJson(path.join(target, "package.json"), buildPackageJson(folderId, { official }));
635
+ writeJson(path.join(sapiDir, "manifest.json"), buildManifest(folderId, displayName, template, schemaRel));
636
+ writeJson(path.join(sapiDir, "tsconfig.json"), buildTsConfigStandalone());
637
+ writeJson(path.join(target, "tsconfig.json"), buildRootTsConfig());
638
+ writeText(path.join(sapiDir, "src", "index.ts"), buildIndexTs(folderId, displayName, pkgName));
639
+ writeText(path.join(target, "test", `${folderId}.test.ts`), buildExampleTest(folderId, displayName));
640
+ writeText(path.join(target, "eslint.config.js"), buildEslintConfigJs());
641
+ writeText(path.join(target, ".gitignore"), buildGitignore());
642
+ writeJson(path.join(target, ".prettierrc.json"), buildPrettierRc());
643
+ writeText(path.join(target, ".prettierignore"), buildPrettierIgnore());
644
+ writeText(path.join(target, "LICENSE"), buildLicense());
645
+ writeText(path.join(target, "README.md"), buildReadme(folderId, displayName, pkgName));
646
+ writeVscodeWorkspace(target);
647
+
648
+ console.log(`[new-module] 已创建 ${target}`);
649
+ console.log(`[new-module] 模式: 单包根`);
650
+ console.log(`[new-module] npm: ${pkgName}`);
651
+ console.log(`[new-module] manifest id: feature-${folderId}`);
652
+ console.log(`[new-module] 下一步:`);
653
+ console.log(`[new-module] npm install && npm run typecheck && npm test && npm run lint`);
654
+ console.log(
655
+ `[new-module] sfmc mod install ${folderId} --from dir:${target} --link (在 SFMC 工作目录执行)`
656
+ );
657
+ }
658
+
659
+ main();