letsdo 0.2.0 → 0.3.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 +4 -4
- data/README.md +8 -1
- data/lib/letsdo/agent_loop.rb +14 -1
- data/lib/letsdo/capture.rb +21 -6
- data/lib/letsdo/cli/launch.rb +6 -1
- data/lib/letsdo/output_streamer.rb +8 -4
- data/lib/letsdo/tui/session/view.rb +3 -5
- data/lib/letsdo/tui/session.rb +12 -2
- data/lib/letsdo/tui/terminal.rb +20 -5
- data/lib/letsdo/version.rb +1 -1
- data/lib/letsdo/watcher.rb +188 -0
- data/lib/letsdo.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f15aee491b4d0954c628e52566908e6b7ea35428c347a79822f5781198ea4be9
|
|
4
|
+
data.tar.gz: 71b47328ea44b224cb250e4bb8d0e58c052ddd054cd950fa1b058c85933f6a91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eab4462fe5a7bec0981c9b2a803a02063d1714cc049f51f5add4053f80c302b917491465dda6d90aa66a59fb668b33a5d2166ea95589d317e1958a3756564f1c
|
|
7
|
+
data.tar.gz: 5f1b243e5bce00874965683d68bebf22c2c9cec992e6f8ab1e825db3a0796781a47bf51be934b5c8c5409c7ad1cf85a83d8a872c807b8d4e5a6023c888f8ca73
|
data/README.md
CHANGED
|
@@ -99,9 +99,16 @@ The gem is built from the repository:
|
|
|
99
99
|
git clone git@github.com:sergio-fry/letsdo.git
|
|
100
100
|
cd letsdo
|
|
101
101
|
gem build letsdo.gemspec
|
|
102
|
-
gem install letsdo-0.
|
|
102
|
+
gem install letsdo-0.3.0.gem
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
> **Ruby 4.0.x note.** Some Ruby 4.0.x builds ship default gems out of sync —
|
|
106
|
+
> rdoc 8.0.0 declares `rbs >= 4.0.0` while rbs 3.x is bundled — so the
|
|
107
|
+
> post-install RDoc hook can raise `Gem::ConflictError` even though the gem
|
|
108
|
+
> files are already installed. Install without documentation to skip the
|
|
109
|
+
> hook (`gem install letsdo-0.3.0.gem --no-document`), or install a matching
|
|
110
|
+
> rbs first (`gem install rbs -v '>= 4.0.0'`).
|
|
111
|
+
|
|
105
112
|
or run it straight from the checkout without installing:
|
|
106
113
|
|
|
107
114
|
```sh
|
data/lib/letsdo/agent_loop.rb
CHANGED
|
@@ -26,9 +26,14 @@ module Letsdo
|
|
|
26
26
|
@stderr.puts('letsdo: stopped')
|
|
27
27
|
0
|
|
28
28
|
ensure
|
|
29
|
+
close_watcher
|
|
29
30
|
restore_signal_handlers
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
def close_watcher
|
|
34
|
+
@watcher&.close
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
def debug(message)
|
|
33
38
|
warn("[letsdo] loop: #{message}") if @debug
|
|
34
39
|
end
|
|
@@ -52,7 +57,15 @@ module Letsdo
|
|
|
52
57
|
@metrics = opts[:metrics]
|
|
53
58
|
@pause_gate = opts[:pause_gate]
|
|
54
59
|
@debug = opts[:debug].nil? ? ENV['LETSDO_DEBUG'] == '1' : opts[:debug]
|
|
55
|
-
@
|
|
60
|
+
@watcher = opts[:watcher] || (Watcher.new(path: opts[:watch_path]) if opts[:watch_path])
|
|
61
|
+
# Precedence is deliberate: an explicitly injected sleeper always wins
|
|
62
|
+
# over the watcher idle path — tests inject a stop/control sleeper that
|
|
63
|
+
# must never be bypassed by a configured watcher (TASK-84).
|
|
64
|
+
@sleeper = opts[:sleeper] || watcher_sleeper || ->(seconds) { sleep(seconds) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def watcher_sleeper
|
|
68
|
+
@watcher && ->(seconds) { @watcher.wait(seconds) }
|
|
56
69
|
end
|
|
57
70
|
|
|
58
71
|
def run_until_stopped
|
data/lib/letsdo/capture.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Letsdo
|
|
|
33
33
|
@err_w.close
|
|
34
34
|
readers = [quiet_reader(@out_r), quiet_reader(@err_r)]
|
|
35
35
|
status = wait_and_reap(@pid)
|
|
36
|
+
@reaped = true
|
|
36
37
|
[readers[0].value, readers[1].value, status]
|
|
37
38
|
ensure
|
|
38
39
|
cleanup
|
|
@@ -41,7 +42,7 @@ module Letsdo
|
|
|
41
42
|
private
|
|
42
43
|
|
|
43
44
|
def spawn_child
|
|
44
|
-
opts = { out: @out_w, err: @err_w }
|
|
45
|
+
opts = { out: @out_w, err: @err_w, pgroup: true }
|
|
45
46
|
opts[:chdir] = @chdir if @chdir
|
|
46
47
|
Process.spawn(*@args, **opts)
|
|
47
48
|
end
|
|
@@ -62,12 +63,26 @@ module Letsdo
|
|
|
62
63
|
status
|
|
63
64
|
end
|
|
64
65
|
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
|
|
66
|
+
# Kills the child's process group and reaps it when the normal wait was
|
|
67
|
+
# interrupted (a stop raised into the main thread). Killing the whole
|
|
68
|
+
# group also terminates any grandchildren that inherited the child's
|
|
69
|
+
# stdout/stderr pipes, so none of them is left holding the pipes open
|
|
70
|
+
# (TASK-84). A no-op when the child was already reaped normally.
|
|
71
|
+
def terminate_and_reap(pid)
|
|
68
72
|
return unless pid
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
kill_group(pid)
|
|
75
|
+
reap(pid)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def kill_group(pid)
|
|
79
|
+
Process.kill('TERM', -pid)
|
|
80
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def reap(pid)
|
|
85
|
+
Process.wait2(pid)
|
|
71
86
|
rescue Errno::ECHILD
|
|
72
87
|
nil
|
|
73
88
|
end
|
|
@@ -77,7 +92,7 @@ module Letsdo
|
|
|
77
92
|
close_quietly(@err_w)
|
|
78
93
|
close_quietly(@out_r)
|
|
79
94
|
close_quietly(@err_r)
|
|
80
|
-
|
|
95
|
+
terminate_and_reap(@pid) unless @reaped
|
|
81
96
|
end
|
|
82
97
|
|
|
83
98
|
def close_quietly(io)
|
data/lib/letsdo/cli/launch.rb
CHANGED
|
@@ -76,7 +76,12 @@ module Letsdo
|
|
|
76
76
|
AgentLoop.new(name: name, handle: handle, agent: agent,
|
|
77
77
|
task_provider: -> { provider.call },
|
|
78
78
|
wait_seconds: wait_seconds, sleeper: @sleeper, stderr: opts[:stderr],
|
|
79
|
-
metrics: opts[:metrics], pause_gate: opts[:pause_gate]
|
|
79
|
+
metrics: opts[:metrics], pause_gate: opts[:pause_gate],
|
|
80
|
+
watcher: backlog_watcher)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def backlog_watcher
|
|
84
|
+
Watcher.new(path: File.join(@root, 'backlog'), poll_seconds: wait_seconds)
|
|
80
85
|
end
|
|
81
86
|
end
|
|
82
87
|
end
|
|
@@ -37,10 +37,14 @@ module Letsdo
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def finish
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
if @last_char && @last_char != "\n"
|
|
41
|
+
text_sink.write("\n")
|
|
42
|
+
text_sink.flush unless @log
|
|
43
|
+
end
|
|
44
|
+
# Reset so the next run's finish is a no-op unless new text arrived —
|
|
45
|
+
# otherwise a tool-only run after a text run writes a spurious blank
|
|
46
|
+
# line (TASK-77).
|
|
47
|
+
@last_char = nil
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
private
|
|
@@ -7,11 +7,9 @@ module Letsdo
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def input_loop
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
loop { break if @stop || !poll_once }
|
|
14
|
-
end
|
|
10
|
+
repaint
|
|
11
|
+
@last_repaint = @clock.call
|
|
12
|
+
loop { break if @stop || !poll_once }
|
|
15
13
|
rescue StandardError
|
|
16
14
|
nil
|
|
17
15
|
end
|
data/lib/letsdo/tui/session.rb
CHANGED
|
@@ -25,8 +25,7 @@ module Letsdo
|
|
|
25
25
|
def run(&work)
|
|
26
26
|
install_winch_handler
|
|
27
27
|
@terminal.enter
|
|
28
|
-
|
|
29
|
-
work.call
|
|
28
|
+
run_work(&work)
|
|
30
29
|
rescue Letsdo::Stopped
|
|
31
30
|
0
|
|
32
31
|
ensure
|
|
@@ -35,6 +34,13 @@ module Letsdo
|
|
|
35
34
|
restore_winch_handler
|
|
36
35
|
end
|
|
37
36
|
|
|
37
|
+
def run_work(&work)
|
|
38
|
+
with_raw_input do
|
|
39
|
+
start_input_thread
|
|
40
|
+
work.call
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
38
44
|
private
|
|
39
45
|
|
|
40
46
|
def assign_identity(opts)
|
|
@@ -68,6 +74,10 @@ module Letsdo
|
|
|
68
74
|
@input_thread = nil
|
|
69
75
|
end
|
|
70
76
|
|
|
77
|
+
# Enters raw mode on the keyboard for the duration of the block. Held
|
|
78
|
+
# by the main thread (not the killable input thread) so the terminal
|
|
79
|
+
# state is restored on every quit path — a killed input thread would
|
|
80
|
+
# leave the shared tty in raw mode and break the calling shell (TASK-83).
|
|
71
81
|
def with_raw_input(&block)
|
|
72
82
|
if @input.stdin.tty? && @input.stdin.respond_to?(:raw)
|
|
73
83
|
@input.stdin.raw(&block)
|
data/lib/letsdo/tui/terminal.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'tty-cursor'
|
|
4
|
-
|
|
5
3
|
module Letsdo
|
|
6
4
|
module Tui
|
|
7
5
|
# Thin wrapper over the terminal: alternate screen, cursor handling and
|
|
@@ -36,19 +34,28 @@ module Letsdo
|
|
|
36
34
|
|
|
37
35
|
# Enters the alternate screen and hides the cursor.
|
|
38
36
|
def enter
|
|
39
|
-
write(ENTER_ALT_SCREEN +
|
|
37
|
+
write(ENTER_ALT_SCREEN + cursor.hide)
|
|
40
38
|
end
|
|
41
39
|
|
|
42
40
|
# Shows the cursor and leaves the alternate screen.
|
|
43
41
|
def leave
|
|
44
|
-
write(
|
|
42
|
+
write(cursor.show + LEAVE_ALT_SCREEN)
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
# Repaints the whole frame: cursor to the top left, then the frame.
|
|
48
46
|
#
|
|
47
|
+
# Row separators are written as CRLF (\r\n), never bare LF. The TUI
|
|
48
|
+
# input thread holds stdin in io-console raw mode for the whole
|
|
49
|
+
# session, and raw mode clears OPOST on the shared tty — without it a
|
|
50
|
+
# bare \n no longer implies a carriage return, so every row after the
|
|
51
|
+
# first would start at the previous row's end column and the frame
|
|
52
|
+
# would wrap/scroll into the "blank activity pane" scramble (TASK-82).
|
|
53
|
+
# Explicit CRLF renders correctly both in raw mode and on terminals
|
|
54
|
+
# whose ONLCR already expands \n (a doubled CR is harmless).
|
|
55
|
+
#
|
|
49
56
|
# @param frame [String] the rendered screen (see Letsdo::Tui::Renderer)
|
|
50
57
|
def render(frame)
|
|
51
|
-
write(HOME + frame)
|
|
58
|
+
write(HOME + frame.gsub("\n", "\r\n"))
|
|
52
59
|
end
|
|
53
60
|
|
|
54
61
|
# The current terminal size.
|
|
@@ -61,6 +68,14 @@ module Letsdo
|
|
|
61
68
|
|
|
62
69
|
private
|
|
63
70
|
|
|
71
|
+
# Loaded lazily (like tty-screen/tty-reader) so requiring letsdo on a
|
|
72
|
+
# clean Ruby loads no tty-* gems — only the interactive TTY path needs
|
|
73
|
+
# them (TASK-78).
|
|
74
|
+
def cursor
|
|
75
|
+
require 'tty-cursor' unless defined?(TTY::Cursor)
|
|
76
|
+
TTY::Cursor
|
|
77
|
+
end
|
|
78
|
+
|
|
64
79
|
def write(text)
|
|
65
80
|
@stream.write(text)
|
|
66
81
|
@stream.flush
|
data/lib/letsdo/version.rb
CHANGED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fiddle/import'
|
|
4
|
+
|
|
5
|
+
module Letsdo
|
|
6
|
+
# OS file-change watcher for the backlog folder.
|
|
7
|
+
#
|
|
8
|
+
# On Linux it subscribes to inotify (via Fiddle, no external gem) and wakes
|
|
9
|
+
# as soon as a backlog entry changes; everywhere else it degrades to the
|
|
10
|
+
# same periodic polling a plain sleeper would do. Either way the wait is
|
|
11
|
+
# interruptible — a signal trap raising Letsdo::Stopped breaks the
|
|
12
|
+
# underlying IO.select — and an internal self-pipe lets #wake interrupt a
|
|
13
|
+
# blocked #wait from another thread.
|
|
14
|
+
class Watcher
|
|
15
|
+
# inotify event masks we subscribe to (Linux <sys/inotify.h>).
|
|
16
|
+
IN_ACCESS = 0x00000001
|
|
17
|
+
IN_MODIFY = 0x00000002
|
|
18
|
+
IN_ATTRIB = 0x00000004
|
|
19
|
+
IN_CLOSE_WRITE = 0x00000008
|
|
20
|
+
IN_CLOSE_NOWRITE = 0x00000010
|
|
21
|
+
IN_OPEN = 0x00000020
|
|
22
|
+
IN_MOVED_FROM = 0x00000040
|
|
23
|
+
IN_MOVED_TO = 0x00000080
|
|
24
|
+
IN_CREATE = 0x00000100
|
|
25
|
+
IN_DELETE = 0x00000200
|
|
26
|
+
IN_DELETE_SELF = 0x00000400
|
|
27
|
+
IN_MOVE_SELF = 0x00000800
|
|
28
|
+
|
|
29
|
+
RELEVANT_MASK = IN_CREATE | IN_MODIFY | IN_CLOSE_WRITE | IN_DELETE |
|
|
30
|
+
IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE_SELF | IN_MOVE_SELF
|
|
31
|
+
|
|
32
|
+
EXCLUDED_DIR = '.locks'
|
|
33
|
+
|
|
34
|
+
# @param path [String] the watched folder (its direct entries)
|
|
35
|
+
# @param poll_seconds [Float] fallback poll interval / select timeout
|
|
36
|
+
def initialize(path:, poll_seconds: 10.0)
|
|
37
|
+
@poll_seconds = poll_seconds
|
|
38
|
+
@wake_r, @wake_w = IO.pipe
|
|
39
|
+
@backend = build_backend(path)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Blocks until a relevant backlog change, an explicit #wake, or the poll
|
|
43
|
+
# timeout. Irrelevant events (e.g. .locks churn) keep the wait going for
|
|
44
|
+
# the remainder of the timeout.
|
|
45
|
+
#
|
|
46
|
+
# @return [Symbol] :change, :wake, or :timeout
|
|
47
|
+
def wait(timeout = @poll_seconds)
|
|
48
|
+
deadline = monotonic + timeout
|
|
49
|
+
loop do
|
|
50
|
+
result = wait_once(deadline)
|
|
51
|
+
return result if result
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Interrupts a blocked #wait from another thread.
|
|
56
|
+
def wake
|
|
57
|
+
@wake_w.write_nonblock('.')
|
|
58
|
+
rescue IO::WaitWritable, Errno::EPIPE
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def close
|
|
63
|
+
@backend&.close
|
|
64
|
+
close_io(@wake_w)
|
|
65
|
+
close_io(@wake_r)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def wait_once(deadline)
|
|
71
|
+
remaining = deadline - monotonic
|
|
72
|
+
return :timeout if remaining <= 0
|
|
73
|
+
|
|
74
|
+
ready = IO.select(read_fds, nil, nil, remaining)
|
|
75
|
+
return :timeout if ready.nil?
|
|
76
|
+
return :wake if wake_ready?(ready)
|
|
77
|
+
|
|
78
|
+
@backend.relevant_event? ? :change : nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def read_fds
|
|
82
|
+
fds = [@wake_r]
|
|
83
|
+
fds << @backend.io if @backend
|
|
84
|
+
fds
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def wake_ready?(ready)
|
|
88
|
+
return false unless ready.first.include?(@wake_r)
|
|
89
|
+
|
|
90
|
+
drain_wake
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def drain_wake
|
|
95
|
+
@wake_r.read_nonblock(4096)
|
|
96
|
+
rescue IO::WaitReadable, EOFError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_backend(path)
|
|
101
|
+
InotifyBackend.new(path, RELEVANT_MASK)
|
|
102
|
+
rescue InotifyBackend::Unavailable, Fiddle::DLError
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def monotonic
|
|
107
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def close_io(io)
|
|
111
|
+
io.close unless io.nil? || io.closed?
|
|
112
|
+
rescue IOError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Linux inotify backend. Construction raises InotifyBackend::Unavailable
|
|
117
|
+
# when inotify is missing or the watch cannot be set up, so the watcher
|
|
118
|
+
# falls back to polling through its self-pipe timeout.
|
|
119
|
+
class InotifyBackend
|
|
120
|
+
# libc inotify bindings via Fiddle (no external gem).
|
|
121
|
+
module Lib
|
|
122
|
+
extend Fiddle::Importer
|
|
123
|
+
dlload Fiddle.dlopen(nil)
|
|
124
|
+
extern 'int inotify_init1(int)'
|
|
125
|
+
extern 'int inotify_add_watch(int, const char*, uint32_t)'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Signals that inotify is unavailable and the watcher should fall back.
|
|
129
|
+
class Unavailable < StandardError; end
|
|
130
|
+
|
|
131
|
+
IN_CLOEXEC = 0x00080000
|
|
132
|
+
EVENT_HEADER_SIZE = 16
|
|
133
|
+
|
|
134
|
+
attr_reader :io
|
|
135
|
+
|
|
136
|
+
def initialize(path, mask)
|
|
137
|
+
fd = Lib.inotify_init1(IN_CLOEXEC)
|
|
138
|
+
guard(fd)
|
|
139
|
+
@io = IO.new(fd, 'r', autoclose: true)
|
|
140
|
+
guard(Lib.inotify_add_watch(fd, path, mask))
|
|
141
|
+
rescue Unavailable, Fiddle::DLError
|
|
142
|
+
@io&.close
|
|
143
|
+
raise Unavailable
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Drains pending events and reports whether any relevant (non-.locks)
|
|
147
|
+
# entry changed.
|
|
148
|
+
def relevant_event?
|
|
149
|
+
buffer = @io.read_nonblock(8192)
|
|
150
|
+
parse_relevant(buffer)
|
|
151
|
+
rescue IO::WaitReadable, EOFError
|
|
152
|
+
false
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def close
|
|
156
|
+
@io&.close
|
|
157
|
+
rescue IOError
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def guard(value)
|
|
164
|
+
raise Unavailable if value.negative?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def parse_relevant(buffer)
|
|
168
|
+
offset = 0
|
|
169
|
+
relevant = false
|
|
170
|
+
while offset + EVENT_HEADER_SIZE <= buffer.bytesize
|
|
171
|
+
_wd, mask, _cookie, len = buffer.byteslice(offset, EVENT_HEADER_SIZE).unpack('L4')
|
|
172
|
+
name = buffer.byteslice(offset + EVENT_HEADER_SIZE, len).to_s.sub(/\0.*/m, '')
|
|
173
|
+
relevant ||= relevant_mask?(mask) && !excluded?(name)
|
|
174
|
+
offset += EVENT_HEADER_SIZE + len
|
|
175
|
+
end
|
|
176
|
+
relevant
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def relevant_mask?(mask)
|
|
180
|
+
(mask & Watcher::RELEVANT_MASK) != 0
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def excluded?(name)
|
|
184
|
+
name == Watcher::EXCLUDED_DIR || name.start_with?("#{Watcher::EXCLUDED_DIR}/")
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
data/lib/letsdo.rb
CHANGED
|
@@ -30,6 +30,7 @@ require_relative 'letsdo/agent'
|
|
|
30
30
|
require_relative 'letsdo/capture'
|
|
31
31
|
require_relative 'letsdo/backlog_tasks'
|
|
32
32
|
require_relative 'letsdo/loop'
|
|
33
|
+
require_relative 'letsdo/watcher'
|
|
33
34
|
require_relative 'letsdo/agent_loop'
|
|
34
35
|
require_relative 'letsdo/control'
|
|
35
36
|
require_relative 'letsdo/tui'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: letsdo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergei O. Udalov
|
|
@@ -122,6 +122,7 @@ files:
|
|
|
122
122
|
- lib/letsdo/tui/session/view.rb
|
|
123
123
|
- lib/letsdo/tui/terminal.rb
|
|
124
124
|
- lib/letsdo/version.rb
|
|
125
|
+
- lib/letsdo/watcher.rb
|
|
125
126
|
homepage: https://github.com/sergio-fry/letsdo
|
|
126
127
|
licenses:
|
|
127
128
|
- MIT
|