Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Release

on:
push:
tags:
- "v*"
workflow_dispatch:
Comment on lines +6 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

When npm publishis run without--tag, what dist-tag is used by default, and how should prerelease versions be published to avoid setting latest?

💡 Result:

When npm publish is run without --tag, the default dist-tag used is latest. This is confirmed across multiple official npm documentation pages and changelogs. To publish prerelease versions (e.g., versions with a prerelease identifier like 1.0.0-alpha.1) without setting the latest tag, use the --tag flag with a different tag name, such as beta, alpha, next, or rc. For example: npm publish --tag beta. In npm 11 (current as of 2026), publishing prereleases without an explicit --tag requires specifying a tag explicitly; otherwise, it fails to prevent accidentally setting latest. For non-prerelease versions, the default latest is applied only if the version is greater than the current latest semver version (excluding prereleases).

Citations:


🏁 Script executed:

# First, let's find and read the release workflow file
find . -path "./.github/workflows/release.yml" -type f

Repository: leap0-dev/leap0-js

Length of output: 1887


Publish prerelease versions with an explicit dist-tag to prevent incorrectly setting latest.

Without --tag, npm publish defaults to the latest dist-tag. Prerelease versions (e.g., v1.0.0-beta.1) trigger on the v* tag pattern but will incorrectly mark themselves as latest for consumers unless explicitly tagged otherwise.

Proposed fix
 jobs:
   publish:
     runs-on: ubuntu-latest
@@
       - name: Run tests
         run: pnpm test
 
+      - name: Resolve npm dist-tag
+        id: npm_tag
+        run: |
+          if [[ "${GITHUB_REF_NAME#v}" == *-* ]]; then
+            echo "tag=next" >> "$GITHUB_OUTPUT"
+          else
+            echo "tag=latest" >> "$GITHUB_OUTPUT"
+          fi
+
       - name: Publish package
-        run: npm publish --provenance --access public
+        run: npm publish --provenance --access public --tag "${{ steps.npm_tag.outputs.tag }}"
         env:
           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 6 - 7, The release workflow
