Skip to content

feat: publish the CLI to npm as @voiceflow/cli (COR-13588) - #8

Open
Bradenream wants to merge 4 commits into
braden/fix-local-build-gosum/COR-0from
braden/npm-distribution/COR-0
Open

feat: publish the CLI to npm as @voiceflow/cli (COR-13588)#8
Bradenream wants to merge 4 commits into
braden/fix-local-build-gosum/COR-0from
braden/npm-distribution/COR-0

Conversation

@Bradenream

@Bradenream Bradenream commented Aug 26, 2026

Copy link
Copy Markdown

Part of COR-13588 — shipping the vf CLI as an agent-era distribution channel.

What

Hand-written (non-generated) npm distribution layer so npx @voiceflow/cli works cold — no postinstall scripts, no compilation, ~2–4s cold start:

  • npm/cli/ — wrapper package exposing vf and voiceflow bins via a zero-dependency shim that resolves the platform binary package (@voiceflow/cli-<os>-<cpu>, optionalDependencies with os/cpu fields — npm downloads only the matching one, ~5.6MB) and hands over stdio, exit codes, and signals
  • npm/scripts/prepare.ts — stages all 7 packages from goreleaser's artifacts.json, stamping the release version and restoring the executable bit the Actions artifact zip round-trip drops
  • npm/scripts/publish.ts — idempotent publish: platform packages first, registry-visibility gate, wrapper last; prereleases go to the next dist-tag so latest never resolves an rc
  • npm/README.md — pipeline invariants + failure recovery
  • CI — one job appended to the existing tag-triggered release workflow; versions stay in lockstep with git tags by construction

Static CGO-free binaries → one linux package per arch, no musl split. Base branch: #7 (needed to build locally).

Human steps before first publish

  1. npm org owner creates a granular NPM_TOKEN (scope @voiceflow, read+write, may-create-packages) → repo Actions secret
  2. Dry-run with a prerelease tag (publishes under next), verify on macOS/Linux/Windows
  3. Real tag → @voiceflow/cli live; then configure npm trusted publishing and retire the token

Verification

  • 7/7 hermetic vitest tests (test/npm-distribution.test.ts): staging layout, version stamping, exec bits, npx bin invariant, publish ordering + dist-tags, shim exec/error paths
  • Full local e2e: real binary → prepare → npm pack → install into a scratch prefix → both bins execute the CLI in 27ms, exit codes propagate, unsupported-platform error teaches
  • release.yaml YAML-validated; existing goreleaser job byte-identical

Adds a hand-written (non-generated) npm distribution layer so
'npx @voiceflow/cli' works cold with no postinstall scripts:

- npm/cli: wrapper package exposing 'vf' and 'voiceflow' bins via a
  zero-dependency shim that resolves the platform binary package
  (@voiceflow/cli-<os>-<cpu>, optionalDependencies with os/cpu fields)
  and hands over stdio, exit codes, and signals
- npm/scripts/prepare.ts: stages all 7 packages from goreleaser's
  artifacts.json, stamping the release version and restoring the
  executable bit the Actions artifact zip round-trip drops
- npm/scripts/publish.ts: idempotent publish — platform packages first,
  registry-visibility gate, wrapper last; prereleases go to the 'next'
  dist-tag so latest never resolves an rc
- npm/README.md: pipeline invariants and failure recovery
- test/npm-distribution.test.ts: hermetic coverage of staging, ordering,
  version stamping, exec bits, and the shim's exec/error paths
Appends an npm-publish job to the existing tag-triggered release
workflow: downloads the goreleaser dist artifact, stages the seven npm
packages, smoke-tests a staged binary, and publishes idempotently with
provenance. Requires the NPM_TOKEN repo secret (human step).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a hand-written npm distribution layer so the Voiceflow CLI can be executed via npx @voiceflow/cli by publishing a thin wrapper package plus six platform-specific binary packages, and wires this into the tag-triggered release workflow.

