Skip to content

Commit a9ffa9c

Browse files
authored
Merge pull request #13 from trouze/feat/auth
feat(src/cli/commands/init.rs): create init command and restructure a…
2 parents ad7ea5d + a2c3496 commit a9ffa9c

12 files changed

Lines changed: 403 additions & 244 deletions

File tree

src/api/discovery/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ pub fn require_environment_id(config: &Config) -> Result<u64> {
6565
.ok_or_else(|| {
6666
DbtpError::config(
6767
"environment_id is required for Discovery API; \
68-
set via --environment-id, DBTP_ENVIRONMENT_ID, or `dbtp configure`",
68+
set via --environment-id or DBTP_ENVIRONMENT_ID",
6969
)
7070
})
7171
}

src/cli/commands/config_cmd.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use clap::{Args, Subcommand};
2+
3+
use crate::core::config;
4+
use crate::core::error::Result;
5+
6+
#[derive(Debug, Args)]
7+
pub struct ConfigArgs {
8+
#[command(subcommand)]
9+
pub command: ConfigCommand,
10+
}
11+
12+
#[derive(Debug, Subcommand)]
13+
pub enum ConfigCommand {
14+
/// Show all current settings
15+
List,
16+
/// Get the value of a single setting
17+
Get {
18+
/// Key name: host, token, account-id, project-id, output
19+
key: String,
20+
},
21+
/// Set a configuration property
22+
Set {
23+
/// Key name: host, token, account-id, project-id, output
24+
key: String,
25+
/// Value to set
26+
value: String,
27+
},
28+
/// Clear a configuration property
29+
Unset {
30+
/// Key name: host, token, account-id, project-id, output
31+
key: String,
32+
},
33+
}
34+
35+
pub async fn exec(args: &ConfigArgs) -> Result<()> {
36+
match &args.command {
37+
ConfigCommand::List => {
38+
let path = config::config_path()?;
39+
if !path.exists() {
40+
eprintln!("No config file found at {}", path.display());
41+
eprintln!("Run `dbtp init` to set up credentials.");
42+
return Ok(());
43+
}
44+
45+
let keys = ["host", "token", "account-id", "project-id", "output"];
46+
for key in &keys {
47+
match config::get_property(key)? {
48+
Some(val) => {
49+
let display = if *key == "token" { mask_token(&val) } else { val };
50+
println!("{key} = {display}");
51+
}
52+
None => println!("{key} = (not set)"),
53+
}
54+
}
55+
}
56+
57+
ConfigCommand::Get { key } => match config::get_property(key)? {
58+
Some(val) => {
59+
let display = if key == "token" { mask_token(&val) } else { val };
60+
println!("{display}");
61+
}
62+
None => eprintln!("{key} is not set"),
63+
},
64+
65+
ConfigCommand::Set { key, value } => {
66+
config::set_property(key, value)?;
67+
eprintln!("Set {key} = {}", if key == "token" { mask_token(value) } else { value.clone() });
68+
}
69+
70+
ConfigCommand::Unset { key } => {
71+
config::unset_property(key)?;
72+
eprintln!("Unset {key}");
73+
}
74+
}
75+
76+
Ok(())
77+
}
78+
79+
fn mask_token(token: &str) -> String {
80+
if token.len() <= 8 {
81+
return "***".to_string();
82+
}
83+
format!("{}...{}", &token[..4], &token[token.len() - 4..])
84+
}

src/cli/commands/configure.rs

Lines changed: 0 additions & 173 deletions
This file was deleted.

