@flydocs/cli 0.6.0-alpha.3 → 0.6.0-alpha.30
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/dist/cli.js +2054 -470
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +43 -48
- package/template/.claude/agents/implementation-agent.md +1 -1
- package/template/.claude/agents/pm-agent.md +1 -1
- package/template/.claude/commands/activate.md +1 -1
- package/template/.claude/commands/attach.md +1 -1
- package/template/.claude/commands/block.md +2 -2
- package/template/.claude/commands/capture.md +1 -1
- package/template/.claude/commands/close.md +1 -1
- package/template/.claude/commands/flydocs-setup.md +359 -72
- package/template/.claude/commands/flydocs-upgrade.md +26 -27
- package/template/.claude/commands/implement.md +1 -1
- package/template/.claude/commands/knowledge.md +61 -0
- package/template/.claude/commands/new-project.md +1 -1
- package/template/.claude/commands/onboard.md +275 -0
- package/template/.claude/commands/project-update.md +1 -1
- package/template/.claude/commands/refine.md +1 -1
- package/template/.claude/commands/review.md +1 -1
- package/template/.claude/commands/start-session.md +1 -1
- package/template/.claude/commands/status.md +1 -1
- package/template/.claude/commands/validate.md +1 -1
- package/template/.claude/commands/wrap-session.md +1 -1
- package/template/.claude/hooks/auto-approve.py +212 -0
- package/template/.claude/hooks/post-pr-check.py +108 -0
- package/template/.claude/hooks/post-transition-check.py +281 -0
- package/template/.claude/hooks/prompt-submit.py +554 -0
- package/template/.claude/hooks/session-start.py +262 -0
- package/template/.claude/hooks/stop-gate.py +162 -0
- package/template/.claude/settings.json +41 -4
- package/template/.claude/skills/README.md +23 -25
- package/template/.claude/skills/flydocs-workflow/SKILL.md +134 -42
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
- package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
- package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
- package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
- package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +120 -0
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
- package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +260 -0
- package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
- package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
- package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +137 -47
- package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +724 -0
- package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -10
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
- package/template/.claude/skills/flydocs-workflow/scripts/issues.py +738 -0
- package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
- package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
- package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
- package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
- package/template/.claude/skills/flydocs-workflow/scripts/test_enforcement.py +225 -0
- package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +902 -0
- package/template/.claude/skills/flydocs-workflow/session.md +87 -29
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +18 -7
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +10 -5
- package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +33 -9
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +22 -6
- package/template/.claude/skills/flydocs-workflow/stages/review.md +16 -4
- package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
- package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
- package/template/.cursor/agents/implementation-agent.md +1 -1
- package/template/.cursor/agents/pm-agent.md +2 -2
- package/template/.cursor/hooks.json +10 -3
- package/template/.env.example +6 -6
- package/template/.flydocs/config.json +5 -18
- package/template/.flydocs/templates/README.md +13 -14
- package/template/.flydocs/templates/bug.md +17 -153
- package/template/.flydocs/templates/chore.md +10 -98
- package/template/.flydocs/templates/feature.md +12 -158
- package/template/.flydocs/templates/idea.md +11 -111
- package/template/.flydocs/templates/quick-capture.md +4 -8
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +44 -32
- package/template/CHANGELOG.md +37 -0
- package/template/flydocs/README.md +1 -3
- package/template/flydocs/context/project.md +6 -3
- package/template/flydocs/design-system/README.md +3 -3
- 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 +24 -20
- package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -113
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -66
- package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
- package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
- package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -210
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
- package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
- package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
- package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
- package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -68
- package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -46
- package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -41
- package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
- package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
- package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -82
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -87
- package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
- package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
- package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
- package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
- package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
- package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
- package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
- package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
- package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
- package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
- package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
- package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
- package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
- package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -34
- package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
- package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
- package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
- package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
- package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
- package/template/.flydocs/hooks/auto-approve.py +0 -71
- package/template/.flydocs/hooks/prompt-submit.py +0 -277
- package/template/.flydocs/scripts/skill_manager.py +0 -541
- /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
- /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{idea.md → issues/idea.md} +0 -0
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Create a team (or sub-team) 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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def main():
|
|
13
|
-
parser = argparse.ArgumentParser(description="Create team")
|
|
14
|
-
parser.add_argument("--name", required=True)
|
|
15
|
-
parser.add_argument("--key", default=None, help="Team key (e.g., PROD). Auto-generated if omitted.")
|
|
16
|
-
parser.add_argument("--description", default=None)
|
|
17
|
-
parser.add_argument("--parent", default=None, dest="parent_id", help="Parent team ID to create a sub-team")
|
|
18
|
-
args = parser.parse_args()
|
|
19
|
-
|
|
20
|
-
body: dict = {"name": args.name}
|
|
21
|
-
if args.key:
|
|
22
|
-
body["key"] = args.key
|
|
23
|
-
if args.description:
|
|
24
|
-
body["description"] = args.description
|
|
25
|
-
if args.parent_id:
|
|
26
|
-
body["parentId"] = args.parent_id
|
|
27
|
-
|
|
28
|
-
client = get_client()
|
|
29
|
-
result = client.post("/teams", body)
|
|
30
|
-
|
|
31
|
-
output_json({
|
|
32
|
-
"id": result["id"],
|
|
33
|
-
"name": result["name"],
|
|
34
|
-
"key": result.get("key", ""),
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if __name__ == "__main__":
|
|
39
|
-
main()
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Set estimate on an issue 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, fail
|
|
9
|
-
|
|
10
|
-
if len(sys.argv) < 3:
|
|
11
|
-
fail("Usage: estimate.py <ref> <1-5>")
|
|
12
|
-
|
|
13
|
-
ref = sys.argv[1]
|
|
14
|
-
try:
|
|
15
|
-
estimate = int(sys.argv[2])
|
|
16
|
-
except ValueError:
|
|
17
|
-
fail("Estimate must be a number (1-5)")
|
|
18
|
-
|
|
19
|
-
if estimate not in (1, 2, 3, 5):
|
|
20
|
-
fail("Estimate must be 1 (XS), 2 (S), 3 (M), or 5 (L)")
|
|
21
|
-
|
|
22
|
-
client = get_client()
|
|
23
|
-
result = client.put(f"/issues/{ref}/estimate", {"estimate": estimate})
|
|
24
|
-
|
|
25
|
-
output_json({
|
|
26
|
-
"success": result.get("success", True),
|
|
27
|
-
"issue": result.get("issue", ref),
|
|
28
|
-
"estimate": result.get("estimate", estimate),
|
|
29
|
-
})
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
"""FlyDocs Cloud API — Relay REST client.
|
|
2
|
-
|
|
3
|
-
Central module for all relay API operations.
|
|
4
|
-
Loads config from .flydocs/config.json and API key from .env files.
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
from flydocs_api import get_client
|
|
8
|
-
client = get_client()
|
|
9
|
-
result = client.post("/issues", {"title": "New issue", "type": "feature"})
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import os
|
|
14
|
-
import sys
|
|
15
|
-
import time
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import Optional
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class FlyDocsClient:
|
|
21
|
-
"""Client for FlyDocs Relay REST API with retry logic and config awareness."""
|
|
22
|
-
|
|
23
|
-
DEFAULT_BASE_URL = "https://app.flydocs.ai/api/relay"
|
|
24
|
-
LOCAL_BASE_URL = "http://localhost:3000/api/relay"
|
|
25
|
-
MAX_RETRIES = 3
|
|
26
|
-
RETRY_DELAY = 2
|
|
27
|
-
|
|
28
|
-
def __init__(self):
|
|
29
|
-
self.project_root = find_project_root()
|
|
30
|
-
self.config_path = self.project_root / ".flydocs" / "config.json"
|
|
31
|
-
self.log_path = self.project_root / ".flydocs" / "logs" / "relay-ops.jsonl"
|
|
32
|
-
|
|
33
|
-
self.config = self._load_config()
|
|
34
|
-
self.api_key = self._load_api_key()
|
|
35
|
-
if not self.api_key:
|
|
36
|
-
print("ERROR: FLYDOCS_API_KEY not found", file=sys.stderr)
|
|
37
|
-
print("Set in environment or .env/.env.local file", file=sys.stderr)
|
|
38
|
-
sys.exit(1)
|
|
39
|
-
|
|
40
|
-
self.base_url = self._resolve_base_url()
|
|
41
|
-
|
|
42
|
-
def _load_config(self) -> dict:
|
|
43
|
-
if self.config_path.exists():
|
|
44
|
-
with open(self.config_path, "r") as f:
|
|
45
|
-
return json.load(f)
|
|
46
|
-
return {}
|
|
47
|
-
|
|
48
|
-
def _load_api_key(self) -> Optional[str]:
|
|
49
|
-
if os.environ.get("FLYDOCS_API_KEY"):
|
|
50
|
-
return os.environ["FLYDOCS_API_KEY"]
|
|
51
|
-
for name in [".env.local", ".env"]:
|
|
52
|
-
env_file = self.project_root / name
|
|
53
|
-
if env_file.exists():
|
|
54
|
-
key = self._parse_env_file(env_file, "FLYDOCS_API_KEY")
|
|
55
|
-
if key:
|
|
56
|
-
return key
|
|
57
|
-
return None
|
|
58
|
-
|
|
59
|
-
def _parse_env_file(self, path: Path, key: str) -> Optional[str]:
|
|
60
|
-
with open(path, "r") as f:
|
|
61
|
-
for line in f:
|
|
62
|
-
line = line.strip()
|
|
63
|
-
if line.startswith("#") or "=" not in line:
|
|
64
|
-
continue
|
|
65
|
-
k, _, v = line.partition("=")
|
|
66
|
-
if k.strip() == key:
|
|
67
|
-
v = v.strip().strip("\"'")
|
|
68
|
-
return v if v else None
|
|
69
|
-
return None
|
|
70
|
-
|
|
71
|
-
def _resolve_base_url(self) -> str:
|
|
72
|
-
"""Resolve base URL: env var > config > default."""
|
|
73
|
-
env_url = os.environ.get("FLYDOCS_RELAY_URL")
|
|
74
|
-
if env_url:
|
|
75
|
-
return env_url.rstrip("/")
|
|
76
|
-
config_url = self.config.get("relay", {}).get("url")
|
|
77
|
-
if config_url:
|
|
78
|
-
return config_url.rstrip("/")
|
|
79
|
-
return self.DEFAULT_BASE_URL
|
|
80
|
-
|
|
81
|
-
def _request(self, method: str, path: str, body: Optional[dict] = None,
|
|
82
|
-
params: Optional[dict] = None) -> dict:
|
|
83
|
-
"""Make an HTTP request to the relay API."""
|
|
84
|
-
import urllib.request
|
|
85
|
-
import urllib.error
|
|
86
|
-
import urllib.parse
|
|
87
|
-
|
|
88
|
-
url = f"{self.base_url}{path}"
|
|
89
|
-
if params:
|
|
90
|
-
filtered = {k: v for k, v in params.items() if v is not None and v != ""}
|
|
91
|
-
if filtered:
|
|
92
|
-
url += "?" + urllib.parse.urlencode(filtered, doseq=True)
|
|
93
|
-
|
|
94
|
-
headers = {
|
|
95
|
-
"Authorization": f"Bearer {self.api_key}",
|
|
96
|
-
"Content-Type": "application/json",
|
|
97
|
-
"Accept": "application/json",
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
data = json.dumps(body).encode("utf-8") if body else None
|
|
101
|
-
|
|
102
|
-
for attempt in range(self.MAX_RETRIES):
|
|
103
|
-
try:
|
|
104
|
-
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
105
|
-
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
106
|
-
response_body = resp.read().decode("utf-8")
|
|
107
|
-
result = json.loads(response_body) if response_body else {}
|
|
108
|
-
self._log_operation(method, path, resp.status, result)
|
|
109
|
-
return result
|
|
110
|
-
except urllib.error.HTTPError as e:
|
|
111
|
-
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
112
|
-
try:
|
|
113
|
-
error_data = json.loads(error_body) if error_body else {}
|
|
114
|
-
except json.JSONDecodeError:
|
|
115
|
-
error_data = {"error": error_body}
|
|
116
|
-
self._log_operation(method, path, e.code, error_data)
|
|
117
|
-
|
|
118
|
-
if e.code == 429 and attempt < self.MAX_RETRIES - 1:
|
|
119
|
-
delay = self.RETRY_DELAY * (2 ** attempt)
|
|
120
|
-
print(f"Rate limited, retrying in {delay}s...", file=sys.stderr)
|
|
121
|
-
time.sleep(delay)
|
|
122
|
-
continue
|
|
123
|
-
if e.code >= 500 and attempt < self.MAX_RETRIES - 1:
|
|
124
|
-
delay = self.RETRY_DELAY * (2 ** attempt)
|
|
125
|
-
print(f"Server error ({e.code}), retrying in {delay}s...", file=sys.stderr)
|
|
126
|
-
time.sleep(delay)
|
|
127
|
-
continue
|
|
128
|
-
|
|
129
|
-
error_msg = error_data.get("error", f"HTTP {e.code}")
|
|
130
|
-
error_code = error_data.get("code", "UNKNOWN")
|
|
131
|
-
provider = error_data.get("provider_error", "")
|
|
132
|
-
msg = f"Relay API error ({error_code}): {error_msg}"
|
|
133
|
-
if provider:
|
|
134
|
-
msg += f" — provider: {provider}"
|
|
135
|
-
fail(msg)
|
|
136
|
-
except (urllib.error.URLError, TimeoutError):
|
|
137
|
-
if attempt < self.MAX_RETRIES - 1:
|
|
138
|
-
delay = self.RETRY_DELAY * (2 ** attempt)
|
|
139
|
-
print(f"Network error, retrying in {delay}s...", file=sys.stderr)
|
|
140
|
-
time.sleep(delay)
|
|
141
|
-
continue
|
|
142
|
-
fail("Network error: unable to reach relay API")
|
|
143
|
-
|
|
144
|
-
fail("Max retries exceeded")
|
|
145
|
-
return {} # unreachable but satisfies type checker
|
|
146
|
-
|
|
147
|
-
def get(self, path: str, params: Optional[dict] = None) -> dict | list:
|
|
148
|
-
"""GET request to relay API."""
|
|
149
|
-
return self._request("GET", path, params=params)
|
|
150
|
-
|
|
151
|
-
def post(self, path: str, body: Optional[dict] = None) -> dict:
|
|
152
|
-
"""POST request to relay API."""
|
|
153
|
-
return self._request("POST", path, body=body)
|
|
154
|
-
|
|
155
|
-
def put(self, path: str, body: Optional[dict] = None) -> dict:
|
|
156
|
-
"""PUT request to relay API."""
|
|
157
|
-
return self._request("PUT", path, body=body)
|
|
158
|
-
|
|
159
|
-
def patch(self, path: str, body: Optional[dict] = None) -> dict:
|
|
160
|
-
"""PATCH request to relay API."""
|
|
161
|
-
return self._request("PATCH", path, body=body)
|
|
162
|
-
|
|
163
|
-
def _log_operation(self, method: str, path: str, status: int, result: dict | list):
|
|
164
|
-
"""Log operation metadata to local log file."""
|
|
165
|
-
try:
|
|
166
|
-
from datetime import datetime, timezone
|
|
167
|
-
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
-
entry = {
|
|
169
|
-
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
170
|
-
"method": method,
|
|
171
|
-
"path": path,
|
|
172
|
-
"status": status,
|
|
173
|
-
"success": 200 <= status < 300,
|
|
174
|
-
}
|
|
175
|
-
with open(self.log_path, "a") as f:
|
|
176
|
-
f.write(json.dumps(entry) + "\n")
|
|
177
|
-
except Exception:
|
|
178
|
-
pass # logging should never break operations
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def find_project_root() -> Path:
|
|
182
|
-
"""Walk up from cwd to find .flydocs/ directory."""
|
|
183
|
-
current = Path.cwd()
|
|
184
|
-
while current != current.parent:
|
|
185
|
-
if (current / ".flydocs").is_dir():
|
|
186
|
-
return current
|
|
187
|
-
current = current.parent
|
|
188
|
-
return Path.cwd()
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
_client: Optional[FlyDocsClient] = None
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def get_client() -> FlyDocsClient:
|
|
195
|
-
"""Get or create singleton client."""
|
|
196
|
-
global _client
|
|
197
|
-
if _client is None:
|
|
198
|
-
_client = FlyDocsClient()
|
|
199
|
-
return _client
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def output_json(data: dict | list) -> None:
|
|
203
|
-
"""Print JSON to stdout — standard contract output."""
|
|
204
|
-
print(json.dumps(data))
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def fail(message: str) -> None:
|
|
208
|
-
"""Print error to stderr and exit 1."""
|
|
209
|
-
print(message, file=sys.stderr)
|
|
210
|
-
sys.exit(1)
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Get full details for an issue 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
|
-
parser = argparse.ArgumentParser(description="Get issue details")
|
|
12
|
-
parser.add_argument("ref", help="Issue reference (e.g., ENG-123)")
|
|
13
|
-
parser.add_argument("--fields", choices=["basic", "full"], default="full",
|
|
14
|
-
help="basic = no comments, full = with comments (default)")
|
|
15
|
-
args = parser.parse_args()
|
|
16
|
-
|
|
17
|
-
params: dict = {}
|
|
18
|
-
if args.fields == "basic":
|
|
19
|
-
params["fields"] = "basic"
|
|
20
|
-
|
|
21
|
-
client = get_client()
|
|
22
|
-
result = client.get(f"/issues/{args.ref}", params=params)
|
|
23
|
-
|
|
24
|
-
output_json(result)
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Create a link between two issues 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, fail
|
|
9
|
-
|
|
10
|
-
if len(sys.argv) < 4:
|
|
11
|
-
fail("Usage: link.py <ref> <related_ref> <type>")
|
|
12
|
-
|
|
13
|
-
ref, related_ref, link_type = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
14
|
-
|
|
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)}")
|
|
18
|
-
|
|
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,28 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""List cycles 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
|
|
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()
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""List issues with optional filters 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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def main():
|
|
13
|
-
parser = argparse.ArgumentParser(description="List issues")
|
|
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")
|
|
16
|
-
parser.add_argument("--project", default="")
|
|
17
|
-
parser.add_argument("--assignee", default="")
|
|
18
|
-
parser.add_argument("--milestone", default="", help="Filter by milestone ID")
|
|
19
|
-
parser.add_argument("--mine", action="store_true")
|
|
20
|
-
parser.add_argument("--limit", type=int, default=50)
|
|
21
|
-
args = parser.parse_args()
|
|
22
|
-
|
|
23
|
-
params: dict = {"limit": str(args.limit)}
|
|
24
|
-
if args.status:
|
|
25
|
-
params["status"] = args.status.upper()
|
|
26
|
-
if args.active:
|
|
27
|
-
params["active"] = "true"
|
|
28
|
-
if args.project:
|
|
29
|
-
params["project"] = args.project
|
|
30
|
-
if args.assignee:
|
|
31
|
-
params["assignee"] = args.assignee
|
|
32
|
-
if args.milestone:
|
|
33
|
-
params["milestone"] = args.milestone
|
|
34
|
-
if args.mine:
|
|
35
|
-
params["mine"] = "true"
|
|
36
|
-
|
|
37
|
-
client = get_client()
|
|
38
|
-
result = client.get("/issues", params=params)
|
|
39
|
-
|
|
40
|
-
output_json(result)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if __name__ == "__main__":
|
|
44
|
-
main()
|
|
@@ -1,19 +0,0 @@
|
|
|
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,28 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""List milestones 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
|
|
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,31 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""List projects 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
|
|
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()
|
|
@@ -1,19 +0,0 @@
|
|
|
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()
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""List available teams via the FlyDocs Relay API."""
|
|
3
|
-
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
sys.path.insert(0, str(Path(__file__).parent))
|
|
8
|
-
from flydocs_api import get_client, output_json
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def main():
|
|
12
|
-
client = get_client()
|
|
13
|
-
result = client.get("/teams")
|
|
14
|
-
|
|
15
|
-
output_json(result)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if __name__ == "__main__":
|
|
19
|
-
main()
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Set priority on an issue 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, fail
|
|
9
|
-
|
|
10
|
-
if len(sys.argv) < 3:
|
|
11
|
-
fail("Usage: priority.py <ref> <0-4>")
|
|
12
|
-
|
|
13
|
-
ref = sys.argv[1]
|
|
14
|
-
try:
|
|
15
|
-
priority = int(sys.argv[2])
|
|
16
|
-
except ValueError:
|
|
17
|
-
fail("Priority must be a number (0-4)")
|
|
18
|
-
|
|
19
|
-
if priority not in range(5):
|
|
20
|
-
fail("Priority must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low)")
|
|
21
|
-
|
|
22
|
-
client = get_client()
|
|
23
|
-
result = client.put(f"/issues/{ref}/priority", {"priority": priority})
|
|
24
|
-
|
|
25
|
-
output_json({
|
|
26
|
-
"success": result.get("success", True),
|
|
27
|
-
"issue": result.get("issue", ref),
|
|
28
|
-
"priority": result.get("priority", priority),
|
|
29
|
-
})
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Post a project update 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="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()
|
|
@@ -1,68 +0,0 @@
|
|
|
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()
|