@flydocs/cli 0.5.0-beta.9 → 0.6.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +1259 -370
  3. package/package.json +1 -1
  4. package/template/.claude/agents/implementation-agent.md +0 -1
  5. package/template/.claude/agents/pm-agent.md +0 -1
  6. package/template/.claude/agents/research-agent.md +0 -1
  7. package/template/.claude/agents/review-agent.md +0 -1
  8. package/template/.claude/commands/flydocs-setup.md +109 -26
  9. package/template/.claude/commands/flydocs-upgrade.md +330 -0
  10. package/template/.claude/skills/flydocs-cloud/SKILL.md +53 -38
  11. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +5 -5
  12. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +8 -24
  13. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +14 -30
  14. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +10 -32
  15. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +15 -25
  16. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +21 -58
  17. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +26 -37
  18. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +24 -31
  19. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +39 -0
  20. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +10 -19
  21. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +103 -170
  22. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +6 -59
  23. package/template/.claude/skills/flydocs-cloud/scripts/link.py +16 -35
  24. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +21 -28
  25. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +16 -77
  26. package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +19 -0
  27. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +21 -33
  28. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +24 -38
  29. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +19 -0
  30. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +10 -19
  31. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +36 -50
  32. package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +68 -0
  33. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +41 -0
  34. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +11 -52
  35. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +16 -27
  36. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +23 -52
  37. package/template/.env.example +16 -7
  38. package/template/.flydocs/config.json +1 -1
  39. package/template/.flydocs/version +1 -1
  40. package/template/CHANGELOG.md +144 -0
  41. package/template/manifest.json +5 -3
@@ -1,8 +1,9 @@
1
1
  ---
2
2
  name: flydocs-cloud
3
3
  description: |
4
- Connected issue management via Linear GraphQL API.
5
- Implements the FlyDocs mechanism contract with extended cloud-only operations.
4
+ Connected issue management via FlyDocs Relay API.
5
+ Implements the FlyDocs mechanism contract with extended cloud operations.
6
+ All provider translation (Linear, Jira) happens server-side.
6
7
  triggers:
7
8
  - create issue
8
9
  - transition
@@ -18,7 +19,9 @@ triggers:
18
19
 
19
20
  # FlyDocs Cloud Mechanism
20
21
 
21
- Issues managed in Linear. Reads config from `.flydocs/config.json` and API key from `.env`.
22
+ Issues managed via the FlyDocs Relay API. The relay handles provider translation server-side scripts are thin REST wrappers.
23
+
24
+ Reads config from `.flydocs/config.json` and API key (`FLYDOCS_API_KEY`) from `.env`.
22
25
 
23
26
  ## Script Catalog
24
27
 
@@ -26,42 +29,51 @@ All scripts: `python3 .claude/skills/flydocs-cloud/scripts/<script>`
26
29
 
27
30
  ### Shared Contract Scripts
28
31
 
