@syedar/seedar-cli 1.2.0 → 1.3.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 (119) hide show
  1. package/README.md +24 -3
  2. package/dist/cli.d.ts +21 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +110 -587
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/doctor.d.ts +2 -0
  7. package/dist/commands/doctor.d.ts.map +1 -0
  8. package/dist/commands/doctor.js +18 -0
  9. package/dist/commands/doctor.js.map +1 -0
  10. package/dist/commands/install.d.ts +3 -0
  11. package/dist/commands/install.d.ts.map +1 -0
  12. package/dist/commands/install.js +35 -0
  13. package/dist/commands/install.js.map +1 -0
  14. package/dist/commands/lifecycle.d.ts +4 -0
  15. package/dist/commands/lifecycle.d.ts.map +1 -0
  16. package/dist/commands/lifecycle.js +65 -0
  17. package/dist/commands/lifecycle.js.map +1 -0
  18. package/dist/commands/logs.d.ts +4 -0
  19. package/dist/commands/logs.d.ts.map +1 -0
  20. package/dist/commands/logs.js +24 -0
  21. package/dist/commands/logs.js.map +1 -0
  22. package/dist/commands/status.d.ts +4 -0
  23. package/dist/commands/status.d.ts.map +1 -0
  24. package/dist/commands/status.js +243 -0
  25. package/dist/commands/status.js.map +1 -0
  26. package/dist/commands/uninstall.d.ts +5 -0
  27. package/dist/commands/uninstall.d.ts.map +1 -0
  28. package/dist/commands/uninstall.js +166 -0
  29. package/dist/commands/uninstall.js.map +1 -0
  30. package/dist/docker/compose.d.ts +6 -0
  31. package/dist/docker/compose.d.ts.map +1 -0
  32. package/dist/docker/compose.js +55 -0
  33. package/dist/docker/compose.js.map +1 -0
  34. package/dist/docker/health.d.ts +3 -0
  35. package/dist/docker/health.d.ts.map +1 -0
  36. package/dist/docker/health.js +38 -0
  37. package/dist/docker/health.js.map +1 -0
  38. package/dist/docker/ports.d.ts.map +1 -0
  39. package/dist/docker/ports.js.map +1 -0
  40. package/dist/docker/prerequisites.d.ts +2 -0
  41. package/dist/docker/prerequisites.d.ts.map +1 -0
  42. package/dist/docker/prerequisites.js +12 -0
  43. package/dist/docker/prerequisites.js.map +1 -0
  44. package/dist/{process.d.ts → docker/process.d.ts} +2 -1
  45. package/dist/docker/process.d.ts.map +1 -0
  46. package/dist/{process.js → docker/process.js} +11 -0
  47. package/dist/docker/process.js.map +1 -0
  48. package/dist/index.js +1 -5
  49. package/dist/index.js.map +1 -1
  50. package/dist/install/config.d.ts +7 -0
  51. package/dist/install/config.d.ts.map +1 -0
  52. package/dist/install/config.js +82 -0
  53. package/dist/install/config.js.map +1 -0
  54. package/dist/install/flow.d.ts +11 -0
  55. package/dist/install/flow.d.ts.map +1 -0
  56. package/dist/install/flow.js +115 -0
  57. package/dist/install/flow.js.map +1 -0
  58. package/dist/install/output.d.ts +8 -0
  59. package/dist/install/output.d.ts.map +1 -0
  60. package/dist/install/output.js +39 -0
  61. package/dist/install/output.js.map +1 -0
  62. package/dist/install/ports.d.ts +8 -0
  63. package/dist/install/ports.d.ts.map +1 -0
  64. package/dist/install/ports.js +38 -0
  65. package/dist/install/ports.js.map +1 -0
  66. package/dist/install/prompts.d.ts +4 -0
  67. package/dist/install/prompts.d.ts.map +1 -0
  68. package/dist/install/prompts.js +204 -0
  69. package/dist/install/prompts.js.map +1 -0
  70. package/dist/runtime/guards.d.ts +3 -0
  71. package/dist/runtime/guards.d.ts.map +1 -0
  72. package/dist/runtime/guards.js +7 -0
  73. package/dist/runtime/guards.js.map +1 -0
  74. package/dist/{runtime.d.ts → runtime/index.d.ts} +3 -3
  75. package/dist/runtime/index.d.ts.map +1 -0
  76. package/dist/{runtime.js → runtime/index.js} +68 -11
  77. package/dist/runtime/index.js.map +1 -0
  78. package/dist/shared/constants.d.ts +23 -0
  79. package/dist/shared/constants.d.ts.map +1 -0
  80. package/dist/{constants.js → shared/constants.js} +44 -4
  81. package/dist/shared/constants.js.map +1 -0
  82. package/dist/shared/logging.d.ts +3 -0
  83. package/dist/shared/logging.d.ts.map +1 -0
  84. package/dist/shared/logging.js +8 -0
  85. package/dist/shared/logging.js.map +1 -0
  86. package/dist/shared/package.d.ts +2 -0
  87. package/dist/shared/package.d.ts.map +1 -0
  88. package/dist/shared/package.js +2 -0
  89. package/dist/shared/package.js.map +1 -0
  90. package/dist/shared/time.d.ts +2 -0
  91. package/dist/shared/time.d.ts.map +1 -0
  92. package/dist/shared/time.js +4 -0
  93. package/dist/shared/time.js.map +1 -0
  94. package/dist/{types.d.ts → shared/types.d.ts} +9 -0
  95. package/dist/shared/types.d.ts.map +1 -0
  96. package/dist/shared/types.js.map +1 -0
  97. package/dist/shared/ui.d.ts +7 -0
  98. package/dist/shared/ui.d.ts.map +1 -0
  99. package/dist/shared/ui.js +25 -0
  100. package/dist/shared/ui.js.map +1 -0
  101. package/package.json +12 -8
  102. package/dist/constants.d.ts +0 -17
  103. package/dist/constants.d.ts.map +0 -1
  104. package/dist/constants.js.map +0 -1
  105. package/dist/ports.d.ts.map +0 -1
  106. package/dist/ports.js.map +0 -1
  107. package/dist/process.d.ts.map +0 -1
  108. package/dist/process.js.map +0 -1
  109. package/dist/prompts.d.ts +0 -4
  110. package/dist/prompts.d.ts.map +0 -1
  111. package/dist/prompts.js +0 -175
  112. package/dist/prompts.js.map +0 -1
  113. package/dist/runtime.d.ts.map +0 -1
  114. package/dist/runtime.js.map +0 -1
  115. package/dist/types.d.ts.map +0 -1
  116. package/dist/types.js.map +0 -1
  117. /package/dist/{ports.d.ts → docker/ports.d.ts} +0 -0
  118. /package/dist/{ports.js → docker/ports.js} +0 -0
  119. /package/dist/{types.js → shared/types.js} +0 -0
