diff --git a/.gitignore b/.gitignore index d661d4c01a1..7fb7a1fbaa0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ __snapshots__ .genenv.* tilt_config.json /.idea +hubspot.config.yml diff --git a/integrations/hubspot/definitions/actions/contact.ts b/integrations/hubspot/definitions/actions/contact.ts index 6227367edc0..24d7c602ef8 100644 --- a/integrations/hubspot/definitions/actions/contact.ts +++ b/integrations/hubspot/definitions/actions/contact.ts @@ -16,11 +16,6 @@ const searchContact: ActionDefinition = { schema: z.object({ email: z.string().optional().title('Email').describe('The email of the contact to search for'), phone: z.string().optional().title('Phone').describe('The phone number of the contact to search for'), - properties: z - .array(z.string()) - .optional() - .title('Property Names') - .describe('The properties to include in the response'), }), }, output: { @@ -85,11 +80,6 @@ const getContact: ActionDefinition = { input: { schema: z.object({ contactIdOrEmail: z.string().title('Contact ID or Email').describe('The ID or email of the contact to get'), - properties: z - .array(z.string()) - .optional() - .title('Properties') - .describe('The properties to include in the response'), }), }, output: { @@ -151,11 +141,6 @@ const listContacts: ActionDefinition = { description: 'List contacts in Hubspot', input: { schema: z.object({ - properties: z - .array(z.string()) - .optional() - .title('Properties') - .describe('The properties to include in the response'), meta: z .object({ nextToken: z diff --git a/integrations/hubspot/definitions/actions/deal.ts b/integrations/hubspot/definitions/actions/deal.ts index 094e7d34a33..864bfda7431 100644 --- a/integrations/hubspot/definitions/actions/deal.ts +++ b/integrations/hubspot/definitions/actions/deal.ts @@ -14,11 +14,6 @@ const searchDeal: ActionDefinition = { input: { schema: z.object({ name: z.string().optional().title('Name').describe('The name of the deal to search for'), - properties: z - .array(z.string()) - .optional() - .title('Properties') - .describe('The properties to include in the response'), }), }, output: { @@ -59,11 +54,6 @@ const getDeal: ActionDefinition = { input: { schema: z.object({ dealId: z.string().title('Deal ID').describe('The ID of the deal to get'), - properties: z - .array(z.string()) - .optional() - .title('Properties') - .describe('The properties to include in the response'), }), }, output: { diff --git a/integrations/hubspot/definitions/actions/lead.ts b/integrations/hubspot/definitions/actions/lead.ts index ec7e7d7aef6..e09befba15d 100644 --- a/integrations/hubspot/definitions/actions/lead.ts +++ b/integrations/hubspot/definitions/actions/lead.ts @@ -14,11 +14,6 @@ const searchLead: ActionDefinition = { input: { schema: z.object({ name: z.string().optional().title('Name').describe('The name of the lead to search for'), - properties: z - .array(z.string()) - .optional() - .title('Properties') - .describe('The properties to include in the response'), }), }, output: { @@ -62,11 +57,6 @@ const getLead: ActionDefinition = { input: { schema: z.object({ leadId: z.string().title('Lead ID').describe('The ID of the lead to get'), - properties: z - .array(z.string()) - .optional() - .title('Properties') - .describe('The properties to include in the response'), }), }, output: { diff --git a/integrations/hubspot/integration.definition.ts b/integrations/hubspot/integration.definition.ts index 39dd22481ad..4a0b5baa5ed 100644 --- a/integrations/hubspot/integration.definition.ts +++ b/integrations/hubspot/integration.definition.ts @@ -5,7 +5,7 @@ export default new IntegrationDefinition({ name: 'hubspot', title: 'HubSpot', description: 'Manage contacts, tickets and more from your chatbot.', - version: '3.0.0', + version: '4.0.0', readme: 'hub.md', icon: 'icon.svg', configuration: { diff --git a/integrations/hubspot/src/actions/contact.ts b/integrations/hubspot/src/actions/contact.ts index 3907dbad71f..8f6b57f8de9 100644 --- a/integrations/hubspot/src/actions/contact.ts +++ b/integrations/hubspot/src/actions/contact.ts @@ -16,6 +16,11 @@ const _mapHsContactToBpContact = (hsContact: HsContact): BpContact => ({ updatedAt: hsContact.updatedAt.toISOString(), }) +const _getContactPropertyKeys = async (hsClient: HubspotClient) => { + const properties = await hsClient.getAllObjectProperties('contacts') + return properties.results.map((property) => property.name) +} + export const searchContact: bp.IntegrationProps['actions']['searchContact'] = async ({ client, ctx, @@ -27,16 +32,17 @@ export const searchContact: bp.IntegrationProps['actions']['searchContact'] = as const phoneStr = input.phone ? `phone ${input.phone}` : 'unknown phone' const emailStr = input.email ? `email ${input.email}` : 'unknown email' const infosStr = `${phoneStr} and ${emailStr}` + const propertyKeys = await _getContactPropertyKeys(hsClient) logger .forBot() .debug( - `Searching for contact with ${infosStr} ${input.properties?.length ? `and properties ${input.properties?.join(', ')}` : ''}` + `Searching for contact with ${infosStr} ${propertyKeys?.length ? `and properties ${propertyKeys?.join(', ')}` : ''}` ) const contact = await hsClient.searchContact({ phone: input.phone, email: input.email, - propertiesToReturn: input.properties, + propertiesToReturn: propertyKeys, }) return { @@ -63,9 +69,11 @@ export const createContact: bp.IntegrationProps['actions']['createContact'] = as export const getContact: bp.IntegrationProps['actions']['getContact'] = async ({ ctx, client, input }) => { const hsClient = await getAuthenticatedHubspotClient({ ctx, client }) + + const propertyKeys = await _getContactPropertyKeys(hsClient) const contact = await hsClient.getContact({ contactId: input.contactIdOrEmail, - propertiesToReturn: input.properties, + propertiesToReturn: propertyKeys, }) return { contact: _mapHsContactToBpContact(contact), @@ -101,8 +109,9 @@ export const deleteContact: bp.IntegrationProps['actions']['deleteContact'] = as export const listContacts: bp.IntegrationProps['actions']['listContacts'] = async ({ ctx, client, input }) => { const hsClient = await getAuthenticatedHubspotClient({ ctx, client }) + const propertyKeys = await _getContactPropertyKeys(hsClient) const { contacts, nextToken } = await hsClient.listContacts({ - properties: input.properties, + properties: propertyKeys, nextToken: input.meta.nextToken, }) return { diff --git a/integrations/hubspot/src/actions/deal.ts b/integrations/hubspot/src/actions/deal.ts index 25672724b9f..fee446598b1 100644 --- a/integrations/hubspot/src/actions/deal.ts +++ b/integrations/hubspot/src/actions/deal.ts @@ -15,10 +15,16 @@ const _mapHsDealToBpDeal = (hsDeal: HsDeal): BpDeal => ({ properties: hsDeal.properties, }) +const _getDealPropertyKeys = async (hsClient: HubspotClient) => { + const properties = await hsClient.getAllObjectProperties('deals') + return properties.results.map((property) => property.name) +} + export const searchDeal: bp.IntegrationProps['actions']['searchDeal'] = async ({ client, ctx, input }) => { const hsClient = await getAuthenticatedHubspotClient({ client, ctx }) + const propertyKeys = await _getDealPropertyKeys(hsClient) - const deal = await hsClient.searchDeal({ name: input.name, propertiesToReturn: input.properties }) + const deal = await hsClient.searchDeal({ name: input.name, propertiesToReturn: propertyKeys }) return { deal: _mapHsDealToBpDeal(deal), @@ -40,8 +46,9 @@ export const createDeal: bp.IntegrationProps['actions']['createDeal'] = async ({ export const getDeal: bp.IntegrationProps['actions']['getDeal'] = async ({ client, ctx, input }) => { const hsClient = await getAuthenticatedHubspotClient({ client, ctx }) + const propertyKeys = await _getDealPropertyKeys(hsClient) - const deal = await hsClient.getDealById({ dealId: input.dealId, propertiesToReturn: input.properties }) + const deal = await hsClient.getDealById({ dealId: input.dealId, propertiesToReturn: propertyKeys }) return { deal: _mapHsDealToBpDeal(deal), diff --git a/integrations/hubspot/src/actions/lead.ts b/integrations/hubspot/src/actions/lead.ts index 8d4cc675e09..0e78d7131fe 100644 --- a/integrations/hubspot/src/actions/lead.ts +++ b/integrations/hubspot/src/actions/lead.ts @@ -15,10 +15,16 @@ const _mapHsLeadToBpLead = (hsLead: HsLead): BpLead => ({ properties: hsLead.properties, }) +const _getLeadPropertyKeys = async (hsClient: HubspotClient) => { + const properties = await hsClient.getAllObjectProperties('leads') + return properties.results.map((property) => property.name) +} + export const searchLead: bp.IntegrationProps['actions']['searchLead'] = async ({ client, ctx, input }) => { const hsClient = await getAuthenticatedHubspotClient({ client, ctx }) + const propertyKeys = await _getLeadPropertyKeys(hsClient) - const lead = await hsClient.searchLead({ name: input.name, propertiesToReturn: input.properties }) + const lead = await hsClient.searchLead({ name: input.name, propertiesToReturn: propertyKeys }) return { lead: _mapHsLeadToBpLead(lead), @@ -41,8 +47,9 @@ export const createLead: bp.IntegrationProps['actions']['createLead'] = async ({ export const getLead: bp.IntegrationProps['actions']['getLead'] = async ({ client, ctx, input }) => { const hsClient = await getAuthenticatedHubspotClient({ client, ctx }) + const propertyKeys = await _getLeadPropertyKeys(hsClient) - const lead = await hsClient.getLeadById({ leadId: input.leadId, propertiesToReturn: input.properties }) + const lead = await hsClient.getLeadById({ leadId: input.leadId, propertiesToReturn: propertyKeys }) return { lead: _mapHsLeadToBpLead(lead), diff --git a/integrations/hubspot/src/hubspot-api/hubspot-client.ts b/integrations/hubspot/src/hubspot-api/hubspot-client.ts index 700baeb36d2..3a1c6eb6c60 100644 --- a/integrations/hubspot/src/hubspot-api/hubspot-client.ts +++ b/integrations/hubspot/src/hubspot-api/hubspot-client.ts @@ -262,6 +262,17 @@ export class HubspotClient { return newContact } + /** Gets the list of properties for a given object type. + * + * Object type examples: + * - 'contacts' + * - 'deals' + * - 'leads' + */ + public getAllObjectProperties(objectType: string) { + return this._hsClient.crm.properties.coreApi.getAll(objectType) + } + @handleErrors('Failed to get contact by ID') public async getContact({ contactId, propertiesToReturn }: { contactId: string; propertiesToReturn?: string[] }) { const allPropertiesToReturn = [...DEFAULT_CONTACT_PROPERTIES, ...(propertiesToReturn ?? [])] diff --git a/packages/cli/package.json b/packages/cli/package.json index 0a406edf55a..84b6449e9a0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cli", - "version": "4.17.15", + "version": "4.17.16", "description": "Botpress CLI", "scripts": { "build": "pnpm run build:types && pnpm run bundle && pnpm run template:gen", diff --git a/packages/cli/src/command-implementations/add-command.ts b/packages/cli/src/command-implementations/add-command.ts index f0be366831f..7a125d951aa 100644 --- a/packages/cli/src/command-implementations/add-command.ts +++ b/packages/cli/src/command-implementations/add-command.ts @@ -10,7 +10,13 @@ import * as errors from '../errors' import * as pkgRef from '../package-ref' import * as utils from '../utils' import { GlobalCommand } from './global-command' -import { ProjectCache, ProjectCommand, ProjectCommandDefinition, ProjectDefinition } from './project-command' +import { + ProjectCache, + ProjectCommand, + ProjectCommandDefinition, + ProjectDefinitionLazy, + ProjectDefinition, +} from './project-command' type InstallablePackage = | { @@ -291,7 +297,8 @@ export class AddCommand extends GlobalCommand { }> { const cmd = this._getProjectCmd(workDir) - const definition = await cmd.readProjectDefinitionFromFS().catch((thrown) => { + const { resolveProjectDefinition } = cmd.readProjectDefinitionFromFS() + const definition = await resolveProjectDefinition().catch((thrown) => { if (thrown instanceof errors.ProjectDefinitionNotFoundError) { return undefined } @@ -334,7 +341,7 @@ class _AnyProjectCommand extends ProjectCommand { throw new errors.BotpressCLIError('Not implemented') } - public async readProjectDefinitionFromFS(): Promise { + public readProjectDefinitionFromFS(): ProjectDefinitionLazy { return super.readProjectDefinitionFromFS() } diff --git a/packages/cli/src/command-implementations/build-command.ts b/packages/cli/src/command-implementations/build-command.ts index 456a97098be..98641c519d5 100644 --- a/packages/cli/src/command-implementations/build-command.ts +++ b/packages/cli/src/command-implementations/build-command.ts @@ -8,16 +8,14 @@ export type BuildCommandDefinition = typeof commandDefinitions.build export class BuildCommand extends ProjectCommand { public async run(buildContext?: utils.esbuild.IncrementalBuildContext): Promise { const t0 = Date.now() - const { type: projectType, definition: integrationDef } = await this.readProjectDefinitionFromFS() + const { projectType } = this.readProjectDefinitionFromFS() if (projectType === 'interface') { this.logger.success('Interface projects have nothing to build.') return } - if (integrationDef) { - await this._runGenerate() - } + await this._runGenerate() await this._runBundle(buildContext) const dt = Date.now() - t0 diff --git a/packages/cli/src/command-implementations/bundle-command.ts b/packages/cli/src/command-implementations/bundle-command.ts index 3a9ceecd4dc..2f60cde92f6 100644 --- a/packages/cli/src/command-implementations/bundle-command.ts +++ b/packages/cli/src/command-implementations/bundle-command.ts @@ -7,29 +7,29 @@ import { ProjectCommand } from './project-command' export type BundleCommandDefinition = typeof commandDefinitions.bundle export class BundleCommand extends ProjectCommand { public async run(buildContext?: utils.esbuild.IncrementalBuildContext): Promise { - const projectDef = await this.readProjectDefinitionFromFS() + const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS() const abs = this.projectPaths.abs const rel = this.projectPaths.rel('workDir') const line = this.logger.line() - if (projectDef.type === 'interface') { + if (projectType === 'interface') { this.logger.success('Interface projects have no implementation to bundle.') - } else if (projectDef.type === 'integration') { + } else if (projectType === 'integration') { + const projectDef = await resolveProjectDefinition() const { name, __advanced } = projectDef.definition line.started(`Bundling integration ${chalk.bold(name)}...`) await this._bundle(abs.outFileCJS, buildContext, __advanced?.esbuild ?? {}) - } else if (projectDef.type === 'bot') { + } else if (projectType === 'bot') { line.started('Bundling bot...') await this._bundle(abs.outFileCJS, buildContext) - } else if (projectDef.type === 'plugin') { + } else if (projectType === 'plugin') { line.started('Bundling plugin with platform node...') await this._bundle(abs.outFileCJS, buildContext) line.started('Bundling plugin with platform browser...') await this._bundle(abs.outFileESM, buildContext, { platform: 'browser', format: 'esm' }) } else { - type _assertion = utils.types.AssertNever throw new errors.UnsupportedProjectType() } diff --git a/packages/cli/src/command-implementations/deploy-command.ts b/packages/cli/src/command-implementations/deploy-command.ts index 83725d4214b..529235591a1 100644 --- a/packages/cli/src/command-implementations/deploy-command.ts +++ b/packages/cli/src/command-implementations/deploy-command.ts @@ -20,18 +20,22 @@ export class DeployCommand extends ProjectCommand { await this._runBuild() // This ensures the bundle is always synced with source code } - const projectDef = await this.readProjectDefinitionFromFS() + const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS() - if (projectDef.type === 'integration') { + if (projectType === 'integration') { + const projectDef = await resolveProjectDefinition() return this._deployIntegration(api, projectDef.definition) } - if (projectDef.type === 'interface') { + if (projectType === 'interface') { + const projectDef = await resolveProjectDefinition() return this._deployInterface(api, projectDef.definition) } - if (projectDef.type === 'plugin') { + if (projectType === 'plugin') { + const projectDef = await resolveProjectDefinition() return this._deployPlugin(api, projectDef.definition) } - if (projectDef.type === 'bot') { + if (projectType === 'bot') { + const projectDef = await resolveProjectDefinition() return this._deployBot(api, projectDef.definition, this.argv.botId, this.argv.createNewBot) } throw new errors.UnsupportedProjectType() diff --git a/packages/cli/src/command-implementations/dev-command.ts b/packages/cli/src/command-implementations/dev-command.ts index 94b2f6c3dc6..79b8892ca2a 100644 --- a/packages/cli/src/command-implementations/dev-command.ts +++ b/packages/cli/src/command-implementations/dev-command.ts @@ -36,10 +36,11 @@ export class DevCommand extends ProjectCommand { const api = await this.ensureLoginAndCreateClient(this.argv) - const projectDef = await this.readProjectDefinitionFromFS() - if (projectDef.type === 'interface') { + const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS() + if (projectType === 'interface') { throw new errors.BotpressCLIError('This feature is not available for interfaces.') } + const projectDef = await resolveProjectDefinition() this._initialDef = projectDef let env: Record = { @@ -189,16 +190,18 @@ export class DevCommand extends ProjectCommand { } private _deploy = async (api: apiUtils.ApiClient, tunnelUrl: string) => { - const projectDef = await this.readProjectDefinitionFromFS() + const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS() - if (projectDef.type === 'interface') { + if (projectType === 'interface') { throw new errors.BotpressCLIError('This feature is not available for interfaces.') } - if (projectDef.type === 'integration') { + if (projectType === 'integration') { + const projectDef = await resolveProjectDefinition() this._checkSecrets(projectDef.definition) return await this._deployDevIntegration(api, tunnelUrl, projectDef.definition) } - if (projectDef.type === 'bot') { + if (projectType === 'bot') { + const projectDef = await resolveProjectDefinition() return await this._deployDevBot(api, tunnelUrl, projectDef.definition) } throw new errors.UnsupportedProjectType() diff --git a/packages/cli/src/command-implementations/gen-command.ts b/packages/cli/src/command-implementations/gen-command.ts index c03e79cdd6e..0e7010cb994 100644 --- a/packages/cli/src/command-implementations/gen-command.ts +++ b/packages/cli/src/command-implementations/gen-command.ts @@ -11,18 +11,21 @@ import { ProjectCommand } from './project-command' export type GenerateCommandDefinition = typeof commandDefinitions.generate export class GenerateCommand extends ProjectCommand { public async run(): Promise { - const projectDef = await this.readProjectDefinitionFromFS() - if (projectDef.type === 'interface') { + const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS() + if (projectType === 'interface') { this.logger.success('Interface projects have no code to generate since they have no implementation.') return } - if (projectDef.type === 'integration') { + if (projectType === 'integration') { + const projectDef = await resolveProjectDefinition() return await this._generateIntegration(projectDef.definition) } - if (projectDef.type === 'bot') { + if (projectType === 'bot') { + const projectDef = await resolveProjectDefinition() return await this._generateBot(projectDef.definition) } - if (projectDef.type === 'plugin') { + if (projectType === 'plugin') { + const projectDef = await resolveProjectDefinition() return await this._generatePlugin(projectDef.definition) } throw new errors.UnsupportedProjectType() diff --git a/packages/cli/src/command-implementations/lint-command.ts b/packages/cli/src/command-implementations/lint-command.ts index f03aa4e2987..ef5c0888020 100644 --- a/packages/cli/src/command-implementations/lint-command.ts +++ b/packages/cli/src/command-implementations/lint-command.ts @@ -17,7 +17,8 @@ import { ProjectCommand } from './project-command' export type LintCommandDefinition = typeof commandDefinitions.lint export class LintCommand extends ProjectCommand { public async run(): Promise { - const projectDef = await this.readProjectDefinitionFromFS() + const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS() + const projectDef = await resolveProjectDefinition() if (projectDef.bpLintDisabled) { this.logger.warn( 'Linting is disabled for this project because of a bplint directive. To enable linting, remove the "bplint-disable" directive from the project definition file' @@ -25,13 +26,19 @@ export class LintCommand extends ProjectCommand { return } - switch (projectDef.type) { - case 'integration': + switch (projectType) { + case 'integration': { + const projectDef = await resolveProjectDefinition() return this._runLintForIntegration(projectDef.definition) - case 'bot': + } + case 'bot': { + const projectDef = await resolveProjectDefinition() return this._runLintForBot(projectDef.definition) - case 'interface': + } + case 'interface': { + const projectDef = await resolveProjectDefinition() return this._runLintForInterface(projectDef.definition) + } default: throw new errors.UnsupportedProjectType() } diff --git a/packages/cli/src/command-implementations/project-command.ts b/packages/cli/src/command-implementations/project-command.ts index 0a30e34e668..81de81d6676 100644 --- a/packages/cli/src/command-implementations/project-command.ts +++ b/packages/cli/src/command-implementations/project-command.ts @@ -33,6 +33,29 @@ export type ProjectDefinition = LintIgnoredConfig & | { type: 'plugin'; definition: sdk.PluginDefinition } ) +type ProjectDefinitionResolver = () => Promise + +export type ProjectDefinitionLazy = + | { + projectType: 'integration' + resolveProjectDefinition: ProjectDefinitionResolver<{ + type: 'integration' + definition: sdk.IntegrationDefinition + }> + } + | { + projectType: 'bot' + resolveProjectDefinition: ProjectDefinitionResolver<{ type: 'bot'; definition: sdk.BotDefinition }> + } + | { + projectType: 'interface' + resolveProjectDefinition: ProjectDefinitionResolver<{ type: 'interface'; definition: sdk.InterfaceDefinition }> + } + | { + projectType: 'plugin' + resolveProjectDefinition: ProjectDefinitionResolver<{ type: 'plugin'; definition: sdk.PluginDefinition }> + } + type UpdatedBot = client.Bot class ProjectPaths extends utils.path.PathStore { @@ -59,40 +82,76 @@ export abstract class ProjectCommand extends return new utils.cache.FSKeyValueCache(this.projectPaths.abs.projectCacheFile) } - protected async readProjectDefinitionFromFS(): Promise { - const projectPaths = this.projectPaths + private _readProjectType(projectPaths: ProjectPaths): ProjectType { + const abs = projectPaths.abs + if (fs.existsSync(abs.integrationDefinition)) { + return 'integration' + } + if (fs.existsSync(abs.interfaceDefinition)) { + return 'interface' + } + if (fs.existsSync(abs.botDefinition)) { + return 'bot' + } + if (fs.existsSync(abs.pluginDefinition)) { + return 'plugin' + } + throw new errors.UnsupportedProjectType() + } + + protected readProjectDefinitionFromFS(): ProjectDefinitionLazy { try { - const integrationDefinition = await this._readIntegrationDefinitionFromFS(projectPaths) - if (integrationDefinition) { - return { type: 'integration', ...integrationDefinition } + const type = this._readProjectType(this.projectPaths) + if (type === 'integration') { + return { + projectType: 'integration', + resolveProjectDefinition: async () => ({ + type: 'integration', + ...(await this._readIntegrationDefinitionFromFS(this.projectPaths)), + }), + } } - const interfaceDefinition = await this._readInterfaceDefinitionFromFS(projectPaths) - if (interfaceDefinition) { - return { type: 'interface', ...interfaceDefinition } + if (type === 'plugin') { + return { + projectType: 'plugin', + resolveProjectDefinition: async () => ({ + type: 'plugin', + ...(await this._readPluginDefinitionFromFS(this.projectPaths)), + }), + } } - const botDefinition = await this._readBotDefinitionFromFS(projectPaths) - if (botDefinition) { - return { type: 'bot', ...botDefinition } + if (type === 'interface') { + return { + projectType: 'interface', + resolveProjectDefinition: async () => ({ + type: 'interface', + ...(await this._readInterfaceDefinitionFromFS(this.projectPaths)), + }), + } } - const pluginDefinition = await this._readPluginDefinitionFromFS(projectPaths) - if (pluginDefinition) { - return { type: 'plugin', ...pluginDefinition } + if (type === 'bot') { + return { + projectType: 'bot', + resolveProjectDefinition: async () => ({ + type: 'bot', + ...(await this._readBotDefinitionFromFS(this.projectPaths)), + }), + } } } catch (thrown: unknown) { throw errors.BotpressCLIError.wrap(thrown, 'Error while reading project definition') } - throw new errors.ProjectDefinitionNotFoundError(this.projectPaths.abs.workDir) } private async _readIntegrationDefinitionFromFS( projectPaths: utils.path.PathStore<'workDir' | 'integrationDefinition'> - ): Promise<({ definition: sdk.IntegrationDefinition } & LintIgnoredConfig) | undefined> { + ): Promise<{ definition: sdk.IntegrationDefinition } & LintIgnoredConfig> { const abs = projectPaths.abs const rel = projectPaths.rel('workDir') if (!fs.existsSync(abs.integrationDefinition)) { - return + throw new errors.BotpressCLIError('Could not read integration definition') } const bpLintDisabled = await this._isBpLintDisabled(abs.integrationDefinition) @@ -114,12 +173,12 @@ export abstract class ProjectCommand extends private async _readInterfaceDefinitionFromFS( projectPaths: utils.path.PathStore<'workDir' | 'interfaceDefinition'> - ): Promise<({ definition: sdk.InterfaceDefinition } & LintIgnoredConfig) | undefined> { + ): Promise<{ definition: sdk.InterfaceDefinition } & LintIgnoredConfig> { const abs = projectPaths.abs const rel = projectPaths.rel('workDir') if (!fs.existsSync(abs.interfaceDefinition)) { - return + throw new errors.BotpressCLIError('Could not read interface definition') } const bpLintDisabled = await this._isBpLintDisabled(abs.interfaceDefinition) @@ -141,12 +200,12 @@ export abstract class ProjectCommand extends private async _readBotDefinitionFromFS( projectPaths: utils.path.PathStore<'workDir' | 'botDefinition'> - ): Promise<({ definition: sdk.BotDefinition } & LintIgnoredConfig) | undefined> { + ): Promise<{ definition: sdk.BotDefinition } & LintIgnoredConfig> { const abs = projectPaths.abs const rel = projectPaths.rel('workDir') if (!fs.existsSync(abs.botDefinition)) { - return + throw new errors.BotpressCLIError('Could not read bot definition') } const bpLintDisabled = await this._isBpLintDisabled(abs.botDefinition) @@ -168,12 +227,12 @@ export abstract class ProjectCommand extends private async _readPluginDefinitionFromFS( projectPaths: utils.path.PathStore<'workDir' | 'pluginDefinition'> - ): Promise<({ definition: sdk.PluginDefinition } & LintIgnoredConfig) | undefined> { + ): Promise<{ definition: sdk.PluginDefinition } & LintIgnoredConfig> { const abs = projectPaths.abs const rel = projectPaths.rel('workDir') if (!fs.existsSync(abs.pluginDefinition)) { - return + throw new errors.BotpressCLIError('Could not read plugin definition') } const bpLintDisabled = await this._isBpLintDisabled(abs.pluginDefinition) diff --git a/packages/cli/src/command-implementations/read-command.ts b/packages/cli/src/command-implementations/read-command.ts index f74c42d81b6..ccc6c14e64d 100644 --- a/packages/cli/src/command-implementations/read-command.ts +++ b/packages/cli/src/command-implementations/read-command.ts @@ -8,29 +8,32 @@ import { ProjectCommand } from './project-command' export type ReadCommandDefinition = typeof commandDefinitions.read export class ReadCommand extends ProjectCommand { public async run(): Promise { - const projectDef = await this.readProjectDefinitionFromFS() - if (projectDef.type === 'integration') { + const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS() + if (projectType === 'integration') { + const projectDef = await resolveProjectDefinition() const parsed = await this._parseIntegration(projectDef.definition) this.logger.json(parsed) return } - if (projectDef.type === 'interface') { + if (projectType === 'interface') { + const projectDef = await resolveProjectDefinition() const parsed = await this._parseInterface(projectDef.definition) this.logger.json(parsed) return } - if (projectDef.type === 'bot') { + if (projectType === 'bot') { + const projectDef = await resolveProjectDefinition() const parsed = await this._parseBot(projectDef.definition) this.logger.json(parsed) return } - if (projectDef.type === 'plugin') { + if (projectType === 'plugin') { + const projectDef = await resolveProjectDefinition() const parsed = await this._parsePlugin(projectDef.definition) this.logger.json(parsed) return } - type _assertion = utils.types.AssertNever throw new errors.BotpressCLIError('Unsupported project type') } diff --git a/packages/cli/src/command-implementations/serve-command.ts b/packages/cli/src/command-implementations/serve-command.ts index 550460a8ff2..4beb3b3fd52 100644 --- a/packages/cli/src/command-implementations/serve-command.ts +++ b/packages/cli/src/command-implementations/serve-command.ts @@ -15,12 +15,13 @@ export class ServeCommand extends ProjectCommand { throw new errors.NoBundleFoundError() } - const projectDef = await this.readProjectDefinitionFromFS() - if (projectDef.type === 'interface') { + const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS() + if (projectType === 'interface') { throw new errors.BotpressCLIError('An interface project has no implementation to serve.') } - if (projectDef.type === 'integration') { + if (projectType === 'integration') { + const projectDef = await resolveProjectDefinition() // TODO: store secrets in local cache to avoid prompting every time const secretEnvVariables = await this.promptSecrets(projectDef.definition, this.argv, { formatEnv: true }) const nonNullSecretEnvVariables = utils.records.filterValues(secretEnvVariables, utils.guards.is.notNull) @@ -29,7 +30,7 @@ export class ServeCommand extends ProjectCommand { } } - this.logger.log(`Serving ${projectDef.type}...`) + this.logger.log(`Serving ${projectType}...`) const { default: serveable } = utils.require.requireJsFile<{ default: Serveable }>(outfile) const server = await serveable.start(this.argv.port)