cyberia 3.2.22 → 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 (185) 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 +33 -24
  11. package/.github/workflows/engine-cyberia.ci.yml +13 -3
  12. package/.github/workflows/ghpkg.ci.yml +88 -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 +10 -7
  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 +301 -1
  21. package/CLI-HELP.md +71 -6
  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 +31 -0
  28. package/bin/cyberia.js +1080 -184
  29. package/bin/deploy.js +2 -2
  30. package/bin/index.js +1080 -184
  31. package/bump.config.js +1 -0
  32. package/compose.env +131 -0
  33. package/conf.js +18 -1
  34. package/deployment.yaml +2 -2
  35. package/docker-compose.yml +316 -0
  36. package/hardhat/hardhat.config.js +2 -2
  37. package/hardhat/package-lock.json +620 -2041
  38. package/hardhat/package.json +7 -5
  39. package/hardhat/scripts/deployObjectLayerToken.js +18 -18
  40. package/hardhat/test/ObjectLayerToken.js +378 -274
  41. package/ipfs/configure-ipfs.sh +13 -0
  42. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  43. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  44. package/manifests/deployment/dd-cyberia-development/deployment.yaml +2 -2
  45. package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
  46. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
  47. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  48. package/mongodb/entrypoint.sh +76 -0
  49. package/nginx.conf +87 -0
  50. package/package.json +31 -19
  51. package/pv-pvc.yaml +32 -0
  52. package/scripts/disk-clean.sh +85 -60
  53. package/scripts/kubeadm-node-setup.sh +317 -0
  54. package/scripts/rocky-kickstart.sh +877 -185
  55. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  56. package/scripts/test-monitor.sh +3 -5
  57. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
  58. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
  59. package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
  60. package/src/api/cyberia-action/cyberia-action.model.js +21 -29
  61. package/src/api/cyberia-action/cyberia-action.router.js +42 -7
  62. package/src/api/cyberia-action/cyberia-action.service.js +20 -4
  63. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
  64. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
  65. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
  66. package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
  67. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
  68. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
  69. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
  70. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
  71. package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
  72. package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
  73. package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
  74. package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
  75. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
  76. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
  77. package/src/api/cyberia-map/cyberia-map.router.js +21 -6
  78. package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
  79. package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
  80. package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
  81. package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
  82. package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
  83. package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
  84. package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
  85. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -129
  86. package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
  87. package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
  88. package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
  89. package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
  90. package/src/api/ipfs/ipfs.service.js +28 -15
  91. package/src/api/object-layer/object-layer.router.js +25 -15
  92. package/src/api/object-layer/object-layer.service.js +15 -20
  93. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
  94. package/src/cli/baremetal.js +717 -16
  95. package/src/cli/cluster.js +80 -5
  96. package/src/cli/deploy.js +127 -11
  97. package/src/cli/docker-compose.js +648 -0
  98. package/src/cli/env.js +11 -2
  99. package/src/cli/fs.js +75 -30
  100. package/src/cli/image.js +129 -51
  101. package/src/cli/index.js +110 -6
  102. package/src/cli/kickstart.js +142 -20
  103. package/src/cli/release.js +46 -4
  104. package/src/cli/repository.js +238 -33
  105. package/src/cli/run.js +520 -114
  106. package/src/cli/secrets.js +82 -48
  107. package/src/cli/ssh.js +234 -0
  108. package/src/cli/static.js +2 -2
  109. package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
  110. package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
  111. package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
  112. package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
  113. package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
  114. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
  115. package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
  116. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
  117. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
  118. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
  119. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +45 -27
  120. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
  121. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
  122. package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
  123. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
  124. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +8 -1
  125. package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
  126. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +5 -5
  127. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  128. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  129. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
  130. package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
  131. package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
  132. package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
  133. package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
  134. package/src/client/services/object-layer/object-layer.management.js +6 -6
  135. package/src/{server → client-builder}/client-build-docs.js +15 -5
  136. package/src/{server → client-builder}/client-build-live.js +3 -3
  137. package/src/{server → client-builder}/client-build.js +25 -22
  138. package/src/{server → client-builder}/client-dev-server.js +3 -3
  139. package/src/{server → client-builder}/client-icons.js +2 -2
  140. package/src/{server → client-builder}/ssr.js +5 -5
  141. package/src/client.build.js +1 -3
  142. package/src/client.dev.js +1 -1
  143. package/src/db/mongo/MongoBootstrap.js +12 -12
  144. package/src/grpc/cyberia/grpc-server.js +255 -70
  145. package/src/index.js +12 -1
  146. package/src/mailer/EmailRender.js +1 -1
  147. package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
  148. package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
  149. package/src/projects/cyberia/catalog-cyberia.js +81 -0
  150. package/src/projects/cyberia/gemini-client.js +175 -0
  151. package/src/projects/cyberia/generate-saga.js +2107 -0
  152. package/src/{server → projects/cyberia}/ipfs-client.js +2 -2
  153. package/src/{server → projects/cyberia}/object-layer.js +12 -108
  154. package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
  155. package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
  156. package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
  157. package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
  158. package/src/{server → projects/cyberia}/shape-generator.js +2 -2
  159. package/src/{server → projects/underpost}/catalog-underpost.js +1 -2
  160. package/src/runtime/cyberia-client/Dockerfile +27 -61
  161. package/src/runtime/cyberia-client/Dockerfile.dev +20 -12
  162. package/src/runtime/cyberia-server/Dockerfile +21 -20
  163. package/src/runtime/cyberia-server/Dockerfile.dev +17 -8
  164. package/src/runtime/engine-cyberia/Dockerfile +143 -0
  165. package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
  166. package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
  167. package/src/runtime/engine-cyberia/compose.env +131 -0
  168. package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
  169. package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
  170. package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
  171. package/src/runtime/engine-cyberia/nginx.conf +87 -0
  172. package/src/runtime/express/Express.js +2 -2
  173. package/src/runtime/nginx/Nginx.js +250 -0
  174. package/src/server/catalog.js +9 -14
  175. package/src/server/conf.js +3 -6
  176. package/src/server/runtime-status.js +18 -1
  177. package/src/server/start.js +12 -2
  178. package/src/server.js +6 -2
  179. package/test/cyberia-instance-conf-defaults.test.js +140 -0
  180. package/test/deploy-monitor.test.js +26 -10
  181. package/test/shape-generator.test.js +7 -1
  182. package/typedoc.dd-cyberia.json +3 -1
  183. package/typedoc.json +3 -1
  184. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  185. /package/src/{server → client-builder}/client-formatted.js +0 -0
