@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
@@ -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()
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+ """Set team/project 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
+ For Jira, this sets the active Jira project.
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ sys.path.insert(0, str(Path(__file__).parent))
15
+ from flydocs_api import get_client, output_json, fail
16
+
17
+
18
+ def main():
19
+ parser = argparse.ArgumentParser(description="Set team or project preference")
20
+ parser.add_argument("team_id", help="Provider team/workspace UUID")
21
+ args = parser.parse_args()
22
+
23
+ client = get_client()
24
+ result = client.post("/auth/team", {"teamId": args.team_id})
25
+
26
+ # Update local config with team ID
27
+ config_path = client.config_path
28
+ if config_path.exists():
29
+ with open(config_path, "r") as f:
30
+ config = json.load(f)
31
+ if "provider" not in config:
32
+ config["provider"] = {"type": None, "teamId": None}
33
+ config["provider"]["teamId"] = args.team_id
34
+ with open(config_path, "w") as f:
35
+ json.dump(config, f, indent=2)
36
+ f.write("\n")
37
+
38
+ output_json(result)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ 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
+ })
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Update an issue's description."""
2
+ """Update an issue's description via the FlyDocs Relay API."""
3
3
 
4
4
  import argparse
5
5
  import sys
@@ -9,39 +9,28 @@ sys.path.insert(0, str(Path(__file__).parent))
9
9
  from flydocs_api import get_client, output_json, fail
10
10
 
11
11
  parser = argparse.ArgumentParser(description="Update issue description")
12
- parser.add_argument("ref")
13
- parser.add_argument("--text", default="")
14
- parser.add_argument("--file", default="", dest="filepath")
12
+ parser.add_argument("ref", help="Issue reference (e.g., ENG-123)")
13
+ parser.add_argument("--text", default=None)
14
+ parser.add_argument("--file", default=None)
15
15
  args = parser.parse_args()
16
16
 
17
+ # Resolve text: --file > stdin > --text
17
18
  text = args.text
18
- if args.filepath:
19
+ if args.file:
19
20
  try:
20
- text = Path(args.filepath).read_text()
21
+ text = Path(args.file).read_text()
21
22
  except FileNotFoundError:
22
- fail(f"File not found: {args.filepath}")
23
- if not text and not sys.stdin.isatty():
23
+ fail(f"File not found: {args.file}")
24
+ elif text is None and not sys.stdin.isatty():
24
25
  text = sys.stdin.read().strip()
26
+
25
27
  if not text:
26
- fail("Provide --text, --file, or pipe via stdin")
28
+ fail("Provide text via --text, --file, or stdin")
27
29
 
28
30
  client = get_client()
29
- issue_uuid = client.resolve_issue_id(args.ref)
30
- if not issue_uuid:
31
- fail(f"Issue not found: {args.ref}")
32
-
33
- result = client.query(
34
- """mutation($id: String!, $description: String!) {
35
- issueUpdate(id: $id, input: { description: $description }) {
36
- success
37
- issue { identifier }
38
- }
39
- }""",
40
- {"id": issue_uuid, "description": text},
41
- )
42
-
43
- if not result.get("data", {}).get("issueUpdate", {}).get("success"):
44
- fail(f"Failed to update: {result}")
31
+ result = client.put(f"/issues/{args.ref}/description", {"text": text})
45
32
 
46
- issue = result["data"]["issueUpdate"]["issue"]
47
- output_json({"success": True, "issue": issue["identifier"]})
33
+ output_json({
34
+ "success": result.get("success", True),
35
+ "issue": result.get("issue", args.ref),
36
+ })
@@ -13,96 +13,85 @@ 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
+ 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)")
22
24
  parser.add_argument("--comment", default=None)
23
25
  args = parser.parse_args()
24
26
 
25
- client = get_client()
26
-
27
- issue_uuid = client.resolve_issue_id(args.ref)
28
- if not issue_uuid:
29
- fail(f"Issue not found: {args.ref}")
30
-
31
- # Build mutation input from provided args
32
- mutation_input: dict = {}
27
+ # Build request body from provided args
28
+ body: dict = {}
33
29
  updated_fields: list[str] = []
34
30
 
35
31
  if args.title is not None:
