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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,6 +28,19 @@ No vendor lock-in. No tedious plumbing. Run a single command to natively orchest
28
28
  * **Databases & Tooling:** Prisma, Drizzle, Atlas, Dbmate
29
29
  * **Messaging & Gateway:** NATS, Traefik
30
30
 
31
+ ### 🤖 Run your AI agents next to your services
32
+
33
+ TDK is the local runtime for agent *systems*: the agent, its MCP tools, its database, its queue, behind one gateway, one command.
34
+
35
+ ```bash
36
+ # Scaffold an agent, an MCP tools server, and a database in one stack
37
+ tdk resource bot --type agent --stack shop --yes
38
+ tdk resource tools-mcp --type mcp --stack shop --yes
39
+ tdk up shop
40
+ ```
41
+
42
+ Agents get durable checkpoint volumes that survive `tdk down`, sticky session routing via `x-session-id`, graceful SIGTERM drain during hot reloads, a Docker health check on `/health`, and application-level readiness checks on `/ready`. See the [AI agents guide](docs/agents/ai-agents.md) and [honest comparison with agent frameworks](docs/compare-honest.md#ai-agent-frameworks-and-workflow-engines-libfx-langgraph-mastra-vercel-workflows-temporal).
43
+
31
44
 
32
45
  TDK CLI — start services on your machine. It is not a deploy and not a Compose file: define each service in `service.json`, then run `tdk up`. No Kubernetes is needed on the machine.
33
46
 
@@ -128,6 +141,7 @@ Set `TDK_DEBUG=1` before running `tdk up --verbose` to include Starlark debug lo
128
141
  - [Mega cheat sheet: every command and flag](docs/cheat-sheet.md)
129
142
  - [Documentation index](docs/README.md)
130
143
  - [Pilot TDK on a real repository](docs/adopt-tdk.md)
144
+ - [Running AI agents in TDK](docs/agents/ai-agents.md)
131
145
  - [FAQ for agencies and multi-client teams](docs/faq-teams.md)
132
146
  - [Working alongside Helm](docs/with-helm.md)
133
147
  - [Scope: local development, and what does not carry over to production](docs/scope.md)
@@ -3,12 +3,194 @@
3
3
  * It is the escape hatch the agent resource type is built on. Replace the `runTurn` body with your model call and tools;
4
4
  * the process contract around it (health, readiness, session checkpoints, SIGTERM drain) is what TDK relies on.
5
5
  */
6
- export function getCustomAgentIndexTemplate(name) {
6
+ export function getCustomAgentIndexTemplate(name, durability = "files") {
7
+ if (durability === "postgres") {
8
+ return `import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
9
+ import { Client } from 'pg';
10
+
11
+ // Contract with TDK (see docs/agents/ai-agents.md):
12
+ // AGENT_DURABILITY postgres
13
+ // DATABASE_URL Postgres connection string injected by TDK
14
+ // PORT port to listen on
15
+ // /health: the process is alive. /ready: the checkpoint store is reachable. Idle is healthy, so /health never checks the store.
16
+
17
+ const DURABILITY = 'postgres';
18
+ const PORT = Number(process.env.PORT || 3000);
19
+ const DRAIN_TIMEOUT_MS = 10_000;
20
+ const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/postgres';
21
+
22
+ interface Checkpoint {
23
+ sessionId: string;
24
+ messages: Array<{ role: 'user' | 'agent'; content: string; at: string }>;
25
+ }
26
+
27
+ let client: Client | null = null;
28
+ let initialized = false;
29
+
30
+ async function getClient(): Promise<Client> {
31
+ if (client) return client;
32
+ const c = new Client({ connectionString: DATABASE_URL });
33
+ await c.connect();
34
+ if (!initialized) {
35
+ await c.query(\`
36
+ CREATE TABLE IF NOT EXISTS tdk_agent_sessions (
37
+ session_id VARCHAR(128) PRIMARY KEY,
38
+ messages JSONB NOT NULL DEFAULT '[]'::jsonb,
39
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
40
+ );
41
+ \`);
42
+ initialized = true;
43
+ }
44
+ client = c;
45
+ return client;
46
+ }
47
+
48
+ const store = {
49
+ async ready(): Promise<boolean> {
50
+ try {
51
+ const c = await getClient();
52
+ await c.query('SELECT 1');
53
+ return true;
54
+ } catch {
55
+ client = null;
56
+ return false;
57
+ }
58
+ },
59
+ async load(sessionId: string): Promise<Checkpoint> {
60
+ try {
61
+ const c = await getClient();
62
+ const res = await c.query('SELECT messages FROM tdk_agent_sessions WHERE session_id = $1', [sessionId]);
63
+ if (res.rows.length > 0 && res.rows[0].messages) {
64
+ return { sessionId, messages: res.rows[0].messages };
65
+ }
66
+ return { sessionId, messages: [] };
67
+ } catch (err) {
68
+ console.error('[agent] load checkpoint failed:', err);
69
+ return { sessionId, messages: [] };
70
+ }
71
+ },
72
+ async save(checkpoint: Checkpoint): Promise<void> {
73
+ const c = await getClient();
74
+ await c.query(
75
+ \`INSERT INTO tdk_agent_sessions (session_id, messages, updated_at)
76
+ VALUES ($1, $2, NOW())
77
+ ON CONFLICT (session_id) DO UPDATE
78
+ SET messages = EXCLUDED.messages, updated_at = NOW()\`,
79
+ [checkpoint.sessionId, JSON.stringify(checkpoint.messages)],
80
+ );
81
+ },
82
+ async close(): Promise<void> {
83
+ if (client) {
84
+ try {
85
+ await client.end();
86
+ } catch {}
87
+ client = null;
88
+ }
89
+ },
90
+ };
91
+
92
+ // Replace this with the agent's work: call your model, run tools, return the reply. Keep it a pure function of the
93
+ // checkpoint so a resumed session continues from what was saved.
94
+ async function runTurn(checkpoint: Checkpoint, input: string): Promise<string> {
95
+ // The caller has already appended this turn's user message, so the turn number is half the message count, rounded up.
96
+ return 'received ' + input + ' (turn ' + Math.ceil(checkpoint.messages.length / 2) + ')';
97
+ }
98
+
99
+ let draining = false;
100
+ let inFlight = 0;
101
+
102
+ function sendJson(res: ServerResponse, status: number, body: unknown) {
103
+ res.statusCode = status;
104
+ res.setHeader('content-type', 'application/json');
105
+ res.end(JSON.stringify(body));
106
+ }
107
+
108
+ async function readJson(req: IncomingMessage): Promise<any> {
109
+ const chunks: Buffer[] = [];
110
+ for await (const chunk of req) chunks.push(chunk as Buffer);
111
+ const raw = Buffer.concat(chunks).toString('utf8');
112
+ return raw ? JSON.parse(raw) : {};
113
+ }
114
+
115
+ const server = createServer(async (req, res) => {
116
+ inFlight++;
117
+ res.on('close', () => inFlight--);
118
+ const url = new URL(req.url ?? '/', 'http://localhost');
119
+
120
+ try {
121
+ if (url.pathname === '/health') {
122
+ return sendJson(res, 200, { status: 'ok', service: '${name}', draining });
123
+ }
124
+
125
+ if (url.pathname === '/ready') {
126
+ const ok = !draining && (await store.ready());
127
+ return sendJson(res, ok ? 200 : 503, { ready: ok, durability: DURABILITY });
128
+ }
129
+
130
+ // Session routing: the session id comes from the x-session-id header, so several instances of one agent can
131
+ // share a Traefik route and each session still reaches its own checkpoint.
132
+ if (url.pathname === '/turn' && req.method === 'POST') {
133
+ if (draining) return sendJson(res, 503, { error: 'draining' });
134
+ const sessionId = String(req.headers['x-session-id'] ?? '');
135
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(sessionId)) {
136
+ return sendJson(res, 400, { error: 'x-session-id header is required (1-128 letters, digits, - or _)' });
137
+ }
138
+ const body = await readJson(req);
139
+ const input = String(body.input ?? '');
140
+ const checkpoint = await store.load(sessionId);
141
+ checkpoint.messages.push({ role: 'user', content: input, at: new Date().toISOString() });
142
+ const reply = await runTurn(checkpoint, input);
143
+ checkpoint.messages.push({ role: 'agent', content: reply, at: new Date().toISOString() });
144
+ await store.save(checkpoint);
145
+ return sendJson(res, 200, { sessionId, reply, turns: checkpoint.messages.length / 2 });
146
+ }
147
+
148
+ if (url.pathname === '/session' && req.method === 'GET') {
149
+ const sessionId = String(req.headers['x-session-id'] ?? '');
150
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(sessionId)) return sendJson(res, 400, { error: 'x-session-id header is required' });
151
+ const checkpoint = await store.load(sessionId);
152
+ return sendJson(res, 200, { sessionId, turns: checkpoint.messages.length / 2, messages: checkpoint.messages });
153
+ }
154
+
155
+ return sendJson(res, 404, { error: 'not_found' });
156
+ } catch (error) {
157
+ console.error('[agent] request failed:', error);
158
+ return sendJson(res, 500, { error: 'internal_error' });
159
+ }
160
+ });
161
+
162
+ // Bind beyond loopback: Traefik reaches the container over the Docker network.
163
+ server.listen(PORT, '0.0.0.0', () => {
164
+ console.log('\\n🤖 ${name} agent on http://localhost:' + PORT + ' (durability: ' + DURABILITY + ')');
165
+ });
166
+
167
+ // Drain on SIGTERM (tdk down, hot reload, docker stop): stop taking turns, let in-flight turns finish, then exit 0.
168
+ // Checkpoints are written before a turn is answered, so a drained exit loses no acknowledged turn.
169
+ function shutdown(signal: string) {
170
+ if (draining) return;
171
+ draining = true;
172
+ console.log('[agent] ' + signal + ' received, draining ' + inFlight + ' in-flight request(s)');
173
+ server.close(async () => {
174
+ await store.close();
175
+ });
176
+ const deadline = Date.now() + DRAIN_TIMEOUT_MS;
177
+ const timer = setInterval(async () => {
178
+ if (inFlight === 0 || Date.now() > deadline) {
179
+ clearInterval(timer);
180
+ await store.close();
181
+ process.exit(0);
182
+ }
183
+ }, 100);
184
+ }
185
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
186
+ process.on('SIGINT', () => shutdown('SIGINT'));
187
+ `;
188
+ }
7
189
  return `import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
8
190
  import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from 'node:fs';
9
191
  import { join } from 'node:path';
10
192
 
11
- // Contract with TDK (see docs/ai-agents.md):
193
+ // Contract with TDK (see docs/agents/ai-agents.md):
12
194
  // AGENT_DURABILITY files (set by tdk from service.json agent.durability)
13
195
  // AGENT_STATE_DIR where files-durability checkpoints live (a named volume that survives tdk down)
14
196
  // PORT port to listen on
@@ -82,7 +264,7 @@ const server = createServer(async (req, res) => {
82
264
  }
83
265
 
84
266
  if (url.pathname === '/ready') {
85
- const ok = !draining && store.ready();
267
+ const ok = !draining && (await store.ready());
86
268
  return sendJson(res, ok ? 200 : 503, { ready: ok, durability: DURABILITY });
87
269
  }
88
270
 
@@ -96,18 +278,18 @@ const server = createServer(async (req, res) => {
96
278
  }
97
279
  const body = await readJson(req);
98
280
  const input = String(body.input ?? '');
99
- const checkpoint = store.load(sessionId);
281
+ const checkpoint = await store.load(sessionId);
100
282
  checkpoint.messages.push({ role: 'user', content: input, at: new Date().toISOString() });
101
283
  const reply = await runTurn(checkpoint, input);
102
284
  checkpoint.messages.push({ role: 'agent', content: reply, at: new Date().toISOString() });
103
- store.save(checkpoint);
285
+ await store.save(checkpoint);
104
286
  return sendJson(res, 200, { sessionId, reply, turns: checkpoint.messages.length / 2 });
105
287
  }
106
288
 
107
289
  if (url.pathname === '/session' && req.method === 'GET') {
108
290
  const sessionId = String(req.headers['x-session-id'] ?? '');
109
291
  if (!/^[A-Za-z0-9_-]{1,128}$/.test(sessionId)) return sendJson(res, 400, { error: 'x-session-id header is required' });
110
- const checkpoint = store.load(sessionId);
292
+ const checkpoint = await store.load(sessionId);
111
293
  return sendJson(res, 200, { sessionId, turns: checkpoint.messages.length / 2, messages: checkpoint.messages });
112
294
  }
113
295
 
@@ -10,9 +10,7 @@ export const AGENT_RUNTIMES = {
10
10
  custom: customAgentRuntime,
11
11
  };
12
12
  export const DEFAULT_AGENT_RUNTIME = "custom";
13
- // `postgres` is deliberately absent: the scaffold has no Postgres session store yet (#1256), so offering it would produce an
14
- // agent that answers every turn with a 500. Add it here together with the store.
15
- export const AGENT_DURABILITY_OPTIONS = ["files"];
13
+ export const AGENT_DURABILITY_OPTIONS = ["files", "postgres"];
16
14
  export function resolveAgentRuntime(runtimeId) {
17
15
  const id = runtimeId ?? DEFAULT_AGENT_RUNTIME;
18
16
  const provider = AGENT_RUNTIMES[id];
@@ -60,7 +60,7 @@ export function resolveBackendFramework(resourceType, frameworkId, language) {
60
60
  // `agent` is scaffolded by an agent runtime (src/agent-runtimes/), not a framework. Only the default runtime is reachable here:
61
61
  // `tdk resource` validates --runtime before it scaffolds, so the package.json and tsconfig always match the chosen runtime.
62
62
  if (resourceType === "agent") {
63
- if (frameworkId !== undefined) {
63
+ if (frameworkId !== undefined && frameworkId !== "custom") {
64
64
  throw new TdkError("--framework cannot be combined with --type agent.", [
65
65
  "An agent resource is scaffolded by an agent runtime; use --runtime <id> instead",
66
66
  ]);
package/cli/dist/cli.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import chalk from "chalk";
5
5
  import { Command } from "commander";
6
6
  import pkg from "../package.json" with { type: "json" };
7
+ import { agentCommand } from "./commands/agent.js";
7
8
  import { completionCommand } from "./commands/completion.js";
8
9
  import { configCommand } from "./commands/config.js";
9
10
  import { doctorCommand } from "./commands/doctor.js";
@@ -19,6 +20,7 @@ import { projectCommand } from "./commands/project.js";
19
20
  import { projectsCommand } from "./commands/projects.js";
20
21
  import { resourceCommand } from "./commands/resource.js";
21
22
  import { resourcesCommand } from "./commands/resources.js";
23
+ import { restartCommand } from "./commands/restart.js";
22
24
  import { runtimeCommand } from "./commands/runtime.js";
23
25
  import { stackCommand } from "./commands/stack.js";
24
26
  import { stacksCommand } from "./commands/stacks.js";
@@ -67,6 +69,8 @@ program.addCommand(doctorCommand);
67
69
  program.addCommand(completionCommand);
68
70
  program.addCommand(upgradeCommand);
69
71
  program.addCommand(networksCommand);
72
+ program.addCommand(agentCommand);
73
+ program.addCommand(restartCommand);
70
74
  // Default: show help if no command provided
71
75
  if (process.argv.length === 2) {
72
76
  const result = await runColdPreflight();
@@ -0,0 +1,117 @@
1
+ // Copyright (c) 2026 TDK Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import chalk from "chalk";
4
+ import { Command } from "commander";
5
+ import { createDiscoveryContext } from "../utils/discovery-context.js";
6
+ import { errorFactories, runCommand, TdkError } from "../utils/errors.js";
7
+ import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
8
+ import { findProjectRoot } from "../utils/paths.js";
9
+ const SESSION_REQUEST_TIMEOUT_MS = 10_000;
10
+ export const agentCommand = new Command("agent").description("Inspect and operate AI agent resources");
11
+ agentCommand
12
+ .command("sessions <resource>")
13
+ .description("Inspect session checkpoints from a running AI agent resource")
14
+ .option("--session-id <id>", "Specific session ID to fetch")
15
+ .option("--json", "Output one JSON object and exit", false)
16
+ .action(async (resourceName, options) => {
17
+ const action = async () => {
18
+ const root = findProjectRoot();
19
+ if (!root) {
20
+ const error = errorFactories.notInProject();
21
+ if (options.json)
22
+ writeMachineError(error);
23
+ error.exit();
24
+ }
25
+ const discovery = createDiscoveryContext();
26
+ const resource = discovery.resources.find((r) => r.name === resourceName);
27
+ if (!resource) {
28
+ const msg = `Resource "${resourceName}" not found in project.`;
29
+ if (options.json) {
30
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "UNKNOWN_RESOURCE", message: msg }])));
31
+ process.exit(1);
32
+ }
33
+ throw new TdkError(msg);
34
+ }
35
+ const appType = resource.config?.appType ?? resource.type;
36
+ if (appType !== "agent") {
37
+ const msg = `Resource "${resourceName}" is of type "${appType}", not "agent".`;
38
+ if (options.json) {
39
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "NOT_AN_AGENT", message: msg }])));
40
+ process.exit(1);
41
+ }
42
+ throw new TdkError(msg);
43
+ }
44
+ const port = resource.port;
45
+ if (!port) {
46
+ const msg = `Agent "${resourceName}" has no assigned port.`;
47
+ if (options.json) {
48
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "NO_PORT", message: msg }])));
49
+ process.exit(1);
50
+ }
51
+ throw new TdkError(msg);
52
+ }
53
+ const headers = {};
54
+ if (options.sessionId) {
55
+ headers["x-session-id"] = String(options.sessionId);
56
+ }
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();
67
+ }
68
+ catch {
69
+ const msg = `Agent "${resourceName}" is not responding on port ${port}. Is the stack running?`;
70
+ if (options.json) {
71
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "AGENT_NOT_RUNNING", message: msg }])));
72
+ process.exit(1);
73
+ }
74
+ throw new TdkError(msg, ["Start the stack with: tdk up"]);
75
+ }
76
+ let body = {};
77
+ try {
78
+ body = bodyText ? JSON.parse(bodyText) : {};
79
+ }
80
+ catch {
81
+ body = { raw: bodyText };
82
+ }
83
+ if (!res.ok) {
84
+ const detail = body?.error ?? bodyText;
85
+ if (options.json) {
86
+ console.log(JSON.stringify(createMachineEnvelope(null, [
87
+ {
88
+ code: "AGENT_REQUEST_FAILED",
89
+ message: `Agent returned ${res.status}: ${detail}`,
90
+ },
91
+ ])));
92
+ process.exit(1);
93
+ }
94
+ throw new TdkError(`Agent error (${res.status}): ${detail}`);
95
+ }
96
+ if (options.json) {
97
+ console.log(JSON.stringify(createMachineEnvelope({
98
+ agent: resourceName,
99
+ port,
100
+ session: body,
101
+ })));
102
+ return;
103
+ }
104
+ console.log(chalk.bold(`Sessions for ${resourceName}:`));
105
+ console.log(JSON.stringify(body, null, 2));
106
+ };
107
+ if (options.json) {
108
+ try {
109
+ await action();
110
+ }
111
+ catch (err) {
112
+ writeMachineError(err);
113
+ }
114
+ return;
115
+ }
116
+ await runCommand(action);
117
+ });
@@ -13,7 +13,7 @@ _tdk_completions() {
13
13
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
14
14
 
15
15
  # Main commands
16
- local commands="project projects stack stacks resource resources up down status ui doctor runtime version completion help upgrade mcp logs eject import config maintainers networks traefik"
16
+ local commands="project projects stack stacks resource resources agent restart up down status ui doctor runtime version completion help upgrade mcp logs eject import config maintainers networks traefik"
17
17
 
18
18
  # Options for specific commands
19
19
  case "\${prev}" in
@@ -136,6 +136,8 @@ _tdk_commands() {
136
136
  'stacks:List all stacks'
137
137
  'resource:Create new resource'
138
138
  'resources:List all resources'
139
+ 'agent:Inspect and operate AI agent resources'
140
+ 'restart:Restart a running resource in the stack'
139
141
  'up:Start services'
140
142
  'down:Stop all services'
141
143
  'status:Show resource status'
@@ -189,6 +191,8 @@ complete -c tdk -n '__fish_use_subcommand' -a 'stack' -d 'Organize resources int
189
191
  complete -c tdk -n '__fish_use_subcommand' -a 'stacks' -d 'List all stacks'
190
192
  complete -c tdk -n '__fish_use_subcommand' -a 'resource' -d 'Create new resource'
191
193
  complete -c tdk -n '__fish_use_subcommand' -a 'resources' -d 'List all resources'
194
+ complete -c tdk -n '__fish_use_subcommand' -a 'agent' -d 'Inspect and operate AI agent resources'
195
+ complete -c tdk -n '__fish_use_subcommand' -a 'restart' -d 'Restart a running resource in the stack'
192
196
  complete -c tdk -n '__fish_use_subcommand' -a 'up' -d 'Start services'
193
197
  complete -c tdk -n '__fish_use_subcommand' -a 'down' -d 'Stop all services'
194
198
  complete -c tdk -n '__fish_use_subcommand' -a 'status' -d 'Show resource status'
@@ -42,6 +42,11 @@ const COMMAND_GROUPS = [
42
42
  commands: [
43
43
  { name: "resource", desc: "Create new resource with templates", alias: "" },
44
44
  { name: "resources", desc: "List all resources with filtering", alias: "--stack, --ports" },
45
+ {
46
+ name: "agent",
47
+ desc: "Inspect and operate AI agent resources",
48
+ alias: "sessions <resource>",
49
+ },
45
50
  ],
46
51
  },
47
52
  {
@@ -50,6 +55,7 @@ const COMMAND_GROUPS = [
50
55
  commands: [
51
56
  { name: "up, deploy", desc: "Start services (optionally by stack)", alias: "[stack-name]" },
52
57
  { name: "down", desc: "Stop all tilt resources", alias: "" },
58
+ { name: "restart", desc: "Restart a running resource in the stack", alias: "<resource>" },
53
59
  { name: "status", desc: "Show status of services and stacks", alias: "" },
54
60
  { name: "logs", desc: "Print recent logs from the running stack", alias: "--json" },
55
61
  ],
@@ -162,10 +162,11 @@ function requireGeneratedViteConfig(configPath) {
162
162
  "Run tdk up <stack> once, or point this script at your own Vite config to build on the host.";
163
163
  return `bun -e 'if(!require("fs").existsSync(${JSON.stringify(configPath)})){console.error(${JSON.stringify(message)});process.exit(1)}'`;
164
164
  }
165
- export function createPackageJson(name, type, frameworkId, prismaEnabled = false) {
165
+ export function createPackageJson(name, type, frameworkId, prismaEnabled = false, agentConfig) {
166
166
  const isFrontend = type === "frontend";
167
167
  const framework = resolveFrontendFramework(type, frameworkId);
168
168
  const backendFramework = resolveBackendFramework(type, frameworkId);
169
+ const isPostgresAgent = type === "agent" && agentConfig?.durability === "postgres";
169
170
  return {
170
171
  name: `@project/${name}`,
171
172
  version: "0.0.1",
@@ -192,11 +193,15 @@ export function createPackageJson(name, type, frameworkId, prismaEnabled = false
192
193
  dependencies: {
193
194
  ...(isFrontend ? {} : (backendFramework?.dependencies ?? { hono: "^4.0.0" })),
194
195
  ...(framework?.dependencies ?? {}),
195
- ...(prismaEnabled
196
+ ...(prismaEnabled || isPostgresAgent
196
197
  ? {
197
- prisma: "^7.5.0",
198
- "@prisma/client": "^7.5.0",
199
- "@prisma/adapter-pg": "^7.5.0",
198
+ ...(prismaEnabled
199
+ ? {
200
+ prisma: "^7.5.0",
201
+ "@prisma/client": "^7.5.0",
202
+ "@prisma/adapter-pg": "^7.5.0",
203
+ }
204
+ : {}),
200
205
  pg: "^8.13.0",
201
206
  }
202
207
  : {}),
@@ -298,7 +303,7 @@ export const resourceCommand = new Command("resource")
298
303
  .option("--port <port>", "Port for the resource, within its type's range (default: next free port in that range)")
299
304
  .option("--feature <feature...>", "Enable a resource feature (for example: prisma)")
300
305
  .option("--runtime <id>", "Agent runtime (for agent only): custom (default)")
301
- .option("--durability <kind>", "Where agent sessions are checkpointed (for agent only): files (default, a named volume that survives tdk down)")
306
+ .option("--durability <kind>", "Where agent sessions are checkpointed (for agent only): files (default, named volume) or postgres")
302
307
  .action(async (name, options) => {
303
308
  if (options.frameworks) {
304
309
  for (const f of listFrontendFrameworks()) {
@@ -636,7 +641,7 @@ This file contains the resource configuration for TDK.
636
641
  const requestedFeatures = Array.isArray(options.feature) ? options.feature : [];
637
642
  const prismaEnabled = requestedFeatures.includes("prisma");
638
643
  const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, [...(dddEnabled ? ["ddd"] : []), ...requestedFeatures], scaffoldFrameworkId, options.language === undefined ? undefined : backendLanguage?.id, agentConfig);
639
- const packageJson = createPackageJson(resourceName, resourceType, scaffoldFrameworkId, prismaEnabled);
644
+ const packageJson = createPackageJson(resourceName, resourceType, scaffoldFrameworkId, prismaEnabled, agentConfig);
640
645
  if (prismaEnabled && resourceType === "backend") {
641
646
  serviceJson.dependsOn = [`${resourceName}-migrator`];
642
647
  }
@@ -674,10 +679,13 @@ This file contains the resource configuration for TDK.
674
679
  ];
675
680
  // Add source files based on resource type
676
681
  if (resourceType === "backend" || resourceType === "mcp" || resourceType === "agent") {
682
+ const indexContent = resourceType === "agent"
683
+ ? (backendFramework?.createIndex ?? getBackendIndexTemplate)(resourceName, agentConfig?.durability)
684
+ : (backendFramework?.createIndex ?? getBackendIndexTemplate)(resourceName);
677
685
  tasks.push({
678
686
  type: "text",
679
687
  filename: "src/index.ts",
680
- content: (backendFramework?.createIndex ?? getBackendIndexTemplate)(resourceName),
688
+ content: indexContent,
681
689
  description: resourceType === "mcp"
682
690
  ? "Generating MCP server source"
683
691
  : resourceType === "agent"
@@ -0,0 +1,79 @@
1
+ // Copyright (c) 2026 TDK Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import chalk from "chalk";
4
+ import { Command } from "commander";
5
+ import { STANDARD_PORTS } from "../utils/constants.js";
6
+ import { errorFactories, runCommand, TdkError } from "../utils/errors.js";
7
+ import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
8
+ import { findProjectRoot } from "../utils/paths.js";
9
+ import { isTiltAvailable, runTilt } from "../utils/tilt.js";
10
+ import { isTiltConnectionFailure } from "../utils/tilt-logs.js";
11
+ import { tiltUnreachableMessage } from "../utils/tilt-unreachable-message.js";
12
+ export const restartCommand = new Command("restart")
13
+ .description("Restart a running resource in the stack")
14
+ .argument("<resource>", "Name of the resource (service) to restart")
15
+ .option("--port <n>", "Tilt UI port (default: TILT_PORT or 10350)")
16
+ .option("--json", "Output one JSON object and exit", false)
17
+ .action(async (resourceName, options) => {
18
+ const action = async () => {
19
+ const root = findProjectRoot();
20
+ if (!root) {
21
+ const error = errorFactories.notInProject();
22
+ if (options.json)
23
+ writeMachineError(error);
24
+ error.exit();
25
+ }
26
+ const portText = options.port ?? process.env.TILT_PORT ?? String(STANDARD_PORTS.tiltUi);
27
+ let tiltAvailable = false;
28
+ try {
29
+ tiltAvailable = await isTiltAvailable();
30
+ }
31
+ catch {
32
+ tiltAvailable = false;
33
+ }
34
+ if (!tiltAvailable) {
35
+ // isTiltAvailable collapses missing binary and nonzero version into false.
36
+ // Report TILT_NOT_RUNNING for both; a missing binary is rare and still surfaces via doctor.
37
+ const msg = tiltUnreachableMessage(portText);
38
+ if (options.json) {
39
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "TILT_NOT_RUNNING", message: msg }])));
40
+ process.exit(1);
41
+ }
42
+ throw new TdkError(msg, ["Start the stack first with: tdk up"]);
43
+ }
44
+ const result = await runTilt("trigger", [resourceName, "--port", portText], {
45
+ inheritStdio: false,
46
+ });
47
+ if (result.exitCode !== 0) {
48
+ const detail = result.stderr.trim();
49
+ if (!detail || isTiltConnectionFailure(detail)) {
50
+ const msg = tiltUnreachableMessage(portText);
51
+ if (options.json) {
52
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "TILT_NOT_RUNNING", message: msg }])));
53
+ process.exit(1);
54
+ }
55
+ throw new TdkError(msg, ["Start the stack first with: tdk up"]);
56
+ }
57
+ if (options.json) {
58
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "TILT_TRIGGER_FAILED", message: detail }])));
59
+ process.exit(1);
60
+ }
61
+ throw new TdkError(`Failed to restart ${resourceName}: ${detail}`);
62
+ }
63
+ if (options.json) {
64
+ console.log(JSON.stringify(createMachineEnvelope({ resource: resourceName, restarted: true })));
65
+ return;
66
+ }
67
+ console.log(chalk.green(`✓ Triggered restart for ${resourceName}`));
68
+ };
69
+ if (options.json) {
70
+ try {
71
+ await action();
72
+ }
73
+ catch (err) {
74
+ writeMachineError(err);
75
+ }
76
+ return;
77
+ }
78
+ await runCommand(action);
79
+ });