@flydocs/cli 0.5.0-beta.0

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 (134) hide show
  1. package/README.md +96 -0
  2. package/dist/cli.js +2666 -0
  3. package/package.json +32 -0
  4. package/template/.claude/CLAUDE.md +90 -0
  5. package/template/.claude/agents/README.md +19 -0
  6. package/template/.claude/agents/implementation-agent.md +29 -0
  7. package/template/.claude/agents/pm-agent.md +29 -0
  8. package/template/.claude/agents/research-agent.md +25 -0
  9. package/template/.claude/agents/review-agent.md +29 -0
  10. package/template/.claude/commands/activate.md +10 -0
  11. package/template/.claude/commands/attach.md +9 -0
  12. package/template/.claude/commands/block.md +10 -0
  13. package/template/.claude/commands/capture.md +10 -0
  14. package/template/.claude/commands/close.md +10 -0
  15. package/template/.claude/commands/flydocs-setup.md +598 -0
  16. package/template/.claude/commands/flydocs-update.md +27 -0
  17. package/template/.claude/commands/implement.md +10 -0
  18. package/template/.claude/commands/new-project.md +11 -0
  19. package/template/.claude/commands/project-update.md +10 -0
  20. package/template/.claude/commands/refine.md +10 -0
  21. package/template/.claude/commands/review.md +10 -0
  22. package/template/.claude/commands/start-session.md +10 -0
  23. package/template/.claude/commands/status.md +10 -0
  24. package/template/.claude/commands/validate.md +10 -0
  25. package/template/.claude/commands/wrap-session.md +10 -0
  26. package/template/.claude/settings.json +49 -0
  27. package/template/.claude/skills/README.md +293 -0
  28. package/template/.claude/skills/flydocs-cloud/SKILL.md +96 -0
  29. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +50 -0
  30. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +38 -0
  31. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +44 -0
  32. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +44 -0
  33. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +39 -0
  34. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +100 -0
  35. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +46 -0
  36. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +40 -0
  37. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +38 -0
  38. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +277 -0
  39. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +77 -0
  40. package/template/.claude/skills/flydocs-cloud/scripts/link.py +47 -0
  41. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +35 -0
  42. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +105 -0
  43. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +40 -0
  44. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +45 -0
  45. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +38 -0
  46. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +59 -0
  47. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +67 -0
  48. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +47 -0
  49. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +111 -0
  50. package/template/.claude/skills/flydocs-context-graph/SKILL.md +87 -0
  51. package/template/.claude/skills/flydocs-context-graph/schema.md +78 -0
  52. package/template/.claude/skills/flydocs-context-graph/scripts/graph_build.py +299 -0
  53. package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +338 -0
  54. package/template/.claude/skills/flydocs-context-graph/scripts/graph_query.py +191 -0
  55. package/template/.claude/skills/flydocs-context-graph/scripts/graph_session.py +161 -0
  56. package/template/.claude/skills/flydocs-context-graph/scripts/graph_update.py +194 -0
  57. package/template/.claude/skills/flydocs-context-graph/scripts/graph_utils.py +118 -0
  58. package/template/.claude/skills/flydocs-estimates/SKILL.md +384 -0
  59. package/template/.claude/skills/flydocs-estimates/references/provider-costs.md +152 -0
  60. package/template/.claude/skills/flydocs-figma/SKILL.md +377 -0
  61. package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +108 -0
  62. package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +112 -0
  63. package/template/.claude/skills/flydocs-local/SKILL.md +103 -0
  64. package/template/.claude/skills/flydocs-local/cursor-rule.mdc +43 -0
  65. package/template/.claude/skills/flydocs-local/scripts/assign.py +20 -0
  66. package/template/.claude/skills/flydocs-local/scripts/comment.py +27 -0
  67. package/template/.claude/skills/flydocs-local/scripts/create_issue.py +44 -0
  68. package/template/.claude/skills/flydocs-local/scripts/estimate.py +37 -0
  69. package/template/.claude/skills/flydocs-local/scripts/flydocs_api.py +272 -0
  70. package/template/.claude/skills/flydocs-local/scripts/get_issue.py +20 -0
  71. package/template/.claude/skills/flydocs-local/scripts/link.py +41 -0
  72. package/template/.claude/skills/flydocs-local/scripts/list_issues.py +34 -0
  73. package/template/.claude/skills/flydocs-local/scripts/priority.py +37 -0
  74. package/template/.claude/skills/flydocs-local/scripts/project_update.py +67 -0
  75. package/template/.claude/skills/flydocs-local/scripts/status_summary.py +16 -0
  76. package/template/.claude/skills/flydocs-local/scripts/transition.py +24 -0
  77. package/template/.claude/skills/flydocs-local/scripts/update_description.py +35 -0
  78. package/template/.claude/skills/flydocs-local/scripts/update_issue.py +84 -0
  79. package/template/.claude/skills/flydocs-workflow/SKILL.md +85 -0
  80. package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +53 -0
  81. package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +131 -0
  82. package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +76 -0
  83. package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +28 -0
  84. package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +50 -0
  85. package/template/.claude/skills/flydocs-workflow/session.md +128 -0
  86. package/template/.claude/skills/flydocs-workflow/stages/activate.md +46 -0
  87. package/template/.claude/skills/flydocs-workflow/stages/capture.md +50 -0
  88. package/template/.claude/skills/flydocs-workflow/stages/close.md +32 -0
  89. package/template/.claude/skills/flydocs-workflow/stages/implement.md +124 -0
  90. package/template/.claude/skills/flydocs-workflow/stages/refine.md +51 -0
  91. package/template/.claude/skills/flydocs-workflow/stages/review.md +86 -0
  92. package/template/.claude/skills/flydocs-workflow/stages/validate.md +90 -0
  93. package/template/.claude/skills/flydocs-workflow/templates/bug.md +95 -0
  94. package/template/.claude/skills/flydocs-workflow/templates/chore.md +75 -0
  95. package/template/.claude/skills/flydocs-workflow/templates/feature.md +93 -0
  96. package/template/.claude/skills/flydocs-workflow/templates/idea.md +84 -0
  97. package/template/.cursor/agents/implementation-agent.md +28 -0
  98. package/template/.cursor/agents/pm-agent.md +27 -0
  99. package/template/.cursor/agents/research-agent.md +23 -0
  100. package/template/.cursor/agents/review-agent.md +27 -0
  101. package/template/.cursor/hooks.json +29 -0
  102. package/template/.cursor/mcp.json +16 -0
  103. package/template/.env.example +44 -0
  104. package/template/.flydocs/config.json +104 -0
  105. package/template/.flydocs/hooks/auto-approve.py +71 -0
  106. package/template/.flydocs/hooks/post-edit.py +72 -0
  107. package/template/.flydocs/hooks/prefer-scripts.py +89 -0
  108. package/template/.flydocs/hooks/prompt-submit.py +277 -0
  109. package/template/.flydocs/scripts/generate_manifest.py +287 -0
  110. package/template/.flydocs/scripts/skill_manager.py +541 -0
  111. package/template/.flydocs/templates/README.md +46 -0
  112. package/template/.flydocs/templates/bug.md +166 -0
  113. package/template/.flydocs/templates/chore.md +110 -0
  114. package/template/.flydocs/templates/design-system/README.md +27 -0
  115. package/template/.flydocs/templates/design-system/component-patterns.md +92 -0
  116. package/template/.flydocs/templates/design-system/token-mapping.md +168 -0
  117. package/template/.flydocs/templates/feature.md +173 -0
  118. package/template/.flydocs/templates/idea.md +122 -0
  119. package/template/.flydocs/templates/instructions.md +228 -0
  120. package/template/.flydocs/templates/quick-capture.md +35 -0
  121. package/template/.flydocs/templates/scripts/check-design-system.template.mjs +179 -0
  122. package/template/.flydocs/version +1 -0
  123. package/template/AGENTS.md +95 -0
  124. package/template/CHANGELOG.md +271 -0
  125. package/template/flydocs/README.md +186 -0
  126. package/template/flydocs/context/project.md +51 -0
  127. package/template/flydocs/design-system/README.md +126 -0
  128. package/template/flydocs/design-system/component-patterns.md +173 -0
  129. package/template/flydocs/design-system/token-mapping.md +114 -0
  130. package/template/flydocs/knowledge/INDEX.md +100 -0
  131. package/template/flydocs/knowledge/README.md +62 -0
  132. package/template/flydocs/knowledge/product/personas.md +79 -0
  133. package/template/flydocs/knowledge/product/user-flows.md +88 -0
  134. package/template/manifest.json +221 -0
