@nocobase/cli 2.1.0-alpha.45 → 2.1.0-alpha.47

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 (87) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +63 -380
  3. package/assets/env-proxy/nginx/app.conf.tpl +23 -0
  4. package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
  5. package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
  6. package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
  7. package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
  8. package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
  9. package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
  10. package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
  11. package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
  12. package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
  13. package/bin/run.js +40 -32
  14. package/dist/commands/app/autostart/disable.js +55 -0
  15. package/dist/commands/app/autostart/enable.js +55 -0
  16. package/dist/commands/app/autostart/list.js +37 -0
  17. package/dist/commands/app/autostart/run.js +84 -0
  18. package/dist/commands/app/autostart/shared.js +49 -0
  19. package/dist/commands/app/destroy.js +21 -12
  20. package/dist/commands/app/down.js +2 -2
  21. package/dist/commands/app/logs.js +2 -1
  22. package/dist/commands/app/restart.js +92 -20
  23. package/dist/commands/app/shared.js +3 -2
  24. package/dist/commands/app/start.js +143 -38
  25. package/dist/commands/app/stop.js +31 -2
  26. package/dist/commands/app/upgrade.js +11 -7
  27. package/dist/commands/config/delete.js +10 -2
  28. package/dist/commands/config/get.js +7 -1
  29. package/dist/commands/config/set.js +12 -2
  30. package/dist/commands/db/ps.js +5 -18
  31. package/dist/commands/env/add.js +24 -44
  32. package/dist/commands/env/auth.js +1 -1
  33. package/dist/commands/env/info.js +3 -2
  34. package/dist/commands/env/list.js +4 -5
  35. package/dist/commands/env/proxy/caddy.js +28 -0
  36. package/dist/commands/env/proxy/index.js +353 -0
  37. package/dist/commands/env/proxy/nginx.js +28 -0
  38. package/dist/commands/env/remove.js +112 -22
  39. package/dist/commands/env/shared.js +17 -9
  40. package/dist/commands/env/status.js +10 -7
  41. package/dist/commands/env/update.js +385 -21
  42. package/dist/commands/init.js +368 -98
  43. package/dist/commands/install.js +271 -104
  44. package/dist/commands/license/activate.js +63 -244
  45. package/dist/commands/license/plugins/shared.js +64 -13
  46. package/dist/commands/plugin/import.js +108 -0
  47. package/dist/commands/revision/create.js +89 -0
  48. package/dist/commands/self/update.js +58 -1
  49. package/dist/commands/source/dev.js +5 -5
  50. package/dist/commands/source/test.js +13 -14
  51. package/dist/lib/app-health.js +39 -32
  52. package/dist/lib/app-managed-resources.js +25 -9
  53. package/dist/lib/app-public-path.js +80 -0
  54. package/dist/lib/app-runtime.js +12 -3
  55. package/dist/lib/auth-store.js +128 -4
  56. package/dist/lib/backup.js +3 -3
  57. package/dist/lib/bootstrap.js +14 -8
  58. package/dist/lib/cli-config.js +229 -3
  59. package/dist/lib/cli-entry-error.js +52 -0
  60. package/dist/lib/command-log.js +284 -0
  61. package/dist/lib/docker-env-file.js +10 -2
  62. package/dist/lib/docker-log-stream.js +45 -0
  63. package/dist/lib/env-auth.js +25 -14
  64. package/dist/lib/env-command-config.js +45 -0
  65. package/dist/lib/env-config.js +7 -2
  66. package/dist/lib/env-guard.js +9 -10
  67. package/dist/lib/env-paths.js +101 -0
  68. package/dist/lib/env-proxy.js +1295 -0
  69. package/dist/lib/inquirer.js +4 -5
  70. package/dist/lib/managed-env-file.js +98 -0
  71. package/dist/lib/plugin-import.js +279 -0
  72. package/dist/lib/prompt-catalog-core.js +3 -2
  73. package/dist/lib/prompt-validators.js +14 -0
  74. package/dist/lib/prompt-web-ui.js +8 -1
  75. package/dist/lib/run-npm.js +56 -12
  76. package/dist/lib/skills-manager.js +3 -1
  77. package/dist/lib/source-publish.js +22 -2
  78. package/dist/lib/startup-update.js +74 -50
  79. package/dist/lib/ui.js +2 -1
  80. package/dist/locale/en-US.json +122 -22
  81. package/dist/locale/zh-CN.json +119 -19
  82. package/package.json +5 -7
  83. package/scripts/build.mjs +34 -0
  84. package/scripts/clean.mjs +9 -0
  85. package/tsconfig.json +19 -0
  86. package/LICENSE.txt +0 -107
  87. package/README.zh-CN.md +0 -355
