underpost 3.2.5 → 3.2.8

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 (138) hide show
  1. package/.github/workflows/release.cd.yml +1 -2
  2. package/CHANGELOG.md +251 -1
  3. package/CLI-HELP.md +26 -13
  4. package/Dockerfile +0 -4
  5. package/README.md +3 -3
  6. package/bin/build.js +13 -3
  7. package/bin/deploy.js +570 -1
  8. package/bin/file.js +5 -0
  9. package/conf.js +11 -2
  10. package/jsconfig.json +1 -1
  11. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  12. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  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 +20 -11
  17. package/src/api/core/core.controller.js +10 -10
  18. package/src/api/core/core.service.js +10 -10
  19. package/src/api/default/default.controller.js +10 -10
  20. package/src/api/default/default.service.js +10 -10
  21. package/src/api/document/document.controller.js +12 -12
  22. package/src/api/document/document.model.js +10 -16
  23. package/src/api/file/file.controller.js +8 -8
  24. package/src/api/file/file.model.js +10 -10
  25. package/src/api/file/file.service.js +36 -36
  26. package/src/api/test/test.controller.js +8 -8
  27. package/src/api/test/test.service.js +8 -8
  28. package/src/api/user/guest.service.js +99 -0
  29. package/src/api/user/user.controller.js +6 -6
  30. package/src/api/user/user.model.js +8 -13
  31. package/src/api/user/user.service.js +3 -20
  32. package/src/cli/deploy.js +33 -30
  33. package/src/cli/fs.js +62 -5
  34. package/src/cli/image.js +43 -1
  35. package/src/cli/index.js +5 -1
  36. package/src/cli/release.js +57 -1
  37. package/src/cli/repository.js +35 -3
  38. package/src/cli/run.js +300 -35
  39. package/src/cli/ssh.js +1 -1
  40. package/src/cli/static.js +43 -115
  41. package/src/client/Default.index.js +21 -33
  42. package/src/client/components/core/404.js +4 -4
  43. package/src/client/components/core/500.js +4 -4
  44. package/src/client/components/core/Account.js +73 -60
  45. package/src/client/components/core/AgGrid.js +23 -33
  46. package/src/client/components/core/Alert.js +12 -13
  47. package/src/client/components/core/AppStore.js +1 -1
  48. package/src/client/components/core/Auth.js +20 -32
  49. package/src/client/components/core/Badge.js +7 -13
  50. package/src/client/components/core/BtnIcon.js +15 -17
  51. package/src/client/components/core/CalendarCore.js +42 -63
  52. package/src/client/components/core/Chat.js +13 -15
  53. package/src/client/components/core/ClientEvents.js +87 -0
  54. package/src/client/components/core/ColorPaletteElement.js +309 -0
  55. package/src/client/components/core/Content.js +17 -14
  56. package/src/client/components/core/Css.js +15 -71
  57. package/src/client/components/core/CssCore.js +12 -16
  58. package/src/client/components/core/D3Chart.js +4 -4
  59. package/src/client/components/core/Docs.js +60 -59
  60. package/src/client/components/core/DropDown.js +69 -91
  61. package/src/client/components/core/EventBus.js +92 -0
  62. package/src/client/components/core/EventsUI.js +14 -17
  63. package/src/client/components/core/FileExplorer.js +102 -234
  64. package/src/client/components/core/FullScreen.js +47 -75
  65. package/src/client/components/core/Input.js +24 -69
  66. package/src/client/components/core/Keyboard.js +25 -18
  67. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  68. package/src/client/components/core/LoadingAnimation.js +25 -31
  69. package/src/client/components/core/LogIn.js +41 -41
  70. package/src/client/components/core/LogOut.js +23 -14
  71. package/src/client/components/core/Modal.js +397 -176
  72. package/src/client/components/core/NotificationManager.js +14 -18
  73. package/src/client/components/core/Panel.js +54 -50
  74. package/src/client/components/core/PanelForm.js +25 -125
  75. package/src/client/components/core/Polyhedron.js +110 -214
  76. package/src/client/components/core/PublicProfile.js +39 -32
  77. package/src/client/components/core/Recover.js +52 -48
  78. package/src/client/components/core/Responsive.js +88 -32
  79. package/src/client/components/core/RichText.js +9 -18
  80. package/src/client/components/core/Router.js +24 -3
  81. package/src/client/components/core/SearchBox.js +37 -37
  82. package/src/client/components/core/SignUp.js +39 -30
  83. package/src/client/components/core/SocketIo.js +31 -2
  84. package/src/client/components/core/SocketIoHandler.js +6 -6
  85. package/src/client/components/core/ToggleSwitch.js +8 -20
  86. package/src/client/components/core/ToolTip.js +5 -17
  87. package/src/client/components/core/Translate.js +56 -59
  88. package/src/client/components/core/Validator.js +26 -16
  89. package/src/client/components/core/Wallet.js +15 -26
  90. package/src/client/components/core/Worker.js +140 -25
  91. package/src/client/components/core/windowGetDimensions.js +7 -7
  92. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +87 -87
  93. package/src/client/components/default/CssDefault.js +12 -12
  94. package/src/client/components/default/LogInDefault.js +6 -4
  95. package/src/client/components/default/LogOutDefault.js +6 -4
  96. package/src/client/components/default/RouterDefault.js +47 -0
  97. package/src/client/components/default/SettingsDefault.js +4 -4
  98. package/src/client/components/default/SignUpDefault.js +6 -4
  99. package/src/client/components/default/TranslateDefault.js +3 -3
  100. package/src/client/services/core/core.service.js +17 -49
  101. package/src/client/services/default/default.management.js +139 -242
  102. package/src/client/services/default/default.service.js +10 -16
  103. package/src/client/services/document/document.service.js +14 -19
  104. package/src/client/services/file/file.service.js +8 -13
  105. package/src/client/services/test/test.service.js +8 -13
  106. package/src/client/services/user/guest.service.js +79 -0
  107. package/src/client/services/user/user.management.js +5 -5
  108. package/src/client/services/user/user.service.js +14 -20
  109. package/src/client/ssr/body/404.js +3 -3
  110. package/src/client/ssr/body/500.js +3 -3
  111. package/src/client/ssr/body/CacheControl.js +5 -2
  112. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  113. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  114. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  115. package/src/client/ssr/offline/Maintenance.js +12 -11
  116. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  117. package/src/client/ssr/pages/Test.js +2 -2
  118. package/src/client/sw/core.sw.js +212 -0
  119. package/src/index.js +1 -1
  120. package/src/runtime/express/Dockerfile +4 -4
  121. package/src/runtime/lampp/Dockerfile +8 -7
  122. package/src/runtime/wp/Dockerfile +11 -17
  123. package/src/server/client-build-docs.js +45 -46
  124. package/src/server/client-build.js +334 -60
  125. package/src/server/client-formatted.js +47 -16
  126. package/src/server/conf.js +5 -4
  127. package/src/server/ipfs-client.js +232 -91
  128. package/src/server/process.js +13 -27
  129. package/src/server/start.js +6 -3
  130. package/src/server/valkey.js +134 -235
  131. package/tsconfig.docs.json +15 -0
  132. package/typedoc.json +20 -0
  133. package/jsdoc.json +0 -52
  134. package/src/client/components/core/ColorPalette.js +0 -5267
  135. package/src/client/components/core/JoyStick.js +0 -80
  136. package/src/client/components/default/RoutesDefault.js +0 -49
  137. package/src/client/sw/default.sw.js +0 -127
  138. package/src/client/sw/template.sw.js +0 -84
