underpost 3.2.5 → 3.2.9

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 (144) hide show
  1. package/.github/workflows/release.cd.yml +1 -2
  2. package/CHANGELOG.md +351 -1
  3. package/CLI-HELP.md +40 -13
  4. package/Dockerfile +0 -4
  5. package/README.md +4 -4
  6. package/bin/build.js +14 -5
  7. package/bin/deploy.js +570 -1
  8. package/bin/file.js +6 -0
  9. package/conf.js +11 -2
  10. package/jsconfig.json +1 -1
  11. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  12. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  13. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  14. package/manifests/deployment/dd-test-development/deployment.yaml +136 -66
  15. package/manifests/deployment/dd-test-development/proxy.yaml +41 -5
  16. package/package.json +24 -15
  17. package/scripts/k3s-node-setup.sh +2 -2
  18. package/scripts/nat-iptables.sh +103 -18
  19. package/src/api/core/core.controller.js +10 -10
  20. package/src/api/core/core.service.js +10 -10
  21. package/src/api/default/default.controller.js +10 -10
  22. package/src/api/default/default.service.js +10 -10
  23. package/src/api/document/document.controller.js +12 -12
  24. package/src/api/document/document.model.js +10 -16
  25. package/src/api/file/file.controller.js +8 -8
  26. package/src/api/file/file.model.js +10 -10
  27. package/src/api/file/file.service.js +36 -36
  28. package/src/api/test/test.controller.js +8 -8
  29. package/src/api/test/test.service.js +8 -8
  30. package/src/api/user/guest.service.js +99 -0
  31. package/src/api/user/user.controller.js +6 -6
  32. package/src/api/user/user.model.js +8 -13
  33. package/src/api/user/user.service.js +3 -20
  34. package/src/cli/cluster.js +61 -14
  35. package/src/cli/db.js +47 -2
  36. package/src/cli/deploy.js +67 -35
  37. package/src/cli/fs.js +79 -8
  38. package/src/cli/image.js +43 -1
  39. package/src/cli/index.js +26 -1
  40. package/src/cli/release.js +57 -1
  41. package/src/cli/repository.js +69 -31
  42. package/src/cli/run.js +415 -36
  43. package/src/cli/ssh.js +1 -1
  44. package/src/cli/static.js +43 -115
  45. package/src/client/Default.index.js +21 -33
  46. package/src/client/components/core/404.js +4 -4
  47. package/src/client/components/core/500.js +4 -4
  48. package/src/client/components/core/Account.js +73 -60
  49. package/src/client/components/core/AgGrid.js +23 -33
  50. package/src/client/components/core/Alert.js +12 -13
  51. package/src/client/components/core/AppStore.js +1 -1
  52. package/src/client/components/core/Auth.js +35 -37
  53. package/src/client/components/core/Badge.js +7 -13
  54. package/src/client/components/core/BtnIcon.js +15 -17
  55. package/src/client/components/core/CalendarCore.js +42 -63
  56. package/src/client/components/core/Chat.js +13 -15
  57. package/src/client/components/core/ClientEvents.js +87 -0
  58. package/src/client/components/core/ColorPaletteElement.js +309 -0
  59. package/src/client/components/core/Content.js +17 -14
  60. package/src/client/components/core/Css.js +15 -71
  61. package/src/client/components/core/CssCore.js +12 -16
  62. package/src/client/components/core/D3Chart.js +4 -4
  63. package/src/client/components/core/Docs.js +64 -91
  64. package/src/client/components/core/DropDown.js +69 -91
  65. package/src/client/components/core/EventBus.js +92 -0
  66. package/src/client/components/core/EventsUI.js +14 -17
  67. package/src/client/components/core/FileExplorer.js +96 -228
  68. package/src/client/components/core/FullScreen.js +47 -75
  69. package/src/client/components/core/Input.js +24 -69
  70. package/src/client/components/core/Keyboard.js +25 -18
  71. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  72. package/src/client/components/core/LoadingAnimation.js +25 -31
  73. package/src/client/components/core/LogIn.js +41 -41
  74. package/src/client/components/core/LogOut.js +23 -14
  75. package/src/client/components/core/Modal.js +462 -178
  76. package/src/client/components/core/NotificationManager.js +14 -18
  77. package/src/client/components/core/Panel.js +54 -50
  78. package/src/client/components/core/PanelForm.js +25 -125
  79. package/src/client/components/core/Polyhedron.js +110 -214
  80. package/src/client/components/core/PublicProfile.js +39 -32
  81. package/src/client/components/core/Recover.js +48 -44
  82. package/src/client/components/core/Responsive.js +88 -32
  83. package/src/client/components/core/RichText.js +9 -18
  84. package/src/client/components/core/Router.js +24 -3
  85. package/src/client/components/core/SearchBox.js +37 -37
  86. package/src/client/components/core/SignUp.js +39 -30
  87. package/src/client/components/core/SocketIo.js +31 -2
  88. package/src/client/components/core/SocketIoHandler.js +6 -6
  89. package/src/client/components/core/ToggleSwitch.js +8 -20
  90. package/src/client/components/core/ToolTip.js +5 -17
  91. package/src/client/components/core/Translate.js +56 -59
  92. package/src/client/components/core/Validator.js +26 -16
  93. package/src/client/components/core/Wallet.js +15 -26
  94. package/src/client/components/core/Worker.js +163 -27
  95. package/src/client/components/core/windowGetDimensions.js +7 -7
  96. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +87 -87
  97. package/src/client/components/default/CssDefault.js +12 -12
  98. package/src/client/components/default/LogInDefault.js +6 -4
  99. package/src/client/components/default/LogOutDefault.js +6 -4
  100. package/src/client/components/default/RouterDefault.js +47 -0
  101. package/src/client/components/default/SettingsDefault.js +4 -4
  102. package/src/client/components/default/SignUpDefault.js +6 -4
  103. package/src/client/components/default/TranslateDefault.js +3 -3
  104. package/src/client/services/core/core.service.js +17 -49
  105. package/src/client/services/default/default.management.js +159 -267
  106. package/src/client/services/default/default.service.js +10 -16
  107. package/src/client/services/document/document.service.js +14 -19
  108. package/src/client/services/file/file.service.js +8 -13
  109. package/src/client/services/test/test.service.js +8 -13
  110. package/src/client/services/user/guest.service.js +86 -0
  111. package/src/client/services/user/user.management.js +5 -5
  112. package/src/client/services/user/user.service.js +14 -20
  113. package/src/client/ssr/body/404.js +3 -3
  114. package/src/client/ssr/body/500.js +3 -3
  115. package/src/client/ssr/body/CacheControl.js +5 -2
  116. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  117. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  118. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  119. package/src/client/ssr/offline/Maintenance.js +12 -11
  120. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  121. package/src/client/ssr/pages/Test.js +2 -2
  122. package/src/client/sw/core.sw.js +212 -0
  123. package/src/index.js +1 -1
  124. package/src/runtime/express/Dockerfile +4 -4
  125. package/src/runtime/lampp/Dockerfile +8 -7
  126. package/src/runtime/wp/Dockerfile +11 -17
  127. package/src/server/client-build-docs.js +45 -46
  128. package/src/server/client-build.js +334 -60
  129. package/src/server/client-formatted.js +47 -16
  130. package/src/server/conf.js +5 -4
  131. package/src/server/data-query.js +32 -20
  132. package/src/server/dns.js +22 -0
  133. package/src/server/ipfs-client.js +232 -91
  134. package/src/server/process.js +13 -27
  135. package/src/server/start.js +17 -3
  136. package/src/server/valkey.js +141 -235
  137. package/tsconfig.docs.json +15 -0
  138. package/typedoc.json +29 -0
  139. package/jsdoc.json +0 -52
  140. package/src/client/components/core/ColorPalette.js +0 -5267
  141. package/src/client/components/core/JoyStick.js +0 -80
  142. package/src/client/components/default/RoutesDefault.js +0 -49
  143. package/src/client/sw/default.sw.js +0 -127
  144. package/src/client/sw/template.sw.js +0 -84
