Files
2026-07-13 13:39:12 +08:00

8.7 KiB
Raw Permalink Blame History

OmniRoute CLI — Internal Conventions

Status: normative. Source: _tasks/features-v3.8.0/cli/fase-0-preparacao/0.3-definir-convencoes.md. This file is the authoritative reference for every new or migrated CLI command. If reality diverges from this document, fix the code first; only edit this file after the discrepancy has been justified in a PR.

1. Subcommand style

Standard: git-style nested verbs.

omniroute keys add openai sk-xxx
omniroute combo switch fastest
omniroute memory search "react hooks"

Not allowed:

omniroute --add-key openai sk-xxx     # ❌ flag-as-verb
omniroute add-key openai sk-xxx       # ❌ hyphen at the top level

2. Flags

  • Only --long and -s shorts (one-letter shorts reserved for very common flags: -h, -v, -o, -q, --no-open).
  • Format: --api-key sk-xxx (space). = accepted for parity but doc uses space.
  • Naming: kebab-case (--api-key, --non-interactive, --max-tokens).
  • Booleans: --no-foo (negative) and --foo (positive). Default false unless documented.
  • Multi-value: repeat the flag (--header X-A=1 --header X-B=2).

3. Output (--output)

Value Use case
table default human-readable
json single JSON object, pretty-printed
jsonl streamed objects, one per line (logs, lists)
csv spreadsheet ingestion

Related flags:

  • --quiet / -q — suppress headers/spinners (pipe-friendly).
  • --no-color — force ANSI off (auto-detected if !stdout.isTTY).

Helper: emit(rows, opts) from bin/cli/output.mjs handles all four formats.

4. Exit codes

Code Meaning
0 success
1 generic error (uncaught, runtime)
2 invalid argument / misuse
3 server offline (when required)
4 auth / permission (401/403)
5 rate limit / quota (429)
124 timeout

Helper: exitWith(code, message?) from bin/cli/exit.mjs (added under output.mjs if needed) — always uses these constants. Never raw process.exit(N) in command code.

5. HTTP errors + retry/backoff