currently triggers on the "v*" tag pattern and runs npm publish without a dist
tag, which causes prerelease tags (e.g., v1.0.0-beta.1) to be published as
latest; update the release job in release.yml so that the npm publish step adds
an explicit --tag for prereleases (e.g., --tag next or a tag derived from the
prerelease identifier) and only omits --tag for normal releases; detect
prerelease by checking the git tag string for a hyphen/prerelease identifier
(the workflow's tag ref matching "v*" and the npm publish command) and pass the
appropriate --tag flag accordingly.


permissions:
contents: read
id-token: write

jobs:
publish:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up pnpm
uses: pnpm/action-setup@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.19.0
cache: pnpm
registry-url: https://registry.npmjs.org

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run tests
run: pnpm test

- name: Publish package
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
61 changes: 33 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,25 @@ export LEAP0_API_KEY="your-api-key"
Or pass it directly when creating a client:

```ts
import { Leap0Client } from "leap0"
import { Leap0Client } from "leap0";

const client = new Leap0Client({ apiKey: "your-api-key" })
const client = new Leap0Client({ apiKey: "your-api-key" });
```

## Quick Start

```ts
import { Leap0Client } from "leap0"
import { Leap0Client } from "leap0";

const client = new Leap0Client()
const sandbox = await client.sandboxes.create()
const client = new Leap0Client();
const sandbox = await client.sandboxes.create();

try {
const result = await sandbox.process.execute({ command: "echo hello from leap0" })
console.log(result.result.trim())
const result = await sandbox.process.execute({ command: "echo hello from leap0" });
console.log(result.result.trim());
} finally {
await sandbox.delete()
await client.close()
await sandbox.delete();
await client.close();
}
```

Expand All @@ -59,88 +59,93 @@ try {
Stateful code execution with streaming output.

```ts
import { CodeLanguage, DEFAULT_CODE_INTERPRETER_TEMPLATE_NAME } from "leap0"

const sandbox = await client.sandboxes.create({ templateName: DEFAULT_CODE_INTERPRETER_TEMPLATE_NAME })
const result = await sandbox.codeInterpreter.execute({ code: "x = 42", language: CodeLanguage.PYTHON })
import { CodeLanguage, DEFAULT_CODE_INTERPRETER_TEMPLATE_NAME } from "leap0";

const sandbox = await client.sandboxes.create({
templateName: DEFAULT_CODE_INTERPRETER_TEMPLATE_NAME,
});
const result = await sandbox.codeInterpreter.execute({
code: "x = 42",
language: CodeLanguage.PYTHON,
});
```

### Filesystem

Read, write, search, and inspect files inside a sandbox.

```ts
await sandbox.filesystem.writeFile("/workspace/hello.txt", "Hello!")
const content = await sandbox.filesystem.readFile("/workspace/hello.txt")
const tree = await sandbox.filesystem.tree("/workspace", 2)
await sandbox.filesystem.writeFile("/workspace/hello.txt", "Hello!");
const content = await sandbox.filesystem.readFile("/workspace/hello.txt");
const tree = await sandbox.filesystem.tree("/workspace", 2);
```

### Git

Clone repositories and run Git operations inside the sandbox.

```ts
await sandbox.git.clone("https://github.com/octocat/Hello-World.git", "/workspace/repo")
const status = await sandbox.git.status("/workspace/repo")
await sandbox.git.clone("https://github.com/octocat/Hello-World.git", "/workspace/repo");
const status = await sandbox.git.status("/workspace/repo");
```

### Process Execution

Run one-off shell commands inside a running sandbox.

```ts
const result = await sandbox.process.execute({ command: "ls -la /workspace" })
console.log(result.result)
const result = await sandbox.process.execute({ command: "ls -la /workspace" });
console.log(result.result);
```

### Interactive Terminal (PTY)

Open persistent terminal sessions over WebSocket.

```ts
const session = await sandbox.pty.create({ cols: 120, rows: 30, cwd: "/home/user" })
const session = await sandbox.pty.create({ cols: 120, rows: 30, cwd: "/home/user" });
```

### Language Server Protocol (LSP)

Use language servers for completions and editor-style workflows.

```ts
await sandbox.lsp.start({ languageId: "python", pathToProject: "/workspace" })
await sandbox.lsp.start({ languageId: "python", pathToProject: "/workspace" });
```

### SSH Access

Generate temporary SSH credentials for direct sandbox access.

```ts
const access = await sandbox.ssh.createAccess()
console.log(access.hostname, access.port, access.username)
const access = await sandbox.ssh.createAccess();
console.log(access.hostname, access.port, access.username);
```

### Desktop Automation

Control a graphical desktop inside the sandbox.

```ts
const screenshot = await sandbox.desktop.screenshot()
const screenshot = await sandbox.desktop.screenshot();
```

### Snapshots

Save and restore sandbox state.

```ts
const snapshot = await client.snapshots.create(sandbox, { name: "my-checkpoint" })
const restored = await client.snapshots.resume({ snapshotName: snapshot.name ?? "my-checkpoint" })
const snapshot = await client.snapshots.create(sandbox, { name: "my-checkpoint" });
const restored = await client.snapshots.resume({ snapshotName: snapshot.name ?? "my-checkpoint" });
```

## Supported Imports

Import clients, enums, and types from the package root:

```ts
import { Leap0Client, SandboxState, type CreateSandboxParams } from "leap0"
import { Leap0Client, SandboxState, type CreateSandboxParams } from "leap0";
```

## Examples
Expand Down
29 changes: 17 additions & 12 deletions examples/code_interpreter_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,33 @@ import {
DEFAULT_CODE_INTERPRETER_TEMPLATE_NAME,
Leap0Client,
StreamEvent,
} from "../src/index.js"
} from "../src/index.js";

async function main(): Promise<void> {
const client = new Leap0Client()
const client = new Leap0Client();

try {
const sandbox = await client.sandboxes.create({ templateName: DEFAULT_CODE_INTERPRETER_TEMPLATE_NAME })
const sandbox = await client.sandboxes.create({
templateName: DEFAULT_CODE_INTERPRETER_TEMPLATE_NAME,
});

try {
for await (const event of sandbox.codeInterpreter.executeStream({
code: "import time\nfor i in range(3):\n print(f'step {i}')\n time.sleep(1)",
language: CodeLanguage.PYTHON,
}, { timeout: 10 })) {
const typedEvent: StreamEvent = event
console.log(typedEvent)
for await (const event of sandbox.codeInterpreter.executeStream(
{
code: "import time\nfor i in range(3):\n print(f'step {i}')\n time.sleep(1)",
language: CodeLanguage.PYTHON,
},
{ timeout: 10 },
)) {
const typedEvent: StreamEvent = event;
console.log(typedEvent);
}
} finally {
await sandbox.delete()
await sandbox.delete();
}
} finally {
await client.close()
await client.close();
}
}

void main()
void main();
52 changes: 25 additions & 27 deletions examples/desktop.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
import { writeFile } from "node:fs/promises"
import { writeFile } from "node:fs/promises";

import { DEFAULT_DESKTOP_TEMPLATE_NAME, Leap0Client } from "../src/index.js"

function decodeScreenshot(value: string): Uint8Array {
const encoded = value.startsWith("data:") ? value.slice(value.indexOf(",") + 1) : value
return Buffer.from(encoded, "base64")
}
import { DEFAULT_DESKTOP_TEMPLATE_NAME, Leap0Client } from "../src/index.js";

async function main(): Promise<void> {
const client = new Leap0Client()
let sandbox: Awaited<ReturnType<Leap0Client["createSandbox"]>> | null = null
const client = new Leap0Client();

try {
sandbox = await client.sandboxes.create({ templateName: DEFAULT_DESKTOP_TEMPLATE_NAME })
await sandbox.desktop.waitUntilReady(60)
console.log("Desktop:", sandbox.desktop.browserUrl())

const display = await sandbox.desktop.display()
console.log("Display:", display)

await sandbox.desktop.movePointer(Math.floor(display.width / 2), Math.floor(display.height / 2))
await sandbox.desktop.click(1)

const screenshot = await sandbox.desktop.screenshot()
await writeFile("desktop-screenshot.png", decodeScreenshot(screenshot))
console.log("Saved screenshot to desktop-screenshot.png")
} finally {
if (sandbox) {
await sandbox.delete()
const sandbox = await client.sandboxes.create({ templateName: DEFAULT_DESKTOP_TEMPLATE_NAME });
try {
await sandbox.desktop.waitUntilReady(60);
console.log("Desktop:", sandbox.desktop.desktopUrl());

const display = await sandbox.desktop.displayInfo();
console.log("Display:", display);

await sandbox.desktop.movePointer(
Math.floor(display.width / 2),
Math.floor(display.height / 2),
);
await sandbox.desktop.click({ button: 1 });

const screenshot = await sandbox.desktop.screenshot();
await writeFile("desktop-screenshot.png", screenshot);
console.log("Saved screenshot to desktop-screenshot.png");
} finally {
await sandbox.delete();
}
await client.close()
} finally {
await client.close();
}
}

void main()
void main();
53 changes: 31 additions & 22 deletions examples/filesystem_and_git.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
import { Leap0Client } from "../src/index.js"
import { Leap0Client } from "../src/index.js";

async function main(): Promise<void> {
const client = new Leap0Client()
const repoPath = "/workspace/hello-world"
const client = new Leap0Client();
const repoPath = "/workspace/hello-world";

try {
const sandbox = await client.sandboxes.create()
const sandbox = await client.sandboxes.create();

try {
const clone = await sandbox.git.clone("https://github.com/octocat/Hello-World.git", repoPath)
console.log("clone exit:", clone.exitCode)

const status = await sandbox.git.status(repoPath)
console.log("git status:\n", status.output)

await sandbox.filesystem.writeFile(`${repoPath}/sdk-demo.txt`, "Hello from the Leap0 JS SDK\n")
const exists = await sandbox.filesystem.exists(`${repoPath}/sdk-demo.txt`)
console.log("file exists:", exists.exists)

const fileInfo = await sandbox.filesystem.stat(`${repoPath}/README`)
console.log("readme size:", fileInfo.size)

const tree = await sandbox.filesystem.tree(repoPath, 2)
console.log("tree items:", tree.entries.map((entry) => entry.name))
const clone = await sandbox.git.clone({
url: "https://github.com/octocat/Hello-World.git",
path: repoPath,
});
console.log("clone exit:", clone.exitCode);

const status = await sandbox.git.status(repoPath);
console.log("git status:\n", status.output);

await sandbox.filesystem.writeFile(
`${repoPath}/sdk-demo.txt`,
"Hello from the Leap0 JS SDK\n",
);
const exists = await sandbox.filesystem.exists(`${repoPath}/sdk-demo.txt`);
console.log("file exists:", exists);

const fileInfo = await sandbox.filesystem.stat(`${repoPath}/README`);
console.log("readme size:", fileInfo.size);

const tree = await sandbox.filesystem.tree(repoPath, { maxDepth: 2 });
console.log(
"tree items:",
tree.items.map((entry: { name: string }) => entry.name),
);
} finally {
await sandbox.delete()
await sandbox.delete();
}
} finally {
await client.close()
await client.close();
}
}

void main()
void main();
Loading