@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.
- package/.github/ISSUE_TEMPLATE/config.yml +3 -0
- package/.github/ISSUE_TEMPLATE/premium_license.yml +43 -0
- package/.github/workflows/ci.yml +13 -0
- package/.github/workflows/release-binaries.yml +76 -1
- package/CHANGELOG.md +55 -0
- package/README.md +52 -17
- package/cli/README.md +16 -43
- package/cli/biome.json +0 -4
- package/cli/dist/commands/config.js +1 -1
- package/cli/dist/commands/config.js.map +1 -1
- package/cli/dist/commands/help.d.ts.map +1 -1
- package/cli/dist/commands/help.js +22 -39
- package/cli/dist/commands/help.js.map +1 -1
- package/cli/dist/commands/project.d.ts +6 -0
- package/cli/dist/commands/project.d.ts.map +1 -1
- package/cli/dist/commands/project.js +16 -0
- package/cli/dist/commands/project.js.map +1 -1
- package/cli/dist/commands/resource.js +1 -1
- package/cli/dist/commands/resource.js.map +1 -1
- package/cli/dist/commands/up.d.ts.map +1 -1
- package/cli/dist/commands/up.js +5 -0
- package/cli/dist/commands/up.js.map +1 -1
- package/cli/dist/commands/upgrade.d.ts +3 -0
- package/cli/dist/commands/upgrade.d.ts.map +1 -1
- package/cli/dist/commands/upgrade.js +43 -1
- package/cli/dist/commands/upgrade.js.map +1 -1
- package/cli/dist/utils/constants.d.ts +1 -1
- package/cli/dist/utils/constants.d.ts.map +1 -1
- package/cli/dist/utils/constants.js +1 -1
- package/cli/dist/utils/constants.js.map +1 -1
- package/cli/dist/utils/doctor-runtime.js +5 -5
- package/cli/dist/utils/doctor-runtime.js.map +1 -1
- package/cli/dist/utils/prompt.d.ts.map +1 -1
- package/cli/dist/utils/prompt.js +6 -1
- package/cli/dist/utils/prompt.js.map +1 -1
- package/cli/dist/utils/service-urls.js +1 -1
- package/cli/dist/utils/service-urls.js.map +1 -1
- package/cli/package.json +2 -1
- package/cli/shared-platform-engineering/docker-templates/infisical-entrypoint.sh +2 -2
- package/cli/src/commands/__tests__/doctor.test.ts +8 -8
- package/cli/src/commands/__tests__/upgrade.test.ts +90 -3
- package/cli/src/commands/config.ts +1 -1
- package/cli/src/commands/help.ts +22 -39
- package/cli/src/commands/project.ts +20 -0
- package/cli/src/commands/resource.ts +1 -1
- package/cli/src/commands/up.ts +10 -0
- package/cli/src/commands/upgrade.ts +52 -1
- package/cli/src/utils/constants.ts +1 -1
- package/cli/src/utils/doctor-runtime.ts +5 -5
- package/cli/src/utils/prompt.ts +8 -1
- package/cli/src/utils/service-urls.ts +1 -1
- package/discovery/identity-management-backend/service.yaml +2 -2
- package/discovery/identity-management-frontend/service.yaml +1 -1
- package/discovery/registry.star +3 -1
- package/discovery/services/platform/orchestrator/orchestrator-glue-backend/service.yaml +2 -2
- package/docs/FEATURES.md +2 -0
- package/engine/assets/docker/golden-layers.Dockerfile +18 -18
- package/engine/assets/docker/wake-gateway/decide.test.ts +25 -0
- package/engine/assets/docker/wake-gateway/decide.ts +19 -0
- package/engine/assets/docker/wake-gateway/server.ts +60 -14
- package/engine/assets/scripts/mac-m1-resource-monitor.sh +1 -1
- package/engine/scripts/debug-container.sh +8 -3
- package/engine/spec.master +2 -2
- package/engine/topologies/platform/docker/layers/prisma/prisma_build.star +1 -1
- package/engine/topologies/tilt/common/utils.star +1 -1
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +53 -11
- package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +20 -0
- package/install.sh +19 -242
- package/openspec/changes/prioritized-cold-start/design.md +5 -1
- package/openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md +9 -3
- package/openspec/changes/prioritized-cold-start/tasks.md +13 -0
- package/package.json +1 -1
- package/platform/services/platform/proxy/service.yaml +1 -1
- package/scripts/release-binaries.sh +8 -5
- package/shared-platform-engineering/docker-templates/infisical-entrypoint.sh +2 -2
- package/tests/tilt-engine/test_sablier_defer_start.py +41 -0
- package/openspec/changes/archive/2026-04-29-scan-repo-gaps-fix/codebase-audit-report.md +0 -62
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/.openspec.yaml +0 -5
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/design.md +0 -94
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/proposal.md +0 -46
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/ai-slop-cleanup/CLEANUP_REPORT_8.md +0 -174
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/ai-slop-cleanup/CRITICAL_ASSESSMENT_8.md +0 -160
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/circular-deps/CRITICAL_ASSESSMENT_4.md +0 -381
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/dead-code/CRITICAL_ASSESSMENT_3.md +0 -316
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/deduplication/CRITICAL_ASSESSMENT_1.md +0 -290
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/deduplication/IMPLEMENTATION_SUMMARY_1.md +0 -233
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/defensive-cleanup/CRITICAL_ASSESSMENT_6.md +0 -835
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/legacy-cleanup/CRITICAL_ASSESSMENT_7.md +0 -254
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/legacy-cleanup/IMPLEMENTATION_SUMMARY_7.md +0 -187
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/strong-typing/CRITICAL_ASSESSMENT_5.md +0 -269
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/type-consolidation/CRITICAL_ASSESSMENT_2.md +0 -312
- package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/type-consolidation/IMPLEMENTATION_SUMMARY.md +0 -170
- 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;
|
package/cli/src/commands/up.ts
CHANGED
|
@@ -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 {
|
|
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 = "
|
|
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
|
-
//
|
|
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\/(
|
|
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\/(
|
|
209
|
+
/failed to resolve source metadata for docker\.io\/library\/(tdk-project-[^:\s]+)/i,
|
|
210
210
|
)?.[1];
|
|
211
|
-
return `base image ${image ?? "
|
|
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 @
|
|
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
|
}
|
package/cli/src/utils/prompt.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -9,8 +9,8 @@ features:
|
|
|
9
9
|
healthCheck: True
|
|
10
10
|
healthCheckPath: /api/v1/health
|
|
11
11
|
params:
|
|
12
|
-
BASE_URL: http://identity.backend.
|
|
13
|
-
EMAIL_FROM: noreply@
|
|
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
|
package/discovery/registry.star
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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
|
-
# -
|
|
19
|
-
# -
|
|
20
|
-
# -
|
|
21
|
-
# -
|
|
22
|
-
# -
|
|
23
|
-
# -
|
|
24
|
-
# -
|
|
25
|
-
# -
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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 {
|
|
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
|
|
186
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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 <<< "$
|
|
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 -
|
|
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 -
|
|
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 -
|
|
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}"
|
package/engine/spec.master
CHANGED
|
@@ -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/
|
|
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 = "
|
|
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
|
|
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.,
|
|
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",
|