chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { Client } from '@botpress/client'
|
||||
|
||||
export type ApiBot = Awaited<ReturnType<Client['listBots']>>['bots'][0]
|
||||
export const fetchAllBots = async (client: Client): Promise<ApiBot[]> => await client.list.bots({}).collect()
|
||||
|
||||
export type ApiIntegration = Awaited<ReturnType<Client['listIntegrations']>>['integrations'][0]
|
||||
export const fetchAllIntegrations = async (client: Client): Promise<ApiIntegration[]> =>
|
||||
await client.list.integrations({}).collect()
|
||||
|
||||
export type ApiInterface = Awaited<ReturnType<Client['listInterfaces']>>['interfaces'][0]
|
||||
|
||||
export type ApiPlugin = Awaited<ReturnType<Client['listPlugins']>>['plugins'][0]
|
||||
export const fetchAllPlugins = async (client: Client): Promise<ApiPlugin[]> => await client.list.plugins({}).collect()
|
||||
@@ -0,0 +1,33 @@
|
||||
const noBuild = false
|
||||
const dryRun = false
|
||||
const secrets = [] satisfies string[]
|
||||
const sourceMap = false
|
||||
const watch = true
|
||||
const verbose = false
|
||||
const confirm = true
|
||||
const json = false
|
||||
const allowDeprecated = false
|
||||
const isPublic = false
|
||||
const visibility = 'private' as const
|
||||
const minify = true
|
||||
const profile = undefined
|
||||
const url = undefined
|
||||
const bypassBreakingChangeDetection = false
|
||||
|
||||
export default {
|
||||
minify,
|
||||
noBuild,
|
||||
dryRun,
|
||||
secrets,
|
||||
sourceMap,
|
||||
watch,
|
||||
verbose,
|
||||
confirm,
|
||||
json,
|
||||
allowDeprecated,
|
||||
public: isPublic,
|
||||
visibility,
|
||||
profile,
|
||||
url,
|
||||
bypassBreakingChangeDetection,
|
||||
} as const
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import integrationWithEntityDependency from './bp_modules/integration-with-entity-dependency'
|
||||
import pluginWithInterfaceDependency from './bp_modules/plugin-with-interface-dependency'
|
||||
|
||||
export default new sdk.BotDefinition({})
|
||||
.addIntegration(integrationWithEntityDependency, {
|
||||
alias: 'integration-with-entity-dep',
|
||||
enabled: true,
|
||||
configuration: {},
|
||||
})
|
||||
.addPlugin(pluginWithInterfaceDependency, {
|
||||
alias: 'plugin-alias',
|
||||
configuration: {
|
||||
foo: 'bar',
|
||||
item: {
|
||||
id: 'foo',
|
||||
name: 'Foo',
|
||||
color: 'blue',
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
'interface-alias': {
|
||||
integrationAlias: 'integration-with-entity-dep',
|
||||
integrationInterfaceAlias: Object.keys(integrationWithEntityDependency.definition.interfaces)[0]!,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "cli-fixtures-bot-with-plugin-dependency",
|
||||
"scripts": {
|
||||
"check:type": "tsc --noEmit"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.16.4",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"bpDependencies": {
|
||||
"integration-with-entity-dependency": "../../integrations/integration-with-entity-dependency",
|
||||
"plugin-with-interface-dependency": "../../plugins/plugin-with-interface-dependency"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const bot = new bp.Bot({
|
||||
actions: {},
|
||||
})
|
||||
|
||||
export default bot
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2022"],
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedParameters": true,
|
||||
"target": "es2017",
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist",
|
||||
"checkJs": false,
|
||||
"incremental": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"resolveJsonModule": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUnusedLocals": false
|
||||
},
|
||||
"include": [".botpress/**/*", "src/**/*", "*.ts", "*.json"]
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import interfaceWithEntities from './bp_modules/interface-with-entities'
|
||||
import * as packageJson from './package.json'
|
||||
|
||||
export default new sdk.IntegrationDefinition({
|
||||
name: packageJson.integrationName,
|
||||
version: '1.0.0',
|
||||
entities: {
|
||||
customItem: {
|
||||
schema: sdk.z.object({
|
||||
id: sdk.z.string(),
|
||||
name: sdk.z.string().optional(),
|
||||
color: sdk.z.string().optional(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}).extend(interfaceWithEntities, ({ entities }) => ({ entities: { item: entities.customItem } }))
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "cli-fixtures-integration-with-entity-dependency",
|
||||
"integrationName": "cli-fixtures-integration-with-entity-dependency",
|
||||
"scripts": {
|
||||
"check:type": "tsc --noEmit"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.16.4",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"bpDependencies": {
|
||||
"interface-with-entities": "../../interfaces/interface-with-entities"
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export default new bp.Integration({
|
||||
register() {
|
||||
throw new Error('Not implemented')
|
||||
},
|
||||
unregister() {
|
||||
throw new Error('Not implemented')
|
||||
},
|
||||
actions: {
|
||||
manipulateItem() {
|
||||
throw new Error('Not implemented')
|
||||
},
|
||||
},
|
||||
channels: {},
|
||||
handler() {
|
||||
throw new Error('Not implemented')
|
||||
},
|
||||
})
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2022"],
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedParameters": true,
|
||||
"target": "es2017",
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist",
|
||||
"checkJs": false,
|
||||
"incremental": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"resolveJsonModule": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUnusedLocals": false
|
||||
},
|
||||
"include": [".botpress/**/*", "src/**/*", "*.ts", "*.json"]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as packageJson from './package.json'
|
||||
|
||||
export default new sdk.InterfaceDefinition({
|
||||
name: packageJson.interfaceName,
|
||||
version: '1.0.0',
|
||||
entities: {
|
||||
item: {
|
||||
schema: sdk.z.object({
|
||||
id: sdk.z.string(),
|
||||
name: sdk.z.string().optional(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
manipulateItem: {
|
||||
input: {
|
||||
schema: (entities) =>
|
||||
sdk.z.object({
|
||||
item: entities.item,
|
||||
foo: sdk.z.number(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: () => sdk.z.object({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
events: {
|
||||
somethingHappened: {
|
||||
schema: (entities) =>
|
||||
sdk.z.object({
|
||||
item: entities.item,
|
||||
message: sdk.z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "cli-fixtures-interface-with-entities",
|
||||
"interfaceName": "cli-fixtures-interface-with-entities",
|
||||
"scripts": {
|
||||
"check:type": "tsc --noEmit"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.16.4",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2022"],
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedParameters": true,
|
||||
"target": "es2017",
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist",
|
||||
"checkJs": false,
|
||||
"incremental": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"resolveJsonModule": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUnusedLocals": false
|
||||
},
|
||||
"include": [".botpress/**/*", "*.ts", "*.json"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "cli-fixtures-plugin-with-interface-dependency",
|
||||
"pluginName": "cli-fixtures-plugin-with-interface-dependency",
|
||||
"scripts": {
|
||||
"check:type": "tsc --noEmit"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.16.4",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"bpDependencies": {
|
||||
"interface-with-entities": "../../interfaces/interface-with-entities"
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import interfaceWithEntities from './bp_modules/interface-with-entities'
|
||||
import * as packageJson from './package.json'
|
||||
|
||||
export default new sdk.PluginDefinition({
|
||||
name: packageJson.pluginName,
|
||||
version: '1.0.0',
|
||||
interfaces: {
|
||||
'interface-alias': interfaceWithEntities,
|
||||
},
|
||||
configuration: {
|
||||
schema: ({ entities }) =>
|
||||
sdk.z.object({
|
||||
item: entities['interface-alias'].item,
|
||||
foo: sdk.z.literal('bar'),
|
||||
}),
|
||||
},
|
||||
actions: {
|
||||
doSomething: {
|
||||
input: {
|
||||
schema: ({ entities }) =>
|
||||
sdk.z.object({
|
||||
item: entities['interface-alias'].item,
|
||||
bar: sdk.z.number(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: sdk.z.object({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const plugin = new bp.Plugin({
|
||||
actions: {},
|
||||
})
|
||||
|
||||
export default plugin
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2022"],
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedParameters": true,
|
||||
"target": "es2017",
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist",
|
||||
"checkJs": false,
|
||||
"incremental": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"resolveJsonModule": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUnusedLocals": false
|
||||
},
|
||||
"include": [".botpress/**/*", "src/**/*", "*.ts", "*.json"]
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Logger } from '@bpinternal/log4bot'
|
||||
import yargs, { YargsConfig, YargsSchema } from '@bpinternal/yargs-extra'
|
||||
import * as consts from '../src/consts'
|
||||
import { requiredBotSecrets } from './tests/bot-secrets'
|
||||
import { createDeployBot } from './tests/create-deploy-bot'
|
||||
import { createDeployBotWithDependencies } from './tests/create-deploy-bot-with-dependencies'
|
||||
import { createDeployIntegration } from './tests/create-deploy-integration'
|
||||
import { devBot } from './tests/dev-bot'
|
||||
import { installAllInterfaces } from './tests/install-interfaces'
|
||||
import {
|
||||
addIntegration,
|
||||
addPlugin,
|
||||
addLocalIntegrationKeepsRelativePath,
|
||||
addDevIntegrationSkipsBpDependencies,
|
||||
addDevIntegrationPreservesExistingBpDependency,
|
||||
reinstallLocalIntegration,
|
||||
} from './tests/install-package'
|
||||
import { requiredIntegrationSecrets } from './tests/integration-secrets'
|
||||
import {
|
||||
enforceWorkspaceHandleIntegration,
|
||||
enforceWorkspaceHandlePlugin,
|
||||
prependWorkspaceHandleIntegration,
|
||||
prependWorkspaceHandlePlugin,
|
||||
} from './tests/manage-workspace-handle'
|
||||
import { removePackage } from './tests/remove-package'
|
||||
import { Test } from './typings'
|
||||
import { sleep, TmpDirectory } from './utils'
|
||||
|
||||
const tests: Test[] = [
|
||||
createDeployBot,
|
||||
createDeployBotWithDependencies,
|
||||
createDeployIntegration,
|
||||
devBot,
|
||||
requiredBotSecrets,
|
||||
requiredIntegrationSecrets,
|
||||
prependWorkspaceHandleIntegration,
|
||||
enforceWorkspaceHandleIntegration,
|
||||
prependWorkspaceHandlePlugin,
|
||||
enforceWorkspaceHandlePlugin,
|
||||
addIntegration,
|
||||
addPlugin,
|
||||
addLocalIntegrationKeepsRelativePath,
|
||||
addDevIntegrationSkipsBpDependencies,
|
||||
addDevIntegrationPreservesExistingBpDependency,
|
||||
reinstallLocalIntegration,
|
||||
installAllInterfaces,
|
||||
removePackage,
|
||||
]
|
||||
|
||||
const timeout = (ms: number) =>
|
||||
sleep(ms).then(() => {
|
||||
throw new Error(`Timeout after ${ms}ms`)
|
||||
})
|
||||
|
||||
const TIMEOUT = 300_000
|
||||
|
||||
const configSchema = {
|
||||
timeout: {
|
||||
type: 'number',
|
||||
default: TIMEOUT,
|
||||
},
|
||||
verbose: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
alias: 'v',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
},
|
||||
workspaceHandle: {
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
},
|
||||
token: {
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
},
|
||||
apiUrl: {
|
||||
type: 'string',
|
||||
default: consts.defaultBotpressApiUrl,
|
||||
},
|
||||
tunnelUrl: {
|
||||
type: 'string',
|
||||
default: consts.defaultTunnelUrl,
|
||||
},
|
||||
sdkPath: {
|
||||
type: 'string',
|
||||
description: 'Path to the Botpress SDK to install; Allows using a version not released on NPM yet.',
|
||||
},
|
||||
clientPath: {
|
||||
type: 'string',
|
||||
description: 'Path to the Botpress Client to install; Allows using a version not released on NPM yet.',
|
||||
},
|
||||
} satisfies YargsSchema
|
||||
|
||||
const main = async (argv: YargsConfig<typeof configSchema>): Promise<never> => {
|
||||
const logger = new Logger('e2e', { level: argv.verbose ? 'debug' : 'info' })
|
||||
|
||||
const filterRegex = argv.filter ? new RegExp(argv.filter) : null
|
||||
const filteredTests = tests.filter(({ name }) => (filterRegex ? filterRegex.test(name) : true))
|
||||
logger.info(`Running ${filteredTests.length} / ${tests.length} tests`)
|
||||
|
||||
const dependencies = { '@botpress/sdk': argv.sdkPath, '@botpress/client': argv.clientPath } satisfies Record<
|
||||
string,
|
||||
string | undefined
|
||||
>
|
||||
|
||||
for (const { name, handler } of filteredTests) {
|
||||
const logLine = `### Running test: "${name}" ###`
|
||||
const logPad = '#'.repeat(logLine.length)
|
||||
logger.info(logPad)
|
||||
logger.info(logLine)
|
||||
logger.info(logPad + '\n')
|
||||
|
||||
const loggerNamespace = name.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '')
|
||||
|
||||
const tmpDir = TmpDirectory.create()
|
||||
try {
|
||||
const t0 = Date.now()
|
||||
await Promise.race([
|
||||
handler({ tmpDir: tmpDir.path, dependencies, logger: logger.sub(loggerNamespace), ...argv }),
|
||||
timeout(argv.timeout),
|
||||
])
|
||||
const t1 = Date.now()
|
||||
logger.info(`SUCCESS: "${name}" (${t1 - t0}ms)`)
|
||||
} catch (thrown) {
|
||||
const err = thrown instanceof Error ? thrown : new Error(`${thrown}`)
|
||||
logger.attachError(err).error(`FAILURE: "${name}"`)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
tmpDir.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('All tests passed')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
void yargs.command('$0', 'Run E2E Tests', configSchema, main).parse()
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as client from '@botpress/client'
|
||||
|
||||
export const config: client.RetryConfig = {
|
||||
retries: 3,
|
||||
retryCondition: (err) =>
|
||||
client.axiosRetry.isNetworkOrIdempotentRequestError(err) || [429, 502].includes(err.response?.status ?? 0),
|
||||
retryDelay: (retryCount) => retryCount * 1000,
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Client } from '@botpress/client'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import fs from 'fs'
|
||||
import pathlib from 'path'
|
||||
import * as uuid from 'uuid'
|
||||
import impl from '../../src'
|
||||
import { ApiBot, fetchAllBots } from '../api'
|
||||
import defaults from '../defaults'
|
||||
import * as retry from '../retry'
|
||||
import { Test } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
type SecretDef = NonNullable<sdk.BotDefinitionProps['secrets']>
|
||||
|
||||
const fetchBot = async (client: Client, botName: string): Promise<ApiBot | undefined> => {
|
||||
const bots = await fetchAllBots(client)
|
||||
return bots.find(({ name }) => name === botName)
|
||||
}
|
||||
|
||||
const appendSecretDefinition = (originalTsContent: string, secrets: SecretDef): string => {
|
||||
const secretEntries = Object.entries(secrets)
|
||||
.map(([secretName, secretDef]) => ` ${secretName}: ${JSON.stringify(secretDef)},`)
|
||||
.join('\n')
|
||||
|
||||
return originalTsContent.replace(
|
||||
'new BotDefinition({})',
|
||||
`new BotDefinition({\n secrets: {\n${secretEntries}\n },\n})`
|
||||
)
|
||||
}
|
||||
|
||||
export const requiredBotSecrets: Test = {
|
||||
name: 'cli should require required bot secrets',
|
||||
handler: async ({ tmpDir, dependencies, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'bots')
|
||||
const botName = uuid.v4()
|
||||
const botDir = pathlib.join(baseDir, botName)
|
||||
|
||||
const definitionPath = pathlib.join(botDir, 'bot.definition.ts')
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
|
||||
await impl
|
||||
.init({ ...argv, workDir: baseDir, name: botName, type: 'bot', template: 'empty' })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
const originalDefinition: string = fs.readFileSync(definitionPath, 'utf-8')
|
||||
const modifiedDefinition = appendSecretDefinition(originalDefinition, {
|
||||
REQUIRED_SECRET: {},
|
||||
OPTIONAL_SECRET: { optional: true },
|
||||
})
|
||||
fs.writeFileSync(definitionPath, modifiedDefinition, 'utf-8')
|
||||
|
||||
await utils.fixBotpressDependencies({ workDir: botDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: botDir }).then(utils.handleExitCode)
|
||||
await impl.build({ ...argv, workDir: botDir }).then(utils.handleExitCode)
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
await impl.bots.create({ ...argv, name: botName, ifNotExists: false }).then(utils.handleExitCode)
|
||||
|
||||
const bot = await fetchBot(client, botName)
|
||||
if (!bot) {
|
||||
throw new Error(`Bot ${botName} should have been created`)
|
||||
}
|
||||
|
||||
const { exitCode } = await impl.deploy({
|
||||
...argv,
|
||||
workDir: botDir,
|
||||
secrets: ['OPTIONAL_SECRET=lol'],
|
||||
botId: bot.id,
|
||||
createNewBot: false,
|
||||
})
|
||||
if (exitCode === 0) {
|
||||
throw new Error('Expected deploy to fail')
|
||||
}
|
||||
|
||||
await impl
|
||||
.deploy({
|
||||
...argv,
|
||||
workDir: botDir,
|
||||
secrets: ['REQUIRED_SECRET=lol'],
|
||||
botId: bot.id,
|
||||
createNewBot: false,
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
const { bot: deployedBot } = await client.getBot({ id: bot.id })
|
||||
if (!deployedBot.secrets.includes('REQUIRED_SECRET')) {
|
||||
throw new Error(`Bot ${botName} should have secret REQUIRED_SECRET, got: ${deployedBot.secrets.join(', ')}`)
|
||||
}
|
||||
|
||||
// cleanup deployed bot
|
||||
await impl.bots.delete({ ...argv, botRef: bot.id }).then(utils.handleExitCode)
|
||||
|
||||
if (await fetchBot(client, botName)) {
|
||||
throw new Error(`Bot ${botName} should have been deleted`)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import { Client } from '@botpress/client'
|
||||
import fs from 'fs/promises'
|
||||
import pathlib from 'path'
|
||||
import * as uuid from 'uuid'
|
||||
import impl from '../../src'
|
||||
import { ApiBot, ApiIntegration, ApiInterface, ApiPlugin } from '../api'
|
||||
import defaults from '../defaults'
|
||||
import * as retry from '../retry'
|
||||
import { Test } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
type ARGV = typeof defaults & {
|
||||
botpressHome: string
|
||||
confirm: true
|
||||
workspaceId: string
|
||||
workspaceHandle: string
|
||||
token: string
|
||||
apiUrl: string
|
||||
tunnelUrl: string
|
||||
sdkPath?: string
|
||||
clientPath?: string
|
||||
}
|
||||
|
||||
export const createDeployBotWithDependencies: Test = {
|
||||
name: 'cli should allow creating, building, deploying and managing a bot that has nested dependencies',
|
||||
handler: async ({ tmpDir, dependencies, logger, ...creds }) => {
|
||||
if (!_isBotpressWorkspace(creds.workspaceHandle, creds.workspaceId)) {
|
||||
// Unfortunately, only the botpress workspace can deploy interfaces
|
||||
logger.info(
|
||||
`Skipping test because the workspace is not a Botpress workspace (workspaceHandle: ${creds.workspaceHandle}, workspaceId: ${creds.workspaceId})`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const fixturesDir = pathlib.join(process.cwd(), 'e2e', 'fixtures')
|
||||
const interfaceDir = pathlib.join(tmpDir, 'interfaces', 'interface-with-entities')
|
||||
const integrationDir = pathlib.join(tmpDir, 'integrations', 'integration-with-entity-dependency')
|
||||
const pluginDir = pathlib.join(tmpDir, 'plugins', 'plugin-with-interface-dependency')
|
||||
const botDir = pathlib.join(tmpDir, 'bots', 'bot-with-plugin-dependency')
|
||||
|
||||
// copy all subfolders from fixturesDir to tmpDir:
|
||||
await fs.cp(fixturesDir, tmpDir, { recursive: true, force: true })
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
} satisfies ARGV
|
||||
|
||||
const client = new Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
|
||||
// Generate names:
|
||||
const interfaceName = _generateInterfaceName()
|
||||
const integrationName = _generateIntegrationName()
|
||||
const pluginName = _generatePluginName()
|
||||
const botName = _generateBotName()
|
||||
|
||||
// Deploy dependencies:
|
||||
await using _iface = await _deployInterface({ argv, client, dependencies, workDir: interfaceDir, interfaceName })
|
||||
await using _integration = await _deployIntegration({
|
||||
argv,
|
||||
client,
|
||||
dependencies,
|
||||
workDir: integrationDir,
|
||||
integrationName,
|
||||
})
|
||||
await using _plugin = await _deployPlugin({ argv, client, dependencies, workDir: pluginDir, pluginName })
|
||||
|
||||
// Deploy bot:
|
||||
await using bot = await _deployBot({ argv, client, dependencies, workDir: botDir, botName })
|
||||
|
||||
// Fetch and verify the deployed bot:
|
||||
const deployedBot = await client.getBot({ id: bot.botId }).then((res) => res.bot)
|
||||
|
||||
// TODO: use vitest for our e2e tests and replace this series of if/else
|
||||
// checks with an expect().toMatchObject() assertion.
|
||||
|
||||
// Check that the plugin is installed and enabled:
|
||||
if (!deployedBot.plugins?.['plugin-alias']?.enabled) {
|
||||
throw new Error('Expected bot to have plugin "plugin-alias"')
|
||||
}
|
||||
|
||||
// Check that the schemas have been merged correctly:
|
||||
const action = deployedBot.actions?.['plugin-alias#doSomething']
|
||||
if (!action?.input?.schema?.properties) {
|
||||
throw new Error('Expected bot to have action "plugin-alias#doSomething"')
|
||||
}
|
||||
|
||||
const itemEntityProperties = Object.keys(action.input.schema.properties.item.properties)
|
||||
// Property defined by the interface:
|
||||
if (!itemEntityProperties.includes('name')) {
|
||||
throw new Error('Expected "plugin-alias#doSomething" action input to have item.name property')
|
||||
}
|
||||
// Property defined by the integration:
|
||||
if (!itemEntityProperties.includes('color')) {
|
||||
throw new Error('Expected "plugin-alias#doSomething" action input to have item.color property')
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const _isBotpressWorkspace = (workspaceHandle: string, workspaceId: string): boolean =>
|
||||
workspaceHandle === 'botpress' ||
|
||||
[
|
||||
'6a76fa10-e150-4ff6-8f59-a300feec06c1',
|
||||
'95de33eb-1551-4af9-9088-e5dcb02efd09',
|
||||
'11111111-1111-1111-aaaa-111111111111',
|
||||
].includes(workspaceId)
|
||||
|
||||
const _deployInterface = async ({
|
||||
argv,
|
||||
client,
|
||||
workDir,
|
||||
interfaceName,
|
||||
dependencies,
|
||||
}: {
|
||||
argv: ARGV
|
||||
client: Client
|
||||
workDir: string
|
||||
interfaceName: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
}) => {
|
||||
await _editPackageJson({ workDir, interfaceName })
|
||||
await _installAndBuild({ argv, workDir, dependencies })
|
||||
|
||||
await impl
|
||||
.deploy({ ...argv, workDir, createNewBot: undefined, botId: undefined, visibility: 'public' })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
return {
|
||||
[Symbol.asyncDispose]: () => _deleteInterfaceIfExists(client, interfaceName),
|
||||
}
|
||||
}
|
||||
|
||||
const _deployIntegration = async ({
|
||||
argv,
|
||||
client,
|
||||
workDir,
|
||||
integrationName,
|
||||
dependencies,
|
||||
}: {
|
||||
argv: ARGV
|
||||
client: Client
|
||||
workDir: string
|
||||
integrationName: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
}) => {
|
||||
await _editPackageJson({ workDir, integrationName })
|
||||
await _installAndBuild({ argv, workDir, dependencies })
|
||||
|
||||
await impl.deploy({ ...argv, workDir, createNewBot: undefined, botId: undefined }).then(utils.handleExitCode)
|
||||
|
||||
return {
|
||||
[Symbol.asyncDispose]: () => _deleteIntegrationIfExists(client, integrationName),
|
||||
}
|
||||
}
|
||||
|
||||
const _deployPlugin = async ({
|
||||
argv,
|
||||
client,
|
||||
workDir,
|
||||
pluginName,
|
||||
dependencies,
|
||||
}: {
|
||||
argv: ARGV
|
||||
client: Client
|
||||
workDir: string
|
||||
pluginName: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
}) => {
|
||||
await _editPackageJson({ workDir, pluginName })
|
||||
await _installAndBuild({ argv, workDir, dependencies })
|
||||
await impl.deploy({ ...argv, workDir, createNewBot: undefined, botId: undefined }).then(utils.handleExitCode)
|
||||
|
||||
return {
|
||||
[Symbol.asyncDispose]: () => _deletePluginIfExists(client, pluginName),
|
||||
}
|
||||
}
|
||||
|
||||
const _deployBot = async ({
|
||||
argv,
|
||||
client,
|
||||
workDir,
|
||||
botName,
|
||||
dependencies,
|
||||
}: {
|
||||
argv: ARGV
|
||||
client: Client
|
||||
workDir: string
|
||||
botName: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
}) => {
|
||||
await _editPackageJson({ workDir, botName })
|
||||
await _installAndBuild({ argv, workDir, dependencies })
|
||||
await impl.bots.create({ ...argv, name: botName, ifNotExists: false }).then(utils.handleExitCode)
|
||||
|
||||
const bot = await _fetchBotByName(client, botName)
|
||||
if (!bot) {
|
||||
throw new Error(`Bot ${botName} should have been created`)
|
||||
}
|
||||
|
||||
await impl.deploy({ ...argv, workDir, createNewBot: false, botId: bot.id }).then(utils.handleExitCode)
|
||||
|
||||
return {
|
||||
botId: bot.id,
|
||||
[Symbol.asyncDispose]: () => _deleteBotIfExists(client, botName),
|
||||
}
|
||||
}
|
||||
|
||||
const _installAndBuild = async ({
|
||||
argv,
|
||||
workDir,
|
||||
dependencies,
|
||||
}: {
|
||||
argv: ARGV
|
||||
workDir: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
}) => {
|
||||
await utils.fixBotpressDependencies({ workDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir }).then(utils.handleExitCode)
|
||||
|
||||
const prevDir = process.cwd()
|
||||
process.chdir(workDir)
|
||||
await impl
|
||||
.add({
|
||||
...argv,
|
||||
installPath: workDir,
|
||||
packageRef: undefined,
|
||||
useDev: false,
|
||||
alias: undefined,
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
process.chdir(prevDir)
|
||||
|
||||
await impl.build({ ...argv, workDir }).then(utils.handleExitCode)
|
||||
}
|
||||
|
||||
const _fetchBotByName = (client: Client, botName: string): Promise<ApiBot | undefined> =>
|
||||
client.list
|
||||
.bots({})
|
||||
.collect()
|
||||
.then((bots) => bots.find(({ name }) => name === botName))
|
||||
|
||||
const _fetchIntegrationByName = (client: Client, integrationName: string): Promise<ApiIntegration | undefined> =>
|
||||
client.list
|
||||
.integrations({})
|
||||
.collect()
|
||||
.then((integrations) => integrations.find(({ name }) => name === integrationName))
|
||||
|
||||
const _fetchInterfaceByName = (client: Client, interfaceName: string): Promise<ApiInterface | undefined> =>
|
||||
client.list
|
||||
.publicInterfaces({})
|
||||
.collect()
|
||||
.then((interfaces) => interfaces.find(({ name }) => name === interfaceName))
|
||||
|
||||
const _fetchPluginByName = (client: Client, pluginName: string): Promise<ApiPlugin | undefined> =>
|
||||
client.list
|
||||
.plugins({})
|
||||
.collect()
|
||||
.then((plugins) => plugins.find(({ name }) => name === pluginName))
|
||||
|
||||
const _editPackageJson = async (payload: { workDir: string; [k: string]: string }) => {
|
||||
const { workDir, ...modifications } = payload
|
||||
const packageJsonPath = pathlib.join(workDir, 'package.json')
|
||||
const originalPackageJson: Record<string, unknown> = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'))
|
||||
|
||||
const newPackageJson = {
|
||||
...originalPackageJson,
|
||||
...modifications,
|
||||
}
|
||||
|
||||
await fs.writeFile(packageJsonPath, JSON.stringify(newPackageJson, null, 2))
|
||||
}
|
||||
|
||||
const _generateInterfaceName = () => `iface-${uuid.v4()}` as const
|
||||
const _generateIntegrationName = () => `integration-${uuid.v4()}` as const
|
||||
const _generatePluginName = () => `plugin-${uuid.v4()}` as const
|
||||
const _generateBotName = () => `bot-${uuid.v4()}` as const
|
||||
|
||||
const _deleteBotIfExists = async (client: Client, botName: string): Promise<void> => {
|
||||
console.debug(`Deleting bot: ${botName}`)
|
||||
const bot = await _fetchBotByName(client, botName)
|
||||
if (!bot) {
|
||||
return
|
||||
}
|
||||
await _swallowErrors(client.deleteBot({ id: bot.id })).then(() => utils.sleep(10_000))
|
||||
}
|
||||
|
||||
const _deleteIntegrationIfExists = async (client: Client, integrationName: string): Promise<void> => {
|
||||
console.debug(`Deleting integration: ${integrationName}`)
|
||||
const integration = await _fetchIntegrationByName(client, integrationName)
|
||||
if (!integration) {
|
||||
return
|
||||
}
|
||||
await _swallowErrors(client.deleteIntegration({ id: integration.id }))
|
||||
}
|
||||
|
||||
const _deleteInterfaceIfExists = async (client: Client, interfaceName: string): Promise<void> => {
|
||||
console.debug(`Deleting interface: ${interfaceName}`)
|
||||
const iface = await _fetchInterfaceByName(client, interfaceName)
|
||||
if (!iface) {
|
||||
return
|
||||
}
|
||||
await _swallowErrors(client.deleteInterface({ id: iface.id }))
|
||||
}
|
||||
|
||||
const _deletePluginIfExists = async (client: Client, pluginName: string): Promise<void> => {
|
||||
console.debug(`Deleting plugin: ${pluginName}`)
|
||||
const plugin = await _fetchPluginByName(client, pluginName)
|
||||
if (!plugin) {
|
||||
return
|
||||
}
|
||||
await _swallowErrors(client.deletePlugin({ id: plugin.id }))
|
||||
}
|
||||
|
||||
const _swallowErrors = async (promise: Promise<unknown>) => {
|
||||
try {
|
||||
await promise
|
||||
} catch (thrown: unknown) {
|
||||
// Swallow errors to allow cleanup to proceed:
|
||||
console.warn('Error during cleanup:', thrown)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Client } from '@botpress/client'
|
||||
import pathlib from 'path'
|
||||
import * as uuid from 'uuid'
|
||||
import impl from '../../src'
|
||||
import { ApiBot, fetchAllBots } from '../api'
|
||||
import defaults from '../defaults'
|
||||
import * as retry from '../retry'
|
||||
import { Test } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
const fetchBot = async (client: Client, botName: string): Promise<ApiBot | undefined> => {
|
||||
const bots = await fetchAllBots(client)
|
||||
return bots.find(({ name }) => name === botName)
|
||||
}
|
||||
|
||||
export const createDeployBot: Test = {
|
||||
name: 'cli should allow creating, building, deploying and managing a bot',
|
||||
handler: async ({ tmpDir, dependencies, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'bots')
|
||||
const botName = uuid.v4()
|
||||
const botDir = pathlib.join(baseDir, botName)
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
|
||||
await impl
|
||||
.init({ ...argv, workDir: baseDir, name: botName, type: 'bot', template: 'empty' })
|
||||
.then(utils.handleExitCode)
|
||||
await utils.fixBotpressDependencies({ workDir: botDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: botDir }).then(utils.handleExitCode)
|
||||
await impl.build({ ...argv, workDir: botDir }).then(utils.handleExitCode)
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
await impl.bots.create({ ...argv, name: botName, ifNotExists: false }).then(utils.handleExitCode)
|
||||
|
||||
const bot = await fetchBot(client, botName)
|
||||
if (!bot) {
|
||||
throw new Error(`Bot ${botName} should have been created`)
|
||||
}
|
||||
|
||||
await impl.deploy({ ...argv, workDir: botDir, createNewBot: false, botId: bot.id }).then(utils.handleExitCode)
|
||||
await impl.bots.delete({ ...argv, botRef: bot.id }).then(utils.handleExitCode)
|
||||
|
||||
if (await fetchBot(client, botName)) {
|
||||
throw new Error(`Bot ${botName} should have been deleted`)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Client } from '@botpress/client'
|
||||
import pathlib from 'path'
|
||||
import * as uuid from 'uuid'
|
||||
import impl from '../../src'
|
||||
import { ApiIntegration, fetchAllIntegrations } from '../api'
|
||||
import defaults from '../defaults'
|
||||
import * as retry from '../retry'
|
||||
import { Test } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
const fetchIntegration = async (client: Client, integrationName: string): Promise<ApiIntegration | undefined> => {
|
||||
const integrations = await fetchAllIntegrations(client)
|
||||
return integrations.find(({ name }) => name === integrationName)
|
||||
}
|
||||
|
||||
export const createDeployIntegration: Test = {
|
||||
name: 'cli should allow creating, building, deploying and managing an integration',
|
||||
handler: async ({ tmpDir, dependencies, workspaceHandle, logger, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'integrations')
|
||||
|
||||
const integrationSuffix = uuid.v4().replace(/-/g, '')
|
||||
const name = `myintegration${integrationSuffix}`
|
||||
const integrationName = `${workspaceHandle}/${name}`
|
||||
const integrationDir = pathlib.join(baseDir, name)
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
|
||||
await impl
|
||||
.init({ ...argv, workDir: baseDir, name: integrationName, type: 'integration', template: 'empty' })
|
||||
.then(utils.handleExitCode)
|
||||
await utils.fixBotpressDependencies({ workDir: integrationDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: integrationDir }).then(utils.handleExitCode)
|
||||
await impl.build({ ...argv, workDir: integrationDir }).then(utils.handleExitCode)
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
|
||||
await impl
|
||||
.deploy({ ...argv, createNewBot: undefined, botId: undefined, workDir: integrationDir })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
logger.debug(`Fetching integration "${integrationName}"`)
|
||||
const integration = await fetchIntegration(client, integrationName)
|
||||
if (!integration) {
|
||||
throw new Error(`Integration ${integrationName} should have been created`)
|
||||
}
|
||||
|
||||
logger.debug(`Deleting integration "${integrationName}"`)
|
||||
await impl.integrations.delete({ ...argv, integrationRef: integration.id }).then(utils.handleExitCode)
|
||||
|
||||
if (await fetchIntegration(client, integrationName)) {
|
||||
throw new Error(`Integration ${integrationName} should have been deleted`)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Client, isApiError } from '@botpress/client'
|
||||
import axios from 'axios'
|
||||
import findProcess from 'find-process'
|
||||
import fslib from 'fs'
|
||||
import pathlib from 'path'
|
||||
import * as uuid from 'uuid'
|
||||
import impl from '../../src'
|
||||
import defaults from '../defaults'
|
||||
import * as retry from '../retry'
|
||||
import { Test } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
const handleExitCode = ({ exitCode }: { exitCode: number }) => {
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`Command exited with code ${exitCode}`)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchDevBotById = async (client: Client, id: string) => {
|
||||
const { bot } = await client.getBot({ id }).catch((err) => {
|
||||
if (isApiError(err) && err.type === 'ResourceNotFound') {
|
||||
return { bot: undefined }
|
||||
}
|
||||
throw err
|
||||
})
|
||||
return bot
|
||||
}
|
||||
|
||||
const readBotCache = async (
|
||||
botDir: string
|
||||
): Promise<{
|
||||
botId?: string
|
||||
tunnelId?: string
|
||||
devId?: string
|
||||
}> => {
|
||||
const botCache = pathlib.join(botDir, '.botpress', 'project.cache.json')
|
||||
if (!fslib.existsSync(botCache)) {
|
||||
return {}
|
||||
}
|
||||
const cacheContent: string = await fslib.promises.readFile(botCache, 'utf-8')
|
||||
try {
|
||||
return JSON.parse(cacheContent)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const PORT = 8075
|
||||
|
||||
export const devBot: Test = {
|
||||
name: 'cli should allow creating and running a bot locally',
|
||||
handler: async ({ tmpDir, tunnelUrl, dependencies, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'bots')
|
||||
const botName = uuid.v4()
|
||||
const tunnelId = uuid.v4()
|
||||
const botDir = pathlib.join(baseDir, botName)
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
|
||||
await impl.init({ ...argv, workDir: baseDir, name: botName, type: 'bot', template: 'empty' }).then(handleExitCode)
|
||||
await utils.fixBotpressDependencies({ workDir: botDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: botDir }).then(handleExitCode)
|
||||
await impl.login({ ...argv }).then(handleExitCode)
|
||||
|
||||
const cmdPromise = impl
|
||||
.dev({ ...argv, workDir: botDir, port: PORT, tunnelUrl, tunnelId, noSecretCaching: true })
|
||||
.then(handleExitCode)
|
||||
await utils.sleep(5000)
|
||||
|
||||
const allProcess = await findProcess('port', PORT)
|
||||
const [botProcess] = allProcess
|
||||
if (allProcess.length > 1) {
|
||||
throw new Error(`Expected to find only one process listening on port ${PORT}`)
|
||||
}
|
||||
if (!botProcess) {
|
||||
throw new Error(`Expected to find a process listening on port ${PORT}`)
|
||||
}
|
||||
|
||||
const botCache = await readBotCache(botDir)
|
||||
const { devId: botId } = botCache
|
||||
if (!botId) {
|
||||
throw new Error('Expected devId to be set in project cache')
|
||||
}
|
||||
|
||||
const resp = await axios.get(`http://localhost:${PORT}/health`)
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`Expected health endpoint to return 200, got ${resp.status}`)
|
||||
}
|
||||
|
||||
process.kill(botProcess.pid)
|
||||
await cmdPromise
|
||||
|
||||
const apiBot = await fetchDevBotById(client, botId)
|
||||
if (!apiBot) {
|
||||
throw new Error(`Dev Bot ${botId} should have been created in the backend`)
|
||||
}
|
||||
|
||||
await impl.bots.delete({ ...argv, botRef: apiBot.id }).then(utils.handleExitCode)
|
||||
|
||||
if (await fetchDevBotById(client, botId)) {
|
||||
throw new Error(`Dev bot ${botId} should have been deleted`)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import pathlib from 'path'
|
||||
import impl from '../../src'
|
||||
import defaults from '../defaults'
|
||||
import { Test } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
export const installAllInterfaces: Test = {
|
||||
name: 'cli should allow installing public interfaces',
|
||||
handler: async ({ tmpDir, logger, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'interfaces')
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
|
||||
const interfaces: string[] = [
|
||||
'creatable',
|
||||
'deletable',
|
||||
'hitl',
|
||||
'listable',
|
||||
'llm',
|
||||
'readable',
|
||||
'speech-to-text',
|
||||
'text-to-image',
|
||||
'typing-indicator',
|
||||
'updatable',
|
||||
]
|
||||
|
||||
for (const iface of interfaces) {
|
||||
logger.info(`Installing interface: ${iface}`)
|
||||
await impl
|
||||
.add({
|
||||
...argv,
|
||||
packageRef: iface,
|
||||
installPath: baseDir,
|
||||
useDev: false,
|
||||
alias: undefined,
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
// TODO: also run a type check on the installed interface
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
import * as client from '@botpress/client'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as fslib from 'fs'
|
||||
import * as pathlib from 'path'
|
||||
import * as uuid from 'uuid'
|
||||
import impl from '../../src'
|
||||
import * as apiUtils from '../../src/api'
|
||||
import * as consts from '../../src/consts'
|
||||
import * as cliUtils from '../../src/utils'
|
||||
import { ApiBot, fetchAllBots } from '../api'
|
||||
import defaults from '../defaults'
|
||||
import * as retry from '../retry'
|
||||
import { Test, TestProps } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
const issueSchema = sdk.z.object({
|
||||
id: sdk.z.string(),
|
||||
priority: sdk.z.enum(['high', 'medium', 'low']),
|
||||
title: sdk.z.string(),
|
||||
body: sdk.z.string(),
|
||||
})
|
||||
const INTEGRATION = {
|
||||
version: '0.0.1',
|
||||
title: 'An Integration',
|
||||
description: 'An integration',
|
||||
user: { tags: { id: { title: 'ID', description: 'The user ID' } } },
|
||||
configuration: { schema: sdk.z.object({}), identifier: { required: true, linkTemplateScript: '' } },
|
||||
configurations: {
|
||||
token: {
|
||||
title: 'API Token',
|
||||
description: 'The token to authenticate with',
|
||||
schema: sdk.z.object({ token: sdk.z.string() }),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
getIssue: {
|
||||
input: { schema: sdk.z.object({ id: sdk.z.string() }) },
|
||||
output: { schema: issueSchema },
|
||||
},
|
||||
},
|
||||
events: {
|
||||
issueCreated: {
|
||||
title: 'Issue Created',
|
||||
description: 'An issue was created',
|
||||
schema: issueSchema,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
issueComment: {
|
||||
title: 'Issue Comment',
|
||||
description: 'Comment on an issue',
|
||||
messages: { text: sdk.messages.defaults.text },
|
||||
conversation: { tags: { id: { title: 'ID', description: 'The issue ID' } } },
|
||||
message: { tags: { id: { title: 'ID', description: 'The issue comment ID' } } },
|
||||
},
|
||||
},
|
||||
entities: {
|
||||
issue: {
|
||||
title: 'Issue',
|
||||
description: 'An issue',
|
||||
schema: issueSchema,
|
||||
},
|
||||
},
|
||||
states: {
|
||||
lastCreatedIssue: {
|
||||
type: 'integration',
|
||||
schema: issueSchema,
|
||||
},
|
||||
},
|
||||
identifier: {
|
||||
extractScript: '',
|
||||
},
|
||||
} satisfies Omit<sdk.IntegrationDefinitionProps, 'name'>
|
||||
|
||||
const getHomeDir = (props: { tmpDir: string }) => pathlib.join(props.tmpDir, '.botpresshome')
|
||||
const initBot = async (props: TestProps, definitionFile: string) => {
|
||||
const { tmpDir, dependencies, ...creds } = props
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: getHomeDir(props),
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
const botName = uuid.v4().replace(/-/g, '')
|
||||
const botDir = pathlib.join(tmpDir, botName)
|
||||
await impl
|
||||
.init({ ...argv, workDir: tmpDir, name: botName, type: 'bot', template: 'empty' })
|
||||
.then(utils.handleExitCode)
|
||||
await utils.fixBotpressDependencies({ workDir: botDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: botDir }).then(utils.handleExitCode)
|
||||
await fslib.promises.writeFile(pathlib.join(botDir, 'bot.definition.ts'), definitionFile)
|
||||
return { botDir }
|
||||
}
|
||||
|
||||
// TODO: add an equivalent test with an interface once interfaces can be created by any workspace
|
||||
|
||||
export const addIntegration: Test = {
|
||||
name: 'cli should allow installing an integration',
|
||||
handler: async (props) => {
|
||||
const { tmpDir, workspaceHandle, logger, ...creds } = props
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: getHomeDir({ tmpDir }),
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const bpClient = new client.Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
|
||||
const integrationSuffix = uuid.v4().replace(/-/g, '')
|
||||
const name = `myintegration${integrationSuffix}`
|
||||
const integrationName = `${workspaceHandle}/${name}`
|
||||
|
||||
const createIntegrationBody = await apiUtils.prepareCreateIntegrationBody(
|
||||
new sdk.IntegrationDefinition({
|
||||
...INTEGRATION,
|
||||
name: integrationName,
|
||||
})
|
||||
)
|
||||
|
||||
const { integration } = await bpClient.createIntegration({
|
||||
...createIntegrationBody,
|
||||
dev: true, // this way we ensure the integration will eventually be janitored if the test fails
|
||||
url: creds.apiUrl,
|
||||
})
|
||||
|
||||
try {
|
||||
logger.info('Initializing bot')
|
||||
const { botDir } = await initBot(
|
||||
props,
|
||||
[
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
`import anIntegration from "./bp_modules/${workspaceHandle}-${name}"`,
|
||||
'export default new sdk.BotDefinition({}).addIntegration(anIntegration, {',
|
||||
' enabled: true,',
|
||||
' configurationType: null,',
|
||||
' configuration: {},',
|
||||
'})',
|
||||
].join('\n')
|
||||
)
|
||||
|
||||
logger.info('Logging in')
|
||||
await impl.login(argv).then(utils.handleExitCode)
|
||||
|
||||
logger.info('Installing integration')
|
||||
await impl
|
||||
.add({
|
||||
...argv,
|
||||
installPath: botDir,
|
||||
packageRef: integration.id,
|
||||
useDev: false,
|
||||
alias: undefined,
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
logger.info('Building bot')
|
||||
await impl.build({ ...argv, workDir: botDir }).then(utils.handleExitCode)
|
||||
await utils.tscCheck({ workDir: botDir }).then(utils.handleExitCode)
|
||||
} finally {
|
||||
await impl.integrations
|
||||
.delete({
|
||||
...argv,
|
||||
integrationRef: integration.id,
|
||||
})
|
||||
.catch(() => {
|
||||
logger.warn(`Failed to delete integration ${integration.id}`) // this is not the purpose of the test
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const addPlugin: Test = {
|
||||
name: 'cli should allow installing a plugin',
|
||||
handler: async (props) => {
|
||||
const { tmpDir, workspaceHandle, logger, ...creds } = props
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: getHomeDir({ tmpDir }),
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const bpClient = new client.Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
|
||||
const pluginSuffix = uuid.v4().replace(/-/g, '')
|
||||
const name = `myplugin${pluginSuffix}`
|
||||
const pluginName = `${workspaceHandle}/${name}`
|
||||
const pluginVersion = '0.1.0'
|
||||
const pluginAlias = `alias-${uuid.v4().replace(/-/g, '')}`
|
||||
const botName = uuid.v4()
|
||||
|
||||
const createPluginBody = await apiUtils.prepareCreatePluginBody(
|
||||
new sdk.PluginDefinition({
|
||||
name: pluginName,
|
||||
version: pluginVersion,
|
||||
})
|
||||
)
|
||||
|
||||
const { plugin } = await bpClient.createPlugin({
|
||||
...createPluginBody,
|
||||
code: { browser: 'export default {}', node: 'export default {}' },
|
||||
})
|
||||
|
||||
let bot: ApiBot | undefined
|
||||
|
||||
try {
|
||||
logger.info('Initializing bot')
|
||||
const { botDir } = await initBot(
|
||||
props,
|
||||
[
|
||||
'import * as sdk from "@botpress/sdk"',
|
||||
`import aPlugin from "./bp_modules/${workspaceHandle}-${name}"`,
|
||||
'export default new sdk.BotDefinition({}).addPlugin(aPlugin, {',
|
||||
` alias: '${pluginAlias}',`,
|
||||
' configuration: {},',
|
||||
' dependencies: {},',
|
||||
'})',
|
||||
].join('\n')
|
||||
)
|
||||
|
||||
logger.info('Logging in')
|
||||
await impl.login(argv).then(utils.handleExitCode)
|
||||
|
||||
logger.info('Installing plugin')
|
||||
await impl
|
||||
.add({
|
||||
...argv,
|
||||
installPath: botDir,
|
||||
packageRef: plugin.id,
|
||||
useDev: false,
|
||||
alias: undefined,
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
logger.info('Building bot')
|
||||
await impl.build({ ...argv, workDir: botDir }).then(utils.handleExitCode)
|
||||
await utils.tscCheck({ workDir: botDir }).then(utils.handleExitCode)
|
||||
|
||||
logger.info('Deploying bot')
|
||||
await impl.bots.create({ ...argv, name: botName, ifNotExists: false }).then(utils.handleExitCode)
|
||||
|
||||
bot = await fetchBot(bpClient, botName)
|
||||
if (!bot) {
|
||||
throw new Error(`Bot ${botName} should have been created`)
|
||||
}
|
||||
|
||||
await impl.deploy({ ...argv, workDir: botDir, createNewBot: false, botId: bot.id }).then(utils.handleExitCode)
|
||||
|
||||
logger.info('Checking if plugin is installed')
|
||||
const plugins = await bpClient.getBot({ id: bot.id }).then(({ bot }) => bot.plugins)
|
||||
|
||||
const isPluginInstalled = Object.entries(plugins).some(
|
||||
([alias, instance]) => alias === pluginAlias && instance.id === plugin.id
|
||||
)
|
||||
|
||||
if (!isPluginInstalled) {
|
||||
throw new Error(`Plugin ${plugin.id} should have been installed in bot ${bot.id}`)
|
||||
}
|
||||
} finally {
|
||||
if (bot) {
|
||||
await impl.bots
|
||||
.delete({
|
||||
...argv,
|
||||
botRef: bot.id,
|
||||
})
|
||||
.catch(() => {
|
||||
logger.warn(`Failed to delete bot ${bot!.id}`) // this is not the purpose of the test
|
||||
})
|
||||
}
|
||||
await impl.plugins
|
||||
.delete({
|
||||
...argv,
|
||||
pluginRef: plugin.id,
|
||||
})
|
||||
.catch(() => {
|
||||
logger.warn(`Failed to delete plugin ${plugin.id}`) // this is not the purpose of the test
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const fetchBot = async (client: client.Client, botName: string): Promise<ApiBot | undefined> =>
|
||||
await fetchAllBots(client).then((bots) => bots.find(({ name }) => name === botName))
|
||||
|
||||
const initIntegration = async (props: TestProps, integrationName: string) => {
|
||||
const { tmpDir, dependencies, workspaceHandle, ...creds } = props
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: getHomeDir({ tmpDir }),
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
const integrationDir = pathlib.join(tmpDir, integrationName)
|
||||
await impl
|
||||
.init({
|
||||
...argv,
|
||||
workDir: tmpDir,
|
||||
name: `${workspaceHandle}/${integrationName}`,
|
||||
type: 'integration',
|
||||
template: 'empty',
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
await utils.fixBotpressDependencies({ workDir: integrationDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: integrationDir }).then(utils.handleExitCode)
|
||||
return { integrationDir }
|
||||
}
|
||||
|
||||
export const addLocalIntegrationKeepsRelativePath: Test = {
|
||||
name: 'cli should store the original relative path in bpDependencies when adding a local integration',
|
||||
handler: async (props) => {
|
||||
const { tmpDir, logger, workspaceHandle, ...creds } = props
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: getHomeDir({ tmpDir }),
|
||||
confirm: true,
|
||||
workspaceHandle,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const integrationName = `myintegration${utils.getUUID()}`
|
||||
const fullIntegrationName = `${workspaceHandle}/${integrationName}`
|
||||
const { integrationDir } = await initIntegration(props, integrationName)
|
||||
|
||||
logger.info('Initializing bot')
|
||||
const { botDir } = await initBot(
|
||||
props,
|
||||
['import * as sdk from "@botpress/sdk"', 'export default new sdk.BotDefinition({})'].join('\n')
|
||||
)
|
||||
|
||||
// The stored path should be relative to installPath (botDir), not process.cwd()
|
||||
// Use integrationDir (absolute) as packageRef to avoid cross-drive issues on Windows
|
||||
const rel = pathlib.relative(botDir, integrationDir).split(pathlib.sep).join('/')
|
||||
const expectedStoredPath = rel.startsWith('.') ? rel : `./${rel}`
|
||||
|
||||
logger.info('Adding local integration via relative path')
|
||||
await impl
|
||||
.add({ ...argv, installPath: botDir, packageRef: integrationDir, useDev: false, alias: undefined })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
const pkgJson = JSON.parse(fslib.readFileSync(pathlib.join(botDir, 'package.json'), 'utf8'))
|
||||
const bpDeps = pkgJson.bpDependencies as Record<string, string> | undefined
|
||||
if (!bpDeps) {
|
||||
throw new Error('Expected bpDependencies to be set in package.json after bp add')
|
||||
}
|
||||
const storedPath = bpDeps[fullIntegrationName]
|
||||
if (storedPath !== expectedStoredPath) {
|
||||
throw new Error(`Expected bpDependencies to store "${expectedStoredPath}" but got "${storedPath}"`)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const addDevIntegrationSkipsBpDependencies: Test = {
|
||||
name: 'cli should not modify bpDependencies when adding a local integration with --use-dev',
|
||||
handler: async (props) => {
|
||||
const { tmpDir, logger, workspaceHandle, ...creds } = props
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: getHomeDir({ tmpDir }),
|
||||
confirm: true,
|
||||
workspaceHandle,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const integrationName = `myintegration${utils.getUUID()}`
|
||||
const fullIntegrationName = `${workspaceHandle}/${integrationName}`
|
||||
const { integrationDir } = await initIntegration(props, integrationName)
|
||||
|
||||
// Simulate a dev integration by writing a devId to the project cache
|
||||
const cacheDir = pathlib.join(integrationDir, '.botpress')
|
||||
fslib.mkdirSync(cacheDir, { recursive: true })
|
||||
fslib.writeFileSync(
|
||||
pathlib.join(cacheDir, 'project.cache.json'),
|
||||
JSON.stringify({ devId: 'fake-dev-integration-id' })
|
||||
)
|
||||
|
||||
logger.info('Initializing bot')
|
||||
const { botDir } = await initBot(
|
||||
props,
|
||||
['import * as sdk from "@botpress/sdk"', 'export default new sdk.BotDefinition({})'].join('\n')
|
||||
)
|
||||
|
||||
logger.info('Adding dev integration')
|
||||
await impl
|
||||
.add({ ...argv, installPath: botDir, packageRef: integrationDir, useDev: true, alias: undefined })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
const pkgJson = JSON.parse(fslib.readFileSync(pathlib.join(botDir, 'package.json'), 'utf8'))
|
||||
const bpDeps = pkgJson.bpDependencies as Record<string, string> | undefined
|
||||
if (bpDeps?.[fullIntegrationName] !== undefined) {
|
||||
throw new Error(
|
||||
`Expected "${fullIntegrationName}" to NOT be in bpDependencies when using --use-dev, but got: ${JSON.stringify(bpDeps)}`
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const addDevIntegrationPreservesExistingBpDependency: Test = {
|
||||
name: 'cli should not overwrite existing bpDependencies entry when re-adding as dev integration',
|
||||
handler: async (props) => {
|
||||
const { tmpDir, logger, workspaceHandle, ...creds } = props
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: getHomeDir({ tmpDir }),
|
||||
confirm: true,
|
||||
workspaceHandle,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const integrationName = `myintegration${utils.getUUID()}`
|
||||
const fullIntegrationName = `${workspaceHandle}/${integrationName}`
|
||||
const { integrationDir } = await initIntegration(props, integrationName)
|
||||
|
||||
logger.info('Initializing bot')
|
||||
const { botDir } = await initBot(
|
||||
props,
|
||||
['import * as sdk from "@botpress/sdk"', 'export default new sdk.BotDefinition({})'].join('\n')
|
||||
)
|
||||
|
||||
logger.info('Adding local integration (no --use-dev) to populate bpDependencies')
|
||||
await impl
|
||||
.add({ ...argv, installPath: botDir, packageRef: integrationDir, useDev: false, alias: undefined })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
const pkgJsonAfterFirst = JSON.parse(fslib.readFileSync(pathlib.join(botDir, 'package.json'), 'utf8'))
|
||||
const storedRelativePath = (pkgJsonAfterFirst.bpDependencies as Record<string, string> | undefined)?.[
|
||||
fullIntegrationName
|
||||
]
|
||||
if (!storedRelativePath) {
|
||||
throw new Error('Expected bpDependencies to contain an entry after initial bp add')
|
||||
}
|
||||
|
||||
// Simulate the integration being run with bp dev (devId present in cache)
|
||||
const cacheDir = pathlib.join(integrationDir, '.botpress')
|
||||
fslib.mkdirSync(cacheDir, { recursive: true })
|
||||
fslib.writeFileSync(
|
||||
pathlib.join(cacheDir, 'project.cache.json'),
|
||||
JSON.stringify({ devId: 'fake-dev-integration-id' })
|
||||
)
|
||||
|
||||
logger.info('Re-adding same integration with --use-dev')
|
||||
await impl
|
||||
.add({ ...argv, installPath: botDir, packageRef: integrationDir, useDev: true, alias: undefined })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
const pkgJsonAfterDev = JSON.parse(fslib.readFileSync(pathlib.join(botDir, 'package.json'), 'utf8'))
|
||||
const bpDepsAfterDev = pkgJsonAfterDev.bpDependencies as Record<string, string> | undefined
|
||||
if (bpDepsAfterDev?.[fullIntegrationName] !== storedRelativePath) {
|
||||
throw new Error(
|
||||
`Expected bpDependencies["${fullIntegrationName}"] to remain "${storedRelativePath}" after --use-dev add, but got: ${JSON.stringify(bpDepsAfterDev?.[fullIntegrationName])}`
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const reinstallLocalIntegration: Test = {
|
||||
name: 'cli should reinstall local integration from bpDependencies regardless of cwd',
|
||||
handler: async (props) => {
|
||||
const { tmpDir, logger, workspaceHandle, ...creds } = props
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: getHomeDir({ tmpDir }),
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const integrationName = `myintegration${utils.getUUID()}`
|
||||
const fullIntegrationName = `${workspaceHandle}/${integrationName}`
|
||||
const { integrationDir } = await initIntegration(props, integrationName)
|
||||
|
||||
logger.info('Initializing bot')
|
||||
const { botDir } = await initBot(
|
||||
props,
|
||||
['import * as sdk from "@botpress/sdk"', 'export default new sdk.BotDefinition({})'].join('\n')
|
||||
)
|
||||
|
||||
logger.info('Adding local integration')
|
||||
await impl
|
||||
.add({ ...argv, installPath: botDir, packageRef: integrationDir, useDev: false, alias: undefined })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
const moduleDir = pathlib.join(botDir, consts.installDirName, cliUtils.casing.to.kebabCase(fullIntegrationName))
|
||||
if (!fslib.existsSync(moduleDir)) {
|
||||
throw new Error(`Expected bp_modules to contain "${fullIntegrationName}" after bp add`)
|
||||
}
|
||||
|
||||
logger.info('Deleting bp_modules')
|
||||
fslib.rmSync(pathlib.join(botDir, 'bp_modules'), { recursive: true, force: true })
|
||||
|
||||
logger.info('Reinstalling from bpDependencies')
|
||||
await impl
|
||||
.add({ ...argv, installPath: botDir, packageRef: undefined, useDev: false, alias: undefined })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
if (!fslib.existsSync(moduleDir)) {
|
||||
throw new Error(`Expected bp_modules to contain "${fullIntegrationName}" after reinstall`)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import pathlib from 'path'
|
||||
import impl from '../../src'
|
||||
import defaults from '../defaults'
|
||||
import { Test } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
export const installAllPlugins: Test = {
|
||||
name: 'cli should allow installing public plugins',
|
||||
handler: async ({ tmpDir, logger, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'plugins')
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
|
||||
const plugins: string[] = ['hitl', 'file-synchronizer']
|
||||
|
||||
for (const iface of plugins) {
|
||||
logger.info(`Installing plugin: ${iface}`)
|
||||
await impl
|
||||
.add({
|
||||
...argv,
|
||||
packageRef: iface,
|
||||
installPath: baseDir,
|
||||
useDev: false,
|
||||
alias: undefined,
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Client } from '@botpress/client'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import fs from 'fs'
|
||||
import pathlib from 'path'
|
||||
import * as uuid from 'uuid'
|
||||
import impl from '../../src'
|
||||
import { fetchAllIntegrations, ApiIntegration } from '../api'
|
||||
import defaults from '../defaults'
|
||||
import * as retry from '../retry'
|
||||
import { Test } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
type SecretDef = NonNullable<sdk.IntegrationDefinitionProps['secrets']>
|
||||
|
||||
const fetchIntegration = async (client: Client, integrationName: string): Promise<ApiIntegration | undefined> => {
|
||||
const integrations = await fetchAllIntegrations(client)
|
||||
return integrations.find(({ name }) => name === integrationName)
|
||||
}
|
||||
|
||||
const appendSecretDefinition = (originalTsContent: string, secrets: SecretDef): string => {
|
||||
const regex = /( *)version: (['"].*['"]),/
|
||||
const replacement = [
|
||||
'version: $2,',
|
||||
'secrets: {',
|
||||
...Object.entries(secrets).map(([secretName, secretDef]) => ` ${secretName}: ${JSON.stringify(secretDef)},`),
|
||||
'},',
|
||||
]
|
||||
.map((s) => `$1${s}`) // for indentation
|
||||
.join('\n')
|
||||
|
||||
const modifiedTsContent = originalTsContent.replace(regex, replacement)
|
||||
return modifiedTsContent
|
||||
}
|
||||
|
||||
export const requiredIntegrationSecrets: Test = {
|
||||
name: 'cli should require required integration secrets',
|
||||
handler: async ({ tmpDir, workspaceHandle, dependencies, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'integrations')
|
||||
|
||||
const integrationSuffix = uuid.v4().replace(/-/g, '')
|
||||
const name = `myintegration${integrationSuffix}`
|
||||
const integrationName = `${workspaceHandle}/${name}`
|
||||
const integrationDir = pathlib.join(baseDir, name)
|
||||
|
||||
const definitionPath = pathlib.join(integrationDir, 'integration.definition.ts')
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
|
||||
await impl
|
||||
.init({ ...argv, workDir: baseDir, name: integrationName, type: 'integration', template: 'empty' })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
const originalDefinition: string = fs.readFileSync(definitionPath, 'utf-8')
|
||||
const modifiedDefinition = appendSecretDefinition(originalDefinition, {
|
||||
REQUIRED_SECRET: {},
|
||||
OPTIONAL_SECRET: { optional: true },
|
||||
})
|
||||
fs.writeFileSync(definitionPath, modifiedDefinition, 'utf-8')
|
||||
|
||||
await utils.fixBotpressDependencies({ workDir: integrationDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: integrationDir }).then(utils.handleExitCode)
|
||||
await impl.build({ ...argv, workDir: integrationDir }).then(utils.handleExitCode)
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
|
||||
const { exitCode } = await impl.deploy({
|
||||
...argv,
|
||||
workDir: integrationDir,
|
||||
secrets: ['OPTIONAL_SECRET=lol'],
|
||||
botId: undefined,
|
||||
createNewBot: undefined,
|
||||
})
|
||||
if (exitCode === 0) {
|
||||
throw new Error('Expected deploy to fail')
|
||||
}
|
||||
|
||||
await impl
|
||||
.deploy({
|
||||
...argv,
|
||||
workDir: integrationDir,
|
||||
secrets: ['REQUIRED_SECRET=lol'],
|
||||
botId: undefined,
|
||||
createNewBot: undefined,
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
// cleanup deployed integration
|
||||
const { integration: deployedIntegration } = await client.getIntegrationByName({
|
||||
name: integrationName,
|
||||
version: '0.1.0',
|
||||
})
|
||||
if (!deployedIntegration.secrets.includes('REQUIRED_SECRET')) {
|
||||
throw new Error(
|
||||
`Integration ${integrationName} should have secret REQUIRED_SECRET, got: ${deployedIntegration.secrets.join(', ')}`
|
||||
)
|
||||
}
|
||||
await client.deleteIntegration({ id: deployedIntegration.id })
|
||||
|
||||
if (await fetchIntegration(client, integrationName)) {
|
||||
throw new Error(`Integration ${integrationName} should have been deleted`)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { Client } from '@botpress/client'
|
||||
import * as fs from 'fs'
|
||||
import pathlib from 'path'
|
||||
import impl from '../../src'
|
||||
import { ApiIntegration, fetchAllIntegrations, ApiPlugin, fetchAllPlugins } from '../api'
|
||||
import defaults from '../defaults'
|
||||
import * as retry from '../retry'
|
||||
import { Test } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
const fetchIntegration = async (client: Client, integrationName: string): Promise<ApiIntegration | undefined> => {
|
||||
const integrations = await fetchAllIntegrations(client)
|
||||
return integrations.find(({ name }) => name === integrationName)
|
||||
}
|
||||
|
||||
const fetchPlugin = async (client: Client, pluginName: string): Promise<ApiPlugin | undefined> => {
|
||||
const plugins = await fetchAllPlugins(client)
|
||||
return plugins.find(({ name }) => name === pluginName)
|
||||
}
|
||||
|
||||
export const prependWorkspaceHandleIntegration: Test = {
|
||||
name: 'cli should automatically preprend the workspace handle to the integration name when deploying',
|
||||
handler: async ({ tmpDir, dependencies, workspaceHandle, logger, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'integrations')
|
||||
|
||||
const integrationSuffix = utils.getUUID()
|
||||
const integrationName = `myintegration${integrationSuffix}`
|
||||
const integrationNameWithHandle = `${workspaceHandle}/${integrationName}`
|
||||
const integrationDirName = integrationName
|
||||
const integrationDir = pathlib.join(baseDir, integrationDirName)
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
|
||||
await impl
|
||||
.init({ ...argv, workDir: baseDir, name: integrationNameWithHandle, type: 'integration', template: 'empty' })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
// Remove handle from package.json:
|
||||
const pkgJsonPath = pathlib.join(integrationDir, 'package.json')
|
||||
const pkgJson = await fs.promises.readFile(pkgJsonPath, 'utf-8').then(JSON.parse)
|
||||
pkgJson.name = pkgJson.name.slice(workspaceHandle.length + 1)
|
||||
await fs.promises.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2))
|
||||
|
||||
await utils.fixBotpressDependencies({ workDir: integrationDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: integrationDir }).then(utils.handleExitCode)
|
||||
await impl.build({ ...argv, workDir: integrationDir }).then(utils.handleExitCode)
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
|
||||
await impl
|
||||
.deploy({ ...argv, createNewBot: undefined, botId: undefined, workDir: integrationDir })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
logger.debug(`Fetching integration "${integrationName}"`)
|
||||
let integration = await fetchIntegration(client, integrationName)
|
||||
if (integration) {
|
||||
throw new Error(`Integration ${integrationName} should not have been created`)
|
||||
}
|
||||
|
||||
const expectedIntegrationName = `${workspaceHandle}/${integrationName}`
|
||||
logger.debug(`Fetching integration "${expectedIntegrationName}"`)
|
||||
integration = await fetchIntegration(client, expectedIntegrationName)
|
||||
if (!integration) {
|
||||
throw new Error(`Integration ${expectedIntegrationName} should have been created`)
|
||||
}
|
||||
|
||||
logger.debug(`Deleting integration "${integrationName}"`)
|
||||
await impl.integrations.delete({ ...argv, integrationRef: integration.id }).then(({ exitCode }) => {
|
||||
exitCode !== 0 && logger.warn(`Failed to delete integration "${integrationName}"`) // not enough to fail the test
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export const prependWorkspaceHandlePlugin: Test = {
|
||||
name: 'cli should automatically prepend the workspace handle to the plugin name when deploying',
|
||||
handler: async ({ tmpDir, dependencies, workspaceHandle, logger, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'plugins')
|
||||
|
||||
const pluginSuffix = utils.getUUID()
|
||||
const pluginName = `myplugin${pluginSuffix}`
|
||||
const pluginNameWithHandle = `${workspaceHandle}/${pluginName}`
|
||||
const pluginDir = pathlib.join(baseDir, pluginName)
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
|
||||
await impl
|
||||
.init({ ...argv, workDir: baseDir, name: pluginNameWithHandle, type: 'plugin', template: 'empty' })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
// Remove handle from package.json pluginName field:
|
||||
const pkgJsonPath = pathlib.join(pluginDir, 'package.json')
|
||||
const pkgJson = await fs.promises.readFile(pkgJsonPath, 'utf-8').then(JSON.parse)
|
||||
pkgJson.pluginName = pluginName
|
||||
await fs.promises.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2))
|
||||
|
||||
await utils.fixBotpressDependencies({ workDir: pluginDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: pluginDir }).then(utils.handleExitCode)
|
||||
await impl.build({ ...argv, workDir: pluginDir }).then(utils.handleExitCode)
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
|
||||
await impl
|
||||
.deploy({ ...argv, createNewBot: undefined, botId: undefined, workDir: pluginDir })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
logger.debug(`Fetching plugin "${pluginName}"`)
|
||||
let plugin = await fetchPlugin(client, pluginName)
|
||||
if (plugin) {
|
||||
throw new Error(`Plugin ${pluginName} should not have been created without handle prefix`)
|
||||
}
|
||||
|
||||
const expectedPluginName = `${workspaceHandle}/${pluginName}`
|
||||
logger.debug(`Fetching plugin "${expectedPluginName}"`)
|
||||
plugin = await fetchPlugin(client, expectedPluginName)
|
||||
if (!plugin) {
|
||||
throw new Error(`Plugin ${expectedPluginName} should have been created`)
|
||||
}
|
||||
|
||||
logger.debug(`Deleting plugin "${expectedPluginName}"`)
|
||||
await impl.plugins.delete({ ...argv, pluginRef: plugin.id }).then(({ exitCode }) => {
|
||||
exitCode !== 0 && logger.warn(`Failed to delete plugin "${expectedPluginName}"`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export const enforceWorkspaceHandleIntegration: Test = {
|
||||
name: 'cli should fail when attempting to deploy an integration with incorrect workspace handle',
|
||||
handler: async ({ tmpDir, dependencies, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'integrations')
|
||||
|
||||
const randomSuffix = utils.getUUID().slice(0, 8)
|
||||
|
||||
const name = 'myintegration'
|
||||
const handle = `myhandle${randomSuffix}`
|
||||
const integrationName = `${handle}/${name}`
|
||||
const integrationDir = pathlib.join(baseDir, name)
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
await impl
|
||||
.init({ ...argv, workDir: baseDir, name: integrationName, type: 'integration', template: 'empty' })
|
||||
.then(utils.handleExitCode)
|
||||
await utils.fixBotpressDependencies({ workDir: integrationDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: integrationDir }).then(utils.handleExitCode)
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
|
||||
const { exitCode } = await impl.deploy({
|
||||
...argv,
|
||||
createNewBot: undefined,
|
||||
botId: undefined,
|
||||
workDir: integrationDir,
|
||||
})
|
||||
|
||||
if (exitCode === 0) {
|
||||
throw new Error(`Integration ${integrationName} should not have been deployed`)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const enforceWorkspaceHandlePlugin: Test = {
|
||||
name: 'cli should fail when attempting to deploy a plugin with incorrect workspace handle',
|
||||
handler: async ({ tmpDir, dependencies, ...creds }) => {
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const baseDir = pathlib.join(tmpDir, 'plugins')
|
||||
|
||||
const randomSuffix = utils.getUUID().slice(0, 8)
|
||||
|
||||
const name = 'myplugin'
|
||||
const handle = `myhandle${randomSuffix}`
|
||||
const pluginName = `${handle}/${name}`
|
||||
const pluginDir = pathlib.join(baseDir, name)
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
|
||||
await impl
|
||||
.init({ ...argv, workDir: baseDir, name: pluginName, type: 'plugin', template: 'empty' })
|
||||
.then(utils.handleExitCode)
|
||||
await utils.fixBotpressDependencies({ workDir: pluginDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: pluginDir }).then(utils.handleExitCode)
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
|
||||
const { exitCode } = await impl.deploy({
|
||||
...argv,
|
||||
createNewBot: undefined,
|
||||
botId: undefined,
|
||||
workDir: pluginDir,
|
||||
})
|
||||
|
||||
if (exitCode === 0) {
|
||||
throw new Error(`Plugin ${pluginName} should not have been deployed`)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import * as fslib from 'fs'
|
||||
import * as pathlib from 'path'
|
||||
import * as uuid from 'uuid'
|
||||
import impl from '../../src'
|
||||
import defaults from '../defaults'
|
||||
import { Test, TestProps } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
const getHomeDir = (props: { tmpDir: string }) => pathlib.join(props.tmpDir, '.botpresshome')
|
||||
const initBot = async (props: TestProps, definitionFile: string) => {
|
||||
const { tmpDir, dependencies, ...creds } = props
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: getHomeDir(props),
|
||||
confirm: true,
|
||||
...creds,
|
||||
}
|
||||
const botName = uuid.v4().replace(/-/g, '')
|
||||
const botDir = pathlib.join(tmpDir, botName)
|
||||
await impl
|
||||
.init({ ...argv, workDir: tmpDir, name: botName, type: 'bot', template: 'empty' })
|
||||
.then(utils.handleExitCode)
|
||||
await utils.fixBotpressDependencies({ workDir: botDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir: botDir }).then(utils.handleExitCode)
|
||||
fslib.writeFileSync(pathlib.join(botDir, 'bot.definition.ts'), definitionFile)
|
||||
return { botDir }
|
||||
}
|
||||
let botDir: string | undefined = undefined
|
||||
const ALIAS = 'alias'
|
||||
export const removePackage: Test = {
|
||||
name: 'cli should allow removing a plugin',
|
||||
handler: async (props) => {
|
||||
try {
|
||||
const botpressHomeDir = pathlib.join(props.tmpDir, '.botpresshome')
|
||||
|
||||
const initializedBot = await initBot(
|
||||
props,
|
||||
['import * as sdk from "@botpress/sdk"', 'export default new sdk.BotDefinition({})'].join('\n')
|
||||
)
|
||||
botDir = initializedBot.botDir
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...props,
|
||||
}
|
||||
|
||||
await impl
|
||||
.login({
|
||||
...argv,
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
const plugin: string = 'hitl'
|
||||
|
||||
props.logger.info(`Installing plugin: ${plugin}`)
|
||||
await impl
|
||||
.add({
|
||||
...argv,
|
||||
packageRef: plugin,
|
||||
installPath: botDir,
|
||||
useDev: false,
|
||||
alias: ALIAS,
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
await impl.remove({
|
||||
...argv,
|
||||
alias: ALIAS,
|
||||
workDir: botDir,
|
||||
})
|
||||
|
||||
const aliasPath = pathlib.join(botDir, 'bp_modules', ALIAS)
|
||||
const exists = await fslib.promises
|
||||
.access(aliasPath)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
if (exists) {
|
||||
throw new Error(`Expected ${aliasPath} to not exist`)
|
||||
}
|
||||
|
||||
const pkgJsonPath = pathlib.join(botDir, 'package.json')
|
||||
const pkgJson = JSON.parse(fslib.readFileSync(pkgJsonPath, 'utf-8'))
|
||||
if (pkgJson.bpDependencies && Object.keys(pkgJson.bpDependencies).length !== 0) {
|
||||
throw new Error('Expected bpDependencies to be empty')
|
||||
}
|
||||
} finally {
|
||||
if (botDir) fslib.rmSync(botDir, { force: true, recursive: true })
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Logger } from '@bpinternal/log4bot'
|
||||
|
||||
export type TestProps = {
|
||||
logger: Logger
|
||||
tmpDir: string
|
||||
workspaceId: string
|
||||
workspaceHandle: string
|
||||
token: string
|
||||
apiUrl: string
|
||||
tunnelUrl: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
sdkPath?: string
|
||||
clientPath?: string
|
||||
}
|
||||
|
||||
export type Test = {
|
||||
name: string
|
||||
handler: (props: TestProps) => Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import childprocess from 'child_process'
|
||||
import fs from 'fs'
|
||||
import _ from 'lodash'
|
||||
import pathlib from 'path'
|
||||
import tmp from 'tmp'
|
||||
import * as uuid from 'uuid'
|
||||
|
||||
type PackageJson = {
|
||||
name: string
|
||||
version?: string
|
||||
description?: string
|
||||
scripts?: Record<string, string>
|
||||
dependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
peerDependencies?: Record<string, string>
|
||||
}
|
||||
|
||||
export const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
export class TmpDirectory {
|
||||
private _closed = false
|
||||
|
||||
public static create() {
|
||||
return new TmpDirectory(tmp.dirSync({ unsafeCleanup: true }))
|
||||
}
|
||||
|
||||
private constructor(private _res: tmp.DirResult) {}
|
||||
|
||||
public get path() {
|
||||
if (this._closed) {
|
||||
throw new Error('Cannot access tmp directory after cleanup')
|
||||
}
|
||||
return this._res.name
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
if (this._closed) {
|
||||
return
|
||||
}
|
||||
this._res.removeCallback()
|
||||
}
|
||||
}
|
||||
|
||||
export type RunCommandOptions = {
|
||||
workDir: string
|
||||
}
|
||||
|
||||
export type RunCommandOutput = {
|
||||
exitCode: number
|
||||
}
|
||||
|
||||
export const runCommand = async (cmd: string, { workDir }: RunCommandOptions): Promise<RunCommandOutput> => {
|
||||
const [program, ...args] = cmd.split(' ')
|
||||
if (!program) {
|
||||
throw new Error('Cannot run empty command')
|
||||
}
|
||||
const { error, status } = childprocess.spawnSync(program, args, {
|
||||
cwd: workDir,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: process.env,
|
||||
})
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
return { exitCode: status ?? 0 }
|
||||
}
|
||||
|
||||
export const npmInstall = ({ workDir }: RunCommandOptions): Promise<RunCommandOutput> => {
|
||||
return runCommand('pnpm install', { workDir })
|
||||
}
|
||||
|
||||
export const tscCheck = ({ workDir }: RunCommandOptions): Promise<RunCommandOutput> => {
|
||||
return runCommand('tsc --noEmit', { workDir })
|
||||
}
|
||||
|
||||
export const fixBotpressDependencies = async ({
|
||||
workDir,
|
||||
target,
|
||||
}: {
|
||||
workDir: string
|
||||
target: Record<string, string | undefined>
|
||||
}) => {
|
||||
const packageJsonPath = pathlib.join(workDir, 'package.json')
|
||||
const originalPackageJson: PackageJson = require(packageJsonPath)
|
||||
|
||||
const newPackageJson = {
|
||||
...originalPackageJson,
|
||||
dependencies: _.mapValues(originalPackageJson.dependencies ?? {}, (version, name) => target[name] ?? version),
|
||||
}
|
||||
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(newPackageJson, null, 2))
|
||||
}
|
||||
|
||||
export const handleExitCode = ({ exitCode }: { exitCode: number }) => {
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`Command exited with code ${exitCode}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const getUUID = () => uuid.v4().replace(/-/g, '')
|
||||
Reference in New Issue
Block a user