@tdk-landscape/tdk-cli-core 1.3.51 → 1.3.53

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 (93) hide show
  1. package/.github/ISSUE_TEMPLATE/config.yml +3 -0
  2. package/.github/ISSUE_TEMPLATE/premium_license.yml +43 -0
  3. package/.github/workflows/ci.yml +13 -0
  4. package/.github/workflows/release-binaries.yml +76 -1
  5. package/CHANGELOG.md +55 -0
  6. package/README.md +52 -17
  7. package/cli/README.md +16 -43
  8. package/cli/biome.json +0 -4
  9. package/cli/dist/commands/config.js +1 -1
  10. package/cli/dist/commands/config.js.map +1 -1
  11. package/cli/dist/commands/help.d.ts.map +1 -1
  12. package/cli/dist/commands/help.js +22 -39
  13. package/cli/dist/commands/help.js.map +1 -1
  14. package/cli/dist/commands/project.d.ts +6 -0
  15. package/cli/dist/commands/project.d.ts.map +1 -1
  16. package/cli/dist/commands/project.js +16 -0
  17. package/cli/dist/commands/project.js.map +1 -1
  18. package/cli/dist/commands/resource.js +1 -1
  19. package/cli/dist/commands/resource.js.map +1 -1
  20. package/cli/dist/commands/up.d.ts.map +1 -1
  21. package/cli/dist/commands/up.js +5 -0
  22. package/cli/dist/commands/up.js.map +1 -1
  23. package/cli/dist/commands/upgrade.d.ts +3 -0
  24. package/cli/dist/commands/upgrade.d.ts.map +1 -1
  25. package/cli/dist/commands/upgrade.js +43 -1
  26. package/cli/dist/commands/upgrade.js.map +1 -1
  27. package/cli/dist/utils/constants.d.ts +1 -1
  28. package/cli/dist/utils/constants.d.ts.map +1 -1
  29. package/cli/dist/utils/constants.js +1 -1
  30. package/cli/dist/utils/constants.js.map +1 -1
  31. package/cli/dist/utils/doctor-runtime.js +5 -5
  32. package/cli/dist/utils/doctor-runtime.js.map +1 -1
  33. package/cli/dist/utils/prompt.d.ts.map +1 -1
  34. package/cli/dist/utils/prompt.js +6 -1
  35. package/cli/dist/utils/prompt.js.map +1 -1
  36. package/cli/dist/utils/service-urls.js +1 -1
  37. package/cli/dist/utils/service-urls.js.map +1 -1
  38. package/cli/package.json +2 -1
  39. package/cli/shared-platform-engineering/docker-templates/infisical-entrypoint.sh +2 -2
  40. package/cli/src/commands/__tests__/doctor.test.ts +8 -8
  41. package/cli/src/commands/__tests__/upgrade.test.ts +90 -3
  42. package/cli/src/commands/config.ts +1 -1
  43. package/cli/src/commands/help.ts +22 -39
  44. package/cli/src/commands/project.ts +20 -0
  45. package/cli/src/commands/resource.ts +1 -1
  46. package/cli/src/commands/up.ts +10 -0
  47. package/cli/src/commands/upgrade.ts +52 -1
  48. package/cli/src/utils/constants.ts +1 -1
  49. package/cli/src/utils/doctor-runtime.ts +5 -5
  50. package/cli/src/utils/prompt.ts +8 -1
  51. package/cli/src/utils/service-urls.ts +1 -1
  52. package/discovery/identity-management-backend/service.yaml +2 -2
  53. package/discovery/identity-management-frontend/service.yaml +1 -1
  54. package/discovery/registry.star +3 -1
  55. package/discovery/services/platform/orchestrator/orchestrator-glue-backend/service.yaml +2 -2
  56. package/docs/FEATURES.md +2 -0
  57. package/engine/assets/docker/golden-layers.Dockerfile +18 -18
  58. package/engine/assets/docker/wake-gateway/decide.test.ts +25 -0
  59. package/engine/assets/docker/wake-gateway/decide.ts +19 -0
  60. package/engine/assets/docker/wake-gateway/server.ts +60 -14
  61. package/engine/assets/scripts/mac-m1-resource-monitor.sh +1 -1
  62. package/engine/scripts/debug-container.sh +8 -3
  63. package/engine/spec.master +2 -2
  64. package/engine/topologies/platform/docker/layers/prisma/prisma_build.star +1 -1
  65. package/engine/topologies/tilt/common/utils.star +1 -1
  66. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +53 -11
  67. package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +20 -0
  68. package/install.sh +19 -242
  69. package/openspec/changes/prioritized-cold-start/design.md +5 -1
  70. package/openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md +9 -3
  71. package/openspec/changes/prioritized-cold-start/tasks.md +13 -0
  72. package/package.json +1 -1
  73. package/platform/services/platform/proxy/service.yaml +1 -1
  74. package/scripts/release-binaries.sh +8 -5
  75. package/shared-platform-engineering/docker-templates/infisical-entrypoint.sh +2 -2
  76. package/tests/tilt-engine/test_sablier_defer_start.py +41 -0
  77. package/openspec/changes/archive/2026-04-29-scan-repo-gaps-fix/codebase-audit-report.md +0 -62
  78. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/.openspec.yaml +0 -5
  79. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/design.md +0 -94
  80. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/proposal.md +0 -46
  81. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/ai-slop-cleanup/CLEANUP_REPORT_8.md +0 -174
  82. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/ai-slop-cleanup/CRITICAL_ASSESSMENT_8.md +0 -160
  83. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/circular-deps/CRITICAL_ASSESSMENT_4.md +0 -381
  84. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/dead-code/CRITICAL_ASSESSMENT_3.md +0 -316
  85. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/deduplication/CRITICAL_ASSESSMENT_1.md +0 -290
  86. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/deduplication/IMPLEMENTATION_SUMMARY_1.md +0 -233
  87. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/defensive-cleanup/CRITICAL_ASSESSMENT_6.md +0 -835
  88. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/legacy-cleanup/CRITICAL_ASSESSMENT_7.md +0 -254
  89. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/legacy-cleanup/IMPLEMENTATION_SUMMARY_7.md +0 -187
  90. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/strong-typing/CRITICAL_ASSESSMENT_5.md +0 -269
  91. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/type-consolidation/CRITICAL_ASSESSMENT_2.md +0 -312
  92. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/type-consolidation/IMPLEMENTATION_SUMMARY.md +0 -170
  93. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/tasks.md +0 -137
