---
title: "Fumadocs CLI"
description: "The Fumadocs CLI (@fumadocs/cli) is an operational tool designed to scaffold projects, manage component installations, customize documentation layouts, and process documentation structures. Operati..."
last_updated: "2026-10-12T07:53:59.736226+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tooling/fumadocs-cli"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/cli/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/index.ts)
- [packages/preview/src/cli/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/cli/index.ts)
- [packages/cli/src/commands/add.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/add.ts)
- [packages/cli/src/commands/customise.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/customise.ts)
- [packages/cli/src/commands/file-tree.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/file-tree.ts)
- [packages/cli/src/commands/export-epub.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/export-epub.ts)
- [packages/create-app/src/bin.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/create-app/src/bin.ts)
- [packages/sanity/registry/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/index.ts)
- [packages/cli/src/commands/shared.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/shared.ts)
- [packages/base-ui/registry/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/registry/index.ts)
- [packages/cli/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/package.json)
- [packages/python/fumapy/__init__.py](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/fumapy/__init__.py)
- [packages/core/src/source/client/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx)
- [packages/epub/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/epub/src/index.ts)
- [packages/radix-ui/registry/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts)
- [packages/asyncapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx)
- [packages/preview/src/pages/...slugs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/create-app/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/create-app/package.json)
- [packages/sanity/registry/files.component.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/files.component.tsx)
- [packages/shared/registry.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/shared/registry.ts)
- [packages/python/pyproject.toml](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/pyproject.toml)
- [packages/python/src/convert.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/src/convert.ts)
- [packages/preview/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/package.json)
- [packages/cli/src/registry/installer.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/registry/installer.ts)
- [packages/cli/src/registry/plugins/preserve.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/registry/plugins/preserve.ts)
- [packages/python/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/package.json)
- [packages/python/src/components/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/src/components/index.tsx)
- [packages/epub/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/epub/package.json)
- [packages/shadcn/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/shadcn/src/index.ts)
- [packages/local-md/src/bin.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/bin.ts)
</details>

## Overview

The Fumadocs CLI (`@fumadocs/cli`) is an operational tool designed to scaffold projects, manage component installations, customize documentation layouts, and process documentation structures. Operating through the `commander` package, it offers commands for configuration initialization, component downloads via `add`, interactive layout modifications via `customise`, [EPUB Book Export](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tooling/epub-book-export)ing, and directory tree parsing into MDX or JavaScript components.

