shapeup-cli 0.3.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 167d55101c1c27e4ce1bd6dc8e6f52599937489e4a6218c248f901a150282273
4
- data.tar.gz: d511b7da6d77e6437ee431951dd591f59b6ed70807be55618564c79b2e9a828b
3
+ metadata.gz: d8ff0645627f89f9f4478145566d6386d3137125bfee4f0baea077476da8d7e7
4
+ data.tar.gz: 94260b420442e04c89015fc58ac2536ba312d177876fe7d310064de341e24a3c
5
5
  SHA512:
6
- metadata.gz: 6cc0fc54f960c09e7338a5ca49ed2eb61820b9246438c2533ce1f1686f5dd4e4b053aed3f63a79e5855a046bff5b859efcf137b1f8db0bdceb8a014220a2c0aa
7
- data.tar.gz: f4dbcbd90cd4ac33b2d9096fb683b08e76f71f042184faee5a4b91bd7a88802655a593c5b4a60ade0925ad14d07c5c8e7d37991dd31327a6f8c6adef598c403f
6
+ metadata.gz: 0c334924b153062646d61d9f09f7a98a9a6932a364fe461b76284392de112242437f5d0381d16b6b8882b6d9b4073dd590172a3389dc0393f05d810ff9117eba
7
+ data.tar.gz: 2cced2c61abe233c7bdaef8b9918239105724ca1b59d4524beec770709d269c814343216e6c7d5ee8545a146229db73201f161b7d711ca0e18eff3895cfb6801
@@ -17,31 +17,12 @@ module ShapeupCli
17
17
  @request_id = 0
18
18
  end
19
19
 
20
- # Initialise the MCP session
21
- def initialize_session
22
- call_method("initialize", protocolVersion: MCP_PROTOCOL_VERSION)
23
- end
24
-
25
- # List available tools (useful for --agent --help introspection)
26
- def list_tools
27
- call_method("tools/list")
28
- end
29
-
30
- # Call an MCP tool by name with arguments
20
+ # Call an MCP tool by name with arguments. The only method the CLI uses —
21
+ # the MCP session/introspection/resource calls were never wired up.
31
22
  def call_tool(name, **arguments)
32
23
  call_method("tools/call", name: name, arguments: arguments)
33
24
  end
34
25
 
35
- # List available resources
36
- def list_resources
37
- call_method("resources/list")
38
- end
39
-
40
- # Read a resource by URI
41
- def read_resource(uri)
42
- call_method("resources/read", uri: uri)
43
- end
44
-
45
26
  private
46
27
  def call_method(method, **params)
47
28
  @request_id += 1
@@ -65,18 +46,28 @@ module ShapeupCli
65
46
  http.open_timeout = 5
66
47
  http.read_timeout = 30
67
48
 
68
- response = http.request(request)
49
+ response = with_network_error_handling { http.request(request) }
50
+ handle_response(response)
51
+ end
52
+
53
+ def with_network_error_handling
54
+ yield
55
+ rescue Net::OpenTimeout, Net::ReadTimeout
56
+ raise ApiError, "The server took too long to respond. Please try again."
57
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT => e
58
+ raise ApiError, "Couldn't reach #{@host} (#{e.class}). Check your connection and 'shapeup config show'."
59
+ end
69
60
 
70
- case response
71
- when Net::HTTPUnauthorized
72
- raise AuthError, "Authentication failed — run 'shapeup login'"
73
- when Net::HTTPForbidden
74
- raise PermissionError, "Check your subscription or permissions"
75
- when Net::HTTPTooManyRequests
76
- raise RateLimitError, "Too many requests"
61
+ def handle_response(response)
62
+ case response.code.to_i
63
+ when 401 then raise AuthError, "Authentication failed — run 'shapeup login'"
64
+ when 403 then raise PermissionError, "Check your subscription or permissions"
65
+ when 429 then raise RateLimitError, "Too many requests slow down and retry"
66
+ when 200..299 then nil
67
+ else raise ApiError, "The server returned an error (HTTP #{response.code})."
77
68
  end
78
69
 
79
- parsed = JSON.parse(response.body)
70
+ parsed = parse_json(response.body)
80
71
 
81
72
  if parsed["error"]
82
73
  message = parsed["error"]["message"] || "Unknown error"
@@ -90,5 +81,11 @@ module ShapeupCli
90
81
 
91
82
  parsed["result"]
92
83
  end
84
+
85
+ def parse_json(body)
86
+ JSON.parse(body.to_s)
87
+ rescue JSON::ParserError
88
+ raise ApiError, "The server returned an unreadable response."
89
+ end
93
90
  end
94
91
  end
@@ -34,7 +34,9 @@ module ShapeupCli
34
34
  end
35
35
 
36
36
  def org_id
37
- resolve_org(@org_id || Config.organisation_id || abort("No organisation set. Run 'shapeup orgs' to see available orgs, then pass --org <id> or --org <name>."))
37
+ # Memoised: a name id lookup hits the API, and call_tool asks for
38
+ # org_id on every call (a multi-arg command makes several).
39
+ @resolved_org_id ||= resolve_org(@org_id || Config.organisation_id || abort("No organisation set. Run 'shapeup orgs' to see available orgs, then pass --org <id> or --org <name>."))
38
40
  end
39
41
 
40
42
  def call_tool(name, **args)
@@ -114,6 +116,45 @@ module ShapeupCli
114
116
  !!@remaining.delete(flag)
115
117
  end
116
118
 
119
+ # Consume a confirmation bypass flag (--yes / -y). Call before reading
120
+ # positionals so the flag isn't mistaken for one.
121
+ def assume_yes?
122
+ # both forms; -y does not start with "--" so it must be consumed explicitly
123
+ consume_flag("--yes") | consume_flag("-y")
124
+ end
125
+
126
+ # Shared assign/unassign for any assignable (Task, Scope, Package,
127
+ # Issue). --user defaults to 'me'.
128
+ def assign_to(type, noun)
129
+ id = positional_arg(1) || abort("Usage: shapeup #{noun} assign <id> [--user <id>]")
130
+ user_id = extract_option("--user") || "me"
131
+ result = call_tool("assign_user", assignable_type: type, assignable_id: id.to_s, user_id: user_id.to_s)
132
+ render result, summary: "Assigned #{noun} ##{id}"
133
+ end
134
+
135
+ def unassign_from(type, noun)
136
+ id = positional_arg(1) || abort("Usage: shapeup #{noun} unassign <id> [--user <id>]")
137
+ user_id = extract_option("--user") || "me"
138
+ result = call_tool("unassign_user", assignable_type: type, assignable_id: id.to_s, user_id: user_id.to_s)
139
+ render result, summary: "Unassigned #{noun} ##{id}"
140
+ end
141
+
142
+ # Guard a destructive action. `assume_yes` skips the prompt (the caller
143
+ # passes the result of assume_yes?). With a TTY we ask for confirmation;
144
+ # without one we refuse rather than delete unattended — so agents and
145
+ # scripts must opt in explicitly with --yes.
146
+ def confirm_destructive!(action, assume_yes)
147
+ return if assume_yes
148
+
149
+ unless $stdin.tty?
150
+ abort "Refusing to #{action} without confirmation. Re-run with --yes to proceed."
151
+ end
152
+
153
+ $stderr.print "#{action}? This cannot be undone [y/N]: "
154
+ answer = $stdin.gets&.strip&.downcase
155
+ abort "Aborted." unless answer == "y" || answer == "yes"
156
+ end
157
+
117
158
  # Parse --comments/--no-comments + --comments-limit N into MCP args.
