import React, { useEffect, useRef, useState } from "react"; import path from "node:path"; import fs from "fs-extra"; import { Text } from "ink"; import { SearchableList } from "../components/SearchableList"; import { buildDefaultCompileOutputFileNames, resolveDefaultCdsModel, scanCapProfiles, scanCapServices, type TCdsServiceDefinition, } from "../../core/cds"; import { readCache, rememberCdsModel, rememberCdsEdmxOutputFileName, rememberCdsService } from "../../core/cache"; import { runCommand } from "../../core/process"; import { resolveRepositoryPath } from "../../core/repository"; import type { InkInteractionService } from "../services/ink-interaction-service"; type TScreenProps = { service: InkInteractionService; onDone: (success: boolean) => void; maxVisibleRows?: number }; const DEFAULT_CDS_COMPILE_FORMATS = ["edmx", "edm", "csn", "json", "sql", "yaml"]; export function CdsProfilesScreen(props: { service: InkInteractionService; onDone: (success: boolean) => void }) { const startedRef = useRef(false); useEffect(() => { if (startedRef.current) return; startedRef.current = true; void (async () => { const repositoryPath = await resolveRepositoryPath(process.cwd()); const profiles = await scanCapProfiles(repositoryPath); if (profiles.length === 0) { props.service.notify({ level: "muted", message: "No CAP profiles found." }); } else { for (const profile of profiles) { props.service.notify({ level: "muted", message: profile }); } } props.onDone(true); })(); }, []); return Working…; } export function CdsServicesScreen(props: { service: InkInteractionService; onDone: (success: boolean) => void }) { const startedRef = useRef(false); useEffect(() => { if (startedRef.current) return; startedRef.current = true; void (async () => { const repositoryPath = await resolveRepositoryPath(process.cwd()); const services = await scanCapServices(repositoryPath); if (services.length === 0) { props.service.notify({ level: "muted", message: "No CAP services found." }); } else { for (const svc of services) { props.service.notify({ level: "muted", message: `${svc.fullServiceName}\t${svc.relativeFilePath}` }); } } props.onDone(true); })(); }, []); return Working…; } /** * Native `cds compile`/`cds edmx`: reproduces the traditional handler's * format -> service -> model -> output-file picker sequence (own Ink * pickers — those helpers call `searchableSelectChoice`/`searchableSelectOrInput`, * raw `prompts`-based, directly), then runs the same non-inherited `cds * compile` the traditional handler uses. */ export function CdsCompileScreen(props: TScreenProps & { defaultFormat?: string }) { const [repositoryPath, setRepositoryPath] = useState(undefined); const [format, setFormat] = useState(props.defaultFormat); const [services, setServices] = useState(undefined); const [serviceDefinition, setServiceDefinition] = useState(undefined); const [serviceChosen, setServiceChosen] = useState(false); const [model, setModel] = useState(undefined); const [modelChoices, setModelChoices] = useState(undefined); const [outputFile, setOutputFile] = useState(undefined); const [outputChoices, setOutputChoices] = useState(undefined); const compileStartedRef = useRef(false); useEffect(() => { void resolveRepositoryPath(process.cwd()).then(setRepositoryPath); }, []); useEffect(() => { if (!repositoryPath || !format || services) return; void scanCapServices(repositoryPath).then(setServices); }, [repositoryPath, format]); useEffect(() => { if (!repositoryPath || !serviceChosen || modelChoices) return; void (async () => { const cache = await readCache(); const defaultModel = serviceDefinition?.relativeFilePath ?? (await resolveDefaultCdsModel(repositoryPath)); const choices = [...new Set([defaultModel, ".", ...cache.cds.models].map((value) => value?.trim() ?? "").filter(Boolean))]; setModelChoices(choices); })(); }, [repositoryPath, serviceChosen, modelChoices, serviceDefinition]); useEffect(() => { if (!repositoryPath || !model || !format || outputChoices) return; void (async () => { const cache = await readCache(); const defaults = await buildDefaultCompileOutputFileNames({ repositoryPath, serviceName: serviceDefinition?.fullServiceName, to: format }); const choices = [...new Set([...defaults, ...cache.cds.edmxOutputFileNames])]; setOutputChoices(choices); })(); }, [repositoryPath, model, format, outputChoices, serviceDefinition]); useEffect(() => { if (!repositoryPath || !model || !outputFile || compileStartedRef.current) return; compileStartedRef.current = true; void (async () => { try { await rememberCdsModel(model); await rememberCdsEdmxOutputFileName(outputFile); if (serviceDefinition) await rememberCdsService(serviceDefinition.fullServiceName); const args = ["compile", model, "--to", format!]; if (serviceDefinition) args.push("--service", serviceDefinition.fullServiceName); props.service.notify({ level: "muted", message: `Running: cds ${args.join(" ")}` }); const result = await runCommand("cds", args, { cwd: repositoryPath, reject: false }); if (result.stderr.trim()) { props.service.notify({ level: "warn", message: result.stderr }); } if (result.exitCode !== 0) { throw new Error(`cds compile failed with exit code ${result.exitCode}`); } const outputPath = path.isAbsolute(outputFile) ? outputFile : path.join(repositoryPath, outputFile); await fs.ensureDir(path.dirname(outputPath)); await fs.writeFile(outputPath, result.stdout, { encoding: "utf8" }); props.service.notify({ level: "success", message: `Metadata exported with utf8 encoding: ${outputPath}` }); props.onDone(true); } catch (error) { props.service.notify({ level: "error", message: error instanceof Error ? error.message : String(error) }); props.onDone(false); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [repositoryPath, model, outputFile]); const limit = props.maxVisibleRows !== undefined ? Math.max(1, props.maxVisibleRows - 2) : undefined; if (!repositoryPath) return Resolving repository…; if (!format) { return ( ({ title: value, value }))} limit={limit} onSubmit={setFormat} onCancel={() => props.onDone(false)} /> ); } if (!serviceChosen) { if (!services) return Scanning CAP services…; if (services.length === 0) { // No service resolvable — proceed without one, matching the // traditional handler's non-required path (edmx/edm formats would // have already required one and thrown by this point instead). setServiceChosen(true); return Working…; } return ( ({ title: svc.fullServiceName === svc.serviceName ? svc.serviceName : `${svc.fullServiceName} (${svc.serviceName})`, value: svc.fullServiceName, description: svc.relativeFilePath, }))} limit={limit} onSubmit={(value) => { setServiceDefinition(services.find((svc) => svc.fullServiceName === value)); setServiceChosen(true); }} onCancel={() => props.onDone(false)} /> ); } if (!model) { if (!modelChoices) return Resolving model…; return ( ({ title: value, value }))} allowCustomValue customValueTitle={(value) => `Use typed model/path: ${value}`} limit={limit} onSubmit={setModel} onCancel={() => props.onDone(false)} /> ); } if (!outputFile) { if (!outputChoices) return Resolving output file…; return ( ({ title: value, value }))} allowCustomValue customValueTitle={(value) => `Use typed output file: ${value}`} limit={limit} onSubmit={setOutputFile} onCancel={() => props.onDone(false)} /> ); } return Working…; } /** `cds edmx` is a fixed-format shortcut alias of `cds compline --to edmx` — same screen, format never prompted. */ export function CdsEdmxScreen(props: TScreenProps) { return ; }