Xano CLI (@xano/cli) is a TypeScript command-line interface for Xano's Metadata API. It provides commands for managing profiles, workspaces, branches, functions, releases, tenants, tests, and static hosting - all from the terminal.
Binary name: xano (invoked as xano <command>)
- oclif 4 CLI framework with custom help class
- TypeScript 5 (strict mode, ES2022 target, Node16 module resolution)
- Node.js 18+ runtime (ESM-first architecture)
- inquirer 8 for interactive prompts
- js-yaml 4 for credentials file parsing
- Mocha 10 + Chai 4 for testing
- ESLint 9 with oclif + Prettier configs
- Small, focused modules (200-400 lines typical)
- High cohesion within modules
- Interface-based design for testability
- Meaningful variable and function names
// Always handle errors with context
try {
await doSomething();
} catch (err) {
throw new Error(`failed to do something: ${err.message}`);
}- TDD: Write tests first
- 80% minimum coverage
- Unit tests for business logic
- Integration tests for API endpoints
- No hardcoded credentials
- Environment variables for secrets
- Validate all user input
- Audit logging for sensitive operations
- NEVER run destructive commands (
push --force,reset --hard,checkout .) unless explicitly requested - NEVER skip hooks (
--no-verify) unless explicitly requested - NEVER force push to main/master
- Always create NEW commits rather than amending unless explicitly requested
npm run build # Clean dist/ and compile TypeScript
npm run dev # Run in dev mode (ts-node, no build needed)
npm run lint # Run ESLint
npm test # Run Mocha tests + lintRunning commands in dev:
./bin/dev.js profile list
./bin/dev.js workspace listsrc/
├── base-command.ts # Base class for all commands (profile flag)
├── help.ts # Custom oclif help class
├── index.ts # Entry point (re-exports oclif run)
├── commands/
│ ├── auth/ # Browser-based authentication
│ ├── profile/ # Profile management (9 commands)
│ │ ├── wizard.ts # Interactive profile creation
│ │ ├── create.ts # Manual profile creation
│ │ ├── list.ts # List profiles
│ │ ├── get-default.ts # Get default profile
│ │ ├── set-default.ts # Set default profile
│ │ ├── edit.ts # Edit profile
│ │ ├── delete.ts # Delete profile
│ │ ├── me.ts # Current user info
│ │ └── token.ts # Token management
│ ├── workspace/ # Workspace management (CRUD, push, pull)
│ ├── branch/ # Branch management (CRUD, set_live)
│ ├── function/ # Function management (4 commands)
│ │ ├── list.ts
│ │ ├── get.ts
│ │ ├── create.ts
│ │ └── edit.ts
│ ├── release/ # Release management (CRUD, import, export)
│ ├── tenant/ # Tenant management (CRUD, deploy, backups)
│ ├── platform/ # Platform management (list, get)
│ ├── unit_test/ # Unit test management (list, run, run_all)
│ ├── workflow_test/ # Workflow test management (list, get, delete, run, run_all)
│ └── static_host/ # Static hosting (4 commands)
│ ├── list.ts
│ └── build/
bin/
├── dev.js # Dev entry (ts-node with ESM loader)
└── run.js # Production entry (compiled dist/)
examples/
├── function/ # Function examples
└── static_host/ # Static host examples
test/
└── commands/ # Mocha test files
Use underscores, not dashes, for all identifiers: directory names, filenames, variable names, and generated output files. This applies to command directories (e.g., get_all/, not get-all/) and default output filenames (e.g., env_my-tenant.yaml, not env-my-tenant.yaml).
Every command extends BaseCommand:
import {Flags} from '@oclif/core'
import BaseCommand from '../../base-command.js'
export default class MyCommand extends BaseCommand {
static override description = 'Command description'
static override examples = [
'$ xano my_topic my_command --flag value',
]
static override flags = {
...BaseCommand.baseFlags,
myFlag: Flags.string({
char: 'f',
description: 'Flag description',
required: true,
}),
}
async run(): Promise<void> {
const {flags} = await this.parse(MyCommand)
// Implementation
}
}| Class | Location | Purpose |
|---|---|---|
BaseCommand |
src/base-command.ts |
All commands - provides -p/--profile flag, credential loading |
Commands MUST have at most one positional argument. Use flags for any additional named values.
oclif sorts static args alphabetically by key name, not by declaration order. This means if you define two args like tenant_name and env_name, oclif will present them as ENV_NAME TENANT_NAME (alphabetically), which confuses users. The fix is to keep only one arg (the primary resource identifier) and move everything else to flags.
// WRONG - multiple args will be alphabetically reordered
static override args = {
tenant_name: Args.string({required: true}),
env_name: Args.string({required: true}), // Will appear BEFORE tenant_name!
}
// RIGHT - one arg, rest are flags
static override args = {
tenant_name: Args.string({required: true}),
}
static override flags = {
...BaseCommand.baseFlags,
name: Flags.string({char: 'n', required: true}),
}- Profile flag (
-p): Inherited fromBaseCommand.baseFlags, selects credential profile - Output flag (
-o):jsonorsummaryformat - Workspace flag (
-w): Workspace ID override - Stdin flag: Accept input from stdin for piping
- Use
this.error('message')for user-facing errors (oclif standard) - Use
this.warn('message')for non-fatal warnings - Use
this.log('message')for standard output
Commands supporting --output flag use:
protected outputJson(data: unknown): void {
this.log(JSON.stringify(data, null, 2))
}Location: ~/.xano/credentials.yaml
profiles:
default:
account_origin: https://app.xano.com
instance_origin: https://instance.xano.com
access_token: <token>
workspace: <workspace_id>
branch: <branch_id>
production:
instance_origin: https://prod.xano.com
access_token: <token>
default: default-p/--profileflagXANO_PROFILEenvironment variable- Default profile from credentials file
From package.json:
{
"oclif": {
"bin": "xano",
"dirname": "xano",
"commands": "./dist/commands",
"helpClass": "./dist/help",
"topicSeparator": " ",
"plugins": ["@oclif/plugin-help", "@oclif/plugin-plugins"]
}
}- Topic separator: Space (e.g.,
xano profile create, notxano profile:create) - Custom help class at
src/help.tsdisplays env var info - Plugin system enabled for extensibility
- Framework: Mocha 10 + Chai 4 + @oclif/test 4
- Timeout: 60 seconds per test
- Config:
.mocharc.jsonwith ts-node ESM loader - Run:
npm test(runs tests then lint)
import {runCommand} from '@oclif/test'
import {expect} from 'chai'
describe('command-name', () => {
it('runs successfully', async () => {
const {stdout} = await runCommand('profile list')
expect(stdout).to.contain('expected output')
})
})- Create file in
src/commands/<topic>/<command>.ts - Extend
BaseCommand - Define static
description,examples,flags,args - Implement
async run()method - Inherit
...BaseCommand.baseFlagsin flags - Run
npm run buildto compile
- ESLint 9 flat config (
eslint.config.mjs) with oclif + Prettier presets - Prettier uses
@oclif/prettier-config - Run
npm run lintto check