Skip to content

feat(fleetcli): protobuf-driven Fleet command-line client#467

Open
krisztiankurucz wants to merge 18 commits into
block:mainfrom
krisztiankurucz:kkurucz/port-fleet-client-api
Open

feat(fleetcli): protobuf-driven Fleet command-line client#467
krisztiankurucz wants to merge 18 commits into
block:mainfrom
krisztiankurucz:kkurucz/port-fleet-client-api

Conversation

@krisztiankurucz

@krisztiankurucz krisztiankurucz commented Jun 15, 2026

Copy link
Copy Markdown

Summary

fleetcli is a new command-line client for the Fleet API. Most of its surface is generated from the same protobuf definitions the server uses — each RPC service becomes a command group with typed flags — so the CLI tracks the API automatically instead of drifting behind a hand-maintained wrapper. A thin handwritten runtime layers in what codegen can't express: transport, auth, and the streaming / long-running flows.

How it works

Generated layer. server/tools/generate-fleet-cli reads a descriptor set (buf build --as-file-descriptor-set) plus overrides.json and emits one cmd_<group>.go per service — miners, racks, groups, pools, schedule, networkinfo, onboarding, minercommand — wired together in cmd_commands.go. Generated commands delegate request construction and execution to a shared runtime, keeping the emitted code small. The generator only rewrites a file when its contents actually change, so routine regen keeps file mtimes stable and never trips the docker-compose watch into restarting fleet-api.

Handwritten runtime. main.go / client.go own the global flags (--server, --api-key, --username, --password, --insecure — all env-backed), the HTTP JSON-RPC transport, and auth dispatch. Flows that don't fit the generated mold are hand-built: auth (session login), apikey (key lifecycle), pairing (streaming discovery), performance (telemetry), and firmware (file upload/lifecycle over the server's plain HTTP endpoints).

Tooling. just gen-fleet-cli builds the descriptor set and runs the generator (also folded into just gen); just build-fleet-cli compiles to .cache/ so the binary lives outside server/ and a CLI rebuild never restarts fleet-api. A bin/scripts/fleetcli wrapper plus a Hermit entry make it runnable in-repo. Full architecture in server/docs/fleet-cli-generation.md.

Test plan

  • just lint — clean (buf, eslint, golangci-lint).
  • cd server && go test ./cmd/fleetcli/... ./tools/generate-fleet-cli/... — pass.
  • just gen-fleet-cli is idempotent against the committed cmd_*.go — generated code is in sync.
  • E2E suite (needs docker-compose). To verify end-to-end: cd server && go test -tags=e2e ./e2e -run TestFleetCLI — covers the command workflow, firmware upload/file lifecycle, subcommands, and leaf-command coverage.

Manual testing:

🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli onboarding create-admin --username "$FLEET_USERNAME" --password "$FLEET_PASSWORD"

