use anyhow::{Context, Result};
use clap::Parser;
use crate::{auth, ui, workspace};
#[derive(Debug, Parser)]
pub struct LinkArgs {
#[arg(long)]
pub module: Option<String>,
#[arg(long)]
pub url: Option<String>,
#[arg(long)]
pub no_browser: bool,
}
pub async fn run(args: LinkArgs) -> Result<()> {
ui::header(
"portaki link",
"Open the repository page — linking needs a GitHub installation, chosen in the dashboard.",
);
let current = workspace::resolve(args.module.as_deref(), None)?
.into_iter()
.next()
.map(|member| member.id)
.unwrap_or_default();
if current.is_empty() {
anyhow::bail!("portaki.module.json carries no id — run from the module root");
}
let cwd = std::env::current_dir()?;
let others: Vec<String> = workspace::members(&cwd)
.into_iter()
.map(|member| member.id)
.filter(|id| *id != current)
.collect();
let page = link_page(&auth::api_base_url(args.url.as_deref()), ¤t).await?;
let target = with_also(&page, &others);
if args.no_browser || !ui::open_browser(&target) {
ui::field("open", &target);
} else {
ui::success("opened your browser");
ui::field("link", &target);
}
ui::blank();
Ok(())
}
pub fn with_also(page: &str, others: &[String]) -> String {
if others.is_empty() {
return page.to_string();
}
let separator = if page.contains('?') { '&' } else { '?' };
format!("{page}{separator}also={}", others.join(","))
}
async fn link_page(api_base: &str, module_id: &str) -> Result<String> {
let url = format!(
"{}/registry/v1/modules/{module_id}/link-page",
api_base.trim_end_matches('/')
);
let response = crate::http::client()
.get(&url)
.send()
.await
.with_context(|| format!("demander la page Dépôt au registre ({url})"))?;
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
if !(200..300).contains(&status) {
anyhow::bail!("le registre n'a pas rendu la page Dépôt ({status}) : {body}");
}
serde_json::from_str::<serde_json::Value>(&body)
.ok()
.and_then(|parsed| parsed.get("url")?.as_str().map(str::to_string))
.context("réponse link-page sans url")
}
#[cfg(test)]
mod tests {
use super::*;
fn ids(raw: &[&str]) -> Vec<String> {
raw.iter().map(ToString::to_string).collect()
}
#[test]
fn the_other_modules_ride_along_in_also() {
assert_eq!(
with_also(
"https://developer.portaki.app/access-guide/repository",
&ids(&["nuki", "wifi-guest"])
),
"https://developer.portaki.app/access-guide/repository?also=nuki,wifi-guest"
);
assert_eq!(
with_also("https://developer.portaki.app/weather/repository", &[]),
"https://developer.portaki.app/weather/repository"
);
assert_eq!(
with_also(
"http://localhost:3000/dev/x/repository?from=cli",
&ids(&["y"])
),
"http://localhost:3000/dev/x/repository?from=cli&also=y"
);
}
}