lsh-framework 0.8.3 → 0.9.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.
@@ -29,8 +29,12 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
29
29
  this.logInfo('Daemon Status:');
30
30
  this.logInfo(` PID: ${status.pid}`);
31
31
  this.logInfo(` Uptime: ${Math.floor(status.uptime / 60)} minutes`);
32
- this.logInfo(` Memory: ${Math.round(status.memoryUsage.heapUsed / 1024 / 1024)} MB`);
33
- this.logInfo(` Jobs: ${status.jobs.total} total, ${status.jobs.running} running`);
32
+ if (status.memoryUsage) {
33
+ this.logInfo(` Memory: ${Math.round(status.memoryUsage.heapUsed / 1024 / 1024)} MB`);
34
+ }
35
+ if (status.jobs) {
36
+ this.logInfo(` Jobs: ${status.jobs.total} total, ${status.jobs.running} running`);
37
+ }
34
38
  }
35
39
  });
36
40
  // Start command
@@ -95,7 +99,8 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
95
99
  { flags: '-f, --force', description: 'Force cleanup without prompts', defaultValue: false }
96
100
  ],
97
101
  action: async (options) => {
98
- await this.cleanupDaemon(options.force);
102
+ const opts = options;
103
+ await this.cleanupDaemon(opts.force);
99
104
  }
100
105
  });
101
106
  }
@@ -122,11 +127,13 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
122
127
  { flags: '--no-database-sync', description: 'Disable database synchronization' }
123
128
  ],
124
129
  action: async (options) => {
125
- if (!options.name || !options.command || (!options.schedule && !options.interval)) {
130
+ const opts = options;
131
+ if (!opts.name || !opts.command || (!opts.schedule && !opts.interval)) {
126
132
  throw new Error('Missing required options: --name, --command, and (--schedule or --interval)');
127
133
  }
128
- const jobSpec = this.createJobSpec(options);
134
+ const jobSpec = this.createJobSpec(opts);
129
135
  await this.withDaemonAction(async (client) => {
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
137
  await client.createDatabaseCronJob(jobSpec);
131
138
  });
132
139
  this.logSuccess('Job created successfully:');
@@ -145,8 +152,9 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
145
152
  { flags: '-f, --filter <filter>', description: 'Filter jobs by status' }
146
153
  ],
147
154
  action: async (options) => {
155
+ const opts = options;
148
156
  const jobs = await this.withDaemonAction(async (client) => {
149
- return await client.listJobs(options.filter ? { status: options.filter } : undefined);
157
+ return await client.listJobs(opts.filter ? { status: opts.filter } : undefined);
150
158
  });
151
159
  this.displayJobs(jobs);
152
160
  }
@@ -185,8 +193,9 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
185
193
  { flags: '-f, --filter <status>', description: 'Filter by job status', defaultValue: 'created' }
186
194
  ],
187
195
  action: async (options) => {
196
+ const opts = options;
188
197
  await this.withDaemonAction(async (client) => {
189
- const jobs = await client.listJobs({ status: options.filter });
198
+ const jobs = await client.listJobs({ status: opts.filter });
190
199
  this.logInfo(`Triggering ${jobs.length} jobs...`);
191
200
  for (const job of jobs) {
192
201
  try {
@@ -215,10 +224,11 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
215
224
  { flags: '-s, --signal <signal>', description: 'Signal to send', defaultValue: 'SIGTERM' }
216
225
  ],
217
226
  action: async (jobId, options) => {
227
+ const opts = options;
218
228
  await this.withDaemonAction(async (client) => {
219
- await client.stopJob(jobId, options.signal);
229
+ await client.stopJob(jobId, opts.signal);
220
230
  });
221
- this.logSuccess(`Job ${jobId} stopped with signal ${options.signal}`);
231
+ this.logSuccess(`Job ${jobId} stopped with signal ${opts.signal}`);
222
232
  }
223
233
  });
224
234
  // Remove job
@@ -230,8 +240,9 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
230
240
  { flags: '-f, --force', description: 'Force removal', defaultValue: false }
231
241
  ],
232
242
  action: async (jobId, options) => {
243
+ const opts = options;
233
244
  await this.withDaemonAction(async (client) => {
234
- await client.removeJob(jobId, options.force);
245
+ await client.removeJob(jobId, opts.force);
235
246
  });
236
247
  this.logSuccess(`Job ${jobId} removed`);
237
248
  }
@@ -275,7 +286,8 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
275
286
  { flags: '-l, --limit <limit>', description: 'Limit number of results', defaultValue: '50' }
276
287
  ],
