imcp 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/cli/commands/install.js +2 -2
  2. package/dist/cli/commands/list.js +2 -2
  3. package/dist/cli/commands/serve.js +1 -1
  4. package/dist/core/RequirementService.d.ts +0 -12
  5. package/dist/core/RequirementService.js +0 -24
  6. package/dist/core/installers/clients/BaseClientInstaller.d.ts +1 -1
  7. package/dist/core/installers/clients/ClientInstaller.d.ts +1 -1
  8. package/dist/core/installers/clients/ClientInstaller.js +1 -1
  9. package/dist/core/installers/clients/ClientInstallerFactory.js +1 -1
  10. package/dist/core/installers/clients/ClineInstaller.d.ts +1 -1
  11. package/dist/core/installers/clients/ClineInstaller.js +1 -1
  12. package/dist/core/installers/clients/ExtensionInstaller.js +1 -1
  13. package/dist/core/installers/clients/GithubCopilotInstaller.d.ts +1 -1
  14. package/dist/core/installers/clients/GithubCopilotInstaller.js +1 -1
  15. package/dist/core/installers/clients/MSRooCodeInstaller.d.ts +1 -1
  16. package/dist/core/installers/clients/MSRooCodeInstaller.js +1 -1
  17. package/dist/core/installers/requirements/BaseInstaller.d.ts +1 -1
  18. package/dist/core/installers/requirements/BaseInstaller.js +1 -1
  19. package/dist/core/installers/requirements/CommandInstaller.d.ts +1 -1
  20. package/dist/core/installers/requirements/CommandInstaller.js +1 -1
  21. package/dist/core/installers/requirements/GeneralInstaller.d.ts +1 -1
  22. package/dist/core/installers/requirements/InstallerFactory.d.ts +1 -1
  23. package/dist/core/installers/requirements/NpmInstaller.d.ts +1 -1
  24. package/dist/core/installers/requirements/NpmInstaller.js +1 -1
  25. package/dist/core/installers/requirements/PipInstaller.d.ts +1 -1
  26. package/dist/core/installers/requirements/RequirementInstaller.d.ts +1 -1
  27. package/dist/core/loaders/ConfigurationLoader.d.ts +32 -0
  28. package/dist/core/loaders/ConfigurationLoader.js +236 -0
  29. package/dist/core/loaders/ConfigurationProvider.d.ts +35 -0
  30. package/dist/core/loaders/ConfigurationProvider.js +375 -0
  31. package/dist/core/loaders/ServerSchemaLoader.d.ts +11 -0
  32. package/{src/core/ServerSchemaLoader.ts → dist/core/loaders/ServerSchemaLoader.js} +43 -48
  33. package/dist/core/loaders/ServerSchemaProvider.d.ts +17 -0
  34. package/{src/core/ServerSchemaProvider.ts → dist/core/loaders/ServerSchemaProvider.js} +120 -137
  35. package/dist/core/metadatas/constants.d.ts +47 -0
  36. package/dist/core/metadatas/constants.js +94 -0
  37. package/dist/core/metadatas/types.d.ts +166 -0
  38. package/dist/core/metadatas/types.js +16 -0
  39. package/dist/core/onboard/FeedOnboardService.d.ts +1 -1
  40. package/dist/core/onboard/FeedOnboardService.js +1 -1
  41. package/dist/core/onboard/OnboardProcessor.d.ts +1 -1
  42. package/dist/core/onboard/OnboardProcessor.js +1 -1
  43. package/dist/core/onboard/OnboardStatus.d.ts +1 -1
  44. package/dist/core/onboard/OnboardStatusManager.d.ts +1 -1
  45. package/dist/core/onboard/OnboardStatusManager.js +1 -1
  46. package/dist/core/validators/FeedValidator.d.ts +1 -1
  47. package/dist/core/validators/IServerValidator.d.ts +1 -1
  48. package/dist/core/validators/SSEServerValidator.d.ts +1 -1
  49. package/dist/core/validators/ServerValidatorFactory.d.ts +1 -1
  50. package/dist/core/validators/StdioServerValidator.d.ts +1 -1
  51. package/dist/core/validators/StdioServerValidator.js +1 -1
  52. package/dist/index.d.ts +3 -3
  53. package/dist/index.js +3 -3
  54. package/dist/services/InstallationService.d.ts +50 -0
  55. package/dist/services/InstallationService.js +350 -0
  56. package/dist/services/MCPManager.d.ts +28 -0
  57. package/dist/services/MCPManager.js +188 -0
  58. package/dist/services/RequirementService.d.ts +40 -0
  59. package/dist/services/RequirementService.js +110 -0
  60. package/dist/services/ServerService.d.ts +2 -2
  61. package/dist/services/ServerService.js +5 -5
  62. package/dist/utils/adoUtils.d.ts +2 -2
  63. package/dist/utils/adoUtils.js +1 -1
  64. package/dist/utils/feedUtils.js +1 -1
  65. package/dist/utils/githubUtils.d.ts +1 -1
  66. package/dist/utils/githubUtils.js +1 -1
  67. package/dist/utils/logger.js +1 -1
  68. package/dist/utils/macroExpressionUtils.d.ts +1 -1
  69. package/dist/utils/osUtils.d.ts +1 -1
  70. package/dist/utils/osUtils.js +1 -1
  71. package/dist/web/contract/serverContract.d.ts +1 -1
  72. package/dist/web/public/index.html +1 -3
  73. package/dist/web/public/js/api.js +2 -80
  74. package/dist/web/server.js +2 -2
  75. package/package.json +1 -1
  76. package/src/cli/commands/install.ts +3 -3
  77. package/src/cli/commands/list.ts +2 -2
  78. package/src/cli/commands/serve.ts +3 -2
  79. package/src/cli/index.ts +1 -1
  80. package/src/core/installers/clients/BaseClientInstaller.ts +134 -3
  81. package/src/core/installers/clients/ClientInstaller.ts +3 -3
  82. package/src/core/installers/clients/ClientInstallerFactory.ts +1 -1
  83. package/src/core/installers/clients/ClineInstaller.ts +1 -101
  84. package/src/core/installers/clients/ExtensionInstaller.ts +1 -1
  85. package/src/core/installers/clients/GithubCopilotInstaller.ts +1 -101
  86. package/src/core/installers/clients/MSRooCodeInstaller.ts +1 -102
  87. package/src/core/installers/requirements/BaseInstaller.ts +2 -2
  88. package/src/core/installers/requirements/CommandInstaller.ts +1 -1
  89. package/src/core/installers/requirements/GeneralInstaller.ts +1 -1
  90. package/src/core/installers/requirements/InstallerFactory.ts +1 -1
  91. package/src/core/installers/requirements/NpmInstaller.ts +12 -12
  92. package/src/core/installers/requirements/PipInstaller.ts +1 -1
  93. package/src/core/installers/requirements/RequirementInstaller.ts +1 -1
  94. package/src/core/{ConfigurationLoader.ts → loaders/ConfigurationLoader.ts} +31 -7
  95. package/src/core/{ConfigurationProvider.ts → loaders/ConfigurationProvider.ts} +18 -10
  96. package/src/core/loaders/ServerSchemaLoader.ts +117 -0
  97. package/src/core/loaders/ServerSchemaProvider.ts +99 -0
  98. package/src/core/{types.ts → metadatas/types.ts} +3 -2
  99. package/src/core/onboard/FeedOnboardService.ts +270 -146
  100. package/src/core/onboard/OnboardProcessor.ts +60 -11
  101. package/src/core/onboard/OnboardStatus.ts +7 -2
  102. package/src/core/onboard/OnboardStatusManager.ts +270 -43
  103. package/src/core/validators/FeedValidator.ts +65 -9
  104. package/src/core/validators/IServerValidator.ts +1 -1
  105. package/src/core/validators/SSEServerValidator.ts +2 -2
  106. package/src/core/validators/ServerValidatorFactory.ts +1 -1
  107. package/src/core/validators/StdioServerValidator.ts +86 -34
  108. package/src/index.ts +3 -3
  109. package/src/{core → services}/InstallationService.ts +5 -5
  110. package/src/{core → services}/MCPManager.ts +10 -5
  111. package/src/{core → services}/RequirementService.ts +2 -31
  112. package/src/services/ServerService.ts +7 -7
  113. package/src/utils/adoUtils.ts +3 -3
  114. package/src/utils/feedUtils.ts +2 -2
  115. package/src/utils/githubUtils.ts +2 -2
  116. package/src/utils/logger.ts +13 -1
  117. package/src/utils/macroExpressionUtils.ts +1 -1
  118. package/src/utils/osUtils.ts +4 -4
  119. package/src/web/contract/serverContract.ts +2 -2
  120. package/src/web/public/index.html +1 -3
  121. package/src/web/public/js/api.js +2 -80
  122. package/src/web/public/js/modal/installation.js +1 -1
  123. package/src/web/public/js/onboard/ONBOARDING_PAGE_DESIGN.md +41 -9
  124. package/src/web/public/js/onboard/formProcessor.js +200 -34
  125. package/src/web/public/js/onboard/index.js +2 -2
  126. package/src/web/public/js/onboard/publishHandler.js +30 -22
  127. package/src/web/public/js/onboard/templates.js +34 -40
  128. package/src/web/public/js/onboard/uiHandlers.js +175 -84
  129. package/src/web/public/js/onboard/validationHandlers.js +147 -64
  130. package/src/web/public/js/serverCategoryDetails.js +19 -4
  131. package/src/web/public/js/serverCategoryList.js +13 -1
  132. package/src/web/public/onboard.html +1 -1
  133. package/src/web/server.ts +30 -14
  134. package/src/services/InstallRequestValidator.ts +0 -112
  135. /package/src/core/{constants.ts → metadatas/constants.ts} +0 -0
