wattetheria 0.1.0 → 0.1.1
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/lib/cli.js +211 -26
- package/package.json +2 -4
package/lib/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require("node:fs");
|
|
|
3
3
|
const os = require("node:os");
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const { spawnSync } = require("node:child_process");
|
|
6
|
+
const { createInterface } = require("node:readline/promises");
|
|
6
7
|
|
|
7
8
|
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
8
9
|
const PACKAGE_JSON = require(path.join(PACKAGE_ROOT, "package.json"));
|
|
@@ -16,6 +17,11 @@ const IMAGE_KEYS = [
|
|
|
16
17
|
"WATTSWARM_RUNTIME_IMAGE",
|
|
17
18
|
"WATTSWARM_WORKER_IMAGE"
|
|
18
19
|
];
|
|
20
|
+
const DOCKER_INSTALL_URLS = {
|
|
21
|
+
darwin: "https://www.docker.com/products/docker-desktop/",
|
|
22
|
+
win32: "https://www.docker.com/products/docker-desktop/",
|
|
23
|
+
linux: "https://docs.docker.com/engine/install/"
|
|
24
|
+
};
|
|
19
25
|
|
|
20
26
|
function printHelp() {
|
|
21
27
|
console.log(`Wattetheria CLI ${PACKAGE_JSON.version}
|
|
@@ -36,6 +42,7 @@ Commands:
|
|
|
36
42
|
help Show this help
|
|
37
43
|
|
|
38
44
|
Options:
|
|
45
|
+
--version Show CLI version
|
|
39
46
|
--dir <path> Deployment directory (default: ${DEFAULT_DEPLOY_DIR})
|
|
40
47
|
--project-name <name> Docker compose project name (default: ${DEFAULT_PROJECT_NAME})
|
|
41
48
|
--tag <tag> Override all release image tags
|
|
@@ -47,6 +54,22 @@ Options:
|
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
function parseArgs(argv) {
|
|
57
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
58
|
+
return {
|
|
59
|
+
command: "version",
|
|
60
|
+
options: {
|
|
61
|
+
dir: DEFAULT_DEPLOY_DIR,
|
|
62
|
+
projectName: DEFAULT_PROJECT_NAME,
|
|
63
|
+
tag: PACKAGE_JSON.version,
|
|
64
|
+
force: false,
|
|
65
|
+
healthChecks: true,
|
|
66
|
+
volumes: false,
|
|
67
|
+
purge: false,
|
|
68
|
+
composeArgs: []
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
let command = DEFAULT_COMMAND;
|
|
51
74
|
let index = 0;
|
|
52
75
|
if (argv[0] && !argv[0].startsWith("-")) {
|
|
@@ -104,29 +127,168 @@ function requireValue(flag, value) {
|
|
|
104
127
|
return value;
|
|
105
128
|
}
|
|
106
129
|
|
|
107
|
-
function
|
|
108
|
-
|
|
130
|
+
function getDockerInstallUrl() {
|
|
131
|
+
return DOCKER_INSTALL_URLS[process.platform] || DOCKER_INSTALL_URLS.linux;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getGitRevision() {
|
|
135
|
+
if (typeof PACKAGE_JSON.gitHead === "string" && PACKAGE_JSON.gitHead.trim()) {
|
|
136
|
+
return PACKAGE_JSON.gitHead.trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const result = spawnSync("git", ["rev-parse", "--short=7", "HEAD"], {
|
|
140
|
+
cwd: PACKAGE_ROOT,
|
|
141
|
+
stdio: "pipe",
|
|
142
|
+
encoding: "utf8"
|
|
143
|
+
});
|
|
109
144
|
if (result.error || result.status !== 0) {
|
|
110
|
-
|
|
145
|
+
return "";
|
|
111
146
|
}
|
|
147
|
+
return (result.stdout || "").trim();
|
|
112
148
|
}
|
|
113
149
|
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
150
|
+
function formatVersionString() {
|
|
151
|
+
const revision = getGitRevision();
|
|
152
|
+
if (revision) {
|
|
153
|
+
return `Wattetheria ${PACKAGE_JSON.version} (${revision})`;
|
|
154
|
+
}
|
|
155
|
+
return `Wattetheria ${PACKAGE_JSON.version}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function formatBanner() {
|
|
159
|
+
return `${formatVersionString()} — Local agent runtime with swarm sync and external agent reach.`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatDockerStatusMessage(status) {
|
|
163
|
+
const installUrl = status.installUrl || getDockerInstallUrl();
|
|
164
|
+
switch (status.code) {
|
|
165
|
+
case "missing-docker":
|
|
166
|
+
return [
|
|
167
|
+
"Docker runtime not found.",
|
|
168
|
+
"Install Docker Desktop or another Docker-compatible runtime, then run the command again.",
|
|
169
|
+
`Download: ${installUrl}`
|
|
170
|
+
].join("\n");
|
|
171
|
+
case "missing-compose":
|
|
172
|
+
return [
|
|
173
|
+
"Docker Compose v2 is required.",
|
|
174
|
+
"Install or upgrade Docker Desktop so `docker compose` is available.",
|
|
175
|
+
`Help: ${installUrl}`
|
|
176
|
+
].join("\n");
|
|
177
|
+
case "daemon-unreachable":
|
|
178
|
+
return [
|
|
179
|
+
"Docker is installed but the daemon is not reachable.",
|
|
180
|
+
"Start Docker Desktop or your Docker service, wait until it is ready, then retry."
|
|
181
|
+
].join("\n");
|
|
182
|
+
default:
|
|
183
|
+
return "Docker runtime check failed.";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getDockerStatus() {
|
|
188
|
+
const installUrl = getDockerInstallUrl();
|
|
189
|
+
const docker = spawnSync("docker", ["--version"], { stdio: "ignore" });
|
|
190
|
+
if (docker.error || docker.status !== 0) {
|
|
191
|
+
return {
|
|
192
|
+
ready: false,
|
|
193
|
+
code: "missing-docker",
|
|
194
|
+
installUrl
|
|
195
|
+
};
|
|
196
|
+
}
|
|
119
197
|
|
|
120
198
|
const compose = spawnSync("docker", ["compose", "version"], {
|
|
121
199
|
stdio: "ignore"
|
|
122
200
|
});
|
|
123
201
|
if (compose.error || compose.status !== 0) {
|
|
124
|
-
|
|
202
|
+
return {
|
|
203
|
+
ready: false,
|
|
204
|
+
code: "missing-compose",
|
|
205
|
+
installUrl
|
|
206
|
+
};
|
|
125
207
|
}
|
|
126
208
|
|
|
127
209
|
const info = spawnSync("docker", ["info"], { stdio: "ignore" });
|
|
128
210
|
if (info.error || info.status !== 0) {
|
|
129
|
-
|
|
211
|
+
return {
|
|
212
|
+
ready: false,
|
|
213
|
+
code: "daemon-unreachable",
|
|
214
|
+
installUrl
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
ready: true
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isInteractiveTerminal() {
|
|
224
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function openUrl(url) {
|
|
228
|
+
let command;
|
|
229
|
+
let args;
|
|
230
|
+
if (process.platform === "darwin") {
|
|
231
|
+
command = "open";
|
|
232
|
+
args = [url];
|
|
233
|
+
} else if (process.platform === "win32") {
|
|
234
|
+
command = "cmd";
|
|
235
|
+
args = ["/c", "start", "", url];
|
|
236
|
+
} else {
|
|
237
|
+
command = "xdg-open";
|
|
238
|
+
args = [url];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const result = spawnSync(command, args, { stdio: "ignore" });
|
|
242
|
+
return !result.error && result.status === 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function promptForDockerSetup(status) {
|
|
246
|
+
const installUrl = status.installUrl || getDockerInstallUrl();
|
|
247
|
+
const rl = createInterface({
|
|
248
|
+
input: process.stdin,
|
|
249
|
+
output: process.stdout
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
while (true) {
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log("Wattetheria needs Docker before it can install the local stack.");
|
|
256
|
+
console.log(formatDockerStatusMessage(status));
|
|
257
|
+
console.log("");
|
|
258
|
+
console.log("1. Open Docker install page");
|
|
259
|
+
console.log("2. Retry Docker check");
|
|
260
|
+
console.log("3. Cancel");
|
|
261
|
+
|
|
262
|
+
const answer = (await rl.question("Select an option [1-3]: ")).trim();
|
|
263
|
+
if (answer === "1") {
|
|
264
|
+
const opened = openUrl(installUrl);
|
|
265
|
+
console.log(opened ? "Opened Docker install page." : `Open this URL in your browser: ${installUrl}`);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (answer === "2") {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (answer === "3" || answer === "") {
|
|
272
|
+
throw new Error(formatDockerStatusMessage(status));
|
|
273
|
+
}
|
|
274
|
+
console.log("Please choose 1, 2, or 3.");
|
|
275
|
+
}
|
|
276
|
+
} finally {
|
|
277
|
+
rl.close();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function ensureDockerAvailable(options = {}) {
|
|
282
|
+
const interactive = Boolean(options.interactive);
|
|
283
|
+
while (true) {
|
|
284
|
+
const status = getDockerStatus();
|
|
285
|
+
if (status.ready) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (!interactive || !isInteractiveTerminal()) {
|
|
289
|
+
throw new Error(formatDockerStatusMessage(status));
|
|
290
|
+
}
|
|
291
|
+
await promptForDockerSetup(status);
|
|
130
292
|
}
|
|
131
293
|
}
|
|
132
294
|
|
|
@@ -303,7 +465,7 @@ function printSummary(options) {
|
|
|
303
465
|
}
|
|
304
466
|
|
|
305
467
|
async function install(options) {
|
|
306
|
-
ensureDockerAvailable();
|
|
468
|
+
await ensureDockerAvailable({ interactive: true });
|
|
307
469
|
ensureDeploymentAssets(options);
|
|
308
470
|
runCompose(options, ["config"], true);
|
|
309
471
|
console.log("Pulling release images...");
|
|
@@ -317,7 +479,7 @@ async function install(options) {
|
|
|
317
479
|
}
|
|
318
480
|
|
|
319
481
|
async function start(options) {
|
|
320
|
-
ensureDockerAvailable();
|
|
482
|
+
await ensureDockerAvailable();
|
|
321
483
|
if (!fs.existsSync(composeFilePath(options)) || !fs.existsSync(envFilePath(options))) {
|
|
322
484
|
throw new Error("Deployment is not initialized. Run install first.");
|
|
323
485
|
}
|
|
@@ -328,13 +490,13 @@ async function start(options) {
|
|
|
328
490
|
printSummary(options);
|
|
329
491
|
}
|
|
330
492
|
|
|
331
|
-
function status(options) {
|
|
332
|
-
ensureDockerAvailable();
|
|
493
|
+
async function status(options) {
|
|
494
|
+
await ensureDockerAvailable();
|
|
333
495
|
runCompose(options, ["ps"]);
|
|
334
496
|
}
|
|
335
497
|
|
|
336
498
|
async function update(options) {
|
|
337
|
-
ensureDockerAvailable();
|
|
499
|
+
await ensureDockerAvailable();
|
|
338
500
|
if (!fs.existsSync(composeFilePath(options)) || !fs.existsSync(envFilePath(options))) {
|
|
339
501
|
throw new Error("Deployment is not initialized. Run install first.");
|
|
340
502
|
}
|
|
@@ -349,13 +511,13 @@ async function update(options) {
|
|
|
349
511
|
printSummary(options);
|
|
350
512
|
}
|
|
351
513
|
|
|
352
|
-
function stop(options) {
|
|
353
|
-
ensureDockerAvailable();
|
|
514
|
+
async function stop(options) {
|
|
515
|
+
await ensureDockerAvailable();
|
|
354
516
|
runCompose(options, ["down"]);
|
|
355
517
|
}
|
|
356
518
|
|
|
357
|
-
function uninstall(options) {
|
|
358
|
-
ensureDockerAvailable();
|
|
519
|
+
async function uninstall(options) {
|
|
520
|
+
await ensureDockerAvailable();
|
|
359
521
|
const args = ["down"];
|
|
360
522
|
if (options.volumes) {
|
|
361
523
|
args.push("-v");
|
|
@@ -367,20 +529,40 @@ function uninstall(options) {
|
|
|
367
529
|
}
|
|
368
530
|
}
|
|
369
531
|
|
|
370
|
-
function logs(options) {
|
|
371
|
-
ensureDockerAvailable();
|
|
532
|
+
async function logs(options) {
|
|
533
|
+
await ensureDockerAvailable();
|
|
372
534
|
runCompose(options, ["logs", ...options.composeArgs]);
|
|
373
535
|
}
|
|
374
536
|
|
|
375
537
|
function doctor() {
|
|
376
|
-
|
|
538
|
+
const status = getDockerStatus();
|
|
539
|
+
if (!status.ready) {
|
|
540
|
+
throw new Error(formatDockerStatusMessage(status));
|
|
541
|
+
}
|
|
377
542
|
console.log("Docker runtime is available.");
|
|
378
543
|
console.log(`Node.js ${process.version} is available.`);
|
|
379
544
|
}
|
|
380
545
|
|
|
546
|
+
function printVersion() {
|
|
547
|
+
console.log(formatVersionString());
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function shouldPrintBanner(command) {
|
|
551
|
+
return !["help", "--help", "-h", "version"].includes(command);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function printBanner() {
|
|
555
|
+
console.log(formatBanner());
|
|
556
|
+
console.log("");
|
|
557
|
+
}
|
|
558
|
+
|
|
381
559
|
async function run(argv) {
|
|
382
560
|
const { command, options } = parseArgs(argv);
|
|
383
561
|
|
|
562
|
+
if (shouldPrintBanner(command)) {
|
|
563
|
+
printBanner();
|
|
564
|
+
}
|
|
565
|
+
|
|
384
566
|
switch (command) {
|
|
385
567
|
case "install":
|
|
386
568
|
await install(options);
|
|
@@ -390,24 +572,27 @@ async function run(argv) {
|
|
|
390
572
|
await start(options);
|
|
391
573
|
return;
|
|
392
574
|
case "status":
|
|
393
|
-
status(options);
|
|
575
|
+
await status(options);
|
|
394
576
|
return;
|
|
395
577
|
case "update":
|
|
396
578
|
await update(options);
|
|
397
579
|
return;
|
|
398
580
|
case "stop":
|
|
399
581
|
case "down":
|
|
400
|
-
stop(options);
|
|
582
|
+
await stop(options);
|
|
401
583
|
return;
|
|
402
584
|
case "uninstall":
|
|
403
|
-
uninstall(options);
|
|
585
|
+
await uninstall(options);
|
|
404
586
|
return;
|
|
405
587
|
case "logs":
|
|
406
|
-
logs(options);
|
|
588
|
+
await logs(options);
|
|
407
589
|
return;
|
|
408
590
|
case "doctor":
|
|
409
591
|
doctor();
|
|
410
592
|
return;
|
|
593
|
+
case "version":
|
|
594
|
+
printVersion();
|
|
595
|
+
return;
|
|
411
596
|
case "help":
|
|
412
597
|
case "--help":
|
|
413
598
|
case "-h":
|
package/package.json
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wattetheria",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Wattetheria deployment CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
7
|
-
"bin":
|
|
8
|
-
"wattetheria": "./bin/wattetheria.js"
|
|
9
|
-
},
|
|
7
|
+
"bin": "bin/wattetheria.js",
|
|
10
8
|
"files": [
|
|
11
9
|
"bin/",
|
|
12
10
|
"lib/",
|