Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,40 @@ starforge/

---

## Privacy & Telemetry

StarForge values your privacy.

### Local-Only Telemetry Guarantee
To help improve CLI usability, starforge collects anonymous usage telemetry (such as command names and execution times). This telemetry data is **stored purely locally** at `~/.starforge/data/telemetry.log`. **No network requests are ever made** for telemetry transmission; your telemetry data never leaves your machine.

### Explicit Opt-Out Methods
You can easily disable telemetry at any time using one of three methods:

1. **Config Command:**
```bash
starforge config set telemetry.enabled false
```

2. **Telemetry Subcommand:**
```bash
starforge telemetry disable
```

3. **Environment Variable:**
Set the `STARFORGE_TELEMETRY` environment variable to `false` or `0` in your shell profile:
```bash
export STARFORGE_TELEMETRY=false
```

To inspect your current telemetry status:
```bash
starforge telemetry status
```

---


## Configuration

starforge stores all data in `~/.starforge/config.toml`:
Expand Down
49 changes: 49 additions & 0 deletions src/commands/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::utils::{config, print as p};
use anyhow::Result;
use clap::Subcommand;

#[derive(Subcommand)]
pub enum ConfigCommands {
/// Set a configuration parameter
Set {
/// Configuration key (e.g., telemetry.enabled)
key: String,
/// Configuration value (e.g., true/false)
value: String,
},
/// Show current configuration
Show,
}

pub fn handle(cmd: ConfigCommands) -> Result<()> {
match cmd {
ConfigCommands::Set { key, value } => set_config(key, value),
ConfigCommands::Show => show_config(),
}
}

fn set_config(key: String, value: String) -> Result<()> {
let mut cfg = config::load()?;
match key.as_str() {
"telemetry.enabled" => {
let enabled = value.parse::<bool>()
.map_err(|_| anyhow::anyhow!("Invalid value '{}' for telemetry.enabled. Must be 'true' or 'false'.", value))?;
cfg.telemetry_enabled = Some(enabled);
config::save(&cfg)?;
p::success(&format!("Configuration key 'telemetry.enabled' set to '{}'", enabled));
}
_ => anyhow::bail!("Unknown configuration key '{}'. Supported keys: telemetry.enabled", key),
}
Ok(())
}

fn show_config() -> Result<()> {
let cfg = config::load()?;
p::header("starforge Configuration");
p::separator();
p::kv("Config file", &config::config_path().display().to_string());
p::kv("network", &cfg.network);
p::kv("telemetry.enabled", &cfg.telemetry_enabled.unwrap_or(true).to_string());
p::separator();
Ok(())
}
3 changes: 3 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod benchmark;
pub mod command_tree;
pub mod completions;
pub mod config;
pub mod contract;
pub mod deploy;
pub mod gas;
Expand All @@ -14,9 +15,11 @@ pub mod new;
pub mod node;
pub mod plugin;
pub mod shell;
pub mod telemetry;
pub mod template;
pub mod test;
pub mod tutorial;
pub mod tx;
pub mod upgrade;
pub mod wallet;

