sweatstack-cli 0.7.1__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 (51) hide show
  1. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/.claude/settings.local.json +2 -1
  2. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/PKG-INFO +3 -3
  3. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/README.md +2 -2
  4. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/pyproject.toml +1 -1
  5. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/__init__.py +1 -1
  6. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/commands/app.py +20 -5
  7. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/commands/page.py +57 -4
  8. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/project.py +34 -0
  9. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_commands/test_app.py +31 -25
  10. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_commands/test_cli.py +2 -1
  11. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/uv.lock +1 -1
  12. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/.claude/skills/sweatstack/SKILL.md +0 -0
  13. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/.claude/skills/sweatstack/api.md +0 -0
  14. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/.claude/skills/sweatstack/auth.md +0 -0
  15. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/.claude/skills/sweatstack/cli.md +0 -0
  16. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/.claude/skills/sweatstack/pyscript.md +0 -0
  17. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/.claude/skills/sweatstack/webapp.md +0 -0
  18. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/.gitignore +0 -0
  19. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/.python-version +0 -0
  20. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/CREATE_PRIVATE_APP.md +0 -0
  21. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/DEVELOPMENT.md +0 -0
  22. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/Makefile +0 -0
  23. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/PLAN.md +0 -0
  24. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/api/__init__.py +0 -0
  25. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/api/client.py +0 -0
  26. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/auth/__init__.py +0 -0
  27. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/auth/callback_server.py +0 -0
  28. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/auth/jwt.py +0 -0
  29. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/auth/pkce.py +0 -0
  30. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/auth/tokens.py +0 -0
  31. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/commands/__init__.py +0 -0
  32. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/commands/auth.py +0 -0
  33. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/config.py +0 -0
  34. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/console.py +0 -0
  35. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/exceptions.py +0 -0
  36. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/main.py +0 -0
  37. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/py.typed +0 -0
  38. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/src/sweatstack_cli/version_check.py +0 -0
  39. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/sweatstack.toml +0 -0
  40. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/__init__.py +0 -0
  41. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/conftest.py +0 -0
  42. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_api/__init__.py +0 -0
  43. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_api/test_client.py +0 -0
  44. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_auth/__init__.py +0 -0
  45. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_auth/test_callback_server.py +0 -0
  46. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_auth/test_jwt.py +0 -0
  47. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_auth/test_pkce.py +0 -0
  48. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_auth/test_tokens.py +0 -0
  49. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_commands/__init__.py +0 -0
  50. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_project.py +0 -0
  51. {sweatstack_cli-0.7.1 → sweatstack_cli-0.8.0}/tests/test_version_check.py +0 -0
@@ -19,7 +19,8 @@
19
19
  "Bash(tree:*)",
20
20
  "Bash(env:*)",
21
21
  "Bash(uv run python:*)",
22
- "Bash(git:*)"
22
+ "Bash(git:*)",
23
+ "Bash(grep:*)"
23
24
  ]
24
25
  }
25
26
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack-cli
3
- Version: 0.7.1
3
+ Version: 0.8.0
4
4
  Summary: Command-line interface for SweatStack — the sports data platform for developers
5
5
  Project-URL: Homepage, https://sweatstack.no
6
6
  Project-URL: Documentation, https://docs.sweatstack.no
@@ -130,8 +130,8 @@ Deploy static sites to SweatStack Pages.
130
130
  # Deploy using sweatstack.toml defaults (recommended)
131
131
  sweatstack page deploy
132
132
 
133
- # Or specify slug and directory directly
134
- sweatstack page deploy my-page ./dist
133
+ # Or specify slug and directory explicitly
134
+ sweatstack page deploy my-page --dir ./dist
135
135
  ```
136
136
 
137
137
  ## License
@@ -100,8 +100,8 @@ Deploy static sites to SweatStack Pages.
100
100
  # Deploy using sweatstack.toml defaults (recommended)
101
101
  sweatstack page deploy
102
102
 
103
- # Or specify slug and directory directly
104
- sweatstack page deploy my-page ./dist
103
+ # Or specify slug and directory explicitly
104
+ sweatstack page deploy my-page --dir ./dist
105
105
  ```
106
106
 
107
107
  ## License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack-cli"
3
- version = "0.7.1"
3
+ version = "0.8.0"
4
4
  description = "Command-line interface for SweatStack — the sports data platform for developers"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Aart Goossens", email = "aart@sweatstack.no" }]
@@ -1,3 +1,3 @@
1
1
  """SweatStack CLI — Command-line interface for the SweatStack platform."""
2
2
 
