diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 5dc9501e3..a854a6a46 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -5,6 +5,7 @@ - [Developer Environment](./framework/developer_environment/overview.md) - [Configuration](./framework/developer_environment/toml.md) - [External Environment Configuration](./framework/developer_environment/toml_external_env_integration.md) + - [Extending a Generated Environment (AGENTS.md)](./framework/developer_environment/agents.md) - [Basic Usage](./framework/getting_started.md) - [Getting Started](./framework/getting_started.md) - [Advanced Usage](./framework/configuration.md) diff --git a/book/src/framework/developer_environment/agents.md b/book/src/framework/developer_environment/agents.md new file mode 100644 index 000000000..d82d99226 --- /dev/null +++ b/book/src/framework/developer_environment/agents.md @@ -0,0 +1,94 @@ +# Extending a Generated Environment (AGENTS.md) + +Every environment generated by `ctf gen env` ships an `AGENTS.md` file at its root that teaches +AI coding agents how the environment is structured and how to extend it following the existing +patterns. + +This page reproduces that guide for reference — useful if your environment was generated before +`AGENTS.md` was added, or if you just want to read it here. Names below use the generator defaults +(`pcli` for the CLI, `myproduct` for the product); substitute your own if you chose different ones. + +> If you work with an AI agent, point it at `devenv/AGENTS.md`. If that file is missing (older +> environment), copy the content below into `devenv/AGENTS.md`, replacing `pcli`/`myproduct` with +> your CLI and product names. + +--- + +A generated Chainlink developer environment composes Docker containers (blockchains, CL node sets, +fakes, custom services) and exposes an interactive CLI called `pcli` with autocompletion. Keep +changes minimal and idiomatic. + +## Layout + +| Path | What it is | +|------|------------| +| `cmd/pcli/pcli.go` | CLI commands (cobra). Add commands here. | +| `cmd/pcli/completion.go` | Shell autocompletion suggestions. Mirror every CLI command here. | +| `environment.go` | Wiring: reads `Cfg` from TOML, spins up all Docker components in order. | +| `services/svc.go` | Template for a custom Docker component (container). Copy this to add services. | +| `products/myproduct/configurator.go` | Product logic: CL node configs, secrets, jobs & contracts. | +| `products/myproduct/*.toml` | Product config profiles (`basic.toml`, `soak.toml`). | +| `fakes/main.go` | HTTP fakes for 3rd-party dependencies (separate Docker image + go.mod). | +| `tests/myproduct/{func,perf}_test.go` | System-level functional / performance (WASP) tests. | +| `env.toml` | Default environment config: blockchains, nodesets, services, products. | +| `Justfile` | Build recipes: `just cli`, `just build-fakes`. | +| `dashboards/myproduct.json` | Grafana dashboard, auto-uploaded on `up`. | + +## Build & run + +```bash +just cli # rebuild the pcli binary after editing cmd/pcli/* +pcli sh # enter the interactive shell (autocompletion) +up # spin up env (inside shell); or: pcli up +test func # run functional tests; test perf for performance +``` + +Config is selected via the `CTF_CONFIGS` env var, a comma-separated list of TOML files +(e.g. `env.toml,products/myproduct/basic.toml`). `up`/`restart` set this for you. + +## Add a CLI command + +Commands are [cobra](https://github.com/spf13/cobra) commands in `cmd/pcli/pcli.go`. + +1. Declare a `*cobra.Command` var with `Use`, `Aliases`, `Short`, and a `RunE` func. +2. Register it in `init()` with `rootCmd.AddCommand(myCmd)` (or `parent.AddCommand(child)` for subcommands; set flags via `parent.PersistentFlags()` — see `bsCmd`/`obsCmd`). +3. **Always** add a matching entry in `cmd/pcli/completion.go`: + - top-level command → add to `getCommands()` + - subcommands/args → add a `case "":` in `getSubCommands()` +4. `just cli` to rebuild. + +Business logic lives in the `framework` package (e.g. `framework.ObservabilityUp()`, +`framework.BlockScoutUp()`); the CLI is a thin wrapper. Prefer reusing framework helpers. + +## Wire up a new Docker component (service) + +1. Copy `services/svc.go` to `services/.go`. Each service = an `Input` struct (with + `Default()`), an `Output` struct (with an `out` TOML tag), and a `New(in)` deploy func + using `testcontainers`. Use `framework.DefaultTCLabels()`, `framework.DefaultNetworkName`, + and static host ports. +2. Add the input field to the `Cfg` struct in `environment.go` (with a `toml` tag). +3. Call `New(in.)` inside `NewEnvironment()`, alongside `services.NewService`. +4. Add its config block to `env.toml`. +5. A deployed service must guard on `in == nil || in.Out != nil` to stay idempotent (see `NewService`). + +## Add a product + +1. Copy `products/myproduct/` → `products//` and `tests/myproduct/` → `tests//`. +2. Implement the `Product` interface (`interface.go`) in `configurator.go`: + `Load`, `Store`, `GenerateNodesConfig`, `GenerateNodesSecrets`, `ConfigureJobsAndContracts`. +3. Register it in `newProduct(name)` in `environment.go` with a new `case`. +4. Add a `[[products]]` entry (with `name`, `instances`) in `env.toml`. + Multiple instances → your `ConfigureJobsAndContracts` must handle `productIdx`. + +## Add / change fakes (3rd-party HTTP) + +Edit `fakes/main.go` (its own Docker image + `go.mod`), then `just build-fakes`. +Tests reach fakes via the `fake_server` output; see `tests/myproduct/func_test.go`. + +## Conventions (do not break) + +- Docker components: static ports, `framework.DefaultTCLabels()`, `framework.DefaultNetworkName`. +- Config is TOML-driven; every new component/product needs a `Cfg`/`env.toml` entry. +- Idempotent deploys: skip if output already set. +- Every CLI command has a completion entry. +- Address any `// TODO` comments the generator left before shipping. diff --git a/book/src/framework/developer_environment/overview.md b/book/src/framework/developer_environment/overview.md index 935b49db6..5238d9719 100644 --- a/book/src/framework/developer_environment/overview.md +++ b/book/src/framework/developer_environment/overview.md @@ -14,7 +14,10 @@ ctf gen -h ctf gen env --cli pcli --product-name MyProduct --output-dir devenv --nodes 4 ``` -Follow further instructions in `devenv/README.md` +Follow further instructions in `devenv/README.md`. + +Use `devenv/AGENTS.md` if you work with AI. You can also read that guide here: +[Extending a Generated Environment](./agents.md) (handy for environments generated before `AGENTS.md` was added). ## Remote Environment diff --git a/book/src/framework/generate.md b/book/src/framework/generate.md index 00246c36d..8ffcbf30d 100644 --- a/book/src/framework/generate.md +++ b/book/src/framework/generate.md @@ -11,6 +11,8 @@ ctf gen env --cli pcli --product-name MyProduct --output-dir devenv --nodes 4 Follow further instructions in `devenv/README.md` +The generated environment also ships an `devenv/AGENTS.md` — a short guide that teaches AI coding agents how the environment is structured and how to extend it (add CLI commands + completion, wire up new Docker components, add products/fakes) following the existing patterns. + # Generating Infrastructure Testing Template If you have Chainlink infrastructure already deployed it's useful to generate a workload + chaos suite template. diff --git a/framework/examples/myproject/chaos/chaos.toml b/framework/examples/myproject/chaos/chaos.toml index 5ef974176..fa08740a6 100644 --- a/framework/examples/myproject/chaos/chaos.toml +++ b/framework/examples/myproject/chaos/chaos.toml @@ -17,4 +17,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/chaos/chaos_gas.toml b/framework/examples/myproject/chaos/chaos_gas.toml index 5ef974176..fa08740a6 100644 --- a/framework/examples/myproject/chaos/chaos_gas.toml +++ b/framework/examples/myproject/chaos/chaos_gas.toml @@ -17,4 +17,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/chaos/chaos_reorg.toml b/framework/examples/myproject/chaos/chaos_reorg.toml index a06e827ae..32f4de043 100644 --- a/framework/examples/myproject/chaos/chaos_reorg.toml +++ b/framework/examples/myproject/chaos/chaos_reorg.toml @@ -23,4 +23,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/fake.toml b/framework/examples/myproject/fake.toml index 59452be0b..147931bf4 100644 --- a/framework/examples/myproject/fake.toml +++ b/framework/examples/myproject/fake.toml @@ -16,4 +16,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/fake_docker.toml b/framework/examples/myproject/fake_docker.toml index 1e6b65c05..7a8d3952d 100644 --- a/framework/examples/myproject/fake_docker.toml +++ b/framework/examples/myproject/fake_docker.toml @@ -17,4 +17,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/fork_plus_offchain.toml b/framework/examples/myproject/fork_plus_offchain.toml index 4051ca2f1..b7113a27a 100644 --- a/framework/examples/myproject/fork_plus_offchain.toml +++ b/framework/examples/myproject/fork_plus_offchain.toml @@ -18,4 +18,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/performance_baseline.toml b/framework/examples/myproject/performance_baseline.toml index 89731a278..325fd7cdb 100644 --- a/framework/examples/myproject/performance_baseline.toml +++ b/framework/examples/myproject/performance_baseline.toml @@ -16,4 +16,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/quick-deploy.toml b/framework/examples/myproject/quick-deploy.toml index 7ad9145f7..911934141 100644 --- a/framework/examples/myproject/quick-deploy.toml +++ b/framework/examples/myproject/quick-deploy.toml @@ -15,4 +15,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/scalability.toml b/framework/examples/myproject/scalability.toml index e302d3265..4e0b512c0 100644 --- a/framework/examples/myproject/scalability.toml +++ b/framework/examples/myproject/scalability.toml @@ -17,4 +17,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/smoke.toml b/framework/examples/myproject/smoke.toml index e16bb8ac3..4cda14a2d 100644 --- a/framework/examples/myproject/smoke.toml +++ b/framework/examples/myproject/smoke.toml @@ -17,4 +17,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" \ No newline at end of file + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" \ No newline at end of file diff --git a/framework/examples/myproject/smoke_import.toml b/framework/examples/myproject/smoke_import.toml index 3085a18ec..ee9f024e6 100644 --- a/framework/examples/myproject/smoke_import.toml +++ b/framework/examples/myproject/smoke_import.toml @@ -14,4 +14,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" \ No newline at end of file + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" \ No newline at end of file diff --git a/framework/examples/myproject/smoke_limited_resources.toml b/framework/examples/myproject/smoke_limited_resources.toml index b82c519d6..04111f0bd 100644 --- a/framework/examples/myproject/smoke_limited_resources.toml +++ b/framework/examples/myproject/smoke_limited_resources.toml @@ -24,7 +24,7 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [nodesets.node_specs.node.resources] cpus = 1 diff --git a/framework/examples/myproject/smoke_sui.toml b/framework/examples/myproject/smoke_sui.toml index 11300a692..81f153bfa 100644 --- a/framework/examples/myproject/smoke_sui.toml +++ b/framework/examples/myproject/smoke_sui.toml @@ -17,24 +17,24 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/upgrade.toml b/framework/examples/myproject/upgrade.toml index fc600814a..7e41ece1a 100644 --- a/framework/examples/myproject/upgrade.toml +++ b/framework/examples/myproject/upgrade.toml @@ -17,24 +17,24 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/upgrade_multi.toml b/framework/examples/myproject/upgrade_multi.toml index af28ffb85..47bb26ac7 100644 --- a/framework/examples/myproject/upgrade_multi.toml +++ b/framework/examples/myproject/upgrade_multi.toml @@ -19,7 +19,7 @@ [[nodeset_a.node_specs]] [nodeset_a.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [nodeset_b] http_port_range_start = 10100 @@ -34,4 +34,4 @@ [[nodeset_b.node_specs]] [nodeset_b.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject/upgrade_multi_each.toml b/framework/examples/myproject/upgrade_multi_each.toml index 1e41dc9da..a19bd15a9 100644 --- a/framework/examples/myproject/upgrade_multi_each.toml +++ b/framework/examples/myproject/upgrade_multi_each.toml @@ -19,27 +19,27 @@ [[nodeset_a.node_specs]] [nodeset_a.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodeset_a.node_specs]] [nodeset_a.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodeset_a.node_specs]] [nodeset_a.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodeset_a.node_specs]] [nodeset_a.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodeset_a.node_specs]] [nodeset_a.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [nodeset_b] http_port_range_start = 10000 @@ -54,24 +54,24 @@ [[nodeset_b.node_specs]] [nodeset_b.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodeset_b.node_specs]] [nodeset_b.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodeset_b.node_specs]] [nodeset_b.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodeset_b.node_specs]] [nodeset_b.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" [[nodeset_b.node_specs]] [nodeset_b.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.16.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/examples/myproject_cll/jd_nodeset.toml b/framework/examples/myproject_cll/jd_nodeset.toml index 82fdaf782..3259f0149 100644 --- a/framework/examples/myproject_cll/jd_nodeset.toml +++ b/framework/examples/myproject_cll/jd_nodeset.toml @@ -24,4 +24,4 @@ [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:v2.17.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" diff --git a/framework/tmpl_gen_env.go b/framework/tmpl_gen_env.go index 6dcb6b3cb..143926473 100644 --- a/framework/tmpl_gen_env.go +++ b/framework/tmpl_gen_env.go @@ -111,6 +111,89 @@ Is simple, just copy [{{ .ProductName }}](./products/{{ .ProductName }})) and [t See [env.toml](./env.toml) comments. +` + // AgentsTmpl is an AGENTS.md guide that teaches AI agents how to extend this devenv + AgentsTmpl = `# AGENTS.md — how to extend this developer environment + +This is a **code-generated Chainlink developer environment** (generated by ` + "`" + `ctf gen env` + "`" + `). +It composes Docker containers (blockchains, CL node sets, fakes, custom services) and exposes +an interactive CLI called **` + "`" + `{{ .CLIName }}` + "`" + `** with autocompletion. This file tells an AI agent how to +understand and extend it **following the existing patterns**. Keep changes minimal and idiomatic. + +## Layout + +| Path | What it is | +|------|------------| +| ` + "`" + `cmd/{{ .CLIName }}/{{ .CLIName }}.go` + "`" + ` | CLI commands (cobra). Add commands here. | +| ` + "`" + `cmd/{{ .CLIName }}/completion.go` + "`" + ` | Shell autocompletion suggestions. Mirror every CLI command here. | +| ` + "`" + `environment.go` + "`" + ` | Wiring: reads ` + "`" + `Cfg` + "`" + ` from TOML, spins up all Docker components in order. | +| ` + "`" + `services/svc.go` + "`" + ` | Template for a custom Docker component (container). Copy this to add services. | +| ` + "`" + `products/{{ .ProductName }}/configurator.go` + "`" + ` | Product logic: CL node configs, secrets, jobs & contracts. | +| ` + "`" + `products/{{ .ProductName }}/*.toml` + "`" + ` | Product config profiles (` + "`" + `basic.toml` + "`" + `, ` + "`" + `soak.toml` + "`" + `). | +| ` + "`" + `fakes/main.go` + "`" + ` | HTTP fakes for 3rd-party dependencies (separate Docker image + go.mod). | +| ` + "`" + `tests/{{ .ProductName }}/{func,perf}_test.go` + "`" + ` | System-level functional / performance (WASP) tests. | +| ` + "`" + `env.toml` + "`" + ` | Default environment config: blockchains, nodesets, services, products. | +| ` + "`" + `Justfile` + "`" + ` | Build recipes: ` + "`" + `just cli` + "`" + `, ` + "`" + `just build-fakes` + "`" + `. | +| ` + "`" + `dashboards/{{ .ProductName }}.json` + "`" + ` | Grafana dashboard, auto-uploaded on ` + "`" + `up` + "`" + `. | + +## Build & run + +` + "```" + `bash +just cli # rebuild the {{ .CLIName }} binary after editing cmd/{{ .CLIName }}/* +{{ .CLIName }} sh # enter the interactive shell (autocompletion) +up # spin up env (inside shell); or: {{ .CLIName }} up +test func # run functional tests; test perf for performance +` + "```" + ` + +Config is selected via the ` + "`" + `CTF_CONFIGS` + "`" + ` env var, a comma-separated list of TOML files +(e.g. ` + "`" + `env.toml,products/{{ .ProductName }}/basic.toml` + "`" + `). ` + "`" + `up` + "`" + `/` + "`" + `restart` + "`" + ` set this for you. + +## Add a CLI command + +Commands are [cobra](https://github.com/spf13/cobra) commands in ` + "`" + `cmd/{{ .CLIName }}/{{ .CLIName }}.go` + "`" + `. + +1. Declare a ` + "`" + `*cobra.Command` + "`" + ` var with ` + "`" + `Use` + "`" + `, ` + "`" + `Aliases` + "`" + `, ` + "`" + `Short` + "`" + `, and a ` + "`" + `RunE` + "`" + ` func. +2. Register it in ` + "`" + `init()` + "`" + ` with ` + "`" + `rootCmd.AddCommand(myCmd)` + "`" + ` (or ` + "`" + `parent.AddCommand(child)` + "`" + ` for subcommands; set flags via ` + "`" + `parent.PersistentFlags()` + "`" + ` — see ` + "`" + `bsCmd` + "`" + `/` + "`" + `obsCmd` + "`" + `). +3. **Always** add a matching entry in ` + "`" + `cmd/{{ .CLIName }}/completion.go` + "`" + `: + - top-level command → add to ` + "`" + `getCommands()` + "`" + ` + - subcommands/args → add a ` + "`" + `case "":` + "`" + ` in ` + "`" + `getSubCommands()` + "`" + ` +4. ` + "`" + `just cli` + "`" + ` to rebuild. + +Business logic lives in the ` + "`" + `framework` + "`" + ` package (e.g. ` + "`" + `framework.ObservabilityUp()` + "`" + `, +` + "`" + `framework.BlockScoutUp()` + "`" + `); the CLI is a thin wrapper. Prefer reusing framework helpers. + +## Wire up a new Docker component (service) + +1. Copy ` + "`" + `services/svc.go` + "`" + ` to ` + "`" + `services/.go` + "`" + `. Each service = an ` + "`" + `Input` + "`" + ` struct (with + ` + "`" + `Default()` + "`" + `), an ` + "`" + `Output` + "`" + ` struct (with an ` + "`" + `out` + "`" + ` TOML tag), and a ` + "`" + `New(in)` + "`" + ` deploy func + using ` + "`" + `testcontainers` + "`" + `. Use ` + "`" + `framework.DefaultTCLabels()` + "`" + `, ` + "`" + `framework.DefaultNetworkName` + "`" + `, + and static host ports. +2. Add the input field to the ` + "`" + `Cfg` + "`" + ` struct in ` + "`" + `environment.go` + "`" + ` (with a ` + "`" + `toml` + "`" + ` tag). +3. Call ` + "`" + `New(in.)` + "`" + ` inside ` + "`" + `NewEnvironment()` + "`" + `, alongside ` + "`" + `services.NewService` + "`" + `. +4. Add its config block to ` + "`" + `env.toml` + "`" + `. +5. A deployed service must guard on ` + "`" + `in == nil || in.Out != nil` + "`" + ` to stay idempotent (see ` + "`" + `NewService` + "`" + `). + +## Add a product + +1. Copy ` + "`" + `products/{{ .ProductName }}/` + "`" + ` → ` + "`" + `products//` + "`" + ` and ` + "`" + `tests/{{ .ProductName }}/` + "`" + ` → ` + "`" + `tests//` + "`" + `. +2. Implement the ` + "`" + `Product` + "`" + ` interface (` + "`" + `interface.go` + "`" + `) in ` + "`" + `configurator.go` + "`" + `: + ` + "`" + `Load` + "`" + `, ` + "`" + `Store` + "`" + `, ` + "`" + `GenerateNodesConfig` + "`" + `, ` + "`" + `GenerateNodesSecrets` + "`" + `, ` + "`" + `ConfigureJobsAndContracts` + "`" + `. +3. Register it in ` + "`" + `newProduct(name)` + "`" + ` in ` + "`" + `environment.go` + "`" + ` with a new ` + "`" + `case` + "`" + `. +4. Add a ` + "`" + `[[products]]` + "`" + ` entry (with ` + "`" + `name` + "`" + `, ` + "`" + `instances` + "`" + `) in ` + "`" + `env.toml` + "`" + `. + Multiple instances → your ` + "`" + `ConfigureJobsAndContracts` + "`" + ` must handle ` + "`" + `productIdx` + "`" + `. + +## Add / change fakes (3rd-party HTTP) + +Edit ` + "`" + `fakes/main.go` + "`" + ` (its own Docker image + ` + "`" + `go.mod` + "`" + `), then ` + "`" + `just build-fakes` + "`" + `. +Tests reach fakes via the ` + "`" + `fake_server` + "`" + ` output; see ` + "`" + `tests/{{ .ProductName }}/func_test.go` + "`" + `. + +## Conventions (do not break) + +- Docker components: static ports, ` + "`" + `framework.DefaultTCLabels()` + "`" + `, ` + "`" + `framework.DefaultNetworkName` + "`" + `. +- Config is TOML-driven; every new component/product needs a ` + "`" + `Cfg` + "`" + `/` + "`" + `env.toml` + "`" + ` entry. +- Idempotent deploys: skip if output already set. +- Every CLI command has a completion entry. +- Address any ` + "`" + `// TODO` + "`" + ` comments the generator left before shipping. ` // ProductsInterfaceTmpl common interface for arbitrary products deployed in devenv ProductsInterfaceTmpl = `package {{ .PackageName }} @@ -958,7 +1041,7 @@ instances = 1 {{- range .NodeIndices }} [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:2.31.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" {{- end }} # additional nodeset to demonstrate how to add many # you need to add "nodesets.http_port_range_start" and "nodesets.db.port" so these nodesets @@ -974,7 +1057,7 @@ instances = 1 port = 13001 [[nodesets.node_specs]] [nodesets.node_specs.node] - image = "public.ecr.aws/chainlink/chainlink:2.31.0" + image = "public.ecr.aws/chainlink/chainlink:2.49.1-rc.2" ` @@ -1611,7 +1694,7 @@ func checkDockerIsRunning() { os.Exit(1) } defer cli.Close() - _, err = cli.Ping(context.Background()) + _, err = cli.Ping(context.Background(), client.PingOptions{}) if err != nil { fmt.Println("Docker is not running, please start Docker daemon first!") os.Exit(1) @@ -2479,6 +2562,19 @@ func (g *EnvCodegen) Write() error { return fmt.Errorf("failed to write README.md file: %w", err) } + // Generate AGENTS.md file + agentsContents, err := g.GenerateAgents() + if err != nil { + return err + } + if err := os.WriteFile( //nolint:gosec + filepath.Join(g.cfg.outputDir, "AGENTS.md"), + []byte(agentsContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write AGENTS.md file: %w", err) + } + // Generate config.go configFileContents, err := g.GenerateConfig() if err != nil { @@ -2737,6 +2833,16 @@ func (g *EnvCodegen) GenerateReadme() (string, error) { return render(ReadmeTmpl, data) } +// GenerateAgents generate AGENTS.md guide for AI agents +func (g *EnvCodegen) GenerateAgents() (string, error) { + log.Info().Msg("Generating AGENTS.md file") + data := ReadmeParams{ + CLIName: g.cfg.cliName, + ProductName: g.cfg.productName, + } + return render(AgentsTmpl, data) +} + // GenerateGitIgnore generate .gitignore file func (g *EnvCodegen) GenerateGitIgnore() (string, error) { log.Info().Msg("Generating .gitignore file")