boarddown-cli 0.1.4

CLI tool for BoardDown
boarddown-cli-0.1.4 is not a library.

BoardDown

Local-first, file-based Kanban storage engine with sync capabilities

Think "Markdown meets Kanban" — version controlled, offline-capable, and programmable.

Rust TypeScript Crates.io

Table of Contents


Philosophy

Why BoardDown?

Existing Kanban tools lock your data in proprietary cloud databases. BoardDown treats your project management data as code:

  • Git-compatible: Boards are Markdown, diff them in PRs
  • Offline-first: Local SQLite with optional cloud sync
  • Programmable: Rust/TS APIs for custom automation
  • Human-readable: Edit in any text editor, render in any app

Use Cases:

  • Personal task management in ~/tasks/ synced via Git
  • Team sprints with conflict-free replicated data types (CRDTs)
  • Embedded Kanban in IDEs (VSCode, Neovim extensions)
  • CI/CD pipeline visualization (/.boarddown/ci.board.md)

Quick Start

Installation

# CLI tool
cargo install boarddown-cli

# Or add to your Rust project
cargo add boarddown-core boarddown-fs

# For Node.js projects  
npm install @boarddown/core

The 30-Second Tutorial

Create a file sprint-42.board.md:

---
id: "sprint-42"
board: "Sprint 42: Auth Refactor"
id-prefix: "TICKET"
---

## Todo
- [ ] BD-001: Implement OAuth2 PKCE flow
      Tags: security, backend
      Estimate: 5
- [ ] BD-002: Add session rotation
      Tags: security
      Depends: BD-001

## In Progress
- [~] BD-003: Refactor JWT middleware
      Assign: alice@example.com
      Started: 2024-01-15

## Done
- [x] BD-004: Setup test fixtures
      Closed: 2024-01-14

Then interact with it:

use boarddown_core::{Workspace, QueryBuilder, Status};
use boarddown_fs::FilesystemStorage;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize storage
    let storage = FilesystemStorage::new("./sprints");
    storage.init().await?;
    
    // Open workspace
    let ws = Workspace::open("./sprints")
        .await?
        .with_storage(Arc::new(storage));
    
    // Load board
    let board = ws.get_board("sprint-42").await?;
    
    // Query tasks
    let blocked = board.query()
        .status(Status::Todo)
        .tag("security")
        .depends_on_existing(false)
        .execute()
        .await?;
    
    // Move task programmatically  
    let task = board.get_task(&"BD-001".into()).await
        .ok_or("Task not found")?;
    
    println!("Found {} blocked tasks", blocked.len());
    Ok(())
}

Or in TypeScript:

import { Workspace, Status } from '@boarddown/core'

const ws = await Workspace.open('./sprints')
const board = await ws.getBoard('sprint-42')

// Subscribe to changes (local or remote)
board.onChange((event) => {
  console.log(`Task ${event.taskId} moved to ${event.to}`)
})

// Create task programmatically
const task = await board.createTask({
  title: 'Fix edge case',
  column: 'Todo',
  metadata: {
    priority: 'High',
    estimate: 3
  }
})

Architecture

┌─────────────────────────────────────────────────────────────┐
│                      Applications                            │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │ CLI Tool │  │ Tauri App│  │ VSCode   │  │ Web App  │   │
│  │          │  │ (Desktop)│  │ Extension│  │ (Next.js)│   │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘   │
└───────┼──────────────┼─────────────┼──────────────┼────────┘
        │              │             │              │
        └──────────────┴──────┬──────┴──────────────┘
                              │
                   ┌──────────▼──────────┐
                   │  boarddown-napi     │  ◄── Node.js bindings
                   │  (napi-rs)          │      via napi-rs
                   └──────────┬──────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
┌───────▼───────┐  ┌──────────▼──────────┐  ┌──────▼───────┐
│  Core Types   │  │    Core Engine      │  │   Sync       │
│  (Schema)     │  │  (Parse/Query/CRDT) │  │  (CRDT/OT)   │
└───────┬───────┘  └──────────┬──────────┘  └──────┬───────┘
        │                     │                    │
        └─────────────────────┼────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
┌───────▼───────┐  ┌──────────▼──────────┐  ┌──────▼───────┐
│  FS Backend   │  │  SQLite Backend     │  │  Cloud       │
│  (.board.md)  │  │  (high-perf cache)  │  │  (optional)  │
└───────────────┘  └─────────────────────┘  └──────────────┘

Crate Structure (crates/)

Crate Description Dependencies
boarddown-core Parser, AST, validation engine, CRDT types serde, serde_yaml, chrono
boarddown-fs Filesystem storage backend, git integration boarddown-core, tokio, notify
boarddown-db SQLite backend, indexing, full-text search boarddown-core, sqlx, rusqlite
boarddown-sync Conflict resolution, operational transforms boarddown-core, automerge (optional)
boarddown-napi Node.js FFI bindings napi, napi-derive, boarddown-core
boarddown-cli Command-line interface clap, boarddown-fs, boarddown-db
boarddown-server Optional WebSocket sync server axum, boarddown-sync

