@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.
Files changed (152) hide show
  1. package/dist/cli.js +281 -256
  2. package/package.json +1 -1
  3. package/template/.claude/CLAUDE.md +62 -66
  4. package/template/.claude/agents/implementation-agent.md +1 -1
  5. package/template/.claude/agents/pm-agent.md +1 -1
  6. package/template/.claude/commands/activate.md +1 -1
  7. package/template/.claude/commands/attach.md +1 -1
  8. package/template/.claude/commands/block.md +2 -2
  9. package/template/.claude/commands/capture.md +1 -1
  10. package/template/.claude/commands/close.md +1 -1
  11. package/template/.claude/commands/flydocs-setup.md +261 -58
  12. package/template/.claude/commands/flydocs-upgrade.md +26 -27
  13. package/template/.claude/commands/implement.md +1 -1
  14. package/template/.claude/commands/new-project.md +1 -1
  15. package/template/.claude/commands/onboard.md +275 -0
  16. package/template/.claude/commands/project-update.md +1 -1
  17. package/template/.claude/commands/refine.md +1 -1
  18. package/template/.claude/commands/review.md +1 -1
  19. package/template/.claude/commands/start-session.md +1 -1
  20. package/template/.claude/commands/status.md +1 -1
  21. package/template/.claude/commands/validate.md +1 -1
  22. package/template/.claude/commands/wrap-session.md +1 -1
  23. package/template/.claude/hooks/auto-approve.py +132 -0
  24. package/template/.claude/hooks/post-pr-check.py +108 -0
  25. package/template/.claude/hooks/post-transition-check.py +94 -0
  26. package/template/{.flydocs → .claude}/hooks/prompt-submit.py +167 -17
  27. package/template/.claude/hooks/session-start.py +146 -0
  28. package/template/.claude/hooks/stop-gate.py +109 -0
  29. package/template/.claude/settings.json +41 -4
  30. package/template/.claude/skills/README.md +23 -25
  31. package/template/.claude/skills/flydocs-workflow/SKILL.md +121 -34
  32. package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
  33. package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
  34. package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
  35. package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +30 -15
  36. package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +1 -1
  37. package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +251 -0
  38. package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
  39. package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
  40. package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +133 -46
  41. package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +693 -0
  42. package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
  43. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
  44. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
  45. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -1
  46. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
  47. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
  48. package/template/.claude/skills/flydocs-workflow/scripts/issues.py +489 -0
  49. package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
  50. package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
  51. package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
  52. package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
  53. package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +860 -0
  54. package/template/.claude/skills/flydocs-workflow/session.md +16 -11
  55. package/template/.claude/skills/flydocs-workflow/stages/activate.md +13 -8
  56. package/template/.claude/skills/flydocs-workflow/stages/capture.md +4 -4
  57. package/template/.claude/skills/flydocs-workflow/stages/close.md +1 -1
  58. package/template/.claude/skills/flydocs-workflow/stages/implement.md +7 -7
  59. package/template/.claude/skills/flydocs-workflow/stages/refine.md +5 -5
  60. package/template/.claude/skills/flydocs-workflow/stages/review.md +2 -2
  61. package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
  62. package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
  63. package/template/.cursor/agents/implementation-agent.md +1 -1
  64. package/template/.cursor/agents/pm-agent.md +2 -2
  65. package/template/.cursor/hooks.json +10 -3
  66. package/template/.env.example +6 -6
  67. package/template/.flydocs/config.json +2 -1
  68. package/template/.flydocs/templates/README.md +13 -14
  69. package/template/.flydocs/templates/quick-capture.md +4 -8
  70. package/template/.flydocs/version +1 -1
  71. package/template/AGENTS.md +39 -32
  72. package/template/flydocs/README.md +1 -3
  73. package/template/flydocs/context/project.md +6 -3
  74. package/template/flydocs/design-system/README.md +3 -3
  75. package/template/manifest.json +17 -19
  76. package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -138
  77. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
  78. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -28
  79. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
  80. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
  81. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
  82. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -83
  83. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
  84. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
  85. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
  86. package/template/.claude/skills/flydocs-cloud/scripts/delete_milestone.py +0 -21
  87. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -33
  88. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -241
  89. package/template/.claude/skills/flydocs-cloud/scripts/generate_config.py +0 -125
  90. package/template/.claude/skills/flydocs-cloud/scripts/get_estimate_scale.py +0 -23
  91. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
  92. package/template/.claude/skills/flydocs-cloud/scripts/get_me.py +0 -103
  93. package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
  94. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
  95. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
  96. package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
  97. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
  98. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
  99. package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +0 -19
  100. package/template/.claude/skills/flydocs-cloud/scripts/list_statuses.py +0 -19
  101. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
  102. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
  103. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
  104. package/template/.claude/skills/flydocs-cloud/scripts/refresh_labels.py +0 -87
  105. package/template/.claude/skills/flydocs-cloud/scripts/set_identity.py +0 -54
  106. package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -54
  107. package/template/.claude/skills/flydocs-cloud/scripts/set_preferences.py +0 -49
  108. package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -31
  109. package/template/.claude/skills/flydocs-cloud/scripts/set_status_mapping.py +0 -57
  110. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -28
  111. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
  112. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
  113. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -100
  114. package/template/.claude/skills/flydocs-cloud/scripts/update_milestone.py +0 -42
  115. package/template/.claude/skills/flydocs-cloud/scripts/validate_setup.py +0 -120
  116. package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -94
  117. package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
  118. package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
  119. package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
  120. package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
  121. package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
  122. package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
  123. package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
  124. package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
  125. package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
  126. package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
  127. package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
  128. package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -29
  129. package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
  130. package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
  131. package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
  132. package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
  133. package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
  134. package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -50
  135. package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
  136. package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
  137. package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
  138. package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
  139. package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
  140. package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
  141. package/template/.flydocs/hooks/auto-approve.py +0 -71
  142. package/template/.flydocs/scripts/skill_manager.py +0 -541
  143. package/template/.flydocs/templates/bug.md +0 -166
  144. package/template/.flydocs/templates/chore.md +0 -110
  145. package/template/.flydocs/templates/feature.md +0 -173
  146. package/template/.flydocs/templates/idea.md +0 -122
  147. /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
  148. /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
  149. /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
  150. /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
  151. /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
  152. /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()