lsh-framework 0.5.13 → 0.7.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.
package/dist/cli.js CHANGED
@@ -36,8 +36,12 @@ function getVersion() {
36
36
  const program = new Command();
37
37
  program
38
38
  .name('lsh')
39
- .description('LSH - A modern shell with ZSH features and superior job management')
40
- .version(getVersion());
39
+ .description('LSH - Encrypted secrets manager with automatic rotation and team sync')
40
+ .version(getVersion())
41
+ .showSuggestionAfterError(true)
42
+ .showHelpAfterError('(add --help for additional information)')
43
+ .allowUnknownOption(false)
44
+ .enablePositionalOptions();
41
45
  // Options for main command
42
46
  program
43
47
  .option('-i, --interactive', 'Start interactive shell')
@@ -64,36 +68,36 @@ program
64
68
  await startInteractiveShell(options);
65
69
  }
66
70
  else {
67
- // No arguments - show commands without verbose options
68
- console.log('LSH - A modern shell with ZSH features and superior job management');
71
+ // No arguments - show secrets-focused help
72
+ console.log('LSH - Encrypted Secrets Manager with Automatic Rotation');
69
73
  console.log('');
70
- console.log('Usage: lsh [options] [command]');
74
+ console.log('🔐 Secrets Management (Primary Features):');
75
+ console.log(' lib secrets sync Check sync status & get recommendations');
76
+ console.log(' lib secrets push Upload .env to encrypted cloud storage');
77
+ console.log(' lib secrets pull Download .env from cloud storage');
78
+ console.log(' lib secrets list List all stored environments');
79
+ console.log(' lib secrets show View secrets (masked)');
80
+ console.log(' lib secrets key Generate encryption key');
81
+ console.log(' lib secrets create Create new .env file');
71
82
  console.log('');
72
- console.log('Commands:');
73
- console.log(' repl JavaScript REPL interactive shell');
74
- console.log(' script <file> Execute shell script');
75
- console.log(' config Manage configuration');
76
- console.log(' zsh ZSH compatibility commands');
77
- console.log(' help Show detailed help');
83
+ console.log('🔄 Automation (Schedule secret rotation):');
84
+ console.log(' lib cron add Schedule automatic tasks');
85
+ console.log(' lib cron list List scheduled jobs');
86
+ console.log(' lib daemon start Start persistent daemon');
78
87
  console.log('');
79
- console.log('Self-Management:');
80
- console.log(' self update Update to latest version');
81
- console.log(' self version Show version information');
82
- console.log(' self uninstall Uninstall LSH from system');
83
- console.log(' self theme Manage themes');
84
- console.log(' self zsh-import Import ZSH configs');
88
+ console.log('🚀 Quick Start:');
89
+ console.log(' lsh lib secrets key # Generate encryption key');
90
+ console.log(' lsh lib secrets push --env dev # Push your secrets');
91
+ console.log(' lsh lib secrets pull --env dev # Pull on another machine');
85
92
  console.log('');
86
- console.log('Library Services:');
93
+ console.log('📚 More Commands:');
87
94
  console.log(' lib api API server management');
88
- console.log(' lib daemon Daemon management');
89
- console.log(' lib cron Cron job management');
90
- console.log(' lib secrets Secrets management');
91
95
  console.log(' lib supabase Supabase database management');
96
+ console.log(' self Self-management commands');
97
+ console.log(' -i, --interactive Start interactive shell');
98
+ console.log(' --help Show all options');
92
99
  console.log('');
93
- console.log('Quick Start:');
94
- console.log(' lsh -i Start interactive shell');
95
- console.log(' lsh --help Show all options');
96
- console.log(' lsh help Show detailed help with examples');
100
+ console.log('📖 Documentation: https://github.com/gwicho38/lsh');
97
101
  }
98
102
  }
