@flydocs/cli 0.5.0-beta.9 → 0.6.0-alpha.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 (41) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +1259 -370
  3. package/package.json +1 -1
  4. package/template/.claude/agents/implementation-agent.md +0 -1
  5. package/template/.claude/agents/pm-agent.md +0 -1
  6. package/template/.claude/agents/research-agent.md +0 -1
  7. package/template/.claude/agents/review-agent.md +0 -1
  8. package/template/.claude/commands/flydocs-setup.md +109 -26
  9. package/template/.claude/commands/flydocs-upgrade.md +330 -0
  10. package/template/.claude/skills/flydocs-cloud/SKILL.md +53 -38
  11. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +5 -5
  12. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +8 -24
  13. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +14 -30
  14. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +10 -32
  15. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +15 -25
  16. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +21 -58
  17. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +26 -37
  18. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +24 -31
  19. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +39 -0
  20. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +10 -19
  21. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +103 -170
  22. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +6 -59
  23. package/template/.claude/skills/flydocs-cloud/scripts/link.py +16 -35
  24. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +21 -28
  25. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +16 -77
  26. package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +19 -0
  27. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +21 -33
  28. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +24 -38
  29. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +19 -0
  30. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +10 -19
  31. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +36 -50
  32. package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +68 -0
  33. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +41 -0
  34. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +11 -52
  35. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +16 -27
  36. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +23 -52
  37. package/template/.env.example +16 -7
  38. package/template/.flydocs/config.json +1 -1
  39. package/template/.flydocs/version +1 -1
  40. package/template/CHANGELOG.md +144 -0
  41. package/template/manifest.json +5 -3
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env python3
2
- """List issues with optional filters."""
2
+ """List issues with optional filters via the FlyDocs Relay API."""
3
3
 
4
4
  import argparse
5
5
  import sys
6
6
  from pathlib import Path
7
7
 
8
8
  sys.path.insert(0, str(Path(__file__).parent))
9
- from flydocs_api import get_client, output_json, fail
9
+ from flydocs_api import get_client, output_json
10
10
 
11
11
 
12
12
  def main():
13
13
  parser = argparse.ArgumentParser(description="List issues")
14
14
  parser.add_argument("--status", default="", help="Single status or comma-separated: READY,IMPLEMENTING,BLOCKED")
15
- parser.add_argument("--active", action="store_true", help="All non-terminal states (excludes Done, Archived, Canceled, Duplicate)")
15
+ parser.add_argument("--active", action="store_true", help="All non-terminal states")
16
16
  parser.add_argument("--project", default="")
17
17
  parser.add_argument("--assignee", default="")
18
18
  parser.add_argument("--milestone", default="", help="Filter by milestone ID")
@@ -20,85 +20,24 @@ def main():
20
20
  parser.add_argument("--limit", type=int, default=50)
21
21
  args = parser.parse_args()
22
22
 
23
- client = get_client()
24
-
25
- # Build filter — all conditions added to base dict, then product scope applied last
26
- filters = {"team": {"id": {"eq": client.team_id}}}
27
-
28
- if args.project:
29
- filters["project"] = {"id": {"eq": args.project}}
30
-
23
+ params: dict = {"limit": str(args.limit)}
24
+ if args.status:
25
+ params["status"] = args.status.upper()
31
26
  if args.active:
32
- # Exclude terminal states
33
- terminal = ["COMPLETE", "ARCHIVED", "CANCELED", "DUPLICATE"]
34
- terminal_ids = [client.get_state_id(s) for s in terminal]
35
- terminal_ids = [sid for sid in terminal_ids if sid]
36
- if terminal_ids:
37
- filters["state"] = {"id": {"nin": terminal_ids}}
38
- elif args.status:
39
- statuses = [s.strip().upper() for s in args.status.split(",")]
40
- if len(statuses) == 1:
41
- state_id = client.get_state_id(statuses[0])
42
- if state_id:
43
- filters["state"] = {"id": {"eq": state_id}}
44
- else:
45
- state_ids = [client.get_state_id(s) for s in statuses]
46
- state_ids = [sid for sid in state_ids if sid]
47
- if state_ids:
48
- filters["state"] = {"id": {"in": state_ids}}
49
-
27
+ params["active"] = "true"
28
+ if args.project:
29
+ params["project"] = args.project
30
+ if args.assignee:
31
+ params["assignee"] = args.assignee
50
32
  if args.milestone:
