@flydocs/cli 0.6.0-alpha.13 → 0.6.0-alpha.20
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 +281 -256
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +62 -66
- 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 +261 -58
- package/template/.claude/commands/flydocs-upgrade.md +26 -27
- package/template/.claude/commands/implement.md +1 -1
- 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 +132 -0
- package/template/.claude/hooks/post-pr-check.py +108 -0
- package/template/.claude/hooks/post-transition-check.py +94 -0
- package/template/{.flydocs → .claude}/hooks/prompt-submit.py +167 -17
- package/template/.claude/hooks/session-start.py +146 -0
- package/template/.claude/hooks/stop-gate.py +109 -0
- package/template/.claude/settings.json +41 -4
- package/template/.claude/skills/README.md +23 -25
- package/template/.claude/skills/flydocs-workflow/SKILL.md +121 -34
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
- 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 +30 -15
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +1 -1
- package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +251 -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} +133 -46
- package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +693 -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 -1
- 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 +489 -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/workspace.py +860 -0
- package/template/.claude/skills/flydocs-workflow/session.md +16 -11
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +13 -8
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +4 -4
- package/template/.claude/skills/flydocs-workflow/stages/close.md +1 -1
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +7 -7
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +5 -5
- package/template/.claude/skills/flydocs-workflow/stages/review.md +2 -2
- 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 +2 -1
- package/template/.flydocs/templates/README.md +13 -14
- package/template/.flydocs/templates/quick-capture.md +4 -8
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +39 -32
- 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/manifest.json +17 -19
- package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -138
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -28
- 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 -83
- 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/delete_milestone.py +0 -21
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -33
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -241
- package/template/.claude/skills/flydocs-cloud/scripts/generate_config.py +0 -125
- package/template/.claude/skills/flydocs-cloud/scripts/get_estimate_scale.py +0 -23
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
- package/template/.claude/skills/flydocs-cloud/scripts/get_me.py +0 -103
- 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_statuses.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/refresh_labels.py +0 -87
- package/template/.claude/skills/flydocs-cloud/scripts/set_identity.py +0 -54
- package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -54
- package/template/.claude/skills/flydocs-cloud/scripts/set_preferences.py +0 -49
- package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -31
- package/template/.claude/skills/flydocs-cloud/scripts/set_status_mapping.py +0 -57
- package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -28
- 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 -100
- package/template/.claude/skills/flydocs-cloud/scripts/update_milestone.py +0 -42
- package/template/.claude/skills/flydocs-cloud/scripts/validate_setup.py +0 -120
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -94
- 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 -29
- 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 -50
- 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/scripts/skill_manager.py +0 -541
- package/template/.flydocs/templates/bug.md +0 -166
- package/template/.flydocs/templates/chore.md +0 -110
- package/template/.flydocs/templates/feature.md +0 -173
- package/template/.flydocs/templates/idea.md +0 -122
- /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,21 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Delete a milestone 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) < 2:
|
|
11
|
-
fail("Usage: delete_milestone.py <milestone_id>")
|
|
12
|
-
|
|
13
|
-
milestone_id = sys.argv[1]
|
|
14
|
-
|
|
15
|
-
client = get_client()
|
|
16
|
-
result = client.delete(f"/milestones/{milestone_id}")
|
|
17
|
-
|
|
18
|
-
output_json({
|
|
19
|
-
"success": result.get("success", True),
|
|
20
|
-
"id": result.get("id", milestone_id),
|
|
21
|
-
})
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Set estimate on an issue via the FlyDocs Relay API.
|
|
3
|
-
|
|
4
|
-
The relay validates the estimate against the provider's scale server-side.
|
|
5
|
-
Use get_estimate_scale.py to discover valid values before setting.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import sys
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
sys.path.insert(0, str(Path(__file__).parent))
|
|
12
|
-
from flydocs_api import get_client, output_json, fail
|
|
13
|
-
|
|
14
|
-
if len(sys.argv) < 3:
|
|
15
|
-
fail("Usage: estimate.py <ref> <points>")
|
|
16
|
-
|
|
17
|
-
ref = sys.argv[1]
|
|
18
|
-
try:
|
|
19
|
-
estimate = int(sys.argv[2])
|
|
20
|
-
except ValueError:
|
|
21
|
-
fail("Estimate must be a number")
|
|
22
|
-
|
|
23
|
-
if estimate < 0:
|
|
24
|
-
fail("Estimate must be a non-negative integer")
|
|
25
|
-
|
|
26
|
-
client = get_client()
|
|
27
|
-
result = client.put(f"/issues/{ref}/estimate", {"estimate": estimate})
|
|
28
|
-
|
|
29
|
-
output_json({
|
|
30
|
-
"success": result.get("success", True),
|
|
31
|
-
"issue": result.get("issue", ref),
|
|
32
|
-
"estimate": result.get("estimate", estimate),
|
|
33
|
-
})
|
|
@@ -1,241 +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 subprocess
|
|
15
|
-
import sys
|
|
16
|
-
import time
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import Optional
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class FlyDocsClient:
|
|
22
|
-
"""Client for FlyDocs Relay REST API with retry logic and config awareness."""
|
|
23
|
-
|
|
24
|
-
DEFAULT_BASE_URL = "https://app.flydocs.ai/api/relay"
|
|
25
|
-
LOCAL_BASE_URL = "http://localhost:3000/api/relay"
|
|
26
|
-
MAX_RETRIES = 3
|
|
27
|
-
RETRY_DELAY = 2
|
|
28
|
-
|
|
29
|
-
def __init__(self):
|
|
30
|
-
self.project_root = find_project_root()
|
|
31
|
-
self.config_path = self.project_root / ".flydocs" / "config.json"
|
|
32
|
-
self.log_path = self.project_root / ".flydocs" / "logs" / "relay-ops.jsonl"
|
|
33
|
-
|
|
34
|
-
self.config = self._load_config()
|
|
35
|
-
self.api_key = self._load_api_key()
|
|
36
|
-
if not self.api_key:
|
|
37
|
-
print("ERROR: FLYDOCS_API_KEY not found", file=sys.stderr)
|
|
38
|
-
print("Set in environment or .env/.env.local file", file=sys.stderr)
|
|
39
|
-
sys.exit(1)
|
|
40
|
-
|
|
41
|
-
self.workspace_id = self.config.get("workspaceId")
|
|
42
|
-
if not self.workspace_id:
|
|
43
|
-
print("ERROR: workspaceId not found in .flydocs/config.json", file=sys.stderr)
|
|
44
|
-
print("Run 'flydocs setup' to configure your workspace", file=sys.stderr)
|
|
45
|
-
sys.exit(1)
|
|
46
|
-
|
|
47
|
-
self.repo_slug = self._detect_repo_slug()
|
|
48
|
-
self.base_url = self._resolve_base_url()
|
|
49
|
-
|
|
50
|
-
def _load_config(self) -> dict:
|
|
51
|
-
if self.config_path.exists():
|
|
52
|
-
with open(self.config_path, "r") as f:
|
|
53
|
-
return json.load(f)
|
|
54
|
-
return {}
|
|
55
|
-
|
|
56
|
-
def _load_api_key(self) -> Optional[str]:
|
|
57
|
-
if os.environ.get("FLYDOCS_API_KEY"):
|
|
58
|
-
return os.environ["FLYDOCS_API_KEY"]
|
|
59
|
-
for name in [".env.local", ".env"]:
|
|
60
|
-
env_file = self.project_root / name
|
|
61
|
-
if env_file.exists():
|
|
62
|
-
key = self._parse_env_file(env_file, "FLYDOCS_API_KEY")
|
|
63
|
-
if key:
|
|
64
|
-
return key
|
|
65
|
-
return None
|
|
66
|
-
|
|
67
|
-
def _parse_env_file(self, path: Path, key: str) -> Optional[str]:
|
|
68
|
-
with open(path, "r") as f:
|
|
69
|
-
for line in f:
|
|
70
|
-
line = line.strip()
|
|
71
|
-
if line.startswith("#") or "=" not in line:
|
|
72
|
-
continue
|
|
73
|
-
k, _, v = line.partition("=")
|
|
74
|
-
if k.strip() == key:
|
|
75
|
-
v = v.strip().strip("\"'")
|
|
76
|
-
return v if v else None
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
def _detect_repo_slug(self) -> Optional[str]:
|
|
80
|
-
"""Derive owner/repo slug from git remote origin URL."""
|
|
81
|
-
try:
|
|
82
|
-
url = subprocess.check_output(
|
|
83
|
-
["git", "remote", "get-url", "origin"],
|
|
84
|
-
stderr=subprocess.DEVNULL,
|
|
85
|
-
timeout=5,
|
|
86
|
-
).decode().strip()
|
|
87
|
-
if url.endswith(".git"):
|
|
88
|
-
url = url[:-4]
|
|
89
|
-
if ":" in url and "@" in url:
|
|
90
|
-
return url.split(":")[-1] # SSH: git@github.com:owner/repo
|
|
91
|
-
return "/".join(url.split("/")[-2:]) # HTTPS
|
|
92
|
-
except Exception:
|
|
93
|
-
return None
|
|
94
|
-
|
|
95
|
-
def _resolve_base_url(self) -> str:
|
|
96
|
-
"""Resolve base URL: env var > config > default."""
|
|
97
|
-
env_url = os.environ.get("FLYDOCS_RELAY_URL")
|
|
98
|
-
if env_url:
|
|
99
|
-
return env_url.rstrip("/")
|
|
100
|
-
config_url = self.config.get("relay", {}).get("url")
|
|
101
|
-
if config_url:
|
|
102
|
-
return config_url.rstrip("/")
|
|
103
|
-
return self.DEFAULT_BASE_URL
|
|
104
|
-
|
|
105
|
-
def _request(self, method: str, path: str, body: Optional[dict] = None,
|
|
106
|
-
params: Optional[dict] = None) -> dict:
|
|
107
|
-
"""Make an HTTP request to the relay API."""
|
|
108
|
-
import urllib.request
|
|
109
|
-
import urllib.error
|
|
110
|
-
import urllib.parse
|
|
111
|
-
|
|
112
|
-
url = f"{self.base_url}{path}"
|
|
113
|
-
if params:
|
|
114
|
-
filtered = {k: v for k, v in params.items() if v is not None and v != ""}
|
|
115
|
-
if filtered:
|
|
116
|
-
url += "?" + urllib.parse.urlencode(filtered, doseq=True)
|
|
117
|
-
|
|
118
|
-
headers = {
|
|
119
|
-
"Authorization": f"Bearer {self.api_key}",
|
|
120
|
-
"X-Workspace": self.workspace_id,
|
|
121
|
-
"Content-Type": "application/json",
|
|
122
|
-
"Accept": "application/json",
|
|
123
|
-
}
|
|
124
|
-
if self.repo_slug:
|
|
125
|
-
headers["X-Repo"] = self.repo_slug
|
|
126
|
-
|
|
127
|
-
data = json.dumps(body).encode("utf-8") if body else None
|
|
128
|
-
|
|
129
|
-
for attempt in range(self.MAX_RETRIES):
|
|
130
|
-
try:
|
|
131
|
-
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
132
|
-
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
133
|
-
response_body = resp.read().decode("utf-8")
|
|
134
|
-
result = json.loads(response_body) if response_body else {}
|
|
135
|
-
self._log_operation(method, path, resp.status, result)
|
|
136
|
-
return result
|
|
137
|
-
except urllib.error.HTTPError as e:
|
|
138
|
-
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
139
|
-
try:
|
|
140
|
-
error_data = json.loads(error_body) if error_body else {}
|
|
141
|
-
except json.JSONDecodeError:
|
|
142
|
-
error_data = {"error": error_body}
|
|
143
|
-
self._log_operation(method, path, e.code, error_data)
|
|
144
|
-
|
|
145
|
-
if e.code == 429 and attempt < self.MAX_RETRIES - 1:
|
|
146
|
-
delay = self.RETRY_DELAY * (2 ** attempt)
|
|
147
|
-
print(f"Rate limited, retrying in {delay}s...", file=sys.stderr)
|
|
148
|
-
time.sleep(delay)
|
|
149
|
-
continue
|
|
150
|
-
if e.code >= 500 and attempt < self.MAX_RETRIES - 1:
|
|
151
|
-
delay = self.RETRY_DELAY * (2 ** attempt)
|
|
152
|
-
print(f"Server error ({e.code}), retrying in {delay}s...", file=sys.stderr)
|
|
153
|
-
time.sleep(delay)
|
|
154
|
-
continue
|
|
155
|
-
|
|
156
|
-
error_msg = error_data.get("error", f"HTTP {e.code}")
|
|
157
|
-
error_code = error_data.get("code", "UNKNOWN")
|
|
158
|
-
provider = error_data.get("provider_error", "")
|
|
159
|
-
msg = f"Relay API error ({error_code}): {error_msg}"
|
|
160
|
-
if provider:
|
|
161
|
-
msg += f" — provider: {provider}"
|
|
162
|
-
fail(msg)
|
|
163
|
-
except (urllib.error.URLError, TimeoutError):
|
|
164
|
-
if attempt < self.MAX_RETRIES - 1:
|
|
165
|
-
delay = self.RETRY_DELAY * (2 ** attempt)
|
|
166
|
-
print(f"Network error, retrying in {delay}s...", file=sys.stderr)
|
|
167
|
-
time.sleep(delay)
|
|
168
|
-
continue
|
|
169
|
-
fail("Network error: unable to reach relay API")
|
|
170
|
-
|
|
171
|
-
fail("Max retries exceeded")
|
|
172
|
-
return {} # unreachable but satisfies type checker
|
|
173
|
-
|
|
174
|
-
def get(self, path: str, params: Optional[dict] = None) -> dict | list:
|
|
175
|
-
"""GET request to relay API."""
|
|
176
|
-
return self._request("GET", path, params=params)
|
|
177
|
-
|
|
178
|
-
def post(self, path: str, body: Optional[dict] = None) -> dict:
|
|
179
|
-
"""POST request to relay API."""
|
|
180
|
-
return self._request("POST", path, body=body)
|
|
181
|
-
|
|
182
|
-
def put(self, path: str, body: Optional[dict] = None) -> dict:
|
|
183
|
-
"""PUT request to relay API."""
|
|
184
|
-
return self._request("PUT", path, body=body)
|
|
185
|
-
|
|
186
|
-
def patch(self, path: str, body: Optional[dict] = None) -> dict:
|
|
187
|
-
"""PATCH request to relay API."""
|
|
188
|
-
return self._request("PATCH", path, body=body)
|
|
189
|
-
|
|
190
|
-
def delete(self, path: str) -> dict:
|
|
191
|
-
"""DELETE request to relay API."""
|
|
192
|
-
return self._request("DELETE", path)
|
|
193
|
-
|
|
194
|
-
def _log_operation(self, method: str, path: str, status: int, result: dict | list):
|
|
195
|
-
"""Log operation metadata to local log file."""
|
|
196
|
-
try:
|
|
197
|
-
from datetime import datetime, timezone
|
|
198
|
-
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
199
|
-
entry = {
|
|
200
|
-
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
201
|
-
"method": method,
|
|
202
|
-
"path": path,
|
|
203
|
-
"status": status,
|
|
204
|
-
"success": 200 <= status < 300,
|
|
205
|
-
}
|
|
206
|
-
with open(self.log_path, "a") as f:
|
|
207
|
-
f.write(json.dumps(entry) + "\n")
|
|
208
|
-
except Exception:
|
|
209
|
-
pass # logging should never break operations
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def find_project_root() -> Path:
|
|
213
|
-
"""Walk up from cwd to find .flydocs/ directory."""
|
|
214
|
-
current = Path.cwd()
|
|
215
|
-
while current != current.parent:
|
|
216
|
-
if (current / ".flydocs").is_dir():
|
|
217
|
-
return current
|
|
218
|
-
current = current.parent
|
|
219
|
-
return Path.cwd()
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
_client: Optional[FlyDocsClient] = None
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def get_client() -> FlyDocsClient:
|
|
226
|
-
"""Get or create singleton client."""
|
|
227
|
-
global _client
|
|
228
|
-
if _client is None:
|
|
229
|
-
_client = FlyDocsClient()
|
|
230
|
-
return _client
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def output_json(data: dict | list) -> None:
|
|
234
|
-
"""Print JSON to stdout — standard contract output."""
|
|
235
|
-
print(json.dumps(data))
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def fail(message: str) -> None:
|
|
239
|
-
"""Print error to stderr and exit 1."""
|
|
240
|
-
print(message, file=sys.stderr)
|
|
241
|
-
sys.exit(1)
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Generate config.json from the relay's canonical workspace config.
|
|
3
|
-
|
|
4
|
-
Calls GET /config/generate which returns server-owned fields (workspace,
|
|
5
|
-
issueLabels, provider, statusMapping, etc.). Merges with local-only fields
|
|
6
|
-
(tier, detectedStack, skills, designSystem, aiLabor, paths) to produce
|
|
7
|
-
the final .flydocs/config.json.
|
|
8
|
-
|
|
9
|
-
This replaces the pattern where each setup script (set_provider, set_team,
|
|
10
|
-
set_labels, set_status_mapping) independently wrote to local config.
|
|
11
|
-
Now those scripts only POST to the relay, and this script pulls the
|
|
12
|
-
merged result once at the end of setup.
|
|
13
|
-
|
|
14
|
-
Usage:
|
|
15
|
-
python3 generate_config.py
|
|
16
|
-
python3 generate_config.py --dry-run # Print merged config without writing
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
import argparse
|
|
20
|
-
import json
|
|
21
|
-
import sys
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
sys.path.insert(0, str(Path(__file__).parent))
|
|
25
|
-
from flydocs_api import get_client, output_json, fail
|
|
26
|
-
|
|
27
|
-
# Fields owned by the server — overwritten from generate response
|
|
28
|
-
SERVER_OWNED_FIELDS = {
|
|
29
|
-
"workspaceId",
|
|
30
|
-
"setupComplete",
|
|
31
|
-
"workspace",
|
|
32
|
-
"issueLabels",
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
# Fields owned locally — never overwritten by generate
|
|
36
|
-
LOCAL_ONLY_FIELDS = {
|
|
37
|
-
"version",
|
|
38
|
-
"sourceRepo",
|
|
39
|
-
"tier",
|
|
40
|
-
"paths",
|
|
41
|
-
"detectedStack",
|
|
42
|
-
"skills",
|
|
43
|
-
"designSystem",
|
|
44
|
-
"aiLabor",
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def main() -> None:
|
|
49
|
-
parser = argparse.ArgumentParser(description="Generate config from relay")
|
|
50
|
-
parser.add_argument(
|
|
51
|
-
"--dry-run",
|
|
52
|
-
action="store_true",
|
|
53
|
-
help="Print merged config without writing to disk",
|
|
54
|
-
)
|
|
55
|
-
args = parser.parse_args()
|
|
56
|
-
|
|
57
|
-
client = get_client()
|
|
58
|
-
|
|
59
|
-
# Fetch server-owned config from relay
|
|
60
|
-
response = client.get("/config/generate")
|
|
61
|
-
|
|
62
|
-
if not response.get("valid", False):
|
|
63
|
-
missing = response.get("missing", [])
|
|
64
|
-
warnings = response.get("warnings", [])
|
|
65
|
-
parts = []
|
|
66
|
-
if missing:
|
|
67
|
-
parts.append(f"missing: {', '.join(missing)}")
|
|
68
|
-
if warnings:
|
|
69
|
-
parts.append(f"warnings: {', '.join(warnings)}")
|
|
70
|
-
detail = "; ".join(parts) if parts else "unknown reason"
|
|
71
|
-
print(f"WARNING: Config not fully valid — {detail}", file=sys.stderr)
|
|
72
|
-
|
|
73
|
-
server_config = response.get("config", {})
|
|
74
|
-
|
|
75
|
-
# Read existing local config
|
|
76
|
-
config_path = client.config_path
|
|
77
|
-
if config_path.exists():
|
|
78
|
-
with open(config_path, "r") as f:
|
|
79
|
-
local_config = json.load(f)
|
|
80
|
-
else:
|
|
81
|
-
local_config = {}
|
|
82
|
-
|
|
83
|
-
# Merge: server-owned fields overwrite, local-only fields preserved
|
|
84
|
-
merged = dict(local_config)
|
|
85
|
-
|
|
86
|
-
# Apply server-owned fields
|
|
87
|
-
if "workspaceId" in server_config:
|
|
88
|
-
merged["workspaceId"] = server_config["workspaceId"]
|
|
89
|
-
if "setupComplete" in server_config:
|
|
90
|
-
merged["setupComplete"] = server_config["setupComplete"]
|
|
91
|
-
if "workspace" in server_config:
|
|
92
|
-
merged["workspace"] = server_config["workspace"]
|
|
93
|
-
if "issueLabels" in server_config:
|
|
94
|
-
merged["issueLabels"] = server_config["issueLabels"]
|
|
95
|
-
|
|
96
|
-
# Store configVersion for freshness tracking
|
|
97
|
-
if "configVersion" in response:
|
|
98
|
-
merged["configVersion"] = response["configVersion"]
|
|
99
|
-
|
|
100
|
-
# Clean up ghost fields that are now server-owned
|
|
101
|
-
# These were written by old setup scripts but aren't in the TS type system
|
|
102
|
-
for ghost in ("provider", "statusMapping", "labels"):
|
|
103
|
-
merged.pop(ghost, None)
|
|
104
|
-
|
|
105
|
-
if args.dry_run:
|
|
106
|
-
output_json(merged)
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
# Write merged config
|
|
110
|
-
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
-
with open(config_path, "w") as f:
|
|
112
|
-
json.dump(merged, f, indent=2)
|
|
113
|
-
f.write("\n")
|
|
114
|
-
|
|
115
|
-
output_json({
|
|
116
|
-
"success": True,
|
|
117
|
-
"configVersion": response.get("configVersion"),
|
|
118
|
-
"valid": response.get("valid", False),
|
|
119
|
-
"missing": response.get("missing", []),
|
|
120
|
-
"warnings": response.get("warnings", []),
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if __name__ == "__main__":
|
|
125
|
-
main()
|
|
@@ -1,23 +0,0 @@
|
|
|
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,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,103 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Fetch current user identity from the FlyDocs Relay API.
|
|
3
|
-
|
|
4
|
-
Calls GET /auth/me (no X-Workspace needed — user-scoped endpoint).
|
|
5
|
-
Writes .flydocs/me.json for local identity resolution (--mine filters,
|
|
6
|
-
display name in comments, etc.). This file is gitignored.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python3 get_me.py
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import os
|
|
14
|
-
import sys
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
sys.path.insert(0, str(Path(__file__).parent))
|
|
18
|
-
from flydocs_api import find_project_root, output_json, fail
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def load_api_key(project_root: Path) -> str | None:
|
|
22
|
-
"""Load API key from environment or .env files."""
|
|
23
|
-
if os.environ.get("FLYDOCS_API_KEY"):
|
|
24
|
-
return os.environ["FLYDOCS_API_KEY"]
|
|
25
|
-
for name in [".env.local", ".env"]:
|
|
26
|
-
env_file = project_root / name
|
|
27
|
-
if env_file.exists():
|
|
28
|
-
with open(env_file, "r") as f:
|
|
29
|
-
for line in f:
|
|
30
|
-
line = line.strip()
|
|
31
|
-
if line.startswith("#") or "=" not in line:
|
|
32
|
-
continue
|
|
33
|
-
k, _, v = line.partition("=")
|
|
34
|
-
if k.strip() == "FLYDOCS_API_KEY":
|
|
35
|
-
v = v.strip().strip("\"'")
|
|
36
|
-
return v if v else None
|
|
37
|
-
return None
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def resolve_base_url() -> str:
|
|
41
|
-
"""Resolve relay base URL from environment."""
|
|
42
|
-
env_url = os.environ.get("FLYDOCS_RELAY_URL")
|
|
43
|
-
if env_url:
|
|
44
|
-
return env_url.rstrip("/")
|
|
45
|
-
return "https://app.flydocs.ai/api/relay"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def main() -> None:
|
|
49
|
-
import urllib.request
|
|
50
|
-
import urllib.error
|
|
51
|
-
|
|
52
|
-
project_root = find_project_root()
|
|
53
|
-
api_key = load_api_key(project_root)
|
|
54
|
-
if not api_key:
|
|
55
|
-
fail("FLYDOCS_API_KEY not found. Set in environment or .env/.env.local file")
|
|
56
|
-
|
|
57
|
-
base_url = resolve_base_url()
|
|
58
|
-
url = f"{base_url}/auth/me"
|
|
59
|
-
|
|
60
|
-
headers = {
|
|
61
|
-
"Authorization": f"Bearer {api_key}",
|
|
62
|
-
"Accept": "application/json",
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try:
|
|
66
|
-
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
67
|
-
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
68
|
-
result = json.loads(resp.read().decode("utf-8"))
|
|
69
|
-
except urllib.error.HTTPError as e:
|
|
70
|
-
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
71
|
-
try:
|
|
72
|
-
error_data = json.loads(error_body)
|
|
73
|
-
except json.JSONDecodeError:
|
|
74
|
-
error_data = {"error": error_body}
|
|
75
|
-
fail(f"API error ({e.code}): {error_data.get('error', 'Unknown')}")
|
|
76
|
-
except (urllib.error.URLError, TimeoutError):
|
|
77
|
-
fail("Network error: unable to reach relay API")
|
|
78
|
-
|
|
79
|
-
# Write me.json
|
|
80
|
-
me_data = {
|
|
81
|
-
"displayName": result.get("displayName"),
|
|
82
|
-
"email": result.get("email"),
|
|
83
|
-
"providerId": result.get("providerId"),
|
|
84
|
-
"provider": result.get("provider"),
|
|
85
|
-
"providerIdentities": result.get("providerIdentities", []),
|
|
86
|
-
"preferences": result.get("preferences", {}),
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
me_path = project_root / ".flydocs" / "me.json"
|
|
90
|
-
me_path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
-
me_path.write_text(json.dumps(me_data, indent=2) + "\n")
|
|
92
|
-
|
|
93
|
-
output_json({
|
|
94
|
-
"success": True,
|
|
95
|
-
"displayName": me_data["displayName"],
|
|
96
|
-
"email": me_data["email"],
|
|
97
|
-
"provider": me_data["provider"],
|
|
98
|
-
"meJson": str(me_path),
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if __name__ == "__main__":
|
|
103
|
-
main()
|
|
@@ -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()
|