99
103
  catch (error) {
@@ -158,6 +162,50 @@ program
158
162
  .action(() => {
159
163
  showDetailedHelp();
160
164
  });
165
+ /**
166
+ * Calculate string similarity (Levenshtein distance)
167
+ */
168
+ function similarity(s1, s2) {
169
+ const longer = s1.length > s2.length ? s1 : s2;
170
+ const shorter = s1.length > s2.length ? s2 : s1;
171
+ if (longer.length === 0)
172
+ return 1.0;
173
+ const editDistance = levenshtein(longer, shorter);
174
+ return (longer.length - editDistance) / longer.length;
175
+ }
176
+ function levenshtein(s1, s2) {
177
+ const costs = [];
178
+ for (let i = 0; i <= s1.length; i++) {
179
+ let lastValue = i;
180
+ for (let j = 0; j <= s2.length; j++) {
181
+ if (i === 0) {
182
+ costs[j] = j;
183
+ }
184
+ else if (j > 0) {
185
+ let newValue = costs[j - 1];
186
+ if (s1.charAt(i - 1) !== s2.charAt(j - 1)) {
187
+ newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
188
+ }
189
+ costs[j - 1] = lastValue;
190
+ lastValue = newValue;
191
+ }
192
+ }
193
+ if (i > 0)
194
+ costs[s2.length] = lastValue;
195
+ }
196
+ return costs[s2.length];
197
+ }
198
+ /**
199
+ * Find similar commands for suggestions
200
+ */
201
+ function findSimilarCommands(input, validCommands) {
202
+ const similarities = validCommands
203
+ .map(cmd => ({ cmd, score: similarity(input, cmd) }))
204
+ .filter(item => item.score > 0.5) // Only suggest if similarity > 50%
205
+ .sort((a, b) => b.score - a.score)
206
+ .slice(0, 3); // Top 3 suggestions
207
+ return similarities.map(item => item.cmd);
208
+ }
161
209
  // Register async command modules
162
210
  (async () => {
163
211
  // REPL interactive shell
@@ -173,6 +221,63 @@ program
173
221
  // Self-management commands with nested utilities
174
222
  registerZshImportCommands(selfCommand);
175
223
  registerThemeCommands(selfCommand);
224
+ // Pre-parse check for unknown commands
225
+ const args = process.argv.slice(2);
226
+ if (args.length > 0) {
227
+ const firstArg = args[0];
228
+ const validCommands = program.commands.map(cmd => cmd.name());
229
+ const validOptions = ['-i', '--interactive', '-c', '--command', '-s', '--script',
230
+ '--rc', '--zsh-compat', '--source-zshrc', '--package-manager',
231
+ '-v', '--verbose', '-d', '--debug', '-h', '--help', '-V', '--version'];
232
+ // Check if first argument looks like a command but isn't valid
233
+ if (!firstArg.startsWith('-') &&
234
+ !validCommands.includes(firstArg) &&
235
+ !validOptions.some(opt => args.includes(opt))) {
236
+ const suggestions = findSimilarCommands(firstArg, validCommands);
237
+ console.error(`error: unknown command '${firstArg}'`);
238
+ if (suggestions.length > 0) {
239
+ console.error(`\nDid you mean one of these?`);
240
+ suggestions.forEach(cmd => console.error(` ${cmd}`));
241
+ }
242
+ console.error(`\nRun 'lsh --help' to see available commands.`);
243
+ process.exit(1);
244
+ }
245
+ }
246
+ // Configure custom error output for better suggestions
247
+ program.configureOutput({
248
+ writeErr: (str) => {
249
+ // Intercept error messages to add suggestions
250
+ if (str.includes('error: unknown command')) {
251
+ const match = str.match(/unknown command '([^']+)'/);
252
+ if (match) {
253
+ const unknownCommand = match[1];
254
+ const validCommands = program.commands.map(cmd => cmd.name());
255
+ const suggestions = findSimilarCommands(unknownCommand, validCommands);
256
+ process.stderr.write(str);
257
+ if (suggestions.length > 0) {
258
+ process.stderr.write(`\nDid you mean one of these?\n`);
259
+ suggestions.forEach(cmd => process.stderr.write(` ${cmd}\n`));
260
+ }
261
+ process.stderr.write(`\nRun 'lsh --help' to see available commands.\n`);
262
+ return;
263
+ }
264
+ }
265
+ process.stderr.write(str);
266
+ }
267
+ });
268
+ // Add custom error handler for unknown commands
269
+ program.on('command:*', (operands) => {
270
+ const unknownCommand = operands[0];
271
+ const validCommands = program.commands.map(cmd => cmd.name());
272
+ const suggestions = findSimilarCommands(unknownCommand, validCommands);
273
+ console.error(`error: unknown command '${unknownCommand}'`);
274
+ if (suggestions.length > 0) {
275
+ console.error(`\nDid you mean one of these?`);
276
+ suggestions.forEach(cmd => console.error(` ${cmd}`));
277
+ }
278
+ console.error(`\nRun 'lsh --help' to see available commands.`);
279
+ process.exit(1);
280
+ });
176
281
  // Parse command line arguments after all commands are registered
177
282
  program.parse(process.argv);
178
283
  })();
