opensandbox-cli 1.1.0__tar.gz → 1.1.1__tar.gz
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.
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/PKG-INFO +109 -2
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/README.md +106 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/pyproject.toml +2 -1
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/client.py +0 -5
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/commands/command.py +47 -61
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/commands/config_cmd.py +46 -57
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/commands/devops.py +0 -9
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/commands/diagnostics.py +1 -10
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/commands/file.py +0 -24
- opensandbox_cli-1.1.1/src/opensandbox_cli/commands/sandbox.py +1057 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/commands/skills.py +0 -3
- opensandbox_cli-1.1.1/src/opensandbox_cli/commands/snapshot.py +170 -0
- opensandbox_cli-1.1.1/src/opensandbox_cli/commands/template.py +324 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/main.py +4 -5
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/output.py +28 -86
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/skill_registry.py +6 -4
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/skills/opensandbox-command-execution.md +2 -1
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/skills/opensandbox-sandbox-lifecycle.md +60 -1
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/utils.py +37 -40
- opensandbox_cli-1.1.0/src/opensandbox_cli/commands/sandbox.py +0 -594
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/.gitignore +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/LICENSE +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/__init__.py +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/__main__.py +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/commands/__init__.py +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/commands/credential_vault.py +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/commands/egress.py +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/config.py +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/py.typed +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/skills/opensandbox-credential-vault.md +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/skills/opensandbox-file-operations.md +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/skills/opensandbox-network-egress.md +0 -0
- {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.1}/src/opensandbox_cli/skills/opensandbox-sandbox-troubleshooting.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opensandbox-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: OpenSandbox CLI - Command-line interface for managing sandboxes
|
|
5
5
|
Project-URL: Homepage, https://open-sandbox.ai
|
|
6
6
|
Project-URL: Repository, https://github.com/opensandbox-group/OpenSandbox
|
|
@@ -224,10 +224,11 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
224
224
|
Classifier: Typing :: Typed
|
|
225
225
|
Requires-Python: >=3.10
|
|
226
226
|
Requires-Dist: click<9.0,>=8.1.0
|
|
227
|
-
Requires-Dist: opensandbox<2.0.0,>=1.1.
|
|
227
|
+
Requires-Dist: opensandbox<2.0.0,>=1.1.1
|
|
228
228
|
Requires-Dist: pyyaml<7.0,>=6.0
|
|
229
229
|
Requires-Dist: rich<14.0,>=13.0.0
|
|
230
230
|
Requires-Dist: tomli>=2.0.0; python_version < '3.11'
|
|
231
|
+
Requires-Dist: tomlkit<1.0,>=0.13.0
|
|
231
232
|
Description-Content-Type: text/markdown
|
|
232
233
|
|
|
233
234
|
# OpenSandbox CLI
|
|
@@ -235,6 +236,7 @@ Description-Content-Type: text/markdown
|
|
|
235
236
|
`osb` is the command-line interface for OpenSandbox. It is built for the common day-to-day flows:
|
|
236
237
|
|
|
237
238
|
- create and manage sandboxes
|
|
239
|
+
- build golden-image templates and create sandboxes from templates or snapshots
|
|
238
240
|
- run commands inside a sandbox
|
|
239
241
|
- read and modify sandbox files
|
|
240
242
|
- inspect runtime egress policy
|
|
@@ -387,6 +389,103 @@ Create with Credential Vault proxy enabled:
|
|
|
387
389
|
osb sandbox create --image python:3.12 --network-policy-file network-policy.json --credential-proxy -o json
|
|
388
390
|
```
|
|
389
391
|
|
|
392
|
+
Create from a template (golden image):
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
osb sandbox create --template <template-id> --timeout 30m -o json
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Template mode fixes the workload shape on the server: `--image`, `--env`,
|
|
399
|
+
`--resource`, `--entrypoint`, `--volumes-file`, and `--credential-proxy` cannot
|
|
400
|
+
be combined with `--template`. Only `--timeout` (required), `--metadata`,
|
|
401
|
+
`--extension`, and `--network-policy-file` may accompany it. Only templates in
|
|
402
|
+
the `Succeeded` phase can be used. See [Manage templates](#manage-templates).
|
|
403
|
+
|
|
404
|
+
Create from a snapshot:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
osb sandbox create --snapshot-id <snapshot-id> --timeout 30m -o json
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`--image`, `--template`, and `--snapshot-id` are mutually exclusive.
|
|
411
|
+
|
|
412
|
+
Create from a request file (`-f`, like `kubectl apply -f`):
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
osb sandbox create -f sandbox-request.json -o json
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`sandbox-request.json` uses the public `CreateSandboxRequest` JSON format
|
|
419
|
+
(camelCase keys):
|
|
420
|
+
|
|
421
|
+
```json
|
|
422
|
+
{
|
|
423
|
+
"image": "python:3.12",
|
|
424
|
+
"timeout": 1800,
|
|
425
|
+
"resourceLimits": { "cpu": "1", "memory": "2Gi" },
|
|
426
|
+
"entrypoint": ["python", "-m", "http.server"],
|
|
427
|
+
"networkPolicy": {
|
|
428
|
+
"defaultAction": "deny",
|
|
429
|
+
"egress": [{ "action": "allow", "target": "pypi.org" }]
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
- Exactly one of `image` (string or `{ "uri": ..., "auth": { "username": ..., "password": ... } }`),
|
|
435
|
+
`templateId`, or `snapshotId` is required; `timeout` is integer seconds, or
|
|
436
|
+
`null` for manual cleanup.
|
|
437
|
+
- Supported fields: `image`, `templateId`, `snapshotId`, `platform`, `timeout`,
|
|
438
|
+
`resourceLimits`, `resourceRequests`, `env`, `metadata`, `extensions`,
|
|
439
|
+
`entrypoint`, `networkPolicy`, `credentialProxy`, `secureAccess`, `volumes`,
|
|
440
|
+
`lifecycle`.
|
|
441
|
+
- `-f` cannot be combined with request-building flags; `--skip-health-check`,
|
|
442
|
+
`--ready-timeout`, and `-o` still apply. In template mode the file may only
|
|
443
|
+
set `timeout`, `metadata`, `extensions`, and `networkPolicy` alongside
|
|
444
|
+
`templateId` (a finite `timeout` is required).
|
|
445
|
+
|
|
446
|
+
### Manage templates
|
|
447
|
+
|
|
448
|
+
Templates are golden-image builds; the build runs asynchronously, so poll
|
|
449
|
+
`template get` until the status phase is `Succeeded`. Template management
|
|
450
|
+
requires a Kubernetes-backed runtime.
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
osb template create --image python:3.12 --publish s3://bucket/publish -o json
|
|
454
|
+
osb template get <template-id> -o json
|
|
455
|
+
osb template list -o json
|
|
456
|
+
osb template delete <template-id> -o json
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
`template create` also accepts `--resource cpu=1 memory=512Mi disk=2Gi`,
|
|
460
|
+
`--entrypoint` (repeat per argv item), `--env KEY=VALUE`, `--metadata
|
|
461
|
+
KEY=VALUE`, `--format native|overlaybd`, `--readiness-probe`, and
|
|
462
|
+
`--warmup-seconds`. Or pass a request file in the public `CreateTemplateRequest`
|
|
463
|
+
JSON format:
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
osb template create -f template-request.json -o json
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
```json
|
|
470
|
+
{
|
|
471
|
+
"image": "python:3.12",
|
|
472
|
+
"publish": "s3://bucket/publish",
|
|
473
|
+
"resourceLimits": { "cpu": "1", "memory": "512Mi" },
|
|
474
|
+
"readiness": { "probe": "tcp://127.0.0.1:44772", "warmupSeconds": 60 }
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Manage snapshots
|
|
479
|
+
|
|
480
|
+
Snapshots capture a sandbox's state and can back new sandboxes.
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
osb snapshot create <sandbox-id> --name golden -o json
|
|
484
|
+
osb snapshot get <snapshot-id> -o json
|
|
485
|
+
osb snapshot list --sandbox-id <sandbox-id> -o json
|
|
486
|
+
osb snapshot delete <snapshot-id> -o json
|
|
487
|
+
```
|
|
488
|
+
|
|
390
489
|
### List and inspect sandboxes
|
|
391
490
|
|
|
392
491
|
```bash
|
|
@@ -431,6 +530,10 @@ osb command status <sandbox-id> <execution-id> -o json
|
|
|
431
530
|
osb command logs <sandbox-id> <execution-id> -o json
|
|
432
531
|
```
|
|
433
532
|
|
|
533
|
+
Background start errors or a stream ending before startup confirmation make the
|
|
534
|
+
CLI exit non-zero without success metadata. After a successful start, use
|
|
535
|
+
`command status` and `command logs` to check the process's eventual result.
|
|
536
|
+
|
|
434
537
|
By default the payload after `--` is joined into one shell command string, so
|
|
435
538
|
pipelines, redirection, and `$VAR` expansion work as in a terminal. Add `--argv`
|
|
436
539
|
to pass the arguments to the executable as a literal argv list (no shell) when
|
|
@@ -561,6 +664,8 @@ Not every command supports every format. Use `--help` on the specific command wh
|
|
|
561
664
|
The main command groups are:
|
|
562
665
|
|
|
563
666
|
- `osb sandbox`: lifecycle management
|
|
667
|
+
- `osb template`: golden-image template builds
|
|
668
|
+
- `osb snapshot`: snapshot management
|
|
564
669
|
- `osb command`: command execution and persistent sessions
|
|
565
670
|
- `osb file`: file and directory operations
|
|
566
671
|
- `osb egress`: runtime egress policy
|
|
@@ -573,6 +678,8 @@ Explore them directly:
|
|
|
573
678
|
|
|
574
679
|
```bash
|
|
575
680
|
osb sandbox --help
|
|
681
|
+
osb template --help
|
|
682
|
+
osb snapshot --help
|
|
576
683
|
osb command --help
|
|
577
684
|
osb file --help
|
|
578
685
|
osb skills --help
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
`osb` is the command-line interface for OpenSandbox. It is built for the common day-to-day flows:
|
|
4
4
|
|
|
5
5
|
- create and manage sandboxes
|
|
6
|
+
- build golden-image templates and create sandboxes from templates or snapshots
|
|
6
7
|
- run commands inside a sandbox
|
|
7
8
|
- read and modify sandbox files
|
|
8
9
|
- inspect runtime egress policy
|
|
@@ -155,6 +156,103 @@ Create with Credential Vault proxy enabled:
|
|
|
155
156
|
osb sandbox create --image python:3.12 --network-policy-file network-policy.json --credential-proxy -o json
|
|
156
157
|
```
|
|
157
158
|
|
|
159
|
+
Create from a template (golden image):
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
osb sandbox create --template <template-id> --timeout 30m -o json
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Template mode fixes the workload shape on the server: `--image`, `--env`,
|
|
166
|
+
`--resource`, `--entrypoint`, `--volumes-file`, and `--credential-proxy` cannot
|
|
167
|
+
be combined with `--template`. Only `--timeout` (required), `--metadata`,
|
|
168
|
+
`--extension`, and `--network-policy-file` may accompany it. Only templates in
|
|
169
|
+
the `Succeeded` phase can be used. See [Manage templates](#manage-templates).
|
|
170
|
+
|
|
171
|
+
Create from a snapshot:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
osb sandbox create --snapshot-id <snapshot-id> --timeout 30m -o json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`--image`, `--template`, and `--snapshot-id` are mutually exclusive.
|
|
178
|
+
|
|
179
|
+
Create from a request file (`-f`, like `kubectl apply -f`):
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
osb sandbox create -f sandbox-request.json -o json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`sandbox-request.json` uses the public `CreateSandboxRequest` JSON format
|
|
186
|
+
(camelCase keys):
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"image": "python:3.12",
|
|
191
|
+
"timeout": 1800,
|
|
192
|
+
"resourceLimits": { "cpu": "1", "memory": "2Gi" },
|
|
193
|
+
"entrypoint": ["python", "-m", "http.server"],
|
|
194
|
+
"networkPolicy": {
|
|
195
|
+
"defaultAction": "deny",
|
|
196
|
+
"egress": [{ "action": "allow", "target": "pypi.org" }]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- Exactly one of `image` (string or `{ "uri": ..., "auth": { "username": ..., "password": ... } }`),
|
|
202
|
+
`templateId`, or `snapshotId` is required; `timeout` is integer seconds, or
|
|
203
|
+
`null` for manual cleanup.
|
|
204
|
+
- Supported fields: `image`, `templateId`, `snapshotId`, `platform`, `timeout`,
|
|
205
|
+
`resourceLimits`, `resourceRequests`, `env`, `metadata`, `extensions`,
|
|
206
|
+
`entrypoint`, `networkPolicy`, `credentialProxy`, `secureAccess`, `volumes`,
|
|
207
|
+
`lifecycle`.
|
|
208
|
+
- `-f` cannot be combined with request-building flags; `--skip-health-check`,
|
|
209
|
+
`--ready-timeout`, and `-o` still apply. In template mode the file may only
|
|
210
|
+
set `timeout`, `metadata`, `extensions`, and `networkPolicy` alongside
|
|
211
|
+
`templateId` (a finite `timeout` is required).
|
|
212
|
+
|
|
213
|
+
### Manage templates
|
|
214
|
+
|
|
215
|
+
Templates are golden-image builds; the build runs asynchronously, so poll
|
|
216
|
+
`template get` until the status phase is `Succeeded`. Template management
|
|
217
|
+
requires a Kubernetes-backed runtime.
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
osb template create --image python:3.12 --publish s3://bucket/publish -o json
|
|
221
|
+
osb template get <template-id> -o json
|
|
222
|
+
osb template list -o json
|
|
223
|
+
osb template delete <template-id> -o json
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`template create` also accepts `--resource cpu=1 memory=512Mi disk=2Gi`,
|
|
227
|
+
`--entrypoint` (repeat per argv item), `--env KEY=VALUE`, `--metadata
|
|
228
|
+
KEY=VALUE`, `--format native|overlaybd`, `--readiness-probe`, and
|
|
229
|
+
`--warmup-seconds`. Or pass a request file in the public `CreateTemplateRequest`
|
|
230
|
+
JSON format:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
osb template create -f template-request.json -o json
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"image": "python:3.12",
|
|
239
|
+
"publish": "s3://bucket/publish",
|
|
240
|
+
"resourceLimits": { "cpu": "1", "memory": "512Mi" },
|
|
241
|
+
"readiness": { "probe": "tcp://127.0.0.1:44772", "warmupSeconds": 60 }
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Manage snapshots
|
|
246
|
+
|
|
247
|
+
Snapshots capture a sandbox's state and can back new sandboxes.
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
osb snapshot create <sandbox-id> --name golden -o json
|
|
251
|
+
osb snapshot get <snapshot-id> -o json
|
|
252
|
+
osb snapshot list --sandbox-id <sandbox-id> -o json
|
|
253
|
+
osb snapshot delete <snapshot-id> -o json
|
|
254
|
+
```
|
|
255
|
+
|
|
158
256
|
### List and inspect sandboxes
|
|
159
257
|
|
|
160
258
|
```bash
|
|
@@ -199,6 +297,10 @@ osb command status <sandbox-id> <execution-id> -o json
|
|
|
199
297
|
osb command logs <sandbox-id> <execution-id> -o json
|
|
200
298
|
```
|
|
201
299
|
|
|
300
|
+
Background start errors or a stream ending before startup confirmation make the
|
|
301
|
+
CLI exit non-zero without success metadata. After a successful start, use
|
|
302
|
+
`command status` and `command logs` to check the process's eventual result.
|
|
303
|
+
|
|
202
304
|
By default the payload after `--` is joined into one shell command string, so
|
|
203
305
|
pipelines, redirection, and `$VAR` expansion work as in a terminal. Add `--argv`
|
|
204
306
|
to pass the arguments to the executable as a literal argv list (no shell) when
|
|
@@ -329,6 +431,8 @@ Not every command supports every format. Use `--help` on the specific command wh
|
|
|
329
431
|
The main command groups are:
|
|
330
432
|
|
|
331
433
|
- `osb sandbox`: lifecycle management
|
|
434
|
+
- `osb template`: golden-image template builds
|
|
435
|
+
- `osb snapshot`: snapshot management
|
|
332
436
|
- `osb command`: command execution and persistent sessions
|
|
333
437
|
- `osb file`: file and directory operations
|
|
334
438
|
- `osb egress`: runtime egress policy
|
|
@@ -341,6 +445,8 @@ Explore them directly:
|
|
|
341
445
|
|
|
342
446
|
```bash
|
|
343
447
|
osb sandbox --help
|
|
448
|
+
osb template --help
|
|
449
|
+
osb snapshot --help
|
|
344
450
|
osb command --help
|
|
345
451
|
osb file --help
|
|
346
452
|
osb skills --help
|
|
@@ -44,10 +44,11 @@ classifiers = [
|
|
|
44
44
|
dependencies = [
|
|
45
45
|
# `command run --argv` passes a list to commands.run; the SDK accepts a
|
|
46
46
|
# list there from 0.1.17. Release the CLI only after that SDK is published.
|
|
47
|
-
"opensandbox>=1.1.
|
|
47
|
+
"opensandbox>=1.1.1,<2.0.0",
|
|
48
48
|
"click>=8.1.0,<9.0",
|
|
49
49
|
"rich>=13.0.0,<14.0",
|
|
50
50
|
"pyyaml>=6.0,<7.0",
|
|
51
|
+
"tomlkit>=0.13.0,<1.0",
|
|
51
52
|
"tomli>=2.0.0; python_version < '3.11'",
|
|
52
53
|
]
|
|
53
54
|
|
|
@@ -90,15 +90,10 @@ class ClientContext:
|
|
|
90
90
|
self._manager = SandboxManagerSync.create(self.connection_config)
|
|
91
91
|
return self._manager
|
|
92
92
|
|
|
93
|
-
def resolve_sandbox_id(self, sandbox_id: str) -> str:
|
|
94
|
-
"""Return the sandbox ID exactly as provided by the user."""
|
|
95
|
-
return sandbox_id
|
|
96
|
-
|
|
97
93
|
def connect_sandbox(
|
|
98
94
|
self, sandbox_id: str, *, skip_health_check: bool = True
|
|
99
95
|
) -> SandboxSync:
|
|
100
96
|
"""Connect to an existing sandbox by ID."""
|
|
101
|
-
sandbox_id = self.resolve_sandbox_id(sandbox_id)
|
|
102
97
|
return SandboxSync.connect(
|
|
103
98
|
sandbox_id,
|
|
104
99
|
connection_config=self.connection_config,
|
|
@@ -18,10 +18,11 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import shlex
|
|
20
20
|
import sys
|
|
21
|
+
from collections.abc import Callable
|
|
21
22
|
from datetime import timedelta
|
|
22
23
|
|
|
23
24
|
import click
|
|
24
|
-
from opensandbox.models.execd import OutputMessage, RunCommandOpts
|
|
25
|
+
from opensandbox.models.execd import Execution, OutputMessage, RunCommandOpts
|
|
25
26
|
from opensandbox.models.execd_sync import ExecutionHandlersSync
|
|
26
27
|
|
|
27
28
|
from opensandbox_cli.client import ClientContext
|
|
@@ -41,7 +42,29 @@ def command_group(ctx: click.Context) -> None:
|
|
|
41
42
|
click.echo(ctx.get_help())
|
|
42
43
|
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
def _stream_handlers() -> tuple[ExecutionHandlersSync, Callable[[], None]]:
|
|
46
|
+
"""Create stdout/stderr streaming handlers and a trailing-newline flusher."""
|
|
47
|
+
last_text = ""
|
|
48
|
+
|
|
49
|
+
def on_stdout(msg: OutputMessage) -> None:
|
|
50
|
+
nonlocal last_text
|
|
51
|
+
last_text = msg.text
|
|
52
|
+
sys.stdout.write(msg.text)
|
|
53
|
+
sys.stdout.flush()
|
|
54
|
+
|
|
55
|
+
def on_stderr(msg: OutputMessage) -> None:
|
|
56
|
+
nonlocal last_text
|
|
57
|
+
last_text = msg.text
|
|
58
|
+
sys.stderr.write(msg.text)
|
|
59
|
+
sys.stderr.flush()
|
|
60
|
+
|
|
61
|
+
def flush_newline() -> None:
|
|
62
|
+
if last_text and not last_text.endswith("\n"):
|
|
63
|
+
sys.stdout.write("\n")
|
|
64
|
+
sys.stdout.flush()
|
|
65
|
+
|
|
66
|
+
return ExecutionHandlersSync(on_stdout=on_stdout, on_stderr=on_stderr), flush_newline
|
|
67
|
+
|
|
45
68
|
|
|
46
69
|
def _run_command(
|
|
47
70
|
obj: ClientContext,
|
|
@@ -55,15 +78,10 @@ def _run_command(
|
|
|
55
78
|
) -> None:
|
|
56
79
|
"""Shared implementation for ``command run``.
|
|
57
80
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Payload contract:
|
|
63
|
-
- default: join the arguments into one shell command string (works against
|
|
64
|
-
every deployed execd)
|
|
65
|
-
- ``--argv``: send the arguments as a literal argv list, no shell; needs an
|
|
66
|
-
execd that accepts the ``argv`` request field
|
|
81
|
+
Foreground mode streams output and allows only ``-o raw``; background mode
|
|
82
|
+
returns a tracked execution object and allows structured output. The payload
|
|
83
|
+
is joined into one shell command string unless ``argv`` sends it as a
|
|
84
|
+
literal argv list (needs an argv-capable execd).
|
|
67
85
|
"""
|
|
68
86
|
allowed = ("table", "json", "yaml") if background else ("raw",)
|
|
69
87
|
fallback = "table" if background else "raw"
|
|
@@ -84,6 +102,7 @@ def _run_command(
|
|
|
84
102
|
|
|
85
103
|
if background:
|
|
86
104
|
execution = sandbox.commands.run(payload, opts=opts)
|
|
105
|
+
_handle_execution_error(obj, execution, background=True)
|
|
87
106
|
obj.output.success_panel(
|
|
88
107
|
{
|
|
89
108
|
"execution_id": execution.id,
|
|
@@ -94,42 +113,33 @@ def _run_command(
|
|
|
94
113
|
)
|
|
95
114
|
return
|
|
96
115
|
|
|
97
|
-
|
|
98
|
-
last_text = ""
|
|
99
|
-
|
|
100
|
-
def on_stdout(msg: OutputMessage) -> None:
|
|
101
|
-
nonlocal last_text
|
|
102
|
-
last_text = msg.text
|
|
103
|
-
sys.stdout.write(msg.text)
|
|
104
|
-
sys.stdout.flush()
|
|
105
|
-
|
|
106
|
-
def on_stderr(msg: OutputMessage) -> None:
|
|
107
|
-
nonlocal last_text
|
|
108
|
-
last_text = msg.text
|
|
109
|
-
sys.stderr.write(msg.text)
|
|
110
|
-
sys.stderr.flush()
|
|
111
|
-
|
|
112
|
-
handlers = ExecutionHandlersSync(on_stdout=on_stdout, on_stderr=on_stderr)
|
|
116
|
+
handlers, flush_newline = _stream_handlers()
|
|
113
117
|
execution = sandbox.commands.run(payload, opts=opts, handlers=handlers)
|
|
114
|
-
|
|
115
|
-
# Ensure terminal prompt starts on a new line
|
|
116
|
-
if last_text and not last_text.endswith("\n"):
|
|
117
|
-
sys.stdout.write("\n")
|
|
118
|
-
sys.stdout.flush()
|
|
119
|
-
|
|
118
|
+
flush_newline()
|
|
120
119
|
_handle_execution_error(obj, execution)
|
|
121
120
|
finally:
|
|
122
121
|
sandbox.close()
|
|
123
122
|
|
|
124
123
|
|
|
125
|
-
def _handle_execution_error(
|
|
126
|
-
|
|
124
|
+
def _handle_execution_error(
|
|
125
|
+
obj: ClientContext, execution: Execution, *, background: bool = False
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Exit non-zero on execution errors or missing completion/startup confirmation."""
|
|
127
128
|
if execution.error:
|
|
128
129
|
obj.output.error_panel(
|
|
129
130
|
f"{execution.error.name}: {execution.error.value}",
|
|
130
131
|
title="Execution Error",
|
|
131
132
|
)
|
|
132
133
|
sys.exit(1)
|
|
134
|
+
if execution.complete is None:
|
|
135
|
+
confirmation = (
|
|
136
|
+
"background command startup was confirmed" if background else "the command finished"
|
|
137
|
+
)
|
|
138
|
+
obj.output.error_panel(
|
|
139
|
+
f"The output stream ended before {confirmation}, so its result is unknown.",
|
|
140
|
+
title="Execution Incomplete",
|
|
141
|
+
)
|
|
142
|
+
sys.exit(1)
|
|
133
143
|
|
|
134
144
|
|
|
135
145
|
@command_group.command(
|
|
@@ -185,8 +195,6 @@ def command_run(
|
|
|
185
195
|
)
|
|
186
196
|
|
|
187
197
|
|
|
188
|
-
# ---- status ---------------------------------------------------------------
|
|
189
|
-
|
|
190
198
|
@command_group.command("status")
|
|
191
199
|
@click.argument("sandbox_id")
|
|
192
200
|
@click.argument("execution_id")
|
|
@@ -209,8 +217,6 @@ def command_status(
|
|
|
209
217
|
sandbox.close()
|
|
210
218
|
|
|
211
219
|
|
|
212
|
-
# ---- logs -----------------------------------------------------------------
|
|
213
|
-
|
|
214
220
|
@command_group.command("logs")
|
|
215
221
|
@click.argument("sandbox_id")
|
|
216
222
|
@click.argument("execution_id")
|
|
@@ -242,8 +248,6 @@ def command_logs(
|
|
|
242
248
|
sandbox.close()
|
|
243
249
|
|
|
244
250
|
|
|
245
|
-
# ---- interrupt ------------------------------------------------------------
|
|
246
|
-
|
|
247
251
|
@command_group.command("interrupt")
|
|
248
252
|
@click.argument("sandbox_id")
|
|
249
253
|
@click.argument("execution_id")
|
|
@@ -330,21 +334,7 @@ def session_run(
|
|
|
330
334
|
cmd_str = " ".join(shlex.quote(arg) for arg in command)
|
|
331
335
|
sandbox = obj.connect_sandbox(sandbox_id)
|
|
332
336
|
try:
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
def on_stdout(msg: OutputMessage) -> None:
|
|
336
|
-
nonlocal last_text
|
|
337
|
-
last_text = msg.text
|
|
338
|
-
sys.stdout.write(msg.text)
|
|
339
|
-
sys.stdout.flush()
|
|
340
|
-
|
|
341
|
-
def on_stderr(msg: OutputMessage) -> None:
|
|
342
|
-
nonlocal last_text
|
|
343
|
-
last_text = msg.text
|
|
344
|
-
sys.stderr.write(msg.text)
|
|
345
|
-
sys.stderr.flush()
|
|
346
|
-
|
|
347
|
-
handlers = ExecutionHandlersSync(on_stdout=on_stdout, on_stderr=on_stderr)
|
|
337
|
+
handlers, flush_newline = _stream_handlers()
|
|
348
338
|
execution = sandbox.commands.run_in_session(
|
|
349
339
|
session_id,
|
|
350
340
|
cmd_str,
|
|
@@ -352,11 +342,7 @@ def session_run(
|
|
|
352
342
|
timeout=timeout,
|
|
353
343
|
handlers=handlers,
|
|
354
344
|
)
|
|
355
|
-
|
|
356
|
-
if last_text and not last_text.endswith("\n"):
|
|
357
|
-
sys.stdout.write("\n")
|
|
358
|
-
sys.stdout.flush()
|
|
359
|
-
|
|
345
|
+
flush_newline()
|
|
360
346
|
_handle_execution_error(obj, execution)
|
|
361
347
|
finally:
|
|
362
348
|
sandbox.close()
|
|
@@ -20,6 +20,10 @@ from collections.abc import Mapping
|
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
22
|
import click
|
|
23
|
+
import tomlkit
|
|
24
|
+
from tomlkit.container import OutOfOrderTableProxy
|
|
25
|
+
from tomlkit.exceptions import ParseError
|
|
26
|
+
from tomlkit.items import InlineTable, Table
|
|
23
27
|
|
|
24
28
|
from opensandbox_cli.client import ClientContext
|
|
25
29
|
from opensandbox_cli.config import init_config_file, resolve_config
|
|
@@ -34,13 +38,11 @@ def config_group(ctx: click.Context) -> None:
|
|
|
34
38
|
click.echo(ctx.get_help())
|
|
35
39
|
|
|
36
40
|
|
|
37
|
-
# ---- init -----------------------------------------------------------------
|
|
38
|
-
|
|
39
41
|
@config_group.command("init")
|
|
40
42
|
@click.option("--force", is_flag=True, default=False, help="Overwrite existing config file.")
|
|
41
43
|
@output_option("table", "json", "yaml")
|
|
42
|
-
@handle_errors
|
|
43
44
|
@click.pass_obj
|
|
45
|
+
@handle_errors
|
|
44
46
|
def config_init(obj: ClientContext, force: bool, output_format: str | None) -> None:
|
|
45
47
|
"""Create a default configuration file."""
|
|
46
48
|
output = prepare_output(
|
|
@@ -54,8 +56,6 @@ def config_init(obj: ClientContext, force: bool, output_format: str | None) -> N
|
|
|
54
56
|
output.warning(str(exc))
|
|
55
57
|
|
|
56
58
|
|
|
57
|
-
# ---- show -----------------------------------------------------------------
|
|
58
|
-
|
|
59
59
|
_SENSITIVE_CONFIG_KEYS = {
|
|
60
60
|
"api_key",
|
|
61
61
|
}
|
|
@@ -80,6 +80,7 @@ def _sanitize_config_for_display(data: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
80
80
|
sanitized[key] = value
|
|
81
81
|
return sanitized
|
|
82
82
|
|
|
83
|
+
|
|
83
84
|
@config_group.command("show")
|
|
84
85
|
@output_option("table", "json", "yaml")
|
|
85
86
|
@click.pass_obj
|
|
@@ -105,14 +106,34 @@ def config_show(obj: ClientContext, output_format: str | None) -> None:
|
|
|
105
106
|
)
|
|
106
107
|
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
def _parse_toml_value(raw: str) -> Any:
|
|
110
|
+
"""Infer a TOML scalar type from a raw CLI string.
|
|
111
|
+
|
|
112
|
+
Booleans, integers, and floats are recognized so values like ``false`` or
|
|
113
|
+
``30`` keep their native TOML type; anything else is stored as a string.
|
|
114
|
+
The value is always serialized by tomlkit, so quotes, escapes, and
|
|
115
|
+
whitespace in string values are encoded correctly.
|
|
116
|
+
"""
|
|
117
|
+
normalized = raw.lower()
|
|
118
|
+
if normalized in ("true", "false"):
|
|
119
|
+
return normalized == "true"
|
|
120
|
+
try:
|
|
121
|
+
return int(raw)
|
|
122
|
+
except ValueError:
|
|
123
|
+
pass
|
|
124
|
+
try:
|
|
125
|
+
return float(raw)
|
|
126
|
+
except ValueError:
|
|
127
|
+
pass
|
|
128
|
+
return raw
|
|
129
|
+
|
|
109
130
|
|
|
110
131
|
@config_group.command("set")
|
|
111
132
|
@click.argument("key")
|
|
112
133
|
@click.argument("value")
|
|
113
134
|
@output_option("table", "json", "yaml")
|
|
114
|
-
@handle_errors
|
|
115
135
|
@click.pass_obj
|
|
136
|
+
@handle_errors
|
|
116
137
|
def config_set(
|
|
117
138
|
obj: ClientContext,
|
|
118
139
|
key: str,
|
|
@@ -125,59 +146,27 @@ def config_set(
|
|
|
125
146
|
if not path.exists():
|
|
126
147
|
raise click.ClickException(f"Config file not found: {path}. Run 'osb config init' first.")
|
|
127
148
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# TODO: Replace this regex-based TOML editing with a parser-backed update
|
|
131
|
-
# path so formatting/comments survive reliably as config complexity grows.
|
|
132
|
-
# Simple key replacement in TOML
|
|
133
|
-
# Supports dotted keys like connection.domain
|
|
134
|
-
parts = key.split(".", 1)
|
|
135
|
-
if len(parts) == 2:
|
|
136
|
-
section, field = parts
|
|
137
|
-
# Try to find and update existing value
|
|
138
|
-
import re
|
|
139
|
-
|
|
140
|
-
section_pattern = rf"(\[{re.escape(section)}\].*?)(?=\n\[|\Z)"
|
|
141
|
-
section_match = re.search(section_pattern, content, re.DOTALL)
|
|
142
|
-
|
|
143
|
-
# Infer TOML value type: bool > int > float > string
|
|
144
|
-
def _toml_value(raw: str) -> str:
|
|
145
|
-
if raw.lower() in ("true", "false"):
|
|
146
|
-
return raw.lower()
|
|
147
|
-
try:
|
|
148
|
-
int(raw)
|
|
149
|
-
return raw
|
|
150
|
-
except ValueError:
|
|
151
|
-
pass
|
|
152
|
-
try:
|
|
153
|
-
float(raw)
|
|
154
|
-
return raw
|
|
155
|
-
except ValueError:
|
|
156
|
-
pass
|
|
157
|
-
return f'"{raw}"'
|
|
158
|
-
|
|
159
|
-
toml_val = _toml_value(value)
|
|
160
|
-
|
|
161
|
-
if section_match:
|
|
162
|
-
section_text = section_match.group(1)
|
|
163
|
-
field_pattern = rf'^(#?\s*{re.escape(field)}\s*=\s*).*$'
|
|
164
|
-
field_match = re.search(field_pattern, section_text, re.MULTILINE)
|
|
165
|
-
if field_match:
|
|
166
|
-
new_line = f'{field} = {toml_val}'
|
|
167
|
-
new_section = section_text[:field_match.start()] + new_line + section_text[field_match.end():]
|
|
168
|
-
content = content[:section_match.start()] + new_section + content[section_match.end():]
|
|
169
|
-
else:
|
|
170
|
-
# Add field to section
|
|
171
|
-
insert_pos = section_match.end()
|
|
172
|
-
content = content[:insert_pos] + f'\n{field} = {toml_val}' + content[insert_pos:]
|
|
173
|
-
else:
|
|
174
|
-
# Add new section
|
|
175
|
-
content += f'\n[{section}]\n{field} = {toml_val}\n'
|
|
176
|
-
else:
|
|
149
|
+
section, _, field = key.partition(".")
|
|
150
|
+
if not section or not field or "." in field:
|
|
177
151
|
raise click.ClickException(
|
|
178
152
|
"Key must be in 'section.field' format (e.g. connection.domain)."
|
|
179
153
|
)
|
|
180
154
|
|
|
181
|
-
|
|
155
|
+
try:
|
|
156
|
+
document = tomlkit.parse(path.read_text(encoding="utf-8"))
|
|
157
|
+
except ParseError as exc:
|
|
158
|
+
raise click.ClickException(
|
|
159
|
+
f"Config file is not valid TOML ({path}): {exc}"
|
|
160
|
+
) from exc
|
|
161
|
+
|
|
162
|
+
table = document.get(section)
|
|
163
|
+
if table is None:
|
|
164
|
+
table = tomlkit.table()
|
|
165
|
+
document[section] = table
|
|
166
|
+
if not isinstance(table, (Table, InlineTable, OutOfOrderTableProxy)):
|
|
167
|
+
raise click.ClickException(f"Cannot set {key}: '{section}' is not a TOML table.")
|
|
168
|
+
|
|
169
|
+
table[field] = _parse_toml_value(value)
|
|
170
|
+
path.write_text(tomlkit.dumps(document), encoding="utf-8")
|
|
182
171
|
|
|
183
172
|
obj.output.success(f"Set {key} = {value}")
|