@@ -0,0 +1,43 @@
1
+ ---
2
+ description: File-based issue management — local mechanism for FlyDocs
3
+ alwaysApply: true
4
+ ---
5
+
6
+ <!-- Condensed from SKILL.md — update both when changing patterns -->
7
+
8
+ # FlyDocs Local Mechanism
9
+
10
+ Issues stored as markdown files with YAML frontmatter. No accounts, no API keys, no network.
11
+
12
+ ## Script Contract
13
+
14
+ All scripts: `python3 .claude/skills/flydocs-local/scripts/<script>`
15
+
16
+ | Script | Key Arguments |
17
+ |--------|---------------|
18
+ | `create_issue.py` | `--title "..." --type feature [--priority 0-4] [--estimate 1-5] [--assignee STR] [--triage]` |
19
+ | `transition.py` | `<ref> <STATUS> "<comment>"` |
20
+ | `comment.py` | `<ref> "<comment>"` |
21
+ | `list_issues.py` | `[--status STATUS] [--mine] [--limit N]` |
22
+ | `get_issue.py` | `<ref>` |
23
+ | `assign.py` | `<ref> <assignee>` |
24
+ | `update_description.py` | `<ref> --text "..." \| --file PATH` |
25
+ | `status_summary.py` | `[--project ID]` |
26
+
27
+ ## Status Values
28
+
29
+ `BACKLOG`, `READY`, `IMPLEMENTING`, `BLOCKED`, `REVIEW`, `TESTING`, `COMPLETE`, `ARCHIVED`, `CANCELED`, `DUPLICATE`
30
+
31
+ ## Storage Layout
32
+
33
+ ```
34
+ flydocs/issues/
35
+ backlog/ implementing/ review/
36
+ ready/ testing/ done/
37
+ ```
38
+
39
+ Issues: `FD-XXX-slug.md` with YAML frontmatter. IDs auto-increment via `.flydocs/issues/.counter`.
40
+
41
+ ## Error Handling
42
+
43
+ Exit 0 = success (JSON on stdout). Exit 1 = error (message on stderr).
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ """Assign an issue to a person."""
3
+
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from flydocs_api import assign_issue
10
+
11
+ if len(sys.argv) < 3:
12
+ print("Usage: assign.py <ref> <assignee>", file=sys.stderr)
13
+ sys.exit(1)
14
+
15
+ try:
16
+ result = assign_issue(sys.argv[1], sys.argv[2])
17
+ print(json.dumps(result))
18
+ except Exception as e:
19
+ print(str(e), file=sys.stderr)
20
+ sys.exit(1)
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ """Add a comment to an issue."""
3
+
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from flydocs_api import add_comment
10
+
11
+ if len(sys.argv) < 2:
12
+ print("Usage: comment.py <ref> [<comment>] (or pipe via stdin)", file=sys.stderr)
13
+ sys.exit(1)
14
+
15
+ try:
16
+ ref = sys.argv[1]
17
+ body = sys.argv[2] if len(sys.argv) > 2 else ""
18
+ if not body and not sys.stdin.isatty():
19
+ body = sys.stdin.read().strip()
20
+ if not body:
21
+ print("Provide comment as argument or pipe via stdin", file=sys.stderr)
22
+ sys.exit(1)
23
+ result = add_comment(ref, body)
24
+ print(json.dumps(result))
25
+ except Exception as e:
26
+ print(str(e), file=sys.stderr)
27
+ sys.exit(1)
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env python3
2
+ """Create a new local issue."""
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).parent))
10
+ from flydocs_api import create_issue, ISSUE_TYPES
11
+
12
+ parser = argparse.ArgumentParser(description="Create a new issue")
13
+ parser.add_argument("--title", required=True)
14
+ parser.add_argument("--type", required=True, choices=sorted(ISSUE_TYPES), dest="issue_type")
15
+ parser.add_argument("--description", default="")
16
+ parser.add_argument("--description-file", default="", dest="description_file")
17
+ parser.add_argument("--priority", type=int, default=3, choices=range(5))
18
+ parser.add_argument("--estimate", type=int, default=0, choices=range(6))
19
+ parser.add_argument("--assignee", default="")
20
+ parser.add_argument("--triage", action="store_true")
21
+
22
+ try:
23
+ args = parser.parse_args()
24
+
25
+ # Resolve description: --description-file > stdin > --description
26
+ description = args.description
27
+ if args.description_file:
28
+ description = Path(args.description_file).read_text()
29
+ elif not description and not sys.stdin.isatty():
30
+ description = sys.stdin.read().strip()
31
+
32
+ result = create_issue(
33
+ title=args.title,
34
+ issue_type=args.issue_type,
35
+ description=description,
36
+ priority=args.priority,
37
+ estimate=args.estimate,
38
+ assignee=args.assignee,
39
+ triage=args.triage,
40
+ )
41
+ print(json.dumps(result))
42
+ except Exception as e:
43
+ print(str(e), file=sys.stderr)
44
+ sys.exit(1)
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+ """Set estimate on an issue."""
3
+
4
+ import json
5
+ import sys
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).parent))
10
+ from flydocs_api import _find_issue, _parse_issue, _write_issue
11
+
12
+ if len(sys.argv) < 3:
13
+ print("Usage: estimate.py <ref> <1-5>", file=sys.stderr)
14
+ sys.exit(1)
15
+
16
+ ref = sys.argv[1]
17
+ try:
18
+ estimate = int(sys.argv[2])
19
+ if estimate < 1 or estimate > 5:
20
+ raise ValueError
21
+ except ValueError:
22
+ print("Estimate must be a number (1-5)", file=sys.stderr)
23
+ sys.exit(1)
24
+
25
+ try:
26
+ filepath = _find_issue(ref)
27
+ data = _parse_issue(filepath)
28
+ data["estimate"] = estimate
29
+ data["updated"] = datetime.now().strftime("%Y-%m-%d")
30
+
31
+ fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
32
+ _write_issue(filepath, fm, data["description"], data["comments"])
33
+
34
+ print(json.dumps({"success": True, "issue": ref, "estimate": estimate}))
35
+ except Exception as e:
36
+ print(str(e), file=sys.stderr)
37
+ sys.exit(1)
@@ -0,0 +1,272 @@
1
+ """FlyDocs Local API — filesystem-based issue management."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ # Status directories map to workflow states
10
+ STATUSES = {
11
+ "BACKLOG": "backlog",
12
+ "READY": "ready",
13
+ "IMPLEMENTING": "implementing",
14
+ "BLOCKED": "blocked",
15
+ "REVIEW": "review",
16
+ "TESTING": "testing",
17
+ "COMPLETE": "done",
18
+ "ARCHIVED": "done",
19
+ "CANCELED": "done",
20
+ "DUPLICATE": "done",
21
+ }
22
+
23
+ ISSUE_TYPES = {"feature", "bug", "chore", "idea"}
24
+
25
+
26
+ def _issues_root() -> Path:
27
+ """Find flydocs/issues/ relative to repo root."""
28
+ cwd = Path.cwd()
29
+ # Walk up to find .flydocs/config.json
30
+ for parent in [cwd, *cwd.parents]:
31
+ if (parent / ".flydocs" / "config.json").exists():
32
+ root = parent / "flydocs" / "issues"
33
+ root.mkdir(parents=True, exist_ok=True)
34
+ return root
35
+ raise RuntimeError("Not in a FlyDocs project (no .flydocs/config.json found)")
36
+
37
+
38
+ def _ensure_dirs() -> Path:
39
+ """Ensure all status directories exist."""
40
+ root = _issues_root()
41
+ for dirname in set(STATUSES.values()):
42
+ (root / dirname).mkdir(exist_ok=True)
43
+ return root
44
+
45
+
46
+ def _counter_path() -> Path:
47
+ return _issues_root().parent.parent / ".flydocs" / "issues.counter"
48
+
49
+
50
+ def _next_id() -> str:
51
+ """Auto-increment and return next FD-XXX identifier."""
52
+ path = _counter_path()
53
+ path.parent.mkdir(parents=True, exist_ok=True)
54
+ current = int(path.read_text().strip()) if path.exists() else 0
55
+ next_num = current + 1
56
+ path.write_text(str(next_num))
57
+ return f"FD-{next_num:03d}"
58
+
59
+
60
+ def _slugify(title: str) -> str:
61
+ slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
62
+ return slug[:50]
63
+
64
+
65
+ def _parse_issue(filepath: Path) -> dict:
66
+ """Parse a markdown issue file into frontmatter dict + body + comments."""
67
+ text = filepath.read_text()
68
+ if not text.startswith("---"):
69
+ raise ValueError(f"Invalid issue file: {filepath}")
70
+ _, fm_raw, *rest = text.split("---", 2)
71
+ body_and_comments = rest[0] if rest else ""
72
+
73
+ # Parse YAML frontmatter manually (avoid pyyaml dependency)
74
+ frontmatter = {}
75
+ for line in fm_raw.strip().splitlines():
76
+ if ":" in line:
77
+ key, val = line.split(":", 1)
78
+ val = val.strip()
79
+ # Handle numeric values
80
+ if val.isdigit():
81
+ val = int(val)
82
+ frontmatter[key.strip()] = val
83
+
84
+ # Split body and comments
85
+ parts = body_and_comments.split("\n---\n## Comments", 1)
86
+ description = parts[0].strip()
87
+ comments_raw = parts[1].strip() if len(parts) > 1 else ""
88
+
89
+ comments = []
90
+ if comments_raw:
91
+ for block in re.split(r"\n(?=\*\*)", comments_raw):
92
+ block = block.strip()
93
+ if block:
94
+ comments.append(block)
95
+
96
+ return {**frontmatter, "description": description, "comments": comments, "_path": filepath}
97
+
98
+
99
+ def _find_issue(ref: str) -> Path:
100
+ """Find an issue file by its identifier (e.g., FD-001) across all directories."""
101
+ root = _issues_root()
102
+ prefix = ref.upper() + "-"
103
+ for dirname in set(STATUSES.values()):
104
+ dirpath = root / dirname
105
+ if not dirpath.exists():
106
+ continue
107
+ for f in dirpath.iterdir():
108
+ if f.name.upper().startswith(prefix) and f.suffix == ".md":
109
+ return f
110
+ raise FileNotFoundError(f"Issue {ref} not found")
111
+
112
+
113
+ def _status_from_path(filepath: Path) -> str:
114
+ """Get the FlyDocs status from a file's parent directory."""
115
+ dirname = filepath.parent.name
116
+ for status, dname in STATUSES.items():
117
+ if dname == dirname:
118
+ return status
119
+ return "BACKLOG"
120
+
121
+
122
+ def _write_issue(filepath: Path, frontmatter: dict, description: str, comments: list[str]) -> None:
123
+ """Write an issue file with frontmatter, description, and comments."""
124
+ fm_lines = "\n".join(f"{k}: {v}" for k, v in frontmatter.items() if not k.startswith("_"))
125
+ parts = [f"---\n{fm_lines}\n---\n\n{description}"]
126
+ if comments:
127
+ parts.append("\n---\n## Comments\n\n" + "\n\n".join(comments))
128
+ filepath.write_text("".join(parts) + "\n")
129
+
130
+
131
+ def create_issue(title: str, issue_type: str, description: str = "",
132
+ priority: int = 3, estimate: int = 0,
133
+ assignee: str = "", triage: bool = False) -> dict:
134
+ root = _ensure_dirs()
135
+ identifier = _next_id()
136
+ slug = _slugify(title)
137
+ filename = f"{identifier}-{slug}.md"
138
+ now = datetime.now().strftime("%Y-%m-%d")
139
+ filepath = root / "backlog" / filename
140
+
141
+ frontmatter = {
142
+ "id": identifier,
143
+ "title": title,
144
+ "type": issue_type,
145
+ "priority": priority,
146
+ "estimate": estimate,
147
+ "assignee": assignee,
148
+ "created": now,
149
+ "updated": now,
150
+ }
151
+ if triage:
152
+ frontmatter["triage"] = "true"
153
+
154
+ _write_issue(filepath, frontmatter, description or f"## Context\n\n{title}", [])
155
+ return {"id": identifier, "identifier": identifier, "title": title}
156
+
157
+
158
+ def transition(ref: str, new_status: str, comment: str) -> dict:
159
+ filepath = _find_issue(ref)
160
+ data = _parse_issue(filepath)
161
+ prev_status = _status_from_path(filepath)
162
+
163
+ if new_status not in STATUSES:
164
+ raise ValueError(f"Invalid status: {new_status}. Valid: {', '.join(STATUSES.keys())}")
165
+
166
+ target_dir = _issues_root() / STATUSES[new_status]
167
+ target_dir.mkdir(exist_ok=True)
168
+ new_path = target_dir / filepath.name
169
+
170
+ # Append transition comment
171
+ now = datetime.now().strftime("%Y-%m-%d %H:%M")
172
+ data["comments"].append(f"**{new_status}** — {comment}\n_{now}_")
173
+ data["updated"] = datetime.now().strftime("%Y-%m-%d")
174
+
175
+ fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
176
+ _write_issue(new_path, fm, data["description"], data["comments"])
177
+ if new_path != filepath:
178
+ filepath.unlink()
179
+
180
+ return {"success": True, "issue": ref, "previousStatus": prev_status, "newStatus": new_status}
181
+
182
+
183
+ def add_comment(ref: str, comment: str) -> dict:
184
+ filepath = _find_issue(ref)
185
+ data = _parse_issue(filepath)
186
+ now = datetime.now().strftime("%Y-%m-%d %H:%M")
187
+ data["comments"].append(f"{comment}\n_{now}_")
188
+ data["updated"] = datetime.now().strftime("%Y-%m-%d")
189
+
190
+ fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
191
+ _write_issue(filepath, fm, data["description"], data["comments"])
192
+ return {"success": True, "commentId": len(data["comments"])}
193
+
194
+
195
+ def list_issues(status: str = "", assignee: str = "", limit: int = 50) -> list[dict]:
196
+ root = _issues_root()
197
+ results = []
198
+ dirs_to_scan = [STATUSES[status]] if status and status in STATUSES else list(set(STATUSES.values()))
199
+
200
+ for dirname in dirs_to_scan:
201
+ dirpath = root / dirname
202
+ if not dirpath.exists():
203
+ continue
204
+ for f in sorted(dirpath.iterdir()):
205
+ if f.suffix != ".md":
206
+ continue
207
+ data = _parse_issue(f)
208
+ if assignee and data.get("assignee", "") != assignee:
209
+ continue
210
+ results.append({
211
+ "id": data.get("id", ""),
212
+ "identifier": data.get("id", ""),
213
+ "title": data.get("title", ""),
214
+ "status": _status_from_path(f),
215
+ "assignee": data.get("assignee", ""),
216
+ "priority": data.get("priority", 3),
217
+ })
218
+ if len(results) >= limit:
219
+ return results
220
+ return results
221
+
222
+
223
+ def get_issue(ref: str) -> dict:
224
+ filepath = _find_issue(ref)
225
+ data = _parse_issue(filepath)
226
+ return {
227
+ "id": data.get("id", ""),
228
+ "identifier": data.get("id", ""),
229
+ "title": data.get("title", ""),
230
+ "description": data.get("description", ""),
231
+ "status": _status_from_path(filepath),
232
+ "assignee": data.get("assignee", ""),
233
+ "priority": data.get("priority", 3),
234
+ "estimate": data.get("estimate", 0),
235
+ "comments": data.get("comments", []),
236
+ }
237
+
238
+
239
+ def assign_issue(ref: str, assignee: str) -> dict:
240
+ filepath = _find_issue(ref)
241
+ data = _parse_issue(filepath)
242
+ data["assignee"] = assignee
243
+ data["updated"] = datetime.now().strftime("%Y-%m-%d")
244
+
245
+ fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
246
+ _write_issue(filepath, fm, data["description"], data["comments"])
247
+ return {"success": True, "issue": ref, "assignee": assignee}
248
+
249
+
250
+ def update_description(ref: str, text: str) -> dict:
251
+ filepath = _find_issue(ref)
252
+ data = _parse_issue(filepath)
253
+ data["updated"] = datetime.now().strftime("%Y-%m-%d")
254
+
255
+ fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
256
+ _write_issue(filepath, fm, text, data["comments"])
257
+ return {"success": True, "issue": ref}
258
+
259
+
260
+ def status_summary() -> dict:
261
+ root = _issues_root()
262
+ counts = {}
263
+ total = 0
264
+ for status, dirname in STATUSES.items():
265
+ dirpath = root / dirname
266
+ if not dirpath.exists():
267
+ continue
268
+ count = len([f for f in dirpath.iterdir() if f.suffix == ".md"])
269
+ if count > 0:
270
+ counts[status] = counts.get(status, 0) + count
271
+ total += count
272
+ return {"statuses": counts, "total": total}
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ """Get full details for an issue."""
3
+
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from flydocs_api import get_issue
10
+
11
+ if len(sys.argv) < 2:
12
+ print("Usage: get_issue.py <ref>", file=sys.stderr)
13
+ sys.exit(1)
14
+
15
+ try:
16
+ result = get_issue(sys.argv[1])
17
+ print(json.dumps(result))
18
+ except Exception as e:
19
+ print(str(e), file=sys.stderr)
20
+ sys.exit(1)
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python3
2
+ """Create a relationship between two issues."""
3
+
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from flydocs_api import _find_issue, add_comment
10
+
11
+ LINK_TYPES = {"blocks", "related", "duplicate"}
12
+
13
+ if len(sys.argv) < 4:
14
+ print(f"Usage: link.py <ref> <related_ref> <type>\nTypes: {', '.join(sorted(LINK_TYPES))}", file=sys.stderr)
15
+ sys.exit(1)
16
+
17
+ ref, related_ref, link_type = sys.argv[1], sys.argv[2], sys.argv[3].lower()
18
+ if link_type not in LINK_TYPES:
19
+ print(f"Invalid type: {link_type}. Valid: {', '.join(sorted(LINK_TYPES))}", file=sys.stderr)
20
+ sys.exit(1)
21
+
22
+ try:
23
+ # Verify both issues exist
24
+ _find_issue(ref)
25
+ _find_issue(related_ref)
26
+
27
+ # Record the link as comments on both issues
28
+ if link_type == "blocks":
29
+ add_comment(ref, f"**Link**: blocks {related_ref}")
30
+ add_comment(related_ref, f"**Link**: blocked by {ref}")
31
+ elif link_type == "duplicate":
32
+ add_comment(ref, f"**Link**: duplicate of {related_ref}")
33
+ add_comment(related_ref, f"**Link**: duplicate of {ref}")
34
+ else:
35
+ add_comment(ref, f"**Link**: related to {related_ref}")
36
+ add_comment(related_ref, f"**Link**: related to {ref}")
37
+
38
+ print(json.dumps({"success": True, "type": link_type}))
39
+ except Exception as e:
40
+ print(str(e), file=sys.stderr)
41
+ sys.exit(1)
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ """List issues with optional filters."""
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ sys.path.insert(0, str(Path(__file__).parent))
11
+ from flydocs_api import list_issues
12
+
13
+ TERMINAL_STATUSES = {"COMPLETE", "ARCHIVED", "CANCELED", "DUPLICATE"}
14
+
15
+ parser = argparse.ArgumentParser(description="List issues")
16
+ parser.add_argument("--status", default="")
17
+ parser.add_argument("--active", action="store_true",
18
+ help="All non-terminal states (excludes Done, Archived, Canceled, Duplicate)")
19
+ parser.add_argument("--assignee", default="")
20
+ parser.add_argument("--mine", action="store_true")
21
+ parser.add_argument("--limit", type=int, default=50)
22
+
23
+ try:
24
+ args = parser.parse_args()
25
+ assignee = args.assignee
26
+ if args.mine:
27
+ assignee = os.environ.get("USER", os.environ.get("USERNAME", ""))
28
+ result = list_issues(status=args.status.upper() if args.status else "", assignee=assignee, limit=args.limit)
29
+ if args.active:
30
+ result = [r for r in result if r.get("status") not in TERMINAL_STATUSES]
31
+ print(json.dumps(result))
32
+ except Exception as e:
33
+ print(str(e), file=sys.stderr)
34
+ sys.exit(1)
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+ """Set priority on an issue."""
3
+
4
+ import json
5
+ import sys
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).parent))
10
+ from flydocs_api import _find_issue, _parse_issue, _write_issue
11
+
12
+ if len(sys.argv) < 3:
13
+ print("Usage: priority.py <ref> <0-4>", file=sys.stderr)
14
+ sys.exit(1)
15
+
16
+ ref = sys.argv[1]
17
+ try:
18
+ priority = int(sys.argv[2])
19
+ if priority < 0 or priority > 4:
20
+ raise ValueError
21
+ except ValueError:
22
+ print("Priority must be a number (0-4)", file=sys.stderr)
23
+ sys.exit(1)
24
+
25
+ try:
26
+ filepath = _find_issue(ref)
27
+ data = _parse_issue(filepath)
28
+ data["priority"] = priority
29
+ data["updated"] = datetime.now().strftime("%Y-%m-%d")
30
+
31
+ fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
32
+ _write_issue(filepath, fm, data["description"], data["comments"])
33
+
34
+ print(json.dumps({"success": True, "issue": ref, "priority": priority}))
35
+ except Exception as e:
36
+ print(str(e), file=sys.stderr)
37
+ sys.exit(1)
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """Post a project health update (local: saves to flydocs/updates/)."""
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ HEALTH_VALUES = {"onTrack", "atRisk", "offTrack"}
11
+
12
+ parser = argparse.ArgumentParser(description="Post project update")
13
+ parser.add_argument("--health", required=True, choices=sorted(HEALTH_VALUES))
14
+ parser.add_argument("--body", default="")
15
+ parser.add_argument("--body-file", default="")
16
+ parser.add_argument("--project", default=None)
17
+
18
+ try:
19
+ args = parser.parse_args()
20
+
21
+ body = args.body
22
+ if args.body_file:
23
+ p = Path(args.body_file)
24
+ if not p.exists():
25
+ print(f"File not found: {args.body_file}", file=sys.stderr)
26
+ sys.exit(1)
27
+ body = p.read_text().strip()
28
+ if not body and not sys.stdin.isatty():
29
+ body = sys.stdin.read().strip()
30
+ if not body:
31
+ print("Provide --body, --body-file, or pipe via stdin", file=sys.stderr)
32
+ sys.exit(1)
33
+
34
+ # Find project root
35
+ cwd = Path.cwd()
36
+ root = None
37
+ for parent in [cwd, *cwd.parents]:
38
+ if (parent / ".flydocs" / "config.json").exists():
39
+ root = parent
40
+ break
41
+ if not root:
42
+ print("Not in a FlyDocs project", file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+ # Save update to flydocs/updates/
46
+ updates_dir = root / "flydocs" / "updates"
47
+ updates_dir.mkdir(parents=True, exist_ok=True)
48
+
49
+ now = datetime.now()
50
+ update_id = now.strftime("%Y%m%d-%H%M%S")
51
+ filename = f"{update_id}.md"
52
+
53
+ content = f"""---
54
+ health: {args.health}
55
+ date: {now.strftime("%Y-%m-%d")}
56
+ time: {now.strftime("%H:%M")}
57
+ ---
58
+
59
+ {body}
60
+ """
61
+
62
+ (updates_dir / filename).write_text(content)
63
+
64
+ print(json.dumps({"success": True, "id": update_id}))
65
+ except Exception as e:
66
+ print(str(e), file=sys.stderr)
67
+ sys.exit(1)
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env python3
2
+ """Show project status summary (issue counts by status)."""
3
+
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from flydocs_api import status_summary
10
+
11
+ try:
12
+ result = status_summary()
13
+ print(json.dumps(result))
14
+ except Exception as e:
15
+ print(str(e), file=sys.stderr)
16
+ sys.exit(1)