@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.
- package/CHANGELOG.md +284 -18
- package/README.md +19 -1
- package/cli/dist/commands/doctor.js +3 -127
- package/cli/dist/commands/down.js +8 -1
- package/cli/dist/commands/eject.js +2 -2
- package/cli/dist/commands/logs.js +16 -10
- package/cli/dist/commands/networks.js +10 -10
- package/cli/dist/commands/resource.js +7 -126
- package/cli/dist/commands/ui.js +35 -14
- package/cli/dist/commands/upgrade.js +39 -52
- package/cli/dist/components/TabBar.js +2 -7
- package/cli/dist/generator/resource-templates.js +127 -0
- package/cli/dist/utils/doctor-starlark.js +131 -0
- package/cli/dist/utils/env-validator.js +61 -19
- package/cli/dist/utils/service-manifest.js +11 -0
- package/cli/dist/utils/services.js +5 -1
- package/cli/dist/utils/tilt.js +2 -1
- package/cli/dist/utils/ui-keymap.js +118 -0
- package/cli/package.json +1 -1
- package/engine/topologies/tilt/common/utils.star +3 -3
- package/engine/topologies/tilt/common/utils_debug.star +1 -1
- package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +4 -6
- package/engine/topologies/tilt/resources/infra-loader.star +12 -11
- package/engine/topologies/tilt/resources/orchestrator/apply.star +2 -1
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +19 -18
- package/package.json +1 -1
|
@@ -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) =>
|
|
190
|
+
.filter((s) => s.config?.appType === "frontend" || isApiServiceType(s.config?.appType))
|
|
190
191
|
.map(async (s) => {
|
|
191
|
-
const
|
|
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}
|
|
195
|
-
const port = s.config
|
|
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
|
|
202
|
+
basePath,
|
|
201
203
|
url,
|
|
202
204
|
...(process.platform === "win32"
|
|
203
|
-
? { loopbackUrl: `http://127.0.0.1:${httpPort}
|
|
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
|
|
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
|
|
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);
|
package/cli/dist/commands/ui.js
CHANGED
|
@@ -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" }),
|
|
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 ? " | " : "
|
|
585
|
-
const enabledHint =
|
|
586
|
-
const common = [enabledHint, "
|
|
587
|
-
const
|
|
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
|
-
?
|
|
591
|
-
:
|
|
592
|
-
const controls = selectedStack
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 (
|
|
296
|
-
|
|
297
|
-
spinner.warn("
|
|
298
|
-
console.log(chalk.yellow("\
|
|
299
|
-
console.log(chalk.cyan(" npm install -g
|
|
300
|
-
console.log(chalk.cyan(" bun install -g
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
345
|
-
spinner.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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("
|
|
431
|
-
console.log(chalk.yellow("\
|
|
432
|
-
console.log(chalk.cyan(" npm
|
|
433
|
-
console.log(chalk.cyan(" bun
|
|
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("\
|
|
570
|
-
|
|
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
|
|
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
|