@tdk-landscape/tdk-cli-core 1.3.34 → 1.3.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/discovery/discovery_orchestrator.star +4 -4
  2. package/discovery/manifest/loading.star +9 -5
  3. package/discovery/manifest/normalize.star +3 -3
  4. package/discovery/manifest/validation.star +1 -1
  5. package/discovery/validation.star +1 -1
  6. package/dist/commands/doctor.d.ts.map +1 -1
  7. package/dist/commands/doctor.js +120 -6
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/resource.js +1 -1
  10. package/dist/commands/resource.js.map +1 -1
  11. package/dist/commands/up.d.ts.map +1 -1
  12. package/dist/commands/up.js +2 -48
  13. package/dist/commands/up.js.map +1 -1
  14. package/dist/types/index.d.ts +3 -1
  15. package/dist/types/index.d.ts.map +1 -1
  16. package/dist/types/index.js.map +1 -1
  17. package/dist/utils/constants.d.ts +1 -0
  18. package/dist/utils/constants.d.ts.map +1 -1
  19. package/dist/utils/constants.js +11 -0
  20. package/dist/utils/constants.js.map +1 -1
  21. package/dist/utils/resource-features.d.ts +1 -1
  22. package/dist/utils/resource-features.js +1 -1
  23. package/dist/utils/service-urls.d.ts +30 -0
  24. package/dist/utils/service-urls.d.ts.map +1 -0
  25. package/dist/utils/service-urls.js +93 -0
  26. package/dist/utils/service-urls.js.map +1 -0
  27. package/engine/REFACTORING_MANIFEST_SYSTEM.md +1 -1
  28. package/engine/lifecycle/orchestrator.star +4 -3
  29. package/engine/schemas/service-schema.json +5 -5
  30. package/engine/topologies/platform/docker/compose/compose.star +44 -24
  31. package/engine/topologies/platform/docker/constants.star +4 -0
  32. package/engine/topologies/platform/docker/generators/golden_docker_generator_v2.star +1 -0
  33. package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +10 -6
  34. package/engine/topologies/platform/docker/networking/api_path_constants.star +30 -0
  35. package/engine/topologies/platform/docker/networking/traefik.star +2 -2
  36. package/engine/topologies/platform/net/AGENTS.md +64 -0
  37. package/engine/topologies/platform/security/AGENTS.md +49 -0
  38. package/engine/topologies/platform/state/AGENTS.md +64 -0
  39. package/engine/topologies/tilt/generators/vite/backend.star +1 -1
  40. package/engine/topologies/tilt/generators/vite/frontend.star +2 -1
  41. package/engine/topologies/tilt/generators/vite/helpers.star +1 -1
  42. package/engine/topologies/tilt/manifest/constants.star +1 -1
  43. package/engine/topologies/tilt/manifest/mapper.star +2 -2
  44. package/engine/topologies/tilt/manifest/parser.star +2 -2
  45. package/engine/topologies/tilt/manifest/schema.star +2 -2
  46. package/engine/topologies/tilt/manifest/tests/test_loader_parser.star +3 -3
  47. package/engine/topologies/tilt/manifest/tests/test_validator.star +2 -2
  48. package/engine/topologies/tilt/manifest/validator.star +7 -7
  49. package/engine/topologies/tilt/resources/orchestrator/generators/env.star +3 -2
  50. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +3 -2
  51. package/package.json +1 -1
  52. package/engine/schemas/manifest-schema.json +0 -301
@@ -2,7 +2,7 @@
2
2
  * Resource-Level Features
3
3
  *
4
4
  * These are code generators and configurations that run per-service/resource.
5
- * They are controlled via the features: [] array in each service.json file.
5
+ * They are controlled via the featuresEnabled: [] array in each service.json file.
6
6
  *
7
7
  * Frontend services: api-client, env-config, api-index (enabled by default)
8
8
  * Backend services: prisma (enabled by default)