29
- | Script | Usage | Output |
30
- |--------|-------|--------|
31
- | `create_issue.py` | `--title "..." --type feature [--description "..."] [--description-file PATH] [--priority 0-4] [--estimate 1-5] [--assignee STR] [--triage] \| stdin` | `{id, identifier, title, url}` |
32
- | `transition.py` | `<ref> <STATUS> "<comment>"` | `{success, issue, previousStatus, newStatus}` |
33
- | `comment.py` | `<ref> ["<comment>"] \| stdin` | `{success, commentId}` |
34
- | `list_issues.py` | `[--status STATUS[,STATUS]] [--active] [--project ID] [--milestone ID] [--assignee STR] [--mine] [--limit N]` | `[{id, identifier, title, status, assignee, priority, dueDate, milestone, milestoneId, milestoneSortOrder, project, projectId}]` |
35
- | `get_issue.py` | `<ref> [--fields basic\|full]` | `{id, identifier, title, description, status, assignee, priority, estimate, dueDate, milestone, milestoneId, project, projectId, comments[]}` |
36
- | `assign.py` | `<ref> <assignee>` | `{success, issue, assignee}` |
37
- | `update_description.py` | `<ref> --text "..." \| --file PATH \| stdin` | `{success, issue}` |
38
-
39
- ### Extended Scripts (cloud-only)
40
-
41
- | Script | Usage | Output |
42
- |--------|-------|--------|
43
- | `update_issue.py` | `<ref> [--title "..."] [--priority 0-4] [--estimate 1-5] [--assignee STR] [--state STATUS] [--description "..."] [--description-file PATH] [--comment "..."]` | `{success, issue, updated[]}` |
44
- | `estimate.py` | `<ref> <1-5>` | `{success, issue, estimate}` |
45
- | `priority.py` | `<ref> <0-4>` | `{success, issue, priority}` |
46
- | `link.py` | `<ref> <related_ref> <type>` | `{success, type}` |
47
- | `project_update.py` | `--health STATUS --body "..." [--body-file PATH]` | `{success, id}` |
48
- | `list_projects.py` | `[--active] [--all]` | `[{id, name, state}]` — `--all` bypasses product scope |
49
- | `create_project.py` | `--name "..." [--description "..."]` | `{id, name, url}` |
50
- | `assign_cycle.py` | `<ref> [cycle_id]` | `{success, issue, cycle}` |
51
- | `list_cycles.py` | `[--active]` | `[{id, name, number, startsAt, endsAt}]` |
52
- | `list_milestones.py` | `[--all]` | `[{id, name, targetDate}]` |
53
- | `create_milestone.py` | `--name "..." [--project ID] [--target-date DATE]` | `{id, name}` — defaults to first activeProject |
54
- | `assign_milestone.py` | `<ref> <milestone_id>` | `{success, issue, milestone}` |
32
+ | Script | Usage | Output |
33
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | `create_issue.py` | `--title "..." --type feature [--description "..."] [--description-file PATH] [--priority 0-4] [--estimate 1-5] [--assignee STR] [--labels "a,b"] [--triage] \| stdin` | `{id, identifier, title, url}` |
35
+ | `transition.py` | `<ref> <STATUS> "<comment>"` | `{success, issue, previousStatus, newStatus}` |
36
+ | `comment.py` | `<ref> ["<comment>"] \| stdin` | `{success, commentId}` |
37
+ | `list_issues.py` | `[--status STATUS[,STATUS]] [--active] [--project ID] [--milestone ID] [--assignee STR] [--mine] [--limit N]` | `[{id, identifier, title, status, assignee, priority, dueDate, milestone, milestoneId, milestoneSortOrder, project, projectId}]` |
38
+ | `get_issue.py` | `<ref> [--fields basic\|full]` | `{id, identifier, title, description, status, assignee, priority, estimate, dueDate, milestone, milestoneId, project, projectId, comments[]}` |
39
+ | `assign.py` | `<ref> <assignee>` | `{success, issue, assignee}` |
40
+ | `update_description.py` | `<ref> --text "..." \| --file PATH \| stdin` | `{success, issue}` |
41
+
42
+ ### Extended Scripts
43
+
44
+ | Script | Usage | Output |
45
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
46
+ | `update_issue.py` | `<ref> [--title "..."] [--priority 0-4] [--estimate 1-5] [--assignee STR] [--state STATUS] [--description "..."] [--description-file PATH] [--labels "a,b"] [--comment "..."]` | `{success, issue, updated[]}` |
47
+ | `estimate.py` | `<ref> <1-5>` | `{success, issue, estimate}` |
48
+ | `priority.py` | `<ref> <0-4>` | `{success, issue, priority}` |
49
+ | `link.py` | `<ref> <related_ref> <type>` | `{success, type}` |
50
+ | `project_update.py` | `--health STATUS --body "..." [--body-file PATH]` | `{success, id}` |
51
+ | `list_projects.py` | `[--active] [--all]` | `[{id, name, state}]` — `--all` bypasses product scope |
52
+ | `create_project.py` | `--name "..." [--description "..."]` | `{id, name, url}` |
53
+ | `assign_cycle.py` | `<ref> [cycle_id]` | `{success, issue, cycle}` |
54
+ | `list_cycles.py` | `[--active]` | `[{id, name, number, startsAt, endsAt}]` |
55
+ | `list_milestones.py` | `[--all]` | `[{id, name, targetDate}]` |
56
+ | `create_milestone.py` | `--name "..." [--project ID] [--target-date DATE]` | `{id, name}` |
57
+ | `assign_milestone.py` | `<ref> <milestone_id>` | `{success, issue, milestone}` |
58
+
59
+ ### Workspace Scripts
60
+
61
+ | Script | Usage | Output |
62
+ | ---------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------- |
63
+ | `list_teams.py` | (no args) | `[{id, name, key}]` |
64
+ | `create_team.py` | `--name "..." [--key KEY] [--description "..."] [--parent <team_id>]` | `{id, name, key}` — `--parent` creates a sub-team |
65
+ | `set_team.py` | `<team_id>` | `{success}` — updates relay preference and local config `provider.teamId` |
66
+ | `list_labels.py` | (no args) | `[{id, name, color}]` — requires team to be set first |
67
+ | `set_labels.py` | `--defaults '["a"]' --type-map '{"feature":["F"],...}' \| stdin` | `{success, validated, defaults, typeMap}` — stores label config on relay |
55
68
 
