@underpostnet/underpost 2.99.8 → 3.0.1

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 (48) hide show
  1. package/.env.production +1 -0
  2. package/.github/workflows/gitlab.ci.yml +20 -0
  3. package/.github/workflows/publish.ci.yml +18 -34
  4. package/.vscode/extensions.json +8 -50
  5. package/.vscode/settings.json +0 -77
  6. package/CHANGELOG.md +116 -1
  7. package/{cli.md → CLI-HELP.md} +48 -41
  8. package/README.md +3 -3
  9. package/bin/build.js +1 -15
  10. package/bin/deploy.js +4 -133
  11. package/bin/file.js +10 -8
  12. package/bin/zed.js +63 -2
  13. package/jsdoc.json +1 -2
  14. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  15. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  16. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  17. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  18. package/manifests/deployment/fastapi/initial_data.sh +4 -52
  19. package/manifests/ipfs/configmap.yaml +57 -0
  20. package/manifests/ipfs/headless-service.yaml +35 -0
  21. package/manifests/ipfs/kustomization.yaml +8 -0
  22. package/manifests/ipfs/statefulset.yaml +149 -0
  23. package/manifests/ipfs/storage-class.yaml +9 -0
  24. package/package.json +9 -5
  25. package/scripts/k3s-node-setup.sh +89 -0
  26. package/scripts/lxd-vm-setup.sh +23 -0
  27. package/scripts/rocky-setup.sh +1 -13
  28. package/src/api/user/user.router.js +0 -47
  29. package/src/cli/baremetal.js +7 -9
  30. package/src/cli/cluster.js +72 -121
  31. package/src/cli/deploy.js +8 -5
  32. package/src/cli/index.js +31 -30
  33. package/src/cli/ipfs.js +184 -0
  34. package/src/cli/lxd.js +192 -237
  35. package/src/cli/repository.js +4 -1
  36. package/src/cli/run.js +3 -2
  37. package/src/client/components/core/Docs.js +92 -6
  38. package/src/client/components/core/VanillaJs.js +36 -25
  39. package/src/client/services/user/user.management.js +0 -5
  40. package/src/client/services/user/user.service.js +1 -1
  41. package/src/index.js +12 -1
  42. package/src/runtime/express/Express.js +3 -2
  43. package/src/server/client-build-docs.js +178 -41
  44. package/src/server/conf.js +1 -1
  45. package/src/server/logger.js +22 -10
  46. package/.vscode/zed.keymap.json +0 -39
  47. package/.vscode/zed.settings.json +0 -20
  48. package/manifests/lxd/underpost-setup.sh +0 -163
@@ -1,9 +1,9 @@
1
1
  import { Badge } from './Badge.js';
2
2
  import { BtnIcon } from './BtnIcon.js';
3
- import { Css, renderCssAttr, simpleIconsRender, ThemeEvents, Themes } from './Css.js';
3
+ import { Css, darkTheme, renderCssAttr, simpleIconsRender, ThemeEvents, Themes } from './Css.js';
4
4
  import { buildBadgeToolTipMenuOption, Modal, renderViewTitle } from './Modal.js';
5
5
  import { listenQueryPathInstance, setQueryPath, closeModalRouteChangeEvent, getProxyPath } from './Router.js';
6
- import { htmls, s } from './VanillaJs.js';
6
+ import { htmls, s, sIframe } from './VanillaJs.js';
7
7
 
8
8
  // https://mintlify.com/docs/quickstart
9
9
 
@@ -39,6 +39,7 @@ const Docs = {
39
39
  RouterInstance: Modal.Data['modal-docs'].options.RouterInstance,
40
40
  });
41
41
  const iframeEl = s(`.iframe-${ModalId}`);
