---
title: "CLI Tool"
description: "The Dub command-line interface (dub-cli) provides a lightweight terminal workflow for interacting with the Dub API, allowing developers to manage short links, authentication credentials, and worksp..."
last_updated: "2026-10-12T07:00:10.116617+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/administration-and-api/cli-tool"
---

<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/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/index.ts)
- [packages/cli/src/commands/login.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/commands/login.ts)
- [packages/cli/src/commands/links.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/commands/links.ts)
- [packages/cli/src/api/links.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/links.ts)
- [apps/web/app/api/links/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/links/route.ts)
- [apps/web/app/api/workspaces/idOrSlug/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/workspaces/%5BidOrSlug%5D/route.ts)
- [packages/cli/package.json](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/package.json)
- [apps/web/lib/integrations/slack/commands.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/slack/commands.ts)
- [packages/cli/src/utils/oauth.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/utils/oauth.ts)
- [apps/web/app/api/callback/bitly/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/callback/bitly/route.ts)
- [apps/web/app/api/workspaces/idOrSlug/import/short/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/workspaces/%5BidOrSlug%5D/import/short/route.ts)
- [apps/web/lib/middleware/link.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/link.ts)
- [packages/cli/src/types/index.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/types/index.ts)
- [apps/web/app/app.dub.co/dashboard/slug/links/page-client.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/links/page-client.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/links/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/links/page.tsx)
- [apps/web/middleware.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/middleware.ts)
- [apps/web/app/app.dub.co/onboarding/workspaces/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(onboarding)/workspaces/page.tsx)
- [packages/cli/src/api/callback.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/callback.ts)
- [apps/web/app/ee/admin.dub.co/dashboard/links/components/disable-restore-workspace.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/admin.dub.co/(dashboard)/links/components/disable-restore-workspace.tsx)
- [apps/web/app/app.dub.co/onboarding/onboarding/steps/success/page-client.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/success/page-client.tsx)
- [apps/web/app/ee/admin.dub.co/dashboard/links/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/admin.dub.co/(dashboard)/links/page.tsx)
- [packages/cli/src/commands/shorten.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/commands/shorten.ts)
- [packages/utils/src/constants/dub-domains.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/utils/src/constants/dub-domains.ts)
- [apps/web/lib/dub.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/dub.ts)
- [apps/web/app/ee/admin.dub.co/dashboard/domains/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/admin.dub.co/(dashboard)/domains/page.tsx)
- [packages/stripe-app/src/views/AppSettings.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/stripe-app/src/views/AppSettings.tsx)
- [packages/cli/src/utils/config.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/utils/config.ts)
- [packages/utils/src/constants/main.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/utils/src/constants/main.ts)
- [packages/cli/src/api/domains.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/domains.ts)
- [apps/web/app/app.dub.co/dashboard/slug/links/...link/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/links/%5B...link%5D/page.tsx)
</details>

## Overview

The Dub command-line interface (`dub-cli`) provides a lightweight terminal workflow for interacting with the Dub API, allowing developers to manage short links, authentication credentials, and workspace domains directly from their shell. Built using Node.js and the Commander framework, the tool bootstraps an executable command registry that coordinates local state persistence, OAuth2 authorization flows, and remote link generation. By integrating the core Dub SDK with automated configuration storage and interactive prompts, the CLI simplifies administrative tasks and URL shortening operations across developer environments.

