Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions CLI-DOCS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# CLI Output Management System

This system automatically captures and caches CLI command outputs in MDX files, converting them to SVG format for consistent rendering.

## Setup

1. Configure `ansi-run.json` with your example project path and starting commit hash:

```json
{
"exampleProjectPath": "/path/to/example/project",
"startingHash": "initial-commit-hash",
"config": {
"cacheDir": "content/cache",
"outputFormat": "svg"
}
}
```

2. Install dependencies:
```bash
pnpm install
```

## Usage

### Adding CLI Commands to MDX

Use special `cli` code blocks in your MDX files:

```mdx
Here's how to check status:

```cli
but status
```

The output will be automatically captured and cached.
```

### Restore Commands

To restore to a specific state before running commands, add a restore comment:

```mdx
{/* restore [commit-hash] */}

```cli
but status
```

This will run `but restore [commit-hash]` before executing the cli command.
```

### Updating CLI Outputs

Run the update script to process all MDX files and update CLI outputs:

```bash
pnpm update-cli
```

This will:
- Read your `ansi-run.json` configuration
- Change to your example project directory
- Restore to the starting hash
- Process all MDX files in `content/docs/`
- Execute CLI commands and capture outputs
- Convert outputs to SVG using ansi2html
- Cache outputs in `content/cache/[hash].svg`
- Update MDX files with hash references: ```cli [hash]
- Report any changes detected

### How It Works

1. **Processing**: The script finds all ````cli` blocks in MDX files
2. **Execution**: Commands are run in your configured example project
3. **Caching**: Output is converted to SVG and stored with a content hash
4. **Updates**: MDX blocks are updated with hash references
5. **Rendering**: The CliBlock component renders cached SVGs or shows placeholders

### File Structure

```
├── ansi-run.json # Configuration
├── content/
│ ├── cache/ # Cached SVG outputs
│ │ ├── abc123def456.svg
│ │ └── def789ghi012.svg
│ └── docs/ # MDX documentation files
│ └── commands/
│ └── status.mdx
├── scripts/
│ └── update-cli-outputs.js # Main processing script
└── app/
└── components/
├── CliBlock.tsx # Rendering component
└── remark-cli.ts # MDX transformer
```

### Example Workflow

1. Create a new MDX file with CLI commands:
```mdx
# Status Command

Check your workspace status:

```cli
but status
```
```

2. Run the update script:
```bash
pnpm update-cli
```

3. The script will show output like:
```
Processing: content/docs/commands/status.mdx
Found CLI command: but status
New CLI block found: but status
Updated: content/docs/commands/status.mdx
```

4. Your MDX file is now updated:
```mdx
# Status Command

Check your workspace status:

```cli [abc123def456]
but status
```
```

5. When rendered, users see the actual command output in SVG format.

## Troubleshooting

- **Missing outputs**: Run `pnpm update-cli` to generate missing cache files
- **Outdated outputs**: The script will detect hash changes and notify you
- **Command failures**: Failed commands will still be cached to show error output
- **Path issues**: Ensure your `ansi-run.json` paths are absolute and correct

### Updating CLI Outputs

Run the update script to process all MDX files and update CLI outputs:

```bash
export CLICOLOR_FORCE=1
export GIT_AUTHOR_DATE="2020-09-09 09:06:03 +0800"
export GIT_COMMITTER_DATE="2020-10-09 09:06:03 +0800"
pnpm update-cli
```

## Commands

The command "man pages" are copied from `../gitbutler/cli-docs` so that changes to the commands docs can be included in changes with the code.

To update the command man-pages, you can run ./scripts/sync-commands.sh


2 changes: 2 additions & 0 deletions app/(docs)/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Callout } from "fumadocs-ui/components/callout"
import { TypeTable } from "fumadocs-ui/components/type-table"
import { Accordion, Accordions } from "fumadocs-ui/components/accordion"
import ImageSection from "@/app/components/ImageSection"
import CliBlock from "@/app/components/CliBlock"
import type { ComponentProps, FC } from "react"

interface Param {
Expand Down Expand Up @@ -103,6 +104,7 @@ export default async function Page(props: { params: Promise<Param> }): Promise<R
Accordion,
Accordions,
ImageSection,
CliBlock,
blockquote: Callout as unknown as FC<ComponentProps<"blockquote">>,
APIPage: openapi.APIPage
}}
Expand Down
63 changes: 63 additions & 0 deletions app/components/CliBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import fs from 'fs/promises';
import path from 'path';

interface CliBlockProps {
hash?: string;
height?: string;
lang?: 'cli' | 'ansi';
children: React.ReactNode;
}