@@ -0,0 +1,30 @@
1
+ import type { DiscoveredResource } from "../types/index.js";
2
+ export interface HealthTarget {
3
+ name: string;
4
+ appType: "frontend" | "backend";
5
+ url: string;
6
+ }
7
+ export interface HealthProbe extends HealthTarget {
8
+ ok: boolean;
9
+ /** HTTP status code, or undefined when the request never completed. */
10
+ status?: number;
11
+ /** Failure reason when the request never completed (DNS, refused, timeout). */
12
+ error?: string;
13
+ }
14
+ export declare function getProjectName(): string;
15
+ export declare function resolveSubdomainBases(): {
16
+ appBase: string;
17
+ apiBase: string;
18
+ };
19
+ export declare function appendHealthPath(path: string): string;
20
+ /** The route `tdk up` advertises for a service, without the /health suffix. */
21
+ export declare function resolveServicePath(resource: DiscoveredResource): string;
22
+ /**
23
+ * Health URLs for every routable service, matching what `tdk up` prints.
24
+ * Workers, libraries, SDKs and migrators have no Traefik route, so they are
25
+ * skipped rather than reported as unreachable.
26
+ */
27
+ export declare function buildHealthTargets(resources: DiscoveredResource[]): HealthTarget[];
28
+ export declare function pingHealthTarget(target: HealthTarget, timeoutMs: number): Promise<HealthProbe>;
29
+ export declare function pingHealthTargets(targets: HealthTarget[], timeoutMs: number): Promise<HealthProbe[]>;
30
+ //# sourceMappingURL=service-urls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-urls.d.ts","sourceRoot":"","sources":["../../src/utils/service-urls.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAG5D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAY,SAAQ,YAAY;IAC/C,EAAE,EAAE,OAAO,CAAC;IACZ,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,cAAc,IAAI,MAAM,CAYvC;AAED,wBAAgB,qBAAqB,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAuB5E;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAMvE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,kBAAkB,EAAE,GAAG,YAAY,EAAE,CAkBlF;AAED,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC,CAiBtB;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,YAAY,EAAE,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,EAAE,CAAC,CAExB"}
@@ -0,0 +1,93 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { findProjectRoot } from "./paths.js";
4
+ export function getProjectName() {
5
+ const root = findProjectRoot();
6
+ if (root) {
7
+ try {
8
+ const content = readFileSync(join(root, ".tdk", "project.json"), "utf-8");
9
+ const parsed = JSON.parse(content);
10
+ if (parsed?.project?.name) {
11
+ return parsed.project.name;
12
+ }
13
+ }
14
+ catch { }
15
+ }
16
+ return "beauty-crm";
17
+ }
18
+ export function resolveSubdomainBases() {
19
+ const projectName = getProjectName();
20
+ const raw = process.env.TDK_SERVICE_BASE_URL ?? `http://${projectName}.localhost`;
21
+ try {
22
+ const u = new URL(raw.includes("://") ? raw : `http://${raw}`);
23
+ const host = u.hostname;
24
+ if (host === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(host)) {
25
+ return {
26
+ appBase: `${u.protocol}//${host}${u.port ? `:${u.port}` : ""}`,
27
+ apiBase: `${u.protocol}//${host}${u.port ? `:${u.port}` : ""}`,
28
+ };
29
+ }
30
+ const bare = host.replace(/^(app|api)\./, "");
31
+ return {
32
+ appBase: `${u.protocol}//app.${bare}${u.port ? `:${u.port}` : ""}`,
33
+ apiBase: `${u.protocol}//api.${bare}${u.port ? `:${u.port}` : ""}`,
34
+ };
35
+ }
36
+ catch {
37
+ return {
38
+ appBase: `http://app.${projectName}.localhost`,
39
+ apiBase: `http://api.${projectName}.localhost`,
40
+ };
41
+ }
42
+ }
43
+ export function appendHealthPath(path) {
44
+ return `${path.replace(/\/+$/, "")}/health`;
45
+ }
46
+ /** The route `tdk up` advertises for a service, without the /health suffix. */
47
+ export function resolveServicePath(resource) {
48
+ if (resource.config?.appType === "backend") {
49
+ const servicePathName = resource.name.replace(/-api$/, "");
50
+ return resource.config?.apiPath ?? `/api/${servicePathName}`;
51
+ }
52
+ return resource.config?.basePath ?? `/${resource.name}`;
53
+ }
54
+ /**
55
+ * Health URLs for every routable service, matching what `tdk up` prints.
56
+ * Workers, libraries, SDKs and migrators have no Traefik route, so they are
57
+ * skipped rather than reported as unreachable.
58
+ */
59
+ export function buildHealthTargets(resources) {
60
+ const { appBase, apiBase } = resolveSubdomainBases();
61
+ return resources
62
+ .filter((resource) => resource.config?.appType === "frontend" || resource.config?.appType === "backend")
63
+ .map((resource) => {
64
+ const base = resource.config.appType === "backend" ? apiBase : appBase;
65
+ return {
66
+ name: resource.name,
67
+ appType: resource.config.appType,
68
+ url: `${base}${appendHealthPath(resolveServicePath(resource))}`,
69
+ };
70
+ });
71
+ }
72
+ export async function pingHealthTarget(target, timeoutMs) {
73
+ try {
74
+ const response = await fetch(target.url, {
75
+ method: "GET",
76
+ redirect: "manual",
77
+ signal: AbortSignal.timeout(timeoutMs),
78
+ });
79
+ return { ...target, ok: response.ok, status: response.status };
80
+ }
81
+ catch (err) {
82
+ const error = err instanceof Error && err.name === "TimeoutError"
83
+ ? `no response within ${timeoutMs}ms`
84
+ : err instanceof Error
85
+ ? err.message
86
+ : String(err);
87
+ return { ...target, ok: false, error };
88
+ }
89
+ }
90
+ export async function pingHealthTargets(targets, timeoutMs) {
91
+ return Promise.all(targets.map((target) => pingHealthTarget(target, timeoutMs)));
92
+ }
93
+ //# sourceMappingURL=service-urls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-urls.js","sourceRoot":"","sources":["../../src/utils/service-urls.ts"],"names":[],"mappings":"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"}
@@ -632,7 +632,7 @@ VALIDATION_RULES = {
632
632
 
633
633
  'prisma_has_database': {
634
634
  'severity': 'warning',
635
- 'condition': lambda m: 'prisma' in m.get('features', []),
635
+ 'condition': lambda m: 'prisma' in m.get('featuresEnabled', []),
636
636
  'check': lambda m: m.get('databaseName'),
637
637
  'message': 'Service with prisma feature should specify databaseName',
638
638
  },
@@ -192,14 +192,15 @@ def should_wait_for_dependencies(resource_config):
192
192
  resources = resource_config.get('resources', [])
193
193
  for res in resources:
194
194
  manifest = res.get('_manifest', {})
195
- if manifest.get('databaseName') or manifest.get('features', []).count('prisma') > 0:
195
+ features = manifest.get('featuresEnabled', [])
196
+ if manifest.get('databaseName') or features.count('prisma') > 0:
196
197
  dependencies.append('database-management')
197
198
  break
198
-
199
+
199
200
  # Check for NATS dependency
200
201
  for res in resources:
201
202
  manifest = res.get('_manifest', {})
202
- if 'nats' in manifest.get('features', []):
203
+ if 'nats' in manifest.get('featuresEnabled', []):
203
204
  dependencies.append('messaging')
204
205
  break
205
206
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://TDK Landscape.local/schemas/service-schema.json",
2
+ "$id": "https://tdk-landscape.github.io/schema.service.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "additionalProperties": false,
5
5
  "allOf": [
@@ -50,14 +50,14 @@
50
50
  },
51
51
  "then": {
52
52
  "properties": {
53
- "features": {
53
+ "featuresEnabled": {
54
54
  "contains": {
55
55
  "const": "nats"
56
56
  }
57
57
  }
58
58
  },
59
59
  "required": [
60
- "features"
60
+ "featuresEnabled"
61
61
  ]
62
62
  }
63
63
  }
@@ -180,7 +180,7 @@
180
180
  ],
