@flydocs/cli 0.6.0-alpha.1 → 0.6.0-alpha.10

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 (50) hide show
  1. package/dist/cli.js +504 -254
  2. package/package.json +1 -1
  3. package/template/.claude/CLAUDE.md +11 -9
  4. package/template/.claude/commands/flydocs-setup.md +114 -17
  5. package/template/.claude/commands/flydocs-upgrade.md +27 -15
  6. package/template/.claude/commands/knowledge.md +61 -0
  7. package/template/.claude/skills/flydocs-cloud/SKILL.md +44 -31
  8. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +10 -4
  9. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +22 -2
  10. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +39 -0
  11. package/template/.claude/skills/flydocs-cloud/scripts/delete_milestone.py +21 -0
  12. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +9 -5
  13. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +11 -0
  14. package/template/.claude/skills/flydocs-cloud/scripts/get_estimate_scale.py +23 -0
  15. package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +19 -0
  16. package/template/.claude/skills/flydocs-cloud/scripts/list_statuses.py +19 -0
  17. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +1 -1
  18. package/template/.claude/skills/flydocs-cloud/scripts/refresh_labels.py +87 -0
  19. package/template/.claude/skills/flydocs-cloud/scripts/set_identity.py +38 -0
  20. package/template/.claude/skills/flydocs-cloud/scripts/set_preferences.py +49 -0
  21. package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +46 -0
  22. package/template/.claude/skills/flydocs-cloud/scripts/set_status_mapping.py +69 -0
  23. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +5 -4
  24. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +22 -4
  25. package/template/.claude/skills/flydocs-cloud/scripts/update_milestone.py +42 -0
  26. package/template/.claude/skills/flydocs-cloud/scripts/validate_setup.py +139 -0
  27. package/template/.claude/skills/flydocs-local/SKILL.md +1 -1
  28. package/template/.claude/skills/flydocs-local/scripts/assign.py +13 -4
  29. package/template/.claude/skills/flydocs-local/scripts/flydocs_api.py +5 -2
  30. package/template/.claude/skills/flydocs-workflow/SKILL.md +23 -18
  31. package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
  32. package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +105 -0
  33. package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
  34. package/template/.claude/skills/flydocs-workflow/session.md +24 -16
  35. package/template/.claude/skills/flydocs-workflow/stages/capture.md +8 -3
  36. package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
  37. package/template/.claude/skills/flydocs-workflow/stages/implement.md +28 -4
  38. package/template/.claude/skills/flydocs-workflow/stages/refine.md +20 -4
  39. package/template/.claude/skills/flydocs-workflow/stages/review.md +14 -2
  40. package/template/.flydocs/config.json +4 -18
  41. package/template/.flydocs/hooks/prompt-submit.py +27 -4
  42. package/template/.flydocs/version +1 -1
  43. package/template/AGENTS.md +8 -8
  44. package/template/CHANGELOG.md +39 -0
  45. package/template/flydocs/knowledge/INDEX.md +38 -53
  46. package/template/flydocs/knowledge/README.md +60 -9
  47. package/template/flydocs/knowledge/templates/decision.md +47 -0
  48. package/template/flydocs/knowledge/templates/feature.md +35 -0
  49. package/template/flydocs/knowledge/templates/note.md +25 -0
  50. package/template/manifest.json +8 -2
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+ """Delete a milestone via the FlyDocs Relay API."""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent))
8
+ from flydocs_api import get_client, output_json, fail
9
+
10
+ if len(sys.argv) < 2:
11
+ fail("Usage: delete_milestone.py <milestone_id>")
12
+
13
+ milestone_id = sys.argv[1]
14
+
15
+ client = get_client()
16
+ result = client.delete(f"/milestones/{milestone_id}")
17
+
18
+ output_json({
19
+ "success": result.get("success", True),
20
+ "id": result.get("id", milestone_id),
21
+ })
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env python3
2
- """Set estimate on an issue via the FlyDocs Relay API."""
2
+ """Set estimate on an issue via the FlyDocs Relay API.
3
+
4
+ The relay validates the estimate against the provider's scale server-side.
5
+ Use get_estimate_scale.py to discover valid values before setting.
6
+ """
3
7
 
4
8
  import sys
5
9
  from pathlib import Path
@@ -8,16 +12,16 @@ sys.path.insert(0, str(Path(__file__).parent))
8
12
  from flydocs_api import get_client, output_json, fail
9
13
 
10
14
  if len(sys.argv) < 3:
