gitarsenal-cli 1.2.7 → 1.3.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.
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,27 +16,86 @@ 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 <name>', '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
+ .action(async (options) => {
42
+ await runContainerCommand(options);
43
+ });
44
+
45
+ // Keys management command
46
+ const keysCmd = program
47
+ .command('keys')
48
+ .description('Manage API keys for services');
49
+
50
+ keysCmd
51
+ .command('add')
52
+ .description('Add an API key')
53
+ .option('-s, --service <service>', 'Service name (openai, wandb, huggingface)')
54
+ .option('-k, --key <key>', 'API key (if not provided, will prompt)')
55
+ .action(async (options) => {
56
+ await handleKeysAdd(options);
57
+ });
58
+
59
+ keysCmd
60
+ .command('list')
61
+ .description('List saved API keys')
62
+ .action(async () => {
63
+ await handleKeysList();
64
+ });
65
+
66
+ keysCmd
67
+ .command('view')
68
+ .description('View a specific API key (masked)')
69
+ .option('-s, --service <service>', 'Service name')
70
+ .action(async (options) => {
71
+ await handleKeysView(options);
72
+ });
73
+
74
+ keysCmd
75
+ .command('delete')
76
+ .description('Delete an API key')
77
+ .option('-s, --service <service>', 'Service name')
78
+ .action(async (options) => {
79
+ await handleKeysDelete(options);
80
+ });
81
+
82
+ // For backward compatibility, support running without a subcommand
83
+ program
30
84
  .option('-r, --repo <url>', 'GitHub repository URL')
31
- .option('-g, --gpu <type>', 'GPU type (A10G, A100, H100, T4, V100)', 'A10G')
85
+ .option('-g, --gpu <type>', 'GPU type (A10G, A100, H100, T4, L4, L40S, V100)', 'A10G')
32
86
  .option('-v, --volume <n>', 'Name of persistent volume')
33
87
  .option('-y, --yes', 'Skip confirmation prompts')
34
88
  .option('-m, --manual', 'Disable automatic setup command detection')
35
- .parse(process.argv);
89
+ .action(async (options) => {
90
+ // If options are provided directly, run the container command
91
+ if (options.repo || process.argv.length <= 3) {
92
+ await runContainerCommand(options);
93
+ }
94
+ });
36
95
 
37
- const options = program.opts();
96
+ program.parse(process.argv);
38
97
 
