fogies 0.0.0.dev10__tar.gz → 0.0.0.dev12__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 (45) hide show
  1. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/PKG-INFO +2 -1
  2. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/pyproject.toml +3 -1
  3. fogies-0.0.0.dev12/src/fogies/age_key.py +48 -0
  4. fogies-0.0.0.dev12/src/fogies/includes/templates/claude_sync.toml.template +16 -0
  5. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tasks/access_key.py +5 -0
  6. fogies-0.0.0.dev12/src/fogies/tasks/claude_sync.py +145 -0
  7. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tasks/format.py +3 -1
  8. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tasks/lint.py +3 -1
  9. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tasks/poetry.py +6 -2
  10. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tasks/terraform.py +101 -49
  11. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tasks/terraform_backend.py +13 -24
  12. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tasks/test.py +3 -1
  13. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/templates.py +4 -0
  14. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/terraform/backend.py +76 -36
  15. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tools/aws_environ.py +8 -5
  16. fogies-0.0.0.dev12/src/fogies/tools/claude_sync.py +302 -0
  17. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tools/terraform.py +53 -24
  18. fogies-0.0.0.dev10/src/fogies/tools/claude_sync.py +0 -130
  19. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/README.md +0 -0
  20. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/__init__.py +0 -0
  21. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/aws_access_key.py +0 -0
  22. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/boto_clients.py +0 -0
  23. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/download_verified.py +0 -0
  24. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/includes/templates/aws.toml.template +0 -0
  25. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/includes/templates/backend_status.toml.template +0 -0
  26. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/includes/templates/poetry.toml.template +0 -0
  27. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/py.typed +0 -0
  28. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/pyfogies.py +0 -0
  29. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/ready_poll.py +0 -0
  30. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/retry.py +0 -0
  31. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tasks/__init__.py +0 -0
  32. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/terraform/__init__.py +0 -0
  33. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/terraform/alb.py +0 -0
  34. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/terraform/cloudwatch.py +0 -0
  35. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/terraform/ecr.py +0 -0
  36. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/terraform/ecs.py +0 -0
  37. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/terraform/hosted_zone.py +0 -0
  38. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/terraform/network.py +0 -0
  39. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tools/__init__.py +0 -0
  40. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tools/boto.py +0 -0
  41. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tools/command.py +0 -0
  42. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tools/environ.py +0 -0
  43. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tools/go.py +0 -0
  44. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tools/ollama.py +0 -0
  45. {fogies-0.0.0.dev10 → fogies-0.0.0.dev12}/src/fogies/tools/terraform_backend.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fogies
3
- Version: 0.0.0.dev10
3
+ Version: 0.0.0.dev12
4
4
  Summary:
5
5
  Author: James Fogarty
6
6
  Author-email: jayfo@users.noreply.github.com
@@ -12,6 +12,7 @@ Requires-Dist: filelock (>=3.25.2,<4.0.0)
12
12
  Requires-Dist: ollama (>=0.6.2,<0.7.0)
13
13
  Requires-Dist: psutil (>=7.2.2,<8.0.0)
14
14
  Requires-Dist: pydantic (>=2.12.5,<3.0.0)
15
+ Requires-Dist: pyrage (>=1.4.0,<2.0.0)
15
16
  Requires-Dist: requests (>=2.33.0,<2.34.0)
16
17
  Requires-Dist: tenacity (>=9.1.4,<10.0.0)
17
18
  Requires-Dist: tomlkit (>=0.15.1,<0.16.0)
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "fogies"
3
3
  # Version follows semantic versioning <https://semver.org/>.
4
- version = "0.0.0-dev.10"
4
+ version = "0.0.0-dev.12"
5
5
  description = ""
