@tdk-landscape/tdk-cli-core 1.3.135 → 1.3.137

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 (36) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/cli/dist/commands/doctor.js +13 -13
  3. package/cli/dist/commands/resource.js +15 -14
  4. package/cli/dist/generator/template-engine.js +16 -9
  5. package/cli/dist/utils/constants.js +22 -3
  6. package/cli/dist/utils/discovery-paths.js +29 -5
  7. package/cli/dist/utils/doctor-report.js +2 -1
  8. package/cli/dist/utils/doctor-runtime.js +12 -4
  9. package/cli/dist/utils/doctor-wiring.js +5 -4
  10. package/cli/dist/utils/generated-paths.js +3 -2
  11. package/cli/dist/utils/host-port-config.js +1 -1
  12. package/cli/dist/utils/project-config-defaults.js +2 -1
  13. package/cli/dist/utils/project-features.js +5 -13
  14. package/cli/dist/utils/resource-features.js +2 -1
  15. package/cli/dist/utils/services.js +2 -2
  16. package/cli/dist/utils/shared-platform-postgres.js +30 -10
  17. package/cli/dist/utils/smoke.js +6 -2
  18. package/cli/dist/utils/stack-features.js +5 -2
  19. package/cli/dist-bundle/chunk-DUOGIS2Y.js +315 -0
  20. package/cli/dist-bundle/cli.js +207 -207
  21. package/cli/dist-bundle/{cold-preflight-DQVK4J7J.js → cold-preflight-UATSRM67.js} +1 -1
  22. package/cli/package.json +1 -1
  23. package/engine/assets/docker/wake-gateway/server.ts +3 -3
  24. package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +3 -3
  25. package/engine/topologies/platform/docker/compose/compose.star +4 -4
  26. package/engine/topologies/platform/docker/constants.star +25 -0
  27. package/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +9 -8
  28. package/engine/topologies/platform/docker/networking/traefik_static_routes.star +3 -1
  29. package/engine/topologies/tilt/generators/vite/templates.star +13 -9
  30. package/engine/topologies/tilt/resources/infra-loader.star +20 -14
  31. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +4 -4
  32. package/engine/topologies/tilt/resources/orchestrator/generators/env.star +3 -2
  33. package/engine/topologies/tilt/resources/orchestrator/generators/frontend.star +2 -1
  34. package/ext/ide-components/shared/templates/base.html +4 -0
  35. package/package.json +1 -1
  36. package/cli/dist-bundle/chunk-M4T3FXXB.js +0 -315
package/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ - Shared platform stacks (postgres, traefik, proxy, messaging, infisical, monitoring, cdc, elk) now run in Compose projects named `<project>-<stack>` (for example `tdk-erp-system-database-management`) instead of the directory-named `database-management`, `tdk-out` and so on. Two TDK projects on one Docker daemon no longer share them.
8
+
9
+ - Two TDK projects that each have a stack with the same name (for example `people`) no longer share Docker names. Compose projects are now `<project>-<stack>-autogenerated`, containers `<project>-<stack>-autogenerated-<service>-1` and images `<project>_<stack>_<service>:dev` (migrators `<project>_<stack>-db-migrator:dev`). Before, the second project's `tdk up` removed the first project's containers as orphans and overwrote its image. Containers from before this change keep the old names and are not removed by the new engine: stop them once, see [docs/layout.md](docs/layout.md).
10
+
7
11
  ## 1.3.130 (2026-10-08)
8
12
 
9
13
  - Releases now come with a signed checksum list: `checksums.txt.sigstore.json` (keyless Sigstore signature from the release workflow) is attached next to the binaries in [tdk-cli-releases](https://github.com/tdk-landscape/tdk-cli-releases/releases) and to a release with the same tag here. How to verify is in [docs/security.md](docs/security.md#releases). The binaries themselves are not signed individually (#869, #840).
@@ -8,7 +8,7 @@ import { Command } from "commander";
8
8
  import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
9
9
  import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_DOCS, WEBCONTAINER_UP_MESSAGE, } from "../utils/agent-host.js";
10
10
  import { projectNeedsBun } from "../utils/bun-requirement.js";
