7.3 KiB
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.
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. Nounknownoutside genuine wire boundaries (HTTP body parse, env vars). Narrow types everywhere else. - Avoid
!non-null assertions. Narrow instead. readonlyon 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. Addstatic agentGuidestring when command benefits from agent workflow docs — seesrc/commands/AGENTS.md.
Layering
| 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. |
| 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 oclif.
- Only
src/http/client.tsandsrc/api/*import ky at runtime; elsewhere useimport type { KyInstance }. process.*lives insrc/io/,src/config/dir.ts,src/util/browser.ts. Nowhere else.- No circular imports.
types/pure leaf.
Dev commands
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 + oclif manifest
pnpm manifest # regenerate oclif.manifest.json only
make covers build / test / release / ci as no-arg targets. Dev runs use pnpm dev directly.
Tests
- Behavior tests run against real Hono mock at
test/fixtures/dify-mock/. Nonock,msw, orfetchMock— every test exercises real HTTP. - Test files co-located:
foo.test.tsnext tofoo.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.