bktide 0.0.1

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 (160) hide show
  1. package/README.md +145 -0
  2. package/WORKFLOW_README.md +65 -0
  3. package/bin/alfred-entrypoint +54 -0
  4. package/dist/commands/BaseCommand.js +159 -0
  5. package/dist/commands/BaseCommand.js.map +1 -0
  6. package/dist/commands/BaseCommandHandler.js +80 -0
  7. package/dist/commands/BaseCommandHandler.js.map +1 -0
  8. package/dist/commands/BuildCommandHandler.js +28 -0
  9. package/dist/commands/BuildCommandHandler.js.map +1 -0
  10. package/dist/commands/HelloCommandHandler.js +6 -0
  11. package/dist/commands/HelloCommandHandler.js.map +1 -0
  12. package/dist/commands/ListAnnotations.js +60 -0
  13. package/dist/commands/ListAnnotations.js.map +1 -0
  14. package/dist/commands/ListBuilds.js +137 -0
  15. package/dist/commands/ListBuilds.js.map +1 -0
  16. package/dist/commands/ListOrganizations.js +27 -0
  17. package/dist/commands/ListOrganizations.js.map +1 -0
  18. package/dist/commands/ListPipelines.js +114 -0
  19. package/dist/commands/ListPipelines.js.map +1 -0
  20. package/dist/commands/ManageToken.js +180 -0
  21. package/dist/commands/ManageToken.js.map +1 -0
  22. package/dist/commands/OrganizationCommandHandler.js +53 -0
  23. package/dist/commands/OrganizationCommandHandler.js.map +1 -0
  24. package/dist/commands/PipelineCommandHandler.js +142 -0
  25. package/dist/commands/PipelineCommandHandler.js.map +1 -0
  26. package/dist/commands/ShowViewer.js +26 -0
  27. package/dist/commands/ShowViewer.js.map +1 -0
  28. package/dist/commands/UserBuildsCommandHandler.js +61 -0
  29. package/dist/commands/UserBuildsCommandHandler.js.map +1 -0
  30. package/dist/commands/ViewerBuildsCommandHandler.js +176 -0
  31. package/dist/commands/ViewerBuildsCommandHandler.js.map +1 -0
  32. package/dist/commands/ViewerCommandHandler.js +46 -0
  33. package/dist/commands/ViewerCommandHandler.js.map +1 -0
  34. package/dist/commands/index.js +8 -0
  35. package/dist/commands/index.js.map +1 -0
  36. package/dist/formatters/BaseFormatter.js +14 -0
  37. package/dist/formatters/BaseFormatter.js.map +1 -0
  38. package/dist/formatters/FormatterFactory.js +48 -0
  39. package/dist/formatters/FormatterFactory.js.map +1 -0
  40. package/dist/formatters/annotations/Formatter.js +10 -0
  41. package/dist/formatters/annotations/Formatter.js.map +1 -0
  42. package/dist/formatters/annotations/JsonFormatter.js +20 -0
  43. package/dist/formatters/annotations/JsonFormatter.js.map +1 -0
  44. package/dist/formatters/annotations/PlainTextFormatter.js +35 -0
  45. package/dist/formatters/annotations/PlainTextFormatter.js.map +1 -0
  46. package/dist/formatters/annotations/index.js +23 -0
  47. package/dist/formatters/annotations/index.js.map +1 -0
  48. package/dist/formatters/builds/AlfredFormatter.js +135 -0
  49. package/dist/formatters/builds/AlfredFormatter.js.map +1 -0
  50. package/dist/formatters/builds/Formatter.js +10 -0
  51. package/dist/formatters/builds/Formatter.js.map +1 -0
  52. package/dist/formatters/builds/JsonFormatter.js +44 -0
  53. package/dist/formatters/builds/JsonFormatter.js.map +1 -0
  54. package/dist/formatters/builds/PlainTextFormatter.js +113 -0
  55. package/dist/formatters/builds/PlainTextFormatter.js.map +1 -0
  56. package/dist/formatters/builds/index.js +26 -0
  57. package/dist/formatters/builds/index.js.map +1 -0
  58. package/dist/formatters/errors/AlfredFormatter.js +110 -0
  59. package/dist/formatters/errors/AlfredFormatter.js.map +1 -0
  60. package/dist/formatters/errors/Formatter.js +98 -0
  61. package/dist/formatters/errors/Formatter.js.map +1 -0
  62. package/dist/formatters/errors/JsonFormatter.js +63 -0
  63. package/dist/formatters/errors/JsonFormatter.js.map +1 -0
  64. package/dist/formatters/errors/PlainTextFormatter.js +52 -0
  65. package/dist/formatters/errors/PlainTextFormatter.js.map +1 -0
  66. package/dist/formatters/errors/index.js +26 -0
  67. package/dist/formatters/errors/index.js.map +1 -0
  68. package/dist/formatters/index.js +9 -0
  69. package/dist/formatters/index.js.map +1 -0
  70. package/dist/formatters/organizations/Formatter.js +10 -0
  71. package/dist/formatters/organizations/Formatter.js.map +1 -0
  72. package/dist/formatters/organizations/JsonFormatter.js +16 -0
  73. package/dist/formatters/organizations/JsonFormatter.js.map +1 -0
  74. package/dist/formatters/organizations/PlainTextFormatter.js +15 -0
  75. package/dist/formatters/organizations/PlainTextFormatter.js.map +1 -0
  76. package/dist/formatters/organizations/index.js +21 -0
  77. package/dist/formatters/organizations/index.js.map +1 -0
  78. package/dist/formatters/pipelines/AlfredFormatter.js +42 -0
  79. package/dist/formatters/pipelines/AlfredFormatter.js.map +1 -0
  80. package/dist/formatters/pipelines/Formatter.js +10 -0
  81. package/dist/formatters/pipelines/Formatter.js.map +1 -0
  82. package/dist/formatters/pipelines/JsonFormatter.js +13 -0
  83. package/dist/formatters/pipelines/JsonFormatter.js.map +1 -0
  84. package/dist/formatters/pipelines/PlainTextFormatter.js +47 -0
  85. package/dist/formatters/pipelines/PlainTextFormatter.js.map +1 -0
  86. package/dist/formatters/pipelines/index.js +28 -0
  87. package/dist/formatters/pipelines/index.js.map +1 -0
  88. package/dist/formatters/token/AlfredFormatter.js +191 -0
  89. package/dist/formatters/token/AlfredFormatter.js.map +1 -0
  90. package/dist/formatters/token/Formatter.js +13 -0
  91. package/dist/formatters/token/Formatter.js.map +1 -0
  92. package/dist/formatters/token/JsonFormatter.js +211 -0
  93. package/dist/formatters/token/JsonFormatter.js.map +1 -0
  94. package/dist/formatters/token/PlainTextFormatter.js +184 -0
  95. package/dist/formatters/token/PlainTextFormatter.js.map +1 -0
  96. package/dist/formatters/token/index.js +26 -0
  97. package/dist/formatters/token/index.js.map +1 -0
  98. package/dist/formatters/viewer/Formatter.js +10 -0
  99. package/dist/formatters/viewer/Formatter.js.map +1 -0
  100. package/dist/formatters/viewer/JsonFormatter.js +20 -0
  101. package/dist/formatters/viewer/JsonFormatter.js.map +1 -0
  102. package/dist/formatters/viewer/PlainTextFormatter.js +20 -0
  103. package/dist/formatters/viewer/PlainTextFormatter.js.map +1 -0
  104. package/dist/formatters/viewer/index.js +21 -0
  105. package/dist/formatters/viewer/index.js.map +1 -0
  106. package/dist/graphql/generated/fragment-masking.js +17 -0
  107. package/dist/graphql/generated/fragment-masking.js.map +1 -0
  108. package/dist/graphql/generated/gql.js +13 -0
  109. package/dist/graphql/generated/gql.js.map +1 -0
  110. package/dist/graphql/generated/graphql.js +852 -0
  111. package/dist/graphql/generated/graphql.js.map +1 -0
  112. package/dist/graphql/generated/index.js +3 -0
  113. package/dist/graphql/generated/index.js.map +1 -0
  114. package/dist/graphql/generated/sdk.js +872 -0
  115. package/dist/graphql/generated/sdk.js.map +1 -0
  116. package/dist/graphql/queries.js +138 -0
  117. package/dist/graphql/queries.js.map +1 -0
  118. package/dist/index.js +271 -0
  119. package/dist/index.js.map +1 -0
  120. package/dist/services/BuildkiteClient.js +520 -0
  121. package/dist/services/BuildkiteClient.js.map +1 -0
  122. package/dist/services/BuildkiteRestClient.js +244 -0
  123. package/dist/services/BuildkiteRestClient.js.map +1 -0
  124. package/dist/services/CacheManager.js +221 -0
  125. package/dist/services/CacheManager.js.map +1 -0
  126. package/dist/services/CredentialManager.js +158 -0
  127. package/dist/services/CredentialManager.js.map +1 -0
  128. package/dist/services/EnhancedBuildkiteClient.js +297 -0
  129. package/dist/services/EnhancedBuildkiteClient.js.map +1 -0
  130. package/dist/services/logger.js +107 -0
  131. package/dist/services/logger.js.map +1 -0
  132. package/dist/types/buildkite.js +5 -0
  133. package/dist/types/buildkite.js.map +1 -0
  134. package/dist/types/credentials.js +2 -0
  135. package/dist/types/credentials.js.map +1 -0
  136. package/dist/types/index.js +3 -0
  137. package/dist/types/index.js.map +1 -0
  138. package/dist/utils/cli-error-handler.js +172 -0
  139. package/dist/utils/cli-error-handler.js.map +1 -0
  140. package/dist/utils/errorUtils.js +59 -0
  141. package/dist/utils/errorUtils.js.map +1 -0
  142. package/dist/utils/parseBuildRef.js +31 -0
  143. package/dist/utils/parseBuildRef.js.map +1 -0
  144. package/dist/utils/textFormatter.js +53 -0
  145. package/dist/utils/textFormatter.js.map +1 -0
  146. package/dist/utils/xdgPaths.js +95 -0
  147. package/dist/utils/xdgPaths.js.map +1 -0
  148. package/env.example +66 -0
  149. package/icons/README.md +68 -0
  150. package/icons/blocked.png +0 -0
  151. package/icons/buildkite.png +0 -0
  152. package/icons/failed.png +0 -0
  153. package/icons/failing.png +0 -0
  154. package/icons/passed.png +0 -0
  155. package/icons/running.png +0 -0
  156. package/icons/scheduled.png +0 -0
  157. package/icons/skipped.png +0 -0
  158. package/icons/unknown.png +0 -0
  159. package/info.plist +734 -0
  160. package/package.json +87 -0