@@ -25,11 +25,11 @@ const handleRequest = (serviceMethod) => async (req, res, options) => {
25
25
  }
26
26
  };
27
27
 
28
- const UserController = {
29
- post: handleRequest(UserService.post),
30
- get: handleRequest(UserService.get),
31
- delete: handleRequest(UserService.delete),
32
- put: handleRequest(UserService.put),
33
- };
28
+ class UserController {
29
+ static post = handleRequest(UserService.post);
30
+ static get = handleRequest(UserService.get);
31
+ static delete = handleRequest(UserService.delete);
32
+ static put = handleRequest(UserService.put);
33
+ }
34
34
 
35
35
  export { UserController };
@@ -3,7 +3,6 @@ import validator from 'validator';
3
3
  import { userRoleEnum } from '../../client/components/core/CommonJs.js';
4
4
  import crypto from 'crypto';
5
5
  // https://mongoosejs.com/docs/2.7.x/docs/schematypes.html
6
-
7
6
  const UserSchema = new Schema(
8
7
  {
9
8
  email: {
@@ -88,13 +87,10 @@ const UserSchema = new Schema(
88
87
  timestamps: true,
89
88
  },
90
89
  );
91
-
92
90
  const UserModel = model('User', UserSchema);
93
-
94
91
  const ProviderSchema = UserSchema;
95
-
96
- const UserDto = {
97
- select: {
92
+ class UserDto {
93
+ static select = {
98
94
  get: () => {
99
95
  return {
100
96
  _id: 1,
@@ -112,8 +108,8 @@ const UserDto = {
112
108
  getAll: () => {
113
109
  return { _id: 1 };
114
110
  },
115
- },
116
- public: {
111
+ };
112
+ static public = {
117
113
  get: () => {
118
114
  return {
119
115
  _id: 1,
@@ -125,8 +121,8 @@ const UserDto = {
125
121
  updatedAt: 1,
126
122
  };
127
123
  },
128
- },
129
- auth: {
124
+ };
125
+ static auth = {
130
126
  payload: (user, jwtid, ip, userAgent, host, path) => {
131
127
  const tokenPayload = {
132
128
  _id: user._id.toString(),
@@ -141,7 +137,6 @@ const UserDto = {
141
137
  };
142
138
  return tokenPayload;
143
139
  },
144
- },
145
- };
146
-
140
+ };
141
+ }
147
142
  export { UserSchema, UserModel, userRoleEnum, ProviderSchema, UserDto };
@@ -9,7 +9,6 @@ import {
9
9
  refreshSessionAndToken,
10
10
  logoutSession,
11
11
  jwtSign,
12
- getBearerToken,
13
12
  validatePasswordMiddleware,
14
13
  } from '../../server/auth.js';
15
14
  import { MailerProvider } from '../../mailer/MailerProvider.js';
@@ -19,8 +18,8 @@ import validator from 'validator';
19
18
  import { DataBaseProvider } from '../../db/DataBaseProvider.js';
20
19
  import { FileFactory, FileCleanup } from '../file/file.service.js';
21
20
  import { UserDto } from './user.model.js';
22
- import { selectDtoFactory, ValkeyAPI } from '../../server/valkey.js';
23
21
  import { timer } from '../../client/components/core/CommonJs.js';
22
+ import { GuestService } from './guest.service.js';
24
23
 
25
24
  const logger = loggerFactory(import.meta);
26
25
 
@@ -226,15 +225,7 @@ const UserService = {
226
225
  } else throw new Error('Invalid credentials');
227
226
 
228
227
  case 'guest': {
229
- const user = await ValkeyAPI.valkeyObjectFactory(options, 'user');
230
- await ValkeyAPI.setValkeyObject(options, user.email, user);
231
- return {
232
- token: jwtSign(
233
- UserDto.auth.payload(user, null, req.ip, req.headers['user-agent'], options.host, options.path),
234
- options,
235
- ),
236
- user: selectDtoFactory(user, UserDto.select.get()),
237
- };
228
+ return await GuestService.create(req, options);
238
229
  }
239
230
 
240
231
  default:
@@ -360,8 +351,7 @@ const UserService = {
360
351
  case 'auth': {
361
352
  let user;
362
353
  if (req.auth.user._id.match('guest')) {
363
- user = await ValkeyAPI.getValkeyObject(options, req.auth.user.email);
364
- if (!user) throw new Error('guest user expired');
354
+ return await GuestService.auth(req, options);
365
355
  } else
366
356
  user = await User.findOne({
367
357
  _id: req.auth.user._id,
@@ -369,13 +359,6 @@ const UserService = {
369
359
 
370
360
  if (!user) throw new Error('user not found');
371
361
 
372
- const guestUser = await ValkeyAPI.getValkeyObject(options, req.auth.user.email);
373
- if (guestUser)
374
- return {
375
- user: selectDtoFactory(guestUser, UserDto.select.get()),
376
- token: getBearerToken(req),
377
- };
378
-
379
362
  return {
380
363
  token: await refreshSessionAndToken(req, res, User, options),
381
364
  user: await User.findOne({
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,
@@ -130,8 +131,8 @@ class UnderpostDeploy {
130
131
  deploymentYamlPartsFactory({ deployId, env, suffix, resources, replicas, image, namespace, volumes, cmd }) {
131
132
  if (!cmd)
132
133
  cmd = [
133
- `npm install -g npm@11.2.0`,
134
- `npm install -g underpost`,
134
+ // `npm install -g npm@11.2.0`,
135
+ // `npm install -g underpost`,
135
136
  `underpost secret underpost --create-from-env`,
136
137
  `underpost start --build --run ${deployId} ${env}`,
137
138
  ];
@@ -141,7 +142,8 @@ class UnderpostDeploy {
141
142
  ? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
142
143
  : [];
143
144
  volumes = volumes.concat(confVolume);
144
- const containerImage = image ? image : `localhost/rockylinux9-underpost:v${packageJson.version}`;
145
+ // const containerImage = image ? image : `localhost/rockylinux9-underpost:v${packageJson.version}`;
146
+ const containerImage = image ? image : `underpost/underpost-engine:v${packageJson.version}`;
145
147
  return `apiVersion: apps/v1
146
148
  kind: Deployment
147
149
  metadata:
@@ -164,6 +166,7 @@ spec:
164
166
  containers:
165
167
  - name: ${deployId}-${env}-${suffix}
166
168
  image: ${containerImage}
169
+ imagePullPolicy: ${containerImage.startsWith('localhost/') ? 'Never' : 'IfNotPresent'}
167
170
  envFrom:
168
171
  - secretRef:
169
172
  name: underpost-config
@@ -624,6 +627,8 @@ EOF`);
624
627
  path: instance.path,
625
628
  fromPort: instance.fromPort,
626
629
  toPort: instance.toPort,
630
+ fromDebugPort: instance.fromDebugPort,
631
+ toDebugPort: instance.toDebugPort,
627
632
  traffic: Underpost.deploy.getCurrentTraffic(_deployId, { namespace, hostTest: instance.host }),
628
633
  });
629
634
  }
@@ -801,9 +806,10 @@ EOF`);
801
806
  * @memberof UnderpostDeploy
802
807
  */
803
808
  configMap(env, namespace = 'default') {
809
+ const cronDeployId = cronDeployIdResolve() || 'dd-cron';
804
810
  shellExec(`kubectl delete secret underpost-config -n ${namespace} --ignore-not-found`);
805
811
  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}`,
812
+ `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
813
  );
808
814
  },
809
815
  /**
@@ -1092,11 +1098,10 @@ ${renderHosts}`,
1092
1098
  * @param {string} targetTraffic - Target traffic status for the deployment.
1093
1099
  * @param {Array<string>} ignorePods - List of pod names to ignore.
1094
1100
  * @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
1095
- * @param {string} [outLogType=''] - Type of log output.
1096
1101
  * @returns {object} - Object containing the ready status of the deployment.
1097
1102
  * @memberof UnderpostDeploy
1098
1103
  */
1099
- async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default', outLogType = '') {
1104
+ async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
1100
1105
  let checkStatusIteration = 0;
1101
1106
  const checkStatusIterationMsDelay = 1000;
1102
1107
  const maxIterations = 3000;
@@ -1134,32 +1139,30 @@ ${renderHosts}`,
1134
1139
  }
1135
1140
  }
1136
1141
 
1137
- switch (outLogType) {
1138
- case 'underpost': {
1139
- let indexOf = -1;
1140
- for (const pod of result.notReadyPods) {
1141
- indexOf++;
1142
- const { NAME, out } = pod;
1142
+ {
1143
+ let indexOf = -1;
1144
+ for (const pod of result.notReadyPods) {
1145
+ indexOf++;
1146
+ const { NAME, out } = pod;
1143
1147
 
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`;
1148
+ if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match(deploymentId))
1149
+ lastMsg[NAME] = 'Starting deployment';
1150
+ // else if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match('underpost'))
1151
+ // lastMsg[NAME] = 'Installing underpost cli';
1152
+ else if (out.match('not') && out.match('found') && checkStatusIteration <= 20 && out.match('task'))
1153
+ lastMsg[NAME] = 'Initializing setup task';
1154
+ else if (out.match('Empty environment variables')) lastMsg[NAME] = 'Setup environment';
1155
+ else if (out.match(`${deployId}-${env}-build-deployment`)) lastMsg[NAME] = 'Building apps/services';
1156
+ else if (out.match(`${deployId}-${env}-initializing-deployment`))
1157
+ lastMsg[NAME] = 'Initializing apps/services';
1158
+ else if (!lastMsg[NAME]) lastMsg[NAME] = `Waiting for status`;
1155
1159
 
1156
- console.log(
1157
- 'Target pod:',
1158
- NAME[NAME.match('green') ? 'bgGreen' : 'bgBlue'].bold.black,
1159
- '| Status:',
1160
- lastMsg[NAME].bold.magenta,
1161
- );
1162
- }
1160
+ console.log(
1161
+ 'Target pod:',
1162
+ NAME[NAME.match('green') ? 'bgGreen' : 'bgBlue'].bold.black,
1163
+ '| Status:',
1164
+ lastMsg[NAME].bold.magenta,
1165
+ );
1163
1166
  }
1164
1167
  }
1165
1168
  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;
@@ -143,12 +184,13 @@ class UnderpostFileStorage {
143
184
  * @param {boolean} [options.force=false] - Flag to force file operations.
144
185
  * @param {boolean} [options.pull=false] - Flag to pull files from storage.
145
186
  * @param {boolean} [options.git=false] - Flag to use Git for file operations.
187
+ * @param {boolean} [options.omitUnzip=false] - If true, do not extract zip and keep downloaded zip file.
146
188
  * @returns {Promise<void>} A promise that resolves when the callback is complete.
147
189
  * @memberof UnderpostFileStorage
148
190
  */
149
191
  async callback(
150
192
  path,
151
- options = { rm: false, recursive: false, deployId: '', force: false, pull: false, git: false },
193
+ options = { rm: false, recursive: false, deployId: '', force: false, pull: false, git: false, omitUnzip: false },
152
194
  ) {
153
195
  if (options.recursive === true || options.git === true)
154
196
  return await Underpost.fs.recursiveCallback(path, options);
@@ -191,21 +233,36 @@ class UnderpostFileStorage {
191
233
  * @method pull
192
234
  * @description Pulls a file from Cloudinary.
193
235
  * @param {string} path - The path to the file to pull.
236
+ * @param {object} [options] - Pull options.
237
+ * @param {boolean} [options.omitUnzip=false] - If true, do not extract zip and keep downloaded zip file.
194
238
  * @returns {Promise<void>} A promise that resolves when the file is pulled.
195
239
  * @memberof UnderpostFileStorage
196
240
  */
197
- async pull(path) {
241
+ async pull(path, options = { omitUnzip: false, force: false }) {
198
242
  Underpost.fs.cloudinaryConfig();
199
243
  const folder = dir.dirname(path);
200
244
  if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
245
+ const zipPath = `${path}.zip`;
246
+
247
+ if (options.omitUnzip === true && options.force !== true && fs.existsSync(zipPath)) {
248
+ logger.warn('pull skipped, zip already exists and omit-unzip is enabled', { path, zipPath });
249
+ return;
250
+ }
251
+
201
252
  const downloadResult = await cloudinary.utils.download_archive_url({
202
253
  public_ids: [path],
203
254
  resource_type: 'raw',
204
255
  });
205
256
  logger.info('download result', downloadResult);
206
- await Downloader.downloadFile(downloadResult, path + '.zip');
207
- path = Underpost.fs.zip2File(path + '.zip');
208
- fs.removeSync(path + '.zip');
257
+ await Downloader.downloadFile(downloadResult, zipPath);
258
+
259
+ if (options.omitUnzip === true) {
260
+ logger.warn('omit unzip enabled, keeping downloaded zip file', { path, zipPath });
261
+ return;
262
+ }
263
+
264
+ path = Underpost.fs.zip2File(zipPath);
265
+ fs.removeSync(`${path}.zip`);
209
266
  },
210
267
  async delete(path) {
211
268
  Underpost.fs.cloudinaryConfig();
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,7 @@ 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.')
65
69
  .action(Underpost.start.callback)
66
70
  .description('Initiates application servers, build pipelines, or other defined services based on the deployment ID.');
67
71
 
@@ -485,6 +489,7 @@ program
485
489
  .option('--recursive', 'Uploads files recursively from the specified path.')
486
490
  .option('--deploy-id <deploy-id>', 'Specifies the deployment configuration ID for file operations.')
487
491
  .option('--pull', 'Downloads the specified file.')
492
+ .option('--omit-unzip', 'With --pull, keeps the downloaded .zip file and skips extraction.')
488
493
  .option('--force', 'Forces the action, overriding any warnings or conflicts.')
489
494
  .option('--storage-file-path <storage-file-path>', 'Specifies a custom file storage path.')
490
495
  .description('Manages file storage, defaulting to file upload operations.')
@@ -613,7 +618,6 @@ program
613
618
  .option('--k3s', 'Sets the k3s cluster context for the runner execution.')
614
619
  .option('--kind', 'Sets the kind cluster context for the runner execution.')
615
620
  .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
621
  .option('--deploy-id <deploy-id>', 'Sets deploy id context for the runner execution.')
618
622
  .option('--user <user>', 'Sets user context for the runner execution.')
619
623
  .option('--hosts <hosts>', 'Comma-separated list of hosts for the runner execution.')
@@ -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}`),
@@ -21,7 +21,7 @@ import {
21
21
  buildReplicaId,
22
22
  writeEnv,
23
23
  } from '../server/conf.js';
24
- import { buildClient } from '../server/client-build.js';
24
+ import { buildClient, unzipClientBuild, mergeClientBuildZip } from '../server/client-build.js';
25
25
  import { DefaultConf } from '../../conf.js';
26
26
  import Underpost from '../index.js';
27
27
 
@@ -642,6 +642,9 @@ class UnderpostRepository {
642
642
  * @param {boolean} [options.syncEnvPort=false] - If true, syncs environment port assignments across all deploy IDs.
643
643
  * @param {boolean} [options.singleReplica=false] - If true, builds single replica folders instead of full client.
644
644
  * @param {boolean} [options.buildZip=false] - If true, creates zip files of the builds.
645
+ * @param {string|number} [options.split=''] - Optional ZIP part size in MB. When set with buildZip, writes split parts.
646
+ * @param {string} [options.unzip=''] - Optional build ZIP prefix to extract from ./build.
647
+ * @param {string} [options.mergeZip=''] - Optional build prefix to merge split ZIP parts into a single ZIP.
645
648
  * @param {boolean} [options.liteBuild=false] - If true, skips full build (default is full build).
646
649
  * @param {boolean} [options.iconsBuild=false] - If true, builds icons.
647
650
  * @returns {Promise<boolean>} A promise that resolves when the build is complete.
@@ -656,12 +659,31 @@ class UnderpostRepository {
656
659
  syncEnvPort: false,
657
660
  singleReplica: false,
658
661
  buildZip: false,
662
+ split: '',
663
+ unzip: '',
664
+ mergeZip: '',
659
665
  liteBuild: false,
660
666
  iconsBuild: false,
661
667
  },
662
668
  ) {
663
669
  return new Promise(async (resolve, reject) => {
664
670
  try {
671
+ if (options.mergeZip) {
672
+ mergeClientBuildZip({
673
+ buildPrefix: options.mergeZip,
674
+ logger,
675
+ });
676
+ return resolve(true);
677
+ }
678
+
679
+ if (options.unzip) {
680
+ unzipClientBuild({
681
+ buildPrefix: options.unzip,
682
+ logger,
683
+ });
684
+ return resolve(true);
685
+ }
686
+
665
687
  // Handle singleReplica operation (must run before syncEnvPort to ensure replica dirs exist)
666
688
  if (options.singleReplica) {
667
689
  const replicaPath = path;
@@ -804,6 +826,9 @@ class UnderpostRepository {
804
826
  let argPath = path ? path.split(',') : [];
805
827
  let deployIdSingleReplicas = [];
806
828
  let singleReplicaHosts = [];
829
+ const isReplicaContext = resolvedDeployId
830
+ ? fs.existsSync(`./engine-private/replica/${resolvedDeployId}`)
831
+ : false;
807
832
  const serverConf = resolvedDeployId
808
833
  ? readConfJson(resolvedDeployId, 'server', { loadReplicas: true })
809
834
  : Config.default.server;
@@ -814,7 +839,7 @@ class UnderpostRepository {
814
839
  if (argHost.length && argPath.length && (!argHost.includes(host) || !argPath.includes(path))) {
815
840
  delete serverConf[host][path];
816
841
  } else {
817
- if (serverConf[host][path].singleReplica && serverConf[host][path].replicas) {
842
+ if (!isReplicaContext && serverConf[host][path].singleReplica && serverConf[host][path].replicas) {
818
843
  singleReplicaHosts.push({ host, path });
819
844
  deployIdSingleReplicas = deployIdSingleReplicas.concat(
820
845
  serverConf[host][path].replicas.map((replica) =>
@@ -827,10 +852,17 @@ class UnderpostRepository {
827
852
  }
828
853
  await buildClient({
829
854
  buildZip: options.buildZip || false,
855
+ split: options.split || '',
830
856
  fullBuild: options.liteBuild ? false : true,
831
857
  iconsBuild: options.iconsBuild || false,
832
858
  });
833
- for (const replicaDeployId of deployIdSingleReplicas) await Underpost.repo.client(replicaDeployId);
859
+ for (const replicaDeployId of deployIdSingleReplicas) {
860
+ if (!fs.existsSync(`./engine-private/replica/${replicaDeployId}`)) {
861
+ logger.warn('Skip replica client build: replica folder not found', { replicaDeployId });
862
+ continue;
863
+ }
864
+ await Underpost.repo.client(replicaDeployId);
865
+ }
834
866
 
835
867
  return resolve(true);
836
868
  }