@flydocs/cli 0.5.0-beta.9 → 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 (74) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +1553 -414
  3. package/package.json +1 -1
  4. package/template/.claude/CLAUDE.md +11 -9
  5. package/template/.claude/agents/implementation-agent.md +0 -1
  6. package/template/.claude/agents/pm-agent.md +0 -1
  7. package/template/.claude/agents/research-agent.md +0 -1
  8. package/template/.claude/agents/review-agent.md +0 -1
  9. package/template/.claude/commands/flydocs-setup.md +202 -35
  10. package/template/.claude/commands/flydocs-upgrade.md +342 -0
  11. package/template/.claude/commands/knowledge.md +61 -0
  12. package/template/.claude/skills/flydocs-cloud/SKILL.md +66 -39
  13. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +5 -5
  14. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +17 -27
  15. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +14 -30
  16. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +10 -32
  17. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +15 -25
  18. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +42 -59
  19. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +26 -37
  20. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +24 -31
  21. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +39 -0
  22. package/template/.claude/skills/flydocs-cloud/scripts/delete_milestone.py +21 -0
  23. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +17 -22
  24. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +113 -169
  25. package/template/.claude/skills/flydocs-cloud/scripts/get_estimate_scale.py +23 -0
  26. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +6 -59
  27. package/template/.claude/skills/flydocs-cloud/scripts/link.py +16 -35
  28. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +21 -28
  29. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +16 -77
  30. package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +19 -0
  31. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +21 -33
  32. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +24 -38
  33. package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +19 -0
  34. package/template/.claude/skills/flydocs-cloud/scripts/list_statuses.py +19 -0
  35. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +19 -0
  36. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +10 -19
  37. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +36 -50
  38. package/template/.claude/skills/flydocs-cloud/scripts/refresh_labels.py +87 -0
  39. package/template/.claude/skills/flydocs-cloud/scripts/set_identity.py +38 -0
  40. package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +68 -0
  41. package/template/.claude/skills/flydocs-cloud/scripts/set_preferences.py +49 -0
  42. package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +46 -0
  43. package/template/.claude/skills/flydocs-cloud/scripts/set_status_mapping.py +69 -0
  44. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +42 -0
  45. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +11 -52
  46. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +16 -27
  47. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +43 -54
  48. package/template/.claude/skills/flydocs-cloud/scripts/update_milestone.py +42 -0
  49. package/template/.claude/skills/flydocs-cloud/scripts/validate_setup.py +139 -0
  50. package/template/.claude/skills/flydocs-local/SKILL.md +1 -1
  51. package/template/.claude/skills/flydocs-local/scripts/assign.py +13 -4
  52. package/template/.claude/skills/flydocs-local/scripts/flydocs_api.py +5 -2
  53. package/template/.claude/skills/flydocs-workflow/SKILL.md +23 -18
  54. package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
  55. package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +105 -0
  56. package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
  57. package/template/.claude/skills/flydocs-workflow/session.md +24 -16
  58. package/template/.claude/skills/flydocs-workflow/stages/capture.md +8 -3
  59. package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
  60. package/template/.claude/skills/flydocs-workflow/stages/implement.md +28 -4
  61. package/template/.claude/skills/flydocs-workflow/stages/refine.md +20 -4
  62. package/template/.claude/skills/flydocs-workflow/stages/review.md +14 -2
  63. package/template/.env.example +16 -7
  64. package/template/.flydocs/config.json +4 -18
  65. package/template/.flydocs/hooks/prompt-submit.py +27 -4
  66. package/template/.flydocs/version +1 -1
  67. package/template/AGENTS.md +8 -8
  68. package/template/CHANGELOG.md +183 -0
  69. package/template/flydocs/knowledge/INDEX.md +38 -53
  70. package/template/flydocs/knowledge/README.md +60 -9
  71. package/template/flydocs/knowledge/templates/decision.md +47 -0
  72. package/template/flydocs/knowledge/templates/feature.md +35 -0
  73. package/template/flydocs/knowledge/templates/note.md +25 -0
  74. package/template/manifest.json +12 -4
@@ -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 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()
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python3
2
+ """List available teams/projects 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,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,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()