@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.
- package/CHANGELOG.md +1 -0
- package/README.md +21 -14
- package/cli/dist/cli.js +1 -1
- package/cli/dist/commands/config.js +6 -2
- package/cli/dist/commands/doctor.js +64 -7
- package/cli/dist/commands/help.js +17 -1
- package/cli/dist/commands/logs.js +34 -7
- package/cli/dist/commands/networks.js +1 -1
- package/cli/dist/commands/resources.js +1 -1
- package/cli/dist/commands/stack.js +94 -22
- package/cli/dist/commands/status.js +35 -2
- package/cli/dist/commands/ui.js +105 -9
- package/cli/dist/commands/up.js +105 -28
- package/cli/dist/components/ServiceIssues.js +12 -0
- package/cli/dist/components/TabBar.js +3 -2
- package/cli/dist/components/index.js +1 -0
- package/cli/dist/generator/__tests__/golden-helpers.js +42 -0
- package/cli/dist/generator/template-engine.js +5 -3
- package/cli/dist/utils/atomic-write.js +35 -0
- package/cli/dist/utils/bun-floor.js +8 -0
- package/cli/dist/utils/cold-preflight.js +6 -4
- package/cli/dist/utils/doctor-runtime.js +6 -6
- package/cli/dist/utils/doctor-wiring.js +62 -1
- package/cli/dist/utils/errors.js +20 -2
- package/cli/dist/utils/host-port-plan.js +3 -0
- package/cli/dist/utils/machine-output.js +8 -1
- package/cli/dist/utils/port-conflict-fix.js +24 -0
- package/cli/dist/utils/port-fallback-notice.js +18 -0
- package/cli/dist/utils/port-listener-probe.js +19 -0
- package/cli/dist/utils/secret-redaction.js +55 -0
- package/cli/dist/utils/service-config-checks.js +188 -0
- package/cli/dist/utils/service-runtime-state.js +67 -0
- package/cli/dist/utils/services.js +2 -15
- package/cli/dist/utils/single-flight.js +20 -0
- package/cli/dist/utils/stack-status.js +15 -0
- package/cli/dist/utils/startup-report.js +73 -0
- package/cli/dist/utils/suggestions.js +58 -0
- package/cli/dist/utils/tilt-events.js +195 -0
- package/cli/dist/utils/tilt-resource-state.js +12 -0
- package/cli/dist/utils/tilt-startup.js +7 -0
- package/cli/dist/utils/tilt-unreachable-message.js +8 -0
- package/cli/dist/utils/ui-service-state.js +35 -0
- package/cli/dist/utils/up-readiness.js +2 -0
- package/cli/package.json +1 -1
- package/cli/templates/.tiltignore.hbs +1 -2
- package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -1
- package/cli/templates/TILT_TECH_STACK.star.hbs +5 -5
- package/cli/templates/Tiltfile.hbs +3 -3
- package/cli/templates/spec.master.hbs +1 -1
- package/engine/topologies/platform/docker/compose/compose.star +1 -86
- package/engine/topologies/platform/docker/compose/traefik_standalone.star +4 -4
- package/engine/topologies/platform/docker/networking/traefik.star +40 -7
- package/engine/topologies/platform/docker/networking/traefik_helpers.star +1 -1
- package/engine/topologies/tilt/resources/infra-loader.star +3 -2
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +20 -0
- 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
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
# =============================================================================
|
|
4
4
|
# SYSTEM-GENERATED BY TDK CLI - DO NOT EDIT
|
|
5
5
|
# Source: .tdk/project.json
|
|
6
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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(
|
|
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 "",
|