@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.
- package/README.md +3 -3
- package/dist/cli/index.js +382 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/config/manager.d.ts +6 -0
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +77 -0
- package/dist/config/manager.js.map +1 -1
- package/dist/storage/index.d.ts +10 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +89 -6
- package/dist/storage/index.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/crypto.d.ts +3 -0
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +12 -0
- package/dist/utils/crypto.js.map +1 -1
- package/package.json +6 -1
- package/.github/workflows/publish.yml +0 -48
- package/__tests__/config.test.ts +0 -65
- package/__tests__/crypto.test.ts +0 -76
- package/__tests__/http.test.ts +0 -49
- package/__tests__/storage.test.ts +0 -94
- package/jest.config.js +0 -11
- package/src/adapters/base.ts +0 -542
- package/src/adapters/gemini.ts +0 -228
- package/src/adapters/index.ts +0 -4
- package/src/adapters/openai.ts +0 -238
- package/src/adapters/rest.ts +0 -298
- package/src/cli/index.ts +0 -516
- package/src/cli.ts +0 -2
- package/src/config/manager.ts +0 -137
- package/src/index.ts +0 -4
- package/src/mcp/index.ts +0 -1
- package/src/mcp/server.ts +0 -67
- package/src/server/index.ts +0 -1
- package/src/server/unified.ts +0 -474
- package/src/storage/index.ts +0 -128
- package/src/types.ts +0 -183
- package/src/utils/crypto.ts +0 -100
- package/src/utils/http.ts +0 -119
- package/src/utils/session.ts +0 -146
- package/tsconfig.json +0 -20
package/src/adapters/base.ts
DELETED
|
@@ -1,542 +0,0 @@
|
|
|
1
|
-
import { StorageManager, LogManager } from '../storage/index.js';
|
|
2
|
-
import { EnvCPConfig, Variable, ToolDefinition } from '../types.js';
|
|
3
|
-
import { maskValue } from '../utils/crypto.js';
|
|
4
|
-
import { canAccess, isBlacklisted, canAIActiveCheck, validateVariableName, matchesPattern } from '../config/manager.js';
|
|
5
|
-
import { SessionManager } from '../utils/session.js';
|
|
6
|
-
import * as fs from 'fs-extra';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
import * as dotenv from 'dotenv';
|
|
9
|
-
|
|
10
|
-
export abstract class BaseAdapter {
|
|
11
|
-
protected storage: StorageManager;
|
|
12
|
-
protected logs: LogManager;
|
|
13
|
-
protected sessionManager: SessionManager;
|
|
14
|
-
protected config: EnvCPConfig;
|
|
15
|
-
protected projectPath: string;
|
|
16
|
-
protected tools: Map<string, ToolDefinition>;
|
|
17
|
-
|
|
18
|
-
constructor(config: EnvCPConfig, projectPath: string, password?: string) {
|
|
19
|
-
this.config = config;
|
|
20
|
-
this.projectPath = projectPath;
|
|
21
|
-
this.storage = new StorageManager(
|
|
22
|
-
path.join(projectPath, config.storage.path),
|
|
23
|
-
config.storage.encrypted
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
this.sessionManager = new SessionManager(
|
|
27
|
-
path.join(projectPath, config.session?.path || '.envcp/.session'),
|
|
28
|
-
config.session?.timeout_minutes || 30,
|
|
29
|
-
config.session?.max_extensions || 5
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
if (password) {
|
|
33
|
-
this.storage.setPassword(password);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
this.logs = new LogManager(path.join(projectPath, '.envcp', 'logs'));
|
|
37
|
-
this.tools = new Map();
|
|
38
|
-
this.registerTools();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
protected abstract registerTools(): void;
|
|
42
|
-
|
|
43
|
-
protected registerDefaultTools(): void {
|
|
44
|
-
const tools: ToolDefinition[] = [
|
|
45
|
-
{
|
|
46
|
-
name: 'envcp_list',
|
|
47
|
-
description: 'List all available environment variable names. Values are never shown.',
|
|
48
|
-
parameters: {
|
|
49
|
-
type: 'object',
|
|
50
|
-
properties: {
|
|
51
|
-
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' },
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
handler: async (params) => this.listVariables(params as { tags?: string[] }),
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'envcp_get',
|
|
58
|
-
description: 'Get an environment variable. Returns masked value by default.',
|
|
59
|
-
parameters: {
|
|
60
|
-
type: 'object',
|
|
61
|
-
properties: {
|
|
62
|
-
name: { type: 'string', description: 'Variable name' },
|
|
63
|
-
show_value: { type: 'boolean', description: 'Show actual value (requires user confirmation)' },
|
|
64
|
-
},
|
|
65
|
-
required: ['name'],
|
|
66
|
-
},
|
|
67
|
-
handler: async (params) => this.getVariable(params as { name: string; show_value?: boolean }),
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
name: 'envcp_set',
|
|
71
|
-
description: 'Create or update an environment variable.',
|
|
72
|
-
parameters: {
|
|
73
|
-
type: 'object',
|
|
74
|
-
properties: {
|
|
75
|
-
name: { type: 'string', description: 'Variable name' },
|
|
76
|
-
value: { type: 'string', description: 'Variable value' },
|
|
77
|
-
tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
|
|
78
|
-
description: { type: 'string', description: 'Description' },
|
|
79
|
-
},
|
|
80
|
-
required: ['name', 'value'],
|
|
81
|
-
},
|
|
82
|
-
handler: async (params) => this.setVariable(params as { name: string; value: string; tags?: string[]; description?: string }),
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
name: 'envcp_delete',
|
|
86
|
-
description: 'Delete an environment variable.',
|
|
87
|
-
parameters: {
|
|
88
|
-
type: 'object',
|
|
89
|
-
properties: {
|
|
90
|
-
name: { type: 'string', description: 'Variable name' },
|
|
91
|
-
},
|
|
92
|
-
required: ['name'],
|
|
93
|
-
},
|
|
94
|
-
handler: async (params) => this.deleteVariable(params as { name: string }),
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
name: 'envcp_sync',
|
|
98
|
-
description: 'Sync variables to .env file.',
|
|
99
|
-
parameters: { type: 'object', properties: {} },
|
|
100
|
-
handler: async () => this.syncToEnv(),
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: 'envcp_run',
|
|
104
|
-
description: 'Execute a command with environment variables injected.',
|
|
105
|
-
parameters: {
|
|
106
|
-
type: 'object',
|
|
107
|
-
properties: {
|
|
108
|
-
command: { type: 'string', description: 'Command to execute' },
|
|
109
|
-
variables: { type: 'array', items: { type: 'string' }, description: 'Variables to inject' },
|
|
110
|
-
},
|
|
111
|
-
required: ['command', 'variables'],
|
|
112
|
-
},
|
|
113
|
-
handler: async (params) => this.runCommand(params as { command: string; variables: string[] }),
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
name: 'envcp_add_to_env',
|
|
117
|
-
description: 'Write a stored variable to a .env file.',
|
|
118
|
-
parameters: {
|
|
119
|
-
type: 'object',
|
|
120
|
-
properties: {
|
|
121
|
-
name: { type: 'string', description: 'Variable name to add' },
|
|
122
|
-
env_file: { type: 'string', description: 'Path to .env file (default: .env)' },
|
|
123
|
-
},
|
|
124
|
-
required: ['name'],
|
|
125
|
-
},
|
|
126
|
-
handler: async (params) => this.addToEnv(params as { name: string; env_file?: string }),
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: 'envcp_check_access',
|
|
130
|
-
description: 'Check if a variable exists and can be accessed.',
|
|
131
|
-
parameters: {
|
|
132
|
-
type: 'object',
|
|
133
|
-
properties: {
|
|
134
|
-
name: { type: 'string', description: 'Variable name to check' },
|
|
135
|
-
},
|
|
136
|
-
required: ['name'],
|
|
137
|
-
},
|
|
138
|
-
handler: async (params) => this.checkAccess(params as { name: string }),
|
|
139
|
-
},
|
|
140
|
-
];
|
|
141
|
-
|
|
142
|
-
tools.forEach(tool => this.tools.set(tool.name, tool));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async init(): Promise<void> {
|
|
146
|
-
await this.logs.init();
|
|
147
|
-
await this.sessionManager.init();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
protected async ensurePassword(): Promise<void> {
|
|
151
|
-
const pwd = this.sessionManager.getPassword();
|
|
152
|
-
if (pwd && await this.sessionManager.isValid()) {
|
|
153
|
-
this.storage.setPassword(pwd);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
throw new Error('Session locked. Please unlock first using: envcp unlock');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
getToolDefinitions(): ToolDefinition[] {
|
|
160
|
-
return Array.from(this.tools.values());
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async callTool(name: string, params: Record<string, unknown>): Promise<unknown> {
|
|
164
|
-
const tool = this.tools.get(name);
|
|
165
|
-
if (!tool) {
|
|
166
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
167
|
-
}
|
|
168
|
-
await this.ensurePassword();
|
|
169
|
-
return tool.handler(params);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Shared tool implementations
|
|
173
|
-
protected async listVariables(args: { tags?: string[] }): Promise<{ variables: string[]; count: number }> {
|
|
174
|
-
if (!this.config.access.allow_ai_read) {
|
|
175
|
-
throw new Error('AI read access is disabled');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!canAIActiveCheck(this.config)) {
|
|
179
|
-
throw new Error('AI active check is disabled. User must explicitly mention variable names.');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const names = await this.storage.list();
|
|
183
|
-
let filtered = names.filter(n => canAccess(n, this.config) && !isBlacklisted(n, this.config));
|
|
184
|
-
|
|
185
|
-
if (args.tags && args.tags.length > 0) {
|
|
186
|
-
const variables = await this.storage.load();
|
|
187
|
-
filtered = filtered.filter(name => {
|
|
188
|
-
const v = variables[name];
|
|
189
|
-
return v.tags && args.tags!.some(t => v.tags!.includes(t));
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
await this.logs.log({
|
|
194
|
-
timestamp: new Date().toISOString(),
|
|
195
|
-
operation: 'list',
|
|
196
|
-
source: 'api',
|
|
197
|
-
success: true,
|
|
198
|
-
message: `Listed ${filtered.length} variables`,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
return { variables: filtered, count: filtered.length };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
protected async getVariable(args: { name: string; show_value?: boolean }): Promise<{
|
|
205
|
-
name: string;
|
|
206
|
-
value: string;
|
|
207
|
-
tags?: string[];
|
|
208
|
-
description?: string;
|
|
209
|
-
encrypted: boolean;
|
|
210
|
-
}> {
|
|
211
|
-
if (!this.config.access.allow_ai_read) {
|
|
212
|
-
throw new Error('AI read access is disabled');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const variable = await this.storage.get(args.name);
|
|
216
|
-
|
|
217
|
-
if (!variable) {
|
|
218
|
-
throw new Error(`Variable '${args.name}' not found`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (isBlacklisted(args.name, this.config)) {
|
|
222
|
-
throw new Error(`Variable '${args.name}' is blacklisted and cannot be accessed`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (!canAccess(args.name, this.config)) {
|
|
226
|
-
throw new Error(`Access denied to variable '${args.name}'`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
variable.accessed = new Date().toISOString();
|
|
230
|
-
await this.storage.set(args.name, variable);
|
|
231
|
-
|
|
232
|
-
const canReveal = args.show_value && !this.config.access.mask_values && !this.config.access.require_confirmation;
|
|
233
|
-
const value = canReveal ? variable.value : maskValue(variable.value);
|
|
234
|
-
|
|
235
|
-
await this.logs.log({
|
|
236
|
-
timestamp: new Date().toISOString(),
|
|
237
|
-
operation: 'get',
|
|
238
|
-
variable: args.name,
|
|
239
|
-
source: 'api',
|
|
240
|
-
success: true,
|
|
241
|
-
message: canReveal ? 'Value revealed' : 'Value masked',
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
name: variable.name,
|
|
246
|
-
value: value,
|
|
247
|
-
tags: variable.tags,
|
|
248
|
-
description: variable.description,
|
|
249
|
-
encrypted: variable.encrypted,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
protected async setVariable(args: {
|
|
254
|
-
name: string;
|
|
255
|
-
value: string;
|
|
256
|
-
tags?: string[];
|
|
257
|
-
description?: string;
|
|
258
|
-
}): Promise<{ success: boolean; message: string }> {
|
|
259
|
-
if (!this.config.access.allow_ai_write) {
|
|
260
|
-
throw new Error('AI write access is disabled');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (!validateVariableName(args.name)) {
|
|
264
|
-
throw new Error(`Invalid variable name '${args.name}'. Must match [A-Za-z_][A-Za-z0-9_]*`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (isBlacklisted(args.name, this.config)) {
|
|
268
|
-
throw new Error(`Variable '${args.name}' is blacklisted`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const existing = await this.storage.get(args.name);
|
|
272
|
-
const now = new Date().toISOString();
|
|
273
|
-
|
|
274
|
-
const variable: Variable = {
|
|
275
|
-
name: args.name,
|
|
276
|
-
value: args.value,
|
|
277
|
-
encrypted: this.config.storage.encrypted,
|
|
278
|
-
tags: args.tags,
|
|
279
|
-
description: args.description,
|
|
280
|
-
created: existing?.created || now,
|
|
281
|
-
updated: now,
|
|
282
|
-
sync_to_env: true,
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
await this.storage.set(args.name, variable);
|
|
286
|
-
|
|
287
|
-
await this.logs.log({
|
|
288
|
-
timestamp: new Date().toISOString(),
|
|
289
|
-
operation: existing ? 'update' : 'add',
|
|
290
|
-
variable: args.name,
|
|
291
|
-
source: 'api',
|
|
292
|
-
success: true,
|
|
293
|
-
message: `Variable ${existing ? 'updated' : 'created'}`,
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
return { success: true, message: `Variable '${args.name}' ${existing ? 'updated' : 'created'}` };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
protected async deleteVariable(args: { name: string }): Promise<{ success: boolean; message: string }> {
|
|
300
|
-
if (!this.config.access.allow_ai_delete) {
|
|
301
|
-
throw new Error('AI delete access is disabled');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const deleted = await this.storage.delete(args.name);
|
|
305
|
-
|
|
306
|
-
await this.logs.log({
|
|
307
|
-
timestamp: new Date().toISOString(),
|
|
308
|
-
operation: 'delete',
|
|
309
|
-
variable: args.name,
|
|
310
|
-
source: 'api',
|
|
311
|
-
success: deleted,
|
|
312
|
-
message: deleted ? 'Variable deleted' : 'Variable not found',
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
success: deleted,
|
|
317
|
-
message: deleted ? `Variable '${args.name}' deleted` : `Variable '${args.name}' not found`
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
protected async syncToEnv(): Promise<{ success: boolean; message: string }> {
|
|
322
|
-
if (!this.config.access.allow_ai_export) {
|
|
323
|
-
throw new Error('AI export access is disabled');
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (!this.config.sync.enabled) {
|
|
327
|
-
throw new Error('Sync is disabled in configuration');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const variables = await this.storage.load();
|
|
331
|
-
const lines: string[] = [];
|
|
332
|
-
|
|
333
|
-
if (this.config.sync.header) {
|
|
334
|
-
lines.push(this.config.sync.header);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
for (const [name, variable] of Object.entries(variables)) {
|
|
338
|
-
if (isBlacklisted(name, this.config)) {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const excluded = this.config.sync.exclude?.some(pattern => matchesPattern(name, pattern));
|
|
343
|
-
|
|
344
|
-
if (excluded || !variable.sync_to_env) {
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const needsQuoting = /[\s#"'\\]/.test(variable.value);
|
|
349
|
-
const val = needsQuoting ? `"${variable.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : variable.value;
|
|
350
|
-
lines.push(`${name}=${val}`);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const envPath = path.join(this.projectPath, this.config.sync.target);
|
|
354
|
-
await fs.writeFile(envPath, lines.join('\n'), 'utf8');
|
|
355
|
-
|
|
356
|
-
await this.logs.log({
|
|
357
|
-
timestamp: new Date().toISOString(),
|
|
358
|
-
operation: 'sync',
|
|
359
|
-
source: 'api',
|
|
360
|
-
success: true,
|
|
361
|
-
message: `Synced ${lines.length} variables to ${this.config.sync.target}`,
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
return { success: true, message: `Synced ${lines.length} variables to ${this.config.sync.target}` };
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
protected async addToEnv(args: { name: string; env_file?: string }): Promise<{ success: boolean; message: string }> {
|
|
368
|
-
const variable = await this.storage.get(args.name);
|
|
369
|
-
|
|
370
|
-
if (!variable) {
|
|
371
|
-
throw new Error(`Variable '${args.name}' not found`);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (isBlacklisted(args.name, this.config)) {
|
|
375
|
-
throw new Error(`Variable '${args.name}' is blacklisted`);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const envPath = path.resolve(this.projectPath, args.env_file || '.env');
|
|
379
|
-
if (!envPath.startsWith(path.resolve(this.projectPath))) {
|
|
380
|
-
throw new Error('env_file must be within the project directory');
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
let content = '';
|
|
384
|
-
|
|
385
|
-
if (await fs.pathExists(envPath)) {
|
|
386
|
-
content = await fs.readFile(envPath, 'utf8');
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const envVars = dotenv.parse(content);
|
|
390
|
-
|
|
391
|
-
const needsQuoting = /[\s#"'\\]/.test(variable.value);
|
|
392
|
-
const quotedValue = needsQuoting ? `"${variable.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : variable.value;
|
|
393
|
-
|
|
394
|
-
if (envVars[args.name]) {
|
|
395
|
-
const lines = content.split('\n');
|
|
396
|
-
const newLines = lines.map(line => {
|
|
397
|
-
if (line.startsWith(`${args.name}=`)) {
|
|
398
|
-
return `${args.name}=${quotedValue}`;
|
|
399
|
-
}
|
|
400
|
-
return line;
|
|
401
|
-
});
|
|
402
|
-
content = newLines.join('\n');
|
|
403
|
-
} else {
|
|
404
|
-
content += `\n${args.name}=${quotedValue}`;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
await fs.writeFile(envPath, content, 'utf8');
|
|
408
|
-
|
|
409
|
-
await this.logs.log({
|
|
410
|
-
timestamp: new Date().toISOString(),
|
|
411
|
-
operation: 'add',
|
|
412
|
-
variable: args.name,
|
|
413
|
-
source: 'api',
|
|
414
|
-
success: true,
|
|
415
|
-
message: `Added to ${args.env_file || '.env'}`,
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
return { success: true, message: `Variable '${args.name}' added to ${args.env_file || '.env'}` };
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
protected async checkAccess(args: { name: string }): Promise<{
|
|
422
|
-
name: string;
|
|
423
|
-
accessible: boolean;
|
|
424
|
-
message: string;
|
|
425
|
-
}> {
|
|
426
|
-
const variable = await this.storage.get(args.name);
|
|
427
|
-
const exists = !!variable;
|
|
428
|
-
const blacklisted = isBlacklisted(args.name, this.config);
|
|
429
|
-
const accessible = exists && !blacklisted && canAccess(args.name, this.config);
|
|
430
|
-
|
|
431
|
-
await this.logs.log({
|
|
432
|
-
timestamp: new Date().toISOString(),
|
|
433
|
-
operation: 'check_access',
|
|
434
|
-
variable: args.name,
|
|
435
|
-
source: 'api',
|
|
436
|
-
success: true,
|
|
437
|
-
message: `Access check: ${accessible ? 'granted' : 'denied'}`,
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
name: args.name,
|
|
442
|
-
accessible,
|
|
443
|
-
message: accessible ? 'Variable exists and can be accessed' : 'Variable cannot be accessed',
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
private parseCommand(command: string): { program: string; args: string[] } {
|
|
448
|
-
const tokens: string[] = [];
|
|
449
|
-
let current = '';
|
|
450
|
-
let inSingle = false;
|
|
451
|
-
let inDouble = false;
|
|
452
|
-
|
|
453
|
-
for (let i = 0; i < command.length; i++) {
|
|
454
|
-
const ch = command[i];
|
|
455
|
-
if (ch === "'" && !inDouble) {
|
|
456
|
-
inSingle = !inSingle;
|
|
457
|
-
} else if (ch === '"' && !inSingle) {
|
|
458
|
-
inDouble = !inDouble;
|
|
459
|
-
} else if (ch === ' ' && !inSingle && !inDouble) {
|
|
460
|
-
if (current.length > 0) {
|
|
461
|
-
tokens.push(current);
|
|
462
|
-
current = '';
|
|
463
|
-
}
|
|
464
|
-
} else {
|
|
465
|
-
current += ch;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (current.length > 0) tokens.push(current);
|
|
469
|
-
|
|
470
|
-
if (tokens.length === 0) throw new Error('Empty command');
|
|
471
|
-
return { program: tokens[0], args: tokens.slice(1) };
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
private validateCommand(command: string): void {
|
|
475
|
-
const shellMetachars = /[;&|`$(){}!><\n\\]/;
|
|
476
|
-
if (shellMetachars.test(command)) {
|
|
477
|
-
throw new Error('Command contains disallowed shell metacharacters: ; & | ` $ ( ) { } ! > < \\');
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
protected async runCommand(args: { command: string; variables: string[] }): Promise<{
|
|
482
|
-
exitCode: number | null;
|
|
483
|
-
stdout: string;
|
|
484
|
-
stderr: string;
|
|
485
|
-
}> {
|
|
486
|
-
if (!this.config.access.allow_ai_execute) {
|
|
487
|
-
throw new Error('AI command execution is disabled');
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
this.validateCommand(args.command);
|
|
491
|
-
|
|
492
|
-
const { spawn } = await import('child_process');
|
|
493
|
-
const { program: prog, args: cmdArgs } = this.parseCommand(args.command);
|
|
494
|
-
|
|
495
|
-
if (this.config.access.allowed_commands && this.config.access.allowed_commands.length > 0) {
|
|
496
|
-
if (!this.config.access.allowed_commands.includes(prog)) {
|
|
497
|
-
throw new Error(`Command '${prog}' is not in the allowed commands list`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
const env: Record<string, string> = { ...process.env } as Record<string, string>;
|
|
501
|
-
|
|
502
|
-
for (const name of args.variables) {
|
|
503
|
-
if (isBlacklisted(name, this.config)) {
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
const variable = await this.storage.get(name);
|
|
507
|
-
if (variable) {
|
|
508
|
-
env[name] = variable.value;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const TIMEOUT_MS = 30000;
|
|
513
|
-
|
|
514
|
-
return new Promise((resolve) => {
|
|
515
|
-
const proc = spawn(prog, cmdArgs, {
|
|
516
|
-
env,
|
|
517
|
-
cwd: this.projectPath,
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
let stdout = '';
|
|
521
|
-
let stderr = '';
|
|
522
|
-
let killed = false;
|
|
523
|
-
|
|
524
|
-
const timer = setTimeout(() => {
|
|
525
|
-
killed = true;
|
|
526
|
-
proc.kill('SIGTERM');
|
|
527
|
-
setTimeout(() => { if (!proc.killed) proc.kill('SIGKILL'); }, 5000);
|
|
528
|
-
}, TIMEOUT_MS);
|
|
529
|
-
|
|
530
|
-
proc.stdout.on('data', (data) => { stdout += data; });
|
|
531
|
-
proc.stderr.on('data', (data) => { stderr += data; });
|
|
532
|
-
|
|
533
|
-
proc.on('close', (code) => {
|
|
534
|
-
clearTimeout(timer);
|
|
535
|
-
if (killed) {
|
|
536
|
-
stderr += '\n[Process killed: exceeded 30s timeout]';
|
|
537
|
-
}
|
|
538
|
-
resolve({ exitCode: code, stdout, stderr });
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
}
|