---
title: Contentstack Command-line Interface (CLI) - Create Custom CLI Plugins for Contentstack
description: 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.
url: https://www.contentstack.com/docs/developers/cli/create-custom-cli-plugins
product: Contentstack
doc_type: developer-guide
audience:
  - developers
  - cli-users
version: v1
last_updated: 2026-03-25
---

# Contentstack Command-line Interface (CLI) - Create Custom CLI Plugins for Contentstack

This page explains how to create and develop external plugins for the Contentstack CLI using oclif, including setup, command structure, testing, publishing, and troubleshooting. It is intended for developers extending the CLI with custom commands and should be used when building, linking, and distributing CLI plugins.

## Create Custom CLI Plugins for Contentstack

This guide explains how to develop an external plugin for [Contentstack CLI](https://www.contentstack.com/docs/developers/cli), including requirements, project structure, best practices, testing, publishing steps, and key internal behaviors to consider.

## Introduction

The Contentstack CLI supports modular extensibility through external plugins.

These plugins offer the following capabilities:
- Extend CLI functionality with custom commands.
- Allow global (system-wide) or local (project-specific) installation.
- Integrate with the [oclif framework](https://oclif.io/) for structured command development.

**Example plugin**: `@contentstack/apps-cli`

## Prerequisites
- [Node.js version 22.x and above](https://nodejs.org/en/download/)
- [Contentstack account](https://www.contentstack.com/login/)
- Familiarity with the oclif CLI framework (recommended)
- Contentstack CLI [installed](/docs/developers/cli/install-the-cli)

## Plugin Structure

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.json
```

**Note:** Use namespacing to prefix all commands to avoid collision. For example, use `csdx myplugin:do` instead of `csdx do`.

## Creating a Plugin

Use oclif's generator to create your plugin:

```
npx oclif generate myplugin
cd myplugin
```

The generator prompts you with the following questions:
- **Select a module type**: Choose `ESM` or `CommonJS`.
- **NPM package name**: Example: `@contentstack/myplugin` or `myplugin`
- **Command bin name the CLI will export**: Example: `myplugin` (this is the binary name, not the full command namespace)
- **Description**: Example: `A new CLI generated with oclif`
- **Author**: Example: `Contentstack` or your organization name
- **License**: Default: `MIT`
- **Who is the GitHub owner of repository**: Example: `@contentstack`
- **What is the GitHub name of repository**: Example: `myplugin`
- **Select a package manager**: Choose: `npm`, `yarn`, or `pnpm`

After answering these prompts, the generator creates the plugin structure in your current directory.

### Configure package.json

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"
  }
}
```

### Move to plugin-directory

```
cd
// Example:
cd ./myplugin
```

### Generate a command

```
npx oclif generate command myplugin:do
```

This creates `src/commands/myplugin/do.ts`.

### Build the plugin

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).

### Generate the manifest

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.

## Plugin Registration and Linking

When developing a plugin locally, you must link it to the Contentstack CLI for testing:

```
cd
csdx plugins:link
```

This sets up the plugin within the `csdx` namespace, allowing you to use your custom commands directly.

### Verify the Setup

Test that your plugin is properly linked by running the following command:

```
csdx myplugin:do
```

You should see your plugin's output in the terminal.

### Development Workflow

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:do
```

**Tip:** After making changes to your plugin, rebuild and regenerate the manifest, then test with `csdx myplugin:do` to ensure everything works correctly.

## Commands and Flags

Each command in your plugin must follow the `oclif` command structure:

### Command Structure
- `static description` - Help text displayed in the CLI
- `static flags` - CLI options and arguments
- `async run()` - Main command logic

### Example Command

```
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}`)
  }
}
```

## Testing

### Testing Setup

Use `@oclif/test` with Mocha or Jest for testing your commands.

### Example Test

```
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
```

## Testing Workflows

### Production Testing

To simulate the end-user experience, follow these steps to test your published plugin:
- [**Install the Contentstack CLI globally**](/docs/developers/cli/install-the-cli):

```
npm i -g @contentstack/cli
```
- [**Set the region**](/docs/developers/cli/configure-regions-in-the-cli#set-region):

```
csdx config:set:region
```
- [**Authenticate**](/docs/developers/cli/cli-authentication#authentication):

```
csdx login
```
- **Install your published plugin**:

```
csdx plugins:install @contentstack/myplugin
```
- **Test the plugin command**:

```
csdx myplugin:do --help
```

### Development Testing

Test your plugin during development:
- **Install the Contentstack CLI globally**:

```
npm i -g @contentstack/cli
```
- **Set the region**:

```
csdx config:set:region
```
- **Authenticate**:

```
csdx login
```
- **Link your local plugin**:

```
csdx plugins:link
```
- **Test the plugin command**:

```
csdx myplugin:do --help
```

## Important Considerations

### Region and Authentication

Core plugins handle region and authentication automatically. Before using any command, ensure the user has configured the region and completed authentication.
- [**Set the region**](/docs/developers/cli/configure-regions-in-the-cli#set-region):

```
csdx config:set:region
```
- [**Authenticate**](/docs/developers/cli/cli-authentication#authentication):

```
csdx login
```

### Contentstack CLI Features

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 |

### Publishing the Plugin
- Publish your plugin package to npm:

```
npm publish
```
- Install via CLI:

```
csdx plugins:install @contentstack/myplugin
```

Once installed, users can use your custom plugin to execute tasks.

## Best Practices

### Do's

| 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](/docs/developers/sdks) like `contentstack-management`. |
| Respect user configs | Use `~/.csdx/config.json` when needed. |
| Log errors gracefully | Use clear error messages and helpful hints. |

### Don'ts

| 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. |

## Uninstall a Plugin

To uninstall a plugin, perform the following steps:
- **List the installed plugins**:

```
csdx plugins:list
```
- **Uninstall a specific plugin**:

```
csdx plugins:uninstall
```

**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.

## Update a Plugin

To update an installed plugin, run the following command:

```
csdx plugins:update
```

## Remove All User Installed Plugins

To remove all user-installed plugins, run the following command:

```
csdx plugins:reset
```

## Display Installation Properties of a Plugin

To display the installation properties of a plugin, run the following commands:

```
cd
csdx plugins:inspect
```

## Available Methods and Utilities

When 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:

### Basic oclif Command Methods

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 version
```

### Contentstack CLI Utilities

**Note:** `@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 spinner
```

**Configuration 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:
- Environment variable: `CS_CLI_LOG_PATH` (highest priority)
- User config: `log.path` from CLI config (set via `csdx config:set:log --path <path>`)

```
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/logs
```

Or set the environment variable:

```
export CS_CLI_LOG_PATH=/path/to/logs
```

### Complete Example: Using Utilities

Here'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 ')
      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)
    }
  }
}
```

### Quick Reference: Essential Utilities

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 |

## Troubleshooting

### Command Not Found After Linking

If your command isn't recognized after linking:
- Verify that the build completed successfully:

```
npm run build
```
- Regenerate the manifest:

```
npx oclif manifest
```
- Check that the command exists in the `dist/commands/` directory:

```
ls dist/commands/myplugin/
```
- Relink the plugin:

```
csdx plugins:uninstall myplugin
csdx plugins:link
```

### ESM Module Warnings

You 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.

### Changes Not Reflecting

If your changes aren't showing up:
- Rebuild the plugin:

```
npm run build
```
- Regenerate the manifest:

```
npx oclif manifest
```
- Relink if necessary:

```
csdx plugins:link
```

### Authentication or Region Errors

If you get authentication or region errors:
- Verify you're logged in:

```
csdx login
```
- Check your region:

```
csdx config:get:region
```
- Set region if required:

```
csdx config:set:region
```

### Plugin Installation Issues

If installation fails:
- Verify that the package is published on npm.
- Check that the package name matches:

```
csdx plugins:install @contentstack/myplugin
```
- Ensure that the package has the correct `oclif.manifest.json` file (generated during `npm publish`).

## Common questions

### Do I need to build the plugin before linking it?
Yes. Ensure to run `npm run build` before linking so the plugin loads compiled code from the `dist/` directory.

### Why is `oclif.manifest.json` required?
The `oclif.manifest.json` file is required for the CLI to discover your commands.

### Can I install a plugin globally or per project?
These plugins allow global (system-wide) or local (project-specific) installation.

### What should I do if my command is not found after linking?
Verify the build, regenerate the manifest, confirm the command exists in `dist/commands/`, and relink the plugin.