lsh-framework 1.2.0 → 1.2.1

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 (73) 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/job-manager.js +2 -1
  19. package/dist/lib/platform-utils.js +211 -0
  20. package/dist/lib/secrets-manager.js +11 -1
  21. package/dist/lib/string-utils.js +128 -0
  22. package/dist/services/daemon/daemon-registrar.js +3 -2
  23. package/dist/services/secrets/secrets.js +54 -30
  24. package/package.json +10 -74
  25. package/dist/app.js +0 -33
  26. package/dist/cicd/analytics.js +0 -261
  27. package/dist/cicd/auth.js +0 -269
  28. package/dist/cicd/cache-manager.js +0 -172
  29. package/dist/cicd/data-retention.js +0 -305
  30. package/dist/cicd/performance-monitor.js +0 -224
  31. package/dist/cicd/webhook-receiver.js +0 -640
  32. package/dist/commands/api.js +0 -346
  33. package/dist/commands/theme.js +0 -261
  34. package/dist/commands/zsh-import.js +0 -240
  35. package/dist/components/App.js +0 -1
  36. package/dist/components/Divider.js +0 -29
  37. package/dist/components/REPL.js +0 -43
  38. package/dist/components/Terminal.js +0 -232
  39. package/dist/components/UserInput.js +0 -30
  40. package/dist/daemon/api-server.js +0 -316
  41. package/dist/daemon/monitoring-api.js +0 -220
  42. package/dist/lib/api-error-handler.js +0 -185
  43. package/dist/lib/associative-arrays.js +0 -285
  44. package/dist/lib/base-api-server.js +0 -290
  45. package/dist/lib/brace-expansion.js +0 -160
  46. package/dist/lib/builtin-commands.js +0 -439
  47. package/dist/lib/executors/builtin-executor.js +0 -52
  48. package/dist/lib/extended-globbing.js +0 -411
  49. package/dist/lib/extended-parameter-expansion.js +0 -227
  50. package/dist/lib/interactive-shell.js +0 -460
  51. package/dist/lib/job-builtins.js +0 -582
  52. package/dist/lib/pathname-expansion.js +0 -216
  53. package/dist/lib/script-runner.js +0 -226
  54. package/dist/lib/shell-executor.js +0 -2504
  55. package/dist/lib/shell-parser.js +0 -958
  56. package/dist/lib/shell-types.js +0 -6
  57. package/dist/lib/shell.lib.js +0 -40
  58. package/dist/lib/theme-manager.js +0 -476
  59. package/dist/lib/variable-expansion.js +0 -385
  60. package/dist/lib/zsh-compatibility.js +0 -659
  61. package/dist/lib/zsh-import-manager.js +0 -707
  62. package/dist/lib/zsh-options.js +0 -328
  63. package/dist/pipeline/job-tracker.js +0 -491
  64. package/dist/pipeline/mcli-bridge.js +0 -309
  65. package/dist/pipeline/pipeline-service.js +0 -1119
  66. package/dist/pipeline/workflow-engine.js +0 -870
  67. package/dist/services/api/api.js +0 -58
  68. package/dist/services/api/auth.js +0 -35
  69. package/dist/services/api/config.js +0 -7
  70. package/dist/services/api/file.js +0 -22
  71. package/dist/services/shell/shell.js +0 -28
  72. package/dist/services/zapier.js +0 -16
  73. 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
+ }
@@ -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);