{
  "user_id": "019eccd1-ff1a-7e44-82ec-825c0bc1e7a3"
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli auth login
{
  "session_expiry": "1781581583",
  "user_info": {
    "last_login_at": "2026-06-15T19:46:23.504424251Z",
    "password_updated_at": null,
    "permissions": [
      "activity:read",
      "apikey:manage",
      "curtailment:ingest",
      "curtailment:manage",
      "curtailment:read",
      "fleet:read",
      "fleetnode:manage",
      "fleetnode:read",
      "miner:blink_led",
      "miner:delete",
      "miner:download_logs",
      "miner:export_csv",
      "miner:firmware_update",
      "miner:pair",
      "miner:read",
      "miner:reboot",
      "miner:rename",
      "miner:set_cooling_mode",
      "miner:set_power_target",
      "miner:start_mining",
      "miner:stop_mining",
      "miner:unpair",
      "miner:update_password",
      "miner:update_pools",
      "miner:update_worker_names",
      "pool:manage",
      "pool:read",
      "rack:manage",
      "rack:read",
      "role:manage",
      "schedule:manage",
      "schedule:read",
      "serverlog:read",
      "site:manage",
      "site:read",
      "user:manage",
      "user:read"
    ],
    "requires_password_change": false,
    "role": "SUPER_ADMIN",
    "user_id": "019eccd1-ff1a-7e44-82ec-825c0bc1e7a3",
    "username": "admin"
  }
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli apikey create --name "$UNIQUE-key"
{
  "api_key": "fleet_8386daa4_YNBwFwgRWUss9TpTUNkl-5FEQYTAgdqNHy679LGNO1w",
  "info": {
    "created_at": "2026-06-15T19:46:37.660481424Z",
    "created_by": "admin",
    "expires_at": null,
    "key_id": "019eccd2-57dc-775a-b32f-b06d12ebf492",
    "last_used_at": null,
    "name": "-key",
    "prefix": "fleet_8386daa4"
  }
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli apikey list
{
  "api_keys": [
    {
      "created_at": "2026-06-15T19:46:37.660481Z",
      "created_by": "admin",
      "expires_at": null,
      "key_id": "019eccd2-57dc-775a-b32f-b06d12ebf492",
      "last_used_at": null,
      "name": "-key",
      "prefix": "fleet_8386daa4"
    }
  ]
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli pairing discover --ip proto-sim --port 8080
{
  "devices": [
    {
      "capabilities": {
        "authentication": {
          "supported_methods": [
            "AUTHENTICATION_METHOD_ASYMMETRIC_KEY"
          ]
        },
        "commands": {
          "air_cooling_supported": true,
          "curtail_efficiency_supported": true,
          "curtail_full_supported": true,
          "factory_reset_supported": false,
          "immersion_cooling_supported": true,
          "led_blink_supported": true,
          "logs_download_supported": true,
          "mining_start_supported": true,
          "mining_stop_supported": true,
          "pool_max_count": 3,
          "pool_priority_supported": true,
          "pool_switching_supported": true,
          "power_mode_efficiency_supported": true,
          "reboot_supported": true,
          "update_miner_password_supported": true
        },
        "firmware": {
          "manual_upload_supported": true,
          "ota_update_supported": true
        },
        "manufacturer": "Proto",
        "telemetry": {
          "efficiency_reported": true,
          "error_count_reported": true,
          "fan_speed_reported": true,
          "hashrate_reported": true,
          "historical_data_supported": true,
          "miner_status_reported": true,
          "per_board_stats_reported": true,
          "per_chip_stats_reported": true,
          "pool_stats_reported": true,
          "power_usage_reported": true,
          "psu_stats_reported": true,
          "realtime_telemetry_supported": true,
          "temperature_reported": true,
          "uptime_reported": true
        }
      },
      "device_identifier": "019eccd3-8e1d-7e1d-9ff3-24f6cf2e4437",
      "driver_name": "proto",
      "firmware_version": "",
      "ip_address": "192.168.2.11",
      "mac_address": "",
      "manufacturer": "Proto",
      "model": "Rig",
      "port": "8080",
      "serial_number": "",
      "url_scheme": "http"
    }
  ],
  "error": ""
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % export DEVICE="019eccd3-8e1d-7e1d-9ff3-24f6cf2e4437"
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli pairing pair --device "$DEVICE"
{
  "failed_device_ids": []
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli miners list --page-size 25
{
  "cursor": "",
  "firmware_versions": [
    "1.8.0"
  ],
  "miners": [
    {
      "capabilities": {
        "authentication": {
          "supported_methods": [
            "AUTHENTICATION_METHOD_ASYMMETRIC_KEY"
          ]
        },
        "commands": {
          "air_cooling_supported": true,
          "curtail_efficiency_supported": true,
          "curtail_full_supported": true,
          "factory_reset_supported": false,
          "immersion_cooling_supported": true,
          "led_blink_supported": true,
          "logs_download_supported": true,
          "mining_start_supported": true,
          "mining_stop_supported": true,
          "pool_max_count": 3,
          "pool_priority_supported": true,
          "pool_switching_supported": true,
          "power_mode_efficiency_supported": true,
          "reboot_supported": true,
          "update_miner_password_supported": true
        },
        "firmware": {
          "manual_upload_supported": true,
          "ota_update_supported": true
        },
        "manufacturer": "Proto",
        "telemetry": {
          "efficiency_reported": true,
          "error_count_reported": true,
          "fan_speed_reported": true,
          "hashrate_reported": true,
          "historical_data_supported": true,
          "miner_status_reported": true,
          "per_board_stats_reported": true,
          "per_chip_stats_reported": true,
          "pool_stats_reported": true,
          "power_usage_reported": true,
          "psu_stats_reported": true,
          "realtime_telemetry_supported": true,
          "temperature_reported": true,
          "uptime_reported": true
        }
      },
      "device_identifier": "019eccd3-8e1d-7e1d-9ff3-24f6cf2e4437",
      "device_status": "DEVICE_STATUS_NEEDS_MINING_POOL",
      "driver_name": "proto",
      "efficiency": [
        {
          "timestamp": "2026-06-15T19:48:10.564077Z",
          "unit": "MEASUREMENT_UNIT_JOULES_PER_TERAHASH",
          "value": 23.73417890447876
        }
      ],
      "firmware_version": "1.8.0",
      "group_labels": [],
      "hashrate": [
        {
          "timestamp": "2026-06-15T19:48:10.564077Z",
          "unit": "MEASUREMENT_UNIT_TERAHASH_PER_SECOND",
          "value": 0
        }
      ],
      "ip_address": "192.168.2.11",
      "mac_address": "02:00:00:98:D8:73",
      "manufacturer": "Proto",
      "model": "Rig",
      "name": "Proto Rig",
      "pairing_status": "PAIRING_STATUS_PAIRED",
      "power_usage": [
        {
          "timestamp": "2026-06-15T19:48:10.564077Z",
          "unit": "MEASUREMENT_UNIT_KILOWATT",
          "value": 0.19921769234441863
        }
      ],
      "rack_label": "",
      "rack_position": "",
      "serial_number": "PROTO-SIM-001",
      "site_label": "",
      "temperature": [
        {
          "timestamp": "2026-06-15T19:48:10.564077Z",
          "unit": "MEASUREMENT_UNIT_CELSIUS",
          "value": 56.03903185261576
        }
      ],
      "temperature_status": "TEMPERATURE_STATUS_UNSPECIFIED",
      "timestamp": "2026-06-15T19:48:10.564077Z",
      "url": "http://192.168.2.11",
      "worker_name": "02:00:00:98:D8:73"
    }
  ],
  "models": [
    "Rig"
  ],
  "total_miners": 1,
  "total_state_counts": {
    "broken_count": 1,
    "hashing_count": 0,
    "offline_count": 0,
    "sleeping_count": 0
  }
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli performance get --metric hashrate --page-size 25
{
  "granularity": "30s",
  "latest": {
    "hashrate": {
      "average": 0,
      "device_count": 1,
      "maximum": 0,
      "minimum": 0,
      "timestamp": "2026-06-15T19:48:00Z"
    }
  },
  "latest_temperature_status": {
    "cold": 0,
    "critical": 0,
    "hot": 0,
    "ok": 1,
    "timestamp": "2026-06-15T19:48:00Z"
  },
  "latest_uptime_status": {
    "hashing": 0,
    "not_hashing": 0,
    "timestamp": "2026-06-15T19:48:26Z"
  },
  "requested_metrics": [
    "hashrate"
  ],
  "returned_rows": 1,
  "source": "telemetry.v1.TelemetryService/GetCombinedMetrics",
  "window": "1h0m0s"
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli networkinfo get
{
  "network_info": {
    "gateway": "192.168.2.1",
    "ipv6_subnet": "",
    "local_ip": "192.168.2.17",
    "local_ipv6": "",
    "network_nickname": "",
    "subnet": "192.168.2.0/24"
  }
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli networkinfo set-nickname --network-nickname test
/networkinfo.v1.NetworkInfoService/UpdateNetworkNickname returned 501 Not Implemented:
{
  "code": "unimplemented",
  "details": [
    {
      "debug": {
        "common": "FLEET_ERROR_CODE_UNSPECIFIED"
      },
      "type": "common.v1.FleetErrorDetails",
      "value": "CAA"
    }
  ],
  "message": "UpdateNetworkNickname is not implemented"
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli groups create --label "test-group" --description "manual test"
{
  "added_count": 0,
  "collection": {
    "created_at": "2026-06-15T19:49:14.964570Z",
    "description": "manual test",
    "device_count": 0,
    "id": "1",
    "label": "test-group",
    "type": "COLLECTION_TYPE_GROUP",
    "updated_at": "2026-06-15T19:49:14.964570Z"
  }
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli groups get --collection-id 1
{
  "collection": {
    "created_at": "2026-06-15T19:49:14.964570Z",
    "description": "manual test",
    "device_count": 0,
    "id": "1",
    "label": "test-group",
    "type": "COLLECTION_TYPE_GROUP",
    "updated_at": "2026-06-15T19:49:14.964570Z"
  }
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli groups list --page-size 25
{
  "collections": [
    {
      "created_at": "2026-06-15T19:49:14.964570Z",
      "description": "manual test",
      "device_count": 0,
      "id": "1",
      "label": "test-group",
      "type": "COLLECTION_TYPE_GROUP",
      "updated_at": "2026-06-15T19:49:14.964570Z"
    }
  ],
  "next_page_token": "",
  "total_count": 1
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli groups members --collection-id 1
{
  "members": [],
  "next_page_token": ""
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli groups stats --collection-ids 1
{
  "stats": [
    {
      "avg_efficiency_jth": 0,
      "broken_count": 0,
      "collection_id": "1",
      "control_board_issue_count": 0,
      "device_count": 0,
      "efficiency_reporting_count": 0,
      "fan_issue_count": 0,
      "hash_board_issue_count": 0,
      "hashing_count": 0,
      "hashrate_reporting_count": 0,
      "max_temperature_c": 0,
      "min_temperature_c": 0,
      "offline_count": 0,
      "power_reporting_count": 0,
      "psu_issue_count": 0,
      "reporting_count": 0,
      "sleeping_count": 0,
      "slot_statuses": [],
      "temperature_reporting_count": 0,
      "total_hashrate_ths": 0,
      "total_power_kw": 0
    }
  ]
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli pools list
{
  "pools": []
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli racks list
{
  "collections": [],
  "next_page_token": "",
  "total_count": 0
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli schedule list
{
  "schedules": []
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli firmware config
{
  "allowed_extensions": [
    ".swu",
    ".tar.gz",
    ".zip"
  ],
  "chunk_size_bytes": 33554432,
  "max_file_size_bytes": 524288000
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % dd if=/dev/urandom of=/tmp/fleetcli-test.swu bs=1024 count=64
64+0 records in
64+0 records out
65536 bytes transferred in 0.000462 secs (141852814 bytes/sec)
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli firmware check /tmp/fleetcli-test.swu
{
  "exists": false
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli firmware upload /tmp/fleetcli-test.swu
computing sha256 of fleetcli-test.swu...
firmware upload: 100% (65536/65536 bytes)
{
  "firmware_file_id": "019eccd6-98eb-7278-8a40-2bde9fcb3f7d"
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli firmware list
{
  "files": [
    {
      "filename": "fleetcli-test.swu",
      "id": "019eccd6-98eb-7278-8a40-2bde9fcb3f7d",
      "size": 65536,
      "uploaded_at": "2026-06-15T19:51:16.457871012Z"
    }
  ]
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli minercommand check-capabilities --command-type blink-led --device 019eccd3-8e1d-7e1d-9ff3-24f6cf2e4437
{
  "all_supported": true,
  "none_supported": false,
  "supported_count": 1,
  "supported_device_identifiers": [
    "019eccd3-8e1d-7e1d-9ff3-24f6cf2e4437"
  ],
  "total_count": 1,
  "unsupported_count": 0,
  "unsupported_groups": []
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli miners list --page-size 25
{
  "cursor": "",
  "firmware_versions": [
    "1.8.0"
  ],
  "miners": [
    {
      "capabilities": {
        "authentication": {
          "supported_methods": [
            "AUTHENTICATION_METHOD_ASYMMETRIC_KEY"
          ]
        },
        "commands": {
          "air_cooling_supported": true,
          "curtail_efficiency_supported": true,
          "curtail_full_supported": true,
          "factory_reset_supported": false,
          "immersion_cooling_supported": true,
          "led_blink_supported": true,
          "logs_download_supported": true,
          "mining_start_supported": true,
          "mining_stop_supported": true,
          "pool_max_count": 3,
          "pool_priority_supported": true,
          "pool_switching_supported": true,
          "power_mode_efficiency_supported": true,
          "reboot_supported": true,
          "update_miner_password_supported": true
        },
        "firmware": {
          "manual_upload_supported": true,
          "ota_update_supported": true
        },
        "manufacturer": "Proto",
        "telemetry": {
          "efficiency_reported": true,
          "error_count_reported": true,
          "fan_speed_reported": true,
          "hashrate_reported": true,
          "historical_data_supported": true,
          "miner_status_reported": true,
          "per_board_stats_reported": true,
          "per_chip_stats_reported": true,
          "pool_stats_reported": true,
          "power_usage_reported": true,
          "psu_stats_reported": true,
          "realtime_telemetry_supported": true,
          "temperature_reported": true,
          "uptime_reported": true
        }
      },
      "device_identifier": "019eccd3-8e1d-7e1d-9ff3-24f6cf2e4437",
      "device_status": "DEVICE_STATUS_NEEDS_MINING_POOL",
      "driver_name": "proto",
      "efficiency": [
        {
          "timestamp": "2026-06-15T19:56:23.372784Z",
          "unit": "MEASUREMENT_UNIT_JOULES_PER_TERAHASH",
          "value": 23.54888841361529
        }
      ],
      "firmware_version": "1.8.0",
      "group_labels": [],
      "hashrate": [
        {
          "timestamp": "2026-06-15T19:56:23.372784Z",
          "unit": "MEASUREMENT_UNIT_TERAHASH_PER_SECOND",
          "value": 0
        }
      ],
      "ip_address": "192.168.2.11",
      "mac_address": "02:00:00:98:D8:73",
      "manufacturer": "Proto",
      "model": "Rig",
      "name": "Proto Rig",
      "pairing_status": "PAIRING_STATUS_PAIRED",
      "power_usage": [
        {
          "timestamp": "2026-06-15T19:56:23.372784Z",
          "unit": "MEASUREMENT_UNIT_KILOWATT",
          "value": 0.19108006401012412
        }
      ],
      "rack_label": "",
      "rack_position": "",
      "serial_number": "PROTO-SIM-001",
      "site_label": "",
      "temperature": [
        {
          "timestamp": "2026-06-15T19:56:23.372784Z",
          "unit": "MEASUREMENT_UNIT_CELSIUS",
          "value": 53.75446022133408
        }
      ],
      "temperature_status": "TEMPERATURE_STATUS_UNSPECIFIED",
      "timestamp": "2026-06-15T19:56:23.372784Z",
      "url": "http://192.168.2.11",
      "worker_name": "02:00:00:98:D8:73"
    }
  ],
  "models": [
    "Rig"
  ],
  "total_miners": 1,
  "total_state_counts": {
    "broken_count": 1,
    "hashing_count": 0,
    "offline_count": 0,
    "sleeping_count": 0
  }
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli groups list
{
  "collections": [
    {
      "created_at": "2026-06-15T19:49:14.964570Z",
      "description": "manual test",
      "device_count": 0,
      "id": "1",
      "label": "test-group",
      "type": "COLLECTION_TYPE_GROUP",
      "updated_at": "2026-06-15T19:49:14.964570Z"
    }
  ],
  "next_page_token": "",
  "total_count": 1
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % export GROUP_ID="1"
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli groups add-devices --collection-id "$GROUP_ID" --device "$DEVICE"
{
  "added_count": 1,
  "collection_id": "1",
  "site_reassigned_count": 0
}
🐚 kkurucz@BLKDKK622M7M3 port-fleet-client-api % fleetcli groups members --collection-id 1
{
  "members": [
    {
      "added_at": "2026-06-15T20:25:24.413222Z",
      "device_identifier": "019eccd3-8e1d-7e1d-9ff3-24f6cf2e4437"
    }
  ],
  "next_page_token": ""
}

E2E tests:

🐚 kkurucz@BLKDKK622M7M3 server % go test -v -tags=e2e ./e2e -run 'TestFleetCLI'
=== RUN   TestFleetCLIWorkflow
    fleetcli_e2e_test.go:38: Waiting for fleet-api to be ready...
    plugin_integration_test.go:428: ✓ Fleet API is healthy
    fleetcli_e2e_test.go:41: Building fleetcli test binary...
=== RUN   TestFleetCLIWorkflow/OnboardingCreateAdmin
    fleetcli_e2e_test.go:65: Fleet already onboarded (expected on re-runs)
=== RUN   TestFleetCLIWorkflow/PairingDiscover
    fleetcli_e2e_test.go:99: ✓ Discovered device: 019ecd19-316c-7c33-9108-9e011a380fc2 (driver: proto, ip: 192.168.2.7)
=== RUN   TestFleetCLIWorkflow/PairingPair
    fleetcli_e2e_test.go:113: ✓ Paired device: 019ecd19-316c-7c33-9108-9e011a380fc2
=== RUN   TestFleetCLIWorkflow/MinersList
    fleetcli_e2e_test.go:141: ✓ Miners list contains 1 miner(s) including the paired device
=== RUN   TestFleetCLIWorkflow/PerformanceGet
    fleetcli_e2e_test.go:154: ✓ Performance summary fetched
--- PASS: TestFleetCLIWorkflow (0.92s)
    --- PASS: TestFleetCLIWorkflow/OnboardingCreateAdmin (0.43s)
    --- PASS: TestFleetCLIWorkflow/PairingDiscover (0.10s)
    --- PASS: TestFleetCLIWorkflow/PairingPair (0.09s)
    --- PASS: TestFleetCLIWorkflow/MinersList (0.10s)
    --- PASS: TestFleetCLIWorkflow/PerformanceGet (0.10s)
=== RUN   TestFleetCLIFirmwareWorkflow
    fleetcli_firmware_e2e_test.go:33: Waiting for fleet-api to be ready...
    plugin_integration_test.go:428: ✓ Fleet API is healthy
    fleetcli_firmware_e2e_test.go:36: Building fleetcli test binary...
=== RUN   TestFleetCLIFirmwareWorkflow/Config
    fleetcli_firmware_e2e_test.go:81: ✓ Config: chunk_size=33554432 max_file_size=524288000 extensions=[.swu .tar.gz .zip]
=== RUN   TestFleetCLIFirmwareWorkflow/CheckBeforeUpload
    fleetcli_firmware_e2e_test.go:88: ✓ Check reports unknown checksum before upload
=== RUN   TestFleetCLIFirmwareWorkflow/UploadDirect
    fleetcli_firmware_e2e_test.go:97: ✓ Direct upload stored firmware file 019ecd1c-ffa2-7064-967b-ac1af4b366a9
=== RUN   TestFleetCLIFirmwareWorkflow/CheckAfterUpload
    fleetcli_firmware_e2e_test.go:106: ✓ Check finds the uploaded file by checksum
=== RUN   TestFleetCLIFirmwareWorkflow/UploadReusesExisting
    fleetcli_firmware_e2e_test.go:116: ✓ Re-upload reused the existing file
=== RUN   TestFleetCLIFirmwareWorkflow/UploadChunked
    fleetcli_firmware_e2e_test.go:141: ✓ Chunked upload stored firmware file 019ecd1d-0175-74a5-9bad-4c3f51b06366 (34603008 bytes)
=== RUN   TestFleetCLIFirmwareWorkflow/List
    fleetcli_firmware_e2e_test.go:159: ✓ List contains both uploads (2 file(s) total)
=== RUN   TestFleetCLIFirmwareWorkflow/Delete
    fleetcli_firmware_e2e_test.go:176: ✓ Deleted firmware file 019ecd1c-ffa2-7064-967b-ac1af4b366a9
=== RUN   TestFleetCLIFirmwareWorkflow/DeleteAll
    fleetcli_firmware_e2e_test.go:192: ✓ Delete-all removed 1 file(s)
--- PASS: TestFleetCLIFirmwareWorkflow (2.08s)
    --- PASS: TestFleetCLIFirmwareWorkflow/Config (0.11s)
    --- PASS: TestFleetCLIFirmwareWorkflow/CheckBeforeUpload (0.09s)
    --- PASS: TestFleetCLIFirmwareWorkflow/UploadDirect (0.10s)
    --- PASS: TestFleetCLIFirmwareWorkflow/CheckAfterUpload (0.10s)
    --- PASS: TestFleetCLIFirmwareWorkflow/UploadReusesExisting (0.09s)
    --- PASS: TestFleetCLIFirmwareWorkflow/UploadChunked (0.39s)
    --- PASS: TestFleetCLIFirmwareWorkflow/List (0.09s)
    --- PASS: TestFleetCLIFirmwareWorkflow/Delete (0.19s)
    --- PASS: TestFleetCLIFirmwareWorkflow/DeleteAll (0.19s)
=== RUN   TestFleetCLISubcommands
    fleetcli_subcommands_e2e_test.go:30: Waiting for fleet-api to be ready...
    plugin_integration_test.go:428: ✓ Fleet API is healthy
    fleetcli_subcommands_e2e_test.go:33: Building fleetcli test binary...
=== RUN   TestFleetCLISubcommands/AuthAndAPIKeys
=== RUN   TestFleetCLISubcommands/PairingAndMiners
=== RUN   TestFleetCLISubcommands/PerformanceAndNetworkInfo
=== RUN   TestFleetCLISubcommands/Groups
=== RUN   TestFleetCLISubcommands/Racks
=== RUN   TestFleetCLISubcommands/Pools
=== RUN   TestFleetCLISubcommands/Schedule
=== RUN   TestFleetCLISubcommands/MinerCommands
=== RUN   TestFleetCLISubcommands/MinerCommands/download-logs
=== RUN   TestFleetCLISubcommands/MinerCommands/set-cooling-mode
=== RUN   TestFleetCLISubcommands/MinerCommands/set-power-target
=== RUN   TestFleetCLISubcommands/MinerCommands/stop
=== RUN   TestFleetCLISubcommands/MinerCommands/start
--- PASS: TestFleetCLISubcommands (48.58s)
    --- PASS: TestFleetCLISubcommands/AuthAndAPIKeys (0.38s)
    --- PASS: TestFleetCLISubcommands/PairingAndMiners (0.30s)
    --- PASS: TestFleetCLISubcommands/PerformanceAndNetworkInfo (0.24s)
    --- PASS: TestFleetCLISubcommands/Groups (0.90s)
    --- PASS: TestFleetCLISubcommands/Racks (1.29s)
    --- PASS: TestFleetCLISubcommands/Pools (0.44s)
    --- PASS: TestFleetCLISubcommands/Schedule (0.78s)
    --- PASS: TestFleetCLISubcommands/MinerCommands (43.87s)
        --- PASS: TestFleetCLISubcommands/MinerCommands/download-logs (2.36s)
        --- PASS: TestFleetCLISubcommands/MinerCommands/set-cooling-mode (2.31s)
        --- PASS: TestFleetCLISubcommands/MinerCommands/set-power-target (2.34s)
        --- PASS: TestFleetCLISubcommands/MinerCommands/stop (2.34s)
        --- PASS: TestFleetCLISubcommands/MinerCommands/start (2.34s)
=== RUN   TestFleetCLILeafCommandCoverage
--- PASS: TestFleetCLILeafCommandCoverage (0.00s)
PASS
ok      github.com/block/proto-fleet/server/e2e 52.568s

Co-authored-by: Caleb Johnston calebjohnston@block.xyz

Copilot AI review requested due to automatic review settings June 15, 2026 21:14
@krisztiankurucz krisztiankurucz requested a review from a team as a code owner June 15, 2026 21:14
@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file automation server labels Jun 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Introduces a protobuf-driven generator for fleetcli, adds handwritten runtime support (auth/session, pairing streaming, firmware HTTP lifecycle), and expands automated validation via unit + e2e tests.

Changes:

  • Add server/tools/generate-fleet-cli generator (templates, overrides, report) and wire it into just gen.
  • Implement/extend server/cmd/fleetcli runtime (auth resolution, generated command runtime, pairing + firmware command groups).
  • Add extensive unit tests and e2e coverage for the CLI workflows.

Reviewed changes

Copilot reviewed 24 out of 36 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
server/tools/generate-fleet-cli/templates/fleet-group.gotmpl Template for generated per-group command files.
server/tools/generate-fleet-cli/templates/fleet-commands.gotmpl Template for generated command registry file.
server/tools/generate-fleet-cli/overrides.json Declarative mapping of services/methods to CLI groups and naming/auth overrides.
server/tools/generate-fleet-cli/main.go Core generator: loads descriptor set + overrides, analyzes requests, renders Go sources and a coverage report.
server/tools/generate-fleet-cli/main_test.go Unit tests for goPackageInfo behavior.
server/go.mod Adds direct deps used by fleetcli output/formatting and cli framework.
server/go.sum Records checksums for new dependencies.
server/e2e/plugin_integration_test.go Updates integration test auth flow (cookie session -> API key) and modernizes selector/telemetry calls.
server/e2e/curtailment_e2e_test.go Switches proto-sim discovery host to container-reachable hostname.
server/e2e/fleetcli_e2e_test.go New e2e test for core CLI workflow (onboarding -> discover -> pair -> miners -> performance).
server/e2e/fleetcli_firmware_e2e_test.go New e2e test covering firmware lifecycle via CLI.
server/e2e/fleetcli_subcommands_e2e_test.go New broad e2e test exercising many CLI subcommands and asserting leaf-command coverage.
server/docs/fleet-cli-generation.md Documents generator architecture, inputs, statuses, and workflow.
server/cmd/fleetcli/main.go Root CLI + manual command groups (auth/apikey/performance/firmware) and shared helpers.
server/cmd/fleetcli/client.go CLI HTTP/Connect client, auth/session handling, URL normalization, secure-cookie loopback support.
server/cmd/fleetcli/generated_runtime.go Shared runtime invoked by generated command expressions (auth dispatch + selector helpers).
server/cmd/fleetcli/pairing.go Handwritten pairing group (streaming discover + selector-based pair).
server/cmd/fleetcli/firmware_client.go Firmware REST client for config/check/upload/list/delete endpoints.
server/cmd/fleetcli/firmware.go Handwritten firmware command group and upload orchestration + progress reporting.
server/cmd/fleetcli/manual.go Aggregates handwritten commands for registry composition.
server/cmd/fleetcli/main_test.go Unit tests for auth flag/env resolution + enum normalization + regression cases.
server/cmd/fleetcli/client_test.go Unit tests for base URL normalization and loopback secure-cookie semantics.
server/cmd/fleetcli/pairing_test.go Unit tests for discover/pair request builders and selector/credential behaviors.
server/cmd/fleetcli/firmware_test.go Unit tests for firmware config/check/upload modes, chunking, validation, and error shaping.
justfile Adds gen-fleet-cli / build-fleet-cli and runs generator as part of gen.
bin/scripts/fleetcli Convenience script that builds and execs the local CLI binary.
bin/hermit.hcl Adds scripts directory to PATH for hermit-managed environments.
Files not reviewed (9)
  • server/cmd/fleetcli/cmd_commands.go: Generated file
  • server/cmd/fleetcli/cmd_groups.go: Generated file
  • server/cmd/fleetcli/cmd_minercommand.go: Generated file
  • server/cmd/fleetcli/cmd_miners.go: Generated file
  • server/cmd/fleetcli/cmd_networkinfo.go: Generated file
  • server/cmd/fleetcli/cmd_onboarding.go: Generated file
  • server/cmd/fleetcli/cmd_pools.go: Generated file
  • server/cmd/fleetcli/cmd_racks.go: Generated file
  • server/cmd/fleetcli/cmd_schedule.go: Generated file

Comment thread server/cmd/fleetcli/firmware_client.go
Comment thread server/cmd/fleetcli/firmware_client.go Outdated
Comment thread server/cmd/fleetcli/main.go Outdated
Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
…ing unchanged files

Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
…atch

Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
…nputs through

Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
…e build output

Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
Signed-off-by: Krisztian Kurucz <kkurucz@squareup.com>
@krisztiankurucz krisztiankurucz force-pushed the kkurucz/port-fleet-client-api branch from c202bcd to d2f92ad Compare June 15, 2026 21:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c202bcd920

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/cmd/fleetcli/main.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f78fad713

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/cmd/fleetcli/main.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e2a7aed9bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/tools/generate-fleet-cli/main.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05605e994a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/cmd/fleetcli/firmware_client.go Outdated

@ankitgoswami ankitgoswami left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I’m very supportive of having a CLI. I think it would be useful for operators, scripting, debugging, and workflows where the UI is too heavy. My hesitation is mostly about timing and where the CLI should attach.

RFC 0001 moves plugin orchestration, miner I/O, discovery, pairing, and command execution toward fleet nodes. Given that, I think the long-term CLI probably wants to be designed around the fleet-node boundary rather than around today’s transitional server/combined-mode API shape.

The main upside of making the CLI run through fleet nodes is that it would match the architecture we’re already moving toward: commands execute near the miners, LAN-only devices stay behind the customer network, local/degraded-mode workflows become possible, and the CLI semantics line up with fleet-node ownership instead of cloud reachability. It also gives us a cleaner story for multi-site and air-gapped setups.

The trade-off is that it likely means waiting for more of the fleet-node surface to settle, and the first version may be less broad. There’s also more complexity around node selection, enrollment state, offline behavior, and explaining which operations are server-wide versus scoped to a specific node. But I think that complexity is inherent in the product direction anyway, so baking it into the CLI from the start seems better than shipping a large CLI now and reshaping it soon after.

@krisztiankurucz

Copy link
Copy Markdown
Author

I’m very supportive of having a CLI. I think it would be useful for operators, scripting, debugging, and workflows where the UI is too heavy. My hesitation is mostly about timing and where the CLI should attach.

RFC 0001 moves plugin orchestration, miner I/O, discovery, pairing, and command execution toward fleet nodes. Given that, I think the long-term CLI probably wants to be designed around the fleet-node boundary rather than around today’s transitional server/combined-mode API shape.

The main upside of making the CLI run through fleet nodes is that it would match the architecture we’re already moving toward: commands execute near the miners, LAN-only devices stay behind the customer network, local/degraded-mode workflows become possible, and the CLI semantics line up with fleet-node ownership instead of cloud reachability. It also gives us a cleaner story for multi-site and air-gapped setups.

The trade-off is that it likely means waiting for more of the fleet-node surface to settle, and the first version may be less broad. There’s also more complexity around node selection, enrollment state, offline behavior, and explaining which operations are server-wide versus scoped to a specific node. But I think that complexity is inherent in the product direction anyway, so baking it into the CLI from the start seems better than shipping a large CLI now and reshaping it soon after.

This fleetcli tool is a dependency for some additional testrack / rig reservation tooling I’m working on, so I still need a CLI surface for higher-level server / control-plane actions.

That said, I agree with the concern about not baking in today’s transitional server-centric model for miner-local operations. For this first version, I’m happy to narrow the scope to control-plane workflows only, and remove or defer miner I/O, discovery, pairing, and command execution while the fleet-node command surface solidifies.

Concretely, I’d keep things like auth/API key management, onboarding, groups/racks metadata, saved pool/config metadata, and read-only server state where appropriate. I’d defer pairing, minercommand, LAN/network discovery, and any command paths that imply direct miner execution or node-involved operations.

Longer term, once the fleet-node API is settled, we can bring those operational commands back with explicit node/site scoping instead of treating the server API as the permanent CLI boundary.

Does that make sense?

@ankitgoswami

Copy link
Copy Markdown
Contributor

yea, that makes sense to me. another thing to note here is that fleetnodes already have a CLI and will essentially be the way to communicate with a cloud hosted version of fleet. I'll review this once you have updated it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants