@underpostnet/cyberia 3.3.73 → 3.3.77

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.
Files changed (269) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +10 -0
  3. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  4. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  5. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  8. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  9. package/.github/workflows/engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  11. package/AGENTS.md +61 -12
  12. package/CHANGELOG.md +159 -161
  13. package/CLI-HELP.md +20 -13
  14. package/Dockerfile +1 -1
  15. package/Dockerfile.dev +1 -1
  16. package/Dockerfile.test +2 -2
  17. package/README.md +28 -28
  18. package/bin/build.js +96 -21
  19. package/bin/cyberia.js +1247 -1373
  20. package/bin/index.js +1247 -1373
  21. package/compose.env +17 -5
  22. package/conf.js +7 -4
  23. package/deploy/cyberia-client/deploy.sh +10 -11
  24. package/deploy/cyberia-client/package.sh +0 -1
  25. package/deploy/cyberia-client/state.sh +3 -2
  26. package/deploy/cyberia-server/deploy.sh +10 -11
  27. package/deploy/cyberia-server/package.sh +0 -1
  28. package/deploy/cyberia-server/state.sh +3 -2
  29. package/deploy/dd-cyberia/deploy.sh +5 -5
  30. package/deploy/dd-cyberia/init.sh +15 -14
  31. package/deploy/dd-cyberia/package.sh +0 -1
  32. package/deploy/dd-cyberia/state.sh +2 -2
  33. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  34. package/deploy/lib/config.sh +29 -0
  35. package/deploy/lib/github-actions-logging.sh +92 -76
  36. package/deploy/lib/host.sh +28 -19
  37. package/deploy/lib/state.sh +4 -1
  38. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  39. package/deploy/release/deploy.sh +0 -2
  40. package/deployment.yaml +1 -1
  41. package/docker-compose.yml +23 -17
  42. package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
  43. package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
  44. package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
  45. package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
  46. package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
  47. package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
  48. package/docs/coverage/cyberia/base.css +362 -0
  49. package/docs/coverage/cyberia/block-navigation.js +82 -0
  50. package/docs/coverage/cyberia/favicon.png +0 -0
  51. package/docs/coverage/cyberia/index.html +161 -0
  52. package/docs/coverage/cyberia/prettify.css +101 -0
  53. package/docs/coverage/cyberia/prettify.js +937 -0
  54. package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
  55. package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
  56. package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
  57. package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
  58. package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
  59. package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
  60. package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
  61. package/docs/coverage/cyberia/sorter.js +205 -0
  62. package/hardhat/package-lock.json +2 -2
  63. package/hardhat/package.json +1 -1
  64. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  65. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  66. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  67. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  68. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  69. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  70. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  71. package/manifests/deployment/mongo-express/service.yaml +16 -0
  72. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  73. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  74. package/package.json +19 -17
  75. package/scripts/test-monitor.sh +3 -3
  76. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  77. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  80. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  81. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  82. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  83. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  84. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  85. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  86. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  87. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  88. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  90. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  91. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  92. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  93. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  94. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  95. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  96. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  97. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  98. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  99. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  100. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  101. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  102. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  103. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  104. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  107. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  110. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  111. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  112. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  113. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  114. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  115. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  116. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  118. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  119. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  120. package/src/api/file/file.ref.js +153 -0
  121. package/src/api/file/file.ref.json +11 -2
  122. package/src/api/object-layer/object-layer.model.js +47 -12
  123. package/src/api/object-layer/object-layer.router.js +10 -3
  124. package/src/api/object-layer/object-layer.service.js +113 -120
  125. package/src/cli/app.js +12 -1
  126. package/src/cli/client.js +2 -2
  127. package/src/cli/cluster.js +80 -18
  128. package/src/cli/deploy.js +215 -34
  129. package/src/cli/docker-compose.js +1 -1
  130. package/src/cli/fs-selection.js +115 -0
  131. package/src/cli/fs.js +158 -342
  132. package/src/cli/index.js +22 -10
  133. package/src/cli/release.js +3 -1
  134. package/src/cli/repository.js +35 -9
  135. package/src/cli/run.js +80 -199
  136. package/src/cli/test.js +4 -0
  137. package/src/cli/wireguard.js +32 -31
  138. package/src/client/components/core/Badge.js +0 -1
  139. package/src/client/components/core/CssCore.js +8 -1
  140. package/src/client/components/core/Docs.js +77 -100
  141. package/src/client/components/core/Modal.js +77 -23
  142. package/src/client/components/core/ToolTip.js +15 -0
  143. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
  144. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  145. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  146. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  147. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  148. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  149. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  150. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  151. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  152. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  153. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  154. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  155. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  156. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
  157. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  158. package/src/client/components/default/AppShellDefault.js +1 -4
  159. package/src/client/components/itemledger/AppShellItemledger.js +0 -2
  160. package/src/client/components/underpost/AppShellUnderpost.js +1 -1
  161. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  162. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  163. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  164. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  165. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  166. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  167. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  168. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  169. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  170. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  171. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  172. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  173. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  174. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  175. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  176. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  177. package/src/client/services/object-layer/object-layer.management.js +6 -10
  178. package/src/client/services/object-layer/object-layer.service.js +3 -1
  179. package/src/client/ssr/views/Cyberia404.js +3 -3
  180. package/src/client-builder/client-build-docs.js +52 -52
  181. package/src/client-builder/client-build.js +67 -44
  182. package/src/client-builder/client-bundle.js +347 -0
  183. package/src/db/mongo/MongoExpress.js +187 -0
  184. package/src/grpc/cyberia/grpc-server.js +5 -7
  185. package/src/index.js +1 -1
  186. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  187. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  188. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  189. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  190. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  191. package/src/projects/cyberia/gemini-client.js +3 -3
  192. package/src/projects/cyberia/generate-saga.js +4 -11
  193. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  194. package/src/projects/cyberia/instance-backup.js +210 -0
  195. package/src/projects/cyberia/instance-data.js +177 -257
  196. package/src/projects/cyberia/map-preview-generator.js +32 -68
  197. package/src/projects/cyberia/object-layer.js +152 -319
  198. package/src/projects/cyberia/seed-audio.js +139 -0
  199. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  200. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  201. package/src/projects/cyberia/stat-balance.js +81 -0
  202. package/src/projects/cyberia/stat-commands.js +23 -0
  203. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  204. package/src/runtime/cyberia-client/Dockerfile +8 -4
  205. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  206. package/src/runtime/cyberia-server/Dockerfile +9 -2
  207. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  208. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  209. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  210. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  211. package/src/runtime/engine-cyberia/compose.env +17 -5
  212. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  213. package/src/server/build/coverage.js +113 -42
  214. package/src/server/build/package.js +55 -12
  215. package/src/server/build/testing.js +59 -9
  216. package/src/server/network/middlewares.js +8 -2
  217. package/src/server/network/underpost-gateway.js +113 -14
  218. package/src/server/ops/logger.js +70 -35
  219. package/src/server/runtime/conf.js +40 -9
  220. package/src/server/runtime/start.js +11 -2
  221. package/src/server/storage/data-query.js +16 -0
  222. package/src/server/storage/downloader.js +62 -14
  223. package/src/server/storage/zip.js +153 -0
  224. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  225. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  226. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  227. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  228. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  229. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  230. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  231. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  232. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  233. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  234. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  235. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  236. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  237. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  238. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  239. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  240. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  241. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  242. package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
  243. package/test/unit/catalog.test.js +13 -2
  244. package/test/unit/client-build-docs.test.js +97 -70
  245. package/test/unit/client-bundle.test.js +313 -0
  246. package/test/unit/conf-resolution.test.js +58 -7
  247. package/test/unit/coverage-artifact.test.js +120 -29
  248. package/test/unit/cyberia/instance-backup.test.js +183 -0
  249. package/test/unit/cyberia/instance-data.test.js +314 -0
  250. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  251. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  252. package/test/unit/cyberia/stat-balance.test.js +69 -0
  253. package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
  254. package/test/unit/deploy-log-table.test.js +76 -5
  255. package/test/unit/downloader-integrity.test.js +98 -0
  256. package/test/unit/file-reference-registry.test.js +141 -0
  257. package/test/unit/fs-storage-paths.test.js +596 -0
  258. package/test/unit/gateway-pod-fetch.test.js +95 -0
  259. package/test/unit/letsencrypt-issuer.test.js +101 -0
  260. package/test/unit/logger-redaction.test.js +29 -0
  261. package/test/unit/package.test.js +70 -16
  262. package/test/unit/prepare-host.test.js +84 -1
  263. package/test/unit/release-bump.test.js +3 -1
  264. package/test/unit/start-options.test.js +12 -0
  265. package/test/unit/test-tiers.test.js +27 -0
  266. package/test/unit/zip-archive.test.js +142 -0
  267. package/vitest.config.js +11 -3
  268. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  269. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