36
- mutation_input["title"] = args.title
32
+ body["title"] = args.title
37
33
  updated_fields.append("title")
38
34
 
39
35
  if args.priority is not None:
40
- mutation_input["priority"] = args.priority
36
+ body["priority"] = args.priority
41
37
  updated_fields.append("priority")
42
38
 
43
39
  if args.estimate is not None:
44
- mutation_input["estimate"] = args.estimate
40
+ body["estimate"] = args.estimate
45
41
  updated_fields.append("estimate")
46
42
 
47
43
  if args.assignee is not None:
48
- user_id, user_name = client.resolve_user_id(args.assignee)
49
- if not user_id:
50
- fail(f"User not found: {args.assignee}")
51
- mutation_input["assigneeId"] = user_id
44
+ body["assignee"] = args.assignee
52
45
  updated_fields.append("assignee")
53
46
 
54
47
  if args.state is not None:
55
- state_id = client.get_state_id(args.state.upper())
56
- if not state_id:
57
- fail(f"Unknown status: {args.state}")
58
- mutation_input["stateId"] = state_id
48
+ body["state"] = args.state.upper()
59
49
  updated_fields.append("state")
60
50
 
61
51
  if args.description_file is not None:
62
52
  path = Path(args.description_file)
63
53
  if not path.exists():
64
54
  fail(f"File not found: {args.description_file}")
65
- mutation_input["description"] = path.read_text()
55
+ body["description"] = path.read_text()
66
56
  updated_fields.append("description")
67
57
  elif args.description is not None:
68
- mutation_input["description"] = args.description
58
+ body["description"] = args.description
69
59
  updated_fields.append("description")
70
60
 
71
- if not mutation_input and not args.comment:
72
- fail("No fields to update. Use --title, --priority, --estimate, --assignee, --state, --description, or --comment")
73
-
74
- # Single mutation for all field updates
75
- result = None
76
- if mutation_input:
77
- result = client.query(
78
- """mutation($id: String!, $input: IssueUpdateInput!) {
79
- issueUpdate(id: $id, input: $input) {
80
- success
81
- issue { identifier title state { name } assignee { name } priority estimate }
82
- }
83
- }""",
84
- {"id": issue_uuid, "input": mutation_input},
85
- )
86
- if not result.get("data", {}).get("issueUpdate", {}).get("success"):
87
- fail(f"Failed to update: {result}")
88
-
89
- # Comment is a separate mutation (Linear doesn't combine these)
90
- if args.comment:
91
- client.query(
92
- """mutation($id: String!, $body: String!) {
93
- commentCreate(input: { issueId: $id, body: $body }) { success }
94
- }""",
95
- {"id": issue_uuid, "body": args.comment},
96
- )
61
+ if args.labels is not None:
62
+ body["labels"] = [l.strip() for l in args.labels.split(",") if l.strip()]
63
+ updated_fields.append("labels")
64
+
65
+ if args.comment is not None:
66
+ body["comment"] = args.comment
97
67
  updated_fields.append("comment")
98
68
 
99
- issue = {}
100
- if result:
101
- issue = result["data"]["issueUpdate"]["issue"]
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")
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
+
90
+ result = client.patch(f"/issues/{args.ref}", body)
102
91
 
103
92
  output_json({
104
- "success": True,
105
- "issue": issue.get("identifier", args.ref),
93
+ "success": result.get("success", True),
94
+ "issue": result.get("issue", args.ref),
106
95
  "updated": updated_fields,
107
96
  })
108
97
 
