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
@@ -8,6 +8,7 @@ import dotenv from 'dotenv';
8
8
  import { commitData } from '../client/components/core/CommonJs.js';
9
9
  import { pbcopy, shellCd, shellExec } from '../server/process.js';
10
10
  import { actionInitLog, loggerFactory } from '../server/logger.js';
11
+ import path from 'path';
11
12
  import fs from 'fs-extra';
12
13
  import {
13
14
  getNpmRootPath,
@@ -21,7 +22,7 @@ import {
21
22
  buildReplicaId,
22
23
  writeEnv,
23
24
  } from '../server/conf.js';
24
- import { buildClient, unzipClientBuild, mergeClientBuildZip } from '../server/client-build.js';
25
+ import { buildClient, unzipClientBuild, mergeClientBuildZip } from '../client-builder/client-build.js';
25
26
  import { DefaultConf } from '../../conf.js';
26
27
  import Underpost from '../index.js';
27
28
 
@@ -105,6 +106,9 @@ class UnderpostRepository {
105
106
  * @param {boolean} [options.changelogBuild=false] - If true, scrapes all git history and builds a full CHANGELOG.md. Commits containing 'New release v:' are used as version section titles. Only commits starting with '[<tag>]' are included as entries.
106
107
  * @param {string} [options.changelogMinVersion=''] - If set, overrides the default minimum version limit (2.85.0) for --changelog-build.
107
108
  * @param {boolean} [options.changelogNoHash=false] - If true, omits commit hashes from the changelog entries.
109
+ * @param {boolean} [options.remoteUrl=false] - If true, prints the current git remote URL (origin) in plain text and returns.
110
+ * @param {string} [options.switchRepo=''] - If set, switches the remote `origin` to this URL and force-pulls the target branch, overwriting the current working tree.
111
+ * @param {string} [options.targetBranch=''] - Target branch for `switchRepo` (defaults to `master`).
108
112
  * @memberof UnderpostRepository
109
113
  */
110
114
  commit(
@@ -133,16 +137,46 @@ class UnderpostRepository {
133
137
  p: undefined,
134
138
  bc: '',
135
139
  isRemoteRepo: '',
140
+ hasChanges: false,
141
+ remoteUrl: false,
142
+ switchRepo: '',
143
+ targetBranch: '',
136
144
  },
137
145
  ) {
138
146
  if (!repoPath) repoPath = '.';
139
147
 
148
+ if (options.hasChanges) {
149
+ const status = shellExec(`cd ${repoPath} && git status --porcelain`, {
150
+ stdout: true,
151
+ silent: true,
152
+ disableLog: true,
153
+ }).trim();
154
+ process.stdout.write(status ? '1' : '');
155
+ return;
156
+ }
157
+
140
158
  if (options.isRemoteRepo) {
141
159
  const accessible = Underpost.repo.isRemoteRepo(options.isRemoteRepo);
142
160
  console.log(accessible);
143
161
  return;
144
162
  }
145
163
 
164
+ if (options.remoteUrl) {
165
+ const url = Underpost.repo.getRemoteUrl({ path: repoPath });
166
+ if (options.copy) pbcopy(url);
167
+ else console.log(url);
168
+ return;
169
+ }
170
+
171
+ if (options.switchRepo) {
172
+ Underpost.repo.switchRemote({
173
+ path: repoPath,
174
+ url: options.switchRepo,
175
+ branch: options.targetBranch || 'master',
176
+ });
177
+ return;
178
+ }
179
+
146
180
  if (options.bc) {
147
181
  console.log(
148
182
  shellExec(`cd ${repoPath} && git for-each-ref --contains ${options.bc} --format='%(refname:short)'`, {
@@ -184,7 +218,7 @@ class UnderpostRepository {
184
218
  return;
185
219
  }
186
220
 
187
- if (options.changelog !== undefined || options.changelogBuild) {
221
+ if (options.changelog !== undefined || options.changelogBuild || options.changelogMsg !== undefined) {
188
222
  const releaseMatch = 'New release v:';
189
223
  // Helper: parse [<tag>] commits into grouped sections
190
224
  const buildSectionChangelog = (commits) => {
@@ -221,7 +255,8 @@ class UnderpostRepository {
221
255
  stdout: true,
222
256
  silent: true,
223
257
  disableLog: true,
224
- });
258
+ silentOnError: true,
259
+ }).toString();
225
260
  return rawLog
226
261
  .split('\n')
227
262
  .map((line) => {
@@ -312,15 +347,18 @@ class UnderpostRepository {
312
347
  fs.writeFileSync(changelogPath, `# Changelog\n\n${changelog}`);
313
348
  logger.info('CHANGELOG.md built at', changelogPath);
314
349
  } else {
315
- // --changelog [latest-n]: print changelog of last N commits (default: 1)
316
- const hasExplicitCount =
317
- options.changelog !== undefined && options.changelog !== true && !isNaN(parseInt(options.changelog));
318
- const scanLimit = hasExplicitCount ? parseInt(options.changelog) : 1;
319
- const allCommits = fetchHistory(scanLimit);
320
-
321
- const sections = buildVersionSections(allCommits);
322
- let changelog = renderSections(sections);
323
- console.log(changelog || `No changelog entries found.\n`);
350
+ // --changelog / --changelog-msg: message from the last N commits, where N is --from-n-commit
351
+ // (default 1, last commit only). No auto-detection.
352
+ const n = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
353
+ const sections = buildVersionSections(fetchHistory(n));
354
+ const changelog = renderSections(sections);
355
+ if (options.changelogMsg !== undefined) {
356
+ // Sanitized, commit-ready message; empty string when there are no tagged entries so
357
+ // callers fall back to their own generic default instead of a placeholder.
358
+ console.log(Underpost.repo.sanitizeChangelogMessage(changelog));
359
+ } else {
360
+ console.log(changelog || `No changelog entries found.\n`);
361
+ }
324
362
  }
325
363
 
326
364
  return;
@@ -556,7 +594,7 @@ class UnderpostRepository {
556
594
  // Handle sync-conf operation
557
595
  if (options.syncConf) {
558
596
  logger.info(`Syncing configuration for deploy ID: ${deployId}`);
559
- shellExec(`node bin/build ${deployId} conf`);
597
+ shellExec(`node bin/build ${deployId} --conf`);
560
598
  logger.info('Configuration synced successfully');
561
599
  return resolve(true);
562
600
  }
@@ -608,7 +646,8 @@ class UnderpostRepository {
608
646
  const npmRoot = getNpmRootPath();
609
647
  const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
610
648
  const destFolder = `./${projectName}`;
611
- logger.info('build app', { destFolder });
649
+ const deployId = projectName.startsWith('dd-') ? projectName : `dd-${projectName}`;
650
+ logger.info('build app', { destFolder, deployId });
612
651
  if (fs.existsSync(destFolder)) fs.removeSync(destFolder);
613
652
  fs.mkdirSync(destFolder, { recursive: true });
614
653
  if (!options.dev) {
@@ -621,8 +660,9 @@ class UnderpostRepository {
621
660
  UnderpostRepository.API.initLocalRepo({ path: destFolder });
622
661
  shellExec(`cd ${destFolder} && git add . && git commit -m "Base template implementation"`);
623
662
  }
624
- shellExec(`cd ${destFolder} && npm run build`);
625
- shellExec(`cd ${destFolder} && npm run dev`);
663
+ shellExec(`cd ${destFolder} && node bin new --deploy-id ${deployId} --default-conf`);
664
+ shellExec(`cd ${destFolder} && node bin client ${deployId}`);
665
+ shellExec(`cd ${destFolder} && DEPLOY_ID=${deployId} npm run dev`);
626
666
  }
627
667
  return resolve(true);
628
668
  } catch (error) {
@@ -852,6 +892,7 @@ class UnderpostRepository {
852
892
  }
853
893
  }
854
894
  await buildClient({
895
+ deployId: resolvedDeployId,
855
896
  buildZip: options.buildZip || false,
856
897
  split: options.split || '',
857
898
  fullBuild: options.liteBuild ? false : true,
@@ -862,7 +903,12 @@ class UnderpostRepository {
862
903
  logger.warn('Skip replica client build: replica folder not found', { replicaDeployId });
863
904
  continue;
864
905
  }
865
- await Underpost.repo.client(replicaDeployId);
906
+ await Underpost.repo.client(replicaDeployId, '', '', '', {
907
+ buildZip: options.buildZip || false,
908
+ split: options.split || '',
909
+ liteBuild: options.liteBuild || false,
910
+ iconsBuild: options.iconsBuild || false,
911
+ });
866
912
  }
867
913
 
868
914
  return resolve(true);
@@ -937,7 +983,7 @@ Prevent build private config repo.`,
937
983
  deployVersion: packageJsonDeploy.version,
938
984
  };
939
985
  }
940
- shellExec(`node bin/build ${deployId} conf`);
986
+ shellExec(`node bin/build ${deployId} --conf`);
941
987
  return {
942
988
  validVersion: true,
943
989
  engineVersion: packageJsonEngine.version,
@@ -1215,17 +1261,40 @@ Prevent build private config repo.`,
1215
1261
  return copiedFiles;
1216
1262
  },
1217
1263
 
1264
+ /**
1265
+ * Resolves the default branch for a remote GitHub repository by querying
1266
+ * `git ls-remote --symref` and extracting the HEAD ref target.
1267
+ * Falls back to `main` when detection fails.
1268
+ * @param {string} repo - The GitHub repository (e.g., "owner/repo").
1269
+ * @returns {string} The default branch name (e.g. "main" or "master").
1270
+ * @memberof UnderpostRepository
1271
+ */
1272
+ getDefaultBranch(repo) {
1273
+ if (!repo) return 'main';
1274
+ const authUrl = Underpost.repo.resolveAuthUrl(repo);
1275
+ const raw = shellExec(
1276
+ `GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote --symref "${authUrl}" HEAD 2>&1`,
1277
+ { stdout: true, silent: true, disableLog: true, silentOnError: true },
1278
+ );
1279
+ // --symref emits a line like: ref: refs/heads/main HEAD
1280
+ const match = typeof raw === 'string' ? raw.match(/^ref:\s*refs\/heads\/(\S+)\tHEAD$/m) : null;
1281
+ const branch = match ? match[1] : 'main';
1282
+ logger.info('getDefaultBranch', { repo, branch });
1283
+ return branch;
1284
+ },
1285
+
1218
1286
  /**
1219
1287
  * Dispatches a GitHub Actions workflow using gh CLI or curl fallback.
1220
1288
  * @param {object} options - Dispatch options.
1221
1289
  * @param {string} options.repo - The GitHub repository (e.g., "owner/repo").
1222
1290
  * @param {string} options.workflowFile - The workflow file name (e.g., "engine-core.cd.yml").
1223
- * @param {string} [options.ref='master'] - The git ref to dispatch against.
1291
+ * @param {string} [options.ref] - The git ref to dispatch against. Auto-detects the remote's default branch when omitted.
1224
1292
  * @param {object} [options.inputs={}] - Key-value inputs for the workflow_dispatch event.
1225
1293
  * @memberof UnderpostRepository
1226
1294
  */
1227
- dispatchWorkflow(options = { repo: '', workflowFile: '', ref: 'master', inputs: {} }) {
1228
- const { repo, workflowFile, ref, inputs } = options;
1295
+ dispatchWorkflow(options = { repo: '', workflowFile: '', ref: '', inputs: {} }) {
1296
+ const { repo, workflowFile, inputs } = options;
1297
+ const ref = options.ref || Underpost.repo.getDefaultBranch(repo);
1229
1298
  const ghAvailable = shellExec('command -v gh 2>/dev/null', {
1230
1299
  stdout: true,
1231
1300
  silent: true,
@@ -1302,14 +1371,77 @@ Prevent build private config repo.`,
1302
1371
  if (!url) return false;
1303
1372
  const authUrl = Underpost.repo.resolveAuthUrl(url);
1304
1373
  // GIT_TERMINAL_PROMPT=0 prevents git from hanging on credential prompts inside containers.
1305
- const raw = shellExec(`GIT_TERMINAL_PROMPT=0 git ls-remote "${authUrl}" HEAD 2>&1`, {
1374
+ // `-c credential.helper=` disables any host-configured credential helper so its stderr
1375
+ // warnings don't pollute the ls-remote output; the token is already embedded in authUrl.
1376
+ const raw = shellExec(`GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote "${authUrl}" HEAD 2>&1`, {
1306
1377
  stdout: true,
1307
1378
  silent: true,
1308
1379
  disableLog: true,
1309
1380
  silentOnError: true,
1310
1381
  });
1311
- logger.info('isRemoteRepo', { url, raw: (raw || '').slice(0, 120) });
1312
- return typeof raw === 'string' && /^[0-9a-f]{40}\t/m.test(raw);
1382
+ const refLine = typeof raw === 'string' ? (raw.match(/^[0-9a-f]{40}\t.*$/m) || [])[0] : undefined;
1383
+ const accessible = !!refLine;
1384
+ logger.info('isRemoteRepo', { url, accessible, ref: refLine || (raw || '').trim().split('\n').pop() });
1385
+ return accessible;
1386
+ },
1387
+
1388
+ /**
1389
+ * Returns the current URL of a git remote in plain text.
1390
+ * @param {object} [opts]
1391
+ * @param {string} [opts.path='.'] - Path to the git repository.
1392
+ * @param {string} [opts.remote='origin'] - Remote name to query.
1393
+ * @returns {string} The remote URL, or '' when the remote is not configured.
1394
+ * @memberof UnderpostRepository
1395
+ */
1396
+ getRemoteUrl({ path: repoPath = '.', remote = 'origin' } = {}) {
1397
+ return shellExec(`cd "${repoPath}" && git remote get-url ${remote}`, {
1398
+ stdout: true,
1399
+ silent: true,
1400
+ disableLog: true,
1401
+ silentOnError: true,
1402
+ }).trim();
1403
+ },
1404
+
1405
+ /**
1406
+ * Switches a local repository onto a different remote and force-syncs its
1407
+ * working tree to a target branch, discarding local commits and tracked
1408
+ * changes — effectively "switch repo to <url>#<branch>".
1409
+ *
1410
+ * Sequence (idempotent, re-runnable):
1411
+ * 1. Normalize the URL (`owner/repo` → full GitHub HTTPS) and set/add the
1412
+ * remote, storing the token-free URL so no secret leaks into `.git/config`.
1413
+ * 2. Force-fetch the target branch (auth injected inline for private repos).
1414
+ * 3. Reset the working tree to the fetched tip and check out the target
1415
+ * branch, overwriting any current tracked content.
1416
+ *
1417
+ * Untracked files are intentionally left in place (no `git clean`).
1418
+ *
1419
+ * @param {object} opts
1420
+ * @param {string} opts.url - New remote URL (full URL or "owner/repo" short form).
1421
+ * @param {string} [opts.path='.'] - Path to the git repository.
1422
+ * @param {string} [opts.branch='master'] - Target branch to overwrite the current tree with.
1423
+ * @param {string} [opts.remote='origin'] - Remote name to set and fetch from.
1424
+ * @returns {void}
1425
+ * @memberof UnderpostRepository
1426
+ */
1427
+ switchRemote({ url, path: repoPath = '.', branch = 'master', remote = 'origin' }) {
1428
+ if (!url) throw new Error('switchRemote requires a target remote url');
1429
+ if (!fs.existsSync(`${repoPath}/.git`)) throw new Error(`switchRemote: not a git repository: ${repoPath}`);
1430
+ // Token-free URL for the stored remote; auth-injected URL only for the fetch.
1431
+ let normalized = url;
1432
+ if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('git@')) {
1433
+ normalized = `https://github.com/${url}`;
1434
+ }
1435
+ const authUrl = Underpost.repo.resolveAuthUrl(url);
1436
+ const current = Underpost.repo.getRemoteUrl({ path: repoPath, remote });
1437
+ if (!current) shellExec(`cd "${repoPath}" && git remote add ${remote} "${normalized}"`);
1438
+ else shellExec(`cd "${repoPath}" && git remote set-url ${remote} "${normalized}"`);
1439
+ logger.info('switchRemote', { path: repoPath, remote, branch, url: normalized });
1440
+ shellExec(`cd "${repoPath}" && GIT_TERMINAL_PROMPT=0 git fetch --force "${authUrl}" ${branch}`);
1441
+ // reset --hard first clears the worktree so the checkout cannot be blocked
1442
+ // by conflicting local changes; -B points the target branch at the fetched tip.
1443
+ shellExec(`cd "${repoPath}" && git reset --hard FETCH_HEAD`);
1444
+ shellExec(`cd "${repoPath}" && git checkout -B ${branch} FETCH_HEAD`);
1313
1445
  },
1314
1446
 
1315
1447
  /**
@@ -1321,17 +1453,31 @@ Prevent build private config repo.`,
1321
1453
  * @memberof UnderpostRepository
1322
1454
  */
1323
1455
  getUnpushedCount(repoPath = '.', fallback = 1) {
1456
+ // Every git call is silentOnError: a detached HEAD (CI checkout) or a missing upstream must
1457
+ // degrade to the fallback, never throw — otherwise the thrown error is logged to stdout and
1458
+ // can be captured as a commit message by callers that read this command's output.
1324
1459
  const branch = shellExec(`cd ${repoPath} && git branch --show-current`, {
1325
1460
  stdout: true,
1326
1461
  silent: true,
1327
1462
  disableLog: true,
1328
- }).trim();
1329
- shellExec(`cd ${repoPath} && git fetch origin 2>/dev/null`, { silent: true, disableLog: true });
1463
+ silentOnError: true,
1464
+ })
1465
+ .toString()
1466
+ .trim();
1467
+ if (!branch) return { count: fallback, branch: '', hasUnpushed: false };
1468
+ shellExec(`cd ${repoPath} && git fetch origin 2>/dev/null`, {
1469
+ silent: true,
1470
+ disableLog: true,
1471
+ silentOnError: true,
1472
+ });
1330
1473
  const raw = shellExec(`cd ${repoPath} && git rev-list --count origin/${branch}..HEAD 2>/dev/null`, {
1331
1474
  stdout: true,
1332
1475
  silent: true,
1333
1476
  disableLog: true,
1334
- }).trim();
1477
+ silentOnError: true,
1478
+ })
1479
+ .toString()
1480
+ .trim();
1335
1481
  const count = parseInt(raw);
1336
1482
  const hasUnpushed = !isNaN(count) && count > 0;
1337
1483
  return { count: hasUnpushed ? count : fallback, branch, hasUnpushed };
@@ -1346,7 +1492,7 @@ Prevent build private config repo.`,
1346
1492
  */
1347
1493
  sanitizeChangelogMessage(message) {
1348
1494
  if (!message) return '';
1349
- return message
1495
+ const sanitized = message
1350
1496
  .replace(/^##\s+\d{4}-\d{2}-\d{2}\s*/gm, '')
1351
1497
  .replace(/^###\s+(\S+)\s*/gm, '[$1] ')
1352
1498
  .replace(/^- /gm, '')
@@ -1358,6 +1504,9 @@ Prevent build private config repo.`,
1358
1504
  .join('\n')
1359
1505
  .trim()
1360
1506
  .replaceAll('] - ', '] ');
1507
+ // The empty-changelog placeholder must never become a commit message; return empty so
1508
+ // callers fall back to their own generic default.
1509
+ return sanitized === 'No changelog entries found.' ? '' : sanitized;
1361
1510
  },
1362
1511
  /**
1363
1512
  * Initializes a git repository at the given path and configures user identity
@@ -1374,8 +1523,9 @@ Prevent build private config repo.`,
1374
1523
  const gitEmail = process.env.GITHUB_EMAIL || `development@underpost.net`;
1375
1524
 
1376
1525
  if (!fs.existsSync(`${repoPath}/.git`)) {
1377
- shellExec(`cd "${repoPath}" && git init`);
1526
+ shellExec(`mkdir -p "${repoPath}" && git init "${repoPath}"`);
1378
1527
  }
1528
+
1379
1529
  shellExec(`cd "${repoPath}" && git config user.name '${gitUsername}'`);
1380
1530
  shellExec(`cd "${repoPath}" && git config user.email '${gitEmail}'`);
1381
1531
  shellExec(`cd "${repoPath}" && git config core.filemode false`);
@@ -1462,18 +1612,63 @@ Prevent build private config repo.`,
1462
1612
  },
1463
1613
 
1464
1614
  /**
1465
- * Runtime-type in-pod site-root directory resolver.
1466
- * Maps each known runtime to the filesystem path where its repository lives inside the container.
1467
- * @param {string} runtime - The runtime identifier (e.g. 'wp').
1468
- * @param {string} host - The virtual-host name.
1469
- * @returns {string|null} Absolute path inside the pod, or null if the runtime has no known mapping.
1615
+ * Resolves the in-pod site-root directory where a conf route's repository lives.
1616
+ *
1617
+ * General-purpose resolution, independent of any single runtime:
1618
+ * 1. An explicit `directory` (the conf-declared document root) always wins.
1619
+ * 2. Otherwise the runtime's base directory is used (e.g. `wp` `/opt/lampp/htdocs/wp/<host>`).
1620
+ * 3. A subdirectory route (e.g. `/wp`) appends `<subDir>` to the resolved root.
1621
+ *
1622
+ * This mirrors {@link WpService.createApp}'s `vhostDir`/`wpDir` layout so backups target the
1623
+ * exact directory provisioning created, including subdirectory installs and custom directories.
1624
+ *
1625
+ * @param {object} opts
1626
+ * @param {string} opts.runtime - The runtime identifier (e.g. 'wp').
1627
+ * @param {string} opts.host - The virtual-host name.
1628
+ * @param {string} [opts.routePath='/'] - The conf route path the repository is mounted under.
1629
+ * @param {string} [opts.directory] - Explicit document root from conf; overrides the runtime base.
1630
+ * @returns {string|null} Absolute path inside the pod, or null when neither a directory nor a
1631
+ * known runtime base resolves.
1470
1632
  * @memberof UnderpostRepository
1471
1633
  */
1472
- runtimeSiteRoot(runtime, host) {
1473
- const runtimePaths = {
1634
+ runtimeSiteRoot({ runtime, host, routePath = '/', directory } = {}) {
1635
+ const runtimeBase = {
1474
1636
  wp: `/opt/lampp/htdocs/wp/${host}`,
1475
1637
  };
1476
- return runtimePaths[runtime] || null;
1638
+ const vhostDir = directory || runtimeBase[runtime];
1639
+ if (!vhostDir) return null;
1640
+ const subDir = routePath && routePath !== '/' ? routePath.replace(/^\/+/, '').replace(/\/+$/, '') : '';
1641
+ return subDir ? `${vhostDir}/${subDir}` : vhostDir;
1642
+ },
1643
+
1644
+ /**
1645
+ * Probes a running pod for the first candidate directory that is a git repository
1646
+ * (i.e. contains a `.git` entry). Used to locate a site root before backing it up so a
1647
+ * missing/unprovisioned directory is detected up-front instead of producing an opaque
1648
+ * shell `exit 1` from a failed `cd`.
1649
+ *
1650
+ * @param {object} opts
1651
+ * @param {string} opts.podName - Target pod name.
1652
+ * @param {string} opts.namespace - Kubernetes namespace.
1653
+ * @param {string[]} opts.candidates - Absolute paths to probe, most-specific first.
1654
+ * @returns {string|null} The first candidate that is a git repo, or null if none match.
1655
+ * @memberof UnderpostRepository
1656
+ */
1657
+ podRepoDir({ podName, namespace, candidates }) {
1658
+ const probe = candidates.map((dir) => `if [ -d '${dir}/.git' ]; then echo '${dir}'; exit 0; fi`).join('; ');
1659
+ let out = '';
1660
+ try {
1661
+ out = Underpost.kubectl.exec({ podName, namespace, command: `${probe}; echo ''` }) || '';
1662
+ } catch (err) {
1663
+ logger.warn(`podRepoDir: probe failed in pod ${podName}`, err.message);
1664
+ return null;
1665
+ }
1666
+ return (
1667
+ out
1668
+ .split('\n')
1669
+ .map((line) => line.trim())
1670
+ .find(Boolean) || null
1671
+ );
1477
1672
  },
1478
1673
 
1479
1674
  /**
@@ -1522,12 +1717,28 @@ Prevent build private config repo.`,
1522
1717
  const entry = confServer[host][routePath];
1523
1718
  if (!entry.repository) continue;
1524
1719
 
1525
- const siteRoot = Underpost.repo.runtimeSiteRoot(entry.runtime, host);
1526
- if (!siteRoot) {
1720
+ const siteRootArgs = { runtime: entry.runtime, host, directory: entry.directory };
1721
+ const repoRoot = Underpost.repo.runtimeSiteRoot({ ...siteRootArgs, routePath });
1722
+ if (!repoRoot) {
1527
1723
  logger.warn(`backupPodRepositories: no site-root mapping for runtime '${entry.runtime}' (${host})`);
1528
1724
  continue;
1529
1725
  }
1530
1726
 
1727
+ // Probe the pod for the actual git repo so an unprovisioned/missing site root is
1728
+ // detected and skipped cleanly instead of failing the in-pod `cd` with exit 1.
1729
+ // Fall back to the vhost dir (root route) to cover conf/path drift.
1730
+ const vhostRoot = Underpost.repo.runtimeSiteRoot({ ...siteRootArgs, routePath: '/' });
1731
+ const candidates = [...new Set([repoRoot, vhostRoot].filter(Boolean))];
1732
+ const siteRoot = Underpost.repo.podRepoDir({ podName, namespace, candidates });
1733
+ if (!siteRoot) {
1734
+ logger.warn(
1735
+ `backupPodRepositories: no git repository found in pod ${podName} for ${host} (checked ${candidates.join(
1736
+ ', ',
1737
+ )}) — site may not be provisioned yet; skipping`,
1738
+ );
1739
+ continue;
1740
+ }
1741
+
1531
1742
  const repoName = entry.repository.split('/').pop().split('.')[0];
1532
1743
 
1533
1744
  // Build the backup script — secrets are injected as env vars in the exec,
@@ -1620,12 +1831,17 @@ Prevent build private config repo.`,
1620
1831
  * 4. Falls back to `${GITHUB_USERNAME}/engine` when no match is found.
1621
1832
  *
1622
1833
  * @param {string} [runtime=''] - The runtime identifier to look up (e.g. `'cyberia-server'`, `'cyberia-client'`).
1834
+ * @param {boolean} [ownRuntimeRepo=false] - Whether to check for the runtime's own repository.
1623
1835
  * @returns {string} The resolved `owner/repo` string.
1624
1836
  * @memberof UnderpostRepository
1625
1837
  */
1626
- resolveInstanceRepo(runtime = '') {
1838
+ resolveInstanceRepo(runtime = '', ownRuntimeRepo = false) {
1627
1839
  const fallback = `${process.env.GITHUB_USERNAME}/engine`;
1628
1840
  if (!runtime) return fallback;
1841
+ // A `.dev` suffix selects the development image workflow
1842
+ // (docker-image.<runtime>.dev.ci.yml) but resolves to the same instance
1843
+ // repo as its production counterpart, so strip it before matching.
1844
+ runtime = runtime.replace(/\.dev$/, '');
1629
1845
  const ddRouter = './engine-private/deploy/dd.router';
1630
1846
  const deployIds = fs.existsSync(ddRouter)
1631
1847
  ? fs
@@ -1651,8 +1867,91 @@ Prevent build private config repo.`,
1651
1867
  logger.warn(`[resolveInstanceRepo] failed to parse ${confPath}: ${err.message}`);
1652
1868
  }
1653
1869
  }
1870
+ if (ownRuntimeRepo) {
1871
+ const runtimeRepo = Underpost.repo.isRemoteRepo(`${process.env.GITHUB_USERNAME}/${runtime}`);
1872
+ if (runtimeRepo) {
1873
+ logger.info(`[resolveInstanceRepo] resolved from ${process.env.GITHUB_USERNAME}/${runtime}`, {
1874
+ runtime,
1875
+ repo: `${process.env.GITHUB_USERNAME}/${runtime}`,
1876
+ });
1877
+ return `${process.env.GITHUB_USERNAME}/${runtime}`;
1878
+ }
1879
+ }
1654
1880
  return fallback;
1655
1881
  },
1882
+
1883
+ /**
1884
+ * Performs a shallow sparse Git checkout of a single subdirectory from any
1885
+ * GitHub repository into a local target directory.
1886
+ *
1887
+ * Uses `--depth 1 --no-checkout` + `git sparse-checkout` so only the
1888
+ * requested path is fetched — no full clone of the remote repo.
1889
+ * Skips the clone entirely when `<targetDir>/<subPath>` already exists on
1890
+ * disk (idempotent).
1891
+ *
1892
+ * Requires `GITHUB_TOKEN` to be set in the environment for authenticated
1893
+ * access to private repositories.
1894
+ *
1895
+ * @param {string} subPath - The subdirectory path within the remote repo to
1896
+ * check out (e.g. `'conf/dd-prototype'`, `'src/api/payments'`).
1897
+ * @param {object} [options]
1898
+ * @param {string} [options.repoOwner='underpostnet'] - GitHub organisation or
1899
+ * user that owns the repository.
1900
+ * @param {string} [options.repoName='engine-private'] - Name of the
1901
+ * repository on GitHub.
1902
+ * @param {string} [options.targetDir='./engine-private'] - Local directory
1903
+ * where the repo will be cloned.
1904
+ * @returns {boolean} `true` when the checkout was performed, `false` when it
1905
+ * was skipped because the target path already existed.
1906
+ * @memberof UnderpostRepository
1907
+ */
1908
+ sparseCheckoutDirectory(
1909
+ subPath,
1910
+ options = { repoOwner: 'underpostnet', repoName: 'engine-private', targetDir: './engine-private' },
1911
+ ) {
1912
+ const { repoOwner = 'underpostnet', repoName = 'engine-private', targetDir = './engine-private' } = options;
1913
+ const localPath = `${targetDir}/${subPath}`;
1914
+ if (fs.existsSync(localPath)) {
1915
+ logger.info('[sparseCheckoutDirectory] path already present, skipping', localPath);
1916
+ return false;
1917
+ }
1918
+ const authUrl = `https://${process.env.GITHUB_TOKEN}@github.com/${repoOwner}/${repoName}.git`;
1919
+ shellExec(`git clone --depth 1 --no-checkout ${authUrl} ${targetDir}`, { disableLog: true });
1920
+ shellExec(`cd ${targetDir} && git sparse-checkout set ${subPath} && git checkout`, { disableLog: true });
1921
+ logger.info('[sparseCheckoutDirectory] sparse checkout complete', localPath);
1922
+ return true;
1923
+ },
1924
+
1925
+ /**
1926
+ * Ensures a deploy's public source repo (e.g. `engine-prototype`) is present
1927
+ * next to the engine and reset to a pristine HEAD, so catalog `sourceMoves`
1928
+ * can (re)pull custom sources even after a previous build moved them out of
1929
+ * the source tree.
1930
+ *
1931
+ * Clones `../<repoName>` when missing; otherwise restores a clean checkout
1932
+ * (`git checkout .` brings back any moved-out tracked files) and pulls latest.
1933
+ * Mirrors the sibling-repo handling used by `syncPrivateConf`.
1934
+ *
1935
+ * @param {string} repoName - Public source repo name (e.g. `engine-prototype`).
1936
+ * @returns {boolean} `true` when the repo is available on disk.
1937
+ * @memberof UnderpostRepository
1938
+ */
1939
+ pullSourceRepo(repoName) {
1940
+ const username = process.env.GITHUB_USERNAME;
1941
+ if (!username || !repoName) return false;
1942
+ const repoPath = `../${repoName}`;
1943
+ const gitUri = `${username}/${repoName}`;
1944
+ if (!fs.existsSync(repoPath)) {
1945
+ shellExec(`cd .. && underpost clone ${gitUri}`, { silent: true });
1946
+ } else {
1947
+ const repoAbsPath = path.resolve(repoPath);
1948
+ shellExec(`git config --global --add safe.directory '${repoAbsPath}'`);
1949
+ shellExec(`cd ${repoPath} && git checkout . && git clean -f -d && underpost pull . ${gitUri}`, {
1950
+ silent: true,
1951
+ });
1952
+ }
1953
+ return fs.existsSync(repoPath);
1954
+ },
1656
1955
  };
1657
1956
  }
1658
1957