@@ -398,7 +398,7 @@ export const resourceCommand = new Command("resource")
398
398
  if (!granted) {
399
399
  throw new Error(
400
400
  "DDD scaffolding is a Premium feature and requires a license key that grants it. " +
401
- "Set export TDK_LICENSE_KEY=<key> (request a Premium license at https://tdk-landscape.github.io/#waitlist) and try again.",
401
+ "Set export TDK_LICENSE_KEY=<key> (request a Premium license at https://tdk-landscape.github.io/tdk-website/#waitlist) and try again.",
402
402
  );
403
403
  }
404
404
  dddEnabled = true;
@@ -15,6 +15,7 @@ import {
15
15
  stackExists,
16
16
  } from "../utils/services.js";
17
17
  import { buildTiltUpArgs, runTilt } from "../utils/tilt.js";
18
+ import { enableDiscoveredStacks } from "./project.js";
18
19
 
19
20
  export const upCommand = new Command("up")
20
21
  .description("Start all services (optionally filtered by stack)")
@@ -32,6 +33,15 @@ export const upCommand = new Command("up")
32
33
  console.log(chalk.gray(`Refreshed runtime assets: ${copiedAssets.join(", ")}`));
33
34
  }
34
35
 
36
+ const newlyEnabledStacks = enableDiscoveredStacks(projectRoot);
37
+ if (newlyEnabledStacks.length > 0 && !options.quiet) {
38
+ console.log(
39
+ chalk.gray(
40
+ `Added ${newlyEnabledStacks.join(", ")} to the pre_alpha phase in .tdk/project.json`,
41
+ ),
42
+ );
43
+ }
44
+
35
45
  let servicesToStart: Awaited<ReturnType<typeof discoverResources>>;
36
46
  let stackDescription: string;
37
47
  let focusServiceNames: string[] = [];
@@ -1,5 +1,6 @@
1
1
  import { execSync } from "node:child_process";
