diff --git a/packages/cli/package.json b/packages/cli/package.json index d00b33b32b2..9f21595a829 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/command-implementations/deploy-command.ts b/packages/cli/src/command-implementations/deploy-command.ts index 462a071bd60..f8b73bd5136 100644 --- a/packages/cli/src/command-implementations/deploy-command.ts +++ b/packages/cli/src/command-implementations/deploy-command.ts @@ -59,7 +59,7 @@ export class DeployCommand extends ProjectCommand { } 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 @@ -518,130 +518,4 @@ export class DeployCommand extends ProjectCommand { 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 } - } } diff --git a/packages/cli/src/command-implementations/dev-command.ts b/packages/cli/src/command-implementations/dev-command.ts index ac30bb517cd..0396e853bb7 100644 --- a/packages/cli/src/command-implementations/dev-command.ts +++ b/packages/cli/src/command-implementations/dev-command.ts @@ -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' @@ -24,6 +24,7 @@ const FILEWATCHER_DEBOUNCE_MS = 500 export type DevCommandDefinition = typeof commandDefinitions.dev export class DevCommand extends ProjectCommand { private _initialDef: ProjectDefinition | undefined = undefined + private _deployedIntegrationName: string | undefined = undefined private _cacheDevRequestBody: apiUtils.UpdateBotRequestBody | apiUtils.UpdateIntegrationRequestBody | undefined private _buildContext: utils.esbuild.BuildCodeContext @@ -35,7 +36,7 @@ export class DevCommand extends ProjectCommand { public async run(): Promise { 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') { @@ -44,6 +45,15 @@ export class DevCommand extends ProjectCommand { 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 = { ...process.env, BP_API_URL: api.url, @@ -159,9 +169,13 @@ export class DevCommand extends ProjectCommand { .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) { @@ -209,10 +223,19 @@ export class DevCommand extends ProjectCommand { 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() diff --git a/packages/cli/src/command-implementations/project-command.ts b/packages/cli/src/command-implementations/project-command.ts index eaeb1bf7cf7..faaff6938cf 100644 --- a/packages/cli/src/command-implementations/project-command.ts +++ b/packages/cli/src/command-implementations/project-command.ts @@ -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' @@ -851,4 +851,130 @@ export abstract class ProjectCommand 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 } + } }