@flydocs/cli 0.6.0-alpha.3 → 0.6.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2053 -469
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +43 -48
- package/template/.claude/agents/implementation-agent.md +1 -1
- package/template/.claude/agents/pm-agent.md +1 -1
- package/template/.claude/commands/activate.md +1 -1
- package/template/.claude/commands/attach.md +1 -1
- package/template/.claude/commands/block.md +2 -2
- package/template/.claude/commands/capture.md +1 -1
- package/template/.claude/commands/close.md +1 -1
- package/template/.claude/commands/flydocs-setup.md +359 -72
- package/template/.claude/commands/flydocs-upgrade.md +26 -27
- package/template/.claude/commands/implement.md +1 -1
- package/template/.claude/commands/knowledge.md +61 -0
- package/template/.claude/commands/new-project.md +1 -1
- package/template/.claude/commands/onboard.md +275 -0
- package/template/.claude/commands/project-update.md +1 -1
- package/template/.claude/commands/refine.md +1 -1
- package/template/.claude/commands/review.md +1 -1
- package/template/.claude/commands/start-session.md +1 -1
- package/template/.claude/commands/status.md +1 -1
- package/template/.claude/commands/validate.md +1 -1
- package/template/.claude/commands/wrap-session.md +1 -1
- package/template/.claude/hooks/auto-approve.py +212 -0
- package/template/.claude/hooks/post-pr-check.py +108 -0
- package/template/.claude/hooks/post-transition-check.py +281 -0
- package/template/.claude/hooks/prompt-submit.py +554 -0
- package/template/.claude/hooks/session-start.py +262 -0
- package/template/.claude/hooks/stop-gate.py +162 -0
- package/template/.claude/settings.json +41 -4
- package/template/.claude/skills/README.md +23 -25
- package/template/.claude/skills/flydocs-workflow/SKILL.md +134 -42
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
- package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
- package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
- package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
- package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +120 -0
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
- package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +260 -0
- package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
- package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
- package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +137 -47
- package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +724 -0
- package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -10
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
- package/template/.claude/skills/flydocs-workflow/scripts/issues.py +738 -0
- package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
- package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
- package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
- package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
- package/template/.claude/skills/flydocs-workflow/scripts/test_enforcement.py +225 -0
- package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +902 -0
- package/template/.claude/skills/flydocs-workflow/session.md +87 -29
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +18 -7
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +10 -5
- package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +33 -9
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +22 -6
- package/template/.claude/skills/flydocs-workflow/stages/review.md +16 -4
- package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
- package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
- package/template/.cursor/agents/implementation-agent.md +1 -1
- package/template/.cursor/agents/pm-agent.md +2 -2
- package/template/.cursor/hooks.json +10 -3
- package/template/.env.example +6 -6
- package/template/.flydocs/config.json +5 -18
- package/template/.flydocs/templates/README.md +13 -14
- package/template/.flydocs/templates/bug.md +17 -153
- package/template/.flydocs/templates/chore.md +10 -98
- package/template/.flydocs/templates/feature.md +12 -158
- package/template/.flydocs/templates/idea.md +11 -111
- package/template/.flydocs/templates/quick-capture.md +4 -8
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +44 -32
- package/template/CHANGELOG.md +37 -0
- package/template/flydocs/README.md +1 -3
- package/template/flydocs/context/project.md +6 -3
- package/template/flydocs/design-system/README.md +3 -3
- package/template/flydocs/knowledge/INDEX.md +38 -53
- package/template/flydocs/knowledge/README.md +60 -9
- package/template/flydocs/knowledge/templates/decision.md +47 -0
- package/template/flydocs/knowledge/templates/feature.md +35 -0
- package/template/flydocs/knowledge/templates/note.md +25 -0
- package/template/manifest.json +24 -20
- package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -113
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -66
- package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
- package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
- package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -210
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
- package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
- package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
- package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
- package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -68
- package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -46
- package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -41
- package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
- package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
- package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -82
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -87
- package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
- package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
- package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
- package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
- package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
- package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
- package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
- package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
- package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
- package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
- package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
- package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
- package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
- package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -34
- package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
- package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
- package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
- package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
- package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
- package/template/.flydocs/hooks/auto-approve.py +0 -71
- package/template/.flydocs/hooks/prompt-submit.py +0 -277
- package/template/.flydocs/scripts/skill_manager.py +0 -541
- /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
- /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{idea.md → issues/idea.md} +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Project, milestone, and cycle operations dispatcher.
|
|
3
|
+
|
|
4
|
+
All subcommands are cloud-only. The unified client handles tier gating
|
|
5
|
+
via require_cloud().
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python projects.py list-projects [--active] [--all]
|
|
9
|
+
python projects.py create-project --name NAME [--description DESC]
|
|
10
|
+
python projects.py list-milestones [--all]
|
|
11
|
+
python projects.py create-milestone --name NAME [--project ID] [--target-date DATE]
|
|
12
|
+
python projects.py update-milestone MILESTONE_ID [--name] [--target-date] [--description]
|
|
13
|
+
python projects.py delete-milestone MILESTONE_ID
|
|
14
|
+
python projects.py list-cycles [--active]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
22
|
+
from flydocs_api import get_client, output_json, fail
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cmd_list_projects(args: argparse.Namespace) -> None:
|
|
26
|
+
client = get_client()
|
|
27
|
+
result = client.list_projects(active=args.active, show_all=args.show_all)
|
|
28
|
+
output_json(result)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cmd_create_project(args: argparse.Namespace) -> None:
|
|
32
|
+
client = get_client()
|
|
33
|
+
result = client.create_project(name=args.name, description=args.description)
|
|
34
|
+
output_json(result)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cmd_list_milestones(args: argparse.Namespace) -> None:
|
|
38
|
+
client = get_client()
|
|
39
|
+
result = client.list_milestones(show_all=args.show_all)
|
|
40
|
+
output_json(result)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cmd_create_milestone(args: argparse.Namespace) -> None:
|
|
44
|
+
client = get_client()
|
|
45
|
+
# Default project from activeProjects if not explicitly provided
|
|
46
|
+
project = args.project
|
|
47
|
+
if not project and client.is_cloud:
|
|
48
|
+
active = client.config.get("workspace", {}).get("activeProjects", [])
|
|
49
|
+
if active:
|
|
50
|
+
project = active[0]
|
|
51
|
+
result = client.create_milestone(
|
|
52
|
+
name=args.name,
|
|
53
|
+
project=project,
|
|
54
|
+
target_date=args.target_date,
|
|
55
|
+
)
|
|
56
|
+
output_json(result)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_update_milestone(args: argparse.Namespace) -> None:
|
|
60
|
+
client = get_client()
|
|
61
|
+
fields: dict = {}
|
|
62
|
+
if args.name is not None:
|
|
63
|
+
fields["name"] = args.name
|
|
64
|
+
if args.target_date is not None:
|
|
65
|
+
fields["targetDate"] = args.target_date
|
|
66
|
+
if args.description is not None:
|
|
67
|
+
fields["description"] = args.description
|
|
68
|
+
if not fields:
|
|
69
|
+
fail("At least one of --name, --target-date, or --description is required")
|
|
70
|
+
result = client.update_milestone(args.milestone_id, **fields)
|
|
71
|
+
output_json(result)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def cmd_delete_milestone(args: argparse.Namespace) -> None:
|
|
75
|
+
client = get_client()
|
|
76
|
+
result = client.delete_milestone(args.milestone_id)
|
|
77
|
+
output_json(result)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_list_cycles(args: argparse.Namespace) -> None:
|
|
81
|
+
client = get_client()
|
|
82
|
+
result = client.list_cycles(active=args.active)
|
|
83
|
+
output_json(result)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main() -> None:
|
|
87
|
+
parser = argparse.ArgumentParser(description="FlyDocs project operations")
|
|
88
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
89
|
+
|
|
90
|
+
# list-projects
|
|
91
|
+
lp = sub.add_parser("list-projects", help="List projects")
|
|
92
|
+
lp.add_argument("--active", action="store_true", help="Active projects only")
|
|
93
|
+
lp.add_argument("--all", action="store_true", dest="show_all",
|
|
94
|
+
help="Include all projects (bypass label filter)")
|
|
95
|
+
|
|
96
|
+
# create-project
|
|
97
|
+
cp = sub.add_parser("create-project", help="Create a project")
|
|
98
|
+
cp.add_argument("--name", required=True, help="Project name")
|
|
99
|
+
cp.add_argument("--description", default=None, help="Project description")
|
|
100
|
+
|
|
101
|
+
# list-milestones
|
|
102
|
+
lm = sub.add_parser("list-milestones", help="List milestones")
|
|
103
|
+
lm.add_argument("--all", action="store_true", dest="show_all",
|
|
104
|
+
help="Include milestones from all projects")
|
|
105
|
+
|
|
106
|
+
# create-milestone
|
|
107
|
+
cm = sub.add_parser("create-milestone", help="Create a milestone")
|
|
108
|
+
cm.add_argument("--name", required=True, help="Milestone name")
|
|
109
|
+
cm.add_argument("--project", default=None, help="Project ID")
|
|
110
|
+
cm.add_argument("--target-date", default=None, dest="target_date",
|
|
111
|
+
help="Target date (YYYY-MM-DD)")
|
|
112
|
+
|
|
113
|
+
# update-milestone
|
|
114
|
+
um = sub.add_parser("update-milestone", help="Update a milestone")
|
|
115
|
+
um.add_argument("milestone_id", help="Milestone ID")
|
|
116
|
+
um.add_argument("--name", default=None, help="New name")
|
|
117
|
+
um.add_argument("--target-date", default=None, dest="target_date",
|
|
118
|
+
help="New target date (YYYY-MM-DD)")
|
|
119
|
+
um.add_argument("--description", default=None, help="New description")
|
|
120
|
+
|
|
121
|
+
# delete-milestone
|
|
122
|
+
dm = sub.add_parser("delete-milestone", help="Delete a milestone")
|
|
123
|
+
dm.add_argument("milestone_id", help="Milestone ID")
|
|
124
|
+
|
|
125
|
+
# list-cycles
|
|
126
|
+
lc = sub.add_parser("list-cycles", help="List cycles")
|
|
127
|
+
lc.add_argument("--active", action="store_true", help="Active cycles only")
|
|
128
|
+
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
|
|
131
|
+
commands = {
|
|
132
|
+
"list-projects": cmd_list_projects,
|
|
133
|
+
"create-project": cmd_create_project,
|
|
134
|
+
"list-milestones": cmd_list_milestones,
|
|
135
|
+
"create-milestone": cmd_create_milestone,
|
|
136
|
+
"update-milestone": cmd_update_milestone,
|
|
137
|
+
"delete-milestone": cmd_delete_milestone,
|
|
138
|
+
"list-cycles": cmd_list_cycles,
|
|
139
|
+
}
|
|
140
|
+
commands[args.command](args)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
main()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pull workspace service descriptors from the relay.
|
|
3
|
+
|
|
4
|
+
Fetches GET /api/relay/workspace/services and caches the composite
|
|
5
|
+
at .flydocs/cache/workspace-services.json for local use.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python3 .claude/skills/flydocs-workflow/scripts/pull_services.py [--root PATH]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
21
|
+
from graph_utils import find_project_root, fail
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_api_key(root):
|
|
25
|
+
"""Load FLYDOCS_API_KEY from environment or .env files."""
|
|
26
|
+
if os.environ.get("FLYDOCS_API_KEY"):
|
|
27
|
+
return os.environ["FLYDOCS_API_KEY"]
|
|
28
|
+
for name in [".env.local", ".env"]:
|
|
29
|
+
env_file = root / name
|
|
30
|
+
if env_file.exists():
|
|
31
|
+
with open(env_file, "r") as f:
|
|
32
|
+
for line in f:
|
|
33
|
+
line = line.strip()
|
|
34
|
+
if line.startswith("#") or "=" not in line:
|
|
35
|
+
continue
|
|
36
|
+
k, _, v = line.partition("=")
|
|
37
|
+
if k.strip() == "FLYDOCS_API_KEY":
|
|
38
|
+
v = v.strip().strip("\"'")
|
|
39
|
+
return v if v else None
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_config(root):
|
|
44
|
+
"""Load .flydocs/config.json."""
|
|
45
|
+
config_path = root / ".flydocs" / "config.json"
|
|
46
|
+
if config_path.exists():
|
|
47
|
+
with open(config_path, "r") as f:
|
|
48
|
+
return json.load(f)
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_base_url(config):
|
|
53
|
+
"""Resolve relay base URL."""
|
|
54
|
+
env_url = os.environ.get("FLYDOCS_RELAY_URL")
|
|
55
|
+
if env_url:
|
|
56
|
+
return env_url.rstrip("/")
|
|
57
|
+
config_url = config.get("relay", {}).get("url")
|
|
58
|
+
if config_url:
|
|
59
|
+
return config_url.rstrip("/")
|
|
60
|
+
return "https://app.flydocs.ai/api/relay"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main():
|
|
64
|
+
parser = argparse.ArgumentParser(description="Pull workspace service descriptors")
|
|
65
|
+
parser.add_argument("--root", type=str, default=None, help="Project root")
|
|
66
|
+
args = parser.parse_args()
|
|
67
|
+
|
|
68
|
+
root = Path(args.root) if args.root else find_project_root()
|
|
69
|
+
if not root:
|
|
70
|
+
fail("Could not find project root (no .flydocs/ directory found)")
|
|
71
|
+
|
|
72
|
+
config = load_config(root)
|
|
73
|
+
api_key = load_api_key(root)
|
|
74
|
+
if not api_key:
|
|
75
|
+
fail("FLYDOCS_API_KEY not found. Set in environment or .env file.")
|
|
76
|
+
|
|
77
|
+
workspace_id = config.get("workspaceId")
|
|
78
|
+
if not workspace_id:
|
|
79
|
+
fail("workspaceId not found in .flydocs/config.json. Run /flydocs-setup first.")
|
|
80
|
+
|
|
81
|
+
base_url = resolve_base_url(config)
|
|
82
|
+
|
|
83
|
+
# Fetch workspace services composite
|
|
84
|
+
url = f"{base_url}/workspace/services"
|
|
85
|
+
headers = {
|
|
86
|
+
"Authorization": f"Bearer {api_key}",
|
|
87
|
+
"X-Workspace": workspace_id,
|
|
88
|
+
"Accept": "application/json",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
93
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
94
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
95
|
+
except urllib.error.HTTPError as e:
|
|
96
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
97
|
+
try:
|
|
98
|
+
error_data = json.loads(error_body) if error_body else {}
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
error_data = {"error": error_body}
|
|
101
|
+
fail(f"Relay API error ({e.code}): {error_data.get('error', str(e))}")
|
|
102
|
+
except (urllib.error.URLError, TimeoutError) as e:
|
|
103
|
+
fail(f"Network error: {e}")
|
|
104
|
+
|
|
105
|
+
# Cache the composite locally
|
|
106
|
+
cache_dir = root / ".flydocs" / "cache"
|
|
107
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
cache_file = cache_dir / "workspace-services.json"
|
|
109
|
+
|
|
110
|
+
cached = {
|
|
111
|
+
"fetchedAt": datetime.now(timezone.utc).isoformat(),
|
|
112
|
+
"services": result,
|
|
113
|
+
}
|
|
114
|
+
cache_file.write_text(json.dumps(cached, indent=2) + "\n", encoding="utf-8")
|
|
115
|
+
|
|
116
|
+
# Report
|
|
117
|
+
repos = result if isinstance(result, list) else result.get("repos", [])
|
|
118
|
+
with_descriptor = sum(1 for r in repos if r.get("serviceDescriptor"))
|
|
119
|
+
print(json.dumps({
|
|
120
|
+
"success": True,
|
|
121
|
+
"totalRepos": len(repos),
|
|
122
|
+
"withDescriptor": with_descriptor,
|
|
123
|
+
"cachedAt": cache_file.name,
|
|
124
|
+
}, indent=2))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Push this repo's service descriptor to the relay.
|
|
3
|
+
|
|
4
|
+
Reads flydocs/context/service.json, strips the local-only `structure` section,
|
|
5
|
+
and pushes to PUT /api/relay/workspace/service.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python3 .claude/skills/flydocs-workflow/scripts/push_service.py [--root PATH]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
20
|
+
from graph_utils import find_project_root, fail
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_api_key(root):
|
|
24
|
+
"""Load FLYDOCS_API_KEY from environment or .env files."""
|
|
25
|
+
if os.environ.get("FLYDOCS_API_KEY"):
|
|
26
|
+
return os.environ["FLYDOCS_API_KEY"]
|
|
27
|
+
for name in [".env.local", ".env"]:
|
|
28
|
+
env_file = root / name
|
|
29
|
+
if env_file.exists():
|
|
30
|
+
with open(env_file, "r") as f:
|
|
31
|
+
for line in f:
|
|
32
|
+
line = line.strip()
|
|
33
|
+
if line.startswith("#") or "=" not in line:
|
|
34
|
+
continue
|
|
35
|
+
k, _, v = line.partition("=")
|
|
36
|
+
if k.strip() == "FLYDOCS_API_KEY":
|
|
37
|
+
v = v.strip().strip("\"'")
|
|
38
|
+
return v if v else None
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_config(root):
|
|
43
|
+
"""Load .flydocs/config.json."""
|
|
44
|
+
config_path = root / ".flydocs" / "config.json"
|
|
45
|
+
if config_path.exists():
|
|
46
|
+
with open(config_path, "r") as f:
|
|
47
|
+
return json.load(f)
|
|
48
|
+
return {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_base_url(config):
|
|
52
|
+
"""Resolve relay base URL."""
|
|
53
|
+
env_url = os.environ.get("FLYDOCS_RELAY_URL")
|
|
54
|
+
if env_url:
|
|
55
|
+
return env_url.rstrip("/")
|
|
56
|
+
config_url = config.get("relay", {}).get("url")
|
|
57
|
+
if config_url:
|
|
58
|
+
return config_url.rstrip("/")
|
|
59
|
+
return "https://app.flydocs.ai/api/relay"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main():
|
|
63
|
+
parser = argparse.ArgumentParser(description="Push service descriptor to relay")
|
|
64
|
+
parser.add_argument("--root", type=str, default=None, help="Project root")
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
|
|
67
|
+
root = Path(args.root) if args.root else find_project_root()
|
|
68
|
+
if not root:
|
|
69
|
+
fail("Could not find project root (no .flydocs/ directory found)")
|
|
70
|
+
|
|
71
|
+
# Load service descriptor
|
|
72
|
+
service_file = root / "flydocs" / "context" / "service.json"
|
|
73
|
+
if not service_file.exists():
|
|
74
|
+
fail("No service descriptor found at flydocs/context/service.json. Run /flydocs-setup first.")
|
|
75
|
+
|
|
76
|
+
descriptor = json.loads(service_file.read_text(encoding="utf-8"))
|
|
77
|
+
|
|
78
|
+
# Strip local-only structure section before pushing
|
|
79
|
+
export_descriptor = {k: v for k, v in descriptor.items() if k != "structure"}
|
|
80
|
+
|
|
81
|
+
# Load config and credentials
|
|
82
|
+
config = load_config(root)
|
|
83
|
+
api_key = load_api_key(root)
|
|
84
|
+
if not api_key:
|
|
85
|
+
fail("FLYDOCS_API_KEY not found. Set in environment or .env file.")
|
|
86
|
+
|
|
87
|
+
workspace_id = config.get("workspaceId")
|
|
88
|
+
if not workspace_id:
|
|
89
|
+
fail("workspaceId not found in .flydocs/config.json. Run /flydocs-setup first.")
|
|
90
|
+
|
|
91
|
+
repo_slug = config.get("workspace", {}).get("repoSlug")
|
|
92
|
+
if not repo_slug:
|
|
93
|
+
repo_slug = descriptor.get("repoSlug")
|
|
94
|
+
if not repo_slug:
|
|
95
|
+
fail("repoSlug not found in config or service descriptor.")
|
|
96
|
+
|
|
97
|
+
base_url = resolve_base_url(config)
|
|
98
|
+
|
|
99
|
+
# Push descriptor
|
|
100
|
+
url = f"{base_url}/workspace/service"
|
|
101
|
+
headers = {
|
|
102
|
+
"Authorization": f"Bearer {api_key}",
|
|
103
|
+
"X-Workspace": workspace_id,
|
|
104
|
+
"X-Repo": repo_slug,
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
"Accept": "application/json",
|
|
107
|
+
}
|
|
108
|
+
data = json.dumps({"descriptor": export_descriptor}).encode("utf-8")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
req = urllib.request.Request(url, data=data, headers=headers, method="PUT")
|
|
112
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
113
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
114
|
+
print(json.dumps({
|
|
115
|
+
"success": True,
|
|
116
|
+
"repoSlug": repo_slug,
|
|
117
|
+
"fieldsExported": list(export_descriptor.keys()),
|
|
118
|
+
"response": result,
|
|
119
|
+
}, indent=2))
|
|
120
|
+
except urllib.error.HTTPError as e:
|
|
121
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
122
|
+
try:
|
|
123
|
+
error_data = json.loads(error_body) if error_body else {}
|
|
124
|
+
except json.JSONDecodeError:
|
|
125
|
+
error_data = {"error": error_body}
|
|
126
|
+
fail(f"Relay API error ({e.code}): {error_data.get('error', str(e))}")
|
|
127
|
+
except (urllib.error.URLError, TimeoutError) as e:
|
|
128
|
+
fail(f"Network error: {e}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Session operations dispatcher — project updates and status summary."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
9
|
+
from flydocs_api import get_client, output_json, fail, resolve_text_input
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cmd_project_update(args):
|
|
13
|
+
body = resolve_text_input(text_arg=args.body, file_arg=args.body_file)
|
|
14
|
+
if not body:
|
|
15
|
+
fail("Provide body via --body, --body-file, or stdin")
|
|
16
|
+
client = get_client()
|
|
17
|
+
# Resolve project: explicit flag > activeProjects[0] > relay discovery
|
|
18
|
+
project_id = args.project
|
|
19
|
+
if not project_id and client.is_cloud:
|
|
20
|
+
active = client.config.get("workspace", {}).get("activeProjects", [])
|
|
21
|
+
if active:
|
|
22
|
+
project_id = active[0]
|
|
23
|
+
result = client.project_update(args.health, body, project_id=project_id)
|
|
24
|
+
output_json(result)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cmd_status_summary(args):
|
|
28
|
+
client = get_client()
|
|
29
|
+
result = client.status_summary()
|
|
30
|
+
output_json(result)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
parser = argparse.ArgumentParser(description="FlyDocs session operations")
|
|
35
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
36
|
+
|
|
37
|
+
p = sub.add_parser("project-update", help="Post a project update")
|
|
38
|
+
p.add_argument("--health", required=True, choices=["onTrack", "atRisk", "offTrack"])
|
|
39
|
+
p.add_argument("--body", default=None)
|
|
40
|
+
p.add_argument("--body-file", default=None, dest="body_file")
|
|
41
|
+
p.add_argument("--project", default=None, help="Target project ID (defaults from activeProjects)")
|
|
42
|
+
|
|
43
|
+
sub.add_parser("status-summary", help="Show issue status counts")
|
|
44
|
+
|
|
45
|
+
args = parser.parse_args()
|
|
46
|
+
commands = {
|
|
47
|
+
"project-update": cmd_project_update,
|
|
48
|
+
"status-summary": cmd_status_summary,
|
|
49
|
+
}
|
|
50
|
+
commands[args.command](args)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for workflow enforcement logic.
|
|
3
|
+
|
|
4
|
+
Run: python3 test_enforcement.py
|
|
5
|
+
Tests the enforcement functions in issues.py, auto-approve.py, stop-gate.py,
|
|
6
|
+
and post-transition-check.py without requiring API access.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from unittest.mock import patch
|
|
15
|
+
|
|
16
|
+
# Add parent dirs to path for imports
|
|
17
|
+
SCRIPT_DIR = Path(__file__).parent.resolve()
|
|
18
|
+
HOOKS_DIR = SCRIPT_DIR.parent.parent.parent / "hooks"
|
|
19
|
+
sys.path.insert(0, str(SCRIPT_DIR))
|
|
20
|
+
sys.path.insert(0, str(HOOKS_DIR))
|
|
21
|
+
|
|
22
|
+
passed = 0
|
|
23
|
+
failed = 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test(name: str):
|
|
27
|
+
"""Decorator to register and run a test."""
|
|
28
|
+
def decorator(fn):
|
|
29
|
+
global passed, failed
|
|
30
|
+
try:
|
|
31
|
+
fn()
|
|
32
|
+
print(f" PASS: {name}")
|
|
33
|
+
passed += 1
|
|
34
|
+
except AssertionError as e:
|
|
35
|
+
print(f" FAIL: {name} — {e}")
|
|
36
|
+
failed += 1
|
|
37
|
+
except Exception as e:
|
|
38
|
+
print(f" ERROR: {name} — {type(e).__name__}: {e}")
|
|
39
|
+
failed += 1
|
|
40
|
+
return fn
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# issues.py enforcement tests
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
print("\n## issues.py enforcement")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@test("create rejects empty description")
|
|
52
|
+
def _():
|
|
53
|
+
"""issues.py create should fail with empty --description."""
|
|
54
|
+
import subprocess
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
[sys.executable, str(SCRIPT_DIR / "issues.py"), "create",
|
|
57
|
+
"--title", "Test", "--type", "feature", "--description", ""],
|
|
58
|
+
capture_output=True, text=True, timeout=10,
|
|
59
|
+
)
|
|
60
|
+
assert result.returncode != 0, f"Expected failure, got rc={result.returncode}"
|
|
61
|
+
assert "Description is required" in result.stderr, f"Expected error message, got: {result.stderr[:200]}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@test("create rejects missing description")
|
|
65
|
+
def _():
|
|
66
|
+
"""issues.py create should fail without --description flag."""
|
|
67
|
+
import subprocess
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
[sys.executable, str(SCRIPT_DIR / "issues.py"), "create",
|
|
70
|
+
"--title", "Test", "--type", "feature"],
|
|
71
|
+
capture_output=True, text=True, timeout=10,
|
|
72
|
+
)
|
|
73
|
+
assert result.returncode != 0, f"Expected failure, got rc={result.returncode}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@test("create allows --triage without description")
|
|
77
|
+
def _():
|
|
78
|
+
"""issues.py create with --triage should not fail on empty description."""
|
|
79
|
+
# This would succeed only if we have API access, so just check the
|
|
80
|
+
# enforcement bypass by testing the error message doesn't say "Description is required"
|
|
81
|
+
import subprocess
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
[sys.executable, str(SCRIPT_DIR / "issues.py"), "create",
|
|
84
|
+
"--title", "Quick test", "--type", "bug", "--triage"],
|
|
85
|
+
capture_output=True, text=True, timeout=10,
|
|
86
|
+
)
|
|
87
|
+
# It may fail for other reasons (no API), but NOT for missing description
|
|
88
|
+
assert "Description is required" not in result.stderr, \
|
|
89
|
+
f"--triage should bypass description check, got: {result.stderr[:200]}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@test("transition rejects empty comment")
|
|
93
|
+
def _():
|
|
94
|
+
"""issues.py transition should reject whitespace-only comment."""
|
|
95
|
+
import subprocess
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
[sys.executable, str(SCRIPT_DIR / "issues.py"), "transition",
|
|
98
|
+
"FLY-999", "REVIEW", " "],
|
|
99
|
+
capture_output=True, text=True, timeout=10,
|
|
100
|
+
)
|
|
101
|
+
assert result.returncode != 0, f"Expected failure, got rc={result.returncode}"
|
|
102
|
+
assert "comment cannot be empty" in result.stderr, f"Got: {result.stderr[:200]}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# auto-approve.py enforcement tests
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
print("\n## auto-approve.py enforcement")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@test("should_approve matches workflow scripts")
|
|
113
|
+
def _():
|
|
114
|
+
# Import the function directly
|
|
115
|
+
sys.path.insert(0, str(HOOKS_DIR))
|
|
116
|
+
# We need to import carefully since it's a hook script
|
|
117
|
+
import importlib.util
|
|
118
|
+
spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
|
|
119
|
+
mod = importlib.util.module_from_spec(spec)
|
|
120
|
+
spec.loader.exec_module(mod)
|
|
121
|
+
|
|
122
|
+
assert mod.should_approve("python3 .claude/skills/flydocs-workflow/scripts/issues.py create --title test")
|
|
123
|
+
assert mod.should_approve("python3 .claude/skills/flydocs-workflow/scripts/workspace.py validate")
|
|
124
|
+
assert not mod.should_approve("python3 malicious.py")
|
|
125
|
+
assert not mod.should_approve("rm -rf /")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@test("validate_create_args warns on missing description")
|
|
129
|
+
def _():
|
|
130
|
+
import importlib.util
|
|
131
|
+
spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
|
|
132
|
+
mod = importlib.util.module_from_spec(spec)
|
|
133
|
+
spec.loader.exec_module(mod)
|
|
134
|
+
|
|
135
|
+
warnings = mod.validate_create_args(
|
|
136
|
+
'python3 .claude/skills/flydocs-workflow/scripts/issues.py create --title "test" --type feature'
|
|
137
|
+
)
|
|
138
|
+
assert len(warnings) > 0, "Should warn about missing --description"
|
|
139
|
+
assert any("description" in w.lower() for w in warnings)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@test("validate_create_args no warning when description present")
|
|
143
|
+
def _():
|
|
144
|
+
import importlib.util
|
|
145
|
+
spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
|
|
146
|
+
mod = importlib.util.module_from_spec(spec)
|
|
147
|
+
spec.loader.exec_module(mod)
|
|
148
|
+
|
|
149
|
+
warnings = mod.validate_create_args(
|
|
150
|
+
'python3 .claude/skills/flydocs-workflow/scripts/issues.py create --title "test" --type feature --description "Full description here with enough content"'
|
|
151
|
+
)
|
|
152
|
+
desc_warnings = [w for w in warnings if "Missing --description" in w]
|
|
153
|
+
assert len(desc_warnings) == 0, f"Should not warn when description present, got: {warnings}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@test("get_transition_comment_hint returns template for known status")
|
|
157
|
+
def _():
|
|
158
|
+
import importlib.util
|
|
159
|
+
spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
|
|
160
|
+
mod = importlib.util.module_from_spec(spec)
|
|
161
|
+
spec.loader.exec_module(mod)
|
|
162
|
+
|
|
163
|
+
hint = mod.get_transition_comment_hint("issues.py transition FLY-123 IMPLEMENTING")
|
|
164
|
+
assert hint is not None, "Should return hint for IMPLEMENTING"
|
|
165
|
+
assert "Starting implementation" in hint
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@test("get_transition_comment_hint returns None for non-transition")
|
|
169
|
+
def _():
|
|
170
|
+
import importlib.util
|
|
171
|
+
spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
|
|
172
|
+
mod = importlib.util.module_from_spec(spec)
|
|
173
|
+
spec.loader.exec_module(mod)
|
|
174
|
+
|
|
175
|
+
hint = mod.get_transition_comment_hint("issues.py list --status BACKLOG")
|
|
176
|
+
assert hint is None, "Should return None for non-transition commands"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Transition validation tests
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
print("\n## Transition validation")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@test("VALID_TRANSITIONS allows IMPLEMENTING -> REVIEW")
|
|
187
|
+
def _():
|
|
188
|
+
# Test the transition map directly from issues.py
|
|
189
|
+
import importlib.util
|
|
190
|
+
spec = importlib.util.spec_from_file_location("issues", SCRIPT_DIR / "issues.py")
|
|
191
|
+
mod = importlib.util.module_from_spec(spec)
|
|
192
|
+
spec.loader.exec_module(mod)
|
|
193
|
+
|
|
194
|
+
assert "REVIEW" in mod.VALID_TRANSITIONS["IMPLEMENTING"]
|
|
195
|
+
assert "BLOCKED" in mod.VALID_TRANSITIONS["IMPLEMENTING"]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@test("VALID_TRANSITIONS blocks BACKLOG -> REVIEW")
|
|
199
|
+
def _():
|
|
200
|
+
import importlib.util
|
|
201
|
+
spec = importlib.util.spec_from_file_location("issues", SCRIPT_DIR / "issues.py")
|
|
202
|
+
mod = importlib.util.module_from_spec(spec)
|
|
203
|
+
spec.loader.exec_module(mod)
|
|
204
|
+
|
|
205
|
+
assert "REVIEW" not in mod.VALID_TRANSITIONS["BACKLOG"]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@test("VALID_TRANSITIONS blocks READY -> REVIEW")
|
|
209
|
+
def _():
|
|
210
|
+
import importlib.util
|
|
211
|
+
spec = importlib.util.spec_from_file_location("issues", SCRIPT_DIR / "issues.py")
|
|
212
|
+
mod = importlib.util.module_from_spec(spec)
|
|
213
|
+
spec.loader.exec_module(mod)
|
|
214
|
+
|
|
215
|
+
assert "REVIEW" not in mod.VALID_TRANSITIONS["READY"]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
# Summary
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
print(f"\n{'='*50}")
|
|
223
|
+
print(f"Results: {passed} passed, {failed} failed, {passed + failed} total")
|
|
224
|
+
if failed > 0:
|
|
225
|
+
sys.exit(1)
|