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
package/src/cli/fs.js CHANGED
@@ -64,6 +64,31 @@ class UnderpostFileStorage {
64
64
  writeStorageConf(storage, storageConf) {
65
65
  if (storage) fs.writeFileSync(storageConf, JSON.stringify(storage, null, 4), 'utf8');
66
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: '' }) {
78
+ 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 });
90
+ }
91
+ },
67
92
  /**
68
93
  * @method recursiveCallback
69
94
  * @description Recursively processes files and directories based on the provided options.
@@ -95,6 +120,17 @@ class UnderpostFileStorage {
95
120
  ) {
96
121
  const { storage, storageConf } = Underpost.fs.getStorageConf(options);
97
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
+
98
134
  // In recursive remove mode, delete every tracked storage key under the requested path,
99
135
  // even when local files/directories are already missing.
100
136
  if (options.rm === true) {
@@ -114,6 +150,7 @@ class UnderpostFileStorage {
114
150
 
115
151
  if (hasPathFilter && options.force === true && fs.existsSync(basePath)) fs.removeSync(basePath);
116
152
 
153
+ // Storage is canonical: persist the removal before any (optional) git tracking runs.
117
154
  Underpost.fs.writeStorageConf(storage, storageConf);
118
155
 
119
156
  if (associatedPaths.length === 0)
@@ -125,22 +162,31 @@ class UnderpostFileStorage {
125
162
  });
126
163
 
127
164
  if (options.git === true) {
128
- const gitPath = hasPathFilter ? basePath : '.';
129
- shellExec(`cd ${gitPath} && git add .`);
130
- shellExec(`underpost cmt ${gitPath} feat`, {
131
- silentOnError: true,
132
- silent: true,
133
- disableLog: true,
134
- });
165
+ const gitPath = !hasPathFilter ? '.' : isSingleFile ? parentDir : basePath;
166
+ Underpost.fs.gitTrack(gitPath);
135
167
  }
136
168
 
137
169
  return;
138
170
  }
139
171
 
140
- const deleteFiles = options.pull === true ? [] : Underpost.repo.getDeleteFiles(path);
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
+
186
+ // When processing a single file, only consider it for deletion
141
187
  for (const relativePath of deleteFiles) {
142
- const _path = path + '/' + relativePath;
143
- if (_path in storage) {
188
+ const _path = isSingleFile ? (relativePath === singleFileName ? path : null) : path + '/' + relativePath;
189
+ if (_path && _path in storage) {
144
190
  await Underpost.fs.delete(_path);
145
191
  delete storage[_path];
146
192
  }
@@ -156,18 +202,21 @@ class UnderpostFileStorage {
156
202
  if (pullSkipCount > 0) logger.warn(`Pull skipped ${pullSkipCount} files that already exist`);
157
203
  // Only run git init/commit when the caller explicitly requests git tracking (--git flag).
158
204
  // For bundle pulls into ./build the git step is unwanted and would error on a non-repo path.
159
- if (options.git === true) {
160
- Underpost.repo.initLocalRepo({ path });
161
- shellExec(`cd ${path} && git add . && git commit -m "Base pull state"`, {
162
- silentOnError: true,
163
- });
164
- }
205
+ if (options.git === true) Underpost.fs.gitTrack(gitContextPath, { init: true, message: 'Base pull state' });
165
206
  } else {
166
- const files =
167
- options.git === true ? Underpost.repo.getChangedFiles(path) : await fs.readdir(path, { recursive: true });
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
+ }
168
217
  for (const relativePath of files) {
169
- const _path = path + '/' + relativePath;
170
- if (fs.statSync(_path).isDirectory()) {
218
+ const _path = isSingleFile ? path : path + '/' + relativePath;
219
+ if (fs.existsSync(_path) && fs.statSync(_path).isDirectory()) {
171
220
  if (options.pull === true && !fs.existsSync(_path)) fs.mkdirSync(_path, { recursive: true });
172
221
  continue;
173
222
  } else if (!(_path in storage) || options.force === true) {
@@ -176,15 +225,9 @@ class UnderpostFileStorage {
176
225
  } else logger.warn('File already exists', _path);
177
226
  }
178
227
  }
228
+ // Storage is canonical and always persisted; git is an optional layer on top.
179
229
  Underpost.fs.writeStorageConf(storage, storageConf);
180
- if (options.git === true) {
181
- shellExec(`cd ${path} && git add .`);
182
- shellExec(`underpost cmt ${path} feat`, {
183
- silentOnError: true,
184
- silent: true,
185
- disableLog: true,
186
- });
187
- }
230
+ if (options.git === true) Underpost.fs.gitTrack(gitContextPath);
188
231
  },
189
232
  /**
190
233
  * @method callback
@@ -206,10 +249,12 @@ class UnderpostFileStorage {
206
249
  path,
207
250
  options = { rm: false, recursive: false, deployId: '', force: false, pull: false, git: false, omitUnzip: false },
208
251
  ) {
209
- if (options.recursive === true || options.git === true)
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)
210
256
  return await Underpost.fs.recursiveCallback(path, options);
211
257
  if (options.pull === true) return await Underpost.fs.pull(path, options);
212
- if (options.rm === true) return await Underpost.fs.delete(path, options);
213
258
  return await Underpost.fs.upload(path, options);
214
259
  },
215
260
  /**
package/src/cli/image.js CHANGED
@@ -5,9 +5,12 @@
5
5
  */
