Claude Code Plugins

Community-maintained marketplace

Feedback

implementing-cli-patterns

@saleor/configurator
20
0

Implements CLI user experience with output formatting, progress indicators, and interactive prompts. Uses chalk, ora, and inquirer for consistent terminal interactions. Triggers on: CLI output, progress bar, spinner, ora, chalk, inquirer prompts, error formatting, exit codes, reporter.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name implementing-cli-patterns
description Implements CLI user experience with output formatting, progress indicators, and interactive prompts. Uses chalk, ora, and inquirer for consistent terminal interactions. Triggers on: CLI output, progress bar, spinner, ora, chalk, inquirer prompts, error formatting, exit codes, reporter.
allowed-tools Read, Grep, Glob, Write, Edit

CLI User Experience Patterns

Purpose

Guide the implementation of consistent and user-friendly CLI interactions including output formatting, progress tracking, interactive prompts, and error messaging.

When to Use

  • Implementing command output
  • Adding progress indicators
  • Creating user prompts
  • Formatting error messages
  • Designing reporter output

Table of Contents

CLI Stack

Tool Purpose
chalk Colored terminal output
ora Spinner/progress indicators
@inquirer/prompts Interactive user prompts
tslog Structured logging

Console Module Patterns

Message Types

Located in src/cli/console.ts:

import chalk from 'chalk';

export const console = {
  // Error messages (red)
  error: (message: string) => {
    console.log(chalk.red(`✖ ${message}`));
  },

  // Success messages (green)
  success: (message: string) => {
    console.log(chalk.green(`✔ ${message}`));
  },

  // Warning messages (yellow)
  warning: (message: string) => {
    console.log(chalk.yellow(`⚠ ${message}`));
  },

  // Hint messages (cyan)
  hint: (message: string) => {
    console.log(chalk.cyan(`💡 ${message}`));
  },

  // Important messages (bold)
  important: (message: string) => {
    console.log(chalk.bold(message));
  },

  // Code blocks (gray background)
  code: (code: string) => {
    console.log(chalk.bgGray.white(` ${code} `));
  },

  // Info messages (blue)
  info: (message: string) => {
    console.log(chalk.blue(`ℹ ${message}`));
  },
};

Usage Examples

import { console } from '@/cli/console';

// Command success
console.success('Configuration deployed successfully');

// Error with context
console.error(`Failed to create category: ${error.message}`);

// Helpful hints
console.hint('Run `configurator diff` to preview changes');

// Important information
console.important('This action cannot be undone');

// Code snippets
console.code('pnpm deploy --url=<URL> --token=<TOKEN>');

Progress Indicators

Spinner for Single Operations

import ora from 'ora';

const spinner = ora('Fetching categories...').start();

try {
  const categories = await fetchCategories();
  spinner.succeed(`Fetched ${categories.length} categories`);
} catch (error) {
  spinner.fail('Failed to fetch categories');
  throw error;
}

Progress Bar for Bulk Operations

// src/cli/progress.ts
export class BulkOperationProgress {
  private total: number;
  private completed: number = 0;
  private failed: number = 0;

  constructor(total: number, private label: string) {
    this.total = total;
  }

  tick(success: boolean = true): void {
    if (success) {
      this.completed++;
    } else {
      this.failed++;
    }
    this.render();
  }

  private render(): void {
    const percent = Math.round(((this.completed + this.failed) / this.total) * 100);
    const bar = this.createBar(percent);
    process.stdout.write(`\r${this.label}: ${bar} ${percent}% (${this.completed}/${this.total})`);
  }

  private createBar(percent: number): string {
    const filled = Math.round(percent / 5);
    const empty = 20 - filled;
    return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
  }

  finish(): void {
    console.log(''); // New line
    if (this.failed > 0) {
      console.warning(`Completed with ${this.failed} failures`);
    } else {
      console.success(`All ${this.completed} items processed`);
    }
  }
}

Usage

const progress = new BulkOperationProgress(items.length, 'Deploying products');

for (const item of items) {
  try {
    await deployItem(item);
    progress.tick(true);
  } catch (error) {
    progress.tick(false);
    failures.push({ item, error });
  }
}

progress.finish();

Interactive Prompts

Confirmation Prompt

import { confirm } from '@inquirer/prompts';

const shouldProceed = await confirm({
  message: 'This will overwrite existing configuration. Continue?',
  default: false,
});

if (!shouldProceed) {
  console.info('Operation cancelled');
  process.exit(0);
}

Select Prompt

import { select } from '@inquirer/prompts';

const action = await select({
  message: 'Select deployment mode:',
  choices: [
    { name: 'Full deployment', value: 'full' },
    { name: 'Incremental (only changes)', value: 'incremental' },
    { name: 'Dry run (preview only)', value: 'dry-run' },
  ],
});

Multi-Select Prompt

import { checkbox } from '@inquirer/prompts';

const entities = await checkbox({
  message: 'Select entities to deploy:',
  choices: [
    { name: 'Product Types', value: 'productTypes', checked: true },
    { name: 'Categories', value: 'categories', checked: true },
    { name: 'Products', value: 'products', checked: false },
    { name: 'Menus', value: 'menus', checked: false },
  ],
});

Input Prompt

import { input } from '@inquirer/prompts';

