underpost 3.2.9 → 3.2.10

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 (81) hide show
  1. package/.github/workflows/npmpkg.ci.yml +1 -0
  2. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  3. package/.github/workflows/release.cd.yml +1 -0
  4. package/.vscode/settings.json +10 -5
  5. package/CHANGELOG.md +122 -1
  6. package/CLI-HELP.md +22 -7
  7. package/README.md +37 -8
  8. package/bin/build.js +26 -9
  9. package/bin/deploy.js +20 -21
  10. package/bin/file.js +31 -13
  11. package/bin/index.js +2 -1
  12. package/bin/vs.js +1 -1
  13. package/bump.config.js +26 -0
  14. package/conf.js +20 -4
  15. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  16. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
  19. package/manifests/kind-config-dev.yaml +8 -0
  20. package/manifests/mongodb/pv-pvc.yaml +44 -8
  21. package/manifests/mongodb/statefulset.yaml +55 -68
  22. package/package.json +27 -12
  23. package/scripts/k3s-node-setup.sh +28 -9
  24. package/src/api/core/core.router.js +19 -14
  25. package/src/api/core/core.service.js +5 -5
  26. package/src/api/default/default.router.js +22 -18
  27. package/src/api/default/default.service.js +5 -5
  28. package/src/api/document/document.router.js +28 -23
  29. package/src/api/document/document.service.js +100 -23
  30. package/src/api/file/file.router.js +19 -13
  31. package/src/api/file/file.service.js +9 -7
  32. package/src/api/test/test.router.js +17 -12
  33. package/src/api/types.js +24 -0
  34. package/src/api/user/guest.service.js +5 -4
  35. package/src/api/user/user.router.js +297 -288
  36. package/src/api/user/user.service.js +100 -35
  37. package/src/cli/baremetal.js +20 -11
  38. package/src/cli/cluster.js +196 -55
  39. package/src/cli/db.js +59 -60
  40. package/src/cli/deploy.js +273 -159
  41. package/src/cli/fs.js +3 -1
  42. package/src/cli/index.js +16 -9
  43. package/src/cli/ipfs.js +4 -6
  44. package/src/cli/kubectl.js +4 -1
  45. package/src/cli/lxd.js +217 -135
  46. package/src/cli/release.js +289 -131
  47. package/src/cli/repository.js +58 -7
  48. package/src/cli/run.js +152 -25
  49. package/src/cli/test.js +9 -3
  50. package/src/client/Default.index.js +9 -3
  51. package/src/client/components/core/Auth.js +4 -0
  52. package/src/client/components/core/PanelForm.js +56 -52
  53. package/src/client/components/core/Worker.js +162 -363
  54. package/src/client/sw/core.sw.js +174 -112
  55. package/src/db/DataBaseProvider.js +120 -20
  56. package/src/db/mongo/MongoBootstrap.js +587 -0
  57. package/src/db/mongo/MongooseDB.js +126 -22
  58. package/src/index.js +1 -1
  59. package/src/runtime/express/Express.js +2 -2
  60. package/src/runtime/wp/Wp.js +8 -5
  61. package/src/server/auth.js +2 -2
  62. package/src/server/client-build-docs.js +1 -1
  63. package/src/server/client-build.js +94 -129
  64. package/src/server/conf.js +20 -65
  65. package/src/server/process.js +180 -19
  66. package/src/server/runtime.js +1 -1
  67. package/src/server/start.js +12 -4
  68. package/src/ws/IoInterface.js +16 -16
  69. package/src/ws/core/channels/core.ws.chat.js +11 -11
  70. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  71. package/src/ws/core/channels/core.ws.stream.js +19 -19
  72. package/src/ws/core/core.ws.connection.js +8 -8
  73. package/src/ws/core/core.ws.server.js +6 -5
  74. package/src/ws/default/channels/default.ws.main.js +10 -10
  75. package/src/ws/default/default.ws.connection.js +4 -4
  76. package/src/ws/default/default.ws.server.js +4 -3
  77. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  78. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  79. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  80. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  81. /package/src/client/ssr/{pages → views}/Test.js +0 -0
@@ -41,6 +41,20 @@ const logger = loggerFactory(import.meta);
41
41
  */
42
42
  const ENV_REF_PREFIX = 'env:';
43
43
 
