wattetheria 0.1.1 → 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
@@ -7,6 +7,7 @@ const { createInterface } = require("node:readline/promises");
7
7
 
8
8
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
9
9
  const PACKAGE_JSON = require(path.join(PACKAGE_ROOT, "package.json"));
10
+ const RELEASE_ENV_TEMPLATE = path.join(PACKAGE_ROOT, ".env.release.example");
10
11
  const DEFAULT_DEPLOY_DIR = path.join(os.homedir(), ".wattetheria", "deploy");
11
12
  const DEFAULT_PROJECT_NAME = "wattetheria";
12
13
  const DEFAULT_COMMAND = "install";
@@ -22,6 +23,10 @@ const DOCKER_INSTALL_URLS = {
22
23
  win32: "https://www.docker.com/products/docker-desktop/",
23
24
  linux: "https://docs.docker.com/engine/install/"
24
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
+ ];
25
30
 
26
31
  function printHelp() {
27
32
  console.log(`Wattetheria CLI ${PACKAGE_JSON.version}
@@ -60,7 +65,7 @@ function parseArgs(argv) {
60
65
  options: {
61
66
  dir: DEFAULT_DEPLOY_DIR,
62
67
  projectName: DEFAULT_PROJECT_NAME,
63
- tag: PACKAGE_JSON.version,
68
+ tag: null,
64
69
  force: false,
65
70
  healthChecks: true,
66
71
  volumes: false,
@@ -80,7 +85,7 @@ function parseArgs(argv) {
80
85
  const options = {
81
86
  dir: DEFAULT_DEPLOY_DIR,
82
87
  projectName: DEFAULT_PROJECT_NAME,
83
- tag: PACKAGE_JSON.version,
88
+ tag: null,
84
89
  force: false,
85
90
  healthChecks: true,
86
91
  volumes: false,
@@ -131,6 +136,23 @@ function getDockerInstallUrl() {
131
136
  return DOCKER_INSTALL_URLS[process.platform] || DOCKER_INSTALL_URLS.linux;
132
137
  }
133
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
+
134
156
  function getGitRevision() {
135
157
  if (typeof PACKAGE_JSON.gitHead === "string" && PACKAGE_JSON.gitHead.trim()) {
136
158
  return PACKAGE_JSON.gitHead.trim();
@@ -147,12 +169,40 @@ function getGitRevision() {
147
169
  return (result.stdout || "").trim();
148
170
  }
149
171
 
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
+
150
199
  function formatVersionString() {
151
200
  const revision = getGitRevision();
201
+ const releaseVersion = getDefaultReleaseVersion();
152
202
  if (revision) {
153
- return `Wattetheria ${PACKAGE_JSON.version} (${revision})`;
203
+ return `Wattetheria ${releaseVersion} (${revision})`;
154
204
  }
155
- return `Wattetheria ${PACKAGE_JSON.version}`;
205
+ return `Wattetheria ${releaseVersion}`;
156
206
  }
157
207
 
158
208
  function formatBanner() {
@@ -163,15 +213,27 @@ function formatDockerStatusMessage(status) {
163
213
  const installUrl = status.installUrl || getDockerInstallUrl();
164
214
  switch (status.code) {
165
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
+ }
166
223
  return [
167
224
  "Docker runtime not found.",
168
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
+ : "",
169
229
  `Download: ${installUrl}`
170
- ].join("\n");
230
+ ].filter(Boolean).join("\n");
171
231
  case "missing-compose":
172
232
  return [
173
233
  "Docker Compose v2 is required.",
174
- "Install or upgrade Docker Desktop so `docker compose` is available.",
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.",
175
237
  `Help: ${installUrl}`
176
238
  ].join("\n");
177
239
  case "daemon-unreachable":
@@ -186,8 +248,8 @@ function formatDockerStatusMessage(status) {
186
248
 
187
249
  function getDockerStatus() {
188
250
  const installUrl = getDockerInstallUrl();
189
- const docker = spawnSync("docker", ["--version"], { stdio: "ignore" });
190
- if (docker.error || docker.status !== 0) {
251
+ const dockerCommand = resolveDockerCommand();
252
+ if (!dockerCommand) {
191
253
  return {
192
254
  ready: false,
193
255
  code: "missing-docker",
@@ -195,28 +257,31 @@ function getDockerStatus() {
195
257
  };
196
258
  }
197
259
 
198
- const compose = spawnSync("docker", ["compose", "version"], {
260
+ const compose = spawnSync(dockerCommand, ["compose", "version"], {
199
261
  stdio: "ignore"
200
262
  });
201
263
  if (compose.error || compose.status !== 0) {
202
264
  return {
203
265
  ready: false,
204
266
  code: "missing-compose",
205
- installUrl
267
+ installUrl,
268
+ dockerCommand
206
269
  };
207
270
  }
208
271
 
209
- const info = spawnSync("docker", ["info"], { stdio: "ignore" });
272
+ const info = spawnSync(dockerCommand, ["info"], { stdio: "ignore" });
210
273
  if (info.error || info.status !== 0) {
211
274
  return {
212
275
  ready: false,
213
276
  code: "daemon-unreachable",
214
- installUrl
277
+ installUrl,
278
+ dockerCommand
215
279
  };
216
280
  }
217
281
 
218
282
  return {
219
- ready: true
283
+ ready: true,
284
+ dockerCommand
220
285
  };
221
286
  }
222
287
 
@@ -255,7 +320,7 @@ async function promptForDockerSetup(status) {
255
320
  console.log("Wattetheria needs Docker before it can install the local stack.");
256
321
  console.log(formatDockerStatusMessage(status));
257
322
  console.log("");
258
- console.log("1. Open Docker install page");
323
+ console.log("1. Open runtime install guide");
259
324
  console.log("2. Retry Docker check");
260
325
  console.log("3. Cancel");
261
326
 
@@ -283,7 +348,7 @@ async function ensureDockerAvailable(options = {}) {
283
348
  while (true) {
284
349
  const status = getDockerStatus();
285
350
  if (status.ready) {
286
- return;
351
+ return status;
287
352
  }
288
353
  if (!interactive || !isInteractiveTerminal()) {
289
354
  throw new Error(formatDockerStatusMessage(status));
@@ -383,8 +448,9 @@ function randomPassword() {
383
448
  }
384
449
 
385
450
  function runCompose(options, args, capture = false) {
451
+ const dockerCommand = resolveDockerCommand() || "docker";
386
452
  const result = spawnSync(
387
- "docker",
453
+ dockerCommand,
388
454
  [
389
455
  "compose",
390
456
  "--project-name",
@@ -404,6 +470,16 @@ function runCompose(options, args, capture = false) {
404
470
  throw result.error;
405
471
  }
406
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
+ }
407
483
  if (capture && result.stderr) {
408
484
  throw new Error(result.stderr.trim() || `docker compose ${args.join(" ")} failed`);
409
485
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wattetheria",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Wattetheria deployment CLI",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",