Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4b604bf
initial plan
dmytrokirpa Mar 2, 2026
09aa6bb
wip
dmytrokirpa Mar 2, 2026
d9ac838
feat: add fluentui-migrate-v8-to-v9 skill
dmytrokirpa Mar 2, 2026
e1af8a9
feat(cli): bootstrap fluentui-cli (#35829)
Hotell Mar 4, 2026
15f6120
feat(fluentui-cli): bootstrap commands infra (#35830)
Hotell Mar 4, 2026
5489cb9
Merge branch 'master' into experimental/fluent-cli
Hotell Mar 4, 2026
6d81861
Merge branch 'master' into experimental/fluent-cli
Hotell Mar 4, 2026
e568178
feat(fluent-cli): implement migrate v8-to-v9 command and skill (#35836)
dmytrokirpa Mar 5, 2026
7aff395
feat(cli): add banner (#35841)
Hotell Mar 6, 2026
5c51d78
feat(cli): implement `report` command with `info` and `usage` subcomm…
Hotell Apr 7, 2026
cb61c8e
feat(cli): implement `metadata` command for API surface extraction (#…
Hotell Apr 7, 2026
b4afd69
feat(cli): add skill instalation hint/detection to migrate cmd (#35957)
Hotell Apr 7, 2026
f43f5e2
feat(cli): expand binary alias so npx @fluentui/cli works by default …
Hotell Apr 8, 2026
022abc3
Merge branch 'master' into experimental/fluent-cli
Hotell Apr 8, 2026
0db9dd9
test(cli): mock skill-check in integration tests to prevent CI timeou…
Hotell Apr 8, 2026
f92690e
Merge branch 'master' into experimental/fluent-cli
Hotell Apr 9, 2026
cb06445
Merge remote-tracking branch 'upstream/master' into experimental/flue…
Hotell Sep 14, 2026
ee684e7
chore(cli): normalize package bin order
Hotell Sep 14, 2026
928ba32
chore(cli): remove experimental migration tooling
Hotell Sep 14, 2026
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 @@
---
Comment thread
Hotell marked this conversation as resolved.
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/
│ ├── metadata/ # API metadata command
│ │ ├── index.ts # CommandModule definition
│ │ ├── handler.ts # Lazy-loaded handler
│ │ └── impl/ # Parsing and formatting
│ └── report/ # Reporting command group
│ ├── index.ts # Registers report subcommands
│ ├── commands/ # info and usage definitions
│ └── impl/ # Lazy-loaded report implementations
```

### 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 keeps command implementations out of the startup path until the selected command runs.

### 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 reportCommand from './commands/report';
import metadataCommand from './commands/metadata';

export async function main(argv: string[]): Promise<void> {
await yargs(argv)
.scriptName('fluentui-cli')
.usage('$0 <command> [options]')
.command(reportCommand)
.command(metadataCommand)
.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('summary', 'Generate a summary', subBuilder => subBuilder, summaryHandler)
.command('details', 'Generate detailed output', subBuilder => subBuilder, detailsHandler)
.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
Loading