2
- import { accessSync, constants, existsSync, realpathSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { accessSync, constants, existsSync, readFileSync, realpathSync } from "node:fs";
3
4
  import { dirname, join, resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import chalk from "chalk";
@@ -78,12 +79,48 @@ function binaryAssetName(): string | null {
78
79
  }
79
80
 
80
81
  const ENGINE_ASSET_NAME = "tdk-cli-engine.tar.gz";
82
+ const CHECKSUMS_ASSET_NAME = "checksums.txt";
81
83
 
82
84
  export interface BinaryRelease {
83
85
  tag: string;
84
86
  assetName: string;
85
87
  downloadUrl: string;
86
88
  engineDownloadUrl: string;
89
+ checksumsUrl: string;
90
+ }
91
+
92
+ /** Parses `shasum -a 256` / `sha256sum` output ("<hash> <name>" or "<hash> *<name>"). */
93
+ export function parseChecksums(text: string): Map<string, string> {
94
+ const sums = new Map<string, string>();
95
+ for (const line of text.split("\n")) {
96
+ const match = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/i);
97
+ if (match) sums.set(match[2].trim(), match[1].toLowerCase());
98
+ }
99
+ return sums;
100
+ }
101
+
102
+ /**
103
+ * Throws unless `filePath` hashes to the checksum listed for `assetName`.
104
+ * A missing entry is an error when `required`; otherwise it is skipped, so
105
+ * releases published before an asset was added to checksums.txt (older
106
+ * releases list only the binaries, not the engine tarball) still install.
107
+ */
108
+ function verifyChecksum(
109
+ filePath: string,
110
+ assetName: string,
111
+ sums: Map<string, string>,
112
+ required: boolean,
113
+ ): void {
114
+ const expected = sums.get(assetName);
115
+ if (!expected) {
116
+ if (required) throw new Error(`${CHECKSUMS_ASSET_NAME} has no entry for ${assetName}`);
117
+ logVerbose(`${CHECKSUMS_ASSET_NAME} has no entry for ${assetName}; skipping verification`);
118
+ return;
119
+ }
120
+ const actual = createHash("sha256").update(readFileSync(filePath)).digest("hex");
121
+ if (actual !== expected) {
122
+ throw new Error(`Checksum mismatch for ${assetName} (expected ${expected}, got ${actual})`);
123
+ }
87
124
  }
88
125
 
89
126
  async function getLatestBinaryRelease(): Promise<BinaryRelease | null> {
@@ -103,6 +140,7 @@ async function getLatestBinaryRelease(): Promise<BinaryRelease | null> {
103
140
  assetName,
104
141
  downloadUrl: `https://github.com/${BINARY_RELEASE_REPO}/releases/download/${data.tag_name}/${assetName}`,
105
142
  engineDownloadUrl: `https://github.com/${BINARY_RELEASE_REPO}/releases/download/${data.tag_name}/${ENGINE_ASSET_NAME}`,
143
+ checksumsUrl: `https://github.com/${BINARY_RELEASE_REPO}/releases/download/${data.tag_name}/${CHECKSUMS_ASSET_NAME}`,
106
144
  };
107
145
  } catch (err: unknown) {
108
146
  logVerbose("Binary release lookup error", err);
@@ -158,6 +196,19 @@ export async function upgradeViaBinary(tdkPath: string, release: BinaryRelease):
158
196
  { stdio: "pipe", timeout: 120000 },
159
197
  );
160
198
 
199
+ // Verify both downloads against the release's checksums.txt before
200
+ // touching the installed binary or engine.
201
+ spinner.text = `Verifying checksums (${release.tag})...`;
202
+ const sums = parseChecksums(
203
+ execSync(`curl -fsSL ${JSON.stringify(release.checksumsUrl)}`, {
204
+ encoding: "utf-8",
205
+ stdio: "pipe",
206
+ timeout: 30000,
207
+ }),
208
+ );
209
+ verifyChecksum(tmpPath, release.assetName, sums, true);
210
+ verifyChecksum(engineTmpPath, ENGINE_ASSET_NAME, sums, false);
211
+
161
212
  execSync(`mv ${JSON.stringify(tmpPath)} ${JSON.stringify(tdkPath)}`);
162
213
  execSync(`rm -rf ${JSON.stringify(engineDir)}`);
163
214
  execSync(`mkdir -p ${JSON.stringify(engineDir)}`);
@@ -54,7 +54,7 @@ export const STANDARD_PORTS = {
54
54
 
55
55
  export const QUICKSTART_DOCS_URL = "https://tdk-landscape.github.io/tdk-website/docs/quickstart/";
56
56
 
57
- export const BASE_DOMAIN_DEFAULT = "beauty-crm.localhost";
57
+ export const BASE_DOMAIN_DEFAULT = "tdk-project.localhost";
58
58
  export const APP_SUBDOMAIN_PREFIX = "app";
59
59
  export const API_SUBDOMAIN_PREFIX = "api";
60
60
 
@@ -62,7 +62,7 @@ export function parsePublishedPortHolders(
62
62
  export function isOwnIngressContainer(containerName: string, projectPrefix: string): boolean {
63
63
  const name = containerName.toLowerCase();
64
64
  const prefix = projectPrefix.toLowerCase();
65
- // beauty_crm_traefik, beauty_crm-traefik-1, etc.
65
+ // tdk_project_traefik, tdk_project-traefik-1, etc.
66
66
  return name === `${prefix}_traefik` || name.startsWith(`${prefix}_traefik`);
67
67
  }
68
68
 
@@ -201,14 +201,14 @@ export function summarizeTiltBuildError(error: string): string {
201
201
  }
202
202
 
203
203
  if (
204
- /failed to resolve source metadata for docker\.io\/library\/(beauty-crm-[^:\s]+)/i.test(
204
+ /failed to resolve source metadata for docker\.io\/library\/(tdk-project-[^:\s]+)/i.test(
205
205
  normalized,
206
206
  )
207
207
  ) {
208
208
  const image = normalized.match(
209
- /failed to resolve source metadata for docker\.io\/library\/(beauty-crm-[^:\s]+)/i,
209
+ /failed to resolve source metadata for docker\.io\/library\/(tdk-project-[^:\s]+)/i,
210
210
  )?.[1];
211
- return `base image ${image ?? "beauty-crm-*"} missing locally (golden-layer build not finished or failed)`;
211
+ return `base image ${image ?? "tdk-project-*"} missing locally (golden-layer build not finished or failed)`;
212
212
  }
213
213
 
214
214
  if (
@@ -364,7 +364,7 @@ export function checkPrivateNpmRegistry(
364
364
  return {
365
365
  name: "Private npm registry",
366
366
  didPass: false,
367
- message: `Private npm registry (Verdaccio) is not reachable at ${registryUrl}. Docker ImageBuilds will fail with ConnectionRefused downloading package manifests for @beauty-crm/* and @tdk-landscape/* packages.`,
367
+ message: `Private npm registry (Verdaccio) is not reachable at ${registryUrl}. Docker ImageBuilds will fail with ConnectionRefused downloading package manifests for @tdk-landscape/* and project-scoped packages.`,
368
368
  fix: "Start the registry with `bun run verdaccio:start` (or `docker compose -f docker-compose.verdaccio.yml up -d`), publish packages with `bun run publish:all` / `scripts/build/publish-to-verdaccio.sh`, then `tilt trigger` failed resources or re-run `tdk up`.",
369
369
  };
370
370
  }
@@ -32,7 +32,14 @@ interface MultiSelectPromptOptions<T extends string> extends ChoicePromptOptions
32
32
  async function question(prompt: string): Promise<string> {
33
33
  const rl = createInterface({ input, output });
34
34
  try {
35
- return (await rl.question(prompt)).trim();
35
+ // Without this, a closed stdin (CI, `< /dev/null`, a pipe) leaves the
36
+ // question pending forever and Node exits 0 having done nothing.
37
+ return await new Promise<string>((resolve, reject) => {
38
+ rl.once("close", () =>
39
+ reject(new Error("No input available to answer the prompt. Run this in a terminal.")),
40
+ );
41
+ rl.question(prompt).then((answer) => resolve(answer.trim()), reject);
42
+ });
36
43
  } finally {
37
44
  rl.close();
38
45
  }
@@ -28,7 +28,7 @@ export function getProjectName(): string {
28
28
  }
29
29
  } catch {}
30
30
  }
31
- return "beauty-crm";
31
+ return "tdk-project";
32
32
  }
33
33
 
34
34
  export function resolveSubdomainBases(): { appBase: string; apiBase: string } {
@@ -9,8 +9,8 @@ features:
9
9
  healthCheck: True
10
10
  healthCheckPath: /api/v1/health
11
11
  params:
12
- BASE_URL: http://identity.backend.beauty.local
13
- EMAIL_FROM: noreply@beauty-crm.local
12
+ BASE_URL: http://identity.backend.tdk-project.local
13
+ EMAIL_FROM: noreply@tdk-project.local
14
14
  UPSTASH_REDIS_REST_URL: http://redis:6379
15
15
  path: identity-management-backend
16
16
  playwright: True
@@ -18,7 +18,7 @@ federation:
18
18
  shared:
19
19
  - react
20
20
  - react-dom
21
- - @beauty-crm/introvertic-ui
21
+ - @tdk-landscape/introvertic-ui
22
22
  healthCheck: True
23
23
  path: identity-management-frontend
24
24
  playwright: True
@@ -423,7 +423,9 @@ def _find_manifest_files(root, manifest_filename):
423
423
  full_path = root
424
424
  else:
425
425
  full_path = project_root + "/" + root
426
- cmd = "find " + full_path + " -type f -name '" + manifest_filename + "' 2>/dev/null"
426
+ # A stack listed in project.json may not have a directory yet (a fresh
427
+ # project lists "proxy"); find exits 1 on a missing path and local() fails.
428
+ cmd = "if [ -d '" + full_path + "' ]; then find '" + full_path + "' -type f -name '" + manifest_filename + "' 2>/dev/null; fi"
427
429
  result = str(local(cmd, quiet=True, echo_off=True)).strip()
428
430
 
429
431
  if result:
@@ -2,7 +2,7 @@ appName: orchestrator-glue-backend
2
2
  appType: backend
3
3
  stack: platform
4
4
  port: 5001
5
- databaseName: beauty_crm_orchestrator
5
+ databaseName: tdk_project_orchestrator
6
6
  features:
7
7
  - nats
8
8
  - prisma
@@ -22,5 +22,5 @@ resources:
22
22
  memory: 256Mi
23
23
  runtime: bun
24
24
  traefik:
25
- host: orchestrator.backend.beauty.local
25
+ host: orchestrator.backend.tdk-project.local
26
26
  pathPrefix: /api/orchestrator
package/docs/FEATURES.md CHANGED
@@ -105,6 +105,8 @@ export TDK_LICENSE_KEY=<your-premium-key>
105
105
  tdk up
106
106
  ```
107
107
 
108
+ To get a key, use the [request form on the website](https://tdk-landscape.github.io/tdk-website/#waitlist) or open a [Premium license request](https://github.com/tdk-landscape/tdk-cli-core/issues/new?template=premium_license.yml).
109
+
108
110
  ---
109
111
 
110
112
  ## Resource-Level Features
@@ -7,7 +7,7 @@
7
7
  # 2. Or update service.json
8
8
  #
9
9
  # Generation Source: GoldenImage.generate_layered_dockerfile()
10
- # Service: beauty-crm-l1-l4
10
+ # Service: tdk-project-l1-l4
11
11
  # Type: GOLDEN LAYERED IMAGES | L1-L4 Pre-built Layers
12
12
  ###############################################################################
13
13
 
@@ -15,14 +15,14 @@
15
15
  # 🏗️ GOLDEN LAYERED IMAGES - Multi-Stage Base Images
16
16
  # =============================================================================
17
17
  # This Dockerfile builds 8 golden images:
18
- # - beauty-crm-l1:latest - OS base + Bun runtime
19
- # - beauty-crm-l2:latest - Dependencies installed
20
- # - beauty-crm-l3-backend:latest - Backend build tools (Prisma, no Infisical CLI)
21
- # - beauty-crm-l3-frontend:latest - Frontend build tools (no Prisma)
22
- # - beauty-crm-l3-migrator:latest - Migrator build tools (Prisma, no Infisical CLI)
23
- # - beauty-crm-l4-backend:latest - Backend production runtime
24
- # - beauty-crm-l4-frontend:latest - Frontend production runtime (Nginx)
25
- # - beauty-crm-l4-migrator:latest - Migrator production runtime
18
+ # - tdk-project-l1:latest - OS base + Bun runtime
19
+ # - tdk-project-l2:latest - Dependencies installed
20
+ # - tdk-project-l3-backend:latest - Backend build tools (Prisma, no Infisical CLI)
21
+ # - tdk-project-l3-frontend:latest - Frontend build tools (no Prisma)
22
+ # - tdk-project-l3-migrator:latest - Migrator build tools (Prisma, no Infisical CLI)
23
+ # - tdk-project-l4-backend:latest - Backend production runtime
24
+ # - tdk-project-l4-frontend:latest - Frontend production runtime (Nginx)
25
+ # - tdk-project-l4-migrator:latest - Migrator production runtime
26
26
  #
27
27
  # Services use these as base images to skip heavy dependency installation.
28
28
  #
@@ -38,7 +38,7 @@
38
38
  # =============================================================================
39
39
  FROM oven/bun:1.3.11-alpine AS l1_golden
40
40
 
41
- LABEL layer="l1" description="OS base with runtime environment" maintainer="beauty-crm"
41
+ LABEL layer="l1" description="OS base with runtime environment" maintainer="tdk-project"
42
42
 
43
43
  # System dependencies for all services
44
44
  RUN apk add --no-cache ca-certificates tzdata curl file bash openssl
@@ -57,7 +57,7 @@ WORKDIR /app
57
57
  # =============================================================================
58
58
  FROM l1_golden AS l2_golden
59
59
 
60
- LABEL layer="l2" description="Base with common dependencies installed" maintainer="beauty-crm"
60
+ LABEL layer="l2" description="Base with common dependencies installed" maintainer="tdk-project"
61
61
 
62
62
  # Install minimal debugging tools (removed: vim, python3, make, g++ = -67MB)
63
63
  RUN apk add --no-cache curl netcat-openbsd jq bind-tools
@@ -74,7 +74,7 @@ RUN echo "Bun version: $(bun --version)"
74
74
  # =============================================================================
75
75
  FROM l2_golden AS l3_backend_golden
76
76
 
77
- LABEL layer="l3-backend" description="Backend build tools with Prisma" maintainer="beauty-crm"
77
+ LABEL layer="l3-backend" description="Backend build tools with Prisma" maintainer="tdk-project"
78
78
 
79
79
  # Install Prisma CLI globally (pinned for deterministic builds)
80
80
  RUN bun add -g prisma@7.5.0 @prisma/client@7.5.0
@@ -94,7 +94,7 @@ RUN echo "Prisma version: $(bunx prisma --version | head -1)"
94
94
  # =============================================================================
95
95
  FROM l2_golden AS l3_frontend_golden
96
96
 
97
- LABEL layer="l3-frontend" description="Frontend build tools without Prisma" maintainer="beauty-crm"
97
+ LABEL layer="l3-frontend" description="Frontend build tools without Prisma" maintainer="tdk-project"
98
98
 
99
99
  # Frontend doesn't need Prisma, just verify Bun
100
100
  RUN echo "Bun version: $(bun --version)"
@@ -105,7 +105,7 @@ RUN echo "Bun version: $(bun --version)"
105
105
  # =============================================================================
106
106
  FROM l2_golden AS l3_migrator_golden
107
107
 
108
- LABEL layer="l3-migrator" description="Migrator build tools with Prisma" maintainer="beauty-crm"
108
+ LABEL layer="l3-migrator" description="Migrator build tools with Prisma" maintainer="tdk-project"
109
109
 
110
110
  # Install Prisma CLI globally (required for migrations, pinned)
111
111
  RUN bun add -g prisma@7.5.0 @prisma/client@7.5.0
@@ -125,7 +125,7 @@ RUN echo "Prisma version: $(bunx prisma --version | head -1)"
125
125
  # =============================================================================
126
126
  FROM l1_golden AS l4_backend_bun
127
127
 
128
- LABEL layer="l4-backend-bun" description="Backend production runtime (Bun)" maintainer="beauty-crm"
128
+ LABEL layer="l4-backend-bun" description="Backend production runtime (Bun)" maintainer="tdk-project"
129
129
 
130
130
  ENV NODE_ENV=production
131
131
 
@@ -150,7 +150,7 @@ WORKDIR /app
150
150
  # =============================================================================
151
151
  FROM node:22-alpine AS l4_backend_node
152
152
 
153
- LABEL layer="l4-backend-node" description="Backend production runtime (Node.js - lightweight)" maintainer="beauty-crm"
153
+ LABEL layer="l4-backend-node" description="Backend production runtime (Node.js - lightweight)" maintainer="tdk-project"
154
154
 
155
155
  ENV NODE_ENV=production
156
156
 
@@ -180,7 +180,7 @@ FROM l4_backend_bun AS l4_backend_golden
180
180
  # =============================================================================
181
181
  FROM nginx:alpine AS l4_frontend_golden
182
182
 
183
- LABEL layer="l4-frontend" description="Frontend production runtime with Nginx" maintainer="beauty-crm"
183
+ LABEL layer="l4-frontend" description="Frontend production runtime with Nginx" maintainer="tdk-project"
184
184
 
185
185
  ENV NODE_ENV=production
186
186
 
@@ -198,7 +198,7 @@ WORKDIR /usr/share/nginx/html
198
198
  # =============================================================================
199
199
  FROM l1_golden AS l4_migrator_golden
200
200
 
201
- LABEL layer="l4-migrator" description="Migrator runtime with Prisma" maintainer="beauty-crm"
201
+ LABEL layer="l4-migrator" description="Migrator runtime with Prisma" maintainer="tdk-project"
202
202
 
203
203
  ENV NODE_ENV=production
204
204
 
@@ -1,8 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  COMPOSE_START_TIMEOUT_MS,
4
+ EARLY_RESPONSE_MS,
5
+ MAX_EARLY_RESPONSE_MS,
6
+ RETRY_AFTER_SECONDS,
4
7
  TILT_TRIGGER_TIMEOUT_MS,
5
8
  decideWakeAction,
9
+ earlyResponseMs,
6
10
  evaluateReadiness,
7
11
  wakeTimeoutMs,
8
12
  } from "./decide";
@@ -112,6 +116,27 @@ describe("wakeTimeoutMs", () => {
112
116
  });
113
117
  });
114
118
 
119
+ describe("earlyResponseMs", () => {
120
+ test("holds a waking request for EARLY_RESPONSE_MS by default", () => {
121
+ expect(earlyResponseMs()).toBe(EARLY_RESPONSE_MS);
122
+ });
123
+
124
+ test("the default answers well before Bun's 255s idle cap drops the connection", () => {
125
+ expect(EARLY_RESPONSE_MS).toBeLessThan(255_000);
126
+ });
127
+
128
+ test("an explicit X-Wake-Respond-Within-Ms wins, but is capped under Bun's idle limit", () => {
129
+ expect(earlyResponseMs(5_000)).toBe(5_000);
130
+ expect(earlyResponseMs(600_000)).toBe(MAX_EARLY_RESPONSE_MS);
131
+ expect(MAX_EARLY_RESPONSE_MS).toBeLessThan(255_000);
132
+ });
133
+
134
+ test("tells the caller to retry soon", () => {
135
+ expect(RETRY_AFTER_SECONDS).toBeGreaterThan(0);
136
+ expect(RETRY_AFTER_SECONDS * 1000).toBeLessThan(EARLY_RESPONSE_MS);
137
+ });
138
+ });
139
+
115
140
  describe("evaluateReadiness", () => {
116
141
  test("ready once every required name reports healthy", () => {
117
142
  const outcome = evaluateReadiness(["a", "b"], { a: true, b: true });
@@ -84,6 +84,25 @@ export function wakeTimeoutMs(startedViaCompose: boolean, overrideMs?: number):
84
84
  return startedViaCompose ? COMPOSE_START_TIMEOUT_MS : TILT_TRIGGER_TIMEOUT_MS;
85
85
  }
86
86
 
87
+ /**
88
+ * How long a waking request is held before the gateway answers
89
+ * `503` + `Retry-After` while the start keeps going in the background.
90
+ * Found live (task 7.2): during a full `tdk up` bring-up the Docker daemon
91
+ * took ~5 min to start one container, and Bun.serve's 255s idle cap dropped
92
+ * the held connection with no response at all.
93
+ */
94
+ export const EARLY_RESPONSE_MS = 60_000;
95
+ /** Largest hold allowed: stays under Bun.serve's 255s idle cap. */
96
+ export const MAX_EARLY_RESPONSE_MS = 240_000;
97
+ /** The `Retry-After` value sent with the early 503. */
98
+ export const RETRY_AFTER_SECONDS = 10;
99
+
100
+ /** How long to hold a request: an explicit X-Wake-Respond-Within-Ms wins, capped under Bun's idle limit. */
101
+ export function earlyResponseMs(overrideMs?: number): number {
102
+ if (overrideMs && overrideMs > 0) return Math.min(overrideMs, MAX_EARLY_RESPONSE_MS);
103
+ return EARLY_RESPONSE_MS;
104
+ }
105
+
87
106
  export interface WaitOutcome {
88
107
  ready: boolean;
89
108
  /** Names still not running/healthy when the wait ended (empty when ready). */
@@ -32,9 +32,14 @@
32
32
  * X-Wake-Image The image the compose service declares
33
33
  * (<service>_<resource>:dev).
34
34
  * X-Wake-Timeout-Ms, X-Wake-Proxy-Port, X-Wake-Session-Duration
35
+ * X-Wake-Respond-Within-Ms How long to hold the request before answering
36
+ * 503 + Retry-After while the start continues in the
37
+ * background (default decide.ts EARLY_RESPONSE_MS).
35
38
  */
36
39
  import {
40
+ RETRY_AFTER_SECONDS,
37
41
  decideWakeAction,
42
+ earlyResponseMs,
38
43
  evaluateReadiness,
39
44
  wakeTimeoutMs,
40
45
  type ContainerState,
@@ -168,6 +173,7 @@ function parseRequest(req: Request) {
168
173
  const container = req.headers.get("X-Wake-Container") || resource;
169
174
  const deps = parseDeps(req.headers.get("X-Wake-Deps") || "");
170
175
  const timeoutOverrideMs = Number(req.headers.get("X-Wake-Timeout-Ms")) || undefined;
176
+ const respondWithinOverrideMs = Number(req.headers.get("X-Wake-Respond-Within-Ms")) || undefined;
171
177
  const proxyPort = req.headers.get("X-Wake-Proxy-Port") || "80";
172
178
  const sessionDuration = req.headers.get("X-Wake-Session-Duration") || "10m";
173
179
  const composeFiles = (req.headers.get("X-Wake-Compose-Files") || "").split(",").filter(Boolean);
@@ -178,16 +184,62 @@ function parseRequest(req: Request) {
178
184
  image: req.headers.get("X-Wake-Image") || "",
179
185
  };
180
186
  const hasCompose = Boolean(compose.project && compose.files.length && compose.image);
181
- return { resource, container, deps, timeoutOverrideMs, proxyPort, sessionDuration, compose, hasCompose };
187
+ return {
188
+ resource,
189
+ container,
190
+ deps,
191
+ timeoutOverrideMs,
192
+ respondWithinOverrideMs,
193
+ proxyPort,
194
+ sessionDuration,
195
+ compose,
196
+ hasCompose,
197
+ };
198
+ }
199
+
200
+ type WakeParams = ReturnType<typeof parseRequest>;
201
+ type WakeOutcome = { ready: true } | { ready: false; timeoutMs: number };
202
+
203
+ // One wake per resource at a time: a caller that got an early 503 and retries,
204
+ // or a second caller arriving meanwhile, joins the wake already in progress
205
+ // instead of starting the container again.
206
+ const inflight = new Map<string, Promise<WakeOutcome>>();
207
+
208
+ function wakeOnce(params: WakeParams): Promise<WakeOutcome> {
209
+ let wake = inflight.get(params.resource);
210
+ if (!wake) {
211
+ wake = runWake(params).finally(() => inflight.delete(params.resource));
212
+ inflight.set(params.resource, wake);
213
+ }
214
+ return wake;
182
215
  }
183
216
 
184
217
  async function handleWake(req: Request): Promise<Response> {
185
- const { resource, container, deps, timeoutOverrideMs, proxyPort, sessionDuration, compose, hasCompose } =
186
- parseRequest(req);
187
- if (!resource) {
218
+ const params = parseRequest(req);
219
+ if (!params.resource) {
188
220
  return new Response("wake gateway: missing X-Wake-Resource header (set by the generated Traefik static route)", { status: 400 });
189
221
  }
190
222
 
223
+ const holdMs = earlyResponseMs(params.respondWithinOverrideMs);
224
+ const outcome = await Promise.race([wakeOnce(params), Bun.sleep(holdMs).then(() => null)]);
225
+ if (outcome === null) {
226
+ console.log(`wake ${params.resource}: still starting after ${holdMs}ms, answering 503`);
227
+ return new Response(
228
+ `wake gateway: ${params.resource} is starting; retry in ${RETRY_AFTER_SECONDS}s`,
229
+ { status: 503, headers: { "Retry-After": String(RETRY_AFTER_SECONDS) } },
230
+ );
231
+ }
232
+ if (outcome.ready) {
233
+ return proxyThrough(req, params.container, params.proxyPort);
234
+ }
235
+ return new Response(
236
+ `wake gateway: timed out after ${outcome.timeoutMs}ms waiting for ${params.resource} and its dependencies to become healthy`,
237
+ { status: 504 },
238
+ );
239
+ }
240
+
241
+ async function runWake(params: WakeParams): Promise<WakeOutcome> {
242
+ const { resource, container, deps, timeoutOverrideMs, sessionDuration, compose, hasCompose } = params;
191
243
  const resourceState = await containerState(container);
192
244
  const dependencies = await Promise.all(
193
245
  deps.map(async (dep) => ({ name: dep.resourceName, state: await containerState(dep.containerName) })),
@@ -225,8 +277,6 @@ async function handleWake(req: Request): Promise<Response> {
225
277
  ]);
226
278
 
227
279
  const requiredNames = [resource, ...deps.map((d) => d.resourceName)];
228
- const containerByName: Record<string, string> = { [resource]: container };
229
- for (const dep of deps) containerByName[dep.resourceName] = dep.containerName;
230
280
 
231
281
  while (Date.now() < deadline) {
232
282
  const healthy: Record<string, boolean> = {};
@@ -236,17 +286,12 @@ async function handleWake(req: Request): Promise<Response> {
236
286
  for (const dep of deps) {
237
287
  healthy[dep.resourceName] = await isHealthy(dep.containerName);
238
288
  }
239
- const outcome = evaluateReadiness(requiredNames, healthy);
240
- if (outcome.ready) {
241
- return proxyThrough(req, container, proxyPort);
289
+ if (evaluateReadiness(requiredNames, healthy).ready) {
290
+ return { ready: true };
242
291
  }
243
292
  await Bun.sleep(POLL_INTERVAL_MS);
244
293
  }
245
-
246
- return new Response(
247
- `wake gateway: timed out after ${timeoutMs}ms waiting for ${resource} and its dependencies to become healthy`,
248
- { status: 504 },
249
- );
294
+ return { ready: false, timeoutMs };
250
295
  }
251
296
 
252
297
  // Headers that must not be forwarded verbatim to the upstream fetch: Host
@@ -258,6 +303,7 @@ const STRIPPED_REQUEST_HEADERS = new Set([
258
303
  "x-wake-container",
259
304
  "x-wake-deps",
260
305
  "x-wake-timeout-ms",
306
+ "x-wake-respond-within-ms",
261
307
  "x-wake-proxy-port",
262
308
  "x-wake-session-duration",
263
309
  "x-wake-compose-project",
@@ -84,7 +84,7 @@ while IFS= read -r container; do
84
84
 
85
85
  printf "%-20s\t%s%%\t%s\t%s\n" "$CONTAINER_DISPLAY" "$CPU_PERCENT" "$MEMORY_USAGE" "$STATUS"
86
86
  fi
87
- done <<< "$BEAUTY_CONTAINERS"
87
+ done <<< "$PROJECT_CONTAINERS"
88
88
 
89
89
  echo ""
90
90
  echo -e "${BLUE}📈 Resource Summary:${NC}"
@@ -13,6 +13,11 @@ YELLOW='\033[1;33m'
13
13
  BLUE='\033[0;34m'
14
14
  NC='\033[0m' # No Color
15
15
 
16
+ # Container name filter: project name from .tdk/project.json (override with TDK_PROJECT_NAME)
17
+ PROJECT_FILTER="${TDK_PROJECT_NAME:-$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' .tdk/project.json 2>/dev/null | head -1)}"
18
+ PROJECT_FILTER="${PROJECT_FILTER:-tdk-project}"
19
+ PROJECT_FILTER="${PROJECT_FILTER//-/[-_]}"
20
+
16
21
  # Help text
17
22
  show_help() {
18
23
  cat << EOF
@@ -68,11 +73,11 @@ EOF
68
73
  # List containers
69
74
  list_containers() {
70
75
  echo -e "${GREEN}=== Running Containers ===${NC}"
71
- docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" | grep -i "beauty"
76
+ docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" | grep -iE "$PROJECT_FILTER"
72
77
 
73
78
  echo ""
74
79
  echo -e "${YELLOW}=== Recently Exited Containers ===${NC}"
75
- docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" | grep -i "beauty" | grep -i "exited"
80
+ docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" | grep -iE "$PROJECT_FILTER" | grep -i "exited"
76
81
  }
77
82
 
78
83
  # Get container ID from service name
@@ -122,7 +127,7 @@ run_command() {
122
127
  # Show failed containers
123
128
  show_failed() {
124
129
  echo -e "${RED}=== Failed/Exited Containers ===${NC}"
125
- docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" | grep -i "beauty" | grep -iE "(exited|error)"
130
+ docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" | grep -iE "$PROJECT_FILTER" | grep -iE "(exited|error)"
126
131
 
127
132
  echo ""
128
133
  echo -e "${YELLOW}To inspect logs of a failed container:${NC}"
@@ -62,7 +62,7 @@
62
62
 
63
63
  # PROJECT_ROOT is automatically set by Tiltfile based on where you run `tilt up`.
64
64
  # You don't need to set it. Example:
65
- # - If you run in /home/dev/beauty-crm, PROJECT_ROOT = "/home/dev/beauty-crm"
65
+ # - If you run in /home/dev/tdk-project, PROJECT_ROOT = "/home/dev/tdk-project"
66
66
  # - All other paths are built from this root.
67
67
  #
68
68
  # This file is manually maintained to match the directory structure.
@@ -563,7 +563,7 @@ FEATURES = struct(
563
563
 
564
564
  CONSTANTS = struct(
565
565
  # System identity - EXPLICIT
566
- SYSTEM_NAME = "beauty-crm-tilt-platform",
566
+ SYSTEM_NAME = "tdk-project-tilt-platform",
567
567
  SYSTEM_VERSION = "2.0.0",
568
568
 
569
569
  # Runtimes - EXPLICIT supported options
@@ -110,7 +110,7 @@ def L3_generate_migration_engine(res_path, use_golden = True):
110
110
  + "COPY " + res_path_rel + "/" + OUTPUT.AUTOGENERATED_FOLDER + "/bunfig.docker.autogenerated.toml ./bunfig.toml\n"
111
111
  + "COPY bun.lock* ./\n"
112
112
  + "# Install runtime deps only for migrator image (skip test/dev toolchain)\n"
113
- + "# NOTE: keep bunfig.toml (it carries the @beauty-crm/@tdk-landscape registry\n"
113
+ + "# NOTE: keep bunfig.toml (it carries the project/@tdk-landscape registry\n"
114
114
  + "# scopes pointing at Verdaccio) - deleting it forces scoped packages to fall\n"
115
115
  + "# back to the public npm registry and fail with ConnectionRefused/404.\n"
116
116
  + "RUN --mount=type=cache,target=/cache/bun,sharing=shared rm -f bun.lock bun.lockb && env -u CI BUN_CONFIG_FROZEN_LOCKFILE=false BUN_TMPDIR=/tmp BUN_INSTALL=/tmp/bun-install bun install --production --no-frozen-lockfile --concurrent-scripts --cache-dir=/cache/bun\n"
@@ -60,7 +60,7 @@ MIGRATED_PACKAGES = {
60
60
  load("../manifest/constants.star", "MANIFEST_FILENAME")
61
61
 
62
62
  # List of all shared platform networks
63
- # All networks use PROJECT_NAME format (e.g., beauty_crm_*)
63
+ # All networks use PROJECT_NAME format (e.g., tdk_project_*)
64
64
  PLATFORM_NETWORKS = [
65
65
  PlatformDockerConstants.NETWORK_TRAEFIK_PUBLIC,
66
66
  PlatformDockerConstants.PROJECT_NAME + "_traefik-private",