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.
- package/.env.release.example +5 -5
- package/lib/cli.js +92 -16
- package/package.json +1 -1
package/.env.release.example
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Coordinated release image set
|
|
2
|
-
WATTETHERIA_KERNEL_IMAGE=ghcr.io/wattetheria/wattetheria-kernel:
|
|
3
|
-
WATTETHERIA_OBSERVATORY_IMAGE=ghcr.io/wattetheria/wattetheria-observatory:
|
|
4
|
-
WATTSWARM_KERNEL_IMAGE=ghcr.io/wattetheria/wattswarm-kernel:
|
|
5
|
-
WATTSWARM_RUNTIME_IMAGE=ghcr.io/wattetheria/wattswarm-runtime:
|
|
6
|
-
WATTSWARM_WORKER_IMAGE=ghcr.io/wattetheria/wattswarm-worker:
|
|
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:
|
|
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:
|
|
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 ${
|
|
203
|
+
return `Wattetheria ${releaseVersion} (${revision})`;
|
|
154
204
|
}
|
|
155
|
-
return `Wattetheria ${
|
|
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
|
-
|
|
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
|
|
190
|
-
if (
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
}
|