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