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 Bare app wires peer-to-peer updates and replication through a worker—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 Pear CLI v3 is built on this same architecture: a Bare standalone executable that updates itself peer-to-peer through pear-runtime.

New to peer-to-peer? Peer-to-peer, demystified explains how peers find each other and connect directly—no servers—and The Pear stack maps the pieces you'll wire together.

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.

Need the pear CLI? Install it from install.pears.com, or prefix any command below with npx. See Install & upgrade for details.

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.mjs --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.mjsThe entrypoint—parses CLI flags, resolves the storage path, constructs the App, and logs its updater events.Yes—your startup and CLI.
app.jsThe App class (a ready-resource). Spawns the Bare worker with PearRuntime.run, wraps its IPC in a FramedStream, and turns updater messages into events.Sometimes—app lifecycle.
workers/main.jsThe Bare worker that owns the peer-to-peer code and the pear-runtime updater.Yes—this is your backend.
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.

Like the Electron template, the peer-to-peer logic lives in a Bare worker; bin.mjs and app.js are the host that spawns it and drives updates. There's no renderer or preload bridge—your "frontend" is the terminal.

Upstream ships the worker as the hello-pear-worker package, so the template's workers/main.js is just require('hello-pear-worker'). That is where your peer-to-peer code goes—open Corestore cores, join Hyperswarm topics, and run your protocol.

How it's wired

The template is three layers: bin.mjs (entry + CLI), app.js (the host that runs the worker and updater), and the Bare worker that holds your peer-to-peer code.

bin.mjs is the Bare entry process. It parses three flags with paparam--version, --storage, and --no-updates—then constructs the App:

bin.mjs
const cmd = command(
  appName,
  summary(pkg.description),
  flag('--version|-v', 'Print the current version'),
  flag('--storage <dir>', 'custom storage directory'),
  flag('--no-updates', 'disable OTA updates for this run')
)

It resolves a storage directory, then hands the runtime configuration to a new App(...). 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.mjs
const updates = cmd.flags.updates
const storage = cmd.flags.storage || (isDev ? null : path.join(persistent(), appName))
const dir = storage || path.join(os.tmpdir(), 'pear', appName)

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

const app = new App({
  dir,
  app: isDev ? null : os.execPath(),
  updates,
  version: pkg.version,
  upgrade: pkg.upgrade,
  name: isWindows ? appName + '.exe' : appName
})

App (in app.js) is a ready-resource. When it opens, it spawns the Bare worker with PearRuntime.run—passing the runtime config as arguments—and wraps the worker's IPC pipe in a FramedStream:

app.js
  _open() {
    this.IPC = PearRuntime.run(require.resolve('./workers/main.js'), [
      String(this.updates),
      this.version,
      this.upgrade,
      this.name,
      this.dir,
      this.app || ''
    ])
    this.pipe = new FramedStream(this.IPC)

    this.pipe.on('data', (data) => this._onmessage(data))

The worker owns the peer-to-peer code and the pear-runtime updater. It messages the host, and App turns those into updating, updated, and update-applied events—which bin.mjs logs—applying each downloaded release automatically. SIGHUP, SIGINT, SIGQUIT, and SIGTERM handlers call app.exit() to close cleanly. For the model behind these updates, see Pear OTA:

bin.mjs
app.on('message', (message) => console.log(message))
app.on('updating', () => console.log('[updater] getting new update'))
app.on('updating-delta', (delta) => console.log('[updater]', delta))
app.on('updated', () => console.log('[updater] update complete... applying'))
app.on('update-applied', () =>
  console.log('[updater] applied update, restart to run latest version')
)
app.on('error', (err) => console.error('[app:error]', err))

process.on('SIGHUP', () => app.exit(129))
process.on('SIGINT', () => app.exit(130))
process.on('SIGQUIT', () => app.exit(131))
process.on('SIGTERM', () => app.exit(143))
PearRuntime.run replicate update drive bin.mjs (entry) app.js (App) workers/main.js (Bare worker) pear-runtime updater Corestore Hyperswarm seeding peers

Build a standalone binary

bare-build compiles bin.mjs 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:

  • macOS: darwin-arm64, darwin-x64
  • Linux: linux-arm64, linux-x64
  • Windows: win32-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 pear install command:

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; bin.mjs holds the CLI and startup logic (your peer-to-peer code lives in the worker). 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,
   summary(pkg.description),
   flag('--version|-v', 'Print the current version'),
   flag('--storage <dir>', 'custom storage directory'),
-  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(`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. That is the CLI layer in bin.mjs; your real peer-to-peer features go in the worker (workers/main.js):

Reuse the storage directory the host passes to the worker 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.mjs 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