All API calls go through apiFetch(path, opts) (bin/cli/api.mjs), which:

  • Reads base URL from OMNIROUTE_BASE_URL env or ~/.omniroute/config.json (active profile).
  • Injects Authorization: Bearer ${OMNIROUTE_API_KEY} when available.
  • Injects x-omniroute-cli-token when applicable (see task 8.12).
  • Applies a per-attempt timeout (--timeout 30000, default 30s).
  • Maps status → exit code (401→4, 429→5, 5xx→1, etc.).
  • Never exposes err.stack (CLAUDE.md hard rule #12).
  • Applies exponential backoff with jitter on retryable statuses.

Retry defaults

export const RETRY_DEFAULTS = {
  maxAttempts: 3, // 1 initial + 2 retries
  baseMs: 500,
  maxMs: 8000, // jitter can slightly exceed
  jitter: true, // ±25%
  retryableStatuses: [408, 425, 429, 502, 503, 504],
  retryableErrorCodes: [
    "ECONNRESET",
    "ECONNREFUSED",
    "ETIMEDOUT",
    "ENOTFOUND",
    "EAI_AGAIN",
    "EPIPE",
  ],
};

Global flags wired

  • --retry (default on) / --no-retry
  • --retry-max <n> (default 3) — total attempts
  • --timeout <ms> (default 30000) — per attempt
  • --retry-on <csv> — extra retryable statuses (e.g. --retry-on 500)

Method semantics

  • Mutations (POST/PUT/DELETE) retry only on idempotent-ish statuses (502/503/504/408/network), never 409/422. This avoids duplicate side-effects.
  • GET retries all RETRY_DEFAULTS.retryableStatuses.
  • SSE / streaming does not auto-retry (operator decides).
  • Optional --idempotency-key <uuid> for extra-safe mutations.

Status → exit code map

Status Exit Retry?
200299 0 n/a
400 2 no
401 4 no
403 4 no
404 2 no
408 124 yes
409 1 no (mutations)
422 2 no
425 1 yes
429 5 yes (respects Retry-After)
500 1 configurable (default no)
502 / 503 / 504 1 yes
Network errors 1 yes
Timeout 124 yes

6. Internationalization

  • Every user-facing string goes through t("module.key", vars).
  • Catalogs live in bin/cli/locales/{locale}.json (nested objects). 42 files ship out-of-the-box: en, pt-BR, and 40 additional locales. 11 locales are scaffold-only (empty {}); all keys fall back to en automatically.
  • Detection order: --lang flag → OMNIROUTE_LANG env → LC_ALLLC_MESSAGESLANGen.
  • Locale persisted via config lang set <code> — saves OMNIROUTE_LANG to ~/.omniroute/.env.
  • Missing keys return the key itself (no crash).
  • PRs that add new strings must update en.json and pt-BR.json. Other locale files are best-effort; missing keys silently fall back to en.
  • normalize() in i18n.mjs validates locale codes via /^[a-zA-Z0-9-]+$/ to prevent path traversal — never pass raw filesystem paths.
  • Canonical locale list: config/i18n.json — source of truth used by both CLI and dashboard i18n pipelines.

Adding a new locale file

  1. Add entry to config/i18n.json with code, english, native, flag.
  2. Run node bin/cli/scripts/generate-locales.mjs — creates bin/cli/locales/{code}.json.
  3. Fill in translations (or leave as {} for en-fallback scaffold).
  4. The pre-commit hook check-cli-i18n will verify all t() keys exist in en.json.

7. Logs / output channels

  • stdout — useful output (parseable when --output json|jsonl|csv).
  • stderr — progress, warnings, errors, spinners.
  • --verbose / -V — extra detail on stderr.
  • --debug — stack traces, request bodies (dev-mode only; redacts secrets).

8. Server-first / DB-fallback

Single helper:

import { withRuntime } from "./runtime.mjs";

await withRuntime(async ({ kind, api, db }) => {
  if (kind === "http")
    return api("/api/combos", { retry: false, timeout: 5000, acceptNotOk: true });
  return db.combos.getCombos();
});
  • kind: "http" when server is up (preferred). api is apiFetch bound to the current profile/base-URL.
  • kind: "db" when server is offline. db exposes typed module exports:
    • db.combossrc/lib/db/combos.ts (getCombos, getComboByName, createCombo, deleteComboByName, setActiveCombo, …)
    • db.recoverysrc/lib/db/recovery.ts (countEncryptedCredentials, resetEncryptedColumns)
  • Mutations that require server must error with exit code 3 when the server is down, never silently fall back.
  • Never write raw SQL in commands — always go through src/lib/db/ modules. The Semgrep rule at .semgrep/rules/cli-no-sqlite.yaml enforces this at commit time.

9. Audit of destructive actions

Commands that mutate state (delete, reset, --force) must:

  • Ask for interactive confirmation (skipped with --yes).
  • POST to /api/compliance/audit-log when the server is up.
  • Support --dry-run (preview without effect).

10. Secrets

  • Never log secrets. Mask as sk-***-xxx via maskSecret() from bin/cli/output.mjs.
  • Never accept a secret via positional without warning. Prefer:
    • env (OMNIROUTE_*_API_KEY)
    • stdin (--api-key-stdin)
    • interactive askSecret() (echo off — already implemented in io.mjs)
  • Secrets must not appear in --verbose / --debug output.

11. Testing baseline

  • Every new command ships with at least one smoke test (happy path + one error path).
  • Use tests/unit/cli-*.test.ts naming. Prefer node:test for CLI suites (no extra deps).
  • Coverage target: ≥60% for bin/cli/commands/, ≥75% for bin/cli/ overall after Fase 8.

12. References

  • CLAUDE.md hard rules — especially #11 (publicCreds), #12 (error sanitization), #13 (shell injection).
  • docs/security/ERROR_SANITIZATION.md — the only acceptable error shapes.
  • tests/unit/cli-tools-i18n.test.ts — current i18n infrastructure (pre-t()).
  • Commander.js docs — Options & subcommand patterns.