@git.zone/cli 3.2.1 → 5.0.0

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 (135) hide show
  1. package/.smartconfig.json +0 -1
  2. package/dist_ts/00_commitinfo_data.js +2 -2
  3. package/dist_ts/classes.gitzoneconfig.js +1 -1
  4. package/dist_ts/classes.project.js +1 -1
  5. package/dist_ts/gitzone.cli.js +1 -1
  6. package/dist_ts/gitzone.logging.js +1 -1
  7. package/dist_ts/helpers.changelog.js +1 -1
  8. package/dist_ts/helpers.climode.js +1 -1
  9. package/dist_ts/helpers.smartconfig.js +61 -11
  10. package/dist_ts/helpers.smartconfigmigrations.js +1 -1
  11. package/dist_ts/helpers.tsdocker.js +1 -1
  12. package/dist_ts/helpers.workflow.d.ts +0 -2
  13. package/dist_ts/helpers.workflow.js +23 -12
  14. package/dist_ts/index.js +1 -1
  15. package/dist_ts/mod_audit/index.js +1 -1
  16. package/dist_ts/mod_commit/index.js +1 -1
  17. package/dist_ts/mod_commit/mod.helpers.d.ts +3 -3
  18. package/dist_ts/mod_commit/mod.helpers.js +10 -10
  19. package/dist_ts/mod_commit/mod.plugins.js +1 -1
  20. package/dist_ts/mod_commit/mod.ui.js +1 -1
  21. package/dist_ts/mod_config/classes.commitconfig.js +1 -1
  22. package/dist_ts/mod_config/classes.releaseconfig.js +1 -1
  23. package/dist_ts/mod_config/index.js +23 -24
  24. package/dist_ts/mod_config/mod.plugins.js +1 -1
  25. package/dist_ts/mod_deprecate/index.js +1 -1
  26. package/dist_ts/mod_deprecate/mod.plugins.js +1 -1
  27. package/dist_ts/mod_docker/classes.dockerpruner.js +7 -1
  28. package/dist_ts/mod_docker/index.js +1 -1
  29. package/dist_ts/mod_docker/mod.plugins.js +1 -1
  30. package/dist_ts/mod_format/classes.baseformatter.js +1 -1
  31. package/dist_ts/mod_format/classes.diffreporter.js +1 -1
  32. package/dist_ts/mod_format/classes.formatcontext.js +1 -1
  33. package/dist_ts/mod_format/classes.formatplanner.js +1 -1
  34. package/dist_ts/mod_format/classes.formatstats.js +1 -1
  35. package/dist_ts/mod_format/formatters/assets.formatter.js +1 -1
  36. package/dist_ts/mod_format/formatters/cleanup.formatter.js +1 -1
  37. package/dist_ts/mod_format/formatters/copy.formatter.js +1 -1
  38. package/dist_ts/mod_format/formatters/gitignore.formatter.js +1 -1
  39. package/dist_ts/mod_format/formatters/license.formatter.js +1 -1
  40. package/dist_ts/mod_format/formatters/packagejson.formatter.js +1 -1
  41. package/dist_ts/mod_format/formatters/prettier.formatter.js +1 -1
  42. package/dist_ts/mod_format/formatters/readme.formatter.js +1 -1
  43. package/dist_ts/mod_format/formatters/smartconfig.formatter.js +1 -1
  44. package/dist_ts/mod_format/formatters/templates.formatter.js +1 -1
  45. package/dist_ts/mod_format/formatters/tsconfig.formatter.js +1 -1
  46. package/dist_ts/mod_format/index.js +1 -1
  47. package/dist_ts/mod_format/interfaces.format.js +1 -1
  48. package/dist_ts/mod_format/mod.plugins.js +1 -1
  49. package/dist_ts/mod_helpers/index.js +1 -1
  50. package/dist_ts/mod_helpers/mod.plugins.js +1 -1
  51. package/dist_ts/mod_meta/index.js +1 -1
  52. package/dist_ts/mod_meta/meta.classes.meta.js +1 -1
  53. package/dist_ts/mod_meta/meta.interfaces.js +1 -1
  54. package/dist_ts/mod_meta/meta.plugins.js +1 -1
  55. package/dist_ts/mod_open/index.js +1 -1
  56. package/dist_ts/mod_open/mod.plugins.js +1 -1
  57. package/dist_ts/mod_release/helpers.releasebranch.d.ts +46 -0
  58. package/dist_ts/mod_release/helpers.releasebranch.js +579 -0
  59. package/dist_ts/mod_release/index.d.ts +10 -0
  60. package/dist_ts/mod_release/index.js +300 -92
  61. package/dist_ts/mod_release/mod.plugins.js +1 -1
  62. package/dist_ts/mod_services/classes.dockercontainer.d.ts +50 -3
  63. package/dist_ts/mod_services/classes.dockercontainer.js +646 -84
  64. package/dist_ts/mod_services/classes.globalregistry.d.ts +10 -24
  65. package/dist_ts/mod_services/classes.globalregistry.js +210 -62
  66. package/dist_ts/mod_services/classes.globalregistrystore.d.ts +48 -0
  67. package/dist_ts/mod_services/classes.globalregistrystore.js +156 -0
  68. package/dist_ts/mod_services/classes.interprocesslock.d.ts +21 -0
  69. package/dist_ts/mod_services/classes.interprocesslock.js +141 -0
  70. package/dist_ts/mod_services/classes.serviceconfiguration.d.ts +15 -12
  71. package/dist_ts/mod_services/classes.serviceconfiguration.js +188 -91
  72. package/dist_ts/mod_services/classes.servicedatamarker.d.ts +32 -7
  73. package/dist_ts/mod_services/classes.servicedatamarker.js +145 -49
  74. package/dist_ts/mod_services/classes.servicemanager.d.ts +70 -28
  75. package/dist_ts/mod_services/classes.servicemanager.js +995 -421
  76. package/dist_ts/mod_services/classes.servicenames.d.ts +8 -0
  77. package/dist_ts/mod_services/classes.servicenames.js +54 -0
  78. package/dist_ts/mod_services/classes.serviceoptions.js +1 -1
  79. package/dist_ts/mod_services/classes.servicepruner.d.ts +3 -0
  80. package/dist_ts/mod_services/classes.servicepruner.js +208 -46
  81. package/dist_ts/mod_services/helpers.js +1 -1
  82. package/dist_ts/mod_services/index.d.ts +2 -0
  83. package/dist_ts/mod_services/index.js +81 -58
  84. package/dist_ts/mod_services/mod.plugins.js +1 -1
  85. package/dist_ts/mod_standard/index.js +2 -2
  86. package/dist_ts/mod_standard/mod.plugins.js +1 -1
  87. package/dist_ts/mod_start/index.js +1 -1
  88. package/dist_ts/mod_start/mod.plugins.js +1 -1
  89. package/dist_ts/mod_template/index.js +1 -1
  90. package/dist_ts/mod_template/mod.plugins.js +1 -1
  91. package/dist_ts/mod_tools/classes.packagemanager.js +1 -1
  92. package/dist_ts/mod_tools/index.js +1 -1
  93. package/dist_ts/mod_tools/mod.plugins.js +1 -1
  94. package/dist_ts/paths.js +1 -1
  95. package/dist_ts/plugins.d.ts +3 -1
  96. package/dist_ts/plugins.js +4 -2
  97. package/dist_ts_migration/001.service-selection.d.ts +9 -0
  98. package/dist_ts_migration/001.service-selection.js +51 -0
  99. package/dist_ts_migration/002.runtime-config.d.ts +19 -0
  100. package/dist_ts_migration/002.runtime-config.js +170 -0
  101. package/dist_ts_migration/003.service-data-marker.d.ts +9 -0
  102. package/dist_ts_migration/003.service-data-marker.js +156 -0
  103. package/dist_ts_migration/004.global-registry.d.ts +13 -0
  104. package/dist_ts_migration/004.global-registry.js +165 -0
  105. package/dist_ts_migration/index.d.ts +11 -0
  106. package/dist_ts_migration/index.js +59 -0
  107. package/package.json +6 -3
  108. package/readme.hints.md +102 -25
  109. package/readme.md +144 -41
  110. package/readme.plan.md +29 -16
  111. package/ts/00_commitinfo_data.ts +1 -1
  112. package/ts/helpers.smartconfig.ts +63 -10
  113. package/ts/helpers.workflow.ts +41 -12
  114. package/ts/mod_commit/mod.helpers.ts +12 -8
  115. package/ts/mod_config/index.ts +22 -23
  116. package/ts/mod_docker/classes.dockerpruner.ts +6 -0
  117. package/ts/mod_release/helpers.releasebranch.ts +1282 -0
  118. package/ts/mod_release/index.ts +608 -129
  119. package/ts/mod_services/classes.dockercontainer.ts +912 -96
  120. package/ts/mod_services/classes.globalregistry.ts +290 -89
  121. package/ts/mod_services/classes.globalregistrystore.ts +279 -0
  122. package/ts/mod_services/classes.interprocesslock.ts +189 -0
  123. package/ts/mod_services/classes.serviceconfiguration.ts +215 -108
  124. package/ts/mod_services/classes.servicedatamarker.ts +210 -52
  125. package/ts/mod_services/classes.servicemanager.ts +1332 -512
  126. package/ts/mod_services/classes.servicenames.ts +76 -0
  127. package/ts/mod_services/classes.servicepruner.ts +310 -52
  128. package/ts/mod_services/index.ts +89 -62
  129. package/ts/mod_standard/index.ts +1 -1
  130. package/ts/plugins.ts +4 -0
  131. package/ts_migration/001.service-selection.ts +77 -0
  132. package/ts_migration/002.runtime-config.ts +218 -0
  133. package/ts_migration/003.service-data-marker.ts +216 -0
  134. package/ts_migration/004.global-registry.ts +272 -0
  135. package/ts_migration/index.ts +83 -0
