@tdk-landscape/tdk-cli-core 1.3.105 → 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 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);
@@ -9,7 +9,7 @@ import { MASTER_CONFIG_FILES, REQUIRED_PACKAGE_SCRIPTS } from "../utils/constant
9
9
  import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
10
10
  import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
11
11
  import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
12
- 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";
13
13
  import { validateEnvFile } from "../utils/env-validator.js";
14
14
  import { execAsync, isExecTimeout } from "../utils/exec-async.js";
15
15
  import { formatCount } from "../utils/formatting.js";
@@ -1106,6 +1106,7 @@ export const doctorCommand = new Command("doctor")
1106
1106
  () => checkServiceUrlPorts(),
1107
1107
  () => checkFrontendBackendUrls(),
1108
1108
  () => checkNatsBroker(),
1109
+ () => checkMigrationsInApi(),
1109
1110
  () => checkTiltInstances(),
1110
1111
  checkEnvironmentVariables,
1111
1112
  // Preflight: Verdaccio down causes ImageBuild bun install ConnectionRefused.
@@ -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
- await runCommand(async () => {
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 (options.noStack) {
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 && !options.noStack && !options.stack) {
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
- await runCommand(async () => {
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 (options.verbose || options.services) {
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,6 +1,7 @@
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";
6
7
  import { getHostPortPlan } from "../utils/host-port-config.js";
@@ -8,6 +9,7 @@ import { createMachineEnvelope, writeMachineError } from "../utils/machine-outpu
8
9
  import { findProjectRoot } from "../utils/paths.js";
9
10
  import { buildServicePorts, buildStackPorts } from "../utils/status-ports.js";
10
11
  import { getTiltfilePath, isTiltAvailable, runTilt } from "../utils/tilt.js";
12
+ import { evaluateTiltReadiness, tiltGetUiResources } from "../utils/up-readiness.js";
11
13
  export const statusCommand = new Command("status")
12
14
  .description("Show status of resources and stacks")
13
15
  .option("--json", "Output a versioned JSON status report", false)
@@ -53,6 +55,21 @@ export const statusCommand = new Command("status")
53
55
  queryError = result.stderr || "Could not retrieve Tilt resource status";
54
56
  }
55
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
+ }
56
73
  if (options.json) {
57
74
  let portPlan = null;
58
75
  try {
@@ -68,6 +85,7 @@ export const statusCommand = new Command("status")
68
85
  available: tiltAvailable,
69
86
  resourcesQueried: options.tilt && tiltAvailable,
70
87
  resources: tiltResources,
88
+ readiness,
71
89
  },
72
90
  resources: discovery.resources.map((resource) => ({
73
91
  name: resource.name,
@@ -5,6 +5,7 @@ import { Command } from "commander";
5
5
  import { ensureProjectRuntimeAssets } from "../generator/template-engine.js";
6
6
  import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_UP_MESSAGE, } from "../utils/agent-host.js";
7
7
  import { handleDryRun } from "../utils/command-helpers.js";
8
+ import { getDeferredResourceNames } from "../utils/doctor-runtime.js";
8
9
  import { completeEnvFile } from "../utils/env-validator.js";
9
10
  import { errorFactories, handleTiltFailure, requireProjectRoot, runCommand, showErrorAndExit, withTiltCheck, } from "../utils/errors.js";
10
11
  import { formatCount } from "../utils/formatting.js";
@@ -15,11 +16,12 @@ import { findProjectRoot } from "../utils/paths.js";
15
16
  import { findAvailablePort } from "../utils/port-assignment.js";
16
17
  import { isApiServiceType } from "../utils/resource-kind.js";
17
18
  import { appendHealthPath, resolveSubdomainBases } from "../utils/service-urls.js";
18
- import { discoverResources, discoverStacks, getResourcesForStack, stackExists, } from "../utils/services.js";
19
+ import { discoverResources, discoverResourcesStrict, discoverStacks, stackExists, } from "../utils/services.js";
19
20
  import { buildSmokePlans, formatSmokeFailure, runSmokePlans } from "../utils/smoke.js";
20
21
  import { buildTiltUpArgs, runTilt } from "../utils/tilt.js";
21
22
  import { stopTiltOnPort } from "../utils/tilt-process.js";
22
- import { waitForTiltResourcesReady } from "../utils/up-readiness.js";
23
+ import { findUnknownServices, resolveOnlySelection } from "../utils/up-only.js";
24
+ import { tiltGetUiResources, waitForTiltResourcesReady } from "../utils/up-readiness.js";
23
25
  import { enableDiscoveredStacks } from "./project.js";
24
26
  export function formatUpSuccess(port, appUrls = []) {
25
27
  return [
@@ -81,6 +83,7 @@ export const upCommand = new Command("up")
81
83
  .option("-q, --quiet", "Suppress non-essential output", false)
82
84
  .option("--dry-run", "Show what would be started without starting", false)
83
85
  .option("-f, --force", "Kill existing Tilt process before starting", false)
86
+ .option("--only <services...>", "Start only these services plus what the Tiltfile enables for them (their dependsOn services and shared infrastructure). With a stack, names must belong to that stack; dependencies may cross stacks")
84
87
  .option("--json", "Print one JSON object on stdout when the stack is ready or the command fails; implies --quiet", false)
85
88
  .action(async (stackName, options) => {
86
89
  const emit = options.json ? createJsonEmitter("UP_FAILED", "tdk up") : undefined;
@@ -102,6 +105,21 @@ export const upCommand = new Command("up")
102
105
  const projectRoot = options.dryRun
103
106
  ? requireProjectRoot()
104
107
  : (findProjectRoot() ?? process.cwd());
108
+ const discoveredResources = discoverResourcesStrict();
109
+ // Reject a bad request before anything below can write to the project (.env, runtime assets, .tdk/project.json).
110
+ if (options.only) {
111
+ if (stackName && !stackExists(stackName))
112
+ errorFactories.stackNotFound(stackName).exit();
113
+ const candidates = stackName
114
+ ? discoveredResources.filter((resource) => resource.stack === stackName)
115
+ : discoveredResources;
116
+ const unknown = findUnknownServices(options.only, candidates);
117
+ if (unknown.length > 0) {
118
+ const message = `Unknown service ${unknown.join(", ")}. Valid names: ${candidates.map((s) => s.name).join(", ")}`;
119
+ emit?.({ ok: false }, [{ code: "UNKNOWN_SERVICE", message }]);
120
+ showErrorAndExit(message, 2);
121
+ }
122
+ }
105
123
  const hostPortPlan = await getHostPortPlan(projectRoot, {
106
124
  inspectDocker: !options.dryRun,
107
125
  });
@@ -127,18 +145,27 @@ export const upCommand = new Command("up")
127
145
  let stackDescription;
128
146
  let focusServiceNames = [];
129
147
  if (stackName) {
130
- if (!stackExists(stackName)) {
148
+ servicesToStart = discoveredResources.filter((resource) => resource.stack === stackName);
149
+ if (servicesToStart.length === 0) {
131
150
  errorFactories.stackNotFound(stackName).exit();
132
151
  }
133
- servicesToStart = getResourcesForStack(stackName);
134
152
  focusServiceNames = servicesToStart.map((s) => s.name);
135
153
  stackDescription = `stack "${stackName}"`;
136
154
  }
137
155
  else {
138
- servicesToStart = discoverResources();
139
- const allStacks = discoverStacks();
156
+ servicesToStart = discoveredResources;
157
+ const allStacks = discoverStacks(discoveredResources);
140
158
  stackDescription = `all stacks (${formatCount(allStacks.length, "stack")}, ${formatCount(servicesToStart.length, "service")})`;
141
159
  }
160
+ let dependencyNames = [];
161
+ if (options.only) {
162
+ // Dependencies come from the whole project, not just the named stack: dependsOn may cross stacks.
163
+ const selection = resolveOnlySelection(options.only, discoverResources());
164
+ servicesToStart = selection.selected;
165
+ dependencyNames = selection.dependencies;
166
+ focusServiceNames = servicesToStart.map((s) => s.name);
167
+ stackDescription = `${formatCount(options.only.length, "requested service")}${dependencyNames.length > 0 ? ` plus ${dependencyNames.join(", ")}` : ""}`;
168
+ }
142
169
  if (options.verbose && !options.quiet) {
143
170
  console.log(chalk.gray(`Found ${formatCount(servicesToStart.length, "service")} in ${stackDescription}`));
144
171
  }
@@ -171,9 +198,14 @@ export const upCommand = new Command("up")
171
198
  });
172
199
  }
173
200
  }
174
- const dryRunCommand = stackName
175
- ? `tilt up -- --focus=${stackName} ${focusServiceNames.join(" ")}`
176
- : "tilt up";
201
+ const focusTargets = options.only ?? (stackName ? [stackName] : undefined);
202
+ const tiltArgs = buildTiltUpArgs(focusServiceNames, {
203
+ verbose: options.verbose,
204
+ quiet: options.quiet,
205
+ force: options.force,
206
+ focusTargets,
207
+ });
208
+ const dryRunCommand = focusTargets ? `tilt up ${tiltArgs.join(" ")}` : "tilt up";
177
209
  if (!options.quiet) {
178
210
  console.log(chalk.blue(formatHostPortPlan(hostPortPlan)));
179
211
  console.log(chalk.gray("Override with TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT."));
@@ -184,6 +216,9 @@ export const upCommand = new Command("up")
184
216
  dryRun: true,
185
217
  stack: stackName ?? null,
186
218
  services: serviceNames,
219
+ ...(options.only
220
+ ? { requested: options.only, declaredDependencies: dependencyNames }
221
+ : {}),
187
222
  command: dryRunCommand,
188
223
  });
189
224
  return;
@@ -195,6 +230,27 @@ export const upCommand = new Command("up")
195
230
  writeSavedHostPortPlan(projectRoot, hostPortPlan);
196
231
  const basePort = 10350;
197
232
  let port = basePort;
233
+ // A second Tilt would apply a different selection to the same containers, whatever port it listens on, so `--only`
234
+ // looks for a Tilt that answers on the default port and on TILT_PORT before it chooses one. A listener that is not
235
+ // Tilt does not count: the port is then picked below as usual.
236
+ if (options.only) {
237
+ const envPort = process.env.TILT_PORT ? Number.parseInt(process.env.TILT_PORT, 10) : NaN;
238
+ const candidatePorts = [
239
+ ...new Set([basePort, ...(Number.isInteger(envPort) ? [envPort] : [])]),
240
+ ];
241
+ const running = [];
242
+ for (const candidate of candidatePorts) {
243
+ if ((await tiltGetUiResources(candidate)) !== null)
244
+ running.push(candidate);
245
+ }
246
+ if (running.length > 0 && !options.force) {
247
+ 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.`;
248
+ emit?.({ ok: false }, [{ code: "TILT_ALREADY_RUNNING", message }]);
249
+ showErrorAndExit(message);
250
+ }
251
+ for (const runningPort of running)
252
+ stopTiltOnPort(runningPort);
253
+ }
198
254
  if (process.env.TILT_PORT) {
199
255
  port = parseInt(process.env.TILT_PORT, 10);
200
256
  }
@@ -216,12 +272,6 @@ export const upCommand = new Command("up")
216
272
  stopTiltOnPort(port);
217
273
  await new Promise((resolve) => setTimeout(resolve, 2000));
218
274
  }
219
- const tiltArgs = buildTiltUpArgs(focusServiceNames, {
220
- verbose: options.verbose,
221
- quiet: options.quiet,
222
- force: options.force,
223
- focusTargets: stackName ? [stackName] : undefined,
224
- });
225
275
  if (!options.quiet) {
226
276
  console.log(chalk.gray("\nRunning tilt up..."));
227
277
  console.log(chalk.gray(`Using Tiltfile: .tdk/.tdk-out/Tiltfile`));
@@ -240,10 +290,26 @@ export const upCommand = new Command("up")
240
290
  const uiReady = await waitForTiltUi(port);
241
291
  // JSON consumers get success only after every non-deferred resource is built and running, not when the UI port opens.
242
292
  let jsonReady = false;
293
+ let startedServices = serviceNames;
294
+ let startedDependencies = dependencyNames;
243
295
  if (uiReady && emit) {
244
- const readiness = await waitForTiltResourcesReady(port);
296
+ const deferred = getDeferredResourceNames();
297
+ const readiness = await waitForTiltResourcesReady(port, {
298
+ deferred,
299
+ // Only what the caller named must be enabled. Dependencies are the Tiltfile's call (it skips some on purpose).
300
+ expected: options.only
301
+ ? options.only.filter((name) => !deferred.has(name))
302
+ : undefined,
303
+ });
245
304
  if (readiness.ready) {
246
305
  jsonReady = true;
306
+ if (options.only) {
307
+ // Report what Tilt actually enabled, not the service.json closure, which can differ from the Tiltfile's expansion.
308
+ const enabled = new Set(readiness.enabled);
309
+ const allNames = discoverResources().map((r) => r.name);
310
+ startedServices = allNames.filter((name) => enabled.has(name));
311
+ startedDependencies = startedServices.filter((name) => !options.only.includes(name));
312
+ }
247
313
  }
248
314
  else {
249
315
  const detail = readiness.failures.map((f) => `${f.name}: ${f.message}`).join("; ");
@@ -288,7 +354,8 @@ export const upCommand = new Command("up")
288
354
  emit?.({
289
355
  ok: true,
290
356
  stack: stackName ?? null,
291
- services: serviceNames,
357
+ services: startedServices,
358
+ ...(options.only ? { requested: options.only, dependencies: startedDependencies } : {}),
292
359
  tiltUrl: `http://localhost:${port}`,
293
360
  });
294
361
  }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * A minimal Model Context Protocol server over newline-delimited JSON-RPC 2.0 (the stdio transport). It implements only
3
+ * what a tools-only server needs: initialize, ping, tools/list and tools/call. Keeping it dependency-free keeps the
4
+ * published CLI small; the protocol surface used here is stable across the listed protocol versions.
5
+ */
6
+ export const SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
7
+ const ERROR = {
8
+ parse: -32700,
9
+ invalidRequest: -32600,
10
+ methodNotFound: -32601,
11
+ invalidParams: -32602,
12
+ };
13
+ function reply(id, result) {
14
+ return JSON.stringify({ jsonrpc: "2.0", id, result });
15
+ }
16
+ function fail(id, code, message) {
17
+ return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
18
+ }
19
+ export function createMcpServer(tools, info) {
20
+ const byName = new Map(tools.map((tool) => [tool.name, tool]));
21
+ async function handle(message) {
22
+ const isNotification = message.id === undefined;
23
+ if (message.jsonrpc !== "2.0" || typeof message.method !== "string") {
24
+ return isNotification ? null : fail(message.id, ERROR.invalidRequest, "Invalid request");
25
+ }
26
+ switch (message.method) {
27
+ case "initialize": {
28
+ const requested = String(message.params?.protocolVersion ?? "");
29
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
30
+ ? requested
31
+ : SUPPORTED_PROTOCOL_VERSIONS[0];
32
+ return reply(message.id, {
33
+ protocolVersion,
34
+ capabilities: { tools: { listChanged: false } },
35
+ serverInfo: info,
36
+ });
37
+ }
38
+ case "ping":
39
+ return reply(message.id, {});
40
+ case "tools/list":
41
+ return reply(message.id, {
42
+ tools: tools.map(({ name, description, inputSchema }) => ({
43
+ name,
44
+ description,
45
+ inputSchema,
46
+ })),
47
+ });
48
+ case "tools/call": {
49
+ const name = message.params?.name;
50
+ const tool = typeof name === "string" ? byName.get(name) : undefined;
51
+ if (!tool)
52
+ return fail(message.id, ERROR.invalidParams, `Unknown tool: ${String(name)}`);
53
+ const args = message.params?.arguments;
54
+ if (args !== undefined &&
55
+ (typeof args !== "object" || args === null || Array.isArray(args))) {
56
+ return fail(message.id, ERROR.invalidParams, "arguments must be an object");
57
+ }
58
+ try {
59
+ const result = await tool.handler((args ?? {}));
60
+ return reply(message.id, {
61
+ content: [{ type: "text", text: JSON.stringify(result.data) }],
62
+ structuredContent: result.data,
63
+ isError: result.isError,
64
+ });
65
+ }
66
+ catch (error) {
67
+ // A tool failure is a tool result, not a protocol error, so the model can read and react to it.
68
+ const text = error instanceof Error ? error.message : String(error);
69
+ return reply(message.id, {
70
+ content: [{ type: "text", text }],
71
+ isError: true,
72
+ });
73
+ }
74
+ }
75
+ default:
76
+ // Notifications such as notifications/initialized need no reply; unknown requests do.
77
+ return isNotification
78
+ ? null
79
+ : fail(message.id, ERROR.methodNotFound, `Method not found: ${message.method}`);
80
+ }
81
+ }
82
+ return {
83
+ async handleLine(line) {
84
+ if (!line.trim())
85
+ return null;
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(line);
89
+ }
90
+ catch {
91
+ return fail(null, ERROR.parse, "Parse error");
92
+ }
93
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
94
+ return fail(null, ERROR.invalidRequest, "Invalid request");
95
+ }
96
+ return handle(parsed);
97
+ },
98
+ };
99
+ }
100
+ //# sourceMappingURL=server.js.map