typescript

typescript sdk

the node and bun package is generated from the rust sdk. typescript adds await using, async iterators and generated zod schemas at the language boundary.

the package is @indexable/sdk, node 22 or newer, or bun. it ships generated declarations; no code generation runs in the consumer project.

boot a machine

set IX_TOKEN first, or run ix login. an omitted region uses IX_REGION, then us-west-1.

import { Client } from '@indexable/sdk'

const ix = new Client()
// no target: a new machine boots the base template.
const machine = await ix.machines().create({ name: 'sdk-example' })
try {
	const result = await machine.execChecked(['uname', '-a'])
	console.log(result.stdout.trim())
} finally {
	await machine.delete()
}

the handle returned by create carries every verb: exec, readFile, writeFile, snapshot, tailLogs, watch. delete the VM when you are done with it, or skip the delete and it keeps running - a VM outlives the process that made it. ix.machines().connect(id) reattaches later.

a stateful repl

Repl is the one piece of language-owned sugar: a long-lived interpreter inside the VM, on one pty. state persists across exec calls, and independent sessions are independent:

import { Client, Repl } from '@indexable/sdk'

const ix = new Client()
const machine = await ix.machines().create({})
try {
	await using py = await Repl.open(machine, 'python')
	await py.exec('import math')
	await py.exec('x = 42')
	const b = await py.exec('print(x * 2, math.pi)')
} finally {
	await machine.delete()
}

a second Repl.open(machine, 'python') is an independent session: it does not see x. await using is typescript and bun syntax; plain node 22 running untranspiled javascript rejects it - call py.close() in a finally there instead.

streams are async iterables everywhere: for await consumes machine.watch() and machine.tailLogs(), and dropping the iterator ends the stream. every call takes a trailing AbortSignal, and aborting it cancels the rust future behind it.

validate external data

the same generator emits a zod schema for every record, and records are plain JSON (integers are numbers, the stripe/openai convention). import schemas only where data has crossed an untyped boundary:

import { MachineInfo } from '@indexable/sdk/schemas'

const machine = MachineInfo.parse(JSON.parse(payload))

values returned directly by Client are already typed and do not need to be parsed again.