Skip to content

Commit 1119c24

Browse files
committed
feat: default git provider for shorthand for adding modules
you can run `nuance add freepicheep/nu-salesforce` and it will install the module since github is the default git provider. you can change this if you want
1 parent b239964 commit 1119c24

4 files changed

Lines changed: 263 additions & 11 deletions

File tree

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ nuance init
3434
# Add a dependency
3535
nuance add https://github.com/user/nu-some-module
3636

37+
# Or use owner/repo shorthand (defaults to github)
38+
nuance add user/nu-some-module
39+
3740
# Install all dependencies from mod.toml
3841
nuance install
3942

@@ -61,11 +64,11 @@ To make the installed modules available to `use` in Nushell without specifying t
6164
Nuance provides two ways to do this:
6265

6366
### 1. Manual Overlay (Recommended)
64-
`nuance install` automatically generates an `activate.nu` script inside the `.nu_modules` directory. This script adds `.nu_modules/` to your `$env.NU_LIB_DIRS` **and automatically imports** all installed modules into your active scope using `export use <module> *`.
67+
`nuance install` and `nuance init` automatically generate an `activate.nu` script inside the `.nu_modules` directory. This script adds `.nu_modules/` to your `$env.NU_LIB_DIRS` **and automatically imports** all installed modules into your active scope using `export use <module> *`.
6568

6669
You can activate it using Nushell's `overlay` command:
6770

68-
```bash
71+
```nu
6972
overlay use .nu_modules/activate.nu
7073
```
7174

@@ -87,7 +90,7 @@ nuance hook
8790
[package]
8891
name = "my-module"
8992
version = "0.1.0"
90-
description = "Something useful"
93+
description = "a wonderful nu module anyone can use"
9194

9295
[dependencies]
9396
nu-utils = { git = "https://github.com/user/nu-utils", tag = "v1.0.0" }
@@ -102,13 +105,24 @@ Each dependency must specify exactly one of `tag`, `branch`, or `rev`.
102105
| Command | Description |
103106
|---------|-------------|
104107
| `nuance init` | Create a new `mod.toml` in the current directory |
105-
| `nuance add <url>` | Add a dependency (auto-detects latest tag) |
108+
| `nuance add <source>` | Add a dependency from a URL or owner/repo shorthand (auto-detects latest tag) |
106109
| `nuance install` | Install dependencies from `mod.toml` |
107110
| `nuance install --frozen` | Install from lockfile only (CI-friendly) |
108111
| `nuance update` | Re-resolve all dependencies |
109112
| `nuance remove <name>` | Remove a dependency |
110113
| `nuance hook` | Print the auto-activate hook for config.nu |
111114

115+
## Global config (`~/.config/nuance/config.toml`)
116+
117+
You can set a default git provider used for `owner/repo` shorthand in `nuance add`.
118+
119+
```toml
120+
default_git_provider = "github" # default
121+
```
122+
123+
Supported provider aliases are `github`, `gitlab`, `codeberg`, and `bitbucket`.
124+
You can also set a custom host like `git.example.com` or a full `https://...` base URL.
125+
112126
## License
113127

114128
MIT

