freestyle-sandboxes 0.1.17 → 0.1.18

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 (7) hide show
  1. package/README.md +96 -0
  2. package/cli.mjs +512 -0
  3. package/index.cjs +969 -941
  4. package/index.d.cts +759 -727
  5. package/index.d.mts +759 -727
  6. package/index.mjs +971 -943
  7. package/package.json +6 -2
package/README.md CHANGED
@@ -2,6 +2,102 @@
2
2
 
3
3
  Learn more at [docs.freestyle.sh](https://docs.freestyle.sh)
4
4
 
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install freestyle-sandboxes
9
+ ```
10
+
11
+ ## CLI Usage
12
+
13
+ The Freestyle SDK includes a command-line interface for managing your Freestyle resources.
14
+
15
+ ### Setup
16
+
17
+ Set the environment variable with your API key:
18
+
19
+ ```bash
20
+ export FREESTYLE_API_KEY="your-api-key"
21
+ ```
22
+
23
+ Or create a `.env` file in your project directory:
24
+
25
+ ```
26
+ FREESTYLE_API_KEY=your-api-key
27
+ ```
28
+
29
+ ### Commands
30
+
31
+ #### Virtual Machines
32
+
33
+ ```bash
34
+ # Create a new VM
35
+ freestyle vm create --name my-vm
36
+
37
+ # Create a VM from a snapshot (for debugging)
38
+ freestyle vm create --snapshot <snapshot-id>
39
+
40
+ # Create a VM with domain
41
+ freestyle vm create --domain myapp.example.com --port 3000
42
+
43
+ # Create VM and SSH into it (auto-deletes on exit)
44
+ freestyle vm create --ssh
45
+
46
+ # Create VM from snapshot and SSH into it
47
+ freestyle vm create --snapshot <snapshot-id> --ssh
48
+
49
+ # List all VMs
50
+ freestyle vm list
51
+
52
+ # SSH into a VM
53
+ freestyle vm ssh <vmId>
54
+
55
+ # SSH into a VM and delete it on exit
56
+ freestyle vm ssh <vmId> --delete-on-exit
57
+
58
+ # Execute a command on a VM
59
+ freestyle vm exec <vmId> "ls -la"
60
+
61
+ # Delete a VM
62
+ freestyle vm delete <vmId>
63
+ ```
64
+
65
+ #### Serverless Deployments
66
+
67
+ ```bash
68
+ # Deploy from inline code
69
+ freestyle deploy --code "export default () => 'Hello World'"
70
+
71
+ # Deploy from a file
72
+ freestyle deploy --file ./my-function.js
73
+
74
+ # Deploy from a Git repository
75
+ freestyle deploy --repo <repoId>
76
+
77
+ # Add environment variables
78
+ freestyle deploy --code "..." --env API_KEY=secret --env DEBUG=true
79
+ ```
80
+
81
+ #### Serverless Runs
82
+
83
+ ```bash
84
+ # Execute a one-off function from inline code
85
+ freestyle run --code "console.log('Hello!')"
86
+
87
+ # Execute from a file
88
+ freestyle run --file ./script.js
89
+ ```
90
+
91
+ #### Utilities
92
+
93
+ ```bash
94
+ # Get help for any command
95
+ freestyle --help
96
+ freestyle vm --help
97
+ ```
98
+
99
+ ## SDK Usage
100
+
5
101
  ```ts
6
102
  import { freestyle } from "freestyle-sandboxes";
7
103
 
package/cli.mjs ADDED
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import yargs from 'yargs';
4
+ import { hideBin } from 'yargs/helpers';
5
+ import { Freestyle, VmSpec } from './index.mjs';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as dotenv from 'dotenv';
9
+ import { spawn } from 'child_process';
10
+
11
+ function getFreestyleClient() {
12
+ const apiKey = process.env.FREESTYLE_API_KEY;
13
+ const baseUrl = process.env.FREESTYLE_API_URL;
14
+ if (!apiKey) {
15
+ console.error("Error: FREESTYLE_API_KEY environment variable is not set.");
16
+ console.error('Run "freestyle init" to set it up, or set it manually.');
17
+ process.exit(1);
18
+ }
19
+ return new Freestyle({ apiKey, baseUrl });
20
+ }
21
+ function handleError(error) {
22
+ if (error.response) {
23
+ console.error("API Error:", error.response.data);
24
+ } else if (error.message) {
25
+ console.error("Error:", error.message);
26
+ } else {
27
+ console.error("Error:", error);
28
+ }
29
+ process.exit(1);
30
+ }
31
+ function loadEnv() {
32
+ const envPath = path.join(process.cwd(), ".env");
33
+ if (fs.existsSync(envPath)) {
34
+ dotenv.config({ path: envPath });
35
+ }
36
+ }
37
+ function formatTable(headers, rows) {
38
+ const colWidths = headers.map((h, i) => {
39
+ const maxRowWidth = Math.max(...rows.map((r) => (r[i] || "").length));
40
+ return Math.max(h.length, maxRowWidth);
41
+ });
42
+ const headerRow = headers.map((h, i) => h.padEnd(colWidths[i])).join(" ");
43
+ const separator = colWidths.map((w) => "-".repeat(w)).join(" ");
44
+ console.log(headerRow);
45
+ console.log(separator);
46
+ rows.forEach((row) => {
47
+ console.log(row.map((cell, i) => (cell || "").padEnd(colWidths[i])).join(" "));
48
+ });
49
+ }
50
+
51
+ async function sshIntoVm(vmId, options = {}) {
52
+ const freestyle = getFreestyleClient();
53
+ console.log("Setting up SSH connection...");
54
+ const { identity, identityId } = await freestyle.identities.create();
55
+ console.log(`Created identity: ${identityId}`);
56
+ await identity.permissions.vms.grant({ vmId });
57
+ const { token, tokenId } = await identity.tokens.create();
58
+ const sshCommand = `ssh ${vmId}:${token}@vm-ssh.freestyle.sh -p 22`;
59
+ console.log(`Connecting to VM ${vmId}...`);
60
+ console.log(`Command: ${sshCommand}
61
+ `);
62
+ return new Promise((resolve, reject) => {
63
+ const sshProcess = spawn(sshCommand, {
64
+ shell: true,
65
+ stdio: "inherit"
66
+ });
67
+ sshProcess.on("close", async (code) => {
68
+ console.log("\nSSH session ended.");
69
+ try {
70
+ console.log("Cleaning up identity and token...");
71
+ await identity.tokens.revoke({ tokenId });
72
+ await freestyle.identities.delete({ identityId });
73
+ console.log("\u2713 Cleanup complete");
74
+ if (options.deleteOnExit) {
75
+ console.log(`Deleting VM ${vmId}...`);
76
+ await freestyle.vms.delete({ vmId });
77
+ console.log("\u2713 VM deleted");
78
+ }
79
+ resolve();
80
+ } catch (error) {
81
+ console.error("Error during cleanup:", error);
82
+ reject(error);
83
+ }
84
+ });
85
+ sshProcess.on("error", (error) => {
86
+ console.error("Error starting SSH:", error);
87
+ reject(error);
88
+ });
89
+ });
90
+ }
91
+ const vmCommand = {
92
+ command: "vm <action>",
93
+ describe: "Manage Virtual Machines",
94
+ builder: (yargs) => {
95
+ return yargs.command(
96
+ "create",
97
+ "Create a new VM",
98
+ (yargs2) => {
99
+ return yargs2.option("name", {
100
+ alias: "n",
101
+ type: "string",
102
+ description: "VM name/discriminator"
103
+ }).option("domain", {
104
+ alias: "d",
105
+ type: "string",
106
+ description: "Custom domain to attach"
107
+ }).option("port", {
108
+ alias: "p",
109
+ type: "number",
110
+ description: "VM port to expose (default: 3000)",
111
+ default: 3e3
112
+ }).option("apt", {
113
+ type: "array",
114
+ description: "APT packages to install",
115
+ default: []
116
+ }).option("snapshot", {
117
+ alias: "s",
118
+ type: "string",
119
+ description: "Snapshot ID to create VM from"
120
+ }).option("exec", {
121
+ alias: "e",
122
+ type: "string",
123
+ description: "Execute a command on the VM after creation"
124
+ }).option("ssh", {
125
+ type: "boolean",
126
+ description: "SSH into VM after creation and delete VM on exit (for debugging)",
127
+ default: false
128
+ }).option("delete", {
129
+ type: "boolean",
130
+ description: "Delete VM after exec completes or when SSH session ends",
131
+ default: false
132
+ }).option("json", {
133
+ type: "boolean",
134
+ description: "Output as JSON",
135
+ default: false
136
+ });
137
+ },
138
+ async (argv) => {
139
+ loadEnv();
140
+ const args = argv;
141
+ try {
142
+ const freestyle = getFreestyleClient();
143
+ let createOptions = {};
144
+ if (args.snapshot) {
145
+ createOptions.snapshotId = args.snapshot;
146
+ } else {
147
+ const spec = new VmSpec({
148
+ discriminator: args.name,
149
+ aptDeps: args.apt
150
+ });
151
+ createOptions.snapshot = spec;
152
+ }
153
+ if (args.domain) {
154
+ createOptions.domains = [
155
+ {
156
+ domain: args.domain,
157
+ vmPort: args.port
158
+ }
159
+ ];
160
+ }
161
+ console.log("Creating VM...");
162
+ const result = await freestyle.vms.create(createOptions);
163
+ let execResult;
164
+ if (args.exec) {
165
+ const vm = freestyle.vms.ref({ vmId: result.vmId });
166
+ console.log(`Executing command on VM ${result.vmId}...`);
167
+ execResult = await vm.exec({
168
+ command: args.exec
169
+ });
170
+ }
171
+ if (args.json && !args.ssh) {
172
+ if (execResult) {
173
+ console.log(
174
+ JSON.stringify(
175
+ {
176
+ vm: result,
177
+ exec: execResult
178
+ },
179
+ null,
180
+ 2
181
+ )
182
+ );
183
+ } else {
184
+ console.log(JSON.stringify(result, null, 2));
185
+ }
186
+ } else {
187
+ console.log("\n\u2713 VM created successfully!");
188
+ console.log(` VM ID: ${result.vmId}`);
189
+ const domainStr = result.domains?.[0];
190
+ if (domainStr) {
191
+ console.log(` Domain: https://${domainStr}`);
192
+ }
193
+ if (execResult) {
194
+ if (execResult.stdout) {
195
+ console.log("\nExec output:");
196
+ console.log(execResult.stdout);
197
+ }
198
+ if (execResult.stderr) {
199
+ console.error("\nExec errors:");
200
+ console.error(execResult.stderr);
201
+ }
202
+ console.log(`
203
+ Exec exit code: ${execResult.statusCode || 0}`);
204
+ }
205
+ }
206
+ if (args.ssh) {
207
+ console.log("");
208
+ await sshIntoVm(result.vmId, { deleteOnExit: args.delete });
209
+ } else if (args.delete) {
210
+ console.log(`Deleting VM ${result.vmId}...`);
211
+ await freestyle.vms.delete({ vmId: result.vmId });
212
+ console.log("\u2713 VM deleted");
213
+ }
214
+ } catch (error) {
215
+ handleError(error);
216
+ }
217
+ }
218
+ ).command(
219
+ "list",
220
+ "List all VMs",
221
+ (yargs2) => {
222
+ return yargs2.option("json", {
223
+ type: "boolean",
224
+ description: "Output as JSON",
225
+ default: false
226
+ });
227
+ },
228
+ async (argv) => {
229
+ loadEnv();
230
+ const args = argv;
231
+ try {
232
+ const freestyle = getFreestyleClient();
233
+ const vms = await freestyle.vms.list();
234
+ if (args.json) {
235
+ console.log(JSON.stringify(vms, null, 2));
236
+ } else {
237
+ if (vms.vms.length === 0) {
238
+ console.log("No VMs found.");
239
+ return;
240
+ }
241
+ const rows = vms.vms.map((vm) => [
242
+ vm.id,
243
+ vm.state || "unknown",
244
+ vm.createdAt ? new Date(vm.createdAt).toLocaleString() : "N/A"
245
+ ]);
246
+ formatTable(["VM ID", "Status", "Created"], rows);
247
+ }
248
+ } catch (error) {
249
+ handleError(error);
250
+ }
251
+ }
252
+ ).command(
253
+ "ssh <vmId>",
254
+ "SSH into a VM",
255
+ (yargs2) => {
256
+ return yargs2.positional("vmId", {
257
+ type: "string",
258
+ description: "VM ID to SSH into",
259
+ demandOption: true
260
+ }).option("delete", {
261
+ type: "boolean",
262
+ description: "Delete VM when SSH session ends",
263
+ default: false
264
+ });
265
+ },
266
+ async (argv) => {
267
+ loadEnv();
268
+ const args = argv;
269
+ try {
270
+ await sshIntoVm(args.vmId, {
271
+ deleteOnExit: args.delete
272
+ });
273
+ } catch (error) {
274
+ handleError(error);
275
+ }
276
+ }
277
+ ).command(
278
+ "exec <vmId> <command>",
279
+ "Execute a command on a VM",
280
+ (yargs2) => {
281
+ return yargs2.positional("vmId", {
282
+ type: "string",
283
+ description: "VM ID",
284
+ demandOption: true
285
+ }).positional("command", {
286
+ type: "string",
287
+ description: "Command to execute",
288
+ demandOption: true
289
+ }).option("json", {
290
+ type: "boolean",
291
+ description: "Output as JSON",
292
+ default: false
293
+ });
294
+ },
295
+ async (argv) => {
296
+ loadEnv();
297
+ const args = argv;
298
+ try {
299
+ const freestyle = getFreestyleClient();
300
+ const vm = freestyle.vms.ref({ vmId: args.vmId });
301
+ console.log(`Executing command on VM ${args.vmId}...`);
302
+ const result = await vm.exec({
303
+ command: args.command
304
+ });
305
+ if (args.json) {
306
+ console.log(JSON.stringify(result, null, 2));
307
+ } else {
308
+ if (result.stdout) {
309
+ console.log("\nOutput:");
310
+ console.log(result.stdout);
311
+ }
312
+ if (result.stderr) {
313
+ console.error("\nErrors:");
314
+ console.error(result.stderr);
315
+ }
316
+ console.log(`
317
+ Exit code: ${result.statusCode || 0}`);
318
+ }
319
+ } catch (error) {
320
+ handleError(error);
321
+ }
322
+ }
323
+ ).command(
324
+ "delete <vmId>",
325
+ "Delete a VM",
326
+ (yargs2) => {
327
+ return yargs2.positional("vmId", {
328
+ type: "string",
329
+ description: "VM ID to delete",
330
+ demandOption: true
331
+ });
332
+ },
333
+ async (argv) => {
334
+ loadEnv();
335
+ const args = argv;
336
+ try {
337
+ const freestyle = getFreestyleClient();
338
+ console.log(`Deleting VM ${args.vmId}...`);
339
+ await freestyle.vms.delete({ vmId: args.vmId });
340
+ console.log("\u2713 VM deleted successfully!");
341
+ } catch (error) {
342
+ handleError(error);
343
+ }
344
+ }
345
+ ).demandCommand(1, "You need to specify a vm action");
346
+ },
347
+ handler: () => {
348
+ }
349
+ };
350
+
351
+ const deployCommand = {
352
+ command: "deploy",
353
+ describe: "Deploy a serverless function",
354
+ builder: (yargs) => {
355
+ return yargs.option("code", {
356
+ alias: "c",
357
+ type: "string",
358
+ description: "Inline code to deploy"
359
+ }).option("file", {
360
+ alias: "f",
361
+ type: "string",
362
+ description: "File path containing code to deploy"
363
+ }).option("repo", {
364
+ alias: "r",
365
+ type: "string",
366
+ description: "Git repository ID to deploy"
367
+ }).option("env", {
368
+ alias: "e",
369
+ type: "array",
370
+ description: "Environment variables (KEY=VALUE)",
371
+ default: []
372
+ }).option("json", {
373
+ type: "boolean",
374
+ description: "Output as JSON",
375
+ default: false
376
+ }).check((argv) => {
377
+ const hasCode = !!argv.code;
378
+ const hasFile = !!argv.file;
379
+ const hasRepo = !!argv.repo;
380
+ if (!hasCode && !hasFile && !hasRepo) {
381
+ throw new Error(
382
+ "You must specify one of --code, --file, or --repo"
383
+ );
384
+ }
385
+ if ([hasCode, hasFile, hasRepo].filter(Boolean).length > 1) {
386
+ throw new Error(
387
+ "You can only specify one of --code, --file, or --repo"
388
+ );
389
+ }
390
+ return true;
391
+ });
392
+ },
393
+ handler: async (argv) => {
394
+ loadEnv();
395
+ const args = argv;
396
+ try {
397
+ const freestyle = getFreestyleClient();
398
+ let code;
399
+ let repo;
400
+ if (args.code) {
401
+ code = args.code;
402
+ } else if (args.file) {
403
+ code = fs.readFileSync(args.file, "utf-8");
404
+ } else if (args.repo) {
405
+ repo = args.repo;
406
+ }
407
+ const env = {};
408
+ if (args.env) {
409
+ for (const envVar of args.env) {
410
+ const [key, ...valueParts] = envVar.split("=");
411
+ if (key) {
412
+ env[key] = valueParts.join("=");
413
+ }
414
+ }
415
+ }
416
+ console.log("Creating deployment...");
417
+ const result = await freestyle.serverless.deployments.create({
418
+ ...code ? { code } : {},
419
+ ...repo ? { repo } : {},
420
+ env: Object.keys(env).length > 0 ? env : void 0
421
+ });
422
+ if (args.json) {
423
+ console.log(JSON.stringify(result, null, 2));
424
+ } else {
425
+ console.log("\n\u2713 Deployment created successfully!");
426
+ console.log(` Deployment ID: ${result.deploymentId}`);
427
+ if (result.url) {
428
+ console.log(` URL: ${result.url}`);
429
+ }
430
+ }
431
+ } catch (error) {
432
+ handleError(error);
433
+ }
434
+ }
435
+ };
436
+
437
+ const runCommand = {
438
+ command: "run",
439
+ describe: "Execute a one-off serverless function",
440
+ builder: (yargs) => {
441
+ return yargs.option("code", {
442
+ alias: "c",
443
+ type: "string",
444
+ description: "Inline code to execute"
445
+ }).option("file", {
446
+ alias: "f",
447
+ type: "string",
448
+ description: "File path containing code to execute"
449
+ }).option("env", {
450
+ alias: "e",
451
+ type: "array",
452
+ description: "Environment variables (KEY=VALUE)",
453
+ default: []
454
+ }).option("json", {
455
+ type: "boolean",
456
+ description: "Output as JSON",
457
+ default: false
458
+ }).check((argv) => {
459
+ const hasCode = !!argv.code;
460
+ const hasFile = !!argv.file;
461
+ if (!hasCode && !hasFile) {
462
+ throw new Error("You must specify either --code or --file");
463
+ }
464
+ if (hasCode && hasFile) {
465
+ throw new Error("You can only specify one of --code or --file");
466
+ }
467
+ return true;
468
+ });
469
+ },
470
+ handler: async (argv) => {
471
+ loadEnv();
472
+ const args = argv;
473
+ try {
474
+ const freestyle = getFreestyleClient();
475
+ let code;
476
+ if (args.code) {
477
+ code = args.code;
478
+ } else if (args.file) {
479
+ code = fs.readFileSync(args.file, "utf-8");
480
+ } else {
481
+ throw new Error("Code is required");
482
+ }
483
+ const env = {};
484
+ if (args.env) {
485
+ for (const envVar of args.env) {
486
+ const [key, ...valueParts] = envVar.split("=");
487
+ if (key) {
488
+ env[key] = valueParts.join("=");
489
+ }
490
+ }
491
+ }
492
+ console.log("Executing serverless function...");
493
+ const result = await freestyle.serverless.runs.create({
494
+ code,
495
+ env: Object.keys(env).length > 0 ? env : void 0
496
+ });
497
+ if (args.json) {
498
+ console.log(JSON.stringify(result, null, 2));
499
+ } else {
500
+ console.log("\n\u2713 Function executed successfully!");
501
+ console.log(` Run ID: ${result.runId}`);
502
+ if (result.output) {
503
+ console.log(` Output: ${result.output}`);
504
+ }
505
+ }
506
+ } catch (error) {
507
+ handleError(error);
508
+ }
509
+ }
510
+ };
511
+
512
+ yargs(hideBin(process.argv)).scriptName("freestyle").usage("$0 <command> [options]").command(vmCommand).command(deployCommand).command(runCommand).demandCommand(1, "You need to specify a command").help().alias("help", "h").version().alias("version", "v").strict().parse();