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

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +59 -13
  3. package/cli/bin/tdk.js +22 -2
  4. package/cli/dist/cli.js +9 -2
  5. package/cli/dist/commands/completion.js +30 -3
  6. package/cli/dist/commands/config.js +59 -1
  7. package/cli/dist/commands/doctor.js +295 -98
  8. package/cli/dist/commands/eject.js +77 -0
  9. package/cli/dist/commands/networks.js +17 -4
  10. package/cli/dist/commands/project.js +28 -5
  11. package/cli/dist/commands/resource.js +118 -10
  12. package/cli/dist/commands/up.js +93 -21
  13. package/cli/dist/commands/upgrade.js +163 -68
  14. package/cli/dist/generator/template-engine.js +77 -13
  15. package/cli/dist/types/index.js +6 -1
  16. package/cli/dist/utils/cold-preflight.js +198 -0
  17. package/cli/dist/utils/constants.js +4 -0
  18. package/cli/dist/utils/docker.js +15 -1
  19. package/cli/dist/utils/doctor-runtime.js +48 -16
  20. package/cli/dist/utils/doctor-wiring.js +269 -0
  21. package/cli/dist/utils/errors.js +5 -5
  22. package/cli/dist/utils/exec-async.js +30 -0
  23. package/cli/dist/utils/generated-paths.js +49 -0
  24. package/cli/dist/utils/platform.js +53 -0
  25. package/cli/dist/utils/port-assignment.js +13 -20
  26. package/cli/dist/utils/project-templates.js +7 -2
  27. package/cli/dist/utils/resource-features.js +6 -2
  28. package/cli/dist/utils/service-manifest.js +73 -0
  29. package/cli/dist/utils/services.js +8 -1
  30. package/cli/dist/utils/tar.js +27 -5
  31. package/cli/dist/utils/tilt-process.js +53 -0
  32. package/cli/dist/utils/tilt.js +3 -1
  33. package/cli/dist/utils/which.js +26 -0
  34. package/cli/dist/utils/windows-doctor.js +175 -0
  35. package/cli/package.json +1 -1
  36. package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -2
  37. package/cli/templates/TILT_TECH_STACK.star.hbs +0 -1
  38. package/cli/templates/Tiltfile.hbs +24 -4
  39. package/cli/templates/spec.master.hbs +1 -2
  40. package/discovery/AGENTS.md +1 -1
  41. package/discovery/constants.star +2 -2
  42. package/discovery/discovery_orchestrator.star +2 -2
  43. package/discovery/manifest/constants.star +1 -1
  44. package/discovery/manifest/loading.star +7 -3
  45. package/discovery/registry.star +17 -3
  46. package/engine/README.md +2 -0
  47. package/engine/schemas/service-schema.json +47 -21
  48. package/engine/scripts/generate-manifest.sh +2 -1
  49. package/engine/topologies/platform/docker/compose/compose.star +37 -5
  50. package/engine/topologies/tilt/common/utils.star +54 -11
  51. package/engine/topologies/tilt/manifest/AGENTS.md +1 -1
  52. package/engine/topologies/tilt/manifest/constants.star +3 -0
  53. package/engine/topologies/tilt/manifest/schema.star +2 -4
  54. package/engine/topologies/tilt/manifest/validator.star +1 -1
  55. package/engine/topologies/tilt/resources/infra-loader.star +14 -3
  56. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +15 -11
  57. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +15 -6
  58. package/examples/tdk-example/LICENSE +21 -0
  59. package/examples/tdk-example/README.md +23 -0
  60. package/examples/tdk-example/TILT_SERVICE_DEFAULTS.star +42 -0
  61. package/examples/tdk-example/TILT_TECH_STACK.star +67 -0
  62. package/examples/tdk-example/Tiltfile +25 -0
  63. package/examples/tdk-example/services/platform/messaging/docker-compose.yml +26 -0
  64. package/examples/tdk-example/services/shop/orders-api/package.json +20 -0
  65. package/examples/tdk-example/services/shop/orders-api/service.json +14 -0
  66. package/examples/tdk-example/services/shop/orders-api/src/index.ts +81 -0
  67. package/examples/tdk-example/services/shop/orders-app/index.html +12 -0
  68. package/examples/tdk-example/services/shop/orders-app/package.json +20 -0
  69. package/examples/tdk-example/services/shop/orders-app/public/.gitkeep +0 -0
  70. package/examples/tdk-example/services/shop/orders-app/service.json +9 -0
  71. package/examples/tdk-example/services/shop/orders-app/src/main.tsx +9 -0
  72. package/examples/tdk-example/services/shop/orders-app/src/orders.tsx +67 -0
  73. package/examples/tdk-example/services/shop/orders-app/src/style.css +15 -0
  74. package/examples/tdk-example/services/shop/orders-worker/package.json +18 -0
  75. package/examples/tdk-example/services/shop/orders-worker/service.json +13 -0
  76. package/examples/tdk-example/services/shop/orders-worker/src/index.ts +33 -0
  77. package/package.json +2 -1
