@git.zone/cli 3.2.1 → 4.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 (125) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/classes.gitzoneconfig.js +1 -1
  3. package/dist_ts/classes.project.js +1 -1
  4. package/dist_ts/gitzone.cli.js +1 -1
  5. package/dist_ts/gitzone.logging.js +1 -1
  6. package/dist_ts/helpers.changelog.js +1 -1
  7. package/dist_ts/helpers.climode.js +1 -1
  8. package/dist_ts/helpers.smartconfig.js +61 -11
  9. package/dist_ts/helpers.smartconfigmigrations.js +1 -1
  10. package/dist_ts/helpers.tsdocker.js +1 -1
  11. package/dist_ts/helpers.workflow.js +1 -1
  12. package/dist_ts/index.js +1 -1
  13. package/dist_ts/mod_audit/index.js +1 -1
  14. package/dist_ts/mod_commit/index.js +1 -1
  15. package/dist_ts/mod_commit/mod.helpers.js +1 -1
  16. package/dist_ts/mod_commit/mod.plugins.js +1 -1
  17. package/dist_ts/mod_commit/mod.ui.js +1 -1
  18. package/dist_ts/mod_config/classes.commitconfig.js +1 -1
  19. package/dist_ts/mod_config/classes.releaseconfig.js +1 -1
  20. package/dist_ts/mod_config/index.js +2 -2
  21. package/dist_ts/mod_config/mod.plugins.js +1 -1
  22. package/dist_ts/mod_deprecate/index.js +1 -1
  23. package/dist_ts/mod_deprecate/mod.plugins.js +1 -1
  24. package/dist_ts/mod_docker/classes.dockerpruner.js +7 -1
  25. package/dist_ts/mod_docker/index.js +1 -1
  26. package/dist_ts/mod_docker/mod.plugins.js +1 -1
  27. package/dist_ts/mod_format/classes.baseformatter.js +1 -1
  28. package/dist_ts/mod_format/classes.diffreporter.js +1 -1
  29. package/dist_ts/mod_format/classes.formatcontext.js +1 -1
  30. package/dist_ts/mod_format/classes.formatplanner.js +1 -1
  31. package/dist_ts/mod_format/classes.formatstats.js +1 -1
  32. package/dist_ts/mod_format/formatters/assets.formatter.js +1 -1
  33. package/dist_ts/mod_format/formatters/cleanup.formatter.js +1 -1
  34. package/dist_ts/mod_format/formatters/copy.formatter.js +1 -1
  35. package/dist_ts/mod_format/formatters/gitignore.formatter.js +1 -1
  36. package/dist_ts/mod_format/formatters/license.formatter.js +1 -1
  37. package/dist_ts/mod_format/formatters/packagejson.formatter.js +1 -1
  38. package/dist_ts/mod_format/formatters/prettier.formatter.js +1 -1
  39. package/dist_ts/mod_format/formatters/readme.formatter.js +1 -1
  40. package/dist_ts/mod_format/formatters/smartconfig.formatter.js +1 -1
  41. package/dist_ts/mod_format/formatters/templates.formatter.js +1 -1
  42. package/dist_ts/mod_format/formatters/tsconfig.formatter.js +1 -1
  43. package/dist_ts/mod_format/index.js +1 -1
  44. package/dist_ts/mod_format/interfaces.format.js +1 -1
  45. package/dist_ts/mod_format/mod.plugins.js +1 -1
  46. package/dist_ts/mod_helpers/index.js +1 -1
  47. package/dist_ts/mod_helpers/mod.plugins.js +1 -1
  48. package/dist_ts/mod_meta/index.js +1 -1
  49. package/dist_ts/mod_meta/meta.classes.meta.js +1 -1
  50. package/dist_ts/mod_meta/meta.interfaces.js +1 -1
  51. package/dist_ts/mod_meta/meta.plugins.js +1 -1
  52. package/dist_ts/mod_open/index.js +1 -1
  53. package/dist_ts/mod_open/mod.plugins.js +1 -1
  54. package/dist_ts/mod_release/index.js +1 -1
  55. package/dist_ts/mod_release/mod.plugins.js +1 -1
  56. package/dist_ts/mod_services/classes.dockercontainer.d.ts +50 -3
  57. package/dist_ts/mod_services/classes.dockercontainer.js +646 -84
  58. package/dist_ts/mod_services/classes.globalregistry.d.ts +10 -24
  59. package/dist_ts/mod_services/classes.globalregistry.js +210 -62
  60. package/dist_ts/mod_services/classes.globalregistrystore.d.ts +48 -0
  61. package/dist_ts/mod_services/classes.globalregistrystore.js +156 -0
  62. package/dist_ts/mod_services/classes.interprocesslock.d.ts +21 -0
  63. package/dist_ts/mod_services/classes.interprocesslock.js +141 -0
  64. package/dist_ts/mod_services/classes.serviceconfiguration.d.ts +15 -12
  65. package/dist_ts/mod_services/classes.serviceconfiguration.js +188 -91
  66. package/dist_ts/mod_services/classes.servicedatamarker.d.ts +32 -7
  67. package/dist_ts/mod_services/classes.servicedatamarker.js +145 -49
  68. package/dist_ts/mod_services/classes.servicemanager.d.ts +70 -28
  69. package/dist_ts/mod_services/classes.servicemanager.js +995 -421
  70. package/dist_ts/mod_services/classes.servicenames.d.ts +8 -0
  71. package/dist_ts/mod_services/classes.servicenames.js +54 -0
  72. package/dist_ts/mod_services/classes.serviceoptions.js +1 -1
  73. package/dist_ts/mod_services/classes.servicepruner.d.ts +3 -0
  74. package/dist_ts/mod_services/classes.servicepruner.js +208 -46
  75. package/dist_ts/mod_services/helpers.js +1 -1
  76. package/dist_ts/mod_services/index.d.ts +2 -0
  77. package/dist_ts/mod_services/index.js +81 -58
  78. package/dist_ts/mod_services/mod.plugins.js +1 -1
  79. package/dist_ts/mod_standard/index.js +2 -2
  80. package/dist_ts/mod_standard/mod.plugins.js +1 -1
  81. package/dist_ts/mod_start/index.js +1 -1
  82. package/dist_ts/mod_start/mod.plugins.js +1 -1
  83. package/dist_ts/mod_template/index.js +1 -1
  84. package/dist_ts/mod_template/mod.plugins.js +1 -1
  85. package/dist_ts/mod_tools/classes.packagemanager.js +1 -1
  86. package/dist_ts/mod_tools/index.js +1 -1
  87. package/dist_ts/mod_tools/mod.plugins.js +1 -1
  88. package/dist_ts/paths.js +1 -1
  89. package/dist_ts/plugins.d.ts +3 -1
  90. package/dist_ts/plugins.js +4 -2
  91. package/dist_ts_migration/001.service-selection.d.ts +9 -0
  92. package/dist_ts_migration/001.service-selection.js +51 -0
  93. package/dist_ts_migration/002.runtime-config.d.ts +19 -0
  94. package/dist_ts_migration/002.runtime-config.js +170 -0
  95. package/dist_ts_migration/003.service-data-marker.d.ts +9 -0
  96. package/dist_ts_migration/003.service-data-marker.js +156 -0
  97. package/dist_ts_migration/004.global-registry.d.ts +13 -0
  98. package/dist_ts_migration/004.global-registry.js +165 -0
  99. package/dist_ts_migration/index.d.ts +11 -0
  100. package/dist_ts_migration/index.js +59 -0
  101. package/package.json +6 -3
  102. package/readme.hints.md +102 -25
  103. package/readme.md +123 -28
  104. package/readme.plan.md +29 -16
  105. package/ts/00_commitinfo_data.ts +1 -1
  106. package/ts/helpers.smartconfig.ts +63 -10
  107. package/ts/mod_config/index.ts +1 -1
  108. package/ts/mod_docker/classes.dockerpruner.ts +6 -0
  109. package/ts/mod_services/classes.dockercontainer.ts +912 -96
  110. package/ts/mod_services/classes.globalregistry.ts +290 -89
  111. package/ts/mod_services/classes.globalregistrystore.ts +279 -0
  112. package/ts/mod_services/classes.interprocesslock.ts +189 -0
  113. package/ts/mod_services/classes.serviceconfiguration.ts +215 -108
  114. package/ts/mod_services/classes.servicedatamarker.ts +210 -52
  115. package/ts/mod_services/classes.servicemanager.ts +1332 -512
  116. package/ts/mod_services/classes.servicenames.ts +76 -0
  117. package/ts/mod_services/classes.servicepruner.ts +310 -52
  118. package/ts/mod_services/index.ts +89 -62
  119. package/ts/mod_standard/index.ts +1 -1
  120. package/ts/plugins.ts +4 -0
  121. package/ts_migration/001.service-selection.ts +77 -0
  122. package/ts_migration/002.runtime-config.ts +218 -0
  123. package/ts_migration/003.service-data-marker.ts +216 -0
  124. package/ts_migration/004.global-registry.ts +272 -0
  125. package/ts_migration/index.ts +83 -0