277
288
  action: async (options) => {
278
- const jobs = await this.withDaemonAction(async (client) => await client.getJobHistory(options.jobId, parseInt(options.limit)), { forUser: true, requireRunning: false });
289
+ const opts = options;
290
+ const jobs = await this.withDaemonAction(async (client) => await client.getJobHistory(opts.jobId, parseInt(opts.limit)), { forUser: true, requireRunning: false });
279
291
  this.logInfo(`Job History (${jobs.length} records):`);
280
292
  jobs.forEach(job => {
281
293
  const started = new Date(job.started_at).toLocaleString();
@@ -297,7 +309,8 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
297
309
  { flags: '-j, --job-id <jobId>', description: 'Filter by job ID' }
298
310
  ],
299
311
  action: async (options) => {
300
- const stats = await this.withDaemonAction(async (client) => await client.getJobStatistics(options.jobId), { forUser: true, requireRunning: false });
312
+ const opts = options;
313
+ const stats = await this.withDaemonAction(async (client) => await client.getJobStatistics(opts.jobId), { forUser: true, requireRunning: false });
301
314
  this.logInfo('Job Statistics:');
302
315
  this.logInfo(` Total Jobs: ${stats.totalJobs}`);
303
316
  this.logInfo(` Success Rate: ${stats.successRate.toFixed(1)}%`);
@@ -316,7 +329,8 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
316
329
  { flags: '-l, --limit <limit>', description: 'Number of recent executions to show', defaultValue: '5' }
317
330
  ],
318
331
  action: async (options) => {
319
- const jobs = await this.withDaemonAction(async (client) => await client.getJobHistory(undefined, parseInt(options.limit)), { forUser: true });
332
+ const opts = options;
333
+ const jobs = await this.withDaemonAction(async (client) => await client.getJobHistory(undefined, parseInt(opts.limit)), { forUser: true });
320
334
  this.logInfo(`Recent Job Executions (${jobs.length} records):`);
321
335
  jobs.forEach((job, index) => {
322
336
  const started = new Date(job.started_at).toLocaleString();
@@ -7,27 +7,26 @@ import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import * as readline from 'readline';
9
9
  export async function init_secrets(program) {
10
- const secretsCmd = program
11
- .command('secrets')
12
- .description('Manage environment secrets across machines');
13
10
  // Push secrets to cloud
14
- secretsCmd
11
+ program
15
12
  .command('push')
16
13
  .description('Push local .env to encrypted cloud storage')
17
14
  .option('-f, --file <path>', 'Path to .env file', '.env')
18
15
  .option('-e, --env <name>', 'Environment name (dev/staging/prod)', 'dev')
16
+ .option('--force', 'Force push even if destructive changes detected')
19
17
  .action(async (options) => {
20
18
  try {
21
19
  const manager = new SecretsManager();
22
- await manager.push(options.file, options.env);
20
+ await manager.push(options.file, options.env, options.force);
23
21
  }
24
22
  catch (error) {
25
- console.error('❌ Failed to push secrets:', error.message);
23
+ const err = error;
24
+ console.error('❌ Failed to push secrets:', err.message);
26
25
  process.exit(1);
27
26
  }
28
27
  });
29
28
  // Pull secrets from cloud
30
- secretsCmd
29
+ program
31
30
  .command('pull')
32
31
  .description('Pull .env from encrypted cloud storage')
33
32
  .option('-f, --file <path>', 'Path to .env file', '.env')
@@ -39,12 +38,13 @@ export async function init_secrets(program) {
39
38
  await manager.pull(options.file, options.env, options.force);
40
39
  }
41
40
  catch (error) {
42
- console.error('❌ Failed to pull secrets:', error.message);
41
+ const err = error;
42
+ console.error('❌ Failed to pull secrets:', err.message);
43
43
  process.exit(1);
44
44
  }
45
45
  });
46
46
  // List environments
47
- secretsCmd
47
+ program
48
48
  .command('list [environment]')
49
49
  .alias('ls')
50
50
  .description('List all stored environments or show secrets for specific environment')
@@ -56,7 +56,7 @@ export async function init_secrets(program) {
56
56
  if (options.allFiles) {
57
57
  const files = await manager.listAllFiles();
58
58
  if (files.length === 0) {
59
- console.log('No .env files found. Push your first file with: lsh secrets push --file <filename>');
59
+ console.log('No .env files found. Push your first file with: lsh push --file <filename>');
60
60
  return;
61
61
  }
62
62
  console.log('\n📦 Tracked .env files:\n');
@@ -74,7 +74,7 @@ export async function init_secrets(program) {
74
74
  // Otherwise, list all environments
75
75
  const envs = await manager.listEnvironments();
76
76
  if (envs.length === 0) {
77
- console.log('No environments found. Push your first .env with: lsh secrets push');
77
+ console.log('No environments found. Push your first .env with: lsh push');
78
78
  return;
79
79
  }
80
80
  console.log('\n📦 Available environments:\n');
@@ -84,12 +84,13 @@ export async function init_secrets(program) {
84
84
  console.log();
85
85
  }
86
86
  catch (error) {
87
- console.error('❌ Failed to list environments:', error.message);
87
+ const err = error;
88
+ console.error('❌ Failed to list environments:', err.message);
88
89
  process.exit(1);
89
90
  }
90
91
  });
91
92
  // Show secrets (masked)
92
- secretsCmd
93
+ program
93
94
  .command('show')
94
95
  .description('Show secrets for an environment (masked)')
95
96
  .option('-e, --env <name>', 'Environment name', 'dev')
@@ -99,12 +100,13 @@ export async function init_secrets(program) {
99
100
  await manager.show(options.env);
100
101
  }
101
102
  catch (error) {
102
- console.error('❌ Failed to show secrets:', error.message);
103
+ const err = error;
104
+ console.error('❌ Failed to show secrets:', err.message);
103
105
  process.exit(1);
104
106
  }
105
107
  });
106
108
  // Generate encryption key
107
- secretsCmd
109
+ program
108
110
  .command('key')
109
111
  .description('Generate a new encryption key')
110
112
  .action(async () => {
@@ -116,7 +118,7 @@ export async function init_secrets(program) {
116
118
  console.log(' Never commit it to git!\n');
117
119
  });
118
120
  // Create .env file
119
- secretsCmd
121
+ program
120
122
  .command('create')
121
123
  .description('Create a new .env file')
122
124
  .option('-f, --file <path>', 'Path to .env file', '.env')
@@ -165,12 +167,13 @@ API_KEY=
165
167
  console.log('');
166
168
  }
167
169
  catch (error) {
168
- console.error('❌ Failed to create .env file:', error.message);
170
+ const err = error;
171
+ console.error('❌ Failed to create .env file:', err.message);
169
172
  process.exit(1);
170
173
  }
171
174
  });
172
175
  // Sync command - automatically set up and synchronize secrets
173
- secretsCmd
176
+ program
174
177
  .command('sync')
175
178
  .description('Automatically set up and synchronize secrets (smart mode)')
176
179
  .option('-f, --file <path>', 'Path to .env file', '.env')
@@ -178,6 +181,7 @@ API_KEY=
178
181
  .option('--dry-run', 'Show what would be done without executing')
179
182
  .option('--legacy', 'Use legacy sync mode (suggestions only)')
180
183
  .option('--load', 'Output eval-able export commands for loading secrets')
184
+ .option('--force', 'Force sync even if destructive changes detected')
181
185
  .action(async (options) => {
182
186
  try {
183
187
  const manager = new SecretsManager();
@@ -187,16 +191,17 @@ API_KEY=
187
191
  }
188
192
  else {
189
193
  // Use new smart sync (auto-execute)
190
- await manager.smartSync(options.file, options.env, !options.dryRun, options.load);
194
+ await manager.smartSync(options.file, options.env, !options.dryRun, options.load, options.force);
191
195
  }
192
196
  }
193
197
  catch (error) {
194
- console.error('❌ Failed to sync:', error.message);
198
+ const err = error;
199
+ console.error('❌ Failed to sync:', err.message);
195
200
  process.exit(1);
196
201
  }
197
202
  });
