FUNC - Tiny typed CLI framework

Use Cases

This page starts from the command a user wants to type, then maps it to the smallest func model that keeps the CLI predictable. The examples assume the executable name is saas.

Static Command

$ saas status

The user needs a quick health check for the SaaS service, such as whether the API, database, and background workers are available. status is a stable top-level verb, so model it as a normal @Command with one default @Handler. This keeps command discovery, help output, and future options attached to the right scope.

import { Command, Handler } from 'func'

@Command({
  name: 'status',
  description: 'Print service status',
})
export class StatusCommand {
  @Handler()
  run() {
    console.log('All systems operational')
  }
}

Command With a Path

$ saas domain add

The user needs to manage domains, and adding a domain is one fixed action under that domain workflow. domain is the command, and add is a static action under that command. Use @Handler({ path: ['add'] }) when the action is a fixed path inside an already selected command.

import { Command, Handler } from 'func'

@Command({
  name: 'domain',
  description: 'Manage custom domains',
})
export class DomainCommand {
  @Handler({ path: ['add'] })
  add() {
    console.log('Adding domain')
  }
}

Command With Flags and a Value

$ saas register --issue --dns --domain google.com

The user needs to register a domain and optionally create an issue or run DNS checks during the same operation. register is still a static command. Boolean switches become @Flag fields, and the domain becomes a required @Value. This lets func parse, assign, and validate the options before the handler runs.

import { Command, Flag, Handler, Required, Value } from 'func'

@Command({
  name: 'register',
  description: 'Register a domain and optional checks',
})
export class RegisterCommand {
  @Flag({ description: 'Create an issue after registration' })
  issue = false

  @Flag({ description: 'Run DNS checks' })
  dns = false

  @Required()
  @Value({ type: String })
  domain?: string

  @Handler()
  run() {
    console.log(this.domain, {
      issue: this.issue,
      dns: this.dns,
    })
  }
}

Fallback Search Input

$ saas alice ally ali --includeProfile --verified

The user needs to fuzzy-search users by several possible names or aliases, then shape the returned users by requesting profile details and limiting results to verified accounts. Here the first token is not a fixed command; it is part of the search input. Use @CommandMissing as a first-class fallback command, then keep the result filters as typed flags.

import { Args, CommandMissing, Flag, FuncArgs, Handler } from 'func'

@CommandMissing()
export class UserSearchFallback {
  @Flag({ description: 'Include profile details' })
  includeProfile = false

  @Flag({ description: 'Only return verified users' })
  verified = false

  @Handler()
  async search(@Args() args: FuncArgs) {
    const [username, ...aliases] = args.inputs
    console.log('Searching user:', {
      username,
      aliases,
      includeProfile: this.includeProfile,
      verified: this.verified,
    })
  }
}

Repeated Values With an Enum

$ saas scale --machine 2x-1024m --machine 1x-2048m

The user needs to resize the deployment to multiple machines in one command, and each machine must use a supported size. scale is a static command, but --machine can be repeated. Use @ArrayValue for repeated input and @Enum to reject machine types outside the allowed set.

import { ArrayValue, Command, Enum, Handler } from 'func'

const MACHINES = ['1x-1024m', '1x-2048m', '2x-1024m', '2x-2048m']

@Command({
  name: 'scale',
  description: 'Scale machines',
})
export class ScaleCommand {
  @Enum(MACHINES)
  @ArrayValue({ name: 'machine' })
  machines: string[] = []

  @Handler()
  run() {
    console.log('Scaling to:', this.machines)
  }
}