@tdk-landscape/tdk-cli-core 1.3.117 → 1.3.119
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
package/README.md
CHANGED
|
@@ -142,3 +142,8 @@ Contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) and the
|
|
|
142
142
|
TDK is MIT-licensed; see [LICENSE](LICENSE). The [license boundary](GOVERNANCE.md#license-boundary-and-donation-scope) says which code is MIT and which is downloaded with a key.
|
|
143
143
|
|
|
144
144
|
Project governance: [GOVERNANCE.md](GOVERNANCE.md), [MAINTAINERS.md](MAINTAINERS.md), [ADOPTERS.md](ADOPTERS.md).
|
|
145
|
+
|
|
146
|
+
Use `tdk ui` to inspect stacks and resources. Arrow keys or `j`/`k` move the
|
|
147
|
+
selection; `g`/`G` or Home/End jump to the first/last item, and PageUp/PageDown
|
|
148
|
+
move one visible page. `/` searches; navigation letters remain search text
|
|
149
|
+
while searching. The selected row stays visible when the terminal is resized.
|
|
@@ -8,12 +8,14 @@ import { generateMasterConfigs, readProjectConfig, TemplateEngine, verifyMasterC
|
|
|
8
8
|
import { isMasterConfigFileName } from "../types/index.js";
|
|
9
9
|
import { assertValid } from "../utils/command-helpers.js";
|
|
10
10
|
import { MASTER_CONFIG_FILES } from "../utils/constants.js";
|
|
11
|
+
import { checkPrismaConsistency } from "../utils/doctor-wiring.js";
|
|
11
12
|
import { errorFactories, requireProjectRoot, runCommand } from "../utils/errors.js";
|
|
12
13
|
import { writeJsonFile } from "../utils/file-helpers.js";
|
|
13
14
|
import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
|
|
14
15
|
import { findProjectRoot } from "../utils/paths.js";
|
|
15
16
|
import { SERVICE_MANIFEST_SCHEMA_VERSION, validateServiceManifest, } from "../utils/service-manifest.js";
|
|
16
17
|
import { discoverServiceManifestPaths } from "../utils/services.js";
|
|
18
|
+
import { evaluateSharedPlatformPostgres } from "../utils/shared-platform-postgres.js";
|
|
17
19
|
import { validateOptionalInfraService } from "../utils/validation.js";
|
|
18
20
|
/**
|
|
19
21
|
* Serialize ProjectConfig to JSON-safe value.
|
|
@@ -182,14 +184,23 @@ export const configCommand = new Command("config")
|
|
|
182
184
|
if (!projectRoot)
|
|
183
185
|
throw errorFactories.notInProject();
|
|
184
186
|
const result = verifyMasterConfigs(projectRoot);
|
|
187
|
+
// Same predicate as tdk doctor's Shared platform Postgres check.
|
|
188
|
+
const sharedPostgres = evaluateSharedPlatformPostgres(projectRoot);
|
|
189
|
+
const prismaConsistency = checkPrismaConsistency(projectRoot);
|
|
190
|
+
const sharedPostgresErrors = sharedPostgres.unknownDependsOnNames.map(({ resource, name }) => `dependsOn "${name}" on ${resource} is not a known service or stack (shared platform Postgres names are postgres and database-management)`);
|
|
185
191
|
if (options.json) {
|
|
186
192
|
console.log(JSON.stringify(createMachineEnvelope({
|
|
187
|
-
valid: result.valid,
|
|
188
|
-
errors:
|
|
193
|
+
valid: result.valid && sharedPostgresErrors.length === 0 && prismaConsistency.didPass,
|
|
194
|
+
errors: [
|
|
195
|
+
...result.errors,
|
|
196
|
+
...sharedPostgresErrors,
|
|
197
|
+
...(prismaConsistency.didPass ? [] : [prismaConsistency.message]),
|
|
198
|
+
],
|
|
189
199
|
warnings: result.warnings,
|
|
190
200
|
diffs: result.diffs,
|
|
201
|
+
sharedPlatformPostgres: sharedPostgres,
|
|
191
202
|
})));
|
|
192
|
-
if (!result.valid)
|
|
203
|
+
if (!result.valid || sharedPostgresErrors.length > 0 || !prismaConsistency.didPass)
|
|
193
204
|
process.exit(1);
|
|
194
205
|
return;
|
|
195
206
|
}
|
|
@@ -197,7 +208,32 @@ export const configCommand = new Command("config")
|
|
|
197
208
|
for (const warning of result.warnings) {
|
|
198
209
|
console.warn(chalk.yellow(`⚠️ ${warning}`));
|
|
199
210
|
}
|
|
200
|
-
|
|
211
|
+
for (const error of sharedPostgresErrors) {
|
|
212
|
+
console.log(chalk.red(`❌ ${error}`));
|
|
213
|
+
}
|
|
214
|
+
if (!prismaConsistency.didPass)
|
|
215
|
+
console.log(chalk.red(`❌ ${prismaConsistency.message}`));
|
|
216
|
+
if (sharedPostgres.willStart) {
|
|
217
|
+
// Project resource set (discoverResourcesFromRoot), not a tdk up --only filter.
|
|
218
|
+
// Feature-on alone is not new behavior — print gray so default projects are not noisy.
|
|
219
|
+
// dependsOn is the reason Postgres starts when the feature is off — green.
|
|
220
|
+
if (sharedPostgres.reason === "feature" && sharedPostgres.dependsOnUsers.length === 0) {
|
|
221
|
+
const source = sharedPostgres.featureOnFromTiltfile || sharedPostgres.featureOnFromSpecMaster
|
|
222
|
+
? " (generated Tiltfile/spec.master)"
|
|
223
|
+
: "";
|
|
224
|
+
console.log(chalk.gray(`ℹ️ Postgres will start because database-management is enabled${source} (project-wide; not a tdk up selection)`));
|
|
225
|
+
}
|
|
226
|
+
else if (sharedPostgres.reason === "feature") {
|
|
227
|
+
console.log(chalk.yellow(`ℹ️ Postgres will start because database-management is enabled and resource(s) ${sharedPostgres.dependsOnUsers.join(", ")} depend on postgres/database-management (project resource set; tdk up may select a subset)`));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(chalk.green(`ℹ️ Postgres will start because resource(s) ${sharedPostgres.dependsOnUsers.join(", ")} depend on postgres/database-management (project resource set; tdk up may select a subset)`));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (sharedPostgres.focusWouldEnableDatabaseManagement) {
|
|
234
|
+
console.log(chalk.yellow("ℹ️ Postgres will not start from project.json/generated Tiltfile, but default focus/CORE_INFRA expansion would enable database-management on a typical tdk up"));
|
|
235
|
+
}
|
|
236
|
+
if (result.valid && sharedPostgresErrors.length === 0 && prismaConsistency.didPass) {
|
|
201
237
|
console.log(chalk.green("✅ All files are in sync!"));
|
|
202
238
|
return;
|
|
203
239
|
}
|
|
@@ -208,7 +244,9 @@ export const configCommand = new Command("config")
|
|
|
208
244
|
}
|
|
209
245
|
for (const { diff } of result.diffs)
|
|
210
246
|
console.log(chalk.gray(`\n${diff}`));
|
|
211
|
-
|
|
247
|
+
if (!result.valid) {
|
|
248
|
+
console.log(chalk.gray("\nRun `tdk config regenerate` to fix."));
|
|
249
|
+
}
|
|
212
250
|
process.exit(1);
|
|
213
251
|
}
|
|
214
252
|
};
|
|
@@ -9,7 +9,7 @@ import { MASTER_CONFIG_FILES, REQUIRED_PACKAGE_SCRIPTS } from "../utils/constant
|
|
|
9
9
|
import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
|
|
10
10
|
import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
|
|
11
11
|
import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
|
|
12
|
-
import { checkDockerNetworkCapacity, checkDuplicateResourceNames, checkDuplicateResourcePorts, checkFrontendBackendUrls, checkMigrationsInApi, checkNatsBroker, checkResourcePackageJson, checkServiceUrlPorts, checkTiltInstances, } from "../utils/doctor-wiring.js";
|
|
12
|
+
import { checkDockerNetworkCapacity, checkDuplicateResourceNames, checkDuplicateResourcePorts, checkFrontendBackendUrls, checkMigrationsInApi, checkNatsBroker, checkPrismaConsistency, checkResourcePackageJson, checkServiceUrlPorts, checkSharedPlatformPostgres, checkTiltInstances, } from "../utils/doctor-wiring.js";
|
|
13
13
|
import { validateEnvFile } from "../utils/env-validator.js";
|
|
14
14
|
import { execAsync, isExecTimeout } from "../utils/exec-async.js";
|
|
15
15
|
import { formatCount } from "../utils/formatting.js";
|
|
@@ -1143,6 +1143,9 @@ export const doctorCommand = new Command("doctor")
|
|
|
1143
1143
|
() => checkServiceUrlPorts(),
|
|
1144
1144
|
() => checkFrontendBackendUrls(),
|
|
1145
1145
|
() => checkNatsBroker(),
|
|
1146
|
+
() => checkPrismaConsistency(),
|
|
1147
|
+
// Shared platform Postgres: will-start report + unknown dependsOn names stay errors.
|
|
1148
|
+
() => checkSharedPlatformPostgres(),
|
|
1146
1149
|
() => checkMigrationsInApi(),
|
|
1147
1150
|
() => checkTiltInstances(),
|
|
1148
1151
|
checkEnvironmentVariables,
|
|
@@ -141,7 +141,7 @@ function requireGeneratedViteConfig(configPath) {
|
|
|
141
141
|
"Run tdk up <stack> once, or point this script at your own Vite config to build on the host.";
|
|
142
142
|
return `bun -e 'if(!require("fs").existsSync(${JSON.stringify(configPath)})){console.error(${JSON.stringify(message)});process.exit(1)}'`;
|
|
143
143
|
}
|
|
144
|
-
export function createPackageJson(name, type, frameworkId) {
|
|
144
|
+
export function createPackageJson(name, type, frameworkId, prismaEnabled = false) {
|
|
145
145
|
const isFrontend = type === "frontend";
|
|
146
146
|
const framework = resolveFrontendFramework(type, frameworkId);
|
|
147
147
|
const backendFramework = resolveBackendFramework(type, frameworkId);
|
|
@@ -171,6 +171,14 @@ export function createPackageJson(name, type, frameworkId) {
|
|
|
171
171
|
dependencies: {
|
|
172
172
|
...(isFrontend ? {} : (backendFramework?.dependencies ?? { hono: "^4.0.0" })),
|
|
173
173
|
...(framework?.dependencies ?? {}),
|
|
174
|
+
...(prismaEnabled
|
|
175
|
+
? {
|
|
176
|
+
prisma: "^7.5.0",
|
|
177
|
+
"@prisma/client": "^7.5.0",
|
|
178
|
+
"@prisma/adapter-pg": "^7.5.0",
|
|
179
|
+
pg: "^8.13.0",
|
|
180
|
+
}
|
|
181
|
+
: {}),
|
|
174
182
|
},
|
|
175
183
|
devDependencies: {
|
|
176
184
|
"@types/bun": "^1.4.2",
|
|
@@ -186,6 +194,22 @@ export function createPackageJson(name, type, frameworkId) {
|
|
|
186
194
|
},
|
|
187
195
|
};
|
|
188
196
|
}
|
|
197
|
+
export const PRISMA_SCHEMA_TEMPLATE = `generator client {
|
|
198
|
+
provider = "prisma-client"
|
|
199
|
+
output = "../generated/prisma"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
datasource db {
|
|
203
|
+
provider = "postgresql"
|
|
204
|
+
}
|
|
205
|
+
`;
|
|
206
|
+
export const PRISMA_CONFIG_TEMPLATE = `export default {
|
|
207
|
+
schema: "prisma/schema.prisma",
|
|
208
|
+
datasource: {
|
|
209
|
+
url: process.env.DATABASE_URL,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
`;
|
|
189
213
|
export function createResourceTsconfig(resourceType, frameworkId) {
|
|
190
214
|
const framework = resolveFrontendFramework(resourceType, frameworkId);
|
|
191
215
|
const backendFramework = resolveBackendFramework(resourceType, frameworkId);
|
|
@@ -375,6 +399,7 @@ export const resourceCommand = new Command("resource")
|
|
|
375
399
|
.option("--restart <policy>", "Compose restart policy: no, on-failure, unless-stopped, always (bring-your-own only; use no for one-shot jobs)")
|
|
376
400
|
.option("--image <name>", "Docker image name instead of building from Dockerfile (for bring-your-own)")
|
|
377
401
|
.option("--port <port>", "Port number (default: next free in 4000-5999)")
|
|
402
|
+
.option("--feature <feature...>", "Enable a resource feature (for example: prisma)")
|
|
378
403
|
.action(async (name, options) => {
|
|
379
404
|
if (options.frameworks) {
|
|
380
405
|
for (const f of listFrontendFrameworks()) {
|
|
@@ -681,8 +706,13 @@ This file contains the resource configuration for TDK.
|
|
|
681
706
|
}
|
|
682
707
|
}
|
|
683
708
|
// Prepare file generation tasks
|
|
684
|
-
const
|
|
685
|
-
const
|
|
709
|
+
const requestedFeatures = Array.isArray(options.feature) ? options.feature : [];
|
|
710
|
+
const prismaEnabled = requestedFeatures.includes("prisma");
|
|
711
|
+
const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, [...(dddEnabled ? ["ddd"] : []), ...requestedFeatures], scaffoldFrameworkId, options.language === undefined ? undefined : backendLanguage?.id);
|
|
712
|
+
const packageJson = createPackageJson(resourceName, resourceType, scaffoldFrameworkId, prismaEnabled);
|
|
713
|
+
if (prismaEnabled && resourceType === "backend") {
|
|
714
|
+
serviceJson.dependsOn = [`${resourceName}-migrator`];
|
|
715
|
+
}
|
|
686
716
|
const tasks = [
|
|
687
717
|
{
|
|
688
718
|
type: "json",
|
|
@@ -776,6 +806,76 @@ This file contains the resource configuration for TDK.
|
|
|
776
806
|
// Execute all file writes with progress
|
|
777
807
|
console.log(chalk.blue("💻 Generating source files..."));
|
|
778
808
|
writeFilesWithProgress(fullPath, tasks);
|
|
809
|
+
if (prismaEnabled && resourceType === "backend") {
|
|
810
|
+
const migratorName = `${resourceName}-migrator`;
|
|
811
|
+
const migratorPath = resolve(projectRoot, `services/${stackName}/${migratorName}`);
|
|
812
|
+
mkdirSync(migratorPath, { recursive: true });
|
|
813
|
+
const migratorConfig = {
|
|
814
|
+
...JSON.parse(JSON.stringify(BASE_TEMPLATE)),
|
|
815
|
+
$schema: SERVICE_MANIFEST_SCHEMA_URL,
|
|
816
|
+
schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
|
|
817
|
+
appName: migratorName,
|
|
818
|
+
appType: "migrator",
|
|
819
|
+
name: migratorName,
|
|
820
|
+
type: "migrator",
|
|
821
|
+
stack: stackName,
|
|
822
|
+
port: 0,
|
|
823
|
+
featuresEnabled: ["prisma"],
|
|
824
|
+
};
|
|
825
|
+
migratorConfig.dependsOn = ["postgres"];
|
|
826
|
+
migratorConfig.port = 0;
|
|
827
|
+
const migratorPackage = createPackageJson(migratorName, "migrator", undefined, true);
|
|
828
|
+
migratorPackage.scripts = {
|
|
829
|
+
...migratorPackage.scripts,
|
|
830
|
+
start: "prisma migrate deploy",
|
|
831
|
+
};
|
|
832
|
+
writeFilesWithProgress(migratorPath, [
|
|
833
|
+
{
|
|
834
|
+
type: "json",
|
|
835
|
+
filename: "service.json",
|
|
836
|
+
content: migratorConfig,
|
|
837
|
+
description: "Generating Prisma migrator manifest",
|
|
838
|
+
emoji: "🗃️",
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
type: "json",
|
|
842
|
+
filename: "package.json",
|
|
843
|
+
content: migratorPackage,
|
|
844
|
+
description: "Generating Prisma migrator package",
|
|
845
|
+
emoji: "📦",
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
type: "text",
|
|
849
|
+
filename: "prisma/schema.prisma",
|
|
850
|
+
content: PRISMA_SCHEMA_TEMPLATE,
|
|
851
|
+
description: "Generating Prisma schema",
|
|
852
|
+
emoji: "🧬",
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
type: "text",
|
|
856
|
+
filename: "prisma.config.ts",
|
|
857
|
+
content: PRISMA_CONFIG_TEMPLATE,
|
|
858
|
+
description: "Generating Prisma config",
|
|
859
|
+
emoji: "⚙️",
|
|
860
|
+
},
|
|
861
|
+
]);
|
|
862
|
+
writeFilesWithProgress(fullPath, [
|
|
863
|
+
{
|
|
864
|
+
type: "text",
|
|
865
|
+
filename: "prisma/schema.prisma",
|
|
866
|
+
content: PRISMA_SCHEMA_TEMPLATE,
|
|
867
|
+
description: "Generating Prisma schema",
|
|
868
|
+
emoji: "🧬",
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
type: "text",
|
|
872
|
+
filename: "prisma.config.ts",
|
|
873
|
+
content: PRISMA_CONFIG_TEMPLATE,
|
|
874
|
+
description: "Generating Prisma config",
|
|
875
|
+
emoji: "⚙️",
|
|
876
|
+
},
|
|
877
|
+
]);
|
|
878
|
+
}
|
|
779
879
|
console.log(chalk.green("\n✅ Resource created successfully!"));
|
|
780
880
|
console.log(chalk.gray(`\nLocation: ${fullPath}`));
|
|
781
881
|
const discoveryPaths = readDiscoveryPaths(projectRoot);
|
package/cli/dist/commands/ui.js
CHANGED
|
@@ -2,31 +2,48 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { Box, render, Text, useApp, useInput, useStdin, useStdout } from "ink";
|
|
4
4
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
5
|
-
import { DetailPanel, FileTree, ResourceSelectInput, ResourceTable, TabBar, } from "../components/index.js";
|
|
5
|
+
import { DetailPanel, FileTree, ResourceSelectInput, ResourceTable, TabBar, TUIHeader, } from "../components/index.js";
|
|
6
|
+
import { TABS } from "../components/TabBar.js";
|
|
7
|
+
import { createTUITheme, TUIThemeContext, useTUITheme } from "../components/ui-theme.js";
|
|
6
8
|
import { errorFactories, requireProjectRoot } from "../utils/errors.js";
|
|
7
|
-
import { findProjectRoot } from "../utils/paths.js";
|
|
9
|
+
import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
|
|
8
10
|
import { describeSearch } from "../utils/search-status.js";
|
|
9
11
|
import { clearMetadataCache, discoverResources, discoverStacks, getResourceMetadata, getStackMetadata, } from "../utils/services.js";
|
|
10
12
|
import { createStatusMessageController } from "../utils/status-message.js";
|
|
11
13
|
import { getListRowFromMouseY, getTerminalRuleWidth } from "../utils/terminal-layout.js";
|
|
12
14
|
import { isTiltAvailable } from "../utils/tilt.js";
|
|
13
15
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
14
|
-
const HelpPanel = ({ onClose }) =>
|
|
16
|
+
const HelpPanel = ({ onClose }) => {
|
|
17
|
+
const theme = useTUITheme();
|
|
18
|
+
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
|
|
19
|
+
? " [UP/DOWN] or j/k Navigate list items"
|
|
20
|
+
: " \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..." }) })] }));
|
|
21
|
+
};
|
|
15
22
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
23
|
+
const ASCII_SPINNER_FRAMES = ["-", "/", "|", "\\"];
|
|
16
24
|
export const LoadingScreen = ({ message = "Discovering resources...", animated = true, }) => {
|
|
25
|
+
const theme = useTUITheme();
|
|
26
|
+
const spinnerFrames = theme.ascii ? ASCII_SPINNER_FRAMES : SPINNER_FRAMES;
|
|
17
27
|
const [frame, setFrame] = useState(0);
|
|
18
28
|
useEffect(() => {
|
|
19
29
|
if (!animated)
|
|
20
30
|
return;
|
|
21
|
-
const timer = setInterval(() => setFrame((f) => (f + 1) %
|
|
31
|
+
const timer = setInterval(() => setFrame((f) => (f + 1) % spinnerFrames.length), 80);
|
|
22
32
|
return () => clearInterval(timer);
|
|
23
|
-
}, [animated]);
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 2, children: [
|
|
33
|
+
}, [animated, spinnerFrames.length]);
|
|
34
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: theme.accent, children: [theme.bannerStart, " TDK NEON EDITION ", theme.bannerEnd] }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: theme.foreground, children: [animated && _jsxs(Text, { color: theme.accent, children: [spinnerFrames[frame], " "] }), message] })] }));
|
|
25
35
|
};
|
|
26
36
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
27
|
-
const ErrorScreen = ({ error, onRetry }) =>
|
|
28
|
-
const
|
|
29
|
-
|
|
37
|
+
const ErrorScreen = ({ error, onRetry }) => {
|
|
38
|
+
const theme = useTUITheme();
|
|
39
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: theme.error, children: "Could Not Load Services" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: theme.error, children: [theme.ascii ? "[x]" : "\u2717", " ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: theme.muted, children: "Troubleshooting:" }), _jsx(Text, { color: theme.muted, children: " 1. Check that every service.json is valid JSON" }), _jsx(Text, { color: theme.muted, children: " 2. Run tdk from your project (the folder with the Tiltfile)" }), _jsx(Text, { color: theme.muted, children: " 3. Try: tdk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: theme.accent, children: "Press [r] to retry or [q] to quit" })] }));
|
|
40
|
+
};
|
|
41
|
+
const EmptyState = ({ message }) => {
|
|
42
|
+
const theme = useTUITheme();
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: theme.warning, children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: theme.muted, children: [theme.ascii ? "[*]" : "\u25c9", " No service.json files found"] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: theme.foreground, children: "To get started:" }), _jsx(Text, { color: theme.foreground, children: " 1. Run: tdk project" }), _jsx(Text, { color: theme.foreground, children: " 2. Run: tdk resource api --type backend" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: theme.accent, children: "Press [r] to refresh or [q] to quit" }), message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: theme.accent, children: [theme.bannerStart, " ", message, " ", theme.bannerEnd] }) }))] }));
|
|
44
|
+
};
|
|
45
|
+
export const TUIApp = ({ animated = true }) => {
|
|
46
|
+
const theme = useTUITheme();
|
|
30
47
|
const { exit } = useApp();
|
|
31
48
|
const { stdout } = useStdout();
|
|
32
49
|
const { stdin, setRawMode } = useStdin();
|
|
@@ -39,9 +56,17 @@ const TUIApp = ({ animated = true }) => {
|
|
|
39
56
|
const [searchQuery, setSearchQuery] = useState("");
|
|
40
57
|
const [isSearching, setIsSearching] = useState(false);
|
|
41
58
|
const [terminalWidth, setTerminalWidth] = useState(stdout.columns || 120);
|
|
59
|
+
const [terminalRows, setTerminalRows] = useState(stdout.rows || 24);
|
|
42
60
|
const [selectedFile, setSelectedFile] = useState(null);
|
|
43
61
|
const [mouseEnabled, setMouseEnabled] = useState(true);
|
|
44
62
|
const [listTop, setListTop] = useState(null);
|
|
63
|
+
const [listStart, setListStart] = useState(0);
|
|
64
|
+
// Reserve the header, tab bar, hints and footer; keep the cursor visible.
|
|
65
|
+
const pageSize = Math.max(1, terminalRows - 14);
|
|
66
|
+
const handleListLayout = useCallback((top, firstVisible = 0) => {
|
|
67
|
+
setListTop(top);
|
|
68
|
+
setListStart(firstVisible);
|
|
69
|
+
}, []);
|
|
45
70
|
const [loading, setLoading] = useState(true);
|
|
46
71
|
const [error, setError] = useState(null);
|
|
47
72
|
const [showTooltips, setShowTooltips] = useState(true);
|
|
@@ -161,7 +186,7 @@ const TUIApp = ({ animated = true }) => {
|
|
|
161
186
|
}
|
|
162
187
|
return [];
|
|
163
188
|
}, [activeTab, filteredStacks, filteredServices, selectedStackData, selectedServiceData]);
|
|
164
|
-
const items = getItems
|
|
189
|
+
const items = useMemo(getItems, [getItems]);
|
|
165
190
|
const selectableListVisible = !loading &&
|
|
166
191
|
!error &&
|
|
167
192
|
!showHelp &&
|
|
@@ -244,9 +269,10 @@ const TUIApp = ({ animated = true }) => {
|
|
|
244
269
|
const isLeftClick = (btn & 0b11) === 0;
|
|
245
270
|
if (isLeftClick && !release) {
|
|
246
271
|
const listRow = selectableListVisible && listTop !== null ? getListRowFromMouseY(y, listTop) : -1;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
272
|
+
const itemIndex = listStart + listRow;
|
|
273
|
+
if (listRow >= 0 && listRow < pageSize && itemIndex < items.length) {
|
|
274
|
+
setHighlightedIndex(itemIndex);
|
|
275
|
+
const item = items[itemIndex];
|
|
250
276
|
if (item) {
|
|
251
277
|
handleSelect(item);
|
|
252
278
|
}
|
|
@@ -260,10 +286,20 @@ const TUIApp = ({ animated = true }) => {
|
|
|
260
286
|
stdin.off("data", handleMouseData);
|
|
261
287
|
stdin.removeAllListeners("data");
|
|
262
288
|
};
|
|
263
|
-
}, [
|
|
289
|
+
}, [
|
|
290
|
+
stdin,
|
|
291
|
+
items,
|
|
292
|
+
listTop,
|
|
293
|
+
listStart,
|
|
294
|
+
pageSize,
|
|
295
|
+
selectableListVisible,
|
|
296
|
+
mouseEnabled,
|
|
297
|
+
handleSelect,
|
|
298
|
+
]);
|
|
264
299
|
const handleResize = useCallback(() => {
|
|
265
300
|
setTerminalWidth(stdout.columns || 120);
|
|
266
|
-
|
|
301
|
+
setTerminalRows(stdout.rows || 24);
|
|
302
|
+
}, [stdout]);
|
|
267
303
|
useEffect(() => {
|
|
268
304
|
stdout.on("resize", handleResize);
|
|
269
305
|
return () => {
|
|
@@ -367,7 +403,7 @@ const TUIApp = ({ animated = true }) => {
|
|
|
367
403
|
return;
|
|
368
404
|
}
|
|
369
405
|
if (key.tab) {
|
|
370
|
-
const tabs =
|
|
406
|
+
const tabs = TABS.map((tab) => tab.id);
|
|
371
407
|
const currentIdx = tabs.indexOf(activeTab);
|
|
372
408
|
const nextIdx = key.shift
|
|
373
409
|
? (currentIdx - 1 + tabs.length) % tabs.length
|
|
@@ -375,23 +411,26 @@ const TUIApp = ({ animated = true }) => {
|
|
|
375
411
|
setActiveTab(tabs[nextIdx]);
|
|
376
412
|
return;
|
|
377
413
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
"2": "resources",
|
|
382
|
-
"3": "events",
|
|
383
|
-
"4": "files",
|
|
384
|
-
"5": "config",
|
|
385
|
-
};
|
|
386
|
-
setActiveTab(tabMap[input]);
|
|
414
|
+
const shortcutTab = TABS.find((tab) => tab.shortcut === input);
|
|
415
|
+
if (shortcutTab) {
|
|
416
|
+
setActiveTab(shortcutTab.id);
|
|
387
417
|
return;
|
|
388
418
|
}
|
|
389
|
-
|
|
419
|
+
const plainKey = !key.ctrl && !key.meta;
|
|
420
|
+
if (items.length > 0 && (key.upArrow || (plainKey && input === "k"))) {
|
|
390
421
|
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
|
|
391
422
|
}
|
|
392
|
-
if (key.downArrow) {
|
|
423
|
+
if (items.length > 0 && (key.downArrow || (plainKey && input === "j"))) {
|
|
393
424
|
setHighlightedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
|
|
394
425
|
}
|
|
426
|
+
if (key.home || (plainKey && input === "g"))
|
|
427
|
+
setHighlightedIndex(0);
|
|
428
|
+
if (key.end || (plainKey && input === "G"))
|
|
429
|
+
setHighlightedIndex(Math.max(0, items.length - 1));
|
|
430
|
+
if (key.pageUp)
|
|
431
|
+
setHighlightedIndex((prev) => Math.max(0, prev - pageSize));
|
|
432
|
+
if (key.pageDown)
|
|
433
|
+
setHighlightedIndex((prev) => Math.min(Math.max(0, items.length - 1), prev + pageSize));
|
|
395
434
|
if (key.return || input === " ") {
|
|
396
435
|
const currentItem = items[highlightedIndex];
|
|
397
436
|
if (currentItem) {
|
|
@@ -433,25 +472,48 @@ const TUIApp = ({ animated = true }) => {
|
|
|
433
472
|
if (error) {
|
|
434
473
|
return _jsx(ErrorScreen, { error: error, onRetry: refresh });
|
|
435
474
|
}
|
|
475
|
+
const header = (_jsxs(_Fragment, { children: [_jsx(TUIHeader, { projectRoot: projectRoot, resourceCount: services.length, terminalWidth: terminalWidth, version: getPackageVersion() }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.muted, children: (theme.ascii ? "-" : "\u2500").repeat(getTerminalRuleWidth(terminalWidth)) }) })] }));
|
|
436
476
|
if (services.length === 0) {
|
|
437
|
-
return _jsx(EmptyState, { message: message });
|
|
477
|
+
return (_jsxs(Box, { flexDirection: "column", height: stdout.rows || 24, children: [header, _jsx(EmptyState, { message: message })] }));
|
|
438
478
|
}
|
|
439
|
-
return (_jsxs(Box, { flexDirection: "column", height: stdout.rows || 24, children: [
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
? `Service "${selectedService}" │ [Space] Expand │ [Esc] Back │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help`
|
|
449
|
-
: activeTab === "files"
|
|
450
|
-
? `Select service to view files │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help`
|
|
451
|
-
: activeTab === "config"
|
|
452
|
-
? `View configurations │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help`
|
|
453
|
-
: `[Tab] Next │ [1-5] Tabs │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help │ [q] Quit` }) })), showHelp && (_jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsx(HelpPanel, { onClose: () => setShowHelp(false) }) })), !showHelp && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(TabBar, { activeTab: activeTab, onTabChange: setActiveTab, terminalWidth: terminalWidth }) }), _jsxs(Box, { flexDirection: "row", paddingX: 1, flexGrow: 1, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, width: mainPanelWidth, children: [activeTab === "overview" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "gray", children: "\u250C\u2500 Stacks \u2500" }) }), _jsx(Box, { marginTop: 1, flexGrow: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, width: mainPanelWidth, onLayout: setListTop }) })] })), activeTab === "resources" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "gray", children: "\u250C\u2500 Resources \u2500" }) }), selectedStackData ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "gray", children: ["Stack: ", selectedStackData.stack.name] }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceTable, { resources: selectedStackData.metadata.resources, maxWidth: terminalWidth - (showSidebar ? 50 : 10) }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: "Select a stack to view resources" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, width: mainPanelWidth, onLayout: setListTop }) })] }))] })), activeTab === "events" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "gray", children: "\u250C\u2500 Events \u2500" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Events tab not yet implemented" }) })] })), activeTab === "files" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "gray", children: "\u250C\u2500 Autogenerated Files \u2500" }) }), selectedServiceData ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "gray", children: ["Service: ", selectedServiceData.service.name] }), _jsx(Box, { marginTop: 1, children: _jsx(FileTree, { nodes: fileTreeNodes, selectedPath: selectedFile || undefined }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: "Select a service to view files" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, width: mainPanelWidth, onLayout: setListTop }) })] }))] })), activeTab === "config" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "gray", children: "\u250C\u2500 Configuration \u2500" }) }), selectedServiceData ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: selectedServiceData.service.configPath }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsx(Text, { color: "gray", wrap: "wrap", children: JSON.stringify(selectedServiceData.service.config, null, 2).slice(0, 1000) }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: "Select a service to view configuration" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, width: mainPanelWidth, onLayout: setListTop }) })] }))] }))] }), showSidebar && (_jsx(Box, { marginLeft: 2, children: _jsx(DetailPanel, { stack: selectedStackData?.stack || null, service: selectedServiceData?.service || null, stackMetadata: selectedStackData?.metadata || null, visible: !!selectedStack || !!selectedService }) }))] }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, height: 3, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: "cyan", bold: true, children: ["\u2593\u2592\u2591 ", activeTab] }), _jsxs(Text, { color: "green", children: ["\u25CF ", services.filter((s) => s.stack).length, " in stack"] }), _jsxs(Text, { color: "yellow", children: ["\u25CB ", services.filter((s) => !s.stack).length, " no stack"] })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: "gray", children: ["Stacks: ", stacks.length] }), _jsxs(Text, { color: "gray", children: ["Services: ", services.length] }), _jsxs(Text, { color: "gray", children: ["\uD83D\uDDB1\uFE0F ", mouseEnabled ? "ON" : "OFF", " \u2502 \u2139\uFE0F ", showTooltips ? "ON" : "OFF", " \u2502", _jsx(Text, { color: showEnabledOnly ? "green" : "yellow", children: showEnabledOnly ? "✓ enabled" : "✓ all" }), " │ ", "[?] Help \u2502 [q] Quit"] })] })] })] }))] }));
|
|
479
|
+
return (_jsxs(Box, { flexDirection: "column", height: stdout.rows || 24, children: [header, isSearching && (_jsxs(Box, { paddingX: 1, height: 1, children: [_jsxs(Text, { color: theme.warning, children: ["Search: ", searchQuery, "_"] }), searchStatus && (_jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: ` ${searchStatus.summary}` }))] })), searchStatus?.emptyMessage && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: theme.muted, children: searchStatus.emptyMessage }) })), !isSearching && message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: theme.accent, children: [theme.bannerStart, " ", message, " ", theme.bannerEnd] }) })), !isSearching && !message && showTooltips && !showHelp && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: createHelpHint(activeTab, selectedStack, selectedService, showEnabledOnly, theme.ascii) }) })), showHelp && (_jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsx(HelpPanel, { onClose: () => setShowHelp(false) }) })), !showHelp && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(TabBar, { activeTab: activeTab, onTabChange: setActiveTab, terminalWidth: terminalWidth }) }), _jsxs(Box, { flexDirection: "row", paddingX: 1, flexGrow: 1, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, width: mainPanelWidth, children: [activeTab === "overview" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: theme.ascii ? "[ Stacks ]" : "┌─ Stacks ─" }) }), _jsx(Box, { marginTop: 1, flexGrow: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] })), activeTab === "resources" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: theme.ascii ? "[ Resources ]" : "┌─ Resources ─" }) }), selectedStackData ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["Stack: ", selectedStackData.stack.name] }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceTable, { resources: selectedStackData.metadata.resources, maxWidth: terminalWidth - (showSidebar ? 50 : 10) }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: "Select a stack to view resources" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] }))] })), activeTab === "files" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: theme.ascii ? "[ Autogenerated Files ]" : "┌─ Autogenerated Files ─" }) }), selectedServiceData ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["Service: ", selectedServiceData.service.name] }), _jsx(Box, { marginTop: 1, children: _jsx(FileTree, { nodes: fileTreeNodes, selectedPath: selectedFile || undefined }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: "Select a service to view files" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] }))] })), activeTab === "config" && (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.muted, children: theme.ascii ? "[ Configuration ]" : "┌─ Configuration ─" }) }), selectedServiceData ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.accent, children: selectedServiceData.service.configPath }), _jsx(Box, { marginTop: 1, ...(theme.ascii
|
|
480
|
+
? {}
|
|
481
|
+
: { borderStyle: "single", borderColor: theme.border }), paddingX: 1, children: _jsx(Text, { color: theme.muted, wrap: "wrap", children: JSON.stringify(selectedServiceData.service.config, null, 2).slice(0, 1000) }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: "Select a service to view configuration" }), _jsx(Box, { marginTop: 1, children: _jsx(ResourceSelectInput, { items: items, onSelect: handleSelect, highlightedIndex: highlightedIndex, isActive: !isSearching, maxVisibleItems: pageSize, width: mainPanelWidth, onLayout: handleListLayout }) })] }))] }))] }), showSidebar && (_jsx(Box, { marginLeft: 2, children: _jsx(DetailPanel, { stack: selectedStackData?.stack || null, service: selectedServiceData?.service || null, stackMetadata: selectedStackData?.metadata || null, visible: !!selectedStack || !!selectedService }) }))] }), _jsxs(Box, { ...(theme.ascii ? {} : { borderStyle: "single", borderColor: theme.border }), paddingX: 1, height: 3, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: theme.accent, bold: true, children: [theme.bannerStart, " ", activeTab] }), _jsxs(Text, { color: theme.success, children: [theme.ascii ? "[+]" : "\u25cf", " ", services.filter((s) => s.stack).length, " in stack"] }), _jsxs(Text, { color: theme.warning, children: [theme.ascii ? "-" : "\u25cb", " ", services.filter((s) => !s.stack).length, " no stack"] })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: theme.muted, children: ["Stacks: ", stacks.length] }), _jsxs(Text, { color: theme.muted, children: ["Services: ", services.length] }), _jsxs(Text, { color: theme.muted, children: [theme.ascii ? "Mouse" : "🖱️", " ", mouseEnabled ? "ON" : "OFF", theme.ascii ? " | " : " \u2502 ", theme.ascii ? "Info" : "ℹ️", " ", showTooltips ? "ON" : "OFF", theme.ascii ? " | " : " \u2502 ", _jsx(Text, { color: showEnabledOnly ? theme.success : theme.warning, children: showEnabledOnly
|
|
482
|
+
? theme.ascii
|
|
483
|
+
? "[+] enabled"
|
|
484
|
+
: "\u2713 enabled"
|
|
485
|
+
: theme.ascii
|
|
486
|
+
? "[+] all"
|
|
487
|
+
: "\u2713 all" }), theme.ascii ? " | " : " \u2502 ", "[?] Help", theme.ascii ? " | " : " \u2502 ", "[q] Quit"] })] })] })] }))] }));
|
|
454
488
|
};
|
|
489
|
+
/** Compose footer guidance for the current tab and terminal character set. */
|
|
490
|
+
function createHelpHint(activeTab, selectedStack, selectedService, showEnabledOnly, ascii) {
|
|
491
|
+
const separator = ascii ? " | " : " \u2502 ";
|
|
492
|
+
const enabledHint = `[e] ${showEnabledOnly ? "show all" : "enabled only"}`;
|
|
493
|
+
const common = [enabledHint, "[?] help"];
|
|
494
|
+
const tabRange = `${TABS[0].shortcut}-${TABS[TABS.length - 1].shortcut}`;
|
|
495
|
+
if (activeTab === "overview") {
|
|
496
|
+
const introduction = selectedStack
|
|
497
|
+
? `Stack "${selectedStack}" selected. [Enter] view`
|
|
498
|
+
: `${ascii ? "[UP/DOWN]" : "[\u2191/\u2193]"} Navigate`;
|
|
499
|
+
const controls = selectedStack ? ["[Esc] back", ...common] : ["[Enter] Select", ...common];
|
|
500
|
+
return [introduction, ...controls].join(separator);
|
|
501
|
+
}
|
|
502
|
+
if (activeTab === "resources") {
|
|
503
|
+
return ["[Tab] Tabs", "[r] Refresh", "[/] Search", ...common].join(separator);
|
|
504
|
+
}
|
|
505
|
+
if (activeTab === "files" && selectedService) {
|
|
506
|
+
return [`Service "${selectedService}"`, "[Esc] Back", ...common].join(separator);
|
|
507
|
+
}
|
|
508
|
+
if (activeTab === "files") {
|
|
509
|
+
return ["Select service to view files", ...common].join(separator);
|
|
510
|
+
}
|
|
511
|
+
if (activeTab === "config") {
|
|
512
|
+
return ["View configurations", ...common].join(separator);
|
|
513
|
+
}
|
|
514
|
+
return ["[Tab] Next", `[${tabRange}] Tabs`, ...common, "[q] Quit"].join(separator);
|
|
515
|
+
}
|
|
516
|
+
const TUIRoot = ({ animated, highContrast, }) => (_jsx(TUIThemeContext.Provider, { value: createTUITheme(Boolean(highContrast)), children: _jsx(TUIApp, { animated: animated }) }));
|
|
455
517
|
export const uiCommand = new Command("ui")
|
|
456
518
|
.description("Interactive TUI for managing stacks and services (Neon Edition)")
|
|
457
519
|
.alias("interactive")
|
|
@@ -465,6 +527,8 @@ export const uiCommand = new Command("ui")
|
|
|
465
527
|
}
|
|
466
528
|
requireProjectRoot();
|
|
467
529
|
// The app has no Static content, so its live layout starts at row 1 in the alternate screen.
|
|
468
|
-
render(_jsx(
|
|
530
|
+
render(_jsx(TUIRoot, { animated: options.animations, highContrast: options.highContrast }), {
|
|
531
|
+
alternateScreen: true,
|
|
532
|
+
});
|
|
469
533
|
});
|
|
470
534
|
//# sourceMappingURL=ui.js.map
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { useTUITheme } from "./ui-theme.js";
|
|
3
4
|
export const BaseTooltip = ({ content, shortcut, visible, maxWidth = 40, wrapText = false, prefix = "", marginTop = 0, }) => {
|
|
5
|
+
const theme = useTUITheme();
|
|
4
6
|
if (!visible)
|
|
5
7
|
return null;
|
|
6
|
-
// Wrap text to maxWidth if requested
|
|
7
8
|
const lines = wrapText ? wrapContent(content, maxWidth) : [content];
|
|
8
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor:
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.ascii ? undefined : "single", borderColor: theme.warning, paddingX: 1, paddingY: 1, backgroundColor: theme.ascii ? undefined : "black", marginTop: marginTop, children: [lines.map((line, i) => (_jsxs(Text, { color: theme.warning, bold: theme.highContrast, children: [prefix, line] }, i))), shortcut && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.muted, children: "Press " }), _jsx(Text, { color: theme.accent, bold: true, children: shortcut.startsWith("[") ? shortcut : `[${shortcut}]` }), _jsx(Text, { color: theme.muted, children: " to use" })] }))] }));
|
|
9
10
|
};
|
|
10
11
|
function wrapContent(content, maxWidth) {
|
|
11
12
|
const words = content.split(" ");
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { formatDate, getStatusColor, getStatusIcon } from "../utils/formatting.js";
|
|
4
|
+
import { getTUIStatusColor, getTUIStatusIcon, useTUITheme } from "./ui-theme.js";
|
|
4
5
|
export const DetailPanel = ({ stack, service, stackMetadata, visible, }) => {
|
|
6
|
+
const theme = useTUITheme();
|
|
5
7
|
if (!visible) {
|
|
6
8
|
return _jsx(Box, { width: 0 });
|
|
7
9
|
}
|
|
8
10
|
if (service) {
|
|
9
|
-
|
|
11
|
+
const title = theme.ascii
|
|
12
|
+
? `--- ${service.name.toUpperCase()} ---`
|
|
13
|
+
: `\u250C\u2500 ${service.name.toUpperCase()} \u2500\u2510`;
|
|
14
|
+
return (_jsxs(Box, { width: 40, flexDirection: "column", borderStyle: theme.ascii ? undefined : "single", borderColor: theme.border, paddingX: 1, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, justifyContent: "center", children: _jsx(Text, { color: theme.accent, bold: true, children: title }) }), _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Name: " }), _jsx(Text, { color: theme.foreground, children: service.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Stack: " }), _jsx(Text, { color: theme.foreground, children: service.stack || "unknown" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Type: " }), _jsx(Text, { color: theme.warning, bold: theme.highContrast, children: (service.stack || "") === "platform" ? "PLATFORM" : "PRODUCT" })] })] }), _jsx(Box, { marginTop: 2, children: _jsxs(Text, { color: theme.muted, dimColor: theme.dimMuted, children: ["Press ", _jsx(Text, { color: theme.accent, children: "[Esc]" }), " to close"] }) })] }));
|
|
10
15
|
}
|
|
11
16
|
if (stack && stackMetadata) {
|
|
12
|
-
const statusColor = getStatusColor(stackMetadata.overallStatus);
|
|
13
|
-
const statusIcon = getStatusIcon(stackMetadata.overallStatus);
|
|
14
|
-
|
|
17
|
+
const statusColor = getTUIStatusColor(theme, getStatusColor(stackMetadata.overallStatus));
|
|
18
|
+
const statusIcon = getTUIStatusIcon(theme, getStatusIcon(stackMetadata.overallStatus), getStatusColor(stackMetadata.overallStatus));
|
|
19
|
+
const title = theme.ascii
|
|
20
|
+
? `--- ${stack.name.toUpperCase()} ---`
|
|
21
|
+
: `\u250C\u2500 ${stack.name.toUpperCase()} \u2500\u2510`;
|
|
22
|
+
return (_jsxs(Box, { width: 40, flexDirection: "column", borderStyle: theme.ascii ? undefined : "single", borderColor: theme.border, paddingX: 1, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, justifyContent: "center", children: _jsx(Text, { color: theme.accent, bold: true, children: title }) }), _jsx(Box, { borderStyle: theme.ascii ? undefined : "single", borderColor: statusColor, paddingX: 1, paddingY: 1, marginY: 1, justifyContent: "center", children: _jsxs(Text, { color: statusColor, bold: true, children: [statusIcon, " ", stackMetadata.overallStatus.toUpperCase()] }) }), _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Created: " }), _jsx(Text, { color: theme.foreground, children: formatDate(stackMetadata.createdAt) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Resources: " }), _jsx(Text, { color: theme.accent, bold: theme.highContrast, children: stackMetadata.resourceCount })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: theme.muted, underline: true, children: "Resources" }) }), _jsx(Box, { flexDirection: "column", children: stack.resources.map((svc, index) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: theme.ascii
|
|
23
|
+
? index === stack.resources.length - 1
|
|
24
|
+
? "L- "
|
|
25
|
+
: "|- "
|
|
26
|
+
: index === stack.resources.length - 1
|
|
27
|
+
? "\u2514\u2500 "
|
|
28
|
+
: "\u251C\u2500 " }), _jsx(Text, { color: theme.foreground, children: svc.name })] }, svc.name))) }), _jsx(Box, { marginTop: 2, children: _jsxs(Text, { color: theme.muted, dimColor: theme.dimMuted, children: ["Press ", _jsx(Text, { color: theme.accent, children: "[Esc]" }), " to close"] }) })] }));
|
|
15
29
|
}
|
|
16
|
-
return (_jsxs(Box, { width: 40, borderStyle: "single", borderColor:
|
|
30
|
+
return (_jsxs(Box, { width: 40, borderStyle: theme.ascii ? undefined : "single", borderColor: theme.border, paddingX: 2, paddingY: 2, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, justifyContent: "center", children: _jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: "Select a stack or service" }) }), _jsx(Box, { justifyContent: "center", children: _jsx(Text, { color: theme.muted, dimColor: theme.dimMuted, children: "to view details" }) })] }));
|
|
17
31
|
};
|
|
18
32
|
//# sourceMappingURL=DetailPanel.js.map
|