@tdk-landscape/tdk-cli-core 1.3.103 → 1.3.105
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 +1 -1
- package/cli/dist/commands/doctor.js +26 -2
- package/cli/dist/commands/down.js +13 -4
- package/cli/dist/commands/status.js +13 -0
- package/cli/dist/commands/ui.js +1 -1
- package/cli/dist/commands/up.js +68 -3
- package/cli/dist/utils/agent-host.js +35 -0
- package/cli/dist/utils/doctor-report.js +14 -1
- package/cli/dist/utils/json-output.js +27 -0
- package/cli/dist/utils/service-manifest.js +1 -0
- package/cli/dist/utils/status-ports.js +32 -0
- package/cli/dist/utils/up-readiness.js +48 -0
- package/cli/package.json +1 -1
- package/discovery/manifest/loading.star +6 -1
- package/engine/schemas/service-schema.json +4 -0
- package/engine/topologies/platform/docker/compose/compose.star +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@tdk-landscape/tdk-cli-core)
|
|
4
4
|
[](https://packagephobia.com/result?p=@tdk-landscape/tdk-cli-core)
|
|
5
|
-
[](https://packagephobia.com/result?p=@tdk-landscape/tdk-cli-core)
|
|
6
6
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml)
|
|
7
7
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml)
|
|
8
8
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/erp-scale-e2e.yml)
|
|
@@ -4,6 +4,7 @@ import { dirname, join, normalize, relative } from "node:path";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
|
|
7
|
+
import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_DOCS, WEBCONTAINER_UP_MESSAGE, } from "../utils/agent-host.js";
|
|
7
8
|
import { MASTER_CONFIG_FILES, REQUIRED_PACKAGE_SCRIPTS } from "../utils/constants.js";
|
|
8
9
|
import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
|
|
9
10
|
import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
|
|
@@ -1005,6 +1006,7 @@ export const doctorCommand = new Command("doctor")
|
|
|
1005
1006
|
if (!options.json) {
|
|
1006
1007
|
console.log("TDK doctor checks your local Docker + Tilt development environment; it does not check cluster deployments.");
|
|
1007
1008
|
}
|
|
1009
|
+
const host = detectHost();
|
|
1008
1010
|
if (process.platform === "win32") {
|
|
1009
1011
|
if (options.json) {
|
|
1010
1012
|
let windowsPortPlan = null;
|
|
@@ -1021,12 +1023,27 @@ export const doctorCommand = new Command("doctor")
|
|
|
1021
1023
|
message: NATIVE_WINDOWS_DOCTOR_MESSAGE,
|
|
1022
1024
|
fix: "Use WSL2 Ubuntu with Docker Desktop integration. Guide: docs/wsl2.md",
|
|
1023
1025
|
},
|
|
1024
|
-
], Boolean(findProjectRoot()), [], windowsPortPlan)));
|
|
1026
|
+
], Boolean(findProjectRoot()), [], windowsPortPlan, { ...host, canUp: false })));
|
|
1025
1027
|
}
|
|
1026
1028
|
console.error(NATIVE_WINDOWS_DOCTOR_MESSAGE);
|
|
1027
1029
|
process.exit(1);
|
|
1028
1030
|
return;
|
|
1029
1031
|
}
|
|
1032
|
+
if (host.kind === "webcontainer") {
|
|
1033
|
+
const report = createDoctorReport([
|
|
1034
|
+
{
|
|
1035
|
+
name: "Host",
|
|
1036
|
+
didPass: false,
|
|
1037
|
+
message: WEBCONTAINER_UP_MESSAGE,
|
|
1038
|
+
fix: `Use a machine with Docker. Guide: ${WEBCONTAINER_DOCS}`,
|
|
1039
|
+
},
|
|
1040
|
+
], Boolean(findProjectRoot()), [], undefined, host);
|
|
1041
|
+
if (options.json)
|
|
1042
|
+
console.log(JSON.stringify(report));
|
|
1043
|
+
console.error(WEBCONTAINER_UP_MESSAGE);
|
|
1044
|
+
process.exit(1);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1030
1047
|
if (!options.json) {
|
|
1031
1048
|
console.log(`\n${chalk.bold("🔍 TDK Doctor")}\n`);
|
|
1032
1049
|
console.log("Checking environment...\n");
|
|
@@ -1113,7 +1130,14 @@ export const doctorCommand = new Command("doctor")
|
|
|
1113
1130
|
// would all fail with "run tdk project".
|
|
1114
1131
|
const inProject = Boolean(projectRoot);
|
|
1115
1132
|
const { checks: results, errors } = await collectDoctorChecks(machineChecks, inProject ? projectChecks : []);
|
|
1116
|
-
|
|
1133
|
+
if (isContainerHost(host.kind)) {
|
|
1134
|
+
for (const result of results) {
|
|
1135
|
+
if (result.name === "Container Runtime" && !result.didPass) {
|
|
1136
|
+
result.fix = DEVCONTAINER_DOCKER_FIX;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const report = createDoctorReport(orderDoctorResults(results), inProject, errors, hostPortPlan, host);
|
|
1117
1141
|
const exitCode = getDoctorExitCode(report);
|
|
1118
1142
|
const allPassed = report.data.ready;
|
|
1119
1143
|
if (options.json) {
|
|
@@ -2,31 +2,40 @@ import chalk from "chalk";
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { handleDryRun } from "../utils/command-helpers.js";
|
|
4
4
|
import { handleTiltFailure, withTiltCheck } from "../utils/errors.js";
|
|
5
|
+
import { createJsonEmitter } from "../utils/json-output.js";
|
|
5
6
|
import { buildTiltDownArgs, runTilt } from "../utils/tilt.js";
|
|
6
7
|
export const downCommand = new Command("down")
|
|
7
8
|
.description("Stop all tilt resources")
|
|
8
9
|
.option("-v, --verbose", "Enable verbose output", false)
|
|
9
10
|
.option("-f, --force", "Skip confirmation", false)
|
|
10
11
|
.option("--dry-run", "Show what would be stopped without stopping", false)
|
|
12
|
+
.option("--json", "Print one JSON object on stdout; implies quiet human output", false)
|
|
11
13
|
.action(async (options) => {
|
|
14
|
+
const emit = options.json ? createJsonEmitter("DOWN_FAILED", "tdk down") : undefined;
|
|
12
15
|
await withTiltCheck(async () => {
|
|
16
|
+
if (emit && options.dryRun) {
|
|
17
|
+
emit({ ok: true, dryRun: true });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
13
20
|
if (handleDryRun(options, "not stopping resources", "tilt down")) {
|
|
14
21
|
return;
|
|
15
22
|
}
|
|
16
23
|
const tiltArgs = buildTiltDownArgs({
|
|
17
24
|
force: options.force,
|
|
18
25
|
});
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
if (!options.json)
|
|
27
|
+
console.log(chalk.blue("Stopping all tilt resources..."));
|
|
28
|
+
if (options.verbose && !options.json) {
|
|
21
29
|
console.log(chalk.gray("Running: tilt down -f .tdk/.tdk-out/Tiltfile"));
|
|
22
30
|
}
|
|
23
31
|
const result = await runTilt("down", tiltArgs, {
|
|
24
|
-
verbose: options.verbose,
|
|
25
|
-
inheritStdio:
|
|
32
|
+
verbose: options.verbose && !options.json,
|
|
33
|
+
inheritStdio: !options.json,
|
|
26
34
|
});
|
|
27
35
|
if (result.exitCode !== 0) {
|
|
28
36
|
handleTiltFailure("down", result.exitCode);
|
|
29
37
|
}
|
|
38
|
+
emit?.({ ok: true });
|
|
30
39
|
});
|
|
31
40
|
});
|
|
32
41
|
//# sourceMappingURL=down.js.map
|
|
@@ -3,7 +3,10 @@ import { Command } from "commander";
|
|
|
3
3
|
import { createDiscoveryContext } from "../utils/discovery-context.js";
|
|
4
4
|
import { runCommand } from "../utils/errors.js";
|
|
5
5
|
import { formatCount, showDetail, showStep } from "../utils/formatting.js";
|
|
6
|
+
import { getHostPortPlan } from "../utils/host-port-config.js";
|
|
6
7
|
import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
|
|
8
|
+
import { findProjectRoot } from "../utils/paths.js";
|
|
9
|
+
import { buildServicePorts, buildStackPorts } from "../utils/status-ports.js";
|
|
7
10
|
import { getTiltfilePath, isTiltAvailable, runTilt } from "../utils/tilt.js";
|
|
8
11
|
export const statusCommand = new Command("status")
|
|
9
12
|
.description("Show status of resources and stacks")
|
|
@@ -51,6 +54,14 @@ export const statusCommand = new Command("status")
|
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
if (options.json) {
|
|
57
|
+
let portPlan = null;
|
|
58
|
+
try {
|
|
59
|
+
const root = findProjectRoot();
|
|
60
|
+
portPlan = root ? await getHostPortPlan(root, { inspectDocker: false }) : null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Port planning is best effort; stack ports then list only the Tilt UI.
|
|
64
|
+
}
|
|
54
65
|
const errors = queryError ? [{ code: "TILT_STATUS_UNAVAILABLE", message: queryError }] : [];
|
|
55
66
|
const data = {
|
|
56
67
|
tilt: {
|
|
@@ -63,7 +74,9 @@ export const statusCommand = new Command("status")
|
|
|
63
74
|
stack: resource.stack ?? null,
|
|
64
75
|
type: resource.type ?? "unknown",
|
|
65
76
|
port: resource.port ?? null,
|
|
77
|
+
...buildServicePorts(resource, portPlan?.ingressHttp),
|
|
66
78
|
})),
|
|
79
|
+
ports: buildStackPorts(portPlan, process.env.TILT_PORT ? Number.parseInt(process.env.TILT_PORT, 10) : undefined),
|
|
67
80
|
stacks: discovery.stacks.map((stack) => ({
|
|
68
81
|
name: stack.name,
|
|
69
82
|
resourceCount: stack.resourceCount,
|
package/cli/dist/commands/ui.js
CHANGED
|
@@ -12,7 +12,7 @@ const HelpPanel = ({ onClose }) => (_jsxs(Box, { borderStyle: "single", borderCo
|
|
|
12
12
|
const LoadingScreen = ({ progress, message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { children: ["Loading: ", message] }), _jsxs(Box, { marginY: 1, borderStyle: "single", borderColor: "gray", width: 50, children: [_jsx(Box, { width: progress / 2, backgroundColor: "cyan", children: _jsx(Text, { children: " ".repeat(progress / 2) }) }), _jsxs(Text, { children: [" ", progress, "%"] })] })] }));
|
|
13
13
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
14
14
|
const ErrorScreen = ({ error, onRetry }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "red", children: "Could Not Load Services" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: "red", children: ["\u2717 ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "Troubleshooting:" }), _jsx(Text, { color: "gray", children: " 1. Check that every service.json is valid JSON" }), _jsx(Text, { color: "gray", children: " 2. Run tdk from your project (the folder with the Tiltfile)" }), _jsx(Text, { color: "gray", children: " 3. Try: tdk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to retry or [q] to quit" })] }));
|
|
15
|
-
const EmptyState = ({ message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk
|
|
15
|
+
const EmptyState = ({ message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk project" }), _jsx(Text, { children: " 2. Run: tdk resource api --type backend" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" }), message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2593\u2592\u2591 ", message, " \u2591\u2592\u2593"] }) }))] }));
|
|
16
16
|
const TUIApp = () => {
|
|
17
17
|
const { exit } = useApp();
|
|
18
18
|
const { stdout } = useStdout();
|
package/cli/dist/commands/up.js
CHANGED
|
@@ -3,12 +3,14 @@ import { join } from "node:path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { ensureProjectRuntimeAssets } from "../generator/template-engine.js";
|
|
6
|
+
import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_UP_MESSAGE, } from "../utils/agent-host.js";
|
|
6
7
|
import { handleDryRun } from "../utils/command-helpers.js";
|
|
7
8
|
import { completeEnvFile } from "../utils/env-validator.js";
|
|
8
9
|
import { errorFactories, handleTiltFailure, requireProjectRoot, runCommand, showErrorAndExit, withTiltCheck, } from "../utils/errors.js";
|
|
9
10
|
import { formatCount } from "../utils/formatting.js";
|
|
10
11
|
import { exportHostPortPlan, getHostPortPlan, writeSavedHostPortPlan, } from "../utils/host-port-config.js";
|
|
11
12
|
import { formatHostPortPlan } from "../utils/host-port-plan.js";
|
|
13
|
+
import { createJsonEmitter } from "../utils/json-output.js";
|
|
12
14
|
import { findProjectRoot } from "../utils/paths.js";
|
|
13
15
|
import { findAvailablePort } from "../utils/port-assignment.js";
|
|
14
16
|
import { isApiServiceType } from "../utils/resource-kind.js";
|
|
@@ -17,6 +19,7 @@ import { discoverResources, discoverStacks, getResourcesForStack, stackExists, }
|
|
|
17
19
|
import { buildSmokePlans, formatSmokeFailure, runSmokePlans } from "../utils/smoke.js";
|
|
18
20
|
import { buildTiltUpArgs, runTilt } from "../utils/tilt.js";
|
|
19
21
|
import { stopTiltOnPort } from "../utils/tilt-process.js";
|
|
22
|
+
import { waitForTiltResourcesReady } from "../utils/up-readiness.js";
|
|
20
23
|
import { enableDiscoveredStacks } from "./project.js";
|
|
21
24
|
export function formatUpSuccess(port, appUrls = []) {
|
|
22
25
|
return [
|
|
@@ -34,6 +37,20 @@ export function nativeWindowsUpRefusal(platform, allowNativeWindows) {
|
|
|
34
37
|
return null;
|
|
35
38
|
return "Landscape startup needs Ubuntu on WSL2. Native Windows is inspect-only.";
|
|
36
39
|
}
|
|
40
|
+
/** Why `tdk up` cannot work on this host, or null when it may proceed. Docker reachability is only probed in container hosts. */
|
|
41
|
+
export async function hostUpRefusal(host = detectHost(), dockerReachable = defaultDockerReachable) {
|
|
42
|
+
if (host.kind === "webcontainer")
|
|
43
|
+
return WEBCONTAINER_UP_MESSAGE;
|
|
44
|
+
if (isContainerHost(host.kind) && !(await dockerReachable()))
|
|
45
|
+
return DEVCONTAINER_DOCKER_FIX;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
async function defaultDockerReachable() {
|
|
49
|
+
const { execFile } = await import("node:child_process");
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
execFile("docker", ["ps"], { timeout: 10_000 }, (error) => resolve(!error));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
37
54
|
function waitForTiltUi(port, timeoutMs = 30_000) {
|
|
38
55
|
return new Promise((resolve) => {
|
|
39
56
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -64,7 +81,16 @@ export const upCommand = new Command("up")
|
|
|
64
81
|
.option("-q, --quiet", "Suppress non-essential output", false)
|
|
65
82
|
.option("--dry-run", "Show what would be started without starting", false)
|
|
66
83
|
.option("-f, --force", "Kill existing Tilt process before starting", false)
|
|
84
|
+
.option("--json", "Print one JSON object on stdout when the stack is ready or the command fails; implies --quiet", false)
|
|
67
85
|
.action(async (stackName, options) => {
|
|
86
|
+
const emit = options.json ? createJsonEmitter("UP_FAILED", "tdk up") : undefined;
|
|
87
|
+
if (options.json)
|
|
88
|
+
options.quiet = true;
|
|
89
|
+
const hostRefusal = options.dryRun ? null : await hostUpRefusal();
|
|
90
|
+
if (hostRefusal) {
|
|
91
|
+
emit?.({ ok: false }, [{ code: "HOST_UNSUPPORTED", message: hostRefusal }]);
|
|
92
|
+
showErrorAndExit(hostRefusal);
|
|
93
|
+
}
|
|
68
94
|
const platformRefusal = nativeWindowsUpRefusal(process.platform, process.env.TDK_ALLOW_NATIVE_WINDOWS);
|
|
69
95
|
if (platformRefusal)
|
|
70
96
|
showErrorAndExit(platformRefusal);
|
|
@@ -152,6 +178,16 @@ export const upCommand = new Command("up")
|
|
|
152
178
|
console.log(chalk.blue(formatHostPortPlan(hostPortPlan)));
|
|
153
179
|
console.log(chalk.gray("Override with TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT."));
|
|
154
180
|
}
|
|
181
|
+
if (emit && options.dryRun) {
|
|
182
|
+
emit({
|
|
183
|
+
ok: true,
|
|
184
|
+
dryRun: true,
|
|
185
|
+
stack: stackName ?? null,
|
|
186
|
+
services: serviceNames,
|
|
187
|
+
command: dryRunCommand,
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
155
191
|
if (handleDryRun(options, "not starting services", dryRunCommand)) {
|
|
156
192
|
return;
|
|
157
193
|
}
|
|
@@ -191,7 +227,7 @@ export const upCommand = new Command("up")
|
|
|
191
227
|
console.log(chalk.gray(`Using Tiltfile: .tdk/.tdk-out/Tiltfile`));
|
|
192
228
|
}
|
|
193
229
|
const tiltRun = runTilt("up", tiltArgs, {
|
|
194
|
-
verbose: options.verbose,
|
|
230
|
+
verbose: options.verbose && !options.json,
|
|
195
231
|
quiet: options.quiet,
|
|
196
232
|
inheritStdio: !options.quiet, // Suppress tilt output in quiet mode
|
|
197
233
|
});
|
|
@@ -202,12 +238,29 @@ export const upCommand = new Command("up")
|
|
|
202
238
|
let printedSuccess = false;
|
|
203
239
|
const successOutput = (async () => {
|
|
204
240
|
const uiReady = await waitForTiltUi(port);
|
|
241
|
+
// JSON consumers get success only after every non-deferred resource is built and running, not when the UI port opens.
|
|
242
|
+
let jsonReady = false;
|
|
243
|
+
if (uiReady && emit) {
|
|
244
|
+
const readiness = await waitForTiltResourcesReady(port);
|
|
245
|
+
if (readiness.ready) {
|
|
246
|
+
jsonReady = true;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const detail = readiness.failures.map((f) => `${f.name}: ${f.message}`).join("; ");
|
|
250
|
+
emit({ ok: false, tiltUrl: `http://localhost:${port}`, failures: readiness.failures }, [
|
|
251
|
+
{
|
|
252
|
+
code: readiness.timedOut ? "UP_TIMEOUT" : "UP_RESOURCE_FAILED",
|
|
253
|
+
message: `${readiness.timedOut ? "Timed out waiting for resources" : "Resources failed"}${detail ? ` (${detail})` : ""}. Tilt is still running; inspect it or run: tdk down`,
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
205
258
|
if (uiReady && !options.quiet) {
|
|
206
259
|
for (const line of formatUpSuccess(port))
|
|
207
260
|
console.log(chalk.blue(line));
|
|
208
261
|
printedSuccess = true;
|
|
209
262
|
}
|
|
210
|
-
if (uiReady && smokePlans.length > 0) {
|
|
263
|
+
if (uiReady && smokePlans.length > 0 && (!emit || jsonReady)) {
|
|
211
264
|
if (!options.quiet) {
|
|
212
265
|
console.log(chalk.gray(`Smoke check: ${smokePlans.map((p) => p.name).join(", ")}`));
|
|
213
266
|
}
|
|
@@ -224,8 +277,20 @@ export const upCommand = new Command("up")
|
|
|
224
277
|
console.error(chalk.red(formatSmokeFailure(smokeResult)));
|
|
225
278
|
}
|
|
226
279
|
}
|
|
227
|
-
if (smokeFailed)
|
|
280
|
+
if (smokeFailed) {
|
|
228
281
|
stopTiltOnPort(port);
|
|
282
|
+
emit?.({ ok: false }, [
|
|
283
|
+
{ code: "UP_SMOKE_FAILED", message: "A smoke check failed; Tilt was stopped" },
|
|
284
|
+
]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (uiReady && jsonReady && !smokeFailed) {
|
|
288
|
+
emit?.({
|
|
289
|
+
ok: true,
|
|
290
|
+
stack: stackName ?? null,
|
|
291
|
+
services: serviceNames,
|
|
292
|
+
tiltUrl: `http://localhost:${port}`,
|
|
293
|
+
});
|
|
229
294
|
}
|
|
230
295
|
})();
|
|
231
296
|
const result = await tiltRun;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
export const WEBCONTAINER_DOCS = "docs/agent-hosts.md";
|
|
3
|
+
export const WEBCONTAINER_UP_MESSAGE = `WebContainers cannot run TDK: there is no Docker daemon or Tilt. Run \`tdk up\` on a machine with Docker. See ${WEBCONTAINER_DOCS}.`;
|
|
4
|
+
export const DEVCONTAINER_DOCKER_FIX = `Docker is not reachable from this Dev Container. Mount the host Docker socket (-v /var/run/docker.sock:/var/run/docker.sock) or add the docker-outside-of-docker feature to devcontainer.json, then retry. Docker-in-docker is not supported. See ${WEBCONTAINER_DOCS}.`;
|
|
5
|
+
function defaultInputs() {
|
|
6
|
+
return {
|
|
7
|
+
env: process.env,
|
|
8
|
+
versions: process.versions,
|
|
9
|
+
platform: process.platform,
|
|
10
|
+
fileExists: existsSync,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Classify where the CLI is running. Informational: Docker reachability is still decided by the runtime checks. */
|
|
14
|
+
export function detectHost(inputs = defaultInputs()) {
|
|
15
|
+
const { env, versions, platform, fileExists } = inputs;
|
|
16
|
+
if (versions.webcontainer) {
|
|
17
|
+
return { kind: "webcontainer", canUp: false };
|
|
18
|
+
}
|
|
19
|
+
if (env.CODESPACES === "true")
|
|
20
|
+
return { kind: "codespaces", canUp: true };
|
|
21
|
+
if (env.REMOTE_CONTAINERS || (fileExists("/.dockerenv") && env.DEVCONTAINER)) {
|
|
22
|
+
return { kind: "devcontainer", canUp: true };
|
|
23
|
+
}
|
|
24
|
+
// Mirrors nativeWindowsUpRefusal in commands/up.ts: native Windows is inspect-only unless explicitly overridden.
|
|
25
|
+
if (platform === "win32") {
|
|
26
|
+
return { kind: "native-windows", canUp: env.TDK_ALLOW_NATIVE_WINDOWS === "1" };
|
|
27
|
+
}
|
|
28
|
+
if (platform === "linux" && env.WSL_DISTRO_NAME)
|
|
29
|
+
return { kind: "wsl2", canUp: true };
|
|
30
|
+
return { kind: "local", canUp: true };
|
|
31
|
+
}
|
|
32
|
+
export function isContainerHost(kind) {
|
|
33
|
+
return kind === "devcontainer" || kind === "codespaces";
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=agent-host.js.map
|
|
@@ -20,8 +20,12 @@ export async function collectDoctorChecks(machineChecks, projectChecks) {
|
|
|
20
20
|
}
|
|
21
21
|
return { checks, errors };
|
|
22
22
|
}
|
|
23
|
+
function containerRuntimeReachable(checks) {
|
|
24
|
+
const runtime = checks.find((check) => check.name === "Container Runtime");
|
|
25
|
+
return runtime ? runtime.didPass : null;
|
|
26
|
+
}
|
|
23
27
|
/** Produce the same readiness decision for human and machine consumers. */
|
|
24
|
-
export function createDoctorReport(checks, inProject, errors = [], portPlan) {
|
|
28
|
+
export function createDoctorReport(checks, inProject, errors = [], portPlan, host) {
|
|
25
29
|
return {
|
|
26
30
|
schemaVersion: 1,
|
|
27
31
|
data: {
|
|
@@ -29,6 +33,15 @@ export function createDoctorReport(checks, inProject, errors = [], portPlan) {
|
|
|
29
33
|
checks.every((check) => check.didPass || check.isSkipped || check.isWarning),
|
|
30
34
|
inProject,
|
|
31
35
|
checks,
|
|
36
|
+
...(host
|
|
37
|
+
? {
|
|
38
|
+
host: {
|
|
39
|
+
...host,
|
|
40
|
+
containerRuntimeReachable: containerRuntimeReachable(checks),
|
|
41
|
+
canUp: host.canUp && containerRuntimeReachable(checks) !== false,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
: {}),
|
|
32
45
|
...(portPlan !== undefined
|
|
33
46
|
? {
|
|
34
47
|
ports: {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { writeSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Returns an emitter that writes at most one `{schemaVersion, data, errors}` object to stdout. If the process
|
|
4
|
+
* exits before anything was emitted (a preflight `process.exit`, Tilt ending early), a failure object is written
|
|
5
|
+
* so a JSON consumer never sees an empty stdout. Uses writeSync because `process.exit` does not flush async writes.
|
|
6
|
+
*/
|
|
7
|
+
export function createJsonEmitter(failureCode, command) {
|
|
8
|
+
let emitted = false;
|
|
9
|
+
const emit = (data, errors = []) => {
|
|
10
|
+
if (emitted)
|
|
11
|
+
return;
|
|
12
|
+
emitted = true;
|
|
13
|
+
writeSync(1, `${JSON.stringify({ schemaVersion: 1, data, errors })}\n`);
|
|
14
|
+
};
|
|
15
|
+
process.once("exit", (code) => {
|
|
16
|
+
emit({ ok: false }, [
|
|
17
|
+
{
|
|
18
|
+
code: failureCode,
|
|
19
|
+
message: code === 0
|
|
20
|
+
? `${command} exited before reporting a result`
|
|
21
|
+
: `${command} exited with code ${code}`,
|
|
22
|
+
},
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
return emit;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=json-output.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { STANDARD_PORTS } from "./constants.js";
|
|
2
|
+
import { isApiServiceType } from "./resource-kind.js";
|
|
3
|
+
import { resolveServicePath, resolveSubdomainBases } from "./service-urls.js";
|
|
4
|
+
export function buildServicePorts(resource, ingressHttp) {
|
|
5
|
+
const appType = resource.config?.appType;
|
|
6
|
+
const routable = appType === "frontend" || isApiServiceType(appType);
|
|
7
|
+
let url = null;
|
|
8
|
+
if (routable) {
|
|
9
|
+
const { appBase, apiBase } = resolveSubdomainBases(ingressHttp);
|
|
10
|
+
url = `${isApiServiceType(appType) ? apiBase : appBase}${resolveServicePath(resource)}`;
|
|
11
|
+
}
|
|
12
|
+
return { url, containerPort: resource.port ?? resource.config?.port ?? null, hostPort: null };
|
|
13
|
+
}
|
|
14
|
+
/** Ports a client should forward or open: ingress, Tilt UI, and the published datastore. Not one per service. */
|
|
15
|
+
export function buildStackPorts(plan, tiltPort) {
|
|
16
|
+
const ports = [];
|
|
17
|
+
if (plan) {
|
|
18
|
+
ports.push({ name: "ingress-http", hostPort: plan.ingressHttp, purpose: "Traefik HTTP ingress" }, { name: "ingress-https", hostPort: plan.ingressHttps, purpose: "Traefik HTTPS ingress" });
|
|
19
|
+
}
|
|
20
|
+
// `tdk up` may move Tilt off 10350 and records that only in its own process, so this is the default or TILT_PORT.
|
|
21
|
+
const validTiltPort = tiltPort !== undefined && Number.isInteger(tiltPort) && tiltPort > 0;
|
|
22
|
+
ports.push({
|
|
23
|
+
name: "tilt-ui",
|
|
24
|
+
hostPort: validTiltPort ? tiltPort : STANDARD_PORTS.tiltUi,
|
|
25
|
+
purpose: "Tilt UI",
|
|
26
|
+
});
|
|
27
|
+
if (plan) {
|
|
28
|
+
ports.push({ name: "postgres", hostPort: plan.postgres, purpose: "Published Postgres" });
|
|
29
|
+
}
|
|
30
|
+
return ports;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=status-ports.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { getDeferredResourceNames, parseTiltResourceFailures } from "./doctor-runtime.js";
|
|
3
|
+
import { findOnPath } from "./which.js";
|
|
4
|
+
function tiltGetUiResources(port) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
execFile(findOnPath("tilt") ?? "tilt", ["get", "uiresources", "-o", "json", "--port", String(port)], { timeout: 15_000, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => resolve(error ? null : stdout));
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Waits until every non-deferred Tilt resource is built and running. Resolves not-ready as soon as the rest has settled
|
|
11
|
+
* with an errored resource, or at the deadline. Sablier-deferred resources are excluded: they stay idle by design.
|
|
12
|
+
*/
|
|
13
|
+
export async function waitForTiltResourcesReady(port, options = {}) {
|
|
14
|
+
const timeoutMs = options.timeoutMs ?? Number(process.env.TDK_UP_READY_TIMEOUT_MS ?? 900_000);
|
|
15
|
+
const intervalMs = options.intervalMs ?? 2_000;
|
|
16
|
+
const fetchJson = options.fetchJson ?? tiltGetUiResources;
|
|
17
|
+
const deferred = options.deferred ?? getDeferredResourceNames();
|
|
18
|
+
const deadline = Date.now() + timeoutMs;
|
|
19
|
+
let last = { ready: false, failures: [], pending: 0, timedOut: false };
|
|
20
|
+
for (;;) {
|
|
21
|
+
const text = await fetchJson(port);
|
|
22
|
+
if (text) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = parseTiltResourceFailures(text, deferred);
|
|
25
|
+
last = {
|
|
26
|
+
ready: parsed.failures.length === 0 && parsed.pendingCount === 0 && parsed.okCount > 0,
|
|
27
|
+
failures: parsed.failures.map((failure) => ({
|
|
28
|
+
name: failure.name,
|
|
29
|
+
message: failure.error || `${failure.updateStatus}/${failure.runtimeStatus}`,
|
|
30
|
+
})),
|
|
31
|
+
pending: parsed.pendingCount,
|
|
32
|
+
timedOut: false,
|
|
33
|
+
};
|
|
34
|
+
if (last.ready)
|
|
35
|
+
return last;
|
|
36
|
+
if (last.failures.length > 0 && last.pending === 0)
|
|
37
|
+
return last;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* Tilt may emit partial output while starting; retry. */
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (Date.now() >= deadline)
|
|
44
|
+
return { ...last, ready: false, timedOut: true };
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=up-readiness.js.map
|
package/cli/package.json
CHANGED
|
@@ -233,7 +233,12 @@ def _apply_manifest_defaults(manifest, resource_path):
|
|
|
233
233
|
# Validate custom dockerfile exists if specified
|
|
234
234
|
if 'dockerfile' in result:
|
|
235
235
|
custom_dockerfile = result['dockerfile']
|
|
236
|
-
|
|
236
|
+
# With a bring-your-own buildContext the dockerfile is relative to that context.
|
|
237
|
+
build_context = result.get('buildContext')
|
|
238
|
+
if build_context and build_context != '.':
|
|
239
|
+
dockerfile_path = resource_path + '/' + build_context + '/' + custom_dockerfile
|
|
240
|
+
else:
|
|
241
|
+
dockerfile_path = resource_path + '/' + custom_dockerfile
|
|
237
242
|
# BUG FIX: Run check from project root since Tilt may run from .tdk/.tdk-out/
|
|
238
243
|
project_root = os.environ.get('TDK_PROJECT_ROOT', '')
|
|
239
244
|
if project_root:
|
|
@@ -162,6 +162,10 @@
|
|
|
162
162
|
"description": "Custom Dockerfile path relative to resource directory. Optional for bring-your-own type.",
|
|
163
163
|
"type": "string"
|
|
164
164
|
},
|
|
165
|
+
"buildContext": {
|
|
166
|
+
"description": "Docker build context for a bring-your-own resource, relative to the resource directory. Defaults to the resource directory. When set, dockerfile is relative to this context.",
|
|
167
|
+
"type": "string"
|
|
168
|
+
},
|
|
165
169
|
"exposeViaProxy": {
|
|
166
170
|
"description": "Whether this bring-your-own resource receives a Traefik route. Defaults to true.",
|
|
167
171
|
"type": "boolean"
|
|
@@ -207,6 +207,12 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
|
|
|
207
207
|
# context instead of the project root used by generated TDK apps.
|
|
208
208
|
context_prefix = '/'.join(['..'] * (len(resource_path.split('/')) + 1))
|
|
209
209
|
build_context = context_prefix + '/' + full_resource_path
|
|
210
|
+
# `buildContext` (relative to the service directory) moves the context
|
|
211
|
+
# to the source tree an imported Dockerfile lives in; `dockerfile` is then
|
|
212
|
+
# relative to that context, as in Compose.
|
|
213
|
+
custom_context = manifest.get('buildContext')
|
|
214
|
+
if custom_context and custom_context != '.':
|
|
215
|
+
build_context = build_context + '/' + custom_context
|
|
210
216
|
dockerfile = manifest.get('dockerfile', 'Dockerfile')
|
|
211
217
|
if dockerfile.startswith('./'):
|
|
212
218
|
dockerfile = dockerfile[2:]
|