src/cli.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ pub enum Commands {
3939
/// Re-resolve all dependencies (ignore existing lockfile)
4040
Update,
4141

42-
/// Add a package from a git repository URL
42+
/// Add a package from a git URL or owner/repo shorthand
4343
Add {
4444
/// Add to global config instead of local mod.toml
4545
#[arg(short = 'g', long)]
4646
global: bool,
4747

48-
/// Git repository URL (e.g. https://github.com/user/nu-module)
48+
/// Git URL (e.g. https://github.com/user/nu-module) or owner/repo shorthand
4949
url: String,
5050

5151
/// Pin to a specific tag

src/config.rs

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,48 @@
11
use serde::{Deserialize, Serialize};
22
use std::collections::HashMap;
3-
use std::path::PathBuf;
3+
use std::path::{Path, PathBuf};
44

55
use crate::error::{NuanceError, Result};
66
use crate::manifest::DependencySpec;
77

8+
const DEFAULT_GIT_PROVIDER: &str = "github";
9+
10+
fn default_git_provider() -> String {
11+
DEFAULT_GIT_PROVIDER.to_string()
12+
}
13+
14+
fn known_provider_base_url(provider: &str) -> Option<&'static str> {
15+
match provider {
16+
"github" => Some("https://github.com"),
17+
"gitlab" => Some("https://gitlab.com"),
18+
"codeberg" => Some("https://codeberg.org"),
19+
"bitbucket" => Some("https://bitbucket.org"),
20+
_ => None,
21+
}
22+
}
23+
24+
fn normalize_provider_base_url(provider: &str) -> Option<String> {
25+
let trimmed = provider.trim().trim_end_matches('/');
26+
if trimmed.is_empty() {
27+
return None;
28+
}
29+
30+
let lowercase = trimmed.to_ascii_lowercase();
31+
if let Some(base) = known_provider_base_url(&lowercase) {
32+
return Some(base.to_string());
33+
}
34+
35+
if trimmed.starts_with("https://") || trimmed.starts_with("http://") {
36+
return Some(trimmed.to_string());
37+
}
38+
39+
if trimmed.contains('.') && !trimmed.contains('/') && !trimmed.contains(' ') {
40+
return Some(format!("https://{trimmed}"));
41+
}
42+
43+
None
44+
}
45+
846
/// The global nuance config file: `~/.config/nuance/config.toml`.
947
///
1048
/// Tracks globally-installed modules and optional path overrides.
@@ -13,24 +51,49 @@ pub struct GlobalConfig {
1351
#[serde(default, skip_serializing_if = "Option::is_none")]
1452
pub modules_dir: Option<String>,
1553

54+
#[serde(default = "default_git_provider")]
55+
pub default_git_provider: String,
56+
1657
#[serde(default)]
1758
pub dependencies: HashMap<String, DependencySpec>,
1859
}
1960

61+
impl Default for GlobalConfig {
62+
fn default() -> Self {
63+
Self {
64+
modules_dir: None,
65+
default_git_provider: default_git_provider(),
66+
dependencies: HashMap::new(),
67+
}
68+
}
69+
}
70+
2071
impl GlobalConfig {
2172
/// Load the global config, creating it with defaults if it doesn't exist.
2273
pub fn load() -> Result<Self> {
2374
let path = global_config_path()?;
2475

2576
if !path.exists() {
26-
let config = GlobalConfig {
27-
modules_dir: None,
28-
dependencies: HashMap::new(),
29-
};
77+
let config = GlobalConfig::default();
3078
config.save()?;
3179
return Ok(config);
3280
}
3381

82+
Self::load_from_path(&path)
83+
}
84+
85+
/// Load global config if present, otherwise return defaults without writing.
86+
pub fn load_or_default() -> Result<Self> {
87+
let path = global_config_path()?;
88+
89+
if !path.exists() {
90+
return Ok(GlobalConfig::default());
91+
}
92+
93+
Self::load_from_path(&path)
94+
}
95+
96+
fn load_from_path(path: &Path) -> Result<Self> {
3497
let content = std::fs::read_to_string(&path)?;
3598
let config: GlobalConfig = toml::from_str(&content)
3699
.map_err(|e| NuanceError::Config(format!("failed to parse {}: {e}", path.display())))?;
@@ -62,6 +125,16 @@ impl GlobalConfig {
62125
global_modules_dir()
63126
}
64127
}
128+
129+
/// Resolve the configured default git provider to a base URL.
130+
pub fn default_git_provider_base_url(&self) -> Result<String> {
131+
normalize_provider_base_url(&self.default_git_provider).ok_or_else(|| {
132+
NuanceError::Config(format!(
133+
"unsupported default_git_provider '{}'; use one of github, gitlab, codeberg, bitbucket, or a custom host like git.example.com",
134+
self.default_git_provider
135+
))
136+
})
137+
}
65138
}
66139

67140
/// Returns the global config directory: `~/.config/nuance/`.
@@ -100,6 +173,7 @@ mod tests {
100173
fn round_trip() {
101174
let config = GlobalConfig {
102175
modules_dir: None,
176+
default_git_provider: "github".to_string(),
103177
dependencies: HashMap::from([(
104178
"nu-utils".to_string(),
105179
DependencySpec {
@@ -117,25 +191,29 @@ mod tests {
117191
assert_eq!(parsed.dependencies.len(), 1);
118192
assert!(parsed.dependencies.contains_key("nu-utils"));
119193
assert!(parsed.modules_dir.is_none());
194+
assert_eq!(parsed.default_git_provider, "github");
120195
}
121196

122197
#[test]
123198
fn round_trip_with_override() {
124199
let config = GlobalConfig {
125200
modules_dir: Some("/custom/path".to_string()),
201+
default_git_provider: "gitlab".to_string(),
126202
dependencies: HashMap::new(),
127203
};
128204

129205
let serialized = toml::to_string_pretty(&config).unwrap();
130206
let parsed: GlobalConfig = toml::from_str(&serialized).unwrap();
131207

132208
assert_eq!(parsed.modules_dir.as_deref(), Some("/custom/path"));
209+
assert_eq!(parsed.default_git_provider, "gitlab");
133210
}
134211

135212
#[test]
136213
fn modules_dir_custom() {
137214
let config = GlobalConfig {
138215
modules_dir: Some("/custom/modules".to_string()),
216+
default_git_provider: "github".to_string(),
139217
dependencies: HashMap::new(),
140218
};
141219
assert_eq!(
@@ -148,6 +226,7 @@ mod tests {
148226
fn modules_dir_default() {
149227
let config = GlobalConfig {
150228
modules_dir: None,
229+
default_git_provider: "github".to_string(),
151230
dependencies: HashMap::new(),
152231
};
153232
let dir = config.modules_dir().unwrap();
@@ -167,4 +246,54 @@ mod tests {
167246
let lock = global_lock_path().unwrap();
168247
assert!(lock.ends_with("nuance/config.lock"));
169248
}
249+
250+
#[test]
251+
fn missing_provider_defaults_to_github() {
252+
let toml = r#"
253+
modules_dir = "/tmp/nuance-modules"
254+
255+
[dependencies]
256+
"#;
257+
let parsed: GlobalConfig = toml::from_str(toml).unwrap();
258+
assert_eq!(parsed.default_git_provider, "github");
259+
}
260+
261+
#[test]
262+
fn default_provider_base_url_resolves_known_aliases() {
263+
let mut config = GlobalConfig::default();
264+
assert_eq!(
265+
config.default_git_provider_base_url().unwrap(),
266+
"https://github.com"
267+
);
268+
269+
config.default_git_provider = "gitlab".to_string();
270+
assert_eq!(
271+
config.default_git_provider_base_url().unwrap(),
272+
"https://gitlab.com"
273+
);
274+
}
275+
276+
#[test]
277+
fn default_provider_base_url_supports_custom_domain() {
278+
let config = GlobalConfig {
279+
modules_dir: None,
280+
default_git_provider: "git.example.com".to_string(),
281+
dependencies: HashMap::new(),
282+
};
283+
assert_eq!(
284+
config.default_git_provider_base_url().unwrap(),
285+
"https://git.example.com"
286+
);
287+
}
288+
289+
#[test]
290+
fn default_provider_base_url_rejects_unknown_provider() {
291+
let config = GlobalConfig {
292+
modules_dir: None,
293+
default_git_provider: "not-a-provider".to_string(),
294+
dependencies: HashMap::new(),
295+
};
296+
let err = config.default_git_provider_base_url().unwrap_err();
297+
assert!(err.to_string().contains("unsupported default_git_provider"));
298+
}
170299
}

0 commit comments

Comments
 (0)