@@ -22,7 +22,7 @@ import {
22
22
  buildReplicaId,
23
23
  writeEnv,
24
24
  } from '../server/conf.js';
25
- import { buildClient, unzipClientBuild, mergeClientBuildZip } from '../server/client-build.js';
25
+ import { buildClient, unzipClientBuild, mergeClientBuildZip } from '../client-builder/client-build.js';
26
26
  import { DefaultConf } from '../../conf.js';
27
27
  import Underpost from '../index.js';
28
28
 
@@ -106,6 +106,9 @@ class UnderpostRepository {
106
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.
107
107
  * @param {string} [options.changelogMinVersion=''] - If set, overrides the default minimum version limit (2.85.0) for --changelog-build.
108
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`).
109
112
  * @memberof UnderpostRepository
110
113
  */
111
114
  commit(
@@ -135,6 +138,9 @@ class UnderpostRepository {
135
138
  bc: '',
136
139
  isRemoteRepo: '',
137
140
  hasChanges: false,
141
+ remoteUrl: false,
142
+ switchRepo: '',
143
+ targetBranch: '',
138
144
  },
139
145
  ) {
140
146
  if (!repoPath) repoPath = '.';
@@ -155,6 +161,22 @@ class UnderpostRepository {
155
161
  return;
156
162
  }
157
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
+
158
180
  if (options.bc) {
159
181
  console.log(
160
182
  shellExec(`cd ${repoPath} && git for-each-ref --contains ${options.bc} --format='%(refname:short)'`, {
@@ -196,7 +218,7 @@ class UnderpostRepository {
196
218
  return;
197
219
  }
198
220
 
199
- if (options.changelog !== undefined || options.changelogBuild) {
221
+ if (options.changelog !== undefined || options.changelogBuild || options.changelogMsg !== undefined) {
200
222
  const releaseMatch = 'New release v:';
201
223
  // Helper: parse [<tag>] commits into grouped sections
202
224
  const buildSectionChangelog = (commits) => {
@@ -233,7 +255,8 @@ class UnderpostRepository {
233
255
  stdout: true,
234
256
  silent: true,
235
257
  disableLog: true,
236
- });
258
+ silentOnError: true,
259
+ }).toString();
237
260
  return rawLog
238
261
  .split('\n')
239
262
  .map((line) => {
@@ -324,15 +347,18 @@ class UnderpostRepository {
324
347
  fs.writeFileSync(changelogPath, `# Changelog\n\n${changelog}`);
325
348
  logger.info('CHANGELOG.md built at', changelogPath);
326
349
  } else {
327
- // --changelog [latest-n]: print changelog of last N commits (default: 1)
328
- const hasExplicitCount =
329
- options.changelog !== undefined && options.changelog !== true && !isNaN(parseInt(options.changelog));
330
- const scanLimit = hasExplicitCount ? parseInt(options.changelog) : 1;
331
- const allCommits = fetchHistory(scanLimit);
332
-
333
- const sections = buildVersionSections(allCommits);
334
- let changelog = renderSections(sections);
335
- 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
+ }
336
362
  }
