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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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.
@@ -1,10 +1,11 @@
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";
9
10
  import { validateEnvFile } from "../utils/env-validator.js";
10
11
  import { formatCount } from "../utils/formatting.js";
@@ -218,6 +219,31 @@ function checkMasterConfigs() {
218
219
  fix: "Run: tdk project",
219
220
  };
220
221
  }
222
+ /**
223
+ * Tilt only builds resources under `discovery.paths`, but the CLI finds every service.json,
224
+ * so a resource outside them is listed and given a URL by `tdk up` yet never started.
225
+ */
226
+ export function checkResourceDiscovery() {
227
+ const projectRoot = findProjectRoot() ?? process.cwd();
228
+ const patterns = readDiscoveryPaths(projectRoot);
229
+ const stranded = discoverResourcesFromRoot(projectRoot).filter((resource) => !isPathDiscovered(projectRoot, resource.path, patterns));
230
+ if (stranded.length === 0) {
231
+ return {
232
+ name: "Resource Discovery",
233
+ didPass: true,
234
+ message: `All resources are covered by discovery.paths ${JSON.stringify(patterns)}`,
235
+ };
236
+ }
237
+ const list = stranded
238
+ .map((resource) => normalize(relative(projectRoot, resource.path)))
239
+ .join(", ");
240
+ return {
241
+ name: "Resource Discovery",
242
+ didPass: false,
243
+ message: `${formatCount(stranded.length, "resource")} outside discovery.paths ${JSON.stringify(patterns)}: ${list}. \`tdk up\` will not start them`,
244
+ fix: "Move each under services/<stack>/<name>, or add a matching pattern to discovery.paths in .tdk/project.json and run: tdk config regenerate",
245
+ };
246
+ }
221
247
  // The scripts `tdk project` copies into every project. Generated Dockerfiles
222
248
  // may reference others; those are checked by scanning the Dockerfiles below.
