From e5a18866d13b766e0fb51623731b42a2b2c8ae59 Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Tue, 16 Jun 2026 16:29:28 -0500 Subject: [PATCH 1/5] docs(docs): add commit format and PR conventions (MVP-0.1) Establish the standard used going forward: one type per commit (feat/bug/docs/tests/gen), scope, and an epic reference in the title. Isolate generated/mechanical churn (formatting, lock files, codegen) into dedicated gen commits, and keep each commit under ~600 lines so reviewers can skim the noise and read the real work top-to-bottom. Co-Authored-By: Claude Opus 4.8 --- CONTRIBUTING.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5206257 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing + +This document defines how we structure commits and pull requests in this repository. +For architecture, plugin conventions, and platform-specific guidance see `CLAUDE.md`, +`plugins/CONVENTIONS.md`, and the `wiki/` docs. + +## Commit format + +``` +(): () +``` + +Every commit declares exactly one ``, names the `` it touches, and references the +`` (or task) it belongs to. One commit is one kind of change — never mix types. + +Example: + +``` +feat(segkit): add leveled logger with color and file output (MVP-0.1) +``` + +### Type — exactly one per commit + +| Type | Use for | +| ------- | ----------------------------------------------------------------------------- | +| `feat` | New behavior or capability. | +| `bug` | A fix for incorrect behavior. | +| `docs` | Documentation only (Markdown, REFERENCE files, comments, task lists). | +| `tests` | Test code only (new tests, fixtures, harness wiring) with no behavior change. | +| `gen` | Generated or mechanical churn — see below. | + +These map onto the conventional-commit types referenced in `CLAUDE.md`: `bug` is `fix`, +`tests` is `test`, and `gen` is new. We do not use `refactor`/`perf`/`chore` as top-level +types — a refactor that changes no behavior is `feat` or `bug` depending on intent, and +mechanical churn is `gen`. + +### `gen` — isolate the noise + +Generated and mechanical changes go in their own `gen` commits, separated from hand-written +work, so reviewers can skim them with confidence instead of auditing them line by line. This +includes: + +- Formatting-only changes (`treefmt`, `cargo fmt`, `gofmt`, prettier). +- Lock files (`Cargo.lock`, `package-lock.json`, `Podfile.lock`, `devbox.lock`, `devices.lock`). +- Scaffolder/codegen output and other regenerated artifacts. + +A `gen` commit's summary must say plainly what produced it, e.g. +`gen(segkit): cargo fmt (MVP-0.1)` or `gen(rn): regenerate package-lock after dep bump`. +Never fold a `gen` change into a `feat`/`bug` commit — if a code change forces a reformat or a +lock-file update, split the mechanical part into a trailing `gen` commit. + +### Scope + +The area of the repo the commit touches: `segkit`, `android`, `ios`, `react-native` (or `rn`), +`ci`, `docs`, `tests`, `examples`. Use the narrowest scope that fits. + +### Epic reference + +Reference the epic or task somewhere in the title, in parentheses at the end: +`(MVP-0.1)`, `(M1.2)`, `(T3)`. This ties the commit back to `notes/segkit-milestones/`. For +work with no epic, use the issue or PR reference (`(#123)`) or omit the suffix for one-off +fixes. + +## Pull requests + +- **Keep each commit reviewable: under ~600 lines of diff.** Split larger work into focused + commits along natural seams (module, layer, feature). Big mechanical diffs (lock files, + generated output) don't count against the budget when isolated in their own `gen` commit — + that is the point of isolating them. +- Order commits so the PR reads top-to-bottom: foundational changes first, then the work that + builds on them, with `gen` and `docs` commits grouped rather than scattered. +- A PR should land one epic or one self-contained slice of one. Don't bundle unrelated epics. + +## Attribution + +Commits created with AI assistance end with a trailer: + +``` +Co-Authored-By: Claude Opus 4.8 +``` From 9f01c86630c4fa511ef8531f01d1fcd92ea9a6ad Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Tue, 16 Jun 2026 17:03:27 -0500 Subject: [PATCH 2/5] feat(segkit): add leveled logger with color and file output (MVP-0.1) Replace the three-function ANSI logger with debug/info/warn/error levels mirroring the shell side's "[LEVEL] [segkit] message" prefix so combined CI logs read uniformly. - debug is gated on SEGKIT_DEBUG=1 or DEBUG=1, matching the shell flags. - logs are diagnostics, so they go to stderr (color-coded per level when stderr is a terminal), leaving stdout clean for program data; color is suppressed when piped/redirected or when NO_COLOR is set. - every line is also appended, uncolored by construction, to ${REPORTS_DIR:-reports}/segkit.log for CI to upload as a historical artifact (reuses the REPORTS_DIR root delegate.rs already writes to). - info/warn/err keep their names as thin wrappers so callers don't churn. - hand-rolled, zero new dependencies. - wire the first debug caller into delegate::run so the new level is exercised on the delegation hot path. Inline unit tests cover the prefix format, the uncolored-file invariant, tag-only coloring, distinct per-level colors, and SEGKIT_DEBUG/DEBUG gating. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 1 + segkit/src/delegate.rs | 4 + segkit/src/util/log.rs | 182 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 184 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 3893b23..0e79c46 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ examples/*/reports/ examples/*/test-results/ plugins/*/tests/reports/ plugins/*/tests/test-results/ +segkit/reports/ notes/ # AI-generated artifacts (don't commit these) diff --git a/segkit/src/delegate.rs b/segkit/src/delegate.rs index 083f630..b9917d2 100644 --- a/segkit/src/delegate.rs +++ b/segkit/src/delegate.rs @@ -5,6 +5,8 @@ use std::time::Instant; use anyhow::{Context, Result}; use chrono::Utc; +use crate::util::log; + fn reports_dir() -> String { std::env::var("REPORTS_DIR").unwrap_or_else(|_| "reports".into()) } @@ -89,6 +91,8 @@ pub fn run(script: &str, args: &[String]) -> ExitCode { } }; + log::debug(&format!("delegating to {script_path} {}", args.join(" "))); + let start = Instant::now(); let status = Command::new(&script_path).args(args).status(); diff --git a/segkit/src/util/log.rs b/segkit/src/util/log.rs index 3c9ea5d..07a220c 100644 --- a/segkit/src/util/log.rs +++ b/segkit/src/util/log.rs @@ -1,11 +1,187 @@ +//! Leveled logger mirroring the shell side's `[LEVEL] [script] message` contract. +//! +//! Logs are diagnostics, so they go to stderr (color-coded by level when stderr +//! is a terminal), leaving stdout for the program's actual data. Every line is +//! also appended, uncolored, to `${REPORTS_DIR:-reports}/segkit.log` so CI can +//! upload a historical log as an artifact. `debug` is emitted only when +//! `SEGKIT_DEBUG=1` or `DEBUG=1`, matching the shell `*_DEBUG` flags. + +use std::fs::OpenOptions; +use std::io::{IsTerminal, Write}; + +const TAG: &str = "segkit"; + +#[derive(Clone, Copy)] +enum Level { + Debug, + Info, + Warn, + Error, +} + +impl Level { + fn label(self) -> &'static str { + match self { + Level::Debug => "DEBUG", + Level::Info => "INFO", + Level::Warn => "WARN", + Level::Error => "ERROR", + } + } + + /// ANSI color for the `[LEVEL]` tag: debug=gray, info=blue, warn=yellow, error=red. + fn color(self) -> &'static str { + match self { + Level::Debug => "\x1b[1;90m", + Level::Info => "\x1b[1;34m", + Level::Warn => "\x1b[1;33m", + Level::Error => "\x1b[1;31m", + } + } +} + +const RESET: &str = "\x1b[0m"; + +/// Uncolored log line, e.g. `[INFO] [segkit] message`. Written to the log file. +fn plain_line(level: Level, msg: &str) -> String { + format!("[{}] [{}] {}", level.label(), TAG, msg) +} + +/// Colored log line for a terminal: only the `[LEVEL]` tag is wrapped in ANSI. +fn colored_line(level: Level, msg: &str) -> String { + format!( + "{}[{}]{} [{}] {}", + level.color(), + level.label(), + RESET, + TAG, + msg + ) +} + +/// Whether `debug` output is enabled, given an env lookup. Mirrors the shell +/// contract: on when `SEGKIT_DEBUG=1` or `DEBUG=1`. +fn debug_enabled_from(get: impl Fn(&str) -> Option) -> bool { + get("SEGKIT_DEBUG").as_deref() == Some("1") || get("DEBUG").as_deref() == Some("1") +} + +fn debug_enabled() -> bool { + debug_enabled_from(|k| std::env::var(k).ok()) +} + +/// Append a plain (uncolored) line to `${REPORTS_DIR:-reports}/segkit.log`. +/// Best-effort: logging must never abort the program, so failures are swallowed. +fn append_to_file(plain: &str) { + let dir = std::env::var("REPORTS_DIR").unwrap_or_else(|_| "reports".into()); + if std::fs::create_dir_all(&dir).is_err() { + return; + } + let path = format!("{dir}/segkit.log"); + if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(&path) { + let _ = writeln!(f, "{plain}"); + } +} + +fn emit(level: Level, msg: &str) { + let plain = plain_line(level, msg); + append_to_file(&plain); + + let mut out = std::io::stderr(); + if out.is_terminal() && std::env::var_os("NO_COLOR").is_none() { + let _ = writeln!(out, "{}", colored_line(level, msg)); + } else { + let _ = writeln!(out, "{plain}"); + } +} + +pub fn debug(msg: &str) { + if debug_enabled() { + emit(Level::Debug, msg); + } +} + pub fn info(msg: &str) { - eprintln!("\x1b[1;34m==> {}\x1b[0m", msg); + emit(Level::Info, msg); } pub fn warn(msg: &str) { - eprintln!("\x1b[1;33m==> {}\x1b[0m", msg); + emit(Level::Warn, msg); } pub fn err(msg: &str) { - eprintln!("\x1b[1;31m==> {}\x1b[0m", msg); + emit(Level::Error, msg); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn plain_line_matches_shell_prefix_format() { + assert_eq!(plain_line(Level::Info, "hello"), "[INFO] [segkit] hello"); + assert_eq!( + plain_line(Level::Warn, "careful"), + "[WARN] [segkit] careful" + ); + assert_eq!(plain_line(Level::Error, "boom"), "[ERROR] [segkit] boom"); + assert_eq!(plain_line(Level::Debug, "trace"), "[DEBUG] [segkit] trace"); + } + + #[test] + fn plain_line_has_no_ansi_codes() { + // The file sink must never receive color codes. + let line = plain_line(Level::Error, "no color here"); + assert!( + !line.contains('\x1b'), + "plain line leaked an ANSI escape: {line:?}" + ); + } + + #[test] + fn colored_line_wraps_only_the_tag() { + let line = colored_line(Level::Info, "msg"); + // Colored tag, then a reset, then the uncolored remainder. + assert_eq!(line, "\x1b[1;34m[INFO]\x1b[0m [segkit] msg"); + assert!(line.starts_with(Level::Info.color())); + assert!(line.contains(RESET)); + // Message text itself is not colored. + assert!(line.ends_with("[segkit] msg")); + } + + #[test] + fn each_level_has_a_distinct_color_and_label() { + let levels = [Level::Debug, Level::Info, Level::Warn, Level::Error]; + let labels: Vec<_> = levels.iter().map(|l| l.label()).collect(); + assert_eq!(labels, ["DEBUG", "INFO", "WARN", "ERROR"]); + let colors: std::collections::BTreeSet<_> = levels.iter().map(|l| l.color()).collect(); + assert_eq!(colors.len(), 4, "level colors must be distinct"); + } + + #[test] + fn debug_enabled_when_segkit_debug_is_one() { + let env = |k: &str| (k == "SEGKIT_DEBUG").then(|| "1".to_string()); + assert!(debug_enabled_from(env)); + } + + #[test] + fn debug_enabled_when_global_debug_is_one() { + let env = |k: &str| (k == "DEBUG").then(|| "1".to_string()); + assert!(debug_enabled_from(env)); + } + + #[test] + fn debug_disabled_when_unset() { + let env = |_: &str| None; + assert!(!debug_enabled_from(env)); + } + + #[test] + fn debug_disabled_when_not_exactly_one() { + let env = |k: &str| match k { + "SEGKIT_DEBUG" => Some("0".to_string()), + "DEBUG" => Some("true".to_string()), + _ => None, + }; + assert!(!debug_enabled_from(env)); + } } From 75666160a264aa8fede3597ea7f3f5e1c3dcedee Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Tue, 16 Jun 2026 17:03:27 -0500 Subject: [PATCH 3/5] docs(segkit): document logging levels, color, and reports artifact (MVP-0.1) Add segkit/README.md covering the four log levels, the SEGKIT_DEBUG/DEBUG gate, terminal color behavior (NO_COLOR-aware), the uncolored reports/segkit.log historical artifact that CI uploads, and the stderr-for-diagnostics / stdout-for-data split (including subprocess stream pass-through). Co-Authored-By: Claude Opus 4.8 --- segkit/README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 segkit/README.md diff --git a/segkit/README.md b/segkit/README.md new file mode 100644 index 0000000..736b5b6 --- /dev/null +++ b/segkit/README.md @@ -0,0 +1,47 @@ +# segkit + +Segment SDK developer toolkit. A Rust CLI that wraps the Devbox plugin scripts +(`android.sh`, `ios.sh`, `rn.sh`, `metro.sh`) and provides project scaffolding, +configuration, and environment management. + +Build and test through Devbox from the repo root: + +```bash +devbox run segkit:build # cargo build +devbox run segkit:test # cargo test +devbox run segkit:check # fmt check + clippy + test +``` + +## Logging + +segkit logs at four levels — `debug`, `info`, `warn`, `error` — using the same +`[LEVEL] [segkit] message` prefix as the shell plugins, so interleaved CI output +reads uniformly across Rust and shell. + +`debug` is suppressed unless `SEGKIT_DEBUG=1` (or the global `DEBUG=1`), mirroring +the shell `*_DEBUG` flags: + +```bash +SEGKIT_DEBUG=1 segkit rn doctor # shows [DEBUG] delegation traces +``` + +### Output sinks + +Logs are diagnostics, not program output, so they go to stderr — leaving stdout +clean for data you might pipe or capture (e.g. `config show`). Every log line is +written to two places: + +- **stderr**, with the `[LEVEL]` tag color-coded by level (gray/blue/yellow/red) + when stderr is a terminal. Color is suppressed automatically when output is + piped or redirected, or when [`NO_COLOR`](https://no-color.org/) is set. +- **`${REPORTS_DIR:-reports}/segkit.log`**, appended across runs and never + colored, so it accumulates a historical log that CI can upload as a job + artifact. `REPORTS_DIR` is the same reports root the delegation timing/error + logs use. + +The file sink is plain text by construction — ANSI color codes only ever reach a +terminal, never the log file. + +Subprocesses segkit spawns (emulator, simulator, the SDK example, process-compose) +inherit segkit's streams: their stdout and stderr pass through unchanged, so their +own diagnostics stay on stderr and their data stays on stdout. From 5fbc89790328853a8f3439bbd741962ad03f1bb0 Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Tue, 16 Jun 2026 17:03:41 -0500 Subject: [PATCH 4/5] gen(segkit): cargo fmt pre-existing files (MVP-0.1) Mechanical `cargo fmt` reflow of files surfaced by the current pinned rustfmt (1.94) but untouched by this epic. Isolated here so the formatting churn is skimmable and segkit:fmt:check passes on this branch. No behavior change. Co-Authored-By: Claude Opus 4.8 --- segkit/src/config_cmd.rs | 20 +++++++-- segkit/src/init_cmd.rs | 93 ++++++++++++++++++++++++++++++++-------- segkit/src/main.rs | 19 +++++--- 3 files changed, 105 insertions(+), 27 deletions(-) diff --git a/segkit/src/config_cmd.rs b/segkit/src/config_cmd.rs index 458011d..d294945 100644 --- a/segkit/src/config_cmd.rs +++ b/segkit/src/config_cmd.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::PathBuf; use std::process::ExitCode; -use crate::init_cmd::{validate_plugin_names, PLUGIN_REGISTRY}; +use crate::init_cmd::{PLUGIN_REGISTRY, validate_plugin_names}; use crate::util::log::{err, info}; use crate::util::project::find_file; use crate::util::xcconfig::XCConfig; @@ -50,7 +50,9 @@ fn apply_plugin_changes( // Start from --plugins (replace) or current value let mut current = if let Some(list) = plugins { - list.iter().map(|s| s.to_lowercase()).collect::>() + list.iter() + .map(|s| s.to_lowercase()) + .collect::>() } else { let raw = config.get("ENABLED_PLUGINS").unwrap_or_default(); parse_plugin_csv(&raw) @@ -77,7 +79,14 @@ pub fn run_show() -> ExitCode { eprintln!("Config: {}", config_path.display()); eprintln!(); - eprintln!(" Write Key: {}", if write_key.is_empty() { "(not set)" } else { &write_key }); + eprintln!( + " Write Key: {}", + if write_key.is_empty() { + "(not set)" + } else { + &write_key + } + ); eprintln!(); eprintln!(" Plugins:"); for p in PLUGIN_REGISTRY { @@ -113,7 +122,10 @@ pub fn run_set( } }; config.set("ENABLED_PLUGINS", &csv); - info(&format!("Enabled plugins: {}", if csv.is_empty() { "(none)" } else { &csv })); + info(&format!( + "Enabled plugins: {}", + if csv.is_empty() { "(none)" } else { &csv } + )); } if let Err(e) = fs::write(&config_path, config.to_string()) { diff --git a/segkit/src/init_cmd.rs b/segkit/src/init_cmd.rs index 824f886..1c7c4bf 100644 --- a/segkit/src/init_cmd.rs +++ b/segkit/src/init_cmd.rs @@ -192,7 +192,9 @@ fn generate_deps_yaml(plugins: &[&Plugin]) -> String { /// Generate the project.yml with dynamic plugin support. fn generate_project_yml(name: &str, org: &str, plugins: &[&Plugin]) -> String { let mut packages_section = String::new(); - packages_section.push_str(" Segment:\n url: https://github.com/segmentio/analytics-swift\n from: 1.9.3\n"); + packages_section.push_str( + " Segment:\n url: https://github.com/segmentio/analytics-swift\n from: 1.9.3\n", + ); packages_section.push_str(&generate_packages_yaml(plugins)); let mut deps_section = String::from(" - package: Segment\n"); @@ -618,7 +620,11 @@ fn prompt(label: &str, default: &str) -> String { return default.to_string(); } let trimmed = line.trim(); - if trimmed.is_empty() { default.to_string() } else { trimmed.to_string() } + if trimmed.is_empty() { + default.to_string() + } else { + trimmed.to_string() + } } /// Prompt user to select plugins interactively (toggle with numbers, Enter to confirm). @@ -715,7 +721,8 @@ pub fn run( } let org = org.unwrap_or_else(|| prompt("Organization identifier", "com.example")); - let write_key = write_key.unwrap_or_else(|| prompt("Segment write key", "demo_write_key_not_real")); + let write_key = + write_key.unwrap_or_else(|| prompt("Segment write key", "demo_write_key_not_real")); let plugin_names = if needs_wizard && plugin_names.is_empty() { prompt_plugins(&plugin_names) @@ -756,10 +763,18 @@ pub fn run( // project.yml — always includes all 7 plugins as SPM dependencies let all_plugins: Vec<&Plugin> = PLUGIN_REGISTRY.iter().collect(); - write_file(&out, "project.yml", &generate_project_yml(&name, &org, &all_plugins)); + write_file( + &out, + "project.yml", + &generate_project_yml(&name, &org, &all_plugins), + ); // devbox.json - write_file(&out, "devbox.json", &apply(DEVBOX_JSON, &name, &org, &write_key, &bundle_id)); + write_file( + &out, + "devbox.json", + &apply(DEVBOX_JSON, &name, &org, &write_key, &bundle_id), + ); // Device definitions write_file(&out, "devbox.d/ios/devices/max.json", DEVICE_MAX_JSON); @@ -770,23 +785,67 @@ pub fn run( // SegmentConfig.conf — runtime config read by Config.swift // Uses .conf extension so Xcode bundles it as a resource (not a build config) let enabled_keys: Vec = plugins.iter().map(|p| p.key.to_string()).collect(); - write_file(&out, &format!("{src}/SegmentConfig.conf"), &generate_xcconfig(&write_key, &enabled_keys)); - - write_file(&out, &format!("{src}/Config.swift"), &apply(CONFIG_SWIFT, &name, &org, &write_key, &bundle_id)); - write_file(&out, &format!("{src}/{name}App.swift"), &apply(APP_SWIFT, &name, &org, &write_key, &bundle_id)); - write_file(&out, &format!("{src}/ContentView.swift"), &generate_content_view(&name)); - write_file(&out, &format!("{src}/ConsoleLoggerPlugin.swift"), CONSOLE_LOGGER_SWIFT); + write_file( + &out, + &format!("{src}/SegmentConfig.conf"), + &generate_xcconfig(&write_key, &enabled_keys), + ); + + write_file( + &out, + &format!("{src}/Config.swift"), + &apply(CONFIG_SWIFT, &name, &org, &write_key, &bundle_id), + ); + write_file( + &out, + &format!("{src}/{name}App.swift"), + &apply(APP_SWIFT, &name, &org, &write_key, &bundle_id), + ); + write_file( + &out, + &format!("{src}/ContentView.swift"), + &generate_content_view(&name), + ); + write_file( + &out, + &format!("{src}/ConsoleLoggerPlugin.swift"), + CONSOLE_LOGGER_SWIFT, + ); write_file(&out, &format!("{src}/IDFAPlugin.swift"), IDFA_PLUGIN_SWIFT); // Asset catalogs - write_file(&out, &format!("{src}/Assets.xcassets/Contents.json"), ASSETS_CONTENTS); - write_file(&out, &format!("{src}/Assets.xcassets/AccentColor.colorset/Contents.json"), ACCENT_COLOR_CONTENTS); - write_file(&out, &format!("{src}/Assets.xcassets/AppIcon.appiconset/Contents.json"), APP_ICON_CONTENTS); + write_file( + &out, + &format!("{src}/Assets.xcassets/Contents.json"), + ASSETS_CONTENTS, + ); + write_file( + &out, + &format!("{src}/Assets.xcassets/AccentColor.colorset/Contents.json"), + ACCENT_COLOR_CONTENTS, + ); + write_file( + &out, + &format!("{src}/Assets.xcassets/AppIcon.appiconset/Contents.json"), + APP_ICON_CONTENTS, + ); // Test files - write_file(&out, &format!("{name}Tests/{name}Tests.swift"), &apply(TESTS_SWIFT, &name, &org, &write_key, &bundle_id)); - write_file(&out, &format!("{name}UITests/{name}UITests.swift"), &apply(UI_TESTS_SWIFT, &name, &org, &write_key, &bundle_id)); - write_file(&out, &format!("{name}UITests/{name}UITestsLaunchTests.swift"), &apply(UI_TESTS_LAUNCH_SWIFT, &name, &org, &write_key, &bundle_id)); + write_file( + &out, + &format!("{name}Tests/{name}Tests.swift"), + &apply(TESTS_SWIFT, &name, &org, &write_key, &bundle_id), + ); + write_file( + &out, + &format!("{name}UITests/{name}UITests.swift"), + &apply(UI_TESTS_SWIFT, &name, &org, &write_key, &bundle_id), + ); + write_file( + &out, + &format!("{name}UITests/{name}UITestsLaunchTests.swift"), + &apply(UI_TESTS_LAUNCH_SWIFT, &name, &org, &write_key, &bundle_id), + ); // .gitignore write_file(&out, ".gitignore", GITIGNORE); diff --git a/segkit/src/main.rs b/segkit/src/main.rs index 7d097a2..d2ebf5f 100644 --- a/segkit/src/main.rs +++ b/segkit/src/main.rs @@ -113,15 +113,22 @@ fn main() -> ExitCode { Some(Commands::Metro { args }) => delegate::run("metro.sh", &args), Some(Commands::Doctor { fix }) => doctor::run(fix), Some(Commands::Uninstall { all, keep }) => uninstall::run(all, &keep), - Some(Commands::Init { sdk, name, org, write_key, plugins }) => { - init_cmd::run(sdk, name, org, write_key, plugins) - } + Some(Commands::Init { + sdk, + name, + org, + write_key, + plugins, + }) => init_cmd::run(sdk, name, org, write_key, plugins), Some(Commands::Update) => update::run(), Some(Commands::Config { action }) => match action { ConfigAction::Show => config_cmd::run_show(), - ConfigAction::Set { write_key, plugins, add_plugins, remove_plugins } => { - config_cmd::run_set(write_key, plugins, add_plugins, remove_plugins) - } + ConfigAction::Set { + write_key, + plugins, + add_plugins, + remove_plugins, + } => config_cmd::run_set(write_key, plugins, add_plugins, remove_plugins), }, None => { println!("segkit {}", env!("CARGO_PKG_VERSION")); From a083b014636d73d711a7b172698cfcb63b6994a0 Mon Sep 17 00:00:00 2001 From: Andrea Bueide Date: Tue, 16 Jun 2026 17:05:35 -0500 Subject: [PATCH 5/5] bug(segkit): fix clippy lints surfaced by pinned toolchain (MVP-0.1) The current pinned clippy (1.94) flags pre-existing code this epic otherwise leaves alone; segkit:clippy runs with -D warnings, so CI is red until they're cleared. Fixes, no behavior change: - collapse nested if / if-let into let-chains (collapsible_if) in init_cmd, uninstall, and xcconfig. - replace XCConfig's inherent to_string with a Display impl (inherent_to_string); the single caller's .to_string() still works via the blanket ToString impl. Co-Authored-By: Claude Opus 4.8 --- segkit/src/init_cmd.rs | 9 +++++---- segkit/src/uninstall.rs | 9 +++++---- segkit/src/util/xcconfig.rs | 24 +++++++++++++----------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/segkit/src/init_cmd.rs b/segkit/src/init_cmd.rs index 1c7c4bf..aecfbce 100644 --- a/segkit/src/init_cmd.rs +++ b/segkit/src/init_cmd.rs @@ -665,10 +665,11 @@ fn prompt_plugins(already_selected: &[String]) -> Vec { "all" => selected.iter_mut().for_each(|s| *s = true), "none" => selected.iter_mut().for_each(|s| *s = false), _ => { - if let Ok(n) = token.parse::() { - if n >= 1 && n <= PLUGIN_REGISTRY.len() { - selected[n - 1] = !selected[n - 1]; - } + if let Ok(n) = token.parse::() + && n >= 1 + && n <= PLUGIN_REGISTRY.len() + { + selected[n - 1] = !selected[n - 1]; } } } diff --git a/segkit/src/uninstall.rs b/segkit/src/uninstall.rs index 1d49060..a5f0697 100644 --- a/segkit/src/uninstall.rs +++ b/segkit/src/uninstall.rs @@ -244,10 +244,11 @@ fn prompt_selection(installed: &[&DoctorDep]) -> Vec { let mut selected = Vec::new(); for part in input.split([',', ' ']) { let part = part.trim(); - if let Ok(n) = part.parse::() { - if n >= 1 && n <= installed.len() { - selected.push(installed[n - 1].name.to_string()); - } + if let Ok(n) = part.parse::() + && n >= 1 + && n <= installed.len() + { + selected.push(installed[n - 1].name.to_string()); } } selected diff --git a/segkit/src/util/xcconfig.rs b/segkit/src/util/xcconfig.rs index de363ea..b13168f 100644 --- a/segkit/src/util/xcconfig.rs +++ b/segkit/src/util/xcconfig.rs @@ -18,10 +18,10 @@ impl XCConfig { if trimmed.starts_with("//") || trimmed.is_empty() { continue; } - if let Some((k, v)) = trimmed.split_once('=') { - if k.trim() == key { - return Some(v.trim().to_string()); - } + if let Some((k, v)) = trimmed.split_once('=') + && k.trim() == key + { + return Some(v.trim().to_string()); } } None @@ -33,22 +33,24 @@ impl XCConfig { if trimmed.starts_with("//") || trimmed.is_empty() { continue; } - if let Some((k, _)) = trimmed.split_once('=') { - if k.trim() == key { - *line = format!("{} = {}", key, value); - return; - } + if let Some((k, _)) = trimmed.split_once('=') + && k.trim() == key + { + *line = format!("{} = {}", key, value); + return; } } // Key not found — append it self.lines.push(format!("{} = {}", key, value)); } +} - pub fn to_string(&self) -> String { +impl std::fmt::Display for XCConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut out = self.lines.join("\n"); if !out.ends_with('\n') { out.push('\n'); } - out + f.write_str(&out) } }