@@ -0,0 +1,244 @@
1
+ import fetch from 'node-fetch';
2
+ import { CacheManager } from './CacheManager.js';
3
+ import { createHash } from 'crypto';
4
+ import { logger } from './logger.js';
5
+ /**
6
+ * BuildkiteRestClient provides methods to interact with the Buildkite REST API
7
+ */
8
+ export class BuildkiteRestClient {
9
+ token;
10
+ baseUrl = 'https://api.buildkite.com/v2';
11
+ cacheManager = null;
12
+ debug = false;
13
+ rateLimitInfo = null;
14
+ /**
15
+ * Create a new BuildkiteRestClient
16
+ * @param token Your Buildkite API token
17
+ * @param options Configuration options
18
+ */
19
+ constructor(token, options) {
20
+ this.token = token;
21
+ this.debug = options?.debug || false;
22
+ if (options?.baseUrl) {
23
+ this.baseUrl = options.baseUrl;
24
+ }
25
+ // Initialize cache if caching is enabled
26
+ if (options?.caching !== false) {
27
+ this.cacheManager = new CacheManager(options?.cacheTTLs, this.debug);
28
+ // Initialize cache and set token hash (async, but we don't wait)
29
+ this.initCache();
30
+ }
31
+ }
32
+ /**
33
+ * Initialize cache asynchronously
34
+ */
35
+ async initCache() {
36
+ if (this.cacheManager) {
37
+ await this.cacheManager.init();
38
+ await this.cacheManager.setTokenHash(this.token);
39
+ }
40
+ }
41
+ /**
42
+ * Generate a cache key for a REST endpoint
43
+ */
44
+ generateCacheKey(endpoint, params) {
45
+ const paramsString = params ? JSON.stringify(params) : '';
46
+ return `REST:${endpoint}:${this.hashString(paramsString)}`;
47
+ }
48
+ /**
49
+ * Hash a string using SHA256
50
+ */
51
+ hashString(str) {
52
+ return createHash('sha256').update(str).digest('hex');
53
+ }
54
+ /**
55
+ * Get cache type from endpoint
56
+ */
57
+ getCacheTypeFromEndpoint(endpoint) {
58
+ if (endpoint.includes('/builds')) {
59
+ return 'builds';
60
+ }
61
+ return 'default';
62
+ }
63
+ /**
64
+ * Make a GET request to the Buildkite REST API
65
+ * @param endpoint The API endpoint
66
+ * @param params Query parameters
67
+ * @returns The API response
68
+ */
69
+ async get(endpoint, params) {
70
+ const url = new URL(`${this.baseUrl}${endpoint}`);
71
+ // Add query parameters
72
+ if (params) {
73
+ Object.entries(params).forEach(([key, value]) => {
74
+ if (value !== undefined && value !== null) {
75
+ url.searchParams.append(key, value);
76
+ }
77
+ });
78
+ }
79
+ // Generate cache key
80
+ const cacheKey = this.generateCacheKey(endpoint, params);
81
+ const cacheType = this.getCacheTypeFromEndpoint(endpoint);
82
+ // Check cache first if enabled
83
+ if (this.cacheManager) {
84
+ const cached = await this.cacheManager.get(cacheKey, cacheType);
85
+ if (cached) {
86
+ if (this.debug) {
87
+ logger.debug(`✅ Served from cache: REST ${endpoint}`);
88
+ }
89
+ return cached;
90
+ }
91
+ }
92
+ const startTime = process.hrtime.bigint();
93
+ if (this.debug) {
94
+ logger.debug(`🕒 Starting REST API request: GET ${endpoint}`);
95
+ logger.debug(`🕒 Request URL: ${url.toString()}`);
96
+ if (params) {
97
+ logger.debug(`🕒 Request params: ${JSON.stringify(params)}`);
98
+ }
99
+ }
100
+ try {
101
+ const response = await fetch(url.toString(), {
102
+ headers: {
103
+ 'Authorization': `Bearer ${this.token}`,
104
+ 'Content-Type': 'application/json',
105
+ },
106
+ });
107
+ // Update rate limit info from headers
108
+ this.rateLimitInfo = {
109
+ remaining: parseInt(response.headers.get('RateLimit-Remaining') || '0'),
110
+ limit: parseInt(response.headers.get('RateLimit-Limit') || '0'),
111
+ reset: parseInt(response.headers.get('RateLimit-Reset') || '0'),
112
+ };
113
+ if (this.debug) {
114
+ logger.debug('Rate limit info:', this.rateLimitInfo);
115
+ }
116
+ if (!response.ok) {
117
+ const errorText = await response.text();
118
+ let errorMessage = `API request failed with status ${response.status}: ${errorText}`;
119
+ // Try to parse the error as JSON for more details
120
+ try {
121
+ const errorJson = JSON.parse(errorText);
122
+ if (errorJson.message) {
123
+ errorMessage = `API request failed: ${errorJson.message}`;
124
+ }
125
+ if (errorJson.errors && Array.isArray(errorJson.errors)) {
126
+ errorMessage += `\nErrors: ${errorJson.errors.map((e) => e.message).join(', ')}`;
127
+ }
128
+ }
129
+ catch (e) {
130
+ // If parsing fails, use the original error text
131
+ }
132
+ // Check if this is an authentication error
133
+ const isAuthError = this.isAuthenticationError(response.status, errorMessage);
134
+ if (isAuthError && this.debug) {
135
+ logger.debug('Authentication error detected, not caching result');
136
+ }
137
+ throw new Error(errorMessage);
138
+ }
139
+ const data = await response.json();
140
+ // Cache the response if caching is enabled
141
+ if (this.cacheManager) {
142
+ await this.cacheManager.set(cacheKey, data, cacheType);
143
+ }
144
+ const endTime = process.hrtime.bigint();
145
+ const duration = Number(endTime - startTime) / 1000000; // Convert to milliseconds
146
+ if (this.debug) {
147
+ logger.debug(`✅ REST API request completed: GET ${endpoint} (${duration.toFixed(2)}ms)`);
148
+ }
149
+ return data;
150
+ }
151
+ catch (error) {
152
+ if (this.debug) {
153
+ logger.error('Error in get request:', error);
154
+ }
155
+ throw error;
156
+ }
157
+ }
158
+ /**
159
+ * Check if an error is an authentication error
160
+ */
161
+ isAuthenticationError(status, message) {
162
+ // Check for HTTP status codes that indicate auth issues
163
+ if (status === 401 || status === 403) {
164
+ return true;
165
+ }
166
+ // Check error message for auth-related keywords
167
+ const lowerMessage = message.toLowerCase();
168
+ return lowerMessage.includes('unauthorized') ||
169
+ lowerMessage.includes('authentication') ||
170
+ lowerMessage.includes('permission') ||
171
+ lowerMessage.includes('invalid token');
172
+ }
173
+ /**
174
+ * Get the current rate limit information
175
+ * @returns Current rate limit information or null if not available
176
+ */
177
+ getRateLimitInfo() {
178
+ return this.rateLimitInfo;
179
+ }
180
+ /**
181
+ * Get builds from an organization filtered by specific parameters
182
+ * @param org Organization slug
183
+ * @param params Query parameters
184
+ * @returns List of builds
185
+ */
186
+ async getBuilds(org, params) {
187
+ const endpoint = `/organizations/${org}/builds`;
188
+ const startTime = process.hrtime.bigint();
189
+ if (this.debug) {
190
+ logger.debug(`🕒 Fetching builds for organization: ${org}`);
191
+ }
192
+ const builds = await this.get(endpoint, params);
193
+ const endTime = process.hrtime.bigint();
194
+ const duration = Number(endTime - startTime) / 1000000; // Convert to milliseconds
195
+ if (this.debug) {
196
+ logger.debug(`✅ Retrieved ${builds.length} builds for ${org} (${duration.toFixed(2)}ms)`);
197
+ }
198
+ return builds;
199
+ }
200
+ async hasBuildAccess(org) {
201
+ try {
202
+ await this.getBuilds(org, { per_page: '1' });
203
+ return true;
204
+ }
205
+ catch (error) {
206
+ return false;
207
+ }
208
+ }
209
+ /**
210
+ * Check if the current user has access to an organization
211
+ * @param org Organization slug
212
+ * @returns True if the user has access, false otherwise
213
+ */
214
+ async hasOrganizationAccess(org) {
215
+ try {
216
+ const endpoint = `/organizations/${org}`;
217
+ await this.get(endpoint);
218
+ return true;
219
+ }
220
+ catch (error) {
221
+ if (this.debug) {
222
+ logger.debug(`User does not have access to organization: ${org}`);
223
+ }
224
+ return false;
225
+ }
226
+ }
227
+ /**
228
+ * Clear all cache entries
229
+ */
230
+ async clearCache() {
231
+ if (this.cacheManager) {
232
+ await this.cacheManager.clear();
233
+ }
234
+ }
235
+ /**
236
+ * Invalidate a specific cache type
237
+ */
238
+ async invalidateCache(type) {
239
+ if (this.cacheManager) {
240
+ await this.cacheManager.invalidateType(type);
241
+ }
242
+ }
243
+ }
244
+ //# sourceMappingURL=BuildkiteRestClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BuildkiteRestClient.js","sourceRoot":"/","sources":["services/BuildkiteRestClient.ts"],"names":[],"mappings":"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","sourcesContent":["import fetch from 'node-fetch';\nimport { CacheManager } from './CacheManager.js';\nimport { createHash } from 'crypto';\nimport { logger } from './logger.js';\n\nexport interface BuildkiteRestClientOptions {\n baseUrl?: string;\n caching?: boolean;\n cacheTTLs?: Partial<{\n builds: number;\n default: number;\n }>;\n debug?: boolean;\n}\n\nexport interface RateLimitInfo {\n remaining: number;\n limit: number;\n reset: number;\n}\n\n/**\n * BuildkiteRestClient provides methods to interact with the Buildkite REST API\n */\nexport class BuildkiteRestClient {\n private token: string;\n private baseUrl: string = 'https://api.buildkite.com/v2';\n private cacheManager: CacheManager | null = null;\n private debug: boolean = false;\n private rateLimitInfo: RateLimitInfo | null = null;\n\n /**\n * Create a new BuildkiteRestClient\n * @param token Your Buildkite API token\n * @param options Configuration options\n */\n constructor(token: string, options?: BuildkiteRestClientOptions) {\n this.token = token;\n this.debug = options?.debug || false;\n \n if (options?.baseUrl) {\n this.baseUrl = options.baseUrl;\n }\n \n // Initialize cache if caching is enabled\n if (options?.caching !== false) {\n this.cacheManager = new CacheManager(options?.cacheTTLs, this.debug);\n // Initialize cache and set token hash (async, but we don't wait)\n this.initCache();\n }\n }\n \n /**\n * Initialize cache asynchronously\n */\n private async initCache(): Promise<void> {\n if (this.cacheManager) {\n await this.cacheManager.init();\n await this.cacheManager.setTokenHash(this.token);\n }\n }\n \n /**\n * Generate a cache key for a REST endpoint\n */\n private generateCacheKey(endpoint: string, params?: Record<string, string>): string {\n const paramsString = params ? JSON.stringify(params) : '';\n return `REST:${endpoint}:${this.hashString(paramsString)}`;\n }\n \n /**\n * Hash a string using SHA256\n */\n private hashString(str: string): string {\n return createHash('sha256').update(str).digest('hex');\n }\n\n /**\n * Get cache type from endpoint\n */\n private getCacheTypeFromEndpoint(endpoint: string): string {\n if (endpoint.includes('/builds')) {\n return 'builds';\n }\n return 'default';\n }\n\n /**\n * Make a GET request to the Buildkite REST API\n * @param endpoint The API endpoint\n * @param params Query parameters\n * @returns The API response\n */\n private async get<T = any>(endpoint: string, params?: Record<string, string>): Promise<T> {\n const url = new URL(`${this.baseUrl}${endpoint}`);\n \n // Add query parameters\n if (params) {\n Object.entries(params).forEach(([key, value]) => {\n if (value !== undefined && value !== null) {\n url.searchParams.append(key, value);\n }\n });\n }\n \n // Generate cache key\n const cacheKey = this.generateCacheKey(endpoint, params);\n const cacheType = this.getCacheTypeFromEndpoint(endpoint);\n \n // Check cache first if enabled\n if (this.cacheManager) {\n const cached = await this.cacheManager.get(cacheKey, cacheType as any);\n if (cached) {\n if (this.debug) {\n logger.debug(`✅ Served from cache: REST ${endpoint}`);\n }\n return cached as T;\n }\n }\n\n const startTime = process.hrtime.bigint();\n if (this.debug) {\n logger.debug(`🕒 Starting REST API request: GET ${endpoint}`);\n logger.debug(`🕒 Request URL: ${url.toString()}`);\n if (params) {\n logger.debug(`🕒 Request params: ${JSON.stringify(params)}`);\n }\n }\n \n try {\n const response = await fetch(url.toString(), {\n headers: {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n });\n\n // Update rate limit info from headers\n this.rateLimitInfo = {\n remaining: parseInt(response.headers.get('RateLimit-Remaining') || '0'),\n limit: parseInt(response.headers.get('RateLimit-Limit') || '0'),\n reset: parseInt(response.headers.get('RateLimit-Reset') || '0'),\n };\n\n if (this.debug) {\n logger.debug('Rate limit info:', this.rateLimitInfo);\n }\n\n if (!response.ok) {\n const errorText = await response.text();\n let errorMessage = `API request failed with status ${response.status}: ${errorText}`;\n \n // Try to parse the error as JSON for more details\n try {\n const errorJson = JSON.parse(errorText);\n if (errorJson.message) {\n errorMessage = `API request failed: ${errorJson.message}`;\n }\n if (errorJson.errors && Array.isArray(errorJson.errors)) {\n errorMessage += `\\nErrors: ${errorJson.errors.map((e: any) => e.message).join(', ')}`;\n }\n } catch (e) {\n // If parsing fails, use the original error text\n }\n \n // Check if this is an authentication error\n const isAuthError = this.isAuthenticationError(response.status, errorMessage);\n if (isAuthError && this.debug) {\n logger.debug('Authentication error detected, not caching result');\n }\n \n throw new Error(errorMessage);\n }\n \n const data = await response.json() as T;\n \n // Cache the response if caching is enabled\n if (this.cacheManager) {\n await this.cacheManager.set(cacheKey, data, cacheType as any);\n }\n \n const endTime = process.hrtime.bigint();\n const duration = Number(endTime - startTime) / 1000000; // Convert to milliseconds\n if (this.debug) {\n logger.debug(`✅ REST API request completed: GET ${endpoint} (${duration.toFixed(2)}ms)`);\n }\n \n return data;\n } catch (error: unknown) {\n if (this.debug) {\n logger.error('Error in get request:', error);\n }\n throw error;\n }\n }\n\n /**\n * Check if an error is an authentication error\n */\n private isAuthenticationError(status: number, message: string): boolean {\n // Check for HTTP status codes that indicate auth issues\n if (status === 401 || status === 403) {\n return true;\n }\n \n // Check error message for auth-related keywords\n const lowerMessage = message.toLowerCase();\n return lowerMessage.includes('unauthorized') || \n lowerMessage.includes('authentication') || \n lowerMessage.includes('permission') ||\n lowerMessage.includes('invalid token');\n }\n\n /**\n * Get the current rate limit information\n * @returns Current rate limit information or null if not available\n */\n public getRateLimitInfo(): RateLimitInfo | null {\n return this.rateLimitInfo;\n }\n\n /**\n * Get builds from an organization filtered by specific parameters\n * @param org Organization slug\n * @param params Query parameters\n * @returns List of builds\n */\n public async getBuilds(org: string, params?: {\n creator?: string; // Creator's user ID, email or API access token\n pipeline?: string;\n branch?: string;\n commit?: string;\n state?: string;\n per_page?: string;\n page?: string;\n }): Promise<any[]> {\n const endpoint = `/organizations/${org}/builds`;\n const startTime = process.hrtime.bigint();\n if (this.debug) {\n logger.debug(`🕒 Fetching builds for organization: ${org}`);\n }\n \n const builds = await this.get<any[]>(endpoint, params as Record<string, string>);\n \n const endTime = process.hrtime.bigint();\n const duration = Number(endTime - startTime) / 1000000; // Convert to milliseconds\n if (this.debug) {\n logger.debug(`✅ Retrieved ${builds.length} builds for ${org} (${duration.toFixed(2)}ms)`);\n }\n \n return builds;\n }\n\n\n\n public async hasBuildAccess(org: string): Promise<boolean> {\n try {\n await this.getBuilds(org, { per_page: '1' });\n return true;\n } catch (error) {\n return false;\n }\n }\n \n /**\n * Check if the current user has access to an organization\n * @param org Organization slug\n * @returns True if the user has access, false otherwise\n */\n public async hasOrganizationAccess(org: string): Promise<boolean> {\n try {\n const endpoint = `/organizations/${org}`;\n await this.get(endpoint);\n return true;\n } catch (error) {\n if (this.debug) {\n logger.debug(`User does not have access to organization: ${org}`);\n }\n return false;\n }\n }\n \n /**\n * Clear all cache entries\n */\n public async clearCache(): Promise<void> {\n if (this.cacheManager) {\n await this.cacheManager.clear();\n }\n }\n\n /**\n * Invalidate a specific cache type\n */\n public async invalidateCache(type: string): Promise<void> {\n if (this.cacheManager) {\n await this.cacheManager.invalidateType(type);\n }\n }\n} "]}
@@ -0,0 +1,221 @@
1
+ import nodePersist from 'node-persist';
2
+ import { createHash } from 'crypto';
3
+ import { logger } from './logger.js';
4
+ import { XDGPaths } from '../utils/xdgPaths.js';
5
+ /**
6
+ * CacheManager for handling persistent caching with node-persist
7
+ */
8
+ export class CacheManager {
9
+ ttls;
10
+ initialized = false;
11
+ tokenHash = '';
12
+ debug = false;
13
+ // Default TTLs in milliseconds
14
+ static DEFAULT_TTLs = {
15
+ viewer: 3600 * 1000, // 1 hour for viewer data
16
+ organizations: 3600 * 1000, // 1 hour for organization data
17
+ pipelines: 60 * 1000, // 1 minute for pipelines
18
+ builds: 30 * 1000, // 30 seconds for builds
19
+ default: 30 * 1000, // default 30 seconds
20
+ };
21
+ constructor(ttls = {}, debug = false) {
22
+ this.ttls = ttls;
23
+ // Merge provided TTLs with defaults
24
+ this.ttls = { ...CacheManager.DEFAULT_TTLs, ...ttls };
25
+ this.debug = debug;
26
+ }
27
+ /**
28
+ * Initialize the storage
29
+ */
30
+ async init() {
31
+ if (this.initialized)
32
+ return;
33
+ if (this.debug) {
34
+ logger.debug('CacheManager.init - starting initialization');
35
+ logger.debug('CacheManager.init - nodePersist:', { type: typeof nodePersist });
36
+ }
37
+ const storageDir = XDGPaths.getAppCacheDir('bktide');
38
+ if (this.debug) {
39
+ logger.debug('CacheManager.init - storageDir:', { storageDir });
40
+ }
41
+ await nodePersist.init({
42
+ dir: storageDir,
43
+ stringify: JSON.stringify,
44
+ parse: JSON.parse,
45
+ encoding: 'utf8',
46
+ logging: false,
47
+ ttl: this.ttls.default // Default TTL
48
+ });
49
+ this.initialized = true;
50
+ if (this.debug) {
51
+ logger.debug('CacheManager.init - initialization complete');
52
+ }
53
+ }
54
+ /**
55
+ * Set the token hash to invalidate caches when token changes
56
+ */
57
+ async setTokenHash(token) {
58
+ await this.init();
59
+ const hash = this.hashString(token);
60
+ if (this.tokenHash !== hash) {
61
+ // Get current stored token hash
62
+ const storedHash = await nodePersist.getItem('token_hash');
63
+ // If token changed, clear viewer-related caches
64
+ if (storedHash !== hash) {
65
+ await this.invalidateType('viewer');
66
+ await nodePersist.setItem('token_hash', hash);
67
+ }
68
+ this.tokenHash = hash;
69
+ }
70
+ }
71
+ /**
72
+ * Generate a cache key for a GraphQL query
73
+ */
74
+ generateCacheKey(query, variables) {
75
+ // Extract operation name from query for better key readability
76
+ const operationName = query.match(/query\s+(\w+)?/)?.[1] || 'UnnamedQuery';
77
+ const varsString = variables ? JSON.stringify(variables) : '';
78
+ return `${operationName}:${this.hashString(varsString)}`;
79
+ }
80
+ /**
81
+ * Hash a string using SHA256
82
+ */
83
+ hashString(str) {
84
+ return createHash('sha256').update(str).digest('hex');
85
+ }
86
+ /**
87
+ * Get cache type from query string
88
+ */
89
+ getCacheTypeFromQuery(query) {
90
+ if (query.includes('viewer') && !query.includes('builds')) {
91
+ return 'viewer';
92
+ }
93
+ else if (query.includes('organizations')) {
94
+ return 'organizations';
95
+ }
96
+ else if (query.includes('pipelines')) {
97
+ return 'pipelines';
98
+ }
99
+ else if (query.includes('builds')) {
100
+ return 'builds';
101
+ }
102
+ return 'default';
103
+ }
104
+ /**
105
+ * Get a value from cache
106
+ */
107
+ async get(query, variables) {
108
+ await this.init();
109
+ // Check if this is a direct key (used by REST client) or GraphQL query
110
+ const key = query.startsWith('REST:') ? query : this.generateCacheKey(query, variables);
111
+ try {
112
+ // Add debug logging to see what's happening
113
+ if (this.debug) {
114
+ logger.debug('CacheManager.get - nodePersist:', { type: typeof nodePersist });
115
+ logger.debug('CacheManager.get - key:', { key });
116
+ }
117
+ const entry = await nodePersist.getItem(key);
118
+ if (!entry)
119
+ return null;
120
+ // Check if manually expired
121
+ if (Date.now() > entry.expiresAt) {
122
+ await nodePersist.removeItem(key);
123
+ return null;
124
+ }
125
+ return entry.value;
126
+ }
127
+ catch (error) {
128
+ if (this.debug) {
129
+ logger.debug('CacheManager.get error:', { error });
130
+ }
131
+ logger.debug(`Cache miss or error for key ${key}:`, error);
132
+ return null;
133
+ }
134
+ }
135
+ /**
136
+ * Set a value in cache
137
+ * @param query The GraphQL query or REST cache key
138
+ * @param value The value to cache
139
+ * @param variables Variables for GraphQL query or cache type for REST
140
+ * @param skipCacheIfAuthError Whether to skip caching if this is an authentication error
141
+ */
142
+ async set(query, value, variables, skipCacheIfAuthError = false) {
143
+ await this.init();
144
+ let key;
145
+ let cacheType;
146
+ // Handle different usage patterns between GraphQL and REST clients
147
+ if (query.startsWith('REST:')) {
148
+ // REST client usage - variables is actually the cache type
149
+ key = query;
150
+ cacheType = variables || 'default';
151
+ }
152
+ else {
153
+ // GraphQL client usage
154
+ key = this.generateCacheKey(query, variables);
155
+ cacheType = this.getCacheTypeFromQuery(query);
156
+ }
157
+ // Skip caching if this is an authentication error and skipCacheIfAuthError is true
158
+ if (skipCacheIfAuthError && this.isAuthenticationError(value)) {
159
+ if (this.debug) {
160
+ logger.debug(`Skipping cache for authentication error: ${key}`);
161
+ }
162
+ return;
163
+ }
164
+ const ttl = this.ttls[cacheType] || CacheManager.DEFAULT_TTLs.default;
165
+ await nodePersist.setItem(key, {
166
+ value,
167
+ expiresAt: Date.now() + ttl,
168
+ type: cacheType,
169
+ createdAt: Date.now()
170
+ });
171
+ }
172
+ /**
173
+ * Check if a result contains authentication error information
174
+ */
175
+ isAuthenticationError(value) {
176
+ if (!value)
177
+ return false;
178
+ // Check for common authentication error patterns in GraphQL responses
179
+ if (value.errors) {
180
+ return value.errors.some((err) => err.message?.includes('unauthorized') ||
181
+ err.message?.includes('authentication') ||
182
+ err.message?.includes('permission') ||
183
+ err.message?.includes('invalid token'));
184
+ }
185
+ // Check for REST API error responses
186
+ if (value.message) {
187
+ const message = value.message.toLowerCase();
188
+ return message.includes('unauthorized') ||
189
+ message.includes('authentication') ||
190
+ message.includes('permission') ||
191
+ message.includes('invalid token');
192
+ }
193
+ return false;
194
+ }
195
+ /**
196
+ * Invalidate all cache entries of a specific type
197
+ */
198
+ async invalidateType(type) {
199
+ await this.init();
200
+ const keys = await nodePersist.keys();
201
+ for (const key of keys) {
202
+ try {
203
+ const entry = await nodePersist.getItem(key);
204
+ if (entry && entry.type === type) {
205
+ await nodePersist.removeItem(key);
206
+ }
207
+ }
208
+ catch (error) {
209
+ // Continue to next item if there's an error
210
+ }
211
+ }
212
+ }
213
+ /**
214
+ * Clear all cache entries
215
+ */
216
+ async clear() {
217
+ await this.init();
218
+ await nodePersist.clear();
219
+ }
220
+ }
221
+ //# sourceMappingURL=CacheManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CacheManager.js","sourceRoot":"/","sources":["services/CacheManager.ts"],"names":[],"mappings":"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","sourcesContent":["import nodePersist from 'node-persist';\nimport { createHash } from 'crypto';\nimport { logger } from './logger.js';\nimport { XDGPaths } from '../utils/xdgPaths.js';\n\n/**\n * CacheManager for handling persistent caching with node-persist\n */\nexport class CacheManager {\n private initialized = false;\n private tokenHash: string = '';\n private debug: boolean = false;\n \n // Default TTLs in milliseconds\n private static DEFAULT_TTLs = {\n viewer: 3600 * 1000, // 1 hour for viewer data\n organizations: 3600 * 1000, // 1 hour for organization data\n pipelines: 60 * 1000, // 1 minute for pipelines\n builds: 30 * 1000, // 30 seconds for builds\n default: 30 * 1000, // default 30 seconds\n };\n\n constructor(private ttls: Partial<typeof CacheManager.DEFAULT_TTLs> = {}, debug: boolean = false) {\n // Merge provided TTLs with defaults\n this.ttls = { ...CacheManager.DEFAULT_TTLs, ...ttls };\n this.debug = debug;\n }\n\n /**\n * Initialize the storage\n */\n public async init(): Promise<void> {\n if (this.initialized) return;\n \n if (this.debug) {\n logger.debug('CacheManager.init - starting initialization');\n logger.debug('CacheManager.init - nodePersist:', { type: typeof nodePersist });\n }\n \n const storageDir = XDGPaths.getAppCacheDir('bktide');\n \n if (this.debug) {\n logger.debug('CacheManager.init - storageDir:', { storageDir });\n }\n \n await nodePersist.init({\n dir: storageDir,\n stringify: JSON.stringify,\n parse: JSON.parse,\n encoding: 'utf8',\n logging: false,\n ttl: this.ttls.default // Default TTL\n });\n \n this.initialized = true;\n \n if (this.debug) {\n logger.debug('CacheManager.init - initialization complete');\n }\n }\n\n /**\n * Set the token hash to invalidate caches when token changes\n */\n public async setTokenHash(token: string): Promise<void> {\n await this.init();\n const hash = this.hashString(token);\n \n if (this.tokenHash !== hash) {\n // Get current stored token hash\n const storedHash = await nodePersist.getItem('token_hash');\n \n // If token changed, clear viewer-related caches\n if (storedHash !== hash) {\n await this.invalidateType('viewer');\n await nodePersist.setItem('token_hash', hash);\n }\n \n this.tokenHash = hash;\n }\n }\n\n /**\n * Generate a cache key for a GraphQL query\n */\n private generateCacheKey(query: string, variables?: Record<string, any>): string {\n // Extract operation name from query for better key readability\n const operationName = query.match(/query\\s+(\\w+)?/)?.[1] || 'UnnamedQuery';\n const varsString = variables ? JSON.stringify(variables) : '';\n return `${operationName}:${this.hashString(varsString)}`;\n }\n\n /**\n * Hash a string using SHA256\n */\n private hashString(str: string): string {\n return createHash('sha256').update(str).digest('hex');\n }\n\n /**\n * Get cache type from query string\n */\n private getCacheTypeFromQuery(query: string): string {\n if (query.includes('viewer') && !query.includes('builds')) {\n return 'viewer';\n } else if (query.includes('organizations')) {\n return 'organizations';\n } else if (query.includes('pipelines')) {\n return 'pipelines';\n } else if (query.includes('builds')) {\n return 'builds';\n }\n return 'default';\n }\n\n /**\n * Get a value from cache\n */\n public async get<T>(query: string, variables?: Record<string, any>): Promise<T | null> {\n await this.init();\n // Check if this is a direct key (used by REST client) or GraphQL query\n const key = query.startsWith('REST:') ? query : this.generateCacheKey(query, variables);\n \n try {\n // Add debug logging to see what's happening\n if (this.debug) {\n logger.debug('CacheManager.get - nodePersist:', { type: typeof nodePersist });\n logger.debug('CacheManager.get - key:', { key });\n }\n \n const entry = await nodePersist.getItem(key);\n \n if (!entry) return null;\n \n // Check if manually expired\n if (Date.now() > entry.expiresAt) {\n await nodePersist.removeItem(key);\n return null;\n }\n \n return entry.value as T;\n } catch (error) {\n if (this.debug) {\n logger.debug('CacheManager.get error:', { error });\n }\n logger.debug(`Cache miss or error for key ${key}:`, error);\n return null;\n }\n }\n\n /**\n * Set a value in cache\n * @param query The GraphQL query or REST cache key\n * @param value The value to cache\n * @param variables Variables for GraphQL query or cache type for REST\n * @param skipCacheIfAuthError Whether to skip caching if this is an authentication error\n */\n public async set<T>(\n query: string, \n value: T, \n variables?: Record<string, any> | keyof typeof CacheManager.DEFAULT_TTLs,\n skipCacheIfAuthError: boolean = false\n ): Promise<void> {\n await this.init();\n \n let key: string;\n let cacheType: string;\n \n // Handle different usage patterns between GraphQL and REST clients\n if (query.startsWith('REST:')) {\n // REST client usage - variables is actually the cache type\n key = query;\n cacheType = (variables as string) || 'default';\n } else {\n // GraphQL client usage\n key = this.generateCacheKey(query, variables as Record<string, any>);\n cacheType = this.getCacheTypeFromQuery(query);\n }\n \n // Skip caching if this is an authentication error and skipCacheIfAuthError is true\n if (skipCacheIfAuthError && this.isAuthenticationError(value)) {\n if (this.debug) {\n logger.debug(`Skipping cache for authentication error: ${key}`);\n }\n return;\n }\n \n const ttl = this.ttls[cacheType as keyof typeof CacheManager.DEFAULT_TTLs] || CacheManager.DEFAULT_TTLs.default;\n \n await nodePersist.setItem(key, {\n value,\n expiresAt: Date.now() + ttl,\n type: cacheType,\n createdAt: Date.now()\n });\n }\n\n /**\n * Check if a result contains authentication error information\n */\n private isAuthenticationError(value: any): boolean {\n if (!value) return false;\n \n // Check for common authentication error patterns in GraphQL responses\n if (value.errors) {\n return value.errors.some((err: any) => \n err.message?.includes('unauthorized') || \n err.message?.includes('authentication') || \n err.message?.includes('permission') ||\n err.message?.includes('invalid token')\n );\n }\n \n // Check for REST API error responses\n if (value.message) {\n const message = value.message.toLowerCase();\n return message.includes('unauthorized') || \n message.includes('authentication') || \n message.includes('permission') ||\n message.includes('invalid token');\n }\n \n return false;\n }\n\n /**\n * Invalidate all cache entries of a specific type\n */\n public async invalidateType(type: string): Promise<void> {\n await this.init();\n const keys = await nodePersist.keys();\n \n for (const key of keys) {\n try {\n const entry = await nodePersist.getItem(key);\n if (entry && entry.type === type) {\n await nodePersist.removeItem(key);\n }\n } catch (error) {\n // Continue to next item if there's an error\n }\n }\n }\n\n /**\n * Clear all cache entries\n */\n public async clear(): Promise<void> {\n await this.init();\n await nodePersist.clear();\n }\n} "]}