shapeup-cli 0.3.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.
- checksums.yaml +7 -0
- data/bin/shapeup +7 -0
- data/install.md +94 -0
- data/lib/shapeup_cli/auth.rb +199 -0
- data/lib/shapeup_cli/client.rb +94 -0
- data/lib/shapeup_cli/commands/auth.rb +127 -0
- data/lib/shapeup_cli/commands/base.rb +113 -0
- data/lib/shapeup_cli/commands/comments.rb +86 -0
- data/lib/shapeup_cli/commands/config_cmd.rb +143 -0
- data/lib/shapeup_cli/commands/cycle.rb +66 -0
- data/lib/shapeup_cli/commands/issues.rb +336 -0
- data/lib/shapeup_cli/commands/login.rb +73 -0
- data/lib/shapeup_cli/commands/logout.rb +12 -0
- data/lib/shapeup_cli/commands/my_work.rb +38 -0
- data/lib/shapeup_cli/commands/orgs.rb +35 -0
- data/lib/shapeup_cli/commands/pitches.rb +170 -0
- data/lib/shapeup_cli/commands/scopes.rb +96 -0
- data/lib/shapeup_cli/commands/search.rb +33 -0
- data/lib/shapeup_cli/commands/setup.rb +85 -0
- data/lib/shapeup_cli/commands/tasks.rb +100 -0
- data/lib/shapeup_cli/commands.rb +161 -0
- data/lib/shapeup_cli/config.rb +155 -0
- data/lib/shapeup_cli/output.rb +230 -0
- data/lib/shapeup_cli.rb +137 -0
- data/skills/shapeup/SKILL.md +333 -0
- metadata +70 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShapeupCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Login
|
|
6
|
+
def self.run(args)
|
|
7
|
+
host = nil
|
|
8
|
+
profile_name = nil
|
|
9
|
+
args.each_with_index do |arg, i|
|
|
10
|
+
case arg
|
|
11
|
+
when "--host" then host = args[i + 1]
|
|
12
|
+
when /\A--host=(.+)\z/ then host = $1
|
|
13
|
+
when "--profile" then profile_name = args[i + 1]
|
|
14
|
+
when /\A--profile=(.+)\z/ then profile_name = $1
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
host ||= Config.host
|
|
19
|
+
token_response = ShapeupCli::Auth.login(host: host)
|
|
20
|
+
token = token_response["access_token"]
|
|
21
|
+
|
|
22
|
+
# Fetch orgs to let user pick one for this profile
|
|
23
|
+
client = Client.new(host: host, token: token)
|
|
24
|
+
result = client.call_tool("list_organisations")
|
|
25
|
+
data = Output.extract_data(result)
|
|
26
|
+
orgs = data.is_a?(Hash) ? (data["organisations"] || []) : []
|
|
27
|
+
|
|
28
|
+
if orgs.empty?
|
|
29
|
+
puts "No organisations found."
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# If only one org, use it automatically
|
|
34
|
+
org = if orgs.length == 1
|
|
35
|
+
orgs.first
|
|
36
|
+
else
|
|
37
|
+
puts "\nChoose an organisation for this profile:\n"
|
|
38
|
+
orgs.each_with_index do |o, i|
|
|
39
|
+
puts " #{i + 1}) #{o["name"]}"
|
|
40
|
+
end
|
|
41
|
+
print "\nEnter number (1-#{orgs.length}): "
|
|
42
|
+
choice = $stdin.gets&.strip&.to_i
|
|
43
|
+
orgs[choice - 1] if choice&.between?(1, orgs.length)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
unless org
|
|
47
|
+
puts "No organisation selected."
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Generate profile name from org name (slug it)
|
|
52
|
+
profile_name ||= org["name"].downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
|
|
53
|
+
|
|
54
|
+
Config.save_profile(
|
|
55
|
+
profile_name,
|
|
56
|
+
token: token,
|
|
57
|
+
host: host,
|
|
58
|
+
organisation_id: org["id"],
|
|
59
|
+
display_name: org["name"]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Also set as default
|
|
63
|
+
Config.switch_profile(profile_name)
|
|
64
|
+
|
|
65
|
+
puts "\nProfile '#{profile_name}' created and set as default."
|
|
66
|
+
puts " org: #{org["name"]} (#{org["id"]})"
|
|
67
|
+
puts " host: #{host}"
|
|
68
|
+
puts "\nTo add another profile, run 'shapeup login' again."
|
|
69
|
+
puts "To switch: 'shapeup auth switch <name>'"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShapeupCli
|
|
4
|
+
module Commands
|
|
5
|
+
class MyWork < Base
|
|
6
|
+
def self.metadata
|
|
7
|
+
{
|
|
8
|
+
command: "my-work",
|
|
9
|
+
path: "shapeup me",
|
|
10
|
+
short: "Show all pitches, scopes, and tasks assigned to you",
|
|
11
|
+
aliases: { "me" => "my-work" },
|
|
12
|
+
flags: [
|
|
13
|
+
{ name: "user", type: "string", usage: "User ID to show work for (default: me)" }
|
|
14
|
+
],
|
|
15
|
+
examples: [
|
|
16
|
+
"shapeup me",
|
|
17
|
+
"shapeup me --json",
|
|
18
|
+
"shapeup my-work --user 5"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def execute
|
|
24
|
+
assignee = extract_option("--user") || "me"
|
|
25
|
+
|
|
26
|
+
result = call_tool("show_my_work", assignee: assignee)
|
|
27
|
+
|
|
28
|
+
render result,
|
|
29
|
+
summary: assignee == "me" ? "My Work" : "Work for #{assignee}",
|
|
30
|
+
breadcrumbs: [
|
|
31
|
+
{ cmd: "shapeup pitch <id>", description: "View pitch details" },
|
|
32
|
+
{ cmd: "shapeup done <id>", description: "Complete a task" },
|
|
33
|
+
{ cmd: "shapeup tasks list --assignee me", description: "List just my tasks" }
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShapeupCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Orgs < Base
|
|
6
|
+
def self.metadata
|
|
7
|
+
{
|
|
8
|
+
command: "orgs",
|
|
9
|
+
path: "shapeup orgs",
|
|
10
|
+
short: "List organisations you have access to",
|
|
11
|
+
flags: [],
|
|
12
|
+
examples: [
|
|
13
|
+
"shapeup orgs",
|
|
14
|
+
"shapeup orgs --json"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execute
|
|
20
|
+
result = client.call_tool("list_organisations")
|
|
21
|
+
|
|
22
|
+
render result,
|
|
23
|
+
summary: "Organisations",
|
|
24
|
+
breadcrumbs: [
|
|
25
|
+
{ cmd: "shapeup pitches list --org <id>", description: "List pitches for an org" },
|
|
26
|
+
{ cmd: "shapeup cycles --org <id>", description: "List cycles for an org" }
|
|
27
|
+
]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
# Override: orgs doesn't need an org_id
|
|
32
|
+
def org_id = nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShapeupCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Pitches < Base
|
|
6
|
+
def self.metadata
|
|
7
|
+
{
|
|
8
|
+
command: "pitches",
|
|
9
|
+
path: "shapeup pitches",
|
|
10
|
+
short: "List and show pitches (packages)",
|
|
11
|
+
subcommands: [
|
|
12
|
+
{ name: "list", short: "List pitches (default)", path: "shapeup pitches list" },
|
|
13
|
+
{ name: "show", short: "Show pitch details with scopes and tasks", path: "shapeup pitches show <id>" },
|
|
14
|
+
{ name: "create", short: "Create a new pitch", path: "shapeup pitches create \"Title\" --stream \"Name\"" },
|
|
15
|
+
{ name: "help", short: "Show usage", path: "shapeup pitches help" }
|
|
16
|
+
],
|
|
17
|
+
flags: [
|
|
18
|
+
{ name: "status", type: "string", usage: "Filter by status: idea, framed, shaped" },
|
|
19
|
+
{ name: "cycle", type: "string", usage: "Filter by cycle ID" },
|
|
20
|
+
{ name: "limit", type: "integer", usage: "Limit number of results" },
|
|
21
|
+
{ name: "stream", type: "string", usage: "Stream name or ID (for create)" },
|
|
22
|
+
{ name: "appetite", type: "string", usage: "Appetite: unknown, small_batch, big_batch (for create, default: big_batch)" },
|
|
23
|
+
{ name: "cycle-id", type: "string", usage: "Assign to cycle ID (for create)" }
|
|
24
|
+
],
|
|
25
|
+
examples: [
|
|
26
|
+
"shapeup pitches list",
|
|
27
|
+
"shapeup pitches list --status shaped",
|
|
28
|
+
"shapeup pitches list --cycle 5",
|
|
29
|
+
"shapeup pitch 42",
|
|
30
|
+
"shapeup pitch 42 --json",
|
|
31
|
+
"shapeup pitches create \"Redesign Search\" --stream \"Platform\"",
|
|
32
|
+
"shapeup pitches create \"Auth Overhaul\" --stream \"Platform\" --appetite small_batch"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def execute
|
|
38
|
+
subcommand = positional_arg(0)
|
|
39
|
+
|
|
40
|
+
case subcommand
|
|
41
|
+
when "show" then show
|
|
42
|
+
when "create" then create
|
|
43
|
+
when "list", nil then list
|
|
44
|
+
when "help" then help
|
|
45
|
+
else
|
|
46
|
+
subcommand.match?(/\A\d+\z/) ? show(subcommand) : list
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
def list
|
|
52
|
+
cycle_id = extract_option("--cycle")
|
|
53
|
+
status = extract_option("--status")
|
|
54
|
+
limit = extract_option("--limit")&.to_i
|
|
55
|
+
args = {}
|
|
56
|
+
args[:cycle] = cycle_id if cycle_id
|
|
57
|
+
|
|
58
|
+
result = call_tool("list_packages", **args)
|
|
59
|
+
data = Output.extract_data(result)
|
|
60
|
+
packages = data.is_a?(Hash) ? (data["packages"] || []) : Array(data)
|
|
61
|
+
|
|
62
|
+
# Client-side filtering
|
|
63
|
+
packages = packages.select { |p| p["status"] == status } if status
|
|
64
|
+
packages = packages.first(limit) if limit
|
|
65
|
+
|
|
66
|
+
summary = "Pitches"
|
|
67
|
+
summary += " (#{status})" if status
|
|
68
|
+
summary += " in cycle #{cycle_id}" if cycle_id
|
|
69
|
+
summary += " — #{packages.length} results"
|
|
70
|
+
|
|
71
|
+
render_list(packages, summary)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def show(id = nil)
|
|
75
|
+
id ||= positional_arg(1) || abort("Usage: shapeup pitches show <id>")
|
|
76
|
+
|
|
77
|
+
result = call_tool("show_package", package: id.to_s)
|
|
78
|
+
|
|
79
|
+
render result,
|
|
80
|
+
summary: "Pitch ##{id}",
|
|
81
|
+
breadcrumbs: [
|
|
82
|
+
{ cmd: "shapeup scopes list --pitch #{id}", description: "List scopes" },
|
|
83
|
+
{ cmd: "shapeup scopes create --pitch #{id} \"Title\"", description: "Add a scope" },
|
|
84
|
+
{ cmd: "shapeup todo \"Task\" --pitch #{id}", description: "Add a task" },
|
|
85
|
+
{ cmd: "shapeup tasks list --pitch #{id}", description: "List all tasks" }
|
|
86
|
+
]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def render_list(packages, summary)
|
|
90
|
+
# Build a simplified list for display
|
|
91
|
+
items = packages.map do |p|
|
|
92
|
+
{
|
|
93
|
+
"id" => p["id"],
|
|
94
|
+
"title" => p["title"],
|
|
95
|
+
"status" => p["status"],
|
|
96
|
+
"appetite" => p["appetite"],
|
|
97
|
+
"cycle" => p["cycle"]
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
Output.render(
|
|
102
|
+
{ "content" => [ { "type" => "text", "text" => JSON.generate(items) } ] },
|
|
103
|
+
breadcrumbs: [
|
|
104
|
+
{ cmd: "shapeup pitch <id>", description: "View pitch details" },
|
|
105
|
+
{ cmd: "shapeup pitches list --status shaped", description: "Show shaped pitches" },
|
|
106
|
+
{ cmd: "shapeup pitches list --cycle <id>", description: "Filter by cycle" },
|
|
107
|
+
{ cmd: "shapeup pitches help", description: "Show usage" }
|
|
108
|
+
],
|
|
109
|
+
mode: @mode,
|
|
110
|
+
summary: summary
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def create
|
|
115
|
+
title = positional_arg(1) || abort("Usage: shapeup pitches create \"Title\" --stream \"Name\"")
|
|
116
|
+
stream = extract_option("--stream") || abort("Usage: shapeup pitches create \"Title\" --stream \"Name\"")
|
|
117
|
+
appetite = extract_option("--appetite")
|
|
118
|
+
cycle_id = extract_option("--cycle-id")
|
|
119
|
+
|
|
120
|
+
args = { title: title, stream: stream }
|
|
121
|
+
args[:appetite] = appetite if appetite
|
|
122
|
+
args[:cycle] = cycle_id if cycle_id
|
|
123
|
+
|
|
124
|
+
result = call_tool("create_package", **args)
|
|
125
|
+
|
|
126
|
+
render result,
|
|
127
|
+
summary: "Pitch created",
|
|
128
|
+
breadcrumbs: [
|
|
129
|
+
{ cmd: "shapeup scopes create --pitch <id> \"Title\"", description: "Add a scope" },
|
|
130
|
+
{ cmd: "shapeup todo \"Task\" --pitch <id>", description: "Add a task" },
|
|
131
|
+
{ cmd: "shapeup pitch <id>", description: "View pitch details" }
|
|
132
|
+
]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def help
|
|
136
|
+
puts <<~HELP
|
|
137
|
+
Usage: shapeup pitches <subcommand> [options]
|
|
138
|
+
|
|
139
|
+
Subcommands:
|
|
140
|
+
list List pitches (default)
|
|
141
|
+
show <id> Show pitch details
|
|
142
|
+
create "Title" Create a new pitch
|
|
143
|
+
help This help
|
|
144
|
+
|
|
145
|
+
Filters (list):
|
|
146
|
+
--status <s> Filter by status: idea, framed, shaped
|
|
147
|
+
--cycle <id> Filter by cycle
|
|
148
|
+
--limit <n> Limit results
|
|
149
|
+
|
|
150
|
+
Options (create):
|
|
151
|
+
--stream <name> Stream name or ID (required)
|
|
152
|
+
--appetite <a> unknown, small_batch, big_batch (default: big_batch)
|
|
153
|
+
--cycle-id <id> Assign to a cycle
|
|
154
|
+
|
|
155
|
+
Output:
|
|
156
|
+
--json JSON envelope with breadcrumbs
|
|
157
|
+
--md Markdown table
|
|
158
|
+
--agent Raw data only
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
shapeup pitches list
|
|
162
|
+
shapeup pitches list --status shaped
|
|
163
|
+
shapeup pitch 42
|
|
164
|
+
shapeup pitches create "Redesign Search" --stream "Platform"
|
|
165
|
+
shapeup pitches create "Auth Overhaul" --stream "Platform" --appetite small_batch
|
|
166
|
+
HELP
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShapeupCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Scopes < Base
|
|
6
|
+
def self.metadata
|
|
7
|
+
{
|
|
8
|
+
command: "scopes",
|
|
9
|
+
path: "shapeup scopes",
|
|
10
|
+
short: "Manage scopes within a pitch",
|
|
11
|
+
subcommands: [
|
|
12
|
+
{ name: "list", short: "List scopes for a pitch", path: "shapeup scopes list --pitch <id>" },
|
|
13
|
+
{ name: "create", short: "Create a new scope", path: "shapeup scopes create --pitch <id> \"Title\"" },
|
|
14
|
+
{ name: "update", short: "Update scope title or color", path: "shapeup scopes update <id> --title \"New\"" },
|
|
15
|
+
{ name: "position", short: "Update hill chart position (0-100)", path: "shapeup scopes position <id> <position>" }
|
|
16
|
+
],
|
|
17
|
+
flags: [
|
|
18
|
+
{ name: "pitch", type: "string", usage: "Pitch ID (required for list and create)" },
|
|
19
|
+
{ name: "title", type: "string", usage: "Scope title (for create/update)" },
|
|
20
|
+
{ name: "color", type: "string", usage: "Hex color code (for update)" }
|
|
21
|
+
],
|
|
22
|
+
examples: [
|
|
23
|
+
"shapeup scopes list --pitch 42",
|
|
24
|
+
"shapeup scopes create --pitch 42 \"User onboarding\"",
|
|
25
|
+
"shapeup scopes update 7 --title \"Revised onboarding\"",
|
|
26
|
+
"shapeup scopes position 7 50"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def execute
|
|
32
|
+
subcommand = positional_arg(0)
|
|
33
|
+
|
|
34
|
+
case subcommand
|
|
35
|
+
when "create" then create
|
|
36
|
+
when "update" then update
|
|
37
|
+
when "position" then position
|
|
38
|
+
when "list", nil then list
|
|
39
|
+
else list
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
def list
|
|
45
|
+
pitch_id = extract_option("--pitch") || abort("Usage: shapeup scopes list --pitch <id>")
|
|
46
|
+
|
|
47
|
+
result = call_tool("show_package", package: pitch_id.to_s)
|
|
48
|
+
|
|
49
|
+
render result,
|
|
50
|
+
summary: "Scopes for Pitch ##{pitch_id}",
|
|
51
|
+
breadcrumbs: [
|
|
52
|
+
{ cmd: "shapeup scopes create --pitch #{pitch_id} \"Title\"", description: "Add a scope" },
|
|
53
|
+
{ cmd: "shapeup tasks list --pitch #{pitch_id}", description: "List all tasks" }
|
|
54
|
+
]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def create
|
|
58
|
+
pitch_id = extract_option("--pitch") || abort("Usage: shapeup scopes create --pitch <id> \"Title\"")
|
|
59
|
+
title = positional_arg(1) || abort("Usage: shapeup scopes create --pitch <id> \"Title\"")
|
|
60
|
+
|
|
61
|
+
result = call_tool("create_scope", package: pitch_id.to_s, title: title)
|
|
62
|
+
|
|
63
|
+
render result,
|
|
64
|
+
summary: "Scope created",
|
|
65
|
+
breadcrumbs: [
|
|
66
|
+
{ cmd: "shapeup tasks list --scope <id>", description: "List tasks in this scope" },
|
|
67
|
+
{ cmd: "shapeup todo \"Task\" --pitch #{pitch_id} --scope <id>", description: "Add a task" }
|
|
68
|
+
]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def position
|
|
72
|
+
scope_id = positional_arg(1) || abort("Usage: shapeup scopes position <id> <position>")
|
|
73
|
+
pos = positional_arg(2) || abort("Usage: shapeup scopes position <id> <position>")
|
|
74
|
+
|
|
75
|
+
result = call_tool("update_scope_position", scope: scope_id.to_s, position: pos.to_f)
|
|
76
|
+
|
|
77
|
+
render result,
|
|
78
|
+
summary: "Scope ##{scope_id} moved to position #{pos}",
|
|
79
|
+
breadcrumbs: [
|
|
80
|
+
{ cmd: "shapeup scopes list --pitch <id>", description: "List scopes" }
|
|
81
|
+
]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def update
|
|
85
|
+
scope_id = positional_arg(1) || abort("Usage: shapeup scopes update <id> [--title \"New\"] [--color #hex]")
|
|
86
|
+
args = { scope: scope_id.to_s }
|
|
87
|
+
args[:title] = extract_option("--title") if @remaining.include?("--title")
|
|
88
|
+
args[:color] = extract_option("--color") if @remaining.include?("--color")
|
|
89
|
+
|
|
90
|
+
result = call_tool("update_scope", **args)
|
|
91
|
+
|
|
92
|
+
render result, summary: "Scope ##{scope_id} updated"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShapeupCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Search < Base
|
|
6
|
+
def self.metadata
|
|
7
|
+
{
|
|
8
|
+
command: "search",
|
|
9
|
+
path: "shapeup search",
|
|
10
|
+
short: "Search across pitches, scopes, tasks, tickets, and users",
|
|
11
|
+
flags: [],
|
|
12
|
+
examples: [
|
|
13
|
+
"shapeup search \"onboarding\"",
|
|
14
|
+
"shapeup search \"login bug\" --json"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execute
|
|
20
|
+
query = positional_arg(0) || abort("Usage: shapeup search \"query\"")
|
|
21
|
+
|
|
22
|
+
result = call_tool("search", query: query)
|
|
23
|
+
|
|
24
|
+
render result,
|
|
25
|
+
summary: "Search: #{query}",
|
|
26
|
+
breadcrumbs: [
|
|
27
|
+
{ cmd: "shapeup pitch <id>", description: "View a pitch" },
|
|
28
|
+
{ cmd: "shapeup cycle show <id>", description: "View a cycle" }
|
|
29
|
+
]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShapeupCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Setup < Base
|
|
6
|
+
SKILL_SOURCE = File.expand_path("../../../skills/shapeup/SKILL.md", __dir__)
|
|
7
|
+
|
|
8
|
+
def self.metadata
|
|
9
|
+
{
|
|
10
|
+
command: "setup",
|
|
11
|
+
path: "shapeup setup",
|
|
12
|
+
short: "Install ShapeUp skills into an AI agent",
|
|
13
|
+
subcommands: [
|
|
14
|
+
{ name: "claude", short: "Install skill into Claude Code (~/.claude/skills/)", path: "shapeup setup claude" },
|
|
15
|
+
{ name: "cursor", short: "Install skill into Cursor (.cursor/skills/)", path: "shapeup setup cursor" },
|
|
16
|
+
{ name: "project", short: "Install skill into current project (.claude/skills/)", path: "shapeup setup project" }
|
|
17
|
+
],
|
|
18
|
+
flags: [],
|
|
19
|
+
examples: [
|
|
20
|
+
"shapeup setup claude",
|
|
21
|
+
"shapeup setup project",
|
|
22
|
+
"shapeup setup cursor"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def execute
|
|
28
|
+
target = positional_arg(0)
|
|
29
|
+
|
|
30
|
+
case target
|
|
31
|
+
when "claude" then install_global("claude")
|
|
32
|
+
when "cursor" then install_global("cursor")
|
|
33
|
+
when "project" then install_project
|
|
34
|
+
else
|
|
35
|
+
puts <<~HELP
|
|
36
|
+
Usage: shapeup setup <target>
|
|
37
|
+
|
|
38
|
+
Targets:
|
|
39
|
+
claude Install skill globally into ~/.claude/skills/
|
|
40
|
+
cursor Install skill globally into ~/.cursor/skills/
|
|
41
|
+
project Install skill into .claude/skills/ (current directory)
|
|
42
|
+
|
|
43
|
+
This copies the ShapeUp SKILL.md so your AI agent knows how to
|
|
44
|
+
use the ShapeUp CLI when triggered by relevant phrases.
|
|
45
|
+
HELP
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
def install_global(agent)
|
|
51
|
+
dir = File.join(Dir.home, ".#{agent}", "skills", "shapeup")
|
|
52
|
+
install_skill(dir, "~/.#{agent}/skills/shapeup/")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def install_project
|
|
56
|
+
dir = File.join(".claude", "skills", "shapeup")
|
|
57
|
+
install_skill(dir, ".claude/skills/shapeup/")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def install_skill(dir, display_path)
|
|
61
|
+
unless File.exist?(SKILL_SOURCE)
|
|
62
|
+
abort "SKILL.md not found at #{SKILL_SOURCE}. Is the CLI installed correctly?"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
FileUtils.mkdir_p(dir)
|
|
66
|
+
dest = File.join(dir, "SKILL.md")
|
|
67
|
+
|
|
68
|
+
if File.exist?(dest)
|
|
69
|
+
puts "Updating #{display_path}SKILL.md"
|
|
70
|
+
else
|
|
71
|
+
puts "Installing #{display_path}SKILL.md"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
FileUtils.cp(SKILL_SOURCE, dest)
|
|
75
|
+
|
|
76
|
+
puts "Done! Restart your agent session to pick up the skill."
|
|
77
|
+
puts
|
|
78
|
+
puts "The skill triggers on phrases like:"
|
|
79
|
+
puts " 'my tasks', 'list pitches', 'cycle progress', 'shapeup', etc."
|
|
80
|
+
puts
|
|
81
|
+
puts "Your agent will use the ShapeUp CLI to handle these requests."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShapeupCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Tasks < Base
|
|
6
|
+
def self.metadata
|
|
7
|
+
{
|
|
8
|
+
command: "tasks",
|
|
9
|
+
path: "shapeup tasks",
|
|
10
|
+
short: "Manage tasks within scopes and pitches",
|
|
11
|
+
aliases: { "todo" => "tasks create", "done" => "tasks complete" },
|
|
12
|
+
subcommands: [
|
|
13
|
+
{ name: "list", short: "List tasks", path: "shapeup tasks list" },
|
|
14
|
+
{ name: "create", short: "Create a task", path: "shapeup todo \"Description\" --pitch <id>" },
|
|
15
|
+
{ name: "complete", short: "Mark task(s) as complete", path: "shapeup done <id> [<id>...]" }
|
|
16
|
+
],
|
|
17
|
+
flags: [
|
|
18
|
+
{ name: "pitch", type: "string", usage: "Pitch ID (required for create)" },
|
|
19
|
+
{ name: "scope", type: "string", usage: "Scope ID (optional filter or target)" },
|
|
20
|
+
{ name: "assignee", type: "string", usage: "User ID or 'me' (for list)" }
|
|
21
|
+
],
|
|
22
|
+
examples: [
|
|
23
|
+
"shapeup tasks list --pitch 42",
|
|
24
|
+
"shapeup tasks list --assignee me",
|
|
25
|
+
"shapeup todo \"Fix login bug\" --pitch 42 --scope 7",
|
|
26
|
+
"shapeup done 123",
|
|
27
|
+
"shapeup done 123 124 125"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def execute
|
|
33
|
+
subcommand = positional_arg(0)
|
|
34
|
+
|
|
35
|
+
case subcommand
|
|
36
|
+
when "create" then create
|
|
37
|
+
when "complete" then complete
|
|
38
|
+
when "list", nil then list
|
|
39
|
+
else list
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
def list
|
|
45
|
+
scope_id = extract_option("--scope")
|
|
46
|
+
pitch_id = extract_option("--pitch")
|
|
47
|
+
assignee = extract_option("--assignee")
|
|
48
|
+
|
|
49
|
+
args = {}
|
|
50
|
+
args[:scope] = scope_id if scope_id
|
|
51
|
+
args[:package] = pitch_id if pitch_id
|
|
52
|
+
args[:assignee] = assignee if assignee
|
|
53
|
+
|
|
54
|
+
result = call_tool("list_tasks", **args)
|
|
55
|
+
|
|
56
|
+
render result,
|
|
57
|
+
summary: "Tasks",
|
|
58
|
+
breadcrumbs: [
|
|
59
|
+
{ cmd: "shapeup todo \"Description\" --pitch <id>", description: "Create a task" },
|
|
60
|
+
{ cmd: "shapeup done <id>", description: "Complete a task" }
|
|
61
|
+
]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def create
|
|
65
|
+
pitch_id = extract_option("--pitch") || abort("Usage: shapeup todo \"Description\" --pitch <id> [--scope <id>]")
|
|
66
|
+
scope_id = extract_option("--scope")
|
|
67
|
+
description = positional_arg(1) || abort("Usage: shapeup todo \"Description\" --pitch <id>")
|
|
68
|
+
|
|
69
|
+
args = { package: pitch_id.to_s, description: description }
|
|
70
|
+
args[:scope] = scope_id if scope_id
|
|
71
|
+
|
|
72
|
+
result = call_tool("create_task", **args)
|
|
73
|
+
|
|
74
|
+
render result,
|
|
75
|
+
summary: "Task created",
|
|
76
|
+
breadcrumbs: [
|
|
77
|
+
{ cmd: "shapeup done <id>", description: "Mark as complete" },
|
|
78
|
+
{ cmd: "shapeup tasks list --pitch #{pitch_id}", description: "List all tasks" }
|
|
79
|
+
]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def complete
|
|
83
|
+
ids = positional_args.drop(1) # drop "complete" subcommand
|
|
84
|
+
ids = positional_args if ids.empty? # handle shortcut: shapeup done <id>
|
|
85
|
+
|
|
86
|
+
abort("Usage: shapeup done <id> [<id>...]") if ids.empty?
|
|
87
|
+
|
|
88
|
+
ids.each do |id|
|
|
89
|
+
result = call_tool("complete_task", task: id.to_s)
|
|
90
|
+
|
|
91
|
+
render result,
|
|
92
|
+
summary: "Task ##{id} completed",
|
|
93
|
+
breadcrumbs: [
|
|
94
|
+
{ cmd: "shapeup me", description: "Show remaining work" }
|
|
95
|
+
]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|