6
6
 
7
7
  import fs from 'fs-extra';
8
+ import os from 'os';
9
+ import nodePath from 'path';
10
+ import crypto from 'crypto';
8
11
  import { loggerFactory } from '../server/logger.js';
9
12
  import Underpost from '../index.js';
10
- import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
13
+ import { getNpmRootPath } from '../server/conf.js';
11
14
  import { shellExec } from '../server/process.js';
12
15
 
13
16
  const logger = loggerFactory(import.meta);
@@ -23,59 +26,32 @@ class UnderpostImage {
23
26
  static API = {
24
27
  /**
25
28
  * @method pullBaseImages
26
- * @description Pulls base images and builds a 'rockylinux9-underpost' image,
27
- * then loads it into the specified Kubernetes cluster type (Kind, Kubeadm, or K3s).
28
- * @param {object} options - Options for pulling and loading images.
29
- * @param {boolean} [options.kind=false] - If true, load image into Kind cluster.
30
- * @param {boolean} [options.kubeadm=false] - If true, load image into Kubeadm cluster.
31
- * @param {boolean} [options.k3s=false] - If true, load image into K3s cluster.
32
- * @param {string} [options.path=''] - Path to the Dockerfile context.
33
- * @param {boolean} [options.dev=false] - If true, use development mode.
34
- * @param {string} [options.version=''] - Version tag for the image.
35
- * @param {string} [options.imageName=''] - Custom name for the image.
29
+ * @description Ensures the base image prerequisites for the runtime Dockerfiles
30
+ * are present on the host (currently `docker.io/rockylinux/rockylinux:9`). This
31
+ * only pulls it does NOT build. Builds run with `podman build --pull=never`,
32
+ * so the base must exist locally beforehand; that is the sole purpose of this
33
+ * step. Combine with `--build` in the same command to pull-then-build.
36
34
  * @memberof UnderpostImage
37
35
  */
38
- pullBaseImages(
39
- options = {
40
- kind: false,
41
- kubeadm: false,
42
- k3s: false,
43
- path: '',
44
- dev: false,
45
- version: '',
46
- imageName: '',
47
- },
48
- ) {
49
- shellExec(`sudo podman pull docker.io/library/rockylinux:9`);
50
- const baseCommand = options.dev ? 'node bin' : 'underpost';
51
- const baseCommandOption = options.dev ? ' --dev' : '';
52
- const IMAGE_NAME = options.imageName
53
- ? `options.imageName${options.version ? `:${options.version}` : ''}`
54
- : `rockylinux9-underpost:${options.version ? options.version : Underpost.version}`;
55
- let LOAD_TYPE = '';
56
- if (options.kind === true) LOAD_TYPE = `--kind`;
57
- else if (options.kubeadm === true) LOAD_TYPE = `--kubeadm`;
58
- else if (options.k3s === true) LOAD_TYPE = `--k3s`;
59
- shellExec(
60
- `${baseCommand} image${baseCommandOption} --build --podman-save --reset --image-path=. --path ${
61
- options.path ? options.path : getUnderpostRootPath()
62
- } --image-name=${IMAGE_NAME} ${LOAD_TYPE}`,
63
- );
36
+ pullBaseImages() {
37
+ shellExec(`sudo podman pull docker.io/rockylinux/rockylinux:9`);
64
38
  },
65
39
  /**
66
40
  * @method build
67
41
  * @description Builds a Docker image using Podman, optionally saves it as a tar archive,
68
- * and loads it into a specified Kubernetes cluster (Kind, Kubeadm, or K3s).
42
+ * and loads it into a specified target (Kind, Kubeadm, or K3s cluster, or the local
43
+ * Docker store for Docker Compose).
69
44
  * @param {object} options - Options for building and loading images.
70
45
  * @param {string} [options.path=''] - The path to the directory containing the Dockerfile.
71
46
  * @param {string} [options.imageName=''] - The name and tag for the image (e.g., 'my-app:latest').
72
47
  * @param {string} [options.version=''] - Version tag for the image.
73
- * @param {string} [options.imagePath=''] - Directory to save the image tar file.
48
+ * @param {string} [options.imageOutPath=''] - Directory to save the image tar file.
74
49
  * @param {string} [options.dockerfileName=''] - Name of the Dockerfile (defaults to 'Dockerfile').
75
50
  * @param {boolean} [options.podmanSave=false] - If true, save the image as a tar archive using Podman.
76
51
  * @param {boolean} [options.kind=false] - If true, load the image archive into a Kind cluster.
77
52
  * @param {boolean} [options.kubeadm=false] - If true, load the image archive into a Kubeadm cluster (uses 'ctr').
78
53
  * @param {boolean} [options.k3s=false] - If true, load the image archive into a K3s cluster (uses 'k3s ctr').
54
+ * @param {boolean} [options.dockerCompose=false] - If true, load the image archive into the local Docker store for Docker Compose.
79
55
  * @param {boolean} [options.reset=false] - If true, perform a no-cache build.
80
56
  * @param {boolean} [options.dev=false] - If true, use development mode.
81
57
  * @memberof UnderpostImage
@@ -85,42 +61,201 @@ class UnderpostImage {
85
61
  path: '',
86
62
  imageName: '',
87
63
  version: '',
88
- imagePath: '',
64
+ imageOutPath: '',
89
65
  dockerfileName: '',
90
66
  podmanSave: false,
91
67
  kind: false,
92
68
  kubeadm: false,
93
69
  k3s: false,
70
+ dockerCompose: false,
94
71
  reset: false,
95
72
  dev: false,
96
73
  },
97
74
  ) {
98
- let { path, imageName, version, imagePath, dockerfileName, podmanSave, kind, kubeadm, k3s, reset, dev } = options;
75
+ let {
76
+ path,
77
+ imageName,
78
+ version,
79
+ imageOutPath,
80
+ dockerfileName,
81
+ podmanSave,
82
+ kind,
83
+ kubeadm,
84
+ k3s,
85
+ dockerCompose,
86
+ reset,
87
+ dev,
88
+ } = options;
99
89
  if (!path) path = '.';
100
90
  if (!imageName) imageName = `rockylinux9-underpost:${Underpost.version}`;
101
- if (!imagePath) imagePath = '.';
91
+ if (!imageOutPath) imageOutPath = '.';
102
92
  if (imageName.match('/')) imageName = imageName.split('/')[1];
103
93
  if (!version) version = 'latest';
104
94
  version = imageName && imageName.match(':') ? '' : `:${version}`;
105
95
  const podManImg = `localhost/${imageName}${version}`;
106
- if (imagePath && typeof imagePath === 'string' && !fs.existsSync(imagePath))
107
- fs.mkdirSync(imagePath, { recursive: true });
108
- const tarFile = `${imagePath}/${imageName.replace(':', '_')}.tar`;
96
+ if (imageOutPath && typeof imageOutPath === 'string' && !fs.existsSync(imageOutPath))
97
+ fs.mkdirSync(imageOutPath, { recursive: true });
98
+ const tarFile = `${imageOutPath}/${imageName.replace(':', '_')}.tar`;
109
99
  let cache = '';
110
100
  if (reset === true) cache += ' --rm --no-cache';
101
+
102
+ // Forward GitHub credentials from the host environment into the build as
103
+ // podman/BuildKit secrets (`--secret id=...,src=...`), matching the
104
+ // `RUN --mount=type=secret,id=github_*` contract in the runtime
105
+ // Dockerfiles (e.g. src/runtime/engine-cyberia). Secrets are written to
106
+ // short-lived 0600 temp files and removed right after the build — they are
107
+ // never passed as build-args (which would persist in image history) nor
108
+ // baked into any layer.
109
+ const secretTmpFiles = [];
110
+ const secretFlags = [];
111
+ const addBuildSecret = (id, value) => {
112
+ if (!value) return;
113
+ const file = nodePath.join(os.tmpdir(), `underpost-secret-${id}-${crypto.randomBytes(6).toString('hex')}`);
114
+ fs.writeFileSync(file, String(value), { mode: 0o600 });
115
+ secretTmpFiles.push(file);
116
+ secretFlags.push(`--secret id=${id},src=${file}`);
117
+ };
118
+ // addBuildSecret('github_token', process.env.GITHUB_TOKEN);
119
+ addBuildSecret('github_username', process.env.GITHUB_USERNAME);
120
+ // Cloudinary creds power build-time asset pulls (`node bin fs --pull`).
121
+ // addBuildSecret('cloudinary_cloud_name', process.env.CLOUDINARY_CLOUD_NAME);
122
+ // addBuildSecret('cloudinary_api_key', process.env.CLOUDINARY_API_KEY);
123
+ // addBuildSecret('cloudinary_api_secret', process.env.CLOUDINARY_API_SECRET);
124
+ const secretArgs = secretFlags.length ? ` ${secretFlags.join(' ')}` : '';
125
+ if (secretFlags.length)
126
+ logger.info('Passing host GitHub credentials as build secrets', { ids: secretFlags.length });
127
+
111
128
  if (path)
112
- shellExec(
113
- `cd ${path} && sudo podman build -f ./${
114
- dockerfileName && typeof dockerfileName === 'string' ? dockerfileName : 'Dockerfile'
115
- } -t ${imageName} --pull=never --cap-add=CAP_AUDIT_WRITE${cache} --network host`,
116
- );
117
- if (podmanSave === true) {
129
+ try {
130
+ shellExec(
131
+ `cd ${path} && sudo podman build -f ./${
132
+ dockerfileName && typeof dockerfileName === 'string' ? dockerfileName : 'Dockerfile'
133
+ } -t ${imageName} --pull=never --cap-add=CAP_AUDIT_WRITE${cache}${secretArgs} --network host`,
134
+ );
135
+ } finally {
136
+ for (const file of secretTmpFiles) {
137
+ try {
138
+ fs.removeSync(file);
139
+ } catch {
140
+ /* best-effort cleanup */
141
+ }
142
+ }
143
+ }
144
+ // Loading into any target requires the tar archive, so imply the save when
145
+ // one is set (kind/kubeadm/k3s/docker-compose) even if --podman-save was omitted.
146
+ const loadTarget = kind === true || kubeadm === true || k3s === true || dockerCompose === true;
147
+ if (podmanSave === true || loadTarget) {
118
148
  if (fs.existsSync(tarFile)) fs.removeSync(tarFile);
119
149
  shellExec(`podman save -o ${tarFile} ${podManImg}`);
120
150
  }