@@ -61,7 +61,8 @@ export function registerThemeCommands(program) {
61
61
  console.log(chalk.dim(' lsh theme import <name> # For Oh-My-Zsh themes'));
62
62
  console.log(chalk.dim(' lsh theme apply <name>'));
63
63
  console.log('');
64
- process.exit(0);
64
+ // Note: Removed process.exit(0) to allow proper Jest testing
65
+ // Commander will handle exit automatically
65
66
  });
66
67
  themeCommand
67
68
  .command('import <name>')
@@ -203,7 +203,8 @@ export class BaseJobManager extends EventEmitter {
203
203
  async removeJob(jobId, force = false) {
204
204
  const job = await this.getJob(jobId);
205
205
  if (!job) {
206
- throw new Error(`Job ${jobId} not found`);
206
+ // Return false instead of throwing when job doesn't exist
207
+ return false;
207
208
  }
208
209
  // Check if job is running
209
210
  if (job.status === 'running' && !force) {
@@ -14,13 +14,54 @@ export class JobManager extends BaseJobManager {
14
14
  nextJobId = 1;
15
15
  persistenceFile;
16
16
  schedulerInterval;
17
+ initPromise;
17
18
  constructor(persistenceFile = '/tmp/lsh-jobs.json') {
18
19
  super(new MemoryJobStorage(), 'JobManager');
19
20
  this.persistenceFile = persistenceFile;
20
- this.loadPersistedJobs();
21
+ this.initPromise = this.loadPersistedJobs();
21
22
  this.startScheduler();
22
23
  this.setupCleanupHandlers();
23
24
  }
25
+ /**
26
+ * Wait for initialization to complete
27
+ */
28
+ async ready() {
29
+ await this.initPromise;
30
+ }
31
+ /**
32
+ * Create a job and persist to filesystem
33
+ */
34
+ async createJob(spec) {
35
+ const job = await super.createJob(spec);
36
+ await this.persistJobs();
37
+ return job;
38
+ }
39
+ /**
40
+ * Update a job and persist to filesystem
41
+ */
42
+ async updateJob(jobId, updates) {
43
+ const job = await super.updateJob(jobId, updates);
44
+ await this.persistJobs();
45
+ return job;
46
+ }
47
+ /**
48
+ * Remove a job and persist to filesystem
49
+ */
50
+ async removeJob(jobId, force = false) {
51
+ const result = await super.removeJob(jobId, force);
52
+ if (result) {
53
+ await this.persistJobs();
54
+ }
55
+ return result;
56
+ }
57
+ /**
58
+ * Update job status and persist to filesystem
59
+ */
60
+ async updateJobStatus(jobId, status, additionalUpdates) {
61
+ const job = await super.updateJobStatus(jobId, status, additionalUpdates);
62
+ await this.persistJobs();
63
+ return job;
64
+ }
24
65
  /**
25
66
  * Start a job (execute it as a process)
26
67
  */
@@ -67,14 +108,19 @@ export class JobManager extends BaseJobManager {
67
108
  this.emit('jobOutput', job.id, 'stderr', data.toString());
68
109
  });
69
110
  // Handle completion
70
- job.process.on('exit', (code, signal) => {
111
+ job.process.on('exit', async (code, signal) => {
112
+ // Check if job still exists (might have been removed during cleanup)
113
+ const existingJob = await this.getJob(job.id);
114
+ if (!existingJob) {
115
+ this.logger.debug(`Job ${job.id} already removed, skipping status update`);
116
+ return;
117
+ }
71
118
  const status = code === 0 ? 'completed' : (signal === 'SIGKILL' ? 'killed' : 'failed');
72
- this.updateJobStatus(job.id, status, {
119
+ await this.updateJobStatus(job.id, status, {
73
120
  completedAt: new Date(),
74
121
  exitCode: code || undefined,
75
122
  });
76
123
  this.emit('jobCompleted', job, code, signal);
77
- this.persistJobs();
78
124
  });
79
125
  // Set timeout if specified
80
126
  if (job.timeout) {
@@ -87,7 +133,6 @@ export class JobManager extends BaseJobManager {
87
133
  startedAt: new Date(),
88
134
  pid: job.pid,
89
135
  });
90
- await this.persistJobs();
91
136
  return updatedJob;
92
137
  }
93
138
  catch (error) {
@@ -96,7 +141,6 @@ export class JobManager extends BaseJobManager {
96
141
  stderr: error.message,
97
142
  });
98
143
  this.emit('jobFailed', job, error);
99
- await this.persistJobs();
100
144
  throw error;
101
145
  }
102
146
  }
@@ -131,7 +175,6 @@ export class JobManager extends BaseJobManager {
131
175
  const updatedJob = await this.updateJobStatus(jobId, 'stopped', {
132
176
  completedAt: new Date(),
133
177
  });
134
- await this.persistJobs();
135
178
  return updatedJob;
136
179
  }
137
180
  /**
@@ -289,6 +332,11 @@ export class JobManager extends BaseJobManager {
289
332
  try {
290
333
  if (fs.existsSync(this.persistenceFile)) {
291
334
  const data = fs.readFileSync(this.persistenceFile, 'utf8');
335
+ // Handle empty file
336
+ if (!data || data.trim() === '') {
337
+ this.logger.info('Persistence file is empty, starting fresh');
338
+ return;
339
+ }
292
340
  const persistedJobs = JSON.parse(data);
293
341
  for (const job of persistedJobs) {
294
342
  // Convert date strings back to Date objects
@@ -11,7 +11,9 @@ const __dirname = path.dirname(__filename);
11
11
  export class LshrcManager {
12
12
  lshrcPath;
13
13
  constructor(lshrcPath) {
14
- this.lshrcPath = lshrcPath || path.join(os.homedir(), '.lshrc');
14
+ // Use process.env.HOME if set (for testability), fallback to os.homedir()
15
+ const homeDir = process.env.HOME || os.homedir();
16
+ this.lshrcPath = lshrcPath || path.join(homeDir, '.lshrc');
15
17
  }
16
18
  /**
17
19
  * Initialize .lshrc if it doesn't exist
@@ -3,6 +3,7 @@
3
3
  * Sync .env files across machines using encrypted Supabase storage
4
4
  */
5
5
  import * as fs from 'fs';
6
+ import * as path from 'path';
6
7
  import * as crypto from 'crypto';
7
8
  import DatabasePersistence from './database-persistence.js';
8
9
  import { createLogger } from './logger.js';
@@ -212,5 +213,131 @@ export class SecretsManager {
212
213
  }
213
214
  console.log();
214
215
  }
216
+ /**
217
+ * Get status of secrets for an environment
218
+ */
219
+ async status(envFilePath = '.env', environment = 'dev') {
220
+ const status = {
221
+ localExists: false,
222
+ localKeys: 0,
223
+ localModified: undefined,
224
+ cloudExists: false,
225
+ cloudKeys: 0,
226
+ cloudModified: undefined,
227
+ keySet: !!process.env.LSH_SECRETS_KEY,
228
+ keyMatches: undefined,
229
+ suggestions: [],
230
+ };
231
+ // Check local file
232
+ if (fs.existsSync(envFilePath)) {
233
+ status.localExists = true;
234
+ const stat = fs.statSync(envFilePath);
235
+ status.localModified = stat.mtime;
236
+ const content = fs.readFileSync(envFilePath, 'utf8');
237
+ const env = this.parseEnvFile(content);
238
+ status.localKeys = Object.keys(env).length;
239
+ }
240
+ // Check cloud storage
241
+ try {
242
+ const jobs = await this.persistence.getActiveJobs();
243
+ const secretsJobs = jobs
244
+ .filter(j => j.command === 'secrets_sync' && j.job_id.includes(environment))
245
+ .sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime());
246
+ if (secretsJobs.length > 0) {
247
+ status.cloudExists = true;
248
+ const latestSecret = secretsJobs[0];
249
+ status.cloudModified = new Date(latestSecret.completed_at || latestSecret.started_at);
250
+ // Try to decrypt to check if key matches
251
+ if (latestSecret.output) {
252
+ try {
253
+ const decrypted = this.decrypt(latestSecret.output);
254
+ const env = this.parseEnvFile(decrypted);
255
+ status.cloudKeys = Object.keys(env).length;
256
+ status.keyMatches = true;
257
+ }
258
+ catch (error) {
259
+ status.keyMatches = false;
260
+ }
261
+ }
262
+ }
263
+ }
264
+ catch (error) {
265
+ // Cloud check failed, likely no connection
266
+ }
267
+ return status;
268
+ }
269
+ /**
270
+ * Sync command - check status and suggest actions
271
+ */
272
+ async sync(envFilePath = '.env', environment = 'dev') {
273
+ console.log(`\n🔍 Checking secrets status for environment: ${environment}\n`);
274
+ const status = await this.status(envFilePath, environment);
275
+ // Display status
276
+ console.log('📊 Status:');
277
+ console.log(` Encryption key set: ${status.keySet ? '✅' : '❌'}`);
278
+ console.log(` Local .env file: ${status.localExists ? `✅ (${status.localKeys} keys)` : '❌'}`);
279
+ console.log(` Cloud storage: ${status.cloudExists ? `✅ (${status.cloudKeys} keys)` : '❌'}`);
280
+ if (status.cloudExists && status.keyMatches !== undefined) {
281
+ console.log(` Key matches cloud: ${status.keyMatches ? '✅' : '❌'}`);
282
+ }
283
+ console.log();
284
+ // Generate suggestions
285
+ const suggestions = [];
286
+ if (!status.keySet) {
287
+ suggestions.push('⚠️ No encryption key set!');
288
+ suggestions.push(' Generate a key: lsh lib secrets key');
289
+ suggestions.push(' Add it to .env: LSH_SECRETS_KEY=<your-key>');
290
+ suggestions.push(' Load it: export $(cat .env | xargs)');
291
+ }
292
+ if (status.cloudExists && status.keyMatches === false) {
293
+ suggestions.push('⚠️ Encryption key does not match cloud storage!');
294
+ suggestions.push(' Either use the original key, or push new secrets:');
295
+ suggestions.push(` lsh lib secrets push -f ${envFilePath} -e ${environment}`);
296
+ }
297
+ if (!status.localExists && status.cloudExists && status.keyMatches) {
298
+ suggestions.push('💡 Cloud secrets available but no local file');
299
+ suggestions.push(` Pull from cloud: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
300
+ }
301
+ if (status.localExists && !status.cloudExists) {
302
+ suggestions.push('💡 Local .env exists but not in cloud');
303
+ suggestions.push(` Push to cloud: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
304
+ }
305
+ if (status.localExists && status.cloudExists && status.keyMatches) {
306
+ if (status.localModified && status.cloudModified) {
307
+ const localNewer = status.localModified > status.cloudModified;
308
+ const timeDiff = Math.abs(status.localModified.getTime() - status.cloudModified.getTime());
309
+ const daysDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
310
+ if (localNewer && daysDiff > 0) {
311
+ suggestions.push('💡 Local file is newer than cloud');
312
+ suggestions.push(` Push to cloud: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
313
+ }
314
+ else if (!localNewer && daysDiff > 0) {
315
+ suggestions.push('💡 Cloud is newer than local file');
316
+ suggestions.push(` Pull from cloud: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
317
+ }
318
+ else {
319
+ suggestions.push('✅ Local and cloud are in sync!');
320
+ }
321
+ }
322
+ }
323
+ // Show how to load secrets in current shell
324
+ if (status.localExists && status.keySet) {
325
+ suggestions.push('');
326
+ suggestions.push('📝 To load secrets in your current shell:');
327
+ suggestions.push(` export $(cat ${envFilePath} | grep -v '^#' | xargs)`);
328
+ suggestions.push('');
329
+ suggestions.push(' Or for safer loading (with quotes):');
330
+ suggestions.push(` set -a; source ${envFilePath}; set +a`);
331
+ suggestions.push('');
332
+ suggestions.push('💡 Add to your shell profile for auto-loading:');
333
+ suggestions.push(` echo "set -a; source ${path.resolve(envFilePath)}; set +a" >> ~/.zshrc`);
334
+ }
335
+ // Display suggestions
336
+ if (suggestions.length > 0) {
337
+ console.log('📋 Recommendations:\n');
338
+ suggestions.forEach(s => console.log(s));
339
+ console.log();
340
+ }
341
+ }
215
342
  }
216
343
  export default SecretsManager;
@@ -154,6 +154,39 @@ API_KEY=
154
154
  process.exit(1);
155
155
  }
156
156
  });