@@ -0,0 +1,175 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { lookup } from "node:dns/promises";
3
+ import { parsePublishedPortHolders, probeHostPort } from "./doctor-runtime.js";
4
+ import { findProjectRoot } from "./paths.js";
5
+ import { getProjectName } from "./service-urls.js";
6
+ import { findOnPath } from "./which.js";
7
+ const WINDOWS_DNS_FAILURE = "Windows did not resolve *.tdk.localhost to 127.0.0.1. Browser URLs with hostnames will fail until you fix hosts or use the 127.0.0.1 URLs printed by tdk networks.";
8
+ function loopbackAddresses(addresses) {
9
+ return (addresses.length > 0 &&
10
+ addresses.every((address) => address === "127.0.0.1" || address === "::1"));
11
+ }
12
+ export function parseExcludedPortRanges(output) {
13
+ return output.split(/\r?\n/).flatMap((line) => {
14
+ const match = line.match(/^\s*(\d+)\s+(\d+)(?:\s|$)/);
15
+ return match ? [[Number(match[1]), Number(match[2])]] : [];
16
+ });
17
+ }
18
+ export function findExternallyOccupiedPorts(occupied, dockerHolders, projectName) {
19
+ const prefix = projectName.replace(/-/g, "_").toLowerCase();
20
+ return occupied.filter((port) => {
21
+ const holder = dockerHolders.find((candidate) => candidate.publishedPorts.includes(port));
22
+ if (!holder || !prefix)
23
+ return true;
24
+ const name = holder.name.toLowerCase();
25
+ return !name.startsWith(`${prefix}_`) && !name.startsWith(`${prefix}-`);
26
+ });
27
+ }
28
+ function checkDockerTool() {
29
+ return findOnPath("docker");
30
+ }
31
+ export function checkWindowsRuntimeTools() {
32
+ const docker = checkDockerTool();
33
+ if (!docker) {
34
+ return {
35
+ name: "Windows Runtime Tools",
36
+ didPass: false,
37
+ message: "Docker is not running. Start Docker Desktop, wait until it is Running, then retry tdk doctor.",
38
+ fix: "Install Docker Desktop and start it in Linux container mode.",
39
+ };
40
+ }
41
+ if (!findOnPath("tilt")) {
42
+ return {
43
+ name: "Windows Runtime Tools",
44
+ didPass: false,
45
+ message: "tilt.exe not found on PATH. Install Tilt from https://docs.tilt.dev/install.html",
46
+ };
47
+ }
48
+ const bun = findOnPath("bun");
49
+ const npmInstall = (process.argv[1] ?? "").toLowerCase().includes("node_modules");
50
+ const node = npmInstall ? findOnPath("node") : true;
51
+ if (!node) {
52
+ return {
53
+ name: "Windows Runtime Tools",
54
+ didPass: false,
55
+ message: "node.exe not found on PATH for this npm installation.",
56
+ };
57
+ }
58
+ return {
59
+ name: "Windows Runtime Tools",
60
+ didPass: true,
61
+ message: bun
62
+ ? `Docker (${docker}), Tilt, and Bun found on PATH`
63
+ : `Docker (${docker}) and Tilt found on PATH; ⚠️ Bun was not found (generated services need Bun)`,
64
+ };
65
+ }
66
+ export function checkWindowsDockerMode() {
67
+ try {
68
+ const osType = execFileSync(findOnPath("docker") ?? "docker", ["info", "--format", "{{.OSType}}"], {
69
+ encoding: "utf-8",
70
+ timeout: 10_000,
71
+ windowsHide: true,
72
+ }).trim();
73
+ if (osType.toLowerCase() === "windows") {
74
+ return {
75
+ name: "Docker container mode",
76
+ didPass: false,
77
+ message: "Docker Desktop is using Windows containers. Switch to Linux containers. TDK stacks are Linux images.",
78
+ fix: "Switch Docker Desktop to Linux containers. TDK does not run Windows containers.",
79
+ };
80
+ }
81
+ return {
82
+ name: "Docker container mode",
83
+ didPass: true,
84
+ message: "Docker Desktop is using Linux containers",
85
+ };
86
+ }
87
+ catch {
88
+ return {
89
+ name: "Docker container mode",
90
+ didPass: false,
91
+ message: "Docker is not running. Start Docker Desktop, wait until it is Running, then retry tdk doctor.",
92
+ };
93
+ }
94
+ }
95
+ export async function checkWindowsHostConfiguration() {
96
+ const warnings = [];
97
+ let excludedRanges = [];
98
+ try {
99
+ const output = execFileSync("netsh", ["interface", "ipv4", "show", "excludedportrange", "protocol=tcp"], { encoding: "utf-8", timeout: 10_000, windowsHide: true });
100
+ excludedRanges = parseExcludedPortRanges(output);
101
+ }
102
+ catch {
103
+ warnings.push("Could not inspect Windows Hyper-V excluded TCP port ranges.");
104
+ }
105
+ const requiredPorts = [80, 443, 5432];
106
+ const excluded = requiredPorts.filter((port) => excludedRanges.some(([start, end]) => port >= start && port <= end));
107
+ if (excluded.length > 0) {
108
+ warnings.push(`⚠️ Windows reserved port${excluded.length > 1 ? "s" : ""} ${excluded.join(", ")} for Hyper-V/WSL. Fix: disable the conflicting Windows feature or change TDK ports in project config. Common fix: run \`net stop winnat\` then \`net start winnat\` in an Administrator terminal, then retry \`tdk doctor\`.`);
109
+ }
110
+ const occupied = [];
111
+ for (const port of requiredPorts) {
112
+ if ((await probeHostPort(port)) === "in-use")
113
+ occupied.push(port);
114
+ }
115
+ let dockerHolders = [];
116
+ try {
117
+ const dockerPs = execFileSync(findOnPath("docker") ?? "docker", ["ps", "--format", "{{.Names}}\\t{{.Ports}}"], { encoding: "utf-8", timeout: 10_000, windowsHide: true });
118
+ dockerHolders = parsePublishedPortHolders(dockerPs, requiredPorts);
119
+ }
120
+ catch {
121
+ // Runtime checks report Docker daemon errors independently.
122
+ }
123
+ const projectName = findProjectRoot() ? getProjectName() : "";
124
+ const externallyOccupied = findExternallyOccupiedPorts(occupied, dockerHolders, projectName);
125
+ if (externallyOccupied.length > 0) {
126
+ try {
127
+ const owners = execFileSync("powershell.exe", [
128
+ "-NoProfile",
129
+ "-Command",
130
+ `$ports = '${externallyOccupied.join(",")}'.Split(','); foreach ($port in $ports) { Get-NetTCPConnection -LocalPort ([int]$port) -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $connection = $_; $service = Get-CimInstance Win32_Service | Where-Object { $_.ProcessId -eq $connection.OwningProcess } | Select-Object -First 1; if ([int]$port -eq 80 -and ($service.Name -eq 'W3SVC' -or $connection.OwningProcess -eq 4)) { '80: IIS / World Wide Web Publishing (HTTP.sys)' } elseif ($service) { '{0}: {1}' -f $port, $service.DisplayName } else { '{0}: PID {1}' -f $port, $connection.OwningProcess } } }`,
131
+ ], { encoding: "utf-8", timeout: 10_000, windowsHide: true }).trim();
132
+ warnings.push(`⚠️ Ports ${externallyOccupied.join(", ")} are already listening${owners ? `:\n${owners}` : "."}`);
133
+ }
134
+ catch {
135
+ warnings.push(`⚠️ Ports ${externallyOccupied.join(", ")} are already in use.`);
136
+ }
137
+ }
138
+ let dnsOk = true;
139
+ for (const host of ["tdk.localhost", "example.tdk.localhost"]) {
140
+ try {
141
+ const records = await lookup(host, { all: true, verbatim: true });
142
+ if (!loopbackAddresses(records.map((record) => record.address)))
143
+ dnsOk = false;
144
+ }
145
+ catch {
146
+ dnsOk = false;
147
+ }
148
+ }
149
+ if (!dnsOk) {
150
+ warnings.push(`${WINDOWS_DNS_FAILURE}\nAdd these lines to C:\\Windows\\System32\\drivers\\etc\\hosts (Administrator Notepad):\n127.0.0.1 tdk.localhost\n127.0.0.1 example.tdk.localhost`);
151
+ }
152
+ try {
153
+ const autocrlf = execFileSync("git", ["config", "core.autocrlf"], {
154
+ encoding: "utf-8",
155
+ timeout: 5_000,
156
+ windowsHide: true,
157
+ }).trim();
158
+ if (autocrlf.toLowerCase() === "true") {
159
+ warnings.push("git core.autocrlf is true; generated files may get CRLF while Docker Linux containers expect LF. Recommended: git config core.autocrlf input in TDK repos.");
160
+ }
161
+ }
162
+ catch {
163
+ // No repository-local setting to report.
164
+ }
165
+ warnings.push("Docker Desktop Settings → Resources → File sharing must include this project directory.");
166
+ return {
167
+ name: "Windows ports and DNS",
168
+ didPass: dnsOk && externallyOccupied.length === 0,
169
+ message: warnings.length > 0
170
+ ? warnings.join("\n")
171
+ : "Required ports are available and TDK localhost DNS resolves to loopback",
172
+ ...(dnsOk ? {} : { fix: WINDOWS_DNS_FAILURE }),
173
+ };
174
+ }
175
+ //# sourceMappingURL=windows-doctor.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.75",
4
+ "version": "1.3.77",
5
5
  "description": "TDK CLI - Tilt Development Kit command line interface",
