@tdk-landscape/tdk-cli-core 1.3.116 → 1.3.118
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 +5 -0
- package/cli/dist/commands/config.js +43 -5
- package/cli/dist/commands/doctor.js +4 -1
- package/cli/dist/commands/resource.js +103 -3
- package/cli/dist/commands/ui.js +108 -44
- package/cli/dist/components/BaseTooltip.js +3 -2
- package/cli/dist/components/DetailPanel.js +19 -5
- package/cli/dist/components/FileTree.js +20 -17
- package/cli/dist/components/ResourceSelectInput.js +16 -11
- package/cli/dist/components/ResourceTable.js +12 -5
- package/cli/dist/components/TUIHeader.js +25 -0
- package/cli/dist/components/TabBar.js +12 -7
- package/cli/dist/components/index.js +1 -0
- package/cli/dist/components/ui-theme.js +114 -0
- package/cli/dist/generator/template-engine.js +2 -1
- package/cli/dist/utils/doctor-wiring.js +192 -0
- package/cli/dist/utils/project-config-defaults.js +15 -0
- package/cli/dist/utils/shared-platform-postgres.js +211 -0
- package/cli/package.json +1 -1
- package/cli/templates/Tiltfile.hbs +5 -1
- package/engine/topologies/tilt/common/utils.star +2 -0
- package/engine/topologies/tilt/generators/validators.star +6 -1
- package/engine/topologies/tilt/manifest/integration.star +11 -4
- package/engine/topologies/tilt/manifest/validator.star +4 -0
- package/engine/topologies/tilt/resources/infra-loader.star +68 -13
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +135 -8
- package/engine/topologies/tilt/resources/shared-platform-postgres.star +28 -0
- package/package.json +1 -1
|
@@ -2,24 +2,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { formatBytes } from "../utils/formatting.js";
|
|
5
|
+
import { useTUITheme } from "./ui-theme.js";
|
|
5
6
|
const FILE_ICONS = {
|
|
6
|
-
docker: "
|
|
7
|
-
tilt: "
|
|
8
|
-
config: "
|
|
9
|
-
prisma: "
|
|
10
|
-
generated: "
|
|
11
|
-
unknown: "
|
|
7
|
+
docker: "\uD83D\uDC33",
|
|
8
|
+
tilt: "\u2699\uFE0F",
|
|
9
|
+
config: "\uD83D\uDD27",
|
|
10
|
+
prisma: "\uD83D\uDDC4\uFE0F",
|
|
11
|
+
generated: "\uD83D\uDCC4",
|
|
12
|
+
unknown: "\uD83D\uDCC3",
|
|
12
13
|
};
|
|
13
|
-
const
|
|
14
|
-
docker: "
|
|
15
|
-
tilt: "
|
|
16
|
-
config: "
|
|
17
|
-
prisma: "
|
|
18
|
-
generated: "
|
|
19
|
-
unknown: "
|
|
14
|
+
const ASCII_FILE_ICONS = {
|
|
15
|
+
docker: "[D]",
|
|
16
|
+
tilt: "[T]",
|
|
17
|
+
config: "[C]",
|
|
18
|
+
prisma: "[P]",
|
|
19
|
+
generated: "[G]",
|
|
20
|
+
unknown: "[?]",
|
|
20
21
|
};
|
|
21
22
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
22
23
|
export const FileTree = ({ nodes, onSelect, selectedPath }) => {
|
|
24
|
+
const theme = useTUITheme();
|
|
23
25
|
const [expandedNodes, setExpandedNodes] = useState(new Set());
|
|
24
26
|
const _toggleNode = (path) => {
|
|
25
27
|
const newExpanded = new Set(expandedNodes);
|
|
@@ -36,11 +38,12 @@ export const FileTree = ({ nodes, onSelect, selectedPath }) => {
|
|
|
36
38
|
const isSelected = selectedPath === node.path;
|
|
37
39
|
const indent = " ".repeat(depth);
|
|
38
40
|
if (node.type === "directory") {
|
|
39
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: isSelected ?
|
|
41
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.selectionForeground : theme.foreground, bold: isSelected, backgroundColor: isSelected ? theme.selectionBackground : undefined, children: [indent, theme.ascii ? (isExpanded ? "[-]" : "[+]") : isExpanded ? "\u25BC" : "\u25B6", " ", node.name] }) }), isExpanded && node.children?.map((child) => renderNode(child, depth + 1))] }, node.path));
|
|
40
42
|
}
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
43
|
+
const fileType = node.fileType ?? "unknown";
|
|
44
|
+
const icon = theme.ascii ? ASCII_FILE_ICONS[fileType] : FILE_ICONS[fileType];
|
|
45
|
+
const color = theme.fileColors[fileType];
|
|
46
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? theme.selectionForeground : color, bold: isSelected, backgroundColor: isSelected ? theme.selectionBackground : undefined, children: [indent, " ", icon, " ", node.name] }), node.size && (_jsxs(Text, { color: theme.muted, dimColor: theme.dimMuted, children: [" ", "(", formatBytes(node.size), ")"] }))] }, node.path));
|
|
44
47
|
};
|
|
45
48
|
return (_jsx(Box, { flexDirection: "column", paddingX: 1, children: nodes.map((node) => renderNode(node, 0)) }));
|
|
46
49
|
};
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, measureElement, Text, useInput } from "ink";
|
|
3
3
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { useTUITheme } from "./ui-theme.js";
|
|
4
5
|
// Keys match what ink-select-input handled: up/down or k/j (wrapping), 1-9 to pick, Enter to select.
|
|
5
|
-
export const ResourceSelectInput = ({ items, onSelect, highlightedIndex, width, onLayout, }) => {
|
|
6
|
+
export const ResourceSelectInput = ({ items, onSelect, highlightedIndex, width, onLayout, isActive = true, maxVisibleItems = items.length, }) => {
|
|
7
|
+
const theme = useTUITheme();
|
|
6
8
|
const listRef = useRef(null);
|
|
7
9
|
const [selected, setSelected] = useState(() => Math.max(0, Math.min(highlightedIndex, items.length - 1)));
|
|
8
|
-
const
|
|
10
|
+
const visibleCount = Math.max(1, maxVisibleItems);
|
|
11
|
+
const firstVisible = Math.min(Math.max(0, selected - visibleCount + 1), Math.max(0, items.length - visibleCount));
|
|
9
12
|
useLayoutEffect(() => {
|
|
10
13
|
if (listRef.current)
|
|
11
|
-
onLayout?.(measureElement(listRef.current).y);
|
|
14
|
+
onLayout?.(measureElement(listRef.current).y, firstVisible);
|
|
12
15
|
});
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setSelected(Math.max(0, Math.min(highlightedIndex, items.length - 1)));
|
|
18
|
+
}, [highlightedIndex, items]);
|
|
15
19
|
useInput((input, key) => {
|
|
16
20
|
if (items.length === 0)
|
|
17
21
|
return;
|
|
18
|
-
|
|
22
|
+
const plainKey = !key.ctrl && !key.meta;
|
|
23
|
+
if ((plainKey && input === "k") || key.upArrow)
|
|
19
24
|
setSelected((i) => (i === 0 ? items.length - 1 : i - 1));
|
|
20
|
-
if (input === "j" || key.downArrow)
|
|
25
|
+
if ((plainKey && input === "j") || key.downArrow)
|
|
21
26
|
setSelected((i) => (i === items.length - 1 ? 0 : i + 1));
|
|
22
27
|
if (/^[1-9]$/.test(input)) {
|
|
23
28
|
const item = items[Number(input) - 1];
|
|
@@ -26,10 +31,10 @@ export const ResourceSelectInput = ({ items, onSelect, highlightedIndex, width,
|
|
|
26
31
|
}
|
|
27
32
|
if (key.return)
|
|
28
33
|
onSelect(items[selected]);
|
|
29
|
-
});
|
|
30
|
-
return (_jsx(Box, { ref: listRef, flexDirection: "column", width: width, children: items.map((item, index) => {
|
|
31
|
-
const isSelected = index === selected;
|
|
32
|
-
return (_jsx(Box, { width: width, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: isSelected ?
|
|
34
|
+
}, { isActive });
|
|
35
|
+
return (_jsx(Box, { ref: listRef, flexDirection: "column", width: width, children: items.slice(firstVisible, firstVisible + visibleCount).map((item, index) => {
|
|
36
|
+
const isSelected = firstVisible + index === selected;
|
|
37
|
+
return (_jsx(Box, { width: width, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: isSelected ? theme.selectedForeground : theme.muted, backgroundColor: isSelected ? theme.selectedBackground : undefined, children: isSelected ? `${theme.selectionMarker} ` : " " }), _jsx(Text, { color: isSelected ? theme.selectedForeground : theme.foreground, bold: isSelected || theme.highContrast, backgroundColor: isSelected ? theme.selectedBackground : undefined, children: item.label })] }) }, item.value));
|
|
33
38
|
}) }));
|
|
34
39
|
};
|
|
35
40
|
//# sourceMappingURL=ResourceSelectInput.js.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { formatShortDate, getStatusColor, getStatusIcon, truncate } from "../utils/formatting.js";
|
|
4
|
+
import { getTUIStatusColor, getTUIStatusIcon, useTUITheme } from "./ui-theme.js";
|
|
4
5
|
export const ResourceTable = ({ resources, maxWidth = 100 }) => {
|
|
6
|
+
const theme = useTUITheme();
|
|
5
7
|
if (resources.length === 0) {
|
|
6
|
-
return (_jsx(Box, { paddingY: 1, children: _jsx(Text, { color:
|
|
8
|
+
return (_jsx(Box, { paddingY: 1, children: _jsx(Text, { color: theme.muted, children: "No resources found" }) }));
|
|
7
9
|
}
|
|
8
10
|
const narrowMode = maxWidth < 80;
|
|
9
11
|
const tableContentWidth = Math.max(1, Math.floor(maxWidth) - 4);
|
|
@@ -17,11 +19,16 @@ export const ResourceTable = ({ resources, maxWidth = 100 }) => {
|
|
|
17
19
|
const createdWidth = narrowMode
|
|
18
20
|
? 0
|
|
19
21
|
: tableContentWidth - nameWidth - stackWidth - typeWidth - statusWidth;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const borderProps = theme.ascii
|
|
23
|
+
? {}
|
|
24
|
+
: { borderStyle: "single", borderColor: theme.border };
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", ...borderProps, paddingX: 1, children: [_jsx(Box, { width: nameWidth, children: _jsx(Text, { bold: true, color: theme.foreground, children: "Name" }) }), _jsx(Box, { width: stackWidth, children: _jsx(Text, { bold: true, color: theme.foreground, children: "Stack" }) }), _jsx(Box, { width: typeWidth, children: _jsx(Text, { bold: true, color: theme.foreground, children: "Type" }) }), _jsx(Box, { width: statusWidth, children: _jsx(Text, { bold: true, color: theme.foreground, children: "Status" }) }), !narrowMode && (_jsx(Box, { width: createdWidth, children: _jsx(Text, { bold: true, color: theme.foreground, children: "Created" }) }))] }), resources.map((resource) => {
|
|
26
|
+
const sourceStatusColor = getStatusColor(resource.status);
|
|
27
|
+
const statusColor = getTUIStatusColor(theme, sourceStatusColor);
|
|
28
|
+
const statusIcon = getTUIStatusIcon(theme, getStatusIcon(resource.status), sourceStatusColor);
|
|
29
|
+
const statusLabel = `${statusIcon} ${resource.status}`;
|
|
23
30
|
const stackName = resource.stack && resource.stack !== "unknown" ? resource.stack : "-";
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "row", paddingX: 1, children: [_jsx(Box, { width: nameWidth, children: _jsx(Text, { children: truncate(resource.name, Math.max(3, nameWidth - 1)) }) }), _jsx(Box, { width: stackWidth, children: _jsx(Text, { color:
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "row", paddingX: 1, children: [_jsx(Box, { width: nameWidth, children: _jsx(Text, { color: theme.foreground, children: truncate(resource.name, Math.max(3, nameWidth - 1)) }) }), _jsx(Box, { width: stackWidth, children: _jsx(Text, { color: theme.muted, children: truncate(stackName, Math.max(3, stackWidth - 1)) }) }), _jsx(Box, { width: typeWidth, children: _jsx(Text, { color: theme.accent, bold: theme.highContrast, children: truncate(resource.type, Math.max(3, typeWidth - 1)) }) }), _jsx(Box, { width: statusWidth, children: _jsx(Text, { color: statusColor, bold: theme.highContrast, children: truncate(statusLabel, Math.max(3, statusWidth - 1)) }) }), !narrowMode && (_jsx(Box, { width: createdWidth, children: _jsx(Text, { color: theme.muted, children: truncate(formatShortDate(resource.createdAt), Math.max(3, createdWidth - 1)) }) }))] }, resource.name));
|
|
25
32
|
})] }));
|
|
26
33
|
};
|
|
27
34
|
//# sourceMappingURL=ResourceTable.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useTUITheme } from "./ui-theme.js";
|
|
4
|
+
export const TUIHeader = ({ projectRoot, resourceCount, terminalWidth, version, }) => {
|
|
5
|
+
const theme = useTUITheme();
|
|
6
|
+
const compact = terminalWidth < 80;
|
|
7
|
+
const title = compact
|
|
8
|
+
? `TDK v${version}`
|
|
9
|
+
: `${theme.bannerStart} TDK NEON EDITION v${version} ${theme.bannerEnd}`;
|
|
10
|
+
const resourceLabel = `${resourceCount} resource${resourceCount === 1 ? "" : "s"} discovered`;
|
|
11
|
+
const paddingX = terminalWidth >= 28 ? 1 : 0;
|
|
12
|
+
const contentWidth = Math.max(1, terminalWidth - paddingX * 2);
|
|
13
|
+
if (compact) {
|
|
14
|
+
const splitHeading = title.length + " | ".length + resourceLabel.length > contentWidth;
|
|
15
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: paddingX, width: terminalWidth, children: [splitHeading ? (_jsxs(_Fragment, { children: [_jsx(Box, { width: contentWidth, children: _jsx(Text, { color: theme.accent, bold: true, wrap: "truncate-end", children: title }) }), _jsx(Box, { width: contentWidth, children: _jsx(Text, { color: theme.success, wrap: "truncate-end", children: resourceLabel }) })] })) : (_jsx(Box, { width: contentWidth, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), _jsx(Text, { color: theme.muted, children: " | " }), _jsx(Text, { color: theme.success, children: resourceLabel })] }) })), _jsx(Box, { width: contentWidth, children: _jsx(Text, { color: theme.foreground, wrap: "truncate-end", children: projectRoot }) })] }));
|
|
16
|
+
}
|
|
17
|
+
const separatorsWidth = " | ".length * 2;
|
|
18
|
+
const headingWidth = title.length + resourceLabel.length + separatorsWidth;
|
|
19
|
+
if (headingWidth >= contentWidth) {
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: paddingX, width: terminalWidth, children: [_jsx(Box, { width: contentWidth, children: _jsx(Text, { color: theme.accent, bold: true, wrap: "truncate-end", children: title }) }), _jsx(Box, { width: contentWidth, children: _jsx(Text, { color: theme.success, wrap: "truncate-end", children: resourceLabel }) }), _jsx(Box, { width: contentWidth, children: _jsx(Text, { color: theme.foreground, wrap: "truncate-end", children: projectRoot }) })] }));
|
|
21
|
+
}
|
|
22
|
+
const rootWidth = contentWidth - headingWidth;
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "row", paddingX: paddingX, width: terminalWidth, children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), _jsx(Text, { color: theme.muted, children: " | " }), _jsx(Box, { flexShrink: 0, width: rootWidth, children: _jsx(Text, { color: theme.foreground, wrap: "truncate-end", children: projectRoot }) }), _jsx(Text, { color: theme.muted, children: " | " }), _jsx(Text, { color: theme.success, children: resourceLabel })] }));
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=TUIHeader.js.map
|
|
@@ -1,25 +1,30 @@
|
|
|
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
|
-
|
|
4
|
+
import { useTUITheme } from "./ui-theme.js";
|
|
5
|
+
export const TABS = [
|
|
5
6
|
{ id: "overview", label: "OVERVIEW", shortcut: "1" },
|
|
6
7
|
{ id: "resources", label: "RESOURCES", shortcut: "2" },
|
|
7
|
-
{ id: "
|
|
8
|
-
{ id: "
|
|
9
|
-
{ id: "config", label: "CONFIG", shortcut: "5" },
|
|
8
|
+
{ id: "files", label: "FILES", shortcut: "3" },
|
|
9
|
+
{ id: "config", label: "CONFIG", shortcut: "4" },
|
|
10
10
|
];
|
|
11
11
|
export const TabBar = ({ activeTab, compact = false, terminalWidth }) => {
|
|
12
|
+
const theme = useTUITheme();
|
|
12
13
|
const responsive = terminalWidth !== undefined;
|
|
13
14
|
const density = terminalWidth === undefined ? "wide" : getTabBarDensity(terminalWidth);
|
|
14
15
|
const compactLayout = responsive && density === "compact";
|
|
15
16
|
const wide = !responsive || density === "wide";
|
|
16
17
|
const ruleWidth = responsive ? getTerminalRuleWidth(terminalWidth) : compact ? 60 : 80;
|
|
17
|
-
|
|
18
|
+
const divider = (theme.ascii ? "-" : "\u2500").repeat(ruleWidth);
|
|
19
|
+
const selectedBorderProps = theme.ascii
|
|
20
|
+
? {}
|
|
21
|
+
: { borderStyle: "single", borderColor: theme.accent };
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: divider }) }), _jsx(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 1, children: TABS.map((tab) => {
|
|
18
23
|
const isActive = activeTab === tab.id;
|
|
19
24
|
const label = compactLayout || (!responsive && compact && !isActive)
|
|
20
25
|
? tab.label.slice(0, 4)
|
|
21
26
|
: tab.label;
|
|
22
|
-
return (_jsx(Box, { children: compactLayout ? (_jsxs(Text, { color: isActive ?
|
|
23
|
-
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color:
|
|
27
|
+
return (_jsx(Box, { children: compactLayout ? (_jsxs(Text, { color: isActive ? theme.selectedForeground : theme.muted, bold: isActive || theme.highContrast, backgroundColor: isActive ? theme.selectedBackground : undefined, children: [isActive ? `${theme.selectionMarker} ` : "", "[", tab.shortcut, "] ", label] })) : isActive ? (_jsx(Box, { ...selectedBorderProps, paddingX: wide ? 1 : 0, backgroundColor: theme.selectedBackground, children: _jsxs(Text, { children: [wide && _jsx(Text, { color: theme.selectedForeground, children: theme.bannerStart }), _jsxs(Text, { color: theme.selectedForeground, bold: true, children: [" ", "[", tab.shortcut, "] ", label, " "] }), wide && _jsx(Text, { color: theme.selectedForeground, children: theme.bannerEnd })] }) })) : (_jsx(Box, { paddingX: wide ? 1 : 0, children: _jsxs(Text, { color: theme.muted, dimColor: theme.dimMuted, bold: theme.highContrast, children: ["[", tab.shortcut, "] ", label] }) })) }, tab.id));
|
|
28
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: divider }) })] }));
|
|
24
29
|
};
|
|
25
30
|
//# sourceMappingURL=TabBar.js.map
|
|
@@ -5,4 +5,5 @@ export { ResourceSelectInput } from "./ResourceSelectInput.js";
|
|
|
5
5
|
export { ResourceTable } from "./ResourceTable.js";
|
|
6
6
|
export { TabBar } from "./TabBar.js";
|
|
7
7
|
export { TOOLTIPS } from "./Tooltip.js";
|
|
8
|
+
export { TUIHeader } from "./TUIHeader.js";
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
const FILE_TYPES = ["docker", "tilt", "config", "prisma", "generated", "unknown"];
|
|
3
|
+
const DEFAULT_FILE_COLORS = {
|
|
4
|
+
docker: "blue",
|
|
5
|
+
tilt: "cyan",
|
|
6
|
+
config: "yellow",
|
|
7
|
+
prisma: "magenta",
|
|
8
|
+
generated: "gray",
|
|
9
|
+
unknown: "white",
|
|
10
|
+
};
|
|
11
|
+
const HIGH_CONTRAST_FILE_COLORS = {
|
|
12
|
+
docker: "blueBright",
|
|
13
|
+
tilt: "cyanBright",
|
|
14
|
+
config: "yellowBright",
|
|
15
|
+
prisma: "magentaBright",
|
|
16
|
+
generated: "whiteBright",
|
|
17
|
+
unknown: "whiteBright",
|
|
18
|
+
};
|
|
19
|
+
/** Build the TUI palette from the requested theme and terminal environment. */
|
|
20
|
+
export function createTUITheme(highContrast, environment = process.env) {
|
|
21
|
+
const ascii = environment.NO_COLOR !== undefined ||
|
|
22
|
+
environment.TERM?.toLowerCase().startsWith("dumb") === true;
|
|
23
|
+
const palette = highContrast
|
|
24
|
+
? {
|
|
25
|
+
accent: "cyanBright",
|
|
26
|
+
border: "whiteBright",
|
|
27
|
+
muted: "white",
|
|
28
|
+
foreground: "whiteBright",
|
|
29
|
+
success: "greenBright",
|
|
30
|
+
warning: "yellowBright",
|
|
31
|
+
error: "redBright",
|
|
32
|
+
info: "blueBright",
|
|
33
|
+
secondary: "magentaBright",
|
|
34
|
+
selectedBackground: "cyanBright",
|
|
35
|
+
selectedForeground: "black",
|
|
36
|
+
selectionBackground: "blueBright",
|
|
37
|
+
selectionForeground: "whiteBright",
|
|
38
|
+
dimMuted: false,
|
|
39
|
+
fileColors: HIGH_CONTRAST_FILE_COLORS,
|
|
40
|
+
}
|
|
41
|
+
: {
|
|
42
|
+
accent: "cyan",
|
|
43
|
+
border: "gray",
|
|
44
|
+
muted: "gray",
|
|
45
|
+
foreground: "white",
|
|
46
|
+
success: "green",
|
|
47
|
+
warning: "yellow",
|
|
48
|
+
error: "red",
|
|
49
|
+
info: "blue",
|
|
50
|
+
secondary: "magenta",
|
|
51
|
+
selectedBackground: "black",
|
|
52
|
+
selectedForeground: "cyan",
|
|
53
|
+
selectionBackground: "blue",
|
|
54
|
+
selectionForeground: "cyan",
|
|
55
|
+
dimMuted: true,
|
|
56
|
+
fileColors: DEFAULT_FILE_COLORS,
|
|
57
|
+
};
|
|
58
|
+
const colorless = ascii
|
|
59
|
+
? {
|
|
60
|
+
accent: undefined,
|
|
61
|
+
border: undefined,
|
|
62
|
+
muted: undefined,
|
|
63
|
+
foreground: undefined,
|
|
64
|
+
success: undefined,
|
|
65
|
+
warning: undefined,
|
|
66
|
+
error: undefined,
|
|
67
|
+
info: undefined,
|
|
68
|
+
secondary: undefined,
|
|
69
|
+
selectedBackground: undefined,
|
|
70
|
+
selectedForeground: undefined,
|
|
71
|
+
selectionBackground: undefined,
|
|
72
|
+
selectionForeground: undefined,
|
|
73
|
+
dimMuted: false,
|
|
74
|
+
fileColors: Object.fromEntries(FILE_TYPES.map((type) => [type, undefined])),
|
|
75
|
+
}
|
|
76
|
+
: {};
|
|
77
|
+
return {
|
|
78
|
+
...palette,
|
|
79
|
+
...colorless,
|
|
80
|
+
highContrast: highContrast && !ascii,
|
|
81
|
+
ascii,
|
|
82
|
+
bannerStart: ascii ? ">>>" : "\u2593\u2592\u2591",
|
|
83
|
+
bannerEnd: ascii ? "<<<" : "\u2591\u2592\u2593",
|
|
84
|
+
selectionMarker: ascii ? ">>>" : "\u2593\u2592\u2591",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const STATUS_COLOR_THEMES = {
|
|
88
|
+
green: "success",
|
|
89
|
+
yellow: "warning",
|
|
90
|
+
red: "error",
|
|
91
|
+
};
|
|
92
|
+
/** Map a formatter status color to the active theme palette. */
|
|
93
|
+
export function getTUIStatusColor(theme, color) {
|
|
94
|
+
const themeKey = STATUS_COLOR_THEMES[color ?? ""];
|
|
95
|
+
return themeKey ? theme[themeKey] : theme.muted;
|
|
96
|
+
}
|
|
97
|
+
/** Return a status marker suited to the active terminal character set. */
|
|
98
|
+
export function getTUIStatusIcon(theme, icon, color) {
|
|
99
|
+
if (!theme.ascii)
|
|
100
|
+
return icon;
|
|
101
|
+
if (color === "green")
|
|
102
|
+
return "[+]";
|
|
103
|
+
if (color === "yellow")
|
|
104
|
+
return "[~]";
|
|
105
|
+
if (color === "red")
|
|
106
|
+
return "[x]";
|
|
107
|
+
return "[?]";
|
|
108
|
+
}
|
|
109
|
+
export const TUIThemeContext = createContext(createTUITheme(false));
|
|
110
|
+
/** Read the theme provided to the current TUI subtree. */
|
|
111
|
+
export function useTUITheme() {
|
|
112
|
+
return useContext(TUIThemeContext);
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=ui-theme.js.map
|
|
@@ -5,6 +5,7 @@ import Handlebars from "handlebars";
|
|
|
5
5
|
import { PLATFORM_STANDARDS } from "../config/platform-standards.js";
|
|
6
6
|
import { writeTextFile } from "../utils/file-helpers.js";
|
|
7
7
|
import { assertTdkGeneratedPath } from "../utils/generated-paths.js";
|
|
8
|
+
import { DEFAULT_ALWAYS_ENABLED_INFRA } from "../utils/project-config-defaults.js";
|
|
8
9
|
import { validateServiceManifestFile } from "../utils/service-manifest.js";
|
|
9
10
|
import { discoverResourcesFromRoot, discoverServiceManifestPaths } from "../utils/services.js";
|
|
10
11
|
import { isStackFeatureEnabledInStacks } from "../utils/stack-features.js";
|
|
@@ -148,7 +149,7 @@ export class TemplateEngine {
|
|
|
148
149
|
runtime: PLATFORM_STANDARDS.runtime,
|
|
149
150
|
project: projectConfig.project,
|
|
150
151
|
phases: dedupePhaseEnabledStacks(projectConfig.phases),
|
|
151
|
-
alwaysEnabledInfra: projectConfig.always_enabled_infra ?? [
|
|
152
|
+
alwaysEnabledInfra: projectConfig.always_enabled_infra ?? [...DEFAULT_ALWAYS_ENABLED_INFRA],
|
|
152
153
|
optionalInfra: projectConfig.optional_infra,
|
|
153
154
|
serviceDescriptions: RESOURCE_DESCRIPTIONS,
|
|
154
155
|
infraDescriptions: INFRA_DESCRIPTIONS,
|
|
@@ -7,6 +7,7 @@ import { formatCount } from "./formatting.js";
|
|
|
7
7
|
import { findProjectRoot } from "./paths.js";
|
|
8
8
|
import { isApiServiceType } from "./resource-kind.js";
|
|
9
9
|
import { discoverResourcesFromRoot } from "./services.js";
|
|
10
|
+
import { evaluateSharedPlatformPostgres } from "./shared-platform-postgres.js";
|
|
10
11
|
/**
|
|
11
12
|
* Checks for wiring mistakes that only show up minutes into `tdk up`: an image
|
|
12
13
|
* build that cannot find package.json, a token check that cannot reach its
|
|
@@ -16,6 +17,7 @@ import { discoverResourcesFromRoot } from "./services.js";
|
|
|
16
17
|
const EXEC_TIMEOUT_MS = 10_000;
|
|
17
18
|
const SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue)$/;
|
|
18
19
|
const TEST_FILE = /(?:\.test\.|\.spec\.|\/tests?\/|\/__tests__\/)/;
|
|
20
|
+
const PRISMA_MAJOR = 7;
|
|
19
21
|
const SKIPPED_DIRS = new Set(["node_modules", ".autogenerated", "dist", "tests", "__tests__"]);
|
|
20
22
|
function projectName(projectRoot) {
|
|
21
23
|
try {
|
|
@@ -35,6 +37,126 @@ function readParams(configPath) {
|
|
|
35
37
|
return {};
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
function packageJson(resourcePath) {
|
|
41
|
+
try {
|
|
42
|
+
const value = JSON.parse(readFileSync(join(resourcePath, "package.json"), "utf-8"));
|
|
43
|
+
return value && typeof value === "object" ? value : {};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function dependencyMajor(value) {
|
|
50
|
+
if (typeof value !== "string")
|
|
51
|
+
return undefined;
|
|
52
|
+
const match = value.match(/(?:^|[^0-9])(\d+)(?:\.|$)/);
|
|
53
|
+
return match ? Number(match[1]) : undefined;
|
|
54
|
+
}
|
|
55
|
+
function prismaSchemaFindings(path) {
|
|
56
|
+
const raw = readText(path);
|
|
57
|
+
if (raw === undefined)
|
|
58
|
+
return ["missing prisma/schema.prisma"];
|
|
59
|
+
const text = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
60
|
+
const findings = [];
|
|
61
|
+
const datasource = text.match(/datasource\s+\w+\s*\{([^}]*)\}/)?.[1] ?? "";
|
|
62
|
+
if (!/provider\s*=\s*["']postgresql["']/.test(datasource))
|
|
63
|
+
findings.push("schema provider is not postgresql");
|
|
64
|
+
if (/\b(?:url|directUrl)\s*=/.test(text))
|
|
65
|
+
findings.push("schema still declares url/directUrl");
|
|
66
|
+
return findings;
|
|
67
|
+
}
|
|
68
|
+
function prismaConfigFindings(resourcePath) {
|
|
69
|
+
const generated = join(resourcePath, ".autogenerated", "prisma.config.autogenerated.ts");
|
|
70
|
+
const candidates = [join(resourcePath, "prisma.config.ts"), generated];
|
|
71
|
+
const path = candidates.find((candidate) => existsSync(candidate));
|
|
72
|
+
if (!path)
|
|
73
|
+
return ["missing prisma.config.ts or generated Prisma config"];
|
|
74
|
+
const text = readText(path) ?? "";
|
|
75
|
+
const datasource = text.match(/datasource\s*:\s*\{([^}]*)\}/)?.[1] ?? "";
|
|
76
|
+
if (!/url\s*:\s*(?:env\(["']DATABASE_URL["']\)|process\.env\.DATABASE_URL)\s*,?/.test(datasource)) {
|
|
77
|
+
return ['Prisma config datasource.url is not env("DATABASE_URL")'];
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
/** Enforces the Prisma 7 project contract before `tdk up` can run. */
|
|
82
|
+
export function checkPrismaConsistency(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
83
|
+
const resources = discoverResourcesFromRoot(projectRoot);
|
|
84
|
+
const prismaResources = resources.filter((resource) => resource.config?.featuresEnabled?.includes("prisma"));
|
|
85
|
+
const findings = [];
|
|
86
|
+
const migrators = resources.filter((resource) => resource.config?.appType === "migrator");
|
|
87
|
+
for (const resource of resources) {
|
|
88
|
+
const legacyFeatures = resource.config?.features;
|
|
89
|
+
if (Array.isArray(legacyFeatures) &&
|
|
90
|
+
legacyFeatures.includes("prisma") &&
|
|
91
|
+
!resource.config?.featuresEnabled?.includes("prisma")) {
|
|
92
|
+
findings.push(`${resource.name}: use featuresEnabled, not features, for Prisma`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (prismaResources.length === 0) {
|
|
96
|
+
return findings.length === 0
|
|
97
|
+
? {
|
|
98
|
+
name: "Prisma 7 consistency",
|
|
99
|
+
didPass: true,
|
|
100
|
+
isSkipped: true,
|
|
101
|
+
message: "No resources enable Prisma; Prisma consistency checks were skipped",
|
|
102
|
+
}
|
|
103
|
+
: {
|
|
104
|
+
name: "Prisma 7 consistency",
|
|
105
|
+
didPass: false,
|
|
106
|
+
message: `${formatCount(findings.length, "Prisma consistency error")}:\n ${findings.join("\n ")}`,
|
|
107
|
+
fix: "Use featuresEnabled to opt into the validated Prisma 7 shape",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
for (const resource of prismaResources) {
|
|
111
|
+
const dependencies = packageJson(resource.path).dependencies;
|
|
112
|
+
const devDependencies = packageJson(resource.path).devDependencies;
|
|
113
|
+
const packages = { ...(devDependencies ?? {}), ...(dependencies ?? {}) };
|
|
114
|
+
const prismaMajor = dependencyMajor(packages.prisma);
|
|
115
|
+
const clientMajor = dependencyMajor(packages["@prisma/client"]);
|
|
116
|
+
if (prismaMajor === undefined || clientMajor === undefined)
|
|
117
|
+
findings.push(`${resource.name}: prisma and @prisma/client are required`);
|
|
118
|
+
else if (prismaMajor !== clientMajor || prismaMajor !== PRISMA_MAJOR)
|
|
119
|
+
findings.push(`${resource.name}: prisma packages must both be major 7`);
|
|
120
|
+
findings.push(...prismaSchemaFindings(join(resource.path, "prisma", "schema.prisma")).map((finding) => `${resource.name}: ${finding}`));
|
|
121
|
+
findings.push(...prismaConfigFindings(resource.path).map((finding) => `${resource.name}: ${finding}`));
|
|
122
|
+
const appType = resource.config?.appType ?? resource.type;
|
|
123
|
+
if (appType === "migrator") {
|
|
124
|
+
const postgresDependency = resource.config?.dependsOn?.some((name) => name === "postgres" || name === "database-management");
|
|
125
|
+
if (!postgresDependency)
|
|
126
|
+
findings.push(`${resource.name}: migrator must depend on postgres or database-management`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const migratorDependency = resource.config?.dependsOn?.some((name) => migrators.some((candidate) => candidate.name === name));
|
|
130
|
+
if (!migratorDependency)
|
|
131
|
+
findings.push(`${resource.name}: API must depend on its Prisma migrator`);
|
|
132
|
+
const scripts = packageJson(resource.path).scripts;
|
|
133
|
+
if (Object.entries(scripts ?? {}).some(([name, command]) => START_SCRIPTS.has(name) &&
|
|
134
|
+
typeof command === "string" &&
|
|
135
|
+
/prisma\s+migrate/.test(command))) {
|
|
136
|
+
findings.push(`${resource.name}: API start script must not run prisma migrate`);
|
|
137
|
+
}
|
|
138
|
+
const dockerfile = join(resource.path, resource.config?.dockerfile ?? "Dockerfile");
|
|
139
|
+
if (fileMigrations(dockerfile, true)) {
|
|
140
|
+
findings.push(`${resource.name}: API image/startup must not run Prisma migrations`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (prismaResources.length > 0 && !evaluateSharedPlatformPostgres(projectRoot).willStart) {
|
|
145
|
+
findings.push("shared platform Postgres will not start for the Prisma resources");
|
|
146
|
+
}
|
|
147
|
+
return findings.length === 0
|
|
148
|
+
? {
|
|
149
|
+
name: "Prisma 7 consistency",
|
|
150
|
+
didPass: true,
|
|
151
|
+
message: "Prisma resources use the validated Prisma 7 shape",
|
|
152
|
+
}
|
|
153
|
+
: {
|
|
154
|
+
name: "Prisma 7 consistency",
|
|
155
|
+
didPass: false,
|
|
156
|
+
message: `${formatCount(findings.length, "Prisma consistency error")}:\n ${findings.join("\n ")}`,
|
|
157
|
+
fix: "Use Prisma 7 packages and config, put migrations in a migrator, and make the API depend on that migrator",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
38
160
|
/** Backends are the only resources that listen on their `port`. */
|
|
39
161
|
function backendPorts(resources) {
|
|
40
162
|
const ports = new Map();
|
|
@@ -356,6 +478,76 @@ export function checkNatsBroker(projectRoot = findProjectRoot() ?? process.cwd()
|
|
|
356
478
|
fix: `Create it with a "nats" service (image nats with -js, container_name ${name}_nats, network ${name}_backend, external) and a "redis" service, which TDK registers alongside it`,
|
|
357
479
|
};
|
|
358
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Shared platform Postgres will-start reporting.
|
|
483
|
+
*
|
|
484
|
+
* Uses the same evaluateSharedPlatformPostgres predicate as `tdk config verify`,
|
|
485
|
+
* which reads project.json **and** the generated Tiltfile/spec.master when present.
|
|
486
|
+
* Resource set is the project discovery set those commands already inspect —
|
|
487
|
+
* not a `tdk up` filter. A resource that depends on Postgres while you bring up
|
|
488
|
+
* a different one still makes this report will-start; the engine start path is
|
|
489
|
+
* selection-bounded and may not start Postgres for that run. Unknown dependsOn
|
|
490
|
+
* names (typos like `postgress`) stay errors. Does NOT report
|
|
491
|
+
* postgres/database-management as missing services and does NOT claim Prisma
|
|
492
|
+
* will start.
|
|
493
|
+
*
|
|
494
|
+
* Preflight loop: edit service.json → `tdk config regenerate` → `tdk doctor`.
|
|
495
|
+
* Doctor is the gate; one real `tdk up` after doctor is green confirms the run.
|
|
496
|
+
*/
|
|
497
|
+
export function checkSharedPlatformPostgres(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
498
|
+
const evaluation = evaluateSharedPlatformPostgres(projectRoot);
|
|
499
|
+
const projectScopeNote = " (project resource set; tdk up may select a subset and not start Postgres)";
|
|
500
|
+
const focusWarning = evaluation.focusWouldEnableDatabaseManagement
|
|
501
|
+
? " Warning: generated Tiltfile default focus expands CORE_INFRA to database-management while project.json has it off; default tdk up takes the feature path."
|
|
502
|
+
: "";
|
|
503
|
+
if (evaluation.unknownDependsOnNames.length > 0) {
|
|
504
|
+
const list = evaluation.unknownDependsOnNames
|
|
505
|
+
.map(({ resource, name }) => `"${name}" (from ${resource})`)
|
|
506
|
+
.join(", ");
|
|
507
|
+
const willStartSuffix = evaluation.willStart
|
|
508
|
+
? ` Postgres will start because ${evaluation.reason === "feature"
|
|
509
|
+
? "database-management is enabled"
|
|
510
|
+
: `resource(s) ${evaluation.dependsOnUsers.join(", ")} depend on postgres/database-management`}${projectScopeNote}`
|
|
511
|
+
: " Postgres will not start for this project.json/generated Tiltfile state.";
|
|
512
|
+
return {
|
|
513
|
+
name: "Shared platform Postgres",
|
|
514
|
+
didPass: false,
|
|
515
|
+
message: `${formatCount(evaluation.unknownDependsOnNames.length, "unknown dependsOn name")}: ${list}. These are not known services or stacks, and are not the shared platform database names (postgres, database-management).${willStartSuffix}${focusWarning}`,
|
|
516
|
+
fix: 'Use "postgres" or "database-management" in dependsOn for the shared platform database, or fix the name to match an existing service',
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
if (!evaluation.willStart) {
|
|
520
|
+
return {
|
|
521
|
+
name: "Shared platform Postgres",
|
|
522
|
+
didPass: true,
|
|
523
|
+
isWarning: Boolean(focusWarning),
|
|
524
|
+
isSkipped: !focusWarning,
|
|
525
|
+
message: focusWarning
|
|
526
|
+
? focusWarning.trim()
|
|
527
|
+
: "Shared platform Postgres will not start: database-management is off and no resource depends on postgres or database-management.",
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (evaluation.reason === "feature" && evaluation.dependsOnUsers.length === 0) {
|
|
531
|
+
const source = evaluation.featureOnFromTiltfile || evaluation.featureOnFromSpecMaster
|
|
532
|
+
? " (generated Tiltfile/spec.master)"
|
|
533
|
+
: "";
|
|
534
|
+
return {
|
|
535
|
+
name: "Shared platform Postgres",
|
|
536
|
+
didPass: true,
|
|
537
|
+
isWarning: Boolean(focusWarning),
|
|
538
|
+
message: `Shared platform Postgres will start because the database-management feature is enabled${source} (project-wide; not a tdk up selection).${focusWarning}`,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const reason = evaluation.reason === "feature"
|
|
542
|
+
? `the database-management feature is enabled and resource(s) ${evaluation.dependsOnUsers.join(", ")} depend on postgres/database-management`
|
|
543
|
+
: `resource(s) ${evaluation.dependsOnUsers.join(", ")} depend on postgres/database-management`;
|
|
544
|
+
return {
|
|
545
|
+
name: "Shared platform Postgres",
|
|
546
|
+
didPass: true,
|
|
547
|
+
isWarning: Boolean(focusWarning),
|
|
548
|
+
message: `Shared platform Postgres will start because ${reason}${projectScopeNote}${focusWarning}`,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
359
551
|
export function parseTiltProcesses(psOutput) {
|
|
360
552
|
const processes = [];
|
|
361
553
|
for (const line of psOutput.split("\n")) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defaults shared by project generation and CLI predicates.
|
|
3
|
+
*
|
|
4
|
+
* Source of truth for `always_enabled_infra` when `.tdk/project.json` omits
|
|
5
|
+
* the field. The generator bakes this into the Tiltfile
|
|
6
|
+
* (`Tiltfile.hbs` → `ALWAYS_ENABLED_INFRA`); `should_enable` then returns
|
|
7
|
+
* `RESOURCE_DEFAULTS.get(name, True)` for names in that list — so an omitted
|
|
8
|
+
* field means database-management is ON for `tdk up`.
|
|
9
|
+
*
|
|
10
|
+
* `tdk doctor` / `tdk config verify` must use the same default, otherwise
|
|
11
|
+
* they report Postgres-will-start while the engine does not start it (or
|
|
12
|
+
* the reverse). See shared-platform-postgres.ts.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_ALWAYS_ENABLED_INFRA = ["database-management", "proxy"];
|
|
15
|
+
//# sourceMappingURL=project-config-defaults.js.map
|