@tdk-landscape/tdk-cli-core 1.3.173 → 1.3.175

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.
@@ -1,17 +1,54 @@
1
1
  // Copyright (c) 2026 TDK Landscape contributors
2
2
  // SPDX-License-Identifier: MIT
3
+ import * as http from "node:http";
3
4
  import chalk from "chalk";
4
5
  import { Command } from "commander";
5
6
  import { createDiscoveryContext } from "../utils/discovery-context.js";
6
7
  import { errorFactories, runCommand, TdkError } from "../utils/errors.js";
8
+ import { getHostPortPlan } from "../utils/host-port-config.js";
7
9
  import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
8
10
  import { findProjectRoot } from "../utils/paths.js";
11
+ import { buildServicePorts } from "../utils/status-ports.js";
9
12
  const SESSION_REQUEST_TIMEOUT_MS = 10_000;
13
+ /** GET with node:http because fetch silently drops a custom Host header, which is what Traefik routes on. */
14
+ export function httpGet(url, headers, timeoutMs) {
15
+ return new Promise((resolve, reject) => {
16
+ // One signal covers the connection and the body read, so a hung agent cannot block the CLI.
17
+ const req = http.get(url, { headers, signal: AbortSignal.timeout(timeoutMs) }, (res) => {
18
+ const chunks = [];
19
+ res.on("data", (chunk) => chunks.push(chunk));
20
+ res.on("end", () => {
21
+ const status = res.statusCode ?? 0;
22
+ resolve({
23
+ ok: status >= 200 && status < 300,
24
+ status,
25
+ text: Buffer.concat(chunks).toString("utf-8"),
26
+ });
27
+ });
28
+ res.on("error", reject);
29
+ });
30
+ req.on("error", reject);
31
+ });
32
+ }
33
+ /**
34
+ * Agent containers publish no host port; the only way in is the Traefik route that `tdk status` prints as `url`
35
+ * (for example http://api.<project>.localhost:8080/api/bot). `*.localhost` does not resolve everywhere (glibc without
36
+ * systemd-resolved), so connect to loopback and keep the route's host in the Host header, which is all Traefik matches on.
37
+ */
38
+ export function agentSessionRequest(routeUrl, sessionId) {
39
+ const route = new URL(routeUrl);
40
+ const headers = { "x-session-id": sessionId };
41
+ if (route.hostname.endsWith(".localhost")) {
42
+ headers.host = route.host;
43
+ route.hostname = "127.0.0.1";
44
+ }
45
+ return { url: `${route.origin}${route.pathname.replace(/\/$/, "")}/session`, headers };
46
+ }
10
47
  export const agentCommand = new Command("agent").description("Inspect and operate AI agent resources");
11
48
  agentCommand
12
49
  .command("sessions <resource>")
13
50
  .description("Inspect session checkpoints from a running AI agent resource")
14
- .option("--session-id <id>", "Specific session ID to fetch")
51
+ .option("--session-id <id>", "Session ID to fetch (required)")
15
52
  .option("--json", "Output one JSON object and exit", false)
16
53
  .action(async (resourceName, options) => {
17
54
  const action = async () => {
@@ -50,28 +87,49 @@ agentCommand
50
87
  }
51
88
  throw new TdkError(msg);
52
89
  }
53
- const headers = {};
54
- if (options.sessionId) {
55
- headers["x-session-id"] = String(options.sessionId);
90
+ // The agent runtime keys GET /session by x-session-id and has no list endpoint, so a missing id
91
+ // would only come back as a confusing 400. Fail before any request. Checked after resource
92
+ // validation so UNKNOWN_RESOURCE, NOT_AN_AGENT and NO_PORT keep their precedence.
93
+ const sessionId = typeof options.sessionId === "string" ? options.sessionId.trim() : "";
94
+ if (!sessionId) {
95
+ const msg = `--session-id is required: the agent "${resourceName}" returns the checkpoint of one session.`;
96
+ if (options.json) {
97
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "SESSION_ID_REQUIRED", message: msg }])));
98
+ process.exit(1);
99
+ }
100
+ throw new TdkError(msg, [`Run: tdk agent sessions ${resourceName} --session-id <id>`]);
56
101
  }
57
- let res;
58
- let bodyText;
59
- try {
60
- // One signal covers both the connection and the body read, so a hung agent cannot block the CLI.
61
- res = await fetch(`http://127.0.0.1:${port}/session`, {
62
- method: "GET",
63
- headers,
64
- signal: AbortSignal.timeout(SESSION_REQUEST_TIMEOUT_MS),
65
- });
66
- bodyText = await res.text();
102
+ const portPlan = root
103
+ ? await getHostPortPlan(root, { inspectDocker: false }).catch(() => null)
104
+ : null;
105
+ const routeUrl = buildServicePorts(resource, portPlan?.ingressHttp).url;
106
+ if (!routeUrl) {
107
+ const msg = `Agent "${resourceName}" has no ingress route to reach it through.`;
108
+ if (options.json) {
109
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "NO_ROUTE", message: msg }])));
110
+ process.exit(1);
111
+ }
112
+ throw new TdkError(msg);
67
113
  }
68
- catch {
69
- const msg = `Agent "${resourceName}" is not responding on port ${port}. Is the stack running?`;
114
+ const request = agentSessionRequest(routeUrl, sessionId);
115
+ // Nothing answered, or Traefik answered for a missing backend: either way the agent is not up.
116
+ const notRunning = () => {
117
+ const msg = `Agent "${resourceName}" is not responding at ${routeUrl}. Is the stack running?`;
70
118
  if (options.json) {
71
119
  console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "AGENT_NOT_RUNNING", message: msg }])));
72
120
  process.exit(1);
73
121
  }
74
122
  throw new TdkError(msg, ["Start the stack with: tdk up"]);
123
+ };
124
+ let res;
125
+ let bodyText;
126
+ try {
127
+ const reply = await httpGet(request.url, request.headers, SESSION_REQUEST_TIMEOUT_MS);
128
+ res = reply;
129
+ bodyText = reply.text;
130
+ }
131
+ catch {
132
+ return notRunning();
75
133
  }
76
134
  let body = {};