6
6
  "keywords": [
7
7
  "tdk",
@@ -6,7 +6,6 @@
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: TDK Platform Standards v{{version}}
8
8
  # Output: .tdk/.tdk-out/
9
- # Generated: {{timestamp}}
10
9
  # Run 'tdk project' to update
11
10
  # =============================================================================
12
11
 
@@ -218,4 +217,4 @@ __all__ = [
218
217
  "HEALTH_CHECK_TIMEOUT",
219
218
  "NAMING_PATTERNS",
220
219
  "TRAEFIK_CONFIG",
221
- ]
220
+ ]
@@ -6,7 +6,6 @@
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: TDK Platform Standards v{{version}}
8
8
  # Output: .tdk/.tdk-out/
9
- # Generated: {{timestamp}}
10
9
  # Run 'tdk project' to update
11
10
  # =============================================================================
12
11
 
@@ -5,7 +5,6 @@
5
5
  # =============================================================================
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: .tdk/project.json
8
- # Generated: {{timestamp}}
9
8
  # Run 'tdk project' to regenerate
10
9
  # =============================================================================
11
10
 
@@ -279,9 +278,30 @@ if FOCUS_MODE and FOCUS_ENABLED_RESOURCES:
279
278
  else:
280
279
  _MATCHED = [kr for kr in _KNOWN_TILT_RESOURCES if kr != 'service-discovery-daemon' and kr.startswith(_R + '-')]
