@tdk-landscape/tdk-cli-core 1.3.99 → 1.3.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,7 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ - Go services can opt in to a live development loop with `"dev": { "liveReload": true }` in `service.json`: edits are copied into the running container and recompiled without an image rebuild, while `go.mod`, `go.sum` and the generated Dockerfile still rebuild the image. The watcher compiles into a staging binary while the old build keeps serving and swaps only on success, so a compile error shows in the Tilt log and the last good build keeps answering (Air, tried first, stopped the app before compiling and left it down for the whole compile; in 2 of 6 edits a Traefik health check then returned 503 for 10 s). Measured on one loaded Mac: median edit-to-ready 1.63 s for the scaffold. The default Go image (`golang:1.23`) now sets `GOTOOLCHAIN=auto`, so a `go.mod` that needs a newer Go, such as Gin 1.12 (`go 1.25`), downloads that toolchain instead of stopping with `requires go >= 1.25`. Offline or air-gapped builds of a go.mod newer than 1.23 still fail, but now at the toolchain download rather than at the version check. `scripts/e2e/go-dev-loop.sh` checks all of this through a real `tdk up`. Refs #369.
7
8
  - `tdk up` keeps each smoke step's status and response body under `.tdk/smoke/<service>/<step>/` (`latest.json` and `body.txt`, body capped at 64 KiB), plus `last-success.json` and `last-success-body.txt` that a later failure never overwrites, so a regression can be diffed against the last good response. The failure line ends with the record path, and `tdk project` gitignores `.tdk/smoke/`. A new `smoke-e2e` workflow runs `scripts/verify-smoke.sh` and uploads the records. Before its first step a smoke check now waits for the service's health path (`healthCheckPath`, default `/health` for backends) with GETs until it answers something other than a connection error, 404 or 5xx, so a write is no longer the first request to hit a service that is still building; write retry rules are unchanged.
8
9
  - Add an opt-in Python backend: `tdk resource <name> --type backend --language python` scaffolds a FastAPI service (`pyproject.toml`, `src/main.py`, a pytest smoke test) and TDK generates its Docker image (`python:3.12-slim`, with `test` and `production` stages). The choice is saved as `language` in `service.json` only when you pass the flag; omitting it keeps the existing Bun + Hono output byte for byte. Unknown ids and `--language` on non-backend types fail before any file is written. A runnable example is in `examples/one-backend-python`, and the provider contract is in `docs/backend-language-providers.md`. Node.js is not included.
9
10
  - `tdk doctor` no longer reports "resource without a package.json" for a Python backend, and a Python service missing `pyproject.toml` or `src/main.py` now fails early with the missing file named.
package/README.md CHANGED
@@ -10,6 +10,10 @@ Docker runs the containers. Tilt watches services and live-updates containers wh
10
10
 