Changes:

  • Introduces npm/scripts/prepare.ts to stage the wrapper + platform binary packages from dist/artifacts.json, stamping versions and restoring executable bits.
  • Introduces npm/scripts/publish.ts to publish packages idempotently with ordering/visibility gating and prerelease dist-tag routing.
  • Adds a Node bin shim (npm/cli/bin/vf.js), documentation, hermetic-ish tests, and a new npm-publish job in the release workflow.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/npm-distribution.test.ts Adds tests for staging, publish planning, and the wrapper shim behavior.
npm/scripts/prepare.ts Stages all npm packages from goreleaser output and stamps versions/modes.
npm/scripts/publish.ts Publishes staged packages with ordering and prerelease dist-tag logic.
npm/README.md Documents pipeline invariants and failure recovery for npm publishing.
npm/cli/README.md End-user docs for installing/running the npm-distributed CLI.
npm/cli/package.json Wrapper package definition with bins + optionalDependencies for platforms.
npm/cli/bin/vf.js Zero-dependency launcher that resolves and spawns the platform binary.
.github/workflows/release.yaml Adds an npm-publish job after goreleaser on tag pushes.
Suppressed comments (2)

test/npm-distribution.test.ts:113

  • This test depends on side effects from a previous it(...) (the out-ok staging). If tests are filtered (e.g. vitest -t 'publish.ts') or the earlier test fails, this one will fail for unrelated reasons. Stage a fresh outDir within this test (or via a beforeAll in this describe) to keep it isolated.
  it('plans platform packages before the wrapper and never invokes a real publish', async () => {
    const outDir = path.join(workDir, 'out-ok'); // staged by the first test
    const result = await execa({ reject: false })(TSX, [PUBLISH, '--version', '0.229.0', '--out', outDir, '--dry-run']);
    expect(result.exitCode, result.stderr).toBe(0);

test/npm-distribution.test.ts:15

  • This hard-codes the tsx binary path to node_modules/.bin/tsx. On Windows, Yarn will typically create tsx.cmd (and tsx.ps1) instead, and spawning an explicit path without the .cmd extension can fail. Consider selecting the correct filename based on platform so the tests are runnable on Windows too.
const TSX = path.join(REPO_ROOT, 'node_modules', '.bin', 'tsx');

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/npm-distribution.test.ts
Comment thread npm/scripts/publish.ts Outdated
- prepare now FAILS when the repo has no LICENSE instead of warning. Every
  package.json declares Apache-2.0; staging without the license text made
  it possible to permanently publish a bare license claim (versions cannot
  be unpublished) if a tag landed before the LICENSE PR.
- Both scripts converted from .ts to dependency-free .mjs run by bare node.
  The publish step holds NPM_TOKEN, and 'npx tsx' had npm resolve tsx's
  transitive tree from the registry at run time with no lockfile — arbitrary
  registry code executing in the token-bearing step. Both scripts only ever
  used node builtins, so the runner bought nothing.
- latest now only moves forward: publish reads the registry's current latest
  and tags a non-newer version 'previous' instead. The documented recovery
  ('re-run the job') could otherwise demote what npx installs when a newer
  release had already shipped.
- Each staged package embeds the LICENSE file.
- Tests: goreleaser fixtures now use cwd-relative dist/-prefixed paths (the
  branch CI actually takes, previously untested), npm is stubbed on PATH so
  dist-tag decisions are hermetic instead of depending on live registry
  state, plus coverage for the license gate, latest-guard, partial-publish
  skip, and shim signal propagation.
@linear-code

linear-code Bot commented Aug 26, 2026

Copy link
Copy Markdown

COR-13587

COR-13588

@Bradenream Bradenream changed the title feat: publish the CLI to npm as @voiceflow/cli feat: publish the CLI to npm as @voiceflow/cli (COR-13588) Aug 26, 2026
npx prompts 'Ok to proceed?' when a TTY is present and the package is
not already cached, which stalls unattended agent runs. This README is
the one npm renders on the package page, so it is the copy most people
and agents will paste.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants