@tdk-landscape/tdk-cli-core 1.3.104 → 1.3.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/cli/dist/cli.js +4 -0
- package/cli/dist/commands/doctor.js +28 -3
- package/cli/dist/commands/down.js +13 -4
- package/cli/dist/commands/logs.js +85 -0
- package/cli/dist/commands/mcp.js +24 -0
- package/cli/dist/commands/projects.js +37 -6
- package/cli/dist/commands/resources.js +3 -2
- package/cli/dist/commands/stacks.js +30 -3
- package/cli/dist/commands/status.js +31 -0
- package/cli/dist/commands/ui.js +1 -1
- package/cli/dist/commands/up.js +149 -17
- package/cli/dist/mcp/server.js +100 -0
- package/cli/dist/mcp/tools.js +273 -0
- package/cli/dist/utils/agent-host.js +35 -0
- package/cli/dist/utils/discovery-context.js +9 -3
- package/cli/dist/utils/doctor-report.js +14 -1
- package/cli/dist/utils/doctor-wiring.js +88 -0
- package/cli/dist/utils/json-output.js +27 -0
- package/cli/dist/utils/services.js +65 -7
- package/cli/dist/utils/status-ports.js +32 -0
- package/cli/dist/utils/tilt-logs.js +62 -0
- package/cli/dist/utils/up-only.js +29 -0
- package/cli/dist/utils/up-readiness.js +115 -0
- package/cli/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,7 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- `tdk doctor` warns when a backend or worker applies database migrations from its own start-up (a `start`/`dev` script, a Dockerfile `CMD`/`ENTRYPOINT`, a shell entrypoint or its source: Prisma, Drizzle, Knex, TypeORM, MikroORM, Sequelize, dbmate, Atlas, Alembic). It is a warning, not a failure: such services work, but migrations belong in a `migrator` resource that runs once before the services that depend on it. `migrator` and bring-your-own resources are not checked.
|
|
7
8
|
- Go services can opt in to a live development loop with `"dev": { "liveReload": true }` in `service.json`: edits are copied into the running container and recompiled without an image rebuild, while `go.mod`, `go.sum` and the generated Dockerfile still rebuild the image. The watcher compiles into a staging binary while the old build keeps serving and swaps only on success, so a compile error shows in the Tilt log and the last good build keeps answering (Air, tried first, stopped the app before compiling and left it down for the whole compile; in 2 of 6 edits a Traefik health check then returned 503 for 10 s). Measured on one loaded Mac: median edit-to-ready 1.63 s for the scaffold. The default Go image (`golang:1.23`) now sets `GOTOOLCHAIN=auto`, so a `go.mod` that needs a newer Go, such as Gin 1.12 (`go 1.25`), downloads that toolchain instead of stopping with `requires go >= 1.25`. Offline or air-gapped builds of a go.mod newer than 1.23 still fail, but now at the toolchain download rather than at the version check. `scripts/e2e/go-dev-loop.sh` checks all of this through a real `tdk up`. Refs #369.
|
|
8
9
|
- `tdk up` keeps each smoke step's status and response body under `.tdk/smoke/<service>/<step>/` (`latest.json` and `body.txt`, body capped at 64 KiB), plus `last-success.json` and `last-success-body.txt` that a later failure never overwrites, so a regression can be diffed against the last good response. The failure line ends with the record path, and `tdk project` gitignores `.tdk/smoke/`. A new `smoke-e2e` workflow runs `scripts/verify-smoke.sh` and uploads the records. Before its first step a smoke check now waits for the service's health path (`healthCheckPath`, default `/health` for backends) with GETs until it answers something other than a connection error, 404 or 5xx, so a write is no longer the first request to hit a service that is still building; write retry rules are unchanged.
|
|
9
10
|
- Add an opt-in Python backend: `tdk resource <name> --type backend --language python` scaffolds a FastAPI service (`pyproject.toml`, `src/main.py`, a pytest smoke test) and TDK generates its Docker image (`python:3.12-slim`, with `test` and `production` stages). The choice is saved as `language` in `service.json` only when you pass the flag; omitting it keeps the existing Bun + Hono output byte for byte. Unknown ids and `--language` on non-backend types fail before any file is written. A runnable example is in `examples/one-backend-python`, and the provider contract is in `docs/backend-language-providers.md`. Node.js is not included.
|
package/cli/dist/cli.js
CHANGED
|
@@ -9,7 +9,9 @@ import { downCommand } from "./commands/down.js";
|
|
|
9
9
|
import { ejectCommand } from "./commands/eject.js";
|
|
10
10
|
import { showHelp } from "./commands/help.js";
|
|
11
11
|
import { importCommand } from "./commands/import.js";
|
|
12
|
+
import { logsCommand } from "./commands/logs.js";
|
|
12
13
|
import { maintainersCommand } from "./commands/maintainers.js";
|
|
14
|
+
import { mcpCommand } from "./commands/mcp.js";
|
|
13
15
|
import { networksCommand } from "./commands/networks.js";
|
|
14
16
|
import { projectCommand } from "./commands/project.js";
|
|
15
17
|
import { projectsCommand } from "./commands/projects.js";
|
|
@@ -46,6 +48,8 @@ program.addCommand(resourcesCommand);
|
|
|
46
48
|
program.addCommand(projectsCommand);
|
|
47
49
|
program.addCommand(upCommand);
|
|
48
50
|
program.addCommand(downCommand);
|
|
51
|
+
program.addCommand(mcpCommand);
|
|
52
|
+
program.addCommand(logsCommand);
|
|
49
53
|
program.addCommand(ejectCommand);
|
|
50
54
|
program.addCommand(importCommand);
|
|
51
55
|
program.addCommand(statusCommand);
|
|
@@ -4,11 +4,12 @@ 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";
|
|
10
11
|
import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
|
|
11
|
-
import { checkDockerNetworkCapacity, checkFrontendBackendUrls, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
|
|
12
|
+
import { checkDockerNetworkCapacity, checkFrontendBackendUrls, checkMigrationsInApi, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
|
|
12
13
|
import { validateEnvFile } from "../utils/env-validator.js";
|
|
13
14
|
import { execAsync, isExecTimeout } from "../utils/exec-async.js";
|
|
14
15
|
import { formatCount } from "../utils/formatting.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");
|
|
@@ -1089,6 +1106,7 @@ export const doctorCommand = new Command("doctor")
|
|
|
1089
1106
|
() => checkServiceUrlPorts(),
|
|
1090
1107
|
() => checkFrontendBackendUrls(),
|
|
1091
1108
|
() => checkNatsBroker(),
|
|
1109
|
+
() => checkMigrationsInApi(),
|
|
1092
1110
|
() => checkTiltInstances(),
|
|
1093
1111
|
checkEnvironmentVariables,
|
|
1094
1112
|
// Preflight: Verdaccio down causes ImageBuild bun install ConnectionRefused.
|
|
@@ -1113,7 +1131,14 @@ export const doctorCommand = new Command("doctor")
|
|
|
1113
1131
|
// would all fail with "run tdk project".
|
|
1114
1132
|
const inProject = Boolean(projectRoot);
|
|
1115
1133
|
const { checks: results, errors } = await collectDoctorChecks(machineChecks, inProject ? projectChecks : []);
|
|
1116
|
-
|
|
1134
|
+
if (isContainerHost(host.kind)) {
|
|
1135
|
+
for (const result of results) {
|
|
1136
|
+
if (result.name === "Container Runtime" && !result.didPass) {
|
|
1137
|
+
result.fix = DEVCONTAINER_DOCKER_FIX;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const report = createDoctorReport(orderDoctorResults(results), inProject, errors, hostPortPlan, host);
|
|
1117
1142
|
const exitCode = getDoctorExitCode(report);
|
|
1118
1143
|
const allPassed = report.data.ready;
|
|
1119
1144
|
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
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { STANDARD_PORTS } from "../utils/constants.js";
|
|
3
|
+
import { runCommand, showErrorAndExit } from "../utils/errors.js";
|
|
4
|
+
import { createMachineEnvelope } from "../utils/machine-output.js";
|
|
5
|
+
import { isTiltAvailable, runTilt } from "../utils/tilt.js";
|
|
6
|
+
import { DEFAULT_LOG_TAIL, isTiltConnectionFailure, isValidPort, isValidSince, isValidTail, MAX_LOG_TAIL, parseTiltLogLines, } from "../utils/tilt-logs.js";
|
|
7
|
+
/** Resource names Tilt knows about, or null when they cannot be listed (the logs call then reports the real error). */
|
|
8
|
+
async function tiltResourceNames(port) {
|
|
9
|
+
try {
|
|
10
|
+
const result = await runTilt("get", ["uiresources", "-o", "json", "--port", port], {
|
|
11
|
+
inheritStdio: false,
|
|
12
|
+
});
|
|
13
|
+
if (result.exitCode !== 0)
|
|
14
|
+
return null;
|
|
15
|
+
const items = JSON.parse(result.stdout)
|
|
16
|
+
.items;
|
|
17
|
+
return (items ?? []).flatMap((item) => (item.metadata?.name ? [item.metadata.name] : []));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const logsCommand = new Command("logs")
|
|
24
|
+
.description("Print a bounded snapshot of recent logs from the running stack")
|
|
25
|
+
.option("-s, --service <names...>", "Only these services (Tilt resource names)")
|
|
26
|
+
.option("--tail <n>", "Number of most recent lines to return", String(DEFAULT_LOG_TAIL))
|
|
27
|
+
.option("--since <duration>", "Only logs newer than this duration, for example 30s, 5m, 1h")
|
|
28
|
+
.option("--port <n>", "Tilt UI port (default: TILT_PORT or 10350)")
|
|
29
|
+
.option("--json", "Output one JSON object and exit", false)
|
|
30
|
+
.action(async (options) => {
|
|
31
|
+
const fail = (code, message, exitCode = 1) => {
|
|
32
|
+
if (options.json) {
|
|
33
|
+
console.log(JSON.stringify(createMachineEnvelope(null, [{ code, message }])));
|
|
34
|
+
console.error(message);
|
|
35
|
+
process.exit(exitCode);
|
|
36
|
+
}
|
|
37
|
+
return showErrorAndExit(message, exitCode);
|
|
38
|
+
};
|
|
39
|
+
if (!isValidTail(options.tail))
|
|
40
|
+
fail("USAGE", `--tail must be an integer from 1 to ${MAX_LOG_TAIL}`, 2);
|
|
41
|
+
if (options.since !== undefined && !isValidSince(options.since)) {
|
|
42
|
+
fail("USAGE", "--since must be a duration such as 30s, 5m or 1h", 2);
|
|
43
|
+
}
|
|
44
|
+
const portText = options.port ?? process.env.TILT_PORT ?? String(STANDARD_PORTS.tiltUi);
|
|
45
|
+
if (!isValidPort(portText))
|
|
46
|
+
fail("USAGE", "--port must be a valid port number", 2);
|
|
47
|
+
const tail = Number(options.tail);
|
|
48
|
+
const services = options.service ?? [];
|
|
49
|
+
const action = async () => {
|
|
50
|
+
if (!(await isTiltAvailable())) {
|
|
51
|
+
fail("TILT_MISSING", "Tilt is not installed. Run: tdk doctor");
|
|
52
|
+
}
|
|
53
|
+
if (services.length > 0) {
|
|
54
|
+
const known = await tiltResourceNames(portText);
|
|
55
|
+
const unknown = services.filter((name) => known && !known.includes(name));
|
|
56
|
+
if (unknown.length > 0) {
|
|
57
|
+
fail("UNKNOWN_SERVICE", `Unknown service ${unknown.join(", ")}. Valid names: ${(known ?? []).join(", ")}`, 2);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const args = ["--port", portText, "--tail", String(tail)];
|
|
61
|
+
if (options.since !== undefined)
|
|
62
|
+
args.push("--since", options.since);
|
|
63
|
+
if (options.json)
|
|
64
|
+
args.push("--json");
|
|
65
|
+
// `--` so a service name that starts with "-" is never read as a Tilt flag.
|
|
66
|
+
if (services.length > 0)
|
|
67
|
+
args.push("--", ...services);
|
|
68
|
+
const result = await runTilt("logs", args, { inheritStdio: false });
|
|
69
|
+
if (result.exitCode !== 0) {
|
|
70
|
+
const detail = result.stderr.trim();
|
|
71
|
+
if (!detail || isTiltConnectionFailure(detail)) {
|
|
72
|
+
fail("TILT_NOT_RUNNING", `Could not reach Tilt on port ${portText}: ${detail || "is the stack running? Start it with: tdk up"}`);
|
|
73
|
+
}
|
|
74
|
+
fail("TILT_LOGS_FAILED", `tilt logs failed: ${detail}`);
|
|
75
|
+
}
|
|
76
|
+
if (!options.json) {
|
|
77
|
+
process.stdout.write(result.stdout);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const lines = parseTiltLogLines(result.stdout, tail);
|
|
81
|
+
console.log(JSON.stringify(createMachineEnvelope({ services, tail, since: options.since ?? null, lines })));
|
|
82
|
+
};
|
|
83
|
+
await runCommand(action);
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=logs.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { createMcpServer } from "../mcp/server.js";
|
|
4
|
+
import { createTdkTools } from "../mcp/tools.js";
|
|
5
|
+
import { getPackageVersion } from "../utils/paths.js";
|
|
6
|
+
export const mcpCommand = new Command("mcp")
|
|
7
|
+
.description("Run a Model Context Protocol server over stdio for coding agents")
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const server = createMcpServer(createTdkTools(), { name: "tdk", version: getPackageVersion() });
|
|
10
|
+
// Stdout is the protocol channel. Nothing else may write to it; diagnostics go to stderr.
|
|
11
|
+
const lines = createInterface({ input: process.stdin });
|
|
12
|
+
const pending = new Set();
|
|
13
|
+
lines.on("line", (line) => {
|
|
14
|
+
const task = server.handleLine(line).then((response) => {
|
|
15
|
+
if (response)
|
|
16
|
+
process.stdout.write(`${response}\n`);
|
|
17
|
+
});
|
|
18
|
+
pending.add(task);
|
|
19
|
+
void task.finally(() => pending.delete(task));
|
|
20
|
+
});
|
|
21
|
+
await new Promise((resolve) => lines.once("close", resolve));
|
|
22
|
+
await Promise.allSettled(pending);
|
|
23
|
+
});
|
|
24
|
+
//# sourceMappingURL=mcp.js.map
|
|
@@ -4,21 +4,42 @@ import chalk from "chalk";
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { MASTER_CONFIG_FILES } from "../utils/constants.js";
|
|
6
6
|
import { createDiscoveryContext } from "../utils/discovery-context.js";
|
|
7
|
-
import { requireProjectRoot, runCommand } from "../utils/errors.js";
|
|
7
|
+
import { errorFactories, requireProjectRoot, runCommand } from "../utils/errors.js";
|
|
8
8
|
import { formatCount } from "../utils/formatting.js";
|
|
9
|
+
import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
|
|
10
|
+
import { findProjectRoot } from "../utils/paths.js";
|
|
9
11
|
export const projectsCommand = new Command("projects")
|
|
10
12
|
.description("Show project information and configuration status")
|
|
11
13
|
.alias("info")
|
|
12
14
|
.option("--check", "Check if project is properly configured", false)
|
|
15
|
+
.option("--json", "Output a versioned JSON response")
|
|
13
16
|
.action(async (options) => {
|
|
14
|
-
|
|
15
|
-
const projectRoot = requireProjectRoot();
|
|
17
|
+
const action = async () => {
|
|
18
|
+
const projectRoot = options.json ? findProjectRoot() : requireProjectRoot();
|
|
19
|
+
if (!projectRoot) {
|
|
20
|
+
throw errorFactories.notInProject();
|
|
21
|
+
}
|
|
22
|
+
const outDir = join(projectRoot, ".tdk", ".tdk-out");
|
|
23
|
+
const missing = MASTER_CONFIG_FILES.filter((file) => !existsSync(join(outDir, file)));
|
|
24
|
+
if (options.json) {
|
|
25
|
+
const discovery = createDiscoveryContext();
|
|
26
|
+
console.log(JSON.stringify(createMachineEnvelope({
|
|
27
|
+
projectRoot,
|
|
28
|
+
configured: missing.length === 0,
|
|
29
|
+
missingFiles: missing,
|
|
30
|
+
resources: discovery.resources.length,
|
|
31
|
+
stacks: discovery.stackNames,
|
|
32
|
+
unassignedResources: discovery.unassignedResources.length,
|
|
33
|
+
}), null, 2));
|
|
34
|
+
if (options.check) {
|
|
35
|
+
process.exitCode = missing.length === 0 ? 0 : 1;
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
16
39
|
console.log(chalk.blue("Project Information\n"));
|
|
17
40
|
console.log(chalk.bold("Project Root:"));
|
|
18
41
|
console.log(chalk.gray(` ${projectRoot}`));
|
|
19
42
|
console.log();
|
|
20
|
-
const outDir = join(projectRoot, ".tdk", ".tdk-out");
|
|
21
|
-
const missing = MASTER_CONFIG_FILES.filter((file) => !existsSync(join(outDir, file)));
|
|
22
43
|
console.log(chalk.bold("Master Configuration:"));
|
|
23
44
|
if (missing.length === 0) {
|
|
24
45
|
for (const file of MASTER_CONFIG_FILES) {
|
|
@@ -66,6 +87,16 @@ export const projectsCommand = new Command("projects")
|
|
|
66
87
|
if (options.check) {
|
|
67
88
|
process.exit(missing.length === 0 ? 0 : 1);
|
|
68
89
|
}
|
|
69
|
-
}
|
|
90
|
+
};
|
|
91
|
+
if (options.json) {
|
|
92
|
+
try {
|
|
93
|
+
await action();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
writeMachineError(error);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await runCommand(action);
|
|
70
101
|
});
|
|
71
102
|
//# sourceMappingURL=projects.js.map
|
|
@@ -23,6 +23,7 @@ export const resourcesCommand = new Command("resources")
|
|
|
23
23
|
requireProjectRoot();
|
|
24
24
|
}
|
|
25
25
|
const discovery = createDiscoveryContext();
|
|
26
|
+
const withoutStackFilter = options.noStack || options.stack === false;
|
|
26
27
|
let resources = discovery.resources;
|
|
27
28
|
if (options.stack) {
|
|
28
29
|
resources = discovery.resourcesByStack.get(options.stack) || [];
|
|
@@ -31,7 +32,7 @@ export const resourcesCommand = new Command("resources")
|
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
if (
|
|
35
|
+
if (withoutStackFilter) {
|
|
35
36
|
resources = discovery.unassignedResources;
|
|
36
37
|
if (resources.length === 0 && !options.json) {
|
|
37
38
|
showAllSatisfyCondition("resources", "assigned to a stack");
|
|
@@ -85,7 +86,7 @@ export const resourcesCommand = new Command("resources")
|
|
|
85
86
|
const withoutStackCount = options.stack
|
|
86
87
|
? resources.filter((r) => !r.stack).length
|
|
87
88
|
: discovery.unassignedResources.length;
|
|
88
|
-
if (withoutStackCount > 0 && !
|
|
89
|
+
if (withoutStackCount > 0 && !withoutStackFilter && !options.stack) {
|
|
89
90
|
console.log(chalk.yellow(`\n${formatCount(withoutStackCount, "resource")} not assigned to any stack.`));
|
|
90
91
|
showDetail('Run "tdk resources --no-stack" to see them, or "tdk stack" to assign them.');
|
|
91
92
|
}
|
|
@@ -3,19 +3,36 @@ 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, showEmptyState, showStep } from "../utils/formatting.js";
|
|
6
|
+
import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
|
|
7
|
+
import { findProjectRoot } from "../utils/paths.js";
|
|
6
8
|
export const stacksCommand = new Command("stacks")
|
|
7
9
|
.description("List all stacks and their resources")
|
|
8
10
|
.alias("ls") // Keep 'tdk ls' as shorthand
|
|
9
11
|
.option("-v, --verbose", "Show detailed information about each stack", false)
|
|
10
12
|
.option("--services", "Include list of services in each stack", false)
|
|
13
|
+
.option("--json", "Output a versioned JSON stack report", false)
|
|
11
14
|
.action(async (options) => {
|
|
12
|
-
|
|
15
|
+
const action = async () => {
|
|
16
|
+
if (options.json && !findProjectRoot()) {
|
|
17
|
+
writeMachineError(new Error("Could not find project root (no .tdk/project.json found)"));
|
|
18
|
+
}
|
|
13
19
|
const discovery = createDiscoveryContext();
|
|
20
|
+
const verbose = options.verbose || stacksCommand.parent?.opts().verbose === true;
|
|
21
|
+
if (options.json) {
|
|
22
|
+
const stacks = discovery.stacks.map((stack) => ({
|
|
23
|
+
name: stack.name,
|
|
24
|
+
resourceCount: stack.resourceCount,
|
|
25
|
+
...(verbose ? { description: stack.description } : {}),
|
|
26
|
+
...(options.services ? { services: stack.resources.map((service) => service.name) } : {}),
|
|
27
|
+
}));
|
|
28
|
+
console.log(JSON.stringify(createMachineEnvelope({ stacks })));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
14
31
|
if (discovery.stackNames.length === 0) {
|
|
15
32
|
showEmptyState("stacks");
|
|
16
33
|
return;
|
|
17
34
|
}
|
|
18
|
-
if (
|
|
35
|
+
if (verbose || options.services) {
|
|
19
36
|
showStep(`Found ${formatCount(discovery.stacks.length, "stack")}:\n`);
|
|
20
37
|
for (const stack of discovery.stacks) {
|
|
21
38
|
console.log(chalk.bold(`${stack.name}`));
|
|
@@ -38,6 +55,16 @@ export const stacksCommand = new Command("stacks")
|
|
|
38
55
|
}
|
|
39
56
|
showDetail("\nRun with --verbose for more details, or --services to see all services in each stack.", 0);
|
|
40
57
|
}
|
|
41
|
-
}
|
|
58
|
+
};
|
|
59
|
+
if (options.json) {
|
|
60
|
+
try {
|
|
61
|
+
await action();
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
writeMachineError(error);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await runCommand(action);
|
|
42
69
|
});
|
|
43
70
|
//# sourceMappingURL=stacks.js.map
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { createDiscoveryContext } from "../utils/discovery-context.js";
|
|
4
|
+
import { getDeferredResourceNames } from "../utils/doctor-runtime.js";
|
|
4
5
|
import { runCommand } from "../utils/errors.js";
|
|
5
6
|
import { formatCount, showDetail, showStep } from "../utils/formatting.js";
|
|
7
|
+
import { getHostPortPlan } from "../utils/host-port-config.js";
|
|
6
8
|
import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
|
|
9
|
+
import { findProjectRoot } from "../utils/paths.js";
|
|
10
|
+
import { buildServicePorts, buildStackPorts } from "../utils/status-ports.js";
|
|
7
11
|
import { getTiltfilePath, isTiltAvailable, runTilt } from "../utils/tilt.js";
|
|
12
|
+
import { evaluateTiltReadiness, tiltGetUiResources } from "../utils/up-readiness.js";
|
|
8
13
|
export const statusCommand = new Command("status")
|
|
9
14
|
.description("Show status of resources and stacks")
|
|
10
15
|
.option("--json", "Output a versioned JSON status report", false)
|
|
@@ -50,20 +55,46 @@ export const statusCommand = new Command("status")
|
|
|
50
55
|
queryError = result.stderr || "Could not retrieve Tilt resource status";
|
|
51
56
|
}
|
|
52
57
|
}
|
|
58
|
+
// A single look at the running Tilt, so a caller that started `tdk up` detached can poll for readiness. Null when no
|
|
59
|
+
// Tilt answers or its output cannot be read.
|
|
60
|
+
let readiness = null;
|
|
61
|
+
if (options.json && tiltAvailable) {
|
|
62
|
+
const port = Number.parseInt(process.env.TILT_PORT ?? "", 10);
|
|
63
|
+
const text = await tiltGetUiResources(Number.isInteger(port) ? port : 10350);
|
|
64
|
+
if (text) {
|
|
65
|
+
try {
|
|
66
|
+
readiness = evaluateTiltReadiness(text, getDeferredResourceNames()).result;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
readiness = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
53
73
|
if (options.json) {
|
|
74
|
+
let portPlan = null;
|
|
75
|
+
try {
|
|
76
|
+
const root = findProjectRoot();
|
|
77
|
+
portPlan = root ? await getHostPortPlan(root, { inspectDocker: false }) : null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Port planning is best effort; stack ports then list only the Tilt UI.
|
|
81
|
+
}
|
|
54
82
|
const errors = queryError ? [{ code: "TILT_STATUS_UNAVAILABLE", message: queryError }] : [];
|
|
55
83
|
const data = {
|
|
56
84
|
tilt: {
|
|
57
85
|
available: tiltAvailable,
|
|
58
86
|
resourcesQueried: options.tilt && tiltAvailable,
|
|
59
87
|
resources: tiltResources,
|
|
88
|
+
readiness,
|
|
60
89
|
},
|
|
61
90
|
resources: discovery.resources.map((resource) => ({
|
|
62
91
|
name: resource.name,
|
|
63
92
|
stack: resource.stack ?? null,
|
|
64
93
|
type: resource.type ?? "unknown",
|
|
65
94
|
port: resource.port ?? null,
|
|
95
|
+
...buildServicePorts(resource, portPlan?.ingressHttp),
|
|
66
96
|
})),
|
|
97
|
+
ports: buildStackPorts(portPlan, process.env.TILT_PORT ? Number.parseInt(process.env.TILT_PORT, 10) : undefined),
|
|
67
98
|
stacks: discovery.stacks.map((stack) => ({
|
|
68
99
|
name: stack.name,
|
|
69
100
|
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();
|