11
- import { MASTER_CONFIG_FILES, REQUIRED_PACKAGE_SCRIPTS } from "../utils/constants.js";
11
+ import { CONTAINER_RUNTIME_CHECK, MASTER_CONFIG_FILES, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
12
12
  import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
13
13
  import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
14
14
  import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
@@ -169,14 +169,14 @@ export async function checkDockerRuntime() {
169
169
  try {
170
170
  await execAsync("docker ps", EXEC_TIMEOUT_MS);
171
171
  return {
172
- name: "Container Runtime",
172
+ name: CONTAINER_RUNTIME_CHECK,
173
173
  didPass: true,
174
174
  message: "Docker Desktop Linux container engine is running",
175
175
  };
176
176
  }
177
177
  catch {
178
178
  return {
179
- name: "Container Runtime",
179
+ name: CONTAINER_RUNTIME_CHECK,
180
180
  didPass: false,
181
181
  message: "Docker is not running",
182
182
  fix: DOCTOR_FIXES.dockerDaemonDown,
@@ -187,7 +187,7 @@ export async function checkDockerRuntime() {
187
187
  try {
188
188
  await execAsync("docker ps", EXEC_TIMEOUT_MS);
189
189
  return {
190
- name: "Container Runtime",
190
+ name: CONTAINER_RUNTIME_CHECK,
191
191
  didPass: true,
192
192
  message: "Docker daemon is running",
193
193
  };
@@ -195,7 +195,7 @@ export async function checkDockerRuntime() {
195
195
  catch (err) {
196
196
  if (isTimeout(err)) {
197
197
  return {
198
- name: "Container Runtime",
198
+ name: CONTAINER_RUNTIME_CHECK,
199
199
  didPass: false,
200
200
  message: "Docker daemon is not responding",
201
201
  fix: DOCTOR_FIXES.dockerDaemonDown,
@@ -205,14 +205,14 @@ export async function checkDockerRuntime() {
205
205
  // Docker not running: check for Colima, then Podman
206
206
  if (await succeeds("colima status")) {
207
207
  return {
208
- name: "Container Runtime",
208
+ name: CONTAINER_RUNTIME_CHECK,
209
209
  didPass: true,
210
210
  message: "Colima (Docker runtime) is running",
211
211
  };
212
212
  }
213
213
  if (findOnPath("colima")) {
214
214
  return {
215
- name: "Container Runtime",
215
+ name: CONTAINER_RUNTIME_CHECK,
216
216
  didPass: false,
217
217
  message: "Colima is installed but not running",
218
218
  fix: DOCTOR_FIXES.dockerDaemonDown,
@@ -220,21 +220,21 @@ export async function checkDockerRuntime() {
220
220
  }
221
221
  if (await succeeds("podman ps")) {
222
222
  return {
223
- name: "Container Runtime",
223
+ name: CONTAINER_RUNTIME_CHECK,
224
224
  didPass: true,
225
225
  message: "Podman is running",
226
226
  };
227
227
  }
228
228
  if (hasDockerCli) {
229
229
  return {
230
- name: "Container Runtime",
230
+ name: CONTAINER_RUNTIME_CHECK,
231
231
  didPass: false,
232
232
  message: "Docker daemon is not running",
233
233
  fix: DOCTOR_FIXES.dockerDaemonDown,
234
234
  };
235
235
  }
236
236
  return {
237
- name: "Container Runtime",
237
+ name: CONTAINER_RUNTIME_CHECK,
238
238
  didPass: false,
239
239
  message: "Docker is not running",
240
240
  fix: DOCTOR_FIXES.dockerMissing,
@@ -831,8 +831,8 @@ function parseDockerStats(raw) {
831
831
  const [name, memPctRaw, cpuPctRaw] = line.split(",");
832
832
  return {
833
833
  name: name ?? "unknown",
834
- memPct: Number.parseFloat((memPctRaw ?? "").replace("%", "")),
835
- cpuPct: Number.parseFloat((cpuPctRaw ?? "").replace("%", "")),
834
+ memPct: Number.parseFloat((memPctRaw ?? "").replace(/%/g, "")),
835
+ cpuPct: Number.parseFloat((cpuPctRaw ?? "").replace(/%/g, "")),
836
836
  };
837
837
  })
838
838
  .filter((stat) => Number.isFinite(stat.memPct) && Number.isFinite(stat.cpuPct));
@@ -1089,7 +1089,7 @@ export const doctorCommand = new Command("doctor")
1089
1089
  const { checks: results, errors } = await collectDoctorChecks(machineChecks, inProject ? projectChecks : []);
1090
1090
  if (isContainerHost(host.kind)) {
1091
1091
  for (const result of results) {
1092
- if (result.name === "Container Runtime" && !result.didPass) {
1092
+ if (result.name === CONTAINER_RUNTIME_CHECK && !result.didPass) {
1093
1093
  result.fix = DEVCONTAINER_DOCKER_FIX;
1094
1094
  }
1095
1095
  }
@@ -12,6 +12,7 @@ import { hasDddLicense } from "../generator/extension-fetch.js";
12
12
  import { getDockerfileTemplate, getTestTemplate, getWorkerIndexTemplate, } from "../generator/resource-templates.js";
13
13
  import { CREATABLE_RESOURCE_TYPES } from "../types/index.js";
14
14
  import { assertValid, confirmOrCancel } from "../utils/command-helpers.js";
15
+ import { BRING_YOUR_OWN_TYPE, SERVICE_JSON } from "../utils/constants.js";
15
16
  import { chooseResourcePath, isPathDiscovered, readDiscoveryPaths, } from "../utils/discovery-paths.js";
16
17
  import { errorFactories, requireProjectRoot, runCommand, TdkError } from "../utils/errors.js";
17
18
  import { writeFilesWithProgress } from "../utils/file-helpers.js";
@@ -55,7 +56,7 @@ export const TYPE_SPECIFIC = {
55
56
  mcp: {
56
57
  healthCheckPath: "/health",
57
58
  },
58
- "bring-your-own": {
59
+ [BRING_YOUR_OWN_TYPE]: {
59
60
  healthCheckPath: "/health",
60
61
  },
61
62
  };
@@ -123,7 +124,7 @@ export function createByoServiceJson(name, stack, port, options) {
123
124
  $schema: SERVICE_MANIFEST_SCHEMA_URL,
124
125
  schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
125
126
  appName: name,
126
- appType: "bring-your-own",
127
+ appType: BRING_YOUR_OWN_TYPE,
127
128
  stack,
128
129
  port,
129
130
  healthCheckPath: options.healthCheckPath,
@@ -251,7 +252,7 @@ export const TSCONFIG_TEMPLATE = {
251
252
  export { getBackendIndexTemplate };
252
253
  /** Resource types `tdk resource --type` accepts; `byo` is an alias of `bring-your-own`. */
253
254
  export function parseResourceType(type) {
254
- const normalized = type === "byo" ? "bring-your-own" : type;
255
+ const normalized = type === "byo" ? BRING_YOUR_OWN_TYPE : type;
255
256
  const validTypes = [...CREATABLE_RESOURCE_TYPES, "sdk"];
256
257
  if (!validTypes.includes(normalized)) {
257
258
  throw new TdkError(`Unknown resource type "${type}". Supported types: ${validTypes.join(", ")}.`, [`Use one of: ${validTypes.join(", ")}`, "Omit --type to create a backend"]);
@@ -412,7 +413,7 @@ export const resourceCommand = new Command("resource")
412
413
  frontend: `apps/${resourceName}`,
413
414
  worker: `workers/${resourceName}`,
414
415
  mcp: `services/${stackName}/${resourceName}`,
415
- "bring-your-own": `services/${stackName}/${resourceName}`,
416
+ [BRING_YOUR_OWN_TYPE]: `services/${stackName}/${resourceName}`,
416
417
  sdk: `packages/${resourceName}`,
417
418
  };
418
419
  finalResourcePath = defaultPaths[resourceType];
@@ -434,7 +435,7 @@ export const resourceCommand = new Command("resource")
434
435
  }
435
436
  // Check if resource already exists
436
437
  const isExistingResource = existsSync(fullPath);
437
- const hasServiceJson = existsSync(resolve(fullPath, "service.json"));
438
+ const hasServiceJson = existsSync(resolve(fullPath, SERVICE_JSON));
438
439
  if (resourceType === "sdk" && !hasServiceJson) {
439
440
  throw new TdkError(`No service.json in ${finalResourcePath}`, [
440
441
  'Add one with "appType": "sdk", then run this again to register it',
@@ -443,14 +444,14 @@ export const resourceCommand = new Command("resource")
443
444
  const shouldRegisterExisting = options.registerExisting ||
444
445
  resourceType === "sdk" ||
445
446
  (isExistingResource && hasServiceJson);
446
- if (isExistingResource && !shouldRegisterExisting && resourceType !== "bring-your-own") {
447
+ if (isExistingResource && !shouldRegisterExisting && resourceType !== BRING_YOUR_OWN_TYPE) {
447
448
  errorFactories.directoryExists(fullPath).exit();
448
449
  }
449
450
  const nextPort = resourceType === "sdk"
450
451
  ? 0
451
452
  : assignPort(resourceType, allResources);
452
453
  if (options.restart !== undefined) {
453
- if (resourceType !== "bring-your-own") {
454
+ if (resourceType !== BRING_YOUR_OWN_TYPE) {
454
455
  throw new TdkError("--restart can only be used with --type bring-your-own.", [
455
456
  "Add --type bring-your-own, or drop --restart",
456
457
  ]);
@@ -461,7 +462,7 @@ export const resourceCommand = new Command("resource")
461
462
  ]);
462
463
  }
463
464
  }
464
- const assignedPort = resourceType === "bring-your-own"
465
+ const assignedPort = resourceType === BRING_YOUR_OWN_TYPE
465
466
  ? resolveByoPort(options.port, nextPort, allResources)
466
467
  : nextPort;
467
468
  console.log(chalk.gray("\nResource details:"));
@@ -483,7 +484,7 @@ export const resourceCommand = new Command("resource")
483
484
  if (shouldRegisterExisting && hasServiceJson) {
484
485
  // Read existing service.json
485
486
  const { readFileSync } = await import("node:fs");
486
- const existingServiceJsonPath = resolve(fullPath, "service.json");
487
+ const existingServiceJsonPath = resolve(fullPath, SERVICE_JSON);
487
488
  const existingContent = readFileSync(existingServiceJsonPath, "utf-8");
488
489
  const existingServiceJson = JSON.parse(existingContent);
489
490
  // Update with new values while preserving existing fields
@@ -506,7 +507,7 @@ export const resourceCommand = new Command("resource")
506
507
  return;
507
508
  }
508
509
  // Handle bring-your-own type
509
- if (resourceType === "bring-your-own") {
510
+ if (resourceType === BRING_YOUR_OWN_TYPE) {
510
511
  console.log(chalk.blue("\n📁 Creating bring-your-own resource..."));
511
512
  // Parse port option
512
513
  const port = assignedPort;
@@ -528,7 +529,7 @@ export const resourceCommand = new Command("resource")
528
529
  ...(options.restart ? { restart: options.restart } : {}),
529
530
  });
530
531
  const { writeFileSync } = await import("node:fs");
531
- writeFileSync(resolve(fullPath, "service.json"), JSON.stringify(byoServiceJson, null, 2));
532
+ writeFileSync(resolve(fullPath, SERVICE_JSON), JSON.stringify(byoServiceJson, null, 2));
532
533
  // Create Dockerfile stub if no image provided and dockerfile doesn't exist
533
534
  if (!options.image && !existsSync(dockerfilePath)) {
534
535
  mkdirSync(resolve(dockerfilePath, ".."), { recursive: true });
@@ -599,7 +600,7 @@ This file contains the resource configuration for TDK.
599
600
  const tasks = [
600
601
  {
601
602
  type: "json",
602
- filename: "service.json",
603
+ filename: SERVICE_JSON,
603
604
  content: serviceJson,
604
605
  description: "Generating service.json",
605
606
  emoji: "📝",
@@ -676,7 +677,7 @@ This file contains the resource configuration for TDK.
676
677
  const languageFiles = backendLanguage?.createFiles?.(resourceName);
677
678
  if (languageFiles) {
678
679
  const owned = new Set(languageFiles.map((file) => file.filename));
679
- const sharedTasks = tasks.filter((task) => task.filename === "service.json" ||
680
+ const sharedTasks = tasks.filter((task) => task.filename === SERVICE_JSON ||
680
681
  !(owned.has(task.filename) ||
681
682
  task.filename === "Dockerfile" ||
682
683
  task.filename === "src/index.ts" ||
@@ -715,7 +716,7 @@ This file contains the resource configuration for TDK.
715
716
  writeFilesWithProgress(migratorPath, [
716
717
  {
717
718
  type: "json",
718
- filename: "service.json",
719
+ filename: SERVICE_JSON,
719
720
  content: migratorConfig,
720
721
  description: "Generating Prisma migrator manifest",
721
722
  emoji: "🗃️",
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import Handlebars from "handlebars";
7
7
  import { PLATFORM_STANDARDS } from "../config/platform-standards.js";
8
8
  import { writeTextFileAtomic } from "../utils/atomic-write.js";
9
+ import { DATABASE_MANAGEMENT_FEATURE, TILTIGNORE_FILE } from "../utils/constants.js";
9
10
  import { writeTextFile } from "../utils/file-helpers.js";
10
11
  import { assertTdkGeneratedPath } from "../utils/generated-paths.js";
11
12
  import { DEFAULT_ALWAYS_ENABLED_INFRA } from "../utils/project-config-defaults.js";
@@ -52,7 +53,7 @@ let embeddedTemplatesCache = null;
52
53
  function getEmbeddedTemplates() {
53
54
  if (!embeddedTemplatesCache) {
54
55
  embeddedTemplatesCache = {
55
- ".tiltignore": loadTemplate(".tiltignore.hbs"),
56
+ [TILTIGNORE_FILE]: loadTemplate(".tiltignore.hbs"),
56
57
  "TILT_RESOURCE_DEFAULTS.star": loadTemplate("TILT_RESOURCE_DEFAULTS.star.hbs"),
57
58
  "TILT_TECH_STACK.star": loadTemplate("TILT_TECH_STACK.star.hbs"),
58
59
  Tiltfile: loadTemplate("Tiltfile.hbs"),
@@ -66,6 +67,7 @@ function getEmbeddedTemplates() {
66
67
  const RESOURCE_DESCRIPTIONS = {
67
68
  proxy: "Traefik reverse proxy",
68
69
  verdaccio: "Private npm registry",
70
+ // This key is emitted into spec.master, so it must stay the literal stack id.
69
71
  "database-management": "PostgreSQL database",
70
72
  };
71
73
  const INFRA_DESCRIPTIONS = {
@@ -93,12 +95,16 @@ export class TemplateEngine {
93
95
  this.templatesDir = templatesDir || path.join(currentDir, "..", "..", "templates");
94
96
  this.registerHelpers();
95
97
  }
98
+ // Backslashes must be escaped before quotes, or a value ending in a backslash escapes the closing quote.
99
+ quoteStarlarkString(s) {
100
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
101
+ }
96
102
  formatStarlarkValue(val) {
97
103
  if (val === null || val === undefined) {
98
104
  return "None";
99
105
  }
100
106
  if (typeof val === "string") {
101
- return `"${val.replace(/"/g, '\\"')}"`;
107
+ return this.quoteStarlarkString(val);
102
108
  }
103
109
  if (typeof val === "boolean") {
104
110
  return val ? "True" : "False";
@@ -112,7 +118,7 @@ export class TemplateEngine {
112
118
  }
113
119
  if (typeof val === "object") {
114
120
  const entries = Object.entries(val).map(([key, v]) => {
115
- return `"${key}": ${this.formatStarlarkValue(v)}`;
121
+ return `${this.quoteStarlarkString(key)}: ${this.formatStarlarkValue(v)}`;
116
122
  });
117
123
  return `{${entries.join(", ")}}`;
118
124
  }
@@ -195,6 +201,7 @@ export class TemplateEngine {
195
201
  "TILT_RESOURCE_DEFAULTS.star": this.generateServiceDefaults(context),
196
202
  "spec.master": this.generateSpecMaster(context),
197
203
  Tiltfile: this.generateTiltfile(context),
204
+ // Key must stay the literal: Record<GeneratedFileName, ...> requires it.
198
205
  ".tiltignore": this.generateTiltIgnore(context),
199
206
  };
200
207
  }
@@ -456,16 +463,16 @@ export async function generateMasterConfigs(projectRoot, options = {}) {
456
463
  writeTextFileAtomic(filePath, content);
457
464
  console.log(`✓ Generated: .tdk/.tdk-out/${filename}`);
458
465
  }
459
- if (isStackFeatureEnabledInStacks(projectConfig.phases, "database-management")) {
460
- const composePath = assertTdkGeneratedPath(projectRoot, path.join("services", "platform", "database-management", "docker-compose.yml"));
466
+ if (isStackFeatureEnabledInStacks(projectConfig.phases, DATABASE_MANAGEMENT_FEATURE)) {
467
+ const composePath = assertTdkGeneratedPath(projectRoot, path.join("services", "platform", DATABASE_MANAGEMENT_FEATURE, "docker-compose.yml"));
461
468
  const composeDir = path.dirname(composePath);
462
469
  fs.mkdirSync(composeDir, { recursive: true });
463
470
  writeTextFileAtomic(composePath, generateDatabaseManagementCompose(projectConfig));
464
471
  console.log("✓ Generated: services/platform/database-management/docker-compose.yml");
465
472
  }
466
473
  // Copy .tiltignore to project root so Tilt uses it
467
- const tiltignoreSource = path.join(outputDir, ".tiltignore");
468
- const tiltignoreTarget = assertTdkGeneratedPath(projectRoot, ".tiltignore");
474
+ const tiltignoreSource = path.join(outputDir, TILTIGNORE_FILE);
475
+ const tiltignoreTarget = assertTdkGeneratedPath(projectRoot, TILTIGNORE_FILE);
469
476
  if (fs.existsSync(tiltignoreSource)) {
470
477
  if (!fs.existsSync(tiltignoreTarget)) {
471
478
  fs.copyFileSync(tiltignoreSource, tiltignoreTarget);
@@ -652,7 +659,7 @@ export function verifyMasterConfigs(projectRoot) {
652
659
  }
653
660
  // `tdk project` never overwrites a root .tiltignore the repo already has, so one
654
661
  // without TDK's header belongs to the team: it is not drift.
655
- const rootTiltIgnore = path.join(projectRoot, ".tiltignore");
662
+ const rootTiltIgnore = path.join(projectRoot, TILTIGNORE_FILE);
656
663
  const rootTiltIgnoreText = fs.existsSync(rootTiltIgnore)
657
664
  ? fs.readFileSync(rootTiltIgnore, "utf-8")
658
665
  : undefined;
@@ -660,7 +667,7 @@ export function verifyMasterConfigs(projectRoot) {
660
667
  warnings.push(".tiltignore: not generated by TDK, left as is");
661
668
  }
662
669
  else {
663
- addDrift(".tiltignore", rootTiltIgnoreText, expectedFiles[".tiltignore"]);
670
+ addDrift(TILTIGNORE_FILE, rootTiltIgnoreText, expectedFiles[TILTIGNORE_FILE]);
664
671
  }
665
672
  for (const manifestPath of discoverServiceManifestPaths(projectRoot)) {
666
673
  const displayPath = path.relative(projectRoot, manifestPath).split(path.sep).join("/");
@@ -1,3 +1,22 @@
1
+ // Name of the doctor check that verifies a container runtime is reachable.
2
+ // It is looked up by name in doctor-report.ts, so both sides must use this
3
+ // constant instead of a bare string literal.
4
+ export const CONTAINER_RUNTIME_CHECK = "Container Runtime";
5
+ // Resource type id for services the user brings themselves. Referenced by
6
+ // value in several modules, so a typo in one copy would compile and silently
7
+ // stop matching. `types/index.ts` keeps its own literal: it is the leaf module
8
+ // that defines ResourceType, so importing this file back would be circular.
9
+ // Typed as the literal (not the wider ResourceType union): computed keys built
10
+ // from it stay narrow enough to satisfy Record<ResourceType, ...>.
11
+ export const BRING_YOUR_OWN_TYPE = "bring-your-own";
12
+ // Compile-time check that the literal above really is a ResourceType member.
13
+ const _bringYourOwnIsResourceType = BRING_YOUR_OWN_TYPE;
14
+ // Stack feature id for the shared platform database.
15
+ export const DATABASE_MANAGEMENT_FEATURE = "database-management";
16
+ // Root .tiltignore written by the generator and checked by doctor drift.
17
+ export const TILTIGNORE_FILE = ".tiltignore";
18
+ // Manifest file name for a resource, used by discovery and the resource command.
19
+ export const SERVICE_JSON = "service.json";
1
20
  export const MASTER_CONFIG_FILES = [
2
21
  "TILT_TECH_STACK.star",
3
22
  "TILT_RESOURCE_DEFAULTS.star",
@@ -11,7 +30,7 @@ export const VALID_RESOURCE_TYPES = [
11
30
  "worker",
12
31
  "migrator",
13
32
  "mcp",
14
- "bring-your-own",
33
+ BRING_YOUR_OWN_TYPE,
15
34
  ];
16
35
  // Frontend/library/sdk services are served statically or consumed as
17
36
  // packages, so they never need a "start" script - only backend/worker/
@@ -25,7 +44,7 @@ export const REQUIRED_PACKAGE_SCRIPTS = {
25
44
  frontend: ["dev", "build"],
26
45
  library: ["dev", "build"],
27
46
  sdk: ["dev", "build"],
28
- "bring-your-own": [],
47
+ [BRING_YOUR_OWN_TYPE]: [],
29
48
  };
30
49
  // Version specifiers that pin nothing at all - a fresh `bun install`/`npm install`
31
50
  // can pull a different (possibly breaking) version on every run.
@@ -42,7 +61,7 @@ export const PORT_RANGES = {
42
61
  backend: { base: 4000, min: 4000, max: 4999, range: "4000-4999" },
43
62
  // An MCP server is an HTTP service, so it shares the backend-compatible range with bring-your-own.
44
63
  mcp: { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
45
- "bring-your-own": { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
64
+ [BRING_YOUR_OWN_TYPE]: { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
46
65
  worker: { base: 6000, min: 6000, max: 6999, range: "6000-6999" },
47
66
  health: { base: 5000, min: 5000, max: 5999, range: "5000-5999" },
48
67
  migrator: { base: 7000, min: 7000, max: 7999, range: "7000-7999" },
@@ -11,12 +11,36 @@ const DEFAULT_DISCOVERY_PATHS = ["services/*/*"];
11
11
  function segments(path) {
12
12
  return path.split(sep).filter(Boolean);
13
13
  }
14
+ // Wildcard match without building a RegExp: `*` matches any run of characters, `?` one character,
15
+ // everything else is literal. Greedy with a single backtrack point, so it stays linear-ish on
16
+ // hostile patterns such as "a*a*a*b".
14
17
  function segmentMatches(pattern, name) {
15
- const source = pattern
16
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
17
- .replace(/\*/g, "[^/]*")
18
- .replace(/\?/g, "[^/]");
19
- return new RegExp(`^${source}$`).test(name);
18
+ let p = 0;
19
+ let n = 0;
20
+ let starP = -1;
21
+ let starN = 0;
22
+ while (n < name.length) {
23
+ if (p < pattern.length &&
24
+ pattern[p] !== "*" &&
25
+ (pattern[p] === "?" || pattern[p] === name[n])) {
26
+ p++;
27
+ n++;
28
+ }
29
+ else if (p < pattern.length && pattern[p] === "*") {
30
+ starP = p++;
31
+ starN = n;
32
+ }
33
+ else if (starP !== -1) {
34
+ p = starP + 1;
35
+ n = ++starN;
36
+ }
37
+ else {
38
+ return false;
39
+ }
40
+ }
41
+ while (p < pattern.length && pattern[p] === "*")
42
+ p++;
43
+ return p === pattern.length;
20
44
  }
21
45
  function globMatchesDir(patternSegments, dirSegments) {
22
46
  return (patternSegments.length === dirSegments.length &&
@@ -1,3 +1,4 @@
1
+ import { CONTAINER_RUNTIME_CHECK } from "./constants.js";
1
2
  /** Wait for concurrent probes to finish even if one throws, so they can release resources. */
2
3
  export async function collectDoctorChecks(machineChecks, projectChecks) {
3
4
  const checks = [];
@@ -21,7 +22,7 @@ export async function collectDoctorChecks(machineChecks, projectChecks) {
21
22
  return { checks, errors };
22
23
  }
23
24
  function containerRuntimeReachable(checks) {
24
- const runtime = checks.find((check) => check.name === "Container Runtime");
25
+ const runtime = checks.find((check) => check.name === CONTAINER_RUNTIME_CHECK);
25
26
  return runtime ? runtime.didPass : null;
26
27
  }
27
28
  /** Produce the same readiness decision for human and machine consumers. */
@@ -56,9 +56,12 @@ export function parsePublishedPortHolders(dockerPsOutput, ports) {
56
56
  const portsField = tab >= 0 ? trimmed.slice(tab + 1) : "";
57
57
  const publishedPorts = [];
58
58
  for (const port of wanted) {
59
- // Matches 0.0.0.0:80->, :::80->, 127.0.0.1:80->
60
- const pattern = new RegExp(`(?:^|[\\s,])(?:\\d[\\d.]*:|\\[::\\]:|::)?${port}->`);
61
- if (pattern.test(portsField) || portsField.includes(`:${port}->`)) {
59
+ // Each mapping is "[ip:]hostPort->containerPort/proto"; match the host port exactly
60
+ // (80-> or 0.0.0.0:80->, :::80->, 127.0.0.1:80->), never a longer number such as 1080->.
61
+ const published = portsField
62
+ .split(/[\s,]+/)
63
+ .some((mapping) => mapping.startsWith(`${port}->`) || mapping.includes(`:${port}->`));
64
+ if (published) {
62
65
  publishedPorts.push(port);
63
66
  }
64
67
  }
@@ -643,7 +646,7 @@ export function orderTiltFailures(failures) {
643
646
  * Best-effort root cause for runtime crashes when buildHistory has no error.
644
647
  * Reads recent docker logs for the matching container name.
645
648
  */
646
- export function probeContainerRuntimeError(resourceName, exec = execSync) {
649
+ export function probeContainerRuntimeError(resourceName, exec = execSync, projectName = getProjectName()) {
647
650
  try {
648
651
  const names = runHostCommand(exec, "docker", ["ps", "-a", "--format", "{{.Names}}"], "docker ps -a --format '{{.Names}}'", {
649
652
  stdio: "pipe",
@@ -653,7 +656,12 @@ export function probeContainerRuntimeError(resourceName, exec = execSync) {
653
656
  .split("\n")
654
657
  .map((line) => line.trim())
655
658
  .filter(Boolean);
659
+ // Compose names the container <project>-<stack>-autogenerated-<resource>-1. Prefer this project's
660
+ // container: another TDK project can run a stack with the same name on the same Docker daemon.
661
+ const composeSuffix = `-autogenerated-${resourceName}-1`;
656
662
  const match = names.find((name) => name === resourceName) ??
663
+ names.find((name) => name.startsWith(`${projectName}-`) && name.endsWith(composeSuffix)) ??
664
+ names.find((name) => name.endsWith(composeSuffix)) ??
657
665
  names.find((name) => name.includes(resourceName));
658
666
  if (!match)
659
667
  return null;
@@ -4,6 +4,7 @@ import { execFileSync, execSync } from "node:child_process";
4
4
  import { existsSync, readdirSync, readFileSync } from "node:fs";
5
5
  import { basename, dirname, join, relative } from "node:path";
6
6
  import { BACKEND_LANGUAGES } from "../backend-languages/registry.js";
7
+ import { BRING_YOUR_OWN_TYPE, DATABASE_MANAGEMENT_FEATURE } from "./constants.js";
7
8
  import { execAsync } from "./exec-async.js";
8
9
  import { formatCount } from "./formatting.js";
9
10
  import { findProjectRoot } from "./paths.js";
@@ -123,7 +124,7 @@ export function checkPrismaConsistency(projectRoot = findProjectRoot() ?? proces
123
124
  findings.push(...prismaConfigFindings(resource.path).map((finding) => `${resource.name}: ${finding}`));
124
125
  const appType = resource.config?.appType ?? resource.type;
125
126
  if (appType === "migrator") {
126
- const postgresDependency = resource.config?.dependsOn?.some((name) => name === "postgres" || name === "database-management");
127
+ const postgresDependency = resource.config?.dependsOn?.some((name) => name === "postgres" || name === DATABASE_MANAGEMENT_FEATURE);
127
128
  if (!postgresDependency)
128
129
  findings.push(`${resource.name}: migrator must depend on postgres or database-management`);
129
130
  }
@@ -182,7 +183,7 @@ function ownsRuntimeFiles(resource) {
182
183
  * (or its `buildContext`), or pulls `image`, and never generates the golden-layer Dockerfile that COPYs a package.json.
183
184
  */
184
185
  function isBringYourOwn(resource) {
185
- return (resource.config?.appType ?? resource.type) === "bring-your-own";
186
+ return (resource.config?.appType ?? resource.type) === BRING_YOUR_OWN_TYPE;
186
187
  }
187
188
  export function checkResourcePackageJson(projectRoot = findProjectRoot() ?? process.cwd()) {
188
189
  const missing = discoverResourcesFromRoot(projectRoot)
@@ -236,7 +237,7 @@ function isRoutedToPort(resource) {
236
237
  const appType = resource.config?.appType;
237
238
  if (isApiServiceType(appType))
238
239
  return true;
239
- return appType === "bring-your-own" && resource.config?.exposeViaProxy !== false;
240
+ return appType === BRING_YOUR_OWN_TYPE && resource.config?.exposeViaProxy !== false;
240
241
  }
241
242
  /**
242
243
  * Two routed resources on one `port`. This is a warning, not a failure: the engine publishes no service
@@ -343,7 +344,7 @@ export function checkSharedStackRoutes(projectRoot = findProjectRoot() ?? proces
343
344
  for (const resource of discoverResourcesFromRoot(projectRoot)) {
344
345
  const config = resource.config;
345
346
  const routable = isApiServiceType(config?.appType) ||
346
- (config?.appType === "bring-your-own" && config.exposeViaProxy !== false);
347
+ (config?.appType === BRING_YOUR_OWN_TYPE && config.exposeViaProxy !== false);
347
348
  if (!routable)
348
349
  continue;
349
350
  const directory = dirname(resource.path);
@@ -2,13 +2,14 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { existsSync, lstatSync } from "node:fs";
4
4
  import { isAbsolute, normalize, relative, sep } from "node:path";
5
+ import { TILTIGNORE_FILE } from "./constants.js";
5
6
  /** Reviewed TDK-owned destinations written by `tdk config regenerate` or Tilt generation. */
6
7
  export const TDK_GENERATED_PATH_ALLOWLIST = [
7
8
  ".autogenerated/**",
8
9
  ".tdk/.tdk-out/**",
9
10
  "Tiltfile",
10
11
  "TILT_*.star",
11
- ".tiltignore",
12
+ TILTIGNORE_FILE,
12
13
  "package.json (create only)",
13
14
  "shared-platform-engineering/docker-templates/**",
14
15
  "services/platform/database-management/docker-compose.yml",
@@ -29,7 +30,7 @@ export function isTdkGeneratedPath(path) {
29
30
  }
30
31
  return (normalized === "Tiltfile" ||
31
32
  /^TILT_[^/]+\.star$/.test(normalized) ||
32
- normalized === ".tiltignore" ||
33
+ normalized === TILTIGNORE_FILE ||
33
34
  normalized === "package.json" ||
34
35
  normalized === "services/platform/database-management/docker-compose.yml");
35
36
  }
@@ -11,7 +11,7 @@ export function isDockerPortOwnedByProject(dockerPs, projectPrefix, port) {
11
11
  const containerName = name.toLowerCase();
12
12
  const prefix = projectPrefix.toLowerCase();
13
13
  return ((containerName.startsWith(`${prefix}_`) || containerName.startsWith(`${prefix}-`)) &&
14
- new RegExp(`:${port}->`).test(ports));
14
+ ports.includes(`:${port}->`));
15
15
  });
16
16
  }
17
17
  export function readSavedHostPortPlan(projectRoot) {
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) 2026 TDK Landscape contributors
2
2
  // SPDX-License-Identifier: MIT
3
+ import { DATABASE_MANAGEMENT_FEATURE } from "./constants.js";
3
4
  /**
4
5
  * Defaults shared by project generation and CLI predicates.
5
6
  *
@@ -13,5 +14,5 @@
13
14
  * they report Postgres-will-start while the engine does not start it (or
14
15
  * the reverse). See shared-platform-postgres.ts.
15
16
  */
16
- export const DEFAULT_ALWAYS_ENABLED_INFRA = ["database-management", "proxy"];
17
+ export const DEFAULT_ALWAYS_ENABLED_INFRA = [DATABASE_MANAGEMENT_FEATURE, "proxy"];
17
18
  //# sourceMappingURL=project-config-defaults.js.map
@@ -1,14 +1,6 @@
1
1
  // Copyright (c) 2026 TDK Landscape contributors
2
2
  // SPDX-License-Identifier: MIT
3
- /**
4
- * Project-Level Features
5
- *
6
- * These are infrastructure and optional services enabled at the project level.
7
- * They are available to all resources and are configured in project.json stacks.
8
- *
9
- * Default: All CORE features enabled in pre_alpha stack
10
- * Optional/premium: disabled by default (can be enabled in optional_infra)
11
- */
3
+ import { DATABASE_MANAGEMENT_FEATURE } from "./constants.js";
12
4
  export const PROJECT_FEATURES = {
13
5
  // ============================================================================
14
6
  // CORE INFRASTRUCTURE (always enabled in pre_alpha)
@@ -33,8 +25,8 @@ export const PROJECT_FEATURES = {
33
25
  enabled_by_default: false,
34
26
  dependsOn: [],
35
27
  },
36
- "database-management": {
37
- name: "database-management",
28
+ [DATABASE_MANAGEMENT_FEATURE]: {
29
+ name: DATABASE_MANAGEMENT_FEATURE,
38
30
  description: "PostgreSQL database for data persistence and SQL operations",
39
31
  category: "core",
40
32
  phase: "pre_alpha",
@@ -50,7 +42,7 @@ export const PROJECT_FEATURES = {
50
42
  category: "optional",
51
43
  phase: "pre_alpha",
52
44
  enabled_by_default: false,
53
- dependsOn: ["database-management"],
45
+ dependsOn: [DATABASE_MANAGEMENT_FEATURE],
54
46
  },
55
47
  elk: {
56
48
  name: "elk",
@@ -66,7 +58,7 @@ export const PROJECT_FEATURES = {
66
58
  category: "optional",
67
59
  phase: "pre_alpha",
68
60
  enabled_by_default: false,
69
- dependsOn: ["database-management"],
61
+ dependsOn: [DATABASE_MANAGEMENT_FEATURE],
70
62
  },
71
63
  "golden-image": {
72
64
  name: "golden-image",
@@ -9,6 +9,7 @@
9
9
  * Frontend services: api-client, env-config, api-index (enabled by default)
10
10
  * Backend services: prisma (enabled by default)
11
11
  */
12
+ import { BRING_YOUR_OWN_TYPE } from "./constants.js";
12
13
  export const RESOURCE_FEATURES = {
13
14
  // ============================================================================
14
15
  // FRONTEND GENERATORS
@@ -161,7 +162,7 @@ export const RESOURCE_FEATURES = {
161
162
  */
162
163
  export function getDefaultFeaturesForResourceType(appType) {
163
164
  // bring-your-own doesn't generate application code, so no features
164
- if (appType === "bring-your-own") {
165
+ if (appType === BRING_YOUR_OWN_TYPE) {
165
166
  return [];
166
167
  }
167
168
  const defaults = [];