claude-inbox 0.13.0 → 0.13.1
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 +4 -4
- data/exe/claude-inbox +4 -0
- data/lib/claude_inbox/app.rb +6 -4
- data/lib/claude_inbox/dialog.rb +6 -6
- data/lib/claude_inbox/headless.rb +4 -1
- data/lib/claude_inbox/new_session_form.rb +9 -16
- data/lib/claude_inbox/renderer.rb +6 -8
- data/lib/claude_inbox/slash_commands.rb +3 -10
- data/lib/claude_inbox/store/row.rb +4 -4
- data/lib/claude_inbox/subprocess.rb +2 -4
- data/lib/claude_inbox/terminal.rb +5 -10
- data/lib/claude_inbox/text_buffer.rb +4 -2
- data/lib/claude_inbox/updater.rb +5 -3
- data/lib/claude_inbox/vt_screen.rb +2 -1
- data/lib/claude_inbox.rb +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36fa381f8d03173896c6b44dff47534ed54119c9ff529833a9783c7f5c524506
|
|
4
|
+
data.tar.gz: '0386da94a6ffa5259bbad165857821f44b082dc5a64d957beae352c46dc61100'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb933d5723ee131285824d2faea2570762854a4695da2ecd98716d5b9d981b2b36aece7abbda8acf92fa063b47f52f40ac2931136ef5fecee194f0fda7cdc72c
|
|
7
|
+
data.tar.gz: 3593591f4192056035fdc13e3480a26824794be46df0d8aa5b72fdb5c84a4b6b039d658b4279ec64cdf35ec94c2d65533117321c01bdd9b3e06a4267571a83b6
|
data/exe/claude-inbox
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
# Claude's files are UTF-8 whatever the locale says; under LANG=C a read of
|
|
5
|
+
# ~/.claude.json would otherwise come back US-ASCII and fail to parse.
|
|
6
|
+
Encoding.default_external = Encoding::UTF_8
|
|
7
|
+
|
|
4
8
|
require "claude_inbox"
|
|
5
9
|
require "claude_inbox/cli"
|
|
6
10
|
|
data/lib/claude_inbox/app.rb
CHANGED
|
@@ -87,7 +87,7 @@ module ClaudeInbox
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def run
|
|
90
|
-
@booted_at =
|
|
90
|
+
@booted_at = monotonic
|
|
91
91
|
install_traps
|
|
92
92
|
@terminal.enter
|
|
93
93
|
@poller.start
|
|
@@ -264,7 +264,7 @@ module ClaudeInbox
|
|
|
264
264
|
end
|
|
265
265
|
|
|
266
266
|
def render
|
|
267
|
-
t0 =
|
|
267
|
+
t0 = monotonic
|
|
268
268
|
now = Time.now
|
|
269
269
|
all = @store.sections(now)
|
|
270
270
|
@snapshot.write(all, now)
|
|
@@ -283,7 +283,7 @@ module ClaudeInbox
|
|
|
283
283
|
frame = frame.plain unless @color # --no-color: decided here and nowhere else
|
|
284
284
|
placed(frame)
|
|
285
285
|
@terminal.paint(frame.lines)
|
|
286
|
-
dt =
|
|
286
|
+
dt = monotonic - t0
|
|
287
287
|
Debug.log("render #{(dt * 1000).round}ms") if dt > 0.05
|
|
288
288
|
end
|
|
289
289
|
|
|
@@ -309,11 +309,13 @@ module ClaudeInbox
|
|
|
309
309
|
# already says how to retry.
|
|
310
310
|
def loading_for
|
|
311
311
|
return nil if @last_poll || @error || !@booted_at
|
|
312
|
-
|
|
312
|
+
monotonic - @booted_at
|
|
313
313
|
end
|
|
314
314
|
|
|
315
315
|
def notice(msg)
|
|
316
316
|
@notice = [msg, Time.now + 4]
|
|
317
317
|
end
|
|
318
|
+
|
|
319
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
318
320
|
end
|
|
319
321
|
end
|
data/lib/claude_inbox/dialog.rb
CHANGED
|
@@ -28,11 +28,11 @@ module ClaudeInbox
|
|
|
28
28
|
.split("\n")
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
private
|
|
32
|
+
|
|
31
33
|
# How wide the box wants to be; the frame caps it at the terminal.
|
|
32
34
|
def fit = self.class::WIDTH
|
|
33
35
|
|
|
34
|
-
private
|
|
35
|
-
|
|
36
36
|
def key(name) = @theme.cyan_bold(name)
|
|
37
37
|
|
|
38
38
|
# A key and what it does.
|
|
@@ -83,10 +83,6 @@ module ClaudeInbox
|
|
|
83
83
|
|
|
84
84
|
def title = " #{kind.to_s.capitalize} pull request "
|
|
85
85
|
|
|
86
|
-
# Titles vary, so the box grows to the longest line plus some air,
|
|
87
|
-
# and never shrinks below a plain dialog.
|
|
88
|
-
def fit = [WIDTH, lines(Float::INFINITY, nil).map { |l| Text.width(l) }.max + MARGIN].max
|
|
89
|
-
|
|
90
86
|
def lines(width, _caret)
|
|
91
87
|
(@prs.each_with_index.map { |pr, i| " #{key((i + 1).to_s)} #{label(pr)}" } +
|
|
92
88
|
["", " #{hint(KEYS.fetch(kind), "#{kind} #{@prs.first.short}")} · #{key("esc")} #{@theme.dim("cancel")}"])
|
|
@@ -102,6 +98,10 @@ module ClaudeInbox
|
|
|
102
98
|
|
|
103
99
|
private
|
|
104
100
|
|
|
101
|
+
# Titles vary, so the box grows to the longest line plus some air,
|
|
102
|
+
# and never shrinks below a plain dialog.
|
|
103
|
+
def fit = [WIDTH, lines(Float::INFINITY, nil).map { |l| Text.width(l) }.max + MARGIN].max
|
|
104
|
+
|
|
105
105
|
# The list's badge, then the title. A bare #190 is ambiguous only
|
|
106
106
|
# once the list spans repos.
|
|
107
107
|
def label(pr)
|
|
@@ -33,8 +33,11 @@ module ClaudeInbox
|
|
|
33
33
|
def run
|
|
34
34
|
return false unless @lock.take(role: "headless")
|
|
35
35
|
@poller.start
|
|
36
|
-
Thread.new { watch_for_updates } if @updater
|
|
36
|
+
updates = Thread.new { watch_for_updates } if @updater
|
|
37
37
|
loop { @queue.pop }
|
|
38
|
+
ensure
|
|
39
|
+
@poller.stop
|
|
40
|
+
updates&.kill
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
private
|
|
@@ -12,12 +12,11 @@ require_relative "text_buffer"
|
|
|
12
12
|
require_relative "theme"
|
|
13
13
|
|
|
14
14
|
module ClaudeInbox
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# App wraps them in a box.
|
|
15
|
+
# The `n` form a new session is composed in. Pure: it says what happened
|
|
16
|
+
# and the App acts on it.
|
|
18
17
|
class NewSessionForm
|
|
19
18
|
# `value` is a TextBuffer on :text and :multiline fields and the picked
|
|
20
|
-
# string on :choice ones
|
|
19
|
+
# string on :choice ones.
|
|
21
20
|
Field = Struct.new(:key, :label, :kind, :value, :choices)
|
|
22
21
|
|
|
23
22
|
# A directory the Directory field offers, shown by its shortest
|
|
@@ -71,11 +70,9 @@ module ClaudeInbox
|
|
|
71
70
|
|
|
72
71
|
# => :cancel | :start | :start_and_attach | :changed
|
|
73
72
|
def press(name, raw)
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
# it; there is no way to cancel a spawn already under way, so every
|
|
78
|
-
# key is swallowed until the App reports back.
|
|
73
|
+
# A start leaves the form up until the spawn has worked, so a failure
|
|
74
|
+
# can hand the prompt back. A spawn under way can't be cancelled, so
|
|
75
|
+
# every key is swallowed until the App reports back.
|
|
79
76
|
return :changed if @busy
|
|
80
77
|
return confirm_discard_press(name, raw) if @confirm_discard
|
|
81
78
|
@error = nil
|
|
@@ -149,8 +146,7 @@ module ClaudeInbox
|
|
|
149
146
|
@defaults = Settings.defaults(cwd, home: @home)
|
|
150
147
|
end
|
|
151
148
|
|
|
152
|
-
#
|
|
153
|
-
# every choice visible. Exactly `height` lines.
|
|
149
|
+
# The whole body, exactly `height` lines.
|
|
154
150
|
def screen(width, height)
|
|
155
151
|
inner_w = width - 4
|
|
156
152
|
fixed = 3 + 2 + 1 + (@fields.size - 1) + 1
|
|
@@ -189,11 +185,8 @@ module ClaudeInbox
|
|
|
189
185
|
@theme.hints(keys)
|
|
190
186
|
end
|
|
191
187
|
|
|
192
|
-
# The App's `claude --bg` call failed after
|
|
193
|
-
#
|
|
194
|
-
# a permissions error. The composed prompt is still here, so the form
|
|
195
|
-
# reopens on it with the reason shown instead of the App just logging
|
|
196
|
-
# the failure and losing the draft.
|
|
188
|
+
# The App's `claude --bg` call failed after a start. The form shows why,
|
|
189
|
+
# over the prompt it kept.
|
|
197
190
|
def submission_failed(message)
|
|
198
191
|
@busy = false
|
|
199
192
|
@error = message
|
|
@@ -7,9 +7,8 @@ require_relative "theme"
|
|
|
7
7
|
require_relative "store"
|
|
8
8
|
|
|
9
9
|
module ClaudeInbox
|
|
10
|
-
# sections
|
|
11
|
-
#
|
|
12
|
-
# Pure: no terminal, no IO, no clock beyond the `now` it is handed.
|
|
10
|
+
# Turns the sections and a View into a Frame, every line exactly `width`
|
|
11
|
+
# columns. Pure: no terminal, no IO, no clock beyond the `now` it is handed.
|
|
13
12
|
class Renderer
|
|
14
13
|
Frame = Struct.new(:lines, :items, :top) do
|
|
15
14
|
# The same frame for a terminal drawn without color.
|
|
@@ -141,7 +140,7 @@ module ClaudeInbox
|
|
|
141
140
|
bar = "█" * filled + "░" * (10 - filled)
|
|
142
141
|
left = w.resets_at.to_i - now.to_i if resets && w.resets_at
|
|
143
142
|
figure = left&.positive? ? "#{w.percent}% · #{Text.age(left)} left" : "#{w.percent}%"
|
|
144
|
-
"#{@p.dim(w.label)} #{@theme.
|
|
143
|
+
"#{@p.dim(w.label)} #{@theme.tint(usage_hue(w.percent), bar)} #{@p.dim(figure)}"
|
|
145
144
|
end.join(" ")
|
|
146
145
|
end
|
|
147
146
|
|
|
@@ -312,7 +311,7 @@ module ClaudeInbox
|
|
|
312
311
|
chrome = 1 + 1 + 1 + 1 + 1 + 2 + Text.width(meta) + 2 + 1
|
|
313
312
|
# Project is only cut once the label has given up all its space too,
|
|
314
313
|
# so the row can never exceed `width` and fall into Text.pad's blind
|
|
315
|
-
# tail-chop
|
|
314
|
+
# tail-chop.
|
|
316
315
|
project_text = Text.truncate(s.project, [width - chrome, 0].max)
|
|
317
316
|
project = quiet ? @p.dim(project_text) : @theme.cyan(project_text)
|
|
318
317
|
|
|
@@ -416,8 +415,8 @@ module ClaudeInbox
|
|
|
416
415
|
end
|
|
417
416
|
|
|
418
417
|
# "working" while the agent is thinking, "idle" once it has stopped and
|
|
419
|
-
# only the work it kicked off is still open
|
|
420
|
-
#
|
|
418
|
+
# only the work it kicked off is still open. Either way the work is
|
|
419
|
+
# named, since "working · 2 agents" answers "working on what?".
|
|
421
420
|
def working_badge(s)
|
|
422
421
|
return @theme.yellow("waiting#{": #{s.waiting_for}" if s.waiting_for}") if s.status == "waiting"
|
|
423
422
|
label = s.job_state&.in_flight_label
|
|
@@ -431,7 +430,6 @@ module ClaudeInbox
|
|
|
431
430
|
|
|
432
431
|
# ----- modal --------------------------------------------------------------
|
|
433
432
|
|
|
434
|
-
# Centre a block of lines over the frame.
|
|
435
433
|
def overlay(lines, block, width)
|
|
436
434
|
block_w = block.map { |l| Text.width(l) }.max || 0
|
|
437
435
|
left = [(width - block_w) / 2, 0].max
|
|
@@ -4,13 +4,9 @@ require_relative "settings"
|
|
|
4
4
|
|
|
5
5
|
module ClaudeInbox
|
|
6
6
|
# The slash commands `claude` would offer at its own prompt, read from the
|
|
7
|
-
# same places it reads them
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# `plugin:name`), and the skills claude.ai syncs down
|
|
11
|
-
# (`anthropic-skills:name`). Built-ins live inside the CLI and are not
|
|
12
|
-
# listed; typing one still works, since the prompt goes to `claude --bg`
|
|
13
|
-
# verbatim.
|
|
7
|
+
# same places it reads them (docs/cli-quirks.md). Built-ins live inside
|
|
8
|
+
# the CLI and are not listed; typing one still works, since the prompt
|
|
9
|
+
# goes to `claude --bg` verbatim.
|
|
14
10
|
#
|
|
15
11
|
# Pure: filesystem in, plain values out. Nothing here runs `claude`.
|
|
16
12
|
module SlashCommands
|
|
@@ -61,7 +57,6 @@ module ClaudeInbox
|
|
|
61
57
|
end
|
|
62
58
|
end
|
|
63
59
|
|
|
64
|
-
# "name", or "plugin:name" when it comes from a plugin.
|
|
65
60
|
def qualify(prefix, name) = [prefix, name].compact.join(":")
|
|
66
61
|
|
|
67
62
|
# `installed_plugins.json` maps "name@marketplace" to where the plugin
|
|
@@ -77,8 +72,6 @@ module ClaudeInbox
|
|
|
77
72
|
end
|
|
78
73
|
end
|
|
79
74
|
|
|
80
|
-
# claude.ai's synced skills sit in one bucket per account under
|
|
81
|
-
# ~/.claude/skills/synced and answer to the anthropic-skills prefix.
|
|
82
75
|
def synced(home)
|
|
83
76
|
Dir.glob(File.join(home, ".claude", "skills", "synced", "*", "")).flat_map do |bucket|
|
|
84
77
|
skills(bucket, prefix: SYNCED_PLUGIN)
|
|
@@ -54,10 +54,6 @@ module ClaudeInbox
|
|
|
54
54
|
# Active rather than Settled, until the state changes again.
|
|
55
55
|
def acknowledged? = unchanged_since?(entry&.acknowledged_at)
|
|
56
56
|
|
|
57
|
-
# Finished or needing you, so it carries a read or unread mark. Only a
|
|
58
|
-
# session you can attach to counts, since nothing else ever clears it.
|
|
59
|
-
def markable? = session.actionable? && (session.finished? || session.needs_you?)
|
|
60
|
-
|
|
61
57
|
# Finished or needing you since you last attached.
|
|
62
58
|
def unread?
|
|
63
59
|
return false unless markable?
|
|
@@ -122,6 +118,10 @@ module ClaudeInbox
|
|
|
122
118
|
|
|
123
119
|
private
|
|
124
120
|
|
|
121
|
+
# Finished or needing you, so it carries a read or unread mark. Only a
|
|
122
|
+
# session you can attach to counts, since nothing else ever clears it.
|
|
123
|
+
def markable? = session.actionable? && (session.finished? || session.needs_you?)
|
|
124
|
+
|
|
125
125
|
def unchanged_since?(at) = at && entry.state_since.to_i <= at.to_i
|
|
126
126
|
end
|
|
127
127
|
end
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ClaudeInbox
|
|
4
|
-
# Runs a helper command with
|
|
5
|
-
# it
|
|
6
|
-
# "active process" of the tty in the tab title; without this the title
|
|
7
|
-
# flips to "claude" or "ps" on every poll.
|
|
4
|
+
# Runs a helper command with its output captured, in a session of its own
|
|
5
|
+
# so it never reaches the tab title (docs/cli-quirks.md).
|
|
8
6
|
module Subprocess
|
|
9
7
|
Result = Struct.new(:out, :err, :status) do
|
|
10
8
|
def success? = status.success?
|
|
@@ -6,10 +6,8 @@ require "tty-screen"
|
|
|
6
6
|
require_relative "painter"
|
|
7
7
|
|
|
8
8
|
module ClaudeInbox
|
|
9
|
-
# The screen the inbox draws on
|
|
10
|
-
#
|
|
11
|
-
# and the Painter that diffs frames onto it. `release` hands the whole
|
|
12
|
-
# thing to a child that wants the tty and takes it back afterwards.
|
|
9
|
+
# The screen the inbox draws on, lent with `release` to a child that
|
|
10
|
+
# wants the tty.
|
|
13
11
|
class Terminal
|
|
14
12
|
ALT_ON = "\e[?1049h"
|
|
15
13
|
ALT_OFF = "\e[?1049l"
|
|
@@ -19,11 +17,8 @@ module ClaudeInbox
|
|
|
19
17
|
# know the mode ignore it and keep scrolling their own history.
|
|
20
18
|
WHEEL_KEYS_ON = "\e[?1007h"
|
|
21
19
|
WHEEL_KEYS_OFF = "\e[?1007l"
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# instead of the terminal only ever translating the wheel to arrow
|
|
25
|
-
# keys. Terminals that don't understand either mode just ignore it and
|
|
26
|
-
# fall back to WHEEL_KEYS_ON's translation, or their own scrollback.
|
|
20
|
+
# Button events in the SGR encoding, which Mouse parses. A terminal that
|
|
21
|
+
# knows neither mode falls back to WHEEL_KEYS_ON, or its own scrollback.
|
|
27
22
|
MOUSE_ON = "\e[?1000h\e[?1006h"
|
|
28
23
|
MOUSE_OFF = "\e[?1006l\e[?1000l"
|
|
29
24
|
# Bracketed paste: what is pasted arrives fenced off from what is typed
|
|
@@ -54,7 +49,7 @@ module ClaudeInbox
|
|
|
54
49
|
resized
|
|
55
50
|
end
|
|
56
51
|
|
|
57
|
-
# Safe to call twice, and from at_exit
|
|
52
|
+
# Safe to call twice, and from at_exit.
|
|
58
53
|
def restore
|
|
59
54
|
return if @restored
|
|
60
55
|
@restored = true
|
|
@@ -35,6 +35,8 @@ module ClaudeInbox
|
|
|
35
35
|
|
|
36
36
|
def initialize(text = "")
|
|
37
37
|
@widths = Hash.new { |h, s| h[s] = Text.width(s) }
|
|
38
|
+
@images = 0
|
|
39
|
+
@pastes = 0
|
|
38
40
|
replace(text)
|
|
39
41
|
end
|
|
40
42
|
|
|
@@ -77,11 +79,11 @@ module ClaudeInbox
|
|
|
77
79
|
|
|
78
80
|
# Chips are numbered per kind and never reused, so a second image is
|
|
79
81
|
# `[Image #2]` even after the first was deleted, as Claude Code does.
|
|
80
|
-
def attach(path) = place(Image.new(@images
|
|
82
|
+
def attach(path) = place(Image.new(@images += 1, path))
|
|
81
83
|
|
|
82
84
|
def paste(text)
|
|
83
85
|
return insert(text) if text.size <= FOLD_CHARS && text.count("\n") < FOLD_LINES
|
|
84
|
-
place(Pasted.new(@pastes
|
|
86
|
+
place(Pasted.new(@pastes += 1, text))
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
# Handles one keypress: readline's editing keys, plus printable text.
|
data/lib/claude_inbox/updater.rb
CHANGED
|
@@ -133,7 +133,7 @@ module ClaudeInbox
|
|
|
133
133
|
# clone follows the fork.
|
|
134
134
|
def fetch_upstream
|
|
135
135
|
remote = git("config", "branch.main.remote") or raise Error, "main tracks no remote"
|
|
136
|
-
fetched =
|
|
136
|
+
fetched = run_git("fetch", "--quiet", remote)
|
|
137
137
|
raise Error, "git fetch #{remote} failed: #{fetched.err.strip}" unless fetched.success?
|
|
138
138
|
git("rev-parse", "main@{upstream}") or raise Error, "main tracks no remote"
|
|
139
139
|
end
|
|
@@ -142,12 +142,14 @@ module ClaudeInbox
|
|
|
142
142
|
branch = git("symbolic-ref", "--short", "HEAD")
|
|
143
143
|
return Applied.new(success: false, output: "not on main (on #{branch})") unless branch == "main"
|
|
144
144
|
return Applied.new(success: false, output: "the working tree is dirty") unless git("status", "--porcelain") == ""
|
|
145
|
-
merged =
|
|
145
|
+
merged = run_git("merge", "--ff-only", "--quiet", "main@{upstream}")
|
|
146
146
|
Applied.from(merged)
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
+
def run_git(*args) = @run.call("git", "-C", @root, *args)
|
|
150
|
+
|
|
149
151
|
def git(*args)
|
|
150
|
-
result =
|
|
152
|
+
result = run_git(*args)
|
|
151
153
|
result.out.strip if result.success?
|
|
152
154
|
end
|
|
153
155
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "strscan"
|
|
4
4
|
require "unicode/display_width"
|
|
5
|
+
require_relative "text"
|
|
5
6
|
|
|
6
7
|
module ClaudeInbox
|
|
7
8
|
# A deliberately small cursor-addressed screen model that turns captured
|
|
@@ -22,7 +23,7 @@ module ClaudeInbox
|
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def feed(str)
|
|
25
|
-
ss = StringScanner.new(
|
|
26
|
+
ss = StringScanner.new(Text.utf8(str))
|
|
26
27
|
until ss.eos?
|
|
27
28
|
if ss.scan(CSI)
|
|
28
29
|
csi(ss[1], ss[2])
|
data/lib/claude_inbox.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-inbox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.13.
|
|
4
|
+
version: 0.13.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jordan Byron
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: optparse
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.5'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.5'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: tty-cursor
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|