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: 2 additions & 0 deletions packages/cli/e2e/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const allowDeprecated = false
const isPublic = false
const visibility = 'private' as const
const minify = true
const profile = undefined

export default {
minify,
Expand All @@ -22,4 +23,5 @@ export default {
allowDeprecated,
public: isPublic,
visibility,
profile,
} as const
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@botpress/cli",
"version": "4.13.0",
"version": "4.14.0",
"description": "Botpress CLI",
"scripts": {
"build": "pnpm run bundle && pnpm run template:gen",
Expand Down Expand Up @@ -51,6 +51,7 @@
"devDependencies": {
"@bpinternal/log4bot": "^0.0.4",
"@types/bluebird": "^3.5.38",
"@types/ini": "^4.1.1",
"@types/json-schema": "^7.0.12",
"@types/prompts": "^2.0.14",
"@types/semver": "^7.3.11",
Expand Down
57 changes: 51 additions & 6 deletions packages/cli/src/command-implementations/global-command.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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 type * as config from '../config'
import * as config from '../config'
import * as consts from '../consts'
import * as errors from '../errors'
import type { CommandArgv, CommandDefinition } from '../typings'
Expand All @@ -14,16 +16,24 @@ import { BaseCommand } from './base-command'
export type GlobalCommandDefinition = CommandDefinition<typeof config.schemas.global>
export type GlobalCache = { apiUrl: string; token: string; workspaceId: string }

export type ConfigurableGlobalPaths = { botpressHomeDir: string; cliRootDir: utils.path.AbsolutePath }
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() })
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)),
})
Expand Down Expand Up @@ -73,9 +83,22 @@ export abstract class GlobalCommand<C extends GlobalCommandDefinition> extends B
protected async getAuthenticatedClient(credentials: Partial<YargsConfig<typeof config.schemas.credentials>>) {
const cache = this.globalCache

const token = credentials.token ?? (await cache.get('token'))
const workspaceId = credentials.workspaceId ?? (await cache.get('workspaceId'))
const apiUrl = credentials.apiUrl ?? (await cache.get('apiUrl'))
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))
} 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
Expand All @@ -88,6 +111,28 @@ export abstract class GlobalCommand<C extends GlobalCommandDefinition> extends B
return this.api.newClient({ apiUrl, token, workspaceId }, this.logger)
}

private async _readProfileFromFS(profile: string): Promise<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 parsedProfiles = JSON.parse(fileContent)

const zodParseResult = z.record(profileCredentialSchema).safeParse(parsedProfiles, {})
if (!zodParseResult.success) {
throw errors.BotpressCLIError.wrap(zodParseResult.error, 'Error parsing profiles: ')
}

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 parsedProfiles[profile]
}

protected async ensureLoginAndCreateClient(credentials: YargsConfig<typeof config.schemas.credentials>) {
const client = await this.getAuthenticatedClient(credentials)

Expand All @@ -98,7 +143,7 @@ export abstract class GlobalCommand<C extends GlobalCommandDefinition> extends B
return client
}

private _notifyUpdateCli = async (): Promise<void> => {
private readonly _notifyUpdateCli = async (): Promise<void> => {
try {
this.logger.debug('Checking if cli is up to date')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import _ from 'lodash'
import semver from 'semver'
import * as apiUtils from '../api'
import * as codegen from '../code-generation'
import type * as config from '../config'
import * as config from '../config'
import * as consts from '../consts'
import * as errors from '../errors'
import { validateIntegrationDefinition, validateBotDefinition } from '../sdk'
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ const globalSchema = {
description: 'The path to the Botpress home directory',
default: consts.defaultBotpressHome,
},
profile: {
type: 'string',
description: 'The CLI profile defined in the $BP_BOTPRESS_HOME/.profiles json format file',
},
} satisfies CommandSchema

const projectSchema = {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const cliRootDir = CLI_ROOT_DIR
export const installDirName = 'bp_modules'
export const outDirName = '.botpress'
export const distDirName = 'dist'
export const profileFileName = '.profiles'

export const fromCliRootDir = {}

Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading