@tdk-landscape/tdk-cli-core 1.3.105 → 1.3.107

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.
@@ -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,13 @@ 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 { parseTiltPort, resolveTiltPort, stopTiltForUp } from "../utils/tilt-startup.js";
24
+ import { findUnknownServices, resolveOnlySelection } from "../utils/up-only.js";
25
+ import { tiltGetUiResources, waitForTiltResourcesReady } from "../utils/up-readiness.js";
23
26
  import { enableDiscoveredStacks } from "./project.js";
24
27
  export function formatUpSuccess(port, appUrls = []) {
25
28
  return [
@@ -81,11 +84,18 @@ export const upCommand = new Command("up")
81
84
  .option("-q, --quiet", "Suppress non-essential output", false)
82
85
  .option("--dry-run", "Show what would be started without starting", false)
83
86
  .option("-f, --force", "Kill existing Tilt process before starting", false)
87
+ .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
88
  .option("--json", "Print one JSON object on stdout when the stack is ready or the command fails; implies --quiet", false)
85
89
  .action(async (stackName, options) => {
86
90
  const emit = options.json ? createJsonEmitter("UP_FAILED", "tdk up") : undefined;
87
91
  if (options.json)
88
92
  options.quiet = true;
93
+ const portSetting = parseTiltPort(process.env.TILT_PORT);
94
+ if (!portSetting.ok) {
95
+ emit?.({ ok: false }, [{ code: "INVALID_TILT_PORT", message: portSetting.message }]);
96
+ showErrorAndExit(portSetting.message);
97
+ }
98
+ const configuredTiltPort = portSetting.port;
89
99
  const hostRefusal = options.dryRun ? null : await hostUpRefusal();
90
100
  if (hostRefusal) {
91
101
  emit?.({ ok: false }, [{ code: "HOST_UNSUPPORTED", message: hostRefusal }]);
@@ -102,6 +112,21 @@ export const upCommand = new Command("up")
102
112
  const projectRoot = options.dryRun
103
113
  ? requireProjectRoot()
104
114
  : (findProjectRoot() ?? process.cwd());
115
+ const discoveredResources = discoverResourcesStrict();
116
+ // Reject a bad request before anything below can write to the project (.env, runtime assets, .tdk/project.json).
117
+ if (options.only) {
118
+ if (stackName && !stackExists(stackName))
119
+ errorFactories.stackNotFound(stackName).exit();
120
+ const candidates = stackName
121
+ ? discoveredResources.filter((resource) => resource.stack === stackName)
122
+ : discoveredResources;
123
+ const unknown = findUnknownServices(options.only, candidates);
124
+ if (unknown.length > 0) {
125
+ const message = `Unknown service ${unknown.join(", ")}. Valid names: ${candidates.map((s) => s.name).join(", ")}`;
126
+ emit?.({ ok: false }, [{ code: "UNKNOWN_SERVICE", message }]);
127
+ showErrorAndExit(message, 2);
128
+ }
129
+ }
105
130
  const hostPortPlan = await getHostPortPlan(projectRoot, {
106
131
  inspectDocker: !options.dryRun,
107
132
  });
@@ -127,18 +152,27 @@ export const upCommand = new Command("up")
127
152
  let stackDescription;
128
153
  let focusServiceNames = [];
129
154
  if (stackName) {
130
- if (!stackExists(stackName)) {
155
+ servicesToStart = discoveredResources.filter((resource) => resource.stack === stackName);
156
+ if (servicesToStart.length === 0) {
131
157
  errorFactories.stackNotFound(stackName).exit();
132
158
  }
133
- servicesToStart = getResourcesForStack(stackName);
134
159
  focusServiceNames = servicesToStart.map((s) => s.name);
135
160
  stackDescription = `stack "${stackName}"`;
136
161
  }
137
162
  else {
138
- servicesToStart = discoverResources();
139
- const allStacks = discoverStacks();
163
+ servicesToStart = discoveredResources;
164
+ const allStacks = discoverStacks(discoveredResources);
140
165
  stackDescription = `all stacks (${formatCount(allStacks.length, "stack")}, ${formatCount(servicesToStart.length, "service")})`;
141
166
  }
167
+ let dependencyNames = [];
168
+ if (options.only) {
169
+ // Dependencies come from the whole project, not just the named stack: dependsOn may cross stacks.
170
+ const selection = resolveOnlySelection(options.only, discoverResources());
171
+ servicesToStart = selection.selected;
172
+ dependencyNames = selection.dependencies;
173
+ focusServiceNames = servicesToStart.map((s) => s.name);
174
+ stackDescription = `${formatCount(options.only.length, "requested service")}${dependencyNames.length > 0 ? ` plus ${dependencyNames.join(", ")}` : ""}`;
175
+ }
142
176
  if (options.verbose && !options.quiet) {
143
177
  console.log(chalk.gray(`Found ${formatCount(servicesToStart.length, "service")} in ${stackDescription}`));
144
178
  }
@@ -171,9 +205,14 @@ export const upCommand = new Command("up")
171
205
  });
172
206
  }
173
207
  }
174
- const dryRunCommand = stackName
175
- ? `tilt up -- --focus=${stackName} ${focusServiceNames.join(" ")}`
176
- : "tilt up";
208
+ const focusTargets = options.only ?? (stackName ? [stackName] : undefined);
209
+ const tiltArgs = buildTiltUpArgs(focusServiceNames, {
210
+ verbose: options.verbose,
211
+ quiet: options.quiet,
212
+ force: options.force,
213
+ focusTargets,
214
+ });
215
+ const dryRunCommand = focusTargets ? `tilt up ${tiltArgs.join(" ")}` : "tilt up";
177
216
  if (!options.quiet) {
178
217
  console.log(chalk.blue(formatHostPortPlan(hostPortPlan)));
179
218
  console.log(chalk.gray("Override with TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT."));
@@ -184,6 +223,9 @@ export const upCommand = new Command("up")
184
223
  dryRun: true,
185
224
  stack: stackName ?? null,
186
225
  services: serviceNames,
226
+ ...(options.only
227
+ ? { requested: options.only, declaredDependencies: dependencyNames }
228
+ : {}),
187
229
  command: dryRunCommand,
188
230
  });
189
231
  return;
@@ -194,33 +236,42 @@ export const upCommand = new Command("up")
194
236
  exportHostPortPlan(hostPortPlan);
195
237
  writeSavedHostPortPlan(projectRoot, hostPortPlan);
196
238
  const basePort = 10350;
197
- let port = basePort;
198
- if (process.env.TILT_PORT) {
199
- port = parseInt(process.env.TILT_PORT, 10);
200
- }
201
- else if (!options.force) {
202
- const availablePort = await findAvailablePort(basePort, 10);
203
- if (availablePort && availablePort !== basePort) {
204
- port = availablePort;
205
- if (!options.quiet) {
206
- console.log(chalk.yellow(`⚠️ Port ${basePort} is already in use`));
207
- console.log(chalk.blue(`🔄 Auto-switching to port ${port}\n`));
208
- }
239
+ // A second Tilt would apply a different selection to the same containers, whatever port it listens on, so `--only`
240
+ // looks for a Tilt that answers on the default port and on TILT_PORT before it chooses one. A listener that is not
241
+ // Tilt does not count: the port is then picked below as usual.
242
+ if (options.only) {
243
+ const candidatePorts = [
244
+ ...new Set([basePort, ...(configuredTiltPort === undefined ? [] : [configuredTiltPort])]),
245
+ ];
246
+ const running = [];
247
+ for (const candidate of candidatePorts) {
248
+ if ((await tiltGetUiResources(candidate)) !== null)
249
+ running.push(candidate);
209
250
  }
210
- }
211
- process.env.TILT_PORT = port.toString();
212
- if (options.force) {
213
- if (!options.quiet) {
214
- console.log(chalk.yellow(`Force flag set - stopping Tilt on port ${port} if running...`));
251
+ if (running.length > 0 && !options.force) {
252
+ 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.`;
253
+ emit?.({ ok: false }, [{ code: "TILT_ALREADY_RUNNING", message }]);
254
+ showErrorAndExit(message);
215
255
  }
216
- stopTiltOnPort(port);
217
- await new Promise((resolve) => setTimeout(resolve, 2000));
256
+ for (const runningPort of running)
257
+ stopTiltOnPort(runningPort);
218
258
  }
219
- const tiltArgs = buildTiltUpArgs(focusServiceNames, {
220
- verbose: options.verbose,
221
- quiet: options.quiet,
259
+ const resolution = await resolveTiltPort({
260
+ configuredPort: configuredTiltPort,
222
261
  force: options.force,
223
- focusTargets: stackName ? [stackName] : undefined,
262
+ basePort,
263
+ findAvailablePort,
264
+ });
265
+ const port = resolution.port;
266
+ if (resolution.autoSwitched && !options.quiet) {
267
+ console.log(chalk.yellow(`⚠️ Port ${basePort} is already in use`));
268
+ console.log(chalk.blue(`🔄 Auto-switching to port ${port}\n`));
269
+ }
270
+ process.env.TILT_PORT = port.toString();
271
+ await stopTiltForUp({ force: options.force, quiet: options.quiet, port }, {
272
+ stop: stopTiltOnPort,
273
+ log: (message) => console.log(chalk.yellow(message)),
274
+ wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
224
275
  });
225
276
  if (!options.quiet) {
226
277
  console.log(chalk.gray("\nRunning tilt up..."));
@@ -240,10 +291,26 @@ export const upCommand = new Command("up")
240
291
  const uiReady = await waitForTiltUi(port);
241
292
  // JSON consumers get success only after every non-deferred resource is built and running, not when the UI port opens.
242
293
  let jsonReady = false;
294
+ let startedServices = serviceNames;
295
+ let startedDependencies = dependencyNames;
243
296
  if (uiReady && emit) {
244
- const readiness = await waitForTiltResourcesReady(port);
297
+ const deferred = getDeferredResourceNames();
298
+ const readiness = await waitForTiltResourcesReady(port, {
299
+ deferred,
300
+ // Only what the caller named must be enabled. Dependencies are the Tiltfile's call (it skips some on purpose).
301
+ expected: options.only
302
+ ? options.only.filter((name) => !deferred.has(name))
303
+ : undefined,
304
+ });
245
305
  if (readiness.ready) {
246
306
  jsonReady = true;
307
+ if (options.only) {
308
+ // Report what Tilt actually enabled, not the service.json closure, which can differ from the Tiltfile's expansion.
309
+ const enabled = new Set(readiness.enabled);
310
+ const allNames = discoverResources().map((r) => r.name);
311
+ startedServices = allNames.filter((name) => enabled.has(name));
312
+ startedDependencies = startedServices.filter((name) => !options.only.includes(name));
313
+ }
247
314
  }
248
315
  else {
249
316
  const detail = readiness.failures.map((f) => `${f.name}: ${f.message}`).join("; ");
@@ -288,7 +355,8 @@ export const upCommand = new Command("up")
288
355
  emit?.({
289
356
  ok: true,
290
357
  stack: stackName ?? null,
291
- services: serviceNames,
358
+ services: startedServices,
359
+ ...(options.only ? { requested: options.only, dependencies: startedDependencies } : {}),
292
360
  tiltUrl: `http://localhost:${port}`,
293
361
  });
294
362
  }
@@ -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
@@ -0,0 +1,273 @@
1
+ import { spawn } from "node:child_process";
2
+ import { closeSync, existsSync, openSync, readFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ /** A service or stack name. A leading "-" is rejected so a value can never be read as a flag. */
6
+ const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
7
+ const SINCE = /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/;
8
+ /**
9
+ * How to run this CLI again: `node <script>` / `bun <script>` when started from a script, or the executable alone when it is
10
+ * a `bun build --compile` binary, whose argv[1] is a virtual path that exists only inside the binary.
11
+ */
12
+ export function cliInvocation(execPath = process.execPath, script = process.argv[1], fileExists = existsSync) {
13
+ if (!script || script.startsWith("/$bunfs/") || script.includes("~BUN") || !fileExists(script)) {
14
+ return [execPath];
15
+ }
16
+ return [execPath, script];
17
+ }
18
+ /** Runs this same CLI (`tdk <args>`) so every tool behaves exactly like its command. */
19
+ export const runTdkCli = (args, options = {}) => new Promise((resolve, reject) => {
20
+ const [command, ...prefix] = cliInvocation();
21
+ const child = spawn(command, [...prefix, ...args], {
22
+ stdio: ["ignore", "pipe", "pipe"],
23
+ env: { ...process.env, NO_COLOR: "1" },
24
+ });
25
+ let stdout = "";
26
+ let stderr = "";
27
+ child.stdout?.on("data", (chunk) => {
28
+ stdout += chunk.toString();
29
+ });
30
+ child.stderr?.on("data", (chunk) => {
31
+ stderr += chunk.toString();
32
+ });
33
+ const timer = setTimeout(() => child.kill("SIGTERM"), options.timeoutMs ?? 120_000);
34
+ child.once("error", (error) => {
35
+ clearTimeout(timer);
36
+ reject(error);
37
+ });
38
+ child.once("close", (exitCode) => {
39
+ clearTimeout(timer);
40
+ resolve({ exitCode, stdout, stderr });
41
+ });
42
+ });
43
+ function names(value, field) {
44
+ if (value === undefined)
45
+ return [];
46
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !NAME.test(item))) {
47
+ throw new Error(`${field} must be an array of service names (letters, digits, ".", "_", "-")`);
48
+ }
49
+ return value;
50
+ }
51
+ function optionalName(value, field) {
52
+ if (value === undefined)
53
+ return undefined;
54
+ if (typeof value !== "string" || !NAME.test(value)) {
55
+ throw new Error(`${field} must be a name (letters, digits, ".", "_", "-")`);
56
+ }
57
+ return value;
58
+ }
59
+ function flag(value, field) {
60
+ if (value === undefined)
61
+ return false;
62
+ if (typeof value !== "boolean")
63
+ throw new Error(`${field} must be a boolean`);
64
+ return value;
65
+ }
66
+ /** Turn a command's `--json` stdout into a tool result. A parseable envelope with errors, or `ok: false`, is an error result. */
67
+ export function toToolResult(run) {
68
+ const text = run.stdout.trim();
69
+ // The envelope is the last JSON line; anything before it is incidental output.
70
+ const lastLine = text
71
+ .split("\n")
72
+ .filter((line) => line.trim().startsWith("{"))
73
+ .pop();
74
+ if (!lastLine) {
75
+ return {
76
+ data: {
77
+ errors: [
78
+ { code: "NO_JSON", message: run.stderr.trim() || `tdk exited with code ${run.exitCode}` },
79
+ ],
80
+ },
81
+ isError: true,
82
+ };
83
+ }
84
+ try {
85
+ const envelope = JSON.parse(lastLine);
86
+ const failed = (envelope.errors?.length ?? 0) > 0 || envelope.data?.ok === false;
87
+ return { data: envelope, isError: failed };
88
+ }
89
+ catch {
90
+ return {
91
+ data: { errors: [{ code: "BAD_JSON", message: lastLine.slice(0, 200) }] },
92
+ isError: true,
93
+ };
94
+ }
95
+ }
96
+ export const defaultUpDeps = {
97
+ spawnUp(args, logFile) {
98
+ const fd = openSync(logFile, "a");
99
+ const [command, ...prefix] = cliInvocation();
100
+ const child = spawn(command, [...prefix, ...args], {
101
+ detached: true,
102
+ stdio: ["ignore", fd, fd],
103
+ env: { ...process.env, NO_COLOR: "1" },
104
+ });
105
+ closeSync(fd);
106
+ return child;
107
+ },
108
+ readLog(logFile) {
109
+ try {
110
+ return readFileSync(logFile, "utf-8");
111
+ }
112
+ catch {
113
+ return "";
114
+ }
115
+ },
116
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
117
+ now: () => Date.now(),
118
+ logFile: () => join(tmpdir(), `tdk-mcp-up-${Date.now()}.log`),
119
+ };
120
+ export function buildUpArgs(args) {
121
+ const stack = optionalName(args.stack, "stack");
122
+ const only = names(args.only, "only");
123
+ const out = ["up"];
124
+ if (stack)
125
+ out.push(stack);
126
+ if (only.length > 0)
127
+ out.push(...only.map((name) => `--only=${name}`));
128
+ if (flag(args.force, "force"))
129
+ out.push("--force");
130
+ out.push("--json");
131
+ return out;
132
+ }
133
+ /**
134
+ * Starts the stack detached and returns without waiting for it to be ready (a build can take minutes, longer than a tool
135
+ * call may last). It watches only for an early result, which is how a refusal such as UNKNOWN_SERVICE, TILT_ALREADY_RUNNING
136
+ * or an unsupported host arrives; otherwise the caller polls the `status` tool for `data.tilt.readiness`.
137
+ */
138
+ export async function startUp(args, deps = defaultUpDeps) {
139
+ const argv = buildUpArgs(args);
140
+ const logFile = deps.logFile();
141
+ const child = deps.spawnUp(argv, logFile);
142
+ let exited;
143
+ child.once("exit", (code) => {
144
+ exited = code;
145
+ });
146
+ const waitMs = typeof args.waitSeconds === "number"
147
+ ? Math.min(Math.max(args.waitSeconds, 0), 60) * 1000
148
+ : 15_000;
149
+ const deadline = deps.now() + waitMs;
150
+ for (;;) {
151
+ const log = deps.readLog(logFile);
152
+ const line = log
153
+ .split("\n")
154
+ .filter((l) => l.startsWith('{"schemaVersion"'))
155
+ .pop();
156
+ if (line)
157
+ return { ...toToolResult({ exitCode: exited ?? null, stdout: line, stderr: "" }) };
158
+ if (exited !== undefined) {
159
+ return toToolResult({ exitCode: exited, stdout: "", stderr: log.slice(-2000) });
160
+ }
161
+ if (deps.now() >= deadline)
162
+ break;
163
+ await deps.sleep(250);
164
+ }
165
+ child.unref();
166
+ return {
167
+ data: {
168
+ schemaVersion: 1,
169
+ data: {
170
+ ok: true,
171
+ started: true,
172
+ pid: child.pid ?? null,
173
+ logFile,
174
+ next: "Poll the status tool until data.tilt.readiness.ready is true. Call down to stop it.",
175
+ },
176
+ errors: [],
177
+ },
178
+ isError: false,
179
+ };
180
+ }
181
+ export function buildLogsArgs(args) {
182
+ const out = ["logs", "--json"];
183
+ for (const name of names(args.services, "services"))
184
+ out.push(`--service=${name}`);
185
+ if (args.tail !== undefined) {
186
+ if (!Number.isInteger(args.tail) || args.tail < 1) {
187
+ throw new Error("tail must be a positive integer");
188
+ }
189
+ out.push(`--tail=${args.tail}`);
190
+ }
191
+ if (args.since !== undefined) {
192
+ if (typeof args.since !== "string" || !SINCE.test(args.since)) {
193
+ throw new Error("since must be a duration such as 30s, 5m or 1h");
194
+ }
195
+ out.push(`--since=${args.since}`);
196
+ }
197
+ return out;
198
+ }
199
+ const OBJECT = (properties) => ({
200
+ type: "object",
201
+ properties,
202
+ additionalProperties: false,
203
+ });
204
+ export function createTdkTools(run = runTdkCli, up = defaultUpDeps) {
205
+ const viaCli = (build) => async (args) => toToolResult(await run(build(args)));
206
+ return [
207
+ {
208
+ name: "doctor",
209
+ description: "Check whether this machine can run the TDK stack. Returns data.ready and data.host.canUp; run it before `up`.",
210
+ inputSchema: OBJECT({
211
+ noPing: { type: "boolean", description: "Skip pinging running services' health endpoints" },
212
+ }),
213
+ handler: viaCli((args) => [
214
+ "doctor",
215
+ "--json",
216
+ ...(flag(args.noPing, "noPing") ? ["--no-ping"] : []),
217
+ ]),
218
+ },
219
+ {
220
+ name: "up",
221
+ description: "Start the stack (or part of it) detached and return without waiting for it to be ready. Poll `status` for data.tilt.readiness.ready. Fails fast on an unknown service, an unsupported host, or a Tilt already running.",
222
+ inputSchema: OBJECT({
223
+ stack: { type: "string", description: "Only this stack" },
224
+ only: {
225
+ type: "array",
226
+ items: { type: "string" },
227
+ description: "Start only these services plus what they depend on and shared infrastructure",
228
+ },
229
+ force: { type: "boolean", description: "Replace a Tilt that is already running" },
230
+ waitSeconds: {
231
+ type: "number",
232
+ description: "How long to watch for an early failure before returning (default 15, max 60)",
233
+ },
234
+ }),
235
+ handler: (args) => startUp(args, up),
236
+ },
237
+ {
238
+ name: "down",
239
+ description: "Stop the running stack.",
240
+ inputSchema: OBJECT({}),
241
+ handler: viaCli(() => ["down", "--json"]),
242
+ },
243
+ {
244
+ name: "status",
245
+ description: "Stacks, resources, their ingress URLs and container ports, the stack-level ports, and Tilt readiness (data.tilt.readiness: ready, pending, failures, enabled; null when no Tilt answers).",
246
+ inputSchema: OBJECT({}),
247
+ handler: viaCli(() => ["status", "--json"]),
248
+ },
249
+ {
250
+ name: "logs",
251
+ description: "A bounded snapshot of recent logs from the running stack (default the last 200 lines, at most 10000). Not a stream.",
252
+ inputSchema: OBJECT({
253
+ services: { type: "array", items: { type: "string" }, description: "Only these services" },
254
+ tail: { type: "integer", minimum: 1, maximum: 10000 },
255
+ since: {
256
+ type: "string",
257
+ description: "Only logs newer than this, for example 30s, 5m, 1h",
258
+ },
259
+ }),
260
+ handler: viaCli(buildLogsArgs),
261
+ },
262
+ {
263
+ name: "resource_list",
264
+ description: "List the project's services (resources) with their stack, type and port.",
265
+ inputSchema: OBJECT({ stack: { type: "string", description: "Only this stack" } }),
266
+ handler: viaCli((args) => {
267
+ const stack = optionalName(args.stack, "stack");
268
+ return ["resources", "--json", ...(stack ? [`--stack=${stack}`] : [])];
269
+ }),
270
+ },
271
+ ];
272
+ }
273
+ //# sourceMappingURL=tools.js.map
@@ -1,5 +1,6 @@
1
1
  import { createCacheValidator } from "./cache.js";
2
- import { discoverResources, discoverStacks, getAllStacks } from "./services.js";
2
+ import { findProjectRoot } from "./paths.js";
3
+ import { discoverResources, discoverResourcesWithProblems, discoverStacks, getAllStacks, warnAboutSkippedFiles, } from "./services.js";
3
4
  let cachedContext = null;
4
5
  const cacheValidator = createCacheValidator(1000);
5
6
  export function clearDiscoveryCache() {
@@ -13,9 +14,13 @@ export function createDiscoveryContext(forceRefresh = false) {
13
14
  if (!forceRefresh && isCacheValid() && cachedContext) {
14
15
  return cachedContext;
15
16
  }
16
- const resources = discoverResources();
17
- const stacks = discoverStacks();
17
+ const projectRoot = findProjectRoot();
18
+ const { resources, problems } = projectRoot
19
+ ? discoverResourcesWithProblems(projectRoot)
20
+ : { resources: discoverResources(), problems: [] };
21
+ const stacks = discoverStacks(resources);
18
22
  const stackNames = getAllStacks(resources);
23
+ warnAboutSkippedFiles(problems);
19
24
  const unassignedResources = resources.filter((r) => !r.stack);
20
25
  const resourcesByStack = new Map();
21
26
  for (const resource of resources) {
@@ -27,6 +32,7 @@ export function createDiscoveryContext(forceRefresh = false) {
27
32
  }
28
33
  const context = {
29
34
  resources,
35
+ problems,
30
36
  stacks,
31
37
  stackNames,
32
38
  unassignedResources,