157
+ // Sync command - check status and suggest actions
158
+ secretsCmd
159
+ .command('sync')
160
+ .description('Check secrets sync status and show recommended actions')
161
+ .option('-f, --file <path>', 'Path to .env file', '.env')
162
+ .option('-e, --env <name>', 'Environment name', 'dev')
163
+ .action(async (options) => {
164
+ try {
165
+ const manager = new SecretsManager();
166
+ await manager.sync(options.file, options.env);
167
+ }
168
+ catch (error) {
169
+ console.error('❌ Failed to check sync status:', error.message);
170
+ process.exit(1);
171
+ }
172
+ });
173
+ // Status command - get detailed status info
174
+ secretsCmd
175
+ .command('status')
176
+ .description('Get detailed secrets status (JSON output)')
177
+ .option('-f, --file <path>', 'Path to .env file', '.env')
178
+ .option('-e, --env <name>', 'Environment name', 'dev')
179
+ .action(async (options) => {
180
+ try {
181
+ const manager = new SecretsManager();
182
+ const status = await manager.status(options.file, options.env);
183
+ console.log(JSON.stringify(status, null, 2));
184
+ }
185
+ catch (error) {
186
+ console.error('❌ Failed to get status:', error.message);
187
+ process.exit(1);
188
+ }
189
+ });
157
190
  // Delete .env file with confirmation
