DEV Community

tdk-cli
tdk-cli

Posted on

Generated Files Without the Silent Overwrite: Why TDK CLI Treats Local Config as Derived Artifacts

Most multi-service local setups force you to maintain Dockerfiles, Vite configs, nginx configs, API clients, tsconfigs, Playwright setups, env files, Tiltfiles, Traefik routes, and health probes by hand. AI coding agents make the application code easy. They remain unreliable at keeping the surrounding local infrastructure consistent across services.

TDK CLI takes a different approach. You write a tiny declarative service.json per service. The tool generates the rest (roughly 17 files for a typical frontend) and treats those outputs as derived artifacts rather than source.

This design directly answers a practical question that came up in a recent discussion: would you rather have a tool generate these files, or ask an AI to write them when you need them? And more importantly—what happens after someone edits a generated file?

The Contract

This is essentially all you maintain for a service:

{
  "$schema": "https://tdk-landscape.github.io/schema.service.json",
  "schemaVersion": 1,
  "appName": "orders-api",
  "appType": "backend",
  "stack": "shop",
  "port": 4000,
  "healthCheckPath": "/health",
  "dependsOn": []
}
Enter fullscreen mode Exit fullscreen mode

From that single file (plus project-level config), TDK generates Dockerfiles, live-reload rules, Traefik routes, shared Postgres wiring, health probes, Vite/nginx/tsconfig/env files, API clients, and the Tilt configuration that actually runs the stack. You run tdk up and get a local Docker + Tilt environment with hot reload. No local Kubernetes required.

Project → Stack → Resource

TDK organizes work as Project → Stack → Resource. A project contains stacks (business slices). Each stack contains resources (services of type backend, frontend, worker, etc.).

flowchart LR
    P["Project<br/>.tdk/project.json"] --> S1["Stack: shop"]
    P --> S2["Stack: billing"]
    S1 --> R1["orders-api<br/>backend"]
    S1 --> R2["storefront-web<br/>frontend"]
    S2 --> R3["checkout-api<br/>backend"]

You can start just one slice with tdk up shop. The hierarchy keeps large systems manageable.

Why Generation Beats “Just Ask AI” Here

AI is excellent at writing application code and even at one-off config snippets. The problem appears when you have many services and need the configs to stay consistent with each other, with the dependency graph, with port conventions, with health checks, and with the live-update rules that Tilt uses.

A dedicated generator has a fixed contract. The same service.json always produces the same class of outputs. When the generator improves, every service can be regenerated from the source of truth. An AI, by contrast, has no durable memory of the last generation and no built-in notion of “this file is derived.”

The critical design point is what happens after generation.

Drift Detection: The Part Most Generators Get Wrong

A common objection to generators is: “What if I need to customize one of those files? The next generation will wipe my changes.”

TDK treats the generated files as derived artifacts. The source of truth stays service.json + your application code. Everything else is regeneratable.

If you edit a generated file by hand, tdk up detects the drift (against a stored snapshot) and stops with exit code 2 instead of silently overwriting. You then have two explicit choices:

  • Run tdk config regenerate (or edit service.json and re-run tdk up) to throw away the hand-edits and rebuild from the contract, or
  • Use --ignore-drift if you intentionally want to keep the customizations.
flowchart TD
    A["Edit service.json<br/>or app code"] --> B["tdk up"]
    B --> C{"Drift check<br/>vs snapshot"}
    C -->|Clean| D["Start Tilt + Docker<br/>Hot reload"]
    C -->|Hand-edited file| E["Exit 2<br/>List drifted files"]
    E --> F["tdk config regenerate"]
    E --> G["tdk up --ignore-drift"]
    F --> D
    G --> H["Continue with warning<br/>Keep customizations"]

Normal workflow: change the manifest, run tdk up, get updated outputs. Hand-edits are never silently discarded.

What Gets Generated (Frontend Example)

For a Bun/TypeScript frontend under services/store/storefront-web/, the .autogenerated/ directory typically contains:

  • nginx.autogenerated.conf — serves the build and routes API requests
  • Dockerfile.app.autogenerated — local app image
  • .env.frontend.autogenerated + env.ts
  • vite.config.frontend.autogenerated.ts and production build variant
  • tsconfig.autogenerated.json (and docker variant)
  • api-client.ts — preconfigured client
  • Package and Bun config pairs for host vs container
  • Snapshots under .autogenerated/.tdk-contract/ for the drift check

Backends, Python/Go/Rust services, and optional features (Playwright, Prisma, etc.) produce their own appropriate subsets. You do not write a docker-compose.yml or the nginx/Vite/tsconfig files yourself for the local stack.

Architecture Sketch

The system is split so each part has a clear job:

flowchart TB
    subgraph CLI["cli/ (TypeScript)"]
        Commands["Commands & validation"]
        Templates["Handlebars templates"]
        Ports["Port allocation"]
    end

    subgraph Discovery["discovery/ (Starlark)"]
        Scan["Scan service.json"]
        DAG["Build dependency DAG"]
    end

    subgraph Engine["engine/ (Starlark)"]
        LiveUpdate["live_update rules"]
        Traefik["Traefik routing"]
        Infra["Postgres + platform"]
    end

    CLI -->|"generates Tiltfile + configs"| Discovery
    Discovery -->|"registry + DAG"| Engine
    Engine -->|"docker_build + containers"| Docker[("Docker")]

The CLI never starts containers itself. The engine never parses CLI flags. Discovery never builds images. tdk up runs preflight (including the drift gate), ensures master configs, then hands off to Tilt. Inside Tilt the engine produces the per-service .autogenerated/ files and starts the stack.

Committed vs Generated

flowchart LR
    subgraph Committed["Committed (source of truth)"]
        PJ[".tdk/project.json"]
        SJ["services/*/*/service.json"]
        Code["Your app source"]
    end

    subgraph Generated["Generated (gitignored)"]
        Out[".tdk/.tdk-out/<br/>Tiltfile, specs"]
        Auto["services/*/*/.autogenerated/"]
        Env[".env"]
    end

    Committed -->|"tdk project / tdk resource / tdk up"| Generated
    Generated -->|"tdk config regenerate"| Committed

Only the contract and your code are committed. Generated artifacts stay out of version control (or are clearly marked) so AI agents and humans cannot accidentally treat them as source.

What It Is Not

TDK is a local development runtime. It optimizes for speed and developer experience on your machine (Docker + Tilt, hot reload, no cluster). Production deployment remains with Helm, Argo CD, or whatever you already use.

Getting Started

npm install -g @tdk-landscape/tdk-cli-core
tdk doctor
tdk project --yes
tdk resource orders-api --type backend --stack shop --yes
tdk up shop
Enter fullscreen mode Exit fullscreen mode

Or clone an example landscape and boot it.

The Practical Preference

In the original exchange, the preference leaned toward generation provided hand-edits are protected. TDK’s drift gate (exit 2 + explicit regenerate or ignore) is the concrete mechanism that makes that preference safe. The source of truth stays the small declarative file and your application code. Everything else is derived, regeneratable, and checked so it never quietly wipes custom work.

That combination—declarative contract + generation + explicit drift handling—is what makes the local multi-service loop durable even when AI is writing most of the application code.

Top comments (1)

Collapse
 
tdk-cli profile image
tdk-cli •

nice!