lsh-framework 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +40 -3
  2. package/dist/cli.js +104 -486
  3. package/dist/commands/doctor.js +427 -0
  4. package/dist/commands/init.js +371 -0
  5. package/dist/constants/api.js +94 -0
  6. package/dist/constants/commands.js +64 -0
  7. package/dist/constants/config.js +56 -0
  8. package/dist/constants/database.js +21 -0
  9. package/dist/constants/errors.js +79 -0
  10. package/dist/constants/index.js +28 -0
  11. package/dist/constants/paths.js +28 -0
  12. package/dist/constants/ui.js +73 -0
  13. package/dist/constants/validation.js +124 -0
  14. package/dist/daemon/lshd.js +11 -32
  15. package/dist/lib/daemon-client-helper.js +7 -4
  16. package/dist/lib/daemon-client.js +9 -2
  17. package/dist/lib/format-utils.js +163 -0
  18. package/dist/lib/fuzzy-match.js +123 -0
  19. package/dist/lib/job-manager.js +2 -1
  20. package/dist/lib/platform-utils.js +211 -0
  21. package/dist/lib/secrets-manager.js +11 -1
  22. package/dist/lib/string-utils.js +128 -0
  23. package/dist/services/daemon/daemon-registrar.js +3 -2
  24. package/dist/services/secrets/secrets.js +119 -59
  25. package/package.json +10 -74
  26. package/dist/app.js +0 -33
  27. package/dist/cicd/analytics.js +0 -261
  28. package/dist/cicd/auth.js +0 -269
  29. package/dist/cicd/cache-manager.js +0 -172
  30. package/dist/cicd/data-retention.js +0 -305
  31. package/dist/cicd/performance-monitor.js +0 -224
  32. package/dist/cicd/webhook-receiver.js +0 -640
  33. package/dist/commands/api.js +0 -346
  34. package/dist/commands/theme.js +0 -261
  35. package/dist/commands/zsh-import.js +0 -240
  36. package/dist/components/App.js +0 -1
  37. package/dist/components/Divider.js +0 -29
  38. package/dist/components/REPL.js +0 -43
  39. package/dist/components/Terminal.js +0 -232
  40. package/dist/components/UserInput.js +0 -30
  41. package/dist/daemon/api-server.js +0 -316
  42. package/dist/daemon/monitoring-api.js +0 -220
  43. package/dist/lib/api-error-handler.js +0 -185
  44. package/dist/lib/associative-arrays.js +0 -285
  45. package/dist/lib/base-api-server.js +0 -290
  46. package/dist/lib/brace-expansion.js +0 -160
  47. package/dist/lib/builtin-commands.js +0 -439
  48. package/dist/lib/executors/builtin-executor.js +0 -52
  49. package/dist/lib/extended-globbing.js +0 -411
  50. package/dist/lib/extended-parameter-expansion.js +0 -227
  51. package/dist/lib/interactive-shell.js +0 -460
  52. package/dist/lib/job-builtins.js +0 -582
  53. package/dist/lib/pathname-expansion.js +0 -216
  54. package/dist/lib/script-runner.js +0 -226
  55. package/dist/lib/shell-executor.js +0 -2504
  56. package/dist/lib/shell-parser.js +0 -958
  57. package/dist/lib/shell-types.js +0 -6
  58. package/dist/lib/shell.lib.js +0 -40
  59. package/dist/lib/theme-manager.js +0 -476
  60. package/dist/lib/variable-expansion.js +0 -385
  61. package/dist/lib/zsh-compatibility.js +0 -659
  62. package/dist/lib/zsh-import-manager.js +0 -707
  63. package/dist/lib/zsh-options.js +0 -328
  64. package/dist/pipeline/job-tracker.js +0 -491
  65. package/dist/pipeline/mcli-bridge.js +0 -309
  66. package/dist/pipeline/pipeline-service.js +0 -1119
  67. package/dist/pipeline/workflow-engine.js +0 -870
  68. package/dist/services/api/api.js +0 -58
  69. package/dist/services/api/auth.js +0 -35
  70. package/dist/services/api/config.js +0 -7
  71. package/dist/services/api/file.js +0 -22
  72. package/dist/services/shell/shell.js +0 -28
  73. package/dist/services/zapier.js +0 -16
  74. package/dist/simple-api-server.js +0 -148