181
181
  "type": "object"
182
182
  },
183
- "features": {
183
+ "featuresEnabled": {
184
184
  "description": "Platform features:\n• nats - NATS JetStream eventing\n• prisma - DB ORM & migrations\n• redis - Caching layer\n• infisical - Secrets management\n• vitest - Test framework\n• traefik - Reverse proxy routing\n• websocket - WS support\n• graphql - GraphQL server\n• grpc - gRPC protocol",
185
185
  "items": {
186
186
  "enum": [
@@ -215,7 +215,7 @@
215
215
  "uniqueItems": true
216
216
  },
217
217
  "nats": {
218
- "description": "Custom NATS configuration. Auto-generated if 'nats' in features.",
218
+ "description": "Custom NATS configuration. Auto-generated if 'nats' in featuresEnabled.",
219
219
  "properties": {
220
220
  "queueGroup": {
221
221
  "description": "NATS queue group for load balancing",
@@ -8,6 +8,9 @@
8
8
  # === INLINED CONSTANTS for pure extension loading ===
9
9
  BASE_PORT_FRONTEND = 3000
10
10
  BASE_PORT_BACKEND = 4000
11
+ # The nginx frontend runtime layer always serves on 80: L4_generate_frontend_runtime
12
+ # emits a fixed `EXPOSE 80` and the generated nginx.conf has a fixed `listen 80`.
13
+ NGINX_CONTAINER_PORT = 80
11
14
  HEALTH_CHECK_PATH = "/health"
12
15
  OUTPUT = struct(AUTOGENERATED_FOLDER = ".autogenerated")
13
16
  # === END INLINED CONSTANTS ===
@@ -15,7 +18,7 @@ OUTPUT = struct(AUTOGENERATED_FOLDER = ".autogenerated")
15
18
 
16
19
  load("./compose_helpers.star", "with_runtime_image_tag", "compose_build_config")
17
20
  load("../networking/traefik.star", "get_frontend_traefik_labels", "get_backend_traefik_labels")
18
- load("../networking/api_path_constants.star", "get_api_path_for_domain")
21
+ load("../networking/api_path_constants.star", "get_api_path_for_domain", "get_frontend_base_path")
19
22
  load("../secrets/infisical.star", "get_infisical_environment_vars")
20
23
  load("../auth/auth.star", "AuthConfig")
21
24
 
@@ -76,11 +79,17 @@ def generate_frontend_compose(resource_path, resource_name, res, manifest=None):
76
79
 
77
80
  target_path = res.get('target_path', manifest.get('target_path', '/usr/share/nginx/html'))
78
81
  use_nginx = 'nginx' in target_path or target_path == '/usr/share/nginx/html'
79
- # Read port from manifest (service.json), fallback to BASE_PORT_FRONTEND or 80 for nginx
80
- port = manifest.get('port', 80 if use_nginx else BASE_PORT_FRONTEND)
81
-
82
- # Base path for routing on {project}.localhost
83
- base_path = '/' + manifest.get('basePath', stack + 's').lstrip('/')
82
+ # The manifest `port` is the Vite dev-server port. These compose entries always
83
+ # build the `production` target, so for an nginx runtime the port inside the
84
+ # container is 80, not the dev port. Using the dev port here made the container
85
+ # healthcheck probe a port nothing listens on, which left the container marked
86
+ # unhealthy -- and Traefik's Docker provider skips unhealthy containers, so the
87
+ # router was never registered and every request 404'd.
88
+ port = NGINX_CONTAINER_PORT if use_nginx else manifest.get('port', BASE_PORT_FRONTEND)
89
+
90
+ # Base path for routing on {project}.localhost. Shared with the Vite config and
91
+ # PUBLIC_URL generators -- see get_frontend_base_path for why they must agree.
92
+ base_path = get_frontend_base_path(manifest, res_name)
84
93
 
85
94
  traefik_cfg = manifest.get('traefik', {}) if manifest else {}
86
95
  traefik_host = traefik_cfg.get('host')
@@ -232,15 +241,33 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
232
241
  # All instances share the same Traefik service for load balancing
233
242
  traefik_resource_name = res_name # LB service name (shared)
234
243
 
235
- traefik_labels = get_backend_traefik_labels(
236
- resource_entry_name=resource_entry_name,
237
- traefik_host=traefik_host,
238
- traefik_path=traefik_path,
239
- traefik_resource_name=traefik_resource_name,
240
- internal_port=internal_port,
241
- health_path=health_path,
242
- manifest=manifest,
243
- )
244
+ # Workers have no HTTP server: they consume from NATS and exit-on-crash. Giving
245
+ # them a Traefik route advertises an endpoint that can never answer, and giving
246
+ # them an HTTP healthcheck marks a perfectly healthy worker as unhealthy forever.
247
+ is_worker = manifest.get('appType') == 'worker' if manifest else False
248
+
249
+ if is_worker:
250
+ traefik_labels = ' - "traefik.enable=false"'
251
+ healthcheck_section = ""
252
+ else:
253
+ traefik_labels = get_backend_traefik_labels(
254
+ resource_entry_name=resource_entry_name,
255
+ traefik_host=traefik_host,
256
+ traefik_path=traefik_path,
257
+ traefik_resource_name=traefik_resource_name,
258
+ internal_port=internal_port,
259
+ health_path=health_path,
260
+ manifest=manifest,
261
+ )
262
+ healthcheck_section = """ healthcheck:
263
+ test: ["CMD", "curl", "-f", "--max-time", "5", "http://localhost:{internal_port}{health_path}"]
264
+ interval: 10s
265
+ timeout: 5s
266
+ retries: 6
267
+ # STORY 4 FIX: 30s grace period to prevent 504 errors during startup
268
+ # Services get 30s to initialize before health checks begin
269
+ start_period: 30s
270
+ """.format(internal_port=internal_port, health_path=health_path)
244
271
  infisical_env = get_infisical_environment_vars(as_array=True, resource_name=res_name)
245
272
 
246
273
  # Build auth environment variables section using centralized auth utilities
@@ -271,15 +298,7 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
271
298
  - {network_backend}
272
299
  - {network_database}
273
300
  - {network_infisical}
274
- healthcheck:
275
- test: ["CMD", "curl", "-f", "--max-time", "5", "http://localhost:{internal_port}{health_path}"]
276
- interval: 10s
277
- timeout: 5s
278
- retries: 6
279
- # STORY 4 FIX: 30s grace period to prevent 504 errors during startup
280
- # Services get 30s to initialize before health checks begin
281
- start_period: 30s
282
- # Note: depends_on removed - Tilt manages service startup order
301
+ {healthcheck_section} # Note: depends_on removed - Tilt manages service startup order
283
302
  deploy:
284
303
  resources:
285
304
  limits:
@@ -311,6 +330,7 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
311
330
  nats_queue=nats_queue,
312
331
  nats_url=nats_url,
313
332
  traefik_labels=traefik_labels,
333
+ healthcheck_section=healthcheck_section,
314
334
  infisical_env=infisical_env,
315
335
  auth_env=auth_env,
316
336
  network_traefik_public=PlatformDockerConstants.NETWORK_TRAEFIK_PUBLIC,
@@ -37,6 +37,9 @@ APP_LOCAL_DOMAIN = APP_SUBDOMAIN_PREFIX + "." + LOCAL_DOMAIN
37
37
  API_LOCAL_DOMAIN = API_SUBDOMAIN_PREFIX + "." + LOCAL_DOMAIN
38
38
  EMAIL_DOMAIN = PROJECT_NAME_HYPHEN + ".local"
39
39
 
40
+ # Published resource-config JSON Schema, same for every project (not project-specific like EMAIL_DOMAIN)
41
+ RESOURCE_SCHEMA_URL = "https://tdk-landscape.github.io/schema.service.json"
42
+
40
43
  # NPM scope constant
41
44
  NPM_SCOPE = "@" + PROJECT_NAME_HYPHEN + "/"
42
45
 
@@ -129,6 +132,7 @@ PlatformDockerConstants = struct(
129
132
  APP_LOCAL_DOMAIN = APP_LOCAL_DOMAIN,
130
133
  API_LOCAL_DOMAIN = API_LOCAL_DOMAIN,
131
134
  EMAIL_DOMAIN = EMAIL_DOMAIN,
135
+ RESOURCE_SCHEMA_URL = RESOURCE_SCHEMA_URL,
132
136
  NPM_SCOPE = NPM_SCOPE,
133
137
  BUN_VERSION = BUN_VERSION,
134
138
  BUN_IMAGE = BUN_IMAGE,
@@ -64,6 +64,7 @@ def L4_generate_orchestrator(res_path, res_type = 'backend', resource_name = 'se
64
64
  use_infisical,
65
65
  resource_name,
66
66
  use_golden=use_golden,
67
+ use_prisma=use_prisma,
67
68
  has_prisma_config=prisma_config_exists,
68
69
  manifest=manifest,
69
70
  ),
@@ -54,10 +54,10 @@ GOLDEN_L4_FRONTEND_IMAGE = _docker_cfg.get('golden_l4_frontend_image', _GOLDEN_P
54
54
  _docker_health = get_docker_healthcheck_config()
55
55
 
56
56
 
57
- def L4_generate_backend_runtime(res_path, port = BASE_PORT_BACKEND, cmd = 'bun run start', use_infisical = True, resource_name = 'service', use_golden = True, has_prisma_config = False, manifest = None):
57
+ def L4_generate_backend_runtime(res_path, port = BASE_PORT_BACKEND, cmd = 'bun run start', use_infisical = True, resource_name = 'service', use_golden = True, use_prisma = True, has_prisma_config = False, manifest = None):
58
58
  """
59
59
  Generate the backend runtime layer for deployed services.
60
-
60
+
61
61
  Args:
62
62
  res_path: Resource path (service directory)
63
63
  port: Port to expose (default: BASE_PORT_BACKEND from master config)
@@ -65,7 +65,11 @@ def L4_generate_backend_runtime(res_path, port = BASE_PORT_BACKEND, cmd = 'bun r
65
65
  use_infisical: Enable Infisical secret management (default: True)
66
66
  resource_name: Service name for logging and configuration (default: 'service')
67
67
  use_golden: Whether to use golden L4-backend image (default: True)
68
- has_prisma_config: Whether service has Prisma config (default: False)
68
+ use_prisma: Whether the service has the "prisma" feature enabled - gates copying
69
+ the .prisma/prisma dirs from the L3 build stage, which only exist there when
70
+ L3 itself ran prisma generate (default: True)
71
+ has_prisma_config: Whether service has a generated prisma.config.ts to copy in
72
+ (default: False)
69
73
  manifest: Service manifest dict for feature detection (default: None)
70
74
 
71
75
  Returns:
@@ -88,16 +92,16 @@ def L4_generate_backend_runtime(res_path, port = BASE_PORT_BACKEND, cmd = 'bun r
88
92
  # Bun isolated linker creates service symlinks that resolve through /app/node_modules/.bun.
89
93
  # Copy from production_purger to avoid dragging full dev dependency store into runtime.
90
94
  "COPY --from=l3_production_purger /app/" + res_path + "/node_modules/.bun /app/node_modules/.bun\n",
91
- prisma_runtime_copy(res_path),
95
+ prisma_runtime_copy(res_path) if use_prisma else "",
92
96
  "COPY --from=l3_backend_build /app/" + res_path + "/dist ./dist\n",
93
97
  "COPY " + res_path_rel + "/package.json ./package.json\n",
94
- prisma_bun_symlink_fix(),
98
+ prisma_bun_symlink_fix() if use_prisma else "",
95
99
  bun_hoisted_packages_symlink_fix(),
96
100
  ]
97
101
 
98
102
  # Check for special tooling requirements from manifest features
99
103
  # Services requiring Hugo (or other tools) should specify in manifest features array
100
- manifest_features = manifest.get('features', []) if manifest else []
104
+ manifest_features = manifest.get('featuresEnabled', []) if manifest else []
101
105
  if "hugo" in manifest_features:
102
106
  parts.append("USER root\n")
103
107
  parts.append("RUN apk add --no-cache hugo\n")
@@ -115,6 +115,36 @@ def get_api_path_for_stack(stack, manifest=None):
115
115
  # Backwards compatibility alias
116
116
  get_api_path_for_domain = get_api_path_for_stack
117
117
 
118
+
119
+ def get_frontend_base_path(manifest, res_name=None):
120
+ """Returns the public route a frontend is served under, e.g. "/floor-app".
121
+
122
+ Single source of truth. Three separate generators need this value and they
123
+ MUST agree, because they are three halves of one route:
124
+
125
+ - compose.star -> the Traefik router rule and its stripprefix middleware
126
+ - vite/frontend -> `base`, which prefixes every asset URL in index.html
127
+ - env.star -> PUBLIC_URL
128
+
129
+ If they disagree the app still "starts" but serves a white page: Traefik
130
+ routes /a, index.html asks for /b/assets/*, and nothing answers. They used
131
+ to default to `stack + 's'`, `'/' + stack + 's'` and `'/' + stack`
132
+ respectively -- three different answers, one of which ("operations" + "s")
133
+ produced the nonsense route /operationss.
134
+
135
+ Defaults to the service name: it is unique per service (a stack with two
136
+ frontends would otherwise collide on one route) and it matches the URL
137
+ `tdk up` prints, so what the CLI advertises is what Traefik serves.
138
+ """
139
+ base_path = manifest.get('basePath') if manifest else None
140
+ if not base_path:
141
+ base_path = res_name or (manifest.get('appName') if manifest else None) or ''
142
+ if not base_path:
143
+ return ''
144
+ if not base_path.startswith('/'):
145
+ base_path = '/' + base_path
146
+ return base_path
147
+
118
148
  def get_api_path_for_resource(resource_name):
119
149
  """Returns the full API path for a resource name.
120
150
 
@@ -60,7 +60,7 @@ def get_frontend_traefik_labels(res_name, domain, base_path, port, traefik_host=
60
60
  middleware_name = res_name + TRAEFIK_MIDDLEWARE_SUFFIX
61
61
 
62
62
  # Check if maintenance feature is enabled
63
- features = manifest.get('features', []) if manifest else []
63
+ features = manifest.get('featuresEnabled', []) if manifest else []
64
64
  maintenance_middleware = ""
65
65
  if 'maintenance' in features:
66
66
  maintenance_middleware = ",maintenance@file"
@@ -112,7 +112,7 @@ def get_backend_traefik_labels(
112
112
  middleware_name = resource_entry_name + TRAEFIK_MIDDLEWARE_SUFFIX
113
113
 
114
114
  # Check if maintenance feature is enabled
115
- features = manifest.get('features', []) if manifest else []
115
+ features = manifest.get('featuresEnabled', []) if manifest else []
116
116
  maintenance_middleware = ""
117
117
  if 'maintenance' in features:
118
118
  maintenance_middleware = ",maintenance@file"
@@ -0,0 +1,64 @@
1
+ # AGENTS.md - Networking & Proxy
2
+
3
+ ## Purpose
4
+
5
+ Traefik reverse proxy, networking configuration, and service routing.
6
+
7
+ ## Key Files
8
+
9
+ ### Traefik
10
+ - **`proxy.star`** - Traefik configuration generation
11
+ - **`labels.star`** - Docker labels for Traefik routing
12
+
13
+ ### Network Management
14
+ - **`network.star`** - Docker network setup
15
+ - **`hosts.star`** - /etc/hosts file management
16
+
17
+ ## Common Tasks
18
+
19
+ ### Generate Traefik labels
20
+ ```starlark
21
+ load("./proxy.star", "generate_traefik_labels")
22
+ labels = generate_traefik_labels(
23
+ resource_name="order-management-backend",
24
+ port=4000,
25
+ host="order.backend.{project}.localhost",
26
+ path_prefix="/api/orders"
27
+ )
28
+ ```
29
+
30
+ ### Output
31
+ ```python
32
+ {
33
+ "traefik.enable": "true",
34
+ "traefik.http.routers.order-management-backend.rule": "Host(`order.backend.{project}.localhost`) && PathPrefix(`/api/orders`)",
35
+ "traefik.http.routers.order-management-backend.entrypoints": "web",
36
+ "traefik.http.services.order-management-backend.loadbalancer.server.port": "4000"
37
+ }
38
+ ```
39
+
40
+ ## Architecture
41
+
42
+ ```
43
+ Internet/localhost
44
+
45
+ Traefik (port 80/443)
46
+
47
+ Routes to services by Host/Path
48
+
49
+ Service containers
50
+ ```
51
+
52
+ ## Integration
53
+
54
+ - Called by `resources/` when creating service resources
55
+ - Used by `platform/docker/` for Docker Compose generation
56
+ - Labels applied to docker_build and docker_compose
57
+
58
+ ## Local Development
59
+
60
+ Services accessible at:
61
+ - `{service}.backend.{project}.localhost`
62
+ - `{service}.frontend.{project}.localhost`
63
+
64
+ Managed in `/etc/hosts` automatically.
@@ -0,0 +1,49 @@
1
+ # AGENTS.md - Security & Secrets
2
+
3
+ ## Purpose
4
+
5
+ Secrets management, Infisical integration, and security configuration.
6
+
7
+ ## Key Files
8
+
9
+ ### Secrets Management
10
+ - **`infisical.star`** - Infisical secrets provider integration
11
+ - **`secrets.star`** - Generic secrets handling
12
+ - **`encryption.star`** - Encryption utilities
13
+
14
+ ### Configuration
15
+ - **`tokens.star`** - Token generation and validation
16
+ - **`keys.star`** - Key management
17
+
18
+ ## Common Tasks
19
+
20
+ ### Load secrets from Infisical
21
+ ```starlark
22
+ load("./infisical.star", "Infisical")
23
+ secrets = Infisical.load_secrets(resource_name, environment)
24
+ ```
25
+
26
+ ### Get database credentials
27
+ ```starlark
28
+ load("./infisical.star", "Infisical")
29
+ db_url = Infisical.get_database_url(resource_name)
30
+ ```
31
+
32
+ ## Integration
33
+
34
+ - Called by `resources/` when setting up service environment
35
+ - Used in Docker Compose for injecting secrets
36
+ - Part of service startup chain
37
+
38
+ ## Local Development
39
+
40
+ Without Infisical, falls back to:
41
+ - Environment variables
42
+ - Default values in `spec.master`
43
+
44
+ ## Security Notes
45
+
46
+ - Never commit real secrets
47
+ - Use Infisical for production
48
+ - Local development uses safe defaults
49
+ - Secrets injected at runtime, not in images
@@ -0,0 +1,64 @@
1
+ # AGENTS.md - Database & State Management
2
+
3
+ ## Purpose
4
+
5
+ PostgreSQL database virtualization, per-service database provisioning, and state management.
6
+
7
+ ## Key Files
8
+
9
+ ### Database Provisioning
10
+ - **`relational.star`** - Main database provisioning logic
11
+ - **`compose.star`** - Docker Compose entries for databases
12
+
13
+ ### Prisma Integration
14
+ - **`prisma/migrate_script.star`** - Migration script generation
15
+ - **`prisma/db_readiness.star`** - Database readiness checks
16
+ - **`prisma/prisma_build.star`** - Prisma in Docker builds
17
+ - **`prisma/prisma_runtime.star`** - Prisma client in runtime
18
+
19
+ ### Utilities
20
+ - **`utils.star`** - Database utilities
21
+ - **`constants.star`** - Database constants (ports, users)
22
+
23
+ ## Common Tasks
24
+
25
+ ### Provision database for service
26
+ ```starlark
27
+ load("./relational.star", "provision_database")
28
+ compose_entry = provision_database("order-management-backend", "{project}_order")
29
+ ```
30
+
31
+ ### Get database URL
32
+ ```starlark
33
+ load("./relational.star", "get_database_url")
34
+ url = get_database_url("order-management-backend")
35
+ # Returns: postgresql://postgres:postgres@order-management-backend-db:5432/{project}_order
36
+ ```
37
+
38
+ ### Wait for database
39
+ ```starlark
40
+ load("./prisma/db_readiness.star", "wait_for_db")
41
+ script = wait_for_db(db_url, timeout=30)
42
+ ```
43
+
44
+ ## Architecture
45
+
46
+ Each service with `databaseName` in manifest gets:
47
+ 1. PostgreSQL container in Docker Compose
48
+ 2. Volume for persistence
49
+ 3. Readiness check before migrations
50
+ 4. Dedicated port mapping (avoids conflicts)
51
+
52
+ ## Integration
53
+
54
+ - Called by `resources/deps.star` - when setting up service dependencies
55
+ - Used by `platform/docker/` - for Docker Compose generation
56
+ - Part of service resource dependencies chain
57
+
58
+ ## Database Naming
59
+
60
+ ```
61
+ {project}_{domain} // e.g., {project}_order, {project}_user
62
+ ```
63
+
64
+ Each domain gets one database shared by its services (backend, migrator, etc).
@@ -45,7 +45,7 @@ def generate_backend(manifest, write_fn=None):
45
45
 
46
46
  # 🔥 SMART DETECTION: Check if service uses vite-plugin-node for HMR
47
47
  use_vite_node = manifest.get('useViteNode', False)
48
- features = manifest.get('features', [])
48
+ features = manifest.get('featuresEnabled', [])
49
49
  if 'vite-node' in features or 'vite-plugin-node' in features or 'hmr' in features:
50
50
  use_vite_node = True
51
51