@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.
- package/CHANGELOG.md +6 -0
- package/README.md +59 -13
- package/cli/bin/tdk.js +22 -2
- package/cli/dist/cli.js +9 -2
- package/cli/dist/commands/completion.js +30 -3
- package/cli/dist/commands/config.js +59 -1
- package/cli/dist/commands/doctor.js +295 -98
- package/cli/dist/commands/eject.js +77 -0
- package/cli/dist/commands/networks.js +17 -4
- package/cli/dist/commands/project.js +28 -5
- package/cli/dist/commands/resource.js +118 -10
- package/cli/dist/commands/up.js +93 -21
- package/cli/dist/commands/upgrade.js +163 -68
- package/cli/dist/generator/template-engine.js +77 -13
- package/cli/dist/types/index.js +6 -1
- package/cli/dist/utils/cold-preflight.js +198 -0
- package/cli/dist/utils/constants.js +4 -0
- package/cli/dist/utils/docker.js +15 -1
- package/cli/dist/utils/doctor-runtime.js +48 -16
- package/cli/dist/utils/doctor-wiring.js +269 -0
- package/cli/dist/utils/errors.js +5 -5
- package/cli/dist/utils/exec-async.js +30 -0
- package/cli/dist/utils/generated-paths.js +49 -0
- package/cli/dist/utils/platform.js +53 -0
- package/cli/dist/utils/port-assignment.js +13 -20
- package/cli/dist/utils/project-templates.js +7 -2
- package/cli/dist/utils/resource-features.js +6 -2
- package/cli/dist/utils/service-manifest.js +73 -0
- package/cli/dist/utils/services.js +8 -1
- package/cli/dist/utils/tar.js +27 -5
- package/cli/dist/utils/tilt-process.js +53 -0
- package/cli/dist/utils/tilt.js +3 -1
- package/cli/dist/utils/which.js +26 -0
- package/cli/dist/utils/windows-doctor.js +175 -0
- package/cli/package.json +1 -1
- package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -2
- package/cli/templates/TILT_TECH_STACK.star.hbs +0 -1
- package/cli/templates/Tiltfile.hbs +24 -4
- package/cli/templates/spec.master.hbs +1 -2
- package/discovery/AGENTS.md +1 -1
- package/discovery/constants.star +2 -2
- package/discovery/discovery_orchestrator.star +2 -2
- package/discovery/manifest/constants.star +1 -1
- package/discovery/manifest/loading.star +7 -3
- package/discovery/registry.star +17 -3
- package/engine/README.md +2 -0
- package/engine/schemas/service-schema.json +47 -21
- package/engine/scripts/generate-manifest.sh +2 -1
- package/engine/topologies/platform/docker/compose/compose.star +37 -5
- package/engine/topologies/tilt/common/utils.star +54 -11
- package/engine/topologies/tilt/manifest/AGENTS.md +1 -1
- package/engine/topologies/tilt/manifest/constants.star +3 -0
- package/engine/topologies/tilt/manifest/schema.star +2 -4
- package/engine/topologies/tilt/manifest/validator.star +1 -1
- package/engine/topologies/tilt/resources/infra-loader.star +14 -3
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +15 -11
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +15 -6
- package/examples/tdk-example/LICENSE +21 -0
- package/examples/tdk-example/README.md +23 -0
- package/examples/tdk-example/TILT_SERVICE_DEFAULTS.star +42 -0
- package/examples/tdk-example/TILT_TECH_STACK.star +67 -0
- package/examples/tdk-example/Tiltfile +25 -0
- package/examples/tdk-example/services/platform/messaging/docker-compose.yml +26 -0
- package/examples/tdk-example/services/shop/orders-api/package.json +20 -0
- package/examples/tdk-example/services/shop/orders-api/service.json +14 -0
- package/examples/tdk-example/services/shop/orders-api/src/index.ts +81 -0
- package/examples/tdk-example/services/shop/orders-app/index.html +12 -0
- package/examples/tdk-example/services/shop/orders-app/package.json +20 -0
- package/examples/tdk-example/services/shop/orders-app/public/.gitkeep +0 -0
- package/examples/tdk-example/services/shop/orders-app/service.json +9 -0
- package/examples/tdk-example/services/shop/orders-app/src/main.tsx +9 -0
- package/examples/tdk-example/services/shop/orders-app/src/orders.tsx +67 -0
- package/examples/tdk-example/services/shop/orders-app/src/style.css +15 -0
- package/examples/tdk-example/services/shop/orders-worker/package.json +18 -0
- package/examples/tdk-example/services/shop/orders-worker/service.json +13 -0
- package/examples/tdk-example/services/shop/orders-worker/src/index.ts +33 -0
- 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
|
@@ -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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
#
|
|
171
|
+
# Generated by TDK CLI v{{version}}
|
|
173
172
|
# =============================================================================
|
|
174
173
|
|
|
175
174
|
# =============================================================================
|
package/discovery/AGENTS.md
CHANGED
|
@@ -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` |
|
package/discovery/constants.star
CHANGED
|
@@ -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 = "
|
|
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 = "
|
|
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'] =
|
|
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')
|
package/discovery/registry.star
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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,
|