package/src/cli/fs.js CHANGED
@@ -1,378 +1,194 @@
1
- /**
2
- * File storage module for managing file operations using Cloudinary.
3
- * @module src/cli/fs.js
4
- * @namespace UnderpostFileStorage
5
- */
6
-
7
1
  import { v2 as cloudinary } from 'cloudinary';
8
- import { loggerFactory } from '../server/ops/logger.js';
9
- import AdmZip from 'adm-zip';
10
- import * as dir from 'path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import * as dir from 'node:path';
11
4
  import fs from 'fs-extra';
5
+ import { loggerFactory } from '../server/ops/logger.js';
6
+ import { readZipEntry } from '../server/storage/zip.js';
12
7
  import Downloader from '../server/storage/downloader.js';
13
- import { shellExec } from '../server/runtime/process.js';
14
- import Underpost from '../index.js';
8
+ import * as selection from './fs-selection.js';
15
9
 
16
10
  const logger = loggerFactory(import.meta);
11
+ const CLOUDINARY_DELIVERY_TYPE = 'private';
12
+ const CLOUDINARY_ACCESS_CONTROL = [{ access_type: 'token' }];
13
+ const DELIVERY_TYPES = ['private', 'upload', 'authenticated'];
17
14
 
18
15
  /**
19
- * @class UnderpostFileStorage
20
- * @description Manages file storage operations using Cloudinary.
21
- * This class provides a set of static methods to upload, pull, and delete files
22
- * from Cloudinary, as well as manage a local storage configuration file.
16
+ * What a pull reports when the local file is already there and no overwrite was asked for.
17
+ * Distinct from a delivery type, which is what a pull that actually transferred returns.
23
18
  */
19
+ const PULL_SKIPPED = 'skipped';
20
+ const manifestIdPattern = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
21
+
24
22
  class UnderpostFileStorage {
25
23
  static API = {
26
- /**
27
- * @method cloudinaryConfig
28
- * @description Configures the Cloudinary client with environment variables.
29
- * @memberof UnderpostFileStorage
30
- */
24
+ ...selection,
25
+
31
26
  cloudinaryConfig() {
32
- // https://console.cloudinary.com/
33
- cloudinary.config({
27
+ const config = {
34
28
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
35
29
  api_key: process.env.CLOUDINARY_API_KEY,
36
30
  api_secret: process.env.CLOUDINARY_API_SECRET,
37
- });
31
+ };
32
+ if (Object.values(config).some((value) => !value))
33
+ throw new Error('Set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET in the environment.');
34
+ cloudinary.config(config);
35
+ },
36
+
37
+ resolveManifest(options = {}) {
38
+ if (options.storageFilePath !== undefined)
39
+ throw new Error('Use --storage-id with a sub-id, not a manifest path.');
40
+ if (typeof options.deployId !== 'string' || !manifestIdPattern.test(options.deployId))
41
+ throw new Error('Provide a valid --deploy-id using letters, digits, hyphens, or underscores.');
42
+ if (
43
+ options.storageId !== undefined &&
44
+ (typeof options.storageId !== 'string' || !manifestIdPattern.test(options.storageId))
45
+ )
46
+ throw new Error('Use a --storage-id sub-id with letters, digits, hyphens, or underscores.');
47
+ const name = options.storageId === undefined ? 'storage.json' : `storage.${options.storageId}.json`;
48
+ return `engine-private/conf/${options.deployId}/${name}`;
38
49
  },
39
- /**
40
- * @method getStorageConf
41
- * @description Retrieves the storage configuration for a specific deployment.
42
- * @param {object} options - An object containing deployment-specific options.
43
- * @param {string} options.deployId - The identifier for the deployment.
44
- * @param {string} [options.storageFilePath] - The path to the storage configuration file.
45
- * @returns {object} An object containing the storage configuration and storage file path.
46
- * @memberof UnderpostFileStorage
47
- */
48
- getStorageConf(options) {
49
- let storage, storageConf;
50
- if (options.deployId && typeof options.deployId === 'string') {
51
- storageConf = options.storageFilePath ?? `./engine-private/conf/${options.deployId}/storage.json`;
52
- if (!fs.existsSync(storageConf)) fs.writeFileSync(storageConf, JSON.stringify({}), 'utf8');
53
- storage = JSON.parse(fs.readFileSync(storageConf, 'utf8'));
50
+
51
+ readManifest(options = {}) {
52
+ const storageConf = UnderpostFileStorage.API.resolveManifest(options);
53
+ const storage = fs.existsSync(storageConf) ? JSON.parse(fs.readFileSync(storageConf, 'utf8')) : {};
54
+ if (!storage || typeof storage !== 'object' || Array.isArray(storage))
55
+ throw new Error(`Manifest must contain an object: ${storageConf}`);
56
+ const paths = new Set();
57
+ for (const [key, entry] of Object.entries(storage)) {
58
+ const normalized = selection.normalizeStoragePath(key);
59
+ if (!key || !entry || typeof entry !== 'object' || Array.isArray(entry) || paths.has(normalized))
60
+ throw new Error(`Invalid or duplicate manifest entry: ${key}`);
61
+ if (entry.type !== undefined && !DELIVERY_TYPES.includes(entry.type))
62
+ throw new Error(`Invalid manifest delivery type: ${key}`);
63
+ if (entry.bytes !== undefined && (!Number.isSafeInteger(entry.bytes) || entry.bytes < 0))
64
+ throw new Error(`Invalid manifest byte count: ${key}`);
65
+ paths.add(normalized);
54
66
  }
55
67
  return { storage, storageConf };
56
68
  },
57
- /**
58
- * @method writeStorageConf
59
- * @description Writes the storage configuration to a file.
60
- * @param {object} storage - The storage configuration object.
61
- * @param {string} storageConf - The path to the storage configuration file.
62
- * @memberof UnderpostFileStorage
63
- */
64
- writeStorageConf(storage, storageConf) {
65
- if (storage) fs.writeFileSync(storageConf, JSON.stringify(storage, null, 4), 'utf8');
66
- },
67
- /**
68
- * @method gitTrack
69
- * @description Optional, non-fatal Git tracking layer. Any Git error is logged and swallowed
70
- * so it can never interrupt or roll back the canonical `storage.*.json` workflow.
71
- * @param {string} gitPath - The working directory to stage/commit.
72
- * @param {object} [options] - Tracking options.
73
- * @param {boolean} [options.init=false] - If true, initialize a local repo before staging.
74
- * @param {string} [options.message=''] - Explicit commit message; when omitted, `underpost cmt` is used.
75
- * @memberof UnderpostFileStorage
76
- */
77
- gitTrack(gitPath, options = { init: false, message: '' }) {
69
+
70
+ writeManifest(storage, storageConf) {
71
+ fs.ensureDirSync(dir.dirname(storageConf));
72
+ const temporary = `${storageConf}.${randomUUID()}.tmp`;
78
73
  try {
79
- if (options.init === true) Underpost.repo.initLocalRepo({ path: gitPath });
80
- shellExec(`cd ${gitPath} && git add .`, { silentOnError: true, silent: true, disableLog: true });
81
- if (options.message)
82
- shellExec(`cd ${gitPath} && git commit -m "${options.message}"`, {
83
- silentOnError: true,
84
- silent: true,
85
- disableLog: true,
86
- });
87
- else shellExec(`underpost cmt ${gitPath} feat`, { silentOnError: true, silent: true, disableLog: true });
88
- } catch (error) {
89
- logger.warn('git tracking skipped (non-fatal)', { gitPath, error: error?.message });
74
+ fs.writeFileSync(temporary, JSON.stringify(storage, null, 4), { encoding: 'utf8', flag: 'wx' });
75
+ fs.renameSync(temporary, storageConf);
76
+ } finally {
77
+ fs.removeSync(temporary);
90
78
  }
91
79
  },
92
- /**
93
- * @method recursiveCallback
94
- * @description Recursively processes files and directories based on the provided options.
95
- * @param {string} path - The path to the directory to process.
96
- * @param {object} [options] - An object containing options for the recursive callback.
97
- * @param {boolean} [options.rm=false] - Flag to remove files and directories.
98
- * @param {boolean} [options.recursive=false] - Flag to process directories recursively.
99
- * @param {string} [options.deployId=''] - The identifier for the deployment.
100
- * @param {boolean} [options.force=false] - Flag to force file operations.
101
- * @param {boolean} [options.pull=false] - Flag to pull files from storage.
102
- * @param {boolean} [options.git=false] - Flag to use Git for file operations.
103
- * @param {boolean} [options.omitUnzip=false] - If true, do not extract zip and keep downloaded zip file.
104
- * @param {string} [options.storageFilePath=''] - The path to the storage configuration file.
105
- * @returns {Promise<void>} A promise that resolves when the recursive callback is complete.
106
- * @memberof UnderpostFileStorage
107
- */
108
- async recursiveCallback(
109
- path,
110
- options = {
111
- rm: false,
112
- recursive: false,
113
- deployId: '',
114
- force: false,
115
- pull: false,
116
- git: false,
117
- omitUnzip: false,
118
- storageFilePath: '',
119
- },
120
- ) {
121
- const { storage, storageConf } = Underpost.fs.getStorageConf(options);
122
-
123
- // ── Single-file handling: when path is a file (not a directory), use parent dir
124
- // as the git working directory and process just that one file. ──────────────
125
- let isSingleFile = false;
126
- let parentDir = path;
127
- let singleFileName = '';
128
- if (fs.existsSync(path) && !fs.statSync(path).isDirectory()) {
129
- isSingleFile = true;
130
- parentDir = dir.dirname(path);
131
- singleFileName = path.split('/').pop();
132
- }
133
-
134
- // In recursive remove mode, delete every tracked storage key under the requested path,
135
- // even when local files/directories are already missing.
136
- if (options.rm === true) {
137
- const normalizedPath = typeof path === 'string' ? path.trim() : '';
138
- const basePath = normalizedPath.replace(/\/+$/, '');
139
- const hasPathFilter = basePath.length > 0;
140
-
141
- const associatedPaths = Object.keys(storage || {}).filter((storedPath) => {
142
- if (!hasPathFilter) return true;
143
- return storedPath === basePath || storedPath.startsWith(`${basePath}/`);
144
- });
145
-
146
- for (const associatedPath of associatedPaths) {
147
- await Underpost.fs.delete(associatedPath);
148
- if (storage) delete storage[associatedPath];
149
- }
150
-
151
- if (hasPathFilter && options.force === true && fs.existsSync(basePath)) fs.removeSync(basePath);
152
-
153
- // Storage is canonical: persist the removal before any (optional) git tracking runs.
154
- Underpost.fs.writeStorageConf(storage, storageConf);
155
-
156
- if (associatedPaths.length === 0)
157
- logger.warn('No associated tracked storage paths found', { path: hasPathFilter ? basePath : '*' });
158
- else
159
- logger.info('Removed associated tracked storage paths', {
160
- path: hasPathFilter ? basePath : '*',
161
- removed: associatedPaths.length,
162
- });
163
-
164
- if (options.git === true) {
165
- const gitPath = !hasPathFilter ? '.' : isSingleFile ? parentDir : basePath;
166
- Underpost.fs.gitTrack(gitPath);
167
- }
168
-
169
- return;
170
- }
171
-
172
- // For single files, run getDeleteFiles against the parent directory to avoid
173
- // trying to `cd` into a file.
174
- const gitContextPath = isSingleFile ? parentDir : path;
175
- // Detecting locally-deleted files is a best-effort enhancement backed by git; if the path is
176
- // not a repo (or git is unavailable) it must not block the canonical storage workflow.
177
- let deleteFiles = [];
178
- if (options.pull !== true) {
179
- try {
180
- deleteFiles = Underpost.repo.getDeleteFiles(gitContextPath);
181
- } catch (error) {
182
- logger.warn('delete detection skipped (git unavailable)', { path: gitContextPath, error: error?.message });
183
- }
184
- }
185
80
 
186
- // When processing a single file, only consider it for deletion
187
- for (const relativePath of deleteFiles) {
188
- const _path = isSingleFile ? (relativePath === singleFileName ? path : null) : path + '/' + relativePath;
189
- if (_path && _path in storage) {
190
- await Underpost.fs.delete(_path);
191
- delete storage[_path];
192
- }
193
- }
194
- if (options.pull === true) {
195
- let pullSkipCount = 0;
196
- for (const _path of Object.keys(storage)) {
197
- if (!fs.existsSync(_path) || options.force === true) {
198
- if (options.force === true && fs.existsSync(_path)) fs.removeSync(_path);
199
- await Underpost.fs.pull(_path, options);
200
- } else pullSkipCount++;
201
- }
202
- if (pullSkipCount > 0) logger.warn(`Pull skipped ${pullSkipCount} files that already exist`);
203
- // Only run git init/commit when the caller explicitly requests git tracking (--git flag).
204
- // For bundle pulls into ./build the git step is unwanted and would error on a non-repo path.
205
- if (options.git === true) Underpost.fs.gitTrack(gitContextPath, { init: true, message: 'Base pull state' });
206
- } else {
207
- let files;
208
- if (isSingleFile) {
209
- // Single file: treat the file itself as the sole item to process
210
- files = [singleFileName];
211
- } else {
212
- files =
213
- options.git === true
214
- ? Underpost.repo.getChangedFiles(gitContextPath)
215
- : await fs.readdir(path, { recursive: true });
216
- }
217
- for (const relativePath of files) {
218
- const _path = isSingleFile ? path : path + '/' + relativePath;
219
- if (fs.existsSync(_path) && fs.statSync(_path).isDirectory()) {
220
- if (options.pull === true && !fs.existsSync(_path)) fs.mkdirSync(_path, { recursive: true });
221
- continue;
222
- } else if (!(_path in storage) || options.force === true) {
223
- await Underpost.fs.upload(_path, options);
224
- if (storage) storage[_path] = {};
225
- } else logger.warn('File already exists', _path);
226
- }
227
- }
228
- // Storage is canonical and always persisted; git is an optional layer on top.
229
- Underpost.fs.writeStorageConf(storage, storageConf);
230
- if (options.git === true) Underpost.fs.gitTrack(gitContextPath);
81
+ updateManifest(manifest, path, entry) {
82
+ const next = { ...manifest.storage };
83
+ if (entry === undefined) delete next[path];
84
+ else Object.defineProperty(next, path, { value: entry, enumerable: true, configurable: true, writable: true });
85
+ UnderpostFileStorage.API.writeManifest(next, manifest.storageConf);
86
+ manifest.storage = next;
231
87
  },
232
- /**
233
- * @method callback
234
- * @description Orchestrates file storage operations based on the provided options.
235
- * This method handles file uploads, deletions, and recursive processing of directories.
236
- * @param {string} path - The path to the file or directory to process.
237
- * @param {object} [options] - An object containing options for the callback.
238
- * @param {boolean} [options.rm=false] - Flag to remove files and directories.
239
- * @param {boolean} [options.recursive=false] - Flag to process directories recursively.
240
- * @param {string} [options.deployId=''] - The identifier for the deployment.
241
- * @param {boolean} [options.force=false] - Flag to force file operations.
242
- * @param {boolean} [options.pull=false] - Flag to pull files from storage.
243
- * @param {boolean} [options.git=false] - Flag to use Git for file operations.
244
- * @param {boolean} [options.omitUnzip=false] - If true, do not extract zip and keep downloaded zip file.
245
- * @returns {Promise<void>} A promise that resolves when the callback is complete.
246
- * @memberof UnderpostFileStorage
247
- */
248
- async callback(
249
- path,
250
- options = { rm: false, recursive: false, deployId: '', force: false, pull: false, git: false, omitUnzip: false },
251
- ) {
252
- // rm always routes through recursiveCallback so storage.*.json is updated regardless of
253
- // --recursive/--git. The bare `delete` primitive only removes the remote asset and would
254
- // otherwise leave the tracked storage key orphaned.
255
- if (options.recursive === true || options.git === true || options.rm === true)
256
- return await Underpost.fs.recursiveCallback(path, options);
257
- if (options.pull === true) return await Underpost.fs.pull(path, options);
258
- return await Underpost.fs.upload(path, options);
259
- },
260
- /**
261
- * @method upload
262
- * @description Uploads a file to Cloudinary.
263
- * @param {string} path - The path to the file to upload.
264
- * @param {object} [options] - An object containing options for the upload.
265
- * @param {string} [options.deployId=''] - The identifier for the deployment (used to locate the storage config file).
266
- * @param {boolean} [options.force=false] - Flag to force file operations (overwrites existing remote asset).
267
- * @param {string} [options.storageFilePath=''] - The path to the storage configuration file.
268
- * @returns {Promise<object>} A promise that resolves to the upload result.
269
- * @memberof UnderpostFileStorage
270
- */
271
88
 
272
- async upload(
273
- path,
274
- options = { rm: false, recursive: false, deployId: '', force: false, pull: false, storageFilePath: '' },
275
- ) {
276
- Underpost.fs.cloudinaryConfig();
277
- const { storage, storageConf } = Underpost.fs.getStorageConf(options);
278
- // path = Underpost.fs.file2Zip(path);
279
- const uploadResult = await cloudinary.uploader
280
- .upload(path, {
281
- public_id: path,
282
- resource_type: 'raw',
283
- overwrite: options.force === true ? true : false,
284
- })
285
- .catch((error) => {
286
- logger.error(error, { path, stack: error.stack });
287
- });
288
- logger.info('upload result', uploadResult);
289
- if (storage) storage[path] = {};
290
- Underpost.fs.writeStorageConf(storage, storageConf);
291
- return uploadResult;
89
+ deliveryTypeCandidates(storage, path) {
90
+ const recorded = storage[path]?.type ?? 'upload';
91
+ return [...new Set([recorded, CLOUDINARY_DELIVERY_TYPE, 'upload'])];
292
92
  },
293
- /**
294
- * @method pull
295
- * @description Pulls a file from Cloudinary.
296
- * @param {string} path - The path to the file to pull.
297
- * @param {object} [options] - Pull options.
298
- * @param {boolean} [options.omitUnzip=false] - If true, do not extract zip and keep downloaded zip file.
299
- * @param {boolean} [options.force=false] - If true, re-download even if the local zip already exists.
300
- * @returns {Promise<void>} A promise that resolves when the file is pulled.
301
- * @memberof UnderpostFileStorage
302
- */
303
- async pull(path, options = { omitUnzip: false, force: false }) {
304
- Underpost.fs.cloudinaryConfig();
305
- const folder = dir.dirname(path);
306
- if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
307
- const zipPath = `${path}.zip`;
308
93
 
309
- if (options.omitUnzip === true && options.force !== true && fs.existsSync(zipPath)) {
310
- logger.warn('pull skipped, zip already exists and omit-unzip is enabled', { path, zipPath });
311
- return;
94
+ async callback(path, options = {}) {
95
+ const api = UnderpostFileStorage.API;
96
+ const manifest = api.readManifest(options);
97
+ const paths = api.resolveSelection(path, manifest.storage, options);
98
+ const operation = options.rm ? 'delete' : options.pull ? 'pull' : 'upload';
99
+ if (paths.length === 0) logger.warn('No storage paths selected.');
100
+ // A skip is the absence of work, and one line per file buries the transfers that did happen
101
+ // under hundreds that did not. The count carries the same information: which files were
102
+ // skipped is exactly which files were already there.
103
+ let skipped = 0;
104
+ for (const selected of paths) {
105
+ if ((await api[operation](selected, options, manifest)) === PULL_SKIPPED) skipped++;
312
106
  }
107
+ if (skipped > 0) logger.info('Pull skipped files that already exist', { skipped, selected: paths.length });
108
+ },
313
109
 
314
- const downloadResult = await cloudinary.utils.download_archive_url({
315
- public_ids: [path],
110
+ async upload(path, options = {}, manifest = UnderpostFileStorage.API.readManifest(options)) {
111
+ const api = UnderpostFileStorage.API;
112
+ if (!fs.statSync(path).isFile()) throw new Error(`Upload requires a regular file: ${path}`);
113
+ api.cloudinaryConfig();
114
+ const result = await cloudinary.uploader.upload(path, {
115
+ public_id: path,
316
116
  resource_type: 'raw',
117
+ type: CLOUDINARY_DELIVERY_TYPE,
118
+ access_control: CLOUDINARY_ACCESS_CONTROL,
119
+ overwrite: options.force === true,
317
120
  });
318
- await Downloader.downloadFile(downloadResult, zipPath);
121
+ const remote = result?.existing
122
+ ? await cloudinary.api.resource(path, { resource_type: 'raw', type: CLOUDINARY_DELIVERY_TYPE })
123
+ : result;
124
+ if (
125
+ remote?.error ||
126
+ remote?.public_id !== path ||
127
+ remote?.type !== CLOUDINARY_DELIVERY_TYPE ||
128
+ !Number.isSafeInteger(remote?.bytes) ||
129
+ remote.bytes < 0
130
+ )
131
+ throw new Error(`Invalid Cloudinary upload response: ${path}`);
132
+ api.updateManifest(manifest, path, { ...manifest.storage[path], type: remote.type, bytes: remote.bytes });
133
+ logger.info(result.existing ? 'Remote asset already exists' : 'Uploaded asset', { path });
134
+ return result;
135
+ },
319
136
 
320
- if (options.omitUnzip === true) {
321
- logger.warn('omit unzip enabled, keeping downloaded zip file', { path, zipPath });
322
- return;
137
+ async pull(path, options = {}, manifest = UnderpostFileStorage.API.readManifest(options)) {
138
+ const api = UnderpostFileStorage.API;
139
+ if (!Object.hasOwn(manifest.storage, path)) throw new Error(`Asset is not in the selected manifest: ${path}`);
140
+ const target = options.omitUnzip ? `${path}.zip` : path;
141
+ if (fs.existsSync(target) && !fs.statSync(target).isFile())
142
+ throw new Error(`Pull target must be a regular file: ${target}`);
143
+ if (fs.existsSync(target) && !options.force) return PULL_SKIPPED;
144
+ api.cloudinaryConfig();
145
+ fs.ensureDirSync(dir.dirname(target));
146
+ const temporary = fs.mkdtempSync(dir.join(dir.dirname(target), '.underpost-fs-'));
147
+ const archive = dir.join(temporary, 'download.zip');
148
+ try {
149
+ let resolvedType;
150
+ let downloadError;
151
+ for (const type of api.deliveryTypeCandidates(manifest.storage, path)) {
152
+ try {
153
+ const url = cloudinary.utils.download_archive_url({ public_ids: [path], resource_type: 'raw', type });
154
+ await Downloader.downloadFile(url, archive);
155
+ resolvedType = type;
156
+ break;
157
+ } catch (error) {
158
+ downloadError = error;
159
+ }
160
+ }
161
+ if (resolvedType === undefined) throw downloadError;
162
+ const content = await readZipEntry(archive, dir.basename(path));
163
+ if (!content) throw new Error(`Downloaded archive has no asset: ${path}`);
164
+ let downloaded = archive;
165
+ if (!options.omitUnzip) {
166
+ downloaded = dir.join(temporary, 'asset');
167
+ fs.writeFileSync(downloaded, content);
168
+ }
169
+ fs.renameSync(downloaded, target);
170
+ api.updateManifest(manifest, path, { ...manifest.storage[path], type: resolvedType, bytes: content.length });
171
+ logger.info('Pulled asset', { path: target });
172
+ return resolvedType;
173
+ } finally {
174
+ fs.removeSync(temporary);
323
175
  }
324
-
325
- path = Underpost.fs.zip2File(zipPath);
326
- fs.removeSync(`${path}.zip`);
327
- },
328
- /**
329
- * @method delete
330
- * @description Deletes a file from Cloudinary by its public ID.
331
- * @param {string} path - The path (public ID) of the file to delete.
332
- * @returns {Promise<object>} A promise that resolves to the Cloudinary delete result.
333
- * @memberof UnderpostFileStorage
334
- */
335
- async delete(path) {
336
- Underpost.fs.cloudinaryConfig();
337
- const deleteResult = await cloudinary.api
338
- .delete_resources([path], { type: 'upload', resource_type: 'raw' })
339
- .catch((error) => {
340
- logger.error(error, { path, stack: error.stack });
341
- });
342
- logger.info('delete result', deleteResult);
343
- return deleteResult;
344
- },
345
- /**
346
- * @method file2Zip
347
- * @description Converts a file to a zip file.
348
- * @param {string} path - The path to the file to convert.
349
- * @returns {string} The path to the zip file.
350
- * @memberof UnderpostFileStorage
351
- */
352
- file2Zip(path) {
353
- const zip = new AdmZip();
354
- zip.addLocalFile(path, '/');
355
- path = path + '.zip';
356
- zip.writeZip(path);
357
- return path;
358
176
  },
359
- /**
360
- * @method zip2File
361
- * @description Converts a zip file to a file.
362
- * @param {string} path - The path to the zip file to convert.
363
- * @returns {string} The path to the file.
364
- * @memberof UnderpostFileStorage
365
- */
366
- zip2File(path) {
367
- const zip = new AdmZip(path);
368
- path = path.replaceAll('.zip', '');
369
- zip.extractEntryTo(
370
- /*entry name*/ path.split('/').pop(),
371
- /*target path*/ dir.dirname(path),
372
- /*maintainEntryPath*/ false,
373
- /*overwrite*/ true,
374
- );
375
- return path;
177
+
178
+ async delete(path, options = {}, manifest = UnderpostFileStorage.API.readManifest(options)) {
179
+ const api = UnderpostFileStorage.API;
180
+ api.cloudinaryConfig();
181
+ const results = {};
182
+ for (const type of api.deliveryTypeCandidates(manifest.storage, path)) {
183
+ const result = await cloudinary.api.delete_resources([path], { type, resource_type: 'raw' });
184
+ const status = result?.deleted?.[path];
185
+ if (result?.error || !['deleted', 'not_found'].includes(status))
186
+ throw new Error(`Cloudinary delete failed for ${path} (${type}): ${status ?? 'missing status'}`);
187
+ results[type] = result;
188
+ }
189
+ api.updateManifest(manifest, path, undefined);
190
+ logger.info('Deleted remote asset', { path });
191
+ return results;
376
192
  },
377
193
  };
378
194
  }
package/src/cli/index.js CHANGED
@@ -289,6 +289,12 @@ program
289
289
  .option('--service-host <host>', 'Set custom host/IP for exposed MongoDB and Valkey clients.')
290
290
  .option('--postgresql', 'Initializes the cluster with a PostgreSQL statefulset.')
291
291
  .option('--mongodb4', 'Initializes the cluster with a MongoDB 4.4 service.')
292
+ .option(
293
+ '--mongo-express',
294
+ 'Deploys the mongo-express web client for inspecting the MongoDB statefulset databases and collections. ' +
295
+ 'Reuses the mongodb-secret credentials, and drops them when the deployed mongod runs without --auth. ' +
296
+ 'Combine with --node-port to expose it on the node network.',
297
+ )
292
298
  .option('--valkey', 'Initializes the cluster with a Valkey service.')
293
299
  .option('--ipfs', 'Initializes the cluster with an ipfs-cluster statefulset.')
294
300
  .option('--contour', 'Initializes the cluster with Project Contour base HTTPProxy and Envoy.')
@@ -622,16 +628,21 @@ program
622
628
 
623
629
  program
624
630
  .command('fs')
625
- .argument('[path]', 'The absolute or relative directory path for file operations.')
626
- .option('--rm', 'Removes the specified file.')
627
- .option('--git', 'Displays current Git changes related to file storage.')
628
- .option('--recursive', 'Uploads files recursively from the specified path.')
629
- .option('--deploy-id <deploy-id>', 'Specifies the deployment configuration ID for file operations.')
630
- .option('--pull', 'Downloads the specified file.')
631
+ .argument('[path]', 'Selects one file or all files below a directory. Required unless --tracked is set.')
632
+ .option('--rm', 'Deletes selected remote assets and their manifest entries.')
633
+ .option('--git', 'Restricts filesystem selection to Git-tracked files. Does not change Git state.')
634
+ .option('--recursive', 'Compatibility option. Directories always select files recursively.')
635
+ .option('--tracked', 'Selects only manifest entries. An optional path limits their scope.')
636
+ .requiredOption('--deploy-id <deploy-id>', 'Selects the deployment configuration.')
637
+ .option('--pull', 'Downloads selected manifest assets. Use --tracked to restore missing local files.')
631
638
  .option('--omit-unzip', 'With --pull, keeps the downloaded .zip file and skips extraction.')
632
- .option('--force', 'Forces the action, overriding any warnings or conflicts.')
633
- .option('--storage-file-path <storage-file-path>', 'Specifies a custom file storage path.')
634
- .description('Manages file storage, defaulting to file upload operations.')
639
+ .option('--force', 'Overwrites remote uploads or local downloads. Does not change selection.')
640
+ .option('--storage-id <sub-id>', 'Selects storage.<sub-id>.json. The default is storage.json.')
641
+ .option('--key <path>', 'Selects one exact manifest key within the path scope and filters.')
642
+ .option('--from-key <path>', 'Starts an inclusive range in manifest key order.')
643
+ .option('--to-key <path>', 'Ends an inclusive range. The default start is the first manifest key.')
644
+ .option('--key-regex <pattern>', 'Filters manifest keys by a JavaScript regular expression.')
645
+ .description('Uploads, pulls, or deletes Cloudinary assets and synchronizes the selected storage manifest.')
635
646
  .action(Underpost.fs.callback);
636
647
 
637
648
  program
@@ -734,7 +745,7 @@ program
734
745
  .option('--service-status', 'Reports whether the underpost-event unit is active and enabled.')
735
746
  .option('--list', 'Lists the registered events with their resolved probe targets.')
736
747
  .option('--port <port>', 'Listening port for --serve and the generated unit (default: 39099).')
737
- .option('--cooldown-ms <ms>', 'Minimum interval between two dispatches of one event in --serve (default: 300000).')
748
+ .option('--cooldown-ms <ms>', 'Minimum interval between two dispatches of one event in --serve (default: 10000).')
738
749
  .option(
739
750
  '--spoke <spoke-id>',
740
751
  'Spoke to remediate when dispatching wireguard-spoke-down by hand; a webhook takes it from the alert labels.',
@@ -1026,6 +1037,7 @@ program
1026
1037
  .option('--deploy-id <deploy-id>', 'Sets deploy id context for the runner execution.')
1027
1038
  .option('--user <user>', 'Sets user context for the runner execution.')
1028
1039
  .option('--hosts <hosts>', 'Comma-separated list of hosts for the runner execution.')
1040
+ .option('--split <mb>', 'Zip part size in MB for push-bundle, or "none" to upload a single zip. Defaults to 8.')
1029
1041
  .option('--instance-id <instance-id>', 'Sets instance id context for the runner execution.')
1030
1042
  .option('--pid <process-id>', 'Sets process id context for the runner execution.')
1031
1043
  .option(
@@ -547,7 +547,9 @@ class UnderpostRelease {
547
547
  logger.info(`CI push commit message: ${commitMsg}`);
548
548
  shellExec(`node engine/bin clone --bare ${githubOrg}/${repoName}`);
549
549
  shellCd('/home/dd/engine');
550
- shellExec(`node bin/build ${buildTarget}`);
550
+ // `--coverage` runs the suites the deploy's `docs.coverage` names so the published
551
+ // source carries the reports; without it the pod serves the unavailable page.
552
+ shellExec(`node bin/build ${buildTarget} --coverage`);
551
553
  shellCd('/home/dd/pwa-microservices-template');
552
554
  shellExec(`rm -rf ./.git`);
553
555
  shellExec(`mv ../${repoName}.git ./.git`);