imcp 0.1.1 → 0.1.3

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 (111) hide show
  1. package/dist/cli/index.js +1 -45
  2. package/dist/core/installers/clients/BaseClientInstaller.d.ts +1 -5
  3. package/dist/core/installers/clients/BaseClientInstaller.js +40 -38
  4. package/dist/core/installers/clients/ClientInstaller.d.ts +9 -9
  5. package/dist/core/installers/clients/ClientInstaller.js +105 -99
  6. package/dist/core/installers/requirements/BaseInstaller.d.ts +9 -1
  7. package/dist/core/installers/requirements/CommandInstaller.d.ts +9 -1
  8. package/dist/core/installers/requirements/CommandInstaller.js +46 -12
  9. package/dist/core/installers/requirements/GeneralInstaller.d.ts +11 -1
  10. package/dist/core/installers/requirements/GeneralInstaller.js +46 -10
  11. package/dist/core/installers/requirements/InstallerFactory.d.ts +3 -1
  12. package/dist/core/installers/requirements/InstallerFactory.js +3 -2
  13. package/dist/core/installers/requirements/NpmInstaller.d.ts +4 -2
  14. package/dist/core/installers/requirements/NpmInstaller.js +38 -22
  15. package/dist/core/installers/requirements/PipInstaller.d.ts +3 -1
  16. package/dist/core/installers/requirements/PipInstaller.js +58 -36
  17. package/dist/core/installers/requirements/RequirementInstaller.d.ts +4 -1
  18. package/dist/core/loaders/InstallOperationManager.d.ts +115 -0
  19. package/dist/core/loaders/InstallOperationManager.js +311 -0
  20. package/dist/core/loaders/SystemSettingsManager.d.ts +54 -0
  21. package/dist/core/loaders/SystemSettingsManager.js +257 -0
  22. package/dist/core/metadatas/recordingConstants.d.ts +44 -0
  23. package/dist/core/metadatas/recordingConstants.js +45 -0
  24. package/dist/core/metadatas/types.d.ts +21 -0
  25. package/dist/core/onboard/InstallOperationManager.d.ts +23 -0
  26. package/dist/core/onboard/InstallOperationManager.js +144 -0
  27. package/dist/core/onboard/OnboardStatusManager.js +2 -1
  28. package/dist/core/validators/StdioServerValidator.js +4 -3
  29. package/dist/services/InstallationService.d.ts +2 -37
  30. package/dist/services/InstallationService.js +45 -313
  31. package/dist/services/MCPManager.d.ts +1 -1
  32. package/dist/services/MCPManager.js +4 -58
  33. package/dist/services/RequirementService.d.ts +85 -12
  34. package/dist/services/RequirementService.js +488 -49
  35. package/dist/services/ServerService.d.ts +0 -6
  36. package/dist/services/ServerService.js +0 -74
  37. package/dist/utils/adoUtils.js +6 -3
  38. package/dist/utils/logger.js +1 -1
  39. package/dist/utils/macroExpressionUtils.js +3 -25
  40. package/dist/utils/osUtils.d.ts +22 -1
  41. package/dist/utils/osUtils.js +92 -1
  42. package/dist/utils/versionUtils.d.ts +20 -1
  43. package/dist/utils/versionUtils.js +51 -4
  44. package/dist/web/public/css/modal.css +292 -1
  45. package/dist/web/public/css/serverDetails.css +14 -1
  46. package/dist/web/public/index.html +122 -20
  47. package/dist/web/public/js/flights/flights.js +1 -0
  48. package/dist/web/public/js/modal/index.js +8 -14
  49. package/dist/web/public/js/modal/installModal.js +3 -4
  50. package/dist/web/public/js/modal/installation.js +122 -137
  51. package/dist/web/public/js/modal/loadingModal.js +155 -25
  52. package/dist/web/public/js/modal/messageQueue.js +45 -101
  53. package/dist/web/public/js/modal/modalSetup.js +125 -43
  54. package/dist/web/public/js/modal/modalUtils.js +0 -12
  55. package/dist/web/public/js/modal.js +23 -10
  56. package/dist/web/public/js/onboard/publishHandler.js +22 -20
  57. package/dist/web/public/js/serverCategoryDetails.js +60 -11
  58. package/dist/web/public/js/serverCategoryList.js +2 -2
  59. package/dist/web/public/js/settings.js +314 -0
  60. package/dist/web/public/settings.html +135 -0
  61. package/dist/web/public/styles.css +32 -0
  62. package/dist/web/server.js +82 -0
  63. package/memory-bank/activeContext.md +13 -1
  64. package/memory-bank/decisionLog.md +63 -0
  65. package/memory-bank/progress.md +30 -0
  66. package/memory-bank/systemPatterns.md +7 -0
  67. package/package.json +1 -1
  68. package/src/cli/index.ts +1 -48
  69. package/src/core/installers/clients/BaseClientInstaller.ts +64 -50
  70. package/src/core/installers/clients/ClientInstaller.ts +130 -130
  71. package/src/core/installers/requirements/BaseInstaller.ts +9 -1
  72. package/src/core/installers/requirements/CommandInstaller.ts +47 -13
  73. package/src/core/installers/requirements/GeneralInstaller.ts +48 -10
  74. package/src/core/installers/requirements/InstallerFactory.ts +4 -3
  75. package/src/core/installers/requirements/NpmInstaller.ts +90 -68
  76. package/src/core/installers/requirements/PipInstaller.ts +81 -55
  77. package/src/core/installers/requirements/RequirementInstaller.ts +4 -3
  78. package/src/core/loaders/InstallOperationManager.ts +367 -0
  79. package/src/core/loaders/SystemSettingsManager.ts +278 -0
  80. package/src/core/metadatas/recordingConstants.ts +62 -0
  81. package/src/core/metadatas/types.ts +23 -0
  82. package/src/core/onboard/OnboardStatusManager.ts +2 -1
  83. package/src/core/validators/StdioServerValidator.ts +4 -3
  84. package/src/services/InstallationService.ts +54 -399
  85. package/src/services/MCPManager.ts +4 -77
  86. package/src/services/RequirementService.ts +564 -67
  87. package/src/services/ServerService.ts +0 -90
  88. package/src/utils/adoUtils.ts +6 -4
  89. package/src/utils/logger.ts +1 -1
  90. package/src/utils/macroExpressionUtils.ts +4 -21
  91. package/src/utils/osUtils.ts +92 -1
  92. package/src/utils/versionUtils.ts +71 -19
  93. package/src/web/public/css/modal.css +292 -1
  94. package/src/web/public/css/serverDetails.css +14 -1
  95. package/src/web/public/index.html +122 -20
  96. package/src/web/public/js/flights/flights.js +1 -1
  97. package/src/web/public/js/modal/index.js +8 -14
  98. package/src/web/public/js/modal/installModal.js +3 -4
  99. package/src/web/public/js/modal/installation.js +122 -137
  100. package/src/web/public/js/modal/loadingModal.js +155 -25
  101. package/src/web/public/js/modal/modalSetup.js +125 -43
  102. package/src/web/public/js/modal/modalUtils.js +0 -12
  103. package/src/web/public/js/modal.js +23 -10
  104. package/src/web/public/js/onboard/publishHandler.js +22 -20
  105. package/src/web/public/js/serverCategoryDetails.js +60 -11
  106. package/src/web/public/js/serverCategoryList.js +5 -5
  107. package/src/web/public/js/settings.js +314 -0
  108. package/src/web/public/settings.html +135 -0
  109. package/src/web/public/styles.css +32 -0
  110. package/src/web/server.ts +85 -0
  111. package/src/web/public/js/modal/messageQueue.js +0 -112
