@pennyfarthing/core 7.8.1 → 7.8.2

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 (126) hide show
  1. package/package.json +2 -1
  2. package/pennyfarthing-dist/scripts/core/prime.sh +8 -0
  3. package/pennyfarthing_scripts/__init__.py +17 -0
  4. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  5. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  6. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  7. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  8. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  9. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  10. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  11. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  12. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  13. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  14. package/pennyfarthing_scripts/bellmode_hook.py +154 -0
  15. package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
  16. package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
  17. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  18. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  19. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  20. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  21. package/pennyfarthing_scripts/brownfield/cli.py +131 -0
  22. package/pennyfarthing_scripts/brownfield/discover.py +753 -0
  23. package/pennyfarthing_scripts/common/__init__.py +49 -0
  24. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  25. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  26. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  27. package/pennyfarthing_scripts/common/config.py +65 -0
  28. package/pennyfarthing_scripts/common/output.py +180 -0
  29. package/pennyfarthing_scripts/config.py +21 -0
  30. package/pennyfarthing_scripts/git/__init__.py +29 -0
  31. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  32. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  33. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  34. package/pennyfarthing_scripts/git/create_branches.py +439 -0
  35. package/pennyfarthing_scripts/git/status_all.py +310 -0
  36. package/pennyfarthing_scripts/hooks.py +455 -0
  37. package/pennyfarthing_scripts/jira/__init__.py +93 -0
  38. package/pennyfarthing_scripts/jira/__main__.py +10 -0
  39. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  40. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  41. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  42. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  43. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  44. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  45. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  46. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  47. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  48. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  49. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  50. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  51. package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
  52. package/pennyfarthing_scripts/jira/claim.py +211 -0
  53. package/pennyfarthing_scripts/jira/cli.py +150 -0
  54. package/pennyfarthing_scripts/jira/client.py +613 -0
  55. package/pennyfarthing_scripts/jira/epic.py +176 -0
  56. package/pennyfarthing_scripts/jira/story.py +219 -0
  57. package/pennyfarthing_scripts/jira/sync.py +350 -0
  58. package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
  59. package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
  60. package/pennyfarthing_scripts/jira_sync.py +36 -0
  61. package/pennyfarthing_scripts/jira_sync_story.py +30 -0
  62. package/pennyfarthing_scripts/output.py +37 -0
  63. package/pennyfarthing_scripts/preflight/__init__.py +17 -0
  64. package/pennyfarthing_scripts/preflight/__main__.py +10 -0
  65. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  66. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  67. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  68. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  69. package/pennyfarthing_scripts/preflight/cli.py +141 -0
  70. package/pennyfarthing_scripts/preflight/finish.py +382 -0
  71. package/pennyfarthing_scripts/pretooluse_hook.py +142 -0
  72. package/pennyfarthing_scripts/prime/__init__.py +38 -0
  73. package/pennyfarthing_scripts/prime/__main__.py +8 -0
  74. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  75. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  76. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  77. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  78. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  79. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  80. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  81. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  82. package/pennyfarthing_scripts/prime/cli.py +220 -0
  83. package/pennyfarthing_scripts/prime/loader.py +239 -0
  84. package/pennyfarthing_scripts/sprint/__init__.py +66 -0
  85. package/pennyfarthing_scripts/sprint/__main__.py +10 -0
  86. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  87. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  88. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  89. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  90. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  91. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  92. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  93. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  94. package/pennyfarthing_scripts/sprint/archive.py +108 -0
  95. package/pennyfarthing_scripts/sprint/cli.py +124 -0
  96. package/pennyfarthing_scripts/sprint/loader.py +193 -0
  97. package/pennyfarthing_scripts/sprint/status.py +122 -0
  98. package/pennyfarthing_scripts/sprint/validator.py +405 -0
  99. package/pennyfarthing_scripts/sprint/work.py +192 -0
  100. package/pennyfarthing_scripts/story/__init__.py +67 -0
  101. package/pennyfarthing_scripts/story/__main__.py +10 -0
  102. package/pennyfarthing_scripts/story/cli.py +105 -0
  103. package/pennyfarthing_scripts/story/create.py +167 -0
  104. package/pennyfarthing_scripts/story/size.py +113 -0
  105. package/pennyfarthing_scripts/story/template.py +151 -0
  106. package/pennyfarthing_scripts/swebench.py +216 -0
  107. package/pennyfarthing_scripts/tests/__init__.py +1 -0
  108. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  109. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  110. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  111. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  112. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  113. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  114. package/pennyfarthing_scripts/tests/conftest.py +106 -0
  115. package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
  116. package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
  117. package/pennyfarthing_scripts/tests/test_common.py +180 -0
  118. package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
  119. package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
  120. package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
  121. package/pennyfarthing_scripts/tests/test_prime.py +397 -0
  122. package/pennyfarthing_scripts/tests/test_sprint_package.py +236 -0
  123. package/pennyfarthing_scripts/tests/test_sprint_validator.py +675 -0
  124. package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
  125. package/pennyfarthing_scripts/welcome_hook.py +157 -0
  126. package/pennyfarthing_scripts/workflow.py +183 -0