14 changes: 8 additions & 6 deletions src/commands/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,13 @@ fn update(name: Option<String>, yes: bool) -> Result<()> {

match status {
Ok(s) if s.success() => {
// Re-discover commands from the updated library.
let cmds = discover_commands_from_library(&pl.path)
.unwrap_or_else(|_| pl.commands.clone());
registry::install_plugin(&pl.name, std::path::Path::new(&pl.path), &pl.source, &pl.starforge_version, &pl.plugin_version, cmds)?;
registry::install_plugin(
&pl.name,
std::path::Path::new(&pl.path),
&pl.source,
&pl.starforge_version,
&pl.plugin_version,
)?;
p::success(&format!(" '{}' updated via cargo install", pl.name));
updated += 1;
}
Expand Down Expand Up @@ -416,7 +419,7 @@ fn update(name: Option<String>, yes: bool) -> Result<()> {
})
.unwrap_or(0);

let installed_epoch = 0u64; // no install timestamp stored; treat as always-stale
let installed_epoch = 0;

if modified > installed_epoch {
// Library on disk is newer — refresh the registry entry.
Expand All @@ -428,7 +431,6 @@ fn update(name: Option<String>, yes: bool) -> Result<()> {
&pl.source,
&pl.starforge_version,
&pl.plugin_version,
cmds,
)?;
p::success(&format!(
" '{}' library on disk is newer — registry refreshed.",
Expand Down
40 changes: 40 additions & 0 deletions src/commands/telemetry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::utils::{config, print as p, telemetry};
use anyhow::Result;
use clap::Subcommand;

#[derive(Subcommand)]
pub enum TelemetryCommands {
/// Enable telemetry collections
Enable,
/// Disable telemetry collections
Disable,
/// Show current telemetry status
Status,
}

pub fn handle(cmd: TelemetryCommands) -> Result<()> {
match cmd {
TelemetryCommands::Enable => {
telemetry::set_telemetry_enabled(true)?;
p::success("Telemetry collections enabled.");
}
TelemetryCommands::Disable => {
telemetry::set_telemetry_enabled(false)?;
p::success("Telemetry collections disabled.");
}
TelemetryCommands::Status => {
let cfg = config::load()?;
let enabled = cfg.telemetry_enabled.unwrap_or(true);
let env_override = std::env::var("STARFORGE_TELEMETRY").ok();

p::header("Telemetry Status");
p::separator();
p::kv("Configured Enabled", &enabled.to_string());
if let Some(env_val) = env_override {
p::kv("Environment Override (STARFORGE_TELEMETRY)", &env_val);
}
p::separator();
}
}
Ok(())
}
4 changes: 4 additions & 0 deletions src/commands/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ fn install(
version,
cli_version_min,
cli_version_max,
None,
None,
None,
None,
)?;
p::header("Template Install");
p::info("Template package installed into the local registry.");
Expand Down
16 changes: 11 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,13 @@ enum Commands {
/// Static analysis and linting for Soroban contracts
Lint(commands::lint::LintArgs),

/// Display the full command tree (all top-level commands, subcommands, and plugin extensions)
#[command(name = "commands")]
CommandTree,
/// Manage configuration settings
#[command(subcommand)]
Config(commands::config::ConfigCommands),

/// Manage telemetry settings
#[command(subcommand)]
Telemetry(commands::telemetry::TelemetryCommands),

/// Execute an installed plugin command (e.g. `starforge defi ...`)
#[command(external_subcommand)]
Expand Down Expand Up @@ -147,7 +151,8 @@ fn main() {
Commands::Template(_) => "template",
Commands::Upgrade(_) => "upgrade",
Commands::Lint(_) => "lint",
Commands::CommandTree => "commands",
Commands::Config(_) => "config",
Commands::Telemetry(_) => "telemetry",
Commands::External(_) => "external",
}
.to_string();
Expand All @@ -174,7 +179,8 @@ fn main() {
Commands::Template(args) => commands::template::handle(args),
Commands::Upgrade(cmd) => commands::upgrade::handle(cmd),
Commands::Lint(args) => commands::lint::handle(args),
Commands::CommandTree => commands::command_tree::handle(),
Commands::Config(cmd) => commands::config::handle(cmd),
Commands::Telemetry(cmd) => commands::telemetry::handle(cmd),
Commands::External(args) => handle_external_plugin(args),
};
let duration = start.elapsed();
Expand Down
1 change: 0 additions & 1 deletion src/plugins/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ pub fn install_plugin(
}

let trust = classify_source(source);

let mut reg = load_registry().unwrap_or_default();
reg.plugins.retain(|p| p.name != name);
reg.plugins.push(InstalledPlugin {
Expand Down
9 changes: 6 additions & 3 deletions src/utils/notifications.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use colored::*;
#[allow(unused_imports)]
use std::process::Command;

pub fn info(message: &str) {
Expand All @@ -25,9 +26,11 @@ pub fn alert(message: &str) {
try_system_notification(message);
}

fn try_system_notification(message: &str) {
fn try_system_notification(_message: &str) {
#[allow(unused_variables)]
let msg = _message;
#[cfg(target_os = "macos")]
let escaped = message.replace('\\', "\\\\").replace('"', "\\\"");
let escaped = msg.replace('\\', "\\\\").replace('"', "\\\"");

#[cfg(target_os = "macos")]
{
Expand All @@ -41,7 +44,7 @@ fn try_system_notification(message: &str) {
#[cfg(target_os = "linux")]
{
let _ = Command::new("notify-send")
.args(["StarForge", message])
.args(["StarForge", msg])
.status();
}
}
11 changes: 9 additions & 2 deletions src/utils/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ pub struct TelemetryData {
}

pub fn track_event(event: &str, properties: serde_json::Value) -> Result<()> {
// Check environment variable first (opt out if false or 0)
if let Ok(val) = std::env::var("STARFORGE_TELEMETRY") {
if val == "false" || val == "0" {
return Ok(());
}
}

let cfg = config::load()?;

// Check if telemetry is enabled (default to true, but respect opt-out)
Expand All @@ -30,8 +37,8 @@ pub fn track_event(event: &str, properties: serde_json::Value) -> Result<()> {
anonymous_id,
};

// In a real app, we would send this to a service.
// For now, we'll log it to a local file in the data directory.
// Telemetry is saved ONLY locally in the data directory.
// Absolutely NO network requests are made for telemetry transmission.
save_telemetry_locally(&data)?;

Ok(())
Expand Down
4 changes: 4 additions & 0 deletions src/utils/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,10 @@ pub fn install_template_package(
version,
cli_version_min,
cli_version_max,
None,
None,
None,
None,
)
}

Expand Down
Loading