gitarsenal-cli 1.2.8 → 1.3.2

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/bin/gitarsenal.js CHANGED
@@ -7,7 +7,7 @@ const ora = require('ora');
7
7
  const path = require('path');
8
8
  const { version } = require('../package.json');
9
9
  const { checkDependencies } = require('../lib/dependencies');
10
- const { runModalSandbox } = require('../lib/sandbox');
10
+ const { runContainer } = require('../lib/sandbox');
11
11
  const updateNotifier = require('update-notifier');
12
12
  const pkg = require('../package.json');
13
13
  const boxen = require('boxen');
@@ -16,28 +16,99 @@ const boxen = require('boxen');
16
16
  updateNotifier({ pkg }).notify();
17
17
 
18
18
  // Display banner
19
- console.log(boxen(chalk.bold.green('GitArsenal CLI') + '\n' + chalk.blue('Create Modal sandboxes with GitHub repositories'), {
19
+ console.log(boxen(chalk.bold.green('GitArsenal CLI') + '\n' + chalk.blue('Create GPU-accelerated development environments'), {
20
20
  padding: 1,
21
21
  margin: 1,
22
22
  borderStyle: 'round',
23
23
  borderColor: 'green'
24
24
  }));
25
25
 
26
- // Set up CLI options
26
+ // Set up main command
27
27
  program
28
28
  .version(version)
29
- .description('Create Modal sandboxes with GitHub repositories')
29
+ .description('GitArsenal CLI - Create GPU-accelerated development environments');
30
+
31
+ // Container command
32
+ const containerCmd = program
33
+ .command('container')
34
+ .description('Create a container with a GitHub repository')
35
+ .option('-g, --gpu <type>', 'GPU type (A10G, A100, H100, T4, L4, L40S, V100)', 'A10G')
36
+ .option('-r, --repo-url <url>', 'GitHub repository URL')
37
+ .option('-v, --volume-name <n>', 'Name of persistent volume')
38
+ .option('-s, --setup-commands <commands...>', 'Setup commands to run in the container')
39
+ .option('-y, --yes', 'Skip confirmation prompts')
40
+ .option('-m, --manual', 'Disable automatic setup command detection')
41
+ .option('-i, --interactive', 'Run in interactive mode with prompts')
42
+ .option('--show-examples', 'Show usage examples')
43
+ .action(async (options) => {
44
+ await runContainerCommand(options);
45
+ });
46
+
47
+ // Keys management command
48
+ const keysCmd = program
49
+ .command('keys')
50
+ .description('Manage API keys for services');
51
+
52
+ keysCmd
53
+ .command('add')
54
+ .description('Add an API key')
55
+ .option('-s, --service <service>', 'Service name (openai, wandb, huggingface)')
56
+ .option('-k, --key <key>', 'API key (if not provided, will prompt)')
57
+ .action(async (options) => {
58
+ await handleKeysAdd(options);
59
+ });
60
+
61
+ keysCmd
62
+ .command('list')
63
+ .description('List saved API keys')
64
+ .action(async () => {
65
+ await handleKeysList();
66
+ });
67
+
68
+ keysCmd
69
+ .command('view')
70
+ .description('View a specific API key (masked)')
71
+ .option('-s, --service <service>', 'Service name')
72
+ .action(async (options) => {
73
+ await handleKeysView(options);
74
+ });
75
+
76
+ keysCmd
77
+ .command('delete')
78
+ .description('Delete an API key')
79
+ .option('-s, --service <service>', 'Service name')
80
+ .action(async (options) => {
81
+ await handleKeysDelete(options);
82
+ });
83
+
84
+ // For backward compatibility, support running without a subcommand
85
+ program
30
86
  .option('-r, --repo <url>', 'GitHub repository URL')
31
- .option('-g, --gpu <type>', 'GPU type (A10G, A100, H100, T4, V100)', 'A10G')
87
+ .option('-g, --gpu <type>', 'GPU type (A10G, A100, H100, T4, L4, L40S, V100)', 'A10G')
32
88
  .option('-v, --volume <n>', 'Name of persistent volume')
33
89
  .option('-y, --yes', 'Skip confirmation prompts')
34
90
  .option('-m, --manual', 'Disable automatic setup command detection')
35
- .parse(process.argv);
91
+ .option('-i, --interactive', 'Run in interactive mode with prompts')
92
+ .option('--show-examples', 'Show usage examples')
93
+ .action(async (options) => {
94
+ // If options are provided directly, run the container command
95
+ if (options.repo || options.interactive || options.showExamples || process.argv.length <= 3) {
96
+ await runContainerCommand(options);
97
+ }
98
+ });
36
99
 
37
- const options = program.opts();
100
+ program.parse(process.argv);
38
101
 
39
- async function main() {
102
+ async function runContainerCommand(options) {
40
103
  try {
104
+ // If show-examples flag is set, just show examples and exit
105
+ if (options.showExamples) {
106
+ await runContainer({
107
+ showExamples: true
108
+ });
109
+ return;
110
+ }
111
+
41
112
  // Check for required dependencies
42
113
  const spinner = ora('Checking dependencies...').start();
43
114
  const dependenciesOk = await checkDependencies();
@@ -48,12 +119,21 @@ async function main() {
48
119
  }
49
120
  spinner.succeed('Dependencies checked');
50
121
 
122
+ // If interactive mode is enabled, let the Python script handle the prompts
123
+ if (options.interactive) {
124
+ await runContainer({
125
+ interactive: true
126
+ });
127
+ return;
128
+ }
129
+
51
130
  // If repo URL not provided, prompt for it
52
- let repoUrl = options.repo;
131
+ let repoUrl = options.repoUrl || options.repo;
53
132
  let gpuType = options.gpu;
54
- let volumeName = options.volume;
133
+ let volumeName = options.volumeName || options.volume;
55
134
  let skipConfirmation = options.yes;
56
135
  let useApi = !options.manual;
136
+ let setupCommands = options.setupCommands || [];
57
137
 
58
138
  if (!repoUrl) {
59
139
  const answers = await inquirer.prompt([
@@ -68,7 +148,7 @@ async function main() {
68
148
  }
69
149
 
70
150
  // Prompt for GPU type if not specified
71
- if (!options.gpu) {
151
+ if (!gpuType) {
72
152
  const gpuAnswers = await inquirer.prompt([
73
153
  {
74
154
  type: 'list',
@@ -79,6 +159,8 @@ async function main() {
79
159
  { name: 'A100 (40GB VRAM)', value: 'A100' },
80
160
  { name: 'H100 (80GB VRAM)', value: 'H100' },
81
161
  { name: 'T4 (16GB VRAM)', value: 'T4' },
162
+ { name: 'L4 (24GB VRAM)', value: 'L4' },
163
+ { name: 'L40S (48GB VRAM)', value: 'L40S' },
82
164
  { name: 'V100 (16GB VRAM)', value: 'V100' }
83
165
  ],
84
166
  default: 'A10G'
@@ -111,7 +193,7 @@ async function main() {
111
193
  }
112
194
 
113
195
  // Ask about setup command detection if not specified via CLI
114
- if (!options.manual && !options.yes) {
196
+ if (!options.manual && !options.yes && setupCommands.length === 0) {
115
197
  const apiAnswers = await inquirer.prompt([
116
198
  {
117
199
  type: 'confirm',
@@ -124,10 +206,8 @@ async function main() {
124
206
  useApi = apiAnswers.useApi;
125
207
  }
126
208
 
127
- let setupCommands = [];
128
-
129
- // Only prompt for custom commands if auto-detection is disabled
130
- if (!useApi) {
209
+ // Only prompt for custom commands if auto-detection is disabled and no commands provided
210
+ if (!useApi && setupCommands.length === 0) {
131
211
  const setupAnswers = await inquirer.prompt([
132
212
  {
133
213
  type: 'confirm',
@@ -156,7 +236,7 @@ async function main() {
156
236
  }
157
237
 
158
238
  // Show configuration summary
159
- console.log(chalk.bold('\n📋 Sandbox Configuration:'));
239
+ console.log(chalk.bold('\n📋 Container Configuration:'));
160
240
  console.log(chalk.cyan('Repository URL: ') + repoUrl);
161
241
  console.log(chalk.cyan('GPU Type: ') + gpuType);
162
242
  console.log(chalk.cyan('Volume: ') + (volumeName || 'None'));
@@ -189,8 +269,8 @@ async function main() {
189
269
  }
190
270
  }
191
271
 
192
- // Run the sandbox
193
- await runModalSandbox({
272
+ // Run the container
273
+ await runContainer({
194
274
  repoUrl,
195
275
  gpuType,
196
276
  volumeName,
@@ -204,4 +284,225 @@ async function main() {
204
284
  }
205
285
  }
206
286
 
207
- main();
287
+ async function handleKeysAdd(options) {
288
+ try {
289
+ const spinner = ora('Adding API key...').start();
290
+
291
+ let service = options.service;
292
+ let key = options.key;
293
+
294
+ if (!service) {
295
+ spinner.stop();
296
+ const serviceAnswer = await inquirer.prompt([
297
+ {
298
+ type: 'list',
299
+ name: 'service',
300
+ message: 'Select service:',
301
+ choices: [
302
+ { name: 'OpenAI', value: 'openai' },
303
+ { name: 'Weights & Biases', value: 'wandb' },
304
+ { name: 'Hugging Face', value: 'huggingface' }
305
+ ]
306
+ }
307
+ ]);
308
+ service = serviceAnswer.service;
309
+ }
310
+
311
+ if (!key) {
312
+ spinner.stop();
313
+ const keyAnswer = await inquirer.prompt([
314
+ {
315
+ type: 'password',
316
+ name: 'key',
317
+ message: `Enter ${service} API key:`,
318
+ mask: '*'
319
+ }
320
+ ]);
321
+ key = keyAnswer.key;
322
+ }
323
+
324
+ // Call Python script to add the key
325
+ const { spawn } = require('child_process');
326
+ const scriptPath = require('../lib/sandbox').getPythonScriptPath();
327
+
328
+ const pythonProcess = spawn('python', [
329
+ scriptPath,
330
+ 'keys',
331
+ 'add',
332
+ '--service', service,
333
+ '--key', key
334
+ ], { stdio: 'pipe' });
335
+
336
+ let output = '';
337
+ pythonProcess.stdout.on('data', (data) => {
338
+ output += data.toString();
339
+ });
340
+
341
+ pythonProcess.stderr.on('data', (data) => {
342
+ output += data.toString();
343
+ });
344
+
345
+ pythonProcess.on('close', (code) => {
346
+ if (code === 0) {
347
+ spinner.succeed(`API key for ${service} added successfully`);
348
+ } else {
349
+ spinner.fail(`Failed to add API key: ${output}`);
350
+ }
351
+ });
352
+
353
+ } catch (error) {
354
+ console.error(chalk.red(`Error: ${error.message}`));
355
+ process.exit(1);
356
+ }
357
+ }
358
+
359
+ async function handleKeysList() {
360
+ try {
361
+ const spinner = ora('Fetching API keys...').start();
362
+
363
+ // Call Python script to list keys
364
+ const { spawn } = require('child_process');
365
+ const scriptPath = require('../lib/sandbox').getPythonScriptPath();
366
+
367
+ const pythonProcess = spawn('python', [
368
+ scriptPath,
369
+ 'keys',
370
+ 'list'
371
+ ], { stdio: 'inherit' });
372
+
373
+ pythonProcess.on('close', (code) => {
374
+ if (code !== 0) {
375
+ spinner.fail('Failed to list API keys');
376
+ } else {
377
+ spinner.stop();
378
+ }
379
+ });
380
+
381
+ } catch (error) {
382
+ console.error(chalk.red(`Error: ${error.message}`));
383
+ process.exit(1);
384
+ }
385
+ }
386
+
387
+ async function handleKeysView(options) {
388
+ try {
389
+ const spinner = ora('Viewing API key...').start();
390
+
391
+ let service = options.service;
392
+
393
+ if (!service) {
394
+ spinner.stop();
395
+ const serviceAnswer = await inquirer.prompt([
396
+ {
397
+ type: 'list',
398
+ name: 'service',
399
+ message: 'Select service:',
400
+ choices: [
401
+ { name: 'OpenAI', value: 'openai' },
402
+ { name: 'Weights & Biases', value: 'wandb' },
403
+ { name: 'Hugging Face', value: 'huggingface' }
404
+ ]
405
+ }
406
+ ]);
407
+ service = serviceAnswer.service;
408
+ }
409
+
410
+ // Call Python script to view the key
411
+ const { spawn } = require('child_process');
412
+ const scriptPath = require('../lib/sandbox').getPythonScriptPath();
413
+
414
+ const pythonProcess = spawn('python', [
415
+ scriptPath,
416
+ 'keys',
417
+ 'view',
418
+ '--service', service
419
+ ], { stdio: 'inherit' });
420
+
421
+ pythonProcess.on('close', (code) => {
422
+ if (code !== 0) {
423
+ spinner.fail(`Failed to view API key for ${service}`);
424
+ } else {
425
+ spinner.stop();
426
+ }
427
+ });
428
+
429
+ } catch (error) {
430
+ console.error(chalk.red(`Error: ${error.message}`));
431
+ process.exit(1);
432
+ }
433
+ }
434
+
435
+ async function handleKeysDelete(options) {
436
+ try {
437
+ const spinner = ora('Deleting API key...').start();
438
+
439
+ let service = options.service;
440
+
441
+ if (!service) {
442
+ spinner.stop();
443
+ const serviceAnswer = await inquirer.prompt([
444
+ {
445
+ type: 'list',
446
+ name: 'service',
447
+ message: 'Select service:',
448
+ choices: [
449
+ { name: 'OpenAI', value: 'openai' },
450
+ { name: 'Weights & Biases', value: 'wandb' },
451
+ { name: 'Hugging Face', value: 'huggingface' }
452
+ ]
453
+ }
454
+ ]);
455
+ service = serviceAnswer.service;
456
+ }
457
+
458
+ // Confirm deletion
459
+ spinner.stop();
460
+ const confirmAnswer = await inquirer.prompt([
461
+ {
462
+ type: 'confirm',
463
+ name: 'confirm',
464
+ message: `Are you sure you want to delete the API key for ${service}?`,
465
+ default: false
466
+ }
467
+ ]);
468
+
469
+ if (!confirmAnswer.confirm) {
470
+ console.log(chalk.yellow('Operation cancelled by user.'));
471
+ return;
472
+ }
473
+
474
+ spinner.start();
475
+
476
+ // Call Python script to delete the key
477
+ const { spawn } = require('child_process');
478
+ const scriptPath = require('../lib/sandbox').getPythonScriptPath();
479
+
480
+ const pythonProcess = spawn('python', [
481
+ scriptPath,
482
+ 'keys',
483
+ 'delete',
484
+ '--service', service
485
+ ], { stdio: 'pipe' });
486
+
487
+ let output = '';
488
+ pythonProcess.stdout.on('data', (data) => {
489
+ output += data.toString();
490
+ });
491
+
492
+ pythonProcess.stderr.on('data', (data) => {
493
+ output += data.toString();
494
+ });
495
+
496
+ pythonProcess.on('close', (code) => {
497
+ if (code === 0) {
498
+ spinner.succeed(`API key for ${service} deleted successfully`);
499
+ } else {
500
+ spinner.fail(`Failed to delete API key: ${output}`);
501
+ }
502
+ });
503
+
504
+ } catch (error) {
505
+ console.error(chalk.red(`Error: ${error.message}`));
506
+ process.exit(1);
507
+ }
508
+ }
package/lib/sandbox.js CHANGED
@@ -26,17 +26,27 @@ function getPythonScriptPath() {
26
26
  }
27
27
 
28
28
  /**
29
- * Run the Modal sandbox with the given options
30
- * @param {Object} options - Sandbox options
29
+ * Run the container with the given options
30
+ * @param {Object} options - Container options
31
31
  * @param {string} options.repoUrl - GitHub repository URL
32
32
  * @param {string} options.gpuType - GPU type
33
33
  * @param {string} options.volumeName - Volume name
34
34
  * @param {Array<string>} options.setupCommands - Setup commands
35
35
  * @param {boolean} options.useApi - Whether to use the API to fetch setup commands
36
+ * @param {boolean} options.interactive - Whether to run in interactive mode
37
+ * @param {boolean} options.showExamples - Whether to show usage examples
36
38
  * @returns {Promise<void>}
37
39
  */
38
- async function runModalSandbox(options) {
39
- const { repoUrl, gpuType, volumeName, setupCommands = [], useApi = true } = options;
40
+ async function runContainer(options) {
41
+ const {
42
+ repoUrl,
43
+ gpuType,
44
+ volumeName,
45
+ setupCommands = [],
46
+ useApi = true,
47
+ interactive = false,
48
+ showExamples = false
49
+ } = options;
40
50
 
41
51
  // Get the path to the Python script
42
52
  const scriptPath = getPythonScriptPath();
@@ -48,10 +58,44 @@ async function runModalSandbox(options) {
48
58
 
49
59
  // Prepare command arguments
50
60
  const args = [
51
- scriptPath,
52
- '--gpu', gpuType,
53
- '--repo-url', repoUrl
61
+ scriptPath
54
62
  ];
63
+
64
+ // If show examples is true, only pass that flag
65
+ if (showExamples) {
66
+ args.push('--show-examples');
67
+
68
+ // Log the command being executed
69
+ console.log(chalk.dim(`\nExecuting: python ${args.join(' ')}`));
70
+
71
+ // Run the Python script with show examples flag
72
+ const pythonProcess = spawn('python', args, {
73
+ stdio: 'inherit' // Inherit stdio to show real-time output
74
+ });
75
+
76
+ return new Promise((resolve, reject) => {
77
+ pythonProcess.on('close', (code) => {
78
+ if (code === 0) {
79
+ resolve();
80
+ } else {
81
+ reject(new Error(`Process exited with code ${code}`));
82
+ }
83
+ });
84
+
85
+ pythonProcess.on('error', (error) => {
86
+ reject(error);
87
+ });
88
+ });
89
+ }
90
+
91
+ // Add interactive flag if specified
92
+ if (interactive) {
93
+ args.push('--interactive');
94
+ } else {
95
+ // Only add these arguments in non-interactive mode
96
+ if (gpuType) args.push('--gpu', gpuType);
97
+ if (repoUrl) args.push('--repo-url', repoUrl);
98
+ }
55
99
 
56
100
  if (volumeName) {
57
101
  args.push('--volume-name', volumeName);
@@ -74,7 +118,7 @@ async function runModalSandbox(options) {
74
118
  console.log(chalk.dim(`\nExecuting: python ${args.join(' ')}`));
75
119
 
76
120
  // Start the spinner
77
- const spinner = ora('Launching Modal sandbox...').start();
121
+ const spinner = ora('Launching container...').start();
78
122
 
79
123
  try {
80
124
  // Run the Python script
@@ -86,10 +130,10 @@ async function runModalSandbox(options) {
86
130
  return new Promise((resolve, reject) => {
87
131
  pythonProcess.on('close', (code) => {
88
132
  if (code === 0) {
89
- spinner.succeed('Modal sandbox launched successfully');
133
+ spinner.succeed('Container launched successfully');
90
134
  resolve();
91
135
  } else {
92
- spinner.fail(`Modal sandbox launch failed with exit code ${code}`);
136
+ spinner.fail(`Container launch failed with exit code ${code}`);
93
137
  reject(new Error(`Process exited with code ${code}`));
94
138
  }
95
139
  });
@@ -101,12 +145,12 @@ async function runModalSandbox(options) {
101
145
  });
102
146
  });
103
147
  } catch (error) {
104
- spinner.fail(`Error launching Modal sandbox: ${error.message}`);
148
+ spinner.fail(`Error launching container: ${error.message}`);
105
149
  throw error;
106
150
  }
107
151
  }
108
152
 
109
153
  module.exports = {
110
- runModalSandbox,
154
+ runContainer,
111
155
  getPythonScriptPath
112
156
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.2.8",
3
+ "version": "1.3.2",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -34,4 +34,4 @@
34
34
  "engines": {
35
35
  "node": ">=14.0.0"
36
36
  }
37
- }
37
+ }
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This script removes emojis from the test_modalSandboxScript.py file to fix syntax errors.
4
+ """
5
+
6
+ import re
7
+ import sys
8
+ import shutil
9
+ from pathlib import Path
10
+
11
+ def remove_emojis(script_path):
12
+ """
13
+ Remove emojis from the Python script to fix syntax errors.
14
+ """
15
+ print(f"Removing emojis from {script_path}...")
16
+
17
+ # Make a backup of the original file
18
+ backup_path = f"{script_path}.emoji_backup"
19
+ shutil.copy2(script_path, backup_path)
20
+
21
+ # Read the file
22
+ with open(script_path, 'r', encoding='utf-8', errors='ignore') as f:
23
+ content = f.read()
24
+
25
+ # Emoji pattern - matches most common emoji characters
26
+ emoji_pattern = re.compile(
27
+ "["
28
+ "\U0001F1E0-\U0001F1FF" # flags (iOS)
29
+ "\U0001F300-\U0001F5FF" # symbols & pictographs
30
+ "\U0001F600-\U0001F64F" # emoticons
31
+ "\U0001F680-\U0001F6FF" # transport & map symbols
32
+ "\U0001F700-\U0001F77F" # alchemical symbols
33
+ "\U0001F780-\U0001F7FF" # Geometric Shapes
34
+ "\U0001F800-\U0001F8FF" # Supplemental Arrows-C
35
+ "\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs
36
+ "\U0001FA00-\U0001FA6F" # Chess Symbols
37
+ "\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A
38
+ "\U00002702-\U000027B0" # Dingbats
39
+ "\U000024C2-\U0001F251"
40
+ "]+", flags=re.UNICODE)
41
+
42
+ # Simply remove all emojis
43
+ content = emoji_pattern.sub('', content)
44
+
45
+ # Fix common syntax issues after emoji removal
46
+ content = re.sub(r'print\(\s*\)', r'print()', content)
47
+ content = re.sub(r'print\(\s*"', r'print("', content)
48
+
49
+ # Fix specific syntax errors
50
+ content = re.sub(r'print\(\s*container\'s', r'print("container\'s', content)
51
+
52
+ # Write the modified content back to the file
53
+ with open(script_path, 'w', encoding='utf-8') as f:
54
+ f.write(content)
55
+
56
+ print(f"Emoji removal complete. Original file backed up to {backup_path}")
57
+ return True
58
+
59
+ def main():
60
+ """
61
+ Main entry point for the script.
62
+ """
63
+ # Get the path to the script
64
+ script_dir = Path(__file__).parent
65
+ script_path = script_dir / "test_modalSandboxScript.py"
66
+
67
+ if not script_path.exists():
68
+ print(f"Error: Script not found at {script_path}")
69
+ return 1
70
+
71
+ try:
72
+ remove_emojis(script_path)
73
+ return 0
74
+ except Exception as e:
75
+ print(f"Error removing emojis: {e}")
76
+ return 1
77
+
78
+ if __name__ == "__main__":
79
+ sys.exit(main())