56
69
  ### Script Notes
57
70
 
58
- - **`list_issues.py` product scope**: Automatically scopes results using config cascade `activeProjects` `product.labelIds` team-wide. Explicit `--project` overrides. All flags (`--active`, `--status`, `--mine`, `--assignee`) filter within the scoped results.
59
- - **`list_issues.py --active`**: Returns all non-terminal issues (excludes Done, Archived, Canceled, Duplicate). Replaces `status_summary.py` — group results by status field instead.
60
- - **`list_issues.py --milestone`**: Filter issues by project milestone ID. Get IDs from `list_milestones.py`.
71
+ - **How it works**: Scripts call the FlyDocs Relay REST API, which translates to the provider (Linear, Jira) server-side. Same interface and output as before the transport changed from direct GraphQL to managed REST.
72
+ - **`list_issues.py --active`**: Returns all non-terminal issues (excludes Done, Archived, Canceled, Duplicate).
61
73
  - **`list_issues.py --status`**: Accepts comma-separated statuses: `--status READY,IMPLEMENTING,BLOCKED`
62
- - **`get_issue.py --fields basic`**: Skips comment fetch for faster responses when comments aren't needed.
63
- - **`update_issue.py`**: Bulk update — sets multiple fields in a single API call. Prefer over separate `estimate.py`/`priority.py`/`assign.py` calls when updating more than one field.
64
- - **Shell-safe text input**: For descriptions, comments, or any text with special characters (apostrophes, quotes, parentheses), pipe via stdin with a single-quoted heredoc instead of `--text`:
74
+ - **`get_issue.py --fields basic`**: Skips comment fetch for faster responses.
75
+ - **`update_issue.py`**: Bulk update — sets multiple fields in a single API call. Prefer over separate scripts when updating more than one field.
76
+ - **Shell-safe text input**: For text with special characters, pipe via stdin with a single-quoted heredoc:
65
77
  ```bash
66
78
  python3 update_description.py ENG-123 <<'EOF'
67
79
  Description with 'apostrophes', (parens), and "quotes"
@@ -83,14 +95,17 @@ All scripts: `python3 .claude/skills/flydocs-cloud/scripts/<script>`
83
95
 
84
96
  ### Issue Reference
85
97
 
86
- `<ref>`: Provider identifier, e.g., `ENG-123`. Resolved via Linear search API.
98
+ `<ref>`: Provider identifier, e.g., `ENG-123`. Resolved server-side by the relay.
87
99
 
88
100
  ## Error Handling
89
101
 
90
102
  Exit 0 = success (JSON on stdout). Exit 1 = error (message on stderr).
91
103
  Network errors: exponential backoff, 3 retries, 2s base delay.
104
+ Relay errors include `code` and optional `provider_error` for debugging.
92
105
 
93
106
  ## Configuration
94
107
 
95
- Reads from `.flydocs/config.json`: tier, provider.teamId, statusMapping, issueLabels.
96
- Reads `LINEAR_API_KEY` from environment or `.env` / `.env.local`.
108
+ Reads from `.flydocs/config.json`: tier, relay URL override.
109
+ Reads `FLYDOCS_API_KEY` from environment or `.env` / `.env.local`.
110
+
111
+ Optional: `FLYDOCS_RELAY_URL` env var or `relay.url` in config to override the base URL (e.g., for local development).
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Connected issue management via Linear — cloud mechanism for FlyDocs
2
+ description: Connected issue management via FlyDocs Relay API — cloud mechanism for FlyDocs
3
3
  alwaysApply: true
4
4
  ---
5
5
 
@@ -7,7 +7,7 @@ alwaysApply: true
7
7
 
8
8
  # FlyDocs Cloud Mechanism
9
9
 
10
- Issues managed in Linear. Reads config from `.flydocs/config.json` and API key from `.env`.
10
+ Issues managed via FlyDocs Relay API. Reads config from `.flydocs/config.json` and API key (`FLYDOCS_API_KEY`) from `.env`.
11
11
 
12
12
  ## Shared Contract Scripts
13
13
 
@@ -23,7 +23,7 @@ All scripts: `python3 .claude/skills/flydocs-cloud/scripts/<script>`
23
23
  | `assign.py` | `<ref> <assignee>` |
24
24
  | `update_description.py` | `<ref> --text "..." \| --file PATH` |
25
25
 
26
- ## Extended Scripts (cloud-only)
26
+ ## Extended Scripts
27
27
 
28
28
  | Script | Key Arguments |
29
29
  |--------|---------------|
@@ -46,5 +46,5 @@ Network errors: exponential backoff, 3 retries, 2s base delay.
46
46
 
47
47
  ## Configuration
48
48
 
49
- Reads from `.flydocs/config.json`: tier, provider.teamId, statusMapping, issueLabels.
50
- Reads `LINEAR_API_KEY` from environment or `.env` / `.env.local`.
49
+ Reads from `.flydocs/config.json`: tier, relay URL override.
50
+ Reads `FLYDOCS_API_KEY` from environment or `.env` / `.env.local`.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Assign an issue to a person."""
2
+ """Assign an issue to a user via the FlyDocs Relay API."""
3
3
 
4
4
  import sys
5
5
  from pathlib import Path
@@ -10,29 +10,13 @@ from flydocs_api import get_client, output_json, fail
10
10
  if len(sys.argv) < 3:
11
11
  fail("Usage: assign.py <ref> <assignee>")
12
12
 
13
- ref, assignee_query = sys.argv[1], sys.argv[2]
13
+ ref, assignee = sys.argv[1], sys.argv[2]
14
14
  client = get_client()
15
15
 
16
- issue_uuid = client.resolve_issue_id(ref)
17
- if not issue_uuid:
18
- fail(f"Issue not found: {ref}")
16
+ result = client.post(f"/issues/{ref}/assign", {"assignee": assignee})
19
17
 
20
- user_id, user_name = client.resolve_user_id(assignee_query)
21
- if not user_id:
22
- fail(f"User not found: {assignee_query}")
23
-
24
- result = client.query(
25
- """mutation($id: String!, $assigneeId: String!) {
26
- issueUpdate(id: $id, input: { assigneeId: $assigneeId }) {
27
- success
28
- issue { identifier }
29
- }
30
- }""",
31
- {"id": issue_uuid, "assigneeId": user_id},
32
- )
33
-
34
- if not result.get("data", {}).get("issueUpdate", {}).get("success"):
35
- fail(f"Failed to assign: {result}")
36
-
37
- issue = result["data"]["issueUpdate"]["issue"]
38
- output_json({"success": True, "issue": issue["identifier"], "assignee": user_name})
18
+ output_json({
19
+ "success": result.get("success", True),
20
+ "issue": result.get("issue", ref),
21
+ "assignee": result.get("assignee", assignee),
22
+ })
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Assign an issue to a cycle (sprint)."""
2
+ """Assign a cycle to an issue via the FlyDocs Relay API."""
3
3
 
4
4
  import sys
5
5
  from pathlib import Path
@@ -11,34 +11,18 @@ if len(sys.argv) < 2:
11
11
  fail("Usage: assign_cycle.py <ref> [cycle_id]")
12
12
 
13
13
  ref = sys.argv[1]
14
+ cycle_id = sys.argv[2] if len(sys.argv) >= 3 else None
15
+
14
16
  client = get_client()
15
17
 
16
- issue_uuid = client.resolve_issue_id(ref)
17
- if not issue_uuid:
18
- fail(f"Issue not found: {ref}")
19
-
20
- # Use specified cycle or active cycle
21
- cycle_id = sys.argv[2] if len(sys.argv) > 2 else None
22
- cycle_name = cycle_id or ""
23
- if not cycle_id:
24
- cycle = client.get_active_cycle()
25
- if not cycle:
26
- fail("No active cycle found")
27
- cycle_id = cycle["id"]
28
- cycle_name = cycle["name"]
29
-
30
- result = client.query(
31
- """mutation($id: String!, $cycleId: String!) {
32
- issueUpdate(id: $id, input: { cycleId: $cycleId }) {
33
- success
34
- issue { identifier }
35
- }
36
- }""",
37
- {"id": issue_uuid, "cycleId": cycle_id},
38
- )
39
-
40
- if not result.get("data", {}).get("issueUpdate", {}).get("success"):
41
- fail(f"Failed to assign cycle: {result}")
42
-
43
- issue = result["data"]["issueUpdate"]["issue"]
44
- output_json({"success": True, "issue": issue["identifier"], "cycle": cycle_name})
18
+ body: dict = {}
19
+ if cycle_id:
20
+ body["cycleId"] = cycle_id
21
+
22
+ result = client.put(f"/issues/{ref}/cycle", body)
23
+
24
+ output_json({
25
+ "success": result.get("success", True),
26
+ "issue": result.get("issue", ref),
27
+ "cycle": result.get("cycle", ""),
28
+ })
@@ -1,44 +1,22 @@
1
1
  #!/usr/bin/env python3
2
- """Assign an issue to a milestone."""
2
+ """Assign a milestone to an issue via the FlyDocs Relay API."""
3
3
 
4
- import argparse
5
4
  import sys
6
5
  from pathlib import Path
7
6
 
8
7
  sys.path.insert(0, str(Path(__file__).parent))
9
8
  from flydocs_api import get_client, output_json, fail
10
9
 
11
- parser = argparse.ArgumentParser(description="Assign issue to milestone")
12
- parser.add_argument("ref", help="Issue reference (e.g., ENG-123)")
13
- parser.add_argument("milestone_id", nargs="?", default=None,
14
- help="Milestone UUID (positional)")
15
- parser.add_argument("--milestone", dest="milestone_flag", default=None,
16
- help="Milestone UUID (flag)")
17
- args = parser.parse_args()
18
-
19
- milestone_id = args.milestone_flag or args.milestone_id
20
- if not milestone_id:
21
- fail("Milestone ID required. Usage: assign_milestone.py <ref> <milestone_id> or --milestone <id>")
10
+ if len(sys.argv) < 3:
11
+ fail("Usage: assign_milestone.py <ref> <milestone_id>")
22
12
 
13
+ ref, milestone_id = sys.argv[1], sys.argv[2]
23
14
  client = get_client()
24
15
 
25
- issue_uuid = client.resolve_issue_id(args.ref)
26
- if not issue_uuid:
27
- fail(f"Issue not found: {args.ref}")
28
-
29
- result = client.query(
30
- """mutation($id: String!, $milestoneId: String!) {
31
- issueUpdate(id: $id, input: { projectMilestoneId: $milestoneId }) {
32
- success
33
- issue { identifier }
34
- }
35
- }""",
36
- {"id": issue_uuid, "milestoneId": milestone_id},
37
- )
38
-
39
- update = result.get("data", {}).get("issueUpdate", {})
40
- if not update.get("success"):
41
- fail(f"Failed to assign milestone: {result}")
16
+ result = client.put(f"/issues/{ref}/milestone", {"milestoneId": milestone_id})
42
17
 
43
- issue = update.get("issue", {})
44
- output_json({"success": True, "issue": issue.get("identifier", args.ref), "milestone": milestone_id})
18
+ output_json({
19
+ "success": result.get("success", True),
20
+ "issue": result.get("issue", ref),
21
+ "milestone": result.get("milestone", ""),
22
+ })
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Add a comment to an issue."""
2
+ """Add a comment to an issue via the FlyDocs Relay API."""
3
3
 
4
4
  import sys
5
5
  from pathlib import Path
@@ -8,32 +8,22 @@ sys.path.insert(0, str(Path(__file__).parent))
8
8
  from flydocs_api import get_client, output_json, fail
9
9
 
10
10
  if len(sys.argv) < 2:
11
- fail("Usage: comment.py <ref> [<comment>] (or pipe via stdin)")
11
+ fail("Usage: comment.py <ref> [comment] | stdin")
12
12
 
13
13
  ref = sys.argv[1]
14
- body = sys.argv[2] if len(sys.argv) > 2 else ""
15
- if not body and not sys.stdin.isatty():
16
- body = sys.stdin.read().strip()
17
- if not body:
18
- fail("Provide comment as argument or pipe via stdin")
19
- client = get_client()
20
-
21
- issue_uuid = client.resolve_issue_id(ref)
22
- if not issue_uuid:
23
- fail(f"Issue not found: {ref}")
24
14
 
25
- result = client.query(
26
- """mutation($id: String!, $body: String!) {
27
- commentCreate(input: { issueId: $id, body: $body }) {
28
- success
29
- comment { id }
30
- }
31
- }""",
32
- {"id": issue_uuid, "body": body},
33
- )
15
+ # Comment from arg > stdin
16
+ if len(sys.argv) >= 3:
17
+ body = sys.argv[2]
18
+ elif not sys.stdin.isatty():
19
+ body = sys.stdin.read().strip()
20
+ else:
21
+ fail("Provide comment as argument or via stdin")
34
22
 
35
- data = result.get("data", {}).get("commentCreate", {})
36
- if not data.get("success"):
37
- fail(f"Failed to add comment: {result}")
23
+ client = get_client()
24
+ result = client.post(f"/issues/{ref}/comment", {"body": body})
38
25
 
39
- output_json({"success": True, "commentId": data.get("comment", {}).get("id", "")})
26
+ output_json({
27
+ "success": result.get("success", True),
28
+ "commentId": result.get("commentId", ""),
29
+ })
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Create a new issue in Linear."""
2
+ """Create a new issue via the FlyDocs Relay API."""
3
3
 
4
4
  import argparse
5
5
  import sys
@@ -18,7 +18,7 @@ def main():
18
18
  parser.add_argument("--priority", type=int, default=3, choices=range(5))
19
19
  parser.add_argument("--estimate", type=int, default=0, choices=[0, 1, 2, 3, 5])
20
20
  parser.add_argument("--assignee", default=None)
21
- parser.add_argument("--project", default=None)
21
+ parser.add_argument("--labels", default=None, help="Comma-separated ad-hoc label names")
22
22
  parser.add_argument("--triage", action="store_true")
23
23
  args = parser.parse_args()
24
24
 
@@ -32,68 +32,31 @@ def main():
32
32
  elif not description and not sys.stdin.isatty():
33
33
  description = sys.stdin.read().strip()
34
34
 
35
- client = get_client()
36
-
37
- issue_input = {
38
- "teamId": client.team_id,
35
+ body: dict = {
39
36
  "title": args.title,
40
- "description": description,
37
+ "type": args.issue_type,
41
38
  "priority": args.priority,
42
39
  }
40
+ if description:
41
+ body["description"] = description
43
42
  if args.estimate:
44
- issue_input["estimate"] = args.estimate
45
-
46
- # Labels
47
- label_ids = []
48
- cat_id = client.get_category_label_id(args.issue_type)
49
- if cat_id:
50
- label_ids.append(cat_id)
51
- if args.triage:
52
- triage_id = client.get_other_label_id("triage")
53
- if triage_id:
54
- label_ids.append(triage_id)
55
- if label_ids:
56
- issue_input["labelIds"] = label_ids
57
-
58
- # Project
59
- project_id = args.project
60
- if not project_id:
61
- active = client.workspace.get("activeProjects", [])
62
- if active:
63
- project_id = active[0]
64
- if project_id:
65
- issue_input["projectId"] = project_id
66
-
67
- # Assignee
43
+ body["estimate"] = args.estimate
68
44
  if args.assignee:
69
- user_id, _ = client.resolve_user_id(args.assignee)
70
- if user_id:
71
- issue_input["assigneeId"] = user_id
72
-
73
- mutation = """mutation($input: IssueCreateInput!) {
74
- issueCreate(input: $input) {
75
- success
76
- issue { id identifier title url }
77
- }
78
- }"""
79
-
80
- result = client.query(mutation, {"input": issue_input})
81
-
82
- data = result.get("data", {}).get("issueCreate", {})
83
- if not data.get("success"):
84
- # Retry without labels if team mismatch (stale label IDs in config)
85
- errors = result.get("errors", [])
86
- label_error = any("label" in (e.get("message", "") or "").lower() for e in errors)
87
- if label_error and "labelIds" in issue_input:
88
- print("Label IDs rejected — retrying without labels...", file=sys.stderr)
89
- del issue_input["labelIds"]
90
- result = client.query(mutation, {"input": issue_input})
91
- data = result.get("data", {}).get("issueCreate", {})
92
- if not data.get("success"):
93
- fail(f"Failed to create issue: {result}")
45
+ body["assignee"] = args.assignee
46
+ if args.labels:
47
+ body["labels"] = [l.strip() for l in args.labels.split(",") if l.strip()]
48
+ if args.triage:
49
+ body["triage"] = True
94
50
 
95
- issue = data["issue"]
96
- output_json({"id": issue["id"], "identifier": issue["identifier"], "title": issue["title"], "url": issue["url"]})
51
+ client = get_client()
52
+ result = client.post("/issues", body)
53
+
54
+ output_json({
55
+ "id": result["id"],
56
+ "identifier": result["identifier"],
57
+ "title": result["title"],
58
+ "url": result["url"],
59
+ })
97
60
 
98
61
 
99
62
  if __name__ == "__main__":
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Create a milestone."""
2
+ """Create a milestone via the FlyDocs Relay API."""
3
3
 
4
4
  import argparse
5
5
  import sys
@@ -8,39 +8,28 @@ from pathlib import Path
8
8
  sys.path.insert(0, str(Path(__file__).parent))
9
9
  from flydocs_api import get_client, output_json, fail
10
10
 
11
- parser = argparse.ArgumentParser(description="Create milestone")
12
- parser.add_argument("--name", required=True)
13
- parser.add_argument("--project", default="", help="Project ID (defaults to first activeProject)")
14
- parser.add_argument("--target-date", default=None, help="YYYY-MM-DD")
15
- args = parser.parse_args()
16
-
17
- client = get_client()
18
-
19
- project_id = args.project
20
- if not project_id:
21
- active = client.workspace.get("activeProjects", [])
22
- if active:
23
- project_id = active[0]
24
- else:
25
- fail("No --project specified and no activeProjects in config")
26
-
27
- milestone_input = {"name": args.name, "projectId": project_id}
28
- if args.target_date:
29
- milestone_input["targetDate"] = args.target_date
30
-
31
- result = client.query(
32
- """mutation($input: ProjectMilestoneCreateInput!) {
33
- projectMilestoneCreate(input: $input) {
34
- success
35
- projectMilestone { id name }
36
- }
37
- }""",
38
- {"input": milestone_input},
39
- )
40
-
41
- data = result.get("data", {}).get("projectMilestoneCreate", {})
42
- if not data.get("success"):
43
- fail(f"Failed to create milestone: {result}")
44
-
45
- ms = data["projectMilestone"]
46
- output_json({"id": ms["id"], "name": ms["name"]})
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="Create milestone")
14
+ parser.add_argument("--name", required=True)
15
+ parser.add_argument("--project", default=None, help="Project ID")
16
+ parser.add_argument("--target-date", default=None, dest="target_date")
17
+ args = parser.parse_args()
18
+
19
+ body: dict = {"name": args.name}
20
+ if args.project:
21
+ body["projectId"] = args.project
22
+ if args.target_date:
23
+ body["targetDate"] = args.target_date
24
+
25
+ client = get_client()
26
+ result = client.post("/milestones", body)
27
+
28
+ output_json({
29
+ "id": result["id"],
30
+ "name": result["name"],
31
+ })
32
+
33
+
34
+ if __name__ == "__main__":
35
+ main()
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Create a new project."""
2
+ """Create a project via the FlyDocs Relay API."""
3
3
 
4
4
  import argparse
5
5
  import sys
@@ -8,33 +8,26 @@ from pathlib import Path
8
8
  sys.path.insert(0, str(Path(__file__).parent))
9
9
  from flydocs_api import get_client, output_json, fail
10
10
 
11
- parser = argparse.ArgumentParser(description="Create project")
12
- parser.add_argument("--name", required=True)
13
- parser.add_argument("--description", default="")
14
- args = parser.parse_args()
15
-
16
- client = get_client()
17
-
18
- project_input = {
19
- "name": args.name,
20
- "teamIds": [client.team_id],
21
- }
22
- if args.description:
23
- project_input["description"] = args.description
24
-
25
- result = client.query(
26
- """mutation($input: ProjectCreateInput!) {
27
- projectCreate(input: $input) {
28
- success
29
- project { id name url }
30
- }
31
- }""",
32
- {"input": project_input},
33
- )
34
-
35
- data = result.get("data", {}).get("projectCreate", {})
36
- if not data.get("success"):
37
- fail(f"Failed to create project: {result}")
38
-
39
- project = data["project"]
40
- output_json({"id": project["id"], "name": project["name"], "url": project["url"]})
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="Create project")
14
+ parser.add_argument("--name", required=True)
15
+ parser.add_argument("--description", default=None)
16
+ args = parser.parse_args()
17
+
18
+ body: dict = {"name": args.name}
19
+ if args.description:
20
+ body["description"] = args.description
21
+
22
+ client = get_client()
23
+ result = client.post("/projects", body)
24
+
25
+ output_json({
26
+ "id": result["id"],
27
+ "name": result["name"],
28
+ "url": result.get("url", ""),
29
+ })
30
+
31
+
32
+ if __name__ == "__main__":
33
+ main()