freestyle
v0.2.20
Published
Freestyle SDK and CLI — manage VMs, snapshots, VPCs, domains, and identities
Readme
freestyle
TypeScript SDK and CLI for the Freestyle API: create and manage VMs, snapshots, private networks (VPCs), domains, and identities.
Both the SDK and the freestyle CLI ship from this one package and talk to
the same public API — everything under https://api.freestyle.sh/v5 (see
/openapi.json for the underlying
HTTP contract). The /v5 prefix is added for you; resource paths stay
unversioned throughout the SDK.
Install
Install the CLI, which targets https://api.freestyle.sh:
npm install --global freestyle@latestOr run it without installing:
npx freestyle@latest vm listAs of 0.2.0 this package is the latest dist-tag — freestyle@beta is now
only the prerelease channel (see "Publishing a beta" below).
For SDK use within a project:
npm install freestyle@latestPublishing a beta
After authenticating to npm as a maintainer of the freestyle package:
npm run publish:betaThe release script typechecks and builds the package, then publishes the
prerelease under the beta dist-tag. It does not change latest.
For local, interactive use, authenticate through Stack Auth:
freestyle login # opens the Stack CLI authorization page
freestyle login --no-browser # prints the URL without opening a browser
freestyle whoami # shows the user and effective team
freestyle team list # refresh and list accessible teams
freestyle team current # show the effective active team
freestyle team use dev # switch by unique team name or ID
freestyle vm list --team prod # override for one command
freestyle logout # remove the stored Stack loginThe CLI stores the Stack refresh token and team preference in
~/.freestyle/config.json (mode 0600). Commands refresh a short-lived Stack
access token; the v2 gateway resolves the selected Freestyle team during HTTP
requests and WebSocket handshakes. Login does not create a permanent API key.
The CLI team selection is local to this machine and independent of the
dashboard. Switching teams in the dashboard does not change the CLI team,
and freestyle team use does not change the dashboard team. Logging in again
uses the team chosen on the browser authorization page; if no choice is
returned, it preserves the local selection while that team remains accessible. Use
freestyle team current to check it or freestyle team use <name-or-id> to
change it.
Use a permanent API key from the dashboard for CI and other non-interactive automation. Credential precedence is:
--api-key <key>on the command lineFREESTYLE_API_KEYin the real environmentFREESTYLE_API_KEYin a.envfile in the current directory- The stored Stack login
Team selection for Stack login is:
--team <name-or-id>FREESTYLE_TEAM- The team set by
freestyle team use - The server default selected during browser authorization
When more than one team remains and no default is available, interactive
login prompts; non-interactive commands fail and require --team.
--proxy (to point at a non-default API base URL) works the same way via
FREESTYLE_PROXY. The SDK's Freestyle constructor only honors apiKey/
FREESTYLE_API_KEY from the real environment — it doesn't read .env
files; that's a CLI-only convenience, so load one yourself (e.g. with
dotenv) if you want it in your own code.
Long-running execs
vm.exec({ command: "...", timeoutMs: 3_600_000 }) can run for up to 24 hours
on VMs whose guest agent advertises native resumable exec support. The default
command timeout remains 30 seconds. Older agents retain the existing exec path
and five-minute limit; requesting longer execution returns AGENT_UNSUPPORTED.
Native exec negotiation does not automatically upgrade the agent.
The public interface stays exec-await plus existing background request IDs.
The agent owns the process, timeout, and retained result. Fast commands return
inline without a spooling shell wrapper or fixed polling delay; longer commands
can be observed again after vms, vms-api, or public-api restarts. Small output
stays in memory and larger output spills to private unlinked files. Results remain
available for ten minutes. Public-api resolves and persists the canonical VM ID
before launching, so a reassigned slug cannot redirect a recovering exec.
Expired agent entries are removed after that result window. A compact timestamp cutoff rejects expired retries without retaining a full day of per-exec claims; it also survives an in-place agent update. Known running execs remain pollable regardless of that cutoff. The default 4,096 retained-entry limit supports about six completed execs per second continuously with ten-minute result retention; host runtime policy can raise it for higher-throughput workloads. A request that has never reached the agent and is older than the cutoff fails without executing. Public-api persists the continuation in shared Redis.
Deploy vms and vms-api before public-api; enable the feature per VM by deploying the new guest agent. SDK polling tolerates temporary transport failures for five minutes, and identity-token polls recheck the original VM and Linux-user grant. Individual upstream requests stay bounded to 30 seconds. Create-time execs and calls without backgrounding retain the five-minute cap.
Guest-agent replacement is separate from host/API replacement: it currently refuses to proceed while native execs or their retained results remain, rather than discard them. VM shutdown/reboot also ends running commands.
SDK
import { Freestyle } from "freestyle";
const freestyle = new Freestyle({ apiKey: process.env.FREESTYLE_API_KEY });
const { vm, vmId } = await freestyle.vms.create();
const { stdout } = await vm.exec("echo hello from the VM");
console.log(stdout);
await vm.fs.writeTextFile("/root/hello.txt", "hi");
console.log(await vm.fs.readTextFile("/root/hello.txt"));
const session = await vm.pty.open({
onData: (bytes) => process.stdout.write(Buffer.from(bytes)),
onExit: (code) => console.log("shell exited", code),
});
session.write("ls -la\n");
await vm.delete();Namespaces
freestyle.vms— create/list/get/delete VMs;freestyle.vms.snapshotsfor snapshot management.vms.ref(id)returns aVmhandle withstart/pause/resize/update/delete/exec/snapshot, plus.fs(guest filesystem) and.pty(interactive terminal WebSockets).freestyle.vpc— create/list/get/delete private networks.vpc.ref(id)returns aVpchandle with.wireguardfor ephemeral or public-key WireGuard tunnels into the network.freestyle.domains— list the domains you hold;.verificationsand.certificatessub-namespaces for proving ownership of a domain and for its TLS certificates.freestyle.tls— TLS rules: publish a domain to a VM port, inject a secret into a VM's outbound calls, reach another VM by name. Any unused subdomain ofstyle.devis free to take and needs no verification.freestyle.identities— scoped access for your end users.identities.ref(id)returns anIdentityhandle with.tokensand.permissions.vmfor per-VM access grants.
Every namespace method and Vm/Vpc/Identity handle method returns a
plain typed object (or void for deletes) — nothing is wrapped in an
envelope. HTTP errors are thrown as FreestyleApiError (.code, .message,
.status).
An identity access token (rather than an API key) can also authenticate,
scoped to one VM's exec/PTY routes:
const freestyle = new Freestyle({ identityAccessToken: token });Transport
On Node the SDK talks HTTP/2 over a small pool of shared connections, so
concurrent calls multiplex instead of each paying its own TCP and TLS
handshake. It needs no setup, and falls back to the runtime's own fetch
wherever HTTP/2 is not available or not useful: plain http: base URLs,
browsers, Cloudflare Workers, and Deno all pool for you already.
A fetch you pass is used exactly as given, and replaces all of this — pass one
to route through a proxy, size the connection pool yourself, add retries, or
force HTTP/1.1.
new Freestyle({ apiKey, fetch: myFetch });Fetch failures with underlying connection details throw FreestyleTransportError
(a TypeError). Its .errors array records each available error code, address,
port, syscall, errno, name, and message, including attempts nested inside an
AggregateError. The message includes codes and endpoints for loggers that only
record error messages; JSON.stringify(error) retains the structured details.
The original fetch error and its cause chain remain available as .cause.
import { FreestyleTransportError } from "freestyle";
try {
await freestyle.vms.get("vm-id");
} catch (error) {
if (error instanceof FreestyleTransportError) {
console.error(JSON.stringify(error));
}
throw error;
}Diagnostics apply to SDK methods, raw freestyle.fetch, and background polls,
including custom fetch implementations. Cancellations and failures without
connection details retain their original error. This adds no retries and does
not attach request headers, bodies, or URLs to errors. Runtime error messages
are retained; apply your normal log redaction policy to custom transport errors.
CLI
freestyle <command>
Commands:
freestyle vm <action> Manage Virtual Machines
freestyle snapshot <action> Create and manage snapshots
freestyle vpc <action> Manage private networks
freestyle tunnels <action> Manage tunnels into private networks
freestyle firewall <action> Manage firewall rules
freestyle tls <action> Manage TLS rules — publish domains to VMs, inject secrets, connect VMs by name
freestyle domain <action> Verify domains and manage their TLS certificates
freestyle identity <action> Scoped access for your end users
freestyle skill <action> Install the Freestyle docs as an agent skill (freestyle-docs)
Options:
--api-key Freestyle API key (defaults to $FREESTYLE_API_KEY) [string]
--team team name or ID (defaults to $FREESTYLE_TEAM) [string]
--proxy override the base url for the freestyle api [string]
-h, --help Show help [boolean]
-v, --version Show version number [boolean]freestyle vm <action>
Commands:
freestyle vm create Create a new VM (or just `freestyle vm`)
freestyle vm list List all VMs
freestyle vm get <vmId> Fetch a VM
freestyle vm update <vmId> Rename a VM, change its slug or idle timeout, or merge in metadata
freestyle vm start <vmId> Boot a stopped VM, or resume a paused one
freestyle vm pause <vmId> Freeze a running VM, keeping its memory
freestyle vm resize <vmId> Change a VM's vCPU, memory, or disk (grow-only)
freestyle vm delete <vmId> Delete a VM
freestyle vm exec <vmId> <command..> Execute a command on a VM
freestyle vm ssh <vmId> SSH into a VM
freestyle vm scp <source> <destination> Copy files or directories to or from
a VM (use '<vmId>:<path>' for the
remote side)
freestyle vm snapshot <action> Manage snapshots
freestyle vm fs <action> Read and write files inside a VMAt a terminal, vm create opens a shell in the new VM; --no-ssh prints the
record instead. Scripts, CI, and coding agents get the record.
freestyle snapshot <action>
Commands:
freestyle snapshot create [vmId] Build a snapshot: set up a fresh VM by hand
or with a script, then capture it
freestyle snapshot list List your snapshots
freestyle snapshot get <snapshotId> Fetch a snapshot
freestyle snapshot update <snapshotId> Rename a snapshot or change its slug
freestyle snapshot delete <snapshotId> Delete a snapshot permanentlyWithout a vmId, snapshot create boots a VM, opens a shell in it (or runs
--script ./setup.sh, streamed to your terminal), captures it, and deletes
the VM. Ctrl-C, or a script that exits non-zero, deletes the VM and takes no
snapshot.
Every command prints the API's JSON response to stdout (pretty-printed) so
it composes with jq; delete-type commands print a one-line confirmation
instead. vm exec streams stdout/stderr directly and exits with the guest's
own status code.
freestyle vpc, freestyle tls, freestyle domain, and freestyle identity
mirror their SDK namespaces the same way — run --help on any of them (or any
subcommand) to see its options.
freestyle skill install needs no login. It takes the skill published in
onboard.md and writes it
into the skills directory of every coding agent it detects on the machine
(Claude Code, Codex, GitHub Copilot; --agent picks, --project installs
into the current repository instead of your home directory).
