bin/cli/CONVENTIONS.md
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.
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
--long and -s shorts (one-letter shorts reserved for very common
flags: -h, -v, -o, -q, --no-open).--api-key sk-xxx (space). = accepted for parity but doc uses space.--api-key, --non-interactive, --max-tokens).--no-foo (negative) and --foo (positive). Default false unless
documented.--header X-A=1 --header X-B=2).--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.
| 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.
All API calls go through apiFetch(path, opts) (bin/cli/api.mjs), which:
OMNIROUTE_BASE_URL env or ~/.omniroute/config.json
(active profile).Authorization: Bearer ${OMNIROUTE_API_KEY} when available.x-omniroute-cli-token when applicable (see task 8.12).--timeout 30000, default 30s).err.stack (CLAUDE.md hard rule #12).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",
],
};
--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)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.--idempotency-key <uuid> for extra-safe mutations.| Status | Exit | Retry? |
|---|---|---|
| 200–299 | 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 |
t("module.key", vars).bin/cli/locales/{locale}.json (nested objects).
43 files ship out-of-the-box: en, pt-BR, and 41 additional locales.
11 locales are scaffold-only (empty {}); all keys fall back to en automatically.--lang flag → OMNIROUTE_LANG env → LC_ALL → LC_MESSAGES → LANG → en.config lang set <code> — saves OMNIROUTE_LANG to ~/.omniroute/.env.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.config/i18n.json — source of truth used by both CLI and
dashboard i18n pipelines.config/i18n.json with code, english, native, flag.node bin/cli/scripts/generate-locales.mjs — creates bin/cli/locales/{code}.json.{} for en-fallback scaffold).check-cli-i18n will verify all t() keys exist in en.json.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).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.combos → src/lib/db/combos.ts (getCombos, getComboByName, createCombo,
deleteComboByName, setActiveCombo, …)db.recovery → src/lib/db/recovery.ts (countEncryptedCredentials,
resetEncryptedColumns)3 when the
server is down, never silently fall back.src/lib/db/ modules.
The Semgrep rule at .semgrep/rules/cli-no-sqlite.yaml enforces this at commit time.Commands that mutate state (delete, reset, --force) must:
--yes)./api/compliance/audit-log when the server is up.--dry-run (preview without effect).sk-***-xxx via maskSecret() from
bin/cli/output.mjs.OMNIROUTE_*_API_KEY)--api-key-stdin)askSecret() (echo off — already implemented in io.mjs)--verbose / --debug output.tests/unit/cli-*.test.ts naming. Prefer node:test for CLI suites
(no extra deps).bin/cli/commands/, ≥75% for bin/cli/ overall
after Fase 8.docs/security/ERROR_SANITIZATION.md — the only acceptable error shapes.tests/unit/cli-tools-i18n.test.ts — current i18n infrastructure (pre-t()).