@tdk-landscape/tdk-cli-core 1.3.86 → 1.3.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ - Add an opt-in Python backend: `tdk resource <name> --type backend --language python` scaffolds a FastAPI service (`pyproject.toml`, `src/main.py`, a pytest smoke test) and TDK generates its Docker image (`python:3.12-slim`, with `test` and `production` stages). The choice is saved as `language` in `service.json` only when you pass the flag; omitting it keeps the existing Bun + Hono output byte for byte. Unknown ids and `--language` on non-backend types fail before any file is written. A runnable example is in `examples/one-backend-python`, and the provider contract is in `docs/backend-language-providers.md`. Node.js is not included.
8
+ - `tdk doctor` no longer reports "resource without a package.json" for a Python backend, and a Python service missing `pyproject.toml` or `src/main.py` now fails early with the missing file named.
9
+ - Live reload now works: Tilt `live_update` sync paths were relative to the project root while Tilt resolves them against `.tdk/.tdk-out`, so they never matched and every source edit rebuilt the image ("Found file(s) not matching any sync"). An edit under `src/` is now copied into the running container without a rebuild, for Bun and Python services. Dependency file changes (`package.json`, `pyproject.toml`) still rebuild the image.
10
+ - The service schema (`https://tdk-landscape.github.io/schema.service.json`) gains the `language` field (`bun`, `python`).
11
+ - Fix the routed health URL in the `one-backend` example README.
12
+
7
13
  ## 1.3.80 (2026-09-30)
8
14
 
9
15
  - Add `tdk doctor --json` with schema-versioned readiness, checks, and structured errors; diagnostics remain on stderr.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  TDK CLI starts your services on your laptop. 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.
4
4
 
5
- Docker runs the containers. Tilt runs the development loop. TDK CLI writes that configuration. Production deployment stays with Helm, Argo CD, or Kustomize.
5
+ Stability: 1.x local dev. Generated files are a contract; verify with `tdk config verify`. Core CLI is MIT and needs no key. Premium is optional.
6
+
7
+ Docker runs the containers. Tilt watches services and live-updates containers while you code. TDK CLI writes the configuration Tilt uses. Production deployment stays with Helm, Argo CD, or Kustomize.
6
8
 