@@ -0,0 +1,311 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { SETTINGS_DIR } from '../metadatas/constants.js';
4
+ import { Logger } from '../../utils/logger.js';
5
+ const INSTALL_STATUS_DIR = path.join(SETTINGS_DIR, 'InstallOperationStatus');
6
+ export class InstallOperationManager {
7
+ installOperationStatus = {};
8
+ statusLock = Promise.resolve();
9
+ categoryName;
10
+ serverName;
11
+ statusFilePath;
12
+ /**
13
+ * Creates an InstallOperationManager instance for a specific category and server.
14
+ * @param categoryName The name of the category.
15
+ * @param serverName The name of the server.
16
+ */
17
+ constructor(categoryName, serverName) {
18
+ this.categoryName = categoryName;
19
+ this.serverName = serverName;
20
+ this.statusFilePath = path.join(INSTALL_STATUS_DIR, this.categoryName, `${this.serverName}.json`);
21
+ this.loadStatuses().catch(error => Logger.error('Failed to initialize InstallOperationManager:', error));
22
+ }
23
+ /**
24
+ * Returns an InstallOperationManager instance for the given category and server.
25
+ * @param categoryName The name of the category.
26
+ * @param serverName The name of the server.
27
+ */
28
+ static instanceMap = new Map();
29
+ /**
30
+ * Returns a cached InstallOperationManager instance for the given category and server.
31
+ * If an instance does not exist, it will be created and cached.
32
+ * @param categoryName The name of the category.
33
+ * @param serverName The name of the server.
34
+ */
35
+ static getInstance(categoryName, serverName) {
36
+ const key = `${categoryName}::${serverName}`;
37
+ if (!this.instanceMap.has(key)) {
38
+ this.instanceMap.set(key, new InstallOperationManager(categoryName, serverName));
39
+ }
40
+ return this.instanceMap.get(key);
41
+ }
42
+ async withLock(operation) {
43
+ const current = this.statusLock;
44
+ let resolve;
45
+ this.statusLock = new Promise(r => resolve = r);
46
+ try {
47
+ await current;
48
+ return await operation();
49
+ }
50
+ finally {
51
+ resolve();
52
+ }
53
+ }
54
+ async loadStatuses() {
55
+ await this.withLock(async () => {
56
+ try {
57
+ await fs.mkdir(path.dirname(this.statusFilePath), { recursive: true });
58
+ const data = await fs.readFile(this.statusFilePath, 'utf-8');
59
+ this.installOperationStatus = JSON.parse(data);
60
+ }
61
+ catch (error) {
62
+ if (error.code === 'ENOENT') {
63
+ this.installOperationStatus = {};
64
+ await this.saveStatuses(); // Create the file if it doesn't exist
65
+ }
66
+ else {
67
+ Logger.error('Failed to load install operation statuses:', error);
68
+ this.installOperationStatus = {}; // Initialize with empty statuses in case of other errors
69
+ }
70
+ }
71
+ });
72
+ }
73
+ async saveStatuses() {
74
+ await fs.mkdir(path.dirname(this.statusFilePath), { recursive: true });
75
+ await fs.writeFile(this.statusFilePath, JSON.stringify(this.installOperationStatus, null, 2));
76
+ }
77
+ get operationKey() {
78
+ return `${this.categoryName}-${this.serverName}`;
79
+ }
80
+ /**
81
+ * Record a step for this category/server instance.
82
+ */
83
+ async recordStep(stepName, status, message) {
84
+ return await this.withLock(async () => {
85
+ let operationDetails = this.installOperationStatus[this.operationKey];
86
+ const newStep = {
87
+ name: stepName,
88
+ status,
89
+ message,
90
+ timestamp: new Date().toISOString(),
91
+ };
92
+ if (!operationDetails) {
93
+ operationDetails = {
94
+ currentStep: stepName,
95
+ steps: [newStep],
96
+ lastUpdated: new Date().toISOString(),
97
+ error: status === 'failed' ? message : undefined,
98
+ overallStatus: status === 'failed' ? 'failed' : 'in-progress',
99
+ };
100
+ }
101
+ else {
102
+ // Check for an existing unfinished step with the same name
103
+ const existingStepIndex = operationDetails.steps.findIndex(s => s.name === stepName && s.status !== 'completed' && s.status !== 'failed');
104
+ if (existingStepIndex !== -1) {
105
+ // Update the existing unfinished step
106
+ operationDetails.steps[existingStepIndex] = newStep;
107
+ }
108
+ else {
109
+ operationDetails.steps.push(newStep);
110
+ }
111
+ operationDetails.currentStep = stepName;
112
+ operationDetails.lastUpdated = new Date().toISOString();
113
+ if (status === 'failed') {
114
+ operationDetails.overallStatus = 'failed';
115
+ if (!operationDetails.error) { // Store the first error
116
+ operationDetails.error = message;
117
+ }
118
+ // Mark all in-progress steps as cancelled
119
+ operationDetails.steps = operationDetails.steps.map(s => s.status === 'in-progress'
120
+ ? {
121
+ ...s,
122
+ status: 'canceled',
123
+ message: (s.message ? s.message + ' ' : '') + '[Canceled due to failure]',
124
+ timestamp: new Date().toISOString(),
125
+ }
126
+ : s);
127
+ }
128
+ else if (status === 'completed' && stepName.toLowerCase().includes('completed')) {
129
+ // Check if all steps are completed or if there are any failures
130
+ const allStepsCompleted = operationDetails.steps.every(s => s.status === 'completed' || (s.name === stepName && status === 'completed'));
131
+ operationDetails.overallStatus = allStepsCompleted ? 'completed' : 'in-progress';
132
+ if (allStepsCompleted) {
133
+ operationDetails.error = undefined; // Clear error on completion
134
+ }
135
+ }
136
+ else {
137
+ if (operationDetails.overallStatus !== 'failed') {
138
+ operationDetails.overallStatus = 'in-progress';
139
+ }
140
+ }
141
+ }
142
+ // Special handling for the "InstallCompleted" step or similar final step names
143
+ if (stepName === 'InstallCompleted') {
144
+ operationDetails.overallStatus = status === 'completed' ? 'completed' : 'failed';
145
+ if (status === 'completed')
146
+ operationDetails.error = undefined;
147
+ }
148
+ this.installOperationStatus[this.operationKey] = operationDetails;
149
+ await this.saveStatuses();
150
+ return operationDetails;
151
+ });
152
+ }
153
+ /**
154
+ * Resets (deletes) the operation status for this category/server.
155
+ * This is useful to call before starting a new installation attempt.
156
+ */
157
+ async resetOperation() {
158
+ await this.withLock(async () => {
159
+ if (this.installOperationStatus[this.operationKey]) {
160
+ delete this.installOperationStatus[this.operationKey];
161
+ Logger.info(`Reset installation operation status for ${this.operationKey}`);
162
+ await this.saveStatuses();
163
+ }
164
+ });
165
+ return this;
166
+ }
167
+ /**
168
+ * Gets the details for this category/server.
169
+ */
170
+ async getDetails() {
171
+ await this.loadStatuses(); // Ensure latest statuses are loaded
172
+ return this.installOperationStatus[this.operationKey];
173
+ }
174
+ /**
175
+ * Gets all operation details in this file (for this category/server).
176
+ */
177
+ async getAllDetails() {
178
+ await this.loadStatuses();
179
+ return this.installOperationStatus;
180
+ }
181
+ /**
182
+ * Explicitly mark the overall status of an operation as 'completed' or 'failed'.
183
+ * This can be used to force the final state regardless of step details.
184
+ * @param status 'completed' or 'failed'
185
+ * @param error Optional error message if failed
186
+ */
187
+ async markOverallStatus(status, error) {
188
+ return await this.withLock(async () => {
189
+ const operationDetails = this.installOperationStatus[this.operationKey];
190
+ if (!operationDetails)
191
+ return undefined;
192
+ operationDetails.overallStatus = status;
193
+ operationDetails.lastUpdated = new Date().toISOString();
194
+ if (status === 'failed') {
195
+ if (error) {
196
+ operationDetails.error = error;
197
+ }
198
+ // Mark all in-progress steps as cancelled
199
+ operationDetails.steps = operationDetails.steps.map(s => s.status === 'in-progress'
200
+ ? {
201
+ ...s,
202
+ status: 'canceled',
203
+ message: (s.message ? s.message + ' ' : '') + '[Canceled due to failure]',
204
+ timestamp: new Date().toISOString(),
205
+ }
206
+ : s);
207
+ }
208
+ if (status === 'completed') {
209
+ operationDetails.error = undefined;
210
+ }
211
+ await this.saveStatuses();
212
+ return operationDetails;
213
+ });
214
+ }
215
+ /**
216
+ * Executes a function and records its step status as 'in-progress', 'completed', or 'failed'.
217
+ *
218
+ * Useful for wrapping tasks with consistent status tracking and flexible error handling.
219
+ *
220
+ * @template T The return type of the function being executed.
221
+ *
222
+ * @param fn The function to execute. Can be synchronous or asynchronous.
223
+ * @param options Optional configuration:
224
+ * - stepName: Custom name for the step. Defaults to the function name or 'unnamedStep'.
225
+ * - inProgressMessage: Optional message to log when the step starts.
226
+ * - endMessage: Optional static string or function to generate a final message from the result or error.
227
+ * - onResult: A function `(result: T) => boolean` that determines whether the step was successful. Defaults to always `true`.
228
+ * - onError: Optional function `(error) => { result: T; message: string } | never` that handles an error and may return a fallback result and message, or throw.
229
+ *
230
+ * @returns A promise that resolves with the result of the function if successful, or rethrows on failure.
231
+ *
232
+ * @throws The original error or any error thrown from `onError`.
233
+ */
234
+ async recording(fn, options) {
235
+ const { stepName, inProgressMessage = 'Step in progress', endMessage, onResult = () => true, onError } = options || {};
236
+ const resolvedStepName = stepName ?? (fn.name || 'unnamedStep');
237
+ const getEndMessage = (data, fallback) => typeof endMessage === 'function' ? endMessage(data) || 'Step completed' : endMessage ?? fallback;
238
+ await this.recordStep(resolvedStepName, 'in-progress', inProgressMessage);
239
+ try {
240
+ const result = await Promise.resolve(fn());
241
+ const isSuccess = onResult(result);
242
+ await this.recordStep(resolvedStepName, isSuccess ? 'completed' : 'failed', getEndMessage(result, isSuccess ? 'Step completed successfully' : 'Step failed'));
243
+ return result;
244
+ }
245
+ catch (err) {
246
+ if (!onError) {
247
+ await this.recordStep(resolvedStepName, 'failed', err?.message || String(err));
248
+ throw err;
249
+ }
250
+ try {
251
+ const { result, message } = await onError(err);
252
+ await this.recordStep(resolvedStepName, 'failed', message);
253
+ return result;
254
+ }
255
+ catch (onErrorThrown) {
256
+ await this.recordStep(resolvedStepName, 'failed', onErrorThrown?.message || String(onErrorThrown));
257
+ throw onErrorThrown;
258
+ }
259
+ }
260
+ }
261
+ /**
262
+ * Executes an asynchronous "fire-and-forget" task and records its step status as 'in-progress', 'completed', or 'failed'.
263
+ *
264
+ * Unlike `recording`, this does not await the result — it runs the task in the background.
265
+ * Useful for side-effecting operations like updates or notifications where progress should be logged but not block flow.
266
+ *
267
+ * @template T The resolved type of the asynchronous task.
268
+ *
269
+ * @param fn The async function to execute.
270
+ * @param options Optional configuration:
271
+ * - stepName: Custom name for the step. Defaults to the function name or 'unnamedStep'.
272
+ * - inProgressMessage: Message to log when the step starts.
273
+ * - endMessage: Static string or function to generate the final message from result or error.
274
+ * - onResult: A boolean or function `(result: T) => boolean` to determine if the step is successful.
275
+ * - onError: A function `(error) => string` or `Promise<string>` to handle errors and return a message.
276
+ * If it throws, the error is rethrown and logged.
277
+ * - onComplete: Optional callback to run after completion, regardless of success or failure.
278
+ *
279
+ * @returns void
280
+ */
281
+ recordingAsync(fn, options) {
282
+ const { stepName, inProgressMessage = 'Step in progress', endMessage, onResult = () => true, onError, onComplete, } = options || {};
283
+ const resolvedStepName = stepName ?? (fn.name || 'unnamedStep');
284
+ const getMessage = (data, fallback) => typeof endMessage === 'function' ? endMessage(data) : endMessage ?? fallback;
285
+ this.recordStep(resolvedStepName, 'in-progress', inProgressMessage).then(() => {
286
+ fn()
287
+ .then(result => {
288
+ const isSuccess = onResult(result);
289
+ const message = getMessage(result, isSuccess ? 'Step completed successfully' : 'Step failed');
290
+ this.recordStep(resolvedStepName, isSuccess ? 'completed' : 'failed', message).then(onComplete).catch();
291
+ })
292
+ .catch(async (err) => {
293
+ if (onError) {
294
+ try {
295
+ const errorMessage = await onError(err);
296
+ await this.recordStep(resolvedStepName, 'failed', errorMessage);
297
+ }
298
+ catch (onErrorThrown) {
299
+ await this.recordStep(resolvedStepName, 'failed', onErrorThrown?.message || String(onErrorThrown));
300
+ throw onErrorThrown;
301
+ }
302
+ }
303
+ else {
304
+ const fallback = getMessage(err, err?.message || String(err));
305
+ await this.recordStep(resolvedStepName, 'failed', fallback);
306
+ }
307
+ });
308
+ });
309
+ }
310
+ }
311
+ //# sourceMappingURL=InstallOperationManager.js.map
@@ -0,0 +1,54 @@
1
+ import { SystemSettings } from '../metadatas/types.js';
2
+ export declare class SystemSettingsManager {
3
+ private static instance;
4
+ private settings;
5
+ private settingsFilePath;
6
+ private settingsLock;
7
+ private constructor();
8
+ static getInstance(): SystemSettingsManager;
9
+ private withLock;
10
+ private _loadSettingsInternal;
11
+ /**
12
+ * Loads settings from the file. This operation is atomic.
13
+ * If the settings file doesn't exist, it initializes with defaults and creates the file.
14
+ * @returns A promise that resolves when settings are loaded.
15
+ */
16
+ loadSettings(): Promise<void>;
17
+ private _applyDefaultsIfNeededInternal;
18
+ /**
19
+ * Applies default values to settings if they are not already set.
20
+ * This operation is atomic and saves settings if defaults are applied.
21
+ * @returns A promise that resolves when defaults are applied and saved if necessary.
22
+ */
23
+ applyDefaultsIfNeeded(): Promise<void>;
24
+ /**
25
+ * Retrieves the current system settings.
26
+ * If settings haven't been loaded or are empty, it attempts to load them.
27
+ * Paths within the settings are normalized (e.g., backslashes to forward slashes).
28
+ * @returns A promise that resolves to the `SystemSettings` object.
29
+ */
30
+ getSystemSettings(): Promise<SystemSettings>;
31
+ /**
32
+ * Creates or updates system settings with the provided partial settings.
33
+ * This operation is atomic and protected by a lock.
34
+ * It loads existing settings if not already loaded, merges the new settings,
35
+ * applies any necessary defaults, and then saves the updated settings.
36
+ * @param newSettings A `Partial<SystemSettings>` object containing the settings to update.
37
+ * @returns A promise that resolves to the fully updated and normalized `SystemSettings`.
38
+ */
39
+ createOrUpdateSystemSettings(newSettings: Partial<SystemSettings>): Promise<SystemSettings>;
40
+ /**
41
+ * Internal method to save settings to the file without acquiring a lock.
42
+ * Assumes the lock is already held by the calling public method.
43
+ * Normalizes paths before saving.
44
+ * @returns A promise that resolves when settings are written to disk.
45
+ */
46
+ private _saveSettingsInternal;
47
+ /**
48
+ * Saves the current settings to the file. This operation is atomic and protected by a lock.
49
+ * Paths are normalized before saving.
50
+ * @returns A promise that resolves when the settings are saved.
51
+ */
52
+ saveSettings(): Promise<void>;
53
+ }
54
+ export declare const systemSettingsManager: SystemSettingsManager;
@@ -0,0 +1,257 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { SETTINGS_DIR } from '../metadatas/constants.js';
4
+ import { Logger } from '../../utils/logger.js';
5
+ import { getSystemPythonExecutablePath, getNpmExecutablePath, getBrowserPath } from '../../utils/osUtils.js';
6
+ const SETTINGS_FILE_NAME = 'system_settings.json';
7
+ const SETTINGS_FILE_PATH = path.join(SETTINGS_DIR, 'settings', SETTINGS_FILE_NAME);
8
+ export class SystemSettingsManager {
9
+ static instance;
10
+ settings;
11
+ settingsFilePath;
12
+ settingsLock = Promise.resolve();
13
+ constructor() {
14
+ this.settingsFilePath = SETTINGS_FILE_PATH;
15
+ this.settings = {};
16
+ // No await here, initialization is best-effort. Lock will protect subsequent calls.
17
+ this.loadSettings().catch(error => {
18
+ Logger.error('Failed to initialize SystemSettingsManager during construction:', error);
19
+ // Initialize with empty settings if load fails, defaults will be applied on first access
20
+ this.settings = {};
21
+ });
22
+ }
23
+ static getInstance() {
24
+ if (!SystemSettingsManager.instance) {
25
+ SystemSettingsManager.instance = new SystemSettingsManager();
26
+ }
27
+ return SystemSettingsManager.instance;
28
+ }
29
+ async withLock(operation) {
30
+ const currentLock = this.settingsLock;
31
+ let releaseLock;
32
+ this.settingsLock = new Promise(resolve => releaseLock = resolve);
33
+ try {
34
+ await currentLock;
35
+ return await operation();
36
+ }
37
+ finally {
38
+ releaseLock();
39
+ }
40
+ }
41
+ // Internal method without lock
42
+ async _loadSettingsInternal() {
43
+ try {
44
+ await fs.mkdir(path.dirname(this.settingsFilePath), { recursive: true });
45
+ const data = await fs.readFile(this.settingsFilePath, 'utf-8');
46
+ this.settings = JSON.parse(data);
47
+ await this._applyDefaultsIfNeededInternal(); // Call internal version
48
+ }
49
+ catch (error) {
50
+ if (error.code === 'ENOENT') {
51
+ Logger.info(`Settings file not found at ${this.settingsFilePath}. Initializing with defaults.`);
52
+ this.settings = {};
53
+ await this._applyDefaultsIfNeededInternal(); // Call internal version
54
+ }
55
+ else {
56
+ Logger.error(`Error loading settings from ${this.settingsFilePath}:`, error);
57
+ this.settings = {};
58
+ await this._applyDefaultsIfNeededInternal(); // Call internal version
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Loads settings from the file. This operation is atomic.
64
+ * If the settings file doesn't exist, it initializes with defaults and creates the file.
65
+ * @returns A promise that resolves when settings are loaded.
66
+ */
67
+ async loadSettings() {
68
+ return this.withLock(() => this._loadSettingsInternal());
69
+ }
70
+ // Internal method without lock
71
+ async _applyDefaultsIfNeededInternal() {
72
+ let updated = false;
73
+ const normalizePath = (p) => p ? p.replace(/\\/g, '/') : undefined;
74
+ if (!this.settings.pythonEnvs) {
75
+ this.settings.pythonEnvs = {};
76
+ updated = true;
77
+ }
78
+ if (!this.settings.pythonEnvs["system"]) {
79
+ try {
80
+ const pythonPath = normalizePath(await getSystemPythonExecutablePath() || undefined);
81
+ if (pythonPath) {
82
+ this.settings.pythonEnvs["system"] = pythonPath;
83
+ updated = true;
84
+ }
85
+ }
86
+ catch (e) {
87
+ Logger.warn(`Could not get default pythonEnv: ${e}`);
88
+ }
89
+ }
90
+ if (!this.settings.nodePath) {
91
+ try {
92
+ const npmExecDir = await getNpmExecutablePath();
93
+ let nodeExecutable = process.platform === 'win32' ? 'node.exe' : 'node';
94
+ const platformNodePath = process.platform === 'win32'
95
+ ? path.join(npmExecDir || '', nodeExecutable)
96
+ : path.join(npmExecDir || '', 'bin', nodeExecutable);
97
+ this.settings.nodePath = npmExecDir ? normalizePath(platformNodePath) : undefined;
98
+ if (this.settings.nodePath)
99
+ updated = true;
100
+ }
101
+ catch (e) {
102
+ Logger.warn(`Could not get default nodePath: ${e}`);
103
+ }
104
+ }
105
+ else {
106
+ this.settings.nodePath = normalizePath(this.settings.nodePath);
107
+ }
108
+ if (!this.settings.browserPath) {
109
+ try {
110
+ this.settings.browserPath = normalizePath(await getBrowserPath() || undefined);
111
+ if (this.settings.browserPath)
112
+ updated = true;
113
+ }
114
+ catch (e) {
115
+ Logger.warn(`Could not get default browserPath: ${e}`);
116
+ }
117
+ }
118
+ else {
119
+ this.settings.browserPath = normalizePath(this.settings.browserPath);
120
+ }
121
+ if (!this.settings.systemEnvironments) {
122
+ this.settings.systemEnvironments = { ...process.env };
123
+ updated = true;
124
+ }
125
+ if (this.settings.userConfigurations === undefined) {
126
+ this.settings.userConfigurations = {};
127
+ updated = true;
128
+ }
129
+ if (updated) {
130
+ await this._saveSettingsInternal();
131
+ }
132
+ }
133
+ /**
134
+ * Applies default values to settings if they are not already set.
135
+ * This operation is atomic and saves settings if defaults are applied.
136
+ * @returns A promise that resolves when defaults are applied and saved if necessary.
137
+ */
138
+ async applyDefaultsIfNeeded() {
139
+ return this.withLock(() => this._applyDefaultsIfNeededInternal());
140
+ }
141
+ /**
142
+ * Retrieves the current system settings.
143
+ * If settings haven't been loaded or are empty, it attempts to load them.
144
+ * Paths within the settings are normalized (e.g., backslashes to forward slashes).
145
+ * @returns A promise that resolves to the `SystemSettings` object.
146
+ */
147
+ async getSystemSettings() {
148
+ // Ensure settings are loaded if they are empty.
149
+ if (Object.keys(this.settings).length === 0) {
150
+ // Use the public, locked version of loadSettings here
151
+ await this.loadSettings();
152
+ }
153
+ // Return a deep copy to prevent external modification of the internal state.
154
+ // Ensure this.settings is used, not a potentially stale local copy.
155
+ const settingsToReturn = JSON.parse(JSON.stringify(this.settings));
156
+ // Ensure paths are normalized when retrieved
157
+ const normalizedSettings = { ...settingsToReturn };
158
+ // Normalize paths in pythonEnvs
159
+ if (normalizedSettings.pythonEnvs) {
160
+ const normalizedPythonEnvs = {};
161
+ Object.keys(normalizedSettings.pythonEnvs).forEach(key => {
162
+ normalizedPythonEnvs[key] = normalizedSettings.pythonEnvs?.[key]?.replace(/\\/g, '/') || '';
163
+ });
164
+ normalizedSettings.pythonEnvs = normalizedPythonEnvs;
165
+ }
166
+ normalizedSettings.nodePath = normalizedSettings.nodePath?.replace(/\\/g, '/');
167
+ normalizedSettings.browserPath = normalizedSettings.browserPath?.replace(/\\/g, '/');
168
+ return normalizedSettings;
169
+ }
170
+ /**
171
+ * Creates or updates system settings with the provided partial settings.
172
+ * This operation is atomic and protected by a lock.
173
+ * It loads existing settings if not already loaded, merges the new settings,
174
+ * applies any necessary defaults, and then saves the updated settings.
175
+ * @param newSettings A `Partial<SystemSettings>` object containing the settings to update.
176
+ * @returns A promise that resolves to the fully updated and normalized `SystemSettings`.
177
+ */
178
+ async createOrUpdateSystemSettings(newSettings) {
179
+ return this.withLock(async () => {
180
+ // Ensure current settings are loaded before modification by calling the internal, non-locking version
181
+ if (Object.keys(this.settings).length === 0) {
182
+ await this._loadSettingsInternal();
183
+ }
184
+ const normalizePath = (p) => p ? p.replace(/\\/g, '/') : undefined;
185
+ // Initialize pythonEnvs if it doesn't exist on the current settings
186
+ if (!this.settings.pythonEnvs) {
187
+ this.settings.pythonEnvs = {};
188
+ }
189
+ // Create a new settings object by merging current and new settings.
190
+ const updatedSettings = {
191
+ ...this.settings, // Start with current settings
192
+ ...newSettings, // Override with new settings
193
+ // Explicitly handle potentially undefined paths from newSettings
194
+ nodePath: normalizePath(newSettings.nodePath !== undefined ? newSettings.nodePath : this.settings.nodePath),
195
+ browserPath: normalizePath(newSettings.browserPath !== undefined ? newSettings.browserPath : this.settings.browserPath),
196
+ // Ensure systemEnvironments and userConfigurations are properly merged or taken from newSettings
197
+ systemEnvironments: newSettings.systemEnvironments !== undefined
198
+ ? newSettings.systemEnvironments
199
+ : this.settings.systemEnvironments, // Keep existing if not in newSettings
200
+ userConfigurations: newSettings.userConfigurations !== undefined
201
+ ? newSettings.userConfigurations
202
+ : this.settings.userConfigurations || {}, // Keep existing or default to empty
203
+ };
204
+ this.settings = updatedSettings;
205
+ // Call the internal, non-locking version of applyDefaultsIfNeeded
206
+ await this._applyDefaultsIfNeededInternal();
207
+ // _applyDefaultsIfNeededInternal will call _saveSettingsInternal if it makes changes.
208
+ // However, if newSettings were provided that didn't trigger a change in _applyDefaultsIfNeededInternal
209
+ // (e.g., just updating an existing path), we still need to save.
210
+ // So, always call _saveSettingsInternal to persist all merged changes.
211
+ await this._saveSettingsInternal();
212
+ Logger.info('System settings updated.');
213
+ // getSystemSettings will handle its own locking if it needs to load.
214
+ return this.getSystemSettings();
215
+ });
216
+ }
217
+ /**
218
+ * Internal method to save settings to the file without acquiring a lock.
219
+ * Assumes the lock is already held by the calling public method.
220
+ * Normalizes paths before saving.
221
+ * @returns A promise that resolves when settings are written to disk.
222
+ */
223
+ async _saveSettingsInternal() {
224
+ try {
225
+ // Normalize paths before saving
226
+ const settingsToSave = { ...this.settings };
227
+ settingsToSave.nodePath = settingsToSave.nodePath?.replace(/\\/g, '/');
228
+ settingsToSave.browserPath = settingsToSave.browserPath?.replace(/\\/g, '/');
229
+ if (settingsToSave.pythonEnvs) {
230
+ const normalizedPythonEnvs = {};
231
+ Object.keys(settingsToSave.pythonEnvs).forEach(key => {
232
+ normalizedPythonEnvs[key] = settingsToSave.pythonEnvs?.[key]?.replace(/\\/g, '/') || '';
233
+ });
234
+ settingsToSave.pythonEnvs = normalizedPythonEnvs;
235
+ }
236
+ await fs.mkdir(path.dirname(this.settingsFilePath), { recursive: true });
237
+ await fs.writeFile(this.settingsFilePath, JSON.stringify(settingsToSave, null, 2));
238
+ Logger.info(`System settings saved to ${this.settingsFilePath}`);
239
+ }
240
+ catch (error) {
241
+ Logger.error(`Error saving settings to ${this.settingsFilePath}:`, error);
242
+ throw error; // Re-throw to indicate failure
243
+ }
244
+ }
245
+ /**
246
+ * Saves the current settings to the file. This operation is atomic and protected by a lock.
247
+ * Paths are normalized before saving.
248
+ * @returns A promise that resolves when the settings are saved.
249
+ */
250
+ async saveSettings() {
251
+ await this.withLock(async () => {
252
+ await this._saveSettingsInternal();
253
+ });
254
+ }
255
+ }
256
+ export const systemSettingsManager = SystemSettingsManager.getInstance();
257
+ //# sourceMappingURL=SystemSettingsManager.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Step name constants for InstallOperationManager and step recording.
3
+ *
4
+ * This file defines all static step names and documents dynamic step name patterns
5
+ * used throughout the installation and onboarding process.
6
+ *
7
+ * Dynamic step names are documented as template string patterns.
8
+ */
9
+ /** Step for processing all requirement updates in a batch operation. */
10
+ export declare const STEP_PROCESS_REQUIREMENT_UPDATES = "Processing all requirement updates";
11
+ /** Step for checking the status of a specific requirement. */
12
+ export declare const STEP_CHECKING_REQUIREMENT_STATUS = "Checking the status of requirement";
13
+ /** Step for installing requirements in the background process. */
14
+ export declare const STEP_INSTALLING_REQUIREMENTS_IN_BACKGROUND = "Installing requirements in the background";
15
+ /** Step for checking and installing all requirements as needed. */
16
+ export declare const STEP_CHECK_AND_INSTALL_REQUIREMENTS = "Checking and installing all requirements";
17
+ /** Step for running the install logic in the CommandInstaller. */
18
+ export declare const STEP_COMMAND_INSTALLER_INSTALL = "Running install in CommandInstaller";
19
+ /** Step for running the install logic in the GeneralInstaller. */
20
+ export declare const STEP_GENERAL_INSTALLER_INSTALL = "Running install in GeneralInstaller";
21
+ /** Step for executing the actual installation command (npm, pip, etc.). */
22
+ export declare const STEP_INSTALLATION_COMMAND_EXECUTION = "Executing installation command for requirement";
23
+ /** Step for running the install logic in the PipInstaller. */
24
+ export declare const STEP_PIP_INSTALLER_INSTALL = "Running install in PipInstaller";
25
+ /** Step for processing requirement updates in the RequirementService. */
26
+ export declare const STEP_PROCESS_REQUIREMENT_UPDATES_SERVICE = "Processing requirement updates in RequirementService";
27
+ /** Step for checking if the server is ready after installation. */
28
+ export declare const STEP_CHECK_SERVER_READINESS = "Checking server readiness after installation";
29
+ /** Step for running the install logic in the NpmInstaller. */
30
+ export declare const STEP_NPM_INSTALLER_INSTALL = "Running install in NpmInstaller";
31
+ /** Prefix for steps that update a specific requirement. */
32
+ export declare const STEP_INSTALL_REQUIREMENT_PREFIX = "Updating requirement:";
33
+ /** Prefix for steps that execute an installation command for a requirement. */
34
+ export declare const STEP_INSTALL_COMMAND_PREFIX = "Executing installation command for:";
35
+ /** Step for checking and installing the VS Code extension for the client. */
36
+ export declare const STEP_CHECK_VSCODE_AND_INSTALL_EXTENSION = "Checking and installing VS Code extension for client";
37
+ /** Step for setting up the installation configuration (env, args, etc.). */
38
+ export declare const STEP_SETUP_INSTALLATION_CONFIG = "Setting up installation configuration";
39
+ /** Step for updating VS Code settings for the client/server. */
40
+ export declare const STEP_UPDATE_VSCODE_SETTINGS = "Updating VS Code settings for client/server";
41
+ /** Step for the overall installation process of a client or server. */
42
+ export declare const STEP_INSTALLATION = "Running overall installation process";
43
+ /** Step for marking the initiation of an onboarding or installation process. */
44
+ export declare const STEP_INITIATED = "Initiating onboarding or installation process";