This is the abridged developer documentation for TerraDart
# Documentation
> Guides for TerraDart — type-safe infrastructure-as-code for Dart, from the Stack to the app.
TerraDart lets you write your infrastructure and your app in one typed Dart codebase. A `Stack` synthesizes standard Terraform JSON for Google Cloud, AWS, Cloudflare and Appwrite, and the values your app needs reach it as a generated Dart file instead of copied strings. Guides track the **0.35.x** line on pub.dev. Every Dart snippet on this site compiles against it in CI. ## Start [Section titled “Start”](#start) Pick the guide for where you start. Each is one path you can paste, from `dart pub global activate terradart_cli` to an apply, and none asks you to install Terraform. * [Start from an existing Flutter app](/docs/start/flutter-app/) — add `infra/` beside the app, apply it, and build the app with the outputs, typed, through `--dart-define-from-file` * [Start from an empty directory](/docs/start/new-project/) — `terradart init`, then `validate`, `plan` and `apply`, with a default environment and the state moved to a bucket * [Migrate an existing Terraform project](/docs/start/migrate-terraform/) — `terradart migrate`, a plan that reports *No changes*, then Dart one resource at a time * [Let an AI agent do it](/docs/start/ai-agent/) — the TerraDart Agent Skill, and the plan you approve * [Getting started](/docs/getting-started/) — the pieces built by hand: a Stack for your provider, synth, apply, and the values your app reads * [Why TerraDart](/docs/why-terradart/) — the problem, the design, and how it compares with HCL, CDKTF and Pulumi ## Guides [Section titled “Guides”](#guides) * [Writing arguments](/docs/arguments/) — which form an argument takes: a literal, a `ref`, an enum member, a variant, a variable, a secret * [Environments](/docs/environments/) — dev, staging and prod as a Dart enum: each with its own project and state, and its own define file for the client * [Outputs in client apps](/docs/client-outputs/) — build a Flutter, web or CLI client with apply-time values, typed, from the define file `terradart apply` writes * [Migrating from HCL](/docs/migrate-from-hcl/) — `terradart migrate` brings an existing Terraform tree over with a plan that reports *No changes* ## Providers [Section titled “Providers”](#providers) * [Google Cloud](/docs/providers/google/) — `terradart_google` and `terradart_google_beta` * [AWS](/docs/providers/aws/) — `terradart_aws`: Lambda, ECS Express Mode, Flutter Web on S3 + CloudFront * [Cloudflare](/docs/providers/cloudflare/) — `terradart_cloudflare`: the DNS and edge in front of your app * [Appwrite](/docs/providers/appwrite/) — `terradart_appwrite`: the backend of a Flutter app [`terradart_time`](https://pub.dev/packages/terradart_time) adds `TimeSleep`, the propagation wait any Stack can use, and everything builds on [`terradart_core`](https://pub.dev/packages/terradart_core). ## Reference [Section titled “Reference”](#reference) * [The terradart command](/docs/cli/) — `terradart synth`, `validate`, `plan`, `apply`, `destroy`, `outputs` and `migrate`, environments declared in Dart, and managed OpenTofu * [Coverage](/docs/coverage/) — every factory of every provider package, its barrel, and the examples that use it * [API reference](https://pub.dev/packages?q=terradart) — the dartdoc of every package on pub.dev * [llms.txt](/llms.txt) — these docs, condensed for LLMs ## Under the hood [Section titled “Under the hood”](#under-the-hood) * [How TerraDart works](/docs/how-it-works/) — the loop from Stack to `*.tf.json` to OpenTofu or Terraform, synth issues, and the constants and outputs that cross into your app * [How TerraDart is built](/docs/how-its-built/) — the generation pipeline and verification harness behind the factories; the maintainer CLI is [`terradart_codegen`](https://pub.dev/packages/terradart_codegen) (`terradart-codegen wrap`) ## Project [Section titled “Project”](#project) * [Upgrading](/docs/upgrading/) — breaking changes in the current minor; read before every minor bump * [Status & versioning](/docs/status/) — alpha, path to beta, 1.0 * [README](https://github.com/nozomi-koborinai/terradart/blob/main/README.md), [examples](https://github.com/nozomi-koborinai/terradart/tree/main/examples) and [cookbook](https://github.com/nozomi-koborinai/terradart/tree/main/cookbook) on GitHub
# Getting started
> Create a TerraDart project with terradart init, then see each piece by hand — the Stack, the synthesized *.tf.json, and the typed file your app imports.
This guide matches the [README quickstart](https://github.com/nozomi-koborinai/terradart#quickstart) for the **0.35.x** line. TerraDart is **alpha** — breaking changes land only on **minor** bumps; see [Status & versioning](/docs/status/) for the change policy and the [path to beta](/docs/status/#path-to-beta). ## Choose your path [Section titled “Choose your path”](#choose-your-path) Each of these is one path you can paste, from installing the `terradart` command to an apply. The rest of this page builds the same pieces by hand, one at a time. [Start from an existing Flutter app ](/docs/start/flutter-app/)infra/ beside the app, and the outputs typed in the app through dart-define. [Start from an empty directory ](/docs/start/new-project/)terradart init, validate, plan and apply. [Migrate an existing Terraform project ](/docs/start/migrate-terraform/)terradart migrate and a plan that reports No changes. [Let an AI agent do it ](/docs/start/ai-agent/)The TerraDart Agent Skill, and the plan you approve. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Dart SDK ≥ 3.10 * Nothing else to install for plan and apply: the [`terradart` command](/docs/cli/) downloads a checksum-verified OpenTofu on first use, or runs the `tofu` or `terraform` already on your `PATH`. * Appwrite only: [Terraform](https://developer.hashicorp.com/terraform/install) on your `PATH`. The `appwrite/appwrite` provider is published to the Terraform registry only, so OpenTofu cannot install it, and an Appwrite Stack runs on Terraform (`--engine terraform`, or `terradart: engine: terraform` in `pubspec.yaml`). * Credentials for your provider at apply time — Google Cloud Application Default Credentials (`gcloud auth application-default login`), the AWS SDK credential chain, `CLOUDFLARE_API_TOKEN`, or `APPWRITE_API_KEY`. Synth needs none, and none are written into the output. ## 1. Create a project [Section titled “1. Create a project”](#1-create-a-project) Install the [`terradart` command](/docs/cli/) once, and let it write the project: ```bash dart pub global activate terradart_cli terradart init --provider google --env dev,prd --gcp-project dev=acme-dev,prd=acme-prd --backend local cd infra terradart plan --env dev ``` `terradart init` writes `infra/` (or the directory you name), runs `dart pub get` in it, and prints what to run next. The project plans as it is: | File | Holds | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------- | | `pubspec.yaml` | `terradart_core` and the provider packages, at the command’s own version; `terradart: engine: terraform` with Appwrite | | `lib/env.dart` | the `Env` enum: each environment and its values — project or account ID, region, state bucket | | `lib/stack.dart` | a Stack with the provider, the backend, one resource and one output | | `bin/infra.dart` | the entry point, which hands the environments to `runEnvironments` | | `.gitignore` | `tf-out/` and `.terradart/` | | `README.md`, `AGENTS.md` | the next steps, and the commands and Agent Skill a coding agent works with | A value you did not give is a placeholder marked `TODO` in `lib/env.dart`. In a terminal, `terradart init` asks for what the flags leave out — the providers, the environment names, each environment’s project, region or account ID, whether a state bucket exists — and ends by printing the `terradart init ...` command that gives the same answers as flags. Asked whether a bucket for the state already exists, *no* keeps the state in a local file (move it later with `terradart state migrate`); *yes* takes its name and the backend of your provider. Without a terminal it never asks and never chooses for you: `--provider`, `--env` and `--backend` (or `--state-bucket`) are required, and a run without them prints the command to edit; `--defaults` accepts `dev,prd` and local state. `--dry-run` lists the files without writing them, and an existing file is never overwritten without `--force`. | Flag | Takes | | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `--provider` | `google`, `aws`, `cloudflare`, `appwrite`, comma-separated | | `--env` | any lowerCamelCase names, comma-separated | | `--gcp-project`, `--aws-region`, `--aws-account`, `--cloudflare-account`, `--appwrite-endpoint`, `--appwrite-project` | `=` pairs, or one value for every environment | | `--state-bucket` | a bucket that already exists, one name or `=` pairs: GCS for google, S3 for aws, R2 for cloudflare | | `--backend` | `local` (a file under `tf-out/`), `gcs`, `s3` or `r2` | | `--defaults` | `--env dev,prd --backend local` | | `--[no-]flutter` | wiring to the Flutter app in the current directory | Run inside a Flutter app, it also generates the reader the app imports into the app’s `lib/generated/`, and declares the [define file](/docs/client-outputs/) the app builds with: ```bash terradart init cd infra && terradart apply --env dev && cd .. flutter run --dart-define-from-file=infra/.terradart/dart_defines.dev.json ``` A directory that already holds `*.tf` files, at any depth, gets no scaffold: `terradart init` names where it found them and points at [`terradart migrate`](/docs/migrate-from-hcl/) instead, and in a terminal offers to run its report. The rest of this guide builds the same pieces by hand, one at a time. ## 2. Add dependencies [Section titled “2. Add dependencies”](#2-add-dependencies) `terradart_core` plus the package for each provider your Stack uses (all on [pub.dev](https://pub.dev/packages/terradart_core)). Every provider barrel re-exports `terradart_core`, so the Stack itself imports only the barrels: * Google Cloud pubspec.yaml ```yaml name: my_app environment: sdk: ^3.10.0 dependencies: terradart_core: ^0.35.0 terradart_google: ^0.35.0 # terradart_google_beta: ^0.35.0 # beta-only types (Firebase, ...) ``` * AWS pubspec.yaml ```yaml name: my_app environment: sdk: ^3.10.0 dependencies: terradart_core: ^0.35.0 terradart_aws: ^0.35.0 ``` * Cloudflare pubspec.yaml ```yaml name: my_app environment: sdk: ^3.10.0 dependencies: terradart_core: ^0.35.0 terradart_cloudflare: ^0.35.0 ``` * Appwrite pubspec.yaml ```yaml name: my_app environment: sdk: ^3.10.0 dependencies: terradart_core: ^0.35.0 terradart_appwrite: ^0.35.0 terradart: engine: terraform # Appwrite currently needs Terraform on PATH ``` ```bash dart pub get dart pub global activate terradart_cli ``` The second line installs the [`terradart` command](/docs/cli/), which synthesizes the Stack and runs `plan` or `apply` for you. To pin it per project instead, `dart pub add --dev terradart_cli` and run it as `dart run terradart_cli:terradart`. Each provider package wraps the whole catalog of its Terraform provider (Google’s beta-only types live in [`terradart_google_beta`](https://pub.dev/packages/terradart_google_beta)), so any resource the provider documents has a factory. One Stack can mix packages. ## 2. Define a Stack [Section titled “2. Define a Stack”](#2-define-a-stack) A Stack is one Terraform root module. `add` registers a resource; `addConstant` and `addOutput` declare what your app reads back — a constant at synth time, an output after apply — and `appExports` names the Dart file they are generated into: * Google Cloud lib/orders\_stack.dart ```dart import 'package:terradart_google/provider.dart'; import 'package:terradart_google/pubsub.dart'; final class OrdersStack extends Stack { OrdersStack({required String projectId}) : super( providers: [GoogleProvider(project: projectId)], appExports: AppExports('lib/generated/orders_stack.app.dart'), ) { final topic = add(GooglePubsubTopic( 'orders', name: .literal('orders-prod'), )); addConstant('ordersTopicName', .ref(topic.name)); addOutput('orders_topic_id', topic.id); } } ``` * AWS lib/queue\_stack.dart ```dart import 'package:terradart_aws/provider.dart'; import 'package:terradart_aws/sqs.dart'; final class QueueStack extends Stack { QueueStack() : super( providers: [const AwsProvider(region: 'us-east-1')], appExports: AppExports('lib/generated/queue_stack.app.dart'), ) { final queue = add(AwsSqsQueue( 'orders', name: .literal('orders-prod'), )); addConstant('ordersQueueName', .ref(queue.name)); addOutput('orders_queue_url', queue.url); } } ``` More on AWS — Lambda, ECS Express Mode, S3 + CloudFront: [AWS](/docs/providers/aws/). * Cloudflare lib/edge\_stack.dart ```dart import 'package:terradart_cloudflare/dns.dart'; import 'package:terradart_cloudflare/provider.dart'; import 'package:terradart_cloudflare/zone.dart'; final class EdgeStack extends Stack { EdgeStack({required String accountId}) : super( providers: [const CloudflareProvider()], appExports: AppExports('lib/generated/edge_stack.app.dart'), ) { final zone = add(CloudflareZone( 'main', name: .literal('example.com'), account: ZoneAccount(id: .literal(accountId)), )); final api = add(CloudflareDnsRecord( 'api', zoneId: zone.ref, // only a CloudflareZone fits here name: .literal('api.example.com'), type: .cname, ttl: .literal(1), content: .content(.literal('ghs.googlehosted.com')), proxied: .literal(true), )); addConstant('apiHost', .ref(api.name)); addOutput('zone_id', zone.id); } } ``` Runnable: [`examples/cloudflare_dns_quickstart`](https://github.com/nozomi-koborinai/terradart/tree/main/examples/cloudflare_dns_quickstart). * Appwrite lib/uploads\_stack.dart ```dart import 'package:terradart_appwrite/provider.dart'; import 'package:terradart_appwrite/storage.dart'; final class UploadsStack extends Stack { UploadsStack() : super( providers: [ const AppwriteProvider( endpoint: 'https://cloud.appwrite.io/v1', projectId: 'my-project', ), ], appExports: AppExports('lib/generated/uploads_stack.app.dart'), ) { final uploads = add(AppwriteStorageBucket( 'uploads', name: .literal('uploads'), fileSecurity: .literal(true), )); addOutput('uploads_bucket_id', uploads.id); } } ``` Runnable: [`examples/appwrite_quickstart`](https://github.com/nozomi-koborinai/terradart/tree/main/examples/appwrite_quickstart), which covers every Appwrite factory. `.literal(...)` is a value known now; `...` is one Terraform resolves during apply. An argument that names another resource takes that resource’s `ref` (`zoneId: zone.ref`) and does not compile with the wrong kind. Both are Dart 3.10 dot shorthands, like the enum in `.cname` — every argument is typed, so the type name can be left out. A block inside another block or inside a sealed choice is built the same way, with `.new(...)`: `delivery: .pushConfig(.new(pushEndpoint: ...))`. Keep the class name on a resource’s own arguments (`template: CloudRunV2ServiceTemplate(...)`), where it tells a reader which block starts. ### Writing arguments [Section titled “Writing arguments”](#writing-arguments) Pick the form by where the value comes from. The argument’s type tells you which forms it takes, and a dot shorthand (`.literal`, `.new`, `.providedAl2023`) names the constructor of that type. | The value is | Write | Example | | --------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------ | | known when you synth | `.literal(...)` | `functionName: .literal('hello')` | | another resource of this Stack (a `RefTo` input) | its `ref` | `role: role.ref` | | one attribute of another block | its getter | `assumeRolePolicy: trust.json` | | a resource outside this Stack (a `RefTo` input) | `.literal(id)` | `zoneId: .literal('023e105f4ecef8ad9ca31a8372d0c353')` | | one of a fixed set (an enum) | the member | `runtime: .providedAl2023` | | one of several exclusive arguments (a sealed type) | the variant | `code: .filename(.literal('build/fn.zip'))` | | a nested block | its helper class; `.new(...)` inside another block or a variant | `environment: LambdaFunctionEnvironment(...)` | | a Terraform variable | the handle `variable()` returns | `memorySize: memory` | | a variable in an enum or `RefTo` input | `.arg(handle)` | `runtime: .arg(runtimeName)` | | a secret (a `Sensitive` input) | a sensitive variable, never a literal | `value: .value(dbPassword)` | | a reference inside a literal list or map | the getter’s `.interpolation` | `{'ROLE_ARN': role.arn.interpolation}` | | anything else Terraform evaluates | `.expression(...)` | `.expression(r'${file("trust.json")}')` | ```dart final memory = variable('memory_mb'); final runtimeName = variable('runtime'); final dbPassword = variable('db_password', sensitive: true); final trust = add( DataAwsIamPolicyDocument( 'trust', statement: [ DataIamPolicyDocumentStatement( actions: .literal(['sts:AssumeRole']), principals: [ .new( type: .literal('Service'), identifiers: .literal(['lambda.amazonaws.com']), ), ], ), ], ), ); final role = add(AwsIamRole('hello', assumeRolePolicy: trust.json)); add( AwsLambdaFunction( 'hello', functionName: .literal('hello'), role: role.ref, runtime: .arg(runtimeName), code: .filename(.literal('build/fn.zip')), memorySize: memory, environment: LambdaFunctionEnvironment( variables: .literal({'ROLE_ARN': role.arn.interpolation}), ), ), ); add( AwsSsmParameter( 'db_password', name: .literal('/hello/db_password'), type: .securestring, value: .value(dbPassword), ), ); ``` More on each row: [Writing arguments](/docs/arguments/). ## 3. Synth Terraform JSON [Section titled “3. Synth Terraform JSON”](#3-synth-terraform-json) Synthesis is plain Dart: `bin/infra.dart`, the project’s entry point, constructs the Stack and hands it to `runStack`, which writes it. The rest of this guide follows the Google Cloud Stack; for the others, construct `QueueStack()`, `EdgeStack(accountId: ...)` or `UploadsStack()` the same way. bin/infra.dart ```dart import 'package:my_app/orders_stack.dart'; import 'package:terradart_core/terradart_core.dart'; Future main(List args) => runStack(args, () => OrdersStack(projectId: 'YOUR-PROJECT-ID')); ``` ```bash terradart synth ``` This runs `bin/infra.dart`, which writes `tf-out/main.tf.json` (with the `orders_topic_id` output) and `lib/generated/orders_stack.app.dart` (with the `ordersTopicName` constant and the `OrdersStackOutputs` reader). `dart run bin/infra.dart` does the same without the command. ## 4. Plan and apply [Section titled “4. Plan and apply”](#4-plan-and-apply) ```bash terradart plan terradart apply ``` Each command synthesizes first, then runs `init` and `plan` or `apply` in `tf-out/` with the `tofu` or `terraform` on your `PATH`, or a checksum-verified OpenTofu it downloads when there is neither. A Stack that uses Appwrite runs on the `terraform` on your `PATH`, and stops before `init` when there is none or `--engine tofu` asks for OpenTofu. For several environments, declare them as a Dart enum and hand it to `runEnvironments` in `bin/infra.dart`; `terradart apply --env prod` then runs one of them. See [Environments](/docs/environments/) and [The terradart command](/docs/cli/). On a new Google Cloud project every API is off, so the first apply fails with a 403 until the Stack turns on the ones it uses with `enableApis([.pubsub])` — see [Enabling APIs](/docs/providers/google/#enabling-apis). The project `terradart init` writes does it already. The output is ordinary Terraform JSON, so your existing remote state backend and modules stay unchanged — TerraDart only replaces HCL/JSON authoring. ## 5. Read it from your app [Section titled “5. Read it from your app”](#5-read-it-from-your-app) App code imports the generated file instead of repeating strings: lib/subscriber.dart ```dart import 'dart:io'; import 'generated/orders_stack.app.dart'; bool acceptsTopic(String eventTopic) => eventTopic == OrdersStackConstants.ordersTopicName; /// Known only after apply, so it comes from the generated outputs reader. String ordersTopicId() => OrdersStackOutputs.fromEnvironment(Platform.environment).ordersTopicId; ``` A deployed service reads outputs from its environment (`ORDERS_TOPIC_ID`) — the Stack passes them with `outputEnvironment()` — and a script can read the define file `terradart outputs` writes. A Flutter or web client is built with them instead. Add `addDartDefineOutput()` to the Stack, and `terradart apply` writes the define file the build compiles in: ```bash terradart apply flutter build web --dart-define-from-file=.terradart/dart_defines.json ``` With [environments](/docs/environments/), each one gets its own file: `terradart apply --env prod` writes `.terradart/dart_defines.prod.json`. The client reads each value with its type through the same `OrdersStackOutputs` reader; see [Outputs in client apps](/docs/client-outputs/). Rename `orders-prod` in the Stack and the subscriber follows on the next synth — there is no second copy of the string to update. Rename or remove the constant and `dart analyze` fails. See [How TerraDart works — the app boundary](/docs/how-it-works/#the-app-boundary-constants-and-outputs) and the runnable [pubsub quickstart](https://github.com/nozomi-koborinai/terradart/tree/main/examples/pubsub_quickstart) (`lib/subscriber_stub.dart`). ## Next steps [Section titled “Next steps”](#next-steps) * [Why TerraDart](/docs/why-terradart/) — motivation and comparisons * [Outputs in client apps](/docs/client-outputs/) — build a Flutter or web client with the values of an apply * [Environments](/docs/environments/) — dev, staging and prod as a Dart enum, one define file each * [How TerraDart works](/docs/how-it-works/) — synth, plan and apply, and the values your app reads back * [Coverage](/docs/coverage/) — every factory of every provider package, its barrel, and runnable examples * Providers: [Google Cloud](/docs/providers/google/) (with google-beta and Firebase), [AWS](/docs/providers/aws/), [Cloudflare](/docs/providers/cloudflare/), [Appwrite](/docs/providers/appwrite/) * [Migrating from HCL](/docs/migrate-from-hcl/) — bring an existing Terraform tree over * [Upgrading](/docs/upgrading/) — read before every minor bump * [Status & versioning](/docs/status/) — alpha vs beta vs 1.0 * [Examples](https://github.com/nozomi-koborinai/terradart/tree/main/examples) and [cookbook](https://github.com/nozomi-koborinai/terradart/tree/main/cookbook) for fuller stacks
# Let an AI agent do it
> Have a coding agent write and check TerraDart for you — the TerraDart Agent Skill, terradart init, validate and plan, and the apply you approve.
A coding agent can do the whole loop: create the project, write the Stack, check it, and show you the plan. TerraDart’s factories are generated Dart in the provider packages, so the agent reads the exact constructor instead of guessing a name, and `terradart validate` checks its work without credentials. You decide the providers, the environments and where the state lives, and you approve every apply. ## 1. Give the agent the skill [Section titled “1. Give the agent the skill”](#1-give-the-agent-the-skill) The **TerraDart Agent Skill** tells the agent where to look and which commands to run. The `terradart` command bundles the skill of its own release, so the agent learns that CLI and not whatever is on `main`. In a project that already has a `pubspec.yaml`: ```bash terradart skill install ``` That writes `.agents/skills/terradart/SKILL.md` (Cursor, Codex, Gemini CLI, GitHub Copilot and most others) and `.claude/skills/terradart/SKILL.md` (Claude Code). Commit both. After upgrading the CLI, `terradart skill update` rewrites them, and leaves a copy you edited alone unless `--force`. A new project gets the same files from `terradart init --agent-skill`. Without the `terradart` command, use the [`skills` CLI](https://github.com/vercel-labs/skills), pinned to the release tag so the skill matches the release you depend on rather than `main`: ```bash npx skills add nozomi-koborinai/terradart#v0.35.0 --skill terradart ``` The `skills` CLI asks which agents you use and writes the skill where each looks for skills. It is the same file `terradart skill install` writes. The skill is [`skills/terradart/SKILL.md`](https://github.com/nozomi-koborinai/terradart/blob/main/skills/terradart/SKILL.md) in the repository, in the [Agent Skills](https://agentskills.io) format; copying it into one of those directories works too. ## 2. Start a project [Section titled “2. Start a project”](#2-start-a-project) Decide three things before the agent runs anything: the providers, the environment names, and whether the state goes in a bucket. `terradart init` never picks them for an agent — without a terminal it refuses to run until the flags name all three — and the skill tells the agent to ask you. A prompt that answers them up front: ```text Create a TerraDart project in my_app with terradart init: Google Cloud, environments dev and prd (projects my-app-dev and my-app-prd), local state, and the agent skill. Then run terradart validate --env dev. ``` The agent runs, with your answers as flags: ```bash terradart init my_app --provider google --env dev,prd --gcp-project dev=my-app-dev,prd=my-app-prd --backend local --agent-skill cd my_app terradart validate --env dev ``` The project comes with an `AGENTS.md`: the commands, where the environments and resources live, and the rules — never edit `tf-out/` or `.terradart/`, never install or call `terraform` or `tofu` directly, and plan before any apply a human has approved. Inside a Flutter app, the same `terradart init` writes `infra/` and wires it to the app ([Start from an existing Flutter app](/docs/start/flutter-app/)). ## 3. Ask for changes [Section titled “3. Ask for changes”](#3-ask-for-changes) Describe the infrastructure, and ask for the check and the plan in the same prompt: ```text Add a Cloud Run service "api" running us-docker.pkg.dev/cloudrun/container/hello in lib/stack.dart, with an output api_url, and enable the APIs it needs. Run dart analyze, terradart validate --env dev and terradart plan --env dev, and show me the plan. ``` The skill gives the agent the loop: | Step | Command | Needs | | --------------------------- | ------------------------------ | ------------------------------------------------- | | The Dart compiles | `dart analyze` | nothing | | The Stack synthesizes | `terradart synth` | nothing | | The configuration is valid | `terradart validate --env dev` | nothing; the first run downloads OpenTofu | | What the apply would change | `terradart plan --env dev` | credentials for the provider in the agent’s shell | An agent without credentials — a cloud agent, a CI job — still gets through `validate`. Have it name the environment with `--env`: there is no terminal for it to answer a question in, and an apply against an environment the command chose by default stops rather than guess. ## 4. Apply [Section titled “4. Apply”](#4-apply) Read the plan, then apply it yourself: ```bash terradart apply --env dev ``` In a terminal of your own, the apply shows the plan again and asks before it changes anything. In an agent’s shell `terradart` never asks: an apply without `--auto-approve` stops before `init` with exit code 3 and prints the command with the flag added. The skill and the generated `AGENTS.md` both tell the agent to run `apply` and `destroy` only when you ask, so an agent should pass `--auto-approve` only when you said so. After the apply, the define file for a Flutter or web client is in `.terradart/dart_defines.dev.json` ([Outputs in client apps](/docs/client-outputs/)). ## Migrate existing Terraform [Section titled “Migrate existing Terraform”](#migrate-existing-terraform) The migrator does the mechanical translation; the agent finishes what it kept in Terraform: ```text Migrate infra/ to TerraDart: run terradart migrate --report --dir infra, then terradart migrate --dir infra --out infra_dart. Port the kept blocks listed in infra_dart/MIGRATION.md into the Stack one at a time, keeping each local name, and run terradart plan after each: it must report No changes. Do not apply. ``` The skill tells the agent not to rewrite a tree by hand, to keep every resource address, to leave a block in the sidecar when no factory covers it, and to fix the Dart — never the plan — when a plan is not empty. The steps it follows are [Migrate an existing Terraform project](/docs/start/migrate-terraform/). ## What the skill tells the agent [Section titled “What the skill tells the agent”](#what-the-skill-tells-the-agent) * **Which package** owns a Terraform type: `google_*`, beta-only `google_*`, `aws_*`, `cloudflare_*`, `appwrite_*`. * **How to find the class and its barrel.** Each package’s generated `lib/src/_catalog.g.dart` maps every Terraform type to its class and barrel, and the class’s doc comment lists its arguments. * **How to write the arguments**: a literal, another resource’s `ref`, an enum member, a variant, a secret — the rules of [Writing arguments](/docs/arguments/). * **Where the runnable examples are**: more than 100 quickstarts under [`examples/`](https://github.com/nozomi-koborinai/terradart/tree/main/examples), each synthesized and validated in CI. * **How to check and deploy**: `dart analyze`, `terradart synth`, `terradart validate`, `terradart plan`, environments declared in Dart with `runEnvironments`, and the define file a client builds with. It never asks you to install or run Terraform. * **How to migrate**: `terradart migrate` first, then the leftovers one block at a time, until `terradart plan` reports *No changes*. ## Without a checkout [Section titled “Without a checkout”](#without-a-checkout) * [`/llms.txt`](/llms.txt) is these docs, condensed for LLMs, with these guides and the command reference first. * [Coverage](/docs/coverage/) lists every factory of every provider package with its barrel and the examples that use it.
# Start from an existing Flutter app
> Add an infra/ package to a Flutter app with terradart init, apply it with the terradart command, and build the app with the outputs, typed, through dart-define.
Your Flutter app needs values that exist only after an apply: a topic ID, an API URL, a bucket. Here they reach the app typed, through a reader class TerraDart generates and the `--dart-define-from-file` flag Flutter already has. Dart and Flutter are all you install; the `terradart` command downloads a checksum-verified OpenTofu the first time it needs an engine. The steps use Google Cloud, with two projects: one for `dev`, one for `prd`. ## The layout [Section titled “The layout”](#the-layout) The infrastructure is a Dart package of its own, `infra/`, inside the app: ```text my_app/ ├── pubspec.yaml the Flutter app ├── lib/ │ ├── main.dart │ └── generated/infra.g.dart written by terradart synth: the typed reader └── infra/ ├── pubspec.yaml terradart_core and the provider packages ├── lib/env.dart dev and prd, and their values ├── lib/stack.dart the Stack ├── bin/infra.dart the entry point the terradart command runs └── .terradart/ dart_defines..json, written by terradart apply ``` It is a package of its own because it runs on the Dart VM (`dart run`, through the `terradart` command), where the app needs the Flutter SDK, and so the TerraDart packages never enter the app’s dependencies or its binary. The two meet at one generated file: the Stack writes `lib/generated/infra.g.dart` into the app, and the app imports it. ## 1. Install the command [Section titled “1. Install the command”](#1-install-the-command) ```bash dart pub global activate terradart_cli terradart --help ``` `terradart` lands in `~/.pub-cache/bin`. If the second line is not found, add that directory to your `PATH`, as `dart pub global activate` says. ## 2. Create `infra/` [Section titled “2. Create infra/”](#2-create-infra) From the app’s directory, the one with its `pubspec.yaml`: ```bash terradart init --provider google --env dev,prd --gcp-project dev=my-app-dev,prd=my-app-prd --backend local ``` Use your own project IDs. `terradart init` sees the Flutter app (a `pubspec.yaml` that depends on `flutter`) and wires `infra/` to it: ```text Created my_app_infra in infra (google; environments dev, prd; local state; wired to the Flutter app): pubspec.yaml lib/env.dart lib/stack.dart bin/infra.dart .gitignore README.md AGENTS.md Defaults: --flutter. Synth writes lib/generated/infra.g.dart for the app to read the Stack's outputs. > dart pub get ... Next: cd infra terradart apply --env dev cd .. flutter run --dart-define-from-file=infra/.terradart/dart_defines.dev.json ``` In a terminal, `terradart init` without flags asks for each of them instead, and asks before it wires the app. `--no-flutter` leaves the app alone. Every flag is in [The terradart command](/docs/cli/#creating-a-project). ## 3. Read the Stack [Section titled “3. Read the Stack”](#3-read-the-stack) infra/lib/env.dart ```dart /// The environments of the Stack: `terradart plan --env ` /// picks one. Replace each placeholder marked TODO. enum Env { dev(projectId: 'my-app-dev', region: 'us-central1'), prd(projectId: 'my-app-prd', region: 'us-central1'); const Env({required this.projectId, required this.region}); /// The Google Cloud project the Stack deploys to. final String projectId; /// The default Google Cloud region. final String region; } ``` Two lines connect the Stack to the app. `appExports` writes the reader into the app’s `lib/generated/`, and `addDartDefineOutput()` declares the define file — every non-sensitive output, as the strings `--dart-define-from-file` reads: infra/lib/stack.dart ```dart import 'package:terradart_google/project.dart'; import 'package:terradart_google/provider.dart'; import 'package:terradart_google/pubsub.dart'; import 'package:terradart_time/terradart_time.dart'; import 'env.dart'; /// The infrastructure of one environment. Replace the example resources /// with your own. final class MyAppInfraStack extends Stack { MyAppInfraStack({required Env env}) : super( providers: [ GoogleProvider(project: env.projectId, region: env.region), const TimeProvider(), ], backend: const LocalBackend(), appExports: AppExports('../lib/generated/infra.g.dart'), ) { // Enables the Pub/Sub API on the project and waits for it to propagate. final apis = enableApis([.pubsub]); final topic = add( GooglePubsubTopic( 'events', name: .literal('${env.name}-events'), dependsOn: apis, ), ); addOutput('events_topic_id', topic.id); // The outputs above, as the file `terradart apply --env ` writes // for `flutter run --dart-define-from-file`. addDartDefineOutput(); } } ``` Each `addOutput` becomes a typed getter of `MyAppInfraStackOutputs`; the `events_topic_id` output is `eventsTopicId`. Add the resources your app uses — a Cloud Run service, a bucket, Firebase — and an output for each value the app reads. Every resource of the provider has a factory, listed in [Coverage](/docs/coverage/google/). `infra/bin/infra.dart` gives `runEnvironments` the first environment as `defaultEnv`, so a command without `--env` runs against `dev` — see [Start from an empty directory](/docs/start/new-project/#4-the-default-environment). The commands below name the environment anyway, as a build script should. ## 4. Synth and validate [Section titled “4. Synth and validate”](#4-synth-and-validate) ```bash cd infra terradart synth terradart validate --env dev ``` `terradart synth` writes `tf-out/dev/` and `tf-out/prd/`, and the reader, `../lib/generated/infra.g.dart`. `terradart validate` checks the configuration without credentials or state; the first time, it downloads OpenTofu into your user cache: ```text env: dev (--env) Using OpenTofu 1.13.1 (no tofu or terraform on PATH; managed OpenTofu 1.13.1) > tofu init -backend=false -input=false (in tf-out/dev) ... > tofu validate (in tf-out/dev) Success! The configuration is valid. ``` ## 5. Plan and apply [Section titled “5. Plan and apply”](#5-plan-and-apply) Sign in as an account that can create resources in the project, then plan and apply `dev`: ```bash gcloud auth application-default login terradart plan --env dev terradart apply --env dev ``` The apply shows the plan and asks before it changes anything. Then it writes the outputs to the define file and prints the lines that build with it: ```text Wrote 1 dart-defines to .terradart/dart_defines.dev.json flutter run --dart-define-from-file=.terradart/dart_defines.dev.json flutter build --dart-define-from-file=.terradart/dart_defines.dev.json ``` Those paths are relative to `infra/`; from the app, the file is `infra/.terradart/dart_defines.dev.json`. ## 6. Read the outputs in the app [Section titled “6. Read the outputs in the app”](#6-read-the-outputs-in-the-app) The reader’s `fromDartDefine` constructor reads the values compiled into the app, so it is a constant: lib/main.dart ```dart import 'package:flutter/material.dart'; import 'generated/infra.g.dart'; const infra = MyAppInfraStackOutputs.fromDartDefine(); void main() => runApp( MaterialApp( home: Scaffold(body: Center(child: Text('Events: ${infra.eventsTopicId}'))), ), ); ``` Run it with the define file of `dev`: ```bash cd .. flutter run --dart-define-from-file=infra/.terradart/dart_defines.dev.json ``` A build without the file fails at the first getter it reads, with a `StateError` that names the variable and the output. Rename the output in the Stack and the getter changes on the next synth, so `flutter analyze` finds every place the app read it. ## 7. Production, and builds in CI [Section titled “7. Production, and builds in CI”](#7-production-and-builds-in-ci) Apply `prd` the same way, and build the release with its file: ```bash cd infra terradart apply --env prd cd .. flutter build web --dart-define-from-file=infra/.terradart/dart_defines.prd.json ``` A build job should read the applied state, not change it. `terradart outputs` synthesizes — which also writes the reader — then writes the define file from the state alone, with no plan and no apply: ```bash cd infra terradart outputs --env prd cd .. flutter build apk --dart-define-from-file=infra/.terradart/dart_defines.prd.json ``` For that job to see the state, it has to live in a bucket rather than in `infra/tf-out/` on your machine: create one, set the Stack’s `backend:` to `GcsBackend(bucket: 'my-app-tfstate', prefix: 'my_app_infra/${env.name}')`, and run `terradart state migrate --env dev` and `terradart state migrate --env prd` once, from `infra/`. Each asks before it copies the state. Only public values belong in a client: anything compiled into an app can be read by whoever has it. The define file leaves sensitive outputs out, and secrets stay on a server — see [Outputs in client apps](/docs/client-outputs/#secrets-stay-out). ## Next [Section titled “Next”](#next) * [Outputs in client apps](/docs/client-outputs/) — one define file per client, the `dart` command, servers and scripts * [Environments](/docs/environments/) — each environment’s own project, state and define file * [Appwrite](/docs/providers/appwrite/) and [Google Cloud with Firebase](/docs/providers/google/) — backends for a Flutter app * [Let an AI agent do it](/docs/start/ai-agent/) — the `AGENTS.md` `infra/` came with, and the Agent Skill
# Migrate an existing Terraform project
> Turn a Terraform tree into a TerraDart package with terradart migrate, prove it with a plan that reports No changes, and move to Dart one resource at a time.
Bring an existing Terraform tree into Dart without a rewrite and without touching your infrastructure. `terradart migrate` translates what it can into typed Dart, keeps the rest in Terraform exactly as written, and keeps every resource address, so the first plan against your existing state reports *No changes*. From there you move the remaining blocks one at a time, with a plan after each. You need the Dart SDK, 3.10 or later, and the access to the state you have today. The migration reads files only: it never runs an engine, never reads or writes state, and never writes into your tree. ## 1. Install the command [Section titled “1. Install the command”](#1-install-the-command) ```bash dart pub global activate terradart_cli terradart --help ``` `terradart` lands in `~/.pub-cache/bin`. If the second line is not found, add that directory to your `PATH`, as `dart pub global activate` says. The migration needs no Dart project: run it from anywhere. ## 2. Size it [Section titled “2. Size it”](#2-size-it) `--report` runs the whole migration in memory and writes nothing: ```bash terradart migrate --report --dir infra ``` ```text terradart migrate 0.x.y --report: infra 2 of 2 resource and data blocks translate (100%); 2 of 2 types have a curated factory. Nothing was written. Types (2): google_pubsub_subscription [resource] x1: 1 translate -> GooglePubsubSubscription (terradart_google/pubsub) google_pubsub_topic [resource] x1: 1 translate -> GooglePubsubTopic (terradart_google/pubsub) By directory: . (root): 2 translate, 0 kept Next: terradart migrate --dir infra --out ``` Each type shows how many of its blocks become Dart and how many stay in Terraform, with the reason for each kept block — a non-literal `count`, a sensitive literal, a type with no factory yet. ## 3. Migrate [Section titled “3. Migrate”](#3-migrate) ```bash terradart migrate --dir infra --out infra_dart ``` ```text terradart migrate 0.x.y: infra → infra_dart (infra) modules: 1 (1 root, 0 children); migrated 3 blocks, kept 0 .: InfraStack — 3 migrated, 0 kept → tf-out Report: infra_dart/MIGRATION.md Next: cd infra_dart && dart pub get && terradart synth then terradart plan ``` `infra_dart/` is a Dart package: one Stack per module directory in `lib/`, `bin/infra.dart`, a `tf-out/` tree that mirrors your source, and beside each `main.tf.json` the **leftover sidecar** — the blocks that stay in Terraform, verbatim. `MIGRATION.md` lists every module and every kept block with its reason. Environments in sibling directories (`envs/dev`, `envs/prod`) can become one Stack and a Dart enum with `--merge-envs`. That and every other flag: [Migrating from HCL](/docs/migrate-from-hcl/). ## 4. Keep the engine your state came from [Section titled “4. Keep the engine your state came from”](#4-keep-the-engine-your-state-came-from) Your state was written by Terraform, so the generated `pubspec.yaml` keeps the `terradart` command on Terraform: ```yaml terradart: engine: terraform ``` `terradart plan` and `apply` then run the `terraform` you already use, on the state it wrote. The section says `engine: tofu` instead when the tree was run with OpenTofu. To leave Terraform behind, change it to `engine: tofu`: `terradart` downloads a checksum-verified OpenTofu when none is on your `PATH`, and OpenTofu rewrites the state for itself at the next apply — after which Terraform may not read it back, so decide this for the whole team. Without the section, `terradart` reads the state before it runs; when the other engine wrote it, it asks in a terminal, and without one stops: ```text terradart: Stopped before running OpenTofu on a state Terraform wrote. Pass --engine terraform to keep Terraform (or set terradart.engine: terraform in pubspec.yaml), or --engine tofu to move the state to OpenTofu. ``` More in [The terradart command](/docs/cli/#the-engine). ## 5. Validate [Section titled “5. Validate”](#5-validate) ```bash cd infra_dart dart pub get terradart validate ``` `terradart validate` synthesizes, then checks the configuration — the Dart and the sidecar together — without credentials, a backend or state. ## 6. Plan: No changes [Section titled “6. Plan: No changes”](#6-plan-no-changes) The Stack carries your backend (`GcsBackend`, `S3Backend`, `LocalBackend`; any other stays in the sidecar’s `backend.tf` as written), so the plan reads the same remote state as before. With a **local** state file, copy it next to `main.tf.json` first — the migrator never copies state: ```bash cp ../infra/terraform.tfstate tf-out/ terradart plan ``` The plan must report *No changes. Your infrastructure matches the configuration.* Nothing is applied by a plan; a diff means a block was translated differently from how it was written. Compare that resource in `tf-out/main.tf.json` with your original, and [open an issue](https://github.com/nozomi-koborinai/terradart/issues/new/choose) with both. A tree of several roots plans one at a time: `terradart plan --env dev` plans `tf-out/envs/dev`. With `--merge-envs`, `--env` names a member of the generated `Env` enum — and giving its `runEnvironments` a `defaultEnv`, or setting `TERRADART_ENV` for your shell, lets `--env` be left out ([Environments](/docs/environments/#run-one-environment)). Where `MIGRATION.md` lists `terraform init && terraform plan` as a next step, `terradart plan` runs both for you. ## 7. Move the rest into Dart [Section titled “7. Move the rest into Dart”](#7-move-the-rest-into-dart) For each block `MIGRATION.md` lists as kept, once its reason no longer holds: 1. look its factory up in [Coverage](/docs/coverage/); 2. add it to the Stack with the **same local name**, so its address does not change; 3. delete the block from the sidecar, and its `addExternalBlock('')` line from the Stack when there is one; 4. run `terradart plan` and expect *No changes* again. A block whose type has no factory stays in the sidecar; the Stack and the sidecar plan and apply together. [What stays in Terraform](/docs/migrate-from-hcl/#what-stays-in-terraform) explains each reason. ## 8. Apply your next change [Section titled “8. Apply your next change”](#8-apply-your-next-change) From here, a change to the infrastructure is a change to the Dart: ```bash terradart plan terradart apply ``` The apply shows the plan and asks before it changes anything. A Stack with outputs can hand them to a Flutter or web client through a define file: add `addDartDefineOutput()`, and `terradart apply` writes it — see [Outputs in client apps](/docs/client-outputs/). ## 9. Move the state, when you want to [Section titled “9. Move the state, when you want to”](#9-move-the-state-when-you-want-to) A migration keeps your backend as it is. To move the state somewhere else later — a local file into a bucket, one bucket to another — change the Stack’s `backend:`, then: ```bash terradart state migrate ``` It synthesizes, names the backend the state moves from and to, asks, and runs the engine’s `init -migrate-state`. With several environments, pass `--env ` for each; without a terminal, `--auto-approve`. ## Next [Section titled “Next”](#next) * [Migrating from HCL](/docs/migrate-from-hcl/) — every flag, the sidecar, modules, `count` and `for_each`, `--merge-envs` * [Let an AI agent do it](/docs/start/ai-agent/#migrate-existing-terraform) — hand step 7 to a coding agent * [Environments](/docs/environments/) — `dev` and `prod` as a Dart enum after `--merge-envs` * [Writing arguments](/docs/arguments/) — the forms an argument takes, for the blocks you port by hand
# Start from an empty directory
> Create a TerraDart project with terradart init, then validate, plan and apply it with the terradart command — Dart is the only thing to install.
From an empty directory to applied infrastructure, with nothing to install but the Dart SDK. The `terradart` command writes the project, checks it, and runs the plan and the apply; it downloads a checksum-verified OpenTofu the first time it needs an engine. You need: * the Dart SDK, 3.10 or later; * credentials for your provider when you plan and apply — not before. The steps use Google Cloud and two projects, one per environment; [other providers](#other-providers) take a different flag at step 2. ## 1. Install the command [Section titled “1. Install the command”](#1-install-the-command) ```bash dart pub global activate terradart_cli terradart --help ``` `terradart` lands in `~/.pub-cache/bin`. If the second line is not found, add that directory to your `PATH`, as `dart pub global activate` says. ## 2. Create the project [Section titled “2. Create the project”](#2-create-the-project) ```bash terradart init my_app --provider google --env dev,prd --gcp-project dev=my-app-dev,prd=my-app-prd --backend local cd my_app ``` Use your own project IDs. `terradart init` writes `my_app/`, runs `dart pub get` in it, and prints what to run next: ```text Created my_app in my_app (google; environments dev, prd; local state): pubspec.yaml lib/env.dart lib/stack.dart bin/infra.dart .gitignore README.md AGENTS.md > dart pub get ... Next: cd my_app terradart plan --env dev ``` Run in a terminal without the flags, `terradart init my_app` asks for each of them — the providers, the environment names, each environment’s project, whether a bucket for the state already exists — and ends with the `terradart init ...` line that gives the same answers. Without a terminal it never asks and never chooses for you: `--provider`, `--env` and `--backend` (or `--state-bucket`) are required. An ID you leave out becomes a placeholder marked `TODO` in `lib/env.dart`. Every flag is in [The terradart command](/docs/cli/#creating-a-project). | File | Holds | | ------------------------ | ----------------------------------------------------------------------- | | `lib/env.dart` | the `Env` enum: each environment and its values | | `lib/stack.dart` | the Stack: the provider, the state backend, one resource and one output | | `bin/infra.dart` | the entry point the `terradart` command runs | | `README.md`, `AGENTS.md` | the next steps, and the rules a coding agent follows here | | `.gitignore` | `tf-out/` and `.terradart/`, which the command writes | ## 3. Read the Stack [Section titled “3. Read the Stack”](#3-read-the-stack) The environments are a Dart enum, and each member carries its values: lib/env.dart ```dart /// The environments of the Stack: `terradart plan --env ` /// picks one. Replace each placeholder marked TODO. enum Env { dev(projectId: 'my-app-dev', region: 'us-central1'), prd(projectId: 'my-app-prd', region: 'us-central1'); const Env({required this.projectId, required this.region}); /// The Google Cloud project the Stack deploys to. final String projectId; /// The default Google Cloud region. final String region; } ``` The Stack is the infrastructure of one environment. It enables the API its resource needs first — a new Google Cloud project has every API off, and the apply would fail with a 403 without `enableApis` — then adds a Pub/Sub topic and an output: lib/stack.dart ```dart import 'package:terradart_google/project.dart'; import 'package:terradart_google/provider.dart'; import 'package:terradart_google/pubsub.dart'; import 'package:terradart_time/terradart_time.dart'; import 'env.dart'; /// The infrastructure of one environment. Replace the example resources /// with your own. final class MyAppStack extends Stack { MyAppStack({required Env env}) : super( providers: [ GoogleProvider(project: env.projectId, region: env.region), const TimeProvider(), ], backend: const LocalBackend(), ) { // Enables the Pub/Sub API on the project and waits for it to propagate. final apis = enableApis([.pubsub]); final topic = add( GooglePubsubTopic( 'events', name: .literal('${env.name}-events'), dependsOn: apis, ), ); addOutput('events_topic_id', topic.id); } } ``` Replace the topic with your own resources as you go: every resource of the provider has a factory, listed in [Coverage](/docs/coverage/google/), and [Writing arguments](/docs/arguments/) says which form each argument takes. ## 4. The default environment [Section titled “4. The default environment”](#4-the-default-environment) `bin/infra.dart` hands the environments to `runEnvironments`, with the first one as `defaultEnv`, so that a command without `--env` runs against `dev`: bin/infra.dart ```dart import 'package:my_app/env.dart'; import 'package:my_app/stack.dart'; import 'package:terradart_core/terradart_core.dart'; /// Writes `tf-out//main.tf.json` for each environment, or the one /// `--env` names. `terradart plan`, `apply` and `destroy` run it first. /// `dev` is the environment those commands use when neither /// `--env` nor `TERRADART_ENV` names one. Future main(List args) => runEnvironments( args, Env.values, (env) => MyAppStack(env: env), defaultEnv: Env.dev, ); ``` A command now runs against the first of: `--env `, the `TERRADART_ENV` environment variable, `defaultEnv`, or the only environment when there is one. It prints which one and why, `env: dev (default)`. Delete the `defaultEnv` line to make every command name its environment. More in [Environments](/docs/environments/#run-one-environment). ## 5. Synth [Section titled “5. Synth”](#5-synth) ```bash terradart synth ``` ```text > dart run bin/infra.dart synthesized tf-out/dev/main.tf.json (dev) synthesized tf-out/prd/main.tf.json (prd) ``` Synth is plain Dart: it needs no credentials and no engine. A Stack that would not make valid Terraform — a reference to a resource it never added, a secret written as a literal — fails here, with every issue at once. ## 6. Validate [Section titled “6. Validate”](#6-validate) ```bash terradart validate ``` ```text env: dev (default) Downloading OpenTofu 1.13.1 (linux_amd64) from https://github.com/opentofu/opentofu/releases/download/... Installed OpenTofu 1.13.1 at ~/.cache/terradart/opentofu/1.13.1/linux_amd64/tofu Using OpenTofu 1.13.1 (no tofu or terraform on PATH; managed OpenTofu 1.13.1) > tofu init -backend=false -input=false (in tf-out/dev) ... > tofu validate (in tf-out/dev) Success! The configuration is valid. ``` `terradart validate` installs the providers and checks the configuration without credentials, a backend or state, so it is also the check for CI. The first command that needs an engine downloads OpenTofu once and keeps it in your user cache; a `tofu` or `terraform` already on your `PATH` is used instead. ## 7. Plan [Section titled “7. Plan”](#7-plan) Sign in to Google Cloud as an account that can create resources in the project, then plan: ```bash gcloud auth application-default login terradart plan ``` `terradart plan` synthesizes, runs `init` and `plan` in `tf-out/dev`, and shows what the apply will create: the Pub/Sub API, a wait for it to propagate, and the `dev-events` topic. ## 8. Apply [Section titled “8. Apply”](#8-apply) ```bash terradart apply ``` `defaultEnv` chose the environment, so the command first asks which one you mean: ```text env: dev (default) Apply environment "dev" (default)? Only "yes" is accepted: ``` Then the engine shows the plan and asks again. After the apply, the command writes the outputs to `.terradart/dart_defines.dev.json` for an app to build with ([step 10](#10-hand-the-outputs-to-an-app)). `--env prd` (or `TERRADART_ENV=prd`) applies the other environment. In CI, name the environment with `--env` and pass `--auto-approve`: without a terminal to answer the question, an apply stops before it changes anything (exit code 3). ```bash terradart apply --env prd ``` ## 9. Move the state to a bucket [Section titled “9. Move the state to a bucket”](#9-move-the-state-to-a-bucket) The state is a local file, `tf-out//terraform.tfstate`, kept out of git. Before anyone else applies, keep it in a bucket: create one, set the Stack’s `backend:` to `GcsBackend(bucket: 'my-app-tfstate', prefix: 'my_app/${env.name}')`, and move each environment’s state once: ```bash terradart state migrate --env dev terradart state migrate --env prd ``` Each run synthesizes, names the backend the state moves from and to, asks, and runs the engine’s `init -migrate-state` in that environment’s directory. Without a terminal, pass `--auto-approve`. ## 10. Hand the outputs to an app [Section titled “10. Hand the outputs to an app”](#10-hand-the-outputs-to-an-app) The Stack’s outputs reach your app typed, from a file the Stack generates: * a Flutter or web client builds with the define file `terradart apply` writes — [Start from an existing Flutter app](/docs/start/flutter-app/) walks through it; * a server reads them from its environment, which the Stack sets with `outputEnvironment()` — see [Outputs in client apps](/docs/client-outputs/#servers-and-scripts); * `terradart outputs --env prd` writes the define file from the applied state, for a build job that may read the state but never apply. ## Other providers [Section titled “Other providers”](#other-providers) Step 2 takes another `--provider` and its IDs; the steps after it are the same. | Provider | `terradart init` | Sign in | | ---------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | AWS | `terradart init my_app --provider aws --env dev,prd --aws-region us-east-1 --backend local` | the AWS SDK credential chain (`AWS_PROFILE`, or the access key variables) | | Cloudflare | `terradart init my_app --provider cloudflare --env dev,prd --cloudflare-account --backend local` | `CLOUDFLARE_API_TOKEN` | `--provider google,aws` puts both in one Stack. An Appwrite project needs one more step today: see [Appwrite](/docs/providers/appwrite/). ## Next [Section titled “Next”](#next) * [Environments](/docs/environments/) — each environment’s own project, state and define file * [The terradart command](/docs/cli/) — every command and flag * [Let an AI agent do it](/docs/start/ai-agent/) — the `AGENTS.md` this project came with, and the Agent Skill * [Getting started](/docs/getting-started/) — the same pieces built by hand, one at a time
# The terradart command
> Synthesize, plan and apply a Stack with one command, per environment, with OpenTofu or Terraform — downloaded and checksum-verified when neither is installed — and write the define file a Flutter client builds with.
`terradart` runs the whole loop of a TerraDart project: it runs the entry point that synthesizes the Stack, then `init` and `plan` or `apply` in the directory it wrote, then writes the [define file](/docs/client-outputs/) the client builds with. You never install or call Terraform yourself: `terradart` downloads a pinned OpenTofu release, checks it against the release’s SHA-256, and keeps it in your user cache. A `tofu` or `terraform` already on your `PATH` is used instead. ## Install [Section titled “Install”](#install) Once per machine: ```bash dart pub global activate terradart_cli terradart init ``` To pin the version per project, so everyone on it runs the same one, add it as a dev dependency of the infrastructure package instead: ```bash dart pub add --dev terradart_cli dart run terradart_cli:terradart apply ``` ## Commands [Section titled “Commands”](#commands) | Command | Runs | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `terradart init [dir]` | writes a new project (default `infra/`) and runs `dart pub get` in it; no project is required | | `terradart synth` | the entry point (`dart run bin/infra.dart`); arguments after `--` go to it | | `terradart validate` | synth, `init -backend=false`, `validate`: checks the configuration without credentials, a backend or state — the step for CI; arguments after `--` go to `validate` (`-- -json`) | | `terradart plan` | synth, `init`, `plan`; arguments after `--` go to the engine (`-- -target=...`); `--detailed-exitcode` exits 2 when the plan has changes | | `terradart apply` | synth, `init`, `apply`, then writes the define file; `--auto-approve` skips the prompt; `--dry-run` stops after `plan` | | `terradart destroy` | synth, `init`, `destroy`; `--auto-approve` skips the prompt; `--dry-run` stops after `plan -destroy` | | `terradart outputs` | synth, `init`, then writes the define file from the applied state — no plan, no apply; `--dry-run` prints the file and its keys and writes nothing | | `terradart state migrate` | synth, then `init -migrate-state`: moves the state to the backend the Stack now configures, after asking (`--auto-approve` skips the question, and is required when it cannot ask: no terminal, `--no-input`, CI or an agent) | | `terradart engine` | prints the engine binary it would run, downloading OpenTofu if that is the one | | `terradart migrate` | turns a Terraform tree into a Dart package. No project is required — it does not look for a `pubspec.yaml` | | `terradart skill install` / `update` / `status` | writes, updates and checks the [agent skill](#the-agent-skill) this CLI bundles | `--no-synth` reuses what the last synth wrote, `--project ` (`-C`) runs against another package, and `--engine tofu|terraform` or `--engine-path ` picks the engine for one run. `--json` prints one result object on stdout, and every exit code means one thing: [JSON and exit codes](#json-and-exit-codes). Every command’s `--help` ends with examples — the simplest use first, the one an agent runs last — and the topics to read next. `terradart help ` prints a short guide in the terminal, and `terradart help --list` lists them: `environments`, `outputs`, `backends`, `engines`, `migrate`, `exit-codes`, `json` and `agents`. Each ends with the page here that covers it in full. `terradart help migrate` prints the command’s usage, then the topic under `Guide:`. With `--json` the guide goes to stderr, like `--help`’s usage. ## Creating a project [Section titled “Creating a project”](#creating-a-project) `terradart init` writes a project that plans as it is — `pubspec.yaml`, an `Env` enum in `lib/env.dart`, a Stack with one resource and one output in `lib/stack.dart`, `bin/infra.dart`, a `.gitignore`, a `README.md` and an `AGENTS.md`, and with `--agent-skill` the [agent skill](#the-agent-skill) — then runs `dart pub get` in it (`--no-pub-get` skips that): ```bash terradart init --provider google --env dev,prd --gcp-project dev=myapp-dev,prd=myapp-prd --state-bucket myapp-tfstate terradart init --provider aws --env dev,stg,prd --aws-region prd=eu-west-1 --aws-account prd=123456789012 --backend local terradart init --provider cloudflare --defaults --cloudflare-account 0123abcd --agent-skill terradart init --dry-run --provider appwrite --defaults # lists the files, writes nothing ``` | Flag | Takes | Default | | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `--provider`, `-p` | `google`, `aws`, `cloudflare`, `appwrite`, comma-separated or repeated | required without a terminal | | `--env`, `-e` | lowerCamelCase environment names, comma-separated or repeated | required without a terminal; `--defaults`: `dev,prd` | | `--gcp-project`, `--aws-region`, `--cloudflare-account`, `--appwrite-endpoint`, `--appwrite-project` | `=` pairs, or one value for every environment | a placeholder marked `TODO` | | `--aws-account` | the same; the provider then refuses credentials of another account | any account | | `--state-bucket` | the bucket that already holds the state, one name or `=` pairs; the backend follows the provider — google `gcs`, aws `s3`, cloudflare `r2` | | | `--backend` | `local`, `gcs`, `s3`, `r2` (Cloudflare R2, with `--provider cloudflare`) | required without a terminal unless `--state-bucket`; `--defaults`: `local` | | `--defaults` | take `--env dev,prd`, `--backend local`, placeholder IDs and the Flutter wiring for what the flags leave out; never the providers | | | `--[no-]flutter` | wiring to the Flutter app in the current directory | on when there is one | | `--[no-]agent-skill` | write the agent skill this CLI bundles into the project | asked in a terminal (yes); off without one or with `--defaults` | | `--agents` | where `--agent-skill` writes it: `agents`, `claude`, `cursor`, `windsurf`, `copilot`, `all`; implies `--agent-skill` | `agents,claude` | | `--dry-run` | list the files, write nothing | | | `--force` | overwrite existing files, scaffold next to existing Terraform | | In a terminal, every flag left out is a question: the providers, the environments, each environment’s IDs (a blank answer leaves a placeholder marked `TODO`), then *Do you already have a bucket for Terraform state?* — yes asks for its name, one for every environment or one each, and takes the kind from the provider (asking when the providers do not settle it); no keeps the state local — and last whether to install the agent skill. Without the skill, `AGENTS.md` says how to add it: `terradart skill install`, or the pinned `npx skills add` line. The run ends with `Re-run with: terradart init ...`, the same answers as flags. Without a terminal it never asks, and never picks the providers, the environments or the state backend for you: a run that leaves out any of `--provider`, `--env` and `--backend` (or `--state-bucket`) fails with exit 64, names every missing flag, and prints a command to edit. `--defaults` accepts `dev,prd` and local state; the IDs it leaves out become placeholders, and the defaults it took are printed. A bucket’s state sits under `/`, so one bucket serves every environment. Local state moves to a bucket later with `terradart state migrate`; `terradart init` only scaffolds. Inside a Flutter app (a `pubspec.yaml` that depends on `flutter` in the current directory), the Stack’s `appExports` writes the reader the app imports to the app’s `lib/generated/infra.g.dart`, the Stack declares `addDartDefineOutput()`, and the steps printed end with `flutter run --dart-define-from-file=infra/.terradart/dart_defines..json`. A working or target directory holding `*.tf` or `*.tf.json` files, at any depth, gets no scaffold: `terradart init` names the directories that hold them (`Found Terraform in envs/dev, modules/net.`), prints the `terradart migrate --report` and `terradart migrate` commands for it and exits 64 — in a terminal it offers to run the report first. `--force` scaffolds anyway. Picking `appwrite` writes `terradart: engine: terraform` into `pubspec.yaml`: an Appwrite project plans and applies with Terraform on your `PATH` ([The engine](#the-engine)). ## The entry point [Section titled “The entry point”](#the-entry-point) `bin/infra.dart` builds the Stack and hands it to `runStack`, which writes `tf-out/` and tells `terradart` what it wrote: lib/orders\_stack.dart ```dart import 'package:terradart_google/provider.dart'; final class OrdersStack extends Stack { OrdersStack({required String projectId}) : super(providers: [GoogleProvider(project: projectId)]) { addOutput('project', .literal(projectId)); } } ``` bin/single.dart ```dart import 'package:my_app/orders_stack.dart'; import 'package:terradart_core/terradart_core.dart'; Future main(List args) => runStack(args, () => OrdersStack(projectId: 'acme-dev')); ``` An entry point that writes `tf-out/` itself (`await OrdersStack(...).writeTo('tf-out')`) works too; `terradart` then looks for `tf-out/main.tf.json`, or the directory `terradart.out` names. ## Environments [Section titled “Environments”](#environments) With an entry point that calls `runEnvironments`, `validate`, `plan`, `apply`, `destroy` and `outputs` run against one member of the project’s environment enum: the one `--env ` (`-e`) names, else the `TERRADART_ENV` environment variable, else the `defaultEnv` the entry point gives `runEnvironments`, else the only member; with none of these the command asks on a terminal, and otherwise stops with the names and the command to run. When `TERRADART_ENV` or `defaultEnv` chose it, `apply` and `destroy` ask first on a terminal unless `--auto-approve` is set. `terradart synth` without `--env` writes every environment. Each environment gets its own Terraform directory (`tf-out/` unless `runEnvironments` says otherwise) and define file (`.terradart/dart_defines..json`), and a name that is not a member stops before anything runs. `--workspace` and `--backend-config` override and extend, for one run, the workspace and backend settings `runEnvironments` gives an environment. How to declare the enum, keep each environment’s state apart, and build each client with its define file: [Environments](/docs/environments/). ## Terminals, CI and agents [Section titled “Terminals, CI and agents”](#terminals-ci-and-agents) `terradart` asks a question only on a terminal (stdin and stdout both one), and never with `--no-input`, `TERRADART_NO_INPUT=1`, `CI` set, or in an AI agent’s shell (`AI_AGENT`, `CURSOR_AGENT`, `CLAUDECODE`, `GEMINI_CLI`, `CODEX_SANDBOX`, `CODEX_THREAD_ID`, `OPENCODE`). Where it would ask, it uses the flag instead or stops and names it: | Question on a terminal | Without one | | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | | Which environment, when `--env`, `TERRADART_ENV`, `defaultEnv` and a single member leave it open | exit code 64, the names, and `Next: terradart plan --env ` | | `apply` / `destroy` approval, and `Apply environment "dev" (default)?` | exit code 3 before `init` unless `--auto-approve` | | `state migrate` copying the state | exit code 3 unless `--auto-approve` | | running one engine on a state the other wrote | exit code 3; `--engine tofu\|terraform` decides | An error a flag would fix prints the flag’s choices and the command to run next: ```text terradart: --env is required: bin/infra.dart declares dev, stg, prd and no defaultEnv. Pass --env, set TERRADART_ENV, or give runEnvironments a defaultEnv. Choices: dev, stg, prd Next: terradart plan --env dev ``` The engine gets `-input=false` whenever nobody can answer. `--quiet` (`-q`) leaves out the values terradart picks by itself (`env: dev (default)`, `Using OpenTofu ...`); errors and warnings still print. ## JSON and exit codes [Section titled “JSON and exit codes”](#json-and-exit-codes) `--json` prints one JSON object on stdout when the command ends — and nothing else there: progress, `--help`, the entry point’s and the engine’s output go to stderr. It implies `--no-input`, and it goes before or after the command (`terradart --json plan`, `terradart plan --json`). `terradart migrate` is the exception: its `--json`, before or after `migrate`, is the migrator’s own report (`terradart migrate --report --json`). ```bash terradart plan --env dev --json --detailed-exitcode ``` ```json { "schemaVersion": 1, "command": "plan", "ok": true, "exitCode": 2, "env": {"name": "dev", "source": "flag"}, "engine": {"kind": "tofu", "version": "1.13.1", "source": "path", "path": "/usr/local/bin/tofu"}, "outDir": "tf-out/dev", "plan": {"add": 3, "change": 0, "destroy": 0, "replace": 0}, "notices": [], "next": ["terradart apply --env dev"] } ``` | Field | Holds | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `schemaVersion` | `1`; raised only for a change that breaks a reader | | `command`, `ok`, `exitCode` | the command path (`state migrate`), whether it succeeded, and the exit code | | `env` | the environment and why: `flag`, `variable` (`TERRADART_ENV`), `default`, `only`, `prompt` | | `engine` | `kind` (`tofu`, `terraform`), `version`, `path`, and why: `engine_path`, `setting` (`--engine` or `pubspec.yaml`), `state` (the engine that last applied it), `path`, `managed` (the OpenTofu download) | | `outDir` | the Terraform directory | | `plan` | `plan`: the resource changes of the saved plan as `show -json` lists them; a replacement counts once, as `replace` | | `defineFile`, `keys` | `apply`, `outputs`: the define file and its keys — never the values, which may be secrets | | `notices` | one-line warnings with a `code`: `skill_outdated` / `skill_newer` when the project’s agent skill is older or newer than this CLI | | `error` | on failure: `code` from the table, `message`, and when they apply `flag`, `choices`, `engineExitCode` | | `dryRun` | `true` when `--dry-run` stopped the command before it changed anything | | `next` | the commands to run next: the failed one with the flag it lacks, `terradart apply` after a plan with changes, or the command without `--dry-run` | | Exit code | `error.code` | Means | | --------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 0 | | success | | 1 | `internal` | anything else: a bug, a cancelled question, a malformed engine output | | 2 | | `plan --detailed-exitcode` found changes — not a failure, `ok` is `true` | | 3 | `input_required` | an answer nobody can give: `--auto-approve`, or `--engine` on a state the other engine wrote | | 4 | `skill_drift` | `skill status --check`: the agent skill is missing, older, newer or edited | | 5 | `local_edits` | `skill update` kept a skill with local edits (`--force` overwrites it) | | 10 | `synth_failed` | the entry point exited non-zero | | 11 | `engine_unavailable` | no engine: not on `PATH`, a missing `engine_path`, or the OpenTofu download or its SHA-256 check failed | | 12 | `engine_failed` | `init`, `plan`, `apply`, `validate`, `output` … failed; `error.engineExitCode` is the engine’s own code | | 64 | `usage` / `missing_flag` | a wrong flag or argument, or a required one is missing (`--env`, `--force`) | | 65 | `project_config` | the `terradart:` section of `pubspec.yaml` is wrong, an `--env` the entry point does not declare, a Terraform directory or dart-define output that is not there | | 66 | `no_project` | no `pubspec.yaml` in the directory or above it | ## Moving the state [Section titled “Moving the state”](#moving-the-state) When the Stack changes its backend — `LocalBackend()` to `GcsBackend(...)`, one bucket to another, or back to a local file — the state has to follow it once: ```bash terradart state migrate --env prod ``` It synthesizes, names the full source and target configuration (the bucket and prefix, not only `gcs` → `gcs`), asks, and runs the engine’s `init -migrate-state` in that environment’s directory, with its `backendConfig`. Without a terminal it stops unless `--auto-approve` is given. Run it before the next `plan` or `apply` of that environment: those reconfigure the directory onto the new backend without copying. When several environments share the directory and pass `backendConfig`, the copy is the state of the environment that last initialized it. If that was another environment, or TerraDart has no record, it stops and tells you to run `terradart plan --env ` first; `--auto-approve` does not skip that. ## The define file [Section titled “The define file”](#the-define-file) After `apply`, a Stack with [`addDartDefineOutput()`](/docs/client-outputs/) gets its define file written to `.terradart/dart_defines.json` — `.terradart/dart_defines..json` with `--env` — and `terradart` prints the line that builds with it: ```text Wrote 2 dart-defines to .terradart/dart_defines.stg.json flutter run --dart-define-from-file=.terradart/dart_defines.stg.json flutter build --dart-define-from-file=.terradart/dart_defines.stg.json ``` `terradart outputs --env stg` writes the same file from the applied state without planning or applying: the command for a client’s build job, which can read the state but not change it. `.terradart/` gets its own `.gitignore`, so the file never reaches git. A Stack with several define outputs takes `--define-output `; `--define-file ` writes elsewhere. When the app starts without the defines, the generated reader says how to get them: ```text Dart define API_URL (Terraform output "api_url") is not set. Build the app with --dart-define-from-file=.terradart/dart_defines.json, which `terradart apply` and `terradart outputs` write (with --env : .terradart/dart_defines..json). ``` ## The engine [Section titled “The engine”](#the-engine) `terradart` picks the first of: 1. `--engine-path`, `--engine`, or `engine_path` / `engine` in `pubspec.yaml`; 2. the engine that last applied this environment’s state, as recorded in `.terradart/engines.json`; 3. `tofu` on `PATH`; 4. `terraform` on `PATH`; 5. OpenTofu downloaded from its GitHub release. The download is the release pinned in `terradart_cli` (`opentofu_version` in `pubspec.yaml` picks another), for Linux, macOS and Windows on amd64 and arm64. Its archive is checked against the SHA-256 checksum `terradart_cli` ships for the pinned release, or against the release’s `SHA256SUMS` for another; a mismatch is deleted and stops the run. The binary is kept in `~/.cache/terradart` on Linux, `~/Library/Caches/terradart` on macOS and `%LOCALAPPDATA%\terradart` on Windows (`TERRADART_CACHE_DIR` moves it; `TERRADART_OPENTOFU_MIRROR` downloads from a mirror of the release layout). Appwrite currently needs Terraform. The `appwrite/appwrite` provider is published to the Terraform registry only, which OpenTofu cannot install from, so a Stack that requires it runs on `terraform` from `PATH` whatever steps 2–5 would pick. When there is none, or `--engine tofu`, `engine: tofu` or a `tofu` `engine_path` asks for OpenTofu, `plan`, `apply`, `destroy` and `outputs` stop before `init` with a message that says so. Install [Terraform](https://developer.hashicorp.com/terraform/install) and run with `--engine terraform`, or set `engine: terraform` in `pubspec.yaml`. State written by one engine is not always readable by another: Terraform 1.6 and later and OpenTofu have diverged. When the engine about to run differs from the one that last applied the state, or is older, `terradart` warns before it runs. Without that record — right after a migration, or in a fresh clone, since `.terradart/` is not committed — `plan`, `apply` and `destroy` read the state itself: the local state file before `init`, and a remote backend’s state (`state pull`) after it. Its provider addresses say which engine wrote it (`registry.terraform.io` or `registry.opentofu.org`), as does a `terraform_version` older than OpenTofu. When the other engine wrote it, `terradart` warns; and when it picked the engine by itself (step 3 to 5), it asks before running on a terminal, and without one stops with the flag that decides: ```text terradart: Stopped before running OpenTofu on a state Terraform wrote. Pass --engine terraform to keep Terraform (or set terradart.engine: terraform in pubspec.yaml), or --engine tofu to move the state to OpenTofu. ``` `terradart migrate` writes `engine: terraform` into the package it generates, so a migrated project keeps the engine its state came from. ## The agent skill [Section titled “The agent skill”](#the-agent-skill) `terradart_cli` bundles the [TerraDart Agent Skill](/docs/start/ai-agent/) of its own release, so a coding agent learns the CLI it runs and not the one on `main`. `terradart skill install` writes it to the project (the directory holding `pubspec.yaml`, or `--project `): ```text .agents/skills/terradart/SKILL.md Cursor, Codex, Gemini CLI, GitHub Copilot and most other agents .claude/skills/terradart/SKILL.md Claude Code, which does not read .agents/skills ``` `--agents` picks other directories, comma-separated: `agents`, `claude`, `cursor` (`.cursor/skills`), `windsurf` (`.windsurf/skills`), `copilot` (`.github/skills`), or `all`. The files are copies, not symlinks; commit them with the project. Each copy records, under `metadata:` in its front matter, the CLI version that wrote it and the SHA-256 of its content (leaving those two lines out): ```yaml metadata: terradart-version: "0.34.0" terradart-sha256: "" ``` | Command | Does | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `terradart skill status` | shows each copy: `current`, `marker-only` (same content, older version), `outdated`, `newer` (written by a newer CLI), `edited`, `missing`, or `foreign` (no marker) | | `terradart skill status --check` | exits 4 when a copy is missing, outdated, newer, edited or foreign — for CI | | `terradart skill update` | rewrites every installed copy with the bundled one. A copy that is edited, foreign or newer is left alone (exit 5) unless `--force` | | `terradart skill update --dry-run` | prints the diff and writes nothing (`install` takes it too) | When a copy in the project is older than the CLI, every other command prints one line on stderr (`TERRADART_NO_SKILL_NOTICE=1` turns it off): ```text terradart: the terradart agent skill in .agents/skills is 0.33.0; this CLI is 0.34.0. Run: terradart skill update ``` Without the CLI, the [`skills` CLI](https://github.com/vercel-labs/skills) installs the same file. Pin it to the release tag, since it fetches `main` otherwise: ```bash npx skills add nozomi-koborinai/terradart#v0.35.0 --skill terradart ``` It is byte for byte the file `terradart skill install` writes, so `terradart skill status` reads it too, and points at `npx skills update terradart -p -y` when `skills-lock.json` records it. ## `pubspec.yaml` [Section titled “pubspec.yaml”](#pubspecyaml) Every key is optional. ```yaml terradart: entrypoint: bin/infra.dart # the Dart file whose main synthesizes out: tf-out # where an entry point without runStack writes engine: tofu # or terraform engine_path: tools/tofu # a binary of your own opentofu_version: 1.13.1 # the release to download dart_defines: output: dart_defines # the addDartDefineOutput name file: ../app/dart_defines.json # instead of .terradart/