Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@botpress/cli",
"version": "6.6.2",
"version": "6.7.0",
"description": "Botpress CLI",
"scripts": {
"build": "pnpm run build:types && pnpm run bundle && pnpm run template:gen",
Expand Down
128 changes: 1 addition & 127 deletions packages/cli/src/command-implementations/deploy-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class DeployCommand extends ProjectCommand<DeployCommandDefinition> {
}

private async _deployIntegration(api: apiUtils.ApiClient, integrationDef: sdk.IntegrationDefinition) {
const res = await this._manageWorkspaceHandle(api, integrationDef)
const res = await this.manageWorkspaceHandle(api, integrationDef)
if (!res) return
const { integration: updatedIntegrationDef, workspaceId } = res
integrationDef = updatedIntegrationDef
Expand Down Expand Up @@ -518,130 +518,4 @@ export class DeployCommand extends ProjectCommand<DeployCommandDefinition> {

return fetchedBot
}

private async _manageWorkspaceHandle(
api: apiUtils.ApiClient,
integration: sdk.IntegrationDefinition
): Promise<
| {
integration: sdk.IntegrationDefinition
workspaceId?: string // Set if user opted to deploy on another available workspace
}
| undefined
> {
const { name: localName, workspaceHandle: localHandle } = this._parseIntegrationName(integration.name)
if (!localHandle && api.isBotpressWorkspace) {
this.logger.debug('Botpress workspace detected; workspace handle omitted')
return { integration } // botpress has the right to omit workspace handle
}

const { handle: remoteHandle, name: workspaceName } = await api.getWorkspace().catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, 'Could not fetch workspace')
})

if (localHandle && remoteHandle) {
let workspaceId: string | undefined = undefined
if (localHandle !== remoteHandle) {
const remoteWorkspace = await api.findWorkspaceByHandle(localHandle).catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, 'Could not list workspaces')
})
if (!remoteWorkspace) {
throw new errors.BotpressCLIError(
`The integration handle "${localHandle}" is not associated with any of your workspaces.`
)
}
this.logger.warn(
`Your are logged in to workspace "${workspaceName}" but integration handle "${localHandle}" belongs to "${remoteWorkspace.name}".`
)
const confirmUseAlternateWorkspace = await this.prompt.confirm(
'Do you want to deploy integration on this workspace instead?'
)
if (!confirmUseAlternateWorkspace) {
throw new errors.BotpressCLIError(
`Cannot deploy integration with handle "${localHandle}" on workspace "${workspaceName}"`
)
}

workspaceId = remoteWorkspace.id
}
return { integration, workspaceId }
}

const workspaceHandleIsMandatoryMsg = 'Cannot deploy integration without workspace handle'

if (!localHandle && remoteHandle) {
const confirmAddHandle = await this.prompt.confirm(
`Your current workspace handle is "${remoteHandle}". Do you want to use the name "${remoteHandle}/${localName}"?`
)
if (!confirmAddHandle) {
this.logger.log('Aborted')
return
}
const newName = `${remoteHandle}/${localName}`
return { integration: new sdk.IntegrationDefinition({ ...integration, name: newName }) }
}

if (localHandle && !remoteHandle) {
const { available } = await api.client.checkHandleAvailability({ handle: localHandle }).catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, 'Could not check handle availability')
})

if (!available) {
throw new errors.BotpressCLIError(`Handle "${localHandle}" is not yours and is not available`)
}

const confirmClaimHandle = await this.prompt.confirm(
`Handle "${localHandle}" is available. Do you want to claim it for your workspace ${workspaceName}?`
)
if (!confirmClaimHandle) {
throw new errors.BotpressCLIError(workspaceHandleIsMandatoryMsg)
}

await api.updateWorkspace({ handle: localHandle }).catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `Could not claim handle "${localHandle}"`)
})

this.logger.success(`Handle "${localHandle}" is now yours!`)
return { integration }
}

