@tdk-landscape/tdk-cli-core 1.3.76 → 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/README.md +58 -12
- 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 +286 -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 +15 -7
- 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 +3 -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 +0 -1
- 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
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,
|
|
@@ -260,6 +260,37 @@ def encode_json(obj, _depth=0):
|
|
|
260
260
|
# 📁 FILE I/O UTILITIES
|
|
261
261
|
# =============================================================================
|
|
262
262
|
|
|
263
|
+
def _is_allowed_generated_path(file_path, project_root):
|
|
264
|
+
path = file_path.replace('\\', '/').strip()
|
|
265
|
+
root = project_root.replace('\\', '/').rstrip('/')
|
|
266
|
+
if root and path.startswith(root + '/'):
|
|
267
|
+
path = path[len(root) + 1:]
|
|
268
|
+
if path.startswith('./'):
|
|
269
|
+
path = path[2:]
|
|
270
|
+
parts = path.split('/')
|
|
271
|
+
if '..' in parts or path.startswith('/'):
|
|
272
|
+
return False
|
|
273
|
+
if '.autogenerated' in parts:
|
|
274
|
+
return True
|
|
275
|
+
if path.startswith('.tdk/.tdk-out/'):
|
|
276
|
+
return True
|
|
277
|
+
if path.startswith('product_autogenerated_resource_manifests/') or path.startswith('product_autogenerated_services_list/'):
|
|
278
|
+
return True
|
|
279
|
+
if path == 'Tiltfile' or path == '.tiltignore':
|
|
280
|
+
return True
|
|
281
|
+
if len(parts) == 1 and path.startswith('TILT_') and path.endswith('.star'):
|
|
282
|
+
return True
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
def _write_generated_file(file_path, content):
|
|
286
|
+
expected = str(content).rstrip()
|
|
287
|
+
dir_path = file_path.rsplit('/', 1)[0]
|
|
288
|
+
if not dir_path:
|
|
289
|
+
dir_path = '/'
|
|
290
|
+
safe_content = (expected + '\n').replace("'", "'\\''")
|
|
291
|
+
cmd = "mkdir -p '" + dir_path + "' && printf '%s' '" + safe_content + "' > '" + file_path + "'"
|
|
292
|
+
local(cmd, quiet=True, echo_off=True)
|
|
293
|
+
|
|
263
294
|
def write_file_if_changed(file_path, content):
|
|
264
295
|
"""
|
|
265
296
|
Writes content to file only if it differs from existing content.
|
|
@@ -272,28 +303,40 @@ def write_file_if_changed(file_path, content):
|
|
|
272
303
|
content = ""
|
|
273
304
|
|
|
274
305
|
# For relative paths, prepend project root to ensure files go to correct location
|
|
306
|
+
project_root = os.environ.get('TDK_PROJECT_ROOT', '.')
|
|
275
307
|
if not file_path.startswith('/'):
|
|
276
|
-
project_root = os.environ.get('TDK_PROJECT_ROOT', '.')
|
|
277
308
|
# Remove leading ./ if present in file_path
|
|
278
309
|
clean_file_path = file_path[2:] if file_path.startswith('./') else file_path
|
|
279
310
|
file_path = project_root + '/' + clean_file_path
|
|
280
311
|
|
|
312
|
+
if not _is_allowed_generated_path(file_path, project_root):
|
|
313
|
+
fail("TDK refused to write outside its generated-file allowlist: " + file_path)
|
|
314
|
+
|
|
281
315
|
expected = str(content).rstrip()
|
|
282
316
|
existing_blob = read_file(file_path, default='')
|
|
283
317
|
existing = str(existing_blob).rstrip()
|
|
284
318
|
|
|
285
319
|
if existing == expected:
|
|
286
320
|
print("DEBUG WRITE: Skipping {} (no changes)".format(file_path))
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
321
|
+
else:
|
|
322
|
+
print("DEBUG WRITE: Writing {} bytes to {}".format(len(expected), file_path))
|
|
323
|
+
_write_generated_file(file_path, expected)
|
|
324
|
+
print("DEBUG WRITE: Completed {}".format(file_path))
|
|
325
|
+
|
|
326
|
+
# Keep a generated-content snapshot beside the service outputs. `tdk config verify`
|
|
327
|
+
# compares these snapshots with the files and the source manifest, detecting both
|
|
328
|
+
# hand edits and stale generated output without needing to run Tilt from CI.
|
|
329
|
+
normalized = file_path.replace('\\', '/')
|
|
330
|
+
generated_marker = '/.autogenerated/'
|
|
331
|
+
marker_index = normalized.rfind(generated_marker)
|
|
332
|
+
if marker_index >= 0 and '/.tdk-contract/' not in normalized:
|
|
333
|
+
service_root = normalized[:marker_index]
|
|
334
|
+
generated_rel = normalized[marker_index + len(generated_marker):]
|
|
335
|
+
contract_root = service_root + '/.autogenerated/.tdk-contract'
|
|
336
|
+
_write_generated_file(contract_root + '/' + generated_rel, expected)
|
|
337
|
+
manifest_content = read_file(service_root + '/service.json', default='')
|
|
338
|
+
if manifest_content:
|
|
339
|
+
_write_generated_file(contract_root + '/service.json.source', str(manifest_content))
|
|
297
340
|
|
|
298
341
|
|
|
299
342
|
# =============================================================================
|
|
@@ -46,7 +46,7 @@ load("./constants.star", "MANIFEST_FILENAME")
|
|
|
46
46
|
```json
|
|
47
47
|
{
|
|
48
48
|
"appName": "resource-name-backend",
|
|
49
|
-
"appType": "backend", // frontend, backend, library, migrator, sdk, worker
|
|
49
|
+
"appType": "backend", // frontend, backend, library, migrator, sdk, worker, bring-your-own
|
|
50
50
|
"stack": "order",
|
|
51
51
|
"port": 4000,
|
|
52
52
|
"features": ["nats", "prisma", "vite-node"],
|
|
@@ -61,6 +61,7 @@ VALID_RESOURCE_TYPES = [
|
|
|
61
61
|
"sdk",
|
|
62
62
|
"worker",
|
|
63
63
|
"infra",
|
|
64
|
+
"bring-your-own",
|
|
64
65
|
]
|
|
65
66
|
|
|
66
67
|
VALID_FEATURES = [
|
|
@@ -86,6 +87,7 @@ VALID_FEATURES = [
|
|
|
86
87
|
PORT_RANGES = {
|
|
87
88
|
"frontend": {"min": 3000, "max": 5999},
|
|
88
89
|
"backend": {"min": 4000, "max": 5999},
|
|
90
|
+
"bring-your-own": {"min": 4000, "max": 5999},
|
|
89
91
|
"worker": {"min": 6000, "max": 6999},
|
|
90
92
|
"migrator": {"min": 7000, "max": 7999},
|
|
91
93
|
"sdk": {"min": 3000, "max": 9999},
|
|
@@ -186,6 +188,7 @@ DEFAULT_SYNCS = {
|
|
|
186
188
|
'migrator': ['prisma'],
|
|
187
189
|
'library': ['src', 'package.json'],
|
|
188
190
|
'sdk': ['src', 'package.json'],
|
|
191
|
+
'bring-your-own': [],
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
RUNTIME_CONFIGS = {
|
|
@@ -64,11 +64,9 @@ MANIFEST_SCHEMA = {
|
|
|
64
64
|
'default': 'backend',
|
|
65
65
|
},
|
|
66
66
|
'stack': {
|
|
67
|
-
'type': '
|
|
67
|
+
'type': 'string',
|
|
68
68
|
'required': True,
|
|
69
|
-
'constraints': {
|
|
70
|
-
'values': VALID_STACKS,
|
|
71
|
-
},
|
|
69
|
+
'constraints': {'min_length': 1},
|
|
72
70
|
'description': 'Technology stack this resource belongs to',
|
|
73
71
|
'example': 'user',
|
|
74
72
|
},
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# === INLINED CONSTANTS for pure extension loading ===
|
|
10
10
|
VALID_STACKS = () # Stacks are project-specific, discovered dynamically
|
|
11
11
|
VALID_FEATURES = "api-client", "env-config", "api-index", "prisma", "ddd", "nats", "redis", "infisical", "vitest", "traefik", "websocket", "graphql", "grpc", "vite-node", "maintenance", "sablier"
|
|
12
|
-
PORT_RANGES = {"frontend": {"min": 3000, "max": 5999}, "backend": {"min": 4000, "max": 5999}, "worker": {"min": 6000, "max": 6999}, "migrator": {"min": 7000, "max": 7999}, "sdk": {"min": 3000, "max": 9999}, "library": {"min": 3000, "max": 9999}}
|
|
12
|
+
PORT_RANGES = {"frontend": {"min": 3000, "max": 5999}, "backend": {"min": 4000, "max": 5999}, "bring-your-own": {"min": 4000, "max": 5999}, "worker": {"min": 6000, "max": 6999}, "migrator": {"min": 7000, "max": 7999}, "sdk": {"min": 3000, "max": 9999}, "library": {"min": 3000, "max": 9999}}
|
|
13
13
|
DEFAULTS = {}
|
|
14
14
|
|
|
15
15
|
# === END INLINED CONSTANTS ===
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
#
|
|
8
8
|
# This module centralizes infrastructure loading:
|
|
9
9
|
# - Database (PostgreSQL, Redis)
|
|
10
|
-
# - Messaging (NATS, Kafka)
|
|
10
|
+
# - Messaging (Redis, NATS, Kafka)
|
|
11
11
|
# - Secrets (Infisical)
|
|
12
12
|
# - Registry (Verdaccio)
|
|
13
13
|
# - Proxy (Traefik)
|
|
@@ -33,6 +33,13 @@ def _file_exists(path):
|
|
|
33
33
|
return result == 'yes'
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _compose_has_service(compose_path, service_name):
|
|
37
|
+
"""Check whether a Compose file declares a named service."""
|
|
38
|
+
document = read_yaml(compose_path, default={})
|
|
39
|
+
services = document.get('services', {}) if type(document) == 'dict' else {}
|
|
40
|
+
return service_name in services
|
|
41
|
+
|
|
42
|
+
|
|
36
43
|
def _docker_compose(compose_paths, env_file):
|
|
37
44
|
"""docker_compose() wrapper: Tilt's builtin rejects env_file=None outright
|
|
38
45
|
(it type-checks the kwarg as path|string, not Optional), so the keyword must be
|
|
@@ -116,8 +123,12 @@ def _load_database_management(should_enable, root_prefix="", env_file=None, writ
|
|
|
116
123
|
if _file_exists(messaging_compose):
|
|
117
124
|
print("DEBUG INFRA: Loading messaging compose from {}".format(messaging_compose))
|
|
118
125
|
_docker_compose(messaging_compose, env_file)
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
# Register only services present in the project compose file. Minimal
|
|
127
|
+
# NATS-only examples should not need to add an unrelated Redis broker.
|
|
128
|
+
if _compose_has_service(messaging_compose, 'redis'):
|
|
129
|
+
dc_resource('redis', labels=['infra.messaging'], auto_init=False)
|
|
130
|
+
if _compose_has_service(messaging_compose, 'nats'):
|
|
131
|
+
dc_resource('nats', labels=['infra.messaging'], auto_init=True)
|
|
121
132
|
else:
|
|
122
133
|
print("DEBUG INFRA: Skipping messaging (compose file not found)")
|
|
123
134
|
|
package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star
CHANGED
|
@@ -137,10 +137,13 @@ def resource_defers_start(manifest):
|
|
|
137
137
|
static route wakes it on first request (see openspec/changes/
|
|
138
138
|
prioritized-cold-start)."""
|
|
139
139
|
_, sablier_licensed_and_enabled = sablier_middleware_suffix(manifest, '')
|
|
140
|
+
sablier = manifest.get('sablier', {}) if manifest else {}
|
|
141
|
+
defers_start = bool(sablier.get('enable', False) and sablier.get('deferStart', False))
|
|
142
|
+
if defers_start and not sablier_licensed_and_enabled:
|
|
143
|
+
print("⚠️ '{}' requests sablier.deferStart but the licensed Sablier overlay is disabled; starting normally".format(manifest.get('name', 'resource')))
|
|
140
144
|
if not sablier_licensed_and_enabled:
|
|
141
145
|
return False
|
|
142
|
-
|
|
143
|
-
return bool(sablier.get('deferStart', False))
|
|
146
|
+
return defers_start
|
|
144
147
|
|
|
145
148
|
|
|
146
149
|
def resolve_manifest_dependency_names(manifest, all_services_map):
|
|
@@ -365,7 +368,7 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
|
|
|
365
368
|
print("DEBUG REGISTER: Registering '{}' app_type='{}' auto_init={}".format(res_name, app_type, effective_auto_init))
|
|
366
369
|
|
|
367
370
|
# Skip docker_build for libraries and SDKs - they don't have Dockerfiles
|
|
368
|
-
if app_type not in ['library', 'sdk'] and config.get('syncs'):
|
|
371
|
+
if app_type not in ['library', 'sdk', 'bring-your-own'] and config.get('syncs'):
|
|
369
372
|
print("DEBUG REGISTER: '{}' has syncs, calling docker_build".format(res_name))
|
|
370
373
|
live_update_rules = _build_live_update_rules(
|
|
371
374
|
config['res_path'],
|
|
@@ -429,13 +432,14 @@ def _process_frontend_resource(res, manifest, full_res_path, resource_path, reso
|
|
|
429
432
|
|
|
430
433
|
def _process_backend_resource(res, manifest, full_res_path, resource_path, resource_name, should_enable):
|
|
431
434
|
"""Process a backend resource and return compose entry."""
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
435
|
+
if manifest.get('appType') != 'bring-your-own':
|
|
436
|
+
Docker.backend(
|
|
437
|
+
res_path=full_res_path,
|
|
438
|
+
resource_name=res['name'],
|
|
439
|
+
port=None, # Use default BASE_PORT_BACKEND
|
|
440
|
+
build_cmd=res.get('start_command', 'bun run build'),
|
|
441
|
+
use_golden=should_enable('golden-image'),
|
|
442
|
+
)
|
|
439
443
|
return Docker.backend_compose(resource_path=resource_path, resource_name=resource_name, res=res, manifest=manifest)
|
|
440
444
|
|
|
441
445
|
|
|
@@ -462,7 +466,7 @@ def _build_resource_config(res, manifest, resource_config, full_res_path, infra_
|
|
|
462
466
|
'replicas': replicas,
|
|
463
467
|
'syncs': res.get('syncs', []),
|
|
464
468
|
'app_type': manifest.get('appType', 'backend'),
|
|
465
|
-
'dockerfile': res.get('dockerfile', OUTPUT.AUTOGENERATED_FOLDER + '/Dockerfile.app.autogenerated'),
|
|
469
|
+
'dockerfile': res.get('dockerfile', manifest.get('dockerfile', OUTPUT.AUTOGENERATED_FOLDER + '/Dockerfile.app.autogenerated')),
|
|
466
470
|
'res_path': full_res_path,
|
|
467
471
|
'env': env_vars,
|
|
468
472
|
'project_root': project_root,
|
|
@@ -126,6 +126,8 @@ def _build_generated_file_list(resource_config, resource_path, manifest):
|
|
|
126
126
|
"""
|
|
127
127
|
files = []
|
|
128
128
|
app_type = manifest.get('appType', 'backend')
|
|
129
|
+
if app_type == 'bring-your-own':
|
|
130
|
+
return []
|
|
129
131
|
is_frontend = resource_config.get('frontend', False) or app_type == 'frontend'
|
|
130
132
|
|
|
131
133
|
# Vite
|
|
@@ -267,6 +269,16 @@ def _generate_all_configs_for_resource(
|
|
|
267
269
|
write_file = ctx['write_file']
|
|
268
270
|
should_enable = ctx['should_enable']
|
|
269
271
|
global_config = ctx['global_config']
|
|
272
|
+
|
|
273
|
+
# Env generation needs the actual service directory on the manifest. Set it
|
|
274
|
+
# before the BYO fast path too; otherwise Compose references an env file that
|
|
275
|
+
# was never written for bring-your-own resources.
|
|
276
|
+
manifest['_resource_path'] = resource_path
|
|
277
|
+
|
|
278
|
+
if manifest.get('appType') == 'bring-your-own':
|
|
279
|
+
_generate_env_file(manifest, backend_manifest, write_file)
|
|
280
|
+
_generate_yaml_manifest(resource_path, manifest, write_file)
|
|
281
|
+
return
|
|
270
282
|
|
|
271
283
|
# WORKAROUND: Prevent infinite recursion by checking recursion depth
|
|
272
284
|
# Use a unique key per resource to track depth, avoiding interference between different resources
|
|
@@ -279,9 +291,6 @@ def _generate_all_configs_for_resource(
|
|
|
279
291
|
ctx = dict(ctx)
|
|
280
292
|
ctx[recursion_key] = recursion_depth + 1
|
|
281
293
|
|
|
282
|
-
# Add resource_path to manifest so generators can access it
|
|
283
|
-
manifest['_resource_path'] = resource_path
|
|
284
|
-
|
|
285
294
|
# ==========================================================================
|
|
286
295
|
# NEW: PRE-FLIGHT VALIDATIONS (v2.0)
|
|
287
296
|
# Run validations BEFORE generating configs to catch issues early
|
|
@@ -444,8 +453,8 @@ def _generate_all_configs_for_resource(
|
|
|
444
453
|
|
|
445
454
|
|
|
446
455
|
def _generate_yaml_manifest(resource_path, manifest, write_file):
|
|
447
|
-
"""Generate YAML
|
|
448
|
-
yaml_path = resource_path + "/" + MANIFEST_FILENAME_YAML
|
|
456
|
+
"""Generate an allowlisted YAML shadow beside other TDK-owned outputs."""
|
|
457
|
+
yaml_path = resource_path + "/.autogenerated/" + MANIFEST_FILENAME_YAML
|
|
449
458
|
|
|
450
459
|
# Check if YAML already exists and is up-to-date by comparing with JSON
|
|
451
460
|
json_path = resource_path + "/" + MANIFEST_FILENAME
|
|
@@ -525,7 +534,7 @@ def create_manifest_config_resource(
|
|
|
525
534
|
local_resource(
|
|
526
535
|
name=config_gen_resource_name,
|
|
527
536
|
cmd=log_cmd,
|
|
528
|
-
deps=[manifest_path, package_json_path],
|
|
537
|
+
deps=[manifest_path] if manifest.get('appType') == 'bring-your-own' else [manifest_path, package_json_path],
|
|
529
538
|
labels=['config-gen', resource_name],
|
|
530
539
|
allow_parallel=True,
|
|
531
540
|
auto_init=auto_init_config_gen
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TDK Landscape
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# TDK product example
|
|
2
|
+
|
|
3
|
+
A small working landscape with a Hono API, PostgreSQL, a NATS worker, a Vite UI, and Traefik routes.
|
|
4
|
+
|
|
5
|
+
This is a local demo, not a Helm chart.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
tdk doctor
|
|
9
|
+
tdk project --yes
|
|
10
|
+
tdk up
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Open `http://app.tdk-example.localhost/orders-app/` and create an order, or use the routed API:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
curl -fsS -X POST http://api.tdk-example.localhost/api/orders \
|
|
17
|
+
-H 'content-type: application/json' -d '{"item":"coffee"}'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The API stores each order in PostgreSQL and publishes its id to NATS. The worker consumes the
|
|
21
|
+
event, reads the stored row, and marks it observed. Poll `GET /api/orders/<id>` through the API
|
|
22
|
+
route until `workerSeenAt` is set. `tdk config verify` checks generated output. Stop the landscape
|
|
23
|
+
with `tdk down`.
|