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 CHANGED
@@ -148,18 +148,11 @@ Read the diagram in layers:
148
148
 
149
149
  ### Tasks, Oracle, And Mailbox
150
150
 
151
- - Legacy task engine with deterministic lifecycle:
152
- - publish
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
- - `.env.release` is the release deployment environment template used by the CLI and fallback scripts
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 Update image tags, pull, and restart
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 templates from package assets
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: "template",
223
- path: RELEASE_ENV_TEMPLATE
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 = getReleaseImageMap(source.path);
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 = tags.length === IMAGE_KEYS.length && new Set(tags).size === 1
234
- ? tags[0]
235
- : "custom";
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
- fs.copyFileSync(templateEnvPath, targetEnvPath);
675
+ writeEnvFile(targetEnvPath, defaultEnvMap());
422
676
  } else {
423
- mergeNewTemplateKeys(templateEnvPath, targetEnvPath);
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 mergeNewTemplateKeys(templatePath, targetPath) {
475
- const templateMap = readEnvFile(templatePath);
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 syncImageTagsFromTemplate(options) {
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 [...IMAGE_KEYS, "RELEASE_TAG"]) {
520
- const templateValue = templateMap.get(key);
521
- if (!templateValue) {
801
+ for (const key of IMAGE_KEYS) {
802
+ const current = targetMap.get(key);
803
+ if (!current) {
522
804
  continue;
523
805
  }
524
- const resolved = resolveEnvReference(templateValue.trim(), templateMap);
525
- const current = targetMap.get(key);
526
- const currentResolved = current ? resolveEnvReference(current.trim(), targetMap) : "";
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
- const newTag = extractImageTag(resolveEnvReference(templateMap.get(IMAGE_KEYS[0]) || "", templateMap));
535
- console.log(`Updated image tags to ${newTag || "latest template values"}.`);
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
- syncImageTagsFromTemplate(options);
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.5",
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