@formfactory-dev/cli
v1.35.5
Published
Form Factory's developer CLI: scaffolds .ff/ workflow projects and wraps gh, acli, and Qase.
Maintainers
Readme
@formfactory-dev/cli
The ff binary — Form Factory's developer CLI.
ff init scaffolds workflow projects (.ff/workflows/*.ts files that import the @formfactory-dev/workflows SDK), and ff wraps gh, acli, and the Qase API as terminal shortcuts.
Workflow files run via pnpm run <name> and need @formfactory-dev/workflows, @formfactory-dev/toolkit, @types/node, and tsx as dev dependencies.
https://github.com/user-attachments/assets/eee2e1a9-b464-4321-a10c-a6602bfaaae9
Prerequisites
- Node.js (v24+)
- Claude Code (v1.0+)
- GitHub CLI (
gh) — only if you useff github * - Atlassian CLI (
acli) — only if you useff jira *orff confluence *
Installation
npm install -g @formfactory-dev/cliRun ff --help to confirm the install.
Tool authentication
GitHub CLI (gh)
gh auth loginSecrets via .ff/.env
ff auto-loads <repoRoot>/.ff/.env at startup. ff init scaffolds a
.ff/.env.example listing every variable the cli + workflows might read;
copy it to .ff/.env (gitignored) and fill in the relevant values.
Shell-exported variables always win over the file.
Atlassian CLI (acli)
ff jira * reuses acli's authenticated session — run acli auth login once.
ff confluence * hits the REST API directly with Basic auth. Set these
in .ff/.env:
[email protected]
ATLASSIAN_API_TOKEN=your-api-tokenGenerate API tokens at https://id.atlassian.com/manage-profile/security/api-tokens.
Qase
ff qase * requires an API token. Generate one at https://app.qase.io/user/api/token,
then add to .ff/.env:
QASE_API_TOKEN=your-token-here
# Optional overrides (also settable per-repo in .ff/settings.json):
QASE_DEFAULT_PROJECT=PROJECT_CODE
QASE_BASE_URL=https://api.qase.io/v1Non-secret Qase config (defaultProject, baseUrl) can also live in
<repo>/.ff/settings.json; env vars win when both are set.
Usage
ff --help lists every command.
Scaffolding a workflow project — ff init
ff init # in any repo
pnpm run work PROJ-123 # invoke the bundled `work` workflowff init writes:
.ff/workflows/work.ts— overnight orchestrator; decomposes a ticket into a subtask DAG and implements each as its own PR.ff/workflows/revise.ts— morning sweep across open PRs; addresses CodeRabbit feedback per-thread.ff/prompts/{plan,implement,review,revise}.md— prompt templates.ff/sandbox/Dockerfile— for thedocker()sandbox provider.ff/settings.json+.ff/tsconfig.json— config and IDE setup- A script entry per workflow file in your repo's
package.json @formfactory-dev/workflows,@formfactory-dev/toolkit,@types/node, andtsxas dev dependencies (auto-detects pnpm/yarn/npm/bun)
The SDK API (run, createWorktree, claudeCode, GitHub helpers) is documented in @formfactory-dev/workflows's README.
Bundled workflows
work— overnight run on Jira ticket(s). Decomposes a ticket into a topo-ordered subtask DAG, then implements each subtask on its own worktree. Multi-ticket invocations run in parallel up to a concurrency cap.revise— morning sweep across your open PRs. For each PR with unresolved CodeRabbit threads, runs.ff/prompts/revise.mdon a worktree of the PR's head branch; the agent addresses or skips per-thread, replies on each (audit trail), and pushes anything committed.
Both files + their .ff/prompts/ templates ship as editable defaults — customize per project.
Run with pnpm run work PROJ-30.
Sandbox Dockerfile
ff init scaffolds a starter .ff/sandbox/Dockerfile. docker() from the SDK builds it lazily on first use; rebuilds happen automatically when its sha changes.
Recommended companion tooling
.coderabbit.yaml— setreviews.request_changes_workflow: trueso reviews are submitted asCHANGES_REQUESTEDandrevisehas something to gate on.devplugin —/pushskill used byimplement.mdandrevise.md. Replaceable withgh pr createbut handy for stack handling.pr-review-toolkitplugin —/pr-review-toolkit:review-prskill used byreview.mdwhen installed. Optional.gh— required for PR creation, comment fetching, thread replies.acli— required for the planner to read Jira tickets and for--label-style discovery.
GitHub
# In a repo: PRs on this repo (gh auto-detects from git remote).
# Outside a repo: iterate every path registered under `repos` in ~/.ff/settings.json.
ff github my-prs
ff github prs-to-review
# Force the iterate-all behaviour even from inside a repo:
ff github my-prs --all
ff github prs-to-review --allJira
ff jira list # repo's project (or iterate all when outside)
ff jira list --all # force iterate-all from inside a repo
ff jira view TASK-123 # specific taskConfluence
ff confluence view https://yourcompany.atlassian.net/wiki/spaces/SPACE/pages/123456789Qase
ff qase search -q "title ~ 'login' AND labels ~ 'critical'"
ff qase cases list --limit 50
ff qase cases view 123
ff qase plans list
ff qase plans view 456
ff qase runs list
ff qase runs view 789
ff qase steps list 123Settings reference
ff reads its configuration from settings.json files at two tiers, both optional. The full schema lives at settings.schema.json and an example home-tier file at settings.example.json.
Tiers
| Tier | Path | Typical contents |
| ---- | -------------------------- | ------------------------------------------------------- |
| Home | ~/.ff/settings.json | Custom worktree root, repo registry, defaults |
| Repo | <repo>/.ff/settings.json | Jira / Confluence / Qase mappings (non-secret metadata) |
Secrets (Atlassian email + API token, Qase API token) come from environment variables — see Tool authentication.
On load, ff reads the home tier, then walks from cwd upward to the git root collecting <dir>/.ff/settings.json at each level, and merges. Closer tiers win on conflict.
Precedence
~/.ff/settings.json (top-level) ← lowest
~/.ff/settings.json repos[<matched>] ← middle (only if cwd is inside a registered path)
<gitRoot>/.ff/settings.json
... walking up from cwd ...
<cwd>/.ff/settings.json ← highestEach field replaces (last wins). The repos map merges by key — home-tier
only.
The repos registry
~/.ff/settings.json can list every repo you work on under a repos map. Keys are absolute paths to repo roots; values are optional overrides.
{
"repos": {
"/Users/me/Work/form-factory/projects/cli": {},
"/Users/me/Work/elf-storefront-horizon-temp": {
"jira": { "organization": "elfcosmetics", "project": "ELFSHOP" }
}
}
}Effects:
- Inside a registered repo path — the entry's fields merge in as a tier between home top-level and the repo's own file. The empty object
{}registers a path without overriding anything. - Outside any registered repo path —
ff github my-prs,ff github prs-to-review, andff jira listiterate every entry instead of erroring. The github commands spawnghinside each path so it auto-detects the repo fromgit config remote.origin.url. The jira command queries each entry'sjiramapping. - Anywhere with
--all— same iterate-every-entry behavior even when cwd is inside a registered path.
Path matching canonicalises both sides (resolves symlinks, handles macOS /var → /private/var). When multiple registered paths are ancestors of cwd, the deepest one wins.
Field reference
type Settings = {
worktreeRoot?: string // default ~/.cache/ff-cli/worktrees
jira?: { organization: string; project: string }
confluence?: { organization: string; space: string }
qase?: {
defaultProject?: string // QASE_DEFAULT_PROJECT env var wins
baseUrl?: string // restricted to qase.io / api.qase.io / app.qase.io
}
repos?: Record<string, RegisteredRepo> // home tier only
}RegisteredRepo is Settings minus the recursive repos field. Secrets
(Atlassian email/API token, Qase API token) come from environment variables
— see Tool authentication.
Bootstrap
# Per-repo settings + workflow scaffold
ff init
# Home-tier credentials (one-time) — copy the example from your global install
# (replace `$(npm root -g)` with `$(pnpm root -g)`, `$(yarn global dir)/node_modules`,
# or `$HOME/.bun/install/global/node_modules` as appropriate):
mkdir -p ~/.ff && cp "$(npm root -g)/@formfactory-dev/cli/settings.example.json" ~/.ff/settings.jsonPublishing
Published via pnpm publish from GitHub Actions using OIDC (the npm scope is configured as a "trusted publisher" tied to this repo and the release workflow — local publishes are rejected). pnpm publish rewrites catalog: and workspace: protocol references in package.json to concrete version ranges before upload, so consumers installing via npm, yarn, pnpm, or bun see normal version strings.
License
MIT — see LICENSE.
