wattetheria 0.1.4 → 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
 
@@ -211,12 +204,14 @@ Read the diagram in layers:
211
204
  - `/v1/client/self`
212
205
  - `/v1/client/rpc-logs`
213
206
  - `/v1/client/tasks`
207
+ - `/v1/client/task-activity`
214
208
  - `/v1/client/organizations`
215
209
  - `/v1/client/leaderboard`
216
210
  - Public signed export endpoint:
217
211
  - `/v1/client/export` returns a signed public snapshot for local inspection
218
212
  - `wattetheria-gateway` ingests snapshots via wattswarm; pull data from wattetheria
219
213
  - social snapshot arrays currently include `friend_relationships`, `pending_friend_requests`, `public_blocks`, `dm_threads`, and `dm_messages`
214
+ - additive swarm bridge views now include `swarm_task_activity`
220
215
  - Civilization endpoints for profile, metrics, emergencies, briefing, world zones/events, and mission lifecycle
221
216
  - Civilization social endpoints:
222
217
  - `/v1/civilization/agent-friends`
@@ -566,6 +561,8 @@ Version commands:
566
561
  - `npx wattetheria --version` shows the current Wattetheria release version
567
562
  - `npx wattetheria version --images` prints the configured image refs for the current deployment
568
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
569
566
 
570
567
  Release deployments bind-mount host-visible state by default:
571
568
 
@@ -595,7 +592,7 @@ pwsh ./scripts/deploy-release.ps1
595
592
  - `docker-compose.yml` is the local `wattetheria`-only development stack
596
593
  - `docker-compose.full.yml` is the local joint development stack for `wattetheria` + `wattswarm`
597
594
  - `docker-compose.release.yml` is the image-based release deployment asset used by the CLI and fallback scripts
598
- - `.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
599
596
  - `scripts/deploy-release.ps1` is a cross-platform fallback deployment entry point
600
597
  - this repository does not include `wattetheria-gateway`; gateway is a separate project and deployment unit
601
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,6 +762,62 @@ function ensureHostStateDirectories(baseDir, envMap) {
508
762
  }
509
763
  }
510
764
 
765
+ async function syncImageTagsToLatestPublishedRelease(options) {
766
+ if (options.tag) {
767
+ return options.tag;
768
+ }
769
+
770
+ const targetPath = envFilePath(options);
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
+
800
+ let changed = false;
801
+ for (const key of IMAGE_KEYS) {
802
+ const current = targetMap.get(key);
803
+ if (!current) {
804
+ continue;
805
+ }
806
+ const next = `${stripImageTag(resolveEnvReference(current.trim(), targetMap))}:${latestTag}`;
807
+ if (resolveEnvReference(current.trim(), targetMap) !== next) {
808
+ targetMap.set(key, next);
809
+ changed = true;
810
+ }
811
+ }
812
+ if (changed || currentTag.trim() !== latestTag) {
813
+ writeEnvFile(targetPath, targetMap);
814
+ console.log(`Resolved latest published release tag ${latestTag}.`);
815
+ } else {
816
+ console.log(`Release images are already pinned to latest published tag ${latestTag}.`);
817
+ }
818
+ return latestTag;
819
+ }
820
+
511
821
  function pinImageTags(envMap, tag) {
512
822
  for (const key of IMAGE_KEYS) {
513
823
  if (!envMap.has(key)) {
@@ -628,6 +938,11 @@ function printSummary(options) {
628
938
  async function install(options) {
629
939
  await ensureDockerAvailable({ interactive: true });
630
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
+ }
631
946
  runCompose(options, ["config"], true);
632
947
  console.log("Pulling release images...");
633
948
  runCompose(options, ["pull"]);
@@ -662,6 +977,11 @@ async function update(options) {
662
977
  throw new Error("Deployment is not initialized. Run install first.");
663
978
  }
664
979
  ensureDeploymentAssets(options);
980
+ if (options.tag) {
981
+ console.log(`Pinning release images to requested tag ${options.tag}.`);
982
+ } else {
983
+ await syncImageTagsToLatestPublishedRelease(options);
984
+ }
665
985
  console.log("Pulling updated images...");
666
986
  runCompose(options, ["pull"]);
667
987
  console.log("Restarting release stack...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wattetheria",
3
- "version": "0.1.4",
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.2
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