3
- __version__ = "0.7.1"
3
+ __version__ = "0.8.0"
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json as json_lib
6
+ import sys
6
7
  from pathlib import Path
7
8
  from typing import Any, cast
8
9
 
@@ -56,7 +57,7 @@ def create(
56
57
  """Create a new private OAuth2 application."""
57
58
  if env and env_file:
58
59
  console.print("[red]Error:[/red] --env and --env-file are mutually exclusive.")
59
- raise typer.Exit(4)
60
+ raise typer.Exit(1)
60
61
 
61
62
  body: dict[str, str | bool] = {"name": name}
62
63
 
@@ -86,10 +87,21 @@ def create(
86
87
  if env_path:
87
88
  _write_env_file(env_path, result)
88
89
 
89
- # Write sweatstack.toml
90
+ # Write sweatstack.toml (protect existing file)
91
+ config_path = Path(CONFIG_FILENAME)
92
+ if config_path.exists():
93
+ if not sys.stdin.isatty():
94
+ console.print(
95
+ f"[red]Error:[/red] {CONFIG_FILENAME} already exists. "
96
+ "Remove it first or run interactively."
97
+ )
98
+ raise typer.Exit(1)
99
+ console.print(f"[yellow]Warning:[/yellow] {CONFIG_FILENAME} already exists.")
100
+ typer.confirm("Overwrite?", abort=True)
101
+
90
102
  config_path = write_config(
91
103
  app_name=result["name"],
92
- client_id=result["client_id"],
104
+ client_id=result["id"],
93
105
  page_slug=page,
94
106
  )
95
107
 
@@ -107,7 +119,7 @@ def _write_env_file(path: Path, result: dict[str, str]) -> None:
107
119
  if path.exists():
108
120
  existing_content = path.read_text()
109
121
 
110
- client_id = result.get("client_id")
122
+ client_id = result.get("id")
111
123
  client_secret = result.get("client_secret")
112
124
 
113
125
  if client_id:
@@ -138,7 +150,7 @@ def _print_app_created(result: dict[str, str], env_path: Path | None, config_pat
138
150
  """Pretty-print the created application details."""
139
151
  console.print(f"[green]✓[/green] Created application [bold]{result['name']}[/bold]")
140
152
  console.print()
141
- console.print(f" Client ID: [cyan]{result['client_id']}[/cyan]")
153
+ console.print(f" Client ID: [cyan]{result['id']}[/cyan]")
142
154
 
143
155
  if "client_secret" in result:
144
156
  console.print(f" Secret: [cyan]{result['client_secret']}[/cyan]")
@@ -206,6 +218,9 @@ def link(
206
218
  console.print(f" [cyan][{i}][/cyan] {app_info['name']}{page_info}")
207
219
 
208
220
  console.print()
221
+ if not sys.stdin.isatty():
222
+ console.print("[red]Error:[/red] Cannot select application in non-interactive mode.")
223
+ raise typer.Exit(1)
209
224
  selection = typer.prompt("Select application", type=int)
210
225
 
211
226
  if selection < 1 or selection > len(apps):
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import mimetypes
6
+ import sys
6
7
  from pathlib import Path
7
8
 
8
9
  import typer
@@ -12,7 +13,7 @@ from sweatstack_cli.api import APIClient
12
13
  from sweatstack_cli.config import get_settings
13
14
  from sweatstack_cli.console import console
14
15
  from sweatstack_cli.exceptions import APIError, AuthenticationError
15
- from sweatstack_cli.project import CONFIG_FILENAME, ProjectConfig
16
+ from sweatstack_cli.project import CONFIG_FILENAME, ProjectConfig, update_config, write_config
16
17
 
17
18
  # URL where users can create pages for their applications
18
19
  SETTINGS_URL = "https://app.sweatstack.no/settings/api"
@@ -59,12 +60,14 @@ def deploy(
59
60
  None,
60
61
  help="Page slug. Reads from sweatstack.toml if not provided.",
61
62
  ),
62
- directory: Path | None = typer.Argument(
63
+ directory: Path | None = typer.Option(
63
64
  None,
64
- help="Directory to deploy. Reads from sweatstack.toml or defaults to current dir.",
65
+ "--dir",
66
+ help="Directory to deploy. Reads from sweatstack.toml if not provided.",
65
67
  ),
66
68
  ) -> None:
67
69
  """Deploy a static site to SweatStack Pages."""
70
+
68
71
  # Load config for defaults
69
72
  config = ProjectConfig.load()
70
73
 
@@ -76,9 +79,19 @@ def deploy(
76
79
  raise typer.Exit(1)
77
80
 
78
81
  # Resolve directory
82
+ directory_explicit = directory is not None
79
83
  if directory is None:
80
84
  dir_from_config = config.page.directory if config else None
81
- directory = Path(dir_from_config) if dir_from_config else Path(".")
85
+ if dir_from_config:
86
+ directory = Path(dir_from_config)
87
+ else:
88
+ if not sys.stdin.isatty():
89
+ console.print("[red]Error:[/red] No deploy directory specified.")
90
+ console.print("Use --dir or set it in sweatstack.toml.")
91
+ raise typer.Exit(1)
92
+ dir_input = typer.prompt("Deploy directory", default="public/")
93
+ directory = Path(dir_input)
94
+ directory_explicit = True
82
95
 
83
96
  if not directory.exists():
84
97
  console.print(f"[red]Error:[/red] Directory '{directory}' does not exist.")
@@ -90,6 +103,32 @@ def deploy(
90
103
 
91
104
  directory = directory.resolve()
92
105
 
106
+ # Compute relative directory for config and warnings
107
+ try:
108
+ rel_dir = str(directory.relative_to(Path.cwd()))
109
+ except ValueError:
110
+ rel_dir = str(directory)
111
+
112
+ # Warn and confirm if deploying with different values than toml
113
+ if config:
114
+ mismatches: list[str] = []
115
+ if config.page.slug and slug and slug != config.page.slug:
116
+ mismatches.append(
117
+ f"Deploying to '{slug}' but "
118
+ f"{CONFIG_FILENAME} has slug '{config.page.slug}'"
119
+ )
120
+ if config.page.directory and rel_dir != config.page.directory:
121
+ mismatches.append(
122
+ f"Deploying '{rel_dir}' but "
123
+ f"{CONFIG_FILENAME} has directory '{config.page.directory}'"
124
+ )
125
+ if mismatches:
126
+ for msg in mismatches:
127
+ console.print(f"[yellow]Warning:[/yellow] {msg}")
128
+ if not sys.stdin.isatty():
129
+ raise typer.Exit(1)
130
+ typer.confirm("Continue?", abort=True)
131
+
93
132
  # Collect files
94
133
  files = _collect_files(directory)
95
134
  file_count = len(files)
@@ -143,6 +182,20 @@ def deploy(
143
182
  console.print(f"[red]Error:[/red] {e}")
144
183
  raise typer.Exit(3)
145
184
 
185
+ # Save to config after successful deploy
186
+ if config:
187
+ update_kwargs: dict[str, str] = {}
188
+ if not config.page.slug:
189
+ update_kwargs["page_slug"] = page_slug
190
+ if not config.page.directory and directory_explicit:
191
+ update_kwargs["page_directory"] = rel_dir
192
+ if update_kwargs:
193
+ update_config(**update_kwargs)
194
+ console.print(f"[green]✓[/green] Saved to [bold]{CONFIG_FILENAME}[/bold]")
195
+ elif directory_explicit:
196
+ write_config(page_slug=page_slug, page_directory=rel_dir)
197
+ console.print(f"[green]✓[/green] Created [bold]{CONFIG_FILENAME}[/bold]")
198
+
146
199
  console.print()
147
200
  console.print(f"[green]✓[/green] Deployed [bold]{page_slug}[/bold] successfully!")
148
201
  console.print(f" Files: {file_count}")
@@ -109,3 +109,37 @@ def write_config(
109
109
  config_path.write_text("\n".join(lines), encoding="utf-8")
110
110
 
111
111
  return config_path
112
+
113
+
114
+ def update_config(
115
+ path: Path | None = None,
116
+ *,
117
+ page_slug: str | None = None,
118
+ page_directory: str | None = None,
119
+ ) -> Path | None:
120
+ """Update specific fields in an existing sweatstack.toml without overwriting other values."""
121
+ config_path = path or Path(CONFIG_FILENAME)
122
+
123
+ if not config_path.exists():
124
+ return None
125
+
126
+ try:
127
+ data = tomllib.loads(config_path.read_text(encoding="utf-8"))
128
+ except tomllib.TOMLDecodeError:
129
+ return None
130
+
131
+ app_data = data.get("app", {})
132
+ page_data = data.get("page", {})
133
+
134
+ if page_slug is not None:
135
+ page_data["slug"] = page_slug
136
+ if page_directory is not None:
137
+ page_data["directory"] = page_directory
138
+
139
+ return write_config(
140
+ path=config_path,
141
+ app_name=app_data.get("name"),
142
+ client_id=app_data.get("client_id"),
143
+ page_slug=page_data.get("slug"),
144
+ page_directory=page_data.get("directory"),
145
+ )
@@ -26,14 +26,15 @@ def mock_api():
26
26
  class TestAppCreate:
27
27
  """Tests for app create command."""
28
28
 
29
- def test_create_minimal(self, mock_api: MagicMock) -> None:
29
+ def test_create_minimal(self, mock_api: MagicMock, tmp_path: Path) -> None:
30
30
  """Should create an app with just a name."""
31
31
  mock_api.post.return_value = {
32
32
  "name": "Test App",
33
- "client_id": "abc123",
33
+ "id": "abc123",
34
34
  }
35
35
 
36
- result = runner.invoke(app, ["app", "create", "Test App"])
36
+ with runner.isolated_filesystem(temp_dir=tmp_path):
37
+ result = runner.invoke(app, ["app", "create", "Test App"])
37
38
 
38
39
  assert result.exit_code == 0
39
40
  assert "Test App" in result.stdout
@@ -43,15 +44,16 @@ class TestAppCreate:
43
44
  json={"name": "Test App"},
44
45
  )
45
46
 
46
- def test_create_with_page(self, mock_api: MagicMock) -> None:
47
+ def test_create_with_page(self, mock_api: MagicMock, tmp_path: Path) -> None:
47
48
  """Should create an app with page association."""
48
49
  mock_api.post.return_value = {
49
50
  "name": "Test App",
50
- "client_id": "abc123",
51
+ "id": "abc123",
51
52
  "redirect_uris": ["https://myapp.sweatstack.pages.dev/callback"],
52
53
  }
53
54
 
54
- result = runner.invoke(app, ["app", "create", "Test App", "--page", "myapp"])
55
+ with runner.isolated_filesystem(temp_dir=tmp_path):
56
+ result = runner.invoke(app, ["app", "create", "Test App", "--page", "myapp"])
55
57
 
56
58
  assert result.exit_code == 0
57
59
  assert "myapp.sweatstack.pages.dev" in result.stdout
@@ -60,15 +62,16 @@ class TestAppCreate:
60
62
  json={"name": "Test App", "page": "myapp"},
61
63
  )
62
64
 
63
- def test_create_with_secret(self, mock_api: MagicMock) -> None:
65
+ def test_create_with_secret(self, mock_api: MagicMock, tmp_path: Path) -> None:
64
66
  """Should create an app with client secret and show warning."""
65
67
  mock_api.post.return_value = {
66
68
  "name": "Test App",
67
- "client_id": "abc123",
69
+ "id": "abc123",
68
70
  "client_secret": "secret456",
69
71
  }
70
72
 
71
- result = runner.invoke(app, ["app", "create", "Test App", "--secret"])
73
+ with runner.isolated_filesystem(temp_dir=tmp_path):
74
+ result = runner.invoke(app, ["app", "create", "Test App", "--secret"])
72
75
 
73
76
  assert result.exit_code == 0
74
77
  assert "secret456" in result.stdout
@@ -78,15 +81,16 @@ class TestAppCreate:
78
81
  json={"name": "Test App", "generate_secret": True},
79
82
  )
80
83
 
81
- def test_create_with_short_flags(self, mock_api: MagicMock) -> None:
84
+ def test_create_with_short_flags(self, mock_api: MagicMock, tmp_path: Path) -> None:
82
85
  """Should support short flags -p and -s."""
83
86
  mock_api.post.return_value = {
84
87
  "name": "Test App",
85
- "client_id": "abc123",
88
+ "id": "abc123",
86
89
  "client_secret": "secret456",
87
90
  }
88
91
 
89
- result = runner.invoke(app, ["app", "create", "Test App", "-p", "myapp", "-s"])
92
+ with runner.isolated_filesystem(temp_dir=tmp_path):
93
+ result = runner.invoke(app, ["app", "create", "Test App", "-p", "myapp", "-s"])
90
94
 
91
95
  assert result.exit_code == 0
92
96
  mock_api.post.assert_called_once_with(
@@ -94,24 +98,25 @@ class TestAppCreate:
94
98
  json={"name": "Test App", "page": "myapp", "generate_secret": True},
95
99
  )
96
100
 
97
- def test_create_json_output(self, mock_api: MagicMock) -> None:
101
+ def test_create_json_output(self, mock_api: MagicMock, tmp_path: Path) -> None:
98
102
  """Should output JSON when --json flag is used."""
99
103
  mock_api.post.return_value = {
100
104
  "name": "Test App",
101
- "client_id": "abc123",
105
+ "id": "abc123",
102
106
  }
103
107
 
104
- result = runner.invoke(app, ["app", "create", "Test App", "--json"])
108
+ with runner.isolated_filesystem(temp_dir=tmp_path):
109
+ result = runner.invoke(app, ["app", "create", "Test App", "--json"])
105
110
 
106
111
  assert result.exit_code == 0
107
- assert '"client_id": "abc123"' in result.stdout
112
+ assert '"id": "abc123"' in result.stdout
108
113
  assert '"name": "Test App"' in result.stdout
109
114
 
110
115
  def test_create_with_env(self, mock_api: MagicMock, tmp_path: Path) -> None:
111
116
  """Should write credentials to .env file."""
112
117
  mock_api.post.return_value = {
113
118
  "name": "Test App",
114
- "client_id": "abc123",
119
+ "id": "abc123",
115
120
  "client_secret": "secret456",
116
121
  }
117
122
 
@@ -129,7 +134,7 @@ class TestAppCreate:
129
134
  """Should write credentials to custom env file."""
130
135
  mock_api.post.return_value = {
131
136
  "name": "Test App",
132
- "client_id": "abc123",
137
+ "id": "abc123",
133
138
  }
134
139
 
135
140
  with runner.isolated_filesystem(temp_dir=tmp_path):
@@ -147,7 +152,7 @@ class TestAppCreate:
147
152
  """Should append to existing .env file."""
148
153
  mock_api.post.return_value = {
149
154
  "name": "Test App",
150
- "client_id": "abc123",
155
+ "id": "abc123",
151
156
  }
152
157
 
153
158
  with runner.isolated_filesystem(temp_dir=tmp_path):
@@ -166,7 +171,7 @@ class TestAppCreate:
166
171
  """Should not overwrite existing env vars."""
167
172
  mock_api.post.return_value = {
168
173
  "name": "Test App",
169
- "client_id": "abc123",
174
+ "id": "abc123",
170
175
  }
171
176
 
172
177
  with runner.isolated_filesystem(temp_dir=tmp_path):
@@ -187,7 +192,7 @@ class TestAppCreate:
187
192
  app, ["app", "create", "Test App", "--env", "--env-file", ".env.local"]
188
193
  )
189
194
 
190
- assert result.exit_code == 4 # ValidationError
195
+ assert result.exit_code == 1
191
196
  assert "mutually exclusive" in result.stdout
192
197
 
193
198
  def test_create_not_authenticated(self) -> None:
@@ -211,16 +216,17 @@ class TestAppCreate:
211
216
  assert "App name already exists" in result.stdout
212
217
 
213
218
  def test_create_env_with_secret_no_warning_without_env(
214
- self, mock_api: MagicMock
219
+ self, mock_api: MagicMock, tmp_path: Path
215
220
  ) -> None:
216
221
  """Should show secret warning when --env is not used."""
217
222
  mock_api.post.return_value = {
218
223
  "name": "Test App",
219
- "client_id": "abc123",
224
+ "id": "abc123",
220
225
  "client_secret": "secret456",
221
226
  }
222
227
 
223
- result = runner.invoke(app, ["app", "create", "Test App", "--secret"])
228
+ with runner.isolated_filesystem(temp_dir=tmp_path):
229
+ result = runner.invoke(app, ["app", "create", "Test App", "--secret"])
224
230
 
225
231
  assert result.exit_code == 0
226
232
  assert "won't be shown again" in result.stdout
@@ -231,7 +237,7 @@ class TestAppCreate:
231
237
  """Should not show secret warning when --env is used."""
232
238
  mock_api.post.return_value = {
233
239
  "name": "Test App",
234
- "client_id": "abc123",
240
+ "id": "abc123",
235
241
  "client_secret": "secret456",
236
242
  }
237
243
 
@@ -62,7 +62,8 @@ class TestPageCommands:
62
62
 
63
63
  assert result.exit_code == 0
64
64
  assert "SLUG" in result.stdout
65
- assert "DIRECTORY" in result.stdout
65
+ assert "--dir" in result.stdout
66
+ assert "-d," not in result.stdout
66
67
 
67
68
 
68
69
  class TestAppCommands:
@@ -463,7 +463,7 @@ wheels = [
463
463
 
464
464
  [[package]]
465
465
  name = "sweatstack-cli"
466
- version = "0.6.0"
466
+ version = "0.8.0"
467
467
  source = { editable = "." }
468
468
  dependencies = [
469
469
  { name = "httpx" },
File without changes
File without changes