11
11
  [Website](https://tdk-landscape.github.io/tdk-website/) · [Quickstart](https://tdk-landscape.github.io/tdk-website/docs/quickstart/) · [Examples](https://tdk-landscape.github.io/tdk-website/docs/examples/) · [Awesome TDK](https://github.com/tdk-landscape/awesome-tdk-framework) · [Demo](https://tdk-landscape.github.io/tdk-demo-animation/) · [Report a bug](https://github.com/tdk-landscape/tdk-cli-core/issues)
12
12
 
13
+ ## Who uses TDK
14
+
15
+ No teams are listed yet. Be the first: [tell us you use TDK](https://github.com/tdk-landscape/tdk-cli-core/issues/new?template=we-use-tdk.yml) or [add a row to ADOPTERS.md](ADOPTERS.md).
16
+
13
17
  ## Quick start
14
18
 
15
19
  ```bash
@@ -60,6 +64,8 @@ curl -fsSL https://tdk-landscape.github.io/install.sh | sh
60
64
  ## Docs
61
65
 
62
66
  - [Documentation index](docs/README.md)
67
+ - [Pilot TDK on a real repository](docs/adopt-tdk.md)
68
+ - [FAQ for agencies and multi-client teams](docs/faq-teams.md)
63
69
  - [Working alongside Helm](docs/with-helm.md)
64
70
  - [Configuration and editor schemas](docs/configuration.md)
65
71
  - [Runnable one-backend example](examples/one-backend/README.md)
@@ -103,4 +109,6 @@ Core stays free. Premium is a separate key for the extras above; no key required
103
109
 
104
110
  Contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) and the [contributor guide](docs/contributing/README.md). Report security issues using [SECURITY.md](SECURITY.md).
105
111
 
106
- TDK is MIT-licensed; see [LICENSE](LICENSE).
112
+ TDK is MIT-licensed; see [LICENSE](LICENSE). The [license boundary](GOVERNANCE.md#license-boundary-and-donation-scope) says which code is MIT and which is downloaded with a key.
113
+
114
+ Project governance: [GOVERNANCE.md](GOVERNANCE.md), [MAINTAINERS.md](MAINTAINERS.md), [ADOPTERS.md](ADOPTERS.md).
package/cli/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { doctorCommand } from "./commands/doctor.js";
8
8
  import { downCommand } from "./commands/down.js";
9
9
  import { ejectCommand } from "./commands/eject.js";
10
10
  import { showHelp } from "./commands/help.js";
11
+ import { maintainersCommand } from "./commands/maintainers.js";
11
12
  import { networksCommand } from "./commands/networks.js";
12
13
  import { projectCommand } from "./commands/project.js";
13
14
  import { projectsCommand } from "./commands/projects.js";
@@ -53,6 +54,7 @@ program.addCommand(projectCommand);
53
54
  program.addCommand(configCommand);
54
55
  program.addCommand(uiCommand);
55
56
  program.addCommand(versionCommand);
57
+ program.addCommand(maintainersCommand);
56
58
  program.addCommand(doctorCommand);
57
59
  program.addCommand(completionCommand);
58
60
  program.addCommand(upgradeCommand);
@@ -0,0 +1,101 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { Command } from "commander";
4
+ export const MIN_MAINTAINERS = 3;
5
+ export const MIN_COMPANIES = 2;
6
+ const REQUIRED_COLUMNS = ["name", "github id", "company"];
7
+ const PLACEHOLDER_COMPANIES = new Set(["", "tbd", "unknown", "tdk-landscape"]);
8
+ function splitRow(line) {
9
+ return line
10
+ .trim()
11
+ .replace(/^\|/, "")
12
+ .replace(/\|$/, "")
13
+ .split("|")
14
+ .map((cell) => cell.trim());
15
+ }
16
+ export function parseMaintainers(markdown) {
17
+ const lines = markdown.split(/\r?\n/);
18
+ const headerIndex = lines.findIndex((line) => {
19
+ if (!line.includes("|"))
20
+ return false;
21
+ const cells = splitRow(line).map((cell) => cell.toLowerCase());
22
+ return REQUIRED_COLUMNS.every((column) => cells.includes(column));
23
+ });
24
+ if (headerIndex === -1) {
25
+ return {
26
+ ok: false,
27
+ error: "table with columns Name, GitHub id, Company not found",
28
+ };
29
+ }
30
+ const header = splitRow(lines[headerIndex]).map((cell) => cell.toLowerCase());
31
+ const nameAt = header.indexOf("name");
32
+ const githubAt = header.indexOf("github id");
33
+ const companyAt = header.indexOf("company");
34
+ const rows = [];
35
+ for (const line of lines.slice(headerIndex + 1)) {
36
+ if (!line.includes("|"))
37
+ break;
38
+ const cells = splitRow(line);
39
+ if (cells.every((cell) => /^:?-+:?$/.test(cell)))
40
+ continue;
41
+ rows.push({
42
+ name: cells[nameAt] ?? "",
43
+ githubId: cells[githubAt] ?? "",
44
+ company: cells[companyAt] ?? "",
45
+ });
46
+ }
47
+ return { ok: true, rows };
48
+ }
49
+ export function evaluateMaintainers(rows) {
50
+ const seen = new Set();
51
+ const valid = rows.filter((row) => {
52
+ const id = row.githubId.toLowerCase();
53
+ if (!row.name || !id || seen.has(id) || PLACEHOLDER_COMPANIES.has(row.company.toLowerCase())) {
54
+ return false;
55
+ }
56
+ seen.add(id);
57
+ return true;
58
+ });
59
+ const companies = new Set(valid.map((row) => row.company.toLowerCase()));
60
+ const missingPeople = Math.max(0, MIN_MAINTAINERS - valid.length);
61
+ const missingCompanies = Math.max(0, MIN_COMPANIES - companies.size);
62
+ return {
63
+ eligible: missingPeople === 0 && missingCompanies === 0,
64
+ people: valid.length,
65
+ companies: companies.size,
66
+ missingPeople,
67
+ missingCompanies,
68
+ };
69
+ }
70
+ export function runMaintainersCheck(file) {
71
+ const path = resolve(file);
72
+ if (!existsSync(path)) {
73
+ return { exitCode: 1, output: `${file} not found` };
74
+ }
75
+ const parsed = parseMaintainers(readFileSync(path, "utf8"));
76
+ if (!parsed.ok) {
77
+ return { exitCode: 1, output: `${file}: ${parsed.error}` };
78
+ }
79
+ const report = evaluateMaintainers(parsed.rows);
80
+ if (report.eligible) {
81
+ return {
82
+ exitCode: 0,
83
+ output: `${report.people} maintainers, ${report.companies} companies`,
84
+ };
85
+ }
86
+ return {
87
+ exitCode: 1,
88
+ output: `not eligible: ${report.people} maintainers (${report.missingPeople} missing), ${report.companies} companies (${report.missingCompanies} missing)`,
89
+ };
90
+ }
91
+ export const maintainersCommand = new Command("maintainers")
92
+ .description("Maintainer file checks")
93
+ .addCommand(new Command("check")
94
+ .description("Check MAINTAINERS.md lists 3 maintainers from 2 companies (a gate, not a claim of eligibility)")
95
+ .argument("[file]", "maintainers file", "MAINTAINERS.md")
96
+ .action((file) => {
97
+ const { exitCode, output } = runMaintainersCheck(file);
98
+ (exitCode === 0 ? console.log : console.error)(output);
99
+ process.exitCode = exitCode;
100
+ }));
101
+ //# sourceMappingURL=maintainers.js.map
@@ -8,11 +8,11 @@ import { findProjectRoot } from "../utils/paths.js";
8
8
  import { clearMetadataCache, discoverResources, discoverStacks, getResourceMetadata, getStackMetadata, } from "../utils/services.js";
9
9
  import { isTiltAvailable } from "../utils/tilt.js";
10
10
  // biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
11
- const HelpPanel = ({ onClose }) => (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60, children: [_jsx(Text, { bold: true, color: "cyan", children: "Keyboard Shortcuts" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, underline: true, children: "Navigation" }), _jsx(Text, { children: " \u2191/\u2193 Navigate list items" }), _jsx(Text, { children: " Enter Select item / Open detail" }), _jsx(Text, { children: " Space Toggle expand (tree view)" }), _jsx(Text, { children: " Tab Next tab" }), _jsx(Text, { children: " 1-5 Direct tab access" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: "Actions" }) }), _jsx(Text, { children: " m Toggle mouse support" }), _jsx(Text, { children: " t Toggle tooltips" }), _jsx(Text, { children: " e Toggle enabled/disabled services" }), _jsx(Text, { children: " r Refresh data" }), _jsx(Text, { children: " / Search/filter" }), _jsx(Text, { children: " ? Show this help" }), _jsx(Text, { children: " q/Esc Quit / Back" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to close..." }) })] }));
11
+ const HelpPanel = ({ onClose }) => (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60, children: [_jsx(Text, { bold: true, color: "cyan", children: "Keyboard Shortcuts" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, underline: true, children: "Navigation" }), _jsx(Text, { children: " \u2191/\u2193 Navigate list items" }), _jsx(Text, { children: " Enter Select item / Open detail" }), _jsx(Text, { children: " Space Toggle expand (tree view)" }), _jsx(Text, { children: " Tab Next tab" }), _jsx(Text, { children: " 1-5 Direct tab access" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: "Actions" }) }), _jsx(Text, { children: " m Toggle mouse support" }), _jsx(Text, { children: " t Toggle tooltips" }), _jsx(Text, { children: " e Toggle enabled/disabled services" }), _jsx(Text, { children: " r Refresh data" }), _jsx(Text, { children: " / Search/filter" }), _jsx(Text, { children: " ? Show this help" }), _jsx(Text, { children: " q Quit \u2502 Esc Back" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to close..." }) })] }));
12
12
  const LoadingScreen = ({ progress, message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { children: ["Loading: ", message] }), _jsxs(Box, { marginY: 1, borderStyle: "single", borderColor: "gray", width: 50, children: [_jsx(Box, { width: progress / 2, backgroundColor: "cyan", children: _jsx(Text, { children: " ".repeat(progress / 2) }) }), _jsxs(Text, { children: [" ", progress, "%"] })] })] }));
13
13
  // biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
14
14
  const ErrorScreen = ({ error, onRetry }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "red", children: "Could Not Load Services" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: "red", children: ["\u2717 ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "Troubleshooting:" }), _jsx(Text, { color: "gray", children: " 1. Check that every service.json is valid JSON" }), _jsx(Text, { color: "gray", children: " 2. Run tdk from your project (the folder with the Tiltfile)" }), _jsx(Text, { color: "gray", children: " 3. Try: tdk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to retry or [q] to quit" })] }));
15
- const EmptyState = () => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk init" }), _jsx(Text, { children: " 2. Or create services manually" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" })] }));
15
+ const EmptyState = ({ message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk init" }), _jsx(Text, { children: " 2. Or create services manually" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" }), message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2593\u2592\u2591 ", message, " \u2591\u2592\u2593"] }) }))] }));
16
16
  const TUIApp = () => {
17
17
  const { exit } = useApp();
18
18
  const { stdout } = useStdout();
@@ -297,7 +297,8 @@ const TUIApp = () => {
297
297
  setSelectedStack(null);
298
298
  return;
299
299
  }
300
- exit();
300
+ setMessage("Press q to quit");
301
+ setTimeout(() => setMessage(""), 2000);
301
302
  return;
302
303
  }
303
304
  if (input === "?") {
@@ -413,7 +414,7 @@ const TUIApp = () => {
413
414
  return _jsx(ErrorScreen, { error: error, onRetry: refresh });
414
415
  }
415
416
  if (services.length === 0) {
416
- return _jsx(EmptyState, {});
417
+ return _jsx(EmptyState, { message: message });
417
418
  }
418
419
  return (_jsxs(Box, { flexDirection: "column", height: stdout.rows || 24, children: [_jsx(Box, { paddingX: 1, paddingY: 0, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { color: "white", children: projectRoot }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsxs(Text, { color: "green", children: [services.length, " services ready"] })] }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", children: "─".repeat(compact ? 60 : Math.min(terminalWidth - 4, 100)) }) }), isSearching && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "yellow", children: ["Search: ", searchQuery, "_"] }) })), !isSearching && message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2593\u2592\u2591 ", message, " \u2591\u2592\u2593"] }) })), !isSearching && !message && showTooltips && !showHelp && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: activeTab === "overview" && selectedStack
419
420
  ? `Stack "${selectedStack}" selected. [Enter] view │ [Esc] back │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help`
package/cli/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tdk-cli-internal",
3
3
  "private": true,
4
- "version": "1.3.99",
4
+ "version": "1.3.101",
5
5
  "description": "TDK CLI — start services on your laptop.",
6
6
  "keywords": [
7
7
  "tdk",
@@ -51,6 +51,9 @@ CMD ["sh", "-c", "uvicorn main:app --app-dir src --host 0.0.0.0 --port ${{PORT:-
51
51
 
52
52
  _GO = """# Generated by TDK for a Go backend. Do not edit; change service.json instead.
53
53
  FROM golang:1.23-bookworm AS build
54
+ # The official image pins GOTOOLCHAIN=local, so a go.mod that needs a newer Go (Gin 1.12 needs 1.25) stops with "requires go >= 1.25".
55
+ # `auto` lets Go fetch the toolchain go.mod asks for; a go.mod at or below the image's Go downloads nothing.
56
+ ENV GOTOOLCHAIN=auto
54
57
 
55
58
  WORKDIR /app/{res_path}
56
59
 
@@ -102,21 +105,97 @@ EXPOSE {port}
102
105
  CMD ["app"]
103
106
  """
104
107
 
105
- # Opt-in Go development target (`dev.liveReload`): the toolchain image with Air, which rebuilds and restarts the app when a `.go`
106
- # file changes. Dependencies are downloaded in their own layer and the module and build caches live in BuildKit cache mounts, so an
107
- # ordinary source edit recompiles incrementally. `go.mod`, `go.sum` and the Dockerfile still rebuild the image.
108
+ # The watcher the Go development target runs. It compiles a change into a staging binary while the running app keeps serving, and stops
109
+ # and swaps the app only when that build succeeds. Air was tried first and stops the app as soon as it sees a change (engine.go `start`
110
+ # calls stopBin before buildRun), so the service stayed down for the whole compile; a Traefik health check landing in that gap kept the
111
+ # route at 503 for up to 10s. A compile error is printed to the log and the last good build keeps serving.
112
+ # `-s -w` drops debug info, which shortens the link step; delve cannot attach to a binary built this way.
113
+ # `\\n` in find's -printf format is written with a doubled backslash so the shell receives a backslash and an n.
114
+ _GO_DEV_WATCH = """#!/bin/sh
115
+ app=/tmp/app
116
+ next=/tmp/app.next
117
+ pid=
118
+
119
+ # Path, mtime and size of every file a Go build can read. Dot-directories (.git, .autogenerated), tmp/, tests and docs are ignored.
120
+ signature() {
121
+ find . -type f -not -path '*/.*' -not -path './tmp/*' -not -name '*_test.go' -not -name '*.md' -printf '%p %T@ %s\\n' | sort | cksum
122
+ }
123
+
124
+ # The app runs in its own session so a signal reaches everything it forked and no child keeps the listen socket.
125
+ start_app() {
126
+ setsid "$app" &
127
+ pid=$!
128
+ }
129
+
130
+ # Succeeds only when the app is really gone; a swap on top of a live process would fail (ETXTBSY) or hit address-in-use.
131
+ stop_app() {
132
+ [ -n "$pid" ] || return 0
133
+ kill -TERM -- "-$pid" 2>/dev/null || kill "$pid" 2>/dev/null
134
+ waited=0
135
+ while kill -0 "$pid" 2>/dev/null && [ "$waited" -lt 50 ]; do
136
+ sleep 0.1
137
+ waited=$((waited + 1))
138
+ done
139
+ kill -KILL -- "-$pid" 2>/dev/null || kill -9 "$pid" 2>/dev/null
140
+ wait "$pid" 2>/dev/null
141
+ if kill -0 "$pid" 2>/dev/null; then
142
+ return 1
143
+ fi
144
+ pid=
145
+ }
146
+
147
+ trap 'stop_app; exit 0' TERM INT
148
+
149
+ last=$(signature)
150
+ echo "[tdk] watching for Go changes; the running app keeps serving while a rebuild compiles"
151
+ start_app
152
+
153
+ while true; do
154
+ sleep 0.2 &
155
+ wait $!
156
+ if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
157
+ wait "$pid"
158
+ echo "[tdk] the app exited with status $?; waiting for a change to rebuild"
159
+ pid=
160
+ fi
161
+ now=$(signature)
162
+ [ "$now" = "$last" ] && continue
163
+ last=$now
164
+ echo "[tdk] change detected, rebuilding..."
165
+ if go build -ldflags='-s -w' -o "$next" . ; then
166
+ if stop_app && mv "$next" "$app"; then
167
+ start_app
168
+ echo "[tdk] rebuilt and restarted"
169
+ else
170
+ echo "[tdk] could not stop the running app or install the new build; save again to retry"
171
+ last=
172
+ fi
173
+ else
174
+ echo "[tdk] build failed; the last good build keeps running. Fix the error and save again."
175
+ fi
176
+ done
177
+ """
178
+
179
+ # Opt-in Go development target (`dev.liveReload`): the toolchain image plus the watcher above, which rebuilds and restarts the app when a
180
+ # source file changes. Dependencies are downloaded in their own layer and the module download is its own image layer, so an ordinary
181
+ # source edit recompiles incrementally. `go.mod`, `go.sum` and the Dockerfile still rebuild the image. The heredoc needs BuildKit's
182
+ # Dockerfile frontend 1.4 (Docker Engine 23+).
108
183
  _GO_DEVELOPMENT = """
109
184
  FROM golang:1.23-bookworm AS development
110
- RUN go install github.com/air-verse/air@v1.61.7
185
+ ENV GOTOOLCHAIN=auto
111
186
  WORKDIR /app/{res_path}
112
187
  COPY {res_path}/go.* ./
113
- RUN --mount=type=cache,target=/go/pkg/mod go mod download
188
+ # A plain RUN (not a cache mount) keeps the modules in a layer keyed on go.mod and go.sum, so the priming build below reuses them.
189
+ RUN go mod download
114
190
  COPY {res_path}/ ./
115
- ENV GOFLAGS=-buildvcs=false
191
+ ENV GOFLAGS=-buildvcs=false CGO_ENABLED=0
116
192
  # Prime the build cache inside the image (a cache mount would not be kept), so the first edit recompiles only what changed.
117
- RUN go build -o /tmp/app .
193
+ # Same flags as the watcher's builds (and CGO off like the production stage), so the primed binary and cache match every later one.
194
+ RUN go build -ldflags='-s -w' -o /tmp/app .
195
+ COPY --chmod=755 <<'TDK_GO_DEV_WATCH' /usr/local/bin/tdk-go-watch
196
+ {watch_script}TDK_GO_DEV_WATCH
118
197
  EXPOSE {port}
119
- CMD ["sh", "-c", "exec air --build.cmd 'go build -o /tmp/app .' --build.bin /tmp/app --build.include_ext go"]
198
+ CMD ["tdk-go-watch"]
120
199
  """
121
200
 
122
201
  def generate_language_dockerfile(res_path, language, port = BASE_PORT_BACKEND, dev = False):
@@ -126,7 +205,7 @@ def generate_language_dockerfile(res_path, language, port = BASE_PORT_BACKEND, d
126
205
  fail("No Dockerfile generator for backend language '{}'".format(language))
127
206
  go_development = ""
128
207
  if dev and language == "go":
129
- go_development = _GO_DEVELOPMENT.format(res_path = res_path, port = port)
208
+ go_development = _GO_DEVELOPMENT.format(res_path = res_path, port = port, watch_script = _GO_DEV_WATCH)
130
209
  return template.format(
131
210
  res_path = res_path,
132
211
  port = port,
@@ -220,7 +220,7 @@ def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', la
220
220
 
221
221
  Python backends reload their own process after a sync (uvicorn --reload), so they have
222
222
  no Bun install step. A changed pyproject.toml is not synced, so Tilt rebuilds the image instead.
223
- Go and Rust backends have no live update at all (see the early return).
223
+ Rust backends, and Go backends without `dev.liveReload`, have no live update (see the early return).
224
224
  """
225
225
  # Go and Rust are compiled into the image and have no reload process, so syncing sources would leave the
226
226
  # old binary running. Return before any sync() step is built (Tilt rejects sync steps that are created
@@ -229,7 +229,7 @@ def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', la
229
229
  return []
230
230
 
231
231
  if go_live_reload:
232
- # Opted-in Go development loop: sync the sources into the development image, where Air rebuilds and restarts.
232
+ # Opted-in Go development loop: sync the sources into the development image, where the tdk-go-watch script rebuilds and restarts.
233
233
  # Dependency and Dockerfile changes cannot be picked up by a restart, so they fall back to a full image rebuild.
234
234
  source_dir = _live_update_sync_source(project_root, full_res_path, '')
235
235
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdk-landscape/tdk-cli-core",
3
- "version": "1.3.99",
3
+ "version": "1.3.101",
4
4
  "description": "TDK CLI — start services on your laptop.",
5
5
  "homepage": "https://tdk-landscape.github.io/tdk-website",
6
6
  "repository": {