hiiro 0.1.146 → 0.1.147
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/lib/hiiro/app_files.rb +148 -0
- data/lib/hiiro/runner_tool.rb +265 -0
- data/lib/hiiro/service_manager.rb +465 -0
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6548d2f79ee793b4c4855565db3b0695c1fb4cc7d677f49359107900af3c3dd
|
|
4
|
+
data.tar.gz: 1d4bc53e4f8cb004c96faa3c1f7940a5754f816b8006faaa7ee0fc0a045168e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 230c9aee91c69295fb32f1d66aef7dbc05210ec9ed08c6dcf25a9300de3b424ccf297cc8efd16b11e856809f898830a7dfb9491142bec1bd66aa89f0ec5c55a2
|
|
7
|
+
data.tar.gz: 59b47e377f278fdde3ca2f74a0bbce190509522e1097b82a735ed0e36eeeee338e2ca115a433b309c2f48edb1fa40a5ce1e95562698fe2b6ea90fcf35a149c75
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
class Hiiro
|
|
5
|
+
class AppFiles
|
|
6
|
+
CONFIG_FILE = File.join(Dir.home, '.config', 'hiiro', 'app_files.yml')
|
|
7
|
+
|
|
8
|
+
attr_reader :config_file
|
|
9
|
+
|
|
10
|
+
def initialize(config_file: CONFIG_FILE)
|
|
11
|
+
@config_file = config_file
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def files_for(app_name)
|
|
15
|
+
data = load_config
|
|
16
|
+
data[app_name] || []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add(app_name, *filenames)
|
|
20
|
+
data = load_config
|
|
21
|
+
data[app_name] ||= []
|
|
22
|
+
filenames.flatten.each do |f|
|
|
23
|
+
data[app_name] << f unless data[app_name].include?(f)
|
|
24
|
+
end
|
|
25
|
+
save_config(data)
|
|
26
|
+
puts "Added #{filenames.length} file(s) to '#{app_name}'"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def remove(app_name, *filenames)
|
|
30
|
+
data = load_config
|
|
31
|
+
unless data.key?(app_name)
|
|
32
|
+
puts "App '#{app_name}' not found in app files"
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
filenames.flatten.each do |f|
|
|
37
|
+
data[app_name].delete(f)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
data.delete(app_name) if data[app_name].empty?
|
|
41
|
+
save_config(data)
|
|
42
|
+
puts "Removed #{filenames.length} file(s) from '#{app_name}'"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def list(app_name: nil)
|
|
46
|
+
data = load_config
|
|
47
|
+
|
|
48
|
+
if app_name
|
|
49
|
+
files = data[app_name]
|
|
50
|
+
if files && files.any?
|
|
51
|
+
puts "Files for '#{app_name}':"
|
|
52
|
+
files.each { |f| puts " #{f}" }
|
|
53
|
+
else
|
|
54
|
+
puts "No files tracked for '#{app_name}'"
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
if data.empty?
|
|
58
|
+
puts "No app files configured."
|
|
59
|
+
puts "Use 'file add <app> <file>' to track files."
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
data.each do |name, files|
|
|
64
|
+
puts "#{name}:"
|
|
65
|
+
files.each { |f| puts " #{f}" }
|
|
66
|
+
puts
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.build_hiiro(parent_hiiro, af, environment: nil)
|
|
72
|
+
parent_hiiro.make_child(:file) do |h|
|
|
73
|
+
h.add_subcmd(:ls) do |app_name=nil|
|
|
74
|
+
af.list(app_name: app_name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
h.add_subcmd(:add) do |app_name=nil, *filenames|
|
|
78
|
+
unless app_name
|
|
79
|
+
puts "Usage: file add <app_name> <file1> [file2 ...]"
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if filenames.empty?
|
|
84
|
+
puts "Usage: file add <app_name> <file1> [file2 ...]"
|
|
85
|
+
next
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
af.add(app_name, *filenames)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
h.add_subcmd(:rm) do |app_name=nil, *filenames|
|
|
92
|
+
unless app_name
|
|
93
|
+
puts "Usage: file rm <app_name> <file1> [file2 ...]"
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if filenames.empty?
|
|
98
|
+
puts "Usage: file rm <app_name> <file1> [file2 ...]"
|
|
99
|
+
next
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
af.remove(app_name, *filenames)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
h.add_subcmd(:edit) do |app_name=nil|
|
|
106
|
+
unless app_name
|
|
107
|
+
puts "Usage: file edit <app_name>"
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
files = af.files_for(app_name)
|
|
112
|
+
if files.empty?
|
|
113
|
+
puts "No files tracked for '#{app_name}'"
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Resolve files relative to tree root if environment available
|
|
118
|
+
resolved = files
|
|
119
|
+
if environment
|
|
120
|
+
tree = environment.tree
|
|
121
|
+
if tree
|
|
122
|
+
resolved = files.map { |f| File.join(tree.path, f) }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
editor = ENV['EDITOR'] || 'nvim'
|
|
127
|
+
if editor.match?(/vim/i)
|
|
128
|
+
system(editor, '-O', *resolved)
|
|
129
|
+
else
|
|
130
|
+
system(editor, *resolved)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def load_config
|
|
139
|
+
return {} unless File.exist?(config_file)
|
|
140
|
+
YAML.safe_load_file(config_file, permitted_classes: [Symbol]) || {}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def save_config(data)
|
|
144
|
+
FileUtils.mkdir_p(File.dirname(config_file))
|
|
145
|
+
File.write(config_file, YAML.dump(data))
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
class Hiiro
|
|
5
|
+
class RunnerTool
|
|
6
|
+
CONFIG_FILE = File.join(Dir.home, '.config', 'hiiro', 'tools.yml')
|
|
7
|
+
|
|
8
|
+
KNOWN_CHANGE_SETS = %w[dirty branch all].freeze
|
|
9
|
+
KNOWN_TOOL_TYPES = %w[lint test format].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :config_file
|
|
12
|
+
|
|
13
|
+
def initialize(config_file: CONFIG_FILE)
|
|
14
|
+
@config_file = config_file
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def tools
|
|
18
|
+
load_config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def find_tool(name)
|
|
22
|
+
configs = tools
|
|
23
|
+
names = configs.keys.map { |k| OpenStruct.new(name: k) }
|
|
24
|
+
result = Hiiro::Matcher.new(names, :name).by_prefix(name)
|
|
25
|
+
match = result.resolved || result.first
|
|
26
|
+
return nil unless match
|
|
27
|
+
|
|
28
|
+
tool_name = match.item.name
|
|
29
|
+
{ name: tool_name, **symbolize_keys(configs[tool_name]) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def find_tools(tool_type: nil, file_type_group: nil)
|
|
33
|
+
configs = tools
|
|
34
|
+
configs.select { |_, cfg|
|
|
35
|
+
(tool_type.nil? || cfg['tool_type'] == tool_type) &&
|
|
36
|
+
(file_type_group.nil? || cfg['file_type_group'] == file_type_group)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def changed_files(change_set, git: nil)
|
|
41
|
+
case change_set
|
|
42
|
+
when 'dirty'
|
|
43
|
+
output = `git status --porcelain 2>/dev/null`
|
|
44
|
+
output.lines.map { |l| l.strip.sub(/^.{3}/, '') }.reject(&:empty?)
|
|
45
|
+
when 'branch'
|
|
46
|
+
base = `git merge-base HEAD main 2>/dev/null`.chomp
|
|
47
|
+
base = 'main' if base.empty?
|
|
48
|
+
output = `git diff --name-only #{base}...HEAD 2>/dev/null`
|
|
49
|
+
output.lines.map(&:chomp).reject(&:empty?)
|
|
50
|
+
when 'all'
|
|
51
|
+
nil
|
|
52
|
+
else
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def run(change_set: 'dirty', tool_type: nil, file_type_group: nil, variation: nil, git: nil)
|
|
58
|
+
matching = find_tools(tool_type: tool_type, file_type_group: file_type_group)
|
|
59
|
+
|
|
60
|
+
if matching.empty?
|
|
61
|
+
puts "No tools found matching criteria"
|
|
62
|
+
puts " tool_type: #{tool_type || '(any)'}"
|
|
63
|
+
puts " file_type_group: #{file_type_group || '(any)'}"
|
|
64
|
+
return false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
files = changed_files(change_set, git: git)
|
|
68
|
+
|
|
69
|
+
matching.each do |name, cfg|
|
|
70
|
+
tool_files = filter_files_for_tool(files, cfg)
|
|
71
|
+
|
|
72
|
+
if files && tool_files.empty?
|
|
73
|
+
puts "No matching files for '#{name}' (#{cfg['file_extensions']})"
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
cmd = if variation && cfg['variations'] && cfg['variations'][variation]
|
|
78
|
+
cfg['variations'][variation]
|
|
79
|
+
else
|
|
80
|
+
cfg['command']
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
filenames = tool_files ? tool_files.join(' ') : ''
|
|
84
|
+
cmd = cmd.gsub('[FILENAMES]', filenames)
|
|
85
|
+
|
|
86
|
+
puts "Running #{name}: #{cmd}"
|
|
87
|
+
system(cmd)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def add_tool(config_hash)
|
|
94
|
+
name = config_hash.delete('name') || config_hash.delete(:name)
|
|
95
|
+
unless name
|
|
96
|
+
puts "Tool name required"
|
|
97
|
+
return false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
configs = load_config
|
|
101
|
+
if configs.key?(name)
|
|
102
|
+
puts "Tool '#{name}' already exists"
|
|
103
|
+
return false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
configs[name] = config_hash.transform_keys(&:to_s)
|
|
107
|
+
save_config(configs)
|
|
108
|
+
puts "Added tool '#{name}'"
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def remove_tool(name)
|
|
113
|
+
configs = load_config
|
|
114
|
+
unless configs.key?(name)
|
|
115
|
+
puts "Tool '#{name}' not found"
|
|
116
|
+
return false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
configs.delete(name)
|
|
120
|
+
save_config(configs)
|
|
121
|
+
puts "Removed tool '#{name}'"
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def edit_config
|
|
126
|
+
editor = ENV['EDITOR'] || 'nvim'
|
|
127
|
+
system(editor, config_file)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.build_hiiro(parent_hiiro, rt, git: nil)
|
|
131
|
+
parent_hiiro.make_child(:run) do |h|
|
|
132
|
+
h.add_default do |*run_args|
|
|
133
|
+
change_set = nil
|
|
134
|
+
tool_type = nil
|
|
135
|
+
file_type_group = nil
|
|
136
|
+
variation = nil
|
|
137
|
+
|
|
138
|
+
positional = []
|
|
139
|
+
args_iter = run_args.each
|
|
140
|
+
loop do
|
|
141
|
+
arg = args_iter.next
|
|
142
|
+
case arg
|
|
143
|
+
when '--variation', '-v'
|
|
144
|
+
variation = args_iter.next
|
|
145
|
+
else
|
|
146
|
+
positional << arg
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
positional.each do |arg|
|
|
151
|
+
if KNOWN_CHANGE_SETS.include?(arg)
|
|
152
|
+
change_set = arg
|
|
153
|
+
elsif KNOWN_TOOL_TYPES.include?(arg)
|
|
154
|
+
tool_type = arg
|
|
155
|
+
else
|
|
156
|
+
# Treat as file_type_group
|
|
157
|
+
file_type_group ||= arg
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
change_set ||= 'dirty'
|
|
162
|
+
|
|
163
|
+
rt.run(
|
|
164
|
+
change_set: change_set,
|
|
165
|
+
tool_type: tool_type,
|
|
166
|
+
file_type_group: file_type_group,
|
|
167
|
+
variation: variation,
|
|
168
|
+
git: git,
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
h.add_subcmd(:ls) do
|
|
173
|
+
configs = rt.tools
|
|
174
|
+
if configs.empty?
|
|
175
|
+
puts "No tools configured."
|
|
176
|
+
puts "Use 'run add' to add one, or edit #{rt.config_file}"
|
|
177
|
+
next
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
puts "Configured tools:"
|
|
181
|
+
puts
|
|
182
|
+
configs.each do |name, cfg|
|
|
183
|
+
variations = cfg['variations'] ? " (#{cfg['variations'].keys.join(', ')})" : ""
|
|
184
|
+
puts format(" %-15s [%s] %-10s exts: %s%s",
|
|
185
|
+
name,
|
|
186
|
+
cfg['tool_type'] || '?',
|
|
187
|
+
cfg['file_type_group'] || '?',
|
|
188
|
+
cfg['file_extensions'] || '*',
|
|
189
|
+
variations)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
h.add_subcmd(:add) do |*add_args|
|
|
194
|
+
template = {
|
|
195
|
+
'tool_type' => 'lint',
|
|
196
|
+
'command' => 'echo [FILENAMES]',
|
|
197
|
+
'variations' => {},
|
|
198
|
+
'file_type_group' => '',
|
|
199
|
+
'file_extensions' => '',
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
require 'tempfile'
|
|
203
|
+
tmpfile = Tempfile.new(['tool', '.yml'])
|
|
204
|
+
tmpfile.write(YAML.dump({ 'new_tool' => template }))
|
|
205
|
+
tmpfile.close
|
|
206
|
+
|
|
207
|
+
editor = ENV['EDITOR'] || 'nvim'
|
|
208
|
+
system(editor, tmpfile.path)
|
|
209
|
+
|
|
210
|
+
begin
|
|
211
|
+
data = YAML.safe_load_file(tmpfile.path, permitted_classes: [Symbol]) || {}
|
|
212
|
+
data.each do |name, cfg|
|
|
213
|
+
rt.add_tool({ 'name' => name }.merge(cfg || {}))
|
|
214
|
+
end
|
|
215
|
+
rescue => e
|
|
216
|
+
puts "Error parsing config: #{e.message}"
|
|
217
|
+
ensure
|
|
218
|
+
tmpfile.unlink
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
h.add_subcmd(:rm) do |tool_name=nil|
|
|
223
|
+
unless tool_name
|
|
224
|
+
puts "Usage: run rm <name>"
|
|
225
|
+
next
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
rt.remove_tool(tool_name)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
h.add_subcmd(:config) do
|
|
232
|
+
rt.edit_config
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def load_config
|
|
240
|
+
return {} unless File.exist?(config_file)
|
|
241
|
+
YAML.safe_load_file(config_file, permitted_classes: [Symbol]) || {}
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def save_config(data)
|
|
245
|
+
FileUtils.mkdir_p(File.dirname(config_file))
|
|
246
|
+
File.write(config_file, YAML.dump(data))
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def filter_files_for_tool(files, cfg)
|
|
250
|
+
return files unless files
|
|
251
|
+
|
|
252
|
+
extensions = (cfg['file_extensions'] || '').split(',').map(&:strip)
|
|
253
|
+
return files if extensions.empty?
|
|
254
|
+
|
|
255
|
+
files.select { |f|
|
|
256
|
+
ext = File.extname(f).delete('.')
|
|
257
|
+
extensions.include?(ext)
|
|
258
|
+
}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def symbolize_keys(hash)
|
|
262
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
class Hiiro
|
|
6
|
+
class ServiceManager
|
|
7
|
+
CONFIG_FILE = File.join(Dir.home, '.config', 'hiiro', 'services.yml')
|
|
8
|
+
STATE_DIR = File.join(Dir.home, '.config', 'hiiro', 'services')
|
|
9
|
+
STATE_FILE = File.join(STATE_DIR, 'running.yml')
|
|
10
|
+
|
|
11
|
+
attr_reader :config_file, :state_file
|
|
12
|
+
|
|
13
|
+
def initialize(config_file: CONFIG_FILE, state_file: STATE_FILE)
|
|
14
|
+
@config_file = config_file
|
|
15
|
+
@state_file = state_file
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def services
|
|
19
|
+
load_config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_service(name)
|
|
23
|
+
configs = services
|
|
24
|
+
names = configs.keys.map { |k| OpenStruct.new(name: k) }
|
|
25
|
+
result = Hiiro::Matcher.new(names, :name).by_prefix(name)
|
|
26
|
+
match = result.resolved || result.first
|
|
27
|
+
return nil unless match
|
|
28
|
+
|
|
29
|
+
svc_name = match.item.name
|
|
30
|
+
{ name: svc_name, **symbolize_keys(configs[svc_name]) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def running?(name)
|
|
34
|
+
state = load_state
|
|
35
|
+
state.key?(name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def running_services
|
|
39
|
+
load_state
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def start(name, tmux_info: {}, task_info: {})
|
|
43
|
+
svc = find_service(name)
|
|
44
|
+
unless svc
|
|
45
|
+
puts "Service '#{name}' not found"
|
|
46
|
+
return false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
svc_name = svc[:name]
|
|
50
|
+
|
|
51
|
+
if running?(svc_name)
|
|
52
|
+
info = running_services[svc_name]
|
|
53
|
+
puts "Service '#{svc_name}' is already running (pid: #{info['pid']}, pane: #{info['tmux_pane']})"
|
|
54
|
+
return false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Run init commands
|
|
58
|
+
if svc[:init]
|
|
59
|
+
svc[:init].each do |cmd|
|
|
60
|
+
base_dir = svc[:base_dir]
|
|
61
|
+
if base_dir
|
|
62
|
+
system("cd #{base_dir} && #{cmd}")
|
|
63
|
+
else
|
|
64
|
+
system(cmd)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Start the service
|
|
70
|
+
start_cmd = svc[:start]
|
|
71
|
+
unless start_cmd
|
|
72
|
+
puts "No start command configured for '#{svc_name}'"
|
|
73
|
+
return false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
base_dir = svc[:base_dir]
|
|
77
|
+
pane_id = tmux_info[:pane] || ENV['TMUX_PANE']
|
|
78
|
+
|
|
79
|
+
if pane_id
|
|
80
|
+
if base_dir
|
|
81
|
+
system('tmux', 'send-keys', '-t', pane_id, "cd #{base_dir} && #{start_cmd}", 'Enter')
|
|
82
|
+
else
|
|
83
|
+
system('tmux', 'send-keys', '-t', pane_id, start_cmd, 'Enter')
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
if base_dir
|
|
87
|
+
system("cd #{base_dir} && #{start_cmd} &")
|
|
88
|
+
else
|
|
89
|
+
system("#{start_cmd} &")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Record state
|
|
94
|
+
state = load_state
|
|
95
|
+
state[svc_name] = {
|
|
96
|
+
'pid' => nil,
|
|
97
|
+
'tmux_session' => tmux_info[:session],
|
|
98
|
+
'tmux_window' => tmux_info[:window],
|
|
99
|
+
'tmux_pane' => pane_id,
|
|
100
|
+
'task' => task_info[:task_name],
|
|
101
|
+
'tree' => task_info[:tree],
|
|
102
|
+
'branch' => task_info[:branch],
|
|
103
|
+
'started_at' => Time.now.iso8601,
|
|
104
|
+
}
|
|
105
|
+
save_state(state)
|
|
106
|
+
|
|
107
|
+
puts "Started service '#{svc_name}'"
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def stop(name)
|
|
112
|
+
svc = find_service(name)
|
|
113
|
+
unless svc
|
|
114
|
+
puts "Service '#{name}' not found"
|
|
115
|
+
return false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
svc_name = svc[:name]
|
|
119
|
+
|
|
120
|
+
unless running?(svc_name)
|
|
121
|
+
puts "Service '#{svc_name}' is not running"
|
|
122
|
+
return false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
info = running_services[svc_name]
|
|
126
|
+
pane_id = info['tmux_pane']
|
|
127
|
+
|
|
128
|
+
if svc[:stop]
|
|
129
|
+
stop_cmd = svc[:stop]
|
|
130
|
+
if info['pid']
|
|
131
|
+
stop_cmd = stop_cmd.gsub('$PID', info['pid'].to_s)
|
|
132
|
+
end
|
|
133
|
+
system(stop_cmd)
|
|
134
|
+
elsif pane_id
|
|
135
|
+
system('tmux', 'send-keys', '-t', pane_id, 'C-c')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Run cleanup commands
|
|
139
|
+
if svc[:cleanup]
|
|
140
|
+
svc[:cleanup].each { |cmd| system(cmd) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Remove from state
|
|
144
|
+
state = load_state
|
|
145
|
+
state.delete(svc_name)
|
|
146
|
+
save_state(state)
|
|
147
|
+
|
|
148
|
+
puts "Stopped service '#{svc_name}'"
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def attach(name)
|
|
153
|
+
svc = find_service(name)
|
|
154
|
+
unless svc
|
|
155
|
+
puts "Service '#{name}' not found"
|
|
156
|
+
return false
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
svc_name = svc[:name]
|
|
160
|
+
unless running?(svc_name)
|
|
161
|
+
puts "Service '#{svc_name}' is not running"
|
|
162
|
+
return false
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
info = running_services[svc_name]
|
|
166
|
+
pane_id = info['tmux_pane']
|
|
167
|
+
session = info['tmux_session']
|
|
168
|
+
|
|
169
|
+
if session
|
|
170
|
+
system('tmux', 'switch-client', '-t', session)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if pane_id
|
|
174
|
+
system('tmux', 'select-pane', '-t', pane_id)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
true
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def url(name)
|
|
181
|
+
svc = find_service(name)
|
|
182
|
+
return nil unless svc
|
|
183
|
+
|
|
184
|
+
host = svc[:host] || 'localhost'
|
|
185
|
+
port = svc[:port]
|
|
186
|
+
return nil unless port
|
|
187
|
+
|
|
188
|
+
"http://#{host}:#{port}"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def port(name)
|
|
192
|
+
svc = find_service(name)
|
|
193
|
+
svc&.[](:port)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def host(name)
|
|
197
|
+
svc = find_service(name)
|
|
198
|
+
svc&.[](:host) || 'localhost'
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def status(name)
|
|
202
|
+
svc = find_service(name)
|
|
203
|
+
unless svc
|
|
204
|
+
puts "Service '#{name}' not found"
|
|
205
|
+
return
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
svc_name = svc[:name]
|
|
209
|
+
puts "Service: #{svc_name}"
|
|
210
|
+
puts "Base dir: #{svc[:base_dir] || '(none)'}"
|
|
211
|
+
puts "URL: #{url(svc_name) || '(none)'}"
|
|
212
|
+
|
|
213
|
+
if running?(svc_name)
|
|
214
|
+
info = running_services[svc_name]
|
|
215
|
+
puts "Status: running"
|
|
216
|
+
puts "PID: #{info['pid'] || '(unknown)'}"
|
|
217
|
+
puts "Pane: #{info['tmux_pane'] || '(unknown)'}"
|
|
218
|
+
puts "Task: #{info['task'] || '(none)'}"
|
|
219
|
+
puts "Started: #{info['started_at']}"
|
|
220
|
+
else
|
|
221
|
+
puts "Status: stopped"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def services_for_dir(base_dir)
|
|
226
|
+
configs = services
|
|
227
|
+
configs.select { |_, v| v['base_dir'] == base_dir }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def add_service(config_hash)
|
|
231
|
+
name = config_hash.delete('name') || config_hash.delete(:name)
|
|
232
|
+
unless name
|
|
233
|
+
puts "Service name required"
|
|
234
|
+
return false
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
configs = load_config
|
|
238
|
+
if configs.key?(name)
|
|
239
|
+
puts "Service '#{name}' already exists"
|
|
240
|
+
return false
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
configs[name] = config_hash.transform_keys(&:to_s)
|
|
244
|
+
save_config(configs)
|
|
245
|
+
puts "Added service '#{name}'"
|
|
246
|
+
true
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def remove_service(name)
|
|
250
|
+
configs = load_config
|
|
251
|
+
unless configs.key?(name)
|
|
252
|
+
puts "Service '#{name}' not found"
|
|
253
|
+
return false
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
configs.delete(name)
|
|
257
|
+
save_config(configs)
|
|
258
|
+
puts "Removed service '#{name}'"
|
|
259
|
+
true
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def self.build_hiiro(parent_hiiro, sm, task_manager: nil)
|
|
263
|
+
parent_hiiro.make_child(:service) do |h|
|
|
264
|
+
h.add_subcmd(:ls) do
|
|
265
|
+
configs = sm.services
|
|
266
|
+
if configs.empty?
|
|
267
|
+
puts "No services configured."
|
|
268
|
+
puts "Use 'service add' to add one, or edit #{sm.config_file}"
|
|
269
|
+
next
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
running = sm.running_services
|
|
273
|
+
puts "Services:"
|
|
274
|
+
puts
|
|
275
|
+
configs.each do |name, cfg|
|
|
276
|
+
status = running.key?(name) ? "running" : "stopped"
|
|
277
|
+
port_str = cfg['port'] ? ":#{cfg['port']}" : ""
|
|
278
|
+
puts format(" %-20s [%s]%s %s", name, status, port_str, cfg['base_dir'] || '')
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
h.add_subcmd(:list) do
|
|
283
|
+
h.run_subcmd(:ls)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
h.add_subcmd(:start) do |svc_name=nil|
|
|
287
|
+
unless svc_name
|
|
288
|
+
puts "Usage: service start <name>"
|
|
289
|
+
next
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
tmux_info = {
|
|
293
|
+
session: ENV['TMUX'] ? `tmux display-message -p '#S'`.chomp : nil,
|
|
294
|
+
window: ENV['TMUX'] ? `tmux display-message -p '#I'`.chomp : nil,
|
|
295
|
+
pane: ENV['TMUX_PANE'],
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
task_info = {}
|
|
299
|
+
if task_manager
|
|
300
|
+
task = task_manager.current_task
|
|
301
|
+
if task
|
|
302
|
+
task_info = {
|
|
303
|
+
task_name: task.name,
|
|
304
|
+
tree: task.tree_name,
|
|
305
|
+
branch: task.branch,
|
|
306
|
+
session: task.session_name,
|
|
307
|
+
}
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
sm.start(svc_name, tmux_info: tmux_info, task_info: task_info)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
h.add_subcmd(:stop) do |svc_name=nil|
|
|
315
|
+
unless svc_name
|
|
316
|
+
puts "Usage: service stop <name>"
|
|
317
|
+
next
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
sm.stop(svc_name)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
h.add_subcmd(:attach) do |svc_name=nil|
|
|
324
|
+
unless svc_name
|
|
325
|
+
puts "Usage: service attach <name>"
|
|
326
|
+
next
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
sm.attach(svc_name)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
h.add_subcmd(:open) do |svc_name=nil|
|
|
333
|
+
unless svc_name
|
|
334
|
+
puts "Usage: service open <name>"
|
|
335
|
+
next
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
svc_url = sm.url(svc_name)
|
|
339
|
+
if svc_url
|
|
340
|
+
system('open', svc_url)
|
|
341
|
+
else
|
|
342
|
+
puts "No URL configured for '#{svc_name}'"
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
h.add_subcmd(:url) do |svc_name=nil|
|
|
347
|
+
unless svc_name
|
|
348
|
+
puts "Usage: service url <name>"
|
|
349
|
+
next
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
svc_url = sm.url(svc_name)
|
|
353
|
+
if svc_url
|
|
354
|
+
puts svc_url
|
|
355
|
+
else
|
|
356
|
+
puts "No URL configured for '#{svc_name}'"
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
h.add_subcmd(:port) do |svc_name=nil|
|
|
361
|
+
unless svc_name
|
|
362
|
+
puts "Usage: service port <name>"
|
|
363
|
+
next
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
p = sm.port(svc_name)
|
|
367
|
+
if p
|
|
368
|
+
puts p
|
|
369
|
+
else
|
|
370
|
+
puts "No port configured for '#{svc_name}'"
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
h.add_subcmd(:status) do |svc_name=nil|
|
|
375
|
+
unless svc_name
|
|
376
|
+
puts "Usage: service status <name>"
|
|
377
|
+
next
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
sm.status(svc_name)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
h.add_subcmd(:add) do |*add_args|
|
|
384
|
+
template = {
|
|
385
|
+
'base_dir' => '',
|
|
386
|
+
'host' => 'localhost',
|
|
387
|
+
'port' => '',
|
|
388
|
+
'init' => [],
|
|
389
|
+
'start' => '',
|
|
390
|
+
'stop' => '',
|
|
391
|
+
'cleanup' => [],
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
require 'tempfile'
|
|
395
|
+
tmpfile = Tempfile.new(['service', '.yml'])
|
|
396
|
+
tmpfile.write(YAML.dump({ 'new_service' => template }))
|
|
397
|
+
tmpfile.close
|
|
398
|
+
|
|
399
|
+
editor = ENV['EDITOR'] || 'nvim'
|
|
400
|
+
system(editor, tmpfile.path)
|
|
401
|
+
|
|
402
|
+
begin
|
|
403
|
+
data = YAML.safe_load_file(tmpfile.path, permitted_classes: [Symbol]) || {}
|
|
404
|
+
data.each do |name, cfg|
|
|
405
|
+
sm.add_service({ 'name' => name }.merge(cfg || {}))
|
|
406
|
+
end
|
|
407
|
+
rescue => e
|
|
408
|
+
puts "Error parsing config: #{e.message}"
|
|
409
|
+
ensure
|
|
410
|
+
tmpfile.unlink
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
h.add_subcmd(:rm) do |svc_name=nil|
|
|
415
|
+
unless svc_name
|
|
416
|
+
puts "Usage: service rm <name>"
|
|
417
|
+
next
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
sm.remove_service(svc_name)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
h.add_subcmd(:remove) do |svc_name=nil|
|
|
424
|
+
unless svc_name
|
|
425
|
+
puts "Usage: service remove <name>"
|
|
426
|
+
next
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
sm.remove_service(svc_name)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
h.add_subcmd(:config) do
|
|
433
|
+
editor = ENV['EDITOR'] || 'nvim'
|
|
434
|
+
system(editor, sm.config_file)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
private
|
|
440
|
+
|
|
441
|
+
def load_config
|
|
442
|
+
return {} unless File.exist?(config_file)
|
|
443
|
+
YAML.safe_load_file(config_file, permitted_classes: [Symbol]) || {}
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def save_config(data)
|
|
447
|
+
FileUtils.mkdir_p(File.dirname(config_file))
|
|
448
|
+
File.write(config_file, YAML.dump(data))
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def load_state
|
|
452
|
+
return {} unless File.exist?(state_file)
|
|
453
|
+
YAML.safe_load_file(state_file, permitted_classes: [Symbol]) || {}
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def save_state(data)
|
|
457
|
+
FileUtils.mkdir_p(File.dirname(state_file))
|
|
458
|
+
File.write(state_file, YAML.dump(data))
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def symbolize_keys(hash)
|
|
462
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
data/lib/hiiro/version.rb
CHANGED
data/lib/hiiro.rb
CHANGED
|
@@ -16,7 +16,7 @@ require_relative "hiiro/queue"
|
|
|
16
16
|
require_relative "hiiro/tasks"
|
|
17
17
|
require_relative "hiiro/tmux"
|
|
18
18
|
require_relative "hiiro/todo"
|
|
19
|
-
require_relative "hiiro/
|
|
19
|
+
require_relative "hiiro/service_manager"
|
|
20
20
|
require_relative "hiiro/runner_tool"
|
|
21
21
|
require_relative "hiiro/app_files"
|
|
22
22
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hiiro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.147
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Toyota
|
|
@@ -118,6 +118,7 @@ files:
|
|
|
118
118
|
- h-tmux-plugins.tmux
|
|
119
119
|
- hiiro.gemspec
|
|
120
120
|
- lib/hiiro.rb
|
|
121
|
+
- lib/hiiro/app_files.rb
|
|
121
122
|
- lib/hiiro/bins.rb
|
|
122
123
|
- lib/hiiro/fuzzyfind.rb
|
|
123
124
|
- lib/hiiro/git.rb
|
|
@@ -133,6 +134,8 @@ files:
|
|
|
133
134
|
- lib/hiiro/options.rb
|
|
134
135
|
- lib/hiiro/paths.rb
|
|
135
136
|
- lib/hiiro/queue.rb
|
|
137
|
+
- lib/hiiro/runner_tool.rb
|
|
138
|
+
- lib/hiiro/service_manager.rb
|
|
136
139
|
- lib/hiiro/shell.rb
|
|
137
140
|
- lib/hiiro/tasks.rb
|
|
138
141
|
- lib/hiiro/tmux.rb
|