@tdk-landscape/tdk-cli-core 1.3.120 → 1.3.122

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 (56) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/README.md +21 -14
  3. package/cli/dist/cli.js +1 -1
  4. package/cli/dist/commands/config.js +6 -2
  5. package/cli/dist/commands/doctor.js +64 -7
  6. package/cli/dist/commands/help.js +17 -1
  7. package/cli/dist/commands/logs.js +34 -7
  8. package/cli/dist/commands/networks.js +1 -1
  9. package/cli/dist/commands/resources.js +1 -1
  10. package/cli/dist/commands/stack.js +94 -22
  11. package/cli/dist/commands/status.js +35 -2
  12. package/cli/dist/commands/ui.js +105 -9
  13. package/cli/dist/commands/up.js +105 -28
  14. package/cli/dist/components/ServiceIssues.js +12 -0
  15. package/cli/dist/components/TabBar.js +3 -2
  16. package/cli/dist/components/index.js +1 -0
  17. package/cli/dist/generator/__tests__/golden-helpers.js +42 -0
  18. package/cli/dist/generator/template-engine.js +5 -3
  19. package/cli/dist/utils/atomic-write.js +35 -0
  20. package/cli/dist/utils/bun-floor.js +8 -0
  21. package/cli/dist/utils/cold-preflight.js +6 -4
  22. package/cli/dist/utils/doctor-runtime.js +6 -6
  23. package/cli/dist/utils/doctor-wiring.js +62 -1
  24. package/cli/dist/utils/errors.js +20 -2
  25. package/cli/dist/utils/host-port-plan.js +3 -0
  26. package/cli/dist/utils/machine-output.js +8 -1
  27. package/cli/dist/utils/port-conflict-fix.js +24 -0
  28. package/cli/dist/utils/port-fallback-notice.js +18 -0
  29. package/cli/dist/utils/port-listener-probe.js +19 -0
  30. package/cli/dist/utils/secret-redaction.js +55 -0
  31. package/cli/dist/utils/service-config-checks.js +188 -0
  32. package/cli/dist/utils/service-runtime-state.js +67 -0
  33. package/cli/dist/utils/services.js +2 -15
  34. package/cli/dist/utils/single-flight.js +20 -0
  35. package/cli/dist/utils/stack-status.js +15 -0
  36. package/cli/dist/utils/startup-report.js +73 -0
  37. package/cli/dist/utils/suggestions.js +58 -0
  38. package/cli/dist/utils/tilt-events.js +195 -0
  39. package/cli/dist/utils/tilt-resource-state.js +12 -0
  40. package/cli/dist/utils/tilt-startup.js +7 -0
  41. package/cli/dist/utils/tilt-unreachable-message.js +8 -0
  42. package/cli/dist/utils/ui-service-state.js +35 -0
  43. package/cli/dist/utils/up-readiness.js +2 -0
  44. package/cli/package.json +1 -1
  45. package/cli/templates/.tiltignore.hbs +1 -2
  46. package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -1
  47. package/cli/templates/TILT_TECH_STACK.star.hbs +5 -5
  48. package/cli/templates/Tiltfile.hbs +3 -3
  49. package/cli/templates/spec.master.hbs +1 -1
  50. package/engine/topologies/platform/docker/compose/compose.star +1 -86
  51. package/engine/topologies/platform/docker/compose/traefik_standalone.star +4 -4
  52. package/engine/topologies/platform/docker/networking/traefik.star +40 -7
  53. package/engine/topologies/platform/docker/networking/traefik_helpers.star +1 -1
  54. package/engine/topologies/tilt/resources/infra-loader.star +3 -2
  55. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +20 -0
  56. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ import { parseTiltResourceFailures, summarizeTiltBuildError } from "./doctor-runtime.js";
