@armynante/cli-core
v0.1.0
Published
Shared CLI helper module with Ink UI for interactive mode and structured non-interactive output for AI agents
Downloads
93
Maintainers
Readme
CLI Core
Shared CLI helper module with structured non-interactive output for AI agents and optional Ink UI for interactive mode.
Features
Default Mode (Non-Interactive)
- Structured JSON output for AI agents (
--json) - Step-by-step progress logging to stderr
- Machine-readable error formats
- Predictable exit codes
Interactive Mode (Opt-In)
- Rich Ink UI enabled with
--interactiveflag - DeployLayout: Full-featured deployment TUI with header, progress tracking, streaming logs, and action bar
- InteractiveProgressList: Navigable progress items with arrow keys, detail views, and retry support
- StreamLogViewer: Auto-scrolling log viewer with color-coded levels and manual scroll support
- Keyboard navigation and prompts for human operators
Usage
import {
createContext,
createPrinter,
renderInteractive,
DeployLayout,
} from '@anthropic/cli-core';
const { context } = createContext({ cliName: 'my-cli', version: '1.0.0' });
// Default: non-interactive for AI agents
if (context.mode === 'non-interactive') {
const printer = createPrinter(context);
printer.step(1, 3, 'Building...');
printer.step(2, 3, 'Deploying...');
printer.step(3, 3, 'Verifying...');
printer.success('Done!');
if (context.outputFormat === 'json') {
printer.flushJson(true, { url: 'https://app.example.com' });
}
} else {
// Opt-in: interactive TUI (requires --interactive flag)
renderInteractive(<DeployLayout {...props} />, context);
}Demo
# Run the TUI demo
bun examples/basic-cli/index.tsx tui-demo --interactive
# Keyboard shortcuts:
# ↑↓ - Navigate progress items
# Enter - View item details
# Esc - Go back from detail view
# l - Toggle logs
# p - Toggle progress
# q - Quit (when complete)
# Ctrl+C - CancelComponents
View Layout Components
These components provide reusable patterns for building consistent TUI views with proper layout handling.
ViewLayout
Full-screen layout wrapper ensuring ActionBar stays pinned to bottom, even during loading/error states.
import { ViewLayout, StateContainer } from '@armynante/cli-core';
<ViewLayout
header={{
title: 'Projects',
subtitle: '5 projects',
status: 'success',
meta: [{ label: 'Region', value: 'us-east-1' }],
}}
shortcuts={[
{ key: '↑↓', label: 'Navigate' },
{ key: 'Enter', label: 'Select' },
{ key: 'Esc', label: 'Back' },
]}
>
<StateContainer loading={isLoading} error={error}>
{/* Your content here */}
</StateContainer>
</ViewLayout>StateContainer
Unified handling of loading, error, and empty states.
<StateContainer
loading={isLoading}
loadingMessage="Fetching projects..."
error={error}
onBack={goBack}
empty={items.length === 0}
emptyMessage="No items found"
emptyAction={{ key: 'n', label: 'create a new item' }}
>
{/* Normal content when not loading/error/empty */}
</StateContainer>SelectableRow
Standard row with selection indicator, icon, badges, and description.
<SelectableRow
isSelected={index === selectedIndex}
icon="📦"
label="my-project"
description="Production deployment"
badges={[
{ text: '[ACTIVE]', color: 'green' },
{ text: '(abc123)', dimmed: true },
]}
secondaryIndicator={{ icon: '●', color: 'green', show: isActive }}
/>SelectableList
Keyboard-navigable list with built-in useInput handling.
<SelectableList
items={projects}
renderItem={(project, isSelected) => (
<SelectableRow isSelected={isSelected} label={project.name} />
)}
onSelect={(project) => navigate('details', { id: project.id })}
onBack={goBack}
additionalHandlers={{
'd': (item) => handleDelete(item.id),
'n': () => navigate('create'),
}}
/>KeyboardHints
Properly spaced inline keyboard hints (alternative to ActionBar).
<KeyboardHints
hints={[
{ key: '↑↓', label: 'navigate' },
{ key: 'Enter', label: 'select' },
{ key: 'q', label: 'quit' },
]}
/>Deployment Components
DeployLayout
Composite layout combining Header, InteractiveProgressList, StreamLogViewer, and ActionBar.
<DeployLayout
header={{ title: 'Deploying', status: 'running', meta: [...] }}
progressState={tracker.state}
logs={logs}
startedAt={startTime}
onCancel={handleCancel}
onRetry={handleRetry}
/>InteractiveProgressList
Selectable list of progress items with keyboard navigation.
<InteractiveProgressList
state={progressState}
onRetry={(operationId) => retry(operationId)}
getDetails={(operationId) => operationDetails[operationId]}
/>Test Stream Helper
Simulates AWS-style deployment logs for testing and demos.
const stream = createTestStream({
projectName: 'my-app',
environment: 'staging',
baseDelay: 150,
});
stream.onLog((entry) => console.log(entry.message));
stream.onProgress((event) => tracker.update(event));
stream.start();AI-First CLI Design Guide
This module is designed with AI agents as the primary consumer. Human interactive mode is opt-in.
Design Philosophy
- AI agents are the primary consumer - Default to structured, parseable output
- Interactive mode is opt-in - Require
--interactiveflag for TUI - Exit codes must be meaningful - Don't just exit(1) for everything
- Errors must be structured - Include actionable information
Global Options Reference
All CLIs using cli-core support these global options:
| Option | Short | Description | Default |
|--------|-------|-------------|---------|
| --interactive | -i | Enable rich Ink UI for humans | OFF (non-interactive) |
| --json | - | Force structured JSON output | text in non-interactive |
| --verbose | -v | Include debug output | OFF |
| --quiet | -q | Suppress non-essential output | OFF |
| --no-color | - | Disable ANSI colors | auto-detect TTY |
| --help | -h | Show help | - |
| --version | - | Show version | - |
Standard JSON Output Format
When using --json, all output follows this structure:
interface JsonOutput {
success: boolean; // Quick success check
data?: unknown; // Operation result data
error?: string; // Error message if success is false
messages: Array<{ // Step-by-step execution log
type: 'success' | 'error' | 'warning' | 'info' | 'step' | 'debug';
message: string;
details?: string;
timestamp: string; // ISO 8601
}>;
meta: {
command: string; // Command that was run
duration?: number; // Execution time in ms
exitCode: number; // Process exit code
timestamp: string; // ISO 8601
};
}Success Example:
{
"success": true,
"data": { "url": "https://app.example.com", "version": "1.2.3" },
"messages": [
{ "type": "step", "message": "[1/3] Building container", "timestamp": "2024-01-15T10:30:00.000Z" },
{ "type": "step", "message": "[2/3] Pushing to registry", "timestamp": "2024-01-15T10:30:05.000Z" },
{ "type": "success", "message": "Deployment complete", "timestamp": "2024-01-15T10:30:10.000Z" }
],
"meta": { "command": "deploy", "duration": 10000, "exitCode": 0, "timestamp": "2024-01-15T10:30:10.000Z" }
}Failure Example:
{
"success": false,
"error": "Build failed: missing dependency 'react'",
"messages": [
{ "type": "step", "message": "[1/3] Building container", "timestamp": "2024-01-15T10:30:00.000Z" },
{ "type": "error", "message": "Build failed: missing dependency 'react'", "timestamp": "2024-01-15T10:30:03.000Z" }
],
"meta": { "command": "deploy", "duration": 3000, "exitCode": 1, "timestamp": "2024-01-15T10:30:03.000Z" }
}Exit Code Conventions
| Code | Meaning | When to Use | |------|---------|-------------| | 0 | Success | Operation completed successfully | | 1 | General Error | Unspecified failure | | 2 | Input Error | Invalid arguments, missing required options | | 3 | Resource Not Found | File, URL, or resource doesn't exist | | 4 | Timeout | Operation exceeded time limit | | 5 | External Service Error | Database, API, or service unavailable |
Error Handling Pattern
import { createPrinter, createContext } from '@anthropic/cli-core';
const { context } = createContext({ cliName: 'my-cli', version: '1.0.0' });
const printer = createPrinter(context);
try {
await operation();
printer.success('Operation complete');
if (context.outputFormat === 'json') {
printer.flushJson(true, { result: 'data' });
}
} catch (error) {
const err = error as Error;
printer.error(err.message);
if (context.verbose) {
console.error(err.stack);
}
if (context.outputFormat === 'json') {
printer.flushJson(false, null, 1);
}
process.exit(1);
}Progress Reporting for Agents
Use printer.step() for progress that agents can parse:
const printer = createPrinter(context);
printer.step(1, 5, 'Validating configuration...');
printer.step(2, 5, 'Building container...');
printer.step(3, 5, 'Pushing to registry...');
printer.step(4, 5, 'Deploying to cluster...');
printer.step(5, 5, 'Running health checks...');
printer.success('Deployment complete');For long-running operations, use the progress tracker:
import { createProgressTracker } from '@anthropic/cli-core';
const tracker = createProgressTracker();
tracker.update({ operationId: 'build', status: 'running', message: 'Building...' });
// ... later
tracker.update({ operationId: 'build', status: 'completed', message: 'Build complete' });
// Get summary for status checks
const summary = tracker.getSummary();
// { total: 5, completed: 3, failed: 0, running: 2 }Integration Guide
When building CLIs that use cli-core, follow these conventions for consistent behavior across all tools.
Setting Up a New CLI
#!/usr/bin/env bun
import {
createContext,
createPrinter,
renderInteractive,
} from '@anthropic/cli-core';
const { context, globals, remainingArgs } = createContext({
cliName: 'my-cli',
version: '1.0.0',
});
// Handle global flags first
if (globals.help) {
showHelp();
process.exit(0);
}
if (globals.version) {
console.log('1.0.0');
process.exit(0);
}
// Route to appropriate mode
if (context.mode === 'interactive') {
// Human mode - requires --interactive flag
await runInteractive(context, remainingArgs);
} else {
// Default: AI agent mode
await runNonInteractive(context, remainingArgs);
}Command Structure Pattern
Define commands with CommandDefinition for automatic help generation:
import { createCommandRunner, CommandDefinition } from '@anthropic/cli-core';
const deployDefinition: CommandDefinition = {
name: 'deploy',
description: 'Deploy the application to a target environment',
options: {
env: {
type: 'string',
short: 'e',
description: 'Target environment (staging, production)',
required: true,
},
force: {
type: 'boolean',
short: 'f',
description: 'Skip confirmation prompts',
},
},
examples: [
{ command: 'my-cli deploy -e production', description: 'Deploy to production' },
{ command: 'my-cli deploy -e staging --json', description: 'Deploy with JSON output' },
],
};
export const deployCommand = createCommandRunner(
deployDefinition,
async (parsed, ctx) => {
const printer = createPrinter(ctx);
// Implementation
}
);Interactive Mode Guidelines
When to support interactive mode:
- Initial setup/configuration (
initcommands) - Dangerous operations requiring confirmation
- Complex workflows with multiple choices
When NOT to require interactive mode:
- Standard CRUD operations
- Status/info commands
- Batch operations
Pattern for dangerous operations:
async function deleteCommand(parsed: ParsedCommand, ctx: CLIContext) {
const printer = createPrinter(ctx);
// In non-interactive mode, require --force flag for dangerous operations
if (ctx.mode === 'non-interactive' && !parsed.options.force) {
printer.error('Destructive operation requires --force flag in non-interactive mode');
if (ctx.outputFormat === 'json') {
printer.flushJson(false, null, 2);
}
process.exit(2);
}
// In interactive mode, prompt for confirmation
if (ctx.mode === 'interactive') {
const confirmed = await confirmDanger('Delete all data? This cannot be undone.');
if (!confirmed) {
printer.info('Cancelled');
return;
}
}
// Proceed with deletion
await performDelete();
printer.success('Deleted successfully');
}Consolidated Output Pattern
Always use createPrinter(context) instead of custom output functions:
// DO THIS
import { createPrinter } from '@anthropic/cli-core';
const printer = createPrinter(context);
printer.success('Operation complete');
printer.error('Something failed');
printer.step(1, 5, 'Processing...');
// DON'T DO THIS
console.log('✓ Operation complete'); // Won't respect --quiet, --json
console.error('✗ Something failed'); // Won't be collected for JSON outputBenefits of using the printer:
- Automatic JSON collection when
--jsonis used - Respects
--quietflag (suppresses non-essential output) - Respects
--verboseflag (shows debug messages) - Consistent formatting across all CLIs
- Built-in elapsed time tracking
Help Generation
Generate consistent help pages with generateHelp:
import { generateHelp, getGlobalOptionsSection } from '@anthropic/cli-core';
const helpConfig: HelpPageConfig = {
name: 'my-cli',
version: '1.0.0',
description: 'My awesome CLI tool',
usage: 'my-cli <command> [options]',
sections: [
{
title: 'Commands',
items: [
{ name: 'deploy', description: 'Deploy the application' },
{ name: 'status', description: 'Check deployment status' },
{ name: 'logs', description: 'View deployment logs' },
],
},
getGlobalOptionsSection(), // Standard global options
],
examples: [
{ command: 'my-cli deploy -e prod', description: 'Deploy to production' },
{ command: 'my-cli status --json', description: 'Get status as JSON' },
],
};
function showHelp() {
console.log(generateHelp(helpConfig, context));
}Testing Conventions
Test both modes and JSON output:
import { describe, test, expect } from 'bun:test';
import { createContext, createPrinter } from '@anthropic/cli-core';
describe('my-cli', () => {
test('defaults to non-interactive mode', () => {
const { context } = createContext({
cliName: 'test-cli',
version: '1.0.0',
args: ['deploy', '-e', 'staging'],
});
expect(context.mode).toBe('non-interactive');
});
test('--interactive enables interactive mode', () => {
const { context } = createContext({
cliName: 'test-cli',
version: '1.0.0',
args: ['deploy', '--interactive'],
});
expect(context.mode).toBe('interactive');
});
test('--json sets output format', () => {
const { context } = createContext({
cliName: 'test-cli',
version: '1.0.0',
args: ['deploy', '--json'],
});
expect(context.outputFormat).toBe('json');
});
test('JSON output has correct structure', () => {
const { context } = createContext({
cliName: 'test-cli',
version: '1.0.0',
args: ['--json'],
});
const printer = createPrinter(context);
printer.step(1, 2, 'Step 1');
printer.success('Done');
const messages = printer.getMessages();
expect(messages).toHaveLength(2);
expect(messages[0].type).toBe('step');
expect(messages[1].type).toBe('success');
});
});Future Enhancements
See ENHANCEMENTS.md for detailed specifications on planned features:
- [ ] Log Storage - Store logs to temp directory for agent grep/search
- [ ] Machine-Readable Help - JSON output for
--help --json - [ ] Progress Polling - Enhanced polling with tracker integration
- [ ] Background Jobs UI - Reusable job status components
Test Bed Validation
Before implementation, features will be validated using:
- deploy-cli - Complex deployment workflows
- database-cli - Database operations with background tasks
Each test bed will generate an IMPLEMENTATION_REPORT.md documenting findings.
Publishing
From the repository root:
bun run publish:cli-coreOr directly:
./scripts/codeartifact-publish.sh Modules/cli-core