198
203
  // Status command - get detailed status info
199
- secretsCmd
204
+ program
200
205
  .command('status')
201
206
  .description('Get detailed secrets status (JSON output)')
202
207
  .option('-f, --file <path>', 'Path to .env file', '.env')
@@ -208,12 +213,13 @@ API_KEY=
208
213
  console.log(JSON.stringify(status, null, 2));
209
214
  }
210
215
  catch (error) {
211
- console.error('❌ Failed to get status:', error.message);
216
+ const err = error;
217
+ console.error('❌ Failed to get status:', err.message);
212
218
  process.exit(1);
213
219
  }
214
220
  });
215
221
  // Get a specific secret value
216
- secretsCmd
222
+ program
217
223
  .command('get <key>')
218
224
  .description('Get a specific secret value from .env file')
219
225
  .option('-f, --file <path>', 'Path to .env file', '.env')
@@ -245,12 +251,13 @@ API_KEY=
245
251
  process.exit(1);
246
252
  }
247
253
  catch (error) {
248
- console.error('❌ Failed to get secret:', error.message);
254
+ const err = error;
255
+ console.error('❌ Failed to get secret:', err.message);
249
256
  process.exit(1);
250
257
  }
251
258
  });
252
259
  // Set a specific secret value
253
- secretsCmd
260
+ program
254
261
  .command('set <key> <value>')
