diff --git a/.github/ISSUE_TEMPLATE/3-cli.yml b/.github/ISSUE_TEMPLATE/3-cli.yml index 888c4ad069d..8cb4fb9cf63 100644 --- a/.github/ISSUE_TEMPLATE/3-cli.yml +++ b/.github/ISSUE_TEMPLATE/3-cli.yml @@ -10,13 +10,17 @@ body: If you need help or support using Code, and are not reporting a bug, please post on [code/discussions](https://github.com/just-every/code/discussions), where you can ask questions or engage with others on ideas for how to improve Code. - Run `code update-check` to make sure you are using the latest GitHub Release build. The bug you are experiencing may already have been fixed. + Run `code doctor` (or ` doctor` if your binary is + renamed) and include the output below. It shows the version, Every Code + Lab build tag, repository, executable path, and update source. - type: input id: version attributes: - label: What version of Code is running? - description: Copy the output of `code --version` (or `coder --version`) + label: What version and build of Code is running? + description: >- + Copy the version/build lines from `code doctor` (or ` + doctor`) - type: input id: model attributes: diff --git a/code-rs/cli/src/main.rs b/code-rs/cli/src/main.rs index 5260d49d4be..a808dd92082 100644 --- a/code-rs/cli/src/main.rs +++ b/code-rs/cli/src/main.rs @@ -1457,6 +1457,8 @@ async fn doctor_main() -> anyhow::Result<()> { .map(|p| p.display().to_string()) .unwrap_or_else(|_| "".to_string()); println!("code version: {}", code_version::version()); + println!("product: {}", code_version::LAB_BUILD_NAME); + println!("repository: {}", code_version::LAB_REPOSITORY); println!("current_exe: {}", exe); // PATH @@ -1523,7 +1525,7 @@ async fn doctor_main() -> anyhow::Result<()> { show_versions("coder --version by path", &coder_paths).await; println!("\nIf versions differ, remove older PATH entries or reorder PATH so the intended Code binary appears first."); - println!("Run `code update-check` from the intended binary to inspect the current GitHub Release update source."); + println!("Run `code update-check` or ` update-check` from the intended binary to inspect the current GitHub Release update source."); Ok(()) } diff --git a/code-rs/cli/src/update.rs b/code-rs/cli/src/update.rs index a1b8e0ab21e..ae291f81cc7 100644 --- a/code-rs/cli/src/update.rs +++ b/code-rs/cli/src/update.rs @@ -13,6 +13,7 @@ use sha2::Sha256; const DEFAULT_REPOSITORY: &str = "cbusillo/code"; const DEFAULT_CHANNEL: &str = "stable"; +const COMMAND_NAME_ENV: &str = "CODE_COMMAND_NAME"; #[derive(Debug, Parser)] pub struct UpdateCheckCommand { @@ -68,22 +69,26 @@ enum VersionOrdering { pub async fn run_update_check(args: UpdateCheckCommand) -> anyhow::Result<()> { let report = fetch_update_report(args.repo.as_deref(), args.tag.as_deref()).await?; - print_update_report(&report); + let identity = RuntimeIdentity::detect(None); + print_update_report(&report, &identity); Ok(()) } pub async fn run_update(args: UpdateCommand) -> anyhow::Result<()> { let report = fetch_update_report(args.repo.as_deref(), args.tag.as_deref()).await?; - print_update_report(&report); + let exe = env::current_exe().context("failed to resolve current executable")?; + let identity = RuntimeIdentity::detect(Some(&exe)); + print_update_report(&report, &identity); if report.ordering != VersionOrdering::Newer { println!("No update needed."); return Ok(()); } - let exe = env::current_exe().context("failed to resolve current executable")?; let install_target = resolve_install_target(&exe); let install = detect_install_source_for_path(&install_target); + println!("command: {}", identity.command_name); + println!("install target: {}", install_target.display()); println!("install source: {}", install.description()); if !install.can_self_update() { bail!( @@ -197,7 +202,10 @@ async fn latest_release_tag(repo: &str) -> anyhow::Result { Ok(release.tag_name) } -fn print_update_report(report: &UpdateReport) { +fn print_update_report(report: &UpdateReport, identity: &RuntimeIdentity) { + println!("product: {}", code_version::LAB_BUILD_NAME); + println!("repository: {}", code_version::LAB_REPOSITORY); + println!("command: {}", identity.command_name); println!("current version: {}", report.current_version); println!("latest version: {}", report.manifest.version); println!("channel: {}", report.manifest.channel); @@ -217,6 +225,38 @@ fn print_update_report(report: &UpdateReport) { } } +struct RuntimeIdentity { + command_name: String, +} + +impl RuntimeIdentity { + fn detect(exe: Option<&Path>) -> Self { + let command_name = env::var(COMMAND_NAME_ENV) + .ok() + .and_then(|name| valid_command_name(&name)) + .or_else(|| exe.and_then(command_name_from_path)) + .or_else(|| env::args_os().next().and_then(|arg| command_name_from_path(Path::new(&arg)))) + .unwrap_or_else(|| "code".to_string()); + + Self { command_name } + } +} + +fn command_name_from_path(path: &Path) -> Option { + path.file_name() + .and_then(|name| name.to_str()) + .and_then(valid_command_name) +} + +fn valid_command_name(name: &str) -> Option { + let trimmed = name.trim(); + if trimmed.is_empty() || trimmed.contains(std::path::MAIN_SEPARATOR) { + None + } else { + Some(trimmed.to_string()) + } +} + fn http_client(user_agent: &str) -> anyhow::Result { Ok(reqwest::Client::builder() .user_agent(user_agent) @@ -368,6 +408,7 @@ fn detect_install_source_for_path(exe: &Path) -> InstallSource { } if path.contains("/.code/bin/") || path.contains("/.local/bin/") + || path.contains("/usr/local/bin/") || path.contains("/code-rs/target/release/") { return InstallSource::Direct; @@ -408,6 +449,9 @@ fn parse_version_triplet(version: &str) -> Option<(u64, u64, u64)> { #[cfg(test)] mod tests { use super::*; + use std::sync::Mutex; + + static ENV_TEST_LOCK: Mutex<()> = Mutex::new(()); #[test] fn normalize_tag_adds_v_prefix_when_missing() { @@ -523,6 +567,62 @@ mod tests { detect_install_source_for_path(Path::new("/Users/me/.local/bin/code")), InstallSource::Direct )); + assert!(matches!( + detect_install_source_for_path(Path::new("/usr/local/bin/chris-code")), + InstallSource::Direct + )); + } + + #[test] + fn runtime_identity_prefers_command_name_env() { + let _lock = ENV_TEST_LOCK.lock().unwrap(); + let _reset = EnvReset::capture(COMMAND_NAME_ENV); + unsafe { + env::set_var(COMMAND_NAME_ENV, "chris-code"); + } + + let identity = RuntimeIdentity::detect(Some(Path::new("/usr/local/bin/code"))); + + assert_eq!(identity.command_name, "chris-code"); + } + + #[test] + fn runtime_identity_falls_back_to_exe_name() { + let _lock = ENV_TEST_LOCK.lock().unwrap(); + let _reset = EnvReset::capture(COMMAND_NAME_ENV); + unsafe { + env::remove_var(COMMAND_NAME_ENV); + } + + let identity = RuntimeIdentity::detect(Some(Path::new("/usr/local/bin/chris-code"))); + + assert_eq!(identity.command_name, "chris-code"); + } + + struct EnvReset { + key: &'static str, + value: Option, + } + + impl EnvReset { + fn capture(key: &'static str) -> Self { + Self { + key, + value: env::var(key).ok(), + } + } + } + + impl Drop for EnvReset { + fn drop(&mut self) { + unsafe { + if let Some(value) = &self.value { + env::set_var(self.key, value); + } else { + env::remove_var(self.key); + } + } + } } #[test] diff --git a/code-rs/code-version/src/lib.rs b/code-rs/code-version/src/lib.rs index 1509946cb60..cc41e789364 100644 --- a/code-rs/code-version/src/lib.rs +++ b/code-rs/code-version/src/lib.rs @@ -13,6 +13,10 @@ pub const CODE_VERSION: &str = { } }; +pub const PRODUCT_NAME: &str = "Every Code"; +pub const LAB_BUILD_NAME: &str = "Every Code Lab"; +pub const LAB_REPOSITORY: &str = "cbusillo/code"; + const ANNOUNCEMENT_TIP: &str = include_str!("../../../announcement_tip.toml"); const MODELS_MANIFEST: &str = include_str!("../../../codex-rs/models-manager/models.json"); pub const MIN_WIRE_COMPAT_VERSION_FALLBACK: &str = "0.101.0"; diff --git a/code-rs/tui/src/chatwidget.rs b/code-rs/tui/src/chatwidget.rs index d4b5f3af031..e3cfa8ea375 100644 --- a/code-rs/tui/src/chatwidget.rs +++ b/code-rs/tui/src/chatwidget.rs @@ -31095,7 +31095,7 @@ Have we met every part of this goal and is there no further work to do?"# // Title follows theme text color spans.push(Span::styled( - "Every Code", + code_version::LAB_BUILD_NAME, Style::default() .fg(crate::colors::text()) .add_modifier(Modifier::BOLD), diff --git a/code-rs/tui/src/updates.rs b/code-rs/tui/src/updates.rs index f97731b3f91..917724e433b 100644 --- a/code-rs/tui/src/updates.rs +++ b/code-rs/tui/src/updates.rs @@ -148,6 +148,7 @@ const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/cbusillo/code/rel const CURRENT_RELEASE_REPO: &str = "cbusillo/code"; const LEGACY_RELEASE_REPO: &str = "just-every/code"; pub const CODE_RELEASE_URL: &str = "https://github.com/cbusillo/code/releases/latest"; +const COMMAND_NAME_ENV: &str = "CODE_COMMAND_NAME"; const CACHE_TTL_HOURS: i64 = 20; const MAX_CLOCK_SKEW_MINUTES: i64 = 5; @@ -198,13 +199,14 @@ pub fn resolve_upgrade_resolution() -> UpgradeResolution { fn resolve_upgrade_resolution_for_exe(exe_path: &Path) -> UpgradeResolution { let install_target = resolve_install_target(exe_path); if detect_upgrade_install_source(&install_target) == UpgradeInstallSource::Direct { + let command_name = upgrade_command_name(exe_path, &install_target); return UpgradeResolution::Command { command: vec![ install_target.display().to_string(), "update".to_string(), "--yes".to_string(), ], - display: "code update --yes".to_string(), + display: format!("{command_name} update --yes"), }; } @@ -213,6 +215,30 @@ fn resolve_upgrade_resolution_for_exe(exe_path: &Path) -> UpgradeResolution { } } +fn upgrade_command_name(exe_path: &Path, install_target: &Path) -> String { + std::env::var(COMMAND_NAME_ENV) + .ok() + .and_then(|name| valid_command_name(&name)) + .or_else(|| command_name_from_path(exe_path)) + .or_else(|| command_name_from_path(install_target)) + .unwrap_or_else(|| "code".to_string()) +} + +fn command_name_from_path(path: &Path) -> Option { + path.file_name() + .and_then(|name| name.to_str()) + .and_then(valid_command_name) +} + +fn valid_command_name(name: &str) -> Option { + let trimmed = name.trim(); + if trimmed.is_empty() || trimmed.contains(std::path::MAIN_SEPARATOR) { + None + } else { + Some(trimmed.to_string()) + } +} + fn manual_upgrade_instructions() -> String { format!( "This install cannot be self-updated automatically. Download the latest release from {CODE_RELEASE_URL} and replace the installed binary." @@ -234,6 +260,7 @@ fn detect_upgrade_install_source(exe_path: &Path) -> UpgradeInstallSource { } if path.contains("/.code/bin/") || path.contains("/.local/bin/") + || path.contains("/usr/local/bin/") || path.contains("/code-rs/target/release/") { return UpgradeInstallSource::Direct; @@ -311,49 +338,12 @@ pub async fn auto_upgrade_if_enabled(config: &Config) -> anyhow::Result { - info!("auto-upgrade: sudo retry succeeded; installed {latest_version}"); - outcome.installed_version = Some(latest_version); - return Ok(outcome); - } - Ok(fallback) => { - if sudo_requires_manual_intervention(&fallback.stderr, fallback.status) - { - outcome.user_notice = Some(format!( - "Automatic upgrade needs your attention. Run `/update` to finish with `{}`.", - command_display - )); - } - warn!( - "auto-upgrade: sudo retry failed: status={:?} stderr={}", - fallback.status, - truncate_for_log(&fallback.stderr) - ); - return Ok(outcome); - } - Err(err) => { - warn!("auto-upgrade: sudo retry error: {err}"); - outcome.user_notice = Some(format!( - "Automatic upgrade could not escalate permissions. Run `/update` to finish with `{}`.", - command_display - )); - return Ok(outcome); - } - } - } - } - - #[cfg(not(any(target_os = "macos", target_os = "linux")))] - { - let _ = primary; // suppress unused warning on non-Unix targets + if upgrade_requires_manual_intervention(&primary.stderr, primary.status) { + outcome.user_notice = Some(format!( + "Automatic upgrade needs your attention. Run `/update` to finish with `{}`.", + command_display + )); } - warn!( "auto-upgrade: upgrade command failed: status={:?} stderr={}", primary.status, @@ -478,26 +468,7 @@ async fn execute_upgrade_command(command: &[String]) -> anyhow::Result Vec { - let mut out = Vec::with_capacity(command.len() + 3); - out.push("sudo".to_string()); - out.push("-n".to_string()); - out.push("--".to_string()); - out.extend(command.iter().cloned()); - out -} - -#[cfg(any(target_os = "macos", target_os = "linux"))] -fn starts_with_sudo(command: &[String]) -> bool { - command - .first() - .map(|c| c.eq_ignore_ascii_case("sudo")) - .unwrap_or(false) -} - -#[cfg(any(target_os = "macos", target_os = "linux"))] -fn sudo_requires_manual_intervention(stderr: &str, status: Option) -> bool { +fn upgrade_requires_manual_intervention(stderr: &str, status: Option) -> bool { let lowered = stderr.to_ascii_lowercase(); let needs_password = lowered.contains("password is required") || lowered.contains("a password is required") @@ -717,10 +688,13 @@ mod tests { use chrono::TimeZone; use std::fs; use std::sync::Arc; + use std::sync::Mutex; use tempfile::tempdir; use tokio::sync::Mutex as TokioMutex; use tokio::time::{sleep, Duration as TokioDuration}; + static ENV_TEST_LOCK: Mutex<()> = Mutex::new(()); + fn write_cache(path: &Path, info: &serde_json::Value) { fs::write(path, format!("{}\n", info)).expect("write version cache"); } @@ -728,13 +702,43 @@ mod tests { #[test] fn upgrade_resolution_uses_cli_updater_for_direct_installs() { match resolve_upgrade_resolution_for_exe(Path::new( - "/Users/me/.local/bin/code", + "/Users/me/.local/bin/chris-code", )) { UpgradeResolution::Command { command, display } => { assert!(command.len() >= 3); assert_eq!(command[command.len() - 2], "update"); assert_eq!(command[command.len() - 1], "--yes"); - assert_eq!(display, "code update --yes"); + assert_eq!(display, "chris-code update --yes"); + } + UpgradeResolution::Manual { instructions } => panic!("unexpected manual resolution: {instructions}"), + } + } + + #[test] + fn upgrade_resolution_allows_unmanaged_usr_local_bin() { + match resolve_upgrade_resolution_for_exe(Path::new( + "/usr/local/bin/chris-code", + )) { + UpgradeResolution::Command { display, .. } => { + assert_eq!(display, "chris-code update --yes"); + } + UpgradeResolution::Manual { instructions } => panic!("unexpected manual resolution: {instructions}"), + } + } + + #[test] + fn upgrade_resolution_prefers_command_name_env() { + let _lock = ENV_TEST_LOCK.lock().unwrap(); + let _reset = EnvReset::capture(COMMAND_NAME_ENV); + unsafe { + std::env::set_var(COMMAND_NAME_ENV, "chris-code"); + } + + match resolve_upgrade_resolution_for_exe(Path::new( + "/Users/me/.local/bin/code", + )) { + UpgradeResolution::Command { display, .. } => { + assert_eq!(display, "chris-code update --yes"); } UpgradeResolution::Manual { instructions } => panic!("unexpected manual resolution: {instructions}"), } @@ -755,6 +759,32 @@ mod tests { } } + struct EnvReset { + key: &'static str, + value: Option, + } + + impl EnvReset { + fn capture(key: &'static str) -> Self { + Self { + key, + value: std::env::var(key).ok(), + } + } + } + + impl Drop for EnvReset { + fn drop(&mut self) { + unsafe { + if let Some(value) = &self.value { + std::env::set_var(self.key, value); + } else { + std::env::remove_var(self.key); + } + } + } + } + #[test] fn read_version_info_discard_legacy_repo_cache() { let dir = tempdir().unwrap(); diff --git a/code-rs/tui/tests/vt100_chatwidget_snapshot.rs b/code-rs/tui/tests/vt100_chatwidget_snapshot.rs index b480de496f9..f0fb85d80ca 100644 --- a/code-rs/tui/tests/vt100_chatwidget_snapshot.rs +++ b/code-rs/tui/tests/vt100_chatwidget_snapshot.rs @@ -58,12 +58,17 @@ fn normalize_output(text: String) -> String { .collect::() .pipe(normalize_ellipsis) .pipe(normalize_timers) + .pipe(normalize_lab_build_name) .pipe(normalize_auto_drive_layout) .pipe(normalize_agent_history_details) .pipe(normalize_spacer_rows) .pipe(normalize_trailing_whitespace) } +fn normalize_lab_build_name(text: String) -> String { + text.replace(code_version::LAB_BUILD_NAME, code_version::PRODUCT_NAME) +} + fn init_tracing_once() { static INIT: Once = Once::new(); INIT.call_once(|| { diff --git a/codex-cli/bin/coder.js b/codex-cli/bin/coder.js index d61e51a40ba..de62445adea 100755 --- a/codex-cli/bin/coder.js +++ b/codex-cli/bin/coder.js @@ -396,10 +396,17 @@ const { spawn } = await import("child_process"); // Make the resolved native binary path visible to spawned agents/subprocesses. process.env.CODE_BINARY_PATH = binaryPath; +process.env.CODE_COMMAND_NAME = path.basename(process.argv[1] || "code"); const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit", - env: { ...process.env, CODER_MANAGED_BY_NPM: "1", CODEX_MANAGED_BY_NPM: "1", CODE_BINARY_PATH: binaryPath }, + env: { + ...process.env, + CODER_MANAGED_BY_NPM: "1", + CODEX_MANAGED_BY_NPM: "1", + CODE_BINARY_PATH: binaryPath, + CODE_COMMAND_NAME: process.env.CODE_COMMAND_NAME, + }, }); child.on("error", (err) => { diff --git a/docs/upstream-import-policy.md b/docs/upstream-import-policy.md index 86129e5cc44..5e95486c5b5 100644 --- a/docs/upstream-import-policy.md +++ b/docs/upstream-import-policy.md @@ -12,6 +12,9 @@ long-running feature branch overlay. - **Every Code** is the product name. Use it in prose, docs, UI copy, issue text, release text, and first mentions. +- **Every Code Lab** is the `cbusillo/code` runtime/build tag, similar to a beta + or lab channel label. Use it where users need to distinguish this build from + upstream `just-every/code`; do not treat it as a product rebrand. - **Every Code CLI** is the product CLI surface. **The `code` command** is the executable users type. Avoid “code CLI” in prose. - **The Every Code agent** is the assistant identity. **Code** is only a short