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
6 changes: 6 additions & 0 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ pub(crate) struct Args {
#[arg(long, global = true, default_value = "true")]
pub(crate) auto_update: bool,

/// Disable all startup network operations (update check, install/update telemetry,
/// provider model-list refresh). Provider API calls during the session itself are
/// not affected. Equivalent to setting `JCODE_OFFLINE=1`.
#[arg(long, global = true)]
pub(crate) offline: bool,

/// Replace the built-in system prompt with the given text for this session.
/// Higher priority than `.jcode/SYSTEM.md` and the `provider.system_prompt`
/// config value. Equivalent to setting `JCODE_SYSTEM_PROMPT`.
Expand Down
16 changes: 16 additions & 0 deletions src/cli/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ fn parse_and_prepare_args() -> Result<Args> {
crate::env::set_var("JCODE_TRACE", "1");
}

// Translate --offline to the in-process JCODE_OFFLINE flag so deep code
// paths can read it without threading an extra argument. See issue #24.
// Honor a pre-existing env value too (the env var is the documented way
// to enable offline mode for wrapper scripts).
if args.offline || std::env::var("JCODE_OFFLINE").is_ok() {
crate::env::set_var("JCODE_OFFLINE", "1");
if !args.quiet {
output::stderr_info(
"Offline mode: startup network operations disabled (JCODE_OFFLINE=1).",
);
}
}

// --system-prompt / --append-system-prompt: translate to env vars so the
// build_system_prompt helpers (which run on demand from many code paths)
// can pick them up without threading args through every layer. Issue #22.
Expand Down Expand Up @@ -139,6 +152,9 @@ fn spawn_background_update_check(args: &Args) {
}

fn should_spawn_background_update_check(args: &Args) -> bool {
if std::env::var("JCODE_OFFLINE").is_ok() {
return false;
}
!args.quiet
&& !args.no_update
&& !matches!(
Expand Down
7 changes: 7 additions & 0 deletions src/provider/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,10 @@ fn anthropic_model_catalog_refresh_throttled(scope: &str) -> bool {
}

pub fn should_refresh_openai_model_catalog() -> bool {
// Offline mode disables every startup network operation (issue #24).
if std::env::var("JCODE_OFFLINE").is_ok() {
return false;
}
if account_model_cache_is_fresh() {
return false;
}
Expand All @@ -702,6 +706,9 @@ pub fn should_refresh_openai_model_catalog() -> bool {
}

pub fn should_refresh_anthropic_model_catalog() -> bool {
if std::env::var("JCODE_OFFLINE").is_ok() {
return false;
}
let scope = current_anthropic_catalog_scope();
if anthropic_model_cache_is_fresh(&scope) {
return false;
Expand Down
4 changes: 4 additions & 0 deletions src/provider/openrouter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,10 @@ impl OpenRouterProvider {
}

fn begin_background_model_catalog_refresh(&self) -> bool {
// Offline mode disables every startup network operation (issue #24).
if std::env::var("JCODE_OFFLINE").is_ok() {
return false;
}
let Some(now) = current_unix_secs() else {
return false;
};
Expand Down
4 changes: 4 additions & 0 deletions src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ pub fn is_enabled() -> bool {
logging::debug("telemetry disabled by environment");
return false;
}
if std::env::var("JCODE_OFFLINE").is_ok() {
logging::debug("telemetry disabled by JCODE_OFFLINE");
return false;
}
if let Ok(dir) = storage::jcode_dir()
&& dir.join("no_telemetry").exists()
{
Expand Down
41 changes: 41 additions & 0 deletions src/telemetry/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,36 @@ fn test_install_marker_tracks_current_telemetry_id() {
}
}

#[test]
fn telemetry_disabled_by_jcode_offline_env() {
// Regression for issue #24: --offline / JCODE_OFFLINE must disable startup
// telemetry without needing the separate JCODE_NO_TELEMETRY knob.
let _lock = lock_test_env();
let prev_offline = std::env::var_os("JCODE_OFFLINE");
let prev_no = std::env::var_os("JCODE_NO_TELEMETRY");
let prev_endpoint = std::env::var_os("JCODE_TELEMETRY_ENDPOINT");
// Pin to a working endpoint so the only thing disabling telemetry is offline.
crate::env::set_var("JCODE_TELEMETRY_ENDPOINT", "https://example.com/v1/event");
crate::env::remove_var("JCODE_NO_TELEMETRY");
crate::env::set_var("JCODE_OFFLINE", "1");

assert!(!super::is_enabled(), "JCODE_OFFLINE must disable telemetry");

if let Some(p) = prev_offline {
crate::env::set_var("JCODE_OFFLINE", p);
} else {
crate::env::remove_var("JCODE_OFFLINE");
}
if let Some(p) = prev_no {
crate::env::set_var("JCODE_NO_TELEMETRY", p);
}
if let Some(p) = prev_endpoint {
crate::env::set_var("JCODE_TELEMETRY_ENDPOINT", p);
} else {
crate::env::remove_var("JCODE_TELEMETRY_ENDPOINT");
}
}

// ---------------------------------------------------------------------------
// Regression tests for issue #73 / upstream PR #77 — telemetry endpoint is
// now resolved at runtime and absent by default in this fork.
Expand All @@ -386,6 +416,7 @@ fn telemetry_endpoint_returns_none_without_config() {
let prev_env = std::env::var_os("JCODE_TELEMETRY_ENDPOINT");
let prev_home = std::env::var_os("JCODE_HOME");
crate::env::remove_var("JCODE_TELEMETRY_ENDPOINT");
crate::env::remove_var("JCODE_OFFLINE");
let temp = tempfile::TempDir::new().expect("create temp dir");
crate::env::set_var("JCODE_HOME", temp.path());

Expand All @@ -409,6 +440,8 @@ fn telemetry_endpoint_returns_none_without_config() {
fn telemetry_endpoint_picks_up_env_var() {
let _lock = lock_test_env();
let prev_env = std::env::var_os("JCODE_TELEMETRY_ENDPOINT");
let prev_offline = std::env::var_os("JCODE_OFFLINE");
crate::env::remove_var("JCODE_OFFLINE");
crate::env::set_var(
"JCODE_TELEMETRY_ENDPOINT",
"https://example.com/telemetry/v1",
Expand All @@ -430,14 +463,19 @@ fn telemetry_endpoint_picks_up_env_var() {
} else {
crate::env::remove_var("JCODE_TELEMETRY_ENDPOINT");
}
if let Some(prev) = prev_offline {
crate::env::set_var("JCODE_OFFLINE", prev);
}
}

#[test]
fn telemetry_endpoint_picks_up_telemetry_toml() {
let _lock = lock_test_env();
let prev_env = std::env::var_os("JCODE_TELEMETRY_ENDPOINT");
let prev_home = std::env::var_os("JCODE_HOME");
let prev_offline = std::env::var_os("JCODE_OFFLINE");
crate::env::remove_var("JCODE_TELEMETRY_ENDPOINT");
crate::env::remove_var("JCODE_OFFLINE");
let temp = tempfile::TempDir::new().expect("create temp dir");
crate::env::set_var("JCODE_HOME", temp.path());
std::fs::write(
Expand All @@ -460,4 +498,7 @@ fn telemetry_endpoint_picks_up_telemetry_toml() {
} else {
crate::env::remove_var("JCODE_HOME");
}
if let Some(prev) = prev_offline {
crate::env::set_var("JCODE_OFFLINE", prev);
}
}
30 changes: 30 additions & 0 deletions src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,10 @@ pub fn spawn_background_session_update(session_id: String) {
}

pub fn check_for_update_blocking() -> Result<Option<GitHubRelease>> {
if std::env::var("JCODE_OFFLINE").is_ok() {
crate::logging::info("Update check skipped (JCODE_OFFLINE=1)");
return Ok(None);
}
let channel = crate::config::config().features.update_channel;
match channel {
crate::config::UpdateChannel::Main => check_for_main_update_blocking(),
Expand Down Expand Up @@ -1120,4 +1124,30 @@ mod tests {
Duration::from_secs(123)
);
}

#[test]
fn check_for_update_blocking_returns_none_in_offline_mode() {
// Regression for issue #24: --offline / JCODE_OFFLINE must short-circuit
// every startup network operation, including the update check.
let _guard = crate::storage::lock_test_env();
let prev = std::env::var_os("JCODE_OFFLINE");
crate::env::set_var("JCODE_OFFLINE", "1");

let result = check_for_update_blocking();

if let Some(prev) = prev {
crate::env::set_var("JCODE_OFFLINE", prev);
} else {
crate::env::remove_var("JCODE_OFFLINE");
}

match result {
Ok(None) => {}
Ok(Some(release)) => panic!(
"offline mode must not return a release; got {:?}",
release.tag_name
),
Err(e) => panic!("offline mode must not error; got {e}"),
}
}
}