Sources: [packages/cli/src/index.ts:8-34](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/index.ts#L8-L34), [packages/cli/package.json:4-13](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/package.json#L4-L13), [packages/cli/src/utils/config.ts:5-32](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/utils/config.ts#L5-L32)

## Entry Point and Command Registry

### CLI Executable Bootstrap and Signal Handlers

The CLI binary initializes via a Node.js environment bootstrap specified by the shebang `#!/usr/bin/env node` and maps to `./dist/index.js` under the `"bin"` field in `package.json`. Process lifecycle management is handled by explicit signal listeners for `SIGINT` and `SIGTERM`, both of which immediately terminate the process with a zero status code.

```typescript
process.on("SIGINT", () => process.exit(0));
process.on("SIGTERM", () => process.exit(0));
```
Sources: [packages/cli/src/index.ts:1-12](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/index.ts#L1-L12), [packages/cli/package.json:12-14](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/package.json#L12-L14)

The asynchronous `main()` function fetches runtime package metadata via `getPackageInfo()` to populate the root Commander program configuration, defining the binary name as `dub`, assigning its description, and attaching version handling flags.

```typescript
  const packageInfo = await getPackageInfo();

  const program = new Command()
    .name("dub")
    .description("A CLI for shortening links with the Dub API.")
    .version(
      packageInfo.version || "1.0.0",
      "-v, --version",
      "display the version number",
    );
```
Sources: [packages/cli/src/index.ts:14-24](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/index.ts#L14-L24)

> [!NOTE]
> If `getPackageInfo()` fails to resolve a valid version string, the root command falls back to `"1.0.0"` while parsing version flag inputs `-v` or `--version`.
> Sources: [packages/cli/src/index.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/index.ts#L15-L24)

### Command Registration and Package Configuration

Subcommands imported from local command modules are registered sequentially onto the root Commander instance before executing `program.parse()`. The registered subcommand suite consists of `login`, `config`, `domains`, `shorten`, and `links`.

```typescript
  program
    .addCommand(login)
    .addCommand(config)
    .addCommand(domains)
    .addCommand(shorten)
    .addCommand(links);

  program.parse();
```
Sources: [packages/cli/src/index.ts:26-33](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/index.ts#L26-L33)

The package configuration establishes `dub-cli` as an ESM module (`"type": "module"`) with global installation preference (`"preferGlobal": true`). TypeScript compilation and bundling are driven by `tsup`, utilizing `rimraf` for cleaning build artifacts and `tsc` for type checking.

| Script Name | Command | Purpose |
| :--- | :--- | :--- |
| `dev` | `tsup --watch` | Runs the bundler in watch mode for development. |
| `build` | `tsup` | Builds distribution files for production release. |
| `typecheck` | `tsc --noEmit` | Performs TypeScript type verification without emitting outputs. |
| `clean` | `rimraf dist` | Removes the `dist` build directory. |
| `start` | `node dist/index.js ` | Executes the compiled bundle directly. |

Sources: [packages/cli/package.json:8-15](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/package.json#L8-L15), [packages/cli/package.json:35-41](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/package.json#L35-L41)

## Authentication and OAuth Callback Flow

### OAuth2 Client Initiation and Authorization Request

The `login` command initializes the authentication workflow by generating a 64-character code verifier using `getNanoid(64)` and establishing a local redirect target at `http://localhost:4587/callback`. It utilizes an `OAuth2Client` instance configured with client ID `dub_app_39527dcc11b452f38bb54a3a1664fd044d7158dfea8abcde`, authorization endpoint `https://app.dub.co/oauth/authorize`, and token endpoint `https://api.dub.co/oauth/token`.

```typescript
export const oauthClient = new OAuth2Client({
  clientId: "dub_app_39527dcc11b452f38bb54a3a1664fd044d7158dfea8abcde",
  authorizationEndpoint: "https://app.dub.co/oauth/authorize",
  tokenEndpoint: "https://api.dub.co/oauth/token",
});
```
Sources: [packages/cli/src/utils/oauth.ts:3-8](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/utils/oauth.ts#L3-L8)

The command requests authorization by passing the `redirectUri`, `codeVerifier`, and required scopes to `oauthClient.authorizationCode.getAuthorizeUri()`. The requested permission scopes are `links.read`, `links.write`, and `domains.read`. Once the authorization URI is generated, an interactive spinner starts via `ora`, and the user's default browser opens via `open(authUrl)` to complete authentication on the Dub platform.

```typescript
      const codeVerifier = getNanoid(64);
      const redirectUri = "http://localhost:4587/callback";

      const authUrl = await oauthClient.authorizationCode.getAuthorizeUri({
        redirectUri,
        codeVerifier,
        scope: ["links.read", "links.write", "domains.read"],
      });

      const spinner = ora("Opening browser for authentication").start();

      await open(authUrl);

      spinner.text = "Waiting for authentication";

      oauthCallbackServer({
        oauthClient,
        redirectUri,
        codeVerifier,
        spinner,
      });
```
Sources: [packages/cli/src/commands/login.ts:14-34](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/commands/login.ts#L14-L34)

> [!NOTE]
> The local redirect URI explicitly binds to port `4587` on `localhost` (`http://localhost:4587/callback`), requiring that port to remain available during the login sequence.
> Sources: [packages/cli/src/commands/login.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/commands/login.ts#L15)

### Local Callback Server and Credential Exchange

The local HTTP server spawned by `oauthCallbackServer` listens on port `4587` to intercept the redirect callback from the authorization provider. Incoming requests are parsed via `url.parse()`. If a request targets any path other than `/callback` or uses a method other than `GET`, the server responds with a `404` status code and terminates the request handling.

```typescript
  const server = http.createServer(async (req, res) => {
    const reqUrl = url.parse(req.url || "", true);

    if (reqUrl.pathname !== "/callback" || req.method !== "GET") {
      res.writeHead(404);
      res.end("Not found");
      return;
    }
```
Sources: [packages/cli/src/api/callback.ts:23-30](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/callback.ts#L23-L30)

Upon receiving a valid `GET /callback` request containing an authorization `code` query parameter, the server transitions the CLI spinner state to `"Verifying"` and exchanges the authorization code for tokens by invoking `oauthClient.authorizationCode.getToken()`. 

```typescript
    const code = reqUrl.query.code as string;

    if (!code) {
      res.writeHead(400);
      res.end(
        "Authorization code not found. Please start the login process again.",
      );

      return;
    }

    try {
      spinner.text = "Verifying";

      const { accessToken, refreshToken, expiresAt } =
        await oauthClient.authorizationCode.getToken({
          code,
          redirectUri,
          codeVerifier,
        });
```
Sources: [packages/cli/src/api/callback.ts:32-51](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/callback.ts#L32-L51)

After acquiring the token bundle, the spinner updates to `"Configuring"`, and a configuration object is constructed containing the trimmed `access_token`, `refresh_token`, `expires_at`, and the default workspace domain `dub.sh`. This configuration is saved via `setConfig(configInfo)`. Upon successful persistence, the server outputs confirmation messages, responds to the browser with an HTML success message, closes the HTTP server, and exits the process with code `0`.

```typescript
      spinner.text = "Configuring";

      const configInfo: DubConfig = {
        access_token: accessToken.trim(),
        refresh_token: refreshToken,
        expires_at: expiresAt,
        domain: "dub.sh",
      };

      await setConfig(configInfo);
      spinner.succeed("Configuration completed");

      logger.info("");
      logger.info(chalk.green("Logged in successfully!"));
      logger.info("");

      res.writeHead(200, { "Content-Type": "text/html" });
      res.end("Authentication successful! You can close this window.");
    } catch (error) {
      res.writeHead(500, { "Content-Type": "text/html" });
      res.end("An error occurred during authentication. Please try again.");
    } finally {
      server.close();
      process.exit(0);
    }
```
Sources: [packages/cli/src/api/callback.ts:53-77](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/callback.ts#L53-L77)

> [!WARNING]
> The callback server enforces a strict inactivity timeout of 300,000 milliseconds (5 minutes) via `setTimeout`, after which the server automatically closes and terminates the process with exit code `0` if authentication has not completed.
> Sources: [packages/cli/src/api/callback.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/callback.ts#L80-L83)

## Configuration Persistence and State Management

### Schema Types and Configstore Backend

The CLI configuration layer relies on the `Configstore` package initialized with the identifier `"dub-cli"` to persist authentication state and user preferences on the local filesystem. The core data structure is defined by the `DubConfig` interface, which tracks credentials and contextual options. API error payloads are handled through the separate `APIError` interface.

```typescript
export interface DubConfig {
  access_token: string;
  refresh_token: string | null;
  expires_at: number | null;
  domain?: string;
}

export interface APIError {
  error: {
    code: string;
    message: string;
    doc_url: string;
  };
}
```
Sources: [packages/cli/src/types/index.ts:1-13](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/types/index.ts#L1-L13)

| Property | Type | Description |
| :--- | :--- | :--- |
| `access_token` | `string` | The active OAuth access token for authenticating API requests |
| `refresh_token` | `string \| null` | The token used to obtain a new access token upon expiration |
| `expires_at` | `number \| null` | Unix timestamp in milliseconds indicating when the access token expires |
| `domain` | `string` (optional) | The active workspace custom or default domain (e.g., `dub.sh`) |

Sources: [packages/cli/src/types/index.ts:1-6](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/types/index.ts#L1-L6)

### Token Retrieval and Automated Refresh Logic

Configuration retrieval is handled by `getConfig()`, which inspects the local `dub-cli` store. If the store is empty, an error is thrown instructing the user to authenticate. If `expires_at` is present and the current timestamp (`Date.now()`) has reached or exceeded that expiration threshold, `getConfig()` automatically invokes `oauthClient.refreshToken()` using the stored credentials before updating the configuration store and returning the refreshed state.

```typescript
export async function getConfig(): Promise<DubConfig> {
  const configStore = new Configstore("dub-cli");

  if (!configStore.size) {
    throw new Error(
      "Access token not found. Please run `dub login` to authenticate with Dub.",
    );
  }

  const config = configStore.all as DubConfig;

  if (config.expires_at && Date.now() >= config.expires_at) {
    const { accessToken, refreshToken, expiresAt } =
      await oauthClient.refreshToken({
        accessToken: config.access_token,
        refreshToken: config.refresh_token,
        expiresAt: config.expires_at,
      });

    return await setConfig({
      access_token: accessToken,
      refresh_token: refreshToken,
      expires_at: expiresAt,
    });
  }

  return await configStore.all;
}
```
Sources: [packages/cli/src/utils/config.ts:5-32](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/utils/config.ts#L5-L32)

> [!WARNING]
> If `configStore.path` is falsy after attempting to write updates via `setConfig()`, the utility immediately throws an error stating `"Failed to create or update config file"`.
> Sources: [packages/cli/src/utils/config.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/utils/config.ts#L47-L49)

Updates to configuration state are handled by `setConfig()`, which merges partial configuration objects into the existing stored state and writes them back to disk.

```typescript
export async function setConfig(
  newConfig: Partial<DubConfig>,
): Promise<DubConfig> {
  const configStore = new Configstore("dub-cli");
  const existingConfig: DubConfig = configStore.all;

  const updatedConfig: DubConfig = {
    ...existingConfig,
    ...newConfig,
  };

  configStore.set(updatedConfig);

  if (!configStore.path) {
    throw new Error("Failed to create or update config file");
  }

  return updatedConfig;
}
```
Sources: [packages/cli/src/utils/config.ts:34-52](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/utils/config.ts#L34-L52)

## Short Link Creation and Management

### Command Definition and Interactive Prompts

The `shorten` command is built with `commander` and accepts an optional destination URL argument and an optional short key argument that defaults to a nanoid generated by `getNanoid()`. If the user invokes the command without passing a destination URL, `prompts` is used to interactively request both the destination URL and the short key, initializing the key prompt with a fresh nanoid value.

```typescript
export const shorten = new Command()
  .name("shorten")
  .description("Create a short link")
  .argument("[url]", "Destination URL")
  .argument("[key]", "Short key", getNanoid())
  .action(async (url, key) => {
    try {
      await getConfig();

      let linkData = { url, key };

      if (!url) {
        linkData = await prompts(
          [
            {
              type: "text",
              name: "url",
              message: "Enter your Destination URL:",
            },
            {
              type: "text",
              name: "key",
              message: "Enter your Short link:",
              initial: getNanoid(),
            },
          ],
```
Sources: [packages/cli/src/commands/shorten.ts:11-36](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/commands/shorten.ts#L11-L36)

> [!WARNING]
> If the user cancels the interactive prompt via keyboard interrupt or abort, the `onCancel` handler prints an empty line followed by `"You canceled the prompt."` and immediately terminates the process with `process.exit(0)`.
> Sources: [packages/cli/src/commands/shorten.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/commands/shorten.ts#L38-L44)

### API Integration and Output Formatting

The API wrapper function `createLink` retrieves local configuration via `getConfig()`, instantiates the `Dub` SDK client with the stored `access_token`, and calls `dub.links.create()` with the workspace domain, destination URL, and short key. 

```typescript
export async function createLink({ url, key }: { url: string; key: string }) {
  const config = await getConfig();

  const dub = new Dub({
    token: config.access_token,
  });

  return await dub.links.create({
    domain: config.domain,
    url: url,
    key: key,
  });
```
Sources: [packages/cli/src/api/links.ts:4-15](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/links.ts#L4-L15)

During execution, `shorten` displays an `ora` spinner with the text `"Creating new short link"`. Upon success, the spinner reports `"New short link created!"` and logs the resulting short link URL styled with `chalk.green`. If link creation fails, the spinner reports `"Failed to create link"`, stops, and re-throws the error to be caught by `handleError`.

```typescript
const spinner = ora("Creating new short link").start();

try {
  const generatedShortLink = await createLink(linkData);

  spinner.succeed("New short link created!");

  logger.info("");
  logger.info(chalk.green(generatedShortLink.shortLink));
  logger.info("");
} catch (error) {
  spinner.fail("Failed to create link");
  spinner.stop();
  logger.info("");
  throw error;
}
```
Sources: [packages/cli/src/commands/shorten.ts:48-63](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/commands/shorten.ts#L48-L63)

Separately, the `links` command queries workspace links using `dub.links.list()`, accepting optional search and limit options where `pageSize` defaults to `10` when no limit is provided. The resulting links are mapped into tabular objects displaying their short link, destination URL, click count, and a formatted creation date string before being output via `console.table`.

```typescript
export const links = new Command()
  .command("links")
  .description("Search for links in your Dub workspace")
  .option("-s, --search [search]", "Search term to filter links by")
  .option("-l, --limit [limit]", "Number of links to fetch")
  .action(async ({ search, limit }) => {
    try {
      const config = await getConfig();

      const spinner = ora("Fetching links").start();

      const dub = new Dub({
        token: config.access_token,
      });

      const links = await dub.links.list({
        search,
        pageSize: limit ? parseInt(limit) : 10,
      });

      spinner.stop();

      const formattedLinks = links.result.map((link) => ({
        "Short Link": link.shortLink,
        "Destination URL": link.url,
        Clicks: link.clicks,
        "Created At": new Date(link.createdAt).toLocaleDateString("en-US", {
          month: "short",
          day: "numeric",
          hour: "numeric",
          minute: "numeric",
          hour12: true,
        }),
      }));

      console.table(formattedLinks);
```
Sources: [packages/cli/src/commands/links.ts:7-42](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/commands/links.ts#L7-L42)

| Command Option | Flag | Description | Default |
| :--- | :--- | :--- | :--- |
| Search | `-s, --search [search]` | Search term to filter links by | `undefined` |
| Limit | `-l, --limit [limit]` | Number of links to fetch | `10` |

Sources: [packages/cli/src/commands/links.ts:10-24](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/commands/links.ts#L10-L24)

## Workspace Domain Discovery

### Querying Workspace Domains

The CLI domain discovery utility `getDomains()` retrieves custom domains associated with the authenticated user's workspace by combining resources from the official SDK and the public API. It initializes the `Dub` SDK client using the stored `access_token` retrieved via `getConfig()`.

```typescript
export async function getDomains() {
  const config = await getConfig();

  const dub = new Dub({
    token: config.access_token,
  });
```
Sources: [packages/cli/src/api/domains.ts:6-11](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/domains.ts#L6-L11)

To compile the complete list of available domain slugs, `getDomains()` executes a concurrent request using `Promise.all` that calls `dub.domains.list()` and performs an HTTP `GET` request against the `https://api.dub.co/domains/default` endpoint, passing the bearer token and application/json headers.

```typescript
const [{ result: domainsResponse }, defaultDomainsResponse] =
  await Promise.all([
    dub.domains.list(),
    fetch("https://api.dub.co/domains/default", {
      method: "GET",
      headers: {
        Authorization: `Bearer ${config.access_token}`,
        "Content-Type": "application/json",
      },
    }),
  ]);
```
Sources: [packages/cli/src/api/domains.ts:13-23](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/domains.ts#L13-L23)

> [!NOTE]
> The default domains response is parsed into an array of strings using `parseApiResponse<string[]>()`, whereas the SDK domain list response yields domain objects whose `slug` properties are extracted.
> Sources: [packages/cli/src/api/domains.ts:25-28](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/domains.ts#L25-L28)

### Response Formatting and Deduplication

Once both asynchronous requests resolve, the domain slugs from the workspace and the default system domains are concatenated into a single array. The combined collection is then passed into a `Set` to eliminate duplicate entries and converted back into a standard array using `Array.from()`.

```typescript
const allSlugs = [...domains.map((domain) => domain.slug), ...defaultDomains];

return Array.from(new Set(allSlugs));
```
Sources: [packages/cli/src/api/domains.ts:30-32](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/domains.ts#L30-L32)

| Data Source | Retrieval Method | Target Type | Processing |
| :--- | :--- | :--- | :--- |
| Workspace Domains | `dub.domains.list()` | Domain Object Array | Mapped to `.slug` property |
| Default Domains | `fetch("https://api.dub.co/domains/default")` | `string[]` | Parsed via `parseApiResponse` |

Sources: [packages/cli/src/api/domains.ts:13-28](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/cli/src/api/domains.ts#L13-L28)

## Related

- [REST API](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/administration-and-api/rest-api)
- [Quick Start](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/getting-started/quick-start)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
