This guide explains how to develop an external plugin for Contentstack CLI, including requirements, project structure, best practices, testing, publishing steps, and key internal behaviors to consider.
The Contentstack CLI supports modular extensibility through external plugins.
These plugins offer the following capabilities:
Example plugin: @contentstack/apps-cli
A well-organized plugin should follow the recommended directory layout:
my-plugin/
├── src/
│ └── commands/
│ └── mycommand.ts
├── test/
│ └── commands/
│ └── mycommand.test.ts
├── .eslintrc.js
├── package.json
├── tsconfig.json
├── README.md
└── oclif.manifest.jsonNote: Use namespacing to prefix all commands to avoid collision. For example, use csdx myplugin:do instead of csdx do.
Use oclif's generator to create your plugin:
npx oclif generate myplugin cd myplugin
The generator prompts you with the following questions:
After answering these prompts, the generator creates the plugin structure in your current directory.
Modify the generated package.json to include the necessary oclif configuration:
{
"name": "myplugin",
"version": "1.0.0",
"oclif": {
"plugins": [],
"commands": "./src/commands"
},
"dependencies": {
"@oclif/core": "^3.0.0"
}
}cd <plugin-directory>
// Example:
cd ./mypluginnpx oclif generate command myplugin:doThis creates src/commands/myplugin/do.ts.
Before linking or using your plugin, you must build it to compile TypeScript to JavaScript:
npm run build
This command compiles your TypeScript commands from src/commands/ to dist/commands/.
During development, oclif reads from src. When published, the CLI loads compiled commands from dist (based on your build settings).
After building the plugin, generate the oclif manifest file by running the following command:
npx oclif manifest
This command creates the oclif.manifest.json file, which is required for the CLI to discover your commands.
When developing a plugin locally, you must link it to the Contentstack CLI for testing:
cd <plugin-directory>
csdx plugins:linkThis sets up the plugin within the csdx namespace, allowing you to use your custom commands directly.
Test that your plugin is properly linked by running the following command:
csdx myplugin:doYou should see your plugin's output in the terminal.
During development, you can use the development mode, which automatically transpiles TypeScript as you make changes.
Alternatively, run the plugin directly using Node.js:
node bin/run myplugin:doTip: After making changes to your plugin, rebuild and regenerate the manifest, then test with csdx myplugin:do to ensure everything works correctly.
Each command in your plugin must follow the oclif command structure:
import {Command, Flags} from '@oclif/core'
export default class MyCommand extends Command {
static description = 'Performs operations with Contentstack'
static flags = {
stack: Flags.string({
char: 's',
description: 'Stack API key',
required: true,
}),
help: Flags.help({char: 'h'}),
}
async run() {
const {flags} = await this.parse(MyCommand)
this.log(`Working with stack: ${flags.stack}`)
}
}Use @oclif/test with Mocha or Jest for testing your commands.
import {expect, test} from '@oclif/test'
describe('myplugin:do', () => {
test
.stdout()
.command(['myplugin:do', '--stack', 'dummy_key'])
.it('runs myplugin:do', ctx => {
expect(ctx.stdout).to.contain('Working with stack: dummy_key')
})
})Run Tests
npm test
To simulate the end-user experience, follow these steps to test your published plugin:
npm i -g @contentstack/cli
csdx config:set:region <region-name>csdx login
csdx plugins:install @contentstack/myplugincsdx myplugin:do --helpTest your plugin during development:
npm i -g @contentstack/cli
csdx config:set:region <region-name>csdx login
csdx plugins:link <plugin-local-path>csdx myplugin:do --helpCore plugins handle region and authentication automatically. Before using any command, ensure the user has configured the region and completed authentication.
csdx config:set:region <region-name>csdx login
The @contentstack/cli package provides additional features that plugins can leverage:
| Feature | Description |
|---|---|
| cdaHost, cmaHost, region | Region-aware endpoints |
| authToken, email, region, config | Auto-loaded from CSDX config |
| utilities | Common helpers for I/O, logging, formatting, etc. |
| Shared base command classes | Consistent CLI behavior |
npm publish
csdx plugins:install @contentstack/mypluginOnce installed, users can use your custom plugin to execute tasks.
| Practice | Description |
|---|---|
| Use namespacing | Prefix commands like myplugin:action to avoid collisions. |
| Follow oclif standards | Maintain command/flag conventions for consistency. |
| Use proper CLI feedback | Use this.log, this.error, ux.prompt for user interaction. |
| Validate inputs | Check required flags/args early in your command logic. |
| Add tests | Include basic tests for every command. |
| Document commands | Add descriptions, usage, and examples. |
| Use Contentstack SDKs | Prefer official SDKs like contentstack-management. |
| Respect user configs | Use ~/.csdx/config.json when needed. |
| Log errors gracefully | Use clear error messages and helpful hints. |
| Practice | Reason |
|---|---|
| Don't overwrite global configs | Avoid altering shared state. |
| Don't hardcode values | Make plugins configurable. |
| Don't break existing flows | Avoid side effects in CLI. |
| Don't ignore security | Never log sensitive information. |
| Don't bypass CLI output patterns | Ensure UX consistency. |
To uninstall a plugin, perform the following steps:
csdx plugins:listcsdx plugins:uninstall <plugin_name>Note: This command does not delete the plugin's code folder from your local machine. It only removes the plugin from the CLI's plugin registry.
To update an installed plugin, run the following command:
csdx plugins:updateTo remove all user-installed plugins, run the following command:
csdx plugins:resetTo display the installation properties of a plugin, run the following commands:
cd <plugin-directory>
csdx plugins:inspectWhen building your plugin, you have access to various methods and utilities from both oclif and the Contentstack CLI. Here are the most commonly used ones:
These methods are available directly in the Command class:
// Logging
this.log('Message') // Print a message
this.error('Error message') // Print an error and exit
this.warn('Warning message') // Print a warning
// Exit handling
this.exit(code) // Exit with code (0 = success)
this.error('Message', {exit: 1}) // Error and exit with code
// Configuration access
this.config.bin // CLI binary name
this.config.version // CLI versionNote: @contentstack/cli-utilities is optional and only required if you plan to use Contentstack-specific utilities such as cliux, configHandler, or managementSDKClient. Skip this if you're building a simple plugin that doesn't interact with Contentstack APIs.
To use Contentstack CLI utilities, install the following package:
npm install @contentstack/cli-utilities
This ensures your plugin works correctly when published and installed via csdx plugins:install.
User Interface (cliux)
import { cliux } from '@contentstack/cli-utilities'
// Printing messages
cliux.print('Message') // Print message
cliux.print('Message', {color: 'cyan'}) // Print with color
cliux.success('Success message') // Print success message
cliux.error('Error message') // Print error message
cliux.warning('Warning message') // Print warning message
cliux.info('Info message') // Print info message
// User prompts
const answer = await cliux.inquire({
type: 'input',
name: 'value',
message: 'Enter a value:',
default: 'default-value'
})
const confirmed = await cliux.confirm('Are you sure?')
// Loading indicators
cliux.loader('Processing...') // Show loading spinnerConfiguration Access (configHandler)
import { configHandler } from '@contentstack/cli-utilities'
// Get configuration values
const email = configHandler.get('email')
const region = configHandler.get('region') // Returns { name, cma, cda }
const config = configHandler.get('config') // Full config object
// Set configuration values
configHandler.set('key', 'value')
// Check authentication
import { isAuthenticated } from '@contentstack/cli-utilities'
if (isAuthenticated()) {
// User is logged in
}Management SDK Client
import { managementSDKClient } from '@contentstack/cli-utilities'
// Get authenticated SDK client
const region = configHandler.get('region')
const client = await managementSDKClient({ host: region.cma })
// Use the client
const stack = await client.stack({ api_key: 'your-api-key' })
const entries = await stack.contentType('content_type_uid').entry().query().find()Essential Helper Functions
import {
isAuthenticated,
formatError,
validatePath,
sanitizePath
} from '@contentstack/cli-utilities'
// Check authentication
if (isAuthenticated()) {
// User is logged in
}
// Format Contentstack API errors
try {
// API call
} catch (error) {
const formattedError = formatError(error)
cliux.error(formattedError)
}
// Path validation and sanitization
if (validatePath('/some/path')) {
const cleanPath = sanitizePath('../../../some/path') // Removes directory traversal
}Logger Service
The logger automatically initializes with the log path determined by the following priority:
import { log, handleAndLogError, getLogPath } from '@contentstack/cli-utilities'
// Get the current log path (useful for debugging)
const logPath = getLogPath()
console.log(`Logs are being written to: ${logPath}`)
// Simple logging (uses singleton logger, automatically initializes)
log.info('Info message')
log.success('Success message')
log.warn('Warning message')
log.debug('Debug message', { context: 'additional data' })
// Structured error logging
log.logError({
type: 'API_ERROR',
message: 'Failed to fetch entries',
error: error,
context: { stackApiKey: 'your-key' },
meta: { additionalInfo: 'value' }
})
// Error handling with classification
try {
// Your code
} catch (error) {
handleAndLogError(error, { command: 'myplugin:do' }, 'Custom error message')
}Configuring Log Path
Users can configure the log path using the CLI logging preference as follows:
csdx config:set:log --path /path/to/logsOr set the environment variable:
export CS_CLI_LOG_PATH=/path/to/logsHere's a complete example showing how to use these utilities together:
import {Command, Flags} from '@oclif/core'
import {
cliux,
configHandler,
isAuthenticated,
managementSDKClient
} from '@contentstack/cli-utilities'
export default class MyCommand extends Command {
static description = 'Fetches entries from Contentstack'
static flags = {
'content-type': Flags.string({
char: 'c',
description: 'Content type UID',
required: true,
}),
'stack-api-key': Flags.string({
char: 's',
description: 'Stack API key',
required: true,
}),
}
async run() {
const {flags} = await this.parse(MyCommand)
// Check authentication
if (!isAuthenticated()) {
cliux.error('Please login first: csdx login')
this.exit(1)
}
// Get region configuration
const region = configHandler.get('region')
if (!region) {
cliux.error('Please set a region: csdx config:set:region <region>')
this.exit(1)
}
cliux.info(`Using region: ${region.name}`)
try {
// Get authenticated client
const client = await managementSDKClient({ host: region.cma })
const stack = client.stack({ api_key: flags['stack-api-key'] })
// Show loading indicator
cliux.loader('Fetching entries...')
// Fetch entries
const entries = await stack
.contentType(flags['content-type'])
.entry()
.query()
.find()
cliux.success(`Found ${entries.items.length} entries`)
// Display results
entries.items.forEach((entry: any) => {
cliux.print(`- ${entry.title} (${entry.uid})`)
})
} catch (error: any) {
cliux.error(`Error: ${error.message}`)
this.exit(1)
}
}
}Here's a quick reference of essential utilities from @contentstack/cli-utilities:
| Utility | Import | Purpose |
|---|---|---|
| User Interface | cliux | Print messages, prompts, confirmations |
| Configuration | configHandler | Access CLI config (authToken, email, region) |
| Authentication | isAuthenticated | Check if the user is logged in |
| Management SDK | managementSDKClient | Contentstack Management API client |
| Error Handling | formatError | Format Contentstack API errors |
| Path Utilities | validatePath, sanitizePath | Validate and sanitize file paths |
| Logging | LoggerService | Logging for your plugin |
If your command isn't recognized after linking:
npm run build
npx oclif manifest
ls dist/commands/myplugin/
csdx plugins:uninstall myplugin
csdx plugins:linkYou may see a warning as given below:
Warning: @contentstack/myplugin is a linked ESM module and cannot be auto-transpiled.
This warning is expected. The plugin loads compiled code from the dist/ directory. Ensure to build your plugin before linking.
If your changes aren't showing up:
npm run build
npx oclif manifest
csdx plugins:linkIf you get authentication or region errors:
csdx login
csdx config:get:regioncsdx config:set:region <region-name>If installation fails:
csdx plugins:install @contentstack/myplugin