Command Palette

Search for a command to run...

Addons / Creating Addons

Build your own addon

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.jsonVariant detection10 step typesRollback on failure

The manifest at a glance

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": [...]
}

Top-level fields

schema_version
Always "1" for the current format.
id
The registry identifier — used in CLI commands as the addon-id.
name
Human-readable display name shown in the registry UI.
version
Semantic version of this addon.
description
One-line description of what the addon adds to a project.
author
The author's GitHub username or organization.
requires
Array of addon IDs that must already be applied in this project (present in anesis.lock) before this addon runs — not merely cached. Anesis refuses to run with a clear error naming the missing dependency.

Inputs — prompting users for information

Inputs 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"]
  }
]

Derived variable forms

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_api

Detection — choosing the right variant

The 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" }
    ]
  }
]

Rule types

file_exists
Passes when a given file path exists in the project.
file_contains
Passes when a file contains a specific string.
json_contains
Passes when a JSON file has a value at a specific key path (dot-separated).
toml_contains
Passes when a TOML file has a value at a specific key path.
yaml_contains
Passes when a YAML file has a value at a specific key path.
negate: true
Add to any rule to invert its result.

match: "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.

Variants — conditional command sets

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.

Commands — the user-facing operations

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": [...]
}
once
When true, the command will not run if its name already appears in anesis.lock for this addon. Use this for one-time setup commands like "install".
requires_commands
A list of command names that must have already run (appear in anesis.lock) before this command executes. Enforces ordering between commands.

Ten step types for all file and shell operations

Steps 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

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

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.

inject

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.

replace

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

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.

delete

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 and move

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"
}

packages

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.

run

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.

Rollback on failure

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.

  • Steps are applied in order. If any step fails, Anesis asks whether to keep the partial changes or roll back everything that ran before the failure.
  • Copy and create steps restore the original file (or delete newly created files) on rollback.
  • Inject, replace, and append steps restore the file content to its pre-step state on rollback.
  • A packages step snapshots the package manifest/lockfile before running and restores them if the install command fails or a later step fails.
  • Delete steps are not rolled back — the file is gone. Place delete steps at the end of your step list if order matters.
  • Run steps are never rolled back — they're recorded as an irreversible action in the journal, so anesis undo skips them and leaves their side effects in place.

Safety model

The CLI enforces path boundaries so addons cannot read or write outside their designated directories.

  • Addon source paths (used in copy steps) are validated against the addon's cache directory. An addon cannot read files outside its own cached folder.
  • Project target paths are normalized and checked to ensure they stay inside the project root. A step cannot write to ../../etc/passwd or any path outside the current directory.
  • Glob targets are canonicalized after expansion so symlinks cannot be used to escape the project root.