@@ -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()
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """Validate workspace setup via the FlyDocs Relay API.
3
+
4
+ Read-only validation that checks provider, team, status mapping, label config,
5
+ and user identity. Writes result to .flydocs/validation-cache.json and sets
6
+ setupComplete: true in config when all required checks pass.
7
+ """
8
+
9
+ import json
10
+ import sys
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+
14
+ sys.path.insert(0, str(Path(__file__).parent))
15
+ from flydocs_api import get_client, output_json, fail
16
+
17
+
18
+ # Required checks — failure blocks setup completion
19
+ REQUIRED_CHECKS = {
20
+ "provider": {
21
+ "test": lambda cfg: cfg.get("provider", {}).get("connected") is True,
22
+ "message": "No provider connected — configure in FlyDocs dashboard",
23
+ },
24
+ "team": {
25
+ "test": lambda cfg: bool(cfg.get("team", {}).get("id")),
26
+ "message": "No team selected — configure in FlyDocs dashboard",
27
+ },
28
+ "statusMapping": {
29
+ "test": lambda cfg: cfg.get("statusMapping", {}).get("configured") is True,
30
+ "message": "Status mapping not configured — configure in FlyDocs dashboard",
31
+ },
32
+ "labelConfig": {
33
+ "test": lambda cfg: cfg.get("labelConfig", {}).get("configured") is True,
34
+ "message": "Label config not configured — configure in FlyDocs dashboard",
35
+ },
36
+ }
37
+
38
+ # Optional checks — warn but don't block
39
+ OPTIONAL_CHECKS = {
40
+ "userIdentity": {
41
+ "test": lambda cfg: cfg.get("userIdentity", {}).get("linked") is True,
42
+ "message": (
43
+ "Provider identity not linked — run: "
44
+ "python3 .claude/skills/flydocs-cloud/scripts/set_identity.py <provider> <id>"
45
+ ),
46
+ },
47
+ "repos": {
48
+ "test": lambda cfg: (cfg.get("repos", {}).get("count", 0) or 0) > 0,
49
+ "message": "No repos linked — link a repo in FlyDocs dashboard settings",
50
+ },
51
+ }
52
+
53
+
54
+ def main() -> None:
55
+ client = get_client()
56
+
57
+ # Fetch workspace config from relay
58
+ config_response = client.get("/auth/config")
59
+
60
+ # Run checks
61
+ checks: dict[str, bool] = {}
62
+ valid: list[str] = []
63
+ missing: list[dict[str, str]] = []
64
+ warnings: list[dict[str, str]] = []
65
+
66
+ for name, check in REQUIRED_CHECKS.items():
67
+ passed = check["test"](config_response)
68
+ checks[name] = passed
69
+ if passed:
70
+ valid.append(name)
71
+ else:
72
+ missing.append({"check": name, "action": check["message"]})
73
+
74
+ for name, check in OPTIONAL_CHECKS.items():
75
+ passed = check["test"](config_response)
76
+ checks[name] = passed
77
+ if passed:
78
+ valid.append(name)
79
+ else:
80
+ warnings.append({"check": name, "action": check["message"]})
81
+
82
+ all_required_pass = len(missing) == 0
83
+
84
+ # Build workspace info from response
85
+ workspace = config_response.get("workspace", {})
86
+ provider_type = config_response.get("provider", {}).get("type", "unknown")
87
+
88
+ # Write validation cache
89
+ cache = {
90
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
91
+ "valid": all_required_pass,
92
+ "workspace": {
93
+ "id": workspace.get("id", ""),
94
+ "name": workspace.get("name", ""),
95
+ },
96
+ "provider": provider_type,
97
+ "checks": checks,
98
+ "missing": [m["check"] for m in missing],
99
+ "warnings": [w["check"] for w in warnings],
100
+ }
101
+
102
+ cache_path = client.project_root / ".flydocs" / "validation-cache.json"
103
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
104
+ with open(cache_path, "w") as f:
105
+ json.dump(cache, f, indent=2)
106
+ f.write("\n")
107
+
108
+ # If all required checks pass, set setupComplete in config
109
+ if all_required_pass:
110
+ config_path = client.config_path
111
+ if config_path.exists():
112
+ with open(config_path, "r") as f:
113
+ local_config = json.load(f)
114
+ else:
115
+ local_config = {}
116
+
117
+ local_config["setupComplete"] = True
118
+ with open(config_path, "w") as f:
119
+ json.dump(local_config, f, indent=2)
120
+ f.write("\n")
121
+
122
+ # Output structured report
123
+ report: dict = {
124
+ "valid": all_required_pass,
125
+ "checks": checks,
126
+ "passed": valid,
127
+ }
128
+ if missing:
129
+ report["missing"] = missing
130
+ if warnings:
131
+ report["warnings"] = warnings
132
+ if all_required_pass:
133
+ report["setupComplete"] = True
134
+
135
+ output_json(report)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()