44
+ /**
45
+ * Resolves a standardized context key from host/path descriptors.
46
+ * The key is used across DB, WS, mailer, and cache registries.
47
+ *
48
+ * @method resolveHostKeyContext
49
+ * @param {{host?: string, path?: string}|string} [context={ host: '', path: '' }] - Context object or prebuilt key.
50
+ * @returns {string} Host key context string.
51
+ * @memberof ServerConfBuilder
52
+ */
53
+ const resolveHostKeyContext = (context = { host: '', path: '' }) => {
54
+ if (typeof context === 'string') return context;
55
+ return `${context.host || ''}${context.path || ''}`;
56
+ };
57
+
44
58
  /**
45
59
  * Recursively walks a configuration object and replaces every string value that
46
60
  * starts with {@link ENV_REF_PREFIX} (`"env:"`) with the corresponding
@@ -1208,7 +1222,7 @@ const validateTemplatePath = (absolutePath = '') => {
1208
1222
  const confSsr = DefaultConf.ssr[ssr];
1209
1223
  const clients = DefaultConf.client.default.services;
1210
1224
 
1211
- if (absolutePath.match('src/api') && !confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))) {
1225
+ if (absolutePath.match('src/api') && !absolutePath.match('src/api/types.js') && !confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))) {
1212
1226
  return false;
1213
1227
  }
1214
1228
  if (absolutePath.match('conf.dd-') && absolutePath.match('.js')) return false;
@@ -1250,14 +1264,8 @@ const validateTemplatePath = (absolutePath = '') => {
1250
1264
  return false;
1251
1265
  }
1252
1266
  if (
1253
- absolutePath.match('src/client/ssr/offline') &&
1254
- !confSsr.offline.find((p) => absolutePath.match(`src/client/ssr/offline/${p.client}.js`))
1255
- ) {
1256
- return false;
1257
- }
1258
- if (
1259
- absolutePath.match('src/client/ssr/pages') &&
1260
- !confSsr.pages.find((p) => absolutePath.match(`src/client/ssr/pages/${p.client}.js`))
1267
+ absolutePath.match('src/client/ssr/views') &&
1268
+ !(confSsr.views || []).find((p) => absolutePath.match(`src/client/ssr/views/${p.client}.js`))
1261
1269
  ) {
1262
1270
  return false;
1263
1271
  }
@@ -1287,6 +1295,7 @@ const validateTemplatePath = (absolutePath = '') => {
1287
1295
  const awaitDeployMonitor = async (init = false, deltaMs = 1000) => {
1288
1296
  if (init) Underpost.env.set('await-deploy', new Date().toISOString());
1289
1297
  await timer(deltaMs);
1298
+ if (Underpost.env.get('container-status') === 'error') throw new Error('Container status error');
1290
1299
  if (Underpost.env.get('await-deploy')) return await awaitDeployMonitor();
1291
1300
  };
1292
1301
 
@@ -1312,59 +1321,6 @@ const mergeFile = async (parts = [], outputFilePath) => {
1312
1321
  });
1313
1322
  };
1314
1323
 
1315
- /**
1316
- * @method rebuildConfFactory
1317
- * @description Rebuilds the conf factory.
1318
- * @param {object} options - The options.
1319
- * @param {string} options.deployId - The deploy ID.
1320
- * @param {string} options.valkey - The valkey.
1321
- * @param {boolean} [options.mongo=false] - The mongo.
1322
- * @returns {object} - The rebuild conf factory.
1323
- * @memberof ServerConfBuilder
1324
- */
1325
- const rebuildConfFactory = ({ deployId, valkey, mongo }) => {
1326
- const confServer = loadReplicas(deployId, loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`));
1327
- const hosts = {};
1328
- for (const host of Object.keys(confServer)) {
1329
- hosts[host] = {};
1330
- for (const path of Object.keys(confServer[host])) {
1331
- if (!confServer[host][path].db) continue;
1332
- const { singleReplica, replicas, db } = confServer[host][path];
1333
- const { provider } = db;
1334
- if (singleReplica) {
1335
- for (const replica of replicas) {
1336
- const deployIdReplica = buildReplicaId({ replica, deployId });
1337
- const confServerReplica = loadConfServerJson(`./engine-private/replica/${deployIdReplica}/conf.server.json`);
1338
- for (const _host of Object.keys(confServerReplica)) {
1339
- for (const _path of Object.keys(confServerReplica[_host])) {
1340
- hosts[host][_path] = { replica: { host, path } };
1341
- confServerReplica[_host][_path].valkey = valkey;
1342
- switch (provider) {
1343
- case 'mongoose':
1344
- confServerReplica[_host][_path].db.host = mongo.host;
1345
- break;
1346
- }
1347
- }
1348
- }
1349
- fs.writeFileSync(
1350
- `./engine-private/replica/${deployIdReplica}/conf.server.json`,
1351
- JSON.stringify(confServerReplica, null, 4),
1352
- 'utf8',
1353
- );
1354
- }
1355
- } else hosts[host][path] = {};
1356
- confServer[host][path].valkey = valkey;
1357
- switch (provider) {
1358
- case 'mongoose':
1359
- confServer[host][path].db.host = mongo.host;
1360
- break;
1361
- }
1362
- }
1363
- }
1364
- fs.writeFileSync(`./engine-private/conf/${deployId}/conf.server.json`, JSON.stringify(confServer, null, 4), 'utf8');
1365
- return { hosts };
1366
- };
1367
-
1368
1324
  /**
1369
1325
  * @method getPathsSSR
1370
1326
  * @description Gets the paths SSR.
@@ -1377,8 +1333,7 @@ const getPathsSSR = (conf) => {
1377
1333
  for (const o of conf.head) paths.push(`src/client/ssr/head/${o}.js`);
1378
1334
  for (const o of conf.body) paths.push(`src/client/ssr/body/${o}.js`);
1379
1335
  for (const o of Object.keys(conf.mailer)) paths.push(`src/client/ssr/mailer/${conf.mailer[o]}.js`);
1380
- for (const o of conf.offline) paths.push(`src/client/ssr/mailer/${o.client}.js`);
1381
- for (const o of conf.pages) paths.push(`src/client/ssr/pages/${o.client}.js`);
1336
+ for (const o of conf.views || []) paths.push(`src/client/ssr/views/${o.client}.js`);
1382
1337
  return paths;
1383
1338
  };
1384
1339
 
@@ -1774,7 +1729,6 @@ export {
1774
1729
  pathPortAssignmentFactory,
1775
1730
  deployRangePortFactory,
1776
1731
  awaitDeployMonitor,
1777
- rebuildConfFactory,
1778
1732
  buildCliDoc,
1779
1733
  getInstanceContext,
1780
1734
  buildApiConf,
@@ -1784,6 +1738,7 @@ export {
1784
1738
  devProxyHostFactory,
1785
1739
  isTlsDevProxy,
1786
1740
  getTlsHosts,
1741
+ resolveHostKeyContext,
1787
1742
  resolveConfSecrets,
1788
1743
  loadConfServerJson,
1789
1744
  getConfFolder,
@@ -1,6 +1,22 @@
1
1
  /**
2
2
  * Module for process and shell command management.
3
- * Provides utilities for executing shell commands, managing signals, and handling environment details.
3
+ * Provides utilities for executing shell commands, managing signals, and
4
+ * handling environment details.
5
+ *
6
+ * Execution semantics:
7
+ * - `shellExec(cmd)` throws `ShellExecError` on non-zero exit (fail-fast
8
+ * is the default). CI/CD chains observe the failure end-to-end.
9
+ * - `shellExec(cmd, { silentOnError: true })` opts out — returns the
10
+ * `ShellString` result with `.code/.stdout/.stderr` so callers can
11
+ * branch on the exit code themselves. Use for existence checks
12
+ * (`test -x …`, `command -v …`, `kubectl get` when "missing" is a
13
+ * normal answer).
14
+ * - `shellExec(cmd, { cwd: "..." })` runs hermetically in `cwd` without
15
+ * touching shelljs's global state.
16
+ * - All children spawned by `shellExec` register in
17
+ * `ProcessController.children` so SIGINT/SIGTERM forwarding can reach
18
+ * them before the parent exits.
19
+ *
4
20
  * @module src/server/process.js
5
21
  * @namespace Process
6
22
  */
@@ -19,6 +35,12 @@ const logger = loggerFactory(import.meta);
19
35
  const getRootDirectory = () => process.cwd().replace(/\\/g, '/');
20
36
  /**
21
37
  * Controls and manages process-level events and signals.
38
+ *
39
+ * Subprocess registry: any child process tracked here will receive
40
+ * SIGTERM (followed by SIGKILL after a short grace period) when the
41
+ * parent receives SIGINT or SIGTERM. This prevents orphaned children
42
+ * during Ctrl+C in dev and during pod-termination in K8S.
43
+ *
22
44
  * @namespace ProcessController
23
45
  */
24
46
  class ProcessController {
@@ -41,9 +63,41 @@ class ProcessController {
41
63
  'SIGSEGV',
42
64
  'SIGILL',
43
65
  ];
66
+
67
+ /**
68
+ * Registry of currently running tracked child processes.
69
+ * Populated when callers spawn via the streaming Node-native path
70
+ * (future expansion). The sets are exposed so signal handlers and
71
+ * test harnesses can introspect / clean up the registry.
72
+ */
73
+ static children = new Set();
74
+
75
+ /** Internal: forward terminating signals to all tracked children. */
76
+ static _forwardToChildren(sig) {
77
+ if (ProcessController.children.size === 0) return;
78
+ for (const child of [...ProcessController.children]) {
79
+ try {
80
+ if (!child.killed) child.kill(sig);
81
+ } catch (_) {
82
+ // child may already have exited; ignore.
83
+ }
84
+ }
85
+ // Hard SIGKILL after 5s grace if any child is still alive.
86
+ setTimeout(() => {
87
+ for (const child of [...ProcessController.children]) {
88
+ try {
89
+ if (!child.killed) child.kill('SIGKILL');
90
+ } catch (_) {
91
+ /* noop */
92
+ }
93
+ }
94
+ }, 5000).unref();
95
+ }
96
+
44
97
  /**
45
98
  * Sets up listeners for various process signals defined in {@link ProcessController.SIG}.
46
- * Handles graceful exit on 'SIGINT' (Ctrl+C).
99
+ * Handles graceful exit on 'SIGINT' (Ctrl+C) — but first forwards the
100
+ * signal to every tracked child so they get a chance to clean up.
47
101
  * @memberof ProcessController
48
102
  * @returns {Array<process.Process>} An array of process listener handles.
49
103
  */
@@ -53,7 +107,14 @@ class ProcessController {
53
107
  ProcessController.logger.info(`process on ${sig}`, args);
54
108
  switch (sig) {
55
109
  case 'SIGINT':
56
- return process.exit();
110
+ case 'SIGTERM':
111
+ case 'SIGHUP':
112
+ ProcessController._forwardToChildren('SIGTERM');
113
+ // Give children a moment to exit cleanly before our own exit.
114
+ if (sig === 'SIGINT') {
115
+ setTimeout(() => process.exit(130), 200).unref();
116
+ }
117
+ break;
57
118
  default:
58
119
  break;
59
120
  }
@@ -71,33 +132,111 @@ class ProcessController {
71
132
  ProcessController.logger = logger;
72
133
  process.on('exit', (...args) => {
73
134
  ProcessController.logger.info(`process on exit`, args);
135
+ // Last-chance reap: any tracked child still alive at exit time
136
+ // gets a hard kill so the parent does not leak orphans into the
137
+ // pod / shell session.
138
+ ProcessController._forwardToChildren('SIGKILL');
74
139
  });
75
140
  ProcessController.onSigListen();
76
- Underpost.env.delete('await-deploy');
141
+ }
142
+ }
143
+ /**
144
+ * `ShellExecError` — thrown by `shellExec` when the underlying command
145
+ * exits with a non-zero code (the default fail-fast behaviour). Carries
146
+ * the exit code, stdout, and stderr for inspection by callers / CI
147
+ * pipelines that need structured failure data.
148
+ */
149
+ class ShellExecError extends Error {
150
+ constructor(cmd, code, stdout, stderr) {
151
+ super(`shellExec failed (exit=${code}): ${cmd}`);
152
+ this.name = 'ShellExecError';
153
+ this.cmd = cmd;
154
+ this.code = code;
155
+ this.stdout = stdout;
156
+ this.stderr = stderr;
77
157
  }
78
158
  }
79
159
  /**
80
160
  * Executes a shell command using shelljs.
161
+ *
162
+ * **Default behaviour is fail-fast**: a non-zero exit code throws
163
+ * `ShellExecError`. Callers that need to branch on the exit code
164
+ * (existence checks, optional commands) must pass `silentOnError: true`
165
+ * to opt out of throwing.
166
+ *
167
+ * The async-callback path is exempt from the throw — shelljs delivers
168
+ * `(code, stdout, stderr)` to the callback, which owns its own error
169
+ * handling.
170
+ *
81
171
  * @memberof Process
82
172
  * @param {string} cmd - The command string to execute.
83
173
  * @param {Object} [options] - Options for execution.
84
- * @param {boolean} [options.silent=false] - Suppress output from shell commands.
85
- * @param {boolean} [options.async=false] - Run command asynchronously.
86
- * @param {boolean} [options.stdout=false] - Return stdout content (string) instead of shelljs result object.
87
- * @param {boolean} [options.disableLog=false] - Prevent logging of the command.
88
- * @param {Function} [options.callback=null] - Callback function for asynchronous execution.
89
- * @returns {string|shelljs.ShellString} The result of the shell command (string if `stdout: true`, otherwise a ShellString object).
174
+ * @param {boolean} [options.silent=false] - Suppress child stdout/stderr to the parent terminal.
175
+ * @param {boolean} [options.async=false] - Run the command asynchronously (use with `callback`).
176
+ * @param {boolean} [options.stdout=false] - Return stdout string instead of the `ShellString` result object.
177
+ * @param {boolean} [options.disableLog=false] - Skip the `[process] cmd …` info log line.
178
+ * @param {Function} [options.callback=null] - Async callback `(code, stdout, stderr) => void` when `async: true`.
179
+ * @param {boolean} [options.silentOnError=false] - When `true`, swallow non-zero exits and return the `ShellString` instead of throwing. Inverse of the previous `throwOnError` flag.
180
+ * @param {string} [options.cwd] - Hermetic working directory (snapshotted + restored — does NOT leak).
181
+ * @returns {string|shelljs.ShellString} `ShellString` by default; the stdout string when `stdout: true`.
182
+ * @throws {ShellExecError} On non-zero exit when `silentOnError` is not set.
90
183
  */
91
- const shellExec = (
92
- cmd,
93
- options = { silent: false, async: false, stdout: false, disableLog: false, callback: null },
94
- ) => {
184
+ const shellExec = (cmd, options = {}) => {
95
185
  if (!options.disableLog) logger.info(`cmd`, cmd);
96
- if (options.callback) return shell.exec(cmd, options, options.callback);
97
- return options.stdout ? shell.exec(cmd, options).stdout : shell.exec(cmd, options);
186
+
187
+ // Whitelist exactly the keys `shelljs.exec` understands. Passing our own
188
+ // bookkeeping keys through (or a literal `cwd: undefined`) makes shelljs
189
+ // call `path.resolve(undefined)` and crash with ERR_INVALID_ARG_TYPE.
190
+ const shellOpts = {};
191
+ if (options.silent !== undefined) shellOpts.silent = options.silent;
192
+ if (options.async !== undefined) shellOpts.async = options.async;
193
+
194
+ // Hermetic cwd. shelljs.cd mutates a process-wide global; instead we
195
+ // snapshot the current cwd here, switch for the duration of this call,
196
+ // and restore in `finally`. We deliberately do NOT forward `cwd` to
197
+ // shelljs — leaving its `cwd` unset means it inherits our just-changed
198
+ // `process.cwd()`, and we keep full control of restore semantics.
199
+ const previousCwd = options.cwd ? process.cwd() : null;
200
+ if (options.cwd) {
201
+ try {
202
+ process.chdir(options.cwd);
203
+ } catch (err) {
204
+ if (Underpost.env.isInsideContainer()) Underpost.env.set('container-status', 'error')
205
+ throw new ShellExecError(cmd, -1, '', `chdir(${options.cwd}) failed: ${err.message}`);
206
+ }
207
+ }
208
+ try {
209
+ if (options.callback) {
210
+ // Async path. shelljs invokes the callback with (code, stdout, stderr).
211
+ // The callback owns its own error handling; the throw default does
212
+ // not apply here.
213
+ return shell.exec(cmd, shellOpts, options.callback);
214
+ }
215
+ const result = shell.exec(cmd, shellOpts);
216
+
217
+ if (!options.silentOnError && result && typeof result.code === 'number' && result.code !== 0) {
218
+ if (Underpost.env.isInsideContainer()) Underpost.env.set('container-status', 'error')
219
+ throw new ShellExecError(cmd, result.code, result.stdout || '', result.stderr || '');
220
+ }
221
+
222
+ return options.stdout ? result.stdout : result;
223
+ } finally {
224
+ if (previousCwd) {
225
+ try {
226
+ process.chdir(previousCwd);
227
+ } catch (_) {
228
+ /* best-effort restore */
229
+ }
230
+ }
231
+ }
98
232
  };
99
233
  /**
100
234
  * Changes the current working directory using shelljs.
235
+ *
236
+ * Note: `shellCd` mutates global state. Prefer `shellExec(cmd, { cwd })`
237
+ * for one-shot directory-scoped commands; use `shellCd` only for the
238
+ * outermost shell where the cwd should persist across many calls.
239
+ *
101
240
  * @memberof Process
102
241
  * @param {string} cd - The path to change the directory to.
103
242
  * @param {Object} [options] - Options for the CD operation.
@@ -110,6 +249,11 @@ const shellCd = (cd, options = { disableLog: false }) => {
110
249
  };
111
250
  /**
112
251
  * Wraps a command to run it as a daemon process in a shell (keeping the process alive/terminal open).
252
+ *
253
+ * NB: callers must ensure `cmd` does not contain unescaped single quotes —
254
+ * the wrapper uses `bash -c '<cmd>; …'`. For arbitrary user input prefer
255
+ * a heredoc or a temporary script file.
256
+ *
113
257
  * @memberof Process
114
258
  * @param {string} cmd - The command to daemonize.
115
259
  * @returns {string} The shell command string for the daemon process.
@@ -119,11 +263,19 @@ const daemonProcess = (cmd) => `exec bash -c '${cmd}; exec tail -f /dev/null'`;
119
263
  * Retrieves the process ID (PID) of the most recently created gnome-terminal instance.
120
264
  * Note: This function is environment-specific (GNOME/Linux) and uses `pgrep -n`.
121
265
  * @memberof Process
122
- * @returns {number} The PID of the last gnome-terminal process.
266
+ * @returns {number|null} The PID of the last gnome-terminal process, or null if none running.
123
267
  */
124
268
  // list all terminals: pgrep gnome-terminal
125
269
  // list last terminal: pgrep -n gnome-terminal
126
- const getTerminalPid = () => JSON.parse(shellExec(`pgrep -n gnome-terminal`, { stdout: true, silent: true }));
270
+ const getTerminalPid = () => {
271
+ const raw = shellExec(`pgrep -n gnome-terminal`, { stdout: true, silent: true, silentOnError: true });
272
+ if (!raw || !raw.trim()) return null;
273
+ try {
274
+ return JSON.parse(raw);
275
+ } catch {
276
+ return null;
277
+ }
278
+ };
127
279
  /**
128
280
  * Copies text content to the system clipboard using clipboardy.
129
281
  * Logs the copied content for confirmation.
@@ -135,4 +287,13 @@ function pbcopy(data) {
135
287
  clipboard.writeSync(data || '🦄');
136
288
  logger.info(`copied to clipboard`, clipboard.readSync());
137
289
  }
138
- export { ProcessController, getRootDirectory, shellExec, shellCd, pbcopy, getTerminalPid, daemonProcess };
290
+ export {
291
+ ProcessController,
292
+ ShellExecError,
293
+ getRootDirectory,
294
+ shellExec,
295
+ shellCd,
296
+ pbcopy,
297
+ getTerminalPid,
298
+ daemonProcess,
299
+ };
@@ -176,7 +176,7 @@ const buildRuntime = async () => {
176
176
  }
177
177
 
178
178
  if (Lampp.enabled() && Lampp.router) Lampp.initService();
179
-
179
+ Underpost.env.delete('await-deploy');
180
180
  Underpost.start.logRuntimeRouter();
181
181
  };
182
182
 
@@ -52,7 +52,7 @@ class UnderpostStartUp {
52
52
  * @param {Function} logic - The logic to execute when the server is listening.
53
53
  * @returns {Object} An object with a listen method.
54
54
  */
55
- listenServerFactory: (logic = async () => {}) => {
55
+ listenServerFactory: (logic = async () => { }) => {
56
56
  return {
57
57
  listen: async (...args) => {
58
58
  const msDelta = 1000;
@@ -198,20 +198,28 @@ class UnderpostStartUp {
198
198
  */
199
199
  async run(deployId = 'dd-default', env = 'development', options = {}) {
200
200
  const runCmd = env === 'production' ? 'run prod:container' : 'run dev:container';
201
+ const makeDeployCallback = (cmd) => (code, out, msg) => {
202
+ if (code !== 0) {
203
+ logger.error(`Deployment process exited with code ${code}`, { cmd, msg });
204
+ Underpost.env.set('container-status', 'error');
205
+ }
206
+ };
201
207
  if (fs.existsSync(`./engine-private/replica`)) {
202
208
  const replicas = await fs.readdir(`./engine-private/replica`);
203
209
  for (const replica of replicas) {
204
210
  if (!replica.match(deployId)) continue;
205
211
  shellExec(`node bin env ${replica} ${env}`);
206
- shellExec(`npm ${runCmd} ${replica}`, { async: true });
212
+ const replicaCmd = `npm ${runCmd} ${replica}`;
213
+ shellExec(replicaCmd, { async: true, callback: makeDeployCallback(replicaCmd) });
207
214
  await awaitDeployMonitor(true);
208
215
  }
209
216
  }
210
217
  shellExec(`node bin env ${deployId} ${env}`);
211
- shellExec(`npm ${runCmd} ${deployId}`, { async: true });
218
+ const deployCmd = `npm ${runCmd} ${deployId}`;
219
+ shellExec(deployCmd, { async: true, callback: makeDeployCallback(deployCmd) });
212
220
  await awaitDeployMonitor(true);
213
221
  if (env === 'production' && Underpost.env.isInsideContainer()) Underpost.secret.globalSecretClean();
214
- Underpost.env.set('container-status', `${deployId}-${env}-running-deployment`);
222
+ if (Underpost.env.get('container-status') !== 'error') Underpost.env.set('container-status', `${deployId}-${env}-running-deployment`);
215
223
  },
216
224
  };
217
225
  }
@@ -46,9 +46,9 @@ class IoChannel {
46
46
  constructor(IoInterface) {
47
47
  this.#IoInterface = {
48
48
  channel: '',
49
- connection: async (socket = {}, client = {}, wsManagementId = '') => {},
50
- controller: async (socket = {}, client = {}, payload = {}, wsManagementId = '', args = []) => {},
51
- disconnect: async (socket = {}, client = {}, reason = '', wsManagementId = '') => {},
49
+ connection: async (socket = {}, client = {}, hostKeyContext = '') => { },
50
+ controller: async (socket = {}, client = {}, payload = {}, hostKeyContext = '', args = []) => { },
51
+ disconnect: async (socket = {}, client = {}, reason = '', hostKeyContext = '') => { },
52
52
  stream: false,
53
53
  ...IoInterface,
54
54
  };
@@ -68,18 +68,18 @@ class IoChannel {
68
68
  * Sets up the listener for the channel message.
69
69
  *
70
70
  * @param {Socket} socket - The Socket.IO socket object.
71
- * @param {string} wsManagementId - Unique identifier for the WebSocket management context.
71
+ * @param {string} hostKeyContext - Unique identifier for the WebSocket management context.
72
72
  * @returns {Promise<void>}
73
73
  */
74
- async connection(socket, wsManagementId) {
74
+ async connection(socket, hostKeyContext) {
75
75
  try {
76
76
  this.client[socket.id] = socket;
77
77
  // Use bind/arrow function to maintain 'this' context for the controller
78
- socket.on(this.channel, (...args) => this.controller(socket, args, wsManagementId));
79
- await this.#IoInterface.connection(socket, this.client, wsManagementId);
78
+ socket.on(this.channel, (...args) => this.controller(socket, args, hostKeyContext));
79
+ await this.#IoInterface.connection(socket, this.client, hostKeyContext);
80
80
  logger.debug(`Socket ${socket.id} connected to channel ${this.channel}`);
81
81
  } catch (error) {
82
- logger.error(error, { channel: this.channel, wsManagementId, stack: error.stack });
82
+ logger.error(error, { channel: this.channel, hostKeyContext, stack: error.stack });
83
83
  }
84
84
  }
85
85
 
@@ -89,10 +89,10 @@ class IoChannel {
89
89
  * @method
90
90
  * @param {Socket} socket - The Socket.IO socket object.
91
91
  * @param {any[]} args - The raw arguments received from the socket event.
92
- * @param {string} wsManagementId - Unique identifier for the WebSocket management context.
92
+ * @param {string} hostKeyContext - Unique identifier for the WebSocket management context.
93
93
  * @returns {Promise<void>}
94
94
  */
95
- async controller(socket, args, wsManagementId) {
95
+ async controller(socket, args, hostKeyContext) {
96
96
  try {
97
97
  if (!args || args.length === 0) {
98
98
  logger.warn(`No arguments received for channel: ${this.channel}`, { socketId: socket.id });
@@ -101,9 +101,9 @@ class IoChannel {
101
101
  // Determine if JSON parsing is needed based on the stream flag
102
102
  const payload = this.#IoInterface.stream ? args[0] : JSON.parse(args[0]);
103
103
 
104
- await this.#IoInterface.controller(socket, this.client, payload, wsManagementId, args);
104
+ await this.#IoInterface.controller(socket, this.client, payload, hostKeyContext, args);
105
105
  } catch (error) {
106
- logger.error(error, { channel: this.channel, wsManagementId, socketId: socket.id, args, stack: error.stack });
106
+ logger.error(error, { channel: this.channel, hostKeyContext, socketId: socket.id, args, stack: error.stack });
107
107
  }
108
108
  }
109
109
 
@@ -112,16 +112,16 @@ class IoChannel {
112
112
  *
113
113
  * @param {Socket} socket - The Socket.IO socket object.
114
114
  * @param {string} reason - The reason for disconnection (e.g., 'client namespace disconnect').
115
- * @param {string} wsManagementId - Unique identifier for the WebSocket management context.
115
+ * @param {string} hostKeyContext - Unique identifier for the WebSocket management context.
116
116
  * @returns {Promise<void>}
117
117
  */
118
- async disconnect(socket, reason, wsManagementId) {
118
+ async disconnect(socket, reason, hostKeyContext) {
119
119
  try {
120
- await this.#IoInterface.disconnect(socket, this.client, reason, wsManagementId);
120
+ await this.#IoInterface.disconnect(socket, this.client, reason, hostKeyContext);
121
121
  delete this.client[socket.id];
122
122
  logger.debug(`Socket ${socket.id} disconnected from channel ${this.channel}. Reason: ${reason}`);
123
123
  } catch (error) {
124
- logger.error(error, { channel: this.channel, wsManagementId, reason, socketId: socket.id, stack: error.stack });
124
+ logger.error(error, { channel: this.channel, hostKeyContext, reason, socketId: socket.id, stack: error.stack });
125
125
  }
126
126
  }
127
127
  }
@@ -12,13 +12,13 @@ import { CoreWsEmitter } from '../core.ws.emit.js';
12
12
  * Broadcasts incoming messages to all other connected sockets.
13
13
  */
14
14
  class CoreWsChatChannel {
15
- /** @type {Object.<string, Object>} Per-instance state keyed by wsManagementId. */
15
+ /** @type {Object.<string, Object>} Per-instance state keyed by hostKeyContext. */
16
16
  static #state = {};
17
17
 
18
18
  /** @type {IoChannel} */
19
19
  static #io = new IoChannel({
20
20
  channel: 'chat',
21
- controller(socket, client, payload, wsManagementId) {
21
+ controller(socket, client, payload, hostKeyContext) {
22
22
  for (const socketId of Object.keys(client)) {
23
23
  if (socketId !== socket.id) {
24
24
  CoreWsEmitter.emit('chat', client[socketId], { id: socket.id, ...payload });
@@ -39,29 +39,29 @@ class CoreWsChatChannel {
39
39
 
40
40
  /**
41
41
  * Initializes state for a server instance.
42
- * @param {string} wsManagementId - Unique server context ID.
42
+ * @param {string} hostKeyContext - Unique server context ID.
43
43
  */
44
- static init(wsManagementId) {
45
- this.#state[wsManagementId] = {};
44
+ static init(hostKeyContext) {
45
+ this.#state[hostKeyContext] = {};
46
46
  }
47
47
 
48
48
  /**
49
49
  * Registers a socket connection.
50
50
  * @param {import('socket.io').Socket} socket
51
- * @param {string} wsManagementId
51
+ * @param {string} hostKeyContext
52
52
  */
53
- static connection(socket, wsManagementId) {
54
- return this.#io.connection(socket, wsManagementId);
53
+ static connection(socket, hostKeyContext) {
54
+ return this.#io.connection(socket, hostKeyContext);
55
55
  }
56
56
 
57
57
  /**
58
58
  * Handles socket disconnection.
59
59
  * @param {import('socket.io').Socket} socket
60
60
  * @param {string} reason
61
- * @param {string} wsManagementId
61
+ * @param {string} hostKeyContext
62
62
  */
63
- static disconnect(socket, reason, wsManagementId) {
64
- return this.#io.disconnect(socket, reason, wsManagementId);
63
+ static disconnect(socket, reason, hostKeyContext) {
64
+ return this.#io.disconnect(socket, reason, hostKeyContext);
65
65
  }
66
66
  }
67
67