@tdk-landscape/tdk-cli-core 1.3.74 → 1.3.76

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
+ - `tdk up` no longer leaves `nats` (and any other service in `services/platform/messaging/docker-compose.yml`, e.g. a local mail catcher) disabled in Tilt. The pre-alpha default always goes through a focus-filter path that only re-enabled `init-networks`/`postgres`/`traefik`/`golden-layers-build`, so a resource with the `nats` feature retried its broker connection forever until you ran `tilt enable nats` by hand. Fixes #155.
8
+
9
+ - `tdk doctor` now catches wiring mistakes that used to surface minutes into `tdk up`: a resource with no `package.json` (the image build stops at "COPY ... not found"), a `params` URL such as `http://auth-emulator:4000` whose port is not the one the target listens on (its `service.json` `port`), a frontend calling a backend through `localhost:<port>` (TDK publishes no backend ports on localhost; use the Traefik route), the `nats` feature without `services/platform/messaging/docker-compose.yml` (TDK does not generate it, so no NATS server starts), two Tilt processes running for one project, and Docker having no address pool left for the networks a project needs. Found while building [tdk-ecommerce-example](https://github.com/tdk-landscape/tdk-ecommerce-example) and a queue/auth example.
10
+
11
+ - `tdk project ecommerce` clones [tdk-ecommerce-example](https://github.com/tdk-landscape/tdk-ecommerce-example), a Vue 3 storefront and Hono catalog API. Before, only restaurant, saas, erp, user-management and example were accepted.
12
+
7
13
  ## 1.3.73 (2026-09-28)
8
14
 
9
15
  - Free-tier projects no longer start the `sablier` and `wake-gateway` containers, and Traefik no longer downloads the Sablier plugin from GitHub on every start. They were always launched (the `sablier` one with a read-write Docker socket) even though the license only gated the per-service labels, so nothing could use them. They are now generated only when a license grants Sablier. Fewer containers, one image build less, and no network fetch at Traefik boot. Spotted in the ERP CI run log.
package/README.md CHANGED
@@ -10,6 +10,7 @@ One CLI that scaffolds your services and runs the whole landscape locally with h
10
10
  [![CI](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml/badge.svg)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml)
11
11
  [![Quickstart E2E](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml/badge.svg)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml)
12
12
  [![ERP scale E2E](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/erp-scale-e2e.yml/badge.svg)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/erp-scale-e2e.yml)
13
+ [![Example apps E2E](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/examples-e2e.yml/badge.svg?branch=main)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/examples-e2e.yml)
13
14
  [![Known Vulnerabilities](https://snyk.io/test/github/tdk-landscape/tdk-cli-core/badge.svg)](https://snyk.io/test/github/tdk-landscape/tdk-cli-core)
14
15
  [![Socket Badge](https://badge.socket.dev/npm/package/@tdk-landscape/tdk-cli-core/latest)](https://socket.dev/npm/package/@tdk-landscape/tdk-cli-core/overview)
15
16
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
@@ -28,6 +29,8 @@ tdk up shop # run it with hot reload
28
29
 
29
30
  ![tdk scaffolding a backend and a frontend, then listing the stack](docs/demo.svg)
30
31
 
32
+ ![npm downloads, last month](docs/npm-downloads.svg)
33
+
31
34
  **Requirements:** Docker (Desktop, OrbStack or Colima; Engine 25+, Compose 2.20+), [Tilt](https://docs.tilt.dev/install.html), [Bun](https://bun.sh) for the generated services, and Node.js 22.12+ for the CLI. Ports 80, 443 and 5432 must be free (a local Postgres is the usual conflict). macOS and Linux are supported. Windows is untested (WSL2 may work). Run `tdk doctor` to check all of it.
32
35
 
33
36
  This is the core monorepo for TDK: the `tdk` CLI, the Starlark-based Tilt orchestration engine that powers it, and the discovery system that turns a directory of services into a running local landscape.
@@ -184,14 +187,14 @@ make pre-commit-run # run pre-commit hooks on all files
184
187
  tdk doctor
185
188
  ```
186
189
 
187
- Checks Docker, Compose and Tilt. Inside a project it also checks the generated configs, `.env`, service scripts and that the ingress ports are free, before you run `tdk up`.
190
+ Checks Docker, Compose and Tilt. Inside a project it also checks the generated configs, `.env`, service scripts and that the ingress ports are free, before you run `tdk up`. It also catches wiring mistakes: a resource without `package.json`, a `params` URL that uses the wrong port for another service, a frontend calling a backend on `localhost:<port>`, the `nats` feature without a broker to start, duplicate Tilt processes, and Docker running out of network address space.
188
191
 
189
192
  ## Community
190
193
 
191
194
  - **Questions and ideas:** [open an issue](https://github.com/tdk-landscape/tdk-cli-core/issues/new/choose).
192
195
  - **Contributing:** see [CONTRIBUTING.md](CONTRIBUTING.md). Issues labeled [`good first issue`](https://github.com/tdk-landscape/tdk-cli-core/labels/good%20first%20issue) are a good place to start.
193
196
  - **Security:** see [SECURITY.md](SECURITY.md). Please don't report vulnerabilities in public issues.
194
- - **Examples:** [ERP system (100 services)](https://github.com/tdk-landscape/tdk-erp-system), [SaaS starter](https://github.com/tdk-landscape/tdk-saas-starter), [restaurant](https://github.com/tdk-landscape/tdk-restaurant-example).
197
+ - **Examples:** [ERP system (100 services)](https://github.com/tdk-landscape/tdk-erp-system), [SaaS starter](https://github.com/tdk-landscape/tdk-saas-starter), [restaurant](https://github.com/tdk-landscape/tdk-restaurant-example), [ecommerce (Vue + Hono)](https://github.com/tdk-landscape/tdk-ecommerce-example).
195
198
 
196
199
  ## License
197
200
 
@@ -1,11 +1,13 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
- import { dirname, join, normalize } from "node:path";
3
+ import { dirname, join, normalize, relative } from "node:path";
4
4
  import chalk from "chalk";
5
5
  import { Command } from "commander";
6
6
  import { hasVerdaccioLicense } from "../generator/extension-fetch.js";
7
7
  import { MASTER_CONFIG_FILES, QUICKSTART_DOCS_URL, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
8
+ import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
8
9
  import { checkHealthRoutes, checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
10
+ import { checkDockerNetworkCapacity, checkFrontendBackendUrls, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
9
11
  import { validateEnvFile } from "../utils/env-validator.js";
10
12
  import { formatCount } from "../utils/formatting.js";
11
13
  import { findProjectRoot } from "../utils/paths.js";
@@ -218,6 +220,31 @@ function checkMasterConfigs() {
218
220
  fix: "Run: tdk project",
219
221
  };
220
222
  }
223
+ /**
224
+ * Tilt only builds resources under `discovery.paths`, but the CLI finds every service.json,
225
+ * so a resource outside them is listed and given a URL by `tdk up` yet never started.
226
+ */
227
+ export function checkResourceDiscovery() {
228
+ const projectRoot = findProjectRoot() ?? process.cwd();
229
+ const patterns = readDiscoveryPaths(projectRoot);
230
+ const stranded = discoverResourcesFromRoot(projectRoot).filter((resource) => !isPathDiscovered(projectRoot, resource.path, patterns));
231
+ if (stranded.length === 0) {
232
+ return {
233
+ name: "Resource Discovery",
234
+ didPass: true,
235
+ message: `All resources are covered by discovery.paths ${JSON.stringify(patterns)}`,
236
+ };
237
+ }
238
+ const list = stranded
239
+ .map((resource) => normalize(relative(projectRoot, resource.path)))
240
+ .join(", ");
241
+ return {
242
+ name: "Resource Discovery",
243
+ didPass: false,
244
+ message: `${formatCount(stranded.length, "resource")} outside discovery.paths ${JSON.stringify(patterns)}: ${list}. \`tdk up\` will not start them`,
245
+ fix: "Move each under services/<stack>/<name>, or add a matching pattern to discovery.paths in .tdk/project.json and run: tdk config regenerate",
246
+ };
247
+ }
221
248
  // The scripts `tdk project` copies into every project. Generated Dockerfiles
222
249
  // may reference others; those are checked by scanning the Dockerfiles below.
223
250
  const REQUIRED_DOCKER_TEMPLATE_FILES = [
@@ -491,6 +518,26 @@ const KNOWN_TYPE_PACKAGE_VERSIONS = {
491
518
  "@types/bun": "^1.4.2",
492
519
  "@types/node": "^20.0.0",
493
520
  };
521
+ /**
522
+ * `@types/bun` (via its underlying `bun-types` package) ships a hardcoded
523
+ * `/// <reference types="node" />` in its own index.d.ts. TypeScript always
524
+ * honors an explicit triple-slash reference, regardless of what the
525
+ * tsconfig's own `"types"` array restricts to -- so a Docker tsconfig with
526
+ * `"types": ["bun"]` still requires `@types/node` to resolve, even though
527
+ * "node" never appears in that array.
528
+ *
529
+ * When `@types/node` isn't a direct dependency, it can only resolve as a
530
+ * *transitive* dependency of `bun-types`, hoisted wherever Bun's installer
531
+ * happens to place it. That is not guaranteed in the isolated, per-service
532
+ * `bun install` that Docker's build layers run (unlike a full workspace-root
533
+ * install), so the same tsconfig can build locally and fail in Docker with
534
+ * `TS2688: Cannot find type definition file for 'node'`. Declaring
535
+ * `@types/node` directly makes the install deterministic either way.
536
+ */
537
+ function needsNodeTypesForBun(installed, types) {
538
+ const usesBunTypes = types.includes("bun") || installed.has("@types/bun");
539
+ return usesBunTypes && !installed.has("@types/node");
540
+ }
494
541
  export function checkTypeScriptTypeDependencies() {
495
542
  const projectRoot = findProjectRoot() ?? process.cwd();
496
543
  const resources = discoverResourcesFromRoot(projectRoot);
@@ -527,6 +574,10 @@ export function checkTypeScriptTypeDependencies() {
527
574
  const described = missing.map((m) => `"${m}" (needs ${neededTypePackage(m)})`).join(", ");
528
575
  problems.push(`${resource.name}: "types" lists ${described}`);
529
576
  }
577
+ if (needsNodeTypesForBun(installed, types)) {
578
+ missingPackages.add("@types/node");
579
+ problems.push(`${resource.name}: uses @types/bun, which references node types internally, but @types/node is not a direct dependency`);
580
+ }
530
581
  }
531
582
  if (problems.length === 0) {
532
583
  return {
@@ -542,7 +593,7 @@ export function checkTypeScriptTypeDependencies() {
542
593
  return {
543
594
  name: "TypeScript Type Dependencies",
544
595
  didPass: false,
545
- message: `Docker tsconfig "types" names packages the service does not depend on, so \`bun run build\` fails with TS2688 (Cannot find type definition file):\n ${problems.join("\n ")}`,
596
+ message: `\`bun run build\` will fail with TS2688 (Cannot find type definition file) because a @types package it needs is not a direct dependency:\n ${problems.join("\n ")}`,
546
597
  fix: `Add to each service's package.json devDependencies: ${lines.join(", ")}`,
547
598
  };
548
599
  }
@@ -573,6 +624,11 @@ export function checkFrontendDockerPreflight() {
573
624
  }
574
625
  const problems = [];
575
626
  for (const resource of resources) {
627
+ // The frontend Docker builder runs `COPY <resource>/public ./public`, so a missing
628
+ // folder fails the image build with "/public: not found".
629
+ if (!existsSync(join(resource.path, "public"))) {
630
+ problems.push(`${resource.name}: no public/ folder, but the Docker build copies it. Create it: mkdir -p ${normalize(relative(projectRoot, join(resource.path, "public")))} && touch ${normalize(relative(projectRoot, join(resource.path, "public", ".gitkeep")))}`);
631
+ }
576
632
  const dockerfilePath = join(resource.path, ".autogenerated", "Dockerfile.app.autogenerated");
577
633
  const dockerTsconfigPath = join(resource.path, ".autogenerated", "tsconfig.docker.autogenerated.json");
578
634
  if (existsSync(dockerfilePath)) {
@@ -740,6 +796,8 @@ export const doctorCommand = new Command("doctor")
740
796
  checkTilt,
741
797
  checkDockerCompose,
742
798
  checkDockerVersions,
799
+ // Each project needs several networks; a full address pool fails `tdk up` late.
800
+ () => checkDockerNetworkCapacity(),
743
801
  ];
744
802
  const projectChecks = [
745
803
  checkMasterConfigs,
@@ -750,6 +808,13 @@ export const doctorCommand = new Command("doctor")
750
808
  () => checkHealthRoutes(),
751
809
  checkTypeScriptTypeDependencies,
752
810
  checkFrontendDockerPreflight,
811
+ checkResourceDiscovery,
812
+ // Wiring mistakes that otherwise surface minutes into `tdk up`.
813
+ () => checkResourcePackageJson(),
814
+ () => checkServiceUrlPorts(),
815
+ () => checkFrontendBackendUrls(),
816
+ () => checkNatsBroker(),
817
+ () => checkTiltInstances(),
753
818
  checkEnvironmentVariables,
754
819
  // Preflight: catch "port 80 already allocated" BEFORE claiming ready.
755
820
  checkIngressPorts,
@@ -2,9 +2,11 @@ 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 { resolveFrontendFramework } from "../frontend-frameworks/registry.js";
5
6
  import { hasDddLicense } from "../generator/extension-fetch.js";
6
7
  import { CREATABLE_RESOURCE_TYPES } from "../types/index.js";
7
8
  import { assertValid, confirmOrCancel } from "../utils/command-helpers.js";
9
+ import { chooseResourcePath, isPathDiscovered, readDiscoveryPaths, } from "../utils/discovery-paths.js";
8
10
  import { errorFactories, requireProjectRoot, runCommand } from "../utils/errors.js";
9
11
  import { writeFilesWithProgress } from "../utils/file-helpers.js";
10
12
  import { showCommandHeader } from "../utils/formatting.js";
@@ -42,7 +44,8 @@ export const TYPE_SPECIFIC = {
42
44
  },
43
45
  },
44
46
  };
45
- export function createServiceJson(name, type, stack, port, extraFeatures = []) {
47
+ export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId) {
48
+ const framework = resolveFrontendFramework(type, frameworkId);
46
49
  const typeSpecific = TYPE_SPECIFIC[type];
47
50
  const base = JSON.parse(JSON.stringify(BASE_TEMPLATE));
48
51
  for (const [key, value] of Object.entries(typeSpecific)) {
@@ -62,17 +65,23 @@ export function createServiceJson(name, type, stack, port, extraFeatures = []) {
62
65
  type,
63
66
  stack,
64
67
  port,
68
+ ...(framework ? { framework: framework.id } : {}),
65
69
  };
66
70
  }
67
- export function createPackageJson(name, type) {
71
+ export function createPackageJson(name, type, frameworkId) {
68
72
  const isFrontend = type === "frontend";
73
+ const framework = resolveFrontendFramework(type, frameworkId);
69
74
  return {
70
75
  name: `@project/${name}`,
71
76
  version: "0.0.1",
72
77
  type: "module",
73
78
  scripts: {
74
- dev: isFrontend ? "vite" : "bun run --watch src/index.ts",
75
- build: isFrontend ? "tsc && vite build" : "tsc",
79
+ dev: isFrontend
80
+ ? "vite --config .autogenerated/vite.config.frontend.autogenerated.ts"
81
+ : "bun run --watch src/index.ts",
82
+ build: isFrontend
83
+ ? "tsc && vite build --config .autogenerated/vite.config.build.autogenerated.ts"
84
+ : "tsc",
76
85
  // Backends and workers run `start` in their production image; doctor checks for it.
77
86
  ...(isFrontend ? {} : { start: "bun run dist/index.js" }),
78
87
  test: "vitest",
@@ -81,6 +90,7 @@ export function createPackageJson(name, type) {
81
90
  },
82
91
  dependencies: {
83
92
  ...(isFrontend ? {} : { hono: "^4.0.0" }),
93
+ ...(framework?.dependencies ?? {}),
84
94
  },
85
95
  devDependencies: {
86
96
  "@types/bun": "^1.4.2",
@@ -91,6 +101,17 @@ export function createPackageJson(name, type) {
91
101
  vitest: "^5.0.0",
92
102
  "@biomejs/biome": "^2.5.13",
93
103
  ...(isFrontend ? { vite: "^5.0.0" } : {}),
104
+ ...(framework?.devDependencies ?? {}),
105
+ },
106
+ };
107
+ }
108
+ export function createResourceTsconfig(resourceType, frameworkId) {
109
+ const framework = resolveFrontendFramework(resourceType, frameworkId);
110
+ return {
111
+ ...TSCONFIG_TEMPLATE,
112
+ compilerOptions: {
113
+ ...TSCONFIG_TEMPLATE.compilerOptions,
114
+ ...(framework?.compilerOptions ?? {}),
94
115
  },
95
116
  };
96
117
  }
@@ -179,44 +200,6 @@ export default {
179
200
  };
180
201
  `;
181
202
  }
182
- function getFrontendIndexTemplate(name) {
183
- return `<!DOCTYPE html>
184
- <html lang="en">
185
- <head>
186
- <meta charset="UTF-8" />
187
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
188
- <title>${name}</title>
189
- </head>
190
- <body>
191
- <div id="root"></div>
192
- <script type="module" src="/src/main.tsx"></script>
193
- </body>
194
- </html>
195
- `;
196
- }
197
- const FRONTEND_MAIN_TEMPLATE = `import React from 'react';
198
- import ReactDOM from 'react-dom/client';
199
- import App from './App';
200
-
201
- ReactDOM.createRoot(document.getElementById('root')!).render(
202
- <React.StrictMode>
203
- <App />
204
- </React.StrictMode>,
205
- );
206
- `;
207
- function getFrontendAppTemplate(name) {
208
- return `function App() {
209
- return (
210
- <div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
211
- <h1>${name}</h1>
212
- <p>Frontend resource created with TDK</p>
213
- </div>
214
- );
215
- }
216
-
217
- export default App;
218
- `;
219
- }
220
203
  function getShutdownHandlerTemplate(signal) {
221
204
  return `process.on('${signal}', () => {
222
205
  console.log('[Worker] ${signal} received, shutting down gracefully...');
@@ -307,6 +290,7 @@ export const resourceCommand = new Command("resource")
307
290
  .description("Create a new resource (service) from scratch, or register an existing one")
308
291
  .argument("[name]", "Resource name (kebab-case)")
309
292
  .option("-t, --type <type>", "Resource type: backend, frontend, worker, sdk", "backend")
293
+ .option("--framework <id>", "Frontend framework: react (default), vue")
310
294
  .option("-s, --stack <stack>", "Stack to assign resource to", "default")
311
295
  .option("-p, --path <path>", "Custom path for resource directory")
312
296
  .option("--resource-path <path>", "Alias for --path (for backward compatibility)")
@@ -351,6 +335,7 @@ export const resourceCommand = new Command("resource")
351
335
  else {
352
336
  resourceType = options.type;
353
337
  }
338
+ const frontendFramework = resolveFrontendFramework(resourceType, options.framework);
354
339
  let dddEnabled = false;
355
340
  if (options.ddd) {
356
341
  if (resourceType !== "backend" && resourceType !== "worker") {
@@ -409,6 +394,7 @@ export const resourceCommand = new Command("resource")
409
394
  }
410
395
  }
411
396
  let finalResourcePath = resourcePath;
397
+ let pathAdjustedFrom;
412
398
  if (!finalResourcePath) {
413
399
  const defaultPaths = {
414
400
  backend: `services/${stackName}/${resourceName}`,
@@ -417,6 +403,12 @@ export const resourceCommand = new Command("resource")
417
403
  sdk: `packages/${resourceName}`,
418
404
  };
419
405
  finalResourcePath = defaultPaths[resourceType];
406
+ // An SDK is registered where it already lives, so its folder is never moved.
407
+ if (resourceType !== "sdk") {
408
+ const chosen = chooseResourcePath(projectRoot, finalResourcePath, stackName, resourceName);
409
+ finalResourcePath = chosen.path;
410
+ pathAdjustedFrom = chosen.adjustedFrom;
411
+ }
420
412
  }
421
413
  const fullPath = resolve(projectRoot, finalResourcePath);
422
414
  // Prevent path traversal attacks
@@ -482,14 +474,17 @@ export const resourceCommand = new Command("resource")
482
474
  mkdirSync(fullPath, { recursive: true });
483
475
  mkdirSync(resolve(fullPath, "src"), { recursive: true });
484
476
  mkdirSync(resolve(fullPath, "tests"), { recursive: true });
477
+ if (resourceType === "frontend") {
478
+ mkdirSync(resolve(fullPath, "public"), { recursive: true });
479
+ }
485
480
  if (dddEnabled) {
486
481
  for (const layer of ["domain", "application", "infrastructure", "presentation"]) {
487
482
  mkdirSync(resolve(fullPath, "src", layer), { recursive: true });
488
483
  }
489
484
  }
490
485
  // Prepare file generation tasks
491
- const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : []);
492
- const packageJson = createPackageJson(resourceName, resourceType);
486
+ const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : [], frontendFramework?.id);
487
+ const packageJson = createPackageJson(resourceName, resourceType, frontendFramework?.id);
493
488
  const tasks = [
494
489
  {
495
490
  type: "json",
@@ -508,7 +503,7 @@ export const resourceCommand = new Command("resource")
508
503
  {
509
504
  type: "json",
510
505
  filename: "tsconfig.json",
511
- content: TSCONFIG_TEMPLATE,
506
+ content: createResourceTsconfig(resourceType, frontendFramework?.id),
512
507
  description: "Generating tsconfig.json",
513
508
  emoji: "⚙️",
514
509
  },
@@ -531,26 +526,19 @@ export const resourceCommand = new Command("resource")
531
526
  });
532
527
  }
533
528
  else if (resourceType === "frontend") {
534
- tasks.push({
529
+ tasks.push(...(frontendFramework?.createFiles(resourceName).map((file) => ({
530
+ ...file,
535
531
  type: "text",
536
- filename: "index.html",
537
- content: getFrontendIndexTemplate(resourceName),
538
- description: "Generating HTML template",
539
- emoji: "💻",
540
- });
532
+ })) ?? []));
533
+ // The shared frontend Docker builder unconditionally runs `COPY <resource>/public`
534
+ // (the Vite static-assets folder), and an empty folder can't be committed, so
535
+ // without a placeholder the image build fails for every framework.
541
536
  tasks.push({
542
537
  type: "text",
543
- filename: "src/main.tsx",
544
- content: FRONTEND_MAIN_TEMPLATE,
545
- description: "Generating React entry",
546
- emoji: "💻",
547
- });
548
- tasks.push({
549
- type: "text",
550
- filename: "src/App.tsx",
551
- content: getFrontendAppTemplate(resourceName),
552
- description: "Generating React app",
553
- emoji: "💻",
538
+ filename: "public/.gitkeep",
539
+ content: "",
540
+ description: "Generating public/ assets folder",
541
+ emoji: "📁",
554
542
  });
555
543
  }
556
544
  else if (resourceType === "worker") {
@@ -575,6 +563,18 @@ export const resourceCommand = new Command("resource")
575
563
  writeFilesWithProgress(fullPath, tasks);
576
564
  console.log(chalk.green("\n✅ Resource created successfully!"));
577
565
  console.log(chalk.gray(`\nLocation: ${fullPath}`));
566
+ const discoveryPaths = readDiscoveryPaths(projectRoot);
567
+ if (pathAdjustedFrom) {
568
+ console.log(chalk.yellow(`\n📁 Placed in ${finalResourcePath} instead of the usual ${pathAdjustedFrom}: ` +
569
+ `discovery.paths in .tdk/project.json is ${JSON.stringify(discoveryPaths)}, ` +
570
+ `so ${pathAdjustedFrom} would never be started by \`tdk up\`.`));
571
+ console.log(chalk.gray(" Use --path to choose another folder."));
572
+ }
573
+ else if (!isPathDiscovered(projectRoot, finalResourcePath, discoveryPaths)) {
574
+ console.log(chalk.yellow(`\n⚠️ ${finalResourcePath} is outside discovery.paths ${JSON.stringify(discoveryPaths)}, ` +
575
+ "so `tdk up` will not start this resource."));
576
+ console.log(chalk.gray(" Add a matching pattern to discovery.paths in .tdk/project.json and run `tdk config regenerate`."));
577
+ }
578
578
  console.log(chalk.gray(`\nNext steps:`));
579
579
  console.log(chalk.gray(` cd ${finalResourcePath}`));
580
580
  console.log(chalk.gray(` bun install`));
@@ -11,8 +11,8 @@ import { isTiltAvailable } from "../utils/tilt.js";
11
11
  const HelpPanel = ({ onClose }) => (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60, children: [_jsx(Text, { bold: true, color: "cyan", children: "Keyboard Shortcuts" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, underline: true, children: "Navigation" }), _jsx(Text, { children: " \u2191/\u2193 Navigate list items" }), _jsx(Text, { children: " Enter Select item / Open detail" }), _jsx(Text, { children: " Space Toggle expand (tree view)" }), _jsx(Text, { children: " Tab Next tab" }), _jsx(Text, { children: " 1-5 Direct tab access" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: "Actions" }) }), _jsx(Text, { children: " a Toggle all/pre-alpha services" }), _jsx(Text, { children: " m Toggle mouse support" }), _jsx(Text, { children: " t Toggle tooltips" }), _jsx(Text, { children: " e Toggle enabled/disabled services" }), _jsx(Text, { children: " r Refresh data" }), _jsx(Text, { children: " / Search/filter" }), _jsx(Text, { children: " ? Show this help" }), _jsx(Text, { children: " q/Esc Quit / Back" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to close..." }) })] }));
12
12
  const LoadingScreen = ({ progress, message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { children: ["Loading: ", message] }), _jsxs(Box, { marginY: 1, borderStyle: "single", borderColor: "gray", width: 50, children: [_jsx(Box, { width: progress / 2, backgroundColor: "cyan", children: _jsx(Text, { children: " ".repeat(progress / 2) }) }), _jsxs(Text, { children: [" ", progress, "%"] })] })] }));
13
13
  // biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
14
- const ErrorScreen = ({ error, onRetry }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "red", children: "Connection Error" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: "red", children: ["\u2717 ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "Troubleshooting:" }), _jsx(Text, { color: "gray", children: " 1. Is Tilt running? Run: tilt up" }), _jsx(Text, { color: "gray", children: " 2. Check Tiltfile exists" }), _jsx(Text, { color: "gray", children: " 3. Try: tdk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to retry or [q] to quit" })] }));
15
- const EmptyState = () => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk init" }), _jsx(Text, { children: " 2. Or create services manually" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "Press [q] to quit" })] }));
14
+ const ErrorScreen = ({ error, onRetry }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "red", children: "Could Not Load Services" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: "red", children: ["\u2717 ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "Troubleshooting:" }), _jsx(Text, { color: "gray", children: " 1. Check that every service.json is valid JSON" }), _jsx(Text, { color: "gray", children: " 2. Run tdk from your project (the folder with the Tiltfile)" }), _jsx(Text, { color: "gray", children: " 3. Try: tdk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to retry or [q] to quit" })] }));
15
+ const EmptyState = () => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk init" }), _jsx(Text, { children: " 2. Or create services manually" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" })] }));
16
16
  const TUIApp = () => {
17
17
  const { exit } = useApp();
18
18
  const { stdout } = useStdout();
@@ -33,21 +33,27 @@ const TUIApp = () => {
33
33
  const [showTooltips, setShowTooltips] = useState(true);
34
34
  const [showEnabledOnly, setShowEnabledOnly] = useState(true);
35
35
  const projectRoot = findProjectRoot() || "unknown";
36
- const { stacks, services } = useMemo(() => ({
37
- stacks: discoverStacks(),
38
- services: discoverResources(),
39
- }), []);
40
- useEffect(() => {
41
- setLoading(false);
42
- }, []);
43
- useEffect(() => {
44
- if (services.length === 0) {
45
- setError('No services found. Run "tdk init" to get started.');
46
- }
47
- else {
36
+ const [{ stacks, services }, setDiscovered] = useState({ stacks: [], services: [] });
37
+ // Re-read service.json files from disk. An empty project is not an error:
38
+ // it renders EmptyState. Only a failed discovery shows ErrorScreen.
39
+ const refresh = useCallback(() => {
40
+ clearMetadataCache();
41
+ try {
42
+ setDiscovered({ stacks: discoverStacks(), services: discoverResources() });
48
43
  setError(null);
44
+ return true;
45
+ }
46
+ catch (err) {
47
+ setError(err instanceof Error ? err.message : String(err));
48
+ return false;
49
49
  }
50
- }, [services.length]);
50
+ finally {
51
+ setLoading(false);
52
+ }
53
+ }, []);
54
+ useEffect(() => {
55
+ refresh();
56
+ }, [refresh]);
51
57
  const selectedStackData = useMemo(() => {
52
58
  if (!selectedStack)
53
59
  return null;
@@ -241,8 +247,7 @@ const TUIApp = () => {
241
247
  useInput((input, key) => {
242
248
  if (error) {
243
249
  if (input === "r" || input === "R") {
244
- setError(null);
245
- setLoading(true);
250
+ refresh();
246
251
  return;
247
252
  }
248
253
  if (input === "q" || key.escape) {
@@ -300,9 +305,10 @@ const TUIApp = () => {
300
305
  return;
301
306
  }
302
307
  if (input === "r") {
303
- clearMetadataCache();
304
- setMessage("Data refreshed");
305
- setTimeout(() => setMessage(""), 1500);
308
+ if (refresh()) {
309
+ setMessage("Data refreshed");
310
+ setTimeout(() => setMessage(""), 1500);
311
+ }
306
312
  return;
307
313
  }
308
314
  if (input === "/") {
@@ -404,10 +410,7 @@ const TUIApp = () => {
404
410
  return _jsx(LoadingScreen, { progress: 100, message: "Initializing..." });
405
411
  }
406
412
  if (error) {
407
- return (_jsx(ErrorScreen, { error: error, onRetry: () => {
408
- setError(null);
409
- setLoading(true);
410
- } }));
413
+ return _jsx(ErrorScreen, { error: error, onRetry: refresh });
411
414
  }
412
415
  if (services.length === 0) {
413
416
  return _jsx(EmptyState, {});
@@ -0,0 +1,70 @@
1
+ export const reactFrontendProvider = {
2
+ id: "react",
3
+ label: "React + Vite",
4
+ dependencies: {
5
+ react: "^19.0.0",
6
+ "react-dom": "^19.0.0",
7
+ },
8
+ devDependencies: {
9
+ "@types/react": "^19.0.0",
10
+ "@types/react-dom": "^19.0.0",
11
+ "@vitejs/plugin-react": "^4.3.4",
12
+ },
13
+ compilerOptions: {
14
+ jsx: "react-jsx",
15
+ },
16
+ createFiles(name) {
17
+ return [
18
+ {
19
+ filename: "index.html",
20
+ content: `<!DOCTYPE html>
21
+ <html lang="en">
22
+ <head>
23
+ <meta charset="UTF-8" />
24
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
25
+ <title>${name}</title>
26
+ </head>
27
+ <body>
28
+ <div id="root"></div>
29
+ <script type="module" src="/src/main.tsx"></script>
30
+ </body>
31
+ </html>
32
+ `,
33
+ description: "Generating HTML template",
34
+ emoji: "💻",
35
+ },
36
+ {
37
+ filename: "src/main.tsx",
38
+ content: `import React from 'react';
39
+ import ReactDOM from 'react-dom/client';
40
+ import App from './App';
41
+
42
+ ReactDOM.createRoot(document.getElementById('root')!).render(
43
+ <React.StrictMode>
44
+ <App />
45
+ </React.StrictMode>,
46
+ );
47
+ `,
48
+ description: "Generating React entry",
49
+ emoji: "💻",
50
+ },
51
+ {
52
+ filename: "src/App.tsx",
53
+ content: `function App() {
54
+ return (
55
+ <div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
56
+ <h1>${name}</h1>
57
+ <p>Frontend resource created with TDK</p>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ export default App;
63
+ `,
64
+ description: "Generating React app",
65
+ emoji: "💻",
66
+ },
67
+ ];
68
+ },
69
+ };
70
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1,29 @@
1
+ import { TdkError } from "../utils/errors.js";
2
+ import { reactFrontendProvider } from "./react.js";
3
+ import { vueFrontendProvider } from "./vue.js";
4
+ export const DEFAULT_FRONTEND_FRAMEWORK = "react";
5
+ export const FRONTEND_FRAMEWORKS = {
6
+ react: reactFrontendProvider,
7
+ vue: vueFrontendProvider,
8
+ };
9
+ export function getFrontendFramework(frameworkId) {
10
+ const id = (frameworkId ?? DEFAULT_FRONTEND_FRAMEWORK).trim().toLowerCase();
11
+ const provider = Object.hasOwn(FRONTEND_FRAMEWORKS, id) ? FRONTEND_FRAMEWORKS[id] : undefined;
12
+ if (!provider) {
13
+ const supportedIds = Object.keys(FRONTEND_FRAMEWORKS).join(", ");
14
+ throw new TdkError(`Unknown frontend framework "${frameworkId}". Supported frameworks: ${supportedIds}.`, [`Use one of: ${supportedIds}`, `Omit --framework to use ${DEFAULT_FRONTEND_FRAMEWORK}`]);
15
+ }
16
+ return provider;
17
+ }
18
+ export function resolveFrontendFramework(resourceType, frameworkId) {
19
+ if (resourceType !== "frontend") {
20
+ if (frameworkId !== undefined) {
21
+ throw new TdkError("--framework can only be used with --type frontend.", [
22
+ "Add --type frontend, or drop --framework for this resource type",
23
+ ]);
24
+ }
25
+ return undefined;
26
+ }
27
+ return getFrontendFramework(frameworkId);
28
+ }
29
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map