11
- fail("Usage: estimate.py <ref> <1-5>")
15
+ fail("Usage: estimate.py <ref> <points>")
12
16
 
13
17
  ref = sys.argv[1]
14
18
  try:
15
19
  estimate = int(sys.argv[2])
16
20
  except ValueError:
17
- fail("Estimate must be a number (1-5)")
21
+ fail("Estimate must be a number")
18
22
 
19
- if estimate not in (1, 2, 3, 5):
20
- fail("Estimate must be 1 (XS), 2 (S), 3 (M), or 5 (L)")
23
+ if estimate < 0:
24
+ fail("Estimate must be a non-negative integer")
21
25
 
22
26
  client = get_client()
23
27
  result = client.put(f"/issues/{ref}/estimate", {"estimate": estimate})
@@ -37,6 +37,12 @@ class FlyDocsClient:
37
37
  print("Set in environment or .env/.env.local file", file=sys.stderr)
38
38
  sys.exit(1)
39
39
 
40
+ self.workspace_id = self.config.get("workspaceId")
41
+ if not self.workspace_id:
42
+ print("ERROR: workspaceId not found in .flydocs/config.json", file=sys.stderr)
43
+ print("Run 'flydocs setup' to configure your workspace", file=sys.stderr)
44
+ sys.exit(1)
45
+
40
46
  self.base_url = self._resolve_base_url()
41
47
 
42
48
  def _load_config(self) -> dict:
@@ -93,6 +99,7 @@ class FlyDocsClient:
93
99
 
94
100
  headers = {
95
101
  "Authorization": f"Bearer {self.api_key}",
102
+ "X-Workspace": self.workspace_id,
96
103
  "Content-Type": "application/json",
97
104
  "Accept": "application/json",
98
105
  }
@@ -160,6 +167,10 @@ class FlyDocsClient:
160
167
  """PATCH request to relay API."""
161
168
  return self._request("PATCH", path, body=body)
162
169
 
170
+ def delete(self, path: str) -> dict:
171
+ """DELETE request to relay API."""
172
+ return self._request("DELETE", path)
173
+
163
174
  def _log_operation(self, method: str, path: str, status: int, result: dict | list):
164
175
  """Log operation metadata to local log file."""
165
176
  try:
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env python3
2
+ """Get the provider's estimate scale via the FlyDocs Relay API.
3
+
4
+ Returns the valid estimate values for the connected provider.
5
+ Linear: fixed scale [0, 1, 2, 3, 5, 8, 13, 21]
6
+ Jira: freeform (any positive number)
7
+ """
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ sys.path.insert(0, str(Path(__file__).parent))
13
+ from flydocs_api import get_client, output_json
14
+
15
+
16
+ def main():
17
+ client = get_client()
18
+ result = client.get("/auth/estimates")
19
+ output_json(result)
20
+
21
+
22
+ if __name__ == "__main__":
23
+ main()
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python3
2
+ """List available providers via the FlyDocs Relay API."""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent))
8
+ from flydocs_api import get_client, output_json
9
+
10
+
11
+ def main():
12
+ client = get_client()
13
+ result = client.get("/providers")
14
+
15
+ output_json(result)
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python3
2
+ """List provider workflow states via the FlyDocs Relay API."""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent))
8
+ from flydocs_api import get_client, output_json
9
+
10
+
11
+ def main():
12
+ client = get_client()
13
+ result = client.get("/auth/statuses")
14
+
15
+ output_json(result)
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """List available teams via the FlyDocs Relay API."""
2
+ """List available teams/projects via the FlyDocs Relay API."""
3
3
 
4
4
  import sys
5
5
  from pathlib import Path
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ """Refresh label config from the relay — validates and updates local config.
3
+
4
+ Fetches current team labels from the relay, compares against local config,
5
+ and reports any stale or missing label IDs. With --fix, updates local config
6
+ to match the relay's current state.
7
+ """
8
+
9
+ import json
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ sys.path.insert(0, str(Path(__file__).parent))
14
+ from flydocs_api import get_client, output_json, fail
15
+
16
+
17
+ def main():
18
+ fix_mode = "--fix" in sys.argv
19
+
20
+ client = get_client()
21
+
22
+ # Fetch current labels from relay
23
+ labels = client.get("/labels")
24
+ label_map = {l["id"]: l["name"] for l in labels}
25
+ label_by_name = {l["name"].lower(): l["id"] for l in labels}
26
+
27
+ # Load local config
28
+ config_path = client.config_path
29
+ if not config_path.exists():
30
+ fail("No .flydocs/config.json found")
31
+
32
+ with open(config_path, "r") as f:
33
+ config = json.load(f)
34
+
35
+ issue_labels = config.get("issueLabels", {})
36
+ stale: list[dict] = []
37
+ valid: list[dict] = []
38
+
39
+ # Check each label ID in config against relay
40
+ for category, entries in issue_labels.items():
41
+ if isinstance(entries, dict):
42
+ for key, label_id in entries.items():
43
+ if label_id in label_map:
44
+ valid.append({"category": category, "key": key, "id": label_id, "name": label_map[label_id]})
45
+ else:
46
+ # Try to find by key name
47
+ resolved = label_by_name.get(key.lower())
48
+ stale.append({
49
+ "category": category,
50
+ "key": key,
51
+ "staleId": label_id,
52
+ "resolvedId": resolved,
53
+ "resolvedName": key if resolved else None,
54
+ })
55
+
56
+ if fix_mode and stale:
57
+ # Update stale IDs in config
58
+ fixed = 0
59
+ for item in stale:
60
+ if item["resolvedId"]:
61
+ issue_labels[item["category"]][item["key"]] = item["resolvedId"]
62
+ fixed += 1
63
+
64
+ with open(config_path, "w") as f:
65
+ json.dump(config, f, indent=2)
66
+ f.write("\n")
67
+
68
+ output_json({
69
+ "success": True,
70
+ "valid": len(valid),
71
+ "stale": len(stale),
72
+ "fixed": fixed,
73
+ "unfixable": len(stale) - fixed,
74
+ "details": stale,
75
+ })
76
+ else:
77
+ output_json({
78
+ "valid": len(valid),
79
+ "stale": len(stale),
80
+ "totalProviderLabels": len(labels),
81
+ "details": stale if stale else "All label IDs are current",
82
+ "hint": "Run with --fix to update stale IDs" if stale else None,
83
+ })
84
+
85
+
86
+ if __name__ == "__main__":
87
+ main()
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env python3
2
+ """Set provider identity via the FlyDocs Relay API.
3
+
4
+ Binds the user's provider-specific ID to their FlyDocs user record.
5
+ Once set, ?mine=true resolves via exact provider ID matching.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).parent))
12
+ from flydocs_api import get_client, output_json, fail
13
+
14
+ VALID_PROVIDERS = ("linear", "jira", "gitlab")
15
+
16
+ if len(sys.argv) < 3:
17
+ fail(f"Usage: set_identity.py <provider> <provider-user-id>\n Providers: {', '.join(VALID_PROVIDERS)}")
18
+
19
+ provider = sys.argv[1].lower()
20
+ provider_id = sys.argv[2]
21
+
22
+ if provider not in VALID_PROVIDERS:
23
+ fail(f"Invalid provider: {provider}. Must be one of: {', '.join(VALID_PROVIDERS)}")
24
+
25
+ if not provider_id:
26
+ fail("Provider user ID cannot be empty")
27
+
28
+ client = get_client()
29
+ result = client.post("/auth/identity", {
30
+ "provider": provider,
31
+ "providerId": provider_id,
32
+ })
33
+
34
+ output_json({
35
+ "success": result.get("success", True),
36
+ "provider": result.get("provider", provider),
37
+ "providerId": result.get("providerId", provider_id),
38
+ })
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env python3
2
+ """Get or set user preferences via the FlyDocs Relay API.
3
+
4
+ With no arguments, returns current preferences (GET).
5
+ With flags, updates preferences (POST).
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ sys.path.insert(0, str(Path(__file__).parent))
13
+ from flydocs_api import get_client, output_json, fail
14
+
15
+
16
+ def main():
17
+ parser = argparse.ArgumentParser(description="Get or set user preferences")
18
+ parser.add_argument("--workspace", default=None, help="Default workspace ID")
19
+ parser.add_argument("--assignee", default=None, help="Default assignee ('self' or user ID)")
20
+ parser.add_argument("--display", default=None, help="Display preferences (JSON string)")
21
+ args = parser.parse_args()
22
+
23
+ client = get_client()
24
+
25
+ # If no flags provided, GET current preferences
26
+ if args.workspace is None and args.assignee is None and args.display is None:
27
+ result = client.get("/auth/preferences")
28
+ output_json(result)
29
+ return
30
+
31
+ # Build update body from provided flags
32
+ body: dict = {}
33
+ if args.workspace is not None:
34
+ body["defaultWorkspaceId"] = args.workspace
35
+ if args.assignee is not None:
36
+ body["defaultAssignee"] = args.assignee
37
+ if args.display is not None:
38
+ body["displayPreferences"] = args.display
39
+
40
+ result = client.post("/auth/preferences", body)
41
+
42
+ output_json({
43
+ "success": result.get("success", True),
44
+ "preferences": result.get("preferences", body),
45
+ })
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ """Set provider preference via the FlyDocs Relay API.
3
+
4
+ Stores the provider type on the relay (for server-side routing)
5
+ and updates the local config (for display/reference).
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ sys.path.insert(0, str(Path(__file__).parent))
14
+ from flydocs_api import get_client, output_json, fail
15
+
16
+
17
+ def main():
18
+ parser = argparse.ArgumentParser(description="Set provider preference")
19
+ parser.add_argument(
20
+ "provider_type",
21
+ choices=["linear", "jira"],
22
+ help="Provider type",
23
+ )
24
+ args = parser.parse_args()
25
+
26
+ client = get_client()
27
+ result = client.post("/auth/provider", {"providerType": args.provider_type})
28
+
29
+ # Update local config with provider type
30
+ config_path = client.config_path
31
+ if config_path.exists():
32
+ with open(config_path, "r") as f:
33
+ config = json.load(f)
34
+ if "provider" not in config:
35
+ config["provider"] = {"type": args.provider_type, "teamId": None}
36
+ else:
37
+ config["provider"]["type"] = args.provider_type
38
+ with open(config_path, "w") as f:
39
+ json.dump(config, f, indent=2)
40
+ f.write("\n")
41
+
42
+ output_json(result)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ """Set status mapping on the relay API key.
3
+
4
+ Maps provider workflow states to FlyDocs statuses. Pass "auto" for
5
+ case-insensitive auto-mapping, or provide a manual mapping object.
6
+
7
+ Usage:
8
+ set_status_mapping.py --auto
9
+ set_status_mapping.py --mapping '{"BACKLOG":"Backlog","IMPLEMENTING":"In Progress",...}'
10
+ echo '{"mapping":"auto"}' | set_status_mapping.py
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ sys.path.insert(0, str(Path(__file__).parent))
19
+ from flydocs_api import get_client, output_json, fail
20
+
21
+
22
+ def main():
23
+ parser = argparse.ArgumentParser(description="Set status mapping on relay")
24
+ parser.add_argument(
25
+ "--auto",
26
+ action="store_true",
27
+ help="Auto-map provider states to FlyDocs statuses by name",
28
+ )
29
+ parser.add_argument(
30
+ "--mapping",
31
+ default=None,
32
+ help="JSON object mapping FlyDocs statuses to provider state names",
33
+ )
34
+ args = parser.parse_args()
35
+
36
+ if args.auto:
37
+ body: dict = {"mapping": "auto"}
38
+ elif args.mapping is not None:
39
+ try:
40
+ body = {"mapping": json.loads(args.mapping)}
41
+ except json.JSONDecodeError:
42
+ fail("Invalid JSON for --mapping")
43
+ elif not sys.stdin.isatty():
44
+ try:
45
+ body = json.loads(sys.stdin.read().strip())
46
+ except json.JSONDecodeError:
47
+ fail("Invalid JSON on stdin")
48
+ else:
49
+ fail("Provide --auto, --mapping '{...}', or pipe JSON via stdin")
50
+
51
+ client = get_client()
52
+ result = client.post("/auth/statuses", body)
53
+
54
+ # Store status mapping in local config as reference
55
+ config_path = client.config_path
56
+ if config_path.exists():
57
+ with open(config_path, "r") as f:
58
+ config = json.load(f)
59
+ if isinstance(result.get("mapping"), dict):
60
+ config["statusMapping"] = result["mapping"]
61
+ with open(config_path, "w") as f:
62
+ json.dump(config, f, indent=2)
63
+ f.write("\n")
64
+
65
+ output_json(result)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env python3
2
- """Set team preference via the FlyDocs Relay API.
2
+ """Set team/project preference via the FlyDocs Relay API.
3
3
 