The BoardDown Specification

File Format (.board.md)

BoardDown files are CommonMark compliant with YAML frontmatter and task syntax:

---
board: "Sprint 42"
id-prefix: "BD"
default-tags: ["sprint-42"]
storage: sqlite
autosync: true
---

# Sprint 42: Auth Refactor

## Meta
- Start: 2024-01-01
- End: 2024-01-15
- Velocity: 34

## Columns

### Todo
- [ ] {BD-001} Implement OAuth2 PKCE
      Description: |
        We need to support PKCE for mobile apps.
        See RFC 7636 for details.
      Tags: security, oauth
      Estimate: 5
      Epic: Auth-2024
      
- [ ] {BD-002} Session rotation  
      Tags: security
      Depends: {BD-001}
      Due: 2024-01-10

### In Progress
- [~] {BD-003} JWT middleware refactor
      Assign: alice@example.com
      Branch: feature/jwt-refactor
      Started: 2024-01-08

### Done
- [x] {BD-004} Test fixtures
      Closed: 2024-01-05
      PR: https://github.com/org/repo/pull/123

Syntax Reference

Task Status:

Syntax Status Emoji
- [ ] Todo
- [~] In Progress 🔄
- [>] Ready/Review 👀
- [x] Done
- [?] Blocked/Icebox 🧊
- [!] Urgent 🔥

Task IDs:

  • {BD-001} → Auto-linking, unique per workspace
  • Auto-generated if omitted: {board-prefix}-{timestamp}

Metadata (YAML inline):

# Standard fields
Assign: string | string[]     # Email or username
Tags: string[]               # Categorization
Priority: Low | Medium | High | Critical
Estimate: number             # Story points or hours
Due: ISO-8601 date
Started: ISO-8601 datetime
Closed: ISO-8601 datetime

# Extensible custom fields
Epic: string                 # Grouping
Branch: string               # Git association
PR: url                      # Pull request link
Refs: id[]                   # Related (non-blocking)
Depends: id[]               # Blocking dependencies

Multi-File Workspaces

For large projects, split boards across files:

my-project/
├── .boarddown/                 # Workspace config
│   ├── config.toml            # Global settings
│   ├── templates/             # Task templates
│   └── hooks/                 # Git hooks
├── product/
│   ├── roadmap.board.md       # High-level roadmap
│   └── backlog.board.md       # Unsorted ideas
├── sprints/
│   ├── sprint-42.board.md     # Current sprint
│   └── sprint-43.board.md     # Next sprint
├── teams/
│   ├── backend.board.md       # Team-specific board
│   └── frontend.board.md
└── archive/
    └── 2023/                  # Completed boards

Workspace Config (.boarddown/config.toml):

[workspace]
name = "My Project"
id-scheme = "TICKET-{board}-{seq}"
default-storage = "sqlite"

[storage.sqlite]
path = ".boarddown/cache.db"
enable-ft-index = true  # Full-text search

[storage.filesystem]
watch = true            # Auto-reload on file changes
git-auto-commit = false # Commit changes automatically

[sync]
enabled = true
provider = "crdt"       # "crdt", "git", or "custom"
conflict-resolution = "last-write-wins"

[hooks]
on-task-move = "scripts/notify.sh"
on-board-save = "git add ."

# Schema validation
[schema.custom-fields]
"risk-level" = { type = "enum", values = ["low", "medium", "high"] }
"story-points" = { type = "number", min = 0, max = 100 }

Rust API

Core Types

use boarddown_core::{
    Workspace, QueryBuilder, OrderBy,
    Storage, Error, SearchResult
};
use boarddown_schema::{
    Board, Task, TaskId, Column, Status,
    Metadata, Priority
};
use boarddown_fs::FilesystemStorage;
use boarddown_db::SqliteStorage;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Error> {
    // Initialize storage backend
    let storage = SqliteStorage::new(".boarddown/cache.db").await?;
    storage.init().await?;

    // Open workspace
    let ws = Workspace::builder()
        .path("./project")
        .storage(Arc::new(storage))
        .build()
        .await?;

    // Access board
    let board = ws.get_board("sprints/sprint-42").await?;

    // Type-safe task creation
    let task = Task::builder()
        .id(TaskId::new("SPRINT", 1))
        .title("Implement feature")
        .status(Status::Todo)
        .column("Todo")
        .metadata(Metadata::new()
            .with("assign", "alice@example.com")
            .with("estimate", 5)
            .with("tags", vec!["backend"]))
        .depends_on(TaskId::parse("BD-001")?)
        .build()?;

    // Save task
    ws.save_task("sprint-42", &task).await?;

    // Complex queries
    let results = board.query()
        .status_in([Status::Todo, Status::InProgress])
        .metadata("assign", "alice@example.com")
        .due_before(chrono::Utc::now() + chrono::Duration::days(7))
        .limit(10)
        .execute()
        .await?;

    Ok(())
}

