@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
|
@@ -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`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# TILT_SERVICE_DEFAULTS.star - Platform Runtime Configuration
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
BASE_PORT_FRONTEND = 3000
|
|
6
|
+
BASE_PORT_BACKEND = 4000
|
|
7
|
+
|
|
8
|
+
HEALTH_CHECK_PATH = "/health"
|
|
9
|
+
HEALTH_CHECK_PATH_LIVE = "/health/live"
|
|
10
|
+
HEALTH_CHECK_PATH_READY = "/health/ready"
|
|
11
|
+
HEALTH_CHECK_INTERVAL = 10
|
|
12
|
+
HEALTH_CHECK_TIMEOUT = 5
|
|
13
|
+
HEALTH_CHECK_RETRIES = 3
|
|
14
|
+
|
|
15
|
+
MEMORY_LIMITS = {
|
|
16
|
+
"frontend": 512,
|
|
17
|
+
"backend": 1024,
|
|
18
|
+
"worker": 768,
|
|
19
|
+
"infra": 256,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
DOCKER_BASE_IMAGES = {
|
|
23
|
+
"bun": "oven/bun:1.2",
|
|
24
|
+
"node": "node:20-alpine",
|
|
25
|
+
"nginx": "nginx:alpine",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
NETWORK_PREFIX = "tdk"
|
|
29
|
+
|
|
30
|
+
exports = {
|
|
31
|
+
"BASE_PORT_FRONTEND": BASE_PORT_FRONTEND,
|
|
32
|
+
"BASE_PORT_BACKEND": BASE_PORT_BACKEND,
|
|
33
|
+
"HEALTH_CHECK_PATH": HEALTH_CHECK_PATH,
|
|
34
|
+
"HEALTH_CHECK_PATH_LIVE": HEALTH_CHECK_PATH_LIVE,
|
|
35
|
+
"HEALTH_CHECK_PATH_READY": HEALTH_CHECK_PATH_READY,
|
|
36
|
+
"HEALTH_CHECK_INTERVAL": HEALTH_CHECK_INTERVAL,
|
|
37
|
+
"HEALTH_CHECK_TIMEOUT": HEALTH_CHECK_TIMEOUT,
|
|
38
|
+
"HEALTH_CHECK_RETRIES": HEALTH_CHECK_RETRIES,
|
|
39
|
+
"MEMORY_LIMITS": MEMORY_LIMITS,
|
|
40
|
+
"DOCKER_BASE_IMAGES": DOCKER_BASE_IMAGES,
|
|
41
|
+
"NETWORK_PREFIX": NETWORK_PREFIX,
|
|
42
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# TILT_TECH_STACK.star - Technology Stack Configuration
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
RUNTIME = "bun"
|
|
6
|
+
RUNTIME_VERSION = "1.2"
|
|
7
|
+
|
|
8
|
+
BUNDLER = "vite"
|
|
9
|
+
BUNDLER_VERSION = "5"
|
|
10
|
+
|
|
11
|
+
ORM = "prisma"
|
|
12
|
+
ORM_VERSION = "7"
|
|
13
|
+
|
|
14
|
+
MESSAGING = "nats"
|
|
15
|
+
MESSAGING_VERSION = "2"
|
|
16
|
+
NATS_SERVER = "nats://nats:4222"
|
|
17
|
+
|
|
18
|
+
TESTING = "vitest"
|
|
19
|
+
TESTING_VERSION = "1"
|
|
20
|
+
|
|
21
|
+
LINTING = "biome"
|
|
22
|
+
LINTING_VERSION = "1.5"
|
|
23
|
+
|
|
24
|
+
WEB_FRAMEWORK = "hono"
|
|
25
|
+
WEB_FRAMEWORK_VERSION = "4"
|
|
26
|
+
|
|
27
|
+
CONTAINER_PLATFORM = "docker"
|
|
28
|
+
COMPOSE_VERSION = "3.8"
|
|
29
|
+
|
|
30
|
+
def assert_tech_stack(loaded_stack):
|
|
31
|
+
required = {
|
|
32
|
+
"bundler": BUNDLER,
|
|
33
|
+
"runtime": RUNTIME,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for key, expected in required.items():
|
|
37
|
+
actual = loaded_stack.get(key)
|
|
38
|
+
if actual != expected:
|
|
39
|
+
fail("Tech stack mismatch: {} should be '{}' but got '{}'".format(
|
|
40
|
+
key, expected, actual
|
|
41
|
+
))
|
|
42
|
+
|
|
43
|
+
print("✅ Tech stack validated: {} / {} / {}".format(
|
|
44
|
+
BUNDLER, RUNTIME, TESTING
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
exports = {
|
|
48
|
+
"RUNTIME": RUNTIME,
|
|
49
|
+
"RUNTIME_VERSION": RUNTIME_VERSION,
|
|
50
|
+
"BUNDLER": BUNDLER,
|
|
51
|
+
"BUNDLER_VERSION": BUNDLER_VERSION,
|
|
52
|
+
"ORM": ORM,
|
|
53
|
+
"ORM_VERSION": ORM_VERSION,
|
|
54
|
+
"MESSAGING": MESSAGING,
|
|
55
|
+
"MESSAGING_VERSION": MESSAGING_VERSION,
|
|
56
|
+
"NATS_SERVER": NATS_SERVER,
|
|
57
|
+
"TESTING": TESTING,
|
|
58
|
+
"TESTING_VERSION": TESTING_VERSION,
|
|
59
|
+
"LINTING": LINTING,
|
|
60
|
+
"LINTING_VERSION": LINTING_VERSION,
|
|
61
|
+
"WEB_FRAMEWORK": WEB_FRAMEWORK,
|
|
62
|
+
"WEB_FRAMEWORK_VERSION": WEB_FRAMEWORK_VERSION,
|
|
63
|
+
"CONTAINER_PLATFORM": CONTAINER_PLATFORM,
|
|
64
|
+
"COMPOSE_VERSION": COMPOSE_VERSION,
|
|
65
|
+
"assert_tech_stack": assert_tech_stack,
|
|
66
|
+
"TECH_STACK": exports,
|
|
67
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# TDK Example Tiltfile
|
|
2
|
+
# Minimal Tiltfile demonstrating TDK PSR model
|
|
3
|
+
|
|
4
|
+
# Must be set before the extension loads: discovery/constants.star reads it to
|
|
5
|
+
# find this project's spec.master (services/identity, services/appointment)
|
|
6
|
+
# instead of falling back to the beauty-crm monorepo's default scan roots.
|
|
7
|
+
os.environ['TDK_PROJECT_ROOT'] = config.main_dir
|
|
8
|
+
|
|
9
|
+
# Load TDK extension
|
|
10
|
+
v1alpha1.extension_repo(name='tdk-cli', url='https://github.com/tdk-landscape/tdk-cli')
|
|
11
|
+
v1alpha1.extension(name='tdk-cli', repo_name='tdk-cli', repo_path='')
|
|
12
|
+
|
|
13
|
+
# Load TDK utilities
|
|
14
|
+
load('ext://tdk-cli', 'Utils', 'Manifest', 'Config')
|
|
15
|
+
|
|
16
|
+
print("🚀 TDK Example Project")
|
|
17
|
+
print(" Project → Stack → Resource")
|
|
18
|
+
print("")
|
|
19
|
+
|
|
20
|
+
# Default to all services if no args
|
|
21
|
+
config.define_string_list("to-run", args=True)
|
|
22
|
+
cfg = config.parse()
|
|
23
|
+
|
|
24
|
+
# Let TDK handle service discovery and registration
|
|
25
|
+
# Services are discovered from service.json files automatically
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
services:
|
|
2
|
+
nats:
|
|
3
|
+
image: nats:2.10-alpine
|
|
4
|
+
container_name: tdk_example_nats
|
|
5
|
+
command: ["--jetstream", "--store_dir=/data/jetstream", "--http_port", "8222"]
|
|
6
|
+
restart: unless-stopped
|
|
7
|
+
networks:
|
|
8
|
+
tdk_example_backend:
|
|
9
|
+
aliases:
|
|
10
|
+
- tdk_example_nats
|
|
11
|
+
volumes:
|
|
12
|
+
- tdk_example_nats_data:/data
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8222/healthz"]
|
|
15
|
+
interval: 5s
|
|
16
|
+
timeout: 3s
|
|
17
|
+
start_period: 5s
|
|
18
|
+
retries: 20
|
|
19
|
+
|
|
20
|
+
networks:
|
|
21
|
+
tdk_example_backend:
|
|
22
|
+
external: true
|
|
23
|
+
name: tdk_example_backend
|
|
24
|
+
|
|
25
|
+
volumes:
|
|
26
|
+
tdk_example_nats_data:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orders-api",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
7
|
+
"start": "bun run dist/index.js",
|
|
8
|
+
"dev": "bun --watch src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@hono/node-server": "^1.19.0",
|
|
12
|
+
"hono": "^4.9.0",
|
|
13
|
+
"nats": "^2.29.3",
|
|
14
|
+
"pg": "^8.16.3"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^20.0.0",
|
|
18
|
+
"@types/pg": "^8.15.5"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://tdk-landscape.github.io/schema.service.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"appName": "orders-api",
|
|
5
|
+
"appType": "backend",
|
|
6
|
+
"stack": "shop",
|
|
7
|
+
"port": 4000,
|
|
8
|
+
"featuresEnabled": ["nats"],
|
|
9
|
+
"params": {
|
|
10
|
+
"NATS_URL": "nats://tdk_example_nats:4222"
|
|
11
|
+
},
|
|
12
|
+
"healthCheckPath": "/health",
|
|
13
|
+
"dependsOn": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { connect, type NatsConnection } from "nats";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { Pool } from "pg";
|
|
5
|
+
|
|
6
|
+
const port = Number(process.env.PORT ?? 4000);
|
|
7
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
8
|
+
const natsUrl = process.env.NATS_URL ?? "nats://tdk_example_nats:4222";
|
|
9
|
+
if (!databaseUrl) throw new Error("DATABASE_URL is required");
|
|
10
|
+
|
|
11
|
+
const pgUrl = new URL(databaseUrl);
|
|
12
|
+
pgUrl.searchParams.delete("schema");
|
|
13
|
+
const db = new Pool({ connectionString: pgUrl.toString() });
|
|
14
|
+
let nats: NatsConnection;
|
|
15
|
+
let workerReady = false;
|
|
16
|
+
|
|
17
|
+
async function connectDependencies(): Promise<void> {
|
|
18
|
+
while (true) {
|
|
19
|
+
try {
|
|
20
|
+
await db.query(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS demo_orders (
|
|
22
|
+
id bigserial PRIMARY KEY,
|
|
23
|
+
item text NOT NULL,
|
|
24
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
25
|
+
worker_seen_at timestamptz
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
nats = await connect({ servers: natsUrl });
|
|
29
|
+
return;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error("waiting for PostgreSQL and NATS", error);
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const app = new Hono();
|
|
38
|
+
app.use("*", async (c, next) => {
|
|
39
|
+
c.header("Access-Control-Allow-Origin", "*");
|
|
40
|
+
c.header("Access-Control-Allow-Headers", "content-type");
|
|
41
|
+
c.header("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
|
42
|
+
await next();
|
|
43
|
+
});
|
|
44
|
+
app.options("*", (c) => c.body(null, 204));
|
|
45
|
+
app.get("/health", (c) => c.json({ ok: true }));
|
|
46
|
+
app.get("/worker-ready", (c) => c.json({ ready: workerReady }, workerReady ? 200 : 503));
|
|
47
|
+
app.get("/", async (c) => {
|
|
48
|
+
const result = await db.query("SELECT id, item, created_at AS \"createdAt\", worker_seen_at AS \"workerSeenAt\" FROM demo_orders ORDER BY id DESC LIMIT 20");
|
|
49
|
+
return c.json({ orders: result.rows });
|
|
50
|
+
});
|
|
51
|
+
app.post("/", async (c) => {
|
|
52
|
+
if (!workerReady) return c.json({ error: "orders worker is not ready" }, 503);
|
|
53
|
+
const body = await c.req.json<{ item?: unknown }>();
|
|
54
|
+
if (typeof body.item !== "string" || body.item.trim().length === 0) {
|
|
55
|
+
return c.json({ error: "item is required" }, 400);
|
|
56
|
+
}
|
|
57
|
+
const result = await db.query(
|
|
58
|
+
"INSERT INTO demo_orders (item) VALUES ($1) RETURNING id, item, created_at AS \"createdAt\", worker_seen_at AS \"workerSeenAt\"",
|
|
59
|
+
[body.item.trim()],
|
|
60
|
+
);
|
|
61
|
+
const order = result.rows[0];
|
|
62
|
+
nats.publish("demo.orders.created", new TextEncoder().encode(String(order.id)));
|
|
63
|
+
return c.json({ order }, 201);
|
|
64
|
+
});
|
|
65
|
+
app.get("/:id", async (c) => {
|
|
66
|
+
const result = await db.query(
|
|
67
|
+
"SELECT id, item, created_at AS \"createdAt\", worker_seen_at AS \"workerSeenAt\" FROM demo_orders WHERE id = $1",
|
|
68
|
+
[c.req.param("id")],
|
|
69
|
+
);
|
|
70
|
+
if (!result.rows[0]) return c.json({ error: "order not found" }, 404);
|
|
71
|
+
return c.json({ order: result.rows[0] });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await connectDependencies();
|
|
75
|
+
const readySubscription = nats.subscribe("demo.orders.worker-ready");
|
|
76
|
+
await nats.flush();
|
|
77
|
+
void (async () => {
|
|
78
|
+
for await (const _message of readySubscription) workerReady = true;
|
|
79
|
+
})();
|
|
80
|
+
serve({ fetch: app.fetch, port });
|
|
81
|
+
console.log(`orders-api listening on ${port}`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>TDK Orders</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|