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
117 changes: 44 additions & 73 deletions .github/workflows/cli-release.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,19 @@
name: CLI Release

on:
release:
types: [published]
workflow_dispatch:
inputs:
dify_release_tag:
description: "dify release tag to attach cli artifacts to (e.g. 1.14.0). Bare semver — dify tags are NOT v-prefixed."
type: string
required: true

concurrency:
group: cli-release-${{ github.event.release.tag_name || inputs.dify_release_tag }}
group: cli-release-${{ github.ref }}
cancel-in-progress: true

jobs:
release:
name: build standalone binaries (all targets)
runs-on: depot-ubuntu-24.04
if: >-
github.repository == 'langgenius/dify' &&
(github.event_name == 'workflow_dispatch' ||
(vars.CLI_AUTO_RELEASE == 'true' && !github.event.release.prerelease))
env:
DIFY_TAG: ${{ github.event.release.tag_name || inputs.dify_release_tag }}
if: github.repository == 'langgenius/dify'
permissions:
contents: write
id-token: write
defaults:
run:
shell: bash
Expand All @@ -41,11 +29,10 @@ jobs:
- name: Setup web environment
uses: ./.github/actions/setup-web

- name: Setup Node registry auth
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Bun
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.2
with:
node-version-file: .nvmrc
registry-url: 'https://registry.npmjs.org'
bun-version: latest

- name: Read cli/package.json
id: manifest
Expand All @@ -64,68 +51,52 @@ jobs:
- name: Validate manifest
run: scripts/release-validate-manifest.sh

- name: Bump guard (auto-path only)
if: github.event_name == 'release'
run: scripts/release-bump-guard.sh
env:
NEW_VERSION: ${{ steps.manifest.outputs.version }}
NEW_MIN_DIFY: ${{ steps.manifest.outputs.minDify }}
NEW_MAX_DIFY: ${{ steps.manifest.outputs.maxDify }}
- name: Install cross-arch native prebuilds
# Re-installs node_modules with every @napi-rs/keyring platform variant
# so `bun build --compile` can embed the right .node into each target.
working-directory: ./
run: NPM_CONFIG_USERCONFIG="$PWD/cli/scripts/cross-arch.npmrc" pnpm install --frozen-lockfile

- name: Verify target dify release exists
run: gh release view "$DIFY_TAG" --repo langgenius/dify > /dev/null
- name: Compile standalone binaries (all targets)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Build cli
CLI_VERSION: ${{ steps.manifest.outputs.version }}
DIFYCTL_CHANNEL: ${{ steps.manifest.outputs.channel }}
DIFYCTL_MIN_DIFY: ${{ steps.manifest.outputs.minDify }}
DIFYCTL_MAX_DIFY: ${{ steps.manifest.outputs.maxDify }}
run: |
DIFYCTL_VERSION="${{ steps.manifest.outputs.version }}" \
DIFYCTL_CHANNEL="${{ steps.manifest.outputs.channel }}" \
DIFYCTL_MIN_DIFY="${{ steps.manifest.outputs.minDify }}" \
DIFYCTL_MAX_DIFY="${{ steps.manifest.outputs.maxDify }}" \
DIFYCTL_COMMIT="$(git rev-parse HEAD)" \
DIFYCTL_BUILD_DATE="$(git log -1 --format=%cI HEAD)" \
pnpm build

- name: Pack tarballs
run: pnpm pack:tarballs