this.logger.warn("It seems you don't have a workspace handle yet.")
let claimedHandle: string | undefined = undefined
do {
const prompted = await this.prompt.text('Please enter a workspace handle')
if (!prompted) {
throw new errors.BotpressCLIError(workspaceHandleIsMandatoryMsg)
}

const { available, suggestions } = await api.client.checkHandleAvailability({ handle: prompted })
if (!available) {
this.logger.warn(`Handle "${prompted}" is not available. Suggestions: ${suggestions.join(', ')}`)
continue
}

claimedHandle = prompted
await api.updateWorkspace({ handle: claimedHandle }).catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `Could not claim handle "${claimedHandle}"`)
})
} while (!claimedHandle)

this.logger.success(`Handle "${claimedHandle}" is yours!`)
const newName = `${claimedHandle}/${localName}`
return { integration: new sdk.IntegrationDefinition({ ...integration, name: newName }) }
}

private _parseIntegrationName = (integrationName: string): { name: string; workspaceHandle?: string } => {
const parts = integrationName.split('/')
if (parts.length > 2) {
throw new errors.BotpressCLIError(
`Invalid integration name "${integrationName}": a single forward slash is allowed`
)
}
if (parts.length === 2) {
const [workspaceHandle, name] = parts as [string, string]
return { name, workspaceHandle }
}
const [name] = parts as [string]
return { name }
}
}
33 changes: 28 additions & 5 deletions packages/cli/src/command-implementations/dev-command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type * as client from '@botpress/client'
import type * as sdk from '@botpress/sdk'
import * as sdk from '@botpress/sdk'
import { TunnelRequest, TunnelResponse } from '@bpinternal/tunnel'
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import chalk from 'chalk'
Expand All @@ -24,6 +24,7 @@ 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

