wattetheria 0.1.5 → 0.1.6
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/README.md +6 -11
- package/lib/cli.js +320 -29
- package/package.json +1 -2
- package/.env.release +0 -58
package/README.md
CHANGED
|
@@ -148,18 +148,11 @@ Read the diagram in layers:
|
|
|
148
148
|
|
|
149
149
|
### Tasks, Oracle, And Mailbox
|
|
150
150
|
|
|
151
|
-
-
|
|
152
|
-
|
|
153
|
-
- claim
|
|
154
|
-
- execute
|
|
155
|
-
- submit
|
|
156
|
-
- verify
|
|
157
|
-
- settle
|
|
158
|
-
- `market.match` task path with deterministic and witness verification modes
|
|
159
|
-
- `swarm_bridge` adapter that maps current task execution into a `wattswarm`-oriented bridge surface
|
|
151
|
+
- Product-layer galaxy task definitions in `crates/kernel-core/src/tasks/galaxy_task.rs`
|
|
152
|
+
- `swarm_bridge` adapter for `wattswarm` topic, task/run read models, and peer/network surfaces
|
|
160
153
|
- Hybrid `swarm_bridge` path for `wattswarm` topic and network read models
|
|
161
154
|
- optional `--wattswarm-ui-base-url` wiring from CLI config into node runtime
|
|
162
|
-
- topic subscribe, post, history, cursor, network-status, and peer-list bridge calls
|
|
155
|
+
- topic subscribe, post, history, cursor, task/run snapshots, network-status, and peer-list bridge calls
|
|
163
156
|
- Oracle registry with signed feed publish, subscribe, pull, and watt-based settlement
|
|
164
157
|
- Cross-subnet mailbox with send, fetch, and ack persistence
|
|
165
158
|
|
|
@@ -568,6 +561,8 @@ Version commands:
|
|
|
568
561
|
- `npx wattetheria --version` shows the current Wattetheria release version
|
|
569
562
|
- `npx wattetheria version --images` prints the configured image refs for the current deployment
|
|
570
563
|
- `npx wattetheria version --cli` shows the deployment CLI package version
|
|
564
|
+
- `npx wattetheria update` resolves the latest shared published image tag across the configured release images and upgrades to it
|
|
565
|
+
- `npx wattetheria update --tag <tag>` pins the deployment to a specific published image tag
|
|
571
566
|
|
|
572
567
|
Release deployments bind-mount host-visible state by default:
|
|
573
568
|
|
|
@@ -597,7 +592,7 @@ pwsh ./scripts/deploy-release.ps1
|
|
|
597
592
|
- `docker-compose.yml` is the local `wattetheria`-only development stack
|
|
598
593
|
- `docker-compose.full.yml` is the local joint development stack for `wattetheria` + `wattswarm`
|
|
599
594
|
- `docker-compose.release.yml` is the image-based release deployment asset used by the CLI and fallback scripts
|
|
600
|
-
-
|
|
595
|
+
- the CLI now generates deployment environment defaults internally and resolves the latest published image release during install and update
|
|
601
596
|
- `scripts/deploy-release.ps1` is a cross-platform fallback deployment entry point
|
|
602
597
|
- this repository does not include `wattetheria-gateway`; gateway is a separate project and deployment unit
|
|
603
598
|
- Entrypoints live in `scripts/docker-kernel-entrypoint.sh` and `scripts/docker-observatory-entrypoint.sh`
|
package/lib/cli.js
CHANGED
|
@@ -7,10 +7,16 @@ 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");
|
|
11
10
|
const DEFAULT_DEPLOY_DIR = path.join(os.homedir(), ".wattetheria", "deploy");
|
|
12
11
|
const DEFAULT_PROJECT_NAME = "wattetheria";
|
|
13
12
|
const DEFAULT_COMMAND = "help";
|
|
13
|
+
const DEFAULT_IMAGE_REFS = new Map([
|
|
14
|
+
["WATTETHERIA_KERNEL_IMAGE", "ghcr.io/wattetheria/wattetheria-kernel:latest"],
|
|
15
|
+
["WATTETHERIA_OBSERVATORY_IMAGE", "ghcr.io/wattetheria/wattetheria-observatory:latest"],
|
|
16
|
+
["WATTSWARM_KERNEL_IMAGE", "ghcr.io/wattetheria/wattswarm-kernel:latest"],
|
|
17
|
+
["WATTSWARM_RUNTIME_IMAGE", "ghcr.io/wattetheria/wattswarm-runtime:latest"],
|
|
18
|
+
["WATTSWARM_WORKER_IMAGE", "ghcr.io/wattetheria/wattswarm-worker:latest"]
|
|
19
|
+
]);
|
|
14
20
|
const IMAGE_KEYS = [
|
|
15
21
|
"WATTETHERIA_KERNEL_IMAGE",
|
|
16
22
|
"WATTETHERIA_OBSERVATORY_IMAGE",
|
|
@@ -19,6 +25,48 @@ const IMAGE_KEYS = [
|
|
|
19
25
|
"WATTSWARM_WORKER_IMAGE"
|
|
20
26
|
];
|
|
21
27
|
const HOST_STATE_DIR_KEYS = ["WATTETHERIA_HOST_STATE_DIR", "WATTSWARM_HOST_STATE_DIR"];
|
|
28
|
+
const REGISTRY_TAG_PAGE_SIZE = 1000;
|
|
29
|
+
const DEFAULT_ENV_ENTRIES = [
|
|
30
|
+
...DEFAULT_IMAGE_REFS.entries(),
|
|
31
|
+
["WATTETHERIA_CONTROL_PLANE_BIND_HOST", "127.0.0.1"],
|
|
32
|
+
["WATTETHERIA_CONTROL_PLANE_PORT", "7777"],
|
|
33
|
+
["WATTETHERIA_OBSERVATORY_BIND_HOST", "127.0.0.1"],
|
|
34
|
+
["WATTETHERIA_OBSERVATORY_PORT", "8780"],
|
|
35
|
+
["WATTSWARM_UI_BIND_HOST", "127.0.0.1"],
|
|
36
|
+
["WATTSWARM_UI_PORT", "7788"],
|
|
37
|
+
["WATTSWARM_SYNC_GRPC_BIND_HOST", "127.0.0.1"],
|
|
38
|
+
["WATTSWARM_SYNC_GRPC_PORT", "7791"],
|
|
39
|
+
["WATTSWARM_P2P_HOST_PORT", "4001"],
|
|
40
|
+
["WATTSWARM_UDP_ANNOUNCE_HOST_PORT", "37931"],
|
|
41
|
+
["WATTETHERIA_HOST_STATE_DIR", "./data/wattetheria"],
|
|
42
|
+
["WATTSWARM_HOST_STATE_DIR", "./data/wattswarm"],
|
|
43
|
+
["WATTETHERIA_RUNTIME_ENV_FILE", ".env.release.local"],
|
|
44
|
+
["WATTETHERIA_AGENT_CONTROL_PLANE_ENDPOINT", "http://127.0.0.1:7777"],
|
|
45
|
+
["WATTETHERIA_AGENT_WATTSWARM_UI_BASE_URL", "http://127.0.0.1:7788"],
|
|
46
|
+
["WATTETHERIA_AGENT_WATTSWARM_SYNC_GRPC_ENDPOINT", "http://127.0.0.1:7791"],
|
|
47
|
+
["WATTETHERIA_AGENT_HOST_DATA_DIR", "./data/wattetheria"],
|
|
48
|
+
["WATTETHERIA_BRAIN_PROVIDER_KIND", "rules"],
|
|
49
|
+
["WATTETHERIA_BRAIN_BASE_URL", ""],
|
|
50
|
+
["WATTETHERIA_BRAIN_MODEL", ""],
|
|
51
|
+
["WATTETHERIA_BRAIN_API_KEY_ENV", ""],
|
|
52
|
+
["WATTETHERIA_SERVICENET_BASE_URL", ""],
|
|
53
|
+
["WATTETHERIA_AUTONOMY_ENABLED", "false"],
|
|
54
|
+
["WATTETHERIA_AUTONOMY_INTERVAL_SEC", "30"],
|
|
55
|
+
["OPENCLAW_API_KEY", ""],
|
|
56
|
+
["WATTSWARM_PG_DB", "wattswarm"],
|
|
57
|
+
["WATTSWARM_PG_USER", "postgres"],
|
|
58
|
+
["WATTSWARM_PG_PASSWORD", "replace-with-strong-password"],
|
|
59
|
+
["WATTSWARM_P2P_ENABLED", "true"],
|
|
60
|
+
["WATTSWARM_P2P_MDNS", "true"],
|
|
61
|
+
["WATTSWARM_P2P_PORT", "4001"],
|
|
62
|
+
["WATTSWARM_WORKER_CONCURRENCY", "16"],
|
|
63
|
+
["WATTSWARM_WORKER_POLL_MS", "250"],
|
|
64
|
+
["WATTSWARM_WORKER_LEASE_MS", "30000"],
|
|
65
|
+
["WATTSWARM_UDP_ANNOUNCE_ENABLED", "false"],
|
|
66
|
+
["WATTSWARM_UDP_ANNOUNCE_MODE", "multicast"],
|
|
67
|
+
["WATTSWARM_UDP_ANNOUNCE_ADDR", "239.255.42.99"],
|
|
68
|
+
["WATTSWARM_UDP_ANNOUNCE_PORT", "37931"]
|
|
69
|
+
];
|
|
22
70
|
const DOCKER_INSTALL_URLS = {
|
|
23
71
|
darwin: "https://www.docker.com/products/docker-desktop/",
|
|
24
72
|
win32: "https://www.docker.com/products/docker-desktop/",
|
|
@@ -42,7 +90,7 @@ Commands:
|
|
|
42
90
|
install Prepare deployment, pull images, and start the stack
|
|
43
91
|
start Start an existing deployment
|
|
44
92
|
status Show docker compose status
|
|
45
|
-
update
|
|
93
|
+
update Resolve latest published release, pull, and restart
|
|
46
94
|
stop Stop the deployment
|
|
47
95
|
uninstall Stop the deployment and optionally remove volumes
|
|
48
96
|
logs Show docker compose logs
|
|
@@ -56,7 +104,7 @@ Options:
|
|
|
56
104
|
--dir <path> Deployment directory (default: ${DEFAULT_DEPLOY_DIR})
|
|
57
105
|
--project-name <name> Docker compose project name (default: ${DEFAULT_PROJECT_NAME})
|
|
58
106
|
--tag <tag> Override all release image tags
|
|
59
|
-
--force Refresh deployment
|
|
107
|
+
--force Refresh deployment defaults and compose assets
|
|
60
108
|
--no-health-checks Skip HTTP health checks
|
|
61
109
|
--volumes With uninstall, remove named docker volumes
|
|
62
110
|
--purge With uninstall, remove the deployment directory
|
|
@@ -179,6 +227,193 @@ function extractImageTag(imageRef) {
|
|
|
179
227
|
return imageRef.slice(lastColon + 1).trim();
|
|
180
228
|
}
|
|
181
229
|
|
|
230
|
+
function stripImageTag(imageRef) {
|
|
231
|
+
if (!imageRef) {
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
const lastColon = imageRef.lastIndexOf(":");
|
|
235
|
+
const lastSlash = imageRef.lastIndexOf("/");
|
|
236
|
+
if (lastColon <= lastSlash) {
|
|
237
|
+
return imageRef.trim();
|
|
238
|
+
}
|
|
239
|
+
return imageRef.slice(0, lastColon).trim();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function parseImageReference(imageRef) {
|
|
243
|
+
const normalized = stripImageTag(imageRef);
|
|
244
|
+
if (!normalized) {
|
|
245
|
+
throw new Error(`Invalid image reference: ${imageRef}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const segments = normalized.split("/");
|
|
249
|
+
const first = segments[0];
|
|
250
|
+
const hasExplicitRegistry = first.includes(".") || first.includes(":") || first === "localhost";
|
|
251
|
+
const registry = hasExplicitRegistry ? first : "registry-1.docker.io";
|
|
252
|
+
const repositorySegments = hasExplicitRegistry ? segments.slice(1) : segments;
|
|
253
|
+
if (repositorySegments.length === 0) {
|
|
254
|
+
throw new Error(`Invalid image repository: ${imageRef}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!hasExplicitRegistry && repositorySegments.length === 1) {
|
|
258
|
+
repositorySegments.unshift("library");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
registry,
|
|
263
|
+
repository: repositorySegments.join("/")
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parseReleaseTag(tag) {
|
|
268
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec((tag || "").trim());
|
|
269
|
+
if (!match) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
raw: tag,
|
|
274
|
+
major: Number.parseInt(match[1], 10),
|
|
275
|
+
minor: Number.parseInt(match[2], 10),
|
|
276
|
+
patch: Number.parseInt(match[3], 10),
|
|
277
|
+
prerelease: match[4] || ""
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function compareReleaseTags(left, right) {
|
|
282
|
+
const a = parseReleaseTag(left);
|
|
283
|
+
const b = parseReleaseTag(right);
|
|
284
|
+
if (!a && !b) {
|
|
285
|
+
return String(left).localeCompare(String(right));
|
|
286
|
+
}
|
|
287
|
+
if (!a) {
|
|
288
|
+
return -1;
|
|
289
|
+
}
|
|
290
|
+
if (!b) {
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
294
|
+
if (a[key] !== b[key]) {
|
|
295
|
+
return a[key] - b[key];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!a.prerelease && b.prerelease) {
|
|
299
|
+
return 1;
|
|
300
|
+
}
|
|
301
|
+
if (a.prerelease && !b.prerelease) {
|
|
302
|
+
return -1;
|
|
303
|
+
}
|
|
304
|
+
return a.prerelease.localeCompare(b.prerelease);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function parseAuthChallenge(header) {
|
|
308
|
+
if (!header) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const match = /^Bearer\s+(.*)$/i.exec(header.trim());
|
|
312
|
+
if (!match) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
const params = new Map();
|
|
316
|
+
for (const [, key, value] of match[1].matchAll(/([a-zA-Z_]+)="([^"]*)"/g)) {
|
|
317
|
+
params.set(key, value);
|
|
318
|
+
}
|
|
319
|
+
const realm = params.get("realm");
|
|
320
|
+
if (!realm) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
realm,
|
|
325
|
+
service: params.get("service") || "",
|
|
326
|
+
scope: params.get("scope") || ""
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function fetchRegistryAccessToken(challenge) {
|
|
331
|
+
const url = new URL(challenge.realm);
|
|
332
|
+
if (challenge.service) {
|
|
333
|
+
url.searchParams.set("service", challenge.service);
|
|
334
|
+
}
|
|
335
|
+
if (challenge.scope) {
|
|
336
|
+
url.searchParams.set("scope", challenge.scope);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const response = await fetch(url, { method: "GET" });
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
throw new Error(`Failed to resolve registry token: ${response.status} ${response.statusText}`);
|
|
342
|
+
}
|
|
343
|
+
const payload = await response.json();
|
|
344
|
+
const token = payload.token || payload.access_token;
|
|
345
|
+
if (!token) {
|
|
346
|
+
throw new Error("Registry token response did not contain an access token.");
|
|
347
|
+
}
|
|
348
|
+
return token;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function fetchRegistryResponse(url, token = "") {
|
|
352
|
+
const headers = {
|
|
353
|
+
Accept: "application/json"
|
|
354
|
+
};
|
|
355
|
+
if (token) {
|
|
356
|
+
headers.Authorization = `Bearer ${token}`;
|
|
357
|
+
}
|
|
358
|
+
const response = await fetch(url, { method: "GET", headers });
|
|
359
|
+
if (response.status === 401 && !token) {
|
|
360
|
+
const challenge = parseAuthChallenge(response.headers.get("www-authenticate"));
|
|
361
|
+
if (!challenge) {
|
|
362
|
+
throw new Error(`Registry authentication challenge is missing for ${url}`);
|
|
363
|
+
}
|
|
364
|
+
const resolvedToken = await fetchRegistryAccessToken(challenge);
|
|
365
|
+
return fetchRegistryResponse(url, resolvedToken);
|
|
366
|
+
}
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
throw new Error(`Registry request failed for ${url}: ${response.status} ${response.statusText}`);
|
|
369
|
+
}
|
|
370
|
+
return response;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function resolveRegistryNextPage(linkHeader, currentUrl) {
|
|
374
|
+
if (!linkHeader) {
|
|
375
|
+
return "";
|
|
376
|
+
}
|
|
377
|
+
const match = /<([^>]+)>;\s*rel="next"/i.exec(linkHeader);
|
|
378
|
+
if (!match) {
|
|
379
|
+
return "";
|
|
380
|
+
}
|
|
381
|
+
return new URL(match[1], currentUrl).toString();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function listPublishedImageTags(imageRef) {
|
|
385
|
+
const { registry, repository } = parseImageReference(imageRef);
|
|
386
|
+
const collected = new Set();
|
|
387
|
+
let nextUrl = `https://${registry}/v2/${repository}/tags/list?n=${REGISTRY_TAG_PAGE_SIZE}`;
|
|
388
|
+
while (nextUrl) {
|
|
389
|
+
const response = await fetchRegistryResponse(nextUrl);
|
|
390
|
+
const payload = await response.json();
|
|
391
|
+
for (const tag of payload.tags || []) {
|
|
392
|
+
if (tag && tag.trim()) {
|
|
393
|
+
collected.add(tag.trim());
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
nextUrl = resolveRegistryNextPage(response.headers.get("link"), nextUrl);
|
|
397
|
+
}
|
|
398
|
+
return [...collected];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function findLatestCommonReleaseTag(tagLists) {
|
|
402
|
+
if (tagLists.length === 0) {
|
|
403
|
+
return "";
|
|
404
|
+
}
|
|
405
|
+
const [first, ...rest] = tagLists.map((tags) => new Set(tags));
|
|
406
|
+
const common = [...first].filter((tag) => rest.every((tags) => tags.has(tag)));
|
|
407
|
+
const releaseCandidates = common
|
|
408
|
+
.map((tag) => ({ tag, parsed: parseReleaseTag(tag) }))
|
|
409
|
+
.filter((entry) => entry.parsed);
|
|
410
|
+
const stableCandidates = releaseCandidates.filter((entry) => !entry.parsed.prerelease);
|
|
411
|
+
const ranked = (stableCandidates.length > 0 ? stableCandidates : releaseCandidates)
|
|
412
|
+
.map((entry) => entry.tag)
|
|
413
|
+
.sort(compareReleaseTags);
|
|
414
|
+
return ranked.at(-1) || "";
|
|
415
|
+
}
|
|
416
|
+
|
|
182
417
|
function resolveEnvReference(value, envMap, seen = new Set()) {
|
|
183
418
|
if (!value) {
|
|
184
419
|
return "";
|
|
@@ -210,6 +445,22 @@ function getReleaseImageMap(filePath) {
|
|
|
210
445
|
return images;
|
|
211
446
|
}
|
|
212
447
|
|
|
448
|
+
function getReleaseImageMapFromEnv(envMap) {
|
|
449
|
+
const images = new Map();
|
|
450
|
+
for (const key of IMAGE_KEYS) {
|
|
451
|
+
const rawValue = envMap.get(key);
|
|
452
|
+
if (!rawValue) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
images.set(key, resolveEnvReference(rawValue.trim(), envMap));
|
|
456
|
+
}
|
|
457
|
+
return images;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function defaultEnvMap() {
|
|
461
|
+
return new Map(DEFAULT_ENV_ENTRIES);
|
|
462
|
+
}
|
|
463
|
+
|
|
213
464
|
function getReleaseSource(options) {
|
|
214
465
|
const deployEnvPath = envFilePath(options);
|
|
215
466
|
if (fs.existsSync(deployEnvPath)) {
|
|
@@ -219,20 +470,24 @@ function getReleaseSource(options) {
|
|
|
219
470
|
};
|
|
220
471
|
}
|
|
221
472
|
return {
|
|
222
|
-
kind: "
|
|
223
|
-
path:
|
|
473
|
+
kind: "uninitialized",
|
|
474
|
+
path: deployEnvPath
|
|
224
475
|
};
|
|
225
476
|
}
|
|
226
477
|
|
|
227
478
|
function getReleaseVersionInfo(options) {
|
|
228
479
|
const source = getReleaseSource(options);
|
|
229
|
-
const images =
|
|
480
|
+
const images = source.kind === "deployment"
|
|
481
|
+
? getReleaseImageMap(source.path)
|
|
482
|
+
: getReleaseImageMapFromEnv(defaultEnvMap());
|
|
230
483
|
const tags = IMAGE_KEYS
|
|
231
484
|
.map((key) => extractImageTag(images.get(key)))
|
|
232
485
|
.filter(Boolean);
|
|
233
|
-
const version =
|
|
234
|
-
?
|
|
235
|
-
:
|
|
486
|
+
const version = source.kind === "uninitialized"
|
|
487
|
+
? "uninitialized"
|
|
488
|
+
: (tags.length === IMAGE_KEYS.length && new Set(tags).size === 1
|
|
489
|
+
? tags[0]
|
|
490
|
+
: "custom");
|
|
236
491
|
return {
|
|
237
492
|
version,
|
|
238
493
|
images,
|
|
@@ -412,15 +667,14 @@ async function ensureDockerAvailable(options = {}) {
|
|
|
412
667
|
function ensureDeploymentAssets(options) {
|
|
413
668
|
fs.mkdirSync(options.dir, { recursive: true });
|
|
414
669
|
|
|
415
|
-
const templateEnvPath = path.join(PACKAGE_ROOT, ".env.release");
|
|
416
670
|
const templateComposePath = path.join(PACKAGE_ROOT, "docker-compose.release.yml");
|
|
417
671
|
const targetEnvPath = envFilePath(options);
|
|
418
672
|
const targetComposePath = composeFilePath(options);
|
|
419
673
|
|
|
420
674
|
if (options.force || !fs.existsSync(targetEnvPath)) {
|
|
421
|
-
|
|
675
|
+
writeEnvFile(targetEnvPath, defaultEnvMap());
|
|
422
676
|
} else {
|
|
423
|
-
|
|
677
|
+
mergeNewDefaultKeys(targetEnvPath);
|
|
424
678
|
}
|
|
425
679
|
if (options.force || !fs.existsSync(targetComposePath)) {
|
|
426
680
|
fs.copyFileSync(templateComposePath, targetComposePath);
|
|
@@ -471,8 +725,8 @@ function writeEnvFile(filePath, envMap) {
|
|
|
471
725
|
fs.writeFileSync(filePath, `${lines.join("\n")}\n`);
|
|
472
726
|
}
|
|
473
727
|
|
|
474
|
-
function
|
|
475
|
-
const templateMap =
|
|
728
|
+
function mergeNewDefaultKeys(targetPath) {
|
|
729
|
+
const templateMap = defaultEnvMap();
|
|
476
730
|
const targetMap = readEnvFile(targetPath);
|
|
477
731
|
let added = 0;
|
|
478
732
|
for (const [key, value] of templateMap.entries()) {
|
|
@@ -508,32 +762,60 @@ function ensureHostStateDirectories(baseDir, envMap) {
|
|
|
508
762
|
}
|
|
509
763
|
}
|
|
510
764
|
|
|
511
|
-
function
|
|
765
|
+
async function syncImageTagsToLatestPublishedRelease(options) {
|
|
512
766
|
if (options.tag) {
|
|
513
|
-
return;
|
|
767
|
+
return options.tag;
|
|
514
768
|
}
|
|
769
|
+
|
|
515
770
|
const targetPath = envFilePath(options);
|
|
516
|
-
const templateMap = readEnvFile(RELEASE_ENV_TEMPLATE);
|
|
517
771
|
const targetMap = readEnvFile(targetPath);
|
|
772
|
+
const images = getReleaseImageMapFromEnv(targetMap);
|
|
773
|
+
const missing = IMAGE_KEYS.filter((key) => !images.get(key));
|
|
774
|
+
if (missing.length > 0) {
|
|
775
|
+
throw new Error(`Release image refs are missing from deployment env: ${missing.join(", ")}`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const tagLists = [];
|
|
779
|
+
for (const key of IMAGE_KEYS) {
|
|
780
|
+
const imageRef = images.get(key);
|
|
781
|
+
const tags = await listPublishedImageTags(imageRef);
|
|
782
|
+
if (tags.length === 0) {
|
|
783
|
+
throw new Error(`No published tags found for ${stripImageTag(imageRef)}`);
|
|
784
|
+
}
|
|
785
|
+
tagLists.push(tags);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const latestTag = findLatestCommonReleaseTag(tagLists);
|
|
789
|
+
if (!latestTag) {
|
|
790
|
+
throw new Error(
|
|
791
|
+
"Could not find a shared published release tag across all configured images."
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const currentTag = targetMap.get("RELEASE_TAG") || "";
|
|
796
|
+
if (currentTag.trim() !== latestTag) {
|
|
797
|
+
targetMap.set("RELEASE_TAG", latestTag);
|
|
798
|
+
}
|
|
799
|
+
|
|
518
800
|
let changed = false;
|
|
519
|
-
for (const key of
|
|
520
|
-
const
|
|
521
|
-
if (!
|
|
801
|
+
for (const key of IMAGE_KEYS) {
|
|
802
|
+
const current = targetMap.get(key);
|
|
803
|
+
if (!current) {
|
|
522
804
|
continue;
|
|
523
805
|
}
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (resolved !== currentResolved) {
|
|
528
|
-
targetMap.set(key, templateValue);
|
|
806
|
+
const next = `${stripImageTag(resolveEnvReference(current.trim(), targetMap))}:${latestTag}`;
|
|
807
|
+
if (resolveEnvReference(current.trim(), targetMap) !== next) {
|
|
808
|
+
targetMap.set(key, next);
|
|
529
809
|
changed = true;
|
|
530
810
|
}
|
|
531
811
|
}
|
|
532
|
-
if (changed) {
|
|
812
|
+
if (changed || currentTag.trim() !== latestTag) {
|
|
533
813
|
writeEnvFile(targetPath, targetMap);
|
|
534
|
-
|
|
535
|
-
|
|
814
|
+
console.log(`Resolved latest published release tag ${latestTag}.`);
|
|
815
|
+
} else {
|
|
816
|
+
console.log(`Release images are already pinned to latest published tag ${latestTag}.`);
|
|
536
817
|
}
|
|
818
|
+
return latestTag;
|
|
537
819
|
}
|
|
538
820
|
|
|
539
821
|
function pinImageTags(envMap, tag) {
|
|
@@ -656,6 +938,11 @@ function printSummary(options) {
|
|
|
656
938
|
async function install(options) {
|
|
657
939
|
await ensureDockerAvailable({ interactive: true });
|
|
658
940
|
ensureDeploymentAssets(options);
|
|
941
|
+
if (options.tag) {
|
|
942
|
+
console.log(`Pinning release images to requested tag ${options.tag}.`);
|
|
943
|
+
} else {
|
|
944
|
+
await syncImageTagsToLatestPublishedRelease(options);
|
|
945
|
+
}
|
|
659
946
|
runCompose(options, ["config"], true);
|
|
660
947
|
console.log("Pulling release images...");
|
|
661
948
|
runCompose(options, ["pull"]);
|
|
@@ -690,7 +977,11 @@ async function update(options) {
|
|
|
690
977
|
throw new Error("Deployment is not initialized. Run install first.");
|
|
691
978
|
}
|
|
692
979
|
ensureDeploymentAssets(options);
|
|
693
|
-
|
|
980
|
+
if (options.tag) {
|
|
981
|
+
console.log(`Pinning release images to requested tag ${options.tag}.`);
|
|
982
|
+
} else {
|
|
983
|
+
await syncImageTagsToLatestPublishedRelease(options);
|
|
984
|
+
}
|
|
694
985
|
console.log("Pulling updated images...");
|
|
695
986
|
runCompose(options, ["pull"]);
|
|
696
987
|
console.log("Restarting release stack...");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wattetheria",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Wattetheria deployment CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"bin/",
|
|
10
10
|
"lib/",
|
|
11
11
|
"docker-compose.release.yml",
|
|
12
|
-
".env.release",
|
|
13
12
|
"README.md",
|
|
14
13
|
"LICENSE"
|
|
15
14
|
],
|
package/.env.release
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# Coordinated release image set
|
|
2
|
-
RELEASE_TAG=1.0.3
|
|
3
|
-
|
|
4
|
-
WATTETHERIA_KERNEL_IMAGE=ghcr.io/wattetheria/wattetheria-kernel:${RELEASE_TAG}
|
|
5
|
-
WATTETHERIA_OBSERVATORY_IMAGE=ghcr.io/wattetheria/wattetheria-observatory:${RELEASE_TAG}
|
|
6
|
-
WATTSWARM_KERNEL_IMAGE=ghcr.io/wattetheria/wattswarm-kernel:${RELEASE_TAG}
|
|
7
|
-
WATTSWARM_RUNTIME_IMAGE=ghcr.io/wattetheria/wattswarm-runtime:${RELEASE_TAG}
|
|
8
|
-
WATTSWARM_WORKER_IMAGE=ghcr.io/wattetheria/wattswarm-worker:${RELEASE_TAG}
|
|
9
|
-
|
|
10
|
-
# Host bindings
|
|
11
|
-
WATTETHERIA_CONTROL_PLANE_BIND_HOST=127.0.0.1
|
|
12
|
-
WATTETHERIA_CONTROL_PLANE_PORT=7777
|
|
13
|
-
WATTETHERIA_OBSERVATORY_BIND_HOST=127.0.0.1
|
|
14
|
-
WATTETHERIA_OBSERVATORY_PORT=8780
|
|
15
|
-
WATTSWARM_UI_BIND_HOST=127.0.0.1
|
|
16
|
-
WATTSWARM_UI_PORT=7788
|
|
17
|
-
WATTSWARM_SYNC_GRPC_BIND_HOST=127.0.0.1
|
|
18
|
-
WATTSWARM_SYNC_GRPC_PORT=7791
|
|
19
|
-
WATTSWARM_P2P_HOST_PORT=4001
|
|
20
|
-
WATTSWARM_UDP_ANNOUNCE_HOST_PORT=37931
|
|
21
|
-
|
|
22
|
-
# Host-mounted state directories for local agent access
|
|
23
|
-
WATTETHERIA_HOST_STATE_DIR=./data/wattetheria
|
|
24
|
-
WATTSWARM_HOST_STATE_DIR=./data/wattswarm
|
|
25
|
-
WATTETHERIA_RUNTIME_ENV_FILE=.env.release.local
|
|
26
|
-
|
|
27
|
-
# Agent-facing endpoints written into .agent-participation/manifest.json
|
|
28
|
-
WATTETHERIA_AGENT_CONTROL_PLANE_ENDPOINT=http://127.0.0.1:7777
|
|
29
|
-
WATTETHERIA_AGENT_WATTSWARM_UI_BASE_URL=http://127.0.0.1:7788
|
|
30
|
-
WATTETHERIA_AGENT_WATTSWARM_SYNC_GRPC_ENDPOINT=http://127.0.0.1:7791
|
|
31
|
-
WATTETHERIA_AGENT_HOST_DATA_DIR=./data/wattetheria
|
|
32
|
-
|
|
33
|
-
# Wattetheria runtime
|
|
34
|
-
WATTETHERIA_BRAIN_PROVIDER_KIND=rules
|
|
35
|
-
WATTETHERIA_BRAIN_BASE_URL=
|
|
36
|
-
WATTETHERIA_BRAIN_MODEL=
|
|
37
|
-
WATTETHERIA_BRAIN_API_KEY_ENV=
|
|
38
|
-
WATTETHERIA_SERVICENET_BASE_URL=
|
|
39
|
-
WATTETHERIA_AUTONOMY_ENABLED=false
|
|
40
|
-
WATTETHERIA_AUTONOMY_INTERVAL_SEC=30
|
|
41
|
-
OPENCLAW_API_KEY=
|
|
42
|
-
|
|
43
|
-
# Wattswarm database
|
|
44
|
-
WATTSWARM_PG_DB=wattswarm
|
|
45
|
-
WATTSWARM_PG_USER=postgres
|
|
46
|
-
WATTSWARM_PG_PASSWORD=replace-with-strong-password
|
|
47
|
-
|
|
48
|
-
# Wattswarm runtime and network
|
|
49
|
-
WATTSWARM_P2P_ENABLED=true
|
|
50
|
-
WATTSWARM_P2P_MDNS=true
|
|
51
|
-
WATTSWARM_P2P_PORT=4001
|
|
52
|
-
WATTSWARM_WORKER_CONCURRENCY=16
|
|
53
|
-
WATTSWARM_WORKER_POLL_MS=250
|
|
54
|
-
WATTSWARM_WORKER_LEASE_MS=30000
|
|
55
|
-
WATTSWARM_UDP_ANNOUNCE_ENABLED=false
|
|
56
|
-
WATTSWARM_UDP_ANNOUNCE_MODE=multicast
|
|
57
|
-
WATTSWARM_UDP_ANNOUNCE_ADDR=239.255.42.99
|
|
58
|
-
WATTSWARM_UDP_ANNOUNCE_PORT=37931
|