@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 +14 -0
- package/cli/dist/agent-runtimes/custom.js +188 -6
- package/cli/dist/agent-runtimes/registry.js +1 -3
- package/cli/dist/backend-frameworks/registry.js +1 -1
- package/cli/dist/cli.js +4 -0
- package/cli/dist/commands/agent.js +117 -0
- package/cli/dist/commands/completion.js +5 -1
- package/cli/dist/commands/help.js +6 -0
- package/cli/dist/commands/resource.js +16 -8
- package/cli/dist/commands/restart.js +79 -0
- package/cli/dist/mcp/tools.js +68 -13
- package/cli/dist/utils/doctor-report.js +2 -1
- package/cli/dist/utils/json-output.js +2 -1
- package/cli/dist/utils/machine-output.js +2 -1
- package/cli/dist/utils/project-templates.js +4 -0
- package/cli/dist-bundle/{chunk-KCHXMU3C.js → chunk-DOZ5U6CI.js} +69 -69
- package/cli/dist-bundle/cli.js +437 -254
- package/cli/dist-bundle/{cold-preflight-6C6O6NDI.js → cold-preflight-3GFZQO6I.js} +1 -1
- package/cli/package.json +1 -1
- package/engine/schemas/service-schema.json +3 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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,
|
|
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:
|
|
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
|
+
});
|