shebangrun 0.2.0__py3-none-any.whl

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.
shebangrun/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ shebangrun - Python client library for shebang.run
3
+
4
+ A helper library to interact with shebang.run API and execute remote scripts.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ from .client import ShebangClient, run
10
+
11
+ __all__ = ["ShebangClient", "run"]
shebangrun/cli.py ADDED
@@ -0,0 +1,530 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ shebang - CLI tool for shebang.run
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ import argparse
9
+ import json
10
+ from pathlib import Path
11
+
12
+ try:
13
+ from shebangrun import ShebangClient, run
14
+ except ImportError:
15
+ print("Error: shebangrun library not found", file=sys.stderr)
16
+ print("Install with: pip install shebangrun", file=sys.stderr)
17
+ sys.exit(1)
18
+
19
+ CONFIG_FILE = Path.home() / '.shebangrc'
20
+
21
+ def load_config():
22
+ """Load configuration from ~/.shebangrc"""
23
+ config = {}
24
+ if CONFIG_FILE.exists():
25
+ with open(CONFIG_FILE) as f:
26
+ for line in f:
27
+ line = line.strip()
28
+ if line and not line.startswith('#') and '=' in line:
29
+ key, value = line.split('=', 1)
30
+ config[key] = value.strip('"')
31
+ return config
32
+
33
+ def save_config(config):
34
+ """Save configuration to ~/.shebangrc"""
35
+ with open(CONFIG_FILE, 'w') as f:
36
+ f.write('# shebang.run CLI configuration\n')
37
+ for key, value in config.items():
38
+ f.write(f'{key}="{value}"\n')
39
+ CONFIG_FILE.chmod(0o600)
40
+
41
+ def cmd_login(args):
42
+ """Login and generate API credentials"""
43
+ print("shebang.run Login")
44
+ print("=" * 50)
45
+ print()
46
+
47
+ url = input("Server URL [https://shebang.run]: ").strip() or "https://shebang.run"
48
+ username = input("Username: ").strip()
49
+ if not username:
50
+ print("Error: Username required", file=sys.stderr)
51
+ sys.exit(1)
52
+
53
+ import getpass
54
+ password = getpass.getpass("Password: ")
55
+ if not password:
56
+ print("Error: Password required", file=sys.stderr)
57
+ sys.exit(1)
58
+
59
+ key_path = input("Private Key Path (optional): ").strip()
60
+
61
+ # Login
62
+ print("Logging in...")
63
+ client = ShebangClient(url=url.replace('https://', '').replace('http://', ''))
64
+
65
+ try:
66
+ response = client.login(username, password)
67
+ token = response['token']
68
+ except Exception as e:
69
+ print(f"Error: Login failed - {e}", file=sys.stderr)
70
+ sys.exit(1)
71
+
72
+ # Generate API token
73
+ print("Generating API credentials...")
74
+ try:
75
+ import datetime
76
+ token_name = f"CLI-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
77
+ api_token = client.create_api_token(token_name)
78
+
79
+ config = {
80
+ 'SHEBANG_URL': url,
81
+ 'SHEBANG_USERNAME': username,
82
+ 'SHEBANG_CLIENT_ID': api_token['client_id'],
83
+ 'SHEBANG_CLIENT_SECRET': api_token['client_secret'],
84
+ 'SHEBANG_KEY_PATH': key_path
85
+ }
86
+ save_config(config)
87
+
88
+ print("✓ API credentials generated!")
89
+ print()
90
+ print(f"Client ID: {api_token['client_id']}")
91
+ print(f"Client Secret: {api_token['client_secret'][:20]}...")
92
+ print()
93
+ print(f"Config saved to {CONFIG_FILE}")
94
+ print("Keep your credentials secure!")
95
+
96
+ except Exception as e:
97
+ print(f"Error: Failed to generate API credentials - {e}", file=sys.stderr)
98
+ sys.exit(1)
99
+
100
+ def cmd_list(args):
101
+ """List scripts"""
102
+ config = load_config()
103
+ if not config.get('SHEBANG_CLIENT_ID'):
104
+ print("Error: Not logged in. Run: shebang login", file=sys.stderr)
105
+ sys.exit(1)
106
+
107
+ client = ShebangClient(url=config['SHEBANG_URL'].replace('https://', '').replace('http://', ''))
108
+ client.session.auth = (config['SHEBANG_CLIENT_ID'], config['SHEBANG_CLIENT_SECRET'])
109
+
110
+ print("Your Scripts:")
111
+ print("=" * 50)
112
+
113
+ try:
114
+ scripts = client.list_scripts()
115
+ for s in scripts:
116
+ vis = s['visibility']
117
+ color = '\033[0;32m' if vis == 'public' else '\033[1;33m' if vis == 'unlisted' else '\033[0;31m'
118
+ print(f"{color}{s['name']}\033[0m (v{s['version']}) - {s.get('description', 'No description')} [{vis}]")
119
+ except Exception as e:
120
+ print(f"Error: {e}", file=sys.stderr)
121
+ sys.exit(1)
122
+
123
+ if args.community:
124
+ print()
125
+ print("Community Scripts:")
126
+ print("=" * 50)
127
+
128
+ try:
129
+ import requests
130
+ response = requests.get(
131
+ f"{config['SHEBANG_URL']}/api/community/scripts",
132
+ auth=(config['SHEBANG_CLIENT_ID'], config['SHEBANG_CLIENT_SECRET'])
133
+ )
134
+ scripts = response.json()
135
+ for s in scripts:
136
+ print(f"\033[0;32m{s['username']}/{s['name']}\033[0m (v{s['version']}) - {s.get('description', 'No description')}")
137
+ except Exception as e:
138
+ print(f"Error: {e}", file=sys.stderr)
139
+
140
+ def cmd_search(args):
141
+ """Search scripts"""
142
+ config = load_config()
143
+ if not config.get('SHEBANG_CLIENT_ID'):
144
+ print("Error: Not logged in. Run: shebang login", file=sys.stderr)
145
+ sys.exit(1)
146
+
147
+ if not args.query:
148
+ print("Error: Search query required", file=sys.stderr)
149
+ sys.exit(1)
150
+
151
+ client = ShebangClient(url=config['SHEBANG_URL'].replace('https://', '').replace('http://', ''))
152
+ client.session.auth = (config['SHEBANG_CLIENT_ID'], config['SHEBANG_CLIENT_SECRET'])
153
+
154
+ query = args.query.lower()
155
+
156
+ print(f"Searching for: {args.query}")
157
+ print()
158
+
159
+ try:
160
+ scripts = client.list_scripts()
161
+ found = False
162
+ for s in scripts:
163
+ if query in s['name'].lower() or query in s.get('description', '').lower():
164
+ found = True
165
+ vis = s['visibility']
166
+ color = '\033[0;32m' if vis == 'public' else '\033[1;33m' if vis == 'unlisted' else '\033[0;31m'
167
+ print(f"{color}{s['name']}\033[0m (v{s['version']}) - {s.get('description', 'No description')} [{vis}]")
168
+
169
+ if not found:
170
+ print("No matches in your scripts")
171
+ except Exception as e:
172
+ print(f"Error: {e}", file=sys.stderr)
173
+
174
+ if args.community:
175
+ print()
176
+ print("Community Results:")
177
+
178
+ try:
179
+ import requests
180
+ response = requests.get(
181
+ f"{config['SHEBANG_URL']}/api/community/scripts",
182
+ auth=(config['SHEBANG_CLIENT_ID'], config['SHEBANG_CLIENT_SECRET'])
183
+ )
184
+ scripts = response.json()
185
+ found = False
186
+ for s in scripts:
187
+ if query in s['name'].lower() or query in s.get('description', '').lower() or query in s['username'].lower():
188
+ found = True
189
+ print(f"\033[0;32m{s['username']}/{s['name']}\033[0m (v{s['version']}) - {s.get('description', 'No description')}")
190
+
191
+ if not found:
192
+ print("No matches in community")
193
+ except Exception as e:
194
+ print(f"Error: {e}", file=sys.stderr)
195
+
196
+ def cmd_get(args):
197
+ """Get a script"""
198
+ config = load_config()
199
+
200
+ user = args.user or config.get('SHEBANG_USERNAME')
201
+ if not user:
202
+ print("Error: Username required. Use -u or login first.", file=sys.stderr)
203
+ sys.exit(1)
204
+
205
+ key_path = args.key or config.get('SHEBANG_KEY_PATH')
206
+ key_content = None
207
+ if key_path and os.path.exists(key_path):
208
+ with open(key_path) as f:
209
+ key_content = f.read()
210
+
211
+ try:
212
+ content = run(
213
+ username=user,
214
+ script=args.script,
215
+ key=key_content,
216
+ url=config.get('SHEBANG_URL', 'shebang.run').replace('https://', '').replace('http://', '')
217
+ )
218
+
219
+ if args.output:
220
+ with open(args.output, 'w') as f:
221
+ f.write(content)
222
+ print(f"✓ Script saved to: {args.output}")
223
+ else:
224
+ print(content)
225
+
226
+ except Exception as e:
227
+ print(f"Error: {e}", file=sys.stderr)
228
+ sys.exit(1)
229
+
230
+ def cmd_run(args):
231
+ """Run a script"""
232
+ config = load_config()
233
+
234
+ user = args.user or config.get('SHEBANG_USERNAME')
235
+ if not user:
236
+ print("Error: Username required. Use -u or login first.", file=sys.stderr)
237
+ sys.exit(1)
238
+
239
+ key_path = args.key or config.get('SHEBANG_KEY_PATH')
240
+ key_content = None
241
+ if key_path and os.path.exists(key_path):
242
+ with open(key_path) as f:
243
+ key_content = f.read()
244
+
245
+ try:
246
+ content = run(
247
+ username=user,
248
+ script=args.script,
249
+ key=key_content,
250
+ url=config.get('SHEBANG_URL', 'shebang.run').replace('https://', '').replace('http://', '')
251
+ )
252
+
253
+ # Save to temp file or specified output
254
+ import tempfile
255
+ if args.output:
256
+ script_file = args.output
257
+ else:
258
+ fd, script_file = tempfile.mkstemp(suffix=f'-{args.script}')
259
+ os.close(fd)
260
+
261
+ with open(script_file, 'w') as f:
262
+ f.write(content)
263
+ os.chmod(script_file, 0o755)
264
+
265
+ # Show script and prompt if not auto-accept
266
+ if not args.accept:
267
+ print()
268
+ print("Script content:")
269
+ print("=" * 60)
270
+ print(content)
271
+ print("=" * 60)
272
+ print()
273
+ confirm = input("Execute this script? (y/N): ").strip().lower()
274
+ if confirm != 'y':
275
+ if not args.output:
276
+ os.unlink(script_file)
277
+ print("Execution cancelled")
278
+ sys.exit(0)
279
+
280
+ # Execute script
281
+ print("Executing script...")
282
+ print()
283
+
284
+ import subprocess
285
+ result = subprocess.run(
286
+ [script_file] + (args.script_args or []),
287
+ capture_output=False
288
+ )
289
+
290
+ # Cleanup
291
+ if args.delete or not args.output:
292
+ os.unlink(script_file)
293
+ elif not args.output:
294
+ print(f"\n✓ Script saved to: {script_file}")
295
+
296
+ sys.exit(result.returncode)
297
+
298
+ except Exception as e:
299
+ print(f"Error: {e}", file=sys.stderr)
300
+ sys.exit(1)
301
+
302
+ def cmd_list_keys(args):
303
+ """List keypairs"""
304
+ config = load_config()
305
+ if not config.get('SHEBANG_CLIENT_ID'):
306
+ print("Error: Not logged in. Run: shebang login", file=sys.stderr)
307
+ sys.exit(1)
308
+
309
+ client = ShebangClient(url=config['SHEBANG_URL'].replace('https://', '').replace('http://', ''))
310
+ client.session.auth = (config['SHEBANG_CLIENT_ID'], config['SHEBANG_CLIENT_SECRET'])
311
+
312
+ try:
313
+ keys = client.list_keys()
314
+ if not keys:
315
+ print("No keys found")
316
+ return
317
+
318
+ print("Your Keys:")
319
+ print("=" * 50)
320
+ for k in keys:
321
+ print(f"{k['name']}")
322
+ print(f" Created: {k['created_at']}")
323
+ print()
324
+ except Exception as e:
325
+ print(f"Error: {e}", file=sys.stderr)
326
+ sys.exit(1)
327
+
328
+ def cmd_create_key(args):
329
+ """Create a new keypair"""
330
+ config = load_config()
331
+ if not config.get('SHEBANG_CLIENT_ID'):
332
+ print("Error: Not logged in. Run: shebang login", file=sys.stderr)
333
+ sys.exit(1)
334
+
335
+ name = input("Key name: ").strip()
336
+ if not name:
337
+ print("Error: Key name required", file=sys.stderr)
338
+ sys.exit(1)
339
+
340
+ client = ShebangClient(url=config['SHEBANG_URL'].replace('https://', '').replace('http://', ''))
341
+ client.session.auth = (config['SHEBANG_CLIENT_ID'], config['SHEBANG_CLIENT_SECRET'])
342
+
343
+ try:
344
+ print("Generating RSA-4096 keypair...")
345
+ key = client.generate_key(name)
346
+
347
+ if args.output:
348
+ with open(args.output, 'w') as f:
349
+ f.write(key['private_key'])
350
+ print(f"✓ Private key saved to: {args.output}")
351
+ print(f"✓ Public key stored in account: {name}")
352
+ else:
353
+ print(key['private_key'])
354
+ except Exception as e:
355
+ print(f"Error: {e}", file=sys.stderr)
356
+ sys.exit(1)
357
+
358
+ def cmd_delete_key(args):
359
+ """Delete a keypair"""
360
+ config = load_config()
361
+ if not config.get('SHEBANG_CLIENT_ID'):
362
+ print("Error: Not logged in. Run: shebang login", file=sys.stderr)
363
+ sys.exit(1)
364
+
365
+ client = ShebangClient(url=config['SHEBANG_URL'].replace('https://', '').replace('http://', ''))
366
+ client.session.auth = (config['SHEBANG_CLIENT_ID'], config['SHEBANG_CLIENT_SECRET'])
367
+
368
+ try:
369
+ # Find key by name
370
+ keys = client.list_keys()
371
+ key = next((k for k in keys if k['name'] == args.keyname), None)
372
+
373
+ if not key:
374
+ print(f"Error: Key '{args.keyname}' not found", file=sys.stderr)
375
+ sys.exit(1)
376
+
377
+ confirm = input(f"Delete key '{args.keyname}'? (y/N): ").strip().lower()
378
+ if confirm != 'y':
379
+ print("Cancelled")
380
+ sys.exit(0)
381
+
382
+ client.delete_key(key['id'])
383
+ print(f"✓ Key '{args.keyname}' deleted")
384
+
385
+ except Exception as e:
386
+ print(f"Error: {e}", file=sys.stderr)
387
+ sys.exit(1)
388
+
389
+ def cmd_put(args):
390
+ """Upload a script"""
391
+ config = load_config()
392
+ if not config.get('SHEBANG_CLIENT_ID'):
393
+ print("Error: Not logged in. Run: shebang login", file=sys.stderr)
394
+ sys.exit(1)
395
+
396
+ # Read content
397
+ if args.stdin:
398
+ content = sys.stdin.read()
399
+ elif args.file:
400
+ with open(args.file) as f:
401
+ content = f.read()
402
+ else:
403
+ print("Error: Must specify -s/--stdin or -f/--file", file=sys.stderr)
404
+ sys.exit(1)
405
+
406
+ # Validate visibility
407
+ visibility_map = {'priv': 'private', 'unlist': 'unlisted', 'public': 'public'}
408
+ visibility = visibility_map.get(args.visibility, args.visibility)
409
+ if visibility not in ['private', 'unlisted', 'public']:
410
+ print("Error: Visibility must be priv, unlist, or public", file=sys.stderr)
411
+ sys.exit(1)
412
+
413
+ client = ShebangClient(url=config['SHEBANG_URL'].replace('https://', '').replace('http://', ''))
414
+ client.session.auth = (config['SHEBANG_CLIENT_ID'], config['SHEBANG_CLIENT_SECRET'])
415
+
416
+ try:
417
+ # Get keypair ID if specified
418
+ keypair_id = None
419
+ if args.keyname:
420
+ keys = client.list_keys()
421
+ key = next((k for k in keys if k['name'] == args.keyname), None)
422
+ if not key:
423
+ print(f"Error: Key '{args.keyname}' not found", file=sys.stderr)
424
+ sys.exit(1)
425
+ keypair_id = key['id']
426
+ elif visibility == 'private':
427
+ print("Error: Private scripts require -k/--keyname", file=sys.stderr)
428
+ sys.exit(1)
429
+
430
+ print(f"Uploading script '{args.name}'...")
431
+ result = client.create_script(
432
+ name=args.name,
433
+ content=content,
434
+ description=args.description or "",
435
+ visibility=visibility,
436
+ keypair_id=keypair_id
437
+ )
438
+
439
+ print(f"✓ Script created: {args.name} (v{result['version']})")
440
+ print(f" URL: {config['SHEBANG_URL']}/{config['SHEBANG_USERNAME']}/{args.name}")
441
+
442
+ except Exception as e:
443
+ print(f"Error: {e}", file=sys.stderr)
444
+ sys.exit(1)
445
+
446
+ def main():
447
+ parser = argparse.ArgumentParser(
448
+ prog='shebang',
449
+ description='CLI tool for shebang.run',
450
+ formatter_class=argparse.RawDescriptionHelpFormatter
451
+ )
452
+
453
+ subparsers = parser.add_subparsers(dest='command', help='Commands')
454
+
455
+ # Login
456
+ subparsers.add_parser('login', help='Login and generate API credentials')
457
+
458
+ # List
459
+ list_parser = subparsers.add_parser('list', help='List scripts')
460
+ list_parser.add_argument('-c', '--community', action='store_true', help='Include community scripts')
461
+
462
+ # Search
463
+ search_parser = subparsers.add_parser('search', help='Search scripts')
464
+ search_parser.add_argument('query', help='Search query')
465
+ search_parser.add_argument('-c', '--community', action='store_true', help='Include community scripts')
466
+
467
+ # Get
468
+ get_parser = subparsers.add_parser('get', help='Download a script')
469
+ get_parser.add_argument('script', help='Script name')
470
+ get_parser.add_argument('-u', '--user', help='Username')
471
+ get_parser.add_argument('-O', '--output', help='Output file')
472
+ get_parser.add_argument('-k', '--key', help='Private key path')
473
+
474
+ # Run
475
+ run_parser = subparsers.add_parser('run', help='Download and execute a script')
476
+ run_parser.add_argument('script', help='Script name')
477
+ run_parser.add_argument('-u', '--user', help='Username')
478
+ run_parser.add_argument('-O', '--output', help='Output file')
479
+ run_parser.add_argument('-k', '--key', help='Private key path')
480
+ run_parser.add_argument('-a', '--accept', action='store_true', help='Auto-accept execution')
481
+ run_parser.add_argument('-d', '--delete', action='store_true', help='Delete after execution')
482
+ run_parser.add_argument('script_args', nargs='*', help='Arguments to pass to script')
483
+
484
+ # List keys
485
+ subparsers.add_parser('list-keys', help='List your keypairs')
486
+
487
+ # Create key
488
+ create_key_parser = subparsers.add_parser('create-key', help='Generate a new keypair')
489
+ create_key_parser.add_argument('-O', '--output', help='Save private key to file')
490
+
491
+ # Delete key
492
+ delete_key_parser = subparsers.add_parser('delete-key', help='Delete a keypair')
493
+ delete_key_parser.add_argument('keyname', help='Key name to delete')
494
+
495
+ # Put (upload script)
496
+ put_parser = subparsers.add_parser('put', help='Upload a script')
497
+ put_parser.add_argument('-n', '--name', required=True, help='Script name')
498
+ put_parser.add_argument('-v', '--visibility', required=True, choices=['priv', 'unlist', 'public'], help='Visibility')
499
+ put_parser.add_argument('-d', '--description', default='', help='Description')
500
+ put_parser.add_argument('-k', '--keyname', help='Key name for encryption (required for private)')
501
+ put_parser.add_argument('-s', '--stdin', action='store_true', help='Read from stdin')
502
+ put_parser.add_argument('-f', '--file', help='Read from file')
503
+
504
+ args = parser.parse_args()
505
+
506
+ if not args.command:
507
+ parser.print_help()
508
+ sys.exit(0)
509
+
510
+ if args.command == 'login':
511
+ cmd_login(args)
512
+ elif args.command == 'list':
513
+ cmd_list(args)
514
+ elif args.command == 'search':
515
+ cmd_search(args)
516
+ elif args.command == 'get':
517
+ cmd_get(args)
518
+ elif args.command == 'run':
519
+ cmd_run(args)
520
+ elif args.command == 'list-keys':
521
+ cmd_list_keys(args)
522
+ elif args.command == 'create-key':
523
+ cmd_create_key(args)
524
+ elif args.command == 'delete-key':
525
+ cmd_delete_key(args)
526
+ elif args.command == 'put':
527
+ cmd_put(args)
528
+
529
+ if __name__ == '__main__':
530
+ main()
shebangrun/client.py ADDED
@@ -0,0 +1,385 @@
1
+ """
2
+ Main client for interacting with shebang.run
3
+ """
4
+
5
+ import requests
6
+ from typing import Optional, Any
7
+
8
+
9
+ class ShebangClient:
10
+ """Client for interacting with shebang.run API"""
11
+
12
+ def __init__(self, url: str = "shebang.run", token: Optional[str] = None):
13
+ """
14
+ Initialize the client
15
+
16
+ Args:
17
+ url: Base URL (default: shebang.run)
18
+ token: JWT token for authenticated requests
19
+ """
20
+ self.base_url = f"https://{url}"
21
+ self.token = token
22
+ self.session = requests.Session()
23
+ if token:
24
+ self.session.headers.update({"Authorization": f"Bearer {token}"})
25
+
26
+ def get_script(self, username: str, script: str, version: Optional[str] = None,
27
+ token: Optional[str] = None) -> tuple:
28
+ """
29
+ Retrieve a script from shebang.run
30
+
31
+ Args:
32
+ username: Script owner's username
33
+ script: Script name
34
+ version: Optional version tag (@latest, @v1, @dev, etc.)
35
+ token: Optional share token for private scripts
36
+
37
+ Returns:
38
+ Tuple of (content, metadata) where metadata includes encryption info
39
+ """
40
+ script_path = f"{script}@{version}" if version else script
41
+ url = f"{self.base_url}/{username}/{script_path}"
42
+
43
+ params = {}
44
+ if token:
45
+ params["token"] = token
46
+
47
+ response = self.session.get(url, params=params)
48
+ response.raise_for_status()
49
+
50
+ metadata = {
51
+ "encrypted": response.headers.get("X-Encrypted") == "true",
52
+ "version": response.headers.get("X-Script-Version"),
53
+ "checksum": response.headers.get("X-Script-Checksum"),
54
+ "key_id": response.headers.get("X-Encryption-KeyID"),
55
+ "wrapped_key": response.headers.get("X-Wrapped-Key")
56
+ }
57
+
58
+ return response.content if metadata["encrypted"] else response.text, metadata
59
+
60
+ def get_metadata(self, username: str, script: str) -> dict:
61
+ """Get script metadata"""
62
+ url = f"{self.base_url}/{username}/{script}/meta"
63
+ response = self.session.get(url)
64
+ response.raise_for_status()
65
+ return response.json()
66
+
67
+ def verify_signature(self, username: str, script: str) -> dict:
68
+ """Verify script signature"""
69
+ url = f"{self.base_url}/{username}/{script}/verify"
70
+ response = self.session.get(url)
71
+ response.raise_for_status()
72
+ return response.json()
73
+
74
+ # Authentication
75
+ def register(self, username: str, email: str, password: str) -> dict:
76
+ """Register a new user"""
77
+ url = f"{self.base_url}/api/auth/register"
78
+ response = self.session.post(url, json={
79
+ "username": username,
80
+ "email": email,
81
+ "password": password
82
+ })
83
+ response.raise_for_status()
84
+ data = response.json()
85
+ if "token" in data:
86
+ self.token = data["token"]
87
+ self.session.headers.update({"Authorization": f"Bearer {self.token}"})
88
+ return data
89
+
90
+ def login(self, username: str, password: str) -> dict:
91
+ """Login and get JWT token"""
92
+ url = f"{self.base_url}/api/auth/login"
93
+ response = self.session.post(url, json={
94
+ "username": username,
95
+ "password": password
96
+ })
97
+ response.raise_for_status()
98
+ data = response.json()
99
+ if "token" in data:
100
+ self.token = data["token"]
101
+ self.session.headers.update({"Authorization": f"Bearer {self.token}"})
102
+ return data
103
+
104
+ # Script Management
105
+ def list_scripts(self) -> list:
106
+ """List user's scripts (requires authentication)"""
107
+ url = f"{self.base_url}/api/scripts"
108
+ response = self.session.get(url)
109
+ response.raise_for_status()
110
+ return response.json()
111
+
112
+ def create_script(self, name: str, content: str, description: str = "",
113
+ visibility: str = "private", keypair_id: Optional[int] = None) -> dict:
114
+ """Create a new script"""
115
+ url = f"{self.base_url}/api/scripts"
116
+ data = {
117
+ "name": name,
118
+ "content": content,
119
+ "description": description,
120
+ "visibility": visibility
121
+ }
122
+ if keypair_id:
123
+ data["keypair_id"] = keypair_id
124
+
125
+ response = self.session.post(url, json=data)
126
+ response.raise_for_status()
127
+ return response.json()
128
+
129
+ def update_script(self, script_id: int, content: Optional[str] = None,
130
+ description: Optional[str] = None, visibility: Optional[str] = None,
131
+ tag: Optional[str] = None, keypair_id: Optional[int] = None) -> dict:
132
+ """Update a script (creates new version if content changed)"""
133
+ url = f"{self.base_url}/api/scripts/{script_id}"
134
+ data = {}
135
+ if content:
136
+ data["content"] = content
137
+ if description:
138
+ data["description"] = description
139
+ if visibility:
140
+ data["visibility"] = visibility
141
+ if tag:
142
+ data["tag"] = tag
143
+ if keypair_id:
144
+ data["keypair_id"] = keypair_id
145
+
146
+ response = self.session.put(url, json=data)
147
+ response.raise_for_status()
148
+ return response.json() if response.text else {}
149
+
150
+ def delete_script(self, script_id: int):
151
+ """Delete a script"""
152
+ url = f"{self.base_url}/api/scripts/{script_id}"
153
+ response = self.session.delete(url)
154
+ response.raise_for_status()
155
+
156
+ def generate_share_token(self, script_id: int) -> str:
157
+ """Generate a share token for a private script"""
158
+ url = f"{self.base_url}/api/scripts/{script_id}/share"
159
+ response = self.session.post(url)
160
+ response.raise_for_status()
161
+ return response.json()["token"]
162
+
163
+ def revoke_share_token(self, script_id: int, token: str):
164
+ """Revoke a share token"""
165
+ url = f"{self.base_url}/api/scripts/{script_id}/share/{token}"
166
+ response = self.session.delete(url)
167
+ response.raise_for_status()
168
+
169
+ # Key Management
170
+ def list_keys(self) -> list:
171
+ """List user's keypairs"""
172
+ url = f"{self.base_url}/api/keys"
173
+ response = self.session.get(url)
174
+ response.raise_for_status()
175
+ return response.json()
176
+
177
+ def generate_key(self, name: str) -> dict:
178
+ """Generate a new keypair (returns private key - save it!)"""
179
+ url = f"{self.base_url}/api/keys/generate"
180
+ response = self.session.post(url, json={"name": name})
181
+ response.raise_for_status()
182
+ return response.json()
183
+
184
+ def import_key(self, name: str, public_key: str) -> dict:
185
+ """Import an existing public key"""
186
+ url = f"{self.base_url}/api/keys/import"
187
+ response = self.session.post(url, json={
188
+ "name": name,
189
+ "public_key": public_key
190
+ })
191
+ response.raise_for_status()
192
+ return response.json()
193
+
194
+ def delete_key(self, key_id: int):
195
+ """Delete a keypair"""
196
+ url = f"{self.base_url}/api/keys/{key_id}"
197
+ response = self.session.delete(url)
198
+ response.raise_for_status()
199
+
200
+ # Account Management
201
+ def change_password(self, current_password: str, new_password: str):
202
+ """Change account password"""
203
+ url = f"{self.base_url}/api/account/password"
204
+ response = self.session.put(url, json={
205
+ "current_password": current_password,
206
+ "new_password": new_password
207
+ })
208
+ response.raise_for_status()
209
+
210
+ def export_data(self) -> dict:
211
+ """Export all user data (GDPR)"""
212
+ url = f"{self.base_url}/api/account/export"
213
+ response = self.session.get(url)
214
+ response.raise_for_status()
215
+ return response.json()
216
+
217
+ def delete_account(self):
218
+ """Delete account permanently"""
219
+ url = f"{self.base_url}/api/account"
220
+ response = self.session.delete(url)
221
+ response.raise_for_status()
222
+
223
+ def create_api_token(self, name: str) -> dict:
224
+ """Create API token for CLI access"""
225
+ url = f"{self.base_url}/api/account/tokens"
226
+ response = self.session.post(url, json={"name": name})
227
+ response.raise_for_status()
228
+ return response.json()
229
+
230
+
231
+ def run(username: str, script: str, key: Optional[str] = None,
232
+ eval: bool = False, accept: bool = False, url: str = "shebang.run",
233
+ version: Optional[str] = None, token: Optional[str] = None) -> Any:
234
+ """
235
+ Convenience function to fetch and optionally execute a script
236
+
237
+ Args:
238
+ username: Script owner's username (required)
239
+ script: Script name (required)
240
+ key: Private key contents for decryption (optional)
241
+ eval: If True, evaluate the script in Python (default: False)
242
+ accept: If True, skip confirmation prompt when eval=True (default: False)
243
+ url: Base URL (default: shebang.run)
244
+ version: Version tag (optional, e.g., "latest", "v1", "dev")
245
+ token: Share token for private scripts (optional)
246
+
247
+ Returns:
248
+ Script content as string if eval=False, or result of eval() if eval=True
249
+
250
+ Examples:
251
+ # Just fetch the script
252
+ content = run(username="mpruitt", script="bashtest")
253
+
254
+ # Fetch encrypted script with private key
255
+ content = run(username="mpruitt", script="private", key="-----BEGIN PRIVATE KEY-----\\n...")
256
+
257
+ # Fetch and evaluate with confirmation
258
+ run(username="mpruitt", script="myscript", eval=True)
259
+ """
260
+ client = ShebangClient(url=url)
261
+
262
+ # Fetch the script
263
+ content, metadata = client.get_script(username, script, version=version, token=token)
264
+
265
+ # If encrypted and key is provided, decrypt
266
+ if metadata.get("encrypted") and key:
267
+ try:
268
+ from cryptography.hazmat.primitives import serialization, hashes
269
+ from cryptography.hazmat.primitives.asymmetric import padding
270
+ from cryptography.hazmat.backends import default_backend
271
+ import nacl.bindings
272
+
273
+ # Parse private key
274
+ private_key = serialization.load_pem_private_key(
275
+ key.encode() if isinstance(key, str) else key,
276
+ password=None,
277
+ backend=default_backend()
278
+ )
279
+
280
+ # Get wrapped key from headers
281
+ wrapped_key_hex = metadata.get("wrapped_key")
282
+ if not wrapped_key_hex:
283
+ raise Exception("No wrapped key found in response")
284
+
285
+ # Unwrap symmetric key with RSA private key
286
+ wrapped_key = bytes.fromhex(wrapped_key_hex)
287
+ symmetric_key = private_key.decrypt(
288
+ wrapped_key,
289
+ padding.OAEP(
290
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
291
+ algorithm=hashes.SHA256(),
292
+ label=None
293
+ )
294
+ )
295
+
296
+ # Decrypt content with XChaCha20-Poly1305
297
+ nonce_size = 24
298
+ nonce = content[:nonce_size]
299
+ ciphertext = content[nonce_size:]
300
+
301
+ decrypted = nacl.bindings.crypto_aead_xchacha20poly1305_ietf_decrypt(
302
+ ciphertext,
303
+ None, # no additional data
304
+ nonce,
305
+ symmetric_key
306
+ )
307
+
308
+ content = decrypted.decode('utf-8')
309
+
310
+ except ImportError:
311
+ raise ImportError("Decryption requires: pip install cryptography pynacl")
312
+ except Exception as e:
313
+ raise Exception(f"Decryption failed: {e}")
314
+ elif metadata.get("encrypted") and not key:
315
+ # Return encrypted bytes if no key provided
316
+ return content
317
+
318
+ # Convert bytes to string if needed
319
+ if isinstance(content, bytes):
320
+ content = content.decode('utf-8')
321
+
322
+ # If not evaluating, just return the content
323
+ if not eval:
324
+ return content
325
+
326
+ # If evaluating, show confirmation unless accept=True
327
+ if not accept:
328
+ print("=" * 60)
329
+ print(f"Script: {username}/{script}")
330
+ print("=" * 60)
331
+ print(content)
332
+ print("=" * 60)
333
+ response = input("Execute this script? (y/N): ").strip().lower()
334
+ if response != 'y':
335
+ print("Execution cancelled.")
336
+ return None
337
+
338
+ # Execute the script
339
+ try:
340
+ import subprocess
341
+ import tempfile
342
+ import os
343
+ import sys
344
+
345
+ # Check for shebang
346
+ lines = content.split('\n')
347
+ if lines[0].startswith('#!'):
348
+ shebang = lines[0].lower()
349
+
350
+ # If shebang contains 'python', execute in current context with exec
351
+ if 'python' in shebang:
352
+ exec_globals = {}
353
+ exec(content, exec_globals)
354
+ return exec_globals
355
+ else:
356
+ # Execute with specified interpreter (bash, sh, etc.)
357
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
358
+ f.write(content)
359
+ temp_path = f.name
360
+
361
+ try:
362
+ os.chmod(temp_path, 0o755)
363
+ result = subprocess.run(
364
+ [temp_path],
365
+ capture_output=True,
366
+ text=True,
367
+ shell=False
368
+ )
369
+
370
+ if result.stdout:
371
+ print(result.stdout, end='')
372
+ if result.stderr:
373
+ print(result.stderr, end='', file=sys.stderr)
374
+
375
+ return result.returncode
376
+ finally:
377
+ os.unlink(temp_path)
378
+ else:
379
+ # No shebang, execute as Python
380
+ exec_globals = {}
381
+ exec(content, exec_globals)
382
+ return exec_globals
383
+ except Exception as e:
384
+ print(f"Error executing script: {e}")
385
+ raise
@@ -0,0 +1,419 @@
1
+ Metadata-Version: 2.4
2
+ Name: shebangrun
3
+ Version: 0.2.0
4
+ Summary: Python client library for shebang.run
5
+ Author-email: "shebang.run" <hello@shebang.run>
6
+ License: MIT
7
+ Project-URL: Homepage, https://shebang.run
8
+ Project-URL: Documentation, https://shebang.run/docs
9
+ Project-URL: Repository, https://github.com/skibare87/shebangrun
10
+ Project-URL: Bug Tracker, https://github.com/skibare87/shebangrun/issues
11
+ Keywords: shebang,scripts,automation,devops
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.7
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Requires-Python: >=3.7
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: requests>=2.25.0
24
+ Requires-Dist: cryptography>=3.4.0
25
+ Requires-Dist: pynacl>=1.4.0
26
+
27
+ # shebangrun Python Client
28
+
29
+ Python client library and CLI tool for [shebang.run](https://shebang.run) - a platform for hosting and sharing shell scripts with versioning, encryption, and signing.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install shebangrun
35
+ ```
36
+
37
+ This installs both the Python library and the `shebang` CLI tool.
38
+
39
+ ## CLI Tool
40
+
41
+ ### Quick Start
42
+
43
+ ```bash
44
+ # Login and generate API credentials
45
+ shebang login
46
+
47
+ # List your scripts
48
+ shebang list
49
+
50
+ # Get a script
51
+ shebang get myscript
52
+
53
+ # Run a script
54
+ shebang run myscript
55
+ ```
56
+
57
+ ### Commands
58
+
59
+ #### Login
60
+ ```bash
61
+ shebang login
62
+ ```
63
+ Interactive wizard that:
64
+ - Prompts for server URL, username, password
65
+ - Generates API credentials (Client ID/Secret)
66
+ - Saves to `~/.shebangrc`
67
+
68
+ #### List Scripts
69
+ ```bash
70
+ # Your scripts
71
+ shebang list
72
+
73
+ # Include community scripts
74
+ shebang list -c
75
+ ```
76
+
77
+ #### Search Scripts
78
+ ```bash
79
+ # Search your scripts
80
+ shebang search "deploy"
81
+
82
+ # Search community
83
+ shebang search -c "backup"
84
+ ```
85
+
86
+ #### Get Script
87
+ ```bash
88
+ # Download to stdout
89
+ shebang get myscript
90
+
91
+ # From another user
92
+ shebang get -u username scriptname
93
+
94
+ # Save to file
95
+ shebang get -O deploy.sh myscript
96
+
97
+ # Decrypt with private key
98
+ shebang get -k private.pem encrypted-script
99
+ ```
100
+
101
+ #### Run Script
102
+ ```bash
103
+ # Run with confirmation
104
+ shebang run myscript
105
+
106
+ # Auto-accept (no prompt)
107
+ shebang run -a myscript
108
+
109
+ # Pass arguments
110
+ shebang run myscript arg1 arg2
111
+
112
+ # Run and delete
113
+ shebang run -d myscript
114
+ ```
115
+
116
+ #### Key Management
117
+ ```bash
118
+ # List keys
119
+ shebang list-keys
120
+
121
+ # Create new keypair
122
+ shebang create-key
123
+ shebang create-key -O mykey.pem
124
+
125
+ # Delete key
126
+ shebang delete-key keyname
127
+ ```
128
+
129
+ #### Upload Script
130
+ ```bash
131
+ # Upload from file
132
+ shebang put -n myscript -v public -f script.sh
133
+
134
+ # Upload from stdin
135
+ cat script.sh | shebang put -n myscript -v public -s
136
+
137
+ # Upload private script with encryption
138
+ shebang put -n private-script -v priv -k my-key -f script.sh -d "My private script"
139
+ ```
140
+
141
+ ### CLI Options
142
+
143
+ **Visibility:**
144
+ - `priv` - Private (encrypted, requires key)
145
+ - `unlist` - Unlisted (accessible via URL only)
146
+ - `public` - Public (listed in community)
147
+
148
+ **Configuration:**
149
+ Stored in `~/.shebangrc`:
150
+ ```bash
151
+ SHEBANG_URL="https://shebang.run"
152
+ SHEBANG_USERNAME="myuser"
153
+ SHEBANG_CLIENT_ID="..."
154
+ SHEBANG_CLIENT_SECRET="..."
155
+ SHEBANG_KEY_PATH="/path/to/key.pem"
156
+ ```
157
+
158
+ ## Python Library
159
+
160
+ ## Python Library
161
+
162
+ ### Simple Script Fetching
163
+
164
+ ```python
165
+ from shebangrun import run
166
+
167
+ # Fetch a script (returns content as string)
168
+ content = run(username="mpruitt", script="bashtest")
169
+ print(content)
170
+ ```
171
+
172
+ ### Execute Python Scripts
173
+
174
+ ```python
175
+ from shebangrun import run
176
+
177
+ # Fetch and execute with confirmation prompt
178
+ run(username="mpruitt", script="myscript", eval=True)
179
+
180
+ # Execute without confirmation (use with caution!)
181
+ run(username="mpruitt", script="myscript", eval=True, accept=True)
182
+ ```
183
+
184
+ ### Working with Versions
185
+
186
+ ```python
187
+ from shebangrun import run
188
+
189
+ # Get latest version
190
+ content = run(username="mpruitt", script="deploy", version="latest")
191
+
192
+ # Get specific version
193
+ content = run(username="mpruitt", script="deploy", version="v5")
194
+
195
+ # Get tagged version
196
+ content = run(username="mpruitt", script="deploy", version="dev")
197
+ ```
198
+
199
+ ### Private Scripts
200
+
201
+ ```python
202
+ from shebangrun import run
203
+
204
+ # Access private script with share token
205
+ content = run(
206
+ username="mpruitt",
207
+ script="private-script",
208
+ token="your-share-token-here"
209
+ )
210
+ ```
211
+
212
+ ## Full API Client
213
+
214
+ For more advanced usage, use the `ShebangClient` class:
215
+
216
+ ```python
217
+ from shebangrun import ShebangClient
218
+
219
+ # Initialize client
220
+ client = ShebangClient(url="shebang.run")
221
+
222
+ # Login
223
+ client.login(username="myuser", password="mypassword")
224
+
225
+ # Create a script
226
+ client.create_script(
227
+ name="hello",
228
+ content="#!/bin/bash\necho 'Hello World'",
229
+ description="My first script",
230
+ visibility="public"
231
+ )
232
+
233
+ # List your scripts
234
+ scripts = client.list_scripts()
235
+ for script in scripts:
236
+ print(f"{script['name']} - v{script['version']}")
237
+
238
+ # Update a script (creates new version)
239
+ client.update_script(
240
+ script_id=1,
241
+ content="#!/bin/bash\necho 'Hello World v2'",
242
+ tag="dev"
243
+ )
244
+
245
+ # Generate share token for private script
246
+ token = client.generate_share_token(script_id=1)
247
+ print(f"Share URL: https://shebang.run/myuser/myscript?token={token}")
248
+
249
+ # Get script metadata
250
+ meta = client.get_metadata(username="mpruitt", script="bashtest")
251
+ print(f"Version: {meta['version']}, Size: {meta['size']} bytes")
252
+
253
+ # Verify signature
254
+ verification = client.verify_signature(username="mpruitt", script="bashtest")
255
+ print(f"Signed: {verification['signed']}")
256
+ ```
257
+
258
+ ## Key Management
259
+
260
+ ```python
261
+ from shebangrun import ShebangClient
262
+
263
+ client = ShebangClient(url="shebang.run")
264
+ client.login(username="myuser", password="mypassword")
265
+
266
+ # Generate a new keypair
267
+ key = client.generate_key(name="my-signing-key")
268
+ print(f"Public Key: {key['public_key']}")
269
+ print(f"Private Key: {key['private_key']}") # Save this securely!
270
+
271
+ # List keys
272
+ keys = client.list_keys()
273
+ for key in keys:
274
+ print(f"{key['name']} - Created: {key['created_at']}")
275
+
276
+ # Import existing public key
277
+ client.import_key(
278
+ name="imported-key",
279
+ public_key="-----BEGIN PUBLIC KEY-----\n..."
280
+ )
281
+
282
+ # Delete a key
283
+ client.delete_key(key_id=1)
284
+ ```
285
+
286
+ ## Account Management
287
+
288
+ ```python
289
+ from shebangrun import ShebangClient
290
+
291
+ client = ShebangClient(url="shebang.run")
292
+ client.login(username="myuser", password="mypassword")
293
+
294
+ # Change password
295
+ client.change_password(
296
+ current_password="oldpass",
297
+ new_password="newpass"
298
+ )
299
+
300
+ # Export all data (GDPR)
301
+ data = client.export_data()
302
+ print(f"Exported {len(data['scripts'])} scripts")
303
+
304
+ # Delete account (permanent!)
305
+ client.delete_account()
306
+ ```
307
+
308
+ ## API Reference
309
+
310
+ ### `run()` Function
311
+
312
+ ```python
313
+ run(username, script, key=None, eval=False, accept=False,
314
+ url="shebang.run", version=None, token=None)
315
+ ```
316
+
317
+ **Parameters:**
318
+ - `username` (str, required): Script owner's username
319
+ - `script` (str, required): Script name
320
+ - `key` (str, optional): Private key for decryption (not yet implemented)
321
+ - `eval` (bool, optional): Execute the script in Python (default: False)
322
+ - `accept` (bool, optional): Skip confirmation when eval=True (default: False)
323
+ - `url` (str, optional): Base URL (default: "shebang.run")
324
+ - `version` (str, optional): Version tag (e.g., "latest", "v1", "dev")
325
+ - `token` (str, optional): Share token for private scripts
326
+
327
+ **Returns:**
328
+ - String content if `eval=False`
329
+ - Execution result if `eval=True`
330
+
331
+ ### `ShebangClient` Class
332
+
333
+ #### Authentication
334
+ - `register(username, email, password)` - Register new user
335
+ - `login(username, password)` - Login and get JWT token
336
+
337
+ #### Script Management
338
+ - `list_scripts()` - List user's scripts
339
+ - `get_script(username, script, version=None, token=None)` - Fetch script content
340
+ - `get_metadata(username, script)` - Get script metadata
341
+ - `verify_signature(username, script)` - Verify script signature
342
+ - `create_script(name, content, description="", visibility="private", keypair_id=None)` - Create script
343
+ - `update_script(script_id, content=None, description=None, visibility=None, tag=None, keypair_id=None)` - Update script
344
+ - `delete_script(script_id)` - Delete script
345
+ - `generate_share_token(script_id)` - Generate share token
346
+ - `revoke_share_token(script_id, token)` - Revoke share token
347
+
348
+ #### Key Management
349
+ - `list_keys()` - List keypairs
350
+ - `generate_key(name)` - Generate new keypair
351
+ - `import_key(name, public_key)` - Import public key
352
+ - `delete_key(key_id)` - Delete keypair
353
+
354
+ #### Account Management
355
+ - `change_password(current_password, new_password)` - Change password
356
+ - `export_data()` - Export all data (GDPR)
357
+ - `delete_account()` - Delete account
358
+
359
+ ## Security Notes
360
+
361
+ ⚠️ **Warning:** Using `eval=True` with `accept=True` will execute remote code without confirmation. Only use this with scripts you trust completely.
362
+
363
+ ✅ **Best Practices:**
364
+ - Always review scripts before executing with `eval=True`
365
+ - Use `accept=False` (default) to see the script before execution
366
+ - Store private keys securely, never commit them to version control
367
+ - Use environment variables for tokens and credentials
368
+ - Verify script signatures when available
369
+
370
+ ## Examples
371
+
372
+ ### Automation Script
373
+
374
+ ```python
375
+ #!/usr/bin/env python3
376
+ from shebangrun import run
377
+
378
+ # Fetch deployment script and execute with confirmation
379
+ run(
380
+ username="devops",
381
+ script="deploy-prod",
382
+ version="latest",
383
+ eval=True,
384
+ accept=False # Always confirm production deployments!
385
+ )
386
+ ```
387
+
388
+ ### CI/CD Integration
389
+
390
+ ```python
391
+ import os
392
+ from shebangrun import ShebangClient
393
+
394
+ client = ShebangClient()
395
+ client.login(
396
+ username=os.environ["SHEBANG_USER"],
397
+ password=os.environ["SHEBANG_PASS"]
398
+ )
399
+
400
+ # Update deployment script
401
+ client.update_script(
402
+ script_id=int(os.environ["DEPLOY_SCRIPT_ID"]),
403
+ content=open("deploy.sh").read(),
404
+ tag="latest"
405
+ )
406
+
407
+ print("Deployment script updated!")
408
+ ```
409
+
410
+ ## License
411
+
412
+ MIT
413
+
414
+ ## Links
415
+
416
+ - Website: https://shebang.run
417
+ - Documentation: https://shebang.run/docs
418
+ - GitHub: https://github.com/skibare87/shebangrun
419
+ - PyPI: https://pypi.org/project/shebangrun/
@@ -0,0 +1,8 @@
1
+ shebangrun/__init__.py,sha256=YQhIJG4ln6svf_ZGIk460mj8Juh8CDmNIYdaQ81BVYc,237
2
+ shebangrun/cli.py,sha256=1BFYH_Cgretn0I4s6sjJIciq5OWLzc01W3xS7WKw3dU,18495
3
+ shebangrun/client.py,sha256=k6kqNgweDdM1i4ZHJGx5FLAh7ZChBAWVgkUEb3uCmPc,14143
4
+ shebangrun-0.2.0.dist-info/METADATA,sha256=p1mxO152mxd_DXVhlWsfPQl1XcSYe-_D0YZ-xAXsTlQ,9941
5
+ shebangrun-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ shebangrun-0.2.0.dist-info/entry_points.txt,sha256=je5p9fSB9d-GzrI4XAeqMnzVAW9a-HNCZOZUnS2vp1U,48
7
+ shebangrun-0.2.0.dist-info/top_level.txt,sha256=XGgqFinUfnCC_bMMYqexpD-W9Pqh2i-LKrqSEdrPhgU,11
8
+ shebangrun-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ shebang = shebangrun.cli:main
@@ -0,0 +1 @@
1
+ shebangrun