package/src/cli/deploy.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  buildPortProxyRouter,
10
10
  buildProxyRouter,
11
11
  Config,
12
+ cronDeployIdResolve,
12
13
  deployRangePortFactory,
13
14
  getDataDeploy,
14
15
  loadConfServerJson,
@@ -124,24 +125,47 @@ class UnderpostDeploy {
124
125
  * @param {string} namespace - Kubernetes namespace for the deployment.
125
126
  * @param {Array<object>} volumes - Volume configurations for the deployment.
126
127
  * @param {Array<string>} cmd - Command to run in the deployment container.
128
+ * @param {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment.
129
+ * @param {boolean} pullBundle - Whether to pull the pre-built client bundle from Cloudinary before starting. Use together with skipFullBuild to skip the local build entirely.
127
130
  * @returns {string} - YAML deployment configuration for the specified deployment.
128
131
  * @memberof UnderpostDeploy
129
132
  */
130
- deploymentYamlPartsFactory({ deployId, env, suffix, resources, replicas, image, namespace, volumes, cmd }) {
133
+ deploymentYamlPartsFactory({
134
+ deployId,
135
+ env,
136
+ suffix,
137
+ resources,
138
+ replicas,
139
+ image,
140
+ namespace,
141
+ volumes,
142
+ cmd,
143
+ skipFullBuild,
144
+ pullBundle,
145
+ }) {
131
146
  if (!cmd)
132
- cmd = [
133
- `npm install -g npm@11.2.0`,
134
- `npm install -g underpost`,
135
- `underpost secret underpost --create-from-env`,
136
- `underpost start --build --run ${deployId} ${env}`,
137
- ];
147
+ cmd =
148
+ pullBundle || skipFullBuild
149
+ ? [
150
+ // When pullBundle (or skipFullBuild) is set the container pulls the pre-built client
151
+ // bundle from Cloudinary (push-bundle must have been run on the dev machine beforehand).
152
+ `underpost secret underpost --create-from-env`,
153
+ `underpost start --build --run --pull-bundle --skip-full-build ${deployId} ${env}`,
154
+ ]
155
+ : [
156
+ // `npm install -g npm@11.2.0`,
157
+ // `npm install -g underpost`,
158
+ `underpost secret underpost --create-from-env`,
159
+ `underpost start --build --run ${deployId} ${env}`,
160
+ ];
138
161
  const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
139
162
  if (!volumes) volumes = [];
140
163
  const confVolume = fs.existsSync(`./engine-private/conf/${deployId}/conf.volume.json`)
141
164
  ? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
142
165
  : [];
143
166
  volumes = volumes.concat(confVolume);
144
- const containerImage = image ? image : `localhost/rockylinux9-underpost:v${packageJson.version}`;
167
+ // const containerImage = image ? image : `localhost/rockylinux9-underpost:v${packageJson.version}`;
168
+ const containerImage = image ? image : `underpost/underpost-engine:v${packageJson.version}`;
145
169
  return `apiVersion: apps/v1
146
170
  kind: Deployment
147
171
  metadata:
@@ -164,6 +188,7 @@ spec:
164
188
  containers:
165
189
  - name: ${deployId}-${env}-${suffix}
166
190
  image: ${containerImage}
191
+ imagePullPolicy: ${containerImage.startsWith('localhost/') ? 'Never' : 'IfNotPresent'}
167
192
  envFrom:
168
193
  - secretRef:
169
194
  name: underpost-config
@@ -221,6 +246,8 @@ spec:
221
246
  * @param {string} [options.retryPerTryTimeout] - Retry per-try timeout setting for the deployment.
222
247
  * @param {boolean} [options.disableDeploymentProxy] - Whether to disable deployment proxy.
223
248
  * @param {string} [options.traffic] - Traffic status for the deployment.
249
+ * @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; forwarded to deploymentYamlPartsFactory to generate a pull-bundle startup command.
250
+ * @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; forwarded to deploymentYamlPartsFactory. Use together with skipFullBuild.
224
251
  * @returns {Promise<void>} - Promise that resolves when the manifest is built.
225
252
  * @memberof UnderpostDeploy
226
253
  */
@@ -257,6 +284,8 @@ ${Underpost.deploy
257
284
  image,
258
285
  namespace: options.namespace,
259
286
  cmd: options.cmd ? options.cmd.split(',').map((c) => c.trim()) : undefined,
287
+ skipFullBuild: options.skipFullBuild,
288
+ pullBundle: options.pullBundle,
260
289
  })
261
290
  .replace('{{ports}}', buildKindPorts(fromPort, toPort))}
262
291
  `;
@@ -550,10 +579,13 @@ spec:
550
579
  * @param {string} [options.kindType] - Type of Kubernetes resource to retrieve information for.
551
580
  * @param {number} [options.port] - Port number for exposing the deployment.
552
581
  * @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
582
+ * @param {number} [options.exposePort] - Local:remote port override when --expose is active (overrides auto-detected service port).
553
583
  * @param {boolean} [options.k3s] - Whether to use k3s cluster context.
554
584
  * @param {boolean} [options.kubeadm] - Whether to use kubeadm cluster context.
555
585
  * @param {boolean} [options.kind] - Whether to use kind cluster context.
556
586
  * @param {boolean} [options.gitClean] - Whether to run git clean on volume mount paths before copying.
587
+ * @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; passed through to buildManifest/deploymentYamlPartsFactory.
588
+ * @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; passed through to buildManifest/deploymentYamlPartsFactory. Use together with skipFullBuild.
557
589
  * @returns {Promise<void>} - Promise that resolves when the deployment process is complete.
558
590
  * @memberof UnderpostDeploy
559
591
  */
@@ -624,6 +656,8 @@ EOF`);
624
656
  path: instance.path,
625
657
  fromPort: instance.fromPort,
626
658
  toPort: instance.toPort,
659
+ fromDebugPort: instance.fromDebugPort,
660
+ toDebugPort: instance.toDebugPort,
627
661
  traffic: Underpost.deploy.getCurrentTraffic(_deployId, { namespace, hostTest: instance.host }),
628
662
  });
629
663
  }
@@ -801,9 +835,10 @@ EOF`);
801
835
  * @memberof UnderpostDeploy
802
836
  */
803
837
  configMap(env, namespace = 'default') {
838
+ const cronDeployId = cronDeployIdResolve() || 'dd-cron';
804
839
  shellExec(`kubectl delete secret underpost-config -n ${namespace} --ignore-not-found`);
805
840
  shellExec(
806
- `kubectl create secret generic underpost-config --from-env-file=/home/dd/engine/engine-private/conf/dd-cron/.env.${env} --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
841
+ `kubectl create secret generic underpost-config --from-env-file=/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${env} --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
807
842
  );
808
843
  },
809
844
  /**
@@ -1092,11 +1127,10 @@ ${renderHosts}`,
1092
1127
  * @param {string} targetTraffic - Target traffic status for the deployment.
1093
1128
  * @param {Array<string>} ignorePods - List of pod names to ignore.
1094
1129
  * @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
1095
- * @param {string} [outLogType=''] - Type of log output.
1096
1130
  * @returns {object} - Object containing the ready status of the deployment.
1097
1131
  * @memberof UnderpostDeploy
1098
1132
  */
1099
- async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default', outLogType = '') {
1133
+ async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
1100
1134
  let checkStatusIteration = 0;
1101
1135
  const checkStatusIterationMsDelay = 1000;
1102
1136
  const maxIterations = 3000;
@@ -1134,32 +1168,30 @@ ${renderHosts}`,
1134
1168
  }
1135
1169
  }
1136
1170
 
1137
- switch (outLogType) {
1138
- case 'underpost': {
1139
- let indexOf = -1;
1140
- for (const pod of result.notReadyPods) {
1141
- indexOf++;
1142
- const { NAME, out } = pod;
1171
+ {
1172
+ let indexOf = -1;
1173
+ for (const pod of result.notReadyPods) {
1174
+ indexOf++;
1175
+ const { NAME, out } = pod;
1143
1176
 
1144
- if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match(deploymentId))
1145
- lastMsg[NAME] = 'Starting deployment';
1146
- else if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match('underpost'))
1147
- lastMsg[NAME] = 'Installing underpost cli';
1148
- else if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match('task'))
1149
- lastMsg[NAME] = 'Initializing setup task';
1150
- else if (out.match('Empty environment variables')) lastMsg[NAME] = 'Setup environment';
1151
- else if (out.match(`${deployId}-${env}-build-deployment`)) lastMsg[NAME] = 'Building apps/services';
1152
- else if (out.match(`${deployId}-${env}-initializing-deployment`))
1153
- lastMsg[NAME] = 'Initializing apps/services';
1154
- else if (!lastMsg[NAME]) lastMsg[NAME] = `Waiting for status`;
1177
+ if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match(deploymentId))
1178
+ lastMsg[NAME] = 'Starting deployment';
1179
+ // else if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match('underpost'))
1180
+ // lastMsg[NAME] = 'Installing underpost cli';
1181
+ else if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match('task'))
1182
+ lastMsg[NAME] = 'Initializing setup task';
1183
+ else if (out.match('Empty environment variables')) lastMsg[NAME] = 'Setup environment';
1184
+ else if (out.match(`${deployId}-${env}-build-deployment`)) lastMsg[NAME] = 'Building apps/services';
1185
+ else if (out.match(`${deployId}-${env}-initializing-deployment`))
1186
+ lastMsg[NAME] = 'Initializing apps/services';
1187
+ else if (!lastMsg[NAME]) lastMsg[NAME] = `Waiting for status`;
1155
1188
 
1156
- console.log(
1157
- 'Target pod:',
1158
- NAME[NAME.match('green') ? 'bgGreen' : 'bgBlue'].bold.black,
1159
- '| Status:',
1160
- lastMsg[NAME].bold.magenta,
1161
- );
1162
- }
1189
+ console.log(
1190
+ 'Target pod:',
1191
+ NAME[NAME.match('green') ? 'bgGreen' : 'bgBlue'].bold.black,
1192
+ '| Status:',
1193
+ lastMsg[NAME].bold.magenta,
1194
+ );
1163
1195
  }
1164
1196
  }
1165
1197
  await timer(checkStatusIterationMsDelay);
package/src/cli/fs.js CHANGED
@@ -75,6 +75,7 @@ class UnderpostFileStorage {
75
75
  * @param {boolean} [options.force=false] - Flag to force file operations.
76
76
  * @param {boolean} [options.pull=false] - Flag to pull files from storage.
77
77
  * @param {boolean} [options.git=false] - Flag to use Git for file operations.
78
+ * @param {boolean} [options.omitUnzip=false] - If true, do not extract zip and keep downloaded zip file.
78
79
  * @param {string} [options.storageFilePath=''] - The path to the storage configuration file.
79
80
  * @returns {Promise<void>} A promise that resolves when the recursive callback is complete.
80
81
  * @memberof UnderpostFileStorage
@@ -88,10 +89,50 @@ class UnderpostFileStorage {
88
89
  force: false,
89
90
  pull: false,
90
91
  git: false,
92
+ omitUnzip: false,
91
93
  storageFilePath: '',
92
94
  },
93
95
  ) {
94
96
  const { storage, storageConf } = Underpost.fs.getStorageConf(options);
97
+
98
+ // In recursive remove mode, delete every tracked storage key under the requested path,
99
+ // even when local files/directories are already missing.
100
+ if (options.rm === true) {
101
+ const normalizedPath = typeof path === 'string' ? path.trim() : '';
102
+ const basePath = normalizedPath.replace(/\/+$/, '');
103
+ const hasPathFilter = basePath.length > 0;
104
+
105
+ const associatedPaths = Object.keys(storage || {}).filter((storedPath) => {
106
+ if (!hasPathFilter) return true;
107
+ return storedPath === basePath || storedPath.startsWith(`${basePath}/`);
108
+ });
109
+
110
+ for (const associatedPath of associatedPaths) {
111
+ await Underpost.fs.delete(associatedPath);
112
+ if (storage) delete storage[associatedPath];
113
+ }
114
+
115
+ if (hasPathFilter && options.force === true && fs.existsSync(basePath)) fs.removeSync(basePath);
116
+
117
+ Underpost.fs.writeStorageConf(storage, storageConf);
118
+
119
+ if (associatedPaths.length === 0)
120
+ logger.warn('No associated tracked storage paths found', { path: hasPathFilter ? basePath : '*' });
121
+ else
122
+ logger.info('Removed associated tracked storage paths', {
123
+ path: hasPathFilter ? basePath : '*',
124
+ removed: associatedPaths.length,
125
+ });
126
+
127
+ if (options.git === true) {
128
+ const gitPath = hasPathFilter ? basePath : '.';
129
+ shellExec(`cd ${gitPath} && git add .`);
130
+ shellExec(`underpost cmt ${gitPath} feat`);
131
+ }
132
+
133
+ return;
134
+ }
135
+
95
136
  const deleteFiles = options.pull === true ? [] : Underpost.repo.getDeleteFiles(path);
96
137
  for (const relativePath of deleteFiles) {
97
138
  const _path = path + '/' + relativePath;
@@ -109,8 +150,12 @@ class UnderpostFileStorage {
109
150
  } else pullSkipCount++;
110
151
  }
111
152
  if (pullSkipCount > 0) logger.warn(`Pull skipped ${pullSkipCount} files that already exist`);
112
- Underpost.repo.initLocalRepo({ path });
113
- shellExec(`cd ${path} && git add . && git commit -m "Base pull state"`);
153
+ // Only run git init/commit when the caller explicitly requests git tracking (--git flag).
154
+ // For bundle pulls into ./build the git step is unwanted and would error on a non-repo path.
155
+ if (options.git === true) {
156
+ Underpost.repo.initLocalRepo({ path });
157
+ shellExec(`cd ${path} && git add . && git commit -m "Base pull state"`);
158
+ }
114
159
  } else {
115
160
  const files =
116
161
  options.git === true ? Underpost.repo.getChangedFiles(path) : await fs.readdir(path, { recursive: true });
@@ -143,12 +188,13 @@ class UnderpostFileStorage {
143
188
  * @param {boolean} [options.force=false] - Flag to force file operations.
144
189
  * @param {boolean} [options.pull=false] - Flag to pull files from storage.
145
190
  * @param {boolean} [options.git=false] - Flag to use Git for file operations.
191
+ * @param {boolean} [options.omitUnzip=false] - If true, do not extract zip and keep downloaded zip file.
146
192
  * @returns {Promise<void>} A promise that resolves when the callback is complete.
147
193
  * @memberof UnderpostFileStorage
148
194
  */
149
195
  async callback(
150
196
  path,
151
- options = { rm: false, recursive: false, deployId: '', force: false, pull: false, git: false },
197
+ options = { rm: false, recursive: false, deployId: '', force: false, pull: false, git: false, omitUnzip: false },
152
198
  ) {
153
199
  if (options.recursive === true || options.git === true)
154
200
  return await Underpost.fs.recursiveCallback(path, options);
@@ -161,11 +207,13 @@ class UnderpostFileStorage {
161
207
  * @description Uploads a file to Cloudinary.
162
208
  * @param {string} path - The path to the file to upload.
163
209
  * @param {object} [options] - An object containing options for the upload.
164
- * @param {boolean} [options.force=false] - Flag to force file operations.
210
+ * @param {string} [options.deployId=''] - The identifier for the deployment (used to locate the storage config file).
211
+ * @param {boolean} [options.force=false] - Flag to force file operations (overwrites existing remote asset).
165
212
  * @param {string} [options.storageFilePath=''] - The path to the storage configuration file.
166
213
  * @returns {Promise<object>} A promise that resolves to the upload result.
167
214
  * @memberof UnderpostFileStorage
168
215
  */
216
+
169
217
  async upload(
170
218
  path,
171
219
  options = { rm: false, recursive: false, deployId: '', force: false, pull: false, storageFilePath: '' },
@@ -191,22 +239,45 @@ class UnderpostFileStorage {
191
239
  * @method pull
192
240
  * @description Pulls a file from Cloudinary.
193
241
  * @param {string} path - The path to the file to pull.
242
+ * @param {object} [options] - Pull options.
243
+ * @param {boolean} [options.omitUnzip=false] - If true, do not extract zip and keep downloaded zip file.
244
+ * @param {boolean} [options.force=false] - If true, re-download even if the local zip already exists.
194
245
  * @returns {Promise<void>} A promise that resolves when the file is pulled.
195
246
  * @memberof UnderpostFileStorage
196
247
  */
197
- async pull(path) {
248
+ async pull(path, options = { omitUnzip: false, force: false }) {
198
249
  Underpost.fs.cloudinaryConfig();
199
250
  const folder = dir.dirname(path);
200
251
  if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
252
+ const zipPath = `${path}.zip`;
253
+
254
+ if (options.omitUnzip === true && options.force !== true && fs.existsSync(zipPath)) {
255
+ logger.warn('pull skipped, zip already exists and omit-unzip is enabled', { path, zipPath });
256
+ return;
257
+ }
258
+
201
259
  const downloadResult = await cloudinary.utils.download_archive_url({
202
260
  public_ids: [path],
203
261
  resource_type: 'raw',
204
262
  });
205
263
  logger.info('download result', downloadResult);
206
- await Downloader.downloadFile(downloadResult, path + '.zip');
207
- path = Underpost.fs.zip2File(path + '.zip');
208
- fs.removeSync(path + '.zip');
264
+ await Downloader.downloadFile(downloadResult, zipPath);
265
+
266
+ if (options.omitUnzip === true) {
267
+ logger.warn('omit unzip enabled, keeping downloaded zip file', { path, zipPath });
268
+ return;
269
+ }
270
+
271
+ path = Underpost.fs.zip2File(zipPath);
272
+ fs.removeSync(`${path}.zip`);
209
273
  },
274
+ /**
275
+ * @method delete
276
+ * @description Deletes a file from Cloudinary by its public ID.
277
+ * @param {string} path - The path (public ID) of the file to delete.
278
+ * @returns {Promise<object>} A promise that resolves to the Cloudinary delete result.
279
+ * @memberof UnderpostFileStorage
280
+ */
210
281
  async delete(path) {
211
282
  Underpost.fs.cloudinaryConfig();
212
283
  const deleteResult = await cloudinary.api
package/src/cli/image.js CHANGED
@@ -99,6 +99,7 @@ class UnderpostImage {
99
99
  if (!path) path = '.';
100
100
  if (!imageName) imageName = `rockylinux9-underpost:${Underpost.version}`;
101
101
  if (!imagePath) imagePath = '.';
102
+ if (imageName.match('/')) imageName = imageName.split('/')[1];
102
103
  if (!version) version = 'latest';
103
104
  version = imageName && imageName.match(':') ? '' : `:${version}`;
104
105
  const podManImg = `localhost/${imageName}${version}`;
@@ -178,6 +179,12 @@ class UnderpostImage {
178
179
  * @memberof UnderpostImage
179
180
  */
180
181
  pullDockerHubImage(options = { k3s: false, kubeadm: false, kind: false, dockerhubImage: '', version: '' }) {
182
+ if (options.dockerhubImage && options.dockerhubImage.startsWith('localhost')) {
183
+ logger.warn(`[image] pullDockerHubImage skipped — local image cannot be pulled from Docker Hub`, {
184
+ dockerhubImage: options.dockerhubImage,
185
+ });
186
+ return;
187
+ }
181
188
  if (options.dockerhubImage === 'underpost') {
182
189
  options.dockerhubImage = 'underpost/underpost-engine';
183
190
  if (!options.version) options.version = Underpost.version;
@@ -185,7 +192,42 @@ class UnderpostImage {
185
192
  if (!options.version) options.version = 'latest';
186
193
  const version = options.dockerhubImage && options.dockerhubImage.match(':') ? '' : `:${options.version}`;
187
194
  const image = `${options.dockerhubImage}${version}`;
188
- if (options.kind === true) {
195
+ const targetKind = options.kind === true;
196
+ const targetK3s = options.k3s === true;
197
+ const targetKubeadm = options.kubeadm === true || (!targetKind && !targetK3s);
198
+
199
+ const requestedRepo = image.replace(/:[^/]+$/, '');
200
+ const requestedTag = image.match(/:([^/]+)$/)?.[1] || 'latest';
201
+ const normalizeRepo = (repo = '') =>
202
+ repo
203
+ .trim()
204
+ .replace(/^localhost\//, '')
205
+ .replace(/^docker\.io\//, '')
206
+ .replace(/^library\//, '');
207
+
208
+ const currentImages = UnderpostImage.API.list({
209
+ kind: targetKind,
210
+ kubeadm: targetKubeadm,
211
+ k3s: targetK3s,
212
+ log: false,
213
+ });
214
+
215
+ const existsInCluster = currentImages.some((row) => {
216
+ const rowImageRaw = String(row.IMAGE || row.image || '').trim();
217
+ if (!rowImageRaw) return false;
218
+ const rowImage = rowImageRaw.replace(/:[^/]+$/, '');
219
+ const rowTag = String(row.TAG || rowImageRaw.match(/:([^/]+)$/)?.[1] || '').trim();
220
+ return normalizeRepo(rowImage) === normalizeRepo(requestedRepo) && rowTag === requestedTag;
221
+ });
222
+
223
+ if (existsInCluster) {
224
+ logger.info(`[image] pull skipped. Image already loaded`, {
225
+ image,
226
+ clusterType: targetKind ? 'kind' : targetK3s ? 'k3s' : 'kubeadm',
227
+ });
228
+ return;
229
+ }
230
+ if (targetKind) {
189
231
  shellExec(`docker pull ${image}`);
190
232
  shellExec(`sudo kind load docker-image ${image}`);
191
233
  } else {
package/src/cli/index.js CHANGED
@@ -46,6 +46,9 @@ program
46
46
  .option('--sync-env-port', 'Sync environment port assignments across all deploy IDs')
47
47
  .option('--single-replica', 'Build single replica folders instead of full client')
48
48
  .option('--build-zip', 'Create zip files of the builds')
49
+ .option('--split <mb>', 'Split generated zip files into parts of the specified size in MB')
50
+ .option('--unzip <build-prefix>', 'Extract a built client zip or split zip parts using the given build prefix')
51
+ .option('--merge-zip <build-prefix>', 'Merge split ZIP parts back into a single ZIP file for the given build prefix')
49
52
  .option('--lite-build', 'Skip full build (default is full build)')
50
53
  .option('--icons-build', 'Build icons')
51
54
  .description('Builds client assets, single replicas, and/or syncs environment ports.')
@@ -62,6 +65,11 @@ program
62
65
  .option('--build', 'Triggers the client-side application build process.')
63
66
  .option('--underpost-quickly-install', 'Uses Underpost Quickly Install for dependency installation.')
64
67
  .option('--skip-pull-base', 'Skips cloning repositories, uses current workspace code directly.')
68
+ .option('--skip-full-build', 'Skips the full client bundle build during deployment.')
69
+ .option(
70
+ '--pull-bundle',
71
+ 'Downloads the pre-built client bundle from Cloudinary via pull-bundle before starting. Use together with --skip-full-build to skip the local build entirely.',
72
+ )
65
73
  .action(Underpost.start.callback)
66
74
  .description('Initiates application servers, build pipelines, or other defined services based on the deployment ID.');
67
75
 
@@ -226,6 +234,7 @@ program
226
234
  .option('--ban-egress-clear', 'Clears all banned egress IP addresses.')
227
235
  .option('--ban-both-add', 'Adds IP addresses to both banned ingress and egress lists.')
228
236
  .option('--ban-both-remove', 'Removes IP addresses from both banned ingress and egress lists.')
237
+ .option('--mac', 'Prints the MAC address of the main network interface.')
229
238
  .description('Displays the current public machine IP addresses.')
230
239
  .action(Underpost.dns.ipDispatcher);
231
240
 
@@ -329,6 +338,14 @@ program
329
338
  'Sets the local:remote port to expose when --expose is active (overrides auto-detected service port).',
330
339
  )
331
340
  .option('--cmd <cmd>', 'Custom initialization command for deployment (comma-separated commands).')
341
+ .option(
342
+ '--skip-full-build',
343
+ 'Skip client bundle rebuild; container will pull pre-built bundle via pull-bundle instead.',
344
+ )
345
+ .option(
346
+ '--pull-bundle',
347
+ 'Explicitly pull the pre-built client bundle from Cloudinary inside the container. Use together with --skip-full-build.',
348
+ )
332
349
  .description('Manages application deployments, defaulting to deploying development pods.')
333
350
  .action(Underpost.deploy.callback);
334
351
 
@@ -485,6 +502,7 @@ program
485
502
  .option('--recursive', 'Uploads files recursively from the specified path.')
486
503
  .option('--deploy-id <deploy-id>', 'Specifies the deployment configuration ID for file operations.')
487
504
  .option('--pull', 'Downloads the specified file.')
505
+ .option('--omit-unzip', 'With --pull, keeps the downloaded .zip file and skips extraction.')
488
506
  .option('--force', 'Forces the action, overriding any warnings or conflicts.')
489
507
  .option('--storage-file-path <storage-file-path>', 'Specifies a custom file storage path.')
490
508
  .description('Manages file storage, defaulting to file upload operations.')
@@ -613,7 +631,6 @@ program
613
631
  .option('--k3s', 'Sets the k3s cluster context for the runner execution.')
614
632
  .option('--kind', 'Sets the kind cluster context for the runner execution.')
615
633
  .option('--git-clean', 'Runs git clean on volume mount paths before copying.')
616
- .option('--log-type <log-type>', 'Sets the log type for the runner execution.')
617
634
  .option('--deploy-id <deploy-id>', 'Sets deploy id context for the runner execution.')
618
635
  .option('--user <user>', 'Sets user context for the runner execution.')
619
636
  .option('--hosts <hosts>', 'Comma-separated list of hosts for the runner execution.')
@@ -657,6 +674,14 @@ program
657
674
  '(e.g., "127.0.0.1=foo.local,bar.local;10.1.2.3=foo.remote,bar.remote").',
658
675
  )
659
676
  .option('--copy', 'Copies the runner output to the clipboard (supported by: generate-pass, template-deploy-local).')
677
+ .option(
678
+ '--skip-full-build',
679
+ 'Skip client bundle rebuild; triggers pull-bundle in container startup (supported by: sync, template-deploy).',
680
+ )
681
+ .option(
682
+ '--pull-bundle',
683
+ 'Explicitly download the pre-built client bundle from Cloudinary inside the container (supported by: sync, template-deploy). Use together with --skip-full-build.',
684
+ )
660
685
  .description('Runs specified scripts using various runners.')
661
686
  .action(Underpost.run.callback);
662
687
 
@@ -120,7 +120,7 @@ class UnderpostRelease {
120
120
  `./manifests/deployment/dd-default-development/deployment.yaml`,
121
121
  fs
122
122
  .readFileSync(`./manifests/deployment/dd-default-development/deployment.yaml`, 'utf8')
123
- .replaceAll(`underpost:v${version}`, `underpost:v${newVersion}`),
123
+ .replaceAll(`underpost-engine:v${version}`, `underpost-engine:v${newVersion}`),
124
124
  'utf8',
125
125
  );
126
126
 
@@ -133,6 +133,62 @@ class UnderpostRelease {
133
133
  'utf8',
134
134
  );
135
135
 
136
+ // Update underpost/* image versions in all engine-*.cd.yml workflows.
137
+ for (const wf of fs.readdirSync(`./.github/workflows`)) {
138
+ if (!wf.match(/^engine-.+\.cd\.yml$/)) continue;
139
+ const wfPath = `./.github/workflows/${wf}`;
140
+ const updated = fs
141
+ .readFileSync(wfPath, 'utf8')
142
+ .replace(/underpost\/([^:'"]+):v[0-9]+\.[0-9]+\.[0-9]+/g, `underpost/$1:v${newVersion}`);
143
+ fs.writeFileSync(wfPath, updated, 'utf8');
144
+ }
145
+
146
+ // Update version tag in all runtime docker image workflows (type=raw,value=v<version>).
147
+ for (const wf of fs.readdirSync(`./.github/workflows`)) {
148
+ if (!wf.match(/^docker-image\..+\.ci\.yml$/) || wf === 'docker-image.ci.yml') continue;
149
+ const wfPath = `./.github/workflows/${wf}`;
150
+ fs.writeFileSync(
151
+ wfPath,
152
+ fs.readFileSync(wfPath, 'utf8').replaceAll(`type=raw,value=v${version}`, `type=raw,value=v${newVersion}`),
153
+ 'utf8',
154
+ );
155
+ }
156
+
157
+ // Update image version in all conf.instances.json files for underpost/* images.
158
+ if (fs.existsSync(`./engine-private/conf`)) {
159
+ const confFiles = await fs.readdir(`./engine-private/conf`, { recursive: true });
160
+ for (const relativePath of confFiles) {
161
+ const filePath = `./engine-private/conf/${relativePath.replaceAll('\\', '/')}`;
162
+ if (filePath.split('/').pop() !== 'conf.instances.json' || !fs.existsSync(filePath)) continue;
163
+
164
+ let instances;
165
+ try {
166
+ instances = JSON.parse(fs.readFileSync(filePath, 'utf8'));
167
+ } catch {
168
+ logger.warn(`Skipping invalid JSON file: ${filePath}`);
169
+ continue;
170
+ }
171
+
172
+ if (!Array.isArray(instances)) continue;
173
+
174
+ let updated = false;
175
+ for (const instance of instances) {
176
+ if (!instance || typeof instance !== 'object') continue;
177
+ if (!instance.image || typeof instance.image !== 'string') continue;
178
+ if (!instance.image.startsWith('underpost/')) continue;
179
+
180
+ const baseImage = instance.image.split('@')[0].split(':')[0];
181
+ const nextImage = `${baseImage}:v${newVersion}`;
182
+ if (instance.image !== nextImage) {
183
+ instance.image = nextImage;
184
+ updated = true;
185
+ }
186
+ }
187
+
188
+ if (updated) fs.writeFileSync(filePath, JSON.stringify(instances, null, 2), 'utf8');
189
+ }
190
+ }
191
+
136
192
  fs.writeFileSync(
137
193
  `./src/index.js`,
138
194
  fs.readFileSync(`./src/index.js`, 'utf8').replaceAll(`${version}`, `${newVersion}`),