Skill v1.0.0
currentTrusted Publisher100/100version: "1.0.0" name: rayfin-cli-ux description: >- Mandatory for Rayfin CLI UX work. Load before designing, speccing, implementing, or reviewing CLI commands, flags, output, or errors.
Rayfin CLI UX Guidelines
Standards for consistent, high-quality CLI experiences in @microsoft/rayfin-cli.
Full reference: references/cli-ux-guidelines.md — load on demand for depth, examples, and rationale.
Architecture Status
These rules define what a good CLI experience must deliver — they stay in force regardless of wiring. How output is wired is mid-migration to the workflow + adapter architecture (`docs/rfc/rayfin-tools-architecture.md`, tracked in `rayfin-tools-architecture-migration.md`).
- New or migrated commands use a
common/src/workflows/<name>/workflow returning a typedResult<T>, with a thin Layer-1 Commander wrapper rendering viaLogger/Progressadapters selected from--output <mode>. Do not importoutput-mode.ts, branch on raw--json/--verbose, or useCliHandledError/process.exit. - Legacy (un-migrated) commands use
output-mode.ts(modeLog/emitJson/resolveOutputMode). This machinery is scheduled for removal in Phase 6. - When this skill and the architecture RFC disagree, the RFC wins.
Constitutional Rules
Non-negotiable for every CLI command. Each rule has a stable ID (R1–R12) for cross-referencing.
| ID | Rule | One-liner | |
|---|---|---|---|
| R1 | Three output modes | Every command supports interactive, plain, and json. See reference §1. | |
| R2 | No console.log | All output goes through mode-aware utilities (or v2 Logger/Progress adapters). | |
| R3 | Recovery hints | Every ❌ error includes an indented next-step action. | |
| R4 | --force for destructive ops | Operations risking data loss fail by default; --force opts in. | |
| R5 | Respect non-interactive | No prompts when --yes, non-TTY stdin, or CI=true. | |
| R6 | Valid JSON output | --json → exactly one JSON object on stdout, no ANSI/emoji. --verbose + --json → reject with error. | |
| R7 | Standard flags | --output <mode> (canonical), --json (shorthand), -y/--yes, --verbose, --dry-run, --force, --env-file <path>. | |
| R8 | Clear progress text | Every step uses ProgressIndicator with action-oriented text. | |
| R9 | No credentials/PII | Truncate tokens, use boolean indicators, never log emails/OIDs. | |
| R10 | --dry-run explains | Lists planned ops without mutations; viable previews exit 0, validation failures exit nonzero. | |
| R11 | --help everywhere | .description() on every command/option/argument. See reference §5 for canonical descriptions. | |
| R12 | Semantic exit codes | 0 = success, 1 = error, 2 = cancelled. |
Decision Shortcuts
- "Command or flag?" → Changes what = command. Changes how = flag.
- "
devorup?" → Local Docker / no auth →dev. Authenticated cloud →up. - "Child service or connector?" → Owned/deployed with app →
rayfin <service> init. External →rayfin connector add. - "
initcreate this?" → Only minimal base app (auth, data, static). Otherwise explicit follow-up command. - "Implicit?" → Only required system actions in
dev/up. User-facing = explicit.
Common Patterns (keep — action-oriented)
Standard action handler (legacy path)
.action(async (cmdOptions) => {const mode = resolveOutputMode({ json: cmdOptions.json });const interactive = isInteractive({ yes: cmdOptions.yes });const verbose = createVerboseLogger(cmdOptions.verbose);try {// ...work...if (mode === 'json') {emitJson({ status: 'success', ...result });}} catch (err) {if (mode === 'json') {emitJsonError(mode, (err as Error).message);}modeError(mode, `❌ ${(err as Error).message}`);modeError(mode, ' <recovery hint>');throw new CliHandledError(err);}})
Error with recovery hint
modeError(mode, '❌ Project name not found in rayfin.yml configuration');modeError(mode, " Run 'rayfin init' to create a new project configuration");throw new CliHandledError(new Error('...'));
Progress step
const spinner = showProgress('Deploying configuration', '🔄', mode);try {await doWork();spinner.succeed('Configuration deployed');} catch (err) {spinner.fail('Configuration deployment failed');throw err;}
Implementing a New Command
First decide: v2 workflow or legacy path? New commands target the workflow + adapter architecture (see migration plan).The legacy steps below are for un-migrated commands only.
V2 command (preferred): orchestration in common/src/workflows/<name>/, Commander .action() is a thin wrapper that parses → builds deps → calls workflow → renders Result via adapters → maps exit code.
Legacy path (un-migrated commands):
- Create command file in
packages/tools/cli/src/commands/. - Import utilities from
../utils/output-mode.js. - Add
--json,-y/--yes,--verboseoptions. - Add
--dry-run/--forcewhen applicable. - Wrap progress in
ProgressIndicator. - Follow the standard action handler skeleton above.
- Register in parent
index.ts.
Reviewing a Command (checklist by rule ID)
- [ ] R1 — supports interactive, plain, json modes
- [ ] R2 — no bare
console.log - [ ] R3 — every
❌has a recovery hint - [ ] R6 —
--jsonemits valid JSON only; no ANSI/emoji - [ ] R4 — destructive ops gated on
--force - [ ] R5 — prompts respect
isInteractive() - [ ] R8 — progress uses
ProgressIndicatorwith action text - [ ] R10 —
--dry-runlists ops without mutations and uses a semantic exit code - [ ] R9 — no PII in verbose output
- [ ] R11 —
--helpdescriptions present, canonical wording for standard flags - [ ] R12 — exit codes are semantic
- [ ] Spec includes user intent, behavior contract, validation/failures, error wording, examples, cross-cutting impacts
Command Topology: dev vs up
See reference §12 for full rationale and patterns.
Key rules:
- Mirror nouns under both
devandupwhen capability exists in both worlds. dev= local Docker, offline, single env, lifecycle via flags (--stop,--down, etc.).up= Fabric cloud, requires auth (except inspection), multi-env via subcommands.devnever authenticates. Inspection commands (status,list) never block on auth.--forcemeans "allow destructive schema changes" — not "skip prompt" (that's--yes).
Pre-flight Checklist (OpenSpec CLI changes)
Every tasks.md for a CLI-touching OpenSpec change MUST begin with this block:
## Pre-flight-[ ] Loaded `.github/skills/rayfin-cli-ux/SKILL.md`. Cite one rule applied: <ID and restatement>.-[ ] Loaded `.github/skills/rayfin-cli-ux/references/cli-ux-guidelines.md`. Cite one section: <name>.-[ ] Every new/changed command, option, argument has a `.description()` (R11).-[ ] Every `❌` error has a recovery hint (R3).-[ ] All three output modes covered in scenarios (R1).-[ ] Destructive ops gated on `--force` (R4).-[ ] Non-interactive behavior preserved (R5).-[ ] `dev` vs `up` topology rules respected if adding lifecycle commands.
Final Scan (Guardrails)
Before submitting, verify none of the NEVER/ALWAYS rules are violated:
- NEVER: bare
console.log(R2), prompt when non-interactive (R5), partial/multiple JSON (R6), error without recovery (R3), destructive without--force(R4), PII in logs (R9), emoji outside interactive (R1), mutating API calls in dry-run (R10), auth indevcommands. - ALWAYS: action-oriented progress text (R8), standard flag names (R7), mirror
dev/upnouns, regenerate.env.localafter lifecycle commands.
Dry-run reads require an explicit documented contract. rayfin up authenticates and reads workspace metadata so its preview can identify the resolved target; it validates local static inputs first and never runs builds or deployment writes. See reference section 7 for this exception to the otherwise offline dry-run contract.