77
135
  try {
@@ -80,6 +138,13 @@ agentCommand
80
138
  catch {
81
139
  body = { raw: bodyText };
82
140
  }
141
+ // Traefik answers 502/503/504 when the route has no live container and a plain-text 404 when it has no route at
142
+ // all. An agent's own errors are JSON, so a non-JSON 404 is the gateway, not the agent.
143
+ const gatewayText = typeof body === "object" && body !== null && "raw" in body;
144
+ if (!res.ok &&
145
+ ([502, 503, 504].includes(res.status) || (res.status === 404 && gatewayText))) {
146
+ return notRunning();
147
+ }
83
148
  if (!res.ok) {
84
149
  const detail = body?.error ?? bodyText;
85
150
  if (options.json) {
@@ -8,8 +8,11 @@ import { DetailPanel, FileTree, ResourceSelectInput, ResourceTable, ServiceIssue
8
8
  import { TABS } from "../components/TabBar.js";
9
9
  import { createTUITheme, sectionTitle, TUIThemeContext, useTUITheme, } from "../components/ui-theme.js";
10
10
  import { errorFactories, requireProjectRoot } from "../utils/errors.js";
11
+ import { readSavedHostPortPlan } from "../utils/host-port-config.js";
11
12
  import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
13
+ import { isApiServiceType } from "../utils/resource-kind.js";
12
14
  import { describeSearch } from "../utils/search-status.js";
15
+ import { buildAdvertisedTargets } from "../utils/service-urls.js";
13
16
  import { clearMetadataCache, discoverResources, discoverStacks, getResourceMetadata, getStackMetadata, } from "../utils/services.js";
14
17
  import { singleFlight } from "../utils/single-flight.js";
15
18
  import { createStatusMessageController } from "../utils/status-message.js";
@@ -19,6 +22,7 @@ import { loadTiltEvents, stripTerminalControls, TiltEventsLoadError, } from "../
19
22
  import { getTiltPollingPort } from "../utils/tilt-startup.js";
20
23
  import { formatUiKeyHint, formatUiKeyNames, UI_KEYMAP } from "../utils/ui-keymap.js";
21
24
  import { applyServiceStates, fetchServiceStates, } from "../utils/ui-service-state.js";
25
+ import { copyToClipboard, openUrl } from "../utils/url-actions.js";
22
26
  // biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
23
27
  const HelpPanel = ({ onClose }) => {
24
28
  const theme = useTUITheme();
@@ -113,6 +117,9 @@ export const TUIApp = ({ animated = true }) => {
113
117
  const statusMessage = useMemo(() => createStatusMessageController(setMessage), []);
114
118
  useEffect(() => () => statusMessage.dispose(), [statusMessage]);
115
119
  const projectRoot = findProjectRoot() || "unknown";
120
+ const ingressPort = projectRoot === "unknown"
121
+ ? undefined
122
+ : (readSavedHostPortPlan(projectRoot)?.ingressHttp ?? undefined);
116
123
  const [{ stacks, services }, setDiscovered] = useState({ stacks: [], services: [] });
117
124
  // What the running Tilt says about each service: the same answer `tdk status` gives. Empty while no Tilt answers.
118
125
  const [serviceStates, setServiceStates] = useState({});
@@ -331,6 +338,38 @@ export const TUIApp = ({ animated = true }) => {
331
338
  statusMessage.show(`Viewing config for: ${item.value}`, 2000);
332
339
  }
333
340
  }, [activeTab, selectedStack, selectedService, statusMessage]);
341
+ const resolveCurrentResource = useCallback(() => {
342
+ if (selectedService) {
343
+ return services.find((s) => s.name === selectedService) ?? null;
344
+ }
345
+ const currentItem = items[highlightedIndex];
346
+ if (currentItem) {
347
+ const directService = services.find((s) => s.name === currentItem.value);
348
+ if (directService) {
349
+ return directService;
350
+ }
351
+ const stack = stacks.find((st) => st.name === currentItem.value);
352
+ if (stack && stack.resources.length > 0) {
353
+ const frontend = stack.resources.find((s) => s.config?.appType === "frontend");
354
+ if (frontend)
355
+ return frontend;
356
+ const api = stack.resources.find((s) => isApiServiceType(s.config?.appType));
357
+ if (api)
358
+ return api;
359
+ return stack.resources[0] ?? null;
360
+ }
361
+ }
362
+ if (selectedStackData && selectedStackData.stack.resources.length > 0) {
363
+ const frontend = selectedStackData.stack.resources.find((s) => s.config?.appType === "frontend");
364
+ if (frontend)
365
+ return frontend;
366
+ const api = selectedStackData.stack.resources.find((s) => isApiServiceType(s.config?.appType));
367
+ if (api)
368
+ return api;
369
+ return selectedStackData.stack.resources[0] ?? null;
370
+ }
371
+ return null;
372
+ }, [selectedService, items, highlightedIndex, services, stacks, selectedStackData]);
334
373
  useEffect(() => {
335
374
  setRawMode(true);
336
375
  stdout.write("\x1b[?1000h");
@@ -498,6 +537,37 @@ export const TUIApp = ({ animated = true }) => {
498
537
  });
499
538
  return;
500
539
  }
540
+ if (input === "o") {
541
+ const resource = resolveCurrentResource();
542
+ const target = resource ? buildAdvertisedTargets([resource], ingressPort)[0] : undefined;
543
+ if (!target?.url) {
544
+ statusMessage.show("No URL for this resource", 2000);
545
+ }
546
+ else {
547
+ void openUrl(target.url).then((opened) => {
548
+ statusMessage.show(opened ? `Opening ${target.url}` : `Failed to open ${target.url}`, 2000);
549
+ });
550
+ }
551
+ return;
552
+ }
553
+ if (input === "y") {
554
+ const resource = resolveCurrentResource();
555
+ const target = resource ? buildAdvertisedTargets([resource], ingressPort)[0] : undefined;
556
+ if (!target?.url) {
557
+ statusMessage.show("No URL for this resource", 2000);
558
+ }
559
+ else {
560
+ void copyToClipboard(target.url).then((copied) => {
561
+ if (copied) {
562
+ statusMessage.show(`Copied ${target.url}`, 2000);
563
+ }
564
+ else {
565
+ statusMessage.show(`Failed to copy ${target.url}`, 2000);
566
+ }
567
+ });
568
+ }
569
+ return;
570
+ }
501
571
  if (key.tab) {
502
572
  const tabs = TABS.map((tab) => tab.id);
503
573
  const currentIdx = tabs.indexOf(activeTab);
@@ -604,6 +674,8 @@ function createHelpHint(activeTab, selectedStack, selectedService, showEnabledOn
604
674
  if (activeTab === "resources") {
605
675
  return [
606
676
  cycleTabs,
677
+ formatUiKeyHint("open-url", "open", ascii),
678
+ formatUiKeyHint("copy-url", "copy", ascii),
607
679
  formatUiKeyHint("refresh", "Refresh", ascii),
608
680
  formatUiKeyHint("search", "Search", ascii),
609
681
  ...common,
@@ -326,23 +326,22 @@ export function createTdkTools(run = runTdkCli, up = defaultUpDeps) {
326
326
  {
327
327
  name: "get_agent_sessions",
328
328
  description: "Inspect session checkpoints from a running AI agent resource. Set projectPath when the MCP server is not running from the project root.",
329
- inputSchema: OBJECT({
330
- projectPath: PROJECT_PATH,
331
- resource: { type: "string", description: "Name of the agent resource" },
332
- sessionId: { type: "string", description: "Optional session ID to fetch" },
333
- }),
329
+ inputSchema: {
330
+ ...OBJECT({
331
+ projectPath: PROJECT_PATH,
332
+ resource: { type: "string", description: "Name of the agent resource" },
333
+ sessionId: { type: "string", description: "Session ID to fetch (required)" },
334
+ }),
335
+ required: ["resource", "sessionId"],
336
+ },
334
337
  handler: viaCli((args) => {
335
338
  const resource = optionalName(args.resource, "resource");
336
339
  if (!resource)
337
340
  throw new Error("resource is required");
338
341
  const sessionId = optionalName(args.sessionId, "sessionId");
339
- return [
340
- "agent",
341
- "sessions",
342
- resource,
343
- ...(sessionId ? [`--session-id=${sessionId}`] : []),
344
- "--json",
345
- ];
342
+ if (!sessionId)
343
+ throw new Error("sessionId is required");
344
+ return ["agent", "sessions", resource, `--session-id=${sessionId}`, "--json"];
346
345
  }),
347
346
  },
348
347
  ];
@@ -73,6 +73,18 @@ const uiKeymap = [
73
73
  label: "Refresh data",
74
74
  group: "Actions",
75
75
  },
76
+ {
77
+ id: "open-url",
78
+ keys: ["o"],
79
+ label: "Open resource URL in browser",
80
+ group: "Actions",
81
+ },
82
+ {
83
+ id: "copy-url",
84
+ keys: ["y"],
85
+ label: "Copy resource URL to clipboard",
86
+ group: "Actions",
87
+ },
76
88
  {
77
89
  id: "search",
78
90
  keys: ["/"],
@@ -0,0 +1,103 @@
1
+ // Copyright (c) 2026 TDK Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import { spawn } from "node:child_process";
4
+ /** Validates that a string is a safe HTTP or HTTPS URL before passing to system commands. */
5
+ export function isValidHttpUrl(candidate) {
6
+ try {
7
+ const parsed = new URL(candidate);
8
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ /**
15
+ * Opens a URL in the user's default browser across platforms.
16
+ * Uses `open` on macOS, `rundll32 url.dll,FileProtocolHandler` on Windows, and `xdg-open` on Linux.
17
+ */
18
+ export function openUrl(url, platform = process.platform, spawnFn = spawn) {
19
+ if (!isValidHttpUrl(url)) {
20
+ return Promise.resolve(false);
21
+ }
22
+ let cmd;
23
+ let args;
24
+ if (platform === "darwin") {
25
+ cmd = "open";
26
+ args = [url];
27
+ }
28
+ else if (platform === "win32") {
29
+ // Avoid cmd.exe: it treats & | ^ < > in the URL as shell syntax.
30
+ cmd = "rundll32.exe";
31
+ args = ["url.dll,FileProtocolHandler", url];
32
+ }
33
+ else {
34
+ cmd = "xdg-open";
35
+ args = [url];
36
+ }
37
+ return new Promise((resolve) => {
38
+ try {
39
+ const child = spawnFn(cmd, args, {
40
+ stdio: "ignore",
41
+ detached: true,
42
+ windowsHide: true,
43
+ });
44
+ child.once("error", () => resolve(false));
45
+ child.once("spawn", () => {
46
+ if (typeof child.unref === "function") {
47
+ child.unref();
48
+ }
49
+ resolve(true);
50
+ });
51
+ }
52
+ catch {
53
+ resolve(false);
54
+ }
55
+ });
56
+ }
57
+ /**
58
+ * Copies text to the system clipboard across platforms.
59
+ * Uses `pbcopy` on macOS, `clip` on Windows, and `xclip` on Linux.
60
+ */
61
+ export function copyToClipboard(text, platform = process.platform, spawnFn = spawn) {
62
+ return new Promise((resolve) => {
63
+ let cmd;
64
+ let args;
65
+ if (platform === "darwin") {
66
+ cmd = "pbcopy";
67
+ args = [];
68
+ }
69
+ else if (platform === "win32") {
70
+ cmd = "clip";
71
+ args = [];
72
+ }
73
+ else {
74
+ cmd = "xclip";
75
+ args = ["-selection", "clipboard"];
76
+ }
77
+ try {
78
+ const child = spawnFn(cmd, args, {
79
+ stdio: ["pipe", "ignore", "ignore"],
80
+ windowsHide: true,
81
+ });
82
+ child.on("error", () => {
83
+ resolve(false);
84
+ });
85
+ child.on("close", (code) => {
86
+ resolve(code === 0);
87
+ });
88
+ if (child.stdin) {
89
+ child.stdin.on("error", () => {
90
+ resolve(false);
91
+ });
92
+ child.stdin.write(text, "utf-8");
93
+ child.stdin.end();
94
+ }
95
+ else {
96
+ resolve(false);
97
+ }
98
+ }
99
+ catch {
100
+ resolve(false);
101
+ }
102
+ });
103
+ }
@@ -12,7 +12,7 @@ import{c as Ke,e as sr}from"./chunk-42IVF3YT.js";import{a as wt}from"./chunk-6EA
12
12
  `:`# TDK (generated files and secrets)
13
13
  `}${s.join(`
14
14
  `)}
15
- `),s}import{existsSync as sa,lstatSync as oa}from"node:fs";import{isAbsolute as wr,normalize as ze,relative as ia,sep as Er}from"node:path";var vp=[`${C}/**`,".tdk/.tdk-out/**","Tiltfile","TILT_*.star",q,`${A} (create only)`,"shared-platform-engineering/docker-templates/**","services/platform/database-management/docker-compose.yml","product_autogenerated_resource_manifests/**","product_autogenerated_services_list/**"];function aa(e){let t=ze(e).split(Er).join("/");return wr(e)||t===".."||t.startsWith("../")?!1:t.split("/").includes(C)||t.startsWith(".tdk/.tdk-out/")||t==="shared-platform-engineering/docker-templates"||t.startsWith("shared-platform-engineering/docker-templates/")||t.startsWith("product_autogenerated_resource_manifests/")||t.startsWith("product_autogenerated_services_list/")?!0:t==="Tiltfile"||/^TILT_[^/]+\.star$/.test(t)||t===q||t===A||t==="services/platform/database-management/docker-compose.yml"}function oe(e,t){let n=wr(t)?ze(t):ze(`${e}/${t}`),r=ia(e,n).split(Er).join("/");if(!aa(r))throw new Error(`Refusing to write outside TDK's generated-file allowlist: ${r}`);let s=e;for(let o of r.split("/"))if(s=ze(`${s}/${o}`),sa(s)&&oa(s).isSymbolicLink())throw new Error(`Refusing to write through a symbolic link in generated path: ${r}`);return n}var _e=[j,"proxy"];import{existsSync as Gr,readFileSync as Vr}from"node:fs";import{copyFileSync as Ar,mkdirSync as ya,rmSync as Dr,writeFileSync as Or}from"node:fs";import{join as de}from"node:path";function I(e){return e==="backend"||e==="mcp"||e==="agent"}import{readFileSync as ma}from"node:fs";import{join as ha}from"node:path";import{existsSync as ua}from"node:fs";import{dirname as la,join as da,resolve as Pr}from"node:path";import{cwd as pa}from"node:process";var vr={name:"tdk-cli-internal",private:!0,version:"1.3.173",description:"TDK CLI \u2014 start services on your machine.",keywords:["tdk","tilt","cli","local-dev","microservices"],homepage:"https://github.com/tdk-landscape/tdk-cli-core#readme",bugs:{url:"https://github.com/tdk-landscape/tdk-cli-core/issues"},repository:{type:"git",url:"git+https://github.com/tdk-landscape/tdk-cli-core.git",directory:"cli"},license:"MIT",author:"TDK Team",type:"module",main:"./dist/index.js",types:"./dist/index.d.ts",bin:{tdk:"bin/tdk.js","tdk-cli":"bin/tdk.js"},files:["dist","dist-bundle","bin","templates","README.md","Tiltfile","engine","discovery","specs","ext","!ext/ide-components/code_executor","shared-platform-engineering/docker-templates"],scripts:{build:"tsc && node scripts/bundle.mjs","build:watch":"tsc --watch",lint:"biome check .","lint:fix":"biome check . --write",typecheck:"tsc --project tsconfig.typecheck.json",clean:"rm -rf dist",dev:"bun run build:watch",test:"vitest run","test:watch":"vitest",prepack:"../scripts/stage-npm-engine.sh && npm run build"},dependencies:{commander:"^15.0.0",handlebars:"^4.7.8"},devDependencies:{"@biomejs/biome":"^2.5.13","@types/node":"^22.0.0","@types/react":"^19.3.0",ajv:"^8.17.1",chalk:"^6.0.1",esbuild:"^0.28.2",ink:"^8.0.0",knip:"^6.38.0",react:"^19.3.0","react-devtools-core":"^8.0.0",typescript:"^7.0.2",vite:"^8.3.1",vitest:"^5.0.0"},engines:{node:">=22.12.0"}};function y(e=pa()){let t=Pr(e),n=Pr("/");for(;t!==n;){if(ua(da(t,".tdk",_)))return t;let r=la(t);if(r===t)break;t=r}return null}var Ye=null;function fa(){if(Ye)return Ye;let e=vr;if(!e||typeof e!="object"||Array.isArray(e))throw new Error("Invalid package.json: expected object");let t=e;return Ye={name:String(t.name??"@tdk-landscape/tdk-cli-core"),version:String(t.version??"0.0.0"),fullPackage:t},Ye}function $r(){return fa().version}function le(){let e=y();if(e)try{let t=ma(ha(e,".tdk",_),"utf-8"),n=JSON.parse(t);if(n?.project?.name)return n.project.name}catch{}return"tdk-project"}function Ft(e){let t=le(),n=process.env.TDK_SERVICE_BASE_URL??`http://${t}.localhost${e?`:${e}`:""}`;try{let r=new URL(n.includes("://")?n:`http://${n}`),s=r.hostname,o=r.protocol==="https:"?"443":"80",i=r.port||(e?String(e):""),a=i===o?"":i;if(s==="localhost"||/^\d+\.\d+\.\d+\.\d+$/.test(s))return{appBase:`${r.protocol}//${s}${a?`:${a}`:""}`,apiBase:`${r.protocol}//${s}${a?`:${a}`:""}`};let c=s.replace(/^(app|api)\./,"");return{appBase:`${r.protocol}//app.${c}${a?`:${a}`:""}`,apiBase:`${r.protocol}//api.${c}${a?`:${a}`:""}`}}catch{return{appBase:`http://app.${t}.localhost`,apiBase:`http://api.${t}.localhost`}}}function ga(e){return`${e.replace(/\/+$/,"")}/health`}function Lt(e){if(I(e.config?.appType)){let t=e.name.replace(/-api$/,"");return e.config?.apiPath??`/api/${t}`}return e.config?.basePath??`/${e.name}`}function Mt(e,t){return Rr(e,t,n=>ga(n))}function _r(e,t){return Rr(e,t,n=>n)}function Rr(e,t,n){let{appBase:r,apiBase:s}=Ft(t);return e.filter(o=>o.config?.appType==="frontend"||I(o.config?.appType)).map(o=>{let i=I(o.config.appType)?s:r;return{name:o.name,appType:o.config.appType,url:`${i}${n(Lt(o))}`}})}async function ka(e,t){try{let n=await fetch(e.url,{method:"GET",redirect:"manual",signal:AbortSignal.timeout(t)});return{...e,ok:n.ok,status:n.status}}catch(n){let r=n instanceof Error&&n.name==="TimeoutError"?`no response within ${t}ms`:n instanceof Error?n.message:String(n);return{...e,ok:!1,error:r}}}async function Xe(e,t){return Promise.all(e.map(n=>ka(n,t)))}var Lr=60,Nr=1e3,Cr=1e4,jr=200,Ir=64*1024,Fr=new Set([404,502,503,504]),Sa=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN","EHOSTUNREACH","ENETUNREACH"]),ba=new Set(["GET","HEAD","OPTIONS"]);function X(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Ta(e,t){if(ba.has(t))return!0;if(!X(e))return!1;let n=X(e.cause)?e.cause:void 0,r=e.code??n?.code;return typeof r=="string"&&Sa.has(r)}function Bt(e){if(!X(e))return['smoke: must be an object with "via" and "steps"'];let t=[];return e.via!=="proxy"&&t.push('smoke.via: only "proxy" is supported (the check must go through the public URL)'),e.timeoutSeconds!==void 0&&!(typeof e.timeoutSeconds=="number"&&e.timeoutSeconds>0)&&t.push("smoke.timeoutSeconds: must be a number greater than 0"),!Array.isArray(e.steps)||e.steps.length===0?(t.push("smoke.steps: needs at least one step"),t):(e.steps.forEach((n,r)=>{let s=`smoke.steps[${r}]`;if(!X(n)){t.push(`${s}: must be an object`);return}let o=n.path;(typeof o!="string"||!o.startsWith("/")||o.startsWith("//"))&&t.push(`${s}.path: must start with "/" and is appended to the public URL; a full URL or relative path would skip the proxy`),n.expect!==void 0&&!(typeof n.expect=="number"&&Number.isInteger(n.expect)&&n.expect>=100&&n.expect<=599)&&t.push(`${s}.expect: must be an HTTP status between 100 and 599`),n.bodyContains!==void 0&&typeof n.bodyContains!="string"&&t.push(`${s}.bodyContains: must be a string`),n.save!==void 0&&(X(n.save)&&Object.values(n.save).every(a=>typeof a=="string"&&a.startsWith("$."))||t.push(`${s}.save: must map names to paths such as "$.id"`))}),t)}function Mr(e,t){let{appBase:n,apiBase:r}=Ft(t),s=[];for(let o of e){let i=o.config;if(!i?.smoke)continue;let a=i.appType,c=I(a)||a===J;if(!c&&a!=="frontend"||i.exposeViaProxy===!1)continue;let u=c?r:n,d=i.healthCheckPath||(c&&a!==J?"/health":void 0);s.push({name:o.name,baseUrl:`${u}${Lt(o).replace(/\/+$/,"")}`,...d?.startsWith("/")?{readyPath:d}:{},smoke:i.smoke})}return s}var xa=10;function Ur(e,t={}){let n=t.timeoutSeconds??xa;return e.flatMap(r=>{let s=r.smoke.steps.filter(i=>(i.method??"GET").toUpperCase()==="GET"&&i.body===void 0&&!i.path.includes("{{")).map(i=>({...i,bodyContains:void 0}));if(s.length===0)return[];let o=Math.min(r.smoke.timeoutSeconds??Lr,n);return[{name:r.name,baseUrl:r.baseUrl,smoke:{via:r.smoke.via,timeoutSeconds:o,steps:s}}]})}function wa(e,t){let n=t.replace(/^\$\.?/,"").match(/[^.[\]]+/g)??[],r=e;for(let s of n){if(r==null||s==="__proto__"||s==="constructor"||s==="prototype"||!Object.hasOwn(r,s))return;let o=Object.getOwnPropertyDescriptor(r,s);if(!o||!("value"in o))return;r=o.value}return r}function Br(e,t){return e.replace(/\{\{(\w+)\}\}/g,(n,r)=>t[r]??n)}function Ut(e,t){return typeof e=="string"?Br(e,t):Array.isArray(e)?e.map(n=>Ut(n,t)):X(e)?Object.fromEntries(Object.entries(e).map(([n,r])=>[n,Ut(r,t)])):e}function Qe(e){let t=e.replace(/\s+/g," ").trim();return t.length>jr?`${t.slice(0,jr)}...`:t}function Hr(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"step"}function Ea(e,t){let n=Hr(t),r=(e.get(n)??0)+1;return e.set(n,r),r===1?n:`${n}-${r}`}function va(e,t,n,r,s){try{let o=de(e,Hr(n.service),t);ya(o,{recursive:!0});let i=de(o,"latest.json"),a=de(o,"body.txt");return r===void 0?Dr(a,{force:!0}):Or(a,r),Or(i,`${JSON.stringify({...n,bodyPath:r===void 0?null:"body.txt"},null,2)}
15
+ `),s}import{existsSync as sa,lstatSync as oa}from"node:fs";import{isAbsolute as wr,normalize as ze,relative as ia,sep as Er}from"node:path";var vp=[`${C}/**`,".tdk/.tdk-out/**","Tiltfile","TILT_*.star",q,`${A} (create only)`,"shared-platform-engineering/docker-templates/**","services/platform/database-management/docker-compose.yml","product_autogenerated_resource_manifests/**","product_autogenerated_services_list/**"];function aa(e){let t=ze(e).split(Er).join("/");return wr(e)||t===".."||t.startsWith("../")?!1:t.split("/").includes(C)||t.startsWith(".tdk/.tdk-out/")||t==="shared-platform-engineering/docker-templates"||t.startsWith("shared-platform-engineering/docker-templates/")||t.startsWith("product_autogenerated_resource_manifests/")||t.startsWith("product_autogenerated_services_list/")?!0:t==="Tiltfile"||/^TILT_[^/]+\.star$/.test(t)||t===q||t===A||t==="services/platform/database-management/docker-compose.yml"}function oe(e,t){let n=wr(t)?ze(t):ze(`${e}/${t}`),r=ia(e,n).split(Er).join("/");if(!aa(r))throw new Error(`Refusing to write outside TDK's generated-file allowlist: ${r}`);let s=e;for(let o of r.split("/"))if(s=ze(`${s}/${o}`),sa(s)&&oa(s).isSymbolicLink())throw new Error(`Refusing to write through a symbolic link in generated path: ${r}`);return n}var _e=[j,"proxy"];import{existsSync as Gr,readFileSync as Vr}from"node:fs";import{copyFileSync as Ar,mkdirSync as ya,rmSync as Dr,writeFileSync as Or}from"node:fs";import{join as de}from"node:path";function I(e){return e==="backend"||e==="mcp"||e==="agent"}import{readFileSync as ma}from"node:fs";import{join as ha}from"node:path";import{existsSync as ua}from"node:fs";import{dirname as la,join as da,resolve as Pr}from"node:path";import{cwd as pa}from"node:process";var vr={name:"tdk-cli-internal",private:!0,version:"1.3.175",description:"TDK CLI \u2014 start services on your machine.",keywords:["tdk","tilt","cli","local-dev","microservices"],homepage:"https://github.com/tdk-landscape/tdk-cli-core#readme",bugs:{url:"https://github.com/tdk-landscape/tdk-cli-core/issues"},repository:{type:"git",url:"git+https://github.com/tdk-landscape/tdk-cli-core.git",directory:"cli"},license:"MIT",author:"TDK Team",type:"module",main:"./dist/index.js",types:"./dist/index.d.ts",bin:{tdk:"bin/tdk.js","tdk-cli":"bin/tdk.js"},files:["dist","dist-bundle","bin","templates","README.md","Tiltfile","engine","discovery","specs","ext","!ext/ide-components/code_executor","shared-platform-engineering/docker-templates"],scripts:{build:"tsc && node scripts/bundle.mjs","build:watch":"tsc --watch",lint:"biome check .","lint:fix":"biome check . --write",typecheck:"tsc --project tsconfig.typecheck.json",clean:"rm -rf dist",dev:"bun run build:watch",test:"vitest run","test:watch":"vitest",prepack:"../scripts/stage-npm-engine.sh && npm run build"},dependencies:{commander:"^15.0.0",handlebars:"^4.7.8"},devDependencies:{"@biomejs/biome":"^2.5.13","@types/node":"^22.0.0","@types/react":"^19.3.0",ajv:"^8.17.1",chalk:"^6.0.1",esbuild:"^0.28.2",ink:"^8.0.0",knip:"^6.38.0",react:"^19.3.0","react-devtools-core":"^8.0.0",typescript:"^7.0.2",vite:"^8.3.1",vitest:"^5.0.0"},engines:{node:">=22.12.0"}};function y(e=pa()){let t=Pr(e),n=Pr("/");for(;t!==n;){if(ua(da(t,".tdk",_)))return t;let r=la(t);if(r===t)break;t=r}return null}var Ye=null;function fa(){if(Ye)return Ye;let e=vr;if(!e||typeof e!="object"||Array.isArray(e))throw new Error("Invalid package.json: expected object");let t=e;return Ye={name:String(t.name??"@tdk-landscape/tdk-cli-core"),version:String(t.version??"0.0.0"),fullPackage:t},Ye}function $r(){return fa().version}function le(){let e=y();if(e)try{let t=ma(ha(e,".tdk",_),"utf-8"),n=JSON.parse(t);if(n?.project?.name)return n.project.name}catch{}return"tdk-project"}function Ft(e){let t=le(),n=process.env.TDK_SERVICE_BASE_URL??`http://${t}.localhost${e?`:${e}`:""}`;try{let r=new URL(n.includes("://")?n:`http://${n}`),s=r.hostname,o=r.protocol==="https:"?"443":"80",i=r.port||(e?String(e):""),a=i===o?"":i;if(s==="localhost"||/^\d+\.\d+\.\d+\.\d+$/.test(s))return{appBase:`${r.protocol}//${s}${a?`:${a}`:""}`,apiBase:`${r.protocol}//${s}${a?`:${a}`:""}`};let c=s.replace(/^(app|api)\./,"");return{appBase:`${r.protocol}//app.${c}${a?`:${a}`:""}`,apiBase:`${r.protocol}//api.${c}${a?`:${a}`:""}`}}catch{return{appBase:`http://app.${t}.localhost`,apiBase:`http://api.${t}.localhost`}}}function ga(e){return`${e.replace(/\/+$/,"")}/health`}function Lt(e){if(I(e.config?.appType)){let t=e.name.replace(/-api$/,"");return e.config?.apiPath??`/api/${t}`}return e.config?.basePath??`/${e.name}`}function Mt(e,t){return Rr(e,t,n=>ga(n))}function _r(e,t){return Rr(e,t,n=>n)}function Rr(e,t,n){let{appBase:r,apiBase:s}=Ft(t);return e.filter(o=>o.config?.appType==="frontend"||I(o.config?.appType)).map(o=>{let i=I(o.config.appType)?s:r;return{name:o.name,appType:o.config.appType,url:`${i}${n(Lt(o))}`}})}async function ka(e,t){try{let n=await fetch(e.url,{method:"GET",redirect:"manual",signal:AbortSignal.timeout(t)});return{...e,ok:n.ok,status:n.status}}catch(n){let r=n instanceof Error&&n.name==="TimeoutError"?`no response within ${t}ms`:n instanceof Error?n.message:String(n);return{...e,ok:!1,error:r}}}async function Xe(e,t){return Promise.all(e.map(n=>ka(n,t)))}var Lr=60,Nr=1e3,Cr=1e4,jr=200,Ir=64*1024,Fr=new Set([404,502,503,504]),Sa=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN","EHOSTUNREACH","ENETUNREACH"]),ba=new Set(["GET","HEAD","OPTIONS"]);function X(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Ta(e,t){if(ba.has(t))return!0;if(!X(e))return!1;let n=X(e.cause)?e.cause:void 0,r=e.code??n?.code;return typeof r=="string"&&Sa.has(r)}function Bt(e){if(!X(e))return['smoke: must be an object with "via" and "steps"'];let t=[];return e.via!=="proxy"&&t.push('smoke.via: only "proxy" is supported (the check must go through the public URL)'),e.timeoutSeconds!==void 0&&!(typeof e.timeoutSeconds=="number"&&e.timeoutSeconds>0)&&t.push("smoke.timeoutSeconds: must be a number greater than 0"),!Array.isArray(e.steps)||e.steps.length===0?(t.push("smoke.steps: needs at least one step"),t):(e.steps.forEach((n,r)=>{let s=`smoke.steps[${r}]`;if(!X(n)){t.push(`${s}: must be an object`);return}let o=n.path;(typeof o!="string"||!o.startsWith("/")||o.startsWith("//"))&&t.push(`${s}.path: must start with "/" and is appended to the public URL; a full URL or relative path would skip the proxy`),n.expect!==void 0&&!(typeof n.expect=="number"&&Number.isInteger(n.expect)&&n.expect>=100&&n.expect<=599)&&t.push(`${s}.expect: must be an HTTP status between 100 and 599`),n.bodyContains!==void 0&&typeof n.bodyContains!="string"&&t.push(`${s}.bodyContains: must be a string`),n.save!==void 0&&(X(n.save)&&Object.values(n.save).every(a=>typeof a=="string"&&a.startsWith("$."))||t.push(`${s}.save: must map names to paths such as "$.id"`))}),t)}function Mr(e,t){let{appBase:n,apiBase:r}=Ft(t),s=[];for(let o of e){let i=o.config;if(!i?.smoke)continue;let a=i.appType,c=I(a)||a===J;if(!c&&a!=="frontend"||i.exposeViaProxy===!1)continue;let u=c?r:n,d=i.healthCheckPath||(c&&a!==J?"/health":void 0);s.push({name:o.name,baseUrl:`${u}${Lt(o).replace(/\/+$/,"")}`,...d?.startsWith("/")?{readyPath:d}:{},smoke:i.smoke})}return s}var xa=10;function Ur(e,t={}){let n=t.timeoutSeconds??xa;return e.flatMap(r=>{let s=r.smoke.steps.filter(i=>(i.method??"GET").toUpperCase()==="GET"&&i.body===void 0&&!i.path.includes("{{")).map(i=>({...i,bodyContains:void 0}));if(s.length===0)return[];let o=Math.min(r.smoke.timeoutSeconds??Lr,n);return[{name:r.name,baseUrl:r.baseUrl,smoke:{via:r.smoke.via,timeoutSeconds:o,steps:s}}]})}function wa(e,t){let n=t.replace(/^\$\.?/,"").match(/[^.[\]]+/g)??[],r=e;for(let s of n){if(r==null||s==="__proto__"||s==="constructor"||s==="prototype"||!Object.hasOwn(r,s))return;let o=Object.getOwnPropertyDescriptor(r,s);if(!o||!("value"in o))return;r=o.value}return r}function Br(e,t){return e.replace(/\{\{(\w+)\}\}/g,(n,r)=>t[r]??n)}function Ut(e,t){return typeof e=="string"?Br(e,t):Array.isArray(e)?e.map(n=>Ut(n,t)):X(e)?Object.fromEntries(Object.entries(e).map(([n,r])=>[n,Ut(r,t)])):e}function Qe(e){let t=e.replace(/\s+/g," ").trim();return t.length>jr?`${t.slice(0,jr)}...`:t}function Hr(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"step"}function Ea(e,t){let n=Hr(t),r=(e.get(n)??0)+1;return e.set(n,r),r===1?n:`${n}-${r}`}function va(e,t,n,r,s){try{let o=de(e,Hr(n.service),t);ya(o,{recursive:!0});let i=de(o,"latest.json"),a=de(o,"body.txt");return r===void 0?Dr(a,{force:!0}):Or(a,r),Or(i,`${JSON.stringify({...n,bodyPath:r===void 0?null:"body.txt"},null,2)}
16
16
  `),s&&(Ar(i,de(o,"last-success.json")),r===void 0?Dr(de(o,"last-success-body.txt"),{force:!0}):Ar(a,de(o,"last-success-body.txt"))),i}catch{return}}function Pa(e){let t=Buffer.from(e,"utf8");if(t.length<=Ir)return{body:e,truncated:!1,bytes:t.length};let n=t.subarray(0,Ir).toString("utf8").replace(/\uFFFD$/,"");return{body:n,truncated:!0,bytes:Buffer.byteLength(n,"utf8")}}async function $a(e,t={}){let n=t.fetch??((d,l)=>fetch(d,l)),r=t.now??Date.now,s=t.sleep??(d=>new Promise(l=>setTimeout(l,d))),o=(e.smoke.timeoutSeconds??Lr)*1e3,i=r()+o,a={},c=Bt(e.smoke);if(c.length>0)return{name:e.name,ok:!1,failure:`${e.name}: invalid smoke block: ${c.join("; ")}`};if(e.readyPath){let d=`${e.baseUrl}${e.readyPath}`;for(;;){try{let l=await n(d,{method:"GET",signal:AbortSignal.timeout(Cr)});if(await l.text(),!Fr.has(l.status)&&l.status<500)break}catch{}if(r()>=i)break;await s(Nr)}}i=r()+o;let u=new Map;for(let[d,l]of e.smoke.steps.entries()){let p=l.name??`step ${d+1}`,h=(l.method??"GET").toUpperCase(),f=`${e.baseUrl}${Br(l.path,a)}`,b=l.expect??200,w=l.body!==void 0,$,U="",g,R;for(;;){g=void 0,R=void 0,$=void 0;let G=!1;try{let F=await n(f,{method:h,headers:w?{"content-type":"application/json"}:void 0,body:w?JSON.stringify(Ut(l.body,a)):void 0,signal:AbortSignal.timeout(Cr)});$=F.status,U=await F.text()}catch(F){G=Ta(F,h),g=F instanceof Error?F.message:String(F);let re=X(F)?F.code??(X(F.cause)?F.cause.code:void 0):void 0;R=typeof re=="string"?re:void 0}if(!(g!==void 0&&G||$!==void 0&&$!==b&&Fr.has($))||r()>=i)break;await s(Nr)}let ne=`${e.name}: ${p}: ${h} ${f}`,ue;if(t.recordDir){let G=$===void 0?void 0:Pa(U),Te=g===void 0&&$===b&&(l.bodyContains===void 0||U.includes(l.bodyContains));ue=va(t.recordDir,Ea(u,p),{service:e.name,step:p,method:h,url:f,expectedStatus:b,status:$??null,...g===void 0?{}:{error:R??g},truncated:G?.truncated??!1,bodyBytes:G?.bytes??0},G?.body,Te)}let be=G=>({name:e.name,ok:!1,failure:G,recordPath:ue});if(g!==void 0)return be(`${ne} never answered (${g})`);if($!==b)return be(`${ne} returned ${$}, expected ${b}${U?`: ${Qe(U)}`:""}`);if(l.bodyContains!==void 0&&!U.includes(l.bodyContains))return be(`${ne} returned ${$} but the body does not contain "${l.bodyContains}": ${Qe(U)}`);if(l.save){let G;try{G=JSON.parse(U)}catch{return be(`${ne} returned a body that is not JSON, cannot save: ${Qe(U)}`)}for(let[Te,F]of Object.entries(l.save)){let re=wa(G,F);if(re==null||typeof re=="object")return be(`${ne} response has no ${F} to save as {{${Te}}}: ${Qe(U)}`);a[Te]=String(re)}}}return{name:e.name,ok:!0}}function qp(e){return`Smoke check failed: ${e.failure}${e.recordPath?` (record: ${e.recordPath})`:""}`}async function Kr(e,t={}){return Promise.all(e.map(n=>$a(n,t)))}var Re=1,Qp="https://tdk-landscape.github.io/schema.service.json",_a={dependencies:"deprecated, use dependsOn (still read for now)",envVars:"deprecated, use params (still read when params is absent)",jwtSecret:"ignored. A secret must not be committed: remove it and set JWT_SECRET in the project .env"},Ra=new Set(["$schema","agent","apiBasePath","apiPath","appName","appType","backendName","basePath","build","buildContext","databaseName","dependencies","dev","dependsOn","dockerfile","enabled","exposeViaProxy","featuresEnabled","framework","healthCheckPath","image","language","name","nats","params","playwright","port","replicas","restart","runtime","sablier","schemaVersion","secrets","smoke","stack","syncs","traefik","type"]),Aa=[{path:["healthCheckPath"],pattern:/^\/[a-z0-9/-]*$/,expected:"a path such as /health"},{path:["traefik","host"],pattern:/^[A-Za-z0-9.-]+$/,expected:"a hostname such as shop.backend.myproject.local"},{path:["traefik","pathPrefix"],pattern:/^\/[a-z0-9/-]*$/,expected:"a path such as /api/v1/shop-management"},{path:["traefik","healthCheck"],pattern:/^\/[a-z0-9/-]*$/,expected:"a path such as /health"},{path:["nats","queueGroup"],pattern:/^[A-Za-z0-9_.-]+$/,expected:"letters, digits, '_', '.' or '-'"},{path:["databaseName"],pattern:/^[A-Za-z0-9_-]+$/,expected:"letters, digits, '_' or '-'"},{path:["stack"],pattern:/^[A-Za-z0-9_-]+$/,expected:"letters, digits, '_' or '-'"},{path:["image"],pattern:/^[A-Za-z0-9._/:@-]+$/,expected:"an image reference such as ghcr.io/acme/app:1.0"}];function Ze(e,t){let n=[],r=new Set;for(let{path:s,pattern:o,expected:i}of Aa){let a=e;for(let u of s)a=a&&typeof a=="object"&&!Array.isArray(a)?a[u]:void 0;if(a==null||a==="")continue;let c=`${t}.${s.join(".")}`;(typeof a!="string"||!o.test(a))&&r.add(c),typeof a!="string"?n.push(`${c}: expected a string`):o.test(a)||n.push(`${c}: must be ${i} (no spaces, line breaks or YAML syntax)`)}for(let[s,o]of Da(e,t))r.has(s)||n.push(`${s}: ${o}`);return n}function Ht(e){for(let t=0;t<e.length;t++){let n=e.charCodeAt(t);if(n<32||n===127||n===133||n===8232||n===8233)return!0}return!1}function Da(e,t){let n=[],r=Object.entries(e).filter(([s])=>s!=="smoke").map(([s,o])=>[o,`${t}.${s}`]);for(let s of Object.keys(e))Ht(s)&&n.push([t,"a key contains a line break"]);for(;r.length>0;){let[s,o]=r.pop();if(typeof s=="string")Ht(s)&&n.push([o,"must not contain line breaks or control characters"]);else if(Array.isArray(s))for(let[i,a]of s.entries())r.push([a,`${o}[${i}]`]);else if(s&&typeof s=="object")for(let[i,a]of Object.entries(s))Ht(i)?n.push([o,"a key contains a line break"]):r.push([a,`${o}.${i}`])}return n.sort(([s],[o])=>s.localeCompare(o))}var Oa=["apiPath","basePath"];function Kt(e){if(!e||typeof e!="object"||Array.isArray(e))return[];let t=e;return Oa.filter(n=>n in t&&typeof t[n]!="string")}function Gt(e,t){if(!e||typeof e!="object"||Array.isArray(e))return{errors:[`${t}: expected a JSON object`],warnings:[]};let n=e,r=[];for(let a of["appName","appType","stack","schemaVersion"])a in n||r.push(`${t}.${a}: required field is missing`);for(let a of["appName","appType","stack"])a in n&&typeof n[a]!="string"&&r.push(`${t}.${a}: expected a string`);"schemaVersion"in n&&n.schemaVersion!==Re&&r.push(`${t}.schemaVersion: unsupported version ${String(n.schemaVersion)} (supported: ${Re})`);for(let a of Kt(n))r.push(`${t}.${a}: expected a string`);let s=n.dev,o=s&&typeof s=="object"&&!Array.isArray(s)?s.liveReload:void 0;if(o!==void 0&&typeof o!="boolean"&&r.push(`${t}.dev.liveReload: expected true or false`),r.push(...Ze(n,t)),"smoke"in n)for(let a of Bt(n.smoke))r.push(`${t}.${a}`);let i=Object.keys(n).flatMap(a=>{let c=_a[a];return c?[`${t}.${a}: ${c}`]:Ra.has(a)?[]:[`${t}.${a}: unknown field is preserved`]});return o===!0&&n.language!=="go"&&i.push(`${t}.dev.liveReload: only applies to Go services (language "go"); it is ignored here`),{errors:r,warnings:i,manifest:n}}function Wr(e,t){if(!Gr(e))return[];try{let n=JSON.parse(Vr(e,"utf-8"));return!n||typeof n!="object"||Array.isArray(n)?[]:Ze(n,t)}catch{return[]}}function Jr(e,t){if(!Gr(e))return{errors:[`${t}: file is missing`],warnings:[]};try{let n=JSON.parse(Vr(e,"utf-8"));return Gt(n,t)}catch(n){let r=n instanceof Error?n.message:String(n);return{errors:[`${t}: invalid JSON (${r})`],warnings:[]}}}import{existsSync as pe,readdirSync as ls,readFileSync as ds,statSync as Xt}from"node:fs";import{dirname as tc,join as Z}from"node:path";var Ae=class{data;options;constructor(t){this.data=new Map,this.options=t}has(t){let n=this.data.get(t);return n?Date.now()-n.timestamp>this.options.ttlMs?(this.data.delete(t),!1):!0:!1}get(t){if(this.has(t))return this.data.get(t)?.value}set(t,n){this.data.set(t,{value:n,timestamp:Date.now()})}delete(t){this.data.delete(t)}clear(){this.data.clear()}keys(){let t=[];for(let n of this.data.keys())this.has(n)&&t.push(n);return t}size(){return this.keys().length}};function qr(e){let t=0;return{isValid(){return Date.now()-t<e},markUpdated(){t=Date.now()},reset(){t=0}}}import{execFileSync as Fa,execSync as La}from"node:child_process";import{existsSync as zr}from"node:fs";import{delimiter as Yr,extname as Xr,join as Qr}from"node:path";import{delimiter as nf,extname as Na}from"node:path";function De(){return process.platform==="win32"}function Ca(e){return e==="linux"?"linux":e==="darwin"?"darwin":e==="win32"?"windows":null}function ja(e){return e==="x64"?"amd64":e==="arm64"?"arm64":null}function Ia(e,t){let n=Ca(e),r=ja(t);if(!n||!r||n==="windows"&&r!=="amd64")return null;let s=`tdk-${n}-${r}`;return n==="windows"?`${s}.exe`:s}function rf(){return Ia(process.platform,process.arch)}function sf(){return De()?"tdk.exe":"tdk"}function Vt(e,t=De()){return t?Na(e)?[e]:[e,`${e}.exe`,`${e}.cmd`,`${e}.bat`]:[e]}function O(e){let t=process.env.PATH??process.env.Path??"",n=De(),r=Vt(e,n);if(n&&!Xr(e))for(let s of(process.env.PATHEXT??".EXE;.CMD;.BAT;.COM").split(";")){let o=s.toLowerCase();o&&!r.includes(`${e}${o}`)&&r.push(`${e}${o}`)}for(let s of t.split(Yr))if(s)for(let o of r){let i=Qr(s,o);if(zr(i))return i}return null}function Zr(e){let t=process.env.PATH??process.env.Path??"",n=De(),r=Vt(e,n);if(n&&!Xr(e))for(let i of(process.env.PATHEXT??".EXE;.CMD;.BAT;.COM").split(";")){let a=i.toLowerCase();a&&!r.includes(`${e}${a}`)&&r.push(`${e}${a}`)}let s=[],o=new Set;for(let i of t.split(Yr))if(i)for(let a of r){let c=Qr(i,a);if(!zr(c))continue;let u=n?c.toLowerCase():c;o.has(u)||(o.add(u),s.push(c))}return s}var es=1e4;function ts(){let e=!1;if(process.platform==="win32")try{return Fa(O("docker")??"docker",["ps"],{stdio:"ignore",timeout:es,windowsHide:!0}),"running"}catch(t){return t.code==="ETIMEDOUT"?"unresponsive":"missing"}for(let t of["docker ps","colima status","podman ps"])try{return La(t,{stdio:"ignore",timeout:es}),"running"}catch(n){n.code==="ETIMEDOUT"&&(e=!0)}return e?"unresponsive":"missing"}function et(e,t){return e[t]??0}function Ma(e,t){let n=Array.from({length:t.length+1},(r,s)=>s);for(let r=1;r<=e.length;r++){let s=[r];for(let o=1;o<=t.length;o++){let i=e[r-1]===t[o-1]?0:1;s[o]=Math.min(et(s,o-1)+1,et(n,o)+1,et(n,o-1)+i)}n=s}return et(n,t.length)}function Ua(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length-1;n++)if(e[n]!==t[n]&&e[n]===t[n+1]&&e[n+1]===t[n]&&e.slice(0,n)===t.slice(0,n)&&e.slice(n+2)===t.slice(n+2))return!0;return!1}function ns(e,t){if(e.length<2||t.length===0||t.includes(e))return;let n=e.toLowerCase(),r,s=Number.POSITIVE_INFINITY;for(let o of t){if(o.length===0)continue;let i=o.toLowerCase(),a=Ua(n,i)?1:Ma(n,i),c=Math.max(1,Math.min(2,Math.floor(Math.max(n.length,i.length)/3)));a>c||(a<s||a===s&&(r===void 0||o<r))&&(r=o,s=a)}return r}import{spawn as Ba}from"node:child_process";import{join as Ha}from"node:path";function Ka(e,t=[],n={}){return new Promise((r,s)=>{let o=[e,...t];n.verbose&&console.log(`Executing: tilt ${o.join(" ")}`);let i=Ba(O("tilt")??"tilt",o,{stdio:n.inheritStdio?"inherit":"pipe",shell:!1,windowsHide:process.platform==="win32",timeout:n.timeoutMs}),a="",c="";n.inheritStdio||(i.stdout?.on("data",u=>{a+=u.toString()}),i.stderr?.on("data",u=>{c+=u.toString()})),i.on("close",u=>{r({exitCode:u??1,stdout:a,stderr:c})}),i.on("error",u=>{s(new Error(`Failed to spawn tilt: ${u.message}`))})})}async function rs(){try{return(await Ka("version",[],{inheritStdio:!1,timeoutMs:1e4})).exitCode===0}catch{return!1}}function Ga(){let e=y();if(!e)throw Y.notInProject();return Ha(e,".tdk",".tdk-out","Tiltfile")}function ss(e){let t=Ga();e.push("-f",t)}function bf(e,t={}){let n=[];return ss(n),t.focusTargets&&t.focusTargets.length>0&&n.push("--",`--focus=${t.focusTargets.join(",")}`),n.push(...e),t.verbose&&!t.quiet&&n.push("--verbose"),t.watch&&n.push("--watch"),n}function Tf(e={}){let t=[];return ss(t),e.focusTargets&&e.focusTargets.length>0&&t.push("--",`--focus=${e.focusTargets.join(",")}`),t}function os(e){return e instanceof Error?e.message:String(e)}function Rf(e,t){if(process.env.TDK_VERBOSE){let n=t!==void 0?`: ${os(t)}`:"";console.warn(m.gray(`${e}${n}`))}}var N=class extends Error{suggestions;exitCode;constructor(t,n=[],r=1){super(t),this.name="TdkError",this.suggestions=n,this.exitCode=r}display(){Ja(this.message,void 0,this.suggestions)}exit(){this.display(),process.exit(this.exitCode)}};function Wt(e,t,n=!1){let r=new Set;for(let s of e){let o=ns(s,t);o&&r.add(n?`Did you mean "${o}" for "${s}"?`:`Did you mean "${o}"?`)}return[...r]}var Y={tiltNotInstalled:()=>new N("Tilt CLI is not installed",["Install Tilt: `brew install tilt` (macOS)","Or download from: https://docs.tilt.dev/install.html","Verify with: `tilt version`",`Setup guide: ${Ct}`]),dockerNotAvailable:()=>new N("Docker (or a compatible container runtime) is not running",["Start Docker Desktop, then verify with: `docker ps`","Or start Colima: `colima start`","Or start the Docker service on Linux: `sudo systemctl start docker`",`Setup guide: ${Ct}`]),dockerNotResponding:()=>new N("Docker is running but not responding (`docker ps` hung for 10s)",["Quit and reopen Docker Desktop, or run `colima restart`","Then verify with: `docker ps`"]),stackNotFound:(e,t=[])=>new N(`Stack "${e}" not found`,[...Wt([e],t),"Run `tdk stacks` to see available stacks","Run `tdk stack` to assign resources to a stack"]),resourceNotFound:(e,t=[])=>new N(`Resource "${e}" not found`,[...Wt([e],t),"Run `tdk resources` to list all resources","Check the resource name spelling"]),unknownServices:(e,t)=>{let n=Wt(e,t,e.length>1);return new N(`Unknown service ${e.join(", ")}. Valid names: ${t.join(", ")}`,n,2)},directoryExists:e=>new N(`Directory already exists: ${e}`,["Use `--path` to specify a different location","Remove the existing directory if no longer needed"]),invalidPath:e=>new N(`Invalid path: ${e}`,["Path must be within the project directory",'Path cannot contain special characters like <>:"|?*']),notInProject:()=>new N("Could not find project root (no .tdk/project.json found)",["Run this from within a TDK project","Run `tdk project --yes` to initialize a new project"])};function Af(){let e=y();return e||Y.notInProject().exit()}function Va(e){e instanceof N&&e.exit(),console.error(m.red(`Error: ${os(e)}`)),process.exit(1)}async function Wa(e,t){try{return await e()}catch(n){return t?.verbose&&n instanceof Error&&n.stack&&console.error(m.gray(n.stack)),Va(n)}}async function Df(e,t){let n=[],r=ts();return r==="unresponsive"?n.push(Y.dockerNotResponding()):r==="missing"&&n.push(Y.dockerNotAvailable()),await rs()||n.push(Y.tiltNotInstalled()),n.length>0&&(console.error(m.red(`
17
17
  TDK can't start - missing prerequisites:
18
18
  `)),n.forEach((s,o)=>{o>0&&console.error(""),s.display()}),console.error(m.gray("\nRun `tdk doctor` for a full environment check.")),process.exit(1)),Wa(e,t)}function Of(e,t=1){console.error(m.red(`Error: ${e}`)),process.exit(t)}function Ja(e,t,n){if(console.error(m.red(`\u274C ${e}`)),t&&console.error(m.gray(` ${t}`)),n&&n.length>0){console.error(m.yellow(`
@@ -374,4 +374,4 @@ ${m.bold("\u{1F50D} TDK Doctor")}
374
374
  `));let o=y(),i;if(e.fix){try{let g;if(o)try{g=rt(o).project.name}catch{}let R=o?oo(o):[],ne=await to(g,void 0,{tiltPids:R,machine:e.scope==="machine"});i=await no(ne,{dryRun:e.dryRun,confirm:e.yes||!e.json&&process.stdin.isTTY?async ue=>e.yes||await zo({message:`Run \`${ue.command}\` (${ue.check}, ${ue.reason})?`}):void 0})}catch(g){i=[{id:"plan-fixes",check:"Docker networks",scope:"project",command:"docker network ls",status:"failed",reason:`could not list Docker networks: ${(g instanceof Error?g.message:String(g)).split(/\r?\n/)[0]}`}]}e.json||Md(i,e.dryRun)}let a,c;try{a=await kt(o??process.cwd())}catch(g){a=null,c=g instanceof Error?g.message:String(g)}let u=[()=>a?{name:"Host Ports",didPass:!0,message:`HTTP ${a.ingressHttp}, HTTPS ${a.ingressHttps}, Postgres ${a.postgres}`}:{name:"Host Ports",didPass:!1,message:c??"Could not select host ports",fix:"Set TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT to available host ports."},()=>a?yn(Zo,void 0,[a.ingressHttp,a.ingressHttps]):{name:"Ingress Ports",didPass:!1,message:"Ingress port plan is unavailable"},er,()=>zs(),hd,rr,tr,nr,gd,()=>cd(y()??process.cwd(),e.strict),()=>ld(process.env,e.strict),()=>Ho(),()=>so()],d=[()=>Sd(),yd,Ed,po,vd,()=>Ls(),_d,Ad,bd,()=>Do(),()=>ft(),()=>Hn(),()=>Kn(),()=>Gn(),()=>Vn(void 0,e.strict),()=>Oo(),()=>No(),()=>Io(),()=>jo(),()=>ht(),()=>Ro(),()=>Uo(),()=>Mo(),()=>Bo(),kd,async()=>{let g=y()??process.cwd(),R=gn(g)?await Ke(g):!0;return kn(Zo,g,void 0,R)},Cd,()=>{let g=y()??process.cwd(),R=[];try{R=vn(g)}catch{return{name:"Crash-looping containers",didPass:!0,isSkipped:!0,message:"Could not read .env - skipped crash-loop check"}}return Js(le(),R)},Tn];if(e.ping){let g=o?qo(o,a?.ingressHttp):a?.ingressHttp;d.push(()=>Id(t,g)),d.push(()=>Fd(t,g)),d.push(()=>Ld(t,g))}let l=!!o,{checks:p,errors:h}=await ao(u,l?d:[]);if(Ts(s.kind))for(let g of p)g.name===B&&!g.didPass&&(g.fix=Ss);let f=ee(ni(p),l,h,a,s);i&&(f.data.actions=i),i&&ro(i)&&(f.data.ready=!1);let w=co(f),$=f.data.ready;if(e.json){let g;try{g=vn(y()??process.cwd())}catch(R){if(!(R instanceof Le))throw R;console.log(JSON.stringify(ee([],l,[...h,{code:"ENV_UNREADABLE",message:R.message}]))),console.error(R.message),process.exit(1)}console.log(JSON.stringify(at(f,g)));for(let R of h)console.error(Me(R.message,g));w!==0&&process.exit(w);return}for(let g of h)console.error(`${m.red("\u2717")} Doctor check failed: ${g.message}`);for(let g of ni(p))!g.didPass&&!g.isSkipped&&!g.isWarning?(console.log(`${m.red("\u2717")} ${g.message}`),g.fix&&console.log(`${m.blue("\u2139")} Fix: ${g.fix}`)):g.isWarning?(console.log(`${m.yellow("\u26A0")} ${m.yellow(g.message)}`),g.fix&&console.log(`${m.blue("\u2139")} Fix: ${g.fix}`)):g.isSkipped?(console.log(`${m.gray("\u25CB")} ${m.gray(g.message)}`),g.fix&&console.log(`${m.blue("\u2139")} ${g.fix}`)):console.log(`${m.green("\u2713")} ${g.message}`);console.log(""),$&&!l&&(console.log(`${m.gray("\u25CB")} ${m.gray("Not in a TDK project, so project checks were skipped")}`),console.log(""));let U=dd(l,$);console.log(`${$?m.green(m.bold("\u2713")):m.red(m.bold("\u2717"))} ${U}`),w!==0&&process.exit(w)});function ii(e){return bt(e,ce)}var Tt=`${ce[0]}.${ce[1]}+`;function Kd(e,t){return{id:e,ok:t.didPass||!!t.isSkipped,message:t.message,fix:t.fix}}function ci(e){let[t=0,n=0]=String(e).split(".").map(r=>Number.parseInt(r,10));return!Number.isFinite(t)||!Number.isFinite(n)||t<22||t===22&&n<12}async function xt(e,t,n){try{return Kd(e,await t())}catch{return{id:e,ok:!1,message:n}}}function Gd(e){return e.isSkipped?{...e,didPass:!1,isSkipped:!1,message:`Could not verify Docker Engine/Compose minimum versions: ${e.message}`,fix:e.fix??"Start Docker and ensure Docker Engine 25+ and Compose 2.20.2+ are installed."}:e}function Vd(e){try{return x(e).some(t=>t.config?.featuresEnabled?.includes("prisma")??!1)}catch{return!1}}async function Wd(e={}){let t=e.cwd??process.cwd(),n=y(t),r=!!n,s=[],o=process.versions.node;s.push({id:"node",ok:!ci(o),message:o,fix:ci(o)?`Install Node.js 22.12+ (current: ${o}). https://nodejs.org`:void 0}),s.push(await xt("docker",er,"Docker daemon is not running")),s.push(await xt("compose",async()=>{let[f,b]=await Promise.all([tr(),nr()]);return f.didPass?Gd(b):f},"Docker Engine or Compose version check failed")),s.push(await xt("tilt",rr,"Tilt CLI not found"));let i=n?ot(x(n)):!0,a=!1,c=i?O("bun"):void 0;if(c)try{let f=ai(c,["--version"],{encoding:"utf-8",timeout:3e3}).trim();a=ii(f)}catch{}s.push(i?{id:"bun",ok:a,message:a?`Bun ${Tt} is available on PATH`:`Bun ${Tt} not found on PATH`,fix:a?void 0:"curl -fsSL https://bun.sh/install | bash"}:{id:"bun",ok:!0,message:"Bun not needed: no generated JS services"});try{let f=await kt(n??t);s.push({id:"ports",ok:!0,message:`HTTP ${f.ingressHttp}, HTTPS ${f.ingressHttps}, Postgres ${f.postgres}`})}catch(f){s.push({id:"ports",ok:!1,message:f instanceof Error?f.message:String(f),fix:"Set TDK_HTTP_PORT, TDK_HTTPS_PORT, or TDK_POSTGRES_PORT to available host ports."})}if(n){let f=await xt("nats",()=>ht(n),"NATS broker check failed");if(f.ok||s.push(f),Vd(n)){let b=!1;try{let w=JSON.parse(Bd(Hd(n,".tdk",_),"utf-8")),$=String(w?.project?.name??"").replace(/-/g,"_");$&&(b=ai(O("docker")??"docker",["ps","--filter",`label=com.docker.compose.project=${$}`,"--filter","label=com.docker.compose.service=postgres","--filter","health=healthy","--format","{{.Names}}"],{encoding:"utf-8",timeout:3e3}).trim().length>0)}catch{}b||s.push({id:"prisma",ok:!1,message:"A Prisma resource is configured, but the project Postgres service is not healthy",fix:"Run `tdk up` to start the bundled Postgres, then check `tdk doctor`."})}}let u=s.filter(f=>!f.ok).slice(0,8),d=u.some(f=>["node","docker","compose","tilt","bun","ports"].includes(f.id)),l=u.some(f=>f.id==="bun"),p=u.length?d?l?`Cold start blocked. Bun ${Tt} must be installed on PATH.
375
375
  Prisma/NATS configuration is project-specific and generated after \`tdk project\`. Fix the machine checks below.`:"Cold start blocked. Bun/Prisma/NATS are not the first failure.\nThey are generated after `tdk project`. Fix the machine checks below.":"Machine is ready. Project wiring is not.":"",h=u.length?"":r?"Environment ready for TDK":`This machine is ready for TDK.
376
376
  Next: tdk project --yes`;return{ok:u.length===0,inProject:r,items:u,header:p,footer:h}}function Jd(e,t={}){let n=[e.header];for(let r of e.items)n.push(`FAIL ${r.id.padEnd(7)} ${r.message}`),r.fix&&n.push(` ${r.fix}`);return e.footer&&(t.includeSuccessFooter??!0)&&n.push(e.footer),n.filter(Boolean).join(`
377
- `)}async function hy(){let e=await Wd();e.items.some(n=>["node","docker","compose","tilt","bun","ports"].includes(n.id))&&(console.error(Jd(e)),process.exit(1))}export{Pt as a,m as b,vr as c,qr as d,J as e,j as f,yr as g,C as h,Je as i,qe as j,_ as k,A as l,Dt as m,ip as n,P as o,Pe as p,ap as q,y as r,$r as s,De as t,rf as u,sf as v,O as w,Zr as x,Ka as y,rs as z,Ga as A,bf as B,Tf as C,os as D,Rf as E,N as F,Y as G,Af as H,Wa as I,Df as J,Of as K,Nf as L,T as M,If as N,Ff as O,Lf as P,Mf as Q,Uf as R,Bf as S,Hf as T,Kf as U,Gf as V,zt as W,Vf as X,Wf as Y,Jf as Z,qf as _,zf as $,Yf as aa,Xf as ba,Qf as ca,I as da,Ft as ea,ga as fa,Lt as ga,Mr as ha,qp as ia,Kr as ja,Re as ka,Qp as la,Gt as ma,as as na,Za as oa,ec as pa,nm as qa,rm as ra,sm as sa,om as ta,im as ua,Yt as va,nn as wa,ps as xa,ic as ya,x as za,Ne as Aa,ym as Ba,Sm as Ca,bm as Da,Tm as Ea,xm as Fa,wm as Ga,cc as Ha,Em as Ia,Yu as Ja,Xu as Ka,Xh as La,yp as Ma,ea as Na,Sp as Oa,bp as Pa,Ce as Qa,nt as Ra,an as Sa,rt as Ta,Um as Ua,$c as Va,_c as Wa,Bm as Xa,rk as Ya,zo as Za,sk as _a,ok as $a,ag as ab,kg as bb,To as cb,jn as db,Ro as eb,Il as fb,cn as gb,Ss as hb,bs as ib,Ts as jb,un as kb,it as lb,Ym as mb,Ps as nb,_s as ob,fn as pb,mn as qb,hn as rb,Vs as sb,hh as tb,gh as ub,Le as vb,vn as wb,Me as xb,at as yb,Dh as zb,Oh as Ab,Nh as Bb,An as Cb,Du as Db,Cu as Eb,Lh as Fb,ye as Gb,Vg as Hb,Jo as Ib,Qg as Jb,Zg as Kb,kt as Lb,mk as Mb,hk as Nb,Qo as Ob,Ud as Pb,ci as Qb,Gd as Rb,Wd as Sb,Jd as Tb,hy as Ub};
377
+ `)}async function hy(){let e=await Wd();e.items.some(n=>["node","docker","compose","tilt","bun","ports"].includes(n.id))&&(console.error(Jd(e)),process.exit(1))}export{Pt as a,m as b,vr as c,qr as d,J as e,j as f,yr as g,C as h,Je as i,qe as j,_ as k,A as l,Dt as m,ip as n,P as o,Pe as p,ap as q,y as r,$r as s,De as t,rf as u,sf as v,O as w,Zr as x,Ka as y,rs as z,Ga as A,bf as B,Tf as C,os as D,Rf as E,N as F,Y as G,Af as H,Wa as I,Df as J,Of as K,Nf as L,T as M,If as N,Ff as O,Lf as P,Mf as Q,Uf as R,Bf as S,Hf as T,Kf as U,Gf as V,zt as W,Vf as X,Wf as Y,Jf as Z,qf as _,zf as $,Yf as aa,Xf as ba,Qf as ca,I as da,Ft as ea,ga as fa,Lt as ga,_r as ha,Mr as ia,qp as ja,Kr as ka,Re as la,Qp as ma,Gt as na,as as oa,Za as pa,ec as qa,nm as ra,rm as sa,sm as ta,om as ua,im as va,Yt as wa,nn as xa,ps as ya,ic as za,x as Aa,Ne as Ba,ym as Ca,Sm as Da,bm as Ea,Tm as Fa,xm as Ga,wm as Ha,cc as Ia,Em as Ja,ye as Ka,Vg as La,Jo as Ma,Qg as Na,Zg as Oa,kt as Pa,Yu as Qa,Xu as Ra,Xh as Sa,yp as Ta,ea as Ua,Sp as Va,bp as Wa,Ce as Xa,nt as Ya,an as Za,rt as _a,Um as $a,$c as ab,_c as bb,Bm as cb,rk as db,zo as eb,sk as fb,ok as gb,ag as hb,kg as ib,To as jb,jn as kb,Ro as lb,Il as mb,cn as nb,Ss as ob,bs as pb,Ts as qb,un as rb,it as sb,Ym as tb,Ps as ub,_s as vb,fn as wb,mn as xb,hn as yb,Vs as zb,hh as Ab,gh as Bb,Le as Cb,vn as Db,Me as Eb,at as Fb,Dh as Gb,Oh as Hb,Nh as Ib,An as Jb,Du as Kb,Cu as Lb,Lh as Mb,mk as Nb,hk as Ob,Qo as Pb,Ud as Qb,ci as Rb,Gd as Sb,Wd as Tb,Jd as Ub,hy as Vb};