51
- filters["projectMilestone"] = {"id": {"eq": args.milestone}}
52
-
33
+ params["milestone"] = args.milestone
53
34
  if args.mine:
54
- viewer = client.query("query { viewer { id } }")
55
- user_id = viewer.get("data", {}).get("viewer", {}).get("id")
56
- if user_id:
57
- filters["assignee"] = {"id": {"eq": user_id}}
58
- elif args.assignee:
59
- user_id, _ = client.resolve_user_id(args.assignee)
60
- if user_id:
61
- filters["assignee"] = {"id": {"eq": user_id}}
35
+ params["mine"] = "true"
62
36
 
63
- # Product scope applied last — wraps complete filter with label AND conditions
64
- if not args.project:
65
- filters = client.build_product_scope(filters)
66
-
67
- result = client.query(
68
- """query($filter: IssueFilter!, $limit: Int!) {
69
- issues(filter: $filter, first: $limit, orderBy: updatedAt) {
70
- nodes {
71
- id identifier title
72
- state { name }
73
- assignee { name }
74
- priority dueDate
75
- project { id name }
76
- projectMilestone { id name sortOrder }
77
- }
78
- }
79
- }""",
80
- {"filter": filters, "limit": args.limit},
81
- )
37
+ client = get_client()
38
+ result = client.get("/issues", params=params)
82
39
 
83
- nodes = result.get("data", {}).get("issues", {}).get("nodes", [])
84
- issues = [
85
- {
86
- "id": n["id"],
87
- "identifier": n["identifier"],
88
- "title": n["title"],
89
- "status": n.get("state", {}).get("name", ""),
90
- "assignee": (n.get("assignee") or {}).get("name", ""),
91
- "priority": n.get("priority", 0),
92
- "dueDate": n.get("dueDate") or "",
93
- "milestone": (n.get("projectMilestone") or {}).get("name", ""),
94
- "milestoneId": (n.get("projectMilestone") or {}).get("id", ""),
95
- "milestoneSortOrder": (n.get("projectMilestone") or {}).get("sortOrder", 0),
96
- "project": (n.get("project") or {}).get("name", ""),
97
- "projectId": (n.get("project") or {}).get("id", ""),
98
- }
99
- for n in nodes
100
- ]
101
- output_json(issues)
40
+ output_json(result)
102
41
 
103
42
 
