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
9 changes: 5 additions & 4 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ Installs the Steel CLI binary and, in an interactive terminal, runs

Flags:
--non-interactive Skip the interactive `steel init` step (install only)
--agent Run `steel init --agent` (print the onboarding guide
to stdout; intended for AI coding agents)
--agent Run `steel init --agent` (non-interactive: auto-accept
prompts and print agent-friendly output; intended for
AI coding agents)
--from <name> Tag the originating coding agent (claude-code, cursor,
opencode, codex, ...). Informational only; used for
onboarding telemetry. Supports `--from=<name>` too.
Expand Down Expand Up @@ -230,8 +231,8 @@ if [ -n "$_completion_shell" ]; then
fi
echo ""

# Agent mode just prints the onboarding guide to stdout, which needs no TTY,
# so it runs regardless of whether the surrounding shell is interactive.
# Agent mode auto-accepts prompts and needs no TTY, so it runs regardless of
# whether the surrounding shell is interactive.
# Human mode drives `dialoguer` prompts, so we only run it when /dev/tty
# exists.
if [ "$STEEL_AGENT_MODE" = "yes" ]; then
Expand Down
149 changes: 0 additions & 149 deletions src/commands/init/init_agent_guide.md

This file was deleted.

77 changes: 74 additions & 3 deletions src/commands/init/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::path::Path;

use clap::Parser;

use crate::commands::{doctor, login, skills};
use crate::config;
use crate::config::settings::{OnboardingConfig, read_config_from, write_config_to};
use crate::status;

#[derive(Parser)]
Expand All @@ -21,10 +25,11 @@ pub struct Args {

pub async fn run(args: Args) -> anyhow::Result<()> {
status!("Steel CLI setup");
if let Ok(from) = std::env::var("STEEL_ONBOARDING_FROM")
&& !from.is_empty()
if let Some(source) =
onboarding_source_from_env(std::env::var("STEEL_ONBOARDING_FROM").ok().as_deref())
{
status!("Onboarding source: {from}");
status!("Onboarding source: {source}");
record_onboarding_source(&source);
}
status!("");

Expand Down Expand Up @@ -91,3 +96,69 @@ async fn install_skills(args: &Args) -> anyhow::Result<()> {
fn is_all_selection(selected: &[String]) -> bool {
selected.len() == 1 && matches!(selected[0].as_str(), "__all__" | "all")
}

fn onboarding_source_from_env(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|source| !source.is_empty())
.map(str::to_string)
}

fn record_onboarding_source(source: &str) {
let config_path = config::config_path_in(&config::config_dir());
let _ = persist_onboarding_source(&config_path, source);
crate::telemetry::set_onboarding_source(source);
}

fn persist_onboarding_source(config_path: &Path, source: &str) -> anyhow::Result<()> {
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut cfg = read_config_from(config_path).unwrap_or_default();
cfg.onboarding = Some(OnboardingConfig {
source: Some(source.to_string()),
});
write_config_to(config_path, &cfg)?;
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;

#[test]
fn onboarding_source_from_env_trims_and_drops_blank() {
assert_eq!(
onboarding_source_from_env(Some(" claude-code ")).as_deref(),
Some("claude-code")
);
assert_eq!(onboarding_source_from_env(Some(" ")), None);
assert_eq!(onboarding_source_from_env(None), None);
}

#[test]
fn persist_onboarding_source_creates_config() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("nested").join("config.json");

persist_onboarding_source(&path, "cursor").unwrap();

let cfg = read_config_from(&path).unwrap();
assert_eq!(cfg.onboarding_source(), Some("cursor"));
}

#[test]
fn persist_onboarding_source_preserves_existing_fields() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("config.json");
std::fs::write(&path, r#"{"apiKey":"k","instance":"cloud"}"#).unwrap();

persist_onboarding_source(&path, "codex").unwrap();

let cfg = read_config_from(&path).unwrap();
assert_eq!(cfg.api_key.as_deref(), Some("k"));
assert_eq!(cfg.instance.as_deref(), Some("cloud"));
assert_eq!(cfg.onboarding_source(), Some("codex"));
}
}
2 changes: 1 addition & 1 deletion src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Global Flags:

Getting Started:
steel init Log in, verify, and install Steel skills into detected agents
--agent Print the agent onboarding guide to stdout and exit
--agent Auto-accept prompts and print agent-friendly output
steel skills list List available Steel Skills
steel skills install --all Install all Steel Skills through npx skills
steel skills install <name> Install a Steel Skill through npx skills
Expand Down
33 changes: 33 additions & 0 deletions src/config/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ pub struct Config {
pub telemetry: Option<TelemetryConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub computer: Option<ComputerConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub onboarding: Option<OnboardingConfig>,
}

#[derive(Debug, Serialize, Deserialize, Default, Clone)]
#[serde(rename_all = "camelCase")]
pub struct OnboardingConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Default, Clone)]
Expand Down Expand Up @@ -159,6 +168,14 @@ impl Config {
.and_then(|c| c.default_id.as_deref())
.filter(|s| !s.trim().is_empty())
}

pub fn onboarding_source(&self) -> Option<&str> {
self.onboarding
.as_ref()
.and_then(|o| o.source.as_deref())
.map(str::trim)
.filter(|s| !s.is_empty())
}
}

pub fn read_config_from(path: &Path) -> Result<Config> {
Expand Down Expand Up @@ -217,6 +234,9 @@ mod tests {
disabled: Some(true),
}),
computer: None,
onboarding: Some(OnboardingConfig {
source: Some("claude-code".into()),
}),
};

write_config_to(&path, &config).unwrap();
Expand All @@ -227,6 +247,19 @@ mod tests {
assert_eq!(loaded.instance.as_deref(), Some("cloud"));
assert_eq!(loaded.local_api_url(), Some("http://localhost:4000/v1"));
assert!(loaded.telemetry_disabled());
assert_eq!(loaded.onboarding_source(), Some("claude-code"));
}

#[test]
fn onboarding_source_ignores_blank_values() {
let config = Config {
onboarding: Some(OnboardingConfig {
source: Some(" ".into()),
}),
..Default::default()
};
assert_eq!(config.onboarding_source(), None);
assert_eq!(Config::default().onboarding_source(), None);
}

#[test]
Expand Down
Loading
Loading