compshare-cli 0.3.3__tar.gz → 0.3.5__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 (58) hide show
  1. compshare_cli-0.3.5/PKG-INFO +109 -0
  2. compshare_cli-0.3.5/README.md +84 -0
  3. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/pyproject.toml +4 -1
  4. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/__init__.py +1 -1
  5. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/api.py +28 -13
  6. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/cli.py +202 -22
  7. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/doctor.py +5 -0
  8. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/image.py +9 -1
  9. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/instance.py +630 -2
  10. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/storage.py +1 -0
  11. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/team.py +25 -3
  12. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/config.py +46 -2
  13. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/i18n.py +119 -7
  14. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/location.py +2 -8
  15. compshare_cli-0.3.5/src/compshare_cli/output.py +362 -0
  16. compshare_cli-0.3.5/src/compshare_cli/remote_jobs.py +670 -0
  17. compshare_cli-0.3.5/src/compshare_cli.egg-info/PKG-INFO +109 -0
  18. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli.egg-info/SOURCES.txt +3 -0
  19. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_api.py +42 -0
  20. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_cli.py +789 -94
  21. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_config.py +29 -0
  22. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_feedback.py +5 -1
  23. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_location.py +1 -3
  24. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_output.py +50 -2
  25. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_qa.py +7 -2
  26. compshare_cli-0.3.5/tests/test_release_version.py +19 -0
  27. compshare_cli-0.3.5/tests/test_remote_jobs.py +230 -0
  28. compshare_cli-0.3.3/PKG-INFO +0 -312
  29. compshare_cli-0.3.3/README.md +0 -287
  30. compshare_cli-0.3.3/src/compshare_cli/output.py +0 -190
  31. compshare_cli-0.3.3/src/compshare_cli.egg-info/PKG-INFO +0 -312
  32. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/LICENSE +0 -0
  33. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/setup.cfg +0 -0
  34. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/__main__.py +0 -0
  35. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/actions.py +0 -0
  36. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/__init__.py +0 -0
  37. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/ask.py +0 -0
  38. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/common.py +0 -0
  39. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/feedback.py +0 -0
  40. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/errors.py +0 -0
  41. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/insights.py +0 -0
  42. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/instance_templates.py +0 -0
  43. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/parsing.py +0 -0
  44. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/qa.py +0 -0
  45. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/runtime.py +0 -0
  46. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/sdk.py +0 -0
  47. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/ssh.py +0 -0
  48. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/ssh_cache.py +0 -0
  49. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli.egg-info/dependency_links.txt +0 -0
  50. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli.egg-info/entry_points.txt +0 -0
  51. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli.egg-info/requires.txt +0 -0
  52. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli.egg-info/top_level.txt +0 -0
  53. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_actions.py +0 -0
  54. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_instance_templates.py +0 -0
  55. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_parsing.py +0 -0
  56. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_sdk.py +0 -0
  57. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_ssh.py +0 -0
  58. {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_ssh_cache.py +0 -0
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: compshare-cli
3
+ Version: 0.3.5
4
+ Summary: Command-line client for CompShare GPU compute
5
+ Author: CompShare
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/BennielAllan/compshare-cli
8
+ Project-URL: Repository, https://github.com/BennielAllan/compshare-cli
9
+ Project-URL: Changelog, https://github.com/BennielAllan/compshare-cli/blob/master/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/BennielAllan/compshare-cli/issues
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: click<9,>=8.1
15
+ Requires-Dist: pexpect<5,>=4.9; platform_system != "Windows"
16
+ Requires-Dist: rich<15,>=13
17
+ Requires-Dist: typer<1,>=0.12
18
+ Requires-Dist: ucloud-sdk-python3<0.12,>=0.11.103
19
+ Requires-Dist: urllib3<2,>=1.26.18; python_version < "3.10" and platform_system == "Darwin"
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest<10,>=8; extra == "dev"
22
+ Requires-Dist: pytest-cov<8,>=5; extra == "dev"
23
+ Requires-Dist: ruff<1,>=0.6; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # CompShare CLI
27
+
28
+ 在终端管理优云智算 GPU 实例、镜像、云盘、US3 挂载和团队资源。
29
+
30
+ - 交互式创建实例,实时查询规格、库存和价格
31
+ - 支持批量生命周期操作、等待状态和跨地域查询
32
+ - 支持可断线恢复的远程任务、状态查询和增量日志读取
33
+ - 支持社区/自定义镜像、云盘、团队额度与账单
34
+ - 默认中文帮助,可切换英文;所有命令支持 JSON 输出
35
+ - 可通过 `compshare ask` 查询产品使用和计费知识
36
+ - 可通过 `compshare feedback` 反馈 CLI 问题和建议
37
+
38
+ ## 安装
39
+
40
+ ```bash
41
+ pip install compshare-cli
42
+ pip install --upgrade compshare-cli
43
+ ```
44
+
45
+ ## 配置
46
+
47
+ ```bash
48
+ compshare config --name default
49
+ ```
50
+
51
+ 凭证保存在 `~/.config/compshare/config.json`,支持多个配置:
52
+
53
+ ```bash
54
+ compshare config list
55
+ compshare config use production
56
+ compshare --profile production instance list
57
+ ```
58
+
59
+ 也可以使用环境变量:
60
+
61
+ ```bash
62
+ export COMPSHARE_PUBLIC_KEY='...'
63
+ export COMPSHARE_PRIVATE_KEY='...'
64
+ ```
65
+
66
+ ## 快速开始
67
+
68
+ ```bash
69
+ # 查询、创建和查看实例
70
+ compshare instance search --region cn-sh2 --zone cn-sh2-02
71
+ compshare instance create
72
+ compshare instance list --all
73
+
74
+ # 连接实例
75
+ compshare instance ssh INSTANCE_ID
76
+
77
+ # 查看帮助
78
+ compshare -h
79
+ compshare instance -h
80
+ ```
81
+
82
+ ## 命令概览
83
+
84
+ ```text
85
+ compshare config API 凭证配置
86
+ compshare feedback 反馈 CLI 问题或建议
87
+ compshare doctor 配置、鉴权、网络与 SSH 环境诊断
88
+ compshare ask 产品使用和计费问答
89
+ compshare instance GPU 实例、规格、库存、价格和生命周期
90
+ compshare image 平台、自定义、社区及共享镜像
91
+ compshare storage 云盘和 US3 挂载
92
+ compshare team 团队、邀请、成员额度、账单和审计
93
+ ```
94
+
95
+ ## Skill
96
+
97
+ 为 Codex、Claude Code、Cursor 等 AI Agent 安装 CompShare CLI Skill:
98
+
99
+ ```bash
100
+ npx skills add compshare-cn/compshare-cli --skill compshare-cli
101
+ ```
102
+
103
+ 完整说明见 [CompShare CLI Skill](skills/compshare-cli/SKILL.md)。
104
+
105
+ ## SDK
106
+
107
+ 如需通过 SDK 集成 CompShare API,可参考独立的
108
+ [CompShare Developer Examples](https://github.com/ucloud/compshare-developer-examples) 仓库。
109
+ 其中包含 Python、Go、Java、JavaScript 和 PHP 示例,覆盖 API 鉴权、GPU 实例创建、规格配置和状态查询。
@@ -0,0 +1,84 @@
1
+ # CompShare CLI
2
+
3
+ 在终端管理优云智算 GPU 实例、镜像、云盘、US3 挂载和团队资源。
4
+
5
+ - 交互式创建实例,实时查询规格、库存和价格
6
+ - 支持批量生命周期操作、等待状态和跨地域查询
7
+ - 支持可断线恢复的远程任务、状态查询和增量日志读取
8
+ - 支持社区/自定义镜像、云盘、团队额度与账单
9
+ - 默认中文帮助,可切换英文;所有命令支持 JSON 输出
10
+ - 可通过 `compshare ask` 查询产品使用和计费知识
11
+ - 可通过 `compshare feedback` 反馈 CLI 问题和建议
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ pip install compshare-cli
17
+ pip install --upgrade compshare-cli
18
+ ```
19
+
20
+ ## 配置
21
+
22
+ ```bash
23
+ compshare config --name default
24
+ ```
25
+
26
+ 凭证保存在 `~/.config/compshare/config.json`,支持多个配置:
27
+
28
+ ```bash
29
+ compshare config list
30
+ compshare config use production
31
+ compshare --profile production instance list
32
+ ```
33
+
34
+ 也可以使用环境变量:
35
+
36
+ ```bash
37
+ export COMPSHARE_PUBLIC_KEY='...'
38
+ export COMPSHARE_PRIVATE_KEY='...'
39
+ ```
40
+
41
+ ## 快速开始
42
+
43
+ ```bash
44
+ # 查询、创建和查看实例
45
+ compshare instance search --region cn-sh2 --zone cn-sh2-02
46
+ compshare instance create
47
+ compshare instance list --all
48
+
49
+ # 连接实例
50
+ compshare instance ssh INSTANCE_ID
51
+
52
+ # 查看帮助
53
+ compshare -h
54
+ compshare instance -h
55
+ ```
56
+
57
+ ## 命令概览
58
+
59
+ ```text
60
+ compshare config API 凭证配置
61
+ compshare feedback 反馈 CLI 问题或建议
62
+ compshare doctor 配置、鉴权、网络与 SSH 环境诊断
63
+ compshare ask 产品使用和计费问答
64
+ compshare instance GPU 实例、规格、库存、价格和生命周期
65
+ compshare image 平台、自定义、社区及共享镜像
66
+ compshare storage 云盘和 US3 挂载
67
+ compshare team 团队、邀请、成员额度、账单和审计
68
+ ```
69
+
70
+ ## Skill
71
+
72
+ 为 Codex、Claude Code、Cursor 等 AI Agent 安装 CompShare CLI Skill:
73
+
74
+ ```bash
75
+ npx skills add compshare-cn/compshare-cli --skill compshare-cli
76
+ ```
77
+
78
+ 完整说明见 [CompShare CLI Skill](skills/compshare-cli/SKILL.md)。
79
+
80
+ ## SDK
81
+
82
+ 如需通过 SDK 集成 CompShare API,可参考独立的
83
+ [CompShare Developer Examples](https://github.com/ucloud/compshare-developer-examples) 仓库。
84
+ 其中包含 Python、Go、Java、JavaScript 和 PHP 示例,覆盖 API 鉴权、GPU 实例创建、规格配置和状态查询。
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "compshare-cli"
7
- version = "0.3.3"
7
+ dynamic = ["version"]
8
8
  description = "Command-line client for CompShare GPU compute"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -39,6 +39,9 @@ dev = [
39
39
  compshare = "compshare_cli.cli:main"
40
40
  compshare-ssh-askpass = "compshare_cli.ssh:askpass"
41
41
 
42
+ [tool.setuptools.dynamic]
43
+ version = { attr = "compshare_cli.__version__" }
44
+
42
45
  [tool.setuptools.packages.find]
43
46
  where = ["src"]
44
47
 
@@ -1,3 +1,3 @@
1
1
  """CompShare command-line client."""
2
2
 
3
- __version__ = "0.3.3"
3
+ __version__ = "0.3.5"
@@ -52,11 +52,12 @@ def invoke(
52
52
  continue
53
53
  raise
54
54
  except CLIError as error:
55
- renderer.error(str(error))
55
+ renderer.coded_error("configuration_error", str(error))
56
56
  raise typer.Exit(1) from error
57
57
  except ucloud_exc.RetCodeException as error:
58
58
  hint = error_hint(error.action or action, error.code)
59
- renderer.error(
59
+ renderer.coded_error(
60
+ "api_error",
60
61
  _with_hint(error.message or str(error), hint),
61
62
  details={
62
63
  "action": error.action,
@@ -67,10 +68,10 @@ def invoke(
67
68
  )
68
69
  raise typer.Exit(1) from error
69
70
  except ucloud_exc.UCloudException as error:
70
- renderer.error(str(error))
71
+ renderer.coded_error("transport_error", str(error))
71
72
  raise typer.Exit(1) from error
72
73
  except Exception as error: # SDK transport exceptions are not all UCloudException subclasses.
73
- renderer.error(str(error))
74
+ renderer.coded_error("transport_error", str(error))
74
75
  raise typer.Exit(1) from error
75
76
 
76
77
  if success:
@@ -79,7 +80,7 @@ def invoke(
79
80
  renderer.data(response)
80
81
  else:
81
82
  rows = row_builder(response) if row_builder else response.get(list_key or "") or []
82
- renderer.data(response, rows=rows, columns=columns)
83
+ renderer.data(response, rows=rows, columns=columns, json_list=True)
83
84
  return response
84
85
 
85
86
 
@@ -88,11 +89,14 @@ def call(runtime: Runtime, action: str, params: Dict[str, Any]) -> Dict[str, Any
88
89
  try:
89
90
  return _sdk(runtime, params).invoke(action, params)
90
91
  except CLIError as error:
91
- Renderer(runtime.json_output, runtime.show_sensitive).error(str(error))
92
+ Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
93
+ "configuration_error", str(error)
94
+ )
92
95
  raise typer.Exit(1) from error
93
96
  except ucloud_exc.RetCodeException as error:
94
97
  hint = error_hint(error.action or action, error.code)
95
- Renderer(runtime.json_output, runtime.show_sensitive).error(
98
+ Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
99
+ "api_error",
96
100
  _with_hint(error.message or str(error), hint),
97
101
  details={
98
102
  "action": error.action,
@@ -103,10 +107,14 @@ def call(runtime: Runtime, action: str, params: Dict[str, Any]) -> Dict[str, Any
103
107
  )
104
108
  raise typer.Exit(1) from error
105
109
  except ucloud_exc.UCloudException as error:
106
- Renderer(runtime.json_output, runtime.show_sensitive).error(str(error))
110
+ Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
111
+ "transport_error", str(error)
112
+ )
107
113
  raise typer.Exit(1) from error
108
114
  except Exception as error:
109
- Renderer(runtime.json_output, runtime.show_sensitive).error(str(error))
115
+ Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
116
+ "transport_error", str(error)
117
+ )
110
118
  raise typer.Exit(1) from error
111
119
 
112
120
 
@@ -195,11 +203,14 @@ def download_file(
195
203
  try:
196
204
  return _sdk(runtime, params).download(action, params)
197
205
  except CLIError as error:
198
- Renderer(runtime.json_output, runtime.show_sensitive).error(str(error))
206
+ Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
207
+ "configuration_error", str(error)
208
+ )
199
209
  raise typer.Exit(1) from error
200
210
  except ucloud_exc.RetCodeException as error:
201
211
  hint = error_hint(error.action or action, error.code)
202
- Renderer(runtime.json_output, runtime.show_sensitive).error(
212
+ Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
213
+ "api_error",
203
214
  _with_hint(error.message or str(error), hint),
204
215
  details={
205
216
  "action": error.action or action,
@@ -210,10 +221,14 @@ def download_file(
210
221
  )
211
222
  raise typer.Exit(1) from error
212
223
  except ucloud_exc.UCloudException as error:
213
- Renderer(runtime.json_output, runtime.show_sensitive).error(str(error))
224
+ Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
225
+ "transport_error", str(error)
226
+ )
214
227
  raise typer.Exit(1) from error
215
228
  except Exception as error:
216
- Renderer(runtime.json_output, runtime.show_sensitive).error(str(error))
229
+ Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
230
+ "transport_error", str(error)
231
+ )
217
232
  raise typer.Exit(1) from error
218
233
 
219
234
 
@@ -1,11 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
- from typing import List, Optional
4
+ from typing import Any, Dict, List, Optional
5
5
 
6
6
  import click
7
7
  import typer
8
8
  from typer import core as typer_core
9
+ from typer.completion import completion_init, install_callback
9
10
  from typer.main import get_command
10
11
 
11
12
  from compshare_cli import __version__
@@ -15,7 +16,7 @@ from compshare_cli.commands import feedback as feedback_command
15
16
  from compshare_cli.commands import image, instance, storage, team
16
17
  from compshare_cli.commands.common import confirm
17
18
  from compshare_cli.config import DEFAULT_PROFILE, ConfigStore, Profile
18
- from compshare_cli.errors import CLIError, UsageError
19
+ from compshare_cli.errors import CLIError, ConfigError, UsageError
19
20
  from compshare_cli.i18n import configured_language, localize_command, normalize_language, tr
20
21
  from compshare_cli.insights import record_command
21
22
  from compshare_cli.output import Renderer
@@ -26,6 +27,8 @@ _TYPER_CLICK_EXCEPTIONS = getattr(_TYPER_CLICK, "exceptions", _TYPER_CLICK)
26
27
  _CLICK_EXCEPTIONS = (click.ClickException, _TYPER_CLICK_EXCEPTIONS.ClickException)
27
28
  _ABORT_EXCEPTIONS = (click.Abort, _TYPER_CLICK_EXCEPTIONS.Abort)
28
29
 
30
+ completion_init()
31
+
29
32
 
30
33
  class RootGroup(typer_core.TyperGroup):
31
34
  def list_commands(self, ctx):
@@ -42,7 +45,7 @@ app = typer.Typer(
42
45
  help="Manage CompShare GPU compute from the terminal.",
43
46
  cls=RootGroup,
44
47
  no_args_is_help=True,
45
- add_completion=True,
48
+ add_completion=False,
46
49
  context_settings={"help_option_names": ["-h", "--help"]},
47
50
  )
48
51
  app.add_typer(instance.app, name="instance")
@@ -57,9 +60,12 @@ config_app = typer.Typer(
57
60
  app.add_typer(config_app, name="config")
58
61
 
59
62
 
60
- def _version_callback(value: bool) -> None:
63
+ def _version_callback(ctx: typer.Context, value: bool) -> None:
61
64
  if value:
62
- typer.echo(__version__)
65
+ if bool(ctx.params.get("json_output")):
66
+ Renderer(True, bool(ctx.params.get("show_sensitive"))).data({"version": __version__})
67
+ else:
68
+ typer.echo(__version__)
63
69
  raise typer.Exit()
64
70
 
65
71
 
@@ -89,11 +95,13 @@ def root(
89
95
  json_output: bool = typer.Option(
90
96
  False,
91
97
  "--json",
98
+ is_eager=True,
92
99
  help="Emit machine-readable JSON.",
93
100
  ),
94
101
  show_sensitive: bool = typer.Option(
95
102
  False,
96
103
  "--show-sensitive",
104
+ is_eager=True,
97
105
  help="Show passwords, IP addresses, access URLs, and login commands.",
98
106
  ),
99
107
  version_option: bool = typer.Option(
@@ -103,6 +111,13 @@ def root(
103
111
  is_eager=True,
104
112
  help="Print the CLI version and exit.",
105
113
  ),
114
+ install_completion: bool = typer.Option(
115
+ None,
116
+ "--install-completion",
117
+ callback=install_callback,
118
+ expose_value=False,
119
+ help="Install completion for the current shell.",
120
+ ),
106
121
  ) -> None:
107
122
  """CompShare CLI."""
108
123
  ctx.obj = Runtime(
@@ -182,13 +197,54 @@ def config_set(
182
197
  def config_list(ctx: typer.Context) -> None:
183
198
  """List credential profiles."""
184
199
  store = ConfigStore()
185
- current = store.current_profile()
186
- profiles = [{"Profile": name, "Active": name == current} for name in store.list_profiles()]
187
200
  state = ctx.find_root().obj
201
+ status = store.credential_status(state.profile_name)
202
+ source = status["credential_source"]
203
+ selected = status["selected_profile"]
204
+ active_profile = selected if source in {"profile", "mixed"} else None
205
+ profiles = [
206
+ {
207
+ "Profile": name,
208
+ "Active": name == active_profile,
209
+ "Source": "mixed" if name == active_profile and source == "mixed" else "profile",
210
+ }
211
+ for name in store.list_profiles()
212
+ ]
213
+ rows = [
214
+ {
215
+ **profile,
216
+ "Source": tr("Profile and environment")
217
+ if profile["Source"] == "mixed"
218
+ else tr("Profile file"),
219
+ }
220
+ for profile in profiles
221
+ ]
222
+ if source == "environment":
223
+ rows.insert(
224
+ 0,
225
+ {"Profile": "-", "Active": True, "Source": tr("Environment variables")},
226
+ )
227
+ elif source in {"unconfigured", "incomplete"} and not any(
228
+ profile["Active"] for profile in profiles
229
+ ):
230
+ rows.insert(
231
+ 0,
232
+ {
233
+ "Profile": selected,
234
+ "Active": False,
235
+ "Source": tr("Incomplete credentials")
236
+ if source == "incomplete"
237
+ else tr("Not configured"),
238
+ },
239
+ )
188
240
  Renderer(state.json_output, state.show_sensitive).data(
189
- {"current_profile": current, "profiles": profiles},
190
- rows=profiles,
191
- columns=(("Profile", "PROFILE"), ("Active", "ACTIVE")),
241
+ {
242
+ **status,
243
+ "current_profile": active_profile,
244
+ "profiles": profiles,
245
+ },
246
+ rows=rows,
247
+ columns=(("Profile", "PROFILE"), ("Active", "ACTIVE"), ("Source", "SOURCE")),
192
248
  )
193
249
 
194
250
 
@@ -270,6 +326,53 @@ def feedback(
270
326
  feedback_command.run(ctx.find_root().obj, category, message)
271
327
 
272
328
 
329
+ def _global_flag_requested(argv: List[str], option: str) -> bool:
330
+ for token in argv:
331
+ if token == "--":
332
+ break
333
+ if token == option:
334
+ return True
335
+ return False
336
+
337
+
338
+ def _normalize_global_options(argv: List[str]) -> List[str]:
339
+ """Move unambiguous root options before the command without crossing --."""
340
+ global_options: List[str] = []
341
+ remaining: List[str] = []
342
+ profile_seen = False
343
+ index = 0
344
+ while index < len(argv):
345
+ token = argv[index]
346
+ if token == "--":
347
+ remaining.extend(argv[index:])
348
+ break
349
+ if token in {"--json", "--show-sensitive"}:
350
+ global_options.append(token)
351
+ index += 1
352
+ continue
353
+ if token == "--profile":
354
+ if profile_seen:
355
+ raise UsageError(tr("Option --profile may only be specified once."))
356
+ profile_seen = True
357
+ if index + 1 >= len(argv) or argv[index + 1].startswith("-"):
358
+ raise UsageError(tr("Option --profile requires a value."))
359
+ global_options.extend((token, argv[index + 1]))
360
+ index += 2
361
+ continue
362
+ if token.startswith("--profile="):
363
+ if profile_seen:
364
+ raise UsageError(tr("Option --profile may only be specified once."))
365
+ profile_seen = True
366
+ if not token.partition("=")[2]:
367
+ raise UsageError(tr("Option --profile requires a value."))
368
+ global_options.append(token)
369
+ index += 1
370
+ continue
371
+ remaining.append(token)
372
+ index += 1
373
+ return [*global_options, *remaining]
374
+
375
+
273
376
  def _command_path(command: click.Command, argv: List[str]) -> Optional[str]:
274
377
  if not argv or "-h" in argv or "--help" in argv:
275
378
  return None
@@ -298,6 +401,77 @@ def _command_path(command: click.Command, argv: List[str]) -> Optional[str]:
298
401
  return ".".join(path) or None
299
402
 
300
403
 
404
+ def _json_help_requested(argv: List[str]) -> bool:
405
+ for token in argv:
406
+ if token == "--":
407
+ return False
408
+ if token in {"-h", "--help"}:
409
+ return True
410
+ return False
411
+
412
+
413
+ def _json_help_payload(command: click.Command, argv: List[str]) -> Dict[str, Any]:
414
+ """Build machine-readable help without invoking Rich's terminal renderer."""
415
+ current = command
416
+ path: List[str] = []
417
+ skip_next = False
418
+ for token in argv:
419
+ if skip_next:
420
+ skip_next = False
421
+ continue
422
+ if token in {"-h", "--help", "--"}:
423
+ break
424
+ if token in {"--profile", "--lang"}:
425
+ skip_next = True
426
+ continue
427
+ if token.startswith(("--profile=", "--lang=")) or token in {
428
+ "--json",
429
+ "--show-sensitive",
430
+ }:
431
+ continue
432
+ if isinstance(current, click.Group) and token in current.commands:
433
+ current = current.commands[token]
434
+ path.append(token)
435
+
436
+ parameters: List[Dict[str, Any]] = []
437
+ for parameter in current.params:
438
+ if isinstance(parameter, click.Option):
439
+ parameters.append(
440
+ {
441
+ "kind": "option",
442
+ "name": parameter.name,
443
+ "flags": [*parameter.opts, *parameter.secondary_opts],
444
+ "required": parameter.required,
445
+ "multiple": parameter.multiple,
446
+ "help": parameter.help,
447
+ }
448
+ )
449
+ elif isinstance(parameter, click.Argument):
450
+ parameters.append(
451
+ {
452
+ "kind": "argument",
453
+ "name": parameter.name,
454
+ "required": parameter.required,
455
+ "multiple": parameter.nargs != 1,
456
+ }
457
+ )
458
+ commands = []
459
+ if isinstance(current, click.Group):
460
+ commands = [
461
+ {"name": name, "help": child.get_short_help_str()}
462
+ for name in current.list_commands(click.Context(current))
463
+ if (child := current.get_command(click.Context(current), name)) is not None
464
+ and not child.hidden
465
+ ]
466
+ command_name = " ".join(["compshare", *path])
467
+ return {
468
+ "command_path": command_name,
469
+ "help": current.help,
470
+ "parameters": parameters,
471
+ "commands": commands,
472
+ }
473
+
474
+
301
475
  def _requested_language(argv: List[str]) -> Optional[str]:
302
476
  """Return the last root --lang value before the command, if present."""
303
477
  requested: Optional[str] = None
@@ -338,8 +512,10 @@ def main(args: Optional[List[str]] = None) -> None:
338
512
  if not argv:
339
513
  argv = ["-h"]
340
514
  telemetry_command: Optional[str] = None
341
- show_sensitive = "--show-sensitive" in argv
515
+ json_output = _global_flag_requested(argv, "--json")
516
+ show_sensitive = _global_flag_requested(argv, "--show-sensitive")
342
517
  try:
518
+ argv = _normalize_global_options(argv)
343
519
  requested_language = _requested_language(argv)
344
520
  if requested_language is None:
345
521
  language = configured_language()
@@ -347,23 +523,27 @@ def main(args: Optional[List[str]] = None) -> None:
347
523
  language = normalize_language(requested_language)
348
524
  ConfigStore().save_language(language)
349
525
  command = localize_command(get_command(app), language)
350
- telemetry_command = _command_path(command, original_argv)
351
- result = command.main(args=argv, prog_name="compshare", standalone_mode=False)
526
+ telemetry_command = _command_path(command, argv)
527
+ if json_output and _json_help_requested(argv):
528
+ Renderer(True, show_sensitive).data(_json_help_payload(command, argv))
529
+ result = None
530
+ else:
531
+ result = command.main(args=argv, prog_name="compshare", standalone_mode=False)
532
+ except ConfigError as error:
533
+ Renderer(json_output, show_sensitive).coded_error("configuration_error", str(error))
534
+ raise SystemExit(2) from error
535
+ except UsageError as error:
536
+ Renderer(json_output, show_sensitive).coded_error("invalid_usage", str(error))
537
+ raise SystemExit(2) from error
352
538
  except CLIError as error:
353
- Renderer("--json" in argv, show_sensitive).error(str(error))
539
+ Renderer(json_output, show_sensitive).coded_error("cli_error", str(error))
354
540
  raise SystemExit(2) from error
355
541
  except _CLICK_EXCEPTIONS as error:
356
542
  message = error.format_message()
357
- if "--json" in argv and "No such option: --json" in message:
358
- hint = tr(
359
- "Global option --json must appear before the command, for example: "
360
- "compshare --json instance list."
361
- )
362
- message = f"{message} {hint}"
363
- Renderer("--json" in argv, show_sensitive).error(message)
543
+ Renderer(json_output, show_sensitive).coded_error("invalid_usage", message)
364
544
  raise SystemExit(error.exit_code) from error
365
545
  except _ABORT_EXCEPTIONS as error:
366
- Renderer("--json" in argv, show_sensitive).error(tr("Aborted"))
546
+ Renderer(json_output, show_sensitive).coded_error("aborted", tr("Aborted"))
367
547
  raise SystemExit(1) from error
368
548
  finally:
369
549
  if telemetry_command:
@@ -127,6 +127,11 @@ def run(state: Runtime) -> None:
127
127
 
128
128
  ok = not any(item["Status"] == "Failed" for item in checks)
129
129
  payload = {"ok": ok, "checks": checks}
130
+ if not ok:
131
+ payload["error"] = {
132
+ "code": "diagnostic_failed",
133
+ "message": tr("One or more diagnostic checks failed."),
134
+ }
130
135
  Renderer(state.json_output, state.show_sensitive).data(
131
136
  payload,
132
137
  rows=checks,
@@ -250,6 +250,8 @@ def list_images(
250
250
  response,
251
251
  rows=rows,
252
252
  columns=IMAGE_COLUMNS,
253
+ json_list=True,
254
+ metadata={"source": source, "all": all_results},
253
255
  )
254
256
 
255
257
 
@@ -293,7 +295,13 @@ def show(
293
295
  ("PRICE/H", item.get("Price")),
294
296
  ("DESCRIPTION", item.get("Description")),
295
297
  ],
296
- response=response,
298
+ response={
299
+ "Action": response.get("Action"),
300
+ "RetCode": response.get("RetCode"),
301
+ "request_uuid": response.get("request_uuid"),
302
+ "source": source,
303
+ "image": item,
304
+ },
297
305
  )
298
306
 
299
307