npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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

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 --interactive flag
  • 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 - Cancel

Components

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

  1. AI agents are the primary consumer - Default to structured, parseable output
  2. Interactive mode is opt-in - Require --interactive flag for TUI
  3. Exit codes must be meaningful - Don't just exit(1) for everything
  4. 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 (init commands)
  • 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 output

Benefits of using the printer:

  • Automatic JSON collection when --json is used
  • Respects --quiet flag (suppresses non-essential output)
  • Respects --verbose flag (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-core

Or directly:

./scripts/codeartifact-publish.sh Modules/cli-core