-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #130 from Hexastack/feat/cli
Feat/cli
- Loading branch information
Showing
7 changed files
with
573 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
{ | ||
"name": "hexabot-cli", | ||
"version": "2.0.0", | ||
"description": "Hexabot CLI for creating and managing chatbots built with Hexabot.", | ||
"main": "dist/index.js", | ||
"type": "module", | ||
"bin": { | ||
"hexabot": "dist/index.js" | ||
}, | ||
"scripts": { | ||
"build": "tsc", | ||
"start": "node dist/cli.js", | ||
"dev": "ts-node src/cli.ts", | ||
"prepare": "npm run build" | ||
}, | ||
"keywords": [], | ||
"author": "Hexastack", | ||
"license": "AGPL-3.0-only", | ||
"dependencies": { | ||
"chalk": "^5.3.0", | ||
"commander": "^12.1.0", | ||
"degit": "^2.8.4", | ||
"dotenv": "^16.4.5", | ||
"figlet": "^1.7.0" | ||
}, | ||
"devDependencies": { | ||
"@types/chalk": "^2.2.0", | ||
"@types/commander": "^2.12.2", | ||
"@types/degit": "^2.8.6", | ||
"@types/figlet": "^1.5.8", | ||
"@types/node": "^22.7.4", | ||
"ts-node": "^10.9.2", | ||
"typescript": "^5.6.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
#!/usr/bin/env node | ||
|
||
import figlet from 'figlet'; | ||
import { Command } from 'commander'; | ||
import { execSync } from 'child_process'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import chalk from 'chalk'; | ||
import degit from 'degit'; | ||
|
||
console.log(figlet.textSync('Hexabot')); | ||
|
||
// Configuration | ||
const FOLDER = path.resolve(process.cwd(), './docker'); | ||
|
||
/** | ||
* Check if the docker folder exists, otherwise prompt the user to cd into the correct folder. | ||
*/ | ||
const checkDockerFolder = (): void => { | ||
if (!fs.existsSync(FOLDER)) { | ||
console.error( | ||
chalk.red( | ||
`Error: The 'docker' folder is not found in the current directory.`, | ||
), | ||
); | ||
console.error( | ||
chalk.yellow( | ||
`Please make sure you're in the Hexabot project directory and try again.`, | ||
), | ||
); | ||
console.log(chalk.cyan(`Example: cd path/to/hexabot`)); | ||
process.exit(1); // Exit the script if the folder is not found | ||
} | ||
}; | ||
|
||
// Initialize Commander | ||
const program = new Command(); | ||
|
||
// Helper Functions | ||
|
||
/** | ||
* Generate Docker Compose file arguments based on provided services. | ||
* @param services List of services | ||
* @param type Optional type ('dev' | 'prod') | ||
* @returns String of Docker Compose file arguments | ||
*/ | ||
const generateComposeFiles = ( | ||
services: string[], | ||
type?: 'dev' | 'prod', | ||
): string => { | ||
let files = [`-f ${path.join(FOLDER, 'docker-compose.yml')}`]; | ||
|
||
services.forEach((service) => { | ||
files.push(`-f ${path.join(FOLDER, `docker-compose.${service}.yml`)}`); | ||
if (type) { | ||
const serviceTypeFile = path.join( | ||
FOLDER, | ||
`docker-compose.${service}.${type}.yml`, | ||
); | ||
if (fs.existsSync(serviceTypeFile)) { | ||
files.push(`-f ${serviceTypeFile}`); | ||
} | ||
} | ||
}); | ||
|
||
if (type) { | ||
const mainTypeFile = path.join(FOLDER, `docker-compose.${type}.yml`); | ||
if (fs.existsSync(mainTypeFile)) { | ||
files.push(`-f ${mainTypeFile}`); | ||
} | ||
} | ||
|
||
return files.join(' '); | ||
}; | ||
|
||
/** | ||
* Execute a Docker Compose command. | ||
* @param args Additional arguments for the docker compose command | ||
*/ | ||
const dockerCompose = (args: string): void => { | ||
try { | ||
execSync(`docker compose ${args}`, { stdio: 'inherit' }); | ||
} catch (error) { | ||
console.error(chalk.red('Error executing Docker Compose command.')); | ||
process.exit(1); | ||
} | ||
}; | ||
|
||
/** | ||
* Parse the comma-separated service list. | ||
* @param serviceString Comma-separated list of services | ||
* @returns Array of services | ||
*/ | ||
const parseServices = (serviceString: string): string[] => { | ||
return serviceString | ||
.split(',') | ||
.map((service) => service.trim()) | ||
.filter((s) => s); | ||
}; | ||
|
||
// Check if the docker folder exists | ||
checkDockerFolder(); | ||
|
||
// Commands | ||
|
||
program | ||
.name('Hexabot') | ||
.description('A CLI to manage your Hexabot chatbot instance') | ||
.version('1.0.0'); | ||
|
||
program | ||
.command('start') | ||
.description('Start specified services with Docker Compose') | ||
.option( | ||
'--enable <services>', | ||
'Comma-separated list of services to enable', | ||
'', | ||
) | ||
.action((options) => { | ||
const services = parseServices(options.enable); | ||
const composeArgs = generateComposeFiles(services); | ||
dockerCompose(`${composeArgs} up -d`); | ||
}); | ||
|
||
program | ||
.command('dev') | ||
.description( | ||
'Start specified services in development mode with Docker Compose', | ||
) | ||
.option( | ||
'--enable <services>', | ||
'Comma-separated list of services to enable', | ||
'', | ||
) | ||
.action((options) => { | ||
const services = parseServices(options.enable); | ||
const composeArgs = generateComposeFiles(services, 'dev'); | ||
dockerCompose(`${composeArgs} up --build -d`); | ||
}); | ||
|
||
program | ||
.command('start-prod') | ||
.description( | ||
'Start specified services in production mode with Docker Compose', | ||
) | ||
.option( | ||
'--enable <services>', | ||
'Comma-separated list of services to enable', | ||
'', | ||
) | ||
.action((options) => { | ||
const services = parseServices(options.enable); | ||
const composeArgs = generateComposeFiles(services, 'prod'); | ||
dockerCompose(`${composeArgs} up -d`); | ||
}); | ||
|
||
program | ||
.command('stop') | ||
.description('Stop specified Docker Compose services') | ||
.option('--enable <services>', 'Comma-separated list of services to stop', '') | ||
.action((options) => { | ||
const services = parseServices(options.enable); | ||
const composeArgs = generateComposeFiles(services); | ||
dockerCompose(`${composeArgs} down`); | ||
}); | ||
|
||
program | ||
.command('destroy') | ||
.description('Destroy specified Docker Compose services and remove volumes') | ||
.option( | ||
'--enable <services>', | ||
'Comma-separated list of services to destroy', | ||
'', | ||
) | ||
.action((options) => { | ||
const services = parseServices(options.enable); | ||
const composeArgs = generateComposeFiles(services); | ||
dockerCompose(`${composeArgs} down -v`); | ||
}); | ||
|
||
// Add install command to install extensions (e.g., channels, plugins) | ||
program | ||
.command('install') | ||
.description('Install an extension for Hexabot') | ||
.argument('<type>', 'The type of extension (e.g., channel, plugin)') | ||
.argument( | ||
'<repository>', | ||
'GitHub repository for the extension (user/repo format)', | ||
) | ||
.action(async (type, repository) => { | ||
// Define the target folder based on the extension type | ||
let targetFolder = ''; | ||
switch (type) { | ||
case 'channel': | ||
targetFolder = 'api/src/extensions/channels/'; | ||
break; | ||
case 'plugin': | ||
targetFolder = 'api/src/extensions/plugins/'; | ||
break; | ||
default: | ||
console.error(chalk.red(`Unknown extension type: ${type}`)); | ||
process.exit(1); | ||
} | ||
|
||
// Get the last part of the repository name | ||
const repoName = repository.split('/').pop(); | ||
|
||
// If the repo name starts with "hexabot-channel-", remove that prefix | ||
const extensionName = repoName.startsWith('hexabot-channel-') | ||
? repoName.replace('hexabot-channel-', '') | ||
: repoName; | ||
|
||
const extensionPath = path.resolve( | ||
process.cwd(), | ||
targetFolder, | ||
extensionName, | ||
); | ||
|
||
// Check if the extension folder already exists | ||
if (fs.existsSync(extensionPath)) { | ||
console.error( | ||
chalk.red(`Error: Extension already exists at ${extensionPath}`), | ||
); | ||
process.exit(1); | ||
} | ||
|
||
try { | ||
console.log( | ||
chalk.cyan(`Fetching ${repository} into ${extensionPath}...`), | ||
); | ||
|
||
// Use degit to fetch the repository without .git history | ||
const emitter = degit(repository); | ||
await emitter.clone(extensionPath); | ||
|
||
console.log(chalk.cyan('Running npm install in the api/ folder...')); | ||
// Run npm install in the api folder to install dependencies | ||
execSync('npm install', { | ||
cwd: path.resolve(process.cwd(), 'api'), | ||
stdio: 'inherit', | ||
}); | ||
|
||
console.log( | ||
chalk.green(`Successfully installed ${extensionName} as a ${type}.`), | ||
); | ||
} catch (error) { | ||
console.error(chalk.red('Error during installation:'), error); | ||
process.exit(1); | ||
} | ||
}); | ||
|
||
// Parse arguments | ||
program.parse(process.argv); | ||
|
||
// If no command is provided, display help | ||
if (!process.argv.slice(2).length) { | ||
program.outputHelp(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "ES2020", | ||
"module": "ES2020", // Change to ES2020 or ESNext | ||
"moduleResolution": "node", // Ensure module resolution is node | ||
"rootDir": "./src", | ||
"outDir": "./dist", | ||
"strict": true, | ||
"esModuleInterop": true, | ||
"forceConsistentCasingInFileNames": true, | ||
"skipLibCheck": true | ||
}, | ||
"include": ["src"], | ||
"exclude": ["node_modules", "dist"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.