lsh-framework 1.3.2 → 1.4.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/.env.example +30 -1
- package/README.md +25 -4
- package/dist/cli.js +6 -0
- package/dist/commands/config.js +240 -0
- package/dist/daemon/saas-api-routes.js +778 -0
- package/dist/daemon/saas-api-server.js +225 -0
- package/dist/lib/config-manager.js +317 -0
- package/dist/lib/database-persistence.js +75 -3
- package/dist/lib/env-validator.js +17 -0
- package/dist/lib/local-storage-adapter.js +493 -0
- package/dist/lib/saas-audit.js +213 -0
- package/dist/lib/saas-auth.js +427 -0
- package/dist/lib/saas-billing.js +402 -0
- package/dist/lib/saas-email.js +402 -0
- package/dist/lib/saas-encryption.js +220 -0
- package/dist/lib/saas-organizations.js +592 -0
- package/dist/lib/saas-secrets.js +378 -0
- package/dist/lib/saas-types.js +108 -0
- package/dist/lib/supabase-client.js +77 -11
- package/package.json +13 -2
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSH SaaS API Server
|
|
3
|
+
* Express-based RESTful API for the SaaS platform
|
|
4
|
+
*/
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import cors from 'cors';
|
|
7
|
+
import rateLimit from 'express-rate-limit';
|
|
8
|
+
import { setupSaaSApiRoutes } from './saas-api-routes.js';
|
|
9
|
+
/**
|
|
10
|
+
* SaaS API Server
|
|
11
|
+
*/
|
|
12
|
+
export class SaaSApiServer {
|
|
13
|
+
app;
|
|
14
|
+
config;
|
|
15
|
+
server;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.config = {
|
|
18
|
+
port: config?.port || parseInt(process.env.LSH_SAAS_API_PORT || '3031'),
|
|
19
|
+
host: config?.host || process.env.LSH_SAAS_API_HOST || '0.0.0.0',
|
|
20
|
+
corsOrigins: config?.corsOrigins || (process.env.LSH_CORS_ORIGINS?.split(',') || ['*']),
|
|
21
|
+
rateLimitWindowMs: config?.rateLimitWindowMs || 15 * 60 * 1000, // 15 minutes
|
|
22
|
+
rateLimitMax: config?.rateLimitMax || 100, // Max 100 requests per windowMs
|
|
23
|
+
};
|
|
24
|
+
this.app = express();
|
|
25
|
+
this.setupMiddleware();
|
|
26
|
+
this.setupRoutes();
|
|
27
|
+
this.setupErrorHandlers();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Setup middleware
|
|
31
|
+
*/
|
|
32
|
+
setupMiddleware() {
|
|
33
|
+
// CORS
|
|
34
|
+
this.app.use(cors({
|
|
35
|
+
origin: this.config.corsOrigins,
|
|
36
|
+
credentials: true,
|
|
37
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
38
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
|
39
|
+
}));
|
|
40
|
+
// Rate limiting - global limit for all requests
|
|
41
|
+
const limiter = rateLimit({
|
|
42
|
+
windowMs: this.config.rateLimitWindowMs,
|
|
43
|
+
max: this.config.rateLimitMax,
|
|
44
|
+
message: {
|
|
45
|
+
success: false,
|
|
46
|
+
error: {
|
|
47
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
48
|
+
message: 'Too many requests, please try again later',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
|
|
52
|
+
legacyHeaders: false, // Disable `X-RateLimit-*` headers
|
|
53
|
+
});
|
|
54
|
+
this.app.use(limiter);
|
|
55
|
+
// Body parsers
|
|
56
|
+
this.app.use(express.json({ limit: '10mb' }));
|
|
57
|
+
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
58
|
+
// Security headers
|
|
59
|
+
this.app.use((req, res, next) => {
|
|
60
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
61
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
62
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
63
|
+
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
64
|
+
// CSP
|
|
65
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'");
|
|
66
|
+
next();
|
|
67
|
+
});
|
|
68
|
+
// Request logging
|
|
69
|
+
this.app.use((req, res, next) => {
|
|
70
|
+
const start = Date.now();
|
|
71
|
+
res.on('finish', () => {
|
|
72
|
+
const duration = Date.now() - start;
|
|
73
|
+
// Sanitize path to prevent log injection attacks
|
|
74
|
+
const sanitizedPath = req.path.replace(/[\r\n]/g, '');
|
|
75
|
+
const sanitizedIp = (req.ip || 'unknown').replace(/[\r\n]/g, '');
|
|
76
|
+
console.log(`${req.method} ${sanitizedPath} ${res.statusCode} - ${duration}ms - ${sanitizedIp}`);
|
|
77
|
+
});
|
|
78
|
+
next();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Setup routes
|
|
83
|
+
*/
|
|
84
|
+
setupRoutes() {
|
|
85
|
+
// Health check
|
|
86
|
+
this.app.get('/health', (req, res) => {
|
|
87
|
+
res.json({
|
|
88
|
+
status: 'ok',
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
uptime: process.uptime(),
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
// API version
|
|
94
|
+
this.app.get('/api/v1', (req, res) => {
|
|
95
|
+
res.json({
|
|
96
|
+
name: 'LSH SaaS API',
|
|
97
|
+
version: '1.0.0',
|
|
98
|
+
endpoints: {
|
|
99
|
+
auth: '/api/v1/auth/*',
|
|
100
|
+
organizations: '/api/v1/organizations/*',
|
|
101
|
+
teams: '/api/v1/organizations/:id/teams/*',
|
|
102
|
+
secrets: '/api/v1/teams/:id/secrets/*',
|
|
103
|
+
billing: '/api/v1/organizations/:id/billing/*',
|
|
104
|
+
audit: '/api/v1/organizations/:id/audit-logs',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// Setup SaaS routes
|
|
109
|
+
setupSaaSApiRoutes(this.app);
|
|
110
|
+
// 404 handler
|
|
111
|
+
this.app.use((req, res) => {
|
|
112
|
+
res.status(404).json({
|
|
113
|
+
success: false,
|
|
114
|
+
error: {
|
|
115
|
+
code: 'NOT_FOUND',
|
|
116
|
+
message: `Endpoint ${req.method} ${req.path} not found`,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Setup error handlers
|
|
123
|
+
*/
|
|
124
|
+
setupErrorHandlers() {
|
|
125
|
+
// Global error handler
|
|
126
|
+
this.app.use((err, req, res, _next) => {
|
|
127
|
+
console.error('API Error:', err);
|
|
128
|
+
// Don't leak error details in production
|
|
129
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
130
|
+
res.status(err.status || 500).json({
|
|
131
|
+
success: false,
|
|
132
|
+
error: {
|
|
133
|
+
code: err.code || 'INTERNAL_ERROR',
|
|
134
|
+
message: isDev ? err.message : 'Internal server error',
|
|
135
|
+
details: isDev ? err.stack : undefined,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Start the server
|
|
142
|
+
*/
|
|
143
|
+
async start() {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
try {
|
|
146
|
+
this.server = this.app.listen(this.config.port, this.config.host, () => {
|
|
147
|
+
console.log(`
|
|
148
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
149
|
+
║ ║
|
|
150
|
+
║ 🔐 LSH SaaS API Server ║
|
|
151
|
+
║ ║
|
|
152
|
+
║ Status: Running ║
|
|
153
|
+
║ Port: ${this.config.port.toString().padEnd(49)} ║
|
|
154
|
+
║ Host: ${this.config.host?.padEnd(49)} ║
|
|
155
|
+
║ ║
|
|
156
|
+
║ Endpoints: ║
|
|
157
|
+
║ - Health: http://${this.config.host}:${this.config.port}/health${' '.repeat(26)} ║
|
|
158
|
+
║ - API: http://${this.config.host}:${this.config.port}/api/v1${' '.repeat(27)} ║
|
|
159
|
+
║ ║
|
|
160
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
161
|
+
`);
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
this.server.on('error', (error) => {
|
|
165
|
+
if (error.code === 'EADDRINUSE') {
|
|
166
|
+
reject(new Error(`Port ${this.config.port} is already in use`));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
reject(error);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
reject(error);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Stop the server
|
|
180
|
+
*/
|
|
181
|
+
async stop() {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
if (!this.server) {
|
|
184
|
+
resolve();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.server.close((err) => {
|
|
188
|
+
if (err) {
|
|
189
|
+
reject(err);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log('LSH SaaS API Server stopped');
|
|
193
|
+
resolve();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get Express app (for testing)
|
|
200
|
+
*/
|
|
201
|
+
getApp() {
|
|
202
|
+
return this.app;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Start the server if run directly
|
|
207
|
+
*/
|
|
208
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
209
|
+
const server = new SaaSApiServer();
|
|
210
|
+
server.start().catch((error) => {
|
|
211
|
+
console.error('Failed to start SaaS API server:', error);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
});
|
|
214
|
+
// Graceful shutdown
|
|
215
|
+
process.on('SIGINT', async () => {
|
|
216
|
+
console.log('\nShutting down gracefully...');
|
|
217
|
+
await server.stop();
|
|
218
|
+
process.exit(0);
|
|
219
|
+
});
|
|
220
|
+
process.on('SIGTERM', async () => {
|
|
221
|
+
console.log('\nShutting down gracefully...');
|
|
222
|
+
await server.stop();
|
|
223
|
+
process.exit(0);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Manager for LSH
|
|
3
|
+
* Manages ~/.config/lsh/lshrc configuration file
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as fsSync from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
export const DEFAULT_CONFIG_DIR = path.join(os.homedir(), '.config', 'lsh');
|
|
10
|
+
export const DEFAULT_CONFIG_FILE = path.join(DEFAULT_CONFIG_DIR, 'lshrc');
|
|
11
|
+
/**
|
|
12
|
+
* Default configuration template
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_CONFIG_TEMPLATE = `# LSH Configuration File
|
|
15
|
+
# This file is in .env format and is automatically loaded on LSH startup
|
|
16
|
+
# Location: ~/.config/lsh/lshrc
|
|
17
|
+
#
|
|
18
|
+
# Edit this file with: lsh config
|
|
19
|
+
|
|
20
|
+
# ============================================================================
|
|
21
|
+
# Storage Backend (choose one)
|
|
22
|
+
# ============================================================================
|
|
23
|
+
|
|
24
|
+
# Option 1: Local Storage (Default - No Configuration Needed)
|
|
25
|
+
# Data stored in ~/.lsh/data/storage.json
|
|
26
|
+
# Perfect for: Single-user, local development, getting started
|
|
27
|
+
|
|
28
|
+
# Option 2: Supabase Cloud (Team Collaboration)
|
|
29
|
+
# SUPABASE_URL=https://your-project.supabase.co
|
|
30
|
+
# SUPABASE_ANON_KEY=your-anon-key-here
|
|
31
|
+
|
|
32
|
+
# Option 3: Local PostgreSQL (via Docker)
|
|
33
|
+
# DATABASE_URL=postgresql://lsh_user:lsh_password@localhost:5432/lsh
|
|
34
|
+
|
|
35
|
+
# ============================================================================
|
|
36
|
+
# Secrets Management
|
|
37
|
+
# ============================================================================
|
|
38
|
+
|
|
39
|
+
# Encryption key for secrets (generate with: lsh key)
|
|
40
|
+
# Share this key with your team for collaboration
|
|
41
|
+
# LSH_SECRETS_KEY=
|
|
42
|
+
|
|
43
|
+
# Master encryption key for SaaS platform
|
|
44
|
+
# LSH_MASTER_KEY=
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# API Server (Optional)
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
# LSH_API_ENABLED=false
|
|
51
|
+
# LSH_API_PORT=3030
|
|
52
|
+
# LSH_API_KEY=
|
|
53
|
+
# LSH_JWT_SECRET=
|
|
54
|
+
|
|
55
|
+
# ============================================================================
|
|
56
|
+
# Webhooks (Optional)
|
|
57
|
+
# ============================================================================
|
|
58
|
+
|
|
59
|
+
# LSH_ENABLE_WEBHOOKS=false
|
|
60
|
+
# WEBHOOK_PORT=3033
|
|
61
|
+
# GITHUB_WEBHOOK_SECRET=
|
|
62
|
+
# GITLAB_WEBHOOK_SECRET=
|
|
63
|
+
# JENKINS_WEBHOOK_SECRET=
|
|
64
|
+
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# Security
|
|
67
|
+
# ============================================================================
|
|
68
|
+
|
|
69
|
+
# WARNING: Only enable if you fully trust all job sources
|
|
70
|
+
# LSH_ALLOW_DANGEROUS_COMMANDS=false
|
|
71
|
+
|
|
72
|
+
# ============================================================================
|
|
73
|
+
# Advanced Configuration
|
|
74
|
+
# ============================================================================
|
|
75
|
+
|
|
76
|
+
# Custom data directory (default: ~/.lsh/data)
|
|
77
|
+
# LSH_DATA_DIR=
|
|
78
|
+
|
|
79
|
+
# Node environment
|
|
80
|
+
# NODE_ENV=development
|
|
81
|
+
|
|
82
|
+
# Redis (for caching)
|
|
83
|
+
# REDIS_URL=redis://localhost:6379
|
|
84
|
+
|
|
85
|
+
# ============================================================================
|
|
86
|
+
# SaaS Platform (Advanced)
|
|
87
|
+
# ============================================================================
|
|
88
|
+
|
|
89
|
+
# LSH_SAAS_API_PORT=3031
|
|
90
|
+
# LSH_SAAS_API_HOST=0.0.0.0
|
|
91
|
+
# LSH_CORS_ORIGINS=http://localhost:3000,http://localhost:3031
|
|
92
|
+
|
|
93
|
+
# Email Service (Resend)
|
|
94
|
+
# RESEND_API_KEY=
|
|
95
|
+
# EMAIL_FROM=noreply@yourdomain.com
|
|
96
|
+
# BASE_URL=https://app.yourdomain.com
|
|
97
|
+
|
|
98
|
+
# Stripe Billing
|
|
99
|
+
# STRIPE_SECRET_KEY=
|
|
100
|
+
# STRIPE_WEBHOOK_SECRET=
|
|
101
|
+
`;
|
|
102
|
+
/**
|
|
103
|
+
* Parse .env format file into config object
|
|
104
|
+
*/
|
|
105
|
+
export function parseEnvFile(content) {
|
|
106
|
+
const config = {};
|
|
107
|
+
const lines = content.split('\n');
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
const trimmed = line.trim();
|
|
110
|
+
// Skip comments and empty lines
|
|
111
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// Parse KEY=VALUE
|
|
115
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
116
|
+
if (match) {
|
|
117
|
+
const key = match[1].trim();
|
|
118
|
+
let value = match[2].trim();
|
|
119
|
+
// Remove quotes if present
|
|
120
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
121
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
122
|
+
value = value.slice(1, -1);
|
|
123
|
+
}
|
|
124
|
+
config[key] = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return config;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Serialize config object to .env format
|
|
131
|
+
*/
|
|
132
|
+
export function serializeConfig(config) {
|
|
133
|
+
const lines = [];
|
|
134
|
+
for (const [key, value] of Object.entries(config)) {
|
|
135
|
+
if (value !== undefined && value !== '') {
|
|
136
|
+
// Quote values that contain spaces or special characters
|
|
137
|
+
const needsQuotes = /[\s#"']/.test(value);
|
|
138
|
+
const serializedValue = needsQuotes ? `"${value}"` : value;
|
|
139
|
+
lines.push(`${key}=${serializedValue}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
export class ConfigManager {
|
|
145
|
+
configFile;
|
|
146
|
+
config = {};
|
|
147
|
+
constructor(configFile = DEFAULT_CONFIG_FILE) {
|
|
148
|
+
this.configFile = configFile;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get config file path
|
|
152
|
+
*/
|
|
153
|
+
getConfigPath() {
|
|
154
|
+
return this.configFile;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if config file exists
|
|
158
|
+
*/
|
|
159
|
+
async exists() {
|
|
160
|
+
try {
|
|
161
|
+
await fs.access(this.configFile);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Check if config file exists (sync)
|
|
170
|
+
*/
|
|
171
|
+
existsSync() {
|
|
172
|
+
return fsSync.existsSync(this.configFile);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Initialize config file with default template
|
|
176
|
+
*/
|
|
177
|
+
async initialize() {
|
|
178
|
+
const configDir = path.dirname(this.configFile);
|
|
179
|
+
// Create config directory if it doesn't exist
|
|
180
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
181
|
+
// Check if config file already exists
|
|
182
|
+
const exists = await this.exists();
|
|
183
|
+
if (exists) {
|
|
184
|
+
console.log(`Config file already exists at ${this.configFile}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// Write default template
|
|
188
|
+
await fs.writeFile(this.configFile, DEFAULT_CONFIG_TEMPLATE, 'utf-8');
|
|
189
|
+
console.log(`✓ Created config file at ${this.configFile}`);
|
|
190
|
+
console.log(` Edit with: lsh config`);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Load configuration from file
|
|
194
|
+
*/
|
|
195
|
+
async load() {
|
|
196
|
+
try {
|
|
197
|
+
// Initialize if doesn't exist
|
|
198
|
+
if (!(await this.exists())) {
|
|
199
|
+
await this.initialize();
|
|
200
|
+
}
|
|
201
|
+
const content = await fs.readFile(this.configFile, 'utf-8');
|
|
202
|
+
this.config = parseEnvFile(content);
|
|
203
|
+
return this.config;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.error(`Failed to load config from ${this.configFile}:`, error);
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Load configuration from file (sync)
|
|
212
|
+
*/
|
|
213
|
+
loadSync() {
|
|
214
|
+
try {
|
|
215
|
+
// Check if exists
|
|
216
|
+
if (!this.existsSync()) {
|
|
217
|
+
// Can't initialize synchronously safely, return empty config
|
|
218
|
+
return {};
|
|
219
|
+
}
|
|
220
|
+
const content = fsSync.readFileSync(this.configFile, 'utf-8');
|
|
221
|
+
this.config = parseEnvFile(content);
|
|
222
|
+
return this.config;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.error(`Failed to load config from ${this.configFile}:`, error);
|
|
226
|
+
return {};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Save configuration to file
|
|
231
|
+
*/
|
|
232
|
+
async save(config) {
|
|
233
|
+
try {
|
|
234
|
+
const configDir = path.dirname(this.configFile);
|
|
235
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
236
|
+
// Merge with existing config
|
|
237
|
+
this.config = { ...this.config, ...config };
|
|
238
|
+
const content = serializeConfig(this.config);
|
|
239
|
+
await fs.writeFile(this.configFile, content, 'utf-8');
|
|
240
|
+
console.log(`✓ Saved config to ${this.configFile}`);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.error(`Failed to save config to ${this.configFile}:`, error);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get a specific config value
|
|
249
|
+
*/
|
|
250
|
+
get(key) {
|
|
251
|
+
return this.config[key];
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Set a specific config value
|
|
255
|
+
*/
|
|
256
|
+
async set(key, value) {
|
|
257
|
+
this.config[key] = value;
|
|
258
|
+
await this.save(this.config);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Delete a specific config value
|
|
262
|
+
*/
|
|
263
|
+
async delete(key) {
|
|
264
|
+
delete this.config[key];
|
|
265
|
+
await this.save(this.config);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get all config values
|
|
269
|
+
*/
|
|
270
|
+
getAll() {
|
|
271
|
+
return { ...this.config };
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Merge config with process.env
|
|
275
|
+
* Config file values take precedence over environment variables
|
|
276
|
+
*/
|
|
277
|
+
mergeWithEnv() {
|
|
278
|
+
for (const [key, value] of Object.entries(this.config)) {
|
|
279
|
+
if (value !== undefined && value !== '') {
|
|
280
|
+
process.env[key] = value;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Global config manager instance
|
|
287
|
+
*/
|
|
288
|
+
let _globalConfigManager = null;
|
|
289
|
+
/**
|
|
290
|
+
* Get global config manager
|
|
291
|
+
*/
|
|
292
|
+
export function getConfigManager() {
|
|
293
|
+
if (!_globalConfigManager) {
|
|
294
|
+
_globalConfigManager = new ConfigManager();
|
|
295
|
+
}
|
|
296
|
+
return _globalConfigManager;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Load global configuration and merge with process.env
|
|
300
|
+
* This should be called early in the application startup
|
|
301
|
+
*/
|
|
302
|
+
export async function loadGlobalConfig() {
|
|
303
|
+
const manager = getConfigManager();
|
|
304
|
+
const config = await manager.load();
|
|
305
|
+
manager.mergeWithEnv();
|
|
306
|
+
return config;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Load global configuration synchronously and merge with process.env
|
|
310
|
+
*/
|
|
311
|
+
export function loadGlobalConfigSync() {
|
|
312
|
+
const manager = getConfigManager();
|
|
313
|
+
const config = manager.loadSync();
|
|
314
|
+
manager.mergeWithEnv();
|
|
315
|
+
return config;
|
|
316
|
+
}
|
|
317
|
+
export default ConfigManager;
|