feat(cli): add shunt add provider to scaffold provider config#69
feat(cli): add shunt add provider to scaffold provider config#69amondnet wants to merge 1 commit into
shunt add provider to scaffold provider config#69Conversation
Print a ready-to-paste [providers.<name>] TOML block for known backends (built-in providers plus Anthropic-compatible gateways like kimi/deepseek/glm/minimax/mimo/openrouter/vercel). Print-only, never writes files, and the output is valid TOML so it can be piped: `shunt add provider kimi >> shunt.toml`. Adds a static catalog table (src/catalog.rs), unit tests that round-trip validate the generated TOML through the real config loader, and docs updates (README x4, site cli.md and providers.md).
There was a problem hiding this comment.
Code Review
이번 풀리퀘스트는 알려진 백엔드 프로바이더의 설정을 쉽게 생성할 수 있는 shunt add provider <name> CLI 명령어를 추가하고 관련 문서들을 업데이트합니다. 리뷰어는 핵심 비즈니스 로직에서 입출력(I/O) 부작용을 격리하기 위해 src/catalog.rs의 add_provider 함수가 표준 출력에 직접 출력하는 대신 Result<String>을 반환하도록 리팩토링하고, 실제 출력 처리는 애플리케이션 진입점인 src/main.rs에서 담당하도록 책임을 분리할 것을 제안했습니다.
| pub fn add_provider(name: Option<&str>) -> anyhow::Result<()> { | ||
| match name { | ||
| None => { | ||
| print!("{}", list_text()); | ||
| Ok(()) | ||
| } | ||
| Some(name) => match find(name) { | ||
| Some(entry) => { | ||
| print!("{}", render(entry)); | ||
| Ok(()) | ||
| } | ||
| None => { | ||
| let known = CATALOG | ||
| .iter() | ||
| .map(|entry| entry.name) | ||
| .collect::<Vec<_>>() | ||
| .join(", "); | ||
| anyhow::bail!("unknown provider '{name}'. Known providers: {known}") | ||
| } | ||
| }, | ||
| } | ||
| } |
There was a problem hiding this comment.
[MEDIUM] add_provider 함수가 표준 출력(stdout)에 직접 출력하는 대신 String을 반환하도록 리팩토링
Symptom: add_provider 함수가 포맷팅된 문자열을 반환하는 대신 print! 매크로를 사용하여 stdout에 직접 출력하고 있습니다.
Source: Hunt & Thomas — Pragmatic Programmer (Orthogonality)
Consequence: 비즈니스 로직과 입출력(I/O) 부작용(side effect)이 결합되어 코드의 직교성(Orthogonality)이 저하됩니다. 이로 인해 CLI가 아닌 다른 환경(예: GUI, 웹 API, 또는 라이브러리 형태)에서 이 함수를 재사용하기 어려워지며, 표준 출력을 캡처하여 검증하는 테스트를 작성하기가 까다로워집니다.
Remedy: add_provider 함수가 anyhow::Result<String>을 반환하도록 수정하여 부작용을 제거하고, 실제 출력은 애플리케이션의 진입점인 main.rs에서 처리하도록 책임을 분리합니다.
pub fn add_provider(name: Option<&str>) -> anyhow::Result<String> {
match name {
None => Ok(list_text()),
Some(name) => match find(name) {
Some(entry) => Ok(render(entry)),
None => {
let known = CATALOG
.iter()
.map(|entry| entry.name)
.collect::<Vec<_>>()
.join(", ");
anyhow::bail!("unknown provider '{name}'. Known providers: {known}")
}
},
}
}References
- Isolate side effects at the boundaries of the application rather than inside core/validation functions.
| Some(Command::Add { | ||
| target: AddTarget::Provider { name }, | ||
| }) => shunt::catalog::add_provider(name.as_deref()), |
There was a problem hiding this comment.
[MEDIUM] add_provider의 반환 값을 받아 main에서 출력하도록 수정
Symptom: main.rs에서 add_provider를 호출할 때, 해당 함수가 내부적으로 직접 출력할 것을 기대하고 반환된 Result<()>만 처리하고 있습니다.
Source: Martin — Clean Architecture (Conceptual Integrity)
Consequence: 입출력(I/O) 부작용이 라이브러리 모듈 내부에서 발생하여 애플리케이션 경계(boundary)에서의 부작용 격리 원칙이 깨집니다.
Remedy: add_provider가 반환하는 Result<String>을 받아 main 함수 내에서 직접 print!를 통해 출력하도록 수정합니다.
Some(Command::Add {
target: AddTarget::Provider { name },
}) => {
let output = shunt::catalog::add_provider(name.as_deref())?;
print!("{output}");
Ok(())
}References
- Isolate side effects at the boundaries of the application rather than inside core/validation functions.
Summary
Adds a new CLI subcommand,
shunt add provider <name>, that prints a ready-to-paste[providers.<name>]TOML block for a known backend — the built-in providers (openai/codex/xai/grok/anthropic) plus the Anthropic-compatible gateways (kimi, deepseek, glm, minimax, mimo, openrouter, vercel). It is print-only and never writes files; the output is valid TOML so it can be piped straight into a config file (e.g.shunt add provider kimi >> shunt.toml). Modeled on Flue'sflue addprint-only philosophy.Milestone / spec
Not tied to a specific
docs/milestone spec — this is a standalone CLI ergonomics feature (config scaffolding), not a protocol/translation change.Changes
src/catalog.rs(AGENTS.md table-driven rule); case-insensitive provider names plus aliases (zai/z.ai→glm,moonshot→kimi,chatgpt→codex,xiaomi→mimo).catalog_entries_render_to_valid_config, which round-trips every catalog entry through the real config loader +Config::validate()so a badbase_url/auth/kindcan't ship; plus a test pinning the literalauth=token per mode, and a catalog name/alias uniqueness guard.reference/cli.md(new## shunt add providersection), andguides/providers.md.Checklist
cargo buildpassescargo testpasses (new behavior is covered; tests run without network/loopback where possible) — 148 passedcargo clippy --all-targets -- -D warningscleancargo fmt --all --checkcleandocs/updated if this change deviates from it — N/A, no spec deviationREADME.md/site/as applicable (wiki/is generated; don't hand-edit)Notes for reviewers
catalog_entries_render_to_valid_configis the key correctness guard here: it forces every catalog entry to round-trip through the real config loader andConfig::validate(), so a typo'dbase_url/auth/kindfails CI instead of shipping silently./review:code-review-looppass locally before pushing.Summary by cubic
Adds
shunt add provider <name>to print a ready-to-paste[providers.<name>]TOML block for known backends. Output is print-only and can be piped intoshunt.tomlfor quick setup.openai,codex,xai,grok,anthropic) and Anthropic-compatible gateways (kimi,deepseek,glm,minimax,mimo,openrouter,vercel); names are case-insensitive with aliases (e.g.,zai/z.ai→glm,moonshot→kimi,chatgpt→codex).kind,base_url,auth, andapi_key_env, and adds a commented example route; never writes files. Omit the name to list known providers; unknown names error and show the list.src/catalog.rswith tests that round-trip each entry through the real config loader andConfig::validate(); docs updated (README ×4, CLI reference, providers guide).Written for commit caa9b7e. Summary will update on new commits.