Sources: [packages/cli/package.json:2-15](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/package.json#L2-L15), [packages/cli/src/index.ts:15-106](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/index.ts#L15-L106)

## CLI Architecture and Command Dispatch

### Entry Point and Command Registration

The Fumadocs CLI executable is defined in `packages/cli/src/index.ts` and configured as a Node.js command-line tool via the `bin` field in `packages/cli/package.json`. It instantiates a root `Command` object from `commander`, reads its version metadata from `packageJson.version`, and registers global options such as `--config <string>`. When executed without arguments, the root action invokes `initConfig()`, which checks for or creates a `./cli.json` configuration file and logs status messages using `picocolors`.

```json
"bin": {
  "cli": "./dist/index.js"
},
```
Sources: [packages/cli/package.json:13-15](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/package.json#L13-L15)

```typescript
const program = new Command().option('--config <string>');

program
  .name('fumadocs')
  .description('CLI to setup Fumadocs, init a config')
  .version(packageJson.version)
  .action(async () => {
    if (await initConfig()) {
      console.log(picocolors.green('Initialized a `./cli.json` config file.'));
    } else {
      console.log(picocolors.redBright('A config file already exists.'));
    }
  });
```
Sources: [packages/cli/src/index.ts:15-27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/index.ts#L15-L106)

Subcommands are registered directly on the `program` instance or nested under parent command groups. The available CLI commands, their aliases, arguments, options, and actions are structured as follows:

| Command | Aliases | Arguments | Options | Action / Handler |
| :--- | :--- | :--- | :--- | :--- |
| `fumadocs` (default) | — | — | `--config <string>` | Initializes `./cli.json` via `initConfig()`. |
| `customise` | `customize` | — | `--dir <string>`, `--config <string>` | Loads config and runs `customise` with a registry client. |
| `add` | — | `[components...]` | `--dir <string>`, `--config <string>` | Downloads components using a registry client and config. |
| `export epub` | — | — | `--framework <name>`, `--output <path>`, `--scaffold-only` | Exports documentation to EPUB format via `exportEpub`. |
| `tree` | — | `[json_or_args]`, `[output]` | `--js`, `--no-root`, `--import-name <string>`, `--config <string>` | Parses directory trees or JSON nodes into MDX or JS formats. |

Sources: [packages/cli/src/index.ts:17-106](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/index.ts#L17-L106)

### Registry Connector Resolution and Configuration

Registry source resolution is handled by `createClientFromDir`, which maps shortcut keywords to predefined remote URLs or distinguishes between HTTP endpoints and local filesystem paths. 

```typescript
const dirShortcuts: Record<string, string> = {
  ':preview': 'https://preview.fumadocs.dev/registry',
  ':dev': 'http://localhost:3000/registry',
};
```
Sources: [packages/cli/src/index.ts:39-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/index.ts#L39-L42)

The shortcut mapping supports the following expansion values:

| Shortcut | Target URL |
| :--- | :--- |
| `:preview` | `https://preview.fumadocs.dev/registry` |
| `:dev` | `http://localhost:3000/registry` |

Sources: [packages/cli/src/index.ts:39-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/index.ts#L39-L42)

> [!NOTE]
> If the directory parameter does not match `:preview` or `:dev`, `createClientFromDir` evaluates whether the string begins with `http://` or `https://`. If it does, it instantiates an `HttpRegistryConnector`; otherwise, it falls back to a `LocalRegistryConnector`.

```typescript
function createClientFromDir(dir = 'https://fumadocs.dev/registry') {
  if (dir in dirShortcuts) dir = dirShortcuts[dir];

  return dir.startsWith('http://') || dir.startsWith('https://')
    ? new HttpRegistryConnector(dir)
    : new LocalRegistryConnector(dir);
}
```
Sources: [packages/cli/src/index.ts:108-114](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/index.ts#L108-L114)

## Registry Component Installation Flow

### Component Selection and Interactive Prompts

The component addition command (`add`) managed in `packages/cli/src/commands/add.ts` facilitates downloading and integrating UI elements, layouts, and integration components into a project. When invoked without explicit component arguments (`input.length === 0`), the CLI displays a loading spinner and fetches registry information using `connector.fetchRegistryInfo(subRegistry)`. It constructs a list of selectable items by scanning across three distinct sources: common components (`undefined` sub-registry with prefix `common`), Sanity components (`fumadocs/sanity` with prefix `sanity`), and the UI library registry configured by `config.uiLibrary` (mapped via `UIRegistries`) with prefix `ui`. 

```typescript
const subRegistry = UIRegistries[config.uiLibrary];

if (input.length === 0) {
  const spin = spinner();
  spin.start('fetching registry');

  async function scan(subRegistry?: string, prefix?: string): Promise<AddOption[]> {
    const info = await connector.fetchRegistryInfo(subRegistry);

    return info.indexes.map((item) => ({
      label: `${prefix ? `${picocolors.bold(prefix)} - ` : ''}${item.title ?? item.name}`,
      value: { name: item.name, subRegistry },
      hint: item.description,
    }));
  }

  spin.stop(picocolors.bold(picocolors.greenBright('registry fetched')));
  const value = await autocompleteMultiselect({
    message: 'Select components to install',
    options: [
      ...(await scan(undefined, 'common')),
      ...(await scan('fumadocs/sanity', 'sanity')),
      ...(await scan(subRegistry, 'ui')),
    ].sort((a, b) => a.label.localeCompare(b.label)),
  });
```
Sources: [packages/cli/src/commands/shared.ts:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/shared.ts#L1-L4), [packages/cli/src/commands/add.ts:22-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/add.ts#L22-L46)

The UI registries mapping defines the sub-registry paths associated with supported UI libraries:

| UI Library Key | Sub-Registry Path |
| :--- | :--- |
| `base-ui` | `fumadocs/base-ui` |
| `radix-ui` | `fumadocs/radix-ui` |

Sources: [packages/cli/src/commands/shared.ts:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/shared.ts#L1-L4)

Presented through `@clack/prompts`'s `autocompleteMultiselect`, the sorted options allow users to select multiple components simultaneously. If the user cancels the prompt via `isCancel(value)`, the command outputs an ending message and exits gracefully. When components are provided directly as arguments via the command line, the CLI checks each item against `connector.hasComponent(item)` to assign the appropriate sub-registry or defaults to the configured UI registry.

```typescript
  if (isCancel(value)) {
    outro('Ended');
    return;
  }

  targets = value;
} else {
  targets = await Promise.all(
    input.map(async (item) =>
      (await connector.hasComponent(item)) ? { name: item } : { subRegistry, name: item },
    ),
  );
}
```
Sources: [packages/cli/src/commands/add.ts:48-60](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/add.ts#L48-L60)

> [!WARNING]
> If a concurrent installation is attempted while `this.interactive` is already populated in `FumadocsComponentInstaller`, an error stating `cannot install while installing another component` is thrown, preventing race conditions during file writing.

```typescript
async installInteractive(name: string, subRegistry?: string): Promise<void> {
  if (this.interactive) {
    throw new Error(`cannot install while installing another component`);
  }

```
Sources: [packages/cli/src/registry/installer.ts:54-58](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/registry/installer.ts#L54-L58)

### Installer Configuration and Layout Preservation

`FumadocsComponentInstaller` extends `ComponentInstaller` from `fuma-cli/registry/installer`, binding output directories to configuration aliases and registering plugins such as `pluginPreserveLayouts()`. 

```typescript
export class FumadocsComponentInstaller extends ComponentInstaller {
  private interactive: {
    name: string;
    spin: SpinnerResult;
  } | null = null;

  constructor(connector: RegistryConnector, config: LoadedConfig, cwd?: string) {
    super(connector, {
      cwd,
      framework: config.framework,
      outDir: {
        base: config.baseDir,
        components: config.aliases.componentsDir,
        css: config.aliases.cssDir,
        layout: config.aliases.layoutDir,
        lib: config.aliases.libDir,
        ui: config.aliases.uiDir,
      },
      io: {
        onWarn: (message) => {
          this.interactive?.spin.message(message);
        },
        confirmFileOverride: async (options) => {
          if (!this.interactive) return true;
          const { name, spin } = this.interactive;
          spin.clear();
          const value = await confirm({
            message: `Do you want to override ${options.path}?`,
            initialValue: false,
          });
          if (isCancel(value)) {
            outro('Installation terminated');
            process.exit(0);
          }
          spin.start(picocolors.bold(picocolors.cyanBright(`Installing ${name}`)));
          return value;
        },
        onFileDownloaded: (options) => {
          this.interactive?.spin.message(options.path);
        },
      },
      plugins: [pluginPreserveLayouts()],
    });
  }
```
Sources: [packages/cli/src/registry/installer.ts:9-52](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/registry/installer.ts#L9-L52)

The `pluginPreserveLayouts` plugin defined in `packages/cli/src/registry/plugins/preserve.ts` intercepts sub-component trees and import statements to maintain references to built-in layout packages unless a layout itself is being installed directly. The tracked layout components include:

| Layout Identifier | Mapped Package / Specifier |
| :--- | :--- |
| `layouts/home` | `fumadocs-ui/layouts/home` |
| `layouts/flux` | `fumadocs-ui/layouts/flux` |
| `layouts/notebook` | `fumadocs-ui/layouts/notebook` |
| `layouts/docs` | `fumadocs-ui/layouts/docs` |
| `layouts/shared` | `fumadocs-ui/layouts/shared` |

Sources: [packages/cli/src/registry/plugins/preserve.ts:7-30](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/registry/plugins/preserve.ts#L7-L30), [packages/cli/src/registry/plugins/preserve.ts:43-50](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/registry/plugins/preserve.ts#L43-L50)

> [!NOTE]
> When downloading files, `FumadocsComponentInstaller` hooks into `confirmFileOverride` to prompt the user via `@clack/prompts` if an existing file conflicts with a downloaded registry asset, terminating the process if cancelled.

```typescript
confirmFileOverride: async (options) => {
  if (!this.interactive) return true;
  const { name, spin } = this.interactive;
  spin.clear();
  const value = await confirm({
    message: `Do you want to override ${options.path}?`,
    initialValue: false,
  });
  if (isCancel(value)) {
    outro('Installation terminated');
    process.exit(0);
  }
  spin.start(picocolors.bold(picocolors.cyanBright(`Installing ${name}`)));
  return value;
},
```
Sources: [packages/cli/src/registry/installer.ts:31-45](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/registry/installer.ts#L31-L45)

### Dependency Resolution and Package Manager Execution

After downloading target components, `installInteractive` evaluates required dependencies through `deps.hasRequired()`. If missing dependencies or devDependencies are identified, they are displayed inside a boxed prompt using `@clack/prompts`. 

```typescript
try {
  this.interactive = { name, spin };
  const deps = await super.install(name, subRegistry).then((res) => res.deps());
  spin.stop(picocolors.bold(picocolors.greenBright(`${name} installed`)));

  if (deps.hasRequired()) {
    log.message();
    box([...deps.dependencies, ...deps.devDependencies].join('\n'), 'New Dependencies');
    const pm = (await detectPackageManager())?.name ?? 'npm';
```
Sources: [packages/cli/src/registry/installer.ts:62-70](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/registry/installer.ts#L62-L70)

The CLI detects the local project's package manager via `detectPackageManager()`, defaulting to `npm` if no manager is found. Users are prompted to confirm whether they want to install the packages immediately with the detected manager.

```typescript
const pm = (await detectPackageManager())?.name ?? 'npm';
const value = await confirm({
  message: `Do you want to install with ${pm}?`,
});

if (isCancel(value)) {
  outro('Installation terminated');
  process.exit(0);
}

```
Sources: [packages/cli/src/registry/installer.ts:70-79](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/registry/installer.ts#L70-L79)

If confirmed, the installer starts a spinner and invokes `deps.installRequired(pm)` to execute the installation. If declined, it invokes `deps.writeRequired()` to write declarations without running the package manager.

```typescript
  if (value) {
    const spin = spinner({
      errorMessage: 'Failed to install dependencies',
    });
    spin.start('Installing dependencies');
    await deps.installRequired(pm);
    spin.stop('Dependencies installed');
  } else {
    await deps.writeRequired();
  }
}
```
Sources: [packages/cli/src/registry/installer.ts:80-90](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/registry/installer.ts#L80-L90)

## Interactive Layout and Slot Customization

### Layout Selection and Customization Targets

The `customise` command guides users through interactive selection prompts using `@clack/prompts` to choose a UI layout and target parts for customization based on the configured UI library registry. The available layout targets and their identifiers are defined with specific integration hints.

| Layout Option | Identifier (`id`) | Target Registry Path | Description |
| :--- | :--- | :--- | :--- |
| Docs Layout | `docs` | `layouts/docs` | The default documentation layout |
| Notebook Layout | `notebook` | `layouts/notebook` | A more compact variant of the docs layout |
| Flux Layout | `flux` | `layouts/flux` | The experimental variant of the docs layout |
| Home Layout | `home` | `layouts/home` | The layout for non-documentation pages |

Sources: [packages/cli/src/commands/customise.ts:31-89](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/customise.ts#L31-L89), [packages/cli/src/commands/shared.ts:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/shared.ts#L1-L4)

When a layout other than `home` is selected, users can choose between installing the entire layout, replacing and rewriting from minimal styles via `layouts/docs-min`, or customizing specific component slots by parsing unlisted registry indexes matching `slots/${selected.id}`.

```typescript
return select<TargetInfo>({
  message: 'Which part do you want to customize?',
  options: [
    {
      label: 'All',
      hint: 'install the entire layout',
      value: selected,
    },
    {
      label: 'Replace & rewrite from minimal styles',
      hint: 'for those who want to build their own UI from ground up',
      value: {
        id: 'docs-min',
        targets: [{ name: 'layouts/docs-min' }],
        print() {
          printLayout(
            ['fumadocs-ui/layouts/docs', '@/layouts/docs'],
            ['fumadocs-ui/layouts/docs/page', '@/layouts/docs/page'],
          );
        },
      },
    },
    ...info.unlistedIndexes.flatMap((index) => {
      const prefix = `slots/${selected.id}`;
      if (!index.name.startsWith(prefix)) return [];
      let name = index.name.slice(prefix.length + 1);

      if (name.startsWith('page/')) {
        name = name.slice('page/'.length);

        return {
          label: `Page: ${name}`,
          hint: "only replace a part of layout's page, useful for adjusting details",
          value: {
            id: index.name,
            targets: [{ subRegistry, name: index.name }],
            print() {
              printSlot({
                at: `@/layouts/${selected.id}/page/slots/${name}`,
                layoutId: selected.id,
                name,
                isPage: true,
              });
            },
          } as TargetInfo,
        };
      }

      return {
        label: `Layout: ${name}`,
        hint: 'only replace a part of layout, useful for adjusting details',
        value: {
          id: index.name,
          targets: [{ subRegistry, name: index.name }],
          print() {
            printSlot({
              at: `@/layouts/${selected.id}/slots/${name}`,
              layoutId: selected.id,
              name,
              isPage: false,
            });
          },
        } as TargetInfo,
      };
    }),
  ],
});
```
Sources: [packages/cli/src/commands/customise.ts:94-160](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/customise.ts#L94-L160)

> [!NOTE]
> For non-home layouts, slot customization paths distinguish between page-level parts prefixed with `page/` and general layout slots, generating import mapping advice for either `<DocsPage />` or `<DocsLayout />` components accordingly.

```typescript
if (name.startsWith('page/')) {
  name = name.slice('page/'.length);

  return {
    label: `Page: ${name}`,
    hint: "only replace a part of layout's page, useful for adjusting details",
    value: {
      id: index.name,
      targets: [{ subRegistry, name: index.name }],
      print() {
        printSlot({
          at: `@/layouts/${selected.id}/page/slots/${name}`,
          layoutId: selected.id,
          name,
          isPage: true,
        });
      },
    } as TargetInfo,
  };
}

return {
  label: `Layout: ${name}`,
  hint: 'only replace a part of layout, useful for adjusting details',
  value: {
    id: index.name,
    targets: [{ subRegistry, name: index.name }],
    print() {
      printSlot({
        at: `@/layouts/${selected.id}/slots/${name}`,
        layoutId: selected.id,
        name,
        isPage: false,
      });
    },
  } as TargetInfo,
};
```
Sources: [packages/cli/src/commands/customise.ts:121-157](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/customise.ts#L121-L157)

## File Tree Inspection and Formatting

### Directory Parsing and JsonTreeNode Schema

The `tree` command processes directory structures by accepting either a raw JSON string argument or scanning a target directory via `runTree`. The underlying AST nodes are defined by the `JsonTreeNode` union type, supporting files, directories, reports, and links.

```typescript
export type JsonTreeNode =
  | {
      type: 'file';
      name: string;
    }
  | {
      type: 'directory';
      name: string;
      contents: JsonTreeNode[];
    }
  | {
      type: 'report';
    }
  | {
      type: 'link';
      name: string;
    };
```
Sources: [packages/cli/src/commands/file-tree.ts:1-18](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/file-tree.ts#L1-L18)

When parsing inputs, valid nodes are filtered against a known scanned list containing `file`, `directory`, and `link` node types.

| Node Type (`type`) | Properties | Description |
| :--- | :--- | :--- |
| `file` | `name: string` | Represents an individual file within the tree |
| `directory` | `name: string`, `contents: JsonTreeNode[]` | Represents a folder containing nested child nodes |
| `link` | `name: string` | Represents a symbolic or external link node |
| `report` | *None* | Represents an execution report or diagnostic entry |

Sources: [packages/cli/src/commands/file-tree.ts:1-19](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/file-tree.ts#L1-L19)

> [!NOTE]
> If a directory node contains a single nested directory child, the parser flattens the path by combining their names (e.g., `item.name/child.name`) to avoid rendering redundant single-child nesting levels.

```typescript
if (item.type === 'directory') {
  if (item.contents.length === 1 && 'name' in item.contents[0]) {
    const child = item.contents[0];

    return toNode({
      ...child,
      name: `${item.name}/${child.name}`,
    });
  }
```
Sources: [packages/cli/src/commands/file-tree.ts:27-35](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/file-tree.ts#L27-L35)

### Conversion to MDX and JavaScript Structures

The `treeToMdx` function recursively transforms `JsonTreeNode` arrays into MDX component syntax utilizing `<Files>`, `<Folder>`, and `<File>` tags. When the `noRoot` option is enabled and the root input consists of a single directory, the top-level container is unwrapped so that its internal contents become the primary children.

```typescript
export function treeToMdx(input: JsonTreeNode[], noRoot = false): string {
  function toNode(item: JsonTreeNode): string {
    if (item.type === 'file' || item.type === 'link') {
      return `<File name=${JSON.stringify(item.name)} />`;
    }

    if (item.type === 'directory') {
      if (item.contents.length === 1 && 'name' in item.contents[0]) {
        const child = item.contents[0];

        return toNode({
          ...child,
          name: `${item.name}/${child.name}`,
        });
      }

      return `<Folder name=${JSON.stringify(item.name)}>
${item.contents.map(toNode).filter(Boolean).join('\n')}
</Folder>`;
    }

    return '';
  }

  let children = input.filter((v) => scanned.includes(v.type));

  if (noRoot && children.length === 1 && input[0].type === 'directory') {
    children = input[0].contents;
  }

  return `<Files>
${children.map(toNode).filter(Boolean).join('\n')}
</Files>`;
}
```
Sources: [packages/cli/src/commands/file-tree.ts:21-54](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/file-tree.ts#L21-L54)

```typescript
export function treeToJavaScript(
  input: JsonTreeNode[],
  noRoot?: boolean,
  importName = 'fumadocs-ui/components/files',
): string {
  return `import { File, Files, Folder } from ${JSON.stringify(importName)}

export default (${treeToMdx(input, noRoot)})`;
}
```
Sources: [packages/cli/src/commands/file-tree.ts:56-64](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/file-tree.ts#L56-L64)

> [!WARNING]
> The CLI automatically selects JavaScript output format if the `--js` flag is passed or if the output file path extension matches `.js`, `.tsx`, or `.jsx`; otherwise, it outputs raw MDX components.

```typescript
const jsExtensions = ['.js', '.tsx', '.jsx'];
const noRoot = !root;
let nodes: JsonTreeNode[];

try {
  nodes = JSON.parse(str ?? '') as JsonTreeNode[];
} catch {
  nodes = await runTree(str ?? './');
}

const out =
  js || (output && jsExtensions.includes(path.extname(output)))
    ? treeToJavaScript(nodes, noRoot, importName)
    : treeToMdx(nodes, noRoot);
```
Sources: [packages/cli/src/index.ts:84-97](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/index.ts#L84-L97)

## Registry Source Organization and Indexing

### UI Library Catalog Structures

Fumadocs manages component registries across different UI packages, including `fumadocs/radix-ui`, `fumadocs/base-ui`, and `fumadocs/sanity`. Each registry defines an export structure containing its registry name, source directory (`dir`), a catalog of components with associated files and targets, and package dependencies.

```typescript
export const registry: Registry = {
  name: 'fumadocs/radix-ui',
  dir,
  components: [
    ...commonComponents,
    ...(await findSlotComponents(dir)),
    // ... layout and component definitions
  ],
  dependencies: {
    'fumadocs-core': null,
    'fumadocs-ui': null,
    react: null,
  },
};
```
Sources: [packages/radix-ui/registry/index.ts:9-345](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts#L9-L345)

Shared utilities and common components—such as URL handlers, class-name merging, and table-of-contents clerks—are reused across libraries through modular registry shares. Slot components are dynamically discovered using `tinyglobby` scanning over layout directories.

```typescript
export async function findSlotComponents(dir: string): Promise<Component[]> {
  const slots: Component[] = [];

  for (const file of await glob('layouts/**/slots/*', { cwd: dir })) {
    const relativePath = path.relative('layouts', file);
    const name = relativePath
      .split(path.sep)
      .filter((v) => v !== 'slots')
      .join('/')
      .slice(0, -path.extname(relativePath).length);

    slots.push({
      name: `slots/${name}`,
      unlisted: true,
      files: [
        {
          path: file,
          type: 'layout',
          target: path.join('<dir>', relativePath),
        },
      ],
    });
  }

  return slots;
}
```
Sources: [packages/shared/registry.ts:5-30](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/shared/registry.ts#L5-L30)

> [!NOTE]
> The order of files matters when writing import paths on the consumer's codebase, ensuring that common utilities and slot components precede layout and UI primitives in the registry array.

```typescript
// in shadcn cli, the order of files matters when writing import paths on consumer's codebase
export const registry: Registry = {
  name: 'fumadocs/radix-ui',
  dir,
  components: [
    ...commonComponents,
    ...(await findSlotComponents(dir)),
```
Sources: [packages/radix-ui/registry/index.ts:8-14](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts#L8-L14)

### Component Manifest Definitions

Component entries within a registry specify metadata, file types, source paths, and installation targets. Unlisted components, such as low-level UI primitives (`button`, `popover`, `scroll-area`, `collapsible`) and internal layout variations (`layouts/sidebar`, `layouts/shared`, `layouts/docs`, `layouts/flux`, `layouts/notebook`, `layouts/home`), are omitted from public component listings while remaining installable by the CLI.

| Component Name | File Types | Target/Preserve Settings | Description / Notes |
| :--- | :--- | :--- | :--- |
| `github-info` | `components` | Default target | A card to display GitHub repo info |
| `image-zoom` | `components`, `css` | Default target | Zoomable Image component with styles |
| `layouts/shared` | `layout` | `<dir>/shared/index.tsx`, `client.tsx` (preserveAs: `layouts/shared`) | Unlisted shared layout files |
| `layouts/docs` | `layout` | `<dir>/docs/index.tsx`, `client.tsx`, `page/index.tsx` | Unlisted documentation layout files |
| `ai/page-actions` | `components` | `<dir>/ai/page-actions.tsx` | Common page actions for AI search integration |

Sources: [packages/radix-ui/registry/index.ts:36-339](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts#L36-L339)

> [!WARNING]
> Registry entries that include layout files must specify metadata with `preserveAs` rules to prevent incorrect path rewriting during consumer project scaffolding.

```typescript
  meta: {
    preserveAs: 'layouts/shared',
  },
},
```
Sources: [packages/radix-ui/registry/index.ts:44-47](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/registry/index.ts#L44-L47)

## Related

- [Create App Generator](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tooling/create-app-generator)
- [EPUB Book Export](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tooling/epub-book-export)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
