@aerol-ai/aerolvm-sdk 0.1.8 → 0.1.9

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.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Image is a fluent Dockerfile builder. The shape mirrors the Daytona SDK's
3
+ * Image API so examples port across by changing the import. The output is a
4
+ * Dockerfile string accessible via `.dockerfile` — the SDK ships it to
5
+ * `POST /v1/images/build` when an Image is passed to `MicroVM.create`.
6
+ *
7
+ * Caller-side context (COPY/ADD from local files) needs the
8
+ * SB_IMAGE_BUILD_CONTEXT_ENABLED operator flag on the daemon. Without that,
9
+ * use only `runCommands`, `env`, `workdir`, `entrypoint`, `cmd`, `user`, and
10
+ * `expose` — every step they emit is RUN/ENV/WORKDIR/etc. against the base
11
+ * image, no local files involved.
12
+ */
13
+ export declare class Image {
14
+ private _dockerfile;
15
+ private constructor();
16
+ get dockerfile(): string;
17
+ /** Start a new image from `FROM <image>`. */
18
+ static base(image: string): Image;
19
+ /**
20
+ * Build from a raw Dockerfile string. Use when you need full control
21
+ * (multi-stage builds, ARG, ONBUILD, HEALTHCHECK, etc.) that the fluent
22
+ * API doesn't expose. Caller is responsible for the Dockerfile validity.
23
+ */
24
+ static fromDockerfile(dockerfile: string): Image;
25
+ /**
26
+ * Append one or more RUN steps. Each argument becomes its own RUN line;
27
+ * an array argument is joined with `&&` so the commands share one layer.
28
+ */
29
+ runCommands(...commands: (string | string[])[]): Image;
30
+ /** Append ENV K=V pairs (one ENV line per call, all vars on it). */
31
+ env(envVars: Record<string, string>): Image;
32
+ workdir(dirPath: string): Image;
33
+ entrypoint(entrypointCommands: string[]): Image;
34
+ cmd(cmd: string[]): Image;
35
+ user(username: string): Image;
36
+ expose(port: number): Image;
37
+ }
package/dist/Image.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Image is a fluent Dockerfile builder. The shape mirrors the Daytona SDK's
3
+ * Image API so examples port across by changing the import. The output is a
4
+ * Dockerfile string accessible via `.dockerfile` — the SDK ships it to
5
+ * `POST /v1/images/build` when an Image is passed to `MicroVM.create`.
6
+ *
7
+ * Caller-side context (COPY/ADD from local files) needs the
8
+ * SB_IMAGE_BUILD_CONTEXT_ENABLED operator flag on the daemon. Without that,
9
+ * use only `runCommands`, `env`, `workdir`, `entrypoint`, `cmd`, `user`, and
10
+ * `expose` — every step they emit is RUN/ENV/WORKDIR/etc. against the base
11
+ * image, no local files involved.
12
+ */
13
+ export class Image {
14
+ _dockerfile = "";
15
+ constructor(dockerfile) {
16
+ this._dockerfile = dockerfile;
17
+ }
18
+ get dockerfile() {
19
+ return this._dockerfile;
20
+ }
21
+ /** Start a new image from `FROM <image>`. */
22
+ static base(image) {
23
+ if (typeof image !== "string" || image.trim() === "") {
24
+ throw new TypeError("Image.base requires a non-empty image string");
25
+ }
26
+ return new Image(`FROM ${image.trim()}\n`);
27
+ }
28
+ /**
29
+ * Build from a raw Dockerfile string. Use when you need full control
30
+ * (multi-stage builds, ARG, ONBUILD, HEALTHCHECK, etc.) that the fluent
31
+ * API doesn't expose. Caller is responsible for the Dockerfile validity.
32
+ */
33
+ static fromDockerfile(dockerfile) {
34
+ if (typeof dockerfile !== "string" || dockerfile.trim() === "") {
35
+ throw new TypeError("Image.fromDockerfile requires a non-empty Dockerfile string");
36
+ }
37
+ return new Image(dockerfile.endsWith("\n") ? dockerfile : dockerfile + "\n");
38
+ }
39
+ /**
40
+ * Append one or more RUN steps. Each argument becomes its own RUN line;
41
+ * an array argument is joined with `&&` so the commands share one layer.
42
+ */
43
+ runCommands(...commands) {
44
+ for (const entry of commands) {
45
+ if (Array.isArray(entry)) {
46
+ const joined = entry.map((c) => c.trim()).filter(Boolean).join(" && ");
47
+ if (joined !== "") {
48
+ this._dockerfile += `RUN ${joined}\n`;
49
+ }
50
+ continue;
51
+ }
52
+ const cmd = entry.trim();
53
+ if (cmd !== "") {
54
+ this._dockerfile += `RUN ${cmd}\n`;
55
+ }
56
+ }
57
+ return this;
58
+ }
59
+ /** Append ENV K=V pairs (one ENV line per call, all vars on it). */
60
+ env(envVars) {
61
+ const parts = Object.entries(envVars).map(([k, v]) => `${k}=${dockerQuote(v)}`);
62
+ if (parts.length > 0) {
63
+ this._dockerfile += `ENV ${parts.join(" ")}\n`;
64
+ }
65
+ return this;
66
+ }
67
+ workdir(dirPath) {
68
+ if (dirPath.trim() === "") {
69
+ throw new TypeError("Image.workdir requires a non-empty path");
70
+ }
71
+ this._dockerfile += `WORKDIR ${dirPath}\n`;
72
+ return this;
73
+ }
74
+ entrypoint(entrypointCommands) {
75
+ this._dockerfile += `ENTRYPOINT ${jsonExecForm(entrypointCommands)}\n`;
76
+ return this;
77
+ }
78
+ cmd(cmd) {
79
+ this._dockerfile += `CMD ${jsonExecForm(cmd)}\n`;
80
+ return this;
81
+ }
82
+ user(username) {
83
+ if (username.trim() === "") {
84
+ throw new TypeError("Image.user requires a non-empty username");
85
+ }
86
+ this._dockerfile += `USER ${username}\n`;
87
+ return this;
88
+ }
89
+ expose(port) {
90
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
91
+ throw new RangeError(`Image.expose: port ${port} is out of range`);
92
+ }
93
+ this._dockerfile += `EXPOSE ${port}\n`;
94
+ return this;
95
+ }
96
+ }
97
+ function dockerQuote(value) {
98
+ // Quote when the value contains whitespace or a special char; otherwise
99
+ // leave bare to match canonical Dockerfile style.
100
+ if (/^[A-Za-z0-9_\-./:@]+$/.test(value)) {
101
+ return value;
102
+ }
103
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
104
+ }
105
+ function jsonExecForm(parts) {
106
+ return JSON.stringify(parts);
107
+ }
package/dist/MicroVM.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type APIVersion } from "./internal/client.js";
2
2
  import { Sandbox } from "./Sandbox.js";