Event System

// Subscribe to changes (cross-process safe)
let mut rx = ws.subscribe();

tokio::spawn(async move {
    while let Ok(event) = rx.recv().await {
        match event {
            BoardEvent::TaskCreated { board_id, task_id } => {
                println!("New task in {}: {}", board_id, task_id);
            }
            BoardEvent::TaskMoved { task_id, from, to } => {
                if to == Status::Done {
                    trigger_ci_pipeline(&task_id).await;
                }
            }
            BoardEvent::DependencyResolved { task_id, dependency } => {
                notify_assignee(&task_id, "Unblocked!").await;
            }
            _ => {}
        }
    }
});

Storage Traits

Implement custom backends:

use boarddown_core::storage::{Storage, Transaction};

pub struct RedisStorage { /* ... */ }

#[async_trait]
impl Storage for RedisStorage {
    async fn load_board(&self, id: &BoardId) -> Result<Board, Error>;
    async fn save_task(&self, task: &Task) -> Result<(), Error>;
    async fn query(&self, q: Query) -> Result<Vec<Task>, Error>;
    
    // Sync support
    async fn get_changeset(&self, since: Version) -> Result<Changeset, Error>;
    async fn apply_changeset(&self, cs: Changeset) -> Result<(), Error>;
}

TypeScript API

Via boarddown-napi (Node.js native addon):

import { 
  Workspace, 
  Board, 
  Status, 
  Priority,
  QueryBuilder 
} from '@boarddown/core'

// Async initialization
const ws = await Workspace.open('./project', {
  storage: 'sqlite',
  watch: true  // Hot-reload on file changes
})

// Type-safe board access
const board: Board = await ws.getBoard('sprint-42')

// Reactive queries (auto-update on changes)
const tasks = board.query({
  status: [Status.Todo, Status.InProgress],
  metadata: {
    priority: Priority.High,
    assign: 'alice@example.com'
  },
  orderBy: 'due',
  limit: 20
}).subscribe((tasks) => {
  console.log('Tasks updated:', tasks.length)
})

// Task manipulation
const task = await board.createTask({
  title: 'New feature',
  column: 'Todo',
  status: Status.Todo,
  metadata: {
    estimate: 5,
    tags: ['backend', 'urgent']
  },
  dependencies: ['BD-001']  // Auto-validates existence
})

// Move with business logic hooks
await board.moveTask(task.id, 'In Progress', {
  validate: (t) => t.dependencies.every(d => d.isResolved()),
  onSuccess: (t) => console.log(`Started ${t.id}`)
})

// Sync across devices
const sync = ws.enableSync({
  provider: 'crdt',
  url: 'wss://sync.boarddown.dev',
  auth: process.env.BOARDDOWN_TOKEN
})

sync.on('conflict', (conflict) => {
  console.log('Merge conflict:', conflict.resolve('local')) // or 'remote', 'manual'
})

React/Vue Integration

// React hook
import { useBoard, useTasks } from '@boarddown/react'

function SprintBoard({ boardId }: { boardId: string }) {
  const { board, loading, error } = useBoard(boardId)
  const { tasks, moveTask } = useTasks(boardId, {
    status: Status.InProgress
  })

  if (loading) return <Spinner />
  
  return (
    <Kanban
      columns={board.columns}
      onCardMove={(cardId, dest) => moveTask(cardId, dest)}
    />
  )
}

Storage Backends

Filesystem (Git-Optimized)

  • One file per board
  • Line-oriented diff for tasks
  • Frontmatter for metadata
  • Attachments in .boarddown/attachments/{board-id}/

SQLite (High Performance)

  • Indexed queries (< 1ms for 10k tasks)
  • Full-text search (SQLite FTS5)
  • JSON metadata column for schemaless fields
  • WAL mode for concurrent access

Hybrid Mode (Recommended)

[storage]
type = "hybrid"
cache = "sqlite"
source-of-truth = "filesystem"
sync-interval = "30s"  # Auto-sync FS <-> SQLite

Roadmap

Phase 1: Core (v0.1.0) - Current

  • Parser (markdown to AST)
  • FS storage backend
  • SQLite backend with FTS5
  • Basic TypeScript bindings (napi-rs)
  • Hybrid storage mode
  • Full-text search (FTS5)
  • Custom fields validation
  • Hook execution system
  • CLI with init, add, list, move commands
  • WebSocket sync server
  • CRDT conflict resolution

Phase 2: Ecosystem (v0.2.0)

  • React integration hooks
  • Vue integration
  • VSCode extension
  • CLI TUI (ratatui)
  • GitHub Actions integration

Phase 3: Enterprise (v0.3.0)

  • RBAC (Role-based access control)
  • Audit logging
  • Attachment support
  • Template system

Contributing

We welcome contributions! See CONTRIBUTING.md.

License

MIT