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
21 changes: 21 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,29 @@ permissions:
jobs:
release-please:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
release-type: rust
package-name: rtk

update-latest-tag:
name: Update 'latest' tag
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Update latest tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -fa latest -m "Latest stable release (${{ needs.release-please.outputs.tag_name }})"
git push origin latest --force
26 changes: 0 additions & 26 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,32 +180,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

update-latest-tag:
name: Update 'latest' tag
needs: release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "version=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi

- name: Update latest tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -fa latest -m "Latest stable release (${{ steps.version.outputs.version }})"
git push origin latest --force

# TODO: Enable when HOMEBREW_TAP_TOKEN is configured
# homebrew:
# name: Update Homebrew Tap
Comment on lines 183 to 185
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With update-latest-tag removed from this workflow, runs triggered by push to version tags (v*) or workflow_dispatch will no longer advance the latest tag. If release.yml is still used for manual/tag-driven releases, consider re-adding a latest update step here (in addition to the release-please path) or moving the behavior to a workflow triggered on release/published events.

Copilot uses AI. Check for mistakes.
Expand Down
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.5.0"
}
10 changes: 10 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"packages": {
".": {
"release-type": "rust",
"package-name": "rtk",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true
}
}
}
45 changes: 38 additions & 7 deletions src/ccusage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,33 +82,64 @@ struct MonthlyEntry {

// ── Public API ──

/// Check if ccusage CLI is available in PATH
pub fn is_available() -> bool {
/// Check if ccusage binary exists in PATH
fn binary_exists() -> bool {
Command::new("which")
.arg("ccusage")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}

/// Build the ccusage command, falling back to npx if binary not in PATH
fn build_command() -> Option<Command> {
if binary_exists() {
return Some(Command::new("ccusage"));
}

// Fallback: try npx
let npx_check = Command::new("npx")
.arg("ccusage")
.arg("--help")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();

if npx_check.map(|s| s.success()).unwrap_or(false) {
Comment on lines +94 to +108
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_command() runs npx ccusage --help as an availability probe, and then fetch() later runs npx ccusage <subcommand> ... again. This adds an extra subprocess invocation (and can trigger npx download/install side effects twice). Consider removing the probe and instead selecting npx ccusage based on the presence of npx, letting the single real fetch() invocation determine success/failure.

Suggested change
/// Build the ccusage command, falling back to npx if binary not in PATH
fn build_command() -> Option<Command> {
if binary_exists() {
return Some(Command::new("ccusage"));
}
// Fallback: try npx
let npx_check = Command::new("npx")
.arg("ccusage")
.arg("--help")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
if npx_check.map(|s| s.success()).unwrap_or(false) {
/// Check if npx exists in PATH
fn npx_exists() -> bool {
Command::new("which")
.arg("npx")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Build the ccusage command, falling back to npx if binary not in PATH
fn build_command() -> Option<Command> {
if binary_exists() {
return Some(Command::new("ccusage"));
}
// Fallback: use npx if available
if npx_exists() {

Copilot uses AI. Check for mistakes.
let mut cmd = Command::new("npx");
cmd.arg("ccusage");
return Some(cmd);
}

None
}

/// Check if ccusage CLI is available (binary or via npx)
pub fn is_available() -> bool {
build_command().is_some()
}

/// Fetch usage data from ccusage for the last 90 days
///
/// Returns `Ok(None)` if ccusage is unavailable (graceful degradation)
/// Returns `Ok(Some(vec))` with parsed data on success
/// Returns `Err` only on unexpected failures (JSON parse, etc.)
pub fn fetch(granularity: Granularity) -> Result<Option<Vec<CcusagePeriod>>> {
if !is_available() {
eprintln!("⚠️ ccusage not found. Install: npm i -g ccusage");
return Ok(None);
}
let mut cmd = match build_command() {
Some(cmd) => cmd,
None => {
eprintln!("⚠️ ccusage not found. Install: npm i -g ccusage (or use npx ccusage)");
return Ok(None);
}
};

let subcommand = match granularity {
Granularity::Daily => "daily",
Granularity::Weekly => "weekly",
Granularity::Monthly => "monthly",
};

let output = Command::new("ccusage")
let output = cmd
.arg(subcommand)
.arg("--json")
.arg("--since")
Expand Down
Loading