@tdk-landscape/tdk-cli-core 1.3.120 → 1.3.121
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/README.md +9 -7
- package/cli/dist/commands/config.js +6 -2
- package/cli/dist/commands/logs.js +10 -4
- package/cli/dist/commands/networks.js +1 -1
- package/cli/dist/commands/resources.js +1 -1
- package/cli/dist/commands/status.js +35 -2
- package/cli/dist/commands/ui.js +25 -4
- package/cli/dist/commands/up.js +89 -26
- package/cli/dist/components/ServiceIssues.js +12 -0
- package/cli/dist/components/index.js +1 -0
- package/cli/dist/generator/template-engine.js +4 -2
- package/cli/dist/utils/atomic-write.js +35 -0
- package/cli/dist/utils/doctor-runtime.js +2 -5
- package/cli/dist/utils/errors.js +20 -2
- package/cli/dist/utils/machine-output.js +8 -1
- package/cli/dist/utils/port-fallback-notice.js +18 -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 +68 -0
- package/cli/dist/utils/suggestions.js +58 -0
- package/cli/dist/utils/tilt-resource-state.js +12 -0
- package/cli/dist/utils/tilt-startup.js +7 -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 +1 -1
- package/cli/templates/Tiltfile.hbs +3 -3
- package/cli/templates/spec.master.hbs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,16 +110,18 @@ curl -fsSL https://tdk-landscape.github.io/install.sh | sh
|
|
|
110
110
|
|
|
111
111
|
## History
|
|
112
112
|
|
|
113
|
-
TDK
|
|
114
|
-
[
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
TDK development began on 21 April 2026. The archived, read-only [tdk-landscape/tdk](https://github.com/tdk-landscape/tdk)
|
|
114
|
+
repository records its [first commit](https://github.com/tdk-landscape/tdk/commit/1714637637196ed54cbfa18534230a782c05ab36)
|
|
115
|
+
("Initial commit: TDK specs, generators, CLI, and standards") from that day. A separate `tdk-cli` history spanning 21 April to
|
|
116
|
+
18 September 2026 was [imported into this repository](https://github.com/tdk-landscape/tdk-cli-core/pull/670) as 306 filtered
|
|
117
|
+
commits. Those commits are now ancestors of `main`; the import did not change the source tree.
|
|
118
|
+
|
|
119
|
+
This `tdk-cli-core` repository was created on 19 September 2026, and the `@tdk-landscape/tdk-cli-core` npm package was first
|
|
120
|
+
published on 21 September 2026. Those dates describe the current repository and package, not the start of TDK development.
|
|
119
121
|
|
|
120
122
|
## Requirements and support
|
|
121
123
|
|
|
122
|
-
For the local runtime, install Docker (Desktop, OrbStack, or Colima; Engine 25+, Compose 2.20+) and [Tilt](https://docs.tilt.dev/install.html). Bun 1.2+ is used by the default generated services. TDK selects host ports from bounded fallback ranges for HTTP, HTTPS, and Postgres; set `TDK_HTTP_PORT`, `TDK_HTTPS_PORT`, or `TDK_POSTGRES_PORT` to override them. TDK supports macOS, Linux, and Windows through WSL2 Ubuntu; native Windows supports CLI inspection only. Run `tdk doctor` to check local readiness. See [WSL2 setup](docs/wsl2.md).
|
|
124
|
+
For the local runtime, install Docker (Desktop, OrbStack, or Colima; Engine 25+, Compose 2.20+) and [Tilt](https://docs.tilt.dev/install.html). Bun 1.2+ is used by the default generated services. TDK selects host ports from bounded fallback ranges for HTTP, HTTPS, and Postgres; set `TDK_HTTP_PORT`, `TDK_HTTPS_PORT`, or `TDK_POSTGRES_PORT` to override them. TDK supports macOS, Linux, and Windows through WSL2 Ubuntu; native Windows supports CLI inspection only. Run `tdk doctor` to check local readiness (supports `--no-ping` to skip service health checks and `--ping-timeout <ms>`). See [WSL2 setup](docs/wsl2.md).
|
|
123
125
|
|
|
124
126
|
On native Windows, `tdk --version`, `tdk doctor`, and `tdk up --dry-run` are inspect-only commands. `tdk up` exits 2 with “Landscape startup needs Ubuntu on WSL2. Native Windows is inspect-only.”
|
|
125
127
|
|
|
@@ -244,8 +244,12 @@ export const configCommand = new Command("config")
|
|
|
244
244
|
}
|
|
245
245
|
for (const { diff } of result.diffs)
|
|
246
246
|
console.log(chalk.gray(`\n${diff}`));
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
const schemaErrors = result.errors.filter((error) => error.includes(".schemaVersion:"));
|
|
248
|
+
if (schemaErrors.length > 0) {
|
|
249
|
+
console.log(chalk.gray("\nRun `tdk config migrate` to fix schemaVersion."));
|
|
250
|
+
}
|
|
251
|
+
if (result.diffs.length > 0) {
|
|
252
|
+
console.log(chalk.gray("\nRun `tdk config regenerate` to fix generated files."));
|
|
249
253
|
}
|
|
250
254
|
process.exit(1);
|
|
251
255
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { STANDARD_PORTS } from "../utils/constants.js";
|
|
3
|
-
import { runCommand, showErrorAndExit } from "../utils/errors.js";
|
|
3
|
+
import { errorFactories, runCommand, showErrorAndExit, TdkError } from "../utils/errors.js";
|
|
4
4
|
import { createMachineEnvelope } from "../utils/machine-output.js";
|
|
5
5
|
import { isTiltAvailable, runTilt } from "../utils/tilt.js";
|
|
6
6
|
import { DEFAULT_LOG_TAIL, isTiltConnectionFailure, isValidPort, isValidSince, isValidTail, MAX_LOG_TAIL, parseTiltLogLines, } from "../utils/tilt-logs.js";
|
|
@@ -28,12 +28,16 @@ export const logsCommand = new Command("logs")
|
|
|
28
28
|
.option("--port <n>", "Tilt UI port (default: TILT_PORT or 10350)")
|
|
29
29
|
.option("--json", "Output one JSON object and exit", false)
|
|
30
30
|
.action(async (options) => {
|
|
31
|
-
const fail = (code, message, exitCode = 1) => {
|
|
31
|
+
const fail = (code, message, exitCode = 1, suggestions = []) => {
|
|
32
32
|
if (options.json) {
|
|
33
|
-
console.log(JSON.stringify(createMachineEnvelope(null, [
|
|
33
|
+
console.log(JSON.stringify(createMachineEnvelope(null, [
|
|
34
|
+
{ code, message, ...(suggestions.length > 0 ? { suggestions } : {}) },
|
|
35
|
+
])));
|
|
34
36
|
console.error(message);
|
|
35
37
|
process.exit(exitCode);
|
|
36
38
|
}
|
|
39
|
+
if (suggestions.length > 0)
|
|
40
|
+
return new TdkError(message, suggestions, exitCode).exit();
|
|
37
41
|
return showErrorAndExit(message, exitCode);
|
|
38
42
|
};
|
|
39
43
|
if (!isValidTail(options.tail))
|
|
@@ -54,7 +58,9 @@ export const logsCommand = new Command("logs")
|
|
|
54
58
|
const known = await tiltResourceNames(portText);
|
|
55
59
|
const unknown = services.filter((name) => known && !known.includes(name));
|
|
56
60
|
if (unknown.length > 0) {
|
|
57
|
-
|
|
61
|
+
const error = errorFactories.unknownServices(unknown, known ?? []);
|
|
62
|
+
const suggestions = error.suggestions;
|
|
63
|
+
fail("UNKNOWN_SERVICE", error.message, error.exitCode, suggestions);
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
const args = ["--port", portText, "--tail", String(tail)];
|
|
@@ -172,7 +172,7 @@ export const networksCommand = new Command("networks")
|
|
|
172
172
|
}
|
|
173
173
|
const discovery = createDiscoveryContext();
|
|
174
174
|
if (options.stack && !stackExists(options.stack, discovery.resources)) {
|
|
175
|
-
const error = errorFactories.stackNotFound(options.stack);
|
|
175
|
+
const error = errorFactories.stackNotFound(options.stack, discovery.stackNames);
|
|
176
176
|
if (options.json || options.jsonLegacy)
|
|
177
177
|
throw error;
|
|
178
178
|
error.exit();
|
|
@@ -33,7 +33,7 @@ export const resourcesCommand = new Command("resources")
|
|
|
33
33
|
}
|
|
34
34
|
const discovery = createDiscoveryContext();
|
|
35
35
|
if (options.stack && !stackExists(options.stack, discovery.resources)) {
|
|
36
|
-
const error = errorFactories.stackNotFound(options.stack);
|
|
36
|
+
const error = errorFactories.stackNotFound(options.stack, discovery.stackNames);
|
|
37
37
|
if (options.json)
|
|
38
38
|
writeMachineError(error);
|
|
39
39
|
error.exit();
|
|
@@ -7,6 +7,7 @@ import { formatCount, showDetail, showStep } from "../utils/formatting.js";
|
|
|
7
7
|
import { getHostPortPlan } from "../utils/host-port-config.js";
|
|
8
8
|
import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
|
|
9
9
|
import { findProjectRoot } from "../utils/paths.js";
|
|
10
|
+
import { deriveServiceStates } from "../utils/service-runtime-state.js";
|
|
10
11
|
import { buildServicePorts, buildStackPorts } from "../utils/status-ports.js";
|
|
11
12
|
import { getTiltfilePath, isTiltAvailable, runTilt } from "../utils/tilt.js";
|
|
12
13
|
import { evaluateTiltReadiness, tiltGetUiResources } from "../utils/up-readiness.js";
|
|
@@ -58,16 +59,26 @@ export const statusCommand = new Command("status")
|
|
|
58
59
|
// A single look at the running Tilt, so a caller that started `tdk up` detached can poll for readiness. Null when no
|
|
59
60
|
// Tilt answers or its output cannot be read.
|
|
60
61
|
let readiness = null;
|
|
61
|
-
|
|
62
|
+
// The same look at Tilt feeds readiness and each service's state, so `tdk status` and `tdk ui` cannot disagree.
|
|
63
|
+
let serviceStates = null;
|
|
64
|
+
if (tiltAvailable) {
|
|
62
65
|
const port = Number.parseInt(process.env.TILT_PORT ?? "", 10);
|
|
63
66
|
const text = await tiltGetUiResources(Number.isInteger(port) ? port : 10350);
|
|
64
67
|
if (text) {
|
|
68
|
+
const deferred = getDeferredResourceNames();
|
|
65
69
|
try {
|
|
66
|
-
|
|
70
|
+
if (options.json)
|
|
71
|
+
readiness = evaluateTiltReadiness(text, deferred).result;
|
|
67
72
|
}
|
|
68
73
|
catch {
|
|
69
74
|
readiness = null;
|
|
70
75
|
}
|
|
76
|
+
try {
|
|
77
|
+
serviceStates = deriveServiceStates(text, Object.fromEntries(discovery.resources.map((r) => [r.name, r.config?.dependsOn ?? []])), deferred);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
serviceStates = null;
|
|
81
|
+
}
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
84
|
if (options.json) {
|
|
@@ -94,6 +105,13 @@ export const statusCommand = new Command("status")
|
|
|
94
105
|
type: resource.type ?? "unknown",
|
|
95
106
|
port: resource.port ?? null,
|
|
96
107
|
...buildServicePorts(resource, portPlan?.ingressHttp),
|
|
108
|
+
status: serviceStates?.[resource.name]?.status ?? "unknown",
|
|
109
|
+
...(serviceStates?.[resource.name]?.reason
|
|
110
|
+
? { statusReason: serviceStates[resource.name]?.reason }
|
|
111
|
+
: {}),
|
|
112
|
+
...(serviceStates?.[resource.name]?.blockedBy
|
|
113
|
+
? { blockedBy: serviceStates[resource.name]?.blockedBy }
|
|
114
|
+
: {}),
|
|
97
115
|
})),
|
|
98
116
|
ports: buildStackPorts(portPlan, process.env.TILT_PORT ? Number.parseInt(process.env.TILT_PORT, 10) : undefined),
|
|
99
117
|
stacks: discovery.stacks.map((stack) => ({
|
|
@@ -132,6 +150,21 @@ export const statusCommand = new Command("status")
|
|
|
132
150
|
}
|
|
133
151
|
}
|
|
134
152
|
}
|
|
153
|
+
if (serviceStates && Object.keys(serviceStates).length > 0) {
|
|
154
|
+
console.log();
|
|
155
|
+
console.log(chalk.bold("Services:"));
|
|
156
|
+
for (const resource of discovery.resources) {
|
|
157
|
+
const state = serviceStates[resource.name];
|
|
158
|
+
if (!state)
|
|
159
|
+
continue;
|
|
160
|
+
const mark = state.status === "ready"
|
|
161
|
+
? chalk.green("✓")
|
|
162
|
+
: state.status === "error"
|
|
163
|
+
? chalk.red("✗")
|
|
164
|
+
: chalk.yellow("…");
|
|
165
|
+
showDetail(`${mark} ${resource.name} ${state.status}${state.reason ? ` (${state.reason})` : ""}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
135
168
|
if (options.tilt && tiltAvailable) {
|
|
136
169
|
console.log();
|
|
137
170
|
showStep("Tilt Resources:");
|
package/cli/dist/commands/ui.js
CHANGED
|
@@ -2,16 +2,18 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { Box, render, Text, useApp, useInput, useStdin, useStdout } from "ink";
|
|
4
4
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
5
|
-
import { DetailPanel, FileTree, ResourceSelectInput, ResourceTable, TabBar, TUIHeader, } from "../components/index.js";
|
|
5
|
+
import { DetailPanel, FileTree, ResourceSelectInput, ResourceTable, ServiceIssues, TabBar, TUIHeader, } from "../components/index.js";
|
|
6
6
|
import { TABS } from "../components/TabBar.js";
|
|
7
7
|
import { createTUITheme, TUIThemeContext, useTUITheme } from "../components/ui-theme.js";
|
|
8
8
|
import { errorFactories, requireProjectRoot } from "../utils/errors.js";
|
|
9
9
|
import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
|
|
10
10
|
import { describeSearch } from "../utils/search-status.js";
|
|
11
11
|
import { clearMetadataCache, discoverResources, discoverStacks, getResourceMetadata, getStackMetadata, } from "../utils/services.js";
|
|
12
|
+
import { singleFlight } from "../utils/single-flight.js";
|
|
12
13
|
import { createStatusMessageController } from "../utils/status-message.js";
|
|
13
14
|
import { getListRowFromMouseY, getTerminalRuleWidth } from "../utils/terminal-layout.js";
|
|
14
15
|
import { isTiltAvailable } from "../utils/tilt.js";
|
|
16
|
+
import { applyServiceStates, fetchServiceStates, } from "../utils/ui-service-state.js";
|
|
15
17
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
16
18
|
const HelpPanel = ({ onClose }) => {
|
|
17
19
|
const theme = useTUITheme();
|
|
@@ -75,6 +77,25 @@ export const TUIApp = ({ animated = true }) => {
|
|
|
75
77
|
useEffect(() => () => statusMessage.dispose(), [statusMessage]);
|
|
76
78
|
const projectRoot = findProjectRoot() || "unknown";
|
|
77
79
|
const [{ stacks, services }, setDiscovered] = useState({ stacks: [], services: [] });
|
|
80
|
+
// What the running Tilt says about each service: the same answer `tdk status` gives. Empty while no Tilt answers.
|
|
81
|
+
const [serviceStates, setServiceStates] = useState({});
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
let cancelled = false;
|
|
84
|
+
const tiltPort = Number.parseInt(process.env.TILT_PORT ?? "", 10);
|
|
85
|
+
// A Tilt call can outlast the 5 s interval; never start a poll while the last one is still running.
|
|
86
|
+
const run = singleFlight((next) => {
|
|
87
|
+
if (cancelled)
|
|
88
|
+
return;
|
|
89
|
+
setServiceStates((previous) => JSON.stringify(previous) === JSON.stringify(next) ? previous : next);
|
|
90
|
+
});
|
|
91
|
+
const poll = () => run(() => fetchServiceStates(services, Number.isInteger(tiltPort) ? tiltPort : 10350));
|
|
92
|
+
void poll();
|
|
93
|
+
const timer = setInterval(() => void poll(), 5000);
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
clearInterval(timer);
|
|
97
|
+
};
|
|
98
|
+
}, [services]);
|
|
78
99
|
// Re-read service.json files from disk. An empty project is not an error:
|
|
79
100
|
// it renders EmptyState. Only a failed discovery shows ErrorScreen.
|
|
80
101
|
const refresh = useCallback(() => {
|
|
@@ -103,9 +124,9 @@ export const TUIApp = ({ animated = true }) => {
|
|
|
103
124
|
return null;
|
|
104
125
|
return {
|
|
105
126
|
stack,
|
|
106
|
-
metadata: getStackMetadata(stack),
|
|
127
|
+
metadata: applyServiceStates(getStackMetadata(stack), serviceStates),
|
|
107
128
|
};
|
|
108
|
-
}, [selectedStack, stacks]);
|
|
129
|
+
}, [selectedStack, stacks, serviceStates]);
|
|
109
130
|
const selectedServiceData = useMemo(() => {
|
|
110
131
|
if (!selectedService)
|
|
111
132
|
return null;
|
|
@@ -476,7 +497,7 @@ export const TUIApp = ({ animated = true }) => {
|
|
|
476
497
|
if (services.length === 0) {
|
|
477
498
|
return (_jsxs(Box, { flexDirection: "column", height: stdout.rows || 24, children: [header, _jsx(EmptyState, { message: message })] }));
|
|
478
499
|
}
|
|
479
|
-
return (_jsxs(Box, { flexDirection: "column", height: stdout.rows || 24, children: [header, isSearching && (_jsxs(Box, { paddingX: 1, height: 1, children: [_jsxs(Text, { color: theme.warning, children: ["Search: ", searchQuery, "_"] }), searchStatus && (_jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: ` ${searchStatus.summary}` }))] })), searchStatus?.emptyMessage && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: theme.muted, children: searchStatus.emptyMessage }) })), !isSearching && message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: theme.accent, children: [theme.bannerStart, " ", message, " ", theme.bannerEnd] }) })), !isSearching && !message && showTooltips && !showHelp && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: createHelpHint(activeTab, selectedStack, selectedService, showEnabledOnly, theme.ascii) }) })), showHelp && (_jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsx(HelpPanel, { onClose: () => setShowHelp(false) }) })), !showHelp && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(TabBar, { activeTab: activeTab, onTabChange: setActiveTab, terminalWidth: terminalWidth }) }), _jsxs(Box, { flexDirection: "row", paddingX: 1, flexGrow: 1, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, width: mainPanelWidth, children: [activeTab === "overview" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: theme.ascii ? "[ Stacks ]" : "┌─ Stacks ─" }) }), _jsx(Box, { marginTop: 1, flexGrow: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] })), activeTab === "resources" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: theme.ascii ? "[ Resources ]" : "┌─ Resources ─" }) }), selectedStackData ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["Stack: ", selectedStackData.stack.name] }),
|
|
500
|
+
return (_jsxs(Box, { flexDirection: "column", height: stdout.rows || 24, children: [header, isSearching && (_jsxs(Box, { paddingX: 1, height: 1, children: [_jsxs(Text, { color: theme.warning, children: ["Search: ", searchQuery, "_"] }), searchStatus && (_jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: ` ${searchStatus.summary}` }))] })), searchStatus?.emptyMessage && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: theme.muted, children: searchStatus.emptyMessage }) })), !isSearching && message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: theme.accent, children: [theme.bannerStart, " ", message, " ", theme.bannerEnd] }) })), !isSearching && !message && showTooltips && !showHelp && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: createHelpHint(activeTab, selectedStack, selectedService, showEnabledOnly, theme.ascii) }) })), showHelp && (_jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsx(HelpPanel, { onClose: () => setShowHelp(false) }) })), !showHelp && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(TabBar, { activeTab: activeTab, onTabChange: setActiveTab, terminalWidth: terminalWidth }) }), _jsxs(Box, { flexDirection: "row", paddingX: 1, flexGrow: 1, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, width: mainPanelWidth, children: [activeTab === "overview" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: theme.ascii ? "[ Stacks ]" : "┌─ Stacks ─" }) }), _jsx(Box, { marginTop: 1, flexGrow: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] })), activeTab === "resources" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: theme.ascii ? "[ Resources ]" : "┌─ Resources ─" }) }), selectedStackData ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["Stack: ", selectedStackData.stack.name] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(ResourceTable, { resources: selectedStackData.metadata.resources, maxWidth: terminalWidth - (showSidebar ? 50 : 10) }), _jsx(ServiceIssues, { resources: selectedStackData.metadata.resources })] })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: "Select a stack to view resources" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] }))] })), activeTab === "files" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: theme.ascii ? "[ Autogenerated Files ]" : "┌─ Autogenerated Files ─" }) }), selectedServiceData ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["Service: ", selectedServiceData.service.name] }), _jsx(Box, { marginTop: 1, children: _jsx(FileTree, { nodes: fileTreeNodes, selectedPath: selectedFile || undefined }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: "Select a service to view files" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] }))] })), activeTab === "config" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: theme.ascii ? "[ Configuration ]" : "┌─ Configuration ─" }) }), selectedServiceData ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.accent, children: selectedServiceData.service.configPath }), _jsx(Box, { marginTop: 1, ...(theme.ascii
|
|
480
501
|
? {}
|
|
481
502
|
: { borderStyle: "single", borderColor: theme.border }), paddingX: 1, children: _jsx(Text, { color: theme.muted, wrap: "wrap", children: JSON.stringify(selectedServiceData.service.config, null, 2).slice(0, 1000) }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: "Select a service to view configuration" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] }))] }))] }), showSidebar && (_jsx(Box, { marginLeft: 2, children: _jsx(DetailPanel, { stack: selectedStackData?.stack || null, service: selectedServiceData?.service || null, stackMetadata: selectedStackData?.metadata || null, visible: !!selectedStack || !!selectedService }) }))] }), _jsxs(Box, { ...(theme.ascii ? {} : { borderStyle: "single", borderColor: theme.border }), paddingX: 1, height: 3, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: theme.accent, bold: true, children: [theme.bannerStart, " ", activeTab] }), _jsxs(Text, { color: theme.success, children: [theme.ascii ? "[+]" : "\u25cf", " ", services.filter((s) => s.stack).length, " in stack"] }), _jsxs(Text, { color: theme.warning, children: [theme.ascii ? "-" : "\u25cb", " ", services.filter((s) => !s.stack).length, " no stack"] })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: theme.muted, children: ["Stacks: ", stacks.length] }), _jsxs(Text, { color: theme.muted, children: ["Services: ", services.length] }), _jsxs(Text, { color: theme.muted, children: [theme.ascii ? "Mouse" : "🖱️", " ", mouseEnabled ? "ON" : "OFF", theme.ascii ? " | " : " \u2502 ", theme.ascii ? "Info" : "ℹ️", " ", showTooltips ? "ON" : "OFF", theme.ascii ? " | " : " \u2502 ", _jsx(Text, { color: showEnabledOnly ? theme.success : theme.warning, children: showEnabledOnly
|
|
482
503
|
? theme.ascii
|
package/cli/dist/commands/up.js
CHANGED
|
@@ -12,16 +12,19 @@ import { formatCount } from "../utils/formatting.js";
|
|
|
12
12
|
import { exportHostPortPlan, getHostPortPlan, writeSavedHostPortPlan, } from "../utils/host-port-config.js";
|
|
13
13
|
import { formatHostPortPlan } from "../utils/host-port-plan.js";
|
|
14
14
|
import { createJsonEmitter } from "../utils/json-output.js";
|
|
15
|
+
import { toMachineError } from "../utils/machine-output.js";
|
|
15
16
|
import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
|
|
16
17
|
import { findAvailablePort } from "../utils/port-assignment.js";
|
|
18
|
+
import { formatPortFallbackNotice } from "../utils/port-fallback-notice.js";
|
|
17
19
|
import { isApiServiceType } from "../utils/resource-kind.js";
|
|
18
20
|
import { appendHealthPath, resolveSubdomainBases } from "../utils/service-urls.js";
|
|
19
21
|
import { discoverResources, discoverResourcesStrict, discoverStacks, stackExists, } from "../utils/services.js";
|
|
20
22
|
import { buildSmokePlans, formatSmokeFailure, runSmokePlans } from "../utils/smoke.js";
|
|
23
|
+
import { buildStartupReport, formatStartupReport, isStartupStalled, } from "../utils/startup-report.js";
|
|
21
24
|
import { evaluateTdkVersionFloor } from "../utils/tdk-version.js";
|
|
22
25
|
import { buildTiltUpArgs, runTilt } from "../utils/tilt.js";
|
|
23
26
|
import { stopTiltOnPort } from "../utils/tilt-process.js";
|
|
24
|
-
import { parseTiltPort, resolveTiltPort, stopTiltForUp } from "../utils/tilt-startup.js";
|
|
27
|
+
import { parseTiltPort, resolveTiltPort, secondUpAction, stopTiltForUp, } from "../utils/tilt-startup.js";
|
|
25
28
|
import { findUnknownServices, resolveOnlySelection } from "../utils/up-only.js";
|
|
26
29
|
import { tiltGetUiResources, waitForTiltResourcesReady } from "../utils/up-readiness.js";
|
|
27
30
|
import { enableDiscoveredStacks } from "./project.js";
|
|
@@ -183,18 +186,34 @@ export const upCommand = new Command("up")
|
|
|
183
186
|
const foundRoot = options.dryRun ? requireProjectRoot() : findProjectRoot();
|
|
184
187
|
const projectRoot = foundRoot ?? process.cwd();
|
|
185
188
|
const discoveredResources = discoverResourcesStrict();
|
|
189
|
+
let discoveredStacks;
|
|
190
|
+
let discoveredStackNames = [];
|
|
191
|
+
if (!options.only || stackName) {
|
|
192
|
+
discoveredStacks = discoverStacks(discoveredResources);
|
|
193
|
+
discoveredStackNames = discoveredStacks.map((stack) => stack.name);
|
|
194
|
+
}
|
|
186
195
|
// Reject a bad request before anything below can write to the project (.env, runtime assets, .tdk/project.json).
|
|
187
196
|
if (options.only) {
|
|
188
|
-
if (stackName && !stackExists(stackName))
|
|
189
|
-
errorFactories.stackNotFound(stackName)
|
|
197
|
+
if (stackName && !stackExists(stackName)) {
|
|
198
|
+
const error = errorFactories.stackNotFound(stackName, discoveredStackNames);
|
|
199
|
+
emit?.({ ok: false }, [toMachineError(error).error]);
|
|
200
|
+
error.exit();
|
|
201
|
+
}
|
|
190
202
|
const candidates = stackName
|
|
191
203
|
? discoveredResources.filter((resource) => resource.stack === stackName)
|
|
192
204
|
: discoveredResources;
|
|
193
205
|
const unknown = findUnknownServices(options.only, candidates);
|
|
194
206
|
if (unknown.length > 0) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
207
|
+
const validNames = candidates.map((service) => service.name);
|
|
208
|
+
const error = errorFactories.unknownServices(unknown, validNames);
|
|
209
|
+
emit?.({ ok: false }, [
|
|
210
|
+
{
|
|
211
|
+
code: "UNKNOWN_SERVICE",
|
|
212
|
+
message: error.message,
|
|
213
|
+
...(error.suggestions.length > 0 ? { suggestions: error.suggestions } : {}),
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
error.exit();
|
|
198
217
|
}
|
|
199
218
|
}
|
|
200
219
|
// Also under --dry-run: the check only reads, and a dry run should show what a real run would refuse.
|
|
@@ -231,14 +250,16 @@ export const upCommand = new Command("up")
|
|
|
231
250
|
if (stackName) {
|
|
232
251
|
servicesToStart = discoveredResources.filter((resource) => resource.stack === stackName);
|
|
233
252
|
if (servicesToStart.length === 0) {
|
|
234
|
-
errorFactories.stackNotFound(stackName)
|
|
253
|
+
const error = errorFactories.stackNotFound(stackName, discoveredStackNames);
|
|
254
|
+
emit?.({ ok: false }, [toMachineError(error).error]);
|
|
255
|
+
error.exit();
|
|
235
256
|
}
|
|
236
257
|
focusServiceNames = servicesToStart.map((s) => s.name);
|
|
237
258
|
stackDescription = `stack "${stackName}"`;
|
|
238
259
|
}
|
|
239
260
|
else {
|
|
240
261
|
servicesToStart = discoveredResources;
|
|
241
|
-
const allStacks = discoverStacks(discoveredResources);
|
|
262
|
+
const allStacks = discoveredStacks ?? discoverStacks(discoveredResources);
|
|
242
263
|
stackDescription = `all stacks (${formatCount(allStacks.length, "stack")}, ${formatCount(servicesToStart.length, "service")})`;
|
|
243
264
|
}
|
|
244
265
|
let dependencyNames = [];
|
|
@@ -292,6 +313,8 @@ export const upCommand = new Command("up")
|
|
|
292
313
|
const dryRunCommand = focusTargets ? `tilt up ${tiltArgs.join(" ")}` : "tilt up";
|
|
293
314
|
if (!options.quiet) {
|
|
294
315
|
console.log(chalk.blue(formatHostPortPlan(hostPortPlan)));
|
|
316
|
+
for (const line of formatPortFallbackNotice(hostPortPlan))
|
|
317
|
+
console.log(chalk.yellow(line));
|
|
295
318
|
console.log(chalk.gray("Override with TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT."));
|
|
296
319
|
}
|
|
297
320
|
if (emit && options.dryRun) {
|
|
@@ -313,23 +336,35 @@ export const upCommand = new Command("up")
|
|
|
313
336
|
exportHostPortPlan(hostPortPlan);
|
|
314
337
|
writeSavedHostPortPlan(projectRoot, hostPortPlan);
|
|
315
338
|
const basePort = 10350;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
339
|
+
const watchedPort = configuredTiltPort ?? basePort;
|
|
340
|
+
const candidatePorts = options.only ? [...new Set([basePort, watchedPort])] : [watchedPort];
|
|
341
|
+
const running = [];
|
|
342
|
+
for (const candidate of candidatePorts) {
|
|
343
|
+
if ((await tiltGetUiResources(candidate)) !== null)
|
|
344
|
+
running.push(candidate);
|
|
345
|
+
}
|
|
346
|
+
const decision = secondUpAction({
|
|
347
|
+
runningPorts: running,
|
|
348
|
+
force: options.force,
|
|
349
|
+
only: Boolean(options.only),
|
|
350
|
+
});
|
|
351
|
+
if (decision.action === "already-running") {
|
|
352
|
+
const message = `Environment is already running on port ${decision.ports.join(", ")}.`;
|
|
353
|
+
emit?.({
|
|
354
|
+
ok: true,
|
|
355
|
+
alreadyRunning: true,
|
|
356
|
+
tiltUrl: `http://localhost:${decision.ports[0]}`,
|
|
357
|
+
});
|
|
358
|
+
if (!options.quiet)
|
|
359
|
+
console.log(chalk.green(message));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (decision.action === "only-blocked") {
|
|
363
|
+
const message = `A Tilt is already running on port ${decision.ports.join(", ")}. --only cannot change a running stack's services: run \`tdk down\` first, or pass --force to replace it.`;
|
|
364
|
+
emit?.({ ok: false }, [{ code: "TILT_ALREADY_RUNNING", message }]);
|
|
365
|
+
showErrorAndExit(message);
|
|
366
|
+
}
|
|
319
367
|
if (options.only) {
|
|
320
|
-
const candidatePorts = [
|
|
321
|
-
...new Set([basePort, ...(configuredTiltPort === undefined ? [] : [configuredTiltPort])]),
|
|
322
|
-
];
|
|
323
|
-
const running = [];
|
|
324
|
-
for (const candidate of candidatePorts) {
|
|
325
|
-
if ((await tiltGetUiResources(candidate)) !== null)
|
|
326
|
-
running.push(candidate);
|
|
327
|
-
}
|
|
328
|
-
if (running.length > 0 && !options.force) {
|
|
329
|
-
const message = `A Tilt is already running on port ${running.join(", ")}. --only cannot change a running stack's services: run \`tdk down\` first, or pass --force to replace it.`;
|
|
330
|
-
emit?.({ ok: false }, [{ code: "TILT_ALREADY_RUNNING", message }]);
|
|
331
|
-
showErrorAndExit(message);
|
|
332
|
-
}
|
|
333
368
|
for (const runningPort of running)
|
|
334
369
|
stopTiltOnPort(runningPort);
|
|
335
370
|
}
|
|
@@ -364,6 +399,7 @@ export const upCommand = new Command("up")
|
|
|
364
399
|
const smokePlans = buildSmokePlans(servicesToStart, hostPortPlan.ingressHttp);
|
|
365
400
|
let smokeFailed = false;
|
|
366
401
|
let printedSuccess = false;
|
|
402
|
+
let startupFailed = false;
|
|
367
403
|
const successOutput = (async () => {
|
|
368
404
|
const uiReady = await waitForTiltUi(port);
|
|
369
405
|
// JSON consumers get success only after every non-deferred resource is built and running, not when the UI port opens.
|
|
@@ -399,12 +435,36 @@ export const upCommand = new Command("up")
|
|
|
399
435
|
]);
|
|
400
436
|
}
|
|
401
437
|
}
|
|
402
|
-
|
|
438
|
+
// The UI port opening only means Tilt started. Say "up" once every resource is built and running, and otherwise name
|
|
439
|
+
// what failed and what it blocks.
|
|
440
|
+
if (uiReady && !emit) {
|
|
441
|
+
const deferred = getDeferredResourceNames();
|
|
442
|
+
const dependsOn = Object.fromEntries(discoverResources().map((r) => [r.name, r.config?.dependsOn ?? []]));
|
|
443
|
+
const readiness = await waitForTiltResourcesReady(port, {
|
|
444
|
+
deferred,
|
|
445
|
+
isStalled: (text) => isStartupStalled(text, dependsOn, deferred),
|
|
446
|
+
});
|
|
447
|
+
if (!readiness.ready) {
|
|
448
|
+
startupFailed = true;
|
|
449
|
+
const resourcesJson = await tiltGetUiResources(port);
|
|
450
|
+
const report = resourcesJson
|
|
451
|
+
? buildStartupReport(resourcesJson, dependsOn, deferred)
|
|
452
|
+
: {
|
|
453
|
+
failed: readiness.failures,
|
|
454
|
+
blocked: [],
|
|
455
|
+
starting: [],
|
|
456
|
+
};
|
|
457
|
+
for (const line of formatStartupReport(report, readiness.timedOut)) {
|
|
458
|
+
console.error(chalk.red(line));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (uiReady && !options.quiet && !startupFailed) {
|
|
403
463
|
for (const line of formatUpSuccess(port))
|
|
404
464
|
console.log(chalk.blue(line));
|
|
405
465
|
printedSuccess = true;
|
|
406
466
|
}
|
|
407
|
-
if (uiReady && smokePlans.length > 0 && (
|
|
467
|
+
if (uiReady && smokePlans.length > 0 && (emit ? jsonReady : !startupFailed)) {
|
|
408
468
|
if (!options.quiet) {
|
|
409
469
|
console.log(chalk.gray(`Smoke check: ${smokePlans.map((p) => p.name).join(", ")}`));
|
|
410
470
|
}
|
|
@@ -446,6 +506,9 @@ export const upCommand = new Command("up")
|
|
|
446
506
|
if (result.exitCode !== 0) {
|
|
447
507
|
handleTiltFailure("up", result.exitCode);
|
|
448
508
|
}
|
|
509
|
+
if (startupFailed && result.exitCode === 0) {
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
449
512
|
if (!options.quiet && result.exitCode === 0 && !printedSuccess) {
|
|
450
513
|
for (const line of formatUpSuccess(port))
|
|
451
514
|
console.log(chalk.blue(line));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useTUITheme } from "./ui-theme.js";
|
|
4
|
+
/** One line per service that is not ready and has a reason, so a failed dependency is named on screen. */
|
|
5
|
+
export function ServiceIssues({ resources }) {
|
|
6
|
+
const theme = useTUITheme();
|
|
7
|
+
const issues = resources.filter((r) => r.status !== "ready" && r.statusReason);
|
|
8
|
+
if (issues.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: issues.map((resource) => (_jsxs(Text, { color: theme.muted, children: [resource.name, ": ", resource.statusReason] }, resource.name))) }));
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=ServiceIssues.js.map
|
|
@@ -3,6 +3,7 @@ export { DetailPanel } from "./DetailPanel.js";
|
|
|
3
3
|
export { FileTree } from "./FileTree.js";
|
|
4
4
|
export { ResourceSelectInput } from "./ResourceSelectInput.js";
|
|
5
5
|
export { ResourceTable } from "./ResourceTable.js";
|
|
6
|
+
export { ServiceIssues } from "./ServiceIssues.js";
|
|
6
7
|
export { TabBar } from "./TabBar.js";
|
|
7
8
|
export { TOOLTIPS } from "./Tooltip.js";
|
|
8
9
|
export { TUIHeader } from "./TUIHeader.js";
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import Handlebars from "handlebars";
|
|
5
5
|
import { PLATFORM_STANDARDS } from "../config/platform-standards.js";
|
|
6
|
+
import { writeTextFileAtomic } from "../utils/atomic-write.js";
|
|
6
7
|
import { writeTextFile } from "../utils/file-helpers.js";
|
|
7
8
|
import { assertTdkGeneratedPath } from "../utils/generated-paths.js";
|
|
8
9
|
import { DEFAULT_ALWAYS_ENABLED_INFRA } from "../utils/project-config-defaults.js";
|
|
@@ -220,6 +221,7 @@ export function generateDatabaseManagementCompose(projectConfig) {
|
|
|
220
221
|
# SYSTEM-GENERATED - DO NOT EDIT
|
|
221
222
|
# Stack feature: database-management
|
|
222
223
|
# Source: .tdk/project.json phases.*.enabledStacks includes "database-management"
|
|
224
|
+
# Regenerate with: tdk config regenerate
|
|
223
225
|
###############################################################################
|
|
224
226
|
|
|
225
227
|
services:
|
|
@@ -443,14 +445,14 @@ export async function generateMasterConfigs(projectRoot, options = {}) {
|
|
|
443
445
|
for (const filename of ALL_GENERATED_FILES) {
|
|
444
446
|
const content = files[filename];
|
|
445
447
|
const filePath = assertTdkGeneratedPath(projectRoot, path.join(".tdk", ".tdk-out", filename));
|
|
446
|
-
|
|
448
|
+
writeTextFileAtomic(filePath, content);
|
|
447
449
|
console.log(`✓ Generated: .tdk/.tdk-out/${filename}`);
|
|
448
450
|
}
|
|
449
451
|
if (isStackFeatureEnabledInStacks(projectConfig.phases, "database-management")) {
|
|
450
452
|
const composePath = assertTdkGeneratedPath(projectRoot, path.join("services", "platform", "database-management", "docker-compose.yml"));
|
|
451
453
|
const composeDir = path.dirname(composePath);
|
|
452
454
|
fs.mkdirSync(composeDir, { recursive: true });
|
|
453
|
-
|
|
455
|
+
writeTextFileAtomic(composePath, generateDatabaseManagementCompose(projectConfig));
|
|
454
456
|
console.log("✓ Generated: services/platform/database-management/docker-compose.yml");
|
|
455
457
|
}
|
|
456
458
|
// Copy .tiltignore to project root so Tilt uses it
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { closeSync, fchmodSync, fsyncSync, openSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Replaces `filePath` in one step. A plain writeFileSync truncates first, so an interrupt mid-write leaves a
|
|
6
|
+
* half-written file that looks current; here readers see the old content or the new content, never a mix.
|
|
7
|
+
*/
|
|
8
|
+
export function writeTextFileAtomic(filePath, content) {
|
|
9
|
+
const tempPath = join(dirname(filePath), `.${basename(filePath)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
10
|
+
let mode;
|
|
11
|
+
try {
|
|
12
|
+
mode = statSync(filePath).mode & 0o777;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
mode = undefined;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const fd = openSync(tempPath, "wx");
|
|
19
|
+
try {
|
|
20
|
+
if (mode !== undefined)
|
|
21
|
+
fchmodSync(fd, mode);
|
|
22
|
+
writeFileSync(fd, content, "utf-8");
|
|
23
|
+
fsyncSync(fd);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
closeSync(fd);
|
|
27
|
+
}
|
|
28
|
+
renameSync(tempPath, filePath);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
rmSync(tempPath, { force: true });
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=atomic-write.js.map
|
|
@@ -7,6 +7,7 @@ import { findProjectRoot } from "./paths.js";
|
|
|
7
7
|
import { isApiServiceType } from "./resource-kind.js";
|
|
8
8
|
import { getProjectName } from "./service-urls.js";
|
|
9
9
|
import { discoverResources, discoverResourcesFromRoot } from "./services.js";
|
|
10
|
+
import { isTiltResourcePending } from "./tilt-resource-state.js";
|
|
10
11
|
import { findOnPath } from "./which.js";
|
|
11
12
|
const EXEC_TIMEOUT_MS = 10_000;
|
|
12
13
|
const REGISTRY_PROBE_TIMEOUT_MS = 3_000;
|
|
@@ -119,11 +120,7 @@ export function parseTiltResourceFailures(jsonText, deferredNames = new Set()) {
|
|
|
119
120
|
okCount += 1;
|
|
120
121
|
continue;
|
|
121
122
|
}
|
|
122
|
-
if (name !== "(Tiltfile)" &&
|
|
123
|
-
(updateStatus === "pending" ||
|
|
124
|
-
runtimeStatus === "pending" ||
|
|
125
|
-
updateStatus === "none" ||
|
|
126
|
-
runtimeStatus === "none")) {
|
|
123
|
+
if (name !== "(Tiltfile)" && isTiltResourcePending(updateStatus, runtimeStatus)) {
|
|
127
124
|
if (deferredNames.has(name)) {
|
|
128
125
|
deferredCount += 1;
|
|
129
126
|
}
|
package/cli/dist/utils/errors.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { QUICKSTART_DOCS_URL } from "./constants.js";
|
|
3
3
|
import { getContainerRuntimeStatus } from "./docker.js";
|
|
4
4
|
import { findProjectRoot } from "./paths.js";
|
|
5
|
+
import { suggestClosest } from "./suggestions.js";
|
|
5
6
|
import { isTiltAvailable } from "./tilt.js";
|
|
6
7
|
export function getErrorMessage(err) {
|
|
7
8
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -30,6 +31,16 @@ export class TdkError extends Error {
|
|
|
30
31
|
process.exit(this.exitCode);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
function closestNameSuggestions(names, candidates, includeInput = false) {
|
|
35
|
+
const suggestions = new Set();
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
const closest = suggestClosest(name, candidates);
|
|
38
|
+
if (closest) {
|
|
39
|
+
suggestions.add(includeInput ? `Did you mean "${closest}" for "${name}"?` : `Did you mean "${closest}"?`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [...suggestions];
|
|
43
|
+
}
|
|
33
44
|
export const errorFactories = {
|
|
34
45
|
tiltNotInstalled: () => new TdkError("Tilt CLI is not installed", [
|
|
35
46
|
"Install Tilt: `brew install tilt` (macOS)",
|
|
@@ -47,14 +58,21 @@ export const errorFactories = {
|
|
|
47
58
|
"Quit and reopen Docker Desktop, or run `colima restart`",
|
|
48
59
|
"Then verify with: `docker ps`",
|
|
49
60
|
]),
|
|
50
|
-
stackNotFound: (name) => new TdkError(`Stack "${name}" not found`, [
|
|
61
|
+
stackNotFound: (name, stackNames = []) => new TdkError(`Stack "${name}" not found`, [
|
|
62
|
+
...closestNameSuggestions([name], stackNames),
|
|
51
63
|
"Run `tdk stacks` to see available stacks",
|
|
52
64
|
"Run `tdk stack` to assign resources to a stack",
|
|
53
65
|
]),
|
|
54
|
-
resourceNotFound: (name) => new TdkError(`Resource "${name}" not found`, [
|
|
66
|
+
resourceNotFound: (name, resourceNames = []) => new TdkError(`Resource "${name}" not found`, [
|
|
67
|
+
...closestNameSuggestions([name], resourceNames),
|
|
55
68
|
"Run `tdk resources` to list all resources",
|
|
56
69
|
"Check the resource name spelling",
|
|
57
70
|
]),
|
|
71
|
+
unknownServices: (names, validNames) => {
|
|
72
|
+
// Service selectors are Tilt resource names, so share the resource suggestion path.
|
|
73
|
+
const suggestions = closestNameSuggestions(names, validNames, names.length > 1);
|
|
74
|
+
return new TdkError(`Unknown service ${names.join(", ")}. Valid names: ${validNames.join(", ")}`, suggestions, 2);
|
|
75
|
+
},
|
|
58
76
|
directoryExists: (path) => new TdkError(`Directory already exists: ${path}`, [
|
|
59
77
|
"Use `--path` to specify a different location",
|
|
60
78
|
"Remove the existing directory if no longer needed",
|
|
@@ -4,7 +4,14 @@ export function createMachineEnvelope(data, errors = []) {
|
|
|
4
4
|
}
|
|
5
5
|
export function toMachineError(error) {
|
|
6
6
|
if (error instanceof TdkError) {
|
|
7
|
-
return {
|
|
7
|
+
return {
|
|
8
|
+
error: {
|
|
9
|
+
code: "COMMAND_FAILED",
|
|
10
|
+
message: error.message,
|
|
11
|
+
...(error.suggestions.length > 0 ? { suggestions: error.suggestions } : {}),
|
|
12
|
+
},
|
|
13
|
+
exitCode: error.exitCode,
|
|
14
|
+
};
|
|
8
15
|
}
|
|
9
16
|
return {
|
|
10
17
|
error: {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const PORT_KEYS = [
|
|
2
|
+
{ key: "ingressHttp", label: "HTTP", env: "TDK_HTTP_PORT" },
|
|
3
|
+
{ key: "ingressHttps", label: "HTTPS", env: "TDK_HTTPS_PORT" },
|
|
4
|
+
{ key: "postgres", label: "Postgres", env: "TDK_POSTGRES_PORT" },
|
|
5
|
+
];
|
|
6
|
+
/** One line per port TDK moved off the one it wanted. Ports the user set explicitly are never listed. */
|
|
7
|
+
export function formatPortFallbackNotice(plan) {
|
|
8
|
+
const lines = [];
|
|
9
|
+
for (const { key, label, env } of PORT_KEYS) {
|
|
10
|
+
const requested = plan.requested[key];
|
|
11
|
+
const chosen = plan[key];
|
|
12
|
+
if (plan.explicit[key] || chosen === requested)
|
|
13
|
+
continue;
|
|
14
|
+
lines.push(`${label}: wanted ${requested}, using ${chosen} (${plan.reason[key]}). To force ${requested}: ${env}=${requested} tdk up`);
|
|
15
|
+
}
|
|
16
|
+
return lines;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=port-fallback-notice.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { buildStartupReport, transitiveDependencies } from "./startup-report.js";
|
|
2
|
+
import { isTiltResourcePending } from "./tilt-resource-state.js";
|
|
3
|
+
import { onlyEnabledResources } from "./up-readiness.js";
|
|
4
|
+
/**
|
|
5
|
+
* The one answer to "is this service ready?" for `tdk status` and `tdk ui`, from Tilt's resources and the `dependsOn`
|
|
6
|
+
* in service.json. A service whose own process is up is still not ready while something it depends on has failed or
|
|
7
|
+
* is still starting. Services Tilt does not list are absent: the caller shows them as unknown.
|
|
8
|
+
*/
|
|
9
|
+
export function deriveServiceStates(jsonText, dependsOn, deferred = new Set()) {
|
|
10
|
+
const report = buildStartupReport(jsonText, dependsOn, deferred);
|
|
11
|
+
const failed = new Map(report.failed.map((entry) => [entry.name, entry.message]));
|
|
12
|
+
const blocked = new Map(report.blocked.map((entry) => [entry.name, entry.because]));
|
|
13
|
+
const starting = new Set(report.starting);
|
|
14
|
+
const items = JSON.parse(onlyEnabledResources(jsonText)).items ?? [];
|
|
15
|
+
const direct = new Map();
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
const name = item.metadata?.name;
|
|
18
|
+
if (!name || name === "(Tiltfile)")
|
|
19
|
+
continue;
|
|
20
|
+
if (failed.has(name)) {
|
|
21
|
+
direct.set(name, { status: "error", reason: failed.get(name) });
|
|
22
|
+
}
|
|
23
|
+
else if (blocked.has(name)) {
|
|
24
|
+
const because = blocked.get(name) ?? [];
|
|
25
|
+
direct.set(name, {
|
|
26
|
+
status: "error",
|
|
27
|
+
reason: `${because.join(", ")} failed`,
|
|
28
|
+
blockedBy: because,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else if (starting.has(name)) {
|
|
32
|
+
direct.set(name, { status: "pending" });
|
|
33
|
+
}
|
|
34
|
+
else if (deferred.has(name) &&
|
|
35
|
+
isTiltResourcePending(item.status?.updateStatus ?? "", item.status?.runtimeStatus ?? "")) {
|
|
36
|
+
direct.set(name, { status: "unknown", reason: "starts on its first request" });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
direct.set(name, { status: "ready" });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const result = {};
|
|
43
|
+
for (const [name, state] of direct) {
|
|
44
|
+
if (state.status !== "ready") {
|
|
45
|
+
result[name] = state;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const deps = [...transitiveDependencies(name, dependsOn)].filter((dep) => direct.has(dep));
|
|
49
|
+
const failedDeps = deps.filter((dep) => direct.get(dep)?.status === "error").sort();
|
|
50
|
+
const waitingDeps = deps.filter((dep) => direct.get(dep)?.status === "pending").sort();
|
|
51
|
+
if (failedDeps.length > 0) {
|
|
52
|
+
result[name] = {
|
|
53
|
+
status: "error",
|
|
54
|
+
reason: `${failedDeps.join(", ")} failed`,
|
|
55
|
+
blockedBy: failedDeps,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
else if (waitingDeps.length > 0) {
|
|
59
|
+
result[name] = { status: "pending", reason: `waiting for ${waitingDeps.join(", ")}` };
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
result[name] = state;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=service-runtime-state.js.map
|
|
@@ -6,6 +6,7 @@ import { errorFactories } from "./errors.js";
|
|
|
6
6
|
import { formatCount } from "./formatting.js";
|
|
7
7
|
import { findProjectRoot } from "./paths.js";
|
|
8
8
|
import { validateServiceManifest } from "./service-manifest.js";
|
|
9
|
+
import { deriveStackStatus } from "./stack-status.js";
|
|
9
10
|
import { includes } from "./validation.js";
|
|
10
11
|
const RESOURCE_JSON_FILENAME = "service.json";
|
|
11
12
|
function findServiceJsonFiles(dir, maxDepth = 5, currentDepth = 0) {
|
|
@@ -338,21 +339,7 @@ export function getStackMetadata(stack) {
|
|
|
338
339
|
const timestamps = resourcesMetadata.map((r) => new Date(r.createdAt).getTime());
|
|
339
340
|
const earliestTimestamp = Math.min(...timestamps);
|
|
340
341
|
const latestTimestamp = Math.max(...timestamps);
|
|
341
|
-
const
|
|
342
|
-
let overallStatus = "unknown";
|
|
343
|
-
if (totalResources > 0) {
|
|
344
|
-
const readyCount = resourcesMetadata.filter((r) => r.status === "ready").length;
|
|
345
|
-
const ratio = readyCount / totalResources;
|
|
346
|
-
if (ratio > 0.9) {
|
|
347
|
-
overallStatus = "healthy";
|
|
348
|
-
}
|
|
349
|
-
else if (ratio > 0.5) {
|
|
350
|
-
overallStatus = "degraded";
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
overallStatus = "error";
|
|
354
|
-
}
|
|
355
|
-
}
|
|
342
|
+
const overallStatus = deriveStackStatus(resourcesMetadata.map((r) => r.status));
|
|
356
343
|
const metadata = {
|
|
357
344
|
name: stack.name,
|
|
358
345
|
resourceCount: resourcesMetadata.length,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a load and applies its result, but starts nothing while a previous load is still running. A poll on a timer
|
|
3
|
+
* therefore never overlaps itself: results cannot arrive out of order, and a slow Tilt cannot make every poll get
|
|
4
|
+
* superseded before it finishes.
|
|
5
|
+
*/
|
|
6
|
+
export function singleFlight(apply) {
|
|
7
|
+
let running = false;
|
|
8
|
+
return async (load) => {
|
|
9
|
+
if (running)
|
|
10
|
+
return;
|
|
11
|
+
running = true;
|
|
12
|
+
try {
|
|
13
|
+
apply(await load());
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
running = false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=single-flight.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stack with no known service status has not been checked, so it is "unknown", not "error". Once any service has a
|
|
3
|
+
* status, the share of ready services decides it.
|
|
4
|
+
*/
|
|
5
|
+
export function deriveStackStatus(statuses) {
|
|
6
|
+
if (statuses.length === 0 || statuses.every((status) => status === "unknown"))
|
|
7
|
+
return "unknown";
|
|
8
|
+
const ratio = statuses.filter((status) => status === "ready").length / statuses.length;
|
|
9
|
+
if (ratio > 0.9)
|
|
10
|
+
return "healthy";
|
|
11
|
+
if (ratio > 0.5)
|
|
12
|
+
return "degraded";
|
|
13
|
+
return "error";
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=stack-status.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { parseTiltResourceFailures, summarizeTiltBuildError } from "./doctor-runtime.js";
|
|
2
|
+
import { isTiltResourcePending } from "./tilt-resource-state.js";
|
|
3
|
+
import { onlyEnabledResources } from "./up-readiness.js";
|
|
4
|
+
export function transitiveDependencies(name, dependsOn) {
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
const visit = (current) => {
|
|
7
|
+
for (const dep of dependsOn[current] ?? []) {
|
|
8
|
+
if (seen.has(dep))
|
|
9
|
+
continue;
|
|
10
|
+
seen.add(dep);
|
|
11
|
+
visit(dep);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
visit(name);
|
|
15
|
+
return seen;
|
|
16
|
+
}
|
|
17
|
+
function isPending(item) {
|
|
18
|
+
return isTiltResourcePending(item.status?.updateStatus ?? "", item.status?.runtimeStatus ?? "");
|
|
19
|
+
}
|
|
20
|
+
export function buildStartupReport(jsonText, dependsOn, deferred = new Set()) {
|
|
21
|
+
const enabledJson = onlyEnabledResources(jsonText);
|
|
22
|
+
const failures = parseTiltResourceFailures(enabledJson, deferred).failures;
|
|
23
|
+
const failed = failures.map((failure) => ({
|
|
24
|
+
name: failure.name,
|
|
25
|
+
message: failure.error
|
|
26
|
+
? summarizeTiltBuildError(failure.error)
|
|
27
|
+
: `${failure.updateStatus}/${failure.runtimeStatus}`,
|
|
28
|
+
}));
|
|
29
|
+
const failedNames = new Set(failed.map((entry) => entry.name));
|
|
30
|
+
const items = JSON.parse(enabledJson).items ?? [];
|
|
31
|
+
const blocked = [];
|
|
32
|
+
const starting = [];
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
const name = item.metadata?.name ?? "unknown";
|
|
35
|
+
if (name === "(Tiltfile)" || failedNames.has(name) || deferred.has(name) || !isPending(item)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const because = [...transitiveDependencies(name, dependsOn)]
|
|
39
|
+
.filter((dep) => failedNames.has(dep))
|
|
40
|
+
.sort();
|
|
41
|
+
if (because.length > 0)
|
|
42
|
+
blocked.push({ name, because });
|
|
43
|
+
else
|
|
44
|
+
starting.push(name);
|
|
45
|
+
}
|
|
46
|
+
return { failed, blocked, starting };
|
|
47
|
+
}
|
|
48
|
+
/** A failure exists and everything still pending is blocked by it, so waiting longer cannot help. */
|
|
49
|
+
export function isStartupStalled(jsonText, dependsOn, deferred = new Set()) {
|
|
50
|
+
const report = buildStartupReport(jsonText, dependsOn, deferred);
|
|
51
|
+
return report.failed.length > 0 && report.starting.length === 0;
|
|
52
|
+
}
|
|
53
|
+
export function formatStartupReport(report, timedOut) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
for (const { name, message } of report.failed)
|
|
56
|
+
lines.push(`✗ ${name} failed: ${message}`);
|
|
57
|
+
for (const { name, because } of report.blocked) {
|
|
58
|
+
lines.push(`✗ ${name} is not ready because ${because.join(", ")} failed`);
|
|
59
|
+
}
|
|
60
|
+
if (report.starting.length > 0) {
|
|
61
|
+
lines.push(`${timedOut ? "Timed out waiting for" : "Still starting:"} ${report.starting.join(", ")}`);
|
|
62
|
+
}
|
|
63
|
+
if (lines.length > 0) {
|
|
64
|
+
lines.push("The environment is not ready. Tilt is still running; inspect it or run: tdk down");
|
|
65
|
+
}
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
//# 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,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,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():
|
|
@@ -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.
|