118
159
  def comment_flags
119
160
  args = {}
@@ -10,7 +10,9 @@ module ShapeupCli
10
10
  short: "List and add comments on issues, pitches, scopes, and tasks",
11
11
  subcommands: [
12
12
  { name: "list", short: "List comments", path: "shapeup comments list --issue <id>" },
13
- { name: "add", short: "Add a comment", path: 'shapeup comments add --issue <id> "Comment text"' }
13
+ { name: "add", short: "Add a comment", path: 'shapeup comments add --issue <id> "Comment text"' },
14
+ { name: "edit", short: "Edit your own comment", path: 'shapeup comments edit <comment_id> "New text"' },
15
+ { name: "remove", short: "Delete your own comment", path: "shapeup comments remove <comment_id>" }
14
16
  ],
15
17
  flags: [
16
18
  { name: "issue", type: "string", usage: "Issue ID" },
@@ -22,7 +24,9 @@ module ShapeupCli
22
24
  "shapeup comments list --issue 42",
23
25
  'shapeup comments add --issue 42 "Investigated — this is a CSS issue in the navbar"',
24
26
  "shapeup comments list --pitch 10",
25
- 'shapeup comments add --pitch 10 "Shaped and ready for betting"'
27
+ 'shapeup comments add --pitch 10 "Shaped and ready for betting"',
28
+ 'shapeup comments edit 88 "Updated: this is a navbar z-index issue"',
29
+ "shapeup comments remove 88"
26
30
  ]
27
31
  }
28
32
  end
@@ -32,6 +36,8 @@ module ShapeupCli
32
36
 
33
37
  case subcommand
34
38
  when "add" then add
39
+ when "edit" then edit
40
+ when "remove" then remove
35
41
  when "list", nil then list
36
42
  else list
37
43
  end
@@ -63,6 +69,25 @@ module ShapeupCli
63
69
  ]
64
70
  end
65
71
 
72
+ def edit
73
+ comment_id = positional_arg(1) || abort('Usage: shapeup comments edit <comment_id> "New text"')
74
+ text = positional_arg(2) || abort('Usage: shapeup comments edit <comment_id> "New text"')
75
+
76
+ result = call_tool("update_comment", comment_id: comment_id.to_s, content: text)
77
+
78
+ render result, summary: "Comment ##{comment_id} updated"
79
+ end
80
+
81
+ def remove
82
+ yes = assume_yes?
83
+ comment_id = positional_arg(1) || abort("Usage: shapeup comments remove <comment_id> [--yes]")
84
+ confirm_destructive!("Delete comment ##{comment_id}", yes)
85
+
86
+ result = call_tool("delete_comment", comment_id: comment_id.to_s)
87
+
88
+ render result, summary: "Comment ##{comment_id} deleted"
89
+ end
90
+
66
91
  def resolve_commentable
67
92
  issue = extract_option("--issue")
68
93
  pitch = extract_option("--pitch")
@@ -44,7 +44,7 @@ module ShapeupCli
44
44
 
45
45
  case key
46
46
  when "org"
47
- resolved = resolve_org_value(value)
47
+ resolved = resolve_org(value)
48
48
  Config.save_config("organisation_id", resolved.to_s)
49
49
  puts "Default organisation set to #{resolved}"
50
50
  when "host"
@@ -113,31 +113,13 @@ module ShapeupCli
113
113
  org_value = positional_arg(1) || extract_option("--org") || @org_id
114
114
  abort("Usage: shapeup config init <org>") unless org_value
115
115
 
116
- resolved = resolve_org_value(org_value)
116
+ resolved = resolve_org(org_value)
117
117
 
118
118
  FileUtils.mkdir_p(".shapeup")
119
119
  File.write(".shapeup/config.json", JSON.pretty_generate(organisation_id: resolved.to_s))
120
120
  puts "Created .shapeup/config.json (org: #{resolved})"
121
121
  puts "All commands in this directory will use this organisation by default."
122
122
  end
123
-
124
- def resolve_org_value(value)
125
- return value if value.to_s.match?(/\A\d+\z/)
126
-
127
- # Need to resolve name to ID
128
- result = client.call_tool("list_organisations")
129
- data = Output.extract_data(result)
130
- orgs = data.is_a?(Hash) ? (data["organisations"] || []) : Array(data)
131
-
132
- match = orgs.find { |o| o["name"]&.downcase == value.downcase }
133
-
134
- if match
135
- match["id"]
136
- else
137
- names = orgs.map { |o| " #{o["id"]} #{o["name"]}" }.join("\n")
138
- abort "Organisation '#{value}' not found. Available:\n#{names}"
139
- end
140
- end
141
123
  end
142
124
  end
143
125
  end
@@ -10,15 +10,24 @@ module ShapeupCli
10
10
  short: "List and show cycles",
11
11
  subcommands: [
12
12
  { name: "list", short: "List cycles (default)", path: "shapeup cycles" },
13
- { name: "show", short: "Show cycle details with pitches and progress", path: "shapeup cycle show <id>" }
13
+ { name: "show", short: "Show cycle details with pitches and progress", path: "shapeup cycle show <id>" },
14
+ { name: "create", short: "Create a cycle", path: "shapeup cycle create \"Title\" --start <date> --end <date>" },
15
+ { name: "edit", short: "Update a cycle's title or dates", path: "shapeup cycle edit <id> --title \"New\"" },
16
+ { name: "delete", short: "Delete a cycle (only if it has no pitches)", path: "shapeup cycle delete <id>" }
14
17
  ],
15
18
  flags: [
16
- { name: "status", type: "string", usage: "Filter by status: active, past, future, all" }
19
+ { name: "status", type: "string", usage: "Filter by status: active, past, future, all" },
20
+ { name: "title", type: "string", usage: "Cycle title (create/edit)" },
21
+ { name: "start", type: "string", usage: "Start date YYYY-MM-DD (create/edit)" },
22
+ { name: "end", type: "string", usage: "End date YYYY-MM-DD (create/edit)" }
17
23
  ],
18
24
  examples: [
19
25
  "shapeup cycles",
20
26
  "shapeup cycles --status active",
21
- "shapeup cycle show 12"
27
+ "shapeup cycle show 12",
28
+ "shapeup cycle create \"Cycle 13\" --start 2026-08-03 --end 2026-09-11",
29
+ "shapeup cycle edit 12 --title \"Renamed\"",
30
+ "shapeup cycle delete 12"
22
31
  ]
23
32
  }
24
33
  end
@@ -27,7 +36,10 @@ module ShapeupCli
27
36
  subcommand = positional_arg(0)
28
37
 
29
38
  case subcommand
30
- when "show" then show
39
+ when "show" then show
40
+ when "create" then create
41
+ when "edit" then edit
42
+ when "delete" then delete
31
43
  when "list", nil then list
32
44
  else
