This file provides guidance for AI agents (Claude Code and subagents) working in this repository.
Templates are not used directly — they're rendered at generation time. The examples/ folder is the ground truth for what the generator actually produces. Always regenerate after making template changes.
The generator does not auto-discover templates. Every output file must have an explicit entry in the getFiles(): array method of the corresponding Language class. Forgetting this means the file silently doesn't get generated.
| Parent | Children affected |
|---|---|
Node |
CLI, ReactNative |
Dart |
Flutter |
Swift |
Apple |
Kotlin |
Android |
Go |
GoCLI |
Modifying a parent's template or getFiles() affects all children. Regenerate and verify child SDKs too.
Two couplings are not visible in the hierarchy.
Concern/CliCommandSurface.php is a trait used by both CLI and GoCLI. It holds
the nine helpers that decide what a generated command looks like — flag syntax, query
flags, promoted root commands, service scopes. It is shared precisely so the two CLIs
cannot drift, which means a change there alters the TypeScript CLI and the Go CLI at once.
templates/cli/install.sh.twig and templates/cli/install.ps1.twig live under
templates/cli/ but are registered in both CLI::getFiles() and GoCLI::getFiles().
They build every download URL from language.params.npmPackage, which also names every
release asset produced by .goreleaser.yaml and consumed by the scoop manifest and the
npm platform packages. Change the asset naming in one place and all four must move
together, for both CLIs.
Either way, regenerate both:
php example.php cli && php example.php go-cliFiles with 'scope' => 'copy' are copied verbatim — no variable substitution happens. If your new file needs template variables, use 'scope' => 'default' (or service, method, etc.).
The destination string in each getFiles() entry supports Twig expressions and filters:
'destination' => 'src/Services/{{ service.name | caseCamel }}.php',Lock file templates (package-lock.json.twig, bun.lock.twig) contain Twig expressions that get corrupted if you copy a raw lock file over them. Always use the update script:
./scripts/update-lockfiles.sh cli # update CLI lock files only
./scripts/update-lockfiles.sh all # update all TS-based SDK lock filesThe script strips Twig expressions before running npm install/bun install, then restores them automatically. Never run cp package-lock.json package-lock.json.twig or edit these files by hand.
- Purpose: Generate Appwrite SDKs and tooling targets for 20+ languages/platforms from Swagger/OpenAPI specs using Twig templates
- Language: PHP (generator engine) + Twig (templates)
- Entry point:
example.php— runs generation for all or a specific SDK - Output:
examples/<lang>/— generated SDK output for local verification. Not checked in —.gitignoreexcludesexamples/*, so it is a scratch area you regenerate, never a diff baseline
src/SDK/Language/<Lang>.php ← Language class: defines files, types, keywords
templates/<lang>/ ← Twig templates for that language
examples/<lang>/ ← Generated SDK output (gitignored; regenerate to verify)
example.php ← Entry point: regenerates all SDKs from specs
Supported SDKs: PHP, Web, Node, CLI, GoCLI, Ruby, Python, Dart, Flutter, React Native, Go, Swift, Apple, DotNet, Android, Kotlin, Unity, REST, GraphQL, Rust, Skills, CursorPlugin, ClaudePlugin, CodexPlugin
- Edit template(s) in
templates/<lang>/ - Regenerate:
php example.php <lang>
- Inspect
examples/<lang>/to verify the output is correct.git diffwill not show it —examples/*is gitignored. To compare before/after, copyexamples/aside,git stash -uyour template changes, regenerate, anddiff -rthe two trees.-umatters: a newly added template is untracked, and a plaingit stashleaves it in place, so the "before" tree is generated with your change still applied and the comparison shows nothing - Run linters and refactor check:
composer refactor:check composer lint-twig # or directly uvx djLint templates/ --lint
- Create the
.twigfile intemplates/<lang>/ - Register it in
src/SDK/Language/<Lang>.php→getFiles()array — this is mandatory:
public function getFiles(): array
{
return [
// ...existing entries...
[
'scope' => 'default', // default|service|method|definition|requestModel|enum|copy|download
'destination' => 'path/to/output.ext',
'template' => 'lang/path/to/template.twig',
],
];
}Scopes:
default— generated once per SDK (config files, README, main entry point)service— generated once per API servicemethod— generated once per service×method combinationdefinition— generated once per model/definitionrequestModel— generated once per request modelenum— generated once per enumcopy— static files copied as-is, no Twig processingdownload— generated once per SDK by downloading the URL intemplatetodestination
- Regenerate and verify
- Create
src/SDK/Language/NewLang.php(extendLanguageor a related language) - Implement:
getName(),getKeywords(),getIdentifierOverrides(),getFiles(),getTypeName(),getParamDefault(),getParamExample() - Create
templates/newlang/and add all Twig files - Register all template files in
getFiles() - Add generation block to
example.php - Generate:
php example.php newlang - Inspect
examples/newlang/
| What you want to change | Where to look |
|---|---|
| Template for a language | templates/<lang>/ |
| Which files get generated | src/SDK/Language/<Lang>.php → getFiles() |
| Type mappings for a language | src/SDK/Language/<Lang>.php → getTypeName() |
| Available Twig filters | src/SDK/SDK.php (around line 62) |
| How specs are parsed | src/Spec/OpenAPI3.php, src/Spec/Swagger2.php |
| Generation orchestration | src/SDK/SDK.php → generate() |
| Example generation script | example.php |
| Generated output for review (gitignored) | examples/<lang>/ |
Pass as first argument to generate only that SDK:
| Argument | Language class | Output dir |
|---|---|---|
php |
PHP | examples/php/ |
unity |
Unity | examples/unity/ |
web |
Web | examples/web/ |
node |
Node | examples/node/ |
cli |
CLI | examples/cli/ |
ruby |
Ruby | examples/ruby/ |
python |
Python | examples/python/ |
dart |
Dart | examples/dart/ |
flutter |
Flutter | examples/flutter/ |
react-native |
ReactNative | examples/react-native/ |
go |
Go | examples/go/ |
go-cli |
GoCLI | examples/go-cli/ |
swift |
Swift | examples/swift/ |
apple |
Apple | examples/apple/ |
dotnet |
DotNet | examples/dotnet/ |
rest |
REST | examples/REST/ |
android |
Android | examples/android/ |
kotlin |
Kotlin | examples/kotlin/ |
graphql |
GraphQL | examples/graphql/ |
rust |
Rust | examples/rust/ |
skills |
Skills | examples/skills/ |
cursor-plugin |
CursorPlugin | examples/cursor-plugin/ |
claude-plugin |
ClaudePlugin | examples/claude-plugin/ |
codex-plugin |
CodexPlugin | examples/codex-plugin/ |
zed-extension |
ZedExtension | examples/zed-extension/ |
| Scope | Extra variables available |
|---|---|
| All scopes | spec, language, sdk |
service |
+ service |
method |
+ service, method |
definition |
+ definition |
requestModel |
+ requestModel |
enum |
+ enum |
- Silent no-op: A new
.twigfile with nogetFiles()entry — generation runs successfully but the file is never created - Wrong scope: Using
defaultscope when you needservicescope means your template can't access{{ service.name }} - Copy scope surprises: A
copy-scoped file with Twig syntax — the syntax is output literally, not rendered - Spec fetch failure:
example.phprequires internet access to fetch the live spec from GitHub; generation fails with an exception if the fetch returns empty. Spec URL pattern (prefix isopen-api3orswagger2depending on the format):https://raw.githubusercontent.com/appwrite/specs/main/specs/{version}/open-api3-{version}-{platform}.json - Spec formats:
example.phpparses OpenAPI 3 specs by default (Appwrite\Spec\OpenAPI3). Swagger 2 (Appwrite\Spec\Swagger2) is still fully supported; both formats produce identical SDKs. Pass the format as the third argument:php example.php <sdk> <platform> swagger2 # use Swagger 2 spec SDK_GEN_SPEC_FILE=/path/to/spec.json php example.php # use a local spec file
- Platform mismatch: Pass the right platform (
console,client,server) as second arg — different platforms expose different API services - Child language gaps: Adding a file to a parent's
getFiles()but the child language needs a different template — child classes can overridegetFiles()to replace or remove entries
composer update --ignore-platform-reqs --optimize-autoloader --no-plugins --no-scripts --prefer-distTests are split into two suites:
tests/unit/— fast, pure-PHP tests (spec parsers); no Docker neededtests/e2e/— per-language SDK tests; generate an SDK fromtests/resources/spec-openapi3.jsonintotests/e2e/sdks/and run it in Docker against a mock API. The mock server (./mock-server) is started insetUp()and removed intearDown()(docker compose down); after interrupted runs, clean up withcd mock-server && docker compose downtests/resources/— shared fixtures (spec file, upload files) used by both suites
vendor/bin/phpunit --testsuite Unit # fast, run these always
vendor/bin/phpunit tests/e2e/PHP83Test.php # one language e2e (needs Docker)If local PHP is missing, is not the required version, or has extension issues, use the matching PHP Docker image as a fallback for that command.
Before submitting changes that touch templates or language classes:
- Regenerated the affected SDK(s) with
example.php - Inspected
examples/<lang>/output looks correct (gitignored — inspect the files directly,git statuswill not list them) - Any new template files are listed in
getFiles()of the language class - Any new language class is added to
example.php - Rector check passes (
composer refactor:check) - Twig linter passes (
composer lint-twig) - If a parent language was modified, child SDKs were also checked
- If
Concern/CliCommandSurface.phpwas touched, both CLIs were regenerated and their e2e suites run — the trait is shared, so a change there moves the shipping TypeScript CLI as well as the Go one - Go CLI changes compile and pass their tests:
cd examples/go-cli && go build ./... && go vet ./... && go test ./...