281
280
  _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)
281
+ # NATS is gated by the same 'database-management' should_enable() check as
282
+ # postgres (see infra-loader.star), so it belongs with the infra that is
283
+ # always kept enabled here -- otherwise config.set_enabled_resources()
284
+ # below disables it on every out-of-the-box `tdk up` (pre-alpha always
285
+ # activates this focus-filter branch), and any resource that enables the
286
+ # 'nats' feature never gets a broker to connect to.
287
+ #
288
+ # services/platform/messaging/docker-compose.yml is user-authored (TDK
289
+ # does not generate it) and may declare more than nats/redis -- e.g. a
290
+ # local mail catcher standing in for a managed email provider. Every
291
+ # service it declares is loaded via docker_compose() in infra-loader.star,
292
+ # so it must be enabled here too, or it is silently disabled the same way.
293
+ _ALWAYS_ENABLED_INFRA_RESOURCES = ['init-networks', 'postgres', 'traefik', 'golden-layers-build']
294
+ _messaging_compose_path = PROJECT_ROOT + '/services/platform/messaging/docker-compose.yml'
295
+ if str(local("test -f '" + _messaging_compose_path + "' && echo yes || echo no", quiet=True, echo_off=True)).strip() == 'yes':
296
+ # infra-loader.star only creates these Tilt resources when this file
297
+ # exists, so only add them here when they actually exist to add.
298
+ _messaging_compose_doc = read_yaml(_messaging_compose_path, default={})
299
+ for _messaging_svc_name in _messaging_compose_doc.get('services', {}).keys():
300
+ _ALWAYS_ENABLED_INFRA_RESOURCES.append(_messaging_svc_name)
301
+ if 'nats' not in _ALWAYS_ENABLED_INFRA_RESOURCES:
302
+ _ALWAYS_ENABLED_INFRA_RESOURCES.append('nats')
303
+ for _INFRA in _ALWAYS_ENABLED_INFRA_RESOURCES:
304
+ _FILTERED_RESOURCES.append(_INFRA)
285
305
  for _KR in _KNOWN_TILT_RESOURCES:
286
306
  if _KR.startswith('provision-db-'):
287
307
  _FILTERED_RESOURCES.append(_KR)
@@ -6,7 +6,6 @@
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: .tdk/project.json + TDK Platform Standards v{{version}}
8
8
  # Output: .tdk/.tdk-out/
9
- # Generated: {{timestamp}}
10
9
  # Run 'tdk project' to update
11
10
  #
12
11
  # 🏗️ ARCHITECTURE: Project → Stack → Service (PSR Model)
@@ -169,7 +168,7 @@ BETA = FOCUS_BETA,
169
168
  # =============================================================================
170
169
  # 📝 CHANGE LOG
171
170
  # =============================================================================