33
45
  subcommand.match?(/\A\d+\z/) ? show(subcommand) : list
@@ -35,6 +47,40 @@ module ShapeupCli
35
47
  end
36
48
 
37
49
  private
50
+ def create
51
+ title = positional_arg(1) || abort("Usage: shapeup cycle create \"Title\" --start <date> --end <date>")
52
+ start_date = extract_option("--start") || abort("Missing --start <YYYY-MM-DD>")
53
+ end_date = extract_option("--end") || abort("Missing --end <YYYY-MM-DD>")
54
+
55
+ result = call_tool("create_cycle", title: title, start_date: start_date, end_date: end_date)
56
+
57
+ render result,
58
+ summary: "Cycle created",
59
+ breadcrumbs: [ { cmd: "shapeup cycle show <id>", description: "View the cycle" } ]
60
+ end
61
+
62
+ def edit
63
+ id = positional_arg(1) || abort("Usage: shapeup cycle edit <id> [--title ..] [--start ..] [--end ..]")
64
+ args = { cycle: id.to_s }
65
+ args[:title] = extract_option("--title") if @remaining.include?("--title")
66
+ args[:start_date] = extract_option("--start") if @remaining.include?("--start")
67
+ args[:end_date] = extract_option("--end") if @remaining.include?("--end")
68
+
69
+ result = call_tool("update_cycle", **args)
70
+
71
+ render result, summary: "Cycle ##{id} updated"
72
+ end
73
+
74
+ def delete
75
+ yes = assume_yes?
76
+ id = positional_arg(1) || abort("Usage: shapeup cycle delete <id> [--yes]")
77
+ confirm_destructive!("Delete cycle ##{id}", yes)
78
+
79
+ result = call_tool("delete_cycle", cycle: id.to_s)
80
+
81
+ render result, summary: "Cycle ##{id} deleted"
82
+ end
83
+
38
84
  def list
39
85
  status = extract_option("--status")
40
86
  args = {}
@@ -15,6 +15,7 @@ module ShapeupCli
15
15
  { name: "create", short: "Create an issue", path: "shapeup issues create \"Title\" --stream <id>" },
16
16
  { name: "update", short: "Update an issue", path: "shapeup issues update <id> --title \"New title\"" },
17
17
  { name: "move", short: "Move to a kanban column", path: "shapeup issues move <id> --column <id>" },
18
+ { name: "columns", short: "List kanban columns and their IDs", path: "shapeup issues columns" },
18
19
  { name: "done", short: "Mark issue as done", path: "shapeup issues done <id>" },
19
20
  { name: "close", short: "Close issue (won't fix)", path: "shapeup issues close <id>" },
20
21
  { name: "reopen", short: "Reopen a done/closed issue", path: "shapeup issues reopen <id>" },
@@ -78,6 +79,7 @@ module ShapeupCli
78
79
  when "create" then create
79
80
  when "update" then update
80
81
  when "move" then move
82
+ when "columns" then columns
81
83
  when "done" then mark_done
82
84
  when "close" then close
83
85
  when "reopen" then reopen
@@ -187,6 +189,16 @@ module ShapeupCli
187
189
  ]
188
190
  end
189
191
 
192
+ def columns
193
+ result = call_tool("list_kanban_columns")
194
+
195
+ render result,
196
+ summary: "Kanban Columns",
197
+ breadcrumbs: [
198
+ { cmd: "shapeup issues move <id> --column <id>", description: "Move an issue to a column" }
199
+ ]
200
+ end
201
+
190
202
  def move
191
203
  id = positional_arg(1) || abort("Usage: shapeup issues move <id> --column <id>")
192
204
  column_id = extract_option("--column") || abort("Usage: shapeup issues move <id> --column <id>")
@@ -230,7 +242,7 @@ module ShapeupCli
230
242
  def close
231
243
  id = positional_arg(1) || abort("Usage: shapeup issues close <id>")
232
244
 
233
- result = call_tool("close_issue", issue: id.to_s, resolution: "closed")
245
+ result = call_tool("close_issue", issue: id.to_s, resolution: "wont_do")
234
246
 
235
247
  render result,
236
248
  summary: "Issue ##{id} closed",
@@ -331,7 +343,9 @@ module ShapeupCli
331
343
  end
332
344
 
333
345
  def delete
334
- id = positional_arg(1) || abort("Usage: shapeup issues delete <id>")
346
+ yes = assume_yes?
347
+ id = positional_arg(1) || abort("Usage: shapeup issues delete <id> [--yes]")
348
+ confirm_destructive!("Delete issue ##{id}", yes)
335
349
 
336
350
  result = call_tool("delete_issue", issue: id.to_s)
337
351
 
@@ -356,8 +370,8 @@ module ShapeupCli
356
370
  end
357
371
 
358
372
  def add_to_pitch
359
- id = positional_arg(1) || abort('Usage: shapeup issues add-to-pitch <id> --pitch <pitch_id>')
360
- pitch_id = extract_option("--pitch") || abort('Usage: shapeup issues add-to-pitch <id> --pitch <pitch_id>')
373
+ id = positional_arg(1) || abort("Usage: shapeup issues add-to-pitch <id> --pitch <pitch_id>")
374
+ pitch_id = extract_option("--pitch") || abort("Usage: shapeup issues add-to-pitch <id> --pitch <pitch_id>")
361
375
 
362
376
  result = call_tool("add_issue_to_pitch", issue: id.to_s, package: pitch_id.to_s)
363
377
 
@@ -12,16 +12,26 @@ module ShapeupCli
12
12
  { name: "list", short: "List pitches (default)", path: "shapeup pitches list" },
13
13
  { name: "show", short: "Show pitch details with scopes and tasks", path: "shapeup pitches show <id>" },
14
14
  { name: "create", short: "Create a new pitch", path: "shapeup pitches create \"Title\" --stream \"Name\"" },
15
+ { name: "update", short: "Update a pitch's title, status, appetite, stream, or cycle", path: "shapeup pitches update <id> --status shaped" },
16
+ { name: "extend", short: "Link a pitch as a continuation of a predecessor", path: "shapeup pitches extend <id> --predecessor <id>" },
17
+ { name: "detach", short: "Remove a pitch's predecessor link", path: "shapeup pitches detach <id>" },
18
+ { name: "delete", short: "Delete a pitch (must have no scopes)", path: "shapeup pitches delete <id>" },
19
+ { name: "assign", short: "Assign a user", path: "shapeup pitches assign <id> [--user <id>]" },
20
+ { name: "unassign", short: "Unassign a user", path: "shapeup pitches unassign <id> [--user <id>]" },
15
21
  { name: "help", short: "Show usage", path: "shapeup pitches help" }
16
22
  ],