2
+ import { portConflictFix } from "./port-conflict-fix.js";
3
+ import { isTiltResourcePending } from "./tilt-resource-state.js";
4
+ import { onlyEnabledResources } from "./up-readiness.js";
5
+ export function transitiveDependencies(name, dependsOn) {
6
+ const seen = new Set();
7
+ const visit = (current) => {
8
+ for (const dep of dependsOn[current] ?? []) {
9
+ if (seen.has(dep))
10
+ continue;
11
+ seen.add(dep);
12
+ visit(dep);
13
+ }
14
+ };
15
+ visit(name);
16
+ return seen;
17
+ }
18
+ function isPending(item) {
19
+ return isTiltResourcePending(item.status?.updateStatus ?? "", item.status?.runtimeStatus ?? "");
20
+ }
21
+ export function buildStartupReport(jsonText, dependsOn, deferred = new Set()) {
22
+ const enabledJson = onlyEnabledResources(jsonText);
23
+ const failures = parseTiltResourceFailures(enabledJson, deferred).failures;
24
+ const failed = failures.map((failure) => ({
25
+ name: failure.name,
26
+ message: failure.error
27
+ ? summarizeTiltBuildError(failure.error)
28
+ : `${failure.updateStatus}/${failure.runtimeStatus}`,
29
+ }));
30
+ const failedNames = new Set(failed.map((entry) => entry.name));
31
+ const items = JSON.parse(enabledJson).items ?? [];
32
+ const blocked = [];
33
+ const starting = [];
34
+ for (const item of items) {
35
+ const name = item.metadata?.name ?? "unknown";
36
+ if (name === "(Tiltfile)" || failedNames.has(name) || deferred.has(name) || !isPending(item)) {
37
+ continue;
38
+ }
39
+ const because = [...transitiveDependencies(name, dependsOn)]
40
+ .filter((dep) => failedNames.has(dep))
41
+ .sort();
42
+ if (because.length > 0)
43
+ blocked.push({ name, because });
44
+ else
45
+ starting.push(name);
46
+ }
47
+ return { failed, blocked, starting };
48
+ }
49
+ /** A failure exists and everything still pending is blocked by it, so waiting longer cannot help. */
50
+ export function isStartupStalled(jsonText, dependsOn, deferred = new Set()) {
51
+ const report = buildStartupReport(jsonText, dependsOn, deferred);
52
+ return report.failed.length > 0 && report.starting.length === 0;
53
+ }
54
+ export function formatStartupReport(report, timedOut) {
55
+ const lines = [];
56
+ for (const { name, message } of report.failed) {
57
+ lines.push(`✗ ${name} failed: ${message}`);
58
+ const fix = portConflictFix(message);
59
+ if (fix)
60
+ lines.push(` ${fix}`);
61
+ }
62
+ for (const { name, because } of report.blocked) {
63
+ lines.push(`✗ ${name} is not ready because ${because.join(", ")} failed`);
64
+ }
65
+ if (report.starting.length > 0) {
66
+ lines.push(`${timedOut ? "Timed out waiting for" : "Still starting:"} ${report.starting.join(", ")}`);
67
+ }
68
+ if (lines.length > 0) {
69
+ lines.push("The environment is not ready. Tilt is still running; inspect it or run: tdk down");
70
+ }
71
+ return lines;
72
+ }
73
+ //# sourceMappingURL=startup-report.js.map
@@ -0,0 +1,58 @@
1
+ function levenshteinDistance(left, right) {
2
+ let previousRow = Array.from({ length: right.length + 1 }, (_, index) => index);
3
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex++) {
4
+ const currentRow = [leftIndex];
5
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex++) {
6
+ const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
7
+ currentRow[rightIndex] = Math.min(currentRow[rightIndex - 1] + 1, previousRow[rightIndex] + 1, previousRow[rightIndex - 1] + substitutionCost);
8
+ }
9
+ previousRow = currentRow;
10
+ }
11
+ return previousRow[right.length];
12
+ }
13
+ function isAdjacentTransposition(left, right) {
14
+ if (left.length !== right.length)
15
+ return false;
16
+ for (let index = 0; index < left.length - 1; index++) {
17
+ if (left[index] !== right[index] &&
18
+ left[index] === right[index + 1] &&
19
+ left[index + 1] === right[index] &&
20
+ left.slice(0, index) === right.slice(0, index) &&
21
+ left.slice(index + 2) === right.slice(index + 2)) {
22
+ return true;
23
+ }
24
+ }
25
+ return false;
26
+ }
27
+ /**
28
+ * Bases the threshold on the longer name and caps it at two edits.
29
+ * An adjacent transposition counts as one typo; ties use deterministic code-unit order.
30
+ */
31
+ export function suggestClosest(input, candidates) {
32
+ if (input.length < 2 || candidates.length === 0)
33
+ return undefined;
34
+ // stackExists and findUnknownServices are case-sensitive, so case-only mismatches need the canonical spelling.
35
+ if (candidates.includes(input))
36
+ return undefined;
37
+ const normalizedInput = input.toLowerCase();
38
+ let closest;
39
+ let closestDistance = Number.POSITIVE_INFINITY;
40
+ for (const candidate of candidates) {
41
+ if (candidate.length === 0)
42
+ continue;
43
+ const normalizedCandidate = candidate.toLowerCase();
44
+ const distance = isAdjacentTransposition(normalizedInput, normalizedCandidate)
45
+ ? 1
46
+ : levenshteinDistance(normalizedInput, normalizedCandidate);
47
+ const threshold = Math.max(1, Math.min(2, Math.floor(Math.max(normalizedInput.length, normalizedCandidate.length) / 3)));
48
+ if (distance > threshold)
49
+ continue;
50
+ if (distance < closestDistance ||
51
+ (distance === closestDistance && (closest === undefined || candidate < closest))) {
52
+ closest = candidate;
53
+ closestDistance = distance;
54
+ }
55
+ }
56
+ return closest;
57
+ }
58
+ //# sourceMappingURL=suggestions.js.map
@@ -0,0 +1,195 @@
1
+ import { STANDARD_PORTS } from "./constants.js";
2
+ import { runTilt } from "./tilt.js";
3
+ export class TiltEventsLoadError extends Error {
4
+ kind;
5
+ constructor(message, kind) {
6
+ super(message);
7
+ this.kind = kind;
8
+ this.name = "TiltEventsLoadError";
9
+ }
10
+ }
11
+ function asRecord(value) {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value)
13
+ ? value
14
+ : null;
15
+ }
16
+ // Tilt-controlled text is rendered in the terminal, so drop ANSI/OSC escape sequences and
17
+ // other control characters (keeping tab/newline for later whitespace normalization).
18
+ const ESC = String.fromCharCode(0x1b);
19
+ const BEL = String.fromCharCode(0x07);
20
+ const ANSI_SEQUENCE = new RegExp(`${ESC}(?:\\[[0-?]*[ -/]*[@-~]|\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)?|[@-Z\\\\-_])`, "g");
21
+ // A regex literal would trip noControlCharactersInRegex, so this stays a string; keep the autofix from rewriting it.
22
+ // biome-ignore lint/complexity/useRegexLiterals: the control-character range must stay in a string
23
+ const CONTROL_CHARS = new RegExp("[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f]", "g");
24
+ export function stripTerminalControls(value) {
25
+ return value.replace(ANSI_SEQUENCE, "").replace(CONTROL_CHARS, "");
26
+ }
27
+ function readString(value) {
28
+ if (typeof value !== "string")
29
+ return undefined;
30
+ const cleaned = stripTerminalControls(value).trim();
31
+ return cleaned || undefined;
32
+ }
33
+ function readStringArray(value) {
34
+ return Array.isArray(value)
35
+ ? value.flatMap((item) => {
36
+ const cleaned = readString(item);
37
+ return cleaned ? [cleaned] : [];
38
+ })
39
+ : [];
40
+ }
41
+ export function parseTiltUiResourceList(value) {
42
+ const list = asRecord(value);
43
+ if (!list || !Array.isArray(list.items)) {
44
+ throw new Error("Tilt returned an invalid UIResource list");
45
+ }
46
+ const resources = [];
47
+ const events = [];
48
+ for (const value of list.items) {
49
+ const item = asRecord(value);
50
+ const metadata = asRecord(item?.metadata);
51
+ const resourceName = readString(metadata?.name);
52
+ if (!item || !resourceName)
53
+ continue;
54
+ const status = asRecord(item.status);
55
+ resources.push({
56
+ name: resourceName,
57
+ runtimeStatus: readString(status?.runtimeStatus),
58
+ updateStatus: readString(status?.updateStatus),
59
+ hasPendingChanges: typeof status?.hasPendingChanges === "boolean" ? status.hasPendingChanges : undefined,
60
+ });
61
+ const history = Array.isArray(status?.buildHistory) ? status.buildHistory : [];
62
+ for (const [buildIndex, value] of history.entries()) {
63
+ const build = asRecord(value);
64
+ if (!build)
65
+ continue;
66
+ const error = readString(build.error);
67
+ const warnings = readStringArray(build.warnings);
68
+ const details = [error, ...warnings].filter((entry) => Boolean(entry));
69
+ const spanId = readString(build.spanID);
70
+ events.push({
71
+ id: `${resourceName}:build:${spanId ?? String(buildIndex)}`,
72
+ resourceName,
73
+ kind: error ? "failed" : warnings.length ? "warning" : "success",
74
+ title: error
75
+ ? "Build failed"
76
+ : warnings.length
77
+ ? "Build completed with warnings"
78
+ : "Build completed",
79
+ occurredAt: readString(build.finishTime) ?? readString(build.startTime),
80
+ details: details.length ? details.join("; ") : undefined,
81
+ });
82
+ }
83
+ const currentBuild = asRecord(status?.currentBuild);
84
+ if (currentBuild) {
85
+ events.push({
86
+ id: `${resourceName}:current-build`,
87
+ resourceName,
88
+ kind: "running",
89
+ title: "Build in progress",
90
+ occurredAt: readString(currentBuild.startTime),
91
+ });
92
+ }
93
+ const pendingSince = readString(status?.pendingBuildSince);
94
+ if (!currentBuild && (status?.queued === true || pendingSince)) {
95
+ events.push({
96
+ id: `${resourceName}:queued`,
97
+ resourceName,
98
+ kind: "warning",
99
+ title: "Update queued",
100
+ occurredAt: pendingSince,
101
+ });
102
+ }
103
+ const lastDeployTime = readString(status?.lastDeployTime);
104
+ const deployTimestamp = lastDeployTime ? Date.parse(lastDeployTime) : Number.NaN;
105
+ const matchingBuild = events.some((event) => event.resourceName === resourceName &&
106
+ event.kind === "success" &&
107
+ event.occurredAt !== undefined &&
108
+ Date.parse(event.occurredAt) === deployTimestamp);
109
+ if (lastDeployTime && !matchingBuild) {
110
+ events.push({
111
+ id: `${resourceName}:last-deploy`,
112
+ resourceName,
113
+ kind: "success",
114
+ title: "Last deployed",
115
+ occurredAt: lastDeployTime,
116
+ });
117
+ }
118
+ const conditions = Array.isArray(status?.conditions) ? status.conditions : [];
119
+ for (const [conditionIndex, value] of conditions.entries()) {
120
+ const condition = asRecord(value);
121
+ if (!condition)
122
+ continue;
123
+ const type = readString(condition.type);
124
+ const state = readString(condition.status);
125
+ const reason = readString(condition.reason);
126
+ const message = readString(condition.message);
127
+ if ((!type && !reason && !message) || (state === "True" && !reason && !message))
128
+ continue;
129
+ const detail = [reason, message]
130
+ .filter((entry) => Boolean(entry))
131
+ .join(": ");
132
+ events.push({
133
+ id: `${resourceName}:condition:${conditionIndex}:${type ?? "status"}`,
134
+ resourceName,
135
+ kind: state === "False" ? "warning" : "status",
136
+ title: `Condition ${type ?? "status"}${state ? `: ${state}` : ""}`,
137
+ occurredAt: readString(condition.lastTransitionTime),
138
+ details: detail || undefined,
139
+ });
140
+ }
141
+ }
142
+ events.sort((left, right) => {
143
+ if (left.kind === "running" && right.kind !== "running")
144
+ return -1;
145
+ if (right.kind === "running" && left.kind !== "running")
146
+ return 1;
147
+ const leftTime = left.occurredAt ? Date.parse(left.occurredAt) : Number.NaN;
148
+ const rightTime = right.occurredAt ? Date.parse(right.occurredAt) : Number.NaN;
149
+ if (Number.isNaN(leftTime)) {
150
+ return Number.isNaN(rightTime) ? left.resourceName.localeCompare(right.resourceName) : 1;
151
+ }
152
+ if (Number.isNaN(rightTime))
153
+ return -1;
154
+ return rightTime - leftTime;
155
+ });
156
+ return { resources, events };
157
+ }
158
+ export async function loadTiltEvents() {
159
+ const port = process.env.TILT_PORT ?? String(STANDARD_PORTS.tiltUi);
160
+ let result;
161
+ try {
162
+ result = await runTilt("get", ["uiresources", "-o", "json", "--port", port], {
163
+ inheritStdio: false,
164
+ timeoutMs: 10_000,
165
+ });
166
+ }
167
+ catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
169
+ const kind = /ENOENT|not found|no such file|connection refused|failed to connect|could not connect|unable to connect|Tilt is not running/i.test(message)
170
+ ? "unavailable"
171
+ : "error";
172
+ throw new TiltEventsLoadError(message, kind);
173
+ }
174
+ if (result.exitCode !== 0) {
175
+ const message = result.stderr.trim() || `tilt get exited with code ${result.exitCode}`;
176
+ const kind = /connection refused|failed to connect|could not connect|unable to connect|Tilt is not running|no running Tilt/i.test(message)
177
+ ? "unavailable"
178
+ : "error";
179
+ throw new TiltEventsLoadError(message, kind);
180
+ }
181
+ let parsed;
182
+ try {
183
+ parsed = JSON.parse(result.stdout);
184
+ }
185
+ catch {
186
+ throw new TiltEventsLoadError("Tilt returned invalid UIResource JSON", "error");
187
+ }
188
+ try {
189
+ return parseTiltUiResourceList(parsed);
190
+ }
191
+ catch (error) {
192
+ throw new TiltEventsLoadError(error instanceof Error ? error.message : "Tilt returned an invalid UIResource list", "error");
193
+ }
194
+ }
195
+ //# sourceMappingURL=tilt-events.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A Tilt resource that is not built and running yet: still waiting (`pending`, `none`) or building right now
3
+ * (`in_progress`). Readiness must treat all of these as not ready.
4
+ */
5
+ export function isTiltResourcePending(updateStatus, runtimeStatus) {
6
+ return (updateStatus === "pending" ||
7
+ updateStatus === "in_progress" ||
8
+ updateStatus === "none" ||
9
+ runtimeStatus === "pending" ||
10
+ runtimeStatus === "none");
11
+ }
12
+ //# sourceMappingURL=tilt-resource-state.js.map
@@ -11,6 +11,13 @@ export function parseTiltPort(value) {
11
11
  }