@@ -0,0 +1,28 @@
1
+ /**
2
+ * LSH Constants
3
+ *
4
+ * Centralized location for all hard-coded strings, configuration values,
5
+ * and magic constants used throughout the LSH codebase.
6
+ *
7
+ * This file serves as the single source of truth for all constant values.
8
+ * Import from this file or its submodules instead of using hard-coded strings.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { PATHS, ERRORS, ENV_VARS } from './constants/index.js';
13
+ *
14
+ * // Use constants instead of hard-coded strings
15
+ * const socketPath = PATHS.DAEMON_SOCKET_TEMPLATE.replace('${USER}', process.env.USER);
16
+ * throw new Error(ERRORS.DAEMON_ALREADY_RUNNING);
17
+ * const apiKey = process.env[ENV_VARS.LSH_API_KEY];
18
+ * ```
19
+ */
20
+ // Export all constants
21
+ export * from './paths.js';
22
+ export * from './errors.js';
23
+ export * from './commands.js';
24
+ export * from './config.js';
25
+ export * from './api.js';
26
+ export * from './ui.js';
27
+ export * from './validation.js';
28
+ export * from './database.js';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * File paths and system locations
3
+ *
4
+ * All file paths, directories, and system locations used throughout LSH.
5
+ */
6
+ export const PATHS = {
7
+ // Package and config files
8
+ PACKAGE_JSON_RELATIVE: '../package.json',
9
+ LSHRC_FILENAME: '.lshrc',
10
+ ROOT_DIR: '/',
11
+ // History and session files
12
+ DEFAULT_HISTORY_FILE: '~/.lsh_history',
13
+ // Daemon files (templates with ${USER} placeholder)
14
+ DAEMON_SOCKET_TEMPLATE: '/tmp/lsh-job-daemon-${USER}.sock',
15
+ DAEMON_PID_FILE_TEMPLATE: '/tmp/lsh-job-daemon-${USER}.pid',
16
+ DAEMON_LOG_FILE_TEMPLATE: '/tmp/lsh-job-daemon-${USER}.log',
17
+ DAEMON_JOBS_FILE_TEMPLATE: '/tmp/lsh-daemon-jobs-${USER}.json',
18
+ // Job persistence
19
+ DEFAULT_JOBS_PERSISTENCE_FILE: '/tmp/lsh-jobs.json',
20
+ };
21
+ export const PREFIXES = {
22
+ SESSION_ID: 'lsh_',
23
+ SECRETS_SEED_SUFFIX: 'lsh-secrets',
24
+ };
25
+ export const SYSTEM = {
26
+ UNKNOWN_USER: 'unknown',
27
+ DEFAULT_HOSTNAME: 'localhost',
28
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * UI strings and console output
3
+ *
4
+ * All user-facing messages, prompts, and terminal output strings.
5
+ */
6
+ export const UI_MESSAGES = {
7
+ // Help and usage
8
+ DID_YOU_MEAN: '\nDid you mean one of these?',
9
+ RUN_HELP_MESSAGE: '\nRun \'lsh --help\' to see available commands.',
10
+ // Configuration messages
11
+ CONFIG_EXISTS: 'Configuration file already exists: ${rcFile}',
12
+ CONFIG_CREATED: '✅ Created configuration file: ${rcFile}',
13
+ CONFIG_CREATE_FAILED: '❌ Failed to create configuration: ${message}',
14
+ CONFIG_NOT_FOUND: '❌ Configuration file not found: ${rcFile}',
15
+ CONFIG_INIT_HINT: 'Run "lsh config --init" to create one.',
16
+ CONFIG_FILE_DISPLAY: '📄 Configuration file: ${rcFile}',
17
+ CONFIG_NOT_FOUND_VALIDATE: '❌ Configuration file not found: ${rcFile}',
18
+ CONFIG_VALID: '✅ Configuration file is valid: ${rcFile}',
19
+ CONFIG_HAS_ERRORS: '❌ Configuration file has errors: ${rcFile}',
20
+ // Secrets messages
21
+ FAILED_PUSH_SECRETS: '❌ Failed to push secrets:',
22
+ FAILED_PULL_SECRETS: '❌ Failed to pull secrets:',
23
+ FILE_NOT_FOUND: '❌ File not found: ${envPath}',
24
+ TIP_PULL_FROM_CLOUD: '💡 Tip: Pull from cloud with: lsh pull --env <environment>',
25
+ SECRETS_IN_FILE: '\n📋 Secrets in ${file}:\n',
26
+ TOTAL_SECRETS: '\n Total: ${count} secrets\n',
27
+ FAILED_LIST_SECRETS: '❌ Failed to list secrets:',
28
+ // Version messages
29
+ CURRENT_VERSION: 'Current version:',
30
+ CHECKING_UPDATES: 'Checking npm for updates...',
31
+ FAILED_FETCH_VERSION: '✗ Failed to fetch version information from npm',
32
+ CHECK_INTERNET: '⚠ Make sure you have internet connectivity',
33
+ LATEST_VERSION: 'Latest version:',
34
+ ALREADY_LATEST: '✓ You\'re already on the latest version!',
35
+ VERSION_NEWER: '✓ Your version (${currentVersion}) is newer than npm',
36
+ DEV_VERSION_HINT: 'You may be using a development version',
37
+ UPDATE_AVAILABLE: '⬆ Update available: ${currentVersion} → ${latestVersion}',
38
+ RUN_UPDATE_HINT: 'ℹ Run \'lsh self update\' to install the update',
39
+ };
40
+ export const LOG_MESSAGES = {
41
+ // Environment validation
42
+ VALIDATING_ENV: 'Validating environment configuration',
43
+ ENV_VALIDATION_FAILED: 'Environment validation failed in production',
44
+ // Daemon lifecycle
45
+ DAEMON_STARTING: 'Starting LSH Job Daemon',
46
+ DAEMON_STARTED: 'Daemon started with PID ${pid}',
47
+ DAEMON_STOPPING: 'Stopping LSH Job Daemon',
48
+ DAEMON_STOPPED: 'Daemon stopped',
49
+ // API server
50
+ API_SERVER_STARTED: 'API Server started on port ${port}',
51
+ API_SERVER_STOPPED: 'API Server stopped',
52
+ // Job operations
53
+ ADDING_JOB: 'Adding job: ${name}',
54
+ STARTING_JOB: 'Starting job: ${jobId}',
55
+ TRIGGERING_JOB: 'Triggering job: ${jobId}',
56
+ // Scheduler
57
+ SCHEDULER_STARTING: '📅 Starting job scheduler...',
58
+ SCHEDULER_STARTED: '✅ Job scheduler started successfully',
59
+ // Secrets operations
60
+ WARN_NO_SECRETS_KEY: '⚠️ Warning: No LSH_SECRETS_KEY set. Using machine-derived key.',
61
+ WARN_GENERATE_KEY_MESSAGE: 'To share secrets across machines, generate a key with: lsh secrets key',
62
+ PUSHING_SECRETS: 'Pushing ${envFilePath} to Supabase (${environment})...',
63
+ SECRETS_PUSHED: '✅ Pushed ${count} secrets from ${filename} to Supabase',
64
+ PULLING_SECRETS: 'Pulling ${filename} (${environment}) from Supabase...',
65
+ BACKUP_CREATED: 'Backed up existing .env to ${backup}',
66
+ SECRETS_PULLED: '✅ Pulled ${count} secrets from Supabase',
67
+ };
68
+ export const LOG_LEVELS = {
69
+ INFO: 'INFO',
70
+ WARN: 'WARN',
71
+ ERROR: 'ERROR',
72
+ DEBUG: 'DEBUG',
73
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Validation patterns and security rules
3
+ *
4
+ * All validation patterns, security rules, and dangerous command patterns.
5
+ */
6
+ import { ERRORS, RISK_LEVELS } from './errors.js';
7
+ export const DANGEROUS_PATTERNS = [
8
+ // Critical risk - Filesystem destruction
9
+ {
10
+ pattern: /rm\s+-rf\s+\/(?!\w)/i,
11
+ description: ERRORS.DELETE_ROOT,
12
+ riskLevel: RISK_LEVELS.CRITICAL,
13
+ },
14
+ {
15
+ pattern: /mkfs/i,
16
+ description: ERRORS.MKFS_DETECTED,
17
+ riskLevel: RISK_LEVELS.CRITICAL,
18
+ },
19
+ {
20
+ pattern: /dd\s+.*of=/i,
21
+ description: ERRORS.DD_DETECTED,
22
+ riskLevel: RISK_LEVELS.CRITICAL,
23
+ },
24
+ // High risk - Privilege escalation
25
+ {
26
+ pattern: /sudo\s+su/i,
27
+ description: ERRORS.PRIV_ESCALATION,
28
+ riskLevel: RISK_LEVELS.HIGH,
29
+ },
30
+ {
31
+ pattern: /sudo\s+.*passwd/i,
32
+ description: ERRORS.PASSWORD_MOD,
33
+ riskLevel: RISK_LEVELS.HIGH,
34
+ },
35
+ // High risk - Remote code execution
36
+ {
37
+ pattern: /curl\s+.*\|\s*bash/i,
38
+ description: ERRORS.REMOTE_EXEC_CURL,
39
+ riskLevel: RISK_LEVELS.HIGH,
40
+ },
41
+ {
42
+ pattern: /wget\s+.*\|\s*sh/i,
43
+ description: ERRORS.REMOTE_EXEC_WGET,
44
+ riskLevel: RISK_LEVELS.HIGH,
45
+ },
46
+ {
47
+ pattern: /nc\s+.*-e/i,
48
+ description: ERRORS.REVERSE_SHELL,
49
+ riskLevel: RISK_LEVELS.HIGH,
50
+ },
51
+ // High risk - Sensitive file access
52
+ {
53
+ pattern: /cat\s+\/etc\/shadow/i,
54
+ description: ERRORS.READ_SHADOW,
55
+ riskLevel: RISK_LEVELS.HIGH,
56
+ },
57
+ {
58
+ pattern: /cat\s+\/etc\/passwd/i,
59
+ description: ERRORS.READ_PASSWD,
60
+ riskLevel: RISK_LEVELS.MEDIUM,
61
+ },
62
+ {
63
+ pattern: /\.ssh\/id_rsa/i,
64
+ description: ERRORS.ACCESS_SSH_KEY,
65
+ riskLevel: RISK_LEVELS.HIGH,
66
+ },
67
+ // High risk - Process killing
68
+ {
69
+ pattern: /kill\s+-9\s+1\b/i,
70
+ description: ERRORS.KILL_INIT,
71
+ riskLevel: RISK_LEVELS.CRITICAL,
72
+ },
73
+ {
74
+ pattern: /pkill\s+-9\s+.*sshd/i,
75
+ description: ERRORS.KILL_SSHD,
76
+ riskLevel: RISK_LEVELS.HIGH,
77
+ },
78
+ // Medium risk - Obfuscation attempts
79
+ {
80
+ pattern: /\$\(.*base64.*\)/i,
81
+ description: ERRORS.BASE64_COMMAND,
82
+ riskLevel: RISK_LEVELS.MEDIUM,
83
+ },
84
+ {
85
+ pattern: /eval.*\$\(/i,
86
+ description: ERRORS.DYNAMIC_EVAL,
87
+ riskLevel: RISK_LEVELS.MEDIUM,
88
+ },
89
+ {
90
+ pattern: /\x00/i,
91
+ description: ERRORS.NULL_BYTE,
92
+ riskLevel: RISK_LEVELS.HIGH,
93
+ },
94
+ ];
95
+ export const WARNING_PATTERNS = [
96
+ {
97
+ pattern: /sudo\s+/i,
98
+ description: 'Command uses sudo (elevated privileges)',
99
+ },
100
+ {
101
+ pattern: /rm\s+-rf/i,
102
+ description: 'Force recursive deletion - use with caution',
103
+ },
104
+ {
105
+ pattern: />\s*\/dev\/(sd[a-z]|hd[a-z]|nvme[0-9])/i,
106
+ description: 'Direct disk device access',
107
+ },
108
+ {
109
+ pattern: /chmod\s+777/i,
110
+ description: 'Setting world-writable permissions',
111
+ },
112
+ {
113
+ pattern: /curl.*\|/i,
114
+ description: 'Piping curl output to another command',
115
+ },
116
+ {
117
+ pattern: /wget.*\|/i,
118
+ description: 'Piping wget output to another command',
119
+ },
120
+ {
121
+ pattern: /exec\s+/i,
122
+ description: 'Using exec to replace current process',
123
+ },
124
+ ];
@@ -10,10 +10,10 @@ import * as path from 'path';
10
10
  import * as net from 'net';
11
11
  import { EventEmitter } from 'events';
12
12
  import JobManager from '../lib/job-manager.js';
13
- import { LSHApiServer } from './api-server.js';
14
13
  import { validateCommand } from '../lib/command-validator.js';
15
14
  import { validateEnvironment, printValidationResults } from '../lib/env-validator.js';
16
15
  import { createLogger } from '../lib/logger.js';
16
+ import { getPlatformPaths } from '../lib/platform-utils.js';
17
17
  const execAsync = promisify(exec);
18
18
  export class LSHJobDaemon extends EventEmitter {
19
19
  config;
@@ -21,18 +21,19 @@ export class LSHJobDaemon extends EventEmitter {
21
21
  isRunning = false;
22
22
  checkTimer;
23
23
  logStream;
24
- ipcServer; // Unix socket server for communication
24
+ ipcServer; // IPC server (Unix sockets or Named Pipes)
25
25
  lastRunTimes = new Map(); // Track last run time per job
26
- apiServer; // API server instance
27
26
  logger = createLogger('LSHJobDaemon');
28
27
  constructor(config) {
29
28
  super();
30
- const userSuffix = process.env.USER ? `-${process.env.USER}` : '';
29
+ // Use cross-platform paths
30
+ const platformPaths = getPlatformPaths('lsh');
31
+ const jobsFilePath = path.join(platformPaths.tmpDir, `lsh-daemon-jobs-${platformPaths.user}.json`);
31
32
  this.config = {
32
- pidFile: `/tmp/lsh-job-daemon${userSuffix}.pid`,
33
- logFile: `/tmp/lsh-job-daemon${userSuffix}.log`,
34
- jobsFile: `/tmp/lsh-daemon-jobs${userSuffix}.json`,
35
- socketPath: `/tmp/lsh-job-daemon${userSuffix}.sock`,
33
+ pidFile: platformPaths.pidFile,
34
+ logFile: platformPaths.logFile,
35
+ jobsFile: jobsFilePath,
36
+ socketPath: platformPaths.socketPath,
36
37
  checkInterval: 2000, // 2 seconds for better cron accuracy
37
38
  maxLogSize: 10 * 1024 * 1024, // 10MB
38
39
  autoRestart: true,
@@ -74,28 +75,11 @@ export class LSHJobDaemon extends EventEmitter {
74
75
  throw new Error('Another daemon instance is already running');
75
76
  }
76
77
  this.log('INFO', 'Starting LSH Job Daemon');
77
- // Write PID file
78
- await fs.promises.writeFile(this.config.pidFile, process.pid.toString());
78
+ // Write PID file with secure permissions (mode 0o600 = rw-------)
79
+ await fs.promises.writeFile(this.config.pidFile, process.pid.toString(), { mode: 0o600 });
79
80
  this.isRunning = true;
80
81
  this.startJobScheduler();
81
82
  this.startIPCServer();
82
- // Start API server if enabled
83
- if (this.config.apiEnabled) {
84
- try {
85
- this.apiServer = new LSHApiServer(this, {
86
- port: this.config.apiPort,
87
- apiKey: this.config.apiKey,
88
- enableWebhooks: this.config.enableWebhooks,
89
- webhookEndpoints: this.config.webhookEndpoints
90
- });
91
- await this.apiServer.start();
92
- this.log('INFO', `API Server started on port ${this.config.apiPort}`);
93
- }
94
- catch (error) {
95
- const err = error;
96
- this.log('ERROR', `Failed to start API server: ${err.message}`);
97
- }
98
- }
99
83
  // Setup cleanup handlers
100
84
  this.setupSignalHandlers();
101
85
  this.log('INFO', `Daemon started with PID ${process.pid}`);
@@ -110,11 +94,6 @@ export class LSHJobDaemon extends EventEmitter {
110
94
  }
111
95
  this.log('INFO', 'Stopping LSH Job Daemon');
112
96
  this.isRunning = false;
113
- // Stop API server if running
114
- if (this.apiServer) {
115
- await this.apiServer.stop();
116
- this.log('INFO', 'API Server stopped');
117
- }
118
97
  if (this.checkTimer) {
119
98
  clearInterval(this.checkTimer);
120
99
  }
@@ -3,17 +3,20 @@
3
3
  * Provides wrapper utilities to eliminate repetitive daemon client connection boilerplate
4
4
  */
5
5
  import DaemonClient from './daemon-client.js';
6
+ import { getPlatformPaths } from './platform-utils.js';
6
7
  /**
7
- * Default socket path for the daemon
8
+ * Default socket path for the daemon (cross-platform)
8
9
  */
9
10
  export function getDefaultSocketPath() {
10
- return `/tmp/lsh-job-daemon-${process.env.USER || 'user'}.sock`;
11
+ const platformPaths = getPlatformPaths('lsh');
12
+ return platformPaths.socketPath;
11
13
  }
12
14
  /**
13
- * Get default user ID
15
+ * Get default user ID (cross-platform)
14
16
  */
15
17
  export function getDefaultUserId() {
16
- return process.env.USER || 'user';
18
+ const platformPaths = getPlatformPaths('lsh');
19
+ return platformPaths.user;
17
20
  }
18
21
  /**
19
22
  * Execute an operation with a daemon client, handling all connection boilerplate
@@ -7,6 +7,7 @@ import * as fs from 'fs';
7
7
  import { EventEmitter } from 'events';
8
8
  import DatabasePersistence from './database-persistence.js';
9
9
  import { createLogger } from './logger.js';
10
+ import { getPlatformPaths } from './platform-utils.js';
10
11
  export class DaemonClient extends EventEmitter {
11
12
  socketPath;
12
13
  socket;
@@ -19,8 +20,14 @@ export class DaemonClient extends EventEmitter {
19
20
  logger = createLogger('DaemonClient');
20
21
  constructor(socketPath, userId) {
21
22
  super();
22
- // Use user-specific socket path if not provided
23
- this.socketPath = socketPath || `/tmp/lsh-job-daemon-${process.env.USER || 'default'}.sock`;
23
+ // Use cross-platform socket path if not provided
24
+ if (!socketPath) {
25
+ const platformPaths = getPlatformPaths('lsh');
26
+ this.socketPath = platformPaths.socketPath;
27
+ }
28
+ else {
29
+ this.socketPath = socketPath;
30
+ }
24
31
  this.userId = userId;
25
32
  this.sessionId = `lsh_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
26
33
  if (userId) {
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Format Utilities for Secret Export
3
+ * Supports multiple output formats: env, json, yaml, toml, export
4
+ */
5
+ import yaml from 'js-yaml';
6
+ /**
7
+ * Mask a secret value showing only first 3 and last 3 characters
8
+ */
9
+ export function maskSecret(value) {
10
+ if (value.length <= 6) {
11
+ return '***';
12
+ }
13
+ return `${value.slice(0, 3)}***${value.slice(-3)}`;
14
+ }
15
+ /**
16
+ * Apply masking to secrets array
17
+ */
18
+ export function maskSecrets(secrets) {
19
+ return secrets.map(({ key, value }) => ({
20
+ key,
21
+ value: maskSecret(value),
22
+ }));
23
+ }
24
+ /**
25
+ * Detect namespaces from key prefixes for TOML grouping
26
+ * E.g., DATABASE_URL, DATABASE_PORT -> namespace: "database"
27
+ */
28
+ export function detectNamespaces(secrets) {
29
+ const namespaces = new Map();
30
+ const ungrouped = [];
31
+ // Common prefixes to detect
32
+ const prefixPattern = /^([A-Z][A-Z0-9]*?)_(.+)$/;
33
+ for (const secret of secrets) {
34
+ const match = secret.key.match(prefixPattern);
35
+ if (match) {
36
+ const [, prefix, remainder] = match;
37
+ const namespace = prefix.toLowerCase();
38
+ // Only create namespace if we have multiple keys with same prefix
39
+ if (!namespaces.has(namespace)) {
40
+ namespaces.set(namespace, []);
41
+ }
42
+ namespaces.get(namespace).push({
43
+ key: remainder,
44
+ value: secret.value,
45
+ });
46
+ }
47
+ else {
48
+ ungrouped.push(secret);
49
+ }
50
+ }
51
+ // Filter out single-item namespaces (not worth grouping)
52
+ const filtered = new Map();
53
+ for (const [ns, entries] of namespaces.entries()) {
54
+ if (entries.length > 1) {
55
+ filtered.set(ns, entries);
56
+ }
57
+ else {
58
+ // Move single-entry namespaces back to ungrouped
59
+ ungrouped.push({
60
+ key: `${ns.toUpperCase()}_${entries[0].key}`,
61
+ value: entries[0].value,
62
+ });
63
+ }
64
+ }
65
+ // Add ungrouped as special namespace if exists
66
+ if (ungrouped.length > 0) {
67
+ filtered.set('_root', ungrouped);
68
+ }
69
+ return filtered;
70
+ }
71
+ /**
72
+ * Format secrets as .env file (KEY=value)
73
+ */
74
+ export function formatAsEnv(secrets) {
75
+ return secrets.map(({ key, value }) => `${key}=${value}`).join('\n');
76
+ }
77
+ /**
78
+ * Format secrets as JSON object
79
+ */
80
+ export function formatAsJSON(secrets) {
81
+ const obj = {};
82
+ for (const { key, value } of secrets) {
83
+ obj[key] = value;
84
+ }
85
+ return JSON.stringify(obj, null, 2);
86
+ }
87
+ /**
88
+ * Format secrets as YAML
89
+ */
90
+ export function formatAsYAML(secrets) {
91
+ const obj = {};
92
+ for (const { key, value } of secrets) {
93
+ obj[key] = value;
94
+ }
95
+ return yaml.dump(obj, {
96
+ lineWidth: -1, // Don't wrap long lines
97
+ noRefs: true,
98
+ });
99
+ }
100
+ /**
101
+ * Format secrets as TOML with namespace detection
102
+ */
103
+ export function formatAsTOML(secrets) {
104
+ const namespaces = detectNamespaces(secrets);
105
+ const lines = [];
106
+ // Process root (ungrouped) keys first
107
+ if (namespaces.has('_root')) {
108
+ const rootEntries = namespaces.get('_root');
109
+ for (const { key, value } of rootEntries) {
110
+ lines.push(`${key} = ${JSON.stringify(value)}`);
111
+ }
112
+ namespaces.delete('_root');
113
+ }
114
+ // Process namespaced keys
115
+ for (const [namespace, entries] of namespaces.entries()) {
116
+ if (lines.length > 0) {
117
+ lines.push(''); // Blank line before section
118
+ }
119
+ lines.push(`[${namespace}]`);
120
+ for (const { key, value } of entries) {
121
+ lines.push(`${key} = ${JSON.stringify(value)}`);
122
+ }
123
+ }
124
+ return lines.join('\n');
125
+ }
126
+ /**
127
+ * Format secrets as shell export statements
128
+ */
129
+ export function formatAsExport(secrets) {
130
+ return secrets
131
+ .map(({ key, value }) => {
132
+ // Escape single quotes in value
133
+ const escapedValue = value.replace(/'/g, "'\\''");
134
+ return `export ${key}='${escapedValue}'`;
135
+ })
136
+ .join('\n');
137
+ }
138
+ /**
139
+ * Format secrets based on specified format
140
+ *
141
+ * @param secrets - Array of secret entries
142
+ * @param format - Output format
143
+ * @param mask - Whether to mask values (auto-disabled for structured formats unless explicitly set)
144
+ */
145
+ export function formatSecrets(secrets, format, mask) {
146
+ // Auto-disable masking for structured formats unless explicitly set to true
147
+ const shouldMask = mask ?? (format === 'env');
148
+ const secretsToFormat = shouldMask ? maskSecrets(secrets) : secrets;
149
+ switch (format) {
150
+ case 'env':
151
+ return formatAsEnv(secretsToFormat);
152
+ case 'json':
153
+ return formatAsJSON(secretsToFormat);
154
+ case 'yaml':
155
+ return formatAsYAML(secretsToFormat);
156
+ case 'toml':
157
+ return formatAsTOML(secretsToFormat);
158
+ case 'export':
159
+ return formatAsExport(secretsToFormat);
160
+ default:
161
+ throw new Error(`Unsupported format: ${format}`);
162
+ }
163
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Fuzzy matching utilities for secret keys
3
+ */
4
+ /**
5
+ * Normalize a string for fuzzy matching by removing spaces and special chars
6
+ */
7
+ function normalizeForMatching(str) {
8
+ // Remove spaces, hyphens, and convert to lowercase
9
+ return str.toLowerCase().replace(/[\s-]/g, '');
10
+ }
11
+ /**
12
+ * Calculate fuzzy match score between search string and key
13
+ * Returns a score where higher is better, or -1 if no match
14
+ *
15
+ * Supports space-separated searches:
16
+ * - "stripe api" matches "STRIPE_API_KEY"
17
+ * - "stripe secret" matches "STRIPE_SECRET_KEY"
18
+ */
19
+ export function calculateMatchScore(searchTerm, key) {
20
+ // Empty search term matches nothing
21
+ if (!searchTerm || searchTerm.trim() === '') {
22
+ return -1;
23
+ }
24
+ const searchLower = searchTerm.toLowerCase();
25
+ const keyLower = key.toLowerCase();
26
+ // Normalize both for space-insensitive matching
27
+ const searchNormalized = normalizeForMatching(searchTerm);
28
+ const keyNormalized = normalizeForMatching(key);
29
+ // Exact match (case sensitive) - highest priority
30
+ if (searchTerm === key) {
31
+ return 1100;
32
+ }
33
+ // Exact match (case insensitive)
34
+ if (searchLower === keyLower || searchNormalized === keyNormalized) {
35
+ return 1000;
36
+ }
37
+ // Key starts with search term (case sensitive)
38
+ if (key.startsWith(searchTerm)) {
39
+ return 950;
40
+ }
41
+ // Key starts with search term (case insensitive, normalized)
42
+ if (keyLower.startsWith(searchLower) || keyNormalized.startsWith(searchNormalized)) {
43
+ return 900;
44
+ }
45
+ // Check if search with spaces can match multiple words in key
46
+ // e.g., "stripe api" should match "STRIPE_API_KEY"
47
+ // Only do multi-word matching if user provided spaces (not underscores)
48
+ const hasSpaces = /\s/.test(searchTerm);
49
+ if (hasSpaces) {
50
+ const searchWords = searchTerm.toLowerCase().split(/[\s_-]+/).filter(w => w.length > 0);
51
+ const keyWords = key.toLowerCase().split(/[_-]/).filter(w => w.length > 0);
52
+ if (searchWords.length > 1) {
53
+ // Try to match all search words in order within key words
54
+ let matchCount = 0;
55
+ let lastMatchIndex = -1;
56
+ for (const searchWord of searchWords) {
57
+ let found = false;
58
+ for (let i = lastMatchIndex + 1; i < keyWords.length; i++) {
59
+ if (keyWords[i].startsWith(searchWord) || keyWords[i].includes(searchWord)) {
60
+ matchCount++;
61
+ lastMatchIndex = i;
62
+ found = true;
63
+ break;
64
+ }
65
+ }
66
+ if (!found) {
67
+ break; // If any word doesn't match, stop
68
+ }
69
+ }
70
+ // If all search words matched, score based on how many matched
71
+ if (matchCount === searchWords.length) {
72
+ // Perfect multi-word match - higher than substring matches
73
+ return 850 - (lastMatchIndex * 10); // Earlier matches score higher
74
+ }
75
+ else if (matchCount > 0) {
76
+ // Partial multi-word match
77
+ return 300 + (matchCount * 50);
78
+ }
79
+ }
80
+ }
81
+ // Key contains search term (case insensitive, normalized)
82
+ // Only apply this for single-word searches (no spaces)
83
+ if (!hasSpaces && (keyLower.includes(searchLower) || keyNormalized.includes(searchNormalized))) {
84
+ // Score based on position (earlier is better)
85
+ const position = keyNormalized.indexOf(searchNormalized);
86
+ const relativePosition = position / keyNormalized.length;
87
+ return 500 - (relativePosition * 100);
88
+ }
89
+ // Substring match with underscores/boundaries
90
+ // e.g., "stripe" matches "STRIPE_API_KEY" or "MY_STRIPE_SECRET"
91
+ const words = keyLower.split('_');
92
+ for (let i = 0; i < words.length; i++) {
93
+ if (words[i].startsWith(searchLower)) {
94
+ // Earlier words get higher scores
95
+ return 700 - (i * 50);
96
+ }
97
+ if (words[i].includes(searchLower)) {
98
+ return 400 - (i * 50);
99
+ }
100
+ }
101
+ // No match
102
+ return -1;
103
+ }
104
+ /**
105
+ * Find fuzzy matches for a search term in a list of key-value pairs
106
+ * Returns matches sorted by relevance (best match first)
107
+ */
108
+ export function findFuzzyMatches(searchTerm, secrets) {
109
+ const results = [];
110
+ for (const secret of secrets) {
111
+ const score = calculateMatchScore(searchTerm, secret.key);
112
+ if (score >= 0) {
113
+ results.push({
114
+ key: secret.key,
115
+ value: secret.value,
116
+ score,
117
+ });
118
+ }
119
+ }
120
+ // Sort by score (descending - best match first)
121
+ results.sort((a, b) => b.score - a.score);
122
+ return results;
123
+ }
@@ -367,7 +367,8 @@ export class JobManager extends BaseJobManager {
367
367
  const { process: _process, timer: _timer, ...serializable } = job;
368
368
  return serializable;
369
369
  });
370
- fs.writeFileSync(this.persistenceFile, JSON.stringify(jobs, null, 2));
370
+ // Write with secure permissions (mode 0o600 = rw-------)
371
+ fs.writeFileSync(this.persistenceFile, JSON.stringify(jobs, null, 2), { mode: 0o600 });
371
372
  }
372
373
  catch (error) {
373
374
  this.logger.error('Failed to persist jobs', error);