wattetheria 0.1.0 → 0.1.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.
@@ -1,9 +1,9 @@
1
1
  # Coordinated release image set
2
- WATTETHERIA_KERNEL_IMAGE=ghcr.io/wattetheria/wattetheria-kernel:0.1.0
3
- WATTETHERIA_OBSERVATORY_IMAGE=ghcr.io/wattetheria/wattetheria-observatory:0.1.0
4
- WATTSWARM_KERNEL_IMAGE=ghcr.io/wattetheria/wattswarm-kernel:0.1.0
5
- WATTSWARM_RUNTIME_IMAGE=ghcr.io/wattetheria/wattswarm-runtime:0.1.0
6
- WATTSWARM_WORKER_IMAGE=ghcr.io/wattetheria/wattswarm-worker:0.1.0
2
+ WATTETHERIA_KERNEL_IMAGE=ghcr.io/wattetheria/wattetheria-kernel:1.0.0
3
+ WATTETHERIA_OBSERVATORY_IMAGE=ghcr.io/wattetheria/wattetheria-observatory:1.0.0
4
+ WATTSWARM_KERNEL_IMAGE=ghcr.io/wattetheria/wattswarm-kernel:1.0.0
5
+ WATTSWARM_RUNTIME_IMAGE=ghcr.io/wattetheria/wattswarm-runtime:1.0.0
6
+ WATTSWARM_WORKER_IMAGE=ghcr.io/wattetheria/wattswarm-worker:1.0.0
7
7
 
8
8
  # Host bindings
9
9
  WATTETHERIA_CONTROL_PLANE_BIND_HOST=127.0.0.1
package/lib/cli.js CHANGED
@@ -3,9 +3,11 @@ 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"));
10
+ const RELEASE_ENV_TEMPLATE = path.join(PACKAGE_ROOT, ".env.release.example");
9
11
  const DEFAULT_DEPLOY_DIR = path.join(os.homedir(), ".wattetheria", "deploy");
10
12
  const DEFAULT_PROJECT_NAME = "wattetheria";
11
13
  const DEFAULT_COMMAND = "install";
@@ -16,6 +18,15 @@ const IMAGE_KEYS = [
16
18
  "WATTSWARM_RUNTIME_IMAGE",
17
19
  "WATTSWARM_WORKER_IMAGE"
18
20
  ];
21
+ const DOCKER_INSTALL_URLS = {
22
+ darwin: "https://www.docker.com/products/docker-desktop/",
23
+ win32: "https://www.docker.com/products/docker-desktop/",
24
+ linux: "https://docs.docker.com/engine/install/"
25
+ };
26
+ const WINDOWS_DOCKER_CANDIDATES = [
27
+ "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe",
28
+ "C:\\Program Files\\Docker\\cli-plugins\\docker.exe"
29
+ ];
19
30
 