12
12
  return { ok: true, port };
13
13
  }
14
+ export function secondUpAction(input) {
15
+ if (input.runningPorts.length === 0 || input.force)
16
+ return { action: "start" };
17
+ if (input.only)
18
+ return { action: "only-blocked", ports: input.runningPorts };
19
+ return { action: "already-running", ports: input.runningPorts };
20
+ }
14
21
  export async function resolveTiltPort(options) {
15
22
  if (options.configuredPort !== undefined) {
16
23
  return { port: options.configuredPort, autoSwitched: false };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * What `tdk logs` says when no Tilt answers on the port. The usual cause is that the stack is not up, so the message
3
+ * names the command that starts it; Tilt's own connection error is not repeated, because it only restates that.
4
+ */
5
+ export function tiltUnreachableMessage(port) {
6
+ return `Could not reach Tilt on port ${port}. The stack is not running, or Tilt is on another port. Start it with: tdk up`;
7
+ }
8
+ //# sourceMappingURL=tilt-unreachable-message.js.map
@@ -0,0 +1,35 @@
1
+ import { getDeferredResourceNames } from "./doctor-runtime.js";
2
+ import { deriveServiceStates } from "./service-runtime-state.js";
3
+ import { deriveStackStatus } from "./stack-status.js";
4
+ import { tiltGetUiResources } from "./up-readiness.js";
5
+ /** One look at the running Tilt. Empty when no Tilt answers, so every service stays unknown. */
6
+ export async function fetchServiceStates(services, tiltPort) {
7
+ try {
8
+ const text = await tiltGetUiResources(tiltPort);
9
+ if (!text)
10
+ return {};
11
+ return deriveServiceStates(text, Object.fromEntries(services.map((r) => [r.name, r.config?.dependsOn ?? []])), getDeferredResourceNames());
12
+ }
13
+ catch {
14
+ return {};
15
+ }
16
+ }
17
+ /** The same states `tdk status` shows, put onto the stack the UI is about to draw. */
18
+ export function applyServiceStates(metadata, states) {
19
+ const resources = metadata.resources.map((resource) => {
20
+ const state = states[resource.name];
21
+ if (!state)
22
+ return resource;
23
+ return {
24
+ ...resource,
25
+ status: state.status,
26
+ ...(state.reason ? { statusReason: state.reason } : {}),
27
+ };
28
+ });
29
+ return {
30
+ ...metadata,
31
+ resources,
32
+ overallStatus: deriveStackStatus(resources.map((r) => r.status)),
33
+ };
34
+ }
35
+ //# sourceMappingURL=ui-service-state.js.map
@@ -102,6 +102,8 @@ export async function waitForTiltResourcesReady(port, options = {}) {
102
102
  last = evaluation.result;
103
103
  if (evaluation.settled)
104
104
  return last;
105
+ if (last.failures.length > 0 && options.isStalled?.(text))
106
+ return last;
105
107
  }
106
108
  catch {
107
109
  /* Tilt may emit partial output while starting; retry. */
package/cli/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tdk-cli-internal",
3
3
  "private": true,
4
- "version": "1.3.120",
4
+ "version": "1.3.122",
5
5
  "description": "TDK CLI — start services on your laptop.",
6
6
  "keywords": [
7
7
  "tdk",
@@ -3,8 +3,7 @@
3
3
  # =============================================================================
4
4
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
5
5
  # Source: .tdk/project.json
6
- # Generated: {{timestamp}}
7
- # Run 'tdk project' to regenerate
6
+ # Regenerate with: tdk config regenerate
8
7
  # =============================================================================
9
8
  #
10
9
  # This file tells Tilt which directories to ignore during file watching.
@@ -6,7 +6,7 @@
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: TDK Platform Standards v{{version}}
8
8
  # Output: .tdk/.tdk-out/
9
- # Run 'tdk project' to update
9
+ # Regenerate with: tdk config regenerate
10
10
  # =============================================================================
11
11
 
12
12
  # Port Configuration
@@ -6,7 +6,7 @@
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: TDK Platform Standards v{{version}}
8
8
  # Output: .tdk/.tdk-out/
9
- # Run 'tdk project' to update
9
+ # Regenerate with: tdk config regenerate
10
10
  # =============================================================================
11
11
 
12
12
  def get_tech_stack():
@@ -69,15 +69,15 @@ def validate_tech_stack(config, strict=False):
69
69
  ))
70
70
  elif strict:
71
71
  errors.append(
72
- "❌ Tech Stack Error: {}: '{}' is not supported. " +
73
- "Platform standard is '{}'. Use _override_reason to document exceptions.".format(
72
+ ("❌ Tech Stack Error: {}: '{}' is not supported. " +
73
+ "Platform standard is '{}'. Use _override_reason to document exceptions.").format(
74
74
  key, value, standard
75
75
  )
76
76
  )
77
77
  else:
78
78
  warnings.append(
79
- "⚠️ Tech Stack Warning: {}: '{}' is not standard. " +
80
- "Platform standard is '{}'.".format(key, value, standard)
79
+ ("⚠️ Tech Stack Warning: {}: '{}' is not standard. " +
80
+ "Platform standard is '{}'.").format(key, value, standard)
81
81
  )
82
82
 
83
83
  # Optional validations (warnings if missing or different)
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env starlark
2
- # -*- coding: utf-8 -*}
2
+ # -*- coding: utf-8 -*-
3
3
  # =============================================================================
4
4
  # 🎯 {{project.name}} - TILT ENTRYPOINT
5
5
  # =============================================================================
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: .tdk/project.json
8
- # Run 'tdk project' to regenerate
8
+ # Regenerate with: tdk config regenerate
9
9
  # =============================================================================
10
10
 
11
11
  print("🚀 Loading {{project.name}}...")
@@ -38,7 +38,7 @@ TDK extension not found.
38
38
  'tdk up' needs the TDK Tilt extension (engine/discovery/specs/ext).
39
39
  Fix it with one of:
40
40
  1) Regenerate the project so the extension is vendored in:
41
- tdk project --yes
41
+ tdk config regenerate
42
42
  (set $TDK_EXTENSION_SOURCE to a tdk-cli checkout if it was not auto-detected)
43
43
  2) Point at any tdk-cli checkout:
44
44
  export TDK_EXTENSION_PATH=/path/to/tdk-cli
@@ -6,7 +6,7 @@
6
6
  # SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
7
7
  # Source: .tdk/project.json + TDK Platform Standards v{{version}}
8
8
  # Output: .tdk/.tdk-out/
9
- # Run 'tdk project' to update
9
+ # Regenerate with: tdk config regenerate
10
10
  #
11
11
  # 🏗️ ARCHITECTURE: Project → Stack → Service (PSR Model)
12
12
  # Services are organized into stacks by release phase.
@@ -450,79 +450,6 @@ def _generate_port_anchors(resource_entries):
450
450
  return "\n".join(anchors)
451
451
 
452
452
 
453
- def _detect_sdk_in_entries(resource_entries):
454
- """Check if any entry is an SDK."""
455
- for entry in resource_entries:
456
- if "-sdk:" in entry or entry.strip().startswith("identity-sdk:"):
457
- return True
458
- return False
459
-
460
- def _detect_frontend_in_entries(resource_entries):
461
- """Check if any entry is a frontend."""
462
- for entry in resource_entries:
463
- if "<<: *frontend-memory-limit" in entry:
464
- return True
465
- return False
466
-
467
- def _detect_backend_in_entries(resource_entries):
468
- """Check if any entry is a backend."""
469
- for entry in resource_entries:
470
- if _is_backend_entry(entry):
471
- return True
472
- return False
473
-
474
- def _is_sdk_entry(entry):
475
- """Check if a specific entry is an SDK."""
476
- return "-sdk:" in entry or entry.strip().startswith("identity-sdk:")
477
-
478
- def _is_backend_entry(entry):
479
- """Check if a specific entry is a backend (not frontend, not SDK)."""
480
- if "-sdk:" in entry or entry.strip().startswith("identity-sdk:"):
481
- return False
482
- if "<<: *frontend-memory-limit" in entry:
483
- return False
484
- # Backend entries have healthcheck with backend port pattern
485
- if "healthcheck:" in entry and "curl" in entry:
486
- return True
487
- return False
488
-
489
- def _replace_ports_with_anchors(entry, has_sdk=False, has_frontend=False, has_backend=False):
490
- """Replace hardcoded ports with YAML anchors in an entry."""
491
- is_sdk = _is_sdk_entry(entry)
492
- is_backend = _is_backend_entry(entry)
493
- lines = entry.split("\n")
494
- result = []
495
-
496
- for line in lines:
497
- new_line = line
498
- # NOTE: We do NOT replace PORT=3000 with an anchor because YAML anchors
499
- # in environment variable values are treated as literal strings, not resolved.
500
- # The actual port value is already correctly set by _generate_single_backend_entry
501
- # using the internal_port variable. Keep PORT=3000 as-is.
502
-
503
- # YAML anchors do not resolve inside quoted Docker labels. Traefik must
504
- # see a numeric loadbalancer.port (e.g. 4000), not `*backend-port`.
505
- # NOTE: We do NOT replace healthcheck URLs with YAML anchors because anchors
506
- # don't work inside quoted strings. The wget healthcheck URLs must use actual
507
- # port numbers (e.g., http://127.0.0.1:3000), not anchor references.
508
- # The correct port is already set by generate_frontend_compose.
509
- # NOTE: We do NOT replace healthcheck URLs with anchors because YAML anchors
510
- # don't work inside quoted strings. The healthcheck URL must use the actual
511
- # port number (e.g., http://localhost:3000/health), not an anchor reference.
512
- # The _generate_single_backend_entry function already uses the correct port
513
- # via the internal_port variable in the healthcheck URL.
514
-
515
- result.append(new_line)
516
-
517
- return "\n".join(result)
518
-
519
- def _process_entries_with_anchors(resource_entries, has_sdk, has_frontend, has_backend):
520
- """Replace ports with anchors in all entries."""
521
- result = []
522
- for entry in resource_entries:
523
- result.append(_replace_ports_with_anchors(entry, has_sdk, has_frontend, has_backend))
524
- return result
525
-
526
453
  def generate_app_compose_from_entries(resource_path, resource_entries, write_fn=None):
527
454
  """Generate docker-compose.app.autogenerated.yml from service entries with port anchors.
528
455
 
@@ -535,14 +462,6 @@ def generate_app_compose_from_entries(resource_path, resource_entries, write_fn=
535
462
  str: Generated docker-compose YAML content
536
463
  """
537
464
 
538
- # Detect service types
539
- has_sdk = _detect_sdk_in_entries(resource_entries)
540
- has_frontend = _detect_frontend_in_entries(resource_entries)
541
- has_backend = _detect_backend_in_entries(resource_entries)
542
-
543
- # Replace ports with anchors in entries
544
- processed_entries = _process_entries_with_anchors(resource_entries, has_sdk, has_frontend, has_backend)
545
-
546
465
  # Generate port anchors
547
466
  port_anchors = _generate_port_anchors(resource_entries)
548
467
 
@@ -598,15 +517,11 @@ networks:
598
517
  network_infisical=PlatformDockerConstants.NETWORK_INFISICAL,
599
518
  )
600
519
 
601
- content = header + "\n".join(processed_entries) + footer
520
+ content = header + "\n".join(resource_entries) + footer
602
521
 
603
522
  if write_fn:
604
523
  compose_path = resource_path + '/' + OUTPUT.AUTOGENERATED_FOLDER + '/docker-compose.app.autogenerated.yml'
605
- print("DEBUG COMPOSE GEN: Writing {} bytes to {}".format(len(content), compose_path))
606
524
  write_fn(compose_path, content)
607
- print("DEBUG COMPOSE GEN: Write completed")
608
- else:
609
- print("DEBUG COMPOSE GEN: No write_fn provided!")
610
525
 
611
526
  return content
612
527
 
@@ -10,7 +10,7 @@ load("../config/healthcheck.star", "compose_healthcheck_timing")
10
10
  load("../networking/traefik_static_routes.star", "normalize_abs_path")
11
11
 
12
12
 
13
- def generate_standalone_traefik_compose(sablier_enabled = False, http_host_port = "8080", https_host_port = "8443"):
13
+ def generate_standalone_traefik_compose(sablier_enabled = False, http_host_port = "8080", https_host_port = "8443", bind_address = "127.0.0.1"):
14
14
  """Minimal Traefik that routes Docker labels on api.{project}.localhost.
15
15
 
16
16
  The Sablier plugin, the sablier container and the wake gateway are only
@@ -31,7 +31,6 @@ services:
31
31
  container_name: {name}_traefik
32
32
  restart: unless-stopped
33
33
  {traefik_depends_on} command:
34
- - "--api.insecure=true"
35
34
  - "--ping=true"
36
35
  - "--providers.docker=true"
37
36
  - "--providers.docker.exposedbydefault=false"
@@ -48,8 +47,8 @@ services:
48
47
  - "--entrypoints.websecure.address=:443"
49
48
  - "--log.level=INFO"
50
49
  {sablier_flags} ports:
51
- - "{http_host_port}:80"
52
- - "{https_host_port}:443"
50
+ - "{bind_address}:{http_host_port}:80"
51
+ - "{bind_address}:{https_host_port}:443"
53
52
  volumes:
54
53
  - /var/run/docker.sock:/var/run/docker.sock:ro
55
54
  - ./{dynamic_dir_name}:/etc/traefik/dynamic:ro
@@ -67,6 +66,7 @@ services:
67
66
  network=network,
68
67
  http_host_port=http_host_port,
69
68
  https_host_port=https_host_port,
69
+ bind_address=bind_address,
70
70
  healthcheck_timing=compose_healthcheck_timing(10),
71
71
  traefik_depends_on=_SABLIER_DEPENDS_ON if sablier_enabled else "",
72
72
  sablier_flags=_SABLIER_FLAGS if sablier_enabled else "",