hiiro 0.1.334 → 0.1.335

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1393ef224407fe149858ba4e6ab28d25d9b1ea8234c38295b7248db64f05fc18
4
- data.tar.gz: 0cb6c207d70bc9b366fe82ad91b0f22d48a3e23d0efd58bb4ce5a9746deb5935
3
+ metadata.gz: 38d14de5a6bc8cbd862309a844bf9637e98e1fe450c85589f4e8c620ff7f65d0
4
+ data.tar.gz: 4de0bc0fe83210c727c275bb05266e27b37b039f4f8c130cb7aac63c471b89c6
5
5
  SHA512:
6
- metadata.gz: e5c17a94ccb9381d940fd594b7c5736433c565c99e78a003b2cf02c6993bf59d727a212e0e98e64ba79812b6b608f9e3472c86702fdb59972fc0d3c5cd9254cb
7
- data.tar.gz: 1bce72784775186056d106cb55aa253ea9a0ce0fb5fef4b3c9b6800b37e822a7b0ff0be4cabcb5f46b64ae89733b23c3b9ecb7988391c9d8cf8f1e2536ff707c
6
+ metadata.gz: 31f62ef65b741598265dcdbdc08ae95a4a0cc0043c435cebfee9d5a5dd21b98ca580e32875bbbf45f1371aad700f9e6b0891392eaada22cc1a38361236f0d8ce
7
+ data.tar.gz: 3e07dff0a8880ab7368cbdcb39df9e461b3d1e0433c1952d979714b19acb1672686c5e17e240e981a14af419aa94ea755cc93823c8a30a82556c3b60e05270ce
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.335] - 2026-04-07
4
+
5
+ ### Added
6
+ - New `Hiiro::PsProcess` class (`lib/hiiro/ps_process.rb`) for encapsulated process info:
7
+ - `PsProcess.from_line(line)` - parse `ps awwux` output
8
+ - `PsProcess.all`, `.search(pattern)`, `.find(pid)`, `.in_dirs(*paths)`
9
+ - Instance methods: `#files`, `#ports`, `#dir`, `#parent`, `#children`
10
+ - Simple `#to_s` output: PID + CMD
11
+ - New `h ps` subcommands: `info`, `files`, `ports`
12
+ - Smart argument resolution in `h ps`: accepts PID, search pattern, or directory path
13
+
14
+ ### Changed
15
+ - Refactored `h-ps` to use `PsProcess` class instead of raw `ps` parsing
16
+
3
17
  ## [0.1.334] - 2026-04-07
4
18
 
5
19
  ### Added
@@ -356,4 +370,4 @@
356
370
  ## [0.1.295]
357
371
 
358
372
  ### Changed
359
- - Filter logic changes for PR management
373
+ - Filter logic changes for PR management
data/README.md CHANGED
@@ -356,3 +356,11 @@ MIT
356
356
 
357
357
 
