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 +11 -0
- shebangrun/cli.py +530 -0
- shebangrun/client.py +385 -0
- shebangrun-0.2.0.dist-info/METADATA +419 -0
- shebangrun-0.2.0.dist-info/RECORD +8 -0
- shebangrun-0.2.0.dist-info/WHEEL +5 -0
- shebangrun-0.2.0.dist-info/entry_points.txt +2 -0
- shebangrun-0.2.0.dist-info/top_level.txt +1 -0
shebangrun/__init__.py
ADDED
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 @@
|
|
|
1
|
+
shebangrun
|