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.
- package/README.md +145 -0
- package/WORKFLOW_README.md +65 -0
- package/bin/alfred-entrypoint +54 -0
- package/dist/commands/BaseCommand.js +159 -0
- package/dist/commands/BaseCommand.js.map +1 -0
- package/dist/commands/BaseCommandHandler.js +80 -0
- package/dist/commands/BaseCommandHandler.js.map +1 -0
- package/dist/commands/BuildCommandHandler.js +28 -0
- package/dist/commands/BuildCommandHandler.js.map +1 -0
- package/dist/commands/HelloCommandHandler.js +6 -0
- package/dist/commands/HelloCommandHandler.js.map +1 -0
- package/dist/commands/ListAnnotations.js +60 -0
- package/dist/commands/ListAnnotations.js.map +1 -0
- package/dist/commands/ListBuilds.js +137 -0
- package/dist/commands/ListBuilds.js.map +1 -0
- package/dist/commands/ListOrganizations.js +27 -0
- package/dist/commands/ListOrganizations.js.map +1 -0
- package/dist/commands/ListPipelines.js +114 -0
- package/dist/commands/ListPipelines.js.map +1 -0
- package/dist/commands/ManageToken.js +180 -0
- package/dist/commands/ManageToken.js.map +1 -0
- package/dist/commands/OrganizationCommandHandler.js +53 -0
- package/dist/commands/OrganizationCommandHandler.js.map +1 -0
- package/dist/commands/PipelineCommandHandler.js +142 -0
- package/dist/commands/PipelineCommandHandler.js.map +1 -0
- package/dist/commands/ShowViewer.js +26 -0
- package/dist/commands/ShowViewer.js.map +1 -0
- package/dist/commands/UserBuildsCommandHandler.js +61 -0
- package/dist/commands/UserBuildsCommandHandler.js.map +1 -0
- package/dist/commands/ViewerBuildsCommandHandler.js +176 -0
- package/dist/commands/ViewerBuildsCommandHandler.js.map +1 -0
- package/dist/commands/ViewerCommandHandler.js +46 -0
- package/dist/commands/ViewerCommandHandler.js.map +1 -0
- package/dist/commands/index.js +8 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/formatters/BaseFormatter.js +14 -0
- package/dist/formatters/BaseFormatter.js.map +1 -0
- package/dist/formatters/FormatterFactory.js +48 -0
- package/dist/formatters/FormatterFactory.js.map +1 -0
- package/dist/formatters/annotations/Formatter.js +10 -0
- package/dist/formatters/annotations/Formatter.js.map +1 -0
- package/dist/formatters/annotations/JsonFormatter.js +20 -0
- package/dist/formatters/annotations/JsonFormatter.js.map +1 -0
- package/dist/formatters/annotations/PlainTextFormatter.js +35 -0
- package/dist/formatters/annotations/PlainTextFormatter.js.map +1 -0
- package/dist/formatters/annotations/index.js +23 -0
- package/dist/formatters/annotations/index.js.map +1 -0
- package/dist/formatters/builds/AlfredFormatter.js +135 -0
- package/dist/formatters/builds/AlfredFormatter.js.map +1 -0
- package/dist/formatters/builds/Formatter.js +10 -0
- package/dist/formatters/builds/Formatter.js.map +1 -0
- package/dist/formatters/builds/JsonFormatter.js +44 -0
- package/dist/formatters/builds/JsonFormatter.js.map +1 -0
- package/dist/formatters/builds/PlainTextFormatter.js +113 -0
- package/dist/formatters/builds/PlainTextFormatter.js.map +1 -0
- package/dist/formatters/builds/index.js +26 -0
- package/dist/formatters/builds/index.js.map +1 -0
- package/dist/formatters/errors/AlfredFormatter.js +110 -0
- package/dist/formatters/errors/AlfredFormatter.js.map +1 -0
- package/dist/formatters/errors/Formatter.js +98 -0
- package/dist/formatters/errors/Formatter.js.map +1 -0
- package/dist/formatters/errors/JsonFormatter.js +63 -0
- package/dist/formatters/errors/JsonFormatter.js.map +1 -0
- package/dist/formatters/errors/PlainTextFormatter.js +52 -0
- package/dist/formatters/errors/PlainTextFormatter.js.map +1 -0
- package/dist/formatters/errors/index.js +26 -0
- package/dist/formatters/errors/index.js.map +1 -0
- package/dist/formatters/index.js +9 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/organizations/Formatter.js +10 -0
- package/dist/formatters/organizations/Formatter.js.map +1 -0
- package/dist/formatters/organizations/JsonFormatter.js +16 -0
- package/dist/formatters/organizations/JsonFormatter.js.map +1 -0
- package/dist/formatters/organizations/PlainTextFormatter.js +15 -0
- package/dist/formatters/organizations/PlainTextFormatter.js.map +1 -0
- package/dist/formatters/organizations/index.js +21 -0
- package/dist/formatters/organizations/index.js.map +1 -0
- package/dist/formatters/pipelines/AlfredFormatter.js +42 -0
- package/dist/formatters/pipelines/AlfredFormatter.js.map +1 -0
- package/dist/formatters/pipelines/Formatter.js +10 -0
- package/dist/formatters/pipelines/Formatter.js.map +1 -0
- package/dist/formatters/pipelines/JsonFormatter.js +13 -0
- package/dist/formatters/pipelines/JsonFormatter.js.map +1 -0
- package/dist/formatters/pipelines/PlainTextFormatter.js +47 -0
- package/dist/formatters/pipelines/PlainTextFormatter.js.map +1 -0
- package/dist/formatters/pipelines/index.js +28 -0
- package/dist/formatters/pipelines/index.js.map +1 -0
- package/dist/formatters/token/AlfredFormatter.js +191 -0
- package/dist/formatters/token/AlfredFormatter.js.map +1 -0
- package/dist/formatters/token/Formatter.js +13 -0
- package/dist/formatters/token/Formatter.js.map +1 -0
- package/dist/formatters/token/JsonFormatter.js +211 -0
- package/dist/formatters/token/JsonFormatter.js.map +1 -0
- package/dist/formatters/token/PlainTextFormatter.js +184 -0
- package/dist/formatters/token/PlainTextFormatter.js.map +1 -0
- package/dist/formatters/token/index.js +26 -0
- package/dist/formatters/token/index.js.map +1 -0
- package/dist/formatters/viewer/Formatter.js +10 -0
- package/dist/formatters/viewer/Formatter.js.map +1 -0
- package/dist/formatters/viewer/JsonFormatter.js +20 -0
- package/dist/formatters/viewer/JsonFormatter.js.map +1 -0
- package/dist/formatters/viewer/PlainTextFormatter.js +20 -0
- package/dist/formatters/viewer/PlainTextFormatter.js.map +1 -0
- package/dist/formatters/viewer/index.js +21 -0
- package/dist/formatters/viewer/index.js.map +1 -0
- package/dist/graphql/generated/fragment-masking.js +17 -0
- package/dist/graphql/generated/fragment-masking.js.map +1 -0
- package/dist/graphql/generated/gql.js +13 -0
- package/dist/graphql/generated/gql.js.map +1 -0
- package/dist/graphql/generated/graphql.js +852 -0
- package/dist/graphql/generated/graphql.js.map +1 -0
- package/dist/graphql/generated/index.js +3 -0
- package/dist/graphql/generated/index.js.map +1 -0
- package/dist/graphql/generated/sdk.js +872 -0
- package/dist/graphql/generated/sdk.js.map +1 -0
- package/dist/graphql/queries.js +138 -0
- package/dist/graphql/queries.js.map +1 -0
- package/dist/index.js +271 -0
- package/dist/index.js.map +1 -0
- package/dist/services/BuildkiteClient.js +520 -0
- package/dist/services/BuildkiteClient.js.map +1 -0
- package/dist/services/BuildkiteRestClient.js +244 -0
- package/dist/services/BuildkiteRestClient.js.map +1 -0
- package/dist/services/CacheManager.js +221 -0
- package/dist/services/CacheManager.js.map +1 -0
- package/dist/services/CredentialManager.js +158 -0
- package/dist/services/CredentialManager.js.map +1 -0
- package/dist/services/EnhancedBuildkiteClient.js +297 -0
- package/dist/services/EnhancedBuildkiteClient.js.map +1 -0
- package/dist/services/logger.js +107 -0
- package/dist/services/logger.js.map +1 -0
- package/dist/types/buildkite.js +5 -0
- package/dist/types/buildkite.js.map +1 -0
- package/dist/types/credentials.js +2 -0
- package/dist/types/credentials.js.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cli-error-handler.js +172 -0
- package/dist/utils/cli-error-handler.js.map +1 -0
- package/dist/utils/errorUtils.js +59 -0
- package/dist/utils/errorUtils.js.map +1 -0
- package/dist/utils/parseBuildRef.js +31 -0
- package/dist/utils/parseBuildRef.js.map +1 -0
- package/dist/utils/textFormatter.js +53 -0
- package/dist/utils/textFormatter.js.map +1 -0
- package/dist/utils/xdgPaths.js +95 -0
- package/dist/utils/xdgPaths.js.map +1 -0
- package/env.example +66 -0
- package/icons/README.md +68 -0
- package/icons/blocked.png +0 -0
- package/icons/buildkite.png +0 -0
- package/icons/failed.png +0 -0
- package/icons/failing.png +0 -0
- package/icons/passed.png +0 -0
- package/icons/running.png +0 -0
- package/icons/scheduled.png +0 -0
- package/icons/skipped.png +0 -0
- package/icons/unknown.png +0 -0
- package/info.plist +734 -0
- 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} "]}
|