@nocobase/cli 2.1.0-alpha.46 → 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 (77) 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 -3
  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 +89 -24
  23. package/dist/commands/app/shared.js +3 -2
  24. package/dist/commands/app/start.js +142 -38
  25. package/dist/commands/app/stop.js +31 -2
  26. package/dist/commands/app/upgrade.js +9 -5
  27. package/dist/commands/config/delete.js +6 -1
  28. package/dist/commands/config/get.js +6 -1
  29. package/dist/commands/config/set.js +7 -2
  30. package/dist/commands/env/add.js +24 -44
  31. package/dist/commands/env/info.js +3 -2
  32. package/dist/commands/env/proxy/caddy.js +28 -0
  33. package/dist/commands/env/proxy/index.js +353 -0
  34. package/dist/commands/env/proxy/nginx.js +28 -0
  35. package/dist/commands/env/remove.js +112 -22
  36. package/dist/commands/env/shared.js +17 -9
  37. package/dist/commands/env/update.js +385 -21
  38. package/dist/commands/init.js +350 -98
  39. package/dist/commands/install.js +218 -75
  40. package/dist/commands/license/activate.js +63 -244
  41. package/dist/commands/license/plugins/shared.js +64 -13
  42. package/dist/commands/plugin/import.js +108 -0
  43. package/dist/commands/revision/create.js +89 -0
  44. package/dist/commands/source/test.js +13 -14
  45. package/dist/lib/app-health.js +5 -5
  46. package/dist/lib/app-managed-resources.js +24 -8
  47. package/dist/lib/app-public-path.js +80 -0
  48. package/dist/lib/app-runtime.js +10 -1
  49. package/dist/lib/auth-store.js +119 -4
  50. package/dist/lib/cli-config.js +191 -3
  51. package/dist/lib/cli-entry-error.js +8 -0
  52. package/dist/lib/command-log.js +284 -0
  53. package/dist/lib/docker-env-file.js +10 -2
  54. package/dist/lib/env-auth.js +4 -1
  55. package/dist/lib/env-command-config.js +45 -0
  56. package/dist/lib/env-config.js +7 -2
  57. package/dist/lib/env-guard.js +9 -10
  58. package/dist/lib/env-paths.js +101 -0
  59. package/dist/lib/env-proxy.js +1295 -0
  60. package/dist/lib/inquirer.js +4 -5
  61. package/dist/lib/managed-env-file.js +98 -0
  62. package/dist/lib/plugin-import.js +279 -0
  63. package/dist/lib/prompt-catalog-core.js +3 -2
  64. package/dist/lib/prompt-validators.js +14 -0
  65. package/dist/lib/prompt-web-ui.js +8 -1
  66. package/dist/lib/run-npm.js +56 -12
  67. package/dist/lib/skills-manager.js +3 -1
  68. package/dist/lib/source-publish.js +18 -1
  69. package/dist/lib/ui.js +2 -1
  70. package/dist/locale/en-US.json +116 -22
  71. package/dist/locale/zh-CN.json +113 -19
  72. package/package.json +5 -7
  73. package/scripts/build.mjs +34 -0
  74. package/scripts/clean.mjs +9 -0
  75. package/tsconfig.json +19 -0
  76. package/LICENSE.txt +0 -107
  77. package/README.zh-CN.md +0 -355
@@ -7,25 +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;
18
25
  export const CLI_UPDATE_POLICY_OPTIONS = ['prompt', 'auto', 'off'];
19
26
  export const DEFAULT_UPDATE_POLICY = 'prompt';
20
27
  export const SUPPORTED_CLI_CONFIG_KEYS = [
21
28
  'locale',
29
+ 'default-ui-host',
30
+ 'default-api-host',
22
31
  'update.policy',
23
32
  'license.pkg-url',
24
33
  'docker.network',
25
34
  'docker.container-prefix',
26
35
  'bin.docker',
36
+ 'bin.caddy',
27
37
  'bin.git',
38
+ 'bin.nginx',
39
+ 'proxy.nb-cli-root',
40
+ 'proxy.upstream-host',
28
41
  'bin.yarn',
42
+ 'log.enabled',
43
+ 'log.retention-days',
29
44
  ];
