sandsnap 0.1.0

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +287 -0
  3. package/dist/commands/delete.d.ts +9 -0
  4. package/dist/commands/delete.d.ts.map +1 -0
  5. package/dist/commands/delete.js +50 -0
  6. package/dist/commands/delete.js.map +1 -0
  7. package/dist/commands/evolve.d.ts +17 -0
  8. package/dist/commands/evolve.d.ts.map +1 -0
  9. package/dist/commands/evolve.js +228 -0
  10. package/dist/commands/evolve.js.map +1 -0
  11. package/dist/commands/list.d.ts +10 -0
  12. package/dist/commands/list.d.ts.map +1 -0
  13. package/dist/commands/list.js +47 -0
  14. package/dist/commands/list.js.map +1 -0
  15. package/dist/commands/prune.d.ts +13 -0
  16. package/dist/commands/prune.d.ts.map +1 -0
  17. package/dist/commands/prune.js +75 -0
  18. package/dist/commands/prune.js.map +1 -0
  19. package/dist/commands/run.d.ts +15 -0
  20. package/dist/commands/run.d.ts.map +1 -0
  21. package/dist/commands/run.js +132 -0
  22. package/dist/commands/run.js.map +1 -0
  23. package/dist/commands/sandboxes.d.ts +11 -0
  24. package/dist/commands/sandboxes.d.ts.map +1 -0
  25. package/dist/commands/sandboxes.js +79 -0
  26. package/dist/commands/sandboxes.js.map +1 -0
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +83 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/lib/client.d.ts +14 -0
  32. package/dist/lib/client.d.ts.map +1 -0
  33. package/dist/lib/client.js +30 -0
  34. package/dist/lib/client.js.map +1 -0
  35. package/dist/lib/copy.d.ts +14 -0
  36. package/dist/lib/copy.d.ts.map +1 -0
  37. package/dist/lib/copy.js +91 -0
  38. package/dist/lib/copy.js.map +1 -0
  39. package/dist/lib/output.d.ts +28 -0
  40. package/dist/lib/output.d.ts.map +1 -0
  41. package/dist/lib/output.js +69 -0
  42. package/dist/lib/output.js.map +1 -0
  43. package/dist/lib/parse.d.ts +23 -0
  44. package/dist/lib/parse.d.ts.map +1 -0
  45. package/dist/lib/parse.js +58 -0
  46. package/dist/lib/parse.js.map +1 -0
  47. package/dist/lib/parse.test.d.ts +2 -0
  48. package/dist/lib/parse.test.d.ts.map +1 -0
  49. package/dist/lib/parse.test.js +139 -0
  50. package/dist/lib/parse.test.js.map +1 -0
  51. package/dist/lib/stdin.d.ts +24 -0
  52. package/dist/lib/stdin.d.ts.map +1 -0
  53. package/dist/lib/stdin.js +79 -0
  54. package/dist/lib/stdin.js.map +1 -0
  55. package/package.json +55 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Libo Shen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,287 @@
