import { Command } from 'commander'; import fs from 'fs'; import { listComponents, normalizeName, searchPuller, searchPusher, searchWebhook, } from '../../../../helpers/profileGeneratorHelper'; import { parse, stringify } from 'yaml'; import inquirer from 'inquirer'; import { paddingList } from '../../../../helpers/paddingList'; import { stepTypeTemplates, stepYaml, } from '../../../../components/stepTemplate'; import { mergeVariables, readVariableValue, } from '../../../../helpers/variablesHelper'; import { WORKER_STAGES } from '@beehexa/hexasync-template-worker-flow'; async function getEntity(entityType: string, name: string) { switch (entityType) { case 'puller': return await searchPuller(name); case 'pusher': return await searchPusher(name); case 'webhook': return await searchWebhook(name); default: console.error(`The requested entity type ${entityType} is not supported`); return null; } } async function askForWebhookFlow( selectedFlow: string, yml: any, ): Promise<{ flowName: string; flow: Array } | null> { const events = Object.keys(yml.targets || {}); if (events.length === 0) { throw new Error(`The selected webhook does not have any event associated.`); } if (events.length === 1) { return { flowName: events[0], flow: yml.targets[events[0]]?.pullSteps || [], }; } const eventNames = events.map((e, i) => `${i + 1}. ${e}`); console.log(`Please choose one of the events below to add a step ${paddingList(eventNames)}`); const answer = await inquirer.prompt<{ value: string }>([ { type: 'input', name: 'value', message: `Answer (default 1):`, }, ]); const index = parseInt(answer.value, 10); if (!index) { throw new Error('Please enter a valid input'); } const selectedEvent = events[index - 1]; return { flowName: selectedEvent, flow: yml.targets[selectedEvent]?.pullSteps || [], }; } async function askForFlow( entityType: string, yml: any, ): Promise<{ flowName: string; flow: Array } | null> { let flows: Array = []; /** * ⛔ Derived from `WORKER_STAGES` since 2026-08-12 (Epic 4 close review), not hand-listed. * * This was the SIXTH hand-kept stage list of the epic, and the one the sweep missed: `finalSteps` was added to the * schema (BUNDLE_VERSION 41→42), `WORKER_STAGES`, the report's phase counts, `duplicates.ts`, `reference.ts` and * `flow.ts` — and the CLI's own step generator still could not target the stage the schema accepts, the validator * checks and the diagram draws. Reading the table means the next stage added arrives here for free. */ switch (entityType) { case 'puller': flows = WORKER_STAGES.pullers.map((stage) => stage.key); break; case 'pusher': flows = WORKER_STAGES.pushers.map((stage) => stage.key); break; case 'webhook': flows = ['pullSteps']; break; default: throw new Error( `Could not determined the flow to add the step. The entity type ${entityType} is not supported.`, ); } let selectedFlow = flows[0]; switch (entityType) { case 'puller': case 'pusher': const flowNames = flows.map((f, i) => `${i + 1}. ${f}`); console.log(`Please select a flow for the step to be added: ${paddingList(flowNames)}`); const answer = await inquirer.prompt<{ value: string }>([ { type: 'input', name: 'value', message: `Answer (default 1):`, }, ]); const index = parseInt(answer.value, 10); if (!index) { throw new Error('Please enter a valid input'); } selectedFlow = flows[index - 1]; return { flowName: selectedFlow, flow: [...yml[selectedFlow]] }; case 'webhook': return askForWebhookFlow(selectedFlow, yml); default: break; } return null; } async function addStep(entityType: string, yml: any, stepYml: any) { const { flowName, flow }: any = await askForFlow(entityType, yml); if (!flow) { throw new Error(`The selected flow ${flow} is invalid`); } const existsKey = flow.find( (f) => f.key.toLowerCase() === stepYml.key.toLowerCase(), ); if (existsKey) { throw new Error(`There is a step with the same key ${stepYml.key}`); } flow.push(stepYml); switch (entityType) { case 'puller': case 'pusher': yml[flowName] = flow; break; case 'webhook': const event = yml.targets[flowName]; yml.targets[flowName] = { ...event, pullSteps: flow, }; break; default: break; } return yml; } async function getConnectionId() { const connectionYmls = listComponents( 'Connection', (f) => f.connectors || [], ); let connectionNames = connectionYmls.map( (c, i) => `${i + 1}. ${readVariableValue(c.name)}`, ); connectionNames = ['0. None', ...connectionNames]; console.log(`Please choose one of the connection below to assign to the step ${paddingList(connectionNames)}`); const answer = await inquirer.prompt<{ value: string }>([ { type: 'input', name: 'value', message: `Answer (default 0):`, }, ]); const index = !answer.value ? 0 : parseInt(answer.value, 10); return index === 0 ? '**SelfConnectorId**' : connectionYmls[index - 1].id; } async function generateStep( entityType: string, stepName: string, ): Promise<{ stepYml: any; variables: any[] } | null> { let stepYml = parse(stepYaml()); const normalizedName = normalizeName(stepName); // no variables stepYml.connectorId = await getConnectionId(); stepYml.key = normalizedName.underscoreSlug.toUpperCase(); stepYml.name = normalizedName.displayName; const stepTypeYmls = stepTypeTemplates(); const stepTypes = stepTypeYmls.map((x) => x.type); const displayTypeNames = stepTypes.map((t, i) => `${i + 1}. ${t}`); console.log(`Please choose one of the stepType below ${paddingList(displayTypeNames)}`); const answer = await inquirer.prompt<{ value: string }>([ { type: 'input', name: 'value', message: `Answer (default 1):`, }, ]); const index = parseInt(answer.value, 10); if (!index) { throw new Error(`Please enter a valid input.`); } stepYml.displayType = stepTypes[index - 1]; const choseStepType = stepTypeYmls[index - 1]; const stepYmlSample = await choseStepType.getSample(entityType); stepYml = { ...stepYml, ...stepYmlSample, }; console.log(`Usages: ${choseStepType.usage(entityType, stepYml.key)}`); return { stepYml, variables: [] }; } export function GenerateStepCommand(): Command { const cmd = new Command('generate') .alias('g') .description('Add a HexaSync Step to Puller, Pusher, or Webhook') .option( '-t, --entity-type ', 'Search entity by type, e.g.: puller, pusher, webhook', ) .option( '-e, --entity-name ', 'Search for name of the pusher, e.g.: product', ) .option( '-n, --step-name ', "Search for name of the pusher, e.g.: 'check exists', 'create', 'update'", ) .action(async ({ entityType, entityName, stepName }) => { let yml: any = await getEntity(entityType, entityName); if (!yml?.__file_path) { console.error(`Could not find ${entityType} name '${entityName}'`); return; } const filePath = yml.__file_path; const stepConfig = await generateStep(entityType, stepName); if (!stepConfig) { console.error(`Could not generate step because of unknown reason.`); return; } yml = await addStep(entityType, yml, stepConfig.stepYml); delete yml.__file_path; delete yml.__real_name; mergeVariables(stepConfig.variables || []); switch (entityType) { case 'puller': fs.writeFileSync( filePath, stringify( { pullers: [yml] }, { keepSourceTokens: true, lineWidth: 0 }, ), ); break; case 'pusher': fs.writeFileSync( filePath, stringify( { pushers: [yml] }, { keepSourceTokens: true, lineWidth: 0 }, ), ); break; case 'webhook': fs.writeFileSync( filePath, stringify( { webhooks: [yml] }, { keepSourceTokens: true, lineWidth: 0 }, ), ); break; } }); return cmd; }