home / skills / gwenwindflower / .charmschool / deno-cliffy-cli

deno-cliffy-cli skill

/agents/claude/skills/deno-cliffy-cli

This skill helps you build structured Deno CLIs with Cliffy, enabling subcommands, lazy imports, and deno.json task integration for robust tooling.

npx playbooks add skill gwenwindflower/.charmschool --skill deno-cliffy-cli

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
3.0 KB
---
name: deno-cliffy-cli
description: >
  Build CLI tools in Deno using the Cliffy command framework.
  Use when: (1) Creating a new CLI tool or adding subcommands in Deno,
  (2) Working with deno.json task definitions alongside a Cliffy CLI,
  (3) Structuring a Deno project with commands/ directory pattern.
---

# Deno Cliffy CLI

Build structured CLI tools in Deno with typed options, help text, and subcommands using `@cliffy/command` from JSR.

## Quick Start

Add to `deno.json`:

```json
{
  "imports": {
    "@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.7"
  }
}
```

Minimal CLI:

```typescript
#!/usr/bin/env -S deno run --allow-all
import { Command } from "@cliffy/command";

const cli = new Command()
  .name("mytool")
  .version("0.1.0")
  .description("What the tool does");

cli
  .command("greet")
  .description("Say hello")
  .option("-n, --name <name:string>", "Who to greet", { default: "world" })
  .action(({ name }) => {
    console.log(`Hello, ${name}!`);
  });

await cli.parse(Deno.args);
```

## Project Structure

For CLIs with multiple commands, use a `commands/` directory:

```text
project/
├── cli.ts           # Entry point, wires up commands
├── commands/
│   ├── build.ts     # export async function build(...)
│   ├── deploy.ts    # export async function deploy(...)
│   └── test.ts      # export async function runTests(...)
├── lib/             # Shared utilities
│   └── helpers.ts
└── deno.json        # Tasks + imports
```

Each command file exports a single async function. The CLI entry point imports and wires them.

## Key Patterns

### Lazy Imports for Heavy Dependencies

When a command depends on something heavy (Playwright, Puppeteer, large npm packages), use dynamic import in the action handler. This prevents loading the dependency for unrelated commands.

```typescript
cli
  .command("scrape")
  .action(async ({ output }) => {
    const { scrape } = await import("./commands/scrape.ts");
    await scrape(output);
  });
```

Critical when dependencies read env vars or spawn processes at import time.

### deno.json Task Integration

Wire subcommands to `deno task` with per-command permissions:

```json
{
  "tasks": {
    "cli": "deno run --allow-all cli.ts",
    "build": "deno run --allow-read=. --allow-write=. cli.ts build",
    "scrape": "deno run --allow-all cli.ts scrape"
  }
}
```

Use `--allow-all` only when genuinely needed. Pass extra flags with `--`: `deno task build -- --watch`.

### npm Packages in Deno

Import npm packages via `npm:` specifier or map them in `deno.json`:

```json
{
  "imports": {
    "handlebars": "npm:handlebars@^4.7.8"
  },
  "nodeModulesDir": "auto"
}
```

Set `"nodeModulesDir": "auto"` when npm packages need to read their own files from disk (templates, assets, partials). Without this, packages using `__dirname` or `fs.readFileSync` relative to their install path will fail.

## Reference

For detailed API patterns (option types, command nesting, global options, error handling), see the [Patterns Reference Guide](patterns.md).

Overview

This skill helps you build structured command-line tools in Deno using the Cliffy command framework. It focuses on typed options, built-in help text, subcommands, and a small project layout that scales from single-file tools to multi-command CLIs. The guidance covers wiring commands, integrating deno.json tasks, and handling heavy or npm dependencies safely.

How this skill works

You create a Command instance from @cliffy/command, register commands and options, and call parse(Deno.args) to run. For multi-command projects, place individual command handlers in a commands/ directory and import or lazy-load them from the entry point. Use deno.json to centralize imports, map npm packages, and define per-command task invocations with appropriate permission flags.

When to use it

  • Creating a new Deno CLI with subcommands and typed options
  • Refactoring an existing script into a maintainable multi-command tool
  • Coordinating deno.json tasks with CLI subcommands and permission scoping
  • Adding heavy dependencies (Playwright, Puppeteer) that should not load on every run
  • Using npm packages in Deno that require filesystem assets or node-style modules

Best practices

  • Keep each command in its own file under commands/ and export a single async function
  • Lazy-import heavy or side-effecting dependencies inside action handlers to avoid unnecessary startup costs
  • Define imports and npm mappings in deno.json and set nodeModulesDir: "auto" when packages read their own files
  • Scope permissions per deno task (avoid --allow-all unless needed) and pass extra CLI flags after -- in deno task invocations
  • Document command options and defaults so auto-generated help is useful and consistent

Example use cases

  • A developer tool with build, test, and deploy subcommands wired from commands/ files
  • A dotfiles manager that applies or removes configurations with per-command permissions
  • A web-scraping command that lazy-loads Playwright only when the scrape subcommand runs
  • A templating CLI that uses an npm handlebars package and requires nodeModulesDir to read partials
  • deno.json tasks that run specific commands with restricted permission flags for CI

FAQ

How do I avoid slow startup when adding heavy libraries?

Lazy-import heavy libraries inside the command's action handler so they load only when that subcommand runs.

When should I use deno.json tasks vs. running the CLI directly?

Use deno.json tasks to pin imports, set nodeModulesDir, and define per-command permission flags. Use the CLI directly for ad-hoc runs or development with full permissions.