- name: Publish to npm (idempotent)
run: scripts/release-npm-publish.sh
env:
CHANNEL: ${{ steps.manifest.outputs.channel }}
NEW_VERSION: ${{ steps.manifest.outputs.version }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
pnpm build:bin

- name: Generate sha256 checksum file
run: scripts/release-write-checksums.sh
env:
CLI_VERSION: ${{ steps.manifest.outputs.version }}
run: scripts/release-write-checksums.sh

- name: Install cosign
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2

- name: Keyless-sign tarballs + checksum file (Sigstore)
run: scripts/release-cosign-sign.sh
env:
CLI_VERSION: ${{ steps.manifest.outputs.version }}
COSIGN_EXPERIMENTAL: '1'

- name: Snapshot tarballs + checksum + signatures as workflow artifact
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: difyctl-${{ steps.manifest.outputs.version }}-${{ env.DIFY_TAG }}
path: |
cli/dist/difyctl-v*.tar.xz
cli/dist/difyctl-v*-checksums.txt
cli/dist/difyctl-v*.sig
cli/dist/difyctl-v*.pem
retention-days: 90
if-no-files-found: error

- name: Upload tarballs + checksum + signatures to dify GH release (idempotent)
run: scripts/release-upload-tarballs.sh
- name: Publish GitHub Release
env:
CLI_VERSION: ${{ steps.manifest.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: difyctl-v${{ steps.manifest.outputs.version }}
VERSION: ${{ steps.manifest.outputs.version }}
CHANNEL: ${{ steps.manifest.outputs.channel }}
working-directory: ./cli/dist/bin
run: |
prerelease_flag=""
if [ "$CHANNEL" != "stable" ]; then
prerelease_flag="--prerelease"
fi

if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
echo "Release $TAG exists — replacing assets"
gh release upload "$TAG" --repo "$REPO" --clobber difyctl-v*
else
echo "Creating release $TAG"
gh release create "$TAG" \
--repo "$REPO" \
--target "$GITHUB_SHA" \
--title "difyctl $VERSION" \
--notes "Automated release built by \`cli-release.yml\` (commit ${GITHUB_SHA:0:7})." \
$prerelease_flag \
difyctl-v*
fi
1 change: 0 additions & 1 deletion cli/.gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
dist/
coverage/
node_modules/
oclif.manifest.json
*.tsbuildinfo
.vitest-cache/
docs/specs/
Expand Down
15 changes: 8 additions & 7 deletions cli/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AGENTS.md — difyctl (TypeScript CLI)

TypeScript port of difyctl. Stack: oclif 4.x, Node 22+, ESM, ky for HTTP, vitest, eslint via @antfu/eslint-config.
TypeScript port of difyctl. Stack: custom CLI framework (`src/framework/`), Node 22+, ESM, ky for HTTP, vitest, eslint via @antfu/eslint-config.

> Architecture patterns, scaffolding recipe, printer chain, strategy pattern, testing conventions, anti-patterns: see **[`ARD.md`]**.

Expand All @@ -24,8 +24,8 @@ TypeScript port of difyctl. Stack: oclif 4.x, Node 22+, ESM, ky for HTTP, vitest

| Layer | Path | Role |
| --------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| commands | `src/commands/` | oclif command shells. Only place oclif imports run. |
| domain | `src/run/`, `src/get/`, etc. | Plain TS modules. Take typed deps via options. Testable without oclif. |
| commands | `src/commands/` | Command class shells (extend `DifyCommand`). Only place framework imports run. |
| domain | `src/run/`, `src/get/`, etc. | Plain TS modules. Take typed deps via options. Testable without the framework. |
| api | `src/api/` | One typed client per resource. Each takes `KyInstance`. |
| http | `src/http/` | `createClient` + middleware (auth, retry, logging, error mapping). Only place ky runs. |
| io | `src/io/` | Streams + spinner. Fence between data-out and progress UI. |
Expand All @@ -45,7 +45,7 @@ Scaffold recipe + checklist: see `ARD.md §New command scaffold`. Full folder co
Layer rules:

- Commands thin shells. Use `this.authedCtx(opts)` for bearer context; delegate to domain function.
- Domain receives deps via options; never imports oclif.
- Domain receives deps via options; never imports `src/framework/`.
- Only `src/http/client.ts` and `src/api/*` import ky at runtime; elsewhere use `import type { KyInstance }`.
- `process.*` lives in `src/io/`, `src/config/dir.ts`, `src/util/browser.ts`. Nowhere else.
- No circular imports. `types/` pure leaf.
Expand All @@ -60,11 +60,12 @@ pnpm test:coverage # with coverage
pnpm type-check # tsc, no emit
pnpm lint # eslint
pnpm lint:fix # eslint --fix
pnpm build # production bundle + oclif manifest
pnpm manifest # regenerate oclif.manifest.json only
pnpm build # production bundle (vp pack)
pnpm tree:gen # regenerate src/commands/tree.ts (registry)
pnpm tree:check # verify tree.ts is up-to-date with the fs
```

`make` covers `build` / `test` / `release` / `ci` as no-arg targets. Dev runs use `pnpm dev` directly.
Release binaries (5 platform targets, Bun-compiled) are produced by `pnpm build:bin` (called from `.github/workflows/cli-release.yml`).

## Tests

Expand Down
27 changes: 14 additions & 13 deletions cli/ARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ Examples: `get/app/`, `auth/devices/revoke/`, `describe/app/`.

**2. Mandatory files**

| File | Responsibility |
| ---------- | ---------------------------------------------------------------------------------------- |
| `index.ts` | oclif `Command` subclass. Flag/arg declaration + `run()` wiring only. No business logic. |
| `run.ts` | Pure async function. Typed options + deps. Returns string. No `@oclif/core` imports. |
| File | Responsibility |
| ---------- | --------------------------------------------------------------------------------------- |
| `index.ts` | `DifyCommand` subclass. Flag/arg declaration + `run()` wiring only. No business logic. |
| `run.ts` | Pure async function. Typed options + deps. Returns string. No `src/framework/` imports. |

**3. Optional files — add as needed**

Expand All @@ -61,10 +61,10 @@ Examples: `get/app/`, `auth/devices/revoke/`, `describe/app/`.
- [ ] Authed command calls `this.authedCtx()`; non-authed skips
- [ ] No try/catch in `run()` — `DifyCommand.catch()` handles `BaseError`
- [ ] `run.ts` returns string; no direct stdout write
- [ ] `run.ts` no `@oclif/core` imports
- [ ] `run.ts` no `src/framework/` imports
- [ ] HTTP client via factory dep, not direct
- [ ] `run.test.ts` written before impl (test-first)
- [ ] `pnpm manifest` run after adding command (updates `oclif.manifest.json`)
- [ ] `pnpm tree:gen` run after adding command (updates `src/commands/tree.ts`)
- [ ] README command table updated by hand

---
Expand Down Expand Up @@ -236,7 +236,7 @@ Never instantiate clients in `index.ts`.

**Test-first.** Write failing test, run to confirm fail, then implement.

Tests live in `run.test.ts` alongside command. Test `run.ts` direct — never oclif `Command` class.
Tests live in `run.test.ts` alongside command. Test `run.ts` direct — never the `DifyCommand` class.

```typescript
const io = bufferStreams()
Expand Down Expand Up @@ -290,13 +290,14 @@ expect(JSON.parse(out).workspaces).toHaveLength(2)
| `pnpm type-check` | `tsc --noEmit` — catches type errors without build |
| `pnpm lint` | ESLint check |
| `pnpm lint:fix` | ESLint auto-fix (perfectionist sort, chaining) |
| `pnpm build` | Production bundle + `oclif manifest` |
| `pnpm manifest` | Regenerate `oclif.manifest.json` only |
| `pnpm pack:tarballs` | Build distributable tarballs (release only) |
| `pnpm build` | Production bundle (`vp pack`) |
| `pnpm tree:gen` | Regenerate `src/commands/tree.ts` (registry) |
| `pnpm tree:check` | Verify `tree.ts` matches the filesystem |
| `pnpm build:bin` | Cross-compile standalone binaries via Bun (CI) |

**`pnpm manifest` rule:** run after adding, removing, renaming any command, flag, or arg. Manifest = runtime command registry — stale manifest causes silent flag failures at runtime.
**`pnpm tree:gen` rule:** run after adding, removing, renaming any command. The generated `tree.ts` is the runtime command registry — stale tree causes commands to be invisible at runtime. (Runs implicitly via `prebuild`/`predev`/`pretest`.)

**README hand-maintained.** `oclif readme` incompatible with this monorepo setup. When adding command, update command table in `README.md` manually.
**README hand-maintained.** When adding a command, update the command table in `README.md` manually.

---

Expand Down Expand Up @@ -336,7 +337,7 @@ Repo runs `@antfu/eslint-config` + perfectionist + unicorn.
| `enabled: !isHuman` in `runWithSpinner` | Set `outputFormat` on `IOStreams`; spinner auto-detects |
| Long positional arg lists | Options struct |
| `Record<string, Strategy>` dispatch map | Named singletons + picker function |
| `@oclif/core` import in `run.ts` | Keep oclif in `index.ts` only |
| `src/framework/` import in `run.ts` | Keep framework imports in `index.ts` only |
| `buildAuthedContext(this, opts)` in command body | `this.authedCtx(opts)` |
| `console.log` in `src/` | Return string from `run.ts`; write in `index.ts` |
| New dependency without approval | Check first |
Expand Down
45 changes: 13 additions & 32 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,23 @@ CLI client for [Dify] platform. Browser device-flow signin, list/inspect apps, r

## Install

### npm
Builds are standalone binaries (Bun-compiled) published as **GitHub Actions workflow artifacts** — no npm, no GitHub Release assets. The installer fetches the latest successful `cli-release.yml` run on `main`, verifies sha256, and copies the binary into `$HOME/.local/bin/difyctl`.

```sh
npm install -g @langgenius/difyctl
# GH_TOKEN with `actions:read` scope is required — workflow artifact downloads
# need auth even on public repos.
export GH_TOKEN=<your-pat>
curl -fsSL https://raw.githubusercontent.com/langgenius/dify/main/cli/scripts/install-cli.sh | sh
```

### Tarball
| Env | Default | Purpose |
| ---------------- | ----------------- | ----------------------------------------------------- |
| `GH_TOKEN` | — | GitHub PAT (or `GITHUB_TOKEN`) with `actions:read`. |
| `DIFYCTL_PREFIX` | `$HOME/.local` | Install root. Binary lands at `<prefix>/bin/difyctl`. |
| `DIFYCTL_REPO` | `langgenius/dify` | Source repo. |
| `DIFYCTL_BRANCH` | `main` | Branch to pick the latest successful run from. |

```sh
# 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

```sh
docker run --rm -it -v "$HOME/.config/difyctl:/root/.config/difyctl" \
ghcr.io/langgenius/difyctl:latest version
```
Supported targets: `darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`, `windows-x64.exe`. The shell installer covers Linux + macOS; Windows users can download the `.exe` directly from the same artifact.

## Quickstart

Expand All @@ -46,17 +37,7 @@ Background docs: `difyctl help account`, `difyctl help external`, `difyctl help

## 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 --help` for the full list of commands.
Run `difyctl <cmd> --help` for per-command reference.

## Output formats
Expand Down
2 changes: 1 addition & 1 deletion cli/bin/dev.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env -S node --import tsx
#!/usr/bin/env -S bun

globalThis.__DIFYCTL_VERSION__ = process.env.DIFYCTL_VERSION ?? '0.0.0-dev'
globalThis.__DIFYCTL_COMMIT__ = process.env.DIFYCTL_COMMIT ?? 'HEAD'
Expand Down
6 changes: 0 additions & 6 deletions cli/bin/run.js

This file was deleted.

10 changes: 10 additions & 0 deletions cli/bin/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env node
// Production entry compiled by `bun build --compile` (see scripts/release-build.sh).
// Imports from src/ so the release pipeline doesn't need `pnpm build` (dist/).
import { commandTree } from '../src/commands/tree.js'
import { run } from '../src/framework/run.js'

// Wrapped instead of top-level await — `bun build --bytecode` doesn't support TLA.
void (async () => {
await run(commandTree, process.argv.slice(2))
})()
Loading