gsd-agent 1.0.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 +221 -0
- package/bin/cli.js +313 -0
- package/dist/auth-flow.d.ts +50 -0
- package/dist/auth-flow.d.ts.map +1 -0
- package/dist/auth-flow.js +233 -0
- package/dist/auth-flow.js.map +1 -0
- package/dist/auth.d.ts +42 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +117 -0
- package/dist/auth.js.map +1 -0
- package/dist/command-executor.d.ts +44 -0
- package/dist/command-executor.d.ts.map +1 -0
- package/dist/command-executor.js +193 -0
- package/dist/command-executor.js.map +1 -0
- package/dist/command-executor.test.d.ts +8 -0
- package/dist/command-executor.test.d.ts.map +1 -0
- package/dist/command-executor.test.js +87 -0
- package/dist/command-executor.test.js.map +1 -0
- package/dist/command-queue.d.ts +44 -0
- package/dist/command-queue.d.ts.map +1 -0
- package/dist/command-queue.js +184 -0
- package/dist/command-queue.js.map +1 -0
- package/dist/command-queue.test.d.ts +7 -0
- package/dist/command-queue.test.d.ts.map +1 -0
- package/dist/command-queue.test.js +220 -0
- package/dist/command-queue.test.js.map +1 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +103 -0
- package/dist/config.js.map +1 -0
- package/dist/conflict-resolver.d.ts +43 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +91 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/conflict-resolver.test.d.ts +7 -0
- package/dist/conflict-resolver.test.d.ts.map +1 -0
- package/dist/conflict-resolver.test.js +123 -0
- package/dist/conflict-resolver.test.js.map +1 -0
- package/dist/discovery.d.ts +59 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +180 -0
- package/dist/discovery.js.map +1 -0
- package/dist/discovery.test.d.ts +8 -0
- package/dist/discovery.test.d.ts.map +1 -0
- package/dist/discovery.test.js +132 -0
- package/dist/discovery.test.js.map +1 -0
- package/dist/hash.d.ts +20 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +35 -0
- package/dist/hash.js.map +1 -0
- package/dist/hash.test.d.ts +7 -0
- package/dist/hash.test.d.ts.map +1 -0
- package/dist/hash.test.js +58 -0
- package/dist/hash.test.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +202 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.test.d.ts +8 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +37 -0
- package/dist/integration.test.js.map +1 -0
- package/dist/logger.d.ts +68 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +159 -0
- package/dist/logger.js.map +1 -0
- package/dist/output-streamer.d.ts +27 -0
- package/dist/output-streamer.d.ts.map +1 -0
- package/dist/output-streamer.js +71 -0
- package/dist/output-streamer.js.map +1 -0
- package/dist/output-streamer.test.d.ts +7 -0
- package/dist/output-streamer.test.d.ts.map +1 -0
- package/dist/output-streamer.test.js +90 -0
- package/dist/output-streamer.test.js.map +1 -0
- package/dist/realtime-subscriber.d.ts +63 -0
- package/dist/realtime-subscriber.d.ts.map +1 -0
- package/dist/realtime-subscriber.js +201 -0
- package/dist/realtime-subscriber.js.map +1 -0
- package/dist/realtime-subscriber.test.d.ts +7 -0
- package/dist/realtime-subscriber.test.d.ts.map +1 -0
- package/dist/realtime-subscriber.test.js +183 -0
- package/dist/realtime-subscriber.test.js.map +1 -0
- package/dist/reconnection-manager.d.ts +88 -0
- package/dist/reconnection-manager.d.ts.map +1 -0
- package/dist/reconnection-manager.js +229 -0
- package/dist/reconnection-manager.js.map +1 -0
- package/dist/reconnection-manager.test.d.ts +8 -0
- package/dist/reconnection-manager.test.d.ts.map +1 -0
- package/dist/reconnection-manager.test.js +151 -0
- package/dist/reconnection-manager.test.js.map +1 -0
- package/dist/remote-sync-handler.d.ts +61 -0
- package/dist/remote-sync-handler.d.ts.map +1 -0
- package/dist/remote-sync-handler.js +197 -0
- package/dist/remote-sync-handler.js.map +1 -0
- package/dist/remote-sync-handler.test.d.ts +7 -0
- package/dist/remote-sync-handler.test.d.ts.map +1 -0
- package/dist/remote-sync-handler.test.js +212 -0
- package/dist/remote-sync-handler.test.js.map +1 -0
- package/dist/retry.d.ts +35 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +63 -0
- package/dist/retry.js.map +1 -0
- package/dist/retry.test.d.ts +5 -0
- package/dist/retry.test.d.ts.map +1 -0
- package/dist/retry.test.js +84 -0
- package/dist/retry.test.js.map +1 -0
- package/dist/storage-client.d.ts +69 -0
- package/dist/storage-client.d.ts.map +1 -0
- package/dist/storage-client.js +168 -0
- package/dist/storage-client.js.map +1 -0
- package/dist/storage-client.test.d.ts +7 -0
- package/dist/storage-client.test.d.ts.map +1 -0
- package/dist/storage-client.test.js +126 -0
- package/dist/storage-client.test.js.map +1 -0
- package/dist/supabase.d.ts +82 -0
- package/dist/supabase.d.ts.map +1 -0
- package/dist/supabase.js +341 -0
- package/dist/supabase.js.map +1 -0
- package/dist/supabase.test.d.ts +7 -0
- package/dist/supabase.test.d.ts.map +1 -0
- package/dist/supabase.test.js +273 -0
- package/dist/supabase.test.js.map +1 -0
- package/dist/sync-engine.d.ts +84 -0
- package/dist/sync-engine.d.ts.map +1 -0
- package/dist/sync-engine.js +251 -0
- package/dist/sync-engine.js.map +1 -0
- package/dist/sync-engine.test.d.ts +7 -0
- package/dist/sync-engine.test.d.ts.map +1 -0
- package/dist/sync-engine.test.js +241 -0
- package/dist/sync-engine.test.js.map +1 -0
- package/dist/sync-state.d.ts +82 -0
- package/dist/sync-state.d.ts.map +1 -0
- package/dist/sync-state.js +145 -0
- package/dist/sync-state.js.map +1 -0
- package/dist/sync-state.test.d.ts +7 -0
- package/dist/sync-state.test.d.ts.map +1 -0
- package/dist/sync-state.test.js +129 -0
- package/dist/sync-state.test.js.map +1 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/types.test.d.ts +7 -0
- package/dist/types.test.d.ts.map +1 -0
- package/dist/types.test.js +73 -0
- package/dist/types.test.js.map +1 -0
- package/dist/watcher.d.ts +55 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +214 -0
- package/dist/watcher.js.map +1 -0
- package/dist/watcher.test.d.ts +8 -0
- package/dist/watcher.test.d.ts.map +1 -0
- package/dist/watcher.test.js +164 -0
- package/dist/watcher.test.js.map +1 -0
- package/package.json +58 -0
package/dist/logger.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging with file rotation for GSD Agent
|
|
3
|
+
*
|
|
4
|
+
* Provides console and file logging with configurable levels, timestamps,
|
|
5
|
+
* and automatic daily log rotation with cleanup of old logs.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
const LOG_LEVELS = {
|
|
10
|
+
ERROR: 0,
|
|
11
|
+
WARN: 1,
|
|
12
|
+
INFO: 2,
|
|
13
|
+
DEBUG: 3
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Logger class for structured logging
|
|
17
|
+
*
|
|
18
|
+
* Writes to both console and file with timestamps, level prefixes, and optional metadata.
|
|
19
|
+
* Respects configured log level to filter messages.
|
|
20
|
+
*/
|
|
21
|
+
export class Logger {
|
|
22
|
+
logLevel;
|
|
23
|
+
logFile;
|
|
24
|
+
logDir;
|
|
25
|
+
rotationDays;
|
|
26
|
+
currentDate;
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.logLevel = config.log_level;
|
|
29
|
+
this.logFile = config.log_file;
|
|
30
|
+
this.logDir = path.dirname(this.logFile);
|
|
31
|
+
this.rotationDays = config.log_rotation_days;
|
|
32
|
+
this.currentDate = this.getDateString();
|
|
33
|
+
// Create log directory if it doesn't exist
|
|
34
|
+
if (!fs.existsSync(this.logDir)) {
|
|
35
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
// Clean up old logs on initialization
|
|
38
|
+
this.cleanupOldLogs();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get current date string for log rotation (YYYY-MM-DD)
|
|
42
|
+
*/
|
|
43
|
+
getDateString() {
|
|
44
|
+
const now = new Date();
|
|
45
|
+
return now.toISOString().split('T')[0];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get rotated log file path for a specific date
|
|
49
|
+
*/
|
|
50
|
+
getRotatedLogPath(date) {
|
|
51
|
+
const ext = path.extname(this.logFile);
|
|
52
|
+
const base = path.basename(this.logFile, ext);
|
|
53
|
+
return path.join(this.logDir, `${base}-${date}${ext}`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check if log rotation is needed and rotate if necessary
|
|
57
|
+
*/
|
|
58
|
+
checkRotation() {
|
|
59
|
+
const today = this.getDateString();
|
|
60
|
+
if (today !== this.currentDate) {
|
|
61
|
+
// Rotate: rename current log to dated log
|
|
62
|
+
if (fs.existsSync(this.logFile)) {
|
|
63
|
+
const rotatedPath = this.getRotatedLogPath(this.currentDate);
|
|
64
|
+
try {
|
|
65
|
+
fs.renameSync(this.logFile, rotatedPath);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// Ignore rotation errors
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.currentDate = today;
|
|
72
|
+
this.cleanupOldLogs();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Delete logs older than rotation_days
|
|
77
|
+
*/
|
|
78
|
+
cleanupOldLogs() {
|
|
79
|
+
try {
|
|
80
|
+
const files = fs.readdirSync(this.logDir);
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const maxAge = this.rotationDays * 24 * 60 * 60 * 1000;
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
if (file.endsWith('.log') && file !== path.basename(this.logFile)) {
|
|
85
|
+
const filePath = path.join(this.logDir, file);
|
|
86
|
+
const stats = fs.statSync(filePath);
|
|
87
|
+
const age = now - stats.mtime.getTime();
|
|
88
|
+
if (age > maxAge) {
|
|
89
|
+
fs.unlinkSync(filePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Ignore cleanup errors
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check if a message at the given level should be logged
|
|
100
|
+
*/
|
|
101
|
+
shouldLog(level) {
|
|
102
|
+
return LOG_LEVELS[level] <= LOG_LEVELS[this.logLevel];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Format and write log message to console and file
|
|
106
|
+
*/
|
|
107
|
+
log(level, message, meta) {
|
|
108
|
+
if (!this.shouldLog(level)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.checkRotation();
|
|
112
|
+
const timestamp = new Date().toISOString();
|
|
113
|
+
const metaStr = meta ? ' ' + JSON.stringify(meta) : '';
|
|
114
|
+
const logLine = `[${timestamp}] [${level}] ${message}${metaStr}\n`;
|
|
115
|
+
// Write to console
|
|
116
|
+
const consoleMethod = level === 'ERROR' ? console.error : console.log;
|
|
117
|
+
consoleMethod(logLine.trim());
|
|
118
|
+
// Write to file
|
|
119
|
+
try {
|
|
120
|
+
fs.appendFileSync(this.logFile, logLine);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// Ignore file write errors
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Log error message
|
|
128
|
+
*/
|
|
129
|
+
error(message, meta) {
|
|
130
|
+
this.log('ERROR', message, meta);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Log warning message
|
|
134
|
+
*/
|
|
135
|
+
warn(message, meta) {
|
|
136
|
+
this.log('WARN', message, meta);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Log info message
|
|
140
|
+
*/
|
|
141
|
+
info(message, meta) {
|
|
142
|
+
this.log('INFO', message, meta);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Log debug message
|
|
146
|
+
*/
|
|
147
|
+
debug(message, meta) {
|
|
148
|
+
this.log('DEBUG', message, meta);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create a logger instance with the given configuration
|
|
153
|
+
* @param config SyncConfig containing log settings
|
|
154
|
+
* @returns Logger instance
|
|
155
|
+
*/
|
|
156
|
+
export function createLogger(config) {
|
|
157
|
+
return new Logger(config);
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Batcher for Command Execution
|
|
3
|
+
*
|
|
4
|
+
* Batches command output writes to prevent database flooding.
|
|
5
|
+
* Flushes on 10 lines OR 100ms inactivity, whichever comes first.
|
|
6
|
+
*/
|
|
7
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
8
|
+
/**
|
|
9
|
+
* OutputBatcher accumulates command output and flushes in batches
|
|
10
|
+
*/
|
|
11
|
+
export declare class OutputBatcher {
|
|
12
|
+
private batch;
|
|
13
|
+
private flushTimer;
|
|
14
|
+
private supabase;
|
|
15
|
+
constructor(supabase: SupabaseClient);
|
|
16
|
+
/**
|
|
17
|
+
* Add a line to the batch
|
|
18
|
+
* Triggers flush if batch reaches 10 lines, otherwise sets timer for 100ms
|
|
19
|
+
*/
|
|
20
|
+
add(commandId: string, lineNumber: number, content: string, stream: 'stdout' | 'stderr'): void;
|
|
21
|
+
/**
|
|
22
|
+
* Flush batch to database
|
|
23
|
+
* Clears batch and timer after insert
|
|
24
|
+
*/
|
|
25
|
+
flush(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=output-streamer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-streamer.d.ts","sourceRoot":"","sources":["../src/output-streamer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAS3D;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAgB;gBAEpB,QAAQ,EAAE,cAAc;IAIpC;;;OAGG;IACH,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI;IAoB9F;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA6B7B"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Batcher for Command Execution
|
|
3
|
+
*
|
|
4
|
+
* Batches command output writes to prevent database flooding.
|
|
5
|
+
* Flushes on 10 lines OR 100ms inactivity, whichever comes first.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* OutputBatcher accumulates command output and flushes in batches
|
|
9
|
+
*/
|
|
10
|
+
export class OutputBatcher {
|
|
11
|
+
batch = [];
|
|
12
|
+
flushTimer = null;
|
|
13
|
+
supabase;
|
|
14
|
+
constructor(supabase) {
|
|
15
|
+
this.supabase = supabase;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Add a line to the batch
|
|
19
|
+
* Triggers flush if batch reaches 10 lines, otherwise sets timer for 100ms
|
|
20
|
+
*/
|
|
21
|
+
add(commandId, lineNumber, content, stream) {
|
|
22
|
+
this.batch.push({
|
|
23
|
+
command_id: commandId,
|
|
24
|
+
line_number: lineNumber,
|
|
25
|
+
content,
|
|
26
|
+
stream
|
|
27
|
+
});
|
|
28
|
+
// Flush immediately if batch is full
|
|
29
|
+
if (this.batch.length >= 10) {
|
|
30
|
+
this.flush();
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// Reset flush timer
|
|
34
|
+
if (this.flushTimer) {
|
|
35
|
+
clearTimeout(this.flushTimer);
|
|
36
|
+
}
|
|
37
|
+
this.flushTimer = setTimeout(() => this.flush(), 100);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Flush batch to database
|
|
42
|
+
* Clears batch and timer after insert
|
|
43
|
+
*/
|
|
44
|
+
async flush() {
|
|
45
|
+
if (this.batch.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Copy batch and clear immediately
|
|
49
|
+
const toInsert = [...this.batch];
|
|
50
|
+
this.batch = [];
|
|
51
|
+
// Clear timer
|
|
52
|
+
if (this.flushTimer) {
|
|
53
|
+
clearTimeout(this.flushTimer);
|
|
54
|
+
this.flushTimer = null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const { error } = await this.supabase
|
|
58
|
+
.from('command_output')
|
|
59
|
+
.insert(toInsert);
|
|
60
|
+
if (error) {
|
|
61
|
+
console.error('Failed to insert command output:', error.message);
|
|
62
|
+
// Don't throw - don't break command execution
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error('Failed to insert command output:', error);
|
|
67
|
+
// Don't throw - don't break command execution
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=output-streamer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-streamer.js","sourceRoot":"","sources":["../src/output-streamer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH;;GAEG;AACH,MAAM,OAAO,aAAa;IAChB,KAAK,GAAiB,EAAE,CAAA;IACxB,UAAU,GAA0B,IAAI,CAAA;IACxC,QAAQ,CAAgB;IAEhC,YAAY,QAAwB;QAClC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,SAAiB,EAAE,UAAkB,EAAE,OAAe,EAAE,MAA2B;QACrF,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;YACd,UAAU,EAAE,SAAS;YACrB,WAAW,EAAE,UAAU;YACvB,OAAO;YACP,MAAM;SACP,CAAC,CAAA;QAEF,qCAAqC;QACrC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;YAC5B,IAAI,CAAC,KAAK,EAAE,CAAA;QACd,CAAC;aAAM,CAAC;YACN,oBAAoB;YACpB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBACpB,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;YAC/B,CAAC;YACD,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,GAAG,CAAC,CAAA;QACvD,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,OAAM;QACR,CAAC;QAED,mCAAmC;QACnC,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;QAChC,IAAI,CAAC,KAAK,GAAG,EAAE,CAAA;QAEf,cAAc;QACd,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;YAC7B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QACxB,CAAC;QAED,IAAI,CAAC;YACH,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ;iBAClC,IAAI,CAAC,gBAAgB,CAAC;iBACtB,MAAM,CAAC,QAAQ,CAAC,CAAA;YAEnB,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;gBAChE,8CAA8C;YAChD,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,CAAC,CAAA;YACxD,8CAA8C;QAChD,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-streamer.test.d.ts","sourceRoot":"","sources":["../src/output-streamer.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for OutputBatcher
|
|
3
|
+
*
|
|
4
|
+
* Verifies batching logic, flush triggers, and database operations.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
|
+
import { OutputBatcher } from './output-streamer.js';
|
|
8
|
+
describe('OutputBatcher', () => {
|
|
9
|
+
let mockSupabase;
|
|
10
|
+
let batcher;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Mock Supabase client
|
|
13
|
+
mockSupabase = {
|
|
14
|
+
from: vi.fn().mockReturnThis(),
|
|
15
|
+
insert: vi.fn().mockResolvedValue({ error: null })
|
|
16
|
+
};
|
|
17
|
+
batcher = new OutputBatcher(mockSupabase);
|
|
18
|
+
});
|
|
19
|
+
it('should accumulate lines in batch', () => {
|
|
20
|
+
batcher.add('cmd-1', 1, 'line 1', 'stdout');
|
|
21
|
+
batcher.add('cmd-1', 2, 'line 2', 'stdout');
|
|
22
|
+
// Batch should have 2 items (not flushed yet)
|
|
23
|
+
expect(batcher['batch'].length).toBe(2);
|
|
24
|
+
});
|
|
25
|
+
it('should flush when batch reaches 10 lines', async () => {
|
|
26
|
+
// Add 10 lines
|
|
27
|
+
for (let i = 1; i <= 10; i++) {
|
|
28
|
+
batcher.add('cmd-1', i, `line ${i}`, 'stdout');
|
|
29
|
+
}
|
|
30
|
+
// Wait for async flush
|
|
31
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
32
|
+
// Verify insert was called
|
|
33
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('command_output');
|
|
34
|
+
expect(mockSupabase.insert).toHaveBeenCalledWith(expect.arrayContaining([
|
|
35
|
+
expect.objectContaining({
|
|
36
|
+
command_id: 'cmd-1',
|
|
37
|
+
line_number: 1,
|
|
38
|
+
content: 'line 1',
|
|
39
|
+
stream: 'stdout'
|
|
40
|
+
})
|
|
41
|
+
]));
|
|
42
|
+
// Batch should be cleared
|
|
43
|
+
expect(batcher['batch'].length).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
it('should flush after 100ms of inactivity', async () => {
|
|
46
|
+
batcher.add('cmd-1', 1, 'line 1', 'stdout');
|
|
47
|
+
// Wait for flush timer (100ms + buffer)
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
49
|
+
// Verify insert was called
|
|
50
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('command_output');
|
|
51
|
+
expect(mockSupabase.insert).toHaveBeenCalled();
|
|
52
|
+
// Batch should be cleared
|
|
53
|
+
expect(batcher['batch'].length).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
it('should insert batch to database on flush', async () => {
|
|
56
|
+
batcher.add('cmd-1', 1, 'line 1', 'stdout');
|
|
57
|
+
batcher.add('cmd-1', 2, 'line 2', 'stderr');
|
|
58
|
+
await batcher.flush();
|
|
59
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('command_output');
|
|
60
|
+
expect(mockSupabase.insert).toHaveBeenCalledWith([
|
|
61
|
+
{
|
|
62
|
+
command_id: 'cmd-1',
|
|
63
|
+
line_number: 1,
|
|
64
|
+
content: 'line 1',
|
|
65
|
+
stream: 'stdout'
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
command_id: 'cmd-1',
|
|
69
|
+
line_number: 2,
|
|
70
|
+
content: 'line 2',
|
|
71
|
+
stream: 'stderr'
|
|
72
|
+
}
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
it('should clear batch after insert', async () => {
|
|
76
|
+
batcher.add('cmd-1', 1, 'line 1', 'stdout');
|
|
77
|
+
await batcher.flush();
|
|
78
|
+
expect(batcher['batch'].length).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
it('should handle database errors gracefully', async () => {
|
|
81
|
+
// Mock database error
|
|
82
|
+
mockSupabase.insert.mockResolvedValue({
|
|
83
|
+
error: { message: 'Database error' }
|
|
84
|
+
});
|
|
85
|
+
batcher.add('cmd-1', 1, 'line 1', 'stdout');
|
|
86
|
+
// Should not throw
|
|
87
|
+
await expect(batcher.flush()).resolves.not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=output-streamer.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-streamer.test.js","sourceRoot":"","sources":["../src/output-streamer.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Realtime subscriber for bidirectional sync
|
|
3
|
+
*
|
|
4
|
+
* Listens for remote file changes via Supabase Realtime postgres_changes events.
|
|
5
|
+
* Detects conflicts when local and remote versions differ.
|
|
6
|
+
* Emits events for remote changes and conflicts.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import type { ConflictEvent, SyncConfig } from './types.js';
|
|
10
|
+
import type { Logger } from './logger.js';
|
|
11
|
+
/**
|
|
12
|
+
* RealtimeSubscriber manages Supabase Realtime subscriptions
|
|
13
|
+
*
|
|
14
|
+
* Subscribes to postgres_changes events on the files table filtered by workspace_id.
|
|
15
|
+
* Emits 'remote-change' events when files are inserted, updated, or deleted remotely.
|
|
16
|
+
* Provides conflict detection by comparing local and remote file hashes.
|
|
17
|
+
*/
|
|
18
|
+
export declare class RealtimeSubscriber extends EventEmitter {
|
|
19
|
+
private client;
|
|
20
|
+
private config;
|
|
21
|
+
private logger;
|
|
22
|
+
private hashFile;
|
|
23
|
+
private channels;
|
|
24
|
+
constructor(client: any, config: SyncConfig, logger: Logger, hashFile: (filePath: string) => Promise<string>);
|
|
25
|
+
/**
|
|
26
|
+
* Subscribe to Realtime events for a workspace
|
|
27
|
+
* @param workspace_id - Workspace ID to subscribe to
|
|
28
|
+
*/
|
|
29
|
+
subscribe(workspace_id: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Handle Postgres change event from Realtime
|
|
32
|
+
* @param payload - Realtime payload
|
|
33
|
+
*/
|
|
34
|
+
private handlePostgresChange;
|
|
35
|
+
/**
|
|
36
|
+
* Detect conflict between local and remote versions
|
|
37
|
+
* @param workspace_id - Workspace ID
|
|
38
|
+
* @param file_path - File path relative to workspace root
|
|
39
|
+
* @param remote_hash - Remote file hash
|
|
40
|
+
* @param remote_content - Remote file content
|
|
41
|
+
* @param local_content - Local file content
|
|
42
|
+
* @returns ConflictEvent if conflict detected, null otherwise
|
|
43
|
+
*/
|
|
44
|
+
detectConflict(workspace_id: string, file_path: string, remote_hash: string, remote_content: string, local_content: string): Promise<ConflictEvent | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Unsubscribe from a workspace's Realtime channel
|
|
47
|
+
* @param workspace_id - Workspace ID to unsubscribe from
|
|
48
|
+
*/
|
|
49
|
+
unsubscribe(workspace_id: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Unsubscribe from all Realtime channels
|
|
52
|
+
*/
|
|
53
|
+
unsubscribeAll(): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Factory function to create RealtimeSubscriber
|
|
57
|
+
* @param supabase - Supabase client instance
|
|
58
|
+
* @param config - Sync configuration
|
|
59
|
+
* @param logger - Logger instance
|
|
60
|
+
* @returns RealtimeSubscriber instance
|
|
61
|
+
*/
|
|
62
|
+
export declare function createRealtimeSubscriber(supabase: any, config: SyncConfig, logger: Logger): RealtimeSubscriber;
|
|
63
|
+
//# sourceMappingURL=realtime-subscriber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-subscriber.d.ts","sourceRoot":"","sources":["../src/realtime-subscriber.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AAErC,OAAO,KAAK,EAAqB,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;IAClD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,QAAQ,CAA8B;gBAG5C,MAAM,EAAE,GAAG,EACX,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC;IAUjD;;;OAGG;IACH,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;IAgCrC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA4C5B;;;;;;;;OAQG;IACG,cAAc,CAClB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAqDhC;;;OAGG;IACG,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBtD;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;CActC;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,GACb,kBAAkB,CAGpB"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Realtime subscriber for bidirectional sync
|
|
3
|
+
*
|
|
4
|
+
* Listens for remote file changes via Supabase Realtime postgres_changes events.
|
|
5
|
+
* Detects conflicts when local and remote versions differ.
|
|
6
|
+
* Emits events for remote changes and conflicts.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import { computeHash, hashFile } from './hash.js';
|
|
10
|
+
/**
|
|
11
|
+
* RealtimeSubscriber manages Supabase Realtime subscriptions
|
|
12
|
+
*
|
|
13
|
+
* Subscribes to postgres_changes events on the files table filtered by workspace_id.
|
|
14
|
+
* Emits 'remote-change' events when files are inserted, updated, or deleted remotely.
|
|
15
|
+
* Provides conflict detection by comparing local and remote file hashes.
|
|
16
|
+
*/
|
|
17
|
+
export class RealtimeSubscriber extends EventEmitter {
|
|
18
|
+
client;
|
|
19
|
+
config;
|
|
20
|
+
logger;
|
|
21
|
+
hashFile;
|
|
22
|
+
channels;
|
|
23
|
+
constructor(client, config, logger, hashFile) {
|
|
24
|
+
super();
|
|
25
|
+
this.client = client;
|
|
26
|
+
this.config = config;
|
|
27
|
+
this.logger = logger;
|
|
28
|
+
this.hashFile = hashFile;
|
|
29
|
+
this.channels = new Map();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to Realtime events for a workspace
|
|
33
|
+
* @param workspace_id - Workspace ID to subscribe to
|
|
34
|
+
*/
|
|
35
|
+
subscribe(workspace_id) {
|
|
36
|
+
const channelName = `workspace-${workspace_id}`;
|
|
37
|
+
// Check if already subscribed
|
|
38
|
+
if (this.channels.has(channelName)) {
|
|
39
|
+
this.logger.warn('Already subscribed to workspace', { workspace_id });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.logger.info('Subscribing to Realtime channel', { workspace_id, channel: channelName });
|
|
43
|
+
// Create Realtime channel with postgres_changes filter
|
|
44
|
+
const channel = this.client
|
|
45
|
+
.channel(channelName)
|
|
46
|
+
.on('postgres_changes', {
|
|
47
|
+
event: '*',
|
|
48
|
+
schema: 'public',
|
|
49
|
+
table: 'files',
|
|
50
|
+
filter: `workspace_id=eq.${workspace_id}`
|
|
51
|
+
}, (payload) => {
|
|
52
|
+
this.handlePostgresChange(payload);
|
|
53
|
+
})
|
|
54
|
+
.subscribe();
|
|
55
|
+
this.channels.set(channelName, channel);
|
|
56
|
+
this.logger.debug('Subscribed to Realtime channel', { workspace_id, channel: channelName });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Handle Postgres change event from Realtime
|
|
60
|
+
* @param payload - Realtime payload
|
|
61
|
+
*/
|
|
62
|
+
handlePostgresChange(payload) {
|
|
63
|
+
const { eventType, new: newRecord, old: oldRecord } = payload;
|
|
64
|
+
if (eventType === 'INSERT' || eventType === 'UPDATE') {
|
|
65
|
+
// Emit remote change event for INSERT/UPDATE
|
|
66
|
+
const event = {
|
|
67
|
+
id: newRecord.id,
|
|
68
|
+
workspace_id: newRecord.workspace_id,
|
|
69
|
+
file_path: newRecord.file_path,
|
|
70
|
+
content: newRecord.content,
|
|
71
|
+
content_hash: newRecord.content_hash,
|
|
72
|
+
size: newRecord.size,
|
|
73
|
+
updated_at: newRecord.updated_at,
|
|
74
|
+
storage_url: newRecord.storage_url
|
|
75
|
+
};
|
|
76
|
+
this.logger.debug('Remote change detected', {
|
|
77
|
+
event_type: eventType,
|
|
78
|
+
workspace_id: event.workspace_id,
|
|
79
|
+
file_path: event.file_path
|
|
80
|
+
});
|
|
81
|
+
this.emit('remote-change', event);
|
|
82
|
+
}
|
|
83
|
+
else if (eventType === 'DELETE') {
|
|
84
|
+
// Emit remote change event for DELETE with null content
|
|
85
|
+
const event = {
|
|
86
|
+
id: oldRecord.id,
|
|
87
|
+
workspace_id: oldRecord.workspace_id,
|
|
88
|
+
file_path: oldRecord.file_path,
|
|
89
|
+
content: null,
|
|
90
|
+
content_hash: '',
|
|
91
|
+
size: 0,
|
|
92
|
+
updated_at: new Date().toISOString()
|
|
93
|
+
};
|
|
94
|
+
this.logger.debug('Remote deletion detected', {
|
|
95
|
+
workspace_id: event.workspace_id,
|
|
96
|
+
file_path: event.file_path
|
|
97
|
+
});
|
|
98
|
+
this.emit('remote-change', event);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Detect conflict between local and remote versions
|
|
103
|
+
* @param workspace_id - Workspace ID
|
|
104
|
+
* @param file_path - File path relative to workspace root
|
|
105
|
+
* @param remote_hash - Remote file hash
|
|
106
|
+
* @param remote_content - Remote file content
|
|
107
|
+
* @param local_content - Local file content
|
|
108
|
+
* @returns ConflictEvent if conflict detected, null otherwise
|
|
109
|
+
*/
|
|
110
|
+
async detectConflict(workspace_id, file_path, remote_hash, remote_content, local_content) {
|
|
111
|
+
try {
|
|
112
|
+
// Compute local hash
|
|
113
|
+
const local_hash = computeHash(local_content);
|
|
114
|
+
// Compare hashes
|
|
115
|
+
if (local_hash === remote_hash) {
|
|
116
|
+
this.logger.debug('No conflict - hashes match', {
|
|
117
|
+
workspace_id,
|
|
118
|
+
file_path,
|
|
119
|
+
hash: local_hash
|
|
120
|
+
});
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
// Conflict detected
|
|
124
|
+
this.logger.warn('Conflict detected', {
|
|
125
|
+
workspace_id,
|
|
126
|
+
file_path,
|
|
127
|
+
local_hash,
|
|
128
|
+
remote_hash
|
|
129
|
+
});
|
|
130
|
+
const conflict = {
|
|
131
|
+
workspace_id,
|
|
132
|
+
file_path,
|
|
133
|
+
local_hash,
|
|
134
|
+
remote_hash,
|
|
135
|
+
local_content,
|
|
136
|
+
remote_content,
|
|
137
|
+
detected_at: new Date().toISOString()
|
|
138
|
+
};
|
|
139
|
+
return conflict;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
this.logger.error('Error detecting conflict', {
|
|
143
|
+
workspace_id,
|
|
144
|
+
file_path,
|
|
145
|
+
error
|
|
146
|
+
});
|
|
147
|
+
// Default to conflict on error (safer than silent overwrite)
|
|
148
|
+
return {
|
|
149
|
+
workspace_id,
|
|
150
|
+
file_path,
|
|
151
|
+
local_hash: 'error',
|
|
152
|
+
remote_hash,
|
|
153
|
+
local_content,
|
|
154
|
+
remote_content,
|
|
155
|
+
detected_at: new Date().toISOString()
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Unsubscribe from a workspace's Realtime channel
|
|
161
|
+
* @param workspace_id - Workspace ID to unsubscribe from
|
|
162
|
+
*/
|
|
163
|
+
async unsubscribe(workspace_id) {
|
|
164
|
+
const channelName = `workspace-${workspace_id}`;
|
|
165
|
+
const channel = this.channels.get(channelName);
|
|
166
|
+
if (!channel) {
|
|
167
|
+
this.logger.warn('Channel not found for unsubscribe', { workspace_id });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.logger.info('Unsubscribing from Realtime channel', { workspace_id, channel: channelName });
|
|
171
|
+
await channel.unsubscribe();
|
|
172
|
+
this.channels.delete(channelName);
|
|
173
|
+
this.logger.debug('Unsubscribed from Realtime channel', { workspace_id });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Unsubscribe from all Realtime channels
|
|
177
|
+
*/
|
|
178
|
+
async unsubscribeAll() {
|
|
179
|
+
this.logger.info('Unsubscribing from all Realtime channels', {
|
|
180
|
+
channel_count: this.channels.size
|
|
181
|
+
});
|
|
182
|
+
const unsubscribePromises = Array.from(this.channels.keys()).map(channelName => {
|
|
183
|
+
const workspace_id = channelName.replace('workspace-', '');
|
|
184
|
+
return this.unsubscribe(workspace_id);
|
|
185
|
+
});
|
|
186
|
+
await Promise.all(unsubscribePromises);
|
|
187
|
+
this.logger.debug('Unsubscribed from all channels');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Factory function to create RealtimeSubscriber
|
|
192
|
+
* @param supabase - Supabase client instance
|
|
193
|
+
* @param config - Sync configuration
|
|
194
|
+
* @param logger - Logger instance
|
|
195
|
+
* @returns RealtimeSubscriber instance
|
|
196
|
+
*/
|
|
197
|
+
export function createRealtimeSubscriber(supabase, config, logger) {
|
|
198
|
+
// Use hashFile from hash module
|
|
199
|
+
return new RealtimeSubscriber(supabase, config, logger, hashFile);
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=realtime-subscriber.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-subscriber.js","sourceRoot":"","sources":["../src/realtime-subscriber.ts"],"names":[],"mappings":"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"}
|