hiiro 0.1.379 → 0.1.381
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/CHANGELOG.md +5 -0
- data/bin/h-docs +159 -299
- data/lib/hiiro/options.rb +6 -0
- data/lib/hiiro/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9daba8055f86a4a6e65540732e1cc8f1eaf0a12e14bc3c8839d71ba4a72eed8c
|
|
4
|
+
data.tar.gz: fa3400a16b8350b4c86ae8f56111068bfbeb95fcf5f644ebf8cef7b562173377
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f928f3c7969fb291e966440aed5b37697b78214cd6d3321f8cec387f587a8436616e7cb325e19cf2cb2f770ef4c1bf1c5562ef8963d968f182cf69fac9c879ae
|
|
7
|
+
data.tar.gz: 387be7b9a9050dcd5b9ad76ebf1487d92d7b1af79c47d7bd282792b726f80c7f3c58d6a170d15e83b47503ae8e5336b514b183fca32c52005cad2b606055433a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.381] - 2026-09-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Hiiro::Options` now includes file/directory filtering methods: `files`, `dirs`, `file_or_dirs`, `not_files`, and `not_file_or_dirs` for convenient argument classification
|
|
7
|
+
|
|
3
8
|
## [0.1.377] - 2026-09-17
|
|
4
9
|
|
|
5
10
|
### Changed
|
data/bin/h-docs
CHANGED
|
@@ -1,385 +1,245 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
3
|
require 'hiiro'
|
|
4
|
-
begin
|
|
5
|
-
require 'hiiro/tui'
|
|
6
|
-
rescue LoadError
|
|
7
|
-
require File.expand_path('~/proj/hiiro/lib/hiiro/tui')
|
|
8
|
-
end
|
|
9
4
|
require 'json'
|
|
10
|
-
require '
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'uri'
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
SERVER = ENV.fetch('HDOCS_SERVER', 'http://127.0.0.1:4387').sub(%r{/$}, '')
|
|
9
|
+
DOCS_DIR = File.join(Dir.home, 'claude', 'docs')
|
|
10
|
+
PROJECT_ROOT = ENV.fetch('PORTFOLIO_HOME', File.join(Dir.home, 'proj', 'portfolio'))
|
|
11
|
+
SERVICE_LABEL = "com.#{ENV.fetch('USER')}.portfolio"
|
|
12
|
+
SERVICE_DOMAIN = "gui/#{Process.uid}"
|
|
13
|
+
SERVICE_TARGET = "#{SERVICE_DOMAIN}/#{SERVICE_LABEL}"
|
|
14
|
+
SERVICE_PLIST = File.join(Dir.home, 'Library', 'LaunchAgents', "#{SERVICE_LABEL}.plist")
|
|
16
15
|
|
|
17
16
|
ls_opts = Hiiro::Options.setup {
|
|
18
|
-
flag(:html,
|
|
19
|
-
flag(:md,
|
|
20
|
-
flag(:full,
|
|
17
|
+
flag(:html, short: :h, default: false, desc: 'show HTML-backed documents')
|
|
18
|
+
flag(:md, short: :m, default: false, desc: 'show Markdown-backed documents')
|
|
19
|
+
flag(:full, short: :f, default: false, desc: 'show API URLs and source paths')
|
|
21
20
|
option(:limit, short: :l, default: nil, desc: 'limit number of results')
|
|
21
|
+
flag(:files, default: false, desc: 'with rm, also delete the tracked source file')
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
open_opts = Hiiro::Options.setup {
|
|
25
|
-
flag(:html, short: :h, default: false, desc: 'consider
|
|
26
|
-
flag(:md,
|
|
27
|
-
flag(:all,
|
|
25
|
+
flag(:html, short: :h, default: false, desc: 'consider HTML-backed documents')
|
|
26
|
+
flag(:md, short: :m, default: false, desc: 'consider Markdown-backed documents')
|
|
27
|
+
flag(:all, short: :a, default: false, desc: 'open all matches')
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
patterns << '*.md' if opts.md
|
|
34
|
-
patterns.empty? ? ['*.html', '*.md'] : patterns
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def build_regex(filters)
|
|
38
|
-
filters = ['.*'] if filters.empty?
|
|
39
|
-
Regexp.new(filters.join(?|), Regexp::IGNORECASE)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def find_docs(opts)
|
|
43
|
-
re = build_regex(opts.args)
|
|
44
|
-
file_patterns(opts).flat_map { |pat| Dir[File.join(DOCS_DIR, '**', pat)] }
|
|
45
|
-
.select { |path| File.basename(path).match?(re) }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def display(path, opts)
|
|
49
|
-
opts.respond_to?(:full) && opts.full ? path : File.basename(path)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def pick_one(paths)
|
|
53
|
-
return paths.first if paths.length == 1
|
|
54
|
-
basenames = paths.map { |p| File.basename(p) }
|
|
55
|
-
choice = fuzzyfind(basenames)
|
|
56
|
-
return nil if choice.nil? || choice.strip.empty?
|
|
57
|
-
paths.find { |p| File.basename(p) == choice.strip }
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
class DocsIndex
|
|
61
|
-
Entry = Struct.new(:key, :entry, :md_path, :html_path, keyword_init: true) do
|
|
62
|
-
def in_index?
|
|
63
|
-
!entry.nil?
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def has_md?
|
|
67
|
-
md_path && File.file?(md_path)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def has_html?
|
|
71
|
-
html_path && File.file?(html_path)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def basename
|
|
75
|
-
"#{key}.html"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def title
|
|
79
|
-
entry&.fetch('title', nil).to_s.empty? ? key : entry['title']
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def to_s
|
|
83
|
-
basename
|
|
84
|
-
end
|
|
30
|
+
class PortfolioApi
|
|
31
|
+
def initialize(base_url)
|
|
32
|
+
@base = URI(base_url)
|
|
85
33
|
end
|
|
86
34
|
|
|
87
|
-
|
|
88
|
-
@docs_dir = docs_dir
|
|
89
|
-
@index_json = index_json
|
|
90
|
-
@mdoc = mdoc
|
|
91
|
-
@pending_delete_paths = []
|
|
92
|
-
@changed = false
|
|
93
|
-
load_entries
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
attr_reader :entries
|
|
97
|
-
|
|
98
|
-
def changed?
|
|
99
|
-
@changed
|
|
100
|
-
end
|
|
35
|
+
attr_reader :base
|
|
101
36
|
|
|
102
|
-
def
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
keys.uniq.sort_by { |key| indexed[key] ? [0, indexed[key][1]] : [1, key.downcase] }.map do |key|
|
|
109
|
-
entry = indexed.dig(key, 0)
|
|
110
|
-
Entry.new(
|
|
111
|
-
key: key,
|
|
112
|
-
entry: entry,
|
|
113
|
-
md_path: File.join(@docs_dir, "#{key}.md"),
|
|
114
|
-
html_path: html_path_for(key, entry)
|
|
115
|
-
)
|
|
116
|
-
end
|
|
37
|
+
def items(filters = [], limit: nil)
|
|
38
|
+
query = {}
|
|
39
|
+
query['q'] = filters.join(' ') unless filters.empty?
|
|
40
|
+
query['limit'] = limit.to_i if limit
|
|
41
|
+
get('/api/items', query).fetch('items')
|
|
117
42
|
end
|
|
118
43
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
remove(row)
|
|
122
|
-
"Marked #{row.basename} for index removal and file deletion on save"
|
|
123
|
-
elsif row.has_html?
|
|
124
|
-
add(row)
|
|
125
|
-
"Added #{row.basename} to index"
|
|
126
|
-
else
|
|
127
|
-
"Cannot add #{row.basename}: no html file exists"
|
|
128
|
-
end
|
|
44
|
+
def health
|
|
45
|
+
get('/health')
|
|
129
46
|
end
|
|
130
47
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
load_entries
|
|
137
|
-
@changed = true
|
|
138
|
-
"Regenerated #{row.basename}"
|
|
48
|
+
def delete(id, files: false)
|
|
49
|
+
target = uri("/api/items/#{id}")
|
|
50
|
+
target.query = URI.encode_www_form(delete_files: 1) if files
|
|
51
|
+
request(Net::HTTP::Delete.new(target))
|
|
52
|
+
true
|
|
139
53
|
end
|
|
140
54
|
|
|
141
|
-
def
|
|
142
|
-
return
|
|
55
|
+
def absolute_url(path)
|
|
56
|
+
return path if path.to_s.match?(%r{\Ahttps?://})
|
|
143
57
|
|
|
144
|
-
|
|
145
|
-
File.delete(path) if path && File.file?(path)
|
|
146
|
-
end
|
|
147
|
-
File.write(@index_json, JSON.pretty_generate(entries) + "\n")
|
|
148
|
-
system(@mdoc, '--rebuild-index')
|
|
58
|
+
URI.join("#{base}/", path.to_s.sub(%r{\A/}, '')).to_s
|
|
149
59
|
end
|
|
150
60
|
|
|
151
61
|
private
|
|
152
62
|
|
|
153
|
-
def
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
63
|
+
def get(path, query = {})
|
|
64
|
+
target = uri(path)
|
|
65
|
+
target.query = URI.encode_www_form(query) unless query.empty?
|
|
66
|
+
JSON.parse(request(Net::HTTP::Get.new(target)))
|
|
157
67
|
end
|
|
158
68
|
|
|
159
|
-
def
|
|
160
|
-
|
|
69
|
+
def uri(path)
|
|
70
|
+
URI.join("#{base}/", path.sub(%r{\A/}, ''))
|
|
161
71
|
end
|
|
162
72
|
|
|
163
|
-
def
|
|
164
|
-
|
|
165
|
-
|
|
73
|
+
def request(message)
|
|
74
|
+
response = Net::HTTP.start(message.uri.hostname, message.uri.port, open_timeout: 2, read_timeout: 15) { |http| http.request(message) }
|
|
75
|
+
raise "Portfolio API returned #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
166
76
|
|
|
167
|
-
|
|
77
|
+
response.body
|
|
78
|
+
rescue Errno::ECONNREFUSED, SocketError => error
|
|
79
|
+
raise "Portfolio is unavailable at #{base}: #{error.message}"
|
|
168
80
|
end
|
|
81
|
+
end
|
|
169
82
|
|
|
170
|
-
|
|
171
|
-
"docs/#{File.basename(row.html_path)}"
|
|
172
|
-
end
|
|
83
|
+
API = PortfolioApi.new(SERVER)
|
|
173
84
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
@pending_delete_paths.reject! { |path| path == row.md_path || path == row.html_path }
|
|
177
|
-
entries.reject! { |entry| entry['file'] == rel }
|
|
178
|
-
entries.unshift(
|
|
179
|
-
'title' => title_for(row),
|
|
180
|
-
'file' => rel,
|
|
181
|
-
'date' => File.mtime(row.html_path).strftime('%Y-%m-%d'),
|
|
182
|
-
'desc' => '',
|
|
183
|
-
'ts' => Time.now.iso8601
|
|
184
|
-
)
|
|
185
|
-
row.entry = entries.first
|
|
186
|
-
@changed = true
|
|
187
|
-
end
|
|
85
|
+
def source_kind?(item, opts)
|
|
86
|
+
return true unless opts.html || opts.md
|
|
188
87
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
@pending_delete_paths << row.md_path
|
|
193
|
-
@pending_delete_paths << row.html_path
|
|
194
|
-
row.entry = nil
|
|
195
|
-
@changed = true
|
|
196
|
-
end
|
|
88
|
+
path = item['source_path'].to_s
|
|
89
|
+
(opts.html && path.end_with?('.html')) || (opts.md && path.end_with?('.md'))
|
|
90
|
+
end
|
|
197
91
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
92
|
+
def matching_items(opts, limit: nil)
|
|
93
|
+
API.items(opts.args, limit: limit).select { |item| source_kind?(item, opts) }
|
|
94
|
+
end
|
|
201
95
|
|
|
202
|
-
|
|
203
|
-
|
|
96
|
+
def item_label(item)
|
|
97
|
+
"#{item['title']} [#{item['type']} ##{item['id']}]"
|
|
98
|
+
end
|
|
204
99
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
next unless line.start_with?('# ')
|
|
100
|
+
def pick_one(items)
|
|
101
|
+
return items.first if items.length == 1
|
|
208
102
|
|
|
209
|
-
|
|
210
|
-
|
|
103
|
+
labels = items.map { |item| item_label(item) }
|
|
104
|
+
choice = fuzzyfind(labels)
|
|
105
|
+
return nil if choice.nil? || choice.strip.empty?
|
|
211
106
|
|
|
212
|
-
|
|
213
|
-
end
|
|
107
|
+
items.find { |item| item_label(item) == choice.strip }
|
|
214
108
|
end
|
|
215
109
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
@message = 'space toggle index/files r regenerate missing html enter save/rebuild q cancel'
|
|
220
|
-
super(items: index.rows, empty_message: 'No docs found.')
|
|
221
|
-
end
|
|
110
|
+
def open_item(item)
|
|
111
|
+
system('open', API.absolute_url(item['href']))
|
|
112
|
+
end
|
|
222
113
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
@message = @index.toggle(items[cursor])
|
|
227
|
-
refresh
|
|
228
|
-
when 'r'
|
|
229
|
-
@message = @index.regenerate(items[cursor])
|
|
230
|
-
refresh
|
|
231
|
-
when :enter
|
|
232
|
-
return @index.save
|
|
233
|
-
else
|
|
234
|
-
super
|
|
235
|
-
end
|
|
236
|
-
end
|
|
114
|
+
def service_loaded?
|
|
115
|
+
system('launchctl', 'print', SERVICE_TARGET, out: File::NULL, err: File::NULL)
|
|
116
|
+
end
|
|
237
117
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
]
|
|
118
|
+
def start_service
|
|
119
|
+
if service_loaded?
|
|
120
|
+
system('launchctl', 'kickstart', SERVICE_TARGET)
|
|
121
|
+
else
|
|
122
|
+
system('launchctl', 'bootstrap', SERVICE_DOMAIN, SERVICE_PLIST)
|
|
244
123
|
end
|
|
124
|
+
end
|
|
245
125
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
end
|
|
126
|
+
def stop_service
|
|
127
|
+
return puts('Portfolio is not loaded.') unless service_loaded?
|
|
249
128
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
indicators = [
|
|
253
|
-
row.in_index? ? 'I' : '-',
|
|
254
|
-
row.has_md? ? 'M' : '-',
|
|
255
|
-
row.has_html? ? 'H' : '-',
|
|
256
|
-
].join
|
|
257
|
-
fixed = "#{prefix}#{indicators} "
|
|
258
|
-
text_cols = [line_cols - fixed.length, 1].max
|
|
259
|
-
text = (fixed + visible_text(row.basename, text_cols, horizontal_offset)).ljust(line_cols)
|
|
260
|
-
style =
|
|
261
|
-
if current
|
|
262
|
-
"\e[7m"
|
|
263
|
-
elsif row.in_index? && row.has_md? && !row.has_html?
|
|
264
|
-
"\e[33m"
|
|
265
|
-
else
|
|
266
|
-
"\e[0m"
|
|
267
|
-
end
|
|
129
|
+
system('launchctl', 'bootout', SERVICE_TARGET)
|
|
130
|
+
end
|
|
268
131
|
|
|
269
|
-
|
|
132
|
+
def restart_service
|
|
133
|
+
if service_loaded?
|
|
134
|
+
system('launchctl', 'kickstart', '-k', SERVICE_TARGET)
|
|
135
|
+
else
|
|
136
|
+
start_service
|
|
270
137
|
end
|
|
138
|
+
end
|
|
271
139
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
140
|
+
Hiiro.run(*ARGV, plugins: [Pins]) {
|
|
141
|
+
add_subcmd(:setup, :install) { |*_args|
|
|
142
|
+
installer = File.join(PROJECT_ROOT, 'bin', 'install')
|
|
143
|
+
abort "Installer not found: #{installer}\nSet PORTFOLIO_HOME to the project directory." unless File.executable?(installer)
|
|
144
|
+
exit($?.exitstatus || 1) unless system(installer)
|
|
145
|
+
}
|
|
276
146
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
147
|
+
add_subcmd(:start) { |*_args|
|
|
148
|
+
abort "LaunchAgent not installed. Run: h-docs setup" unless File.file?(SERVICE_PLIST)
|
|
149
|
+
exit($?.exitstatus || 1) unless start_service
|
|
150
|
+
puts 'Portfolio started.'
|
|
151
|
+
}
|
|
281
152
|
|
|
282
|
-
|
|
283
|
-
|
|
153
|
+
add_subcmd(:stop) { |*_args|
|
|
154
|
+
stop_service
|
|
155
|
+
}
|
|
284
156
|
|
|
285
|
-
|
|
157
|
+
add_subcmd(:restart) { |*_args|
|
|
158
|
+
abort "LaunchAgent not installed. Run: h-docs setup" unless File.file?(SERVICE_PLIST)
|
|
159
|
+
exit($?.exitstatus || 1) unless restart_service
|
|
160
|
+
puts 'Portfolio restarted.'
|
|
161
|
+
}
|
|
286
162
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
163
|
+
add_subcmd(:status) { |*_args|
|
|
164
|
+
if service_loaded?
|
|
165
|
+
system('launchctl', 'print', SERVICE_TARGET)
|
|
166
|
+
else
|
|
167
|
+
warn 'Portfolio is not loaded. Run: h-docs start'
|
|
168
|
+
exit 1
|
|
293
169
|
end
|
|
170
|
+
}
|
|
294
171
|
|
|
295
|
-
@cursor = [[cursor, 0].max, items.length - 1].min
|
|
296
|
-
@top = [[top, 0].max, @cursor].min
|
|
297
|
-
:continue
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
Hiiro.run(*ARGV, plugins: [Pins]) {
|
|
302
172
|
add_subcmd(:ls) { |*args|
|
|
303
|
-
opts
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
173
|
+
opts = ls_opts.parse(args)
|
|
174
|
+
matching_items(opts, limit: opts.limit).each do |item|
|
|
175
|
+
if opts.full
|
|
176
|
+
puts [API.absolute_url(item['href']), item['source_path']].compact.join("\t")
|
|
177
|
+
else
|
|
178
|
+
puts item_label(item)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
307
181
|
}
|
|
308
182
|
|
|
309
183
|
add_subcmd(:recent) { |*args|
|
|
310
|
-
opts
|
|
311
|
-
numeric = opts.args.find { |
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
paths = file_patterns(opts).flat_map { |pat| Dir[File.join(DOCS_DIR, '**', pat)] }
|
|
317
|
-
.select { |p| File.basename(p).match?(re) }
|
|
318
|
-
.sort_by { |p| -File.mtime(p).to_i }
|
|
319
|
-
.first(n)
|
|
320
|
-
|
|
321
|
-
paths.each { |p| puts display(p, opts) }
|
|
184
|
+
opts = ls_opts.parse(args)
|
|
185
|
+
numeric = opts.args.find { |arg| arg.match?(/^\d+$/) }
|
|
186
|
+
count = (opts.limit || numeric || 10).to_i
|
|
187
|
+
opts.args.delete(numeric) if numeric
|
|
188
|
+
matching_items(opts, limit: count).each { |item| puts item_label(item) }
|
|
322
189
|
}
|
|
323
190
|
|
|
324
191
|
add_subcmd(:open) { |*args|
|
|
325
|
-
opts
|
|
326
|
-
|
|
327
|
-
|
|
192
|
+
opts = open_opts.parse(args)
|
|
193
|
+
if opts.args.empty? && !opts.html && !opts.md && !opts.all
|
|
194
|
+
next system('open', SERVER)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
items = matching_items(opts)
|
|
198
|
+
next puts('No matching items.') if items.empty?
|
|
328
199
|
|
|
329
|
-
|
|
330
|
-
targets.each { |t| system('open', t) }
|
|
200
|
+
(opts.all ? items : [pick_one(items)].compact).each { |item| open_item(item) }
|
|
331
201
|
}
|
|
332
202
|
|
|
333
203
|
add_subcmd(:vim) { |*args|
|
|
334
|
-
opts
|
|
335
|
-
paths =
|
|
336
|
-
next puts('No matching
|
|
204
|
+
opts = ls_opts.parse(args)
|
|
205
|
+
paths = matching_items(opts).filter_map { |item| item['source_path'] if File.file?(item['source_path'].to_s) }
|
|
206
|
+
next puts('No matching local sources.') if paths.empty?
|
|
207
|
+
|
|
337
208
|
edit_files(*paths)
|
|
338
209
|
}
|
|
339
210
|
|
|
340
211
|
add_subcmd(:cat) { |*args|
|
|
341
|
-
opts
|
|
342
|
-
paths =
|
|
343
|
-
next puts('No matching
|
|
344
|
-
|
|
345
|
-
paths.each
|
|
346
|
-
puts "===== #{File.basename(p)} ====="
|
|
347
|
-
puts File.read(p)
|
|
348
|
-
puts
|
|
349
|
-
end
|
|
212
|
+
opts = ls_opts.parse(args)
|
|
213
|
+
paths = matching_items(opts).filter_map { |item| item['source_path'] if File.file?(item['source_path'].to_s) }
|
|
214
|
+
next puts('No matching local sources.') if paths.empty?
|
|
215
|
+
|
|
216
|
+
paths.each { |path| puts "===== #{File.basename(path)} =====", File.read(path), '' }
|
|
350
217
|
}
|
|
351
218
|
|
|
352
219
|
add_subcmd(:rm) { |*args|
|
|
353
|
-
opts
|
|
354
|
-
|
|
355
|
-
next puts('No matching
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
next unless
|
|
359
|
-
|
|
360
|
-
|
|
220
|
+
opts = ls_opts.parse(args)
|
|
221
|
+
items = matching_items(opts)
|
|
222
|
+
next puts('No matching items.') if items.empty?
|
|
223
|
+
|
|
224
|
+
item = pick_one(items)
|
|
225
|
+
next unless item
|
|
226
|
+
|
|
227
|
+
API.delete(item['id'], files: opts.files)
|
|
228
|
+
suffix = opts.files ? ' and deleted tracked source file' : ''
|
|
229
|
+
puts "Removed from Portfolio#{suffix}: #{item_label(item)}"
|
|
361
230
|
}
|
|
362
231
|
|
|
363
232
|
add_subcmd(:count) { |*_args|
|
|
364
|
-
|
|
365
|
-
md = Dir[File.join(DOCS_DIR, '**', '*.md')].count
|
|
366
|
-
puts "html: #{html}"
|
|
367
|
-
puts "md: #{md}"
|
|
368
|
-
puts "total: #{html + md}"
|
|
233
|
+
puts "items: #{API.health.fetch('items')}"
|
|
369
234
|
}
|
|
370
235
|
|
|
371
|
-
add_subcmd(:index) { |*_args|
|
|
372
|
-
system('open',
|
|
236
|
+
add_subcmd(:index, :tui) { |*_args|
|
|
237
|
+
system('open', SERVER)
|
|
373
238
|
}
|
|
374
239
|
|
|
375
240
|
add_subcmd(:reindex, :rebuild) { |*_args|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
add_subcmd(:tui) { |*_args|
|
|
380
|
-
index = DocsIndex.new(docs_dir: DOCS_DIR, index_json: INDEX_JSON, mdoc: MDOC)
|
|
381
|
-
saved = DocsIndexTui.new(index: index).run
|
|
382
|
-
puts(saved ? 'Saved docs index.' : 'Canceled docs index changes.')
|
|
241
|
+
status = API.health
|
|
242
|
+
puts "Portfolio search index is live (#{status.fetch('items')} items)."
|
|
383
243
|
}
|
|
384
244
|
|
|
385
245
|
add_subcmd(:path) { |*_args|
|
data/lib/hiiro/options.rb
CHANGED
|
@@ -124,6 +124,12 @@ class Hiiro
|
|
|
124
124
|
do_parse(raw_args.dup)
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
+
def files = @files ||= args.select{|x| File.file?(x) }
|
|
128
|
+
def dirs = @dirs ||= args.select{|x| File.directory?(x) }
|
|
129
|
+
def file_or_dirs = @file_or_dirs ||= args.select{|x| File.file?(x) || File.directory?(x) }
|
|
130
|
+
def not_files = args - files
|
|
131
|
+
def not_file_or_dirs = args - file_or_dirs
|
|
132
|
+
|
|
127
133
|
def [](name)
|
|
128
134
|
@values[name.to_sym]
|
|
129
135
|
end
|
data/lib/hiiro/version.rb
CHANGED