unifi-cli 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/.github/workflows/release.yml +10 -3
  2. unifi_cli-0.2.3/.gitignore +2 -0
  3. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/CHANGELOG.md +7 -0
  4. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/Cargo.lock +1 -1
  5. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/Cargo.toml +1 -1
  6. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/PKG-INFO +1 -1
  7. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/api/client.rs +23 -4
  8. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/tests/mock_server.rs +74 -0
  9. unifi_cli-0.2.2/.gitignore +0 -1
  10. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/.github/workflows/ci.yml +0 -0
  11. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/LICENSE +0 -0
  12. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/Makefile +0 -0
  13. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/README.md +0 -0
  14. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/prek.toml +0 -0
  15. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/pyproject.toml +0 -0
  16. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/api/mod.rs +0 -0
  17. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/api/tests.rs +0 -0
  18. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/api/types.rs +0 -0
  19. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/commands/clients.rs +0 -0
  20. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/commands/devices.rs +0 -0
  21. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/commands/events.rs +0 -0
  22. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/commands/mod.rs +0 -0
  23. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/commands/networks.rs +0 -0
  24. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/commands/protect.rs +0 -0
  25. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/commands/system.rs +0 -0
  26. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/lib.rs +0 -0
  27. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/main.rs +0 -0
  28. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/output.rs +0 -0
  29. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/schema.rs +0 -0
  30. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/src/tui.rs +0 -0
  31. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/tests/fixtures/clispec-v0.2.json +0 -0
  32. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/tests/spec_compliance.rs +0 -0
  33. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/unifi_cli/__init__.py +0 -0
  34. {unifi_cli-0.2.2 → unifi_cli-0.2.3}/unifi_cli/__main__.py +0 -0
@@ -332,7 +332,7 @@ jobs:
332
332
 
333
333
  - name: Update Homebrew formula
334
334
  env:
335
- GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
335
+ HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
336
336
  run: |
337
337
  VERSION="${{ github.ref_name }}"
338
338
  VERSION_NUM="${VERSION#v}"
@@ -381,8 +381,15 @@ jobs:
381
381
  sed -i "s/SHA_AARCH64_UNKNOWN_LINUX_GNU/${{ steps.hashes.outputs.aarch64_unknown_linux_gnu }}/g" /tmp/unifi-cli.rb
382
382
  sed -i "s/SHA_X86_64_UNKNOWN_LINUX_GNU/${{ steps.hashes.outputs.x86_64_unknown_linux_gnu }}/g" /tmp/unifi-cli.rb
383
383
 
384
- # Clone tap repo, update formula, push
385
- git clone https://x-access-token:${GH_TOKEN}@github.com/rvben/homebrew-tap.git /tmp/tap
384
+ # Authenticate to the tap with a write deploy key scoped to
385
+ # rvben/homebrew-tap. The key does not expire, so cross-repo pushes
386
+ # keep working without periodic token rotation.
387
+ mkdir -p ~/.ssh
388
+ echo "$HOMEBREW_TAP_DEPLOY_KEY" > ~/.ssh/id_ed25519
389
+ chmod 600 ~/.ssh/id_ed25519
390
+ ssh-keyscan -t ed25519 github.com >> ~/.ssh/known_hosts 2>/dev/null
391
+
392
+ git clone git@github.com:rvben/homebrew-tap.git /tmp/tap
386
393
  mkdir -p /tmp/tap/Formula
387
394
  cp /tmp/unifi-cli.rb /tmp/tap/Formula/unifi-cli.rb
388
395
  cd /tmp/tap
@@ -0,0 +1,2 @@
1
+ /target
2
+ .claude/investigations/
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
 
6
6
 
