@tdk-landscape/tdk-cli-core 1.3.109 → 1.3.110

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,8 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ - `tdk config verify` no longer fails on a root `.tiltignore` the repository already had. `tdk project` never overwrote it, but verify compared it with the generated one and reported `Out of sync: .tiltignore`; it now warns `not generated by TDK, left as is`. A `.tiltignore` carrying TDK's generated header that was edited is still reported. Refs #568.
8
+ - A repo can pin the oldest CLI it works with: add `"minTdkVersion": "1.3.80"` to `.tdk/project.json`, and `tdk doctor` fails with `Run: tdk upgrade` on an older `tdk` (a value that is not `MAJOR.MINOR.PATCH` also fails). It is a floor, not an exact pin. Documented in `docs/upgrading.md`. Refs #564.
7
9
  - `tdk doctor` warns when a backend or worker applies database migrations from its own start-up (a `start`/`dev` script, a Dockerfile `CMD`/`ENTRYPOINT`, a shell entrypoint or its source: Prisma, Drizzle, Knex, TypeORM, MikroORM, Sequelize, dbmate, Atlas, Alembic). It is a warning, not a failure: such services work, but migrations belong in a `migrator` resource that runs once before the services that depend on it. `migrator` and bring-your-own resources are not checked.
8
10
  - 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.