@@ -7,22 +7,40 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { loadExactAuthConfig, saveAuthConfig } from './auth-store.js';
10
- import { resolveDefaultConfigScope } from './cli-home.js';
10
+ import { resolveCliHomeRoot, resolveDefaultConfigScope } from './cli-home.js';
11
11
  import { CLI_LOCALE_FLAG_OPTIONS, normalizeCliLocale, resolveCliLocale } from './cli-locale.js';
12
12
  export const DEFAULT_LICENSE_PKG_URL = 'https://pkg.nocobase.com/';
13
13
  export const DEFAULT_DOCKER_NETWORK = 'nocobase';
14
14
  export const DEFAULT_DOCKER_CONTAINER_PREFIX = 'nb';
15
15
  export const DEFAULT_DOCKER_BIN = 'docker';
16
+ export const DEFAULT_CADDY_BIN = 'caddy';
16
17
  export const DEFAULT_GIT_BIN = 'git';
18
+ export const DEFAULT_NGINX_BIN = 'nginx';
19
+ export const PROXY_PROVIDER_OPTIONS = ['nginx', 'caddy'];
20
+ export const DEFAULT_PROXY_PROVIDER = 'nginx';
21
+ export const DEFAULT_PROXY_HOST = '127.0.0.1';
17
22
  export const DEFAULT_YARN_BIN = 'yarn';
23
+ export const DEFAULT_LOG_RETENTION_DAYS = 14;
24
+ export const DEFAULT_LOG_ENABLED = true;
25
+ export const CLI_UPDATE_POLICY_OPTIONS = ['prompt', 'auto', 'off'];
26
+ export const DEFAULT_UPDATE_POLICY = 'prompt';
18
27
  export const SUPPORTED_CLI_CONFIG_KEYS = [
19
28
  'locale',
29
+ 'default-ui-host',
30
+ 'default-api-host',
31
+ 'update.policy',
20
32
  'license.pkg-url',
21
33
  'docker.network',
22
34
  'docker.container-prefix',
23
35
  'bin.docker',
36
+ 'bin.caddy',
24
37
  'bin.git',
38
+ 'bin.nginx',
39
+ 'proxy.nb-cli-root',
40
+ 'proxy.upstream-host',
25
41
  'bin.yarn',
42
+ 'log.enabled',
43
+ 'log.retention-days',
26
44
  ];
27
45
  function trimValue(value) {
28
46
  const text = String(value ?? '').trim();
@@ -42,18 +60,44 @@ export function assertSupportedCliConfigKey(value) {
42
60
  }
43
61
  return value;
44
62
  }
63
+ export function normalizeCliUpdatePolicy(value) {
64
+ const normalized = trimValue(value);
65
+ if (!normalized) {
66
+ return undefined;
67
+ }
68
+ return CLI_UPDATE_POLICY_OPTIONS.includes(normalized) ? normalized : undefined;
69
+ }
70
+ export function normalizeProxyProvider(value) {
71
+ const normalized = trimValue(value);
72
+ if (!normalized) {
73
+ return undefined;
74
+ }
75
+ return PROXY_PROVIDER_OPTIONS.includes(normalized) ? normalized : undefined;
76
+ }
45
77
  function cloneSettings(config) {
46
78
  return {
47
79
  ...(config.settings?.locale ? { locale: trimValue(config.settings.locale) } : {}),
80
+ init: config.settings?.init ? { ...config.settings.init } : undefined,
81
+ update: config.settings?.update ? { ...config.settings.update } : undefined,
48
82
  license: config.settings?.license ? { ...config.settings.license } : undefined,
49
83
  docker: config.settings?.docker ? { ...config.settings.docker } : undefined,
50
84
  bin: config.settings?.bin ? { ...config.settings.bin } : undefined,
85
+ proxy: config.settings?.proxy ? { ...config.settings.proxy } : undefined,
86
+ log: config.settings?.log ? { ...config.settings.log } : undefined,
51
87
  };
52
88
  }