17
23
  flags: [
18
- { name: "status", type: "string", usage: "Filter by status: idea, framed, shaped" },
19
- { name: "cycle", type: "string", usage: "Filter by cycle ID" },
24
+ { name: "user", type: "string", usage: "User ID or 'me' (assign/unassign; defaults to me)" },
25
+ { name: "status", type: "string", usage: "Filter by, or set, status: idea, framed, shaped" },
26
+ { name: "cycle", type: "string", usage: "Filter by cycle ID (list), or assign to cycle ID (update)" },
20
27
  { name: "tag", type: "string", usage: "Filter by tag name" },
21
28
  { name: "limit", type: "integer", usage: "Limit number of results" },
22
- { name: "stream", type: "string", usage: "Stream name or ID (for create)" },
23
- { name: "appetite", type: "string", usage: "Appetite: unknown, small_batch, big_batch (for create, default: big_batch)" },
29
+ { name: "stream", type: "string", usage: "Stream name or ID (for create/update)" },
30
+ { name: "appetite", type: "string", usage: "Appetite: unknown, small_batch, big_batch (create default: big_batch)" },
24
31
  { name: "cycle-id", type: "string", usage: "Assign to cycle ID (for create)" },
32
+ { name: "title", type: "string", usage: "New title (for update)" },
33
+ { name: "content", type: "string", usage: "New description (for update)" },
34
+ { name: "predecessor", type: "string", usage: "Predecessor pitch ID (for extend)" },
25
35
  { name: "no-comments", type: "bool", usage: "Hide embedded comments on show (default: show)" },
26
36
  { name: "comments-limit", type: "integer", usage: "Max comments to embed on show (default: 10, max: 50)" }
27
37
  ],
@@ -33,7 +43,13 @@ module ShapeupCli
33
43
  "shapeup pitch 42",
34
44
  "shapeup pitch 42 --json",
35
45
  "shapeup pitches create \"Redesign Search\" --stream \"Platform\"",
36
- "shapeup pitches create \"Auth Overhaul\" --stream \"Platform\" --appetite small_batch"
46
+ "shapeup pitches create \"Auth Overhaul\" --stream \"Platform\" --appetite small_batch",
47
+ "shapeup pitches update 42 --status shaped",
48
+ "shapeup pitches update 42 --title \"Redesign Search v2\" --appetite small_batch",
49
+ "shapeup pitches update 42 --cycle 5",
50
+ "shapeup pitches extend 42 --predecessor 30",
51
+ "shapeup pitches detach 42",
52
+ "shapeup pitches delete 42"
37
53
  ]
38
54
  }
39
55
  end
@@ -44,6 +60,12 @@ module ShapeupCli
44
60
  case subcommand
45
61
  when "show" then show
46
62
  when "create" then create
63
+ when "update" then update
64
+ when "extend" then extend_pitch
65
+ when "detach" then detach
66
+ when "delete" then delete
67
+ when "assign" then assign_to("Package", "pitches")
68
+ when "unassign" then unassign_from("Package", "pitches")
47
69
  when "list", nil then list
48
70
  when "help" then help
49
71
  else
@@ -140,6 +162,68 @@ module ShapeupCli
140
162
  ]
141
163
  end
142
164
 
165
+ def update
166
+ id = positional_arg(1) || abort("Usage: shapeup pitches update <id> [--title \"...\"] [--status idea|framed|shaped] [--appetite ...] [--stream \"Name\"] [--content \"...\"] [--cycle <id>]")
167
+
168
+ args = { package: id.to_s }
169
+ args[:title] = extract_option("--title") if @remaining.include?("--title")
170
+ args[:status] = extract_option("--status") if @remaining.include?("--status")
171
+ args[:appetite] = extract_option("--appetite") if @remaining.include?("--appetite")
172
+ args[:stream] = extract_option("--stream") if @remaining.include?("--stream")
173
+ args[:content] = extract_option("--content") if @remaining.include?("--content")
174
+ args[:cycle] = extract_option("--cycle") if @remaining.include?("--cycle")
175
+
176
+ abort("Nothing to update. Pass at least one of --title, --status, --appetite, --stream, --content, --cycle") if args.size == 1
177
+
178
+ result = call_tool("update_package", **args)
179
+
180
+ render result,
181
+ summary: "Pitch ##{id} updated",
182
+ breadcrumbs: [
183
+ { cmd: "shapeup pitch #{id}", description: "View pitch details" }
184
+ ]
185
+ end
186
+
187
+ def extend_pitch
188
+ id = positional_arg(1) || abort("Usage: shapeup pitches extend <id> --predecessor <id>")
189
+ predecessor = extract_option("--predecessor") || abort("Usage: shapeup pitches extend <id> --predecessor <id>")
190
+
191
+ result = call_tool("extend_package", package: id.to_s, predecessor: predecessor.to_s)
192
+
193
+ render result,
194
+ summary: "Pitch ##{id} now extends ##{predecessor}",
195
+ breadcrumbs: [
196
+ { cmd: "shapeup pitch #{id}", description: "View pitch details" },
197
+ { cmd: "shapeup pitches detach #{id}", description: "Remove the predecessor link" }
198
+ ]
199
+ end
200
+
201
+ def detach
202
+ id = positional_arg(1) || abort("Usage: shapeup pitches detach <id>")
203
+
204
+ result = call_tool("detach_package", package: id.to_s)
205
+
206
+ render result,
207
+ summary: "Pitch ##{id} detached from its predecessor",
208
+ breadcrumbs: [
209
+ { cmd: "shapeup pitch #{id}", description: "View pitch details" }
210
+ ]
211
+ end
212
+
213
+ def delete
214
+ yes = assume_yes?
215
+ id = positional_arg(1) || abort("Usage: shapeup pitches delete <id> [--yes]")
216
+ confirm_destructive!("Delete pitch ##{id}", yes)
217
+
218
+ result = call_tool("delete_package", package: id.to_s)
219
+
220
+ render result,
221
+ summary: "Pitch ##{id} deleted",
222
+ breadcrumbs: [
223
+ { cmd: "shapeup pitches list", description: "List remaining pitches" }
224
+ ]
225
+ end
226
+
143
227
  def help
144
228
  puts <<~HELP
145
229
  Usage: shapeup pitches <subcommand> [options]
@@ -10,11 +10,17 @@ module ShapeupCli
10
10
  short: "Manage scopes within a pitch",
11
11
  subcommands: [
12
12
  { name: "list", short: "List scopes for a pitch", path: "shapeup scopes list --pitch <id>" },
13
+ { name: "show", short: "Show a scope with its tasks", path: "shapeup scopes show <id>" },
13
14
  { name: "create", short: "Create a new scope", path: "shapeup scopes create --pitch <id> \"Title\"" },
14
15
  { 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
+ { name: "position", short: "Update hill chart position (0-100)", path: "shapeup scopes position <id> <position>" },
17
+ { name: "history", short: "Show hill chart trajectory", path: "shapeup scopes history <id>" },
18
+ { name: "delete", short: "Delete a scope (must have no tasks or comments)", path: "shapeup scopes delete <id>" },
19
+ { name: "assign", short: "Assign a user", path: "shapeup scopes assign <id> [--user <id>]" },
20
+ { name: "unassign", short: "Unassign a user", path: "shapeup scopes unassign <id> [--user <id>]" }
16
21
  ],
17
22
  flags: [
23
+ { name: "user", type: "string", usage: "User ID or 'me' (assign/unassign; defaults to me)" },
18
24
  { name: "pitch", type: "string", usage: "Pitch ID (required for list and create)" },
19
25
  { name: "title", type: "string", usage: "Scope title (for create/update)" },
20
26
  { name: "color", type: "string", usage: "Hex color code (for update)" }
@@ -23,7 +29,8 @@ module ShapeupCli
23
29
  "shapeup scopes list --pitch 42",
24
30
  "shapeup scopes create --pitch 42 \"User onboarding\"",
25
31
  "shapeup scopes update 7 --title \"Revised onboarding\"",
26
- "shapeup scopes position 7 50"
32
+ "shapeup scopes position 7 50",
33
+ "shapeup scopes delete 7"
27
34
  ]
28
35
  }
