From e733b3ccb5c32e10fa0d3dff94607ac0d790f7ed Mon Sep 17 00:00:00 2001 From: Alexey Nikandrov Date: Mon, 10 Aug 2026 07:47:40 +0300 Subject: [PATCH 1/5] Add RFC-0009: external renderer for view command via RFC_VIEWER --- docs/rfcs/.index.json | 28 +++++-- docs/rfcs/0009.md | 172 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 docs/rfcs/0009.md diff --git a/docs/rfcs/.index.json b/docs/rfcs/.index.json index 6045701..97a3c96 100644 --- a/docs/rfcs/.index.json +++ b/docs/rfcs/.index.json @@ -7,7 +7,7 @@ "dependencies": [], "superseded_by": null, "links": [], - "mtime": "1776237919", + "mtime": "1786335262", "content_hash": "30eebe7082e44818bfa175f3a4ca2e2ff42b06854bf0e593e2e457fc62586b8a" }, { @@ -31,7 +31,7 @@ "tests/integration_test.rs", "Makefile" ], - "mtime": "1776237919", + "mtime": "1786335262", "content_hash": "5bf21ef585c8bdb3527d4631ec4d2a939df13b5687676f65df5bea7c0f6c69cb" }, { @@ -56,7 +56,7 @@ "Cargo.toml", "tests/integration_test.rs" ], - "mtime": "1776237919", + "mtime": "1786335262", "content_hash": "043bafbe3a26c13acd0176d2e3c53491c0716930cc564f503ac1224d9f72ed9e" }, { @@ -79,7 +79,7 @@ "src/rfclib/index.rs", "tests/integration_test.rs" ], - "mtime": "1776237919", + "mtime": "1786335262", "content_hash": "18f707229fe93107f0d6a24b92a24fe1071cb7934b7ab4d37453a6c0e5754d30" }, { @@ -101,7 +101,7 @@ "src/rfclib/rfc.rs", "tests/integration_test.rs" ], - "mtime": "1776237919", + "mtime": "1786335262", "content_hash": "5c7bc496cbc74b62cd8289c6759685d67c72d1cc160bb58e5a4bd29d89838957" }, { @@ -120,7 +120,7 @@ "src/commands/mod.rs", "tests/integration_test.rs" ], - "mtime": "1776237919", + "mtime": "1786335262", "content_hash": "f0cf2461fdd5ba254ad6662bdf1a8cf3122309cb2eac20b2cf87c242e4994870" }, { @@ -134,7 +134,7 @@ ], "superseded_by": null, "links": [], - "mtime": "1776972649", + "mtime": "1786335262", "content_hash": null }, { @@ -151,8 +151,20 @@ "src/commands/doctor.rs", "tests/integration_test.rs" ], - "mtime": "1776974269", + "mtime": "1786335262", "content_hash": "f12b678f491d089550a31c233dc117df04d35e0de6cfd9f1b25aa247adbc330d" + }, + { + "number": "0009", + "title": "RFC-0009: просмотр RFC через внешний рендерер в команде view", + "status": "draft", + "dependencies": [ + "RFC-0003" + ], + "superseded_by": null, + "links": [], + "mtime": "1786337251", + "content_hash": null } ] } \ No newline at end of file diff --git a/docs/rfcs/0009.md b/docs/rfcs/0009.md new file mode 100644 index 0000000..76bbe2e --- /dev/null +++ b/docs/rfcs/0009.md @@ -0,0 +1,172 @@ +--- +title: "RFC-0009: просмотр RFC через внешний рендерер в команде view" +status: draft +dependencies: [RFC-0003] +superseded_by: null +links: [] +--- + +## Problem + +Команда `rfc-cli view N` выводит «сырой» Markdown в stdout. Для комфортного чтения в терминале приходится вручную прогонять вывод через внешний рендерер, например: + +```sh +rfc-cli view 0001 | mdt +``` + +где `mdt` — терминальный рендерер Markdown. + +Это неудобно: + +1. Нужно каждый раз помнить и дописывать пайп `| mdt`. +2. Нет единого способа задать предпочитаемый рендерер один раз (по аналогии с `$EDITOR` для команды `edit`). +3. Поведение `view` и `edit` асимметрично: `edit` уже умеет запускать внешнюю программу из переменной окружения, а `view` — нет. + +Команда `edit` (RFC-0003) уже реализует нужный паттерн: она берёт программу из `$EDITOR` и запускает её через `std::process::Command`. Логично дать `view` аналогичную возможность. + +## Goal + +Дать команде `view` возможность автоматически отображать содержимое RFC через внешний рендерер Markdown, заданный переменной окружения, по аналогии с тем, как `edit` использует `$EDITOR`. + +### Requirements + +1. Команда `view` должна читать имя внешнего рендерера из переменной окружения `RFC_VIEWER`. +2. Если переменная `RFC_VIEWER` **не задана** (или пуста) — команда должна вести себя как раньше: печатать содержимое RFC в stdout. Это гарантирует обратную совместимость и работоспособность пайпов вида `rfc-cli view N | mdt`. +3. Если переменная `RFC_VIEWER` задана — команда должна запустить указанную программу и передать ей полное содержимое RFC-файла на стандартный вход (stdin). +4. Значение `RFC_VIEWER` может содержать имя программы с аргументами (например, `RFC_VIEWER="mdt"` или `RFC_VIEWER="glow -"`). Строка разбивается по пробелам: первое слово — исполняемый файл, остальные — аргументы. +5. Существующее поведение по остальным пунктам сохраняется: + - номер можно указывать с ведущими нулями или без (`view 1` == `view 0001`); + - если RFC не найдено — ошибка `Error: RFC-NNNN not found.` +6. Код выхода: + - `0` — при успешном выводе/рендере; + - `1` — при ошибке (RFC не найдено, не удалось запустить рендерер, рендерер завершился с ошибкой). +7. Если рендерер не удалось запустить (например, программа не найдена) — команда должна выводить понятную ошибку: `Failed to launch viewer '': `. +8. Все сообщения программы — на английском. + +## Design + +### CLI + +Сигнатура подкоманды `view` в `src/cli.rs` **не меняется** — новый флаг не вводится, поведение управляется только переменной окружения: + +```rust +/// View RFC contents +View { + /// RFC number + number: String, +}, +``` + +### Команда + +Файл: `src/commands/view.rs`. Логика расширяется: + +```rust +use std::env; +use std::fs; +use std::io::Write; +use std::path::Path; +use std::process::{Command, Stdio}; + +use crate::rfclib::rfc; + +pub fn execute(project_root: &Path, number: &str) -> Result<(), String> { + let normalized = rfc::normalize_number(number)?; + let path = rfc::rfc_path(project_root, number)?; + + if !path.exists() { + return Err(format!("RFC-{} not found.", normalized)); + } + + let content = fs::read_to_string(&path) + .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + + // RFC_VIEWER задан и непуст — рендерим через внешнюю программу + match env::var("RFC_VIEWER") { + Ok(viewer) if !viewer.trim().is_empty() => { + render_with_viewer(&viewer, &content) + } + _ => { + print!("{}", content); + Ok(()) + } + } +} +``` + +Функция запуска рендерера: + +```rust +fn render_with_viewer(viewer: &str, content: &str) -> Result<(), String> { + let mut parts = viewer.split_whitespace(); + let program = parts.next().unwrap(); // непустая строка гарантирована вызывающим кодом + let args: Vec<&str> = parts.collect(); + + let mut child = Command::new(program) + .args(&args) + .stdin(Stdio::piped()) + .spawn() + .map_err(|e| format!("Failed to launch viewer '{}': {}", viewer, e))?; + + if let Some(mut stdin) = child.stdin.take() { + stdin + .write_all(content.as_bytes()) + .map_err(|e| format!("Failed to write to viewer '{}': {}", viewer, e))?; + } + + let status = child + .wait() + .map_err(|e| format!("Failed to wait for viewer '{}': {}", viewer, e))?; + + if !status.success() { + return Err(format!("Viewer '{}' exited with error", viewer)); + } + + Ok(()) +} +``` + +### Замечания + +- `view` не трогает индекс и не изменяет файлы — это read-only команда, поведение относительно индекса не меняется. +- Разбор `RFC_VIEWER` по пробелам — простое и предсказуемое решение; сложный парсинг с кавычками не требуется для типовых значений (`mdt`, `glow -`, `bat -l md`). +- Симметрично `edit`: там программа берётся из `$EDITOR` и получает путь к файлу аргументом; здесь программа берётся из `RFC_VIEWER` и получает содержимое через stdin (типичный контракт Markdown-рендереров вроде `mdt`). + +## Alternatives + +1. **Использовать стандартную `$PAGER`.** + Недостаток: `$PAGER` обычно указывает на `less`/`more` — постраничные просмотрщики, а не Markdown-рендереры. Смешивание семантики привело бы к неожиданному поведению у пользователей с настроенным `$PAGER`. + +2. **Добавить флаг `--pager ` / `--render`.** + Недостаток: избыточно для основного сценария (пользователь хочет задать рендерер один раз). Env-переменная повторяет уже привычный паттерн `$EDITOR` из `edit`. При необходимости флаг можно добавить отдельным RFC позже. + +3. **Передавать содержимое рендереру аргументом-файлом, а не через stdin.** + Недостаток: не все рендереры принимают путь; передача через stdin — более универсальный контракт и совпадает с тем, как пользователь уже вызывает `rfc-cli view N | mdt`. + +4. **Оставить всё как есть (`view N | mdt`).** + Недостаток: не решает проблему — сохраняется ручной пайп и асимметрия с `edit`. + +## Voting + +## Migration + +Миграция не требуется. Изменение обратно совместимо: при отсутствии `RFC_VIEWER` поведение `view` не меняется. + +Шаги реализации: + +1. Расширить `src/commands/view.rs`: чтение `RFC_VIEWER`, запуск рендерера через stdin, fallback на stdout. +2. Добавить тесты: + - `view` печатает содержимое в stdout, когда `RFC_VIEWER` не задана (регресс существующего поведения); + - `view` запускает рендерер и передаёт ему содержимое, когда `RFC_VIEWER` задана (например, `RFC_VIEWER="cat"`); + - `view` возвращает ошибку, если рендерер не удалось запустить (несуществующая программа); + - `view` возвращает ошибку `Error: RFC-NNNN not found.` для несуществующего RFC. +3. Обновить документацию: секции «Environment Variables» в `README.md` и `README_ru.md` (добавить `RFC_VIEWER`). +4. Привязать файлы к RFC: `rfc-cli link 0009 src/commands/view.rs tests/integration_test.rs`. + +## Test Plan + +- Без `RFC_VIEWER`: `rfc-cli view N` печатает сырой Markdown в stdout (обратная совместимость, пайпы работают). +- С `RFC_VIEWER="cat"`: содержимое проходит через рендерер и выводится. +- С `RFC_VIEWER` = несуществующая команда: ошибка запуска, код выхода `1`. +- Несуществующий RFC: ошибка `Error: RFC-NNNN not found.`, код выхода `1`. +- Значение с аргументами (`RFC_VIEWER="glow -"`): корректный разбор программы и аргументов. From 0d6db8e5290d10fafa157de3e45f90a0d411bb2b Mon Sep 17 00:00:00 2001 From: Alexey Nikandrov Date: Mon, 10 Aug 2026 07:52:33 +0300 Subject: [PATCH 2/5] RFC-0009: add --raw flag to force raw output ignoring RFC_VIEWER --- docs/rfcs/0009.md | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/docs/rfcs/0009.md b/docs/rfcs/0009.md index 76bbe2e..10aefe6 100644 --- a/docs/rfcs/0009.md +++ b/docs/rfcs/0009.md @@ -26,7 +26,7 @@ rfc-cli view 0001 | mdt ## Goal -Дать команде `view` возможность автоматически отображать содержимое RFC через внешний рендерер Markdown, заданный переменной окружения, по аналогии с тем, как `edit` использует `$EDITOR`. +Дать команде `view` возможность автоматически отображать содержимое RFC через внешний рендерер Markdown, заданный переменной окружения, по аналогии с тем, как `edit` использует `$EDITOR`. При этом сохранить простой способ получить сырой Markdown через флаг `--raw`. ### Requirements @@ -34,29 +34,35 @@ rfc-cli view 0001 | mdt 2. Если переменная `RFC_VIEWER` **не задана** (или пуста) — команда должна вести себя как раньше: печатать содержимое RFC в stdout. Это гарантирует обратную совместимость и работоспособность пайпов вида `rfc-cli view N | mdt`. 3. Если переменная `RFC_VIEWER` задана — команда должна запустить указанную программу и передать ей полное содержимое RFC-файла на стандартный вход (stdin). 4. Значение `RFC_VIEWER` может содержать имя программы с аргументами (например, `RFC_VIEWER="mdt"` или `RFC_VIEWER="glow -"`). Строка разбивается по пробелам: первое слово — исполняемый файл, остальные — аргументы. -5. Существующее поведение по остальным пунктам сохраняется: +5. Команда должна поддерживать флаг `--raw`. Если флаг указан — команда печатает сырой Markdown в stdout **вне зависимости** от значения `RFC_VIEWER` (то есть `RFC_VIEWER` игнорируется, рендерер не запускается). Это нужно для пайпов и отладки, когда рендерер настроен глобально, но в конкретном вызове требуется исходный текст. +6. Существующее поведение по остальным пунктам сохраняется: - номер можно указывать с ведущими нулями или без (`view 1` == `view 0001`); - если RFC не найдено — ошибка `Error: RFC-NNNN not found.` -6. Код выхода: +7. Код выхода: - `0` — при успешном выводе/рендере; - `1` — при ошибке (RFC не найдено, не удалось запустить рендерер, рендерер завершился с ошибкой). -7. Если рендерер не удалось запустить (например, программа не найдена) — команда должна выводить понятную ошибку: `Failed to launch viewer '': `. -8. Все сообщения программы — на английском. +8. Если рендерер не удалось запустить (например, программа не найдена) — команда должна выводить понятную ошибку: `Failed to launch viewer '': `. +9. Все сообщения программы — на английском. ## Design ### CLI -Сигнатура подкоманды `view` в `src/cli.rs` **не меняется** — новый флаг не вводится, поведение управляется только переменной окружения: +Подкоманда `view` в `src/cli.rs` получает новый булев флаг `--raw`: ```rust /// View RFC contents View { /// RFC number number: String, + /// Print raw Markdown to stdout, ignoring RFC_VIEWER + #[arg(long)] + raw: bool, }, ``` +Диспетчер в `src/main.rs` пробрасывает флаг: `commands::view::execute(&project_root, &number, raw)`. + ### Команда Файл: `src/commands/view.rs`. Логика расширяется: @@ -70,7 +76,7 @@ use std::process::{Command, Stdio}; use crate::rfclib::rfc; -pub fn execute(project_root: &Path, number: &str) -> Result<(), String> { +pub fn execute(project_root: &Path, number: &str, raw: bool) -> Result<(), String> { let normalized = rfc::normalize_number(number)?; let path = rfc::rfc_path(project_root, number)?; @@ -81,6 +87,12 @@ pub fn execute(project_root: &Path, number: &str) -> Result<(), String> { let content = fs::read_to_string(&path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + // --raw форсирует сырой вывод и игнорирует RFC_VIEWER + if raw { + print!("{}", content); + return Ok(()); + } + // RFC_VIEWER задан и непуст — рендерим через внешнюю программу match env::var("RFC_VIEWER") { Ok(viewer) if !viewer.trim().is_empty() => { @@ -137,8 +149,8 @@ fn render_with_viewer(viewer: &str, content: &str) -> Result<(), String> { 1. **Использовать стандартную `$PAGER`.** Недостаток: `$PAGER` обычно указывает на `less`/`more` — постраничные просмотрщики, а не Markdown-рендереры. Смешивание семантики привело бы к неожиданному поведению у пользователей с настроенным `$PAGER`. -2. **Добавить флаг `--pager ` / `--render`.** - Недостаток: избыточно для основного сценария (пользователь хочет задать рендерер один раз). Env-переменная повторяет уже привычный паттерн `$EDITOR` из `edit`. При необходимости флаг можно добавить отдельным RFC позже. +2. **Управлять рендерингом только флагом `--pager ` / `--render`, без env-переменной.** + Недостаток: избыточно для основного сценария (пользователь хочет задать рендерер один раз). Env-переменная повторяет уже привычный паттерн `$EDITOR` из `edit`. Флаг `--raw` при этом остаётся полезным как разовое исключение — принудительный сырой вывод в обход настроенного `RFC_VIEWER`. 3. **Передавать содержимое рендереру аргументом-файлом, а не через stdin.** Недостаток: не все рендереры принимают путь; передача через stdin — более универсальный контракт и совпадает с тем, как пользователь уже вызывает `rfc-cli view N | mdt`. @@ -154,19 +166,22 @@ fn render_with_viewer(viewer: &str, content: &str) -> Result<(), String> { Шаги реализации: -1. Расширить `src/commands/view.rs`: чтение `RFC_VIEWER`, запуск рендерера через stdin, fallback на stdout. -2. Добавить тесты: +1. Расширить `src/commands/view.rs`: чтение `RFC_VIEWER`, флаг `--raw`, запуск рендерера через stdin, fallback на stdout. +2. Добавить флаг `--raw` в `src/cli.rs` и пробросить его через `src/main.rs`. +3. Добавить тесты: - `view` печатает содержимое в stdout, когда `RFC_VIEWER` не задана (регресс существующего поведения); - `view` запускает рендерер и передаёт ему содержимое, когда `RFC_VIEWER` задана (например, `RFC_VIEWER="cat"`); + - `view --raw` печатает сырой Markdown в stdout, даже когда `RFC_VIEWER` задана; - `view` возвращает ошибку, если рендерер не удалось запустить (несуществующая программа); - `view` возвращает ошибку `Error: RFC-NNNN not found.` для несуществующего RFC. -3. Обновить документацию: секции «Environment Variables» в `README.md` и `README_ru.md` (добавить `RFC_VIEWER`). -4. Привязать файлы к RFC: `rfc-cli link 0009 src/commands/view.rs tests/integration_test.rs`. +4. Обновить документацию: секции «Environment Variables» в `README.md` и `README_ru.md` (добавить `RFC_VIEWER`) и описание флага `--raw` у команды `view`. +5. Привязать файлы к RFC: `rfc-cli link 0009 src/commands/view.rs src/cli.rs src/main.rs tests/integration_test.rs`. ## Test Plan - Без `RFC_VIEWER`: `rfc-cli view N` печатает сырой Markdown в stdout (обратная совместимость, пайпы работают). - С `RFC_VIEWER="cat"`: содержимое проходит через рендерер и выводится. +- С `RFC_VIEWER="cat"` и флагом `--raw`: рендерер игнорируется, выводится сырой Markdown. - С `RFC_VIEWER` = несуществующая команда: ошибка запуска, код выхода `1`. - Несуществующий RFC: ошибка `Error: RFC-NNNN not found.`, код выхода `1`. - Значение с аргументами (`RFC_VIEWER="glow -"`): корректный разбор программы и аргументов. From 278e7b18028819f02fb7c6debddef91a2d32ba6e Mon Sep 17 00:00:00 2001 From: Alexey Nikandrov Date: Mon, 10 Aug 2026 07:55:56 +0300 Subject: [PATCH 3/5] Add local env configs and refresh RFC index --- .github/copilot-instructions.md | 27 +++++++++++++++++++++++++++ .github/hooks/rtk-rewrite.json | 22 ++++++++++++++++++++++ .mcp.json | 10 ++++++++++ .mem-project | 1 + AGENTS.md | 11 +++++++++++ docs/rfcs/.index.json | 2 +- 6 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/hooks/rtk-rewrite.json create mode 100644 .mcp.json create mode 100644 .mem-project create mode 100644 AGENTS.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..9609411 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,27 @@ + +# RTK — Token-Optimized CLI + +**rtk** is a CLI proxy that filters and compresses command outputs, saving 60-90% tokens. + +## Rule + +Always prefix shell commands with `rtk`: + +```bash +# Instead of: Use: +git status rtk git status +git log -10 rtk git log -10 +cargo test rtk cargo test +docker ps rtk docker ps +kubectl get pods rtk kubectl pods +``` + +## Meta commands (use directly) + +```bash +rtk gain # Token savings dashboard +rtk gain --history # Per-command savings history +rtk discover # Find missed rtk opportunities +rtk proxy # Run raw (no filtering) but track usage +``` + diff --git a/.github/hooks/rtk-rewrite.json b/.github/hooks/rtk-rewrite.json new file mode 100644 index 0000000..e854c76 --- /dev/null +++ b/.github/hooks/rtk-rewrite.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "hooks": { + "PreToolUse": [ + { + "type": "command", + "command": "rtk hook copilot", + "cwd": ".", + "timeout": 5 + } + ], + "preToolUse": [ + { + "type": "command", + "bash": "rtk hook copilot", + "powershell": "rtk hook copilot", + "cwd": ".", + "timeoutSec": 5 + } + ] + } +} diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..61f1b67 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,10 @@ +{ + "mcpServers": { + "mem-cli": { + "args": [ + "mcp" + ], + "command": "mem-cli" + } + } +} diff --git a/.mem-project b/.mem-project new file mode 100644 index 0000000..a5c9b92 --- /dev/null +++ b/.mem-project @@ -0,0 +1 @@ +rfc-cli-b3b906143933a80a diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a87e322 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,11 @@ + +## mem-cli context storage + +Project context is stored locally per developer, outside the repository: +`${XDG_DATA_HOME:-~/.local/share}/mem//project_context.db`. +The project slug (`rfc-cli-b3b906143933a80a`) is fixed in the `.mem-project` file. +The path can be overridden via the `MEMORY_DB_DIR` variable. + +Use the `mcp mem-cli` server for context memory; if `mcp mem-cli` is not +connected, fall back to the `mem-cli` shell command. + diff --git a/docs/rfcs/.index.json b/docs/rfcs/.index.json index 97a3c96..1378511 100644 --- a/docs/rfcs/.index.json +++ b/docs/rfcs/.index.json @@ -163,7 +163,7 @@ ], "superseded_by": null, "links": [], - "mtime": "1786337251", + "mtime": "1786337548", "content_hash": null } ] From fa7531de0c06633f22234eaee8c267f1a2c19178 Mon Sep 17 00:00:00 2001 From: Alexey Nikandrov Date: Mon, 10 Aug 2026 08:09:13 +0300 Subject: [PATCH 4/5] Implement RFC-0009: RFC_VIEWER support and --raw flag for view command - view now pipes content to $RFC_VIEWER when set - add --raw flag to force raw Markdown output - update README/README_ru env vars and view docs - add integration tests for viewer, --raw, and launch failure - link source files to RFC-0009 --- README.md | 10 +++++ README_ru.md | 10 +++++ docs/rfcs/.index.json | 13 +++++-- docs/rfcs/0009.md | 8 +++- src/cli.rs | 3 ++ src/commands/view.rs | 50 +++++++++++++++++++++++-- src/main.rs | 2 +- tests/integration_test.rs | 79 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 165 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2a6206b..610025f 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ rfc-cli doctor |----------|-------------|---------| | `RFC_HOME` | Project root directory | Current directory | | `EDITOR` | Editor for the `edit` command | — (required for `edit`) | +| `RFC_VIEWER` | External Markdown renderer for the `view` command (receives content on stdin) | — (raw output to stdout) | ## Commands @@ -107,6 +108,15 @@ rfc-cli view 1 # equivalent to: rfc-cli view 0001 ``` +If the `RFC_VIEWER` environment variable is set, the content is piped to that program (e.g. a terminal Markdown renderer) instead of being printed raw: + +```sh +export RFC_VIEWER="mdt" +rfc-cli view 1 # rendered through mdt + +rfc-cli view 1 --raw # force raw Markdown, ignoring RFC_VIEWER +``` + ### `status ` — current status Displays the RFC status from the index (fast, without reading the file). diff --git a/README_ru.md b/README_ru.md index 4db11eb..3746d78 100644 --- a/README_ru.md +++ b/README_ru.md @@ -50,6 +50,7 @@ rfc-cli doctor |------------|----------|--------------| | `RFC_HOME` | Корневой каталог проекта | Текущая директория | | `EDITOR` | Редактор для команды `edit` | — (обязателен для `edit`) | +| `RFC_VIEWER` | Внешний рендерер Markdown для команды `view` (получает содержимое через stdin) | — (сырой вывод в stdout) | ## Команды @@ -100,6 +101,15 @@ rfc-cli view 1 # эквивалентно: rfc-cli view 0001 ``` +Если задана переменная окружения `RFC_VIEWER`, содержимое передаётся этой программе (например, терминальному рендереру Markdown) вместо сырого вывода: + +```sh +export RFC_VIEWER="mdt" +rfc-cli view 1 # рендерится через mdt + +rfc-cli view 1 --raw # принудительно сырой Markdown, игнорируя RFC_VIEWER +``` + ### `status ` — текущий статус Выводит статус RFC из индекса (быстро, без чтения файла). diff --git a/docs/rfcs/.index.json b/docs/rfcs/.index.json index 1378511..9e82177 100644 --- a/docs/rfcs/.index.json +++ b/docs/rfcs/.index.json @@ -157,14 +157,19 @@ { "number": "0009", "title": "RFC-0009: просмотр RFC через внешний рендерер в команде view", - "status": "draft", + "status": "accepted", "dependencies": [ "RFC-0003" ], "superseded_by": null, - "links": [], - "mtime": "1786337548", - "content_hash": null + "links": [ + "src/commands/view.rs", + "src/cli.rs", + "src/main.rs", + "tests/integration_test.rs" + ], + "mtime": "1786338446", + "content_hash": "d6761d12938c73969ae963bb8f9c74dd3ba6b94a4024fadfdd8f8457c2777121" } ] } \ No newline at end of file diff --git a/docs/rfcs/0009.md b/docs/rfcs/0009.md index 10aefe6..9d28d08 100644 --- a/docs/rfcs/0009.md +++ b/docs/rfcs/0009.md @@ -1,9 +1,13 @@ --- title: "RFC-0009: просмотр RFC через внешний рендерер в команде view" -status: draft +status: accepted dependencies: [RFC-0003] superseded_by: null -links: [] +links: + - src/commands/view.rs + - src/cli.rs + - src/main.rs + - tests/integration_test.rs --- ## Problem diff --git a/src/cli.rs b/src/cli.rs index 9e31dd4..5e2896c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -33,6 +33,9 @@ pub enum Commands { View { /// RFC number number: String, + /// Print raw Markdown to stdout, ignoring RFC_VIEWER + #[arg(long)] + raw: bool, }, /// Show RFC status Status { diff --git a/src/commands/view.rs b/src/commands/view.rs index 97cea5b..5b04b89 100644 --- a/src/commands/view.rs +++ b/src/commands/view.rs @@ -1,10 +1,14 @@ +use std::env; use std::fs; +use std::io::Write; use std::path::Path; +use std::process::{Command, Stdio}; use crate::rfclib::rfc; -/// Executes the `view` command: prints RFC content to stdout -pub fn execute(project_root: &Path, number: &str) -> Result<(), String> { +/// Executes the `view` command: prints RFC content to stdout, +/// or renders it through an external viewer set via $RFC_VIEWER. +pub fn execute(project_root: &Path, number: &str, raw: bool) -> Result<(), String> { let normalized = rfc::normalize_number(number)?; let path = rfc::rfc_path(project_root, number)?; @@ -15,7 +19,47 @@ pub fn execute(project_root: &Path, number: &str) -> Result<(), String> { let content = fs::read_to_string(&path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; - print!("{}", content); + // --raw forces raw output and ignores RFC_VIEWER + if raw { + print!("{}", content); + return Ok(()); + } + + // RFC_VIEWER is set and non-empty — render through an external program + match env::var("RFC_VIEWER") { + Ok(viewer) if !viewer.trim().is_empty() => render_with_viewer(&viewer, &content), + _ => { + print!("{}", content); + Ok(()) + } + } +} + +/// Runs the external viewer, passing RFC content on stdin. +fn render_with_viewer(viewer: &str, content: &str) -> Result<(), String> { + let mut parts = viewer.split_whitespace(); + let program = parts.next().unwrap(); // non-empty string guaranteed by caller + let args: Vec<&str> = parts.collect(); + + let mut child = Command::new(program) + .args(&args) + .stdin(Stdio::piped()) + .spawn() + .map_err(|e| format!("Failed to launch viewer '{}': {}", viewer, e))?; + + if let Some(mut stdin) = child.stdin.take() { + stdin + .write_all(content.as_bytes()) + .map_err(|e| format!("Failed to write to viewer '{}': {}", viewer, e))?; + } + + let status = child + .wait() + .map_err(|e| format!("Failed to wait for viewer '{}': {}", viewer, e))?; + + if !status.success() { + return Err(format!("Viewer '{}' exited with error", viewer)); + } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 81d9b5c..113c397 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ fn main() { Commands::Init => commands::init::execute(&project_root), Commands::New { title } => commands::new::execute(&project_root, &title), Commands::List { status } => commands::list::execute(&project_root, status.as_deref()), - Commands::View { number } => commands::view::execute(&project_root, &number), + Commands::View { number, raw } => commands::view::execute(&project_root, &number, raw), Commands::Status { number } => commands::status::execute(&project_root, &number), Commands::Edit { number, force } => commands::edit::execute(&project_root, &number, force), Commands::Set { number, status, by } => { diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 9965d3d..2eeb894 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -52,6 +52,17 @@ fn run_rfc_cli_without_editor(project_dir: &Path, args: &[&str]) -> Output { .expect("Failed to execute rfc-cli") } +// Helper: run rfc-cli with a custom $RFC_VIEWER +fn run_rfc_cli_with_viewer(project_dir: &Path, args: &[&str], viewer: &str) -> Output { + let binary = env!("CARGO_BIN_EXE_rfc-cli"); + std::process::Command::new(binary) + .args(args) + .env("RFC_HOME", project_dir.as_os_str()) + .env("RFC_VIEWER", viewer) + .output() + .expect("Failed to execute rfc-cli") +} + // Helper: write an RFC file with given status AND update the index entry fn write_rfc_with_status(dir: &Path, number: &str, title: &str, status: &str) { let content = format!( @@ -782,6 +793,74 @@ fn test_view_invalid_number() { cleanup(&dir); } +#[test] +fn test_view_uses_rfc_viewer() { + let dir = create_temp_dir("view_viewer"); + + run_rfc_cli(&dir, &["init"]); + run_rfc_cli(&dir, &["new", "rendered RFC"]); + + // `cat` acts as a trivial viewer: it echoes stdin to stdout + let output = run_rfc_cli_with_viewer(&dir, &["view", "1"], "cat"); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(output.status.success()); + assert!( + stdout.contains("RFC-0001: rendered RFC"), + "viewer should receive and emit RFC content, got: {}", + stdout + ); + + cleanup(&dir); +} + +#[test] +fn test_view_raw_ignores_rfc_viewer() { + let dir = create_temp_dir("view_raw"); + + run_rfc_cli(&dir, &["init"]); + run_rfc_cli(&dir, &["new", "raw RFC"]); + + // Even with RFC_VIEWER set, --raw must print the raw Markdown to stdout. + // Use a viewer that would produce different output if invoked. + let output = run_rfc_cli_with_viewer(&dir, &["view", "1", "--raw"], "true"); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(output.status.success()); + assert!( + stdout.contains("RFC-0001: raw RFC"), + "--raw should print raw content regardless of RFC_VIEWER, got: {}", + stdout + ); + assert!( + stdout.contains("## Problem"), + "--raw should print full Markdown, got: {}", + stdout + ); + + cleanup(&dir); +} + +#[test] +fn test_view_viewer_launch_failure() { + let dir = create_temp_dir("view_viewer_fail"); + + run_rfc_cli(&dir, &["init"]); + run_rfc_cli(&dir, &["new", "viewer fail RFC"]); + + let output = run_rfc_cli_with_viewer(&dir, &["view", "1"], "definitely_not_a_real_program_xyz"); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!(!output.status.success()); + assert!( + stderr.contains("Failed to launch viewer"), + "should report viewer launch failure, got: {}", + stderr + ); + + cleanup(&dir); +} + // ============================================================ // Tests for `status` command // ============================================================ From 3e1195426e1b80b393b14bf4ae0d63c06b9f385d Mon Sep 17 00:00:00 2001 From: Alexey Nikandrov Date: Mon, 10 Aug 2026 08:11:35 +0300 Subject: [PATCH 5/5] Bump version to 0.1.4, mark RFC-0009 implemented, add CHANGELOG --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- docs/rfcs/.index.json | 6 +++--- docs/rfcs/0009.md | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0a016b6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [0.1.4] + +### Added +- `view` command now renders RFC content through an external Markdown renderer + when the `RFC_VIEWER` environment variable is set (content is piped on stdin), + mirroring how `edit` uses `$EDITOR` (RFC-0009). +- `--raw` flag for `view` to force raw Markdown output, ignoring `RFC_VIEWER`. diff --git a/Cargo.lock b/Cargo.lock index ab38b73..d5e0d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,7 +239,7 @@ dependencies = [ [[package]] name = "rfc-cli" -version = "0.1.3" +version = "0.1.4" dependencies = [ "clap", "clap_builder", diff --git a/Cargo.toml b/Cargo.toml index 813aff7..7d1e519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rfc-cli" -version = "0.1.3" +version = "0.1.4" edition = "2021" [dependencies] diff --git a/docs/rfcs/.index.json b/docs/rfcs/.index.json index 9e82177..036cc4f 100644 --- a/docs/rfcs/.index.json +++ b/docs/rfcs/.index.json @@ -157,7 +157,7 @@ { "number": "0009", "title": "RFC-0009: просмотр RFC через внешний рендерер в команде view", - "status": "accepted", + "status": "implemented", "dependencies": [ "RFC-0003" ], @@ -168,8 +168,8 @@ "src/main.rs", "tests/integration_test.rs" ], - "mtime": "1786338446", - "content_hash": "d6761d12938c73969ae963bb8f9c74dd3ba6b94a4024fadfdd8f8457c2777121" + "mtime": "1786338690", + "content_hash": "57258823363c37be4463a51b3fe9b4dc23794f457c3f4c80392bc227335574d7" } ] } \ No newline at end of file diff --git a/docs/rfcs/0009.md b/docs/rfcs/0009.md index 9d28d08..102fed8 100644 --- a/docs/rfcs/0009.md +++ b/docs/rfcs/0009.md @@ -1,6 +1,6 @@ --- title: "RFC-0009: просмотр RFC через внешний рендерер в команде view" -status: accepted +status: implemented dependencies: [RFC-0003] superseded_by: null links: