proxctl 0.2.4__tar.gz → 0.2.6__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.
- {proxctl-0.2.4 → proxctl-0.2.6}/CHANGELOG.md +14 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/Cargo.lock +1 -1
- {proxctl-0.2.4 → proxctl-0.2.6}/Cargo.toml +1 -1
- {proxctl-0.2.4 → proxctl-0.2.6}/PKG-INFO +1 -1
- {proxctl-0.2.4 → proxctl-0.2.6}/src/api/error.rs +32 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/backup.rs +16 -4
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/container/lifecycle.rs +14 -5
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/container/mod.rs +10 -1
- proxctl-0.2.6/src/commands/list_args.rs +67 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/mod.rs +1 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/node.rs +24 -6
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/pool.rs +20 -6
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/storage.rs +20 -4
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/task.rs +18 -5
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/vm/lifecycle.rs +14 -5
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/vm/mod.rs +10 -1
- {proxctl-0.2.4 → proxctl-0.2.6}/src/main.rs +32 -11
- {proxctl-0.2.4 → proxctl-0.2.6}/src/schema.rs +34 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/.github/workflows/ci.yml +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/.github/workflows/release.yml +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/.gitignore +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/LICENSE +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/Makefile +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/README.md +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/prek.toml +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/proxctl_py/__init__.py +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/proxctl_py/__main__.py +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/pyproject.toml +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/api/client.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/api/mod.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/api/token.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/api/types.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/access.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/api.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/apply/container.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/apply/diff.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/apply/firewall.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/apply/manifest.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/apply/mod.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/apply/reconciler.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/apply/vm.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/ceph.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/cluster.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/container/config.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/container/firewall.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/container/migrate.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/container/snapshot.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/export.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/firewall.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/vm/agent.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/vm/cloudinit.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/vm/config.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/vm/firewall.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/vm/migrate.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/commands/vm/snapshot.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/lib.rs +0 -0
- {proxctl-0.2.4 → proxctl-0.2.6}/src/output.rs +0 -0
|
@@ -6,6 +6,20 @@ All notable changes to this project will be documented in this file.
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [0.2.6](https://github.com/rvben/proxctl/compare/v0.2.5...v0.2.6) - 2026-04-03
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- add --limit, --offset, --fields to all list commands ([0ecd684](https://github.com/rvben/proxctl/commit/0ecd6844273d697dcc1f7247f26743d6ac8252af))
|
|
16
|
+
|
|
17
|
+
## [0.2.5](https://github.com/rvben/proxctl/compare/v0.2.4...v0.2.5) - 2026-04-03
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- add error kinds to schema and structured JSON errors on stderr ([b46b7d6](https://github.com/rvben/proxctl/commit/b46b7d67d0a40412ffb90771e221e5dbf9b08f73))
|
|
22
|
+
|
|
9
23
|
## [0.2.4](https://github.com/rvben/proxctl/compare/v0.2.3...v0.2.4) - 2026-04-03
|
|
10
24
|
|
|
11
25
|
### Added
|
|
@@ -40,6 +40,19 @@ pub enum Error {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
impl Error {
|
|
43
|
+
/// Returns a machine-readable error kind string for structured error output.
|
|
44
|
+
pub fn kind(&self) -> &'static str {
|
|
45
|
+
match self {
|
|
46
|
+
Error::Config(_) => "config",
|
|
47
|
+
Error::Auth(_) => "auth",
|
|
48
|
+
Error::NotFound(_) => "not_found",
|
|
49
|
+
Error::Api { .. } | Error::TaskFailed(_) => "api",
|
|
50
|
+
Error::Conflict(_) => "conflict",
|
|
51
|
+
Error::Timeout(_) => "timeout",
|
|
52
|
+
Error::Http(_) | Error::Other(_) => "other",
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
pub fn exit_code(&self) -> i32 {
|
|
44
57
|
match self {
|
|
45
58
|
Error::Config(_) => exit_codes::CONFIG_ERROR,
|
|
@@ -174,6 +187,25 @@ mod tests {
|
|
|
174
187
|
assert_eq!(exit_codes::TIMEOUT, 7);
|
|
175
188
|
}
|
|
176
189
|
|
|
190
|
+
#[test]
|
|
191
|
+
fn test_kind_maps_correctly() {
|
|
192
|
+
assert_eq!(Error::Config("x".into()).kind(), "config");
|
|
193
|
+
assert_eq!(Error::Auth("x".into()).kind(), "auth");
|
|
194
|
+
assert_eq!(Error::NotFound("x".into()).kind(), "not_found");
|
|
195
|
+
assert_eq!(
|
|
196
|
+
Error::Api {
|
|
197
|
+
status: 500,
|
|
198
|
+
message: "x".into()
|
|
199
|
+
}
|
|
200
|
+
.kind(),
|
|
201
|
+
"api"
|
|
202
|
+
);
|
|
203
|
+
assert_eq!(Error::TaskFailed("x".into()).kind(), "api");
|
|
204
|
+
assert_eq!(Error::Conflict("x".into()).kind(), "conflict");
|
|
205
|
+
assert_eq!(Error::Timeout("x".into()).kind(), "timeout");
|
|
206
|
+
assert_eq!(Error::Other("x".into()).kind(), "other");
|
|
207
|
+
}
|
|
208
|
+
|
|
177
209
|
#[test]
|
|
178
210
|
fn test_error_display_config() {
|
|
179
211
|
let err = Error::Config("missing host".to_string());
|
|
@@ -4,6 +4,7 @@ use serde_json::json;
|
|
|
4
4
|
|
|
5
5
|
use crate::api::Error;
|
|
6
6
|
use crate::api::client::ProxmoxClient;
|
|
7
|
+
use crate::commands::list_args::ListArgs;
|
|
7
8
|
use crate::output::{OutputConfig, use_color};
|
|
8
9
|
|
|
9
10
|
fn require_node<'a>(node: Option<&'a str>, global_node: Option<&'a str>) -> Result<&'a str, Error> {
|
|
@@ -69,6 +70,8 @@ pub enum BackupCommand {
|
|
|
69
70
|
/// Node name
|
|
70
71
|
#[arg(long)]
|
|
71
72
|
node: Option<String>,
|
|
73
|
+
#[command(flatten)]
|
|
74
|
+
list: ListArgs,
|
|
72
75
|
},
|
|
73
76
|
/// Create a backup
|
|
74
77
|
Create {
|
|
@@ -128,9 +131,10 @@ pub async fn run(
|
|
|
128
131
|
vmid,
|
|
129
132
|
storage,
|
|
130
133
|
node,
|
|
134
|
+
list: list_args,
|
|
131
135
|
} => {
|
|
132
136
|
let n = require_node(node.as_deref(), global_node)?;
|
|
133
|
-
list(client, out, n, vmid, storage.as_deref()).await
|
|
137
|
+
list(client, out, n, vmid, storage.as_deref(), &list_args).await
|
|
134
138
|
}
|
|
135
139
|
BackupCommand::Create {
|
|
136
140
|
vmid,
|
|
@@ -206,6 +210,7 @@ async fn list(
|
|
|
206
210
|
node: &str,
|
|
207
211
|
vmid_filter: Option<u32>,
|
|
208
212
|
storage_filter: Option<&str>,
|
|
213
|
+
list_args: &ListArgs,
|
|
209
214
|
) -> Result<(), Error> {
|
|
210
215
|
// Find backup-capable storages
|
|
211
216
|
let storages: Vec<serde_json::Value> = client.get(&format!("/nodes/{node}/storage")).await?;
|
|
@@ -253,12 +258,19 @@ async fn list(
|
|
|
253
258
|
});
|
|
254
259
|
}
|
|
255
260
|
|
|
261
|
+
let total = all_backups.len();
|
|
262
|
+
|
|
256
263
|
if out.json {
|
|
257
|
-
|
|
264
|
+
let paginated: Vec<serde_json::Value> = list_args.paginate(&all_backups).to_vec();
|
|
265
|
+
let paginated = list_args.filter_fields(paginated);
|
|
266
|
+
let envelope = list_args.paginated_json(&paginated, total);
|
|
267
|
+
out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
|
|
258
268
|
return Ok(());
|
|
259
269
|
}
|
|
260
270
|
|
|
261
|
-
|
|
271
|
+
let page = list_args.paginate(&all_backups);
|
|
272
|
+
|
|
273
|
+
if page.is_empty() {
|
|
262
274
|
out.print_message("No backups found.");
|
|
263
275
|
return Ok(());
|
|
264
276
|
}
|
|
@@ -273,7 +285,7 @@ async fn list(
|
|
|
273
285
|
println!("{header}");
|
|
274
286
|
println!("{}", "-".repeat(total_w));
|
|
275
287
|
}
|
|
276
|
-
for b in
|
|
288
|
+
for b in page {
|
|
277
289
|
let volid = b.get("volid").and_then(|v| v.as_str()).unwrap_or("-");
|
|
278
290
|
let vmid = b
|
|
279
291
|
.get("vmid")
|
|
@@ -3,6 +3,7 @@ use serde_json::json;
|
|
|
3
3
|
|
|
4
4
|
use crate::api::Error;
|
|
5
5
|
use crate::api::client::ProxmoxClient;
|
|
6
|
+
use crate::commands::list_args::ListArgs;
|
|
6
7
|
use crate::output::{OutputConfig, use_color};
|
|
7
8
|
|
|
8
9
|
/// Format bytes as a human-readable string (e.g. "2.00 GiB").
|
|
@@ -47,6 +48,7 @@ pub async fn list(
|
|
|
47
48
|
node: Option<&str>,
|
|
48
49
|
status_filter: Option<&str>,
|
|
49
50
|
pool_filter: Option<&str>,
|
|
51
|
+
list_args: &ListArgs,
|
|
50
52
|
) -> Result<(), Error> {
|
|
51
53
|
let containers: Vec<serde_json::Value> = if let Some(n) = node {
|
|
52
54
|
let items: Vec<serde_json::Value> = client.get(&format!("/nodes/{n}/lxc")).await?;
|
|
@@ -69,8 +71,8 @@ pub async fn list(
|
|
|
69
71
|
.collect()
|
|
70
72
|
};
|
|
71
73
|
|
|
72
|
-
let containers: Vec
|
|
73
|
-
.
|
|
74
|
+
let containers: Vec<serde_json::Value> = containers
|
|
75
|
+
.into_iter()
|
|
74
76
|
.filter(|v| {
|
|
75
77
|
if let Some(sf) = status_filter {
|
|
76
78
|
let s = v.get("status").and_then(|x| x.as_str()).unwrap_or("");
|
|
@@ -88,12 +90,19 @@ pub async fn list(
|
|
|
88
90
|
})
|
|
89
91
|
.collect();
|
|
90
92
|
|
|
93
|
+
let total = containers.len();
|
|
94
|
+
|
|
91
95
|
if out.json {
|
|
92
|
-
|
|
96
|
+
let paginated: Vec<serde_json::Value> = list_args.paginate(&containers).to_vec();
|
|
97
|
+
let paginated = list_args.filter_fields(paginated);
|
|
98
|
+
let envelope = list_args.paginated_json(&paginated, total);
|
|
99
|
+
out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
|
|
93
100
|
return Ok(());
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
|
|
103
|
+
let page = list_args.paginate(&containers);
|
|
104
|
+
|
|
105
|
+
if page.is_empty() {
|
|
97
106
|
out.print_message("No containers found.");
|
|
98
107
|
return Ok(());
|
|
99
108
|
}
|
|
@@ -112,7 +121,7 @@ pub async fn list(
|
|
|
112
121
|
println!("{}", "-".repeat(total_w));
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
for ct in
|
|
124
|
+
for ct in page {
|
|
116
125
|
let vmid = ct.get("vmid").and_then(|v| v.as_u64()).unwrap_or(0);
|
|
117
126
|
let name = ct
|
|
118
127
|
.get("name")
|
|
@@ -8,6 +8,7 @@ use clap::Subcommand;
|
|
|
8
8
|
|
|
9
9
|
use crate::api::Error;
|
|
10
10
|
use crate::api::client::ProxmoxClient;
|
|
11
|
+
use crate::commands::list_args::ListArgs;
|
|
11
12
|
use crate::output::OutputConfig;
|
|
12
13
|
|
|
13
14
|
#[derive(Subcommand)]
|
|
@@ -105,6 +106,8 @@ pub enum ContainerCommand {
|
|
|
105
106
|
/// Filter by pool
|
|
106
107
|
#[arg(long)]
|
|
107
108
|
pool: Option<String>,
|
|
109
|
+
#[command(flatten)]
|
|
110
|
+
list: ListArgs,
|
|
108
111
|
},
|
|
109
112
|
/// Show container status
|
|
110
113
|
Status {
|
|
@@ -314,7 +317,12 @@ pub async fn run(
|
|
|
314
317
|
global_node: Option<&str>,
|
|
315
318
|
) -> Result<(), Error> {
|
|
316
319
|
match cmd {
|
|
317
|
-
ContainerCommand::List {
|
|
320
|
+
ContainerCommand::List {
|
|
321
|
+
node,
|
|
322
|
+
status,
|
|
323
|
+
pool,
|
|
324
|
+
list,
|
|
325
|
+
} => {
|
|
318
326
|
let effective_node = node.as_deref().or(global_node);
|
|
319
327
|
lifecycle::list(
|
|
320
328
|
client,
|
|
@@ -322,6 +330,7 @@ pub async fn run(
|
|
|
322
330
|
effective_node,
|
|
323
331
|
status.as_deref(),
|
|
324
332
|
pool.as_deref(),
|
|
333
|
+
&list,
|
|
325
334
|
)
|
|
326
335
|
.await
|
|
327
336
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use serde_json::json;
|
|
4
|
+
|
|
5
|
+
/// Shared arguments for list commands providing bounded output support.
|
|
6
|
+
#[derive(clap::Args, Clone, Debug)]
|
|
7
|
+
pub struct ListArgs {
|
|
8
|
+
/// Maximum number of items to return
|
|
9
|
+
#[arg(long)]
|
|
10
|
+
pub limit: Option<usize>,
|
|
11
|
+
|
|
12
|
+
/// Number of items to skip
|
|
13
|
+
#[arg(long, default_value = "0")]
|
|
14
|
+
pub offset: usize,
|
|
15
|
+
|
|
16
|
+
/// Comma-separated list of fields to include in JSON output
|
|
17
|
+
#[arg(long)]
|
|
18
|
+
pub fields: Option<String>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl ListArgs {
|
|
22
|
+
/// Apply offset and limit to a slice, returning a paginated subset.
|
|
23
|
+
pub fn paginate<'a, T>(&self, items: &'a [T]) -> &'a [T] {
|
|
24
|
+
let start = self.offset.min(items.len());
|
|
25
|
+
let remaining = &items[start..];
|
|
26
|
+
match self.limit {
|
|
27
|
+
Some(limit) => &remaining[..limit.min(remaining.len())],
|
|
28
|
+
None => remaining,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Filter JSON values to only include specified fields.
|
|
33
|
+
/// Returns items unchanged if no fields filter is set.
|
|
34
|
+
pub fn filter_fields(&self, items: Vec<serde_json::Value>) -> Vec<serde_json::Value> {
|
|
35
|
+
let fields = match &self.fields {
|
|
36
|
+
Some(f) => f,
|
|
37
|
+
None => return items,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let field_set: HashSet<&str> = fields.split(',').map(|f| f.trim()).collect();
|
|
41
|
+
items
|
|
42
|
+
.into_iter()
|
|
43
|
+
.map(|item| {
|
|
44
|
+
if let Some(obj) = item.as_object() {
|
|
45
|
+
let filtered: serde_json::Map<String, serde_json::Value> = obj
|
|
46
|
+
.iter()
|
|
47
|
+
.filter(|(k, _)| field_set.contains(k.as_str()))
|
|
48
|
+
.map(|(k, v)| (k.clone(), v.clone()))
|
|
49
|
+
.collect();
|
|
50
|
+
serde_json::Value::Object(filtered)
|
|
51
|
+
} else {
|
|
52
|
+
item
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
.collect()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Wrap items in a pagination envelope for JSON output.
|
|
59
|
+
pub fn paginated_json(&self, items: &[serde_json::Value], total: usize) -> serde_json::Value {
|
|
60
|
+
json!({
|
|
61
|
+
"items": items,
|
|
62
|
+
"total": total,
|
|
63
|
+
"offset": self.offset,
|
|
64
|
+
"limit": self.limit,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -4,6 +4,7 @@ use serde_json::json;
|
|
|
4
4
|
|
|
5
5
|
use crate::api::Error;
|
|
6
6
|
use crate::api::client::ProxmoxClient;
|
|
7
|
+
use crate::commands::list_args::ListArgs;
|
|
7
8
|
use crate::output::{OutputConfig, use_color};
|
|
8
9
|
|
|
9
10
|
#[derive(Subcommand)]
|
|
@@ -89,7 +90,10 @@ pub enum CertificateCommand {
|
|
|
89
90
|
#[derive(Subcommand)]
|
|
90
91
|
pub enum NodeCommand {
|
|
91
92
|
/// List all nodes
|
|
92
|
-
List
|
|
93
|
+
List {
|
|
94
|
+
#[command(flatten)]
|
|
95
|
+
list: ListArgs,
|
|
96
|
+
},
|
|
93
97
|
/// Show node status
|
|
94
98
|
Status {
|
|
95
99
|
/// Node name (uses default node if not specified)
|
|
@@ -221,7 +225,7 @@ pub async fn run(
|
|
|
221
225
|
global_node: Option<&str>,
|
|
222
226
|
) -> Result<(), Error> {
|
|
223
227
|
match cmd {
|
|
224
|
-
NodeCommand::List => list(client, out).await,
|
|
228
|
+
NodeCommand::List { list: list_args } => list(client, out, &list_args).await,
|
|
225
229
|
NodeCommand::Status { node } => {
|
|
226
230
|
let n = require_node(node.as_deref(), global_node)?;
|
|
227
231
|
status(client, out, n).await
|
|
@@ -270,15 +274,29 @@ pub async fn run(
|
|
|
270
274
|
}
|
|
271
275
|
}
|
|
272
276
|
|
|
273
|
-
async fn list(
|
|
277
|
+
async fn list(
|
|
278
|
+
client: &ProxmoxClient,
|
|
279
|
+
out: OutputConfig,
|
|
280
|
+
list_args: &ListArgs,
|
|
281
|
+
) -> Result<(), Error> {
|
|
274
282
|
let nodes = client.list_nodes().await?;
|
|
283
|
+
let total = nodes.len();
|
|
275
284
|
|
|
276
285
|
if out.json {
|
|
277
|
-
|
|
286
|
+
let values: Vec<serde_json::Value> = nodes
|
|
287
|
+
.iter()
|
|
288
|
+
.map(|n| serde_json::to_value(n).expect("serialize node"))
|
|
289
|
+
.collect();
|
|
290
|
+
let paginated: Vec<serde_json::Value> = list_args.paginate(&values).to_vec();
|
|
291
|
+
let paginated = list_args.filter_fields(paginated);
|
|
292
|
+
let envelope = list_args.paginated_json(&paginated, total);
|
|
293
|
+
out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
|
|
278
294
|
return Ok(());
|
|
279
295
|
}
|
|
280
296
|
|
|
281
|
-
|
|
297
|
+
let page = list_args.paginate(&nodes);
|
|
298
|
+
|
|
299
|
+
if page.is_empty() {
|
|
282
300
|
out.print_message("No nodes found.");
|
|
283
301
|
return Ok(());
|
|
284
302
|
}
|
|
@@ -297,7 +315,7 @@ async fn list(client: &ProxmoxClient, out: OutputConfig) -> Result<(), Error> {
|
|
|
297
315
|
println!("{}", "-".repeat(total_w));
|
|
298
316
|
}
|
|
299
317
|
|
|
300
|
-
for node in
|
|
318
|
+
for node in page {
|
|
301
319
|
let hours = node.uptime / 3600;
|
|
302
320
|
let days = hours / 24;
|
|
303
321
|
let uptime_str = if days > 0 {
|
|
@@ -4,6 +4,7 @@ use serde_json::json;
|
|
|
4
4
|
|
|
5
5
|
use crate::api::Error;
|
|
6
6
|
use crate::api::client::ProxmoxClient;
|
|
7
|
+
use crate::commands::list_args::ListArgs;
|
|
7
8
|
use crate::output::{OutputConfig, use_color};
|
|
8
9
|
|
|
9
10
|
fn confirm_action(action: &str, yes: bool) -> Result<(), Error> {
|
|
@@ -25,7 +26,10 @@ fn confirm_action(action: &str, yes: bool) -> Result<(), Error> {
|
|
|
25
26
|
#[derive(Subcommand)]
|
|
26
27
|
pub enum PoolCommand {
|
|
27
28
|
/// List resource pools
|
|
28
|
-
List
|
|
29
|
+
List {
|
|
30
|
+
#[command(flatten)]
|
|
31
|
+
list: ListArgs,
|
|
32
|
+
},
|
|
29
33
|
/// Show pool details
|
|
30
34
|
Show {
|
|
31
35
|
/// Pool ID
|
|
@@ -70,7 +74,7 @@ pub async fn run(
|
|
|
70
74
|
_global_node: Option<&str>,
|
|
71
75
|
) -> Result<(), Error> {
|
|
72
76
|
match cmd {
|
|
73
|
-
PoolCommand::List => list(client, out).await,
|
|
77
|
+
PoolCommand::List { list: list_args } => list(client, out, &list_args).await,
|
|
74
78
|
PoolCommand::Show { poolid } => show(client, out, &poolid).await,
|
|
75
79
|
PoolCommand::Create { poolid, comment } => {
|
|
76
80
|
create(client, out, &poolid, comment.as_deref()).await
|
|
@@ -95,15 +99,25 @@ pub async fn run(
|
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
async fn list(
|
|
102
|
+
async fn list(
|
|
103
|
+
client: &ProxmoxClient,
|
|
104
|
+
out: OutputConfig,
|
|
105
|
+
list_args: &ListArgs,
|
|
106
|
+
) -> Result<(), Error> {
|
|
99
107
|
let data: Vec<serde_json::Value> = client.get("/pools").await?;
|
|
108
|
+
let total = data.len();
|
|
100
109
|
|
|
101
110
|
if out.json {
|
|
102
|
-
|
|
111
|
+
let paginated: Vec<serde_json::Value> = list_args.paginate(&data).to_vec();
|
|
112
|
+
let paginated = list_args.filter_fields(paginated);
|
|
113
|
+
let envelope = list_args.paginated_json(&paginated, total);
|
|
114
|
+
out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
|
|
103
115
|
return Ok(());
|
|
104
116
|
}
|
|
105
117
|
|
|
106
|
-
|
|
118
|
+
let page = list_args.paginate(&data);
|
|
119
|
+
|
|
120
|
+
if page.is_empty() {
|
|
107
121
|
out.print_message("No resource pools found.");
|
|
108
122
|
return Ok(());
|
|
109
123
|
}
|
|
@@ -118,7 +132,7 @@ async fn list(client: &ProxmoxClient, out: OutputConfig) -> Result<(), Error> {
|
|
|
118
132
|
println!("{header}");
|
|
119
133
|
println!("{}", "-".repeat(total_w));
|
|
120
134
|
}
|
|
121
|
-
for pool in
|
|
135
|
+
for pool in page {
|
|
122
136
|
let poolid = pool.get("poolid").and_then(|v| v.as_str()).unwrap_or("-");
|
|
123
137
|
let comment = pool.get("comment").and_then(|v| v.as_str()).unwrap_or("");
|
|
124
138
|
if color {
|
|
@@ -4,6 +4,7 @@ use serde_json::json;
|
|
|
4
4
|
|
|
5
5
|
use crate::api::Error;
|
|
6
6
|
use crate::api::client::ProxmoxClient;
|
|
7
|
+
use crate::commands::list_args::ListArgs;
|
|
7
8
|
use crate::output::{OutputConfig, use_color};
|
|
8
9
|
|
|
9
10
|
fn require_node<'a>(node: Option<&'a str>, global_node: Option<&'a str>) -> Result<&'a str, Error> {
|
|
@@ -37,6 +38,8 @@ pub enum StorageCommand {
|
|
|
37
38
|
/// Filter by storage type
|
|
38
39
|
#[arg(long, rename_all = "kebab-case")]
|
|
39
40
|
r#type: Option<String>,
|
|
41
|
+
#[command(flatten)]
|
|
42
|
+
list: ListArgs,
|
|
40
43
|
},
|
|
41
44
|
/// Show storage status
|
|
42
45
|
Status {
|
|
@@ -142,12 +145,17 @@ pub async fn run(
|
|
|
142
145
|
global_node: Option<&str>,
|
|
143
146
|
) -> Result<(), Error> {
|
|
144
147
|
match cmd {
|
|
145
|
-
StorageCommand::List {
|
|
148
|
+
StorageCommand::List {
|
|
149
|
+
node,
|
|
150
|
+
r#type,
|
|
151
|
+
list: list_args,
|
|
152
|
+
} => {
|
|
146
153
|
list(
|
|
147
154
|
client,
|
|
148
155
|
out,
|
|
149
156
|
node.as_deref().or(global_node),
|
|
150
157
|
r#type.as_deref(),
|
|
158
|
+
&list_args,
|
|
151
159
|
)
|
|
152
160
|
.await
|
|
153
161
|
}
|
|
@@ -231,6 +239,7 @@ async fn list(
|
|
|
231
239
|
out: OutputConfig,
|
|
232
240
|
node: Option<&str>,
|
|
233
241
|
type_filter: Option<&str>,
|
|
242
|
+
list_args: &ListArgs,
|
|
234
243
|
) -> Result<(), Error> {
|
|
235
244
|
let mut data: Vec<serde_json::Value> = client.get("/storage").await?;
|
|
236
245
|
|
|
@@ -250,12 +259,19 @@ async fn list(
|
|
|
250
259
|
});
|
|
251
260
|
}
|
|
252
261
|
|
|
262
|
+
let total = data.len();
|
|
263
|
+
|
|
253
264
|
if out.json {
|
|
254
|
-
|
|
265
|
+
let paginated: Vec<serde_json::Value> = list_args.paginate(&data).to_vec();
|
|
266
|
+
let paginated = list_args.filter_fields(paginated);
|
|
267
|
+
let envelope = list_args.paginated_json(&paginated, total);
|
|
268
|
+
out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
|
|
255
269
|
return Ok(());
|
|
256
270
|
}
|
|
257
271
|
|
|
258
|
-
|
|
272
|
+
let page = list_args.paginate(&data);
|
|
273
|
+
|
|
274
|
+
if page.is_empty() {
|
|
259
275
|
out.print_message("No storage pools found.");
|
|
260
276
|
return Ok(());
|
|
261
277
|
}
|
|
@@ -273,7 +289,7 @@ async fn list(
|
|
|
273
289
|
println!("{header}");
|
|
274
290
|
println!("{}", "-".repeat(total_w));
|
|
275
291
|
}
|
|
276
|
-
for s in
|
|
292
|
+
for s in page {
|
|
277
293
|
let name = s.get("storage").and_then(|v| v.as_str()).unwrap_or("-");
|
|
278
294
|
let stype = s.get("type").and_then(|v| v.as_str()).unwrap_or("-");
|
|
279
295
|
let content = s.get("content").and_then(|v| v.as_str()).unwrap_or("-");
|
|
@@ -4,6 +4,7 @@ use serde_json::json;
|
|
|
4
4
|
|
|
5
5
|
use crate::api::Error;
|
|
6
6
|
use crate::api::client::{ProxmoxClient, parse_upid_node};
|
|
7
|
+
use crate::commands::list_args::ListArgs;
|
|
7
8
|
use crate::output::{OutputConfig, use_color};
|
|
8
9
|
|
|
9
10
|
#[derive(Subcommand)]
|
|
@@ -19,6 +20,8 @@ pub enum TaskCommand {
|
|
|
19
20
|
/// Filter by status (running, error, ok)
|
|
20
21
|
#[arg(long)]
|
|
21
22
|
status: Option<String>,
|
|
23
|
+
#[command(flatten)]
|
|
24
|
+
list: ListArgs,
|
|
22
25
|
},
|
|
23
26
|
/// Show task status
|
|
24
27
|
Status {
|
|
@@ -56,6 +59,7 @@ pub async fn run(
|
|
|
56
59
|
node,
|
|
57
60
|
source,
|
|
58
61
|
status,
|
|
62
|
+
list: list_args,
|
|
59
63
|
} => {
|
|
60
64
|
list(
|
|
61
65
|
client,
|
|
@@ -63,6 +67,7 @@ pub async fn run(
|
|
|
63
67
|
node.as_deref().or(global_node),
|
|
64
68
|
source.as_deref(),
|
|
65
69
|
status.as_deref(),
|
|
70
|
+
&list_args,
|
|
66
71
|
)
|
|
67
72
|
.await
|
|
68
73
|
}
|
|
@@ -79,6 +84,7 @@ async fn list(
|
|
|
79
84
|
node: Option<&str>,
|
|
80
85
|
source: Option<&str>,
|
|
81
86
|
status_filter: Option<&str>,
|
|
87
|
+
list_args: &ListArgs,
|
|
82
88
|
) -> Result<(), Error> {
|
|
83
89
|
let data: Vec<serde_json::Value> = if let Some(n) = node {
|
|
84
90
|
let mut path = format!("/nodes/{n}/tasks");
|
|
@@ -101,8 +107,8 @@ async fn list(
|
|
|
101
107
|
};
|
|
102
108
|
|
|
103
109
|
// Apply client-side status filter for ok/error
|
|
104
|
-
let data: Vec
|
|
105
|
-
.
|
|
110
|
+
let data: Vec<serde_json::Value> = data
|
|
111
|
+
.into_iter()
|
|
106
112
|
.filter(|t| {
|
|
107
113
|
if let Some(sf) = status_filter {
|
|
108
114
|
match sf {
|
|
@@ -128,12 +134,19 @@ async fn list(
|
|
|
128
134
|
})
|
|
129
135
|
.collect();
|
|
130
136
|
|
|
137
|
+
let total = data.len();
|
|
138
|
+
|
|
131
139
|
if out.json {
|
|
132
|
-
|
|
140
|
+
let paginated: Vec<serde_json::Value> = list_args.paginate(&data).to_vec();
|
|
141
|
+
let paginated = list_args.filter_fields(paginated);
|
|
142
|
+
let envelope = list_args.paginated_json(&paginated, total);
|
|
143
|
+
out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
|
|
133
144
|
return Ok(());
|
|
134
145
|
}
|
|
135
146
|
|
|
136
|
-
|
|
147
|
+
let page = list_args.paginate(&data);
|
|
148
|
+
|
|
149
|
+
if page.is_empty() {
|
|
137
150
|
out.print_message("No tasks found.");
|
|
138
151
|
return Ok(());
|
|
139
152
|
}
|
|
@@ -151,7 +164,7 @@ async fn list(
|
|
|
151
164
|
println!("{header}");
|
|
152
165
|
println!("{}", "-".repeat(total_w));
|
|
153
166
|
}
|
|
154
|
-
for task in
|
|
167
|
+
for task in page {
|
|
155
168
|
let node_name = task.get("node").and_then(|v| v.as_str()).unwrap_or("-");
|
|
156
169
|
let task_type = task.get("type").and_then(|v| v.as_str()).unwrap_or("-");
|
|
157
170
|
let id = task.get("id").and_then(|v| v.as_str()).unwrap_or("-");
|
|
@@ -3,6 +3,7 @@ use serde_json::json;
|
|
|
3
3
|
|
|
4
4
|
use crate::api::Error;
|
|
5
5
|
use crate::api::client::ProxmoxClient;
|
|
6
|
+
use crate::commands::list_args::ListArgs;
|
|
6
7
|
use crate::output::{OutputConfig, use_color};
|
|
7
8
|
|
|
8
9
|
/// Format bytes as a human-readable string (e.g. "2.00 GiB").
|
|
@@ -47,6 +48,7 @@ pub async fn list(
|
|
|
47
48
|
node: Option<&str>,
|
|
48
49
|
status_filter: Option<&str>,
|
|
49
50
|
pool_filter: Option<&str>,
|
|
51
|
+
list_args: &ListArgs,
|
|
50
52
|
) -> Result<(), Error> {
|
|
51
53
|
let vms: Vec<serde_json::Value> = if let Some(n) = node {
|
|
52
54
|
let items: Vec<serde_json::Value> = client.get(&format!("/nodes/{n}/qemu")).await?;
|
|
@@ -71,8 +73,8 @@ pub async fn list(
|
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
// Apply filters
|
|
74
|
-
let vms: Vec
|
|
75
|
-
.
|
|
76
|
+
let vms: Vec<serde_json::Value> = vms
|
|
77
|
+
.into_iter()
|
|
76
78
|
.filter(|v| {
|
|
77
79
|
if let Some(sf) = status_filter {
|
|
78
80
|
let s = v.get("status").and_then(|x| x.as_str()).unwrap_or("");
|
|
@@ -90,12 +92,19 @@ pub async fn list(
|
|
|
90
92
|
})
|
|
91
93
|
.collect();
|
|
92
94
|
|
|
95
|
+
let total = vms.len();
|
|
96
|
+
|
|
93
97
|
if out.json {
|
|
94
|
-
|
|
98
|
+
let paginated: Vec<serde_json::Value> = list_args.paginate(&vms).to_vec();
|
|
99
|
+
let paginated = list_args.filter_fields(paginated);
|
|
100
|
+
let envelope = list_args.paginated_json(&paginated, total);
|
|
101
|
+
out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
|
|
95
102
|
return Ok(());
|
|
96
103
|
}
|
|
97
104
|
|
|
98
|
-
|
|
105
|
+
let page = list_args.paginate(&vms);
|
|
106
|
+
|
|
107
|
+
if page.is_empty() {
|
|
99
108
|
out.print_message("No virtual machines found.");
|
|
100
109
|
return Ok(());
|
|
101
110
|
}
|
|
@@ -114,7 +123,7 @@ pub async fn list(
|
|
|
114
123
|
println!("{}", "-".repeat(total_w));
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
for vm in
|
|
126
|
+
for vm in page {
|
|
118
127
|
let vmid = vm.get("vmid").and_then(|v| v.as_u64()).unwrap_or(0);
|
|
119
128
|
let name = vm.get("name").and_then(|v| v.as_str()).unwrap_or("-");
|
|
120
129
|
let status = vm
|
|
@@ -10,6 +10,7 @@ use clap::Subcommand;
|
|
|
10
10
|
|
|
11
11
|
use crate::api::Error;
|
|
12
12
|
use crate::api::client::ProxmoxClient;
|
|
13
|
+
use crate::commands::list_args::ListArgs;
|
|
13
14
|
use crate::output::OutputConfig;
|
|
14
15
|
|
|
15
16
|
#[derive(Subcommand)]
|
|
@@ -173,6 +174,8 @@ pub enum VmCommand {
|
|
|
173
174
|
/// Filter by pool
|
|
174
175
|
#[arg(long)]
|
|
175
176
|
pool: Option<String>,
|
|
177
|
+
#[command(flatten)]
|
|
178
|
+
list: ListArgs,
|
|
176
179
|
},
|
|
177
180
|
/// Show VM status
|
|
178
181
|
Status {
|
|
@@ -393,7 +396,12 @@ pub async fn run(
|
|
|
393
396
|
global_node: Option<&str>,
|
|
394
397
|
) -> Result<(), Error> {
|
|
395
398
|
match cmd {
|
|
396
|
-
VmCommand::List {
|
|
399
|
+
VmCommand::List {
|
|
400
|
+
node,
|
|
401
|
+
status,
|
|
402
|
+
pool,
|
|
403
|
+
list,
|
|
404
|
+
} => {
|
|
397
405
|
let effective_node = node.as_deref().or(global_node);
|
|
398
406
|
lifecycle::list(
|
|
399
407
|
client,
|
|
@@ -401,6 +409,7 @@ pub async fn run(
|
|
|
401
409
|
effective_node,
|
|
402
410
|
status.as_deref(),
|
|
403
411
|
pool.as_deref(),
|
|
412
|
+
&list,
|
|
404
413
|
)
|
|
405
414
|
.await
|
|
406
415
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
use std::io::IsTerminal;
|
|
1
2
|
use std::path::PathBuf;
|
|
2
3
|
use std::process;
|
|
3
4
|
|
|
@@ -6,7 +7,6 @@ use clap_complete::Shell;
|
|
|
6
7
|
use serde_json::json;
|
|
7
8
|
|
|
8
9
|
use proxctl::api::client::ProxmoxClient;
|
|
9
|
-
use proxctl::api::error::exit_codes;
|
|
10
10
|
use proxctl::api::token::ApiToken;
|
|
11
11
|
use proxctl::commands::access::AccessCommand;
|
|
12
12
|
use proxctl::commands::apply::ApplyCommand;
|
|
@@ -760,6 +760,22 @@ async fn run_config_init() -> Result<(), Error> {
|
|
|
760
760
|
Ok(())
|
|
761
761
|
}
|
|
762
762
|
|
|
763
|
+
/// Print an error to stderr. When stdout is not a TTY (piped), output
|
|
764
|
+
/// structured JSON so consuming agents can parse error details.
|
|
765
|
+
fn print_error(e: &Error) {
|
|
766
|
+
if std::io::stdout().is_terminal() {
|
|
767
|
+
eprintln!("Error: {e}");
|
|
768
|
+
} else {
|
|
769
|
+
let json = json!({
|
|
770
|
+
"error": {
|
|
771
|
+
"kind": e.kind(),
|
|
772
|
+
"message": e.to_string(),
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
eprintln!("{}", serde_json::to_string(&json).expect("serialize error"));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
763
779
|
// ── Main ────────────────────────────────────────────────────────────
|
|
764
780
|
|
|
765
781
|
#[tokio::main]
|
|
@@ -780,7 +796,7 @@ async fn main() {
|
|
|
780
796
|
}
|
|
781
797
|
Command::Config(ConfigCommand::Init) => {
|
|
782
798
|
if let Err(e) = run_config_init().await {
|
|
783
|
-
|
|
799
|
+
print_error(&e);
|
|
784
800
|
process::exit(e.exit_code());
|
|
785
801
|
}
|
|
786
802
|
return;
|
|
@@ -796,15 +812,19 @@ async fn main() {
|
|
|
796
812
|
let (cfg_host, cfg_token, cfg_insecure) = load_config(cli.profile.as_deref());
|
|
797
813
|
|
|
798
814
|
let host = cli.host.or(cfg_host).unwrap_or_else(|| {
|
|
799
|
-
|
|
800
|
-
|
|
815
|
+
let e = Error::Config(
|
|
816
|
+
"no host configured. Set --host, PROXMOX_HOST, or configure a profile.".to_string(),
|
|
817
|
+
);
|
|
818
|
+
print_error(&e);
|
|
819
|
+
process::exit(e.exit_code());
|
|
801
820
|
});
|
|
802
821
|
|
|
803
822
|
let token_str = cli.token.or(cfg_token).unwrap_or_else(|| {
|
|
804
|
-
|
|
805
|
-
"
|
|
823
|
+
let e = Error::Config(
|
|
824
|
+
"no token configured. Set --token, PROXMOX_TOKEN, or configure a profile.".to_string(),
|
|
806
825
|
);
|
|
807
|
-
|
|
826
|
+
print_error(&e);
|
|
827
|
+
process::exit(e.exit_code());
|
|
808
828
|
});
|
|
809
829
|
|
|
810
830
|
let insecure = cli.insecure || cfg_insecure.unwrap_or(false);
|
|
@@ -812,15 +832,16 @@ async fn main() {
|
|
|
812
832
|
let token: ApiToken = match token_str.parse() {
|
|
813
833
|
Ok(t) => t,
|
|
814
834
|
Err(e) => {
|
|
815
|
-
|
|
816
|
-
|
|
835
|
+
let err = Error::Config(e.to_string());
|
|
836
|
+
print_error(&err);
|
|
837
|
+
process::exit(err.exit_code());
|
|
817
838
|
}
|
|
818
839
|
};
|
|
819
840
|
|
|
820
841
|
let client = match ProxmoxClient::new(&host, token, insecure) {
|
|
821
842
|
Ok(c) => c,
|
|
822
843
|
Err(e) => {
|
|
823
|
-
|
|
844
|
+
print_error(&e);
|
|
824
845
|
process::exit(e.exit_code());
|
|
825
846
|
}
|
|
826
847
|
};
|
|
@@ -882,7 +903,7 @@ async fn main() {
|
|
|
882
903
|
};
|
|
883
904
|
|
|
884
905
|
if let Err(e) = result {
|
|
885
|
-
|
|
906
|
+
print_error(&e);
|
|
886
907
|
process::exit(e.exit_code());
|
|
887
908
|
}
|
|
888
909
|
}
|
|
@@ -184,6 +184,15 @@ pub fn generate(cmd: &clap::Command) -> Value {
|
|
|
184
184
|
"version": env!("CARGO_PKG_VERSION"),
|
|
185
185
|
"description": "CLI for Proxmox VE — manage VMs, containers, nodes, storage, and more",
|
|
186
186
|
"usage": "proxctl [OPTIONS] <COMMAND> [SUBCOMMAND] [ARGS]",
|
|
187
|
+
"errors": [
|
|
188
|
+
{"kind": "config", "retryable": false, "description": "Configuration error"},
|
|
189
|
+
{"kind": "auth", "retryable": false, "description": "Authentication failed"},
|
|
190
|
+
{"kind": "not_found", "retryable": false, "description": "Resource not found"},
|
|
191
|
+
{"kind": "api", "retryable": true, "description": "API error"},
|
|
192
|
+
{"kind": "conflict", "retryable": false, "description": "Resource conflict"},
|
|
193
|
+
{"kind": "timeout", "retryable": true, "description": "Operation timed out"},
|
|
194
|
+
{"kind": "other", "retryable": false, "description": "General error"}
|
|
195
|
+
],
|
|
187
196
|
"global_flags": {
|
|
188
197
|
"--host": {"type": "string", "env": "PROXMOX_HOST", "description": "Proxmox host (e.g., 192.168.1.25:8006)"},
|
|
189
198
|
"--token": {"type": "string", "env": "PROXMOX_TOKEN", "description": "API token (user@realm!tokenid=secret)"},
|
|
@@ -443,6 +452,7 @@ mod tests {
|
|
|
443
452
|
assert!(schema.get("version").is_some());
|
|
444
453
|
assert!(schema.get("global_flags").is_some());
|
|
445
454
|
assert!(schema.get("exit_codes").is_some());
|
|
455
|
+
assert!(schema.get("errors").is_some());
|
|
446
456
|
assert!(schema.get("commands").is_some());
|
|
447
457
|
assert!(schema.get("notes").is_some());
|
|
448
458
|
}
|
|
@@ -481,6 +491,30 @@ mod tests {
|
|
|
481
491
|
assert_eq!(enums, &[json!("fast"), json!("slow")]);
|
|
482
492
|
}
|
|
483
493
|
|
|
494
|
+
#[test]
|
|
495
|
+
fn schema_errors_array_has_all_kinds() {
|
|
496
|
+
let schema = generate(&test_cmd());
|
|
497
|
+
let errors = schema["errors"].as_array().unwrap();
|
|
498
|
+
assert_eq!(errors.len(), 7);
|
|
499
|
+
|
|
500
|
+
let kinds: Vec<&str> = errors.iter().map(|e| e["kind"].as_str().unwrap()).collect();
|
|
501
|
+
assert!(kinds.contains(&"config"));
|
|
502
|
+
assert!(kinds.contains(&"auth"));
|
|
503
|
+
assert!(kinds.contains(&"not_found"));
|
|
504
|
+
assert!(kinds.contains(&"api"));
|
|
505
|
+
assert!(kinds.contains(&"conflict"));
|
|
506
|
+
assert!(kinds.contains(&"timeout"));
|
|
507
|
+
assert!(kinds.contains(&"other"));
|
|
508
|
+
|
|
509
|
+
// Verify retryable fields
|
|
510
|
+
let api = errors.iter().find(|e| e["kind"] == "api").unwrap();
|
|
511
|
+
assert_eq!(api["retryable"], true);
|
|
512
|
+
let timeout = errors.iter().find(|e| e["kind"] == "timeout").unwrap();
|
|
513
|
+
assert_eq!(timeout["retryable"], true);
|
|
514
|
+
let config = errors.iter().find(|e| e["kind"] == "config").unwrap();
|
|
515
|
+
assert_eq!(config["retryable"], false);
|
|
516
|
+
}
|
|
517
|
+
|
|
484
518
|
#[test]
|
|
485
519
|
fn schema_handles_nested_subcommands() {
|
|
486
520
|
let schema = generate(&test_cmd());
|
|
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
|
|
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
|
|
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
|