@tdk-landscape/tdk-cli-core 1.3.75 → 1.3.76

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,12 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ - `tdk up` no longer leaves `nats` (and any other service in `services/platform/messaging/docker-compose.yml`, e.g. a local mail catcher) disabled in Tilt. The pre-alpha default always goes through a focus-filter path that only re-enabled `init-networks`/`postgres`/`traefik`/`golden-layers-build`, so a resource with the `nats` feature retried its broker connection forever until you ran `tilt enable nats` by hand. Fixes #155.
8
+
9
+ - `tdk doctor` now catches wiring mistakes that used to surface minutes into `tdk up`: a resource with no `package.json` (the image build stops at "COPY ... not found"), a `params` URL such as `http://auth-emulator:4000` whose port is not the one the target listens on (its `service.json` `port`), a frontend calling a backend through `localhost:<port>` (TDK publishes no backend ports on localhost; use the Traefik route), the `nats` feature without `services/platform/messaging/docker-compose.yml` (TDK does not generate it, so no NATS server starts), two Tilt processes running for one project, and Docker having no address pool left for the networks a project needs. Found while building [tdk-ecommerce-example](https://github.com/tdk-landscape/tdk-ecommerce-example) and a queue/auth example.
10
+
11
+ - `tdk project ecommerce` clones [tdk-ecommerce-example](https://github.com/tdk-landscape/tdk-ecommerce-example), a Vue 3 storefront and Hono catalog API. Before, only restaurant, saas, erp, user-management and example were accepted.
12
+
7
13
  ## 1.3.73 (2026-09-28)
8
14
 
9
15
  - Free-tier projects no longer start the `sablier` and `wake-gateway` containers, and Traefik no longer downloads the Sablier plugin from GitHub on every start. They were always launched (the `sablier` one with a read-write Docker socket) even though the license only gated the per-service labels, so nothing could use them. They are now generated only when a license grants Sablier. Fewer containers, one image build less, and no network fetch at Traefik boot. Spotted in the ERP CI run log.
package/README.md CHANGED
@@ -187,14 +187,14 @@ make pre-commit-run # run pre-commit hooks on all files
187
187
  tdk doctor
188
188
  ```
189
189
 
190
- Checks Docker, Compose and Tilt. Inside a project it also checks the generated configs, `.env`, service scripts and that the ingress ports are free, before you run `tdk up`.
190
+ Checks Docker, Compose and Tilt. Inside a project it also checks the generated configs, `.env`, service scripts and that the ingress ports are free, before you run `tdk up`. It also catches wiring mistakes: a resource without `package.json`, a `params` URL that uses the wrong port for another service, a frontend calling a backend on `localhost:<port>`, the `nats` feature without a broker to start, duplicate Tilt processes, and Docker running out of network address space.
191
191
 
192
192
  ## Community
193
193
 
194
194
  - **Questions and ideas:** [open an issue](https://github.com/tdk-landscape/tdk-cli-core/issues/new/choose).
195
195
  - **Contributing:** see [CONTRIBUTING.md](CONTRIBUTING.md). Issues labeled [`good first issue`](https://github.com/tdk-landscape/tdk-cli-core/labels/good%20first%20issue) are a good place to start.
196
196
  - **Security:** see [SECURITY.md](SECURITY.md). Please don't report vulnerabilities in public issues.
197
- - **Examples:** [ERP system (100 services)](https://github.com/tdk-landscape/tdk-erp-system), [SaaS starter](https://github.com/tdk-landscape/tdk-saas-starter), [restaurant](https://github.com/tdk-landscape/tdk-restaurant-example).
197
+ - **Examples:** [ERP system (100 services)](https://github.com/tdk-landscape/tdk-erp-system), [SaaS starter](https://github.com/tdk-landscape/tdk-saas-starter), [restaurant](https://github.com/tdk-landscape/tdk-restaurant-example), [ecommerce (Vue + Hono)](https://github.com/tdk-landscape/tdk-ecommerce-example).
198
198
 
199
199
  ## License
200
200
 
@@ -7,6 +7,7 @@ import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
7
7
  import { MASTER_CONFIG_FILES, QUICKSTART_DOCS_URL, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
8
8
  import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
9
9
  import { checkHealthRoutes, checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
10
+ import { checkDockerNetworkCapacity, checkFrontendBackendUrls, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
10
11
  import { validateEnvFile } from "../utils/env-validator.js";
11
12
  import { formatCount } from "../utils/formatting.js";
12
13
  import { findProjectRoot } from "../utils/paths.js";
@@ -795,6 +796,8 @@ export const doctorCommand = new Command("doctor")
795
796
  checkTilt,
796
797
  checkDockerCompose,
797
798
  checkDockerVersions,
799
+ // Each project needs several networks; a full address pool fails `tdk up` late.
800
+ () => checkDockerNetworkCapacity(),
798
801
  ];
799
802
  const projectChecks = [
800
803
  checkMasterConfigs,
@@ -806,6 +809,12 @@ export const doctorCommand = new Command("doctor")
806
809
  checkTypeScriptTypeDependencies,
807
810
  checkFrontendDockerPreflight,
808
811
  checkResourceDiscovery,
812
+ // Wiring mistakes that otherwise surface minutes into `tdk up`.
813
+ () => checkResourcePackageJson(),
814
+ () => checkServiceUrlPorts(),
815
+ () => checkFrontendBackendUrls(),
816
+ () => checkNatsBroker(),
817
+ () => checkTiltInstances(),
809
818
  checkEnvironmentVariables,
810
819
  // Preflight: catch "port 80 already allocated" BEFORE claiming ready.
811
820
  checkIngressPorts,
@@ -0,0 +1,261 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { formatCount } from "./formatting.js";
5
+ import { findProjectRoot } from "./paths.js";
6
+ import { discoverResourcesFromRoot } from "./services.js";
7
+ /**
8
+ * Checks for wiring mistakes that only show up minutes into `tdk up`: an image
9
+ * build that cannot find package.json, a token check that cannot reach its
10
+ * identity provider, a frontend calling a port nothing publishes, a queue
11
+ * feature with no broker. Each one is decidable from the files on disk.
12
+ */
13
+ const EXEC_TIMEOUT_MS = 10_000;
14
+ const SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue)$/;
15
+ const TEST_FILE = /(?:\.test\.|\.spec\.|\/tests?\/|\/__tests__\/)/;
16
+ const SKIPPED_DIRS = new Set(["node_modules", ".autogenerated", "dist", "tests", "__tests__"]);
17
+ function projectName(projectRoot) {
18
+ try {
19
+ const parsed = JSON.parse(readFileSync(join(projectRoot, ".tdk", "project.json"), "utf-8"));
20
+ return typeof parsed?.project?.name === "string" ? parsed.project.name : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ function readParams(configPath) {
27
+ try {
28
+ const params = JSON.parse(readFileSync(configPath, "utf-8")).params;
29
+ return params && typeof params === "object" ? params : {};
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ }
35
+ /** Backends are the only resources that listen on their `port`. */
36
+ function backendPorts(resources) {
37
+ const ports = new Map();
38
+ for (const resource of resources) {
39
+ const port = resource.config?.port;
40
+ if (resource.config?.appType === "backend" && typeof port === "number") {
41
+ ports.set(resource.name, port);
42
+ }
43
+ }
44
+ return ports;
45
+ }
46
+ export function checkResourcePackageJson(projectRoot = findProjectRoot() ?? process.cwd()) {
47
+ const missing = discoverResourcesFromRoot(projectRoot)
48
+ .filter((resource) => !existsSync(join(resource.path, "package.json")))
49
+ .map((resource) => resource.name);
50
+ if (missing.length === 0) {
51
+ return {
52
+ name: "Resource package.json",
53
+ didPass: true,
54
+ message: "Every resource has a package.json",
55
+ };
56
+ }
57
+ return {
58
+ name: "Resource package.json",
59
+ didPass: false,
60
+ message: `${formatCount(missing.length, "resource")} without a package.json: ${missing.join(", ")}. The image build stops at "COPY .../package.json: not found"`,
61
+ fix: "Add a package.json next to each service.json (copy one from a working resource), or delete the folder's service.json if it is not a service",
62
+ };
63
+ }
64
+ const CONTAINER_URL = /https?:\/\/([a-z][a-z0-9-]*):(\d+)/g;
65
+ export function checkServiceUrlPorts(projectRoot = findProjectRoot() ?? process.cwd()) {
66
+ const resources = discoverResourcesFromRoot(projectRoot);
67
+ const ports = backendPorts(resources);
68
+ const byPort = new Map([...ports].map(([name, port]) => [port, name]));
69
+ const problems = [];
70
+ for (const resource of resources) {
71
+ for (const [key, value] of Object.entries(readParams(resource.configPath))) {
72
+ for (const [, host, portText] of String(value).matchAll(CONTAINER_URL)) {
73
+ const port = Number(portText);
74
+ const expected = ports.get(host);
75
+ if (expected !== undefined && expected !== port) {
76
+ problems.push(`${resource.name} param ${key}: ${host}:${port}, but ${host} listens on ${expected}`);
77
+ }
78
+ else if ((host === "localhost" || host === "127.0.0.1") &&
79
+ byPort.has(port) &&
80
+ byPort.get(port) !== resource.name) {
81
+ problems.push(`${resource.name} param ${key}: localhost:${port} is ${resource.name} itself inside its container; ${byPort.get(port)} is reached as ${byPort.get(port)}:${port}`);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ if (problems.length === 0) {
87
+ return {
88
+ name: "Service URL ports",
89
+ didPass: true,
90
+ message: "Service URLs in params point at the ports the services listen on",
91
+ };
92
+ }
93
+ return {
94
+ name: "Service URL ports",
95
+ didPass: false,
96
+ message: `${formatCount(problems.length, "service URL")} in params that cannot connect:\n ${problems.join("\n ")}`,
97
+ fix: 'TDK runs each backend on the "port" in its service.json, and containers reach each other by resource name (http://<resource>:<port>). Use that port in the URL',
98
+ };
99
+ }
100
+ function listSourceFiles(dir) {
101
+ const files = [];
102
+ let entries;
103
+ try {
104
+ entries = readdirSync(dir, { withFileTypes: true });
105
+ }
106
+ catch {
107
+ return files;
108
+ }
109
+ for (const entry of entries) {
110
+ const path = join(dir, entry.name);
111
+ if (entry.isDirectory()) {
112
+ if (!SKIPPED_DIRS.has(entry.name))
113
+ files.push(...listSourceFiles(path));
114
+ }
115
+ else if (SOURCE_EXTENSIONS.test(entry.name) && !TEST_FILE.test(path)) {
116
+ files.push(path);
117
+ }
118
+ }
119
+ return files;
120
+ }
121
+ const LOCALHOST_PORT = /(?:localhost|127\.0\.0\.1):(\d{2,5})\b/g;
122
+ const COMMENT_LINE = /^\s*(?:\/\/|\/\*|\*|<!--|#)/;
123
+ export function checkFrontendBackendUrls(projectRoot = findProjectRoot() ?? process.cwd()) {
124
+ const resources = discoverResourcesFromRoot(projectRoot);
125
+ const byPort = new Map([...backendPorts(resources)].map(([name, port]) => [port, name]));
126
+ const problems = [];
127
+ for (const resource of resources.filter((r) => r.config?.appType === "frontend")) {
128
+ for (const file of listSourceFiles(join(resource.path, "src"))) {
129
+ readFileSync(file, "utf-8")
130
+ .split("\n")
131
+ .forEach((line, index) => {
132
+ if (COMMENT_LINE.test(line))
133
+ return;
134
+ for (const [, portText] of line.matchAll(LOCALHOST_PORT)) {
135
+ const backend = byPort.get(Number(portText));
136
+ if (backend) {
137
+ problems.push(`${relative(projectRoot, file)}:${index + 1} calls localhost:${portText} (${backend})`);
138
+ }
139
+ }
140
+ });
141
+ }
142
+ }
143
+ if (problems.length === 0) {
144
+ return {
145
+ name: "Frontend backend URLs",
146
+ didPass: true,
147
+ message: "No frontend calls a backend through a localhost port",
148
+ };
149
+ }
150
+ const project = projectName(projectRoot) ?? "<project>";
151
+ return {
152
+ name: "Frontend backend URLs",
153
+ didPass: false,
154
+ message: `${formatCount(problems.length, "frontend call")} to a backend port that TDK does not publish on localhost:\n ${problems.join("\n ")}`,
155
+ fix: `Backends are only reachable through Traefik: http://api.${project}.localhost/api/<name without -api>/... (for example catalog-api is /api/catalog). Use that URL, or the generated .autogenerated/env.ts`,
156
+ };
157
+ }
158
+ export function checkNatsBroker(projectRoot = findProjectRoot() ?? process.cwd()) {
159
+ const users = discoverResourcesFromRoot(projectRoot)
160
+ .filter((resource) => resource.config?.featuresEnabled?.includes("nats"))
161
+ .map((resource) => resource.name);
162
+ if (users.length === 0) {
163
+ return {
164
+ name: "NATS broker",
165
+ didPass: true,
166
+ message: "No resource enables the nats feature",
167
+ isSkipped: true,
168
+ };
169
+ }
170
+ if (existsSync(join(projectRoot, "services", "platform", "messaging", "docker-compose.yml"))) {
171
+ return {
172
+ name: "NATS broker",
173
+ didPass: true,
174
+ message: `${formatCount(users.length, "resource")} enable nats and services/platform/messaging/docker-compose.yml is present`,
175
+ };
176
+ }
177
+ const name = (projectName(projectRoot) ?? "project").replace(/-/g, "_");
178
+ return {
179
+ name: "NATS broker",
180
+ didPass: false,
181
+ message: `${users.join(", ")} enable the nats feature, but no NATS server will start: TDK only starts one when services/platform/messaging/docker-compose.yml exists, and it does not generate that file`,
182
+ fix: `Create it with a "nats" service (image nats with -js, container_name ${name}_nats, network ${name}_backend, external) and a "redis" service, which TDK registers alongside it`,
183
+ };
184
+ }
185
+ export function parseTiltProcesses(psOutput) {
186
+ const processes = [];
187
+ for (const line of psOutput.split("\n")) {
188
+ const match = line.match(/^\s*(\d+)\s+.*\btilt up\b.*?\s-f\s+(\S+)/);
189
+ if (!match)
190
+ continue;
191
+ processes.push({ pid: Number(match[1]), root: match[2].split("/.tdk/")[0] });
192
+ }
193
+ return processes;
194
+ }
195
+ export function checkTiltInstances(projectRoot = findProjectRoot() ?? process.cwd(), exec = execSync) {
196
+ let output;
197
+ try {
198
+ output = exec("ps -Ao pid=,command=", { encoding: "utf-8", timeout: EXEC_TIMEOUT_MS });
199
+ }
200
+ catch {
201
+ return {
202
+ name: "Tilt processes",
203
+ didPass: true,
204
+ isSkipped: true,
205
+ message: "Could not list processes",
206
+ };
207
+ }
208
+ const running = parseTiltProcesses(output);
209
+ const mine = running.filter((p) => p.root === projectRoot);
210
+ const others = running.filter((p) => p.root !== projectRoot);
211
+ if (mine.length > 1) {
212
+ return {
213
+ name: "Tilt processes",
214
+ didPass: false,
215
+ message: `${mine.length} Tilt processes are running for this project (pids ${mine.map((p) => p.pid).join(", ")}). They fight over ports and containers`,
216
+ fix: `Stop them all, then start one: tdk down && kill ${mine.map((p) => p.pid).join(" ")} && tdk up`,
217
+ };
218
+ }
219
+ if (others.length > 0) {
220
+ const list = others.map((p) => `${p.root} (pid ${p.pid})`).join(", ");
221
+ return {
222
+ name: "Tilt processes",
223
+ didPass: true,
224
+ isSkipped: true,
225
+ message: `Another project's Tilt is running: ${list}`,
226
+ fix: "tdk up will use the next free Tilt UI port (10351, ...). If it is a leftover, run tdk down in that project",
227
+ };
228
+ }
229
+ return { name: "Tilt processes", didPass: true, message: "No stray Tilt processes" };
230
+ }
231
+ export function checkDockerNetworkCapacity(exec = execSync) {
232
+ const probe = `tdk_doctor_probe_${process.pid}`;
233
+ try {
234
+ exec(`docker network create ${probe}`, { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
235
+ }
236
+ catch (error) {
237
+ const stderr = String(error.stderr ?? error);
238
+ if (/address pools/i.test(stderr)) {
239
+ return {
240
+ name: "Docker networks",
241
+ didPass: false,
242
+ message: "Docker cannot create another network: all predefined address pools are in use. Each TDK project needs several",
243
+ fix: "docker network prune (removes unused networks), or run tdk down in projects you are not using",
244
+ };
245
+ }
246
+ return {
247
+ name: "Docker networks",
248
+ didPass: true,
249
+ isSkipped: true,
250
+ message: "Could not test network creation",
251
+ };
252
+ }
253
+ try {
254
+ exec(`docker network rm ${probe}`, { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
255
+ }
256
+ catch {
257
+ // A leftover probe network is harmless and tdk_doctor_probe_* is easy to prune.
258
+ }
259
+ return { name: "Docker networks", didPass: true, message: "Docker can create networks" };
260
+ }
261
+ //# sourceMappingURL=doctor-wiring.js.map
@@ -16,6 +16,10 @@ export const PROJECT_TEMPLATES = {
16
16
  repo: "https://github.com/tdk-landscape/tdk-user-management.git",
17
17
  description: "User/auth management services",
18
18
  },
19
+ ecommerce: {
20
+ repo: "https://github.com/tdk-landscape/tdk-ecommerce-example.git",
21
+ description: "Vue 3 storefront and Hono catalog API (needs TDK 1.3.75+ for --framework vue)",
22
+ },
19
23
  example: {
20
24
  repo: "https://github.com/tdk-landscape/tdk-example.git",
21
25
  description: "Minimal Project-Stack-Resource (PSR) demonstration",
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.75",
4
+ "version": "1.3.76",
5
5
  "description": "TDK CLI - Tilt Development Kit command line interface",
6
6
  "keywords": [
7
7
  "tdk",
@@ -279,9 +279,30 @@ if FOCUS_MODE and FOCUS_ENABLED_RESOURCES:
279
279
  else:
280
280
  _MATCHED = [kr for kr in _KNOWN_TILT_RESOURCES if kr != 'service-discovery-daemon' and kr.startswith(_R + '-')]
281
281
  _FILTERED_RESOURCES.extend(_MATCHED)
282
- for _INFRA in ['init-networks', 'postgres', 'traefik', 'golden-layers-build']:
283
- if _INFRA in _KNOWN_TILT_RESOURCES:
284
- _FILTERED_RESOURCES.append(_INFRA)
282
+ # NATS is gated by the same 'database-management' should_enable() check as
283
+ # postgres (see infra-loader.star), so it belongs with the infra that is
284
+ # always kept enabled here -- otherwise config.set_enabled_resources()
285
+ # below disables it on every out-of-the-box `tdk up` (pre-alpha always
286
+ # activates this focus-filter branch), and any resource that enables the
287
+ # 'nats' feature never gets a broker to connect to.
288
+ #
289
+ # services/platform/messaging/docker-compose.yml is user-authored (TDK
290
+ # does not generate it) and may declare more than nats/redis -- e.g. a
291
+ # local mail catcher standing in for a managed email provider. Every
292
+ # service it declares is loaded via docker_compose() in infra-loader.star,
293
+ # so it must be enabled here too, or it is silently disabled the same way.
294
+ _ALWAYS_ENABLED_INFRA_RESOURCES = ['init-networks', 'postgres', 'traefik', 'golden-layers-build']
295
+ _messaging_compose_path = PROJECT_ROOT + '/services/platform/messaging/docker-compose.yml'
296
+ if str(local("test -f '" + _messaging_compose_path + "' && echo yes || echo no", quiet=True, echo_off=True)).strip() == 'yes':
297
+ # infra-loader.star only creates these Tilt resources when this file
298
+ # exists, so only add them here when they actually exist to add.
299
+ _messaging_compose_doc = read_yaml(_messaging_compose_path, default={})
300
+ for _messaging_svc_name in _messaging_compose_doc.get('services', {}).keys():
301
+ _ALWAYS_ENABLED_INFRA_RESOURCES.append(_messaging_svc_name)
302
+ if 'nats' not in _ALWAYS_ENABLED_INFRA_RESOURCES:
303
+ _ALWAYS_ENABLED_INFRA_RESOURCES.append('nats')
304
+ for _INFRA in _ALWAYS_ENABLED_INFRA_RESOURCES:
305
+ _FILTERED_RESOURCES.append(_INFRA)
285
306
  for _KR in _KNOWN_TILT_RESOURCES:
286
307
  if _KR.startswith('provision-db-'):
287
308
  _FILTERED_RESOURCES.append(_KR)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdk-landscape/tdk-cli-core",
3
- "version": "1.3.75",
3
+ "version": "1.3.76",
4
4
  "description": "Run your whole microservice landscape locally without Kubernetes. Scaffold services and run them with hot reload, built on Tilt.",
5
5
  "homepage": "https://tdk-landscape.github.io/tdk-website",
6
6
  "repository": {