@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.
Files changed (87) hide show
  1. package/README.md +304 -1
  2. package/bin/boss-claude.js +1138 -0
  3. package/bin/commands/mode.js +250 -0
  4. package/bin/onyx-guard.js +259 -0
  5. package/bin/onyx-guard.sh +251 -0
  6. package/bin/prompts.js +284 -0
  7. package/bin/rollback.js +85 -0
  8. package/bin/setup-wizard.js +492 -0
  9. package/config/.env.example +17 -0
  10. package/lib/README.md +83 -0
  11. package/lib/agent-logger.js +61 -0
  12. package/lib/agents/memory-engineers/github-memory-engineer.js +251 -0
  13. package/lib/agents/memory-engineers/postgres-memory-engineer.js +633 -0
  14. package/lib/agents/memory-engineers/qdrant-memory-engineer.js +358 -0
  15. package/lib/agents/memory-engineers/redis-memory-engineer.js +383 -0
  16. package/lib/agents/memory-supervisor.js +526 -0
  17. package/lib/agents/registry.js +135 -0
  18. package/lib/auto-monitor.js +131 -0
  19. package/lib/checkpoint-hook.js +112 -0
  20. package/lib/checkpoint.js +319 -0
  21. package/lib/commentator.js +213 -0
  22. package/lib/context-scribe.js +120 -0
  23. package/lib/delegation-strategies.js +326 -0
  24. package/lib/hierarchy-validator.js +643 -0
  25. package/lib/index.js +15 -0
  26. package/lib/init-with-mode.js +261 -0
  27. package/lib/init.js +44 -6
  28. package/lib/memory-result-aggregator.js +252 -0
  29. package/lib/memory.js +35 -7
  30. package/lib/mode-enforcer.js +473 -0
  31. package/lib/onyx-banner.js +169 -0
  32. package/lib/onyx-identity.js +214 -0
  33. package/lib/onyx-monitor.js +381 -0
  34. package/lib/onyx-reminder.js +188 -0
  35. package/lib/onyx-tool-interceptor.js +341 -0
  36. package/lib/onyx-wrapper.js +315 -0
  37. package/lib/orchestrator-gate.js +334 -0
  38. package/lib/output-formatter.js +296 -0
  39. package/lib/postgres.js +1 -1
  40. package/lib/prompt-injector.js +220 -0
  41. package/lib/prompts.js +532 -0
  42. package/lib/session.js +153 -6
  43. package/lib/setup/README.md +187 -0
  44. package/lib/setup/env-manager.js +785 -0
  45. package/lib/setup/error-recovery.js +630 -0
  46. package/lib/setup/explain-scopes.js +385 -0
  47. package/lib/setup/github-instructions.js +333 -0
  48. package/lib/setup/github-repo.js +254 -0
  49. package/lib/setup/import-credentials.js +498 -0
  50. package/lib/setup/index.js +62 -0
  51. package/lib/setup/init-postgres.js +785 -0
  52. package/lib/setup/init-redis.js +456 -0
  53. package/lib/setup/integration-test.js +652 -0
  54. package/lib/setup/progress.js +357 -0
  55. package/lib/setup/rollback.js +670 -0
  56. package/lib/setup/rollback.test.js +452 -0
  57. package/lib/setup/setup-with-rollback.example.js +351 -0
  58. package/lib/setup/summary.js +400 -0
  59. package/lib/setup/test-github-setup.js +10 -0
  60. package/lib/setup/test-postgres-init.js +98 -0
  61. package/lib/setup/verify-setup.js +102 -0
  62. package/lib/task-agent-worker.js +235 -0
  63. package/lib/token-monitor.js +466 -0
  64. package/lib/tool-wrapper-integration.js +369 -0
  65. package/lib/tool-wrapper.js +387 -0
  66. package/lib/validators/README.md +497 -0
  67. package/lib/validators/config.js +583 -0
  68. package/lib/validators/config.test.js +175 -0
  69. package/lib/validators/github.js +310 -0
  70. package/lib/validators/github.test.js +61 -0
  71. package/lib/validators/index.js +15 -0
  72. package/lib/validators/postgres.js +525 -0
  73. package/package.json +98 -13
  74. package/scripts/benchmark-memory.js +433 -0
  75. package/scripts/check-secrets.sh +12 -0
  76. package/scripts/fetch-todos.mjs +148 -0
  77. package/scripts/graceful-shutdown.sh +156 -0
  78. package/scripts/install-onyx-hooks.js +373 -0
  79. package/scripts/install.js +119 -18
  80. package/scripts/redis-monitor.js +284 -0
  81. package/scripts/redis-setup.js +412 -0
  82. package/scripts/test-memory-retrieval.js +201 -0
  83. package/scripts/validate-exports.js +68 -0
  84. package/scripts/validate-package.js +120 -0
  85. package/scripts/verify-onyx-deployment.js +309 -0
  86. package/scripts/verify-redis-deployment.js +354 -0
  87. 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
+ }