337
363
 
338
364
  return;
@@ -1235,17 +1261,40 @@ Prevent build private config repo.`,
1235
1261
  return copiedFiles;
1236
1262
  },
1237
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
+
1238
1286
  /**
1239
1287
  * Dispatches a GitHub Actions workflow using gh CLI or curl fallback.
1240
1288
  * @param {object} options - Dispatch options.
1241
1289
  * @param {string} options.repo - The GitHub repository (e.g., "owner/repo").
1242
1290
  * @param {string} options.workflowFile - The workflow file name (e.g., "engine-core.cd.yml").
1243
- * @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.
1244
1292
  * @param {object} [options.inputs={}] - Key-value inputs for the workflow_dispatch event.
1245
1293
  * @memberof UnderpostRepository
1246
1294
  */
1247
- dispatchWorkflow(options = { repo: '', workflowFile: '', ref: 'master', inputs: {} }) {
1248
- 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);
1249
1298
  const ghAvailable = shellExec('command -v gh 2>/dev/null', {
1250
1299
  stdout: true,
1251
1300
  silent: true,
@@ -1322,14 +1371,77 @@ Prevent build private config repo.`,
1322
1371
  if (!url) return false;
1323
1372
  const authUrl = Underpost.repo.resolveAuthUrl(url);
1324
1373
  // GIT_TERMINAL_PROMPT=0 prevents git from hanging on credential prompts inside containers.
1325
- 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`, {
1326
1377
  stdout: true,
1327
1378
  silent: true,
1328
1379
  disableLog: true,
1329
1380
  silentOnError: true,
1330
1381
  });
1331
- logger.info('isRemoteRepo', { url, raw: (raw || '').slice(0, 120) });
1332
- 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`);
1333
1445
  },
1334
1446
 
1335
1447
  /**
@@ -1341,17 +1453,31 @@ Prevent build private config repo.`,
1341
1453
  * @memberof UnderpostRepository
1342
1454
  */
1343
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.
1344
1459
  const branch = shellExec(`cd ${repoPath} && git branch --show-current`, {
1345
1460
  stdout: true,
1346
1461
  silent: true,
1347
1462
  disableLog: true,
1348
- }).trim();
1349
- 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
+ });
1350
1473
  const raw = shellExec(`cd ${repoPath} && git rev-list --count origin/${branch}..HEAD 2>/dev/null`, {
1351
1474
  stdout: true,
1352
1475
  silent: true,
1353
1476
  disableLog: true,
1354
- }).trim();
1477
+ silentOnError: true,
1478
+ })
1479
+ .toString()
1480
+ .trim();
1355
1481
  const count = parseInt(raw);