30
45
  function trimValue(value) {
31
46
  const text = String(value ?? '').trim();
@@ -52,19 +67,33 @@ export function normalizeCliUpdatePolicy(value) {
52
67
  }
53
68
  return CLI_UPDATE_POLICY_OPTIONS.includes(normalized) ? normalized : undefined;
54
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
+ }
55
77
  function cloneSettings(config) {
56
78
  return {
57
79
  ...(config.settings?.locale ? { locale: trimValue(config.settings.locale) } : {}),
80
+ init: config.settings?.init ? { ...config.settings.init } : undefined,
58
81
  update: config.settings?.update ? { ...config.settings.update } : undefined,
59
82
  license: config.settings?.license ? { ...config.settings.license } : undefined,
60
83
  docker: config.settings?.docker ? { ...config.settings.docker } : undefined,
61
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,
62
87
  };
63
88
  }
64
89
  function pruneSettings(config) {
65
90
  if (config.settings && !trimValue(config.settings.locale)) {
66
91
  delete config.settings.locale;
67
92
  }
93
+ const init = config.settings?.init;
94
+ if (init && !trimValue(init.defaultUiHost) && !trimValue(init.defaultApiHost)) {
95
+ delete config.settings?.init;
96
+ }
68
97
  const update = config.settings?.update;
69
98
  if (update && !normalizeCliUpdatePolicy(update.policy)) {
70
99
  delete config.settings?.update;
@@ -78,15 +107,31 @@ function pruneSettings(config) {
78
107
  delete config.settings?.docker;
79
108
  }
80
109
  const bin = config.settings?.bin;
81
- 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)) {
82
116
  delete config.settings?.bin;
83
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
+ }
84
126
  if (config.settings &&
85
127
  !config.settings.locale &&
128
+ !config.settings.init &&
86
129
  !config.settings.update &&
87
130
  !config.settings.license &&
88
131
  !config.settings.docker &&
89
- !config.settings.bin) {
132
+ !config.settings.bin &&
133
+ !config.settings.proxy &&
134
+ !config.settings.log) {
90
135
  delete config.settings;
91
136
  }
92
137
  }
@@ -94,6 +139,10 @@ export function getExplicitCliConfigValue(config, key) {
94
139
  switch (key) {
95
140
  case 'locale':
96
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);
97
146
  case 'update.policy':
98
147
  return normalizeCliUpdatePolicy(config.settings?.update?.policy);
99
148
  case 'license.pkg-url':
@@ -104,10 +153,24 @@ export function getExplicitCliConfigValue(config, key) {
104
153
  return trimValue(config.settings?.docker?.containerPrefix);
105
154
  case 'bin.docker':
106
155
  return trimValue(config.settings?.bin?.docker);
156
+ case 'bin.caddy':
157
+ return trimValue(config.settings?.bin?.caddy);
107
158
  case 'bin.git':
108
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);
109
166
  case 'bin.yarn':
110
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;
111
174
  }
112
175
  }
113
176
  export function getEffectiveCliConfigValue(config, key) {
@@ -118,6 +181,10 @@ export function getEffectiveCliConfigValue(config, key) {
118
181
  switch (key) {
119
182
  case 'locale':
120
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';
121
188
  case 'update.policy':
122
189
  return explicit ?? DEFAULT_UPDATE_POLICY;
123
190
  case 'license.pkg-url':
@@ -128,10 +195,22 @@ export function getEffectiveCliConfigValue(config, key) {
128
195
  return trimValue(config.name) || DEFAULT_DOCKER_CONTAINER_PREFIX;
129
196
  case 'bin.docker':
130
197
  return DEFAULT_DOCKER_BIN;
198
+ case 'bin.caddy':
199
+ return DEFAULT_CADDY_BIN;
131
200
  case 'bin.git':
132
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;
133
208
  case 'bin.yarn':
134
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);
135
214
  }
136
215
  }
137
216
  export function normalizeCliConfigValue(key, value) {
@@ -156,6 +235,19 @@ export function normalizeCliConfigValue(key, value) {
156
235
  }
157
236
  return policy;
158
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
+ }
159
251
  return normalized;
160
252
  }