const apiUrl = await input({
  message: 'Enter Saleor API URL:',
  default: 'https://your-store.saleor.cloud/graphql/',
  validate: (value) => {
    if (!value.startsWith('https://')) {
      return 'URL must start with https://';
    }
    return true;
  },
});

Password Prompt

import { password } from '@inquirer/prompts';

const token = await password({
  message: 'Enter API token:',
  mask: '*',
});

Reporter Patterns

Deployment Result Reporter

// src/cli/reporters/deployment-reporter.ts
export interface DeploymentResult {
  entity: string;
  created: number;
  updated: number;
  deleted: number;
  failed: number;
  duration: number;
}

export const reportDeploymentResults = (results: DeploymentResult[]): void => {
  console.log('');
  console.important('Deployment Summary');
  console.log('─'.repeat(60));

  const headers = ['Entity', 'Created', 'Updated', 'Deleted', 'Failed', 'Time'];
  console.log(formatTableRow(headers));
  console.log('─'.repeat(60));

  for (const result of results) {
    const row = [
      result.entity,
      chalk.green(String(result.created)),
      chalk.blue(String(result.updated)),
      chalk.yellow(String(result.deleted)),
      result.failed > 0 ? chalk.red(String(result.failed)) : '0',
      `${result.duration}ms`,
    ];
    console.log(formatTableRow(row));
  }

  console.log('─'.repeat(60));

  const totals = results.reduce(
    (acc, r) => ({
      created: acc.created + r.created,
      updated: acc.updated + r.updated,
      deleted: acc.deleted + r.deleted,
      failed: acc.failed + r.failed,
    }),
    { created: 0, updated: 0, deleted: 0, failed: 0 }
  );

  if (totals.failed > 0) {
    console.warning(`Completed with ${totals.failed} failures`);
  } else {
    console.success('Deployment completed successfully');
  }
};

Diff Reporter

export const reportDiff = (diffs: DiffResult[]): void => {
  for (const diff of diffs) {
    switch (diff.action) {
      case 'create':
        console.log(chalk.green(`+ ${diff.entity}: ${diff.identifier}`));
        break;
      case 'update':
        console.log(chalk.blue(`~ ${diff.entity}: ${diff.identifier}`));
        for (const change of diff.changes) {
          console.log(chalk.gray(`  ${change.field}: ${change.from} → ${change.to}`));
        }
        break;
      case 'delete':
        console.log(chalk.red(`- ${diff.entity}: ${diff.identifier}`));
        break;
    }
  }

  console.log('');
  console.info(`Total: ${diffs.filter(d => d.action === 'create').length} to create, ` +
    `${diffs.filter(d => d.action === 'update').length} to update, ` +
    `${diffs.filter(d => d.action === 'delete').length} to delete`);
};

Duplicate Issue Reporter

export const reportDuplicates = (duplicates: DuplicateIssue[]): void => {
  if (duplicates.length === 0) return;

  console.log('');
  console.warning('Duplicate identifiers detected:');
  console.log('');

  for (const dup of duplicates) {
    console.log(chalk.yellow(`  ${dup.entityType}: "${dup.identifier}"`));
    console.log(chalk.gray(`    Found at: ${dup.locations.join(', ')}`));
  }

  console.log('');
  console.hint('Fix duplicates before deploying to avoid conflicts');
};

Error Message Formatting

Structured Error Output

export const formatError = (error: BaseError): void => {
  console.log('');
  console.error(error.message);

  if (error.code) {
    console.log(chalk.gray(`  Error code: ${error.code}`));
  }

  if (error.context) {
    console.log(chalk.gray(`  Context: ${JSON.stringify(error.context)}`));
  }

  const suggestions = error.getSuggestions?.();
  if (suggestions?.length) {
    console.log('');
    console.hint('Suggestions:');
    for (const suggestion of suggestions) {
      console.log(chalk.cyan(`  • ${suggestion}`));
    }
  }
};

GraphQL Error Formatting

export const formatGraphQLError = (error: GraphQLError): void => {
  console.error(`GraphQL operation failed: ${error.operationName}`);

  if (error.errors?.length) {
    for (const gqlError of error.errors) {
      console.log(chalk.red(`  • ${gqlError.message}`));
      if (gqlError.path) {
        console.log(chalk.gray(`    Path: ${gqlError.path.join('.')}`));
      }
    }
  }
};

Exit Codes

export const ExitCodes = {
  SUCCESS: 0,
  GENERAL_ERROR: 1,
  VALIDATION_ERROR: 2,
  NETWORK_ERROR: 3,
  AUTH_ERROR: 4,
  CONFLICT_ERROR: 5,
} as const;

// Usage
if (validationErrors.length > 0) {
  reportValidationErrors(validationErrors);
  process.exit(ExitCodes.VALIDATION_ERROR);
}

Best Practices

Do's

  • Use consistent colors for message types
  • Provide progress feedback for long operations
  • Include actionable suggestions with errors
  • Confirm destructive operations
  • Support both interactive and non-interactive modes

Don'ts

  • Don't spam the console with debug output
  • Don't use raw console.log in production code
  • Don't block on prompts in CI/headless mode
  • Don't hide important information behind verbosity flags

References

Related Skills

  • Complete entity workflow: See adding-entity-types for CLI integration patterns
  • Error handling: See reviewing-typescript-code for error message standards