chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import * as client from '@botpress/client'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as errors from '../errors'
|
||||
import * as utils from '../utils'
|
||||
import * as types from './types'
|
||||
|
||||
export const prepareCreateBotBody = async (bot: sdk.BotDefinition): Promise<types.CreateBotRequestBody> => ({
|
||||
user: bot.user,
|
||||
conversation: bot.conversation,
|
||||
message: bot.message,
|
||||
recurringEvents: bot.recurringEvents,
|
||||
actions: bot.actions
|
||||
? await utils.records.mapValuesAsync(bot.actions, async (action, actionName) => ({
|
||||
...action,
|
||||
input: {
|
||||
...action.input,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(action.input, {
|
||||
useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(
|
||||
thrown,
|
||||
`Failed to convert ZUI to JSON schema for bot action ${actionName} input`
|
||||
)
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
...action.output,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(action.output, {
|
||||
useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(
|
||||
thrown,
|
||||
`Failed to convert ZUI to JSON schema for bot action ${actionName} output`
|
||||
)
|
||||
}),
|
||||
},
|
||||
}))
|
||||
: undefined,
|
||||
configuration: bot.configuration
|
||||
? {
|
||||
...bot.configuration,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(bot.configuration, {
|
||||
useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Failed to convert ZUI to JSON schema for bot configuration')
|
||||
}),
|
||||
}
|
||||
: undefined,
|
||||
events: bot.events
|
||||
? await utils.records.mapValuesAsync(bot.events, async (event, eventName) => ({
|
||||
...event,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(event, {
|
||||
useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(
|
||||
thrown,
|
||||
`Failed to convert ZUI to JSON schema for bot event ${eventName}`
|
||||
)
|
||||
}),
|
||||
}))
|
||||
: undefined,
|
||||
states: bot.states
|
||||
? (utils.records.filterValues(
|
||||
await utils.records.mapValuesAsync(bot.states, async (state, stateName) => ({
|
||||
...state,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(state, {
|
||||
useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(
|
||||
thrown,
|
||||
`Failed to convert ZUI to JSON schema for bot state ${stateName}`
|
||||
)
|
||||
}),
|
||||
})),
|
||||
({ type }) => type !== 'workflow'
|
||||
) as types.CreateBotRequestBody['states'])
|
||||
: undefined,
|
||||
tags: bot.attributes,
|
||||
})
|
||||
|
||||
export const prepareUpdateBotBody = (
|
||||
localBot: types.UpdateBotRequestBody,
|
||||
remoteBot: client.Bot
|
||||
): types.UpdateBotRequestBody => ({
|
||||
...localBot,
|
||||
states: utils.records.setNullOnMissingValues(localBot.states, remoteBot.states),
|
||||
recurringEvents: utils.records.setNullOnMissingValues(localBot.recurringEvents, remoteBot.recurringEvents),
|
||||
events: utils.attributes.prepareAttributeUpdateBody({
|
||||
localItems: utils.records.setNullOnMissingValues(localBot.events, remoteBot.events),
|
||||
remoteItems: remoteBot.events,
|
||||
}),
|
||||
actions: utils.attributes.prepareAttributeUpdateBody({
|
||||
localItems: utils.records.setNullOnMissingValues(localBot.actions, remoteBot.actions),
|
||||
remoteItems: remoteBot.actions,
|
||||
}),
|
||||
user: {
|
||||
...localBot.user,
|
||||
tags: utils.records.setNullOnMissingValues(localBot.user?.tags, remoteBot.user?.tags),
|
||||
},
|
||||
conversation: {
|
||||
...localBot.conversation,
|
||||
tags: utils.records.setNullOnMissingValues(localBot.conversation?.tags, remoteBot.conversation?.tags),
|
||||
},
|
||||
message: {
|
||||
...localBot.message,
|
||||
tags: utils.records.setNullOnMissingValues(localBot.message?.tags, remoteBot.message?.tags),
|
||||
},
|
||||
integrations: utils.integrations.prepareIntegrationsUpdate(
|
||||
utils.records.setNullOnMissingValues(localBot.integrations, remoteBot.integrations),
|
||||
remoteBot.integrations
|
||||
),
|
||||
plugins: utils.records.setNullOnMissingValues(localBot.plugins, remoteBot.plugins),
|
||||
tags: localBot.tags, // TODO: allow removing bot tags (aka attributes) by setting to null
|
||||
})
|
||||
@@ -0,0 +1,348 @@
|
||||
import * as client from '@botpress/client'
|
||||
import semver from 'semver'
|
||||
import yn from 'yn'
|
||||
import * as errors from '../errors'
|
||||
import type { Logger } from '../logger'
|
||||
import { formatPackageRef, ApiPackageRef, NamePackageRef } from '../package-ref'
|
||||
import * as utils from '../utils'
|
||||
import * as paging from './paging'
|
||||
import * as retry from './retry'
|
||||
|
||||
import {
|
||||
ApiClientProps,
|
||||
PublicOrUnlistedIntegration,
|
||||
PrivateIntegration,
|
||||
PublicOrPrivateIntegration,
|
||||
PublicInterface,
|
||||
PrivateInterface,
|
||||
PublicOrPrivateInterface,
|
||||
PrivatePlugin,
|
||||
PublicPlugin,
|
||||
PublicOrPrivatePlugin,
|
||||
BotSummary,
|
||||
} from './types'
|
||||
export * from './types'
|
||||
|
||||
/**
|
||||
* This class is used to wrap the Botpress API and provide a more convenient way to interact with it.
|
||||
*/
|
||||
export class ApiClient {
|
||||
public readonly client: client.Client
|
||||
public readonly url: string
|
||||
public readonly token: string
|
||||
public readonly workspaceId: string
|
||||
public readonly botId?: string
|
||||
public readonly extraHeaders: Record<string, string>
|
||||
|
||||
public static newClient = (props: ApiClientProps, logger: Logger) => new ApiClient(props, logger)
|
||||
|
||||
public constructor(
|
||||
props: ApiClientProps,
|
||||
private _logger: Logger
|
||||
) {
|
||||
const { apiUrl, token, workspaceId, botId, extraHeaders = {} } = props
|
||||
this.client = new client.Client({
|
||||
apiUrl,
|
||||
token,
|
||||
workspaceId,
|
||||
botId,
|
||||
retry: retry.config,
|
||||
headers: { 'x-multiple-integrations': 'true', ...extraHeaders },
|
||||
})
|
||||
this.url = apiUrl
|
||||
this.token = token
|
||||
this.workspaceId = workspaceId
|
||||
this.botId = botId
|
||||
this.extraHeaders = extraHeaders
|
||||
}
|
||||
|
||||
public get isBotpressWorkspace(): boolean {
|
||||
// this environment variable is undocumented and only used internally for dev purposes
|
||||
const isBotpressWorkspace = yn(process.env.BP_IS_BOTPRESS_WORKSPACE)
|
||||
if (isBotpressWorkspace !== undefined) {
|
||||
return isBotpressWorkspace
|
||||
}
|
||||
return [
|
||||
'6a76fa10-e150-4ff6-8f59-a300feec06c1',
|
||||
'95de33eb-1551-4af9-9088-e5dcb02efd09',
|
||||
'11111111-1111-1111-aaaa-111111111111',
|
||||
].includes(this.workspaceId)
|
||||
}
|
||||
|
||||
public async safeListTables(req: client.ClientInputs['listTables']): Promise<
|
||||
| {
|
||||
success: true
|
||||
tables: client.ClientOutputs['listTables']['tables']
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
error: Error
|
||||
}
|
||||
> {
|
||||
try {
|
||||
const result = await this.client.listTables(req)
|
||||
return { success: true, tables: result.tables }
|
||||
} catch (thrown) {
|
||||
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
|
||||
return { success: false, error }
|
||||
}
|
||||
}
|
||||
|
||||
public async getWorkspace(): Promise<client.ClientOutputs['getWorkspace']> {
|
||||
return this.client.getWorkspace({ id: this.workspaceId })
|
||||
}
|
||||
|
||||
public async findWorkspaceByHandle(handle: string): Promise<client.ClientOutputs['getWorkspace'] | undefined> {
|
||||
const { workspaces } = await this.client.listWorkspaces({ handle })
|
||||
return workspaces[0] // There should be only one workspace with a given handle
|
||||
}
|
||||
|
||||
public withExtraHeaders(headers: Record<string, string>): ApiClient {
|
||||
return ApiClient.newClient(
|
||||
{
|
||||
apiUrl: this.url,
|
||||
token: this.token,
|
||||
workspaceId: this.workspaceId,
|
||||
botId: this.botId,
|
||||
extraHeaders: { ...this.extraHeaders, ...headers },
|
||||
},
|
||||
this._logger
|
||||
)
|
||||
}
|
||||
|
||||
public switchWorkspace(workspaceId: string): ApiClient {
|
||||
return ApiClient.newClient(
|
||||
{ apiUrl: this.url, token: this.token, workspaceId, extraHeaders: this.extraHeaders },
|
||||
this._logger
|
||||
)
|
||||
}
|
||||
|
||||
public switchBot(botId: string): ApiClient {
|
||||
return ApiClient.newClient(
|
||||
{ apiUrl: this.url, token: this.token, botId, workspaceId: this.workspaceId, extraHeaders: this.extraHeaders },
|
||||
this._logger
|
||||
)
|
||||
}
|
||||
|
||||
public async updateWorkspace(
|
||||
props: utils.types.SafeOmit<client.ClientInputs['updateWorkspace'], 'id'>
|
||||
): Promise<client.ClientOutputs['updateWorkspace']> {
|
||||
return this.client.updateWorkspace({ id: this.workspaceId, ...props })
|
||||
}
|
||||
|
||||
public async getPublicOrPrivateIntegration(ref: ApiPackageRef): Promise<PublicOrPrivateIntegration> {
|
||||
const integration = await this.findPublicOrPrivateIntegration(ref)
|
||||
if (!integration) {
|
||||
throw new Error(`Integration "${formatPackageRef(ref)}" not found`)
|
||||
}
|
||||
return integration
|
||||
}
|
||||
|
||||
public async findPublicOrPrivateIntegration(ref: ApiPackageRef): Promise<PublicOrPrivateIntegration | undefined> {
|
||||
const formatted = formatPackageRef(ref)
|
||||
|
||||
const privateIntegration = await this.findPrivateIntegration(ref)
|
||||
if (privateIntegration) {
|
||||
this._logger.debug(`Found integration "${formatted}" in workspace`)
|
||||
return privateIntegration
|
||||
}
|
||||
|
||||
const publicIntegration = await this.findPublicIntegration(ref)
|
||||
if (publicIntegration) {
|
||||
this._logger.debug(`Found integration "${formatted}" in hub`)
|
||||
return publicIntegration
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
public async findPrivateIntegration(ref: ApiPackageRef): Promise<PrivateIntegration | undefined> {
|
||||
const { workspaceId } = this
|
||||
if (ref.type === 'id') {
|
||||
return this.client
|
||||
.getIntegration(ref)
|
||||
.then((r) => ({ ...r.integration, workspaceId }))
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
return this.client
|
||||
.getIntegrationByName(ref)
|
||||
.then((r) => ({ ...r.integration, workspaceId }))
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
|
||||
public async findPublicIntegration(ref: ApiPackageRef): Promise<PublicOrUnlistedIntegration | undefined> {
|
||||
if (ref.type === 'id') {
|
||||
return this.client
|
||||
.getPublicIntegrationById(ref)
|
||||
.then((r) => ({ ...r.integration, visibility: r.integration.visibility as 'public' | 'unlisted' }) as const)
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
return this.client
|
||||
.getPublicIntegration(ref)
|
||||
.then((r) => ({ ...r.integration, visibility: r.integration.visibility as 'public' | 'unlisted' }) as const)
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
|
||||
public async findPublicOrPrivateInterface(ref: ApiPackageRef): Promise<PublicOrPrivateInterface | undefined> {
|
||||
const formatted = formatPackageRef(ref)
|
||||
|
||||
const privateInterface = await this.findPrivateInterface(ref)
|
||||
if (privateInterface) {
|
||||
this._logger.debug(`Found interface "${formatted}" in workspace`)
|
||||
return privateInterface
|
||||
}
|
||||
|
||||
const publicInterface = await this.findPublicInterface(ref)
|
||||
if (publicInterface) {
|
||||
this._logger.debug(`Found interface "${formatted}" in hub`)
|
||||
return publicInterface
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
public async findPrivateInterface(ref: ApiPackageRef): Promise<PrivateInterface | undefined> {
|
||||
const { workspaceId } = this
|
||||
if (ref.type === 'id') {
|
||||
return this.client
|
||||
.getInterface(ref)
|
||||
.then((r) => ({ ...r.interface, workspaceId }))
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
return this.client
|
||||
.getInterfaceByName(ref)
|
||||
.then((r) => ({ ...r.interface, workspaceId }))
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
|
||||
public async getPublicInterface(ref: ApiPackageRef): Promise<PublicInterface> {
|
||||
const intrface = await this.findPublicInterface(ref)
|
||||
if (!intrface) {
|
||||
throw new Error(`Interface "${formatPackageRef(ref)}" not found`)
|
||||
}
|
||||
return intrface
|
||||
}
|
||||
|
||||
public async findPublicInterface(ref: ApiPackageRef): Promise<PublicInterface | undefined> {
|
||||
if (ref.type === 'id') {
|
||||
return this.client
|
||||
.getPublicInterfaceById(ref)
|
||||
.then((r) => ({ ...r.interface, public: true }) as const)
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
|
||||
return this.client
|
||||
.getPublicInterface(ref)
|
||||
.then((r) => ({ ...r.interface, public: true }) as const)
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
|
||||
public async findPublicPlugin(ref: ApiPackageRef): Promise<PublicPlugin | undefined> {
|
||||
if (ref.type === 'id') {
|
||||
return this.client
|
||||
.getPublicPluginById(ref)
|
||||
.then((r) => ({ ...r.plugin, public: true }) as const)
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
|
||||
return this.client
|
||||
.getPublicPlugin(ref)
|
||||
.then((r) => ({ ...r.plugin, public: true }) as const)
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
|
||||
public async getPublicOrPrivatePlugin(ref: ApiPackageRef): Promise<PublicOrPrivatePlugin> {
|
||||
const plugin = await this.findPublicOrPrivatePlugin(ref)
|
||||
if (!plugin) {
|
||||
throw new Error(`Plugin "${formatPackageRef(ref)}" not found`)
|
||||
}
|
||||
return plugin
|
||||
}
|
||||
|
||||
public async findPublicOrPrivatePlugin(ref: ApiPackageRef): Promise<PublicOrPrivatePlugin | undefined> {
|
||||
const formatted = formatPackageRef(ref)
|
||||
|
||||
const privatePlugin = await this.findPrivatePlugin(ref)
|
||||
if (privatePlugin) {
|
||||
this._logger.debug(`Found plugin "${formatted}" in workspace`)
|
||||
return privatePlugin
|
||||
}
|
||||
|
||||
const publicPlugin = await this.findPublicPlugin(ref)
|
||||
if (publicPlugin) {
|
||||
this._logger.debug(`Found plugin "${formatted}" in hub`)
|
||||
return publicPlugin
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
public async findPrivatePlugin(ref: ApiPackageRef): Promise<PrivatePlugin | undefined> {
|
||||
const { workspaceId } = this
|
||||
if (ref.type === 'id') {
|
||||
return this.client
|
||||
.getPlugin(ref)
|
||||
.then((r) => ({ ...r.plugin, workspaceId }))
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
return this.client
|
||||
.getPluginByName(ref)
|
||||
.then((r) => ({ ...r.plugin, workspaceId }))
|
||||
.catch(this._returnUndefinedOnError('ResourceNotFound'))
|
||||
}
|
||||
|
||||
public async testLogin(): Promise<void> {
|
||||
await this.client.listBots({})
|
||||
}
|
||||
|
||||
public listAllPages = paging.listAllPages
|
||||
|
||||
public async findPreviousIntegrationVersion(ref: NamePackageRef): Promise<PublicOrPrivateIntegration | undefined> {
|
||||
const isValidSemverVersion = semver.valid(ref.version)
|
||||
|
||||
// Sanity check (this should never happen):
|
||||
if (!isValidSemverVersion) {
|
||||
throw new errors.BotpressCLIError(`Invalid version "${ref.version}" for integration "${ref.name}"`)
|
||||
}
|
||||
|
||||
return this.findPublicOrPrivateIntegration({ ...ref, version: `<${ref.version}` })
|
||||
}
|
||||
|
||||
public async findBotByName(name: string): Promise<BotSummary | undefined> {
|
||||
// api does not allow filtering bots by name
|
||||
const allBots = await this.listAllPages(this.client.listBots, (r) => r.bots)
|
||||
return allBots.find((b) => b.name === name)
|
||||
}
|
||||
|
||||
private _returnUndefinedOnError =
|
||||
(type: client.ApiError['type']) =>
|
||||
(thrown: any): undefined => {
|
||||
if (client.isApiError(thrown) && thrown.type === type) {
|
||||
return
|
||||
}
|
||||
throw thrown
|
||||
}
|
||||
|
||||
public async getOrGenerateShareableId(
|
||||
botId: string,
|
||||
integrationId: string,
|
||||
integrationAlias: string
|
||||
): Promise<string> {
|
||||
const { shareableId, isExpired } = await this.client
|
||||
.getIntegrationShareableId({
|
||||
botId,
|
||||
integrationId,
|
||||
integrationInstanceAlias: integrationAlias,
|
||||
})
|
||||
.catch(() => ({ shareableId: undefined, isExpired: true }))
|
||||
if (shareableId && !isExpired) {
|
||||
return shareableId
|
||||
}
|
||||
const { shareableId: newShareableId } = await this.client.createIntegrationShareableId({
|
||||
botId,
|
||||
integrationId,
|
||||
integrationInstanceAlias: integrationAlias,
|
||||
})
|
||||
return newShareableId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './client'
|
||||
export * from './types'
|
||||
export * from './integration-body'
|
||||
export * from './interface-body'
|
||||
export * from './bot-body'
|
||||
export * from './plugin-body'
|
||||
@@ -0,0 +1,243 @@
|
||||
import * as client from '@botpress/client'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as errors from '../errors'
|
||||
import * as utils from '../utils'
|
||||
import * as types from './types'
|
||||
|
||||
export const prepareCreateIntegrationBody = async (
|
||||
integration: sdk.IntegrationDefinition
|
||||
): Promise<types.CreateIntegrationRequestBody> => {
|
||||
const base = `Failed to convert ZUI to JSON schema for integration ${integration.name}`
|
||||
return {
|
||||
name: integration.name,
|
||||
version: integration.version,
|
||||
title: 'title' in integration ? integration.title : undefined,
|
||||
description: 'description' in integration ? integration.description : undefined,
|
||||
user: integration.user,
|
||||
events: integration.events
|
||||
? await utils.records.mapValuesAsync(integration.events, async (event, eventName) => ({
|
||||
...event,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(event, {
|
||||
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for event ${eventName}`)
|
||||
}),
|
||||
}))
|
||||
: undefined,
|
||||
actions: integration.actions
|
||||
? await utils.records.mapValuesAsync(integration.actions, async (action, actionName) => ({
|
||||
...action,
|
||||
input: {
|
||||
...action.input,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(action.input, {
|
||||
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} input`)
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
...action.output,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(action.output, {
|
||||
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} output`)
|
||||
}),
|
||||
},
|
||||
}))
|
||||
: undefined,
|
||||
channels: integration.channels
|
||||
? await utils.records.mapValuesAsync(integration.channels, async (channel, channelName) => ({
|
||||
...channel,
|
||||
messages: await utils.records.mapValuesAsync(channel.messages, async (message, messageName) => ({
|
||||
...message,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(message, {
|
||||
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(
|
||||
thrown,
|
||||
`${base} for channel ${channelName} for message ${messageName}`
|
||||
)
|
||||
}),
|
||||
})),
|
||||
}))
|
||||
: undefined,
|
||||
states: integration.states
|
||||
? await utils.records.mapValuesAsync(integration.states, async (state, stateName) => ({
|
||||
...state,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(state, {
|
||||
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for state ${stateName}`)
|
||||
}),
|
||||
}))
|
||||
: undefined,
|
||||
entities: integration.entities
|
||||
? await utils.records.mapValuesAsync(integration.entities, async (entity, entityName) => ({
|
||||
...entity,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(entity, {
|
||||
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for entity ${entityName}`)
|
||||
}),
|
||||
}))
|
||||
: undefined,
|
||||
attributes: integration.attributes,
|
||||
extraOperations: '__advanced' in integration ? integration.__advanced?.extraOperations : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateIntegrationChannelsBody = NonNullable<types.UpdateIntegrationRequestBody['channels']>
|
||||
type UpdateIntegrationChannelBody = UpdateIntegrationChannelsBody[string]
|
||||
type Channels = client.Integration['channels']
|
||||
type Channel = client.Integration['channels'][string]
|
||||
export const prepareUpdateIntegrationBody = (
|
||||
localIntegration: types.UpdateIntegrationRequestBody,
|
||||
remoteIntegration: client.Integration
|
||||
): types.UpdateIntegrationRequestBody => {
|
||||
const actions = utils.attributes.prepareAttributeUpdateBody({
|
||||
localItems: utils.records.setNullOnMissingValues(localIntegration.actions, remoteIntegration.actions),
|
||||
remoteItems: remoteIntegration.actions,
|
||||
})
|
||||
const events = utils.attributes.prepareAttributeUpdateBody({
|
||||
localItems: utils.records.setNullOnMissingValues(localIntegration.events, remoteIntegration.events),
|
||||
remoteItems: remoteIntegration.events,
|
||||
})
|
||||
const states = utils.records.setNullOnMissingValues(localIntegration.states, remoteIntegration.states)
|
||||
const entities = utils.records.setNullOnMissingValues(localIntegration.entities, remoteIntegration.entities)
|
||||
const user = {
|
||||
...localIntegration.user,
|
||||
tags: utils.records.setNullOnMissingValues(localIntegration.user?.tags, remoteIntegration.user?.tags),
|
||||
}
|
||||
|
||||
const channels = _prepareUpdateIntegrationChannelsBody(localIntegration.channels ?? {}, remoteIntegration.channels)
|
||||
|
||||
const interfaces = utils.records.setNullOnMissingValues(localIntegration.interfaces, remoteIntegration.interfaces)
|
||||
|
||||
const configurations = utils.records.setNullOnMissingValues(
|
||||
localIntegration.configurations,
|
||||
remoteIntegration.configurations
|
||||
)
|
||||
|
||||
const readme = localIntegration.readme
|
||||
const icon = localIntegration.icon
|
||||
|
||||
const attributes = utils.records.setNullOnMissingValues(localIntegration.attributes, remoteIntegration.attributes)
|
||||
|
||||
const extraOperations = localIntegration.extraOperations
|
||||
return {
|
||||
..._maybeRemoveVrlScripts(localIntegration, remoteIntegration),
|
||||
actions,
|
||||
events,
|
||||
states,
|
||||
entities,
|
||||
user,
|
||||
channels,
|
||||
interfaces,
|
||||
configurations,
|
||||
readme,
|
||||
icon,
|
||||
attributes,
|
||||
extraOperations,
|
||||
}
|
||||
}
|
||||
|
||||
const _maybeRemoveVrlScripts = (
|
||||
localIntegration: types.UpdateIntegrationRequestBody,
|
||||
remoteIntegration: client.Integration
|
||||
): types.UpdateIntegrationRequestBody => {
|
||||
const newIntegration = structuredClone(localIntegration)
|
||||
|
||||
if (
|
||||
remoteIntegration.configuration?.identifier?.linkTemplateScript &&
|
||||
!localIntegration.configuration?.identifier?.linkTemplateScript
|
||||
) {
|
||||
newIntegration.configuration ??= remoteIntegration.configuration
|
||||
newIntegration.configuration.identifier ??= remoteIntegration.configuration.identifier
|
||||
newIntegration.configuration.identifier.linkTemplateScript = null
|
||||
newIntegration.configuration.identifier.required = false
|
||||
}
|
||||
|
||||
if (remoteIntegration.identifier.extractScript && !localIntegration.identifier?.extractScript) {
|
||||
newIntegration.identifier ??= remoteIntegration.identifier
|
||||
newIntegration.identifier.extractScript = null
|
||||
}
|
||||
|
||||
if (remoteIntegration.identifier.fallbackHandlerScript && !localIntegration.identifier?.fallbackHandlerScript) {
|
||||
newIntegration.identifier ??= remoteIntegration.identifier
|
||||
newIntegration.identifier.fallbackHandlerScript = null
|
||||
}
|
||||
|
||||
for (const configName of Object.keys(localIntegration.configurations ?? {})) {
|
||||
if (
|
||||
remoteIntegration.configurations[configName]?.identifier.linkTemplateScript &&
|
||||
!localIntegration.configurations?.[configName]?.identifier?.linkTemplateScript
|
||||
) {
|
||||
newIntegration.configurations ??= remoteIntegration.configurations
|
||||
newIntegration.configurations[configName] ??= remoteIntegration.configurations[configName]
|
||||
newIntegration.configurations[configName].identifier ??= remoteIntegration.configurations[configName].identifier
|
||||
newIntegration.configurations[configName].identifier.linkTemplateScript = null
|
||||
newIntegration.configurations[configName].identifier.required = false
|
||||
}
|
||||
}
|
||||
|
||||
return newIntegration
|
||||
}
|
||||
|
||||
const _prepareUpdateIntegrationChannelsBody = (
|
||||
localChannels: UpdateIntegrationChannelsBody,
|
||||
remoteChannels: Channels
|
||||
): UpdateIntegrationChannelsBody => {
|
||||
const channelBody: UpdateIntegrationChannelsBody = {}
|
||||
|
||||
const zipped = utils.records.zipObjects(localChannels, remoteChannels)
|
||||
for (const [channelName, [localChannel, remoteChannel]] of Object.entries(zipped)) {
|
||||
if (localChannel && remoteChannel) {
|
||||
// channel has to be updated
|
||||
channelBody[channelName] = _prepareUpdateIntegrationChannelBody(localChannel, remoteChannel)
|
||||
} else if (localChannel) {
|
||||
// channel has to be created
|
||||
channelBody[channelName] = localChannel
|
||||
continue
|
||||
} else if (remoteChannel) {
|
||||
// channel has to be deleted
|
||||
channelBody[channelName] = null
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return channelBody
|
||||
}
|
||||
|
||||
const _prepareUpdateIntegrationChannelBody = (
|
||||
localChannel: UpdateIntegrationChannelBody,
|
||||
remoteChannel: Channel
|
||||
): UpdateIntegrationChannelBody => ({
|
||||
...localChannel,
|
||||
messages: utils.records.setNullOnMissingValues(localChannel?.messages, remoteChannel.messages),
|
||||
message: {
|
||||
...localChannel?.message,
|
||||
tags: utils.records.setNullOnMissingValues(localChannel?.message?.tags, remoteChannel.message.tags),
|
||||
},
|
||||
conversation: {
|
||||
...localChannel?.conversation,
|
||||
tags: utils.records.setNullOnMissingValues(localChannel?.conversation?.tags, remoteChannel.conversation.tags),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,128 @@
|
||||
import * as client from '@botpress/client'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as errors from '../errors'
|
||||
import * as utils from '../utils'
|
||||
import * as types from './types'
|
||||
|
||||
export const prepareCreateInterfaceBody = async (
|
||||
intrface: sdk.InterfaceDefinition
|
||||
): Promise<types.CreateInterfaceRequestBody> => {
|
||||
const base = `Failed to convert ZUI to JSON schema for interface ${intrface.name}`
|
||||
return {
|
||||
name: intrface.name,
|
||||
version: intrface.version,
|
||||
title: 'title' in intrface ? intrface.title : undefined,
|
||||
description: 'description' in intrface ? intrface.description : undefined,
|
||||
entities: intrface.entities
|
||||
? await utils.records.mapValuesAsync(intrface.entities, async (entity, entityName) => ({
|
||||
...entity,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(entity, {
|
||||
useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for entity ${entityName}`)
|
||||
}),
|
||||
}))
|
||||
: {},
|
||||
events: intrface.events
|
||||
? await utils.records.mapValuesAsync(intrface.events, async (event, eventName) => ({
|
||||
...event,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(event, {
|
||||
useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for event ${eventName}`)
|
||||
}),
|
||||
}))
|
||||
: {},
|
||||
actions: intrface.actions
|
||||
? await utils.records.mapValuesAsync(intrface.actions, async (action, actionName) => ({
|
||||
...action,
|
||||
input: {
|
||||
...action.input,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(action.input, {
|
||||
useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} input`)
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
...action.output,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(action.output, {
|
||||
useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} output`)
|
||||
}),
|
||||
},
|
||||
}))
|
||||
: {},
|
||||
channels: intrface.channels
|
||||
? await utils.records.mapValuesAsync(intrface.channels, async (channel, channelName) => ({
|
||||
...channel,
|
||||
messages: await utils.records.mapValuesAsync(channel.messages, async (message, messageName) => ({
|
||||
...message,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(message, {
|
||||
useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(
|
||||
thrown,
|
||||
`${base} for channel ${channelName} for message ${messageName}`
|
||||
)
|
||||
}),
|
||||
})),
|
||||
}))
|
||||
: {},
|
||||
attributes: intrface.attributes,
|
||||
}
|
||||
}
|
||||
|
||||
export const prepareUpdateInterfaceBody = (
|
||||
localInterface: types.CreateInterfaceRequestBody & { id: string },
|
||||
remoteInterface: client.Interface
|
||||
): types.UpdateInterfaceRequestBody => {
|
||||
const actions = utils.attributes.prepareAttributeUpdateBody({
|
||||
localItems: utils.records.setNullOnMissingValues(localInterface.actions, remoteInterface.actions),
|
||||
remoteItems: remoteInterface.actions,
|
||||
})
|
||||
const events = utils.attributes.prepareAttributeUpdateBody({
|
||||
localItems: utils.records.setNullOnMissingValues(localInterface.events, remoteInterface.events),
|
||||
remoteItems: remoteInterface.events,
|
||||
})
|
||||
const entities = utils.records.setNullOnMissingValues(localInterface.entities, remoteInterface.entities)
|
||||
|
||||
const currentChannels: types.UpdateInterfaceRequestBody['channels'] = localInterface.channels
|
||||
? utils.records.mapValues(localInterface.channels, (channel, channelName) => ({
|
||||
...channel,
|
||||
messages: utils.records.setNullOnMissingValues(
|
||||
channel?.messages,
|
||||
remoteInterface.channels[channelName]?.messages
|
||||
),
|
||||
}))
|
||||
: undefined
|
||||
|
||||
const channels = utils.records.setNullOnMissingValues(currentChannels, remoteInterface.channels)
|
||||
|
||||
const attributes = utils.records.setNullOnMissingValues(localInterface.attributes, remoteInterface.attributes)
|
||||
|
||||
return {
|
||||
...localInterface,
|
||||
entities,
|
||||
actions,
|
||||
events,
|
||||
channels,
|
||||
attributes,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export type PageLister<R extends object> = (t: { nextToken?: string }) => Promise<R & { meta: { nextToken?: string } }>
|
||||
|
||||
export async function listAllPages<R extends object>(lister: PageLister<R>): Promise<R[]>
|
||||
export async function listAllPages<R extends object, M>(lister: PageLister<R>, mapper?: (r: R) => M[]): Promise<M[]>
|
||||
export async function listAllPages<R extends object, M>(lister: PageLister<R>, mapper?: (r: R) => M[]) {
|
||||
let nextToken: string | undefined
|
||||
const all: R[] = []
|
||||
|
||||
do {
|
||||
const { meta, ...r } = await lister({ nextToken })
|
||||
all.push(r as R)
|
||||
nextToken = meta.nextToken
|
||||
} while (nextToken)
|
||||
|
||||
if (!mapper) {
|
||||
return all
|
||||
}
|
||||
|
||||
const mapped: M[] = all.flatMap((r) => mapper(r))
|
||||
return mapped
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import * as client from '@botpress/client'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as errors from '../errors'
|
||||
import * as utils from '../utils'
|
||||
import * as types from './types'
|
||||
|
||||
export const prepareCreatePluginBody = async (plugin: sdk.PluginDefinition): Promise<types.CreatePluginRequestBody> => {
|
||||
const base = `Failed to convert ZUI to JSON schema for plugin ${plugin.name}`
|
||||
return {
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
title: 'title' in plugin ? plugin.title : undefined,
|
||||
description: 'description' in plugin ? plugin.description : undefined,
|
||||
user: {
|
||||
tags: plugin.user?.tags ?? {},
|
||||
},
|
||||
conversation: {
|
||||
tags: plugin.conversation?.tags ?? {},
|
||||
},
|
||||
message: {
|
||||
tags: plugin.message?.tags ?? {},
|
||||
},
|
||||
configuration: plugin.configuration
|
||||
? {
|
||||
...plugin.configuration,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(plugin.configuration, {
|
||||
useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for configuration`)
|
||||
}),
|
||||
}
|
||||
: undefined,
|
||||
events: plugin.events
|
||||
? await utils.records.mapValuesAsync(plugin.events, async (event, eventName) => ({
|
||||
...event,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(event, {
|
||||
useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for event ${eventName}`)
|
||||
}),
|
||||
}))
|
||||
: undefined,
|
||||
actions: plugin.actions
|
||||
? await utils.records.mapValuesAsync(plugin.actions, async (action, actionName) => ({
|
||||
...action,
|
||||
input: {
|
||||
...action.input,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(action.input, {
|
||||
useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} input`)
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
...action.output,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(action.output, {
|
||||
useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} output`)
|
||||
}),
|
||||
},
|
||||
}))
|
||||
: undefined,
|
||||
states: plugin.states
|
||||
? (utils.records.filterValues(
|
||||
await utils.records.mapValuesAsync(plugin.states, async (state, stateName) => ({
|
||||
...state,
|
||||
schema: await utils.schema
|
||||
.mapZodToJsonSchema(state, {
|
||||
useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer,
|
||||
toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `${base} for state ${stateName}`)
|
||||
}),
|
||||
})),
|
||||
({ type }) => type !== 'workflow'
|
||||
) as types.CreatePluginRequestBody['states'])
|
||||
: undefined,
|
||||
attributes: plugin.attributes,
|
||||
}
|
||||
}
|
||||
|
||||
export const prepareUpdatePluginBody = (
|
||||
localPlugin: types.UpdatePluginRequestBody,
|
||||
remotePlugin: client.Plugin
|
||||
): types.UpdatePluginRequestBody => {
|
||||
const actions = utils.attributes.prepareAttributeUpdateBody({
|
||||
localItems: utils.records.setNullOnMissingValues(localPlugin.actions, remotePlugin.actions),
|
||||
remoteItems: remotePlugin.actions,
|
||||
})
|
||||
const events = utils.attributes.prepareAttributeUpdateBody({
|
||||
localItems: utils.records.setNullOnMissingValues(localPlugin.events, remotePlugin.events),
|
||||
remoteItems: remotePlugin.events,
|
||||
})
|
||||
const states = utils.records.setNullOnMissingValues(localPlugin.states, remotePlugin.states)
|
||||
|
||||
const attributes = utils.records.setNullOnMissingValues(localPlugin.attributes, remotePlugin.attributes)
|
||||
|
||||
const dependencies: types.UpdatePluginRequestBody['dependencies'] = {
|
||||
integrations: utils.records.setNullOnMissingValues(
|
||||
localPlugin.dependencies?.integrations,
|
||||
remotePlugin.dependencies?.integrations
|
||||
),
|
||||
interfaces: utils.records.setNullOnMissingValues(
|
||||
localPlugin.dependencies?.interfaces,
|
||||
remotePlugin.dependencies?.interfaces
|
||||
),
|
||||
}
|
||||
|
||||
// TODO: set null to conversation, user and message tags that are removed
|
||||
|
||||
return {
|
||||
...localPlugin,
|
||||
actions,
|
||||
events,
|
||||
states,
|
||||
attributes,
|
||||
dependencies,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as client from '@botpress/client'
|
||||
|
||||
// TODO: we probably shouldnt retry on 500 errors, but this is a temporary fix for the botpress repo CI
|
||||
const HTTP_STATUS_TO_RETRY_ON = [429, 500, 502, 503, 504]
|
||||
|
||||
function getRetryAfterMs(error: Parameters<NonNullable<client.RetryConfig['retryDelay']>>[1]): number | undefined {
|
||||
const headers = error?.response?.headers
|
||||
if (!headers) return undefined
|
||||
|
||||
const headerNames = ['retry-after', 'ratelimit-reset', 'x-ratelimit-reset']
|
||||
for (const name of headerNames) {
|
||||
const raw = headers[name]
|
||||
if (!raw) continue
|
||||
const value = String(raw)
|
||||
|
||||
// HTTP-date format (e.g. "Mon, 28 Apr 2026 12:00:00 GMT")
|
||||
if (value.includes(' ')) {
|
||||
const futureDate = new Date(value)
|
||||
if (!isNaN(futureDate.getTime())) {
|
||||
return Math.max(0, futureDate.getTime() - Date.now())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Seconds format (e.g. "120")
|
||||
const seconds = parseInt(value, 10)
|
||||
if (!isNaN(seconds) && seconds >= 0) {
|
||||
return seconds * 1000
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const config: client.RetryConfig = {
|
||||
retries: 3,
|
||||
retryCondition: (err) =>
|
||||
client.axiosRetry.isNetworkOrIdempotentRequestError(err) ||
|
||||
HTTP_STATUS_TO_RETRY_ON.includes(err.response?.status ?? 0),
|
||||
retryDelay: (retryCount, error) => {
|
||||
if (error?.response?.status === 429) {
|
||||
const retryAfterMs = getRetryAfterMs(error)
|
||||
if (retryAfterMs !== undefined) {
|
||||
return retryAfterMs
|
||||
}
|
||||
}
|
||||
return Math.max(retryCount, 1) * 1000
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as client from '@botpress/client'
|
||||
import { Logger } from '../logger'
|
||||
import { SafeOmit, Merge } from '../utils/type-utils'
|
||||
import { ApiClient } from './client'
|
||||
|
||||
export type ApiClientProps = {
|
||||
apiUrl: string
|
||||
token: string
|
||||
workspaceId: string
|
||||
botId?: string
|
||||
extraHeaders?: Record<string, string>
|
||||
}
|
||||
|
||||
export type ApiClientFactory = {
|
||||
newClient: (props: ApiClientProps, logger: Logger) => ApiClient
|
||||
}
|
||||
|
||||
export type PublicOrUnlistedIntegration = client.Integration & { visibility: 'public' | 'unlisted' }
|
||||
export type PrivateIntegration = client.Integration & { workspaceId: string }
|
||||
export type PublicOrPrivateIntegration = client.Integration & { workspaceId?: string }
|
||||
export type IntegrationSummary = (
|
||||
| client.ClientOutputs['listIntegrations']
|
||||
| client.ClientOutputs['listPublicIntegrations']
|
||||
)['integrations'][number]
|
||||
export type BotSummary = client.ClientOutputs['listBots']['bots'][number]
|
||||
export type PublicInterface = client.Interface & { public: true }
|
||||
export type PrivateInterface = client.Interface & { workspaceId: string }
|
||||
export type PublicOrPrivateInterface = client.Interface & { workspaceId?: string }
|
||||
export type PublicPlugin = client.Plugin & { public: true }
|
||||
export type PrivatePlugin = client.Plugin & { workspaceId: string }
|
||||
export type PublicOrPrivatePlugin = client.Plugin & { workspaceId?: string }
|
||||
|
||||
export type CreateBotRequestBody = client.ClientInputs['createBot']
|
||||
export type UpdateBotRequestBody = client.ClientInputs['updateBot']
|
||||
|
||||
export type CreateIntegrationRequestBody = client.ClientInputs['createIntegration']
|
||||
export type UpdateIntegrationRequestBody = client.ClientInputs['updateIntegration']
|
||||
|
||||
export type CreateInterfaceRequestBody = client.ClientInputs['createInterface']
|
||||
export type UpdateInterfaceRequestBody = client.ClientInputs['updateInterface']
|
||||
|
||||
type PluginDependency = client.Plugin['dependencies']['integrations'][string]
|
||||
|
||||
export type CreatePluginRequestBody = Merge<
|
||||
SafeOmit<client.ClientInputs['createPlugin'], 'code'>,
|
||||
{
|
||||
dependencies?: {
|
||||
integrations?: Record<string, PluginDependency>
|
||||
interfaces?: Record<string, PluginDependency>
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
export type UpdatePluginRequestBody = Merge<
|
||||
client.ClientInputs['updatePlugin'],
|
||||
{
|
||||
dependencies?: {
|
||||
integrations?: Record<string, PluginDependency | null>
|
||||
interfaces?: Record<string, PluginDependency | null>
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -0,0 +1,234 @@
|
||||
import * as chat from '@botpress/chat'
|
||||
import chalk from 'chalk'
|
||||
import * as readline from 'readline'
|
||||
import * as uuid from 'uuid'
|
||||
import * as utils from '../utils'
|
||||
|
||||
type MessageSource = 'myself' | 'bot' | 'other'
|
||||
type ChatMessage = chat.Message & { source: MessageSource }
|
||||
type ChatState =
|
||||
| {
|
||||
status: 'stopped'
|
||||
}
|
||||
| {
|
||||
status: 'running'
|
||||
messages: ChatMessage[]
|
||||
connection: chat.SignalListener
|
||||
keyboard: readline.Interface
|
||||
}
|
||||
|
||||
const USER_ICONS: Record<MessageSource, string> = {
|
||||
myself: '👤',
|
||||
bot: '🤖',
|
||||
other: '👥',
|
||||
}
|
||||
|
||||
const MESSAGE_ICONS: Record<chat.Message['payload']['type'], string> = {
|
||||
audio: '🎵',
|
||||
card: '🃏',
|
||||
carousel: '🎠',
|
||||
choice: '🔽',
|
||||
dropdown: '🔽',
|
||||
file: '📁',
|
||||
image: '🌅',
|
||||
location: '📍',
|
||||
text: '',
|
||||
video: '🎥',
|
||||
markdown: '',
|
||||
bloc: '🧱',
|
||||
}
|
||||
|
||||
const EXIT_KEYWORDS = ['exit', '.exit']
|
||||
|
||||
export type ChatProps = {
|
||||
client: chat.AuthenticatedClient
|
||||
conversationId: string
|
||||
protocol: chat.ServerEventsProtocol
|
||||
}
|
||||
|
||||
export class Chat {
|
||||
private _events = new utils.emitter.EventEmitter<{ state: ChatState }>()
|
||||
private _state: ChatState = { status: 'stopped' }
|
||||
|
||||
public static launch(props: ChatProps): Chat {
|
||||
const instance = new Chat(props)
|
||||
void instance._run()
|
||||
return instance
|
||||
}
|
||||
|
||||
private constructor(private _props: ChatProps) {}
|
||||
|
||||
private async _run() {
|
||||
this._switchAlternateScreenBuffer()
|
||||
this._events.on('state', this._renderMessages)
|
||||
|
||||
const connection = await this._props.client.listenConversation({
|
||||
id: this._props.conversationId,
|
||||
protocol: this._props.protocol,
|
||||
})
|
||||
const keyboard = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
|
||||
connection.on('message_created', (m) => void this._onMessageReceived(m))
|
||||
keyboard.on('line', (l) => void this._onKeyboardInput(l))
|
||||
process.stdin.on('keypress', (_, key) => {
|
||||
if (key.name === 'escape') {
|
||||
void this._onExit()
|
||||
}
|
||||
})
|
||||
|
||||
this._setState({ status: 'running', messages: [], connection, keyboard })
|
||||
}
|
||||
|
||||
private _setState = (newState: ChatState) => {
|
||||
this._state = newState
|
||||
this._events.emit('state', this._state)
|
||||
}
|
||||
|
||||
private _onMessageReceived = async (message: chat.Signals['message_created']) => {
|
||||
if (this._state.status === 'stopped') {
|
||||
return
|
||||
}
|
||||
if (message.userId === this._props.client.user.id) {
|
||||
return
|
||||
}
|
||||
const source: MessageSource = message.isBot ? 'bot' : 'other'
|
||||
this._setState({ ...this._state, messages: [...this._state.messages, { ...message, source }] })
|
||||
}
|
||||
|
||||
private _onKeyboardInput = async (line: string) => {
|
||||
if (this._state.status === 'stopped') {
|
||||
return
|
||||
}
|
||||
|
||||
if (EXIT_KEYWORDS.includes(line)) {
|
||||
await this._onExit()
|
||||
return
|
||||
}
|
||||
|
||||
if (!line) {
|
||||
this._setState({ ...this._state })
|
||||
return
|
||||
}
|
||||
|
||||
const message = this._textToMessage(line)
|
||||
this._setState({ ...this._state, messages: [...this._state.messages, message] })
|
||||
await this._props.client.createMessage(message)
|
||||
}
|
||||
|
||||
private _onExit = async () => {
|
||||
if (this._state.status === 'stopped') {
|
||||
return
|
||||
}
|
||||
const { connection, keyboard } = this._state
|
||||
await connection.disconnect()
|
||||
connection.cleanup()
|
||||
keyboard.close()
|
||||
this._setState({ status: 'stopped' })
|
||||
this._clearStdOut()
|
||||
this._restoreOriginalScreenBuffer()
|
||||
}
|
||||
|
||||
public wait(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
const cb = (state: ChatState) => {
|
||||
if (state.status === 'stopped') {
|
||||
this._events.off('state', cb)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
this._events.on('state', cb)
|
||||
})
|
||||
}
|
||||
|
||||
private _renderMessages = () => {
|
||||
if (this._state.status === 'stopped') {
|
||||
return
|
||||
}
|
||||
|
||||
this._clearStdOut()
|
||||
this._printHeader()
|
||||
|
||||
for (const message of this._state.messages) {
|
||||
const prefix = USER_ICONS[message.source]
|
||||
const text = this._messageToText(message)
|
||||
const coloredText = message.source === 'bot' ? text : chalk.gray(text)
|
||||
process.stdout.write(`${prefix} ${coloredText}\n`)
|
||||
}
|
||||
|
||||
this._state.keyboard.setPrompt('>> ')
|
||||
this._state.keyboard.prompt(true) // Redisplay the prompt and maintain current input
|
||||
}
|
||||
|
||||
private _printHeader = () => {
|
||||
process.stdout.write(chalk.bold('Botpress Chat\n'))
|
||||
process.stdout.write(chalk.gray('Type "exit" or press ESC key to quit\n'))
|
||||
}
|
||||
|
||||
private _switchAlternateScreenBuffer = () => {
|
||||
process.stdout.write('\x1B[?1049h')
|
||||
}
|
||||
|
||||
private _restoreOriginalScreenBuffer = () => {
|
||||
process.stdout.write('\x1B[?1049l')
|
||||
}
|
||||
|
||||
private _clearStdOut = () => {
|
||||
process.stdout.write('\x1B[2J\x1B[0;0H')
|
||||
}
|
||||
|
||||
private _messageToText = (message: Pick<chat.Message, 'payload'>): string => {
|
||||
const prefix = MESSAGE_ICONS[message.payload.type]
|
||||
switch (message.payload.type) {
|
||||
case 'audio':
|
||||
return prefix + message.payload.audioUrl
|
||||
case 'card':
|
||||
return prefix + JSON.stringify(message.payload)
|
||||
case 'carousel':
|
||||
return prefix + JSON.stringify(message.payload)
|
||||
case 'choice':
|
||||
return (
|
||||
prefix +
|
||||
[message.payload.text, ...message.payload.options.map((o) => ` - ${o.label} (${o.value})`)].join('\n')
|
||||
)
|
||||
case 'dropdown':
|
||||
return (
|
||||
prefix +
|
||||
[message.payload.text, ...message.payload.options.map((o) => ` - ${o.label} (${o.value})`)].join('\n')
|
||||
)
|
||||
case 'file':
|
||||
return prefix + message.payload.fileUrl
|
||||
case 'image':
|
||||
return prefix + message.payload.imageUrl
|
||||
case 'location':
|
||||
return prefix + `${message.payload.latitude},${message.payload.longitude} (${message.payload.address})`
|
||||
case 'text':
|
||||
return prefix + message.payload.text
|
||||
case 'video':
|
||||
return prefix + message.payload.videoUrl
|
||||
case 'markdown':
|
||||
return prefix + message.payload.markdown
|
||||
case 'bloc':
|
||||
return [
|
||||
prefix,
|
||||
...message.payload.items.map((item) => this._messageToText({ payload: item })).map((l) => `\t${l}`),
|
||||
].join('\n')
|
||||
default:
|
||||
type _assertion = utils.types.AssertNever<typeof message.payload>
|
||||
return '<unknown>'
|
||||
}
|
||||
}
|
||||
|
||||
private _textToMessage = (text: string): ChatMessage => {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
userId: this._props.client.user.id,
|
||||
source: 'myself',
|
||||
conversationId: this._props.conversationId,
|
||||
createdAt: new Date().toISOString(),
|
||||
payload: { type: 'text', text },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'dotenv/config'
|
||||
import yargs from '@bpinternal/yargs-extra'
|
||||
import commandDefinitions from './command-definitions'
|
||||
import commandImplementations from './command-implementations'
|
||||
import * as tree from './command-tree'
|
||||
import * as errors from './errors'
|
||||
import { Logger } from './logger'
|
||||
import { registerYargs } from './register-yargs'
|
||||
|
||||
const logError = (thrown: unknown) => {
|
||||
const error = errors.BotpressCLIError.map(thrown)
|
||||
// genuine crashes only: print the full chain so headless callers (no -v) still get the reason.
|
||||
new Logger().error(errors.BotpressCLIError.fullStack(error))
|
||||
}
|
||||
|
||||
const onError = (thrown: unknown) => {
|
||||
logError(thrown)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const yargsFail = (msg?: string) => {
|
||||
// usage errors are bad input, not crashes; show the clean message and help, never a stack.
|
||||
if (msg !== undefined) {
|
||||
new Logger().error(`${msg}\n`)
|
||||
}
|
||||
yargs.showHelp()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
process.on('uncaughtException', (thrown: unknown) => onError(thrown))
|
||||
process.on('unhandledRejection', (thrown: unknown) => onError(thrown))
|
||||
|
||||
const commands = tree.zipTree(commandDefinitions, commandImplementations)
|
||||
|
||||
registerYargs(yargs, commands)
|
||||
|
||||
void yargs
|
||||
.version()
|
||||
.scriptName('bp')
|
||||
.demandCommand(1, "You didn't provide any command. Use the --help flag to see the list of available commands.")
|
||||
.recommendCommands()
|
||||
.strict()
|
||||
.help()
|
||||
.fail(yargsFail)
|
||||
.parse()
|
||||
@@ -0,0 +1,95 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../consts'
|
||||
import { Module } from '../module'
|
||||
import { BotPluginsIndexModule } from './bot-plugins'
|
||||
import { BotTypingsModule } from './bot-typings'
|
||||
|
||||
export class BotImplementationModule extends Module {
|
||||
private _typingsModule: BotTypingsModule
|
||||
private _pluginsModule: BotPluginsIndexModule
|
||||
|
||||
public constructor(bot: sdk.BotDefinition) {
|
||||
super({
|
||||
exportName: 'Bot',
|
||||
path: consts.INDEX_FILE,
|
||||
})
|
||||
|
||||
this._typingsModule = new BotTypingsModule(bot)
|
||||
this._typingsModule.unshift('typings')
|
||||
this.pushDep(this._typingsModule)
|
||||
|
||||
this._pluginsModule = new BotPluginsIndexModule(bot)
|
||||
this._pluginsModule.unshift(consts.fromOutDir.pluginsDir)
|
||||
this.pushDep(this._pluginsModule)
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const {
|
||||
//
|
||||
_typingsModule: typingsModule,
|
||||
_pluginsModule: pluginsModule,
|
||||
} = this
|
||||
|
||||
const typingsImport = typingsModule.import(this)
|
||||
const pluginsImport = pluginsModule.import(this)
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
`import * as ${typingsModule.name} from "./${typingsImport}"`,
|
||||
`import * as ${pluginsModule.name} from "./${pluginsImport}"`,
|
||||
'',
|
||||
`export * from "./${typingsImport}"`,
|
||||
`export * from "./${pluginsImport}"`,
|
||||
'',
|
||||
`type TPlugins = ${pluginsModule.name}.TPlugins`,
|
||||
`type TBot = sdk.DefaultBot<${typingsModule.name}.${typingsModule.exportName}>`,
|
||||
'',
|
||||
"export type BotProps = Omit<sdk.BotProps<TBot, TPlugins>, 'plugins'>",
|
||||
'',
|
||||
'export class Bot extends sdk.Bot<TBot, TPlugins> {',
|
||||
' public constructor(props: BotProps) {',
|
||||
' super({',
|
||||
' ...props,',
|
||||
` plugins: ${pluginsModule.name}.${pluginsModule.exportName}`,
|
||||
' })',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'// extra types',
|
||||
'',
|
||||
'type AsyncFunction = (...args: any[]) => Promise<any>',
|
||||
'',
|
||||
'export type BotHandlers = sdk.InjectedBotHandlers<TBot>',
|
||||
'',
|
||||
'export type EventHandlers = Required<{',
|
||||
" [K in keyof BotHandlers['eventHandlers']]: NonNullable<BotHandlers['eventHandlers'][K]>[number]",
|
||||
'}>',
|
||||
'export type MessageHandlers = Required<{',
|
||||
" [K in keyof BotHandlers['messageHandlers']]: NonNullable<BotHandlers['messageHandlers'][K]>[number]",
|
||||
'}>',
|
||||
'export type WorkflowHandlers = {',
|
||||
" [TWorkflowName in keyof Required<BotHandlers['workflowHandlers'][keyof BotHandlers['workflowHandlers']]>]:",
|
||||
" NonNullable<Required<BotHandlers['workflowHandlers'][keyof BotHandlers['workflowHandlers']]>[TWorkflowName]>[number]",
|
||||
'}',
|
||||
'',
|
||||
"export type MessageHandlerProps = Parameters<MessageHandlers['*']>[0]",
|
||||
"export type EventHandlerProps = Parameters<EventHandlers['*']>[0]",
|
||||
'export type WorkflowHandlerProps = {',
|
||||
' [TWorkflowName in keyof WorkflowHandlers]: WorkflowHandlers[TWorkflowName] extends',
|
||||
' (..._: infer U) => any ? U[0] : never',
|
||||
'}',
|
||||
'',
|
||||
"export type Client = (MessageHandlerProps | EventHandlerProps)['client']",
|
||||
'export type ClientOperation = keyof {',
|
||||
' [K in keyof Client as Client[K] extends AsyncFunction ? K : never]: null',
|
||||
'}',
|
||||
'export type ClientInputs = {',
|
||||
' [K in ClientOperation]: Parameters<Client[K]>[0]',
|
||||
'}',
|
||||
'export type ClientOutputs = {',
|
||||
' [K in ClientOperation]: Awaited<ReturnType<Client[K]>>',
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../../consts'
|
||||
import * as mod from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import { BotPluginModule } from './plugin-module'
|
||||
|
||||
export class BotPluginsIndexModule extends mod.Module {
|
||||
private _pluginModules: BotPluginModule[]
|
||||
|
||||
public constructor(sdkBotDefinition: sdk.BotDefinition) {
|
||||
super({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: 'plugins',
|
||||
})
|
||||
|
||||
const pluginsModules: BotPluginModule[] = []
|
||||
for (const plugin of Object.values(sdkBotDefinition.plugins ?? {})) {
|
||||
const pluginModule = new BotPluginModule(plugin)
|
||||
pluginModule.unshift(pluginModule.pluginKey)
|
||||
this.pushDep(pluginModule)
|
||||
pluginsModules.push(pluginModule)
|
||||
}
|
||||
|
||||
this._pluginModules = pluginsModules
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
const modules = this._pluginModules.map((module) => ({
|
||||
importAlias: strings.importAlias(module.name),
|
||||
importFrom: module.import(this),
|
||||
module,
|
||||
}))
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
...modules.map(({ importAlias, importFrom }) => `import * as ${importAlias} from "./${importFrom}";`),
|
||||
...modules.map(({ importAlias, importFrom }) => `export * as ${importAlias} from "./${importFrom}";`),
|
||||
'',
|
||||
`export const ${this.exportName} = {`,
|
||||
...modules.map(({ module, importAlias }) => ` "${module.pluginKey}": ${importAlias}.${module.exportName},`),
|
||||
'}',
|
||||
'',
|
||||
'export type TPlugins = {',
|
||||
...modules.map(({ module, importAlias }) => ` "${module.pluginKey}": ${importAlias}.TPlugin;`),
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../../consts'
|
||||
import { Module, ModuleProps } from '../../module'
|
||||
import { PluginTypingsModule } from '../../plugin-implementation/plugin-typings'
|
||||
|
||||
type PluginInstance = NonNullable<sdk.BotDefinition['plugins']>[string]
|
||||
|
||||
class BundleJsModule extends Module {
|
||||
private _indexJs: string
|
||||
public constructor(plugin: PluginInstance) {
|
||||
super({
|
||||
path: 'bundle.js',
|
||||
exportName: 'default',
|
||||
})
|
||||
this._indexJs = plugin.implementation.toString()
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return this._indexJs
|
||||
}
|
||||
}
|
||||
|
||||
class BundleDtsModule extends Module {
|
||||
public constructor(private _typingsModule: PluginTypingsModule) {
|
||||
super({
|
||||
path: 'bundle.d.ts',
|
||||
exportName: 'default',
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const typingsImport = this._typingsModule.import(this)
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
`import * as ${this._typingsModule.name} from "./${typingsImport}"`,
|
||||
`type TPlugin = sdk.DefaultPlugin<${this._typingsModule.name}.${this._typingsModule.exportName}>`,
|
||||
'export default new sdk.Plugin<TPlugin>({})',
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
class PluginConfigModule extends Module {
|
||||
private _plugin: PluginInstance
|
||||
public constructor(config: ModuleProps & { plugin: PluginInstance }) {
|
||||
super(config)
|
||||
this._plugin = config.plugin
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const { interfaces, integrations, configuration, alias } = this._plugin
|
||||
const content = JSON.stringify({ alias, interfaces, integrations, configuration }, null, 2)
|
||||
return `export default ${content}`
|
||||
}
|
||||
}
|
||||
|
||||
export class BotPluginModule extends Module {
|
||||
private _typingsModule: PluginTypingsModule
|
||||
private _bundleJsModule: BundleJsModule
|
||||
private _bundleDtsModule: BundleDtsModule
|
||||
private _configModule: PluginConfigModule
|
||||
|
||||
public readonly pluginKey: string
|
||||
|
||||
public constructor(plugin: PluginInstance) {
|
||||
super({
|
||||
exportName: 'default',
|
||||
path: consts.INDEX_FILE,
|
||||
})
|
||||
|
||||
this.pluginKey = plugin.alias ?? plugin.name
|
||||
|
||||
this._typingsModule = new PluginTypingsModule(plugin.definition)
|
||||
this._typingsModule.unshift('typings')
|
||||
this.pushDep(this._typingsModule)
|
||||
|
||||
this._bundleJsModule = new BundleJsModule(plugin)
|
||||
this.pushDep(this._bundleJsModule)
|
||||
|
||||
this._bundleDtsModule = new BundleDtsModule(this._typingsModule)
|
||||
this.pushDep(this._bundleDtsModule)
|
||||
|
||||
this._configModule = new PluginConfigModule({
|
||||
path: 'config.ts',
|
||||
exportName: 'default',
|
||||
plugin,
|
||||
})
|
||||
this.pushDep(this._configModule)
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const configImport = this._configModule.import(this)
|
||||
const typingsImport = this._typingsModule.import(this)
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
'import bundle from "./bundle"',
|
||||
`import * as ${this._typingsModule.name} from "./${typingsImport}"`,
|
||||
`import * as ${this._configModule.name} from "./${configImport}"`,
|
||||
'',
|
||||
`export type TPlugin = sdk.DefaultPlugin<${this._typingsModule.name}.${this._typingsModule.exportName}>`,
|
||||
'',
|
||||
`export const configuration = ${this._configModule.name}.${this._configModule.exportName}.configuration`,
|
||||
`export const interfaces = ${this._configModule.name}.${this._configModule.exportName}.interfaces`,
|
||||
'',
|
||||
`export default bundle.initialize(${this._configModule.name}.${this._configModule.exportName})`,
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
type ActionInput = sdk.BotActionDefinition['input']
|
||||
type ActionOutput = sdk.BotActionDefinition['output']
|
||||
|
||||
export class ActionInputModule extends Module {
|
||||
public constructor(private _input: ActionInput) {
|
||||
const name = 'input'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._input.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionOutputModule extends Module {
|
||||
public constructor(private _output: ActionOutput) {
|
||||
const name = 'output'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._output.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionModule extends ReExportTypeModule {
|
||||
public constructor(actionName: string, action: sdk.BotActionDefinition) {
|
||||
super({ exportName: strings.typeName(actionName) })
|
||||
|
||||
const inputModule = new ActionInputModule(action.input)
|
||||
const outputModule = new ActionOutputModule(action.output)
|
||||
|
||||
this.pushDep(inputModule)
|
||||
this.pushDep(outputModule)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionsModule extends ReExportTypeModule {
|
||||
public constructor(actions: Record<string, sdk.BotActionDefinition>) {
|
||||
super({ exportName: strings.typeName('actions') })
|
||||
for (const [actionName, action] of Object.entries(actions)) {
|
||||
const module = new ActionModule(actionName, action)
|
||||
module.unshift(actionName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class EventModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _event: sdk.BotEventDefinition
|
||||
) {
|
||||
const eventName = name
|
||||
const exportName = strings.typeName(eventName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._event.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class EventsModule extends ReExportTypeModule {
|
||||
public constructor(events: Record<string, sdk.BotEventDefinition>) {
|
||||
super({ exportName: strings.typeName('events') })
|
||||
for (const [eventName, event] of Object.entries(events)) {
|
||||
const module = new EventModule(eventName, event)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../../consts'
|
||||
import { IntegrationTypingsModule } from '../../integration-implementation/integration-typings'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import { ActionsModule } from './actions-module'
|
||||
import { EventsModule } from './events-module'
|
||||
import { StatesModule } from './states-module'
|
||||
import { TablesModule } from './tables-module'
|
||||
import { WorkflowsModule } from './workflows-module'
|
||||
|
||||
class BotIntegrationsModule extends ReExportTypeModule {
|
||||
public constructor(bot: sdk.BotDefinition) {
|
||||
super({
|
||||
exportName: 'Integrations',
|
||||
})
|
||||
|
||||
for (const [alias, integration] of Object.entries(bot.integrations ?? {})) {
|
||||
const integrationModule = new IntegrationTypingsModule(integration.definition).setCustomTypeName(alias)
|
||||
integrationModule.unshift(strings.dirName(alias))
|
||||
this.pushDep(integrationModule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BotTypingsIndexDependencies = {
|
||||
integrationsModule: BotIntegrationsModule
|
||||
eventsModule: EventsModule
|
||||
statesModule: StatesModule
|
||||
actionsModule: ActionsModule
|
||||
tablesModule: TablesModule
|
||||
workflowsModule: WorkflowsModule
|
||||
}
|
||||
|
||||
export class BotTypingsModule extends Module {
|
||||
private _dependencies: BotTypingsIndexDependencies
|
||||
|
||||
public constructor(bot: sdk.BotDefinition) {
|
||||
super({
|
||||
exportName: 'TBot',
|
||||
path: consts.INDEX_FILE,
|
||||
})
|
||||
|
||||
const integrationsModule = new BotIntegrationsModule(bot)
|
||||
integrationsModule.unshift('integrations')
|
||||
this.pushDep(integrationsModule)
|
||||
|
||||
const eventsModule = new EventsModule(bot.withPlugins.events ?? {})
|
||||
eventsModule.unshift('events')
|
||||
this.pushDep(eventsModule)
|
||||
|
||||
const statesModule = new StatesModule(bot.withPlugins.states ?? {})
|
||||
statesModule.unshift('states')
|
||||
this.pushDep(statesModule)
|
||||
|
||||
const tablesModule = new TablesModule(bot.withPlugins.tables ?? {})
|
||||
tablesModule.unshift('tables')
|
||||
this.pushDep(tablesModule)
|
||||
|
||||
const actionsModule = new ActionsModule(bot.withPlugins.actions ?? {})
|
||||
actionsModule.unshift('actions')
|
||||
this.pushDep(actionsModule)
|
||||
|
||||
const workflowsModule = new WorkflowsModule(bot.withPlugins.workflows ?? {})
|
||||
workflowsModule.unshift('workflows')
|
||||
this.pushDep(workflowsModule)
|
||||
|
||||
this._dependencies = {
|
||||
integrationsModule,
|
||||
eventsModule,
|
||||
statesModule,
|
||||
actionsModule,
|
||||
tablesModule,
|
||||
workflowsModule,
|
||||
}
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const { integrationsModule, eventsModule, statesModule, actionsModule, tablesModule, workflowsModule } =
|
||||
this._dependencies
|
||||
|
||||
const integrationsImport = integrationsModule.import(this)
|
||||
const eventsImport = eventsModule.import(this)
|
||||
const statesImport = statesModule.import(this)
|
||||
const actionsImport = actionsModule
|
||||
const tablesImport = tablesModule.import(this)
|
||||
const workflowsImport = workflowsModule
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
`import * as ${integrationsModule.name} from './${integrationsImport}'`,
|
||||
`import * as ${eventsModule.name} from './${eventsModule.name}'`,
|
||||
`import * as ${statesModule.name} from './${statesModule.name}'`,
|
||||
`import * as ${actionsModule.name} from './${actionsImport.name}'`,
|
||||
`import * as ${tablesModule.name} from './${tablesImport}'`,
|
||||
`import * as ${workflowsModule.name} from './${workflowsImport.name}'`,
|
||||
'',
|
||||
`export * as ${integrationsModule.name} from './${integrationsImport}'`,
|
||||
`export * as ${eventsModule.name} from './${eventsImport}'`,
|
||||
`export * as ${statesModule.name} from './${statesImport}'`,
|
||||
`export * as ${actionsModule.name} from './${actionsImport.name}'`,
|
||||
`export * as ${tablesModule.name} from './${tablesImport}'`,
|
||||
`export * as ${workflowsModule.name} from './${workflowsImport.name}'`,
|
||||
'',
|
||||
'export type TBot = {',
|
||||
` integrations: ${integrationsModule.name}.${integrationsModule.exportName}`,
|
||||
` events: ${eventsModule.name}.${eventsModule.exportName}`,
|
||||
` states: ${statesModule.name}.${statesModule.exportName}`,
|
||||
` actions: ${actionsModule.name}.${actionsModule.exportName}`,
|
||||
` tables: ${tablesModule.name}.${tablesModule.exportName}`,
|
||||
` workflows: ${workflowsModule.name}.${workflowsModule.exportName}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../../consts'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class StatePayloadModule extends Module {
|
||||
public constructor(private _state: sdk.BotStateDefinition) {
|
||||
super({
|
||||
path: 'payload.ts',
|
||||
exportName: strings.typeName('Payload'),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return gen.zuiSchemaToTypeScriptType(this._state.schema, 'Payload')
|
||||
}
|
||||
}
|
||||
|
||||
export class StateModule extends Module {
|
||||
private _payloadModule: StatePayloadModule
|
||||
|
||||
public constructor(
|
||||
private _name: string,
|
||||
private _state: sdk.BotStateDefinition
|
||||
) {
|
||||
super({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: strings.typeName(_name),
|
||||
})
|
||||
|
||||
this._payloadModule = new StatePayloadModule(_state)
|
||||
this.pushDep(this._payloadModule)
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
const { _payloadModule } = this
|
||||
const payloadImport = _payloadModule.import(this)
|
||||
|
||||
const exportName = strings.typeName(this._name)
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
`import * as ${_payloadModule.name} from './${payloadImport}'`,
|
||||
`export type ${exportName} = {`,
|
||||
` type: ${gen.primitiveToTypescriptValue(this._state.type)},`,
|
||||
` payload: ${_payloadModule.name}.${_payloadModule.exportName}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
export class StatesModule extends ReExportTypeModule {
|
||||
public constructor(states: Record<string, sdk.BotStateDefinition>) {
|
||||
super({ exportName: strings.typeName('states') })
|
||||
for (const [stateName, state] of Object.entries(states)) {
|
||||
const module = new StateModule(stateName, state)
|
||||
module.unshift(stateName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class TableModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _table: sdk.BotTableDefinition
|
||||
) {
|
||||
super({
|
||||
path: `${name}.ts`,
|
||||
exportName: strings.typeName(name),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._table.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class TablesModule extends ReExportTypeModule {
|
||||
public constructor(tables: Record<string, sdk.BotTableDefinition>) {
|
||||
super({ exportName: strings.typeName('tables') })
|
||||
for (const [tableName, table] of Object.entries(tables)) {
|
||||
const module = new TableModule(tableName, table)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { primitiveToTypescriptValue, zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
type WorkflowInput = sdk.BotWorkflowDefinition['input']
|
||||
type WorkflowOutput = sdk.BotWorkflowDefinition['output']
|
||||
type WorkflowTags = sdk.BotWorkflowDefinition['tags']
|
||||
|
||||
export class WorkflowInputModule extends Module {
|
||||
public constructor(private _input: WorkflowInput) {
|
||||
const name = 'input'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._input.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkflowOutputModule extends Module {
|
||||
public constructor(private _output: WorkflowOutput) {
|
||||
const name = 'output'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._output.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkflowTagsModule extends Module {
|
||||
public constructor(private _tags: WorkflowTags) {
|
||||
const name = 'tags'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const tags = Object.keys(this._tags ?? {})
|
||||
.map((tagName) => `${primitiveToTypescriptValue(tagName)}: string`)
|
||||
.join(', ')
|
||||
|
||||
return `export type ${this.exportName} = { ${tags} }`
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkflowModule extends ReExportTypeModule {
|
||||
public constructor(workflowName: string, workflow: sdk.BotWorkflowDefinition) {
|
||||
super({ exportName: strings.typeName(workflowName) })
|
||||
|
||||
const inputModule = new WorkflowInputModule(workflow.input)
|
||||
const outputModule = new WorkflowOutputModule(workflow.output)
|
||||
const tagsModule = new WorkflowTagsModule(workflow.tags)
|
||||
|
||||
this.pushDep(inputModule)
|
||||
this.pushDep(outputModule)
|
||||
this.pushDep(tagsModule)
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkflowsModule extends ReExportTypeModule {
|
||||
public constructor(workflows: Record<string, sdk.BotWorkflowDefinition>) {
|
||||
super({ exportName: strings.typeName('workflows') })
|
||||
for (const [workflowName, workflow] of Object.entries(workflows)) {
|
||||
const module = new WorkflowModule(workflowName, workflow)
|
||||
module.unshift(workflowName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../consts'
|
||||
import * as mod from '../module'
|
||||
import { SecretIndexModule } from '../secret-module'
|
||||
import * as types from '../typings'
|
||||
import { BotImplementationModule } from './bot-implementation'
|
||||
|
||||
class BotIndexModule extends mod.Module {
|
||||
private _botImplModule: BotImplementationModule
|
||||
private _botSecretModule: SecretIndexModule
|
||||
|
||||
public constructor(sdkBotDefinition: sdk.BotDefinition) {
|
||||
super({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: '',
|
||||
})
|
||||
|
||||
const botImpl = new BotImplementationModule(sdkBotDefinition)
|
||||
botImpl.unshift(consts.fromOutDir.implementationDir)
|
||||
this.pushDep(botImpl)
|
||||
this._botImplModule = botImpl
|
||||
|
||||
const botSecrets = new SecretIndexModule(sdkBotDefinition.secrets ?? {})
|
||||
botSecrets.unshift(consts.fromOutDir.secretsDir)
|
||||
this.pushDep(botSecrets)
|
||||
this._botSecretModule = botSecrets
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
const botImplImport = this._botImplModule.import(this)
|
||||
const botSecretImport = this._botSecretModule.import(this)
|
||||
|
||||
return [
|
||||
//
|
||||
consts.GENERATED_HEADER,
|
||||
`export * from "./${botImplImport}"`,
|
||||
`export * from "./${botSecretImport}"`,
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
export const generateBotImplementation = async (sdkBotDefinition: sdk.BotDefinition): Promise<types.File[]> => {
|
||||
const botIndexModule = new BotIndexModule(sdkBotDefinition)
|
||||
return botIndexModule.flatten()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from '../consts'
|
||||
export const GENERATED_HEADER = [
|
||||
'/* eslint-disable */',
|
||||
'/* tslint:disable */',
|
||||
'// This file is generated. Do not edit it manually.',
|
||||
'',
|
||||
].join('\n')
|
||||
export const INDEX_FILE = 'index.ts'
|
||||
export const INDEX_DECLARATION_FILE = 'index.d.ts'
|
||||
export const DEFAULT_EXPORT_NAME = 'default'
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { JSONSchema7 } from 'json-schema'
|
||||
import _ from 'lodash'
|
||||
import * as prettier from 'prettier'
|
||||
import * as utils from '../utils'
|
||||
import * as consts from './consts'
|
||||
|
||||
export type Primitive = string | number | boolean | null | undefined
|
||||
|
||||
export const zuiSchemaToTypeScriptType = async (zuiSchema: sdk.z.Schema, name: string): Promise<string> => {
|
||||
let code = zuiSchema.toTypescriptType({ treatDefaultAsOptional: true })
|
||||
code = `export type ${name} = ${code}`
|
||||
code = await prettier.format(code, { parser: 'typescript' })
|
||||
return [
|
||||
//
|
||||
consts.GENERATED_HEADER,
|
||||
code,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export const jsonSchemaToTypescriptZuiSchema = async (
|
||||
schema: JSONSchema7,
|
||||
name: string,
|
||||
extraProps: Record<string, string> = {}
|
||||
): Promise<string> => {
|
||||
schema = await utils.schema.dereferenceSchema(schema)
|
||||
const zuiSchema = sdk.z.transforms.fromJSONSchemaLegacy(schema)
|
||||
|
||||
const allProps = {
|
||||
...extraProps,
|
||||
schema: zuiSchema.toTypescriptSchema(),
|
||||
}
|
||||
|
||||
let code = [
|
||||
consts.GENERATED_HEADER,
|
||||
'import { z } from "@botpress/sdk"',
|
||||
`export const ${name} = ${typescriptValuesToRecordString(allProps)}`,
|
||||
].join('\n')
|
||||
code = await prettier.format(code, { parser: 'typescript' })
|
||||
return code
|
||||
}
|
||||
|
||||
export const stringifySingleLine = (x: object): string => {
|
||||
return JSON.stringify(x, null, 1).replace(/\n */g, ' ')
|
||||
}
|
||||
|
||||
export function primitiveToTypescriptValue(x: Primitive): string {
|
||||
if (typeof x === 'undefined') {
|
||||
return 'undefined'
|
||||
}
|
||||
return JSON.stringify(x)
|
||||
}
|
||||
|
||||
export function primitiveRecordToTypescriptValues(x: Record<string, Primitive>): Record<string, string> {
|
||||
return _(x)
|
||||
.toPairs()
|
||||
.filter(([_key, value]) => value !== undefined)
|
||||
.map(([key, value]) => [key, primitiveToTypescriptValue(value)])
|
||||
.fromPairs()
|
||||
.value()
|
||||
}
|
||||
|
||||
export const primitiveRecordToRecordString = (record: Record<string, Primitive>): string =>
|
||||
typescriptValuesToRecordString(primitiveRecordToTypescriptValues(record))
|
||||
|
||||
export const typescriptValuesToRecordString = (record: Record<string, string>): string =>
|
||||
['{', ...Object.entries(record).map(([key, value]) => ` ${key}: ${value},`), '}'].join('\n')
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './typings'
|
||||
export { secretEnvVariableName } from './secret-module'
|
||||
export { generateBotImplementation } from './bot-implementation'
|
||||
export { generateIntegrationImplementation } from './integration-implementation'
|
||||
export { generatePluginImplementation } from './plugin-implementation'
|
||||
export { generateIntegrationPackage } from './integration-package'
|
||||
export { generateInterfacePackage } from './interface-package'
|
||||
export { generatePluginPackage } from './plugin-package'
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../consts'
|
||||
import { SecretIndexModule } from '../secret-module'
|
||||
import * as types from '../typings'
|
||||
import { IntegrationImplementationModule } from './integration-implementation'
|
||||
|
||||
const generateIntegrationImplementationCls = async (
|
||||
sdkIntegrationDefinition: sdk.IntegrationDefinition,
|
||||
implPath: string
|
||||
): Promise<types.File[]> => {
|
||||
const indexModule = new IntegrationImplementationModule(sdkIntegrationDefinition)
|
||||
indexModule.unshift(implPath)
|
||||
return indexModule.flatten()
|
||||
}
|
||||
|
||||
const generateIntegrationSecrets = async (
|
||||
sdkIntegrationDefinition: sdk.IntegrationDefinition,
|
||||
secretsPath: string
|
||||
): Promise<types.File[]> => {
|
||||
const indexModule = new SecretIndexModule(sdkIntegrationDefinition.secrets ?? {})
|
||||
indexModule.unshift(secretsPath)
|
||||
return indexModule.flatten()
|
||||
}
|
||||
|
||||
const generateIntegrationImplementationIndex = async (implPath: string, secretsPath: string): Promise<types.File> => {
|
||||
let content = ''
|
||||
content += `export * from './${implPath}'\n`
|
||||
content += `export * from './${secretsPath}'\n`
|
||||
return {
|
||||
path: consts.INDEX_FILE,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
export const generateIntegrationImplementation = async (
|
||||
sdkIntegrationDefinition: sdk.IntegrationDefinition
|
||||
): Promise<types.File[]> => {
|
||||
const implPath = consts.fromOutDir.implementationDir
|
||||
const secretsPath = consts.fromOutDir.secretsDir
|
||||
const implFiles = await generateIntegrationImplementationCls(sdkIntegrationDefinition, implPath)
|
||||
const secretFiles = await generateIntegrationSecrets(sdkIntegrationDefinition, secretsPath)
|
||||
const indexFile = await generateIntegrationImplementationIndex(implPath, secretsPath)
|
||||
return [...implFiles, ...secretFiles, indexFile]
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../consts'
|
||||
import { Module } from '../module'
|
||||
import { IntegrationTypingsModule } from './integration-typings'
|
||||
|
||||
export class IntegrationImplementationModule extends Module {
|
||||
private _typingsModule: IntegrationTypingsModule
|
||||
|
||||
public constructor(integration: sdk.IntegrationDefinition) {
|
||||
super({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: 'Integration',
|
||||
})
|
||||
this._typingsModule = new IntegrationTypingsModule(integration)
|
||||
this._typingsModule.unshift('typings')
|
||||
this.pushDep(this._typingsModule)
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
let content = ''
|
||||
|
||||
const typingsImport = this._typingsModule.import(this)
|
||||
|
||||
content += [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
`import * as ${this._typingsModule.name} from "./${typingsImport}"`,
|
||||
`export * from "./${typingsImport}"`,
|
||||
'',
|
||||
`type TIntegration = sdk.DefaultIntegration<${this._typingsModule.name}.${this._typingsModule.exportName}>`,
|
||||
'',
|
||||
'export type IntegrationProps = sdk.IntegrationProps<TIntegration>',
|
||||
'',
|
||||
'export class Integration extends sdk.Integration<TIntegration> {}',
|
||||
'',
|
||||
'export type Client = sdk.IntegrationSpecificClient<TIntegration>',
|
||||
'',
|
||||
'// extra types',
|
||||
'',
|
||||
'type Cast<X, Y> = X extends Y ? X : Y',
|
||||
'type ValueOf<T> = T[keyof T]',
|
||||
'type AsyncFunction = (...args: any[]) => Promise<any>',
|
||||
'',
|
||||
"export type HandlerProps = Parameters<IntegrationProps['handler']>[0]",
|
||||
'',
|
||||
"export type Context = HandlerProps['ctx']",
|
||||
"export type Logger = HandlerProps['logger']",
|
||||
'',
|
||||
'export type CommonHandlerProps = {',
|
||||
' ctx: Context',
|
||||
' client: Client',
|
||||
' logger: Logger',
|
||||
'}',
|
||||
'',
|
||||
'export type ActionProps = {',
|
||||
" [K in keyof IntegrationProps['actions']]: Parameters<IntegrationProps['actions'][K]>[0]",
|
||||
'}',
|
||||
'export type AnyActionProps = ValueOf<ActionProps>',
|
||||
'',
|
||||
'export type MessageProps = {',
|
||||
" [TChannel in keyof IntegrationProps['channels']]: {",
|
||||
" [TMessage in keyof IntegrationProps['channels'][TChannel]['messages']]: Parameters<",
|
||||
" IntegrationProps['channels'][TChannel]['messages'][TMessage]",
|
||||
' >[0]',
|
||||
' }',
|
||||
'}',
|
||||
'export type AnyMessageProps = ValueOf<ValueOf<MessageProps>>',
|
||||
'',
|
||||
'export type AckFunctions = {',
|
||||
' [TChannel in keyof MessageProps]: {',
|
||||
" [TMessage in keyof MessageProps[TChannel]]: Cast<MessageProps[TChannel][TMessage], AnyMessageProps>['ack']",
|
||||
' }',
|
||||
'}',
|
||||
'export type AnyAckFunction = ValueOf<ValueOf<AckFunctions>>',
|
||||
'',
|
||||
'export type ClientOperation = ValueOf<{',
|
||||
' [K in keyof Client as Client[K] extends AsyncFunction ? K : never]: K',
|
||||
'}>',
|
||||
'export type ClientRequests = {',
|
||||
' [K in ClientOperation]: Parameters<Client[K]>[0]',
|
||||
'}',
|
||||
'export type ClientResponses = {',
|
||||
' [K in ClientOperation]: Awaited<ReturnType<Client[K]>>',
|
||||
'}',
|
||||
].join('\n')
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
type ActionInput = sdk.ActionDefinition['input']
|
||||
type ActionOutput = sdk.ActionDefinition['output']
|
||||
|
||||
export class ActionInputModule extends Module {
|
||||
public constructor(private _input: ActionInput) {
|
||||
const name = 'input'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._input.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionOutputModule extends Module {
|
||||
public constructor(private _output: ActionOutput) {
|
||||
const name = 'output'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._output.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionModule extends ReExportTypeModule {
|
||||
public constructor(actionName: string, action: sdk.ActionDefinition) {
|
||||
super({ exportName: strings.typeName(actionName) })
|
||||
|
||||
const inputModule = new ActionInputModule(action.input)
|
||||
const outputModule = new ActionOutputModule(action.output)
|
||||
|
||||
this.pushDep(inputModule)
|
||||
this.pushDep(outputModule)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionsModule extends ReExportTypeModule {
|
||||
public constructor(actions: Record<string, sdk.ActionDefinition>) {
|
||||
super({ exportName: strings.typeName('actions') })
|
||||
for (const [actionName, action] of Object.entries(actions)) {
|
||||
const module = new ActionModule(actionName, action)
|
||||
module.unshift(actionName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { GENERATED_HEADER, INDEX_FILE } from '../../consts'
|
||||
import { zuiSchemaToTypeScriptType, stringifySingleLine } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
class MessageModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _message: sdk.MessageDefinition
|
||||
) {
|
||||
super({
|
||||
path: `${name}.ts`,
|
||||
exportName: strings.typeName(name),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._message.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
class MessagesModule extends ReExportTypeModule {
|
||||
public constructor(channel: sdk.ChannelDefinition) {
|
||||
super({ exportName: strings.typeName('messages') })
|
||||
for (const [messageName, message] of Object.entries(channel.messages ?? {})) {
|
||||
const module = new MessageModule(messageName, message)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelModule extends Module {
|
||||
private _messagesModule: MessagesModule
|
||||
|
||||
public constructor(
|
||||
channelName: string,
|
||||
private _channel: sdk.ChannelDefinition
|
||||
) {
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName: strings.typeName(channelName),
|
||||
})
|
||||
|
||||
this._messagesModule = new MessagesModule(_channel)
|
||||
this._messagesModule.unshift('messages')
|
||||
this.pushDep(this._messagesModule)
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const messageImport = this._messagesModule.import(this)
|
||||
|
||||
const conversation = {
|
||||
tags: this._channel.conversation?.tags ?? {},
|
||||
creation: this._channel.conversation?.creation ?? { enabled: false, requiredTags: [] },
|
||||
}
|
||||
|
||||
const message = {
|
||||
tags: this._channel.message?.tags ?? {},
|
||||
}
|
||||
|
||||
return [
|
||||
GENERATED_HEADER,
|
||||
`import { ${this._messagesModule.exportName} } from './${messageImport}'`,
|
||||
`export * from './${messageImport}'`,
|
||||
'',
|
||||
`export type ${this.exportName} = {`,
|
||||
` messages: ${this._messagesModule.exportName}`,
|
||||
` message: ${stringifySingleLine(message)}`,
|
||||
` conversation: ${stringifySingleLine(conversation)}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelsModule extends ReExportTypeModule {
|
||||
public constructor(channels: Record<string, sdk.ChannelDefinition>) {
|
||||
super({ exportName: strings.typeName('channels') })
|
||||
for (const [channelName, channel] of Object.entries(channels)) {
|
||||
const module = new ChannelModule(channelName, channel)
|
||||
module.unshift(channelName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { INDEX_FILE } from '../../consts'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class DefaultConfigurationModule extends Module {
|
||||
public constructor(private _configuration: sdk.ConfigurationDefinition | undefined) {
|
||||
const name = 'configuration'
|
||||
const exportName = strings.typeName(name)
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName,
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
if (!this._configuration) {
|
||||
return [
|
||||
'/** Default Configuration of the Integration */',
|
||||
'export type Configuration = Record<string, never>;',
|
||||
].join('\n')
|
||||
}
|
||||
return zuiSchemaToTypeScriptType(this._configuration.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class ConfigurationModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _configuration: sdk.ConfigurationDefinition
|
||||
) {
|
||||
const configurationName = name
|
||||
const exportName = strings.typeName(`${configurationName}Config`)
|
||||
super({
|
||||
path: `${name}.ts`,
|
||||
exportName,
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const { schema } = this._configuration
|
||||
if (!schema) {
|
||||
return `export type ${this.exportName} = Record<string, never>;`
|
||||
}
|
||||
return zuiSchemaToTypeScriptType(schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigurationsModule extends ReExportTypeModule {
|
||||
public constructor(configurations: Record<string, sdk.ConfigurationDefinition>) {
|
||||
super({ exportName: strings.typeName('configurations') })
|
||||
for (const [configurationName, configuration] of Object.entries(configurations)) {
|
||||
const module = new ConfigurationModule(configurationName, configuration)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class EntityModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _entity: sdk.EntityDefinition
|
||||
) {
|
||||
const entityName = name
|
||||
const exportName = strings.typeName(entityName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._entity.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class EntitiesModule extends ReExportTypeModule {
|
||||
public constructor(entities: Record<string, sdk.EntityDefinition>) {
|
||||
super({ exportName: strings.typeName('entities') })
|
||||
|
||||
for (const [entityName, entity] of Object.entries(entities)) {
|
||||
const module = new EntityModule(entityName, entity)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class EventModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _event: sdk.EventDefinition
|
||||
) {
|
||||
const eventName = name
|
||||
const exportName = strings.typeName(eventName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._event.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class EventsModule extends ReExportTypeModule {
|
||||
public constructor(events: Record<string, sdk.EventDefinition>) {
|
||||
super({ exportName: strings.typeName('events') })
|
||||
for (const [eventName, event] of Object.entries(events)) {
|
||||
const module = new EventModule(eventName, event)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { GENERATED_HEADER, INDEX_FILE } from '../../consts'
|
||||
import { stringifySingleLine } from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import { ActionsModule } from './actions-module'
|
||||
import { ChannelsModule } from './channels-module'
|
||||
import { DefaultConfigurationModule } from './configuration-module'
|
||||
import { ConfigurationsModule } from './configurations-module'
|
||||
import { EntitiesModule } from './entities-module'
|
||||
import { EventsModule } from './events-module'
|
||||
import { StatesModule } from './states-module'
|
||||
|
||||
type IntegrationTypingsModuleDependencies = {
|
||||
defaultConfigModule: DefaultConfigurationModule
|
||||
configurationsModule: ConfigurationsModule
|
||||
actionsModule: ActionsModule
|
||||
channelsModule: ChannelsModule
|
||||
eventsModule: EventsModule
|
||||
statesModule: StatesModule
|
||||
entitiesModule: EntitiesModule
|
||||
}
|
||||
|
||||
export class IntegrationTypingsModule extends Module {
|
||||
private _dependencies: IntegrationTypingsModuleDependencies
|
||||
|
||||
public constructor(private _integration: sdk.IntegrationPackage['definition']) {
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName: 'TIntegration',
|
||||
})
|
||||
|
||||
const defaultConfigModule = new DefaultConfigurationModule(_integration.configuration)
|
||||
defaultConfigModule.unshift('configuration')
|
||||
|
||||
const configurationsModule = new ConfigurationsModule(_integration.configurations ?? {})
|
||||
configurationsModule.unshift('configurations')
|
||||
|
||||
const actionsModule = new ActionsModule(_integration.actions ?? {})
|
||||
actionsModule.unshift('actions')
|
||||
|
||||
const channelsModule = new ChannelsModule(_integration.channels ?? {})
|
||||
channelsModule.unshift('channels')
|
||||
|
||||
const eventsModule = new EventsModule(_integration.events ?? {})
|
||||
eventsModule.unshift('events')
|
||||
|
||||
const statesModule = new StatesModule(_integration.states ?? {})
|
||||
statesModule.unshift('states')
|
||||
|
||||
const entitiesModule = new EntitiesModule(_integration.entities ?? {})
|
||||
entitiesModule.unshift('entities')
|
||||
|
||||
this._dependencies = {
|
||||
defaultConfigModule,
|
||||
configurationsModule,
|
||||
actionsModule,
|
||||
channelsModule,
|
||||
eventsModule,
|
||||
statesModule,
|
||||
entitiesModule,
|
||||
}
|
||||
|
||||
for (const dep of Object.values(this._dependencies)) {
|
||||
this.pushDep(dep)
|
||||
}
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
let content = ''
|
||||
|
||||
const {
|
||||
defaultConfigModule,
|
||||
configurationsModule,
|
||||
actionsModule,
|
||||
channelsModule,
|
||||
eventsModule,
|
||||
statesModule,
|
||||
entitiesModule,
|
||||
} = this._dependencies
|
||||
|
||||
const defaultConfigImport = defaultConfigModule.import(this)
|
||||
const configurationsImport = configurationsModule.import(this)
|
||||
const actionsImport = actionsModule.import(this)
|
||||
const channelsImport = channelsModule.import(this)
|
||||
const eventsImport = eventsModule.import(this)
|
||||
const statesImport = statesModule.import(this)
|
||||
const entitiesImport = entitiesModule.import(this)
|
||||
|
||||
const user = {
|
||||
tags: this._integration.user?.tags ?? {},
|
||||
creation: this._integration.user?.creation ?? { enabled: false, requiredTags: [] },
|
||||
}
|
||||
|
||||
content += [
|
||||
GENERATED_HEADER,
|
||||
`import * as ${defaultConfigModule.name} from "./${defaultConfigImport}"`,
|
||||
`import * as ${configurationsModule.name} from "./${configurationsImport}"`,
|
||||
`import * as ${actionsModule.name} from "./${actionsImport}"`,
|
||||
`import * as ${channelsModule.name} from "./${channelsImport}"`,
|
||||
`import * as ${eventsModule.name} from "./${eventsImport}"`,
|
||||
`import * as ${statesModule.name} from "./${statesImport}"`,
|
||||
`import * as ${entitiesModule.name} from "./${entitiesImport}"`,
|
||||
`export * as ${defaultConfigModule.name} from "./${defaultConfigImport}"`,
|
||||
`export * as ${configurationsModule.name} from "./${configurationsImport}"`,
|
||||
`export * as ${actionsModule.name} from "./${actionsImport}"`,
|
||||
`export * as ${channelsModule.name} from "./${channelsImport}"`,
|
||||
`export * as ${eventsModule.name} from "./${eventsImport}"`,
|
||||
`export * as ${statesModule.name} from "./${statesImport}"`,
|
||||
`export * as ${entitiesModule.name} from "./${entitiesImport}"`,
|
||||
'',
|
||||
'export type TIntegration = {',
|
||||
` name: "${this._integration.name}"`,
|
||||
` version: "${this._integration.version}"`,
|
||||
` user: ${stringifySingleLine(user)}`,
|
||||
` configuration: ${defaultConfigModule.name}.${defaultConfigModule.exportName}`,
|
||||
` configurations: ${configurationsModule.name}.${configurationsModule.exportName}`,
|
||||
` actions: ${actionsModule.name}.${actionsModule.exportName}`,
|
||||
` channels: ${channelsModule.name}.${channelsModule.exportName}`,
|
||||
` events: ${eventsModule.name}.${eventsModule.exportName}`,
|
||||
` states: ${statesModule.name}.${statesModule.exportName}`,
|
||||
` entities: ${entitiesModule.name}.${entitiesModule.exportName}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../../consts'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class StatePayloadModule extends Module {
|
||||
public constructor(private _state: sdk.StateDefinition) {
|
||||
super({
|
||||
path: 'payload.ts',
|
||||
exportName: strings.typeName('Payload'),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return gen.zuiSchemaToTypeScriptType(this._state.schema, 'Payload')
|
||||
}
|
||||
}
|
||||
|
||||
export class StateModule extends Module {
|
||||
private _payloadModule: StatePayloadModule
|
||||
|
||||
public constructor(
|
||||
private _name: string,
|
||||
private _state: sdk.StateDefinition
|
||||
) {
|
||||
super({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: strings.typeName(_name),
|
||||
})
|
||||
|
||||
this._payloadModule = new StatePayloadModule(_state)
|
||||
this.pushDep(this._payloadModule)
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
const { _payloadModule } = this
|
||||
const payloadImport = _payloadModule.import(this)
|
||||
|
||||
const exportName = strings.typeName(this._name)
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
`import * as ${_payloadModule.name} from './${payloadImport}'`,
|
||||
`export type ${exportName} = {`,
|
||||
` type: ${gen.primitiveToTypescriptValue(this._state.type)},`,
|
||||
` payload: ${_payloadModule.name}.${_payloadModule.exportName}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
export class StatesModule extends ReExportTypeModule {
|
||||
public constructor(states: Record<string, sdk.StateDefinition>) {
|
||||
super({ exportName: strings.typeName('states') })
|
||||
for (const [stateName, state] of Object.entries(states)) {
|
||||
const module = new StateModule(stateName, state)
|
||||
module.unshift(stateName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as consts from '../consts'
|
||||
import * as gen from '../generators'
|
||||
import * as types from '../typings'
|
||||
import { IntegrationPackageDefinitionModule } from './integration-package-definition'
|
||||
|
||||
const generateIntegrationPackageModule = (
|
||||
definitionImport: string,
|
||||
pkg: types.IntegrationInstallablePackage
|
||||
): string => {
|
||||
const id = pkg.integration.id ?? pkg.devId
|
||||
const uri = pkg.path
|
||||
|
||||
const tsId = gen.primitiveToTypescriptValue(id)
|
||||
const tsUri = gen.primitiveToTypescriptValue(uri)
|
||||
const tsName = gen.primitiveToTypescriptValue(pkg.name)
|
||||
const tsVersion = gen.primitiveToTypescriptValue(pkg.version)
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
'',
|
||||
`import definition from "${definitionImport}"`,
|
||||
'',
|
||||
'export default {',
|
||||
' type: "integration",',
|
||||
` id: ${tsId},`,
|
||||
` uri: ${tsUri},`,
|
||||
` name: ${tsName},`,
|
||||
` version: ${tsVersion},`,
|
||||
' definition,',
|
||||
'} satisfies sdk.IntegrationPackage',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export const generateIntegrationPackage = async (pkg: types.IntegrationInstallablePackage): Promise<types.File[]> => {
|
||||
const definitionDir = 'definition'
|
||||
const definitionModule = new IntegrationPackageDefinitionModule(pkg.integration)
|
||||
definitionModule.unshift(definitionDir)
|
||||
|
||||
const definitionFiles = await definitionModule.flatten()
|
||||
return [
|
||||
...definitionFiles,
|
||||
{
|
||||
path: consts.INDEX_FILE,
|
||||
content: generateIntegrationPackageModule(`./${definitionDir}`, pkg),
|
||||
},
|
||||
]
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
type ActionInput = types.ActionDefinition['input']
|
||||
type ActionOutput = types.ActionDefinition['output']
|
||||
|
||||
export class ActionInputModule extends Module {
|
||||
public constructor(private _input: ActionInput) {
|
||||
const name = 'input'
|
||||
const exportName = strings.varName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(this._input.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionOutputModule extends Module {
|
||||
public constructor(private _output: ActionOutput) {
|
||||
const name = 'output'
|
||||
const exportName = strings.varName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(this._output.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionModule extends ReExportVariableModule {
|
||||
public constructor(actionName: string, action: types.ActionDefinition) {
|
||||
super({
|
||||
exportName: strings.varName(actionName),
|
||||
extraProps: {
|
||||
...gen.primitiveRecordToTypescriptValues({
|
||||
title: action.title,
|
||||
description: action.description,
|
||||
billable: action.billable,
|
||||
cacheable: action.cacheable,
|
||||
}),
|
||||
...(action.attributes ? { attributes: gen.stringifySingleLine(action.attributes) } : undefined),
|
||||
},
|
||||
})
|
||||
|
||||
const inputModule = new ActionInputModule(action.input)
|
||||
const outputModule = new ActionOutputModule(action.output)
|
||||
|
||||
this.pushDep(inputModule)
|
||||
this.pushDep(outputModule)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionsModule extends ReExportVariableModule {
|
||||
public constructor(actions: Record<string, types.ActionDefinition>) {
|
||||
super({ exportName: strings.varName('actions') })
|
||||
for (const [actionName, action] of Object.entries(actions)) {
|
||||
const module = new ActionModule(actionName, action)
|
||||
module.unshift(actionName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { GENERATED_HEADER, INDEX_FILE } from '../../consts'
|
||||
import { jsonSchemaToTypescriptZuiSchema, stringifySingleLine } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
class MessageModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _message: types.MessageDefinition
|
||||
) {
|
||||
super({
|
||||
path: `${name}.ts`,
|
||||
exportName: strings.varName(name),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(
|
||||
this._message.schema,
|
||||
this.exportName,
|
||||
gen.primitiveRecordToTypescriptValues({
|
||||
title: this._message.title,
|
||||
description: this._message.description,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class MessagesModule extends ReExportVariableModule {
|
||||
public constructor(channel: types.ChannelDefinition) {
|
||||
super({ exportName: strings.varName('messages') })
|
||||
for (const [messageName, message] of Object.entries(channel.messages ?? {})) {
|
||||
const module = new MessageModule(messageName, message)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelModule extends Module {
|
||||
private _messagesModule: MessagesModule
|
||||
|
||||
public constructor(
|
||||
channelName: string,
|
||||
private _channel: types.ChannelDefinition
|
||||
) {
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName: strings.varName(channelName),
|
||||
})
|
||||
|
||||
this._messagesModule = new MessagesModule(_channel)
|
||||
this._messagesModule.unshift('messages')
|
||||
this.pushDep(this._messagesModule)
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const messageImport = this._messagesModule.import(this)
|
||||
|
||||
const conversation = {
|
||||
tags: this._channel.conversation?.tags ?? {},
|
||||
creation: this._channel.conversation?.creation ?? { enabled: false, requiredTags: [] },
|
||||
}
|
||||
|
||||
const message = {
|
||||
tags: this._channel.message?.tags ?? {},
|
||||
}
|
||||
|
||||
return [
|
||||
GENERATED_HEADER,
|
||||
`import { ${this._messagesModule.exportName} } from './${messageImport}'`,
|
||||
`export * from './${messageImport}'`,
|
||||
'',
|
||||
`export const ${this.exportName} = {`,
|
||||
` title: ${gen.primitiveToTypescriptValue(this._channel.title)},`,
|
||||
` description: ${gen.primitiveToTypescriptValue(this._channel.description)},`,
|
||||
` messages: ${this._messagesModule.exportName},`,
|
||||
` message: ${stringifySingleLine(message)},`,
|
||||
` conversation: ${stringifySingleLine(conversation)},`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelsModule extends ReExportVariableModule {
|
||||
public constructor(channels: Record<string, types.ChannelDefinition>) {
|
||||
super({ exportName: strings.varName('channels') })
|
||||
for (const [channelName, channel] of Object.entries(channels)) {
|
||||
const module = new ChannelModule(channelName, channel)
|
||||
module.unshift(channelName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { JSONSchema7 } from 'json-schema'
|
||||
import { INDEX_FILE } from '../../consts'
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class DefaultConfigurationModule extends Module {
|
||||
public constructor(private _configuration: types.ConfigurationDefinition) {
|
||||
const name = 'configuration'
|
||||
const exportName = strings.varName(name)
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName,
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const schema: JSONSchema7 = this._configuration.schema ?? { type: 'object', properties: {} }
|
||||
return jsonSchemaToTypescriptZuiSchema(
|
||||
schema,
|
||||
this.exportName,
|
||||
gen.primitiveRecordToTypescriptValues({
|
||||
title: this._configuration.title,
|
||||
description: this._configuration.description,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { JSONSchema7 } from 'json-schema'
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class ConfigurationModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _configuration: types.ConfigurationDefinition
|
||||
) {
|
||||
const configurationName = name
|
||||
const exportName = strings.varName(`${configurationName}Config`)
|
||||
super({
|
||||
path: `${name}.ts`,
|
||||
exportName,
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const schema: JSONSchema7 = this._configuration.schema ?? { type: 'object', properties: {} }
|
||||
return jsonSchemaToTypescriptZuiSchema(
|
||||
schema,
|
||||
this.exportName,
|
||||
gen.primitiveRecordToTypescriptValues({
|
||||
title: this._configuration.title,
|
||||
description: this._configuration.description,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigurationsModule extends ReExportVariableModule {
|
||||
public constructor(configurations: Record<string, types.ConfigurationDefinition>) {
|
||||
super({ exportName: strings.varName('configurations') })
|
||||
for (const [configurationName, configuration] of Object.entries(configurations)) {
|
||||
const module = new ConfigurationModule(configurationName, configuration)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class EntityModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _entity: types.EntityDefinition
|
||||
) {
|
||||
const entityName = name
|
||||
const exportName = strings.varName(entityName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(
|
||||
this._entity.schema,
|
||||
this.exportName,
|
||||
gen.primitiveRecordToTypescriptValues({
|
||||
title: this._entity.title,
|
||||
description: this._entity.description,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class EntitiesModule extends ReExportVariableModule {
|
||||
public constructor(entities: Record<string, types.EntityDefinition>) {
|
||||
super({ exportName: strings.varName('entities') })
|
||||
|
||||
for (const [entityName, entity] of Object.entries(entities)) {
|
||||
const module = new EntityModule(entityName, entity)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class EventModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _event: types.EventDefinition
|
||||
) {
|
||||
const eventName = name
|
||||
const exportName = strings.varName(eventName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(this._event.schema, this.exportName, {
|
||||
...gen.primitiveRecordToTypescriptValues({
|
||||
title: this._event.title,
|
||||
description: this._event.description,
|
||||
}),
|
||||
...(this._event.attributes ? { attributes: gen.stringifySingleLine(this._event.attributes) } : undefined),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class EventsModule extends ReExportVariableModule {
|
||||
public constructor(events: Record<string, types.EventDefinition>) {
|
||||
super({ exportName: strings.varName('events') })
|
||||
for (const [eventName, event] of Object.entries(events)) {
|
||||
const module = new EventModule(eventName, event)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import * as consts from '../../consts'
|
||||
import { stringifySingleLine } from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import { ActionsModule } from './actions-module'
|
||||
import { ChannelsModule } from './channels-module'
|
||||
import { DefaultConfigurationModule } from './configuration-module'
|
||||
import { ConfigurationsModule } from './configurations-module'
|
||||
import { EntitiesModule } from './entities-module'
|
||||
import { EventsModule } from './events-module'
|
||||
import { InterfacesModule } from './interfaces-module'
|
||||
import { StatesModule } from './states-module'
|
||||
import * as types from './typings'
|
||||
|
||||
type IntegrationPackageModuleDependencies = {
|
||||
defaultConfigModule: DefaultConfigurationModule
|
||||
configurationsModule: ConfigurationsModule
|
||||
actionsModule: ActionsModule
|
||||
channelsModule: ChannelsModule
|
||||
eventsModule: EventsModule
|
||||
statesModule: StatesModule
|
||||
entitiesModule: EntitiesModule
|
||||
interfacesModule: InterfacesModule
|
||||
}
|
||||
|
||||
export class IntegrationPackageDefinitionModule extends Module {
|
||||
private _dependencies: IntegrationPackageModuleDependencies
|
||||
|
||||
public constructor(private _integration: types.IntegrationDefinition) {
|
||||
super({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: consts.DEFAULT_EXPORT_NAME,
|
||||
})
|
||||
|
||||
const defaultConfigModule = new DefaultConfigurationModule(_integration.configuration ?? {})
|
||||
defaultConfigModule.unshift('configuration')
|
||||
|
||||
const configurationsModule = new ConfigurationsModule(_integration.configurations ?? {})
|
||||
configurationsModule.unshift('configurations')
|
||||
|
||||
const actionsModule = new ActionsModule(_integration.actions ?? {})
|
||||
actionsModule.unshift('actions')
|
||||
|
||||
const channelsModule = new ChannelsModule(_integration.channels ?? {})
|
||||
channelsModule.unshift('channels')
|
||||
|
||||
const eventsModule = new EventsModule(_integration.events ?? {})
|
||||
eventsModule.unshift('events')
|
||||
|
||||
const statesModule = new StatesModule(_integration.states ?? {})
|
||||
statesModule.unshift('states')
|
||||
|
||||
const entitiesModule = new EntitiesModule(_integration.entities ?? {})
|
||||
entitiesModule.unshift('entities')
|
||||
|
||||
const interfacesModule = new InterfacesModule(_integration.interfaces ?? {})
|
||||
interfacesModule.unshift('interfaces')
|
||||
|
||||
this._dependencies = {
|
||||
defaultConfigModule,
|
||||
configurationsModule,
|
||||
actionsModule,
|
||||
channelsModule,
|
||||
eventsModule,
|
||||
statesModule,
|
||||
entitiesModule,
|
||||
interfacesModule,
|
||||
}
|
||||
|
||||
for (const dep of Object.values(this._dependencies)) {
|
||||
this.pushDep(dep)
|
||||
}
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
let content = ''
|
||||
|
||||
const {
|
||||
defaultConfigModule,
|
||||
configurationsModule,
|
||||
actionsModule,
|
||||
channelsModule,
|
||||
eventsModule,
|
||||
statesModule,
|
||||
entitiesModule,
|
||||
interfacesModule,
|
||||
} = this._dependencies
|
||||
|
||||
const defaultConfigImport = defaultConfigModule.import(this)
|
||||
const configurationsImport = configurationsModule.import(this)
|
||||
const actionsImport = actionsModule.import(this)
|
||||
const channelsImport = channelsModule.import(this)
|
||||
const eventsImport = eventsModule.import(this)
|
||||
const statesImport = statesModule.import(this)
|
||||
const entitiesImport = entitiesModule.import(this)
|
||||
const interfacesImport = interfacesModule.import(this)
|
||||
|
||||
const user = {
|
||||
tags: this._integration.user?.tags ?? {},
|
||||
creation: this._integration.user?.creation ?? { enabled: false, requiredTags: [] },
|
||||
}
|
||||
|
||||
const integrationAttributes = this._integration.attributes
|
||||
? stringifySingleLine(this._integration.attributes)
|
||||
: '{}'
|
||||
|
||||
content += [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
'',
|
||||
`import * as ${defaultConfigModule.name} from "./${defaultConfigImport}"`,
|
||||
`import * as ${configurationsModule.name} from "./${configurationsImport}"`,
|
||||
`import * as ${actionsModule.name} from "./${actionsImport}"`,
|
||||
`import * as ${channelsModule.name} from "./${channelsImport}"`,
|
||||
`import * as ${eventsModule.name} from "./${eventsImport}"`,
|
||||
`import * as ${statesModule.name} from "./${statesImport}"`,
|
||||
`import * as ${entitiesModule.name} from "./${entitiesImport}"`,
|
||||
`import * as ${interfacesModule.name} from "./${interfacesImport}"`,
|
||||
`export * as ${defaultConfigModule.name} from "./${defaultConfigImport}"`,
|
||||
`export * as ${configurationsModule.name} from "./${configurationsImport}"`,
|
||||
`export * as ${actionsModule.name} from "./${actionsImport}"`,
|
||||
`export * as ${channelsModule.name} from "./${channelsImport}"`,
|
||||
`export * as ${eventsModule.name} from "./${eventsImport}"`,
|
||||
`export * as ${statesModule.name} from "./${statesImport}"`,
|
||||
`export * as ${entitiesModule.name} from "./${entitiesImport}"`,
|
||||
`export * as ${interfacesModule.name} from "./${interfacesImport}"`,
|
||||
'',
|
||||
'export default {',
|
||||
` name: "${this._integration.name}",`,
|
||||
` version: "${this._integration.version}",`,
|
||||
` attributes: ${integrationAttributes},`,
|
||||
` user: ${stringifySingleLine(user)},`,
|
||||
` configuration: ${defaultConfigModule.name}.${defaultConfigModule.exportName},`,
|
||||
` configurations: ${configurationsModule.name}.${configurationsModule.exportName},`,
|
||||
` actions: ${actionsModule.name}.${actionsModule.exportName},`,
|
||||
` channels: ${channelsModule.name}.${channelsModule.exportName},`,
|
||||
` events: ${eventsModule.name}.${eventsModule.exportName},`,
|
||||
` states: ${statesModule.name}.${statesModule.exportName},`,
|
||||
` entities: ${entitiesModule.name}.${entitiesModule.exportName},`,
|
||||
` interfaces: ${interfacesModule.name}.${interfacesModule.exportName},`,
|
||||
'} satisfies sdk.IntegrationPackage["definition"]',
|
||||
].join('\n')
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class InterfaceModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _interface: types.InterfaceExtension
|
||||
) {
|
||||
const exportName = strings.varName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return `export const ${this.exportName} = ${JSON.stringify(this._interface, null, 2)}`
|
||||
}
|
||||
}
|
||||
|
||||
export class InterfacesModule extends ReExportVariableModule {
|
||||
public constructor(interfaces: Record<string, types.InterfaceExtension>) {
|
||||
super({ exportName: strings.varName('interfaces') })
|
||||
for (const [interfaceName, intrface] of Object.entries(interfaces)) {
|
||||
const module = new InterfaceModule(interfaceName, intrface)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class StateModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _state: types.StateDefinition
|
||||
) {
|
||||
super({
|
||||
path: `${name}.ts`,
|
||||
exportName: strings.varName(name),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(this._state.schema, this.exportName, {
|
||||
type: `"${this._state.type}" as const`,
|
||||
...gen.primitiveRecordToTypescriptValues({
|
||||
title: this._state.title,
|
||||
description: this._state.description,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class StatesModule extends ReExportVariableModule {
|
||||
public constructor(states: Record<string, types.StateDefinition>) {
|
||||
super({ exportName: strings.varName('states') })
|
||||
for (const [stateName, state] of Object.entries(states)) {
|
||||
const module = new StateModule(stateName, state)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import * as types from '../../typings'
|
||||
export type IntegrationDefinition = types.IntegrationDefinition
|
||||
export type ActionDefinition = NonNullable<IntegrationDefinition['actions']>[string]
|
||||
export type ChannelDefinition = NonNullable<IntegrationDefinition['channels']>[string]
|
||||
export type MessageDefinition = NonNullable<ChannelDefinition['messages']>[string]
|
||||
export type ConfigurationDefinition = NonNullable<IntegrationDefinition['configurations']>[string]
|
||||
export type EntityDefinition = NonNullable<IntegrationDefinition['entities']>[string]
|
||||
export type EventDefinition = NonNullable<IntegrationDefinition['events']>[string]
|
||||
export type StateDefinition = NonNullable<IntegrationDefinition['states']>[string]
|
||||
export type InterfaceExtension = NonNullable<IntegrationDefinition['interfaces']>[string]
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* An interface has no implementation, but its typings are used by the plugin and bot implementations.
|
||||
*/
|
||||
export { InterfaceTypingsModule } from './integration-typings'
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
type ActionInput = sdk.ActionDefinition['input']
|
||||
type ActionOutput = sdk.ActionDefinition['output']
|
||||
|
||||
export class ActionInputModule extends Module {
|
||||
public constructor(private _input: ActionInput) {
|
||||
const name = 'input'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._input.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionOutputModule extends Module {
|
||||
public constructor(private _output: ActionOutput) {
|
||||
const name = 'output'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._output.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionModule extends ReExportTypeModule {
|
||||
public constructor(actionName: string, action: sdk.ActionDefinition) {
|
||||
super({ exportName: strings.typeName(actionName) })
|
||||
|
||||
const inputModule = new ActionInputModule(action.input)
|
||||
const outputModule = new ActionOutputModule(action.output)
|
||||
|
||||
this.pushDep(inputModule)
|
||||
this.pushDep(outputModule)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionsModule extends ReExportTypeModule {
|
||||
public constructor(actions: Record<string, sdk.ActionDefinition>) {
|
||||
super({ exportName: strings.typeName('actions') })
|
||||
for (const [actionName, action] of Object.entries(actions)) {
|
||||
const module = new ActionModule(actionName, action)
|
||||
module.unshift(actionName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { GENERATED_HEADER, INDEX_FILE } from '../../consts'
|
||||
import { zuiSchemaToTypeScriptType, stringifySingleLine } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
class MessageModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _message: sdk.MessageDefinition
|
||||
) {
|
||||
super({
|
||||
path: `${name}.ts`,
|
||||
exportName: strings.typeName(name),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._message.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
class MessagesModule extends ReExportTypeModule {
|
||||
public constructor(channel: sdk.ChannelDefinition) {
|
||||
super({ exportName: strings.typeName('messages') })
|
||||
for (const [messageName, message] of Object.entries(channel.messages ?? {})) {
|
||||
const module = new MessageModule(messageName, message)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelModule extends Module {
|
||||
private _messagesModule: MessagesModule
|
||||
|
||||
public constructor(
|
||||
channelName: string,
|
||||
private _channel: sdk.ChannelDefinition
|
||||
) {
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName: strings.typeName(channelName),
|
||||
})
|
||||
|
||||
this._messagesModule = new MessagesModule(_channel)
|
||||
this._messagesModule.unshift('messages')
|
||||
this.pushDep(this._messagesModule)
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const messageImport = this._messagesModule.import(this)
|
||||
|
||||
const conversation = {
|
||||
tags: this._channel.conversation?.tags ?? {},
|
||||
creation: this._channel.conversation?.creation ?? { enabled: false, requiredTags: [] },
|
||||
}
|
||||
|
||||
const message = {
|
||||
tags: this._channel.message?.tags ?? {},
|
||||
}
|
||||
|
||||
return [
|
||||
GENERATED_HEADER,
|
||||
`import { ${this._messagesModule.exportName} } from './${messageImport}'`,
|
||||
`export * from './${messageImport}'`,
|
||||
'',
|
||||
`export type ${this.exportName} = {`,
|
||||
` messages: ${this._messagesModule.exportName}`,
|
||||
` message: ${stringifySingleLine(message)}`,
|
||||
` conversation: ${stringifySingleLine(conversation)}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelsModule extends ReExportTypeModule {
|
||||
public constructor(channels: Record<string, sdk.ChannelDefinition>) {
|
||||
super({ exportName: strings.typeName('channels') })
|
||||
for (const [channelName, channel] of Object.entries(channels)) {
|
||||
const module = new ChannelModule(channelName, channel)
|
||||
module.unshift(channelName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class EntityModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _entity: sdk.EntityDefinition
|
||||
) {
|
||||
const entityName = name
|
||||
const exportName = strings.typeName(entityName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._entity.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class EntitiesModule extends ReExportTypeModule {
|
||||
public constructor(entities: Record<string, sdk.EntityDefinition>) {
|
||||
super({ exportName: strings.typeName('entities') })
|
||||
|
||||
for (const [entityName, entity] of Object.entries(entities)) {
|
||||
const module = new EntityModule(entityName, entity)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class EventModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _event: sdk.EventDefinition
|
||||
) {
|
||||
const eventName = name
|
||||
const exportName = strings.typeName(eventName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._event.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class EventsModule extends ReExportTypeModule {
|
||||
public constructor(events: Record<string, sdk.EventDefinition>) {
|
||||
super({ exportName: strings.typeName('events') })
|
||||
for (const [eventName, event] of Object.entries(events)) {
|
||||
const module = new EventModule(eventName, event)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import _ from 'lodash'
|
||||
import { GENERATED_HEADER, INDEX_FILE } from '../../consts'
|
||||
import { Module } from '../../module'
|
||||
import { ActionsModule } from './actions-module'
|
||||
import { ChannelsModule } from './channels-module'
|
||||
import { EntitiesModule } from './entities-module'
|
||||
import { EventsModule } from './events-module'
|
||||
|
||||
type InterfaceTypingsModuleDependencies = {
|
||||
actionsModule: ActionsModule
|
||||
channelsModule: ChannelsModule
|
||||
eventsModule: EventsModule
|
||||
entitiesModule: EntitiesModule
|
||||
}
|
||||
|
||||
export class InterfaceTypingsModule extends Module {
|
||||
private _dependencies: InterfaceTypingsModuleDependencies
|
||||
|
||||
public constructor(private _interface: sdk.InterfacePackage['definition']) {
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName: 'TInterface',
|
||||
})
|
||||
|
||||
const references: Record<string, sdk.z.Schema> = _.mapValues(_interface.entities, (e) => e.schema)
|
||||
|
||||
type ZodObjectSchema = sdk.z.ZodObject | sdk.z.ZodRecord
|
||||
const derefObject = (obj: { schema: ZodObjectSchema }) => {
|
||||
return {
|
||||
...obj,
|
||||
schema: obj.schema.dereference(references) as ZodObjectSchema,
|
||||
}
|
||||
}
|
||||
|
||||
_interface = {
|
||||
..._interface,
|
||||
actions: _.mapValues(_interface.actions, (a) => ({
|
||||
...a,
|
||||
input: derefObject(a.input),
|
||||
output: derefObject(a.output),
|
||||
})),
|
||||
channels: _.mapValues(_interface.channels, (c) => ({
|
||||
...c,
|
||||
messages: _.mapValues(c.messages, (m) => derefObject(m)),
|
||||
})),
|
||||
events: _.mapValues(_interface.events, (e) => derefObject(e)),
|
||||
}
|
||||
|
||||
const actionsModule = new ActionsModule(_interface.actions ?? {})
|
||||
actionsModule.unshift('actions')
|
||||
|
||||
const channelsModule = new ChannelsModule(_interface.channels ?? {})
|
||||
channelsModule.unshift('channels')
|
||||
|
||||
const eventsModule = new EventsModule(_interface.events ?? {})
|
||||
eventsModule.unshift('events')
|
||||
|
||||
const entitiesModule = new EntitiesModule(_interface.entities ?? {})
|
||||
entitiesModule.unshift('entities')
|
||||
|
||||
this._dependencies = {
|
||||
actionsModule,
|
||||
channelsModule,
|
||||
eventsModule,
|
||||
entitiesModule,
|
||||
}
|
||||
|
||||
for (const dep of Object.values(this._dependencies)) {
|
||||
this.pushDep(dep)
|
||||
}
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
let content = ''
|
||||
|
||||
const { actionsModule, channelsModule, eventsModule, entitiesModule } = this._dependencies
|
||||
|
||||
const actionsImport = actionsModule.import(this)
|
||||
const channelsImport = channelsModule.import(this)
|
||||
const eventsImport = eventsModule.import(this)
|
||||
const entitiesImport = entitiesModule.import(this)
|
||||
|
||||
content += [
|
||||
GENERATED_HEADER,
|
||||
`import * as ${actionsModule.name} from "./${actionsImport}"`,
|
||||
`import * as ${channelsModule.name} from "./${channelsImport}"`,
|
||||
`import * as ${eventsModule.name} from "./${eventsImport}"`,
|
||||
`import * as ${entitiesModule.name} from "./${entitiesImport}"`,
|
||||
`export * as ${actionsModule.name} from "./${actionsImport}"`,
|
||||
`export * as ${channelsModule.name} from "./${channelsImport}"`,
|
||||
`export * as ${eventsModule.name} from "./${eventsImport}"`,
|
||||
`export * as ${entitiesModule.name} from "./${entitiesImport}"`,
|
||||
'',
|
||||
'export type TInterface = {',
|
||||
` name: "${this._interface.name}"`,
|
||||
` version: "${this._interface.version}"`,
|
||||
` actions: ${actionsModule.name}.${actionsModule.exportName}`,
|
||||
` channels: ${channelsModule.name}.${channelsModule.exportName}`,
|
||||
` events: ${eventsModule.name}.${eventsModule.exportName}`,
|
||||
` entities: ${entitiesModule.name}.${entitiesModule.exportName}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as consts from '../consts'
|
||||
import * as gen from '../generators'
|
||||
import * as types from '../typings'
|
||||
import { InterfacePackageDefinitionModule } from './interface-package-definition'
|
||||
|
||||
const generateInterfacePackageModule = (definitionImport: string, pkg: types.InterfaceInstallablePackage): string => {
|
||||
const id = pkg.interface.id
|
||||
const uri = pkg.path
|
||||
|
||||
const tsId = gen.primitiveToTypescriptValue(id)
|
||||
const tsUri = gen.primitiveToTypescriptValue(uri)
|
||||
const tsName = gen.primitiveToTypescriptValue(pkg.name)
|
||||
const tsVersion = gen.primitiveToTypescriptValue(pkg.version)
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
'',
|
||||
`import definition from "${definitionImport}"`,
|
||||
'',
|
||||
'export default {',
|
||||
' type: "interface",',
|
||||
` id: ${tsId},`,
|
||||
` uri: ${tsUri},`,
|
||||
` name: ${tsName},`,
|
||||
` version: ${tsVersion},`,
|
||||
' definition,',
|
||||
'} satisfies sdk.InterfacePackage',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export const generateInterfacePackage = async (pkg: types.InterfaceInstallablePackage): Promise<types.File[]> => {
|
||||
const definitionDir = 'definition'
|
||||
const definitionModule = new InterfacePackageDefinitionModule(pkg.interface)
|
||||
definitionModule.unshift(definitionDir)
|
||||
|
||||
const definitionFiles = await definitionModule.flatten()
|
||||
return [
|
||||
...definitionFiles,
|
||||
{
|
||||
path: consts.INDEX_FILE,
|
||||
content: generateInterfacePackageModule(`./${definitionDir}`, pkg),
|
||||
},
|
||||
]
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
type ActionInput = types.ActionDefinition['input']
|
||||
type ActionOutput = types.ActionDefinition['output']
|
||||
|
||||
export class ActionInputModule extends Module {
|
||||
public constructor(private _input: ActionInput) {
|
||||
const name = 'input'
|
||||
const exportName = strings.varName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(this._input.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionOutputModule extends Module {
|
||||
public constructor(private _output: ActionOutput) {
|
||||
const name = 'output'
|
||||
const exportName = strings.varName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(this._output.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionModule extends ReExportVariableModule {
|
||||
public constructor(actionName: string, action: types.ActionDefinition) {
|
||||
super({
|
||||
exportName: strings.varName(actionName),
|
||||
extraProps: {
|
||||
...gen.primitiveRecordToTypescriptValues({
|
||||
title: action.title,
|
||||
description: action.description,
|
||||
billable: action.billable,
|
||||
cacheable: action.cacheable,
|
||||
}),
|
||||
...(action.attributes ? { attributes: gen.stringifySingleLine(action.attributes) } : undefined),
|
||||
},
|
||||
})
|
||||
|
||||
const inputModule = new ActionInputModule(action.input)
|
||||
const outputModule = new ActionOutputModule(action.output)
|
||||
|
||||
this.pushDep(inputModule)
|
||||
this.pushDep(outputModule)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionsModule extends ReExportVariableModule {
|
||||
public constructor(actions: Record<string, types.ActionDefinition>) {
|
||||
super({ exportName: strings.varName('actions') })
|
||||
for (const [actionName, action] of Object.entries(actions)) {
|
||||
const module = new ActionModule(actionName, action)
|
||||
module.unshift(actionName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { GENERATED_HEADER, INDEX_FILE } from '../../consts'
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
class MessageModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _message: types.MessageDefinition
|
||||
) {
|
||||
super({
|
||||
path: `${name}.ts`,
|
||||
exportName: strings.varName(name),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(
|
||||
this._message.schema,
|
||||
this.exportName,
|
||||
gen.primitiveRecordToTypescriptValues({
|
||||
title: this._message.title,
|
||||
description: this._message.description,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class MessagesModule extends ReExportVariableModule {
|
||||
public constructor(channel: types.ChannelDefinition) {
|
||||
super({ exportName: strings.varName('messages') })
|
||||
for (const [messageName, message] of Object.entries(channel.messages ?? {})) {
|
||||
const module = new MessageModule(messageName, message)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelModule extends Module {
|
||||
private _messagesModule: MessagesModule
|
||||
|
||||
public constructor(
|
||||
channelName: string,
|
||||
private _channel: types.ChannelDefinition
|
||||
) {
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName: strings.varName(channelName),
|
||||
})
|
||||
|
||||
this._messagesModule = new MessagesModule(_channel)
|
||||
this._messagesModule.unshift('messages')
|
||||
this.pushDep(this._messagesModule)
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const messageImport = this._messagesModule.import(this)
|
||||
|
||||
return [
|
||||
GENERATED_HEADER,
|
||||
`import { ${this._messagesModule.exportName} } from './${messageImport}'`,
|
||||
`export * from './${messageImport}'`,
|
||||
'',
|
||||
`export const ${this.exportName} = {`,
|
||||
` messages: ${this._messagesModule.exportName},`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelsModule extends ReExportVariableModule {
|
||||
public constructor(channels: Record<string, types.ChannelDefinition>) {
|
||||
super({ exportName: strings.varName('channels') })
|
||||
for (const [channelName, channel] of Object.entries(channels)) {
|
||||
const module = new ChannelModule(channelName, channel)
|
||||
module.unshift(channelName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class EntityModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _entity: types.EntityDefinition
|
||||
) {
|
||||
const entityName = name
|
||||
const exportName = strings.varName(entityName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(
|
||||
this._entity.schema,
|
||||
this.exportName,
|
||||
gen.primitiveRecordToTypescriptValues({
|
||||
title: this._entity.title,
|
||||
description: this._entity.description,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class EntitiesModule extends ReExportVariableModule {
|
||||
public constructor(entities: Record<string, types.EntityDefinition>) {
|
||||
super({ exportName: strings.varName('entities') })
|
||||
|
||||
for (const [entityName, entity] of Object.entries(entities)) {
|
||||
const module = new EntityModule(entityName, entity)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class EventModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _event: types.EventDefinition
|
||||
) {
|
||||
const eventName = name
|
||||
const exportName = strings.varName(eventName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
if (!this._event.schema) {
|
||||
return `export const ${this.exportName} = z.object({});`
|
||||
}
|
||||
return jsonSchemaToTypescriptZuiSchema(this._event.schema, this.exportName, {
|
||||
...gen.primitiveRecordToTypescriptValues({
|
||||
title: this._event.title,
|
||||
description: this._event.description,
|
||||
}),
|
||||
...(this._event.attributes ? { attributes: gen.stringifySingleLine(this._event.attributes) } : undefined),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class EventsModule extends ReExportVariableModule {
|
||||
public constructor(events: Record<string, types.EventDefinition>) {
|
||||
super({ exportName: strings.varName('events') })
|
||||
for (const [eventName, event] of Object.entries(events)) {
|
||||
const module = new EventModule(eventName, event)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import * as consts from '../../consts'
|
||||
import { stringifySingleLine } from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import { ActionsModule } from './actions-module'
|
||||
import { ChannelsModule } from './channels-module'
|
||||
import { EntitiesModule } from './entities-module'
|
||||
import { EventsModule } from './events-module'
|
||||
import * as types from './typings'
|
||||
|
||||
type InterfacePackageModuleDependencies = {
|
||||
actionsModule: ActionsModule
|
||||
channelsModule: ChannelsModule
|
||||
eventsModule: EventsModule
|
||||
entitiesModule: EntitiesModule
|
||||
}
|
||||
|
||||
export class InterfacePackageDefinitionModule extends Module {
|
||||
private _dependencies: InterfacePackageModuleDependencies
|
||||
|
||||
public constructor(private _interface: types.InterfaceDefinition) {
|
||||
super({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: consts.DEFAULT_EXPORT_NAME,
|
||||
})
|
||||
|
||||
const actionsModule = new ActionsModule(_interface.actions ?? {})
|
||||
actionsModule.unshift('actions')
|
||||
|
||||
const channelsModule = new ChannelsModule(_interface.channels ?? {})
|
||||
channelsModule.unshift('channels')
|
||||
|
||||
const eventsModule = new EventsModule(_interface.events ?? {})
|
||||
eventsModule.unshift('events')
|
||||
|
||||
const entitiesModule = new EntitiesModule(_interface.entities ?? {})
|
||||
entitiesModule.unshift('entities')
|
||||
|
||||
this._dependencies = {
|
||||
actionsModule,
|
||||
channelsModule,
|
||||
eventsModule,
|
||||
entitiesModule,
|
||||
}
|
||||
|
||||
for (const dep of Object.values(this._dependencies)) {
|
||||
this.pushDep(dep)
|
||||
}
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
let content = ''
|
||||
|
||||
const { actionsModule, channelsModule, eventsModule, entitiesModule } = this._dependencies
|
||||
|
||||
const actionsImport = actionsModule.import(this)
|
||||
const channelsImport = channelsModule.import(this)
|
||||
const eventsImport = eventsModule.import(this)
|
||||
const entitiesImport = entitiesModule.import(this)
|
||||
|
||||
const interfaceAttributes = this._interface.attributes ? stringifySingleLine(this._interface.attributes) : '{}'
|
||||
|
||||
content += [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
'',
|
||||
`import * as ${actionsModule.name} from "./${actionsImport}"`,
|
||||
`import * as ${channelsModule.name} from "./${channelsImport}"`,
|
||||
`import * as ${eventsModule.name} from "./${eventsImport}"`,
|
||||
`import * as ${entitiesModule.name} from "./${entitiesImport}"`,
|
||||
`export * as ${actionsModule.name} from "./${actionsImport}"`,
|
||||
`export * as ${channelsModule.name} from "./${channelsImport}"`,
|
||||
`export * as ${eventsModule.name} from "./${eventsImport}"`,
|
||||
`export * as ${entitiesModule.name} from "./${entitiesImport}"`,
|
||||
'',
|
||||
'export default {',
|
||||
` name: "${this._interface.name}",`,
|
||||
` version: "${this._interface.version}",`,
|
||||
` attributes: ${interfaceAttributes},`,
|
||||
` actions: ${actionsModule.name}.${actionsModule.exportName},`,
|
||||
` channels: ${channelsModule.name}.${channelsModule.exportName},`,
|
||||
` events: ${eventsModule.name}.${eventsModule.exportName},`,
|
||||
` entities: ${entitiesModule.name}.${entitiesModule.exportName},`,
|
||||
'} satisfies sdk.InterfacePackage["definition"]',
|
||||
].join('\n')
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import * as types from '../../typings'
|
||||
export type InterfaceDefinition = types.InterfaceDefinition
|
||||
export type ActionDefinition = NonNullable<InterfaceDefinition['actions']>[string]
|
||||
export type ChannelDefinition = NonNullable<InterfaceDefinition['channels']>[string]
|
||||
export type MessageDefinition = NonNullable<ChannelDefinition['messages']>[string]
|
||||
export type EntityDefinition = NonNullable<InterfaceDefinition['entities']>[string]
|
||||
export type EventDefinition = NonNullable<InterfaceDefinition['events']>[string]
|
||||
@@ -0,0 +1,185 @@
|
||||
import { posix as pathlib } from 'path'
|
||||
import * as utils from '../utils'
|
||||
import * as consts from './consts'
|
||||
import * as strings from './strings'
|
||||
import { File } from './typings'
|
||||
|
||||
export type ModuleProps = {
|
||||
path: string
|
||||
exportName: string
|
||||
}
|
||||
|
||||
export abstract class Module {
|
||||
private _localDependencies: Module[] = []
|
||||
private _customTypeName: string | undefined
|
||||
|
||||
public get path(): string {
|
||||
return this._def.path.split(pathlib.sep).map(strings.fileName).join(pathlib.sep)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns module name (equivalent to unescaped file name without extension)
|
||||
*/
|
||||
public get name(): string {
|
||||
const path = this._def.path
|
||||
const basename = pathlib.basename(path)
|
||||
if (basename === consts.INDEX_FILE || basename === consts.INDEX_DECLARATION_FILE) {
|
||||
const dirPath = pathlib.dirname(path)
|
||||
const dirname = pathlib.basename(dirPath)
|
||||
return dirname
|
||||
}
|
||||
const withoutExtension = utils.path.rmExtension(basename)
|
||||
return withoutExtension
|
||||
}
|
||||
|
||||
public get isDefaultExport(): boolean {
|
||||
return this._def.exportName === consts.DEFAULT_EXPORT_NAME
|
||||
}
|
||||
|
||||
public get exportName(): string {
|
||||
return this._def.exportName
|
||||
}
|
||||
|
||||
public get deps(): Module[] {
|
||||
return [...this._localDependencies]
|
||||
}
|
||||
|
||||
public get typeName(): string {
|
||||
return this._customTypeName ?? this.name
|
||||
}
|
||||
|
||||
public get importAlias(): string {
|
||||
return this.typeName.split(/\\|\//).map(strings.importAlias).join('__')
|
||||
}
|
||||
|
||||
protected constructor(private _def: ModuleProps) {}
|
||||
|
||||
public abstract getContent(): Promise<string>
|
||||
|
||||
public setCustomTypeName(alias: string): this {
|
||||
this._customTypeName = alias
|
||||
return this
|
||||
}
|
||||
|
||||
public pushDep(...dependencies: Module[]): this {
|
||||
this._localDependencies.push(...dependencies)
|
||||
return this
|
||||
}
|
||||
|
||||
public unshift(...basePath: string[]): this {
|
||||
this._def = {
|
||||
...this._def,
|
||||
path: pathlib.join(...basePath, this._def.path),
|
||||
}
|
||||
this._localDependencies = this._localDependencies.map((d) => d.unshift(...basePath))
|
||||
return this
|
||||
}
|
||||
|
||||
public async toFile(): Promise<File> {
|
||||
return {
|
||||
path: this.path,
|
||||
content: await this.getContent(),
|
||||
}
|
||||
}
|
||||
|
||||
public async flatten(): Promise<File[]> {
|
||||
const self = await this.toFile()
|
||||
const allFiles: File[] = [self]
|
||||
for (const dep of this._localDependencies) {
|
||||
const depFiles = await dep.flatten()
|
||||
allFiles.push(...depFiles)
|
||||
}
|
||||
return allFiles
|
||||
}
|
||||
|
||||
public import(base: Module): string {
|
||||
let relativePath = pathlib.relative(pathlib.dirname(base.path), this.path)
|
||||
relativePath = pathlib.join('.', relativePath)
|
||||
return utils.path.rmExtension(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
export class ReExportTypeModule extends Module {
|
||||
protected constructor(def: { exportName: string }) {
|
||||
super({
|
||||
...def,
|
||||
path: consts.INDEX_FILE,
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
let content = consts.GENERATED_HEADER
|
||||
|
||||
for (const m of this.deps) {
|
||||
const { importAlias } = m
|
||||
const importFrom = m.import(this)
|
||||
content += `import * as ${importAlias} from "./${importFrom}";\n`
|
||||
content += `export * as ${importAlias} from "./${importFrom}";\n`
|
||||
}
|
||||
|
||||
content += '\n'
|
||||
|
||||
content += `export type ${this.exportName} = {\n`
|
||||
for (const { importAlias, typeName, exportName: exports } of this.deps) {
|
||||
content += ` "${typeName}": ${importAlias}.${exports};\n`
|
||||
}
|
||||
content += '}'
|
||||
|
||||
content += '\n'
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
export class ReExportVariableModule extends Module {
|
||||
private _extraProps: Record<string, string> = {}
|
||||
|
||||
protected constructor(def: { exportName: string; extraProps?: Record<string, string> }) {
|
||||
super({
|
||||
...def,
|
||||
path: consts.INDEX_FILE,
|
||||
})
|
||||
this._extraProps = def.extraProps ?? {}
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
let content = consts.GENERATED_HEADER
|
||||
|
||||
for (const m of this.deps) {
|
||||
const { importAlias } = m
|
||||
const importFrom = m.import(this)
|
||||
content += `import * as ${importAlias} from "./${importFrom}";\n`
|
||||
content += `export * as ${importAlias} from "./${importFrom}";\n`
|
||||
}
|
||||
|
||||
content += '\n'
|
||||
|
||||
const depProps: Record<string, string> = Object.fromEntries(
|
||||
this.deps.map(({ name, exportName, importAlias }) => [name, `${importAlias}.${exportName}`])
|
||||
)
|
||||
|
||||
const allProps = { ...depProps, ...this._extraProps }
|
||||
|
||||
content += `export const ${this.exportName} = {\n`
|
||||
for (const [key, value] of Object.entries(allProps)) {
|
||||
content += ` "${key}": ${value},\n`
|
||||
}
|
||||
content += '}'
|
||||
|
||||
content += '\n'
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
export class SingleFileModule extends Module {
|
||||
private _content: string
|
||||
public constructor(def: ModuleProps & { content: string }) {
|
||||
super(def)
|
||||
this._content = def.content
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
return this._content
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../consts'
|
||||
import * as types from '../typings'
|
||||
import { PluginImplementationModule } from './plugin-implementation'
|
||||
|
||||
const generatePluginImplementationCls = async (
|
||||
sdkPluginDefinition: sdk.PluginDefinition,
|
||||
implPath: string
|
||||
): Promise<types.File[]> => {
|
||||
const indexModule = new PluginImplementationModule(sdkPluginDefinition)
|
||||
indexModule.unshift(implPath)
|
||||
return indexModule.flatten()
|
||||
}
|
||||
|
||||
const generatePluginIndex = async (implPath: string): Promise<types.File> => {
|
||||
let content = ''
|
||||
content += `export * from './${implPath}'\n`
|
||||
return {
|
||||
path: consts.INDEX_FILE,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
export const generatePluginImplementation = async (
|
||||
sdkPluginDefinition: sdk.PluginDefinition
|
||||
): Promise<types.File[]> => {
|
||||
const implPath = consts.fromOutDir.implementationDir
|
||||
const typingFiles = await generatePluginImplementationCls(sdkPluginDefinition, implPath)
|
||||
const indexFile = await generatePluginIndex(implPath)
|
||||
return [...typingFiles, indexFile]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../consts'
|
||||
import { Module } from '../module'
|
||||
import { PluginTypingsModule } from './plugin-typings'
|
||||
|
||||
export class PluginImplementationModule extends Module {
|
||||
private _typingsModule: PluginTypingsModule
|
||||
|
||||
public constructor(plugin: sdk.PluginDefinition) {
|
||||
super({
|
||||
exportName: 'Plugin',
|
||||
path: consts.INDEX_FILE,
|
||||
})
|
||||
|
||||
this._typingsModule = new PluginTypingsModule(plugin)
|
||||
this._typingsModule.unshift('typings')
|
||||
this.pushDep(this._typingsModule)
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const typingsImport = this._typingsModule.import(this)
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
`import * as ${this._typingsModule.name} from "./${typingsImport}"`,
|
||||
`export * from "./${typingsImport}"`,
|
||||
'',
|
||||
`type TPlugin = sdk.DefaultPlugin<${this._typingsModule.name}.${this._typingsModule.exportName}>`,
|
||||
'',
|
||||
'export class Plugin extends sdk.Plugin<TPlugin> {}',
|
||||
'',
|
||||
'export type PluginProps = sdk.PluginProps<TPlugin>',
|
||||
'export type PluginRuntimeProps = sdk.PluginRuntimeProps<TPlugin>',
|
||||
'',
|
||||
'// extra types',
|
||||
'',
|
||||
'type ValueOf<T> = T[keyof T]',
|
||||
'type AsyncFunction = (...args: any[]) => Promise<any>',
|
||||
'',
|
||||
'export type PluginHandlers = sdk.InjectedPluginHandlers<TPlugin>',
|
||||
'',
|
||||
'export type EventHandlers = Required<{',
|
||||
" [K in keyof PluginHandlers['eventHandlers']]: NonNullable<PluginHandlers['eventHandlers'][K]>[number]",
|
||||
'}>',
|
||||
'export type MessageHandlers = Required<{',
|
||||
" [K in keyof PluginHandlers['messageHandlers']]: NonNullable<PluginHandlers['messageHandlers'][K]>[number]",
|
||||
'}>',
|
||||
'export type HookHandlers = Required<{',
|
||||
" [H in keyof PluginHandlers['hookHandlers']]: Required<{",
|
||||
" [K in keyof PluginHandlers['hookHandlers'][H]]: NonNullable<PluginHandlers['hookHandlers'][H][K]>[number]",
|
||||
' }>',
|
||||
'}>',
|
||||
'export type WorkflowHandlers = {',
|
||||
" [TWorkflowName in keyof Required<PluginHandlers['workflowHandlers'][keyof PluginHandlers['workflowHandlers']]>]:",
|
||||
" NonNullable<Required<PluginHandlers['workflowHandlers'][keyof PluginHandlers['workflowHandlers']]>[TWorkflowName]>[number]",
|
||||
'}',
|
||||
'',
|
||||
"export type AnyMessageHandler = MessageHandlers['*']",
|
||||
"export type AnyEventHandler = EventHandlers['*']",
|
||||
"export type AnyActionHandler = ValueOf<PluginHandlers['actionHandlers']>",
|
||||
'export type AnyHookHanders = {',
|
||||
" [H in keyof HookHandlers]: NonNullable<HookHandlers[H]['*']>",
|
||||
'}',
|
||||
'',
|
||||
'export type MessageHandlerProps = Parameters<AnyMessageHandler>[0]',
|
||||
'export type EventHandlerProps = Parameters<AnyEventHandler>[0]',
|
||||
'export type ActionHandlerProps = Parameters<AnyActionHandler>[0]',
|
||||
'export type HookHandlerProps = {',
|
||||
' [H in keyof AnyHookHanders]: Parameters<NonNullable<AnyHookHanders[H]>>[0]',
|
||||
'}',
|
||||
'export type WorkflowHandlerProps = {',
|
||||
' [TWorkflowName in keyof WorkflowHandlers]: WorkflowHandlers[TWorkflowName] extends',
|
||||
' (..._: infer U) => any ? U[0] : never',
|
||||
'}',
|
||||
'',
|
||||
"export type Client = (MessageHandlerProps | EventHandlerProps)['client']",
|
||||
'export type ClientOperation = keyof {',
|
||||
' [K in keyof Client as Client[K] extends AsyncFunction ? K : never]: null',
|
||||
'}',
|
||||
'export type ClientInputs = {',
|
||||
' [K in ClientOperation]: Parameters<Client[K]>[0]',
|
||||
'}',
|
||||
'export type ClientOutputs = {',
|
||||
' [K in ClientOperation]: Awaited<ReturnType<Client[K]>>',
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
type ActionInput = sdk.BotActionDefinition['input']
|
||||
type ActionOutput = sdk.BotActionDefinition['output']
|
||||
|
||||
export class ActionInputModule extends Module {
|
||||
public constructor(private _input: ActionInput) {
|
||||
const name = 'input'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._input.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionOutputModule extends Module {
|
||||
public constructor(private _output: ActionOutput) {
|
||||
const name = 'output'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._output.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionModule extends ReExportTypeModule {
|
||||
public constructor(actionName: string, action: sdk.BotActionDefinition) {
|
||||
super({ exportName: strings.typeName(actionName) })
|
||||
|
||||
const inputModule = new ActionInputModule(action.input)
|
||||
const outputModule = new ActionOutputModule(action.output)
|
||||
|
||||
this.pushDep(inputModule)
|
||||
this.pushDep(outputModule)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionsModule extends ReExportTypeModule {
|
||||
public constructor(actions: Record<string, sdk.BotActionDefinition>) {
|
||||
super({ exportName: strings.typeName('actions') })
|
||||
for (const [actionName, action] of Object.entries(actions)) {
|
||||
const module = new ActionModule(actionName, action)
|
||||
module.unshift(actionName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { INDEX_FILE } from '../../consts'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class DefaultConfigurationModule extends Module {
|
||||
public constructor(private _configuration: sdk.PluginDefinition['configuration']) {
|
||||
const name = 'configuration'
|
||||
const exportName = strings.typeName(name)
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName,
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
if (!this._configuration) {
|
||||
return ['/** Default Configuration of the Plugin */', 'export type Configuration = Record<string, never>;'].join(
|
||||
'\n'
|
||||
)
|
||||
}
|
||||
return zuiSchemaToTypeScriptType(this._configuration.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { INDEX_FILE } from '../../consts'
|
||||
import * as consts from '../../consts'
|
||||
import { stringifySingleLine } from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class ConversationModule extends Module {
|
||||
public constructor(private _conversation: sdk.PluginDefinition['conversation']) {
|
||||
const name = 'conversation'
|
||||
const exportName = strings.typeName(name)
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName,
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const conversation = { tags: this._conversation?.tags ?? {} }
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
`export type ${this.exportName} = {`,
|
||||
` tags: ${stringifySingleLine(conversation.tags)}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class EventModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _event: sdk.BotEventDefinition
|
||||
) {
|
||||
const eventName = name
|
||||
const exportName = strings.typeName(eventName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._event.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class EventsModule extends ReExportTypeModule {
|
||||
public constructor(events: Record<string, sdk.BotEventDefinition>) {
|
||||
super({ exportName: strings.typeName('events') })
|
||||
for (const [eventName, event] of Object.entries(events)) {
|
||||
const module = new EventModule(eventName, event)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as utils from '../../../utils'
|
||||
import * as consts from '../../consts'
|
||||
import { IntegrationTypingsModule } from '../../integration-implementation/integration-typings'
|
||||
import { InterfaceTypingsModule } from '../../interface-implementation'
|
||||
import { Module, ReExportTypeModule, SingleFileModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import { ActionsModule } from './actions-module'
|
||||
import { DefaultConfigurationModule } from './configuration-module'
|
||||
import { ConversationModule } from './conversation-module'
|
||||
import { EventsModule } from './events-module'
|
||||
import { MessageModule } from './message-module'
|
||||
import { StatesModule } from './states-module'
|
||||
import { TablesModule } from './tables-module'
|
||||
import { UserModule } from './user-module'
|
||||
import { WorkflowsModule } from './workflows-module'
|
||||
|
||||
class PluginIntegrationsModule extends ReExportTypeModule {
|
||||
public constructor(plugin: sdk.PluginDefinition) {
|
||||
super({
|
||||
exportName: 'Integrations',
|
||||
})
|
||||
|
||||
for (const [alias, integration] of Object.entries(plugin.integrations ?? {})) {
|
||||
const integrationModule = new IntegrationTypingsModule(integration.definition)
|
||||
integrationModule.unshift(strings.dirName(alias))
|
||||
this.pushDep(integrationModule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PluginInterfacesModule extends ReExportTypeModule {
|
||||
public constructor(plugin: sdk.PluginDefinition) {
|
||||
super({
|
||||
exportName: 'Interfaces',
|
||||
})
|
||||
|
||||
for (const [alias, intrface] of Object.entries(plugin.interfaces ?? {})) {
|
||||
const interfaceModule = new InterfaceTypingsModule(intrface.definition)
|
||||
interfaceModule.unshift(strings.dirName(alias))
|
||||
this.pushDep(interfaceModule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PluginTypingsIndexDependencies = {
|
||||
integrationsModule: PluginIntegrationsModule
|
||||
interfacesModule: PluginInterfacesModule
|
||||
defaultConfigModule: DefaultConfigurationModule
|
||||
eventsModule: EventsModule
|
||||
statesModule: StatesModule
|
||||
actionsModule: ActionsModule
|
||||
tablesModule: TablesModule
|
||||
workflowsModule: WorkflowsModule
|
||||
conversationModule: ConversationModule
|
||||
messageModule: MessageModule
|
||||
userModule: UserModule
|
||||
}
|
||||
|
||||
type _assertPropsInPluginDefinition = utils.types.AssertKeyOf<'props', sdk.PluginDefinition>
|
||||
const _isLocalPluginDefinition = (
|
||||
plugin: sdk.PluginDefinition | sdk.PluginPackage['definition']
|
||||
): plugin is sdk.PluginDefinition => {
|
||||
return 'props' in plugin
|
||||
}
|
||||
|
||||
export class PluginTypingsModule extends Module {
|
||||
private _dependencies: PluginTypingsIndexDependencies
|
||||
|
||||
public constructor(private _plugin: sdk.PluginDefinition | sdk.PluginPackage['definition']) {
|
||||
super({
|
||||
exportName: 'TPlugin',
|
||||
path: consts.INDEX_FILE,
|
||||
})
|
||||
|
||||
const integrationsModule = _isLocalPluginDefinition(_plugin)
|
||||
? new PluginIntegrationsModule(_plugin)
|
||||
: new SingleFileModule({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: 'Integrations',
|
||||
content: 'export type Integrations = {}',
|
||||
})
|
||||
integrationsModule.unshift('integrations')
|
||||
this.pushDep(integrationsModule)
|
||||
|
||||
const interfacesModule = _isLocalPluginDefinition(_plugin)
|
||||
? new PluginInterfacesModule(_plugin)
|
||||
: new SingleFileModule({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: 'Interfaces',
|
||||
content: 'export type Interfaces = {}',
|
||||
})
|
||||
interfacesModule.unshift('interfaces')
|
||||
this.pushDep(interfacesModule)
|
||||
|
||||
const defaultConfigModule = new DefaultConfigurationModule(_plugin.configuration)
|
||||
defaultConfigModule.unshift('configuration')
|
||||
this.pushDep(defaultConfigModule)
|
||||
|
||||
const eventsModule = new EventsModule(_plugin.events ?? {})
|
||||
eventsModule.unshift('events')
|
||||
this.pushDep(eventsModule)
|
||||
|
||||
const statesModule = new StatesModule(_plugin.states ?? {})
|
||||
statesModule.unshift('states')
|
||||
this.pushDep(statesModule)
|
||||
|
||||
const actionsModule = new ActionsModule(_plugin.actions ?? {})
|
||||
actionsModule.unshift('actions')
|
||||
this.pushDep(actionsModule)
|
||||
|
||||
const tablesModule = new TablesModule(_plugin.tables ?? {})
|
||||
tablesModule.unshift('tables')
|
||||
this.pushDep(tablesModule)
|
||||
|
||||
const workflowsModule = new WorkflowsModule(_plugin.workflows ?? {})
|
||||
workflowsModule.unshift('workflows')
|
||||
this.pushDep(workflowsModule)
|
||||
|
||||
const conversationModule = new ConversationModule(_plugin.conversation)
|
||||
conversationModule.unshift('conversation')
|
||||
this.pushDep(conversationModule)
|
||||
|
||||
const messageModule = new MessageModule(_plugin.message)
|
||||
messageModule.unshift('message')
|
||||
this.pushDep(messageModule)
|
||||
|
||||
const userModule = new UserModule(_plugin.user)
|
||||
userModule.unshift('user')
|
||||
this.pushDep(userModule)
|
||||
|
||||
this._dependencies = {
|
||||
integrationsModule,
|
||||
interfacesModule,
|
||||
defaultConfigModule,
|
||||
eventsModule,
|
||||
statesModule,
|
||||
actionsModule,
|
||||
tablesModule,
|
||||
workflowsModule,
|
||||
conversationModule,
|
||||
messageModule,
|
||||
userModule,
|
||||
}
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const {
|
||||
integrationsModule,
|
||||
interfacesModule,
|
||||
defaultConfigModule,
|
||||
eventsModule,
|
||||
statesModule,
|
||||
actionsModule,
|
||||
tablesModule,
|
||||
workflowsModule,
|
||||
conversationModule,
|
||||
messageModule,
|
||||
userModule,
|
||||
} = this._dependencies
|
||||
|
||||
const integrationsImport = integrationsModule.import(this)
|
||||
const interfacesImport = interfacesModule.import(this)
|
||||
const defaultConfigImport = defaultConfigModule.import(this)
|
||||
const eventsImport = eventsModule.import(this)
|
||||
const statesImport = statesModule.import(this)
|
||||
const actionsImport = actionsModule
|
||||
const tablesImport = tablesModule.import(this)
|
||||
const workflowsImport = workflowsModule
|
||||
const conversationImport = conversationModule.import(this)
|
||||
const messageImport = messageModule.import(this)
|
||||
const userImport = userModule.import(this)
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
`import * as ${integrationsModule.name} from './${integrationsImport}'`,
|
||||
`import * as ${interfacesModule.name} from './${interfacesImport}'`,
|
||||
`import * as ${defaultConfigModule.name} from './${defaultConfigImport}'`,
|
||||
`import * as ${eventsModule.name} from './${eventsModule.name}'`,
|
||||
`import * as ${statesModule.name} from './${statesModule.name}'`,
|
||||
`import * as ${actionsModule.name} from './${actionsImport.name}'`,
|
||||
`import * as ${tablesModule.name} from './${tablesImport}'`,
|
||||
`import * as ${workflowsModule.name} from './${workflowsImport.name}'`,
|
||||
`import * as ${conversationModule.name} from './${conversationImport}'`,
|
||||
`import * as ${messageModule.name} from './${messageImport}'`,
|
||||
`import * as ${userModule.name} from './${userImport}'`,
|
||||
'',
|
||||
`export * as ${integrationsModule.name} from './${integrationsImport}'`,
|
||||
`export * as ${interfacesModule.name} from './${interfacesImport}'`,
|
||||
`export * as ${defaultConfigModule.name} from './${defaultConfigImport}'`,
|
||||
`export * as ${eventsModule.name} from './${eventsImport}'`,
|
||||
`export * as ${statesModule.name} from './${statesImport}'`,
|
||||
`export * as ${actionsModule.name} from './${actionsImport.name}'`,
|
||||
`export * as ${tablesModule.name} from './${tablesImport}'`,
|
||||
`export * as ${workflowsModule.name} from './${workflowsImport.name}'`,
|
||||
`export * as ${conversationModule.name} from './${conversationImport}'`,
|
||||
`export * as ${messageModule.name} from './${messageImport}'`,
|
||||
`export * as ${userModule.name} from './${userImport}'`,
|
||||
'',
|
||||
'export type TPlugin = {',
|
||||
` name: "${this._plugin.name}"`,
|
||||
` version: "${this._plugin.version}"`,
|
||||
` integrations: ${integrationsModule.name}.${integrationsModule.exportName}`,
|
||||
` interfaces: ${interfacesModule.name}.${interfacesModule.exportName}`,
|
||||
` configuration: ${defaultConfigModule.name}.${defaultConfigModule.exportName}`,
|
||||
` events: ${eventsModule.name}.${eventsModule.exportName}`,
|
||||
` states: ${statesModule.name}.${statesModule.exportName}`,
|
||||
` actions: ${actionsModule.name}.${actionsModule.exportName}`,
|
||||
` tables: ${tablesModule.name}.${tablesModule.exportName}`,
|
||||
` workflows: ${workflowsModule.name}.${workflowsModule.exportName}`,
|
||||
` conversation: ${conversationModule.name}.${conversationModule.exportName}`,
|
||||
` message: ${messageModule.name}.${messageModule.exportName}`,
|
||||
` user: ${userModule.name}.${userModule.exportName}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { INDEX_FILE } from '../../consts'
|
||||
import * as consts from '../../consts'
|
||||
import { stringifySingleLine } from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class MessageModule extends Module {
|
||||
public constructor(private _message: sdk.PluginDefinition['message']) {
|
||||
const name = 'message'
|
||||
const exportName = strings.typeName(name)
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName,
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const message = { tags: this._message?.tags ?? {} }
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
`export type ${this.exportName} = {`,
|
||||
` tags: ${stringifySingleLine(message.tags)}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as consts from '../../consts'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class StatePayloadModule extends Module {
|
||||
public constructor(private _state: sdk.BotStateDefinition) {
|
||||
super({
|
||||
path: 'payload.ts',
|
||||
exportName: strings.typeName('Payload'),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return gen.zuiSchemaToTypeScriptType(this._state.schema, 'Payload')
|
||||
}
|
||||
}
|
||||
|
||||
export class StateModule extends Module {
|
||||
private _payloadModule: StatePayloadModule
|
||||
|
||||
public constructor(
|
||||
private _name: string,
|
||||
private _state: sdk.BotStateDefinition
|
||||
) {
|
||||
super({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: strings.typeName(_name),
|
||||
})
|
||||
|
||||
this._payloadModule = new StatePayloadModule(_state)
|
||||
this.pushDep(this._payloadModule)
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
const { _payloadModule } = this
|
||||
const payloadImport = _payloadModule.import(this)
|
||||
|
||||
const exportName = strings.typeName(this._name)
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
`import * as ${_payloadModule.name} from './${payloadImport}'`,
|
||||
`export type ${exportName} = {`,
|
||||
` type: ${gen.primitiveToTypescriptValue(this._state.type)},`,
|
||||
` payload: ${_payloadModule.name}.${_payloadModule.exportName}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
export class StatesModule extends ReExportTypeModule {
|
||||
public constructor(states: Record<string, sdk.BotStateDefinition>) {
|
||||
super({ exportName: strings.typeName('states') })
|
||||
for (const [stateName, state] of Object.entries(states)) {
|
||||
const module = new StateModule(stateName, state)
|
||||
module.unshift(stateName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class TableModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _table: sdk.BotTableDefinition
|
||||
) {
|
||||
super({
|
||||
path: `${name}.ts`,
|
||||
exportName: strings.typeName(name),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._table.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class TablesModule extends ReExportTypeModule {
|
||||
public constructor(tables: Record<string, sdk.BotTableDefinition>) {
|
||||
super({ exportName: strings.typeName('tables') })
|
||||
for (const [tableName, table] of Object.entries(tables)) {
|
||||
const module = new TableModule(tableName, table)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { INDEX_FILE } from '../../consts'
|
||||
import * as consts from '../../consts'
|
||||
import { stringifySingleLine } from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
export class UserModule extends Module {
|
||||
public constructor(private _user: sdk.PluginDefinition['user']) {
|
||||
const name = 'user'
|
||||
const exportName = strings.typeName(name)
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName,
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const user = { tags: this._user?.tags ?? {} }
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
`export type ${this.exportName} = {`,
|
||||
` tags: ${stringifySingleLine(user.tags)}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { primitiveToTypescriptValue, zuiSchemaToTypeScriptType } from '../../generators'
|
||||
import { Module, ReExportTypeModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
|
||||
type WorkflowInput = sdk.BotWorkflowDefinition['input']
|
||||
type WorkflowOutput = sdk.BotWorkflowDefinition['output']
|
||||
type WorkflowTags = sdk.BotWorkflowDefinition['tags']
|
||||
|
||||
export class WorkflowInputModule extends Module {
|
||||
public constructor(private _input: WorkflowInput) {
|
||||
const name = 'input'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._input.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkflowOutputModule extends Module {
|
||||
public constructor(private _output: WorkflowOutput) {
|
||||
const name = 'output'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return zuiSchemaToTypeScriptType(this._output.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkflowTagsModule extends Module {
|
||||
public constructor(private _tags: WorkflowTags) {
|
||||
const name = 'tags'
|
||||
const exportName = strings.typeName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const tags = Object.keys(this._tags ?? {})
|
||||
.map((tagName) => `${primitiveToTypescriptValue(tagName)}: string`)
|
||||
.join(', ')
|
||||
|
||||
return `export type ${this.exportName} = { ${tags} }`
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkflowModule extends ReExportTypeModule {
|
||||
public constructor(workflowName: string, workflow: sdk.BotWorkflowDefinition) {
|
||||
super({ exportName: strings.typeName(workflowName) })
|
||||
|
||||
const inputModule = new WorkflowInputModule(workflow.input)
|
||||
const outputModule = new WorkflowOutputModule(workflow.output)
|
||||
const tagsModule = new WorkflowTagsModule(workflow.tags)
|
||||
|
||||
this.pushDep(inputModule)
|
||||
this.pushDep(outputModule)
|
||||
this.pushDep(tagsModule)
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkflowsModule extends ReExportTypeModule {
|
||||
public constructor(workflows: Record<string, sdk.BotWorkflowDefinition>) {
|
||||
super({ exportName: strings.typeName('workflows') })
|
||||
for (const [workflowName, workflow] of Object.entries(workflows)) {
|
||||
const module = new WorkflowModule(workflowName, workflow)
|
||||
module.unshift(workflowName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import _ from 'lodash'
|
||||
import * as consts from '../consts'
|
||||
import * as gen from '../generators'
|
||||
import * as mod from '../module'
|
||||
import * as types from '../typings'
|
||||
import { PluginPackageDefinitionModule } from './plugin-package-definition'
|
||||
|
||||
class ImplementationModule extends mod.Module {
|
||||
public constructor(private _implementationCode: string) {
|
||||
super({
|
||||
path: 'implementation.ts',
|
||||
exportName: 'default',
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
const base64Str = Buffer.from(this._implementationCode).toString('base64')
|
||||
const chunks: string[] = _.chunk(base64Str, 80).map((chunk) => chunk.join(''))
|
||||
|
||||
return [
|
||||
//
|
||||
'export default Buffer.from([',
|
||||
...chunks.map((chunk) => ` "${chunk}",`),
|
||||
'].join(""), "base64")',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
class PluginModule extends mod.Module {
|
||||
private _implModule: ImplementationModule
|
||||
private _defModule: PluginPackageDefinitionModule
|
||||
|
||||
public constructor(private _pkg: types.PluginInstallablePackage) {
|
||||
super({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: consts.DEFAULT_EXPORT_NAME,
|
||||
})
|
||||
|
||||
this._implModule = new ImplementationModule(this._pkg.code)
|
||||
this.pushDep(this._implModule)
|
||||
|
||||
this._defModule = new PluginPackageDefinitionModule(this._pkg.plugin)
|
||||
this._defModule.unshift('definition')
|
||||
this.pushDep(this._defModule)
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
const implImport = this._implModule.import(this)
|
||||
const defImport = this._defModule.import(this)
|
||||
|
||||
const id = this._pkg.plugin.id
|
||||
const uri = this._pkg.path
|
||||
|
||||
const tsId = gen.primitiveToTypescriptValue(id)
|
||||
const tsUri = gen.primitiveToTypescriptValue(uri)
|
||||
const tsName = gen.primitiveToTypescriptValue(this._pkg.name)
|
||||
const tsVersion = gen.primitiveToTypescriptValue(this._pkg.version)
|
||||
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
'',
|
||||
`import definition from "./${defImport}"`,
|
||||
`import implementation from "./${implImport}"`,
|
||||
'',
|
||||
'export default {',
|
||||
' type: "plugin",',
|
||||
` id: ${tsId},`,
|
||||
` uri: ${tsUri},`,
|
||||
` name: ${tsName},`,
|
||||
` version: ${tsVersion},`,
|
||||
' definition,',
|
||||
' implementation,',
|
||||
'} satisfies sdk.PluginPackage',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
export const generatePluginPackage = async (pkg: types.PluginInstallablePackage): Promise<types.File[]> => {
|
||||
const module = new PluginModule(pkg)
|
||||
return module.flatten()
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
type ActionInput = types.ActionDefinition['input']
|
||||
type ActionOutput = types.ActionDefinition['output']
|
||||
|
||||
export class ActionInputModule extends Module {
|
||||
public constructor(private _input: ActionInput) {
|
||||
const name = 'input'
|
||||
const exportName = strings.varName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(this._input.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionOutputModule extends Module {
|
||||
public constructor(private _output: ActionOutput) {
|
||||
const name = 'output'
|
||||
const exportName = strings.varName(name)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(this._output.schema, this.exportName)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionModule extends ReExportVariableModule {
|
||||
public constructor(actionName: string, action: types.ActionDefinition) {
|
||||
super({
|
||||
exportName: strings.varName(actionName),
|
||||
extraProps: {
|
||||
...gen.primitiveRecordToTypescriptValues({
|
||||
title: action.title,
|
||||
description: action.description,
|
||||
}),
|
||||
...(action.attributes ? { attributes: gen.stringifySingleLine(action.attributes) } : undefined),
|
||||
},
|
||||
})
|
||||
|
||||
const inputModule = new ActionInputModule(action.input)
|
||||
const outputModule = new ActionOutputModule(action.output)
|
||||
|
||||
this.pushDep(inputModule)
|
||||
this.pushDep(outputModule)
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionsModule extends ReExportVariableModule {
|
||||
public constructor(actions: Record<string, types.ActionDefinition>) {
|
||||
super({ exportName: strings.varName('actions') })
|
||||
for (const [actionName, action] of Object.entries(actions)) {
|
||||
const module = new ActionModule(actionName, action)
|
||||
module.unshift(actionName)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { JSONSchema7 } from 'json-schema'
|
||||
import { INDEX_FILE } from '../../consts'
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class DefaultConfigurationModule extends Module {
|
||||
public constructor(private _configuration: types.ConfigurationDefinition) {
|
||||
const name = 'configuration'
|
||||
const exportName = strings.varName(name)
|
||||
super({
|
||||
path: INDEX_FILE,
|
||||
exportName,
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
const schema: JSONSchema7 = this._configuration.schema ?? { type: 'object', properties: {} }
|
||||
return jsonSchemaToTypescriptZuiSchema(
|
||||
schema,
|
||||
this.exportName,
|
||||
gen.primitiveRecordToTypescriptValues({
|
||||
title: this._configuration.title,
|
||||
description: this._configuration.description,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class EventModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _event: types.EventDefinition
|
||||
) {
|
||||
const eventName = name
|
||||
const exportName = strings.varName(eventName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(this._event.schema, this.exportName, {
|
||||
...gen.primitiveRecordToTypescriptValues({
|
||||
title: this._event.title,
|
||||
description: this._event.description,
|
||||
}),
|
||||
...(this._event.attributes ? { attributes: gen.stringifySingleLine(this._event.attributes) } : undefined),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class EventsModule extends ReExportVariableModule {
|
||||
public constructor(events: Record<string, types.EventDefinition>) {
|
||||
super({ exportName: strings.varName('events') })
|
||||
for (const [eventName, event] of Object.entries(events)) {
|
||||
const module = new EventModule(eventName, event)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import * as consts from '../../consts'
|
||||
import { stringifySingleLine } from '../../generators'
|
||||
import { Module } from '../../module'
|
||||
import { ActionsModule } from './actions-module'
|
||||
import { DefaultConfigurationModule } from './configuration-module'
|
||||
import { EventsModule } from './events-module'
|
||||
import { InterfacesModule } from './interfaces-module'
|
||||
import { RecurringEventsModule } from './recurring-events-module'
|
||||
import { StatesModule } from './states-module'
|
||||
import * as types from './typings'
|
||||
|
||||
type PluginPackageModuleDependencies = {
|
||||
defaultConfigModule: DefaultConfigurationModule
|
||||
actionsModule: ActionsModule
|
||||
eventsModule: EventsModule
|
||||
statesModule: StatesModule
|
||||
interfacesModule: InterfacesModule
|
||||
recurringEventsModule: RecurringEventsModule
|
||||
}
|
||||
|
||||
export class PluginPackageDefinitionModule extends Module {
|
||||
private _dependencies: PluginPackageModuleDependencies
|
||||
|
||||
public constructor(private _plugin: types.PluginDefinition) {
|
||||
super({
|
||||
path: consts.INDEX_FILE,
|
||||
exportName: consts.DEFAULT_EXPORT_NAME,
|
||||
})
|
||||
|
||||
const defaultConfigModule = new DefaultConfigurationModule(_plugin.configuration ?? {})
|
||||
defaultConfigModule.unshift('configuration')
|
||||
|
||||
const actionsModule = new ActionsModule(_plugin.actions ?? {})
|
||||
actionsModule.unshift('actions')
|
||||
|
||||
const eventsModule = new EventsModule(_plugin.events ?? {})
|
||||
eventsModule.unshift('events')
|
||||
|
||||
const statesModule = new StatesModule(_plugin.states ?? {})
|
||||
statesModule.unshift('states')
|
||||
|
||||
const interfacesModule = new InterfacesModule(_plugin.dependencies?.interfaces ?? {})
|
||||
interfacesModule.unshift('interfaces')
|
||||
|
||||
const recurringEventsModule = new RecurringEventsModule(_plugin.recurringEvents ?? {})
|
||||
recurringEventsModule.unshift('recurringEvents')
|
||||
|
||||
this._dependencies = {
|
||||
defaultConfigModule,
|
||||
actionsModule,
|
||||
eventsModule,
|
||||
statesModule,
|
||||
interfacesModule,
|
||||
recurringEventsModule,
|
||||
}
|
||||
|
||||
for (const dep of Object.values(this._dependencies)) {
|
||||
this.pushDep(dep)
|
||||
}
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
let content = ''
|
||||
|
||||
const { defaultConfigModule, actionsModule, eventsModule, statesModule, interfacesModule, recurringEventsModule } =
|
||||
this._dependencies
|
||||
|
||||
const defaultConfigImport = defaultConfigModule.import(this)
|
||||
const actionsImport = actionsModule.import(this)
|
||||
const eventsImport = eventsModule.import(this)
|
||||
const statesImport = statesModule.import(this)
|
||||
const interfacesImport = interfacesModule.import(this)
|
||||
const recurringEventsImport = recurringEventsModule.import(this)
|
||||
|
||||
const user = {
|
||||
tags: this._plugin.user?.tags ?? {},
|
||||
}
|
||||
|
||||
const conversation = {
|
||||
tags: this._plugin.conversation?.tags ?? {},
|
||||
}
|
||||
|
||||
const pluginAttributes = this._plugin.attributes ? stringifySingleLine(this._plugin.attributes) : '{}'
|
||||
|
||||
content += [
|
||||
consts.GENERATED_HEADER,
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
'',
|
||||
`import * as ${defaultConfigModule.name} from "./${defaultConfigImport}"`,
|
||||
`import * as ${actionsModule.name} from "./${actionsImport}"`,
|
||||
`import * as ${eventsModule.name} from "./${eventsImport}"`,
|
||||
`import * as ${statesModule.name} from "./${statesImport}"`,
|
||||
`import * as ${interfacesModule.name} from "./${interfacesImport}"`,
|
||||
`import * as ${recurringEventsModule.name} from "./${recurringEventsImport}"`,
|
||||
`export * as ${defaultConfigModule.name} from "./${defaultConfigImport}"`,
|
||||
`export * as ${actionsModule.name} from "./${actionsImport}"`,
|
||||
`export * as ${eventsModule.name} from "./${eventsImport}"`,
|
||||
`export * as ${statesModule.name} from "./${statesImport}"`,
|
||||
`export * as ${interfacesModule.name} from "./${interfacesImport}"`,
|
||||
`export * as ${recurringEventsModule.name} from "./${recurringEventsImport}"`,
|
||||
'',
|
||||
'export default {',
|
||||
` name: "${this._plugin.name}",`,
|
||||
` version: "${this._plugin.version}",`,
|
||||
` attributes: ${pluginAttributes},`,
|
||||
` user: ${stringifySingleLine(user)},`,
|
||||
` conversation: ${stringifySingleLine(conversation)},`,
|
||||
` configuration: ${defaultConfigModule.name}.${defaultConfigModule.exportName},`,
|
||||
` actions: ${actionsModule.name}.${actionsModule.exportName},`,
|
||||
` events: ${eventsModule.name}.${eventsModule.exportName},`,
|
||||
` states: ${statesModule.name}.${statesModule.exportName},`,
|
||||
` interfaces: ${interfacesModule.name}.${interfacesModule.exportName},`,
|
||||
` recurringEvents: ${recurringEventsModule.name}.${recurringEventsModule.exportName},`,
|
||||
'} satisfies sdk.PluginPackage["definition"]',
|
||||
].join('\n')
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import * as consts from '../../consts'
|
||||
import * as mod from '../../module'
|
||||
import * as types from './typings'
|
||||
|
||||
export class InterfacesModule extends mod.Module {
|
||||
public constructor(private _interfaces: Record<string, types.InterfaceDefinition>) {
|
||||
super({ exportName: 'interfaces', path: 'index.ts' })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return [
|
||||
consts.GENERATED_HEADER,
|
||||
'export const interfaces = {',
|
||||
...Object.entries(this._interfaces).map(([k, { id, name, version }]) => {
|
||||
const keyStr = JSON.stringify(k)
|
||||
const valueStr = JSON.stringify({ id, name, version })
|
||||
return ` [${keyStr}]: ${valueStr},`
|
||||
}),
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import * as prettier from 'prettier'
|
||||
import * as consts from '../../consts'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as commonTypes from '../../typings'
|
||||
|
||||
class RecurringEventModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _event: commonTypes.RecurringEventDefinition
|
||||
) {
|
||||
const eventName = name
|
||||
const exportName = strings.varName(eventName)
|
||||
super({ path: `${name}.ts`, exportName })
|
||||
}
|
||||
|
||||
public async getContent(): Promise<string> {
|
||||
const code = [
|
||||
consts.GENERATED_HEADER,
|
||||
`export const ${this.exportName} = {`,
|
||||
` type: ${gen.primitiveToTypescriptValue(this._event.type)},`,
|
||||
` schedule: ${gen.stringifySingleLine(this._event.schedule)},`,
|
||||
` payload: ${gen.primitiveRecordToRecordString(this._event.payload)}`,
|
||||
'}',
|
||||
].join('\n')
|
||||
return await prettier.format(code, { parser: 'typescript' })
|
||||
}
|
||||
}
|
||||
|
||||
export class RecurringEventsModule extends ReExportVariableModule {
|
||||
public constructor(recurringEvents: Exclude<commonTypes.PluginDefinition['recurringEvents'], undefined>) {
|
||||
super({ exportName: strings.varName('recurringEvents') })
|
||||
for (const [eventName, event] of Object.entries(recurringEvents)) {
|
||||
const module = new RecurringEventModule(eventName, event)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { jsonSchemaToTypescriptZuiSchema } from '../../generators'
|
||||
import * as gen from '../../generators'
|
||||
import { Module, ReExportVariableModule } from '../../module'
|
||||
import * as strings from '../../strings'
|
||||
import * as types from './typings'
|
||||
|
||||
export class StateModule extends Module {
|
||||
public constructor(
|
||||
name: string,
|
||||
private _state: types.StateDefinition
|
||||
) {
|
||||
super({
|
||||
path: `${name}.ts`,
|
||||
exportName: strings.varName(name),
|
||||
})
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
return jsonSchemaToTypescriptZuiSchema(this._state.schema, this.exportName, {
|
||||
type: `"${this._state.type}" as const`,
|
||||
...gen.primitiveRecordToTypescriptValues({
|
||||
title: this._state.title,
|
||||
description: this._state.description,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class StatesModule extends ReExportVariableModule {
|
||||
public constructor(states: Record<string, types.StateDefinition>) {
|
||||
super({ exportName: strings.varName('states') })
|
||||
for (const [stateName, state] of Object.entries(states)) {
|
||||
const module = new StateModule(stateName, state)
|
||||
this.pushDep(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as types from '../../typings'
|
||||
export type PluginDefinition = types.PluginDefinition
|
||||
export type ActionDefinition = NonNullable<PluginDefinition['actions']>[string]
|
||||
export type ConfigurationDefinition = NonNullable<PluginDefinition['configuration']>
|
||||
export type EventDefinition = NonNullable<PluginDefinition['events']>[string]
|
||||
export type StateDefinition = NonNullable<PluginDefinition['states']>[string]
|
||||
export type InterfaceDefinition = types.InterfaceDefinition
|
||||
@@ -0,0 +1,39 @@
|
||||
import { casing } from '../utils'
|
||||
import * as consts from './consts'
|
||||
import { Module } from './module'
|
||||
|
||||
export const secretEnvVariableName = (secretName: string) => `SECRET_${casing.to.screamingSnakeCase(secretName)}`
|
||||
export const stripSecretEnvVariablePrefix = (secretName: string) => secretName.replace(/^SECRET_/, '')
|
||||
|
||||
type SecretDefinitions = Record<string, { optional?: boolean }>
|
||||
|
||||
export class SecretIndexModule extends Module {
|
||||
public constructor(private _secrets: SecretDefinitions) {
|
||||
super({ exportName: 'secrets', path: consts.INDEX_FILE })
|
||||
}
|
||||
|
||||
public async getContent() {
|
||||
let content = consts.GENERATED_HEADER
|
||||
content += 'class Secrets {\n'
|
||||
for (const [secretName, { optional }] of Object.entries(this._secrets)) {
|
||||
const envVariableName = secretEnvVariableName(secretName)
|
||||
const fieldName = casing.to.screamingSnakeCase(secretName)
|
||||
|
||||
if (optional) {
|
||||
content += ` public get ${fieldName}(): string | undefined {\n`
|
||||
content += ` const envVarValue = process.env.${envVariableName}\n`
|
||||
content += ' return envVarValue\n'
|
||||
content += ' }\n'
|
||||
} else {
|
||||
content += ` public get ${fieldName}(): string {\n`
|
||||
content += ` const envVarValue = process.env.${envVariableName}\n`
|
||||
content += ` if (!envVarValue) throw new Error('Missing required secret "${secretName}"')\n`
|
||||
content += ' return envVarValue\n'
|
||||
content += ' }\n'
|
||||
}
|
||||
}
|
||||
content += '}\n'
|
||||
content += 'export const secrets = new Secrets()\n'
|
||||
return content
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import * as utils from '../utils'
|
||||
type StrTransform = (str: string) => string
|
||||
|
||||
const TYPESCRIPT_RESERVED_TOKENS = new Set([
|
||||
'break',
|
||||
'as',
|
||||
'any',
|
||||
'case',
|
||||
'implements',
|
||||
'boolean',
|
||||
'catch',
|
||||
'interface',
|
||||
'constructor',
|
||||
'class',
|
||||
'let',
|
||||
'declare',
|
||||
'const',
|
||||
'package',
|
||||
'get',
|
||||
'continue',
|
||||
'private',
|
||||
'module',
|
||||
'debugger',
|
||||
'protected',
|
||||
'require',
|
||||
'default',
|
||||
'public',
|
||||
'number',
|
||||
'delete',
|
||||
'static',
|
||||
'set',
|
||||
'do',
|
||||
'yield',
|
||||
'string',
|
||||
'else',
|
||||
'symbol',
|
||||
'enum',
|
||||
'type',
|
||||
'export',
|
||||
'from',
|
||||
'extends',
|
||||
'of',
|
||||
'false',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'import',
|
||||
'in',
|
||||
'instanceof',
|
||||
'new',
|
||||
'null',
|
||||
'return',
|
||||
'super',
|
||||
'switch',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typeof',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
])
|
||||
|
||||
const SPECIAL_FILE_NAME_CHARS = /[^a-zA-Z0-9_\-\.]/g
|
||||
|
||||
const escapeTypescriptReserved = (str: string): string => {
|
||||
if (TYPESCRIPT_RESERVED_TOKENS.has(str)) {
|
||||
return `_${str}`
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
const escapeFileNameSpecialChars = (str: string): string => str.replace(SPECIAL_FILE_NAME_CHARS, '-')
|
||||
|
||||
const apply = (str: string, ...transforms: StrTransform[]) => transforms.reduce((acc, transform) => transform(acc), str)
|
||||
|
||||
export const typeName = (name: string) => apply(name, utils.casing.to.pascalCase)
|
||||
export const importAlias = (name: string) => apply(name, utils.casing.to.camelCase, escapeTypescriptReserved)
|
||||
export const varName = (name: string) => apply(name, utils.casing.to.camelCase, escapeTypescriptReserved)
|
||||
export const fileName = (name: string) => apply(name, escapeFileNameSpecialChars)
|
||||
export const dirName = fileName
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as apiUtils from '../api'
|
||||
import * as client from '@botpress/client'
|
||||
import * as utils from '../utils'
|
||||
import { IntegrationDefinition, InterfaceDefinition, PluginDefinition } from './typings'
|
||||
import { test } from 'vitest'
|
||||
|
||||
test('integration response extends integration definition', () => {
|
||||
type Actual = client.Integration
|
||||
type Expected = IntegrationDefinition
|
||||
type _assertion = utils.types.AssertExtends<Actual, Expected>
|
||||
})
|
||||
|
||||
test('integration request extends integration definition', () => {
|
||||
type Actual = client.ClientInputs['createIntegration']
|
||||
type Expected = IntegrationDefinition
|
||||
type _assertion = utils.types.AssertExtends<Actual, Expected>
|
||||
})
|
||||
|
||||
test('interface response extends interface definition', () => {
|
||||
type Actual = client.Interface
|
||||
type Expected = InterfaceDefinition
|
||||
type _assertion = utils.types.AssertExtends<Actual, Expected>
|
||||
})
|
||||
|
||||
test('interface request extends interface definition', () => {
|
||||
type Actual = client.ClientInputs['createInterface']
|
||||
type Expected = InterfaceDefinition
|
||||
type _assertion = utils.types.AssertExtends<Actual, Expected>
|
||||
})
|
||||
|
||||
test('plugin response extends plugin definition', () => {
|
||||
type Actual = client.Plugin
|
||||
type Expected = PluginDefinition
|
||||
type _assertion = utils.types.AssertExtends<Actual, Expected>
|
||||
})
|
||||
|
||||
test('plugin request extends plugin definition', () => {
|
||||
type Actual = apiUtils.CreatePluginRequestBody & { code: string }
|
||||
type Expected = PluginDefinition
|
||||
type _assertion = utils.types.AssertExtends<Actual, Expected>
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
import * as client from '@botpress/client'
|
||||
import * as utils from '../utils'
|
||||
|
||||
type NameVersion = { name: string; version: string }
|
||||
type PackageRef = { id?: string; name: string; version: string }
|
||||
type Schema = Record<string, any>
|
||||
type Aliases = Record<string, { name: string }>
|
||||
|
||||
type TitleDescription = { title?: string; description?: string }
|
||||
type Tags = { tags: Record<string, {}> }
|
||||
type InputOutput = { input: { schema: Schema }; output: { schema: Schema } }
|
||||
type Attributes = { attributes?: Record<string, string> }
|
||||
|
||||
export type File = { path: string; content: string }
|
||||
|
||||
export type IntegrationDefinition = PackageRef &
|
||||
Attributes & {
|
||||
interfaces?: Record<
|
||||
string,
|
||||
{
|
||||
id?: string
|
||||
entities?: Aliases
|
||||
actions?: Aliases
|
||||
events?: Aliases
|
||||
channels?: Aliases
|
||||
}
|
||||
>
|
||||
configuration?: TitleDescription & {
|
||||
schema?: Schema
|
||||
}
|
||||
configurations?: Record<
|
||||
string,
|
||||
TitleDescription & {
|
||||
schema?: Schema
|
||||
}
|
||||
>
|
||||
channels?: Record<
|
||||
string,
|
||||
TitleDescription & {
|
||||
messages: Record<string, TitleDescription & { schema: Schema }>
|
||||
conversation?: {
|
||||
tags?: Record<string, {}>
|
||||
creation?: {
|
||||
enabled: boolean
|
||||
requiredTags: string[]
|
||||
}
|
||||
}
|
||||
message?: {
|
||||
tags?: Record<string, {}>
|
||||
}
|
||||
}
|
||||
>
|
||||
states?: Record<
|
||||
string,
|
||||
TitleDescription & {
|
||||
type: client.State['type']
|
||||
schema: Schema
|
||||
}
|
||||
>
|
||||
events?: Record<string, TitleDescription & Attributes & { schema: Schema }>
|
||||
actions?: Record<
|
||||
string,
|
||||
TitleDescription &
|
||||
Attributes & {
|
||||
billable?: boolean
|
||||
cacheable?: boolean
|
||||
input: {
|
||||
schema: Schema
|
||||
}
|
||||
output: {
|
||||
schema: Schema
|
||||
}
|
||||
}
|
||||
>
|
||||
entities?: Record<string, TitleDescription & { schema: Schema }>
|
||||
user?: {
|
||||
tags?: Record<string, {}>
|
||||
creation?: {
|
||||
enabled: boolean
|
||||
requiredTags: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type InterfaceDefinition = PackageRef &
|
||||
Attributes & {
|
||||
entities?: Record<string, TitleDescription & { schema: Schema }>
|
||||
events?: Record<string, TitleDescription & Attributes & { schema: Schema }>
|
||||
actions?: Record<
|
||||
string,
|
||||
TitleDescription &
|
||||
Attributes & {
|
||||
billable?: boolean
|
||||
cacheable?: boolean
|
||||
input: { schema: Schema }
|
||||
output: { schema: Schema }
|
||||
}
|
||||
>
|
||||
channels?: Record<string, TitleDescription & { messages: Record<string, TitleDescription & { schema: Schema }> }>
|
||||
}
|
||||
|
||||
export type RecurringEventDefinition = {
|
||||
type: string
|
||||
payload: Record<string, any>
|
||||
schedule: { cron: string }
|
||||
}
|
||||
|
||||
export type PluginDefinition = PackageRef &
|
||||
Attributes & {
|
||||
configuration?: TitleDescription & { schema?: Schema }
|
||||
user?: { tags: Record<string, {}> }
|
||||
conversation?: Tags
|
||||
states?: Record<string, TitleDescription & { type: client.State['type']; schema: Schema }>
|
||||
events?: Record<string, TitleDescription & Attributes & { schema: Schema }>
|
||||
actions?: Record<string, TitleDescription & Attributes & InputOutput>
|
||||
workflows?: Record<string, TitleDescription & Tags & InputOutput>
|
||||
dependencies?: {
|
||||
interfaces?: Record<string, PackageRef>
|
||||
integrations?: Record<string, PackageRef>
|
||||
}
|
||||
recurringEvents?: Record<string, RecurringEventDefinition>
|
||||
}
|
||||
|
||||
export type IntegrationInstallablePackage = NameVersion & {
|
||||
integration: IntegrationDefinition
|
||||
devId?: string
|
||||
path?: utils.path.AbsolutePath
|
||||
}
|
||||
|
||||
export type InterfaceInstallablePackage = NameVersion & {
|
||||
interface: InterfaceDefinition
|
||||
path?: utils.path.AbsolutePath
|
||||
}
|
||||
|
||||
export type PluginInstallablePackage = NameVersion & {
|
||||
plugin: PluginDefinition
|
||||
path?: utils.path.AbsolutePath
|
||||
code: string
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { DefinitionTree } from './command-tree'
|
||||
import * as config from './config'
|
||||
|
||||
export default {
|
||||
login: { description: 'Login to Botpress Cloud', schema: config.schemas.login },
|
||||
logout: { description: 'Logout of Botpress Cloud', schema: config.schemas.logout },
|
||||
bots: {
|
||||
description: 'Bot related commands',
|
||||
subcommands: {
|
||||
create: { description: 'Create new bot', schema: config.schemas.createBot, alias: 'new' },
|
||||
get: { description: 'Get bot', schema: config.schemas.getBot },
|
||||
delete: { description: 'Delete bot', schema: config.schemas.deleteBot, alias: 'rm' },
|
||||
list: { description: 'List bots', schema: config.schemas.listBots, alias: 'ls' },
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
description: 'Integration related commands',
|
||||
subcommands: {
|
||||
get: { description: 'Get integration', schema: config.schemas.getIntegration },
|
||||
delete: { description: 'Delete integration', schema: config.schemas.deleteIntegration, alias: 'rm' },
|
||||
list: { description: 'List integrations', schema: config.schemas.listIntegrations, alias: 'ls' },
|
||||
},
|
||||
},
|
||||
interfaces: {
|
||||
description: 'Interface related commands',
|
||||
subcommands: {
|
||||
get: { description: 'Get interface', schema: config.schemas.getInterface },
|
||||
delete: { description: 'Delete interface', schema: config.schemas.deleteInterface, alias: 'rm' },
|
||||
list: { description: 'List interfaces', schema: config.schemas.listInterfaces, alias: 'ls' },
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
description: 'Plugin related commands',
|
||||
subcommands: {
|
||||
get: { description: 'Get plugin', schema: config.schemas.getPlugin },
|
||||
delete: { description: 'Delete plugin', schema: config.schemas.deletePlugin, alias: 'rm' },
|
||||
list: { description: 'List plugins', schema: config.schemas.listPlugins, alias: 'ls' },
|
||||
},
|
||||
},
|
||||
init: { description: 'Initialize a new project', schema: config.schemas.init },
|
||||
generate: { description: 'Generate typings for intellisense', schema: config.schemas.generate, alias: 'gen' },
|
||||
bundle: { description: 'Bundle a botpress project', schema: config.schemas.bundle },
|
||||
build: { description: 'Generate typings and bundle a botpress project', schema: config.schemas.build },
|
||||
read: { description: 'Read and parse an integration definition', schema: config.schemas.read },
|
||||
serve: { description: 'Serve your project locally', schema: config.schemas.serve },
|
||||
deploy: { description: 'Deploy your project to the cloud', schema: config.schemas.deploy },
|
||||
add: {
|
||||
description: 'Install a package; could be an integration or an interface',
|
||||
schema: config.schemas.add,
|
||||
alias: ['i', 'install'],
|
||||
},
|
||||
remove: {
|
||||
description: "Remove a package from your project's dependencies",
|
||||
schema: config.schemas.remove,
|
||||
alias: 'rm',
|
||||
},
|
||||
dev: { description: 'Run your project in dev mode', schema: config.schemas.dev },
|
||||
lint: { description: 'EXPERIMENTAL: Lint an integration definition', schema: config.schemas.lint },
|
||||
chat: { description: 'EXPERIMENTAL: Chat with a bot directly from the CLI', schema: config.schemas.chat },
|
||||
profiles: {
|
||||
description: 'Commands for using CLI profiles',
|
||||
subcommands: {
|
||||
list: { description: 'List all available profiles', schema: config.schemas.listProfiles, alias: 'ls' },
|
||||
active: {
|
||||
description: 'Get the profile properties you are currently using',
|
||||
schema: config.schemas.activeProfile,
|
||||
},
|
||||
use: {
|
||||
description: 'Set the current profile',
|
||||
schema: config.schemas.useProfile,
|
||||
},
|
||||
get: {
|
||||
description: 'Get a specific profile by name',
|
||||
schema: config.schemas.getProfile,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies DefinitionTree
|
||||
@@ -0,0 +1,453 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as fslib from 'fs'
|
||||
import * as pathlib from 'path'
|
||||
import * as apiUtils from '../api'
|
||||
import * as codegen from '../code-generation'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as consts from '../consts'
|
||||
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,
|
||||
ProjectDefinitionLazy,
|
||||
ProjectDefinition,
|
||||
} from './project-command'
|
||||
|
||||
type InstallablePackage =
|
||||
| {
|
||||
type: 'integration'
|
||||
pkg: codegen.IntegrationInstallablePackage
|
||||
}
|
||||
| {
|
||||
type: 'interface'
|
||||
pkg: codegen.InterfaceInstallablePackage
|
||||
}
|
||||
| {
|
||||
type: 'plugin'
|
||||
pkg: codegen.PluginInstallablePackage
|
||||
}
|
||||
|
||||
type RefWithAlias = pkgRef.PackageRef & { alias?: string }
|
||||
|
||||
export type AddCommandDefinition = typeof commandDefinitions.add
|
||||
export class AddCommand extends GlobalCommand<AddCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const ref = this._parseArgvRef()
|
||||
if (ref) {
|
||||
return await this._addNewSinglePackage({ ...ref, alias: this.argv.alias })
|
||||
}
|
||||
|
||||
const pkgJson = await utils.pkgJson.readPackageJson(this.argv.installPath).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Failed to read package.json file')
|
||||
})
|
||||
|
||||
if (!pkgJson) {
|
||||
this.logger.warn('No package.json found in the install path')
|
||||
return
|
||||
}
|
||||
|
||||
const { bpDependencies } = pkgJson
|
||||
if (!bpDependencies) {
|
||||
this.logger.log('No bp dependencies found in package.json')
|
||||
return
|
||||
}
|
||||
|
||||
const bpDependenciesSchema = sdk.z.record(sdk.z.string())
|
||||
const parseResults = bpDependenciesSchema.safeParse(bpDependencies)
|
||||
if (!parseResults.success) {
|
||||
throw new errors.BotpressCLIError('Invalid bpDependencies found in package.json')
|
||||
}
|
||||
|
||||
const baseInstallPath = utils.path.absoluteFrom(utils.path.cwd(), this.argv.installPath)
|
||||
const modulesPath = utils.path.join(baseInstallPath, consts.installDirName)
|
||||
fslib.rmSync(modulesPath, { force: true, recursive: true })
|
||||
fslib.mkdirSync(modulesPath)
|
||||
|
||||
for (const [pkgAlias, pkgRefStr] of Object.entries(parseResults.data)) {
|
||||
const parsed = pkgRef.parsePackageRef(pkgRefStr)
|
||||
if (!parsed) {
|
||||
throw new errors.InvalidPackageReferenceError(pkgRefStr)
|
||||
}
|
||||
|
||||
// Resolve path refs against installPath so reinstall works regardless of cwd
|
||||
const normalized =
|
||||
parsed.type === 'path' ? { ...parsed, path: utils.path.absoluteFrom(baseInstallPath, parsed.path) } : parsed
|
||||
const refWithAlias = { ...normalized, alias: pkgAlias }
|
||||
const foundPkg = await this._findPackage(refWithAlias)
|
||||
await this._addSinglePackage(refWithAlias, foundPkg)
|
||||
}
|
||||
}
|
||||
|
||||
private _parseArgvRef = (): pkgRef.PackageRef | undefined => {
|
||||
if (!this.argv.packageRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const parsed = pkgRef.parsePackageRef(this.argv.packageRef)
|
||||
if (!parsed) {
|
||||
throw new errors.InvalidPackageReferenceError(this.argv.packageRef)
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
private async _addSinglePackage(
|
||||
ref: RefWithAlias,
|
||||
props: { packageName: string; targetPackage: InstallablePackage }
|
||||
) {
|
||||
const { packageName, targetPackage } = props
|
||||
|
||||
const baseInstallPath = utils.path.absoluteFrom(utils.path.cwd(), this.argv.installPath)
|
||||
const packageDirName = utils.casing.to.kebabCase(packageName)
|
||||
const installPath = utils.path.join(baseInstallPath, consts.installDirName, packageDirName)
|
||||
|
||||
const alreadyInstalled = fslib.existsSync(installPath)
|
||||
if (alreadyInstalled) {
|
||||
await this._uninstall(installPath)
|
||||
}
|
||||
|
||||
if (ref.type === 'name') {
|
||||
targetPackage.pkg.version = ref.version
|
||||
}
|
||||
|
||||
let files: codegen.File[]
|
||||
if (targetPackage.type === 'integration') {
|
||||
files = await codegen.generateIntegrationPackage(targetPackage.pkg)
|
||||
} else if (targetPackage.type === 'interface') {
|
||||
files = await codegen.generateInterfacePackage(targetPackage.pkg)
|
||||
} else if (targetPackage.type === 'plugin') {
|
||||
files = await codegen.generatePluginPackage(targetPackage.pkg)
|
||||
} else {
|
||||
type _assertion = utils.types.AssertNever<typeof targetPackage>
|
||||
throw new errors.BotpressCLIError('Invalid package type')
|
||||
}
|
||||
|
||||
await this._install(installPath, files)
|
||||
}
|
||||
private async _chooseNewAlias(existingPackages: Record<string, string>) {
|
||||
const setAliasConfirmation = await this.prompt.confirm(
|
||||
'Do you want to set an alias to the package you are installing?'
|
||||
)
|
||||
if (!setAliasConfirmation) {
|
||||
throw new errors.AbortedOperationError()
|
||||
}
|
||||
|
||||
const alias = this._chooseUnusedAlias(existingPackages)
|
||||
|
||||
return alias
|
||||
}
|
||||
|
||||
private async _chooseUnusedAlias(existingPackages: Record<string, string>): Promise<string> {
|
||||
const alias = await this.prompt.text('Enter the new alias')
|
||||
const existingAlias = Object.entries(existingPackages).find(([dep, _]) => dep === alias)
|
||||
|
||||
if (!alias) {
|
||||
throw new errors.BotpressCLIError('You cannot set an empty alias')
|
||||
}
|
||||
if (!existingAlias) {
|
||||
return alias
|
||||
}
|
||||
|
||||
if (
|
||||
await this.prompt.confirm(
|
||||
`The alias ${alias} is already used for dependency ${existingAlias[1]}. Do you want to overwrite it?`
|
||||
)
|
||||
) {
|
||||
return alias
|
||||
}
|
||||
return this._chooseUnusedAlias(existingPackages)
|
||||
}
|
||||
|
||||
private async _addNewSinglePackage(ref: RefWithAlias) {
|
||||
const foundPackage = await this._findPackage(ref)
|
||||
const targetPackage = foundPackage.targetPackage
|
||||
const isDevPackage = targetPackage.type === 'integration' && !!targetPackage.pkg.devId
|
||||
let packageName: string
|
||||
if (isDevPackage) {
|
||||
this.logger.debug('Skipping bpDependencies update for dev integration')
|
||||
packageName = foundPackage.packageName
|
||||
} else {
|
||||
packageName = await this._addDependencyToPackage(ref, foundPackage.packageName, targetPackage)
|
||||
}
|
||||
await this._addSinglePackage(ref, { packageName, targetPackage })
|
||||
}
|
||||
|
||||
private async _findPackage(ref: RefWithAlias): Promise<{ packageName: string; targetPackage: InstallablePackage }> {
|
||||
const targetPackage = ref.type === 'path' ? await this._findLocalPackage(ref) : await this._findRemotePackage(ref)
|
||||
if (!targetPackage) {
|
||||
const strRef = pkgRef.formatPackageRef(ref)
|
||||
throw new errors.BotpressCLIError(`Could not find package "${strRef}"`)
|
||||
}
|
||||
const packageName = ref.alias ?? targetPackage.pkg.name
|
||||
|
||||
return { packageName, targetPackage }
|
||||
}
|
||||
|
||||
private async _findRemotePackage(ref: pkgRef.ApiPackageRef): Promise<InstallablePackage | undefined> {
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
if (this._pkgCouldBe(ref, 'integration')) {
|
||||
const integration = await api.findPublicOrPrivateIntegration(ref)
|
||||
if (integration) {
|
||||
const { name, version } = integration
|
||||
return { type: 'integration', pkg: { integration, name, version } }
|
||||
}
|
||||
}
|
||||
if (this._pkgCouldBe(ref, 'interface')) {
|
||||
const intrface = await api.findPublicOrPrivateInterface(ref)
|
||||
if (intrface) {
|
||||
const { name, version } = intrface
|
||||
return { type: 'interface', pkg: { interface: intrface, name, version } }
|
||||
}
|
||||
}
|
||||
if (this._pkgCouldBe(ref, 'plugin')) {
|
||||
const plugin = await api.findPublicOrPrivatePlugin(ref)
|
||||
if (plugin) {
|
||||
const { code } = plugin.public
|
||||
? await api.client.getPublicPluginCode({ id: plugin.id, platform: 'node' })
|
||||
: await api.client.getPluginCode({ id: plugin.id, platform: 'node' })
|
||||
const { name, version } = plugin
|
||||
return {
|
||||
type: 'plugin',
|
||||
pkg: {
|
||||
name,
|
||||
version,
|
||||
plugin,
|
||||
code,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
private async _findLocalPackage(ref: pkgRef.LocalPackageRef): Promise<InstallablePackage | undefined> {
|
||||
const absPath = utils.path.absoluteFrom(utils.path.cwd(), ref.path)
|
||||
const {
|
||||
definition: projectDefinition,
|
||||
implementation: projectImplementation,
|
||||
devId: projectDevId,
|
||||
} = await this._readProject(absPath)
|
||||
|
||||
if (projectDefinition?.type === 'integration') {
|
||||
const { name, version } = projectDefinition.definition
|
||||
let devId: string | undefined
|
||||
if (this.argv.useDev && projectDevId) {
|
||||
this.logger.warn(`Installing integration "${name}" with dev version "${projectDevId}"`)
|
||||
devId = projectDevId
|
||||
}
|
||||
|
||||
let createIntegrationReqBody = await this._getProjectCmd(ref.path).prepareCreateIntegrationBody(
|
||||
projectDefinition.definition
|
||||
)
|
||||
createIntegrationReqBody = {
|
||||
...createIntegrationReqBody,
|
||||
interfaces: utils.records.mapValues(projectDefinition.definition.interfaces ?? {}, (i) => ({
|
||||
id: '', // TODO: do this better
|
||||
...i,
|
||||
})),
|
||||
}
|
||||
return {
|
||||
type: 'integration',
|
||||
pkg: { path: absPath, devId, name, version, integration: createIntegrationReqBody },
|
||||
}
|
||||
}
|
||||
|
||||
if (projectDefinition?.type === 'interface') {
|
||||
const { name, version } = projectDefinition.definition
|
||||
const createInterfaceReqBody = await apiUtils.prepareCreateInterfaceBody(projectDefinition.definition)
|
||||
return {
|
||||
type: 'interface',
|
||||
pkg: { path: absPath, name, version, interface: createInterfaceReqBody },
|
||||
}
|
||||
}
|
||||
|
||||
if (projectDefinition?.type === 'plugin') {
|
||||
if (!projectImplementation) {
|
||||
throw new errors.BotpressCLIError(
|
||||
'Plugin implementation not found; Please build the plugin project before installing'
|
||||
)
|
||||
}
|
||||
|
||||
const pluginDefinition = projectDefinition.definition
|
||||
const { name, version } = pluginDefinition
|
||||
const code = projectImplementation
|
||||
|
||||
const createPluginReqBody = await apiUtils.prepareCreatePluginBody(pluginDefinition)
|
||||
return {
|
||||
type: 'plugin',
|
||||
pkg: {
|
||||
path: absPath,
|
||||
name,
|
||||
version,
|
||||
code,
|
||||
plugin: {
|
||||
...createPluginReqBody,
|
||||
dependencies: {
|
||||
interfaces: pluginDefinition.interfaces,
|
||||
integrations: pluginDefinition.integrations,
|
||||
},
|
||||
recurringEvents: pluginDefinition.recurringEvents,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (projectDefinition?.type === 'bot') {
|
||||
throw new errors.BotpressCLIError('Cannot install a bot as a package')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
private async _install(installPath: utils.path.AbsolutePath, files: codegen.File[]): Promise<void> {
|
||||
const line = this.logger.line()
|
||||
line.started(`Installing ${files.length} files to "${installPath}"`)
|
||||
try {
|
||||
for (const file of files) {
|
||||
const filePath = utils.path.absoluteFrom(installPath, file.path)
|
||||
const dirPath = pathlib.dirname(filePath)
|
||||
await fslib.promises.mkdir(dirPath, { recursive: true })
|
||||
await fslib.promises.writeFile(filePath, file.content)
|
||||
}
|
||||
line.success(`Installed ${files.length} files to "${installPath}"`)
|
||||
} finally {
|
||||
line.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private async _uninstall(installPath: utils.path.AbsolutePath): Promise<void> {
|
||||
await fslib.promises.rm(installPath, { recursive: true })
|
||||
}
|
||||
|
||||
private async _readProject(workDir: utils.path.AbsolutePath): Promise<{
|
||||
definition?: ProjectDefinition
|
||||
implementation?: string
|
||||
devId?: string
|
||||
}> {
|
||||
const cmd = this._getProjectCmd(workDir)
|
||||
|
||||
const { resolveProjectDefinition } = cmd.readProjectDefinitionFromFS()
|
||||
const definition = await resolveProjectDefinition().catch((thrown) => {
|
||||
if (thrown instanceof errors.ProjectDefinitionNotFoundError) {
|
||||
return undefined
|
||||
}
|
||||
throw thrown
|
||||
})
|
||||
|
||||
const devId = await cmd.projectCache.get('devId').catch((thrown) => {
|
||||
const err = errors.BotpressCLIError.wrap(thrown, 'Failed to read project dev ID from cache')
|
||||
this.logger.debug(err.message)
|
||||
return undefined
|
||||
})
|
||||
|
||||
const implementationAbsPath = utils.path.join(workDir, consts.fromWorkDir.outFileCJS)
|
||||
if (!fslib.existsSync(implementationAbsPath)) {
|
||||
return { definition, devId }
|
||||
}
|
||||
|
||||
const implementation = await fslib.promises.readFile(implementationAbsPath, 'utf8')
|
||||
return { definition, implementation, devId }
|
||||
}
|
||||
|
||||
private _pkgCouldBe = (ref: pkgRef.ApiPackageRef, pkgType: InstallablePackage['type']) => {
|
||||
if (ref.type === 'id') {
|
||||
// TODO: use ULID prefixes to determine the type of the package
|
||||
return true
|
||||
}
|
||||
if (!ref.pkg) {
|
||||
return true // ref does not specify the package type
|
||||
}
|
||||
return ref.pkg === pkgType
|
||||
}
|
||||
|
||||
private _getProjectCmd(workDir: string): _AnyProjectCommand {
|
||||
return new _AnyProjectCommand(apiUtils.ApiClient, this.prompt, this.logger, {
|
||||
...this.argv,
|
||||
workDir,
|
||||
})
|
||||
}
|
||||
|
||||
private async _addDependencyToPackage(
|
||||
ref: RefWithAlias,
|
||||
packageName: string,
|
||||
targetPackage: InstallablePackage
|
||||
): Promise<string> {
|
||||
const pkgJson = await utils.pkgJson.readPackageJson(this.argv.installPath).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Failed to read package.json file')
|
||||
})
|
||||
|
||||
if (!pkgJson) {
|
||||
this.logger.warn('No package.json found in the install path')
|
||||
return packageName
|
||||
}
|
||||
|
||||
const version =
|
||||
ref.type === 'path'
|
||||
? utils.path.relativePathFrom(
|
||||
utils.path.absoluteFrom(utils.path.cwd(), this.argv.installPath),
|
||||
utils.path.absoluteFrom(utils.path.cwd(), ref.path)
|
||||
)
|
||||
: `${targetPackage.type}:${targetPackage.pkg.name}@${targetPackage.pkg.version}`
|
||||
const { bpDependencies } = pkgJson
|
||||
if (!bpDependencies) {
|
||||
pkgJson.bpDependencies = { [packageName]: version }
|
||||
await utils.pkgJson.writePackageJson(this.argv.installPath, pkgJson)
|
||||
return packageName
|
||||
}
|
||||
|
||||
const bpDependenciesSchema = sdk.z.record(sdk.z.string())
|
||||
const parseResult = bpDependenciesSchema.safeParse(bpDependencies)
|
||||
if (!parseResult.success) {
|
||||
throw new errors.BotpressCLIError('Invalid bpDependencies found in package.json')
|
||||
}
|
||||
|
||||
const { data: validatedBpDeps } = parseResult
|
||||
|
||||
const alreadyPresentDep = Object.entries(validatedBpDeps).find(([key]) => key === packageName)
|
||||
if (alreadyPresentDep) {
|
||||
const alreadyPresentVersion = alreadyPresentDep[1]
|
||||
if (alreadyPresentVersion !== version) {
|
||||
this.logger.warn(
|
||||
`The dependency with alias ${packageName} is already present in the bpDependencies of package.json, but with version ${alreadyPresentVersion}.`
|
||||
)
|
||||
const res = await this.prompt.confirm(`Do you want to overwrite the dependency with version ${version}?`)
|
||||
if (!res) {
|
||||
const newAlias = await this._chooseNewAlias(validatedBpDeps)
|
||||
packageName = newAlias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pkgJson.bpDependencies = {
|
||||
...validatedBpDeps,
|
||||
[packageName]: version,
|
||||
}
|
||||
|
||||
await utils.pkgJson.writePackageJson(this.argv.installPath, pkgJson)
|
||||
return packageName
|
||||
}
|
||||
}
|
||||
|
||||
// this is a hack to avoid refactoring the project command class
|
||||
class _AnyProjectCommand extends ProjectCommand<ProjectCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
throw new errors.BotpressCLIError('Not implemented')
|
||||
}
|
||||
|
||||
public readProjectDefinitionFromFS(): ProjectDefinitionLazy {
|
||||
return super.readProjectDefinitionFromFS()
|
||||
}
|
||||
|
||||
public async prepareCreateIntegrationBody(
|
||||
integrationDef: sdk.IntegrationDefinition
|
||||
): Promise<apiUtils.CreateIntegrationRequestBody> {
|
||||
return super.prepareCreateIntegrationBody(integrationDef)
|
||||
}
|
||||
|
||||
public get projectCache(): utils.cache.FSKeyValueCache<ProjectCache> {
|
||||
return super.projectCache
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as errors from '../errors'
|
||||
import type { Logger } from '../logger'
|
||||
import type { CommandArgv, CommandDefinition } from '../typings'
|
||||
|
||||
export abstract class BaseCommand<C extends CommandDefinition> {
|
||||
public constructor(
|
||||
protected readonly logger: Logger,
|
||||
protected readonly argv: CommandArgv<C>
|
||||
) {}
|
||||
|
||||
protected abstract run(): Promise<void>
|
||||
protected bootstrap?(): Promise<void>
|
||||
protected teardown?(): Promise<void>
|
||||
|
||||
private get _cmdName(): string {
|
||||
return this.constructor.name
|
||||
}
|
||||
|
||||
public async handler(): Promise<{ exitCode: number }> {
|
||||
let exitCode = 0
|
||||
try {
|
||||
if (this.bootstrap) {
|
||||
await this.bootstrap()
|
||||
}
|
||||
await this.run()
|
||||
} catch (thrown) {
|
||||
const error = errors.BotpressCLIError.map(thrown)
|
||||
|
||||
this.logger.error(error.message)
|
||||
this.logger.debug(`[${this._cmdName}] ${errors.BotpressCLIError.fullStack(error)}`)
|
||||
|
||||
exitCode = 1
|
||||
} finally {
|
||||
if (this.teardown) {
|
||||
await this.teardown()
|
||||
}
|
||||
}
|
||||
|
||||
return { exitCode }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import chalk from 'chalk'
|
||||
import { ApiClient } from '../api/client'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as errors from '../errors'
|
||||
import { GlobalCommand } from './global-command'
|
||||
|
||||
export type GetBotCommandDefinition = typeof commandDefinitions.bots.subcommands.get
|
||||
export class GetBotCommand extends GlobalCommand<GetBotCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const { client } = await this.ensureLoginAndCreateClient(this.argv)
|
||||
|
||||
try {
|
||||
const { bot } = await client.getBot({ id: this.argv.botRef })
|
||||
this.logger.success(`Bot ${chalk.bold(this.argv.botRef)}:`)
|
||||
this.logger.json(bot)
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not get bot ${this.argv.botRef}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ListBotsCommandDefinition = typeof commandDefinitions.bots.subcommands.list
|
||||
export class ListBotsCommand extends GlobalCommand<ListBotsCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
|
||||
try {
|
||||
const { dev } = this.argv
|
||||
const bots = await api.listAllPages(
|
||||
(x) => api.client.listBots({ ...x, dev }),
|
||||
(r) => r.bots
|
||||
)
|
||||
this.logger.success('Bots:')
|
||||
this.logger.json(bots)
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Could not list bots')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type DeleteBotCommandDefinition = typeof commandDefinitions.bots.subcommands.delete
|
||||
export class DeleteBotCommand extends GlobalCommand<DeleteBotCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const { client } = await this.ensureLoginAndCreateClient(this.argv)
|
||||
|
||||
try {
|
||||
await client.deleteBot({ id: this.argv.botRef })
|
||||
this.logger.success(`Bot ${chalk.bold(this.argv.botRef)} deleted`)
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not delete bot ${this.argv.botRef}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type CreateBotCommandDefinition = typeof commandDefinitions.bots.subcommands.create
|
||||
export class CreateBotCommand extends GlobalCommand<CreateBotCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
|
||||
try {
|
||||
if (this.argv.ifNotExists) {
|
||||
await this._getOrCreate(api, this.argv.name)
|
||||
return
|
||||
}
|
||||
await this._create(api, this.argv.name)
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Could not create bot')
|
||||
}
|
||||
}
|
||||
|
||||
private _getOrCreate = async (api: ApiClient, name: string | undefined) => {
|
||||
if (!name) {
|
||||
throw new errors.BotpressCLIError('option --if-not-exists requires that a name be provided')
|
||||
}
|
||||
const existingBot = await api.findBotByName(name)
|
||||
if (existingBot) {
|
||||
this.logger.success(`Bot ${chalk.bold(name)} already exists`)
|
||||
this.logger.json(existingBot)
|
||||
return
|
||||
}
|
||||
return this._create(api, name)
|
||||
}
|
||||
|
||||
private _create = async (api: ApiClient, name: string | undefined) => {
|
||||
const { bot } = await api.client.createBot({ name })
|
||||
this.logger.success(`Bot ${chalk.bold(bot.id)}:`)
|
||||
this.logger.json(bot)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as utils from '../utils'
|
||||
import { BundleCommand } from './bundle-command'
|
||||
import { GenerateCommand } from './gen-command'
|
||||
import { ProjectCommand } from './project-command'
|
||||
|
||||
export type BuildCommandDefinition = typeof commandDefinitions.build
|
||||
export class BuildCommand extends ProjectCommand<BuildCommandDefinition> {
|
||||
public async run(buildContext?: utils.esbuild.BuildCodeContext): Promise<void> {
|
||||
const t0 = Date.now()
|
||||
const { projectType } = this.readProjectDefinitionFromFS()
|
||||
|
||||
if (projectType === 'interface') {
|
||||
this.logger.success('Interface projects have nothing to build.')
|
||||
return
|
||||
}
|
||||
|
||||
await this._runGenerate()
|
||||
|
||||
await this._runBundle(buildContext)
|
||||
const dt = Date.now() - t0
|
||||
this.logger.log(`Build completed in ${dt}ms`)
|
||||
}
|
||||
|
||||
private _runGenerate() {
|
||||
return new GenerateCommand(this.api, this.prompt, this.logger, this.argv)
|
||||
.setProjectContext(this.projectContext)
|
||||
.run()
|
||||
}
|
||||
|
||||
private _runBundle(buildContext?: utils.esbuild.BuildCodeContext) {
|
||||
return new BundleCommand(this.api, this.prompt, this.logger, this.argv)
|
||||
.setProjectContext(this.projectContext)
|
||||
.run(buildContext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import chalk from 'chalk'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as errors from '../errors'
|
||||
import * as utils from '../utils'
|
||||
import { ProjectCommand } from './project-command'
|
||||
|
||||
export type BundleCommandDefinition = typeof commandDefinitions.bundle
|
||||
export class BundleCommand extends ProjectCommand<BundleCommandDefinition> {
|
||||
public async run(buildContext?: utils.esbuild.BuildCodeContext): Promise<void> {
|
||||
const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS()
|
||||
|
||||
const abs = this.projectPaths.abs
|
||||
const rel = this.projectPaths.rel('workDir')
|
||||
const line = this.logger.line()
|
||||
|
||||
if (projectType === 'interface') {
|
||||
this.logger.success('Interface projects have no implementation to bundle.')
|
||||
} 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 (projectType === 'bot') {
|
||||
line.started('Bundling bot...')
|
||||
await this._bundle(abs.outFileCJS, buildContext)
|
||||
} else if (projectType === 'plugin') {
|
||||
line.started('Bundling plugin for node and browser platforms...')
|
||||
await Promise.all([
|
||||
this._bundle(abs.outFileCJS, buildContext),
|
||||
this._bundle(abs.outFileESM, buildContext, { platform: 'browser', format: 'esm' }),
|
||||
])
|
||||
} else {
|
||||
throw new errors.UnsupportedProjectType()
|
||||
}
|
||||
|
||||
line.success(`Bundle available at ${chalk.grey(rel.outDir)}`)
|
||||
}
|
||||
|
||||
private async _bundle(
|
||||
outfile: string,
|
||||
buildContext?: utils.esbuild.BuildCodeContext,
|
||||
props: Partial<utils.esbuild.BuildOptions> = {}
|
||||
) {
|
||||
const abs = this.projectPaths.abs
|
||||
const buildProps = {
|
||||
outfile,
|
||||
absWorkingDir: abs.workDir,
|
||||
code: this._code,
|
||||
}
|
||||
const buildOptions = {
|
||||
...this._buildOptions,
|
||||
...props,
|
||||
}
|
||||
|
||||
if (buildContext) {
|
||||
await buildContext.rebuild(buildProps, buildOptions)
|
||||
return
|
||||
}
|
||||
|
||||
await utils.esbuild.buildCode(buildProps, buildOptions)
|
||||
}
|
||||
|
||||
private get _code() {
|
||||
const rel = this.projectPaths.rel('workDir')
|
||||
const unixPath = utils.path.toUnix(rel.entryPoint)
|
||||
const importFrom = utils.path.rmExtension(unixPath)
|
||||
return `import x from './${importFrom}'; export default x; export const handler = x.handler;`
|
||||
}
|
||||
|
||||
private get _buildOptions(): Partial<utils.esbuild.BuildOptions> {
|
||||
return {
|
||||
logLevel: this.argv.verbose ? 'info' : 'silent',
|
||||
sourcemap: this.argv.sourceMap,
|
||||
minify: this.argv.minify,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import * as chat from '@botpress/chat'
|
||||
import * as client from '@botpress/client'
|
||||
import semver from 'semver'
|
||||
import { ApiClient } from '../api'
|
||||
import { Chat } from '../chat'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as consts from '../consts'
|
||||
import * as errors from '../errors'
|
||||
import * as utils from '../utils'
|
||||
import { GlobalCommand } from './global-command'
|
||||
|
||||
type IntegrationInstance = {
|
||||
alias: string
|
||||
instance: client.Bot['integrations'][string]
|
||||
}
|
||||
|
||||
export type ChatCommandDefinition = typeof commandDefinitions.chat
|
||||
export class ChatCommand extends GlobalCommand<ChatCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
if (process.platform === 'win32') {
|
||||
this.logger.warn('The chat command was not tested on Windows and may not work as expected')
|
||||
}
|
||||
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
const botId = this.argv.botId ?? (await this._selectBot(api))
|
||||
const { bot } = await api.client.getBot({ id: botId }).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not fetch bot "${botId}"`)
|
||||
})
|
||||
|
||||
const targetChatVersion = this._getChatApiTargetVersionRange()
|
||||
let chatIntegrationInstance = this._findChatInstance(bot)
|
||||
|
||||
if (!chatIntegrationInstance) {
|
||||
this.logger.log(`Chat integration with version ${targetChatVersion} is not installed in the selected bot`)
|
||||
const confirmInstall = await this.prompt.confirm('Do you wish to install it now?')
|
||||
if (!confirmInstall) {
|
||||
throw new errors.BotpressCLIError('Chat integration is required to proceed')
|
||||
}
|
||||
chatIntegrationInstance = await this._installChatIntegration(api, botId)
|
||||
}
|
||||
|
||||
const { webhookId } = chatIntegrationInstance.instance
|
||||
const chatApiBaseUrl = this._getChatApiUrl(api)
|
||||
this.logger.debug(`using chat api url: "${chatApiBaseUrl}"`)
|
||||
|
||||
const chatApiUrl = `${chatApiBaseUrl}/${webhookId}`
|
||||
const chatClient = await chat.Client.connect({ apiUrl: chatApiUrl })
|
||||
await this._chat(chatClient)
|
||||
}
|
||||
|
||||
private _chat = async (client: chat.AuthenticatedClient): Promise<void> => {
|
||||
const convLine = this.logger.line()
|
||||
convLine.started('Creating a conversation...')
|
||||
const { conversation } = await client.createConversation({})
|
||||
convLine.success(`Conversation created with id "${conversation.id}"`)
|
||||
convLine.commit()
|
||||
|
||||
const chat = Chat.launch({ client, conversationId: conversation.id, protocol: this.argv.protocol })
|
||||
await chat.wait()
|
||||
}
|
||||
|
||||
private _getChatApiUrl = (api: ApiClient): string => {
|
||||
if (this.argv.chatApiUrl) {
|
||||
return this.argv.chatApiUrl
|
||||
}
|
||||
|
||||
const parseResult = utils.url.parse(api.url)
|
||||
if (parseResult.status === 'error') {
|
||||
return consts.defaultChatApiUrl
|
||||
}
|
||||
|
||||
const { host, ...url } = parseResult.url
|
||||
if (!host.startsWith('api.')) {
|
||||
return consts.defaultChatApiUrl
|
||||
}
|
||||
|
||||
const newHost = host.replace('api.', 'chat.')
|
||||
return utils.url.format({ ...url, host: newHost })
|
||||
}
|
||||
|
||||
private _selectBot = async (api: ApiClient): Promise<string> => {
|
||||
const availableBots = await api
|
||||
.listAllPages(api.client.listBots, (r) => r.bots)
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Could not fetch existing bots')
|
||||
})
|
||||
|
||||
if (!availableBots.length) {
|
||||
throw new errors.NoBotsFoundError()
|
||||
}
|
||||
|
||||
const prompted = await this.prompt.select('Which bot do you want to deploy?', {
|
||||
choices: availableBots.map((bot) => ({ title: bot.name, value: bot.id })),
|
||||
})
|
||||
|
||||
if (!prompted) {
|
||||
throw new errors.ParamRequiredError('Bot Id')
|
||||
}
|
||||
|
||||
return prompted
|
||||
}
|
||||
|
||||
private _installChatIntegration = async (api: ApiClient, botId: string): Promise<IntegrationInstance> => {
|
||||
const line = this.logger.line()
|
||||
line.started('Installing chat integration...')
|
||||
|
||||
const { integration } = await api.client.getPublicIntegration({
|
||||
name: 'chat',
|
||||
version: this._getChatApiTargetVersion(),
|
||||
})
|
||||
|
||||
const { bot } = await api.client.updateBot({
|
||||
id: botId,
|
||||
integrations: {
|
||||
[integration.name]: {
|
||||
integrationId: integration.id,
|
||||
enabled: true,
|
||||
configuration: {}, // empty object will always be a valid chat integration configuration
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
line.success('Chat integration installed')
|
||||
line.commit()
|
||||
|
||||
const inst = this._findChatInstance(bot)
|
||||
if (!inst) {
|
||||
throw new errors.BotpressCLIError('Chat integration was installed but could not be found')
|
||||
}
|
||||
|
||||
return inst
|
||||
}
|
||||
|
||||
private _findChatInstance = (bot: client.Bot): IntegrationInstance | undefined => {
|
||||
const integrationInstances = Object.entries(bot.integrations).map(([alias, integrationInstance]) => ({
|
||||
alias,
|
||||
instance: integrationInstance,
|
||||
}))
|
||||
|
||||
const targetChatVersion = this._getChatApiTargetVersionRange()
|
||||
return integrationInstances.find(
|
||||
(i) =>
|
||||
i.instance.name === 'chat' &&
|
||||
(semver.satisfies(i.instance.version, targetChatVersion) || i.instance.version === 'dev')
|
||||
)
|
||||
}
|
||||
|
||||
private _getChatApiTargetVersionRange = (): string => {
|
||||
const targetApiVersion = this._getChatApiTargetVersion()
|
||||
const nextMajor = semver.inc(targetApiVersion, 'major')
|
||||
return `>=${targetApiVersion} <${nextMajor}`
|
||||
}
|
||||
|
||||
private _getChatApiTargetVersion = (): string => {
|
||||
const dummyClient = new chat.Client({ apiUrl: '' })
|
||||
return dummyClient.apiVersion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
import * as client from '@botpress/client'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import chalk from 'chalk'
|
||||
import * as fs from 'fs'
|
||||
import semver from 'semver'
|
||||
import * as apiUtils from '../api'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as errors from '../errors'
|
||||
import * as tables from '../tables'
|
||||
import * as utils from '../utils'
|
||||
import { BuildCommand } from './build-command'
|
||||
import { ProjectCommand } from './project-command'
|
||||
|
||||
export type DeployCommandDefinition = typeof commandDefinitions.deploy
|
||||
export class DeployCommand extends ProjectCommand<DeployCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
|
||||
if (!this.argv.noBuild) {
|
||||
await this._runBuild() // This ensures the bundle is always synced with source code
|
||||
}
|
||||
|
||||
const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS()
|
||||
|
||||
if (projectType === 'integration') {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
return this._deployIntegration(api, projectDef.definition)
|
||||
}
|
||||
if (projectType === 'interface') {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
return this._deployInterface(api, projectDef.definition)
|
||||
}
|
||||
if (projectType === 'plugin') {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
return this._deployPlugin(api, projectDef.definition)
|
||||
}
|
||||
if (projectType === 'bot') {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
return this._deployBot(api, projectDef.definition, this.argv.botId, this.argv.createNewBot)
|
||||
}
|
||||
throw new errors.UnsupportedProjectType()
|
||||
}
|
||||
|
||||
private async _runBuild() {
|
||||
return new BuildCommand(this.api, this.prompt, this.logger, this.argv).setProjectContext(this.projectContext).run()
|
||||
}
|
||||
|
||||
private get _visibility(): 'public' | 'private' | 'unlisted' {
|
||||
if (this.argv.public && this.argv.visibility === 'private') {
|
||||
this.logger.warn('The --public flag is deprecated. Please use "--visibility public" instead.')
|
||||
return 'public'
|
||||
}
|
||||
|
||||
if (this.argv.public && this.argv.visibility !== 'private') {
|
||||
this.logger.warn('The --public flag and --visibility option are both present. Ignoring the --public flag...')
|
||||
}
|
||||
|
||||
return this.argv.visibility
|
||||
}
|
||||
|
||||
private async _deployIntegration(api: apiUtils.ApiClient, integrationDef: sdk.IntegrationDefinition) {
|
||||
const res = await this.manageWorkspaceHandle(api, { type: 'integration', definition: integrationDef })
|
||||
if (!res) return
|
||||
const { definition: updatedIntegrationDef, workspaceId } = res
|
||||
integrationDef = updatedIntegrationDef
|
||||
if (workspaceId) {
|
||||
api = api.switchWorkspace(workspaceId)
|
||||
}
|
||||
if (this.argv.bypassBreakingChangeDetection) {
|
||||
api = api.withExtraHeaders({ 'x-bypass-breaking-changes-detection': 'true' })
|
||||
}
|
||||
|
||||
const { name, version } = integrationDef
|
||||
|
||||
if (integrationDef.icon && !integrationDef.icon.toLowerCase().endsWith('.svg')) {
|
||||
throw new errors.BotpressCLIError('Icon must be an SVG file')
|
||||
}
|
||||
|
||||
if (integrationDef.readme && !integrationDef.readme.toLowerCase().endsWith('.md')) {
|
||||
throw new errors.BotpressCLIError('Readme must be a Markdown file')
|
||||
}
|
||||
|
||||
const integration = await api.findPublicOrPrivateIntegration({ type: 'name', name, version })
|
||||
if (integration && integration.workspaceId !== api.workspaceId) {
|
||||
throw new errors.BotpressCLIError(
|
||||
`Public integration ${name} v${version} is already deployed in another workspace.`
|
||||
)
|
||||
}
|
||||
|
||||
if (integration && integration.visibility !== 'private' && !api.isBotpressWorkspace) {
|
||||
this.logger.warn(
|
||||
`Integration ${name} v${version} is already deployed publicly and cannot be updated. You should publish a new version instead.`
|
||||
)
|
||||
}
|
||||
|
||||
let message: string
|
||||
if (integration) {
|
||||
this.logger.warn('Integration already exists. If you decide to deploy, it will override the existing one.')
|
||||
message = `Are you sure you want to override integration ${name} v${version}?`
|
||||
} else {
|
||||
message = `Are you sure you want to deploy integration ${name} v${version}?`
|
||||
}
|
||||
|
||||
const confirm = await this.prompt.confirm(message)
|
||||
if (!confirm) {
|
||||
this.logger.log('Aborted')
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.debug('Preparing integration request body...')
|
||||
|
||||
const createBody = {
|
||||
...(await this.prepareCreateIntegrationBody(integrationDef)),
|
||||
...(await this.prepareIntegrationDependencies(integrationDef, api)),
|
||||
visibility: this._visibility,
|
||||
sdkVersion: integrationDef.metadata?.sdkVersion,
|
||||
url: this.argv.url,
|
||||
}
|
||||
|
||||
const startedMessage = `Deploying integration ${chalk.bold(name)} v${version}...`
|
||||
const successMessage = 'Integration deployed'
|
||||
if (integration) {
|
||||
const updateBody = apiUtils.prepareUpdateIntegrationBody(
|
||||
{
|
||||
id: integration.id,
|
||||
...createBody,
|
||||
},
|
||||
integration
|
||||
)
|
||||
|
||||
const { secrets: knownSecrets } = integration
|
||||
updateBody.secrets = await this.promptSecrets(integrationDef, this.argv, { knownSecrets })
|
||||
this._detectDeprecatedFeatures(integrationDef, { allowDeprecated: true })
|
||||
|
||||
const line = this.logger.line()
|
||||
line.started(startedMessage)
|
||||
|
||||
if (this.argv.dryRun) {
|
||||
this.logger.log('Dry-run mode is active. Simulating integration update...')
|
||||
|
||||
await api.client.validateIntegrationUpdate(updateBody).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not update integration "${name}"`)
|
||||
})
|
||||
} else {
|
||||
await api.client.updateIntegration(updateBody).catch((thrown) => {
|
||||
const error = errors.BotpressCLIError.wrap(thrown, `Could not update integration "${name}"`)
|
||||
if (
|
||||
api.isBotpressWorkspace &&
|
||||
!this.argv.bypassBreakingChangeDetection &&
|
||||
client.isApiError(thrown) &&
|
||||
thrown.type === 'BreakingChanges'
|
||||
) {
|
||||
this.logger.warn('Tip: redeploy with --bypassBreakingChangeDetection to skip this check')
|
||||
}
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
line.success(successMessage)
|
||||
} else {
|
||||
this.logger.debug(`looking for previous version of integration "${name}"`)
|
||||
const previousVersion = await api.findPreviousIntegrationVersion({ type: 'name', name, version })
|
||||
|
||||
if (previousVersion) {
|
||||
this.logger.debug(`previous version found: ${previousVersion.version}`)
|
||||
} else {
|
||||
this.logger.debug('no previous version found')
|
||||
}
|
||||
|
||||
const knownSecrets = previousVersion?.secrets
|
||||
|
||||
createBody.secrets = await this.promptSecrets(integrationDef, this.argv, { knownSecrets })
|
||||
this._detectDeprecatedFeatures(integrationDef, {
|
||||
allowDeprecated: this._allowDeprecatedFeatures(integrationDef, previousVersion),
|
||||
})
|
||||
|
||||
const line = this.logger.line()
|
||||
line.started(startedMessage)
|
||||
|
||||
if (this.argv.dryRun) {
|
||||
this.logger.log('Dry-run mode is active. Simulating integration creation...')
|
||||
|
||||
await api.client.validateIntegrationCreation(createBody).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not create integration "${name}"`)
|
||||
})
|
||||
} else {
|
||||
await api.client.createIntegration(createBody).catch((thrown) => {
|
||||
const error = errors.BotpressCLIError.wrap(thrown, `Could not create integration "${name}"`)
|
||||
if (
|
||||
api.isBotpressWorkspace &&
|
||||
!this.argv.bypassBreakingChangeDetection &&
|
||||
client.isApiError(thrown) &&
|
||||
thrown.type === 'BreakingChanges'
|
||||
) {
|
||||
this.logger.warn('Tip: redeploy with --bypassBreakingChangeDetection to skip this check')
|
||||
}
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
line.success(successMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private async _deployInterface(api: apiUtils.ApiClient, interfaceDeclaration: sdk.InterfaceDefinition) {
|
||||
if (this._visibility === 'unlisted') {
|
||||
throw new errors.BotpressCLIError(
|
||||
'Unlisted visibility is not supported for interfaces. Please use "public" or "private".'
|
||||
)
|
||||
}
|
||||
|
||||
if (interfaceDeclaration.icon && !interfaceDeclaration.icon.toLowerCase().endsWith('.svg')) {
|
||||
throw new errors.BotpressCLIError('Icon must be an SVG file')
|
||||
}
|
||||
|
||||
if (interfaceDeclaration.readme && !interfaceDeclaration.readme.toLowerCase().endsWith('.md')) {
|
||||
throw new errors.BotpressCLIError('Readme must be a Markdown file')
|
||||
}
|
||||
|
||||
const { name, version } = interfaceDeclaration
|
||||
const intrface = await api.findPublicOrPrivateInterface({ type: 'name', name, version })
|
||||
|
||||
let message: string
|
||||
if (intrface) {
|
||||
this.logger.warn('Interface already exists. If you decide to deploy, it will override the existing one.')
|
||||
message = `Are you sure you want to override interface ${name} v${version}?`
|
||||
} else {
|
||||
message = `Are you sure you want to deploy interface ${name} v${version}?`
|
||||
}
|
||||
|
||||
const confirm = await this.prompt.confirm(message)
|
||||
if (!confirm) {
|
||||
this.logger.log('Aborted')
|
||||
return
|
||||
}
|
||||
|
||||
const icon = await this.readProjectFile(interfaceDeclaration.icon, 'base64')
|
||||
const readme = await this.readProjectFile(interfaceDeclaration.readme, 'base64')
|
||||
|
||||
if (this._visibility !== 'public') {
|
||||
this.logger.warn(
|
||||
'You are currently publishing a private interface, which cannot be used by integrations and plugins. To fix this, change the visibility to "public"'
|
||||
)
|
||||
}
|
||||
|
||||
const createBody = {
|
||||
...(await apiUtils.prepareCreateInterfaceBody(interfaceDeclaration)),
|
||||
public: this._visibility === 'public',
|
||||
icon,
|
||||
readme,
|
||||
sdkVersion: interfaceDeclaration.metadata?.sdkVersion,
|
||||
}
|
||||
|
||||
const startedMessage = `Deploying interface ${chalk.bold(name)} v${version}...`
|
||||
const successMessage = 'Interface deployed'
|
||||
if (intrface) {
|
||||
const updateBody = apiUtils.prepareUpdateInterfaceBody(
|
||||
{
|
||||
id: intrface.id,
|
||||
...createBody,
|
||||
},
|
||||
intrface
|
||||
)
|
||||
|
||||
const line = this.logger.line()
|
||||
line.started(startedMessage)
|
||||
|
||||
if (this.argv.dryRun) {
|
||||
this.logger.warn('Dry-run mode is not supported for interface updates. Skipping deployment...')
|
||||
} else {
|
||||
await api.client.updateInterface(updateBody).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not update interface "${name}"`)
|
||||
})
|
||||
}
|
||||
|
||||
line.success(successMessage)
|
||||
} else {
|
||||
const line = this.logger.line()
|
||||
line.started(startedMessage)
|
||||
|
||||
if (this.argv.dryRun) {
|
||||
this.logger.warn('Dry-run mode is not supported for interface creation. Skipping deployment...')
|
||||
} else {
|
||||
await api.client.createInterface(createBody).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not create interface "${name}"`)
|
||||
})
|
||||
}
|
||||
|
||||
line.success(successMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private async _deployPlugin(api: apiUtils.ApiClient, pluginDef: sdk.PluginDefinition) {
|
||||
const res = await this.manageWorkspaceHandle(api, { type: 'plugin', definition: pluginDef })
|
||||
if (!res) return
|
||||
const { definition: updatedPluginDef, workspaceId } = res
|
||||
pluginDef = updatedPluginDef
|
||||
if (workspaceId) {
|
||||
api = api.switchWorkspace(workspaceId)
|
||||
}
|
||||
|
||||
const codeCJS = await fs.promises.readFile(this.projectPaths.abs.outFileCJS, 'utf-8')
|
||||
const codeESM = await fs.promises.readFile(this.projectPaths.abs.outFileESM, 'utf-8')
|
||||
|
||||
const { name, version } = pluginDef
|
||||
|
||||
if (pluginDef.icon && !pluginDef.icon.toLowerCase().endsWith('.svg')) {
|
||||
throw new errors.BotpressCLIError('Icon must be an SVG file')
|
||||
}
|
||||
|
||||
if (pluginDef.readme && !pluginDef.readme.toLowerCase().endsWith('.md')) {
|
||||
throw new errors.BotpressCLIError('Readme must be a Markdown file')
|
||||
}
|
||||
|
||||
const plugin = await api.findPublicOrPrivatePlugin({ type: 'name', name, version })
|
||||
|
||||
let message: string
|
||||
if (plugin) {
|
||||
this.logger.warn('Plugin already exists. If you decide to deploy, it will override the existing one.')
|
||||
message = `Are you sure you want to override plugin ${name} v${version}?`
|
||||
} else {
|
||||
message = `Are you sure you want to deploy plugin ${name} v${version}?`
|
||||
}
|
||||
|
||||
const confirm = await this.prompt.confirm(message)
|
||||
if (!confirm) {
|
||||
this.logger.log('Aborted')
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.debug('Preparing plugin request body...')
|
||||
|
||||
const icon = await this.readProjectFile(pluginDef.icon, 'base64')
|
||||
const readme = await this.readProjectFile(pluginDef.readme, 'base64')
|
||||
|
||||
const createBody = {
|
||||
...(await apiUtils.prepareCreatePluginBody(pluginDef)),
|
||||
...(await this.preparePluginDependencies(pluginDef, api)),
|
||||
visibility: this._visibility,
|
||||
icon,
|
||||
readme,
|
||||
code: {
|
||||
node: codeCJS,
|
||||
browser: codeESM,
|
||||
},
|
||||
sdkVersion: pluginDef.metadata?.sdkVersion,
|
||||
}
|
||||
|
||||
const startedMessage = `Deploying plugin ${chalk.bold(name)} v${version}...`
|
||||
const successMessage = 'Plugin deployed'
|
||||
if (plugin) {
|
||||
const updateBody = apiUtils.prepareUpdatePluginBody(
|
||||
{
|
||||
id: plugin.id,
|
||||
...createBody,
|
||||
},
|
||||
plugin
|
||||
)
|
||||
|
||||
const line = this.logger.line()
|
||||
line.started(startedMessage)
|
||||
|
||||
if (this.argv.dryRun) {
|
||||
this.logger.warn('Dry-run mode is not supported for plugin updates. Skipping deployment...')
|
||||
} else {
|
||||
await api.client.updatePlugin(updateBody).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not update plugin "${name}"`)
|
||||
})
|
||||
}
|
||||
|
||||
line.success(successMessage)
|
||||
} else {
|
||||
const line = this.logger.line()
|
||||
line.started(startedMessage)
|
||||
|
||||
if (this.argv.dryRun) {
|
||||
this.logger.warn('Dry-run mode is not supported for plugin creation. Skipping deployment...')
|
||||
} else {
|
||||
await api.client.createPlugin(createBody).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not create plugin "${name}"`)
|
||||
})
|
||||
}
|
||||
|
||||
line.success(successMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private _allowDeprecatedFeatures(
|
||||
integrationDef: sdk.IntegrationDefinition,
|
||||
previousVersion: client.Integration | undefined
|
||||
): boolean {
|
||||
if (this.argv.allowDeprecated) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!previousVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
const versionDiff = semver.diff(integrationDef.version, previousVersion.version)
|
||||
if (!versionDiff) {
|
||||
return false
|
||||
}
|
||||
|
||||
return utils.semver.releases.lt(versionDiff, 'major')
|
||||
}
|
||||
|
||||
private _detectDeprecatedFeatures(
|
||||
integrationDef: sdk.IntegrationDefinition,
|
||||
opts: { allowDeprecated?: boolean } = {}
|
||||
) {
|
||||
const deprecatedFields: string[] = []
|
||||
const { user, channels } = integrationDef
|
||||
if (user?.creation?.enabled) {
|
||||
deprecatedFields.push('user.creation')
|
||||
}
|
||||
|
||||
for (const [channelName, channel] of Object.entries(channels ?? {})) {
|
||||
if (channel?.conversation?.creation?.enabled) {
|
||||
deprecatedFields.push(`channels.${channelName}.creation`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!deprecatedFields.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const errorMessage = `The following fields of the integration's definition are deprecated: ${deprecatedFields.join(
|
||||
', '
|
||||
)}`
|
||||
|
||||
if (opts.allowDeprecated) {
|
||||
this.logger.warn(errorMessage)
|
||||
} else {
|
||||
throw new errors.BotpressCLIError(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private async _deployBot(
|
||||
api: apiUtils.ApiClient,
|
||||
botDefinition: sdk.BotDefinition,
|
||||
argvBotId: string | undefined,
|
||||
argvCreateNew: boolean | undefined
|
||||
) {
|
||||
if (this.argv.dryRun) {
|
||||
this.logger.warn('Dry-run mode is not supported for bot deployments. Skipping deployment...')
|
||||
return
|
||||
}
|
||||
|
||||
const outfile = this.projectPaths.abs.outFileCJS
|
||||
const code = await fs.promises.readFile(outfile, 'utf-8')
|
||||
|
||||
let bot: client.Bot
|
||||
if (argvBotId && argvCreateNew) {
|
||||
throw new errors.BotpressCLIError('Cannot specify both --botId and --createNew')
|
||||
} else if (argvCreateNew) {
|
||||
const confirm = await this.prompt.confirm('Are you sure you want to create a new bot ?')
|
||||
if (!confirm) {
|
||||
this.logger.log('Aborted')
|
||||
return
|
||||
}
|
||||
|
||||
bot = await this._createNewBot(api)
|
||||
} else {
|
||||
bot = await this._getExistingBot(api, argvBotId)
|
||||
|
||||
const confirm = await this.prompt.confirm(`Are you sure you want to deploy the bot "${bot.name}"?`)
|
||||
if (!confirm) {
|
||||
this.logger.log('Aborted')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const line = this.logger.line()
|
||||
line.started(`Deploying bot ${chalk.bold(bot.name)}...`)
|
||||
|
||||
const updateBotBody = apiUtils.prepareUpdateBotBody(
|
||||
{
|
||||
...(await apiUtils.prepareCreateBotBody(botDefinition)),
|
||||
...(await this.prepareBotDependencies(botDefinition, api)),
|
||||
id: bot.id,
|
||||
code,
|
||||
},
|
||||
bot
|
||||
)
|
||||
|
||||
updateBotBody.secrets = await this.promptSecrets(botDefinition, this.argv, { knownSecrets: bot.secrets })
|
||||
|
||||
const { bot: updatedBot } = await api.client.updateBot(updateBotBody).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not update bot "${bot.name}"`)
|
||||
})
|
||||
|
||||
this.validateIntegrationRegistration(updatedBot, (failedIntegrations) =>
|
||||
this.logger.warn(
|
||||
`Some integrations failed to register:\n${Object.entries(failedIntegrations)
|
||||
.map(([key, int]) => `• ${key}: ${int.statusReason}`)
|
||||
.join('\n')}`
|
||||
)
|
||||
)
|
||||
|
||||
const tablesPublisher = new tables.TablesPublisher({ api, logger: this.logger, prompt: this.prompt })
|
||||
await tablesPublisher.deployTables({ botId: updatedBot.id, botDefinition })
|
||||
|
||||
line.success('Bot deployed')
|
||||
await this.displayIntegrationUrls({ api, bot: updatedBot })
|
||||
}
|
||||
|
||||
private async _createNewBot(api: apiUtils.ApiClient): Promise<client.Bot> {
|
||||
const line = this.logger.line()
|
||||
const { bot: createdBot } = await api.client.createBot({}).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Could not create bot')
|
||||
})
|
||||
line.success(`Bot created with ID "${createdBot.id}" and name "${createdBot.name}"`)
|
||||
await this.projectCache.set('botId', createdBot.id)
|
||||
return createdBot
|
||||
}
|
||||
|
||||
private async _getExistingBot(api: apiUtils.ApiClient, botId: string | undefined): Promise<client.Bot> {
|
||||
const promptedBotId = await this.projectCache.sync('botId', botId, async (defaultId) => {
|
||||
const userBots = await api
|
||||
.listAllPages(api.client.listBots, (r) => r.bots)
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Could not fetch existing bots')
|
||||
})
|
||||
|
||||
if (!userBots.length) {
|
||||
throw new errors.NoBotsFoundError()
|
||||
}
|
||||
|
||||
const initial = userBots.find((bot) => bot.id === defaultId)
|
||||
|
||||
const prompted = await this.prompt.select('Which bot do you want to deploy?', {
|
||||
initial: initial && { title: initial.name, value: initial.id },
|
||||
choices: userBots.map((bot) => ({ title: bot.name, value: bot.id })),
|
||||
})
|
||||
|
||||
if (!prompted) {
|
||||
throw new errors.ParamRequiredError('Bot Id')
|
||||
}
|
||||
|
||||
return prompted
|
||||
})
|
||||
|
||||
const { bot: fetchedBot } = await api.client.getBot({ id: promptedBotId }).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Could not get bot info')
|
||||
})
|
||||
|
||||
return fetchedBot
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
import type * as client from '@botpress/client'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { TunnelRequest, TunnelResponse } from '@bpinternal/tunnel'
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import chalk from 'chalk'
|
||||
import { isEqual } from 'lodash'
|
||||
import * as pathlib from 'path'
|
||||
import * as uuid from 'uuid'
|
||||
import * as apiUtils from '../api'
|
||||
import { secretEnvVariableName, stripSecretEnvVariablePrefix } from '../code-generation/secret-module'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as errors from '../errors'
|
||||
import * as tables from '../tables'
|
||||
import * as utils from '../utils'
|
||||
import { Worker } from '../worker'
|
||||
import { BuildCommand } from './build-command'
|
||||
import { ProjectCommand, ProjectDefinition } from './project-command'
|
||||
|
||||
const DEFAULT_BOT_PORT = 8075
|
||||
const DEFAULT_INTEGRATION_PORT = 8076
|
||||
const TUNNEL_HELLO_INTERVAL = 5000
|
||||
const FILEWATCHER_DEBOUNCE_MS = 500
|
||||
|
||||
export type DevCommandDefinition = typeof commandDefinitions.dev
|
||||
export class DevCommand extends ProjectCommand<DevCommandDefinition> {
|
||||
private _initialDef: ProjectDefinition | undefined = undefined
|
||||
private _deployedIntegrationName: string | undefined = undefined
|
||||
private _cacheDevRequestBody: apiUtils.UpdateBotRequestBody | apiUtils.UpdateIntegrationRequestBody | undefined
|
||||
private _buildContext: utils.esbuild.BuildCodeContext
|
||||
|
||||
public constructor(...args: ConstructorParameters<typeof ProjectCommand<DevCommandDefinition>>) {
|
||||
super(...args)
|
||||
this._buildContext = new utils.esbuild.BuildCodeContext()
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
this.logger.warn('This command is experimental and subject to breaking changes without notice.')
|
||||
|
||||
const watchEnabled = this.argv.watch !== false
|
||||
let api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
|
||||
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
|
||||
|
||||
if (projectDef.type === 'integration') {
|
||||
const handleResult = await this.manageWorkspaceHandle(api, projectDef)
|
||||
if (!handleResult) return
|
||||
if (handleResult.workspaceId) {
|
||||
api = api.switchWorkspace(handleResult.workspaceId)
|
||||
}
|
||||
this._deployedIntegrationName = handleResult.definition.name
|
||||
}
|
||||
|
||||
let env: Record<string, string> = {
|
||||
...process.env,
|
||||
BP_API_URL: api.url,
|
||||
BP_TOKEN: api.token,
|
||||
}
|
||||
|
||||
const defaultPort = this._initialDef.type === 'integration' ? DEFAULT_INTEGRATION_PORT : DEFAULT_BOT_PORT
|
||||
if (this._initialDef.type === 'integration' || this._initialDef.type === 'bot') {
|
||||
const knownSecrets = await this._readKnownSecretsFromCache()
|
||||
let secretEnvVariables = await this.promptSecrets(this._initialDef.definition, this.argv, {
|
||||
knownSecrets: Object.keys(knownSecrets),
|
||||
formatEnv: true,
|
||||
})
|
||||
secretEnvVariables = { ...this._applyPrefixToSecrets(knownSecrets), ...secretEnvVariables }
|
||||
const nonNullSecretEnvVariables = utils.records.filterValues(secretEnvVariables, utils.guards.is.notNull)
|
||||
|
||||
if (!this.argv.noSecretCaching) {
|
||||
await this._writeKnownSecretsToCache(secretEnvVariables)
|
||||
}
|
||||
|
||||
env = { ...env, ...nonNullSecretEnvVariables }
|
||||
}
|
||||
|
||||
const port = this.argv.port ?? defaultPort
|
||||
|
||||
const urlParseResult = utils.url.parse(this.argv.tunnelUrl)
|
||||
if (urlParseResult.status === 'error') {
|
||||
throw new errors.BotpressCLIError(`Invalid tunnel URL: ${urlParseResult.error}`)
|
||||
}
|
||||
|
||||
const cachedTunnelId = await this.projectCache.get('tunnelId')
|
||||
|
||||
let tunnelId: string
|
||||
if (this.argv.tunnelId) {
|
||||
tunnelId = this.argv.tunnelId
|
||||
} else if (cachedTunnelId) {
|
||||
tunnelId = cachedTunnelId
|
||||
} else {
|
||||
tunnelId = uuid.v4()
|
||||
}
|
||||
|
||||
if (cachedTunnelId !== tunnelId) {
|
||||
await this.projectCache.set('tunnelId', tunnelId)
|
||||
}
|
||||
|
||||
const { url: parsedTunnelUrl } = urlParseResult
|
||||
const isSecured = parsedTunnelUrl.protocol === 'https' || parsedTunnelUrl.protocol === 'wss'
|
||||
|
||||
const wsTunnelUrl: string = utils.url.format({ ...parsedTunnelUrl, protocol: isSecured ? 'wss' : 'ws' })
|
||||
const httpTunnelUrl: string = utils.url.format({
|
||||
...parsedTunnelUrl,
|
||||
protocol: isSecured ? 'https' : 'http',
|
||||
path: `/${tunnelId}`,
|
||||
})
|
||||
|
||||
let worker: Worker | undefined = undefined
|
||||
|
||||
const supervisor = new utils.tunnel.TunnelSupervisor(wsTunnelUrl, tunnelId, this.logger)
|
||||
supervisor.events.on('connected', ({ tunnel }) => {
|
||||
// prevents the tunnel from closing due to inactivity
|
||||
const timer = setInterval(() => {
|
||||
if (tunnel.closed) {
|
||||
return handleClose()
|
||||
}
|
||||
tunnel.hello()
|
||||
}, TUNNEL_HELLO_INTERVAL)
|
||||
const handleClose = (): void => clearInterval(timer)
|
||||
tunnel.events.on('close', handleClose)
|
||||
|
||||
tunnel.events.on('request', (req) => {
|
||||
if (!worker) {
|
||||
this.logger.debug('Worker not ready yet, ignoring request')
|
||||
tunnel.send({ requestId: req.id, status: 503, body: 'Worker not ready yet' })
|
||||
return
|
||||
}
|
||||
|
||||
void this._forwardTunnelRequest(`http://localhost:${port}`, req)
|
||||
.then((res) => {
|
||||
tunnel.send(res)
|
||||
})
|
||||
.catch((thrown) => {
|
||||
const err = errors.BotpressCLIError.wrap(
|
||||
thrown,
|
||||
`An error occurred while handling request ${req.method} ${req.path}`
|
||||
)
|
||||
this.logger.error(err.message)
|
||||
this.logger.debug(errors.BotpressCLIError.fullStack(err))
|
||||
tunnel.send({
|
||||
requestId: req.id,
|
||||
status: 500,
|
||||
body: 'Internal error while handling request',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
supervisor.events.on('manuallyClosed', () => {
|
||||
this.logger.debug('Tunnel manually closed')
|
||||
})
|
||||
|
||||
await supervisor.start()
|
||||
|
||||
await this._runBuild(watchEnabled)
|
||||
worker = await this._spawnWorker(env, port)
|
||||
|
||||
try {
|
||||
await this._deploy(api, httpTunnelUrl)
|
||||
} catch (thrown) {
|
||||
if (worker.running) {
|
||||
await worker.kill()
|
||||
}
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'An error occurred while deploying the dev server')
|
||||
}
|
||||
|
||||
try {
|
||||
let watcher: Awaited<ReturnType<typeof utils.filewatcher.FileWatcher.watch>> | undefined
|
||||
if (!watchEnabled) {
|
||||
await this._disposeBuildResources({ stopEsbuild: true })
|
||||
await Promise.race([worker.wait(), supervisor.wait()])
|
||||
} else {
|
||||
watcher = await utils.filewatcher.FileWatcher.watch(
|
||||
this.argv.workDir,
|
||||
async (events) => {
|
||||
if (!worker) {
|
||||
this.logger.debug('Worker not ready yet, ignoring file change event')
|
||||
return
|
||||
}
|
||||
|
||||
const typescriptEvents = events
|
||||
.filter((e) => !e.path.startsWith(this.projectPaths.abs.outDir))
|
||||
.filter((e) => pathlib.extname(e.path) === '.ts')
|
||||
|
||||
const packageJsonEvents = events
|
||||
.filter((e) => !e.path.startsWith(this.projectPaths.abs.outDir))
|
||||
.filter((e) => pathlib.basename(e.path) === 'package.json')
|
||||
|
||||
const distEvents = events.filter((e) => e.path.startsWith(this.projectPaths.abs.distDir))
|
||||
|
||||
if (typescriptEvents.length > 0 || packageJsonEvents.length > 0) {
|
||||
this.logger.log('Changes detected, rebuilding')
|
||||
await this._restart(api, worker, httpTunnelUrl)
|
||||
} else if (distEvents.length > 0) {
|
||||
this.logger.log('Changes detected in output directory, reloading worker')
|
||||
await worker.reload()
|
||||
}
|
||||
},
|
||||
{
|
||||
debounceMs: FILEWATCHER_DEBOUNCE_MS,
|
||||
}
|
||||
)
|
||||
|
||||
await Promise.race([worker.wait(), watcher.wait(), supervisor.wait()])
|
||||
}
|
||||
|
||||
if (worker.running) {
|
||||
await worker.kill()
|
||||
}
|
||||
await watcher?.close()
|
||||
supervisor.close()
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'An error occurred while running the dev server')
|
||||
} finally {
|
||||
if (worker.running) {
|
||||
await worker.kill()
|
||||
}
|
||||
await this._disposeBuildResources()
|
||||
}
|
||||
}
|
||||
|
||||
private _restart = async (api: apiUtils.ApiClient, worker: Worker, tunnelUrl: string) => {
|
||||
try {
|
||||
await this._runBuild()
|
||||
} catch (thrown) {
|
||||
const error = errors.BotpressCLIError.wrap(thrown, 'Build failed')
|
||||
this.logger.error(error.message)
|
||||
this.logger.debug(errors.BotpressCLIError.fullStack(error))
|
||||
return
|
||||
}
|
||||
|
||||
await worker.reload()
|
||||
await this._deploy(api, tunnelUrl)
|
||||
}
|
||||
|
||||
private _deploy = async (api: apiUtils.ApiClient, tunnelUrl: string) => {
|
||||
const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS()
|
||||
|
||||
if (projectType === 'interface') {
|
||||
throw new errors.BotpressCLIError('This feature is not available for interfaces.')
|
||||
}
|
||||
if (projectType === 'integration' && this._initialDef?.type === 'integration') {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
this._checkSecrets(projectDef.definition)
|
||||
if (projectDef.definition.name !== this._initialDef.definition.name) {
|
||||
throw new errors.BotpressCLIError(
|
||||
`Integration name changed from "${this._initialDef.definition.name}" to "${projectDef.definition.name}". Renaming integrations during bp dev is not supported. Please restart bp dev.`
|
||||
)
|
||||
}
|
||||
const integrationDef = new sdk.IntegrationDefinition({
|
||||
...projectDef.definition,
|
||||
name: this._deployedIntegrationName ?? this._initialDef.definition.name,
|
||||
})
|
||||
return await this._deployDevIntegration(api, tunnelUrl, integrationDef)
|
||||
}
|
||||
if (projectType === 'bot') {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
this._checkSecrets(projectDef.definition)
|
||||
return await this._deployDevBot(api, tunnelUrl, projectDef.definition)
|
||||
}
|
||||
throw new errors.UnsupportedProjectType()
|
||||
}
|
||||
|
||||
private async _writeKnownSecretsToCache(secretEnvVariables: Record<string, string | null>) {
|
||||
const knownSecrets: Record<string, string | null> = {}
|
||||
for (const [prefixedSecretName, secretValue] of Object.entries(secretEnvVariables)) {
|
||||
const secretName = stripSecretEnvVariablePrefix(prefixedSecretName)
|
||||
knownSecrets[secretName] = secretValue
|
||||
}
|
||||
|
||||
const nonNullKnownSecrets = utils.records.filterValues(knownSecrets, utils.guards.is.notNull)
|
||||
if (Object.keys(nonNullKnownSecrets).length === 0) {
|
||||
await this.projectCache.rm('secrets')
|
||||
return
|
||||
}
|
||||
await this.projectCache.set('secrets', nonNullKnownSecrets)
|
||||
}
|
||||
|
||||
private async _readKnownSecretsFromCache() {
|
||||
return (await this.projectCache.get('secrets')) ?? {}
|
||||
}
|
||||
|
||||
private _applyPrefixToSecrets(secrets: Record<string, string>): Record<string, string> {
|
||||
const prefixedSecretEntries = Object.entries(secrets).map(([secretName, secretValue]) => [
|
||||
secretEnvVariableName(secretName),
|
||||
secretValue,
|
||||
])
|
||||
return Object.fromEntries(prefixedSecretEntries)
|
||||
}
|
||||
|
||||
private _checkSecrets(projectDef: sdk.IntegrationDefinition | sdk.BotDefinition) {
|
||||
if (this._initialDef?.type !== 'integration' && this._initialDef?.type !== 'bot') {
|
||||
return
|
||||
}
|
||||
const initialSecrets = this._initialDef?.definition.secrets ?? {}
|
||||
const currentSecrets = projectDef.secrets ?? {}
|
||||
const newSecrets = Object.keys(currentSecrets).filter((s) => !initialSecrets[s])
|
||||
if (newSecrets.length > 0) {
|
||||
throw new errors.BotpressCLIError('Secrets were added while the server was running. A restart is required.')
|
||||
}
|
||||
}
|
||||
|
||||
private _spawnWorker = async (env: Record<string, string>, port: number) => {
|
||||
const outfile = this.projectPaths.abs.outFileCJS
|
||||
const importPath = utils.path.toUnix(outfile)
|
||||
const code = `require('${importPath}').default.start(${port})`
|
||||
const worker = await Worker.spawn(
|
||||
{
|
||||
type: 'code',
|
||||
code,
|
||||
env,
|
||||
},
|
||||
this.logger
|
||||
).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not start dev worker on port ${port}`)
|
||||
})
|
||||
|
||||
return worker
|
||||
}
|
||||
|
||||
private _runBuild(watchEnabled = true) {
|
||||
return new BuildCommand(this.api, this.prompt, this.logger, this.argv)
|
||||
.setProjectContext(this.projectContext)
|
||||
.run(watchEnabled ? this._buildContext : undefined)
|
||||
}
|
||||
|
||||
private async _disposeBuildResources({ stopEsbuild = false } = {}) {
|
||||
// Best-effort teardown: this runs from the `finally` of `run()`, so it must never throw —
|
||||
// a failure here would mask the original error being propagated by the dev server.
|
||||
try {
|
||||
await Promise.all([this._buildContext.dispose(), this.projectContext.dispose()])
|
||||
if (stopEsbuild) {
|
||||
await utils.esbuild.stop()
|
||||
}
|
||||
} catch (thrown: unknown) {
|
||||
const err = errors.BotpressCLIError.map(thrown)
|
||||
this.logger.debug(`Failed to dispose build resources: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async _deployDevIntegration(
|
||||
api: apiUtils.ApiClient,
|
||||
externalUrl: string,
|
||||
integrationDef: sdk.IntegrationDefinition
|
||||
): Promise<void> {
|
||||
const devId = await this.projectCache.get('devId')
|
||||
|
||||
let integration: client.Integration | undefined = undefined
|
||||
|
||||
if (devId) {
|
||||
const resp = await api.client.getIntegration({ id: devId }).catch(async (thrown) => {
|
||||
const err = errors.BotpressCLIError.wrap(thrown, `Could not find existing dev integration with id "${devId}"`)
|
||||
this.logger.warn(err.message)
|
||||
this.logger.debug(errors.BotpressCLIError.fullStack(err))
|
||||
return { integration: undefined }
|
||||
})
|
||||
|
||||
if (resp.integration?.dev) {
|
||||
integration = resp.integration
|
||||
} else {
|
||||
await this.projectCache.rm('devId')
|
||||
}
|
||||
}
|
||||
|
||||
const line = this.logger.line()
|
||||
line.started(`Deploying dev integration ${chalk.bold(integrationDef.name)}...`)
|
||||
|
||||
const createIntegrationBody = {
|
||||
...(await this.prepareCreateIntegrationBody(integrationDef)),
|
||||
...(await this.prepareIntegrationDependencies(integrationDef, api)),
|
||||
url: externalUrl,
|
||||
}
|
||||
|
||||
if (integration) {
|
||||
const updateIntegrationBody = apiUtils.prepareUpdateIntegrationBody(
|
||||
{ ...createIntegrationBody, id: integration.id },
|
||||
integration
|
||||
)
|
||||
|
||||
const resp = await api.client.updateIntegration(updateIntegrationBody).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not update dev integration "${integrationDef.name}"`)
|
||||
})
|
||||
integration = resp.integration
|
||||
} else {
|
||||
const resp = await api.client.createIntegration({ ...createIntegrationBody, dev: true }).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not deploy dev integration "${integrationDef.name}"`)
|
||||
})
|
||||
integration = resp.integration
|
||||
}
|
||||
|
||||
line.success(`Dev Integration deployed with id "${integration.id}" at "${externalUrl}"`)
|
||||
line.commit()
|
||||
|
||||
await this.projectCache.set('devId', integration.id)
|
||||
}
|
||||
|
||||
private async _deployDevBot(api: apiUtils.ApiClient, externalUrl: string, botDef: sdk.BotDefinition): Promise<void> {
|
||||
const devId = await this.projectCache.get('devId')
|
||||
|
||||
let bot: client.Bot | undefined = undefined
|
||||
|
||||
if (devId) {
|
||||
const resp = await api.client.getBot({ id: devId }).catch(async (thrown) => {
|
||||
const err = errors.BotpressCLIError.wrap(thrown, `Could not find existing dev bot with id "${devId}"`)
|
||||
this.logger.warn(err.message)
|
||||
this.logger.debug(errors.BotpressCLIError.fullStack(err))
|
||||
return { bot: undefined }
|
||||
})
|
||||
|
||||
if (resp.bot?.dev) {
|
||||
bot = resp.bot
|
||||
} else {
|
||||
await this.projectCache.rm('devId')
|
||||
}
|
||||
}
|
||||
|
||||
if (!bot) {
|
||||
const createLine = this.logger.line()
|
||||
createLine.started('Creating dev bot...')
|
||||
const resp = await api.client
|
||||
.createBot({
|
||||
dev: true,
|
||||
url: externalUrl,
|
||||
})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Could not deploy dev bot')
|
||||
})
|
||||
|
||||
bot = resp.bot
|
||||
createLine.log('Dev Bot created')
|
||||
createLine.commit()
|
||||
await this.projectCache.set('devId', bot.id)
|
||||
}
|
||||
|
||||
const updateBotBody = apiUtils.prepareUpdateBotBody(
|
||||
{
|
||||
...(await apiUtils.prepareCreateBotBody(botDef)),
|
||||
...(await this.prepareBotDependencies(botDef, api)),
|
||||
id: bot.id,
|
||||
url: externalUrl,
|
||||
},
|
||||
bot
|
||||
)
|
||||
|
||||
if (!(await this._didDefinitionChange(updateBotBody))) {
|
||||
this.logger.log('Skipping deployment step. No changes found in bot.definition.ts')
|
||||
return
|
||||
}
|
||||
const updateLine = this.logger.line()
|
||||
updateLine.started('Deploying dev bot...')
|
||||
|
||||
const { bot: updatedBot } = await api.client.updateBot(updateBotBody).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Could not deploy dev bot')
|
||||
})
|
||||
|
||||
this.validateIntegrationRegistration(updatedBot, (failedIntegrations) => {
|
||||
throw new errors.BotpressCLIError(
|
||||
`Some integrations failed to register:\n${Object.entries(failedIntegrations)
|
||||
.map(([key, int]) => `• ${key}: ${int.statusReason}`)
|
||||
.join('\n')}`
|
||||
)
|
||||
})
|
||||
|
||||
updateLine.success(`Dev Bot deployed with id "${updatedBot.id}" at "${externalUrl}"`)
|
||||
updateLine.commit()
|
||||
|
||||
const tablesPublisher = new tables.TablesPublisher({ api, logger: this.logger, prompt: this.prompt })
|
||||
await tablesPublisher.deployTables({ botId: updatedBot.id, botDefinition: botDef })
|
||||
|
||||
await this.displayIntegrationUrls({ api, bot: updatedBot })
|
||||
}
|
||||
|
||||
private async _didDefinitionChange(body: apiUtils.UpdateBotRequestBody | apiUtils.UpdateIntegrationRequestBody) {
|
||||
const didChange = !isEqual(body, this._cacheDevRequestBody)
|
||||
this._cacheDevRequestBody = { ...body }
|
||||
return didChange
|
||||
}
|
||||
|
||||
private _forwardTunnelRequest = async (baseUrl: string, request: TunnelRequest): Promise<TunnelResponse> => {
|
||||
const axiosConfig = {
|
||||
method: request.method,
|
||||
url: this._formatLocalUrl(baseUrl, request),
|
||||
headers: request.headers,
|
||||
data: request.body,
|
||||
responseType: 'text',
|
||||
validateStatus: () => true,
|
||||
} satisfies AxiosRequestConfig
|
||||
|
||||
this.logger.debug(`Forwarding request to ${axiosConfig.url}`)
|
||||
const response = await axios(axiosConfig)
|
||||
this.logger.debug('Sending back response up the tunnel')
|
||||
|
||||
return {
|
||||
requestId: request.id,
|
||||
status: response.status,
|
||||
headers: this._getHeaders(response.headers),
|
||||
body: response.data,
|
||||
}
|
||||
}
|
||||
|
||||
private _formatLocalUrl = (baseUrl: string, req: TunnelRequest): string => {
|
||||
if (req.query) {
|
||||
return `${baseUrl}${req.path}?${req.query}`
|
||||
}
|
||||
return `${baseUrl}${req.path}`
|
||||
}
|
||||
|
||||
private _getHeaders = (res: AxiosResponse['headers']): TunnelResponse['headers'] => {
|
||||
const headers: TunnelResponse['headers'] = {}
|
||||
for (const key in res) {
|
||||
if (typeof res[key] === 'string' || typeof res[key] === 'number') {
|
||||
headers[key] = String(res[key])
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import chalk from 'chalk'
|
||||
import fslib from 'fs'
|
||||
import pathlib from 'path'
|
||||
import * as codegen from '../code-generation'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as errors from '../errors'
|
||||
import * as utils from '../utils'
|
||||
import { ProjectCommand } from './project-command'
|
||||
|
||||
export type GenerateCommandDefinition = typeof commandDefinitions.generate
|
||||
export class GenerateCommand extends ProjectCommand<GenerateCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
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 (projectType === 'integration') {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
return await this._generateIntegration(projectDef.definition)
|
||||
}
|
||||
if (projectType === 'bot') {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
return await this._generateBot(projectDef.definition)
|
||||
}
|
||||
if (projectType === 'plugin') {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
return await this._generatePlugin(projectDef.definition)
|
||||
}
|
||||
throw new errors.UnsupportedProjectType()
|
||||
}
|
||||
|
||||
private async _generateIntegration(integrationDef: sdk.IntegrationDefinition): Promise<void> {
|
||||
this._validateSecrets(integrationDef)
|
||||
|
||||
const line = this.logger.line()
|
||||
|
||||
const { name } = integrationDef
|
||||
line.started(`Generating typings for integration ${chalk.bold(name)}...`)
|
||||
|
||||
const fromWorkDir = this.projectPaths.rel('workDir')
|
||||
|
||||
const generatedFiles = await codegen.generateIntegrationImplementation(integrationDef)
|
||||
|
||||
await this._writeGeneratedFilesToOutFolder(generatedFiles)
|
||||
|
||||
line.success(`Typings available at ${chalk.grey(fromWorkDir.outDir)}`)
|
||||
}
|
||||
|
||||
private async _generateBot(botDefinition: sdk.BotDefinition): Promise<void> {
|
||||
const line = this.logger.line()
|
||||
|
||||
line.started('Generating typings for bot...')
|
||||
|
||||
const fromWorkDir = this.projectPaths.rel('workDir')
|
||||
|
||||
const generatedFiles = await codegen.generateBotImplementation(botDefinition.dereferencePluginEntities())
|
||||
|
||||
await this._writeGeneratedFilesToOutFolder(generatedFiles)
|
||||
|
||||
line.success(`Typings available at ${chalk.grey(fromWorkDir.outDir)}`)
|
||||
}
|
||||
|
||||
private async _generatePlugin(pluginDefinition: sdk.PluginDefinition): Promise<void> {
|
||||
const line = this.logger.line()
|
||||
|
||||
const { name } = pluginDefinition
|
||||
line.started(`Generating typings for plugin ${chalk.bold(name)}...`)
|
||||
|
||||
const fromWorkDir = this.projectPaths.rel('workDir')
|
||||
|
||||
const generatedFiles = await codegen.generatePluginImplementation(pluginDefinition.dereferenceEntities())
|
||||
|
||||
await this._writeGeneratedFilesToOutFolder(generatedFiles)
|
||||
|
||||
line.success(`Typings available at ${chalk.grey(fromWorkDir.outDir)}`)
|
||||
}
|
||||
|
||||
private async _writeGeneratedFilesToOutFolder(files: codegen.File[]) {
|
||||
for (const file of files) {
|
||||
const filePath = utils.path.absoluteFrom(this.projectPaths.abs.outDir, file.path)
|
||||
const dirPath = pathlib.dirname(filePath)
|
||||
await fslib.promises.mkdir(dirPath, { recursive: true })
|
||||
await fslib.promises.writeFile(filePath, file.content)
|
||||
}
|
||||
}
|
||||
|
||||
private _validateSecrets(integrationDef: sdk.IntegrationDefinition): void {
|
||||
const { secrets } = integrationDef
|
||||
if (!secrets) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const secretName in secrets) {
|
||||
if (!utils.casing.is.screamingSnakeCase(secretName)) {
|
||||
throw new errors.BotpressCLIError(`Secret ${secretName} should be in SCREAMING_SNAKE_CASE`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import type { YargsConfig } from '@bpinternal/yargs-extra'
|
||||
import chalk from 'chalk'
|
||||
import * as fs from 'fs'
|
||||
import latestVersion from 'latest-version'
|
||||
import _ from 'lodash'
|
||||
import semver from 'semver'
|
||||
import type { ApiClientFactory } from '../api/client'
|
||||
import * as config from '../config'
|
||||
import * as consts from '../consts'
|
||||
import * as errors from '../errors'
|
||||
import type { CommandArgv, CommandDefinition } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
import { BaseCommand } from './base-command'
|
||||
|
||||
export type GlobalCommandDefinition = CommandDefinition<typeof config.schemas.global>
|
||||
export type GlobalCache = { apiUrl: string; token: string; workspaceId: string; activeProfile: string }
|
||||
|
||||
export type ConfigurableGlobalPaths = {
|
||||
botpressHomeDir: string
|
||||
cliRootDir: utils.path.AbsolutePath
|
||||
profilesPath: string
|
||||
}
|
||||
export type ConstantGlobalPaths = typeof consts.fromHomeDir & typeof consts.fromCliRootDir
|
||||
export type AllGlobalPaths = ConfigurableGlobalPaths & ConstantGlobalPaths
|
||||
|
||||
const profileCredentialSchema = z.object({ apiUrl: z.string(), workspaceId: z.string(), token: z.string() })
|
||||
export type ProfileCredentials = z.infer<typeof profileCredentialSchema>
|
||||
|
||||
class GlobalPaths extends utils.path.PathStore<keyof AllGlobalPaths> {
|
||||
public constructor(argv: CommandArgv<GlobalCommandDefinition>) {
|
||||
const absBotpressHome = utils.path.absoluteFrom(utils.path.cwd(), argv.botpressHome)
|
||||
super({
|
||||
cliRootDir: consts.cliRootDir,
|
||||
botpressHomeDir: absBotpressHome,
|
||||
profilesPath: utils.path.absoluteFrom(absBotpressHome, consts.profileFileName),
|
||||
..._.mapValues(consts.fromHomeDir, (p) => utils.path.absoluteFrom(absBotpressHome, p)),
|
||||
..._.mapValues(consts.fromCliRootDir, (p) => utils.path.absoluteFrom(consts.cliRootDir, p)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class GlobalCommand<C extends GlobalCommandDefinition> extends BaseCommand<C> {
|
||||
protected api: ApiClientFactory
|
||||
protected prompt: utils.prompt.CLIPrompt
|
||||
private _pkgJson: utils.pkgJson.PackageJson | undefined
|
||||
|
||||
public constructor(
|
||||
api: ApiClientFactory,
|
||||
prompt: utils.prompt.CLIPrompt,
|
||||
...args: ConstructorParameters<typeof BaseCommand<C>>
|
||||
) {
|
||||
super(...args)
|
||||
this.api = api
|
||||
this.prompt = prompt
|
||||
}
|
||||
|
||||
protected get globalPaths() {
|
||||
return new GlobalPaths(this.argv)
|
||||
}
|
||||
|
||||
protected get globalCache() {
|
||||
return new utils.cache.FSKeyValueCache<GlobalCache>(this.globalPaths.abs.globalCacheFile)
|
||||
}
|
||||
|
||||
protected override async bootstrap() {
|
||||
const pkgJson = await this.readCLIPkgJson()
|
||||
const versionText = chalk.bold(`v${pkgJson.version}`)
|
||||
this.logger.log(`Botpress CLI ${versionText}`, { prefix: '🤖' })
|
||||
|
||||
await this._notifyUpdateCli()
|
||||
|
||||
const paths = this.globalPaths
|
||||
if (paths.abs.botpressHomeDir !== consts.defaultBotpressHome) {
|
||||
this.logger.log(`Using custom botpress home: ${paths.abs.botpressHomeDir}`, { prefix: '🏠' })
|
||||
}
|
||||
}
|
||||
|
||||
protected override teardown = async () => {
|
||||
this.logger.cleanup()
|
||||
}
|
||||
|
||||
protected async getAuthenticatedClient(credentials: Partial<YargsConfig<typeof config.schemas.credentials>>) {
|
||||
try {
|
||||
const cache = this.globalCache
|
||||
|
||||
let token: string | undefined
|
||||
let workspaceId: string | undefined
|
||||
let apiUrl: string | undefined
|
||||
|
||||
if (this.argv.profile) {
|
||||
if (credentials.token || credentials.workspaceId || credentials.apiUrl) {
|
||||
this.logger.warn(
|
||||
'You are currently using credential command line arguments or environment variables as well as a profile. Your profile has overwritten the variables'
|
||||
)
|
||||
}
|
||||
;({ token, workspaceId, apiUrl } = await this.readProfileFromFS(this.argv.profile))
|
||||
this.logger.log(`Using profile "${this.argv.profile}"`, { prefix: '👤' })
|
||||
} else {
|
||||
token = credentials.token ?? (await cache.get('token'))
|
||||
workspaceId = credentials.workspaceId ?? (await cache.get('workspaceId'))
|
||||
apiUrl = credentials.apiUrl ?? (await cache.get('apiUrl'))
|
||||
}
|
||||
|
||||
if (!(token && workspaceId && apiUrl)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (apiUrl !== consts.defaultBotpressApiUrl) {
|
||||
this.logger.log(`Using custom url ${apiUrl}`, { prefix: '🔗' })
|
||||
}
|
||||
|
||||
return this.api.newClient({ apiUrl, token, workspaceId }, this.logger)
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'failed to create authenticated client')
|
||||
}
|
||||
}
|
||||
|
||||
protected async readProfileFromFS(profile: string): Promise<ProfileCredentials> {
|
||||
const parsedProfiles = await this.readProfilesFromFS()
|
||||
|
||||
const profileData = parsedProfiles[profile]
|
||||
if (!profileData) {
|
||||
throw new errors.BotpressCLIError(
|
||||
`Profile "${profile}" not found in "${this.globalPaths.abs.profilesPath}". Found profiles '${Object.keys(parsedProfiles).join("', '")}'.`
|
||||
)
|
||||
}
|
||||
|
||||
return profileData
|
||||
}
|
||||
|
||||
protected async readProfilesFromFS(): Promise<Record<string, ProfileCredentials>> {
|
||||
if (!fs.existsSync(this.globalPaths.abs.profilesPath)) {
|
||||
throw new errors.BotpressCLIError(`Profile file not found at "${this.globalPaths.abs.profilesPath}"`)
|
||||
}
|
||||
const fileContent = await fs.promises.readFile(this.globalPaths.abs.profilesPath, 'utf-8')
|
||||
const jsonParseResult = utils.json.safeParseJson(fileContent)
|
||||
if (!jsonParseResult.success) {
|
||||
throw new errors.BotpressCLIError(`Error parsing profiles file: ${jsonParseResult.error.message}`)
|
||||
}
|
||||
|
||||
const zodParseResult = z.record(profileCredentialSchema).safeParse(jsonParseResult.data)
|
||||
if (!zodParseResult.success) {
|
||||
throw errors.BotpressCLIError.wrap(zodParseResult.error, 'Error parsing profiles: ')
|
||||
}
|
||||
|
||||
return zodParseResult.data
|
||||
}
|
||||
|
||||
protected async writeProfileToFS(profileName: string, profile: ProfileCredentials): Promise<void> {
|
||||
let profiles: Record<string, ProfileCredentials>
|
||||
if (fs.existsSync(this.globalPaths.abs.profilesPath)) {
|
||||
profiles = await this.readProfilesFromFS()
|
||||
} else {
|
||||
profiles = {}
|
||||
}
|
||||
profiles[profileName] = profile
|
||||
|
||||
await fs.promises.writeFile(
|
||||
this.globalPaths.abs.profilesPath,
|
||||
JSON.stringify({ [consts.defaultProfileName]: profiles.defaultProfileName, ...profiles }, null, 2),
|
||||
'utf-8'
|
||||
)
|
||||
}
|
||||
|
||||
protected async ensureLoginAndCreateClient(credentials: YargsConfig<typeof config.schemas.credentials>) {
|
||||
const client = await this.getAuthenticatedClient(credentials)
|
||||
|
||||
if (client === null) {
|
||||
throw new errors.NotLoggedInError()
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
private readonly _notifyUpdateCli = async (): Promise<void> => {
|
||||
try {
|
||||
this.logger.debug('Checking if cli is up to date')
|
||||
|
||||
const pkgJson = await this.readCLIPkgJson()
|
||||
if (!pkgJson.version) {
|
||||
throw new errors.BotpressCLIError('Could not find version in package.json')
|
||||
}
|
||||
|
||||
const latest = await latestVersion(pkgJson.name)
|
||||
const isOutdated = semver.lt(pkgJson.version, latest)
|
||||
if (isOutdated) {
|
||||
this.logger.box(
|
||||
[
|
||||
`${chalk.bold('Update available')} ${chalk.dim(pkgJson.version)} → ${chalk.green(latest)}`,
|
||||
'',
|
||||
'To update, run:',
|
||||
` for npm ${chalk.cyan(`npm i -g ${pkgJson.name}`)}`,
|
||||
` for yarn ${chalk.cyan(`yarn global add ${pkgJson.name}`)}`,
|
||||
` for pnpm ${chalk.cyan(`pnpm i -g ${pkgJson.name}`)}`,
|
||||
].join('\n')
|
||||
)
|
||||
}
|
||||
} catch (thrown) {
|
||||
const err = errors.BotpressCLIError.map(thrown)
|
||||
this.logger.debug(`Failed to check if cli is up to date: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
protected async readCLIPkgJson(): Promise<utils.pkgJson.PackageJson> {
|
||||
if (this._pkgJson) {
|
||||
return this._pkgJson
|
||||
}
|
||||
const { cliRootDir } = this.globalPaths.abs
|
||||
const pkgJson = await utils.pkgJson.readPackageJson(cliRootDir).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Failed to read CLI package.json file at "${cliRootDir}"`)
|
||||
})
|
||||
|
||||
if (!pkgJson) {
|
||||
throw new errors.BotpressCLIError(`Could not find package.json at "${cliRootDir}"`)
|
||||
}
|
||||
|
||||
this._pkgJson = pkgJson
|
||||
return pkgJson
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { ApiClient } from '../api/client'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import type { ImplementationTree } from '../command-tree'
|
||||
import { Logger } from '../logger'
|
||||
import type { CommandArgv } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
import { AddCommand } from './add-command'
|
||||
import type { BaseCommand } from './base-command'
|
||||
import * as bots from './bot-commands'
|
||||
import { BuildCommand } from './build-command'
|
||||
import { BundleCommand } from './bundle-command'
|
||||
import { ChatCommand } from './chat-command'
|
||||
import { DeployCommand } from './deploy-command'
|
||||
import { DevCommand } from './dev-command'
|
||||
import { GenerateCommand } from './gen-command'
|
||||
import type { GlobalCommand, GlobalCommandDefinition } from './global-command'
|
||||
import { InitCommand } from './init-command'
|
||||
import * as integrations from './integration-commands'
|
||||
import * as interfaces from './interface-commands'
|
||||
import { LintCommand } from './lint-command'
|
||||
import { LoginCommand } from './login-command'
|
||||
import { LogoutCommand } from './logout-command'
|
||||
import * as plugins from './plugin-commands'
|
||||
import * as profiles from './profile-commands'
|
||||
import { ReadCommand } from './read-command'
|
||||
import { RemoveCommand } from './remove-command'
|
||||
import { ServeCommand } from './serve-command'
|
||||
|
||||
type GlobalCtor<C extends GlobalCommandDefinition> = new (
|
||||
...args: ConstructorParameters<typeof GlobalCommand<C>>
|
||||
) => BaseCommand<C>
|
||||
|
||||
const getHandler =
|
||||
<C extends GlobalCommandDefinition>(cls: GlobalCtor<C>) =>
|
||||
async (argv: CommandArgv<C>) => {
|
||||
const logger = new Logger(argv)
|
||||
const prompt = new utils.prompt.CLIPrompt(argv, logger)
|
||||
return new cls(ApiClient, prompt, logger, argv).handler()
|
||||
}
|
||||
|
||||
export default {
|
||||
login: getHandler(LoginCommand),
|
||||
logout: getHandler(LogoutCommand),
|
||||
bots: {
|
||||
subcommands: {
|
||||
create: getHandler(bots.CreateBotCommand),
|
||||
get: getHandler(bots.GetBotCommand),
|
||||
delete: getHandler(bots.DeleteBotCommand),
|
||||
list: getHandler(bots.ListBotsCommand),
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
subcommands: {
|
||||
get: getHandler(integrations.GetIntegrationCommand),
|
||||
list: getHandler(integrations.ListIntegrationsCommand),
|
||||
delete: getHandler(integrations.DeleteIntegrationCommand),
|
||||
},
|
||||
},
|
||||
interfaces: {
|
||||
subcommands: {
|
||||
get: getHandler(interfaces.GetInterfaceCommand),
|
||||
list: getHandler(interfaces.ListInterfacesCommand),
|
||||
delete: getHandler(interfaces.DeleteInterfaceCommand),
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
subcommands: {
|
||||
get: getHandler(plugins.GetPluginCommand),
|
||||
list: getHandler(plugins.ListPluginsCommand),
|
||||
delete: getHandler(plugins.DeletePluginCommand),
|
||||
},
|
||||
},
|
||||
init: getHandler(InitCommand),
|
||||
generate: getHandler(GenerateCommand),
|
||||
bundle: getHandler(BundleCommand),
|
||||
build: getHandler(BuildCommand),
|
||||
read: getHandler(ReadCommand),
|
||||
serve: getHandler(ServeCommand),
|
||||
deploy: getHandler(DeployCommand),
|
||||
add: getHandler(AddCommand),
|
||||
remove: getHandler(RemoveCommand),
|
||||
dev: getHandler(DevCommand),
|
||||
lint: getHandler(LintCommand),
|
||||
chat: getHandler(ChatCommand),
|
||||
profiles: {
|
||||
subcommands: {
|
||||
list: getHandler(profiles.ListProfilesCommand),
|
||||
active: getHandler(profiles.ActiveProfileCommand),
|
||||
use: getHandler(profiles.UseProfileCommand),
|
||||
get: getHandler(profiles.GetProfileCommand),
|
||||
},
|
||||
},
|
||||
} satisfies ImplementationTree<typeof commandDefinitions>
|
||||
@@ -0,0 +1,377 @@
|
||||
import type * as client from '@botpress/client'
|
||||
import chalk from 'chalk'
|
||||
import * as fs from 'fs'
|
||||
import * as pathlib from 'path'
|
||||
import { ApiClient } from '../api'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as errors from '../errors'
|
||||
import { Logger } from '../logger'
|
||||
import { ProjectTemplates } from '../project-templates'
|
||||
import * as utils from '../utils'
|
||||
import { GlobalCommand } from './global-command'
|
||||
|
||||
const projectTypes = ['bot', 'integration', 'plugin'] as const
|
||||
type ProjectType = (typeof projectTypes)[number]
|
||||
|
||||
type CopyProps = { srcDir: string; destDir: string; name: string; pkgJson: Record<string, unknown> }
|
||||
|
||||
export type InitCommandDefinition = typeof commandDefinitions.init
|
||||
export class InitCommand extends GlobalCommand<InitCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const projectType = await this._promptProjectType()
|
||||
const workDir = utils.path.absoluteFrom(utils.path.cwd(), this.argv.workDir)
|
||||
|
||||
try {
|
||||
if (projectType === 'bot') {
|
||||
await this._initBot({ workDir })
|
||||
return
|
||||
}
|
||||
|
||||
if (projectType === 'integration') {
|
||||
const workspaceHandle = await this._promptWorkspaceHandle()
|
||||
await this._initIntegration({ workDir, workspaceHandle })
|
||||
return
|
||||
}
|
||||
|
||||
if (projectType === 'plugin') {
|
||||
const workspaceHandle = await this._promptWorkspaceHandle()
|
||||
await this._initPlugin({ workDir, workspaceHandle })
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof errors.AbortedOperationError) {
|
||||
this.logger.log(error.message)
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
type _assertion = utils.types.AssertNever<typeof projectType>
|
||||
throw new errors.BotpressCLIError(`Unknown project type: ${projectType}`)
|
||||
}
|
||||
|
||||
private async _promptWorkspaceHandle() {
|
||||
const client = (await this.getAuthenticatedClient({})) ?? undefined
|
||||
|
||||
const nameParts = this.argv.name?.split('/', 2) ?? []
|
||||
const workspaceHandle = nameParts.length > 1 ? nameParts[0] : undefined
|
||||
|
||||
const resolver = await WorkspaceResolver.from({
|
||||
client,
|
||||
workspaceHandle,
|
||||
prompt: this.prompt,
|
||||
logger: this.logger,
|
||||
})
|
||||
|
||||
return await resolver.getWorkspaceHandle()
|
||||
}
|
||||
|
||||
private async _promptProjectType() {
|
||||
if (this.argv.type) {
|
||||
return this.argv.type
|
||||
}
|
||||
|
||||
const promptedType = await this.prompt.select('What type of project do you wish to initialize?', {
|
||||
choices: projectTypes.map((t) => ({ title: t, value: t })),
|
||||
})
|
||||
|
||||
if (!promptedType) {
|
||||
throw new errors.ParamRequiredError('Project Type')
|
||||
}
|
||||
|
||||
return promptedType
|
||||
}
|
||||
|
||||
private _initPlugin = async (args: { workDir: string; workspaceHandle: string }) => {
|
||||
const { workDir, workspaceHandle } = args
|
||||
const template = await this._getOrPromptForTemplate('plugin')
|
||||
const name = await this._getName('plugin', template.defaultProjectName)
|
||||
const { fullName, shortName } = this._getFullNameAndShortName({ workspaceHandle, name })
|
||||
|
||||
await this._copy({
|
||||
srcDir: template.absolutePath,
|
||||
destDir: workDir,
|
||||
name: shortName,
|
||||
pkgJson: {
|
||||
pluginName: fullName,
|
||||
},
|
||||
})
|
||||
this.logger.success(`Plugin project initialized in ${chalk.bold(pathlib.join(workDir, shortName))}`)
|
||||
}
|
||||
|
||||
private async _getOrPromptForTemplate(type: ProjectType): Promise<ProjectTemplates.Template> {
|
||||
const availableTemplates = ProjectTemplates.templates[type]
|
||||
|
||||
if (this.argv.template) {
|
||||
const template = availableTemplates.find((t) => t.identifier === this.argv.template)
|
||||
if (!template) {
|
||||
throw new errors.BotpressCLIError(`No ${type} template found for identifier "${this.argv.template}"`)
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
if (availableTemplates.length === 1) {
|
||||
this.logger.log(`Using default template: ${chalk.bold(availableTemplates[0].fullName)}`)
|
||||
return availableTemplates[0]
|
||||
}
|
||||
|
||||
return await this._promptForTemplate(availableTemplates)
|
||||
}
|
||||
|
||||
private async _promptForTemplate(
|
||||
availableTemplates: ProjectTemplates.TemplateArray
|
||||
): Promise<ProjectTemplates.Template> {
|
||||
const templateIndex = await this.prompt.select<number>('Which template do you want to use?', {
|
||||
choices: availableTemplates.map((template, index) => ({
|
||||
title: template.fullName,
|
||||
value: index,
|
||||
})),
|
||||
default: 0,
|
||||
})
|
||||
|
||||
if (templateIndex === undefined || templateIndex < 0 || templateIndex >= availableTemplates.length) {
|
||||
this.logger.log(`Using default template: ${chalk.bold(availableTemplates[0].fullName)}`)
|
||||
return availableTemplates[0]
|
||||
}
|
||||
|
||||
return availableTemplates[templateIndex]!
|
||||
}
|
||||
|
||||
private _getFullNameAndShortName(args: { workspaceHandle: string; name: string }) {
|
||||
const [workspaceOrName, projectName] = args.name.split('/', 2)
|
||||
const shortName = projectName ?? workspaceOrName!
|
||||
const fullName = `${args.workspaceHandle}/${shortName}`
|
||||
|
||||
return { shortName, fullName }
|
||||
}
|
||||
|
||||
private _initBot = async (args: { workDir: string }) => {
|
||||
const { workDir } = args
|
||||
const template = await this._getOrPromptForTemplate('bot')
|
||||
const name = await this._getName('bot', template.defaultProjectName)
|
||||
|
||||
await this._copy({ srcDir: template.absolutePath, destDir: workDir, name, pkgJson: {} })
|
||||
this.logger.success(`Bot project initialized in ${chalk.bold(pathlib.join(workDir, name))}`)
|
||||
}
|
||||
|
||||
private _initIntegration = async (args: { workDir: string; workspaceHandle: string }) => {
|
||||
const { workDir, workspaceHandle } = args
|
||||
const template = await this._getOrPromptForTemplate('integration')
|
||||
const name = await this._getName('integration', template.defaultProjectName)
|
||||
const { fullName, shortName } = this._getFullNameAndShortName({ workspaceHandle, name })
|
||||
|
||||
await this._copy({
|
||||
srcDir: template.absolutePath,
|
||||
destDir: workDir,
|
||||
name: shortName,
|
||||
pkgJson: {
|
||||
integrationName: fullName,
|
||||
},
|
||||
})
|
||||
this.logger.success(`Integration project initialized in ${chalk.bold(pathlib.join(workDir, shortName))}`)
|
||||
return
|
||||
}
|
||||
|
||||
private _getName = async (projectType: ProjectType, defaultName: string): Promise<string> => {
|
||||
if (this.argv.name) {
|
||||
return this.argv.name
|
||||
}
|
||||
const promptMessage = `What is the name of your ${projectType}?`
|
||||
const promptedName = await this.prompt.text(promptMessage, { initial: defaultName })
|
||||
if (!promptedName) {
|
||||
throw new errors.ParamRequiredError('Project Name')
|
||||
}
|
||||
return promptedName
|
||||
}
|
||||
|
||||
private _copy = async (props: CopyProps) => {
|
||||
const { srcDir, destDir, name, pkgJson } = props
|
||||
|
||||
const dirName = utils.casing.to.kebabCase(name)
|
||||
const destination = pathlib.join(destDir, dirName)
|
||||
|
||||
const destinationCanBeUsed = await this._checkIfDestinationCanBeUsed(destination)
|
||||
if (!destinationCanBeUsed) {
|
||||
throw new errors.AbortedOperationError()
|
||||
}
|
||||
await fs.promises.rm(destination, { recursive: true, force: true })
|
||||
|
||||
await fs.promises.cp(srcDir, destination, { recursive: true })
|
||||
|
||||
const json = await utils.pkgJson.readPackageJson(destination).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Failed to read package.json file')
|
||||
})
|
||||
|
||||
const pkgJsonName = utils.casing.to.snakeCase(name)
|
||||
const updatedJson = { name: pkgJsonName, ...json, ...pkgJson }
|
||||
await utils.pkgJson.writePackageJson(destination, updatedJson).catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Failed to write package.json file')
|
||||
})
|
||||
}
|
||||
|
||||
private _checkIfDestinationCanBeUsed = async (destination: string) => {
|
||||
if (fs.existsSync(destination)) {
|
||||
const override = await this.prompt.confirm(
|
||||
`Directory ${chalk.bold(destination)} already exists. Do you want to overwrite it?`
|
||||
)
|
||||
if (!override) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class WorkspaceResolver {
|
||||
private _workspaces: client.Workspace[] = []
|
||||
private _currentWorkspace?: client.Workspace
|
||||
|
||||
private constructor(
|
||||
private readonly _client: ApiClient | undefined,
|
||||
private readonly _workspaceHandle: string | undefined,
|
||||
private readonly _prompt: utils.prompt.CLIPrompt,
|
||||
private readonly _logger: Logger
|
||||
) {}
|
||||
|
||||
public static async from({
|
||||
client,
|
||||
workspaceHandle,
|
||||
prompt,
|
||||
logger,
|
||||
}: {
|
||||
client?: ApiClient
|
||||
workspaceHandle?: string
|
||||
prompt: utils.prompt.CLIPrompt
|
||||
logger: Logger
|
||||
}): Promise<WorkspaceResolver> {
|
||||
const resolver = new WorkspaceResolver(client, workspaceHandle, prompt, logger)
|
||||
await resolver._fetchWorkspaces()
|
||||
return resolver
|
||||
}
|
||||
|
||||
public async getWorkspaceHandle(): Promise<string> {
|
||||
if (this._hasNoWorkspaces()) {
|
||||
return this._promptForArbitraryWorkspaceHandle()
|
||||
}
|
||||
|
||||
const workspace = await this._promptUserToSelectWorkspace()
|
||||
return workspace.handle || (await this._assignHandleToWorkspace(workspace))
|
||||
}
|
||||
|
||||
private async _fetchWorkspaces(): Promise<void> {
|
||||
if (!this._isAuthenticated()) {
|
||||
return
|
||||
}
|
||||
|
||||
const workspaces = await this._getClient()
|
||||
.client.list.workspaces({})
|
||||
.collect({})
|
||||
.catch((thrown) => {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Unable to list your workspaces')
|
||||
})
|
||||
const currentWorkspace = workspaces.find((ws) => ws.id === this._getClient().workspaceId)
|
||||
|
||||
this._workspaces = workspaces
|
||||
this._currentWorkspace = currentWorkspace
|
||||
}
|
||||
|
||||
private _isAuthenticated(): boolean {
|
||||
return !!this._client
|
||||
}
|
||||
|
||||
private _hasNoWorkspaces(): boolean {
|
||||
return !this._isAuthenticated() || this._workspaces.length === 0 || !this._currentWorkspace
|
||||
}
|
||||
|
||||
private async _promptForArbitraryWorkspaceHandle(): Promise<string> {
|
||||
if (this._workspaceHandle) {
|
||||
return this._workspaceHandle
|
||||
}
|
||||
|
||||
const handle = await this._prompt.text('Enter your workspace handle')
|
||||
|
||||
if (!handle) {
|
||||
throw new errors.ParamRequiredError('Workspace handle')
|
||||
}
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
private async _promptUserToSelectWorkspace(): Promise<client.Workspace> {
|
||||
const workspaceChoices = this._workspaces.map((ws) => ({
|
||||
title: ws.name,
|
||||
value: ws.id,
|
||||
}))
|
||||
|
||||
const initialChoice = {
|
||||
title: this._currentWorkspace!.name,
|
||||
value: this._currentWorkspace!.id,
|
||||
}
|
||||
|
||||
const workspaceId = await this._prompt.select('Which workspace do you want to use?', {
|
||||
initial: initialChoice,
|
||||
choices: workspaceChoices,
|
||||
})
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new errors.ParamRequiredError('Workspace')
|
||||
}
|
||||
|
||||
return this._workspaces.find((ws) => ws.id === workspaceId)!
|
||||
}
|
||||
|
||||
private async _assignHandleToWorkspace(workspace: client.Workspace): Promise<string> {
|
||||
this._logger.warn("It seems you don't have a workspace handle yet.")
|
||||
|
||||
let claimedHandle: string | undefined
|
||||
|
||||
do {
|
||||
const desiredHandle = await this._promptForDesiredHandle()
|
||||
const isAvailable = await this._checkHandleAvailability(desiredHandle)
|
||||
|
||||
if (isAvailable) {
|
||||
claimedHandle = desiredHandle
|
||||
await this._updateWorkspaceWithHandle(workspace.id, claimedHandle)
|
||||
}
|
||||
} while (!claimedHandle)
|
||||
|
||||
this._logger.success(`Handle "${claimedHandle}" is yours!`)
|
||||
return claimedHandle
|
||||
}
|
||||
|
||||
private async _promptForDesiredHandle(): Promise<string> {
|
||||
const desiredHandle = await this._prompt.text('Please enter a workspace handle')
|
||||
|
||||
if (!desiredHandle) {
|
||||
throw new errors.BotpressCLIError('Workspace handle is required')
|
||||
}
|
||||
|
||||
return desiredHandle
|
||||
}
|
||||
|
||||
private async _checkHandleAvailability(handle: string): Promise<boolean> {
|
||||
const { available, suggestions } = await this._getClient().client.checkHandleAvailability({ handle })
|
||||
|
||||
if (!available) {
|
||||
this._logger.warn(`Handle "${handle}" is not available. Suggestions: ${suggestions.join(', ')}`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private async _updateWorkspaceWithHandle(workspaceId: string, handle: string): Promise<void> {
|
||||
try {
|
||||
await this._getClient().switchWorkspace(workspaceId).updateWorkspace({ handle })
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not claim handle "${handle}"`)
|
||||
}
|
||||
}
|
||||
|
||||
private _getClient(): ApiClient {
|
||||
if (!this._client) {
|
||||
throw new errors.BotpressCLIError('Could not authenticate')
|
||||
}
|
||||
return this._client
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import chalk from 'chalk'
|
||||
import _ from 'lodash'
|
||||
import { ApiClient, PublicOrPrivateIntegration, IntegrationSummary } from '../api/client'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as errors from '../errors'
|
||||
import { NamePackageRef, parsePackageRef } from '../package-ref'
|
||||
import { GlobalCommand } from './global-command'
|
||||
|
||||
export type GetIntegrationCommandDefinition = typeof commandDefinitions.integrations.subcommands.get
|
||||
export class GetIntegrationCommand extends GlobalCommand<GetIntegrationCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
const parsedRef = parsePackageRef(this.argv.integrationRef)
|
||||
if (!parsedRef) {
|
||||
throw new errors.InvalidPackageReferenceError(this.argv.integrationRef)
|
||||
}
|
||||
if (parsedRef.type === 'path') {
|
||||
throw new errors.BotpressCLIError('Cannot get local integration')
|
||||
}
|
||||
|
||||
try {
|
||||
const integration = await api.findPublicOrPrivateIntegration(parsedRef)
|
||||
if (integration) {
|
||||
this.logger.success(`Integration ${chalk.bold(this.argv.integrationRef)}:`)
|
||||
this.logger.json(integration)
|
||||
return
|
||||
}
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not get integration ${this.argv.integrationRef}`)
|
||||
}
|
||||
|
||||
throw new errors.BotpressCLIError(`Integration ${this.argv.integrationRef} not found`)
|
||||
}
|
||||
}
|
||||
|
||||
export type ListIntegrationsCommandDefinition = typeof commandDefinitions.integrations.subcommands.list
|
||||
export class ListIntegrationsCommand extends GlobalCommand<ListIntegrationsCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
|
||||
const { dev, public: isPublic, owned } = this.argv
|
||||
|
||||
if (dev && isPublic) {
|
||||
throw new errors.BotpressCLIError(
|
||||
'Cannot use --dev and --public flags together as dev integrations are always private'
|
||||
)
|
||||
}
|
||||
if (dev && owned) {
|
||||
throw new errors.BotpressCLIError(
|
||||
'Cannot use --dev and --owned flags together as dev integrations are always owned by the current workspace'
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const integrations = await this._listAllIntegrations(api)
|
||||
this.logger.success('Integrations:')
|
||||
this.logger.json(integrations)
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Could not list integrations')
|
||||
}
|
||||
}
|
||||
|
||||
private _listAllIntegrations = async (api: ApiClient): Promise<IntegrationSummary[]> => {
|
||||
if (this.argv.dev) {
|
||||
return this._listDevIntegrations(api)
|
||||
}
|
||||
|
||||
if (this.argv.public && this.argv.owned) {
|
||||
const [owned, publicIntegrations] = await Promise.all([
|
||||
this._listOwnedIntegrations(api),
|
||||
this._listPublicIntegrations(api),
|
||||
])
|
||||
return _.intersectionBy(owned, publicIntegrations, (i) => i.id).slice(0, this.argv.limit)
|
||||
}
|
||||
|
||||
if (this.argv.owned) {
|
||||
return this._listOwnedIntegrations(api)
|
||||
}
|
||||
|
||||
if (this.argv.public) {
|
||||
return this._listPublicIntegrations(api)
|
||||
}
|
||||
|
||||
const [owned, publicIntegrations] = await Promise.all([
|
||||
this._listOwnedIntegrations(api),
|
||||
this._listPublicIntegrations(api),
|
||||
])
|
||||
return _.uniqBy([...owned, ...publicIntegrations], (i) => i.id).slice(0, this.argv.limit)
|
||||
}
|
||||
|
||||
private _listDevIntegrations = async (api: ApiClient): Promise<IntegrationSummary[]> => {
|
||||
const { name, versionNumber: version } = this.argv
|
||||
return api.client.list.integrations({ dev: true, name, version }).collect({ limit: this.argv.limit })
|
||||
}
|
||||
|
||||
private _listOwnedIntegrations = async (api: ApiClient): Promise<IntegrationSummary[]> => {
|
||||
const { name, versionNumber: version } = this.argv
|
||||
return api.client.list.integrations({ name, version }).collect({ limit: this.argv.limit })
|
||||
}
|
||||
|
||||
private _listPublicIntegrations = async (api: ApiClient): Promise<IntegrationSummary[]> => {
|
||||
const { name, versionNumber: version } = this.argv
|
||||
return api.client.list.publicIntegrations({ name, version }).collect({ limit: this.argv.limit })
|
||||
}
|
||||
}
|
||||
|
||||
export type DeleteIntegrationCommandDefinition = typeof commandDefinitions.integrations.subcommands.delete
|
||||
export class DeleteIntegrationCommand extends GlobalCommand<DeleteIntegrationCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
const parsedRef = parsePackageRef(this.argv.integrationRef)
|
||||
if (!parsedRef) {
|
||||
throw new errors.InvalidPackageReferenceError(this.argv.integrationRef)
|
||||
}
|
||||
if (parsedRef.type === 'path') {
|
||||
throw new errors.BotpressCLIError('Cannot delete local integration')
|
||||
}
|
||||
|
||||
let integrationId: string | undefined
|
||||
if (parsedRef.type === 'id') {
|
||||
integrationId = parsedRef.id
|
||||
} else {
|
||||
const integration = await this._findIntegration(api, parsedRef)
|
||||
integrationId = integration.id
|
||||
}
|
||||
|
||||
try {
|
||||
await api.client.deleteIntegration({ id: integrationId })
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not delete integration ${this.argv.integrationRef}`)
|
||||
}
|
||||
|
||||
this.logger.success(`Integration ${chalk.bold(this.argv.integrationRef)} deleted`)
|
||||
return
|
||||
}
|
||||
|
||||
private _findIntegration = async (api: ApiClient, parsedRef: NamePackageRef) => {
|
||||
let integration: PublicOrPrivateIntegration | undefined
|
||||
|
||||
try {
|
||||
integration = await api.findPrivateIntegration(parsedRef)
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not get integration ${this.argv.integrationRef}`)
|
||||
}
|
||||
|
||||
if (!integration) {
|
||||
const publicIntegration = await api.findPublicIntegration(parsedRef)
|
||||
if (publicIntegration) {
|
||||
throw new errors.BotpressCLIError(`Integration ${this.argv.integrationRef} does not belong to your workspace`)
|
||||
}
|
||||
|
||||
throw new errors.BotpressCLIError(`Integration ${this.argv.integrationRef} not found`)
|
||||
}
|
||||
|
||||
return integration
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type * as client from '@botpress/client'
|
||||
import chalk from 'chalk'
|
||||
import _ from 'lodash'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as errors from '../errors'
|
||||
import { parsePackageRef } from '../package-ref'
|
||||
import { GlobalCommand } from './global-command'
|
||||
|
||||
export type GetInterfaceCommandDefinition = typeof commandDefinitions.interfaces.subcommands.get
|
||||
export class GetInterfaceCommand extends GlobalCommand<GetInterfaceCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
const parsedRef = parsePackageRef(this.argv.interfaceRef)
|
||||
if (!parsedRef) {
|
||||
throw new errors.InvalidPackageReferenceError(this.argv.interfaceRef)
|
||||
}
|
||||
if (parsedRef.type === 'path') {
|
||||
throw new errors.BotpressCLIError('Cannot get local interface')
|
||||
}
|
||||
|
||||
try {
|
||||
const intrface = await api.findPublicOrPrivateInterface(parsedRef)
|
||||
if (intrface) {
|
||||
this.logger.success(`Interface ${chalk.bold(this.argv.interfaceRef)}:`)
|
||||
this.logger.json(intrface)
|
||||
return
|
||||
}
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not get interface ${this.argv.interfaceRef}`)
|
||||
}
|
||||
|
||||
throw new errors.BotpressCLIError(`Interface ${this.argv.interfaceRef} not found`)
|
||||
}
|
||||
}
|
||||
|
||||
export type ListInterfacesCommandDefinition = typeof commandDefinitions.interfaces.subcommands.list
|
||||
export class ListInterfacesCommand extends GlobalCommand<ListInterfacesCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
|
||||
const privateLister = (req: { nextToken?: string }) => api.client.listInterfaces({ nextToken: req.nextToken })
|
||||
const publicLister = (req: { nextToken?: string }) => api.client.listPublicInterfaces({ nextToken: req.nextToken })
|
||||
|
||||
try {
|
||||
const [privateInterfaces, publicInterfaces] = await Promise.all([
|
||||
api.listAllPages(privateLister, (r) => r.interfaces),
|
||||
api.listAllPages(publicLister, (r) => r.interfaces),
|
||||
])
|
||||
const interfaces = _.uniqBy([...privateInterfaces, ...publicInterfaces], (i) => i.id)
|
||||
|
||||
this.logger.success('Interfaces:')
|
||||
this.logger.json(interfaces)
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, 'Could not list interfaces')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type DeleteInterfaceCommandDefinition = typeof commandDefinitions.interfaces.subcommands.delete
|
||||
export class DeleteInterfaceCommand extends GlobalCommand<DeleteInterfaceCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
const api = await this.ensureLoginAndCreateClient(this.argv)
|
||||
const parsedRef = parsePackageRef(this.argv.interfaceRef)
|
||||
if (!parsedRef) {
|
||||
throw new errors.InvalidPackageReferenceError(this.argv.interfaceRef)
|
||||
}
|
||||
if (parsedRef.type === 'path') {
|
||||
throw new errors.BotpressCLIError('Cannot delete local interface')
|
||||
}
|
||||
|
||||
let intrface: client.Interface | undefined
|
||||
try {
|
||||
intrface = await api.findPublicOrPrivateInterface(parsedRef)
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not get interface ${this.argv.interfaceRef}`)
|
||||
}
|
||||
|
||||
if (!intrface) {
|
||||
throw new errors.BotpressCLIError(`Interface ${this.argv.interfaceRef} not found`)
|
||||
}
|
||||
|
||||
try {
|
||||
await api.client.deleteInterface({ id: intrface.id })
|
||||
} catch (thrown) {
|
||||
throw errors.BotpressCLIError.wrap(thrown, `Could not delete interface ${this.argv.interfaceRef}`)
|
||||
}
|
||||
|
||||
this.logger.success(`Interface ${chalk.bold(this.argv.interfaceRef)} deleted`)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
type IntegrationDefinition,
|
||||
type BotDefinition,
|
||||
type InterfaceDefinition,
|
||||
ActionDefinition,
|
||||
z,
|
||||
EventDefinition,
|
||||
} from '@botpress/sdk'
|
||||
import * as apiUtils from '../api'
|
||||
import type commandDefinitions from '../command-definitions'
|
||||
import * as errors from '../errors'
|
||||
import { BaseLinter } from '../linter/base-linter'
|
||||
import { BotLinter } from '../linter/bot-linter'
|
||||
import { IntegrationLinter } from '../linter/integration-linter'
|
||||
import { InterfaceLinter } from '../linter/interface-linter'
|
||||
import { ProjectCommand } from './project-command'
|
||||
|
||||
const _getIssuesDetectedMessage = (linter: BaseLinter<unknown>, nonEmptyPrefix: string = '') => {
|
||||
const message = linter
|
||||
.getIssuesCountBySeverity()
|
||||
.map(({ name, count }) => `${count} ${name}(s)`)
|
||||
.join(', ')
|
||||
|
||||
return message.length > 0 ? `${nonEmptyPrefix}${message}` : ''
|
||||
}
|
||||
|
||||
export type LintCommandDefinition = typeof commandDefinitions.lint
|
||||
export class LintCommand extends ProjectCommand<LintCommandDefinition> {
|
||||
public async run(): Promise<void> {
|
||||
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'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
switch (projectType) {
|
||||
case 'integration': {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
return this._runLintForIntegration(projectDef.definition)
|
||||
}
|
||||
case 'bot': {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
return this._runLintForBot(projectDef.definition)
|
||||
}
|
||||
case 'interface': {
|
||||
const projectDef = await resolveProjectDefinition()
|
||||
return this._runLintForInterface(projectDef.definition)
|
||||
}
|
||||
default:
|
||||
throw new errors.UnsupportedProjectType()
|
||||
}
|
||||
}
|
||||
|
||||
private async _runLintForInterface(definition: InterfaceDefinition): Promise<void> {
|
||||
for (const [actionName, actionDef] of Object.entries(definition.actions)) {
|
||||
definition.actions[actionName] = this._dereferenceActionDefinition(actionDef)
|
||||
}
|
||||
|
||||
for (const [eventName, eventDef] of Object.entries(definition.events)) {
|
||||
definition.events[eventName] = this._dereferenceEventDefinition(eventDef)
|
||||
}
|
||||
|
||||
const parsedInterfaceDefinition = await apiUtils.prepareCreateInterfaceBody(definition)
|
||||
const linter = new InterfaceLinter(parsedInterfaceDefinition, this.logger)
|
||||
|
||||
await linter.lint()
|
||||
linter.logResults(this.logger)
|
||||
|
||||
const issueCountsSuffix = _getIssuesDetectedMessage(linter, ' - ')
|
||||
|
||||
if (linter.hasErrors()) {
|
||||
throw new errors.BotpressCLIError(`Interface definition contains linting errors${issueCountsSuffix}`)
|
||||
}
|
||||
|
||||
this.logger.success(`Interface definition is valid${issueCountsSuffix}`)
|
||||
}
|
||||
|
||||
private async _runLintForBot(definition: BotDefinition): Promise<void> {
|
||||
const strippedDefinition = this._stripAutoGeneratedContentFromBot(definition)
|
||||
const parsedBotDefinition = await apiUtils.prepareCreateBotBody(strippedDefinition)
|
||||
const linter = new BotLinter(parsedBotDefinition, this.logger)
|
||||
|
||||
await linter.lint()
|
||||
linter.logResults(this.logger)
|
||||
|
||||
const issueCountsSuffix = _getIssuesDetectedMessage(linter, ' - ')
|
||||
|
||||
if (linter.hasErrors()) {
|
||||
throw new errors.BotpressCLIError(`Bot definition contains linting errors${issueCountsSuffix}`)
|
||||
}
|
||||
|
||||
this.logger.success(`Bot definition is valid${issueCountsSuffix}`)
|
||||
}
|
||||
|
||||
private _stripAutoGeneratedContentFromBot(definition: BotDefinition) {
|
||||
return {
|
||||
...definition,
|
||||
integrations: {},
|
||||
} as BotDefinition
|
||||
}
|
||||
|
||||
private async _runLintForIntegration(definition: IntegrationDefinition): Promise<void> {
|
||||
const strippedDefinition = this._stripAutoGeneratedContentFromIntegration(definition)
|
||||
const parsedIntegrationDefinition = await this.prepareCreateIntegrationBody(strippedDefinition)
|
||||
const linter = new IntegrationLinter(
|
||||
{ ...parsedIntegrationDefinition, secrets: strippedDefinition.secrets },
|
||||
this.logger
|
||||
)
|
||||
|
||||
await linter.lint()
|
||||
linter.logResults(this.logger)
|
||||
|
||||
const issueCountsSuffix = _getIssuesDetectedMessage(linter, ' - ')
|
||||
|
||||
if (linter.hasErrors()) {
|
||||
throw new errors.BotpressCLIError(`Integration definition contains linting errors${issueCountsSuffix}`)
|
||||
}
|
||||
|
||||
this.logger.success(`Integration definition is valid${issueCountsSuffix}`)
|
||||
}
|
||||
|
||||
private _stripAutoGeneratedContentFromIntegration(definition: IntegrationDefinition) {
|
||||
const { actionNames, eventNames } = this._getAutoGeneratedContentOfIntegration(definition)
|
||||
|
||||
return {
|
||||
...definition,
|
||||
actions: Object.fromEntries(Object.entries(definition.actions ?? {}).filter(([key]) => !actionNames.has(key))),
|
||||
events: Object.fromEntries(Object.entries(definition.events ?? {}).filter(([key]) => !eventNames.has(key))),
|
||||
} as IntegrationDefinition
|
||||
}
|
||||
|
||||
private _getAutoGeneratedContentOfIntegration(definition: IntegrationDefinition) {
|
||||
const actionNames = new Set<string>()
|
||||
const eventNames = new Set<string>()
|
||||
|
||||
const interfacesStatements = definition.interfaces ?? {}
|
||||
for (const iface of Object.values(interfacesStatements)) {
|
||||
for (const actionDefinition of Object.values(iface.actions)) {
|
||||
actionNames.add(actionDefinition.name)
|
||||
}
|
||||
for (const eventDefinition of Object.values(iface.events)) {
|
||||
eventNames.add(eventDefinition.name)
|
||||
}
|
||||
}
|
||||
|
||||
return { actionNames, eventNames } as const
|
||||
}
|
||||
|
||||
private _dereferenceActionDefinition = (actionDef: ActionDefinition): ActionDefinition => {
|
||||
const inputRefs = actionDef.input.schema.getReferences()
|
||||
const outputRefs = actionDef.output.schema.getReferences()
|
||||
|
||||
const inputRefSchemas = Object.fromEntries(inputRefs.map((ref) => [ref, this._replaceRef(ref)]))
|
||||
const outputRefSchemas = Object.fromEntries(outputRefs.map((ref) => [ref, this._replaceRef(ref)]))
|
||||
|
||||
actionDef.input.schema = actionDef.input.schema.dereference(inputRefSchemas) as z.ZodObject
|
||||
actionDef.output.schema = actionDef.output.schema.dereference(outputRefSchemas) as z.ZodObject
|
||||
|
||||
return actionDef
|
||||
}
|
||||
|
||||
private _dereferenceEventDefinition = (eventDef: EventDefinition): EventDefinition => {
|
||||
const refs = eventDef.schema.getReferences()
|
||||
|
||||
const refSchemas = Object.fromEntries(refs.map((ref) => [ref, this._replaceRef(ref)]))
|
||||
|
||||
eventDef.schema = eventDef.schema.dereference(refSchemas) as z.ZodObject
|
||||
|
||||
return eventDef
|
||||
}
|
||||
|
||||
private _replaceRef = (refUri: string): z.ZodObject => z.object({}).title(refUri).describe(refUri)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user