Skip to content
Draft
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
116 changes: 116 additions & 0 deletions .github/skills/fluentui-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
Copy link

@github-actions github-actions bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/CalendarCompat 4 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/CalendarCompat.multiDayView - High Contrast.default.chromium.png 1226 Changed
vr-tests-react-components/CalendarCompat.multiDayView - Dark Mode.default.chromium.png 1102 Changed
vr-tests-react-components/CalendarCompat.multiDayView - RTL.default.chromium.png 493 Changed
vr-tests-react-components/CalendarCompat.multiDayView.default.chromium_1.png 495 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default.submenus open.chromium.png 413 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default - RTL.submenus open.chromium.png 599 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.chromium.png 915 Changed
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 516 Changed
vr-tests-react-components/TagPicker 4 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - High Contrast.disabled input hover.chromium.png 1319 Changed
vr-tests-react-components/TagPicker.disabled - Dark Mode.disabled input hover.chromium.png 658 Changed
vr-tests-react-components/TagPicker.disabled - RTL.disabled input hover.chromium.png 635 Changed
vr-tests-react-components/TagPicker.disabled.disabled input hover.chromium.png 677 Changed
vr-tests-web-components/Avatar 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-web-components/Avatar. - Dark Mode.normal.chromium_1.png 298 Changed
vr-tests-web-components/Avatar. - Dark Mode.normal.chromium.png 10380 Changed
vr-tests-web-components/MenuList 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-web-components/MenuList. - Dark Mode.normal.chromium.png 498 Changed
vr-tests/Callout 5 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/Callout.Gap space 25.default.chromium.png 2195 Changed
vr-tests/Callout.Bottom right edge - RTL.default.chromium.png 1124 Changed
vr-tests/Callout.Beak 25.default.chromium.png 2198 Changed
vr-tests/Callout.No callout width specified.default.chromium.png 2143 Changed
vr-tests/Callout.Right top edge.default.chromium.png 1126 Changed
vr-tests/Keytip 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/Keytip.Disabled.default.chromium.png 26 Changed
vr-tests/Keytip.Offset.default.chromium.png 86 Changed
vr-tests/react-charting-AreaChart 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/react-charting-AreaChart.Custom Accessibility.default.chromium.png 11 Changed
vr-tests/react-charting-MultiStackBarChart 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/react-charting-MultiStackBarChart.Basic_PartToWhole.default.chromium.png 359 Changed
vr-tests/react-charting-MultiStackBarChart.Basic_PartToWhole - RTL.default.chromium.png 343 Changed
vr-tests/react-charting-MultiStackBarChart.Basic_Absolute - RTL.default.chromium.png 343 Changed
vr-tests/react-charting-SankeyChart 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/react-charting-SankeyChart.PlaceHolder - RTL.default.chromium.png 77 Changed
vr-tests/react-charting-VerticalBarChart 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/react-charting-VerticalBarChart.Basic - Secondary Y Axis.default.chromium.png 3 Changed

There were 5 duplicate changes discarded. Check the build logs for more information.

name: fluentui-cli
description: 'Guides working with @fluentui/cli — the internal Fluent UI command-line tool. Use when asked to add a new CLI command, modify an existing command, understand CLI architecture, debug CLI issues, or work with the CLI build/test workflow. Covers: architecture overview, yargs conventions, lazy-loading pattern, testing, and available Nx generators.'
---

# `@fluentui/cli`

The `@fluentui/cli` package (`tools/cli/`) is the internal Fluent UI command-line tool built with **yargs**. It uses a modular, lazy-loading architecture where each command lives in its own directory and is only loaded at runtime when invoked.

## Architecture

### File Structure

```
tools/cli/
├── bin/fluentui-cli.js # Node entry point (requires compiled output)
├── src/
│ ├── cli.ts # Main yargs setup, registers all commands
│ ├── index.ts # Public API re-exports
│ ├── utils/
│ │ ├── index.ts # Barrel exports
│ │ └── types.ts # Shared CommandHandler type
│ └── commands/
│ ├── migrate/ # Command module
│ │ ├── index.ts # CommandModule definition (eagerly loaded)
│ │ ├── handler.ts # Handler implementation (lazy-loaded)
│ │ └── handler.spec.ts # Tests
│ └── report/ # Command module
│ ├── index.ts
│ ├── handler.ts
│ └── handler.spec.ts
```

### Lazy Loading Pattern

Command definitions (name, description, options builder) are eagerly imported — they are lightweight. The actual handler logic is **lazy-loaded via dynamic `import()`** only when the command is invoked:

```typescript
// index.ts — always loaded (lightweight)
handler: async argv => {
// handler.ts only loaded when this specific command runs
const { handler } = await import('./handler');
return handler(argv);
},
```

This means running `fluentui-cli migrate` will never load the code for `report` or any other command.

### CommandHandler Type

All handlers use the shared `CommandHandler<T>` type from `src/utils/types.ts`:

