@gowelle/stint-agent 1.1.0 → 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 +44 -34
- package/dist/StatusDashboard-7ZIGEOQ4.js +232 -0
- package/dist/api-HLTR2LTF.js +7 -0
- package/dist/{chunk-OHOFKJL7.js → chunk-FBQA4K5J.js} +95 -207
- package/dist/chunk-IJUJ6NEL.js +369 -0
- package/dist/chunk-RHMTZK2J.js +297 -0
- package/dist/{chunk-IAERVP6F.js → chunk-W4JGOGR7.js} +42 -291
- package/dist/daemon/runner.js +8 -236
- package/dist/index.js +577 -70
- package/package.json +23 -1
- package/dist/api-YI2HWZGL.js +0 -6
|
@@ -1,277 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
apiUrl: "https://stint.codes",
|
|
7
|
-
wsUrl: "wss://stint.codes/reverb",
|
|
8
|
-
reverbAppKey: "wtn6tu6lirfv6yflujk7",
|
|
9
|
-
projects: {}
|
|
10
|
-
};
|
|
11
|
-
var ConfigManager = class {
|
|
12
|
-
conf;
|
|
13
|
-
constructor() {
|
|
14
|
-
this.conf = new Conf({
|
|
15
|
-
projectName: "stint",
|
|
16
|
-
defaults: {
|
|
17
|
-
...DEFAULT_CONFIG,
|
|
18
|
-
machineId: randomUUID(),
|
|
19
|
-
machineName: os.hostname()
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
get(key) {
|
|
24
|
-
return this.conf.get(key);
|
|
25
|
-
}
|
|
26
|
-
set(key, value) {
|
|
27
|
-
this.conf.set(key, value);
|
|
28
|
-
}
|
|
29
|
-
getAll() {
|
|
30
|
-
return this.conf.store;
|
|
31
|
-
}
|
|
32
|
-
clear() {
|
|
33
|
-
this.conf.clear();
|
|
34
|
-
}
|
|
35
|
-
// Token management
|
|
36
|
-
getToken() {
|
|
37
|
-
return this.conf.get("token");
|
|
38
|
-
}
|
|
39
|
-
setToken(token) {
|
|
40
|
-
this.conf.set("token", token);
|
|
41
|
-
}
|
|
42
|
-
clearToken() {
|
|
43
|
-
this.conf.delete("token");
|
|
44
|
-
}
|
|
45
|
-
// Machine info
|
|
46
|
-
getMachineId() {
|
|
47
|
-
return this.conf.get("machineId");
|
|
48
|
-
}
|
|
49
|
-
getMachineName() {
|
|
50
|
-
return this.conf.get("machineName");
|
|
51
|
-
}
|
|
52
|
-
// Project management
|
|
53
|
-
getProjects() {
|
|
54
|
-
return this.conf.get("projects") || {};
|
|
55
|
-
}
|
|
56
|
-
getProject(path2) {
|
|
57
|
-
const projects = this.getProjects();
|
|
58
|
-
return projects[path2];
|
|
59
|
-
}
|
|
60
|
-
setProject(path2, project) {
|
|
61
|
-
const projects = this.getProjects();
|
|
62
|
-
projects[path2] = project;
|
|
63
|
-
this.conf.set("projects", projects);
|
|
64
|
-
}
|
|
65
|
-
removeProject(path2) {
|
|
66
|
-
const projects = this.getProjects();
|
|
67
|
-
delete projects[path2];
|
|
68
|
-
this.conf.set("projects", projects);
|
|
69
|
-
}
|
|
70
|
-
// API URLs
|
|
71
|
-
getApiUrl() {
|
|
72
|
-
return this.conf.get("apiUrl");
|
|
73
|
-
}
|
|
74
|
-
getWsUrl() {
|
|
75
|
-
const environment = this.getEnvironment();
|
|
76
|
-
const reverbAppKey = this.getReverbAppKey();
|
|
77
|
-
let baseUrl;
|
|
78
|
-
if (environment === "development") {
|
|
79
|
-
baseUrl = "ws://localhost:8080";
|
|
80
|
-
} else {
|
|
81
|
-
baseUrl = this.conf.get("wsUrl") || "wss://stint.codes/reverb";
|
|
82
|
-
}
|
|
83
|
-
if (reverbAppKey && reverbAppKey.trim() !== "") {
|
|
84
|
-
const cleanBaseUrl2 = baseUrl.replace(/\/$/, "");
|
|
85
|
-
const baseWithoutReverb = cleanBaseUrl2.replace(/\/reverb$/, "");
|
|
86
|
-
return `${baseWithoutReverb}/app/${reverbAppKey}`;
|
|
87
|
-
}
|
|
88
|
-
const cleanBaseUrl = baseUrl.replace(/\/$/, "");
|
|
89
|
-
if (!cleanBaseUrl.includes("/reverb")) {
|
|
90
|
-
return `${cleanBaseUrl}/reverb`;
|
|
91
|
-
}
|
|
92
|
-
return cleanBaseUrl;
|
|
93
|
-
}
|
|
94
|
-
// Environment management
|
|
95
|
-
getEnvironment() {
|
|
96
|
-
const configEnv = this.conf.get("environment");
|
|
97
|
-
if (configEnv === "development" || configEnv === "production") {
|
|
98
|
-
return configEnv;
|
|
99
|
-
}
|
|
100
|
-
const nodeEnv = process.env.NODE_ENV;
|
|
101
|
-
if (nodeEnv === "development" || nodeEnv === "dev") {
|
|
102
|
-
return "development";
|
|
103
|
-
}
|
|
104
|
-
return "production";
|
|
105
|
-
}
|
|
106
|
-
setEnvironment(environment) {
|
|
107
|
-
this.conf.set("environment", environment);
|
|
108
|
-
}
|
|
109
|
-
// Reverb App Key management
|
|
110
|
-
getReverbAppKey() {
|
|
111
|
-
return process.env.REVERB_APP_KEY || process.env.STINT_REVERB_APP_KEY || this.conf.get("reverbAppKey");
|
|
112
|
-
}
|
|
113
|
-
setReverbAppKey(reverbAppKey) {
|
|
114
|
-
this.conf.set("reverbAppKey", reverbAppKey);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
var config = new ConfigManager();
|
|
118
|
-
|
|
119
|
-
// src/utils/crypto.ts
|
|
120
|
-
import crypto from "crypto";
|
|
121
|
-
import os2 from "os";
|
|
122
|
-
function getMachineKey() {
|
|
123
|
-
const machineInfo = `${os2.hostname()}-${os2.platform()}-${os2.arch()}`;
|
|
124
|
-
return crypto.createHash("sha256").update(machineInfo).digest();
|
|
125
|
-
}
|
|
126
|
-
var ALGORITHM = "aes-256-gcm";
|
|
127
|
-
var IV_LENGTH = 16;
|
|
128
|
-
var AUTH_TAG_LENGTH = 16;
|
|
129
|
-
function encrypt(text) {
|
|
130
|
-
const key = getMachineKey();
|
|
131
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
132
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
133
|
-
let encrypted = cipher.update(text, "utf8", "hex");
|
|
134
|
-
encrypted += cipher.final("hex");
|
|
135
|
-
const authTag = cipher.getAuthTag();
|
|
136
|
-
return iv.toString("hex") + authTag.toString("hex") + encrypted;
|
|
137
|
-
}
|
|
138
|
-
function decrypt(encryptedText) {
|
|
139
|
-
const key = getMachineKey();
|
|
140
|
-
const iv = Buffer.from(encryptedText.slice(0, IV_LENGTH * 2), "hex");
|
|
141
|
-
const authTag = Buffer.from(
|
|
142
|
-
encryptedText.slice(IV_LENGTH * 2, (IV_LENGTH + AUTH_TAG_LENGTH) * 2),
|
|
143
|
-
"hex"
|
|
144
|
-
);
|
|
145
|
-
const encrypted = encryptedText.slice((IV_LENGTH + AUTH_TAG_LENGTH) * 2);
|
|
146
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
147
|
-
decipher.setAuthTag(authTag);
|
|
148
|
-
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
149
|
-
decrypted += decipher.final("utf8");
|
|
150
|
-
return decrypted;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// src/utils/logger.ts
|
|
154
|
-
import fs from "fs";
|
|
155
|
-
import path from "path";
|
|
156
|
-
import os3 from "os";
|
|
157
|
-
var LOG_DIR = path.join(os3.homedir(), ".config", "stint", "logs");
|
|
158
|
-
var AGENT_LOG = path.join(LOG_DIR, "agent.log");
|
|
159
|
-
var ERROR_LOG = path.join(LOG_DIR, "error.log");
|
|
160
|
-
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
161
|
-
var MAX_LOG_FILES = 7;
|
|
162
|
-
var Logger = class {
|
|
163
|
-
ensureLogDir() {
|
|
164
|
-
if (!fs.existsSync(LOG_DIR)) {
|
|
165
|
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
rotateLog(logFile) {
|
|
169
|
-
if (!fs.existsSync(logFile)) return;
|
|
170
|
-
const stats = fs.statSync(logFile);
|
|
171
|
-
if (stats.size < MAX_LOG_SIZE) return;
|
|
172
|
-
for (let i = MAX_LOG_FILES - 1; i > 0; i--) {
|
|
173
|
-
const oldFile = `${logFile}.${i}`;
|
|
174
|
-
const newFile = `${logFile}.${i + 1}`;
|
|
175
|
-
if (fs.existsSync(oldFile)) {
|
|
176
|
-
if (i === MAX_LOG_FILES - 1) {
|
|
177
|
-
fs.unlinkSync(oldFile);
|
|
178
|
-
} else {
|
|
179
|
-
fs.renameSync(oldFile, newFile);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
fs.renameSync(logFile, `${logFile}.1`);
|
|
184
|
-
}
|
|
185
|
-
writeLog(level, category, message, logFile) {
|
|
186
|
-
this.ensureLogDir();
|
|
187
|
-
this.rotateLog(logFile);
|
|
188
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
189
|
-
const logLine = `[${timestamp}] ${level.padEnd(5)} [${category}] ${message}
|
|
190
|
-
`;
|
|
191
|
-
fs.appendFileSync(logFile, logLine);
|
|
192
|
-
}
|
|
193
|
-
info(category, message) {
|
|
194
|
-
this.writeLog("INFO", category, message, AGENT_LOG);
|
|
195
|
-
console.log(`\u2139 [${category}] ${message}`);
|
|
196
|
-
}
|
|
197
|
-
warn(category, message) {
|
|
198
|
-
this.writeLog("WARN", category, message, AGENT_LOG);
|
|
199
|
-
console.warn(`\u26A0 [${category}] ${message}`);
|
|
200
|
-
}
|
|
201
|
-
error(category, message, error) {
|
|
202
|
-
const fullMessage = error ? `${message}: ${error.message}` : message;
|
|
203
|
-
this.writeLog("ERROR", category, fullMessage, ERROR_LOG);
|
|
204
|
-
this.writeLog("ERROR", category, fullMessage, AGENT_LOG);
|
|
205
|
-
console.error(`\u2716 [${category}] ${fullMessage}`);
|
|
206
|
-
if (error?.stack) {
|
|
207
|
-
this.writeLog("ERROR", category, error.stack, ERROR_LOG);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
debug(category, message) {
|
|
211
|
-
if (process.env.DEBUG) {
|
|
212
|
-
this.writeLog("DEBUG", category, message, AGENT_LOG);
|
|
213
|
-
console.debug(`\u{1F41B} [${category}] ${message}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
success(category, message) {
|
|
217
|
-
this.writeLog("INFO", category, message, AGENT_LOG);
|
|
218
|
-
console.log(`\u2713 [${category}] ${message}`);
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
var logger = new Logger();
|
|
222
|
-
|
|
223
|
-
// src/services/auth.ts
|
|
224
|
-
var AuthServiceImpl = class {
|
|
225
|
-
async saveToken(token) {
|
|
226
|
-
try {
|
|
227
|
-
const encryptedToken = encrypt(token);
|
|
228
|
-
config.setToken(encryptedToken);
|
|
229
|
-
logger.info("auth", "Token saved successfully");
|
|
230
|
-
} catch (error) {
|
|
231
|
-
logger.error("auth", "Failed to save token", error);
|
|
232
|
-
throw error;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
async getToken() {
|
|
236
|
-
try {
|
|
237
|
-
const encryptedToken = config.getToken();
|
|
238
|
-
if (!encryptedToken) {
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
return decrypt(encryptedToken);
|
|
242
|
-
} catch (error) {
|
|
243
|
-
logger.error("auth", "Failed to decrypt token", error);
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
async clearToken() {
|
|
248
|
-
config.clearToken();
|
|
249
|
-
logger.info("auth", "Token cleared");
|
|
250
|
-
}
|
|
251
|
-
async validateToken() {
|
|
252
|
-
const token = await this.getToken();
|
|
253
|
-
if (!token) {
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
try {
|
|
257
|
-
const { apiService: apiService2 } = await import("./api-YI2HWZGL.js");
|
|
258
|
-
const user = await apiService2.getCurrentUser();
|
|
259
|
-
logger.info("auth", `Token validated for user: ${user.email}`);
|
|
260
|
-
return user;
|
|
261
|
-
} catch (error) {
|
|
262
|
-
logger.warn("auth", "Token validation failed");
|
|
263
|
-
logger.error("auth", "Failed to validate token", error);
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
getMachineId() {
|
|
268
|
-
return config.getMachineId();
|
|
269
|
-
}
|
|
270
|
-
getMachineName() {
|
|
271
|
-
return config.getMachineName();
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
var authService = new AuthServiceImpl();
|
|
1
|
+
import {
|
|
2
|
+
authService,
|
|
3
|
+
config,
|
|
4
|
+
logger
|
|
5
|
+
} from "./chunk-RHMTZK2J.js";
|
|
275
6
|
|
|
276
7
|
// src/utils/circuit-breaker.ts
|
|
277
8
|
var CircuitBreaker = class {
|
|
@@ -367,7 +98,7 @@ var CircuitBreaker = class {
|
|
|
367
98
|
};
|
|
368
99
|
|
|
369
100
|
// src/services/api.ts
|
|
370
|
-
var AGENT_VERSION = "1.
|
|
101
|
+
var AGENT_VERSION = "1.2.0";
|
|
371
102
|
var ApiServiceImpl = class {
|
|
372
103
|
sessionId = null;
|
|
373
104
|
circuitBreaker = new CircuitBreaker({
|
|
@@ -459,14 +190,14 @@ var ApiServiceImpl = class {
|
|
|
459
190
|
*/
|
|
460
191
|
async connect() {
|
|
461
192
|
logger.info("api", "Connecting agent session...");
|
|
462
|
-
const
|
|
193
|
+
const os = `${process.platform}-${process.arch}`;
|
|
463
194
|
return this.withRetry(async () => {
|
|
464
195
|
const response = await this.request("/api/agent/connect", {
|
|
465
196
|
method: "POST",
|
|
466
197
|
body: JSON.stringify({
|
|
467
198
|
machine_id: authService.getMachineId(),
|
|
468
199
|
machine_name: authService.getMachineName(),
|
|
469
|
-
os
|
|
200
|
+
os,
|
|
470
201
|
agent_version: AGENT_VERSION
|
|
471
202
|
})
|
|
472
203
|
});
|
|
@@ -523,13 +254,20 @@ var ApiServiceImpl = class {
|
|
|
523
254
|
const response = await this.request(
|
|
524
255
|
`/api/agent/pending-commits?project_id=${projectId}`
|
|
525
256
|
);
|
|
526
|
-
const commits = response.data.map((item) =>
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
+
});
|
|
533
271
|
logger.info("api", `Found ${commits.length} pending commits`);
|
|
534
272
|
return commits;
|
|
535
273
|
}
|
|
@@ -550,14 +288,20 @@ var ApiServiceImpl = class {
|
|
|
550
288
|
}
|
|
551
289
|
);
|
|
552
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
|
+
}
|
|
553
297
|
const commit = {
|
|
554
298
|
id: data.id,
|
|
555
|
-
projectId
|
|
299
|
+
projectId,
|
|
556
300
|
message: data.message,
|
|
557
301
|
sha: data.sha,
|
|
558
302
|
status: data.status,
|
|
559
|
-
createdAt
|
|
560
|
-
executedAt
|
|
303
|
+
createdAt,
|
|
304
|
+
executedAt,
|
|
561
305
|
error: data.error
|
|
562
306
|
};
|
|
563
307
|
logger.success("api", `Commit ${commitId} marked as executed`);
|
|
@@ -599,6 +343,16 @@ var ApiServiceImpl = class {
|
|
|
599
343
|
logger.success("api", `Project ${projectId} synced`);
|
|
600
344
|
}, "Sync project");
|
|
601
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
|
+
}
|
|
602
356
|
async getLinkedProjects() {
|
|
603
357
|
logger.info("api", "Fetching linked projects");
|
|
604
358
|
const response = await this.request("/api/agent/projects");
|
|
@@ -626,8 +380,5 @@ var ApiServiceImpl = class {
|
|
|
626
380
|
var apiService = new ApiServiceImpl();
|
|
627
381
|
|
|
628
382
|
export {
|
|
629
|
-
|
|
630
|
-
logger,
|
|
631
|
-
apiService,
|
|
632
|
-
authService
|
|
383
|
+
apiService
|
|
633
384
|
};
|
package/dist/daemon/runner.js
CHANGED
|
@@ -1,253 +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
|
-
/**
|
|
37
|
-
* Connect to the WebSocket server
|
|
38
|
-
* @throws Error if connection fails or no auth token available
|
|
39
|
-
*/
|
|
40
|
-
async connect() {
|
|
41
|
-
try {
|
|
42
|
-
const token = await authService.getToken();
|
|
43
|
-
if (!token) {
|
|
44
|
-
throw new Error("No authentication token available");
|
|
45
|
-
}
|
|
46
|
-
const wsUrl = config.getWsUrl();
|
|
47
|
-
const url = `${wsUrl}?token=${encodeURIComponent(token)}`;
|
|
48
|
-
logger.info("websocket", `Connecting to ${wsUrl}...`);
|
|
49
|
-
this.ws = new WebSocket(url);
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
if (!this.ws) {
|
|
52
|
-
reject(new Error("WebSocket not initialized"));
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
this.ws.on("open", () => {
|
|
56
|
-
logger.success("websocket", "WebSocket connected");
|
|
57
|
-
this.reconnectAttempts = 0;
|
|
58
|
-
this.isManualDisconnect = false;
|
|
59
|
-
resolve();
|
|
60
|
-
});
|
|
61
|
-
this.ws.on("message", (data) => {
|
|
62
|
-
this.handleMessage(data);
|
|
63
|
-
});
|
|
64
|
-
this.ws.on("close", () => {
|
|
65
|
-
logger.warn("websocket", "WebSocket disconnected");
|
|
66
|
-
this.handleDisconnect();
|
|
67
|
-
});
|
|
68
|
-
this.ws.on("error", (error) => {
|
|
69
|
-
logger.error("websocket", "WebSocket error", error);
|
|
70
|
-
reject(error);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
} catch (error) {
|
|
74
|
-
logger.error("websocket", "Failed to connect", error);
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Disconnect from the WebSocket server
|
|
80
|
-
* Prevents automatic reconnection
|
|
81
|
-
*/
|
|
82
|
-
disconnect() {
|
|
83
|
-
this.isManualDisconnect = true;
|
|
84
|
-
if (this.reconnectTimer) {
|
|
85
|
-
clearTimeout(this.reconnectTimer);
|
|
86
|
-
this.reconnectTimer = null;
|
|
87
|
-
}
|
|
88
|
-
if (this.ws) {
|
|
89
|
-
if (this.userId) {
|
|
90
|
-
this.sendMessage({
|
|
91
|
-
event: "pusher:unsubscribe",
|
|
92
|
-
data: {
|
|
93
|
-
channel: `private-user.${this.userId}`
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
this.ws.close();
|
|
98
|
-
this.ws = null;
|
|
99
|
-
logger.info("websocket", "WebSocket disconnected");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Check if WebSocket is currently connected
|
|
104
|
-
* @returns True if connected and ready
|
|
105
|
-
*/
|
|
106
|
-
isConnected() {
|
|
107
|
-
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Subscribe to user-specific channel for real-time updates
|
|
111
|
-
* @param userId - User ID to subscribe to
|
|
112
|
-
*/
|
|
113
|
-
subscribeToUserChannel(userId) {
|
|
114
|
-
this.userId = userId;
|
|
115
|
-
if (!this.isConnected()) {
|
|
116
|
-
logger.warn("websocket", "Cannot subscribe: not connected");
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
const channel = `private-user.${userId}`;
|
|
120
|
-
logger.info("websocket", `Subscribing to channel: ${channel}`);
|
|
121
|
-
this.sendMessage({
|
|
122
|
-
event: "pusher:subscribe",
|
|
123
|
-
data: {
|
|
124
|
-
channel
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Register handler for commit approved events
|
|
130
|
-
* @param handler - Callback function
|
|
131
|
-
*/
|
|
132
|
-
onCommitApproved(handler) {
|
|
133
|
-
this.commitApprovedHandlers.push(handler);
|
|
134
|
-
}
|
|
135
|
-
onCommitPending(handler) {
|
|
136
|
-
this.commitPendingHandlers.push(handler);
|
|
137
|
-
}
|
|
138
|
-
onSuggestionCreated(handler) {
|
|
139
|
-
this.suggestionCreatedHandlers.push(handler);
|
|
140
|
-
}
|
|
141
|
-
onProjectUpdated(handler) {
|
|
142
|
-
this.projectUpdatedHandlers.push(handler);
|
|
143
|
-
}
|
|
144
|
-
onDisconnect(handler) {
|
|
145
|
-
this.disconnectHandlers.push(handler);
|
|
146
|
-
}
|
|
147
|
-
onAgentDisconnected(handler) {
|
|
148
|
-
this.agentDisconnectedHandlers.push(handler);
|
|
149
|
-
}
|
|
150
|
-
onSyncRequested(handler) {
|
|
151
|
-
this.syncRequestedHandlers.push(handler);
|
|
152
|
-
}
|
|
153
|
-
sendMessage(message) {
|
|
154
|
-
if (!this.isConnected()) {
|
|
155
|
-
logger.warn("websocket", "Cannot send message: not connected");
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
this.ws.send(JSON.stringify(message));
|
|
159
|
-
}
|
|
160
|
-
handleMessage(data) {
|
|
161
|
-
try {
|
|
162
|
-
const message = JSON.parse(data.toString());
|
|
163
|
-
logger.debug("websocket", `Received message: ${message.event}`);
|
|
164
|
-
if (message.event === "pusher:connection_established") {
|
|
165
|
-
logger.success("websocket", "Connection established");
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (message.event === "pusher_internal:subscription_succeeded") {
|
|
169
|
-
logger.success("websocket", `Subscribed to channel: ${message.channel}`);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (message.event === "commit.approved") {
|
|
173
|
-
const { commit, project } = message.data;
|
|
174
|
-
logger.info("websocket", `Commit approved: ${commit.id}`);
|
|
175
|
-
this.commitApprovedHandlers.forEach((handler) => handler(commit, project));
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (message.event === "commit.pending") {
|
|
179
|
-
const { pendingCommit } = message.data;
|
|
180
|
-
logger.info("websocket", `Commit pending: ${pendingCommit.id}`);
|
|
181
|
-
this.commitPendingHandlers.forEach((handler) => handler(pendingCommit));
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (message.event === "suggestion.created") {
|
|
185
|
-
const { suggestion } = message.data;
|
|
186
|
-
logger.info("websocket", `Suggestion created: ${suggestion.id}`);
|
|
187
|
-
this.suggestionCreatedHandlers.forEach((handler) => handler(suggestion));
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
if (message.event === "project.updated") {
|
|
191
|
-
const { project } = message.data;
|
|
192
|
-
logger.info("websocket", `Project updated: ${project.id}`);
|
|
193
|
-
this.projectUpdatedHandlers.forEach((handler) => handler(project));
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (message.event === "sync.requested") {
|
|
197
|
-
const { projectId } = message.data;
|
|
198
|
-
logger.info("websocket", `Sync requested for project: ${projectId}`);
|
|
199
|
-
this.syncRequestedHandlers.forEach((handler) => handler(projectId));
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (message.event === "agent.disconnected") {
|
|
203
|
-
const reason = message.data?.reason || "Server requested disconnect";
|
|
204
|
-
logger.warn("websocket", `Agent disconnected by server: ${reason}`);
|
|
205
|
-
this.agentDisconnectedHandlers.forEach((handler) => handler(reason));
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
logger.debug("websocket", `Unhandled event: ${message.event}`);
|
|
209
|
-
} catch (error) {
|
|
210
|
-
logger.error("websocket", "Failed to parse message", error);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
handleDisconnect() {
|
|
214
|
-
this.ws = null;
|
|
215
|
-
this.disconnectHandlers.forEach((handler) => handler());
|
|
216
|
-
if (this.isManualDisconnect) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
220
|
-
const delay = this.getReconnectDelay();
|
|
221
|
-
this.reconnectAttempts++;
|
|
222
|
-
logger.info("websocket", `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
223
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
224
|
-
try {
|
|
225
|
-
await this.connect();
|
|
226
|
-
if (this.userId) {
|
|
227
|
-
this.subscribeToUserChannel(this.userId);
|
|
228
|
-
}
|
|
229
|
-
} catch (error) {
|
|
230
|
-
logger.error("websocket", "Reconnection failed", error);
|
|
231
|
-
}
|
|
232
|
-
}, delay);
|
|
233
|
-
} else {
|
|
234
|
-
logger.error("websocket", "Max reconnection attempts reached");
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Get reconnect delay with exponential backoff and jitter
|
|
239
|
-
* Jitter prevents thundering herd problem when many clients reconnect simultaneously
|
|
240
|
-
*/
|
|
241
|
-
getReconnectDelay() {
|
|
242
|
-
const delays = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
243
|
-
const index = Math.min(this.reconnectAttempts, delays.length - 1);
|
|
244
|
-
const baseDelay = delays[index];
|
|
245
|
-
const jitter = baseDelay * (Math.random() * 0.3);
|
|
246
|
-
return Math.floor(baseDelay + jitter);
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
var websocketService = new WebSocketServiceImpl();
|
|
250
|
-
|
|
251
23
|
// src/daemon/watcher.ts
|
|
252
24
|
import fs from "fs";
|
|
253
25
|
var FileWatcher = class {
|