1356
1482
  const hasUnpushed = !isNaN(count) && count > 0;
1357
1483
  return { count: hasUnpushed ? count : fallback, branch, hasUnpushed };
@@ -1366,7 +1492,7 @@ Prevent build private config repo.`,
1366
1492
  */
1367
1493
  sanitizeChangelogMessage(message) {
1368
1494
  if (!message) return '';
1369
- return message
1495
+ const sanitized = message
1370
1496
  .replace(/^##\s+\d{4}-\d{2}-\d{2}\s*/gm, '')
1371
1497
  .replace(/^###\s+(\S+)\s*/gm, '[$1] ')
1372
1498
  .replace(/^- /gm, '')
@@ -1378,6 +1504,9 @@ Prevent build private config repo.`,
1378
1504
  .join('\n')
1379
1505
  .trim()
1380
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;
1381
1510
  },
1382
1511
  /**
1383
1512
  * Initializes a git repository at the given path and configures user identity
@@ -1483,18 +1612,63 @@ Prevent build private config repo.`,
1483
1612
  },
1484
1613
 
1485
1614
  /**
1486
- * Runtime-type in-pod site-root directory resolver.
1487
- * Maps each known runtime to the filesystem path where its repository lives inside the container.
1488
- * @param {string} runtime - The runtime identifier (e.g. 'wp').
1489
- * @param {string} host - The virtual-host name.
1490
- * @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.
1491
1632
  * @memberof UnderpostRepository
1492
1633
  */
1493
- runtimeSiteRoot(runtime, host) {
1494
- const runtimePaths = {
1634
+ runtimeSiteRoot({ runtime, host, routePath = '/', directory } = {}) {
1635
+ const runtimeBase = {
1495
1636
  wp: `/opt/lampp/htdocs/wp/${host}`,
1496
1637
  };
1497
- 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
+ );
1498
1672
  },
1499
1673
 
1500
1674
  /**
@@ -1543,12 +1717,28 @@ Prevent build private config repo.`,
1543
1717
  const entry = confServer[host][routePath];
1544
1718
  if (!entry.repository) continue;
1545
1719
 
1546
- const siteRoot = Underpost.repo.runtimeSiteRoot(entry.runtime, host);
1547
- if (!siteRoot) {
1720
+ const siteRootArgs = { runtime: entry.runtime, host, directory: entry.directory };
1721
+ const repoRoot = Underpost.repo.runtimeSiteRoot({ ...siteRootArgs, routePath });
1722
+ if (!repoRoot) {
1548
1723
  logger.warn(`backupPodRepositories: no site-root mapping for runtime '${entry.runtime}' (${host})`);
1549
1724
  continue;
1550
1725
  }
1551
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
+
1552
1742
  const repoName = entry.repository.split('/').pop().split('.')[0];
1553
1743
 
1554
1744
  // Build the backup script — secrets are injected as env vars in the exec,
@@ -1641,12 +1831,17 @@ Prevent build private config repo.`,
1641
1831
  * 4. Falls back to `${GITHUB_USERNAME}/engine` when no match is found.
1642
1832
  *
1643
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.
1644
1835
  * @returns {string} The resolved `owner/repo` string.
1645
1836
  * @memberof UnderpostRepository
1646
1837
  */
1647
- resolveInstanceRepo(runtime = '') {
1838
+ resolveInstanceRepo(runtime = '', ownRuntimeRepo = false) {
1648
1839
  const fallback = `${process.env.GITHUB_USERNAME}/engine`;
1649
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$/, '');
1650
1845
  const ddRouter = './engine-private/deploy/dd.router';
1651
1846
  const deployIds = fs.existsSync(ddRouter)
1652
1847
  ? fs
@@ -1672,6 +1867,16 @@ Prevent build private config repo.`,
1672
1867
  logger.warn(`[resolveInstanceRepo] failed to parse ${confPath}: ${err.message}`);
1673
1868
  }
1674
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
+ }
1675
1880
  return fallback;
1676
1881
  },
1677
1882