@@ -2,8 +2,8 @@ import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import fs from 'fs/promises';
4
4
  import path from 'path';
5
- import { GITHUB_REPO, SETTINGS_DIR } from '../constants.js';
6
- import { FeedConfiguration } from '../types.js';
5
+ import { GITHUB_REPO, SETTINGS_DIR } from '../metadatas/constants.js';
6
+ import { FeedConfiguration } from '../metadatas/types.js';
7
7
  import { Logger } from '../../utils/logger.js';
8
8
 
9
9
  const execAsync = promisify(exec);
@@ -110,16 +110,63 @@ export class OnboardProcessor {
110
110
  * @param onboardingId The ID of the onboarding process.
111
111
  * @param config The feed configuration to save.
112
112
  * @param repoDir The root directory of the cloned repository.
113
+ * @param serverList The list of MCP server names to process.
114
+ * @returns A promise that resolves to an object containing the path to the saved feed file and the category-specific schemas directory.
113
115
  * @throws An error if saving the configuration fails, with a 'step' property set to 'saveFeedConfigToRepo'.
114
116
  */
115
- public async saveFeedConfigToRepo(onboardingId: string, config: FeedConfiguration, repoDir: string): Promise<void> {
117
+ public async saveFeedConfigToRepo(onboardingId: string, config: FeedConfiguration, repoDir: string, serverList: string[]): Promise<{ feedFilePath: string; categorySchemasPath: string; }> {
116
118
  try {
117
119
  const feedsDir = path.join(repoDir, GITHUB_REPO.feedsPath);
118
120
  await fs.mkdir(feedsDir, { recursive: true });
119
121
 
120
- const configPath = path.join(feedsDir, `${config.name}.json`);
121
- await fs.writeFile(configPath, JSON.stringify(config, null, 2));
122
- Logger.debug(`[${onboardingId}] Saved feed configuration to ${configPath}`);
122
+ const categoryName = config.name; // Feed name is the category name
123
+ const categorySchemasPath = path.join(feedsDir, 'schemas', categoryName);
124
+ await fs.mkdir(categorySchemasPath, { recursive: true }); // Ensure category schema directory exists
125
+
126
+ // Process schemas for each MCP server
127
+ // Create a mutable copy of mcpServers to update schema paths
128
+ const updatedMcpServers = [];
129
+ for (const server of config.mcpServers) {
130
+ if (!serverList.includes(server.name)) continue;
131
+ let updatedServer = { ...server }; // Shallow copy server config
132
+ if (updatedServer.schemas && typeof updatedServer.schemas === 'string' && updatedServer.schemas.trim() !== '') {
133
+ const originalSchemaPath = updatedServer.schemas;
134
+ const serverName = updatedServer.name;
135
+ const newSchemaFileName = `${serverName}.json`;
136
+
137
+ // Schemas are now directly under categorySchemasPath
138
+ const newSchemaPathInRepo = path.join(categorySchemasPath, newSchemaFileName);
139
+
140
+ try {
141
+ // Read content from the original schema path
142
+ const schemaContent = await fs.readFile(originalSchemaPath, 'utf-8');
143
+ // Write content to the new schema file in the repo
144
+ await fs.writeFile(newSchemaPathInRepo, schemaContent);
145
+ Logger.debug(`[${onboardingId}] Copied schema for server '${serverName}' from '${originalSchemaPath}' to '${newSchemaPathInRepo}'`);
146
+
147
+ // Update the schemas property to the new filename, as per instruction "rename schemas in McpServer as serverName].json"
148
+ updatedServer.schemas = newSchemaFileName;
149
+ } catch (schemaError) {
150
+ const errorMsg = `Error processing schema for server '${serverName}' (source: ${originalSchemaPath}): ${schemaError instanceof Error ? schemaError.message : String(schemaError)}`;
151
+ Logger.error(`[${onboardingId}] ${errorMsg}`);
152
+ // Propagate the error to fail the onboarding step
153
+ throw new Error(errorMsg);
154
+ }
155
+ }
156
+ updatedMcpServers.push(updatedServer);
157
+ }
158
+
159
+ // Create a new config object with the updated mcpServers
160
+ const processedConfig = {
161
+ ...config,
162
+ mcpServers: updatedMcpServers
163
+ };
164
+
165
+ const feedFilePath = path.join(feedsDir, `${config.name}.json`);
166
+ await fs.writeFile(feedFilePath, JSON.stringify(processedConfig, null, 2));
167
+ Logger.debug(`[${onboardingId}] Saved feed configuration (with processed schemas) to ${feedFilePath}`);
168
+
169
+ return { feedFilePath, categorySchemasPath };
123
170
  } catch (error) {
124
171
  const errorMessage = error instanceof Error ? error.message : String(error);
125
172
  Logger.error(`[${onboardingId}] Error during saveFeedConfigToRepo: ${errorMessage}`);
@@ -160,7 +207,7 @@ export class OnboardProcessor {
160
207
  Logger.debug(`[${onboardingId}] Fetched from upstream.`);
161
208
 
162
209
  try {
163
- await execAsync('git checkout main');
210
+ await execAsync('git checkout main --force');
164
211
  Logger.debug(`[${onboardingId}] Switched to existing local branch 'main'.`);
165
212
  } catch (checkoutMainError: any) {
166
213
  const errStdErr = checkoutMainError.stderr || '';
@@ -169,7 +216,7 @@ export class OnboardProcessor {
169
216
  errStdErr.includes('pathspec \'main\' did not match any file(s) known to git')) {
170
217
  Logger.debug(`[${onboardingId}] Local branch 'main' not found or invalid. Attempting to create it from 'upstream/main'.`);
171
218
  try {
172
- await execAsync('git checkout -b main upstream/main');
219
+ await execAsync('git checkout -b main --force upstream/main');
173
220
  Logger.debug(`[${onboardingId}] Created and switched to local branch 'main' from 'upstream/main'.`);
174
221
  } catch (createMainError: any) {
175
222
  const errMsg = createMainError instanceof Error ? createMainError.message : String(createMainError);
@@ -221,7 +268,7 @@ export class OnboardProcessor {
221
268
  * @returns Object containing the PR URL and branch name.
222
269
  * @throws An error if PR creation fails.
223
270
  */
224
- public async createPullRequest(onboardingId: string, config: FeedConfiguration, repoDir: string, branchName: string): Promise<{ url: string; branchName: string }> {
271
+ public async createPullRequest(onboardingId: string, config: FeedConfiguration, repoDir: string, branchName: string): Promise<{ url: string; branchName: string; prExists: boolean }> {
225
272
  try {
226
273
  process.chdir(repoDir);
227
274
 
@@ -251,19 +298,21 @@ export class OnboardProcessor {
251
298
  const username = await this.getGitHubUsername();
252
299
  const explicitHeadRef = `${username}:${branchName}`;
253
300
 
254
- const prCheckCommand = `gh pr list --repo ${GITHUB_REPO.repoName} --head "${explicitHeadRef}" --state open --json url --limit 1`;
301
+ const prCheckCommand = `gh pr list --repo ${GITHUB_REPO.repoName} --head ${branchName} --author ${username} --state open --json url --limit 1`;
255
302
  Logger.debug(`[${onboardingId}] Checking for existing PR with command: ${prCheckCommand}`);
256
303
  const { stdout: existingPrJson } = await execAsync(prCheckCommand);
257
304
 
258
305
  let prUrlToReturn: string;
259
306
 
260
307
  const createPRCommand = `gh pr create --repo ${GITHUB_REPO.repoName} --base main --head ${explicitHeadRef} --title "${title}" --body "${body}"`
308
+ let prExists = false;
261
309
 
262
310
  if (existingPrJson && existingPrJson.trim() !== '[]' && existingPrJson.trim() !== '') {
263
311
  try {
264
312
  const prs = JSON.parse(existingPrJson);
265
313
  if (prs.length > 0 && prs[0].url) {
266
314
  prUrlToReturn = prs[0].url;
315
+ prExists = true;
267
316
  Logger.log(`[${onboardingId}] Found existing open PR for ${explicitHeadRef} on ${GITHUB_REPO.repoName}: ${prUrlToReturn}`);
268
317
  } else {
269
318
  Logger.debug(`[${onboardingId}] No existing PR found or unable to parse PR list JSON. Proceeding to create PR.`);
@@ -282,7 +331,7 @@ export class OnboardProcessor {
282
331
  }
283
332
 
284
333
  Logger.debug(`[${onboardingId}] Pull request operation completed. PR URL: ${prUrlToReturn}`);
285
- return { url: prUrlToReturn, branchName };
334
+ return { url: prUrlToReturn, branchName: branchName, prExists: prExists };
286
335
  } catch (error) {
287
336
  const errorMessage = error instanceof Error ? error.message : String(error);
288
337
  Logger.error(`[${onboardingId}] Error during createPullRequest: ${errorMessage}`);
@@ -1,4 +1,4 @@
1
- import { FeedConfiguration } from "../types.js";
1
+ import { FeedConfiguration } from "../metadatas/types.js";
2
2
 
3
3
  export enum OnboardingProcessStatus {
4
4
  VALIDATING = 'validating',
@@ -20,7 +20,12 @@ export interface ServerValidationResult {
20
20
  export interface OnboardStatus {
21
21
  onboardingId: string;
22
22
  status: OnboardingProcessStatus;
23
- currentStep?: string; // e.g., "Validating feed configuration", "Forking repository"
23
+ steps?: {
24
+ stepName: string;
25
+ timestamp: string;
26
+ status?: OnboardingProcessStatus; // Status of this specific step, if different from overall
27
+ errorMessage?: string;
28
+ }[];
24
29
  validationStatus?: {
25
30
  isValid: boolean;
26
31
  message?: string;
@@ -1,17 +1,24 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
- import { SETTINGS_DIR } from '../constants.js';
4
- import { FeedConfiguration } from '../types.js';
3
+ import { SETTINGS_DIR } from '../metadatas/constants.js';
4
+ import { FeedConfiguration, McpConfig } from '../metadatas/types.js';
5
5
  import { OnboardStatus, OnboardingProcessStatus, OperationType } from './OnboardStatus.js';
6
6
  import { Logger } from '../../utils/logger.js';
7
7
 
8
8
  const ONBOARD_STATUS_DIR = path.join(SETTINGS_DIR, 'onboard');
9
- const CATEGORY_OPERATIONS_STATUS_FILE = path.join(ONBOARD_STATUS_DIR, 'category_operations_status.json');
9
+ const CATEGORY_OPERATIONS_STATUS_FILE = path.join(ONBOARD_STATUS_DIR, 'OnboardStatus.json');
10
10
  const FEED_CONFIG_DIR = path.join(ONBOARD_STATUS_DIR, 'feed_configs'); // Staging for feed configs during operation
11
11
 
12
+ const NON_COMPLETED_ONBOARDING_STATUSES: OnboardingProcessStatus[] = [
13
+ OnboardingProcessStatus.PENDING,
14
+ OnboardingProcessStatus.VALIDATING,
15
+ OnboardingProcessStatus.VALIDATED,
16
+ OnboardingProcessStatus.PR_CREATING,
17
+ ];
18
+
12
19
  export class OnboardStatusManager {
13
20
  private static instance: OnboardStatusManager;
14
- private activeCategoryOperations: Record<string, OnboardStatus> = {};
21
+ private activeCategoryOperations: Record<string, Partial<Record<OperationType, OnboardStatus>>> = {};
15
22
  private statusLock: Promise<void> = Promise.resolve();
16
23
 
17
24
  private constructor() {
@@ -67,12 +74,13 @@ export class OnboardStatusManager {
67
74
  * @param categoryName The name of the category (acting as operationId).
68
75
  * @param config The FeedConfiguration to save.
69
76
  */
70
- public async saveFeedConfiguration(categoryName: string, config: FeedConfiguration): Promise<void> {
77
+ public async saveFeedConfiguration(categoryName: string, operationType: OperationType, config: FeedConfiguration): Promise<void> {
71
78
  await this.withLock(async () => {
72
79
  try {
73
80
  await fs.mkdir(FEED_CONFIG_DIR, { recursive: true });
74
81
  // Suffix `_feed.json` to distinguish from potential status files if names overlap
75
- const configPath = path.join(FEED_CONFIG_DIR, `${categoryName}_feed.json`);
82
+ const operationId = this.createOperationId(categoryName, operationType);
83
+ const configPath = path.join(FEED_CONFIG_DIR, `${operationId}_feed.json`);
76
84
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
77
85
  Logger.debug(`Saved feed configuration for category ${categoryName} to ${configPath}`);
78
86
  } catch (error) {
@@ -87,10 +95,11 @@ export class OnboardStatusManager {
87
95
  * @param categoryName The name of the category (acting as operationId).
88
96
  * @returns A promise that resolves to the FeedConfiguration, or undefined if not found.
89
97
  */
90
- public async getFeedConfiguration(categoryName: string): Promise<FeedConfiguration | undefined> {
98
+ public async getFeedConfiguration(categoryName: string, operationType: OperationType): Promise<FeedConfiguration | undefined> {
91
99
  return await this.withLock(async () => {
92
100
  try {
93
- const configPath = path.join(FEED_CONFIG_DIR, `${categoryName}_feed.json`);
101
+ const operationId = this.createOperationId(categoryName, operationType);
102
+ const configPath = path.join(FEED_CONFIG_DIR, `${operationId}_feed.json`);
94
103
  const data = await fs.readFile(configPath, 'utf-8');
95
104
  return JSON.parse(data) as FeedConfiguration;
96
105
  } catch (error) {
@@ -106,56 +115,180 @@ export class OnboardStatusManager {
106
115
 
107
116
 
108
117
  /**
109
- * Retrieves the status of a category's active operation.
110
- * @param categoryName The name of the category (acting as operationId).
111
- * @returns A promise that resolves to the OnboardStatus, or undefined if no active operation.
118
+ * Retrieves the status of a specific operation for a category.
119
+ * @param categoryName The name of the category.
120
+ * @param operationType The type of operation.
121
+ * @returns A promise that resolves to the OnboardStatus, or undefined if no such operation or category.
112
122
  */
113
- public async getStatus(categoryName: string): Promise<OnboardStatus | undefined> {
114
- await this.loadStatuses(); // Ensure latest statuses are loaded (though constructor does it initially)
115
- return this.activeCategoryOperations[categoryName];
123
+ public async getStatus(categoryName: string, operationType: OperationType): Promise<OnboardStatus | undefined> {
124
+ await this.loadStatuses(); // Ensure latest statuses are loaded
125
+ return this.activeCategoryOperations[categoryName]?.[operationType];
116
126
  }
117
127
 
118
128
  /**
119
- * Retrieves all active category operation statuses.
120
- * @returns A promise that resolves to a record of all active OnboardStatus objects, keyed by categoryName.
129
+ * Retrieves all active category operation statuses, structured by category and then by operation type.
130
+ * @returns A promise that resolves to a record of all active OnboardStatus objects.
121
131
  */
122
- public async getAllStatuses(): Promise<Record<string, OnboardStatus>> {
132
+ public async getAllStatuses(): Promise<Record<string, Partial<Record<OperationType, OnboardStatus>>>> {
123
133
  await this.loadStatuses();
124
134
  return this.activeCategoryOperations;
125
135
  }
126
136
 
127
137
  /**
128
- * Updates the status of a category's active operation.
129
- * If no operation exists for the category, it might create one based on the updates,
130
- * or fail if `onboardingId` (categoryName) is not present in `newStatus`.
131
- * For clarity, it's better if `createInitialStatus` is called first.
132
- * @param categoryName The name of the category (acting as operationId).
133
- * @param updates Partial OnboardStatus object with fields to update.
138
+ * Updates the status of a specific operation for a category.
139
+ * If a `stepName` is provided in `updates`, it will also add a new step.
140
+ * @param categoryName The name of the category.
141
+ * @param operationType The type of operation.
142
+ * @param updates Partial OnboardStatus object with fields to update. Can include `stepName` to record a new step.
143
+ * @param stepName Optional. If provided, a new step with this name will be recorded.
144
+ * @param stepStatus Optional. Status for the new step, if `stepName` is provided.
145
+ * @param errorMessage Optional. Error message for the new step, if `stepName` is provided.
134
146
  * @returns A promise that resolves to the updated OnboardStatus.
135
147
  */
136
- public async updateStatus(categoryName: string, updates: Partial<OnboardStatus>): Promise<OnboardStatus> {
148
+ public async updateStatus(
149
+ categoryName: string,
150
+ operationType: OperationType,
151
+ updates: Partial<Omit<OnboardStatus, 'steps'>>, // Exclude steps from direct updates here
152
+ stepName?: string,
153
+ stepStatus?: OnboardingProcessStatus,
154
+ errorMessage?: string
155
+ ): Promise<OnboardStatus> {
137
156
  return await this.withLock(async () => {
138
- const existingStatus = this.activeCategoryOperations[categoryName] || {
139
- onboardingId: categoryName, // Ensure onboardingId is set to categoryName if creating new
140
- feedName: categoryName, // Ensure feedName is set to categoryName
141
- status: OnboardingProcessStatus.PENDING, // Default status if creating new
142
- lastUpdated: new Date().toISOString(),
143
- operationType: updates.operationType || 'FULL_ONBOARDING', // Default operation type
144
- ...updates // Apply initial updates if creating new
145
- };
157
+ const operationId = this.createOperationId(categoryName, operationType);
158
+ const categoryOps = this.activeCategoryOperations[categoryName] || {};
159
+ let existingStatus = categoryOps[operationType];
146
160
 
147
- const updatedStatus: OnboardStatus = {
161
+ if (!existingStatus) {
162
+ // If no status exists, create an initial one.
163
+ // This might happen if updateStatus is called before createInitialStatus,
164
+ // though ideally createInitialStatus should be the entry point.
165
+ existingStatus = {
166
+ onboardingId: operationId,
167
+ feedName: categoryName,
168
+ status: updates.status || OnboardingProcessStatus.PENDING,
169
+ lastUpdated: new Date().toISOString(),
170
+ operationType: operationType,
171
+ steps: [],
172
+ };
173
+ }
174
+
175
+ // Apply general updates
176
+ let updatedStatus: OnboardStatus = {
148
177
  ...existingStatus,
149
178
  ...updates,
179
+ onboardingId: operationId,
180
+ feedName: categoryName,
181
+ operationType: operationType,
150
182
  lastUpdated: new Date().toISOString(),
151
183
  };
152
- this.activeCategoryOperations[categoryName] = updatedStatus;
184
+
185
+ // If stepName is provided, add it as a new step
186
+ if (stepName) {
187
+ const newStep = {
188
+ stepName,
189
+ timestamp: new Date().toISOString(),
190
+ status: stepStatus ?? updates.status ?? existingStatus.status,
191
+ errorMessage: errorMessage ?? updates.errorMessage,
192
+ };
193
+ updatedStatus.steps = [...(existingStatus.steps || []), newStep];
194
+
195
+ // If the step has a specific status, it might also update the overall status
196
+ if (newStep.status && newStep.status !== updatedStatus.status) {
197
+ updatedStatus.status = newStep.status;
198
+ }
199
+ if (newStep.errorMessage && !updatedStatus.errorMessage) {
200
+ updatedStatus.errorMessage = newStep.errorMessage;
201
+ }
202
+ }
203
+
204
+ categoryOps[operationType] = updatedStatus;
205
+ this.activeCategoryOperations[categoryName] = categoryOps;
153
206
  await this.saveStatuses();
154
207
  return updatedStatus;
155
208
  });
156
209
  }
157
210
 
158
211
  /**
212
+ * A more specific method to record a step, which can also update the overall status.
213
+ * This is intended to replace the local `updateStatus` functions in validators/services.
214
+ * @param categoryName The name of the category (e.g., feed name).
215
+ * @param operationType The type of operation.
216
+ * @param stepName The description of the current step.
217
+ * @param serverName Optional server context for this step.
218
+ * @param newStatus Optional overall status to set for the operation.
219
+ * @param errorMessage Optional error message if this step resulted in a failure.
220
+ * @returns A promise that resolves to the updated OnboardStatus.
221
+ */
222
+ public async recordStep(
223
+ categoryName: string,
224
+ operationType: OperationType,
225
+ stepName: string,
226
+ serverName?: string, // Added serverName to be part of the step if relevant
227
+ newStatus?: OnboardingProcessStatus,
228
+ errorMessage?: string
229
+ ): Promise<OnboardStatus> {
230
+ return await this.withLock(async () => {
231
+ const operationId = this.createOperationId(categoryName, operationType);
232
+ const categoryOps = this.activeCategoryOperations[categoryName] || {};
233
+ let currentStatus = categoryOps[operationType];
234
+
235
+ if (!currentStatus) {
236
+ currentStatus = await this.createInitialStatus(categoryName, operationType, serverName);
237
+ }
238
+
239
+ const stepToAdd = {
240
+ stepName,
241
+ timestamp: new Date().toISOString(),
242
+ status: newStatus ?? (errorMessage ? OnboardingProcessStatus.FAILED : currentStatus.status), // Step inherits current or becomes FAILED
243
+ errorMessage: errorMessage,
244
+ ...(serverName && { serverName }), // Include serverName in the step if provided
245
+ };
246
+
247
+ const updatedSteps = [...(currentStatus.steps || []), stepToAdd];
248
+
249
+ const statusUpdatePayload: Partial<OnboardStatus> = {
250
+ steps: updatedSteps,
251
+ lastUpdated: new Date().toISOString(),
252
+ };
253
+
254
+ if (newStatus) {
255
+ statusUpdatePayload.status = newStatus;
256
+ } else if (errorMessage && currentStatus.status !== OnboardingProcessStatus.FAILED) {
257
+ // If an error occurs and we're not already FAILED, mark as FAILED.
258
+ statusUpdatePayload.status = OnboardingProcessStatus.FAILED;
259
+ }
260
+
261
+ if (errorMessage && !currentStatus.errorMessage) { // Store the first error message
262
+ statusUpdatePayload.errorMessage = errorMessage;
263
+ }
264
+ if (serverName && !currentStatus.serverName) { // Store serverName if not already set at top level
265
+ statusUpdatePayload.serverName = serverName;
266
+ }
267
+
268
+
269
+ const updatedStatus: OnboardStatus = {
270
+ ...currentStatus,
271
+ ...statusUpdatePayload,
272
+ };
273
+
274
+ categoryOps[operationType] = updatedStatus;
275
+ this.activeCategoryOperations[categoryName] = categoryOps;
276
+ await this.saveStatuses();
277
+ return updatedStatus;
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Creates an operation ID that combines category name and operation type.
283
+ * @param categoryName The name of the category.
284
+ * @param operationType The type of operation.
285
+ * @returns Combined operation ID string.
286
+ */
287
+ private createOperationId(categoryName: string, operationType: OperationType): string {
288
+ return `${categoryName}_${operationType}`;
289
+ }
290
+
291
+ /**
159
292
  * Creates an initial status for a new category-wide operation.
160
293
  * The operation is identified by the categoryName.
161
294
  * @param categoryName The name of the category (also used as feedName and operationId).
@@ -165,22 +298,116 @@ export class OnboardStatusManager {
165
298
  * @returns A promise that resolves to the initial OnboardStatus.
166
299
  */
167
300
  public async createInitialStatus(categoryName: string, operationType: OperationType, serverName?: string): Promise<OnboardStatus> {
168
- // The operationId is the categoryName itself for category-wide operations.
169
- const operationId = categoryName;
301
+ // Create operation ID that includes both category and operation type
302
+ const operationId = this.createOperationId(categoryName, operationType);
170
303
 
171
304
  const initialStatus: OnboardStatus = {
172
- onboardingId: operationId, // This is the categoryName
173
- feedName: categoryName, // Feed name is the category name
174
- serverName, // Optional server context
305
+ onboardingId: operationId,
306
+ feedName: categoryName,
307
+ serverName,
175
308
  status: OnboardingProcessStatus.PENDING,
176
- currentStep: 'Initiated',
177
- lastUpdated: new Date().toISOString(), // timestamp was an error, OnboardStatus uses lastUpdated
309
+ steps: [{ stepName: 'Initiated', timestamp: new Date().toISOString() }],
310
+ lastUpdated: new Date().toISOString(),
178
311
  operationType,
179
312
  errorMessage: undefined,
180
313
  prInfo: undefined
181
314
  };
182
- // Use updateStatus to ensure it's saved correctly and lock is handled
183
- return this.updateStatus(operationId, initialStatus);
315
+
316
+ // Save directly without using updateStatus, into the new structure
317
+ await this.withLock(async () => {
318
+ const categoryOps = this.activeCategoryOperations[categoryName] || {};
319
+ categoryOps[operationType] = initialStatus;
320
+ this.activeCategoryOperations[categoryName] = categoryOps;
321
+ await this.saveStatuses();
322
+ });
323
+
324
+ return initialStatus;
325
+ }
326
+
327
+
328
+ /**
329
+ * Finds a succeeded operation for a given feed name and operation type that has a matching feed configuration.
330
+ * @param feedName The name of the feed.
331
+ * @param operationType The type of operation.
332
+ * @param currentConfig The current feed configuration to compare against.
333
+ * @returns A promise that resolves to the OnboardStatus of the succeeded operation, or undefined if not found.
334
+ */
335
+ public async findSucceededOperation(
336
+ feedName: string,
337
+ operationType: OperationType,
338
+ currentConfig: FeedConfiguration
339
+ ): Promise<OnboardStatus | undefined> {
340
+ return await this.withLock(async () => {
341
+ for (const categoryName in this.activeCategoryOperations) {
342
+ const operations = this.activeCategoryOperations[categoryName];
343
+ if (operations) {
344
+ const specificOperationStatus = operations[operationType];
345
+ if (
346
+ specificOperationStatus &&
347
+ specificOperationStatus.feedName === feedName &&
348
+ specificOperationStatus.status === OnboardingProcessStatus.SUCCEEDED
349
+ ) {
350
+ // The FeedConfiguration is stored in the result of the SUCCEEDED operation
351
+ const savedConfig = specificOperationStatus.result?.feedConfiguration as FeedConfiguration;
352
+
353
+ if (savedConfig && this.isFeedConfigurationEqual(savedConfig, currentConfig)) {
354
+ return specificOperationStatus;
355
+ }
356
+ }
357
+ }
358
+ }
359
+ return undefined;
360
+ });
361
+ }
362
+
363
+ /**
364
+ * Compares two feed configurations for equality.
365
+ * @param config1 First feed configuration.
366
+ * @param config2 Second feed configuration.
367
+ * @returns True if the configurations are identical, false otherwise.
368
+ */
369
+ private isFeedConfigurationEqual(config1: FeedConfiguration, config2: FeedConfiguration): boolean {
370
+ // Compare basic properties
371
+ if (config1.name !== config2.name) return false;
372
+
373
+ // Compare arrays with specific order handling
374
+ const compareArrays = <T>(arr1: T[], arr2: T[], sortFn?: (a: T, b: T) => number): boolean => {
375
+ if (arr1.length !== arr2.length) return false;
376
+ const sorted1 = sortFn ? [...arr1].sort(sortFn) : arr1;
377
+ const sorted2 = sortFn ? [...arr2].sort(sortFn) : arr2;
378
+ return JSON.stringify(sorted1) === JSON.stringify(sorted2);
379
+ };
380
+
381
+ // Compare requirements array
382
+ if (!compareArrays(config1.requirements || [], config2.requirements || [])) return false;
383
+
384
+ // Compare MCP servers with name-based sorting
385
+ const sortByName = (a: McpConfig, b: McpConfig) => (a.name || '').localeCompare(b.name || '');
386
+ if (!compareArrays(config1.mcpServers || [], config2.mcpServers || [], sortByName)) return false;
387
+
388
+ return true;
389
+ }
390
+
391
+ /**
392
+ * Finds an existing non-completed operation for a given feed name, server name, and operation type.
393
+ * @param feedName The name of the feed.
394
+ * @param operationType The type of operation.
395
+ * @returns A promise that resolves to the OnboardStatus of the existing operation, or undefined if not found.
396
+ */
397
+ public async _findExistingNonCompletedOperation(
398
+ feedName: string,
399
+ operationType: OperationType
400
+ ): Promise<OnboardStatus | undefined> {
401
+ const allCategoryStatuses = await this.getAllStatuses();
402
+ // feedName is the categoryName
403
+ const categoryOperations = allCategoryStatuses[feedName];
404
+ if (categoryOperations) {
405
+ const specificOperationStatus = categoryOperations[operationType];
406
+ if (specificOperationStatus && NON_COMPLETED_ONBOARDING_STATUSES.includes(specificOperationStatus.status)) {
407
+ return specificOperationStatus;
408
+ }
409
+ }
410
+ return undefined;
184
411
  }
185
412
  }
186
413