121
151
  if (kind === true) shellExec(`sudo kind load image-archive ${tarFile}`);
122
152
  else if (kubeadm === true) shellExec(`sudo ctr -n k8s.io images import ${tarFile}`);
123
153
  else if (k3s === true) shellExec(`sudo k3s ctr images import ${tarFile}`);
154
+ // Independent of any cluster target: make the local image available to the
155
+ // Docker daemon so `docker compose` can resolve it (e.g. ENGINE_CYBERIA_IMAGE).
156
+ if (dockerCompose === true) shellExec(`sudo docker load -i ${tarFile}`);
157
+ },
158
+ /**
159
+ * @method importTar
160
+ * @description Loads a pre-built image tar archive into each enabled target
161
+ * without building anything. Mirrors the load step of {@link build}, but
162
+ * the archive is supplied directly via `--import-tar <tar-path>` and every
163
+ * enabled target flag is honored (the same archive is loaded into each), so
164
+ * `--kind --docker-compose` loads it into both.
165
+ * @param {object} options - CLI options.
166
+ * @param {string} options.importTar - Path to the image tar archive (e.g. `./image-v1.0.0.tar`).
167
+ * @param {boolean} [options.kind] - Load into the Kind cluster (`kind load image-archive`).
168
+ * @param {boolean} [options.kubeadm] - Import into kubeadm containerd (`ctr -n k8s.io images import`).
169
+ * @param {boolean} [options.k3s] - Import into k3s containerd (`k3s ctr images import`).
170
+ * @param {boolean} [options.dockerCompose] - Load into the local Docker daemon (`docker load`) for Docker Compose.
171
+ * @returns {void}
172
+ * @memberof UnderpostImage
173
+ */
174
+ importTar(options = { importTar: '', kind: false, kubeadm: false, k3s: false, dockerCompose: false }) {
175
+ const { importTar, kind, kubeadm, k3s, dockerCompose } = options;
176
+ if (!importTar || typeof importTar !== 'string' || !fs.existsSync(importTar)) {
177
+ logger.error('image --import-tar: archive not found', { importTar });
178
+ return;
179
+ }
180
+ const targets = [];
181
+ if (kind === true) {
182
+ shellExec(`sudo kind load image-archive ${importTar}`);
183
+ targets.push('kind');
184
+ }
185
+ if (kubeadm === true) {
186
+ shellExec(`sudo ctr -n k8s.io images import ${importTar}`);
187
+ targets.push('kubeadm');
188
+ }
189
+ if (k3s === true) {
190
+ shellExec(`sudo k3s ctr images import ${importTar}`);
191
+ targets.push('k3s');
192
+ }
193
+ if (dockerCompose === true) {
194
+ shellExec(`sudo docker load -i ${importTar}`);
195
+ targets.push('docker-compose');
196
+ }
197
+ if (targets.length === 0)
198
+ logger.warn(
199
+ 'image --import-tar: no target enabled; combine with --kind, --kubeadm, --k3s and/or --docker-compose',
200
+ );
201
+ else logger.info('image --import-tar: archive loaded', { importTar, targets });
202
+ },
203
+ /**
204
+ * @method getCurrentLoaded
205
+ * @description Retrieves the currently loaded images in the Kubernetes cluster.
206
+ * @param {string} [node='kind-worker'] - Node name to check for loaded images.
207
+ * @param {object} options - Options for the image retrieval.
208
+ * @param {boolean} options.spec - Whether to retrieve images from the pod specifications.
209
+ * @param {string} options.namespace - Kubernetes namespace to filter pods.
210
+ * @returns {Array<object>} - Array of objects containing pod names and their corresponding images.
211
+ * @memberof UnderpostImage
212
+ */
213
+ getCurrentLoaded(node = 'kind-worker', options = { spec: false, namespace: '' }) {
214
+ if (options.spec) {
215
+ const raw = shellExec(
216
+ `kubectl get pods ${options.namespace ? `--namespace ${options.namespace}` : `--all-namespaces`} -o=jsonpath='{range .items[*]}{"\\n"}{.metadata.namespace}{"/"}{.metadata.name}{":\\t"}{range .spec.containers[*]}{.image}{", "}{end}{end}'`,
217
+ {
218
+ stdout: true,
219
+ silent: true,
220
+ },
221
+ );
222
+ return raw
223
+ .split(`\n`)
224
+ .map((lines) => ({
225
+ pod: lines.split('\t')[0].replaceAll(':', '').trim(),
226
+ image: lines.split('\t')[1] ? lines.split('\t')[1].replaceAll(',', '').trim() : null,
227
+ }))
228
+ .filter((o) => o.image);
229
+ }
230
+ const raw = shellExec(node === 'kind-worker' ? `docker exec -i ${node} crictl images` : `crictl images`, {
231
+ stdout: true,
232
+ silent: true,
233
+ });
234
+
235
+ const heads = raw
236
+ .split(`\n`)[0]
237
+ .split(' ')
238
+ .filter((_r) => _r.trim());
239
+
240
+ const pods = raw
241
+ .split(`\n`)
242
+ .filter((r) => !r.match('IMAGE'))
243
+ .map((r) => r.split(' ').filter((_r) => _r.trim()));
244
+
245
+ const result = [];
246
+
247
+ for (const row of pods) {
248
+ if (row.length === 0) continue;
249
+ const pod = {};
250
+ let index = -1;
251
+ for (const head of heads) {
252
+ if (head in pod) continue;
253
+ index++;
254
+ pod[head] = row[index];
255
+ }
256
+ result.push(pod);
257
+ }
258
+ return result;
124
259
  },
125
260
  /**
126
261
  * @method list
@@ -139,10 +274,7 @@ class UnderpostImage {
139
274
  list(options = { nodeName: '', namespace: '', spec: false, log: false, k3s: false, kubeadm: false, kind: false }) {
140
275
  if ((options.kubeadm === true || options.k3s === true) && !options.nodeName)
141
276
  options.nodeName = shellExec('echo $HOSTNAME', { stdout: true, silent: true }).trim();
142
- const list = Underpost.deploy.getCurrentLoadedImages(
143
- options.nodeName ? options.nodeName : 'kind-worker',
144
- options,
145
- );
277
+ const list = Underpost.image.getCurrentLoaded(options.nodeName ? options.nodeName : 'kind-worker', options);
146
278
  if (options.log) console.table(list);
147
279
  return list;
148
280
  },