ralph.rb 1.2.435535439 → 2.0.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/.github/workflows/gem-push.yml +2 -2
- data/Gemfile +1 -1
- data/Gemfile.lock +53 -0
- data/lib/ralph/cli.rb +67 -186
- data/lib/ralph/display.rb +105 -0
- data/lib/ralph/events.rb +117 -0
- data/lib/ralph/loop.rb +113 -170
- data/lib/ralph/metrics.rb +88 -0
- data/lib/ralph/opencode.rb +66 -0
- data/lib/ralph/version.rb +1 -1
- data/lib/ralph.rb +0 -3
- data/plans/00-complete-implementation.md +120 -0
- data/plans/01-cli-implementation.md +53 -0
- data/plans/02-loop-implementation.md +78 -0
- data/plans/03-agents-implementation.md +76 -0
- data/plans/04-metrics-implementation.md +98 -0
- data/plans/README.md +63 -0
- data/specs/README.md +4 -15
- data/specs/__templates__/API_TEMPLATE.md +0 -0
- data/specs/__templates__/AUTOMATION_ACTION_TEMPLATE.md +0 -0
- data/specs/__templates__/AUTOMATION_TRIGGER_TEMPLATE.md +0 -0
- data/specs/__templates__/CONTROLLER_TEMPLATE.md +32 -0
- data/specs/__templates__/INTEGRATION_TEMPLATE.md +0 -0
- data/specs/__templates__/MODEL_TEMPLATE.md +0 -0
- data/specs/agents.md +426 -120
- data/specs/cli.md +11 -218
- data/specs/lib/todo_item.rb +144 -0
- data/specs/log +15 -0
- data/specs/loop.md +42 -0
- data/specs/metrics.md +51 -0
- metadata +23 -39
- data/lib/ralph/agents/base.rb +0 -132
- data/lib/ralph/agents/claude_code.rb +0 -24
- data/lib/ralph/agents/codex.rb +0 -25
- data/lib/ralph/agents/open_code.rb +0 -30
- data/lib/ralph/agents.rb +0 -24
- data/lib/ralph/config.rb +0 -40
- data/lib/ralph/git/file_snapshot.rb +0 -60
- data/lib/ralph/helpers.rb +0 -76
- data/lib/ralph/iteration.rb +0 -220
- data/lib/ralph/output/active_loop_error.rb +0 -13
- data/lib/ralph/output/banner.rb +0 -29
- data/lib/ralph/output/completion_deferred.rb +0 -12
- data/lib/ralph/output/completion_detected.rb +0 -17
- data/lib/ralph/output/config_summary.rb +0 -31
- data/lib/ralph/output/context_consumed.rb +0 -11
- data/lib/ralph/output/iteration.rb +0 -45
- data/lib/ralph/output/max_iterations_reached.rb +0 -16
- data/lib/ralph/output/no_plugin_warning.rb +0 -14
- data/lib/ralph/output/nonzero_exit_warning.rb +0 -11
- data/lib/ralph/output/plugin_error.rb +0 -12
- data/lib/ralph/output/status.rb +0 -176
- data/lib/ralph/output/struggle_warning.rb +0 -18
- data/lib/ralph/output/task_completion.rb +0 -12
- data/lib/ralph/output/tasks_file_created.rb +0 -11
- data/lib/ralph/prompt_template.rb +0 -183
- data/lib/ralph/storage/context.rb +0 -58
- data/lib/ralph/storage/history.rb +0 -117
- data/lib/ralph/storage/state.rb +0 -178
- data/lib/ralph/storage/tasks.rb +0 -244
- data/lib/ralph/threads/heartbeat.rb +0 -44
- data/lib/ralph/threads/stream_reader.rb +0 -50
- data/original/bin/ralph.js +0 -13
- data/original/ralph.ts +0 -1706
- data/specs/iteration.md +0 -173
- data/specs/output.md +0 -104
- data/specs/storage/local-data-structure.md +0 -246
- data/specs/tasks.md +0 -295
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ralph.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Kidd
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -68,58 +68,42 @@ files:
|
|
|
68
68
|
- ".ruby-version"
|
|
69
69
|
- AGENTS.md
|
|
70
70
|
- Gemfile
|
|
71
|
+
- Gemfile.lock
|
|
71
72
|
- LICENSE
|
|
72
73
|
- README.md
|
|
73
74
|
- bin/rubocop
|
|
74
75
|
- bin/test
|
|
75
76
|
- exe/ralph
|
|
76
77
|
- lib/ralph.rb
|
|
77
|
-
- lib/ralph/agents.rb
|
|
78
|
-
- lib/ralph/agents/base.rb
|
|
79
|
-
- lib/ralph/agents/claude_code.rb
|
|
80
|
-
- lib/ralph/agents/codex.rb
|
|
81
|
-
- lib/ralph/agents/open_code.rb
|
|
82
78
|
- lib/ralph/cli.rb
|
|
83
|
-
- lib/ralph/
|
|
84
|
-
- lib/ralph/
|
|
85
|
-
- lib/ralph/helpers.rb
|
|
86
|
-
- lib/ralph/iteration.rb
|
|
79
|
+
- lib/ralph/display.rb
|
|
80
|
+
- lib/ralph/events.rb
|
|
87
81
|
- lib/ralph/loop.rb
|
|
88
|
-
- lib/ralph/
|
|
89
|
-
- lib/ralph/
|
|
90
|
-
- lib/ralph/output/completion_deferred.rb
|
|
91
|
-
- lib/ralph/output/completion_detected.rb
|
|
92
|
-
- lib/ralph/output/config_summary.rb
|
|
93
|
-
- lib/ralph/output/context_consumed.rb
|
|
94
|
-
- lib/ralph/output/iteration.rb
|
|
95
|
-
- lib/ralph/output/max_iterations_reached.rb
|
|
96
|
-
- lib/ralph/output/no_plugin_warning.rb
|
|
97
|
-
- lib/ralph/output/nonzero_exit_warning.rb
|
|
98
|
-
- lib/ralph/output/plugin_error.rb
|
|
99
|
-
- lib/ralph/output/status.rb
|
|
100
|
-
- lib/ralph/output/struggle_warning.rb
|
|
101
|
-
- lib/ralph/output/task_completion.rb
|
|
102
|
-
- lib/ralph/output/tasks_file_created.rb
|
|
103
|
-
- lib/ralph/prompt_template.rb
|
|
104
|
-
- lib/ralph/storage/context.rb
|
|
105
|
-
- lib/ralph/storage/history.rb
|
|
106
|
-
- lib/ralph/storage/state.rb
|
|
107
|
-
- lib/ralph/storage/tasks.rb
|
|
108
|
-
- lib/ralph/threads/heartbeat.rb
|
|
109
|
-
- lib/ralph/threads/stream_reader.rb
|
|
82
|
+
- lib/ralph/metrics.rb
|
|
83
|
+
- lib/ralph/opencode.rb
|
|
110
84
|
- lib/ralph/version.rb
|
|
111
|
-
-
|
|
112
|
-
-
|
|
85
|
+
- plans/00-complete-implementation.md
|
|
86
|
+
- plans/01-cli-implementation.md
|
|
87
|
+
- plans/02-loop-implementation.md
|
|
88
|
+
- plans/03-agents-implementation.md
|
|
89
|
+
- plans/04-metrics-implementation.md
|
|
90
|
+
- plans/README.md
|
|
113
91
|
- ralph.gemspec
|
|
114
92
|
- ralph2.gemspec
|
|
115
93
|
- screenshot.webp
|
|
116
94
|
- specs/README.md
|
|
95
|
+
- specs/__templates__/API_TEMPLATE.md
|
|
96
|
+
- specs/__templates__/AUTOMATION_ACTION_TEMPLATE.md
|
|
97
|
+
- specs/__templates__/AUTOMATION_TRIGGER_TEMPLATE.md
|
|
98
|
+
- specs/__templates__/CONTROLLER_TEMPLATE.md
|
|
99
|
+
- specs/__templates__/INTEGRATION_TEMPLATE.md
|
|
100
|
+
- specs/__templates__/MODEL_TEMPLATE.md
|
|
117
101
|
- specs/agents.md
|
|
118
102
|
- specs/cli.md
|
|
119
|
-
- specs/
|
|
120
|
-
- specs/
|
|
121
|
-
- specs/
|
|
122
|
-
- specs/
|
|
103
|
+
- specs/lib/todo_item.rb
|
|
104
|
+
- specs/log
|
|
105
|
+
- specs/loop.md
|
|
106
|
+
- specs/metrics.md
|
|
123
107
|
homepage: https://github.com/n-at-han-k/ralph.rb
|
|
124
108
|
licenses:
|
|
125
109
|
- MIT
|
data/lib/ralph/agents/base.rb
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "open3"
|
|
4
|
-
|
|
5
|
-
module Ralph
|
|
6
|
-
module Agents
|
|
7
|
-
class Base
|
|
8
|
-
include ::Ralph::Helpers
|
|
9
|
-
|
|
10
|
-
# Result of executing an agent process
|
|
11
|
-
ExecutionResult = Struct.new(:stdout_text, :stderr_text, :tool_counts, :exit_code, keyword_init: true) do
|
|
12
|
-
def combined_output
|
|
13
|
-
"#{stdout_text}\n#{stderr_text}"
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def type = raise NotImplementedError
|
|
18
|
-
def command = raise NotImplementedError
|
|
19
|
-
def config_name = raise NotImplementedError
|
|
20
|
-
|
|
21
|
-
def parse_tool_output(_line) = raise NotImplementedError
|
|
22
|
-
def build_args(_prompt, _model, _options) = raise NotImplementedError
|
|
23
|
-
|
|
24
|
-
def build_env(_options) = ENV.to_h.dup
|
|
25
|
-
|
|
26
|
-
def execute(prompt, options = {})
|
|
27
|
-
command_args = build_args(prompt, options[:model],
|
|
28
|
-
{ allow_all_permissions: options[:allow_all_permissions] })
|
|
29
|
-
environment = build_env(
|
|
30
|
-
filter_plugins: options[:disable_plugins],
|
|
31
|
-
allow_all_permissions: options[:allow_all_permissions]
|
|
32
|
-
)
|
|
33
|
-
full_command = [command] + command_args
|
|
34
|
-
|
|
35
|
-
if options[:stream_output]
|
|
36
|
-
execute_streaming(environment, full_command, options[:on_line])
|
|
37
|
-
else
|
|
38
|
-
execute_captured(environment, full_command)
|
|
39
|
-
end
|
|
40
|
-
rescue StandardError => agent_error
|
|
41
|
-
Agents::Base::ExecutionResult.new(
|
|
42
|
-
stdout_text: "", stderr_text: agent_error.to_s, tool_counts: {}, exit_code: -1
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Collects tool usage counts from output text.
|
|
47
|
-
# Delegates to parse_tool_output for each line.
|
|
48
|
-
def collect_tool_counts(text)
|
|
49
|
-
Hash.new(0).tap do |counts|
|
|
50
|
-
text.each_line do |line|
|
|
51
|
-
tool = parse_tool_output(line)
|
|
52
|
-
counts[tool] += 1 if tool
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Returns a fatal error message if one is detected in the output,
|
|
58
|
-
# or nil if no fatal error is found. Subclasses may override.
|
|
59
|
-
def detect_fatal_error(_output) = nil
|
|
60
|
-
|
|
61
|
-
# Extracts error patterns from agent output. Subclasses may override
|
|
62
|
-
# for agent-specific error formats.
|
|
63
|
-
def extract_errors(output)
|
|
64
|
-
errors = []
|
|
65
|
-
output.each_line do |line|
|
|
66
|
-
lower = line.downcase
|
|
67
|
-
if lower.include?("error:") ||
|
|
68
|
-
lower.include?("failed:") ||
|
|
69
|
-
lower.include?("exception:") ||
|
|
70
|
-
lower.include?("typeerror") ||
|
|
71
|
-
lower.include?("syntaxerror") ||
|
|
72
|
-
lower.include?("referenceerror") ||
|
|
73
|
-
(lower.include?("test") && lower.include?("fail"))
|
|
74
|
-
cleaned = line.strip[0, 200]
|
|
75
|
-
errors << cleaned if cleaned && !cleaned.empty? && !errors.include?(cleaned)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
errors.first(10)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def validate!
|
|
82
|
-
path = which(command)
|
|
83
|
-
unless path
|
|
84
|
-
$stderr.puts "Error: #{config_name} CLI ('#{command}') not found."
|
|
85
|
-
exit 1
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
private
|
|
90
|
-
|
|
91
|
-
def execute_streaming(environment, full_command, on_line)
|
|
92
|
-
tool_counts = Hash.new(0)
|
|
93
|
-
stdout_text = +""
|
|
94
|
-
stderr_text = +""
|
|
95
|
-
mutex = Mutex.new
|
|
96
|
-
|
|
97
|
-
stdin, stdout, stderr, wait_thread = Open3.popen3(environment, *full_command)
|
|
98
|
-
stdin.close
|
|
99
|
-
|
|
100
|
-
stdout_reader = Threads::StreamReader.new(
|
|
101
|
-
stdout, stdout_text, mutex, tool_counts, on_line, false, method(:parse_tool_output)
|
|
102
|
-
)
|
|
103
|
-
stderr_reader = Threads::StreamReader.new(
|
|
104
|
-
stderr, stderr_text, mutex, tool_counts, on_line, true, method(:parse_tool_output)
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
stdout_reader.join
|
|
108
|
-
stderr_reader.join
|
|
109
|
-
exit_status = wait_thread.value
|
|
110
|
-
|
|
111
|
-
ExecutionResult.new(
|
|
112
|
-
stdout_text: stdout_text,
|
|
113
|
-
stderr_text: stderr_text,
|
|
114
|
-
tool_counts: tool_counts,
|
|
115
|
-
exit_code: exit_status.exitstatus || 1
|
|
116
|
-
)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def execute_captured(environment, full_command)
|
|
120
|
-
stdout, stderr, status = Open3.capture3(environment, *full_command, stdin_data: "")
|
|
121
|
-
tool_counts = collect_tool_counts("#{stdout}\n#{stderr}")
|
|
122
|
-
|
|
123
|
-
ExecutionResult.new(
|
|
124
|
-
stdout_text: stdout,
|
|
125
|
-
stderr_text: stderr,
|
|
126
|
-
tool_counts: tool_counts,
|
|
127
|
-
exit_code: status.exitstatus || 1
|
|
128
|
-
)
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ralph
|
|
4
|
-
module Agents
|
|
5
|
-
class ClaudeCode < Base
|
|
6
|
-
|
|
7
|
-
def type = :claude_code
|
|
8
|
-
def command = "claude"
|
|
9
|
-
def config_name = "Claude Code"
|
|
10
|
-
|
|
11
|
-
def build_args(prompt, model, options)
|
|
12
|
-
args = ["-p", prompt]
|
|
13
|
-
args.push("--model", model) if model && !model.empty?
|
|
14
|
-
args.push("--dangerously-skip-permissions") if options && options[:allow_all_permissions]
|
|
15
|
-
args
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def parse_tool_output(line)
|
|
19
|
-
match = strip_ansi(line).match(/(?:Using|Called|Tool:)\s+([A-Za-z0-9_-]+)/i)
|
|
20
|
-
match ? match[1] : nil
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
data/lib/ralph/agents/codex.rb
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ralph
|
|
4
|
-
module Agents
|
|
5
|
-
class Codex < Base
|
|
6
|
-
|
|
7
|
-
def type = :codex
|
|
8
|
-
def command = "codex"
|
|
9
|
-
def config_name = "Codex"
|
|
10
|
-
|
|
11
|
-
def build_args(prompt, model, options)
|
|
12
|
-
args = ["exec"]
|
|
13
|
-
args.push("--model", model) if model && !model.empty?
|
|
14
|
-
args.push("--full-auto") if options && options[:allow_all_permissions]
|
|
15
|
-
args.push(prompt)
|
|
16
|
-
args
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def parse_tool_output(line)
|
|
20
|
-
match = strip_ansi(line).match(/(?:Tool:|Using|Calling|Running)\s+([A-Za-z0-9_-]+)/i)
|
|
21
|
-
match ? match[1] : nil
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ralph
|
|
4
|
-
module Agents
|
|
5
|
-
class OpenCode < Base
|
|
6
|
-
|
|
7
|
-
def type = :opencode
|
|
8
|
-
def command = "opencode"
|
|
9
|
-
def config_name = "OpenCode"
|
|
10
|
-
|
|
11
|
-
def build_args(prompt, model, _options)
|
|
12
|
-
args = ["run"]
|
|
13
|
-
args.push("-m", model) if model && !model.empty?
|
|
14
|
-
args.push(prompt)
|
|
15
|
-
args
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def parse_tool_output(line)
|
|
19
|
-
match = strip_ansi(line).match(/^\|\s{2}([A-Za-z0-9_-]+)/)
|
|
20
|
-
match ? match[1] : nil
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def detect_fatal_error(output)
|
|
24
|
-
if output.include?("ralph-wiggum is not yet ready for use. This is a placeholder package.")
|
|
25
|
-
"Placeholder plugin error detected"
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
data/lib/ralph/agents.rb
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module Ralph
|
|
2
|
-
module Agents
|
|
3
|
-
|
|
4
|
-
module_function
|
|
5
|
-
|
|
6
|
-
AGENT_NAME_MAP = {
|
|
7
|
-
"opencode" => :opencode,
|
|
8
|
-
"claude-code" => :claude_code,
|
|
9
|
-
"codex" => :codex
|
|
10
|
-
}.freeze
|
|
11
|
-
|
|
12
|
-
def valid_agent_names = AGENT_NAME_MAP.keys
|
|
13
|
-
|
|
14
|
-
def resolve(name_str)
|
|
15
|
-
AGENT_NAME_MAP[name_str].then do |sym|
|
|
16
|
-
case sym
|
|
17
|
-
when :opencode then OpenCode.new
|
|
18
|
-
when :claude_code then ClaudeCode.new
|
|
19
|
-
when :codex then Codex.new
|
|
20
|
-
else nil end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
data/lib/ralph/config.rb
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
module Ralph
|
|
2
|
-
class Config
|
|
3
|
-
# Single source of truth for config option names and their defaults.
|
|
4
|
-
# Use a lambda for mutable defaults to avoid shared state.
|
|
5
|
-
OPTIONS = {
|
|
6
|
-
prompt: -> { PromptTemplate.new("") },
|
|
7
|
-
min_iterations: 1,
|
|
8
|
-
max_iterations: 0,
|
|
9
|
-
completion_promise: "COMPLETE",
|
|
10
|
-
tasks_mode: false,
|
|
11
|
-
task_promise: "READY_FOR_NEXT_TASK",
|
|
12
|
-
model: "",
|
|
13
|
-
chosen_agent: "opencode",
|
|
14
|
-
disable_plugins: false,
|
|
15
|
-
allow_all_permissions: true,
|
|
16
|
-
stream_output: true,
|
|
17
|
-
verbose_tools: false,
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
20
|
-
attr_accessor(*OPTIONS.keys)
|
|
21
|
-
|
|
22
|
-
# Runtime state (not constructor args, not in to_h).
|
|
23
|
-
attr_accessor :current_pid, :stopping
|
|
24
|
-
|
|
25
|
-
def initialize(**opts)
|
|
26
|
-
OPTIONS.each do |key, default|
|
|
27
|
-
value = opts.fetch(key) { default.respond_to?(:call) ? default.call : default }
|
|
28
|
-
instance_variable_set(:"@#{key}", value)
|
|
29
|
-
end
|
|
30
|
-
@current_pid = nil
|
|
31
|
-
@stopping = false
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Returns a hash of options suitable for passing to Loop#call.
|
|
35
|
-
# Excludes runtime state (current_pid, stopping).
|
|
36
|
-
def to_h
|
|
37
|
-
OPTIONS.keys.map { |k| [k, send(k)] }.to_h
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "open3"
|
|
4
|
-
|
|
5
|
-
module Ralph
|
|
6
|
-
module Git
|
|
7
|
-
# Captures and compares file state via git for change detection
|
|
8
|
-
class FileSnapshot
|
|
9
|
-
attr_reader :files
|
|
10
|
-
|
|
11
|
-
def initialize(files)
|
|
12
|
-
@files = files
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Capture a snapshot of all tracked/modified files and their git hashes
|
|
16
|
-
def self.capture
|
|
17
|
-
files = {}
|
|
18
|
-
begin
|
|
19
|
-
status, _, _ = Open3.capture3("git", "status", "--porcelain")
|
|
20
|
-
tracked, _, _ = Open3.capture3("git", "ls-files")
|
|
21
|
-
|
|
22
|
-
all_files = Set.new
|
|
23
|
-
status.strip.each_line do |line|
|
|
24
|
-
name = line[3..]&.strip
|
|
25
|
-
all_files.add(name) if name && !name.empty?
|
|
26
|
-
end
|
|
27
|
-
tracked.strip.each_line do |file|
|
|
28
|
-
f = file.strip
|
|
29
|
-
all_files.add(f) unless f.empty?
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
all_files.each do |file|
|
|
33
|
-
begin
|
|
34
|
-
hash, _, _ = Open3.capture3("git", "hash-object", file)
|
|
35
|
-
files[file] = hash.strip unless hash.strip.empty?
|
|
36
|
-
rescue StandardError
|
|
37
|
-
# skip
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
rescue StandardError
|
|
41
|
-
# git not available
|
|
42
|
-
end
|
|
43
|
-
new(files)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Return list of files that changed between this snapshot and a later one
|
|
47
|
-
def modified_since(other)
|
|
48
|
-
Array.new.tap do |changed|
|
|
49
|
-
other.files.each do |file, hash|
|
|
50
|
-
changed << file if files[file] != hash
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
files.each_key do |file|
|
|
54
|
-
changed << file unless other.files.key?(file)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
data/lib/ralph/helpers.rb
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ralph
|
|
4
|
-
module Helpers
|
|
5
|
-
def strip_ansi(input)
|
|
6
|
-
input.gsub(/\x1B\[[0-9;]*m/, "")
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def escape_regex(str)
|
|
10
|
-
Regexp.escape(str)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def format_duration(ms)
|
|
14
|
-
total_seconds = [0, (ms / 1000).floor].max
|
|
15
|
-
hours = total_seconds / 3600
|
|
16
|
-
minutes = (total_seconds % 3600) / 60
|
|
17
|
-
seconds = total_seconds % 60
|
|
18
|
-
|
|
19
|
-
if hours > 0
|
|
20
|
-
"#{hours}:#{minutes.to_s.rjust(2, '0')}:#{seconds.to_s.rjust(2, '0')}"
|
|
21
|
-
else
|
|
22
|
-
"#{minutes}:#{seconds.to_s.rjust(2, '0')}"
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def format_duration_long(ms)
|
|
27
|
-
total_seconds = [0, (ms / 1000).floor].max
|
|
28
|
-
hours = total_seconds / 3600
|
|
29
|
-
minutes = (total_seconds % 3600) / 60
|
|
30
|
-
seconds = total_seconds % 60
|
|
31
|
-
|
|
32
|
-
if hours > 0
|
|
33
|
-
"#{hours}h #{minutes}m #{seconds}s"
|
|
34
|
-
elsif minutes > 0
|
|
35
|
-
"#{minutes}m #{seconds}s"
|
|
36
|
-
else
|
|
37
|
-
"#{seconds}s"
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def format_tool_summary(tool_counts, max_items = 6)
|
|
42
|
-
if tool_counts.empty?
|
|
43
|
-
""
|
|
44
|
-
else
|
|
45
|
-
entries = tool_counts.sort_by { |_, v| -v }
|
|
46
|
-
shown = entries.first(max_items)
|
|
47
|
-
remaining = entries.length - shown.length
|
|
48
|
-
|
|
49
|
-
shown.map { |name, count| "#{name} #{count}" }
|
|
50
|
-
.tap { |parts| parts << "+#{remaining} more" if remaining > 0 }
|
|
51
|
-
.then { |parts| parts.join(" • ") }
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def check_completion(output, promise)
|
|
56
|
-
pattern = /<promise>\s*#{escape_regex(promise)}\s*<\/promise>/i
|
|
57
|
-
output.match?(pattern)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Cross-platform which
|
|
61
|
-
def which(cmd)
|
|
62
|
-
exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
|
|
63
|
-
ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
|
|
64
|
-
exts.each do |ext|
|
|
65
|
-
exe = File.join(path, "#{cmd}#{ext}")
|
|
66
|
-
return exe if File.executable?(exe) && !File.directory?(exe)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
nil
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def now_ms
|
|
73
|
-
(Time.now.to_f * 1000).to_i
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|