minitest-cli 0.7.0__tar.gz → 0.8.0__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 (77) hide show
  1. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/PKG-INFO +15 -6
  2. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/README.md +14 -5
  3. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/pyproject.toml +1 -1
  4. minitest_cli-0.8.0/src/minitest_cli/api/apps_manager_client.py +89 -0
  5. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/api/client.py +4 -0
  6. minitest_cli-0.8.0/src/minitest_cli/commands/app_knowledge.py +145 -0
  7. minitest_cli-0.8.0/src/minitest_cli/commands/app_knowledge_helpers.py +99 -0
  8. minitest_cli-0.8.0/src/minitest_cli/commands/apps.py +174 -0
  9. minitest_cli-0.8.0/src/minitest_cli/commands/apps_helpers.py +97 -0
  10. minitest_cli-0.8.0/src/minitest_cli/commands/flow_types.py +99 -0
  11. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/user_story.py +36 -3
  12. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/user_story_helpers.py +19 -4
  13. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/user_story_modify.py +56 -12
  14. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/core/config.py +10 -0
  15. minitest_cli-0.8.0/src/minitest_cli/core/tenants.py +128 -0
  16. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/main.py +4 -0
  17. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/models/__init__.py +8 -1
  18. minitest_cli-0.8.0/src/minitest_cli/models/app.py +62 -0
  19. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/models/user_story.py +2 -0
  20. minitest_cli-0.8.0/tests/test_app_knowledge_commands.py +318 -0
  21. minitest_cli-0.8.0/tests/test_apps_commands.py +466 -0
  22. minitest_cli-0.7.0/tests/test_apps_commands.py → minitest_cli-0.8.0/tests/test_flow_types_commands.py +58 -53
  23. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/test_user_story_commands.py +223 -0
  24. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/uv.lock +1 -1
  25. minitest_cli-0.7.0/src/minitest_cli/commands/apps.py +0 -68
  26. minitest_cli-0.7.0/src/minitest_cli/models/app.py +0 -17
  27. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/.env.example +0 -0
  28. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/.github/workflows/ci.yml +0 -0
  29. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/.github/workflows/install-scripts.yml +0 -0
  30. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/.github/workflows/release.yml +0 -0
  31. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/.gitignore +0 -0
  32. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/.opencode/skill/release/SKILL.md +0 -0
  33. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/AGENTS.md +0 -0
  34. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/RELEASE.md +0 -0
  35. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/install.ps1 +0 -0
  36. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/install.sh +0 -0
  37. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/pyrightconfig.json +0 -0
  38. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/__init__.py +0 -0
  39. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/api/__init__.py +0 -0
  40. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/assets/__init__.py +0 -0
  41. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/assets/callback.html +0 -0
  42. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/__init__.py +0 -0
  43. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/auth.py +0 -0
  44. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/batch.py +0 -0
  45. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/batch_helpers.py +0 -0
  46. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/build.py +0 -0
  47. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/build_helpers.py +0 -0
  48. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/maintenance_check.py +0 -0
  49. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/run.py +0 -0
  50. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/run_display.py +0 -0
  51. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/run_helpers.py +0 -0
  52. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/skill.py +0 -0
  53. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/commands/upgrade.py +0 -0
  54. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/core/__init__.py +0 -0
  55. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/core/app_context.py +0 -0
  56. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/core/auth.py +0 -0
  57. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/core/credentials.py +0 -0
  58. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/core/oauth.py +0 -0
  59. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/core/token_exchange.py +0 -0
  60. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/models/base.py +0 -0
  61. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/models/build.py +0 -0
  62. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/models/maintenance_check.py +0 -0
  63. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/models/story_run.py +0 -0
  64. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/utils/__init__.py +0 -0
  65. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/utils/output.py +0 -0
  66. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/utils/skill_refresh.py +0 -0
  67. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/src/minitest_cli/utils/update_check.py +0 -0
  68. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/__init__.py +0 -0
  69. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/test_auth.py +0 -0
  70. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/test_auth_commands.py +0 -0
  71. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/test_batch_commands.py +0 -0
  72. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/test_build_commands.py +0 -0
  73. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/test_code_quality.py +0 -0
  74. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/test_run_commands.py +0 -0
  75. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/test_skill_command.py +0 -0
  76. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/test_upgrade_command.py +0 -0
  77. {minitest_cli-0.7.0 → minitest_cli-0.8.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: minitest-cli
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Minitest CLI – command-line interface for the Minitest testing platform
5
5
  Project-URL: Homepage, https://minitap.ai/
6
6
  Project-URL: Source, https://github.com/minitap-ai/minitest-cli
@@ -91,17 +91,22 @@ minitest auth login
91
91
  # List your apps
92
92
  minitest apps list
93
93
 
94
+ # Create a new app on your tenant
95
+ minitest apps create --name "My App"
96
+
94
97
  # Run tests
95
98
  minitest run --app <app-id>
96
99
  ```
97
100
 
98
101
  ## Configuration
99
102
 
100
- | Environment Variable | Description | Required |
101
- | -------------------- | ------------------------ | ---------------------------------- |
102
- | `MINITEST_TOKEN` | API authentication token | Yes (or use `minitest auth login`) |
103
- | `MINITEST_APP_ID` | Default app ID | No (can use `--app` flag) |
104
- | `MINITEST_API_URL` | API base URL | No (defaults to production) |
103
+ | Environment Variable | Description | Required |
104
+ | ----------------------------- | ---------------------------------------------------------- | ---------------------------------- |
105
+ | `MINITEST_TOKEN` | API authentication token | Yes (or use `minitest auth login`) |
106
+ | `MINITEST_APP_ID` | Default app ID | No (can use `--app` flag) |
107
+ | `MINITEST_API_URL` | testing-service base URL | No (defaults to production) |
108
+ | `MINITEST_APPS_MANAGER_URL` | apps-manager base URL (used by `minitest apps create`) | No (defaults to production) |
109
+ | `MINITEST_INTEGRATIONS_URL` | minihands-integrations base URL (used to list tenants) | No (defaults to production) |
105
110
 
106
111
  ## Global Flags
107
112
 
@@ -140,6 +145,8 @@ To point the CLI at the **dev** environment instead of production, set these env
140
145
  MINITEST_SUPABASE_URL=https://qrezuucghnmfvaxghqsv.supabase.co \
141
146
  MINITEST_SUPABASE_PUBLISHABLE_KEY=sb_publishable_4JRhoCm8pa5PbII0dhS09A_jhpkQhmy \
142
147
  MINITEST_API_URL=https://testing-service.dev.minitap.ai \
148
+ MINITEST_APPS_MANAGER_URL=https://apps-manager.dev.minitap.ai \
149
+ MINITEST_INTEGRATIONS_URL=https://integrations.dev.minitap.ai \
143
150
  minitest auth login
144
151
  ```
145
152
 
@@ -149,6 +156,8 @@ This authenticates against the dev environment and stores a dev-specific auth to
149
156
  MINITEST_SUPABASE_URL=https://qrezuucghnmfvaxghqsv.supabase.co \
150
157
  MINITEST_SUPABASE_PUBLISHABLE_KEY=sb_publishable_4JRhoCm8pa5PbII0dhS09A_jhpkQhmy \
151
158
  MINITEST_API_URL=https://testing-service.dev.minitap.ai \
159
+ MINITEST_APPS_MANAGER_URL=https://apps-manager.dev.minitap.ai \
160
+ MINITEST_INTEGRATIONS_URL=https://integrations.dev.minitap.ai \
152
161
  minitest apps list
153
162
  ```
154
163
 
@@ -58,17 +58,22 @@ minitest auth login
58
58
  # List your apps
59
59
  minitest apps list
60
60
 
61
+ # Create a new app on your tenant
62
+ minitest apps create --name "My App"
63
+
61
64
  # Run tests
62
65
  minitest run --app <app-id>
63
66
  ```
64
67
 
65
68
  ## Configuration
66
69
 
67
- | Environment Variable | Description | Required |
68
- | -------------------- | ------------------------ | ---------------------------------- |
69
- | `MINITEST_TOKEN` | API authentication token | Yes (or use `minitest auth login`) |
70
- | `MINITEST_APP_ID` | Default app ID | No (can use `--app` flag) |
71
- | `MINITEST_API_URL` | API base URL | No (defaults to production) |
70
+ | Environment Variable | Description | Required |
71
+ | ----------------------------- | ---------------------------------------------------------- | ---------------------------------- |
72
+ | `MINITEST_TOKEN` | API authentication token | Yes (or use `minitest auth login`) |
73
+ | `MINITEST_APP_ID` | Default app ID | No (can use `--app` flag) |
74
+ | `MINITEST_API_URL` | testing-service base URL | No (defaults to production) |
75
+ | `MINITEST_APPS_MANAGER_URL` | apps-manager base URL (used by `minitest apps create`) | No (defaults to production) |
76
+ | `MINITEST_INTEGRATIONS_URL` | minihands-integrations base URL (used to list tenants) | No (defaults to production) |
72
77
 
73
78
  ## Global Flags
74
79
 
@@ -107,6 +112,8 @@ To point the CLI at the **dev** environment instead of production, set these env
107
112
  MINITEST_SUPABASE_URL=https://qrezuucghnmfvaxghqsv.supabase.co \
108
113
  MINITEST_SUPABASE_PUBLISHABLE_KEY=sb_publishable_4JRhoCm8pa5PbII0dhS09A_jhpkQhmy \
109
114
  MINITEST_API_URL=https://testing-service.dev.minitap.ai \
115
+ MINITEST_APPS_MANAGER_URL=https://apps-manager.dev.minitap.ai \
116
+ MINITEST_INTEGRATIONS_URL=https://integrations.dev.minitap.ai \
110
117
  minitest auth login
111
118
  ```
112
119
 
@@ -116,6 +123,8 @@ This authenticates against the dev environment and stores a dev-specific auth to
116
123
  MINITEST_SUPABASE_URL=https://qrezuucghnmfvaxghqsv.supabase.co \
117
124
  MINITEST_SUPABASE_PUBLISHABLE_KEY=sb_publishable_4JRhoCm8pa5PbII0dhS09A_jhpkQhmy \
118
125
  MINITEST_API_URL=https://testing-service.dev.minitap.ai \
126
+ MINITEST_APPS_MANAGER_URL=https://apps-manager.dev.minitap.ai \
127
+ MINITEST_INTEGRATIONS_URL=https://integrations.dev.minitap.ai \
119
128
  minitest apps list
120
129
  ```
121
130
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "minitest-cli"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  description = "Minitest CLI – command-line interface for the Minitest testing platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,89 @@
1
+ """Async HTTP client for the apps-manager service.
2
+
3
+ Mirrors the shape of :class:`minitest_cli.api.client.ApiClient` but targets a
4
+ different base URL (``MINITEST_APPS_MANAGER_URL``). Reuses the same Supabase
5
+ JWT auth flow.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from minitest_cli.core.auth import load_token
13
+ from minitest_cli.core.config import Settings
14
+
15
+ CHANNEL_HEADER = "X-Minitest-Channel"
16
+ CHANNEL_VALUE = "cli"
17
+ DEFAULT_TIMEOUT = 30.0
18
+ UPLOAD_TIMEOUT = 300.0 # 5 minutes for large file uploads
19
+
20
+
21
+ class AppsManagerClient:
22
+ """Wraps httpx.AsyncClient pointed at the apps-manager service.
23
+
24
+ Auth and channel headers are auto-injected just like
25
+ :class:`minitest_cli.api.client.ApiClient`.
26
+
27
+ Usage::
28
+
29
+ async with AppsManagerClient(settings) as client:
30
+ response = await client.post("/api/v1/tenants/<id>/apps", data=...)
31
+ """
32
+
33
+ def __init__(self, settings: Settings) -> None:
34
+ self._settings = settings
35
+ self._client: httpx.AsyncClient | None = None
36
+
37
+ async def __aenter__(self) -> "AppsManagerClient":
38
+ token = load_token(self._settings)
39
+ self._client = httpx.AsyncClient(
40
+ base_url=self._settings.apps_manager_url,
41
+ headers={
42
+ "Authorization": f"Bearer {token}",
43
+ CHANNEL_HEADER: CHANNEL_VALUE,
44
+ },
45
+ timeout=DEFAULT_TIMEOUT,
46
+ )
47
+ return self
48
+
49
+ async def __aexit__(
50
+ self,
51
+ exc_type: type[BaseException] | None,
52
+ exc_val: BaseException | None,
53
+ exc_tb: object,
54
+ ) -> None:
55
+ if self._client:
56
+ await self._client.aclose()
57
+
58
+ def _ensure_client(self) -> httpx.AsyncClient:
59
+ if self._client is None:
60
+ msg = "AppsManagerClient must be used as an async context manager"
61
+ raise RuntimeError(msg)
62
+ return self._client
63
+
64
+ async def get(self, path: str, **kwargs: Any) -> httpx.Response:
65
+ """Send a GET request."""
66
+ return await self._ensure_client().get(path, **kwargs)
67
+
68
+ async def post(self, path: str, **kwargs: Any) -> httpx.Response:
69
+ """Send a POST request."""
70
+ return await self._ensure_client().post(path, **kwargs)
71
+
72
+ async def upload_form(
73
+ self,
74
+ path: str,
75
+ *,
76
+ data: dict[str, str] | None = None,
77
+ files: dict[str, Any] | None = None,
78
+ timeout: float = UPLOAD_TIMEOUT,
79
+ **kwargs: Any,
80
+ ) -> httpx.Response:
81
+ """Send a POST request with multipart form data and extended timeout."""
82
+ client = self._ensure_client()
83
+ return await client.post(
84
+ path,
85
+ data=data or {},
86
+ files=files or {},
87
+ timeout=timeout,
88
+ **kwargs,
89
+ )
@@ -61,6 +61,10 @@ class ApiClient:
61
61
  """Send a POST request."""
62
62
  return await self._ensure_client().post(path, **kwargs)
63
63
 
64
+ async def put(self, path: str, **kwargs: Any) -> httpx.Response:
65
+ """Send a PUT request."""
66
+ return await self._ensure_client().put(path, **kwargs)
67
+
64
68
  async def patch(self, path: str, **kwargs: Any) -> httpx.Response:
65
69
  """Send a PATCH request."""
66
70
  return await self._ensure_client().patch(path, **kwargs)
@@ -0,0 +1,145 @@
1
+ """App-knowledge commands: get and update an app's AppKnowledge prompt.
2
+
3
+ - ``get`` reads from ``GET /api/v1/apps/{app_id}/test-config`` and extracts the
4
+ ``app_knowledge`` field. testing-service has no dedicated GET endpoint for
5
+ app knowledge alone — the test-config response carries the latest version.
6
+ - ``update`` calls ``PUT /api/v1/apps/{app_id}/app-knowledge`` with body
7
+ ``{"content": "..."}`` and returns the new ``version_number``.
8
+ """
9
+
10
+ from pathlib import Path
11
+ from typing import Annotated, Any
12
+
13
+ import typer
14
+
15
+ from minitest_cli.commands.app_knowledge_helpers import (
16
+ fetch_app_knowledge,
17
+ update_app_knowledge,
18
+ )
19
+ from minitest_cli.core.auth import require_auth
20
+ from minitest_cli.core.config import Settings
21
+ from minitest_cli.utils.output import (
22
+ print_error,
23
+ print_info,
24
+ print_json,
25
+ print_success,
26
+ )
27
+
28
+ app = typer.Typer(name="app-knowledge", help="Read and update an app's AppKnowledge.")
29
+
30
+
31
+ @app.callback()
32
+ def _callback() -> None:
33
+ """App-knowledge operations."""
34
+
35
+
36
+ def _get_settings() -> Settings:
37
+ return typer.Context.settings # type: ignore[attr-defined]
38
+
39
+
40
+ def _is_json_mode() -> bool:
41
+ return typer.Context.json_mode # type: ignore[attr-defined]
42
+
43
+
44
+ @app.command(name="get")
45
+ def get_app_knowledge(
46
+ app_id: Annotated[
47
+ str,
48
+ typer.Option(
49
+ "--app",
50
+ help="App ID to read AppKnowledge for. Required.",
51
+ ),
52
+ ],
53
+ ) -> None:
54
+ """Read the current AppKnowledge content for an app.
55
+
56
+ Without ``--json``, prints the markdown content to stdout. With ``--json``,
57
+ prints the full record (including ``versionNumber`` if available).
58
+ """
59
+ settings = _get_settings()
60
+ json_mode = _is_json_mode()
61
+ require_auth(settings)
62
+
63
+ record = fetch_app_knowledge(settings, app_id)
64
+
65
+ if json_mode:
66
+ print_json(record)
67
+ return
68
+
69
+ content = record.get("content")
70
+ if not content:
71
+ print_info("No AppKnowledge content set for this app.")
72
+ return
73
+ print(content) # noqa: T201
74
+
75
+
76
+ @app.command(name="update")
77
+ def update_app_knowledge_command(
78
+ app_id: Annotated[
79
+ str,
80
+ typer.Option(
81
+ "--app",
82
+ help="App ID to update AppKnowledge for. Required.",
83
+ ),
84
+ ],
85
+ content: Annotated[
86
+ str | None,
87
+ typer.Option(
88
+ "--content",
89
+ help="New AppKnowledge markdown content (inline string).",
90
+ ),
91
+ ] = None,
92
+ content_file: Annotated[
93
+ Path | None,
94
+ typer.Option(
95
+ "--content-file",
96
+ help="Path to a file whose contents become the new AppKnowledge.",
97
+ exists=True,
98
+ readable=True,
99
+ dir_okay=False,
100
+ ),
101
+ ] = None,
102
+ ) -> None:
103
+ """Create a new version of the AppKnowledge prompt for an app.
104
+
105
+ Either ``--content`` or ``--content-file`` must be provided (not both).
106
+ Prints the new version number to stdout. With ``--json``, prints the full
107
+ response record.
108
+ """
109
+ settings = _get_settings()
110
+ json_mode = _is_json_mode()
111
+ require_auth(settings)
112
+
113
+ body = _resolve_content(content, content_file)
114
+ record: dict[str, Any] = update_app_knowledge(settings, app_id, body)
115
+
116
+ if json_mode:
117
+ print_json(record)
118
+ return
119
+
120
+ version_number = record.get("versionNumber") or record.get("version_number")
121
+ if version_number is None:
122
+ print_error("Backend response did not include a version number.")
123
+ raise typer.Exit(code=1)
124
+ print_success(f"AppKnowledge updated (version {version_number}).")
125
+ print(version_number) # noqa: T201
126
+
127
+
128
+ def _resolve_content(content: str | None, content_file: Path | None) -> str:
129
+ if content is None and content_file is None:
130
+ print_error("Provide --content or --content-file.")
131
+ raise typer.Exit(code=1)
132
+ if content is not None and content_file is not None:
133
+ print_error("Use either --content or --content-file, not both.")
134
+ raise typer.Exit(code=1)
135
+ if content is not None:
136
+ if content == "":
137
+ print_error("--content must not be empty.")
138
+ raise typer.Exit(code=1)
139
+ return content
140
+ assert content_file is not None # noqa: S101 for type checkers
141
+ text = content_file.read_text(encoding="utf-8")
142
+ if not text:
143
+ print_error(f"--content-file is empty: {content_file}")
144
+ raise typer.Exit(code=1)
145
+ return text
@@ -0,0 +1,99 @@
1
+ """Helpers for app-knowledge commands: error mapping and HTTP calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Coroutine
7
+ from typing import Any
8
+
9
+ import httpx
10
+ import typer
11
+
12
+ from minitest_cli.api.client import ApiClient
13
+ from minitest_cli.core.config import Settings
14
+ from minitest_cli.utils.output import print_error
15
+
16
+ EXIT_GENERAL_ERROR = 1
17
+ EXIT_NETWORK_ERROR = 3
18
+ EXIT_NOT_FOUND = 4
19
+
20
+
21
+ def _extract_detail(resp: httpx.Response) -> str:
22
+ try:
23
+ body = resp.json()
24
+ if isinstance(body, dict):
25
+ detail = body.get("detail") or body.get("message")
26
+ if detail:
27
+ return str(detail)
28
+ except Exception: # noqa: BLE001
29
+ pass
30
+ return resp.text or f"HTTP {resp.status_code}"
31
+
32
+
33
+ def _handle_response_error(resp: httpx.Response, *, resource: str = "App") -> None:
34
+ if resp.status_code < 400:
35
+ return
36
+ detail = _extract_detail(resp)
37
+ if resp.status_code == 404:
38
+ print_error(f"{resource} not found: {detail}")
39
+ raise typer.Exit(code=EXIT_NOT_FOUND)
40
+ if resp.status_code in (401, 403):
41
+ print_error(f"Authentication failed ({resp.status_code}): {detail}")
42
+ raise typer.Exit(code=EXIT_GENERAL_ERROR)
43
+ if 400 <= resp.status_code < 500:
44
+ print_error(f"Validation error ({resp.status_code}): {detail}")
45
+ raise typer.Exit(code=EXIT_GENERAL_ERROR)
46
+ print_error(f"Backend error ({resp.status_code}): {detail}")
47
+ raise typer.Exit(code=EXIT_NETWORK_ERROR)
48
+
49
+
50
+ def _run_api_call[T](coro: Coroutine[Any, Any, T]) -> T:
51
+ try:
52
+ return asyncio.run(coro)
53
+ except httpx.HTTPError as exc:
54
+ print_error(f"Network error: {exc}")
55
+ raise typer.Exit(code=EXIT_NETWORK_ERROR) from exc
56
+
57
+
58
+ def fetch_app_knowledge(settings: Settings, app_id: str) -> dict[str, Any]:
59
+ """Read the current AppKnowledge for an app.
60
+
61
+ testing-service has no dedicated GET endpoint, so we read the test-config
62
+ record (which carries the latest ``app_knowledge`` content) and surface a
63
+ minimal ``{appId, content}`` shape for callers.
64
+ """
65
+
66
+ async def _run() -> dict[str, Any]:
67
+ async with ApiClient(settings) as client:
68
+ resp = await client.get(f"/api/v1/apps/{app_id}/test-config")
69
+ _handle_response_error(resp)
70
+ payload = resp.json()
71
+ content: Any = None
72
+ if isinstance(payload, dict):
73
+ content = payload.get("appKnowledge") or payload.get("app_knowledge")
74
+ return {"appId": app_id, "content": content}
75
+
76
+ return _run_api_call(_run())
77
+
78
+
79
+ def update_app_knowledge(settings: Settings, app_id: str, content: str) -> dict[str, Any]:
80
+ """Push a new AppKnowledge version for an app.
81
+
82
+ Calls ``PUT /api/v1/apps/{app_id}/app-knowledge`` with body
83
+ ``{"content": ...}`` and returns the parsed response.
84
+ """
85
+
86
+ async def _run() -> dict[str, Any]:
87
+ async with ApiClient(settings) as client:
88
+ resp = await client.put(
89
+ f"/api/v1/apps/{app_id}/app-knowledge",
90
+ json={"content": content},
91
+ )
92
+ _handle_response_error(resp)
93
+ data = resp.json()
94
+ if not isinstance(data, dict):
95
+ print_error("Unexpected response shape from update endpoint.")
96
+ raise typer.Exit(code=EXIT_NETWORK_ERROR)
97
+ return data
98
+
99
+ return _run_api_call(_run())
@@ -0,0 +1,174 @@
1
+ """App management commands: list, create."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import httpx
8
+ import typer
9
+
10
+ from minitest_cli.api.apps_manager_client import AppsManagerClient # noqa: F401 re-exported for tests
11
+ from minitest_cli.api.client import ApiClient
12
+ from minitest_cli.commands.apps_helpers import create_app_request
13
+ from minitest_cli.core.auth import require_auth
14
+ from minitest_cli.core.config import Settings
15
+ from minitest_cli.core.tenants import fetch_user_tenants, resolve_tenant_id
16
+ from minitest_cli.models.app import AppDetailResponse, AppListResponse
17
+ from minitest_cli.utils.output import (
18
+ err_console,
19
+ print_error,
20
+ print_json,
21
+ print_success,
22
+ print_table,
23
+ )
24
+
25
+ EXIT_NETWORK_ERROR = 3
26
+
27
+ app = typer.Typer(name="apps", help="App management.")
28
+
29
+
30
+ @app.callback()
31
+ def _callback() -> None:
32
+ """App management."""
33
+
34
+
35
+ APP_TABLE_HEADERS = ["ID", "Name", "Platform"]
36
+
37
+ _PLATFORM_LABELS: dict[str, str] = {
38
+ "android": "Android",
39
+ "ios": "iOS",
40
+ "cross_platform": "Cross-platform",
41
+ }
42
+
43
+
44
+ def _format_platform(value: str | None) -> str:
45
+ """Render an AppPlatform value for the table; pass through unknown values."""
46
+ if value is None:
47
+ return "—"
48
+ return _PLATFORM_LABELS.get(value, value)
49
+
50
+
51
+ def _get_settings() -> Settings:
52
+ return typer.Context.settings # type: ignore[attr-defined]
53
+
54
+
55
+ def _is_json_mode() -> bool:
56
+ return typer.Context.json_mode # type: ignore[attr-defined]
57
+
58
+
59
+ @app.command(name="list")
60
+ def list_apps() -> None:
61
+ """List all apps."""
62
+ settings = _get_settings()
63
+ json_mode = _is_json_mode()
64
+ require_auth(settings)
65
+
66
+ async def _list() -> AppListResponse:
67
+ async with ApiClient(settings) as client:
68
+ resp = await client.get("/api/v1/apps")
69
+ if resp.status_code >= 400:
70
+ detail = resp.text
71
+ try:
72
+ body = resp.json()
73
+ if isinstance(body, dict):
74
+ detail = body.get("detail") or body.get("message") or detail
75
+ except Exception: # noqa: BLE001
76
+ pass
77
+ print_error(f"API error ({resp.status_code}): {detail}")
78
+ raise typer.Exit(code=EXIT_NETWORK_ERROR)
79
+ return AppListResponse.model_validate(resp.json())
80
+
81
+ try:
82
+ data = asyncio.run(_list())
83
+ except httpx.HTTPError as exc:
84
+ print_error(f"Network error: {exc}")
85
+ raise typer.Exit(code=EXIT_NETWORK_ERROR) from exc
86
+
87
+ if json_mode:
88
+ print_json([a.model_dump(mode="json", by_alias=True) for a in data.apps])
89
+ return
90
+
91
+ rows = [[a.id, a.name, _format_platform(a.platform)] for a in data.apps]
92
+ print_table(APP_TABLE_HEADERS, rows, title="Apps")
93
+
94
+
95
+ @app.command(name="create")
96
+ def create_app(
97
+ name: Annotated[
98
+ str,
99
+ typer.Option(
100
+ "--name",
101
+ help="Human-readable app name.",
102
+ ),
103
+ ],
104
+ tenant: Annotated[
105
+ str | None,
106
+ typer.Option(
107
+ "--tenant",
108
+ help=(
109
+ "Tenant ID to create the app under. Auto-resolved when the user has a "
110
+ "single tenant; prompts on a TTY otherwise."
111
+ ),
112
+ ),
113
+ ] = None,
114
+ description: Annotated[
115
+ str | None,
116
+ typer.Option("--description", help="Optional description."),
117
+ ] = None,
118
+ slug: Annotated[
119
+ str | None,
120
+ typer.Option(
121
+ "--slug",
122
+ help="URL-friendly slug. Auto-generated from the name if omitted.",
123
+ ),
124
+ ] = None,
125
+ icon: Annotated[
126
+ Path | None,
127
+ typer.Option(
128
+ "--icon",
129
+ help="Optional path to an icon image to upload.",
130
+ exists=True,
131
+ readable=True,
132
+ dir_okay=False,
133
+ ),
134
+ ] = None,
135
+ ) -> None:
136
+ """Create a new app on a tenant.
137
+
138
+ Calls ``POST /api/v1/tenants/{tenant_id}/apps`` on apps-manager. Prints the
139
+ created app's id to stdout; with ``--json``, prints the full record.
140
+ """
141
+ settings = _get_settings()
142
+ json_mode = _is_json_mode()
143
+ require_auth(settings)
144
+
145
+ async def _run() -> AppDetailResponse:
146
+ if tenant:
147
+ resolved_tenant_id = tenant
148
+ else:
149
+ tenants = await fetch_user_tenants(settings)
150
+ resolved_tenant_id = resolve_tenant_id(settings, None, tenants)
151
+ if len(tenants) > 1:
152
+ err_console.print(f"[dim]Using tenant {resolved_tenant_id}[/dim]")
153
+
154
+ return await create_app_request(
155
+ settings,
156
+ tenant_id=resolved_tenant_id,
157
+ name=name,
158
+ description=description,
159
+ slug=slug,
160
+ icon=icon,
161
+ )
162
+
163
+ try:
164
+ created = asyncio.run(_run())
165
+ except httpx.HTTPError as exc:
166
+ print_error(f"Network error: {exc}")
167
+ raise typer.Exit(code=EXIT_NETWORK_ERROR) from exc
168
+
169
+ if json_mode:
170
+ print_json(created.model_dump(mode="json", by_alias=True))
171
+ return
172
+
173
+ print_success(f"Created app '{created.name}' (slug: {created.slug})")
174
+ print(created.id) # noqa: T201