7
9
  [Website](https://tdk-landscape.github.io/tdk-website/) · [Quickstart](https://tdk-landscape.github.io/tdk-website/docs/quickstart/) · [Examples](https://tdk-landscape.github.io/tdk-website/docs/examples/) · [Awesome TDK](https://github.com/tdk-landscape/awesome-tdk-framework) · [Demo](https://tdk-landscape.github.io/tdk-demo-animation/) · [Report a bug](https://github.com/tdk-landscape/tdk-cli-core/issues)
8
10
 
@@ -20,6 +22,8 @@ curl http://api.shop.localhost/api/orders-api/health
20
22
 
21
23
  *Scaffolding a backend and frontend, then listing the stack.*
22
24
 
25
+ ![Top 9 places to use TDK CLI: scaffold a service, start a stack, hot reload, port management, auto-discovery, boot order, proxy routing, include infra, and config verify](docs/tdk-cli-top-9-uses.jpg)
26
+
23
27
  If Helm, Compose, or your existing Tilt setup already gives you a working local environment, keep using it. TDK CLI is for engineers managing several services who want a clear local service contract and one command to start the stack.
24
28
 
25
29
  See [how TDK CLI works alongside Helm](https://tdk-landscape.github.io/tdk-website/docs/with-helm/), the [service schema](engine/schemas/service-schema.json), and the [project configuration schema](engine/schemas/project-schema.json).
@@ -57,9 +61,12 @@ curl -fsSL https://tdk-landscape.github.io/install.sh | sh
57
61
  - [Working alongside Helm](docs/with-helm.md)
58
62
  - [Configuration and editor schemas](docs/configuration.md)
59
63
  - [Runnable one-backend example](examples/one-backend/README.md)
64
+ - [Runnable Python backend example](examples/one-backend-python/README.md)
60
65
  - [Full multi-service example](examples/tdk-example/README.md)
61
66
  - [Features and license limits](docs/FEATURES.md)
62
67
  - [Honest comparison and known limits](docs/compare-honest.md)
68
+ - [Public claims registry](docs/claims.md)
69
+ - [Show HN draft](docs/show-hn.md)
63
70
  - [Architecture and repository map](docs/project-overview.md)
64
71
  - [Scale fixture measurements and caveats](docs/scale-bench.md)
65
72
 
@@ -67,6 +74,18 @@ curl -fsSL https://tdk-landscape.github.io/install.sh | sh
67
74
 
68
75
  For the local runtime, install Docker (Desktop, OrbStack, or Colima; Engine 25+, Compose 2.20+) and [Tilt](https://docs.tilt.dev/install.html). Bun 1.2+ is used by the default generated services. TDK selects host ports from bounded fallback ranges for HTTP, HTTPS, and Postgres; set `TDK_HTTP_PORT`, `TDK_HTTPS_PORT`, or `TDK_POSTGRES_PORT` to override them. TDK supports macOS, Linux, and Windows through WSL2 Ubuntu; native Windows supports CLI inspection only. Run `tdk doctor` to check local readiness. See [WSL2 setup](docs/wsl2.md).
69
76
 
77
+ On native Windows, `tdk --version`, `tdk doctor`, and `tdk up --dry-run` are inspect-only commands. `tdk up` exits 2 with “Landscape startup needs Ubuntu on WSL2. Native Windows is inspect-only.”
78
+
79
+ ## License matrix
80
+
81
+ | Capability | Free | Premium |
82
+ | --- | --- | --- |
83
+ | `tdk up`, scaffold, Traefik, Postgres, Tilt live update, golden layers | yes | yes |
84
+ | Verdaccio, DDD scaffold, Sablier idle stop | no | key |
85
+ | Playwright, C4, AGENTS.md | free if generated in-repo | only if the implementation is downloaded with a key |
86
+
87
+ Core stays free. Premium is a separate key for the extras above; no key required to run `tdk up`.
88
+
70
89
  **Ecosystem map:** examples, articles, related tools, and notes live in [awesome-tdk-framework](https://github.com/tdk-landscape/awesome-tdk-framework). Star this repo (`tdk-cli-core`) if the CLI is what you run; use the awesome list to browse the rest.
71
90
 
72
91
  ## Contributing and license
@@ -0,0 +1,53 @@
1
+ export function getBackendIndexTemplate(name) {
2
+ return `import { Hono } from 'hono';
3
+
4
+ const app = new Hono();
5
+
6
+ // Health check endpoint (required by TILT_RESOURCE_DEFAULTS.star)
7
+ app.get('/health', (c) => {
8
+ return c.json({ status: 'ok', service: '${name}' });
9
+ });
10
+
11
+ app.get('/health/live', (c) => {
12
+ return c.json({ status: 'alive', timestamp: Date.now() });
13
+ });
14
+
15
+ app.get('/health/ready', async (c) => {
16
+ const dependencies: Record<string, string> = {};
17
+ let allReady = true;
18
+
19
+ // Add dependency checks here (database, cache, etc.)
20
+ // Mark allReady = false if any dependency is unhealthy
21
+
22
+ const status = allReady ? 'ready' : 'not_ready';
23
+ return c.json({ status, dependencies }, allReady ? 200 : 503);
24
+ });
25
+
26
+ app.get('/', (c) => {
27
+ return c.json({
28
+ service: '${name}',
29
+ version: '1.0.0',
30
+ endpoints: ['/health', '/health/live', '/health/ready']
31
+ });
32
+ });
33
+
34
+ const port = process.env.PORT || 3000;
35
+ console.log('\\n🚀 ${name} running on http://localhost:' + port);
36
+ console.log('📊 Health check: http://localhost:' + port + '/health\\n');
37
+
38
+ export default {
39
+ port,
40
+ fetch: app.fetch,
41
+ };
42
+ `;
43
+ }
44
+ /**
45
+ * The historical default. Package scripts, tsconfig, Dockerfile, and the vitest smoke test stay in
46
+ * the shared scaffold so omitted-language output is byte-identical to before providers existed.
47
+ */
48
+ export const bunBackendProvider = {
49
+ id: "bun",
50
+ label: "Bun + Hono",
51
+ installHint: "bun install",
52
+ };
53
+ //# sourceMappingURL=bun.js.map
@@ -0,0 +1,89 @@
1
+ export const PYTHON_DEV_COMMAND = "uvicorn main:app --app-dir src --host 0.0.0.0 --port ${PORT:-4000} --reload --reload-dir src";
2
+ export const pythonBackendProvider = {
3
+ id: "python",
4
+ label: "Python 3.12 + FastAPI",
5
+ devCommand: PYTHON_DEV_COMMAND,
6
+ watch: ["src/**/*"],
7
+ installHint: 'pip install -e ".[dev]" # optional: Docker builds the image for you',
8
+ createFiles(name) {
9
+ return [
10
+ {
11
+ filename: "pyproject.toml",
12
+ content: `[project]
13
+ name = "${name}"
14
+ version = "0.0.1"
15
+ requires-python = ">=3.12"
16
+ dependencies = [
17
+ "fastapi==0.115.6",
18
+ "uvicorn[standard]==0.34.0",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "pytest==8.3.4",
24
+ "httpx==0.28.1",
25
+ ]
26
+
27
+ [build-system]
28
+ requires = ["setuptools>=68"]
29
+ build-backend = "setuptools.build_meta"
30
+
31
+ # Only the dependencies are installed into the image; the source is synced for live reload.
32
+ [tool.setuptools]
33
+ py-modules = []
34
+
35
+ [tool.pytest.ini_options]
36
+ pythonpath = ["src"]
37
+ testpaths = ["tests"]
38
+ `,
39
+ description: "Generating pyproject.toml",
40
+ emoji: "📦",
41
+ },
42
+ {
43
+ filename: "src/main.py",
44
+ content: `import os
45
+
46
+ from fastapi import FastAPI
47
+
48
+ app = FastAPI(title="${name}")
49
+
50
+
51
+ @app.get("/health")
52
+ def health() -> dict[str, str]:
53
+ return {"status": "ok", "service": "${name}"}
54
+
55
+
56
+ @app.get("/")
57
+ def root() -> dict[str, object]:
58
+ return {"service": "${name}", "version": "1.0.0", "endpoints": ["/health"]}
59
+
60
+
61
+ if __name__ == "__main__":
62
+ import uvicorn
63
+
64
+ uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "3000")))
65
+ `,
66
+ description: "Generating backend source",
67
+ emoji: "💻",
68
+ },
69
+ {
70
+ filename: "tests/test_health.py",
71
+ content: `from fastapi.testclient import TestClient
72
+
73
+ from main import app
74
+
75
+ client = TestClient(app)
76
+
77
+
78
+ def test_health() -> None:
79
+ response = client.get("/health")
80
+ assert response.status_code == 200
81
+ assert response.json() == {"status": "ok", "service": "${name}"}
82
+ `,
83
+ description: "Generating test file",
84
+ emoji: "🧪",
85
+ },
86
+ ];
87
+ },
88
+ };
89
+ //# sourceMappingURL=python.js.map
@@ -0,0 +1,30 @@
1
+ import { TdkError } from "../utils/errors.js";
2
+ import { bunBackendProvider } from "./bun.js";
3
+ import { pythonBackendProvider } from "./python.js";
4
+ export const DEFAULT_BACKEND_LANGUAGE = "bun";
5
+ export const BACKEND_LANGUAGES = {
6
+ bun: bunBackendProvider,
7
+ python: pythonBackendProvider,
8
+ };
9
+ export function getBackendLanguage(languageId) {
10
+ const id = (languageId ?? DEFAULT_BACKEND_LANGUAGE).trim().toLowerCase();
11
+ const provider = Object.hasOwn(BACKEND_LANGUAGES, id) ? BACKEND_LANGUAGES[id] : undefined;
12
+ if (!provider) {
13
+ const supportedIds = Object.keys(BACKEND_LANGUAGES).join(", ");
14
+ throw new TdkError(`Unknown backend language "${languageId}". Supported languages: ${supportedIds}.`, [`Use one of: ${supportedIds}`, `Omit --language to use ${DEFAULT_BACKEND_LANGUAGE}`]);
15
+ }
16
+ return provider;
17
+ }
18
+ /** Returns the provider for a backend, or undefined for other types. Rejects --language elsewhere. */
19
+ export function resolveBackendLanguage(resourceType, languageId) {
20
+ if (resourceType !== "backend") {
21
+ if (languageId !== undefined) {
22
+ throw new TdkError("--language can only be used with --type backend.", [
23
+ "Add --type backend, or drop --language for this resource type",
24
+ ]);
25
+ }
26
+ return undefined;
27
+ }
28
+ return getBackendLanguage(languageId);
29
+ }
30
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync } from "node:fs";
2
2
  import { isAbsolute, relative, resolve } from "node:path";
3
3
  import chalk from "chalk";
4
4
  import { Command } from "commander";
5
+ import { getBackendIndexTemplate } from "../backend-languages/bun.js";
6
+ import { resolveBackendLanguage } from "../backend-languages/registry.js";
5
7
  import { resolveFrontendFramework } from "../frontend-frameworks/registry.js";
6
8
  import { hasDddLicense } from "../generator/extension-fetch.js";
7
9
  import { CREATABLE_RESOURCE_TYPES } from "../types/index.js";
@@ -63,8 +65,10 @@ export function resolveByoPort(value, assignedPort, resources) {
63
65
  }
64
66
  return port;
65
67
  }
66
- export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId) {
68
+ export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId, languageId) {
67
69
  const framework = resolveFrontendFramework(type, frameworkId);
70
+ // Only an explicit selection is persisted: a missing `language` has always meant Bun.
71
+ const language = languageId === undefined ? undefined : resolveBackendLanguage(type, languageId);
68
72
  const typeSpecific = TYPE_SPECIFIC[type];
69
73
  const base = JSON.parse(JSON.stringify(BASE_TEMPLATE));
70
74
  for (const [key, value] of Object.entries(typeSpecific)) {
@@ -75,18 +79,33 @@ export function createServiceJson(name, type, stack, port, extraFeatures = [], f
75
79
  base[key] = value;
76
80
  }
77
81
  }
82
+ if (language?.devCommand) {
83
+ base.dev = { command: language.devCommand, watch: language.watch ?? base.dev.watch };
84
+ // Provider-owned runtimes declare the probe path the engine reads; Bun keeps its historical key.
85
+ base.healthCheckPath = base.healthCheck;
86
+ delete base.healthCheck;
87
+ // The engine generates the image (.autogenerated/Dockerfile.app.autogenerated); a build
88
+ // block would point at a Dockerfile that does not exist.
89
+ delete base.build;
90
+ }
78
91
  return {
79
92
  ...base,
80
93
  $schema: SERVICE_MANIFEST_SCHEMA_URL,
81
94
  schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
82
95
  appName: name,
83
96
  appType: type,
84
- featuresEnabled: [...getDefaultFeaturesForResourceType(type), ...extraFeatures],
97
+ featuresEnabled: [
98
+ ...getDefaultFeaturesForResourceType(type).filter(
99
+ // Prisma wiring is Bun-only; the Python provider owns their data layer.
100
+ (feature) => !(language?.createFiles && feature === "prisma")),
101
+ ...extraFeatures,
102
+ ],
85
103
  name,
86
104
  type,
87
105
  stack,
88
106
  port,
89
107
  ...(framework ? { framework: framework.id } : {}),
108
+ ...(language ? { language: language.id } : {}),
90
109
  };
91
110
  }
92
111
  export function createPackageJson(name, type, frameworkId) {
@@ -178,49 +197,7 @@ EXPOSE 3000
178
197
 
179
198
  CMD ["bun", "run", "start"]
180
199
  `;
181
- export function getBackendIndexTemplate(name) {
182
- return `import { Hono } from 'hono';
183
-
184
- const app = new Hono();
185
-
186
- // Health check endpoint (required by TILT_RESOURCE_DEFAULTS.star)
187
- app.get('/health', (c) => {
188
- return c.json({ status: 'ok', service: '${name}' });
189
- });
190
-
191
- app.get('/health/live', (c) => {
192
- return c.json({ status: 'alive', timestamp: Date.now() });
193
- });
194
-
195
- app.get('/health/ready', async (c) => {
196
- const dependencies: Record<string, string> = {};
197
- let allReady = true;
198
-
199
- // Add dependency checks here (database, cache, etc.)
200
- // Mark allReady = false if any dependency is unhealthy
201
-
202
- const status = allReady ? 'ready' : 'not_ready';
203
- return c.json({ status, dependencies }, allReady ? 200 : 503);
204
- });
205
-
206
- app.get('/', (c) => {
207
- return c.json({
208
- service: '${name}',
209
- version: '1.0.0',
210
- endpoints: ['/health', '/health/live', '/health/ready']
211
- });
212
- });
213
-
214
- const port = process.env.PORT || 3000;
215
- console.log('\\n🚀 ${name} running on http://localhost:' + port);
216
- console.log('📊 Health check: http://localhost:' + port + '/health\\n');
217
-
218
- export default {
219
- port,
220
- fetch: app.fetch,
221
- };
222
- `;
223
- }
200
+ export { getBackendIndexTemplate };
224
201
  function getShutdownHandlerTemplate(signal) {
225
202
  return `process.on('${signal}', () => {
226
203
  console.log('[Worker] ${signal} received, shutting down gracefully...');
@@ -312,6 +289,7 @@ export const resourceCommand = new Command("resource")
312
289
  .argument("[name]", "Resource name (kebab-case)")
313
290
  .option("-t, --type <type>", "Resource type: backend, frontend, worker, bring-your-own, sdk", "backend")
314
291
  .option("--framework <id>", "Frontend framework: react (default), vue")
292
+ .option("--language <id>", "Backend language: bun (default), python")
315
293
  .option("-s, --stack <stack>", "Stack to assign resource to", "default")
316
294
  .option("-p, --path <path>", "Custom path for resource directory")
317
295
  .option("--resource-path <path>", "Alias for --path (for backward compatibility)")
@@ -371,6 +349,7 @@ export const resourceCommand = new Command("resource")
371
349
  resourceType = normalizedType;
372
350
  }
373
351
  const frontendFramework = resolveFrontendFramework(resourceType, options.framework);
352
+ const backendLanguage = resolveBackendLanguage(resourceType, options.language);
374
353
  let dddEnabled = false;
375
354
  if (options.ddd) {
376
355
  if (resourceType !== "backend" && resourceType !== "worker") {
@@ -594,7 +573,7 @@ This file contains the resource configuration for TDK.
594
573
  }
595
574
  }
596
575
  // Prepare file generation tasks
597
- const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : [], frontendFramework?.id);
576
+ const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : [], frontendFramework?.id, options.language === undefined ? undefined : backendLanguage?.id);
598
577
  const packageJson = createPackageJson(resourceName, resourceType, frontendFramework?.id);
599
578
  const tasks = [
600
579
  {
@@ -669,6 +648,21 @@ This file contains the resource configuration for TDK.
669
648
  description: "Generating test file",
670
649
  emoji: "🧪",
671
650
  });
651
+ // A language provider that owns its runtime files replaces the shared Bun/TypeScript
652
+ // scaffold (package.json, tsconfig, source, test, and the Dockerfile, which the engine generates); service.json stays shared.
653
+ const languageFiles = backendLanguage?.createFiles?.(resourceName);
654
+ if (languageFiles) {
655
+ const owned = new Set(languageFiles.map((file) => file.filename));
656
+ const sharedTasks = tasks.filter((task) => task.filename === "service.json" ||
657
+ !(owned.has(task.filename) ||
658
+ task.filename === "Dockerfile" ||
659
+ task.filename === "src/index.ts" ||
660
+ task.filename === "package.json" ||
661
+ task.filename === "tsconfig.json" ||
662
+ task.filename.endsWith(".test.ts")));
663
+ tasks.length = 0;
664
+ tasks.push(...sharedTasks, ...languageFiles.map((file) => ({ ...file, type: "text" })));
665
+ }
672
666
  // Execute all file writes with progress
673
667
  console.log(chalk.blue("💻 Generating source files..."));
674
668
  writeFilesWithProgress(fullPath, tasks);
@@ -688,7 +682,7 @@ This file contains the resource configuration for TDK.
688
682
  }