```typescript
import type { ArgumentsCamelCase } from 'yargs';

export type CommandHandler<T = {}> = (argv: ArgumentsCamelCase<T>) => Promise<void>;
```

### Command Registration in cli.ts

Each command is imported and registered in `tools/cli/src/cli.ts`:

```typescript
import yargs from 'yargs';
import migrateCommand from './commands/migrate';
import reportCommand from './commands/report';

export async function main(argv: string[]): Promise<void> {
await yargs(argv)
.scriptName('fluentui-cli')
.usage('$0 <command> [options]')
.command(migrateCommand)
.command(reportCommand)
.demandCommand(1, 'You need to specify a command to run.')
.help()
.strict()
.parse();
}
```

## Build & Test

```sh
# Build the CLI
yarn nx run cli:build

# Run tests
yarn nx run cli:test

# Test --help output
node tools/cli/bin/fluentui-cli.js --help
node tools/cli/bin/fluentui-cli.js <command-name> --help
```

## Conventions

- **Always use the Nx generator** to scaffold new commands — do not create command files manually. See the [adding commands](references/adding-commands.md) reference.
- Place shared utilities in `tools/cli/src/utils/` and export through the barrel file.
- Every command must support `--help` (handled by yargs `.help()` in the builder).
- Handler files must export a named `handler` constant typed with `CommandHandler<T>`.
- Tests live adjacent to handlers as `handler.spec.ts`.

## Common Patterns

### Subcommands (nested commands)

If a command needs subcommands, use yargs nested command pattern in the builder:

```typescript
builder: yargs =>
yargs
.command('run', 'Run migrations', subBuilder => subBuilder, subHandler)
.command('list', 'List available migrations', subBuilder => subBuilder, subHandler)
.demandCommand(1)
.help(),
```
135 changes: 135 additions & 0 deletions .github/skills/fluentui-cli/references/adding-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Adding a New CLI Command

## Step 1 — Scaffold the Command

Run the `cli-command` Nx generator:

```sh
yarn nx g @fluentui/workspace-plugin:cli-command <command-name> --description "<short description>"
```

### Example

```sh
yarn nx g @fluentui/workspace-plugin:cli-command analyze --description "Analyze bundle sizes"
```

### What Gets Generated

```
tools/cli/src/commands/<command-name>/
├── index.ts # Yargs CommandModule definition (lightweight, eagerly loaded)
├── handler.ts # Handler implementation (lazy-loaded via dynamic import)
└── handler.spec.ts # Jest unit tests for the handler
```

The generator also **automatically registers** the new command in `tools/cli/src/cli.ts` by:

1. Adding an import statement for the command module
2. Adding a `.command()` registration call

Preview what will be generated without writing to disk:

```sh
yarn nx g @fluentui/workspace-plugin:cli-command <command-name> --dry-run
```

## Step 2 — Implement the Handler

Open `tools/cli/src/commands/<command-name>/handler.ts` and implement the command logic:

```typescript
import type { CommandHandler } from '../../utils/types';

// Define the shape of your command's arguments
interface AnalyzeArgs {
project?: string;
verbose?: boolean;
}

export const handler: CommandHandler<AnalyzeArgs> = async argv => {
const { project, verbose } = argv;

// Your implementation here
console.log(`Analyzing${project ? ` project: ${project}` : ''}...`);
};
```

## Step 3 — Add Options and Arguments

Edit `tools/cli/src/commands/<command-name>/index.ts` to add yargs options in the `builder`:

```typescript
import type { CommandModule } from 'yargs';

const command: CommandModule = {
command: 'analyze',
describe: 'Analyze bundle sizes',
builder: yargs =>
yargs
.option('project', {
alias: 'p',
type: 'string',
describe: 'Project name to analyze',
})
.option('verbose', {
type: 'boolean',
default: false,
describe: 'Show detailed output',
})
.version(false)
.help(),
handler: async argv => {
const { handler } = await import('./handler');
return handler(argv);
},
};

export default command;
```

## Step 4 — Write Tests

Update `tools/cli/src/commands/<command-name>/handler.spec.ts` with meaningful tests:

```typescript
import { handler } from './handler';

describe('analyze handler', () => {
let logSpy: jest.SpyInstance;

beforeEach(() => {
logSpy = jest.spyOn(console, 'log').mockImplementation();
});

afterEach(() => {
logSpy.mockRestore();
});

it('should analyze all projects when no project specified', async () => {
await handler({ _: ['analyze'], $0: 'fluentui-cli' });

expect(logSpy).toHaveBeenCalledWith('Analyzing...');
});

it('should analyze specific project when specified', async () => {
await handler({ _: ['analyze'], $0: 'fluentui-cli', project: 'react-button' });

expect(logSpy).toHaveBeenCalledWith('Analyzing project: react-button...');
});
});
```

## Step 5 — Verify

```sh
# Build the CLI
yarn nx run cli:build

# Run tests
yarn nx run cli:test

# Test --help output
node tools/cli/bin/fluentui-cli.js --help
node tools/cli/bin/fluentui-cli.js <command-name> --help
```
Loading