29
36
  end
@@ -32,9 +39,14 @@ module ShapeupCli
32
39
  subcommand = positional_arg(0)
33
40
 
34
41
  case subcommand
42
+ when "show" then show
35
43
  when "create" then create
36
44
  when "update" then update
37
45
  when "position" then position
46
+ when "history" then history
47
+ when "delete" then delete
48
+ when "assign" then assign_to("Scope", "scopes")
49
+ when "unassign" then unassign_from("Scope", "scopes")
38
50
  when "list", nil then list
39
51
  else list
40
52
  end
@@ -44,13 +56,37 @@ module ShapeupCli
44
56
  def list
45
57
  pitch_id = extract_option("--pitch") || abort("Usage: shapeup scopes list --pitch <id>")
46
58
 
47
- result = call_tool("show_package", package: pitch_id.to_s)
59
+ result = call_tool("list_scopes", package: pitch_id.to_s)
48
60
 
49
61
  render result,
50
62
  summary: "Scopes for Pitch ##{pitch_id}",
51
63
  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" }
64
+ { cmd: "shapeup scopes show <id>", description: "Show a scope with its tasks" },
65
+ { cmd: "shapeup scopes create --pitch #{pitch_id} \"Title\"", description: "Add a scope" }
66
+ ]
67
+ end
68
+
69
+ def show
70
+ scope_id = positional_arg(1) || abort("Usage: shapeup scopes show <id>")
71
+
72
+ result = call_tool("show_scope", scope: scope_id.to_s)
73
+
74
+ render result,
75
+ summary: "Scope ##{scope_id}",
76
+ breadcrumbs: [
77
+ { cmd: "shapeup scopes history #{scope_id}", description: "Hill chart trajectory" }
78
+ ]
79
+ end
80
+
81
+ def history
82
+ scope_id = positional_arg(1) || abort("Usage: shapeup scopes history <id>")
83
+
84
+ result = call_tool("list_scope_history", scope: scope_id.to_s)
85
+
86
+ render result,
87
+ summary: "Scope ##{scope_id} history",
88
+ breadcrumbs: [
89
+ { cmd: "shapeup scopes position #{scope_id} <0-100>", description: "Update hill position" }
54
90
  ]
55
91
  end
56
92
 
@@ -91,6 +127,20 @@ module ShapeupCli
91
127
 
92
128
  render result, summary: "Scope ##{scope_id} updated"
93
129
  end
130
+
131
+ def delete
132
+ yes = assume_yes?
133
+ scope_id = positional_arg(1) || abort("Usage: shapeup scopes delete <id> [--yes]")
134
+ confirm_destructive!("Delete scope ##{scope_id}", yes)
135
+
136
+ result = call_tool("delete_scope", scope: scope_id.to_s)
137
+
138
+ render result,
139
+ summary: "Scope ##{scope_id} deleted",
140
+ breadcrumbs: [
141
+ { cmd: "shapeup scopes list --pitch <id>", description: "List remaining scopes" }
142
+ ]
143
+ end
94
144
  end
95
145
  end
96
146
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShapeupCli
4
+ module Commands
5
+ class Streams < Base
6
+ def self.metadata
7
+ {
8
+ command: "streams",
9
+ path: "shapeup streams",
10
+ short: "List and show streams (product areas)",
11
+ subcommands: [
12
+ { name: "list", short: "List streams (default)", path: "shapeup streams list" },
13
+ { name: "show", short: "Show stream details with pitches and issue counts", path: "shapeup streams show <id>" },
14
+ { name: "create", short: "Create a stream", path: "shapeup streams create \"Title\"" },
15
+ { name: "edit", short: "Update a stream's title, description, or color", path: "shapeup streams edit <id> --title \"New\"" }
16
+ ],
17
+ flags: [
18
+ { name: "all", type: "bool", usage: "Include archived streams (for list)" },
19
+ { name: "title", type: "string", usage: "Stream title (create/edit)" },
20
+ { name: "description", type: "string", usage: "Stream description (create/edit)" },
21
+ { name: "color", type: "string", usage: "Hex color code (edit)" }
22
+ ],
23
+ examples: [
24
+ "shapeup streams",
25
+ "shapeup streams --all --json",
26
+ "shapeup streams show 3",
27
+ "shapeup streams create \"Platform\"",
28
+ "shapeup streams edit 3 --title \"Platform & Infra\" --color \"#3b82f6\""
29
+ ]
30
+ }
31
+ end
32
+
33
+ def execute
34
+ subcommand = positional_arg(0)
35
+
36
+ case subcommand
37
+ when "show" then show
38
+ when "create" then create
39
+ when "edit" then edit
40
+ when "list", nil then list
41
+ else
42
+ subcommand.match?(/\A\d+\z/) ? show(subcommand) : list
43
+ end
44
+ end
45
+
46
+ private
47
+ def create
48
+ title = positional_arg(1) || abort("Usage: shapeup streams create \"Title\" [--description \"..\"]")
49
+ args = { title: title }
50
+ args[:description] = extract_option("--description") if @remaining.include?("--description")
51
+
52
+ result = call_tool("create_stream", **args)
53
+
54
+ render result,
55
+ summary: "Stream created",
56
+ breadcrumbs: [ { cmd: "shapeup streams edit <id> --color \"#3b82f6\"", description: "Set its colour" } ]
57
+ end
58
+
59
+ def edit
60
+ id = positional_arg(1) || abort("Usage: shapeup streams edit <id> [--title ..] [--description ..] [--color ..]")
61
+ args = { stream: id.to_s }
62
+ args[:title] = extract_option("--title") if @remaining.include?("--title")
63
+ args[:description] = extract_option("--description") if @remaining.include?("--description")
64
+ args[:color] = extract_option("--color") if @remaining.include?("--color")
65
+
66
+ result = call_tool("update_stream", **args)
67
+
68
+ render result, summary: "Stream ##{id} updated"
69
+ end
70
+
71
+ def list
72
+ args = {}
73
+ args[:include_archived] = true if consume_flag("--all")
74
+
75
+ result = call_tool("list_streams", **args)
76
+
77
+ render result,
78
+ summary: "Streams",
79
+ breadcrumbs: [
80
+ { cmd: "shapeup streams show <id>", description: "View stream details" },
81
+ { cmd: "shapeup pitches create \"Title\" --stream \"Name\"", description: "Create a pitch in a stream" }
82
+ ]
83
+ end
84
+
85
+ def show(id = nil)
86
+ id ||= positional_arg(1) || abort("Usage: shapeup streams show <id>")
87
+
88
+ result = call_tool("show_stream", stream: id.to_s)
89
+
90
+ render result,
91
+ summary: "Stream ##{id}",
92
+ breadcrumbs: [
93
+ { cmd: "shapeup pitches list --json", description: "List pitches" },
94
+ { cmd: "shapeup issues --stream \"Name\"", description: "List issues in this stream" }
95
+ ]
96
+ end
97
+ end
98
+ end
99
+ end
@@ -8,23 +8,35 @@ module ShapeupCli
8
8
  command: "tasks",