Expand All @@ -35,7 +36,7 @@ export class DevCommand extends ProjectCommand<DevCommandDefinition> {
public async run(): Promise<void> {
this.logger.warn('This command is experimental and subject to breaking changes without notice.')

const api = await this.ensureLoginAndCreateClient(this.argv)
let api = await this.ensureLoginAndCreateClient(this.argv)

const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS()
if (projectType === 'interface') {
Expand All @@ -44,6 +45,15 @@ export class DevCommand extends ProjectCommand<DevCommandDefinition> {
const projectDef = await resolveProjectDefinition()
this._initialDef = projectDef

if (projectDef.type === 'integration') {
const handleResult = await this.manageWorkspaceHandle(api, projectDef.definition)
if (!handleResult) return
if (handleResult.workspaceId) {
api = api.switchWorkspace(handleResult.workspaceId)
}
this._deployedIntegrationName = handleResult.integration.name
}

let env: Record<string, string> = {
...process.env,
BP_API_URL: api.url,
Expand Down Expand Up @@ -159,9 +169,13 @@ export class DevCommand extends ProjectCommand<DevCommandDefinition> {
.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) {
if (typescriptEvents.length > 0 || packageJsonEvents.length > 0) {
this.logger.log('Changes detected, rebuilding')
await this._restart(api, worker, httpTunnelUrl)
} else if (distEvents.length > 0) {
Expand Down Expand Up @@ -209,10 +223,19 @@ export class DevCommand extends ProjectCommand<DevCommandDefinition> {
if (projectType === 'interface') {
throw new errors.BotpressCLIError('This feature is not available for interfaces.')
}
if (projectType === 'integration') {
if (projectType === 'integration' && this._initialDef?.type === 'integration') {
const projectDef = await resolveProjectDefinition()
this._checkSecrets(projectDef.definition)
return await this._deployDevIntegration(api, tunnelUrl, 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()
Expand Down
128 changes: 127 additions & 1 deletion packages/cli/src/command-implementations/project-command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type * as client from '@botpress/client'
import type * as sdk from '@botpress/sdk'
import * as sdk from '@botpress/sdk'
import type { YargsConfig } from '@bpinternal/yargs-extra'
import chalk from 'chalk'
import fs from 'fs'
Expand Down Expand Up @@ -851,4 +851,130 @@ export abstract class ProjectCommand<C extends ProjectCommandDefinition> extends
onFailCallback(failedIntegrations)
}
}

protected async manageWorkspaceHandle(
api: apiUtils.ApiClient,
integration: sdk.IntegrationDefinition
): Promise<
| {
integration: sdk.IntegrationDefinition
workspaceId?: string
}
| undefined
> {
const { name: localName, workspaceHandle: localHandle } = this._parseIntegrationName(integration.name)
if (!localHandle && api.isBotpressWorkspace) {
this.logger.debug('Botpress workspace detected; workspace handle omitted')
return { integration } // botpress has the right to omit workspace handle
}

const { handle: remoteHandle, name: workspaceName } = await api.getWorkspace().catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, 'Could not fetch workspace')
})

if (localHandle && remoteHandle) {
let workspaceId: string | undefined = undefined
if (localHandle !== remoteHandle) {
const remoteWorkspace = await api.findWorkspaceByHandle(localHandle).catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, 'Could not list workspaces')
})
if (!remoteWorkspace) {
throw new errors.BotpressCLIError(
`The integration handle "${localHandle}" is not associated with any of your workspaces.`
)
}
this.logger.warn(
`Your are logged in to workspace "${workspaceName}" but integration handle "${localHandle}" belongs to "${remoteWorkspace.name}".`
)
const confirmUseAlternateWorkspace = await this.prompt.confirm(
'Do you want to deploy integration on this workspace instead?'
)
if (!confirmUseAlternateWorkspace) {
throw new errors.BotpressCLIError(
`Cannot deploy integration with handle "${localHandle}" on workspace "${workspaceName}"`
)
}

workspaceId = remoteWorkspace.id
}
return { integration, workspaceId }
}

const workspaceHandleIsMandatoryMsg = 'Cannot deploy integration without workspace handle'

if (!localHandle && remoteHandle) {
const confirmAddHandle = await this.prompt.confirm(
`Your current workspace handle is "${remoteHandle}". Do you want to use the name "${remoteHandle}/${localName}"?`
)
if (!confirmAddHandle) {
this.logger.log('Aborted')
return
}
const newName = `${remoteHandle}/${localName}`
return { integration: new sdk.IntegrationDefinition({ ...integration, name: newName }) }
}

if (localHandle && !remoteHandle) {
const { available } = await api.client.checkHandleAvailability({ handle: localHandle }).catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, 'Could not check handle availability')
})

if (!available) {
throw new errors.BotpressCLIError(`Handle "${localHandle}" is not yours and is not available`)
}

const confirmClaimHandle = await this.prompt.confirm(
`Handle "${localHandle}" is available. Do you want to claim it for your workspace ${workspaceName}?`
)
if (!confirmClaimHandle) {
throw new errors.BotpressCLIError(workspaceHandleIsMandatoryMsg)
}

await api.updateWorkspace({ handle: localHandle }).catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `Could not claim handle "${localHandle}"`)
})

this.logger.success(`Handle "${localHandle}" is now yours!`)
return { integration }
}

this.logger.warn("It seems you don't have a workspace handle yet.")
let claimedHandle: string | undefined = undefined
do {
const prompted = await this.prompt.text('Please enter a workspace handle')
if (!prompted) {
throw new errors.BotpressCLIError(workspaceHandleIsMandatoryMsg)
}

const { available, suggestions } = await api.client.checkHandleAvailability({ handle: prompted })
if (!available) {
this.logger.warn(`Handle "${prompted}" is not available. Suggestions: ${suggestions.join(', ')}`)
continue
}

claimedHandle = prompted
await api.updateWorkspace({ handle: claimedHandle }).catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `Could not claim handle "${claimedHandle}"`)
})
} while (!claimedHandle)

this.logger.success(`Handle "${claimedHandle}" is yours!`)
const newName = `${claimedHandle}/${localName}`
return { integration: new sdk.IntegrationDefinition({ ...integration, name: newName }) }
}

protected _parseIntegrationName = (integrationName: string): { name: string; workspaceHandle?: string } => {
const parts = integrationName.split('/')
if (parts.length > 2) {
throw new errors.BotpressCLIError(
`Invalid integration name "${integrationName}": a single forward slash is allowed`
)
}
if (parts.length === 2) {
const [workspaceHandle, name] = parts as [string, string]
return { name, workspaceHandle }
}
const [name] = parts as [string]
return { name }
}
}
Loading