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.
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShapeupCli
4
+ module Commands
5
+ class Comments < Base
6
+ def self.metadata
7
+ {
8
+ command: "comments",
9
+ path: "shapeup comments",
10
+ short: "List and add comments on issues, pitches, scopes, and tasks",
11
+ subcommands: [
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"' }
14
+ ],
15
+ flags: [
16
+ { name: "issue", type: "string", usage: "Issue ID" },
17
+ { name: "pitch", type: "string", usage: "Pitch ID" },
18
+ { name: "scope", type: "string", usage: "Scope ID" },
19
+ { name: "task", type: "string", usage: "Task ID" }
20
+ ],
21
+ examples: [
22
+ "shapeup comments list --issue 42",
23
+ 'shapeup comments add --issue 42 "Investigated — this is a CSS issue in the navbar"',
24
+ "shapeup comments list --pitch 10",
25
+ 'shapeup comments add --pitch 10 "Shaped and ready for betting"'
26
+ ]
27
+ }
28
+ end
29
+
30
+ def execute
31
+ subcommand = positional_arg(0)
32
+
33
+ case subcommand
34
+ when "add" then add
35
+ when "list", nil then list
36
+ else list
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def list
43
+ type, id = resolve_commentable
44
+ result = call_tool("list_comments", commentable_type: type, commentable_id: id.to_s)
45
+
46
+ render result,
47
+ summary: "Comments on #{type} ##{id}",
48
+ breadcrumbs: [
49
+ { cmd: "shapeup comments add --#{type.downcase} #{id} \"Your comment\"", description: "Add a comment" }
50
+ ]
51
+ end
52
+
53
+ def add
54
+ type, id = resolve_commentable
55
+ text = positional_arg(1) || abort('Usage: shapeup comments add --issue <id> "Comment text"')
56
+
57
+ result = call_tool("create_comment", commentable_type: type, commentable_id: id.to_s, content: text)
58
+
59
+ render result,
60
+ summary: "Comment added to #{type} ##{id}",
61
+ breadcrumbs: [
62
+ { cmd: "shapeup comments list --#{type.downcase} #{id}", description: "View all comments" }
63
+ ]
64
+ end
65
+
66
+ def resolve_commentable
67
+ issue = extract_option("--issue")
68
+ pitch = extract_option("--pitch")
69
+ scope = extract_option("--scope")
70
+ task = extract_option("--task")
71
+
72
+ if issue
73
+ [ "Issue", issue ]
74
+ elsif pitch
75
+ [ "Package", pitch ]
76
+ elsif scope
77
+ [ "Scope", scope ]
78
+ elsif task
79
+ [ "Task", task ]
80
+ else
81
+ abort("Specify a target: --issue <id>, --pitch <id>, --scope <id>, or --task <id>")
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShapeupCli
4
+ module Commands
5
+ class ConfigCmd < Base
6
+ def self.metadata
7
+ {
8
+ command: "config",
9
+ path: "shapeup config",
10
+ short: "Show and manage CLI configuration",
11
+ subcommands: [
12
+ { name: "show", short: "Show current config (default)", path: "shapeup config show" },
13
+ { name: "set", short: "Set a config value", path: "shapeup config set <key> <value>" },
14
+ { name: "init", short: "Create .shapeup/config.json for this directory", path: "shapeup config init <org>" }
15
+ ],
16
+ flags: [],
17
+ notes: [
18
+ "Config keys: org (organisation name or ID), host (ShapeUp URL)",
19
+ "Resolution order: --org flag > .shapeup/config.json > ~/.config/shapeup/config.json"
20
+ ],
21
+ examples: [
22
+ "shapeup config show",
23
+ "shapeup config set org \"Acme Corp\"",
24
+ "shapeup config init \"Acme Corp\""
25
+ ]
26
+ }
27
+ end
28
+
29
+ def execute
30
+ subcommand = positional_arg(0)
31
+
32
+ case subcommand
33
+ when "set" then set
34
+ when "show" then show
35
+ when "init" then init_project
36
+ else show
37
+ end
38
+ end
39
+
40
+ private
41
+ def set
42
+ key = positional_arg(1) || abort("Usage: shapeup config set <key> <value>")
43
+ value = positional_arg(2) || abort("Usage: shapeup config set #{key} <value>")
44
+
45
+ case key
46
+ when "org"
47
+ resolved = resolve_org_value(value)
48
+ Config.save_config("organisation_id", resolved.to_s)
49
+ puts "Default organisation set to #{resolved}"
50
+ when "host"
51
+ Config.save_config("host", value)
52
+ puts "Host set to #{value}"
53
+ else
54
+ abort "Unknown config key: #{key}. Available: org, host"
55
+ end
56
+ end
57
+
58
+ def show
59
+ config = Config.load_config
60
+ profile = Config.current_profile
61
+
62
+ puts "Profile:"
63
+ if profile
64
+ puts " active #{profile["profile_name"]}"
65
+ puts " org #{profile["name"]} (#{profile["organisation_id"]})"
66
+ puts " host #{profile["host"]}"
67
+ puts " token #{profile["token"][0..7]}..."
68
+ else
69
+ puts " (not logged in)"
70
+ end
71
+ puts
72
+
73
+ overrides = []
74
+ overrides << "org=#{config["organisation_id"]}" if config["organisation_id"]
75
+ overrides << "host=#{config["host"]}" if config["host"]
76
+ if overrides.any?
77
+ puts "Overrides:"
78
+ puts " #{overrides.join(", ")}"
79
+ puts
80
+ end
81
+
82
+ puts "Files:"
83
+ puts " profiles #{Config::PROFILES_FILE}"
84
+ puts " config #{Config::CONFIG_FILE}"
85
+ puts " project #{Config::PROJECT_CONFIG_NAME} #{find_project_display}"
86
+
87
+ env_vars = []
88
+ env_vars << "SHAPEUP_TOKEN" if ENV["SHAPEUP_TOKEN"]
89
+ env_vars << "SHAPEUP_ORG" if ENV["SHAPEUP_ORG"]
90
+ env_vars << "SHAPEUP_HOST" if ENV["SHAPEUP_HOST"]
91
+ env_vars << "SHAPEUP_PROFILE" if ENV["SHAPEUP_PROFILE"]
92
+ if env_vars.any?
93
+ puts
94
+ puts "Env vars active:"
95
+ puts " #{env_vars.join(", ")}"
96
+ end
97
+ end
98
+
99
+ def find_project_display
100
+ dir = Dir.pwd
101
+ loop do
102
+ candidate = File.join(dir, Config::PROJECT_CONFIG_NAME)
103
+ return "(found: #{candidate})" if File.exist?(candidate)
104
+ parent = File.dirname(dir)
105
+ break if parent == dir
106
+ dir = parent
107
+ end
108
+ "(not found)"
109
+ end
110
+
111
+ # Create .shapeup/config.json in the current directory
112
+ def init_project
113
+ org_value = positional_arg(1) || extract_option("--org") || @org_id
114
+ abort("Usage: shapeup config init <org>") unless org_value
115
+
116
+ resolved = resolve_org_value(org_value)
117
+
118
+ FileUtils.mkdir_p(".shapeup")
119
+ File.write(".shapeup/config.json", JSON.pretty_generate(organisation_id: resolved.to_s))
120
+ puts "Created .shapeup/config.json (org: #{resolved})"
121
+ puts "All commands in this directory will use this organisation by default."
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
+ end
142
+ end
143
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShapeupCli
4
+ module Commands
5
+ class Cycle < Base
6
+ def self.metadata
7
+ {
8
+ command: "cycle",
9
+ path: "shapeup cycle",
10
+ short: "List and show cycles",
11
+ subcommands: [
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>" }
14
+ ],
15
+ flags: [
16
+ { name: "status", type: "string", usage: "Filter by status: active, past, future, all" }
17
+ ],
18
+ examples: [
19
+ "shapeup cycles",
20
+ "shapeup cycles --status active",
21
+ "shapeup cycle show 12"
22
+ ]
23
+ }
24
+ end
25
+
26
+ def execute
27
+ subcommand = positional_arg(0)
28
+
29
+ case subcommand
30
+ when "show" then show
31
+ when "list", nil then list
32
+ else
33
+ subcommand.match?(/\A\d+\z/) ? show(subcommand) : list
34
+ end
35
+ end
36
+
37
+ private
38
+ def list
39
+ status = extract_option("--status")
40
+ args = {}
41
+ args[:status] = status if status
42
+
43
+ result = call_tool("list_cycles", **args)
44
+
45
+ render result,
46
+ summary: "Cycles",
47
+ breadcrumbs: [
48
+ { cmd: "shapeup cycle show <id>", description: "View cycle details and progress" },
49
+ { cmd: "shapeup cycles --status active", description: "Show active cycles only" }
50
+ ]
51
+ end
52
+
53
+ def show(id = nil)
54
+ id ||= positional_arg(1) || abort("Usage: shapeup cycle show <id>")
55
+
56
+ result = call_tool("show_cycle", cycle: id.to_s)
57
+
58
+ render result,
59
+ summary: "Cycle ##{id}",
60
+ breadcrumbs: [
61
+ { cmd: "shapeup pitches list --cycle #{id}", description: "List pitches in this cycle" }
62
+ ]
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShapeupCli
4
+ module Commands
5
+ class Issues < Base
6
+ def self.metadata
7
+ {
8
+ command: "issues",
9
+ path: "shapeup issues",
10
+ short: "Manage issues on the kanban board",
11
+ aliases: { "issue" => "issues show", "watching" => "issues watching" },
12
+ subcommands: [
13
+ { name: "list", short: "List active issues", path: "shapeup issues" },
14
+ { name: "show", short: "Show issue details", path: "shapeup issue <id>" },
15
+ { name: "create", short: "Create an issue", path: "shapeup issues create \"Title\" --stream <id>" },
16
+ { name: "update", short: "Update an issue", path: "shapeup issues update <id> --title \"New title\"" },
17
+ { name: "move", short: "Move to a kanban column", path: "shapeup issues move <id> --column <id>" },
18
+ { name: "done", short: "Mark issue as done", path: "shapeup issues done <id>" },
19
+ { name: "close", short: "Close issue (won't fix)", path: "shapeup issues close <id>" },
20
+ { name: "reopen", short: "Reopen a done/closed issue", path: "shapeup issues reopen <id>" },
21
+ { name: "icebox", short: "Move issue to icebox", path: "shapeup issues icebox <id>" },
22
+ { name: "defrost", short: "Restore issue from icebox", path: "shapeup issues defrost <id>" },
23
+ { name: "assign", short: "Assign a user to an issue", path: "shapeup issues assign <id> [--user <id>]" },
24
+ { name: "unassign", short: "Unassign a user from an issue", path: "shapeup issues unassign <id> [--user <id>]" },
25
+ { name: "watch", short: "Watch an issue", path: "shapeup issues watch <id>" },
26
+ { name: "unwatch", short: "Stop watching an issue", path: "shapeup issues unwatch <id>" },
27
+ { name: "watching", short: "List issues you are watching", path: "shapeup watching" },
28
+ { name: "delete", short: "Delete an issue", path: "shapeup issues delete <id>" }
29
+ ],
30
+ flags: [
31
+ { name: "stream", type: "string", usage: "Stream ID (required for create, optional filter for list)" },
32
+ { name: "column", type: "string", usage: "Kanban column ID (for move or filter)" },
33
+ { name: "kind", type: "string", usage: "Filter by kind: bug, request, all" },
34
+ { name: "assignee", type: "string", usage: "User ID or 'me' (for list)" },
35
+ { name: "user", type: "string", usage: "User ID or 'me' (for assign/unassign, defaults to 'me')" },
36
+ { name: "tag", type: "string", usage: "Filter by tag name (for list)" },
37
+ { name: "all", type: "bool", usage: "Include done/closed issues (hidden by default)" },
38
+ { name: "content", type: "string", usage: "Issue content/description" },
39
+ { name: "title", type: "string", usage: "Issue title (for update)" },
40
+ { name: "archived", type: "bool", usage: "Include iceboxed issues in list" }
41
+ ],
42
+ examples: [
43
+ "shapeup issues",
44
+ "shapeup issues --tag seo",
45
+ "shapeup issues --assignee me",
46
+ "shapeup issues --column 3",
47
+ "shapeup issues --all",
48
+ "shapeup issues --stream 3 --kind bug",
49
+ "shapeup issue 42",
50
+ "shapeup issues create \"Fix checkout\" --stream 3 --content \"The button is broken\"",
51
+ "shapeup issues move 42 --column 5",
52
+ "shapeup issues done 42",
53
+ "shapeup issues close 42",
54
+ "shapeup issues reopen 42",
55
+ "shapeup issues icebox 42",
56
+ "shapeup issues defrost 42",
57
+ "shapeup issues assign 42",
58
+ "shapeup issues assign 42 --user 7",
59
+ "shapeup issues unassign 42",
60
+ "shapeup issues watch 42",
61
+ "shapeup watching"
62
+ ]
63
+ }
64
+ end
65
+
66
+ def execute
67
+ subcommand = positional_arg(0)
68
+
69
+ case subcommand
70
+ when "show" then show
71
+ when "create" then create
72
+ when "update" then update
73
+ when "move" then move
74
+ when "done" then mark_done
75
+ when "close" then close
76
+ when "reopen" then reopen
77
+ when "icebox" then icebox
78
+ when "defrost" then defrost
79
+ when "assign" then assign
80
+ when "unassign" then unassign
81
+ when "watch" then watch
82
+ when "unwatch" then unwatch
83
+ when "watching" then watching
84
+ when "delete" then delete
85
+ when "list", nil then list
86
+ else
87
+ # Bare numeric arg = show
88
+ if subcommand&.match?(/\A\d+\z/)
89
+ @remaining.unshift(subcommand)
90
+ show
91
+ else
92
+ list
93
+ end
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def list
100
+ stream = extract_option("--stream")
101
+ column = extract_option("--column")
102
+ kind = extract_option("--kind")
103
+ assignee = extract_option("--assignee")
104
+ tag = extract_option("--tag")
105
+ show_all = @remaining.delete("--all")
106
+
107
+ args = {}
108
+ args[:stream] = stream if stream
109
+ args[:kanban_column] = column if column
110
+ args[:kind] = kind if kind
111
+ args[:assignee] = assignee if assignee
112
+ args[:tag] = tag if tag
113
+ args[:include_closed] = true if show_all
114
+ args[:include_archived] = true if @remaining.delete("--archived")
115
+
116
+ result = call_tool("list_issues", **args)
117
+
118
+ render result,
119
+ summary: "Issues",
120
+ breadcrumbs: [
121
+ { cmd: "shapeup issue <id>", description: "View issue details" },
122
+ { cmd: "shapeup issues create \"Title\" --stream <id>", description: "Create an issue" }
123
+ ]
124
+ end
125
+
126
+ def show
127
+ id = positional_arg(1) || positional_arg(0) || abort("Usage: shapeup issue <id>")
128
+ result = call_tool("show_issue", issue: id.to_s)
129
+
130
+ render result,
131
+ summary: "Issue ##{id}",
132
+ breadcrumbs: [
133
+ { cmd: "shapeup issues move #{id} --column <id>", description: "Move to column" },
134
+ { cmd: "shapeup issues icebox #{id}", description: "Move to icebox" },
135
+ { cmd: "shapeup issues watch #{id}", description: "Watch this issue" }
136
+ ]
137
+ end
138
+
139
+ def create
140
+ stream_id = extract_option("--stream") || abort("Usage: shapeup issues create \"Title\" --stream <id> [--content \"...\"] [--kind bug|request]")
141
+ content = extract_option("--content")
142
+ kind = extract_option("--kind")
143
+ column = extract_option("--column")
144
+ title = positional_arg(1) || abort("Usage: shapeup issues create \"Title\" --stream <id>")
145
+
146
+ args = { stream: stream_id.to_s, title: title, content: content || title }
147
+ args[:kind] = kind if kind
148
+ args[:kanban_column] = column if column
149
+
150
+ result = call_tool("create_issue", **args)
151
+
152
+ render result,
153
+ summary: "Issue created",
154
+ breadcrumbs: [
155
+ { cmd: "shapeup issues", description: "List all issues" },
156
+ { cmd: "shapeup issue <id>", description: "View the issue" }
157
+ ]
158
+ end
159
+
160
+ def update
161
+ id = positional_arg(1) || abort("Usage: shapeup issues update <id> [--title \"...\"] [--content \"...\"] [--kind bug|request]")
162
+ title = extract_option("--title")
163
+ content = extract_option("--content")
164
+ kind = extract_option("--kind")
165
+
166
+ args = { issue: id.to_s }
167
+ args[:title] = title if title
168
+ args[:content] = content if content
169
+ args[:kind] = kind if kind
170
+
171
+ result = call_tool("update_issue", **args)
172
+
173
+ render result,
174
+ summary: "Issue ##{id} updated",
175
+ breadcrumbs: [
176
+ { cmd: "shapeup issue #{id}", description: "View issue" }
177
+ ]
178
+ end
179
+
180
+ def move
181
+ id = positional_arg(1) || abort("Usage: shapeup issues move <id> --column <id>")
182
+ column_id = extract_option("--column") || abort("Usage: shapeup issues move <id> --column <id>")
183
+
184
+ result = call_tool("move_issue", issue: id.to_s, kanban_column: column_id.to_s)
185
+
186
+ render result,
187
+ summary: "Issue ##{id} moved",
188
+ breadcrumbs: [
189
+ { cmd: "shapeup issue #{id}", description: "View issue" },
190
+ { cmd: "shapeup issues", description: "List all issues" }
191
+ ]
192
+ end
193
+
194
+ def icebox
195
+ id = positional_arg(1) || abort("Usage: shapeup issues icebox <id>")
196
+
197
+ result = call_tool("archive_issue", issue: id.to_s)
198
+
199
+ render result,
200
+ summary: "Issue ##{id} iceboxed",
201
+ breadcrumbs: [
202
+ { cmd: "shapeup issues defrost #{id}", description: "Defrost this issue" },
203
+ { cmd: "shapeup issues --archived", description: "List iceboxed issues" }
204
+ ]
205
+ end
206
+
207
+ def mark_done
208
+ id = positional_arg(1) || abort("Usage: shapeup issues done <id>")
209
+
210
+ result = call_tool("close_issue", issue: id.to_s, resolution: "done")
211
+
212
+ render result,
213
+ summary: "Issue ##{id} done",
214
+ breadcrumbs: [
215
+ { cmd: "shapeup issues reopen #{id}", description: "Reopen this issue" },
216
+ { cmd: "shapeup issues", description: "List open issues" }
217
+ ]
218
+ end
219
+
220
+ def close
221
+ id = positional_arg(1) || abort("Usage: shapeup issues close <id>")
222
+
223
+ result = call_tool("close_issue", issue: id.to_s, resolution: "closed")
224
+
225
+ render result,
226
+ summary: "Issue ##{id} closed",
227
+ breadcrumbs: [
228
+ { cmd: "shapeup issues reopen #{id}", description: "Reopen this issue" },
229
+ { cmd: "shapeup issues", description: "List open issues" }
230
+ ]
231
+ end
232
+
233
+ def reopen
234
+ id = positional_arg(1) || abort("Usage: shapeup issues reopen <id>")
235
+
236
+ result = call_tool("reopen_issue", issue: id.to_s)
237
+
238
+ render result,
239
+ summary: "Issue ##{id} reopened",
240
+ breadcrumbs: [
241
+ { cmd: "shapeup issue #{id}", description: "View issue" },
242
+ { cmd: "shapeup issues", description: "List all issues" }
243
+ ]
244
+ end
245
+
246
+ def defrost
247
+ id = positional_arg(1) || abort("Usage: shapeup issues defrost <id>")
248
+
249
+ result = call_tool("unarchive_issue", issue: id.to_s)
250
+
251
+ render result,
252
+ summary: "Issue ##{id} defrosted",
253
+ breadcrumbs: [
254
+ { cmd: "shapeup issue #{id}", description: "View issue" },
255
+ { cmd: "shapeup issues", description: "List all issues" }
256
+ ]
257
+ end
258
+
259
+ def assign
260
+ id = positional_arg(1) || abort("Usage: shapeup issues assign <id> [--user <id>]")
261
+ user_id = extract_option("--user") || "me"
262
+
263
+ result = call_tool("assign_user", assignable_type: "Issue", assignable_id: id.to_s, user_id: user_id.to_s)
264
+
265
+ render result,
266
+ summary: "Assigned to issue ##{id}",
267
+ breadcrumbs: [
268
+ { cmd: "shapeup issues unassign #{id}", description: "Unassign from issue" },
269
+ { cmd: "shapeup issue #{id}", description: "View issue" }
270
+ ]
271
+ end
272
+
273
+ def unassign
274
+ id = positional_arg(1) || abort("Usage: shapeup issues unassign <id> [--user <id>]")
275
+ user_id = extract_option("--user") || "me"
276
+
277
+ result = call_tool("unassign_user", assignable_type: "Issue", assignable_id: id.to_s, user_id: user_id.to_s)
278
+
279
+ render result,
280
+ summary: "Unassigned from issue ##{id}",
281
+ breadcrumbs: [
282
+ { cmd: "shapeup issues assign #{id}", description: "Assign to issue" },
283
+ { cmd: "shapeup issue #{id}", description: "View issue" }
284
+ ]
285
+ end
286
+
287
+ def watch
288
+ id = positional_arg(1) || abort("Usage: shapeup issues watch <id>")
289
+
290
+ result = call_tool("watch_issue", issue: id.to_s)
291
+
292
+ render result,
293
+ summary: "Watching issue ##{id}",
294
+ breadcrumbs: [
295
+ { cmd: "shapeup watching", description: "List watched issues" },
296
+ { cmd: "shapeup issues unwatch #{id}", description: "Stop watching" }
297
+ ]
298
+ end
299
+
300
+ def unwatch
301
+ id = positional_arg(1) || abort("Usage: shapeup issues unwatch <id>")
302
+
303
+ result = call_tool("unwatch_issue", issue: id.to_s)
304
+
305
+ render result,
306
+ summary: "Unwatched issue ##{id}",
307
+ breadcrumbs: [
308
+ { cmd: "shapeup watching", description: "List watched issues" }
309
+ ]
310
+ end
311
+
312
+ def watching
313
+ result = call_tool("list_watched_issues")
314
+
315
+ render result,
316
+ summary: "Watched Issues",
317
+ breadcrumbs: [
318
+ { cmd: "shapeup issue <id>", description: "View issue details" },
319
+ { cmd: "shapeup issues unwatch <id>", description: "Stop watching" }
320
+ ]
321
+ end
322
+
323
+ def delete
324
+ id = positional_arg(1) || abort("Usage: shapeup issues delete <id>")
325
+
326
+ result = call_tool("delete_issue", issue: id.to_s)
327
+
328
+ render result,
329
+ summary: "Issue ##{id} deleted",
330
+ breadcrumbs: [
331
+ { cmd: "shapeup issues", description: "List remaining issues" }
332
+ ]
333
+ end
334
+ end
335
+ end
336
+ end