158
191
  secretsCmd
159
192
  .command('delete')
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "0.5.13",
4
- "description": "A powerful, extensible shell with advanced job management, database persistence, and modern CLI features",
3
+ "version": "0.7.0",
4
+ "description": "Encrypted secrets manager with automatic rotation, team sync, and multi-environment support. Built on a powerful shell with daemon scheduling and CI/CD integration.",
5
5
  "main": "dist/app.js",
6
6
  "bin": {
7
7
  "lsh": "./dist/cli.js"
@@ -36,21 +36,25 @@
36
36
  "audit:security": "npm audit --audit-level moderate"
37
37
  },
38
38
  "keywords": [
39
- "cli",
39
+ "secrets-manager",
40
+ "secrets",
41
+ "env-manager",
42
+ "dotenv",
43
+ "environment-variables",
44
+ "encryption",
45
+ "credential-management",
46
+ "team-sync",
47
+ "secrets-rotation",
48
+ "multi-environment",
49
+ "devops",
50
+ "security",
40
51
  "shell",
41
- "terminal",
42
- "job-manager",
52
+ "automation",
43
53
  "cron",
44
54
  "daemon",
45
- "zsh",
46
- "bash",
47
- "posix",
48
- "database-persistence",
49
- "task-scheduler",
50
- "command-line",
51
- "automation",
52
- "devops",
53
- "cicd"
55
+ "job-scheduler",
56
+ "cicd",
57
+ "cli"
54
58
  ],
55
59
  "engines": {
56
60
  "node": ">=20.18.0",
@@ -138,7 +142,7 @@
138
142
  "eslint-plugin-react": "^7.37.5",
139
143
  "eslint-plugin-react-hooks": "^5.2.0",
140
144
  "supertest": "^7.1.4",
141
- "ts-jest": "^29.4.4",
145
+ "ts-jest": "^29.2.5",
142
146
  "typescript": "^5.4.5"
143
147
  }
144
148
  }