255
262
  .description('Set a specific secret value in .env file')
256
263
  .option('-f, --file <path>', 'Path to .env file', '.env')
@@ -297,12 +304,13 @@ API_KEY=
297
304
  console.log(`✅ Set ${key} in ${options.file}`);
298
305
  }
299
306
  catch (error) {
300
- console.error('❌ Failed to set secret:', error.message);
307
+ const err = error;
308
+ console.error('❌ Failed to set secret:', err.message);
301
309
  process.exit(1);
302
310
  }
303
311
  });
304
312
  // Delete .env file with confirmation
305
- secretsCmd
313
+ program
306
314
  .command('delete')
307
315
  .description('Delete .env file (requires confirmation)')
308
316
  .option('-f, --file <path>', 'Path to .env file', '.env')
@@ -351,7 +359,8 @@ API_KEY=
351
359
  console.log('');
352
360
  }
353
361
  catch (error) {
354
- console.error('❌ Failed to delete .env file:', error.message);
362
+ const err = error;
363
+ console.error('❌ Failed to delete .env file:', err.message);
355
364
  process.exit(1);
356
365
  }
357
366
  });
@@ -89,9 +89,10 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
89
89
  { flags: '-s, --search <query>', description: 'Search history entries' }
90
90
  ],
91
91
  action: async (options) => {
92
+ const opts = options;
92
93
  const persistence = new DatabasePersistence();
93
- if (options.list) {
94
- const count = parseInt(options.count);
94
+ if (opts.list) {
95
+ const count = parseInt(opts.count);
95
96
  const entries = await persistence.getHistoryEntries(count);
96
97
  this.logInfo(`Recent ${entries.length} history entries:`);
97
98
  entries.forEach((entry, index) => {
@@ -100,9 +101,9 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
100
101
  this.logInfo(`${index + 1}. [${timestamp}] ${entry.command}${exitCode}`);
101
102
  });
102
103
  }
103
- else if (options.search) {
104
+ else if (opts.search) {
104
105
  const entries = await persistence.getHistoryEntries(100);
105
- const filtered = entries.filter(entry => entry.command.toLowerCase().includes(options.search.toLowerCase()));
106
+ const filtered = entries.filter(entry => entry.command.toLowerCase().includes(opts.search.toLowerCase()));
106
107
  this.logInfo(`Found ${filtered.length} matching entries:`);
107
108
  filtered.forEach((entry, index) => {
108
109
  const timestamp = new Date(entry.timestamp).toLocaleString();
@@ -126,33 +127,34 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
126
127
  { flags: '-e, --export', description: 'Export configuration to JSON', defaultValue: false }
127
128
  ],
128
129
  action: async (options) => {
130
+ const opts = options;
129
131
  const configManager = new CloudConfigManager();
130
- if (options.list) {
132
+ if (opts.list) {
131
133
  const config = configManager.getAll();
132
134
  this.logInfo('Current configuration:');
133
135
  config.forEach(item => {
134
136
  this.logInfo(` ${item.key}: ${JSON.stringify(item.value)}`);
135
137
  });
136
138
  }
137
- else if (options.get) {
138
- const value = configManager.get(options.get);
139
+ else if (opts.get) {
140
+ const value = configManager.get(opts.get);
139
141
  if (value !== undefined) {
140
- this.logInfo(`${options.get}: ${JSON.stringify(value)}`);
142
+ this.logInfo(`${opts.get}: ${JSON.stringify(value)}`);
141
143
  }
142
144
  else {
143
- this.logWarning(`Configuration key '${options.get}' not found`);
145
+ this.logWarning(`Configuration key '${opts.get}' not found`);
144
146
  }
145
147
  }
146
- else if (options.set) {
147
- const [key, value] = options.set;
148
+ else if (opts.set) {
149
+ const [key, value] = opts.set;
148
150
  configManager.set(key, value);
149
151
  this.logSuccess(`Configuration '${key}' set to: ${value}`);
150
152
  }
151
- else if (options.delete) {
152
- configManager.delete(options.delete);
153
- this.logSuccess(`Configuration '${options.delete}' deleted`);
153
+ else if (opts.delete) {
154
+ configManager.delete(opts.delete);
155
+ this.logSuccess(`Configuration '${opts.delete}' deleted`);
154
156
  }
155
- else if (options.export) {
157
+ else if (opts.export) {
156
158
  const exported = configManager.export();
157
159
  this.logInfo(exported);
158
160
  }
@@ -170,8 +172,9 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
170
172
  { flags: '-h, --history', description: 'List job history', defaultValue: false }
171
173
  ],
172
174
  action: async (options) => {
175
+ const opts = options;
173
176
  const persistence = new DatabasePersistence();
174
- if (options.list) {
177
+ if (opts.list) {
175
178
  const jobs = await persistence.getActiveJobs();
176
179
  this.logInfo(`Active jobs (${jobs.length}):`);
177
180
  jobs.forEach(job => {
@@ -179,7 +182,7 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
179
182
  this.logInfo(`${job.job_id}: ${job.command} (${job.status}) - Started: ${started}`);
180
183
  });
181
184
  }
182
- else if (options.history) {
185
+ else if (opts.history) {
183
186
  this.logInfo('Job history feature not yet implemented');
184
187
  }
185
188
  else {
@@ -196,17 +199,18 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
196
199
  { flags: '-t, --table <name>', description: 'Show rows from specific table only' }
197
200
  ],
198
201
  action: async (options) => {
202
+ const opts = options;
199
203
  const persistence = new DatabasePersistence();
200
- const limit = parseInt(options.limit);
204
+ const limit = parseInt(opts.limit);
201
205
  // Test connection first
202
206
  const isConnected = await persistence.testConnection();
203
207
  if (!isConnected) {
204
208
  throw new Error('Cannot fetch rows - database not available');
205
209
  }
206
- if (options.table) {
210
+ if (opts.table) {
207
211
  // Show rows from specific table
208
- this.logInfo(`Latest ${limit} entries from table '${options.table}':`);
209
- const rows = await persistence.getLatestRowsFromTable(options.table, limit);
212
+ this.logInfo(`Latest ${limit} entries from table '${opts.table}':`);
213
+ const rows = await persistence.getLatestRowsFromTable(opts.table, limit);
210
214
  if (rows.length === 0) {
211
215
  this.logInfo('No entries found.');
212
216
  }
@@ -252,13 +256,14 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
252
256
  { flags: '--dataset <name>', description: 'Dataset name for new job' }
253
257
  ],
254
258
  action: async (options) => {
255
- if (options.list) {
259
+ const opts = options;
260
+ if (opts.list) {
256
261
  let query = supabaseClient.getClient()
257
262
  .from('ml_training_jobs')
258
263
  .select('*')
259
264
  .order('created_at', { ascending: false });
260
- if (options.status) {
261
- query = query.eq('status', options.status);
265
+ if (opts.status) {
266
+ query = query.eq('status', opts.status);
262
267
  }
263
268
  const { data: jobs, error } = await query.limit(20);
264
269
  if (error) {
@@ -273,16 +278,16 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
273
278
  this.logInfo(` Dataset: ${job.dataset_name}`);
274
279
  });
275
280
  }
276
- else if (options.create) {
277
- if (!options.modelType || !options.dataset) {
281
+ else if (opts.create) {
282
+ if (!opts.modelType || !opts.dataset) {
278
283
  throw new Error('Both --model-type and --dataset are required to create a job');
279
284
  }
280
285
  const { data, error } = await supabaseClient.getClient()
281
286
  .from('ml_training_jobs')
282
287
  .insert({
283
- job_name: options.create,
284
- model_type: options.modelType,
285
- dataset_name: options.dataset,
288
+ job_name: opts.create,
289
+ model_type: opts.modelType,
290
+ dataset_name: opts.dataset,
286
291
  status: 'pending',
287
292
  created_at: new Date().toISOString()
288
293
  })
@@ -290,7 +295,7 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
290
295
  if (error) {
291
296
  throw new Error(`Failed to create training job: ${error.message}`);
292
297
  }
293
- this.logSuccess(`Created training job: ${options.create}`);
298
+ this.logSuccess(`Created training job: ${opts.create}`);
294
299
  this.logInfo(JSON.stringify(data, null, 2));
295
300
  }
296
301
  else {
@@ -307,12 +312,13 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
307
312
  { flags: '--deployed', description: 'Filter by deployed models only', defaultValue: false }
308
313
  ],
309
314
  action: async (options) => {
310
- if (options.list) {
315
+ const opts = options;
316
+ if (opts.list) {
311
317
  let query = supabaseClient.getClient()
312
318
  .from('ml_models')
313
319
  .select('*')
314
320
  .order('created_at', { ascending: false });
315
- if (options.deployed) {
321
+ if (opts.deployed) {
316
322
  query = query.eq('deployed', true);
317
323
  }
318
324
  const { data: models, error } = await query.limit(20);
@@ -342,7 +348,8 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
342
348
  { flags: '-l, --list', description: 'List feature definitions', defaultValue: false }
343
349
  ],
344
350
  action: async (options) => {
345
- if (options.list) {
351
+ const opts = options;
352
+ if (opts.list) {
346
353
  const { data: features, error } = await supabaseClient.getClient()
347
354
  .from('ml_features')
348
355
  .select('*')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "0.8.3",
3
+ "version": "0.9.1",
4
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": {