358
358
  ```
359
+
360
+ # Development
361
+
362
+ Testing locally:
363
+
364
+ ```sh
365
+ ruby -I lib bin/h-ps search ruby
366
+ ```
data/bin/h-ps CHANGED
@@ -3,19 +3,33 @@
3
3
  require "hiiro"
4
4
 
5
5
  Hiiro.run(*ARGV) do
6
+ # Resolve argument to list of PsProcess instances
7
+ # - all digits: find by PID
8
+ # - valid path: processes with files open in that dir
9
+ # - otherwise: search by pattern
10
+ def resolve_processes(arg)
11
+ return [] if arg.nil?
12
+
13
+ if arg.match?(/^\d+$/)
14
+ p = Hiiro::PsProcess.find(arg)
15
+ p ? [p] : []
16
+ elsif File.directory?(File.expand_path(arg))
17
+ Hiiro::PsProcess.in_dirs(arg)
18
+ else
19
+ Hiiro::PsProcess.search(arg)
20
+ end
21
+ end
22
+
6
23
  add_subcmd(:search) { |pattern = nil|
7
24
  if pattern.nil?
8
25
  puts "Usage: h ps search <pattern>"
9
26
  next
10
27
  end
11
28
 
12
- output = `ps auxww`.lines
13
- header = output.first
14
- matches = output[1..].select { |line| line.include?(pattern) }
29
+ processes = Hiiro::PsProcess.search(pattern)
15
30
 
16
- if matches.any?
17
- puts header
18
- matches.each { |line| puts line }
31
+ if processes.any?
32
+ processes.each { |p| puts p }
19
33
  else
20
34
  puts "No processes found matching '#{pattern}'"
21
35
  end
@@ -27,64 +41,119 @@ Hiiro.run(*ARGV) do
27
41
  next
28
42
  end
29
43
 
30
- pids = Set.new
31
- paths.each do |path|
32
- expanded = File.expand_path(path)
33
- lsof_output = `lsof +D #{expanded.shellescape} 2>/dev/null`.lines[1..]
34
- next unless lsof_output
44
+ processes = Hiiro::PsProcess.in_dirs(*paths)
45
+
46
+ if processes.empty?
47
+ puts "No processes found with files open in: #{paths.join(', ')}"
48
+ else
49
+ processes.each { |p| puts p }
50
+ end
51
+ }
52
+
53
+ add_subcmd(:getdir) { |pattern = nil|
54
+ if pattern.nil?
55
+ puts "Usage: h ps getdir <pattern>"
56
+ next
57
+ end
58
+
59
+ processes = Hiiro::PsProcess.search(pattern)
60
+
61
+ if processes.empty?
62
+ puts "No processes found matching '#{pattern}'"
63
+ next
64
+ end
35
65
 
36
- lsof_output.each do |line|
37
- fields = line.split
38
- pids << fields[1] if fields[1]
66
+ found_any = false
67
+ processes.each do |p|
68
+ dir = p.dir
69
+ if dir
70
+ found_any = true
71
+ puts "#{p.pid}\t#{dir}\t#{p.cmd}"
39
72
  end
40
73
  end
41
74
 
42
- if pids.empty?
43
- puts "No processes found with files open in: #{paths.join(', ')}"
75
+ puts "Could not determine working directories for matching processes" unless found_any
76
+ }
77
+
78
+ add_subcmd(:info) { |arg = nil|
79
+ if arg.nil?
80
+ puts "Usage: h ps info <pid|pattern|path>"
44
81
  next
45
82
  end
46
83
 
47
- ps_output = `ps auxww`.lines
48
- header = ps_output.first
49
- matches = ps_output[1..].select { |line| pids.include?(line.split[1]) }
84
+ processes = resolve_processes(arg)
50
85
 
51
- puts header
52
- matches.each { |line| puts line }
86
+ if processes.empty?
87
+ puts "No processes found for '#{arg}'"
88
+ next
89
+ end
90
+
91
+ processes.each_with_index do |p, i|
92
+ puts "---" if i > 0
93
+ puts "PID: #{p.pid}"
94
+ puts "User: #{p.user}"
95
+ puts "CPU: #{p.cpu}%"
96
+ puts "Mem: #{p.mem}%"
97
+ puts "Stat: #{p.stat}"
98
+ puts "Dir: #{p.dir || '(unknown)'}"
99
+ puts "Cmd: #{p.cmd}"
100
+
101
+ parent = p.parent
102
+ puts "Parent: #{parent.pid} #{parent.cmd.slice(0, 60)}" if parent
103
+
104
+ children = p.children
105
+ if children.any?
106
+ puts "Children:"
107
+ children.each { |c| puts " #{c.pid}\t#{c.cmd.slice(0, 60)}" }
108
+ end
109
+ end
53
110
  }
54
111
 
