Skip to content
Merged
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
71 changes: 71 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: build

# On a PR to main: install, build, unit-test on Linux + Windows (no publish).
# On push to main (or manual dispatch): after tests pass, publish a new build
# to GitHub Packages under the `next` dist-tag (from Ubuntu only). Versions are
# plain semver (major.minor from package.json; patch = highest published + 1),
# so testers can install ANY build: `@reply-team/reply-cli@0.1.42` or `@next`.
# Promotion to `@latest` is handled by the separate release workflow.
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read
packages: write

# Serialize main publishes so two merges can't grab the same patch number.
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://npm.pkg.github.com
scope: '@reply-team'

- name: Install
run: npm ci

- name: Build
run: npm run build

- name: Unit tests
run: npm test

# Publish only from a single OS (Ubuntu) on push/dispatch — never on PRs,
# never twice.
- name: Compute next @next version
if: ${{ github.event_name != 'pull_request' && matrix.os == 'ubuntu-latest' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
MM=$(node -p "require('./package.json').version.split('.').slice(0,2).join('.')")
PUB=$(npm view "@reply-team/reply-cli" versions --json 2>/dev/null || echo '[]')
NEXT=$(PUB="$PUB" MM="$MM" node -e "const mm=process.env.MM;let vs=[];try{vs=JSON.parse(process.env.PUB)}catch{}; if(!Array.isArray(vs)) vs = vs ? [vs] : []; const ps=vs.filter(v=>typeof v==='string'&&v.startsWith(mm+'.')).map(v=>parseInt(v.slice(mm.length+1),10)).filter(Number.isInteger); process.stdout.write(String(ps.length?Math.max(...ps)+1:0))")
VERSION="${MM}.${NEXT}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "Publishing $VERSION under @next"

- name: Publish to GitHub Packages (@next)
if: ${{ github.event_name != 'pull_request' && matrix.os == 'ubuntu-latest' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npm version "$VERSION" --no-git-tag-version --allow-same-version
npm pkg set commit="${{ github.sha }}"
npm publish --tag next
48 changes: 48 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: release

# Promote a tested build to the `latest` dist-tag — the manual, human-gated
# "ship it" step. No rebuild: the exact bytes testers ran on @next become
# @latest. Also tags the exact source commit the build came from.
on:
workflow_dispatch:
inputs:
version:
description: 'Tested version to promote to @latest (e.g. 0.1.42)'
required: true

permissions:
contents: write # push the release tag
packages: write # move the dist-tag

jobs:
promote:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true

- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://npm.pkg.github.com
scope: '@reply-team'

- name: Promote to @latest and tag the source commit
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VER='${{ github.event.inputs.version }}'
# Fail fast if the version was never published.
npm view "@reply-team/reply-cli@${VER}" version
npm dist-tag add "@reply-team/reply-cli@${VER}" latest
echo "Promoted @reply-team/reply-cli@${VER} to @latest"
SHA=$(npm view "@reply-team/reply-cli@${VER}" commit 2>/dev/null || true)
if [ -n "$SHA" ] && ! git rev-parse "v${VER}" >/dev/null 2>&1; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "v${VER}" -m "release ${VER}" "$SHA"
git push origin "v${VER}"
echo "Tagged v${VER} at ${SHA}"
fi
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
*.tgz
coverage/
88 changes: 88 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Contributing

Anyone can clone the repo, build it, and open a pull request — contributions and
bug reports are welcome. Merges are restricted: only Reply employees can approve
and merge a PR. Open one from a branch (or a fork), and a maintainer will review.

## Prerequisites

[Node.js](https://nodejs.org) 20 or newer.

## Setup

```sh
npm install
npm run build # compile TypeScript to dist/
npm test # run the test suite (vitest)
npm link # put the built `reply` binary on your PATH
```

## Tests

The suite is fully offline — no test contacts the Reply.io API or the identity
server. `fetch` is stubbed, and the OAuth loopback flow is exercised against a
local `127.0.0.1` listener with an injected browser stub. CI runs the build and
tests on Linux and Windows.

## Conventions

- Data is written to stdout; status and error messages go to stderr.
- `--json` emits compact JSON and `--pretty` indented JSON. On either, an error
is a single machine-readable line:
`{"error":{"status":…,"code":…,"title":…,"detail":…,"hint":…}}`.
- Exit codes: `0` success, `1` API or runtime failure, `2` usage error.
- Secrets are never printed; token and key fields are redacted in all output.

## Credentials on disk

Credentials are stored as JSON in the config directory (`~/.config/reply`, or
`%APPDATA%\reply` on Windows), created `0600` inside a `0700` directory — the
same plaintext-file model as `gh`, `aws`, and `az`. On Windows the strict mode
bits are a no-op and it relies on the per-user `%APPDATA%` ACLs, as those tools
do.

Each record is keyed by profile name, so multiple accounts never collide even
when they hit the same backend. A record is either an OAuth entry (access token
+ refresh token + expiry) or an API-key entry. Expired OAuth tokens refresh
automatically; if a refresh fails the record is cleared and the user is prompted
to log in again. The store sits behind a `CredentialStore` interface so an
OS-keychain backend can be added later without touching callers.

## Credential resolution

Resolved in strict order, first hit wins:

1. `--api-key <key>` flag
2. `REPLY_API_KEY` environment variable
3. the stored credential (from `auth login`)

The flag and env var are ephemeral — used for the current invocation only, never
written to disk. There is no `.env` file lookup.

## Testing against a non-prod backend

Profiles inherit the built-in prod URLs; override them to point a profile at
another environment (internal testing only):

```sh
reply profile add dev \
--authority https://oauth.dev.replyapp.io \
--api-base https://api.dev.reply.io/v3
reply --profile dev auth login
```

Any field left off is inherited from the default (prod). Profiles live in
`config.json` in the config directory and can also be hand-edited:

```jsonc
// ~/.config/reply/config.json
{ "profiles": { "dev": { "authority": "https://…", "api_base": "https://…/v3" } } }
```

## Releases

- Every push to `main` publishes a build to the `@next` dist-tag:
`npm install -g @reply-team/reply-cli@next`.
- A release promotes a tested `@next` version to `@latest` with
`npm dist-tag add` — the exact published bytes, no rebuild — and tags the
source commit.
88 changes: 87 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,87 @@
# reply-cli
# Reply CLI

`reply` is the command-line interface for [Reply.io](https://reply.io). Sign in
once and every Reply.io API request runs as you — from your terminal or your
scripts. Today it handles authentication and identity; resource commands for
sequences, contacts, and the inbox are on the way.

## Installation

Requires [Node.js](https://nodejs.org) 20 or newer. The CLI is published to
GitHub Packages under the `@reply-team` scope, so point that scope at the
registry once, then install globally:

```sh
echo "@reply-team:registry=https://npm.pkg.github.com" >> ~/.npmrc
npm install -g @reply-team/reply-cli
```

```sh
reply --version
```

## Usage

```sh
reply <command> [flags]
reply <command> --help
```

Run `reply --help` for the full command list. Add `--json` to any command for
machine-readable output suitable for scripts.

## Authentication

Log in through your browser with OAuth:

```sh
reply auth login
```

Or store an API key, read from stdin so it never lands in your shell history:

```sh
reply auth login --with-token
```

Inspect and manage the active credential:

```sh
reply auth status # who you're signed in as, and how — no secrets shown
reply auth whoami # verify the stored credential against the API
reply auth logout # remove the stored credential
```

Pass a key for a single command with `--api-key` or the `REPLY_API_KEY`
environment variable; both take precedence over a stored login and are never
written to disk.

## Profiles

Profiles keep more than one Reply.io account signed in at once — each stores its
own credential. Name them however you like; account emails work well:

```sh
reply profile add alice@reply.io
reply profile use alice@reply.io # make it the active profile
reply auth login # signs in alice@reply.io

reply profile list # '*' marks the active profile
reply --profile bob@reply.io auth whoami # override for a single command
```

The active profile is resolved as `--profile` → `REPLY_PROFILE` → the profile
set with `profile use` → the built-in default.

## Environment variables

| Variable | Description |
|----------|-------------|
| `REPLY_API_KEY` | API key used as the credential for the current invocation |
| `REPLY_PROFILE` | Profile to use (same as `--profile`) |
| `REPLY_CONFIG_DIR` | Config directory (default `~/.config/reply`; `%APPDATA%\reply` on Windows) |

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for building from source, running the
tests, credential-store internals, and the release process.
Loading
Loading