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.
- package/.env.example +51 -0
- package/README.md +399 -0
- package/dist/app.js +33 -0
- package/dist/cicd/analytics.js +261 -0
- package/dist/cicd/auth.js +269 -0
- package/dist/cicd/cache-manager.js +172 -0
- package/dist/cicd/data-retention.js +305 -0
- package/dist/cicd/performance-monitor.js +224 -0
- package/dist/cicd/webhook-receiver.js +634 -0
- package/dist/cli.js +500 -0
- package/dist/commands/api.js +343 -0
- package/dist/commands/self.js +318 -0
- package/dist/commands/theme.js +257 -0
- package/dist/commands/zsh-import.js +240 -0
- package/dist/components/App.js +1 -0
- package/dist/components/Divider.js +29 -0
- package/dist/components/REPL.js +43 -0
- package/dist/components/Terminal.js +232 -0
- package/dist/components/UserInput.js +30 -0
- package/dist/daemon/api-server.js +315 -0
- package/dist/daemon/job-registry.js +554 -0
- package/dist/daemon/lshd.js +822 -0
- package/dist/daemon/monitoring-api.js +220 -0
- package/dist/examples/supabase-integration.js +106 -0
- package/dist/lib/api-error-handler.js +183 -0
- package/dist/lib/associative-arrays.js +285 -0
- package/dist/lib/base-api-server.js +290 -0
- package/dist/lib/base-command-registrar.js +286 -0
- package/dist/lib/base-job-manager.js +293 -0
- package/dist/lib/brace-expansion.js +160 -0
- package/dist/lib/builtin-commands.js +439 -0
- package/dist/lib/cloud-config-manager.js +347 -0
- package/dist/lib/command-validator.js +190 -0
- package/dist/lib/completion-system.js +344 -0
- package/dist/lib/cron-job-manager.js +364 -0
- package/dist/lib/daemon-client-helper.js +141 -0
- package/dist/lib/daemon-client.js +501 -0
- package/dist/lib/database-persistence.js +638 -0
- package/dist/lib/database-schema.js +259 -0
- package/dist/lib/enhanced-history-system.js +246 -0
- package/dist/lib/env-validator.js +265 -0
- package/dist/lib/executors/builtin-executor.js +52 -0
- package/dist/lib/extended-globbing.js +411 -0
- package/dist/lib/extended-parameter-expansion.js +227 -0
- package/dist/lib/floating-point-arithmetic.js +256 -0
- package/dist/lib/history-system.js +245 -0
- package/dist/lib/interactive-shell.js +460 -0
- package/dist/lib/job-builtins.js +580 -0
- package/dist/lib/job-manager.js +386 -0
- package/dist/lib/job-storage-database.js +156 -0
- package/dist/lib/job-storage-memory.js +73 -0
- package/dist/lib/logger.js +274 -0
- package/dist/lib/lshrc-init.js +177 -0
- package/dist/lib/pathname-expansion.js +216 -0
- package/dist/lib/prompt-system.js +328 -0
- package/dist/lib/script-runner.js +226 -0
- package/dist/lib/secrets-manager.js +193 -0
- package/dist/lib/shell-executor.js +2504 -0
- package/dist/lib/shell-parser.js +958 -0
- package/dist/lib/shell-types.js +6 -0
- package/dist/lib/shell.lib.js +40 -0
- package/dist/lib/supabase-client.js +58 -0
- package/dist/lib/theme-manager.js +476 -0
- package/dist/lib/variable-expansion.js +385 -0
- package/dist/lib/zsh-compatibility.js +658 -0
- package/dist/lib/zsh-import-manager.js +699 -0
- package/dist/lib/zsh-options.js +328 -0
- package/dist/pipeline/job-tracker.js +491 -0
- package/dist/pipeline/mcli-bridge.js +302 -0
- package/dist/pipeline/pipeline-service.js +1116 -0
- package/dist/pipeline/workflow-engine.js +867 -0
- package/dist/services/api/api.js +58 -0
- package/dist/services/api/auth.js +35 -0
- package/dist/services/api/config.js +7 -0
- package/dist/services/api/file.js +22 -0
- package/dist/services/cron/cron-registrar.js +235 -0
- package/dist/services/cron/cron.js +9 -0
- package/dist/services/daemon/daemon-registrar.js +565 -0
- package/dist/services/daemon/daemon.js +9 -0
- package/dist/services/lib/lib.js +86 -0
- package/dist/services/log-file-extractor.js +170 -0
- package/dist/services/secrets/secrets.js +94 -0
- package/dist/services/shell/shell.js +28 -0
- package/dist/services/supabase/supabase-registrar.js +367 -0
- package/dist/services/supabase/supabase.js +9 -0
- package/dist/services/zapier.js +16 -0
- package/dist/simple-api-server.js +148 -0
- package/dist/store/store.js +31 -0
- package/dist/util/lib.util.js +11 -0
- package/package.json +144 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating Point Arithmetic Implementation
|
|
3
|
+
* Provides ZSH-compatible floating point math support
|
|
4
|
+
*/
|
|
5
|
+
export class FloatingPointArithmetic {
|
|
6
|
+
mathFunctions = new Map();
|
|
7
|
+
precision = 6;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.setupMathFunctions();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Evaluate arithmetic expression with floating point support
|
|
13
|
+
*/
|
|
14
|
+
evaluate(expression) {
|
|
15
|
+
try {
|
|
16
|
+
// Clean and validate expression
|
|
17
|
+
const cleaned = this.cleanExpression(expression);
|
|
18
|
+
// Replace variables with their values
|
|
19
|
+
const withVariables = this.replaceVariables(cleaned);
|
|
20
|
+
// Replace function calls
|
|
21
|
+
const withFunctions = this.replaceFunctions(withVariables);
|
|
22
|
+
// Evaluate the expression
|
|
23
|
+
const result = this.evaluateExpression(withFunctions);
|
|
24
|
+
// Round to specified precision
|
|
25
|
+
return this.roundToPrecision(result);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error(`Arithmetic error: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Set precision for floating point results
|
|
33
|
+
*/
|
|
34
|
+
setPrecision(precision) {
|
|
35
|
+
this.precision = Math.max(0, Math.min(15, precision));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get current precision
|
|
39
|
+
*/
|
|
40
|
+
getPrecision() {
|
|
41
|
+
return this.precision;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Add a custom math function
|
|
45
|
+
*/
|
|
46
|
+
addMathFunction(func) {
|
|
47
|
+
this.mathFunctions.set(func.name, func);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get available math functions
|
|
51
|
+
*/
|
|
52
|
+
getAvailableFunctions() {
|
|
53
|
+
return Array.from(this.mathFunctions.keys());
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Setup built-in math functions
|
|
57
|
+
*/
|
|
58
|
+
setupMathFunctions() {
|
|
59
|
+
// Basic trigonometric functions
|
|
60
|
+
this.addMathFunction({ name: 'sin', func: Math.sin, arity: 1 });
|
|
61
|
+
this.addMathFunction({ name: 'cos', func: Math.cos, arity: 1 });
|
|
62
|
+
this.addMathFunction({ name: 'tan', func: Math.tan, arity: 1 });
|
|
63
|
+
this.addMathFunction({ name: 'asin', func: Math.asin, arity: 1 });
|
|
64
|
+
this.addMathFunction({ name: 'acos', func: Math.acos, arity: 1 });
|
|
65
|
+
this.addMathFunction({ name: 'atan', func: Math.atan, arity: 1 });
|
|
66
|
+
this.addMathFunction({ name: 'atan2', func: (x, y) => Math.atan2(y ?? 0, x), arity: 2 });
|
|
67
|
+
// Hyperbolic functions
|
|
68
|
+
this.addMathFunction({ name: 'sinh', func: Math.sinh, arity: 1 });
|
|
69
|
+
this.addMathFunction({ name: 'cosh', func: Math.cosh, arity: 1 });
|
|
70
|
+
this.addMathFunction({ name: 'tanh', func: Math.tanh, arity: 1 });
|
|
71
|
+
this.addMathFunction({ name: 'asinh', func: Math.asinh, arity: 1 });
|
|
72
|
+
this.addMathFunction({ name: 'acosh', func: Math.acosh, arity: 1 });
|
|
73
|
+
this.addMathFunction({ name: 'atanh', func: Math.atanh, arity: 1 });
|
|
74
|
+
// Exponential and logarithmic functions
|
|
75
|
+
this.addMathFunction({ name: 'exp', func: Math.exp, arity: 1 });
|
|
76
|
+
this.addMathFunction({ name: 'log', func: Math.log, arity: 1 });
|
|
77
|
+
this.addMathFunction({ name: 'log10', func: Math.log10, arity: 1 });
|
|
78
|
+
this.addMathFunction({ name: 'log2', func: Math.log2, arity: 1 });
|
|
79
|
+
this.addMathFunction({ name: 'pow', func: (x, y) => Math.pow(x, y ?? 0), arity: 2 });
|
|
80
|
+
// Power and root functions
|
|
81
|
+
this.addMathFunction({ name: 'sqrt', func: Math.sqrt, arity: 1 });
|
|
82
|
+
this.addMathFunction({ name: 'cbrt', func: Math.cbrt, arity: 1 });
|
|
83
|
+
// Rounding functions
|
|
84
|
+
this.addMathFunction({ name: 'ceil', func: Math.ceil, arity: 1 });
|
|
85
|
+
this.addMathFunction({ name: 'floor', func: Math.floor, arity: 1 });
|
|
86
|
+
this.addMathFunction({ name: 'round', func: Math.round, arity: 1 });
|
|
87
|
+
this.addMathFunction({ name: 'trunc', func: Math.trunc, arity: 1 });
|
|
88
|
+
// Absolute value and sign
|
|
89
|
+
this.addMathFunction({ name: 'abs', func: Math.abs, arity: 1 });
|
|
90
|
+
this.addMathFunction({ name: 'sign', func: Math.sign, arity: 1 });
|
|
91
|
+
// Random functions
|
|
92
|
+
this.addMathFunction({ name: 'random', func: () => Math.random(), arity: 1 });
|
|
93
|
+
this.addMathFunction({ name: 'rand', func: () => Math.random(), arity: 1 });
|
|
94
|
+
// Constants
|
|
95
|
+
this.addMathFunction({ name: 'pi', func: () => Math.PI, arity: 1 });
|
|
96
|
+
this.addMathFunction({ name: 'e', func: () => Math.E, arity: 1 });
|
|
97
|
+
// Additional utility functions
|
|
98
|
+
this.addMathFunction({ name: 'min', func: (x, y) => Math.min(x, y ?? x), arity: 2 });
|
|
99
|
+
this.addMathFunction({ name: 'max', func: (x, y) => Math.max(x, y ?? x), arity: 2 });
|
|
100
|
+
this.addMathFunction({ name: 'clz32', func: Math.clz32, arity: 1 });
|
|
101
|
+
this.addMathFunction({ name: 'fround', func: Math.fround, arity: 1 });
|
|
102
|
+
this.addMathFunction({ name: 'imul', func: (x, y) => Math.imul(x, y ?? 0), arity: 2 });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Clean expression by removing whitespace and validating characters
|
|
106
|
+
*/
|
|
107
|
+
cleanExpression(expression) {
|
|
108
|
+
// Remove whitespace
|
|
109
|
+
const cleaned = expression.replace(/\s/g, '');
|
|
110
|
+
// Validate characters (allow numbers, operators, parentheses, function names, and dots)
|
|
111
|
+
if (!/^[0-9+\-*/().,a-zA-Z_]+$/.test(cleaned)) {
|
|
112
|
+
throw new Error('Invalid characters in expression');
|
|
113
|
+
}
|
|
114
|
+
return cleaned;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Replace variables in expression (placeholder for future variable support)
|
|
118
|
+
*/
|
|
119
|
+
replaceVariables(expression) {
|
|
120
|
+
// For now, just return the expression as-is
|
|
121
|
+
// In a full implementation, this would replace variables like $x with their values
|
|
122
|
+
return expression;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Replace function calls with their results
|
|
126
|
+
*/
|
|
127
|
+
replaceFunctions(expression) {
|
|
128
|
+
let result = expression;
|
|
129
|
+
// Find function calls like sin(1.5) or pow(2,3)
|
|
130
|
+
const functionRegex = /([a-zA-Z_][a-zA-Z0-9_]*)\(([^)]*)\)/g;
|
|
131
|
+
let match;
|
|
132
|
+
while ((match = functionRegex.exec(expression)) !== null) {
|
|
133
|
+
const [fullMatch, funcName, argsStr] = match;
|
|
134
|
+
const func = this.mathFunctions.get(funcName);
|
|
135
|
+
if (!func) {
|
|
136
|
+
throw new Error(`Unknown function: ${funcName}`);
|
|
137
|
+
}
|
|
138
|
+
const args = this.parseFunctionArguments(argsStr);
|
|
139
|
+
if (args.length !== func.arity) {
|
|
140
|
+
throw new Error(`Function ${funcName} expects ${func.arity} arguments, got ${args.length}`);
|
|
141
|
+
}
|
|
142
|
+
const funcResult = func.func(args[0], args[1]);
|
|
143
|
+
result = result.replace(fullMatch, funcResult.toString());
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parse function arguments
|
|
149
|
+
*/
|
|
150
|
+
parseFunctionArguments(argsStr) {
|
|
151
|
+
if (!argsStr.trim())
|
|
152
|
+
return [];
|
|
153
|
+
const args = [];
|
|
154
|
+
let current = '';
|
|
155
|
+
let parenCount = 0;
|
|
156
|
+
for (let i = 0; i < argsStr.length; i++) {
|
|
157
|
+
const char = argsStr[i];
|
|
158
|
+
if (char === '(') {
|
|
159
|
+
parenCount++;
|
|
160
|
+
current += char;
|
|
161
|
+
}
|
|
162
|
+
else if (char === ')') {
|
|
163
|
+
parenCount--;
|
|
164
|
+
current += char;
|
|
165
|
+
}
|
|
166
|
+
else if (char === ',' && parenCount === 0) {
|
|
167
|
+
args.push(parseFloat(current.trim()));
|
|
168
|
+
current = '';
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
current += char;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (current.trim()) {
|
|
175
|
+
args.push(parseFloat(current.trim()));
|
|
176
|
+
}
|
|
177
|
+
return args;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Evaluate the mathematical expression
|
|
181
|
+
*/
|
|
182
|
+
evaluateExpression(expression) {
|
|
183
|
+
try {
|
|
184
|
+
// Use Function constructor for safe evaluation
|
|
185
|
+
// This is a simplified approach - a full implementation would use a proper parser
|
|
186
|
+
const func = new Function(`return ${expression}`);
|
|
187
|
+
const result = func();
|
|
188
|
+
if (typeof result !== 'number' || isNaN(result)) {
|
|
189
|
+
throw new Error('Invalid expression result');
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw new Error(`Expression evaluation failed: ${error.message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Round number to specified precision
|
|
199
|
+
*/
|
|
200
|
+
roundToPrecision(num) {
|
|
201
|
+
const factor = Math.pow(10, this.precision);
|
|
202
|
+
return Math.round(num * factor) / factor;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Format number with specified precision
|
|
206
|
+
*/
|
|
207
|
+
formatNumber(num, precision) {
|
|
208
|
+
const prec = precision !== undefined ? precision : this.precision;
|
|
209
|
+
return num.toFixed(prec);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Check if a string represents a valid floating point number
|
|
213
|
+
*/
|
|
214
|
+
isValidFloat(str) {
|
|
215
|
+
const num = parseFloat(str);
|
|
216
|
+
return !isNaN(num) && isFinite(num);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Convert string to floating point number
|
|
220
|
+
*/
|
|
221
|
+
parseFloat(str) {
|
|
222
|
+
const num = parseFloat(str);
|
|
223
|
+
if (isNaN(num)) {
|
|
224
|
+
throw new Error(`Invalid floating point number: ${str}`);
|
|
225
|
+
}
|
|
226
|
+
return num;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get mathematical constants
|
|
230
|
+
*/
|
|
231
|
+
getConstants() {
|
|
232
|
+
return {
|
|
233
|
+
pi: Math.PI,
|
|
234
|
+
e: Math.E,
|
|
235
|
+
ln2: Math.LN2,
|
|
236
|
+
ln10: Math.LN10,
|
|
237
|
+
log2e: Math.LOG2E,
|
|
238
|
+
log10e: Math.LOG10E,
|
|
239
|
+
sqrt1_2: Math.SQRT1_2,
|
|
240
|
+
sqrt2: Math.SQRT2,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Evaluate expression with error handling
|
|
245
|
+
*/
|
|
246
|
+
safeEvaluate(expression) {
|
|
247
|
+
try {
|
|
248
|
+
const result = this.evaluate(expression);
|
|
249
|
+
return { success: true, result };
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
return { success: false, error: error.message };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
export default FloatingPointArithmetic;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command History System Implementation
|
|
3
|
+
* Provides ZSH-compatible history functionality
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
export class HistorySystem {
|
|
9
|
+
entries = [];
|
|
10
|
+
currentIndex = -1;
|
|
11
|
+
config;
|
|
12
|
+
isEnabled = true;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = {
|
|
15
|
+
maxSize: 10000,
|
|
16
|
+
filePath: path.join(os.homedir(), '.lsh_history'),
|
|
17
|
+
shareHistory: false,
|
|
18
|
+
ignoreDups: true,
|
|
19
|
+
ignoreSpace: false,
|
|
20
|
+
expireDuplicatesFirst: true,
|
|
21
|
+
...config,
|
|
22
|
+
};
|
|
23
|
+
this.loadHistory();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Add a command to history
|
|
27
|
+
*/
|
|
28
|
+
addCommand(command, exitCode) {
|
|
29
|
+
if (!this.isEnabled)
|
|
30
|
+
return;
|
|
31
|
+
// Skip empty commands
|
|
32
|
+
if (!command.trim())
|
|
33
|
+
return;
|
|
34
|
+
// Skip commands starting with space if ignoreSpace is enabled
|
|
35
|
+
if (this.config.ignoreSpace && command.startsWith(' '))
|
|
36
|
+
return;
|
|
37
|
+
// Remove duplicates if configured
|
|
38
|
+
if (this.config.ignoreDups) {
|
|
39
|
+
this.removeDuplicateCommand(command);
|
|
40
|
+
}
|
|
41
|
+
const entry = {
|
|
42
|
+
lineNumber: this.entries.length + 1,
|
|
43
|
+
command: command.trim(),
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
exitCode,
|
|
46
|
+
};
|
|
47
|
+
this.entries.push(entry);
|
|
48
|
+
// Trim history if it exceeds max size
|
|
49
|
+
if (this.entries.length > this.config.maxSize) {
|
|
50
|
+
this.entries = this.entries.slice(-this.config.maxSize);
|
|
51
|
+
this.renumberEntries();
|
|
52
|
+
}
|
|
53
|
+
this.currentIndex = this.entries.length - 1;
|
|
54
|
+
this.saveHistory();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get history entry by line number
|
|
58
|
+
*/
|
|
59
|
+
getEntry(lineNumber) {
|
|
60
|
+
return this.entries.find(entry => entry.lineNumber === lineNumber);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get history entry by command prefix
|
|
64
|
+
*/
|
|
65
|
+
getEntryByPrefix(prefix) {
|
|
66
|
+
return this.entries
|
|
67
|
+
.slice()
|
|
68
|
+
.reverse()
|
|
69
|
+
.find(entry => entry.command.startsWith(prefix));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get all history entries
|
|
73
|
+
*/
|
|
74
|
+
getAllEntries() {
|
|
75
|
+
return [...this.entries];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Search history for commands matching pattern
|
|
79
|
+
*/
|
|
80
|
+
searchHistory(pattern) {
|
|
81
|
+
const regex = new RegExp(pattern, 'i');
|
|
82
|
+
return this.entries.filter(entry => regex.test(entry.command));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get previous command in history
|
|
86
|
+
*/
|
|
87
|
+
getPreviousCommand() {
|
|
88
|
+
if (this.currentIndex > 0) {
|
|
89
|
+
this.currentIndex--;
|
|
90
|
+
return this.entries[this.currentIndex].command;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get next command in history
|
|
96
|
+
*/
|
|
97
|
+
getNextCommand() {
|
|
98
|
+
if (this.currentIndex < this.entries.length - 1) {
|
|
99
|
+
this.currentIndex++;
|
|
100
|
+
return this.entries[this.currentIndex].command;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Reset history navigation index
|
|
106
|
+
*/
|
|
107
|
+
resetIndex() {
|
|
108
|
+
this.currentIndex = this.entries.length - 1;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Expand history references like !! !n !string
|
|
112
|
+
*/
|
|
113
|
+
expandHistory(command) {
|
|
114
|
+
let result = command;
|
|
115
|
+
// Handle !! (last command)
|
|
116
|
+
result = result.replace(/!!/g, () => {
|
|
117
|
+
const lastEntry = this.entries[this.entries.length - 1];
|
|
118
|
+
return lastEntry ? lastEntry.command : '!!';
|
|
119
|
+
});
|
|
120
|
+
// Handle !n (command number n)
|
|
121
|
+
result = result.replace(/!(\d+)/g, (match, numStr) => {
|
|
122
|
+
const num = parseInt(numStr, 10);
|
|
123
|
+
const entry = this.getEntry(num);
|
|
124
|
+
return entry ? entry.command : match;
|
|
125
|
+
});
|
|
126
|
+
// Handle !string (last command starting with string)
|
|
127
|
+
result = result.replace(/!([a-zA-Z0-9_]+)/g, (match, prefix) => {
|
|
128
|
+
const entry = this.getEntryByPrefix(prefix);
|
|
129
|
+
return entry ? entry.command : match;
|
|
130
|
+
});
|
|
131
|
+
// Handle ^old^new (quick substitution)
|
|
132
|
+
result = result.replace(/\^([^^]+)\^([^^]*)/g, (match, old, replacement) => {
|
|
133
|
+
const lastEntry = this.entries[this.entries.length - 1];
|
|
134
|
+
if (lastEntry) {
|
|
135
|
+
return lastEntry.command.replace(new RegExp(old, 'g'), replacement);
|
|
136
|
+
}
|
|
137
|
+
return match;
|
|
138
|
+
});
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Clear history
|
|
143
|
+
*/
|
|
144
|
+
clearHistory() {
|
|
145
|
+
this.entries = [];
|
|
146
|
+
this.currentIndex = -1;
|
|
147
|
+
this.saveHistory();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get history statistics
|
|
151
|
+
*/
|
|
152
|
+
getStats() {
|
|
153
|
+
const unique = new Set(this.entries.map(e => e.command)).size;
|
|
154
|
+
const oldest = this.entries.length > 0 ? new Date(this.entries[0].timestamp) : null;
|
|
155
|
+
const newest = this.entries.length > 0 ? new Date(this.entries[this.entries.length - 1].timestamp) : null;
|
|
156
|
+
return {
|
|
157
|
+
total: this.entries.length,
|
|
158
|
+
unique,
|
|
159
|
+
oldest,
|
|
160
|
+
newest,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Enable/disable history
|
|
165
|
+
*/
|
|
166
|
+
setEnabled(enabled) {
|
|
167
|
+
this.isEnabled = enabled;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Update configuration
|
|
171
|
+
*/
|
|
172
|
+
updateConfig(newConfig) {
|
|
173
|
+
this.config = { ...this.config, ...newConfig };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Load history from file
|
|
177
|
+
*/
|
|
178
|
+
loadHistory() {
|
|
179
|
+
try {
|
|
180
|
+
if (fs.existsSync(this.config.filePath)) {
|
|
181
|
+
const content = fs.readFileSync(this.config.filePath, 'utf8');
|
|
182
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
183
|
+
this.entries = lines.map((line, index) => {
|
|
184
|
+
// Parse history line format: timestamp:command
|
|
185
|
+
const colonIndex = line.indexOf(':');
|
|
186
|
+
if (colonIndex > 0) {
|
|
187
|
+
const timestamp = parseInt(line.substring(0, colonIndex), 10);
|
|
188
|
+
const command = line.substring(colonIndex + 1);
|
|
189
|
+
return {
|
|
190
|
+
lineNumber: index + 1,
|
|
191
|
+
command,
|
|
192
|
+
timestamp: isNaN(timestamp) ? Date.now() : timestamp,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
return {
|
|
197
|
+
lineNumber: index + 1,
|
|
198
|
+
command: line,
|
|
199
|
+
timestamp: Date.now(),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (_error) {
|
|
206
|
+
// If loading fails, start with empty history
|
|
207
|
+
this.entries = [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Save history to file
|
|
212
|
+
*/
|
|
213
|
+
saveHistory() {
|
|
214
|
+
try {
|
|
215
|
+
const content = this.entries
|
|
216
|
+
.map(entry => `${entry.timestamp}:${entry.command}`)
|
|
217
|
+
.join('\n');
|
|
218
|
+
// Ensure directory exists
|
|
219
|
+
const dir = path.dirname(this.config.filePath);
|
|
220
|
+
if (!fs.existsSync(dir)) {
|
|
221
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
fs.writeFileSync(this.config.filePath, content, 'utf8');
|
|
224
|
+
}
|
|
225
|
+
catch (_error) {
|
|
226
|
+
// Silently fail if we can't save history
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Remove duplicate command from history
|
|
231
|
+
*/
|
|
232
|
+
removeDuplicateCommand(command) {
|
|
233
|
+
const trimmedCommand = command.trim();
|
|
234
|
+
this.entries = this.entries.filter(entry => entry.command !== trimmedCommand);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Renumber entries after trimming
|
|
238
|
+
*/
|
|
239
|
+
renumberEntries() {
|
|
240
|
+
this.entries.forEach((entry, index) => {
|
|
241
|
+
entry.lineNumber = index + 1;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export default HistorySystem;
|