@syedar/seedar-cli 1.2.0 → 1.3.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 +24 -3
- package/dist/cli.d.ts +21 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +110 -587
- package/dist/cli.js.map +1 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/install.d.ts +3 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +35 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/lifecycle.d.ts +4 -0
- package/dist/commands/lifecycle.d.ts.map +1 -0
- package/dist/commands/lifecycle.js +65 -0
- package/dist/commands/lifecycle.js.map +1 -0
- package/dist/commands/logs.d.ts +4 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +24 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +243 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/uninstall.d.ts +5 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +166 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/docker/compose.d.ts +6 -0
- package/dist/docker/compose.d.ts.map +1 -0
- package/dist/docker/compose.js +55 -0
- package/dist/docker/compose.js.map +1 -0
- package/dist/docker/health.d.ts +3 -0
- package/dist/docker/health.d.ts.map +1 -0
- package/dist/docker/health.js +38 -0
- package/dist/docker/health.js.map +1 -0
- package/dist/docker/ports.d.ts.map +1 -0
- package/dist/docker/ports.js.map +1 -0
- package/dist/docker/prerequisites.d.ts +2 -0
- package/dist/docker/prerequisites.d.ts.map +1 -0
- package/dist/docker/prerequisites.js +12 -0
- package/dist/docker/prerequisites.js.map +1 -0
- package/dist/{process.d.ts → docker/process.d.ts} +2 -1
- package/dist/docker/process.d.ts.map +1 -0
- package/dist/{process.js → docker/process.js} +11 -0
- package/dist/docker/process.js.map +1 -0
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/install/config.d.ts +7 -0
- package/dist/install/config.d.ts.map +1 -0
- package/dist/install/config.js +82 -0
- package/dist/install/config.js.map +1 -0
- package/dist/install/flow.d.ts +11 -0
- package/dist/install/flow.d.ts.map +1 -0
- package/dist/install/flow.js +115 -0
- package/dist/install/flow.js.map +1 -0
- package/dist/install/output.d.ts +8 -0
- package/dist/install/output.d.ts.map +1 -0
- package/dist/install/output.js +39 -0
- package/dist/install/output.js.map +1 -0
- package/dist/install/ports.d.ts +8 -0
- package/dist/install/ports.d.ts.map +1 -0
- package/dist/install/ports.js +38 -0
- package/dist/install/ports.js.map +1 -0
- package/dist/install/prompts.d.ts +4 -0
- package/dist/install/prompts.d.ts.map +1 -0
- package/dist/install/prompts.js +204 -0
- package/dist/install/prompts.js.map +1 -0
- package/dist/runtime/guards.d.ts +3 -0
- package/dist/runtime/guards.d.ts.map +1 -0
- package/dist/runtime/guards.js +7 -0
- package/dist/runtime/guards.js.map +1 -0
- package/dist/{runtime.d.ts → runtime/index.d.ts} +3 -3
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/{runtime.js → runtime/index.js} +68 -11
- package/dist/runtime/index.js.map +1 -0
- package/dist/shared/constants.d.ts +23 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/{constants.js → shared/constants.js} +44 -4
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/logging.d.ts +3 -0
- package/dist/shared/logging.d.ts.map +1 -0
- package/dist/shared/logging.js +8 -0
- package/dist/shared/logging.js.map +1 -0
- package/dist/shared/package.d.ts +2 -0
- package/dist/shared/package.d.ts.map +1 -0
- package/dist/shared/package.js +2 -0
- package/dist/shared/package.js.map +1 -0
- package/dist/shared/time.d.ts +2 -0
- package/dist/shared/time.d.ts.map +1 -0
- package/dist/shared/time.js +4 -0
- package/dist/shared/time.js.map +1 -0
- package/dist/{types.d.ts → shared/types.d.ts} +9 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/ui.d.ts +7 -0
- package/dist/shared/ui.d.ts.map +1 -0
- package/dist/shared/ui.js +25 -0
- package/dist/shared/ui.js.map +1 -0
- package/package.json +12 -8
- package/dist/constants.d.ts +0 -17
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/ports.d.ts.map +0 -1
- package/dist/ports.js.map +0 -1
- package/dist/process.d.ts.map +0 -1
- package/dist/process.js.map +0 -1
- package/dist/prompts.d.ts +0 -4
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js +0 -175
- package/dist/prompts.js.map +0 -1
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- /package/dist/{ports.d.ts → docker/ports.d.ts} +0 -0
- /package/dist/{ports.js → docker/ports.js} +0 -0
- /package/dist/{types.js → shared/types.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,610 +1,133 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
44
|
+
program.command("start").description("Start Seedar services").action(async () => {
|
|
45
|
+
await handlers.start();
|
|
112
46
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
stdio: "inherit",
|
|
47
|
+
program.command("stop").description("Stop Seedar services").action(async () => {
|
|
48
|
+
await handlers.stop();
|
|
116
49
|
});
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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
|