package/dist/cli.js CHANGED
@@ -1,610 +1,133 @@
1
- import { access, appendFile, mkdir, rm } from "node:fs/promises";
2
- import path from "node:path";
3
- import { DEFAULT_SERVER_IMAGE, DEFAULT_VERSION, DEFAULT_WEB_IMAGE, MIN_NODE_MAJOR, REQUIRED_ENV_KEYS, VALID_SERVICES, } from "./constants.js";
4
- import { runCommand, runCommandOrThrow, runDockerCompose, runDockerComposeOrThrow } from "./process.js";
5
- import { getAvailablePort, isPortAvailable } from "./ports.js";
6
- import { collectInstallConfig, ensurePortsAvailable } from "./prompts.js";
7
- import { backupRuntime, getRuntimeLayout, hasRuntimeConfig, readEnvConfig, readInstallState, readInstalledVersion, restoreRuntimeFromBackup, writeInstalledVersion, writeInstallState, writeRuntimeFiles, } from "./runtime.js";
8
- const PORT_ENV_KEYS = ["MYSQL_PORT", "SERVER_PORT", "WEB_PORT"];
9
- const COMPOSE_PORT_CONFLICT_REGEX = /Bind for (?:\[[^\]]+\]|[0-9.]+):(\d+) failed: port is already allocated/i;
10
- const ENV_PORT_CONFLICT_REGEX = /(MYSQL_PORT|SERVER_PORT|WEB_PORT)=(\d+) 已被占用/i;
11
- function parseArgs(rawArgs) {
12
- const flags = {
13
- yes: false,
14
- force: false,
15
- follow: false,
16
- removeData: false,
17
- };
18
- const positional = [];
19
- for (const arg of rawArgs) {
20
- if (arg === "--yes" || arg === "-y") {
21
- flags.yes = true;
22
- continue;
23
- }
24
- if (arg === "--force") {
25
- flags.force = true;
26
- continue;
27
- }
28
- if (arg === "--follow" || arg === "-f") {
29
- flags.follow = true;
30
- continue;
31
- }
32
- if (arg === "--remove-data") {
33
- flags.removeData = true;
34
- continue;
35
- }
36
- positional.push(arg);
37
- }
38
- const [command = "help", ...rest] = positional;
1
+ import { readFileSync } from "node:fs";
2
+ import { Command, CommanderError } from "commander";
3
+ import { doctorCommand } from "./commands/doctor.js";
4
+ import { installCommand } from "./commands/install.js";
5
+ import { logsCommand } from "./commands/logs.js";
6
+ import { purgeCommand, removeAllCommand, uninstallCommand } from "./commands/uninstall.js";
7
+ import { startCommand, stopCommand, updateCommand } from "./commands/lifecycle.js";
8
+ import { statusCommand } from "./commands/status.js";
9
+ function readPackageVersion() {
10
+ const packageJsonUrl = new URL("../package.json", import.meta.url);
11
+ const packageJson = JSON.parse(readFileSync(packageJsonUrl, "utf8"));
12
+ return packageJson.version ?? "0.0.0";
13
+ }
14
+ function createDefaultHandlers() {
39
15
  return {
40
- command,
41
- positional: rest,
42
- flags,
16
+ install: installCommand,
17
+ start: startCommand,
18
+ stop: stopCommand,
19
+ update: updateCommand,
20
+ uninstall: uninstallCommand,
21
+ removeAll: removeAllCommand,
22
+ purge: purgeCommand,
23
+ status: statusCommand,
24
+ logs: logsCommand,
25
+ doctor: doctorCommand,
43
26
  };
44
27
  }
45
- async function writeCliLog(layout, message) {
46
- const line = `[${new Date().toISOString()}] ${message}\n`;
47
- await mkdir(layout.logsDir, { recursive: true });
48
- await appendFile(path.join(layout.logsDir, "cli.log"), line, "utf8");
49
- }
50
- async function requireRuntimeConfig(layout) {
51
- if (!(await hasRuntimeConfig(layout))) {
52
- throw new Error(`未检测到运行时配置,请先执行 seedar install。安装目录: ${layout.installRoot}`);
53
- }
54
- }
55
- async function ensurePrerequisites() {
56
- const major = Number(process.versions.node.split(".")[0]);
57
- if (major < MIN_NODE_MAJOR) {
58
- throw new Error(`Node.js 版本过低,当前 ${process.version},需要 >= ${MIN_NODE_MAJOR}`);
59
- }
60
- await runCommandOrThrow("docker", ["--version"]);
61
- await runCommandOrThrow("docker", ["compose", "version"]);
62
- await runCommandOrThrow("docker", ["info"]);
63
- }
64
- async function wait(ms) {
65
- await new Promise((resolve) => setTimeout(resolve, ms));
66
- }
67
- async function getServiceContainerId(layout, service) {
68
- const result = await runDockerCompose(layout, ["ps", "-q", service]);
69
- const id = result.stdout.trim();
70
- return id || null;
71
- }
72
- async function waitForServiceHealthy(layout, service, timeoutMs = 120_000) {
73
- const startedAt = Date.now();
74
- while (Date.now() - startedAt < timeoutMs) {
75
- const containerId = await getServiceContainerId(layout, service);
76
- if (!containerId) {
77
- await wait(2_000);
78
- continue;
79
- }
80
- const inspectResult = await runCommand("docker", [
81
- "inspect",
82
- "--format",
83
- "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}",
84
- containerId,
85
- ]);
86
- const state = inspectResult.stdout.trim();
87
- if (state === "healthy" || state === "running") {
88
- return;
89
- }
90
- if (state === "unhealthy" || state === "exited") {
91
- throw new Error(`${service} 服务状态异常: ${state}`);
92
- }
93
- await wait(3_000);
94
- }
95
- throw new Error(`等待 ${service} 服务健康检查超时`);
96
- }
97
- function printInstallSummary(layout, env) {
98
- console.log("Seedar 安装完成。");
99
- console.log(`安装目录: ${layout.installRoot}`);
100
- console.log(`Web: http://localhost:${env.WEB_PORT}`);
101
- console.log(`Server: http://localhost:${env.SERVER_PORT}`);
102
- console.log(`MySQL: localhost:${env.MYSQL_PORT}`);
103
- console.log(`版本: ${env.SEEDAR_VERSION}`);
28
+ function toCliFlags(options) {
29
+ return {
30
+ yes: Boolean(options.yes),
31
+ force: Boolean(options.force),
32
+ follow: Boolean(options.follow),
33
+ removeData: Boolean(options.removeData),
34
+ all: Boolean(options.all),
35
+ };
104
36
  }
105
- async function runInstallFlow(layout, env) {
106
- await writeCliLog(layout, `开始安装,目标版本 ${env.SEEDAR_VERSION}`);
107
- await runDockerComposeOrThrow(layout, ["pull", "mysql", "server", "web"], {
108
- stdio: "inherit",
37
+ function registerCommands(program, handlers) {
38
+ program
39
+ .command("install [version]")
40
+ .description("Install Seedar")
41
+ .action(async function (version) {
42
+ await handlers.install(version, toCliFlags(this.optsWithGlobals()));
109
43
  });
110
- await runDockerComposeOrThrow(layout, ["up", "-d", "mysql"], {
111
- stdio: "inherit",
44
+ program.command("start").description("Start Seedar services").action(async () => {
45
+ await handlers.start();
112
46
  });
113
- await waitForServiceHealthy(layout, "mysql");
114
- await runDockerComposeOrThrow(layout, ["run", "--rm", "migrate"], {
115
- stdio: "inherit",
47
+ program.command("stop").description("Stop Seedar services").action(async () => {
48
+ await handlers.stop();
116
49
  });
117
- await runDockerComposeOrThrow(layout, ["up", "-d", "server", "web"], {
118
- stdio: "inherit",
50
+ program
51
+ .command("update [version]")
52
+ .description("Update Seedar to a target version")
53
+ .action(async function (version) {
54
+ await handlers.update(version);
119
55
  });
120
- await writeCliLog(layout, `安装完成,版本 ${env.SEEDAR_VERSION}`);
121
- }
122
- function parseComposePortConflict(error) {
123
- const message = error instanceof Error ? error.message : String(error);
124
- const matched = COMPOSE_PORT_CONFLICT_REGEX.exec(message);
125
- if (!matched) {
126
- return null;
127
- }
128
- const port = Number(matched[1]);
129
- return Number.isInteger(port) ? port : null;
130
- }
131
- function parseEnsurePortConflict(error) {
132
- const message = error instanceof Error ? error.message : String(error);
133
- const matched = ENV_PORT_CONFLICT_REGEX.exec(message);
134
- if (!matched) {
135
- return null;
136
- }
137
- const key = matched[1];
138
- const port = Number(matched[2]);
139
- if (!Number.isInteger(port)) {
140
- return null;
141
- }
142
- return { key, port };
143
- }
144
- function findPortKeyByPort(env, port) {
145
- const targetPort = String(port);
146
- for (const key of PORT_ENV_KEYS) {
147
- if (env[key] === targetPort) {
148
- return key;
149
- }
150
- }
151
- return null;
152
- }
153
- async function autoShiftConflictPort(env, key, fromPort) {
154
- const occupiedByConfig = new Set(PORT_ENV_KEYS.filter((candidateKey) => candidateKey !== key).map((candidateKey) => Number(env[candidateKey])));
155
- const nextPort = await getAvailablePort(fromPort + 1, occupiedByConfig);
156
- env[key] = String(nextPort);
157
- return nextPort;
158
- }
159
- async function runInstallFlowWithRetry(layout, env) {
160
- const maxAttempts = 2;
161
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
162
- try {
163
- await ensurePortsAvailable(env);
164
- await writeRuntimeFiles(layout, env);
165
- await runInstallFlow(layout, env);
166
- return;
167
- }
168
- catch (error) {
169
- const ensureConflict = parseEnsurePortConflict(error);
170
- if (ensureConflict && attempt < maxAttempts) {
171
- const shiftedTo = await autoShiftConflictPort(env, ensureConflict.key, ensureConflict.port);
172
- console.warn(`检测到 ${ensureConflict.key}=${ensureConflict.port} 已被占用,自动调整为 ${shiftedTo} 后重试安装。`);
173
- continue;
174
- }
175
- const composeConflictPort = parseComposePortConflict(error);
176
- if (composeConflictPort && attempt < maxAttempts) {
177
- const conflictKey = findPortKeyByPort(env, composeConflictPort);
178
- if (conflictKey) {
179
- const shiftedTo = await autoShiftConflictPort(env, conflictKey, composeConflictPort);
180
- console.warn(`安装过程中检测到端口冲突 ${conflictKey}=${composeConflictPort},自动调整为 ${shiftedTo} 后重试。`);
181
- continue;
182
- }
183
- }
184
- throw error;
185
- }
186
- }
187
- throw new Error("安装重试失败");
188
- }
189
- async function parseComposePsOutput(layout) {
190
- const result = await runDockerCompose(layout, ["ps", "--all", "--format", "json"]);
191
- const raw = result.stdout.trim();
192
- if (!raw) {
193
- return [];
194
- }
195
- try {
196
- const parsed = JSON.parse(raw);
197
- return Array.isArray(parsed) ? parsed : [];
198
- }
199
- catch {
200
- return raw
201
- .split(/\r?\n/)
202
- .filter(Boolean)
203
- .map((line) => JSON.parse(line));
204
- }
205
- }
206
- function getPublishersFromServices(services) {
207
- const ports = new Set();
208
- for (const service of services) {
209
- const publishers = service.Publishers;
210
- if (!Array.isArray(publishers)) {
211
- continue;
212
- }
213
- for (const publisher of publishers) {
214
- if (publisher &&
215
- typeof publisher === "object" &&
216
- "PublishedPort" in publisher &&
217
- typeof publisher.PublishedPort === "number") {
218
- ports.add(String(publisher.PublishedPort));
219
- }
220
- }
221
- }
222
- return ports;
223
- }
224
- async function getDiskFreeBytes(targetPath) {
225
- if (process.platform === "win32") {
226
- const root = path.parse(path.resolve(targetPath)).root.replace(/\\$/, "");
227
- const result = await runCommand("powershell", [
228
- "-NoProfile",
229
- "-Command",
230
- `(Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='${root}'").FreeSpace`,
231
- ], { shell: false });
232
- const value = Number(result.stdout.trim());
233
- return Number.isFinite(value) ? value : null;
234
- }
235
- const result = await runCommand("df", ["-Pk", targetPath]);
236
- const lines = result.stdout.trim().split(/\r?\n/);
237
- if (lines.length < 2) {
238
- return null;
239
- }
240
- const parts = lines[1].trim().split(/\s+/);
241
- const availableKb = Number(parts[3]);
242
- if (!Number.isFinite(availableKb)) {
243
- return null;
244
- }
245
- return availableKb * 1024;
246
- }
247
- async function collectDoctorChecks(layout) {
248
- const checks = [];
249
- const nodeMajor = Number(process.versions.node.split(".")[0]);
250
- checks.push({
251
- code: "D001",
252
- status: nodeMajor >= MIN_NODE_MAJOR ? "ok" : "fail",
253
- title: "Node.js 版本",
254
- detail: nodeMajor >= MIN_NODE_MAJOR
255
- ? `当前 ${process.version}`
256
- : `当前 ${process.version},需要 >= ${MIN_NODE_MAJOR}`,
56
+ program
57
+ .command("uninstall")
58
+ .description("Uninstall the current Seedar installation")
59
+ .action(async function () {
60
+ await handlers.uninstall(toCliFlags(this.optsWithGlobals()));
257
61
  });
258
- const dockerVersion = await runCommand("docker", ["--version"]);
259
- checks.push({
260
- code: "D002",
261
- status: dockerVersion.code === 0 ? "ok" : "fail",
262
- title: "Docker CLI",
263
- detail: dockerVersion.code === 0
264
- ? dockerVersion.stdout.trim()
265
- : dockerVersion.stderr.trim() || "无法执行 docker --version",
62
+ program
63
+ .command("remove")
64
+ .description("Remove the installation and self-uninstall the CLI")
65
+ .action(async function () {
66
+ await handlers.removeAll(toCliFlags(this.optsWithGlobals()));
266
67
  });
267
- const dockerInfo = await runCommand("docker", ["info"]);
268
- checks.push({
269
- code: "D003",
270
- status: dockerInfo.code === 0 ? "ok" : "fail",
271
- title: "Docker Daemon",
272
- detail: dockerInfo.code === 0
273
- ? "Docker daemon 可用"
274
- : dockerInfo.stderr.trim() || "无法连接 Docker daemon",
68
+ program
69
+ .command("purge")
70
+ .description("Purge the installation directory and all data")
71
+ .action(async function () {
72
+ await handlers.purge(toCliFlags(this.optsWithGlobals()));
275
73
  });
276
- const composeVersion = await runCommand("docker", ["compose", "version"]);
277
- checks.push({
278
- code: "D004",
279
- status: composeVersion.code === 0 ? "ok" : "fail",
280
- title: "Docker Compose",
281
- detail: composeVersion.code === 0
282
- ? composeVersion.stdout.trim()
283
- : composeVersion.stderr.trim() || "无法执行 docker compose version",
74
+ program.command("status").description("Show the current installation status").action(async () => {
75
+ await handlers.status();
284
76
  });
285
- const installRootParent = path.dirname(layout.installRoot);
286
- try {
287
- await access(installRootParent);
288
- checks.push({
289
- code: "D005",
290
- status: "ok",
291
- title: "安装目录访问",
292
- detail: `可访问 ${installRootParent}`,
293
- });
294
- }
295
- catch (error) {
296
- checks.push({
297
- code: "D005",
298
- status: "fail",
299
- title: "安装目录访问",
300
- detail: error instanceof Error ? error.message : "无法访问安装目录父级路径",
301
- });
302
- }
303
- const diskFree = await getDiskFreeBytes(layout.installRoot);
304
- if (diskFree === null) {
305
- checks.push({
306
- code: "D006",
307
- status: "warn",
308
- title: "磁盘剩余空间",
309
- detail: "无法识别磁盘剩余空间",
310
- });
311
- }
312
- else {
313
- const diskFreeGb = (diskFree / 1024 / 1024 / 1024).toFixed(2);
314
- checks.push({
315
- code: "D006",
316
- status: diskFree >= 2 * 1024 * 1024 * 1024 ? "ok" : "warn",
317
- title: "磁盘剩余空间",
318
- detail: `${diskFreeGb} GB`,
319
- });
320
- }
321
- const hasConfig = await hasRuntimeConfig(layout);
322
- if (!hasConfig) {
323
- const defaultPorts = [
324
- ["D009", "3306"],
325
- ["D010", "8090"],
326
- ["D011", "8080"],
327
- ];
328
- for (const [code, port] of defaultPorts) {
329
- const available = await isPortAvailable(Number(port));
330
- checks.push({
331
- code,
332
- status: available ? "ok" : "warn",
333
- title: `默认端口 ${port}`,
334
- detail: available ? "端口当前可用" : "端口已被占用,安装时需要改配",
335
- });
336
- }
337
- return checks;
338
- }
339
- let envConfig = null;
340
- try {
341
- envConfig = await readEnvConfig(layout);
342
- checks.push({
343
- code: "D007",
344
- status: "ok",
345
- title: "运行时配置",
346
- detail: `已检测到 ${REQUIRED_ENV_KEYS.length} 个必填字段`,
347
- });
348
- }
349
- catch (error) {
350
- checks.push({
351
- code: "D007",
352
- status: "fail",
353
- title: "运行时配置",
354
- detail: error instanceof Error ? error.message : "运行时配置损坏",
355
- });
356
- }
357
- if (!envConfig) {
358
- return checks;
359
- }
360
- const composeConfig = await runDockerCompose(layout, ["config", "-q"]);
361
- checks.push({
362
- code: "D008",
363
- status: composeConfig.code === 0 ? "ok" : "fail",
364
- title: "Compose 配置",
365
- detail: composeConfig.code === 0
366
- ? "docker compose config 校验通过"
367
- : composeConfig.stderr.trim() || "docker compose config 校验失败",
77
+ program
78
+ .command("logs [service]")
79
+ .description("Show service logs")
80
+ .action(async function (service) {
81
+ await handlers.logs(service, toCliFlags(this.optsWithGlobals()));
368
82
  });
369
- const services = await parseComposePsOutput(layout);
370
- const publishedPorts = getPublishersFromServices(services);
371
- const envPorts = [envConfig.MYSQL_PORT, envConfig.SERVER_PORT, envConfig.WEB_PORT];
372
- for (const [index, port] of envPorts.entries()) {
373
- const inUseByCurrentProject = publishedPorts.has(port);
374
- const available = await isPortAvailable(Number(port));
375
- const status = inUseByCurrentProject || available ? "ok" : "warn";
376
- checks.push({
377
- code: `D01${index}`,
378
- status,
379
- title: `端口 ${port}`,
380
- detail: inUseByCurrentProject
381
- ? "端口已由当前 Seedar 安装占用"
382
- : available
383
- ? "端口配置可用"
384
- : "端口已被其他进程占用",
385
- });
386
- }
387
- const serverImage = `${DEFAULT_SERVER_IMAGE}:${envConfig.SEEDAR_VERSION}`;
388
- const webImage = `${DEFAULT_WEB_IMAGE}:${envConfig.SEEDAR_VERSION}`;
389
- for (const [index, image] of [serverImage, webImage].entries()) {
390
- const manifest = await runCommand("docker", ["manifest", "inspect", image]);
391
- checks.push({
392
- code: `D02${index}`,
393
- status: manifest.code === 0 ? "ok" : "fail",
394
- title: `镜像可访问性 ${image}`,
395
- detail: manifest.code === 0
396
- ? "镜像清单可访问"
397
- : manifest.stderr.trim() || "无法访问镜像清单,请检查 DockerHub 凭证或镜像发布状态",
398
- });
399
- }
400
- return checks;
401
- }
402
- async function installCommand(versionArg, flags) {
403
- const layout = getRuntimeLayout();
404
- await ensurePrerequisites();
405
- const state = await readInstallState(layout);
406
- const hasConfig = await hasRuntimeConfig(layout);
407
- if (state === "installed" && hasConfig) {
408
- throw new Error(`检测到现有安装目录 ${layout.installRoot}。如需升级请使用 seedar update。`);
409
- }
410
- let env;
411
- if (state === "uninstalled" && hasConfig) {
412
- env = await readEnvConfig(layout);
413
- env.SEEDAR_VERSION = versionArg ?? env.SEEDAR_VERSION;
414
- await writeCliLog(layout, `复用已有配置重新安装,目标版本 ${env.SEEDAR_VERSION}`);
415
- }
416
- else {
417
- env = await collectInstallConfig(layout, versionArg, flags);
418
- }
419
- await runInstallFlowWithRetry(layout, env);
420
- await writeInstalledVersion(layout, env.SEEDAR_VERSION);
421
- await writeInstallState(layout, "installed");
422
- printInstallSummary(layout, env);
83
+ program.command("doctor").description("Run environment health checks").action(async () => {
84
+ await handlers.doctor();
85
+ });
86
+ return program;
423
87
  }
424
- async function updateCommand(versionArg) {
425
- const layout = getRuntimeLayout();
426
- await ensurePrerequisites();
427
- await requireRuntimeConfig(layout);
428
- const currentEnv = await readEnvConfig(layout);
429
- const currentVersion = await readInstalledVersion(layout);
430
- const nextVersion = versionArg ?? DEFAULT_VERSION;
431
- const backupDir = await backupRuntime(layout);
432
- const nextEnv = {
433
- ...currentEnv,
434
- SEEDAR_VERSION: nextVersion,
88
+ export function createProgram(overrides = {}) {
89
+ const handlers = {
90
+ ...createDefaultHandlers(),
91
+ ...overrides,
435
92
  };
436
- await writeRuntimeFiles(layout, nextEnv);
93
+ const program = new Command();
94
+ program
95
+ .name("seedar")
96
+ .description("Seedar deployment CLI")
97
+ .option("-y, --yes", "Use default answers without prompting")
98
+ .option("-f, --follow", "Follow log output")
99
+ .option("--force", "Skip confirmation prompts")
100
+ .option("--remove-data", "Remove local data directory")
101
+ .option("--all", "Remove the installation and self-uninstall the CLI")
102
+ .version(readPackageVersion())
103
+ .addHelpCommand(true)
104
+ .showHelpAfterError("(use --help for usage)")
105
+ .exitOverride();
106
+ program.addHelpText("afterAll", `
107
+ Examples:
108
+ seedar install
109
+ seedar install 1.2.3 -y
110
+ seedar logs server --follow
111
+ seedar logs postgres --follow
112
+ seedar uninstall --remove-data --force
113
+ `);
114
+ return registerCommands(program, handlers);
115
+ }
116
+ export async function main(rawArgs = process.argv) {
117
+ const program = createProgram();
437
118
  try {
438
- await writeCliLog(layout, `开始升级,当前版本 ${currentVersion ?? "unknown"} -> ${nextVersion}`);
439
- await runDockerComposeOrThrow(layout, ["pull", "mysql", "server", "web"], {
440
- stdio: "inherit",
441
- });
442
- await runDockerComposeOrThrow(layout, ["up", "-d", "mysql"], {
443
- stdio: "inherit",
444
- });
445
- await waitForServiceHealthy(layout, "mysql");
446
- await runDockerComposeOrThrow(layout, ["run", "--rm", "migrate"], {
447
- stdio: "inherit",
448
- });
449
- await runDockerComposeOrThrow(layout, ["up", "-d", "server", "web"], {
450
- stdio: "inherit",
451
- });
452
- await writeInstalledVersion(layout, nextVersion);
453
- await writeInstallState(layout, "installed");
454
- await writeCliLog(layout, `升级完成,版本 ${nextVersion}`);
455
- console.log(`Seedar 已升级到 ${nextVersion}`);
456
- console.log(`备份目录: ${backupDir}`);
119
+ await program.parseAsync(rawArgs);
457
120
  }
458
121
  catch (error) {
459
- await restoreRuntimeFromBackup(layout, backupDir);
460
- await writeCliLog(layout, `升级失败,已恢复运行时配置。备份目录 ${backupDir}`);
461
- throw new Error(`升级失败,已恢复运行时配置。备份目录: ${backupDir}\n${error instanceof Error ? error.message : "未知错误"}`);
462
- }
463
- }
464
- async function uninstallCommand(flags) {
465
- const layout = getRuntimeLayout();
466
- await ensurePrerequisites();
467
- await requireRuntimeConfig(layout);
468
- await writeCliLog(layout, "开始卸载");
469
- await runDockerComposeOrThrow(layout, ["down", "--remove-orphans"], {
470
- stdio: "inherit",
471
- });
472
- if (flags.removeData) {
473
- if (!flags.force && process.stdin.isTTY) {
474
- console.log("检测到 --remove-data,将删除本地数据目录。若需跳过确认,请附带 --force。");
475
- throw new Error("请确认后重新执行: seedar uninstall --remove-data --force");
476
- }
477
- const resolvedDataPath = path.resolve(layout.dataDir);
478
- const installRoot = path.resolve(layout.installRoot);
479
- if (!resolvedDataPath.startsWith(installRoot)) {
480
- throw new Error(`拒绝删除 installRoot 之外的路径: ${resolvedDataPath}`);
481
- }
482
- await rm(layout.dataDir, { recursive: true, force: true });
483
- await writeCliLog(layout, `已删除数据目录 ${layout.dataDir}`);
484
- }
485
- await writeInstallState(layout, "uninstalled");
486
- await writeCliLog(layout, "卸载完成");
487
- console.log("Seedar 已卸载。");
488
- console.log(`配置保留在: ${layout.runtimeDir}`);
489
- console.log(`备份保留在: ${layout.backupsDir}`);
490
- if (!flags.removeData) {
491
- console.log(`数据保留在: ${layout.dataDir}`);
492
- }
493
- }
494
- async function statusCommand() {
495
- const layout = getRuntimeLayout();
496
- const hasConfig = await hasRuntimeConfig(layout);
497
- if (!hasConfig) {
498
- console.log(`未检测到 Seedar 安装。预期目录: ${layout.installRoot}`);
499
- return;
500
- }
501
- const env = await readEnvConfig(layout);
502
- const state = await readInstallState(layout);
503
- const version = await readInstalledVersion(layout);
504
- const services = await parseComposePsOutput(layout);
505
- console.log(`安装目录: ${layout.installRoot}`);
506
- console.log(`状态: ${state}`);
507
- console.log(`目标版本: ${version ?? env.SEEDAR_VERSION}`);
508
- console.log(`Web: http://localhost:${env.WEB_PORT}`);
509
- console.log(`Server: http://localhost:${env.SERVER_PORT}`);
510
- console.log(`MySQL: localhost:${env.MYSQL_PORT}`);
511
- if (services.length === 0) {
512
- console.log("当前没有运行中的容器。");
513
- return;
514
- }
515
- console.log("");
516
- console.log("容器状态:");
517
- for (const service of services) {
518
- const serviceName = String(service.Service ?? service.Name ?? "unknown");
519
- const status = String(service.State ?? "unknown");
520
- const health = service.Health ? `, health=${String(service.Health)}` : "";
521
- const publishers = Array.isArray(service.Publishers)
522
- ? service.Publishers.map((item) => {
523
- if (item &&
524
- typeof item === "object" &&
525
- "PublishedPort" in item &&
526
- "TargetPort" in item) {
527
- return `${String(item.PublishedPort)}->${String(item.TargetPort)}`;
528
- }
529
- return null;
530
- })
531
- .filter(Boolean)
532
- .join(", ")
533
- : "";
534
- const ports = publishers ? `, ports=${publishers}` : "";
535
- console.log(`- ${serviceName}: ${status}${health}${ports}`);
536
- }
537
- }
538
- async function logsCommand(serviceArg, flags) {
539
- const layout = getRuntimeLayout();
540
- await requireRuntimeConfig(layout);
541
- if (serviceArg && !VALID_SERVICES.includes(serviceArg)) {
542
- throw new Error(`未知服务 ${serviceArg},可选值: ${VALID_SERVICES.join(", ")}`);
543
- }
544
- const args = ["logs", "--tail", "200"];
545
- if (flags.follow) {
546
- args.push("--follow");
547
- }
548
- if (serviceArg) {
549
- args.push(serviceArg);
550
- }
551
- await runDockerComposeOrThrow(layout, args, { stdio: "inherit" });
552
- }
553
- async function doctorCommand() {
554
- const layout = getRuntimeLayout();
555
- const checks = await collectDoctorChecks(layout);
556
- let failed = false;
557
- for (const check of checks) {
558
- const prefix = check.status === "ok" ? "OK" : check.status === "warn" ? "WARN" : "FAIL";
559
- console.log(`[${prefix} ${check.code}] ${check.title}: ${check.detail}`);
560
- if (check.status === "fail") {
561
- failed = true;
122
+ if (error instanceof CommanderError) {
123
+ if (error.code !== "commander.helpDisplayed" && error.code !== "commander.version") {
124
+ process.exitCode = 1;
125
+ }
126
+ return;
562
127
  }
563
- }
564
- if (failed) {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ console.error(message);
565
130
  process.exitCode = 1;
566
131
  }
567
132
  }
568
- function printHelp() {
569
- console.log(`Seedar CLI
570
-
571
- 用法:
572
- seedar install [version]
573
- seedar update [version]
574
- seedar uninstall [--remove-data] [--force]
575
- seedar status
576
- seedar logs [mysql|server|web|migrate] [--follow]
577
- seedar doctor
578
- `);
579
- }
580
- export async function main(rawArgs) {
581
- const parsed = parseArgs(rawArgs);
582
- switch (parsed.command) {
583
- case "install":
584
- await installCommand(parsed.positional[0], parsed.flags);
585
- return;
586
- case "update":
587
- await updateCommand(parsed.positional[0]);
588
- return;
589
- case "uninstall":
590
- await uninstallCommand(parsed.flags);
591
- return;
592
- case "status":
593
- await statusCommand();
594
- return;
595
- case "logs":
596
- await logsCommand(parsed.positional[0], parsed.flags);
597
- return;
598
- case "doctor":
599
- await doctorCommand();
600
- return;
601
- case "help":
602
- case "--help":
603
- case "-h":
604
- printHelp();
605
- return;
606
- default:
607
- throw new Error(`未知命令 ${parsed.command}。使用 seedar help 查看帮助。`);
608
- }
609
- }
610
133
  //# sourceMappingURL=cli.js.map