172
- # {{timestamp}}: Generated by TDK CLI v{{version}}
171
+ # Generated by TDK CLI v{{version}}
173
172
  # =============================================================================
174
173
 
175
174
  # =============================================================================
@@ -195,7 +195,7 @@ The discovery system now validates all manifests during the scan process:
195
195
  | Field | Validation |
196
196
  |-------|------------|
197
197
  | `appName` | Required, 3-64 chars, kebab-case (lowercase, numbers, hyphens) |
198
- | `appType` | Required, must be: backend, frontend, library, migrator, sdk, worker |
198
+ | `appType` | Required, must be: backend, frontend, library, migrator, sdk, worker, bring-your-own |
199
199
  | `domain` | Required, must be a valid domain from `constants.star` |
200
200
  | `port` | Required, integer 3000-9999, appType-specific ranges enforced |
201
201
  | `features` | Optional, each must be a valid feature from `constants.star` |
@@ -149,7 +149,7 @@ MAX_SCAN_TIMEOUT_MS = 5000 # 5 seconds per root (warn if exceeded)
149
149
  MAX_MANIFESTS_PER_ROOT = 110 # Warn if root has more manifests
150
150
 
151
151
  # Autogenerated files directory (relative to this file's location)
152
- PRODUCT_SNAPSHOT_DIR = "../../product_autogenerated_resource_manifests"
152
+ PRODUCT_SNAPSHOT_DIR = ".tdk/.tdk-out/product_autogenerated_resource_manifests"
153
153
 
