@tdk-landscape/tdk-cli-core 1.3.124 → 1.3.125

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.
@@ -61,17 +61,23 @@ export const logsCommand = new Command("logs")
61
61
  fail("USAGE", "--port must be a valid port number", 2);
62
62
  const tail = Number(options.tail);
63
63
  const services = options.service ?? [];
64
- try {
65
- secrets = envSecretValues(findProjectRoot() ?? process.cwd());
66
- }
67
- catch (error) {
68
- if (!(error instanceof EnvUnreadableError))
69
- throw error;
70
- fail("ENV_UNREADABLE", error.message, 1, [
71
- "Fix the read permission on .env, or move it out of the project",
72
- ]);
73
- }
74
64
  const action = async () => {
65
+ const projectRoot = findProjectRoot();
66
+ if (!projectRoot) {
67
+ // Same code `writeMachineError` gives this error in `down`, `stacks` and `resources`.
68
+ const error = errorFactories.notInProject();
69
+ return fail("COMMAND_FAILED", error.message, error.exitCode, error.suggestions);
70
+ }
71
+ try {
72
+ secrets = envSecretValues(projectRoot);
73
+ }
74
+ catch (error) {
75
+ if (!(error instanceof EnvUnreadableError))
76
+ throw error;
77
+ fail("ENV_UNREADABLE", error.message, 1, [
78
+ "Fix the read permission on .env, or move it out of the project",
79
+ ]);
80
+ }
75
81
  if (!(await isTiltAvailable())) {
76
82
  fail("TILT_MISSING", "Tilt is not installed. Run: tdk doctor");
77
83
  }
@@ -12,6 +12,7 @@ import { createMachineEnvelope, writeMachineError } from "../utils/machine-outpu
12
12
  import { findProjectRoot } from "../utils/paths.js";
13
13
  import { checkPortStatus } from "../utils/port-assignment.js";
14
14
  import { isApiServiceType } from "../utils/resource-kind.js";
15
+ import { resolveServicePath } from "../utils/service-urls.js";
15
16
  import { stackExists } from "../utils/services.js";
16
17
  import { isValidPort, sanitizeForShell } from "../utils/validation.js";
17
18
  import { findOnPath } from "../utils/which.js";
@@ -186,21 +187,22 @@ export const networksCommand = new Command("networks")
186
187
  const apiDomain = `api.${bareDomain}`;
187
188
  const services = discovery.resources;
188
189
  const servicesWithUrls = await Promise.all(services
189
- .filter((s) => typeof s.config?.basePath === "string")
190
+ .filter((s) => s.config?.appType === "frontend" || isApiServiceType(s.config?.appType))
190
191
  .map(async (s) => {
191
- const basePath = s.config.basePath.replace(/^\//, "");
192
+ const resolvedPath = resolveServicePath(s);
193
+ const basePath = resolvedPath.startsWith("/") ? resolvedPath : `/${resolvedPath}`;
192
194
  const isBackend = isApiServiceType(s.config?.appType);
193
195
  const host = isBackend ? apiDomain : appDomain;
194
- const url = `http://${host}:${httpPort}/${basePath}`;
195
- const port = s.config.port;
196
+ const url = `http://${host}:${httpPort}${basePath}`;
197
+ const port = s.config?.port;
196
198
  const status = await checkServiceStatus(s.name, port, url);
197
199
  return {
198
200
  name: s.name,
199
201
  stack: s.stack,
200
- basePath: s.config.basePath,
202
+ basePath,
201
203
  url,
202
204
  ...(process.platform === "win32"
203
- ? { loopbackUrl: `http://127.0.0.1:${httpPort}/${basePath}`.replace(/\/$/, "") }
205
+ ? { loopbackUrl: `http://127.0.0.1:${httpPort}${basePath}`.replace(/\/$/, "") }
204
206
  : {}),
205
207
  port,
206
208
  status,
@@ -218,12 +220,10 @@ export const networksCommand = new Command("networks")
218
220
  }
219
221
  if (filteredServices.length === 0) {
220
222
  if (options.stack) {
221
- console.log(chalk.yellow(`⚠️ No services with basePath found in stack "${options.stack}"`));
223
+ console.log(chalk.yellow(`⚠️ No routable services found in stack "${options.stack}"`));
222
224
  }
223
225
  else {
224
- console.log(chalk.yellow("⚠️ No services with basePath found"));
225
- console.log(chalk.gray("\nAdd basePath to your service.json:"));
226
- console.log(chalk.gray(' "basePath": "/my-service"'));
226
+ console.log(chalk.yellow("⚠️ No routable services found"));
227
227
  }
228
228
  process.exit(0);
229
229
  }
@@ -7,6 +7,7 @@ import { getBackendIndexTemplate } from "../backend-languages/bun.js";
7
7
  import { resolveBackendLanguage } from "../backend-languages/registry.js";
8
8
  import { DEFAULT_FRONTEND_FRAMEWORK, listFrontendFrameworks, resolveFrontendFramework, } from "../frontend-frameworks/registry.js";
9
9
  import { hasDddLicense } from "../generator/extension-fetch.js";
10
+ import { getDockerfileTemplate, getTestTemplate, getWorkerIndexTemplate, } from "../generator/resource-templates.js";
10
11
  import { CREATABLE_RESOURCE_TYPES } from "../types/index.js";
11
12
  import { assertValid, confirmOrCancel } from "../utils/command-helpers.js";
12
13
  import { chooseResourcePath, isPathDiscovered, readDiscoveryPaths, } from "../utils/discovery-paths.js";
@@ -19,6 +20,7 @@ import { getDefaultFeaturesForResourceType } from "../utils/resource-features.js
19
20
  import { SERVICE_MANIFEST_SCHEMA_URL, SERVICE_MANIFEST_SCHEMA_VERSION, } from "../utils/service-manifest.js";
20
21
  import { discoverResources } from "../utils/services.js";
21
22
  import { createKebabCaseValidator, isPathSafe, validateResourceName, validateStackName, } from "../utils/validation.js";
23
+ export { getDockerfileTemplate, getShutdownHandlerTemplate, getTestTemplate, getWorkerIndexTemplate, } from "../generator/resource-templates.js";
22
24
  export const BASE_TEMPLATE = {
23
25
  port: 0, // Will be assigned
24
26
  // `dependsOn` is the current name; `dependencies` is deprecated and `tdk doctor` warns about it, so a fresh scaffold must not write it.
@@ -244,133 +246,7 @@ export const TSCONFIG_TEMPLATE = {
244
246
  include: ["src/**/*"],
245
247
  exclude: ["node_modules", "dist"],
246
248
  };
247
- /**
248
- * The plain `Dockerfile` written next to service.json. `tdk up` does not build it: for generated
249
- * resources the engine builds `.autogenerated/Dockerfile.app.autogenerated`. It is a starting point
250
- * for building the service image outside TDK, so its port follows the service's `port`.
251
- */
252
- export function getDockerfileTemplate(port, healthCheckPath) {
253
- // `oven/bun` has no curl, so the probe uses bun itself.
254
- const healthcheck = healthCheckPath
255
- ? `# Health check
256
- HEALTHCHECK --interval=10s --timeout=5s --retries=3 \\
257
- CMD bun -e "fetch('http://localhost:${port}${healthCheckPath}').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
258
-
259
- `
260
- : "";
261
- return `# Scaffolded by \`tdk resource\`. \`tdk up\` does NOT build this file: it builds
262
- # .autogenerated/Dockerfile.app.autogenerated (golden layers, project root as context).
263
- # Use this one as a starting point for building the image outside TDK, and keep its
264
- # port in step with "port" in service.json.
265
- FROM oven/bun:1.2
266
-
267
- WORKDIR /app
268
-
269
- # Copy package files (bun.lock is optional: a TDK workspace keeps it at the project root)
270
- COPY package.json bun.lock* ./
271
-
272
- # Install dependencies
273
- RUN bun install
274
-
275
- # Copy source
276
- COPY . .
277
-
278
- # Build if needed
279
- RUN bun run build
280
-
281
- ${healthcheck}ENV PORT=${port}
282
- EXPOSE ${port}
283
-
284
- CMD ["bun", "run", "start"]
285
- `;
286
- }
287
249
  export { getBackendIndexTemplate };
288
- function getShutdownHandlerTemplate(signal) {
289
- return `process.on('${signal}', () => {
290
- console.log('[Worker] ${signal} received, shutting down gracefully...');
291
- process.exit(0);
292
- });`;
293
- }
294
- export function getWorkerIndexTemplate(name) {
295
- return `console.log('🚀 ${name} worker started');
296
-
297
- interface Job {
298
- id: string;
299
- type: string;
300
- payload: Record<string, unknown>;
301
- priority?: number;
302
- timestamp?: string;
303
- }
304
-
305
- const CONFIG = {
306
- pollIntervalMs: parseInt(process.env.WORKER_POLL_INTERVAL || '5000'),
307
- maxRetries: parseInt(process.env.WORKER_MAX_RETRIES || '3'),
308
- batchSize: parseInt(process.env.WORKER_BATCH_SIZE || '10'),
309
- };
310
-
311
- async function processJob(job: Job): Promise<void> {
312
- console.log('[Worker] Processing job:', job.id, 'type:', job.type);
313
-
314
- // Add job processing logic here using job.payload
315
-
316
- await new Promise(resolve => setTimeout(resolve, 1000));
317
- console.log('[Worker] Job completed:', job.id);
318
- }
319
-
320
- async function fetchJobs(): Promise<Job[]> {
321
- // Connect to your queue (Redis, RabbitMQ, etc.) and fetch jobs
322
- return [];
323
- }
324
-
325
- async function main() {
326
- console.log('[Worker] Configuration:', CONFIG);
327
-
328
- while (true) {
329
- try {
330
- const jobs = await fetchJobs();
331
-
332
- if (jobs.length === 0) {
333
- await new Promise(resolve => setTimeout(resolve, CONFIG.pollIntervalMs));
334
- continue;
335
- }
336
-
337
- console.log('[Worker] Fetched \${jobs.length} jobs');
338
-
339
- for (const job of jobs) {
340
- try {
341
- await processJob(job);
342
- } catch (error: unknown) {
343
- console.error('[Worker] Job failed:', error);
344
- }
345
- }
346
- } catch (error: unknown) {
347
- console.error('[Worker] Error in main loop:', error);
348
- // Wait before retrying to avoid tight error loops
349
- await new Promise(resolve => setTimeout(resolve, CONFIG.pollIntervalMs));
350
- }
351
- }
352
- }
353
-
354
- ${getShutdownHandlerTemplate("SIGTERM")}
355
-
356
- ${getShutdownHandlerTemplate("SIGINT")}
357
-
358
- main().catch((err) => {
359
- console.error('[Worker] Fatal error:', err);
360
- process.exit(1);
361
- });
362
- `;
363
- }
364
- function getTestTemplate(name) {
365
- return `import { describe, it, expect } from 'vitest';
366
-
367
- describe('${name}', () => {
368
- it('should pass a basic test', () => {
369
- expect(true).toBe(true);
370
- });
371
- });
372
- `;
373
- }
374
250
  /** Resource types `tdk resource --type` accepts; `byo` is an alias of `bring-your-own`. */
375
251
  export function parseResourceType(type) {
376
252
  const normalized = type === "byo" ? "bring-your-own" : type;
@@ -557,6 +433,11 @@ export const resourceCommand = new Command("resource")
557
433
  // Check if resource already exists
558
434
  const isExistingResource = existsSync(fullPath);
559
435
  const hasServiceJson = existsSync(resolve(fullPath, "service.json"));
436
+ if (resourceType === "sdk" && !hasServiceJson) {
437
+ throw new TdkError(`No service.json in ${finalResourcePath}`, [
438
+ 'Add one with "appType": "sdk", then run this again to register it',
439
+ ]);
440
+ }
560
441
  const shouldRegisterExisting = options.registerExisting ||
561
442
  resourceType === "sdk" ||
562
443
  (isExistingResource && hasServiceJson);
@@ -14,13 +14,12 @@ import { createStatusMessageController } from "../utils/status-message.js";
14
14
  import { getListRowFromMouseY, getTerminalRuleWidth } from "../utils/terminal-layout.js";
15
15
  import { isTiltAvailable } from "../utils/tilt.js";
16
16
  import { loadTiltEvents, stripTerminalControls, TiltEventsLoadError, } from "../utils/tilt-events.js";
17
+ import { formatUiKeyHint, formatUiKeyNames, UI_KEYMAP } from "../utils/ui-keymap.js";
17
18
  import { applyServiceStates, fetchServiceStates, } from "../utils/ui-service-state.js";
18
19
  // biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
19
20
  const HelpPanel = ({ onClose }) => {
20
21
  const theme = useTUITheme();
21
- return (_jsxs(Box, { ...(theme.ascii ? {} : { borderStyle: "single", borderColor: theme.accent }), paddingX: 2, paddingY: 1, flexDirection: "column", width: 60, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Keyboard Shortcuts" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, underline: true, color: theme.foreground, children: "Navigation" }), _jsx(Text, { color: theme.foreground, children: theme.ascii
22
- ? " [UP/DOWN] or j/k Navigate list items"
23
- : " \u2191/\u2193 or j/k Navigate list items" }), _jsx(Text, { color: theme.foreground, children: " g/G or Home/End First/last item" }), _jsx(Text, { color: theme.foreground, children: " PgUp/PgDn Move one page" }), _jsx(Text, { color: theme.foreground, children: " Enter Select item / Open detail" }), _jsx(Text, { color: theme.foreground, children: " Tab Next tab" }), _jsxs(Text, { color: theme.foreground, children: [" ", TABS[0].shortcut, "-", TABS[TABS.length - 1].shortcut, " Direct tab access"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, color: theme.foreground, children: "Actions" }) }), _jsx(Text, { color: theme.foreground, children: " m Toggle mouse support" }), _jsx(Text, { color: theme.foreground, children: " t Toggle tooltips" }), _jsx(Text, { color: theme.foreground, children: " e Toggle enabled/disabled services" }), _jsx(Text, { color: theme.foreground, children: " r Refresh data" }), _jsx(Text, { color: theme.foreground, children: " / Search/filter" }), _jsx(Text, { color: theme.foreground, children: " ? Show this help" }), _jsx(Text, { color: theme.foreground, children: theme.ascii ? " q Quit | Esc Back" : " q Quit \u2502 Esc Back" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: "Press any key to close..." }) })] }));
22
+ return (_jsxs(Box, { ...(theme.ascii ? {} : { borderStyle: "single", borderColor: theme.accent }), paddingX: 2, paddingY: 1, flexDirection: "column", width: 60, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Keyboard Shortcuts" }), _jsx(Box, { marginY: 1, flexDirection: "column", children: ["Navigation", "Actions"].map((group) => (_jsxs(Box, { marginTop: group === "Actions" ? 1 : 0, flexDirection: "column", children: [_jsx(Text, { bold: true, underline: true, color: theme.foreground, children: group }), UI_KEYMAP.filter((shortcut) => shortcut.group === group).map((shortcut) => (_jsx(Text, { color: theme.foreground, children: ` ${formatUiKeyNames(shortcut.id, theme.ascii)} ${shortcut.label}` }, shortcut.id)))] }, group))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: "Press any key to close..." }) })] }));
24
23
  };
25
24
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
26
25
  const ASCII_SPINNER_FRAMES = ["-", "/", "|", "\\"];
@@ -392,6 +391,7 @@ export const TUIApp = ({ animated = true }) => {
392
391
  stdout.off("resize", handleResize);
393
392
  };
394
393
  }, [stdout, handleResize]);
394
+ // Keep this dispatcher in sync with UI_KEYMAP; the shared map drives the help panel and footer.
395
395
  useInput((input, key) => {
396
396
  if (error) {
397
397
  if (input === "r" || input === "R") {
@@ -581,33 +581,54 @@ export const TUIApp = ({ animated = true }) => {
581
581
  };
582
582
  /** Compose footer guidance for the current tab and terminal character set. */
583
583
  function createHelpHint(activeTab, selectedStack, selectedService, showEnabledOnly, ascii) {
584
- const separator = ascii ? " | " : " \u2502 ";
585
- const enabledHint = `[e] ${showEnabledOnly ? "show all" : "enabled only"}`;
586
- const common = [enabledHint, "[?] help"];
587
- const tabRange = `${TABS[0].shortcut}-${TABS[TABS.length - 1].shortcut}`;
584
+ const separator = ascii ? " | " : " │ ";
585
+ const enabledHint = formatUiKeyHint("toggle-enabled", showEnabledOnly ? "show all" : "enabled only", ascii);
586
+ const common = [enabledHint, formatUiKeyHint("help", "help", ascii)];
587
+ const cycleTabs = formatUiKeyHint("cycle-tabs", "Tabs", ascii);
588
588
  if (activeTab === "overview") {
589
589
  const introduction = selectedStack
590
- ? `Stack "${selectedStack}" selected. [Enter] view`
591
- : `${ascii ? "[UP/DOWN]" : "[\u2191/\u2193]"} Navigate`;
592
- const controls = selectedStack ? ["[Esc] back", ...common] : ["[Enter] Select", ...common];
590
+ ? 'Stack "' + selectedStack + '" selected. ' + formatUiKeyHint("select", "view", ascii)
591
+ : formatUiKeyHint("list-navigation", "Navigate", ascii);
592
+ const controls = selectedStack
593
+ ? [formatUiKeyHint("back", "back", ascii), ...common]
594
+ : [formatUiKeyHint("select", "Select", ascii), ...common];
593
595
  return [introduction, ...controls].join(separator);
594
596
  }
595
597
  if (activeTab === "resources") {
596
- return ["[Tab] Tabs", "[r] Refresh", "[/] Search", ...common].join(separator);
598
+ return [
599
+ cycleTabs,
600
+ formatUiKeyHint("refresh", "Refresh", ascii),
601
+ formatUiKeyHint("search", "Search", ascii),
602
+ ...common,
603
+ ].join(separator);
597
604
  }
598
605
  if (activeTab === "files" && selectedService) {
599
- return [`Service "${selectedService}"`, "[Esc] Back", ...common].join(separator);
606
+ return [
607
+ 'Service "' + selectedService + '"',
608
+ formatUiKeyHint("back", "Back", ascii),
609
+ ...common,
610
+ ].join(separator);
600
611
  }
601
612
  if (activeTab === "files") {
602
613
  return ["Select service to view files", ...common].join(separator);
603
614
  }
604
615
  if (activeTab === "events") {
605
- return ["[r] Refresh events", "[Tab] Next", "[?] help", "[q] Quit"].join(separator);
616
+ return [
617
+ formatUiKeyHint("refresh", "Refresh events", ascii),
618
+ formatUiKeyHint("cycle-tabs", "Tabs", ascii),
619
+ formatUiKeyHint("help", "help", ascii),
620
+ formatUiKeyHint("quit", "Quit", ascii),
621
+ ].join(separator);
606
622
  }
607
623
  if (activeTab === "config") {
608
624
  return ["View configurations", ...common].join(separator);
609
625
  }
610
- return ["[Tab] Next", `[${tabRange}] Tabs`, ...common, "[q] Quit"].join(separator);
626
+ return [
627
+ formatUiKeyHint("cycle-tabs", "Tabs", ascii),
628
+ formatUiKeyHint("direct-tabs", "Tabs", ascii),
629
+ ...common,
630
+ formatUiKeyHint("quit", "Quit", ascii),
631
+ ].join(separator);
611
632
  }
612
633
  const TUIRoot = ({ animated, highContrast, }) => (_jsx(TUIThemeContext.Provider, { value: createTUITheme(Boolean(highContrast)), children: _jsx(TUIApp, { animated: animated }) }));
613
634
  export const uiCommand = new Command("ui")
@@ -292,45 +292,49 @@ async function getLatestVersion() {
292
292
  spinner.succeed(`Latest version: ${chalk.green(result)}`);
293
293
  return result;
294
294
  }
295
- catch (_err) {
296
- // npm registry failed - package not published yet
297
- spinner.warn("Package not yet published to npm registry");
298
- console.log(chalk.yellow("\n💡 For now, please upgrade manually from GitHub:"));
299
- console.log(chalk.cyan(" npm install -g github:tdk-landscape/tdk-cli-core"));
300
- console.log(chalk.cyan(" bun install -g github:tdk-landscape/tdk-cli-core"));
301
- console.log(chalk.gray("\n (npm package will be available soon)"));
295
+ catch (err) {
296
+ logVerbose("npm registry check failed", err);
297
+ spinner.warn("Could not check the npm registry");
298
+ console.log(chalk.yellow("\nInstall the latest published release manually:"));
299
+ console.log(chalk.cyan(" npm install -g @tdk-landscape/tdk-cli-core@latest"));
300
+ console.log(chalk.cyan(" bun install -g @tdk-landscape/tdk-cli-core@latest"));
302
301
  return null;
303
302
  }
304
303
  }
305
- async function upgradeViaNpm() {
304
+ export async function upgradeViaNpm() {
306
305
  const spinner = startSpinner("Upgrading via npm...");
307
306
  try {
308
307
  execSync("npm install -g @tdk-landscape/tdk-cli-core@latest", {
309
- stdio: "inherit",
308
+ stdio: ["inherit", "inherit", "pipe"],
310
309
  timeout: 120000,
311
310
  });
312
311
  spinner.succeed("Upgraded successfully via npm");
313
312
  return true;
314
313
  }
315
314
  catch (err) {
316
- // npm registry failed (package may not exist or network issue) - try GitHub fallback
317
- spinner.text = "npm registry failed, trying GitHub...";
318
- logVerbose("npm registry error", err);
319
- try {
320
- execSync("npm install -g github:tdk-landscape/tdk-cli-core", {
321
- stdio: "inherit",
322
- timeout: 120000,
323
- });
324
- spinner.succeed("Upgraded successfully via GitHub");
325
- return true;
326
- }
327
- catch (err) {
328
- spinner.fail(`Upgrade failed: ${getErrorMessage(err)}`);
329
- return false;
315
+ const errorStderr = err && typeof err === "object" && "stderr" in err
316
+ ? String(err.stderr ?? "")
317
+ : "";
318
+ if (errorStderr)
319
+ process.stderr.write(errorStderr);
320
+ logVerbose("npm upgrade error", err);
321
+ spinner.fail(`npm upgrade failed: ${getErrorMessage(err)}`);
322
+ console.log(chalk.yellow("\nTo retry manually, install the latest published npm release:"));
323
+ console.log(chalk.cyan(" npm install -g @tdk-landscape/tdk-cli-core@latest"));
324
+ const errorCode = err && typeof err === "object" && "code" in err
325
+ ? String(err.code ?? "")
326
+ : "";
327
+ if (/EACCES|EPERM|permission denied/i.test(`${errorCode} ${getErrorMessage(err)} ${errorStderr}`)) {
328
+ console.log(chalk.gray("\nFor EACCES or EPERM, fix npm's global prefix or use the binary installer:"));
329
+ console.log(chalk.cyan(" https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally/"));
330
+ console.log(chalk.cyan(isWindows()
331
+ ? " irm https://tdk-landscape.github.io/install.ps1 | iex"
332
+ : " curl -fsSL https://tdk-landscape.github.io/install.sh | sh"));
330
333
  }
334
+ return false;
331
335
  }
332
336
  }
333
- async function upgradeViaBun() {
337
+ export async function upgradeViaBun() {
334
338
  const spinner = startSpinner("Upgrading via bun...");
335
339
  try {
336
340
  execFileSync(findOnPath("bun") ?? "bun", ["install", "-g", "@tdk-landscape/tdk-cli-core@latest"], {
@@ -341,21 +345,11 @@ async function upgradeViaBun() {
341
345
  return true;
342
346
  }
343
347
  catch (err) {
344
- // bun registry failed (package may not exist or network issue) - try GitHub fallback
345
- spinner.text = "bun registry failed, trying GitHub...";
346
- logVerbose("bun registry error", err);
347
- try {
348
- execFileSync(findOnPath("bun") ?? "bun", ["install", "-g", "github:tdk-landscape/tdk-cli-core"], {
349
- stdio: "inherit",
350
- timeout: 120000,
351
- });
352
- spinner.succeed("Upgraded successfully via GitHub");
353
- return true;
354
- }
355
- catch (err) {
356
- spinner.fail(`Upgrade failed: ${getErrorMessage(err)}`);
357
- return false;
358
- }
348
+ logVerbose("bun upgrade error", err);
349
+ spinner.fail(`Bun upgrade failed: ${getErrorMessage(err)}`);
350
+ console.log(chalk.yellow("\nTo retry manually, install the latest published Bun release:"));
351
+ console.log(chalk.cyan(" bun install -g @tdk-landscape/tdk-cli-core@latest"));
352
+ return false;
359
353
  }
360
354
  }
361
355
  async function upgradeViaGit(path) {
@@ -427,10 +421,10 @@ export const upgradeCommand = new Command("upgrade")
427
421
  return;
428
422
  }
429
423
  if (installInfo.method === "unknown") {
430
- console.error(chalk.red("❌ Could not detect installation method"));
431
- console.log(chalk.yellow("\n💡 Manual upgrade (package not on npm yet, use GitHub):"));
432
- console.log(chalk.cyan(" npm: npm install -g github:tdk-landscape/tdk-cli-core"));
433
- console.log(chalk.cyan(" bun: bun install -g github:tdk-landscape/tdk-cli-core"));
424
+ console.error(chalk.red("Could not detect installation method"));
425
+ console.log(chalk.yellow("\nManual upgrade using a published package:"));
426
+ console.log(chalk.cyan(" npm install -g @tdk-landscape/tdk-cli-core@latest"));
427
+ console.log(chalk.cyan(" bun install -g @tdk-landscape/tdk-cli-core@latest"));
434
428
  console.log(chalk.cyan(" git: cd /path/to/tdk-cli && git pull && bun link --force"));
435
429
  process.exit(1);
436
430
  }
@@ -566,15 +560,8 @@ export const upgradeCommand = new Command("upgrade")
566
560
  break;
567
561
  }
568
562
  if (!success) {
569
- console.error(chalk.red("\n❌ Upgrade failed"));
570
- console.log(chalk.yellow("\n💡 Try manual upgrade (use GitHub until npm package is published):"));
571
- if (installInfo.method === "npm") {
572
- console.log(chalk.cyan(" npm install -g github:tdk-landscape/tdk-cli-core"));
573
- }
574
- else if (installInfo.method === "bun") {
575
- console.log(chalk.cyan(" bun install -g github:tdk-landscape/tdk-cli-core"));
576
- }
577
- else if (installInfo.method === "git") {
563
+ console.error(chalk.red("\nUpgrade failed"));
564
+ if (installInfo.method === "git") {
578
565
  console.log(chalk.cyan(` cd ${installInfo.path} && git pull && bun link --force`));
579
566
  }
580
567
  else if (installInfo.method === "binary" && binaryRelease && installInfo.path) {
@@ -1,14 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { getTabBarDensity, getTerminalRuleWidth } from "../utils/terminal-layout.js";
4
+ import { TABS } from "../utils/ui-keymap.js";
4
5
  import { useTUITheme } from "./ui-theme.js";
5
- export const TABS = [
6
- { id: "overview", label: "OVERVIEW", shortcut: "1" },
7
- { id: "resources", label: "RESOURCES", shortcut: "2" },
8
- { id: "events", label: "EVENTS", shortcut: "3" },
9
- { id: "files", label: "FILES", shortcut: "4" },
10
- { id: "config", label: "CONFIG", shortcut: "5" },
11
- ];
6
+ export { TABS };
12
7
  export const TabBar = ({ activeTab, compact = false, terminalWidth }) => {
13
8
  const theme = useTUITheme();
14
9
  const responsive = terminalWidth !== undefined;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The plain `Dockerfile` written next to service.json. `tdk up` does not build it: for generated
3
+ * resources the engine builds `.autogenerated/Dockerfile.app.autogenerated`. It is a starting point
4
+ * for building the service image outside TDK, so its port follows the service's `port`.
5
+ */
6
+ export function getDockerfileTemplate(port, healthCheckPath) {
7
+ // `oven/bun` has no curl, so the probe uses bun itself.
8
+ const healthcheck = healthCheckPath
9
+ ? `# Health check
10
+ HEALTHCHECK --interval=10s --timeout=5s --retries=3 \\
11
+ CMD bun -e "fetch('http://localhost:${port}${healthCheckPath}').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
12
+
13
+ `
14
+ : "";
15
+ return `# Scaffolded by \`tdk resource\`. \`tdk up\` does NOT build this file: it builds
16
+ # .autogenerated/Dockerfile.app.autogenerated (golden layers, project root as context).
17
+ # Use this one as a starting point for building the image outside TDK, and keep its
18
+ # port in step with "port" in service.json.
19
+ FROM oven/bun:1.2
20
+
21
+ WORKDIR /app
22
+
23
+ # Copy package files (bun.lock is optional: a TDK workspace keeps it at the project root)
24
+ COPY package.json bun.lock* ./
25
+
26
+ # Install dependencies
27
+ RUN bun install
28
+
29
+ # Copy source
30
+ COPY . .
31
+
32
+ # Build if needed
33
+ RUN bun run build
34
+
35
+ ${healthcheck}ENV PORT=${port}
36
+ EXPOSE ${port}
37
+
38
+ CMD ["bun", "run", "start"]
39
+ `;
40
+ }
41
+ export function getShutdownHandlerTemplate(signal) {
42
+ return `process.on('${signal}', () => {
43
+ console.log('[Worker] ${signal} received, shutting down gracefully...');
44
+ process.exit(0);
45
+ });`;
46
+ }
47
+ export function getWorkerIndexTemplate(name) {
48
+ return `console.log('🚀 ${name} worker started');
49
+
50
+ interface Job {
51
+ id: string;
52
+ type: string;
53
+ payload: Record<string, unknown>;
54
+ priority?: number;
55
+ timestamp?: string;
56
+ }
57
+
58
+ const CONFIG = {
59
+ pollIntervalMs: parseInt(process.env.WORKER_POLL_INTERVAL || '5000'),
60
+ maxRetries: parseInt(process.env.WORKER_MAX_RETRIES || '3'),
61
+ batchSize: parseInt(process.env.WORKER_BATCH_SIZE || '10'),
62
+ };
63
+
64
+ async function processJob(job: Job): Promise<void> {
65
+ console.log('[Worker] Processing job:', job.id, 'type:', job.type);
66
+
67
+ // Add job processing logic here using job.payload
68
+
69
+ await new Promise(resolve => setTimeout(resolve, 1000));
70
+ console.log('[Worker] Job completed:', job.id);
71
+ }
72
+
73
+ async function fetchJobs(): Promise<Job[]> {
74
+ // Connect to your queue (Redis, RabbitMQ, etc.) and fetch jobs
75
+ return [];
76
+ }
77
+
78
+ async function main() {
79
+ console.log('[Worker] Configuration:', CONFIG);
80
+
81
+ while (true) {
82
+ try {
83
+ const jobs = await fetchJobs();
84
+
85
+ if (jobs.length === 0) {
86
+ await new Promise(resolve => setTimeout(resolve, CONFIG.pollIntervalMs));
87
+ continue;
88
+ }
89
+
90
+ console.log('[Worker] Fetched \${jobs.length} jobs');
91
+
92
+ for (const job of jobs) {
93
+ try {
94
+ await processJob(job);
95
+ } catch (error: unknown) {
96
+ console.error('[Worker] Job failed:', error);
97
+ }
98
+ }
99
+ } catch (error: unknown) {
100
+ console.error('[Worker] Error in main loop:', error);
101
+ // Wait before retrying to avoid tight error loops
102
+ await new Promise(resolve => setTimeout(resolve, CONFIG.pollIntervalMs));
103
+ }
104
+ }
105
+ }
106
+
107
+ ${getShutdownHandlerTemplate("SIGTERM")}
108
+
109
+ ${getShutdownHandlerTemplate("SIGINT")}
110
+
111
+ main().catch((err) => {
112
+ console.error('[Worker] Fatal error:', err);
113
+ process.exit(1);
114
+ });
115
+ `;
116
+ }
117
+ export function getTestTemplate(name) {
118
+ return `import { describe, it, expect } from 'vitest';
119
+
120
+ describe('${name}', () => {
121
+ it('should pass a basic test', () => {
122
+ expect(true).toBe(true);
123
+ });
124
+ });
125
+ `;
126
+ }
127
+ //# sourceMappingURL=resource-templates.js.map