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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Run this one-liner in PowerShell (5.1 or later):
irm https://cship.dev/install.ps1 | iex
```

Installs to `%LOCALAPPDATA%\Programs\cship\cship.exe`, writes config to `%USERPROFILE%\.config\cship.toml`, and registers the statusline in `%APPDATA%\Claude\settings.json`.
Installs to `%USERPROFILE%\.local\bin\cship.exe`, writes config to `%USERPROFILE%\.config\cship.toml`, and registers the statusline in `%USERPROFILE%\.claude\settings.json`.

> You can inspect the script before running: [install.ps1](https://cship.dev/install.ps1)

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Run this one-liner in PowerShell (5.1 or later):
irm https://cship.dev/install.ps1 | iex
```

Installs to `%LOCALAPPDATA%\Programs\cship\cship.exe`, writes config to `%USERPROFILE%\.config\cship.toml`, and registers the statusline in `%APPDATA%\Claude\settings.json`.
Installs to `%USERPROFILE%\.local\bin\cship.exe`, writes config to `%USERPROFILE%\.config\cship.toml`, and registers the statusline in `%USERPROFILE%\.claude\settings.json`.

> You can inspect the script before running: [install.ps1](https://cship.dev/install.ps1)

Expand Down
42 changes: 20 additions & 22 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Install cship Claude Code statusline tool for Windows.
Install cship - Claude Code statusline tool for Windows.
.DESCRIPTION
Downloads the cship binary from GitHub Releases, installs it to
%LOCALAPPDATA%\Programs\cship\, writes a default cship.toml, and
Expand All @@ -11,11 +11,11 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$REPO = "stephenleo/cship"
$INSTALL_DIR = Join-Path $env:LOCALAPPDATA "Programs\cship"
$INSTALL_DIR = Join-Path $env:USERPROFILE ".local\bin"
$BIN = Join-Path $INSTALL_DIR "cship.exe"
$CONFIG_DIR = Join-Path $env:USERPROFILE ".config"
$CONFIG_FILE = Join-Path $CONFIG_DIR "cship.toml"
$SETTINGS = Join-Path $env:APPDATA "Claude\settings.json"
$SETTINGS = Join-Path $env:USERPROFILE ".claude\settings.json"

# --- Arch detection ---
$arch = $env:PROCESSOR_ARCHITECTURE
Expand Down Expand Up @@ -53,19 +53,16 @@ New-Item -ItemType Directory -Force -Path $INSTALL_DIR | Out-Null
Invoke-WebRequest -Uri $downloadUrl -OutFile $BIN -UseBasicParsing
Write-Host "Installed to: $BIN"

# --- Add to PATH (offer) ---
# --- Ensure ~/.local/bin is on PATH ---
$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User")
if ($currentPath -notlike "*$INSTALL_DIR*") {
$add = Read-Host "Add $INSTALL_DIR to your PATH? [Y/n]"
if ($add -ne "n" -and $add -ne "N") {
[Environment]::SetEnvironmentVariable(
"PATH",
"$currentPath;$INSTALL_DIR",
"User"
)
$env:PATH += ";$INSTALL_DIR"
Write-Host "Added to PATH (effective in new shells)."
}
[Environment]::SetEnvironmentVariable(
"PATH",
"$currentPath;$INSTALL_DIR",
"User"
)
$env:PATH += ";$INSTALL_DIR"
Write-Host "Added $INSTALL_DIR to your user PATH (effective in new shells)."
}

# --- Write default cship.toml ---
Expand All @@ -83,28 +80,29 @@ disabled = false
'@ | Set-Content -Path $CONFIG_FILE -Encoding UTF8
Write-Host "Config written to: $CONFIG_FILE"
} else {
Write-Host "Config already exists at $CONFIG_FILE skipping."
Write-Host "Config already exists at $CONFIG_FILE - skipping."
}