@@ -0,0 +1,176 @@
1
+ """
2
+ Create Jira epics from Pennyfarthing sprint YAML definitions.
3
+
4
+ Usage:
5
+ python -m pennyfarthing_scripts.jira create epic <epic_id> [options]
6
+
7
+ Options:
8
+ --dry-run Show what would be done without making changes
9
+
10
+ Examples:
11
+ python -m pennyfarthing_scripts.jira create epic epic-63 --dry-run
12
+ python -m pennyfarthing_scripts.jira create epic 63
13
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from typing import Any
19
+
20
+ from pennyfarthing_scripts.jira.client import JiraClient, JIRA_PROJECT
21
+ from pennyfarthing_scripts.sprint.loader import find_epic, load_sprint as load_current_sprint
22
+
23
+
24
+ def parse_args(args: list[str] | None = None) -> argparse.Namespace:
25
+ """Parse command line arguments.
26
+
27
+ Args:
28
+ args: Command line arguments (defaults to sys.argv[1:])
29
+
30
+ Returns:
31
+ Parsed arguments namespace
32
+ """
33
+ parser = argparse.ArgumentParser(
34
+ description="Create Jira epic from Pennyfarthing sprint YAML"
35
+ )
36
+ parser.add_argument("epic_id", help="Epic ID (e.g., epic-63 or 63)")
37
+ parser.add_argument("--dry-run", action="store_true", help="Show changes without applying")
38
+
39
+ return parser.parse_args(args)
40
+
41
+
42
+ def build_epic_payload(epic_data: dict[str, Any]) -> dict[str, Any]:
43
+ """Build Jira API payload for creating an epic.
44
+
45
+ Args:
46
+ epic_data: Epic data from sprint YAML
47
+
48
+ Returns:
49
+ Jira API request payload
50
+ """
51
+ title = epic_data.get("title", "")
52
+ description = epic_data.get("description", "")
53
+
54
+ # Build ADF (Atlassian Document Format) for description
55
+ description_adf = {
56
+ "type": "doc",
57
+ "version": 1,
58
+ "content": [
59
+ {
60
+ "type": "paragraph",
61
+ "content": [{"type": "text", "text": description}] if description else [],
62
+ }
63
+ ],
64
+ }
65
+
66
+ return {
67
+ "fields": {
68
+ "project": {"key": JIRA_PROJECT},
69
+ "summary": title,
70
+ "description": description_adf,
71
+ "issuetype": {"name": "Epic"},
72
+ }
73
+ }
74
+
75
+
76
+ def create_epic(
77
+ title: str,
78
+ description: str = "",
79
+ *,
80
+ dry_run: bool = False,
81
+ ) -> dict[str, Any]:
82
+ """Create a Jira epic.
83
+
84
+ Args:
85
+ title: Epic title/summary
86
+ description: Epic description
87
+ dry_run: If True, show changes without applying
88
+
89
+ Returns:
90
+ Result dict with success, key, error fields
91
+ """
92
+ epic_data = {"title": title, "description": description}
93
+ payload = build_epic_payload(epic_data)
94
+
95
+ if dry_run:
96
+ return {
97
+ "success": True,
98
+ "dry_run": True,
99
+ "payload": payload,
100
+ }
101
+
102
+ client = JiraClient()
103
+ response = client.create_issue_sync(payload)
104
+
105
+ if response and "key" in response:
106
+ return {
107
+ "success": True,
108
+ "key": response["key"],
109
+ "id": response.get("id"),
110
+ }
111
+ else:
112
+ return {
113
+ "success": False,
114
+ "error": "Failed to create epic",
115
+ "response": response,
116
+ }
117
+
118
+
119
+ def create_epic_from_yaml(epic_id: str, *, dry_run: bool = False) -> dict[str, Any]:
120
+ """Create a Jira epic from sprint YAML definition.
121
+
122
+ Args:
123
+ epic_id: Epic ID (e.g., "epic-63" or "63")
124
+ dry_run: If True, show changes without applying
125
+
126
+ Returns:
127
+ Result dict with success, key, error fields
128
+ """
129
+ sprint_data = load_current_sprint()
130
+ if not sprint_data:
131
+ return {"success": False, "error": "Could not load sprint YAML"}
132
+
133
+ epic = find_epic(sprint_data, epic_id)
134
+ if not epic:
135
+ return {"success": False, "error": f"Epic '{epic_id}' not found in sprint YAML"}
136
+
137
+ # Check if epic already has a Jira key
138
+ if epic.get("jira"):
139
+ return {
140
+ "success": False,
141
+ "error": f"Epic already has Jira key: {epic['jira']}",
142
+ }
143
+
144
+ title = epic.get("title", f"Epic {epic_id}")
145
+ description = epic.get("description", "")
146
+
147
+ return create_epic(title, description, dry_run=dry_run)
148
+
149
+
150
+ def main(args: list[str] | None = None) -> int:
151
+ """CLI entry point.
152
+
153
+ Args:
154
+ args: Command line arguments (defaults to sys.argv[1:])
155
+
156
+ Returns:
157
+ Exit code (0 for success, 1 for failure)
158
+ """
159
+ parsed_args = parse_args(args)
160
+
161
+ result = create_epic_from_yaml(parsed_args.epic_id, dry_run=parsed_args.dry_run)
162
+
163
+ if result["success"]:
164
+ if result.get("dry_run"):
165
+ print(f"[DRY-RUN] Would create epic for {parsed_args.epic_id}")
166
+ print(f" Payload: {json.dumps(result.get('payload', {}), indent=2)}")
167
+ else:
168
+ print(f"Created epic: {result.get('key')}")
169
+ return 0
170
+ else:
171
+ print(f"Failed: {result.get('error', 'Unknown error')}", file=sys.stderr)
172
+ return 1
173
+
174
+
175
+ if __name__ == "__main__":
176
+ sys.exit(main())
@@ -0,0 +1,219 @@
1
+ """
2
+ Sync a single story between Pennyfarthing sprint YAML and Jira.
3
+
4
+ Usage:
5
+ python -m pennyfarthing_scripts.jira story <story_key> [options]
6
+
7
+ Options:
8
+ --transition Sync status (transition Jira issue)
9
+ --points Sync story points
10
+ --comment MSG Add comment to issue
11
+ --dry-run Show what would be done without making changes
12
+
13
+ Examples:
14
+ python -m pennyfarthing_scripts.jira story 63-7 --transition
15
+ python -m pennyfarthing_scripts.jira story MSSCI-12401 --points --dry-run
16
+ """
17
+
18
+ import argparse
19
+ import sys
20
+ from typing import Any
21
+
22
+ from pennyfarthing_scripts.jira import client as jira_client
23
+ from pennyfarthing_scripts.sprint.loader import (
24
+ find_epic,
25
+ find_story,
26
+ get_story_by_id,
27
+ load_sprint as load_current_sprint,
28
+ )
29
+
30
+
31
+ def parse_args(args: list[str] | None = None) -> argparse.Namespace:
32
+ """Parse command line arguments.
33
+
34
+ Args:
35
+ args: Command line arguments (defaults to sys.argv[1:])
36
+
37
+ Returns:
38
+ Parsed arguments namespace
39
+ """
40
+ parser = argparse.ArgumentParser(
41
+ description="Sync a single story between Pennyfarthing and Jira"
42
+ )
43
+ parser.add_argument("story_key", help="Story ID (e.g., 63-7) or Jira key (MSSCI-12401)")
44
+ parser.add_argument("--transition", action="store_true", help="Sync status")
45
+ parser.add_argument("--points", action="store_true", help="Sync story points")
46
+ parser.add_argument("--comment", type=str, help="Add comment to issue")
47
+ parser.add_argument("--dry-run", action="store_true", help="Show changes without applying")
48
+
49
+ return parser.parse_args(args)
50
+
51
+
52
+ def get_story_from_sprint(story_key: str) -> dict[str, Any] | None:
53
+ """Find a story in the sprint YAML.
54
+
55
+ Args:
56
+ story_key: Story ID or Jira key
57
+
58
+ Returns:
59
+ Story dict if found, None otherwise
60
+ """
61
+ # Try direct lookup first
62
+ story = get_story_by_id(story_key)
63
+ if story:
64
+ return story
65
+
66
+ # If it looks like a local ID (e.g., "63-7"), try finding via epic
67
+ if "-" in story_key and not story_key.startswith("MSSCI"):
68
+ sprint_data = load_current_sprint()
69
+ if sprint_data:
70
+ parts = story_key.split("-")
71
+ if len(parts) >= 2:
72
+ epic_num = parts[0]
73
+ epic = find_epic(sprint_data, epic_num)
74
+ if epic:
75
+ return find_story(epic, story_key)
76
+
77
+ return None
78
+
79
+
80
+ def fetch_jira_issue(jira_key: str) -> dict[str, Any] | None:
81
+ """Fetch issue from Jira.
82
+
83
+ Args:
84
+ jira_key: Jira issue key
85
+
86
+ Returns:
87
+ Issue JSON if found, None otherwise
88
+ """
89
+ return jira_client.get_issue(jira_key)
90
+
91
+
92
+ def sync_story(
93
+ story_key: str,
94
+ *,
95
+ do_transition: bool = False,
96
+ sync_points: bool = False,
97
+ comment: str | None = None,
98
+ dry_run: bool = False,
99
+ ) -> dict[str, Any]:
100
+ """Sync a story between Pennyfarthing and Jira.
101
+
102
+ Args:
103
+ story_key: Story ID or Jira key
104
+ do_transition: Whether to sync status
105
+ sync_points: Whether to sync story points
106
+ comment: Comment to add (optional)
107
+ dry_run: If True, show changes without applying
108
+
109
+ Returns:
110
+ Result dict with success, actions, error fields
111
+ """
112
+ # Find story in sprint YAML
113
+ story = get_story_from_sprint(story_key)
114
+ if not story:
115
+ return {
116
+ "success": False,
117
+ "error": f"Story '{story_key}' not found in sprint YAML",
118
+ }
119
+
120
+ jira_key = story.get("jira")
121
+ if not jira_key:
122
+ return {
123
+ "success": False,
124
+ "error": f"Story '{story_key}' has no Jira key",
125
+ }
126
+
127
+ # Fetch current Jira state
128
+ issue = fetch_jira_issue(jira_key)
129
+ if not issue:
130
+ return {
131
+ "success": False,
132
+ "error": f"Could not fetch Jira issue {jira_key}",
133
+ }
134
+
135
+ actions = []
136
+ errors = []
137
+
138
+ # Sync status if requested
139
+ if do_transition:
140
+ current_jira_status = jira_client.get_jira_field(issue, "fields.status.name")
141
+ target_status = jira_client.map_status_to_jira(story.get("status"))
142
+
143
+ if current_jira_status != target_status:
144
+ action = f"transition {jira_key}: {current_jira_status} -> {target_status}"
145
+ if dry_run:
146
+ actions.append(f"[DRY-RUN] {action}")
147
+ else:
148
+ if jira_client.update_issue_status(jira_key, target_status):
149
+ actions.append(action)
150
+ else:
151
+ errors.append(f"Failed to transition {jira_key}")
152
+ else:
153
+ actions.append(f"status already {current_jira_status}")
154
+
155
+ # Sync points if requested
156
+ if sync_points:
157
+ story_points = story.get("points")
158
+ if story_points is not None:
159
+ current_points = jira_client.get_story_points(jira_key, issue)
160
+ if current_points != story_points:
161
+ action = f"sync points {jira_key}: {current_points} -> {story_points}"
162
+ if dry_run:
163
+ actions.append(f"[DRY-RUN] {action}")
164
+ else:
165
+ # Note: point sync requires REST API, not CLI
166
+ actions.append(f"[SKIP] {action} (requires REST API)")
167
+
168
+ # Add comment if provided
169
+ if comment:
170
+ action = f"add comment to {jira_key}"
171
+ if dry_run:
172
+ actions.append(f"[DRY-RUN] {action}")
173
+ else:
174
+ if jira_client.add_comment(jira_key, comment):
175
+ actions.append(action)
176
+ else:
177
+ errors.append(f"Failed to add comment to {jira_key}")
178
+
179
+ return {
180
+ "success": len(errors) == 0,
181
+ "story_id": story.get("id"),
182
+ "jira_key": jira_key,
183
+ "actions": actions,
184
+ "errors": errors if errors else None,
185
+ "dry_run": dry_run,
186
+ }
187
+
188
+
189
+ def main(args: list[str] | None = None) -> int:
190
+ """CLI entry point.
191
+
192
+ Args:
193
+ args: Command line arguments (defaults to sys.argv[1:])
194
+
195
+ Returns:
196
+ Exit code (0 for success, 1 for failure)
197
+ """
198
+ parsed_args = parse_args(args)
199
+
200
+ result = sync_story(
201
+ parsed_args.story_key,
202
+ do_transition=parsed_args.transition,
203
+ sync_points=parsed_args.points,
204
+ comment=parsed_args.comment,
205
+ dry_run=parsed_args.dry_run,
206
+ )
207
+
208
+ if result["success"]:
209
+ print(f"Synced {result.get('story_id', parsed_args.story_key)}")
210
+ for action in result.get("actions", []):
211
+ print(f" {action}")
212
+ return 0
213
+ else:
214
+ print(f"Failed: {result.get('error', 'Unknown error')}", file=sys.stderr)
215
+ return 1
216
+
217
+
218
+ if __name__ == "__main__":
219
+ sys.exit(main())