gitarsenal-cli 1.9.5 → 1.9.7

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/.venv_status.json CHANGED
@@ -1 +1 @@
1
- {"created":"2025-08-03T17:35:41.855Z","packages":["modal","gitingest","requests"],"uv_version":"uv 0.8.4 (e176e1714 2025-07-30)"}
1
+ {"created":"2025-08-03T18:14:08.672Z","packages":["modal","gitingest","requests"],"uv_version":"uv 0.8.4 (e176e1714 2025-07-30)"}
package/bin/gitarsenal.js CHANGED
@@ -13,6 +13,8 @@ const pkg = require('../package.json');
13
13
  const boxen = require('boxen');
14
14
  const { spawn } = require('child_process');
15
15
  const fs = require('fs');
16
+ const https = require('https');
17
+ const http = require('http');
16
18
 
17
19
  // Function to activate virtual environment
18
20
  function activateVirtualEnvironment() {
@@ -102,6 +104,148 @@ function activateVirtualEnvironment() {
102
104
  return true;
103
105
  }
104
106
 
107
+ // Function to send user data to web application
108
+ async function sendUserData(userId, userName, command, repoUrl = null, success = true, error = null) {
109
+ try {
110
+ const userData = {
111
+ userId,
112
+ userName,
113
+ command,
114
+ repoUrl,
115
+ success,
116
+ error,
117
+ timestamp: new Date().toISOString()
118
+ };
119
+
120
+ const data = JSON.stringify(userData);
121
+
122
+ // Get webhook URL from config or use default
123
+ let webhookUrl = 'https://gitarsenal.dev/api/users/cli-log';
124
+ const configPath = path.join(__dirname, '..', 'config.json');
125
+ if (fs.existsSync(configPath)) {
126
+ try {
127
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
128
+ if (config.webhookUrl) {
129
+ webhookUrl = config.webhookUrl;
130
+ }
131
+ } catch (error) {
132
+ // Use default URL if config can't be read
133
+ }
134
+ }
135
+
136
+ const urlObj = new URL(webhookUrl);
137
+ const options = {
138
+ hostname: urlObj.hostname,
139
+ port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
140
+ path: urlObj.pathname,
141
+ method: 'POST',
142
+ headers: {
143
+ 'Content-Type': 'application/json',
144
+ 'Content-Length': Buffer.byteLength(data)
145
+ }
146
+ };
147
+
148
+ return new Promise((resolve, reject) => {
149
+ const client = urlObj.protocol === 'https:' ? https : http;
150
+ const req = client.request(options, (res) => {
151
+ let responseData = '';
152
+ res.on('data', (chunk) => {
153
+ responseData += chunk;
154
+ });
155
+ res.on('end', () => {
156
+ if (res.statusCode >= 200 && res.statusCode < 300) {
157
+ console.log(chalk.green('✅ User activity logged to GitArsenal dashboard'));
158
+ resolve(responseData);
159
+ } else {
160
+ console.log(chalk.yellow('⚠️ Failed to log user activity (non-critical)'));
161
+ resolve(responseData);
162
+ }
163
+ });
164
+ });
165
+
166
+ req.on('error', (err) => {
167
+ console.log(chalk.yellow('⚠️ Could not connect to GitArsenal dashboard (non-critical)'));
168
+ resolve();
169
+ });
170
+
171
+ req.write(data);
172
+ req.end();
173
+ });
174
+ } catch (error) {
175
+ console.log(chalk.yellow('⚠️ Error logging user activity (non-critical)'));
176
+ }
177
+ }
178
+
179
+ // Function to collect user credentials
180
+ async function collectUserCredentials(options) {
181
+ let userId = options.userId;
182
+ let userName = options.userName;
183
+
184
+ // Check for config file first
185
+ const configPath = path.join(__dirname, '..', 'config.json');
186
+ if (fs.existsSync(configPath)) {
187
+ try {
188
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
189
+ if (config.userId && config.userName) {
190
+ userId = config.userId;
191
+ userName = config.userName;
192
+ }
193
+ } catch (error) {
194
+ console.log(chalk.yellow('⚠️ Could not read config file'));
195
+ }
196
+ }
197
+
198
+ // If not provided via CLI or config, prompt for them
199
+ if (!userId || !userName) {
200
+ console.log(chalk.blue('\n🔐 GitArsenal User Identification'));
201
+ console.log(chalk.gray('Help us track your usage and improve GitArsenal!'));
202
+ console.log(chalk.gray('Your credentials will be saved locally for future use.'));
203
+
204
+ const credentials = await inquirer.prompt([
205
+ {
206
+ type: 'input',
207
+ name: 'userId',
208
+ message: 'Enter your user ID (or email):',
209
+ default: userId || 'anonymous',
210
+ validate: (input) => input.trim() !== '' ? true : 'User ID is required'
211
+ },
212
+ {
213
+ type: 'input',
214
+ name: 'userName',
215
+ message: 'Enter your name:',
216
+ default: userName || 'Anonymous User',
217
+ validate: (input) => input.trim() !== '' ? true : 'Name is required'
218
+ },
219
+ {
220
+ type: 'confirm',
221
+ name: 'saveConfig',
222
+ message: 'Save these credentials for future use?',
223
+ default: true
224
+ }
225
+ ]);
226
+
227
+ userId = credentials.userId;
228
+ userName = credentials.userName;
229
+
230
+ // Save to config file if requested
231
+ if (credentials.saveConfig) {
232
+ try {
233
+ const config = {
234
+ userId,
235
+ userName,
236
+ webhookUrl: 'https://gitarsenal.dev/api/users/cli-log'
237
+ };
238
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
239
+ console.log(chalk.green('✅ Credentials saved to config file'));
240
+ } catch (error) {
241
+ console.log(chalk.yellow('⚠️ Could not save config file'));
242
+ }
243
+ }
244
+ }
245
+
246
+ return { userId, userName };
247
+ }
248
+
105
249
  // Activate virtual environment
106
250
  activateVirtualEnvironment();
107
251
 
@@ -140,6 +284,8 @@ const containerCmd = program
140
284
  .option('-y, --yes', 'Skip confirmation prompts')
141
285
  .option('-m, --manual', 'Disable automatic setup command detection')
142
286
  .option('--show-examples', 'Show usage examples')
287
+ .option('--user-id <id>', 'User ID for tracking')
288
+ .option('--user-name <name>', 'User name for tracking')
143
289
  .action(async (options) => {
144
290
  await runContainerCommand(options);
145
291
  });
@@ -189,6 +335,8 @@ program
189
335
  .option('-y, --yes', 'Skip confirmation prompts')
190
336
  .option('-m, --manual', 'Disable automatic setup command detection')
191
337
  .option('--show-examples', 'Show usage examples')
338
+ .option('--user-id <id>', 'User ID for tracking')
339
+ .option('--user-name <name>', 'User name for tracking')
192
340
  .action(async (options) => {
193
341
  // If options are provided directly, run the container command
194
342
  if (options.repo || options.showExamples || process.argv.length <= 3) {
@@ -208,12 +356,18 @@ async function runContainerCommand(options) {
208
356
  return;
209
357
  }
210
358
 
359
+ // Collect user credentials
360
+ const userCredentials = await collectUserCredentials(options);
361
+ const { userId, userName } = userCredentials;
362
+
211
363
  // Check for required dependencies
212
364
  const spinner = ora('Checking dependencies...').start();
213
365
  const dependenciesOk = await checkDependencies();
214
366
 
215
367
  if (!dependenciesOk) {
216
368
  spinner.fail('Missing dependencies. Please install them and try again.');
369
+ // Log failed dependency check
370
+ await sendUserData(userId, userName, 'dependency-check', null, false, 'Missing dependencies');
217
371
  process.exit(1);
218
372
  }
219
373
  spinner.succeed('Dependencies checked');
@@ -345,14 +499,28 @@ async function runContainerCommand(options) {
345
499
  }
346
500
  }
347
501
 
502
+ // Log command start
503
+ const commandString = `gitarsenal container ${repoUrl ? `--repo ${repoUrl}` : ''} ${gpuType ? `--gpu ${gpuType}` : ''}`;
504
+ await sendUserData(userId, userName, commandString, repoUrl, true);
505
+
348
506
  // Run the container
349
- await runContainer({
350
- repoUrl,
351
- gpuType,
352
- volumeName,
353
- setupCommands,
354
- useApi
355
- });
507
+ try {
508
+ await runContainer({
509
+ repoUrl,
510
+ gpuType,
511
+ volumeName,
512
+ setupCommands,
513
+ useApi
514
+ });
515
+
516
+ // Log successful completion
517
+ await sendUserData(userId, userName, commandString, repoUrl, true);
518
+
519
+ } catch (containerError) {
520
+ // Log error
521
+ await sendUserData(userId, userName, commandString, repoUrl, false, containerError.message);
522
+ throw containerError;
523
+ }
356
524
 
357
525
  } catch (error) {
358
526
  console.error(chalk.red(`Error: ${error.message}`));
@@ -362,6 +530,10 @@ async function runContainerCommand(options) {
362
530
 
363
531
  async function handleKeysAdd(options) {
364
532
  try {
533
+ // Collect user credentials for keys operations
534
+ const userCredentials = await collectUserCredentials(options);
535
+ const { userId, userName } = userCredentials;
536
+
365
537
  const spinner = ora('Adding API key...').start();
366
538
 
367
539
  let service = options.service;
@@ -413,11 +585,15 @@ async function handleKeysAdd(options) {
413
585
  output += data.toString();
414
586
  });
415
587
 
416
- pythonProcess.on('close', (code) => {
588
+ pythonProcess.on('close', async (code) => {
417
589
  if (code === 0) {
418
590
  spinner.succeed(`API key for ${service} added successfully`);
591
+ // Log successful key addition
592
+ await sendUserData(userId, userName, `gitarsenal keys add --service ${service}`, null, true);
419
593
  } else {
420
594
  spinner.fail(`Failed to add API key: ${output}`);
595
+ // Log failed key addition
596
+ await sendUserData(userId, userName, `gitarsenal keys add --service ${service}`, null, false, output);
421
597
  }
422
598
  });
423
599
 
@@ -429,6 +605,10 @@ async function handleKeysAdd(options) {
429
605
 
430
606
  async function handleKeysList() {
431
607
  try {
608
+ // Collect user credentials for keys operations
609
+ const userCredentials = await collectUserCredentials({});
610
+ const { userId, userName } = userCredentials;
611
+
432
612
  const spinner = ora('Fetching API keys...').start();
433
613
 
434
614
  // Call Python script to list keys
@@ -440,11 +620,13 @@ async function handleKeysList() {
440
620
  'list'
441
621
  ], { stdio: 'inherit' });
442
622
 
443
- pythonProcess.on('close', (code) => {
623
+ pythonProcess.on('close', async (code) => {
444
624
  if (code !== 0) {
445
625
  spinner.fail('Failed to list API keys');
626
+ await sendUserData(userId, userName, 'gitarsenal keys list', null, false, 'Failed to list API keys');
446
627
  } else {
447
628
  spinner.stop();
629
+ await sendUserData(userId, userName, 'gitarsenal keys list', null, true);
448
630
  }
449
631
  });
450
632
 
@@ -456,6 +638,10 @@ async function handleKeysList() {
456
638
 
457
639
  async function handleKeysView(options) {
458
640
  try {
641
+ // Collect user credentials for keys operations
642
+ const userCredentials = await collectUserCredentials(options);
643
+ const { userId, userName } = userCredentials;
644
+
459
645
  const spinner = ora('Viewing API key...').start();
460
646
 
461
647
  let service = options.service;
@@ -483,11 +669,13 @@ async function handleKeysView(options) {
483
669
  '--service', service
484
670
  ], { stdio: 'inherit' });
485
671
 
486
- pythonProcess.on('close', (code) => {
672
+ pythonProcess.on('close', async (code) => {
487
673
  if (code !== 0) {
488
674
  spinner.fail(`Failed to view API key for ${service}`);
675
+ await sendUserData(userId, userName, `gitarsenal keys view --service ${service}`, null, false, `Failed to view API key for ${service}`);
489
676
  } else {
490
677
  spinner.stop();
678
+ await sendUserData(userId, userName, `gitarsenal keys view --service ${service}`, null, true);
491
679
  }
492
680
  });
493
681
 
@@ -499,6 +687,10 @@ async function handleKeysView(options) {
499
687
 
500
688
  async function handleKeysDelete(options) {
501
689
  try {
690
+ // Collect user credentials for keys operations
691
+ const userCredentials = await collectUserCredentials(options);
692
+ const { userId, userName } = userCredentials;
693
+
502
694
  const spinner = ora('Deleting API key...').start();
503
695
 
504
696
  let service = options.service;
@@ -529,6 +721,7 @@ async function handleKeysDelete(options) {
529
721
 
530
722
  if (!confirmAnswer.confirm) {
531
723
  console.log(chalk.yellow('Operation cancelled by user.'));
724
+ await sendUserData(userId, userName, `gitarsenal keys delete --service ${service}`, null, false, 'Operation cancelled by user');
532
725
  return;
533
726
  }
534
727
 
@@ -553,11 +746,13 @@ async function handleKeysDelete(options) {
553
746
  output += data.toString();
554
747
  });
555
748
 
556
- pythonProcess.on('close', (code) => {
749
+ pythonProcess.on('close', async (code) => {
557
750
  if (code === 0) {
558
751
  spinner.succeed(`API key for ${service} deleted successfully`);
752
+ await sendUserData(userId, userName, `gitarsenal keys delete --service ${service}`, null, true);
559
753
  } else {
560
754
  spinner.fail(`Failed to delete API key: ${output}`);
755
+ await sendUserData(userId, userName, `gitarsenal keys delete --service ${service}`, null, false, output);
561
756
  }
562
757
  });
563
758
 
package/config.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "userId": "rs545837",
3
+ "userName": "Rohan Sharma",
4
+ "webhookUrl": "https://gitarsenal.dev/api/users/cli-log"
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.9.5",
3
+ "version": "1.9.7",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -32,9 +32,10 @@ async function checkAndInstallUv() {
32
32
  if (platform === 'darwin') {
33
33
  // macOS - prioritize Homebrew
34
34
  installMethods = [
35
- 'brew install uv',
36
35
  'curl -LsSf https://astral.sh/uv/install.sh | sh',
36
+ 'pip install uv',
37
37
  'pip3 install uv',
38
+ 'brew install uv',
38
39
  'cargo install uv'
39
40
  ];
40
41
  } else if (platform === 'win32') {