wave-agent-sdk 0.0.7 → 0.0.8
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/dist/agent.d.ts +32 -20
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +202 -20
- package/dist/constants/events.d.ts +28 -0
- package/dist/constants/events.d.ts.map +1 -0
- package/dist/constants/events.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/managers/aiManager.d.ts +34 -1
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +243 -128
- package/dist/managers/backgroundBashManager.d.ts.map +1 -1
- package/dist/managers/backgroundBashManager.js +7 -6
- package/dist/managers/hookManager.d.ts +9 -4
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +62 -30
- package/dist/managers/liveConfigManager.d.ts +58 -0
- package/dist/managers/liveConfigManager.d.ts.map +1 -0
- package/dist/managers/liveConfigManager.js +160 -0
- package/dist/managers/messageManager.d.ts +38 -13
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +163 -30
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +4 -1
- package/dist/managers/subagentManager.d.ts +51 -0
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +189 -18
- package/dist/services/aiService.d.ts +13 -5
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +350 -74
- package/dist/services/configurationWatcher.d.ts +120 -0
- package/dist/services/configurationWatcher.d.ts.map +1 -0
- package/dist/services/configurationWatcher.js +439 -0
- package/dist/services/fileWatcher.d.ts +69 -0
- package/dist/services/fileWatcher.d.ts.map +1 -0
- package/dist/services/fileWatcher.js +213 -0
- package/dist/services/hook.d.ts +91 -9
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +393 -43
- package/dist/services/jsonlHandler.d.ts +62 -0
- package/dist/services/jsonlHandler.d.ts.map +1 -0
- package/dist/services/jsonlHandler.js +257 -0
- package/dist/services/memory.d.ts +9 -0
- package/dist/services/memory.d.ts.map +1 -1
- package/dist/services/memory.js +81 -12
- package/dist/services/memoryStore.d.ts +81 -0
- package/dist/services/memoryStore.d.ts.map +1 -0
- package/dist/services/memoryStore.js +200 -0
- package/dist/services/session.d.ts +64 -49
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +310 -132
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +5 -4
- package/dist/tools/deleteFileTool.d.ts.map +1 -1
- package/dist/tools/deleteFileTool.js +2 -1
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +3 -2
- package/dist/tools/multiEditTool.d.ts.map +1 -1
- package/dist/tools/multiEditTool.js +4 -3
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +2 -1
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +5 -6
- package/dist/types/commands.d.ts +4 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/core.d.ts +35 -0
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/environment.d.ts +42 -0
- package/dist/types/environment.d.ts.map +1 -0
- package/dist/types/environment.js +21 -0
- package/dist/types/hooks.d.ts +8 -2
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +8 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/memoryStore.d.ts +82 -0
- package/dist/types/memoryStore.d.ts.map +1 -0
- package/dist/types/memoryStore.js +7 -0
- package/dist/types/messaging.d.ts +14 -2
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/session.d.ts +20 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +7 -0
- package/dist/utils/bashHistory.d.ts.map +1 -1
- package/dist/utils/bashHistory.js +27 -26
- package/dist/utils/cacheControlUtils.d.ts +121 -0
- package/dist/utils/cacheControlUtils.d.ts.map +1 -0
- package/dist/utils/cacheControlUtils.js +367 -0
- package/dist/utils/commandPathResolver.d.ts +52 -0
- package/dist/utils/commandPathResolver.d.ts.map +1 -0
- package/dist/utils/commandPathResolver.js +145 -0
- package/dist/utils/configPaths.d.ts +85 -0
- package/dist/utils/configPaths.d.ts.map +1 -0
- package/dist/utils/configPaths.js +121 -0
- package/dist/utils/configResolver.d.ts +37 -10
- package/dist/utils/configResolver.d.ts.map +1 -1
- package/dist/utils/configResolver.js +127 -23
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +7 -5
- package/dist/utils/customCommands.d.ts.map +1 -1
- package/dist/utils/customCommands.js +66 -21
- package/dist/utils/fileUtils.d.ts +15 -0
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +61 -0
- package/dist/utils/globalLogger.d.ts +102 -0
- package/dist/utils/globalLogger.d.ts.map +1 -0
- package/dist/utils/globalLogger.js +136 -0
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +25 -3
- package/dist/utils/messageOperations.d.ts +20 -8
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +25 -16
- package/dist/utils/pathEncoder.d.ts +104 -0
- package/dist/utils/pathEncoder.d.ts.map +1 -0
- package/dist/utils/pathEncoder.js +272 -0
- package/dist/utils/subagentParser.d.ts.map +1 -1
- package/dist/utils/subagentParser.js +2 -1
- package/dist/utils/tokenCalculation.d.ts +26 -0
- package/dist/utils/tokenCalculation.d.ts.map +1 -0
- package/dist/utils/tokenCalculation.js +36 -0
- package/package.json +6 -3
- package/src/agent.ts +298 -34
- package/src/constants/events.ts +38 -0
- package/src/index.ts +2 -0
- package/src/managers/aiManager.ts +323 -170
- package/src/managers/backgroundBashManager.ts +7 -6
- package/src/managers/hookManager.ts +83 -40
- package/src/managers/liveConfigManager.ts +248 -0
- package/src/managers/messageManager.ts +230 -63
- package/src/managers/slashCommandManager.ts +4 -1
- package/src/managers/subagentManager.ts +283 -21
- package/src/services/aiService.ts +474 -83
- package/src/services/configurationWatcher.ts +622 -0
- package/src/services/fileWatcher.ts +301 -0
- package/src/services/hook.ts +538 -47
- package/src/services/jsonlHandler.ts +319 -0
- package/src/services/memory.ts +92 -12
- package/src/services/memoryStore.ts +279 -0
- package/src/services/session.ts +381 -157
- package/src/tools/bashTool.ts +5 -4
- package/src/tools/deleteFileTool.ts +2 -1
- package/src/tools/editTool.ts +3 -2
- package/src/tools/multiEditTool.ts +4 -3
- package/src/tools/readTool.ts +2 -1
- package/src/tools/writeTool.ts +7 -6
- package/src/types/commands.ts +6 -0
- package/src/types/core.ts +44 -0
- package/src/types/environment.ts +60 -0
- package/src/types/hooks.ts +21 -8
- package/src/types/index.ts +2 -0
- package/src/types/memoryStore.ts +94 -0
- package/src/types/messaging.ts +14 -2
- package/src/types/session.ts +25 -0
- package/src/utils/bashHistory.ts +27 -27
- package/src/utils/cacheControlUtils.ts +540 -0
- package/src/utils/commandPathResolver.ts +189 -0
- package/src/utils/configPaths.ts +163 -0
- package/src/utils/configResolver.ts +182 -22
- package/src/utils/constants.ts +1 -1
- package/src/utils/convertMessagesForAPI.ts +7 -5
- package/src/utils/customCommands.ts +90 -22
- package/src/utils/fileUtils.ts +65 -0
- package/src/utils/globalLogger.ts +145 -0
- package/src/utils/mcpUtils.ts +34 -3
- package/src/utils/messageOperations.ts +42 -20
- package/src/utils/pathEncoder.ts +379 -0
- package/src/utils/subagentParser.ts +2 -1
- package/src/utils/tokenCalculation.ts +43 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Watcher Service
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates live configuration reload functionality by coordinating file watching,
|
|
5
|
+
* configuration validation, and error recovery. Provides automatic reload of settings.json
|
|
6
|
+
* changes without restart.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EventEmitter } from "events";
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { FileWatcherService, type FileWatchEvent } from "./fileWatcher.js";
|
|
12
|
+
import { loadMergedWaveConfigWithFallback } from "./hook.js";
|
|
13
|
+
import type { WaveConfiguration, ValidationResult } from "../types/hooks.js";
|
|
14
|
+
import { isValidHookEvent, isValidHookEventConfig } from "../types/hooks.js";
|
|
15
|
+
import type { Logger } from "../types/index.js";
|
|
16
|
+
import {
|
|
17
|
+
CONFIGURATION_EVENTS,
|
|
18
|
+
FILE_WATCHER_EVENTS,
|
|
19
|
+
} from "../constants/events.js";
|
|
20
|
+
|
|
21
|
+
export interface ConfigurationChangeEvent {
|
|
22
|
+
type: "settings_changed" | "memory_changed" | "env_changed";
|
|
23
|
+
path: string;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
changes: {
|
|
26
|
+
added: string[];
|
|
27
|
+
modified: string[];
|
|
28
|
+
removed: string[];
|
|
29
|
+
};
|
|
30
|
+
isValid: boolean;
|
|
31
|
+
errorMessage?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ConfigurationReloadService {
|
|
35
|
+
initializeWatching(
|
|
36
|
+
userPaths: string[],
|
|
37
|
+
projectPaths?: string[],
|
|
38
|
+
): Promise<void>;
|
|
39
|
+
reloadConfiguration(): Promise<WaveConfiguration>;
|
|
40
|
+
getCurrentConfiguration(): WaveConfiguration | null;
|
|
41
|
+
validateEnvironmentVariables(env: Record<string, string>): {
|
|
42
|
+
isValid: boolean;
|
|
43
|
+
errors: string[];
|
|
44
|
+
warnings: string[];
|
|
45
|
+
};
|
|
46
|
+
shutdown(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class ConfigurationWatcher
|
|
50
|
+
extends EventEmitter
|
|
51
|
+
implements ConfigurationReloadService
|
|
52
|
+
{
|
|
53
|
+
private fileWatcher: FileWatcherService;
|
|
54
|
+
private logger?: Logger;
|
|
55
|
+
private currentConfiguration: WaveConfiguration | null = null;
|
|
56
|
+
private lastValidConfiguration: WaveConfiguration | null = null;
|
|
57
|
+
private userConfigPaths?: string[];
|
|
58
|
+
private projectConfigPaths?: string[];
|
|
59
|
+
private workdir: string;
|
|
60
|
+
private isWatching: boolean = false;
|
|
61
|
+
private reloadInProgress: boolean = false;
|
|
62
|
+
|
|
63
|
+
constructor(workdir: string, logger?: Logger) {
|
|
64
|
+
super();
|
|
65
|
+
this.workdir = workdir;
|
|
66
|
+
this.logger = logger;
|
|
67
|
+
this.fileWatcher = new FileWatcherService(logger);
|
|
68
|
+
this.setupFileWatcherEvents();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Initialize configuration watching
|
|
73
|
+
* Maps to FR-004: System MUST watch settings.json files
|
|
74
|
+
* Supports watching multiple file paths (e.g., settings.local.json and settings.json)
|
|
75
|
+
*/
|
|
76
|
+
async initializeWatching(
|
|
77
|
+
userPaths: string[],
|
|
78
|
+
projectPaths?: string[],
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
this.logger?.info("Live Config: Initializing configuration watching...");
|
|
82
|
+
|
|
83
|
+
this.userConfigPaths = userPaths;
|
|
84
|
+
this.projectConfigPaths = projectPaths;
|
|
85
|
+
|
|
86
|
+
// Load initial configuration
|
|
87
|
+
await this.reloadConfiguration();
|
|
88
|
+
|
|
89
|
+
// Start watching user configs that exist
|
|
90
|
+
for (const userPath of userPaths) {
|
|
91
|
+
if (existsSync(userPath)) {
|
|
92
|
+
this.logger?.debug(
|
|
93
|
+
`Live Config: Starting to watch user config: ${userPath}`,
|
|
94
|
+
);
|
|
95
|
+
await this.fileWatcher.watchFile(userPath, (event) =>
|
|
96
|
+
this.handleFileChange(event, "user"),
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
this.logger?.debug(
|
|
100
|
+
`Live Config: User config file does not exist: ${userPath}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Start watching project configs that exist
|
|
106
|
+
if (projectPaths) {
|
|
107
|
+
for (const projectPath of projectPaths) {
|
|
108
|
+
if (existsSync(projectPath)) {
|
|
109
|
+
this.logger?.debug(
|
|
110
|
+
`Live Config: Starting to watch project config: ${projectPath}`,
|
|
111
|
+
);
|
|
112
|
+
await this.fileWatcher.watchFile(projectPath, (event) =>
|
|
113
|
+
this.handleFileChange(event, "project"),
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
this.logger?.debug(
|
|
117
|
+
`Live Config: Project config file does not exist: ${projectPath}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.isWatching = true;
|
|
124
|
+
this.logger?.info(
|
|
125
|
+
"Live Config: Configuration watching initialized successfully",
|
|
126
|
+
);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const errorMessage = `Failed to initialize configuration watching: ${(error as Error).message}`;
|
|
129
|
+
this.logger?.error(`Live Config: ${errorMessage}`);
|
|
130
|
+
throw new Error(errorMessage);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reload configuration from files
|
|
136
|
+
* Maps to FR-008: Continue with previous valid configuration on errors
|
|
137
|
+
*/
|
|
138
|
+
async reloadConfiguration(): Promise<WaveConfiguration> {
|
|
139
|
+
if (this.reloadInProgress) {
|
|
140
|
+
this.logger?.debug("Live Config: Reload already in progress, skipping");
|
|
141
|
+
return this.currentConfiguration || {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.reloadInProgress = true;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
this.logger?.debug("Live Config: Reloading configuration from files...");
|
|
148
|
+
|
|
149
|
+
// Load merged configuration with fallback support
|
|
150
|
+
const loadResult = loadMergedWaveConfigWithFallback(
|
|
151
|
+
this.workdir,
|
|
152
|
+
this.lastValidConfiguration,
|
|
153
|
+
);
|
|
154
|
+
const newConfig = loadResult.config;
|
|
155
|
+
const timestamp = Date.now();
|
|
156
|
+
|
|
157
|
+
// Check for errors during loading
|
|
158
|
+
if (loadResult.errors.length > 0) {
|
|
159
|
+
const errorMessage = `Configuration load errors: ${loadResult.errors.join("; ")}`;
|
|
160
|
+
this.logger?.error(`Live Config: ${errorMessage}`);
|
|
161
|
+
|
|
162
|
+
// Emit error event
|
|
163
|
+
this.emit(CONFIGURATION_EVENTS.CONFIGURATION_CHANGE, {
|
|
164
|
+
type: "settings_changed",
|
|
165
|
+
path:
|
|
166
|
+
this.getFirstExistingProjectPath() ||
|
|
167
|
+
this.getFirstExistingUserPath() ||
|
|
168
|
+
"unknown",
|
|
169
|
+
timestamp,
|
|
170
|
+
changes: { added: [], modified: [], removed: [] },
|
|
171
|
+
isValid: false,
|
|
172
|
+
errorMessage,
|
|
173
|
+
} as ConfigurationChangeEvent);
|
|
174
|
+
|
|
175
|
+
// Use fallback configuration if available
|
|
176
|
+
if (loadResult.usedFallback && this.lastValidConfiguration) {
|
|
177
|
+
this.logger?.warn(
|
|
178
|
+
"Live Config: Using previous valid configuration due to load errors",
|
|
179
|
+
);
|
|
180
|
+
this.currentConfiguration = this.lastValidConfiguration;
|
|
181
|
+
return this.currentConfiguration;
|
|
182
|
+
} else {
|
|
183
|
+
this.logger?.warn(
|
|
184
|
+
"Live Config: No previous valid configuration available, using empty config",
|
|
185
|
+
);
|
|
186
|
+
this.currentConfiguration = {};
|
|
187
|
+
return this.currentConfiguration;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Validate new configuration if it exists
|
|
192
|
+
if (newConfig) {
|
|
193
|
+
const validation = this.validateConfiguration(newConfig);
|
|
194
|
+
if (!validation.valid) {
|
|
195
|
+
const errorMessage = `Invalid configuration: ${validation.errors.join(", ")}`;
|
|
196
|
+
this.logger?.error(`Live Config: ${errorMessage}`);
|
|
197
|
+
|
|
198
|
+
// Emit error event but continue with previous valid config
|
|
199
|
+
this.emit(CONFIGURATION_EVENTS.CONFIGURATION_CHANGE, {
|
|
200
|
+
type: "settings_changed",
|
|
201
|
+
path:
|
|
202
|
+
this.getFirstExistingProjectPath() ||
|
|
203
|
+
this.getFirstExistingUserPath() ||
|
|
204
|
+
"unknown",
|
|
205
|
+
timestamp,
|
|
206
|
+
changes: { added: [], modified: [], removed: [] },
|
|
207
|
+
isValid: false,
|
|
208
|
+
errorMessage,
|
|
209
|
+
} as ConfigurationChangeEvent);
|
|
210
|
+
|
|
211
|
+
// Use previous valid configuration for error recovery
|
|
212
|
+
if (this.lastValidConfiguration) {
|
|
213
|
+
this.logger?.warn(
|
|
214
|
+
"Live Config: Using previous valid configuration due to validation errors",
|
|
215
|
+
);
|
|
216
|
+
this.currentConfiguration = this.lastValidConfiguration;
|
|
217
|
+
return this.currentConfiguration;
|
|
218
|
+
} else {
|
|
219
|
+
this.logger?.warn(
|
|
220
|
+
"Live Config: No previous valid configuration available, using empty config",
|
|
221
|
+
);
|
|
222
|
+
this.currentConfiguration = {};
|
|
223
|
+
return this.currentConfiguration;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Detect changes between old and new configuration
|
|
229
|
+
const changes = this.detectChanges(this.currentConfiguration, newConfig);
|
|
230
|
+
|
|
231
|
+
// Update current configuration
|
|
232
|
+
this.currentConfiguration = newConfig || {};
|
|
233
|
+
|
|
234
|
+
// Save as last valid configuration if it's valid and not empty
|
|
235
|
+
if (newConfig && (newConfig.hooks || newConfig.env)) {
|
|
236
|
+
this.lastValidConfiguration = { ...newConfig };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.logger?.info(
|
|
240
|
+
`Live Config: Configuration reloaded successfully with ${Object.keys(newConfig?.hooks || {}).length} event types and ${Object.keys(newConfig?.env || {}).length} environment variables`,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Emit configuration change event
|
|
244
|
+
this.emit(CONFIGURATION_EVENTS.CONFIGURATION_CHANGE, {
|
|
245
|
+
type: "settings_changed",
|
|
246
|
+
path:
|
|
247
|
+
this.getFirstExistingProjectPath() ||
|
|
248
|
+
this.getFirstExistingUserPath() ||
|
|
249
|
+
"merged",
|
|
250
|
+
timestamp,
|
|
251
|
+
changes,
|
|
252
|
+
isValid: true,
|
|
253
|
+
} as ConfigurationChangeEvent);
|
|
254
|
+
|
|
255
|
+
return this.currentConfiguration;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
const errorMessage = `Failed to reload configuration: ${(error as Error).message}`;
|
|
258
|
+
this.logger?.error(`Live Config: ${errorMessage}`);
|
|
259
|
+
|
|
260
|
+
// Use previous valid configuration for error recovery
|
|
261
|
+
if (this.lastValidConfiguration) {
|
|
262
|
+
this.logger?.warn(
|
|
263
|
+
"Live Config: Using previous valid configuration due to reload error",
|
|
264
|
+
);
|
|
265
|
+
this.currentConfiguration = this.lastValidConfiguration;
|
|
266
|
+
} else {
|
|
267
|
+
this.logger?.warn(
|
|
268
|
+
"Live Config: No previous valid configuration available, using empty config",
|
|
269
|
+
);
|
|
270
|
+
this.currentConfiguration = {};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Emit error event
|
|
274
|
+
this.emit(CONFIGURATION_EVENTS.CONFIGURATION_CHANGE, {
|
|
275
|
+
type: "settings_changed",
|
|
276
|
+
path: "reload-error",
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
changes: { added: [], modified: [], removed: [] },
|
|
279
|
+
isValid: false,
|
|
280
|
+
errorMessage,
|
|
281
|
+
} as ConfigurationChangeEvent);
|
|
282
|
+
|
|
283
|
+
return this.currentConfiguration;
|
|
284
|
+
} finally {
|
|
285
|
+
this.reloadInProgress = false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get current effective configuration
|
|
291
|
+
* Maps to FR-002: Merged configuration with project precedence
|
|
292
|
+
*/
|
|
293
|
+
getCurrentConfiguration(): WaveConfiguration | null {
|
|
294
|
+
return this.currentConfiguration ? { ...this.currentConfiguration } : null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Validate environment variables
|
|
299
|
+
* Maps to FR-003: Validate env field format
|
|
300
|
+
*/
|
|
301
|
+
validateEnvironmentVariables(env: Record<string, string>): {
|
|
302
|
+
isValid: boolean;
|
|
303
|
+
errors: string[];
|
|
304
|
+
warnings: string[];
|
|
305
|
+
} {
|
|
306
|
+
const errors: string[] = [];
|
|
307
|
+
const warnings: string[] = [];
|
|
308
|
+
|
|
309
|
+
if (typeof env !== "object" || env === null) {
|
|
310
|
+
errors.push("Environment variables must be an object");
|
|
311
|
+
return { isValid: false, errors, warnings };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (Array.isArray(env)) {
|
|
315
|
+
errors.push("Environment variables cannot be an array");
|
|
316
|
+
return { isValid: false, errors, warnings };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const [key, value] of Object.entries(env)) {
|
|
320
|
+
// Validate key format
|
|
321
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
322
|
+
errors.push(
|
|
323
|
+
`Invalid environment variable key: '${key}' (must be non-empty string)`,
|
|
324
|
+
);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Validate key naming convention (optional warning)
|
|
329
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) {
|
|
330
|
+
warnings.push(
|
|
331
|
+
`Environment variable '${key}' doesn't follow conventional naming (A-Z, 0-9, underscore)`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Validate value type
|
|
336
|
+
if (typeof value !== "string") {
|
|
337
|
+
errors.push(
|
|
338
|
+
`Environment variable '${key}' must have a string value (got ${typeof value})`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
isValid: errors.length === 0,
|
|
345
|
+
errors,
|
|
346
|
+
warnings,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Stop watching and cleanup resources
|
|
352
|
+
* Maps to cleanup requirements
|
|
353
|
+
*/
|
|
354
|
+
async shutdown(): Promise<void> {
|
|
355
|
+
this.logger?.info("Live Config: Shutting down configuration watcher...");
|
|
356
|
+
|
|
357
|
+
this.isWatching = false;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
await this.fileWatcher.cleanup();
|
|
361
|
+
this.removeAllListeners();
|
|
362
|
+
this.logger?.info(
|
|
363
|
+
"Live Config: Configuration watcher shutdown completed",
|
|
364
|
+
);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
this.logger?.error(
|
|
367
|
+
`Live Config: Error during shutdown: ${(error as Error).message}`,
|
|
368
|
+
);
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Watch an additional file (like AGENTS.md) for changes
|
|
375
|
+
* Maps to T033: Add AGENTS.md file watching to HookManager
|
|
376
|
+
*/
|
|
377
|
+
async watchAdditionalFile(
|
|
378
|
+
filePath: string,
|
|
379
|
+
callback: (event: FileWatchEvent) => void,
|
|
380
|
+
): Promise<void> {
|
|
381
|
+
try {
|
|
382
|
+
await this.fileWatcher.watchFile(filePath, callback);
|
|
383
|
+
this.logger?.info(
|
|
384
|
+
`Live Config: Started watching additional file: ${filePath}`,
|
|
385
|
+
);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
this.logger?.error(
|
|
388
|
+
`Live Config: Failed to watch additional file ${filePath}: ${(error as Error).message}`,
|
|
389
|
+
);
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Stop watching an additional file
|
|
396
|
+
*/
|
|
397
|
+
async unwatchAdditionalFile(filePath: string): Promise<void> {
|
|
398
|
+
try {
|
|
399
|
+
await this.fileWatcher.unwatchFile(filePath);
|
|
400
|
+
this.logger?.info(
|
|
401
|
+
`Live Config: Stopped watching additional file: ${filePath}`,
|
|
402
|
+
);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
this.logger?.warn(
|
|
405
|
+
`Live Config: Failed to stop watching file ${filePath}: ${(error as Error).message}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Check if watching is active
|
|
412
|
+
*/
|
|
413
|
+
isWatchingActive(): boolean {
|
|
414
|
+
return this.isWatching;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get watcher status for monitoring
|
|
419
|
+
*/
|
|
420
|
+
getWatcherStatus() {
|
|
421
|
+
const statuses = this.fileWatcher.getAllWatcherStatuses();
|
|
422
|
+
return {
|
|
423
|
+
isActive: this.isWatching,
|
|
424
|
+
configurationLoaded: this.currentConfiguration !== null,
|
|
425
|
+
hasValidConfiguration: this.lastValidConfiguration !== null,
|
|
426
|
+
reloadInProgress: this.reloadInProgress,
|
|
427
|
+
watchedFiles: statuses.map((s) => ({
|
|
428
|
+
path: s.path,
|
|
429
|
+
isActive: s.isActive,
|
|
430
|
+
method: s.method,
|
|
431
|
+
errorCount: s.errorCount,
|
|
432
|
+
})),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private setupFileWatcherEvents(): void {
|
|
437
|
+
this.fileWatcher.on("watcherError", (error: Error) => {
|
|
438
|
+
this.logger?.error(`Live Config: File watcher error: ${error.message}`);
|
|
439
|
+
this.emit(CONFIGURATION_EVENTS.WATCHER_ERROR, error);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private async handleFileChange(
|
|
444
|
+
event: FileWatchEvent,
|
|
445
|
+
source: "user" | "project",
|
|
446
|
+
): Promise<void> {
|
|
447
|
+
this.logger?.debug(
|
|
448
|
+
`Live Config: File ${event.type} detected for ${source} config: ${event.path}`,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
// Handle file deletion
|
|
453
|
+
if (event.type === FILE_WATCHER_EVENTS.DELETE) {
|
|
454
|
+
this.logger?.info(
|
|
455
|
+
`Live Config: ${source} config file deleted: ${event.path}`,
|
|
456
|
+
);
|
|
457
|
+
// Reload configuration without the deleted file
|
|
458
|
+
await this.reloadConfiguration();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Handle file creation or modification
|
|
463
|
+
if (
|
|
464
|
+
event.type === FILE_WATCHER_EVENTS.CHANGE ||
|
|
465
|
+
event.type === FILE_WATCHER_EVENTS.CREATE
|
|
466
|
+
) {
|
|
467
|
+
this.logger?.info(
|
|
468
|
+
`Live Config: ${source} config file ${event.type}: ${event.path}`,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Add small delay to ensure file write is complete
|
|
472
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
473
|
+
|
|
474
|
+
// Reload configuration
|
|
475
|
+
await this.reloadConfiguration();
|
|
476
|
+
}
|
|
477
|
+
} catch (error) {
|
|
478
|
+
this.logger?.error(
|
|
479
|
+
`Live Config: Error handling file change for ${source} config: ${(error as Error).message}`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Validate configuration structure and content
|
|
486
|
+
*/
|
|
487
|
+
private validateConfiguration(config: WaveConfiguration): ValidationResult {
|
|
488
|
+
const errors: string[] = [];
|
|
489
|
+
|
|
490
|
+
if (!config || typeof config !== "object") {
|
|
491
|
+
return { valid: false, errors: ["Configuration must be an object"] };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Validate hooks if present
|
|
495
|
+
if (config.hooks) {
|
|
496
|
+
if (typeof config.hooks !== "object") {
|
|
497
|
+
errors.push("hooks property must be an object");
|
|
498
|
+
} else {
|
|
499
|
+
// Validate each hook event
|
|
500
|
+
for (const [eventName, eventConfigs] of Object.entries(config.hooks)) {
|
|
501
|
+
// Validate event name
|
|
502
|
+
if (!isValidHookEvent(eventName)) {
|
|
503
|
+
errors.push(`Invalid hook event: ${eventName}`);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Validate event configurations
|
|
508
|
+
if (!Array.isArray(eventConfigs)) {
|
|
509
|
+
errors.push(
|
|
510
|
+
`Hook event ${eventName} must be an array of configurations`,
|
|
511
|
+
);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
eventConfigs.forEach((eventConfig, index) => {
|
|
516
|
+
if (!isValidHookEventConfig(eventConfig)) {
|
|
517
|
+
errors.push(
|
|
518
|
+
`Invalid hook event configuration at ${eventName}[${index}]`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Validate environment variables if present
|
|
527
|
+
if (config.env) {
|
|
528
|
+
if (typeof config.env !== "object" || Array.isArray(config.env)) {
|
|
529
|
+
errors.push("env property must be an object");
|
|
530
|
+
} else {
|
|
531
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
532
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
533
|
+
errors.push(`Invalid environment variable key: ${key}`);
|
|
534
|
+
}
|
|
535
|
+
if (typeof value !== "string") {
|
|
536
|
+
errors.push(`Environment variable ${key} must have a string value`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
valid: errors.length === 0,
|
|
544
|
+
errors,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private detectChanges(
|
|
549
|
+
oldConfig: WaveConfiguration | null,
|
|
550
|
+
newConfig: WaveConfiguration | null,
|
|
551
|
+
): {
|
|
552
|
+
added: string[];
|
|
553
|
+
modified: string[];
|
|
554
|
+
removed: string[];
|
|
555
|
+
} {
|
|
556
|
+
const added: string[] = [];
|
|
557
|
+
const modified: string[] = [];
|
|
558
|
+
const removed: string[] = [];
|
|
559
|
+
|
|
560
|
+
// Handle environment variables changes
|
|
561
|
+
const oldEnv = oldConfig?.env || {};
|
|
562
|
+
const newEnv = newConfig?.env || {};
|
|
563
|
+
|
|
564
|
+
for (const key of Object.keys(newEnv)) {
|
|
565
|
+
if (!(key in oldEnv)) {
|
|
566
|
+
added.push(`env.${key}`);
|
|
567
|
+
} else if (oldEnv[key] !== newEnv[key]) {
|
|
568
|
+
modified.push(`env.${key}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
for (const key of Object.keys(oldEnv)) {
|
|
573
|
+
if (!(key in newEnv)) {
|
|
574
|
+
removed.push(`env.${key}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Handle hooks changes (simplified)
|
|
579
|
+
const oldHooks = oldConfig?.hooks || {};
|
|
580
|
+
const newHooks = newConfig?.hooks || {};
|
|
581
|
+
|
|
582
|
+
for (const event of Object.keys(newHooks)) {
|
|
583
|
+
if (isValidHookEvent(event)) {
|
|
584
|
+
if (!(event in oldHooks)) {
|
|
585
|
+
added.push(`hooks.${event}`);
|
|
586
|
+
} else if (
|
|
587
|
+
JSON.stringify(oldHooks[event]) !== JSON.stringify(newHooks[event])
|
|
588
|
+
) {
|
|
589
|
+
modified.push(`hooks.${event}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
for (const event of Object.keys(oldHooks)) {
|
|
595
|
+
if (isValidHookEvent(event) && !(event in newHooks)) {
|
|
596
|
+
removed.push(`hooks.${event}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return { added, modified, removed };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Get the first existing user config path for error reporting
|
|
605
|
+
*/
|
|
606
|
+
private getFirstExistingUserPath(): string | undefined {
|
|
607
|
+
return (
|
|
608
|
+
this.userConfigPaths?.find((path) => existsSync(path)) ||
|
|
609
|
+
this.userConfigPaths?.[0]
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Get the first existing project config path for error reporting
|
|
615
|
+
*/
|
|
616
|
+
private getFirstExistingProjectPath(): string | undefined {
|
|
617
|
+
return (
|
|
618
|
+
this.projectConfigPaths?.find((path) => existsSync(path)) ||
|
|
619
|
+
this.projectConfigPaths?.[0]
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|