Vendor & Publish Your Own
Own a branded companion - the daemon source vendors into your repo, one command publishes it to a public GitHub repo you own, and your users install and auto-update from your releases.
With companionSource: "vendored" the daemon is yours to build and publish: its source lives in your repo, you brand it, and you own its public release repo and cadence. This page is the full ownership runbook - what vendors in, the dev loop, and the one-command release.
On a shared-source project this page describes the OTHER mode - switch to vendored first.
What vendors into your project
Turning the companion on ships three things into your repo. You edit the identity, run from source, and publish - you never touch the daemon's internals.
| Path | What it is |
|---|---|
apps/companion | The daemon source - the terminal agent your users run. Branded per project; you rarely edit it. |
scripts/companion-export/ | The exporter (its workspace templates + publish.mjs), which turns the daemon source into a standalone public repo and cuts signed, attested releases. |
apps/companion/brand.json | The identity file - one JSON that names your daemon and points at your release repo. Every export token, install command, service label, and env var derives from it. |
The brand file
apps/companion/brand.json is the single source of the daemon's identity. init fills the derived fields from your companion name; pnpm companion:publish fills the repo URLs on its first run.
| Field | Meaning |
|---|---|
name | Display name your users see (e.g. "Acme Companion"). |
binary | The command users type and the installer writes to PATH (a kebab slug of name). |
scope | npm scope for the exported packages, @<binary>. |
serviceLabel | Reverse-DNS label for the always-on OS service (launchd / systemd / Scheduled Task). |
appDirName | The daemon's on-disk data + config folder name. |
envPrefix | Prefix for the daemon's environment variables (e.g. ACME_COMPANION). |
docsUrl | Docs link baked into the exported README. |
repoUrl | Your public release repo. Empty until your first publish creates it. |
installBase | Base URL of the latest release assets. Empty until your first publish. |
Re-pointing brand.json re-brands the whole export with no code change. A guard test keeps it in lockstep with the config.companion.daemon block your frontend reads.
The dev loop
Run the daemon from source against your local backend - no publish needed to develop and test.
http://localhost:3000/api).pnpm --filter companion cli pair --url http://localhost:3000/api, then approve the device in your browser.pnpm --filter companion dev runs the paired daemon from apps/companion/src. Dispatch a run from the Companions page and watch it execute locally.The honest costs
Shipping your own daemon means owning it. State this plainly to yourself before you enable it.
- The public repo and its releases are yours. Publishing creates a public GitHub repo under your account; you own its release cadence and its issues. Direct commits there are overwritten at the next export - the repo is generated from your project.
- Install weight lives in your repo, not the download.
pnpm installin your project (locally and in CI) pulls the coding-CLI engine's platform binaries intonode_modules(~212MB). That weight is in your repo installs. Your users' daemon download is a self-contained standalone bundle and carries none of it. - Driver churn is your duty. Coding-CLI SDKs move fast. The loop is:
generatesaas updatepulls the new daemon source, the update summary flags that the companion trees changed, you runpnpm companion:publish, and your users auto-update once the tag lands. - Signing is optional and paid. Releases build and install unsigned (with an OS caveat your users see); macOS signing + notarization and Windows signing are opt-in. See optional signing.
pnpm companion:publish is the one command that turns the vendored daemon in apps/companion into a public release your users can install. It exports the brand-parameterized daemon, verifies the export is self-contained, mirrors it into your own public GitHub repo, and tags the version. Releases build six platform targets with a SHA256SUMS manifest, a VERSION asset, and build-provenance attestations - and your users auto-update to each new tag.
Prerequisites
gh auth login. The command refuses to run without an authenticated gh.--allow-dirty to override the check.The command
pnpm companion:publish| Flag | Effect |
|---|---|
--owner <o> | GitHub owner for the release repo. Defaults to the last publish's owner, then your gh login. |
--repo <r> | Repo name. Defaults to a slug derived from brand.json. |
--version <v> | Version to publish. Defaults to the next patch after the last published version. |
--check | Local staleness check only - no network, no publish. See re-release cadence. |
--dry-run | Print every mutating command (repo create, push, tag) instead of running it. |
--allow-dirty | Skip the clean-working-tree preflight. |
--skip-verify | Skip companion:verify - the pushed repo is then unverified. Avoid. |
The command exports at the resolved version, runs companion:verify (unless --skip-verify), clones your release repo, mirrors the export in, commits, pushes, and tags vX.Y.Z. The release workflow in the public repo builds the six targets (darwin x64/arm64, linux x64/arm64, win32 x64/arm64) on push of the tag.
First run
The very first publish - no release state and no brand.repoUrl yet - creates the public repo and writes its URLs back into your project. It prompts before creating anything (a non-interactive shell refuses rather than creating a repo unattended).
After a first publish succeeds, do two things before it goes live:
repoUrl + installBase in apps/companion/brand.json and packages/config/src/index.ts. Commit both - your Companions page renders the install command from that config.Optional signing
Releases build and install unsigned with an OS caveat your users see (macOS Gatekeeper, Windows SmartScreen). Signing removes the warning and is opt-in. Set these as repository secrets on your public release repo; a fork without them simply skips signing.
| Purpose | Repository secrets |
|---|---|
| macOS code signing | MACOS_CERT_P12_BASE64, MACOS_CERT_PASSWORD, MACOS_SIGNING_IDENTITY |
| macOS notarization | APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID |
| Windows code signing | Configure via SignPath (free for OSS); unsigned until then, with the SmartScreen caveat. |
With the macOS secrets present, the release workflow codesigns the Mach-O binaries and notarizes each macOS leg automatically. Without them the release still ships - unsigned, with the caveat.
Windows notes
You likely develop on macOS or Linux, so the exported repo's CI is your only Windows gate. The public repo runs install / check-types / test on Windows on every push, and the release workflow builds the win32 targets. Watch those legs on your first release; if GitHub denies a runner label for your account, drop that leg and note it in the repo README. Windows releases stay unsigned until you wire up SignPath.
Tag rules
Never move a published tag. Before tagging, the command queries the release repo for an existing release object at vX.Y.Z; if one exists, it refuses and tells you to bump --version. A tag that already has a release is what your users' auto-updater trusts - moving it would ship different bytes under a version they already installed. If a release run fails before the publish step (nothing shipped), re-tagging the same version is fine; once a release object exists, it is frozen.
Re-release cadence
Coding-CLI SDKs move fast, so plan to re-release. The loop:
generatesaas update. When it changes the vendored companion trees, the update summary prints: "Companion: vendored daemon changed since your published vX - run pnpm companion:publish (your users auto-update once tagged)".pnpm companion:publish --check - it re-exports today's tree at the published version, fingerprints it, and reports whether it differs from what you shipped. This is the exact gate; the update notice is a cheap heuristic.pnpm companion:publish (a fresh patch version). Your users' daemons auto-update to the new tag on their own boxes.Because daemons auto-update by default, a re-release reaches unattended machines. Keep the wire additive - your deployed backend and your users' daemons update on independent schedules.
App-scoped installs need a re-publish. The desktop app sets a companion up for a user by passing --app-scoped to your installer - it installs the binary only and skips the service so the app can supervise the daemon itself. Installers published before this support do not understand the flag, so the app's automatic setup fails against them (users can still install manually with the one-liner). generatesaas update flags the scripts/companion-export/ change like any other, so re-publish with pnpm companion:publish to give your users an installer with app-scoped support.
Migrating from the universal OpenCompanion
Projects generated before v1.46 pointed users at the universal OpenCompanion at github.com/Duzbee/OpenCompanion. After generatesaas update pulls in apps/companion and this pipeline, run pnpm companion:publish to mint your own repo, then re-point your users at your install command. Existing OpenCompanion installs keep working and auto-updating until then, so nobody is stranded.
Point at a Shared Companion
Run the companion feature with zero daemon maintenance - point your project at an existing companion repo (OpenCompanion by default) and let your app render its install command.
Data Fetching
Call the Hono backend from Next.js with a typed RPC client across Server and Client Components.