@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
|
@@ -1,53 +1,49 @@
|
|
|
1
|
-
"""FlyDocs Cloud API —
|
|
1
|
+
"""FlyDocs Cloud API — Relay REST client.
|
|
2
2
|
|
|
3
|
-
Central module for all
|
|
3
|
+
Central module for all relay API operations.
|
|
4
4
|
Loads config from .flydocs/config.json and API key from .env files.
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
7
|
from flydocs_api import get_client
|
|
8
8
|
client = get_client()
|
|
9
|
-
result = client.
|
|
9
|
+
result = client.post("/issues", {"title": "New issue", "type": "feature"})
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
15
|
import time
|
|
16
|
-
import urllib.request
|
|
17
|
-
import urllib.error
|
|
18
|
-
from datetime import datetime, timezone
|
|
19
16
|
from pathlib import Path
|
|
20
17
|
from typing import Optional
|
|
21
18
|
|
|
22
19
|
|
|
23
20
|
class FlyDocsClient:
|
|
24
|
-
"""Client for
|
|
21
|
+
"""Client for FlyDocs Relay REST API with retry logic and config awareness."""
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
DEFAULT_BASE_URL = "https://app.flydocs.ai/api/relay"
|
|
24
|
+
LOCAL_BASE_URL = "http://localhost:3000/api/relay"
|
|
25
|
+
MAX_RETRIES = 3
|
|
28
26
|
RETRY_DELAY = 2
|
|
29
27
|
|
|
30
28
|
def __init__(self):
|
|
31
29
|
self.project_root = find_project_root()
|
|
32
30
|
self.config_path = self.project_root / ".flydocs" / "config.json"
|
|
33
|
-
self.log_path = self.project_root / ".flydocs" / "logs" / "
|
|
31
|
+
self.log_path = self.project_root / ".flydocs" / "logs" / "relay-ops.jsonl"
|
|
34
32
|
|
|
35
33
|
self.config = self._load_config()
|
|
36
34
|
self.api_key = self._load_api_key()
|
|
37
35
|
if not self.api_key:
|
|
38
|
-
print("ERROR:
|
|
36
|
+
print("ERROR: FLYDOCS_API_KEY not found", file=sys.stderr)
|
|
39
37
|
print("Set in environment or .env/.env.local file", file=sys.stderr)
|
|
40
38
|
sys.exit(1)
|
|
41
39
|
|
|
42
|
-
self.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
self.workspace_id = self.config.get("workspaceId")
|
|
41
|
+
if not self.workspace_id:
|
|
42
|
+
print("ERROR: workspaceId not found in .flydocs/config.json", file=sys.stderr)
|
|
43
|
+
print("Run 'flydocs setup' to configure your workspace", file=sys.stderr)
|
|
44
|
+
sys.exit(1)
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
self._id_cache: dict[str, str] = {}
|
|
49
|
-
self._team_members: Optional[list] = None
|
|
50
|
-
self._active_cycle: Optional[dict] = None
|
|
46
|
+
self.base_url = self._resolve_base_url()
|
|
51
47
|
|
|
52
48
|
def _load_config(self) -> dict:
|
|
53
49
|
if self.config_path.exists():
|
|
@@ -56,12 +52,12 @@ class FlyDocsClient:
|
|
|
56
52
|
return {}
|
|
57
53
|
|
|
58
54
|
def _load_api_key(self) -> Optional[str]:
|
|
59
|
-
if os.environ.get("
|
|
60
|
-
return os.environ["
|
|
55
|
+
if os.environ.get("FLYDOCS_API_KEY"):
|
|
56
|
+
return os.environ["FLYDOCS_API_KEY"]
|
|
61
57
|
for name in [".env.local", ".env"]:
|
|
62
58
|
env_file = self.project_root / name
|
|
63
59
|
if env_file.exists():
|
|
64
|
-
key = self._parse_env_file(env_file, "
|
|
60
|
+
key = self._parse_env_file(env_file, "FLYDOCS_API_KEY")
|
|
65
61
|
if key:
|
|
66
62
|
return key
|
|
67
63
|
return None
|
|
@@ -78,176 +74,123 @@ class FlyDocsClient:
|
|
|
78
74
|
return v if v else None
|
|
79
75
|
return None
|
|
80
76
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
def _resolve_base_url(self) -> str:
|
|
78
|
+
"""Resolve base URL: env var > config > default."""
|
|
79
|
+
env_url = os.environ.get("FLYDOCS_RELAY_URL")
|
|
80
|
+
if env_url:
|
|
81
|
+
return env_url.rstrip("/")
|
|
82
|
+
config_url = self.config.get("relay", {}).get("url")
|
|
83
|
+
if config_url:
|
|
84
|
+
return config_url.rstrip("/")
|
|
85
|
+
return self.DEFAULT_BASE_URL
|
|
86
|
+
|
|
87
|
+
def _request(self, method: str, path: str, body: Optional[dict] = None,
|
|
88
|
+
params: Optional[dict] = None) -> dict:
|
|
89
|
+
"""Make an HTTP request to the relay API."""
|
|
90
|
+
import urllib.request
|
|
91
|
+
import urllib.error
|
|
92
|
+
import urllib.parse
|
|
93
|
+
|
|
94
|
+
url = f"{self.base_url}{path}"
|
|
95
|
+
if params:
|
|
96
|
+
filtered = {k: v for k, v in params.items() if v is not None and v != ""}
|
|
97
|
+
if filtered:
|
|
98
|
+
url += "?" + urllib.parse.urlencode(filtered, doseq=True)
|
|
85
99
|
|
|
86
|
-
data = json.dumps(payload).encode("utf-8")
|
|
87
100
|
headers = {
|
|
101
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
102
|
+
"X-Workspace": self.workspace_id,
|
|
88
103
|
"Content-Type": "application/json",
|
|
89
|
-
"
|
|
104
|
+
"Accept": "application/json",
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
data = json.dumps(body).encode("utf-8") if body else None
|
|
108
|
+
|
|
92
109
|
for attempt in range(self.MAX_RETRIES):
|
|
93
110
|
try:
|
|
94
|
-
req = urllib.request.Request(
|
|
95
|
-
with urllib.request.urlopen(req, timeout=
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# and .get("data", {}) returns None (key exists, value is null).
|
|
100
|
-
if result.get("data") is None:
|
|
101
|
-
result["data"] = {}
|
|
102
|
-
self._log_operation(query, variables, result)
|
|
111
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
112
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
113
|
+
response_body = resp.read().decode("utf-8")
|
|
114
|
+
result = json.loads(response_body) if response_body else {}
|
|
115
|
+
self._log_operation(method, path, resp.status, result)
|
|
103
116
|
return result
|
|
104
117
|
except urllib.error.HTTPError as e:
|
|
118
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
119
|
+
try:
|
|
120
|
+
error_data = json.loads(error_body) if error_body else {}
|
|
121
|
+
except json.JSONDecodeError:
|
|
122
|
+
error_data = {"error": error_body}
|
|
123
|
+
self._log_operation(method, path, e.code, error_data)
|
|
124
|
+
|
|
105
125
|
if e.code == 429 and attempt < self.MAX_RETRIES - 1:
|
|
106
126
|
delay = self.RETRY_DELAY * (2 ** attempt)
|
|
107
127
|
print(f"Rate limited, retrying in {delay}s...", file=sys.stderr)
|
|
108
128
|
time.sleep(delay)
|
|
109
129
|
continue
|
|
110
|
-
|
|
130
|
+
if e.code >= 500 and attempt < self.MAX_RETRIES - 1:
|
|
131
|
+
delay = self.RETRY_DELAY * (2 ** attempt)
|
|
132
|
+
print(f"Server error ({e.code}), retrying in {delay}s...", file=sys.stderr)
|
|
133
|
+
time.sleep(delay)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
error_msg = error_data.get("error", f"HTTP {e.code}")
|
|
137
|
+
error_code = error_data.get("code", "UNKNOWN")
|
|
138
|
+
provider = error_data.get("provider_error", "")
|
|
139
|
+
msg = f"Relay API error ({error_code}): {error_msg}"
|
|
140
|
+
if provider:
|
|
141
|
+
msg += f" — provider: {provider}"
|
|
142
|
+
fail(msg)
|
|
111
143
|
except (urllib.error.URLError, TimeoutError):
|
|
112
144
|
if attempt < self.MAX_RETRIES - 1:
|
|
113
145
|
delay = self.RETRY_DELAY * (2 ** attempt)
|
|
114
146
|
print(f"Network error, retrying in {delay}s...", file=sys.stderr)
|
|
115
147
|
time.sleep(delay)
|
|
116
148
|
continue
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
{"teamId": self.team_id},
|
|
159
|
-
)
|
|
160
|
-
self._team_members = result.get("data", {}).get("team", {}).get("members", {}).get("nodes", [])
|
|
161
|
-
|
|
162
|
-
for member in self._team_members:
|
|
163
|
-
if user_query.lower() in member.get("email", "").lower() or \
|
|
164
|
-
user_query.lower() in member.get("name", "").lower():
|
|
165
|
-
return member["id"], member["name"]
|
|
166
|
-
return None, None
|
|
167
|
-
|
|
168
|
-
def get_state_id(self, flydocs_status: str) -> Optional[str]:
|
|
169
|
-
linear_state_name = self.status_mapping.get(flydocs_status)
|
|
170
|
-
if not linear_state_name:
|
|
171
|
-
return None
|
|
172
|
-
result = self.query(
|
|
173
|
-
"""query($teamId: ID!) {
|
|
174
|
-
workflowStates(filter: { team: { id: { eq: $teamId } } }) {
|
|
175
|
-
nodes { id name }
|
|
176
|
-
}
|
|
177
|
-
}""",
|
|
178
|
-
{"teamId": self.team_id},
|
|
179
|
-
)
|
|
180
|
-
states = result.get("data", {}).get("workflowStates", {}).get("nodes", [])
|
|
181
|
-
for state in states:
|
|
182
|
-
if state["name"].lower() == linear_state_name.lower():
|
|
183
|
-
return state["id"]
|
|
184
|
-
return None
|
|
185
|
-
|
|
186
|
-
def build_product_scope(self, filters: dict) -> dict:
|
|
187
|
-
"""Apply product scope to an existing filter dict.
|
|
188
|
-
|
|
189
|
-
Priority cascade:
|
|
190
|
-
1. activeProjects set → filter to those projects
|
|
191
|
-
2. product.labelIds set → filter to issues with ALL those labels
|
|
192
|
-
3. Neither set → no additional filter (team-wide)
|
|
193
|
-
"""
|
|
194
|
-
active = self.workspace.get("activeProjects", [])
|
|
195
|
-
if active:
|
|
196
|
-
if len(active) == 1:
|
|
197
|
-
filters["project"] = {"id": {"eq": active[0]}}
|
|
198
|
-
else:
|
|
199
|
-
filters["project"] = {"id": {"in": active}}
|
|
200
|
-
return filters
|
|
201
|
-
|
|
202
|
-
label_ids = self.workspace.get("product", {}).get("labelIds", [])
|
|
203
|
-
if label_ids:
|
|
204
|
-
if len(label_ids) == 1:
|
|
205
|
-
filters["labels"] = {"id": {"eq": label_ids[0]}}
|
|
206
|
-
else:
|
|
207
|
-
# Multiple labels: issue must have ALL — use AND conditions
|
|
208
|
-
label_conditions = [{"labels": {"id": {"eq": lid}}} for lid in label_ids]
|
|
209
|
-
return {"and": [filters, *label_conditions]}
|
|
210
|
-
return filters
|
|
211
|
-
|
|
212
|
-
return filters
|
|
213
|
-
|
|
214
|
-
def get_category_label_id(self, category: str) -> Optional[str]:
|
|
215
|
-
return self.issue_labels.get("category", {}).get(category)
|
|
216
|
-
|
|
217
|
-
def get_other_label_id(self, label: str) -> Optional[str]:
|
|
218
|
-
return self.issue_labels.get("other", {}).get(label)
|
|
219
|
-
|
|
220
|
-
def get_active_cycle(self) -> Optional[dict]:
|
|
221
|
-
if self._active_cycle is not None:
|
|
222
|
-
return self._active_cycle
|
|
223
|
-
result = self.query(
|
|
224
|
-
"""query($teamId: ID!) {
|
|
225
|
-
cycles(filter: {
|
|
226
|
-
team: { id: { eq: $teamId } },
|
|
227
|
-
isActive: { eq: true }
|
|
228
|
-
}, first: 1) {
|
|
229
|
-
nodes { id name number startsAt endsAt }
|
|
230
|
-
}
|
|
231
|
-
}""",
|
|
232
|
-
{"teamId": self.team_id},
|
|
233
|
-
)
|
|
234
|
-
cycles = result.get("data", {}).get("cycles", {}).get("nodes", [])
|
|
235
|
-
self._active_cycle = cycles[0] if cycles else None
|
|
236
|
-
return self._active_cycle
|
|
237
|
-
|
|
238
|
-
def _log_operation(self, query: str, variables: Optional[dict], result: dict):
|
|
239
|
-
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
240
|
-
entry = {
|
|
241
|
-
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
242
|
-
"query_hash": hash(query) % 10000,
|
|
243
|
-
"variables": variables,
|
|
244
|
-
"success": "errors" not in result,
|
|
245
|
-
}
|
|
246
|
-
with open(self.log_path, "a") as f:
|
|
247
|
-
f.write(json.dumps(entry) + "\n")
|
|
149
|
+
fail("Network error: unable to reach relay API")
|
|
150
|
+
|
|
151
|
+
fail("Max retries exceeded")
|
|
152
|
+
return {} # unreachable but satisfies type checker
|
|
153
|
+
|
|
154
|
+
def get(self, path: str, params: Optional[dict] = None) -> dict | list:
|
|
155
|
+
"""GET request to relay API."""
|
|
156
|
+
return self._request("GET", path, params=params)
|
|
157
|
+
|
|
158
|
+
def post(self, path: str, body: Optional[dict] = None) -> dict:
|
|
159
|
+
"""POST request to relay API."""
|
|
160
|
+
return self._request("POST", path, body=body)
|
|
161
|
+
|
|
162
|
+
def put(self, path: str, body: Optional[dict] = None) -> dict:
|
|
163
|
+
"""PUT request to relay API."""
|
|
164
|
+
return self._request("PUT", path, body=body)
|
|
165
|
+
|
|
166
|
+
def patch(self, path: str, body: Optional[dict] = None) -> dict:
|
|
167
|
+
"""PATCH request to relay API."""
|
|
168
|
+
return self._request("PATCH", path, body=body)
|
|
169
|
+
|
|
170
|
+
def delete(self, path: str) -> dict:
|
|
171
|
+
"""DELETE request to relay API."""
|
|
172
|
+
return self._request("DELETE", path)
|
|
173
|
+
|
|
174
|
+
def _log_operation(self, method: str, path: str, status: int, result: dict | list):
|
|
175
|
+
"""Log operation metadata to local log file."""
|
|
176
|
+
try:
|
|
177
|
+
from datetime import datetime, timezone
|
|
178
|
+
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
entry = {
|
|
180
|
+
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
181
|
+
"method": method,
|
|
182
|
+
"path": path,
|
|
183
|
+
"status": status,
|
|
184
|
+
"success": 200 <= status < 300,
|
|
185
|
+
}
|
|
186
|
+
with open(self.log_path, "a") as f:
|
|
187
|
+
f.write(json.dumps(entry) + "\n")
|
|
188
|
+
except Exception:
|
|
189
|
+
pass # logging should never break operations
|
|
248
190
|
|
|
249
191
|
|
|
250
192
|
def find_project_root() -> Path:
|
|
193
|
+
"""Walk up from cwd to find .flydocs/ directory."""
|
|
251
194
|
current = Path.cwd()
|
|
252
195
|
while current != current.parent:
|
|
253
196
|
if (current / ".flydocs").is_dir():
|
|
@@ -260,6 +203,7 @@ _client: Optional[FlyDocsClient] = None
|
|
|
260
203
|
|
|
261
204
|
|
|
262
205
|
def get_client() -> FlyDocsClient:
|
|
206
|
+
"""Get or create singleton client."""
|
|
263
207
|
global _client
|
|
264
208
|
if _client is None:
|
|
265
209
|
_client = FlyDocsClient()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Get the provider's estimate scale via the FlyDocs Relay API.
|
|
3
|
+
|
|
4
|
+
Returns the valid estimate values for the connected provider.
|
|
5
|
+
Linear: fixed scale [0, 1, 2, 3, 5, 8, 13, 21]
|
|
6
|
+
Jira: freeform (any positive number)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
13
|
+
from flydocs_api import get_client, output_json
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
client = get_client()
|
|
18
|
+
result = client.get("/auth/estimates")
|
|
19
|
+
output_json(result)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Get full details for an issue."""
|
|
2
|
+
"""Get full details for an issue via the FlyDocs Relay API."""
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
5
|
import sys
|
|
@@ -14,64 +14,11 @@ parser.add_argument("--fields", choices=["basic", "full"], default="full",
|
|
|
14
14
|
help="basic = no comments, full = with comments (default)")
|
|
15
15
|
args = parser.parse_args()
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
issue_uuid = client.resolve_issue_id(args.ref)
|
|
20
|
-
if not issue_uuid:
|
|
21
|
-
fail(f"Issue not found: {args.ref}")
|
|
22
|
-
|
|
17
|
+
params: dict = {}
|
|
23
18
|
if args.fields == "basic":
|
|
24
|
-
|
|
25
|
-
issue(id: $id) {
|
|
26
|
-
id identifier title description
|
|
27
|
-
state { name }
|
|
28
|
-
assignee { name }
|
|
29
|
-
priority estimate dueDate
|
|
30
|
-
project { id name }
|
|
31
|
-
projectMilestone { id name }
|
|
32
|
-
}
|
|
33
|
-
}"""
|
|
34
|
-
else:
|
|
35
|
-
query = """query($id: String!) {
|
|
36
|
-
issue(id: $id) {
|
|
37
|
-
id identifier title description
|
|
38
|
-
state { name }
|
|
39
|
-
assignee { name }
|
|
40
|
-
priority estimate dueDate
|
|
41
|
-
project { id name }
|
|
42
|
-
projectMilestone { id name }
|
|
43
|
-
comments(first: 50) {
|
|
44
|
-
nodes { id body createdAt user { name } }
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}"""
|
|
48
|
-
|
|
49
|
-
result = client.query(query, {"id": issue_uuid})
|
|
19
|
+
params["fields"] = "basic"
|
|
50
20
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
fail(f"Issue not found: {args.ref}")
|
|
54
|
-
|
|
55
|
-
output = {
|
|
56
|
-
"id": issue["id"],
|
|
57
|
-
"identifier": issue["identifier"],
|
|
58
|
-
"title": issue["title"],
|
|
59
|
-
"description": issue.get("description", ""),
|
|
60
|
-
"status": issue.get("state", {}).get("name", ""),
|
|
61
|
-
"assignee": (issue.get("assignee") or {}).get("name", ""),
|
|
62
|
-
"priority": issue.get("priority", 0),
|
|
63
|
-
"estimate": issue.get("estimate", 0),
|
|
64
|
-
"dueDate": issue.get("dueDate") or "",
|
|
65
|
-
"milestone": (issue.get("projectMilestone") or {}).get("name", ""),
|
|
66
|
-
"milestoneId": (issue.get("projectMilestone") or {}).get("id", ""),
|
|
67
|
-
"project": (issue.get("project") or {}).get("name", ""),
|
|
68
|
-
"projectId": (issue.get("project") or {}).get("id", ""),
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if args.fields == "full":
|
|
72
|
-
output["comments"] = [
|
|
73
|
-
{"id": c["id"], "body": c["body"], "createdAt": c["createdAt"], "user": c.get("user", {}).get("name", "")}
|
|
74
|
-
for c in issue.get("comments", {}).get("nodes", [])
|
|
75
|
-
]
|
|
21
|
+
client = get_client()
|
|
22
|
+
result = client.get(f"/issues/{args.ref}", params=params)
|
|
76
23
|
|
|
77
|
-
output_json(
|
|
24
|
+
output_json(result)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Create a
|
|
2
|
+
"""Create a link between two issues via the FlyDocs Relay API."""
|
|
3
3
|
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
@@ -7,41 +7,22 @@ from pathlib import Path
|
|
|
7
7
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
8
8
|
from flydocs_api import get_client, output_json, fail
|
|
9
9
|
|
|
10
|
-
LINK_TYPES = {"blocks": "blocks", "related": "relatedTo", "duplicate": "duplicate"}
|
|
11
|
-
|
|
12
10
|
if len(sys.argv) < 4:
|
|
13
|
-
fail(
|
|
14
|
-
|
|
15
|
-
ref, related_ref, link_type = sys.argv[1], sys.argv[2], sys.argv[3].lower()
|
|
16
|
-
if link_type not in LINK_TYPES:
|
|
17
|
-
fail(f"Invalid type: {link_type}. Valid: {', '.join(LINK_TYPES.keys())}")
|
|
18
|
-
|
|
19
|
-
client = get_client()
|
|
11
|
+
fail("Usage: link.py <ref> <related_ref> <type>")
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
if not issue_uuid:
|
|
23
|
-
fail(f"Issue not found: {ref}")
|
|
13
|
+
ref, related_ref, link_type = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
24
14
|
|
|
25
|
-
|
|
26
|
-
if not
|
|
27
|
-
fail(f"
|
|
15
|
+
valid_types = ("blocks", "related", "duplicate")
|
|
16
|
+
if link_type not in valid_types:
|
|
17
|
+
fail(f"Link type must be one of: {', '.join(valid_types)}")
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}}
|
|
40
|
-
}}""",
|
|
41
|
-
{"issueId": issue_uuid, "relatedId": related_uuid},
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
if not result.get("data", {}).get("issueRelationCreate", {}).get("success"):
|
|
45
|
-
fail(f"Failed to link: {result}")
|
|
46
|
-
|
|
47
|
-
output_json({"success": True, "type": link_type})
|
|
19
|
+
client = get_client()
|
|
20
|
+
result = client.post(f"/issues/{ref}/link", {
|
|
21
|
+
"relatedRef": related_ref,
|
|
22
|
+
"type": link_type,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
output_json({
|
|
26
|
+
"success": result.get("success", True),
|
|
27
|
+
"type": result.get("type", link_type),
|
|
28
|
+
})
|
|
@@ -1,35 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""List cycles
|
|
2
|
+
"""List cycles 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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if args.active:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
nodes = result.get("data", {}).get("cycles", {}).get("nodes", [])
|
|
31
|
-
cycles = [
|
|
32
|
-
{"id": n["id"], "name": n.get("name", ""), "number": n["number"], "startsAt": n["startsAt"], "endsAt": n["endsAt"]}
|
|
33
|
-
for n in nodes
|
|
34
|
-
]
|
|
35
|
-
output_json(cycles)
|
|
9
|
+
from flydocs_api import get_client, output_json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
parser = argparse.ArgumentParser(description="List cycles")
|
|
14
|
+
parser.add_argument("--active", action="store_true")
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
|
|
17
|
+
params: dict = {}
|
|
18
|
+
if args.active:
|
|
19
|
+
params["active"] = "true"
|
|
20
|
+
|
|
21
|
+
client = get_client()
|
|
22
|
+
result = client.get("/cycles", params=params)
|
|
23
|
+
|
|
24
|
+
output_json(result)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|