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