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.
Files changed (2) hide show
  1. package/lib/cli.js +211 -26
  2. 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 ensureCommandAvailable(command, helpText) {
108
- const result = spawnSync(command, ["--version"], { stdio: "ignore" });
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
- throw new Error(helpText);
145
+ return "";
111
146
  }
147
+ return (result.stdout || "").trim();
112
148
  }
113
149
 
114
- function ensureDockerAvailable() {
115
- ensureCommandAvailable(
116
- "docker",
117
- "Docker is required. Install Docker Desktop or another Docker-compatible runtime first."
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
- throw new Error("Docker Compose v2 is required.");
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
- throw new Error("Docker daemon is not reachable. Start Docker Desktop or the Docker service first.");
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
- ensureDockerAvailable();
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.0",
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/",