@gowelle/stint-agent 1.0.8 → 1.2.0
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 +55 -47
- package/dist/StatusDashboard-7ZIGEOQ4.js +232 -0
- package/dist/api-HLTR2LTF.js +7 -0
- package/dist/{chunk-56UZEICO.js → chunk-FBQA4K5J.js} +106 -207
- package/dist/chunk-IJUJ6NEL.js +369 -0
- package/dist/{chunk-5OKRSNU4.js → chunk-RHMTZK2J.js} +60 -242
- package/dist/chunk-W4JGOGR7.js +384 -0
- package/dist/daemon/runner.js +8 -210
- package/dist/index.js +577 -70
- package/package.json +24 -1
- package/dist/api-2MI5XBJX.js +0 -6
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import {
|
|
2
|
+
authService,
|
|
3
|
+
config,
|
|
4
|
+
logger
|
|
5
|
+
} from "./chunk-RHMTZK2J.js";
|
|
6
|
+
|
|
7
|
+
// src/utils/circuit-breaker.ts
|
|
8
|
+
var CircuitBreaker = class {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.options.windowSize = options.windowSize || 6e4;
|
|
12
|
+
}
|
|
13
|
+
state = "CLOSED";
|
|
14
|
+
failures = 0;
|
|
15
|
+
successes = 0;
|
|
16
|
+
lastFailureTime = 0;
|
|
17
|
+
openedAt = 0;
|
|
18
|
+
failureTimestamps = [];
|
|
19
|
+
/**
|
|
20
|
+
* Execute an operation through the circuit breaker
|
|
21
|
+
*/
|
|
22
|
+
async execute(operation, fallback) {
|
|
23
|
+
if (this.state === "OPEN") {
|
|
24
|
+
if (Date.now() - this.openedAt >= this.options.timeout) {
|
|
25
|
+
this.state = "HALF_OPEN";
|
|
26
|
+
this.successes = 0;
|
|
27
|
+
} else {
|
|
28
|
+
if (fallback) {
|
|
29
|
+
return fallback();
|
|
30
|
+
}
|
|
31
|
+
throw new Error("Circuit breaker is OPEN - service unavailable");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const result = await operation();
|
|
36
|
+
this.onSuccess();
|
|
37
|
+
return result;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
this.onFailure();
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Handle successful operation
|
|
45
|
+
*/
|
|
46
|
+
onSuccess() {
|
|
47
|
+
this.failures = 0;
|
|
48
|
+
if (this.state === "HALF_OPEN") {
|
|
49
|
+
this.successes++;
|
|
50
|
+
if (this.successes >= this.options.successThreshold) {
|
|
51
|
+
this.state = "CLOSED";
|
|
52
|
+
this.successes = 0;
|
|
53
|
+
this.failureTimestamps = [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Handle failed operation
|
|
59
|
+
*/
|
|
60
|
+
onFailure() {
|
|
61
|
+
this.lastFailureTime = Date.now();
|
|
62
|
+
this.failureTimestamps.push(this.lastFailureTime);
|
|
63
|
+
const windowStart = this.lastFailureTime - this.options.windowSize;
|
|
64
|
+
this.failureTimestamps = this.failureTimestamps.filter((ts) => ts > windowStart);
|
|
65
|
+
this.failures = this.failureTimestamps.length;
|
|
66
|
+
if (this.state === "HALF_OPEN") {
|
|
67
|
+
this.state = "OPEN";
|
|
68
|
+
this.openedAt = Date.now();
|
|
69
|
+
this.successes = 0;
|
|
70
|
+
} else if (this.state === "CLOSED") {
|
|
71
|
+
if (this.failures >= this.options.failureThreshold) {
|
|
72
|
+
this.state = "OPEN";
|
|
73
|
+
this.openedAt = Date.now();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get current state
|
|
79
|
+
*/
|
|
80
|
+
getState() {
|
|
81
|
+
return this.state;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get failure count in current window
|
|
85
|
+
*/
|
|
86
|
+
getFailureCount() {
|
|
87
|
+
return this.failures;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Reset the circuit breaker to CLOSED state
|
|
91
|
+
*/
|
|
92
|
+
reset() {
|
|
93
|
+
this.state = "CLOSED";
|
|
94
|
+
this.failures = 0;
|
|
95
|
+
this.successes = 0;
|
|
96
|
+
this.failureTimestamps = [];
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/services/api.ts
|
|
101
|
+
var AGENT_VERSION = "1.2.0";
|
|
102
|
+
var ApiServiceImpl = class {
|
|
103
|
+
sessionId = null;
|
|
104
|
+
circuitBreaker = new CircuitBreaker({
|
|
105
|
+
failureThreshold: 5,
|
|
106
|
+
successThreshold: 2,
|
|
107
|
+
timeout: 3e4,
|
|
108
|
+
// 30s before trying half-open
|
|
109
|
+
windowSize: 6e4
|
|
110
|
+
// 60s failure window
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* Get authentication headers for API requests
|
|
114
|
+
* @returns Headers object with Authorization and Content-Type
|
|
115
|
+
* @throws Error if no authentication token is found
|
|
116
|
+
*/
|
|
117
|
+
async getHeaders() {
|
|
118
|
+
const token = await authService.getToken();
|
|
119
|
+
if (!token) {
|
|
120
|
+
throw new Error('No authentication token found. Please run "stint login" first.');
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
Authorization: `Bearer ${token}`,
|
|
124
|
+
"Content-Type": "application/json"
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Make an HTTP request to the API with circuit breaker protection
|
|
129
|
+
* @param endpoint - API endpoint path (e.g., '/api/user')
|
|
130
|
+
* @param options - Fetch options (method, body, headers, etc.)
|
|
131
|
+
* @returns Parsed JSON response
|
|
132
|
+
* @throws Error if request fails or circuit breaker is open
|
|
133
|
+
*/
|
|
134
|
+
async request(endpoint, options = {}) {
|
|
135
|
+
const url = `${config.getApiUrl()}${endpoint}`;
|
|
136
|
+
const headers = await this.getHeaders();
|
|
137
|
+
return this.circuitBreaker.execute(async () => {
|
|
138
|
+
try {
|
|
139
|
+
const response = await fetch(url, {
|
|
140
|
+
...options,
|
|
141
|
+
headers: {
|
|
142
|
+
...headers,
|
|
143
|
+
...options.headers
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const errorText = await response.text();
|
|
148
|
+
throw new Error(`API request failed: ${response.status} ${errorText}`);
|
|
149
|
+
}
|
|
150
|
+
return await response.json();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error("api", `Request to ${endpoint} failed`, error);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Retry wrapper with exponential backoff
|
|
159
|
+
* @param operation - Async operation to retry
|
|
160
|
+
* @param operationName - Name for logging
|
|
161
|
+
* @param maxRetries - Maximum number of retry attempts (default: 3)
|
|
162
|
+
* @returns Result of the operation
|
|
163
|
+
* @throws Last error if all retries fail
|
|
164
|
+
*/
|
|
165
|
+
async withRetry(operation, operationName, maxRetries = 3) {
|
|
166
|
+
let lastError;
|
|
167
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
168
|
+
try {
|
|
169
|
+
return await operation();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
lastError = error;
|
|
172
|
+
if (lastError.message.includes("401") || lastError.message.includes("403")) {
|
|
173
|
+
throw lastError;
|
|
174
|
+
}
|
|
175
|
+
if (attempt < maxRetries) {
|
|
176
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 5e3);
|
|
177
|
+
logger.warn("api", `${operationName} failed, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`);
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
179
|
+
} else {
|
|
180
|
+
logger.error("api", `${operationName} failed after ${maxRetries} attempts`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
throw lastError;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Connect agent session to the API
|
|
188
|
+
* @returns Agent session data including session ID
|
|
189
|
+
* @throws Error if connection fails
|
|
190
|
+
*/
|
|
191
|
+
async connect() {
|
|
192
|
+
logger.info("api", "Connecting agent session...");
|
|
193
|
+
const os = `${process.platform}-${process.arch}`;
|
|
194
|
+
return this.withRetry(async () => {
|
|
195
|
+
const response = await this.request("/api/agent/connect", {
|
|
196
|
+
method: "POST",
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
machine_id: authService.getMachineId(),
|
|
199
|
+
machine_name: authService.getMachineName(),
|
|
200
|
+
os,
|
|
201
|
+
agent_version: AGENT_VERSION
|
|
202
|
+
})
|
|
203
|
+
});
|
|
204
|
+
const session = response.data;
|
|
205
|
+
this.sessionId = session.id;
|
|
206
|
+
logger.success("api", `Agent session connected: ${session.id}`);
|
|
207
|
+
return session;
|
|
208
|
+
}, "Connect");
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Disconnect agent session from the API
|
|
212
|
+
* @param reason - Optional reason for disconnection
|
|
213
|
+
*/
|
|
214
|
+
async disconnect(reason) {
|
|
215
|
+
if (!this.sessionId) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
logger.info("api", "Disconnecting agent session...");
|
|
219
|
+
await this.request("/api/agent/disconnect", {
|
|
220
|
+
method: "POST",
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
session_id: this.sessionId,
|
|
223
|
+
reason: reason || "Agent disconnected"
|
|
224
|
+
})
|
|
225
|
+
});
|
|
226
|
+
this.sessionId = null;
|
|
227
|
+
logger.success("api", "Agent session disconnected");
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Send heartbeat to keep session alive
|
|
231
|
+
* @throws Error if no active session
|
|
232
|
+
*/
|
|
233
|
+
async heartbeat() {
|
|
234
|
+
if (!this.sessionId) {
|
|
235
|
+
throw new Error("No active session");
|
|
236
|
+
}
|
|
237
|
+
await this.withRetry(async () => {
|
|
238
|
+
await this.request("/api/agent/heartbeat", {
|
|
239
|
+
method: "POST",
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
session_id: this.sessionId
|
|
242
|
+
})
|
|
243
|
+
});
|
|
244
|
+
logger.debug("api", "Heartbeat sent");
|
|
245
|
+
}, "Heartbeat");
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get pending commits for a project
|
|
249
|
+
* @param projectId - Project ID
|
|
250
|
+
* @returns Array of pending commits
|
|
251
|
+
*/
|
|
252
|
+
async getPendingCommits(projectId) {
|
|
253
|
+
logger.info("api", `Fetching pending commits for project ${projectId}`);
|
|
254
|
+
const response = await this.request(
|
|
255
|
+
`/api/agent/pending-commits?project_id=${projectId}`
|
|
256
|
+
);
|
|
257
|
+
const commits = response.data.map((item) => {
|
|
258
|
+
const projectId2 = item.project_id || item.projectId;
|
|
259
|
+
const createdAt = item.created_at || item.createdAt;
|
|
260
|
+
if (!projectId2 || !createdAt) {
|
|
261
|
+
throw new Error(`Invalid commit data received from API: ${JSON.stringify(item)}`);
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
id: item.id,
|
|
265
|
+
projectId: projectId2,
|
|
266
|
+
message: item.message,
|
|
267
|
+
files: item.files,
|
|
268
|
+
createdAt
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
logger.info("api", `Found ${commits.length} pending commits`);
|
|
272
|
+
return commits;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Mark a commit as successfully executed
|
|
276
|
+
* @param commitId - Commit ID
|
|
277
|
+
* @param sha - Git commit SHA
|
|
278
|
+
* @returns Updated commit data
|
|
279
|
+
*/
|
|
280
|
+
async markCommitExecuted(commitId, sha) {
|
|
281
|
+
logger.info("api", `Marking commit ${commitId} as executed (SHA: ${sha})`);
|
|
282
|
+
return this.withRetry(async () => {
|
|
283
|
+
const response = await this.request(
|
|
284
|
+
`/api/agent/commits/${commitId}/executed`,
|
|
285
|
+
{
|
|
286
|
+
method: "POST",
|
|
287
|
+
body: JSON.stringify({ sha })
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
const data = response.data;
|
|
291
|
+
const projectId = data.project_id || data.projectId;
|
|
292
|
+
const createdAt = data.created_at || data.createdAt;
|
|
293
|
+
const executedAt = data.executed_at || data.executedAt;
|
|
294
|
+
if (!projectId || !createdAt) {
|
|
295
|
+
throw new Error(`Invalid commit data received from API: ${JSON.stringify(data)}`);
|
|
296
|
+
}
|
|
297
|
+
const commit = {
|
|
298
|
+
id: data.id,
|
|
299
|
+
projectId,
|
|
300
|
+
message: data.message,
|
|
301
|
+
sha: data.sha,
|
|
302
|
+
status: data.status,
|
|
303
|
+
createdAt,
|
|
304
|
+
executedAt,
|
|
305
|
+
error: data.error
|
|
306
|
+
};
|
|
307
|
+
logger.success("api", `Commit ${commitId} marked as executed`);
|
|
308
|
+
return commit;
|
|
309
|
+
}, "Mark commit executed");
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Mark a commit as failed
|
|
313
|
+
* @param commitId - Commit ID
|
|
314
|
+
* @param error - Error message
|
|
315
|
+
*/
|
|
316
|
+
async markCommitFailed(commitId, error) {
|
|
317
|
+
logger.error("api", `Marking commit ${commitId} as failed: ${error}`);
|
|
318
|
+
await this.withRetry(async () => {
|
|
319
|
+
await this.request(`/api/agent/commits/${commitId}/failed`, {
|
|
320
|
+
method: "POST",
|
|
321
|
+
body: JSON.stringify({ error })
|
|
322
|
+
});
|
|
323
|
+
}, "Mark commit failed");
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Sync project repository information with the API
|
|
327
|
+
* @param projectId - Project ID
|
|
328
|
+
* @param data - Repository information (path, remote URL, branches)
|
|
329
|
+
*/
|
|
330
|
+
async syncProject(projectId, data) {
|
|
331
|
+
logger.info("api", `Syncing project ${projectId}`);
|
|
332
|
+
await this.withRetry(async () => {
|
|
333
|
+
const payload = {
|
|
334
|
+
repo_path: data.repoPath,
|
|
335
|
+
remote_url: data.remoteUrl,
|
|
336
|
+
default_branch: data.defaultBranch,
|
|
337
|
+
current_branch: data.currentBranch
|
|
338
|
+
};
|
|
339
|
+
await this.request(`/api/agent/projects/${projectId}/sync`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
body: JSON.stringify(payload)
|
|
342
|
+
});
|
|
343
|
+
logger.success("api", `Project ${projectId} synced`);
|
|
344
|
+
}, "Sync project");
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Test API connectivity
|
|
348
|
+
*/
|
|
349
|
+
async ping() {
|
|
350
|
+
try {
|
|
351
|
+
await this.request("/api/agent/ping");
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw new Error(`Failed to connect to API: ${error.message}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async getLinkedProjects() {
|
|
357
|
+
logger.info("api", "Fetching linked projects");
|
|
358
|
+
const response = await this.request("/api/agent/projects");
|
|
359
|
+
const projects = response.data;
|
|
360
|
+
logger.info("api", `Found ${projects.length} linked projects`);
|
|
361
|
+
return projects;
|
|
362
|
+
}
|
|
363
|
+
async getCurrentUser() {
|
|
364
|
+
logger.info("api", "Fetching current user");
|
|
365
|
+
const user = await this.request("/api/user");
|
|
366
|
+
logger.info("api", `Fetched user: ${user.email}`);
|
|
367
|
+
return user;
|
|
368
|
+
}
|
|
369
|
+
async createProject(data) {
|
|
370
|
+
logger.info("api", `Creating project: ${data.name}`);
|
|
371
|
+
const response = await this.request("/api/agent/projects", {
|
|
372
|
+
method: "POST",
|
|
373
|
+
body: JSON.stringify(data)
|
|
374
|
+
});
|
|
375
|
+
const project = response.data;
|
|
376
|
+
logger.success("api", `Created project: ${project.name} (${project.id})`);
|
|
377
|
+
return project;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
var apiService = new ApiServiceImpl();
|
|
381
|
+
|
|
382
|
+
export {
|
|
383
|
+
apiService
|
|
384
|
+
};
|
package/dist/daemon/runner.js
CHANGED
|
@@ -1,227 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
commitQueue,
|
|
4
|
+
websocketService
|
|
5
|
+
} from "../chunk-IJUJ6NEL.js";
|
|
6
|
+
import {
|
|
7
|
+
apiService
|
|
8
|
+
} from "../chunk-W4JGOGR7.js";
|
|
9
|
+
import {
|
|
4
10
|
gitService,
|
|
5
11
|
projectService,
|
|
6
12
|
removePidFile,
|
|
7
13
|
writePidFile
|
|
8
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-FBQA4K5J.js";
|
|
9
15
|
import {
|
|
10
|
-
apiService,
|
|
11
16
|
authService,
|
|
12
|
-
config,
|
|
13
17
|
logger
|
|
14
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-RHMTZK2J.js";
|
|
15
19
|
|
|
16
20
|
// src/daemon/runner.ts
|
|
17
21
|
import "dotenv/config";
|
|
18
22
|
|
|
19
|
-
// src/services/websocket.ts
|
|
20
|
-
import WebSocket from "ws";
|
|
21
|
-
var WebSocketServiceImpl = class {
|
|
22
|
-
ws = null;
|
|
23
|
-
userId = null;
|
|
24
|
-
reconnectAttempts = 0;
|
|
25
|
-
maxReconnectAttempts = 10;
|
|
26
|
-
reconnectTimer = null;
|
|
27
|
-
isManualDisconnect = false;
|
|
28
|
-
// Event handlers
|
|
29
|
-
commitApprovedHandlers = [];
|
|
30
|
-
commitPendingHandlers = [];
|
|
31
|
-
suggestionCreatedHandlers = [];
|
|
32
|
-
projectUpdatedHandlers = [];
|
|
33
|
-
disconnectHandlers = [];
|
|
34
|
-
agentDisconnectedHandlers = [];
|
|
35
|
-
syncRequestedHandlers = [];
|
|
36
|
-
async connect() {
|
|
37
|
-
try {
|
|
38
|
-
const token = await authService.getToken();
|
|
39
|
-
if (!token) {
|
|
40
|
-
throw new Error("No authentication token available");
|
|
41
|
-
}
|
|
42
|
-
const wsUrl = config.getWsUrl();
|
|
43
|
-
const url = `${wsUrl}?token=${encodeURIComponent(token)}`;
|
|
44
|
-
logger.info("websocket", `Connecting to ${wsUrl}...`);
|
|
45
|
-
this.ws = new WebSocket(url);
|
|
46
|
-
return new Promise((resolve, reject) => {
|
|
47
|
-
if (!this.ws) {
|
|
48
|
-
reject(new Error("WebSocket not initialized"));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
this.ws.on("open", () => {
|
|
52
|
-
logger.success("websocket", "WebSocket connected");
|
|
53
|
-
this.reconnectAttempts = 0;
|
|
54
|
-
this.isManualDisconnect = false;
|
|
55
|
-
resolve();
|
|
56
|
-
});
|
|
57
|
-
this.ws.on("message", (data) => {
|
|
58
|
-
this.handleMessage(data);
|
|
59
|
-
});
|
|
60
|
-
this.ws.on("close", () => {
|
|
61
|
-
logger.warn("websocket", "WebSocket disconnected");
|
|
62
|
-
this.handleDisconnect();
|
|
63
|
-
});
|
|
64
|
-
this.ws.on("error", (error) => {
|
|
65
|
-
logger.error("websocket", "WebSocket error", error);
|
|
66
|
-
reject(error);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
} catch (error) {
|
|
70
|
-
logger.error("websocket", "Failed to connect", error);
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
disconnect() {
|
|
75
|
-
this.isManualDisconnect = true;
|
|
76
|
-
if (this.reconnectTimer) {
|
|
77
|
-
clearTimeout(this.reconnectTimer);
|
|
78
|
-
this.reconnectTimer = null;
|
|
79
|
-
}
|
|
80
|
-
if (this.ws) {
|
|
81
|
-
if (this.userId) {
|
|
82
|
-
this.sendMessage({
|
|
83
|
-
event: "pusher:unsubscribe",
|
|
84
|
-
data: {
|
|
85
|
-
channel: `private-user.${this.userId}`
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
this.ws.close();
|
|
90
|
-
this.ws = null;
|
|
91
|
-
logger.info("websocket", "WebSocket disconnected");
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
isConnected() {
|
|
95
|
-
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
96
|
-
}
|
|
97
|
-
subscribeToUserChannel(userId) {
|
|
98
|
-
this.userId = userId;
|
|
99
|
-
if (!this.isConnected()) {
|
|
100
|
-
logger.warn("websocket", "Cannot subscribe: not connected");
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const channel = `private-user.${userId}`;
|
|
104
|
-
logger.info("websocket", `Subscribing to channel: ${channel}`);
|
|
105
|
-
this.sendMessage({
|
|
106
|
-
event: "pusher:subscribe",
|
|
107
|
-
data: {
|
|
108
|
-
channel
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
onCommitApproved(handler) {
|
|
113
|
-
this.commitApprovedHandlers.push(handler);
|
|
114
|
-
}
|
|
115
|
-
onCommitPending(handler) {
|
|
116
|
-
this.commitPendingHandlers.push(handler);
|
|
117
|
-
}
|
|
118
|
-
onSuggestionCreated(handler) {
|
|
119
|
-
this.suggestionCreatedHandlers.push(handler);
|
|
120
|
-
}
|
|
121
|
-
onProjectUpdated(handler) {
|
|
122
|
-
this.projectUpdatedHandlers.push(handler);
|
|
123
|
-
}
|
|
124
|
-
onDisconnect(handler) {
|
|
125
|
-
this.disconnectHandlers.push(handler);
|
|
126
|
-
}
|
|
127
|
-
onAgentDisconnected(handler) {
|
|
128
|
-
this.agentDisconnectedHandlers.push(handler);
|
|
129
|
-
}
|
|
130
|
-
onSyncRequested(handler) {
|
|
131
|
-
this.syncRequestedHandlers.push(handler);
|
|
132
|
-
}
|
|
133
|
-
sendMessage(message) {
|
|
134
|
-
if (!this.isConnected()) {
|
|
135
|
-
logger.warn("websocket", "Cannot send message: not connected");
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
this.ws.send(JSON.stringify(message));
|
|
139
|
-
}
|
|
140
|
-
handleMessage(data) {
|
|
141
|
-
try {
|
|
142
|
-
const message = JSON.parse(data.toString());
|
|
143
|
-
logger.debug("websocket", `Received message: ${message.event}`);
|
|
144
|
-
if (message.event === "pusher:connection_established") {
|
|
145
|
-
logger.success("websocket", "Connection established");
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (message.event === "pusher_internal:subscription_succeeded") {
|
|
149
|
-
logger.success("websocket", `Subscribed to channel: ${message.channel}`);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (message.event === "commit.approved") {
|
|
153
|
-
const { commit, project } = message.data;
|
|
154
|
-
logger.info("websocket", `Commit approved: ${commit.id}`);
|
|
155
|
-
this.commitApprovedHandlers.forEach((handler) => handler(commit, project));
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (message.event === "commit.pending") {
|
|
159
|
-
const { pendingCommit } = message.data;
|
|
160
|
-
logger.info("websocket", `Commit pending: ${pendingCommit.id}`);
|
|
161
|
-
this.commitPendingHandlers.forEach((handler) => handler(pendingCommit));
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (message.event === "suggestion.created") {
|
|
165
|
-
const { suggestion } = message.data;
|
|
166
|
-
logger.info("websocket", `Suggestion created: ${suggestion.id}`);
|
|
167
|
-
this.suggestionCreatedHandlers.forEach((handler) => handler(suggestion));
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (message.event === "project.updated") {
|
|
171
|
-
const { project } = message.data;
|
|
172
|
-
logger.info("websocket", `Project updated: ${project.id}`);
|
|
173
|
-
this.projectUpdatedHandlers.forEach((handler) => handler(project));
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (message.event === "sync.requested") {
|
|
177
|
-
const { projectId } = message.data;
|
|
178
|
-
logger.info("websocket", `Sync requested for project: ${projectId}`);
|
|
179
|
-
this.syncRequestedHandlers.forEach((handler) => handler(projectId));
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (message.event === "agent.disconnected") {
|
|
183
|
-
const reason = message.data?.reason || "Server requested disconnect";
|
|
184
|
-
logger.warn("websocket", `Agent disconnected by server: ${reason}`);
|
|
185
|
-
this.agentDisconnectedHandlers.forEach((handler) => handler(reason));
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
logger.debug("websocket", `Unhandled event: ${message.event}`);
|
|
189
|
-
} catch (error) {
|
|
190
|
-
logger.error("websocket", "Failed to parse message", error);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
handleDisconnect() {
|
|
194
|
-
this.ws = null;
|
|
195
|
-
this.disconnectHandlers.forEach((handler) => handler());
|
|
196
|
-
if (this.isManualDisconnect) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
200
|
-
const delay = this.getReconnectDelay();
|
|
201
|
-
this.reconnectAttempts++;
|
|
202
|
-
logger.info("websocket", `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
203
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
204
|
-
try {
|
|
205
|
-
await this.connect();
|
|
206
|
-
if (this.userId) {
|
|
207
|
-
this.subscribeToUserChannel(this.userId);
|
|
208
|
-
}
|
|
209
|
-
} catch (error) {
|
|
210
|
-
logger.error("websocket", "Reconnection failed", error);
|
|
211
|
-
}
|
|
212
|
-
}, delay);
|
|
213
|
-
} else {
|
|
214
|
-
logger.error("websocket", "Max reconnection attempts reached");
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
getReconnectDelay() {
|
|
218
|
-
const delays = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
219
|
-
const index = Math.min(this.reconnectAttempts, delays.length - 1);
|
|
220
|
-
return delays[index];
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
var websocketService = new WebSocketServiceImpl();
|
|
224
|
-
|
|
225
23
|
// src/daemon/watcher.ts
|
|
226
24
|
import fs from "fs";
|
|
227
25
|
var FileWatcher = class {
|