An addon is a directory with an anesis.addon.json manifest. The manifest declares who the addon is, what it depends on, how to detect which variant of a project it's working with, and what commands it exposes — each with its own inputs and file operation steps.
anesis.addon.json must be at the root of your addon directory. All top-level fields are required except requires.
{
"schema_version": "1",
"id": "nest-drizzle",
"name": "Nest Drizzle",
"version": "0.1.0",
"description": "Adds Drizzle ORM to a NestJS project.",
"author": "your-github-username",
"requires": ["dotenv"],
"inputs": [...],
"detect": [...],
"variants": [...]
}schema_versionidnameversiondescriptionauthorrequiresInputs are declared at two levels: the manifest level (asked once per addon run) and the command level (asked when a specific command runs). Both use the same structure.
"inputs": [
{
"name": "package_name",
"type": "text",
"description": "Package scope to use (e.g. @myorg)",
"default": "@app",
"required": true
},
{
"name": "use_ssl",
"type": "boolean",
"description": "Enable SSL for the database connection?",
"default": false,
"required": false
},
{
"name": "driver",
"type": "select",
"description": "Database driver",
"default": "postgres",
"required": true,
"options": ["postgres", "sqlite", "mysql"]
}
]Every input value is automatically available in step templates in five casing forms. You don't need to transform strings manually:
# Given input name: "package_name" with value "@myorg/api"
{{ package_name }} → @myorg/api
{{ package_name_pascal }} → MyorgApi
{{ package_name_camel }} → myorgApi
{{ package_name_kebab }} → myorg-api
{{ package_name_snake }} → myorg_apiThe detect array lets your addon behave differently depending on the target project's setup. Anesis evaluates each detect block in order and uses the first one that matches.
"detect": [
{
"id": "fastify",
"match": "all",
"rules": [
{
"type": "json_contains",
"file": "package.json",
"key_path": "dependencies.@nestjs/platform-fastify"
}
]
},
{
"id": "has-env",
"match": "any",
"rules": [
{ "type": "file_exists", "file": ".env" },
{ "type": "file_exists", "file": ".env.local" }
]
}
]file_existsfile_containsjson_containstoml_containsyaml_containsnegate: truematch: "all" requires every rule in the block to pass. match: "any" (the default) requires at least one rule to pass. If no detect block matches, Anesis falls back to the variant with when: null.
Each variant holds a set of commands. The variant whose when matches the detected id is used. Include a variant with when: null as a fallback for projects that don't match any detect block.
"variants": [
{
"when": "fastify",
"commands": [
{ "name": "install", "steps": [...] }
]
},
{
"when": null,
"commands": [
{ "name": "install", "steps": [...] }
]
}
]You can have as many variants as you need. Each variant can expose a different set of commands — or the same command names with different step implementations for different project setups.
Each command in a variant has a name, optional constraints, its own inputs, and a list of steps to execute.
{
"name": "install",
"description": "Install Drizzle ORM support",
"once": true,
"requires_commands": ["bootstrap"],
"inputs": [
{
"name": "driver",
"type": "select",
"description": "Database driver",
"default": "postgres",
"required": true,
"options": ["postgres", "sqlite"]
}
],
"steps": [...]
}oncerequires_commandsSteps are executed in order. Every step that writes to a file renders its content and paths through Tera — inputs and their derived forms are all available. Targets can be a single { "type": "file", "file": "path" } or a glob { "type": "glob", "glob": "src/**/*.ts" }.
Copy a file from your addon's directory into the project.
{
"type": "copy",
"src": "templates/drizzle.config.ts",
"dest": "drizzle.config.ts",
"if_exists": "ask"
}if_exists controls behavior when the destination file already exists: overwrite (default), ask, or skip.
Create a new file with rendered content at a given path.
{
"type": "create",
"path": "src/db/{{ driver_kebab }}.ts",
"content": "export const driver = '{{ driver }}';\n",
"if_exists": "overwrite"
}Both path and content are rendered through Tera. Use input variables in the path to generate files with dynamic names.
Insert content into an existing file at a named marker.
{
"type": "inject",
"target": { "type": "file", "file": "src/app.module.ts" },
"content": "import { DrizzleModule } from './db/drizzle.module';",
"after": "// anesis:top-imports",
"if_not_found": "error"
}Use after or before to name a marker string. If neither is specified, content is prepended to the file. The if_not_found field controls what happens if the marker is absent: warn_and_ask (default), skip, or error.
Find a string in file(s) and replace it with new content.
{
"type": "replace",
"target": { "type": "glob", "glob": "src/**/*.ts" },
"find": "// TODO: add-db-import",
"replace": "import { db } from './db';",
"if_not_found": "skip"
}Both find and replace are rendered through Tera. Use glob targets to apply the same replacement across many files. Use // anesis: comment markers in your template source to create predictable insertion points.
Append rendered content to the end of one or more files.
{
"type": "append",
"target": { "type": "file", "file": ".env" },
"content": "DATABASE_URL={{ db_url }}\n"
}Anesis ensures the file ends with a newline before appending, so you don't get content accidentally concatenated onto the last existing line.
Remove files from the project. Supports both single files and glob patterns.
{
"type": "delete",
"target": { "type": "file", "file": "src/db/placeholder.ts" }
}Delete is not rolled back on failure. If you need to delete files as part of a reversible operation, place delete steps at the end of your step list after all create/inject steps succeed.
Rename a file or move it to a new location. Both the source and destination paths are rendered through Tera.
{
"type": "rename",
"from": "src/db/template.ts",
"to": "src/db/{{ driver_snake }}.ts"
}Add production and/or dev dependencies using whatever package manager the project already uses.
{
"type": "packages",
"dependencies": ["drizzle-orm", "{{ driver }}"],
"dev_dependencies": ["drizzle-kit"]
}Anesis detects the manager from lockfiles/manifests already in the project root — bun.lock(b), pnpm-lock.yaml, yarn.lock, package.json (npm), or Cargo.toml — and runs its native add command (npm install for npm, <pm> add otherwise, with each tool's own dev-dependency flag). The manifest and lockfile are snapshotted first and restored if the command fails, so a failed install doesn't leave a half-modified package.json.
Execute an arbitrary shell command in the project root — for anything the other step types can't express, like running a code generator.
{
"type": "run",
"command": "npx drizzle-kit generate --dialect {{ driver }}",
"description": "Generate the initial Drizzle migration"
}command is rendered through Tera first. Because this runs unsandboxed code, Anesis always prints the exact resolved command and asks for an explicit yes before executing it — unless --yes or non-interactive mode is active. A run step is not reversible: it's recorded in the rollback journal as an irreversible action, so anesis undo skips it rather than trying to undo its side effects.
If a step fails mid-run, Anesis gives the user a choice: keep the partial changes as they are, or roll back all completed steps.
anesis undo skips them and leaves their side effects in place.The CLI enforces path boundaries so addons cannot read or write outside their designated directories.
copy steps) are validated against the addon's cache directory. An addon cannot read files outside its own cached folder.../../etc/passwd or any path outside the current directory.