7
+
8
+ ## [0.2.3](https://github.com/rvben/unifi-cli/compare/v0.2.2...v0.2.3) - 2026-07-07
9
+
10
+ ### Fixed
11
+
12
+ - **events**: fall back to rest/alarm when stat/event is 404 on UniFi OS 9+ ([ac8a69c](https://github.com/rvben/unifi-cli/commit/ac8a69c743c5eab05def7bcd67e08e5b060275ea))
13
+
7
14
  ## [0.2.2](https://github.com/rvben/unifi-cli/compare/v0.2.1...v0.2.2) - 2026-06-11
8
15
 
9
16
  ### Added
@@ -2786,7 +2786,7 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
2786
2786
 
2787
2787
  [[package]]
2788
2788
  name = "unifi-cli"
2789
- version = "0.2.2"
2789
+ version = "0.2.3"
2790
2790
  dependencies = [
2791
2791
  "clap",
2792
2792
  "clap_complete",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "unifi-cli"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  edition = "2024"
5
5
  rust-version = "1.90"
6
6
  description = "CLI for UniFi Network controller"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unifi-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: System Administrators
@@ -367,11 +367,30 @@ impl UnifiClient {
367
367
  }
368
368
 
369
369
  // Events
370
+ //
371
+ // Legacy `stat/event` was removed in UniFi Network 9+ (UniFi OS) and now
372
+ // returns api.err.NotFound (404). On those controllers the surviving REST
373
+ // surface for notable events is `rest/alarm`, whose records share this
374
+ // `Event` shape, so fall back to it. (The full live event stream on newer
375
+ // controllers is only exposed over the events WebSocket, which this REST
376
+ // client does not consume.)
370
377
  pub async fn list_events(&self, limit: usize) -> Result<Vec<Event>, ApiError> {
371
- let events: Vec<Event> = self
372
- .get_legacy(&format!("/stat/event?_limit={limit}"))
373
- .await?;
374
- Ok(events)
378
+ match self
379
+ .get_legacy::<Event>(&format!("/stat/event?_limit={limit}"))
380
+ .await
381
+ {
382
+ Ok(events) => Ok(events),
383
+ Err(ApiError::NotFound(_)) => {
384
+ let mut alarms: Vec<Event> = self.get_legacy("/rest/alarm").await?;
385
+ // `rest/alarm` is neither time-ordered nor limited server-side;
386
+ // present the most recent `limit` records to match the
387
+ // semantics `stat/event?_limit=` provided on older controllers.
388
+ alarms.sort_by_key(|e| std::cmp::Reverse(e.time));
389
+ alarms.truncate(limit);
390
+ Ok(alarms)
391
+ }
392
+ Err(e) => Err(e),
393
+ }
375
394
  }
376
395
 
377
396
  // Port table for a specific device
@@ -1706,6 +1706,80 @@ mod command_output {
1706
1706
  .unwrap_err();
1707
1707
  assert!(err.to_string().contains("Not found"));
1708
1708
  }
1709
+
1710
+ #[tokio::test]
1711
+ async fn list_events_returns_stat_event_records() {
1712
+ let server = MockServer::start().await;
1713
+ Mock::given(method("GET"))
1714
+ .and(path("/proxy/network/api/s/default/stat/event"))
1715
+ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1716
+ "meta": {"rc": "ok"},
1717
+ "data": [
1718
+ {"key": "EVT_AP_Connected", "msg": "AP connected", "subsystem": "wlan", "time": 200, "datetime": "2026-07-07T16:00:00Z"},
1719
+ {"key": "EVT_SW_LostContact", "msg": "Switch lost contact", "subsystem": "lan", "time": 100, "datetime": "2026-07-07T15:00:00Z"}
1720
+ ]
1721
+ })))
1722
+ .mount(&server)
1723
+ .await;
1724
+
1725
+ let client = mock_client(&server).await;
1726
+ let events = client.list_events(10).await.unwrap();
1727
+ assert_eq!(events.len(), 2);
1728
+ assert_eq!(events[0].key.as_deref(), Some("EVT_AP_Connected"));
1729
+ }
1730
+
1731
+ // UniFi Network 9+ (UniFi OS) removed the legacy stat/event route, which now
1732
+ // returns api.err.NotFound (404). list_events must fall back to rest/alarm and
1733
+ // return the most recent `limit` records.
1734
+ #[tokio::test]
1735
+ async fn list_events_falls_back_to_alarms_on_404() {
1736
+ let server = MockServer::start().await;
1737
+ Mock::given(method("GET"))
1738
+ .and(path("/proxy/network/api/s/default/stat/event"))
1739
+ .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1740
+ "meta": {"rc": "error", "msg": "api.err.NotFound"},
1741
+ "data": []
1742
+ })))
1743
+ .expect(1)
1744
+ .mount(&server)
1745
+ .await;
1746
+ Mock::given(method("GET"))
1747
+ .and(path("/proxy/network/api/s/default/rest/alarm"))
1748
+ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1749
+ "meta": {"rc": "ok"},
1750
+ "data": [
1751
+ {"key": "EVT_GW_Older", "msg": "older", "time": 100, "datetime": "a"},
1752
+ {"key": "EVT_GW_Newest", "msg": "newest", "time": 300, "datetime": "c"},
1753
+ {"key": "EVT_GW_Middle", "msg": "middle", "time": 200, "datetime": "b"}
1754
+ ]
1755
+ })))
1756
+ .expect(1)
1757
+ .mount(&server)
1758
+ .await;
1759
+
1760
+ let client = mock_client(&server).await;
1761
+ let events = client.list_events(2).await.unwrap();
1762
+ // Most-recent-first, truncated to the requested limit.
1763
+ assert_eq!(events.len(), 2);
1764
+ assert_eq!(events[0].msg.as_deref(), Some("newest"));
1765
+ assert_eq!(events[1].msg.as_deref(), Some("middle"));
1766
+ }
1767
+
1768
+ #[tokio::test]
1769
+ async fn list_events_propagates_non_404_errors() {
1770
+ let server = MockServer::start().await;
1771
+ Mock::given(method("GET"))
1772
+ .and(path("/proxy/network/api/s/default/stat/event"))
1773
+ .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
1774
+ "meta": {"rc": "error", "msg": "api.err.ServerError"},
1775
+ "data": []
1776
+ })))
1777
+ .mount(&server)
1778
+ .await;
1779
+
1780
+ let client = mock_client(&server).await;
1781
+ assert!(client.list_events(10).await.is_err());
1782
+ }
1709
1783
  }
1710
1784
 
1711
1785
  // --- Client construction tests ---
@@ -1 +0,0 @@
1
- /target
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes