@ryanfw/prompt-orchestration-pipeline 0.0.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.
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Centralized configuration management for Prompt Orchestration Pipeline
3
+ *
4
+ * This module provides a single source of truth for all configuration values,
5
+ * supporting both environment variables and config file overrides.
6
+ */
7
+
8
+ import { promises as fs } from "node:fs";
9
+ import path from "node:path";
10
+
11
+ /**
12
+ * Default configuration values
13
+ * These can be overridden by environment variables or config file
14
+ */
15
+ export const defaultConfig = {
16
+ orchestrator: {
17
+ shutdownTimeout: 2000,
18
+ processSpawnRetries: 3,
19
+ processSpawnRetryDelay: 1000,
20
+ lockFileTimeout: 5000,
21
+ watchDebounce: 100,
22
+ watchStabilityThreshold: 200,
23
+ watchPollInterval: 50,
24
+ },
25
+ taskRunner: {
26
+ maxRefinementAttempts: 2,
27
+ stageTimeout: 300000,
28
+ llmRequestTimeout: 60000,
29
+ },
30
+ llm: {
31
+ defaultProvider: "openai",
32
+ defaultModel: "gpt-5-chat-latest",
33
+ maxConcurrency: 5,
34
+ retryMaxAttempts: 3,
35
+ retryBackoffMs: 1000,
36
+ },
37
+ ui: {
38
+ port: 3000,
39
+ host: "localhost",
40
+ heartbeatInterval: 30000,
41
+ maxRecentChanges: 10,
42
+ },
43
+ paths: {
44
+ root: process.env.PO_ROOT || process.cwd(),
45
+ dataDir: "pipeline-data",
46
+ configDir: "pipeline-config",
47
+ pendingDir: "pending",
48
+ currentDir: "current",
49
+ completeDir: "complete",
50
+ },
51
+ validation: {
52
+ seedNameMinLength: 1,
53
+ seedNameMaxLength: 100,
54
+ seedNamePattern: "^[a-zA-Z0-9-_]+$",
55
+ },
56
+ logging: {
57
+ level: "info",
58
+ format: "json",
59
+ destination: "stdout",
60
+ },
61
+ };
62
+
63
+ /**
64
+ * Current loaded configuration
65
+ * Initialized with defaults, then overridden by environment and config file
66
+ */
67
+ let currentConfig = null;
68
+
69
+ /**
70
+ * Load configuration from environment variables
71
+ * Environment variables take precedence over defaults
72
+ */
73
+ function loadFromEnvironment(config) {
74
+ const envConfig = { ...config };
75
+
76
+ // Orchestrator settings
77
+ if (process.env.PO_SHUTDOWN_TIMEOUT) {
78
+ envConfig.orchestrator.shutdownTimeout = parseInt(
79
+ process.env.PO_SHUTDOWN_TIMEOUT,
80
+ 10
81
+ );
82
+ }
83
+ if (process.env.PO_PROCESS_SPAWN_RETRIES) {
84
+ envConfig.orchestrator.processSpawnRetries = parseInt(
85
+ process.env.PO_PROCESS_SPAWN_RETRIES,
86
+ 10
87
+ );
88
+ }
89
+ if (process.env.PO_LOCK_FILE_TIMEOUT) {
90
+ envConfig.orchestrator.lockFileTimeout = parseInt(
91
+ process.env.PO_LOCK_FILE_TIMEOUT,
92
+ 10
93
+ );
94
+ }
95
+ if (process.env.PO_WATCH_DEBOUNCE) {
96
+ envConfig.orchestrator.watchDebounce = parseInt(
97
+ process.env.PO_WATCH_DEBOUNCE,
98
+ 10
99
+ );
100
+ }
101
+
102
+ // Task runner settings
103
+ if (process.env.PO_MAX_REFINEMENT_ATTEMPTS) {
104
+ envConfig.taskRunner.maxRefinementAttempts = parseInt(
105
+ process.env.PO_MAX_REFINEMENT_ATTEMPTS,
106
+ 10
107
+ );
108
+ }
109
+ if (process.env.PO_STAGE_TIMEOUT) {
110
+ envConfig.taskRunner.stageTimeout = parseInt(
111
+ process.env.PO_STAGE_TIMEOUT,
112
+ 10
113
+ );
114
+ }
115
+ if (process.env.PO_LLM_REQUEST_TIMEOUT) {
116
+ envConfig.taskRunner.llmRequestTimeout = parseInt(
117
+ process.env.PO_LLM_REQUEST_TIMEOUT,
118
+ 10
119
+ );
120
+ }
121
+
122
+ // LLM settings
123
+ if (process.env.PO_DEFAULT_PROVIDER) {
124
+ envConfig.llm.defaultProvider = process.env.PO_DEFAULT_PROVIDER;
125
+ }
126
+ if (process.env.PO_DEFAULT_MODEL) {
127
+ envConfig.llm.defaultModel = process.env.PO_DEFAULT_MODEL;
128
+ }
129
+ if (process.env.PO_MAX_CONCURRENCY) {
130
+ envConfig.llm.maxConcurrency = parseInt(process.env.PO_MAX_CONCURRENCY, 10);
131
+ }
132
+
133
+ // UI settings
134
+ if (process.env.PO_UI_PORT || process.env.PORT) {
135
+ envConfig.ui.port = parseInt(
136
+ process.env.PO_UI_PORT || process.env.PORT,
137
+ 10
138
+ );
139
+ }
140
+ if (process.env.PO_UI_HOST) {
141
+ envConfig.ui.host = process.env.PO_UI_HOST;
142
+ }
143
+ if (process.env.PO_HEARTBEAT_INTERVAL) {
144
+ envConfig.ui.heartbeatInterval = parseInt(
145
+ process.env.PO_HEARTBEAT_INTERVAL,
146
+ 10
147
+ );
148
+ }
149
+
150
+ // Path settings
151
+ if (process.env.PO_ROOT) {
152
+ envConfig.paths.root = process.env.PO_ROOT;
153
+ }
154
+ if (process.env.PO_DATA_DIR) {
155
+ envConfig.paths.dataDir = process.env.PO_DATA_DIR;
156
+ }
157
+ if (process.env.PO_CONFIG_DIR) {
158
+ envConfig.paths.configDir = process.env.PO_CONFIG_DIR;
159
+ }
160
+
161
+ // Logging settings
162
+ if (process.env.PO_LOG_LEVEL) {
163
+ envConfig.logging.level = process.env.PO_LOG_LEVEL;
164
+ }
165
+ if (process.env.PO_LOG_FORMAT) {
166
+ envConfig.logging.format = process.env.PO_LOG_FORMAT;
167
+ }
168
+ if (process.env.PO_LOG_DESTINATION) {
169
+ envConfig.logging.destination = process.env.PO_LOG_DESTINATION;
170
+ }
171
+
172
+ return envConfig;
173
+ }
174
+
175
+ /**
176
+ * Deep merge two configuration objects
177
+ */
178
+ function deepMerge(target, source) {
179
+ const result = { ...target };
180
+
181
+ for (const key in source) {
182
+ if (
183
+ source[key] &&
184
+ typeof source[key] === "object" &&
185
+ !Array.isArray(source[key])
186
+ ) {
187
+ result[key] = deepMerge(target[key] || {}, source[key]);
188
+ } else {
189
+ result[key] = source[key];
190
+ }
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ /**
197
+ * Load configuration from a JSON file
198
+ * Returns null if file doesn't exist
199
+ */
200
+ async function loadFromFile(configPath) {
201
+ try {
202
+ const content = await fs.readFile(configPath, "utf8");
203
+ return JSON.parse(content);
204
+ } catch (error) {
205
+ if (error.code === "ENOENT") {
206
+ return null;
207
+ }
208
+ throw new Error(
209
+ `Failed to load config file ${configPath}: ${error.message}`
210
+ );
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Validate configuration values
216
+ * Throws if configuration is invalid
217
+ */
218
+ function validateConfig(config) {
219
+ const errors = [];
220
+
221
+ // Validate numeric values are positive
222
+ if (config.orchestrator.shutdownTimeout <= 0) {
223
+ errors.push("orchestrator.shutdownTimeout must be positive");
224
+ }
225
+ if (config.orchestrator.processSpawnRetries < 0) {
226
+ errors.push("orchestrator.processSpawnRetries must be non-negative");
227
+ }
228
+ if (config.taskRunner.maxRefinementAttempts < 0) {
229
+ errors.push("taskRunner.maxRefinementAttempts must be non-negative");
230
+ }
231
+ if (config.llm.maxConcurrency <= 0) {
232
+ errors.push("llm.maxConcurrency must be positive");
233
+ }
234
+ if (config.ui.port < 1 || config.ui.port > 65535) {
235
+ errors.push("ui.port must be between 1 and 65535");
236
+ }
237
+
238
+ // Validate provider
239
+ const validProviders = ["openai", "deepseek", "anthropic"];
240
+ if (!validProviders.includes(config.llm.defaultProvider)) {
241
+ errors.push(
242
+ `llm.defaultProvider must be one of: ${validProviders.join(", ")}`
243
+ );
244
+ }
245
+
246
+ // Validate log level
247
+ const validLogLevels = ["debug", "info", "warn", "error"];
248
+ if (!validLogLevels.includes(config.logging.level)) {
249
+ errors.push(`logging.level must be one of: ${validLogLevels.join(", ")}`);
250
+ }
251
+
252
+ if (errors.length > 0) {
253
+ throw new Error(
254
+ `Configuration validation failed:\n${errors.map((e) => ` - ${e}`).join("\n")}`
255
+ );
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Initialize and load configuration
261
+ *
262
+ * Priority order (highest to lowest):
263
+ * 1. Environment variables
264
+ * 2. Config file (if provided)
265
+ * 3. Default values
266
+ *
267
+ * @param {Object} options - Configuration options
268
+ * @param {string} options.configPath - Path to config file (optional)
269
+ * @param {boolean} options.validate - Whether to validate config (default: true)
270
+ * @returns {Object} Loaded configuration
271
+ */
272
+ export async function loadConfig(options = {}) {
273
+ const { configPath, validate = true } = options;
274
+
275
+ // Start with defaults
276
+ let config = JSON.parse(JSON.stringify(defaultConfig));
277
+
278
+ // Load from config file if provided
279
+ if (configPath) {
280
+ const fileConfig = await loadFromFile(configPath);
281
+ if (fileConfig) {
282
+ config = deepMerge(config, fileConfig);
283
+ }
284
+ }
285
+
286
+ // Override with environment variables
287
+ config = loadFromEnvironment(config);
288
+
289
+ // Validate if requested
290
+ if (validate) {
291
+ validateConfig(config);
292
+ }
293
+
294
+ // Cache the loaded config
295
+ currentConfig = config;
296
+
297
+ return config;
298
+ }
299
+
300
+ /**
301
+ * Get the current configuration
302
+ * Loads default config if not already loaded
303
+ *
304
+ * @returns {Object} Current configuration
305
+ */
306
+ export function getConfig() {
307
+ if (!currentConfig) {
308
+ // Load defaults synchronously for first access
309
+ currentConfig = loadFromEnvironment(
310
+ JSON.parse(JSON.stringify(defaultConfig))
311
+ );
312
+ }
313
+ return currentConfig;
314
+ }
315
+
316
+ /**
317
+ * Reset configuration to defaults
318
+ * Useful for testing
319
+ */
320
+ export function resetConfig() {
321
+ currentConfig = null;
322
+ }
323
+
324
+ /**
325
+ * Get a specific configuration value by path
326
+ *
327
+ * @param {string} path - Dot-separated path (e.g., "orchestrator.shutdownTimeout")
328
+ * @param {*} defaultValue - Default value if path not found
329
+ * @returns {*} Configuration value
330
+ */
331
+ export function getConfigValue(path, defaultValue = undefined) {
332
+ const config = getConfig();
333
+ const parts = path.split(".");
334
+ let value = config;
335
+
336
+ for (const part of parts) {
337
+ if (value && typeof value === "object" && part in value) {
338
+ value = value[part];
339
+ } else {
340
+ return defaultValue;
341
+ }
342
+ }
343
+
344
+ return value;
345
+ }
@@ -0,0 +1,56 @@
1
+ import { config } from "dotenv";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+
5
+ export async function loadEnvironment(options = {}) {
6
+ const rootDir = options.rootDir || process.cwd();
7
+ const envFiles = options.envFiles || [".env", ".env.local"];
8
+ const loaded = [];
9
+
10
+ for (const envFile of envFiles) {
11
+ const envPath = path.join(rootDir, envFile);
12
+ if (fs.existsSync(envPath)) {
13
+ config({ path: envPath, override: true });
14
+ loaded.push(envFile);
15
+ }
16
+ }
17
+
18
+ const warnings = validateEnvironment();
19
+ return { loaded, warnings, config: getEnvironmentConfig() };
20
+ }
21
+
22
+ export function validateEnvironment() {
23
+ const warnings = [];
24
+ const commonKeys = [
25
+ "OPENAI_API_KEY",
26
+ "ANTHROPIC_API_KEY",
27
+ "DEEPSEEK_API_KEY",
28
+ "GEMINI_API_KEY",
29
+ ];
30
+ const foundKeys = commonKeys.filter((key) => process.env[key]);
31
+ if (foundKeys.length === 0) {
32
+ warnings.push("No LLM API keys found in environment.");
33
+ }
34
+ return warnings;
35
+ }
36
+
37
+ export function getEnvironmentConfig() {
38
+ return {
39
+ openai: {
40
+ apiKey: process.env.OPENAI_API_KEY,
41
+ organization: process.env.OPENAI_ORGANIZATION,
42
+ baseURL: process.env.OPENAI_BASE_URL,
43
+ },
44
+ anthropic: {
45
+ apiKey: process.env.ANTHROPIC_API_KEY,
46
+ baseURL: process.env.ANTHROPIC_BASE_URL,
47
+ },
48
+ deepseek: {
49
+ apiKey: process.env.DEEPSEEK_API_KEY,
50
+ },
51
+ gemini: {
52
+ apiKey: process.env.GEMINI_API_KEY,
53
+ baseURL: process.env.GEMINI_BASE_URL,
54
+ },
55
+ };
56
+ }