@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.
- package/README.md +6 -0
- package/dist/cli.js +1553 -414
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +11 -9
- package/template/.claude/agents/implementation-agent.md +0 -1
- package/template/.claude/agents/pm-agent.md +0 -1
- package/template/.claude/agents/research-agent.md +0 -1
- package/template/.claude/agents/review-agent.md +0 -1
- package/template/.claude/commands/flydocs-setup.md +202 -35
- package/template/.claude/commands/flydocs-upgrade.md +342 -0
- package/template/.claude/commands/knowledge.md +61 -0
- package/template/.claude/skills/flydocs-cloud/SKILL.md +66 -39
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +5 -5
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +17 -27
- package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +14 -30
- package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +10 -32
- package/template/.claude/skills/flydocs-cloud/scripts/comment.py +15 -25
- package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +42 -59
- package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +26 -37
- package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +24 -31
- package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +39 -0
- package/template/.claude/skills/flydocs-cloud/scripts/delete_milestone.py +21 -0
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +17 -22
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +113 -169
- package/template/.claude/skills/flydocs-cloud/scripts/get_estimate_scale.py +23 -0
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +6 -59
- package/template/.claude/skills/flydocs-cloud/scripts/link.py +16 -35
- package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +21 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +16 -77
- package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +19 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +21 -33
- package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +24 -38
- package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +19 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_statuses.py +19 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +19 -0
- package/template/.claude/skills/flydocs-cloud/scripts/priority.py +10 -19
- package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +36 -50
- package/template/.claude/skills/flydocs-cloud/scripts/refresh_labels.py +87 -0
- package/template/.claude/skills/flydocs-cloud/scripts/set_identity.py +38 -0
- package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +68 -0
- package/template/.claude/skills/flydocs-cloud/scripts/set_preferences.py +49 -0
- package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +46 -0
- package/template/.claude/skills/flydocs-cloud/scripts/set_status_mapping.py +69 -0
- package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +42 -0
- package/template/.claude/skills/flydocs-cloud/scripts/transition.py +11 -52
- package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +16 -27
- package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +43 -54
- package/template/.claude/skills/flydocs-cloud/scripts/update_milestone.py +42 -0
- package/template/.claude/skills/flydocs-cloud/scripts/validate_setup.py +139 -0
- package/template/.claude/skills/flydocs-local/SKILL.md +1 -1
- package/template/.claude/skills/flydocs-local/scripts/assign.py +13 -4
- package/template/.claude/skills/flydocs-local/scripts/flydocs_api.py +5 -2
- package/template/.claude/skills/flydocs-workflow/SKILL.md +23 -18
- package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
- package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +105 -0
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
- package/template/.claude/skills/flydocs-workflow/session.md +24 -16
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +8 -3
- package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +28 -4
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +20 -4
- package/template/.claude/skills/flydocs-workflow/stages/review.md +14 -2
- package/template/.env.example +16 -7
- package/template/.flydocs/config.json +4 -18
- package/template/.flydocs/hooks/prompt-submit.py +27 -4
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +8 -8
- package/template/CHANGELOG.md +183 -0
- package/template/flydocs/knowledge/INDEX.md +38 -53
- package/template/flydocs/knowledge/README.md +60 -9
- package/template/flydocs/knowledge/templates/decision.md +47 -0
- package/template/flydocs/knowledge/templates/feature.md +35 -0
- package/template/flydocs/knowledge/templates/note.md +25 -0
- 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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=
|
|
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.
|
|
19
|
+
if args.file:
|
|
19
20
|
try:
|
|
20
|
-
text = Path(args.
|
|
21
|
+
text = Path(args.file).read_text()
|
|
21
22
|
except FileNotFoundError:
|
|
22
|
-
fail(f"File not found: {args.
|
|
23
|
-
|
|
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
|
|
28
|
+
fail("Provide text via --text, --file, or stdin")
|
|
27
29
|
|
|
28
30
|
client = get_client()
|
|
29
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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,
|
|
17
|
-
parser.add_argument("--estimate", type=int,
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
body["title"] = args.title
|
|
37
33
|
updated_fields.append("title")
|
|
38
34
|
|
|
39
35
|
if args.priority is not None:
|
|
40
|
-
|
|
36
|
+
body["priority"] = args.priority
|
|
41
37
|
updated_fields.append("priority")
|
|
42
38
|
|
|
43
39
|
if args.estimate is not None:
|
|
44
|
-
|
|
40
|
+
body["estimate"] = args.estimate
|
|
45
41
|
updated_fields.append("estimate")
|
|
46
42
|
|
|
47
43
|
if args.assignee is not None:
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
body["description"] = path.read_text()
|
|
66
56
|
updated_fields.append("description")
|
|
67
57
|
elif args.description is not None:
|
|
68
|
-
|
|
58
|
+
body["description"] = args.description
|
|
69
59
|
updated_fields.append("description")
|
|
70
60
|
|
|
71
|
-
if
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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":
|
|
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()
|