39
- async function main() {
98
+ async function runContainerCommand(options) {
40
99
  try {
41
100
  // Check for required dependencies
42
101
  const spinner = ora('Checking dependencies...').start();
@@ -49,11 +108,12 @@ async function main() {
49
108
  spinner.succeed('Dependencies checked');
50
109
 
51
110
  // If repo URL not provided, prompt for it
52
- let repoUrl = options.repo;
111
+ let repoUrl = options.repoUrl || options.repo;
53
112
  let gpuType = options.gpu;
54
- let volumeName = options.volume;
113
+ let volumeName = options.volumeName || options.volume;
55
114
  let skipConfirmation = options.yes;
56
115
  let useApi = !options.manual;
116
+ let setupCommands = options.setupCommands || [];
57
117
 
58
118
  if (!repoUrl) {
59
119
  const answers = await inquirer.prompt([
@@ -68,7 +128,7 @@ async function main() {
68
128
  }
69
129
 
70
130
  // Prompt for GPU type if not specified
71
- if (!options.gpu) {
131
+ if (!gpuType) {
72
132
  const gpuAnswers = await inquirer.prompt([
73
133
  {
74
134
  type: 'list',
@@ -79,6 +139,8 @@ async function main() {
79
139
  { name: 'A100 (40GB VRAM)', value: 'A100' },
80
140
  { name: 'H100 (80GB VRAM)', value: 'H100' },
81
141
  { name: 'T4 (16GB VRAM)', value: 'T4' },
142
+ { name: 'L4 (24GB VRAM)', value: 'L4' },
143
+ { name: 'L40S (48GB VRAM)', value: 'L40S' },
82
144
  { name: 'V100 (16GB VRAM)', value: 'V100' }
83
145
  ],
84
146
  default: 'A10G'
@@ -111,7 +173,7 @@ async function main() {
111
173
  }
112
174
 
113
175
  // Ask about setup command detection if not specified via CLI
114
- if (!options.manual && !options.yes) {
176
+ if (!options.manual && !options.yes && setupCommands.length === 0) {
115
177
  const apiAnswers = await inquirer.prompt([
116
178
  {
117
179
  type: 'confirm',
@@ -124,10 +186,8 @@ async function main() {
124
186
  useApi = apiAnswers.useApi;
125
187
  }
126
188
 
127
- let setupCommands = [];
128
-
129
- // Only prompt for custom commands if auto-detection is disabled
130
- if (!useApi) {
189
+ // Only prompt for custom commands if auto-detection is disabled and no commands provided
190
+ if (!useApi && setupCommands.length === 0) {
131
191
  const setupAnswers = await inquirer.prompt([
132
192
  {
133
193
  type: 'confirm',
@@ -156,7 +216,7 @@ async function main() {
156
216
  }
157
217
 
158
218
  // Show configuration summary
159
- console.log(chalk.bold('\n📋 Sandbox Configuration:'));
219
+ console.log(chalk.bold('\n📋 Container Configuration:'));
160
220
  console.log(chalk.cyan('Repository URL: ') + repoUrl);
161
221
  console.log(chalk.cyan('GPU Type: ') + gpuType);
162
222
  console.log(chalk.cyan('Volume: ') + (volumeName || 'None'));
@@ -189,8 +249,8 @@ async function main() {
189
249
  }
190
250
  }
191
251
 
192
- // Run the sandbox
193
- await runModalSandbox({
252
+ // Run the container
253
+ await runContainer({
194
254
  repoUrl,
195
255
  gpuType,
196
256
  volumeName,
@@ -204,4 +264,225 @@ async function main() {
204
264
  }
205
265
  }
206
266
 
207
- main();
267
+ async function handleKeysAdd(options) {
268
+ try {
269
+ const spinner = ora('Adding API key...').start();
270
+
271
+ let service = options.service;
272
+ let key = options.key;
273
+
274
+ if (!service) {
275
+ spinner.stop();
276
+ const serviceAnswer = await inquirer.prompt([
277
+ {
278
+ type: 'list',
279
+ name: 'service',
280
+ message: 'Select service:',
281
+ choices: [
282
+ { name: 'OpenAI', value: 'openai' },
283
+ { name: 'Weights & Biases', value: 'wandb' },
284
+ { name: 'Hugging Face', value: 'huggingface' }
285
+ ]
286
+ }
287
+ ]);
288
+ service = serviceAnswer.service;
289
+ }
290
+
291
+ if (!key) {
292
+ spinner.stop();
293
+ const keyAnswer = await inquirer.prompt([
294
+ {
295
+ type: 'password',
296
+ name: 'key',
297
+ message: `Enter ${service} API key:`,
298
+ mask: '*'
299
+ }
300
+ ]);
301
+ key = keyAnswer.key;
302
+ }
303
+
304
+ // Call Python script to add the key
305
+ const { spawn } = require('child_process');
306
+ const scriptPath = require('../lib/sandbox').getPythonScriptPath();
307
+
308
+ const pythonProcess = spawn('python', [
309
+ scriptPath,
310
+ 'keys',
311
+ 'add',
312
+ '--service', service,
313
+ '--key', key
314
+ ], { stdio: 'pipe' });
315
+
316
+ let output = '';
317
+ pythonProcess.stdout.on('data', (data) => {
318
+ output += data.toString();
319
+ });
320
+
321
+ pythonProcess.stderr.on('data', (data) => {
322
+ output += data.toString();
323
+ });
324
+
325
+ pythonProcess.on('close', (code) => {
326
+ if (code === 0) {
327
+ spinner.succeed(`API key for ${service} added successfully`);
328
+ } else {
329
+ spinner.fail(`Failed to add API key: ${output}`);
330
+ }
331
+ });
332
+
333
+ } catch (error) {
334
+ console.error(chalk.red(`Error: ${error.message}`));
335
+ process.exit(1);
336
+ }
337
+ }
338
+
339
+ async function handleKeysList() {
340
+ try {
341
+ const spinner = ora('Fetching API keys...').start();
342
+
343
+ // Call Python script to list keys
344
+ const { spawn } = require('child_process');
345
+ const scriptPath = require('../lib/sandbox').getPythonScriptPath();
346
+
347
+ const pythonProcess = spawn('python', [
348
+ scriptPath,
349
+ 'keys',
350
+ 'list'
351
+ ], { stdio: 'inherit' });
352
+
353
+ pythonProcess.on('close', (code) => {
354
+ if (code !== 0) {
355
+ spinner.fail('Failed to list API keys');
356
+ } else {
357
+ spinner.stop();
358
+ }
359
+ });
360
+
361
+ } catch (error) {
362
+ console.error(chalk.red(`Error: ${error.message}`));
363
+ process.exit(1);
364
+ }
365
+ }
366
+
367
+ async function handleKeysView(options) {
368
+ try {
369
+ const spinner = ora('Viewing API key...').start();
370
+
371
+ let service = options.service;
372
+
373
+ if (!service) {
374
+ spinner.stop();
375
+ const serviceAnswer = await inquirer.prompt([
376
+ {
377
+ type: 'list',
378
+ name: 'service',
379
+ message: 'Select service:',
380
+ choices: [
381
+ { name: 'OpenAI', value: 'openai' },
382
+ { name: 'Weights & Biases', value: 'wandb' },
383
+ { name: 'Hugging Face', value: 'huggingface' }
384
+ ]
385
+ }
386
+ ]);
387
+ service = serviceAnswer.service;
388
+ }
389
+
390
+ // Call Python script to view the key
391
+ const { spawn } = require('child_process');
392
+ const scriptPath = require('../lib/sandbox').getPythonScriptPath();
393
+
394
+ const pythonProcess = spawn('python', [
395
+ scriptPath,
396
+ 'keys',
397
+ 'view',
398
+ '--service', service
399
+ ], { stdio: 'inherit' });
400
+
401
+ pythonProcess.on('close', (code) => {
402
+ if (code !== 0) {
403
+ spinner.fail(`Failed to view API key for ${service}`);
404
+ } else {
405
+ spinner.stop();
406
+ }
407
+ });
408
+
409
+ } catch (error) {
410
+ console.error(chalk.red(`Error: ${error.message}`));
411
+ process.exit(1);
412
+ }
413
+ }
414
+
415
+ async function handleKeysDelete(options) {
416
+ try {
417
+ const spinner = ora('Deleting API key...').start();
418
+
419
+ let service = options.service;
420
+
421
+ if (!service) {
422
+ spinner.stop();
423
+ const serviceAnswer = await inquirer.prompt([
424
+ {
425
+ type: 'list',
426
+ name: 'service',
427
+ message: 'Select service:',
428
+ choices: [
429
+ { name: 'OpenAI', value: 'openai' },
430
+ { name: 'Weights & Biases', value: 'wandb' },
431
+ { name: 'Hugging Face', value: 'huggingface' }
432
+ ]
433
+ }
434
+ ]);
435
+ service = serviceAnswer.service;
436
+ }
437
+
438
+ // Confirm deletion
439
+ spinner.stop();
440
+ const confirmAnswer = await inquirer.prompt([
441
+ {
442
+ type: 'confirm',
443
+ name: 'confirm',
444
+ message: `Are you sure you want to delete the API key for ${service}?`,
445
+ default: false
446
+ }
447
+ ]);
448
+
449
+ if (!confirmAnswer.confirm) {
450
+ console.log(chalk.yellow('Operation cancelled by user.'));
451
+ return;
452
+ }
453
+
454
+ spinner.start();
455
+
456
+ // Call Python script to delete the key
457
+ const { spawn } = require('child_process');
458
+ const scriptPath = require('../lib/sandbox').getPythonScriptPath();
459
+
460
+ const pythonProcess = spawn('python', [
461
+ scriptPath,
462
+ 'keys',
463
+ 'delete',
464
+ '--service', service
465
+ ], { stdio: 'pipe' });
466
+
467
+ let output = '';
468
+ pythonProcess.stdout.on('data', (data) => {
469
+ output += data.toString();
470
+ });
471
+
472
+ pythonProcess.stderr.on('data', (data) => {
473
+ output += data.toString();
474
+ });
475
+
476
+ pythonProcess.on('close', (code) => {
477
+ if (code === 0) {
478
+ spinner.succeed(`API key for ${service} deleted successfully`);
479
+ } else {
480
+ spinner.fail(`Failed to delete API key: ${output}`);
481
+ }
482
+ });
483
+
484
+ } catch (error) {
485
+ console.error(chalk.red(`Error: ${error.message}`));
486
+ process.exit(1);
487
+ }
488
+ }
package/lib/sandbox.js CHANGED
@@ -26,8 +26,8 @@ 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
@@ -35,7 +35,7 @@ function getPythonScriptPath() {
35
35
  * @param {boolean} options.useApi - Whether to use the API to fetch setup commands
36
36
  * @returns {Promise<void>}
37
37
  */
38
- async function runModalSandbox(options) {
38
+ async function runContainer(options) {
39
39
  const { repoUrl, gpuType, volumeName, setupCommands = [], useApi = true } = options;
40
40
 
41
41
  // Get the path to the Python script
@@ -74,7 +74,7 @@ async function runModalSandbox(options) {
74
74
  console.log(chalk.dim(`\nExecuting: python ${args.join(' ')}`));
75
75
 
76
76
  // Start the spinner
77
- const spinner = ora('Launching Modal sandbox...').start();
77
+ const spinner = ora('Launching container...').start();
78
78
 
79
79
  try {
80
80
  // Run the Python script
@@ -86,10 +86,10 @@ async function runModalSandbox(options) {
86
86
  return new Promise((resolve, reject) => {
87
87
  pythonProcess.on('close', (code) => {
88
88
  if (code === 0) {
89
- spinner.succeed('Modal sandbox launched successfully');
89
+ spinner.succeed('Container launched successfully');
90
90
  resolve();
91
91
  } else {
92
- spinner.fail(`Modal sandbox launch failed with exit code ${code}`);
92
+ spinner.fail(`Container launch failed with exit code ${code}`);
93
93
  reject(new Error(`Process exited with code ${code}`));
94
94
  }
95
95
  });
@@ -101,12 +101,12 @@ async function runModalSandbox(options) {
101
101
  });
102
102
  });
103
103
  } catch (error) {
104
- spinner.fail(`Error launching Modal sandbox: ${error.message}`);
104
+ spinner.fail(`Error launching container: ${error.message}`);
105
105
  throw error;
106
106
  }
107
107
  }
108
108
 
109
109
  module.exports = {
110
- runModalSandbox,
110
+ runContainer,
111
111
  getPythonScriptPath
112
112
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.2.7",
3
+ "version": "1.3.1",
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())
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This script patches the test_modalSandboxScript.py file to replace "modal" with "container" in the logs.
4
+ """
5
+
6
+ import os
7
+ import re
8
+ import sys
9
+ import shutil
10
+ from pathlib import Path
11
+
12
+ def patch_modal_logs(script_path):
13
+ """
14
+ Patch the Python script to replace "modal" with "container" in the logs.
15
+ """
16
+ print(f"Patching {script_path} to replace 'modal' with 'container' in logs...")
17
+
18
+ # Make a backup of the original file
19
+ backup_path = f"{script_path}.bak"
20
+ shutil.copy2(script_path, backup_path)
21
+
22
+ # Read the file
23
+ with open(script_path, 'r', encoding='utf-8') as f:
24
+ content = f.read()
25
+
26
+ # Define replacements
27
+ replacements = [
28
+ # Function names
29
+ (r'def create_modal_sandbox', r'def create_container'),
30
+ (r'def create_modal_ssh_container', r'def create_ssh_container'),
31
+
32
+ # Log messages - case sensitive replacements
33
+ (r'Modal sandbox', r'Container'),
34
+ (r'Modal authentication', r'Container authentication'),
35
+ (r'Modal token', r'Container token'),
36
+ (r'Modal package', r'Container package'),
37
+ (r'Modal operations', r'Container operations'),
38
+
39
+ # Log messages - case insensitive replacements (lowercase)
40
+ (r'modal sandbox', r'container'),
41
+ (r'modal authentication', r'container authentication'),
42
+ (r'modal token', r'container token'),
43
+ (r'modal package', r'container package'),
44
+ (r'modal operations', r'container operations'),
45
+
46
+ # Keep function calls to modal library intact but change logs
47
+ (r'print\([\'"](.*)modal(.*)[\'"]', r'print(\1container\2'),
48
+ (r'log\([\'"](.*)modal(.*)[\'"]', r'log(\1container\2'),
49
+ (r'logger\.info\([\'"](.*)modal(.*)[\'"]', r'logger.info(\1container\2'),
50
+ (r'logger\.error\([\'"](.*)modal(.*)[\'"]', r'logger.error(\1container\2'),
51
+ (r'logger\.warning\([\'"](.*)modal(.*)[\'"]', r'logger.warning(\1container\2'),
52
+ (r'logger\.debug\([\'"](.*)modal(.*)[\'"]', r'logger.debug(\1container\2'),
53
+ ]
54
+
55
+ # Apply replacements
56
+ for pattern, replacement in replacements:
57
+ content = re.sub(pattern, replacement, content, flags=re.IGNORECASE)
58
+
59
+ # Write the modified content back to the file
60
+ with open(script_path, 'w', encoding='utf-8') as f:
61
+ f.write(content)
62
+
63
+ print(f"Patching complete. Original file backed up to {backup_path}")
64
+ return True
65
+
66
+ def main():
67
+ """
68
+ Main entry point for the script.
69
+ """
70
+ # Get the path to the script
71
+ script_dir = Path(__file__).parent
72
+ script_path = script_dir / "test_modalSandboxScript.py"
73
+
74
+ if not script_path.exists():
75
+ print(f"Error: Script not found at {script_path}")
76
+ return 1
77
+
78
+ try:
79
+ patch_modal_logs(script_path)
80
+ return 0
81
+ except Exception as e:
82
+ print(f"Error patching file: {e}")
83
+ return 1
84
+
85
+ if __name__ == "__main__":
86
+ sys.exit(main())