PR #36328 (remove oclif) dropped the DifyCommand.catch() override that routed BaseError through formatErrorForCli with semantic exit codes. The replacement catch in framework/run.ts wrote raw err.message and always exited 1, losing the code prefix, hint, http_status line, JSON envelope path, and Auth/Usage/VersionCompat exit codes. framework/run.ts: - Add sniffOutputFormat(argv) helper: detects --output / -o (= and space forms), stops at --, first-occurrence-wins. Schema-free so it survives command-construction failures and pre-parse throws. - Rewrite catch block: branch BaseError -> Error -> non-Error. BaseError branch routes through formatErrorForCli({ format: sniffOutputFormat(argv) }) and exits via err.exit(). Explicit return after each process.exit defends against stubbed exits in tests. run/app/sse-collector.ts: - decodeStreamError now unwraps openapi-v1 InvokeError envelopes ({error_type, args, message}) buried inside env.message. Prefers args.description, falls back to inner.message, then raw on shape mismatch. framework/command.ts: - Sort named imports (fix pre-existing lint error). Tests (run.test.ts new, sse-collector.test.ts extended): - 10 sniffOutputFormat cases. - 12 run() catch-routing cases: BaseError human/JSON, Usage/Server5xx exit codes, withRequest method+url in human and JSON, generic Error, non-Error throw, success path, constructor-time BaseError, -- separator. - 5 decodeStreamError unwrap cases. Full suite: 675/675. type-check + lint clean. No subclass changes.
difyctl
CLI client for Dify platform. Browser device-flow signin, list/inspect apps, run with structured input, parse output as JSON, YAML, or human text.
Install
npm
npm install -g @langgenius/difyctl
Tarball
# macOS arm64
curl -fsSL https://github.com/langgenius/dify/releases/latest/download/difyctl-darwin-arm64.tar.xz | tar xJ -C /usr/local
ln -sf /usr/local/difyctl/bin/difyctl /usr/local/bin/difyctl
# Linux x64
curl -fsSL https://github.com/langgenius/dify/releases/latest/download/difyctl-linux-x64.tar.xz | tar xJ -C /opt
ln -sf /opt/difyctl/bin/difyctl /usr/local/bin/difyctl
Other targets: darwin-x64, linux-arm64, win32-x64.
Container
docker run --rm -it -v "$HOME/.config/difyctl:/root/.config/difyctl" \
ghcr.io/langgenius/difyctl:latest version
Quickstart
difyctl auth login # opens browser; paste the device code shown
difyctl get app # list apps in default workspace
difyctl describe app <app-id> # inspect parameters
difyctl run app <app-id> "hello" # run, blocking
difyctl run app <app-id> "hello" -o json | jq .answer # JSON output
difyctl run app <app-id> --input name=world --input topic=cats # workflow inputs
Background docs: difyctl help account, difyctl help external, difyctl help environment.
Commands
| Group | Commands |
|---|---|
auth |
login, logout, status, whoami, use <workspace>, devices list/revoke |
get |
get app [<id>] [-A] [--mode] [--name] [--tag] [-o json|yaml|name|wide], get workspace |
describe |
describe app <id> [--refresh] [-o json|yaml] |
run |
run app <id> [<message>] [--input k=v]... [--conversation <id>] [--stream] [-o json|yaml|text] |
config |
view, get <key>, set <key> <value>, unset <key>, path |
env |
list |
help |
account, external, environment |
version |
version [--json] |
Run difyctl <cmd> --help for per-command reference.
Output formats
| Flag | Behavior |
|---|---|
| (none) | Human table, columns auto-sized to terminal. |
-o wide |
Same as table, no column truncation. |
-o json |
Pretty-printed JSON, machine-parseable, stable shape. |
-o yaml |
YAML mirror of -o json. |
-o name |
IDs only, newline-separated — pipes into xargs. |
-o text |
kubectl-describe style human text (describe, run). |
Errors emit JSON envelope to stderr in -o json mode; else human message. Exit codes deterministic.
Configuration
| OS | Config path |
|---|---|
| Linux | ${XDG_CONFIG_HOME:-$HOME/.config}/difyctl/ |
| macOS | $HOME/.config/difyctl/ |
| Windows | %APPDATA%\difyctl\ |
Override with DIFY_CONFIG_DIR=/some/path. Files written 0600, directory 0700. Tokens use OS keychain by default, fall back to sealed file on hosts without one.
For every env var difyctl reads, run difyctl env list (machine-readable) or difyctl help environment (narrative).
Streaming
run app uses blocking transport by default. For long-running apps (likely exceed ~30s) pass --stream:
difyctl run app app-1 "tell me about cats" --stream
Agent apps (mode === 'agent-chat' or is_agent flag set) stream regardless — Dify backend rejects blocking requests for agent mode. Combining --stream with -o json or -o yaml aggregates SSE events into same envelope shape as blocking response, so structured output identical regardless of transport.
HTTP retry
Idempotent requests (GET, PUT, DELETE) retry on transient network/DNS failures with exponential backoff. Default count: 3. POST and PATCH never retry — side effects possible.
| Knob | Effect |
|---|---|
--http-retry <n> |
Per-invocation override. 0 disables retries. |
DIFYCTL_HTTP_RETRY=<n> |
Process-level default. |
Resolution: flag → env → 3.
Contributing
See ARD.md for architecture patterns, scaffolding recipe, dev workflow.
License
Apache-2.0.