20
31
  function printHelp() {
21
32
  console.log(`Wattetheria CLI ${PACKAGE_JSON.version}
@@ -36,6 +47,7 @@ Commands:
36
47
  help Show this help
37
48
 
38
49
  Options:
50
+ --version Show CLI version
39
51
  --dir <path> Deployment directory (default: ${DEFAULT_DEPLOY_DIR})
40
52
  --project-name <name> Docker compose project name (default: ${DEFAULT_PROJECT_NAME})
41
53
  --tag <tag> Override all release image tags
@@ -47,6 +59,22 @@ Options:
47
59
  }
48
60
 
49
61
  function parseArgs(argv) {
62
+ if (argv[0] === "--version" || argv[0] === "-v") {
63
+ return {
64
+ command: "version",
65
+ options: {
66
+ dir: DEFAULT_DEPLOY_DIR,
67
+ projectName: DEFAULT_PROJECT_NAME,
68
+ tag: null,
69
+ force: false,
70
+ healthChecks: true,
71
+ volumes: false,
72
+ purge: false,
73
+ composeArgs: []
74
+ }
75
+ };
76
+ }
77
+
50
78
  let command = DEFAULT_COMMAND;
51
79
  let index = 0;
52
80
  if (argv[0] && !argv[0].startsWith("-")) {
@@ -57,7 +85,7 @@ function parseArgs(argv) {
57
85
  const options = {
58
86
  dir: DEFAULT_DEPLOY_DIR,
59
87
  projectName: DEFAULT_PROJECT_NAME,
60
- tag: PACKAGE_JSON.version,
88
+ tag: null,
61
89
  force: false,
62
90
  healthChecks: true,
63
91
  volumes: false,
@@ -104,29 +132,228 @@ function requireValue(flag, value) {
104
132
  return value;
105
133
  }
106
134
 
107
- function ensureCommandAvailable(command, helpText) {
108
- const result = spawnSync(command, ["--version"], { stdio: "ignore" });
135
+ function getDockerInstallUrl() {
136
+ return DOCKER_INSTALL_URLS[process.platform] || DOCKER_INSTALL_URLS.linux;
137
+ }
138
+
139
+ function getDockerCandidates() {
140
+ if (process.platform === "win32") {
141
+ return ["docker", ...WINDOWS_DOCKER_CANDIDATES];
142
+ }
143
+ return ["docker"];
144
+ }
145
+
146
+ function resolveDockerCommand() {
147
+ for (const candidate of getDockerCandidates()) {
148
+ const result = spawnSync(candidate, ["--version"], { stdio: "ignore" });
149
+ if (!result.error && result.status === 0) {
150
+ return candidate;
151
+ }
152
+ }
153
+ return "";
154
+ }
155
+
156
+ function getGitRevision() {
157
+ if (typeof PACKAGE_JSON.gitHead === "string" && PACKAGE_JSON.gitHead.trim()) {
158
+ return PACKAGE_JSON.gitHead.trim();
159
+ }
160
+
161
+ const result = spawnSync("git", ["rev-parse", "--short=7", "HEAD"], {
162
+ cwd: PACKAGE_ROOT,
163
+ stdio: "pipe",
164
+ encoding: "utf8"
165
+ });
109
166
  if (result.error || result.status !== 0) {
110
- throw new Error(helpText);
167
+ return "";
111
168
  }
169
+ return (result.stdout || "").trim();
112
170
  }
113
171
 
114
- function ensureDockerAvailable() {
115
- ensureCommandAvailable(
116
- "docker",
117
- "Docker is required. Install Docker Desktop or another Docker-compatible runtime first."
118
- );
172
+ function extractImageTag(imageRef) {
173
+ if (!imageRef) {
174
+ return "";
175
+ }
176
+ const lastColon = imageRef.lastIndexOf(":");
177
+ const lastSlash = imageRef.lastIndexOf("/");
178
+ if (lastColon <= lastSlash) {
179
+ return "";
180
+ }
181
+ return imageRef.slice(lastColon + 1).trim();
182
+ }
183
+
184
+ function getDefaultReleaseVersion() {
185
+ try {
186
+ const envMap = readEnvFile(RELEASE_ENV_TEMPLATE);
187
+ const tags = IMAGE_KEYS
188
+ .map((key) => extractImageTag(envMap.get(key)))
189
+ .filter(Boolean);
190
+ if (tags.length === IMAGE_KEYS.length && new Set(tags).size === 1) {
191
+ return tags[0];
192
+ }
193
+ } catch (error) {
194
+ // fall through to package version
195
+ }
196
+ return PACKAGE_JSON.version;
197
+ }
198
+
199
+ function formatVersionString() {
200
+ const revision = getGitRevision();
201
+ const releaseVersion = getDefaultReleaseVersion();
202
+ if (revision) {
203
+ return `Wattetheria ${releaseVersion} (${revision})`;
204
+ }
205
+ return `Wattetheria ${releaseVersion}`;
206
+ }
207
+
208
+ function formatBanner() {
209
+ return `${formatVersionString()} — Local agent runtime with swarm sync and external agent reach.`;
210
+ }
211
+
212
+ function formatDockerStatusMessage(status) {
213
+ const installUrl = status.installUrl || getDockerInstallUrl();
214
+ switch (status.code) {
215
+ case "missing-docker":
216
+ if (process.platform === "linux") {
217
+ return [
218
+ "Docker runtime not found.",
219
+ "Install Docker Engine or another Docker-compatible runtime, then run the command again.",
220
+ `Install guide: ${installUrl}`
221
+ ].join("\n");
222
+ }
223
+ return [
224
+ "Docker runtime not found.",
225
+ "Install Docker Desktop or another Docker-compatible runtime, then run the command again.",
226
+ process.platform === "win32"
227
+ ? "If you just installed Docker Desktop, open a new PowerShell window and retry."
228
+ : "",
229
+ `Download: ${installUrl}`
230
+ ].filter(Boolean).join("\n");
231
+ case "missing-compose":
232
+ return [
233
+ "Docker Compose v2 is required.",
234
+ process.platform === "linux"
235
+ ? "Install or upgrade Docker Engine/Compose so `docker compose` is available."
236
+ : "Install or upgrade Docker Desktop so `docker compose` is available.",
237
+ `Help: ${installUrl}`
238
+ ].join("\n");
239
+ case "daemon-unreachable":
240
+ return [
241
+ "Docker is installed but the daemon is not reachable.",
242
+ "Start Docker Desktop or your Docker service, wait until it is ready, then retry."
243
+ ].join("\n");
244
+ default:
245
+ return "Docker runtime check failed.";
246
+ }
247
+ }
248
+
249
+ function getDockerStatus() {
250
+ const installUrl = getDockerInstallUrl();
251
+ const dockerCommand = resolveDockerCommand();
252
+ if (!dockerCommand) {
253
+ return {
254
+ ready: false,
255
+ code: "missing-docker",
256
+ installUrl
257
+ };
258
+ }
119
259
 
120
- const compose = spawnSync("docker", ["compose", "version"], {
260
+ const compose = spawnSync(dockerCommand, ["compose", "version"], {
121
261
  stdio: "ignore"
122
262
  });
123
263
  if (compose.error || compose.status !== 0) {
124
- throw new Error("Docker Compose v2 is required.");
264
+ return {
265
+ ready: false,
266
+ code: "missing-compose",
267
+ installUrl,
268
+ dockerCommand
269
+ };
125
270
  }
126
271
 
127
- const info = spawnSync("docker", ["info"], { stdio: "ignore" });
272
+ const info = spawnSync(dockerCommand, ["info"], { stdio: "ignore" });
128
273
  if (info.error || info.status !== 0) {
129
- throw new Error("Docker daemon is not reachable. Start Docker Desktop or the Docker service first.");
274
+ return {
275
+ ready: false,
276
+ code: "daemon-unreachable",
277
+ installUrl,
278
+ dockerCommand
279
+ };
280
+ }
281
+
282
+ return {
283
+ ready: true,
284
+ dockerCommand
285
+ };
286
+ }
287
+
288
+ function isInteractiveTerminal() {
289
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
290
+ }
291
+
292
+ function openUrl(url) {
293
+ let command;
294
+ let args;
295
+ if (process.platform === "darwin") {
296
+ command = "open";
297
+ args = [url];
298
+ } else if (process.platform === "win32") {
299
+ command = "cmd";
300
+ args = ["/c", "start", "", url];
301
+ } else {
302
+ command = "xdg-open";
303
+ args = [url];
304
+ }
305
+
306
+ const result = spawnSync(command, args, { stdio: "ignore" });
307
+ return !result.error && result.status === 0;
308
+ }
309
+
310
+ async function promptForDockerSetup(status) {
311
+ const installUrl = status.installUrl || getDockerInstallUrl();
312
+ const rl = createInterface({
313
+ input: process.stdin,
314
+ output: process.stdout
315
+ });
316
+
317
+ try {
318
+ while (true) {
319
+ console.log("");
320
+ console.log("Wattetheria needs Docker before it can install the local stack.");
321
+ console.log(formatDockerStatusMessage(status));
322
+ console.log("");
323
+ console.log("1. Open runtime install guide");
324
+ console.log("2. Retry Docker check");
325
+ console.log("3. Cancel");
326
+
327
+ const answer = (await rl.question("Select an option [1-3]: ")).trim();
328
+ if (answer === "1") {
329
+ const opened = openUrl(installUrl);
330
+ console.log(opened ? "Opened Docker install page." : `Open this URL in your browser: ${installUrl}`);
331
+ continue;
332
+ }
333
+ if (answer === "2") {
334
+ return;
335
+ }
336
+ if (answer === "3" || answer === "") {
337
+ throw new Error(formatDockerStatusMessage(status));
338
+ }
339
+ console.log("Please choose 1, 2, or 3.");
340
+ }
341
+ } finally {
342
+ rl.close();
343
+ }
344
+ }
345
+
346
+ async function ensureDockerAvailable(options = {}) {
347
+ const interactive = Boolean(options.interactive);
348
+ while (true) {
349
+ const status = getDockerStatus();
350
+ if (status.ready) {
351
+ return status;
352
+ }
353
+ if (!interactive || !isInteractiveTerminal()) {
354
+ throw new Error(formatDockerStatusMessage(status));
355
+ }
356
+ await promptForDockerSetup(status);
130
357
  }
131
358
  }
132
359
 
@@ -221,8 +448,9 @@ function randomPassword() {
221
448
  }
222
449
 
223
450
  function runCompose(options, args, capture = false) {
451
+ const dockerCommand = resolveDockerCommand() || "docker";
224
452
  const result = spawnSync(
225
- "docker",
453
+ dockerCommand,
226
454
  [
227
455
  "compose",
228
456
  "--project-name",
@@ -242,6 +470,16 @@ function runCompose(options, args, capture = false) {
242
470
  throw result.error;
243
471
  }
244
472
  if (result.status !== 0) {
473
+ const stderr = capture ? (result.stderr || "").trim() : "";
474
+ if (stderr.includes("failed to resolve reference") && stderr.includes(": not found")) {
475
+ throw new Error(
476
+ [
477
+ "One or more release images were not found in the container registry.",
478
+ "This usually means the requested image tag has not been published yet.",
479
+ "Publish the matching GHCR images first, or run the command with --tag <published-tag>."
480
+ ].join("\n")
481
+ );
482
+ }
245
483
  if (capture && result.stderr) {
246
484
  throw new Error(result.stderr.trim() || `docker compose ${args.join(" ")} failed`);
247
485
  }
@@ -303,7 +541,7 @@ function printSummary(options) {
303
541
  }
304
542
 
305
543
  async function install(options) {
306
- ensureDockerAvailable();
544
+ await ensureDockerAvailable({ interactive: true });
307
545
  ensureDeploymentAssets(options);
308
546
  runCompose(options, ["config"], true);
309
547
  console.log("Pulling release images...");
@@ -317,7 +555,7 @@ async function install(options) {
317
555
  }
318
556
 
319
557
  async function start(options) {
320
- ensureDockerAvailable();
558
+ await ensureDockerAvailable();
321
559
  if (!fs.existsSync(composeFilePath(options)) || !fs.existsSync(envFilePath(options))) {
322
560
  throw new Error("Deployment is not initialized. Run install first.");
323
561
  }
@@ -328,13 +566,13 @@ async function start(options) {
328
566
  printSummary(options);
329
567
  }
330
568
 
331
- function status(options) {
332
- ensureDockerAvailable();
569
+ async function status(options) {
570
+ await ensureDockerAvailable();
333
571
  runCompose(options, ["ps"]);
334
572
  }
335
573
 
336
574
  async function update(options) {
337
- ensureDockerAvailable();
575
+ await ensureDockerAvailable();
338
576
  if (!fs.existsSync(composeFilePath(options)) || !fs.existsSync(envFilePath(options))) {
339
577
  throw new Error("Deployment is not initialized. Run install first.");
340
578
  }
@@ -349,13 +587,13 @@ async function update(options) {
349
587
  printSummary(options);
350
588
  }
351
589
 
352
- function stop(options) {
353
- ensureDockerAvailable();
590
+ async function stop(options) {
591
+ await ensureDockerAvailable();
354
592
  runCompose(options, ["down"]);
355
593
  }
356
594
 
357
- function uninstall(options) {
358
- ensureDockerAvailable();
595
+ async function uninstall(options) {
596
+ await ensureDockerAvailable();
359
597
  const args = ["down"];
360
598
  if (options.volumes) {
361
599
  args.push("-v");
@@ -367,20 +605,40 @@ function uninstall(options) {
367
605
  }
368
606
  }
369
607
 
370
- function logs(options) {
371
- ensureDockerAvailable();
608
+ async function logs(options) {
609
+ await ensureDockerAvailable();
372
610
  runCompose(options, ["logs", ...options.composeArgs]);
373
611
  }
374
612
 
375
613
  function doctor() {
376
- ensureDockerAvailable();
614
+ const status = getDockerStatus();
615
+ if (!status.ready) {
616
+ throw new Error(formatDockerStatusMessage(status));
617
+ }
377
618
  console.log("Docker runtime is available.");
378
619
  console.log(`Node.js ${process.version} is available.`);
379
620
  }
380
621
 
622
+ function printVersion() {
623
+ console.log(formatVersionString());
624
+ }
625
+
626
+ function shouldPrintBanner(command) {
627
+ return !["help", "--help", "-h", "version"].includes(command);
628
+ }
629
+
630
+ function printBanner() {
631
+ console.log(formatBanner());
632
+ console.log("");
633
+ }
634
+
381
635
  async function run(argv) {
382
636
  const { command, options } = parseArgs(argv);
383
637
 
638
+ if (shouldPrintBanner(command)) {
639
+ printBanner();
640
+ }
641
+
384
642
  switch (command) {
385
643
  case "install":
386
644
  await install(options);
@@ -390,24 +648,27 @@ async function run(argv) {
390
648
  await start(options);
391
649
  return;
392
650
  case "status":
393
- status(options);
651
+ await status(options);
394
652
  return;
395
653
  case "update":
396
654
  await update(options);
397
655
  return;
398
656
  case "stop":
399
657
  case "down":
400
- stop(options);
658
+ await stop(options);
401
659
  return;
402
660
  case "uninstall":
403
- uninstall(options);
661
+ await uninstall(options);
404
662
  return;
405
663
  case "logs":
406
- logs(options);
664
+ await logs(options);
407
665
  return;
408
666
  case "doctor":
409
667
  doctor();
410
668
  return;
669
+ case "version":
670
+ printVersion();
671
+ return;
411
672
  case "help":
412
673
  case "--help":
413
674
  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.2",
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/",