4
4
  Stores the team preference on the relay (for server-side scoping)
5
5
  and updates the local config (for display/reference).
6
+ For Jira, this sets the active Jira project.
6
7
  """
7
8
 
8
9
  import argparse
@@ -15,8 +16,8 @@ from flydocs_api import get_client, output_json, fail
15
16
 
16
17
 
17
18
  def main():
18
- parser = argparse.ArgumentParser(description="Set team preference")
19
- parser.add_argument("team_id", help="Linear team UUID")
19
+ parser = argparse.ArgumentParser(description="Set team or project preference")
20
+ parser.add_argument("team_id", help="Provider team/workspace UUID")
20
21
  args = parser.parse_args()
21
22
 
22
23
  client = get_client()
@@ -28,7 +29,7 @@ def main():
28
29
  with open(config_path, "r") as f:
29
30
  config = json.load(f)
30
31
  if "provider" not in config:
31
- config["provider"] = {"type": "linear", "teamId": None}
32
+ config["provider"] = {"type": None, "teamId": None}
32
33
  config["provider"]["teamId"] = args.team_id
33
34
  with open(config_path, "w") as f:
34
35
  json.dump(config, f, indent=2)
@@ -13,13 +13,14 @@ def main():
13
13
  parser = argparse.ArgumentParser(description="Update issue fields")
14
14
  parser.add_argument("ref", help="Issue reference (e.g., ENG-123)")
15
15
  parser.add_argument("--title", default=None)
16
- parser.add_argument("--priority", type=int, choices=range(5))
17
- parser.add_argument("--estimate", type=int, choices=range(1, 6))
16
+ parser.add_argument("--priority", type=int, help="Priority (0-4, relay translates per provider)")
17
+ parser.add_argument("--estimate", type=int, help="Estimate points (relay translates per provider)")
18
18
  parser.add_argument("--assignee", default=None)
19
19
  parser.add_argument("--state", default=None)
20
20
  parser.add_argument("--description", default=None)
21
21
  parser.add_argument("--description-file", default=None)
22
22
  parser.add_argument("--labels", default=None, help="Comma-separated label names")
23
+ parser.add_argument("--milestone", default=None, help="Milestone ID or name (resolved by name lookup)")
23
24
  parser.add_argument("--comment", default=None)
24
25
  args = parser.parse_args()
25
26
 
@@ -65,10 +66,27 @@ def main():
65
66
  body["comment"] = args.comment
66
67
  updated_fields.append("comment")
67
68
 
68
- if not body:
69
- fail("No fields to update. Use --title, --priority, --estimate, --assignee, --state, --description, --labels, or --comment")
69
+ if not body and args.milestone is None:
70
+ fail("No fields to update. Use --title, --priority, --estimate, --assignee, --state, --description, --labels, --milestone, or --comment")
70
71
 
71
72
  client = get_client()
73
+
74
+ if args.milestone is not None:
75
+ milestone_id = args.milestone
76
+ # If it doesn't look like a UUID, resolve by name
77
+ if len(milestone_id) != 36 or "-" not in milestone_id:
78
+ milestones = client.get("/milestones")
79
+ match = None
80
+ for m in milestones:
81
+ if m["name"].lower() == milestone_id.lower():
82
+ match = m
83
+ break
84
+ if not match:
85
+ fail(f"Milestone not found: {milestone_id}")
86
+ milestone_id = match["id"]
87
+ body["milestoneId"] = milestone_id
88
+ updated_fields.append("milestone")
89
+
72
90
  result = client.patch(f"/issues/{args.ref}", body)
73
91
 
74
92
  output_json({
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+ """Update a milestone via the FlyDocs Relay API."""
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from flydocs_api import get_client, output_json, fail
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="Update milestone")
14
+ parser.add_argument("milestone_id", help="Milestone UUID")
15
+ parser.add_argument("--name", default=None)
16
+ parser.add_argument("--target-date", default=None, dest="target_date")
17
+ parser.add_argument("--description", default=None)
18
+ args = parser.parse_args()
19
+
20
+ body: dict = {}
21
+ if args.name is not None:
22
+ body["name"] = args.name
23
+ if args.target_date is not None:
24
+ body["targetDate"] = args.target_date
25
+ if args.description is not None:
26
+ body["description"] = args.description
27
+
28
+ if not body:
29
+ fail("No fields to update. Use --name, --target-date, or --description")
30
+
31
+ client = get_client()
32
+ result = client.patch(f"/milestones/{args.milestone_id}", body)
33
+
34
+ output_json({
35
+ "success": result.get("success", True),
36
+ "id": result.get("id", args.milestone_id),
37
+ "name": result.get("name", args.name),
38
+ })
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()