| # 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 | |
| ```js | |
| 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? | | |
| | --------------- | ---- | ------------------------------ | | |
| | 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** | | |
| ## 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_ALL` β `LC_MESSAGES` β `LANG` β `en`. | |
| - 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: | |
| ```js | |
| 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) | |
| - 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. | |