53
89
  function pruneSettings(config) {
54
90
  if (config.settings && !trimValue(config.settings.locale)) {
55
91
  delete config.settings.locale;
56
92
  }
93
+ const init = config.settings?.init;
94
+ if (init && !trimValue(init.defaultUiHost) && !trimValue(init.defaultApiHost)) {
95
+ delete config.settings?.init;
96
+ }
97
+ const update = config.settings?.update;
98
+ if (update && !normalizeCliUpdatePolicy(update.policy)) {
99
+ delete config.settings?.update;
100
+ }
57
101
  const license = config.settings?.license;
58
102
  if (license && !trimValue(license.pkgUrl)) {
59
103
  delete config.settings?.license;
@@ -63,14 +107,31 @@ function pruneSettings(config) {
63
107
  delete config.settings?.docker;
64
108
  }
65
109
  const bin = config.settings?.bin;
66
- if (bin && !trimValue(bin.docker) && !trimValue(bin.git) && !trimValue(bin.yarn)) {
110
+ if (bin &&
111
+ !trimValue(bin.docker) &&
112
+ !trimValue(bin.caddy) &&
113
+ !trimValue(bin.git) &&
114
+ !trimValue(bin.nginx) &&
115
+ !trimValue(bin.yarn)) {
67
116
  delete config.settings?.bin;
68
117
  }
118
+ const proxy = config.settings?.proxy;
119
+ if (proxy && !trimValue(proxy.nbCliRoot) && !trimValue(proxy.upstreamHost)) {
120
+ delete config.settings?.proxy;
121
+ }
122
+ const log = config.settings?.log;
123
+ if (log && typeof log.enabled !== 'boolean' && (!Number.isInteger(log.retentionDays) || log.retentionDays < 0)) {
124
+ delete config.settings?.log;
125
+ }
69
126
  if (config.settings &&
70
127
  !config.settings.locale &&
128
+ !config.settings.init &&
129
+ !config.settings.update &&
71
130
  !config.settings.license &&
72
131
  !config.settings.docker &&
73
- !config.settings.bin) {
132
+ !config.settings.bin &&
133
+ !config.settings.proxy &&
134
+ !config.settings.log) {
74
135
  delete config.settings;
75
136
  }
76
137
  }
@@ -78,6 +139,12 @@ export function getExplicitCliConfigValue(config, key) {
78
139
  switch (key) {
79
140
  case 'locale':
80
141
  return trimValue(config.settings?.locale);
142
+ case 'default-ui-host':
143
+ return trimValue(config.settings?.init?.defaultUiHost);
144
+ case 'default-api-host':
145
+ return trimValue(config.settings?.init?.defaultApiHost);
146
+ case 'update.policy':
147
+ return normalizeCliUpdatePolicy(config.settings?.update?.policy);
81
148
  case 'license.pkg-url':
82
149
  return trimValue(config.settings?.license?.pkgUrl);
83
150
  case 'docker.network':
@@ -86,10 +153,24 @@ export function getExplicitCliConfigValue(config, key) {
86
153
  return trimValue(config.settings?.docker?.containerPrefix);
87
154
  case 'bin.docker':
88
155
  return trimValue(config.settings?.bin?.docker);
156
+ case 'bin.caddy':
157
+ return trimValue(config.settings?.bin?.caddy);
89
158
  case 'bin.git':
90
159
  return trimValue(config.settings?.bin?.git);
160
+ case 'bin.nginx':
161
+ return trimValue(config.settings?.bin?.nginx);
162
+ case 'proxy.nb-cli-root':
163
+ return trimValue(config.settings?.proxy?.nbCliRoot);
164
+ case 'proxy.upstream-host':
165
+ return trimValue(config.settings?.proxy?.upstreamHost);
91
166
  case 'bin.yarn':
92
167
  return trimValue(config.settings?.bin?.yarn);
168
+ case 'log.enabled':
169
+ return typeof config.settings?.log?.enabled === 'boolean' ? String(config.settings?.log?.enabled) : undefined;
170
+ case 'log.retention-days':
171
+ return Number.isInteger(config.settings?.log?.retentionDays)
172
+ ? String(config.settings?.log?.retentionDays)
173
+ : undefined;
93
174
  }
94
175
  }
95
176
  export function getEffectiveCliConfigValue(config, key) {
@@ -100,6 +181,12 @@ export function getEffectiveCliConfigValue(config, key) {
100
181
  switch (key) {
101
182
  case 'locale':
102
183
  return resolveCliLocale(undefined, { configuredLocale: trimValue(config.settings?.locale) });
184
+ case 'default-ui-host':
185
+ return '127.0.0.1';
186
+ case 'default-api-host':
187
+ return '127.0.0.1';
188
+ case 'update.policy':
189
+ return explicit ?? DEFAULT_UPDATE_POLICY;
103
190
  case 'license.pkg-url':
104
191
  return DEFAULT_LICENSE_PKG_URL;
105
192
  case 'docker.network':
@@ -108,10 +195,22 @@ export function getEffectiveCliConfigValue(config, key) {
108
195
  return trimValue(config.name) || DEFAULT_DOCKER_CONTAINER_PREFIX;
109
196
  case 'bin.docker':
110
197
  return DEFAULT_DOCKER_BIN;
198
+ case 'bin.caddy':
199
+ return DEFAULT_CADDY_BIN;
111
200
  case 'bin.git':
112
201
  return DEFAULT_GIT_BIN;
202
+ case 'bin.nginx':
203
+ return DEFAULT_NGINX_BIN;
204
+ case 'proxy.nb-cli-root':
205
+ return explicit ?? resolveCliHomeRoot();
206
+ case 'proxy.upstream-host':
207
+ return explicit ?? DEFAULT_PROXY_HOST;
113
208
  case 'bin.yarn':
114
209
  return DEFAULT_YARN_BIN;
210
+ case 'log.enabled':
211
+ return explicit ?? String(DEFAULT_LOG_ENABLED);
212
+ case 'log.retention-days':
213
+ return explicit ?? String(DEFAULT_LOG_RETENTION_DAYS);
115
214
  }
116
215
  }
117
216
  export function normalizeCliConfigValue(key, value) {
@@ -129,6 +228,26 @@ export function normalizeCliConfigValue(key, value) {
129
228
  }
130
229
  return locale;
131
230
  }
231
+ if (key === 'update.policy') {
232
+ const policy = normalizeCliUpdatePolicy(normalized);
233
+ if (!policy) {
234
+ throw new Error(`Config key "${key}" must be one of: ${CLI_UPDATE_POLICY_OPTIONS.join(', ')}`);
235
+ }
236
+ return policy;
237
+ }
238
+ if (key === 'log.retention-days') {
239
+ const retentionDays = Number.parseInt(normalized, 10);
240
+ if (!Number.isInteger(retentionDays) || retentionDays < 0) {
241
+ throw new Error(`Config key "${key}" must be a non-negative integer.`);
242
+ }
243
+ return String(retentionDays);
244
+ }
245
+ if (key === 'log.enabled') {
246
+ if (normalized !== 'true' && normalized !== 'false') {
247
+ throw new Error(`Config key "${key}" must be either "true" or "false".`);
248
+ }
249
+ return normalized;
250
+ }
132
251
  return normalized;
133
252
  }
134
253
  export async function loadCliConfig(options = {}) {
@@ -158,6 +277,24 @@ export async function setCliConfigValue(key, value, options = {}) {
158
277
  case 'locale':
159
278
  config.settings.locale = normalized;
160
279
  break;
280
+ case 'default-ui-host':
281
+ config.settings.init = {
282
+ ...(config.settings.init ?? {}),
283
+ defaultUiHost: normalized,
284
+ };
285
+ break;
286
+ case 'default-api-host':
287
+ config.settings.init = {
288
+ ...(config.settings.init ?? {}),
289
+ defaultApiHost: normalized,
290
+ };
291
+ break;
292
+ case 'update.policy':
293
+ config.settings.update = {
294
+ ...(config.settings.update ?? {}),
295
+ policy: normalized,
296
+ };
297
+ break;
161
298
  case 'license.pkg-url':
162
299
  config.settings.license = {
163
300
  ...(config.settings.license ?? {}),
@@ -182,18 +319,54 @@ export async function setCliConfigValue(key, value, options = {}) {
182
319
  docker: normalized,
183
320
  };
184
321
  break;
322
+ case 'bin.caddy':
323
+ config.settings.bin = {
324
+ ...(config.settings.bin ?? {}),
325
+ caddy: normalized,
326
+ };
327
+ break;
185
328
  case 'bin.git':
186
329
  config.settings.bin = {
187
330
  ...(config.settings.bin ?? {}),
188
331
  git: normalized,
189
332
  };
190
333
  break;
334
+ case 'bin.nginx':
335
+ config.settings.bin = {
336
+ ...(config.settings.bin ?? {}),
337
+ nginx: normalized,
338
+ };
339
+ break;
340
+ case 'proxy.nb-cli-root':
341
+ config.settings.proxy = {
342
+ ...(config.settings.proxy ?? {}),
343
+ nbCliRoot: normalized,
344
+ };
345
+ break;
346
+ case 'proxy.upstream-host':
347
+ config.settings.proxy = {
348
+ ...(config.settings.proxy ?? {}),
349
+ upstreamHost: normalized,
350
+ };
351
+ break;
191
352
  case 'bin.yarn':
192
353
  config.settings.bin = {
193
354
  ...(config.settings.bin ?? {}),
194
355
  yarn: normalized,
195
356
  };
196
357
  break;
358
+ case 'log.enabled':
359
+ config.settings.log = {
360
+ ...(config.settings.log ?? {}),
361
+ enabled: normalized === 'true',
362
+ };
363
+ break;
364
+ case 'log.retention-days':
365
+ config.settings.log = {
366
+ ...(config.settings.log ?? {}),
367
+ retentionDays: Number.parseInt(normalized, 10),
368
+ };
369
+ break;
197
370
  }
198
371
  pruneSettings(config);
199
372
  await saveAuthConfig(config, scope);
@@ -211,6 +384,21 @@ export async function deleteCliConfigValue(key, options = {}) {
211
384
  case 'locale':
212
385
  delete config.settings.locale;
213
386
  break;
387
+ case 'default-ui-host':
388
+ if (config.settings.init) {
389
+ delete config.settings.init.defaultUiHost;
390
+ }
391
+ break;
392
+ case 'default-api-host':
393
+ if (config.settings.init) {
394
+ delete config.settings.init.defaultApiHost;
395
+ }
396
+ break;
397
+ case 'update.policy':
398
+ if (config.settings.update) {
399
+ delete config.settings.update.policy;
400
+ }
401
+ break;
214
402
  case 'license.pkg-url':
215
403
  if (config.settings.license) {
216
404
  delete config.settings.license.pkgUrl;
@@ -231,16 +419,46 @@ export async function deleteCliConfigValue(key, options = {}) {
231
419
  delete config.settings.bin.docker;
232
420
  }
233
421
  break;
422
+ case 'bin.caddy':
423
+ if (config.settings.bin) {
424
+ delete config.settings.bin.caddy;
425
+ }
426
+ break;
234
427
  case 'bin.git':
235
428
  if (config.settings.bin) {
236
429
  delete config.settings.bin.git;
237
430
  }
238
431
  break;
432
+ case 'bin.nginx':
433
+ if (config.settings.bin) {
434
+ delete config.settings.bin.nginx;
435
+ }
436
+ break;
437
+ case 'proxy.nb-cli-root':
438
+ if (config.settings.proxy) {
439
+ delete config.settings.proxy.nbCliRoot;
440
+ }
441
+ break;
442
+ case 'proxy.upstream-host':
443
+ if (config.settings.proxy) {
444
+ delete config.settings.proxy.upstreamHost;
445
+ }
446
+ break;
239
447
  case 'bin.yarn':
240
448
  if (config.settings.bin) {
241
449
  delete config.settings.bin.yarn;
242
450
  }
243
451
  break;
452
+ case 'log.enabled':
453
+ if (config.settings.log) {
454
+ delete config.settings.log.enabled;
455
+ }
456
+ break;
457
+ case 'log.retention-days':
458
+ if (config.settings.log) {
459
+ delete config.settings.log.retentionDays;
460
+ }
461
+ break;
244
462
  }
245
463
  pruneSettings(config);
246
464
  await saveAuthConfig(config, scope);
@@ -249,6 +467,12 @@ export async function deleteCliConfigValue(key, options = {}) {
249
467
  export async function resolveDockerNetworkName(options = {}) {
250
468
  return await getCliConfigValue('docker.network', options);
251
469
  }
470
+ export async function resolveDefaultUiHost(options = {}) {
471
+ return await getCliConfigValue('default-ui-host', options);
472
+ }
473
+ export async function resolveDefaultApiHost(options = {}) {
474
+ return await getCliConfigValue('default-api-host', options);
475
+ }
252
476
  export async function resolveDockerContainerPrefix(options = {}) {
253
477
  return await getCliConfigValue('docker.container-prefix', options);
254
478
  }
@@ -257,7 +481,9 @@ export async function resolveLicensePkgUrlFromConfig(options = {}) {
257
481
  }
258
482
  const CONFIGURABLE_COMMAND_KEYS = {
259
483
  docker: 'bin.docker',
484
+ caddy: 'bin.caddy',
260
485
  git: 'bin.git',
486
+ nginx: 'bin.nginx',
261
487
  yarn: 'bin.yarn',
262
488
  };
263
489
  export function isConfigurableCommandName(value) {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ export function getCommandPathTokens(argv) {
10
+ const tokens = [];
11
+ for (const token of argv) {
12
+ if (!token) {
13
+ continue;
14
+ }
15
+ if (token.startsWith('-')) {
16
+ break;
17
+ }
18
+ tokens.push(token);
19
+ }
20
+ return tokens;
21
+ }
22
+ export function formatCliEntryError(error, argv) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ const missingCommandMatch = message.match(/^Command (.+) not found\.$/);
25
+ if (!missingCommandMatch) {
26
+ return message;
27
+ }
28
+ const commandPathTokens = getCommandPathTokens(argv);
29
+ const attemptedCommand = commandPathTokens.join(' ') || missingCommandMatch[1];
30
+ const isApiCommand = commandPathTokens[0] === 'api';
31
+ if (isApiCommand) {
32
+ const helpCommandTokens = commandPathTokens.length > 2 ? commandPathTokens.slice(0, -1) : ['api'];
33
+ const helpCommand = `nb ${helpCommandTokens.join(' ')} --help`;
34
+ return [
35
+ `Unknown command: \`${attemptedCommand}\`.`,
36
+ `If this is a built-in command or a typo, run \`${helpCommand}\` to inspect the commands available under that API group.`,
37
+ ].join('\n');
38
+ }
39
+ return [
40
+ `Unknown command: \`${attemptedCommand}\`.`,
41
+ 'If this is a built-in command or a typo, run `nb --help` to inspect available commands.',
42
+ `If \`${attemptedCommand}\` should be a runtime command from your NocoBase app, check whether the connected app exposes it, then retry the command.`,
43
+ ].join('\n');
44
+ }
45
+ export function appendDiagnosticLogPath(message, logFile) {
46
+ const normalizedMessage = String(message ?? '').trim();
47
+ const normalizedLogFile = String(logFile ?? '').trim();
48
+ if (!normalizedLogFile) {
49
+ return normalizedMessage;
50
+ }
51
+ return [normalizedMessage, `Diagnostic log: ${normalizedLogFile}`].filter(Boolean).join('\n\n');
52
+ }