export default async function CliBlock({ hash, height, lang = 'cli', children }: CliBlockProps) {
// If no hash, render as regular code block
if (!hash) {
return (
<div className="rounded-lg border bg-muted p-4 mb-4">
<div className="font-mono text-base text-muted-foreground mb-2">$ {children}</div>
<div className="text-sm text-muted-foreground">
Run <code>pnpm update-cli</code> to generate output
</div>
</div>
);
}

// Determine which directory to use based on lang
const dir = lang === 'ansi'
? path.join(process.cwd(), 'public/cli-examples')
: path.join(process.cwd(), 'public/cache/cli-output');
const htmlPath = path.join(dir, `${hash}.html`);

try {
// Read the HTML file content
const fullHtmlContent = await fs.readFile(htmlPath, 'utf8');

// Extract content from body tag if present (for full HTML documents)
const bodyMatch = fullHtmlContent.match(/<body[^>]*>([\s\S]*)<\/body>/i);
const htmlContent = bodyMatch ? bodyMatch[1] : fullHtmlContent;

return (
<div className="rounded-lg border bg-muted overflow-hidden mb-4">
<div className="bg-gray-50 px-4 py-2 border-b">
<div className="font-mono text-base text-muted-foreground">$ {children}</div>
</div>
<div className="p-4 cli-output-container">
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
</div>
</div>
);
} catch (error) {
// HTML file not found, show placeholder
const errorMessage = lang === 'ansi'
? 'Output not found. Run ./scripts/sync-commands.sh to sync examples.'
: 'Output cache not found. Run pnpm update-cli to generate.';

return (
<div className="rounded-lg border bg-muted p-4 mb-4">
<div className="font-mono text-base text-muted-foreground mb-2">$ {children}</div>
<div className="text-sm text-muted-foreground">
{errorMessage} (hash: {hash}{height && `, height: ${height}`})
</div>
</div>
);
}
}
22 changes: 22 additions & 0 deletions app/components/ResizableIframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
interface ResizableIframeProps {
src: string
title: string
sandbox: string
className?: string
fixedHeight?: string
}

export default function ResizableIframe({ src, title, sandbox, className, fixedHeight }: ResizableIframeProps) {
const height = fixedHeight || '200px'
const minHeight = fixedHeight ? undefined : '200px'

return (
<iframe
src={src}
className={className}
style={{ height, minHeight }}
title={title}
sandbox={sandbox}
/>
)
}
69 changes: 69 additions & 0 deletions app/components/remark-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { visit } from 'unist-util-visit';
import type { Code } from 'mdast';
import type { Plugin } from 'unified';

export const remarkCli: Plugin = () => {
return (tree) => {
visit(tree, 'code', (node: Code) => {
// Handle both 'cli' and 'ansi' code blocks
if (node.lang === 'cli' || node.lang === 'ansi') {
const meta = node.meta || '';
let hash: string | undefined;
let height: string | undefined;

if (node.lang === 'ansi') {
// For ansi blocks, the meta is the hash directly
hash = meta.trim() || undefined;
} else {
// For cli blocks, parse the old format [hash, height]
const paramsMatch = meta.match(/\[([^\]]+)\]/);
if (paramsMatch) {
const params = paramsMatch[1].split(',').map(p => p.trim());
hash = params[0] || undefined;
height = params[1] || undefined;
}
}

// Build attributes array
const attributes = [];

// Always pass the lang attribute
attributes.push({
type: 'mdxJsxAttribute',
name: 'lang',
value: node.lang
});

if (hash) {
attributes.push({
type: 'mdxJsxAttribute',
name: 'hash',
value: hash
});
}
if (height) {
attributes.push({
type: 'mdxJsxAttribute',
name: 'height',
value: height
});
}

// Transform to JSX component
const componentNode = {
type: 'mdxJsxFlowElement',
name: 'CliBlock',
attributes,
children: [
{
type: 'text',
value: node.value
}
]
};

Object.assign(node, componentNode);
}
});
};
};
17 changes: 17 additions & 0 deletions app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
@tailwind components;
@tailwind utilities;

/* Import JetBrains Mono Nerd Font */
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap');

@import "open-props/easings";
@import "open-props/animations";

Expand Down Expand Up @@ -62,3 +65,17 @@ iframe[src*="youtube"] {
.font-accent {
font-family: var(--font-accent);
}

/* Override fumadocs description paragraph margin */
p.mb-8.text-lg.text-fd-muted-foreground,
p.mb-8[class*="text-fd-muted-foreground"],
p[class*="mb-8"][class*="text-lg"][class*="text-fd-muted-foreground"] {
@apply mb-4 !important;
}

/* CLI output styling - remove vertical padding from pre tags */
.cli-output-container pre {
margin: 0;
padding: 0;
line-height: 22px;
}
10 changes: 10 additions & 0 deletions cli-examples-run.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"exampleProjectPath": "/Users/schacon/projects/why",
"startingHash": "f0f437258043",
"ansi_senor_path": "/Users/schacon/.cargo/bin/ansi-senor",
"but_path": "/usr/local/bin/but",
"config": {
"cacheDir": "public/cache/cli-output",
"outputFormat": "html"
}
}
Loading