@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.
- package/README.md +96 -0
- package/dist/cli.js +2666 -0
- package/package.json +32 -0
- package/template/.claude/CLAUDE.md +90 -0
- package/template/.claude/agents/README.md +19 -0
- package/template/.claude/agents/implementation-agent.md +29 -0
- package/template/.claude/agents/pm-agent.md +29 -0
- package/template/.claude/agents/research-agent.md +25 -0
- package/template/.claude/agents/review-agent.md +29 -0
- package/template/.claude/commands/activate.md +10 -0
- package/template/.claude/commands/attach.md +9 -0
- package/template/.claude/commands/block.md +10 -0
- package/template/.claude/commands/capture.md +10 -0
- package/template/.claude/commands/close.md +10 -0
- package/template/.claude/commands/flydocs-setup.md +598 -0
- package/template/.claude/commands/flydocs-update.md +27 -0
- package/template/.claude/commands/implement.md +10 -0
- package/template/.claude/commands/new-project.md +11 -0
- package/template/.claude/commands/project-update.md +10 -0
- package/template/.claude/commands/refine.md +10 -0
- package/template/.claude/commands/review.md +10 -0
- package/template/.claude/commands/start-session.md +10 -0
- package/template/.claude/commands/status.md +10 -0
- package/template/.claude/commands/validate.md +10 -0
- package/template/.claude/commands/wrap-session.md +10 -0
- package/template/.claude/settings.json +49 -0
- package/template/.claude/skills/README.md +293 -0
- package/template/.claude/skills/flydocs-cloud/SKILL.md +96 -0
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +50 -0
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +38 -0
- package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +44 -0
- package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +44 -0
- package/template/.claude/skills/flydocs-cloud/scripts/comment.py +39 -0
- package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +100 -0
- package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +46 -0
- package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +40 -0
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +38 -0
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +277 -0
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +77 -0
- package/template/.claude/skills/flydocs-cloud/scripts/link.py +47 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +35 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +105 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +40 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +45 -0
- package/template/.claude/skills/flydocs-cloud/scripts/priority.py +38 -0
- package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +59 -0
- package/template/.claude/skills/flydocs-cloud/scripts/transition.py +67 -0
- package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +47 -0
- package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +111 -0
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +87 -0
- package/template/.claude/skills/flydocs-context-graph/schema.md +78 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_build.py +299 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +338 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_query.py +191 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_session.py +161 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_update.py +194 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_utils.py +118 -0
- package/template/.claude/skills/flydocs-estimates/SKILL.md +384 -0
- package/template/.claude/skills/flydocs-estimates/references/provider-costs.md +152 -0
- package/template/.claude/skills/flydocs-figma/SKILL.md +377 -0
- package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +108 -0
- package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +112 -0
- package/template/.claude/skills/flydocs-local/SKILL.md +103 -0
- package/template/.claude/skills/flydocs-local/cursor-rule.mdc +43 -0
- package/template/.claude/skills/flydocs-local/scripts/assign.py +20 -0
- package/template/.claude/skills/flydocs-local/scripts/comment.py +27 -0
- package/template/.claude/skills/flydocs-local/scripts/create_issue.py +44 -0
- package/template/.claude/skills/flydocs-local/scripts/estimate.py +37 -0
- package/template/.claude/skills/flydocs-local/scripts/flydocs_api.py +272 -0
- package/template/.claude/skills/flydocs-local/scripts/get_issue.py +20 -0
- package/template/.claude/skills/flydocs-local/scripts/link.py +41 -0
- package/template/.claude/skills/flydocs-local/scripts/list_issues.py +34 -0
- package/template/.claude/skills/flydocs-local/scripts/priority.py +37 -0
- package/template/.claude/skills/flydocs-local/scripts/project_update.py +67 -0
- package/template/.claude/skills/flydocs-local/scripts/status_summary.py +16 -0
- package/template/.claude/skills/flydocs-local/scripts/transition.py +24 -0
- package/template/.claude/skills/flydocs-local/scripts/update_description.py +35 -0
- package/template/.claude/skills/flydocs-local/scripts/update_issue.py +84 -0
- package/template/.claude/skills/flydocs-workflow/SKILL.md +85 -0
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +53 -0
- package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +131 -0
- package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +76 -0
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +28 -0
- package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +50 -0
- package/template/.claude/skills/flydocs-workflow/session.md +128 -0
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +46 -0
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +50 -0
- package/template/.claude/skills/flydocs-workflow/stages/close.md +32 -0
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +124 -0
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +51 -0
- package/template/.claude/skills/flydocs-workflow/stages/review.md +86 -0
- package/template/.claude/skills/flydocs-workflow/stages/validate.md +90 -0
- package/template/.claude/skills/flydocs-workflow/templates/bug.md +95 -0
- package/template/.claude/skills/flydocs-workflow/templates/chore.md +75 -0
- package/template/.claude/skills/flydocs-workflow/templates/feature.md +93 -0
- package/template/.claude/skills/flydocs-workflow/templates/idea.md +84 -0
- package/template/.cursor/agents/implementation-agent.md +28 -0
- package/template/.cursor/agents/pm-agent.md +27 -0
- package/template/.cursor/agents/research-agent.md +23 -0
- package/template/.cursor/agents/review-agent.md +27 -0
- package/template/.cursor/hooks.json +29 -0
- package/template/.cursor/mcp.json +16 -0
- package/template/.env.example +44 -0
- package/template/.flydocs/config.json +104 -0
- package/template/.flydocs/hooks/auto-approve.py +71 -0
- package/template/.flydocs/hooks/post-edit.py +72 -0
- package/template/.flydocs/hooks/prefer-scripts.py +89 -0
- package/template/.flydocs/hooks/prompt-submit.py +277 -0
- package/template/.flydocs/scripts/generate_manifest.py +287 -0
- package/template/.flydocs/scripts/skill_manager.py +541 -0
- package/template/.flydocs/templates/README.md +46 -0
- package/template/.flydocs/templates/bug.md +166 -0
- package/template/.flydocs/templates/chore.md +110 -0
- package/template/.flydocs/templates/design-system/README.md +27 -0
- package/template/.flydocs/templates/design-system/component-patterns.md +92 -0
- package/template/.flydocs/templates/design-system/token-mapping.md +168 -0
- package/template/.flydocs/templates/feature.md +173 -0
- package/template/.flydocs/templates/idea.md +122 -0
- package/template/.flydocs/templates/instructions.md +228 -0
- package/template/.flydocs/templates/quick-capture.md +35 -0
- package/template/.flydocs/templates/scripts/check-design-system.template.mjs +179 -0
- package/template/.flydocs/version +1 -0
- package/template/AGENTS.md +95 -0
- package/template/CHANGELOG.md +271 -0
- package/template/flydocs/README.md +186 -0
- package/template/flydocs/context/project.md +51 -0
- package/template/flydocs/design-system/README.md +126 -0
- package/template/flydocs/design-system/component-patterns.md +173 -0
- package/template/flydocs/design-system/token-mapping.md +114 -0
- package/template/flydocs/knowledge/INDEX.md +100 -0
- package/template/flydocs/knowledge/README.md +62 -0
- package/template/flydocs/knowledge/product/personas.md +79 -0
- package/template/flydocs/knowledge/product/user-flows.md +88 -0
- 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)
|