3
- import type { CreateOptions, CreateSessionOptions, ExecStreamHandle, ExecStreamOptions, HealthStatus, Lifecycle, MountSpecRedacted, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions } from "./types.js";
3
+ import { Image } from "./Image.js";
4
+ import type { BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, ExecStreamHandle, ExecStreamOptions, HealthStatus, Lifecycle, MountSpecRedacted, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions } from "./types.js";
4
5
  type FetchLike = typeof fetch;
5
6
  export interface MicroVMConfig {
6
7
  patToken?: string;
@@ -29,6 +30,13 @@ export declare class MicroVM {
29
30
  resize(id: string, options: ResizeOptions): Promise<Sandbox>;
30
31
  updateLifecycle(id: string, lifecycle: Lifecycle): Promise<Sandbox>;
31
32
  reconcile(): Promise<void>;
33
+ /**
34
+ * Build an {@link Image} on the daemon and optionally push the result to a
35
+ * remote registry. Push credentials are forwarded per request and never
36
+ * persisted by the daemon. Returns the local content-addressed tag and,
37
+ * when push was requested, the pushed reference.
38
+ */
39
+ buildImage(image: Image, options?: BuildImageOptions): Promise<BuildImageResult>;
32
40
  health(): Promise<HealthStatus>;
33
41
  mounts(sandboxID: string): Promise<MountSpecRedacted[]>;
34
42
  execStream(sandboxID: string, options: ExecStreamOptions): ExecStreamHandle;
package/dist/MicroVM.js CHANGED
@@ -58,6 +58,15 @@ export class MicroVM {
58
58
  async reconcile() {
59
59
  await this.client.reconcile();
60
60
  }
61
+ /**
62
+ * Build an {@link Image} on the daemon and optionally push the result to a
63
+ * remote registry. Push credentials are forwarded per request and never
64
+ * persisted by the daemon. Returns the local content-addressed tag and,
65
+ * when push was requested, the pushed reference.
66
+ */
67
+ async buildImage(image, options) {
68
+ return this.client.buildImage(image, options);
69
+ }
61
70
  async health() {
62
71
  return this.client.health();
63
72
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { MicroVM } from "./MicroVM.js";
2
2
  export { Sandbox } from "./Sandbox.js";
3
+ export { Image } from "./Image.js";
3
4
  export type { MicroVMConfig } from "./MicroVM.js";
4
- export type { BinaryLike, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, ExposePortOptions, ExposeProtocol, ExposeResult, HealthStatus, Lifecycle, MountSpec, MountSpecRedacted, MountType, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxSnapshot, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, UpdateLifecycleOptions, } from "./types.js";
5
+ export type { BinaryLike, BuildImageOptions, BuildImagePushOptions, BuildImageResult, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, ExposePortOptions, ExposeProtocol, ExposeResult, HealthStatus, Lifecycle, MountSpec, MountSpecRedacted, MountType, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxSnapshot, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, UpdateLifecycleOptions, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { MicroVM } from "./MicroVM.js";
2
2
  export { Sandbox } from "./Sandbox.js";
3
+ export { Image } from "./Image.js";
@@ -1,7 +1,7 @@
1
1
  // v1 path-prefix constant. The TypeScript SDK uses this to build URLs against
2
- // the v1 surface of the sandbox daemon. When v2 lands, a sibling
3
- // internal/api/v2/paths.ts will export its own PATH_PREFIX, and the client
4
- // constructor's apiVersion option selects which one to use.
2
+ // the v1 surface of the sandbox daemon. When a new wire version lands, a
3
+ // sibling internal/api/vN/paths.ts will export its own PATH_PREFIX, and the
4
+ // client constructor's apiVersion option selects which one to use.
5
5
  //
6
6
  // Keep this in sync with pkg/api/v1/dto.go::PathPrefix on the server.
7
7
  export const PATH_PREFIX = "/v1";
@@ -1,10 +1,19 @@
1
- import type { BinaryLike, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, ExposePortOptions, ExposeResult, HealthStatus, Lifecycle, MountSpecRedacted, NetworkUsage, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions } from "../types.js";
1
+ import { Image } from "../Image.js";
2
+ import type { BinaryLike, BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, ExposePortOptions, ExposeResult, HealthStatus, Lifecycle, MountSpecRedacted, NetworkUsage, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions } from "../types.js";
2
3
  type FetchLike = typeof fetch;
3
4
  /**
4
- * Wire version of the sandbox daemon API this client speaks. The SDK and the
5
- * server version independently — bumping the SDK package does not move the
6
- * wire version. Today only "v1" exists; "v2" will be added when a wire-level
7
- * breaking change is needed on the server.
5
+ * Wire version of the v1-style sandbox endpoints (`/v1/sandboxes`, etc.) this
6
+ * client speaks. The SDK and the server version independently — bumping the
7
+ * SDK package does not move the wire version. Today only "v1" exists; future
8
+ * breaking changes to the sandbox endpoints will land in a new version
9
+ * prefix.
10
+ *
11
+ * The `/v1/images/build` endpoint used by {@link Image}-shaped image inputs
12
+ * is called unconditionally when an Image is supplied to
13
+ * {@link APIClient.create}, regardless of `apiVersion`. Daemons older than
14
+ * the image-build feature return 404; the SDK turns that into a clear
15
+ * "daemon does not support image builds" error so the user can switch to a
16
+ * string image.
8
17
  */
9
18
  export type APIVersion = "v1";
10
19
  export interface APIClientConfig {
@@ -28,11 +37,24 @@ export declare class APIClient {
28
37
  /**
29
38
  * Build a versioned API path. Pass the suffix beginning with "/" (e.g.
30
39
  * "/sandboxes") and the active version's prefix is prepended. Use this for
31
- * every versioned API call so v2 (when it lands) can be selected by the
40
+ * every versioned API call so a future wire version can be selected by the
32
41
  * apiVersion option without touching call sites.
33
42
  */
34
43
  private versioned;
35
44
  create(options: CreateOptions): Promise<SandboxResource>;
45
+ /**
46
+ * Compile a fluent {@link Image} into a content-addressed image tag by
47
+ * POSTing its Dockerfile to `/v1/images/build`. The daemon caches by
48
+ * content hash, so repeated calls with the same Dockerfile are a no-op.
49
+ * String images are passed through unchanged.
50
+ *
51
+ * Daemons predating the image-build feature do not register the route and
52
+ * return 404; this method translates that into a clear error telling the
53
+ * caller to pass a string image instead, rather than the generic "request
54
+ * failed with status 404" the JSON decoder would otherwise produce.
55
+ */
56
+ buildImage(image: Image, options?: BuildImageOptions): Promise<BuildImageResult>;
57
+ private resolveImage;
36
58
  list(): Promise<SandboxResource[]>;
37
59
  get(id: string): Promise<SandboxResource>;
38
60
  start(id: string): Promise<SandboxResource>;
@@ -1,5 +1,6 @@
1
1
  import { basename } from "node:path";
2
2
  import { PATH_PREFIX as V1_PATH_PREFIX } from "./api/v1/paths.js";
3
+ import { Image } from "../Image.js";
3
4
  const DEFAULT_API_VERSION = "v1";
4
5
  const PATH_PREFIXES = {
5
6
  v1: V1_PATH_PREFIX,
@@ -20,16 +21,70 @@ export class APIClient {
20
21
  /**
21
22
  * Build a versioned API path. Pass the suffix beginning with "/" (e.g.
22
23
  * "/sandboxes") and the active version's prefix is prepended. Use this for
23
- * every versioned API call so v2 (when it lands) can be selected by the
24
+ * every versioned API call so a future wire version can be selected by the
24
25
  * apiVersion option without touching call sites.
25
26
  */
26
27
  versioned(suffix) {
27
28
  return `${this.versionPrefix}${suffix}`;
28
29
  }
29
30
  async create(options) {
30
- const response = await this.doJSON("POST", this.versioned("/sandboxes"), toApiCreateOptions(options));
31
+ const resolved = await this.resolveImage(options);
32
+ const response = await this.doJSON("POST", this.versioned("/sandboxes"), toApiCreateOptions(resolved));
31
33
  return new SandboxResource(this, fromApiCreateSandboxResponse(response));
32
34
  }
35
+ /**
36
+ * Compile a fluent {@link Image} into a content-addressed image tag by
37
+ * POSTing its Dockerfile to `/v1/images/build`. The daemon caches by
38
+ * content hash, so repeated calls with the same Dockerfile are a no-op.
39
+ * String images are passed through unchanged.
40
+ *
41
+ * Daemons predating the image-build feature do not register the route and
42
+ * return 404; this method translates that into a clear error telling the
43
+ * caller to pass a string image instead, rather than the generic "request
44
+ * failed with status 404" the JSON decoder would otherwise produce.
45
+ */
46
+ async buildImage(image, options) {
47
+ const body = { dockerfile_content: image.dockerfile };
48
+ if (options?.push) {
49
+ const p = options.push;
50
+ if (!p.registry) {
51
+ throw new Error("buildImage: push.registry is required when push is set");
52
+ }
53
+ if (!p.username || !p.password) {
54
+ throw new Error("buildImage: push.username and push.password are required when push is set");
55
+ }
56
+ body.push = {
57
+ registry: p.registry,
58
+ tag: p.tag,
59
+ server: p.server,
60
+ username: p.username,
61
+ password: p.password,
62
+ };
63
+ }
64
+ const response = await this.request("POST", "/v1/images/build", {
65
+ body: JSON.stringify(body),
66
+ headers: { "Content-Type": "application/json" },
67
+ });
68
+ if (response.status === 404) {
69
+ await response.text().catch(() => undefined);
70
+ throw new Error("this daemon does not support Image builds (POST /v1/images/build is not registered) — pass a string image reference (e.g. \"ubuntu:22.04\") instead, or upgrade the daemon");
71
+ }
72
+ if (!response.ok) {
73
+ throw await decodeError(response);
74
+ }
75
+ const payload = (await response.json());
76
+ return { image: payload.image, pushed: payload.pushed };
77
+ }
78
+ async resolveImage(options) {
79
+ if (typeof options.image === "string") {
80
+ return options;
81
+ }
82
+ if (!(options.image instanceof Image)) {
83
+ throw new TypeError("CreateOptions.image must be a string or Image");
84
+ }
85
+ const result = await this.buildImage(options.image);
86
+ return { ...options, image: result.image };
87
+ }
33
88
  async list() {
34
89
  const response = await this.doJSON("GET", this.versioned("/sandboxes"));
35
90
  return response.map((item) => this.wrap(item));
package/dist/types.d.ts CHANGED
@@ -4,6 +4,30 @@ export interface RegistryAuth {
4
4
  username: string;
5
5
  password: string;
6
6
  }
7
+ /**
8
+ * Per-request push directive for `MicroVM.buildImage`. Credentials are
9
+ * forwarded to the daemon as a one-shot `X-Registry-Auth` header on the
10
+ * push call and are never persisted.
11
+ */
12
+ export interface BuildImagePushOptions {
13
+ /** Destination repository, e.g. "ghcr.io/my-org/my-image". */
14
+ registry: string;
15
+ /** Destination tag. Defaults to "latest" on the daemon when omitted. */
16
+ tag?: string;
17
+ /** Registry serveraddress, e.g. "ghcr.io". Sent inside X-Registry-Auth. */
18
+ server?: string;
19
+ username: string;
20
+ password: string;
21
+ }
22
+ export interface BuildImageOptions {
23
+ push?: BuildImagePushOptions;
24
+ }
25
+ export interface BuildImageResult {
26
+ /** Local content-addressed tag (always returned). */
27
+ image: string;
28
+ /** Pushed reference, e.g. "ghcr.io/my-org/my-image:v1.2.3". */
29
+ pushed?: string;
30
+ }
7
31
  export type MountType = "s3" | "nfs" | "sshfs" | "rclone";
8
32
  export interface MountSpec {
9
33
  type: MountType;
@@ -60,8 +84,17 @@ export interface GPUOptions {
60
84
  */
61
85
  deviceIDs?: string[];
62
86
  }
87
+ import type { Image } from "./Image.js";
63
88
  export interface CreateOptions {
64
- image: string;
89
+ /**
90
+ * The base image for this sandbox. A bare image reference (e.g.
91
+ * `"ubuntu:22.04"`) is pulled by the daemon as-is. An {@link Image} builder
92
+ * is compiled to a Dockerfile and sent to the daemon's
93
+ * `POST /v1/images/build` endpoint; the resulting content-addressed tag is
94
+ * used for the sandbox. The daemon must have an image builder configured
95
+ * (every official deployment does); otherwise the build call returns 503.
96
+ */
97
+ image: string | Image;
65
98
  /** Number of CPU cores to allocate. Fractional values are supported (e.g. 0.5 = half a core). */
66
99
  cpu?: number;
67
100
  memoryMB?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerol-ai/aerolvm-sdk",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",