| name | contentstack-cli |
|---|---|
| description | Contentstack CLI development patterns, OCLIF commands, API integration, and auth/config workflows. Use for CLI plugins, OCLIF commands, or API integration—including Developer Hub apps (packages/contentstack-apps-cli, app:* commands). |
import { BaseCommand } from '../../base-command';
import { FlagInput, Flags } from '@contentstack/cli-utilities';
import { cliux, handleAndLogError } from '@contentstack/cli-utilities';
export default class ImportCommand extends BaseCommand<typeof ImportCommand> {
static id = 'cm:stacks:import';
static description = 'Import content into a stack';
static flags: FlagInput = {
'stack-api-key': Flags.string({
char: 'k',
description: 'Stack API key',
required: true,
}),
'data-dir': Flags.string({
char: 'd',
description: 'Directory with import data',
required: true,
}),
verbose: Flags.boolean({
char: 'v',
description: 'Show verbose output',
default: false
})
};
static examples = [
'$ csdx cm:stacks:import -k <api-key> -d ./data',
'$ csdx cm:stacks:import -k <api-key> -d ./data --verbose',
];
async run(): Promise<void> {
try {
const { flags: parsedFlags } = await this.parse(ImportCommand);
// Validate flags
if (!parsedFlags['stack-api-key']) {
this.error('Stack API key is required');
}
// Delegate to service
cliux.print('Starting import...', { color: 'blue' });
const importService = new ImportService(parsedFlags);
const result = await importService.import();
cliux.success('✅ Import completed successfully');
} catch (error) {
handleAndLogError(error, {
module: 'import-command',
stackApiKey: this.flags['stack-api-key']
});
}
}
}Commands are organized by topic hierarchy under cm:
src/commands/cm/stacks/import.ts→ commandcm:stacks:importsrc/commands/cm/stacks/export.ts→ commandcm:stacks:exportsrc/commands/cm/stacks/audit/index.ts→ commandcm:stacks:auditsrc/commands/cm/stacks/audit/fix.ts→ commandcm:stacks:audit:fix
async run(): Promise<void> {
const { flags } = await this.parse(MyCommand);
// Validate required flags
if (!flags.region) {
this.error('--region is required');
}
// Validate flag values
const validRegions = ['us', 'eu', 'au'];
if (!validRegions.includes(flags.region)) {
this.error(`Region must be one of: ${validRegions.join(', ')}`);
}
}static flags: FlagInput = {
username: flags.string({
char: 'u',
exclusive: ['oauth'] // Cannot use with oauth flag
}),
oauth: flags.boolean({
exclusive: ['username', 'password']
})
};static flags: FlagInput = {
cma: flags.string({
dependsOn: ['cda', 'name']
}),
cda: flags.string({
dependsOn: ['cma', 'name']
})
};export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
static description = 'User sessions login';
static aliases = ['login'];
static flags: FlagInput = {
username: flags.string({
char: 'u',
description: 'Email address of your Contentstack account',
exclusive: ['oauth']
}),
password: flags.string({
char: 'p',
description: 'Password of your Contentstack account',
exclusive: ['oauth']
}),
oauth: flags.boolean({
description: 'Enable single sign-on (SSO)',
default: false,
exclusive: ['username', 'password']
})
};
async run(): Promise<void> {
try {
const managementAPIClient = await managementSDKClient({
host: this.cmaHost,
skipTokenValidity: true
});
const { flags: loginFlags } = await this.parse(LoginCommand);
authHandler.client = managementAPIClient;
if (loginFlags.oauth) {
log.debug('Starting OAuth flow', this.contextDetails);
oauthHandler.host = this.cmaHost;
await oauthHandler.oauth();
} else {
const username = loginFlags.username || await interactive.askUsername();
const password = loginFlags.password || await interactive.askPassword();
await authHandler.login(username, password);
}
cliux.success('✅ Authenticated successfully');
} catch (error) {
handleAndLogError(error, this.contextDetails);
}
}
}export default class LogoutCommand extends BaseCommand<typeof LogoutCommand> {
static description = 'Logout from Contentstack';
async run(): Promise<void> {
try {
await authHandler.setConfigData('logout');
cliux.success('✅ Logged out successfully');
} catch (error) {
handleAndLogError(error, this.contextDetails);
}
}
}// Add token
export default class TokenAddCommand extends BaseCommand<typeof TokenAddCommand> {
static description = 'Add authentication token';
static flags: FlagInput = {
email: flags.string({
char: 'e',
description: 'Email address',
required: true
}),
label: flags.string({
char: 'l',
description: 'Token label',
required: false
})
};
async run(): Promise<void> {
const { flags } = await this.parse(TokenAddCommand);
// Add token logic
cliux.success('✅ Token added successfully');
}
}export default class ConfigGetCommand extends BaseCommand<typeof ConfigGetCommand> {
static description = 'Get CLI configuration values';
async run(): Promise<void> {
try {
const region = configHandler.get('region');
cliux.print(`Region: ${region}`);
} catch (error) {
handleAndLogError(error, { ...this.contextDetails, module: 'config-get' });
}
}
}export default class RegionSetCommand extends BaseCommand<typeof RegionSetCommand> {
static description = 'Set region for CLI';
static args = {
region: args.string({ description: 'Region name (AWS-NA, AWS-EU, etc.)' })
};
static examples = [
'$ csdx config:set:region',
'$ csdx config:set:region AWS-NA',
'$ csdx config:set:region --cma <url> --cda <url> --ui-host <url> --name "Custom"'
];
async run(): Promise<void> {
try {
const { args, flags } = await this.parse(RegionSetCommand);
let selectedRegion = args.region;
if (!selectedRegion) {
selectedRegion = await interactive.askRegions();
}
const regionDetails = regionHandler.setRegion(selectedRegion);
await authHandler.setConfigData('logout'); // Reset auth on region change
cliux.success(`✅ Region set to ${regionDetails.name}`);
cliux.print(`CMA host: ${regionDetails.cma}`);
cliux.print(`CDA host: ${regionDetails.cda}`);
} catch (error) {
handleAndLogError(error, { ...this.contextDetails, module: 'config-set-region' });
}
}
}export default class ProxyRemoveCommand extends BaseCommand<typeof ProxyRemoveCommand> {
static description = 'Remove proxy configuration';
async run(): Promise<void> {
try {
configHandler.remove('proxy');
cliux.success('✅ Proxy configuration removed');
} catch (error) {
handleAndLogError(error, this.contextDetails);
}
}
}import { managementSDKClient } from '@contentstack/cli-utilities';
// Initialize client
const managementClient = await managementSDKClient({
host: this.cmaHost,
skipTokenValidity: false
});
// Get stack
const stack = managementClient.stack({ api_key: stackApiKey });
// Fetch entry
const entry = await stack.entry(entryUid).fetch();
// Query entries
const entries = await stack
.entry()
.query({ query: { title: 'My Entry' } })
.find();
// Update entry
const updatedEntry = await stack.entry(entryUid).update({ ...entry });try {
const stack = client.stack({ api_key: apiKey });
const entry = await stack.entry(uid).fetch();
} catch (error: any) {
if (error.status === 401) {
throw new CLIError('Authentication failed. Please login again.');
} else if (error.status === 404) {
throw new CLIError(`Entry with UID "${uid}" not found.`);
} else if (error.status === 429) {
throw new CLIError('Rate limited. Please try again later.');
}
handleAndLogError(error, {
module: 'entry-service',
entryUid: uid,
stackApiKey: apiKey
});
}import { interactive } from '../../utils';
// Ask for region selection
const region = await interactive.askRegions();
// Ask for username
const username = await interactive.askUsername();
// Ask for password
const password = await interactive.askPassword();
// Ask custom question
const customResponse = await cliux.prompt('Enter your choice:');// Success message
cliux.success('✅ Operation completed');
// Error message
cliux.error('❌ Operation failed');
// Info message
cliux.print('Processing...', { color: 'blue' });
// Show data
cliux.table([
{ name: 'Alice', region: 'us', status: 'active' },
{ name: 'Bob', region: 'eu', status: 'inactive' }
]);log.debug('LoginCommand started', this.contextDetails);
log.debug('Management API client initialized', this.contextDetails);
log.debug('Token parsed', {
...this.contextDetails,
flags: loginFlags
});
try {
await this.performOperation();
} catch (error) {
log.debug('Operation failed', {
...this.contextDetails,
error: error.message,
errorCode: error.code
});
}The BaseCommand provides contextDetails with:
contextDetails = {
command: 'auth:login',
userId: '12345',
email: 'user@example.com',
sessionId: 'session-123'
};// messages/en.json
{
"auth": {
"login": {
"success": "Authentication successful",
"failed": "Authentication failed"
},
"logout": {
"success": "Logged out successfully"
}
},
"config": {
"region": {
"set": "Region set to {{name}}"
}
}
}import { messageHandler } from '@contentstack/cli-utilities';
const message = messageHandler.get(['auth', 'login', 'success']);
cliux.success(message);export default class MyCommand extends BaseCommand<typeof MyCommand> {
// 1. Static properties
static description = '...';
static examples = [...];
static flags = {...};
// 2. Instance variables
private someHelper: Helper;
// 3. run method
async run(): Promise<void> {
try {
const { flags } = await this.parse(MyCommand);
await this.execute(flags);
} catch (error) {
handleAndLogError(error, this.contextDetails);
}
}
// 4. Private helper methods
private async execute(flags: any): Promise<void> {}
private validate(input: any): void {}
}- Be specific about what went wrong
- Provide actionable feedback
- Example: "Region must be AWS-NA, AWS-EU, or AWS-AU"
- Not: "Invalid region"
cliux.print('🔄 Processing...', { color: 'blue' });
// ... operation ...
cliux.success('✅ Completed successfully');Package: packages/contentstack-apps-cli (@contentstack/apps-cli). SDK, config, HTTP, manifests, GraphQL: framework.
| Path | Command id | Purpose |
|---|---|---|
src/commands/app/index.ts |
app |
Topic help |
src/commands/app/create.ts |
app:create |
Create app + optional boilerplate |
src/commands/app/get.ts |
app:get |
Fetch app details |
src/commands/app/update.ts |
app:update |
Update from manifest |
src/commands/app/delete.ts |
app:delete |
Delete from marketplace |
src/commands/app/install.ts |
app:install |
Install on stack |
src/commands/app/reinstall.ts |
app:reinstall |
Reinstall on stack |
src/commands/app/uninstall.ts |
app:uninstall |
Uninstall (factory + strategy) |
src/commands/app/deploy.ts |
app:deploy |
Deploy (Launch / custom hosting) |
OCLIF topic: app (oclif.topics.app in package.json). Short names in csdxConfig.shortCommandName (e.g. app:create → APCRT).
import { BaseCommand } from "../../base-command";
import { AppCLIBaseCommand } from "../../app-cli-base-command";
// Org-level or SDK-only commands
export default class AppInstall extends BaseCommand<typeof AppInstall> {
static flags = { /* ... */, ...BaseCommand.baseFlags };
async run() { /* this.flags, this.managementSdk, this.marketplaceAppSdk ready after init */ }
}
// Commands that read manifest.json from cwd
export default class AppUpdate extends AppCLIBaseCommand {
async run() {
// this.manifestData, this.manifestPath set in AppCLIBaseCommand.init
}
}BaseCommand(src/base-command.ts) —init()parses flags/args,registerConfig, logger,validateRegionAndAuth, SDK init.baseFlags:org,yes. Also:getValPrompt,messages/$t,catch/finally(nonexistent flag → exit 2).AppCLIBaseCommand— Aftersuper.init(),getManifestData()from{cwd}/manifest.json.
Always call await super.init() first in init() overrides.
- CLI layer — static
flags/examples, prompts (cliux,getValPrompt,src/util/inquirer.ts),this.log,this.error/ exit. - Business logic —
src/util/,src/factories/,src/strategies/; keeprun()small.
Parse runs inside BaseCommand.init only; do not re-parse in run(). After init, use this.flags and this.args. Inherit BaseCommand.baseFlags when org / --yes apply.
OCLIF hook: src/hooks/init/load-chalk.ts (registered in package.json oclif.hooks.init).
export default class AppGet extends BaseCommand<typeof AppGet> {
static description = "Get details of an app in developer hub";
static flags = {
"app-uid": Flags.string({ description: "App UID" }),
"app-type": Flags.string({
options: ["stack", "organization"],
default: "stack",
}),
...BaseCommand.baseFlags,
};
async run(): Promise<void> {
const { org, "app-uid": appUid } = this.flags;
// Use this.marketplaceAppSdk / GraphQL / apiRequestHandler — see framework Apps CLI section
this.log(this.$t(this.messages.APP_FETCH_SUCCESS), "info");
}
}--config— JSON merged intosharedConfig(seeregisterConfiginbase-command.ts). Used by create/deploy for Launch project settings.--data-dir— Working directory for boilerplate clone and manifest paths.
BaseCommand.catch handles NonExistent flag with exit code 2. Prefer actionable messages via messages / $t and existing util error helpers.
| Command | Purpose |
|---|---|
pnpm --filter @contentstack/apps-cli run build |
tsc → lib/ |
pnpm --filter @contentstack/apps-cli test |
Mocha + ESLint (posttest) |
pnpm --filter @contentstack/apps-cli run test:unit:report |
Unit tests with nyc |
Unit tests under test/unit/commands/app/. Use stubAuthentication from test/unit/helpers/auth-stub-helper.ts and nock Developer Hub hosts from getDeveloperHubUrl(). See testing and dev-workflow.
Package: packages/contentstack-cli-tsgen (contentstack-cli-tsgen). Top-level command csdx tsgen (short TSGEN). Requires a delivery token alias (-a / --token-alias).
Full flags, REST vs GraphQL, and library boundaries: typescript-cli-tsgen. Integration tests (Jest): package testing skill. Repo move: TSGEN-MIGRATION.md.
| Command | Purpose |
|---|---|
pnpm --filter contentstack-cli-tsgen run build |
tsc → lib/ + OCLIF manifest |
pnpm --filter contentstack-cli-tsgen run test:integration |
Live stack integration tests |