154
154
  PRODUCT_SNAPSHOT_FILES = {
155
155
  "services": "services.autogenerated.star",
@@ -160,6 +160,6 @@ PRODUCT_SNAPSHOT_FILES = {
160
160
  "readme": "README.autogenerated.md",
161
161
  }
162
162
 
163
- PRODUCT_TOPOLOGY_DIR = "../../product_autogenerated_services_list"
163
+ PRODUCT_TOPOLOGY_DIR = ".tdk/.tdk-out/product_autogenerated_services_list"
164
164
  PRODUCT_STACKS_INDEX_FILE = "stacks.autogenerated.star"
165
165
  PRODUCT_STACK_RESOURCES_FILE = "resources.autogenerated.star"
@@ -77,7 +77,7 @@ def _determine_app_type(manifest, resource_path):
77
77
  Determine app type from manifest or service path.
78
78
  """
79
79
  # Check if manifest has explicit appType
80
- if manifest.get("appType") in ["frontend", "backend", "library", "migrator", "sdk", "worker", "infra"]:
80
+ if manifest.get("appType") in ["frontend", "backend", "library", "migrator", "sdk", "worker", "infra", "bring-your-own"]:
81
81
  return manifest["appType"]
82
82
 
83
83
  # Extract from path
@@ -120,7 +120,7 @@ def normalize_manifest(manifest, resource_path):
120
120
 
121
121
  resource = {
122
122
  "name": app_name,
123
- "dockerfile": app_name + "/" + dockerfile,
123
+ "dockerfile": dockerfile if app_type == "bring-your-own" else app_name + "/" + dockerfile,
124
124
  "syncs": syncs,
125
125
  "port": port,
126
126
  "stack": stack,
@@ -28,7 +28,7 @@ MANIFEST_DEFAULTS = {
28
28
  }
29
29
 
30
30
  # Valid app types
31
- VALID_APP_TYPES = ["backend", "frontend", "library", "migrator", "sdk", "worker"]
31
+ VALID_APP_TYPES = ["backend", "frontend", "library", "migrator", "sdk", "worker", "bring-your-own"]
32
32
 
33
33
  # Default syncs
34
34
  DEFAULT_SYNCS = ["src", "prisma"]
@@ -39,7 +39,7 @@ def get_default_syncs_for_type(app_type, features=None):
39
39
  if features == None:
40
40
  features = []
41
41
 
42
- syncs = list(DEFAULT_SYNCS.get(app_type, ['src', 'package.json']))
42
+ syncs = list(DEFAULT_SYNCS.get(app_type, [] if app_type == 'bring-your-own' else ['src', 'package.json']))
43
43
 
44
44
  # Add prisma to syncs if using prisma feature
45
45
  if app_type == 'backend' and 'prisma' in features and 'prisma' not in syncs:
@@ -261,7 +261,11 @@ def _apply_manifest_defaults(manifest, resource_path):
261
261
  features = result.get('featuresEnabled', [])
262
262
  result['usePrisma'] = 'prisma' in features
263
263
  result['useNats'] = 'nats' in features
264
- result['useTraefik'] = 'traefik' in features or app_type in ['backend', 'sdk']
264
+ result['useTraefik'] = (
265
+ 'traefik' in features
266
+ or app_type in ['backend', 'sdk']
267
+ or (app_type == 'bring-your-own' and result.get('exposeViaProxy', True))
268
+ )
265
269
 
266
270
  # Extract stack from path if not set
267
271
  if 'stack' not in result:
@@ -278,7 +282,7 @@ def _apply_manifest_defaults(manifest, resource_path):
278
282
 
279
283
  # 🎯 SMART TRAEFIK CONFIG (Convention over Configuration)
280
284
  # Apply to backends and SDKs that need Traefik routing
281
- if result['useTraefik'] and app_type in ['backend', 'sdk']:
285
+ if result['useTraefik'] and app_type in ['backend', 'sdk', 'bring-your-own']:
282
286
  existing_traefik = result.get('traefik', {})
283
287
  # Use apiPath from root level if available, otherwise use default pattern
284
288
  root_api_path = result.get('apiPath')
@@ -303,6 +303,16 @@ def _render_product_stacks_index(resources):
303
303
 
304
304
  def _write_file_if_changed(path, content):
305
305
  """Write file only if content changed. Returns True if written."""
306
+ normalized = path.replace('\\', '/').replace('./', '', 1)
307
+ if '..' in normalized.split('/') or not (
308
+ '/.autogenerated/' in normalized or
309
+ normalized.startswith('.autogenerated/') or
310
+ normalized.startswith('.tdk/.tdk-out/') or
311
+ normalized in ['Tiltfile', '.tiltignore'] or
312
+ (normalized.startswith('TILT_') and normalized.endswith('.star') and '/' not in normalized)
313
+ ):
314
+ fail("TDK discovery refused to write outside its generated-file allowlist: " + path)
315
+
306
316
  # Read existing file (returns empty string on error via shell)
307
317
  existing = str(local("cat " + path + " 2>/dev/null || echo ''", quiet=True, echo_off=True))
308
318
 
@@ -322,6 +332,8 @@ def generate_product_stack_topology():
322
332
  changes = 0
323
333
  for resource in APP_RESOURCES:
324
334
  stack = resource.get("name", "unknown")
335
+ if not stack or '/' in stack or '\\' in stack or stack == '..':
336
+ fail("Invalid stack name for generated topology path: " + str(stack))
325
337
  stack_file = PRODUCT_TOPOLOGY_DIR + "/" + stack + "/stack.star"
326
338
  if _write_file_if_changed(stack_file, _render_product_stack_file(resource)):
327
339
  changes += 1
@@ -443,7 +455,8 @@ def _generate_yaml_from_json_manifests():
443
455
  json_files = _find_manifest_files(root, MANIFEST_FILENAME)
444
456
 
445
457
  for json_file in json_files:
446
- yaml_file = json_file.replace(MANIFEST_FILENAME, MANIFEST_FILENAME_YAML)
458
+ resource_dir = json_file.rsplit('/', 1)[0]
459
+ yaml_file = resource_dir + "/.autogenerated/" + MANIFEST_FILENAME_YAML
447
460
  load_result = ManifestLoader.load_from_file(json_file)
448
461
  if not load_result.error and load_result.manifest:
449
462
  yaml_content = _json_to_yaml(load_result.manifest)
@@ -461,7 +474,7 @@ def load_yaml_manifests_as_resources():
461
474
  for root in DISCOVERY_SCAN_ROOTS:
462
475
  files = _find_manifest_files(root, MANIFEST_FILENAME_YAML)
463
476
  for f in files:
464
- if f not in yaml_files:
477
+ if '/.autogenerated/' + MANIFEST_FILENAME_YAML in f and f not in yaml_files:
465
478
  yaml_files.append(f)
466
479
 
467
480
  if not yaml_files:
@@ -469,7 +482,8 @@ def load_yaml_manifests_as_resources():
469
482
 
470
483
  created_resources = {}
471
484
  for yaml_file in yaml_files:
472
- json_file = yaml_file.replace(MANIFEST_FILENAME_YAML, MANIFEST_FILENAME)
485
+ service_dir = yaml_file.rsplit('/.autogenerated/', 1)[0]
486
+ json_file = service_dir + '/' + MANIFEST_FILENAME
473
487
  load_result = ManifestLoader.load_from_file(json_file)
474
488
  resource_name = ""
475
489
 
package/engine/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # .tilt-engine/
2
2
 
3
+ > **Contributing to the current engine:** follow the [engine and discovery recipe](../docs/contributing/02-feature-recipes.md#change-the-engine-or-service-discovery). This README contains older architecture notes; check paths against the current `engine/topologies/` source before relying on them.
4
+
3
5
  **Tilt Platform Framework Code**
4
6
 
5
7
  This directory contains the TDK Landscape Tilt infrastructure framework - the code that powers our local development orchestration.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$id": "https://tdk-landscape.github.io/schema.service.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
- "additionalProperties": false,
4
+ "additionalProperties": true,
5
5
  "allOf": [
6
6
  {
7
7
  "if": {
@@ -60,6 +60,27 @@
60
60
  "featuresEnabled"
61
61
  ]
62
62
  }
63
+ },
64
+ {
65
+ "if": {
66
+ "properties": {
67
+ "appType": {
68
+ "const": "bring-your-own"
69
+ }
70
+ }
71
+ },
72
+ "then": {
73
+ "properties": {
74
+ "port": {
75
+ "maximum": 5999,
76
+ "minimum": 4000
77
+ }
78
+ },
79
+ "required": [
80
+ "port",
81
+ "healthCheckPath"
82
+ ]
83
+ }
63
84
  }
64
85
  ],
65
86
  "description": "Configuration for TDK Landscape resources. Drives Tilt orchestration, Docker Compose generation, and dependency resolution. Supports both explicit service.json files and synthesis from directory structure.",
@@ -86,14 +107,15 @@
86
107
  "type": "string"
87
108
  },
88
109
  "appType": {
89
- "description": "Resource type determining build strategy:\n• backend - Hono/Express API server\n• frontend - Vite single-page application (React or Vue, see framework)\n• sdk - Shared library package\n• worker - Background job processor\n• migrator - Database migration runner\n• library - Shared code library",
110
+ "description": "Resource type determining build strategy:\n• backend - Hono/Express API server\n• frontend - Vite single-page application (React or Vue, see framework)\n• sdk - Shared library package\n• worker - Background job processor\n• migrator - Database migration runner\n• library - Shared code library\n• bring-your-own - Wrap an existing service. No app scaffold. Needs Dockerfile or --image.",
90
111
  "enum": [
91
112
  "backend",
92
113
  "frontend",
93
114
  "sdk",
94
115
  "worker",
95
116
  "migrator",
96
- "library"
117
+ "library",
118
+ "bring-your-own"
97
119
  ],
98
120
  "type": "string"
99
121
  },
@@ -124,26 +146,19 @@
124
146
  },
125
147
  "dockerfile": {
126
148
  "default": "autogenerated/Dockerfile.app.autogenerated",
127
- "description": "Custom Dockerfile path relative to resource directory.",
149
+ "description": "Custom Dockerfile path relative to resource directory. Optional for bring-your-own type.",
150
+ "type": "string"
151
+ },
152
+ "exposeViaProxy": {
153
+ "description": "Whether this bring-your-own resource receives a Traefik route. Defaults to true.",
154
+ "type": "boolean"
155
+ },
156
+ "image": {
157
+ "description": "Docker image name instead of building from Dockerfile. Optional for bring-your-own type.",
128
158
  "type": "string"
129
159
  },
130
160
  "stack": {
131
161
  "description": "Technology stack this resource belongs to.\nUsed for:\n• Database naming: TDK_{stack}\n• Traefik routing: /api/{stack}\n• NATS queue: {stack}_backend_svc",
132
- "enum": [
133
- "user",
134
- "team",
135
- "auth",
136
- "order",
137
- "order-planner",
138
- "service",
139
- "inventory",
140
- "billing",
141
- "notification",
142
- "analytics",
143
- "profile",
144
- "platform",
145
- "product"
146
- ],
147
162
  "type": "string"
148
163
  },
149
164
  "params": {
@@ -273,6 +288,11 @@
273
288
  ],
274
289
  "type": "string"
275
290
  },
291
+ "schemaVersion": {
292
+ "const": 1,
293
+ "description": "Version of the TDK service manifest contract.",
294
+ "type": "integer"
295
+ },
276
296
  "sablier": {
277
297
  "description": "On-demand scaling: stop this container when idle, wake it on the next request.\nNot recommended for databases or other stateful resources with in-flight writes.\n(Premium - requires TDK_LICENSE_KEY; see hasSablierLicense() in extension-fetch.ts)",
278
298
  "properties": {
@@ -281,6 +301,11 @@
281
301
  "description": "Scale this resource to zero when idle",
282
302
  "type": "boolean"
283
303
  },
304
+ "deferStart": {
305
+ "default": false,
306
+ "description": "Build the image during tdk up, then create and start the container on its first request",
307
+ "type": "boolean"
308
+ },
284
309
  "group": {
285
310
  "description": "Resources sharing a group wake and sleep together. Defaults to `stack`.",
286
311
  "type": "string"
@@ -322,8 +347,9 @@
322
347
  "required": [
323
348
  "appName",
324
349
  "appType",
325
- "stack"
350
+ "stack",
351
+ "schemaVersion"
326
352
  ],
327
353
  "title": "TDK Landscape Resource Configuration",
328
354
  "type": "object"
329
- }
355
+ }
@@ -170,12 +170,13 @@ main() {
170
170
  cat > "$manifest_file" << EOF
171
171
  {
172
172
  "\$schema": "../../.tilt/schemas/service-schema.json",
173
+ "schemaVersion": 1,
173
174
  "appName": "${full_name}",
174
175
  "appType": "${resource_type}",
175
176
  "stack": "${stack}",
176
177
  "port": ${port},
177
178
  "replicas": 1,
178
- "features": ${features},
179
+ "featuresEnabled": ${features},
179
180
  "dependsOn": ${deps}
180
181
  }
181
182
  EOF
@@ -217,7 +217,26 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
217
217
  auth_env = AuthConfig.generate_docker_compose_auth_env(auth_config)
218
218
 
219
219
  # Use .autogenerated/Dockerfile.app.autogenerated by default for build config (primary only)
220
- build_config = compose_build_config(resource_path, res_name)
220
+ if manifest and manifest.get('appType') == 'bring-your-own':
221
+ if manifest.get('image'):
222
+ build_config = ""
223
+ else:
224
+ # A BYO Dockerfile copies paths relative to the service directory
225
+ # (for example, `COPY app.py .`). Build with that directory as the
226
+ # context instead of the project root used by generated TDK apps.
227
+ context_prefix = '/'.join(['..'] * (len(resource_path.split('/')) + 1))
228
+ build_context = context_prefix + '/' + full_resource_path
229
+ dockerfile = manifest.get('dockerfile', 'Dockerfile')
230
+ if dockerfile.startswith('./'):
231
+ dockerfile = dockerfile[2:]
232
+ build_config = """ build:
233
+ context: {build_context}
234
+ dockerfile: {dockerfile}""".format(
235
+ build_context=build_context,
236
+ dockerfile=dockerfile,
237
+ )
238
+ else:
239
+ build_config = compose_build_config(resource_path, res_name)
221
240
 
222
241
  # REMOVED: External port exposure - all services accessed via Traefik only
223
242
  # Services are available at {project}.localhost/api/v1/{service}
@@ -243,10 +262,16 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
243
262
  # them a Traefik route advertises an endpoint that can never answer, and giving
244
263
  # them an HTTP healthcheck marks a perfectly healthy worker as unhealthy forever.
245
264
  is_worker = manifest.get('appType') == 'worker' if manifest else False
265
+ is_byo = manifest.get('appType') == 'bring-your-own' if manifest else False
266
+ is_proxy_disabled_byo = (
267
+ is_byo and not manifest.get('exposeViaProxy', True)
268
+ ) if manifest else False
246
269
 
247
- if is_worker:
270
+ if is_worker or is_proxy_disabled_byo:
248
271
  traefik_labels = ' - "traefik.enable=false"'
249
- healthcheck_section = ""
272
+ healthcheck_section = "" if is_worker or is_byo else """ healthcheck:
273
+ test: ["CMD", "curl", "-f", "--max-time", "5", "http://localhost:{internal_port}{health_path}"]
274
+ """.format(internal_port=internal_port, health_path=health_path) + compose_healthcheck_timing()
250
275
  else:
251
276
  # Traefik's load-balancer check can be overridden separately in service.json
252
277
  # (`traefik.healthCheck`); it defaults to the container check's path.
@@ -260,7 +285,10 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
260
285
  health_path=traefik_health_path,
261
286
  manifest=manifest,
262
287
  )
263
- healthcheck_section = """ healthcheck:
288
+ # BYO images may not contain curl. Traefik uses its own HTTP health
289
+ # check labels above, so don't mark an otherwise healthy container
290
+ # unhealthy because the image lacks a command TDK cannot require.
291
+ healthcheck_section = "" if is_byo else """ healthcheck:
264
292
  test: ["CMD", "curl", "-f", "--max-time", "5", "http://localhost:{internal_port}{health_path}"]
265
293
  """.format(internal_port=internal_port, health_path=health_path) + compose_healthcheck_timing()
266
294
  infisical_env = get_infisical_environment_vars(as_array=True, resource_name=res_name)
@@ -311,7 +339,11 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
311
339
  resource_entry_name=resource_entry_name,
312
340
  res_name=res_name,
313
341
  resource_name=resource_name,
314
- image_name=with_runtime_image_tag(resource_name + '_' + res_name),
342
+ image_name=(
343
+ manifest.get('image')
344
+ if manifest and manifest.get('appType') == 'bring-your-own' and manifest.get('image')
345
+ else with_runtime_image_tag(resource_name + '_' + res_name)
346
+ ),
315
347
  build_config=build_config,
316
348
  ports_section=ports_section,
317
349
  env_file_rel_path=env_file_rel_path,