src/cli/commands/environments.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub async fn exec(
5656
.ok_or_else(|| {
5757
DbtpError::config(
5858
"project_id is required for environment operations; \
59-
set via --project-id, DBTP_PROJECT_ID, or `dbtp configure`",
59+
set via --project-id, DBTP_PROJECT_ID, or `dbtp config set project-id`",
6060
)
6161
})?;
6262
let pid = resolve::resolve_project(client, raw_pid).await?;

src/cli/commands/init.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use clap::Args;
2+
3+
use crate::api::admin::{accounts, projects};
4+
use crate::core::config::{self, ConfigFile, Connection, Defaults};
5+
use crate::core::error::Result;
6+
use crate::core::rest_client::RestClient;
7+
8+
#[derive(Debug, Args)]
9+
pub struct InitArgs {
10+
/// Disable interactive selection menus (use plain prompts only)
11+
#[arg(long)]
12+
pub non_interactive: bool,
13+
}
14+
15+
pub async fn exec(args: &InitArgs) -> Result<()> {
16+
eprintln!("Setting up dbtp credentials.\n");
17+
18+
let host = config::prompt("dbt Cloud host", "https://cloud.getdbt.com");
19+
let token = config::prompt("API token", "");
20+
21+
let host = if !host.starts_with("https://") && !host.starts_with("http://") {
22+
format!("https://{host}")
23+
} else {
24+
host
25+
};
26+
27+
let account_id_str = config::prompt("Account ID", "");
28+
let account_id = account_id_str
29+
.parse::<u64>()
30+
.map_err(|_| crate::core::error::DbtpError::config("Account ID must be a number"))?;
31+
32+
// Validate the connection
33+
eprint!("\nValidating connection...");
34+
match accounts::get_by_id(&host, &token, account_id).await {
35+
Ok(_) => eprintln!(" ok"),
36+
Err(e) => {
37+
eprintln!(" failed\nWarning: could not verify credentials: {e}");
38+
eprintln!("Saving anyway. Run `dbtp accounts show` to verify later.\n");
39+
}
40+
}
41+
42+
let project_id = if !args.non_interactive && config::is_interactive() && !token.is_empty() {
43+
pick_project(&host, &token, account_id).await
44+
} else {
45+
None
46+
};
47+
48+
let file = ConfigFile {
49+
connection: Connection {
50+
host: Some(host),
51+
token: Some(token),
52+
account_id: Some(account_id),
53+
},
54+
defaults: Defaults {
55+
project_id,
56+
output: "table".to_string(),
57+
},
58+
};
59+
60+
config::save_config(&file)?;
61+
62+
let path = config::config_path()?;
63+
eprintln!("\nConfiguration saved to {}", path.display());
64+
eprintln!("Run `dbtp config set project-id <id-or-name>` to set a default project.");
65+
66+
Ok(())
67+
}
68+
69+
async fn pick_project(host: &str, token: &str, account_id: u64) -> Option<String> {
70+
let client = match RestClient::new(host, token, Some(account_id)) {
71+
Ok(c) => c,
72+
Err(e) => {
73+
eprintln!("\nCould not build API client: {e}");
74+
return None;
75+
}
76+
};
77+
78+
eprintln!("\nFetching projects...");
79+
let list = match projects::list(&client, &[], None).await {
80+
Ok(l) => l,
81+
Err(e) => {
82+
eprintln!("Could not fetch projects: {e}");
83+
return None;
84+
}
85+
};
86+
87+
if list.is_empty() {
88+
eprintln!(" No projects found.");
89+
return None;
90+
}
91+
92+
let items: Vec<(String, String)> = list
93+
.iter()
94+
.filter_map(|p| {
95+
let name = p["name"].as_str()?.to_string();
96+
let id = p["id"].as_u64()?.to_string();
97+
Some((name, id))
98+
})
99+
.collect();
100+
101+
config::prompt_select("Select default project", &items)
102+
}

src/cli/commands/jobs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ pub async fn exec(args: &JobsArgs, client: &RestClient, config: &Config) -> Resu
121121
.ok_or_else(|| {
122122
DbtpError::config(
123123
"project_id is required for job creation; \
124-
set via --project-id, DBTP_PROJECT_ID, or `dbtp configure`",
124+
set via --project-id, DBTP_PROJECT_ID, or `dbtp config set project-id`",
125125
)
126126
})?;
127127
let pid = resolve::resolve_project(client, raw_pid).await?;

0 commit comments

Comments
 (0)