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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +7 -1
  2. package/dist/cli-result.d.ts +12 -0
  3. package/dist/cli-result.d.ts.map +1 -0
  4. package/dist/cli-result.js +15 -0
  5. package/dist/cli-result.js.map +7 -0
  6. package/dist/commands-behavior-pack.d.ts +11 -42
  7. package/dist/commands-behavior-pack.d.ts.map +1 -1
  8. package/dist/commands-behavior-pack.js +40 -159
  9. package/dist/commands-behavior-pack.js.map +7 -1
  10. package/dist/commands-reload.d.ts +9 -0
  11. package/dist/commands-reload.d.ts.map +1 -0
  12. package/dist/commands-reload.js +30 -0
  13. package/dist/commands-reload.js.map +7 -0
  14. package/dist/commands.d.ts +7 -0
  15. package/dist/commands.d.ts.map +1 -1
  16. package/dist/commands.js +182 -172
  17. package/dist/commands.js.map +7 -1
  18. package/dist/i18n/index.d.ts +26 -0
  19. package/dist/i18n/index.d.ts.map +1 -0
  20. package/dist/i18n/index.js +107 -0
  21. package/dist/i18n/index.js.map +7 -0
  22. package/dist/i18n/locales/en.d.ts +381 -0
  23. package/dist/i18n/locales/en.d.ts.map +1 -0
  24. package/dist/i18n/locales/en.js +393 -0
  25. package/dist/i18n/locales/en.js.map +7 -0
  26. package/dist/i18n/locales/zh-CN.d.ts +4 -0
  27. package/dist/i18n/locales/zh-CN.d.ts.map +1 -0
  28. package/dist/i18n/locales/zh-CN.js +379 -0
  29. package/dist/i18n/locales/zh-CN.js.map +7 -0
  30. package/dist/i18n/types.d.ts +7 -0
  31. package/dist/i18n/types.d.ts.map +1 -0
  32. package/dist/i18n/types.js +17 -0
  33. package/dist/i18n/types.js.map +7 -0
  34. package/dist/index.d.ts +5 -11
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +51 -43
  37. package/dist/index.js.map +7 -1
  38. package/dist/interactive-prompts.d.ts +8 -0
  39. package/dist/interactive-prompts.d.ts.map +1 -0
  40. package/dist/interactive-prompts.js +57 -0
  41. package/dist/interactive-prompts.js.map +7 -0
  42. package/dist/locale-command.d.ts +3 -0
  43. package/dist/locale-command.d.ts.map +1 -0
  44. package/dist/locale-command.js +19 -0
  45. package/dist/locale-command.js.map +7 -0
  46. package/dist/logs.d.ts +22 -4
  47. package/dist/logs.d.ts.map +1 -1
  48. package/dist/logs.js +208 -193
  49. package/dist/logs.js.map +7 -1
  50. package/dist/main.js +135 -173
  51. package/dist/main.js.map +7 -1
  52. package/dist/module-commands.d.ts +43 -4
  53. package/dist/module-commands.d.ts.map +1 -1
  54. package/dist/module-commands.js +440 -327
  55. package/dist/module-commands.js.map +7 -1
  56. package/dist/module-fingerprint.d.ts +7 -0
  57. package/dist/module-fingerprint.d.ts.map +1 -0
  58. package/dist/module-fingerprint.js +31 -0
  59. package/dist/module-fingerprint.js.map +7 -0
  60. package/dist/module-wizard.d.ts +14 -0
  61. package/dist/module-wizard.d.ts.map +1 -0
  62. package/dist/module-wizard.js +313 -0
  63. package/dist/module-wizard.js.map +7 -0
  64. package/dist/pack-lifecycle.d.ts +95 -0
  65. package/dist/pack-lifecycle.d.ts.map +1 -0
  66. package/dist/pack-lifecycle.js +634 -0
  67. package/dist/pack-lifecycle.js.map +7 -0
  68. package/dist/pack-update/bindings.d.ts +12 -0
  69. package/dist/pack-update/bindings.d.ts.map +1 -0
  70. package/dist/pack-update/bindings.js +64 -0
  71. package/dist/pack-update/bindings.js.map +7 -0
  72. package/dist/pack-update/config.d.ts +22 -0
  73. package/dist/pack-update/config.d.ts.map +1 -0
  74. package/dist/pack-update/config.js +141 -0
  75. package/dist/pack-update/config.js.map +7 -0
  76. package/dist/pack-update/providers/curseforge.d.ts +23 -0
  77. package/dist/pack-update/providers/curseforge.d.ts.map +1 -0
  78. package/dist/pack-update/providers/curseforge.js +209 -0
  79. package/dist/pack-update/providers/curseforge.js.map +7 -0
  80. package/dist/pack-update/providers/index.d.ts +16 -0
  81. package/dist/pack-update/providers/index.d.ts.map +1 -0
  82. package/dist/pack-update/providers/index.js +37 -0
  83. package/dist/pack-update/providers/index.js.map +7 -0
  84. package/dist/pack-update/service.d.ts +21 -0
  85. package/dist/pack-update/service.d.ts.map +1 -0
  86. package/dist/pack-update/service.js +567 -0
  87. package/dist/pack-update/service.js.map +7 -0
  88. package/dist/pack-update/types.d.ts +126 -0
  89. package/dist/pack-update/types.d.ts.map +1 -0
  90. package/dist/pack-update/types.js +8 -0
  91. package/dist/pack-update/types.js.map +7 -0
  92. package/dist/pack-update/version-policy.d.ts +80 -0
  93. package/dist/pack-update/version-policy.d.ts.map +1 -0
  94. package/dist/pack-update/version-policy.js +216 -0
  95. package/dist/pack-update/version-policy.js.map +7 -0
  96. package/dist/registry.d.ts +12 -4
  97. package/dist/registry.d.ts.map +1 -1
  98. package/dist/registry.js +73 -106
  99. package/dist/registry.js.map +7 -1
  100. package/dist/remote-agent.d.ts.map +1 -1
  101. package/dist/remote-agent.js +160 -176
  102. package/dist/remote-agent.js.map +7 -1
  103. package/dist/repl.d.ts +4 -0
  104. package/dist/repl.d.ts.map +1 -1
  105. package/dist/repl.js +683 -605
  106. package/dist/repl.js.map +7 -1
  107. package/dist/runtime.d.ts +40 -14
  108. package/dist/runtime.d.ts.map +1 -1
  109. package/dist/runtime.js +185 -62
  110. package/dist/runtime.js.map +7 -1
  111. package/dist/services.d.ts +2 -0
  112. package/dist/services.d.ts.map +1 -1
  113. package/dist/services.js +251 -238
  114. package/dist/services.js.map +7 -1
  115. package/dist/sfmc-modules-root.d.ts +24 -0
  116. package/dist/sfmc-modules-root.d.ts.map +1 -0
  117. package/dist/sfmc-modules-root.js +70 -0
  118. package/dist/sfmc-modules-root.js.map +7 -0
  119. package/dist/theme.d.ts +4 -0
  120. package/dist/theme.d.ts.map +1 -1
  121. package/dist/theme.js +134 -48
  122. package/dist/theme.js.map +7 -1
  123. package/dist/wizard.d.ts.map +1 -1
  124. package/dist/wizard.js +363 -438
  125. package/dist/wizard.js.map +7 -1
  126. package/dist/world-packs.d.ts +22 -0
  127. package/dist/world-packs.d.ts.map +1 -0
  128. package/dist/world-packs.js +872 -0
  129. package/dist/world-packs.js.map +7 -0
  130. package/package.json +10 -10
  131. package/dist/dispatcher.d.ts +0 -13
  132. package/dist/dispatcher.d.ts.map +0 -1
  133. package/dist/dispatcher.js +0 -23
  134. package/dist/dispatcher.js.map +0 -1
  135. package/dist/terminal.d.ts +0 -3
  136. package/dist/terminal.d.ts.map +0 -1
  137. package/dist/terminal.js +0 -178
  138. package/dist/terminal.js.map +0 -1
@@ -1,367 +1,480 @@
1
- /**
2
- * sfmc module-commands — runtime CLI for inspecting and managing modules
3
- * already on disk under `modules/packages/<id>/`.
4
- *
5
- * Subcommands:
6
- * list List every installed module (reads each
7
- * modules/packages/<id>/sapi/manifest.json).
8
- * Marks registry-known modules with `●` and
9
- * modules from an unknown publisher with `?`.
10
- * info <id> Show one module's manifest + on-disk fingerprint
11
- * verify [id] Recompute the SHA-256 fingerprint of installed
12
- * modules (id = one; no id = all)
13
- * install <id> Wrapper around `tools/fetch-module.mjs install`.
14
- * If `--from` is omitted, the first-party registry
15
- * is consulted (Tanya7z/sfmc-modules).
16
- * uninstall <id> Remove packages/<id>/ + catalog/lock via fetch-module
17
- * enable <id> POST /api/sfmc/modules/:id/enable on db-server
18
- * disable <id> POST /api/sfmc/modules/:id/disable on db-server
19
- *
20
- * The runtime SEA process never connects to the network. `install`/`uninstall`
21
- * delegate to `tools/fetch-module.mjs` (install syncs catalog + lock).
22
- * enable/disable go through db-server REST so module-lock.json stays consistent.
23
- */
1
+ const MODULE_CMD_NAMES = ["module", "mod"];
2
+ function isModuleCommand(cmd) {
3
+ return !!cmd && MODULE_CMD_NAMES.includes(cmd);
4
+ }
5
+ function paintModuleCmdAlias(paint) {
6
+ return MODULE_CMD_NAMES.map((name) => paint(name)).join("/");
7
+ }
8
+ const MODULE_SUBCOMMANDS = [
9
+ "list",
10
+ "search",
11
+ "install",
12
+ "uninstall",
13
+ "verify",
14
+ "info",
15
+ "enable",
16
+ "disable",
17
+ "build",
18
+ "reload",
19
+ "create",
20
+ "link",
21
+ "dev"
22
+ ];
24
23
  import fs from "node:fs/promises";
25
- import { existsSync } from "node:fs";
24
+ import { existsSync, readdirSync } from "node:fs";
26
25
  import path from "node:path";
27
- import { createHash } from "node:crypto";
28
26
  import { spawn } from "node:child_process";
29
- import { configPath, readJson } from "@sfmc-bds/sdk/node/config";
27
+ import { configPath, modulePath, readJson } from "@sfmc-bds/sdk/node/config";
28
+ import { failResult, okResult } from "./cli-result.js";
29
+ import { t } from "./i18n/index.js";
30
30
  import { c } from "./theme.js";
31
- import { ROOT } from "./runtime.js";
32
- import { findUnknownModules } from "./registry.js";
33
- /** Where modules live on disk. SEA reads this same path at runtime. */
31
+ import { ROOT, resolveFetchModule } from "./runtime.js";
32
+ import { dirFingerprint } from "./module-fingerprint.js";
33
+ import {
34
+ DEFAULT_REGISTRY_REPO,
35
+ DEFAULT_REGISTRY_TAG,
36
+ findUnknownModules,
37
+ resolveRegistryIndex
38
+ } from "./registry.js";
39
+ function moduleUsage() {
40
+ return t("mod.usage", {
41
+ cmds: MODULE_CMD_NAMES.join("|"),
42
+ subs: MODULE_SUBCOMMANDS.join("|")
43
+ });
44
+ }
34
45
  function modulesDir() {
35
- return path.join(ROOT, "modules", "packages");
46
+ return path.join(ROOT, "modules", "packages");
36
47
  }