9
11
  - `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.
@@ -15,7 +15,7 @@ import { execAsync, isExecTimeout } from "../utils/exec-async.js";
15
15
  import { formatCount } from "../utils/formatting.js";
16
16
  import { getHostPortPlan } from "../utils/host-port-config.js";
17
17
  import { createHostPortPlan } from "../utils/host-port-plan.js";
18
- import { findProjectRoot } from "../utils/paths.js";
18
+ import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
19
19
  import { isApiServiceType } from "../utils/resource-kind.js";
20
20
  import { buildHealthTargets, pingHealthTargets } from "../utils/service-urls.js";
21
21
  import { discoverResourcesFromRoot } from "../utils/services.js";
@@ -416,6 +416,60 @@ function checkMasterConfigs() {
416
416
  fix: "Run: tdk project",
417
417
  };
418
418
  }
419
+ /**
420
+ * A repo can pin the oldest CLI it works with: `"minTdkVersion": "1.3.80"` in
421
+ * .tdk/project.json. An older `tdk` fails here, so a team sees one clear line
422
+ * instead of a half-working `tdk up`.
423
+ */
424
+ export function checkTdkVersion(currentVersion = getPackageVersion()) {
425
+ const projectRoot = findProjectRoot();
426
+ if (!projectRoot) {
427
+ return {
428
+ name: "TDK version",
429
+ didPass: true,
430
+ isSkipped: true,
431
+ message: "Not in a project - skipping TDK version check",
432
+ };
433
+ }
434
+ let minimum;
435
+ try {
436
+ const parsed = JSON.parse(readFileSync(join(projectRoot, ".tdk", "project.json"), "utf-8"));
437
+ minimum = parsed?.minTdkVersion;
438
+ }
439
+ catch {
440
+ minimum = undefined;
441
+ }
442
+ if (minimum === undefined) {
443
+ return {
444
+ name: "TDK version",
445
+ didPass: true,
446
+ isSkipped: true,
447
+ message: `tdk ${currentVersion} (no minTdkVersion in .tdk/project.json)`,
448
+ };
449
+ }
450
+ const required = typeof minimum === "string" ? parseVersion(minimum) : null;
451
+ if (!required || !/^\d+\.\d+\.\d+$/.test(String(minimum).trim())) {
452
+ return {
453
+ name: "TDK version",
454
+ didPass: false,
455
+ message: `.tdk/project.json minTdkVersion ${JSON.stringify(minimum)} is not a version like "1.3.80"`,
456
+ fix: 'Set minTdkVersion to a MAJOR.MINOR.PATCH string, for example "1.3.80".',
457
+ };
458
+ }
459
+ if (!versionMeetsMinimum(currentVersion, required)) {
460
+ return {
461
+ name: "TDK version",
462
+ didPass: false,
463
+ message: `tdk ${currentVersion} is older than the ${required.join(".")} this project requires (minTdkVersion)`,
464
+ fix: "Run: tdk upgrade",
465
+ };
466
+ }
467
+ return {
468
+ name: "TDK version",
469
+ didPass: true,
470
+ message: `tdk ${currentVersion} meets minTdkVersion ${required.join(".")}`,
471
+ };
472
+ }
419
473
  /**
420
474
  * Tilt only builds resources under `discovery.paths`, but the CLI finds every service.json,
421
475
  * so a resource outside them is listed and given a URL by `tdk up` yet never started.
@@ -1092,6 +1146,7 @@ export const doctorCommand = new Command("doctor")
1092
1146
  () => checkDockerNetworkCapacity(),
1093
1147
  ];
1094
1148
  const projectChecks = [
1149
+ () => checkTdkVersion(),
1095
1150
  checkMasterConfigs,
1096
1151
  checkGeneratedProjectRuntimeAssets,
1097
1152
  checkStarlarkLoadExports,
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
5
5
  import { DetailPanel, FileTree, ResourceSelectInput, ResourceTable, TabBar, } from "../components/index.js";
6
6
  import { errorFactories, requireProjectRoot } from "../utils/errors.js";
7
7
  import { findProjectRoot } from "../utils/paths.js";
8
+ import { describeSearch } from "../utils/search-status.js";
8
9
  import { clearMetadataCache, discoverResources, discoverStacks, getResourceMetadata, getStackMetadata, } from "../utils/services.js";
9
10
  import { createStatusMessageController } from "../utils/status-message.js";
10
11
  import { isTiltAvailable } from "../utils/tilt.js";
@@ -159,6 +160,15 @@ const TUIApp = ({ animated = true }) => {
159
160
  return [];
160
161
  }, [activeTab, filteredStacks, filteredServices, selectedStackData, selectedServiceData]);
161
162
  const items = getItems();
163
+ // Only the stack and service lists are filtered by the query; drilled-in views are not.
164
+ const searchList = activeTab === "overview" || (activeTab === "resources" && !selectedStackData)
165
+ ? { total: stacks.length }
166
+ : (activeTab === "files" && !selectedServiceData) || activeTab === "config"
167
+ ? { total: services.length }
168
+ : null;
169
+ const searchStatus = searchList
170
+ ? describeSearch(searchQuery, items.length, searchList.total)
171
+ : null;
162
172
  useEffect(() => {
163
173
  if (highlightedIndex >= items.length && items.length > 0) {
164
174
  setHighlightedIndex(items.length - 1);
@@ -419,7 +429,7 @@ const TUIApp = ({ animated = true }) => {
419
429
  if (services.length === 0) {
420
430
  return _jsx(EmptyState, { message: message });
421
431
  }
422
- 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
432
+ 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 && (_jsxs(Box, { paddingX: 1, height: 1, children: [_jsxs(Text, { color: "yellow", children: ["Search: ", searchQuery, "_"] }), searchStatus && _jsx(Text, { color: "gray", dimColor: true, children: ` ${searchStatus.summary}` })] })), searchStatus?.emptyMessage && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: "gray", children: searchStatus.emptyMessage }) })), !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
423
433
  ? `Stack "${selectedStack}" selected. [Enter] view │ [Esc] back │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help`
424
434
  : activeTab === "overview" && !selectedStack
425
435
  ? `[↑/↓] Navigate │ [Enter] Select │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help`
@@ -603,6 +603,7 @@ async function vendorTdkExtension(projectRoot) {
603
603
  console.warn(`⚠️ Premium overlay skipped: ${err.message}`);
604
604
  }
605
605
  }
606
+ const TDK_GENERATED_MARKER = "SYSTEM-GENERATED BY TDK CLI";
606
607
  export function verifyMasterConfigs(projectRoot) {
607
608
  const errors = [];
608
609
  const warnings = [];
@@ -626,8 +627,18 @@ export function verifyMasterConfigs(projectRoot) {
626
627
  const filePath = path.join(outputDir, filename);
627
628
  addDrift(`.tdk/.tdk-out/${filename}`, fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : undefined, expectedContent);
628
629
  }
630
+ // `tdk project` never overwrites a root .tiltignore the repo already has, so one
631
+ // without TDK's header belongs to the team: it is not drift.
629
632
  const rootTiltIgnore = path.join(projectRoot, ".tiltignore");
630
- addDrift(".tiltignore", fs.existsSync(rootTiltIgnore) ? fs.readFileSync(rootTiltIgnore, "utf-8") : undefined, expectedFiles[".tiltignore"]);
633
+ const rootTiltIgnoreText = fs.existsSync(rootTiltIgnore)
634
+ ? fs.readFileSync(rootTiltIgnore, "utf-8")
635
+ : undefined;
636
+ if (rootTiltIgnoreText !== undefined && !rootTiltIgnoreText.includes(TDK_GENERATED_MARKER)) {
637
+ warnings.push(".tiltignore: not generated by TDK, left as is");
638
+ }
639
+ else {
640
+ addDrift(".tiltignore", rootTiltIgnoreText, expectedFiles[".tiltignore"]);
641
+ }
631
642
  for (const manifestPath of discoverServiceManifestPaths(projectRoot)) {
632
643
  const displayPath = path.relative(projectRoot, manifestPath).split(path.sep).join("/");
633
644
  const validation = validateServiceManifestFile(manifestPath, displayPath);
@@ -0,0 +1,13 @@
1
+ /** Describes a `/` search result for the `tdk ui` header; null when there is no query. */
2
+ export function describeSearch(query, matches, total) {
3
+ if (!query)
4
+ return null;
5
+ if (matches === 0) {
6
+ return {
7
+ summary: "0 matches",
8
+ emptyMessage: `No resources match "${query}". Press Esc to clear.`,
9
+ };
10
+ }
11
+ return { summary: `${matches} of ${total}` };
12
+ }
13
+ //# sourceMappingURL=search-status.js.map
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.109",
4
+ "version": "1.3.110",
5
5
  "description": "TDK CLI — start services on your laptop.",
6
6
  "keywords": [
7
7
  "tdk",
@@ -178,7 +178,7 @@ FROM l4_backend_bun AS l4_backend_golden
178
178
  # =============================================================================
179
179
  # L4-FRONTEND: FRONTEND PRODUCTION RUNTIME
180
180
  # =============================================================================
181
- FROM nginx:alpine AS l4_frontend_golden
181
+ FROM nginx:1.27-alpine AS l4_frontend_golden
182
182
 
183
183
  LABEL layer="l4-frontend" description="Frontend production runtime with Nginx" maintainer="tdk-project"
184
184
 
@@ -16,6 +16,11 @@
16
16
  "version": {
17
17
  "type": "string"
18
18
  },
19
+ "minTdkVersion": {
20
+ "type": "string",
21
+ "pattern": "^\\d+\\.\\d+\\.\\d+$",
22
+ "description": "Oldest tdk CLI this project works with, as MAJOR.MINOR.PATCH. tdk doctor fails on an older CLI."
23
+ },
19
24
  "project": {
20
25
  "type": "object",
21
26
  "additionalProperties": true,
@@ -227,7 +227,7 @@ FROM l4_backend_bun AS l4_backend_golden
227
227
  # =============================================================================
228
228
  # L4-FRONTEND: FRONTEND PRODUCTION RUNTIME
229
229
  # =============================================================================
230
- FROM nginx:alpine AS l4_frontend_golden
230
+ FROM nginx:1.27-alpine AS l4_frontend_golden
231
231
 
232
232
  LABEL layer="l4-frontend" \
233
233
  description="Frontend production runtime with Nginx" \
@@ -1,6 +1,6 @@
1
1
  # === INLINED CONSTANTS for pure extension loading ===
2
2
  WEB_FRAMEWORK = "hono"
3
- RUNTIME_CONFIG = {"bun": {"image_base": "oven/bun:latest", "package_manager": "bun", "lockfile": "bun.lock", "backend_start_command": "bun run start", "frontend_start_command": "bun run dev"}, "node": {"image_base": "node:20-alpine", "package_manager": "npm", "lockfile": "package-lock.json", "backend_start_command": "node dist/index.js"}}
3
+ RUNTIME_CONFIG = {"bun": {"image_base": "oven/bun:1.3.11-alpine", "package_manager": "bun", "lockfile": "bun.lock", "backend_start_command": "bun run start", "frontend_start_command": "bun run dev"}, "node": {"image_base": "node:20-alpine", "package_manager": "npm", "lockfile": "package-lock.json", "backend_start_command": "node dist/index.js"}}
4
4
  OUTPUT = struct(AUTOGENERATED_FOLDER = ".autogenerated")
5
5
  # === END INLINED CONSTANTS ===
6
6
 
@@ -215,7 +215,7 @@ def L4_generate_frontend_runtime(res_path, target_path = '/usr/share/nginx/html'
215
215
 
216
216
  if use_nginx:
217
217
  # Use golden L4-frontend image if enabled (Nginx pre-configured)
218
- base_image = GOLDEN_L4_FRONTEND_IMAGE if use_golden else "nginx:alpine"
218
+ base_image = GOLDEN_L4_FRONTEND_IMAGE if use_golden else "nginx:1.27-alpine"
219
219
 
220
220
  # If using golden image, skip curl installation (already there)
221
221
  healthcheck_setup = "" if use_golden else "RUN apk add --no-cache curl\n"
@@ -195,7 +195,7 @@ DEFAULT_SYNCS = {
195
195
 
196
196
  RUNTIME_CONFIGS = {
197
197
  'bun': {
198
- 'image_base': 'oven/bun:latest',
198
+ 'image_base': 'oven/bun:1.3.11-alpine',
199
199
  'package_manager': 'bun',
200
200
  'lockfile': 'bun.lock',
201
201
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdk-landscape/tdk-cli-core",
3
- "version": "1.3.109",
3
+ "version": "1.3.110",
4
4
  "description": "TDK CLI — start services on your laptop.",
5
5
  "homepage": "https://tdk-landscape.github.io/tdk-website",
6
6
  "repository": {