@structured-world/gitlab-mcp 6.62.2 → 7.0.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/README.md +22 -1
- package/README.md.in +21 -0
- package/dist/generated/prisma/internal/class.js +2 -2
- package/dist/generated/prisma/internal/prismaNamespace.js +2 -2
- package/dist/generated/prisma/models/AuthCodeFlowState.d.ts +1 -2
- package/dist/generated/prisma/models/AuthorizationCode.d.ts +1 -2
- package/dist/generated/prisma/models/DeviceFlowState.d.ts +1 -2
- package/dist/generated/prisma/models/McpSessionMapping.d.ts +1 -2
- package/dist/generated/prisma/models/OAuthSession.d.ts +1 -2
- package/dist/src/cli/init/connection.js +11 -11
- package/dist/src/cli/init/connection.js.map +1 -1
- package/dist/src/cli/instances/instances-command.js +5 -1
- package/dist/src/cli/instances/instances-command.js.map +1 -1
- package/dist/src/config.d.ts +7 -0
- package/dist/src/config.js +37 -36
- package/dist/src/config.js.map +1 -1
- package/dist/src/entities/pipelines/schema-readonly.d.ts +1 -1
- package/dist/src/handlers.d.ts +1 -0
- package/dist/src/handlers.js +210 -38
- package/dist/src/handlers.js.map +1 -1
- package/dist/src/logging/types.d.ts +1 -1
- package/dist/src/logging/types.js.map +1 -1
- package/dist/src/middleware/index.d.ts +1 -0
- package/dist/src/middleware/index.js +3 -1
- package/dist/src/middleware/index.js.map +1 -1
- package/dist/src/middleware/response-write-timeout.d.ts +2 -0
- package/dist/src/middleware/response-write-timeout.js +62 -0
- package/dist/src/middleware/response-write-timeout.js.map +1 -0
- package/dist/src/oauth/gitlab-device-flow.js +31 -26
- package/dist/src/oauth/gitlab-device-flow.js.map +1 -1
- package/dist/src/registry-manager.d.ts +20 -10
- package/dist/src/registry-manager.js +255 -113
- package/dist/src/registry-manager.js.map +1 -1
- package/dist/src/server.js +37 -21
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/ConnectionManager.d.ts +29 -20
- package/dist/src/services/ConnectionManager.js +265 -140
- package/dist/src/services/ConnectionManager.js.map +1 -1
- package/dist/src/services/HealthMonitor.d.ts +42 -0
- package/dist/src/services/HealthMonitor.js +544 -0
- package/dist/src/services/HealthMonitor.js.map +1 -0
- package/dist/src/services/InstanceRegistry.d.ts +0 -1
- package/dist/src/services/InstanceRegistry.js +9 -21
- package/dist/src/services/InstanceRegistry.js.map +1 -1
- package/dist/src/services/SchemaIntrospector.d.ts +1 -0
- package/dist/src/services/SchemaIntrospector.js +3 -0
- package/dist/src/services/SchemaIntrospector.js.map +1 -1
- package/dist/src/services/TokenScopeDetector.d.ts +2 -2
- package/dist/src/services/TokenScopeDetector.js +59 -36
- package/dist/src/services/TokenScopeDetector.js.map +1 -1
- package/dist/src/services/ToolAvailability.d.ts +9 -5
- package/dist/src/services/ToolAvailability.js +30 -10
- package/dist/src/services/ToolAvailability.js.map +1 -1
- package/dist/src/services/WidgetAvailability.d.ts +3 -3
- package/dist/src/services/WidgetAvailability.js +7 -6
- package/dist/src/services/WidgetAvailability.js.map +1 -1
- package/dist/src/utils/error-handler.d.ts +10 -1
- package/dist/src/utils/error-handler.js +85 -0
- package/dist/src/utils/error-handler.js.map +1 -1
- package/dist/src/utils/fetch.d.ts +7 -0
- package/dist/src/utils/fetch.js +53 -39
- package/dist/src/utils/fetch.js.map +1 -1
- package/dist/src/utils/gitlab-api.js +5 -2
- package/dist/src/utils/gitlab-api.js.map +1 -1
- package/dist/src/utils/url.d.ts +1 -0
- package/dist/src/utils/url.js +38 -0
- package/dist/src/utils/url.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +19 -25
- package/dist/structured-world-gitlab-mcp-6.62.2.tgz +0 -0
|
@@ -9,38 +9,59 @@ const config_1 = require("../config");
|
|
|
9
9
|
const index_1 = require("../oauth/index");
|
|
10
10
|
const fetch_1 = require("../utils/fetch");
|
|
11
11
|
const logger_1 = require("../logger");
|
|
12
|
-
const
|
|
12
|
+
const InstanceRegistry_1 = require("./InstanceRegistry");
|
|
13
|
+
const url_1 = require("../utils/url");
|
|
13
14
|
class ConnectionManager {
|
|
14
15
|
static instance = null;
|
|
15
|
-
|
|
16
|
-
versionDetector = null;
|
|
17
|
-
schemaIntrospector = null;
|
|
18
|
-
instanceInfo = null;
|
|
19
|
-
schemaInfo = null;
|
|
20
|
-
tokenScopeInfo = null;
|
|
21
|
-
isInitialized = false;
|
|
16
|
+
instances = new Map();
|
|
22
17
|
currentInstanceUrl = null;
|
|
23
|
-
introspectedInstanceUrl = null;
|
|
24
18
|
introspectionPromises = new Map();
|
|
25
19
|
static introspectionCache = new Map();
|
|
26
20
|
static CACHE_TTL = 10 * 60 * 1000;
|
|
21
|
+
initializePromises = new Map();
|
|
22
|
+
latestRequestedUrl = null;
|
|
27
23
|
constructor() { }
|
|
28
24
|
static getInstance() {
|
|
29
25
|
ConnectionManager.instance ??= new ConnectionManager();
|
|
30
26
|
return ConnectionManager.instance;
|
|
31
27
|
}
|
|
32
28
|
async initialize(instanceUrl) {
|
|
33
|
-
|
|
29
|
+
const url = (0, url_1.normalizeInstanceUrl)(instanceUrl ?? config_1.GITLAB_BASE_URL);
|
|
30
|
+
this.latestRequestedUrl = url;
|
|
31
|
+
const existing = this.instances.get(url);
|
|
32
|
+
if (existing?.isInitialized) {
|
|
33
|
+
this.currentInstanceUrl = url;
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
+
const inflight = this.initializePromises.get(url);
|
|
37
|
+
if (inflight) {
|
|
38
|
+
return inflight;
|
|
39
|
+
}
|
|
40
|
+
const promise = this.doInitialize(url);
|
|
41
|
+
this.initializePromises.set(url, promise);
|
|
42
|
+
try {
|
|
43
|
+
await promise;
|
|
44
|
+
const isOurPromise = this.initializePromises.get(url) === promise;
|
|
45
|
+
const initSucceeded = this.instances.get(url)?.isInitialized === true;
|
|
46
|
+
if ((isOurPromise && url === this.latestRequestedUrl) ||
|
|
47
|
+
(!this.currentInstanceUrl && initSucceeded)) {
|
|
48
|
+
this.currentInstanceUrl = url;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (this.initializePromises.get(url) === promise) {
|
|
53
|
+
this.initializePromises.delete(url);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async doInitialize(baseUrl) {
|
|
58
|
+
let state;
|
|
36
59
|
try {
|
|
37
60
|
const oauthMode = (0, index_1.isOAuthEnabled)();
|
|
38
|
-
const registry =
|
|
61
|
+
const registry = InstanceRegistry_1.InstanceRegistry.getInstance();
|
|
39
62
|
if (!registry.isInitialized()) {
|
|
40
63
|
await registry.initialize();
|
|
41
64
|
}
|
|
42
|
-
const baseUrl = instanceUrl ?? config_1.GITLAB_BASE_URL;
|
|
43
|
-
this.currentInstanceUrl = baseUrl;
|
|
44
65
|
if (!baseUrl) {
|
|
45
66
|
throw new Error('GitLab base URL is required');
|
|
46
67
|
}
|
|
@@ -54,19 +75,33 @@ class ConnectionManager {
|
|
|
54
75
|
const clientOptions = oauthMode
|
|
55
76
|
? {}
|
|
56
77
|
: { headers: { 'PRIVATE-TOKEN': String(config_1.GITLAB_TOKEN) } };
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
const client = new client_1.GraphQLClient(endpoint, clientOptions);
|
|
79
|
+
const versionDetector = new GitLabVersionDetector_1.GitLabVersionDetector(client);
|
|
80
|
+
const schemaIntrospector = new SchemaIntrospector_1.SchemaIntrospector(client);
|
|
81
|
+
state = {
|
|
82
|
+
client,
|
|
83
|
+
versionDetector,
|
|
84
|
+
schemaIntrospector,
|
|
85
|
+
instanceInfo: null,
|
|
86
|
+
schemaInfo: null,
|
|
87
|
+
tokenScopeInfo: null,
|
|
88
|
+
isInitialized: false,
|
|
89
|
+
introspectedInstanceUrl: null,
|
|
90
|
+
};
|
|
91
|
+
this.instances.set(baseUrl, state);
|
|
60
92
|
if (oauthMode) {
|
|
61
93
|
(0, logger_1.logInfo)('OAuth mode: attempting unauthenticated version detection');
|
|
62
94
|
try {
|
|
63
|
-
const versionResponse = await
|
|
95
|
+
const versionResponse = await (0, fetch_1.enhancedFetch)(`${baseUrl}/api/v4/version`, {
|
|
96
|
+
retry: false,
|
|
97
|
+
skipAuth: true,
|
|
98
|
+
});
|
|
64
99
|
if (versionResponse.ok) {
|
|
65
100
|
const versionData = (await versionResponse.json());
|
|
66
101
|
(0, logger_1.logInfo)('Detected GitLab version without authentication', {
|
|
67
102
|
version: versionData.version,
|
|
68
103
|
});
|
|
69
|
-
|
|
104
|
+
state.instanceInfo = {
|
|
70
105
|
version: versionData.version,
|
|
71
106
|
tier: versionData.enterprise ? 'premium' : 'free',
|
|
72
107
|
features: this.getDefaultFeatures(versionData.enterprise ?? false),
|
|
@@ -85,36 +120,43 @@ class ConnectionManager {
|
|
|
85
120
|
error: error instanceof Error ? error.message : String(error),
|
|
86
121
|
});
|
|
87
122
|
}
|
|
88
|
-
this.
|
|
123
|
+
if (this.instances.get(baseUrl) !== state)
|
|
124
|
+
return;
|
|
125
|
+
state.isInitialized = true;
|
|
89
126
|
return;
|
|
90
127
|
}
|
|
91
|
-
|
|
92
|
-
if (
|
|
128
|
+
state.tokenScopeInfo = await (0, TokenScopeDetector_1.detectTokenScopes)(baseUrl);
|
|
129
|
+
if (state.tokenScopeInfo) {
|
|
93
130
|
const totalTools = Object.keys((0, TokenScopeDetector_1.getToolScopeRequirements)()).length;
|
|
94
|
-
(0, TokenScopeDetector_1.logTokenScopeInfo)(
|
|
95
|
-
if (!
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
(0, TokenScopeDetector_1.logTokenScopeInfo)(state.tokenScopeInfo, totalTools, baseUrl);
|
|
132
|
+
if (!state.tokenScopeInfo.hasGraphQLAccess) {
|
|
133
|
+
state.instanceInfo = await this.detectVersionViaREST(baseUrl);
|
|
134
|
+
state.isInitialized = true;
|
|
98
135
|
return;
|
|
99
136
|
}
|
|
100
137
|
}
|
|
101
138
|
const cached = ConnectionManager.introspectionCache.get(endpoint);
|
|
102
139
|
const now = Date.now();
|
|
103
140
|
if (cached && now - cached.timestamp < ConnectionManager.CACHE_TTL) {
|
|
141
|
+
if (this.instances.get(baseUrl) !== state)
|
|
142
|
+
return;
|
|
104
143
|
(0, logger_1.logInfo)('Using cached GraphQL introspection data');
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
144
|
+
state.instanceInfo = cached.instanceInfo;
|
|
145
|
+
state.schemaInfo = cached.schemaInfo;
|
|
146
|
+
state.schemaIntrospector.rehydrate(cached.schemaInfo);
|
|
147
|
+
state.introspectedInstanceUrl = baseUrl;
|
|
108
148
|
}
|
|
109
149
|
else {
|
|
110
150
|
(0, logger_1.logDebug)('Introspecting GitLab GraphQL schema...');
|
|
111
151
|
const [instanceInfo, schemaInfo] = await Promise.all([
|
|
112
|
-
|
|
113
|
-
|
|
152
|
+
versionDetector.detectInstance(),
|
|
153
|
+
schemaIntrospector.introspectSchema(),
|
|
114
154
|
]);
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
155
|
+
if (this.instances.get(baseUrl) !== state)
|
|
156
|
+
return;
|
|
157
|
+
state.instanceInfo = instanceInfo;
|
|
158
|
+
state.schemaInfo = schemaInfo;
|
|
159
|
+
state.introspectedInstanceUrl = baseUrl;
|
|
118
160
|
ConnectionManager.introspectionCache.set(endpoint, {
|
|
119
161
|
instanceInfo,
|
|
120
162
|
schemaInfo,
|
|
@@ -122,30 +164,37 @@ class ConnectionManager {
|
|
|
122
164
|
});
|
|
123
165
|
(0, logger_1.logInfo)('GraphQL schema introspection completed');
|
|
124
166
|
}
|
|
125
|
-
|
|
167
|
+
state.isInitialized = true;
|
|
126
168
|
(0, logger_1.logInfo)('GitLab instance and schema detected', {
|
|
127
|
-
version:
|
|
128
|
-
tier:
|
|
129
|
-
features:
|
|
130
|
-
? Object.entries(
|
|
169
|
+
version: state.instanceInfo?.version,
|
|
170
|
+
tier: state.instanceInfo?.tier,
|
|
171
|
+
features: state.instanceInfo
|
|
172
|
+
? Object.entries(state.instanceInfo.features)
|
|
131
173
|
.filter(([, enabled]) => enabled)
|
|
132
174
|
.map(([feature]) => feature)
|
|
133
175
|
: [],
|
|
134
|
-
widgetTypes:
|
|
135
|
-
schemaTypes:
|
|
176
|
+
widgetTypes: state.schemaInfo?.workItemWidgetTypes.length || 0,
|
|
177
|
+
schemaTypes: state.schemaInfo?.typeDefinitions.size || 0,
|
|
136
178
|
});
|
|
137
179
|
}
|
|
138
180
|
catch (error) {
|
|
181
|
+
if (state && this.instances.get(baseUrl) === state) {
|
|
182
|
+
this.instances.delete(baseUrl);
|
|
183
|
+
}
|
|
139
184
|
(0, logger_1.logError)('Failed to initialize connection', { err: error });
|
|
140
185
|
throw error;
|
|
141
186
|
}
|
|
142
187
|
}
|
|
143
|
-
async ensureIntrospected() {
|
|
144
|
-
|
|
188
|
+
async ensureIntrospected(explicitUrl) {
|
|
189
|
+
const instanceUrl = (0, url_1.normalizeInstanceUrl)(explicitUrl ?? (0, index_1.getGitLabApiUrlFromContext)() ?? this.currentInstanceUrl ?? config_1.GITLAB_BASE_URL);
|
|
190
|
+
const state = this.instances.get(instanceUrl);
|
|
191
|
+
if (!state?.client || !state.versionDetector || !state.schemaIntrospector) {
|
|
145
192
|
throw new Error('Connection not initialized. Call initialize() first.');
|
|
146
193
|
}
|
|
147
|
-
|
|
148
|
-
|
|
194
|
+
if (state.instanceInfo && state.schemaInfo && state.introspectedInstanceUrl === instanceUrl) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (state.tokenScopeInfo && !state.tokenScopeInfo.hasGraphQLAccess) {
|
|
149
198
|
return;
|
|
150
199
|
}
|
|
151
200
|
const existingPromise = this.introspectionPromises.get(instanceUrl);
|
|
@@ -160,34 +209,37 @@ class ConnectionManager {
|
|
|
160
209
|
await promise;
|
|
161
210
|
}
|
|
162
211
|
finally {
|
|
163
|
-
this.introspectionPromises.
|
|
212
|
+
if (this.introspectionPromises.get(instanceUrl) === promise) {
|
|
213
|
+
this.introspectionPromises.delete(instanceUrl);
|
|
214
|
+
}
|
|
164
215
|
}
|
|
165
216
|
}
|
|
166
217
|
async doIntrospection(instanceUrl) {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
const schemaDet = this.schemaIntrospector;
|
|
170
|
-
if (!client || !versionDet || !schemaDet) {
|
|
218
|
+
const state = this.instances.get(instanceUrl);
|
|
219
|
+
if (!state?.client || !state.versionDetector || !state.schemaIntrospector) {
|
|
171
220
|
throw new Error('Connection not initialized. Call initialize() first.');
|
|
172
221
|
}
|
|
222
|
+
const { client, versionDetector, schemaIntrospector } = state;
|
|
173
223
|
const endpoint = client.endpoint;
|
|
174
|
-
const registry =
|
|
224
|
+
const registry = InstanceRegistry_1.InstanceRegistry.getInstance();
|
|
175
225
|
if (registry.isInitialized()) {
|
|
176
226
|
const cachedIntrospection = registry.getIntrospection(instanceUrl);
|
|
177
227
|
if (cachedIntrospection) {
|
|
178
228
|
(0, logger_1.logInfo)('Using cached introspection from InstanceRegistry', { url: instanceUrl });
|
|
179
|
-
|
|
229
|
+
state.instanceInfo = {
|
|
180
230
|
version: cachedIntrospection.version,
|
|
181
231
|
tier: cachedIntrospection.tier,
|
|
182
232
|
features: cachedIntrospection.features,
|
|
183
233
|
detectedAt: cachedIntrospection.cachedAt,
|
|
184
234
|
};
|
|
185
|
-
|
|
186
|
-
|
|
235
|
+
const restoredSchema = cachedIntrospection.schemaInfo;
|
|
236
|
+
state.schemaInfo = restoredSchema;
|
|
237
|
+
state.schemaIntrospector.rehydrate(restoredSchema);
|
|
238
|
+
state.introspectedInstanceUrl = instanceUrl;
|
|
187
239
|
return;
|
|
188
240
|
}
|
|
189
241
|
}
|
|
190
|
-
const primaryCacheKey = instanceUrl
|
|
242
|
+
const primaryCacheKey = instanceUrl;
|
|
191
243
|
const legacyCacheKey = endpoint;
|
|
192
244
|
let cached = ConnectionManager.introspectionCache.get(primaryCacheKey);
|
|
193
245
|
if (!cached && primaryCacheKey !== legacyCacheKey) {
|
|
@@ -196,29 +248,22 @@ class ConnectionManager {
|
|
|
196
248
|
const now = Date.now();
|
|
197
249
|
if (cached && now - cached.timestamp < ConnectionManager.CACHE_TTL) {
|
|
198
250
|
(0, logger_1.logInfo)('Using cached GraphQL introspection data');
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
251
|
+
state.instanceInfo = cached.instanceInfo;
|
|
252
|
+
state.schemaInfo = cached.schemaInfo;
|
|
253
|
+
state.schemaIntrospector.rehydrate(cached.schemaInfo);
|
|
254
|
+
state.introspectedInstanceUrl = instanceUrl;
|
|
202
255
|
return;
|
|
203
256
|
}
|
|
204
257
|
(0, logger_1.logDebug)('Introspecting GitLab GraphQL schema (deferred OAuth mode)...');
|
|
205
|
-
let versionDetector = versionDet;
|
|
206
|
-
let schemaIntrospector = schemaDet;
|
|
207
|
-
if (registry.isInitialized() && instanceUrl !== this.currentInstanceUrl) {
|
|
208
|
-
const instanceClient = this.getInstanceClient(instanceUrl);
|
|
209
|
-
if (instanceClient !== client) {
|
|
210
|
-
versionDetector = new GitLabVersionDetector_1.GitLabVersionDetector(instanceClient);
|
|
211
|
-
schemaIntrospector = new SchemaIntrospector_1.SchemaIntrospector(instanceClient);
|
|
212
|
-
(0, logger_1.logDebug)('Using per-instance detectors for introspection', { instanceUrl });
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
258
|
const [instanceInfo, schemaInfo] = await Promise.all([
|
|
216
259
|
versionDetector.detectInstance(),
|
|
217
260
|
schemaIntrospector.introspectSchema(),
|
|
218
261
|
]);
|
|
219
|
-
this.
|
|
220
|
-
|
|
221
|
-
|
|
262
|
+
if (this.instances.get(instanceUrl) !== state)
|
|
263
|
+
return;
|
|
264
|
+
state.instanceInfo = instanceInfo;
|
|
265
|
+
state.schemaInfo = schemaInfo;
|
|
266
|
+
state.introspectedInstanceUrl = instanceUrl;
|
|
222
267
|
ConnectionManager.introspectionCache.set(primaryCacheKey, {
|
|
223
268
|
instanceInfo,
|
|
224
269
|
schemaInfo,
|
|
@@ -235,93 +280,131 @@ class ConnectionManager {
|
|
|
235
280
|
registry.setIntrospection(instanceUrl, cachedIntrospection);
|
|
236
281
|
}
|
|
237
282
|
(0, logger_1.logInfo)('GraphQL schema introspection completed (deferred)', {
|
|
238
|
-
version:
|
|
239
|
-
tier:
|
|
240
|
-
widgetTypes:
|
|
283
|
+
version: state.instanceInfo?.version,
|
|
284
|
+
tier: state.instanceInfo?.tier,
|
|
285
|
+
widgetTypes: state.schemaInfo?.workItemWidgetTypes.length || 0,
|
|
241
286
|
});
|
|
242
287
|
}
|
|
243
|
-
|
|
244
|
-
|
|
288
|
+
resolveState(instanceUrl) {
|
|
289
|
+
const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
|
|
290
|
+
if (!url) {
|
|
245
291
|
throw new Error('Connection not initialized. Call initialize() first.');
|
|
246
292
|
}
|
|
247
|
-
|
|
293
|
+
const state = this.instances.get(url);
|
|
294
|
+
if (!state) {
|
|
295
|
+
throw new Error(`Connection not initialized for ${url}. Call initialize() first.`);
|
|
296
|
+
}
|
|
297
|
+
return [state, url];
|
|
298
|
+
}
|
|
299
|
+
getClient(instanceUrl) {
|
|
300
|
+
const [state] = this.resolveState(instanceUrl);
|
|
301
|
+
return state.client;
|
|
248
302
|
}
|
|
249
303
|
getInstanceClient(instanceUrl, authHeaders) {
|
|
250
|
-
const registry =
|
|
251
|
-
const
|
|
304
|
+
const registry = InstanceRegistry_1.InstanceRegistry.getInstance();
|
|
305
|
+
const rawTargetUrl = instanceUrl ?? (0, index_1.getGitLabApiUrlFromContext)() ?? this.currentInstanceUrl;
|
|
306
|
+
const targetUrl = rawTargetUrl ? (0, url_1.normalizeInstanceUrl)(rawTargetUrl) : null;
|
|
252
307
|
if (targetUrl && registry.isInitialized() && registry.has(targetUrl)) {
|
|
253
308
|
const client = registry.getGraphQLClient(targetUrl, authHeaders);
|
|
254
309
|
if (client) {
|
|
255
310
|
return client;
|
|
256
311
|
}
|
|
257
312
|
}
|
|
258
|
-
if (
|
|
259
|
-
|
|
313
|
+
if (targetUrl) {
|
|
314
|
+
const state = this.instances.get(targetUrl);
|
|
315
|
+
if (state)
|
|
316
|
+
return state.client;
|
|
317
|
+
throw new Error(`Connection not initialized for ${targetUrl}. Call initialize() first.`);
|
|
260
318
|
}
|
|
261
|
-
return this.
|
|
319
|
+
return this.getClient();
|
|
262
320
|
}
|
|
263
|
-
getVersionDetector() {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
return this.versionDetector;
|
|
321
|
+
getVersionDetector(instanceUrl) {
|
|
322
|
+
const [state] = this.resolveState(instanceUrl);
|
|
323
|
+
return state.versionDetector;
|
|
268
324
|
}
|
|
269
|
-
getSchemaIntrospector() {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
return this.schemaIntrospector;
|
|
325
|
+
getSchemaIntrospector(instanceUrl) {
|
|
326
|
+
const [state] = this.resolveState(instanceUrl);
|
|
327
|
+
return state.schemaIntrospector;
|
|
274
328
|
}
|
|
275
|
-
getInstanceInfo() {
|
|
276
|
-
|
|
277
|
-
|
|
329
|
+
getInstanceInfo(instanceUrl) {
|
|
330
|
+
const [state, resolvedUrl] = this.resolveState(instanceUrl);
|
|
331
|
+
if (!state.instanceInfo) {
|
|
332
|
+
throw new Error(`Instance information is not available for ${resolvedUrl}. Initialization may have completed without version detection (OAuth deferred/REST-only mode).`);
|
|
278
333
|
}
|
|
279
|
-
return
|
|
334
|
+
return state.instanceInfo;
|
|
280
335
|
}
|
|
281
|
-
getSchemaInfo() {
|
|
282
|
-
|
|
283
|
-
|
|
336
|
+
getSchemaInfo(instanceUrl) {
|
|
337
|
+
const [state, resolvedUrl] = this.resolveState(instanceUrl);
|
|
338
|
+
if (!state.schemaInfo) {
|
|
339
|
+
throw new Error(`Schema information is not available for ${resolvedUrl}. Initialization may have completed without schema introspection.`);
|
|
284
340
|
}
|
|
285
|
-
return
|
|
341
|
+
return state.schemaInfo;
|
|
286
342
|
}
|
|
287
343
|
getCurrentInstanceUrl() {
|
|
288
344
|
return this.currentInstanceUrl;
|
|
289
345
|
}
|
|
290
|
-
|
|
291
|
-
|
|
346
|
+
isConnected(instanceUrl) {
|
|
347
|
+
const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
|
|
348
|
+
if (!url)
|
|
292
349
|
return false;
|
|
293
|
-
|
|
294
|
-
return
|
|
350
|
+
const state = this.instances.get(url);
|
|
351
|
+
return state?.isInitialized ?? false;
|
|
352
|
+
}
|
|
353
|
+
isFeatureAvailable(feature, instanceUrl) {
|
|
354
|
+
const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
|
|
355
|
+
if (!url)
|
|
356
|
+
return false;
|
|
357
|
+
const state = this.instances.get(url);
|
|
358
|
+
if (!state?.instanceInfo)
|
|
359
|
+
return false;
|
|
360
|
+
return state.instanceInfo.features[feature];
|
|
295
361
|
}
|
|
296
|
-
getTier() {
|
|
297
|
-
|
|
362
|
+
getTier(instanceUrl) {
|
|
363
|
+
const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
|
|
364
|
+
if (!url)
|
|
298
365
|
return 'unknown';
|
|
299
|
-
|
|
300
|
-
|
|
366
|
+
const state = this.instances.get(url);
|
|
367
|
+
if (!state?.instanceInfo)
|
|
368
|
+
return 'unknown';
|
|
369
|
+
return state.instanceInfo.tier;
|
|
301
370
|
}
|
|
302
|
-
getVersion() {
|
|
303
|
-
|
|
371
|
+
getVersion(instanceUrl) {
|
|
372
|
+
const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
|
|
373
|
+
if (!url)
|
|
304
374
|
return 'unknown';
|
|
305
|
-
|
|
306
|
-
|
|
375
|
+
const state = this.instances.get(url);
|
|
376
|
+
if (!state?.instanceInfo)
|
|
377
|
+
return 'unknown';
|
|
378
|
+
return state.instanceInfo.version;
|
|
307
379
|
}
|
|
308
|
-
isWidgetAvailable(widgetType) {
|
|
309
|
-
|
|
380
|
+
isWidgetAvailable(widgetType, instanceUrl) {
|
|
381
|
+
const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
|
|
382
|
+
if (!url)
|
|
310
383
|
return false;
|
|
311
|
-
|
|
312
|
-
return
|
|
384
|
+
const state = this.instances.get(url);
|
|
385
|
+
return state?.schemaInfo?.workItemWidgetTypes.includes(widgetType) ?? false;
|
|
313
386
|
}
|
|
314
|
-
getTokenScopeInfo() {
|
|
315
|
-
|
|
387
|
+
getTokenScopeInfo(instanceUrl) {
|
|
388
|
+
const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
|
|
389
|
+
if (!url)
|
|
390
|
+
return null;
|
|
391
|
+
const state = this.instances.get(url);
|
|
392
|
+
return state?.tokenScopeInfo ?? null;
|
|
316
393
|
}
|
|
317
394
|
async refreshTokenScopes() {
|
|
318
395
|
if ((0, index_1.isOAuthEnabled)()) {
|
|
319
396
|
return false;
|
|
320
397
|
}
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
398
|
+
const url = this.currentInstanceUrl;
|
|
399
|
+
if (!url)
|
|
400
|
+
return false;
|
|
401
|
+
const state = this.instances.get(url);
|
|
402
|
+
if (!state)
|
|
403
|
+
return false;
|
|
404
|
+
const previousScopes = state.tokenScopeInfo?.scopes ?? [];
|
|
405
|
+
const previousHasGraphQL = state.tokenScopeInfo?.hasGraphQLAccess ?? false;
|
|
406
|
+
const previousHasWrite = state.tokenScopeInfo?.hasWriteAccess ?? false;
|
|
407
|
+
const newScopeInfo = await (0, TokenScopeDetector_1.detectTokenScopes)(url);
|
|
325
408
|
if (!newScopeInfo) {
|
|
326
409
|
return false;
|
|
327
410
|
}
|
|
@@ -330,8 +413,12 @@ class ConnectionManager {
|
|
|
330
413
|
!previousScopes.every((s) => newScopes.includes(s)) ||
|
|
331
414
|
previousHasGraphQL !== newScopeInfo.hasGraphQLAccess ||
|
|
332
415
|
previousHasWrite !== newScopeInfo.hasWriteAccess;
|
|
416
|
+
const currentState = this.instances.get(url);
|
|
417
|
+
if (currentState !== state) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
state.tokenScopeInfo = newScopeInfo;
|
|
333
421
|
if (scopesChanged) {
|
|
334
|
-
this.tokenScopeInfo = newScopeInfo;
|
|
335
422
|
(0, logger_1.logInfo)('Token scopes changed - tool registry will be refreshed', {
|
|
336
423
|
previousScopes,
|
|
337
424
|
newScopes,
|
|
@@ -341,10 +428,10 @@ class ConnectionManager {
|
|
|
341
428
|
}
|
|
342
429
|
return scopesChanged;
|
|
343
430
|
}
|
|
344
|
-
async detectVersionViaREST() {
|
|
431
|
+
async detectVersionViaREST(baseUrl) {
|
|
345
432
|
try {
|
|
346
|
-
const
|
|
347
|
-
const response = await (0, fetch_1.enhancedFetch)(`${
|
|
433
|
+
const url = baseUrl ?? this.currentInstanceUrl ?? config_1.GITLAB_BASE_URL;
|
|
434
|
+
const response = await (0, fetch_1.enhancedFetch)(`${url}/api/v4/version`, {
|
|
348
435
|
headers: {
|
|
349
436
|
'PRIVATE-TOKEN': config_1.GITLAB_TOKEN ?? '',
|
|
350
437
|
Accept: 'application/json',
|
|
@@ -407,31 +494,69 @@ class ConnectionManager {
|
|
|
407
494
|
emailParticipants: true,
|
|
408
495
|
};
|
|
409
496
|
}
|
|
410
|
-
async reinitialize(
|
|
497
|
+
async reinitialize(rawInstanceUrl) {
|
|
498
|
+
const newInstanceUrl = (0, url_1.normalizeInstanceUrl)(rawInstanceUrl);
|
|
411
499
|
(0, logger_1.logInfo)('Re-initializing ConnectionManager for new instance', {
|
|
412
500
|
newInstanceUrl,
|
|
413
501
|
});
|
|
414
|
-
this.
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
502
|
+
const previousUrl = this.currentInstanceUrl;
|
|
503
|
+
const savedState = this.instances.get(newInstanceUrl);
|
|
504
|
+
const restorableState = savedState?.isInitialized ? savedState : undefined;
|
|
505
|
+
this.initializePromises.delete(newInstanceUrl);
|
|
506
|
+
this.introspectionPromises.delete(newInstanceUrl);
|
|
507
|
+
this.instances.delete(newInstanceUrl);
|
|
508
|
+
try {
|
|
509
|
+
const registry = InstanceRegistry_1.InstanceRegistry.getInstance();
|
|
510
|
+
registry.clearIntrospectionCache(newInstanceUrl);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
}
|
|
514
|
+
ConnectionManager.introspectionCache.delete(newInstanceUrl);
|
|
515
|
+
ConnectionManager.introspectionCache.delete(`${newInstanceUrl}/api/graphql`);
|
|
516
|
+
try {
|
|
517
|
+
await this.initialize(newInstanceUrl);
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
if (restorableState) {
|
|
521
|
+
this.instances.set(newInstanceUrl, restorableState);
|
|
522
|
+
}
|
|
523
|
+
if (previousUrl && this.instances.has(previousUrl)) {
|
|
524
|
+
this.currentInstanceUrl = previousUrl;
|
|
525
|
+
}
|
|
526
|
+
else if (restorableState) {
|
|
527
|
+
this.currentInstanceUrl = newInstanceUrl;
|
|
528
|
+
}
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
if (previousUrl && previousUrl !== newInstanceUrl) {
|
|
532
|
+
this.initializePromises.delete(previousUrl);
|
|
533
|
+
this.introspectionPromises.delete(previousUrl);
|
|
534
|
+
this.instances.delete(previousUrl);
|
|
535
|
+
}
|
|
536
|
+
const state = this.instances.get(newInstanceUrl);
|
|
418
537
|
(0, logger_1.logInfo)('ConnectionManager re-initialized', {
|
|
419
|
-
version:
|
|
420
|
-
tier:
|
|
538
|
+
version: state?.instanceInfo?.version,
|
|
539
|
+
tier: state?.instanceInfo?.tier,
|
|
421
540
|
instanceUrl: this.currentInstanceUrl,
|
|
422
541
|
});
|
|
423
542
|
}
|
|
543
|
+
clearInflight(rawUrl) {
|
|
544
|
+
const url = (0, url_1.normalizeInstanceUrl)(rawUrl);
|
|
545
|
+
this.initializePromises.delete(url);
|
|
546
|
+
this.introspectionPromises.delete(url);
|
|
547
|
+
}
|
|
424
548
|
reset() {
|
|
425
|
-
this.
|
|
426
|
-
this.versionDetector = null;
|
|
427
|
-
this.schemaIntrospector = null;
|
|
428
|
-
this.instanceInfo = null;
|
|
429
|
-
this.schemaInfo = null;
|
|
430
|
-
this.tokenScopeInfo = null;
|
|
549
|
+
this.instances.clear();
|
|
431
550
|
this.currentInstanceUrl = null;
|
|
432
|
-
this.
|
|
551
|
+
this.latestRequestedUrl = null;
|
|
433
552
|
this.introspectionPromises.clear();
|
|
434
|
-
this.
|
|
553
|
+
this.initializePromises.clear();
|
|
554
|
+
ConnectionManager.introspectionCache.clear();
|
|
555
|
+
try {
|
|
556
|
+
InstanceRegistry_1.InstanceRegistry.getInstance().clearIntrospectionCache();
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
}
|
|
435
560
|
}
|
|
436
561
|
}
|
|
437
562
|
exports.ConnectionManager = ConnectionManager;
|