cyberia 3.2.12 → 3.2.70

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 (194) hide show
  1. package/.env.example +127 -68
  2. package/.github/workflows/cyberia-client.cd.yml +40 -0
  3. package/.github/workflows/cyberia-server.cd.yml +40 -0
  4. package/.github/workflows/docker-image.cyberia-client.ci.yml +49 -0
  5. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +48 -0
  6. package/.github/workflows/docker-image.cyberia-server.ci.yml +69 -0
  7. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +69 -0
  8. package/.github/workflows/docker-image.engine-cyberia.ci.yml +52 -0
  9. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +52 -0
  10. package/.github/workflows/engine-cyberia.cd.yml +34 -24
  11. package/.github/workflows/engine-cyberia.ci.yml +27 -5
  12. package/.github/workflows/ghpkg.ci.yml +89 -1
  13. package/.github/workflows/gitlab.ci.yml +1 -1
  14. package/.github/workflows/hardhat.ci.yml +1 -1
  15. package/.github/workflows/npmpkg.ci.yml +19 -12
  16. package/.github/workflows/publish.ci.yml +2 -2
  17. package/.github/workflows/publish.cyberia.ci.yml +5 -16
  18. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -8
  19. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  20. package/CHANGELOG.md +451 -1
  21. package/CLI-HELP.md +1040 -1130
  22. package/Dockerfile +141 -43
  23. package/Dockerfile.dev +143 -0
  24. package/Dockerfile.test +165 -0
  25. package/README.md +1 -1
  26. package/baremetal/commission-workflows.json +1 -0
  27. package/bin/build.js +128 -136
  28. package/bin/build.template.js +25 -179
  29. package/bin/cyberia.js +1089 -188
  30. package/bin/deploy.js +6 -3
  31. package/bin/index.js +1089 -188
  32. package/bump.config.js +1 -0
  33. package/compose.env +131 -0
  34. package/conf.js +19 -1
  35. package/deployment.yaml +74 -2
  36. package/docker-compose.yml +316 -0
  37. package/hardhat/hardhat.config.js +2 -2
  38. package/hardhat/package-lock.json +620 -2041
  39. package/hardhat/package.json +7 -5
  40. package/hardhat/scripts/deployObjectLayerToken.js +18 -18
  41. package/hardhat/test/ObjectLayerToken.js +378 -274
  42. package/ipfs/configure-ipfs.sh +13 -0
  43. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  44. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  45. package/manifests/deployment/dd-cyberia-development/deployment.yaml +74 -2
  46. package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
  47. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
  48. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  49. package/mongodb/entrypoint.sh +76 -0
  50. package/nginx.conf +87 -0
  51. package/package.json +34 -22
  52. package/pv-pvc.yaml +32 -0
  53. package/scripts/disk-clean.sh +85 -60
  54. package/scripts/kubeadm-node-setup.sh +317 -0
  55. package/scripts/link-local-underpost-cli.sh +6 -0
  56. package/scripts/rocky-kickstart.sh +877 -185
  57. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  58. package/scripts/test-monitor.sh +248 -0
  59. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
  60. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
  61. package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
  62. package/src/api/cyberia-action/cyberia-action.model.js +21 -29
  63. package/src/api/cyberia-action/cyberia-action.router.js +42 -7
  64. package/src/api/cyberia-action/cyberia-action.service.js +20 -4
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
  66. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
  67. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
  68. package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
  69. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
  70. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
  71. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
  72. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
  73. package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
  74. package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
  75. package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
  76. package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
  77. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
  78. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
  79. package/src/api/cyberia-map/cyberia-map.router.js +21 -6
  80. package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
  81. package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
  82. package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
  83. package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
  84. package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
  85. package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
  86. package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
  87. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -122
  88. package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
  89. package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
  90. package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
  91. package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
  92. package/src/api/ipfs/ipfs.service.js +28 -15
  93. package/src/api/object-layer/object-layer.router.js +25 -15
  94. package/src/api/object-layer/object-layer.service.js +15 -20
  95. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
  96. package/src/cli/baremetal.js +717 -16
  97. package/src/cli/cluster.js +80 -5
  98. package/src/cli/deploy.js +326 -292
  99. package/src/cli/docker-compose.js +648 -0
  100. package/src/cli/env.js +12 -6
  101. package/src/cli/fs.js +75 -30
  102. package/src/cli/image.js +187 -55
  103. package/src/cli/index.js +157 -6
  104. package/src/cli/kickstart.js +142 -20
  105. package/src/cli/monitor.js +387 -6
  106. package/src/cli/release.js +72 -15
  107. package/src/cli/repository.js +339 -40
  108. package/src/cli/run.js +678 -186
  109. package/src/cli/secrets.js +82 -48
  110. package/src/cli/ssh.js +234 -0
  111. package/src/cli/static.js +2 -2
  112. package/src/client/components/core/PanelForm.js +44 -44
  113. package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
  114. package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
  115. package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
  116. package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
  117. package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
  118. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
  119. package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
  120. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
  121. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
  122. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
  123. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +92 -20
  124. package/src/client/public/cyberia-docs/ARCHITECTURE.md +272 -50
  125. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
  126. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
  127. package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
  128. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
  129. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +28 -12
  130. package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
  131. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +27 -5
  132. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  133. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  134. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
  135. package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
  136. package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
  137. package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
  138. package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
  139. package/src/client/services/object-layer/object-layer.management.js +6 -6
  140. package/src/{server → client-builder}/client-build-docs.js +15 -5
  141. package/src/{server → client-builder}/client-build-live.js +3 -3
  142. package/src/{server → client-builder}/client-build.js +25 -22
  143. package/src/{server → client-builder}/client-dev-server.js +3 -3
  144. package/src/{server → client-builder}/client-icons.js +2 -2
  145. package/src/{server → client-builder}/ssr.js +5 -5
  146. package/src/client.build.js +1 -3
  147. package/src/client.dev.js +1 -1
  148. package/src/db/mongo/MongoBootstrap.js +12 -12
  149. package/src/db/mongo/MongooseDB.js +2 -1
  150. package/src/grpc/cyberia/grpc-server.js +255 -70
  151. package/src/index.js +12 -1
  152. package/src/mailer/EmailRender.js +1 -1
  153. package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
  154. package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
  155. package/src/projects/cyberia/catalog-cyberia.js +81 -0
  156. package/src/projects/cyberia/gemini-client.js +175 -0
  157. package/src/projects/cyberia/generate-saga.js +2107 -0
  158. package/src/{server → projects/cyberia}/ipfs-client.js +6 -4
  159. package/src/{server → projects/cyberia}/object-layer.js +12 -108
  160. package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
  161. package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
  162. package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
  163. package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
  164. package/src/{server → projects/cyberia}/shape-generator.js +2 -2
  165. package/src/projects/underpost/catalog-underpost.js +60 -0
  166. package/src/runtime/cyberia-client/Dockerfile +31 -83
  167. package/src/runtime/cyberia-client/Dockerfile.dev +23 -30
  168. package/src/runtime/cyberia-server/Dockerfile +23 -42
  169. package/src/runtime/cyberia-server/Dockerfile.dev +20 -35
  170. package/src/runtime/engine-cyberia/Dockerfile +143 -0
  171. package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
  172. package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
  173. package/src/runtime/engine-cyberia/compose.env +131 -0
  174. package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
  175. package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
  176. package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
  177. package/src/runtime/engine-cyberia/nginx.conf +87 -0
  178. package/src/runtime/express/Express.js +2 -2
  179. package/src/runtime/nginx/Nginx.js +250 -0
  180. package/src/runtime/wp/Dockerfile +3 -3
  181. package/src/server/catalog.js +72 -0
  182. package/src/server/conf.js +415 -60
  183. package/src/server/runtime-status.js +252 -0
  184. package/src/server/start.js +42 -11
  185. package/src/server.js +6 -2
  186. package/test/cyberia-instance-conf-defaults.test.js +140 -0
  187. package/test/deploy-monitor.test.js +267 -0
  188. package/test/shape-generator.test.js +7 -1
  189. package/typedoc.dd-cyberia.json +3 -1
  190. package/typedoc.json +3 -1
  191. package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
  192. package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
  193. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  194. /package/src/{server → client-builder}/client-formatted.js +0 -0