42
+ let swaggerThemeEventKey = null;
42
43
  if (iframeEl) {
43
44
  iframeEl.addEventListener('load', () => {
44
45
  try {
@@ -51,7 +52,95 @@ const Docs = {
51
52
  // cross-origin or security restriction — safe to ignore
52
53
  }
53
54
  window.scrollTo(0, 0);
55
+ // Bind Shift+K inside the iframe to focus the parent SearchBox (mirrors app-wide shortcut)
56
+ try {
57
+ const iframeDoc = iframeEl.contentDocument || iframeEl.contentWindow?.document;
58
+ if (iframeDoc) {
59
+ iframeDoc.addEventListener('keydown', (e) => {
60
+ if (e.shiftKey && e.key.toLowerCase() === 'k') {
61
+ e.preventDefault();
62
+ e.stopPropagation();
63
+ if (s(`.top-bar-search-box`)) {
64
+ if (s(`.main-body-btn-ui-close`) && s(`.main-body-btn-ui-close`).classList.contains('hide')) {
65
+ s(`.main-body-btn-ui-open`).click();
66
+ }
67
+ s(`.top-bar-search-box`).blur();
68
+ s(`.top-bar-search-box`).focus();
69
+ s(`.top-bar-search-box`).select();
70
+ }
71
+ }
72
+ });
73
+ }
74
+ } catch (e) {
75
+ // cross-origin or security restriction — safe to ignore
76
+ }
54
77
  });
78
+
79
+ if (type === 'src') {
80
+ swaggerThemeEventKey = `jsdocs-iframe-${ModalId}`;
81
+
82
+ const applyJsDocsTheme = (isDark) => {
83
+ try {
84
+ const iframeWin = iframeEl.contentWindow;
85
+ if (!iframeWin) return;
86
+ const theme = isDark ? 'dark' : 'light';
87
+ if (typeof iframeWin.updateTheme === 'function') {
88
+ // clean-jsdoc-theme exposes updateTheme() globally
89
+ iframeWin.updateTheme(theme);
90
+ } else {
91
+ // Fallback: replicate localUpdateTheme manually
92
+ const iframeDoc = iframeEl.contentDocument || iframeWin.document;
93
+ if (!iframeDoc || !iframeDoc.body) return;
94
+ iframeDoc.body.setAttribute('data-theme', theme);
95
+ iframeDoc.body.classList.remove('dark', 'light');
96
+ iframeDoc.body.classList.add(theme);
97
+ const iconID = isDark ? '#light-theme-icon' : '#dark-theme-icon';
98
+ const svgUses = sIframe(iframeEl, '.theme-svg-use') ? iframeDoc.querySelectorAll('.theme-svg-use') : [];
99
+ svgUses.forEach((svg) => svg.setAttribute('xlink:href', iconID));
100
+ iframeWin.localStorage?.setItem('theme', theme);
101
+ }
102
+ } catch (e) {
103
+ // cross-origin or security restriction — safe to ignore
104
+ }
105
+ };
106
+
107
+ // Apply current theme as soon as the iframe content is ready
108
+ iframeEl.addEventListener('load', () => applyJsDocsTheme(darkTheme));
109
+
110
+ // Keep in sync whenever the parent page theme changes
111
+ ThemeEvents[swaggerThemeEventKey] = () => {
112
+ if (s(`.iframe-${ModalId}`)) applyJsDocsTheme(darkTheme);
113
+ };
114
+ }
115
+
116
+ if (type === 'api') {
117
+ swaggerThemeEventKey = `swagger-iframe-${ModalId}`;
118
+
119
+ const applySwaggerTheme = (isDark) => {
120
+ try {
121
+ const iframeDoc = iframeEl.contentDocument || iframeEl.contentWindow?.document;
122
+ if (!iframeDoc || !iframeDoc.body) return;
123
+ if (isDark) {
124
+ iframeDoc.body.classList.add('swagger-dark');
125
+ } else {
126
+ iframeDoc.body.classList.remove('swagger-dark');
127
+ }
128
+ iframeEl.contentWindow?.localStorage?.setItem('swagger-theme', isDark ? 'dark' : 'light');
129
+ const toggleBtn = sIframe(iframeEl, '#swagger-theme-toggle');
130
+ if (toggleBtn) toggleBtn.textContent = isDark ? '\u2600\uFE0F Light Mode' : '\uD83C\uDF19 Dark Mode';
131
+ } catch (e) {
132
+ // cross-origin or security restriction — safe to ignore
133
+ }
134
+ };
135
+
136
+ // Apply current theme as soon as the iframe content is ready
137
+ iframeEl.addEventListener('load', () => applySwaggerTheme(darkTheme));
138
+
139
+ // Keep in sync whenever the parent page theme changes
140
+ ThemeEvents[swaggerThemeEventKey] = () => {
141
+ if (s(`.iframe-${ModalId}`)) applySwaggerTheme(darkTheme);
142
+ };
143
+ }
55
144
  }
56
145
  Modal.Data[ModalId].onObserverListener[ModalId] = () => {
57
146
  if (s(`.iframe-${ModalId}`)) {
@@ -67,6 +156,7 @@ const Docs = {
67
156
  };
68
157
  Modal.Data[ModalId].onObserverListener[ModalId]();
69
158
  Modal.Data[ModalId].onCloseListener[ModalId] = () => {
159
+ if (swaggerThemeEventKey) delete ThemeEvents[swaggerThemeEventKey];
70
160
  closeModalRouteChangeEvent({ closedId: ModalId });
71
161
  };
72
162
  },
@@ -342,10 +432,6 @@ const Docs = {
342
432
  <div class="docs-landing">
343
433
  <div class="docs-header">
344
434
  <h1>Documentation</h1>
345
- <p>
346
- Find everything you need to build amazing applications with our platform. Get started with our guides, API
347
- reference, and examples.
348
- </p>
349
435
  <!--
350
436
  <div class="search-bar">
351
437
  <i class="fas fa-search"></i>
@@ -7,31 +7,6 @@
7
7
  import { s4 } from './CommonJs.js';
8
8
  import { windowGetH, windowGetW } from './windowGetDimensions.js';
9
9
 
10
- /*
11
-
12
- Name: es6-string-html
13
- Id: Tobermory.es6-string-html
14
- Description: Syntax highlighting in es6 multiline strings
15
- Version: 2.12.1
16
- Publisher: Tobermory
17
- VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html
18
-
19
- Name: es6-string-css
20
- Id: bashmish.es6-string-css
21
- Description: Highlight CSS language in ES6 template literals
22
- Version: 0.1.0
23
- Publisher: Mikhail Bashkirov
24
- VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=bashmish.es6-string-css
25
-
26
- Name: lit-html
27
- Id: bierner.lit-html
28
- Description: Syntax highlighting and IntelliSense for html inside of JavaScript and TypeScript tagged template strings
29
- Version: 1.11.1
30
- Publisher: Matt Bierner
31
- VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=bierner.lit-html
32
-
33
- */
34
-
35
10
  /**
36
11
  * Query selector.
37
12
  *
@@ -443,12 +418,48 @@ function hexToRgbA(hex) {
443
418
 
444
419
  const htmlStrSanitize = (str) => (str ? str.replace(/<\/?[^>]+(>|$)/g, '').trim() : '');
445
420
 
421
+ /**
422
+ * Query selector inside an iframe. Allows obtaining a single element that is
423
+ * inside an iframe in order to execute events on it.
424
+ * Note: the iframe must be same-origin for this to work.
425
+ *
426
+ * @param {string|Element} iframeEl The CSS selector string or the iframe Element itself.
427
+ * @param {string} el The query selector for the element inside the iframe.
428
+ * @returns {Element|null} The matched element inside the iframe, or null if not found.
429
+ * @memberof VanillaJS
430
+ */
431
+ const sIframe = (iframeEl, el) => {
432
+ const iframe = typeof iframeEl === 'string' ? s(iframeEl) : iframeEl;
433
+ if (!iframe) return null;
434
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
435
+ return iframeDoc ? iframeDoc.querySelector(el) : null;
436
+ };
437
+
438
+ /**
439
+ * Query selector all inside an iframe. Allows obtaining all elements matching a
440
+ * selector that are inside an iframe in order to execute events on them.
441
+ * Note: the iframe must be same-origin for this to work.
442
+ *
443
+ * @param {string|Element} iframeEl The CSS selector string or the iframe Element itself.
444
+ * @param {string} el The query selector for the elements inside the iframe.
445
+ * @returns {NodeList|null} A NodeList of matched elements inside the iframe, or null if not found.
446
+ * @memberof VanillaJS
447
+ */
448
+ const saIframe = (iframeEl, el) => {
449
+ const iframe = typeof iframeEl === 'string' ? s(iframeEl) : iframeEl;
450
+ if (!iframe) return null;
451
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
452
+ return iframeDoc ? iframeDoc.querySelectorAll(el) : null;
453
+ };
454
+
446
455
  export {
447
456
  s,
448
457
  htmls,
449
458
  append,
450
459
  prepend,
451
460
  sa,
461
+ sIframe,
462
+ saIframe,
452
463
  copyData,
453
464
  pasteData,
454
465
  preHTML,
@@ -45,11 +45,6 @@ const UserManagement = {
45
45
  ],
46
46
  defaultColKeyFocus: 'username',
47
47
  ServiceProvider: UserService,
48
- serviceOptions: {
49
- get: {
50
- id: 'all',
51
- },
52
- },
53
48
  });
54
49
  },
55
50
  };