689
683
  console.log(chalk.gray(`\nNext steps:`));
690
684
  console.log(chalk.gray(` cd ${finalResourcePath}`));
691
- console.log(chalk.gray(` bun install`));
685
+ console.log(chalk.gray(` ${backendLanguage?.installHint ?? "bun install"}`));
692
686
  console.log(chalk.gray(` tdk up ${stackName}`));
693
687
  });
694
688
  });
@@ -28,7 +28,7 @@ export function formatUpSuccess(port, appUrls = []) {
28
28
  export function nativeWindowsUpRefusal(platform, allowNativeWindows) {
29
29
  if (platform !== "win32" || allowNativeWindows === "1")
30
30
  return null;
31
- return "Native Windows is not supported for `tdk up`. Use WSL2 with Docker Desktop integration, then run `tdk doctor` inside your WSL project.";
31
+ return "Landscape startup needs Ubuntu on WSL2. Native Windows is inspect-only.";
32
32
  }
33
33
  function waitForTiltUi(port, timeoutMs = 30_000) {
34
34
  return new Promise((resolve) => {
@@ -1,6 +1,7 @@
1
1
  import { execFileSync, execSync } from "node:child_process";
2
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { join, relative } from "node:path";
4
+ import { BACKEND_LANGUAGES } from "../backend-languages/registry.js";
4
5
  import { execAsync } from "./exec-async.js";
5
6
  import { formatCount } from "./formatting.js";
6
7
  import { findProjectRoot } from "./paths.js";
@@ -44,9 +45,16 @@ function backendPorts(resources) {
44
45
  }
45
46
  return ports;
46
47
  }
48
+ /** A backend whose language provider scaffolds its own runtime files (Python) has no package.json. */
49
+ function ownsRuntimeFiles(resource) {
50
+ const language = resource.config?.language;
51
+ return (typeof language === "string" &&
52
+ Object.hasOwn(BACKEND_LANGUAGES, language) &&
53
+ BACKEND_LANGUAGES[language].createFiles !== undefined);
54
+ }
47
55
  export function checkResourcePackageJson(projectRoot = findProjectRoot() ?? process.cwd()) {
48
56
  const missing = discoverResourcesFromRoot(projectRoot)
49
- .filter((resource) => !existsSync(join(resource.path, "package.json")))
57
+ .filter((resource) => !ownsRuntimeFiles(resource) && !existsSync(join(resource.path, "package.json")))
50
58
  .map((resource) => resource.name);
51
59
  if (missing.length === 0) {
52
60
  return {
@@ -20,6 +20,7 @@ const KNOWN_SERVICE_FIELDS = new Set([
20
20
  "framework",
21
21
  "healthCheckPath",
22
22
  "image",
23
+ "language",
23
24
  "name",
24
25
  "nats",
25
26
  "params",
package/cli/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tdk-cli-internal",
3
3
  "private": true,
4
- "version": "1.3.86",
4
+ "version": "1.3.87",
5
5
  "description": "TDK CLI — start services on your laptop.",
6
6
  "keywords": [
7
7
  "tdk",
@@ -222,6 +222,14 @@
222
222
  ],
223
223
  "type": "string"
224
224
  },
225
+ "language": {
226
+ "description": "Backend language provider used to scaffold and run the service. Only meaningful for backend resources; omitted means bun (legacy manifests).",
227
+ "enum": [
228
+ "bun",
229
+ "python"
230
+ ],
231
+ "type": "string"
232
+ },
225
233
  "healthCheckPath": {
226
234
  "default": "/health",
227
235
  "description": "HTTP endpoint for health checks. Must return 200 OK.",
@@ -0,0 +1,62 @@
1
+ # Dockerfiles for backends whose `language` is not the Bun default.
2
+ #
3
+ # Bun backends use the golden-image generator. Python backends are a small
4
+ # standalone image: language-specific base image and reload command, while the build
5
+ # context (project root), `production` target, health check, and PORT contract stay the
6
+ # same as every other backend.
7
+
8
+ BASE_PORT_BACKEND = 4000
9
+
10
+ # Languages whose runtime files are owned by the provider (no Vite/tsconfig/Bun config).
11
+ PROVIDER_OWNED_LANGUAGES = ["python"]
12
+
13
+ _CURL = """RUN apt-get update && apt-get install -y --no-install-recommends curl \\
14
+ && rm -rf /var/lib/apt/lists/*
15
+ """
16
+
17
+ _HEALTHCHECK = """HEALTHCHECK --interval=10s --timeout=5s --retries=3 \\
18
+ CMD curl -f http://localhost:${{PORT:-{port}}}/health || exit 1
19
+ """
20
+
21
+ _PYTHON = """# Generated by TDK for a Python backend. Do not edit; change service.json instead.
22
+ FROM python:3.12-slim AS base
23
+
24
+ WORKDIR /app/{res_path}
25
+
26
+ {curl}
27
+ COPY {res_path}/pyproject.toml ./
28
+ RUN pip install --no-cache-dir .
29
+
30
+ COPY {res_path}/src ./src
31
+
32
+ FROM base AS test
33
+ RUN pip install --no-cache-dir ".[dev]"
34
+ COPY {res_path}/tests ./tests
35
+ RUN pytest
36
+
37
+ FROM base AS production
38
+
39
+ {healthcheck}
40
+ EXPOSE {port}
41
+
42
+ CMD ["sh", "-c", "uvicorn main:app --app-dir src --host 0.0.0.0 --port ${{PORT:-{port}}} --reload --reload-dir src"]
43
+ """
44
+
45
+ def generate_language_dockerfile(res_path, language, port = BASE_PORT_BACKEND):
46
+ """Dockerfile text for a provider-owned backend language, or fail for an unknown one."""
47
+ template = {"python": _PYTHON}.get(language)
48
+ if template == None:
49
+ fail("No Dockerfile generator for backend language '{}'".format(language))
50
+ return template.format(
51
+ res_path = res_path,
52
+ port = port,
53
+ curl = _CURL,
54
+ healthcheck = _HEALTHCHECK.format(port = port),
55
+ )
56
+
57
+
58
+ def is_provider_owned_language(manifest):
59
+ """True when the manifest selects Python (a missing language means Bun)."""
60
+ if not manifest:
61
+ return False
62
+ return manifest.get("appType", "backend") == "backend" and manifest.get("language", "bun") in PROVIDER_OWNED_LANGUAGES
@@ -4,6 +4,11 @@ load(
4
4
  "generate_app_dockerfile",
5
5
  "generate_migrator_dockerfile",
6
6
  )
7
+ load(
8
+ "./dockerfile/language_dockerfile.star",
9
+ "generate_language_dockerfile",
10
+ "is_provider_owned_language",
11
+ )
7
12
  load("./config/dockerignore.star", "generate_dockerignore")
8
13
  load(
9
14
  "./compose/compose.star",
@@ -18,6 +23,8 @@ Docker = struct(
18
23
  frontend = generate_frontend_dockerfile,
19
24
  backend = generate_app_dockerfile,
20
25
  migrator = generate_migrator_dockerfile,
26
+ backend_language = generate_language_dockerfile,
27
+ is_provider_owned_language = is_provider_owned_language,
21
28
  generate_dockerignore = generate_dockerignore,
22
29
  frontend_compose = generate_frontend_compose,
23
30
  backend_compose = generate_backend_compose_entry,
@@ -556,8 +556,11 @@ def generate_resource_health_check(resource_name, resource_path, manifest, check
556
556
 
557
557
  # Check 1: Required files exist
558
558
  frontend_entry = "src/main.ts" if manifest.get("framework", "react") == "vue" else "src/main.tsx"
559
+ backend_files = {
560
+ "python": ["pyproject.toml", "src/main.py"],
561
+ }.get(manifest.get("language", "bun"), ["package.json", "prisma/schema.prisma"])
559
562
  required_files = {
560
- "backend": ["package.json", "prisma/schema.prisma"],
563
+ "backend": backend_files,
561
564
  "frontend": ["package.json", "index.html", frontend_entry],
562
565
  "library": ["package.json"],
563
566
  }.get(app_type, ["package.json"])
@@ -570,8 +573,9 @@ if [ ! -f "{}" ]; then
570
573
  fi
571
574
  '''.format(req_file, req_file))
572
575
 
573
- # Check 2: Package.json is valid JSON
574
- health_checks.append('''
576
+ # Check 2: Package.json is valid JSON (Python backends have no package.json)
577
+ if manifest.get("language", "bun") != "python":
578
+ health_checks.append('''
575
579
  if ! cat package.json | head -1 > /dev/null 2>&1; then
576
580
  echo "❌ package.json is not valid"
577
581
  exit 1
@@ -201,14 +201,39 @@ def _register_replicas(config, auto_init_apps):
201
201
  )
202
202
 
203
203
 
204
- def _build_live_update_rules(res_path, full_res_path, syncs):
205
- """Build live update sync and run rules for a resource."""
204
+ def _live_update_sync_source(project_root, full_res_path, sync_path):
205
+ """Local path Tilt watches for a live-update sync.
206
+
207
+ Tilt resolves sync() local paths against the Tiltfile directory (.tdk/.tdk-out), not
208
+ the docker_build context, so a project-relative path never matches the changed files
209
+ and every edit falls back to a full image rebuild ("Found file(s) not matching any
210
+ sync"). Anchor it at the project root, like the Dockerfile and build context.
211
+ """
212
+ relative_path = full_res_path + '/' + sync_path
213
+ if project_root:
214
+ return project_root.rstrip('/') + '/' + relative_path
215
+ return relative_path
216
+
217
+
218
+ def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', language='bun'):
219
+ """Build live update sync and run rules for a resource.
220
+
221
+ Python backends reload their own process after a sync (uvicorn --reload), so they have
222
+ no Bun install step. A changed pyproject.toml is not synced, so Tilt rebuilds the image instead.
223
+ """
206
224
  live_update_rules = []
207
225
  for sync_path in syncs:
208
- full_sync_path = full_res_path + '/' + sync_path
226
+ # Python images never read the npm/Bun registry config, and the engine does not
227
+ # generate it for provider-owned languages, so there is nothing to sync.
228
+ if language == 'python' and (sync_path.endswith('.npmrc.docker.autogenerated') or sync_path.endswith('bunfig.docker.autogenerated.toml')):
229
+ continue
230
+ full_sync_path = _live_update_sync_source(project_root, full_res_path, sync_path)
209
231
  dest = '/app/' + full_res_path + '/' + sync_path
210
232
  live_update_rules.append(sync(full_sync_path, dest))
211
233
 
234
+ if language == 'python':
235
+ return live_update_rules
236
+
212
237
  package_json_path = '/app/' + full_res_path + '/package.json'
213
238
  bun_lock_path = '/app/' + full_res_path + '/bun.lock'
214
239
  live_update_rules.append(run('cd /app/' + full_res_path + ' && bun install',
@@ -373,7 +398,9 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
373
398
  live_update_rules = _build_live_update_rules(
374
399
  config['res_path'],
375
400
  config['res_path'],
376
- config['syncs']
401
+ config['syncs'],
402
+ project_root=config.get('project_root', ''),
403
+ language=config.get('manifest', {}).get('language', 'bun'),
377
404
  )
378
405
  _register_docker_build(config, live_update_rules)
379
406
  if defers_start:
@@ -310,6 +310,18 @@ def _generate_all_configs_for_resource(
310
310
 
311
311
  app_type = manifest.get('appType', 'backend')
312
312
  is_frontend = resource_config.get('frontend', False) or app_type == 'frontend'
313
+ # Python backends own their runtime files (package/pyproject, Dockerfile,
314
+ # reload command). Bun-only configs below (Vite, tsconfig, Prisma, bunfig) are skipped.
315
+ provider_owned = Docker.is_provider_owned_language(manifest)
316
+ if provider_owned:
317
+ # Fail before generating anything, with the missing file named, instead of a Docker
318
+ # COPY error halfway through the image build.
319
+ root = ctx.get('project_root', '')
320
+ prefix = (root.rstrip('/') + '/' if root else '') + resource_path + '/'
321
+ for required in ['pyproject.toml', 'src/main.py']:
322
+ if not os.path.exists(prefix + required):
323
+ fail("{}: language '{}' requires {} but {}{} does not exist".format(
324
+ resource_name, manifest.get('language'), required, resource_path + '/', required))
313
325
 
314
326
  # ==========================================================================
315
327
  # 1. VITE CONFIG
@@ -319,7 +331,7 @@ def _generate_all_configs_for_resource(
319
331
  Vite.for_manifest(manifest, backend_manifest, write_file)
320
332
  else:
321
333
  Vite.for_manifest(manifest, None, write_file)
322
- elif app_type == 'backend':
334
+ elif app_type == 'backend' and not provider_owned:
323
335
  Vite.for_manifest(manifest, None, write_file)
324
336
  elif app_type == 'sdk':
325
337
  Vite.for_manifest(manifest, None, write_file)
@@ -365,6 +377,12 @@ def _generate_all_configs_for_resource(
365
377
  use_nginx=use_nginx,
366
378
  use_golden=should_enable('golden-image')
367
379
  )
380
+ elif provider_owned:
381
+ dockerfile_content = Docker.backend_language(
382
+ res_path=resource_path,
383
+ language=manifest.get('language'),
384
+ port=manifest.get('port', 4000),
385
+ )
368
386
  else:
369
387
  dockerfile_content = Docker.backend(
370
388
  res_path=resource_path,
@@ -419,7 +437,7 @@ def _generate_all_configs_for_resource(
419
437
  resource_path, write_file, internal_deps=internal_deps_map, is_docker=False,
420
438
  framework=manifest.get('framework', 'react'),
421
439
  )
422
- else:
440
+ elif not provider_owned:
423
441
  prisma_path = resource_config.get('prisma_client_path', './node_modules/.prisma/client')
424
442
  resource_features = manifest.get('featuresEnabled', [])
425
443
  TSConfig.backend(resource_path, prisma_path, write_file, internal_deps=internal_deps_map, is_docker=True, features=resource_features)
@@ -434,10 +452,11 @@ def _generate_all_configs_for_resource(
434
452
  # 8. PACKAGE CONFIGS (npmrc, bunfig)
435
453
  # ==========================================================================
436
454
  verdaccio_enabled = global_config.get('verdaccio_enabled', True)
437
- PackageConfig.npmrc(resource_path=resource_path, registry_url=global_config['verdaccio_url_docker'], is_docker=True, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
438
- PackageConfig.bunfig(resource_path=resource_path, registry_url=global_config['verdaccio_url_docker'], is_docker=True, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
439
- PackageConfig.npmrc(resource_path=resource_path, registry_url=global_config['verdaccio_url_local'], is_docker=False, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
440
- PackageConfig.bunfig(resource_path=resource_path, registry_url=global_config['verdaccio_url_local'], is_docker=False, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
455
+ if not provider_owned:
456
+ PackageConfig.npmrc(resource_path=resource_path, registry_url=global_config['verdaccio_url_docker'], is_docker=True, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
457
+ PackageConfig.bunfig(resource_path=resource_path, registry_url=global_config['verdaccio_url_docker'], is_docker=True, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
458
+ PackageConfig.npmrc(resource_path=resource_path, registry_url=global_config['verdaccio_url_local'], is_docker=False, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
459
+ PackageConfig.bunfig(resource_path=resource_path, registry_url=global_config['verdaccio_url_local'], is_docker=False, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
441
460
 
442
461
  # ==========================================================================
443
462
  # 9. YAML MANIFEST (for verification)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdk-landscape/tdk-cli-core",
3
- "version": "1.3.86",
3
+ "version": "1.3.87",
4
4
  "description": "TDK CLI — start services on your laptop.",
5
5
  "homepage": "https://tdk-landscape.github.io/tdk-website",
6
6
  "repository": {