@flydocs/cli 0.6.0-alpha.2 → 0.6.0-alpha.21
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 +705 -393
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +62 -63
- 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 +387 -74
- package/template/.claude/commands/flydocs-upgrade.md +48 -37
- 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 +132 -0
- package/template/.claude/hooks/post-pr-check.py +108 -0
- package/template/.claude/hooks/post-transition-check.py +94 -0
- package/template/.claude/hooks/prompt-submit.py +513 -0
- 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 +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 +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} +137 -47
- 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 -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 +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 +63 -25
- 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/quick-capture.md +4 -8
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +39 -32
- package/template/CHANGELOG.md +39 -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 -111
- 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 -63
- 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_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_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/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
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Issue operations dispatcher — create, get, list, transition, assign, update, and more."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
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, resolve_text_input
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Subcommand handlers
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
def cmd_create(args: argparse.Namespace) -> None:
|
|
20
|
+
"""Create a new issue."""
|
|
21
|
+
# Description resolution: --description-file > stdin > --description
|
|
22
|
+
description = resolve_text_input(file_arg=args.description_file)
|
|
23
|
+
if description is None:
|
|
24
|
+
description = args.description or ""
|
|
25
|
+
|
|
26
|
+
client = get_client()
|
|
27
|
+
result = client.create_issue(
|
|
28
|
+
title=args.title,
|
|
29
|
+
issue_type=args.type,
|
|
30
|
+
description=description,
|
|
31
|
+
priority=args.priority,
|
|
32
|
+
estimate=args.estimate,
|
|
33
|
+
assignee=args.assignee,
|
|
34
|
+
project=args.project,
|
|
35
|
+
triage=args.triage,
|
|
36
|
+
)
|
|
37
|
+
output_json(result)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def cmd_get(args: argparse.Namespace) -> None:
|
|
41
|
+
"""Get a single issue by reference."""
|
|
42
|
+
client = get_client()
|
|
43
|
+
result = client.get_issue(args.ref, fields=args.fields)
|
|
44
|
+
output_json(result)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_list(args: argparse.Namespace) -> None:
|
|
48
|
+
"""List issues with optional filters."""
|
|
49
|
+
client = get_client()
|
|
50
|
+
result = client.list_issues(
|
|
51
|
+
status=args.status,
|
|
52
|
+
active=args.active,
|
|
53
|
+
project=args.project,
|
|
54
|
+
assignee=args.assignee,
|
|
55
|
+
milestone=args.milestone,
|
|
56
|
+
mine=args.mine,
|
|
57
|
+
show_all=args.show_all,
|
|
58
|
+
limit=args.limit,
|
|
59
|
+
)
|
|
60
|
+
output_json(result)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cmd_transition(args: argparse.Namespace) -> None:
|
|
64
|
+
"""Transition an issue to a new status with a comment."""
|
|
65
|
+
client = get_client()
|
|
66
|
+
result = client.transition(args.ref, args.status, args.comment)
|
|
67
|
+
output_json(result)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmd_assign(args: argparse.Namespace) -> None:
|
|
71
|
+
"""Assign or unassign an issue."""
|
|
72
|
+
assignee = None if args.unassign else args.assignee
|
|
73
|
+
client = get_client()
|
|
74
|
+
result = client.assign(args.ref, assignee)
|
|
75
|
+
output_json(result)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def cmd_update(args: argparse.Namespace) -> None:
|
|
79
|
+
"""Update one or more fields on an issue."""
|
|
80
|
+
# Description resolution: --description-file > --description
|
|
81
|
+
description = args.description
|
|
82
|
+
if args.description_file:
|
|
83
|
+
path = Path(args.description_file)
|
|
84
|
+
if not path.exists():
|
|
85
|
+
fail(f"File not found: {args.description_file}")
|
|
86
|
+
description = path.read_text()
|
|
87
|
+
|
|
88
|
+
fields: dict = {}
|
|
89
|
+
if args.title is not None:
|
|
90
|
+
fields["title"] = args.title
|
|
91
|
+
if args.priority is not None:
|
|
92
|
+
fields["priority"] = args.priority
|
|
93
|
+
if args.estimate is not None:
|
|
94
|
+
fields["estimate"] = args.estimate
|
|
95
|
+
if args.assignee is not None:
|
|
96
|
+
fields["assignee"] = args.assignee
|
|
97
|
+
if args.state is not None:
|
|
98
|
+
fields["state"] = args.state
|
|
99
|
+
if description is not None:
|
|
100
|
+
fields["description"] = description
|
|
101
|
+
if args.labels is not None:
|
|
102
|
+
fields["labels"] = args.labels
|
|
103
|
+
if args.milestone is not None:
|
|
104
|
+
fields["milestone"] = args.milestone
|
|
105
|
+
if args.comment is not None:
|
|
106
|
+
fields["comment"] = args.comment
|
|
107
|
+
|
|
108
|
+
if not fields:
|
|
109
|
+
fail("No fields to update")
|
|
110
|
+
|
|
111
|
+
client = get_client()
|
|
112
|
+
result = client.update_issue(args.ref, **fields)
|
|
113
|
+
output_json(result)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cmd_description(args: argparse.Namespace) -> None:
|
|
117
|
+
"""Update an issue's description."""
|
|
118
|
+
# Text resolution: --file > stdin > --text
|
|
119
|
+
text = resolve_text_input(text_arg=args.text, file_arg=args.file)
|
|
120
|
+
if text is None:
|
|
121
|
+
fail("No description text provided (use --text, --file, or pipe to stdin)")
|
|
122
|
+
|
|
123
|
+
client = get_client()
|
|
124
|
+
result = client.update_description(args.ref, text)
|
|
125
|
+
output_json(result)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def cmd_comment(args: argparse.Namespace) -> None:
|
|
129
|
+
"""Add a comment to an issue."""
|
|
130
|
+
body = args.body
|
|
131
|
+
if body is None and not sys.stdin.isatty():
|
|
132
|
+
body = sys.stdin.read().strip()
|
|
133
|
+
if not body:
|
|
134
|
+
fail("No comment body provided (pass as argument or pipe to stdin)")
|
|
135
|
+
|
|
136
|
+
client = get_client()
|
|
137
|
+
result = client.comment(args.ref, body)
|
|
138
|
+
output_json(result)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cmd_estimate(args: argparse.Namespace) -> None:
|
|
142
|
+
"""Set estimate points on an issue."""
|
|
143
|
+
if args.points < 0:
|
|
144
|
+
fail("Estimate points must be non-negative")
|
|
145
|
+
|
|
146
|
+
client = get_client()
|
|
147
|
+
result = client.estimate(args.ref, args.points)
|
|
148
|
+
output_json(result)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_priority(args: argparse.Namespace) -> None:
|
|
152
|
+
"""Set priority level on an issue."""
|
|
153
|
+
if args.level < 0 or args.level > 4:
|
|
154
|
+
fail("Priority level must be 0-4")
|
|
155
|
+
|
|
156
|
+
client = get_client()
|
|
157
|
+
result = client.priority(args.ref, args.level)
|
|
158
|
+
output_json(result)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def cmd_link(args: argparse.Namespace) -> None:
|
|
162
|
+
"""Link two issues together."""
|
|
163
|
+
client = get_client()
|
|
164
|
+
result = client.link(args.ref, args.related_ref, args.type)
|
|
165
|
+
output_json(result)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def cmd_assign_milestone(args: argparse.Namespace) -> None:
|
|
169
|
+
"""Assign an issue to a milestone."""
|
|
170
|
+
client = get_client()
|
|
171
|
+
result = client.assign_milestone(args.ref, args.milestone_id)
|
|
172
|
+
output_json(result)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def cmd_assign_cycle(args: argparse.Namespace) -> None:
|
|
176
|
+
"""Assign an issue to a cycle (or remove from current cycle)."""
|
|
177
|
+
client = get_client()
|
|
178
|
+
result = client.assign_cycle(args.ref, args.cycle_id)
|
|
179
|
+
output_json(result)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# PR creation
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def _detect_platform() -> str:
|
|
187
|
+
"""Detect git hosting platform from remote URL. Returns github|gitlab|bitbucket|unknown."""
|
|
188
|
+
try:
|
|
189
|
+
result = subprocess.run(
|
|
190
|
+
["git", "remote", "get-url", "origin"],
|
|
191
|
+
capture_output=True, text=True, timeout=5,
|
|
192
|
+
)
|
|
193
|
+
url = result.stdout.strip().lower()
|
|
194
|
+
if "github" in url:
|
|
195
|
+
return "github"
|
|
196
|
+
if "gitlab" in url:
|
|
197
|
+
return "gitlab"
|
|
198
|
+
if "bitbucket" in url:
|
|
199
|
+
return "bitbucket"
|
|
200
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
|
201
|
+
pass
|
|
202
|
+
return "unknown"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _get_current_branch() -> str:
|
|
206
|
+
"""Get current git branch name."""
|
|
207
|
+
try:
|
|
208
|
+
result = subprocess.run(
|
|
209
|
+
["git", "branch", "--show-current"],
|
|
210
|
+
capture_output=True, text=True, timeout=5,
|
|
211
|
+
)
|
|
212
|
+
return result.stdout.strip()
|
|
213
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
|
214
|
+
return ""
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _get_default_branch() -> str:
|
|
218
|
+
"""Detect the default branch (main or master)."""
|
|
219
|
+
try:
|
|
220
|
+
result = subprocess.run(
|
|
221
|
+
["git", "rev-parse", "--verify", "refs/heads/main"],
|
|
222
|
+
capture_output=True, timeout=5,
|
|
223
|
+
)
|
|
224
|
+
if result.returncode == 0:
|
|
225
|
+
return "main"
|
|
226
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
|
227
|
+
pass
|
|
228
|
+
return "master"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _build_pr_body(issue_ref: str | None, summary: str | None) -> str:
|
|
232
|
+
"""Build PR body from template, optionally populating from issue data."""
|
|
233
|
+
# Read the PR template
|
|
234
|
+
script_dir = Path(__file__).parent
|
|
235
|
+
template_path = script_dir.parent / "templates" / "pr" / "default.md"
|
|
236
|
+
|
|
237
|
+
if template_path.exists():
|
|
238
|
+
body = template_path.read_text()
|
|
239
|
+
else:
|
|
240
|
+
body = "## Summary\n\n{ISSUE_SUMMARY}\n\n## Changes\n\n- \n\n## Test Plan\n\n- [ ] \n\n## Notes\n\n"
|
|
241
|
+
|
|
242
|
+
# Populate issue context if available
|
|
243
|
+
if issue_ref:
|
|
244
|
+
body = body.replace("{ISSUE_REF}", issue_ref)
|
|
245
|
+
try:
|
|
246
|
+
client = get_client()
|
|
247
|
+
issue = client.get_issue(issue_ref, fields="full")
|
|
248
|
+
if isinstance(issue, dict):
|
|
249
|
+
title = issue.get("title", "")
|
|
250
|
+
description = issue.get("description", "")
|
|
251
|
+
|
|
252
|
+
# Extract AC checkboxes from description
|
|
253
|
+
ac_lines = []
|
|
254
|
+
if description:
|
|
255
|
+
for line in description.splitlines():
|
|
256
|
+
if re.match(r"^\s*-\s*\[[ x]\]", line, re.IGNORECASE):
|
|
257
|
+
ac_lines.append(line)
|
|
258
|
+
|
|
259
|
+
body = body.replace("{ISSUE_SUMMARY}", title)
|
|
260
|
+
body = body.replace(
|
|
261
|
+
"{ACCEPTANCE_CRITERIA}",
|
|
262
|
+
"\n".join(ac_lines) if ac_lines else "See issue for acceptance criteria.",
|
|
263
|
+
)
|
|
264
|
+
except Exception:
|
|
265
|
+
body = body.replace("{ISSUE_SUMMARY}", summary or "")
|
|
266
|
+
body = body.replace("{ACCEPTANCE_CRITERIA}", "See issue for acceptance criteria.")
|
|
267
|
+
else:
|
|
268
|
+
body = body.replace("{ISSUE_REF}", "")
|
|
269
|
+
body = body.replace("{ISSUE_SUMMARY}", summary or "")
|
|
270
|
+
body = body.replace("{ACCEPTANCE_CRITERIA}", "")
|
|
271
|
+
|
|
272
|
+
# Clean up remaining placeholders
|
|
273
|
+
body = body.replace("{CHANGE_1}", "")
|
|
274
|
+
body = body.replace("{CHANGE_2}", "")
|
|
275
|
+
body = body.replace("{TEST_1}", "")
|
|
276
|
+
body = body.replace("{TEST_2}", "")
|
|
277
|
+
body = body.replace("{NOTES}", "")
|
|
278
|
+
|
|
279
|
+
return body
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def cmd_pr(args: argparse.Namespace) -> None:
|
|
283
|
+
"""Create a pull/merge request with standard template."""
|
|
284
|
+
platform = _detect_platform()
|
|
285
|
+
branch = _get_current_branch()
|
|
286
|
+
base = args.base or _get_default_branch()
|
|
287
|
+
|
|
288
|
+
if not branch or branch == base:
|
|
289
|
+
fail(f"Cannot create PR from branch '{branch}' — switch to a feature branch first.")
|
|
290
|
+
|
|
291
|
+
# Build title
|
|
292
|
+
title = args.title
|
|
293
|
+
if not title and args.issue:
|
|
294
|
+
try:
|
|
295
|
+
client = get_client()
|
|
296
|
+
issue = client.get_issue(args.issue, fields="basic")
|
|
297
|
+
if isinstance(issue, dict):
|
|
298
|
+
title = issue.get("title", args.issue)
|
|
299
|
+
except Exception:
|
|
300
|
+
title = args.issue
|
|
301
|
+
if not title:
|
|
302
|
+
title = branch
|
|
303
|
+
|
|
304
|
+
# Build body from template
|
|
305
|
+
body = _build_pr_body(issue_ref=args.issue, summary=title)
|
|
306
|
+
|
|
307
|
+
if args.dry_run:
|
|
308
|
+
output_json({
|
|
309
|
+
"platform": platform,
|
|
310
|
+
"branch": branch,
|
|
311
|
+
"base": base,
|
|
312
|
+
"title": title,
|
|
313
|
+
"body": body,
|
|
314
|
+
})
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
# Create PR via platform CLI
|
|
318
|
+
if platform == "github":
|
|
319
|
+
cmd = ["gh", "pr", "create", "--title", title, "--body", body, "--base", base]
|
|
320
|
+
if args.draft:
|
|
321
|
+
cmd.append("--draft")
|
|
322
|
+
elif platform == "gitlab":
|
|
323
|
+
cmd = ["glab", "mr", "create", "--title", title, "--description", body, "--target-branch", base]
|
|
324
|
+
if args.draft:
|
|
325
|
+
cmd.append("--draft")
|
|
326
|
+
elif platform == "bitbucket":
|
|
327
|
+
fail("Bitbucket PR creation via CLI is not yet supported. Use the web UI or Bitbucket API.")
|
|
328
|
+
else:
|
|
329
|
+
fail(f"Could not detect git platform from remote URL. Detected: {platform}")
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
333
|
+
if result.returncode != 0:
|
|
334
|
+
fail(f"PR creation failed: {result.stderr.strip()}")
|
|
335
|
+
|
|
336
|
+
pr_url = result.stdout.strip()
|
|
337
|
+
|
|
338
|
+
# Post PR link as comment on the issue
|
|
339
|
+
if args.issue:
|
|
340
|
+
try:
|
|
341
|
+
client = get_client()
|
|
342
|
+
client.comment(args.issue, f"PR created: {pr_url}")
|
|
343
|
+
except Exception:
|
|
344
|
+
pass # Non-blocking — PR was created successfully
|
|
345
|
+
|
|
346
|
+
output_json({
|
|
347
|
+
"success": True,
|
|
348
|
+
"platform": platform,
|
|
349
|
+
"url": pr_url,
|
|
350
|
+
"branch": branch,
|
|
351
|
+
"base": base,
|
|
352
|
+
})
|
|
353
|
+
except FileNotFoundError:
|
|
354
|
+
cli = "gh" if platform == "github" else "glab"
|
|
355
|
+
fail(f"{cli} CLI not found. Install it to create PRs from the command line.")
|
|
356
|
+
except subprocess.TimeoutExpired:
|
|
357
|
+
fail("PR creation timed out.")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
# Argument parser
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
def main() -> None:
|
|
365
|
+
parser = argparse.ArgumentParser(description="FlyDocs issue operations")
|
|
366
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
367
|
+
|
|
368
|
+
# -- create --
|
|
369
|
+
p = sub.add_parser("create", help="Create a new issue")
|
|
370
|
+
p.add_argument("--title", required=True)
|
|
371
|
+
p.add_argument("--type", required=True, choices=["feature", "bug", "chore", "idea"])
|
|
372
|
+
p.add_argument("--description", default=None)
|
|
373
|
+
p.add_argument("--description-file", default=None)
|
|
374
|
+
p.add_argument("--priority", type=int, default=3, choices=[0, 1, 2, 3, 4])
|
|
375
|
+
p.add_argument("--estimate", type=int, default=None, choices=[0, 1, 2, 3, 5])
|
|
376
|
+
p.add_argument("--assignee", default=None)
|
|
377
|
+
p.add_argument("--project", default=None)
|
|
378
|
+
p.add_argument("--triage", action="store_true")
|
|
379
|
+
|
|
380
|
+
# -- get --
|
|
381
|
+
p = sub.add_parser("get", help="Get a single issue")
|
|
382
|
+
p.add_argument("ref")
|
|
383
|
+
p.add_argument("--fields", default="full", choices=["basic", "full"])
|
|
384
|
+
|
|
385
|
+
# -- list --
|
|
386
|
+
p = sub.add_parser("list", help="List issues")
|
|
387
|
+
p.add_argument("--status", default=None)
|
|
388
|
+
p.add_argument("--active", action="store_true")
|
|
389
|
+
p.add_argument("--project", default=None)
|
|
390
|
+
p.add_argument("--assignee", default=None)
|
|
391
|
+
p.add_argument("--milestone", default=None)
|
|
392
|
+
p.add_argument("--mine", action="store_true")
|
|
393
|
+
p.add_argument("--all", action="store_true", dest="show_all", help="Bypass product scope cascade")
|
|
394
|
+
p.add_argument("--limit", type=int, default=50)
|
|
395
|
+
|
|
396
|
+
# -- transition --
|
|
397
|
+
p = sub.add_parser("transition", help="Transition issue status")
|
|
398
|
+
p.add_argument("ref")
|
|
399
|
+
p.add_argument("status")
|
|
400
|
+
p.add_argument("comment")
|
|
401
|
+
|
|
402
|
+
# -- assign --
|
|
403
|
+
p = sub.add_parser("assign", help="Assign or unassign an issue")
|
|
404
|
+
p.add_argument("ref")
|
|
405
|
+
p.add_argument("assignee", nargs="?", default=None)
|
|
406
|
+
p.add_argument("--unassign", action="store_true")
|
|
407
|
+
|
|
408
|
+
# -- update --
|
|
409
|
+
p = sub.add_parser("update", help="Update issue fields")
|
|
410
|
+
p.add_argument("ref")
|
|
411
|
+
p.add_argument("--title", default=None)
|
|
412
|
+
p.add_argument("--priority", type=int, default=None)
|
|
413
|
+
p.add_argument("--estimate", type=int, default=None)
|
|
414
|
+
p.add_argument("--assignee", default=None)
|
|
415
|
+
p.add_argument("--state", default=None)
|
|
416
|
+
p.add_argument("--description", default=None)
|
|
417
|
+
p.add_argument("--description-file", default=None)
|
|
418
|
+
p.add_argument("--labels", default=None)
|
|
419
|
+
p.add_argument("--milestone", default=None)
|
|
420
|
+
p.add_argument("--comment", default=None)
|
|
421
|
+
|
|
422
|
+
# -- description --
|
|
423
|
+
p = sub.add_parser("description", help="Update issue description")
|
|
424
|
+
p.add_argument("ref")
|
|
425
|
+
p.add_argument("--text", default=None)
|
|
426
|
+
p.add_argument("--file", default=None)
|
|
427
|
+
|
|
428
|
+
# -- comment --
|
|
429
|
+
p = sub.add_parser("comment", help="Add a comment to an issue")
|
|
430
|
+
p.add_argument("ref")
|
|
431
|
+
p.add_argument("body", nargs="?", default=None)
|
|
432
|
+
|
|
433
|
+
# -- estimate --
|
|
434
|
+
p = sub.add_parser("estimate", help="Set estimate points")
|
|
435
|
+
p.add_argument("ref")
|
|
436
|
+
p.add_argument("points", type=int)
|
|
437
|
+
|
|
438
|
+
# -- priority --
|
|
439
|
+
p = sub.add_parser("priority", help="Set priority level")
|
|
440
|
+
p.add_argument("ref")
|
|
441
|
+
p.add_argument("level", type=int)
|
|
442
|
+
|
|
443
|
+
# -- link --
|
|
444
|
+
p = sub.add_parser("link", help="Link two issues")
|
|
445
|
+
p.add_argument("ref")
|
|
446
|
+
p.add_argument("related_ref")
|
|
447
|
+
p.add_argument("type", choices=["blocks", "related", "duplicate"])
|
|
448
|
+
|
|
449
|
+
# -- assign-milestone --
|
|
450
|
+
p = sub.add_parser("assign-milestone", help="Assign issue to milestone")
|
|
451
|
+
p.add_argument("ref")
|
|
452
|
+
p.add_argument("milestone_id")
|
|
453
|
+
|
|
454
|
+
# -- assign-cycle --
|
|
455
|
+
p = sub.add_parser("assign-cycle", help="Assign issue to cycle")
|
|
456
|
+
p.add_argument("ref")
|
|
457
|
+
p.add_argument("cycle_id", nargs="?", default=None)
|
|
458
|
+
|
|
459
|
+
# -- pr --
|
|
460
|
+
p = sub.add_parser("pr", help="Create pull/merge request with standard template")
|
|
461
|
+
p.add_argument("--issue", default=None, help="Issue ref to link (e.g. FLY-123)")
|
|
462
|
+
p.add_argument("--title", default=None, help="PR title (defaults to issue title or branch name)")
|
|
463
|
+
p.add_argument("--base", default=None, help="Base branch (defaults to main/master)")
|
|
464
|
+
p.add_argument("--draft", action="store_true", help="Create as draft PR")
|
|
465
|
+
p.add_argument("--dry-run", action="store_true", help="Show what would be created without creating")
|
|
466
|
+
|
|
467
|
+
args = parser.parse_args()
|
|
468
|
+
|
|
469
|
+
commands = {
|
|
470
|
+
"create": cmd_create,
|
|
471
|
+
"get": cmd_get,
|
|
472
|
+
"list": cmd_list,
|
|
473
|
+
"transition": cmd_transition,
|
|
474
|
+
"assign": cmd_assign,
|
|
475
|
+
"update": cmd_update,
|
|
476
|
+
"description": cmd_description,
|
|
477
|
+
"comment": cmd_comment,
|
|
478
|
+
"estimate": cmd_estimate,
|
|
479
|
+
"priority": cmd_priority,
|
|
480
|
+
"link": cmd_link,
|
|
481
|
+
"assign-milestone": cmd_assign_milestone,
|
|
482
|
+
"assign-cycle": cmd_assign_cycle,
|
|
483
|
+
"pr": cmd_pr,
|
|
484
|
+
}
|
|
485
|
+
commands[args.command](args)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
if __name__ == "__main__":
|
|
489
|
+
main()
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Project, milestone, and cycle operations dispatcher.
|
|
3
|
+
|
|
4
|
+
All subcommands are cloud-only. The unified client handles tier gating
|
|
5
|
+
via require_cloud().
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python projects.py list-projects [--active] [--all]
|
|
9
|
+
python projects.py create-project --name NAME [--description DESC]
|
|
10
|
+
python projects.py list-milestones [--all]
|
|
11
|
+
python projects.py create-milestone --name NAME [--project ID] [--target-date DATE]
|
|
12
|
+
python projects.py update-milestone MILESTONE_ID [--name] [--target-date] [--description]
|
|
13
|
+
python projects.py delete-milestone MILESTONE_ID
|
|
14
|
+
python projects.py list-cycles [--active]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
22
|
+
from flydocs_api import get_client, output_json, fail
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cmd_list_projects(args: argparse.Namespace) -> None:
|
|
26
|
+
client = get_client()
|
|
27
|
+
result = client.list_projects(active=args.active, show_all=args.show_all)
|
|
28
|
+
output_json(result)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cmd_create_project(args: argparse.Namespace) -> None:
|
|
32
|
+
client = get_client()
|
|
33
|
+
result = client.create_project(name=args.name, description=args.description)
|
|
34
|
+
output_json(result)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cmd_list_milestones(args: argparse.Namespace) -> None:
|
|
38
|
+
client = get_client()
|
|
39
|
+
result = client.list_milestones(show_all=args.show_all)
|
|
40
|
+
output_json(result)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cmd_create_milestone(args: argparse.Namespace) -> None:
|
|
44
|
+
client = get_client()
|
|
45
|
+
# Default project from activeProjects if not explicitly provided
|
|
46
|
+
project = args.project
|
|
47
|
+
if not project and client.is_cloud:
|
|
48
|
+
active = client.config.get("workspace", {}).get("activeProjects", [])
|
|
49
|
+
if active:
|
|
50
|
+
project = active[0]
|
|
51
|
+
result = client.create_milestone(
|
|
52
|
+
name=args.name,
|
|
53
|
+
project=project,
|
|
54
|
+
target_date=args.target_date,
|
|
55
|
+
)
|
|
56
|
+
output_json(result)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_update_milestone(args: argparse.Namespace) -> None:
|
|
60
|
+
client = get_client()
|
|
61
|
+
fields: dict = {}
|
|
62
|
+
if args.name is not None:
|
|
63
|
+
fields["name"] = args.name
|
|
64
|
+
if args.target_date is not None:
|
|
65
|
+
fields["targetDate"] = args.target_date
|
|
66
|
+
if args.description is not None:
|
|
67
|
+
fields["description"] = args.description
|
|
68
|
+
if not fields:
|
|
69
|
+
fail("At least one of --name, --target-date, or --description is required")
|
|
70
|
+
result = client.update_milestone(args.milestone_id, **fields)
|
|
71
|
+
output_json(result)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def cmd_delete_milestone(args: argparse.Namespace) -> None:
|
|
75
|
+
client = get_client()
|
|
76
|
+
result = client.delete_milestone(args.milestone_id)
|
|
77
|
+
output_json(result)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_list_cycles(args: argparse.Namespace) -> None:
|
|
81
|
+
client = get_client()
|
|
82
|
+
result = client.list_cycles(active=args.active)
|
|
83
|
+
output_json(result)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main() -> None:
|
|
87
|
+
parser = argparse.ArgumentParser(description="FlyDocs project operations")
|
|
88
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
89
|
+
|
|
90
|
+
# list-projects
|
|
91
|
+
lp = sub.add_parser("list-projects", help="List projects")
|
|
92
|
+
lp.add_argument("--active", action="store_true", help="Active projects only")
|
|
93
|
+
lp.add_argument("--all", action="store_true", dest="show_all",
|
|
94
|
+
help="Include all projects (bypass label filter)")
|
|
95
|
+
|
|
96
|
+
# create-project
|
|
97
|
+
cp = sub.add_parser("create-project", help="Create a project")
|
|
98
|
+
cp.add_argument("--name", required=True, help="Project name")
|
|
99
|
+
cp.add_argument("--description", default=None, help="Project description")
|
|
100
|
+
|
|
101
|
+
# list-milestones
|
|
102
|
+
lm = sub.add_parser("list-milestones", help="List milestones")
|
|
103
|
+
lm.add_argument("--all", action="store_true", dest="show_all",
|
|
104
|
+
help="Include milestones from all projects")
|
|
105
|
+
|
|
106
|
+
# create-milestone
|
|
107
|
+
cm = sub.add_parser("create-milestone", help="Create a milestone")
|
|
108
|
+
cm.add_argument("--name", required=True, help="Milestone name")
|
|
109
|
+
cm.add_argument("--project", default=None, help="Project ID")
|
|
110
|
+
cm.add_argument("--target-date", default=None, dest="target_date",
|
|
111
|
+
help="Target date (YYYY-MM-DD)")
|
|
112
|
+
|
|
113
|
+
# update-milestone
|
|
114
|
+
um = sub.add_parser("update-milestone", help="Update a milestone")
|
|
115
|
+
um.add_argument("milestone_id", help="Milestone ID")
|
|
116
|
+
um.add_argument("--name", default=None, help="New name")
|
|
117
|
+
um.add_argument("--target-date", default=None, dest="target_date",
|
|
118
|
+
help="New target date (YYYY-MM-DD)")
|
|
119
|
+
um.add_argument("--description", default=None, help="New description")
|
|
120
|
+
|
|
121
|
+
# delete-milestone
|
|
122
|
+
dm = sub.add_parser("delete-milestone", help="Delete a milestone")
|
|
123
|
+
dm.add_argument("milestone_id", help="Milestone ID")
|
|
124
|
+
|
|
125
|
+
# list-cycles
|
|
126
|
+
lc = sub.add_parser("list-cycles", help="List cycles")
|
|
127
|
+
lc.add_argument("--active", action="store_true", help="Active cycles only")
|
|
128
|
+
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
|
|
131
|
+
commands = {
|
|
132
|
+
"list-projects": cmd_list_projects,
|
|
133
|
+
"create-project": cmd_create_project,
|
|
134
|
+
"list-milestones": cmd_list_milestones,
|
|
135
|
+
"create-milestone": cmd_create_milestone,
|
|
136
|
+
"update-milestone": cmd_update_milestone,
|
|
137
|
+
"delete-milestone": cmd_delete_milestone,
|
|
138
|
+
"list-cycles": cmd_list_cycles,
|
|
139
|
+
}
|
|
140
|
+
commands[args.command](args)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
main()
|