harnex 0.2.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 +7 -0
- data/GUIDE.md +242 -0
- data/LICENSE +21 -0
- data/README.md +119 -0
- data/TECHNICAL.md +595 -0
- data/bin/harnex +18 -0
- data/lib/harnex/adapters/base.rb +134 -0
- data/lib/harnex/adapters/claude.rb +105 -0
- data/lib/harnex/adapters/codex.rb +112 -0
- data/lib/harnex/adapters/generic.rb +14 -0
- data/lib/harnex/adapters.rb +32 -0
- data/lib/harnex/cli.rb +115 -0
- data/lib/harnex/commands/guide.rb +23 -0
- data/lib/harnex/commands/logs.rb +184 -0
- data/lib/harnex/commands/pane.rb +251 -0
- data/lib/harnex/commands/recipes.rb +104 -0
- data/lib/harnex/commands/run.rb +384 -0
- data/lib/harnex/commands/send.rb +415 -0
- data/lib/harnex/commands/skills.rb +163 -0
- data/lib/harnex/commands/status.rb +171 -0
- data/lib/harnex/commands/stop.rb +127 -0
- data/lib/harnex/commands/wait.rb +165 -0
- data/lib/harnex/core.rb +286 -0
- data/lib/harnex/runtime/api_server.rb +187 -0
- data/lib/harnex/runtime/file_change_hook.rb +111 -0
- data/lib/harnex/runtime/inbox.rb +207 -0
- data/lib/harnex/runtime/message.rb +23 -0
- data/lib/harnex/runtime/session.rb +380 -0
- data/lib/harnex/runtime/session_state.rb +55 -0
- data/lib/harnex/version.rb +3 -0
- data/lib/harnex/watcher/inotify.rb +43 -0
- data/lib/harnex/watcher/polling.rb +92 -0
- data/lib/harnex/watcher.rb +24 -0
- data/lib/harnex.rb +25 -0
- data/recipes/01_fire_and_watch.md +82 -0
- data/recipes/02_chain_implement.md +115 -0
- data/skills/chain-implement/SKILL.md +234 -0
- data/skills/close/SKILL.md +47 -0
- data/skills/dispatch/SKILL.md +171 -0
- data/skills/harnex/SKILL.md +304 -0
- data/skills/open/SKILL.md +32 -0
- metadata +88 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "time"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Harnex
|
|
8
|
+
class Status
|
|
9
|
+
DESCRIPTION_WIDTH = 30
|
|
10
|
+
REPO_WIDTH = 20
|
|
11
|
+
|
|
12
|
+
def self.usage(program_name = "harnex status")
|
|
13
|
+
<<~TEXT
|
|
14
|
+
Usage: #{program_name} [options]
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--id ID Show a specific session
|
|
18
|
+
--repo PATH Filter to PATH's repo root (default: current repo)
|
|
19
|
+
--all List sessions across all repos
|
|
20
|
+
--json Output JSON instead of a table
|
|
21
|
+
-h, --help Show this help
|
|
22
|
+
TEXT
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(argv)
|
|
26
|
+
@argv = argv.dup
|
|
27
|
+
@options = {
|
|
28
|
+
id: nil,
|
|
29
|
+
repo_path: Dir.pwd,
|
|
30
|
+
all: false,
|
|
31
|
+
json: false,
|
|
32
|
+
help: false
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run
|
|
37
|
+
parser.parse!(@argv)
|
|
38
|
+
if @options[:help]
|
|
39
|
+
puts self.class.usage
|
|
40
|
+
return 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sessions = load_sessions
|
|
44
|
+
if @options[:json]
|
|
45
|
+
puts JSON.generate(sessions)
|
|
46
|
+
return 0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if sessions.empty?
|
|
50
|
+
if @options[:all]
|
|
51
|
+
puts "No active harnex sessions."
|
|
52
|
+
else
|
|
53
|
+
puts "No active harnex sessions for #{Harnex.resolve_repo_root(@options[:repo_path])}."
|
|
54
|
+
end
|
|
55
|
+
return 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts render_table(sessions)
|
|
59
|
+
0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def parser
|
|
65
|
+
@parser ||= OptionParser.new do |opts|
|
|
66
|
+
opts.banner = "Usage: harnex status [options]"
|
|
67
|
+
opts.on("--id ID", "Show a specific session") { |value| @options[:id] = Harnex.normalize_id(value) }
|
|
68
|
+
opts.on("--repo PATH", "Filter to PATH's repo root") { |value| @options[:repo_path] = value }
|
|
69
|
+
opts.on("--all", "List sessions across all repos") { @options[:all] = true }
|
|
70
|
+
opts.on("--json", "Output JSON instead of a table") { @options[:json] = true }
|
|
71
|
+
opts.on("-h", "--help", "Show help") { @options[:help] = true }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def load_sessions
|
|
76
|
+
repo_root = @options[:all] ? nil : Harnex.resolve_repo_root(@options[:repo_path])
|
|
77
|
+
sessions = Harnex.active_sessions(repo_root, id: @options[:id])
|
|
78
|
+
|
|
79
|
+
sessions.map { |session| load_live_status(session) }
|
|
80
|
+
.sort_by { |session| [session["repo_root"].to_s, session["started_at"].to_s, session["id"].to_s] }
|
|
81
|
+
.reverse
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def load_live_status(session)
|
|
85
|
+
uri = URI("http://#{session.fetch('host')}:#{session.fetch('port')}/status")
|
|
86
|
+
request = Net::HTTP::Get.new(uri)
|
|
87
|
+
request["Authorization"] = "Bearer #{session['token']}" if session["token"]
|
|
88
|
+
|
|
89
|
+
response = Net::HTTP.start(uri.host, uri.port, open_timeout: 0.25, read_timeout: 0.25) do |http|
|
|
90
|
+
http.request(request)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
return session unless response.is_a?(Net::HTTPSuccess)
|
|
94
|
+
|
|
95
|
+
session.merge(JSON.parse(response.body))
|
|
96
|
+
rescue StandardError
|
|
97
|
+
session
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def render_table(sessions)
|
|
101
|
+
columns = ["ID", "CLI", "PID", "PORT", "AGE", "STATE", "REPO", "DESC"]
|
|
102
|
+
|
|
103
|
+
rows = sessions.map { |session| table_row(session, columns) }
|
|
104
|
+
widths = columns.to_h { |column| [column, ([column.length] + rows.map { |row| row.fetch(column).length }).max] }
|
|
105
|
+
|
|
106
|
+
lines = []
|
|
107
|
+
lines << format_row(columns.to_h { |column| [column, column] }, columns, widths)
|
|
108
|
+
lines << format_row(columns.to_h { |column| [column, "-" * widths.fetch(column)] }, columns, widths)
|
|
109
|
+
lines.concat(rows.map { |row| format_row(row, columns, widths) })
|
|
110
|
+
lines.join("\n")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def table_row(session, columns)
|
|
114
|
+
row = {
|
|
115
|
+
"ID" => session["id"].to_s,
|
|
116
|
+
"CLI" => Harnex.session_cli(session).empty? ? "-" : Harnex.session_cli(session),
|
|
117
|
+
"PID" => session["pid"].to_s,
|
|
118
|
+
"PORT" => session["port"].to_s,
|
|
119
|
+
"AGE" => timeago(session["started_at"]),
|
|
120
|
+
"STATE" => session.dig("input_state", "state").to_s.empty? ? "-" : session.dig("input_state", "state").to_s,
|
|
121
|
+
"DESC" => truncate(session["description"])
|
|
122
|
+
}
|
|
123
|
+
row["REPO"] = truncate_repo(session["repo_root"])
|
|
124
|
+
row
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def format_row(row, columns, widths)
|
|
128
|
+
columns.map { |column| row.fetch(column).ljust(widths.fetch(column)) }.join(" ")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def timeago(timestamp)
|
|
132
|
+
return "-" if timestamp.to_s.empty?
|
|
133
|
+
|
|
134
|
+
seconds = (Time.now - Time.parse(timestamp.to_s)).to_i
|
|
135
|
+
seconds = 0 if seconds.negative?
|
|
136
|
+
|
|
137
|
+
case seconds
|
|
138
|
+
when 0...60
|
|
139
|
+
"#{seconds}s"
|
|
140
|
+
when 60...3600
|
|
141
|
+
"#{seconds / 60}m"
|
|
142
|
+
when 3600...86_400
|
|
143
|
+
"#{seconds / 3600}h"
|
|
144
|
+
else
|
|
145
|
+
"#{seconds / 86_400}d"
|
|
146
|
+
end
|
|
147
|
+
rescue StandardError
|
|
148
|
+
timestamp.to_s
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def truncate(value)
|
|
152
|
+
text = value.to_s
|
|
153
|
+
return "-" if text.empty?
|
|
154
|
+
return text if text.length <= DESCRIPTION_WIDTH
|
|
155
|
+
|
|
156
|
+
"#{text[0, DESCRIPTION_WIDTH - 3]}..."
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def truncate_repo(path)
|
|
160
|
+
text = display_path(path)
|
|
161
|
+
return "-" if text.empty?
|
|
162
|
+
return text if text.length <= REPO_WIDTH
|
|
163
|
+
|
|
164
|
+
"..#{text[-(REPO_WIDTH - 2)..]}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def display_path(path)
|
|
168
|
+
path.to_s.sub(/\A#{Regexp.escape(Dir.home)}/, "~")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Harnex
|
|
7
|
+
class Stopper
|
|
8
|
+
DEFAULT_TIMEOUT = 5.0
|
|
9
|
+
MIN_HTTP_TIMEOUT = 0.1
|
|
10
|
+
|
|
11
|
+
class TimeoutError < RuntimeError; end
|
|
12
|
+
|
|
13
|
+
def self.usage(program_name = "harnex stop")
|
|
14
|
+
<<~TEXT
|
|
15
|
+
Usage: #{program_name} [options]
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--id ID Session ID to stop (required)
|
|
19
|
+
--repo PATH Resolve the session using PATH's repo root (default: current repo)
|
|
20
|
+
--cli CLI Filter by CLI type
|
|
21
|
+
--timeout S How long to retry transient API failures (default: #{DEFAULT_TIMEOUT})
|
|
22
|
+
-h, --help Show this help
|
|
23
|
+
|
|
24
|
+
Sends the adapter stop sequence to the session.
|
|
25
|
+
Use `harnex wait --id ID` afterward to block until the session finishes.
|
|
26
|
+
TEXT
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(argv)
|
|
30
|
+
@argv = argv.dup
|
|
31
|
+
@options = {
|
|
32
|
+
id: nil,
|
|
33
|
+
repo_path: Dir.pwd,
|
|
34
|
+
cli: nil,
|
|
35
|
+
timeout: DEFAULT_TIMEOUT,
|
|
36
|
+
help: false
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def run
|
|
41
|
+
parser.parse!(@argv)
|
|
42
|
+
if @options[:help]
|
|
43
|
+
puts self.class.usage
|
|
44
|
+
return 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
raise "--id is required for harnex stop" unless @options[:id]
|
|
48
|
+
|
|
49
|
+
repo_root = Harnex.resolve_repo_root(@options[:repo_path])
|
|
50
|
+
registry = Harnex.read_registry(repo_root, @options[:id], cli: @options[:cli])
|
|
51
|
+
unless registry
|
|
52
|
+
warn("harnex stop: no session found with id #{@options[:id].inspect}")
|
|
53
|
+
return 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
uri = URI("http://#{registry.fetch('host')}:#{registry.fetch('port')}/stop")
|
|
57
|
+
request = Net::HTTP::Post.new(uri)
|
|
58
|
+
request["Authorization"] = "Bearer #{registry['token']}" if registry["token"]
|
|
59
|
+
|
|
60
|
+
deadline = monotonic_now + @options[:timeout]
|
|
61
|
+
response = with_http_retry(deadline: deadline) do
|
|
62
|
+
Net::HTTP.start(
|
|
63
|
+
uri.host,
|
|
64
|
+
uri.port,
|
|
65
|
+
open_timeout: http_timeout(deadline, cap: 1.0),
|
|
66
|
+
read_timeout: http_timeout(deadline, cap: 2.0)
|
|
67
|
+
) { |http| http.request(request) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
parsed = parse_json_body(response.body)
|
|
71
|
+
puts JSON.generate(parsed)
|
|
72
|
+
response.is_a?(Net::HTTPSuccess) && parsed["error"].nil? ? 0 : 1
|
|
73
|
+
rescue TimeoutError => e
|
|
74
|
+
puts JSON.generate(ok: false, id: @options[:id], status: "timeout", error: e.message)
|
|
75
|
+
124
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def with_http_retry(deadline:)
|
|
81
|
+
loop do
|
|
82
|
+
raise TimeoutError, timeout_message if deadline_reached?(deadline)
|
|
83
|
+
|
|
84
|
+
return yield
|
|
85
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, EOFError, Net::ReadTimeout, Net::OpenTimeout
|
|
86
|
+
raise TimeoutError, timeout_message if deadline_reached?(deadline)
|
|
87
|
+
|
|
88
|
+
sleep 0.1
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def monotonic_now
|
|
93
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def deadline_reached?(deadline)
|
|
97
|
+
monotonic_now >= deadline
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def http_timeout(deadline, cap: nil)
|
|
101
|
+
remaining = deadline - monotonic_now
|
|
102
|
+
remaining = [remaining, MIN_HTTP_TIMEOUT].max
|
|
103
|
+
cap ? [remaining, cap].min : remaining
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def timeout_message
|
|
107
|
+
"request timed out after #{@options[:timeout]}s"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parse_json_body(body)
|
|
111
|
+
JSON.parse(body.to_s.empty? ? "{}" : body.to_s)
|
|
112
|
+
rescue JSON::ParserError
|
|
113
|
+
{ "ok" => false, "error" => "invalid json response", "raw_body" => body.to_s }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parser
|
|
117
|
+
@parser ||= OptionParser.new do |opts|
|
|
118
|
+
opts.banner = "Usage: harnex stop [options]"
|
|
119
|
+
opts.on("--id ID", "Session ID to stop") { |value| @options[:id] = Harnex.normalize_id(value) }
|
|
120
|
+
opts.on("--repo PATH", "Resolve the session using PATH's repo root") { |value| @options[:repo_path] = value }
|
|
121
|
+
opts.on("--cli CLI", "Filter by CLI type") { |value| @options[:cli] = value }
|
|
122
|
+
opts.on("--timeout SECS", Float, "How long to retry transient API failures") { |value| @options[:timeout] = value }
|
|
123
|
+
opts.on("-h", "--help", "Show help") { @options[:help] = true }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Harnex
|
|
7
|
+
class Waiter
|
|
8
|
+
POLL_INTERVAL = 0.5
|
|
9
|
+
|
|
10
|
+
def self.usage(program_name = "harnex wait")
|
|
11
|
+
<<~TEXT
|
|
12
|
+
Usage: #{program_name} [options]
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--id ID Session ID to wait for (required)
|
|
16
|
+
--until STATE Wait until session reaches STATE (e.g. "prompt")
|
|
17
|
+
Without --until, waits for session exit (default)
|
|
18
|
+
--repo PATH Resolve session using PATH's repo root (default: current repo)
|
|
19
|
+
--timeout SECS Maximum time to wait in seconds (default: unlimited)
|
|
20
|
+
-h, --help Show this help
|
|
21
|
+
TEXT
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(argv)
|
|
25
|
+
@argv = argv.dup
|
|
26
|
+
@options = {
|
|
27
|
+
id: nil,
|
|
28
|
+
until_state: nil,
|
|
29
|
+
repo_path: Dir.pwd,
|
|
30
|
+
timeout: nil,
|
|
31
|
+
help: false
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def run
|
|
36
|
+
parser.parse!(@argv)
|
|
37
|
+
if @options[:help]
|
|
38
|
+
puts self.class.usage
|
|
39
|
+
return 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
raise "--id is required for harnex wait" unless @options[:id]
|
|
43
|
+
|
|
44
|
+
if @options[:until_state]
|
|
45
|
+
wait_until_state
|
|
46
|
+
else
|
|
47
|
+
wait_until_exit
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def wait_until_state
|
|
54
|
+
repo_root = Harnex.resolve_repo_root(@options[:repo_path])
|
|
55
|
+
target_state = @options[:until_state]
|
|
56
|
+
start_time = Time.now
|
|
57
|
+
deadline = @options[:timeout] ? start_time + @options[:timeout] : nil
|
|
58
|
+
|
|
59
|
+
registry = Harnex.read_registry(repo_root, @options[:id])
|
|
60
|
+
unless registry
|
|
61
|
+
warn("harnex wait: no session found with id #{@options[:id].inspect}")
|
|
62
|
+
return 1
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
target_pid = registry["pid"]
|
|
66
|
+
host = registry["host"]
|
|
67
|
+
port = registry["port"]
|
|
68
|
+
token = registry["token"]
|
|
69
|
+
|
|
70
|
+
warn("harnex wait: waiting for #{@options[:id]} to reach #{target_state}")
|
|
71
|
+
|
|
72
|
+
loop do
|
|
73
|
+
unless Harnex.alive_pid?(target_pid)
|
|
74
|
+
waited = (Time.now - start_time).round(1)
|
|
75
|
+
puts JSON.generate(ok: false, id: @options[:id], state: "exited", waited_seconds: waited)
|
|
76
|
+
return 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
state = fetch_agent_state(host, port, token)
|
|
80
|
+
if state == target_state
|
|
81
|
+
waited = (Time.now - start_time).round(1)
|
|
82
|
+
puts JSON.generate(ok: true, id: @options[:id], state: state, waited_seconds: waited)
|
|
83
|
+
return 0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if deadline && Time.now >= deadline
|
|
87
|
+
waited = (Time.now - start_time).round(1)
|
|
88
|
+
puts JSON.generate(ok: false, id: @options[:id], state: state || "unknown", waited_seconds: waited, status: "timeout")
|
|
89
|
+
return 124
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sleep POLL_INTERVAL
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def wait_until_exit
|
|
97
|
+
repo_root = Harnex.resolve_repo_root(@options[:repo_path])
|
|
98
|
+
deadline = @options[:timeout] ? Time.now + @options[:timeout] : nil
|
|
99
|
+
exit_path = Harnex.exit_status_path(repo_root, @options[:id])
|
|
100
|
+
|
|
101
|
+
registry = Harnex.read_registry(repo_root, @options[:id])
|
|
102
|
+
unless registry
|
|
103
|
+
return read_exit_status(exit_path, @options[:id]) if File.exist?(exit_path)
|
|
104
|
+
|
|
105
|
+
warn("harnex wait: no session found with id #{@options[:id].inspect}")
|
|
106
|
+
return 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
target_pid = registry["pid"]
|
|
110
|
+
warn("harnex wait: watching session #{@options[:id]} (pid #{target_pid})")
|
|
111
|
+
|
|
112
|
+
loop do
|
|
113
|
+
unless Harnex.alive_pid?(target_pid)
|
|
114
|
+
return read_exit_status(exit_path, @options[:id])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if deadline && Time.now >= deadline
|
|
118
|
+
puts JSON.generate(ok: false, id: @options[:id], status: "timeout", pid: target_pid)
|
|
119
|
+
return 124
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
sleep POLL_INTERVAL
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def fetch_agent_state(host, port, token)
|
|
127
|
+
uri = URI("http://#{host}:#{port}/status")
|
|
128
|
+
request = Net::HTTP::Get.new(uri)
|
|
129
|
+
request["Authorization"] = "Bearer #{token}" if token
|
|
130
|
+
|
|
131
|
+
response = Net::HTTP.start(uri.host, uri.port, open_timeout: 1, read_timeout: 1) do |http|
|
|
132
|
+
http.request(request)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
136
|
+
|
|
137
|
+
data = JSON.parse(response.body)
|
|
138
|
+
data["agent_state"]
|
|
139
|
+
rescue StandardError
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def read_exit_status(exit_path, id)
|
|
144
|
+
if File.exist?(exit_path)
|
|
145
|
+
data = JSON.parse(File.read(exit_path))
|
|
146
|
+
puts JSON.generate(data)
|
|
147
|
+
data["exit_code"] || 0
|
|
148
|
+
else
|
|
149
|
+
puts JSON.generate(ok: true, id: id, status: "exited")
|
|
150
|
+
0
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def parser
|
|
155
|
+
@parser ||= OptionParser.new do |opts|
|
|
156
|
+
opts.banner = "Usage: harnex wait [options]"
|
|
157
|
+
opts.on("--id ID", "Session ID to wait for") { |value| @options[:id] = Harnex.normalize_id(value) }
|
|
158
|
+
opts.on("--until STATE", "Wait until session reaches STATE") { |value| @options[:until_state] = value }
|
|
159
|
+
opts.on("--repo PATH", "Resolve session using PATH's repo root") { |value| @options[:repo_path] = value }
|
|
160
|
+
opts.on("--timeout SECONDS", Float, "Maximum time to wait") { |value| @options[:timeout] = value }
|
|
161
|
+
opts.on("-h", "--help", "Show help") { @options[:help] = true }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|