37
48
  function fmtBytes(n) {
38
- if (n < 1024)
39
- return `${n} B`;
40
- if (n < 1024 * 1024)
41
- return `${(n / 1024).toFixed(1)} KB`;
42
- return `${(n / 1024 / 1024).toFixed(2)} MB`;
49
+ if (n < 1024) return `${n} B`;
50
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
51
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
43
52
  }
44
53
  function shortHash(h) {
45
- return h.slice(0, 8) + "" + h.slice(-8);
54
+ return h.slice(0, 8) + "\u2026" + h.slice(-8);
46
55
  }
47
- /** Enumerate every installed module by scanning `modules/packages/<id>/`. */
48
56
  async function scanInstalled() {
49
- const dir = modulesDir();
50
- if (!existsSync(dir))
51
- return [];
52
- const entries = await fs.readdir(dir, { withFileTypes: true });
53
- const out = [];
54
- for (const e of entries) {
55
- if (!e.isDirectory())
56
- continue;
57
- const id = e.name;
58
- const modPath = path.join(dir, id);
59
- const manifestPath = path.join(modPath, "sapi", "manifest.json");
60
- let manifest = null;
61
- if (existsSync(manifestPath)) {
62
- try {
63
- manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
64
- }
65
- catch {
66
- /* corrupt manifest is not fatal — just skip its fields */
67
- }
68
- }
69
- const { totalBytes, fileCount } = await dirSize(modPath);
70
- const fingerprint = await dirFingerprint(modPath);
71
- out.push({ id, path: modPath, manifest, totalBytes, fileCount, fingerprint });
57
+ const dir = modulesDir();
58
+ if (!existsSync(dir)) return [];
59
+ const entries = await fs.readdir(dir, { withFileTypes: true });
60
+ const out = [];
61
+ for (const e of entries) {
62
+ if (!e.isDirectory()) continue;
63
+ const id = e.name;
64
+ const modPath = path.join(dir, id);
65
+ const manifestPath = path.join(modPath, "sapi", "manifest.json");
66
+ let manifest = null;
67
+ if (existsSync(manifestPath)) {
68
+ try {
69
+ manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
70
+ } catch {
71
+ }
72
72
  }
73
- out.sort((a, b) => a.id.localeCompare(b.id));
74
- return out;
75
- }
76
- /** Compute the canonical SHA-256 of a module directory (POSIX `find ... | sha256sum` compatible). */
77
- async function dirFingerprint(rootDir) {
78
- const hash = createHash("sha256");
79
- const entries = [];
80
- async function walk(rel) {
81
- const full = path.join(rootDir, rel);
82
- const items = await fs.readdir(full, { withFileTypes: true });
83
- const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
84
- for (const it of sorted) {
85
- const child = rel ? `${rel}/${it.name}` : it.name;
86
- if (it.isDirectory())
87
- await walk(child);
88
- else if (it.isFile())
89
- entries.push(child);
90
- }
91
- }
92
- await walk("");
93
- entries.sort();
94
- for (const rel of entries) {
95
- const data = await fs.readFile(path.join(rootDir, rel));
96
- hash.update(rel.replaceAll("\\", "/"));
97
- hash.update("\n");
98
- hash.update(data);
99
- hash.update("\n");
100
- }
101
- return hash.digest("hex");
73
+ const { totalBytes, fileCount } = await dirSize(modPath);
74
+ const fingerprint = await dirFingerprint(modPath);
75
+ out.push({ id, path: modPath, manifest, totalBytes, fileCount, fingerprint });
76
+ }
77
+ out.sort((a, b) => a.id.localeCompare(b.id));
78
+ return out;
102
79
  }
103
80
  async function dirSize(dir) {
104
- let totalBytes = 0;
105
- let fileCount = 0;
106
- async function walk(p) {
107
- for (const e of await fs.readdir(p, { withFileTypes: true })) {
108
- const child = path.join(p, e.name);
109
- if (e.isDirectory())
110
- await walk(child);
111
- else if (e.isFile()) {
112
- const s = await fs.stat(child);
113
- totalBytes += s.size;
114
- fileCount++;
115
- }
116
- }
81
+ let totalBytes = 0;
82
+ let fileCount = 0;
83
+ async function walk(p) {
84
+ for (const e of await fs.readdir(p, { withFileTypes: true })) {
85
+ const child = path.join(p, e.name);
86
+ if (e.isDirectory()) await walk(child);
87
+ else if (e.isFile()) {
88
+ const s = await fs.stat(child);
89
+ totalBytes += s.size;
90
+ fileCount++;
91
+ }
117
92
  }
118
- await walk(dir);
119
- return { totalBytes, fileCount };
93
+ }
94
+ await walk(dir);
95
+ return { totalBytes, fileCount };
120
96
  }
121
- /* ─────────────────────────────────────────────────────────────────
122
- * list
123
- * ──────────────────────────────────────────────────────────────── */
124
- export async function cmdModuleList(_args) {
125
- const installed = await scanInstalled();
126
- if (installed.length === 0) {
127
- return c.dim(`\nNo modules installed. Drop a module folder under ${modulesDir()} or run \`sfmc module install <id>\`.\n`);
128
- }
129
- const ids = installed.map((m) => m.id);
130
- const unknown = new Set(await findUnknownModules(ids));
131
- const lines = [c.bold("\nInstalled modules"), c.dim(` ${modulesDir()}`)];
132
- const header = ` ${"id".padEnd(28)}${"files".padEnd(8)}${"size".padEnd(10)}${"routes".padEnd(8)}${"fingerprint"}`;
133
- lines.push(c.dim(header));
134
- for (const m of installed) {
135
- const routeCount = m.manifest?.routes?.length ?? 0;
136
- let mark;
137
- if (!m.manifest) {
138
- mark = c.yellow("○");
139
- }
140
- else if (unknown.has(m.id)) {
141
- mark = c.yellow("?");
142
- }
143
- else {
144
- mark = c.green("");
145
- }
146
- lines.push(` ${mark} ${m.id.padEnd(26)}${String(m.fileCount).padEnd(8)}${fmtBytes(m.totalBytes).padEnd(10)}${String(routeCount).padEnd(8)}${shortHash(m.fingerprint)}`);
97
+ async function cmdModuleList(_args) {
98
+ const installed = await scanInstalled();
99
+ if (installed.length === 0) {
100
+ return c.dim(t("mod.noneInstalled", { dir: modulesDir() }));
101
+ }
102
+ const lock = readJson(modulePath(path.join(ROOT, "modules"), "module-lock.json")) ?? {
103
+ version: 1,
104
+ modules: {}
105
+ };
106
+ const ids = installed.map((m) => m.id);
107
+ const unknown = new Set(await findUnknownModules(ids));
108
+ const lines = [c.bold(`
109
+ ${t("mod.list.title")}`), c.dim(` ${modulesDir()}`)];
110
+ const header = ` ${"id".padEnd(24)}${"state".padEnd(8)}${"files".padEnd(8)}${"size".padEnd(10)}${"fingerprint"}`;
111
+ lines.push(c.dim(header));
112
+ for (const m of installed) {
113
+ const logicalId = typeof m.manifest?.id === "string" && m.manifest.id ? m.manifest.id : m.id;
114
+ const enabled = lock.modules?.[logicalId]?.enabled === true;
115
+ const stateCol = (enabled ? c.green("on") : c.dim("off")).padEnd(8);
116
+ let mark;
117
+ if (!m.manifest) {
118
+ mark = c.yellow("\u25CB");
119
+ } else if (unknown.has(m.id)) {
120
+ mark = c.yellow("?");
121
+ } else {
122
+ mark = enabled ? c.green("\u25CF") : c.dim("\u25CB");
147
123
  }
148
- return lines.join("\n") + "\n";
124
+ const idLabel = logicalId !== m.id ? `${m.id}(${logicalId})` : m.id;
125
+ lines.push(
126
+ ` ${mark} ${idLabel.padEnd(22)}${stateCol}${String(m.fileCount).padEnd(8)}${fmtBytes(m.totalBytes).padEnd(10)}${shortHash(m.fingerprint)}`
127
+ );
128
+ }
129
+ lines.push(c.dim(`
130
+ tip: ${MODULE_CMD_NAMES[1] ?? "mod"} enable|disable <logicalId>`));
131
+ return lines.join("\n") + "\n";
149
132
  }
150
- /**
151
- * Walk `modules/packages/<id>/` and print a one-line yellow warning for each
152
- * id that isn't in the first-party registry. Intended for the REPL startup
153
- * hook so users immediately see modules they installed from somewhere else.
154
- *
155
- * Best-effort: registry unreachable → no warning (no false positives).
156
- */
157
- export async function scanAndWarnUnknown() {
158
- const installed = await scanInstalled();
159
- if (installed.length === 0)
160
- return "";
161
- const unknown = await findUnknownModules(installed.map((m) => m.id));
162
- if (unknown.length === 0)
163
- return "";
164
- const list = unknown.map((id) => ` ${c.yellow("?")} ${id}`).join("\n");
165
- return `${c.yellow("[sfmc] modules installed from unknown publisher — verify before use:")}\n${list}\n`;
133
+ async function scanAndWarnUnknown() {
134
+ const installed = await scanInstalled();
135
+ if (installed.length === 0) return "";
136
+ const unknown = await findUnknownModules(installed.map((m) => m.id));
137
+ if (unknown.length === 0) return "";
138
+ const list = unknown.map((id) => ` ${c.yellow("?")} ${id}`).join("\n");
139
+ return `${c.yellow("[sfmc] modules installed from unknown publisher \u2014 verify before use:")}
140
+ ${list}
141
+ `;
166
142
  }
167
- /* ─────────────────────────────────────────────────────────────────
168
- * info
169
- * ──────────────────────────────────────────────────────────────── */
170
- export async function cmdModuleInfo(args) {
171
- const id = args[0];
172
- if (!id)
173
- return c.yellow("Usage: sfmc module info <id>");
174
- const all = await scanInstalled();
175
- const m = all.find((x) => x.id === id);
176
- if (!m)
177
- return c.red(`Module ${id} not installed at ${path.join(modulesDir(), id)}`);
178
- const lines = [c.bold(`\n${id}`)];
179
- lines.push(` path : ${m.path}`);
180
- lines.push(` files : ${m.fileCount}`);
181
- lines.push(` size : ${fmtBytes(m.totalBytes)}`);
182
- lines.push(` fingerprint : ${m.fingerprint}`);
183
- if (m.manifest) {
184
- lines.push(` schemaVer : ${m.manifest.schemaVersion ?? "(none)"}`);
185
- lines.push(` routes : ${m.manifest.routes?.length ?? 0}`);
186
- if (m.manifest.routes?.length) {
187
- for (const r of m.manifest.routes)
188
- lines.push(c.dim(` ${r.method.padEnd(7)} ${r.path.padEnd(36)} ${r.handler}`));
189
- }
190
- lines.push(` migrations : ${m.manifest.migrations?.length ?? 0}`);
191
- if (m.manifest.migrations?.length) {
192
- for (const mg of m.manifest.migrations)
193
- lines.push(c.dim(` v${mg.version} ${mg.name}`));
194
- }
195
- if (m.manifest.notes)
196
- lines.push(` notes : ${c.dim(m.manifest.notes)}`);
143
+ async function cmdModuleSearch(args) {
144
+ const query = args[0];
145
+ const { index, stale } = await resolveRegistryIndex({ force: true });
146
+ const ids = Object.keys(index).sort((a, b) => a.localeCompare(b));
147
+ if (ids.length === 0) {
148
+ return c.red(
149
+ `
150
+ Registry empty or unreachable (${DEFAULT_REGISTRY_REPO}@${DEFAULT_REGISTRY_TAG}).
151
+ Check network, or install with an explicit source: sfmc mod install <id> --from github:owner/repo@tag
152
+ `
153
+ );
154
+ }
155
+ const staleNote = stale ? c.yellow(" (offline \u2014 showing cached index)\n") : "";
156
+ if (!query) {
157
+ const installed = new Set(listInstalledModuleIdsSync());
158
+ const lines2 = [
159
+ c.bold(`
160
+ First-party registry (${DEFAULT_REGISTRY_REPO}@${DEFAULT_REGISTRY_TAG}) \u2014 ${ids.length} modules`)
161
+ ];
162
+ if (staleNote) lines2.push(staleNote.trimEnd());
163
+ lines2.push(c.dim(` ${"id".padEnd(28)}${"source".padEnd(40)}local`));
164
+ for (const id of ids) {
165
+ const e = index[id];
166
+ const src = `${e.repo}@${e.tag}`;
167
+ const local = installed.has(id) ? c.green("\u25CF") : c.dim("\u25CB");
168
+ lines2.push(` ${local} ${id.padEnd(26)}${src.padEnd(40)}${installed.has(id) ? "installed" : ""}`);
197
169
  }
198
- else {
199
- lines.push(c.yellow(` manifest : (missing or unreadable)`));
200
- }
201
- return lines.join("\n") + "\n";
170
+ lines2.push(c.dim(`
171
+ tip: sfmc mod search <id> \u2192 registry info`));
172
+ lines2.push(c.dim(` sfmc mod install <id> \u2192 download + catalog sync`));
173
+ return lines2.join("\n") + "\n";
174
+ }
175
+ const entry = index[query];
176
+ if (!entry) {
177
+ const hints = ids.filter((id) => id.includes(query) || query.includes(id)).slice(0, 8);
178
+ const hintBlock = hints.length > 0 ? c.dim(`
179
+ Did you mean:
180
+ `) + hints.map((h) => ` ${h}`).join("\n") : c.dim(`
181
+ Known ids: ${ids.slice(0, 12).join(", ")}${ids.length > 12 ? ", \u2026" : ""}`);
182
+ return c.red(t("mod.notInRegistry", { query })) + hintBlock + "\n";
183
+ }
184
+ const installedPath = path.join(modulesDir(), query);
185
+ const isInstalled = existsSync(installedPath);
186
+ const lines = [c.bold(`
187
+ ${query}`)];
188
+ if (staleNote) lines.push(staleNote.trimEnd());
189
+ lines.push(` registry : ${DEFAULT_REGISTRY_REPO}@${DEFAULT_REGISTRY_TAG}`);
190
+ lines.push(` repo : ${entry.repo}`);
191
+ lines.push(` tag : ${entry.tag}`);
192
+ lines.push(` source : github:${entry.repo}@${entry.tag}`);
193
+ lines.push(` github : https://github.com/${entry.repo}/tree/${entry.tag}`);
194
+ lines.push(
195
+ ` local : ${isInstalled ? c.green(`installed @ ${installedPath}`) : c.dim("not installed")}`
196
+ );
197
+ if (!isInstalled) {
198
+ lines.push(c.dim(`
199
+ install: sfmc mod install ${query}`));
200
+ } else {
201
+ lines.push(c.dim(`
202
+ details: sfmc mod info ${query}`));
203
+ }
204
+ return lines.join("\n") + "\n";
202
205
  }
203
- /* ─────────────────────────────────────────────────────────────────
204
- * verify
205
- * ──────────────────────────────────────────────────────────────── */
206
- export async function cmdModuleVerify(args) {
207
- const id = args[0];
208
- if (id) {
209
- const all = await scanInstalled();
210
- const m = all.find((x) => x.id === id);
211
- if (!m)
212
- return c.red(`Module ${id} not installed`);
213
- return c.green(`${id} ok\n fingerprint: ${m.fingerprint}\n`);
206
+ async function cmdModuleInfo(args) {
207
+ const id = args[0];
208
+ if (!id) return c.yellow(t("mod.info.usage"));
209
+ const all = await scanInstalled();
210
+ const m = all.find((x) => x.id === id);
211
+ if (!m) return c.red(t("mod.notInstalledAt", { id, path: path.join(modulesDir(), id) }));
212
+ const lines = [c.bold(`
213
+ ${id}`)];
214
+ lines.push(` path : ${m.path}`);
215
+ lines.push(` files : ${m.fileCount}`);
216
+ lines.push(` size : ${fmtBytes(m.totalBytes)}`);
217
+ lines.push(` fingerprint : ${m.fingerprint}`);
218
+ if (m.manifest) {
219
+ lines.push(` schemaVer : ${m.manifest.schemaVersion ?? "(none)"}`);
220
+ lines.push(` routes : ${m.manifest.routes?.length ?? 0}`);
221
+ if (m.manifest.routes?.length) {
222
+ for (const r of m.manifest.routes) lines.push(c.dim(` ${r.method.padEnd(7)} ${r.path.padEnd(36)} ${r.handler}`));
214
223
  }
215
- const all = await scanInstalled();
216
- const lines = [c.bold("\nVerifying installed modules")];
217
- for (const m of all) {
218
- lines.push(` ${m.id.padEnd(28)} ${shortHash(m.fingerprint)}`);
224
+ lines.push(` migrations : ${m.manifest.migrations?.length ?? 0}`);
225
+ if (m.manifest.migrations?.length) {
226
+ for (const mg of m.manifest.migrations) lines.push(c.dim(` v${mg.version} ${mg.name}`));
219
227
  }
220
- if (all.length === 0)
221
- lines.push(c.dim(" (none)"));
222
- return lines.join("\n") + "\n";
228
+ if (m.manifest.notes) lines.push(` notes : ${c.dim(m.manifest.notes)}`);
229
+ } else {
230
+ lines.push(c.yellow(` manifest : (missing or unreadable)`));
231
+ }
232
+ return lines.join("\n") + "\n";
233
+ }
234
+ async function cmdModuleVerify(args) {
235
+ const id = args[0];
236
+ if (id) {
237
+ const all2 = await scanInstalled();
238
+ const m = all2.find((x) => x.id === id);
239
+ if (!m) return c.red(t("mod.notInstalled", { id }));
240
+ return c.green(t("mod.verifyOk", { id, fp: m.fingerprint }));
241
+ }
242
+ const all = await scanInstalled();
243
+ const lines = [c.bold("\nVerifying installed modules")];
244
+ for (const m of all) {
245
+ lines.push(` ${m.id.padEnd(28)} ${shortHash(m.fingerprint)}`);
246
+ }
247
+ if (all.length === 0) lines.push(c.dim(" (none)"));
248
+ return lines.join("\n") + "\n";
223
249
  }
224
- /* ─────────────────────────────────────────────────────────────────
225
- * install — shells out to tools/fetch-module.mjs
226
- * ──────────────────────────────────────────────────────────────── */
227
- /* ───────────────────────────────────────────────────────────────
228
- * enable / disable — talk to db-server over loopback HTTP
229
- * ─────────────────────────────────────────────────────────────── */
230
250
  function readDbConfig() {
231
- return (readJson(configPath(ROOT, "db_config.json")) ?? {});
251
+ return readJson(configPath(ROOT, "db_config.json")) ?? {};
232
252
  }
233
253
  async function postModuleToggle(id, action) {
234
- const cfg = readDbConfig();
235
- const port = cfg.db_port ?? 3001;
236
- const token = cfg.http_auth || "";
237
- const url = `http://127.0.0.1:${port}/api/sfmc/modules/${encodeURIComponent(id)}/${action}`;
238
- let res;
239
- try {
240
- res = await fetch(url, {
241
- method: "POST",
242
- headers: {
243
- "Content-Type": "application/json",
244
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
245
- },
246
- });
247
- }
248
- catch (err) {
249
- return c.red(`Cannot reach db-server at ${url}: ${err.message}. Is db-service running? (sfmc> start db)`);
250
- }
251
- const text = await res.text();
252
- let body;
253
- try {
254
- body = JSON.parse(text);
255
- }
256
- catch {
257
- body = text;
258
- }
259
- if (!res.ok) {
260
- const err = body?.error ?? `HTTP ${res.status}`;
261
- return c.red(`${action} ${id} failed: ${err}`);
262
- }
263
- const ok = body?.success !== false;
264
- return ok ? c.green(`${action}d ${id}`) : c.red(`${action} ${id} returned: ${text}`);
254
+ const cfg = readDbConfig();
255
+ const port = cfg.db_port ?? 3001;
256
+ const token = cfg.http_auth || "";
257
+ const url = `http://127.0.0.1:${port}/api/sfmc/modules/${encodeURIComponent(id)}/${action}`;
258
+ let res;
259
+ try {
260
+ res = await fetch(url, {
261
+ method: "POST",
262
+ headers: {
263
+ "Content-Type": "application/json",
264
+ ...token ? { Authorization: `Bearer ${token}` } : {}
265
+ }
266
+ });
267
+ } catch (err) {
268
+ return failResult(
269
+ c.red(t("mod.dbUnreachable", { url, message: err.message }))
270
+ );
271
+ }
272
+ const text = await res.text();
273
+ let body;
274
+ try {
275
+ body = JSON.parse(text);
276
+ } catch {
277
+ body = text;
278
+ }
279
+ if (!res.ok) {
280
+ const err = body?.error ?? `HTTP ${res.status}`;
281
+ return failResult(c.red(t("mod.toggleFailed", { action, id, err })));
282
+ }
283
+ const ok = body?.success !== false;
284
+ if (ok) {
285
+ const label = action === "enable" ? t("mod.action.enabled") : t("mod.action.disabled");
286
+ return okResult(c.green(t("mod.toggleOk", { label, id })));
287
+ }
288
+ return failResult(c.red(t("mod.toggleBadBody", { action, id, text })));
265
289
  }
266
- export async function cmdModuleEnable(args) {
267
- const id = args[0];
268
- if (!id)
269
- return c.yellow("Usage: sfmc module enable <id>");
270
- return postModuleToggle(id, "enable");
290
+ async function cmdModuleEnable(args) {
291
+ const id = args[0];
292
+ if (!id) return failResult(c.yellow(t("mod.enable.usage")));
293
+ return postModuleToggle(id, "enable");
271
294
  }
272
- export async function cmdModuleDisable(args) {
273
- const id = args[0];
274
- if (!id)
275
- return c.yellow("Usage: sfmc module disable <id>");
276
- return postModuleToggle(id, "disable");
295
+ async function cmdModuleDisable(args) {
296
+ const id = args[0];
297
+ if (!id) return failResult(c.yellow(t("mod.disable.usage")));
298
+ return postModuleToggle(id, "disable");
277
299
  }
278
- /* ───────────────────────────────────────────────────────────────
279
- * install — shells out to tools/fetch-module.mjs
280
- * ─────────────────────────────────────────────────────────────── */
281
- export async function cmdModuleInstall(args) {
282
- // 支持: install <id> [id2 ...] [--from ...]
283
- const flags = parseFlags(args);
284
- const positional = [];
285
- for (let i = 0; i < args.length; i++) {
286
- if (args[i] === "--from" || args[i] === "--sha256") {
287
- i++;
288
- continue;
289
- }
290
- if (args[i]?.startsWith("--from=") || args[i]?.startsWith("--sha256="))
291
- continue;
292
- if (args[i]?.startsWith("--"))
293
- continue;
294
- positional.push(args[i]);
300
+ async function cmdModuleInstall(args) {
301
+ const flags = parseFlags(args);
302
+ const positional = [];
303
+ for (let i = 0; i < args.length; i++) {
304
+ if (args[i] === "--from" || args[i] === "--sha256") {
305
+ i++;
306
+ continue;
295
307
  }
296
- if (positional.length === 0)
297
- return c.yellow("Usage: sfmc module install <id> [id2 ...] [--from <source>]");
298
- const fetchScript = path.join(ROOT, "tools", "fetch-module.mjs");
299
- if (!existsSync(fetchScript)) {
300
- return c.red(`tools/fetch-module.mjs not found at ${fetchScript}`);
308
+ if (args[i]?.startsWith("--from=") || args[i]?.startsWith("--sha256=")) continue;
309
+ if (args[i]?.startsWith("--")) continue;
310
+ positional.push(args[i]);
311
+ }
312
+ if (positional.length === 0) {
313
+ return c.yellow(t("mod.install.usage"));
314
+ }
315
+ if (flags.link && !flags.from) {
316
+ const { resolveSfmcModulesRoot, packageDirForId } = await import("./sfmc-modules-root.js");
317
+ const modulesRoot = resolveSfmcModulesRoot();
318
+ if (!modulesRoot) {
319
+ return c.red(t("mod.linkNeedsFrom"));
301
320
  }
302
- const sub = ["install", ...positional];
303
- if (flags.from)
304
- sub.push("--from", flags.from);
305
- if (flags.sha256)
306
- sub.push("--sha256", flags.sha256);
307
- return new Promise((resolve) => {
308
- const proc = spawn(process.execPath, [fetchScript, ...sub], { stdio: ["ignore", "pipe", "pipe"] });
309
- let out = "";
310
- proc.stdout?.on("data", (d) => {
311
- out += d.toString();
312
- });
313
- proc.stderr?.on("data", (d) => {
314
- out += d.toString();
315
- });
316
- proc.on("exit", (code) => {
317
- resolve(out + (code === 0 ? "" : `\n[exit ${code}]`));
318
- });
319
- proc.on("error", (e) => resolve(c.red(`spawn failed: ${e.message}`)));
321
+ flags.from = positional.length === 1 ? `dir:${packageDirForId(modulesRoot, positional[0])}` : `dir:${path.join(modulesRoot, "packages")}`;
322
+ }
323
+ const fetchScript = resolveFetchModule();
324
+ if (!fetchScript) {
325
+ return c.red(t("mod.fetchMissing"));
326
+ }
327
+ const sub = ["install", ...positional];
328
+ if (flags.from) sub.push("--from", flags.from);
329
+ if (flags.sha256) sub.push("--sha256", flags.sha256);
330
+ if (flags.link) sub.push("--link");
331
+ return new Promise((resolve) => {
332
+ const proc = spawn(process.execPath, [fetchScript, ...sub], {
333
+ stdio: ["ignore", "pipe", "pipe"],
334
+ env: { ...process.env, SFMC_ROOT: ROOT }
335
+ });
336
+ let out = "";
337
+ proc.stdout?.on("data", (d) => {
338
+ out += d.toString();
339
+ });
340
+ proc.stderr?.on("data", (d) => {
341
+ out += d.toString();
342
+ });
343
+ proc.on("exit", (code) => {
344
+ resolve(out + (code === 0 ? "" : `
345
+ [exit ${code}]`));
320
346
  });
347
+ proc.on("error", (e) => resolve(c.red(`spawn failed: ${e.message}`)));
348
+ });
321
349
  }
322
- /* ─────────────────────────────────────────────────────────────────
323
- * uninstall
324
- * ──────────────────────────────────────────────────────────────── */
325
- export async function cmdModuleUninstall(args) {
326
- const id = args[0];
327
- if (!id)
328
- return c.yellow("Usage: sfmc module uninstall <id>");
329
- const fetchScript = path.join(ROOT, "tools", "fetch-module.mjs");
330
- if (!existsSync(fetchScript)) {
331
- // 回退:仅删目录(旧行为)
332
- const target = path.join(modulesDir(), id);
333
- if (!existsSync(target))
334
- return c.yellow(`Module ${id} not installed (no folder at ${target})`);
335
- await fs.rm(target, { recursive: true, force: true });
336
- return c.green(`Removed ${id} from ${target}\n`);
350
+ async function cmdModuleLink(args) {
351
+ const flags = parseFlags(args);
352
+ const positional = [];
353
+ for (let i = 0; i < args.length; i++) {
354
+ if (args[i] === "--from" || args[i] === "--sha256") {
355
+ i++;
356
+ continue;
337
357
  }
338
- return new Promise((resolve) => {
339
- const proc = spawn(process.execPath, [fetchScript, "uninstall", id], {
340
- stdio: ["ignore", "pipe", "pipe"],
341
- });
342
- let out = "";
343
- proc.stdout?.on("data", (d) => {
344
- out += d.toString();
345
- });
346
- proc.stderr?.on("data", (d) => {
347
- out += d.toString();
348
- });
349
- proc.on("exit", (code) => {
350
- resolve(out + (code === 0 ? "" : `\n[exit ${code}]`));
351
- });
352
- proc.on("error", (e) => resolve(c.red(`spawn failed: ${e.message}`)));
358
+ if (args[i]?.startsWith("--from=") || args[i]?.startsWith("--sha256=")) continue;
359
+ if (args[i]?.startsWith("--")) continue;
360
+ positional.push(args[i]);
361
+ }
362
+ if (positional.length === 0) {
363
+ const { runModuleLinkWizard } = await import("./module-wizard.js");
364
+ return runModuleLinkWizard();
365
+ }
366
+ const id = positional[0];
367
+ const installArgs = [id, "--link"];
368
+ if (flags.from) installArgs.push("--from", flags.from);
369
+ return cmdModuleInstall(installArgs);
370
+ }
371
+ async function cmdModuleUninstall(args) {
372
+ const id = args[0];
373
+ if (!id) return c.yellow(t("mod.uninstall.usage"));
374
+ const fetchScript = resolveFetchModule();
375
+ if (!fetchScript) {
376
+ const target = path.join(modulesDir(), id);
377
+ if (!existsSync(target)) return c.yellow(t("mod.notInstalledFolder", { id, path: target }));
378
+ await fs.rm(target, { recursive: true, force: true });
379
+ return c.green(t("mod.removed", { id, path: target }));
380
+ }
381
+ return new Promise((resolve) => {
382
+ const proc = spawn(process.execPath, [fetchScript, "uninstall", id], {
383
+ stdio: ["ignore", "pipe", "pipe"],
384
+ env: { ...process.env, SFMC_ROOT: ROOT }
385
+ });
386
+ let out = "";
387
+ proc.stdout?.on("data", (d) => {
388
+ out += d.toString();
389
+ });
390
+ proc.stderr?.on("data", (d) => {
391
+ out += d.toString();
353
392
  });
393
+ proc.on("exit", (code) => {
394
+ resolve(out + (code === 0 ? "" : `
395
+ [exit ${code}]`));
396
+ });
397
+ proc.on("error", (e) => resolve(c.red(`spawn failed: ${e.message}`)));
398
+ });
354
399
  }
355
400
  function parseFlags(args) {
356
- const flags = { from: null, sha256: null };
357
- for (let i = 0; i < args.length; i++) {
358
- if (args[i] === "--from")
359
- flags.from = args[++i] ?? null;
360
- else if (args[i]?.startsWith("--from="))
361
- flags.from = args[i].slice("--from=".length);
362
- else if (args[i] === "--sha256")
363
- flags.sha256 = args[++i] ?? null;
401
+ const flags = { from: null, sha256: null, link: false };
402
+ for (let i = 0; i < args.length; i++) {
403
+ if (args[i] === "--from") flags.from = args[++i] ?? null;
404
+ else if (args[i]?.startsWith("--from=")) flags.from = args[i].slice("--from=".length);
405
+ else if (args[i] === "--sha256") flags.sha256 = args[++i] ?? null;
406
+ else if (args[i]?.startsWith("--sha256=")) flags.sha256 = args[i].slice("--sha256=".length);
407
+ else if (args[i] === "--link") flags.link = true;
408
+ }
409
+ return flags;
410
+ }
411
+ async function dispatchModuleCommand(sub, args) {
412
+ switch (sub) {
413
+ case "list":
414
+ return cmdModuleList(args);
415
+ case "search":
416
+ return cmdModuleSearch(args);
417
+ case "install":
418
+ return cmdModuleInstall(args);
419
+ case "uninstall":
420
+ case "remove":
421
+ return cmdModuleUninstall(args);
422
+ case "verify":
423
+ return cmdModuleVerify(args);
424
+ case "info":
425
+ return cmdModuleInfo(args);
426
+ case "enable":
427
+ return (await cmdModuleEnable(args)).message;
428
+ case "disable":
429
+ return (await cmdModuleDisable(args)).message;
430
+ case "build": {
431
+ const { cmdPackBuild } = await import("./pack-lifecycle.js");
432
+ return (await cmdPackBuild(args)).message;
433
+ }
434
+ case "reload": {
435
+ const { cmdReload } = await import("./commands-reload.js");
436
+ return cmdReload(args);
364
437
  }
365
- return flags;
438
+ case "create": {
439
+ const { runModuleCreateWizard } = await import("./module-wizard.js");
440
+ return runModuleCreateWizard();
441
+ }
442
+ case "link":
443
+ return cmdModuleLink(args);
444
+ case "dev": {
445
+ const { runModuleDevWizard } = await import("./module-wizard.js");
446
+ return runModuleDevWizard();
447
+ }
448
+ default:
449
+ return c.yellow(moduleUsage());
450
+ }
451
+ }
452
+ function listInstalledModuleIdsSync() {
453
+ const dir = modulesDir();
454
+ if (!existsSync(dir)) return [];
455
+ try {
456
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => a.localeCompare(b));
457
+ } catch {
458
+ return [];
459
+ }
366
460
  }
367
- //# sourceMappingURL=module-commands.js.map
461
+ export {
462
+ MODULE_CMD_NAMES,
463
+ MODULE_SUBCOMMANDS,
464
+ cmdModuleDisable,
465
+ cmdModuleEnable,
466
+ cmdModuleInfo,
467
+ cmdModuleInstall,
468
+ cmdModuleLink,
469
+ cmdModuleList,
470
+ cmdModuleSearch,
471
+ cmdModuleUninstall,
472
+ cmdModuleVerify,
473
+ dispatchModuleCommand,
474
+ isModuleCommand,
475
+ listInstalledModuleIdsSync,
476
+ moduleUsage,
477
+ paintModuleCmdAlias,
478
+ scanAndWarnUnknown
479
+ };
480
+ //# sourceMappingURL=module-commands.js.map