network-infra-utility 0.3.0 → 0.6.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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +21 -0
  3. data/GUIDE.md +212 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +70 -0
  6. data/Rakefile +1 -1
  7. data/bin/dns-query +836 -0
  8. data/bin/geo-doc +135 -0
  9. data/bin/geo-get +1 -1
  10. data/bin/geo-update +429 -0
  11. data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
  12. data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
  13. data/document/Geo/345/221/275/344/273/244/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +441 -0
  14. data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
  15. data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
  16. data/document/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/344/275/277/347/224/250/346/226/271/346/263/225.md +764 -0
  17. data/network-infra-utility.gemspec +4 -2
  18. data/network.rb +3 -1
  19. data/service/geodb/geodb.rb +278 -1
  20. data/service/ssh/README.md +955 -0
  21. data/service/ssh/bin/ssh-client +198 -0
  22. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/345/212/237/350/203/275/351/234/200/346/261/202/346/226/207/346/241/243.md +292 -0
  23. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/257/246/347/273/206/350/256/276/350/256/241/346/226/207/346/241/243.md +1521 -0
  24. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/275/257/344/273/266/350/256/276/350/256/241/346/226/207/346/241/243.md +2493 -0
  25. data/service/ssh/ext/ssh_core/bin/ssh_core.cmd +28 -0
  26. data/service/ssh/ext/ssh_core/config/sys.config +0 -0
  27. data/service/ssh/ext/ssh_core/config/vm.args +0 -0
  28. data/service/ssh/ext/ssh_core/local_deps/jsx/CHECKSUM +1 -0
  29. data/service/ssh/ext/ssh_core/local_deps/jsx/LICENSE +21 -0
  30. data/service/ssh/ext/ssh_core/local_deps/jsx/README.md +696 -0
  31. data/service/ssh/ext/ssh_core/local_deps/jsx/VERSION +1 -0
  32. data/service/ssh/ext/ssh_core/local_deps/jsx/contents.tar.gz +0 -0
  33. data/service/ssh/ext/ssh_core/local_deps/jsx/metadata.config +15 -0
  34. data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.config +17 -0
  35. data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.lock +1 -0
  36. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.app.src +10 -0
  37. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.erl +506 -0
  38. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.erl +393 -0
  39. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.hrl +18 -0
  40. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_consult.erl +81 -0
  41. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_decoder.erl +1909 -0
  42. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_encoder.erl +116 -0
  43. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_parser.erl +1214 -0
  44. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_json.erl +408 -0
  45. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_term.erl +389 -0
  46. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_verify.erl +121 -0
  47. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx.erl +506 -0
  48. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.erl +393 -0
  49. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.hrl +18 -0
  50. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_consult.erl +81 -0
  51. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_decoder.erl +1909 -0
  52. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_encoder.erl +116 -0
  53. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_parser.erl +1214 -0
  54. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_json.erl +408 -0
  55. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_term.erl +389 -0
  56. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_verify.erl +121 -0
  57. data/service/ssh/ext/ssh_core/rebar.config +24 -0
  58. data/service/ssh/ext/ssh_core/rebar.lock +1 -0
  59. data/service/ssh/ext/ssh_core/src/ssh_auth_engine.erl +156 -0
  60. data/service/ssh/ext/ssh_core/src/ssh_channel_stm.erl +232 -0
  61. data/service/ssh/ext/ssh_core/src/ssh_codec.erl +83 -0
  62. data/service/ssh/ext/ssh_core/src/ssh_conn_sup.erl +48 -0
  63. data/service/ssh/ext/ssh_core/src/ssh_conn_worker.erl +535 -0
  64. data/service/ssh/ext/ssh_core/src/ssh_core.app.src +36 -0
  65. data/service/ssh/ext/ssh_core/src/ssh_core_app.erl +11 -0
  66. data/service/ssh/ext/ssh_core/src/ssh_core_sup.erl +46 -0
  67. data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +117 -0
  68. data/service/ssh/ext/ssh_core/src/ssh_ipc.hrl +80 -0
  69. data/service/ssh/ext/ssh_core/src/ssh_ipc_coalesce.erl +94 -0
  70. data/service/ssh/ext/ssh_core/src/ssh_ipc_gateway.erl +467 -0
  71. data/service/ssh/ext/ssh_core/src/ssh_ipc_proto.erl +95 -0
  72. data/service/ssh/ext/ssh_core/src/ssh_jump_chain.erl +101 -0
  73. data/service/ssh/ext/ssh_core/src/ssh_keepalive_mgr.erl +222 -0
  74. data/service/ssh/ext/ssh_core/src/ssh_known_hosts_proxy.erl +67 -0
  75. data/service/ssh/ext/ssh_core/src/ssh_port_fwd.erl +225 -0
  76. data/service/ssh/ext/ssh_core/src/ssh_sftp_session.erl +250 -0
  77. data/service/ssh/ext/ssh_core/src/ssh_sftp_sup.erl +62 -0
  78. data/service/ssh/ext/ssh_core_rs/Cargo.lock +2345 -0
  79. data/service/ssh/ext/ssh_core_rs/Cargo.toml +30 -0
  80. data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs +34 -0
  81. data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs.cmd +40 -0
  82. data/service/ssh/ext/ssh_core_rs/src/channel.rs +296 -0
  83. data/service/ssh/ext/ssh_core_rs/src/coalesce.rs +143 -0
  84. data/service/ssh/ext/ssh_core_rs/src/codec.rs +71 -0
  85. data/service/ssh/ext/ssh_core_rs/src/conn.rs +628 -0
  86. data/service/ssh/ext/ssh_core_rs/src/gateway.rs +389 -0
  87. data/service/ssh/ext/ssh_core_rs/src/handler.rs +293 -0
  88. data/service/ssh/ext/ssh_core_rs/src/keepalive.rs +194 -0
  89. data/service/ssh/ext/ssh_core_rs/src/main.rs +351 -0
  90. data/service/ssh/ext/ssh_core_rs/src/portfwd.rs +378 -0
  91. data/service/ssh/ext/ssh_core_rs/src/proto.rs +198 -0
  92. data/service/ssh/ext/ssh_core_rs/src/sftp.rs +294 -0
  93. data/service/ssh/lib/network_infra_utility/ssh/automation/macro_engine.rb +213 -0
  94. data/service/ssh/lib/network_infra_utility/ssh/client.rb +257 -0
  95. data/service/ssh/lib/network_infra_utility/ssh/config/schema.rb +90 -0
  96. data/service/ssh/lib/network_infra_utility/ssh/config/settings.rb +103 -0
  97. data/service/ssh/lib/network_infra_utility/ssh/config/store.rb +90 -0
  98. data/service/ssh/lib/network_infra_utility/ssh/ipc/coalesce.rb +83 -0
  99. data/service/ssh/lib/network_infra_utility/ssh/ipc/errors.rb +36 -0
  100. data/service/ssh/lib/network_infra_utility/ssh/ipc/router.rb +212 -0
  101. data/service/ssh/lib/network_infra_utility/ssh/ipc/transport.rb +81 -0
  102. data/service/ssh/lib/network_infra_utility/ssh/security/host_key.rb +211 -0
  103. data/service/ssh/lib/network_infra_utility/ssh/security/vault.rb +211 -0
  104. data/service/ssh/lib/network_infra_utility/ssh/session/history.rb +56 -0
  105. data/service/ssh/lib/network_infra_utility/ssh/session/manager.rb +92 -0
  106. data/service/ssh/lib/network_infra_utility/ssh/session/session.rb +109 -0
  107. data/service/ssh/lib/network_infra_utility/ssh/session/tree.rb +95 -0
  108. data/service/ssh/lib/network_infra_utility/ssh/terminal/ansi_parser.rb +435 -0
  109. data/service/ssh/lib/network_infra_utility/ssh/terminal/buffer.rb +78 -0
  110. data/service/ssh/lib/network_infra_utility/ssh/terminal/emulator.rb +159 -0
  111. data/service/ssh/lib/network_infra_utility/ssh/terminal/logger.rb +195 -0
  112. data/service/ssh/lib/network_infra_utility/ssh/terminal/screen.rb +212 -0
  113. data/service/ssh/lib/network_infra_utility/ssh/terminal/theme.rb +127 -0
  114. data/service/ssh/lib/network_infra_utility/ssh/version.rb +7 -0
  115. data/service/ssh/lib/network_infra_utility/ssh.rb +44 -0
  116. data/support/basic/as_num.rb +221 -0
  117. data/support/basic/mac_address.rb +281 -0
  118. data/version.rb +1 -1
  119. metadata +142 -1
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "time"
5
+ require "stringio"
6
+
7
+ module NetworkInfraUtility
8
+ module SSH
9
+ module Terminal
10
+ # 会话日志记录与自动轮转。
11
+ # 对应需求 FR-TERM-006。
12
+ #
13
+ # LLD §10.1 可观测性:
14
+ # 路径模式 logs/<sess_id>/<ts>.log
15
+ # 格式 纯文本带时间戳;可选 HTML
16
+ # 轮转 单文件 100MB / 保留 10
17
+ #
18
+ # DoD(LLD §11.2):自动轮转;HTML 格式带颜色还原
19
+ #
20
+ # 集成点:Emulator#feed 中 @logger&.write(data)
21
+ class Logger
22
+ DEFAULT_MAX_SIZE = 100 * 1024 * 1024 # 100MB
23
+ DEFAULT_ROTATE = 10 # 保留 10 个文件
24
+ TIMESTAMP_FORMAT = "%Y%m%d_%H%M%S"
25
+
26
+ attr_reader :path, :max_size, :rotate, :format, :current_size
27
+
28
+ # @param path [String] 日志文件路径(不含扩展名,自动加 .log 或 .html)
29
+ # @param max_size [Integer] 单文件最大字节数
30
+ # @param rotate [Integer] 保留轮转文件数
31
+ # @param format [Symbol] :text 或 :html
32
+ def initialize(path, max_size: DEFAULT_MAX_SIZE, rotate: DEFAULT_ROTATE, format: :text)
33
+ @base_path = path
34
+ @max_size = max_size
35
+ @rotate = rotate
36
+ @format = format
37
+ @current_size = 0
38
+ @write_mutex = Mutex.new
39
+ @io = nil
40
+ @html_header_written = false
41
+ @seq = 0
42
+ open_new_file
43
+ end
44
+
45
+ # 写入原始终端数据。
46
+ # 由 Emulator#feed 调用,每次收到 SSH 通道数据即写入。
47
+ # @param data [String] 原始字节
48
+ def write(data)
49
+ @write_mutex.synchronize do
50
+ return unless @io
51
+
52
+ if @format == :html
53
+ write_html(data)
54
+ else
55
+ write_text(data)
56
+ end
57
+
58
+ @current_size += data.bytesize
59
+ rotate_if_needed
60
+ end
61
+ end
62
+ alias << write
63
+
64
+ # 关闭当前日志文件
65
+ def close
66
+ @write_mutex.synchronize do
67
+ if @format == :html && @io
68
+ @io.write("</pre>\n</body>\n</html>\n")
69
+ end
70
+ @io&.close
71
+ @io = nil
72
+ end
73
+ end
74
+
75
+ # 切换格式(需重新打开文件)
76
+ def format=(fmt)
77
+ close
78
+ @format = fmt
79
+ @html_header_written = false
80
+ open_new_file
81
+ end
82
+
83
+ private
84
+
85
+ # 打开新的日志文件
86
+ def open_new_file
87
+ dir = File.dirname(@base_path)
88
+ FileUtils.mkdir_p(dir)
89
+
90
+ ext = @format == :html ? ".html" : ".log"
91
+ timestamp = Time.now.strftime(TIMESTAMP_FORMAT)
92
+ @seq += 1
93
+ @path = "#{@base_path}_#{timestamp}_#{@seq}#{ext}"
94
+
95
+ @io = File.open(@path, "wb")
96
+ @io.sync = true
97
+ @current_size = 0
98
+
99
+ if @format == :html
100
+ write_html_header
101
+ else
102
+ # 纯文本头部:时间戳
103
+ @io.write("# Session log started at #{Time.now.iso8601}\n")
104
+ @current_size += 30
105
+ end
106
+ end
107
+
108
+ # 纯文本写入:带时间戳前缀
109
+ def write_text(data)
110
+ # 每行加时间戳前缀
111
+ lines = data.split("\n", -1)
112
+ lines.each_with_index do |line, i|
113
+ next if line.empty? && i == lines.size - 1
114
+
115
+ ts = Time.now.strftime("[%H:%M:%S] ")
116
+ @io.write("#{ts}#{line}\n")
117
+ end
118
+ end
119
+
120
+ # HTML 写入:保留 ANSI 颜色信息,转义 HTML 实体
121
+ def write_html_header
122
+ @io.write(<<~HTML)
123
+ <!DOCTYPE html>
124
+ <html>
125
+ <head>
126
+ <meta charset="utf-8">
127
+ <title>SSH Session Log</title>
128
+ <style>
129
+ body { background: #1e1e1e; color: #d4d4d4; font-family: monospace; }
130
+ pre { white-space: pre-wrap; word-wrap: break-word; }
131
+ </style>
132
+ </head>
133
+ <body>
134
+ <pre>
135
+ HTML
136
+ @html_header_written = true
137
+ end
138
+
139
+ # HTML 写入:ANSI 转义转为 span 标签
140
+ def write_html(data)
141
+ # 转义 HTML 实体
142
+ escaped = data.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
143
+
144
+ # ANSI SGR 转义序列 → <span style="color:...">
145
+ # 这里的实现是骨架,完整 ANSI→HTML 转换在具体实现阶段完善
146
+ # 覆盖常见 SGR:重置 \e[0m、前景色 \e[3Xm、加粗 \e[1m
147
+ escaped.gsub!(/\e\[0m/, "</span>")
148
+ escaped.gsub!(/\e\[1m/, '<span style="font-weight:bold">')
149
+ escaped.gsub!(/\e\[3(\d)m/) do
150
+ idx = Regexp.last_match(1).to_i
151
+ color = ansi_color_to_css(idx)
152
+ %(<span style="color:#{color}">)
153
+ end
154
+
155
+ @io.write(escaped)
156
+ end
157
+
158
+ # ANSI 8 色映射到 CSS 颜色
159
+ def ansi_color_to_css(idx)
160
+ %w[#000000 #cc0000 #00aa00 #aa5500 #0033cc #aa00aa #00aaaa #aaaaaa
161
+ #555555 #ff5555 #55ff55 #ffff55 #5555ff #ff55ff #55ffff #ffffff][idx] || "#ffffff"
162
+ end
163
+
164
+ # 检查是否需要轮转
165
+ def rotate_if_needed
166
+ return if @current_size < @max_size
167
+
168
+ @io.close
169
+ if @format == :html
170
+ # 需要给当前文件写尾部
171
+ File.open(@path, "a") { |f| f.write("</pre>\n</body>\n</html>\n") }
172
+ end
173
+
174
+ # 清理超出 rotate 数量的旧文件
175
+ cleanup_old_files
176
+ open_new_file
177
+ end
178
+
179
+ # 清理超出保留数量的旧日志文件
180
+ def cleanup_old_files
181
+ dir = File.dirname(@base_path)
182
+ base = File.basename(@base_path)
183
+ ext = @format == :html ? ".html" : ".log"
184
+
185
+ pattern = File.join(dir, "#{base}_*#{ext}")
186
+ files = Dir.glob(pattern).sort_by { |f| File.mtime(f) }
187
+ excess = files.size - @rotate
188
+ return if excess <= 0
189
+
190
+ files.first(excess).each { |f| File.delete(f) }
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NetworkInfraUtility
4
+ module SSH
5
+ module Terminal
6
+ # 屏幕状态:行数组 + 光标 + 滚动区 + alt buffer + 字符属性。
7
+ # 由 AnsiParser 经 Emulator 回调驱动。
8
+ class Screen
9
+ attr_reader :cols, :rows, :cursor_x, :cursor_y
10
+ attr_reader :scroll_top, :scroll_bottom
11
+ attr_reader :alt_mode
12
+
13
+ def initialize(cols, rows)
14
+ @cols = cols
15
+ @rows = rows
16
+ @cursor_x = 0
17
+ @cursor_y = 0
18
+ @scroll_top = 0
19
+ @scroll_bottom = rows - 1
20
+ @alt_mode = false
21
+ @main_lines = Array.new(rows) { Line.new(@cols) }
22
+ @alt_lines = Array.new(rows) { Line.new(@cols) }
23
+ @current_style = {}
24
+ end
25
+
26
+ def lines
27
+ @alt_mode ? @alt_lines : @main_lines
28
+ end
29
+
30
+ def put_char(ch, x, y, style = {})
31
+ line = lines[y]
32
+ return unless line
33
+
34
+ line.put_char(ch, x, style.empty? ? @current_style : style)
35
+ @cursor_x = x + 1
36
+ @cursor_y = y
37
+ end
38
+
39
+ def cursor_to(x, y)
40
+ @cursor_x = [[x, 0].max, @cols - 1].min
41
+ @cursor_y = [[y, 0].max, @rows - 1].min
42
+ end
43
+
44
+ def newline
45
+ if @cursor_y >= @scroll_bottom
46
+ scroll_up(1)
47
+ else
48
+ @cursor_y += 1
49
+ end
50
+ @cursor_x = 0
51
+ end
52
+
53
+ def backspace
54
+ @cursor_x = [@cursor_x - 1, 0].max
55
+ end
56
+
57
+ def tab
58
+ @cursor_x = ((@cursor_x / 8) + 1) * 8
59
+ @cursor_x = [@cursor_x, @cols - 1].min
60
+ end
61
+
62
+ def clear(mode = :full)
63
+ case mode
64
+ when :full
65
+ @main_lines.each { |l| l.clear(@cols) }
66
+ @alt_lines.each { |l| l.clear(@cols) }
67
+ @cursor_x = 0
68
+ @cursor_y = 0
69
+ when :above
70
+ clear_above_cursor
71
+ when :below
72
+ clear_below_cursor
73
+ end
74
+ end
75
+
76
+ def clear_line(mode = :full)
77
+ line = lines[@cursor_y]
78
+ return unless line
79
+
80
+ case mode
81
+ when :full then line.clear(@cols)
82
+ when :right then line.clear_from(@cursor_x, @cols)
83
+ when :left then line.clear_range(0, @cursor_x)
84
+ end
85
+ end
86
+
87
+ def scroll_up(n = 1)
88
+ n.times do
89
+ # 将滚动区首行移入回滚(由 Buffer 处理),后续行上移
90
+ @scroll_top.upto(@scroll_bottom - 1) do |i|
91
+ lines[i] = lines[i + 1]
92
+ end
93
+ lines[@scroll_bottom] = Line.new(@cols)
94
+ end
95
+ end
96
+
97
+ def scroll_down(n = 1)
98
+ n.times do
99
+ @scroll_bottom.downto(@scroll_top + 1) do |i|
100
+ lines[i] = lines[i - 1]
101
+ end
102
+ lines[@scroll_top] = Line.new(@cols)
103
+ end
104
+ end
105
+
106
+ def set_scroll_region(top, bottom)
107
+ @scroll_top = [[top, 0].max, @rows - 1].min
108
+ @scroll_bottom = [[bottom, 0].max, @rows - 1].min
109
+ @cursor_x = 0
110
+ @cursor_y = @scroll_top
111
+ end
112
+
113
+ def enter_alt
114
+ @alt_mode = true
115
+ @cursor_x = 0
116
+ @cursor_y = 0
117
+ end
118
+
119
+ def exit_alt
120
+ @alt_mode = false
121
+ @cursor_x = 0
122
+ @cursor_y = 0
123
+ end
124
+
125
+ def set_style(style)
126
+ @current_style = @current_style.merge(style)
127
+ end
128
+
129
+ def reset_style
130
+ @current_style = {}
131
+ end
132
+
133
+ def resize(cols, rows)
134
+ old_cols = @cols
135
+ @cols = cols
136
+ [@main_lines, @alt_lines].each do |ary|
137
+ if rows > ary.size
138
+ (rows - ary.size).times { ary << Line.new(cols) }
139
+ elsif rows < ary.size
140
+ ary.slice!(rows..)
141
+ end
142
+ ary.each { |l| l.resize(cols) } if cols != old_cols
143
+ end
144
+ @rows = rows
145
+ @scroll_bottom = rows - 1
146
+ @cursor_x = [@cursor_x, cols - 1].min
147
+ @cursor_y = [@cursor_y, rows - 1].min
148
+ end
149
+
150
+ def to_text
151
+ lines.map(&:to_s).join("\n")
152
+ end
153
+
154
+ private
155
+
156
+ def clear_above_cursor
157
+ 0.upto(@cursor_y - 1) { |i| lines[i]&.clear(@cols) }
158
+ lines[@cursor_y]&.clear_range(0, @cursor_x)
159
+ end
160
+
161
+ def clear_below_cursor
162
+ lines[@cursor_y]&.clear_from(@cursor_x, @cols)
163
+ (@cursor_y + 1).upto(@rows - 1) { |i| lines[i]&.clear(@cols) }
164
+ end
165
+
166
+ # 一行字符
167
+ class Line
168
+ attr_reader :cells
169
+
170
+ def initialize(cols)
171
+ @cells = Array.new(cols) { Cell.new }
172
+ end
173
+
174
+ def put_char(ch, x, style = {})
175
+ @cells[x] = Cell.new(ch, style) if x < @cells.size
176
+ end
177
+
178
+ def clear(cols)
179
+ @cells = Array.new(cols) { Cell.new }
180
+ end
181
+
182
+ def clear_from(x, cols)
183
+ x.upto([cols - 1, @cells.size - 1].min) { |i| @cells[i] = Cell.new }
184
+ end
185
+
186
+ def clear_range(from, to)
187
+ from.upto([to, @cells.size - 1].min) { |i| @cells[i] = Cell.new }
188
+ end
189
+
190
+ def resize(cols)
191
+ if cols > @cells.size
192
+ @cells += Array.new(cols - @cells.size) { Cell.new }
193
+ elsif cols < @cells.size
194
+ @cells = @cells[0...cols]
195
+ end
196
+ end
197
+
198
+ def to_s
199
+ @cells.map(&:char).join.rstrip
200
+ end
201
+ end
202
+
203
+ # 单字符单元
204
+ Cell = Struct.new(:char, :style) do
205
+ def initialize(char = " ", style = {})
206
+ super
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module NetworkInfraUtility
6
+ module SSH
7
+ module Terminal
8
+ # 配色方案加载/查询。
9
+ # 对应需求 FR-TERM-002:内置 ≥ 10 套配色,支持导入 iTerm2/VSCode。
10
+ class Theme
11
+ BUILTIN_THEMES = {
12
+ "default" => {
13
+ bg: "#1e1e1e", fg: "#d4d4d4", cursor: "#d4d4d4",
14
+ palette: %w[#000000 #cd3131 #0dbc79 #e5e510 #2472c8 #bc3fbc #11a8cd #e5e5e5
15
+ #666666 #f14c4c #23d18b #f5f543 #3b8eea #d670d6 #29b8db #e5e5e5]
16
+ },
17
+ "solarized-dark" => {
18
+ bg: "#002b36", fg: "#839496", cursor: "#93a1a1",
19
+ palette: %w[#073642 #dc322f #859900 #b58900 #268bd2 #d33682 #2aa198 #eee8d5
20
+ #002b36 #cb4b16 #586e75 #657b83 #839496 #6c71c4 #93a1a1 #fdf6e3]
21
+ },
22
+ "solarized-light" => {
23
+ bg: "#fdf6e3", fg: "#657b83", cursor: "#586e75",
24
+ palette: %w[#073642 #dc322f #859900 #b58900 #268bd2 #d33682 #2aa198 #eee8d5
25
+ #002b36 #cb4b16 #586e75 #657b83 #839496 #6c71c4 #93a1a1 #fdf6e3]
26
+ },
27
+ "dracula" => {
28
+ bg: "#282a36", fg: "#f8f8f2", cursor: "#f8f8f2",
29
+ palette: %w[#000000 #ff5555 #50fa7b #f1fa8c #bd93f9 #ff79c6 #8be9fd #bfbfbf
30
+ #4d4d4d #ff6e67 #5af78e #f4f99d #caa9fa #ff92d0 #9aedfe #e6e6e6]
31
+ },
32
+ "monokai" => {
33
+ bg: "#272822", fg: "#f8f8f2", cursor: "#f8f8f0",
34
+ palette: %w[#272822 #f92672 #a6e22e #fd971f #66d9ef #ae81ff #a1efe4 #f8f8f2
35
+ #75715e #f92672 #a6e22e #fd971f #66d9ef #ae81ff #a1efe4 #f9f8f5]
36
+ },
37
+ "nord" => {
38
+ bg: "#2e3440", fg: "#d8dee9", cursor: "#d8dee9",
39
+ palette: %w[#3b4252 #bf616a #a3be8c #ebcb8b #81a1c1 #b48ead #88c0d0 #e5e9f0
40
+ #4c566a #bf616a #a3be8c #ebcb8b #81a1c1 #b48ead #8fbcbb #eceff4]
41
+ },
42
+ "gruvbox-dark" => {
43
+ bg: "#282828", fg: "#ebdbb2", cursor: "#ebdbb2",
44
+ palette: %w[#282828 #cc241d #98971a #d79921 #458588 #b16286 #689d6a #a89984
45
+ #928374 #fb4934 #b8bb26 #fabd2f #83a598 #d3869b #8ec07c #ebdbb2]
46
+ },
47
+ "one-dark" => {
48
+ bg: "#282c34", fg: "#abb2bf", cursor: "#abb2bf",
49
+ palette: %w[#282c34 #e06c75 #98c379 #e5c07b #61afef #c678dd #56b6c2 #abb2bf
50
+ #5c6370 #e06c75 #98c379 #e5c07b #61afef #c678dd #56b6c2 #ffffff]
51
+ },
52
+ "tokyo-night" => {
53
+ bg: "#1a1b26", fg: "#a9b1d6", cursor: "#c0caf5",
54
+ palette: %w[#15161e #f7768e #9ece6a #e0af68 #7aa2f7 #bb9af7 #7dcfff #a9b1d6
55
+ #414868 #f7768e #9ece6a #e0af68 #7aa2f7 #bb9af7 #7dcfff #c0caf5]
56
+ },
57
+ "catppuccin-mocha" => {
58
+ bg: "#1e1e2e", fg: "#cdd6f4", cursor: "#f5e0dc",
59
+ palette: %w[#45475a #f38ba8 #a6e3a1 #f9e2af #89b4fa #f5c2e7 #94e2d5 #bac2de
60
+ #585b70 #f38ba8 #a6e3a1 #f9e2af #89b4fa #f5c2e7 #94e2d5 #a6adc8]
61
+ },
62
+ "github-dark" => {
63
+ bg: "#0d1117", fg: "#c9d1d9", cursor: "#6cae50",
64
+ palette: %w[#484f58 #ff7b72 #7ee787 #f2cc60 #79c0ff #d2a8ff #56d4dd #c9d1d9
65
+ #6e7681 #ffa198 #56d364 #e3b341 #79c0ff #bc8cff #56d4dd #f0f6fc]
66
+ }
67
+ }.freeze
68
+
69
+ attr_reader :name, :data
70
+
71
+ class << self
72
+ def load(name)
73
+ data = BUILTIN_THEMES[name] || load_custom(name) || BUILTIN_THEMES["default"]
74
+ new(name, data)
75
+ end
76
+
77
+ def default
78
+ load("default")
79
+ end
80
+
81
+ def builtin_names
82
+ BUILTIN_THEMES.keys
83
+ end
84
+
85
+ # 从 iTerm2 .itermcolors 文件导入
86
+ def import_iterm(path)
87
+ # V2.0 功能,预留接口
88
+ raise NotImplementedError, "iTerm2 import is V2.0"
89
+ end
90
+
91
+ # 从 VSCode .json 主题文件导入
92
+ def import_vscode(path)
93
+ # V2.0 功能,预留接口
94
+ raise NotImplementedError, "VSCode import is V2.0"
95
+ end
96
+
97
+ private
98
+
99
+ def load_custom(name)
100
+ # 从 ~/.network-infra-utility/themes/<name>.yml 加载
101
+ themes_dir = File.expand_path("~/.network-infra-utility/themes")
102
+ path = File.join(themes_dir, "#{name}.yml")
103
+ return nil unless File.exist?(path)
104
+
105
+ YAML.safe_load(File.read(path), symbolize_names: true)
106
+ end
107
+ end
108
+
109
+ def initialize(name, data)
110
+ @name = name
111
+ @data = data.transform_keys(&:to_sym)
112
+ end
113
+
114
+ def bg; @data[:bg] || "#000000"; end
115
+ def fg; @data[:fg] || "#ffffff"; end
116
+ def cursor; @data[:cursor] || fg; end
117
+ def palette; @data[:palette] || []; end
118
+
119
+ def color(index)
120
+ return fg if index.nil? || index < 0 || index >= palette.size
121
+
122
+ palette[index]
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NetworkInfraUtility
4
+ module SSH
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ssh/version"
4
+ require_relative "ssh/client"
5
+ require_relative "ssh/ipc/transport"
6
+ require_relative "ssh/ipc/router"
7
+ require_relative "ssh/ipc/coalesce"
8
+ require_relative "ssh/ipc/errors"
9
+ require_relative "ssh/session/manager"
10
+ require_relative "ssh/session/session"
11
+ require_relative "ssh/session/tree"
12
+ require_relative "ssh/session/history"
13
+ require_relative "ssh/terminal/emulator"
14
+ require_relative "ssh/terminal/ansi_parser"
15
+ require_relative "ssh/terminal/screen"
16
+ require_relative "ssh/terminal/buffer"
17
+ require_relative "ssh/terminal/theme"
18
+ require_relative "ssh/terminal/logger"
19
+ require_relative "ssh/security/vault"
20
+ require_relative "ssh/security/host_key"
21
+ require_relative "ssh/automation/macro_engine"
22
+ require_relative "ssh/config/settings"
23
+ require_relative "ssh/config/schema"
24
+ require_relative "ssh/config/store"
25
+
26
+ # NetworkInfraUtility::SSH — SSH 连接客户端
27
+ #
28
+ # 架构(LLD §1):
29
+ # Erlang/OTP ssh 做协议核心,Ruby 做调度与终端,
30
+ # 两者经 JSON-RPC 2.0 over Unix Socket/TCP 通信。
31
+ #
32
+ # 组合根:SSH::Client——构造时编排全部子系统,
33
+ # 管理 Erlang 引擎生命周期与 IPC 连接。
34
+ #
35
+ # 用法:
36
+ # client = NetworkInfraUtility::SSH::Client.new
37
+ # client.start_engine
38
+ # session = client.connect(host: "10.0.0.1", user: "admin")
39
+ # terminal = session.open_terminal
40
+ # terminal.puts("show version")
41
+ module NetworkInfraUtility
42
+ module SSH
43
+ end
44
+ end