9
9
  path: "shapeup tasks",
10
10
  short: "Manage tasks within scopes and pitches",
11
- aliases: { "todo" => "tasks create", "done" => "tasks complete" },
11
+ aliases: { "todo" => "tasks create", "done" => "tasks complete", "undone" => "tasks uncomplete" },
12
12
  subcommands: [
13
13
  { name: "list", short: "List tasks", path: "shapeup tasks list" },
14
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>...]" }
15
+ { name: "complete", short: "Mark task(s) as complete", path: "shapeup done <id> [<id>...]" },
16
+ { name: "uncomplete", short: "Mark task as incomplete", path: "shapeup undone <id>" },
17
+ { name: "update", short: "Edit a task's description or move it to a scope", path: "shapeup tasks update <id> --description \"New\"" },
18
+ { name: "delete", short: "Delete a task", path: "shapeup tasks delete <id>" },
19
+ { name: "assign", short: "Assign a user", path: "shapeup tasks assign <id> [--user <id>]" },
20
+ { name: "unassign", short: "Unassign a user", path: "shapeup tasks unassign <id> [--user <id>]" }
16
21
  ],
17
22
  flags: [
23
+ { name: "user", type: "string", usage: "User ID or 'me' (assign/unassign; defaults to me)" },
18
24
  { 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)" }
25
+ { name: "scope", type: "string", usage: "Scope ID (filter, create target, or update target; use 'none' to unmap)" },
26
+ { name: "assignee", type: "string", usage: "User ID or 'me' (for list)" },
27
+ { name: "description", type: "string", usage: "New description (for update)" }
21
28
  ],
22
29
  examples: [
23
30
  "shapeup tasks list --pitch 42",
24
31
  "shapeup tasks list --assignee me",
25
32
  "shapeup todo \"Fix login bug\" --pitch 42 --scope 7",
26
33
  "shapeup done 123",
27
- "shapeup done 123 124 125"
34
+ "shapeup done 123 124 125",
35
+ "shapeup undone 123",
36
+ "shapeup tasks update 123 --description \"Fix login + signup bug\"",
37
+ "shapeup tasks update 123 --scope 7",
38
+ "shapeup tasks update 123 --scope none",
39
+ "shapeup tasks delete 123"
28
40
  ]
29
41
  }
30
42
  end
@@ -33,9 +45,14 @@ module ShapeupCli
33
45
  subcommand = positional_arg(0)
34
46
 
35
47
  case subcommand
36
- when "create" then create
37
- when "complete" then complete
38
- when "list", nil then list
48
+ when "create" then create
49
+ when "complete" then complete
50
+ when "uncomplete" then uncomplete
51
+ when "update" then update
52
+ when "delete" then delete
53
+ when "assign" then assign_to("Task", "tasks")
54
+ when "unassign" then unassign_from("Task", "tasks")
55
+ when "list", nil then list
39
56
  else list
40
57
  end
41
58
  end
@@ -91,10 +108,56 @@ module ShapeupCli
91
108
  render result,
92
109
  summary: "Task ##{id} completed",
93
110
  breadcrumbs: [
111
+ { cmd: "shapeup undone #{id}", description: "Mark incomplete again" },
94
112
  { cmd: "shapeup me", description: "Show remaining work" }
95
113
  ]
96
114
  end
97
115
  end
116
+
117
+ def uncomplete
118
+ id = positional_arg(1) || abort("Usage: shapeup undone <id>")
119
+
120
+ result = call_tool("uncomplete_task", task: id.to_s)
121
+
122
+ render result,
123
+ summary: "Task ##{id} marked incomplete",
124
+ breadcrumbs: [
125
+ { cmd: "shapeup done #{id}", description: "Complete it again" }
126
+ ]
127
+ end
128
+
129
+ def update
130
+ id = positional_arg(1) || abort("Usage: shapeup tasks update <id> [--description \"New\"] [--scope <id>|none]")
131
+ description = extract_option("--description")
132
+ scope = extract_option("--scope")
133
+ abort("Nothing to update. Pass --description \"...\" and/or --scope <id>|none") unless description || scope
134
+
135
+ args = { task: id.to_s }
136
+ args[:description] = description if description
137
+ args[:scope] = scope if scope
138
+
139
+ result = call_tool("update_task", **args)
140
+
141
+ render result,
142
+ summary: "Task ##{id} updated",
143
+ breadcrumbs: [
144
+ { cmd: "shapeup tasks list --pitch <id>", description: "List tasks" }
145
+ ]
146
+ end
147
+
148
+ def delete
149
+ yes = assume_yes?
150
+ id = positional_arg(1) || abort("Usage: shapeup tasks delete <id> [--yes]")
151
+ confirm_destructive!("Delete task ##{id}", yes)
152
+
153
+ result = call_tool("delete_task", task: id.to_s)
154
+
155
+ render result,
156
+ summary: "Task ##{id} deleted",
157
+ breadcrumbs: [
158
+ { cmd: "shapeup tasks list --pitch <id>", description: "List remaining tasks" }
159
+ ]
160
+ end
98
161
  end
99
162
  end
100
163
  end
@@ -24,20 +24,35 @@ module ShapeupCli
24
24
  pitches list List pitches (packages)
25
25
  pitches show <id> Show pitch details with scopes and tasks
26
26
  pitch <id> Shortcut for pitches show
27
+ pitches create "Title" --stream "Name" [--appetite small_batch] [--cycle-id <id>]
28
+ pitches update <id> [--title|--status|--appetite|--stream|--content|--cycle ...]
29
+ pitches extend <id> --predecessor <id> Continue a previous pitch
30
+ pitches detach <id> Remove the predecessor link
31
+ pitches delete <id> Delete a pitch (asks to confirm; --yes to skip)
27
32
 
28
33
  Cycles:
29
34
  cycles List all cycles
30
35
  cycle show <id> Show cycle details with pitches and progress
31
36
 
37
+ Streams:
38
+ streams List streams (product areas)
39
+ streams --all Include archived streams
40
+ streams show <id> Show stream details
41
+
32
42
  Scopes:
33
43
  scopes list --pitch <id> List scopes for a pitch
34
44
  scopes create --pitch <id> "Title"
35
45
  scopes update <id> --title "New title"
46
+ scopes position <id> <0-100> Update hill chart position
47
+ scopes delete <id> Delete a scope (asks to confirm; --yes to skip)
36
48
 
37
49
  Tasks:
38
50
  tasks list --scope <id> List tasks for a scope
39
51
  todo "Description" --pitch <id> [--scope <id>]
40
52
  done <id> [<id>...] Mark task(s) as complete
53
+ undone <id> Mark a task as incomplete
54
+ tasks update <id> [--description "New"] [--scope <id>|none]
55
+ tasks delete <id> Delete a task (asks to confirm; --yes to skip)
41
56
 
42
57
  Issues:
43
58
  issues List open issues