@@ -9,6 +9,7 @@ import {
9
9
  writeMongodbAuthOption,
10
10
  type TServiceOptionSource,
11
11
  } from './classes.serviceoptions.js';
12
+ import { withInterProcessLock } from './classes.interprocesslock.js';
12
13
 
13
14
  /** Hosts that are considered local for the purposes of the no-auth guard. */
14
15
  const localMongoHosts = ['localhost', '127.0.0.1', '::1'];
@@ -99,22 +100,6 @@ export const deriveS3BucketName = (projectNameArg: string): string => {
99
100
  return canonical;
100
101
  };
101
102
 
102
- export type TLegacyS3BucketRepair =
103
- | { bucket: unknown; repaired: false }
104
- | { bucket: string; repaired: true };
105
-
106
- /** Repair only the exact invalid value emitted by older GitZone releases. */
107
- export const repairLegacyGeneratedS3Bucket = (
108
- projectNameArg: string,
109
- bucketArg: unknown,
110
- ): TLegacyS3BucketRepair => {
111
- const legacyGeneratedBucket = `${projectNameArg}-documents`;
112
- if (bucketArg !== legacyGeneratedBucket || isValidS3BucketName(bucketArg)) {
113
- return { bucket: bucketArg, repaired: false };
114
- }
115
- return { bucket: deriveS3BucketName(projectNameArg), repaired: true };
116
- };
117
-
118
103
  export interface IServiceConfig {
119
104
  PROJECT_NAME: string;
120
105
  MONGODB_HOST: string;
@@ -131,9 +116,11 @@ export interface IServiceConfig {
131
116
  MONGODB_AUTH_ENABLED: boolean;
132
117
  S3_HOST: string;
133
118
  S3_PORT: string;
134
- S3_CONSOLE_PORT: string;
119
+ S3_UI_PORT: string;
135
120
  S3_ACCESSKEY: string;
136
121
  S3_SECRETKEY: string;
122
+ S3_ADMIN_PASSWORD: string;
123
+ S3_REGION: string;
137
124
  S3_BUCKET: string;
138
125
  S3_ENDPOINT: string;
139
126
  S3_USESSL: boolean;
@@ -144,9 +131,75 @@ export interface IServiceConfig {
144
131
  ELASTICSEARCH_URL: string;
145
132
  }
146
133
 
134
+ const runtimeStringFields: Array<keyof IServiceConfig> = [
135
+ 'PROJECT_NAME',
136
+ 'MONGODB_HOST',
137
+ 'MONGODB_NAME',
138
+ 'MONGODB_PORT',
139
+ 'MONGODB_USER',
140
+ 'MONGODB_PASS',
141
+ 'MONGODB_URL',
142
+ 'S3_HOST',
143
+ 'S3_PORT',
144
+ 'S3_UI_PORT',
145
+ 'S3_ACCESSKEY',
146
+ 'S3_SECRETKEY',
147
+ 'S3_ADMIN_PASSWORD',
148
+ 'S3_REGION',
149
+ 'S3_BUCKET',
150
+ 'S3_ENDPOINT',
151
+ 'ELASTICSEARCH_HOST',
152
+ 'ELASTICSEARCH_PORT',
153
+ 'ELASTICSEARCH_USER',
154
+ 'ELASTICSEARCH_PASS',
155
+ 'ELASTICSEARCH_URL',
156
+ ];
157
+
158
+ const runtimePortFields: Array<keyof IServiceConfig> = [
159
+ 'MONGODB_PORT',
160
+ 'S3_PORT',
161
+ 'S3_UI_PORT',
162
+ 'ELASTICSEARCH_PORT',
163
+ ];
164
+
165
+ /** Validate the complete canonical env.json shape before runtime use or persistence. */
166
+ export const assertCanonicalServiceRuntimeConfig: (
167
+ configArg: unknown,
168
+ ) => asserts configArg is IServiceConfig = (configArg) => {
169
+ if (typeof configArg !== 'object' || configArg === null || Array.isArray(configArg)) {
170
+ throw new Error('.nogit/env.json must contain a JSON object at its root.');
171
+ }
172
+ const config = configArg as Record<string, unknown>;
173
+ for (const field of runtimeStringFields) {
174
+ if (typeof config[field] !== 'string' || !(config[field] as string).trim()) {
175
+ throw new Error(`.nogit/env.json field ${field} must be a non-empty string.`);
176
+ }
177
+ }
178
+ if (typeof config.MONGODB_AUTH_ENABLED !== 'boolean') {
179
+ throw new Error('.nogit/env.json field MONGODB_AUTH_ENABLED must be a boolean.');
180
+ }
181
+ if (typeof config.S3_USESSL !== 'boolean') {
182
+ throw new Error('.nogit/env.json field S3_USESSL must be a boolean.');
183
+ }
184
+ for (const field of runtimePortFields) {
185
+ const rawPort = config[field];
186
+ if (typeof rawPort !== 'string' || !/^\d{1,5}$/u.test(rawPort)) {
187
+ throw new Error(`.nogit/env.json field ${field} must be a decimal port string.`);
188
+ }
189
+ const port = Number(rawPort);
190
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
191
+ throw new Error(`.nogit/env.json field ${field} is outside the TCP port range.`);
192
+ }
193
+ }
194
+ if (config.S3_PORT === config.S3_UI_PORT) {
195
+ throw new Error('.nogit/env.json S3 API and UI ports must differ.');
196
+ }
197
+ };
198
+
147
199
  export class ServiceConfiguration {
148
200
  private configPath: string;
149
201
  private config!: IServiceConfig;
202
+ private persistedConfigSnapshot?: string;
150
203
  private docker: DockerContainer;
151
204
  /** Where the effective MongoDB auth mode came from. */
152
205
  private mongoAuthSource: TServiceOptionSource = 'default';
@@ -173,12 +226,20 @@ export class ServiceConfiguration {
173
226
  // value, so a fresh checkout reproduces the declared setup.
174
227
  await this.applyDeclaredServiceOptions();
175
228
 
176
- // Sync ports from existing Docker containers if they exist
229
+ // Only non-S3 services retain legacy port synchronization. ObjectStorage
230
+ // configuration is authoritative and is reconciled on its guarded start path.
177
231
  await this.syncPortsFromDocker();
178
232
 
179
233
  return this.config;
180
234
  }
181
235
 
236
+ /** Reload and validate persisted runtime state without deriving or writing fields. */
237
+ public async reloadFromDisk(): Promise<IServiceConfig> {
238
+ await this.loadConfig();
239
+ assertCanonicalServiceRuntimeConfig(this.config);
240
+ return this.config;
241
+ }
242
+
182
243
  /**
183
244
  * Fold `@git.zone/cli.serviceOptions` into the runtime configuration.
184
245
  */
@@ -237,7 +298,6 @@ export class ServiceConfiguration {
237
298
  */
238
299
  private updateDerivedFields(): void {
239
300
  this.config.MONGODB_URL = this.buildMongoUrl();
240
- this.config.S3_ENDPOINT = this.config.S3_HOST;
241
301
  this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
242
302
  }
243
303
 
@@ -261,6 +321,38 @@ export class ServiceConfiguration {
261
321
  }
262
322
  }
263
323
 
324
+ /** Reject ObjectStorage's prohibited defaults and credential reuse before Docker mutation. */
325
+ public assertObjectStorageCredentialsAreSafe(): void {
326
+ const config = this.config;
327
+ for (const [name, value] of [
328
+ ['S3_ACCESSKEY', config.S3_ACCESSKEY],
329
+ ['S3_SECRETKEY', config.S3_SECRETKEY],
330
+ ['S3_ADMIN_PASSWORD', config.S3_ADMIN_PASSWORD],
331
+ ] as const) {
332
+ if (typeof value !== 'string' || value.length === 0) {
333
+ throw new Error(`${name} must be a non-empty string in .nogit/env.json.`);
334
+ }
335
+ }
336
+ if (config.S3_ACCESSKEY === 'admin' && config.S3_SECRETKEY === 'admin') {
337
+ throw new Error(
338
+ 'ObjectStorage refuses the default S3 credentials. Set distinct non-default S3_ACCESSKEY and S3_SECRETKEY values in .nogit/env.json.',
339
+ );
340
+ }
341
+ if (config.S3_ADMIN_PASSWORD === 'admin') {
342
+ throw new Error(
343
+ 'ObjectStorage refuses the default admin password. Set a non-default S3_ADMIN_PASSWORD in .nogit/env.json.',
344
+ );
345
+ }
346
+ if (
347
+ config.S3_ADMIN_PASSWORD === config.S3_ACCESSKEY ||
348
+ config.S3_ADMIN_PASSWORD === config.S3_SECRETKEY
349
+ ) {
350
+ throw new Error(
351
+ 'S3_ADMIN_PASSWORD must be separate from S3_ACCESSKEY and S3_SECRETKEY in .nogit/env.json.',
352
+ );
353
+ }
354
+ }
355
+
264
356
  /**
265
357
  * Persist the MongoDB auth mode.
266
358
  *
@@ -281,10 +373,45 @@ export class ServiceConfiguration {
281
373
  * Save the configuration to file
282
374
  */
283
375
  public async saveConfig(): Promise<void> {
284
- await plugins.smartfs
285
- .file(this.configPath)
286
- .encoding('utf8')
287
- .write(JSON.stringify(this.config, null, 2));
376
+ assertCanonicalServiceRuntimeConfig(this.config);
377
+ await withInterProcessLock(
378
+ {
379
+ lockPath: `${this.configPath}.lock`,
380
+ description: 'Service runtime configuration',
381
+ },
382
+ async () => {
383
+ let currentSnapshot: string | undefined;
384
+ try {
385
+ currentSnapshot = await plugins.fs.readFile(this.configPath, 'utf8');
386
+ } catch (error) {
387
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
388
+ throw error;
389
+ }
390
+ }
391
+ if (currentSnapshot !== this.persistedConfigSnapshot) {
392
+ throw new Error(
393
+ 'Refusing to overwrite .nogit/env.json because it changed after loading.',
394
+ );
395
+ }
396
+
397
+ const content = `${JSON.stringify(this.config, null, 2)}\n`;
398
+ const tempPath = `${this.configPath}.tmp-${process.pid}-${plugins.crypto
399
+ .randomBytes(12)
400
+ .toString('hex')}`;
401
+ try {
402
+ await plugins.fs.writeFile(tempPath, content, {
403
+ encoding: 'utf8',
404
+ flag: 'wx',
405
+ mode: 0o600,
406
+ });
407
+ await plugins.fs.rename(tempPath, this.configPath);
408
+ this.persistedConfigSnapshot = content;
409
+ } catch (error) {
410
+ await plugins.fs.rm(tempPath, { force: true }).catch(() => {});
411
+ throw new Error('Could not persist .nogit/env.json atomically.', { cause: error });
412
+ }
413
+ },
414
+ );
288
415
  }
289
416
 
290
417
  /**
@@ -310,7 +437,12 @@ export class ServiceConfiguration {
310
437
  .file(this.configPath)
311
438
  .encoding('utf8')
312
439
  .read()) as string;
313
- this.config = JSON.parse(configContent);
440
+ const parsed = JSON.parse(configContent);
441
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
442
+ throw new Error('.nogit/env.json must contain a JSON object at its root.');
443
+ }
444
+ this.config = parsed as IServiceConfig;
445
+ this.persistedConfigSnapshot = configContent;
314
446
  }
315
447
 
316
448
  /**
@@ -320,11 +452,11 @@ export class ServiceConfiguration {
320
452
  const projectName = await helpers.getProjectName();
321
453
  const mongoPort = await helpers.getRandomAvailablePort();
322
454
  const s3Port = await helpers.getRandomAvailablePort();
323
- let s3ConsolePort = s3Port + 1;
455
+ let s3UiPort = s3Port + 1;
324
456
 
325
- // Ensure console port is also available
326
- while (!(await helpers.isPortAvailable(s3ConsolePort))) {
327
- s3ConsolePort++;
457
+ // Ensure management UI port is also available
458
+ while (!(await helpers.isPortAvailable(s3UiPort))) {
459
+ s3UiPort++;
328
460
  }
329
461
 
330
462
  const mongoUser = 'defaultadmin';
@@ -350,9 +482,11 @@ export class ServiceConfiguration {
350
482
  MONGODB_AUTH_ENABLED: true,
351
483
  S3_HOST: s3Host,
352
484
  S3_PORT: s3PortStr,
353
- S3_CONSOLE_PORT: s3ConsolePort.toString(),
485
+ S3_UI_PORT: s3UiPort.toString(),
354
486
  S3_ACCESSKEY: 'defaultadmin',
355
487
  S3_SECRETKEY: 'defaultpass',
488
+ S3_ADMIN_PASSWORD: plugins.crypto.randomBytes(24).toString('base64url'),
489
+ S3_REGION: 'us-east-1',
356
490
  S3_BUCKET: deriveS3BucketName(projectName),
357
491
  S3_ENDPOINT: s3Host,
358
492
  S3_USESSL: false,
@@ -363,12 +497,13 @@ export class ServiceConfiguration {
363
497
  ELASTICSEARCH_URL: `http://${esUser}:${esPass}@${esHost}:${esPort}`
364
498
  };
365
499
 
500
+ assertCanonicalServiceRuntimeConfig(this.config);
366
501
  await this.saveConfig();
367
502
 
368
503
  logger.log('ok', '✅ Created .nogit/env.json with project defaults');
369
504
  logger.log('info', `📍 MongoDB port: ${mongoPort}`);
370
505
  logger.log('info', `📍 S3 API port: ${s3Port}`);
371
- logger.log('info', `📍 S3 Console port: ${s3ConsolePort}`);
506
+ logger.log('info', `📍 S3 management UI port: ${s3UiPort}`);
372
507
  logger.log('info', `📍 Elasticsearch port: ${esPort}`);
373
508
  }
374
509
 
@@ -445,16 +580,16 @@ export class ServiceConfiguration {
445
580
  updated = true;
446
581
  }
447
582
 
448
- if (!this.config.S3_CONSOLE_PORT) {
583
+ if (!this.config.S3_UI_PORT) {
449
584
  const s3Port = parseInt(this.config.S3_PORT);
450
- let consolePort = s3Port + 1;
585
+ let uiPort = s3Port + 1;
451
586
 
452
- while (!(await helpers.isPortAvailable(consolePort))) {
453
- consolePort++;
587
+ while (!(await helpers.isPortAvailable(uiPort))) {
588
+ uiPort++;
454
589
  }
455
590
 
456
- this.config.S3_CONSOLE_PORT = consolePort.toString();
457
- fieldsAdded.push(`S3_CONSOLE_PORT(${consolePort})`);
591
+ this.config.S3_UI_PORT = uiPort.toString();
592
+ fieldsAdded.push(`S3_UI_PORT(${uiPort})`);
458
593
  updated = true;
459
594
  }
460
595
 
@@ -469,21 +604,11 @@ export class ServiceConfiguration {
469
604
  fieldsAdded.push('S3_SECRETKEY');
470
605
  updated = true;
471
606
  }
472
-
607
+
473
608
  if (!this.config.S3_BUCKET) {
474
609
  this.config.S3_BUCKET = deriveS3BucketName(this.config.PROJECT_NAME);
475
610
  fieldsAdded.push('S3_BUCKET');
476
611
  updated = true;
477
- } else {
478
- const repair = repairLegacyGeneratedS3Bucket(
479
- this.config.PROJECT_NAME,
480
- this.config.S3_BUCKET,
481
- );
482
- if (repair.repaired) {
483
- this.config.S3_BUCKET = repair.bucket;
484
- fieldsAdded.push('S3_BUCKET(repaired legacy generated name)');
485
- updated = true;
486
- }
487
612
  }
488
613
 
489
614
  // `undefined`, not falsy: a stored `false` is a valid value, and treating it
@@ -494,10 +619,8 @@ export class ServiceConfiguration {
494
619
  updated = true;
495
620
  }
496
621
 
497
- // Always update S3_ENDPOINT based on current settings
498
- const oldEndpoint = this.config.S3_ENDPOINT;
499
- this.config.S3_ENDPOINT = this.config.S3_HOST;
500
- if (oldEndpoint !== this.config.S3_ENDPOINT) {
622
+ if (!this.config.S3_ENDPOINT) {
623
+ this.config.S3_ENDPOINT = this.config.S3_HOST;
501
624
  fieldsAdded.push('S3_ENDPOINT');
502
625
  updated = true;
503
626
  }
@@ -534,6 +657,7 @@ export class ServiceConfiguration {
534
657
  updated = true;
535
658
  }
536
659
 
660
+ assertCanonicalServiceRuntimeConfig(this.config);
537
661
  if (updated) {
538
662
  await this.saveConfig();
539
663
  logger.log('ok', `✅ Added missing fields: ${fieldsAdded.join(', ')}`);
@@ -560,7 +684,8 @@ export class ServiceConfiguration {
560
684
  public getContainerNames() {
561
685
  return {
562
686
  mongo: `${this.config.PROJECT_NAME}-mongodb`,
563
- minio: `${this.config.PROJECT_NAME}-minio`,
687
+ objectstorage: `${this.config.PROJECT_NAME}-objectstorage`,
688
+ legacyMinio: `${this.config.PROJECT_NAME}-minio`,
564
689
  elasticsearch: `${this.config.PROJECT_NAME}-elasticsearch`
565
690
  };
566
691
  }
@@ -574,7 +699,7 @@ export class ServiceConfiguration {
574
699
  public getDataDirectories() {
575
700
  return {
576
701
  mongo: getServiceDataDirectory(process.cwd(), 'mongodb'),
577
- minio: getServiceDataDirectory(process.cwd(), 'minio'),
702
+ objectstorage: getServiceDataDirectory(process.cwd(), 'objectstorage'),
578
703
  elasticsearch: getServiceDataDirectory(process.cwd(), 'elasticsearch')
579
704
  };
580
705
  }
@@ -602,30 +727,6 @@ export class ServiceConfiguration {
602
727
  }
603
728
  }
604
729
 
605
- // Check MinIO container
606
- const minioStatus = await this.docker.getStatus(containers.minio);
607
- if (minioStatus !== 'not_exists') {
608
- const portMappings = await this.docker.getPortMappings(containers.minio);
609
- if (portMappings) {
610
- if (portMappings['9000']) {
611
- const dockerPort = portMappings['9000'];
612
- if (this.config.S3_PORT !== dockerPort) {
613
- logger.log('note', `📍 Syncing S3 API port from Docker: ${dockerPort}`);
614
- this.config.S3_PORT = dockerPort;
615
- updated = true;
616
- }
617
- }
618
- if (portMappings['9001']) {
619
- const dockerPort = portMappings['9001'];
620
- if (this.config.S3_CONSOLE_PORT !== dockerPort) {
621
- logger.log('note', `📍 Syncing S3 Console port from Docker: ${dockerPort}`);
622
- this.config.S3_CONSOLE_PORT = dockerPort;
623
- updated = true;
624
- }
625
- }
626
- }
627
- }
628
-
629
730
  // Check Elasticsearch container
630
731
  const esStatus = await this.docker.getStatus(containers.elasticsearch);
631
732
  if (esStatus !== 'not_exists') {
@@ -656,7 +757,6 @@ export class ServiceConfiguration {
656
757
 
657
758
  // Check if containers exist - if they do, ports are fine
658
759
  const mongoExists = await this.docker.exists(containers.mongo);
659
- const minioExists = await this.docker.exists(containers.minio);
660
760
  const esExists = await this.docker.exists(containers.elasticsearch);
661
761
 
662
762
  // Only check port availability if containers don't exist
@@ -671,30 +771,6 @@ export class ServiceConfiguration {
671
771
  }
672
772
  }
673
773
 
674
- if (!minioExists) {
675
- const s3Port = parseInt(this.config.S3_PORT);
676
- const s3ConsolePort = parseInt(this.config.S3_CONSOLE_PORT);
677
-
678
- if (!(await helpers.isPortAvailable(s3Port))) {
679
- logger.log('note', `⚠️ S3 API port ${s3Port} is in use, finding new port...`);
680
- const newPort = await helpers.getRandomAvailablePort();
681
- this.config.S3_PORT = newPort.toString();
682
- logger.log('ok', `✅ New S3 API port: ${newPort}`);
683
- updated = true;
684
- }
685
-
686
- if (!(await helpers.isPortAvailable(s3ConsolePort))) {
687
- logger.log('note', `⚠️ S3 Console port ${s3ConsolePort} is in use, finding new port...`);
688
- let newPort = parseInt(this.config.S3_PORT) + 1;
689
- while (!(await helpers.isPortAvailable(newPort))) {
690
- newPort++;
691
- }
692
- this.config.S3_CONSOLE_PORT = newPort.toString();
693
- logger.log('ok', `✅ New S3 Console port: ${newPort}`);
694
- updated = true;
695
- }
696
- }
697
-
698
774
  if (!esExists) {
699
775
  const esPort = parseInt(this.config.ELASTICSEARCH_PORT);
700
776
  if (!(await helpers.isPortAvailable(esPort))) {
@@ -713,6 +789,37 @@ export class ServiceConfiguration {
713
789
 
714
790
  return updated;
715
791
  }
792
+
793
+ /** Validate ObjectStorage host ports only after legacy/conflict checks pass. */
794
+ public async validateObjectStoragePorts(): Promise<boolean> {
795
+ let updated = false;
796
+ let s3Port = parseInt(this.config.S3_PORT);
797
+ if (!(await helpers.isPortAvailable(s3Port))) {
798
+ logger.log('note', `⚠️ S3 API port ${s3Port} is in use, finding new port...`);
799
+ s3Port = await helpers.getRandomAvailablePort();
800
+ this.config.S3_PORT = s3Port.toString();
801
+ logger.log('ok', `✅ New S3 API port: ${s3Port}`);
802
+ updated = true;
803
+ }
804
+
805
+ let uiPort = parseInt(this.config.S3_UI_PORT);
806
+ if (uiPort === s3Port || !(await helpers.isPortAvailable(uiPort))) {
807
+ logger.log('note', `⚠️ S3 management UI port ${uiPort} is in use, finding new port...`);
808
+ uiPort = s3Port + 1;
809
+ while (!(await helpers.isPortAvailable(uiPort))) {
810
+ uiPort++;
811
+ }
812
+ this.config.S3_UI_PORT = uiPort.toString();
813
+ logger.log('ok', `✅ New S3 management UI port: ${uiPort}`);
814
+ updated = true;
815
+ }
816
+
817
+ if (updated) {
818
+ this.updateDerivedFields();
819
+ await this.saveConfig();
820
+ }
821
+ return updated;
822
+ }
716
823
 
717
824
  /**
718
825
  * Force reconfigure all ports with new available ones
@@ -722,11 +829,11 @@ export class ServiceConfiguration {
722
829
 
723
830
  const mongoPort = await helpers.getRandomAvailablePort();
724
831
  const s3Port = await helpers.getRandomAvailablePort();
725
- let s3ConsolePort = s3Port + 1;
832
+ let s3UiPort = s3Port + 1;
726
833
 
727
- // Ensure console port is also available
728
- while (!(await helpers.isPortAvailable(s3ConsolePort))) {
729
- s3ConsolePort++;
834
+ // Ensure management UI port is also available
835
+ while (!(await helpers.isPortAvailable(s3UiPort))) {
836
+ s3UiPort++;
730
837
  }
731
838
 
732
839
  // Elasticsearch uses standard port 9200
@@ -734,7 +841,7 @@ export class ServiceConfiguration {
734
841
 
735
842
  this.config.MONGODB_PORT = mongoPort.toString();
736
843
  this.config.S3_PORT = s3Port.toString();
737
- this.config.S3_CONSOLE_PORT = s3ConsolePort.toString();
844
+ this.config.S3_UI_PORT = s3UiPort.toString();
738
845
  this.config.ELASTICSEARCH_PORT = esPort;
739
846
 
740
847
  this.updateDerivedFields();
@@ -743,7 +850,7 @@ export class ServiceConfiguration {
743
850
  logger.log('ok', '✅ New port configuration:');
744
851
  logger.log('info', ` 📍 MongoDB: ${mongoPort}`);
745
852
  logger.log('info', ` 📍 S3 API: ${s3Port}`);
746
- logger.log('info', ` 📍 S3 Console: ${s3ConsolePort}`);
853
+ logger.log('info', ` 📍 S3 management UI: ${s3UiPort}`);
747
854
  logger.log('info', ` 📍 Elasticsearch: ${esPort}`);
748
855
  }
749
856
  }