implementing-cli-patterns
π―Skillfrom saleor/configurator
Here's a concise, practical description for the "implementing-cli-patterns" skill: Implements consistent CLI user experiences with color-coded output, progress indicators, interactive prompts, and...
Installation
npx skills add https://github.com/saleor/configurator --skill implementing-cli-patternsSkill Details
"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."
Overview
# 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](#cli-stack)
- [Console Module Patterns](#console-module-patterns)
- [Progress Indicators](#progress-indicators)
- [Interactive Prompts](#interactive-prompts)
- [Reporter Patterns](#reporter-patterns)
- [Error Message Formatting](#error-message-formatting)
- [Exit Codes](#exit-codes)
- [Best Practices](#best-practices)
- [References](#references)
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:
```typescript
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
```typescript
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=
```
Progress Indicators
Spinner for Single Operations
```typescript
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
```typescript
// 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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
import { password } from '@inquirer/prompts';
const token = await password({
message: 'Enter API token:',
mask: '*',
});
```
Reporter Patterns
Deployment Result Reporter
```typescript
// 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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
{baseDir}/src/cli/console.ts- Console module{baseDir}/src/cli/progress.ts- Progress tracking{baseDir}/src/cli/reporters/- Reporter implementations- chalk documentation: https://github.com/chalk/chalk
- ora documentation: https://github.com/sindresorhus/ora
- inquirer documentation: https://github.com/SBoudrias/Inquirer.js
Related Skills
- Complete entity workflow: See
adding-entity-typesfor CLI integration patterns - Error handling: See
reviewing-typescript-codefor error message standards
Quick Reference Rule
For a condensed quick reference, see .claude/rules/cli-development.md (automatically loaded when editing src/cli/*/.ts and src/commands/*/.ts files).
More from this repository8
Automates changeset creation for semantic versioning, determining version bump type and generating detailed release notes.
Explains Saleor e-commerce domain, revealing entity identification, deployment pipeline, and configuration management strategies for developers.
Reviews TypeScript code for type safety, clean code principles, functional patterns, Zod validation, and error handling across various project scenarios.
Designs and validates Zod schemas with type inference, supporting complex validation patterns, branded types, and transformations.
Generates type-safe GraphQL operations using gql.tada and urql, implementing a repository pattern for Saleor API interactions with robust error handling.
Configures and manages GitHub Actions workflows, automates releases via Changesets, validates PRs, and troubleshoots CI pipeline failures.
Analyzes and generates comprehensive tests using Vitest and MSW, creating test builders, mocking repositories, and configuring integration tests.
Validates code quality by running linting, TypeScript compilation, tests, and CI checks before committing or pushing changes.