104
43
  if __name__ == "__main__":
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python3
2
+ """List available team labels 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("/labels")
14
+
15
+ output_json(result)
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -1,40 +1,28 @@
1
1
  #!/usr/bin/env python3
2
- """List milestones."""
2
+ """List milestones via the FlyDocs Relay API."""
3
3
 
4
4
  import argparse
5
5
  import sys
6
6
  from pathlib import Path
7
7
 
8
8
  sys.path.insert(0, str(Path(__file__).parent))
9
- from flydocs_api import get_client, output_json, fail
10
-
11
- parser = argparse.ArgumentParser(description="List milestones")
12
- parser.add_argument("--all", action="store_true", dest="show_all", help="Include completed")
13
- args = parser.parse_args()
14
-
15
- client = get_client()
16
-
17
- # Linear doesn't have team-scoped milestones — query all accessible
18
- result = client.query(
19
- """query {
20
- projectMilestones(first: 50) {
21
- nodes { id name sortOrder targetDate project { id name } }
22
- }
23
- }"""
24
- )
25
-
26
- nodes = result.get("data", {}).get("projectMilestones", {}).get("nodes", [])
27
- milestones = [
28
- {
29
- "id": n["id"],
30
- "name": n["name"],
31
- "sortOrder": n.get("sortOrder", 0),
32
- "targetDate": n.get("targetDate", ""),
33
- "project": (n.get("project") or {}).get("name", ""),
34
- "projectId": (n.get("project") or {}).get("id", ""),
35
- }
36
- for n in nodes
37
- ]
38
- # Sort by project then sortOrder for consistent display
39
- milestones.sort(key=lambda m: (m["project"], m["sortOrder"]))
40
- output_json(milestones)
9
+ from flydocs_api import get_client, output_json
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="List milestones")
14
+ parser.add_argument("--all", action="store_true", dest="show_all")
15
+ args = parser.parse_args()
16
+
17
+ params: dict = {}
18
+ if args.show_all:
19
+ params["all"] = "true"
20
+
21
+ client = get_client()
22
+ result = client.get("/milestones", params=params)
23
+
24
+ output_json(result)
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main()
@@ -1,45 +1,31 @@
1
1
  #!/usr/bin/env python3
2
- """List projects."""
2
+ """List projects via the FlyDocs Relay API."""
3
3
 
4
4
  import argparse
5
5
  import sys
6
6
  from pathlib import Path
7
7
 
8
8
  sys.path.insert(0, str(Path(__file__).parent))
9
- from flydocs_api import get_client, output_json, fail
10
-
11
- parser = argparse.ArgumentParser(description="List projects")
12
- parser.add_argument("--active", action="store_true", help="Only started projects")
13
- parser.add_argument("--all", action="store_true", dest="show_all", help="Include completed and canceled")
14
- args = parser.parse_args()
15
-
16
- client = get_client()
17
-
18
- filters = {"accessibleTeams": {"some": {"id": {"eq": client.team_id}}}}
19
-
20
- if args.active:
21
- filters["state"] = {"eq": "started"}
22
- elif not args.show_all:
23
- filters["state"] = {"nin": ["completed", "canceled"]}
24
-
25
- # Product scope — filter to activeProjects if configured (--all bypasses)
26
- if not args.show_all:
27
- active_projects = client.workspace.get("activeProjects", [])
28
- if active_projects:
29
- if len(active_projects) == 1:
30
- filters["id"] = {"eq": active_projects[0]}
31
- else:
32
- filters["id"] = {"in": active_projects}
33
-
34
- result = client.query(
35
- """query($filter: ProjectFilter!) {
36
- projects(filter: $filter, first: 50, orderBy: updatedAt) {
37
- nodes { id name state description }
38
- }
39
- }""",
40
- {"filter": filters},
41
- )
42
-
43
- nodes = result.get("data", {}).get("projects", {}).get("nodes", [])
44
- projects = [{"id": n["id"], "name": n["name"], "state": n.get("state", "")} for n in nodes]
45
- output_json(projects)
9
+ from flydocs_api import get_client, output_json
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="List projects")
14
+ parser.add_argument("--active", action="store_true")
15
+ parser.add_argument("--all", action="store_true", dest="show_all")
16
+ args = parser.parse_args()
17
+
18
+ params: dict = {}
19
+ if args.active:
20
+ params["active"] = "true"
21
+ if args.show_all:
22
+ params["all"] = "true"
23
+
24
+ client = get_client()
25
+ result = client.get("/projects", params=params)
26
+
27
+ output_json(result)
28
+
29
+
30
+ if __name__ == "__main__":
31
+ main()
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python3
2
+ """List available teams 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("/teams")
14
+
15
+ output_json(result)
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Set priority on an issue."""
2
+ """Set priority on an issue via the FlyDocs Relay API."""
3
3
 
4
4
  import sys
5
5
  from pathlib import Path
@@ -16,23 +16,14 @@ try:
16
16
  except ValueError:
17
17
  fail("Priority must be a number (0-4)")
18
18
 
19
- client = get_client()
20
- issue_uuid = client.resolve_issue_id(ref)
21
- if not issue_uuid:
22
- fail(f"Issue not found: {ref}")
23
-
24
- result = client.query(
25
- """mutation($id: String!, $priority: Int!) {
26
- issueUpdate(id: $id, input: { priority: $priority }) {
27
- success
28
- issue { identifier priority }
29
- }
30
- }""",
31
- {"id": issue_uuid, "priority": priority},
32
- )
19
+ if priority not in range(5):
20
+ fail("Priority must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low)")
33
21
 
34
- if not result.get("data", {}).get("issueUpdate", {}).get("success"):
35
- fail(f"Failed to set priority: {result}")
22
+ client = get_client()
23
+ result = client.put(f"/issues/{ref}/priority", {"priority": priority})
36
24
 
37
- issue = result["data"]["issueUpdate"]["issue"]
38
- output_json({"success": True, "issue": issue["identifier"], "priority": issue["priority"]})
25
+ output_json({
26
+ "success": result.get("success", True),
27
+ "issue": result.get("issue", ref),
28
+ "priority": result.get("priority", priority),
29
+ })
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Post a project health update."""
2
+ """Post a project update via the FlyDocs Relay API."""
3
3
 
4
4
  import argparse
5
5
  import sys
@@ -8,52 +8,38 @@ from pathlib import Path
8
8
  sys.path.insert(0, str(Path(__file__).parent))
9
9
  from flydocs_api import get_client, output_json, fail
10
10
 
