lsh-framework 0.5.4

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 (90) hide show
  1. package/.env.example +51 -0
  2. package/README.md +399 -0
  3. package/dist/app.js +33 -0
  4. package/dist/cicd/analytics.js +261 -0
  5. package/dist/cicd/auth.js +269 -0
  6. package/dist/cicd/cache-manager.js +172 -0
  7. package/dist/cicd/data-retention.js +305 -0
  8. package/dist/cicd/performance-monitor.js +224 -0
  9. package/dist/cicd/webhook-receiver.js +634 -0
  10. package/dist/cli.js +500 -0
  11. package/dist/commands/api.js +343 -0
  12. package/dist/commands/self.js +318 -0
  13. package/dist/commands/theme.js +257 -0
  14. package/dist/commands/zsh-import.js +240 -0
  15. package/dist/components/App.js +1 -0
  16. package/dist/components/Divider.js +29 -0
  17. package/dist/components/REPL.js +43 -0
  18. package/dist/components/Terminal.js +232 -0
  19. package/dist/components/UserInput.js +30 -0
  20. package/dist/daemon/api-server.js +315 -0
  21. package/dist/daemon/job-registry.js +554 -0
  22. package/dist/daemon/lshd.js +822 -0
  23. package/dist/daemon/monitoring-api.js +220 -0
  24. package/dist/examples/supabase-integration.js +106 -0
  25. package/dist/lib/api-error-handler.js +183 -0
  26. package/dist/lib/associative-arrays.js +285 -0
  27. package/dist/lib/base-api-server.js +290 -0
  28. package/dist/lib/base-command-registrar.js +286 -0
  29. package/dist/lib/base-job-manager.js +293 -0
  30. package/dist/lib/brace-expansion.js +160 -0
  31. package/dist/lib/builtin-commands.js +439 -0
  32. package/dist/lib/cloud-config-manager.js +347 -0
  33. package/dist/lib/command-validator.js +190 -0
  34. package/dist/lib/completion-system.js +344 -0
  35. package/dist/lib/cron-job-manager.js +364 -0
  36. package/dist/lib/daemon-client-helper.js +141 -0
  37. package/dist/lib/daemon-client.js +501 -0
  38. package/dist/lib/database-persistence.js +638 -0
  39. package/dist/lib/database-schema.js +259 -0
  40. package/dist/lib/enhanced-history-system.js +246 -0
  41. package/dist/lib/env-validator.js +265 -0
  42. package/dist/lib/executors/builtin-executor.js +52 -0
  43. package/dist/lib/extended-globbing.js +411 -0
  44. package/dist/lib/extended-parameter-expansion.js +227 -0
  45. package/dist/lib/floating-point-arithmetic.js +256 -0
  46. package/dist/lib/history-system.js +245 -0
  47. package/dist/lib/interactive-shell.js +460 -0
  48. package/dist/lib/job-builtins.js +580 -0
  49. package/dist/lib/job-manager.js +386 -0
  50. package/dist/lib/job-storage-database.js +156 -0
  51. package/dist/lib/job-storage-memory.js +73 -0
  52. package/dist/lib/logger.js +274 -0
  53. package/dist/lib/lshrc-init.js +177 -0
  54. package/dist/lib/pathname-expansion.js +216 -0
  55. package/dist/lib/prompt-system.js +328 -0
  56. package/dist/lib/script-runner.js +226 -0
  57. package/dist/lib/secrets-manager.js +193 -0
  58. package/dist/lib/shell-executor.js +2504 -0
  59. package/dist/lib/shell-parser.js +958 -0
  60. package/dist/lib/shell-types.js +6 -0
  61. package/dist/lib/shell.lib.js +40 -0
  62. package/dist/lib/supabase-client.js +58 -0
  63. package/dist/lib/theme-manager.js +476 -0
  64. package/dist/lib/variable-expansion.js +385 -0
  65. package/dist/lib/zsh-compatibility.js +658 -0
  66. package/dist/lib/zsh-import-manager.js +699 -0
  67. package/dist/lib/zsh-options.js +328 -0
  68. package/dist/pipeline/job-tracker.js +491 -0
  69. package/dist/pipeline/mcli-bridge.js +302 -0
  70. package/dist/pipeline/pipeline-service.js +1116 -0
  71. package/dist/pipeline/workflow-engine.js +867 -0
  72. package/dist/services/api/api.js +58 -0
  73. package/dist/services/api/auth.js +35 -0
  74. package/dist/services/api/config.js +7 -0
  75. package/dist/services/api/file.js +22 -0
  76. package/dist/services/cron/cron-registrar.js +235 -0
  77. package/dist/services/cron/cron.js +9 -0
  78. package/dist/services/daemon/daemon-registrar.js +565 -0
  79. package/dist/services/daemon/daemon.js +9 -0
  80. package/dist/services/lib/lib.js +86 -0
  81. package/dist/services/log-file-extractor.js +170 -0
  82. package/dist/services/secrets/secrets.js +94 -0
  83. package/dist/services/shell/shell.js +28 -0
  84. package/dist/services/supabase/supabase-registrar.js +367 -0
  85. package/dist/services/supabase/supabase.js +9 -0
  86. package/dist/services/zapier.js +16 -0
  87. package/dist/simple-api-server.js +148 -0
  88. package/dist/store/store.js +31 -0
  89. package/dist/util/lib.util.js +11 -0
  90. package/package.json +144 -0
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Logging Framework
3
+ * Centralized logging utility with support for different log levels,
4
+ * structured logging, and environment-based configuration.
5
+ */
6
+ export var LogLevel;
7
+ (function (LogLevel) {
8
+ LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
9
+ LogLevel[LogLevel["INFO"] = 1] = "INFO";
10
+ LogLevel[LogLevel["WARN"] = 2] = "WARN";
11
+ LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
12
+ LogLevel[LogLevel["NONE"] = 4] = "NONE";
13
+ })(LogLevel || (LogLevel = {}));
14
+ /**
15
+ * ANSI color codes for terminal output
16
+ */
17
+ const colors = {
18
+ reset: '\x1b[0m',
19
+ bright: '\x1b[1m',
20
+ dim: '\x1b[2m',
21
+ // Foreground colors
22
+ black: '\x1b[30m',
23
+ red: '\x1b[31m',
24
+ green: '\x1b[32m',
25
+ yellow: '\x1b[33m',
26
+ blue: '\x1b[34m',
27
+ magenta: '\x1b[35m',
28
+ cyan: '\x1b[36m',
29
+ white: '\x1b[37m',
30
+ // Background colors
31
+ bgBlack: '\x1b[40m',
32
+ bgRed: '\x1b[41m',
33
+ bgGreen: '\x1b[42m',
34
+ bgYellow: '\x1b[43m',
35
+ bgBlue: '\x1b[44m',
36
+ bgMagenta: '\x1b[45m',
37
+ bgCyan: '\x1b[46m',
38
+ bgWhite: '\x1b[47m',
39
+ };
40
+ /**
41
+ * Get log level from environment variable
42
+ */
43
+ function getLogLevelFromEnv() {
44
+ const level = process.env.LSH_LOG_LEVEL?.toUpperCase();
45
+ switch (level) {
46
+ case 'DEBUG':
47
+ return LogLevel.DEBUG;
48
+ case 'INFO':
49
+ return LogLevel.INFO;
50
+ case 'WARN':
51
+ return LogLevel.WARN;
52
+ case 'ERROR':
53
+ return LogLevel.ERROR;
54
+ case 'NONE':
55
+ return LogLevel.NONE;
56
+ default:
57
+ return process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.DEBUG;
58
+ }
59
+ }
60
+ /**
61
+ * Logger class for structured logging
62
+ */
63
+ export class Logger {
64
+ config;
65
+ constructor(config) {
66
+ this.config = {
67
+ level: config?.level ?? getLogLevelFromEnv(),
68
+ enableTimestamp: config?.enableTimestamp ?? true,
69
+ enableColors: config?.enableColors ?? (!process.env.NO_COLOR && process.stdout.isTTY),
70
+ enableJSON: config?.enableJSON ?? (process.env.LSH_LOG_FORMAT === 'json'),
71
+ context: config?.context,
72
+ };
73
+ }
74
+ /**
75
+ * Create a child logger with a specific context
76
+ */
77
+ child(context) {
78
+ return new Logger({
79
+ ...this.config,
80
+ context: this.config.context ? `${this.config.context}:${context}` : context,
81
+ });
82
+ }
83
+ /**
84
+ * Set log level dynamically
85
+ */
86
+ setLevel(level) {
87
+ this.config.level = level;
88
+ }
89
+ /**
90
+ * Check if a log level is enabled
91
+ */
92
+ isLevelEnabled(level) {
93
+ return level >= this.config.level;
94
+ }
95
+ /**
96
+ * Format timestamp
97
+ */
98
+ formatTimestamp() {
99
+ const now = new Date();
100
+ return now.toISOString();
101
+ }
102
+ /**
103
+ * Get level name string
104
+ */
105
+ getLevelName(level) {
106
+ switch (level) {
107
+ case LogLevel.DEBUG:
108
+ return 'DEBUG';
109
+ case LogLevel.INFO:
110
+ return 'INFO';
111
+ case LogLevel.WARN:
112
+ return 'WARN';
113
+ case LogLevel.ERROR:
114
+ return 'ERROR';
115
+ default:
116
+ return 'UNKNOWN';
117
+ }
118
+ }
119
+ /**
120
+ * Get color for log level
121
+ */
122
+ getLevelColor(level) {
123
+ switch (level) {
124
+ case LogLevel.DEBUG:
125
+ return colors.cyan;
126
+ case LogLevel.INFO:
127
+ return colors.green;
128
+ case LogLevel.WARN:
129
+ return colors.yellow;
130
+ case LogLevel.ERROR:
131
+ return colors.red;
132
+ default:
133
+ return colors.white;
134
+ }
135
+ }
136
+ /**
137
+ * Format log entry as JSON
138
+ */
139
+ formatJSON(entry) {
140
+ const obj = {
141
+ timestamp: entry.timestamp.toISOString(),
142
+ level: this.getLevelName(entry.level),
143
+ message: entry.message,
144
+ };
145
+ if (entry.context) {
146
+ obj.context = entry.context;
147
+ }
148
+ if (entry.metadata && Object.keys(entry.metadata).length > 0) {
149
+ obj.metadata = entry.metadata;
150
+ }
151
+ if (entry.error) {
152
+ obj.error = {
153
+ name: entry.error.name,
154
+ message: entry.error.message,
155
+ stack: entry.error.stack,
156
+ };
157
+ }
158
+ return JSON.stringify(obj);
159
+ }
160
+ /**
161
+ * Format log entry as text
162
+ */
163
+ formatText(entry) {
164
+ const parts = [];
165
+ // Timestamp
166
+ if (this.config.enableTimestamp) {
167
+ const timestamp = this.formatTimestamp();
168
+ parts.push(this.config.enableColors ? `${colors.dim}${timestamp}${colors.reset}` : timestamp);
169
+ }
170
+ // Level
171
+ const levelName = this.getLevelName(entry.level);
172
+ const levelColor = this.getLevelColor(entry.level);
173
+ const formattedLevel = this.config.enableColors
174
+ ? `${levelColor}${levelName.padEnd(5)}${colors.reset}`
175
+ : levelName.padEnd(5);
176
+ parts.push(formattedLevel);
177
+ // Context
178
+ if (entry.context) {
179
+ const formattedContext = this.config.enableColors
180
+ ? `${colors.magenta}[${entry.context}]${colors.reset}`
181
+ : `[${entry.context}]`;
182
+ parts.push(formattedContext);
183
+ }
184
+ // Message
185
+ parts.push(entry.message);
186
+ // Metadata
187
+ if (entry.metadata && Object.keys(entry.metadata).length > 0) {
188
+ const metadataStr = JSON.stringify(entry.metadata);
189
+ const formattedMetadata = this.config.enableColors
190
+ ? `${colors.dim}${metadataStr}${colors.reset}`
191
+ : metadataStr;
192
+ parts.push(formattedMetadata);
193
+ }
194
+ return parts.join(' ');
195
+ }
196
+ /**
197
+ * Core logging method
198
+ */
199
+ log(level, message, metadata, error) {
200
+ if (!this.isLevelEnabled(level)) {
201
+ return;
202
+ }
203
+ const entry = {
204
+ timestamp: new Date(),
205
+ level,
206
+ message,
207
+ context: this.config.context,
208
+ metadata,
209
+ error,
210
+ };
211
+ const output = this.config.enableJSON
212
+ ? this.formatJSON(entry)
213
+ : this.formatText(entry);
214
+ // Output to appropriate stream
215
+ if (level >= LogLevel.ERROR) {
216
+ console.error(output);
217
+ if (error?.stack) {
218
+ console.error(error.stack);
219
+ }
220
+ }
221
+ else if (level >= LogLevel.WARN) {
222
+ console.warn(output);
223
+ }
224
+ else {
225
+ // INFO and DEBUG go to stdout
226
+ // Using console.log here is intentional for the logger itself
227
+ // eslint-disable-next-line no-console
228
+ console.log(output);
229
+ }
230
+ }
231
+ /**
232
+ * Debug level logging
233
+ */
234
+ debug(message, metadata) {
235
+ this.log(LogLevel.DEBUG, message, metadata);
236
+ }
237
+ /**
238
+ * Info level logging
239
+ */
240
+ info(message, metadata) {
241
+ this.log(LogLevel.INFO, message, metadata);
242
+ }
243
+ /**
244
+ * Warning level logging
245
+ */
246
+ warn(message, metadata) {
247
+ this.log(LogLevel.WARN, message, metadata);
248
+ }
249
+ /**
250
+ * Error level logging
251
+ */
252
+ error(message, error, metadata) {
253
+ const err = error instanceof Error ? error : undefined;
254
+ const errorMetadata = error && !(error instanceof Error) ? { error } : undefined;
255
+ this.log(LogLevel.ERROR, message, { ...metadata, ...errorMetadata }, err);
256
+ }
257
+ }
258
+ /**
259
+ * Default logger instance
260
+ */
261
+ export const logger = new Logger();
262
+ /**
263
+ * Create a logger with a specific context
264
+ */
265
+ export function createLogger(context, config) {
266
+ return new Logger({
267
+ ...config,
268
+ context,
269
+ });
270
+ }
271
+ /**
272
+ * Export for default usage
273
+ */
274
+ export default logger;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * .lshrc Initialization
3
+ * Creates and manages the user's .lshrc configuration file
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import { fileURLToPath } from 'url';
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ export class LshrcManager {
12
+ lshrcPath;
13
+ constructor(lshrcPath) {
14
+ this.lshrcPath = lshrcPath || path.join(os.homedir(), '.lshrc');
15
+ }
16
+ /**
17
+ * Initialize .lshrc if it doesn't exist
18
+ */
19
+ initialize(options = {}) {
20
+ try {
21
+ // Check if .lshrc already exists
22
+ if (fs.existsSync(this.lshrcPath)) {
23
+ return false; // Already exists, no action needed
24
+ }
25
+ if (!options.createIfMissing) {
26
+ return false; // Don't create if not requested
27
+ }
28
+ // Copy template to user's home directory
29
+ const templatePath = path.join(__dirname, '../../templates/.lshrc.template');
30
+ if (!fs.existsSync(templatePath)) {
31
+ // Template not found, create basic .lshrc
32
+ this.createBasicLshrc(options);
33
+ return true;
34
+ }
35
+ // Copy template
36
+ let content = fs.readFileSync(templatePath, 'utf8');
37
+ // Enable auto-import if requested
38
+ if (options.autoImportZsh) {
39
+ const importCmd = `zsh-source${options.importOptions ? ' ' + options.importOptions.join(' ') : ''}`;
40
+ content = content.replace('# zsh-source', importCmd);
41
+ }
42
+ fs.writeFileSync(this.lshrcPath, content, 'utf8');
43
+ console.log(`✅ Created ${this.lshrcPath}`);
44
+ return true;
45
+ }
46
+ catch (error) {
47
+ console.error(`Failed to initialize .lshrc: ${error.message}`);
48
+ return false;
49
+ }
50
+ }
51
+ /**
52
+ * Create basic .lshrc without template
53
+ */
54
+ createBasicLshrc(options) {
55
+ const content = `# LSH Configuration File
56
+ # Location: ${this.lshrcPath}
57
+
58
+ ${options.autoImportZsh ? `# Auto-import ZSH configurations
59
+ zsh-source${options.importOptions ? ' ' + options.importOptions.join(' ') : ''}
60
+ ` : '# Uncomment to auto-import ZSH configurations\n# zsh-source\n'}
61
+
62
+ # Add your aliases, functions, and environment variables here
63
+ `;
64
+ fs.writeFileSync(this.lshrcPath, content, 'utf8');
65
+ console.log(`✅ Created ${this.lshrcPath}`);
66
+ }
67
+ /**
68
+ * Check if .lshrc exists
69
+ */
70
+ exists() {
71
+ return fs.existsSync(this.lshrcPath);
72
+ }
73
+ /**
74
+ * Source .lshrc commands
75
+ */
76
+ async source(_executor) {
77
+ if (!this.exists()) {
78
+ return [];
79
+ }
80
+ try {
81
+ const content = fs.readFileSync(this.lshrcPath, 'utf8');
82
+ const commands = [];
83
+ for (const line of content.split('\n')) {
84
+ const trimmed = line.trim();
85
+ // Skip comments and empty lines
86
+ if (trimmed.startsWith('#') || trimmed === '') {
87
+ continue;
88
+ }
89
+ commands.push(trimmed);
90
+ }
91
+ return commands;
92
+ }
93
+ catch (error) {
94
+ console.error(`Failed to source .lshrc: ${error.message}`);
95
+ return [];
96
+ }
97
+ }
98
+ /**
99
+ * Enable auto-import in existing .lshrc
100
+ */
101
+ enableAutoImport(options = []) {
102
+ try {
103
+ if (!this.exists()) {
104
+ // Create new .lshrc with auto-import enabled
105
+ this.initialize({ autoImportZsh: true, importOptions: options, createIfMissing: true });
106
+ return true;
107
+ }
108
+ let content = fs.readFileSync(this.lshrcPath, 'utf8');
109
+ // Check if auto-import is already enabled
110
+ if (content.includes('zsh-source') && !content.match(/^#.*zsh-source/m)) {
111
+ console.log('Auto-import is already enabled in .lshrc');
112
+ return false;
113
+ }
114
+ // Add auto-import configuration
115
+ const autoImportBlock = `
116
+ # ZSH Auto-Import (added by LSH)
117
+ zsh-source${options.length > 0 ? ' ' + options.join(' ') : ''}
118
+ `;
119
+ // Find the ZSH import section or add at the top
120
+ if (content.includes('# ZSH IMPORT CONFIGURATION')) {
121
+ content = content.replace(/# zsh-source/, `zsh-source${options.length > 0 ? ' ' + options.join(' ') : ''}`);
122
+ }
123
+ else {
124
+ content = autoImportBlock + '\n' + content;
125
+ }
126
+ fs.writeFileSync(this.lshrcPath, content, 'utf8');
127
+ console.log('✅ Auto-import enabled in .lshrc');
128
+ return true;
129
+ }
130
+ catch (error) {
131
+ console.error(`Failed to enable auto-import: ${error.message}`);
132
+ return false;
133
+ }
134
+ }
135
+ /**
136
+ * Disable auto-import in .lshrc
137
+ */
138
+ disableAutoImport() {
139
+ try {
140
+ if (!this.exists()) {
141
+ return false;
142
+ }
143
+ let content = fs.readFileSync(this.lshrcPath, 'utf8');
144
+ // Comment out zsh-source lines
145
+ content = content.replace(/^(zsh-source.*)$/gm, '# $1');
146
+ // Remove auto-import blocks
147
+ content = content.replace(/# ZSH Auto-Import[\s\S]*?(?=\n#|\n\n|$)/g, '');
148
+ fs.writeFileSync(this.lshrcPath, content, 'utf8');
149
+ console.log('✅ Auto-import disabled in .lshrc');
150
+ return true;
151
+ }
152
+ catch (error) {
153
+ console.error(`Failed to disable auto-import: ${error.message}`);
154
+ return false;
155
+ }
156
+ }
157
+ /**
158
+ * Get .lshrc path
159
+ */
160
+ getPath() {
161
+ return this.lshrcPath;
162
+ }
163
+ }
164
+ /**
165
+ * Initialize .lshrc on first run
166
+ */
167
+ export function initializeLshrc(options = {}) {
168
+ const manager = new LshrcManager();
169
+ return manager.initialize(options);
170
+ }
171
+ /**
172
+ * Check if .lshrc exists
173
+ */
174
+ export function lshrcExists() {
175
+ const manager = new LshrcManager();
176
+ return manager.exists();
177
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * POSIX Pathname Expansion Implementation
3
+ * Implements POSIX.1-2017 Section 2.13 Pathname Expansion (Globbing)
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ export class PathnameExpander {
8
+ cwd;
9
+ constructor(cwd = process.cwd()) {
10
+ this.cwd = cwd;
11
+ }
12
+ async expandPathnames(pattern, options = {}) {
13
+ // If no glob characters, return as-is
14
+ if (!this.containsGlobChars(pattern)) {
15
+ return [pattern];
16
+ }
17
+ // Handle tilde expansion first
18
+ const expandedPattern = this.expandTilde(pattern);
19
+ // Perform pathname expansion
20
+ return this.glob(expandedPattern, options);
21
+ }
22
+ containsGlobChars(str) {
23
+ // Check for unescaped glob characters
24
+ return /[*?[\]{}~]/.test(str);
25
+ }
26
+ expandTilde(pattern) {
27
+ if (pattern.startsWith('~/')) {
28
+ const homeDir = process.env.HOME || '/';
29
+ return path.join(homeDir, pattern.slice(2));
30
+ }
31
+ if (pattern === '~') {
32
+ return process.env.HOME || '/';
33
+ }
34
+ return pattern;
35
+ }
36
+ async glob(pattern, options) {
37
+ const baseCwd = options.cwd || this.cwd;
38
+ // Handle absolute vs relative paths
39
+ const isAbsolute = path.isAbsolute(pattern);
40
+ const searchBase = isAbsolute ? '/' : baseCwd;
41
+ const relativePath = isAbsolute ? pattern.slice(1) : pattern;
42
+ // Split pattern into segments
43
+ const segments = relativePath.split('/').filter(seg => seg.length > 0);
44
+ if (segments.length === 0) {
45
+ return [searchBase];
46
+ }
47
+ // Start recursive matching
48
+ const results = await this.matchSegments(searchBase, segments, options);
49
+ // Sort results for consistent output
50
+ return results.sort();
51
+ }
52
+ async matchSegments(currentPath, remainingSegments, options) {
53
+ if (remainingSegments.length === 0) {
54
+ return [currentPath];
55
+ }
56
+ const [currentSegment, ...restSegments] = remainingSegments;
57
+ const results = [];
58
+ try {
59
+ const entries = await fs.promises.readdir(currentPath, { withFileTypes: true });
60
+ for (const entry of entries) {
61
+ // Skip hidden files unless explicitly included
62
+ if (!options.includeHidden && entry.name.startsWith('.')) {
63
+ continue;
64
+ }
65
+ // Check if this entry matches the current segment pattern
66
+ if (this.matchSegment(entry.name, currentSegment)) {
67
+ const fullPath = path.join(currentPath, entry.name);
68
+ if (restSegments.length === 0) {
69
+ // This is the final segment, add to results
70
+ results.push(fullPath);
71
+ }
72
+ else if (entry.isDirectory()) {
73
+ // Recurse into directory for remaining segments
74
+ const subResults = await this.matchSegments(fullPath, restSegments, options);
75
+ results.push(...subResults);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ catch (_error) {
81
+ // Directory doesn't exist or not readable, return empty
82
+ return [];
83
+ }
84
+ return results;
85
+ }
86
+ matchSegment(filename, pattern) {
87
+ // Convert glob pattern to regex
88
+ const regex = this.patternToRegex(pattern);
89
+ return regex.test(filename);
90
+ }
91
+ patternToRegex(pattern) {
92
+ let regexStr = '';
93
+ let i = 0;
94
+ while (i < pattern.length) {
95
+ const char = pattern[i];
96
+ switch (char) {
97
+ case '*':
98
+ regexStr += '.*';
99
+ break;
100
+ case '?':
101
+ regexStr += '.';
102
+ break;
103
+ case '[': {
104
+ // Character class
105
+ const closeIdx = this.findClosingBracket(pattern, i);
106
+ if (closeIdx === -1) {
107
+ // Invalid bracket, treat as literal
108
+ regexStr += '\\[';
109
+ }
110
+ else {
111
+ let charClass = pattern.slice(i + 1, closeIdx);
112
+ // Handle negation
113
+ if (charClass.startsWith('!') || charClass.startsWith('^')) {
114
+ charClass = '^' + charClass.slice(1);
115
+ }
116
+ // Handle character ranges and classes
117
+ charClass = this.processCharacterClass(charClass);
118
+ regexStr += '[' + charClass + ']';
119
+ i = closeIdx;
120
+ }
121
+ break;
122
+ }
123
+ case '{': {
124
+ // Brace expansion - simplified implementation
125
+ const braceEnd = this.findClosingBrace(pattern, i);
126
+ if (braceEnd === -1) {
127
+ regexStr += '\\{';
128
+ }
129
+ else {
130
+ const braceContent = pattern.slice(i + 1, braceEnd);
131
+ const alternatives = braceContent.split(',');
132
+ regexStr += '(' + alternatives.map(alt => this.escapeRegex(alt)).join('|') + ')';
133
+ i = braceEnd;
134
+ }
135
+ break;
136
+ }
137
+ case '\\':
138
+ // Escape next character
139
+ if (i + 1 < pattern.length) {
140
+ regexStr += '\\' + pattern[i + 1];
141
+ i++;
142
+ }
143
+ else {
144
+ regexStr += '\\\\';
145
+ }
146
+ break;
147
+ default:
148
+ // Literal character - escape regex special chars
149
+ regexStr += this.escapeRegex(char);
150
+ break;
151
+ }
152
+ i++;
153
+ }
154
+ return new RegExp('^' + regexStr + '$');
155
+ }
156
+ findClosingBracket(str, startIdx) {
157
+ let depth = 1;
158
+ for (let i = startIdx + 1; i < str.length; i++) {
159
+ if (str[i] === '[')
160
+ depth++;
161
+ else if (str[i] === ']') {
162
+ depth--;
163
+ if (depth === 0)
164
+ return i;
165
+ }
166
+ }
167
+ return -1;
168
+ }
169
+ findClosingBrace(str, startIdx) {
170
+ let depth = 1;
171
+ for (let i = startIdx + 1; i < str.length; i++) {
172
+ if (str[i] === '{')
173
+ depth++;
174
+ else if (str[i] === '}') {
175
+ depth--;
176
+ if (depth === 0)
177
+ return i;
178
+ }
179
+ }
180
+ return -1;
181
+ }
182
+ processCharacterClass(charClass) {
183
+ // Handle POSIX character classes like [:alpha:]
184
+ charClass = charClass.replace(/\[:alpha:\]/g, 'a-zA-Z');
185
+ charClass = charClass.replace(/\[:digit:\]/g, '0-9');
186
+ charClass = charClass.replace(/\[:alnum:\]/g, 'a-zA-Z0-9');
187
+ charClass = charClass.replace(/\[:lower:\]/g, 'a-z');
188
+ charClass = charClass.replace(/\[:upper:\]/g, 'A-Z');
189
+ charClass = charClass.replace(/\[:space:\]/g, ' \\t\\n\\r\\f\\v');
190
+ charClass = charClass.replace(/\[:blank:\]/g, ' \\t');
191
+ charClass = charClass.replace(/\[:punct:\]/g, '!-/:-@\\[-`{-~');
192
+ charClass = charClass.replace(/\[:cntrl:\]/g, '\\x00-\\x1F\\x7F');
193
+ charClass = charClass.replace(/\[:print:\]/g, '\\x20-\\x7E');
194
+ charClass = charClass.replace(/\[:graph:\]/g, '\\x21-\\x7E');
195
+ charClass = charClass.replace(/\[:xdigit:\]/g, '0-9A-Fa-f');
196
+ return charClass;
197
+ }
198
+ escapeRegex(str) {
199
+ return str.replace(/[.+^$()|[\]{}\\]/g, '\\$&');
200
+ }
201
+ // Utility method for expanding multiple patterns
202
+ async expandMultiplePatterns(patterns, options = {}) {
203
+ const results = [];
204
+ for (const pattern of patterns) {
205
+ const expanded = await this.expandPathnames(pattern, options);
206
+ results.push(...expanded);
207
+ }
208
+ // Remove duplicates and sort
209
+ return [...new Set(results)].sort();
210
+ }
211
+ // Check if a pattern would match any files (useful for error reporting)
212
+ async hasMatches(pattern, options = {}) {
213
+ const matches = await this.expandPathnames(pattern, options);
214
+ return matches.length > 0 && matches[0] !== pattern;
215
+ }
216
+ }