@@ -38,7 +38,7 @@ const UserService = {
38
38
  }),
39
39
  ),
40
40
  get: (options = {}) => {
41
- const { id, page, limit, filterModel, sortModel, sort, asc, order } = options;
41
+ const { id = 'all', page, limit, filterModel, sortModel, sort, asc, order } = options;
42
42
  const url = buildQueryUrl(getApiBaseUrl({ id, endpoint }), {
43
43
  page,
44
44
  limit,
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ import UnderpostDB from './cli/db.js';
12
12
  import UnderpostDeploy from './cli/deploy.js';
13
13
  import UnderpostRootEnv from './cli/env.js';
14
14
  import UnderpostFileStorage from './cli/fs.js';
15
+ import UnderpostIPFS from './cli/ipfs.js';
15
16
  import UnderpostImage from './cli/image.js';
16
17
  import UnderpostLxd from './cli/lxd.js';
17
18
  import UnderpostMonitor from './cli/monitor.js';
@@ -41,7 +42,7 @@ class Underpost {
41
42
  * @type {String}
42
43
  * @memberof Underpost
43
44
  */
44
- static version = 'v2.99.8';
45
+ static version = 'v3.0.1';
45
46
 
46
47
  /**
47
48
  * Required Node.js major version
@@ -143,6 +144,15 @@ class Underpost {
143
144
  static get fs() {
144
145
  return UnderpostFileStorage.API;
145
146
  }
147
+ /**
148
+ * IPFS cli API
149
+ * @static
150
+ * @type {UnderpostIPFS.API}
151
+ * @memberof Underpost
152
+ */
153
+ static get ipfs() {
154
+ return UnderpostIPFS.API;
155
+ }
146
156
  /**
147
157
  * Monitor cli API
148
158
  * @static
@@ -296,6 +306,7 @@ export {
296
306
  UnderpostStatic,
297
307
  UnderpostLxd,
298
308
  UnderpostKickStart,
309
+ UnderpostIPFS,
299
310
  UnderpostMonitor,
300
311
  UnderpostRepository,
301
312
  UnderpostRun,
@@ -20,6 +20,7 @@ import { createPeerServer } from '../../server/peer.js';
20
20
  import { createValkeyConnection } from '../../server/valkey.js';
21
21
  import { applySecurity, authMiddlewareFactory } from '../../server/auth.js';
22
22
  import { ssrMiddlewareFactory } from '../../server/ssr.js';
23
+ import { buildSwaggerUiOptions } from '../../server/client-build-docs.js';
23
24
 
24
25
  import { shellExec } from '../../server/process.js';
25
26
  import { devProxyHostFactory, isDevProxyContext, isTlsDevProxy } from '../../server/conf.js';
@@ -167,8 +168,8 @@ class ExpressService {
167
168
  // Swagger UI setup
168
169
  if (fs.existsSync(swaggerJsonPath)) {
169
170
  const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
170
- // Reusing swaggerPath defined outside, removing unnecessary redeclaration
171
- app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc));
171
+ const swaggerUiOptions = await buildSwaggerUiOptions();
172
+ app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc, swaggerUiOptions));
172
173
  }
173
174
 
174
175
  // Security and CORS
@@ -7,10 +7,10 @@
7
7
  */
8
8
 
9
9
  import fs from 'fs-extra';
10
- import swaggerAutoGen from 'swagger-autogen';
11
10
  import { shellExec } from './process.js';
12
11
  import { loggerFactory } from './logger.js';
13
12
  import { JSONweb } from './client-formatted.js';
13
+ import { ssrFactory } from './ssr.js';
14
14
 
15
15
  /**
16
16
  * Builds API documentation using Swagger
@@ -63,53 +63,91 @@ const buildApiDocs = async ({
63
63
  components: {
64
64
  schemas: {
65
65
  userRequest: {
66
- username: 'user123',
67
- password: 'Password123',
68
- email: 'user@example.com',
66
+ type: 'object',
67
+ required: ['username', 'password', 'email'],
68
+ properties: {
69
+ username: { type: 'string', example: 'user123' },
70
+ password: { type: 'string', example: 'Password123!' },
71
+ email: { type: 'string', format: 'email', example: 'user@example.com' },
72
+ },
69
73
  },
70
74
  userResponse: {
71
- status: 'success',
72
- data: {
73
- token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Il9pZCI6IjY2YzM3N2Y1N2Y5OWU1OTY5YjgxZG...',
74
- user: {
75
- _id: '66c377f57f99e5969b81de89',
76
- email: 'user@example.com',
77
- emailConfirmed: false,
78
- username: 'user123',
79
- role: 'user',
80
- profileImageId: '66c377f57f99e5969b81de87',
75
+ type: 'object',
76
+ properties: {
77
+ status: { type: 'string', example: 'success' },
78
+ data: {
79
+ type: 'object',
80
+ properties: {
81
+ token: {
82
+ type: 'string',
83
+ example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Il9pZCI6IjY2YzM3N2Y1N2Y5OWU1OTY5YjgxZG...',
84
+ },
85
+ user: {
86
+ type: 'object',
87
+ properties: {
88
+ _id: { type: 'string', example: '66c377f57f99e5969b81de89' },
89
+ email: { type: 'string', format: 'email', example: 'user@example.com' },
90
+ emailConfirmed: { type: 'boolean', example: false },
91
+ username: { type: 'string', example: 'user123' },
92
+ role: { type: 'string', example: 'user' },
93
+ profileImageId: { type: 'string', example: '66c377f57f99e5969b81de87' },
94
+ },
95
+ },
96
+ },
81
97
  },
82
98
  },
83
99
  },
84
100
  userUpdateResponse: {
85
- status: 'success',
86
- data: {
87
- _id: '66c377f57f99e5969b81de89',
88
- email: 'user@example.com',
89
- emailConfirmed: false,
90
- username: 'user123222',
91
- role: 'user',
92
- profileImageId: '66c377f57f99e5969b81de87',
101
+ type: 'object',
102
+ properties: {
103
+ status: { type: 'string', example: 'success' },
104
+ data: {
105
+ type: 'object',
106
+ properties: {
107
+ _id: { type: 'string', example: '66c377f57f99e5969b81de89' },
108
+ email: { type: 'string', format: 'email', example: 'user@example.com' },
109
+ emailConfirmed: { type: 'boolean', example: false },
110
+ username: { type: 'string', example: 'user123222' },
111
+ role: { type: 'string', example: 'user' },
112
+ profileImageId: { type: 'string', example: '66c377f57f99e5969b81de87' },
113
+ },
114
+ },
93
115
  },
94
116
  },
95
117
  userGetResponse: {
96
- status: 'success',
97
- data: {
98
- _id: '66c377f57f99e5969b81de89',
99
- email: 'user@example.com',
100
- emailConfirmed: false,
101
- username: 'user123222',
102
- role: 'user',
103
- profileImageId: '66c377f57f99e5969b81de87',
118
+ type: 'object',
119
+ properties: {
120
+ status: { type: 'string', example: 'success' },
121
+ data: {
122
+ type: 'object',
123
+ properties: {
124
+ _id: { type: 'string', example: '66c377f57f99e5969b81de89' },
125
+ email: { type: 'string', format: 'email', example: 'user@example.com' },
126
+ emailConfirmed: { type: 'boolean', example: false },
127
+ username: { type: 'string', example: 'user123222' },
128
+ role: { type: 'string', example: 'user' },
129
+ profileImageId: { type: 'string', example: '66c377f57f99e5969b81de87' },
130
+ },
131
+ },
104
132
  },
105
133
  },
106
134
  userLogInRequest: {
107
- email: 'user@example.com',
108
- password: 'Password123',
135
+ type: 'object',
136
+ required: ['email', 'password'],
137
+ properties: {
138
+ email: { type: 'string', format: 'email', example: 'user@example.com' },
139
+ password: { type: 'string', example: 'Password123!' },
140
+ },
109
141
  },
110
142
  userBadRequestResponse: {
111
- status: 'error',
112
- message: 'Bad request. Please check your inputs, and try again',
143
+ type: 'object',
144
+ properties: {
145
+ status: { type: 'string', example: 'error' },
146
+ message: {
147
+ type: 'string',
148
+ example: 'Bad request. Please check your inputs, and try again',
149
+ },
150
+ },
113
151
  },
114
152
  },
115
153
  securitySchemes: {
@@ -121,15 +159,100 @@ const buildApiDocs = async ({
121
159
  },
122
160
  };
123
161
 
162
+ /**
163
+ * swagger-autogen has no requestBody annotation support — it only handles
164
+ * #swagger.parameters, responses, security, etc. We define the requestBody
165
+ * objects here and inject them into the generated JSON as a post-processing step.
166
+ *
167
+ * Each key is an "<method> <path>" pair matching the generated paths object.
168
+ * The value is a valid OAS 3.0 requestBody object.
169
+ */
170
+ const requestBodies = {
171
+ 'post /user': {
172
+ description: 'User registration data',
173
+ required: true,
174
+ content: {
175
+ 'application/json': {
176
+ schema: { $ref: '#/components/schemas/userRequest' },
177
+ },
178
+ },
179
+ },
180
+ 'post /user/auth': {
181
+ description: 'User login credentials',
182
+ required: true,
183
+ content: {
184
+ 'application/json': {
185
+ schema: { $ref: '#/components/schemas/userLogInRequest' },
186
+ },
187
+ },
188
+ },
189
+ 'put /user/{id}': {
190
+ description: 'User fields to update',
191
+ required: true,
192
+ content: {
193
+ 'application/json': {
194
+ schema: { $ref: '#/components/schemas/userRequest' },
195
+ },
196
+ },
197
+ },
198
+ };
199
+
124
200
  logger.warn('build swagger api docs', doc.info);
125
201
 
126
- const outputFile = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
127
- const routes = [];
128
- for (const api of apis) {
129
- if (['user'].includes(api)) routes.push(`./src/api/${api}/${api}.router.js`);
130
- }
202
+ // swagger-autogen@2.9.2 bug: getProducesTag, getConsumesTag, getResponsesTag missing __¬¬¬__ decode before eval
203
+ fs.writeFileSync(
204
+ `node_modules/swagger-autogen/src/swagger-tags.js`,
205
+ fs
206
+ .readFileSync(`node_modules/swagger-autogen/src/swagger-tags.js`, 'utf8')
207
+ // getProducesTag and getConsumesTag: already decode &quot; but not __¬¬¬__
208
+ .replaceAll(
209
+ `data.replaceAll('\\n', ' ').replaceAll('\u201c', '\u201d')`,
210
+ `data.replaceAll('\\n', ' ').replaceAll('\u201c', '\u201d').replaceAll('__\u00ac\u00ac\u00ac__', '"')`,
211
+ )
212
+ // getResponsesTag: decodes neither &quot; nor __¬¬¬__
213
+ .replaceAll(
214
+ `data.replaceAll('\\n', ' ');`,
215
+ `data.replaceAll('\\n', ' ').replaceAll('__\u00ac\u00ac\u00ac__', '"');`,
216
+ ),
217
+ 'utf8',
218
+ );
219
+ setTimeout(async () => {
220
+ const { default: swaggerAutoGen } = await import('swagger-autogen');
221
+ const outputFile = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
222
+ const routes = [];
223
+ for (const api of apis) {
224
+ if (['user'].includes(api)) routes.push(`./src/api/${api}/${api}.router.js`);
225
+ }
226
+
227
+ await swaggerAutoGen({ openapi: '3.0.0' })(outputFile, routes, doc);
228
+
229
+ // Post-process: inject requestBody into operations — swagger-autogen silently
230
+ // ignores #swagger.requestBody annotations and has no internal OAS-3 body support.
231
+ if (fs.existsSync(outputFile)) {
232
+ const swaggerJson = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
233
+ let patched = false;
234
+
235
+ for (const [key, requestBody] of Object.entries(requestBodies)) {
236
+ const [method, ...pathParts] = key.split(' ');
237
+ const opPath = pathParts.join(' ');
238
+ if (swaggerJson.paths?.[opPath]?.[method]) {
239
+ swaggerJson.paths[opPath][method].requestBody = requestBody;
240
+ // Remove any stale in:body entry from parameters (OAS 3.0 doesn't allow it)
241
+ if (Array.isArray(swaggerJson.paths[opPath][method].parameters)) {
242
+ swaggerJson.paths[opPath][method].parameters = swaggerJson.paths[opPath][method].parameters.filter(
243
+ (p) => p.in !== 'body',
244
+ );
245
+ }
246
+ patched = true;
247
+ }
248
+ }
131
249
 
132
- await swaggerAutoGen({ openapi: '3.0.0' })(outputFile, routes, doc);
250
+ if (patched) {
251
+ fs.writeFileSync(outputFile, JSON.stringify(swaggerJson, null, 2), 'utf8');
252
+ // logger.warn('swagger post-process: requestBody injected', Object.keys(requestBodies));
253
+ }
254
+ }
255
+ });
133
256
  };
134
257
 
135
258
  /**
@@ -228,4 +351,18 @@ const buildDocs = async ({
228
351
  });
229
352
  };
230
353
 
231
- export { buildDocs };
354
+ /**
355
+ * Builds Swagger UI customization options by rendering the SwaggerDarkMode SSR body component.
356
+ * Returns the customCss and customJsStr strings required by swagger-ui-express to enable
357
+ * a dark/light mode toggle button with a black/gray gradient dark theme.
358
+ * @function buildSwaggerUiOptions
359
+ * @memberof clientBuildDocs
360
+ * @returns {Promise<{customCss: string, customJsStr: string}>} Swagger UI setup options
361
+ */
362
+ const buildSwaggerUiOptions = async () => {
363
+ const swaggerDarkMode = await ssrFactory('./src/client/ssr/body/SwaggerDarkMode.js');
364
+ const { css, js } = swaggerDarkMode();
365
+ return { customCss: css, customJsStr: js };
366
+ };
367
+
368
+ export { buildDocs, buildSwaggerUiOptions };
@@ -1335,7 +1335,7 @@ const buildCliDoc = (program, oldVersion, newVersion) => {
1335
1335
  });
1336
1336
  md = md.replaceAll(oldVersion, newVersion);
1337
1337
  fs.writeFileSync(`./src/client/public/nexodev/docs/references/Command Line Interface.md`, md, 'utf8');
1338
- fs.writeFileSync(`./cli.md`, md, 'utf8');
1338
+ fs.writeFileSync(`./CLI-HELP.md`, md, 'utf8');
1339
1339
  const readme = fs.readFileSync(`./README.md`, 'utf8');
1340
1340
  fs.writeFileSync('./README.md', readme.replaceAll(oldVersion, newVersion), 'utf8');
1341
1341
  };
@@ -101,27 +101,37 @@ const setUpInfo = async (logger = new winston.Logger()) => {
101
101
  * @param meta - The `meta` parameter in the `loggerFactory` function is used to extract the last part
102
102
  * of a URL and use it to create log files in a specific directory.
103
103
  * @param logLevel - Specify the logging level for the logger instance. e.g., 'error', 'warn', 'info', 'debug'.
104
+ * @param enableFileLogs - Whether to write logs to files. Defaults to the value of the `ENABLE_FILE_LOGS` environment variable.
104
105
  * @returns {underpostLogger} The `loggerFactory` function returns a logger instance created using Winston logger
105
106
  * library. The logger instance is configured with various transports for printing out messages to
106
107
  * different destinations such as the terminal, error.log file, and all.log file. The logger instance
107
108
  * also has a method `setUpInfo` attached to it for setting up additional information.
108
109
  * @memberof Logger
109
110
  */
110
- const loggerFactory = (meta = { url: '' }, logLevel = '') => {
111
+ const loggerFactory = (
112
+ meta = { url: '' },
113
+ logLevel = '',
114
+ enableFileLogs = process.env.ENABLE_FILE_LOGS === 'true' || process.env.ENABLE_FILE_LOGS === true,
115
+ ) => {
111
116
  meta = meta.url.split('/').pop();
112
117
  // Define which transports the logger must use to print out messages.
113
118
  // In this example, we are using three different transports
114
119
  const transports = [
115
120
  // Allow the use the terminal to print the messages
116
121
  new winston.transports.Console(),
117
- // Allow to print all the error level messages inside the error.log file
118
- // new winston.transports.File({
119
- // filename: `logs/${meta}/error.log`,
120
- // level: 'error',
121
- // }),
122
- // Allow to print all the error message inside the all.log file
123
- // (also the error log that are also printed inside the error.log(
124
- new winston.transports.File({ filename: `logs/${meta}/all.log` }),
122
+ // Optionally write log files when enableFileLogs is true
123
+ ...(enableFileLogs
124
+ ? [
125
+ // Allow to print all the error level messages inside the error.log file
126
+ new winston.transports.File({
127
+ filename: `logs/${meta}/error.log`,
128
+ level: 'error',
129
+ }),
130
+ // Allow to print all the error messages inside the all.log file
131
+ // (also includes error logs that are also printed inside error.log)
132
+ new winston.transports.File({ filename: `logs/${meta}/all.log` }),
133
+ ]
134
+ : []),
125
135
  ];
126
136
 
127
137
  // Create the logger instance that has to be exported
@@ -154,6 +164,7 @@ const loggerFactory = (meta = { url: '' }, logLevel = '') => {
154
164
  * @param {Object} meta - An object containing metadata, such as the URL, to be used in the logger.
155
165
  * @param {string} logLevel - The logging level to be used for the logger (e.g., 'error', 'warn', 'info', 'debug').
156
166
  * @param {Function} skip - A function to determine whether to skip logging for a particular request.
167
+ * @param {boolean} enableFileLogs - Whether to write logs to files. Defaults to false.
157
168
  * @returns {Function} A middleware function that can be used in an Express application to log HTTP requests.
158
169
  * @memberof Logger
159
170
  */
@@ -161,10 +172,11 @@ const loggerMiddleware = (
161
172
  meta = { url: '' },
162
173
  logLevel = 'info',
163
174
  skip = (req, res) => process.env.NODE_ENV === 'production',
175
+ enableFileLogs = process.env.ENABLE_FILE_LOGS === 'true' || process.env.ENABLE_FILE_LOGS === true,
164
176
  ) => {
165
177
  const stream = {
166
178
  // Use the http severity
167
- write: (message) => loggerFactory(meta, logLevel).http(message),
179
+ write: (message) => loggerFactory(meta, logLevel, enableFileLogs).http(message),
168
180
  };
169
181
 
170
182
  morgan.token('host', function (req, res) {