Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,36 @@ jobs:
dist/
!dist/*.txt
retention-days: 30

npm-publish:
needs: goreleaser
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # npm --provenance (Sigstore via GitHub OIDC)
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org

- name: Download goreleaser dist
uses: actions/download-artifact@v4
with:
name: release-artifacts
path: dist

- name: Assemble npm packages
run: node npm/scripts/prepare.mjs --version "${GITHUB_REF_NAME#v}" --dist dist --out dist/npm

- name: Smoke test staged linux-x64 binary
run: dist/npm/cli-linux-x64/bin/vf version

- name: Publish (idempotent)
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: node npm/scripts/publish.mjs --version "${GITHUB_REF_NAME#v}" --out dist/npm
80 changes: 80 additions & 0 deletions npm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# npm distribution pipeline

This directory publishes the CLI to npm as `@voiceflow/cli` plus six platform
binary packages, so `npx @voiceflow/cli` works cold with no postinstall
scripts and no compilation. It is entirely hand-written — nothing here is
Speakeasy-generated.

## How it works

- `cli/` is the wrapper package, checked in verbatim at version `0.0.0`.
Its `bin/vf.js` shim resolves `@voiceflow/cli-<os>-<cpu>` (installed via
`optionalDependencies` with `os`/`cpu` fields, so npm downloads only the
matching platform) and execs the Go binary.
- `scripts/prepare.mjs` stages all seven packages into `dist/npm/` from
goreleaser's `dist/artifacts.json`, stamping the release version everywhere
and embedding the repo's LICENSE in each package.
- `scripts/publish.mjs` publishes idempotently: platform packages first, a
registry-visibility gate, wrapper last. Re-running after a partial failure
skips what already went out.
- Both scripts are dependency-free ESM run by bare `node`. That is deliberate:
the publish step holds `NPM_TOKEN`, and a TypeScript runner would have npm
resolve its transitive dependency tree from the registry at run time — with
no lockfile — inside the token-bearing step. Keep them on node builtins.
- The `npm-publish` job in `.github/workflows/release.yaml` runs both after
goreleaser on every `v*` tag. Versions are in lockstep with git tags by
construction (`GITHUB_REF_NAME`).

## Invariants (breaking any of these breaks users)

1. **Every `bin` entry in `cli/package.json` must point at the same file.**
npx only resolves `npx @voiceflow/cli` when all bin values are identical.
`prepare.ts` enforces this at staging time.
2. **Platform binaries must be `chmod 755` at staging.** The Actions artifact
zip round-trip drops the executable bit, and npm records file modes from
disk into the tarball. `prepare.ts` does this; the workflow smoke-tests a
staged binary to catch regressions.
3. **Wrapper pins platform packages with exact versions** — never ranges.
4. **Platform packages carry no `bin`, no `exports`, no `main`.** An exports
map would block the shim's `require.resolve` of the `/bin/vf` subpath.
5. **One linux package per arch, no musl split.** The Go binaries are
CGO-free static builds; the same package works on glibc and Alpine.
6. **Never republish or unpublish a version.** Recovery is always: fix, bump
patch, tag again. A bad release gets `npm deprecate`, not `npm unpublish`.
7. **Prerelease versions (containing `-`) publish under the `next` dist-tag**
so `latest` never resolves an rc.
8. **`latest` only moves forward.** Before tagging, `publish.mjs` reads the
registry's current `latest`; a version that is not newer publishes under
the `previous` tag instead. Without this, a recovery re-run of an older
version would silently downgrade what `npx @voiceflow/cli` installs.
9. **A publish without a LICENSE file is impossible.** Every package.json
declares Apache-2.0, so `prepare.mjs` exits 1 when the repo root has no
LICENSE rather than shipping a license claim with no license text — a
mislabeled version cannot be taken back. goreleaser marks them prereleases too.

## Failure recovery

- **Partial publish** (some packages live, job died): re-run the
`npm-publish` job from the Actions UI. Existence checks make it a no-op
for published packages; the wrapper only goes out after all six platforms
are visible. Safe even if a newer version shipped in the meantime —
invariant 8 keeps the recovered older version off the `latest` tag.
- **Artifact expired** (>30 days): do not rebuild-and-republish the same
version — rebuilt binaries would not match the GitHub release. Bump patch,
tag again.
- **Speakeasy regeneration**: `release.yaml` is generation-tracked with
persistent edits enabled; the appended `npm-publish` job is expected to
survive (precedent: the goreleaser naming fix did). If a regeneration ever
drops it, move the job unchanged into its own `workflow_run`-triggered
workflow file (needs `actions: read` and `run-id` on download-artifact).

## First-time setup (human steps)

1. npm org owner creates a granular access token: scope `@voiceflow`,
read+write, allowed to create new packages, bypass-2FA if the org
enforces publish-2FA. Add it as the `NPM_TOKEN` Actions secret.
2. Dry-run with a prerelease tag (e.g. `v0.NNN.0-rc.1`) — publishes under
`next`. Verify `npx @voiceflow/cli@next version` on macOS/Linux/Windows.
3. Cut the real tag.
4. After first publish, configure npm trusted publishing (tokenless OIDC)
for all seven packages on npmjs.com and retire `NPM_TOKEN`.
49 changes: 49 additions & 0 deletions npm/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Voiceflow CLI

`vf` is the command line for [Voiceflow](https://www.voiceflow.com), the AI agent platform for customer experience automation. Manage agents, knowledge bases, workflows, tests, and transcripts from your terminal or CI.

## Install

```bash
# Run without installing
npx -y @voiceflow/cli --help

# Or install globally (provides both `vf` and `voiceflow`)
npm install -g @voiceflow/cli
```

This package installs a prebuilt binary for your platform via an optional dependency (`@voiceflow/cli-<os>-<cpu>`). No postinstall scripts, no compilation.

## Authenticate

Create a personal access token in Voiceflow under **Settings → Access tokens** (tokens start with `vfp_`), then:

```bash
export VF_TOKEN=vfp_...
```

Every command also accepts `--token`.

## First conversation in four commands

```bash
vf workspace list --output-format json # -> .workspaces[].id
vf project create --name "My Agent" --type webchat \
--workspace-id $WORKSPACE_ID --output-format json # -> .project.id
vf conversation send --user-id demo-user --project-id $PROJECT_ID \
--environment-alias main --version-param draft \
--action '{"type":"launch"}' --output-format json
vf conversation send --user-id demo-user --project-id $PROJECT_ID \
--environment-alias main --version-param draft \
--action '{"type":"text","payload":"What can you do?"}' --output-format json
```

## Docs

- CLI documentation: https://www.voiceflow.com/docs/cli/overview
- Authentication: https://www.voiceflow.com/docs/api-reference/authentication
- Source and prebuilt binaries: https://github.com/voiceflow/cli

## Supported platforms

macOS (arm64, x64), Linux (arm64, x64 — glibc and musl), Windows (arm64, x64). Other platforms can use the binaries on the [releases page](https://github.com/voiceflow/cli/releases) or `go install github.com/voiceflow/cli/cmd/vf@latest`.
76 changes: 76 additions & 0 deletions npm/cli/bin/vf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env node
'use strict';
// Launcher for the Voiceflow CLI (vf).
//
// The real CLI is a Go binary shipped in a platform-specific package
// (@voiceflow/cli-<os>-<cpu>) installed as an optionalDependency of
// @voiceflow/cli. This shim resolves the right binary and hands over.
//
// INVARIANT: every entry in this package's "bin" map must point at this
// same file — npx only resolves `npx @voiceflow/cli` when all bin values
// are identical (npm/libnpmexec checks that before the name match).

const { spawn } = require('node:child_process');

const platformKey = `${process.platform}-${process.arch}`;
const SUPPORTED_PLATFORMS = [
'darwin-arm64',
'darwin-x64',
'linux-arm64',
'linux-x64',
'win32-arm64',
'win32-x64',
];

function fail(lines) {
console.error(lines.join('\n'));
process.exit(1);
}

if (!SUPPORTED_PLATFORMS.includes(platformKey)) {
fail([
`The Voiceflow CLI does not ship a prebuilt binary for your platform (${platformKey}).`,
`Supported platforms: ${SUPPORTED_PLATFORMS.join(', ')}.`,
'Binaries for other platforms may be available at:',
' https://github.com/voiceflow/cli/releases',
]);
}

const executableName = process.platform === 'win32' ? 'vf.exe' : 'vf';
let binaryPath;
try {
binaryPath = require.resolve(`@voiceflow/cli-${platformKey}/bin/${executableName}`);
} catch {
fail([
`Could not find the Voiceflow CLI binary package "@voiceflow/cli-${platformKey}".`,
'It is installed automatically as an optionalDependency of @voiceflow/cli.',
'This usually means optional dependencies were skipped (npm --omit=optional,',
'yarn --ignore-optional) or the lockfile was created on a different platform.',
'Reinstall without those flags, or download a binary directly:',
' https://github.com/voiceflow/cli/releases',
]);
}

const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' });

// Terminal-generated SIGINT reaches the child directly through the shared
// process group, so the shim only ignores it and waits. SIGTERM/SIGHUP are
// delivered to the shim alone, so those are forwarded.
process.on('SIGINT', () => {});
for (const signal of ['SIGTERM', 'SIGHUP']) {
process.on(signal, () => child.kill(signal));
}

child.on('error', (error) => {
fail([`Failed to start ${binaryPath}: ${error.message}`]);
});

child.on('close', (code, signal) => {
if (signal) {
// Die by the same signal so callers observe the real termination reason.
process.removeAllListeners(signal);
process.kill(process.pid, signal);
} else {
process.exit(code ?? 1);
}
});
46 changes: 46 additions & 0 deletions npm/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@voiceflow/cli",
"version": "0.0.0",
"description": "Voiceflow CLI (vf) — command line for Voiceflow, the AI agent platform for customer experience automation. Manage agents, knowledge bases, workflows, tests, and transcripts from your terminal or CI.",
"keywords": [
"voiceflow",
"vf",
"cli",
"ai",
"ai-agents",
"agents",
"chatbot",
"voice",
"conversational-ai",
"customer-experience",
"automation",
"llm"
],
"license": "Apache-2.0",
"homepage": "https://www.voiceflow.com/docs/cli/overview",
"repository": {
"type": "git",
"url": "git+https://github.com/voiceflow/cli.git"
},
"bugs": {
"url": "https://github.com/voiceflow/cli/issues"
},
"bin": {
"vf": "bin/vf.js",
"voiceflow": "bin/vf.js"
},
"files": [
"bin/"
],
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@voiceflow/cli-darwin-arm64": "0.0.0",
"@voiceflow/cli-darwin-x64": "0.0.0",
"@voiceflow/cli-linux-arm64": "0.0.0",
"@voiceflow/cli-linux-x64": "0.0.0",
"@voiceflow/cli-win32-arm64": "0.0.0",
"@voiceflow/cli-win32-x64": "0.0.0"
}
}
Loading