LogoPear Docs

Start from the hello-pear-bare template

An alternative getting started path for terminal apps: clone the hello-pear-bare boilerplate and learn how a standalone Bare process wires pear-runtime over-the-air updates, Corestore, and Hyperswarm—then build it into a cross-platform binary.

This is the terminal counterpart to Start from the hello-pear-electron template. Instead of a desktop window, you start from a finished command-line boilerplate and learn how a single Bare process wires peer-to-peer updates and replication—then ship it as a standalone binary.

holepunchto/hello-pear-bare is Holepunch's official boilerplate for peer-to-peer terminal applications. Where the Electron template embeds pear-runtime into Electron, this template embeds it into Bare, the zero-core embeddable JavaScript runtime. The application and runtime compile into a single standalone executable per OS and architecture with no peer dependencies—no Node.js, and no Bare or Pear CLI required on the user's machine.

With this shape you can build CLIs, REPLs, TUIs, services and daemons, or transport hooks—anything headless—and ship it with peer-to-peer over-the-air (OTA) updates. The upcoming Pear CLI v3 is built on this same architecture: a Bare standalone executable that updates itself peer-to-peer through pear-runtime.

Take this path when you want to distribute a terminal app as a single executable and update it over the air. If you want to build a desktop app instead, follow the hello-pear-electron template.

Clone and run

1. Clone the repository

Clone the repository and install dependencies with the following commands:

git clone https://github.com/holepunchto/hello-pear-bare
cd hello-pear-bare
npm install

The template ships with a placeholder upgrade link in package.json. Until you replace it, startup fails with INVALID_URL. Create a real link with pear touch:

pear touch

This prints a link, for example: pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o.

Set the upgrade field in package.json to the link you just created:

"upgrade": "pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o"

4. Run the app

npm start runs bare bin.js --no-updates: the process starts in development mode with over-the-air updates disabled, so a live release never swaps the binary while you work.

npm start

To exercise the updater locally, opt back in:

npm start -- --updates

Map the template

PathWhat it isDo you edit it?
bin.jsThe entrypoint—CLI flag parsing, the storage path, and the Corestore + Hyperswarm + pear-runtime updater wiring.Yes—this is your app.
package.jsonApp metadata, scripts, the upgrade link, and the per-platform make:* build targets.Yes—branding and release link.
scripts/make.jsDetects the host OS and architecture and runs the matching make: target.Rarely.
test/index.jsThe brittle test entry, run by npm test.Yes—your tests.

Unlike the Electron template, there's no renderer, preload bridge, or separate worker. A single Bare process in bin.js owns everything.

How it's wired

bin.js runs as a single Bare process. It parses two flags with paparam:

bin.js
const cmd = command(
  appName,
  flag('--storage <dir>', 'custom storage directory for pear-runtime'),
  flag('--no-updates', 'disable OTA updates for this run')
)

Then it picks a storage directory, opens a Corestore and a Hyperswarm, and constructs the pear-runtime instance from the upgrade link and version in package.json. In development (launched with bare) storage is a temporary directory; a packaged binary uses the persistent per-app directory from bare-storage, and --storage overrides both—see Storage and distribution:

bin.js
const updates = cmd.flags.updates
const isDev = path.basename(Bare.argv[0] || '').startsWith('bare')
const storage = cmd.flags.storage || (isDev ? null : path.join(storageAPI.persistent(), appName))
const dir = storage || path.join(os.tmpdir(), 'pear', appName)
const store = new Corestore(path.join(dir, 'pear-runtime', 'corestore'))
const swarm = new Hyperswarm()

console.log(`${appName} v${pkg.version}`)
console.log(`Updates: ${updates === false ? 'disabled' : 'enabled'}`)

function getRunningAppPath() {
  if (isDev) return null
  return os.execPath()
}

const pear = new PearRuntime({
  dir,
  app: getRunningAppPath(),
  updates,
  version: pkg.version,
  upgrade: pkg.upgrade,
  name: isWindows ? appName + '.exe' : appName,
  store,
  swarm
})

When updates are enabled, it replicates the store on every swarm connection and joins the update drive's discovery key as a client to pull new releases:

bin.js
  swarm.on('connection', (connection) => store.replicate(connection))

  swarm.join(pear.updater.drive.core.discoveryKey, {
    client: true,
    server: false
  })

The updater runs on pear.updater: on an updated event it applies the downloaded release with applyUpdate(), and SIGHUP, SIGINT, SIGQUIT, and SIGTERM handlers close pear cleanly before exit. For the model behind these updates, see Runtime.

replicate update drive bin.js (Bare process) pear-runtime updater Corestore Hyperswarm seeding peers

Build a standalone binary

bare-build compiles bin.js and its dependencies into a single standalone executable with no peer dependencies—users don't need Node.js, Bare, or the Pear CLI installed. Build for your current host with:

npm run make

scripts/make.js detects your OS and architecture and runs the matching target, writing the binary to out/<platform>-<arch>. To build for a specific target—for example, in CI—call that target directly:

npm run make:darwin-arm64

The template ships a target for every supported platform:

  • macOSdarwin-arm64, darwin-x64
  • Linuxlinux-arm64, linux-x64
  • Windowswin32-arm64, win32-x64

Build each platform's binary on a matching host.

Install over the air

Distribute the executable through the usual channels—a download on your website, apt, or Homebrew. You can also install it peer-to-peer: once your upgrade link is seeding a release, pull it directly with the preview pear install command:

npx pear-install pear://<key>

After the first install, new releases reach users through the swarm—no reinstall needed. For how staging and seeding work, see Deploy your application and Installing applications.

Example: add a flag

The boilerplate is a minimal CLI plus updater, and bin.js is where your logic goes. Flags are parsed with paparam, so adding one is a two-line change. Add a --name flag to the command definition:

 const cmd = command(
   appName,
   flag('--storage <dir>', 'custom storage directory for pear-runtime'),
-  flag('--no-updates', 'disable OTA updates for this run')
+  flag('--no-updates', 'disable OTA updates for this run'),
+  flag('--name <name>', 'who to greet on startup')
 )

Then log the greeting after the other startup logs:

 console.log(`${appName} v${pkg.version}`)
 console.log(`Updates: ${updates === false ? 'disabled' : 'enabled'}`)
+
+if (cmd.flags.name) console.log(`Hello, ${cmd.flags.name}!`)

Run npm start -- --name YourName, and the process greets you on boot. From here, build real peer-to-peer features on the store and swarm the template already creates:

Reuse the same storage directory so your data lands alongside the updater's—see Storage and distribution.

Customize for your brand

Before you ship, set your identity in package.json:

bin.js reads productName || name for both the binary name and the persistent storage directory, so set these before your first release. Then build with npm run make and release with Deploy your application.

Where to go next

On this page