# The Nexafon CLI

> Set up an integration with nexafon init, check it with nexafon doctor, and give AI coding agents the Nexafon skill with nexafon skill add.

Source: https://docs.nexafone.so/v1/tools/cli/

`@nexafon/cli` does the setup steps of the [quickstart](/v1/quickstart/) in your app’s own idiom and checks the result.

:::note[Not published yet]
The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon
repository is public and tagged, so installing them by name fails for now. Until then,
[install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here
are the ones to use after release.
:::

`@nexafon/cli` is not on npm yet, so `npx @nexafon/cli` fails for now. Until it is published, build it in a checkout
of the Nexafon repo (after `task setup`) and run it from there; `--from` then installs the SDK packages from the same
checkout:

```sh
pnpm --filter @nexafon/cli build
node /path/to/nexafon/cli/dist/bin.js init --dir /path/to/your/app --from /path/to/nexafon
node /path/to/nexafon/cli/dist/bin.js doctor --dir /path/to/your/app
node /path/to/nexafon/cli/dist/bin.js skill add --dir /path/to/your/app
```

Or pack it once and run the tarball from your app’s root:

```sh
pnpm --filter @nexafon/cli pack --pack-destination /tmp      # in the checkout
npx --package /tmp/nexafon-cli-0.1.0.tgz nexafon doctor      # in your app
```

After the release the commands are:

```sh
npx @nexafon/cli init      # in your app's root
npx @nexafon/cli doctor
npx @nexafon/cli skill add
```

The CLI has no Nexafon address built in: pass them as flags or set `NEXAFON_API_URL` (https://api.nexafone.so),
`NEXAFON_APP_URL` (https://app.nexafone.so) and `NEXAFON_EMBED_CDN_URL` (https://app.nexafone.so/embed).

## nexafon init

Run it in your app’s root. It never writes a key: it leaves empty placeholders and tells you where to paste them.

1. **Detects the framework** from `package.json`, `composer.json` and the files around them: React (Vite), Next.js,
   Vue, Nuxt, Angular, Svelte, SvelteKit, plain HTML, Laravel, PHP or WordPress (`--framework` overrides it).
2. **Installs the packages** with your package manager (npm, pnpm, yarn or bun, from the lockfile), or Composer for
   PHP. With `--from` it builds and packs them from a checkout, or takes a folder of packed `.tgz` files; without it,
   it prints the command to use after release.
3. **Writes env placeholders**: `.env.local` for Vite and Next.js, `.env` for Nuxt, SvelteKit, Angular and Laravel,
   with the secret key never under a public prefix. It adds the file to `.gitignore` when git would commit it, and
   adds the names (keys left empty) to `.env.example`, creating it when your app has none.
4. **Scaffolds the token endpoint and the component**, and wires what it can safely change: the Vite plugin list (the
   endpoint runs in Vite’s dev and preview servers; the config imports `./server/nexafon-token.ts` with its
   extension, as Vite’s native config loader needs), Nuxt’s `runtimeConfig`, Laravel’s `routes/web.php`. It never
   replaces an existing file without `--force`.
5. **Sets the appearance from your brand colour**: it reads your Tailwind config, CSS custom properties (Tailwind v4
   `@theme`, shadcn’s `--primary`, `--brand`), Sass variables and theme files (MUI, Vuetify), converts HSL and OKLCH to
   hex, and passes it as `appearance.primaryColor` ([appearance](/v1/web/appearance/)). `--color #rrggbb` overrides
   it.
6. **Prints the next steps**, including the origin to add to the publishable key’s allowed websites and the embed
   domains: `--origin`, else `APP_URL` in your env file, else your framework’s dev server. It must be the exact origin
   you open the app on, port included.

`--dry-run` prints every step the real run would take (env files, `.gitignore`, `.env.example`, files, patches and the
install commands) and changes nothing. `nexafon <command> --help` prints the options of one command.

Every scaffolded token endpoint starts with a `signedInUser` that returns nobody, so it answers `401` until you
connect it to your app’s own session, as [user tokens](/v1/server/user-tokens/) describes. It also starts with a
`mayUseNexafon` (`nexafon_may_use()` in plain PHP) that returns false, so it answers `403` to every signed-in user
until you return true for your staff: a token can make its user a member of your workspace, so never for customers who
signed up. Use your app’s staff flag or role; return true for every signed-in user only when your app truly has no
customer accounts (no public sign-up), and say so in a comment there. WordPress uses the `nexafon_use` capability
instead. Both steps are yours: the CLI cannot know your sign-in or who your staff are. The Laravel and WordPress
scaffolds already sign for the framework’s own user (`$request->user()`, `wp_get_current_user()`), so there only the
staff check is left. When the user record has no role, sign with `agent`; `admin` only for your app’s own admins.

Until you paste the secret key, every scaffolded endpoint answers `500` with the plain text “Nexafon secret key is not
set” (no stack trace). In Laravel the CSRF check runs before the controller: a `POST` without the `X-CSRF-TOKEN`
header gets `419`, and `401` is what a signed-out request with a valid CSRF token gets. The scaffolded Blade
component sends the header.

Script-tag integrations (Laravel, WordPress, plain HTML and PHP) need the loader’s address, `NEXAFON_EMBED_CDN_URL`:
the folder that serves `v1/nexafon.js`, here https://app.nexafone.so/embed (the same address as in the
[HTML guide’s](/v1/web/html/) script tag; for a local Nexafon stack, `EMBED_CDN_URL` in the checkout’s `.env`). Pass
it as `--embed-cdn-url` or set it in the environment and `init` writes it in; left empty, the page asks its own host
for `/v1/nexafon.js` and fails with “Nexafon is not defined”. The npm packages have it built in.

Without Composer, `init` prints the PHP install also as `docker run` commands with the pinned Composer image. Installed
from a checkout, `composer.json` gets a path repository at the checkout’s absolute path, which works only where that
path exists until `nexafon/nexafon-php` is published.

| Framework | Token endpoint | Component |
| --- | --- | --- |
| React (Vite) | `server/nexafon-token.ts` and a Vite plugin | `src/NexafonSupport.tsx` |
| Next.js | `app/api/nexafon-token/route.ts` | `app/nexafon-support.tsx` |
| Vue | `server/nexafon-token.ts` and a Vite plugin | `src/nexafon.ts`, `src/components/NexafonSupport.vue` |
| Nuxt | `server/api/nexafon-token.post.ts` | `plugins/nexafon.client.ts`, `components/NexafonSupport.vue` |
| Angular | `src/nexafon-token.ts` (Express) | `src/app/nexafon.ts`, `src/app/nexafon-support.ts` |
| Svelte, SvelteKit | a Vite plugin, or `src/routes/api/nexafon-token/+server.ts` | `src/lib/NexafonSupport.svelte` |
| HTML | `nexafon/nexafon-token.ts` (Node) | `nexafon/nexafon.html` |
| Laravel | `NexafonTokenController` and `POST /nexafon/token` | `<x-nexafon-support />`, `config/nexafon.php` |
| PHP | `nexafon-token.php` | `nexafon-snippet.html` |
| WordPress | a plugin’s REST route | the `[nexafon_inbox]` shortcode |

| Flag | |
| --- | --- |
| `--dir <path>` | The app’s root (default: the current folder) |
| `--framework <name>` | `react-vite`, `next`, `vue`, `nuxt`, `angular`, `svelte`, `sveltekit`, `html`, `laravel`, `php`, `wordpress` |
| `--from <path>` | A Nexafon checkout or a folder of packed `.tgz` files to install from |
| `--no-install` | Only print the install commands |
| `--color <hex>` | The brand colour, instead of the detected one |
| `--embed-cdn-url <url>` | The loader’s address for script-tag integrations (`NEXAFON_EMBED_CDN_URL`) |
| `--origin <url>` | The origin you open your app on, printed for the allowed websites (`NEXAFON_APP_ORIGIN`) |
| `--force` | Replace files `init` created before |
| `--dry-run` | Print every step it would take, write and install nothing |

## nexafon doctor

`doctor` reads the keys from your app’s env files (and `wp-config.php`, and the environment) without printing them,
and checks the integration end to end. It exits with `1` when a check fails, so it can run in CI.

| Check | What it catches |
| --- | --- |
| Keys | A missing or empty key, the **wrong key type** (a secret key in the publishable key’s variable or the reverse), test and live keys mixed, a workspace key with a partner key |
| Loader script | Whether the loader address your page uses (`NEXAFON_EMBED_CDN_URL` for Laravel and WordPress, the HTML/PHP snippet’s script tag, or the address built into the installed `@nexafon/js`) is set and serves `v1/nexafon.js` |
| Secrets out of the frontend | A secret key under a `VITE_`, `NEXT_PUBLIC_`, `NUXT_PUBLIC_` or `PUBLIC_` variable, an env file with secrets that git tracks, a secret key written in frontend code |
| Token signing | Signs a test user token and exchanges it at `POST /api/v1/sdk/sessions`; Nexafon checks the signature and stops before creating a session. Finds a secret key of another workspace or mode, a revoked key, a plan without SDK embeds |
| Allowed origins | Whether your app’s origin may start sessions with the publishable key (**origin not allowed**: `client_not_allowed`) |
| Embed domains | Whether Nexafon’s embed pages list your origin in their CSP `frame-ancestors` |
| Webhooks | Whether your `NEXAFON_WEBHOOK_SECRET` is the endpoint’s signing secret (**bad webhook secret**), then sends a real `webhook.test` delivery and reports whether your receiver accepted it; `--listen <port>` receives it in `doctor` and verifies the signature there |
| Mobile push credentials | Not available yet: the [mobile SDKs](/v1/mobile/) are coming soon |

| Flag | |
| --- | --- |
| `--api-url <url>` | Nexafon’s API (`NEXAFON_API_URL`) |
| `--nexafon-app-url <url>` | Nexafon’s app, which serves the embed pages (`NEXAFON_APP_URL`) |
| `--origin <url>` | Your app’s origin (`NEXAFON_APP_ORIGIN`; default: your framework’s dev server) |
| `--workspace <id>` | Your workspace id, to check webhooks with a secret key (`NEXAFON_WORKSPACE_ID`) |
| `--webhook-endpoint <id>` | The endpoint to test (default: the one whose secret you configured) |
| `--listen <port>` | Receive the test delivery on this port and verify it |
| `--json` | Machine-readable output |

Webhook checks need a secret key with `developers:manage` (`partner.webhooks:manage` for a partner key).

`doctor` takes Nexafon’s addresses only from its flags or your shell’s environment, never from the app’s env files,
because it sends your secret key there. It refuses an address that is not `https`, except `http` to `localhost`.

## nexafon skill add

Installs the [Nexafon agent skill](/v1/tools/ai-agents/) in a project: `.claude/skills/nexafon/` for Claude Code, a
Nexafon section in `AGENTS.md` for Codex, Copilot and other agents, and `.cursor/rules/nexafon.mdc` for Cursor.
Running it again only updates them. `--no-agents-md` and `--no-cursor` leave those files alone.
