@cpretzinger/boss-claude 1.0.0 → 1.0.2
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 +304 -1
- package/bin/boss-claude.js +1138 -0
- package/bin/commands/mode.js +250 -0
- package/bin/onyx-guard.js +259 -0
- package/bin/onyx-guard.sh +251 -0
- package/bin/prompts.js +284 -0
- package/bin/rollback.js +85 -0
- package/bin/setup-wizard.js +492 -0
- package/config/.env.example +17 -0
- package/lib/README.md +83 -0
- package/lib/agent-logger.js +61 -0
- package/lib/agents/memory-engineers/github-memory-engineer.js +251 -0
- package/lib/agents/memory-engineers/postgres-memory-engineer.js +633 -0
- package/lib/agents/memory-engineers/qdrant-memory-engineer.js +358 -0
- package/lib/agents/memory-engineers/redis-memory-engineer.js +383 -0
- package/lib/agents/memory-supervisor.js +526 -0
- package/lib/agents/registry.js +135 -0
- package/lib/auto-monitor.js +131 -0
- package/lib/checkpoint-hook.js +112 -0
- package/lib/checkpoint.js +319 -0
- package/lib/commentator.js +213 -0
- package/lib/context-scribe.js +120 -0
- package/lib/delegation-strategies.js +326 -0
- package/lib/hierarchy-validator.js +643 -0
- package/lib/index.js +15 -0
- package/lib/init-with-mode.js +261 -0
- package/lib/init.js +44 -6
- package/lib/memory-result-aggregator.js +252 -0
- package/lib/memory.js +35 -7
- package/lib/mode-enforcer.js +473 -0
- package/lib/onyx-banner.js +169 -0
- package/lib/onyx-identity.js +214 -0
- package/lib/onyx-monitor.js +381 -0
- package/lib/onyx-reminder.js +188 -0
- package/lib/onyx-tool-interceptor.js +341 -0
- package/lib/onyx-wrapper.js +315 -0
- package/lib/orchestrator-gate.js +334 -0
- package/lib/output-formatter.js +296 -0
- package/lib/postgres.js +1 -1
- package/lib/prompt-injector.js +220 -0
- package/lib/prompts.js +532 -0
- package/lib/session.js +153 -6
- package/lib/setup/README.md +187 -0
- package/lib/setup/env-manager.js +785 -0
- package/lib/setup/error-recovery.js +630 -0
- package/lib/setup/explain-scopes.js +385 -0
- package/lib/setup/github-instructions.js +333 -0
- package/lib/setup/github-repo.js +254 -0
- package/lib/setup/import-credentials.js +498 -0
- package/lib/setup/index.js +62 -0
- package/lib/setup/init-postgres.js +785 -0
- package/lib/setup/init-redis.js +456 -0
- package/lib/setup/integration-test.js +652 -0
- package/lib/setup/progress.js +357 -0
- package/lib/setup/rollback.js +670 -0
- package/lib/setup/rollback.test.js +452 -0
- package/lib/setup/setup-with-rollback.example.js +351 -0
- package/lib/setup/summary.js +400 -0
- package/lib/setup/test-github-setup.js +10 -0
- package/lib/setup/test-postgres-init.js +98 -0
- package/lib/setup/verify-setup.js +102 -0
- package/lib/task-agent-worker.js +235 -0
- package/lib/token-monitor.js +466 -0
- package/lib/tool-wrapper-integration.js +369 -0
- package/lib/tool-wrapper.js +387 -0
- package/lib/validators/README.md +497 -0
- package/lib/validators/config.js +583 -0
- package/lib/validators/config.test.js +175 -0
- package/lib/validators/github.js +310 -0
- package/lib/validators/github.test.js +61 -0
- package/lib/validators/index.js +15 -0
- package/lib/validators/postgres.js +525 -0
- package/package.json +98 -13
- package/scripts/benchmark-memory.js +433 -0
- package/scripts/check-secrets.sh +12 -0
- package/scripts/fetch-todos.mjs +148 -0
- package/scripts/graceful-shutdown.sh +156 -0
- package/scripts/install-onyx-hooks.js +373 -0
- package/scripts/install.js +119 -18
- package/scripts/redis-monitor.js +284 -0
- package/scripts/redis-setup.js +412 -0
- package/scripts/test-memory-retrieval.js +201 -0
- package/scripts/validate-exports.js +68 -0
- package/scripts/validate-package.js +120 -0
- package/scripts/verify-onyx-deployment.js +309 -0
- package/scripts/verify-redis-deployment.js +354 -0
- package/scripts/verify-redis-init.js +219 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Boss Claude - Environment Variable Manager
|
|
4
|
+
*
|
|
5
|
+
* Safely manages ~/.boss-claude/.env with:
|
|
6
|
+
* - Read/write operations with validation
|
|
7
|
+
* - Automatic backups before changes
|
|
8
|
+
* - File permission checks (600 for security)
|
|
9
|
+
* - Atomic updates to prevent corruption
|
|
10
|
+
* - Key-value parsing with comments preservation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs/promises';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import os from 'os';
|
|
17
|
+
import { createHash } from 'crypto';
|
|
18
|
+
|
|
19
|
+
const ENV_DIR = path.join(os.homedir(), '.boss-claude');
|
|
20
|
+
const ENV_FILE = path.join(ENV_DIR, '.env');
|
|
21
|
+
const BACKUP_DIR = path.join(ENV_DIR, 'backups');
|
|
22
|
+
const SECURE_PERMISSIONS = 0o600; // rw-------
|
|
23
|
+
|
|
24
|
+
export class EnvManager {
|
|
25
|
+
constructor(envPath = ENV_FILE) {
|
|
26
|
+
this.envPath = envPath;
|
|
27
|
+
this.envDir = path.dirname(envPath);
|
|
28
|
+
this.backupDir = BACKUP_DIR;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize environment file and directories
|
|
33
|
+
*/
|
|
34
|
+
async init() {
|
|
35
|
+
try {
|
|
36
|
+
// Create .boss-claude directory if needed
|
|
37
|
+
if (!existsSync(this.envDir)) {
|
|
38
|
+
await fs.mkdir(this.envDir, { recursive: true, mode: 0o700 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create backup directory
|
|
42
|
+
if (!existsSync(this.backupDir)) {
|
|
43
|
+
await fs.mkdir(this.backupDir, { recursive: true, mode: 0o700 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Create .env if it doesn't exist
|
|
47
|
+
if (!existsSync(this.envPath)) {
|
|
48
|
+
await this._createDefaultEnv();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Validate permissions
|
|
52
|
+
await this._validatePermissions();
|
|
53
|
+
|
|
54
|
+
return { success: true, path: this.envPath };
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(`Failed to initialize env manager: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read and parse .env file
|
|
62
|
+
* @returns {Object} Parsed environment variables with metadata
|
|
63
|
+
*/
|
|
64
|
+
async read() {
|
|
65
|
+
try {
|
|
66
|
+
await this._validatePermissions();
|
|
67
|
+
|
|
68
|
+
const content = await fs.readFile(this.envPath, 'utf8');
|
|
69
|
+
const parsed = this._parse(content);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
vars: parsed.vars,
|
|
74
|
+
comments: parsed.comments,
|
|
75
|
+
raw: content,
|
|
76
|
+
path: this.envPath
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error.code === 'ENOENT') {
|
|
80
|
+
return {
|
|
81
|
+
success: true,
|
|
82
|
+
vars: {},
|
|
83
|
+
comments: [],
|
|
84
|
+
raw: '',
|
|
85
|
+
path: this.envPath
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Failed to read env file: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Set or update an environment variable
|
|
94
|
+
* @param {string} key - Variable name
|
|
95
|
+
* @param {string} value - Variable value
|
|
96
|
+
* @param {Object} options - Options (comment, skipBackup)
|
|
97
|
+
*/
|
|
98
|
+
async set(key, value, options = {}) {
|
|
99
|
+
try {
|
|
100
|
+
this._validateKey(key);
|
|
101
|
+
this._validateValue(value);
|
|
102
|
+
|
|
103
|
+
// Create backup unless skipped
|
|
104
|
+
if (!options.skipBackup) {
|
|
105
|
+
await this._createBackup();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const current = await this.read();
|
|
109
|
+
const lines = current.raw.split('\n');
|
|
110
|
+
let updated = false;
|
|
111
|
+
const newLines = [];
|
|
112
|
+
|
|
113
|
+
// Try to update existing key
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
if (line.trim().startsWith('#') || line.trim() === '') {
|
|
116
|
+
newLines.push(line);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
121
|
+
if (match && match[1].trim() === key) {
|
|
122
|
+
// Update existing key
|
|
123
|
+
const formattedValue = this._formatValue(value);
|
|
124
|
+
let newLine = `${key}=${formattedValue}`;
|
|
125
|
+
|
|
126
|
+
if (options.comment) {
|
|
127
|
+
newLine += ` # ${options.comment}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
newLines.push(newLine);
|
|
131
|
+
updated = true;
|
|
132
|
+
} else {
|
|
133
|
+
newLines.push(line);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add new key if not found
|
|
138
|
+
if (!updated) {
|
|
139
|
+
if (newLines.length > 0 && newLines[newLines.length - 1] !== '') {
|
|
140
|
+
newLines.push(''); // Add blank line before new entry
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (options.comment) {
|
|
144
|
+
newLines.push(`# ${options.comment}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const formattedValue = this._formatValue(value);
|
|
148
|
+
newLines.push(`${key}=${formattedValue}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Write atomically
|
|
152
|
+
await this._writeAtomic(newLines.join('\n'));
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
action: updated ? 'updated' : 'added',
|
|
157
|
+
key,
|
|
158
|
+
value,
|
|
159
|
+
path: this.envPath
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new Error(`Failed to set ${key}: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Remove an environment variable
|
|
168
|
+
* @param {string} key - Variable name to remove
|
|
169
|
+
* @param {Object} options - Options (skipBackup)
|
|
170
|
+
*/
|
|
171
|
+
async remove(key, options = {}) {
|
|
172
|
+
try {
|
|
173
|
+
this._validateKey(key);
|
|
174
|
+
|
|
175
|
+
// Create backup unless skipped
|
|
176
|
+
if (!options.skipBackup) {
|
|
177
|
+
await this._createBackup();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const current = await this.read();
|
|
181
|
+
|
|
182
|
+
if (!current.vars[key]) {
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
error: 'Key not found',
|
|
186
|
+
key
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lines = current.raw.split('\n');
|
|
191
|
+
const newLines = [];
|
|
192
|
+
let removed = false;
|
|
193
|
+
let skipNextComment = false;
|
|
194
|
+
|
|
195
|
+
// Remove key and its associated comment if it's directly above
|
|
196
|
+
for (let i = 0; i < lines.length; i++) {
|
|
197
|
+
const line = lines[i];
|
|
198
|
+
|
|
199
|
+
if (line.trim().startsWith('#')) {
|
|
200
|
+
// Check if next line is the key we're removing
|
|
201
|
+
const nextLine = lines[i + 1];
|
|
202
|
+
if (nextLine) {
|
|
203
|
+
const match = nextLine.match(/^([^=]+)=(.*)$/);
|
|
204
|
+
if (match && match[1].trim() === key) {
|
|
205
|
+
skipNextComment = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
newLines.push(line);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
214
|
+
if (match && match[1].trim() === key) {
|
|
215
|
+
removed = true;
|
|
216
|
+
continue; // Skip this line
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
newLines.push(line);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Clean up multiple consecutive blank lines
|
|
223
|
+
const cleaned = this._cleanBlankLines(newLines);
|
|
224
|
+
|
|
225
|
+
await this._writeAtomic(cleaned.join('\n'));
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
action: 'removed',
|
|
230
|
+
key,
|
|
231
|
+
path: this.envPath
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw new Error(`Failed to remove ${key}: ${error.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Bulk update multiple variables
|
|
240
|
+
* @param {Object} vars - Key-value pairs to set
|
|
241
|
+
* @param {Object} options - Options (skipBackup)
|
|
242
|
+
*/
|
|
243
|
+
async bulkSet(vars, options = {}) {
|
|
244
|
+
try {
|
|
245
|
+
// Create single backup for all operations
|
|
246
|
+
if (!options.skipBackup) {
|
|
247
|
+
await this._createBackup();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const results = [];
|
|
251
|
+
|
|
252
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
253
|
+
const result = await this.set(key, value, { skipBackup: true });
|
|
254
|
+
results.push(result);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
success: true,
|
|
259
|
+
results,
|
|
260
|
+
count: results.length,
|
|
261
|
+
path: this.envPath
|
|
262
|
+
};
|
|
263
|
+
} catch (error) {
|
|
264
|
+
throw new Error(`Failed to bulk set: ${error.message}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get a specific environment variable
|
|
270
|
+
* @param {string} key - Variable name
|
|
271
|
+
*/
|
|
272
|
+
async get(key) {
|
|
273
|
+
try {
|
|
274
|
+
const current = await this.read();
|
|
275
|
+
|
|
276
|
+
if (!current.vars[key]) {
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
error: 'Key not found',
|
|
280
|
+
key
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
success: true,
|
|
286
|
+
key,
|
|
287
|
+
value: current.vars[key],
|
|
288
|
+
path: this.envPath
|
|
289
|
+
};
|
|
290
|
+
} catch (error) {
|
|
291
|
+
throw new Error(`Failed to get ${key}: ${error.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* List all environment variables
|
|
297
|
+
*/
|
|
298
|
+
async list() {
|
|
299
|
+
try {
|
|
300
|
+
const current = await this.read();
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
vars: current.vars,
|
|
305
|
+
count: Object.keys(current.vars).length,
|
|
306
|
+
path: this.envPath
|
|
307
|
+
};
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw new Error(`Failed to list vars: ${error.message}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Restore from a backup
|
|
315
|
+
* @param {string} backupName - Backup file name (or 'latest')
|
|
316
|
+
*/
|
|
317
|
+
async restore(backupName = 'latest') {
|
|
318
|
+
try {
|
|
319
|
+
let backupPath;
|
|
320
|
+
|
|
321
|
+
if (backupName === 'latest') {
|
|
322
|
+
const backups = await this.listBackups();
|
|
323
|
+
if (backups.length === 0) {
|
|
324
|
+
throw new Error('No backups found');
|
|
325
|
+
}
|
|
326
|
+
backupPath = backups[0].path;
|
|
327
|
+
} else {
|
|
328
|
+
backupPath = path.join(this.backupDir, backupName);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!existsSync(backupPath)) {
|
|
332
|
+
throw new Error(`Backup not found: ${backupName}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const backupContent = await fs.readFile(backupPath, 'utf8');
|
|
336
|
+
|
|
337
|
+
// Create backup of current state before restore
|
|
338
|
+
await this._createBackup('pre-restore');
|
|
339
|
+
|
|
340
|
+
await this._writeAtomic(backupContent);
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
success: true,
|
|
344
|
+
action: 'restored',
|
|
345
|
+
from: backupPath,
|
|
346
|
+
to: this.envPath
|
|
347
|
+
};
|
|
348
|
+
} catch (error) {
|
|
349
|
+
throw new Error(`Failed to restore: ${error.message}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* List all backups
|
|
355
|
+
*/
|
|
356
|
+
async listBackups() {
|
|
357
|
+
try {
|
|
358
|
+
if (!existsSync(this.backupDir)) {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const files = await fs.readdir(this.backupDir);
|
|
363
|
+
const backups = [];
|
|
364
|
+
|
|
365
|
+
for (const file of files) {
|
|
366
|
+
if (!file.startsWith('.env.backup-')) continue;
|
|
367
|
+
|
|
368
|
+
const filePath = path.join(this.backupDir, file);
|
|
369
|
+
const stats = await fs.stat(filePath);
|
|
370
|
+
|
|
371
|
+
backups.push({
|
|
372
|
+
name: file,
|
|
373
|
+
path: filePath,
|
|
374
|
+
size: stats.size,
|
|
375
|
+
created: stats.mtime,
|
|
376
|
+
timestamp: file.replace('.env.backup-', '').replace('.bak', '')
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Sort by creation time, newest first
|
|
381
|
+
backups.sort((a, b) => b.created - a.created);
|
|
382
|
+
|
|
383
|
+
return backups;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
throw new Error(`Failed to list backups: ${error.message}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Clean old backups (keep last N)
|
|
391
|
+
* @param {number} keep - Number of backups to keep (default: 10)
|
|
392
|
+
*/
|
|
393
|
+
async cleanBackups(keep = 10) {
|
|
394
|
+
try {
|
|
395
|
+
const backups = await this.listBackups();
|
|
396
|
+
|
|
397
|
+
if (backups.length <= keep) {
|
|
398
|
+
return {
|
|
399
|
+
success: true,
|
|
400
|
+
removed: 0,
|
|
401
|
+
kept: backups.length
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const toRemove = backups.slice(keep);
|
|
406
|
+
|
|
407
|
+
for (const backup of toRemove) {
|
|
408
|
+
await fs.unlink(backup.path);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
success: true,
|
|
413
|
+
removed: toRemove.length,
|
|
414
|
+
kept: keep
|
|
415
|
+
};
|
|
416
|
+
} catch (error) {
|
|
417
|
+
throw new Error(`Failed to clean backups: ${error.message}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Validate file permissions
|
|
423
|
+
*/
|
|
424
|
+
async validatePermissions() {
|
|
425
|
+
return await this._validatePermissions();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Fix file permissions to secure mode (600)
|
|
430
|
+
*/
|
|
431
|
+
async fixPermissions() {
|
|
432
|
+
try {
|
|
433
|
+
await fs.chmod(this.envPath, SECURE_PERMISSIONS);
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
success: true,
|
|
437
|
+
permissions: '600',
|
|
438
|
+
path: this.envPath
|
|
439
|
+
};
|
|
440
|
+
} catch (error) {
|
|
441
|
+
throw new Error(`Failed to fix permissions: ${error.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ============================================================================
|
|
446
|
+
// PRIVATE METHODS
|
|
447
|
+
// ============================================================================
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Create default .env file
|
|
451
|
+
*/
|
|
452
|
+
async _createDefaultEnv() {
|
|
453
|
+
const defaultContent = `# Boss Claude Environment Configuration
|
|
454
|
+
# This file contains sensitive configuration - keep permissions at 600
|
|
455
|
+
# Auto-generated on ${new Date().toISOString()}
|
|
456
|
+
|
|
457
|
+
# Redis Configuration
|
|
458
|
+
REDIS_HOST=localhost
|
|
459
|
+
REDIS_PORT=6379
|
|
460
|
+
REDIS_PASSWORD=
|
|
461
|
+
|
|
462
|
+
# Database Configuration (if used)
|
|
463
|
+
# DATABASE_URL=
|
|
464
|
+
|
|
465
|
+
# API Keys (add as needed)
|
|
466
|
+
# OPENAI_API_KEY=
|
|
467
|
+
# ANTHROPIC_API_KEY=
|
|
468
|
+
|
|
469
|
+
# Boss Claude Settings
|
|
470
|
+
BOSS_CLAUDE_DEBUG=false
|
|
471
|
+
`;
|
|
472
|
+
|
|
473
|
+
await fs.writeFile(this.envPath, defaultContent, { mode: SECURE_PERMISSIONS });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Parse .env file content
|
|
478
|
+
*/
|
|
479
|
+
_parse(content) {
|
|
480
|
+
const vars = {};
|
|
481
|
+
const comments = [];
|
|
482
|
+
const lines = content.split('\n');
|
|
483
|
+
|
|
484
|
+
for (const line of lines) {
|
|
485
|
+
const trimmed = line.trim();
|
|
486
|
+
|
|
487
|
+
// Capture comments
|
|
488
|
+
if (trimmed.startsWith('#')) {
|
|
489
|
+
comments.push(trimmed.substring(1).trim());
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Skip empty lines
|
|
494
|
+
if (trimmed === '') continue;
|
|
495
|
+
|
|
496
|
+
// Parse key=value
|
|
497
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
498
|
+
if (match) {
|
|
499
|
+
const key = match[1].trim();
|
|
500
|
+
let value = match[2].trim();
|
|
501
|
+
|
|
502
|
+
// Remove inline comments
|
|
503
|
+
const commentIndex = value.indexOf('#');
|
|
504
|
+
if (commentIndex > 0) {
|
|
505
|
+
value = value.substring(0, commentIndex).trim();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Remove quotes if present
|
|
509
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
510
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
511
|
+
value = value.slice(1, -1);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
vars[key] = value;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return { vars, comments };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Format value for .env file (add quotes if needed)
|
|
523
|
+
*/
|
|
524
|
+
_formatValue(value) {
|
|
525
|
+
const stringValue = String(value);
|
|
526
|
+
|
|
527
|
+
// Add quotes if value contains spaces or special characters
|
|
528
|
+
if (stringValue.includes(' ') ||
|
|
529
|
+
stringValue.includes('#') ||
|
|
530
|
+
stringValue.includes('$') ||
|
|
531
|
+
stringValue.includes('\\')) {
|
|
532
|
+
return `"${stringValue.replace(/"/g, '\\"')}"`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return stringValue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Validate key name
|
|
540
|
+
*/
|
|
541
|
+
_validateKey(key) {
|
|
542
|
+
if (!key || typeof key !== 'string') {
|
|
543
|
+
throw new Error('Key must be a non-empty string');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) {
|
|
547
|
+
throw new Error('Key must contain only letters, numbers, and underscores');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Validate value
|
|
553
|
+
*/
|
|
554
|
+
_validateValue(value) {
|
|
555
|
+
if (value === undefined || value === null) {
|
|
556
|
+
throw new Error('Value cannot be undefined or null');
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Create backup of current .env file
|
|
562
|
+
*/
|
|
563
|
+
async _createBackup(suffix = '') {
|
|
564
|
+
try {
|
|
565
|
+
if (!existsSync(this.envPath)) {
|
|
566
|
+
return null; // Nothing to backup
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
570
|
+
const backupName = suffix
|
|
571
|
+
? `.env.backup-${timestamp}-${suffix}.bak`
|
|
572
|
+
: `.env.backup-${timestamp}.bak`;
|
|
573
|
+
const backupPath = path.join(this.backupDir, backupName);
|
|
574
|
+
|
|
575
|
+
const content = await fs.readFile(this.envPath, 'utf8');
|
|
576
|
+
await fs.writeFile(backupPath, content, { mode: SECURE_PERMISSIONS });
|
|
577
|
+
|
|
578
|
+
// Clean old backups (keep last 10)
|
|
579
|
+
await this.cleanBackups(10);
|
|
580
|
+
|
|
581
|
+
return backupPath;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
console.warn(`Warning: Failed to create backup: ${error.message}`);
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Write file atomically (write to temp, then rename)
|
|
590
|
+
*/
|
|
591
|
+
async _writeAtomic(content) {
|
|
592
|
+
const tempPath = `${this.envPath}.tmp.${Date.now()}`;
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
// Write to temp file
|
|
596
|
+
await fs.writeFile(tempPath, content, { mode: SECURE_PERMISSIONS });
|
|
597
|
+
|
|
598
|
+
// Atomic rename
|
|
599
|
+
await fs.rename(tempPath, this.envPath);
|
|
600
|
+
|
|
601
|
+
// Ensure permissions
|
|
602
|
+
await fs.chmod(this.envPath, SECURE_PERMISSIONS);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
// Cleanup temp file on error
|
|
605
|
+
try {
|
|
606
|
+
await fs.unlink(tempPath);
|
|
607
|
+
} catch {}
|
|
608
|
+
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Validate and fix file permissions
|
|
615
|
+
*/
|
|
616
|
+
async _validatePermissions() {
|
|
617
|
+
try {
|
|
618
|
+
if (!existsSync(this.envPath)) {
|
|
619
|
+
return { valid: true, permissions: null };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const stats = await fs.stat(this.envPath);
|
|
623
|
+
const mode = stats.mode & 0o777;
|
|
624
|
+
const expected = SECURE_PERMISSIONS;
|
|
625
|
+
|
|
626
|
+
if (mode !== expected) {
|
|
627
|
+
console.warn(`Warning: .env has insecure permissions (${mode.toString(8)}), fixing to 600`);
|
|
628
|
+
await fs.chmod(this.envPath, expected);
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
valid: false,
|
|
632
|
+
fixed: true,
|
|
633
|
+
oldPermissions: mode.toString(8),
|
|
634
|
+
newPermissions: expected.toString(8)
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
valid: true,
|
|
640
|
+
permissions: mode.toString(8)
|
|
641
|
+
};
|
|
642
|
+
} catch (error) {
|
|
643
|
+
throw new Error(`Permission validation failed: ${error.message}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Clean up multiple consecutive blank lines
|
|
649
|
+
*/
|
|
650
|
+
_cleanBlankLines(lines) {
|
|
651
|
+
const cleaned = [];
|
|
652
|
+
let prevBlank = false;
|
|
653
|
+
|
|
654
|
+
for (const line of lines) {
|
|
655
|
+
const isBlank = line.trim() === '';
|
|
656
|
+
|
|
657
|
+
if (isBlank && prevBlank) {
|
|
658
|
+
continue; // Skip consecutive blank lines
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
cleaned.push(line);
|
|
662
|
+
prevBlank = isBlank;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Remove trailing blank lines
|
|
666
|
+
while (cleaned.length > 0 && cleaned[cleaned.length - 1].trim() === '') {
|
|
667
|
+
cleaned.pop();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return cleaned;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ============================================================================
|
|
675
|
+
// CLI INTERFACE
|
|
676
|
+
// ============================================================================
|
|
677
|
+
|
|
678
|
+
export async function runCLI() {
|
|
679
|
+
const args = process.argv.slice(2);
|
|
680
|
+
const command = args[0];
|
|
681
|
+
const manager = new EnvManager();
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
await manager.init();
|
|
685
|
+
|
|
686
|
+
switch (command) {
|
|
687
|
+
case 'get':
|
|
688
|
+
if (!args[1]) {
|
|
689
|
+
console.error('Usage: env-manager.js get <key>');
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
const getResult = await manager.get(args[1]);
|
|
693
|
+
if (getResult.success) {
|
|
694
|
+
console.log(getResult.value);
|
|
695
|
+
} else {
|
|
696
|
+
console.error(`Error: ${getResult.error}`);
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
break;
|
|
700
|
+
|
|
701
|
+
case 'set':
|
|
702
|
+
if (!args[1] || !args[2]) {
|
|
703
|
+
console.error('Usage: env-manager.js set <key> <value> [comment]');
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
const setResult = await manager.set(args[1], args[2], {
|
|
707
|
+
comment: args[3]
|
|
708
|
+
});
|
|
709
|
+
console.log(`✓ ${setResult.action} ${setResult.key}`);
|
|
710
|
+
break;
|
|
711
|
+
|
|
712
|
+
case 'remove':
|
|
713
|
+
case 'rm':
|
|
714
|
+
if (!args[1]) {
|
|
715
|
+
console.error('Usage: env-manager.js remove <key>');
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
const rmResult = await manager.remove(args[1]);
|
|
719
|
+
if (rmResult.success) {
|
|
720
|
+
console.log(`✓ Removed ${rmResult.key}`);
|
|
721
|
+
} else {
|
|
722
|
+
console.error(`Error: ${rmResult.error}`);
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
break;
|
|
726
|
+
|
|
727
|
+
case 'list':
|
|
728
|
+
case 'ls':
|
|
729
|
+
const listResult = await manager.list();
|
|
730
|
+
console.log(`Environment variables (${listResult.count}):`);
|
|
731
|
+
for (const [key, value] of Object.entries(listResult.vars)) {
|
|
732
|
+
const displayValue = value.length > 50
|
|
733
|
+
? value.substring(0, 47) + '...'
|
|
734
|
+
: value;
|
|
735
|
+
console.log(` ${key}=${displayValue}`);
|
|
736
|
+
}
|
|
737
|
+
break;
|
|
738
|
+
|
|
739
|
+
case 'backups':
|
|
740
|
+
const backups = await manager.listBackups();
|
|
741
|
+
console.log(`Backups (${backups.length}):`);
|
|
742
|
+
backups.forEach((b, i) => {
|
|
743
|
+
console.log(` ${i + 1}. ${b.name} (${b.created.toLocaleString()})`);
|
|
744
|
+
});
|
|
745
|
+
break;
|
|
746
|
+
|
|
747
|
+
case 'restore':
|
|
748
|
+
const restoreResult = await manager.restore(args[1] || 'latest');
|
|
749
|
+
console.log(`✓ Restored from backup`);
|
|
750
|
+
break;
|
|
751
|
+
|
|
752
|
+
case 'validate':
|
|
753
|
+
const validation = await manager.validatePermissions();
|
|
754
|
+
if (validation.valid) {
|
|
755
|
+
console.log(`✓ Permissions are secure (${validation.permissions})`);
|
|
756
|
+
} else if (validation.fixed) {
|
|
757
|
+
console.log(`✓ Fixed permissions: ${validation.oldPermissions} → ${validation.newPermissions}`);
|
|
758
|
+
}
|
|
759
|
+
break;
|
|
760
|
+
|
|
761
|
+
default:
|
|
762
|
+
console.log(`Boss Claude Environment Manager
|
|
763
|
+
|
|
764
|
+
Usage:
|
|
765
|
+
env-manager.js get <key> Get a variable
|
|
766
|
+
env-manager.js set <key> <value> Set a variable
|
|
767
|
+
env-manager.js remove <key> Remove a variable
|
|
768
|
+
env-manager.js list List all variables
|
|
769
|
+
env-manager.js backups List backups
|
|
770
|
+
env-manager.js restore [name] Restore from backup
|
|
771
|
+
env-manager.js validate Check permissions
|
|
772
|
+
|
|
773
|
+
Location: ${manager.envPath}
|
|
774
|
+
`);
|
|
775
|
+
}
|
|
776
|
+
} catch (error) {
|
|
777
|
+
console.error(`Error: ${error.message}`);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Run CLI if executed directly
|
|
783
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
784
|
+
runCLI();
|
|
785
|
+
}
|