@@ -0,0 +1,648 @@
1
+ /**
2
+ * @description Docker Compose pipeline CLI for the local development stack that
3
+ * mirrors the Kubernetes manifests under `manifests/`. Manages dynamic
4
+ * generation of supporting config (nginx router, env-file) and the compose
5
+ * lifecycle (up/down/logs/...). General-purpose: any compose subcommand can be
6
+ * forwarded via `--exec`.
7
+ * @module src/cli/docker-compose.js
8
+ * @namespace UnderpostDockerCompose
9
+ */
10
+
11
+ import fs from 'fs-extra';
12
+ import nodePath from 'path';
13
+ import { getRootDirectory, shellExec } from '../server/process.js';
14
+ import { loggerFactory } from '../server/logger.js';
15
+ import Nginx from '../runtime/nginx/Nginx.js';
16
+
17
+ const logger = loggerFactory(import.meta);
18
+
19
+ /**
20
+ * Reverse-proxy route table derived from
21
+ * manifests/deployment/dd-default-development/proxy.yaml (Contour HTTPProxy).
22
+ * Longer prefixes precede '/' so nginx matches them first.
23
+ * @constant PROXY_HOSTS
24
+ * @memberof UnderpostDockerCompose
25
+ */
26
+ const PROXY_HOSTS = [
27
+ {
28
+ host: 'default.net',
29
+ routes: [
30
+ { location: '/peer', service: 'app', port: 4002 },
31
+ { location: '/', service: 'app', port: 4001 },
32
+ ],
33
+ },
34
+ {
35
+ host: 'www.default.net',
36
+ routes: [{ location: '/', service: 'app', port: 4003 }],
37
+ },
38
+ ];
39
+
40
+ /** Fallback upstream for unmatched Host headers and the proxy healthcheck. */
41
+ const DEFAULT_UPSTREAM = { service: 'app', port: 4001 };
42
+
43
+ /** Prometheus scrape target: the app's /metrics endpoint on its base port. */
44
+ const METRICS_TARGET = { service: 'app', port: 4001 };
45
+
46
+ /** Default deployment identifier (the self-bootstrapping engine deploy). */
47
+ const DEFAULT_DEPLOY_ID = 'dd-default';
48
+
49
+ /**
50
+ * Generated artifact paths (relative to repo root) that `--generate` produces
51
+ * and `--reset` prunes. The working env-file (docker/compose.env) is excluded
52
+ * so a reset never destroys configured credentials.
53
+ */
54
+ const GENERATED_ARTIFACTS = [
55
+ 'docker/mongodb/entrypoint.sh',
56
+ 'docker/nginx/default.conf',
57
+ 'docker/prometheus/prometheus.yml',
58
+ 'docker/grafana/provisioning',
59
+ 'docker/compose.app.yml',
60
+ 'docker/compose.env.example',
61
+ ];
62
+
63
+ /**
64
+ * @class UnderpostDockerCompose
65
+ * @description Docker Compose development pipeline. A single {@link UnderpostDockerCompose.API.callback}
66
+ * dispatches actions based on flag options, mirroring the cluster CLI pattern.
67
+ * @memberof UnderpostDockerCompose
68
+ */
69
+ class UnderpostDockerCompose {
70
+ /**
71
+ * Resolves a repo-root-relative path to an absolute path.
72
+ * @param {string} relPath - Path relative to the engine root.
73
+ * @returns {string} Absolute path.
74
+ * @memberof UnderpostDockerCompose
75
+ */
76
+ static resolve(relPath) {
77
+ return nodePath.isAbsolute(relPath) ? relPath : nodePath.join(getRootDirectory(), relPath);
78
+ }
79
+
80
+ /**
81
+ * Resolves the canonical directory for a custom docker-compose workflow,
82
+ * keyed by `--deploy-id` + `--docker-compose-id`:
83
+ * `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>`.
84
+ * This directory ships its own `docker-compose.yml`, `compose.env`, and
85
+ * `nginx.conf` (used as-is, never generated). Returns null when
86
+ * `--docker-compose-id` is not set (the default, self-generating workflow).
87
+ * @param {object} options - CLI options.
88
+ * @returns {string|null} Repo-root-relative canonical dir, or null.
89
+ * @memberof UnderpostDockerCompose
90
+ */
91
+ static composeIdBase(options = {}) {
92
+ if (!options.dockerComposeId) return null;
93
+ const deployId = options.deployId || DEFAULT_DEPLOY_ID;
94
+ return `engine-private/conf/${deployId}/docker-compose/${options.dockerComposeId}`;
95
+ }
96
+
97
+ /**
98
+ * Builds the base `docker compose` invocation with explicit file and env-file,
99
+ * so behavior is independent of the caller's working directory.
100
+ *
101
+ * Custom workflow (`--docker-compose-id`): the compose file, env-file, and
102
+ * bind-mounted config (nginx.conf, mongodb/) all live in the canonical dir, so
103
+ * compose runs with `--project-directory` pinned there and no app-override.
104
+ * @param {object} options - CLI options.
105
+ * @returns {string} The base command string (without a subcommand).
106
+ * @memberof UnderpostDockerCompose
107
+ */
108
+ static baseCmd(options = {}) {
109
+ const base = UnderpostDockerCompose.composeIdBase(options);
110
+ if (base) {
111
+ const projectDir = UnderpostDockerCompose.resolve(base);
112
+ const composeFile = UnderpostDockerCompose.resolve(options.composeFile || `${base}/docker-compose.yml`);
113
+ const envFile = UnderpostDockerCompose.resolve(options.envFile || `${base}/compose.env`);
114
+ return `docker compose --project-directory ${projectDir} --env-file ${envFile} -f ${composeFile}`;
115
+ }
116
+ const composeFile = UnderpostDockerCompose.resolve(options.composeFile || 'docker-compose.yml');
117
+ const envFile = UnderpostDockerCompose.resolve(options.envFile || 'docker/compose.env');
118
+ const overrideFile = UnderpostDockerCompose.resolve(options.appOverride || 'docker/compose.app.yml');
119
+ const overrideFlag = fs.existsSync(overrideFile) ? ` -f ${overrideFile}` : '';
120
+ return `docker compose --env-file ${envFile} -f ${composeFile}${overrideFlag}`;
121
+ }
122
+
123
+ /**
124
+ * Resolves the app container start command for a deployment, mirroring
125
+ * src/cli/deploy.js. The `dd-default` deploy self-bootstraps a fresh engine
126
+ * (matching manifests/deployment/dd-default-development); any other deploy-id
127
+ * follows the standard deploy command (matching
128
+ * manifests/deployment/dd-test-development and
129
+ * UnderpostDeploy.deploymentYamlPartsFactory's default cmd).
130
+ * @param {string} deployId - Deployment identifier (conf id, e.g. `dd-test`).
131
+ * @param {string} env - Deployment environment (e.g. `development`).
132
+ * @returns {string[]} Ordered shell steps joined with `&&` at render time.
133
+ * @memberof UnderpostDockerCompose
134
+ */
135
+ static appCommand(deployId = DEFAULT_DEPLOY_ID, env = 'development') {
136
+ if (deployId === DEFAULT_DEPLOY_ID)
137
+ return [
138
+ // `$$` escapes to a literal `$` so /bin/sh (not Compose) performs the
139
+ // command/variable substitution at container runtime.
140
+ 'cd $$(underpost root)/underpost',
141
+ 'node bin new --default-conf --conf-workflow-id template',
142
+ // Point the template .env.example at the Docker service-discovery hosts
143
+ // before `new engine` copies it. The generated dd-engine/.env.development
144
+ // is seeded from this file and is applied over the process env by
145
+ // loadConf (src/server/conf.js), so without this the engine would fall
146
+ // back to the .env.example localhost defaults for DB_HOST/VALKEY_HOST.
147
+ 'sed -i "s#^DB_HOST=.*#DB_HOST=$${DB_HOST}#" .env.example',
148
+ 'sed -i "s#^VALKEY_HOST=.*#VALKEY_HOST=$${VALKEY_HOST}#" .env.example',
149
+ 'mkdir -p /home/dd',
150
+ 'cd /home/dd',
151
+ 'underpost new engine',
152
+ ];
153
+ return ['underpost secret underpost --create-from-env', `underpost start --build --run ${deployId} ${env}`];
154
+ }
155
+
156
+ /**
157
+ * Renders a Compose override file that sets only the `app` service command
158
+ * for the selected deployment. Keeping the command in an override file makes
159
+ * the generator the single source of truth and leaves docker-compose.yml
160
+ * deployment-agnostic.
161
+ * @param {string} deployId - Deployment identifier.
162
+ * @param {string} env - Deployment environment.
163
+ * @returns {string} The override YAML document.
164
+ * @memberof UnderpostDockerCompose
165
+ */
166
+ static appOverrideContent(deployId = DEFAULT_DEPLOY_ID, env = 'development') {
167
+ const steps = UnderpostDockerCompose.appCommand(deployId, env).join(' &&\n ');
168
+ return `# Generated by 'underpost docker-compose --generate --deploy-id ${deployId}' — do not hand-edit.
169
+ # Overrides the app service start command for deploy '${deployId}' (${env}).
170
+ services:
171
+ app:
172
+ command:
173
+ - /bin/sh
174
+ - -c
175
+ - >
176
+ ${steps}
177
+ `;
178
+ }
179
+
180
+ /**
181
+ * Renders the dynamic env-file example content for the compose stack.
182
+ * @returns {string} The env-file template.
183
+ * @memberof UnderpostDockerCompose
184
+ */
185
+ static envExampleContent() {
186
+ return `# Generated by 'underpost docker-compose --generate' — copy to docker/compose.env.
187
+ # Loaded explicitly via --env-file so container service-discovery values are not
188
+ # overridden by the application's host-local (127.0.0.1) root .env.
189
+
190
+ # --- Container images ----------------------------------------------------
191
+ MONGO_IMAGE=mongo:latest
192
+ VALKEY_IMAGE=valkey/valkey:latest
193
+ APP_IMAGE=underpost/underpost-engine
194
+ APP_TAG=v3.2.70
195
+ PROXY_IMAGE=nginx:stable-alpine
196
+ PROMETHEUS_IMAGE=prom/prometheus:latest
197
+ GRAFANA_IMAGE=grafana/grafana:latest
198
+
199
+ # --- MongoDB (translation of Secret mongodb-secret) ----------------------
200
+ MONGO_INITDB_ROOT_USERNAME=admin
201
+ MONGO_INITDB_ROOT_PASSWORD=changeme
202
+
203
+ # --- Application DB connection (Docker service discovery, not localhost) --
204
+ NODE_ENV=development
205
+ DB_PROVIDER=mongoose
206
+ DB_HOST=mongodb://mongodb:27017
207
+ DB_NAME=default
208
+ DB_REPLICA_SET=rs0
209
+ DB_AUTH_SOURCE=admin
210
+
211
+ # --- Valkey (Docker service discovery) -----------------------------------
212
+ VALKEY_HOST=valkey-service
213
+ VALKEY_PORT=6379
214
+
215
+ # --- Monitoring ----------------------------------------------------------
216
+ GRAFANA_ADMIN_USER=admin
217
+ GRAFANA_ADMIN_PASSWORD=admin
218
+
219
+ # --- Host-published ports ------------------------------------------------
220
+ PROXY_HTTP_PORT=80
221
+ MONGO_HOST_PORT=27017
222
+ VALKEY_NODEPORT=32079
223
+ PROMETHEUS_PORT=9090
224
+ GRAFANA_PORT=3000
225
+ APP_PORT_4001=4001
226
+ APP_PORT_4002=4002
227
+ APP_PORT_4003=4003
228
+ APP_PORT_4004=4004
229
+ `;
230
+ }
231
+
232
+ /**
233
+ * Renders the MongoDB container entrypoint. Mirrors src/db/mongo/MongoBootstrap.js:
234
+ * generates the cluster-auth keyfile, launches mongod (replSet + keyFile + auth),
235
+ * and bootstraps the replica set + root user over the loopback localhost
236
+ * exception (single-node member is the `mongodb` service name so app clients
237
+ * connecting with replicaSet=rs0 resolve a reachable host). Idempotent across
238
+ * restarts: re-runs are no-ops once auth is enforced.
239
+ * @returns {string} entrypoint.sh content.
240
+ * @memberof UnderpostDockerCompose
241
+ */
242
+ static mongoEntrypointContent() {
243
+ return `#!/usr/bin/env bash
244
+ # Generated by 'underpost docker-compose --generate' — do not hand-edit.
245
+ set -e
246
+
247
+ KEYFILE=/opt/keyfile/mongodb-keyfile
248
+ RS="\${DB_REPLICA_SET:-rs0}"
249
+ MEMBER_HOST="mongodb:27017"
250
+
251
+ if [ ! -s "$KEYFILE" ]; then
252
+ openssl rand -base64 756 > "$KEYFILE"
253
+ fi
254
+ chmod 400 "$KEYFILE"
255
+ chown 999:999 "$KEYFILE"
256
+ mkdir -p /data/db
257
+ chown -R 999:999 /data/db
258
+
259
+ # One-time replica-set + root-user bootstrap via the localhost exception,
260
+ # performed in the background once mongod accepts loopback connections.
261
+ (
262
+ for i in $(seq 1 60); do
263
+ if mongosh --quiet --host 127.0.0.1 --eval 'db.adminCommand({ ping: 1 })' >/dev/null 2>&1; then
264
+ break
265
+ fi
266
+ sleep 1
267
+ done
268
+
269
+ cat > /tmp/mongo-bootstrap.js <<JS
270
+ var RS = "$RS";
271
+ var HOST = "$MEMBER_HOST";
272
+ var U = "$MONGO_INITDB_ROOT_USERNAME";
273
+ var P = "$MONGO_INITDB_ROOT_PASSWORD";
274
+ function waitPrimary() {
275
+ for (var i = 0; i < 60; i++) {
276
+ var h = db.hello();
277
+ if (h.isWritablePrimary) return true;
278
+ sleep(1000);
279
+ }
280
+ return false;
281
+ }
282
+ var initialized = false;
283
+ try {
284
+ var s = rs.status();
285
+ if (s && s.ok === 1) initialized = true;
286
+ } catch (e) {
287
+ var m = String(e);
288
+ if (m.indexOf("requires authentication") >= 0 || m.indexOf("not authorized") >= 0 || m.indexOf("Unauthorized") >= 0) {
289
+ initialized = true;
290
+ } else if (m.indexOf("NotYetInitialized") < 0 && m.indexOf("no replset config") < 0) {
291
+ throw e;
292
+ }
293
+ }
294
+ if (!initialized) {
295
+ rs.initiate({ _id: RS, members: [{ _id: 0, host: HOST }] });
296
+ waitPrimary();
297
+ var admin = db.getSiblingDB("admin");
298
+ try {
299
+ admin.createUser({ user: U, pwd: P, roles: [{ role: "root", db: "admin" }] });
300
+ print("BOOTSTRAP_USER_CREATED");
301
+ } catch (e) {
302
+ var s2 = String(e);
303
+ if (s2.indexOf("already exists") < 0 && s2.indexOf("not authorized") < 0 && s2.indexOf("requires authentication") < 0) {
304
+ throw e;
305
+ }
306
+ print("BOOTSTRAP_USER_SKIP");
307
+ }
308
+ print("BOOTSTRAP_DONE");
309
+ } else {
310
+ print("BOOTSTRAP_ALREADY_INITIALIZED");
311
+ }
312
+ JS
313
+
314
+ mongosh --quiet --host 127.0.0.1 /tmp/mongo-bootstrap.js > /tmp/mongo-bootstrap.log 2>&1 || true
315
+ ) &
316
+
317
+ exec gosu mongodb mongod \\
318
+ --replSet "$RS" --auth --clusterAuthMode keyFile --keyFile "$KEYFILE" --bind_ip_all
319
+ `;
320
+ }
321
+
322
+ /**
323
+ * Renders the Prometheus scrape config (mirrors manifests/prometheus).
324
+ * @returns {string} prometheus.yml content.
325
+ * @memberof UnderpostDockerCompose
326
+ */
327
+ static prometheusContent() {
328
+ return `# Generated by 'underpost docker-compose --generate' — do not hand-edit.
329
+ global:
330
+ scrape_interval: 30s
331
+ evaluation_interval: 30s
332
+
333
+ scrape_configs:
334
+ - job_name: 'underpost-app'
335
+ metrics_path: /metrics
336
+ scheme: http
337
+ static_configs:
338
+ - targets: ['${METRICS_TARGET.service}:${METRICS_TARGET.port}']
339
+ - job_name: 'prometheus'
340
+ static_configs:
341
+ - targets: ['localhost:9090']
342
+ `;
343
+ }
344
+
345
+ /**
346
+ * Renders the Grafana datasource provisioning file wiring the Prometheus
347
+ * service as the default datasource.
348
+ * @returns {string} datasource.yml content.
349
+ * @memberof UnderpostDockerCompose
350
+ */
351
+ static grafanaDatasourceContent() {
352
+ return `# Generated by 'underpost docker-compose --generate' — do not hand-edit.
353
+ apiVersion: 1
354
+ datasources:
355
+ - name: Prometheus
356
+ type: prometheus
357
+ access: proxy
358
+ url: http://prometheus:9090
359
+ isDefault: true
360
+ editable: true
361
+ `;
362
+ }
363
+
364
+ /**
365
+ * Renders all dynamic supporting files: the nginx reverse-proxy config (from
366
+ * PROXY_HOSTS), the monitoring configs (Prometheus + Grafana datasource), and
367
+ * the env-file example. Creates a working env-file from the example only when
368
+ * one does not already exist (never overwrites credentials).
369
+ * @param {object} options - CLI options.
370
+ * @returns {void}
371
+ * @memberof UnderpostDockerCompose
372
+ */
373
+ static generate(options = {}) {
374
+ // Custom workflow: docker-compose.yml, compose.env, and nginx.conf are
375
+ // pre-authored in the canonical dir and used as-is — do NOT generate them.
376
+ // Only (re)write the required MongoDB entrypoint (replica-set bootstrap),
377
+ // the single generated infra artifact, into <canonical>/mongodb so the
378
+ // stack stays self-contained under `--project-directory`.
379
+ const composeIdBase = UnderpostDockerCompose.composeIdBase(options);
380
+ if (composeIdBase) {
381
+ const mongoEntrypointPath = UnderpostDockerCompose.resolve(`${composeIdBase}/mongodb/entrypoint.sh`);
382
+ fs.mkdirpSync(nodePath.dirname(mongoEntrypointPath));
383
+ fs.writeFileSync(mongoEntrypointPath, UnderpostDockerCompose.mongoEntrypointContent(), { mode: 0o755 });
384
+ logger.info('mongodb entrypoint written (custom workflow)', { path: mongoEntrypointPath });
385
+ return;
386
+ }
387
+
388
+ Nginx.removeRouter();
389
+ for (const { host, routes } of PROXY_HOSTS) Nginx.createApp({ host, routes });
390
+ Nginx.createDefaultServer(DEFAULT_UPSTREAM);
391
+ Nginx.writeConf(UnderpostDockerCompose.resolve(options.nginxConf || 'docker/nginx/default.conf'));
392
+
393
+ const mongoEntrypointPath = UnderpostDockerCompose.resolve('docker/mongodb/entrypoint.sh');
394
+ fs.mkdirpSync(nodePath.dirname(mongoEntrypointPath));
395
+ fs.writeFileSync(mongoEntrypointPath, UnderpostDockerCompose.mongoEntrypointContent(), { mode: 0o755 });
396
+ logger.info('mongodb entrypoint written', { path: mongoEntrypointPath });
397
+
398
+ const promPath = UnderpostDockerCompose.resolve('docker/prometheus/prometheus.yml');
399
+ fs.mkdirpSync(nodePath.dirname(promPath));
400
+ fs.writeFileSync(promPath, UnderpostDockerCompose.prometheusContent(), 'utf8');
401
+ const grafanaDsPath = UnderpostDockerCompose.resolve('docker/grafana/provisioning/datasources/datasource.yml');
402
+ fs.mkdirpSync(nodePath.dirname(grafanaDsPath));
403
+ fs.writeFileSync(grafanaDsPath, UnderpostDockerCompose.grafanaDatasourceContent(), 'utf8');
404
+ logger.info('monitoring config written', { prometheus: promPath, grafanaDatasource: grafanaDsPath });
405
+
406
+ const examplePath = UnderpostDockerCompose.resolve('docker/compose.env.example');
407
+ fs.mkdirpSync(nodePath.dirname(examplePath));
408
+ fs.writeFileSync(examplePath, UnderpostDockerCompose.envExampleContent(), 'utf8');
409
+ logger.info('env example written', { path: examplePath });
410
+
411
+ const envPath = UnderpostDockerCompose.resolve(options.envFile || 'docker/compose.env');
412
+ if (!fs.existsSync(envPath)) {
413
+ fs.copySync(examplePath, envPath);
414
+ logger.warn('created working env-file from example — set real credentials', { path: envPath });
415
+ } else logger.info('working env-file already present (left untouched)', { path: envPath });
416
+
417
+ const deployId = options.deployId || DEFAULT_DEPLOY_ID;
418
+ const env = options.env || 'development';
419
+ const overridePath = UnderpostDockerCompose.resolve(options.appOverride || 'docker/compose.app.yml');
420
+ fs.writeFileSync(overridePath, UnderpostDockerCompose.appOverrideContent(deployId, env), 'utf8');
421
+ logger.info('app command override written', { path: overridePath, deployId, env });
422
+ }
423
+
424
+ /**
425
+ * Installs Docker Engine and the Compose v2 plugin on RHEL-compatible hosts
426
+ * (Rocky Linux) from the official Docker CE repository. Idempotent and
427
+ * host-safe: skips installation when `docker compose` already works (unless
428
+ * `--force`), validates the platform, enables the service, and adds the
429
+ * invoking user to the `docker` group.
430
+ * @param {object} options - CLI options.
431
+ * @param {boolean} [options.force] - Reinstall even if Compose is already present.
432
+ * @returns {void}
433
+ * @memberof UnderpostDockerCompose
434
+ */
435
+ static install(options = {}) {
436
+ if (process.platform !== 'linux') {
437
+ logger.warn('docker-compose --install only supports Linux (RHEL/Rocky); skipping', {
438
+ platform: process.platform,
439
+ });
440
+ return;
441
+ }
442
+
443
+ if (!fs.existsSync('/etc/redhat-release')) {
444
+ logger.warn('Host does not look RHEL-compatible (/etc/redhat-release missing); proceeding with dnf anyway');
445
+ }
446
+
447
+ if (!options.force) {
448
+ const probe = shellExec('docker compose version', { silent: true, silentOnError: true });
449
+ if (probe && probe.code === 0) {
450
+ logger.info('Docker Compose already installed; skipping (use --force to reinstall)', {
451
+ version: (probe.stdout || '').trim(),
452
+ });
453
+ return;
454
+ }
455
+ }
456
+
457
+ shellExec(`sudo dnf -y install dnf-plugins-core`);
458
+ shellExec(`sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo`);
459
+ shellExec(`sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin`);
460
+
461
+ shellExec(`sudo systemctl enable --now docker`);
462
+
463
+ const user = options.user || process.env.SUDO_USER || process.env.USER || '';
464
+ if (user && user !== 'root') {
465
+ shellExec(`sudo groupadd docker 2>/dev/null || true`);
466
+ shellExec(`sudo usermod -aG docker ${user}`);
467
+ logger.info(`Added '${user}' to the docker group — log out/in (or run 'newgrp docker') to use docker rootless`);
468
+ }
469
+
470
+ const verify = shellExec('docker compose version', { silent: true, silentOnError: true, stdout: true });
471
+ logger.info('Docker Compose installation complete', { version: `${verify || ''}`.trim() });
472
+ }
473
+
474
+ /**
475
+ * Comprehensive reset of the Docker Compose stack — the compose equivalent of
476
+ * `cluster --reset` in src/cli/cluster.js. Tears down all stack containers,
477
+ * the project network, and the named volumes (destroying persisted MongoDB /
478
+ * Valkey / Prometheus / Grafana data), removes orphans, and prunes generated
479
+ * artifacts so the next `--up` is a clean slate. The working env-file
480
+ * (credentials) is preserved unless `--force` is passed.
481
+ * @param {object} options - CLI options.
482
+ * @param {boolean} [options.force] - Also remove the working env-file (docker/compose.env).
483
+ * @returns {void}
484
+ * @memberof UnderpostDockerCompose
485
+ */
486
+ static reset(options = {}) {
487
+ logger.info('=== DOCKER COMPOSE RESET (destroys containers, network, and volume data) ===');
488
+
489
+ // Phase 1: tear down containers, orphans, named volumes, and compose-built images.
490
+ shellExec(`${UnderpostDockerCompose.baseCmd(options)} down --remove-orphans --volumes --rmi local`, {
491
+ silentOnError: true,
492
+ });
493
+
494
+ // Custom workflow: the compose file, compose.env, and nginx.conf are
495
+ // hand-authored source — never prune them. Only drop the one generated
496
+ // artifact (the mongo entrypoint), regenerated on the next --up/--generate.
497
+ const composeIdBase = UnderpostDockerCompose.composeIdBase(options);
498
+ if (composeIdBase) {
499
+ const mongoEntrypointPath = UnderpostDockerCompose.resolve(`${composeIdBase}/mongodb/entrypoint.sh`);
500
+ if (fs.existsSync(mongoEntrypointPath)) {
501
+ fs.removeSync(mongoEntrypointPath);
502
+ logger.info('removed generated artifact', { path: mongoEntrypointPath });
503
+ }
504
+ logger.info('Docker Compose reset complete. Run `--up` to recreate the stack.');
505
+ return;
506
+ }
507
+
508
+ // Phase 2: prune generated artifacts (regenerated on the next --generate/--up).
509
+ const artifacts = options.force
510
+ ? [...GENERATED_ARTIFACTS, options.envFile || 'docker/compose.env']
511
+ : GENERATED_ARTIFACTS;
512
+ for (const rel of artifacts) {
513
+ const target = UnderpostDockerCompose.resolve(rel);
514
+ if (fs.existsSync(target)) {
515
+ fs.removeSync(target);
516
+ logger.info('removed generated artifact', { path: target });
517
+ }
518
+ }
519
+
520
+ logger.info('Docker Compose reset complete. Run `--up` to recreate the stack.');
521
+ }
522
+
523
+ static API = {
524
+ /**
525
+ * @method callback
526
+ * @description Single CLI entrypoint for the docker-compose pipeline. The
527
+ * action is selected by flag options; multiple lifecycle flags may be
528
+ * combined (e.g. `--generate --up`). When no action flag is supplied it
529
+ * defaults to `--up`.
530
+ * @param {string} [target] - Optional service name (logs/shell/restart) or
531
+ * passthrough subcommand context.
532
+ * @param {object} [options] - CLI options.
533
+ * @param {boolean} [options.install] - Install Docker + Compose on RHEL/Rocky hosts.
534
+ * @param {boolean} [options.reset] - Comprehensive teardown of the whole stack (containers, network, volumes) + prune generated artifacts.
535
+ * @param {boolean} [options.force] - Force reinstall (--install), remove volumes (--down), or also drop the env-file (--reset).
536
+ * @param {boolean} [options.generate] - Render nginx config + env-file.
537
+ * @param {boolean} [options.up] - Start the full stack detached (implies generate).
538
+ * @param {boolean} [options.down] - Stop and remove containers.
539
+ * @param {boolean} [options.volumes] - With --down, also remove named volumes (destroys data).
540
+ * @param {boolean} [options.restart] - Restart services (optionally a single `target`).
541
+ * @param {boolean} [options.build] - With --up rebuild images; alone, `build --no-cache`.
542
+ * @param {boolean} [options.pull] - Pull upstream images.
543
+ * @param {boolean} [options.logs] - Follow logs (optionally a single `target`).
544
+ * @param {boolean} [options.status] - Show a formatted status table.
545
+ * @param {boolean} [options.shell] - Open an interactive shell in `target` (default: app).
546
+ * @param {string} [options.exec] - General-purpose passthrough docker compose subcommand.
547
+ * @param {string} [options.deployId] - Deployment to run as the app (default: dd-default). `dd-default` self-bootstraps a fresh engine; any other id runs the standard `underpost start` command.
548
+ * @param {string} [options.dockerComposeId] - Custom-workflow selector. When set, use the canonical stack at `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>/` (docker-compose.yml + compose.env + nginx.conf, used as-is), skipping nginx/env/app-override generation. Used by the Cyberia MMO ecosystem (`--deploy-id dd-cyberia --docker-compose-id cyberia`).
549
+ * @param {string} [options.env] - Deployment environment for non-default deploy ids (default: development).
550
+ * @param {string} [options.composeFile] - Override compose file path.
551
+ * @param {string} [options.envFile] - Override env-file path.
552
+ * @param {string} [options.nginxConf] - Override generated nginx config path.
553
+ * @param {string} [options.appOverride] - Override generated app-command override path.
554
+ * @returns {Promise<void>}
555
+ * @memberof UnderpostDockerCompose
556
+ */
557
+ async callback(target = '', options = {}) {
558
+ try {
559
+ if (options.install) {
560
+ UnderpostDockerCompose.install(options);
561
+ const onlyInstall = !options.up && !options.down && !options.generate && !options.exec;
562
+ if (onlyInstall) return;
563
+ }
564
+
565
+ if (options.reset) {
566
+ UnderpostDockerCompose.reset(options);
567
+ const onlyReset = !options.up && !options.generate;
568
+ if (onlyReset) return;
569
+ }
570
+
571
+ // "Bring up" is the explicit --up or the no-action default.
572
+ const isUp =
573
+ options.up ||
574
+ !(
575
+ options.exec ||
576
+ options.down ||
577
+ options.restart ||
578
+ options.build ||
579
+ options.pull ||
580
+ options.logs ||
581
+ options.status ||
582
+ options.shell ||
583
+ options.generate
584
+ );
585
+
586
+ // Generate dynamic config BEFORE composing the invocation: baseCmd()
587
+ // conditionally includes `-f compose.app.yml`, so the override must
588
+ // exist first (notably after --reset, which prunes it).
589
+ if (isUp || options.generate) UnderpostDockerCompose.generate(options);
590
+
591
+ const base = UnderpostDockerCompose.baseCmd(options);
592
+
593
+ if (options.exec) {
594
+ shellExec(`${base} ${options.exec}`);
595
+ return;
596
+ }
597
+
598
+ if (isUp) {
599
+ shellExec(`${base} up -d${options.build ? ' --build' : ''}`);
600
+ return;
601
+ }
602
+
603
+ if (options.down) {
604
+ shellExec(`${base} down --remove-orphans${options.volumes ? ' --volumes' : ''}`);
605
+ return;
606
+ }
607
+
608
+ if (options.restart) {
609
+ shellExec(`${base} restart${target ? ` ${target}` : ''}`);
610
+ return;
611
+ }
612
+
613
+ if (options.build) {
614
+ shellExec(`${base} build --no-cache${target ? ` ${target}` : ''}`);
615
+ return;
616
+ }
617
+
618
+ if (options.pull) {
619
+ shellExec(`${base} pull`);
620
+ return;
621
+ }
622
+
623
+ if (options.logs) {
624
+ shellExec(`${base} logs -f --tail=200${target ? ` ${target}` : ''}`);
625
+ return;
626
+ }
627
+
628
+ if (options.status) {
629
+ shellExec(`${base} ps --format 'table {{.Name}}\\t{{.Service}}\\t{{.Status}}\\t{{.Ports}}'`);
630
+ return;
631
+ }
632
+
633
+ if (options.shell) {
634
+ const service = target || 'app';
635
+ shellExec(`${base} exec ${service} ${service === 'app' ? '/bin/bash' : '/bin/sh'}`);
636
+ return;
637
+ }
638
+
639
+ // Remaining case: --generate alone (config already written above).
640
+ } catch (error) {
641
+ logger.error(error);
642
+ process.exit(1);
643
+ }
644
+ },
645
+ };
646
+ }
647
+
648
+ export default UnderpostDockerCompose;
package/src/cli/env.js CHANGED
@@ -95,10 +95,7 @@ class UnderpostRootEnv {
95
95
  get(key, value, options = { plain: false, disableLog: false, copy: false }) {
96
96
  const exeRootPath = `${getNpmRootPath()}/underpost`;
97
97
  const envPath = `${exeRootPath}/.env`;
98
- if (!fs.existsSync(envPath) || !fs.statSync(envPath).isFile()) {
99
- logger.warn(`Empty environment variables`);
100
- return undefined;
101
- }
98
+ if (!fs.existsSync(envPath) || !fs.statSync(envPath).isFile()) return undefined;
102
99
  const env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
103
100
  if (!options.disableLog)
104
101
  options?.plain === true ? console.log(env[key]) : logger.info(`${key}(${typeof env[key]})`, env[key]);
@@ -146,12 +143,21 @@ class UnderpostRootEnv {
146
143
  /**
147
144
  * @method clean
148
145
  * @description Cleans the underpost root environment by removing the environment file.
146
+ * @param {object} options - Options for cleaning the environment.
147
+ * @param {Array<string>} [options.keepKeys=[]] - List of keys to keep in the environment file. If provided, only these keys will be retained.
149
148
  * @memberof UnderpostEnv
150
149
  */
151
- clean() {
150
+ clean(options = { keepKeys: [] }) {
151
+ const { keepKeys } = options;
152
152
  const exeRootPath = `${getNpmRootPath()}/underpost`;
153
153
  const envPath = `${exeRootPath}/.env`;
154
- fs.removeSync(envPath);
154
+ if (keepKeys && keepKeys.length > 0 && fs.existsSync(envPath)) {
155
+ const env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
156
+ const filteredEnv = Object.fromEntries(Object.entries(env).filter(([key]) => keepKeys.includes(key)));
157
+ writeEnv(envPath, filteredEnv);
158
+ } else {
159
+ fs.removeSync(envPath);
160
+ }
155
161
  },
156
162
  /**
157
163
  * @method isInsideContainer