# --- Register statusline in Claude Code settings.json ---
$claudeDir = Split-Path $SETTINGS
if (-not (Test-Path $claudeDir)) {
Write-Host "Claude Code settings directory not found at $claudeDir skipping settings update."
Write-Host "Claude Code settings directory not found at $claudeDir - skipping settings update."
Write-Host "Authenticate in Claude Code first, then re-run this script."
} elseif (-not (Test-Path $SETTINGS)) {
# Create minimal settings.json
New-Item -ItemType Directory -Force -Path $claudeDir | Out-Null
'{"statusline": "cship"}' | Set-Content -Path $SETTINGS -Encoding UTF8
Write-Host "Created settings.json with statusline entry."
'{"statusLine": {"type": "command", "command": "cship"}}' | Set-Content -Path $SETTINGS -Encoding UTF8
Write-Host "Created settings.json with statusLine entry: $SETTINGS"
} else {
$json = Get-Content $SETTINGS -Raw | ConvertFrom-Json
if (-not $json.PSObject.Properties["statusline"]) {
$json | Add-Member -NotePropertyName "statusline" -NotePropertyValue "cship"
$statusLineValue = [PSCustomObject]@{ type = "command"; command = "cship" }
if (-not $json.PSObject.Properties["statusLine"]) {
$json | Add-Member -NotePropertyName "statusLine" -NotePropertyValue $statusLineValue
} else {
$json.statusline = "cship"
$json.statusLine = $statusLineValue
}
$json | ConvertTo-Json -Depth 100 | Set-Content -Path $SETTINGS -Encoding UTF8
Write-Host "Updated settings.json with statusline entry."
Write-Host "Updated settings.json with statusLine entry: $SETTINGS"
}

# --- First-run preview ---
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ fi
SETTINGS="$ROOT/.claude/settings.json"
if ! command -v python3 >/dev/null 2>&1; then
echo "Warning: python3 not found. Skipping settings.json update."
echo "To wire cship manually, add \"statusline\": \"cship\" to $SETTINGS"
echo "To wire cship manually, add \"statusLine\": {\"type\": \"command\", \"command\": \"cship\"} to $SETTINGS"
elif [ -f "$SETTINGS" ]; then
python3 - "$SETTINGS" <<'PYEOF' || echo "Warning: failed to update settings.json — add statusLine manually."
import json, sys
Expand Down
8 changes: 4 additions & 4 deletions src/explain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,11 @@ fn error_hint_for(
match segment {
"model" => (
"model data absent from Claude Code context".into(),
"Ensure Claude Code is running and cship is invoked via the \"statusline\" key in ~/.claude/settings.json.".into(),
"Ensure Claude Code is running and cship is wired via \"statusLine\": {\"type\": \"command\", \"command\": \"cship\"} in ~/.claude/settings.json.".into(),
),
"cost" => (
"cost data absent from Claude Code context".into(),
"Ensure Claude Code is running and cship is invoked via the \"statusline\" key in ~/.claude/settings.json.".into(),
"Ensure Claude Code is running and cship is wired via \"statusLine\": {\"type\": \"command\", \"command\": \"cship\"} in ~/.claude/settings.json.".into(),
),
"context_bar" | "context_window" => (
"context_window data absent from Claude Code context (may be absent early in a session)".into(),
Expand All @@ -224,11 +224,11 @@ fn error_hint_for(
),
"cwd" | "session_id" | "transcript_path" | "version" | "output_style" => (
"session field absent from Claude Code context".into(),
"Ensure Claude Code is running and cship is invoked via the \"statusline\" key in ~/.claude/settings.json.".into(),
"Ensure Claude Code is running and cship is wired via \"statusLine\": {\"type\": \"command\", \"command\": \"cship\"} in ~/.claude/settings.json.".into(),
),
"workspace" => (
"workspace data absent from Claude Code context".into(),
"Ensure Claude Code is running and cship is invoked via the \"statusline\" key in ~/.claude/settings.json.".into(),
"Ensure Claude Code is running and cship is wired via \"statusLine\": {\"type\": \"command\", \"command\": \"cship\"} in ~/.claude/settings.json.".into(),
),
"usage_limits" => {
// Probe credential state to distinguish missing token from expired token.
Expand Down
74 changes: 12 additions & 62 deletions src/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,53 +52,40 @@ fn remove_binary(home: &std::path::Path) {
}

fn remove_statusline_from_settings(home: &std::path::Path) {
#[cfg(target_os = "windows")]
let path = match std::env::var("APPDATA") {
Ok(app_data) => std::path::Path::new(&app_data)
.join("Claude")
.join("settings.json"),
Err(_) => {
tracing::warn!(
"APPDATA env var not set; falling back to ~/.claude/settings.json for settings path"
);
home.join(".claude/settings.json")
}
};
#[cfg(not(target_os = "windows"))]
let path = home.join(".claude/settings.json");

if !path.exists() {
println!("settings.json not found — skipping.");
println!("settings.json not found at {} — skipping.", path.display());
return;
}
let raw = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
println!("Could not read settings.json: {e}");
println!("Could not read {}: {e}", path.display());
return;
}
};
let mut map: serde_json::Map<String, serde_json::Value> = match serde_json::from_str(&raw) {
Ok(m) => m,
Err(e) => {
println!("Could not parse settings.json: {e}");
println!("Could not parse {}: {e}", path.display());
return;
}
};
if map.remove("statusline").is_some() {
if map.remove("statusLine").is_some() {
let updated = match serde_json::to_string_pretty(&map) {
Ok(s) => s,
Err(e) => {
println!("Could not serialize settings.json: {e}");
println!("Could not serialize {}: {e}", path.display());
return;
}
};
match std::fs::write(&path, updated + "\n") {
Ok(()) => println!("Removed \"statusline\" from settings.json"),
Err(e) => println!("Could not write settings.json: {e}"),
Ok(()) => println!("Removed \"statusLine\" from {}", path.display()),
Err(e) => println!("Could not write {}: {e}", path.display()),
}
} else {
println!("\"statusline\" not found in settings.json — skipping.");
println!("\"statusLine\" not found in {} — skipping.", path.display());
}
}

Expand Down Expand Up @@ -211,15 +198,15 @@ mod tests {
let settings_path = claude_dir.join("settings.json");
std::fs::write(
&settings_path,
r#"{"statusline":"cship","otherKey":"value"}"#,
r#"{"statusLine":{"type":"command","command":"cship"},"otherKey":"value"}"#,
)
.unwrap();
remove_statusline_from_settings(home);
let content = std::fs::read_to_string(&settings_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(
parsed.get("statusline").is_none(),
"statusline key should be removed"
parsed.get("statusLine").is_none(),
"statusLine key should be removed"
);
assert_eq!(
parsed.get("otherKey").and_then(|v| v.as_str()),
Expand All @@ -229,43 +216,6 @@ mod tests {
});
}

#[test]
#[cfg(target_os = "windows")]
fn test_remove_statusline_uses_appdata_on_windows() {
with_tempdir(|home| {
// Create a temp APPDATA directory with Claude/settings.json
let tmp_appdata = tempfile::tempdir().unwrap();
let claude_dir = tmp_appdata.path().join("Claude");
std::fs::create_dir_all(&claude_dir).unwrap();
let settings_path = claude_dir.join("settings.json");
std::fs::write(
&settings_path,
r#"{"statusline":"cship","otherKey":"value"}"#,
)
.unwrap();

// SAFETY: guarded by HOME_MUTEX; no other threads read APPDATA concurrently.
unsafe { std::env::set_var("APPDATA", tmp_appdata.path()) };

remove_statusline_from_settings(home);

let content = std::fs::read_to_string(&settings_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(
parsed.get("statusline").is_none(),
"statusline key should be removed from APPDATA path on Windows"
);
assert_eq!(
parsed.get("otherKey").and_then(|v| v.as_str()),
Some("value"),
"other keys should be preserved"
);

// SAFETY: guarded by HOME_MUTEX; no other threads read APPDATA concurrently.
unsafe { std::env::remove_var("APPDATA") };
});
}

#[test]
fn test_remove_statusline_absent_key() {
with_tempdir(|home| {
Expand All @@ -278,7 +228,7 @@ mod tests {
// File should still be parseable and unchanged in content
let content = std::fs::read_to_string(&settings_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(parsed.get("statusline").is_none());
assert!(parsed.get("statusLine").is_none());
assert_eq!(
parsed.get("otherKey").and_then(|v| v.as_str()),
Some("value")
Expand Down
Loading