Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
148 changes: 148 additions & 0 deletions .github/workflows/publish-node.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: Publish Node Package

on:
push:
tags:
- "v*"
workflow_dispatch:

permissions:
contents: read

env:
CARGO_INCREMENTAL: "0"
NODE_VERSION: "24"

jobs:
build-native:
name: Build Node native binding on ${{ matrix.node_target }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
rust_target: x86_64-unknown-linux-gnu
node_target: linux-x64-gnu
smoke: true
- os: ubuntu-24.04
rust_target: aarch64-unknown-linux-gnu
node_target: linux-arm64-gnu
smoke: false
- os: macos-latest
rust_target: aarch64-apple-darwin
node_target: darwin-arm64
smoke: true
- os: macos-15-intel
rust_target: x86_64-apple-darwin
node_target: darwin-x64
smoke: true

steps:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@c93f4f9c67595668add93d3d6895795ce52d8c2d # 1.85.0
with:
targets: ${{ matrix.rust_target }}

- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false

- name: Install Linux arm64 build tools
if: matrix.rust_target == 'aarch64-unknown-linux-gnu'
timeout-minutes: 15
run: |
set -euo pipefail
sudo apt-get update \
-o Acquire::Retries=5 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30
sudo apt-get install -y --no-install-recommends \
-o Acquire::Retries=5 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30 \
gcc-aarch64-linux-gnu libc6-dev-arm64-cross

- name: Install TypeScript SDK dependencies
working-directory: bindings/node
run: npm ci

- name: Build native binding
working-directory: bindings/node
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
run: npm run build -- --target ${{ matrix.rust_target }}

- name: Smoke-test native binding
if: matrix.smoke
working-directory: bindings/node
run: node -e "const { doctor } = require('./index.js'); const report = doctor(); if (!report.storageBackend) process.exit(1)"

- name: Upload native binding artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: smolfs-${{ matrix.node_target }}
path: bindings/node/smolfs.${{ matrix.node_target }}.node
if-no-files-found: error

publish:
name: Publish to npm
runs-on: ubuntu-24.04
needs: build-native
if: startsWith(github.ref, 'refs/tags/v')
timeout-minutes: 10
environment: npm
permissions:
contents: read
id-token: write

steps:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: "https://registry.npmjs.org"
package-manager-cache: false

- name: Check release tag matches package versions
run: |
set -euo pipefail
tag_version="${GITHUB_REF_NAME#v}"
cargo_version=$(awk -F '"' '/^version = / { print $2; exit }' Cargo.toml)
node_version=$(node -p "require('./bindings/node/package.json').version")
if [ "$tag_version" != "$cargo_version" ] || [ "$tag_version" != "$node_version" ]; then
echo "Release tag v$tag_version does not match Cargo version $cargo_version and Node package version $node_version"
exit 1
fi

- name: Download native binding artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: bindings/node
merge-multiple: true

- name: Install TypeScript SDK dependencies
working-directory: bindings/node
run: npm ci

- name: Verify npm package contents
working-directory: bindings/node
run: |
set -euo pipefail
for target in darwin-arm64 darwin-x64 linux-arm64-gnu linux-x64-gnu; do
test -f "smolfs.$target.node"
done
npm pack --dry-run --json

- name: Publish package
working-directory: bindings/node
run: npm publish --access public
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,14 @@ SmolFS.
## TypeScript SDK

The TypeScript package is a native Node.js binding over the same Rust core. The
npm package is not published yet; for now, use the local checkout flow below.
npm package ships prebuilt native bindings for Linux and macOS on x86_64 and
arm64.

Install it with npm:

```bash
npm install @celestoai/smolfs
```

For local development from this checkout, use Node.js 18 or newer:

Expand Down Expand Up @@ -472,13 +479,28 @@ Python packaging uses `uv` and `maturin`. The GitHub workflow at
`.github/workflows/publish-python.yml` builds wheels for Linux and macOS, builds
an sdist, and publishes to PyPI when a `v*` tag is pushed.

Node.js packaging uses `npm` and `napi-rs`. The GitHub workflow at
`.github/workflows/publish-node.yml` builds native TypeScript SDK bindings for
Linux and macOS, checks the npm package contents, and publishes to npm when a
`v*` tag is pushed.

Before the first Python release, configure PyPI Trusted Publishing:

1. Create or claim the `smolfs` project on PyPI.
2. Add a trusted publisher for repository `CelestoAI/smolfs`.
3. Set the workflow name to `publish-python.yml`.
4. Set the environment name to `pypi`.

Before the first npm release, configure npm trusted publishing for
`@celestoai/smolfs`:

1. Add a trusted publisher for GitHub Actions.
2. Set the organization or user to `CelestoAI`.
3. Set the repository to `smolfs`.
4. Set the workflow filename to `publish-node.yml`.
5. Set the environment name to `npm`.
6. Allow `npm publish`.

Release:

```bash
Expand All @@ -488,7 +510,6 @@ git push origin v0.1.0

## Roadmap

- Add npm publishing with prebuilt TypeScript SDK native artifacts.
- Add type stubs for the Python package.
- Add a Linux CI job that mounts a local dev volume when mount support is
available.
Expand Down
95 changes: 95 additions & 0 deletions bindings/node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# SmolFS TypeScript SDK

SmolFS gives TypeScript agents a workspace folder that can survive after the
agent process stops. You can create a volume, mount it like a normal directory,
write files into it, flush important changes, unmount it, and mount the same
files again later.

The npm package is a native Node.js SDK over the same Rust core as the `smolfs`
command. It is useful when an agent runner or automation service wants to manage
workspace volumes from TypeScript without shelling out for every operation.

## Install

Install the SDK from npm:

```bash
npm install @celestoai/smolfs
```

Mounting volumes also needs SmolFS' managed storage backend on the machine. The
installer sets up both the CLI and backend:

```bash
curl -fsSL https://raw.githubusercontent.com/CelestoAI/smolfs/main/scripts/install.sh | sh
```

## Quickstart

Start with a local development volume:

```ts
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { SmolFS, doctor } from "@celestoai/smolfs";

const report = doctor();
if (!report.storageBackend.found || !report.mountSupport.found) {
throw new Error(`SmolFS is not ready: ${JSON.stringify(report)}`);
}

const fs = SmolFS.fromEnv();
const volume = fs.ensureVolume({ name: "demo", dev: true });
const mount = fs.mount({ name: volume.name, path: "./workspace" });

mkdirSync(mount.mountpoint, { recursive: true });
writeFileSync(join(mount.mountpoint, "hello.txt"), "hello from SmolFS\n");

try {
fs.flush(volume.name);
} finally {
fs.unmount(volume.name);
}
```

`dev: true` creates a local-only volume. It is the easiest way to test SmolFS on
a single machine before connecting shared metadata and object storage.

## Cloud Volumes

Cloud volumes use the same API with explicit metadata and object storage
settings:

```ts
import { SmolFS } from "@celestoai/smolfs";

const fs = SmolFS.fromEnv();
fs.ensureVolume({
name: "agent-workspace",
metadata: "redis://localhost:6379/1",
storage: "s3",
bucket: "https://my-bucket.s3.us-east-2.amazonaws.com",
});
```

Keep storage credentials in the environment used by SmolFS. Do not print them in
logs or store them in source files.

## API Overview

- `doctor()` checks whether the machine can create and mount volumes.
- `SmolFS.fromEnv()` creates a client using `SMOLFS_HOME` and the current
environment.
- `ensureVolume(...)` creates a volume if it does not exist and returns the
existing volume if it does.
- `init(...)` creates a new volume.
- `mount({ name, path })` mounts a volume at a local directory.
- `flush(name)` asks SmolFS to sync important writes.
- `unmount(name, { force })` unmounts a mounted volume.
- `status(name?)` lists known volumes and mountpoints.

## Links

- Repository: https://github.com/CelestoAI/smolfs
- Issues: https://github.com/CelestoAI/smolfs/issues
- CLI installer: https://raw.githubusercontent.com/CelestoAI/smolfs/main/scripts/install.sh
8 changes: 6 additions & 2 deletions bindings/node/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"name": "@celestoai/smolfs",
"version": "0.1.0",
"description": "TypeScript bindings for SmolFS",
"private": true,
"description": "Durable workspace folders for TypeScript agents",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand All @@ -17,8 +16,13 @@
"agents",
"filesystem",
"object-storage",
"persistent-workspaces",
"smolfs"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"main": "index.js",
"types": "index.d.ts",
"files": [
Expand Down
Loading