11
- HEALTH_MAP = {"onTrack": "onTrack", "atRisk": "atRisk", "offTrack": "offTrack"}
12
-
13
- parser = argparse.ArgumentParser(description="Post project update")
14
- parser.add_argument("--health", required=True, choices=HEALTH_MAP.keys())
15
- parser.add_argument("--body", default="")
16
- parser.add_argument("--body-file", default="")
17
- parser.add_argument("--project", default=None)
18
- args = parser.parse_args()
19
-
20
- body = args.body
21
- if args.body_file:
22
- try:
23
- body = Path(args.body_file).read_text().strip()
24
- except FileNotFoundError:
25
- fail(f"File not found: {args.body_file}")
26
- if not body and not sys.stdin.isatty():
27
- body = sys.stdin.read().strip()
28
- if not body:
29
- fail("Provide --body, --body-file, or pipe via stdin")
30
-
31
- client = get_client()
32
-
33
- project_id = args.project
34
- if not project_id:
35
- active = client.workspace.get("activeProjects", [])
36
- if active:
37
- project_id = active[0]
38
- if not project_id:
39
- fail("No project specified and no active project configured")
40
-
41
- result = client.query(
42
- """mutation($projectId: String!, $body: String!, $health: ProjectUpdateHealthType!) {
43
- projectUpdateCreate(input: {
44
- projectId: $projectId,
45
- body: $body,
46
- health: $health
47
- }) {
48
- success
49
- projectUpdate { id }
50
- }
51
- }""",
52
- {"projectId": project_id, "body": body, "health": HEALTH_MAP[args.health]},
53
- )
54
-
55
- data = result.get("data", {}).get("projectUpdateCreate", {})
56
- if not data.get("success"):
57
- fail(f"Failed to post update: {result}")
58
-
59
- output_json({"success": True, "id": data.get("projectUpdate", {}).get("id", "")})
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="Post project update")
14
+ parser.add_argument("--health", required=True, choices=["onTrack", "atRisk", "offTrack"])
15
+ parser.add_argument("--body", default=None)
16
+ parser.add_argument("--body-file", default=None, dest="body_file")
17
+ args = parser.parse_args()
18
+
19
+ # Resolve body: --body-file > stdin > --body
20
+ body = args.body
21
+ if args.body_file:
22
+ try:
23
+ body = Path(args.body_file).read_text()
24
+ except FileNotFoundError:
25
+ fail(f"File not found: {args.body_file}")
26
+ elif body is None and not sys.stdin.isatty():
27
+ body = sys.stdin.read().strip()
28
+
29
+ if not body:
30
+ fail("Provide body via --body, --body-file, or stdin")
31
+
32
+ client = get_client()
33
+ result = client.post("/projects/update", {
34
+ "health": args.health,
35
+ "body": body,
36
+ })
37
+
38
+ output_json({
39
+ "success": result.get("success", True),
40
+ "id": result.get("id", ""),
41
+ })
42
+
43
+
44
+ if __name__ == "__main__":
45
+ main()
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ """Set label config on the relay API key.
3
+
4
+ Configures default labels and type-to-label mapping for automatic label
5
+ application during issue creation.
6
+
7
+ Usage:
8
+ set_labels.py --defaults '["app"]' --type-map '{"feature":["Feature"],...}'
9
+ echo '{"defaults":["app"],"typeMap":{...}}' | set_labels.py
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ sys.path.insert(0, str(Path(__file__).parent))
18
+ from flydocs_api import get_client, output_json, fail
19
+
20
+
21
+ def main():
22
+ parser = argparse.ArgumentParser(description="Set label config on relay")
23
+ parser.add_argument("--defaults", default=None, help="JSON array of default label names")
24
+ parser.add_argument("--type-map", default=None, dest="type_map", help="JSON object mapping issue types to label arrays")
25
+ args = parser.parse_args()
26
+
27
+ # Build body from flags or stdin
28
+ if args.defaults is not None or args.type_map is not None:
29
+ body: dict = {}
30
+ if args.defaults is not None:
31
+ try:
32
+ body["defaults"] = json.loads(args.defaults)
33
+ except json.JSONDecodeError:
34
+ fail("Invalid JSON for --defaults")
35
+ if args.type_map is not None:
36
+ try:
37
+ body["typeMap"] = json.loads(args.type_map)
38
+ except json.JSONDecodeError:
39
+ fail("Invalid JSON for --type-map")
40
+ elif not sys.stdin.isatty():
41
+ try:
42
+ body = json.loads(sys.stdin.read().strip())
43
+ except json.JSONDecodeError:
44
+ fail("Invalid JSON on stdin")
45
+ else:
46
+ fail("Provide --defaults/--type-map flags or pipe JSON via stdin")
47
+
48
+ client = get_client()
49
+ result = client.post("/auth/labels", body)
50
+
51
+ # Store label config in local config as reference
52
+ config_path = client.config_path
53
+ if config_path.exists():
54
+ with open(config_path, "r") as f:
55
+ config = json.load(f)
56
+ config["labels"] = {
57
+ "defaults": body.get("defaults", []),
58
+ "typeMap": body.get("typeMap", {}),
59
+ }
60
+ with open(config_path, "w") as f:
61
+ json.dump(config, f, indent=2)
62
+ f.write("\n")
63
+
64
+ output_json(result)
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python3
2
+ """Set team preference via the FlyDocs Relay API.
3
+
4
+ Stores the team preference on the relay (for server-side scoping)
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 team preference")
19
+ parser.add_argument("team_id", help="Linear team UUID")
20
+ args = parser.parse_args()
21
+
22
+ client = get_client()
23
+ result = client.post("/auth/team", {"teamId": args.team_id})
24
+
25
+ # Update local config with team ID
26
+ config_path = client.config_path
27
+ if config_path.exists():
28
+ with open(config_path, "r") as f:
29
+ config = json.load(f)
30
+ if "provider" not in config:
31
+ config["provider"] = {"type": "linear", "teamId": None}
32
+ config["provider"]["teamId"] = args.team_id
33
+ with open(config_path, "w") as f:
34
+ json.dump(config, f, indent=2)
35
+ f.write("\n")
36
+
37
+ output_json(result)
38
+
39
+
40
+ if __name__ == "__main__":
41
+ main()
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """Transition an issue to a new status with a required comment."""
3
3
 