@@ -61,7 +76,7 @@ module ShapeupCli
61
76
  issues watch <id> Watch an issue
62
77
  issues unwatch <id> Stop watching
63
78
  watching List issues you are watching
64
- issues delete <id> Delete an issue
79
+ issues delete <id> Delete an issue (asks to confirm; --yes to skip)
65
80
  issues convert <id> Convert issue to a new pitch
66
81
  issues add-to-pitch <id> --pitch <pid>
67
82
  Fold issue into an existing pitch
@@ -71,6 +86,8 @@ module ShapeupCli
71
86
  comments list --pitch <id> List comments on a pitch
72
87
  comments add --issue <id> "Text" Add a comment to an issue
73
88
  comments add --pitch <id> "Text" Add a comment to a pitch
89
+ comments edit <comment_id> "Text" Edit your own comment
90
+ comments remove <comment_id> Delete your own comment (asks to confirm)
74
91
 
75
92
  Checklist:
76
93
  checklist --pitch <id> List the checklist on a pitch
@@ -116,6 +133,8 @@ module ShapeupCli
116
133
  Flags:
117
134
  --org <id|name> Override default organisation
118
135
  --host <url> Override ShapeUp host (default: https://shapeup.cc)
136
+ --yes, -y Skip the confirmation prompt on delete commands
137
+ (required when deleting non-interactively)
119
138
 
120
139
  Environment variables:
121
140
  SHAPEUP_TOKEN Bearer token (skips OAuth, for CI/scripts)
@@ -139,18 +158,20 @@ module ShapeupCli
139
158
  logout Clear all credentials
140
159
  auth Manage profiles (status, list, switch, remove)
141
160
  orgs List organisations
142
- pitches List/show pitches (list, show)
161
+ pitches Manage pitches (list, show, create, update, extend, detach, delete)
143
162
  pitch Show a pitch (shortcut)
144
163
  cycles List cycles
145
164
  cycle Show cycle details (list, show)
146
- scopes Manage scopes (list, create, update)
147
- tasks Manage tasks (list, create, complete)
165
+ streams List/show streams (list, show)
166
+ scopes Manage scopes (list, create, update, position, delete)
167
+ tasks Manage tasks (list, create, complete, uncomplete, update, delete)
148
168
  todo Create a task (shortcut)
149
169
  done Complete task(s) (shortcut)
150
- issues Manage issues (list, show, create, move, icebox, watch, convert, add-to-pitch)
170
+ undone Mark a task incomplete (shortcut)
171
+ issues Manage issues (list, show, create, move, icebox, watch, convert, add-to-pitch, delete)
151
172
  issue Show an issue (shortcut)
152
173
  watching List watched issues (shortcut)
153
- comments List and add comments (list, add)
174
+ comments Manage comments (list, add, edit, remove)
154
175
  checklist Manage checklist items on pitches/issues (list, add, tick, untick, edit, remove)
155
176
  tags List/add/remove tags on pitches and issues
156
177
  my-work / me Show my assigned work
@@ -171,6 +192,7 @@ require_relative "commands/logout"
171
192
  require_relative "commands/orgs"
172
193
  require_relative "commands/pitches"
173
194
  require_relative "commands/cycle"
195
+ require_relative "commands/streams"
174
196
  require_relative "commands/scopes"
175
197
  require_relative "commands/tasks"
176
198
  require_relative "commands/issues"
@@ -92,7 +92,7 @@ module ShapeupCli
92
92
  when Hash
93
93
  render_markdown_hash(data)
94
94
  else
95
- puts data.to_s
95
+ puts data
96
96
  end
97
97
  end
98
98
 
@@ -106,7 +106,7 @@ module ShapeupCli
106
106
  when Hash
107
107
  render_styled_hash(data)
108
108
  else
109
- puts data.to_s
109
+ puts data
110
110
  end
111
111
 
112
112
  if breadcrumbs.any?
@@ -177,7 +177,7 @@ module ShapeupCli
177
177
  when Hash
178
178
  id = value["id"]
179
179
  label = value["title"] || value["name"] || value["description"]
180
- [id, label].compact.join(" ")
180
+ [ id, label ].compact.join(" ")
181
181
  else
182
182
  value.to_s
183
183
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShapeupCli
4
+ VERSION = "0.5.0"
5
+ end
data/lib/shapeup_cli.rb CHANGED
@@ -9,6 +9,7 @@ require "digest"
9
9
  require "base64"
10
10
  require "socket"
11
11
 
12
+ require_relative "shapeup_cli/version"
12
13
  require_relative "shapeup_cli/config"
13
14
  require_relative "shapeup_cli/auth"
14
15
  require_relative "shapeup_cli/client"
@@ -16,7 +17,6 @@ require_relative "shapeup_cli/output"
16
17
  require_relative "shapeup_cli/commands"
17
18
 
18
19
  module ShapeupCli
19
- VERSION = "0.3.4"
20
20
  DEFAULT_HOST = "https://shapeup.cc"
21
21
 
22
22
  # Exit codes
@@ -33,6 +33,7 @@ module ShapeupCli
33
33
  "orgs" => Commands::Orgs,
34
34
  "pitches" => Commands::Pitches,
35
35
  "cycle" => Commands::Cycle,
36
+ "streams" => Commands::Streams,
36
37
  "scopes" => Commands::Scopes,
37
38
  "tasks" => Commands::Tasks,
38
39
  "issues" => Commands::Issues,
@@ -68,16 +69,18 @@ module ShapeupCli
68
69
  when "auth" then Commands::Auth.run(args)
69
70
  when "orgs" then Commands::Orgs.run(args)
70
71
  when "pitches" then Commands::Pitches.run(args)
71
- when "pitch" then Commands::Pitches.run(["show"] + args)
72
+ when "pitch" then Commands::Pitches.run([ "show" ] + args)
72
73
  when "cycle" then Commands::Cycle.run(args)
73
- when "cycles" then Commands::Cycle.run(["list"] + args)
74
+ when "cycles" then Commands::Cycle.run([ "list" ] + args)
75
+ when "streams" then Commands::Streams.run(args)
74
76
  when "scopes" then Commands::Scopes.run(args)
75
77
  when "tasks" then Commands::Tasks.run(args)
76
- when "todo" then Commands::Tasks.run(["create"] + args)
77
- when "done" then Commands::Tasks.run(["complete"] + args)
78
+ when "todo" then Commands::Tasks.run([ "create" ] + args)
79
+ when "done" then Commands::Tasks.run([ "complete" ] + args)
80
+ when "undone" then Commands::Tasks.run([ "uncomplete" ] + args)
78
81
  when "issues" then Commands::Issues.run(args)
79
- when "issue" then Commands::Issues.run(["show"] + args)
80
- when "watching" then Commands::Issues.run(["watching"] + args)
82
+ when "issue" then Commands::Issues.run([ "show" ] + args)
83
+ when "watching" then Commands::Issues.run([ "watching" ] + args)
81
84
  when "comments" then Commands::Comments.run(args)
82
85
  when "checklist" then Commands::Checklist.run(args)
83
86
  when "tags" then Commands::Tags.run(args)
@@ -126,6 +129,7 @@ module ShapeupCli
126
129
  "cycles" => "cycle list",
127
130
  "todo \"...\"" => "tasks create \"...\"",
128
131
  "done <id>" => "tasks complete <id>",
132
+ "undone <id>" => "tasks uncomplete <id>",
129
133
  "issue <id>" => "issues show <id>",
130
134
  "watching" => "issues watching",
131
135
  "me" => "my-work"
@@ -78,6 +78,7 @@ Manage pitches, scopes, tasks, issues, and cycles via the ShapeUp CLI. Columns a
78
78
  5. **"Pitch" = "Package" in code** — users say "pitch", the API uses "package". The CLI uses "pitch" everywhere.
79
79
  6. **Use 'me' and 'none'** — `--assignee me` for current user, `--assignee none` for unassigned items.
80
80
  7. **Check exit codes** — 0=OK, 2=not found, 3=auth error, 4=permission denied, 5=API error. Branch on exit code without parsing error text.
81
+ 8. **Deletes need confirmation** — `delete`/`remove` commands (pitches, scopes, tasks, issues, comments) prompt `[y/N]` interactively and **refuse when run non-interactively** unless you pass `--yes` (or `-y`). As an agent you have no TTY, so add `--yes` only after you have confirmed the deletion is intended.
81
82
 
82
83
  ### Output Modes
83
84
 
@@ -124,6 +125,7 @@ Manage pitches, scopes, tasks, issues, and cycles via the ShapeUp CLI. Columns a
124
125
  | Filter by stream | `shapeup issues --stream "Platform" --json` |
125
126
  | Show issue detail | `shapeup issue <id> --json` |
126
127
  | Create issue | `shapeup issues create "Title" --stream "Platform"` |
128
+ | List kanban columns | `shapeup issues columns --json` |
127
129
  | Move to column | `shapeup issues move <id> --column doing` |
128
130
  | Mark issue done | `shapeup issues done <id>` |
129
131
  | Close issue (won't fix) | `shapeup issues close <id>` |
@@ -140,6 +142,8 @@ Manage pitches, scopes, tasks, issues, and cycles via the ShapeUp CLI. Columns a
140
142
  | List comments on pitch | `shapeup comments list --pitch <id> --json` |
141
143
  | Add comment to issue | `shapeup comments add --issue <id> "Comment text"` |
142
144
  | Add comment to pitch | `shapeup comments add --pitch <id> "Comment text"` |
145
+ | Edit your comment | `shapeup comments edit <comment_id> "New text"` |
146
+ | Delete your comment | `shapeup comments remove <comment_id> --yes` |
143
147
  | **Checklist** | |
144
148
  | List checklist on pitch | `shapeup checklist --pitch <id>` |
145
149
  | List checklist on issue | `shapeup checklist --issue <id>` |
@@ -159,17 +163,42 @@ Manage pitches, scopes, tasks, issues, and cycles via the ShapeUp CLI. Columns a
159
163
  | Show pitch detail | `shapeup pitch <id> --json` |
160
164
  | Create pitch | `shapeup pitches create "Title" --stream "Name"` |
161
165
  | Create with appetite | `shapeup pitches create "Title" --stream "Name" --appetite small_batch` |
166
+ | Update pitch | `shapeup pitches update <id> --status shaped` |
167
+ | Extend a pitch | `shapeup pitches extend <id> --predecessor <id>` |
168
+ | Detach predecessor | `shapeup pitches detach <id>` |
169
+ | Delete pitch | `shapeup pitches delete <id> --yes` |
170
+ | Assign to pitch | `shapeup pitches assign <id>` (self) / `--user <id>` |
171
+ | Unassign from pitch | `shapeup pitches unassign <id>` (self) / `--user <id>` |
162
172
  | **Cycles** | |
163
173
  | List cycles | `shapeup cycles --json` |
164
174
  | Active cycles | `shapeup cycles --status active --json` |
165
175
  | Show cycle | `shapeup cycle show <id> --json` |
176
+ | Create cycle | `shapeup cycle create "Q3" --start 2026-07-01 --end 2026-08-12` |
177
+ | Edit cycle | `shapeup cycle edit <id> --title "New" --end 2026-08-19` |
178
+ | Delete cycle | `shapeup cycle delete <id> --yes` |
179
+ | **Streams** | |
180
+ | List streams | `shapeup streams --json` |
181
+ | Include archived | `shapeup streams --all --json` |
182
+ | Show stream | `shapeup streams show <id> --json` |
183
+ | Create stream | `shapeup streams create "Platform" --description "..."` |
184
+ | Edit stream | `shapeup streams edit <id> --title "New" --color "#3b82f6"` |
166
185
  | **Scopes & Tasks** | |
167
186
  | List scopes | `shapeup scopes list --pitch <id> --json` |
187
+ | Show scope + tasks | `shapeup scopes show <id> --json` |
168
188
  | Create scope | `shapeup scopes create --pitch <id> "Title"` |
169
189
  | Update hill position | `shapeup scopes position <id> <0-100>` |
190
+ | Hill chart history | `shapeup scopes history <id> --json` |
191
+ | Delete scope | `shapeup scopes delete <id> --yes` |
192
+ | Assign to scope | `shapeup scopes assign <id>` (self) / `--user <id>` |
193
+ | Unassign from scope | `shapeup scopes unassign <id>` (self) / `--user <id>` |
170
194
  | List tasks | `shapeup tasks list --pitch <id> --json` |
171
195
  | Create task | `shapeup todo "Description" --pitch <id>` |
172
196
  | Complete task(s) | `shapeup done <id> [<id>...]` |
197
+ | Uncomplete task | `shapeup undone <id>` |
198
+ | Update task | `shapeup tasks update <id> --description "New"` |
199
+ | Delete task | `shapeup tasks delete <id> --yes` |
200
+ | Assign to task | `shapeup tasks assign <id>` (self) / `--user <id>` |
201
+ | Unassign from task | `shapeup tasks unassign <id>` (self) / `--user <id>` |
173
202
  | **My Work** | |
174
203
  | All my assignments | `shapeup me --json` |
175
204
  | My work (alias) | `shapeup my-work --json` |
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shapeup-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ShapeUp
@@ -40,10 +40,12 @@ files:
40
40
  - lib/shapeup_cli/commands/scopes.rb
41
41
  - lib/shapeup_cli/commands/search.rb
42
42
  - lib/shapeup_cli/commands/setup.rb
43
+ - lib/shapeup_cli/commands/streams.rb
43
44
  - lib/shapeup_cli/commands/tags.rb
44
45
  - lib/shapeup_cli/commands/tasks.rb
45
46
  - lib/shapeup_cli/config.rb
46
47
  - lib/shapeup_cli/output.rb
48
+ - lib/shapeup_cli/version.rb
47
49
  - skills/shapeup/SKILL.md
48
50
  homepage: https://github.com/shapeupcc/shapeup-cli
49
51
  licenses:
@@ -66,7 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
68
  - !ruby/object:Gem::Version
67
69
  version: '0'
68
70
  requirements: []
69
- rubygems_version: 4.0.6
71
+ rubygems_version: 3.6.9
70
72
  specification_version: 4
71
73
  summary: ShapeUp CLI — manage pitches, scopes, tasks, and cycles from the terminal
72
74
  test_files: []