Skip to main content
doc0blade47/dubdocumentation home
Sign in

CLI Tool

Last updated Oct 12, 2026100% grounded
Relevant source files

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

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, packages/cli/package.json:4-13, packages/cli/src/utils/config.ts:5-32

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.

process.on("SIGINT", () => process.exit(0));
process.on("SIGTERM", () => process.exit(0));

Sources: packages/cli/src/index.ts:1-12, packages/cli/package.json:12-14

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.

  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

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

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.

  program
    .addCommand(login)
    .addCommand(config)
    .addCommand(domains)
    .addCommand(shorten)
    .addCommand(links);
 
  program.parse();

Sources: packages/cli/src/index.ts:26-33

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 NameCommandPurpose
devtsup --watchRuns the bundler in watch mode for development.
buildtsupBuilds distribution files for production release.
typechecktsc --noEmitPerforms TypeScript type verification without emitting outputs.
cleanrimraf distRemoves the dist build directory.
startnode dist/index.js Executes the compiled bundle directly.

Sources: packages/cli/package.json:8-15, packages/cli/package.json:35-41

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.

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

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.

      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

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

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.

  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

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().

    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

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.

      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

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

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.

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

PropertyTypeDescription
access_tokenstringThe active OAuth access token for authenticating API requests
refresh_tokenstring | nullThe token used to obtain a new access token upon expiration
expires_atnumber | nullUnix timestamp in milliseconds indicating when the access token expires
domainstring (optional)The active workspace custom or default domain (e.g., dub.sh)

Sources: packages/cli/src/types/index.ts:1-6

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.

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

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

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

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

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.

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

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

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.

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

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.

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

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.

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

Command OptionFlagDescriptionDefault
Search-s, --search [search]Search term to filter links byundefined
Limit-l, --limit [limit]Number of links to fetch10

Sources: packages/cli/src/commands/links.ts:10-24

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().

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

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.

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

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

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().

const allSlugs = [...domains.map((domain) => domain.slug), ...defaultDomains];
 
return Array.from(new Set(allSlugs));

Sources: packages/cli/src/api/domains.ts:30-32

Data SourceRetrieval MethodTarget TypeProcessing
Workspace Domainsdub.domains.list()Domain Object ArrayMapped to .slug property
Default Domainsfetch("https://api.dub.co/domains/default")string[]Parsed via parseApiResponse

Sources: packages/cli/src/api/domains.ts:13-28

Was this page helpful?