4
- import json
5
4
  import sys
6
5
  from pathlib import Path
7
6
 
@@ -14,54 +13,14 @@ if len(sys.argv) < 4:
14
13
  ref, status, comment = sys.argv[1], sys.argv[2].upper(), sys.argv[3]
15
14
  client = get_client()
16
15
 
17
- # Resolve issue
18
- issue_uuid = client.resolve_issue_id(ref)
19
- if not issue_uuid:
20
- fail(f"Issue not found: {ref}")
21
-
22
- # Get current state for previousStatus
23
- current_result = client.query(
24
- """query($id: String!) {
25
- issue(id: $id) { state { name } }
26
- }""",
27
- {"id": issue_uuid},
28
- )
29
- prev_state = (current_result.get("data") or {}).get("issue", {}).get("state", {}).get("name", "Unknown")
30
-
31
- # Resolve target state
32
- state_id = client.get_state_id(status)
33
- if not state_id:
34
- fail(f"Unknown status: {status}. Check statusMapping in .flydocs/config.json")
35
-
36
- # Build mutation input — combine state + cycle in one call
37
- mutation_input: dict = {"stateId": state_id}
38
- if status == "IMPLEMENTING":
39
- cycle = client.get_active_cycle()
40
- if cycle:
41
- mutation_input["cycleId"] = cycle["id"]
42
-
43
- # Update state (and cycle if IMPLEMENTING) in a single mutation
44
- result = client.query(
45
- """mutation($id: String!, $input: IssueUpdateInput!) {
46
- issueUpdate(id: $id, input: $input) {
47
- success
48
- issue { identifier state { name } }
49
- }
50
- }""",
51
- {"id": issue_uuid, "input": mutation_input},
52
- )
53
- update_data = result.get("data", {}).get("issueUpdate", {})
54
- if not update_data.get("success"):
55
- fail(f"Failed to transition: {result}")
56
-
57
- # Add comment
58
- formatted = f"**{status}** — {comment}"
59
- client.query(
60
- """mutation($id: String!, $body: String!) {
61
- commentCreate(input: { issueId: $id, body: $body }) { success }
62
- }""",
63
- {"id": issue_uuid, "body": formatted},
64
- )
65
-
66
- issue = update_data["issue"]
67
- output_json({"success": True, "issue": issue["identifier"], "previousStatus": prev_state, "newStatus": status})
16
+ result = client.post(f"/issues/{ref}/transition", {
17
+ "status": status,
18
+ "comment": comment,
19
+ })
20
+
21
+ output_json({
22
+ "success": result.get("success", True),
23
+ "issue": result.get("issue", ref),
24
+ "previousStatus": result.get("previousStatus", ""),
25
+ "newStatus": result.get("newStatus", status),
26
+ })