@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
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Workspace setup, validation, and configuration dispatcher.
|
|
3
|
+
|
|
4
|
+
All subcommands are cloud-only. Routes through the unified client's relay
|
|
5
|
+
backend for provider operations.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python workspace.py validate
|
|
9
|
+
python workspace.py list-labels
|
|
10
|
+
python workspace.py refresh-labels [--fix]
|
|
11
|
+
python workspace.py list-statuses
|
|
12
|
+
python workspace.py list-providers
|
|
13
|
+
python workspace.py set-provider linear
|
|
14
|
+
python workspace.py list-teams
|
|
15
|
+
python workspace.py create-team --name NAME [--key KEY] [--description DESC] [--parent ID]
|
|
16
|
+
python workspace.py set-team TEAM_ID
|
|
17
|
+
python workspace.py set-labels --defaults '["app"]' --type-map '{"feature":["Feature"]}'
|
|
18
|
+
python workspace.py set-status-mapping --auto
|
|
19
|
+
python workspace.py set-status-mapping --mapping '{"BACKLOG":"Backlog",...}'
|
|
20
|
+
python workspace.py set-identity linear USER_ID
|
|
21
|
+
python workspace.py set-preferences [--workspace ID] [--assignee SELF] [--display JSON]
|
|
22
|
+
python workspace.py get-estimate-scale
|
|
23
|
+
python workspace.py generate-config [--dry-run]
|
|
24
|
+
python workspace.py get-me
|
|
25
|
+
python workspace.py set-active-project PROJECT_ID
|
|
26
|
+
python workspace.py add-active-project PROJECT_ID
|
|
27
|
+
python workspace.py remove-active-project PROJECT_ID
|
|
28
|
+
python workspace.py clear-active-projects
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Optional
|
|
38
|
+
|
|
39
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
40
|
+
from flydocs_api import get_client, output_json, fail, find_project_root
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Human-readable messages for validation checks
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
CHECK_MESSAGES: dict[str, str] = {
|
|
48
|
+
"provider": "No provider connected — configure in FlyDocs dashboard",
|
|
49
|
+
"team": "No team selected — configure in FlyDocs dashboard",
|
|
50
|
+
"statusMapping": "Status mapping not configured — configure in FlyDocs dashboard",
|
|
51
|
+
"labelConfig": "Label config not configured — configure in FlyDocs dashboard",
|
|
52
|
+
"userIdentity": (
|
|
53
|
+
"Provider identity not linked — link your identity in the FlyDocs dashboard "
|
|
54
|
+
"profile page, or run: workspace.py set-identity <provider> <your-account-id>"
|
|
55
|
+
),
|
|
56
|
+
"repos": "No repos linked — GitHub features won't work until you push and link a repo",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
DEFAULT_MESSAGE = "Not configured — check FlyDocs dashboard"
|
|
60
|
+
|
|
61
|
+
VALID_PROVIDERS = ("linear", "jira")
|
|
62
|
+
|
|
63
|
+
# Fields owned by the server — overwritten from generate response
|
|
64
|
+
SERVER_OWNED_FIELDS = {
|
|
65
|
+
"workspaceId",
|
|
66
|
+
"setupComplete",
|
|
67
|
+
"workspace",
|
|
68
|
+
"issueLabels",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Fields owned locally — never overwritten by generate
|
|
72
|
+
LOCAL_ONLY_FIELDS = {
|
|
73
|
+
"version",
|
|
74
|
+
"sourceRepo",
|
|
75
|
+
"tier",
|
|
76
|
+
"paths",
|
|
77
|
+
"detectedStack",
|
|
78
|
+
"skills",
|
|
79
|
+
"designSystem",
|
|
80
|
+
"aiLabor",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Subcommand handlers
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def _check_integrity(client: "FlyDocsClient") -> dict:
|
|
89
|
+
"""Check install integrity against .flydocs/integrity.json."""
|
|
90
|
+
integrity_path = client.project_root / ".flydocs" / "integrity.json"
|
|
91
|
+
if not integrity_path.exists():
|
|
92
|
+
return {"checked": False, "reason": "integrity.json not found"}
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
data = json.loads(integrity_path.read_text())
|
|
96
|
+
except (json.JSONDecodeError, OSError):
|
|
97
|
+
return {"checked": False, "reason": "integrity.json unreadable"}
|
|
98
|
+
|
|
99
|
+
missing_files = []
|
|
100
|
+
for f in data.get("ownedFiles", []):
|
|
101
|
+
if not (client.project_root / f).exists():
|
|
102
|
+
missing_files.append(f)
|
|
103
|
+
|
|
104
|
+
missing_dirs = []
|
|
105
|
+
for d in data.get("ownedDirectories", []):
|
|
106
|
+
if not (client.project_root / d).exists():
|
|
107
|
+
missing_dirs.append(d)
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
"checked": True,
|
|
111
|
+
"valid": len(missing_files) == 0 and len(missing_dirs) == 0,
|
|
112
|
+
"version": data.get("version", "unknown"),
|
|
113
|
+
"missingFiles": missing_files,
|
|
114
|
+
"missingDirectories": missing_dirs,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _try_auto_resolve_identity(client: "FlyDocsClient") -> bool:
|
|
119
|
+
"""Attempt to auto-resolve provider identity via get-me. Returns True if resolved."""
|
|
120
|
+
try:
|
|
121
|
+
result = client.relay.get("/auth/me")
|
|
122
|
+
except Exception:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
provider_id = result.get("providerId")
|
|
126
|
+
if not provider_id:
|
|
127
|
+
# Check providerIdentities array as fallback
|
|
128
|
+
identities = result.get("providerIdentities", [])
|
|
129
|
+
if identities:
|
|
130
|
+
provider_id = identities[0].get("providerId")
|
|
131
|
+
|
|
132
|
+
if not provider_id:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
# Write me.json
|
|
136
|
+
me_data = {
|
|
137
|
+
"displayName": result.get("displayName"),
|
|
138
|
+
"email": result.get("email"),
|
|
139
|
+
"providerId": provider_id,
|
|
140
|
+
"provider": result.get("provider"),
|
|
141
|
+
"providerIdentities": result.get("providerIdentities", []),
|
|
142
|
+
"preferences": result.get("preferences", {}),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
me_path = client.project_root / ".flydocs" / "me.json"
|
|
146
|
+
me_path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
me_path.write_text(json.dumps(me_data, indent=2) + "\n")
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_validate(args: argparse.Namespace) -> None:
|
|
152
|
+
"""Validate workspace setup via GET /auth/config."""
|
|
153
|
+
client = get_client()
|
|
154
|
+
client.require_cloud("validate")
|
|
155
|
+
|
|
156
|
+
config_response = client.relay.get("/auth/config")
|
|
157
|
+
|
|
158
|
+
is_valid = config_response.get("valid", False)
|
|
159
|
+
missing_keys: list[str] = config_response.get("missing", [])
|
|
160
|
+
warning_keys: list[str] = config_response.get("warnings", [])
|
|
161
|
+
|
|
162
|
+
# Auto-resolve identity if missing — try get-me before requiring manual step
|
|
163
|
+
if "userIdentity" in missing_keys or "userIdentity" in warning_keys:
|
|
164
|
+
if _try_auto_resolve_identity(client):
|
|
165
|
+
missing_keys = [k for k in missing_keys if k != "userIdentity"]
|
|
166
|
+
warning_keys = [k for k in warning_keys if k != "userIdentity"]
|
|
167
|
+
# Re-check validity — if userIdentity was the only missing item, we're valid now
|
|
168
|
+
if not missing_keys:
|
|
169
|
+
is_valid = True
|
|
170
|
+
|
|
171
|
+
# Build structured missing/warning lists with messages
|
|
172
|
+
missing = [
|
|
173
|
+
{"check": k, "action": CHECK_MESSAGES.get(k, DEFAULT_MESSAGE)}
|
|
174
|
+
for k in missing_keys
|
|
175
|
+
]
|
|
176
|
+
warnings = [
|
|
177
|
+
{"check": k, "action": CHECK_MESSAGES.get(k, DEFAULT_MESSAGE)}
|
|
178
|
+
for k in warning_keys
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
# Build checks map for cache
|
|
182
|
+
all_keys = set(CHECK_MESSAGES.keys())
|
|
183
|
+
checks: dict[str, bool] = {}
|
|
184
|
+
for k in all_keys:
|
|
185
|
+
checks[k] = k not in missing_keys and k not in warning_keys
|
|
186
|
+
|
|
187
|
+
# Build workspace info from response
|
|
188
|
+
workspace = config_response.get("workspace", {})
|
|
189
|
+
provider_type = config_response.get("provider", {}).get("type", "unknown")
|
|
190
|
+
|
|
191
|
+
# Write validation cache
|
|
192
|
+
cache = {
|
|
193
|
+
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
194
|
+
"valid": is_valid,
|
|
195
|
+
"workspace": {
|
|
196
|
+
"id": workspace.get("id", ""),
|
|
197
|
+
"name": workspace.get("name", ""),
|
|
198
|
+
},
|
|
199
|
+
"provider": provider_type,
|
|
200
|
+
"checks": checks,
|
|
201
|
+
"missing": missing_keys,
|
|
202
|
+
"warnings": warning_keys,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
cache_path = client.project_root / ".flydocs" / "validation-cache.json"
|
|
206
|
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
with open(cache_path, "w") as f:
|
|
208
|
+
json.dump(cache, f, indent=2)
|
|
209
|
+
f.write("\n")
|
|
210
|
+
|
|
211
|
+
# If all required checks pass, set setupComplete in config
|
|
212
|
+
if is_valid:
|
|
213
|
+
config_path = client.config_path
|
|
214
|
+
if config_path.exists():
|
|
215
|
+
with open(config_path, "r") as f:
|
|
216
|
+
local_config = json.load(f)
|
|
217
|
+
else:
|
|
218
|
+
local_config = {}
|
|
219
|
+
|
|
220
|
+
local_config["setupComplete"] = True
|
|
221
|
+
with open(config_path, "w") as f:
|
|
222
|
+
json.dump(local_config, f, indent=2)
|
|
223
|
+
f.write("\n")
|
|
224
|
+
|
|
225
|
+
# Check install integrity
|
|
226
|
+
integrity = _check_integrity(client)
|
|
227
|
+
|
|
228
|
+
# Check activeProjects (local validation, not server-side)
|
|
229
|
+
active_projects = client.config.get("workspace", {}).get("activeProjects", [])
|
|
230
|
+
active_projects_set = len(active_projects) > 0
|
|
231
|
+
|
|
232
|
+
# Output structured report
|
|
233
|
+
report: dict = {
|
|
234
|
+
"valid": is_valid,
|
|
235
|
+
"checks": checks,
|
|
236
|
+
"passed": [k for k, v in checks.items() if v],
|
|
237
|
+
"integrity": integrity,
|
|
238
|
+
"activeProjects": {
|
|
239
|
+
"set": active_projects_set,
|
|
240
|
+
"count": len(active_projects),
|
|
241
|
+
"ids": active_projects,
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
if not active_projects_set:
|
|
245
|
+
report.setdefault("warnings", [])
|
|
246
|
+
if isinstance(report["warnings"], list):
|
|
247
|
+
report["warnings"].append({
|
|
248
|
+
"check": "activeProjects",
|
|
249
|
+
"action": "No active project set — run: workspace.py set-active-project <PROJECT_ID>",
|
|
250
|
+
})
|
|
251
|
+
if missing:
|
|
252
|
+
report["missing"] = missing
|
|
253
|
+
if warnings:
|
|
254
|
+
report["warnings"] = warnings
|
|
255
|
+
if is_valid:
|
|
256
|
+
report["setupComplete"] = True
|
|
257
|
+
|
|
258
|
+
output_json(report)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def cmd_list_labels(args: argparse.Namespace) -> None:
|
|
262
|
+
"""List available team labels."""
|
|
263
|
+
client = get_client()
|
|
264
|
+
client.require_cloud("list-labels")
|
|
265
|
+
result = client.relay.get("/labels")
|
|
266
|
+
output_json(result)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def cmd_refresh_labels(args: argparse.Namespace) -> None:
|
|
270
|
+
"""Refresh label config — validate and optionally fix stale IDs."""
|
|
271
|
+
client = get_client()
|
|
272
|
+
client.require_cloud("refresh-labels")
|
|
273
|
+
|
|
274
|
+
# Fetch current labels from relay
|
|
275
|
+
labels = client.relay.get("/labels")
|
|
276
|
+
label_map = {label["id"]: label["name"] for label in labels}
|
|
277
|
+
label_by_name = {label["name"].lower(): label["id"] for label in labels}
|
|
278
|
+
|
|
279
|
+
# Load local config
|
|
280
|
+
config_path = client.config_path
|
|
281
|
+
if not config_path.exists():
|
|
282
|
+
fail("No .flydocs/config.json found")
|
|
283
|
+
|
|
284
|
+
with open(config_path, "r") as f:
|
|
285
|
+
config = json.load(f)
|
|
286
|
+
|
|
287
|
+
issue_labels = config.get("issueLabels", {})
|
|
288
|
+
stale: list[dict] = []
|
|
289
|
+
valid: list[dict] = []
|
|
290
|
+
|
|
291
|
+
# Check each label ID in config against relay
|
|
292
|
+
for category, entries in issue_labels.items():
|
|
293
|
+
if isinstance(entries, dict):
|
|
294
|
+
for key, label_id in entries.items():
|
|
295
|
+
if label_id in label_map:
|
|
296
|
+
valid.append({
|
|
297
|
+
"category": category,
|
|
298
|
+
"key": key,
|
|
299
|
+
"id": label_id,
|
|
300
|
+
"name": label_map[label_id],
|
|
301
|
+
})
|
|
302
|
+
else:
|
|
303
|
+
# Try to find by key name
|
|
304
|
+
resolved = label_by_name.get(key.lower())
|
|
305
|
+
stale.append({
|
|
306
|
+
"category": category,
|
|
307
|
+
"key": key,
|
|
308
|
+
"staleId": label_id,
|
|
309
|
+
"resolvedId": resolved,
|
|
310
|
+
"resolvedName": key if resolved else None,
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
if args.fix and stale:
|
|
314
|
+
# Update stale IDs in config
|
|
315
|
+
fixed = 0
|
|
316
|
+
for item in stale:
|
|
317
|
+
if item["resolvedId"]:
|
|
318
|
+
issue_labels[item["category"]][item["key"]] = item["resolvedId"]
|
|
319
|
+
fixed += 1
|
|
320
|
+
|
|
321
|
+
with open(config_path, "w") as f:
|
|
322
|
+
json.dump(config, f, indent=2)
|
|
323
|
+
f.write("\n")
|
|
324
|
+
|
|
325
|
+
output_json({
|
|
326
|
+
"success": True,
|
|
327
|
+
"valid": len(valid),
|
|
328
|
+
"stale": len(stale),
|
|
329
|
+
"fixed": fixed,
|
|
330
|
+
"unfixable": len(stale) - fixed,
|
|
331
|
+
"details": stale,
|
|
332
|
+
})
|
|
333
|
+
else:
|
|
334
|
+
output_json({
|
|
335
|
+
"valid": len(valid),
|
|
336
|
+
"stale": len(stale),
|
|
337
|
+
"totalProviderLabels": len(labels),
|
|
338
|
+
"details": stale if stale else "All label IDs are current",
|
|
339
|
+
"hint": "Run with --fix to update stale IDs" if stale else None,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def cmd_list_statuses(args: argparse.Namespace) -> None:
|
|
344
|
+
"""List provider workflow states."""
|
|
345
|
+
client = get_client()
|
|
346
|
+
client.require_cloud("list-statuses")
|
|
347
|
+
result = client.relay.get("/auth/statuses")
|
|
348
|
+
output_json(result)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def cmd_list_providers(args: argparse.Namespace) -> None:
|
|
352
|
+
"""List available providers."""
|
|
353
|
+
client = get_client()
|
|
354
|
+
client.require_cloud("list-providers")
|
|
355
|
+
result = client.relay.get("/providers")
|
|
356
|
+
output_json(result)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def cmd_set_provider(args: argparse.Namespace) -> None:
|
|
360
|
+
"""Set provider preference."""
|
|
361
|
+
client = get_client()
|
|
362
|
+
client.require_cloud("set-provider")
|
|
363
|
+
result = client.relay.post("/auth/provider", {"providerType": args.provider_type})
|
|
364
|
+
output_json(result)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def cmd_list_teams(args: argparse.Namespace) -> None:
|
|
368
|
+
"""List available teams/projects."""
|
|
369
|
+
client = get_client()
|
|
370
|
+
client.require_cloud("list-teams")
|
|
371
|
+
result = client.relay.get("/teams")
|
|
372
|
+
output_json(result)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def cmd_create_team(args: argparse.Namespace) -> None:
|
|
376
|
+
"""Create a team/project."""
|
|
377
|
+
client = get_client()
|
|
378
|
+
client.require_cloud("create-team")
|
|
379
|
+
|
|
380
|
+
body: dict = {"name": args.name}
|
|
381
|
+
if args.key:
|
|
382
|
+
body["key"] = args.key
|
|
383
|
+
if args.description:
|
|
384
|
+
body["description"] = args.description
|
|
385
|
+
if args.parent:
|
|
386
|
+
body["parentId"] = args.parent
|
|
387
|
+
|
|
388
|
+
result = client.relay.post("/teams", body)
|
|
389
|
+
output_json({
|
|
390
|
+
"id": result["id"],
|
|
391
|
+
"name": result["name"],
|
|
392
|
+
"key": result.get("key", ""),
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def cmd_set_team(args: argparse.Namespace) -> None:
|
|
397
|
+
"""Set team/project preference."""
|
|
398
|
+
client = get_client()
|
|
399
|
+
client.require_cloud("set-team")
|
|
400
|
+
result = client.relay.post("/auth/team", {"teamId": args.team_id})
|
|
401
|
+
output_json(result)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def cmd_set_labels(args: argparse.Namespace) -> None:
|
|
405
|
+
"""Set label config on the relay."""
|
|
406
|
+
client = get_client()
|
|
407
|
+
client.require_cloud("set-labels")
|
|
408
|
+
|
|
409
|
+
# Build body from flags or stdin
|
|
410
|
+
if args.defaults is not None or args.type_map is not None:
|
|
411
|
+
body: dict = {}
|
|
412
|
+
if args.defaults is not None:
|
|
413
|
+
try:
|
|
414
|
+
body["defaults"] = json.loads(args.defaults)
|
|
415
|
+
except json.JSONDecodeError:
|
|
416
|
+
fail("Invalid JSON for --defaults")
|
|
417
|
+
if args.type_map is not None:
|
|
418
|
+
try:
|
|
419
|
+
body["typeMap"] = json.loads(args.type_map)
|
|
420
|
+
except json.JSONDecodeError:
|
|
421
|
+
fail("Invalid JSON for --type-map")
|
|
422
|
+
elif not sys.stdin.isatty():
|
|
423
|
+
try:
|
|
424
|
+
body = json.loads(sys.stdin.read().strip())
|
|
425
|
+
except json.JSONDecodeError:
|
|
426
|
+
fail("Invalid JSON on stdin")
|
|
427
|
+
else:
|
|
428
|
+
fail("Provide --defaults/--type-map flags or pipe JSON via stdin")
|
|
429
|
+
|
|
430
|
+
result = client.relay.post("/auth/labels", body)
|
|
431
|
+
output_json(result)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def cmd_set_status_mapping(args: argparse.Namespace) -> None:
|
|
435
|
+
"""Set status mapping on the relay."""
|
|
436
|
+
client = get_client()
|
|
437
|
+
client.require_cloud("set-status-mapping")
|
|
438
|
+
|
|
439
|
+
if args.auto:
|
|
440
|
+
body: dict = {"mapping": "auto"}
|
|
441
|
+
elif args.mapping is not None:
|
|
442
|
+
try:
|
|
443
|
+
body = {"mapping": json.loads(args.mapping)}
|
|
444
|
+
except json.JSONDecodeError:
|
|
445
|
+
fail("Invalid JSON for --mapping")
|
|
446
|
+
elif not sys.stdin.isatty():
|
|
447
|
+
try:
|
|
448
|
+
body = json.loads(sys.stdin.read().strip())
|
|
449
|
+
except json.JSONDecodeError:
|
|
450
|
+
fail("Invalid JSON on stdin")
|
|
451
|
+
else:
|
|
452
|
+
fail("Provide --auto, --mapping '{...}', or pipe JSON via stdin")
|
|
453
|
+
|
|
454
|
+
result = client.relay.post("/auth/statuses", body)
|
|
455
|
+
output_json(result)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def cmd_set_identity(args: argparse.Namespace) -> None:
|
|
459
|
+
"""Set provider identity and write me.json."""
|
|
460
|
+
provider = args.provider.lower()
|
|
461
|
+
if provider not in VALID_PROVIDERS:
|
|
462
|
+
fail(f"Invalid provider: {provider}. Must be one of: {', '.join(VALID_PROVIDERS)}")
|
|
463
|
+
|
|
464
|
+
provider_user_id = args.provider_user_id
|
|
465
|
+
if not provider_user_id:
|
|
466
|
+
fail("Provider user ID cannot be empty")
|
|
467
|
+
|
|
468
|
+
client = get_client()
|
|
469
|
+
client.require_cloud("set-identity")
|
|
470
|
+
|
|
471
|
+
result = client.relay.post("/auth/identity", {
|
|
472
|
+
"provider": provider,
|
|
473
|
+
"providerId": provider_user_id,
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
# Write me.json for local identity resolution
|
|
477
|
+
me_data = {
|
|
478
|
+
"provider": result.get("provider", provider),
|
|
479
|
+
"providerId": result.get("providerId", provider_user_id),
|
|
480
|
+
"displayName": result.get("displayName"),
|
|
481
|
+
"email": result.get("email"),
|
|
482
|
+
}
|
|
483
|
+
me_path = client.project_root / ".flydocs" / "me.json"
|
|
484
|
+
me_path.parent.mkdir(parents=True, exist_ok=True)
|
|
485
|
+
me_path.write_text(json.dumps(me_data, indent=2) + "\n")
|
|
486
|
+
|
|
487
|
+
output_json({
|
|
488
|
+
"success": result.get("success", True),
|
|
489
|
+
"provider": me_data["provider"],
|
|
490
|
+
"providerId": me_data["providerId"],
|
|
491
|
+
"meJson": str(me_path),
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def cmd_set_preferences(args: argparse.Namespace) -> None:
|
|
496
|
+
"""Get or set user preferences."""
|
|
497
|
+
client = get_client()
|
|
498
|
+
client.require_cloud("set-preferences")
|
|
499
|
+
|
|
500
|
+
# If no flags provided, GET current preferences
|
|
501
|
+
if args.workspace is None and args.assignee is None and args.display is None:
|
|
502
|
+
result = client.relay.get("/auth/preferences")
|
|
503
|
+
output_json(result)
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
# Build update body from provided flags
|
|
507
|
+
body: dict = {}
|
|
508
|
+
if args.workspace is not None:
|
|
509
|
+
body["defaultWorkspaceId"] = args.workspace
|
|
510
|
+
if args.assignee is not None:
|
|
511
|
+
body["defaultAssignee"] = args.assignee
|
|
512
|
+
if args.display is not None:
|
|
513
|
+
body["displayPreferences"] = args.display
|
|
514
|
+
|
|
515
|
+
result = client.relay.post("/auth/preferences", body)
|
|
516
|
+
output_json({
|
|
517
|
+
"success": result.get("success", True),
|
|
518
|
+
"preferences": result.get("preferences", body),
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def cmd_get_estimate_scale(args: argparse.Namespace) -> None:
|
|
523
|
+
"""Get the provider's estimate scale."""
|
|
524
|
+
client = get_client()
|
|
525
|
+
client.require_cloud("get-estimate-scale")
|
|
526
|
+
result = client.relay.get("/auth/estimates")
|
|
527
|
+
output_json(result)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def cmd_generate_config(args: argparse.Namespace) -> None:
|
|
531
|
+
"""Generate config.json from the relay's canonical workspace config."""
|
|
532
|
+
client = get_client()
|
|
533
|
+
client.require_cloud("generate-config")
|
|
534
|
+
|
|
535
|
+
# Fetch server-owned config from relay
|
|
536
|
+
response = client.relay.get("/config/generate")
|
|
537
|
+
|
|
538
|
+
if not response.get("valid", False):
|
|
539
|
+
missing = response.get("missing", [])
|
|
540
|
+
warnings = response.get("warnings", [])
|
|
541
|
+
parts = []
|
|
542
|
+
if missing:
|
|
543
|
+
parts.append(f"missing: {', '.join(missing)}")
|
|
544
|
+
if warnings:
|
|
545
|
+
parts.append(f"warnings: {', '.join(warnings)}")
|
|
546
|
+
detail = "; ".join(parts) if parts else "unknown reason"
|
|
547
|
+
print(f"WARNING: Config not fully valid — {detail}", file=sys.stderr)
|
|
548
|
+
|
|
549
|
+
server_config = response.get("config", {})
|
|
550
|
+
|
|
551
|
+
# Read existing local config
|
|
552
|
+
config_path = client.config_path
|
|
553
|
+
if config_path.exists():
|
|
554
|
+
with open(config_path, "r") as f:
|
|
555
|
+
local_config = json.load(f)
|
|
556
|
+
else:
|
|
557
|
+
local_config = {}
|
|
558
|
+
|
|
559
|
+
# Merge: server-owned fields overwrite, local-only fields preserved
|
|
560
|
+
merged = dict(local_config)
|
|
561
|
+
|
|
562
|
+
# Apply server-owned fields
|
|
563
|
+
if "workspaceId" in server_config:
|
|
564
|
+
merged["workspaceId"] = server_config["workspaceId"]
|
|
565
|
+
if "setupComplete" in server_config:
|
|
566
|
+
merged["setupComplete"] = server_config["setupComplete"]
|
|
567
|
+
if "workspace" in server_config:
|
|
568
|
+
local_ws = merged.get("workspace", {})
|
|
569
|
+
server_ws = server_config["workspace"]
|
|
570
|
+
# Start with server values
|
|
571
|
+
merged_ws = dict(server_ws)
|
|
572
|
+
# Preserve local values when server returns empty/null
|
|
573
|
+
if not merged_ws.get("activeProjects") and local_ws.get("activeProjects"):
|
|
574
|
+
merged_ws["activeProjects"] = local_ws["activeProjects"]
|
|
575
|
+
if not merged_ws.get("defaultMilestoneId") and local_ws.get("defaultMilestoneId"):
|
|
576
|
+
merged_ws["defaultMilestoneId"] = local_ws["defaultMilestoneId"]
|
|
577
|
+
if not merged_ws.get("repoSlug") and local_ws.get("repoSlug"):
|
|
578
|
+
merged_ws["repoSlug"] = local_ws["repoSlug"]
|
|
579
|
+
merged["workspace"] = merged_ws
|
|
580
|
+
if "issueLabels" in server_config:
|
|
581
|
+
merged["issueLabels"] = server_config["issueLabels"]
|
|
582
|
+
|
|
583
|
+
# Store configVersion for freshness tracking
|
|
584
|
+
if "configVersion" in response:
|
|
585
|
+
merged["configVersion"] = response["configVersion"]
|
|
586
|
+
|
|
587
|
+
# Clean up ghost fields that are now server-owned
|
|
588
|
+
for ghost in ("provider", "statusMapping", "labels"):
|
|
589
|
+
merged.pop(ghost, None)
|
|
590
|
+
|
|
591
|
+
if args.dry_run:
|
|
592
|
+
output_json(merged)
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
# Write merged config
|
|
596
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
597
|
+
with open(config_path, "w") as f:
|
|
598
|
+
json.dump(merged, f, indent=2)
|
|
599
|
+
f.write("\n")
|
|
600
|
+
|
|
601
|
+
output_json({
|
|
602
|
+
"success": True,
|
|
603
|
+
"configVersion": response.get("configVersion"),
|
|
604
|
+
"valid": response.get("valid", False),
|
|
605
|
+
"missing": response.get("missing", []),
|
|
606
|
+
"warnings": response.get("warnings", []),
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _update_active_projects(project_root: Path, config_path: Path,
|
|
611
|
+
operation: str, project_id: str | None = None) -> dict:
|
|
612
|
+
"""Read config, modify workspace.activeProjects, write config."""
|
|
613
|
+
if config_path.exists():
|
|
614
|
+
with open(config_path, "r") as f:
|
|
615
|
+
config = json.load(f)
|
|
616
|
+
else:
|
|
617
|
+
config = {}
|
|
618
|
+
|
|
619
|
+
workspace = config.setdefault("workspace", {})
|
|
620
|
+
current = workspace.get("activeProjects", [])
|
|
621
|
+
|
|
622
|
+
if operation == "set":
|
|
623
|
+
workspace["activeProjects"] = [project_id] if project_id else []
|
|
624
|
+
elif operation == "add":
|
|
625
|
+
if project_id and project_id not in current:
|
|
626
|
+
current.append(project_id)
|
|
627
|
+
workspace["activeProjects"] = current
|
|
628
|
+
elif operation == "remove":
|
|
629
|
+
workspace["activeProjects"] = [p for p in current if p != project_id]
|
|
630
|
+
elif operation == "clear":
|
|
631
|
+
workspace["activeProjects"] = []
|
|
632
|
+
|
|
633
|
+
config["workspace"] = workspace
|
|
634
|
+
with open(config_path, "w") as f:
|
|
635
|
+
json.dump(config, f, indent=2)
|
|
636
|
+
f.write("\n")
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
"success": True,
|
|
640
|
+
"activeProjects": workspace["activeProjects"],
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def cmd_set_active_project(args: argparse.Namespace) -> None:
|
|
645
|
+
"""Set the active project (replaces any existing)."""
|
|
646
|
+
client = get_client()
|
|
647
|
+
# Validate project exists on cloud tier
|
|
648
|
+
if client.is_cloud:
|
|
649
|
+
projects = client.list_projects(show_all=True)
|
|
650
|
+
if not any(p["id"] == args.project_id for p in projects):
|
|
651
|
+
fail(f"Project not found: {args.project_id}")
|
|
652
|
+
result = _update_active_projects(
|
|
653
|
+
client.project_root, client.config_path, "set", args.project_id
|
|
654
|
+
)
|
|
655
|
+
output_json(result)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def cmd_add_active_project(args: argparse.Namespace) -> None:
|
|
659
|
+
"""Add a project to the active projects list."""
|
|
660
|
+
client = get_client()
|
|
661
|
+
if client.is_cloud:
|
|
662
|
+
projects = client.list_projects(show_all=True)
|
|
663
|
+
if not any(p["id"] == args.project_id for p in projects):
|
|
664
|
+
fail(f"Project not found: {args.project_id}")
|
|
665
|
+
result = _update_active_projects(
|
|
666
|
+
client.project_root, client.config_path, "add", args.project_id
|
|
667
|
+
)
|
|
668
|
+
output_json(result)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def cmd_remove_active_project(args: argparse.Namespace) -> None:
|
|
672
|
+
"""Remove a project from the active projects list."""
|
|
673
|
+
client = get_client()
|
|
674
|
+
result = _update_active_projects(
|
|
675
|
+
client.project_root, client.config_path, "remove", args.project_id
|
|
676
|
+
)
|
|
677
|
+
output_json(result)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def cmd_clear_active_projects(args: argparse.Namespace) -> None:
|
|
681
|
+
"""Clear all active projects."""
|
|
682
|
+
client = get_client()
|
|
683
|
+
result = _update_active_projects(
|
|
684
|
+
client.project_root, client.config_path, "clear"
|
|
685
|
+
)
|
|
686
|
+
output_json(result)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _load_api_key(project_root: Path) -> Optional[str]:
|
|
690
|
+
"""Load API key from environment or .env files (for get-me direct HTTP)."""
|
|
691
|
+
if os.environ.get("FLYDOCS_API_KEY"):
|
|
692
|
+
return os.environ["FLYDOCS_API_KEY"]
|
|
693
|
+
for name in [".env.local", ".env", ".env.development", ".env.production"]:
|
|
694
|
+
env_file = project_root / name
|
|
695
|
+
if env_file.exists():
|
|
696
|
+
with open(env_file, "r") as f:
|
|
697
|
+
for line in f:
|
|
698
|
+
line = line.strip()
|
|
699
|
+
if line.startswith("#") or "=" not in line:
|
|
700
|
+
continue
|
|
701
|
+
k, _, v = line.partition("=")
|
|
702
|
+
if k.strip() == "FLYDOCS_API_KEY":
|
|
703
|
+
v = v.strip().strip("\"'")
|
|
704
|
+
return v if v else None
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _resolve_base_url() -> str:
|
|
709
|
+
"""Resolve relay base URL from environment (for get-me direct HTTP)."""
|
|
710
|
+
env_url = os.environ.get("FLYDOCS_RELAY_URL")
|
|
711
|
+
if env_url:
|
|
712
|
+
return env_url.rstrip("/")
|
|
713
|
+
return "https://app.flydocs.ai/api/relay"
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def cmd_get_me(args: argparse.Namespace) -> None:
|
|
717
|
+
"""Fetch current user identity via direct HTTP (no client/workspace needed)."""
|
|
718
|
+
import urllib.request
|
|
719
|
+
import urllib.error
|
|
720
|
+
|
|
721
|
+
project_root = find_project_root()
|
|
722
|
+
api_key = _load_api_key(project_root)
|
|
723
|
+
if not api_key:
|
|
724
|
+
fail("FLYDOCS_API_KEY not found. Set in environment or .env/.env.local file")
|
|
725
|
+
|
|
726
|
+
base_url = _resolve_base_url()
|
|
727
|
+
url = f"{base_url}/auth/me"
|
|
728
|
+
|
|
729
|
+
headers = {
|
|
730
|
+
"Authorization": f"Bearer {api_key}",
|
|
731
|
+
"Accept": "application/json",
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
try:
|
|
735
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
736
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
737
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
738
|
+
except urllib.error.HTTPError as e:
|
|
739
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
740
|
+
try:
|
|
741
|
+
error_data = json.loads(error_body)
|
|
742
|
+
except json.JSONDecodeError:
|
|
743
|
+
error_data = {"error": error_body}
|
|
744
|
+
fail(f"API error ({e.code}): {error_data.get('error', 'Unknown')}")
|
|
745
|
+
except (urllib.error.URLError, TimeoutError):
|
|
746
|
+
fail("Network error: unable to reach relay API")
|
|
747
|
+
|
|
748
|
+
# Write me.json
|
|
749
|
+
me_data = {
|
|
750
|
+
"displayName": result.get("displayName"),
|
|
751
|
+
"email": result.get("email"),
|
|
752
|
+
"providerId": result.get("providerId"),
|
|
753
|
+
"provider": result.get("provider"),
|
|
754
|
+
"providerIdentities": result.get("providerIdentities", []),
|
|
755
|
+
"preferences": result.get("preferences", {}),
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
me_path = project_root / ".flydocs" / "me.json"
|
|
759
|
+
me_path.parent.mkdir(parents=True, exist_ok=True)
|
|
760
|
+
me_path.write_text(json.dumps(me_data, indent=2) + "\n")
|
|
761
|
+
|
|
762
|
+
output_json({
|
|
763
|
+
"success": True,
|
|
764
|
+
"displayName": me_data["displayName"],
|
|
765
|
+
"email": me_data["email"],
|
|
766
|
+
"provider": me_data["provider"],
|
|
767
|
+
"meJson": str(me_path),
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
# ---------------------------------------------------------------------------
|
|
772
|
+
# CLI parser
|
|
773
|
+
# ---------------------------------------------------------------------------
|
|
774
|
+
|
|
775
|
+
def main() -> None:
|
|
776
|
+
parser = argparse.ArgumentParser(
|
|
777
|
+
description="FlyDocs workspace setup and configuration"
|
|
778
|
+
)
|
|
779
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
780
|
+
|
|
781
|
+
# validate
|
|
782
|
+
sub.add_parser("validate", help="Validate workspace setup")
|
|
783
|
+
|
|
784
|
+
# list-labels
|
|
785
|
+
sub.add_parser("list-labels", help="List available team labels")
|
|
786
|
+
|
|
787
|
+
# refresh-labels
|
|
788
|
+
rl = sub.add_parser("refresh-labels", help="Refresh label config from relay")
|
|
789
|
+
rl.add_argument("--fix", action="store_true", help="Update stale label IDs")
|
|
790
|
+
|
|
791
|
+
# list-statuses
|
|
792
|
+
sub.add_parser("list-statuses", help="List provider workflow states")
|
|
793
|
+
|
|
794
|
+
# list-providers
|
|
795
|
+
sub.add_parser("list-providers", help="List available providers")
|
|
796
|
+
|
|
797
|
+
# set-provider
|
|
798
|
+
sp = sub.add_parser("set-provider", help="Set provider preference")
|
|
799
|
+
sp.add_argument(
|
|
800
|
+
"provider_type",
|
|
801
|
+
choices=["linear", "jira"],
|
|
802
|
+
help="Provider type",
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
# list-teams
|
|
806
|
+
sub.add_parser("list-teams", help="List available teams/projects")
|
|
807
|
+
|
|
808
|
+
# create-team
|
|
809
|
+
ct = sub.add_parser("create-team", help="Create a team/project")
|
|
810
|
+
ct.add_argument("--name", required=True, help="Team name")
|
|
811
|
+
ct.add_argument("--key", default=None, help="Team key (e.g., PROD)")
|
|
812
|
+
ct.add_argument("--description", default=None, help="Team description")
|
|
813
|
+
ct.add_argument("--parent", default=None, help="Parent team ID")
|
|
814
|
+
|
|
815
|
+
# set-team
|
|
816
|
+
st = sub.add_parser("set-team", help="Set team/project preference")
|
|
817
|
+
st.add_argument("team_id", help="Provider team/workspace UUID")
|
|
818
|
+
|
|
819
|
+
# set-labels
|
|
820
|
+
sl = sub.add_parser("set-labels", help="Set label config on relay")
|
|
821
|
+
sl.add_argument("--defaults", default=None, help="JSON array of default label names")
|
|
822
|
+
sl.add_argument(
|
|
823
|
+
"--type-map", default=None, dest="type_map",
|
|
824
|
+
help="JSON object mapping issue types to label arrays",
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
# set-status-mapping
|
|
828
|
+
ssm = sub.add_parser("set-status-mapping", help="Set status mapping on relay")
|
|
829
|
+
ssm.add_argument(
|
|
830
|
+
"--auto", action="store_true",
|
|
831
|
+
help="Auto-map provider states to FlyDocs statuses",
|
|
832
|
+
)
|
|
833
|
+
ssm.add_argument("--mapping", default=None, help="JSON mapping object")
|
|
834
|
+
|
|
835
|
+
# set-identity
|
|
836
|
+
si = sub.add_parser("set-identity", help="Set provider identity")
|
|
837
|
+
si.add_argument("provider", help="Provider type (linear, jira)")
|
|
838
|
+
si.add_argument("provider_user_id", help="Provider-specific user ID")
|
|
839
|
+
|
|
840
|
+
# set-preferences
|
|
841
|
+
spref = sub.add_parser("set-preferences", help="Get or set user preferences")
|
|
842
|
+
spref.add_argument("--workspace", default=None, help="Default workspace ID")
|
|
843
|
+
spref.add_argument("--assignee", default=None, help="Default assignee")
|
|
844
|
+
spref.add_argument("--display", default=None, help="Display preferences (JSON)")
|
|
845
|
+
|
|
846
|
+
# get-estimate-scale
|
|
847
|
+
sub.add_parser("get-estimate-scale", help="Get provider estimate scale")
|
|
848
|
+
|
|
849
|
+
# generate-config
|
|
850
|
+
gc = sub.add_parser("generate-config", help="Generate config from relay")
|
|
851
|
+
gc.add_argument(
|
|
852
|
+
"--dry-run", action="store_true",
|
|
853
|
+
help="Print merged config without writing",
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
# get-me
|
|
857
|
+
sub.add_parser("get-me", help="Fetch current user identity")
|
|
858
|
+
|
|
859
|
+
# set-active-project
|
|
860
|
+
sap = sub.add_parser("set-active-project", help="Set the active project")
|
|
861
|
+
sap.add_argument("project_id", help="Project UUID")
|
|
862
|
+
|
|
863
|
+
# add-active-project
|
|
864
|
+
aap = sub.add_parser("add-active-project", help="Add to active projects")
|
|
865
|
+
aap.add_argument("project_id", help="Project UUID")
|
|
866
|
+
|
|
867
|
+
# remove-active-project
|
|
868
|
+
rap = sub.add_parser("remove-active-project", help="Remove from active projects")
|
|
869
|
+
rap.add_argument("project_id", help="Project UUID")
|
|
870
|
+
|
|
871
|
+
# clear-active-projects
|
|
872
|
+
sub.add_parser("clear-active-projects", help="Clear all active projects")
|
|
873
|
+
|
|
874
|
+
args = parser.parse_args()
|
|
875
|
+
|
|
876
|
+
commands = {
|
|
877
|
+
"validate": cmd_validate,
|
|
878
|
+
"list-labels": cmd_list_labels,
|
|
879
|
+
"refresh-labels": cmd_refresh_labels,
|
|
880
|
+
"list-statuses": cmd_list_statuses,
|
|
881
|
+
"list-providers": cmd_list_providers,
|
|
882
|
+
"set-provider": cmd_set_provider,
|
|
883
|
+
"list-teams": cmd_list_teams,
|
|
884
|
+
"create-team": cmd_create_team,
|
|
885
|
+
"set-team": cmd_set_team,
|
|
886
|
+
"set-labels": cmd_set_labels,
|
|
887
|
+
"set-status-mapping": cmd_set_status_mapping,
|
|
888
|
+
"set-identity": cmd_set_identity,
|
|
889
|
+
"set-preferences": cmd_set_preferences,
|
|
890
|
+
"get-estimate-scale": cmd_get_estimate_scale,
|
|
891
|
+
"generate-config": cmd_generate_config,
|
|
892
|
+
"get-me": cmd_get_me,
|
|
893
|
+
"set-active-project": cmd_set_active_project,
|
|
894
|
+
"add-active-project": cmd_add_active_project,
|
|
895
|
+
"remove-active-project": cmd_remove_active_project,
|
|
896
|
+
"clear-active-projects": cmd_clear_active_projects,
|
|
897
|
+
}
|
|
898
|
+
commands[args.command](args)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
if __name__ == "__main__":
|
|
902
|
+
main()
|