6
6
  authors = [
7
7
  {name = "James Fogarty",email = "jayfo@users.noreply.github.com"}
@@ -18,6 +18,7 @@ include = [
18
18
  {path = "src/fogies/includes/templates/aws.toml.template", format = ["sdist", "wheel"]},
19
19
  {path = "src/fogies/includes/templates/poetry.toml.template", format = ["sdist", "wheel"]},
20
20
  {path = "src/fogies/includes/templates/backend_status.toml.template", format = ["sdist", "wheel"]},
21
+ {path = "src/fogies/includes/templates/claude_sync.toml.template", format = ["sdist", "wheel"]},
21
22
  ]
22
23
 
23
24
  [tool.poetry.dependencies]
@@ -33,6 +34,7 @@ pydantic = "^2.12.5"
33
34
  requests = "~2.33.0"
34
35
  tenacity = "^9.1.4"
35
36
  tomlkit = "^0.15.1"
37
+ pyrage = "^1.4.0"
36
38
 
37
39
  [tool.poetry.group.dev.dependencies]
38
40
  # ^ allows all minor and patch releases.
@@ -0,0 +1,48 @@
1
+ """Age secret keys, which encrypt what claude-sync uploads.
2
+
3
+ An age secret key is a string like AGE-SECRET-KEY-1...: see
4
+ <https://github.com/FiloSottile/age>. pyrage, which binds to a Rust
5
+ implementation of age, creates and checks them. It ships no type stubs, so the
6
+ little of it that is used is given types here and nowhere else.
7
+ """
8
+
9
+ from typing import Protocol, cast
10
+
11
+ import pyrage # pyright: ignore[reportMissingTypeStubs]
12
+
13
+
14
+ class _IdentityFactory(Protocol):
15
+ """The parts of pyrage's x25519 Identity class that are used."""
16
+
17
+ def generate(self) -> object: ...
18
+
19
+ def from_str(self, value: str) -> object: ...
20
+
21
+
22
+ _IDENTITY = cast(
23
+ _IdentityFactory,
24
+ pyrage.x25519.Identity, # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
25
+ )
26
+
27
+ _IDENTITY_ERROR = cast(
28
+ type[Exception],
29
+ pyrage.IdentityError, # pyright: ignore[reportAttributeAccessIssue]
30
+ )
31
+
32
+
33
+ def generate_age_key() -> str:
34
+ """Return a new, random age secret key."""
35
+ return str(_IDENTITY.generate())
36
+
37
+
38
+ def validate_age_key(value: str) -> str:
39
+ """Return value, if it is an age secret key.
40
+
41
+ Raises ValueError if it is not, including a key with a mistyped character,
42
+ since the checksum is checked.
43
+ """
44
+ try:
45
+ _ = _IDENTITY.from_str(value)
46
+ except _IDENTITY_ERROR as exc:
47
+ raise ValueError("Not an age secret key") from exc
48
+ return value
@@ -0,0 +1,16 @@
1
+ # Config for claude-sync tasks.
2
+ # See https://github.com/tawanorg/claude-sync
3
+
4
+ # Profile in the AWS config, for an IAM user that can use the bucket.
5
+ aws_profile = "..."
6
+
7
+ # S3 bucket.
8
+ bucket = "..."
9
+ region = "..."
10
+
11
+ # What to sync: "sessions" or "full".
12
+ scope = "sessions"
13
+
14
+ # Encryption key.
15
+ # claude-sync.configure will create, so that all machines have the same key.
16
+ # age_key = "AGE-SECRET-KEY-1..."
@@ -98,6 +98,7 @@ def get_task_create(
98
98
  ) -> Task[Callable[..., None]]:
99
99
  @task(
100
100
  name="create",
101
+ auto_shortflags=False,
101
102
  positional=[],
102
103
  help={
103
104
  "prompt": "Prompt for admin credentials.",
@@ -125,6 +126,7 @@ def get_task_delete(
125
126
  ) -> Task[Callable[..., None]]:
126
127
  @task(
127
128
  name="delete",
129
+ auto_shortflags=False,
128
130
  positional=[],
129
131
  help={
130
132
  "prompt": "Prompt for admin credentials.",
@@ -159,6 +161,7 @@ def get_task_delete_key(
159
161
  ) -> Task[Callable[..., None]]:
160
162
  @task(
161
163
  name="delete-key",
164
+ auto_shortflags=False,
162
165
  positional=[],
163
166
  help={
164
167
  "prompt": "Prompt for admin credentials.",
@@ -206,6 +209,7 @@ def get_task_list(
206
209
  ) -> Task[Callable[..., None]]:
207
210
  @task(
208
211
  name="list",
212
+ auto_shortflags=False,
209
213
  help={
210
214
  "prompt": "Prompt for admin credentials.",
211
215
  },
@@ -235,6 +239,7 @@ def get_task_rotate_key(
235
239
  ) -> Task[Callable[..., None]]:
236
240
  @task(
237
241
  name="rotate-key",
242
+ auto_shortflags=False,
238
243
  positional=[],
239
244
  help={
240
245
  "prompt": "Prompt for admin credentials.",
@@ -0,0 +1,145 @@
1
+ """Tasks for setting up and running claude-sync."""
2
+
3
+ import pathlib
4
+ from collections.abc import Callable
5
+ from typing import cast
6
+
7
+ from invoke.collection import Collection
8
+ from invoke.context import Context
9
+ from invoke.tasks import Task, task
10
+
11
+ from fogies.tools.claude_sync import claude_sync, configure_claude_sync
12
+ from fogies.tools.command import CommandParams
13
+
14
+
15
+ def get_task_configure(
16
+ *,
17
+ binary_cache_path: pathlib.Path,
18
+ claude_sync_config_path: pathlib.Path,
19
+ aws_config_path: pathlib.Path,
20
+ ) -> Task[Callable[..., None]]:
21
+ @task(
22
+ name="configure",
23
+ auto_shortflags=False,
24
+ help={
25
+ "force": "Force overwrite based on current configuration.",
26
+ },
27
+ ) # pyright: ignore[reportUntypedFunctionDecorator]
28
+ def task_configure(context: Context, force: bool = False) -> None:
29
+ """
30
+ Configure claude-sync on this machine.
31
+ """
32
+ setup = configure_claude_sync(
33
+ claude_sync_config_path=claude_sync_config_path,
34
+ aws_config_path=aws_config_path,
35
+ force=force,
36
+ )
37
+
38
+ if setup.generated_age_key:
39
+ print("Generated encryption key.")
40
+ if setup.applied_to_claude_sync_files:
41
+ print("Applied configuration to claude-sync files.")
42
+ else:
43
+ print("Already configured.")
44
+
45
+ # Reads from the bucket and changes nothing, so this fails now, and
46
+ # clearly, if the credentials, bucket or key are wrong.
47
+ print("Checking the connection:")
48
+ with claude_sync(
49
+ binary_cache_path=binary_cache_path, raise_if_env_exists=False
50
+ ) as cs:
51
+ _ = cs.pull(command_params=CommandParams(context=context), dry_run=True)
52
+
53
+ return cast(Task[Callable[..., None]], task_configure)
54
+
55
+
56
+ def get_task_pull(
57
+ *,
58
+ binary_cache_path: pathlib.Path,
59
+ default_dry_run: bool = False,
60
+ default_force: bool = False,
61
+ default_rebuild_history: bool = True,
62
+ ) -> Task[Callable[..., None]]:
63
+ @task(
64
+ name="pull",
65
+ auto_shortflags=False,
66
+ help={
67
+ "dry_run": "Show what would be downloaded.",
68
+ "force": "Force overwrite of local files.",
69
+ "rebuild_history": "Rebuild history from session files.",
70
+ },
71
+ ) # pyright: ignore[reportUntypedFunctionDecorator]
72
+ def task_pull(
73
+ context: Context,
74
+ dry_run: bool = default_dry_run,
75
+ force: bool = default_force,
76
+ rebuild_history: bool = default_rebuild_history,
77
+ ) -> None:
78
+ """
79
+ Download remote changes.
80
+ """
81
+ with claude_sync(
82
+ binary_cache_path=binary_cache_path, raise_if_env_exists=False
83
+ ) as cs:
84
+ _ = cs.pull(
85
+ command_params=CommandParams(context=context),
86
+ dry_run=dry_run,
87
+ force=force,
88
+ rebuild_history=rebuild_history,
89
+ )
90
+
91
+ return cast(Task[Callable[..., None]], task_pull)
92
+
93
+
94
+ def get_task_push(*, binary_cache_path: pathlib.Path) -> Task[Callable[..., None]]:
95
+ @task(
96
+ name="push", auto_shortflags=False
97
+ ) # pyright: ignore[reportUntypedFunctionDecorator]
98
+ def task_push(context: Context) -> None:
99
+ """
100
+ Upload local changes.
101
+ """
102
+ with claude_sync(
103
+ binary_cache_path=binary_cache_path, raise_if_env_exists=False
104
+ ) as cs:
105
+ _ = cs.push(command_params=CommandParams(context=context))
106
+
107
+ return cast(Task[Callable[..., None]], task_push)
108
+
109
+
110
+ def get_task_status(*, binary_cache_path: pathlib.Path) -> Task[Callable[..., None]]:
111
+ @task(
112
+ name="status", auto_shortflags=False
113
+ ) # pyright: ignore[reportUntypedFunctionDecorator]
114
+ def task_status(context: Context) -> None:
115
+ """
116
+ Show local changes since prior synchronization.
117
+ """
118
+ with claude_sync(
119
+ binary_cache_path=binary_cache_path, raise_if_env_exists=False
120
+ ) as cs:
121
+ _ = cs.status(command_params=CommandParams(context=context))
122
+
123
+ return cast(Task[Callable[..., None]], task_status)
124
+
125
+
126
+ def get_collection(
127
+ *,
128
+ collection_name: str,
129
+ binary_cache_path: pathlib.Path,
130
+ claude_sync_config_path: pathlib.Path,
131
+ aws_config_path: pathlib.Path,
132
+ ) -> Collection:
133
+ """Get a collection of tasks for setting up and running claude-sync."""
134
+ collection = Collection(collection_name)
135
+ collection.add_task(
136
+ get_task_configure(
137
+ binary_cache_path=binary_cache_path,
138
+ claude_sync_config_path=claude_sync_config_path,
139
+ aws_config_path=aws_config_path,
140
+ )
141
+ )
142
+ collection.add_task(get_task_pull(binary_cache_path=binary_cache_path))
143
+ collection.add_task(get_task_push(binary_cache_path=binary_cache_path))
144
+ collection.add_task(get_task_status(binary_cache_path=binary_cache_path))
145
+ return collection
@@ -26,7 +26,9 @@ def get_task_format(
26
26
  if not (fmt_black or fmt_isort or fmt_terraform):
27
27
  raise ValueError("At least one fmt flag must be set.")
28
28
 
29
- @task(name="format") # pyright: ignore[reportUntypedFunctionDecorator]
29
+ @task(
30
+ name="format", auto_shortflags=False
31
+ ) # pyright: ignore[reportUntypedFunctionDecorator]
30
32
  def task_format(context: Context) -> None:
31
33
  """
32
34
  Apply code formatting.
@@ -9,7 +9,9 @@ from invoke.tasks import Task, task
9
9
 
10
10
 
11
11
  def get_task_lint() -> Task[Callable[[Context], None]]:
12
- @task(name="lint") # pyright: ignore[reportUntypedFunctionDecorator]
12
+ @task(
13
+ name="lint", auto_shortflags=False
14
+ ) # pyright: ignore[reportUntypedFunctionDecorator]
13
15
  def task_lint(context: Context) -> None:
14
16
  """
15
17
  Run linting.
@@ -14,7 +14,9 @@ from fogies.templates import ensure_from_template, poetry_toml_template_factory
14
14
 
15
15
 
16
16
  def get_task_build() -> Task[Callable[[Context], None]]:
17
- @task(name="build") # pyright: ignore[reportUntypedFunctionDecorator]
17
+ @task(
18
+ name="build", auto_shortflags=False
19
+ ) # pyright: ignore[reportUntypedFunctionDecorator]
18
20
  def task_build(context: Context) -> None:
19
21
  """
20
22
  Build package artifacts.
@@ -33,7 +35,9 @@ def get_task_build() -> Task[Callable[[Context], None]]:
33
35
 
34
36
 
35
37
  def get_task_publish(*, secrets_poetry_path: Path) -> Task[Callable[[Context], None]]:
36
- @task(name="publish") # pyright: ignore[reportUntypedFunctionDecorator]
38
+ @task(
39
+ name="publish", auto_shortflags=False
40
+ ) # pyright: ignore[reportUntypedFunctionDecorator]
37
41
  def task_publish(context: Context) -> None:
38
42
  """
39
43
  Publish package to PyPI.
@@ -21,62 +21,94 @@ from fogies.tools.terraform import (
21
21
  )
22
22
 
23
23
 
24
+ def validate_init_params(*, init: bool, init_params: InitParams) -> None:
25
+ """Raise ValueError for task flags that cannot be given together.
26
+
27
+ The init options only apply when init runs, and Terraform does not allow
28
+ migrating state while also reconfiguring the backend.
29
+ """
30
+ if init_params.migrate_state and not init:
31
+ raise ValueError("--init-migrate-state requires --init")
32
+ if init_params.reconfigure and not init:
33
+ raise ValueError("--init-reconfigure requires --init")
34
+ if init_params.upgrade and not init:
35
+ raise ValueError("--init-upgrade requires --init")
36
+ if init_params.migrate_state and init_params.reconfigure:
37
+ raise ValueError(
38
+ "--init-migrate-state and --init-reconfigure cannot be combined"
39
+ )
40
+
41
+
24
42
  def get_task_apply(
25
43
  *,
26
44
  binary_cache_path: pathlib.Path,
27
45
  module_path: pathlib.Path,
28
46
  aws_environ_factory: AwsEnvironFactory | None = None,
29
- backend: BackendConfig | None = None,
47
+ backend_config: BackendConfig | None = None,
30
48
  backend_status_path: pathlib.Path | None = None,
31
49
  tfbackend_factory: TfbackendFactory | None = None,
32
50
  tfvars_factory: TfvarsFactory | None = None,
33
51
  default_init: bool = True,
34
- default_init_upgrade: bool = False,
52
+ default_init_migrate_state: bool = False,
35
53
  default_init_reconfigure: bool = False,
54
+ default_init_upgrade: bool = False,
36
55
  default_apply_auto_approve: bool = False,
37
- ) -> Task[Callable[[Context, bool, bool, bool, bool], None]]:
38
- if backend is not None and tfbackend_factory is None:
39
- raise ValueError("tfbackend_factory is required when backend is set")
40
- if (backend_status_path is None) != (backend is None):
41
- raise ValueError("backend_status_path and backend must be provided together")
56
+ ) -> Task[Callable[[Context, bool, bool, bool, bool, bool], None]]:
57
+ if backend_config is not None and tfbackend_factory is None:
58
+ raise ValueError("tfbackend_factory is required when backend_config is set")
59
+ if (backend_config is not None or tfbackend_factory is not None) and (
60
+ aws_environ_factory is None
61
+ ):
62
+ raise ValueError(
63
+ "aws_environ_factory is required when backend_config or tfbackend_factory is set"
64
+ )
65
+ if (backend_status_path is None) != (backend_config is None):
66
+ raise ValueError(
67
+ "backend_status_path and backend_config must be provided together"
68
+ )
42
69
 
43
70
  @task(
44
71
  name="apply",
72
+ auto_shortflags=False,
45
73
  help={
46
74
  "init": "Include terraform init. Skip with --no-init.",
47
- "init_upgrade": "During init, upgrade providers.",
75
+ "init_migrate_state": "During init, migrate state to a changed backend.",
48
76
  "init_reconfigure": "During init, reconfigure the backend.",
77
+ "init_upgrade": "During init, upgrade providers.",
49
78
  "apply_auto_approve": "Skip interactive confirmation.",
50
79
  },
51
80
  ) # pyright: ignore[reportUntypedFunctionDecorator]
52
81
  def task_apply(
53
82
  context: Context,
54
83
  init: bool = default_init,
55
- init_upgrade: bool = default_init_upgrade,
84
+ init_migrate_state: bool = default_init_migrate_state,
56
85
  init_reconfigure: bool = default_init_reconfigure,
86
+ init_upgrade: bool = default_init_upgrade,
57
87
  apply_auto_approve: bool = default_apply_auto_approve,
58
88
  ) -> None:
59
89
  """
60
90
  Apply a Terraform configuration.
61
91
  """
62
- if init_upgrade and not init:
63
- raise ValueError("--init-upgrade requires --init")
64
- if init_reconfigure and not init:
65
- raise ValueError("--init-reconfigure requires --init")
92
+ init_params = InitParams(
93
+ migrate_state=init_migrate_state,
94
+ reconfigure=init_reconfigure,
95
+ upgrade=init_upgrade,
96
+ )
97
+ validate_init_params(init=init, init_params=init_params)
66
98
 
67
99
  command_params = CommandParams(context=context)
68
- init_params = (
69
- InitParams(upgrade=init_upgrade, reconfigure=init_reconfigure)
70
- if init
71
- else None
72
- )
73
100
 
74
101
  with ExitStack() as stack:
75
- if aws_environ_factory is not None:
76
- _ = stack.enter_context(aws_environ_factory())
102
+ aws_environ = (
103
+ stack.enter_context(aws_environ_factory())
104
+ if aws_environ_factory
105
+ else None
106
+ )
77
107
 
78
108
  tfbackend_path = (
79
- stack.enter_context(tfbackend_factory()) if tfbackend_factory else None
109
+ stack.enter_context(tfbackend_factory(aws_environ))
110
+ if tfbackend_factory and aws_environ
111
+ else None
80
112
  )
81
113
  tfvars_path = (
82
114
  stack.enter_context(tfvars_factory()) if tfvars_factory else None
@@ -87,7 +119,8 @@ def get_task_apply(
87
119
  binary_cache_path=binary_cache_path,
88
120
  command_params=command_params,
89
121
  module_path=module_path,
90
- backend=backend,
122
+ backend_config=backend_config,
123
+ aws_environ=aws_environ,
91
124
  backend_status_path=backend_status_path,
92
125
  tfbackend_path=tfbackend_path,
93
126
  tfvars_path=tfvars_path,
@@ -99,7 +132,9 @@ def get_task_apply(
99
132
  )
100
133
  )
101
134
 
102
- return cast(Task[Callable[[Context, bool, bool, bool, bool], None]], task_apply)
135
+ return cast(
136
+ Task[Callable[[Context, bool, bool, bool, bool, bool], None]], task_apply
137
+ )
103
138
 
104
139
 
105
140
  def get_task_destroy(
@@ -107,57 +142,71 @@ def get_task_destroy(
107
142
  binary_cache_path: pathlib.Path,
108
143
  module_path: pathlib.Path,
109
144
  aws_environ_factory: AwsEnvironFactory | None = None,
110
- backend: BackendConfig | None = None,
145
+ backend_config: BackendConfig | None = None,
111
146
  backend_status_path: pathlib.Path | None = None,
112
147
  tfbackend_factory: TfbackendFactory | None = None,
113
148
  tfvars_factory: TfvarsFactory | None = None,
114
149
  default_init: bool = True,
115
- default_init_upgrade: bool = False,
150
+ default_init_migrate_state: bool = False,
116
151
  default_init_reconfigure: bool = False,
152
+ default_init_upgrade: bool = False,
117
153
  default_destroy_auto_approve: bool = False,
118
- ) -> Task[Callable[[Context, bool, bool, bool, bool], None]]:
119
- if backend is not None and tfbackend_factory is None:
120
- raise ValueError("tfbackend_factory is required when backend is set")
121
- if (backend_status_path is None) != (backend is None):
122
- raise ValueError("backend_status_path and backend must be provided together")
154
+ ) -> Task[Callable[[Context, bool, bool, bool, bool, bool], None]]:
155
+ if backend_config is not None and tfbackend_factory is None:
156
+ raise ValueError("tfbackend_factory is required when backend_config is set")
157
+ if (backend_config is not None or tfbackend_factory is not None) and (
158
+ aws_environ_factory is None
159
+ ):
160
+ raise ValueError(
161
+ "aws_environ_factory is required when backend_config or tfbackend_factory is set"
162
+ )
163
+ if (backend_status_path is None) != (backend_config is None):
164
+ raise ValueError(
165
+ "backend_status_path and backend_config must be provided together"
166
+ )
123
167
 
124
168
  @task(
125
169
  name="destroy",
170
+ auto_shortflags=False,
126
171
  help={
127
172
  "init": "Include terraform init. Skip with --no-init.",
128
- "init_upgrade": "During init, upgrade providers.",
173
+ "init_migrate_state": "During init, migrate state to a changed backend.",
129
174
  "init_reconfigure": "During init, reconfigure the backend.",
175
+ "init_upgrade": "During init, upgrade providers.",
130
176
  "destroy_auto_approve": "Skip interactive confirmation.",
131
177
  },
132
178
  ) # pyright: ignore[reportUntypedFunctionDecorator]
133
179
  def task_destroy(
134
180
  context: Context,
135
181
  init: bool = default_init,
136
- init_upgrade: bool = default_init_upgrade,
182
+ init_migrate_state: bool = default_init_migrate_state,
137
183
  init_reconfigure: bool = default_init_reconfigure,
184
+ init_upgrade: bool = default_init_upgrade,
138
185
  destroy_auto_approve: bool = default_destroy_auto_approve,
139
186
  ) -> None:
140
187
  """
141
188
  Destroy a Terraform configuration.
142
189
  """
143
- if init_upgrade and not init:
144
- raise ValueError("--init-upgrade requires --init")
145
- if init_reconfigure and not init:
146
- raise ValueError("--init-reconfigure requires --init")
190
+ init_params = InitParams(
191
+ migrate_state=init_migrate_state,
192
+ reconfigure=init_reconfigure,
193
+ upgrade=init_upgrade,
194
+ )
195
+ validate_init_params(init=init, init_params=init_params)
147
196
 
148
197
  command_params = CommandParams(context=context)
149
- init_params = (
150
- InitParams(upgrade=init_upgrade, reconfigure=init_reconfigure)
151
- if init
152
- else None
153
- )
154
198
 
155
199
  with ExitStack() as stack:
156
- if aws_environ_factory is not None:
157
- _ = stack.enter_context(aws_environ_factory())
200
+ aws_environ = (
201
+ stack.enter_context(aws_environ_factory())
202
+ if aws_environ_factory
203
+ else None
204
+ )
158
205
 
159
206
  tfbackend_path = (
160
- stack.enter_context(tfbackend_factory()) if tfbackend_factory else None
207
+ stack.enter_context(tfbackend_factory(aws_environ))
208
+ if tfbackend_factory and aws_environ
209
+ else None
161
210
  )
162
211
  tfvars_path = (
163
212
  stack.enter_context(tfvars_factory()) if tfvars_factory else None
@@ -168,7 +217,8 @@ def get_task_destroy(
168
217
  binary_cache_path=binary_cache_path,
169
218
  command_params=command_params,
170
219
  module_path=module_path,
171
- backend=backend,
220
+ backend_config=backend_config,
221
+ aws_environ=aws_environ,
172
222
  backend_status_path=backend_status_path,
173
223
  tfbackend_path=tfbackend_path,
174
224
  tfvars_path=tfvars_path,
@@ -179,7 +229,9 @@ def get_task_destroy(
179
229
  )
180
230
  )
181
231
 
182
- return cast(Task[Callable[[Context, bool, bool, bool, bool], None]], task_destroy)
232
+ return cast(
233
+ Task[Callable[[Context, bool, bool, bool, bool, bool], None]], task_destroy
234
+ )
183
235
 
184
236
 
185
237
  def get_collection(
@@ -188,7 +240,7 @@ def get_collection(
188
240
  binary_cache_path: pathlib.Path,
189
241
  module_path: pathlib.Path,
190
242
  aws_environ_factory: AwsEnvironFactory | None = None,
191
- backend: BackendConfig | None = None,
243
+ backend_config: BackendConfig | None = None,
192
244
  backend_status_path: pathlib.Path | None = None,
193
245
  tfbackend_factory: TfbackendFactory | None = None,
194
246
  tfvars_factory: TfvarsFactory | None = None,
@@ -200,7 +252,7 @@ def get_collection(
200
252
  binary_cache_path=binary_cache_path,
201
253
  module_path=module_path,
202
254
  aws_environ_factory=aws_environ_factory,
203
- backend=backend,
255
+ backend_config=backend_config,
204
256
  backend_status_path=backend_status_path,
205
257
  tfbackend_factory=tfbackend_factory,
206
258
  tfvars_factory=tfvars_factory,
@@ -211,7 +263,7 @@ def get_collection(
211
263
  binary_cache_path=binary_cache_path,
212
264
  module_path=module_path,
213
265
  aws_environ_factory=aws_environ_factory,
214
- backend=backend,
266
+ backend_config=backend_config,
215
267
  backend_status_path=backend_status_path,
216
268
  tfbackend_factory=tfbackend_factory,
217
269
  tfvars_factory=tfvars_factory,