# AGENTS.md — difyctl (TypeScript CLI)

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`]**.

## Code rules

- **Spaces, not tabs.**
- **Minimum comments.** Code speak for self. Comment only non-obvious WHY — hidden constraints, subtle invariants, bug-workaround notes. Never restate code. Never reference tasks, PRs, current callers.
- **No magic strings or numbers.** Enums or named constants for bounded value sets.
- **No long positional arg lists.** Use options objects.
- **No long if/switch ladders on discriminator.** Polymorphism, dispatch tables, or strategy pattern. Name concept, let implementations plug in.
- **No `any`. No `unknown` outside genuine wire boundaries** (HTTP body parse, env vars). Narrow types everywhere else.
- **Avoid `!` non-null assertions.** Narrow instead.
- **`readonly` on inputs not mutated.**
- **Discriminated unions** for variant data (SSE events, run outputs, error shapes), not optional-field bags.
- **No backwards-compat shims.** No re-exports of old names, no `// removed:` markers, no deprecation notes. Delete, update callers.
- **No new dependencies without explicit approval.**
- **No CLI behavior changes in refactor commit.** Same flags, same output, same exit codes.
- **Every leaf command extends `DifyCommand`.** Add `static agentGuide` string when command benefits from agent workflow docs — see `src/commands/AGENTS.md`.

## Layering

| Layer     | Path                             | Role                                                                                                                                  |
| --------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| 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.                                                                            |
| printers  | `src/printers/`                  | `CompositePrintFlags` + `-o {json,yaml,name,wide,text}` matrix.                                                                       |
| errors    | `src/errors/`                    | `BaseError`, `ErrorCode` enum, `ExitCode` enum, dispatch table, `formatErrorForCli`.                                                  |
| guide     | `src/commands/**/<cmd>/guide.ts` | Per-command agent guide string. Export `agentGuide`, assign `static agentGuide = agentGuide` in command class. Surfaced via `--help`. |
| cache     | `src/cache/`                     | On-disk caches (app-info, etc.).                                                                                                      |
| auth      | `src/auth/`                      | Hosts file, token store, login flow.                                                                                                  |
| config    | `src/config/`                    | XDG dir resolution, config.yml load/save.                                                                                             |
| workspace | `src/workspace/`                 | Resolver: flag → env → bundle.                                                                                                        |
| types     | `src/types/`                     | Pure data + zod schemas for server contracts. No runtime imports outward.                                                             |

## Command Structure

Scaffold recipe + checklist: see `ARD.md §New command scaffold`. Full folder convention (subcommands, guide.ts): see `src/commands/AGENTS.md`.

Layer rules:

- Commands thin shells. Use `this.authedCtx(opts)` for bearer context; delegate to domain function.
- 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/store/dir.ts`, `src/util/browser.ts`. Nowhere else.
- No circular imports. `types/` pure leaf.

## Dev commands

```sh
pnpm install                                   # one-time
pnpm dev <command> [args...]                   # run CLI from source (no -- separator)
pnpm test                                      # vitest
pnpm test:coverage                             # with coverage
pnpm type-check                                # tsc, no emit
pnpm lint                                      # eslint
pnpm lint:fix                                  # eslint --fix
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
```

Release binaries (5 platform targets, Bun-compiled) are produced by `pnpm build:bin` (called from `.github/workflows/cli-release.yml`).

## Tests

- Behavior tests run against real Hono mock at `test/fixtures/dify-mock/`. No `nock`, `msw`, or `fetchMock` — every test exercises real HTTP.
- Test files co-located: `foo.test.ts` next to `foo.ts`.
- Type-check, lint, full test suite must be green before any commit.

## Spec docs (`docs/specs/`)

Behavior contracts. Living tree — amended in place, no version subfolders.

**Keep:** HTTP wire shape (req/resp JSON, headers, status codes), SQL DDL, Redis keys + TTL, state transitions, audit event names + payload, error/exit codes, rate-limit values, JWS/cookie envelope claims.

**Cut:** language type decls, internal helper sigs, decorator snippets, file-path tables, pseudocode mirroring code, "Open items"/"Handler walk"/"CI guard"/"Migration" sections, rationale (`Rejected:`/`Why X not Y`/`Historical note:`/product comparisons), release-pipeline lines, version-pinning (`in v1.0`, `post-v1.0`, milestone codes), frontmatter `date`/`status`/`author`.

**Test:** "rewrite in Rust tomorrow, does spec hold?" HTTP/SQL/Redis stays; type defs go.

**Rules:** behavior, not rationale. One topic per file; cross-refs = `auth.md §Storage`. Tables beat prose. Code wins on drift — update spec.

## Out of scope for unrelated work

Do not modify in passing:

- `test/fixtures/dify-mock/` public surface (endpoints, JSON shapes, status codes, scenario names) — that's the dify-api contract.
- `bin/`, `scripts/`, `Makefile`, `eslint.config.js`, `tsconfig*.json`, `package.json` (unless the change is required by the task).

## Commits

- One concern per commit. Style: `<type>(<scope>): <imperative subject>` lowercase. Body explains why if non-obvious.
- Never push, amend, force-push, or skip hooks (`--no-verify`) without explicit user approval.

[`ARD.md`]: ARD.md