161
253
  export async function loadCliConfig(options = {}) {
@@ -185,6 +277,18 @@ export async function setCliConfigValue(key, value, options = {}) {
185
277
  case 'locale':
186
278
  config.settings.locale = normalized;
187
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;
188
292
  case 'update.policy':
189
293
  config.settings.update = {
190
294
  ...(config.settings.update ?? {}),
@@ -215,18 +319,54 @@ export async function setCliConfigValue(key, value, options = {}) {
215
319
  docker: normalized,
216
320
  };
217
321
  break;
322
+ case 'bin.caddy':
323
+ config.settings.bin = {
324
+ ...(config.settings.bin ?? {}),
325
+ caddy: normalized,
326
+ };
327
+ break;
218
328
  case 'bin.git':
219
329
  config.settings.bin = {
220
330
  ...(config.settings.bin ?? {}),
221
331
  git: normalized,
222
332
  };
223
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;
224
352
  case 'bin.yarn':
225
353
  config.settings.bin = {
226
354
  ...(config.settings.bin ?? {}),
227
355
  yarn: normalized,
228
356
  };
229
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;
230
370
  }
231
371
  pruneSettings(config);
232
372
  await saveAuthConfig(config, scope);
@@ -244,6 +384,16 @@ export async function deleteCliConfigValue(key, options = {}) {
244
384
  case 'locale':
245
385
  delete config.settings.locale;
246
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;
247
397
  case 'update.policy':
248
398
  if (config.settings.update) {
249
399
  delete config.settings.update.policy;
@@ -269,16 +419,46 @@ export async function deleteCliConfigValue(key, options = {}) {
269
419
  delete config.settings.bin.docker;
270
420
  }
271
421
  break;
422
+ case 'bin.caddy':
423
+ if (config.settings.bin) {
424
+ delete config.settings.bin.caddy;
425
+ }
426
+ break;
272
427
  case 'bin.git':
273
428
  if (config.settings.bin) {
274
429
  delete config.settings.bin.git;
275
430
  }
276
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;
277
447
  case 'bin.yarn':
278
448
  if (config.settings.bin) {
279
449
  delete config.settings.bin.yarn;
280
450
  }
281
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;
282
462
  }
283
463
  pruneSettings(config);
284
464
  await saveAuthConfig(config, scope);
@@ -287,6 +467,12 @@ export async function deleteCliConfigValue(key, options = {}) {
287
467
  export async function resolveDockerNetworkName(options = {}) {
288
468
  return await getCliConfigValue('docker.network', options);
289
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
+ }
290
476
  export async function resolveDockerContainerPrefix(options = {}) {
291
477
  return await getCliConfigValue('docker.container-prefix', options);
292
478
  }
@@ -295,7 +481,9 @@ export async function resolveLicensePkgUrlFromConfig(options = {}) {
295
481
  }
296
482
  const CONFIGURABLE_COMMAND_KEYS = {
297
483
  docker: 'bin.docker',
484
+ caddy: 'bin.caddy',
298
485
  git: 'bin.git',
486
+ nginx: 'bin.nginx',
299
487
  yarn: 'bin.yarn',
300
488
  };
301
489
  export function isConfigurableCommandName(value) {
@@ -42,3 +42,11 @@ export function formatCliEntryError(error, argv) {
42
42
  `If \`${attemptedCommand}\` should be a runtime command from your NocoBase app, check whether the connected app exposes it, then retry the command.`,
43
43
  ].join('\n');
44
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
+ }
@@ -0,0 +1,284 @@
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
+ import fs from 'node:fs';
10
+ import fsp from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import { resolveCliHomeDir } from './cli-home.js';
13
+ import { DEFAULT_LOG_ENABLED, DEFAULT_LOG_RETENTION_DAYS, getCliConfigValue } from './cli-config.js';
14
+ const ACTIVE_LOG_FILE_ENV = 'NB_CLI_ACTIVE_LOG_FILE';
15
+ const ACTIVE_META_FILE_ENV = 'NB_CLI_ACTIVE_META_FILE';
16
+ const LOG_DIR_ENV = 'NB_CLI_LOG_DIR';
17
+ const LOG_DISABLED_ENV = 'NB_CLI_LOG_DISABLED';
18
+ const ANSI_ESCAPE_PATTERN = new RegExp(String.raw `\u001B\[[0-?]*[ -/]*[@-~]`, 'g');
19
+ const CLEANUP_STATE_FILE = '.cleanup-state.json';
20
+ const CLEANUP_INTERVAL_MS = 12 * 60 * 60 * 1000;
21
+ function isTruthyEnvFlag(value) {
22
+ const normalized = String(value ?? '').trim().toLowerCase();
23
+ return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
24
+ }
25
+ function pad(value) {
26
+ return String(value).padStart(2, '0');
27
+ }
28
+ function formatDateParts(date) {
29
+ const year = date.getFullYear();
30
+ const month = pad(date.getMonth() + 1);
31
+ const day = pad(date.getDate());
32
+ const hour = pad(date.getHours());
33
+ const minute = pad(date.getMinutes());
34
+ const second = pad(date.getSeconds());
35
+ return {
36
+ dayKey: `${year}-${month}-${day}`,
37
+ timestampKey: `${year}${month}${day}-${hour}${minute}${second}`,
38
+ };
39
+ }
40
+ function sanitizeFileNameSegment(value) {
41
+ const normalized = value
42
+ .trim()
43
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
44
+ .replace(/-+/g, '-')
45
+ .replace(/^-|-$/g, '');
46
+ return normalized || 'command';
47
+ }
48
+ function resolveSessionDirName(sessionId) {
49
+ const trimmed = String(sessionId ?? '').trim();
50
+ if (!trimmed) {
51
+ return 'no-session';
52
+ }
53
+ return sanitizeFileNameSegment(trimmed);
54
+ }
55
+ export function stripAnsi(value) {
56
+ return value.replace(ANSI_ESCAPE_PATTERN, '');
57
+ }
58
+ export function shouldEnableCommandLog(env = process.env) {
59
+ return !isTruthyEnvFlag(env[LOG_DISABLED_ENV]);
60
+ }
61
+ export function resolveCommandLogDir(env = process.env) {
62
+ if (!shouldEnableCommandLog(env)) {
63
+ return undefined;
64
+ }
65
+ const configured = String(env[LOG_DIR_ENV] ?? '').trim();
66
+ if (configured) {
67
+ return path.resolve(configured);
68
+ }
69
+ return path.join(resolveCliHomeDir(), 'logs');
70
+ }
71
+ export function getActiveCommandLogFile(env = process.env) {
72
+ const value = String(env[ACTIVE_LOG_FILE_ENV] ?? '').trim();
73
+ return value || undefined;
74
+ }
75
+ function readActiveCommandMetaFile(env = process.env) {
76
+ const value = String(env[ACTIVE_META_FILE_ENV] ?? '').trim();
77
+ return value || undefined;
78
+ }
79
+ export function hasActiveCommandLog(env = process.env) {
80
+ return Boolean(getActiveCommandLogFile(env));
81
+ }
82
+ export function sanitizeArgv(argv) {
83
+ const sensitiveFlags = new Set(['--token', '--access-token', '--password', '--secret']);
84
+ const sanitized = [];
85
+ for (let index = 0; index < argv.length; index += 1) {
86
+ const token = argv[index];
87
+ if (sensitiveFlags.has(token)) {
88
+ sanitized.push(token);
89
+ if (index + 1 < argv.length) {
90
+ sanitized.push('***');
91
+ index += 1;
92
+ }
93
+ continue;
94
+ }
95
+ const matched = token.match(/^(--(?:token|access-token|password|secret))=(.*)$/);
96
+ if (matched) {
97
+ sanitized.push(`${matched[1]}=***`);
98
+ continue;
99
+ }
100
+ sanitized.push(token);
101
+ }
102
+ return sanitized;
103
+ }
104
+ export function getCommandLogCommandId(argv) {
105
+ const positional = argv.filter((token) => token && !token.startsWith('-'));
106
+ if (positional.length === 0) {
107
+ return 'command';
108
+ }
109
+ const commandPath = positional[0] === 'api' ? positional.slice(0, 3) : positional.slice(0, 2);
110
+ return sanitizeFileNameSegment(commandPath.join('-'));
111
+ }
112
+ function parseDateDirName(name) {
113
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(name)) {
114
+ return undefined;
115
+ }
116
+ const parsed = new Date(`${name}T00:00:00.000Z`);
117
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed;
118
+ }
119
+ async function readCleanupState(filePath) {
120
+ try {
121
+ return JSON.parse(await fsp.readFile(filePath, 'utf8'));
122
+ }
123
+ catch {
124
+ return {};
125
+ }
126
+ }
127
+ async function writeCleanupState(filePath, state) {
128
+ await fsp.writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
129
+ }
130
+ async function resolveCommandLogRetentionDays() {
131
+ const configured = await getCliConfigValue('log.retention-days').catch(() => String(DEFAULT_LOG_RETENTION_DAYS));
132
+ const parsed = Number.parseInt(String(configured).trim(), 10);
133
+ if (!Number.isInteger(parsed) || parsed < 0) {
134
+ return DEFAULT_LOG_RETENTION_DAYS;
135
+ }
136
+ return parsed;
137
+ }
138
+ async function resolveCommandLogEnabled() {
139
+ const configured = await getCliConfigValue('log.enabled').catch(() => String(DEFAULT_LOG_ENABLED));
140
+ const normalized = String(configured).trim().toLowerCase();
141
+ if (normalized === 'false') {
142
+ return false;
143
+ }
144
+ if (normalized === 'true') {
145
+ return true;
146
+ }
147
+ return DEFAULT_LOG_ENABLED;
148
+ }
149
+ async function cleanupCommandLogs(rootDir, retentionDays, now = new Date()) {
150
+ if (retentionDays === 0) {
151
+ return;
152
+ }
153
+ const entries = await fsp.readdir(rootDir, { withFileTypes: true }).catch(() => []);
154
+ const cutoff = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
155
+ cutoff.setUTCDate(cutoff.getUTCDate() - retentionDays + 1);
156
+ for (const entry of entries) {
157
+ if (!entry.isDirectory()) {
158
+ continue;
159
+ }
160
+ const dirDate = parseDateDirName(entry.name);
161
+ if (!dirDate || dirDate >= cutoff) {
162
+ continue;
163
+ }
164
+ await fsp.rm(path.join(rootDir, entry.name), { recursive: true, force: true });
165
+ }
166
+ }
167
+ async function maybeCleanupCommandLogs(rootDir, env = process.env) {
168
+ await fsp.mkdir(rootDir, { recursive: true });
169
+ const stateFile = path.join(rootDir, CLEANUP_STATE_FILE);
170
+ const state = await readCleanupState(stateFile);
171
+ const lastCleanupAt = Date.parse(String(state.lastCleanupAt ?? ''));
172
+ if (!Number.isNaN(lastCleanupAt) && Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) {
173
+ return;
174
+ }
175
+ const retentionDays = await resolveCommandLogRetentionDays();
176
+ await cleanupCommandLogs(rootDir, retentionDays);
177
+ await writeCleanupState(stateFile, { lastCleanupAt: new Date().toISOString() });
178
+ }
179
+ async function writeMetaFile(filePath, meta) {
180
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
181
+ await fsp.writeFile(filePath, `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
182
+ }
183
+ function writeMetaFileSync(filePath, meta) {
184
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
185
+ fs.writeFileSync(filePath, `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
186
+ }
187
+ export async function initCommandLogSession(options) {
188
+ const env = options.env ?? process.env;
189
+ if (!shouldEnableCommandLog(env)) {
190
+ return undefined;
191
+ }
192
+ const enabled = await resolveCommandLogEnabled().catch(() => DEFAULT_LOG_ENABLED);
193
+ if (!enabled) {
194
+ return undefined;
195
+ }
196
+ const rootDir = resolveCommandLogDir(env);
197
+ if (!rootDir) {
198
+ return undefined;
199
+ }
200
+ await maybeCleanupCommandLogs(rootDir, env).catch(() => undefined);
201
+ const startedAt = options.startedAt ?? new Date();
202
+ const { dayKey, timestampKey } = formatDateParts(startedAt);
203
+ const commandId = getCommandLogCommandId(options.argv);
204
+ const fileStem = `${timestampKey}-${commandId}-${options.pid ?? process.pid}`;
205
+ const sessionDirName = resolveSessionDirName(options.sessionId);
206
+ const logDir = path.join(rootDir, dayKey, sessionDirName);
207
+ const logFile = path.join(logDir, `${fileStem}.log`);
208
+ const metaFile = path.join(logDir, `${fileStem}.meta.json`);
209
+ const meta = {
210
+ command: commandId,
211
+ argv: sanitizeArgv(options.argv),
212
+ cwd: options.cwd,
213
+ pid: options.pid ?? process.pid,
214
+ ppid: options.ppid ?? process.ppid,
215
+ sessionId: options.sessionId,
216
+ startedAt: startedAt.toISOString(),
217
+ logFile,
218
+ cliVersion: options.cliVersion,
219
+ nodeVersion: options.nodeVersion,
220
+ platform: options.platform,
221
+ interactive: options.interactive,
222
+ verbose: options.verbose,
223
+ };
224
+ await fsp.mkdir(logDir, { recursive: true });
225
+ await fsp.writeFile(logFile, '', 'utf8');
226
+ await writeMetaFile(metaFile, meta);
227
+ env[ACTIVE_LOG_FILE_ENV] = logFile;
228
+ env[ACTIVE_META_FILE_ENV] = metaFile;
229
+ return {
230
+ logFile,
231
+ metaFile,
232
+ meta,
233
+ };
234
+ }
235
+ export function finalizeCommandLogSessionSync(session, options = {}) {
236
+ if (!session) {
237
+ return;
238
+ }
239
+ const endedAt = options.endedAt ?? new Date();
240
+ const startedAtMs = Date.parse(session.meta.startedAt);
241
+ const nextMeta = {
242
+ ...session.meta,
243
+ endedAt: endedAt.toISOString(),
244
+ durationMs: Number.isNaN(startedAtMs) ? undefined : Math.max(0, endedAt.getTime() - startedAtMs),
245
+ exitCode: options.exitCode ?? session.meta.exitCode,
246
+ signal: options.signal ?? session.meta.signal,
247
+ errorMessage: options.errorMessage ?? session.meta.errorMessage,
248
+ };
249
+ writeMetaFileSync(session.metaFile, nextMeta);
250
+ session.meta = nextMeta;
251
+ }
252
+ export function appendCommandLogChunk(chunk, env = process.env) {
253
+ const logFile = getActiveCommandLogFile(env);
254
+ if (!logFile) {
255
+ return;
256
+ }
257
+ const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
258
+ if (!text) {
259
+ return;
260
+ }
261
+ fs.appendFileSync(logFile, stripAnsi(text), 'utf8');
262
+ }
263
+ export function installCommandLogWriteHooks(env = process.env) {
264
+ if (!hasActiveCommandLog(env)) {
265
+ return undefined;
266
+ }
267
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
268
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
269
+ process.stdout.write = ((chunk, encoding, callback) => {
270
+ appendCommandLogChunk(chunk, env);
271
+ return originalStdoutWrite(chunk, encoding, callback);
272
+ });
273
+ process.stderr.write = ((chunk, encoding, callback) => {
274
+ appendCommandLogChunk(chunk, env);
275
+ return originalStderrWrite(chunk, encoding, callback);
276
+ });
277
+ return () => {
278
+ process.stdout.write = originalStdoutWrite;
279
+ process.stderr.write = originalStderrWrite;
280
+ };
281
+ }
282
+ export function getActiveCommandMetaFile(env = process.env) {
283
+ return readActiveCommandMetaFile(env);
284
+ }
@@ -8,15 +8,23 @@
8
8
  */
9
9
  import { access } from 'node:fs/promises';
10
10
  import { resolveConfiguredEnvPath } from './cli-home.js';
11
+ import { inferConfiguredAppPathFromLegacyConfig } from './env-paths.js';
11
12
  function trimValue(value) {
12
13
  const text = String(value ?? '').trim();
13
14
  return text || undefined;
14
15
  }
15
- export function defaultDockerEnvFilePath(envName) {
16
+ function appendDotEnvPath(basePath) {
17
+ return `${basePath.replace(/[\\/]+$/, '')}/.env`;
18
+ }
19
+ export function defaultDockerEnvFilePath(envName, config) {
20
+ const appPath = inferConfiguredAppPathFromLegacyConfig(config ?? {});
21
+ if (appPath) {
22
+ return appendDotEnvPath(appPath);
23
+ }
16
24
  return `${envName}/.env`;
17
25
  }
18
26
  export function resolveConfiguredDockerEnvFilePath(envName, config) {
19
- return trimValue(config?.envFile) || defaultDockerEnvFilePath(envName);
27
+ return trimValue(config?.envFile) || defaultDockerEnvFilePath(envName, config);
20
28
  }
21
29
  export function hasExplicitDockerEnvFile(config) {
22
30
  return Boolean(trimValue(config?.envFile));