55
- add_subcmd(:getdir) { |pattern = nil|
56
- if pattern.nil?
57
- puts "Usage: h ps getdir <pattern>"
112
+ add_subcmd(:files) { |arg = nil|
113
+ if arg.nil?
114
+ puts "Usage: h ps files <pid|pattern|path>"
58
115
  next
59
116
  end
60
117
 
61
- ps_output = `ps auxww`.lines
62
- pids = ps_output[1..].select { |line| line.include?(pattern) }.map { |line| line.split[1] }
118
+ processes = resolve_processes(arg)
63
119
 
64
- if pids.empty?
65
- puts "No processes found matching '#{pattern}'"
120
+ if processes.empty?
121
+ puts "No processes found for '#{arg}'"
66
122
  next
67
123
  end
68
124
 
69
- dirs = {}
70
- pids.each do |pid|
71
- cwd = `lsof -p #{pid} 2>/dev/null | grep cwd`.strip
72
- if cwd && !cwd.empty?
73
- # lsof cwd line format: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
74
- dir = cwd.split.last
75
- dirs[pid] = dir
125
+ processes.each do |p|
126
+ puts "=== #{p.pid}\t#{p.cmd.slice(0, 60)}" if processes.size > 1
127
+ files = p.files
128
+ if files.empty?
129
+ puts " (no open files)" if processes.size > 1
130
+ else
131
+ files.each { |f| puts "#{f[:fd]}\t#{f[:type]}\t#{f[:name]}" }
76
132
  end
77
133
  end
134
+ }
78
135
 
79
- if dirs.empty?
80
- puts "Could not determine working directories for matching processes"
136
+ add_subcmd(:ports) { |arg = nil|
137
+ if arg.nil?
138
+ puts "Usage: h ps ports <pid|pattern|path>"
81
139
  next
82
140
  end
83
141
 
84
- dirs.each do |pid, dir|
85
- process_line = ps_output.find { |l| l.split[1] == pid }
86
- cmd = process_line.split[10..].join(' ') rescue '(unknown)'
87
- puts "#{pid}\t#{dir}\t#{cmd}"
142
+ processes = resolve_processes(arg)
143
+
144
+ if processes.empty?
145
+ puts "No processes found for '#{arg}'"
146
+ next
147
+ end
148
+
149
+ processes.each do |p|
150
+ puts "=== #{p.pid}\t#{p.cmd.slice(0, 60)}" if processes.size > 1
151
+ ports = p.ports
152
+ if ports.empty?
153
+ puts " (no open ports)" if processes.size > 1
154
+ else
155
+ ports.each { |pt| puts "#{pt[:protocol]}\t#{pt[:name]}" }
156
+ end
88
157
  end
89
158
  }
90
159
  end
@@ -0,0 +1,119 @@
1
+ require 'set'
2
+ require 'shellwords'
3
+
4
+ class Hiiro::PsProcess
5
+ attr_reader :user, :pid, :cpu, :mem, :vsz, :rss, :tty, :stat, :start, :time, :cmd
6
+
7
+ def initialize(user:, pid:, cpu:, mem:, vsz:, rss:, tty:, stat:, start:, time:, cmd:)
8
+ @user = user
9
+ @pid = pid
10
+ @cpu = cpu
11
+ @mem = mem
12
+ @vsz = vsz
13
+ @rss = rss
14
+ @tty = tty
15
+ @stat = stat
16
+ @start = start
17
+ @time = time
18
+ @cmd = cmd
19
+ end
20
+
21
+ # Parse a line from `ps awwux` output
22
+ # Format: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND...
23
+ def self.from_line(line)
24
+ parts = line.split
25
+ return nil if parts.size < 11
26
+
27
+ new(
28
+ user: parts[0],
29
+ pid: parts[1],
30
+ cpu: parts[2],
31
+ mem: parts[3],
32
+ vsz: parts[4],
33
+ rss: parts[5],
34
+ tty: parts[6],
35
+ stat: parts[7],
36
+ start: parts[8],
37
+ time: parts[9],
38
+ cmd: parts[10..].join(' ')
39
+ )
40
+ end
41
+
42
+ # Get all processes
43
+ def self.all
44
+ `ps awwux`.lines[1..].filter_map { |line| from_line(line) }
45
+ end
46
+
47
+ # Search processes by pattern (matches against full line)
48
+ def self.search(pattern)
49
+ all.select { |p| p.cmd.include?(pattern) || p.user.include?(pattern) }
50
+ end
51
+
52
+ # Find process by PID
53
+ def self.find(pid)
54
+ all.find { |p| p.pid == pid.to_s }
55
+ end
56
+
57
+ # Find processes with files open in given directories
58
+ def self.in_dirs(*paths)
59
+ pids = Set.new
60
+ paths.each do |path|
61
+ expanded = File.expand_path(path)
62
+ lsof_output = `lsof +D #{expanded.shellescape} 2>/dev/null`.lines[1..]
63
+ next unless lsof_output
64
+
65
+ lsof_output.each do |line|
66
+ fields = line.split
67
+ pids << fields[1] if fields[1]
68
+ end
69
+ end
70
+
71
+ all.select { |p| pids.include?(p.pid) }
72
+ end
73
+
74
+ # Open files for this process
75
+ def files
76
+ `lsof -p #{pid} 2>/dev/null`.lines[1..].map do |line|
77
+ parts = line.split
78
+ { fd: parts[3], type: parts[4], name: parts[8] } if parts.size >= 9
79
+ end.compact
80
+ end
81
+
82
+ # Open network ports for this process
83
+ def ports
84
+ `lsof -p #{pid} -i 2>/dev/null`.lines[1..].map do |line|
85
+ parts = line.split
86
+ { protocol: parts[7], name: parts[8] } if parts.size >= 9
87
+ end.compact
88
+ end
89
+
90
+ # Current working directory
91
+ def dir
92
+ cwd_line = `lsof -p #{pid} 2>/dev/null | grep ' cwd '`.strip
93
+ return nil if cwd_line.empty?
94
+ cwd_line.split.last
95
+ end
96
+
97
+ # Parent process
98
+ def parent
99
+ ppid = `ps -o ppid= -p #{pid}`.strip
100
+ return nil if ppid.empty?
101
+ self.class.find(ppid)
102
+ end
103
+
104
+ # Child processes
105
+ def children
106
+ child_pids = `pgrep -P #{pid}`.lines.map(&:strip)
107
+ child_pids.filter_map { |cpid| self.class.find(cpid) }
108
+ end
109
+
110
+ # Simple display: PID and CMD
111
+ def to_s
112
+ "#{pid}\t#{cmd}"
113
+ end
114
+
115
+ # Detailed display
116
+ def inspect
117
+ "#<PsProcess pid=#{pid} user=#{user} stat=#{stat} cmd=#{cmd.slice(0, 40)}...>"
118
+ end
119
+ end
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.334"
2
+ VERSION = "0.1.335"
3
3
  end
data/lib/hiiro.rb CHANGED
@@ -35,6 +35,7 @@ require_relative "hiiro/todo"
35
35
  require_relative "hiiro/service_manager"
36
36
  require_relative "hiiro/runner_tool"
37
37
  require_relative "hiiro/app_files"
38
+ require_relative "hiiro/ps_process"
38
39
  require_relative "hiiro/rbenv"
39
40
  require_relative "hiiro/any_struct"
40
41
  require_relative "hiiro/pinned_pr"
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.334
4
+ version: 0.1.335
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
@@ -351,6 +351,7 @@ files:
351
351
  - lib/hiiro/pinned_pr_manager.rb
352
352
  - lib/hiiro/project.rb
353
353
  - lib/hiiro/projects.rb
354
+ - lib/hiiro/ps_process.rb
354
355
  - lib/hiiro/queue.rb
355
356
  - lib/hiiro/rbenv.rb
356
357
  - lib/hiiro/registry.rb