@@ -0,0 +1,218 @@
1
+ import * as plugins from '../ts/plugins.js';
2
+ import {
3
+ deriveS3BucketName,
4
+ isValidS3BucketName,
5
+ } from '../ts/mod_services/classes.serviceconfiguration.js';
6
+ import { withInterProcessLock } from '../ts/mod_services/classes.interprocesslock.js';
7
+
8
+ const isPlainObject = (valueArg: unknown): valueArg is Record<string, unknown> =>
9
+ typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
10
+
11
+ const stringFields = [
12
+ 'PROJECT_NAME',
13
+ 'MONGODB_HOST',
14
+ 'MONGODB_NAME',
15
+ 'MONGODB_PORT',
16
+ 'MONGODB_USER',
17
+ 'MONGODB_PASS',
18
+ 'MONGODB_URL',
19
+ 'S3_HOST',
20
+ 'S3_PORT',
21
+ 'S3_UI_PORT',
22
+ 'S3_CONSOLE_PORT',
23
+ 'S3_ACCESSKEY',
24
+ 'S3_SECRETKEY',
25
+ 'S3_ADMIN_PASSWORD',
26
+ 'S3_REGION',
27
+ 'S3_BUCKET',
28
+ 'S3_ENDPOINT',
29
+ 'ELASTICSEARCH_HOST',
30
+ 'ELASTICSEARCH_PORT',
31
+ 'ELASTICSEARCH_USER',
32
+ 'ELASTICSEARCH_PASS',
33
+ 'ELASTICSEARCH_URL',
34
+ ] as const;
35
+
36
+ const booleanFields = ['MONGODB_AUTH_ENABLED', 'S3_USESSL'] as const;
37
+ const portFields = [
38
+ 'MONGODB_PORT',
39
+ 'S3_PORT',
40
+ 'S3_UI_PORT',
41
+ 'S3_CONSOLE_PORT',
42
+ 'ELASTICSEARCH_PORT',
43
+ ] as const;
44
+
45
+ const assertValidPort = (nameArg: string, valueArg: unknown): void => {
46
+ if (typeof valueArg !== 'string' || !/^\d{1,5}$/u.test(valueArg)) {
47
+ throw new Error(`Cannot migrate .nogit/env.json: ${nameArg} must be a decimal port string.`);
48
+ }
49
+ const port = Number(valueArg);
50
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
51
+ throw new Error(`Cannot migrate .nogit/env.json: ${nameArg} is outside the TCP port range.`);
52
+ }
53
+ };
54
+
55
+ /** Validate every known persisted field before a migration is allowed to write. */
56
+ export const assertServiceRuntimeConfigMigrationInput = (
57
+ configArg: unknown,
58
+ ): Record<string, unknown> => {
59
+ if (!isPlainObject(configArg)) {
60
+ throw new Error('Cannot migrate .nogit/env.json: root must be a JSON object.');
61
+ }
62
+ for (const field of stringFields) {
63
+ if (
64
+ Object.hasOwn(configArg, field) &&
65
+ (typeof configArg[field] !== 'string' || !configArg[field].trim())
66
+ ) {
67
+ throw new Error(
68
+ `Cannot migrate .nogit/env.json: ${field} must be a non-empty string.`,
69
+ );
70
+ }
71
+ }
72
+ for (const field of booleanFields) {
73
+ if (Object.hasOwn(configArg, field) && typeof configArg[field] !== 'boolean') {
74
+ throw new Error(`Cannot migrate .nogit/env.json: ${field} must be a boolean.`);
75
+ }
76
+ }
77
+ for (const field of portFields) {
78
+ if (Object.hasOwn(configArg, field)) {
79
+ assertValidPort(field, configArg[field]);
80
+ }
81
+ }
82
+ const s3Port = configArg.S3_PORT;
83
+ const uiPort = configArg.S3_UI_PORT ?? configArg.S3_CONSOLE_PORT;
84
+ if (s3Port !== undefined && uiPort !== undefined && s3Port === uiPort) {
85
+ throw new Error('Cannot migrate .nogit/env.json: S3 API and UI ports must differ.');
86
+ }
87
+ return configArg;
88
+ };
89
+
90
+ export type TLegacyS3BucketRepair =
91
+ | { bucket: unknown; repaired: false }
92
+ | { bucket: string; repaired: true };
93
+
94
+ /** Repair only the exact invalid bucket value generated by older GitZone releases. */
95
+ export const repairLegacyGeneratedS3Bucket = (
96
+ projectNameArg: string,
97
+ bucketArg: unknown,
98
+ ): TLegacyS3BucketRepair => {
99
+ const legacyGeneratedBucket = `${projectNameArg}-documents`;
100
+ if (bucketArg !== legacyGeneratedBucket || isValidS3BucketName(bucketArg)) {
101
+ return { bucket: bucketArg, repaired: false };
102
+ }
103
+ return { bucket: deriveS3BucketName(projectNameArg), repaired: true };
104
+ };
105
+
106
+ export interface IServiceRuntimeConfigMigrationResult {
107
+ config: Record<string, unknown>;
108
+ migrated: boolean;
109
+ }
110
+
111
+ /** Convert only shipped ObjectStorage predecessor fields and generated values. */
112
+ export const migrateLegacyServiceRuntimeConfigValue = (
113
+ configArg: unknown,
114
+ ): IServiceRuntimeConfigMigrationResult => {
115
+ const source = assertServiceRuntimeConfigMigrationInput(configArg);
116
+ const config = { ...source };
117
+ let migrated = false;
118
+
119
+ if (Object.hasOwn(config, 'S3_CONSOLE_PORT')) {
120
+ const legacyPort = config.S3_CONSOLE_PORT as string;
121
+ if (Object.hasOwn(config, 'S3_UI_PORT') && config.S3_UI_PORT !== legacyPort) {
122
+ throw new Error(
123
+ 'Cannot migrate .nogit/env.json: S3_CONSOLE_PORT and S3_UI_PORT contain different values. ' +
124
+ 'Choose the intended management UI port explicitly, keep it as S3_UI_PORT, and remove S3_CONSOLE_PORT.',
125
+ );
126
+ }
127
+ config.S3_UI_PORT = legacyPort;
128
+ delete config.S3_CONSOLE_PORT;
129
+ migrated = true;
130
+ }
131
+
132
+ if (!Object.hasOwn(config, 'S3_ADMIN_PASSWORD')) {
133
+ config.S3_ADMIN_PASSWORD = plugins.crypto.randomBytes(24).toString('base64url');
134
+ migrated = true;
135
+ }
136
+ if (!Object.hasOwn(config, 'S3_REGION')) {
137
+ config.S3_REGION = 'us-east-1';
138
+ migrated = true;
139
+ }
140
+
141
+ if (typeof config.PROJECT_NAME === 'string' && typeof config.S3_BUCKET === 'string') {
142
+ const repair = repairLegacyGeneratedS3Bucket(config.PROJECT_NAME, config.S3_BUCKET);
143
+ if (repair.repaired) {
144
+ config.S3_BUCKET = repair.bucket;
145
+ migrated = true;
146
+ }
147
+ }
148
+
149
+ assertServiceRuntimeConfigMigrationInput(config);
150
+ return { config, migrated };
151
+ };
152
+
153
+ const readRuntimeConfig = async (projectPathArg: string): Promise<unknown | undefined> => {
154
+ const configPath = plugins.path.join(projectPathArg, '.nogit', 'env.json');
155
+ if (!(await plugins.smartfs.file(configPath).exists())) {
156
+ return undefined;
157
+ }
158
+ const content = (await plugins.smartfs.file(configPath).encoding('utf8').read()) as string;
159
+ try {
160
+ return JSON.parse(content);
161
+ } catch {
162
+ throw new Error('Cannot migrate .nogit/env.json: file contains invalid JSON.');
163
+ }
164
+ };
165
+
166
+ const writeRuntimeConfigAtomic = async (
167
+ projectPathArg: string,
168
+ configArg: Record<string, unknown>,
169
+ ): Promise<void> => {
170
+ const configPath = plugins.path.join(projectPathArg, '.nogit', 'env.json');
171
+ const tempPath = `${configPath}.tmp-${process.pid}-${plugins.crypto
172
+ .randomBytes(12)
173
+ .toString('hex')}`;
174
+ try {
175
+ await plugins.fs.writeFile(tempPath, `${JSON.stringify(configArg, null, 2)}\n`, {
176
+ encoding: 'utf8',
177
+ flag: 'wx',
178
+ mode: 0o600,
179
+ });
180
+ await plugins.fs.rename(tempPath, configPath);
181
+ } catch (error) {
182
+ await plugins.fs.rm(tempPath, { force: true }).catch(() => {});
183
+ throw new Error('Cannot migrate .nogit/env.json atomically.', { cause: error });
184
+ }
185
+ };
186
+
187
+ export const preflightServiceRuntimeConfigMigration = async (
188
+ projectPathArg: string,
189
+ ): Promise<void> => {
190
+ const parsed = await readRuntimeConfig(projectPathArg);
191
+ if (parsed !== undefined) {
192
+ migrateLegacyServiceRuntimeConfigValue(parsed);
193
+ }
194
+ };
195
+
196
+ export const runServiceRuntimeConfigMigration = async (
197
+ projectPathArg: string,
198
+ ): Promise<boolean> => {
199
+ await preflightServiceRuntimeConfigMigration(projectPathArg);
200
+ const configPath = plugins.path.join(projectPathArg, '.nogit', 'env.json');
201
+ return withInterProcessLock(
202
+ {
203
+ lockPath: `${configPath}.lock`,
204
+ description: 'Service runtime configuration',
205
+ },
206
+ async () => {
207
+ const parsed = await readRuntimeConfig(projectPathArg);
208
+ if (parsed === undefined) {
209
+ return false;
210
+ }
211
+ const migration = migrateLegacyServiceRuntimeConfigValue(parsed);
212
+ if (migration.migrated) {
213
+ await writeRuntimeConfigAtomic(projectPathArg, migration.config);
214
+ }
215
+ return migration.migrated;
216
+ },
217
+ );
218
+ };
@@ -0,0 +1,216 @@
1
+ import * as plugins from '../ts/plugins.js';
2
+ import {
3
+ getLegacyMinioDataDirectory,
4
+ getServiceDataDirectory,
5
+ getServiceDataMarkerLockPath,
6
+ getServiceDataMarkerPath,
7
+ isValidServiceDataMarker,
8
+ serviceDataMarkerSchemaVersion,
9
+ serviceDataOwner,
10
+ type IServiceDataMarker,
11
+ type IServiceDataMarkerEntry,
12
+ type TLegacyServiceName,
13
+ type TServiceName,
14
+ } from '../ts/mod_services/classes.servicedatamarker.js';
15
+ import { withInterProcessLock } from '../ts/mod_services/classes.interprocesslock.js';
16
+
17
+ interface IServiceDataMarkerV1 {
18
+ owner: string;
19
+ kind: 'service-data';
20
+ schemaVersion: 1;
21
+ projectPath: string;
22
+ safeToPrune: true;
23
+ createdAt: string;
24
+ dataDirectories: Array<{
25
+ service: TServiceName | TLegacyServiceName;
26
+ path: string;
27
+ }>;
28
+ }
29
+
30
+ const isPlainObject = (valueArg: unknown): valueArg is Record<string, unknown> =>
31
+ typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
32
+
33
+ const hasOnlyKeys = (
34
+ valueArg: Record<string, unknown>,
35
+ allowedKeysArg: readonly string[],
36
+ ): boolean =>
37
+ Object.keys(valueArg).every((keyArg) => allowedKeysArg.includes(keyArg));
38
+
39
+ const isValidServiceDataMarkerV1 = (markerArg: unknown): markerArg is IServiceDataMarkerV1 => {
40
+ if (
41
+ !isPlainObject(markerArg) ||
42
+ !hasOnlyKeys(markerArg, [
43
+ 'owner',
44
+ 'kind',
45
+ 'schemaVersion',
46
+ 'projectPath',
47
+ 'safeToPrune',
48
+ 'createdAt',
49
+ 'dataDirectories',
50
+ ]) ||
51
+ markerArg.owner !== serviceDataOwner ||
52
+ markerArg.kind !== 'service-data' ||
53
+ markerArg.schemaVersion !== 1 ||
54
+ markerArg.safeToPrune !== true ||
55
+ typeof markerArg.projectPath !== 'string' ||
56
+ !markerArg.projectPath ||
57
+ typeof markerArg.createdAt !== 'string' ||
58
+ !markerArg.createdAt ||
59
+ !Array.isArray(markerArg.dataDirectories)
60
+ ) {
61
+ return false;
62
+ }
63
+ const projectPath = plugins.path.resolve(markerArg.projectPath);
64
+ const seenServices = new Set<string>();
65
+ for (const entry of markerArg.dataDirectories) {
66
+ const service = isPlainObject(entry) ? entry.service : undefined;
67
+ if (
68
+ !isPlainObject(entry) ||
69
+ !hasOnlyKeys(entry, ['service', 'path']) ||
70
+ typeof service !== 'string' ||
71
+ !['mongodb', 'minio', 'elasticsearch'].includes(service) ||
72
+ seenServices.has(service) ||
73
+ typeof entry.path !== 'string'
74
+ ) {
75
+ return false;
76
+ }
77
+ const expectedPath =
78
+ service === 'minio'
79
+ ? getLegacyMinioDataDirectory(projectPath)
80
+ : getServiceDataDirectory(projectPath, service as TServiceName);
81
+ if (plugins.path.resolve(entry.path) !== expectedPath) {
82
+ return false;
83
+ }
84
+ seenServices.add(service);
85
+ }
86
+ return true;
87
+ };
88
+
89
+ export interface IServiceDataMarkerMigrationResult {
90
+ marker?: IServiceDataMarker;
91
+ migrated: boolean;
92
+ }
93
+
94
+ /** Convert the exact shipped v1 marker without reinterpreting legacy storage data. */
95
+ export const migrateServiceDataMarkerValue = (
96
+ markerArg: unknown,
97
+ projectPathArg: string,
98
+ ): IServiceDataMarkerMigrationResult => {
99
+ const projectPath = plugins.path.resolve(projectPathArg);
100
+ if (isValidServiceDataMarker(markerArg)) {
101
+ return {
102
+ marker:
103
+ plugins.path.resolve(markerArg.projectPath) === projectPath ? markerArg : undefined,
104
+ migrated: false,
105
+ };
106
+ }
107
+ if (
108
+ !isValidServiceDataMarkerV1(markerArg) ||
109
+ plugins.path.resolve(markerArg.projectPath) !== projectPath
110
+ ) {
111
+ return { migrated: false };
112
+ }
113
+
114
+ const dataDirectories: IServiceDataMarkerEntry[] = markerArg.dataDirectories
115
+ .filter((entry) => entry.service !== 'minio')
116
+ .map((entry) => ({
117
+ service: entry.service as TServiceName,
118
+ path: plugins.path.resolve(entry.path),
119
+ }))
120
+ .sort((entryA, entryB) => entryA.service.localeCompare(entryB.service));
121
+ const legacyEntry = markerArg.dataDirectories.find((entry) => entry.service === 'minio');
122
+ const marker: IServiceDataMarker = {
123
+ owner: serviceDataOwner,
124
+ kind: 'service-data',
125
+ schemaVersion: serviceDataMarkerSchemaVersion,
126
+ projectPath,
127
+ safeToPrune: true,
128
+ createdAt: markerArg.createdAt,
129
+ dataDirectories,
130
+ ...(legacyEntry
131
+ ? {
132
+ legacyDataDirectories: [
133
+ {
134
+ service: 'minio' as const,
135
+ path: plugins.path.resolve(legacyEntry.path),
136
+ safeToPrune: false as const,
137
+ },
138
+ ],
139
+ }
140
+ : {}),
141
+ };
142
+ return { marker, migrated: true };
143
+ };
144
+
145
+ const readServiceDataMarkerMigrationInput = async (
146
+ projectPathArg: string,
147
+ ): Promise<unknown | undefined> => {
148
+ const markerPath = getServiceDataMarkerPath(projectPathArg);
149
+ if (!(await plugins.smartfs.file(markerPath).exists())) {
150
+ return undefined;
151
+ }
152
+ let parsed: unknown;
153
+ try {
154
+ const content = (await plugins.smartfs.file(markerPath).encoding('utf8').read()) as string;
155
+ parsed = JSON.parse(content);
156
+ } catch {
157
+ throw new Error(`Cannot migrate invalid service data marker: ${markerPath}`);
158
+ }
159
+ return parsed;
160
+ };
161
+
162
+ export const preflightServiceDataMarkerMigration = async (
163
+ projectPathArg: string,
164
+ ): Promise<IServiceDataMarker | undefined> => {
165
+ const markerPath = getServiceDataMarkerPath(projectPathArg);
166
+ const parsed = await readServiceDataMarkerMigrationInput(projectPathArg);
167
+ if (parsed === undefined) {
168
+ return undefined;
169
+ }
170
+ const result = migrateServiceDataMarkerValue(parsed, projectPathArg);
171
+ if (!result.marker) {
172
+ throw new Error(`Cannot migrate invalid or foreign service data marker: ${markerPath}`);
173
+ }
174
+ return result.marker;
175
+ };
176
+
177
+ export const runServiceDataMarkerMigration = async (
178
+ projectPathArg: string,
179
+ ): Promise<IServiceDataMarker | undefined> => {
180
+ await preflightServiceDataMarkerMigration(projectPathArg);
181
+ const markerPath = getServiceDataMarkerPath(projectPathArg);
182
+ return withInterProcessLock(
183
+ {
184
+ lockPath: getServiceDataMarkerLockPath(projectPathArg),
185
+ description: 'Service data marker',
186
+ },
187
+ async () => {
188
+ const parsed = await readServiceDataMarkerMigrationInput(projectPathArg);
189
+ if (parsed === undefined) {
190
+ return undefined;
191
+ }
192
+ const result = migrateServiceDataMarkerValue(parsed, projectPathArg);
193
+ if (!result.marker) {
194
+ throw new Error(`Cannot migrate invalid or foreign service data marker: ${markerPath}`);
195
+ }
196
+ if (result.migrated) {
197
+ const tempPath = `${markerPath}.tmp-${process.pid}-${plugins.crypto
198
+ .randomBytes(12)
199
+ .toString('hex')}`;
200
+ try {
201
+ await plugins.fs.writeFile(tempPath, `${JSON.stringify(result.marker, null, 2)}\n`, {
202
+ encoding: 'utf8',
203
+ flag: 'wx',
204
+ });
205
+ await plugins.fs.rename(tempPath, markerPath);
206
+ } catch (error) {
207
+ await plugins.fs.rm(tempPath, { force: true }).catch(() => {});
208
+ throw new Error(`Cannot migrate service data marker atomically: ${markerPath}`, {
209
+ cause: error,
210
+ });
211
+ }
212
+ }
213
+ return result.marker;
214
+ },
215
+ );
216
+ };
@@ -0,0 +1,272 @@
1
+ import * as plugins from '../ts/plugins.js';
2
+ import { migratePersistedServiceNamesValue } from './001.service-selection.js';
3
+ import {
4
+ GlobalRegistryStore,
5
+ type IGlobalRegistryTransactionResult,
6
+ type IGlobalRegistryData,
7
+ type IRegisteredProject,
8
+ } from '../ts/mod_services/classes.globalregistrystore.js';
9
+
10
+ export interface IGlobalRegistryMigrationResult {
11
+ data: IGlobalRegistryData;
12
+ migrated: boolean;
13
+ }
14
+
15
+ export interface IGlobalRegistryMigrationStore {
16
+ readRaw(): Promise<unknown>;
17
+ transact<T>(
18
+ operationArg: (
19
+ rawDataArg: unknown,
20
+ ) => Promise<IGlobalRegistryTransactionResult<T>> | IGlobalRegistryTransactionResult<T>,
21
+ ): Promise<T>;
22
+ }
23
+
24
+ const isPlainObject = (valueArg: unknown): valueArg is Record<string, unknown> =>
25
+ typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
26
+
27
+ const assertAllowedKeys = (
28
+ valueArg: Record<string, unknown>,
29
+ allowedKeysArg: readonly string[],
30
+ contextArg: string,
31
+ ): void => {
32
+ if (Object.keys(valueArg).some((keyArg) => !allowedKeysArg.includes(keyArg))) {
33
+ throw new Error(
34
+ `Cannot migrate global services registry: ${contextArg} contains unsupported fields.`,
35
+ );
36
+ }
37
+ };
38
+
39
+ const assertOptionalStrings = (
40
+ valueArg: Record<string, unknown>,
41
+ keysArg: readonly string[],
42
+ contextArg: string,
43
+ ): void => {
44
+ for (const key of keysArg) {
45
+ if (valueArg[key] !== undefined && (typeof valueArg[key] !== 'string' || !valueArg[key])) {
46
+ throw new Error(
47
+ `Cannot migrate global services registry: ${contextArg}.${key} must be a non-empty string.`,
48
+ );
49
+ }
50
+ }
51
+ };
52
+
53
+ const assertOptionalNumbers = (
54
+ valueArg: Record<string, unknown>,
55
+ keysArg: readonly string[],
56
+ contextArg: string,
57
+ ): void => {
58
+ for (const key of keysArg) {
59
+ if (valueArg[key] !== undefined && !Number.isFinite(valueArg[key])) {
60
+ throw new Error(
61
+ `Cannot migrate global services registry: ${contextArg}.${key} must be a finite number.`,
62
+ );
63
+ }
64
+ }
65
+ };
66
+
67
+ /** Convert the exact legacy registry fields without activating old provider resources. */
68
+ export const migrateLegacyGlobalRegistryData = (
69
+ dataArg: unknown,
70
+ ): IGlobalRegistryMigrationResult => {
71
+ if (!isPlainObject(dataArg)) {
72
+ throw new Error('Cannot migrate global services registry: root must be a JSON object.');
73
+ }
74
+ const source = dataArg;
75
+ assertAllowedKeys(source, ['schemaVersion', 'projects'], 'root');
76
+ if (source.schemaVersion !== undefined && source.schemaVersion !== 2) {
77
+ throw new Error('Cannot migrate global services registry: unsupported schema version.');
78
+ }
79
+ if (source.projects === undefined) {
80
+ if (source.schemaVersion === 2) {
81
+ throw new Error('Cannot migrate global services registry: projects map is missing.');
82
+ }
83
+ return {
84
+ data: { schemaVersion: 2, projects: {} },
85
+ migrated: true,
86
+ };
87
+ }
88
+ if (!isPlainObject(source.projects)) {
89
+ throw new Error('Cannot migrate global services registry: projects must be a JSON object.');
90
+ }
91
+ let migrated = source.schemaVersion !== 2;
92
+ const projects: Record<string, IRegisteredProject> = {};
93
+
94
+ for (const [projectPath, rawProject] of Object.entries(source.projects)) {
95
+ if (!isPlainObject(rawProject)) {
96
+ throw new Error(
97
+ `Cannot migrate global services registry for ${projectPath}: project must be a JSON object.`,
98
+ );
99
+ }
100
+ assertAllowedKeys(
101
+ rawProject,
102
+ [
103
+ 'projectPath',
104
+ 'projectName',
105
+ 'containers',
106
+ 'ports',
107
+ 'legacy',
108
+ 'enabledServices',
109
+ 'lastActive',
110
+ ],
111
+ `project ${projectPath}`,
112
+ );
113
+ if (
114
+ typeof rawProject.projectPath !== 'string' ||
115
+ !rawProject.projectPath ||
116
+ plugins.path.resolve(rawProject.projectPath) !== plugins.path.resolve(projectPath)
117
+ ) {
118
+ throw new Error(
119
+ `Cannot migrate global services registry for ${projectPath}: project path is invalid or inconsistent.`,
120
+ );
121
+ }
122
+ if (typeof rawProject.projectName !== 'string' || !rawProject.projectName) {
123
+ throw new Error(
124
+ `Cannot migrate global services registry for ${projectPath}: project name is invalid.`,
125
+ );
126
+ }
127
+ if (!isPlainObject(rawProject.containers) || !isPlainObject(rawProject.ports)) {
128
+ throw new Error(
129
+ `Cannot migrate global services registry for ${projectPath}: containers and ports must be JSON objects.`,
130
+ );
131
+ }
132
+ if (!Array.isArray(rawProject.enabledServices)) {
133
+ throw new Error(
134
+ `Cannot migrate global services registry for ${projectPath}: enabledServices must be an array.`,
135
+ );
136
+ }
137
+ if (
138
+ typeof rawProject.lastActive !== 'number' ||
139
+ !Number.isFinite(rawProject.lastActive)
140
+ ) {
141
+ throw new Error(
142
+ `Cannot migrate global services registry for ${projectPath}: lastActive must be a finite number.`,
143
+ );
144
+ }
145
+ const rawContainers = rawProject.containers;
146
+ const rawPorts = rawProject.ports;
147
+ assertAllowedKeys(
148
+ rawContainers,
149
+ ['mongo', 'minio', 'objectstorage', 'elasticsearch'],
150
+ `project ${projectPath} containers`,
151
+ );
152
+ assertOptionalStrings(
153
+ rawContainers,
154
+ ['mongo', 'minio', 'objectstorage', 'elasticsearch'],
155
+ `project ${projectPath}.containers`,
156
+ );
157
+ assertAllowedKeys(
158
+ rawPorts,
159
+ ['mongo', 's3', 's3Console', 's3Ui', 'elasticsearch'],
160
+ `project ${projectPath} ports`,
161
+ );
162
+ assertOptionalNumbers(
163
+ rawPorts,
164
+ ['mongo', 's3', 's3Console', 's3Ui', 'elasticsearch'],
165
+ `project ${projectPath}.ports`,
166
+ );
167
+ let rawLegacy: Record<string, unknown> = {};
168
+ if (rawProject.legacy !== undefined) {
169
+ if (!isPlainObject(rawProject.legacy)) {
170
+ throw new Error(
171
+ `Cannot migrate global services registry for ${projectPath}: legacy must be a JSON object.`,
172
+ );
173
+ }
174
+ rawLegacy = rawProject.legacy;
175
+ assertAllowedKeys(rawLegacy, ['minioContainer'], `project ${projectPath} legacy`);
176
+ assertOptionalStrings(
177
+ rawLegacy,
178
+ ['minioContainer'],
179
+ `project ${projectPath}.legacy`,
180
+ );
181
+ if (!rawLegacy.minioContainer) {
182
+ throw new Error(
183
+ `Cannot migrate global services registry for ${projectPath}: empty legacy evidence is invalid.`,
184
+ );
185
+ }
186
+ }
187
+ if (
188
+ rawLegacy.minioContainer !== undefined &&
189
+ rawContainers.minio !== undefined &&
190
+ rawLegacy.minioContainer !== rawContainers.minio
191
+ ) {
192
+ throw new Error(
193
+ `Cannot migrate global services registry for ${projectPath}: legacy MinIO references differ.`,
194
+ );
195
+ }
196
+ const minioContainer =
197
+ typeof rawLegacy.minioContainer === 'string'
198
+ ? rawLegacy.minioContainer
199
+ : typeof rawContainers.minio === 'string'
200
+ ? rawContainers.minio
201
+ : undefined;
202
+ if (typeof rawContainers.minio === 'string') {
203
+ migrated = true;
204
+ }
205
+ if (
206
+ rawPorts.s3Ui !== undefined &&
207
+ rawPorts.s3Console !== undefined &&
208
+ rawPorts.s3Ui !== rawPorts.s3Console
209
+ ) {
210
+ throw new Error(
211
+ `Cannot migrate global services registry for ${projectPath}: s3Ui and s3Console differ.`,
212
+ );
213
+ }
214
+ if (rawPorts.s3Console !== undefined) {
215
+ migrated = true;
216
+ }
217
+ const s3UiPort = rawPorts.s3Ui ?? rawPorts.s3Console;
218
+ const serviceMigration = migratePersistedServiceNamesValue(rawProject.enabledServices);
219
+ migrated ||= serviceMigration.migrated;
220
+
221
+ projects[projectPath] = {
222
+ projectPath: rawProject.projectPath,
223
+ projectName: rawProject.projectName,
224
+ containers: {
225
+ ...(typeof rawContainers.mongo === 'string' ? { mongo: rawContainers.mongo } : {}),
226
+ ...(typeof rawContainers.objectstorage === 'string'
227
+ ? { objectstorage: rawContainers.objectstorage }
228
+ : {}),
229
+ ...(typeof rawContainers.elasticsearch === 'string'
230
+ ? { elasticsearch: rawContainers.elasticsearch }
231
+ : {}),
232
+ },
233
+ ports: {
234
+ ...(typeof rawPorts.mongo === 'number' ? { mongo: rawPorts.mongo } : {}),
235
+ ...(typeof rawPorts.s3 === 'number' ? { s3: rawPorts.s3 } : {}),
236
+ ...(typeof s3UiPort === 'number' ? { s3Ui: s3UiPort } : {}),
237
+ ...(typeof rawPorts.elasticsearch === 'number'
238
+ ? { elasticsearch: rawPorts.elasticsearch }
239
+ : {}),
240
+ },
241
+ ...(minioContainer ? { legacy: { minioContainer } } : {}),
242
+ enabledServices: serviceMigration.services,
243
+ lastActive: rawProject.lastActive,
244
+ };
245
+ }
246
+
247
+ return {
248
+ data: { schemaVersion: 2, projects },
249
+ migrated,
250
+ };
251
+ };
252
+
253
+ const createDefaultStore = (): IGlobalRegistryMigrationStore => new GlobalRegistryStore();
254
+
255
+ export const preflightGlobalRegistryMigration = async (
256
+ storeArg: IGlobalRegistryMigrationStore = createDefaultStore(),
257
+ ): Promise<IGlobalRegistryData> => {
258
+ return migrateLegacyGlobalRegistryData(await storeArg.readRaw()).data;
259
+ };
260
+
261
+ export const runGlobalRegistryMigration = async (
262
+ storeArg: IGlobalRegistryMigrationStore = createDefaultStore(),
263
+ ): Promise<IGlobalRegistryData> => {
264
+ await preflightGlobalRegistryMigration(storeArg);
265
+ return storeArg.transact((rawData) => {
266
+ const migration = migrateLegacyGlobalRegistryData(rawData);
267
+ return {
268
+ result: migration.data,
269
+ ...(migration.migrated ? { writeData: migration.data } : {}),
270
+ };
271
+ });
272
+ };