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

225 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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? |
| --------------- | ---- | ------------------------------ |
| 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_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.