223
249
  const REQUIRED_DOCKER_TEMPLATE_FILES = [
@@ -491,6 +517,26 @@ const KNOWN_TYPE_PACKAGE_VERSIONS = {
491
517
  "@types/bun": "^1.4.2",
492
518
  "@types/node": "^20.0.0",
493
519
  };
520
+ /**
521
+ * `@types/bun` (via its underlying `bun-types` package) ships a hardcoded
522
+ * `/// <reference types="node" />` in its own index.d.ts. TypeScript always
523
+ * honors an explicit triple-slash reference, regardless of what the
524
+ * tsconfig's own `"types"` array restricts to -- so a Docker tsconfig with
525
+ * `"types": ["bun"]` still requires `@types/node` to resolve, even though
526
+ * "node" never appears in that array.
527
+ *
528
+ * When `@types/node` isn't a direct dependency, it can only resolve as a
529
+ * *transitive* dependency of `bun-types`, hoisted wherever Bun's installer
530
+ * happens to place it. That is not guaranteed in the isolated, per-service
531
+ * `bun install` that Docker's build layers run (unlike a full workspace-root
532
+ * install), so the same tsconfig can build locally and fail in Docker with
533
+ * `TS2688: Cannot find type definition file for 'node'`. Declaring
534
+ * `@types/node` directly makes the install deterministic either way.
535
+ */
536
+ function needsNodeTypesForBun(installed, types) {
537
+ const usesBunTypes = types.includes("bun") || installed.has("@types/bun");
538
+ return usesBunTypes && !installed.has("@types/node");
539
+ }
494
540
  export function checkTypeScriptTypeDependencies() {
495
541
  const projectRoot = findProjectRoot() ?? process.cwd();
496
542
  const resources = discoverResourcesFromRoot(projectRoot);
@@ -527,6 +573,10 @@ export function checkTypeScriptTypeDependencies() {
527
573
  const described = missing.map((m) => `"${m}" (needs ${neededTypePackage(m)})`).join(", ");
528
574
  problems.push(`${resource.name}: "types" lists ${described}`);
529
575
  }
576
+ if (needsNodeTypesForBun(installed, types)) {
577
+ missingPackages.add("@types/node");
578
+ problems.push(`${resource.name}: uses @types/bun, which references node types internally, but @types/node is not a direct dependency`);
579
+ }
530
580
  }
531
581
  if (problems.length === 0) {
532
582
  return {
@@ -542,7 +592,7 @@ export function checkTypeScriptTypeDependencies() {
542
592
  return {
543
593
  name: "TypeScript Type Dependencies",
544
594
  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 ")}`,
595
+ 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
596
  fix: `Add to each service's package.json devDependencies: ${lines.join(", ")}`,
547
597
  };
548
598
  }
@@ -573,6 +623,11 @@ export function checkFrontendDockerPreflight() {
573
623
  }
574
624
  const problems = [];
575
625
  for (const resource of resources) {
626
+ // The frontend Docker builder runs `COPY <resource>/public ./public`, so a missing
627
+ // folder fails the image build with "/public: not found".
628
+ if (!existsSync(join(resource.path, "public"))) {
629
+ 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")))}`);
630
+ }
576
631
  const dockerfilePath = join(resource.path, ".autogenerated", "Dockerfile.app.autogenerated");
577
632
  const dockerTsconfigPath = join(resource.path, ".autogenerated", "tsconfig.docker.autogenerated.json");
578
633
  if (existsSync(dockerfilePath)) {
@@ -750,6 +805,7 @@ export const doctorCommand = new Command("doctor")
750
805
  () => checkHealthRoutes(),
751
806
  checkTypeScriptTypeDependencies,
752
807
  checkFrontendDockerPreflight,
808
+ checkResourceDiscovery,
753
809
  checkEnvironmentVariables,
754
810
  // Preflight: catch "port 80 already allocated" BEFORE claiming ready.
755
811
  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
@@ -0,0 +1,73 @@
1
+ export const vueFrontendProvider = {
2
+ id: "vue",
3
+ label: "Vue + Vite",
4
+ dependencies: {
5
+ vue: "^3.5.0",
6
+ },
7
+ devDependencies: {
8
+ "@vitejs/plugin-vue": "^5.2.4",
9
+ },
10
+ compilerOptions: {},
11
+ createFiles(name) {
12
+ return [
13
+ {
14
+ filename: "index.html",
15
+ content: `<!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
+ <title>${name}</title>
21
+ </head>
22
+ <body>
23
+ <div id="app"></div>
24
+ <script type="module" src="/src/main.ts"></script>
25
+ </body>
26
+ </html>
27
+ `,
28
+ description: "Generating HTML template",
29
+ emoji: "💻",
30
+ },
31
+ {
32
+ filename: "src/main.ts",
33
+ content: `import { createApp } from 'vue';
34
+ import App from './App.vue';
35
+
36
+ createApp(App).mount('#app');
37
+ `,
38
+ description: "Generating Vue entry",
39
+ emoji: "💻",
40
+ },
41
+ {
42
+ filename: "src/App.vue",
43
+ content: `<script setup lang="ts">
44
+ const message = 'Frontend resource created with TDK';
45
+ </script>
46
+
47
+ <template>
48
+ <main style="padding: 2rem; font-family: system-ui">
49
+ <h1>${name}</h1>
50
+ <p>{{ message }}</p>
51
+ </main>
52
+ </template>
53
+ `,
54
+ description: "Generating Vue app",
55
+ emoji: "💻",
56
+ },
57
+ {
58
+ filename: "src/vite-env.d.ts",
59
+ content: `/// <reference types="vite/client" />
60
+
61
+ declare module '*.vue' {
62
+ import type { Component } from 'vue';
63
+ const component: Component;
64
+ export default component;
65
+ }
66
+ `,
67
+ description: "Generating Vue module types",
68
+ emoji: "⚙️",
69
+ },
70
+ ];
71
+ },
72
+ };
73
+ //# sourceMappingURL=vue.js.map