@fentz26/envcp 1.0.2 → 1.0.3

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.
Files changed (45) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/index.js +382 -5
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/config/manager.d.ts +6 -0
  5. package/dist/config/manager.d.ts.map +1 -1
  6. package/dist/config/manager.js +77 -0
  7. package/dist/config/manager.js.map +1 -1
  8. package/dist/storage/index.d.ts +10 -1
  9. package/dist/storage/index.d.ts.map +1 -1
  10. package/dist/storage/index.js +89 -6
  11. package/dist/storage/index.js.map +1 -1
  12. package/dist/types.d.ts +18 -0
  13. package/dist/types.d.ts.map +1 -1
  14. package/dist/types.js +4 -0
  15. package/dist/types.js.map +1 -1
  16. package/dist/utils/crypto.d.ts +3 -0
  17. package/dist/utils/crypto.d.ts.map +1 -1
  18. package/dist/utils/crypto.js +12 -0
  19. package/dist/utils/crypto.js.map +1 -1
  20. package/package.json +6 -1
  21. package/.github/workflows/publish.yml +0 -48
  22. package/__tests__/config.test.ts +0 -65
  23. package/__tests__/crypto.test.ts +0 -76
  24. package/__tests__/http.test.ts +0 -49
  25. package/__tests__/storage.test.ts +0 -94
  26. package/jest.config.js +0 -11
  27. package/src/adapters/base.ts +0 -542
  28. package/src/adapters/gemini.ts +0 -228
  29. package/src/adapters/index.ts +0 -4
  30. package/src/adapters/openai.ts +0 -238
  31. package/src/adapters/rest.ts +0 -298
  32. package/src/cli/index.ts +0 -516
  33. package/src/cli.ts +0 -2
  34. package/src/config/manager.ts +0 -137
  35. package/src/index.ts +0 -4
  36. package/src/mcp/index.ts +0 -1
  37. package/src/mcp/server.ts +0 -67
  38. package/src/server/index.ts +0 -1
  39. package/src/server/unified.ts +0 -474
  40. package/src/storage/index.ts +0 -128
  41. package/src/types.ts +0 -183
  42. package/src/utils/crypto.ts +0 -100
  43. package/src/utils/http.ts +0 -119
  44. package/src/utils/session.ts +0 -146
  45. package/tsconfig.json +0 -20
package/src/mcp/server.ts DELETED
@@ -1,67 +0,0 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import {
4
- CallToolRequestSchema,
5
- ListToolsRequestSchema,
6
- ErrorCode,
7
- McpError,
8
- } from '@modelcontextprotocol/sdk/types.js';
9
- import { BaseAdapter } from '../adapters/base.js';
10
- import { EnvCPConfig } from '../types.js';
11
-
12
- class McpAdapter extends BaseAdapter {
13
- protected registerTools(): void {
14
- this.registerDefaultTools();
15
- }
16
- }
17
-
18
- export class EnvCPServer {
19
- private server: Server;
20
- private adapter: McpAdapter;
21
-
22
- constructor(config: EnvCPConfig, projectPath: string, password?: string) {
23
- this.adapter = new McpAdapter(config, projectPath, password);
24
-
25
- this.server = new Server(
26
- { name: 'envcp', version: '1.0.0' },
27
- { capabilities: { tools: {} } }
28
- );
29
-
30
- this.setupHandlers();
31
- }
32
-
33
- private setupHandlers(): void {
34
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
35
- tools: this.adapter.getToolDefinitions().map(tool => ({
36
- name: tool.name,
37
- description: tool.description,
38
- inputSchema: tool.parameters,
39
- })),
40
- }));
41
-
42
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
43
- const { name, arguments: args } = request.params;
44
-
45
- try {
46
- const result = await this.adapter.callTool(name, (args || {}) as Record<string, unknown>);
47
- return {
48
- content: [
49
- {
50
- type: 'text',
51
- text: JSON.stringify(result, null, 2),
52
- },
53
- ],
54
- };
55
- } catch (error: unknown) {
56
- const message = error instanceof Error ? error.message : String(error);
57
- throw new McpError(ErrorCode.InternalError, message);
58
- }
59
- });
60
- }
61
-
62
- async start(): Promise<void> {
63
- await this.adapter.init();
64
- const transport = new StdioServerTransport();
65
- await this.server.connect(transport);
66
- }
67
- }
@@ -1 +0,0 @@
1
- export { UnifiedServer } from './unified.js';
@@ -1,474 +0,0 @@
1
- import { EnvCPConfig, ServerMode, ServerConfig, ClientType } from '../types.js';
2
- import { RESTAdapter } from '../adapters/rest.js';
3
- import { OpenAIAdapter } from '../adapters/openai.js';
4
- import { GeminiAdapter } from '../adapters/gemini.js';
5
- import { EnvCPServer } from '../mcp/server.js';
6
- import { setCorsHeaders, sendJson, parseBody, validateApiKey, RateLimiter, rateLimitMiddleware } from '../utils/http.js';
7
- import * as http from 'http';
8
-
9
- export class UnifiedServer {
10
- private config: EnvCPConfig;
11
- private serverConfig: ServerConfig;
12
- private projectPath: string;
13
- private password?: string;
14
-
15
- private restAdapter: RESTAdapter | null = null;
16
- private openaiAdapter: OpenAIAdapter | null = null;
17
- private geminiAdapter: GeminiAdapter | null = null;
18
- private mcpServer: EnvCPServer | null = null;
19
- private httpServer: http.Server | null = null;
20
- private rateLimiter: RateLimiter = new RateLimiter(60, 60000);
21
-
22
- constructor(config: EnvCPConfig, serverConfig: ServerConfig, projectPath: string, password?: string) {
23
- this.config = config;
24
- this.serverConfig = serverConfig;
25
- this.projectPath = projectPath;
26
- this.password = password;
27
- }
28
-
29
- // Detect client type from request headers
30
- detectClientType(req: http.IncomingMessage): ClientType {
31
- const userAgent = req.headers['user-agent']?.toLowerCase() || '';
32
- const pathname = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`).pathname;
33
-
34
- // Check for OpenAI-style requests
35
- if (pathname.startsWith('/v1/chat') ||
36
- pathname.startsWith('/v1/functions') ||
37
- pathname.startsWith('/v1/tool_calls') ||
38
- req.headers['openai-organization'] ||
39
- userAgent.includes('openai')) {
40
- return 'openai';
41
- }
42
-
43
- // Check for Gemini-style requests
44
- if (pathname.includes(':generateContent') ||
45
- pathname.startsWith('/v1beta') ||
46
- pathname.startsWith('/v1/function_calls') ||
47
- req.headers['x-goog-api-key'] ||
48
- userAgent.includes('google') ||
49
- userAgent.includes('gemini')) {
50
- return 'gemini';
51
- }
52
-
53
- // Check for MCP (typically stdio, but could be HTTP)
54
- if (req.headers['x-mcp-version'] ||
55
- userAgent.includes('mcp') ||
56
- userAgent.includes('claude')) {
57
- return 'mcp';
58
- }
59
-
60
- // Default to REST for standard HTTP requests
61
- if (pathname.startsWith('/api')) {
62
- return 'rest';
63
- }
64
-
65
- return 'unknown';
66
- }
67
-
68
-
69
- async start(): Promise<void> {
70
- const { mode, port, host, api_key } = this.serverConfig;
71
-
72
- // MCP mode uses stdio, not HTTP
73
- if (mode === 'mcp') {
74
- this.mcpServer = new EnvCPServer(this.config, this.projectPath, this.password);
75
- await this.mcpServer.start();
76
- return;
77
- }
78
-
79
- // Initialize adapters based on mode
80
- if (mode === 'rest' || mode === 'all' || mode === 'auto') {
81
- this.restAdapter = new RESTAdapter(this.config, this.projectPath, this.password);
82
- await this.restAdapter.init();
83
- }
84
-
85
- if (mode === 'openai' || mode === 'all' || mode === 'auto') {
86
- this.openaiAdapter = new OpenAIAdapter(this.config, this.projectPath, this.password);
87
- await this.openaiAdapter.init();
88
- }
89
-
90
- if (mode === 'gemini' || mode === 'all' || mode === 'auto') {
91
- this.geminiAdapter = new GeminiAdapter(this.config, this.projectPath, this.password);
92
- await this.geminiAdapter.init();
93
- }
94
-
95
- // Single mode - start specific adapter server
96
- if (mode === 'rest') {
97
- await this.restAdapter!.startServer(port, host, api_key);
98
- return;
99
- }
100
-
101
- if (mode === 'openai') {
102
- await this.openaiAdapter!.startServer(port, host, api_key);
103
- return;
104
- }
105
-
106
- if (mode === 'gemini') {
107
- await this.geminiAdapter!.startServer(port, host, api_key);
108
- return;
109
- }
110
-
111
- // Auto or All mode - unified server that routes based on detection
112
- this.httpServer = http.createServer(async (req, res) => {
113
- setCorsHeaders(res, undefined, req.headers.origin);
114
-
115
- if (req.method === 'OPTIONS') {
116
- res.writeHead(204);
117
- res.end();
118
- return;
119
- }
120
-
121
- if (!rateLimitMiddleware(this.rateLimiter, req, res)) {
122
- return;
123
- }
124
-
125
- // API key validation
126
- if (api_key) {
127
- const providedKey = (req.headers['x-api-key'] ||
128
- req.headers['x-goog-api-key'] ||
129
- req.headers['authorization']?.replace('Bearer ', '')) as string | undefined;
130
- if (!validateApiKey(providedKey, api_key)) {
131
- sendJson(res, 401, { error: 'Invalid API key' });
132
- return;
133
- }
134
- }
135
-
136
- const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
137
- const pathname = parsedUrl.pathname;
138
-
139
- // Root endpoint - show server info and detected mode
140
- if (pathname === '/' && req.method === 'GET') {
141
- const detectedType = this.serverConfig.auto_detect ? this.detectClientType(req) : 'unknown';
142
- sendJson(res, 200, {
143
- name: 'EnvCP Unified Server',
144
- version: '1.0.0',
145
- mode: mode,
146
- detected_client: detectedType,
147
- auto_detect: this.serverConfig.auto_detect,
148
- available_modes: ['rest', 'openai', 'gemini', 'mcp'],
149
- endpoints: {
150
- rest: '/api/*',
151
- openai: '/v1/chat/completions, /v1/functions/*, /v1/tool_calls',
152
- gemini: '/v1/models/envcp:generateContent, /v1/function_calls',
153
- },
154
- });
155
- return;
156
- }
157
-
158
- // Detect client type
159
- let clientType: ClientType = 'unknown';
160
- if (this.serverConfig.auto_detect) {
161
- clientType = this.detectClientType(req);
162
- }
163
-
164
- // Force mode query param
165
- const forceMode = parsedUrl.searchParams.get('mode') || undefined;
166
- if (forceMode && ['rest', 'openai', 'gemini'].includes(forceMode)) {
167
- clientType = forceMode as ClientType;
168
- }
169
-
170
- try {
171
- // Route to appropriate adapter
172
- // REST API routes
173
- if ((clientType === 'rest' || clientType === 'unknown') && pathname.startsWith('/api')) {
174
- await this.handleRESTRequest(req, res);
175
- return;
176
- }
177
-
178
- // OpenAI routes
179
- if (clientType === 'openai' || pathname.startsWith('/v1/chat') || pathname.startsWith('/v1/functions') || pathname === '/v1/tool_calls' || pathname === '/v1/models') {
180
- await this.handleOpenAIRequest(req, res);
181
- return;
182
- }
183
-
184
- // Gemini routes
185
- if (clientType === 'gemini' || pathname.includes(':generateContent') || pathname === '/v1/function_calls' || pathname === '/v1/tools') {
186
- await this.handleGeminiRequest(req, res);
187
- return;
188
- }
189
-
190
- // Default to REST for unknown paths
191
- if (pathname.startsWith('/api')) {
192
- await this.handleRESTRequest(req, res);
193
- return;
194
- }
195
-
196
- // 404 with helpful info
197
- sendJson(res, 404, {
198
- error: 'Not found',
199
- hint: 'Use /api/* for REST, /v1/* for OpenAI, or include :generateContent for Gemini',
200
- available_endpoints: {
201
- rest: '/api/variables, /api/tools',
202
- openai: '/v1/functions, /v1/chat/completions',
203
- gemini: '/v1/models/envcp:generateContent',
204
- },
205
- });
206
-
207
- } catch (error: unknown) {
208
- const message = error instanceof Error ? error.message : String(error);
209
- sendJson(res, 500, { error: message });
210
- }
211
- });
212
-
213
- const shutdown = () => {
214
- this.stop();
215
- process.exit(0);
216
- };
217
- process.on('SIGTERM', shutdown);
218
- process.on('SIGINT', shutdown);
219
-
220
- return new Promise((resolve) => {
221
- this.httpServer!.listen(port, host, () => {
222
- resolve();
223
- });
224
- });
225
- }
226
-
227
- private async handleRESTRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
228
- // Delegate to REST adapter's internal handling
229
- const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
230
- const pathname = parsedUrl.pathname || '/';
231
- const segments = pathname.split('/').filter(Boolean);
232
-
233
- if (!this.restAdapter) {
234
- sendJson(res, 503, { success: false, error: 'REST adapter not initialized' });
235
- return;
236
- }
237
-
238
- const body = (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') ? await parseBody(req) : {};
239
-
240
- try {
241
- if (segments[0] === 'api') {
242
- const resource = segments[1];
243
-
244
- // Health
245
- if (pathname === '/api/health' || pathname === '/api') {
246
- sendJson(res, 200, { success: true, data: { status: 'ok', mode: 'rest' }, timestamp: new Date().toISOString() });
247
- return;
248
- }
249
-
250
- // Tools
251
- if (resource === 'tools' && !segments[2] && req.method === 'GET') {
252
- const tools = this.restAdapter.getToolDefinitions().map(t => ({ name: t.name, description: t.description }));
253
- sendJson(res, 200, { success: true, data: { tools }, timestamp: new Date().toISOString() });
254
- return;
255
- }
256
-
257
- if (resource === 'tools' && segments[2] && req.method === 'POST') {
258
- const result = await this.restAdapter.callTool(segments[2], body);
259
- sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
260
- return;
261
- }
262
-
263
- // Variables
264
- if (resource === 'variables') {
265
- if (!segments[2] && req.method === 'GET') {
266
- const result = await this.restAdapter.callTool('envcp_list', {});
267
- sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
268
- return;
269
- }
270
- if (!segments[2] && req.method === 'POST') {
271
- const result = await this.restAdapter.callTool('envcp_set', body);
272
- sendJson(res, 201, { success: true, data: result, timestamp: new Date().toISOString() });
273
- return;
274
- }
275
- if (segments[2] && req.method === 'GET') {
276
- const result = await this.restAdapter.callTool('envcp_get', { name: segments[2], show_value: parsedUrl.searchParams.get('show_value') === 'true' });
277
- sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
278
- return;
279
- }
280
- if (segments[2] && req.method === 'PUT') {
281
- const result = await this.restAdapter.callTool('envcp_set', { ...body, name: segments[2] });
282
- sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
283
- return;
284
- }
285
- if (segments[2] && req.method === 'DELETE') {
286
- const result = await this.restAdapter.callTool('envcp_delete', { name: segments[2] });
287
- sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
288
- return;
289
- }
290
- }
291
-
292
- // Sync
293
- if (resource === 'sync' && req.method === 'POST') {
294
- const result = await this.restAdapter.callTool('envcp_sync', {});
295
- sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
296
- return;
297
- }
298
-
299
- // Run
300
- if (resource === 'run' && req.method === 'POST') {
301
- const result = await this.restAdapter.callTool('envcp_run', body);
302
- sendJson(res, 200, { success: true, data: result, timestamp: new Date().toISOString() });
303
- return;
304
- }
305
- }
306
-
307
- sendJson(res, 404, { success: false, error: 'Not found', timestamp: new Date().toISOString() });
308
-
309
- } catch (error: unknown) {
310
- const message = error instanceof Error ? error.message : String(error);
311
- sendJson(res, 500, { success: false, error: message, timestamp: new Date().toISOString() });
312
- }
313
- }
314
-
315
- private async handleOpenAIRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
316
- if (!this.openaiAdapter) {
317
- sendJson(res, 503, { error: { message: 'OpenAI adapter not initialized', type: 'service_unavailable' } });
318
- return;
319
- }
320
-
321
- const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
322
- const pathname = parsedUrl.pathname || '/';
323
- const body = req.method === 'POST' ? await parseBody(req) : {};
324
-
325
- try {
326
- if (pathname === '/v1/models' && req.method === 'GET') {
327
- sendJson(res, 200, {
328
- object: 'list',
329
- data: [{ id: 'envcp-1.0', object: 'model', created: Date.now(), owned_by: 'envcp' }],
330
- });
331
- return;
332
- }
333
-
334
- if (pathname === '/v1/functions' && req.method === 'GET') {
335
- sendJson(res, 200, { object: 'list', data: this.openaiAdapter.getOpenAIFunctions() });
336
- return;
337
- }
338
-
339
- if (pathname === '/v1/functions/call' && req.method === 'POST') {
340
- const { name, arguments: args } = body as any;
341
- const result = await this.openaiAdapter.callTool(name, args || {});
342
- sendJson(res, 200, { object: 'function_result', name, result });
343
- return;
344
- }
345
-
346
- if (pathname === '/v1/tool_calls' && req.method === 'POST') {
347
- const { tool_calls } = body as any;
348
- const results = await this.openaiAdapter.processToolCalls(tool_calls);
349
- sendJson(res, 200, { object: 'list', data: results });
350
- return;
351
- }
352
-
353
- if (pathname === '/v1/chat/completions' && req.method === 'POST') {
354
- const messages = (body as any).messages;
355
- const lastMessage = messages?.[messages.length - 1];
356
-
357
- if (lastMessage?.tool_calls) {
358
- const results = await this.openaiAdapter.processToolCalls(lastMessage.tool_calls);
359
- sendJson(res, 200, {
360
- id: `chatcmpl-${Date.now()}`,
361
- object: 'chat.completion',
362
- model: 'envcp-1.0',
363
- choices: [{ index: 0, message: { role: 'assistant', content: null }, finish_reason: 'tool_calls' }],
364
- tool_results: results,
365
- });
366
- return;
367
- }
368
-
369
- sendJson(res, 200, {
370
- id: `chatcmpl-${Date.now()}`,
371
- object: 'chat.completion',
372
- model: 'envcp-1.0',
373
- choices: [{ index: 0, message: { role: 'assistant', content: 'EnvCP tools available.' }, finish_reason: 'stop' }],
374
- available_tools: this.openaiAdapter.getOpenAIFunctions().map(f => ({ type: 'function', function: f })),
375
- });
376
- return;
377
- }
378
-
379
- sendJson(res, 404, { error: { message: 'Not found', type: 'not_found' } });
380
-
381
- } catch (error: unknown) {
382
- const message = error instanceof Error ? error.message : String(error);
383
- sendJson(res, 500, { error: { message, type: 'internal_error' } });
384
- }
385
- }
386
-
387
- private async handleGeminiRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
388
- if (!this.geminiAdapter) {
389
- sendJson(res, 503, { error: { code: 503, message: 'Gemini adapter not initialized', status: 'UNAVAILABLE' } });
390
- return;
391
- }
392
-
393
- const parsedUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
394
- const pathname = parsedUrl.pathname || '/';
395
- const body = req.method === 'POST' ? await parseBody(req) : {};
396
-
397
- try {
398
- if (pathname === '/v1/tools' && req.method === 'GET') {
399
- sendJson(res, 200, { tools: [{ functionDeclarations: this.geminiAdapter.getGeminiFunctionDeclarations() }] });
400
- return;
401
- }
402
-
403
- if (pathname === '/v1/functions/call' && req.method === 'POST') {
404
- const { name, args } = body as any;
405
- const result = await this.geminiAdapter.callTool(name, args || {});
406
- sendJson(res, 200, { name, response: { result } });
407
- return;
408
- }
409
-
410
- if (pathname === '/v1/function_calls' && req.method === 'POST') {
411
- const { functionCalls } = body as any;
412
- const results = await this.geminiAdapter.processFunctionCalls(functionCalls);
413
- sendJson(res, 200, { functionResponses: results });
414
- return;
415
- }
416
-
417
- if (pathname.includes(':generateContent') && req.method === 'POST') {
418
- const contents = (body as any).contents;
419
- const functionCalls: any[] = [];
420
-
421
- if (contents) {
422
- for (const content of contents) {
423
- for (const part of content.parts || []) {
424
- if (part.functionCall) functionCalls.push(part.functionCall);
425
- }
426
- }
427
- }
428
-
429
- if (functionCalls.length > 0) {
430
- const results = await this.geminiAdapter.processFunctionCalls(functionCalls);
431
- sendJson(res, 200, {
432
- candidates: [{
433
- content: { parts: results.map(r => ({ functionResponse: r })), role: 'model' },
434
- finishReason: 'STOP',
435
- }],
436
- });
437
- return;
438
- }
439
-
440
- sendJson(res, 200, {
441
- candidates: [{
442
- content: { parts: [{ text: 'EnvCP tools available.' }], role: 'model' },
443
- finishReason: 'STOP',
444
- }],
445
- availableTools: [{ functionDeclarations: this.geminiAdapter.getGeminiFunctionDeclarations() }],
446
- });
447
- return;
448
- }
449
-
450
- sendJson(res, 404, { error: { code: 404, message: 'Not found', status: 'NOT_FOUND' } });
451
-
452
- } catch (error: unknown) {
453
- const message = error instanceof Error ? error.message : String(error);
454
- sendJson(res, 500, { error: { code: 500, message, status: 'INTERNAL' } });
455
- }
456
- }
457
-
458
-
459
- stop(): void {
460
- if (this.httpServer) {
461
- this.httpServer.close();
462
- this.httpServer = null;
463
- }
464
- if (this.restAdapter) {
465
- this.restAdapter.stopServer();
466
- }
467
- if (this.openaiAdapter) {
468
- this.openaiAdapter.stopServer();
469
- }
470
- if (this.geminiAdapter) {
471
- this.geminiAdapter.stopServer();
472
- }
473
- }
474
- }
@@ -1,128 +0,0 @@
1
- import * as fs from 'fs-extra';
2
- import * as path from 'path';
3
- import { Variable, OperationLog } from '../types.js';
4
- import { encrypt, decrypt } from '../utils/crypto.js';
5
-
6
- export class StorageManager {
7
- private storePath: string;
8
- private encrypted: boolean;
9
- private password?: string;
10
- private cache: Record<string, Variable> | null = null;
11
-
12
- constructor(storePath: string, encrypted: boolean = true) {
13
- this.storePath = storePath;
14
- this.encrypted = encrypted;
15
- }
16
-
17
- setPassword(password: string): void {
18
- if (this.password !== password) {
19
- this.password = password;
20
- this.cache = null;
21
- }
22
- }
23
-
24
- async load(): Promise<Record<string, Variable>> {
25
- if (this.cache !== null) {
26
- return this.cache;
27
- }
28
-
29
- if (!await fs.pathExists(this.storePath)) {
30
- this.cache = {};
31
- return this.cache;
32
- }
33
-
34
- const data = await fs.readFile(this.storePath, 'utf8');
35
-
36
- if (this.encrypted && this.password) {
37
- try {
38
- const decrypted = decrypt(data, this.password);
39
- this.cache = JSON.parse(decrypted);
40
- return this.cache!;
41
- } catch (error) {
42
- throw new Error('Failed to decrypt storage. Invalid password or corrupted data.');
43
- }
44
- }
45
-
46
- this.cache = JSON.parse(data);
47
- return this.cache!;
48
- }
49
-
50
- async save(variables: Record<string, Variable>): Promise<void> {
51
- this.cache = variables;
52
- const data = JSON.stringify(variables, null, 2);
53
-
54
- await fs.ensureDir(path.dirname(this.storePath));
55
-
56
- if (this.encrypted && this.password) {
57
- const encryptedData = encrypt(data, this.password);
58
- await fs.writeFile(this.storePath, encryptedData, 'utf8');
59
- } else {
60
- await fs.writeFile(this.storePath, data, 'utf8');
61
- }
62
- }
63
-
64
- async get(name: string): Promise<Variable | undefined> {
65
- const variables = await this.load();
66
- return variables[name];
67
- }
68
-
69
- async set(name: string, variable: Variable): Promise<void> {
70
- const variables = await this.load();
71
- variables[name] = variable;
72
- await this.save(variables);
73
- }
74
-
75
- async delete(name: string): Promise<boolean> {
76
- const variables = await this.load();
77
- if (variables[name]) {
78
- delete variables[name];
79
- await this.save(variables);
80
- return true;
81
- }
82
- return false;
83
- }
84
-
85
- async list(): Promise<string[]> {
86
- const variables = await this.load();
87
- return Object.keys(variables);
88
- }
89
-
90
- async exists(): Promise<boolean> {
91
- return fs.pathExists(this.storePath);
92
- }
93
-
94
- invalidateCache(): void {
95
- this.cache = null;
96
- }
97
- }
98
-
99
- export class LogManager {
100
- private logDir: string;
101
-
102
- constructor(logDir: string) {
103
- this.logDir = logDir;
104
- }
105
-
106
- async init(): Promise<void> {
107
- await fs.ensureDir(this.logDir);
108
- }
109
-
110
- async log(entry: OperationLog): Promise<void> {
111
- const date = new Date().toISOString().split('T')[0];
112
- const logFile = path.join(this.logDir, `operations-${date}.log`);
113
- const logLine = JSON.stringify(entry) + '\n';
114
- await fs.appendFile(logFile, logLine);
115
- }
116
-
117
- async getLogs(date?: string): Promise<OperationLog[]> {
118
- const logDate = date || new Date().toISOString().split('T')[0];
119
- const logFile = path.join(this.logDir, `operations-${logDate}.log`);
120
-
121
- if (!await fs.pathExists(logFile)) {
122
- return [];
123
- }
124
-
125
- const content = await fs.readFile(logFile, 'utf8');
126
- return content.trim().split('\n').map(line => JSON.parse(line));
127
- }
128
- }