@flydocs/cli 0.6.0-alpha.3 → 0.6.0-alpha.31
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 +2053 -469
- 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,738 @@
|
|
|
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
|
+
|
|
22
|
+
Enforces required fields to prevent incomplete issues:
|
|
23
|
+
- Description must be non-empty unless --triage is set
|
|
24
|
+
- Title is already required by argparse
|
|
25
|
+
- Type is already required by argparse
|
|
26
|
+
|
|
27
|
+
Auto-resolves from config when not explicitly passed:
|
|
28
|
+
- Category labels from issueLabels.category
|
|
29
|
+
- Repo labels from issueLabels.repo (multi-repo workspaces)
|
|
30
|
+
- Project from workspace.activeProjects
|
|
31
|
+
- Milestone from workspace.defaultMilestoneId
|
|
32
|
+
"""
|
|
33
|
+
# Description resolution: --description-file > stdin > --description > --template
|
|
34
|
+
description = resolve_text_input(file_arg=args.description_file)
|
|
35
|
+
if description is None:
|
|
36
|
+
description = args.description or ""
|
|
37
|
+
|
|
38
|
+
# Template fallback — read type template if no description provided
|
|
39
|
+
if not description.strip() and args.template:
|
|
40
|
+
template_path = Path(f".flydocs/templates/{args.type}.md")
|
|
41
|
+
if template_path.exists():
|
|
42
|
+
try:
|
|
43
|
+
raw = template_path.read_text()
|
|
44
|
+
# Strip agent instruction comments
|
|
45
|
+
import re as _re
|
|
46
|
+
description = _re.sub(r'<!--\s*AGENT:.*?-->\s*\n?', '', raw).strip()
|
|
47
|
+
except OSError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
# Enforce non-empty description — triage bypasses with warning
|
|
51
|
+
if not description.strip():
|
|
52
|
+
if args.triage:
|
|
53
|
+
description = f"[Quick capture — needs refinement via /refine]\n\n{args.title}"
|
|
54
|
+
import sys as _sys
|
|
55
|
+
print(
|
|
56
|
+
"Note: Triage issue created with minimal description. "
|
|
57
|
+
"Run /refine to add full description and AC.",
|
|
58
|
+
file=_sys.stderr,
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
template_path = f".flydocs/templates/{args.type}.md"
|
|
62
|
+
fail(
|
|
63
|
+
"Description is required when creating an issue. "
|
|
64
|
+
"Use --description, --description-file, or pipe to stdin.\n"
|
|
65
|
+
f"Read the template at {template_path} for the expected format, "
|
|
66
|
+
"then populate all sections before creating the issue."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Auto-resolve assignee from me.json
|
|
70
|
+
assignee = args.assignee
|
|
71
|
+
if assignee in ("self", "me"):
|
|
72
|
+
me_file = Path(".flydocs/me.json")
|
|
73
|
+
if me_file.exists():
|
|
74
|
+
try:
|
|
75
|
+
me = json.loads(me_file.read_text())
|
|
76
|
+
assignee = me.get("providerId") or me.get("displayName")
|
|
77
|
+
except (json.JSONDecodeError, OSError):
|
|
78
|
+
fail("Could not read .flydocs/me.json — run: workspace.py get-me")
|
|
79
|
+
else:
|
|
80
|
+
fail("No .flydocs/me.json found — run: workspace.py get-me")
|
|
81
|
+
|
|
82
|
+
client = get_client()
|
|
83
|
+
result = client.create_issue(
|
|
84
|
+
title=args.title,
|
|
85
|
+
issue_type=args.type,
|
|
86
|
+
description=description,
|
|
87
|
+
priority=args.priority,
|
|
88
|
+
estimate=args.estimate,
|
|
89
|
+
assignee=assignee,
|
|
90
|
+
project=args.project,
|
|
91
|
+
milestone=args.milestone,
|
|
92
|
+
triage=args.triage,
|
|
93
|
+
)
|
|
94
|
+
output_json(result)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cmd_get(args: argparse.Namespace) -> None:
|
|
98
|
+
"""Get a single issue by reference."""
|
|
99
|
+
client = get_client()
|
|
100
|
+
result = client.get_issue(args.ref, fields=args.fields)
|
|
101
|
+
output_json(result)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmd_list(args: argparse.Namespace) -> None:
|
|
105
|
+
"""List issues with optional filters."""
|
|
106
|
+
client = get_client()
|
|
107
|
+
result = client.list_issues(
|
|
108
|
+
status=args.status,
|
|
109
|
+
active=args.active,
|
|
110
|
+
project=args.project,
|
|
111
|
+
assignee=args.assignee,
|
|
112
|
+
milestone=args.milestone,
|
|
113
|
+
mine=args.mine,
|
|
114
|
+
show_all=args.show_all,
|
|
115
|
+
limit=args.limit,
|
|
116
|
+
)
|
|
117
|
+
output_json(result)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
VALID_TRANSITIONS: dict[str, set[str]] = {
|
|
121
|
+
"BACKLOG": {"READY", "IMPLEMENTING", "CANCELED"},
|
|
122
|
+
"TRIAGE": {"BACKLOG", "READY", "IMPLEMENTING", "CANCELED"},
|
|
123
|
+
"READY": {"IMPLEMENTING", "CANCELED"},
|
|
124
|
+
"IMPLEMENTING": {"REVIEW", "BLOCKED", "CANCELED"},
|
|
125
|
+
"BLOCKED": {"IMPLEMENTING", "CANCELED"},
|
|
126
|
+
"REVIEW": {"COMPLETE", "TESTING", "IMPLEMENTING", "CANCELED"},
|
|
127
|
+
"TESTING": {"COMPLETE", "IMPLEMENTING", "CANCELED"},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cmd_transition(args: argparse.Namespace) -> None:
|
|
132
|
+
"""Transition an issue to a new status with a comment.
|
|
133
|
+
|
|
134
|
+
Validates:
|
|
135
|
+
- Comment is non-empty (not just whitespace)
|
|
136
|
+
- Transition is valid (from->to) based on current status
|
|
137
|
+
"""
|
|
138
|
+
# Enforce non-empty comment
|
|
139
|
+
if not args.comment.strip():
|
|
140
|
+
fail(
|
|
141
|
+
"Transition comment cannot be empty. Every status transition "
|
|
142
|
+
"requires a meaningful comment describing what changed."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
target = args.status.upper()
|
|
146
|
+
|
|
147
|
+
# Validate transition if we know the current status
|
|
148
|
+
status_file = Path(".flydocs/session/status")
|
|
149
|
+
if status_file.exists():
|
|
150
|
+
try:
|
|
151
|
+
current = status_file.read_text().strip().upper()
|
|
152
|
+
if current in VALID_TRANSITIONS:
|
|
153
|
+
allowed = VALID_TRANSITIONS[current]
|
|
154
|
+
if target not in allowed:
|
|
155
|
+
allowed_str = ", ".join(sorted(allowed))
|
|
156
|
+
fail(
|
|
157
|
+
f"Invalid transition: {current} -> {target}. "
|
|
158
|
+
f"Allowed targets from {current}: {allowed_str}"
|
|
159
|
+
)
|
|
160
|
+
except OSError:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
client = get_client()
|
|
164
|
+
result = client.transition(args.ref, target, args.comment)
|
|
165
|
+
output_json(result)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def cmd_assign(args: argparse.Namespace) -> None:
|
|
169
|
+
"""Assign or unassign an issue."""
|
|
170
|
+
assignee = None if args.unassign else args.assignee
|
|
171
|
+
client = get_client()
|
|
172
|
+
result = client.assign(args.ref, assignee)
|
|
173
|
+
output_json(result)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def cmd_update(args: argparse.Namespace) -> None:
|
|
177
|
+
"""Update one or more fields on an issue."""
|
|
178
|
+
# Description resolution: --description-file > --description
|
|
179
|
+
description = args.description
|
|
180
|
+
if args.description_file:
|
|
181
|
+
path = Path(args.description_file)
|
|
182
|
+
if not path.exists():
|
|
183
|
+
fail(f"File not found: {args.description_file}")
|
|
184
|
+
description = path.read_text()
|
|
185
|
+
|
|
186
|
+
fields: dict = {}
|
|
187
|
+
if args.title is not None:
|
|
188
|
+
fields["title"] = args.title
|
|
189
|
+
if args.priority is not None:
|
|
190
|
+
fields["priority"] = args.priority
|
|
191
|
+
if args.estimate is not None:
|
|
192
|
+
fields["estimate"] = args.estimate
|
|
193
|
+
if args.assignee is not None:
|
|
194
|
+
fields["assignee"] = args.assignee
|
|
195
|
+
if args.state is not None:
|
|
196
|
+
fields["state"] = args.state
|
|
197
|
+
if description is not None:
|
|
198
|
+
fields["description"] = description
|
|
199
|
+
if args.labels is not None:
|
|
200
|
+
fields["labels"] = args.labels
|
|
201
|
+
if args.milestone is not None:
|
|
202
|
+
fields["milestone"] = args.milestone
|
|
203
|
+
if args.comment is not None:
|
|
204
|
+
fields["comment"] = args.comment
|
|
205
|
+
|
|
206
|
+
if not fields:
|
|
207
|
+
fail("No fields to update")
|
|
208
|
+
|
|
209
|
+
client = get_client()
|
|
210
|
+
result = client.update_issue(args.ref, **fields)
|
|
211
|
+
output_json(result)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def cmd_description(args: argparse.Namespace) -> None:
|
|
215
|
+
"""Update an issue's description."""
|
|
216
|
+
# Text resolution: --file > stdin > --text
|
|
217
|
+
text = resolve_text_input(text_arg=args.text, file_arg=args.file)
|
|
218
|
+
if text is None:
|
|
219
|
+
fail("No description text provided (use --text, --file, or pipe to stdin)")
|
|
220
|
+
|
|
221
|
+
client = get_client()
|
|
222
|
+
result = client.update_description(args.ref, text)
|
|
223
|
+
output_json(result)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def cmd_comment(args: argparse.Namespace) -> None:
|
|
227
|
+
"""Add a comment to an issue."""
|
|
228
|
+
body = args.body
|
|
229
|
+
if body is None and not sys.stdin.isatty():
|
|
230
|
+
body = sys.stdin.read().strip()
|
|
231
|
+
if not body:
|
|
232
|
+
fail("No comment body provided (pass as argument or pipe to stdin)")
|
|
233
|
+
|
|
234
|
+
client = get_client()
|
|
235
|
+
result = client.comment(args.ref, body)
|
|
236
|
+
output_json(result)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def cmd_estimate(args: argparse.Namespace) -> None:
|
|
240
|
+
"""Set estimate points on an issue."""
|
|
241
|
+
if args.points < 0:
|
|
242
|
+
fail("Estimate points must be non-negative")
|
|
243
|
+
|
|
244
|
+
client = get_client()
|
|
245
|
+
result = client.estimate(args.ref, args.points)
|
|
246
|
+
output_json(result)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def cmd_priority(args: argparse.Namespace) -> None:
|
|
250
|
+
"""Set priority level on an issue."""
|
|
251
|
+
if args.level < 0 or args.level > 4:
|
|
252
|
+
fail("Priority level must be 0-4")
|
|
253
|
+
|
|
254
|
+
client = get_client()
|
|
255
|
+
result = client.priority(args.ref, args.level)
|
|
256
|
+
output_json(result)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def cmd_link(args: argparse.Namespace) -> None:
|
|
260
|
+
"""Link two issues together."""
|
|
261
|
+
client = get_client()
|
|
262
|
+
result = client.link(args.ref, args.related_ref, args.type)
|
|
263
|
+
output_json(result)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def cmd_assign_milestone(args: argparse.Namespace) -> None:
|
|
267
|
+
"""Assign an issue to a milestone."""
|
|
268
|
+
client = get_client()
|
|
269
|
+
result = client.assign_milestone(args.ref, args.milestone_id)
|
|
270
|
+
output_json(result)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def cmd_assign_cycle(args: argparse.Namespace) -> None:
|
|
274
|
+
"""Assign an issue to a cycle (or remove from current cycle)."""
|
|
275
|
+
client = get_client()
|
|
276
|
+
result = client.assign_cycle(args.ref, args.cycle_id)
|
|
277
|
+
output_json(result)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# PR creation
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
def _detect_platform() -> str:
|
|
285
|
+
"""Detect git hosting platform from remote URL. Returns github|gitlab|bitbucket|unknown."""
|
|
286
|
+
try:
|
|
287
|
+
result = subprocess.run(
|
|
288
|
+
["git", "remote", "get-url", "origin"],
|
|
289
|
+
capture_output=True, text=True, timeout=5,
|
|
290
|
+
)
|
|
291
|
+
url = result.stdout.strip().lower()
|
|
292
|
+
if "github" in url:
|
|
293
|
+
return "github"
|
|
294
|
+
if "gitlab" in url:
|
|
295
|
+
return "gitlab"
|
|
296
|
+
if "bitbucket" in url:
|
|
297
|
+
return "bitbucket"
|
|
298
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
|
299
|
+
pass
|
|
300
|
+
return "unknown"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _get_current_branch() -> str:
|
|
304
|
+
"""Get current git branch name."""
|
|
305
|
+
try:
|
|
306
|
+
result = subprocess.run(
|
|
307
|
+
["git", "branch", "--show-current"],
|
|
308
|
+
capture_output=True, text=True, timeout=5,
|
|
309
|
+
)
|
|
310
|
+
return result.stdout.strip()
|
|
311
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
|
312
|
+
return ""
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _get_default_branch() -> str:
|
|
316
|
+
"""Detect the default branch (main or master)."""
|
|
317
|
+
try:
|
|
318
|
+
result = subprocess.run(
|
|
319
|
+
["git", "rev-parse", "--verify", "refs/heads/main"],
|
|
320
|
+
capture_output=True, timeout=5,
|
|
321
|
+
)
|
|
322
|
+
if result.returncode == 0:
|
|
323
|
+
return "main"
|
|
324
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
|
325
|
+
pass
|
|
326
|
+
return "master"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _build_pr_body(issue_ref: str | None, summary: str | None) -> str:
|
|
330
|
+
"""Build PR body from template, optionally populating from issue data."""
|
|
331
|
+
# Read the PR template
|
|
332
|
+
script_dir = Path(__file__).parent
|
|
333
|
+
template_path = script_dir.parent / "templates" / "pr" / "default.md"
|
|
334
|
+
|
|
335
|
+
if template_path.exists():
|
|
336
|
+
body = template_path.read_text()
|
|
337
|
+
else:
|
|
338
|
+
body = "## Summary\n\n{ISSUE_SUMMARY}\n\n## Changes\n\n- \n\n## Test Plan\n\n- [ ] \n\n## Notes\n\n"
|
|
339
|
+
|
|
340
|
+
# Populate issue context if available
|
|
341
|
+
if issue_ref:
|
|
342
|
+
body = body.replace("{ISSUE_REF}", issue_ref)
|
|
343
|
+
try:
|
|
344
|
+
client = get_client()
|
|
345
|
+
issue = client.get_issue(issue_ref, fields="full")
|
|
346
|
+
if isinstance(issue, dict):
|
|
347
|
+
title = issue.get("title", "")
|
|
348
|
+
description = issue.get("description", "")
|
|
349
|
+
|
|
350
|
+
# Extract AC checkboxes from description
|
|
351
|
+
ac_lines = []
|
|
352
|
+
if description:
|
|
353
|
+
for line in description.splitlines():
|
|
354
|
+
if re.match(r"^\s*-\s*\[[ x]\]", line, re.IGNORECASE):
|
|
355
|
+
ac_lines.append(line)
|
|
356
|
+
|
|
357
|
+
body = body.replace("{ISSUE_SUMMARY}", title)
|
|
358
|
+
body = body.replace(
|
|
359
|
+
"{ACCEPTANCE_CRITERIA}",
|
|
360
|
+
"\n".join(ac_lines) if ac_lines else "See issue for acceptance criteria.",
|
|
361
|
+
)
|
|
362
|
+
except Exception:
|
|
363
|
+
body = body.replace("{ISSUE_SUMMARY}", summary or "")
|
|
364
|
+
body = body.replace("{ACCEPTANCE_CRITERIA}", "See issue for acceptance criteria.")
|
|
365
|
+
else:
|
|
366
|
+
body = body.replace("{ISSUE_REF}", "")
|
|
367
|
+
body = body.replace("{ISSUE_SUMMARY}", summary or "")
|
|
368
|
+
body = body.replace("{ACCEPTANCE_CRITERIA}", "")
|
|
369
|
+
|
|
370
|
+
# Clean up remaining placeholders
|
|
371
|
+
body = body.replace("{CHANGE_1}", "")
|
|
372
|
+
body = body.replace("{CHANGE_2}", "")
|
|
373
|
+
body = body.replace("{TEST_1}", "")
|
|
374
|
+
body = body.replace("{TEST_2}", "")
|
|
375
|
+
body = body.replace("{NOTES}", "")
|
|
376
|
+
|
|
377
|
+
return body
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def cmd_pr(args: argparse.Namespace) -> None:
|
|
381
|
+
"""Create a pull/merge request with standard template."""
|
|
382
|
+
platform = _detect_platform()
|
|
383
|
+
branch = _get_current_branch()
|
|
384
|
+
base = args.base or _get_default_branch()
|
|
385
|
+
|
|
386
|
+
if not branch or branch == base:
|
|
387
|
+
fail(f"Cannot create PR from branch '{branch}' — switch to a feature branch first.")
|
|
388
|
+
|
|
389
|
+
# Build title
|
|
390
|
+
title = args.title
|
|
391
|
+
if not title and args.issue:
|
|
392
|
+
try:
|
|
393
|
+
client = get_client()
|
|
394
|
+
issue = client.get_issue(args.issue, fields="basic")
|
|
395
|
+
if isinstance(issue, dict):
|
|
396
|
+
title = issue.get("title", args.issue)
|
|
397
|
+
except Exception:
|
|
398
|
+
title = args.issue
|
|
399
|
+
if not title:
|
|
400
|
+
title = branch
|
|
401
|
+
|
|
402
|
+
# Build body from template
|
|
403
|
+
body = _build_pr_body(issue_ref=args.issue, summary=title)
|
|
404
|
+
|
|
405
|
+
if args.dry_run:
|
|
406
|
+
output_json({
|
|
407
|
+
"platform": platform,
|
|
408
|
+
"branch": branch,
|
|
409
|
+
"base": base,
|
|
410
|
+
"title": title,
|
|
411
|
+
"body": body,
|
|
412
|
+
})
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
# Create PR via platform CLI
|
|
416
|
+
if platform == "github":
|
|
417
|
+
cmd = ["gh", "pr", "create", "--title", title, "--body", body, "--base", base]
|
|
418
|
+
if args.draft:
|
|
419
|
+
cmd.append("--draft")
|
|
420
|
+
elif platform == "gitlab":
|
|
421
|
+
cmd = ["glab", "mr", "create", "--title", title, "--description", body, "--target-branch", base]
|
|
422
|
+
if args.draft:
|
|
423
|
+
cmd.append("--draft")
|
|
424
|
+
elif platform == "bitbucket":
|
|
425
|
+
fail("Bitbucket PR creation via CLI is not yet supported. Use the web UI or Bitbucket API.")
|
|
426
|
+
else:
|
|
427
|
+
fail(f"Could not detect git platform from remote URL. Detected: {platform}")
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
431
|
+
if result.returncode != 0:
|
|
432
|
+
fail(f"PR creation failed: {result.stderr.strip()}")
|
|
433
|
+
|
|
434
|
+
pr_url = result.stdout.strip()
|
|
435
|
+
|
|
436
|
+
# Post PR link as comment on the issue
|
|
437
|
+
if args.issue:
|
|
438
|
+
try:
|
|
439
|
+
client = get_client()
|
|
440
|
+
client.comment(args.issue, f"PR created: {pr_url}")
|
|
441
|
+
except Exception:
|
|
442
|
+
pass # Non-blocking — PR was created successfully
|
|
443
|
+
|
|
444
|
+
output_json({
|
|
445
|
+
"success": True,
|
|
446
|
+
"platform": platform,
|
|
447
|
+
"url": pr_url,
|
|
448
|
+
"branch": branch,
|
|
449
|
+
"base": base,
|
|
450
|
+
})
|
|
451
|
+
except FileNotFoundError:
|
|
452
|
+
cli = "gh" if platform == "github" else "glab"
|
|
453
|
+
fail(f"{cli} CLI not found. Install it to create PRs from the command line.")
|
|
454
|
+
except subprocess.TimeoutExpired:
|
|
455
|
+
fail("PR creation timed out.")
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
# Audit
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
CHECKBOX_PATTERN = re.compile(r"^\s*-\s*\[[ x]\]", re.MULTILINE | re.IGNORECASE)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def cmd_audit(args: argparse.Namespace) -> None:
|
|
466
|
+
"""Audit issues for workflow compliance.
|
|
467
|
+
|
|
468
|
+
Checks for: missing descriptions, missing labels, missing AC checkboxes,
|
|
469
|
+
unassigned in-progress issues, and issues in wrong states.
|
|
470
|
+
"""
|
|
471
|
+
client = get_client()
|
|
472
|
+
issues_data = client.list_issues(
|
|
473
|
+
status=args.status,
|
|
474
|
+
show_all=True,
|
|
475
|
+
limit=args.limit,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Handle both list and dict response shapes
|
|
479
|
+
issues = issues_data if isinstance(issues_data, list) else issues_data.get("issues", [])
|
|
480
|
+
|
|
481
|
+
findings: list[dict] = []
|
|
482
|
+
|
|
483
|
+
for issue in issues:
|
|
484
|
+
ref = issue.get("identifier", "?")
|
|
485
|
+
title = issue.get("title", "")
|
|
486
|
+
status = issue.get("status", "").upper()
|
|
487
|
+
description = issue.get("description", "") or ""
|
|
488
|
+
assignee = issue.get("assignee")
|
|
489
|
+
labels = issue.get("labels", [])
|
|
490
|
+
priority = issue.get("priority")
|
|
491
|
+
issue_findings: list[str] = []
|
|
492
|
+
|
|
493
|
+
# Missing description
|
|
494
|
+
if not description.strip():
|
|
495
|
+
issue_findings.append("missing_description")
|
|
496
|
+
|
|
497
|
+
# Missing AC checkboxes (for non-idea types in active states)
|
|
498
|
+
if status in ("IMPLEMENTING", "REVIEW", "TESTING", "COMPLETE"):
|
|
499
|
+
if description and not CHECKBOX_PATTERN.search(description):
|
|
500
|
+
issue_findings.append("no_acceptance_criteria")
|
|
501
|
+
|
|
502
|
+
# Unassigned but in progress
|
|
503
|
+
if status in ("IMPLEMENTING", "REVIEW") and not assignee:
|
|
504
|
+
issue_findings.append("unassigned_active")
|
|
505
|
+
|
|
506
|
+
# Missing labels
|
|
507
|
+
if not labels:
|
|
508
|
+
issue_findings.append("no_labels")
|
|
509
|
+
|
|
510
|
+
# Default priority (might be intentional, but flag it)
|
|
511
|
+
if priority == 0 and status not in ("CANCELED",):
|
|
512
|
+
issue_findings.append("no_priority")
|
|
513
|
+
|
|
514
|
+
if issue_findings:
|
|
515
|
+
findings.append({
|
|
516
|
+
"ref": ref,
|
|
517
|
+
"title": title,
|
|
518
|
+
"status": status,
|
|
519
|
+
"findings": issue_findings,
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
output_json({
|
|
523
|
+
"total_checked": len(issues),
|
|
524
|
+
"issues_with_findings": len(findings),
|
|
525
|
+
"findings": findings,
|
|
526
|
+
"checks": [
|
|
527
|
+
"missing_description",
|
|
528
|
+
"no_acceptance_criteria",
|
|
529
|
+
"unassigned_active",
|
|
530
|
+
"no_labels",
|
|
531
|
+
"no_priority",
|
|
532
|
+
],
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def cmd_fix(args: argparse.Namespace) -> None:
|
|
537
|
+
"""Fix missing fields on an issue using config defaults.
|
|
538
|
+
|
|
539
|
+
Auto-applies: category labels from type, project from activeProjects,
|
|
540
|
+
milestone from defaultMilestoneId.
|
|
541
|
+
"""
|
|
542
|
+
client = get_client()
|
|
543
|
+
issue = client.get_issue(args.ref, fields="full")
|
|
544
|
+
if not isinstance(issue, dict):
|
|
545
|
+
fail(f"Could not fetch issue {args.ref}")
|
|
546
|
+
|
|
547
|
+
fixes: dict = {}
|
|
548
|
+
applied: list[str] = []
|
|
549
|
+
|
|
550
|
+
# Fix missing labels (if we can detect type from title/description)
|
|
551
|
+
labels = issue.get("labels", [])
|
|
552
|
+
if not labels and client.is_cloud:
|
|
553
|
+
# Try to detect type from title keywords
|
|
554
|
+
title_lower = issue.get("title", "").lower()
|
|
555
|
+
detected_type = None
|
|
556
|
+
if any(w in title_lower for w in ["bug", "fix", "broken", "error"]):
|
|
557
|
+
detected_type = "bug"
|
|
558
|
+
elif any(w in title_lower for w in ["add", "implement", "create", "new"]):
|
|
559
|
+
detected_type = "feature"
|
|
560
|
+
elif any(w in title_lower for w in ["refactor", "clean", "update", "upgrade", "remove"]):
|
|
561
|
+
detected_type = "chore"
|
|
562
|
+
if detected_type:
|
|
563
|
+
cat_id = client.relay.get_category_label_id(detected_type)
|
|
564
|
+
if cat_id:
|
|
565
|
+
fixes["labels"] = cat_id
|
|
566
|
+
applied.append(f"label:{detected_type}")
|
|
567
|
+
|
|
568
|
+
# Fix missing project
|
|
569
|
+
project = issue.get("project")
|
|
570
|
+
if not project and client.is_cloud:
|
|
571
|
+
active = client.relay.workspace.get("activeProjects", [])
|
|
572
|
+
if active:
|
|
573
|
+
fixes["projectId"] = active[0]
|
|
574
|
+
applied.append("project:activeProjects")
|
|
575
|
+
|
|
576
|
+
# Fix missing milestone
|
|
577
|
+
milestone = issue.get("milestone")
|
|
578
|
+
if not milestone and client.is_cloud:
|
|
579
|
+
default_ms = client.relay.workspace.get("defaultMilestoneId")
|
|
580
|
+
if default_ms:
|
|
581
|
+
fixes["milestone"] = default_ms
|
|
582
|
+
applied.append("milestone:default")
|
|
583
|
+
|
|
584
|
+
if not fixes:
|
|
585
|
+
output_json({"ref": args.ref, "fixed": [], "message": "No fixes needed"})
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
result = client.update_issue(args.ref, **fixes)
|
|
589
|
+
output_json({
|
|
590
|
+
"ref": args.ref,
|
|
591
|
+
"fixed": applied,
|
|
592
|
+
"result": result,
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
# ---------------------------------------------------------------------------
|
|
597
|
+
# Argument parser
|
|
598
|
+
# ---------------------------------------------------------------------------
|
|
599
|
+
|
|
600
|
+
def main() -> None:
|
|
601
|
+
parser = argparse.ArgumentParser(description="FlyDocs issue operations")
|
|
602
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
603
|
+
|
|
604
|
+
# -- create --
|
|
605
|
+
p = sub.add_parser("create", help="Create a new issue")
|
|
606
|
+
p.add_argument("--title", required=True)
|
|
607
|
+
p.add_argument("--type", required=True, choices=["feature", "bug", "chore", "idea"])
|
|
608
|
+
p.add_argument("--description", default=None)
|
|
609
|
+
p.add_argument("--description-file", default=None)
|
|
610
|
+
p.add_argument("--priority", type=int, default=3, choices=[0, 1, 2, 3, 4])
|
|
611
|
+
p.add_argument("--estimate", type=int, default=None, choices=[0, 1, 2, 3, 5])
|
|
612
|
+
p.add_argument("--assignee", default=None, help="Assignee name/ID, or 'self'/'me' for current user")
|
|
613
|
+
p.add_argument("--project", default=None)
|
|
614
|
+
p.add_argument("--milestone", default=None, help="Milestone ID (defaults to workspace.defaultMilestoneId)")
|
|
615
|
+
p.add_argument("--template", action="store_true", help="Read type template as description skeleton")
|
|
616
|
+
p.add_argument("--triage", action="store_true", help="Quick capture — bypasses description enforcement")
|
|
617
|
+
|
|
618
|
+
# -- get --
|
|
619
|
+
p = sub.add_parser("get", help="Get a single issue")
|
|
620
|
+
p.add_argument("ref")
|
|
621
|
+
p.add_argument("--fields", default="full", choices=["basic", "full"])
|
|
622
|
+
|
|
623
|
+
# -- list --
|
|
624
|
+
p = sub.add_parser("list", help="List issues")
|
|
625
|
+
p.add_argument("--status", default=None)
|
|
626
|
+
p.add_argument("--active", action="store_true")
|
|
627
|
+
p.add_argument("--project", default=None)
|
|
628
|
+
p.add_argument("--assignee", default=None)
|
|
629
|
+
p.add_argument("--milestone", default=None)
|
|
630
|
+
p.add_argument("--mine", action="store_true")
|
|
631
|
+
p.add_argument("--all", action="store_true", dest="show_all", help="Bypass product scope cascade")
|
|
632
|
+
p.add_argument("--limit", type=int, default=50)
|
|
633
|
+
|
|
634
|
+
# -- transition --
|
|
635
|
+
p = sub.add_parser("transition", help="Transition issue status")
|
|
636
|
+
p.add_argument("ref")
|
|
637
|
+
p.add_argument("status")
|
|
638
|
+
p.add_argument("comment")
|
|
639
|
+
|
|
640
|
+
# -- assign --
|
|
641
|
+
p = sub.add_parser("assign", help="Assign or unassign an issue")
|
|
642
|
+
p.add_argument("ref")
|
|
643
|
+
p.add_argument("assignee", nargs="?", default=None)
|
|
644
|
+
p.add_argument("--unassign", action="store_true")
|
|
645
|
+
|
|
646
|
+
# -- update --
|
|
647
|
+
p = sub.add_parser("update", help="Update issue fields")
|
|
648
|
+
p.add_argument("ref")
|
|
649
|
+
p.add_argument("--title", default=None)
|
|
650
|
+
p.add_argument("--priority", type=int, default=None)
|
|
651
|
+
p.add_argument("--estimate", type=int, default=None)
|
|
652
|
+
p.add_argument("--assignee", default=None)
|
|
653
|
+
p.add_argument("--state", default=None)
|
|
654
|
+
p.add_argument("--description", default=None)
|
|
655
|
+
p.add_argument("--description-file", default=None)
|
|
656
|
+
p.add_argument("--labels", default=None)
|
|
657
|
+
p.add_argument("--milestone", default=None)
|
|
658
|
+
p.add_argument("--comment", default=None)
|
|
659
|
+
|
|
660
|
+
# -- description --
|
|
661
|
+
p = sub.add_parser("description", help="Update issue description")
|
|
662
|
+
p.add_argument("ref")
|
|
663
|
+
p.add_argument("--text", default=None)
|
|
664
|
+
p.add_argument("--file", default=None)
|
|
665
|
+
|
|
666
|
+
# -- comment --
|
|
667
|
+
p = sub.add_parser("comment", help="Add a comment to an issue")
|
|
668
|
+
p.add_argument("ref")
|
|
669
|
+
p.add_argument("body", nargs="?", default=None)
|
|
670
|
+
|
|
671
|
+
# -- estimate --
|
|
672
|
+
p = sub.add_parser("estimate", help="Set estimate points")
|
|
673
|
+
p.add_argument("ref")
|
|
674
|
+
p.add_argument("points", type=int)
|
|
675
|
+
|
|
676
|
+
# -- priority --
|
|
677
|
+
p = sub.add_parser("priority", help="Set priority level")
|
|
678
|
+
p.add_argument("ref")
|
|
679
|
+
p.add_argument("level", type=int)
|
|
680
|
+
|
|
681
|
+
# -- link --
|
|
682
|
+
p = sub.add_parser("link", help="Link two issues")
|
|
683
|
+
p.add_argument("ref")
|
|
684
|
+
p.add_argument("related_ref")
|
|
685
|
+
p.add_argument("type", choices=["blocks", "related", "duplicate"])
|
|
686
|
+
|
|
687
|
+
# -- assign-milestone --
|
|
688
|
+
p = sub.add_parser("assign-milestone", help="Assign issue to milestone")
|
|
689
|
+
p.add_argument("ref")
|
|
690
|
+
p.add_argument("milestone_id")
|
|
691
|
+
|
|
692
|
+
# -- assign-cycle --
|
|
693
|
+
p = sub.add_parser("assign-cycle", help="Assign issue to cycle")
|
|
694
|
+
p.add_argument("ref")
|
|
695
|
+
p.add_argument("cycle_id", nargs="?", default=None)
|
|
696
|
+
|
|
697
|
+
# -- pr --
|
|
698
|
+
p = sub.add_parser("pr", help="Create pull/merge request with standard template")
|
|
699
|
+
p.add_argument("--issue", default=None, help="Issue ref to link (e.g. FLY-123)")
|
|
700
|
+
p.add_argument("--title", default=None, help="PR title (defaults to issue title or branch name)")
|
|
701
|
+
p.add_argument("--base", default=None, help="Base branch (defaults to main/master)")
|
|
702
|
+
p.add_argument("--draft", action="store_true", help="Create as draft PR")
|
|
703
|
+
p.add_argument("--dry-run", action="store_true", help="Show what would be created without creating")
|
|
704
|
+
|
|
705
|
+
# -- audit --
|
|
706
|
+
p = sub.add_parser("audit", help="Audit issues for workflow compliance")
|
|
707
|
+
p.add_argument("--status", default=None, help="Filter by status (default: all)")
|
|
708
|
+
p.add_argument("--limit", type=int, default=50, help="Max issues to check")
|
|
709
|
+
|
|
710
|
+
# -- fix --
|
|
711
|
+
p = sub.add_parser("fix", help="Fix missing fields on an issue using config defaults")
|
|
712
|
+
p.add_argument("ref")
|
|
713
|
+
|
|
714
|
+
args = parser.parse_args()
|
|
715
|
+
|
|
716
|
+
commands = {
|
|
717
|
+
"create": cmd_create,
|
|
718
|
+
"get": cmd_get,
|
|
719
|
+
"list": cmd_list,
|
|
720
|
+
"transition": cmd_transition,
|
|
721
|
+
"assign": cmd_assign,
|
|
722
|
+
"update": cmd_update,
|
|
723
|
+
"description": cmd_description,
|
|
724
|
+
"comment": cmd_comment,
|
|
725
|
+
"estimate": cmd_estimate,
|
|
726
|
+
"priority": cmd_priority,
|
|
727
|
+
"link": cmd_link,
|
|
728
|
+
"assign-milestone": cmd_assign_milestone,
|
|
729
|
+
"assign-cycle": cmd_assign_cycle,
|
|
730
|
+
"pr": cmd_pr,
|
|
731
|
+
"audit": cmd_audit,
|
|
732
|
+
"fix": cmd_fix,
|
|
733
|
+
}
|
|
734
|
+
commands[args.command](args)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
if __name__ == "__main__":
|
|
738
|
+
main()
|