@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,541 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""FlyDocs Skill Manager — search, add, remove, and list community skills.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 skill_manager.py search <keyword>
|
|
6
|
+
python3 skill_manager.py add <source>
|
|
7
|
+
python3 skill_manager.py remove <name>
|
|
8
|
+
python3 skill_manager.py list
|
|
9
|
+
|
|
10
|
+
Source formats for add:
|
|
11
|
+
Known name: react-best-practices
|
|
12
|
+
Repo reference: vercel-labs/agent-skills:react-best-practices
|
|
13
|
+
GitHub URL: https://github.com/owner/repo/tree/main/skills/name
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import shutil
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.request
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
RED = "\033[0;31m"
|
|
28
|
+
GREEN = "\033[0;32m"
|
|
29
|
+
BLUE = "\033[0;34m"
|
|
30
|
+
YELLOW = "\033[1;33m"
|
|
31
|
+
CYAN = "\033[0;36m"
|
|
32
|
+
NC = "\033[0m"
|
|
33
|
+
|
|
34
|
+
# ─── Curated community skills catalog ────────────────────────────────────────
|
|
35
|
+
# Mirrors install.sh COMMUNITY_SKILLS_MAP. Designed to swap with real API later.
|
|
36
|
+
|
|
37
|
+
COMMUNITY_SKILLS = [
|
|
38
|
+
{
|
|
39
|
+
"name": "react-best-practices",
|
|
40
|
+
"repo": "vercel-labs/agent-skills",
|
|
41
|
+
"description": "React/Next.js performance optimization (40+ rules from Vercel)",
|
|
42
|
+
"tags": ["react", "nextjs", "performance", "components"],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "web-design-guidelines",
|
|
46
|
+
"repo": "vercel-labs/agent-skills",
|
|
47
|
+
"description": "UI/UX accessibility and design patterns (100+ rules)",
|
|
48
|
+
"tags": ["design", "accessibility", "ux", "ui", "nextjs", "web"],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "convex-best-practices",
|
|
52
|
+
"repo": "waynesutton/convexskills",
|
|
53
|
+
"description": "Convex database patterns and best practices",
|
|
54
|
+
"tags": ["convex", "database", "backend"],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "convex-functions",
|
|
58
|
+
"repo": "waynesutton/convexskills",
|
|
59
|
+
"description": "Convex query/mutation/action patterns",
|
|
60
|
+
"tags": ["convex", "functions", "query", "mutation"],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "tailwind-v4",
|
|
64
|
+
"repo": "blencorp/claude-code-kit",
|
|
65
|
+
"description": "Tailwind CSS v4 patterns and configuration",
|
|
66
|
+
"tags": ["tailwind", "css", "styling"],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "clerk-auth",
|
|
70
|
+
"repo": "blencorp/claude-code-kit",
|
|
71
|
+
"description": "Clerk authentication patterns",
|
|
72
|
+
"tags": ["clerk", "auth", "authentication"],
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def find_project_root():
|
|
81
|
+
"""Find the project root by walking up to .flydocs/config.json."""
|
|
82
|
+
path = os.getcwd()
|
|
83
|
+
while True:
|
|
84
|
+
if os.path.isfile(os.path.join(path, ".flydocs", "config.json")):
|
|
85
|
+
return path
|
|
86
|
+
parent = os.path.dirname(path)
|
|
87
|
+
if parent == path:
|
|
88
|
+
break
|
|
89
|
+
path = parent
|
|
90
|
+
return os.getcwd()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_config(root):
|
|
94
|
+
"""Load .flydocs/config.json."""
|
|
95
|
+
config_path = os.path.join(root, ".flydocs", "config.json")
|
|
96
|
+
if not os.path.isfile(config_path):
|
|
97
|
+
print(f"{RED}✗{NC} No .flydocs/config.json found. Is FlyDocs installed?", file=sys.stderr)
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
100
|
+
return json.load(f)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def save_config(root, config):
|
|
104
|
+
"""Save .flydocs/config.json."""
|
|
105
|
+
config_path = os.path.join(root, ".flydocs", "config.json")
|
|
106
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
107
|
+
json.dump(config, f, indent=2)
|
|
108
|
+
f.write("\n")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse_frontmatter(text):
|
|
112
|
+
"""Parse YAML frontmatter from a SKILL.md file.
|
|
113
|
+
|
|
114
|
+
Duplicated from generate_manifest.py — each script is self-contained.
|
|
115
|
+
"""
|
|
116
|
+
match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
|
|
117
|
+
if not match:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
block = match.group(1)
|
|
121
|
+
result = {}
|
|
122
|
+
current_key = None
|
|
123
|
+
current_mode = None # "scalar", "block", "list"
|
|
124
|
+
current_lines = []
|
|
125
|
+
|
|
126
|
+
for line in block.split("\n"):
|
|
127
|
+
key_match = re.match(r"^(\w[\w-]*):\s*(.*)", line)
|
|
128
|
+
if key_match and not line.startswith(" "):
|
|
129
|
+
if current_key is not None:
|
|
130
|
+
result[current_key] = _flush(current_mode, current_lines)
|
|
131
|
+
|
|
132
|
+
current_key = key_match.group(1)
|
|
133
|
+
value = key_match.group(2).strip()
|
|
134
|
+
|
|
135
|
+
if value in ("|", ">"):
|
|
136
|
+
current_mode = "block"
|
|
137
|
+
current_lines = []
|
|
138
|
+
elif value == "":
|
|
139
|
+
current_mode = "list"
|
|
140
|
+
current_lines = []
|
|
141
|
+
else:
|
|
142
|
+
current_mode = "scalar"
|
|
143
|
+
current_lines = [value.strip("\"'")]
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
stripped = line.strip()
|
|
147
|
+
if current_key is not None:
|
|
148
|
+
if current_mode == "list" and stripped.startswith("- "):
|
|
149
|
+
current_lines.append(stripped[2:].strip().strip("\"'"))
|
|
150
|
+
elif current_mode == "block":
|
|
151
|
+
current_lines.append(line.lstrip())
|
|
152
|
+
|
|
153
|
+
if current_key is not None:
|
|
154
|
+
result[current_key] = _flush(current_mode, current_lines)
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _flush(mode, lines):
|
|
160
|
+
if mode == "scalar":
|
|
161
|
+
return lines[0] if lines else ""
|
|
162
|
+
elif mode == "list":
|
|
163
|
+
return lines
|
|
164
|
+
elif mode == "block":
|
|
165
|
+
return "\n".join(lines).strip()
|
|
166
|
+
return ""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def is_platform_skill(name):
|
|
170
|
+
"""Check if a skill name is platform-managed."""
|
|
171
|
+
return name.startswith("flydocs-")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def regenerate_manifest(root):
|
|
175
|
+
"""Run generate_manifest.py to update CLAUDE.md/AGENTS.md."""
|
|
176
|
+
script = os.path.join(root, ".flydocs", "scripts", "generate_manifest.py")
|
|
177
|
+
if not os.path.isfile(script):
|
|
178
|
+
print(f"{YELLOW}⚠{NC} generate_manifest.py not found — skipping manifest update.")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
subprocess.run(
|
|
183
|
+
[sys.executable, script, "--root", root],
|
|
184
|
+
check=True,
|
|
185
|
+
capture_output=True,
|
|
186
|
+
text=True,
|
|
187
|
+
)
|
|
188
|
+
print(f"{GREEN}✓{NC} Regenerated skill manifest")
|
|
189
|
+
except subprocess.CalledProcessError as e:
|
|
190
|
+
print(f"{YELLOW}⚠{NC} Manifest regeneration failed: {e.stderr.strip()}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ─── search ──────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def cmd_search(args):
|
|
197
|
+
"""Search curated community skills catalog."""
|
|
198
|
+
keyword = args.keyword.lower()
|
|
199
|
+
matches = []
|
|
200
|
+
|
|
201
|
+
for skill in COMMUNITY_SKILLS:
|
|
202
|
+
searchable = f"{skill['name']} {skill['description']} {' '.join(skill['tags'])}".lower()
|
|
203
|
+
if keyword in searchable:
|
|
204
|
+
matches.append(skill)
|
|
205
|
+
|
|
206
|
+
if not matches:
|
|
207
|
+
print(f"\n No skills found matching '{args.keyword}'.")
|
|
208
|
+
print(f" Browse more at: https://skills.sh/\n")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
print(f"\n Found {len(matches)} skill(s) matching '{args.keyword}':\n")
|
|
212
|
+
for skill in matches:
|
|
213
|
+
print(f" {CYAN}{skill['name']}{NC}")
|
|
214
|
+
print(f" {skill['description']}")
|
|
215
|
+
print(f" Source: {skill['repo']}")
|
|
216
|
+
print(f" Tags: {', '.join(skill['tags'])}")
|
|
217
|
+
print()
|
|
218
|
+
|
|
219
|
+
print(f" Install with: flydocs skills add <name>")
|
|
220
|
+
print(f" Browse more at: https://skills.sh/\n")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ─── add ─────────────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def parse_source(source):
|
|
227
|
+
"""Parse a skill source into (repo, skill_name).
|
|
228
|
+
|
|
229
|
+
Accepts:
|
|
230
|
+
- Known name: "react-best-practices"
|
|
231
|
+
- Repo reference: "vercel-labs/agent-skills:react-best-practices"
|
|
232
|
+
- GitHub URL: "https://github.com/owner/repo/tree/main/skills/name"
|
|
233
|
+
"""
|
|
234
|
+
# GitHub URL
|
|
235
|
+
url_match = re.match(
|
|
236
|
+
r"https?://github\.com/([^/]+/[^/]+)/tree/[^/]+/skills/([^/]+)/?$",
|
|
237
|
+
source,
|
|
238
|
+
)
|
|
239
|
+
if url_match:
|
|
240
|
+
return url_match.group(1), url_match.group(2)
|
|
241
|
+
|
|
242
|
+
# Repo reference (owner/repo:skill-name)
|
|
243
|
+
if ":" in source:
|
|
244
|
+
repo, name = source.rsplit(":", 1)
|
|
245
|
+
if "/" in repo:
|
|
246
|
+
return repo, name
|
|
247
|
+
|
|
248
|
+
# Known name — lookup in curated catalog
|
|
249
|
+
for skill in COMMUNITY_SKILLS:
|
|
250
|
+
if skill["name"] == source:
|
|
251
|
+
return skill["repo"], skill["name"]
|
|
252
|
+
|
|
253
|
+
print(f"{RED}✗{NC} Unknown skill: {source}", file=sys.stderr)
|
|
254
|
+
print(f" Try: flydocs skills search <keyword>", file=sys.stderr)
|
|
255
|
+
print(f" Or use full reference: owner/repo:skill-name", file=sys.stderr)
|
|
256
|
+
sys.exit(1)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def download_skill_tree(repo, skill_name, target_dir):
|
|
260
|
+
"""Download a skill directory tree via GitHub Contents API (recursive)."""
|
|
261
|
+
api_url = f"https://api.github.com/repos/{repo}/contents/skills/{skill_name}"
|
|
262
|
+
|
|
263
|
+
def fetch_json(url):
|
|
264
|
+
req = urllib.request.Request(url, headers={"User-Agent": "flydocs-cli"})
|
|
265
|
+
try:
|
|
266
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
267
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
268
|
+
except urllib.error.HTTPError as e:
|
|
269
|
+
if e.code == 404:
|
|
270
|
+
return None
|
|
271
|
+
raise
|
|
272
|
+
|
|
273
|
+
def download_recursive(url, dest_dir):
|
|
274
|
+
items = fetch_json(url)
|
|
275
|
+
if items is None:
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
# Single file response comes as dict, directory as list
|
|
279
|
+
if isinstance(items, dict):
|
|
280
|
+
items = [items]
|
|
281
|
+
|
|
282
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
283
|
+
|
|
284
|
+
for item in items:
|
|
285
|
+
dest_path = os.path.join(dest_dir, item["name"])
|
|
286
|
+
if item["type"] == "file":
|
|
287
|
+
if item.get("download_url"):
|
|
288
|
+
req = urllib.request.Request(
|
|
289
|
+
item["download_url"],
|
|
290
|
+
headers={"User-Agent": "flydocs-cli"},
|
|
291
|
+
)
|
|
292
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
293
|
+
content = resp.read()
|
|
294
|
+
with open(dest_path, "wb") as f:
|
|
295
|
+
f.write(content)
|
|
296
|
+
elif item["type"] == "dir":
|
|
297
|
+
download_recursive(item["url"], dest_path)
|
|
298
|
+
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
return download_recursive(api_url, target_dir)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def cmd_add(args):
|
|
305
|
+
"""Install a community skill."""
|
|
306
|
+
root = find_project_root()
|
|
307
|
+
config = load_config(root)
|
|
308
|
+
source = args.source
|
|
309
|
+
|
|
310
|
+
# Platform guard (check before parse_source which would reject as "unknown")
|
|
311
|
+
if is_platform_skill(source):
|
|
312
|
+
print(f"{RED}✗{NC} Cannot install platform skill '{source}'.")
|
|
313
|
+
print(f" Platform skills (flydocs-*) are managed by the installer.")
|
|
314
|
+
sys.exit(1)
|
|
315
|
+
|
|
316
|
+
repo, skill_name = parse_source(source)
|
|
317
|
+
|
|
318
|
+
# Platform guard (for repo references that resolve to a flydocs-* name)
|
|
319
|
+
if is_platform_skill(skill_name):
|
|
320
|
+
print(f"{RED}✗{NC} Cannot install platform skill '{skill_name}'.")
|
|
321
|
+
print(f" Platform skills (flydocs-*) are managed by the installer.")
|
|
322
|
+
sys.exit(1)
|
|
323
|
+
|
|
324
|
+
# Already installed check
|
|
325
|
+
skills_dir = os.path.join(root, ".claude", "skills", skill_name)
|
|
326
|
+
if os.path.isdir(skills_dir):
|
|
327
|
+
print(f"{YELLOW}⚠{NC} Skill '{skill_name}' is already installed.")
|
|
328
|
+
print(f" Remove first: flydocs skills remove {skill_name}")
|
|
329
|
+
sys.exit(1)
|
|
330
|
+
|
|
331
|
+
print(f"\n{BLUE}→{NC} Installing {CYAN}{skill_name}{NC} from {repo}...\n")
|
|
332
|
+
|
|
333
|
+
# Download
|
|
334
|
+
try:
|
|
335
|
+
success = download_skill_tree(repo, skill_name, skills_dir)
|
|
336
|
+
except Exception as e:
|
|
337
|
+
if os.path.isdir(skills_dir):
|
|
338
|
+
shutil.rmtree(skills_dir)
|
|
339
|
+
print(f"{RED}✗{NC} Download failed: {e}")
|
|
340
|
+
sys.exit(1)
|
|
341
|
+
|
|
342
|
+
if not success:
|
|
343
|
+
if os.path.isdir(skills_dir):
|
|
344
|
+
shutil.rmtree(skills_dir)
|
|
345
|
+
print(f"{RED}✗{NC} Skill not found at {repo}/skills/{skill_name}")
|
|
346
|
+
sys.exit(1)
|
|
347
|
+
|
|
348
|
+
# Validate SKILL.md exists
|
|
349
|
+
skill_md = os.path.join(skills_dir, "SKILL.md")
|
|
350
|
+
if not os.path.isfile(skill_md):
|
|
351
|
+
shutil.rmtree(skills_dir)
|
|
352
|
+
print(f"{RED}✗{NC} Invalid skill: SKILL.md not found.")
|
|
353
|
+
sys.exit(1)
|
|
354
|
+
|
|
355
|
+
# Validate frontmatter
|
|
356
|
+
with open(skill_md, "r", encoding="utf-8") as f:
|
|
357
|
+
content = f.read()
|
|
358
|
+
|
|
359
|
+
fm = parse_frontmatter(content)
|
|
360
|
+
if fm is None or not fm.get("name") or not fm.get("description"):
|
|
361
|
+
shutil.rmtree(skills_dir)
|
|
362
|
+
print(f"{RED}✗{NC} Invalid skill: SKILL.md missing required frontmatter (name, description).")
|
|
363
|
+
sys.exit(1)
|
|
364
|
+
|
|
365
|
+
if not fm.get("triggers"):
|
|
366
|
+
print(f"{YELLOW}⚠{NC} SKILL.md has no triggers — skill won't appear in manifest index.")
|
|
367
|
+
|
|
368
|
+
print(f"{GREEN}✓{NC} Downloaded skill files")
|
|
369
|
+
|
|
370
|
+
# Copy cursor-rule.mdc if present
|
|
371
|
+
cursor_rule = os.path.join(skills_dir, "cursor-rule.mdc")
|
|
372
|
+
if os.path.isfile(cursor_rule):
|
|
373
|
+
cursor_rules_dir = os.path.join(root, ".cursor", "rules")
|
|
374
|
+
os.makedirs(cursor_rules_dir, exist_ok=True)
|
|
375
|
+
dest = os.path.join(cursor_rules_dir, f"{skill_name}.mdc")
|
|
376
|
+
shutil.copy2(cursor_rule, dest)
|
|
377
|
+
print(f"{GREEN}✓{NC} Installed cursor rule")
|
|
378
|
+
|
|
379
|
+
# Update config.json skills.installed[]
|
|
380
|
+
installed = config.get("skills", {}).get("installed", [])
|
|
381
|
+
entry = f"{repo}/{skill_name}"
|
|
382
|
+
if entry not in installed:
|
|
383
|
+
installed.append(entry)
|
|
384
|
+
if "skills" not in config:
|
|
385
|
+
config["skills"] = {"installed": [], "custom": []}
|
|
386
|
+
config["skills"]["installed"] = installed
|
|
387
|
+
save_config(root, config)
|
|
388
|
+
print(f"{GREEN}✓{NC} Updated config.json")
|
|
389
|
+
|
|
390
|
+
# Regenerate manifest
|
|
391
|
+
regenerate_manifest(root)
|
|
392
|
+
|
|
393
|
+
print(f"\n{GREEN}✓{NC} Installed {CYAN}{skill_name}{NC}\n")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ─── remove ──────────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def cmd_remove(args):
|
|
400
|
+
"""Remove an installed community skill."""
|
|
401
|
+
root = find_project_root()
|
|
402
|
+
config = load_config(root)
|
|
403
|
+
skill_name = args.name
|
|
404
|
+
|
|
405
|
+
# Platform guard
|
|
406
|
+
if is_platform_skill(skill_name):
|
|
407
|
+
print(f"{RED}✗{NC} Cannot remove platform skill '{skill_name}'.")
|
|
408
|
+
print(f" Platform skills (flydocs-*) are managed by the installer.")
|
|
409
|
+
sys.exit(1)
|
|
410
|
+
|
|
411
|
+
skills_dir = os.path.join(root, ".claude", "skills", skill_name)
|
|
412
|
+
if not os.path.isdir(skills_dir):
|
|
413
|
+
print(f"{RED}✗{NC} Skill '{skill_name}' is not installed.")
|
|
414
|
+
sys.exit(1)
|
|
415
|
+
|
|
416
|
+
# Remove skill directory
|
|
417
|
+
shutil.rmtree(skills_dir)
|
|
418
|
+
print(f"{GREEN}✓{NC} Removed skill directory")
|
|
419
|
+
|
|
420
|
+
# Remove cursor rule
|
|
421
|
+
cursor_rule = os.path.join(root, ".cursor", "rules", f"{skill_name}.mdc")
|
|
422
|
+
if os.path.isfile(cursor_rule):
|
|
423
|
+
os.remove(cursor_rule)
|
|
424
|
+
print(f"{GREEN}✓{NC} Removed cursor rule")
|
|
425
|
+
|
|
426
|
+
# Update config.json
|
|
427
|
+
installed = config.get("skills", {}).get("installed", [])
|
|
428
|
+
config["skills"]["installed"] = [
|
|
429
|
+
s for s in installed if not s.endswith(f"/{skill_name}")
|
|
430
|
+
]
|
|
431
|
+
save_config(root, config)
|
|
432
|
+
print(f"{GREEN}✓{NC} Updated config.json")
|
|
433
|
+
|
|
434
|
+
# Regenerate manifest
|
|
435
|
+
regenerate_manifest(root)
|
|
436
|
+
|
|
437
|
+
print(f"\n{GREEN}✓{NC} Removed {CYAN}{skill_name}{NC}\n")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ─── list ────────────────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def cmd_list(args):
|
|
444
|
+
"""List all installed skills with type and trigger count."""
|
|
445
|
+
root = find_project_root()
|
|
446
|
+
skills_dir = os.path.join(root, ".claude", "skills")
|
|
447
|
+
|
|
448
|
+
if not os.path.isdir(skills_dir):
|
|
449
|
+
print(f"\n No skills installed.\n")
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
platform = []
|
|
453
|
+
community = []
|
|
454
|
+
|
|
455
|
+
for entry in sorted(os.listdir(skills_dir)):
|
|
456
|
+
skill_file = os.path.join(skills_dir, entry, "SKILL.md")
|
|
457
|
+
if not os.path.isfile(skill_file):
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
with open(skill_file, "r", encoding="utf-8") as f:
|
|
461
|
+
content = f.read()
|
|
462
|
+
|
|
463
|
+
fm = parse_frontmatter(content)
|
|
464
|
+
name = fm.get("name", entry) if fm else entry
|
|
465
|
+
triggers = fm.get("triggers", []) if fm else []
|
|
466
|
+
if isinstance(triggers, str):
|
|
467
|
+
triggers = [t.strip() for t in triggers.split(",") if t.strip()]
|
|
468
|
+
|
|
469
|
+
info = {
|
|
470
|
+
"name": name,
|
|
471
|
+
"dir": entry,
|
|
472
|
+
"type": "platform" if is_platform_skill(entry) else "community",
|
|
473
|
+
"triggers": len(triggers),
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if info["type"] == "platform":
|
|
477
|
+
platform.append(info)
|
|
478
|
+
else:
|
|
479
|
+
community.append(info)
|
|
480
|
+
|
|
481
|
+
total = len(platform) + len(community)
|
|
482
|
+
if total == 0:
|
|
483
|
+
print(f"\n No skills installed.\n")
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
print(f"\n {total} skill(s) installed:\n")
|
|
487
|
+
|
|
488
|
+
if platform:
|
|
489
|
+
print(f" {YELLOW}Platform{NC}")
|
|
490
|
+
for s in platform:
|
|
491
|
+
print(f" {CYAN}{s['name']}{NC} ({s['triggers']} triggers)")
|
|
492
|
+
|
|
493
|
+
if community:
|
|
494
|
+
if platform:
|
|
495
|
+
print()
|
|
496
|
+
print(f" {YELLOW}Community{NC}")
|
|
497
|
+
for s in community:
|
|
498
|
+
print(f" {CYAN}{s['name']}{NC} ({s['triggers']} triggers)")
|
|
499
|
+
|
|
500
|
+
print()
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# ─── main ────────────────────────────────────────────────────────────────────
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def main():
|
|
507
|
+
parser = argparse.ArgumentParser(
|
|
508
|
+
prog="flydocs skills",
|
|
509
|
+
description="Manage FlyDocs skills — search, install, and remove community skills.",
|
|
510
|
+
)
|
|
511
|
+
sub = parser.add_subparsers(dest="command")
|
|
512
|
+
|
|
513
|
+
search_p = sub.add_parser("search", help="Search available community skills")
|
|
514
|
+
search_p.add_argument("keyword", help="Keyword to search for")
|
|
515
|
+
|
|
516
|
+
add_p = sub.add_parser("add", help="Install a community skill")
|
|
517
|
+
add_p.add_argument("source", help="Skill name, repo:name, or GitHub URL")
|
|
518
|
+
|
|
519
|
+
remove_p = sub.add_parser("remove", help="Remove an installed community skill")
|
|
520
|
+
remove_p.add_argument("name", help="Skill directory name")
|
|
521
|
+
|
|
522
|
+
sub.add_parser("list", help="List installed skills")
|
|
523
|
+
|
|
524
|
+
args = parser.parse_args()
|
|
525
|
+
|
|
526
|
+
if args.command is None:
|
|
527
|
+
parser.print_help()
|
|
528
|
+
sys.exit(1)
|
|
529
|
+
|
|
530
|
+
commands = {
|
|
531
|
+
"search": cmd_search,
|
|
532
|
+
"add": cmd_add,
|
|
533
|
+
"remove": cmd_remove,
|
|
534
|
+
"list": cmd_list,
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
commands[args.command](args)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
if __name__ == "__main__":
|
|
541
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Issue Templates
|
|
2
|
+
|
|
3
|
+
This folder contains templates for creating and refining Linear issues. Each template includes agent instructions to ensure consistent, high-quality issue structure.
|
|
4
|
+
|
|
5
|
+
## Template Usage
|
|
6
|
+
|
|
7
|
+
**Agents use these templates when:**
|
|
8
|
+
- Creating new issues via `/capture`
|
|
9
|
+
- Refining issues via `/refine`
|
|
10
|
+
- Triaging quick captures with `triage` label
|
|
11
|
+
|
|
12
|
+
**Humans use `quick-capture.md` in Linear** as the default template for fast idea/bug capture.
|
|
13
|
+
|
|
14
|
+
## Available Templates
|
|
15
|
+
|
|
16
|
+
| Template | Purpose | When to Use |
|
|
17
|
+
|----------|---------|-------------|
|
|
18
|
+
| `feature.md` | New functionality | Building something new with user story |
|
|
19
|
+
| `bug.md` | Fix defects | Something is broken |
|
|
20
|
+
| `chore.md` | Maintenance/cleanup | Refactoring, polish, improvements |
|
|
21
|
+
| `idea.md` | Capture concepts | Rough ideas for future exploration |
|
|
22
|
+
| `quick-capture.md` | Fast capture | Human entry in Linear (default template) |
|
|
23
|
+
|
|
24
|
+
## Agent Instructions
|
|
25
|
+
|
|
26
|
+
Each template contains `<!-- AGENT: ... -->` comments that guide the AI when filling out sections. These instructions are removed from the final issue in Linear.
|
|
27
|
+
|
|
28
|
+
**Key principles:**
|
|
29
|
+
1. Fill all `[bracketed]` sections with specific content
|
|
30
|
+
2. Don't remove sections - mark as "N/A" if not applicable
|
|
31
|
+
3. Be specific in acceptance criteria (testable, measurable)
|
|
32
|
+
4. Include context about "why" not just "what"
|
|
33
|
+
5. Reference project context from `{content-folder}/context/` when relevant
|
|
34
|
+
|
|
35
|
+
## Template Evolution
|
|
36
|
+
|
|
37
|
+
Update these templates as your workflow evolves:
|
|
38
|
+
- Add sections that are consistently needed
|
|
39
|
+
- Remove sections that are never used
|
|
40
|
+
- Improve agent instructions based on what works
|
|
41
|
+
|
|
42
|
+
Changes here apply to all future issues.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|