GenerateSaaS

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.

PathWhat it is
apps/companionThe 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.jsonThe 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.

FieldMeaning
nameDisplay name your users see (e.g. "Acme Companion").
binaryThe command users type and the installer writes to PATH (a kebab slug of name).
scopenpm scope for the exported packages, @<binary>.
serviceLabelReverse-DNS label for the always-on OS service (launchd / systemd / Scheduled Task).
appDirNameThe daemon's on-disk data + config folder name.
envPrefixPrefix for the daemon's environment variables (e.g. ACME_COMPANION).
docsUrlDocs link baked into the exported README.
repoUrlYour public release repo. Empty until your first publish creates it.
installBaseBase 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.

Start your app locally so the API is reachable (e.g. http://localhost:3000/api).
Pair the daemon from source: pnpm --filter companion cli pair --url http://localhost:3000/api, then approve the device in your browser.
Serve it: 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 install in your project (locally and in CI) pulls the coding-CLI engine's platform binaries into node_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 update pulls the new daemon source, the update summary flags that the companion trees changed, you run pnpm 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

Install the GitHub CLI and authenticate: gh auth login. The command refuses to run without an authenticated gh.
Public-repo rights on the account that will own the release repo. The first publish creates a public repo; public matters because the free Linux + Windows ARM CI runners only apply to public repos.
A clean working tree (commit or stash first), or pass --allow-dirty to override the check.

The command

pnpm companion:publish
FlagEffect
--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.
--checkLocal staleness check only - no network, no publish. See re-release cadence.
--dry-runPrint every mutating command (repo create, push, tag) instead of running it.
--allow-dirtySkip the clean-working-tree preflight.
--skip-verifySkip 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:

Commit the write-back. The first run fills 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.
Redeploy your app. Until the deployed config carries the new URLs, the Companions page stays in its publish-first state and shows no install command.

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.

PurposeRepository secrets
macOS code signingMACOS_CERT_P12_BASE64, MACOS_CERT_PASSWORD, MACOS_SIGNING_IDENTITY
macOS notarizationAPPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID
Windows code signingConfigure 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:

Run 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)".
Confirm a re-release is actually needed with 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.
Re-publish with 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.

On this page