1
+ # sandsnap
2
+
3
+ A CLI for managing Deno Sandbox environments with simplified snapshot-based workflows. Create, evolve, and run isolated Linux environments without dealing with volumes directly.
4
+
5
+ ## Concept
6
+
7
+ ```
8
+ base-image ──[commands]──► snapshot-A ──[commands]──► snapshot-B
9
+
10
+ ──[different commands]──► snapshot-C
11
+ ```
12
+
13
+ Snapshots are immutable environment states. You **evolve** from one snapshot to create another, or **run** ephemeral commands against a snapshot without modifying it.
14
+
15
+ ## Features
16
+
17
+ - **Snapshot-based workflow** - Think in terms of environment states, not volumes
18
+ - **Evolve environments** - `snapshot A` + `commands` → `snapshot B`
19
+ - **Ephemeral execution** - Run commands against a snapshot without modifying it
20
+ - **Heredoc support** - Perfect for AI agents and scripting
21
+ - **Auto-cleanup** - Smart deletion of snapshots and their backing storage
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ # Using npm
27
+ npm install -g sandsnap
28
+
29
+ # Or run directly with npx
30
+ npx sandsnap <command>
31
+ ```
32
+
33
+ Requires Node.js 24+ and a [Deno Deploy](https://console.deno.com/) account.
34
+
35
+ ## Setup
36
+
37
+ 1. Create an account at [console.deno.com](https://console.deno.com/)
38
+ 2. Go to **Settings → Organization tokens** and create a token
39
+ 3. Set the token:
40
+ ```bash
41
+ export DENO_DEPLOY_TOKEN=<your-token>
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ ```bash
47
+ # Create a snapshot with Python installed
48
+ sandsnap evolve python-env <<'EOF'
49
+ sudo apt-get update
50
+ sudo apt-get install -y python3 python3-pip
51
+ python3 --version
52
+ EOF
53
+
54
+ # Run commands against it (ephemeral - changes discarded)
55
+ sandsnap run python-env <<'EOF'
56
+ python3 -c "print('Hello from sandbox!')"
57
+ EOF
58
+
59
+ # Evolve further from existing snapshot
60
+ sandsnap evolve python-ml --from python-env <<'EOF'
61
+ pip3 install numpy pandas scikit-learn
62
+ python3 -c "import numpy; print(numpy.__version__)"
63
+ EOF
64
+
65
+ # Interactive shell
66
+ sandsnap run python-ml
67
+
68
+ # List all snapshots
69
+ sandsnap list
70
+
71
+ # Delete a snapshot (auto-cleans backing storage)
72
+ sandsnap delete python-env --force
73
+ ```
74
+
75
+ ## Commands
76
+
77
+ ### `sandsnap evolve <name>`
78
+
79
+ Create a new snapshot by running commands on a base state.
80
+
81
+ ```bash
82
+ # From base Debian image (default)
83
+ sandsnap evolve my-env
84
+
85
+ # From existing snapshot
86
+ sandsnap evolve my-env-v2 --from my-env
87
+
88
+ # With script file
89
+ sandsnap evolve my-env --script setup.sh
90
+
91
+ # With heredoc
92
+ sandsnap evolve my-env <<'EOF'
93
+ apt-get update
94
+ apt-get install -y curl git
95
+ EOF
96
+
97
+ # Overwrite existing snapshot
98
+ sandsnap evolve my-env --overwrite <<'EOF'
99
+ # new setup
100
+ EOF
101
+ ```
102
+
103
+ **Options:**
104
+ | Option | Description | Default |
105
+ |--------|-------------|---------|
106
+ | `--from <snapshot>` | Base snapshot to evolve from | debian-13 |
107
+ | `--timeout <duration>` | Sandbox timeout (e.g., `10m`, `30m`) | `10m` |
108
+ | `--capacity <size>` | Volume capacity (e.g., `2GiB`, `10GiB`) | `10GiB` |
109
+ | `--memory <size>` | Sandbox memory (e.g., `1GiB`, `2GiB`, `4GiB`) | `1280MiB` |
110
+ | `--region <region>` | Region (`ord` or `ams`) | `ord` |
111
+ | `--env <KEY=VALUE>` | Set environment variable (repeatable) | - |
112
+ | `--copy <src:dst>` | Copy file/dir from host to sandbox (repeatable) | - |
113
+ | `--script <file>` | Read commands from file | - |
114
+ | `--overwrite` | Replace existing snapshot | `false` |
115
+
116
+ ### `sandsnap run <snapshot>`
117
+
118
+ Run commands in an ephemeral sandbox. Changes are discarded after exit.
119
+
120
+ ```bash
121
+ # Interactive shell
122
+ sandsnap run my-env
123
+
124
+ # Run commands
125
+ sandsnap run my-env <<'EOF'
126
+ echo "Hello"
127
+ python3 script.py
128
+ EOF
129
+
130
+ # With script file
131
+ sandsnap run my-env --script test.sh
132
+ ```
133
+
134
+ **Options:**
135
+ | Option | Description | Default |
136
+ |--------|-------------|---------|
137
+ | `--timeout <duration>` | Sandbox timeout | `session` |
138
+ | `--memory <size>` | Sandbox memory (e.g., `1GiB`, `2GiB`, `4GiB`) | `1280MiB` |
139
+ | `--region <region>` | Region (`ord` or `ams`) | `ord` |
140
+ | `--env <KEY=VALUE>` | Set environment variable (repeatable) | - |
141
+ | `--copy <src:dst>` | Copy file/dir from host to sandbox (repeatable) | - |
142
+ | `--copy-out <src:dst>` | Copy file/dir from sandbox to host (repeatable) | - |
143
+ | `--script <file>` | Read commands from file | - |
144
+
145
+ ### `sandsnap list`
146
+
147
+ List all snapshots.
148
+
149
+ ```bash
150
+ sandsnap list
151
+ sandsnap list --json
152
+ sandsnap list --region ord
153
+ ```
154
+
155
+ ### `sandsnap delete <name>`
156
+
157
+ Delete a snapshot and clean up its backing storage.
158
+
159
+ ```bash
160
+ sandsnap delete my-env
161
+ sandsnap delete my-env --force # Skip confirmation
162
+ ```
163
+
164
+ ### `sandsnap prune`
165
+
166
+ Clean up orphaned temporary volumes.
167
+
168
+ ```bash
169
+ sandsnap prune --dry-run # See what would be deleted
170
+ sandsnap prune --force # Delete without confirmation
171
+ ```
172
+
173
+ ### `sandsnap sandboxes`
174
+
175
+ Manage running sandboxes (for debugging).
176
+
177
+ ```bash
178
+ sandsnap sandboxes list # List all sandboxes
179
+ sandsnap sandboxes kill <id> # Kill a specific sandbox
180
+ sandsnap sandboxes kill-all # Kill all running sandboxes
181
+ ```
182
+
183
+ ## Global Options
184
+
185
+ | Option | Description |
186
+ |--------|-------------|
187
+ | `-v, --verbose` | Show detailed progress logs |
188
+ | `-h, --help` | Show help |
189
+ | `--version` | Show version |
190
+
191
+ ## Examples
192
+
193
+ ### Setting up a development environment
194
+
195
+ ```bash
196
+ # Create base environment
197
+ sandsnap evolve dev-base <<'EOF'
198
+ sudo apt-get update
199
+ sudo apt-get install -y git curl wget build-essential
200
+ EOF
201
+
202
+ # Add Node.js
203
+ sandsnap evolve dev-node --from dev-base <<'EOF'
204
+ curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
205
+ sudo apt-get install -y nodejs
206
+ node --version
207
+ EOF
208
+
209
+ # Add Python
210
+ sandsnap evolve dev-python --from dev-base <<'EOF'
211
+ sudo apt-get install -y python3 python3-pip python3-venv
212
+ python3 --version
213
+ EOF
214
+ ```
215
+
216
+ ### Running tests in isolation
217
+
218
+ ```bash
219
+ # Run tests against a clean environment every time
220
+ sandsnap run dev-node <<'EOF'
221
+ cd /tmp
222
+ git clone https://github.com/user/repo.git
223
+ cd repo
224
+ npm install
225
+ npm test
226
+ EOF
227
+ ```
228
+
229
+ ### Environment variables
230
+
231
+ ```bash
232
+ # Pass environment variables to the sandbox
233
+ sandsnap run python-env \
234
+ --env API_KEY=secret123 \
235
+ --env DEBUG=true \
236
+ <<'EOF'
237
+ echo "API_KEY=$API_KEY"
238
+ python3 -c "import os; print(os.environ.get('DEBUG'))"
239
+ EOF
240
+ ```
241
+
242
+ ### File copy workflow
243
+
244
+ ```bash
245
+ # Copy files into sandbox, process, copy results out
246
+ sandsnap run python-env \
247
+ --copy ./data:/home/app/data \
248
+ --copy ./config.json:/home/app/config.json \
249
+ --copy-out /home/app/results:/tmp/results \
250
+ <<'EOF'
251
+ python3 process.py --config /home/app/config.json
252
+ EOF
253
+
254
+ # Check results
255
+ ls /tmp/results/
256
+ ```
257
+
258
+ ### AI agent workflow
259
+
260
+ ```bash
261
+ # Agent can use heredocs to run arbitrary commands
262
+ RESULT=$(sandsnap run python-env <<'EOF'
263
+ python3 -c "
264
+ import json
265
+ result = {'status': 'ok', 'value': 42}
266
+ print(json.dumps(result))
267
+ "
268
+ EOF
269
+ )
270
+ echo "$RESULT"
271
+ ```
272
+
273
+ ## Notes
274
+
275
+ - Commands run as `app` user; use `sudo` for system operations
276
+ - Snapshots are region-specific (`ord` = Chicago, `ams` = Amsterdam)
277
+ - Default timeout is `session` for `run` (dies when CLI exits) and `10m` for `evolve`
278
+
279
+ ## Environment Variables
280
+
281
+ | Variable | Description |
282
+ |----------|-------------|
283
+ | `DENO_DEPLOY_TOKEN` | Required. Your Deno Deploy organization token |
284
+
285
+ ## License
286
+
287
+ MIT
@@ -0,0 +1,9 @@
1
+ /**
2
+ * sandboxer delete - Delete a snapshot (and its parent volume if possible)
3
+ */
4
+ interface DeleteOptions {
5
+ force?: boolean;
6
+ }
7
+ export declare function deleteSnapshot(name: string, options: DeleteOptions): Promise<void>;
8
+ export {};
9
+ //# sourceMappingURL=delete.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../src/commands/delete.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,UAAU,aAAa;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA6CxF"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * sandboxer delete - Delete a snapshot (and its parent volume if possible)
3
+ */
4
+ import { getClient } from "../lib/client.js";
5
+ import { confirm } from "../lib/stdin.js";
6
+ import { success, error, info } from "../lib/output.js";
7
+ export async function deleteSnapshot(name, options) {
8
+ const client = getClient();
9
+ try {
10
+ // Get snapshot to find its parent volume
11
+ const snapshot = await client.snapshots.get(name);
12
+ if (!snapshot) {
13
+ error(`Snapshot '${name}' not found.`);
14
+ process.exit(1);
15
+ }
16
+ // Confirm deletion unless --force
17
+ if (!options.force) {
18
+ const confirmed = await confirm(`Delete snapshot '${name}'?`);
19
+ if (!confirmed) {
20
+ console.log("Aborted.");
21
+ return;
22
+ }
23
+ }
24
+ // Get parent volume info before deleting snapshot
25
+ const parentVolumeId = snapshot.volume?.id;
26
+ const parentVolumeSlug = snapshot.volume?.slug;
27
+ // Delete the snapshot
28
+ await client.snapshots.delete(name);
29
+ success(`Deleted snapshot '${name}'`);
30
+ // Try to delete parent volume (smart cleanup)
31
+ if (parentVolumeId && parentVolumeSlug) {
32
+ // Only delete if it's a temp volume we created (starts with 'tmp-')
33
+ if (parentVolumeSlug.startsWith('tmp-')) {
34
+ try {
35
+ await client.volumes.delete(parentVolumeId);
36
+ info(`Cleaned up parent volume '${parentVolumeSlug}'`);
37
+ }
38
+ catch {
39
+ // Volume may have other snapshots depending on it, that's ok
40
+ info(`Parent volume '${parentVolumeSlug}' kept (may have other dependents)`);
41
+ }
42
+ }
43
+ }
44
+ }
45
+ catch (err) {
46
+ error(`Failed to delete snapshot: ${err}`);
47
+ process.exit(1);
48
+ }
49
+ }
50
+ //# sourceMappingURL=delete.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete.js","sourceRoot":"","sources":["../../src/commands/delete.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAMxD,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,OAAsB;IACvE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAE3B,IAAI,CAAC;QACH,yCAAyC;QACzC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,KAAK,CAAC,aAAa,IAAI,cAAc,CAAC,CAAC;YACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,kCAAkC;QAClC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,oBAAoB,IAAI,IAAI,CAAC,CAAC;YAC9D,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;gBACxB,OAAO;YACT,CAAC;QACH,CAAC;QAED,kDAAkD;QAClD,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;QAC3C,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;QAE/C,sBAAsB;QACtB,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpC,OAAO,CAAC,qBAAqB,IAAI,GAAG,CAAC,CAAC;QAEtC,8CAA8C;QAC9C,IAAI,cAAc,IAAI,gBAAgB,EAAE,CAAC;YACvC,oEAAoE;YACpE,IAAI,gBAAgB,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBACxC,IAAI,CAAC;oBACH,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;oBAC5C,IAAI,CAAC,6BAA6B,gBAAgB,GAAG,CAAC,CAAC;gBACzD,CAAC;gBAAC,MAAM,CAAC;oBACP,6DAA6D;oBAC7D,IAAI,CAAC,kBAAkB,gBAAgB,oCAAoC,CAAC,CAAC;gBAC/E,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,8BAA8B,GAAG,EAAE,CAAC,CAAC;QAC3C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * sandboxer evolve - Create a new snapshot by running commands on a base state
3
+ */
4
+ interface EvolveOptions {
5
+ from?: string;
6
+ timeout: string;
7
+ capacity: string;
8
+ memory: string;
9
+ region: string;
10
+ env: string[];
11
+ copy: string[];
12
+ script?: string;
13
+ overwrite?: boolean;
14
+ }
15
+ export declare function evolve(name: string, options: EvolveOptions): Promise<void>;
16
+ export {};
17
+ //# sourceMappingURL=evolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evolve.d.ts","sourceRoot":"","sources":["../../src/commands/evolve.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA6IhF"}
@@ -0,0 +1,228 @@
1
+ /**
2
+ * sandboxer evolve - Create a new snapshot by running commands on a base state
3
+ */
4
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
5
+ if (value !== null && value !== void 0) {
6
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
7
+ var dispose, inner;
8
+ if (async) {
9
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
10
+ dispose = value[Symbol.asyncDispose];
11
+ }
12
+ if (dispose === void 0) {
13
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
14
+ dispose = value[Symbol.dispose];
15
+ if (async) inner = dispose;
16
+ }
17
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
18
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
19
+ env.stack.push({ value: value, dispose: dispose, async: async });
20
+ }
21
+ else if (async) {
22
+ env.stack.push({ async: true });
23
+ }
24
+ return value;
25
+ };
26
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
27
+ return function (env) {
28
+ function fail(e) {
29
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
30
+ env.hasError = true;
31
+ }
32
+ var r, s = 0;
33
+ function next() {
34
+ while (r = env.stack.pop()) {
35
+ try {
36
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
37
+ if (r.dispose) {
38
+ var result = r.dispose.call(r.value);
39
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
40
+ }
41
+ else s |= 1;
42
+ }
43
+ catch (e) {
44
+ fail(e);
45
+ }
46
+ }
47
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
48
+ if (env.hasError) throw env.error;
49
+ }
50
+ return next();
51
+ };
52
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
53
+ var e = new Error(message);
54
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
55
+ });
56
+ import { spawn } from "node:child_process";
57
+ import { Sandbox } from "@deno/sandbox";
58
+ import { getClient, snapshotExists, tempVolumeSlug } from "../lib/client.js";
59
+ import { isInteractive, readStdin, readScriptFile, promptOnFailure } from "../lib/stdin.js";
60
+ import { info, success, error, step } from "../lib/output.js";
61
+ import { copyToSandbox } from "../lib/copy.js";
62
+ import { parseCapacity, parseMemory, parseTimeout, parseRegion, parseEnvVars } from "../lib/parse.js";
63
+ export async function evolve(name, options) {
64
+ const client = getClient();
65
+ const volumeSlug = tempVolumeSlug();
66
+ // Parse and validate options
67
+ const capacity = parseCapacity(options.capacity);
68
+ const timeout = parseTimeout(options.timeout);
69
+ const region = parseRegion(options.region);
70
+ const memory = parseMemory(options.memory);
71
+ const env = parseEnvVars(options.env);
72
+ // Determine the source for the volume
73
+ const fromSource = options.from || "builtin:debian-13";
74
+ try {
75
+ // Check if target snapshot already exists
76
+ if (!options.overwrite && await snapshotExists(name)) {
77
+ error(`Snapshot '${name}' already exists. Use --overwrite to replace it.`);
78
+ process.exit(1);
79
+ }
80
+ // If overwriting, delete existing snapshot first
81
+ if (options.overwrite && await snapshotExists(name)) {
82
+ info(`Deleting existing snapshot '${name}'...`);
83
+ await client.snapshots.delete(name);
84
+ }
85
+ // Determine script source
86
+ let script = null;
87
+ const interactive = isInteractive();
88
+ if (options.script) {
89
+ script = await readScriptFile(options.script);
90
+ }
91
+ else if (!interactive) {
92
+ // Read from stdin (heredoc or pipe)
93
+ script = await readStdin();
94
+ }
95
+ // Step 1: Create bootable volume
96
+ step(1, 4, `Creating volume from ${options.from ? `snapshot '${options.from}'` : "base image"}...`);
97
+ const volume = await client.volumes.create({
98
+ slug: volumeSlug,
99
+ region: region,
100
+ capacity: capacity,
101
+ from: fromSource,
102
+ });
103
+ info(`Volume created: ${volume.id}`);
104
+ let shouldSnapshot = false;
105
+ // Step 2 & 3: Boot sandbox and execute commands
106
+ // Use a block so sandbox is closed before we snapshot
107
+ {
108
+ const env_1 = { stack: [], error: void 0, hasError: false };
109
+ try {
110
+ step(2, 4, "Booting sandbox...");
111
+ const sandbox = __addDisposableResource(env_1, await Sandbox.create({
112
+ region: region,
113
+ root: volumeSlug,
114
+ timeout: timeout,
115
+ memory: memory,
116
+ env: Object.keys(env).length > 0 ? env : undefined,
117
+ }), true);
118
+ info(`Sandbox ready: ${sandbox.id}`);
119
+ // Copy files into sandbox
120
+ if (options.copy.length > 0) {
121
+ await copyToSandbox(sandbox, options.copy);
122
+ }
123
+ // Step 3: Execute commands
124
+ step(3, 4, script ? "Executing script..." : "Opening interactive shell...");
125
+ shouldSnapshot = true;
126
+ if (script) {
127
+ // Write script to temp file and execute
128
+ await sandbox.fs.writeTextFile("/tmp/sandboxer-script.sh", script);
129
+ try {
130
+ await sandbox.sh `bash /tmp/sandboxer-script.sh`;
131
+ }
132
+ catch (err) {
133
+ shouldSnapshot = false;
134
+ error(`Script failed: ${err}`);
135
+ // Prompt user for action
136
+ const action = await promptOnFailure();
137
+ if (action === "shell") {
138
+ info("Opening shell for debugging...");
139
+ await openInteractiveShell(sandbox);
140
+ // After shell, ask again
141
+ const action2 = await promptOnFailure();
142
+ shouldSnapshot = action2 === "save";
143
+ }
144
+ else {
145
+ shouldSnapshot = action === "save";
146
+ }
147
+ }
148
+ }
149
+ else {
150
+ // Interactive mode
151
+ await openInteractiveShell(sandbox);
152
+ shouldSnapshot = true; // User explicitly exited
153
+ }
154
+ // Explicitly kill sandbox before snapshotting
155
+ info("Stopping sandbox...");
156
+ await sandbox.kill();
157
+ // Wait a moment for volume to unmount
158
+ await new Promise(resolve => setTimeout(resolve, 2000));
159
+ }
160
+ catch (e_1) {
161
+ env_1.error = e_1;
162
+ env_1.hasError = true;
163
+ }
164
+ finally {
165
+ const result_1 = __disposeResources(env_1);
166
+ if (result_1)
167
+ await result_1;
168
+ }
169
+ }
170
+ // Sandbox is now stopped
171
+ if (!shouldSnapshot) {
172
+ info("Discarding changes...");
173
+ // Clean up volume
174
+ try {
175
+ await client.volumes.delete(volumeSlug);
176
+ }
177
+ catch {
178
+ // Ignore
179
+ }
180
+ return;
181
+ }
182
+ // Step 4: Create snapshot (sandbox must be closed first)
183
+ step(4, 4, `Creating snapshot '${name}'...`);
184
+ await client.volumes.snapshot(volume.id, {
185
+ slug: name,
186
+ });
187
+ success(`Snapshot '${name}' created!`);
188
+ // Note: We don't delete the volume here because the snapshot
189
+ // depends on it (copy-on-write). The volume is now "owned" by
190
+ // the snapshot and will be managed by the Deno Sandbox service.
191
+ }
192
+ catch (err) {
193
+ error(`Failed: ${err}`);
194
+ // Attempt to clean up volume on error
195
+ try {
196
+ await client.volumes.delete(volumeSlug);
197
+ }
198
+ catch {
199
+ // Ignore cleanup errors
200
+ }
201
+ process.exit(1);
202
+ }
203
+ }
204
+ /**
205
+ * Open an interactive SSH shell to the sandbox
206
+ */
207
+ async function openInteractiveShell(sandbox) {
208
+ const ssh = await sandbox.exposeSsh();
209
+ info(`Connecting via SSH to ${ssh.username}@${ssh.hostname}...`);
210
+ console.log("(Type 'exit' when done)\n");
211
+ return new Promise((resolve, reject) => {
212
+ const sshProcess = spawn("ssh", [
213
+ "-o", "StrictHostKeyChecking=no",
214
+ "-o", "UserKnownHostsFile=/dev/null",
215
+ "-o", "LogLevel=ERROR",
216
+ "-t",
217
+ `${ssh.username}@${ssh.hostname}`,
218
+ ], {
219
+ stdio: "inherit",
220
+ });
221
+ sshProcess.on("close", () => {
222
+ console.log("");
223
+ resolve();
224
+ });
225
+ sshProcess.on("error", reject);
226
+ });
227
+ }
228
+ //# sourceMappingURL=evolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evolve.js","sourceRoot":"","sources":["../../src/commands/evolve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * sandboxer list - List all snapshots
3
+ */
4
+ interface ListOptions {
5
+ region?: string;
6
+ json?: boolean;
7
+ }
8
+ export declare function list(options: ListOptions): Promise<void>;
9
+ export {};
10
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/commands/list.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA6C9D"}