ralphblaster-agent 0.1.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/LICENSE +21 -0
- package/README.md +294 -0
- package/bin/agent-dashboard.sh +168 -0
- package/bin/monitor-agent.sh +264 -0
- package/bin/ralphblaster.js +247 -0
- package/package.json +64 -0
- package/postinstall-colored.js +66 -0
- package/src/api-client.js +764 -0
- package/src/claude-plugin/.claude-plugin/plugin.json +9 -0
- package/src/claude-plugin/README.md +42 -0
- package/src/claude-plugin/skills/ralph/SKILL.md +259 -0
- package/src/commands/add-project.js +257 -0
- package/src/commands/init.js +79 -0
- package/src/config-file-manager.js +84 -0
- package/src/config.js +66 -0
- package/src/error-window.js +86 -0
- package/src/executor/claude-runner.js +716 -0
- package/src/executor/error-handler.js +65 -0
- package/src/executor/git-helper.js +196 -0
- package/src/executor/index.js +296 -0
- package/src/executor/job-handlers/clarifying-questions.js +213 -0
- package/src/executor/job-handlers/code-execution.js +145 -0
- package/src/executor/job-handlers/prd-generation.js +259 -0
- package/src/executor/path-helper.js +74 -0
- package/src/executor/prompt-validator.js +51 -0
- package/src/executor.js +4 -0
- package/src/index.js +342 -0
- package/src/logger.js +193 -0
- package/src/logging/README.md +93 -0
- package/src/logging/config.js +179 -0
- package/src/logging/destinations/README.md +290 -0
- package/src/logging/destinations/api-destination-unbatched.js +118 -0
- package/src/logging/destinations/api-destination.js +40 -0
- package/src/logging/destinations/base-destination.js +85 -0
- package/src/logging/destinations/batched-destination.js +198 -0
- package/src/logging/destinations/console-destination.js +172 -0
- package/src/logging/destinations/file-destination.js +208 -0
- package/src/logging/destinations/index.js +29 -0
- package/src/logging/destinations/progress-batch-destination-unbatched.js +92 -0
- package/src/logging/destinations/progress-batch-destination.js +41 -0
- package/src/logging/formatter.js +288 -0
- package/src/logging/log-manager.js +426 -0
- package/src/progress-throttle.js +101 -0
- package/src/system-monitor.js +64 -0
- package/src/utils/format.js +16 -0
- package/src/utils/log-file-helper.js +265 -0
- package/src/utils/progress-parser.js +250 -0
- package/src/worktree-manager.js +255 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseDestination - Abstract base class for log destinations
|
|
3
|
+
*
|
|
4
|
+
* Defines the common interface that all log destinations must implement.
|
|
5
|
+
* Each destination handles writing logs to a specific output (console, file, API, etc.)
|
|
6
|
+
* and manages its own lifecycle (initialization, flushing, cleanup).
|
|
7
|
+
*
|
|
8
|
+
* Destinations are pluggable and independent - they can be added or removed without
|
|
9
|
+
* affecting other destinations.
|
|
10
|
+
*/
|
|
11
|
+
class BaseDestination {
|
|
12
|
+
/**
|
|
13
|
+
* Create a new log destination
|
|
14
|
+
* @param {Object} [config={}] - Destination-specific configuration
|
|
15
|
+
*/
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.isShuttingDown = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Write a log entry to this destination
|
|
23
|
+
* Must be implemented by subclasses.
|
|
24
|
+
* @param {string} level - Log level ('error', 'warn', 'info', 'debug')
|
|
25
|
+
* @param {string} message - Log message
|
|
26
|
+
* @param {Object} [metadata={}] - Structured metadata for filtering/searching
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
* @abstract
|
|
29
|
+
*/
|
|
30
|
+
async write(level, message, metadata = {}) {
|
|
31
|
+
throw new Error('write() must be implemented by subclass');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Flush any buffered logs
|
|
36
|
+
* Some destinations (like API batchers) buffer logs for efficiency.
|
|
37
|
+
* This ensures all pending logs are written immediately.
|
|
38
|
+
* @returns {Promise<void>}
|
|
39
|
+
*/
|
|
40
|
+
async flush() {
|
|
41
|
+
// Default implementation - no buffering
|
|
42
|
+
// Override in subclasses that implement buffering
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Close the destination and release resources
|
|
47
|
+
* Called during shutdown to clean up connections, streams, timers, etc.
|
|
48
|
+
* Should call flush() to ensure no logs are lost.
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async close() {
|
|
52
|
+
this.isShuttingDown = true;
|
|
53
|
+
await this.flush();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if this destination should accept a log at the given level
|
|
58
|
+
* Allows destinations to filter logs based on their own criteria.
|
|
59
|
+
* @param {string} level - Log level to check
|
|
60
|
+
* @returns {boolean} True if this destination should handle this level
|
|
61
|
+
*/
|
|
62
|
+
shouldLog(level) {
|
|
63
|
+
// Default: accept all levels
|
|
64
|
+
// Override in subclasses to implement filtering
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Handle errors that occur during write operations
|
|
70
|
+
* Provides a consistent error handling strategy across destinations.
|
|
71
|
+
* By default, errors are silently caught to prevent log failures from
|
|
72
|
+
* disrupting the application.
|
|
73
|
+
* @param {Error} error - The error that occurred
|
|
74
|
+
* @param {string} level - Log level of the failed write
|
|
75
|
+
* @param {string} message - Log message of the failed write
|
|
76
|
+
* @protected
|
|
77
|
+
*/
|
|
78
|
+
handleError(error, level, message) {
|
|
79
|
+
// Silent by default to prevent cascading failures
|
|
80
|
+
// Subclasses can override to implement error reporting
|
|
81
|
+
// (e.g., console.error for console destination)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = BaseDestination;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const BaseDestination = require('./base-destination');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BatchedDestination - Generic batching wrapper for any log destination
|
|
5
|
+
*
|
|
6
|
+
* Wraps any destination implementing the BaseDestination interface and adds batching
|
|
7
|
+
* capabilities. This reduces overhead for destinations that benefit from batched sends
|
|
8
|
+
* (e.g., network-based destinations like API, syslog, etc.).
|
|
9
|
+
*
|
|
10
|
+
* Key features:
|
|
11
|
+
* - Buffers logs and sends in batches to reduce overhead
|
|
12
|
+
* - Automatic flush on buffer size or time interval
|
|
13
|
+
* - Immediate flush on shutdown to prevent log loss
|
|
14
|
+
* - Graceful fallback to individual sends if batch send fails
|
|
15
|
+
* - Composable - can wrap any destination that implements write()
|
|
16
|
+
*
|
|
17
|
+
* Example usage:
|
|
18
|
+
* ```javascript
|
|
19
|
+
* const apiDestination = new ApiDestination(config);
|
|
20
|
+
* const batchedApi = new BatchedDestination(apiDestination, {
|
|
21
|
+
* maxBatchSize: 10,
|
|
22
|
+
* flushInterval: 2000
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
class BatchedDestination extends BaseDestination {
|
|
27
|
+
/**
|
|
28
|
+
* Create a new batched destination wrapper
|
|
29
|
+
* @param {BaseDestination} destination - The underlying destination to wrap
|
|
30
|
+
* @param {Object} [config={}] - Batching configuration
|
|
31
|
+
* @param {number} [config.maxBatchSize=10] - Maximum logs to buffer before flushing
|
|
32
|
+
* @param {number} [config.flushInterval=2000] - Interval in ms to auto-flush buffered logs
|
|
33
|
+
* @param {boolean} [config.useBatchSend=true] - Whether to try batch sending (via sendBatch)
|
|
34
|
+
*/
|
|
35
|
+
constructor(destination, config = {}) {
|
|
36
|
+
super(config);
|
|
37
|
+
|
|
38
|
+
if (!destination) {
|
|
39
|
+
throw new Error('BatchedDestination requires a destination to wrap');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.destination = destination;
|
|
43
|
+
this.buffer = [];
|
|
44
|
+
|
|
45
|
+
// Configuration
|
|
46
|
+
this.maxBatchSize = config.maxBatchSize || 10;
|
|
47
|
+
this.flushInterval = config.flushInterval || 2000; // 2 seconds
|
|
48
|
+
this.useBatchSend = config.useBatchSend !== false; // Default true
|
|
49
|
+
|
|
50
|
+
// Start automatic flush timer
|
|
51
|
+
this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
|
|
52
|
+
|
|
53
|
+
// Track if we're shutting down
|
|
54
|
+
this.isShuttingDown = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add a log entry to the buffer
|
|
59
|
+
* If buffer is full, automatically flushes.
|
|
60
|
+
* If shutting down, sends immediately without batching.
|
|
61
|
+
* @param {string} level - Log level ('error', 'warn', 'info', 'debug')
|
|
62
|
+
* @param {string} message - Log message
|
|
63
|
+
* @param {Object} [metadata={}] - Structured metadata
|
|
64
|
+
* @returns {Promise<void>}
|
|
65
|
+
*/
|
|
66
|
+
async write(level, message, metadata = {}) {
|
|
67
|
+
if (this.isShuttingDown) {
|
|
68
|
+
// If shutting down, send immediately without batching
|
|
69
|
+
try {
|
|
70
|
+
await this.destination.write(level, message, metadata);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
this.handleError(error, level, message);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const logEntry = {
|
|
78
|
+
timestamp: new Date().toISOString(),
|
|
79
|
+
level,
|
|
80
|
+
message,
|
|
81
|
+
metadata: metadata && Object.keys(metadata).length > 0 ? metadata : undefined
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
this.buffer.push(logEntry);
|
|
85
|
+
|
|
86
|
+
// Flush if buffer is full
|
|
87
|
+
if (this.buffer.length >= this.maxBatchSize) {
|
|
88
|
+
await this.flush();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Flush buffered logs to the wrapped destination
|
|
94
|
+
* Tries batch send first (if destination supports it), falls back to individual sends.
|
|
95
|
+
* Safe to call when buffer is empty (no-op).
|
|
96
|
+
* @returns {Promise<void>}
|
|
97
|
+
*/
|
|
98
|
+
async flush() {
|
|
99
|
+
if (this.buffer.length === 0) return;
|
|
100
|
+
|
|
101
|
+
const batch = [...this.buffer];
|
|
102
|
+
this.buffer = [];
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Check if destination supports batch sending
|
|
106
|
+
if (this.useBatchSend && typeof this.destination.sendBatch === 'function') {
|
|
107
|
+
// Try batch send (more efficient)
|
|
108
|
+
await this.destination.sendBatch(batch);
|
|
109
|
+
} else {
|
|
110
|
+
// Fall back to individual sends
|
|
111
|
+
await this.sendIndividually(batch);
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// If batch send fails, try individual sends as fallback
|
|
115
|
+
try {
|
|
116
|
+
await this.sendIndividually(batch);
|
|
117
|
+
} catch (fallbackError) {
|
|
118
|
+
this.handleError(fallbackError, 'error', 'Failed to send buffered logs');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Send logs individually (fallback method)
|
|
125
|
+
* Used when batch sending fails or is unavailable.
|
|
126
|
+
* Silently fails individual log sends to prevent cascading errors.
|
|
127
|
+
* @param {Array<Object>} logs - Array of log entries with {timestamp, level, message, metadata}
|
|
128
|
+
* @returns {Promise<void>}
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
async sendIndividually(logs) {
|
|
132
|
+
const promises = logs.map(log =>
|
|
133
|
+
this.destination.write(log.level, log.message, log.metadata)
|
|
134
|
+
.catch((error) => {
|
|
135
|
+
this.handleError(error, log.level, log.message);
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await Promise.all(promises);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Shutdown the batcher and flush remaining logs
|
|
144
|
+
* Stops the automatic flush timer and ensures all buffered logs are sent.
|
|
145
|
+
* @returns {Promise<void>}
|
|
146
|
+
*/
|
|
147
|
+
async close() {
|
|
148
|
+
this.isShuttingDown = true;
|
|
149
|
+
|
|
150
|
+
// Stop automatic flush timer
|
|
151
|
+
if (this.flushTimer) {
|
|
152
|
+
clearInterval(this.flushTimer);
|
|
153
|
+
this.flushTimer = null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Flush any remaining logs
|
|
157
|
+
await this.flush();
|
|
158
|
+
|
|
159
|
+
// Close the wrapped destination
|
|
160
|
+
if (this.destination && typeof this.destination.close === 'function') {
|
|
161
|
+
await this.destination.close();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get current buffer size (for testing/debugging)
|
|
167
|
+
* @returns {number} Number of logs currently buffered
|
|
168
|
+
*/
|
|
169
|
+
getBufferSize() {
|
|
170
|
+
return this.buffer.length;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if this destination should accept a log at the given level
|
|
175
|
+
* Delegates to the wrapped destination
|
|
176
|
+
* @param {string} level - Log level to check
|
|
177
|
+
* @returns {boolean} True if this destination should handle this level
|
|
178
|
+
*/
|
|
179
|
+
shouldLog(level) {
|
|
180
|
+
return this.destination.shouldLog(level);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle errors during batching operations
|
|
185
|
+
* @param {Error} error - The error that occurred
|
|
186
|
+
* @param {string} level - Log level of failed write
|
|
187
|
+
* @param {string} message - Log message of failed write
|
|
188
|
+
* @protected
|
|
189
|
+
*/
|
|
190
|
+
handleError(error, level, message) {
|
|
191
|
+
// Delegate to wrapped destination's error handling
|
|
192
|
+
if (typeof this.destination.handleError === 'function') {
|
|
193
|
+
this.destination.handleError(error, level, message);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = BatchedDestination;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const BaseDestination = require('./base-destination');
|
|
2
|
+
const {
|
|
3
|
+
formatMessage,
|
|
4
|
+
formatLevel,
|
|
5
|
+
formatMetadata,
|
|
6
|
+
redactSensitiveData,
|
|
7
|
+
formatConsoleData
|
|
8
|
+
} = require('../formatter');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ConsoleDestination - Outputs logs to console (stdout/stderr)
|
|
12
|
+
*
|
|
13
|
+
* Formats logs for human-readable console output with optional colors.
|
|
14
|
+
* Supports both pretty (formatted) and JSON output modes.
|
|
15
|
+
* Does not buffer - writes immediately for real-time visibility.
|
|
16
|
+
*/
|
|
17
|
+
class ConsoleDestination extends BaseDestination {
|
|
18
|
+
/**
|
|
19
|
+
* Create a new console destination
|
|
20
|
+
* @param {Object} [config={}] - Configuration options
|
|
21
|
+
* @param {boolean} [config.colors=true] - Enable colored output
|
|
22
|
+
* @param {string} [config.format='pretty'] - Output format: 'pretty' or 'json'
|
|
23
|
+
* @param {string} [config.minLevel='info'] - Minimum log level to output
|
|
24
|
+
*/
|
|
25
|
+
constructor(config = {}) {
|
|
26
|
+
super(config);
|
|
27
|
+
|
|
28
|
+
this.colors = config.colors !== false;
|
|
29
|
+
this.format = config.format || 'pretty';
|
|
30
|
+
this.minLevel = config.minLevel || 'info';
|
|
31
|
+
|
|
32
|
+
// Log level priorities for filtering
|
|
33
|
+
this.levelPriorities = {
|
|
34
|
+
error: 0,
|
|
35
|
+
warn: 1,
|
|
36
|
+
info: 2,
|
|
37
|
+
debug: 3
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if this destination should log at the given level
|
|
43
|
+
* @param {string} level - Log level to check
|
|
44
|
+
* @returns {boolean} True if should log
|
|
45
|
+
*/
|
|
46
|
+
shouldLog(level) {
|
|
47
|
+
const levelPriority = this.levelPriorities[level];
|
|
48
|
+
const minPriority = this.levelPriorities[this.minLevel];
|
|
49
|
+
|
|
50
|
+
if (levelPriority === undefined || minPriority === undefined) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return levelPriority <= minPriority;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Write a log entry to console
|
|
59
|
+
* @param {string} level - Log level
|
|
60
|
+
* @param {string} message - Log message
|
|
61
|
+
* @param {Object} [metadata={}] - Structured metadata
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
async write(level, message, metadata = {}) {
|
|
65
|
+
if (!this.shouldLog(level)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Redact sensitive data before output
|
|
71
|
+
const safeMessage = redactSensitiveData(message);
|
|
72
|
+
const safeMetadata = redactSensitiveData(metadata);
|
|
73
|
+
|
|
74
|
+
let output;
|
|
75
|
+
|
|
76
|
+
if (this.format === 'json') {
|
|
77
|
+
output = this.formatJson(level, safeMessage, safeMetadata);
|
|
78
|
+
} else {
|
|
79
|
+
output = this.formatPretty(level, safeMessage, safeMetadata);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Write to stderr for errors, stdout for everything else
|
|
83
|
+
if (level === 'error') {
|
|
84
|
+
console.error(output);
|
|
85
|
+
} else {
|
|
86
|
+
console.log(output);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
this.handleError(error, level, message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format log entry as JSON
|
|
95
|
+
* @param {string} level - Log level
|
|
96
|
+
* @param {string} message - Log message
|
|
97
|
+
* @param {Object} metadata - Log metadata
|
|
98
|
+
* @returns {string} JSON formatted log
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
formatJson(level, message, metadata) {
|
|
102
|
+
const entry = {
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
level,
|
|
105
|
+
message
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Add metadata if present
|
|
109
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
110
|
+
entry.metadata = metadata;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return formatMetadata(entry, { indent: 0 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format log entry for pretty console output
|
|
118
|
+
* @param {string} level - Log level
|
|
119
|
+
* @param {string} message - Log message
|
|
120
|
+
* @param {Object} metadata - Log metadata
|
|
121
|
+
* @returns {string} Pretty formatted log
|
|
122
|
+
* @private
|
|
123
|
+
*/
|
|
124
|
+
formatPretty(level, message, metadata) {
|
|
125
|
+
const timestamp = new Date().toISOString();
|
|
126
|
+
const formattedLevel = formatLevel(level, {
|
|
127
|
+
colors: this.colors,
|
|
128
|
+
uppercase: true
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Format message with metadata inline
|
|
132
|
+
const formattedMessage = formatMessage(message, metadata, {
|
|
133
|
+
includeMetadata: true,
|
|
134
|
+
maxFieldLength: 100
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Build output line
|
|
138
|
+
let output = `[${timestamp}] ${formattedLevel} ${formattedMessage}`;
|
|
139
|
+
|
|
140
|
+
// Add detailed metadata if present (excluding fields already shown inline)
|
|
141
|
+
const detailedData = formatConsoleData(metadata, {
|
|
142
|
+
indent: ' ',
|
|
143
|
+
maxKeys: 20,
|
|
144
|
+
maxValueLength: 200
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (detailedData) {
|
|
148
|
+
output += detailedData;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return output;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Handle errors during console writing
|
|
156
|
+
* Attempts to output error to stderr as last resort
|
|
157
|
+
* @param {Error} error - The error that occurred
|
|
158
|
+
* @param {string} level - Log level of failed write
|
|
159
|
+
* @param {string} message - Log message of failed write
|
|
160
|
+
* @protected
|
|
161
|
+
*/
|
|
162
|
+
handleError(error, level, message) {
|
|
163
|
+
try {
|
|
164
|
+
// Try to output a minimal error message
|
|
165
|
+
console.error(`[ConsoleDestination Error] Failed to write log: ${error.message}`);
|
|
166
|
+
} catch {
|
|
167
|
+
// If even this fails, give up silently
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = ConsoleDestination;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
const BaseDestination = require('./base-destination');
|
|
2
|
+
const LogFileHelper = require('../../utils/log-file-helper');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const fsPromises = require('fs').promises;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* FileDestination - Writes logs to job-specific log files
|
|
8
|
+
*
|
|
9
|
+
* Wraps LogFileHelper to provide file-based logging through the destination interface.
|
|
10
|
+
* Supports both streaming (for real-time output) and batch writing modes.
|
|
11
|
+
* Each job gets its own log file in the .rb-logs directory.
|
|
12
|
+
*/
|
|
13
|
+
class FileDestination extends BaseDestination {
|
|
14
|
+
/**
|
|
15
|
+
* Create a new file destination
|
|
16
|
+
* @param {Object} config - Configuration options
|
|
17
|
+
* @param {string} config.workingDir - Working directory for log files
|
|
18
|
+
* @param {Object} config.job - Job object with id and task_title
|
|
19
|
+
* @param {number} config.startTime - Job start timestamp
|
|
20
|
+
* @param {string} config.jobType - Type of job (e.g., 'PRD Generation')
|
|
21
|
+
* @param {boolean} [config.useStream=true] - Use streaming mode vs batch writes
|
|
22
|
+
*/
|
|
23
|
+
constructor(config) {
|
|
24
|
+
super(config);
|
|
25
|
+
|
|
26
|
+
if (!config.workingDir) {
|
|
27
|
+
throw new Error('FileDestination requires workingDir in config');
|
|
28
|
+
}
|
|
29
|
+
if (!config.job) {
|
|
30
|
+
throw new Error('FileDestination requires job in config');
|
|
31
|
+
}
|
|
32
|
+
if (!config.startTime) {
|
|
33
|
+
throw new Error('FileDestination requires startTime in config');
|
|
34
|
+
}
|
|
35
|
+
if (!config.jobType) {
|
|
36
|
+
throw new Error('FileDestination requires jobType in config');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.workingDir = config.workingDir;
|
|
40
|
+
this.job = config.job;
|
|
41
|
+
this.startTime = config.startTime;
|
|
42
|
+
this.jobType = config.jobType;
|
|
43
|
+
this.useStream = config.useStream !== false;
|
|
44
|
+
|
|
45
|
+
// Will be initialized on first write
|
|
46
|
+
this.logFile = null;
|
|
47
|
+
this.logStream = null;
|
|
48
|
+
this.initialized = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Initialize log file/stream on first write
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
async initialize() {
|
|
57
|
+
if (this.initialized) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (this.useStream) {
|
|
63
|
+
// Create log file with stream for real-time writing
|
|
64
|
+
const result = await LogFileHelper.createJobLogStream(
|
|
65
|
+
this.workingDir,
|
|
66
|
+
this.job,
|
|
67
|
+
this.startTime,
|
|
68
|
+
this.jobType
|
|
69
|
+
);
|
|
70
|
+
this.logFile = result.logFile;
|
|
71
|
+
this.logStream = result.logStream;
|
|
72
|
+
} else {
|
|
73
|
+
// For non-streaming mode, we'll use append operations
|
|
74
|
+
// Create initial log file with header
|
|
75
|
+
this.logFile = await LogFileHelper.createJobLogWithContent(
|
|
76
|
+
this.workingDir,
|
|
77
|
+
this.job,
|
|
78
|
+
this.startTime,
|
|
79
|
+
this.jobType,
|
|
80
|
+
'' // Empty content, we'll append logs
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.initialized = true;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
this.handleError(error, 'error', 'Failed to initialize file destination');
|
|
87
|
+
throw error; // Re-throw to indicate initialization failure
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Write a log entry to the log file
|
|
93
|
+
* @param {string} level - Log level
|
|
94
|
+
* @param {string} message - Log message
|
|
95
|
+
* @param {Object} [metadata={}] - Structured metadata
|
|
96
|
+
* @returns {Promise<void>}
|
|
97
|
+
*/
|
|
98
|
+
async write(level, message, metadata = {}) {
|
|
99
|
+
try {
|
|
100
|
+
// Initialize on first write
|
|
101
|
+
if (!this.initialized) {
|
|
102
|
+
await this.initialize();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Format log entry
|
|
106
|
+
const timestamp = new Date().toISOString();
|
|
107
|
+
const formattedLevel = level.toUpperCase().padEnd(5);
|
|
108
|
+
let logLine = `[${timestamp}] ${formattedLevel} ${message}`;
|
|
109
|
+
|
|
110
|
+
// Add metadata if present
|
|
111
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
112
|
+
const metadataStr = JSON.stringify(metadata);
|
|
113
|
+
logLine += ` | ${metadataStr}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
logLine += '\n';
|
|
117
|
+
|
|
118
|
+
// Write to file
|
|
119
|
+
if (this.useStream && this.logStream) {
|
|
120
|
+
// Streaming mode - write immediately
|
|
121
|
+
this.logStream.write(logLine);
|
|
122
|
+
} else if (this.logFile) {
|
|
123
|
+
// Batch mode - append to file
|
|
124
|
+
await fsPromises.appendFile(this.logFile, logLine);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.handleError(error, level, message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Flush any buffered writes
|
|
133
|
+
* For streams, this is a no-op as writes are immediate.
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
*/
|
|
136
|
+
async flush() {
|
|
137
|
+
// Stream writes are immediate, no buffering to flush
|
|
138
|
+
// Non-stream mode uses fsPromises.appendFile which is also immediate
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Close the log file and write completion footer
|
|
143
|
+
* @returns {Promise<void>}
|
|
144
|
+
*/
|
|
145
|
+
async close() {
|
|
146
|
+
if (!this.initialized) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.isShuttingDown = true;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// Flush any pending writes
|
|
154
|
+
await this.flush();
|
|
155
|
+
|
|
156
|
+
// Write completion footer
|
|
157
|
+
if (this.useStream && this.logStream) {
|
|
158
|
+
LogFileHelper.writeCompletionFooterToStream(
|
|
159
|
+
this.logStream,
|
|
160
|
+
this.jobType,
|
|
161
|
+
{}
|
|
162
|
+
);
|
|
163
|
+
// Close the stream
|
|
164
|
+
await new Promise((resolve, reject) => {
|
|
165
|
+
this.logStream.end((err) => {
|
|
166
|
+
if (err) reject(err);
|
|
167
|
+
else resolve();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
} else if (this.logFile) {
|
|
171
|
+
await LogFileHelper.writeCompletionFooter(
|
|
172
|
+
this.logFile,
|
|
173
|
+
this.jobType,
|
|
174
|
+
{}
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
this.handleError(error, 'error', 'Failed to close file destination');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get the log file path
|
|
184
|
+
* Useful for reporting or accessing the log file after job completion.
|
|
185
|
+
* @returns {string|null} Path to log file, or null if not initialized
|
|
186
|
+
*/
|
|
187
|
+
getLogFilePath() {
|
|
188
|
+
return this.logFile;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handle errors during file operations
|
|
193
|
+
* Outputs to console.error since we can't write to the log file
|
|
194
|
+
* @param {Error} error - The error that occurred
|
|
195
|
+
* @param {string} level - Log level of failed write
|
|
196
|
+
* @param {string} message - Log message of failed write
|
|
197
|
+
* @protected
|
|
198
|
+
*/
|
|
199
|
+
handleError(error, level, message) {
|
|
200
|
+
// Can't log to file since that's what failed, use console instead
|
|
201
|
+
console.error(
|
|
202
|
+
`[FileDestination Error] Failed to write to log file: ${error.message}`,
|
|
203
|
+
`\nOriginal log: [${level}] ${message}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = FileDestination;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Destinations Module
|
|
3
|
+
*
|
|
4
|
+
* Exports all available log destinations for easy importing.
|
|
5
|
+
* Each destination implements the BaseDestination interface and can be
|
|
6
|
+
* used independently or coordinated by LogManager.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BaseDestination = require('./base-destination');
|
|
10
|
+
const ConsoleDestination = require('./console-destination');
|
|
11
|
+
const FileDestination = require('./file-destination');
|
|
12
|
+
const ApiDestination = require('./api-destination');
|
|
13
|
+
const BatchedDestination = require('./batched-destination');
|
|
14
|
+
const ApiDestinationUnbatched = require('./api-destination-unbatched');
|
|
15
|
+
const ProgressBatchDestination = require('./progress-batch-destination');
|
|
16
|
+
const ProgressBatchDestinationUnbatched = require('./progress-batch-destination-unbatched');
|
|
17
|
+
const LogManager = require('../log-manager');
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
BaseDestination,
|
|
21
|
+
ConsoleDestination,
|
|
22
|
+
FileDestination,
|
|
23
|
+
ApiDestination,
|
|
24
|
+
BatchedDestination,
|
|
25
|
+
ApiDestinationUnbatched,
|
|
26
|
+
ProgressBatchDestination,
|
|
27
|
+
ProgressBatchDestinationUnbatched,
|
|
28
|
+
LogManager
|
|
29
|
+
};
|