okf 2.1.0 → 2.2.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/.okf/capabilities/agent-skill.md +112 -0
- data/.okf/capabilities/bundles-manager.md +144 -0
- data/.okf/capabilities/graph-server.md +678 -0
- data/.okf/capabilities/index.md +26 -0
- data/.okf/capabilities/library-api.md +82 -0
- data/.okf/capabilities/linter.md +83 -0
- data/.okf/capabilities/read-views.md +228 -0
- data/.okf/capabilities/render.md +66 -0
- data/.okf/capabilities/search.md +297 -0
- data/.okf/capabilities/validator.md +60 -0
- data/.okf/cli.md +214 -0
- data/.okf/design/browser-tests.md +211 -0
- data/.okf/design/core-shell-split.md +73 -0
- data/.okf/design/index.md +17 -0
- data/.okf/design/integration-first.md +140 -0
- data/.okf/design/packaging.md +65 -0
- data/.okf/design/ruby-floor.md +53 -0
- data/.okf/design/runtime-dependencies.md +82 -0
- data/.okf/design/search-engines.md +154 -0
- data/.okf/design/server-trust-boundary.md +139 -0
- data/.okf/index.md +40 -0
- data/.okf/log.md +724 -0
- data/.okf/model/bundle.md +47 -0
- data/.okf/model/concept.md +75 -0
- data/.okf/model/graph.md +59 -0
- data/.okf/model/index.md +9 -0
- data/.okf/model/skeleton.md +76 -0
- data/.okf/overview.md +87 -0
- data/.okf/registry.md +432 -0
- data/.okf/structure/format-layer.md +59 -0
- data/.okf/structure/index.md +22 -0
- data/.okf/structure/search.md +53 -0
- data/.okf/structure/the-analysers.md +60 -0
- data/.okf/structure/the-cli.md +99 -0
- data/.okf/structure/the-disk-shell.md +76 -0
- data/.okf/structure/the-model.md +81 -0
- data/.okf/structure/the-server.md +74 -0
- data/.okf/structure/the-skill.md +52 -0
- data/.okf/testing/adding-a-verb.md +76 -0
- data/.okf/testing/index.md +12 -0
- data/.okf/testing/the-harness.md +45 -0
- data/CHANGELOG.md +161 -16
- data/README.md +226 -17
- data/lib/okf/cli/command.rb +8 -3
- data/lib/okf/cli/registry.rb +306 -47
- data/lib/okf/cli.rb +1 -1
- data/lib/okf/registry.rb +448 -22
- data/lib/okf/render/graph/template.html.erb +6 -2
- data/lib/okf/server/hub.rb +6 -2
- data/lib/okf/skill/reference/cli/registry.md +49 -6
- data/lib/okf/skill/reference/cli.md +1 -1
- data/lib/okf/version.rb +1 -1
- metadata +46 -5
data/lib/okf/cli/registry.rb
CHANGED
|
@@ -10,7 +10,7 @@ module OKF
|
|
|
10
10
|
class Registry < Command
|
|
11
11
|
# The `registry` umbrella's subcommands — the dispatch, and the words a
|
|
12
12
|
# flag-first invocation is checked against.
|
|
13
|
-
SUBCOMMANDS = %w[init set del list default rename group ungroup].freeze
|
|
13
|
+
SUBCOMMANDS = %w[init set del list default rename group ungroup link unlink import].freeze
|
|
14
14
|
|
|
15
15
|
def self.id
|
|
16
16
|
:registry
|
|
@@ -20,17 +20,29 @@ module OKF
|
|
|
20
20
|
:registry
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# One subcommand per row — the table `okf registry --help` prints, and the
|
|
24
|
+
# single place each subcommand's grammar is written down.
|
|
25
|
+
SUBCOMMAND_ROWS = [
|
|
26
|
+
[ "init", "create a project-local .okf.json (nearest one wins)" ],
|
|
27
|
+
[ "list [--json]", "list registered bundles (* marks the default)" ],
|
|
28
|
+
[ "set <dir|@slug> [--as SLUG] [--default]", "add or update a bundle (a bare `server` serves them)" ],
|
|
29
|
+
[ "del <dir|@slug>", "remove a bundle or group from the registry" ],
|
|
30
|
+
[ "default <@slug>", "move a bundle to the front (the default)" ],
|
|
31
|
+
[ "rename <@slug> <new>", "rename a bundle or group (<new> is a new name, not a ref)" ],
|
|
32
|
+
[ "group <slug> <@member…>", "create a group, or add members (search/server can target @slug)" ],
|
|
33
|
+
[ "ungroup <slug> <@member…>", "remove members from a group (emptying it deletes it)" ],
|
|
34
|
+
[ "link <name> <file>", "point the global registry at another one (its bundles resolve here)" ],
|
|
35
|
+
[ "unlink <name>", "drop a link and every bundle that arrived through it" ],
|
|
36
|
+
[ "import <@slug…> [--from FILE]", "copy bundles out of another registry (global by default) into this one" ]
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# The umbrella is one row in `okf help`. Ten made a third of the map about
|
|
40
|
+
# registry management, which is not a third of what okf does — and the map's
|
|
41
|
+
# job is to name the verbs, not to be every verb's manual. The subcommands
|
|
42
|
+
# are one `--help` away, and the row says so rather than leaving a reader to
|
|
43
|
+
# guess that an umbrella has any.
|
|
23
44
|
def self.help_rows
|
|
24
|
-
[
|
|
25
|
-
[ "registry init", "create a project-local .okf-registry.json (nearest one wins)" ],
|
|
26
|
-
[ "registry list [--json]", "list registered bundles (* marks the default)" ],
|
|
27
|
-
[ "registry set <dir|@slug> [--as SLUG] [--default]", "add or update a bundle (a bare `server` serves them)" ],
|
|
28
|
-
[ "registry del <dir|@slug>", "remove a bundle or group from the registry" ],
|
|
29
|
-
[ "registry default <@slug>", "move a bundle to the front (the default)" ],
|
|
30
|
-
[ "registry rename <@slug> <new>", "rename a bundle or group (<new> is a new name, not a ref)" ],
|
|
31
|
-
[ "registry group <slug> <@member…>", "create a group, or add members (search/server can target @slug)" ],
|
|
32
|
-
[ "registry ungroup <slug> <@member…>", "remove members from a group (emptying it deletes it)" ]
|
|
33
|
-
]
|
|
45
|
+
[ [ "registry <command> [-g]", "name, group and link your bundles (okf registry --help)" ] ]
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
def call(argv)
|
|
@@ -38,6 +50,7 @@ module OKF
|
|
|
38
50
|
|
|
39
51
|
sub = argv.first
|
|
40
52
|
case sub
|
|
53
|
+
when "--help", "-h" then umbrella_help
|
|
41
54
|
when "init" then registry_init(argv.drop(1))
|
|
42
55
|
when "set" then registry_set(argv.drop(1))
|
|
43
56
|
when "del" then registry_del(argv.drop(1))
|
|
@@ -46,6 +59,9 @@ module OKF
|
|
|
46
59
|
when "rename" then registry_rename(argv.drop(1))
|
|
47
60
|
when "group" then registry_group(argv.drop(1))
|
|
48
61
|
when "ungroup" then registry_ungroup(argv.drop(1))
|
|
62
|
+
when "link" then registry_link(argv.drop(1))
|
|
63
|
+
when "unlink" then registry_unlink(argv.drop(1))
|
|
64
|
+
when "import" then registry_import(argv.drop(1))
|
|
49
65
|
else
|
|
50
66
|
# A bare word that isn't a known subcommand is a typo (`registry remove x`
|
|
51
67
|
# must not silently render the list and read as success).
|
|
@@ -67,7 +83,32 @@ module OKF
|
|
|
67
83
|
|
|
68
84
|
private
|
|
69
85
|
|
|
70
|
-
#
|
|
86
|
+
# The umbrella's own help: every subcommand, its grammar, and the one flag
|
|
87
|
+
# they share. A bare `okf registry` is still `registry list` — the shorthand
|
|
88
|
+
# is documented and predates this — so only an explicit --help lands here.
|
|
89
|
+
def umbrella_help
|
|
90
|
+
width = SUBCOMMAND_ROWS.map { |usage, _| usage.length }.max
|
|
91
|
+
@out.puts "Usage: okf registry <command> [options]"
|
|
92
|
+
@out.puts ""
|
|
93
|
+
SUBCOMMAND_ROWS.each { |usage, blurb| @out.puts " #{usage.ljust(width)} #{blurb}" }
|
|
94
|
+
@out.puts ""
|
|
95
|
+
@out.puts " -g, --global act on the global $OKF_HOME registry, ignoring a project-local one"
|
|
96
|
+
@out.puts " (every command but `init`; OKF_NO_DISCOVERY=1 does it for a whole shell)"
|
|
97
|
+
@out.puts ""
|
|
98
|
+
@out.puts "A bare `okf registry` lists them, the same as `okf registry list`."
|
|
99
|
+
@out.puts "`okf registry <command> --help` has that command's own flags."
|
|
100
|
+
0
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# `-g/--global` on every subcommand but `init`, whose whole job is to create
|
|
104
|
+
# a *local* file. One options hash per verb carries it to #open_registry.
|
|
105
|
+
def global_flag(parser, options)
|
|
106
|
+
parser.on("-g", "--global", "act on the global $OKF_HOME registry, ignoring a project-local one") do
|
|
107
|
+
options[:global] = true
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Create a project-local .okf.json in the current directory. Once it
|
|
71
112
|
# exists, discovery finds it (walking up from cwd) and every registry op —
|
|
72
113
|
# and every @ref — resolves through it instead of the global $OKF_HOME one.
|
|
73
114
|
# init only writes the empty file; `registry set` fills it. Refuses to clobber
|
|
@@ -81,8 +122,11 @@ module OKF
|
|
|
81
122
|
no_extras?(argv) or return 2
|
|
82
123
|
|
|
83
124
|
target = File.join(Dir.pwd, OKF::Registry::LOCAL_FILE)
|
|
84
|
-
|
|
85
|
-
|
|
125
|
+
# Either name is "already initialized": the legacy one is still
|
|
126
|
+
# discovered, so writing a second registry beside it would shadow a file
|
|
127
|
+
# the user still thinks is in force — the trap, not the fix.
|
|
128
|
+
existing = OKF::Registry::LOCAL_FILES.find { |name| File.exist?(File.join(Dir.pwd, name)) }
|
|
129
|
+
return usage_error("already initialized: ./#{existing}") if existing
|
|
86
130
|
|
|
87
131
|
# The parent it would shadow, if any — a courtesy, not a barrier: nested
|
|
88
132
|
# registries resolve nearest-first, so creating one here is legitimate.
|
|
@@ -90,7 +134,7 @@ module OKF
|
|
|
90
134
|
@err.puts "note: a parent registry at #{parent} — the nearest one wins" if parent
|
|
91
135
|
|
|
92
136
|
OKF::Registry.new(target).save
|
|
93
|
-
@out.puts "initialized
|
|
137
|
+
@out.puts "initialized ./#{OKF::Registry::LOCAL_FILE}"
|
|
94
138
|
0
|
|
95
139
|
rescue OptionParser::ParseError => e
|
|
96
140
|
@err.puts e.message
|
|
@@ -104,11 +148,12 @@ module OKF
|
|
|
104
148
|
# path already registered refreshes its title in place, and --as renames it. A
|
|
105
149
|
# new path is added, slugged by directory basename unless --as says otherwise.
|
|
106
150
|
def registry_set(argv)
|
|
107
|
-
options = { as: nil, default: false }
|
|
151
|
+
options = { as: nil, default: false, global: false }
|
|
108
152
|
parser = OptionParser.new do |o|
|
|
109
|
-
o.banner = "Usage: okf registry set <dir|@slug> [--as SLUG] [--default]"
|
|
153
|
+
o.banner = "Usage: okf registry set <dir|@slug> [--as SLUG] [--default] [-g]"
|
|
110
154
|
o.on("--as SLUG", "slug to register under (default: directory basename)") { |v| options[:as] = v }
|
|
111
155
|
o.on("--default", "put it first — the bundle a bare `okf server` opens") { options[:default] = true }
|
|
156
|
+
global_flag(o, options)
|
|
112
157
|
help_flag(o)
|
|
113
158
|
end
|
|
114
159
|
# No no_extras? here: positional_dir has already refused a trailing
|
|
@@ -116,7 +161,7 @@ module OKF
|
|
|
116
161
|
# positional through `positional`, which does not check.
|
|
117
162
|
dir = positional_dir(parser, argv) or return 2
|
|
118
163
|
|
|
119
|
-
reg = open_registry
|
|
164
|
+
reg = open_registry(global: options[:global])
|
|
120
165
|
# Said before the upsert: after it, an update is indistinguishable from an
|
|
121
166
|
# add, and "registered" for what was a rename reads as a duplicate entry.
|
|
122
167
|
known = reg.listing.any? { |row| row[:dir] == File.expand_path(dir) }
|
|
@@ -135,14 +180,16 @@ module OKF
|
|
|
135
180
|
|
|
136
181
|
# Remove a bundle from the persistent registry by slug or by its directory.
|
|
137
182
|
def registry_del(argv)
|
|
183
|
+
options = { global: false }
|
|
138
184
|
parser = OptionParser.new do |o|
|
|
139
|
-
o.banner = "Usage: okf registry del <dir|@slug>"
|
|
185
|
+
o.banner = "Usage: okf registry del <dir|@slug> [-g]"
|
|
186
|
+
global_flag(o, options)
|
|
140
187
|
help_flag(o)
|
|
141
188
|
end
|
|
142
189
|
slug = positional(parser, argv) or return 2
|
|
143
190
|
no_extras?(argv) or return 2
|
|
144
191
|
|
|
145
|
-
reg = open_registry
|
|
192
|
+
reg = open_registry(global: options[:global])
|
|
146
193
|
slug = registry_slug(slug, reg) or return 2
|
|
147
194
|
removed = reg.remove(slug)
|
|
148
195
|
return usage_error("no such bundle: #{slug}") unless removed
|
|
@@ -154,11 +201,12 @@ module OKF
|
|
|
154
201
|
end
|
|
155
202
|
|
|
156
203
|
def registry_list(argv)
|
|
157
|
-
options = { json: false }
|
|
204
|
+
options = { json: false, global: false }
|
|
158
205
|
parser = OptionParser.new do |o|
|
|
159
|
-
o.banner = "Usage: okf registry list [--json] [--pretty]\n " \
|
|
206
|
+
o.banner = "Usage: okf registry list [--json] [--pretty] [-g]\n " \
|
|
160
207
|
"okf registry set <dir|@slug> | del <dir|@slug> | default <@slug> | rename <@slug> <new>"
|
|
161
208
|
json_flags(o, options, "emit the registry as JSON")
|
|
209
|
+
global_flag(o, options)
|
|
162
210
|
help_flag(o)
|
|
163
211
|
end
|
|
164
212
|
begin
|
|
@@ -169,13 +217,14 @@ module OKF
|
|
|
169
217
|
end
|
|
170
218
|
no_extras?(argv) or return 2
|
|
171
219
|
|
|
172
|
-
reg = open_registry
|
|
220
|
+
reg = open_registry(global: options[:global])
|
|
173
221
|
if options[:json]
|
|
174
|
-
|
|
175
|
-
|
|
222
|
+
extra = { "groups" => reg.groups_listing.map { |row| stringify(row) },
|
|
223
|
+
"links" => reg.links_listing.map { |row| stringify(row) } }
|
|
224
|
+
return emit_list_json({ "registry" => reg.path }, "bundles", reg.listing.map { |row| stringify(row) }, options, extra)
|
|
176
225
|
end
|
|
177
226
|
|
|
178
|
-
print_registry(reg)
|
|
227
|
+
print_registry(reg, global: options[:global])
|
|
179
228
|
0
|
|
180
229
|
rescue OKF::Error => e
|
|
181
230
|
usage_error(e.message)
|
|
@@ -186,15 +235,17 @@ module OKF
|
|
|
186
235
|
# meant to be hand-editable, so the move is stated rather than left to be
|
|
187
236
|
# discovered from a reordered file.
|
|
188
237
|
def registry_default(argv)
|
|
238
|
+
options = { global: false }
|
|
189
239
|
parser = OptionParser.new do |o|
|
|
190
|
-
o.banner = "Usage: okf registry default <@slug
|
|
240
|
+
o.banner = "Usage: okf registry default <@slug> [-g]\n " \
|
|
191
241
|
"moves it to the front — the first registered bundle is the default until you do"
|
|
242
|
+
global_flag(o, options)
|
|
192
243
|
help_flag(o)
|
|
193
244
|
end
|
|
194
245
|
slug = positional(parser, argv) or return 2
|
|
195
246
|
no_extras?(argv) or return 2
|
|
196
247
|
|
|
197
|
-
reg = open_registry
|
|
248
|
+
reg = open_registry(global: options[:global])
|
|
198
249
|
slug = registry_slug(slug, reg) or return 2
|
|
199
250
|
reg.default = slug
|
|
200
251
|
@out.puts "default bundle → #{reg.default.slug} (now first)"
|
|
@@ -225,8 +276,10 @@ module OKF
|
|
|
225
276
|
|
|
226
277
|
# Rename a registered bundle's slug — its mount path and switcher name.
|
|
227
278
|
def registry_rename(argv)
|
|
279
|
+
options = { global: false }
|
|
228
280
|
parser = OptionParser.new do |o|
|
|
229
|
-
o.banner = "Usage: okf registry rename <@slug> <new>"
|
|
281
|
+
o.banner = "Usage: okf registry rename <@slug> <new> [-g]"
|
|
282
|
+
global_flag(o, options)
|
|
230
283
|
help_flag(o)
|
|
231
284
|
end
|
|
232
285
|
parser.parse!(argv)
|
|
@@ -237,7 +290,7 @@ module OKF
|
|
|
237
290
|
end
|
|
238
291
|
no_extras?(argv) or return 2
|
|
239
292
|
|
|
240
|
-
reg = open_registry
|
|
293
|
+
reg = open_registry(global: options[:global])
|
|
241
294
|
# The old name may be a ref; the new one is a name being minted, never one.
|
|
242
295
|
old_slug = registry_slug(old_slug, reg) or return 2
|
|
243
296
|
entry = reg.rename(old_slug, new_slug)
|
|
@@ -256,8 +309,10 @@ module OKF
|
|
|
256
309
|
# bare or as @refs; the model normalizes, unions, checks each names something,
|
|
257
310
|
# and refuses a cycle. Only `search`/`server` can then target @slug.
|
|
258
311
|
def registry_group(argv)
|
|
312
|
+
options = { global: false }
|
|
259
313
|
parser = OptionParser.new do |o|
|
|
260
|
-
o.banner = "Usage: okf registry group <slug> <@member…>"
|
|
314
|
+
o.banner = "Usage: okf registry group <slug> <@member…> [-g]"
|
|
315
|
+
global_flag(o, options)
|
|
261
316
|
help_flag(o)
|
|
262
317
|
end
|
|
263
318
|
parser.parse!(argv)
|
|
@@ -267,7 +322,7 @@ module OKF
|
|
|
267
322
|
return 2
|
|
268
323
|
end
|
|
269
324
|
|
|
270
|
-
reg = open_registry
|
|
325
|
+
reg = open_registry(global: options[:global])
|
|
271
326
|
group = reg.set_group(slug, argv)
|
|
272
327
|
count = reg.expand(group.slug).size
|
|
273
328
|
@out.puts "grouped #{group.slug} → #{group.members.map { |m| "@#{m}" }.join(", ")} " \
|
|
@@ -283,8 +338,10 @@ module OKF
|
|
|
283
338
|
# Remove members from a group. Emptying it deletes the group — an empty group
|
|
284
339
|
# resolves to nothing, so it is not worth keeping.
|
|
285
340
|
def registry_ungroup(argv)
|
|
341
|
+
options = { global: false }
|
|
286
342
|
parser = OptionParser.new do |o|
|
|
287
|
-
o.banner = "Usage: okf registry ungroup <slug> <@member…>"
|
|
343
|
+
o.banner = "Usage: okf registry ungroup <slug> <@member…> [-g]"
|
|
344
|
+
global_flag(o, options)
|
|
288
345
|
help_flag(o)
|
|
289
346
|
end
|
|
290
347
|
parser.parse!(argv)
|
|
@@ -294,7 +351,7 @@ module OKF
|
|
|
294
351
|
return 2
|
|
295
352
|
end
|
|
296
353
|
|
|
297
|
-
reg = open_registry
|
|
354
|
+
reg = open_registry(global: options[:global])
|
|
298
355
|
removed, emptied = reg.unset_group_members(slug, argv)
|
|
299
356
|
name = OKF::Registry.normalize(slug)
|
|
300
357
|
if emptied
|
|
@@ -312,39 +369,241 @@ module OKF
|
|
|
312
369
|
usage_error(e.message)
|
|
313
370
|
end
|
|
314
371
|
|
|
315
|
-
|
|
372
|
+
# Point the global registry at another registry file. Refused from a
|
|
373
|
+
# project-local one rather than silently retargeting: `link` writes, and a
|
|
374
|
+
# write that lands in a file the user is not standing in is the one surprise
|
|
375
|
+
# worth an error. -g is the way to say it on purpose.
|
|
376
|
+
def registry_link(argv)
|
|
377
|
+
options = { global: false }
|
|
378
|
+
parser = OptionParser.new do |o|
|
|
379
|
+
o.banner = "Usage: okf registry link <name> <file> [-g]"
|
|
380
|
+
global_flag(o, options)
|
|
381
|
+
help_flag(o)
|
|
382
|
+
end
|
|
383
|
+
parser.parse!(argv)
|
|
384
|
+
name, file = argv.shift(2)
|
|
385
|
+
if name.nil? || file.nil?
|
|
386
|
+
@err.puts parser.banner
|
|
387
|
+
return 2
|
|
388
|
+
end
|
|
389
|
+
no_extras?(argv) or return 2
|
|
390
|
+
|
|
391
|
+
reg = open_registry(global: options[:global])
|
|
392
|
+
return links_are_global(reg) if local_registry?(reg)
|
|
393
|
+
|
|
394
|
+
link = reg.link(name, file)
|
|
395
|
+
count = reg.links_listing.find { |row| row[:slug] == link.slug }[:bundles]
|
|
396
|
+
@out.puts "linked #{link.slug} → #{link.registry} (#{count} #{pluralize(count, "bundle")})"
|
|
397
|
+
0
|
|
398
|
+
rescue OptionParser::ParseError => e
|
|
399
|
+
@err.puts e.message
|
|
400
|
+
2
|
|
401
|
+
rescue OKF::Error => e
|
|
402
|
+
usage_error(e.message)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Drop a link and every bundle that arrived through it. The bundles
|
|
406
|
+
# themselves are untouched — a link never owned them.
|
|
407
|
+
def registry_unlink(argv)
|
|
408
|
+
options = { global: false }
|
|
409
|
+
parser = OptionParser.new do |o|
|
|
410
|
+
o.banner = "Usage: okf registry unlink <name> [-g]"
|
|
411
|
+
global_flag(o, options)
|
|
412
|
+
help_flag(o)
|
|
413
|
+
end
|
|
414
|
+
name = positional(parser, argv) or return 2
|
|
415
|
+
no_extras?(argv) or return 2
|
|
416
|
+
|
|
417
|
+
reg = open_registry(global: options[:global])
|
|
418
|
+
return links_are_global(reg) if local_registry?(reg)
|
|
419
|
+
|
|
420
|
+
removed = reg.unlink(name)
|
|
421
|
+
return usage_error("no such link: #{name}") unless removed
|
|
422
|
+
|
|
423
|
+
@out.puts "unlinked #{removed.slug}"
|
|
424
|
+
0
|
|
425
|
+
rescue OKF::Error => e
|
|
426
|
+
usage_error(e.message)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Copy chosen bundles — and the groups that hold them — out of another
|
|
430
|
+
# registry file into the one in force. The counterpart to `link`, not a
|
|
431
|
+
# variant of it: a link is live, whole-file and read-only, and an import
|
|
432
|
+
# copies the reference and hands over ownership.
|
|
433
|
+
#
|
|
434
|
+
# Two registries are named here, and only one flag names each. `-g` goes on
|
|
435
|
+
# meaning what it means on every sibling subcommand — the registry written
|
|
436
|
+
# *to* — and `--from` names the one read, defaulting to the global registry,
|
|
437
|
+
# which inside a repo is the only other one there is. Overloading `-g` into
|
|
438
|
+
# a source flag on this verb alone would make it mean "write here" nine
|
|
439
|
+
# times and "read there" once.
|
|
440
|
+
def registry_import(argv)
|
|
441
|
+
options = { from: nil, as: nil, global: false }
|
|
442
|
+
parser = OptionParser.new do |o|
|
|
443
|
+
o.banner = "Usage: okf registry import <@slug…> [--from FILE] [--as SLUG] [-g]"
|
|
444
|
+
o.on("--from FILE", "registry file to copy from (default: the global $OKF_HOME one)") do |v|
|
|
445
|
+
options[:from] = v
|
|
446
|
+
end
|
|
447
|
+
o.on("--as SLUG", "slug to import under, renaming one bundle or group") { |v| options[:as] = v }
|
|
448
|
+
global_flag(o, options)
|
|
449
|
+
help_flag(o)
|
|
450
|
+
end
|
|
451
|
+
parser.parse!(argv)
|
|
452
|
+
if argv.empty?
|
|
453
|
+
@err.puts parser.banner
|
|
454
|
+
return 2
|
|
455
|
+
end
|
|
456
|
+
# --as names *one* thing. Applying it to a list would either rename them
|
|
457
|
+
# all to the same slug (a collision the user did not ask for) or pick one
|
|
458
|
+
# silently, and there is no reading of "import a b --as c" worth guessing.
|
|
459
|
+
return usage_error("--as names one bundle or group, and #{argv.length} were asked for") if options[:as] && argv.length > 1
|
|
460
|
+
|
|
461
|
+
# Expanded for the report as `link` expands its target: a relative --from
|
|
462
|
+
# is the shorthand, and the file it actually read is the answer.
|
|
463
|
+
from = OKF::Registry.expand(options[:from] || OKF::Registry.path)
|
|
464
|
+
reg = open_registry(global: options[:global])
|
|
465
|
+
print_imported(reg.import(argv, from: from, as: options[:as]), from)
|
|
466
|
+
0
|
|
467
|
+
rescue OptionParser::ParseError => e
|
|
468
|
+
@err.puts e.message
|
|
469
|
+
2
|
|
470
|
+
rescue OKF::Error => e
|
|
471
|
+
usage_error(e.message)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# What landed: a count against the file it came from, then a row per bundle
|
|
475
|
+
# in `set`'s shape (with its concept count, the signal that says whether it
|
|
476
|
+
# was the right bundle), then a row per group. Groups print after the
|
|
477
|
+
# bundles they hold, which is the order they were planned in.
|
|
478
|
+
def print_imported(imported, from)
|
|
479
|
+
bundles = imported[:bundles]
|
|
480
|
+
@out.puts "imported #{bundles.length} #{pluralize(bundles.length, "bundle")} from #{from}"
|
|
481
|
+
bundles.each do |entry|
|
|
482
|
+
folder = OKF::Bundle::Folder.load(entry.path)
|
|
483
|
+
report_skipped(folder)
|
|
484
|
+
count = folder.graph(minimal: true).nodes.size
|
|
485
|
+
@out.puts " #{entry.slug} → #{entry.path} (#{count} #{pluralize(count, "concept")})"
|
|
486
|
+
end
|
|
487
|
+
imported[:groups].each do |group|
|
|
488
|
+
@out.puts " #{group.slug} → #{group.members.map { |member| "@#{member}" }.join(", ")} (group)"
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# The one refusal both link verbs share: a project-local registry is in
|
|
493
|
+
# force, and links are the global one's alone — which is what keeps a linked
|
|
494
|
+
# file's own links unread, and so keeps depth at one with nothing to enforce.
|
|
495
|
+
def links_are_global(reg)
|
|
496
|
+
usage_error("links live in the global registry, and #{registry_display(reg)} is a project-local one " \
|
|
497
|
+
"(okf registry link … --global)")
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def print_registry(reg, global: false)
|
|
316
501
|
# A header only when a project-local registry is in play — the case where
|
|
317
502
|
# "which registry am I looking at?" is a real question. The global $OKF_HOME
|
|
318
503
|
# one is the default, so it stays headerless (and the JSON envelope names
|
|
319
504
|
# the file for a script either way).
|
|
320
|
-
|
|
505
|
+
# Named whenever the answer could have been the other file: a discovered
|
|
506
|
+
# local registry, or a -g that just overrode one. The bare global case
|
|
507
|
+
# stays headerless — nothing was chosen, so there is nothing to disclose.
|
|
508
|
+
@out.puts "registry: #{registry_display(reg)}" if local_registry?(reg) || global
|
|
509
|
+
# groups_listing carries the linked groups too; here they print under
|
|
510
|
+
# their link rather than among the ones `group`/`ungroup` can touch.
|
|
321
511
|
groups = reg.groups_listing
|
|
322
|
-
|
|
512
|
+
own, linked = groups.partition { |group| group[:link].nil? }
|
|
513
|
+
links = reg.links_listing
|
|
514
|
+
return @out.puts "no bundles registered — okf registry set <dir>" if reg.empty? && groups.empty? && links.empty?
|
|
323
515
|
|
|
324
516
|
rows = reg.listing
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
517
|
+
width = width_of(rows)
|
|
518
|
+
rows.reject { |row| row[:link] }.each { |row| @out.puts bundle_row(row, width) }
|
|
519
|
+
print_groups(own, rows) unless own.empty?
|
|
520
|
+
print_links(links, linked, rows, width) unless links.empty?
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Why a link contributed nothing, when it did: a target that is gone, or one
|
|
524
|
+
# that is there and cannot be parsed. Both are reported, never raised.
|
|
525
|
+
def link_state(link)
|
|
526
|
+
return " (missing)" if link[:missing]
|
|
527
|
+
return " (unreadable)" if link[:unreadable]
|
|
528
|
+
|
|
529
|
+
""
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def width_of(rows)
|
|
533
|
+
rows.empty? ? 0 : rows.map { |row| row[:slug].length }.max
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def bundle_row(row, width)
|
|
537
|
+
marker = row[:default] ? "*" : " "
|
|
538
|
+
missing = row[:missing] ? " (missing)" : ""
|
|
539
|
+
# The slug it *had* in the file it came from, shown only when this
|
|
540
|
+
# registry had to move it — the one place a ref that shifted is visible.
|
|
541
|
+
moved = row[:origin] && row[:origin] != row[:slug] ? " [#{row[:origin]}]" : ""
|
|
542
|
+
"#{marker} #{row[:slug].ljust(width)} #{row[:title]} (#{row[:dir]})#{missing}#{moved}"
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# The links section: one heading per link naming the file it points at, then
|
|
546
|
+
# the bundles that arrived through it, then any groups that came with them. A
|
|
547
|
+
# target that is gone or unreadable says so instead of listing nothing.
|
|
548
|
+
def print_links(links, groups, rows, width)
|
|
549
|
+
@out.puts ""
|
|
550
|
+
@out.puts "links:"
|
|
551
|
+
links.each do |link|
|
|
552
|
+
state = link_state(link)
|
|
553
|
+
@out.puts " #{link[:slug]} → #{link[:registry]} " \
|
|
554
|
+
"(#{link[:bundles]} #{pluralize(link[:bundles], "bundle")})#{state}"
|
|
555
|
+
rows.select { |row| row[:link] == link[:slug] }.each { |row| @out.puts " #{bundle_row(row, width)}" }
|
|
556
|
+
groups.select { |group| group[:link] == link[:slug] && group[:slug] != link[:slug] }.each do |group|
|
|
557
|
+
@out.puts " #{group[:slug]} #{group[:members].map { |m| "@#{m}" }.join(", ")}"
|
|
331
558
|
end
|
|
332
559
|
end
|
|
333
|
-
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Every registry read this verb makes, with the filename note attached. The
|
|
563
|
+
# note is the `registry` umbrella's alone: this is the one verb whose
|
|
564
|
+
# *subject* is a registry file, and the one nobody runs in a loop or pipes
|
|
565
|
+
# into something else. `lint` and `search` are both, and a note there is
|
|
566
|
+
# noise people learn to redirect away rather than act on.
|
|
567
|
+
#
|
|
568
|
+
# Once per invocation, not once per read — `registry set @slug` opens the
|
|
569
|
+
# registry twice (the ref, then the write), and saying it twice reads as
|
|
570
|
+
# two problems.
|
|
571
|
+
def open_registry(global: false)
|
|
572
|
+
reg = super
|
|
573
|
+
note_legacy_name(reg) unless @named_registry
|
|
574
|
+
@named_registry = true
|
|
575
|
+
reg
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# Two things worth saying about a project-local registry's filename, and
|
|
579
|
+
# nothing at all about the global one (which is `registry.json` and always
|
|
580
|
+
# was). A legacy file that is *in force* gets the move that retires it. A
|
|
581
|
+
# legacy file sitting beside the `.okf.json` that beat it gets named too:
|
|
582
|
+
# reading one while the other lies there unread is a silent wrong answer
|
|
583
|
+
# unless somebody says so.
|
|
584
|
+
def note_legacy_name(reg)
|
|
585
|
+
return unless local_registry?(reg)
|
|
586
|
+
|
|
587
|
+
legacy = OKF::Registry::LEGACY_LOCAL_FILE
|
|
588
|
+
if File.basename(reg.path) == legacy
|
|
589
|
+
@err.puts "note: #{legacy} is the old name — git mv #{legacy} #{OKF::Registry::LOCAL_FILE}"
|
|
590
|
+
elsif File.file?(File.join(File.dirname(reg.path), legacy))
|
|
591
|
+
@err.puts "note: #{legacy} is ignored — #{OKF::Registry::LOCAL_FILE} beside it wins (delete the old one)"
|
|
592
|
+
end
|
|
334
593
|
end
|
|
335
594
|
|
|
336
595
|
# Whether this registry was discovered as a project-local file rather than
|
|
337
596
|
# read from $OKF_HOME — the basename settles it (only a local one is named
|
|
338
|
-
# .okf-registry.json).
|
|
597
|
+
# .okf.json, or the legacy .okf-registry.json).
|
|
339
598
|
def local_registry?(reg)
|
|
340
|
-
File.basename(reg.path)
|
|
599
|
+
OKF::Registry::LOCAL_FILES.include?(File.basename(reg.path))
|
|
341
600
|
end
|
|
342
601
|
|
|
343
602
|
# How to name the local registry in the header: `./` when it sits in cwd
|
|
344
603
|
# (the common case, a bare `init` here), its absolute path when discovery
|
|
345
604
|
# walked up to an ancestor.
|
|
346
605
|
def registry_display(reg)
|
|
347
|
-
File.dirname(reg.path) == Dir.pwd ? "./#{
|
|
606
|
+
File.dirname(reg.path) == Dir.pwd ? "./#{File.basename(reg.path)}" : reg.path
|
|
348
607
|
end
|
|
349
608
|
|
|
350
609
|
# The groups section under the bundle listing: one row per group, its members
|
data/lib/okf/cli.rb
CHANGED
|
@@ -45,7 +45,7 @@ module OKF
|
|
|
45
45
|
"references" => %w[path dir kind referenced_by],
|
|
46
46
|
"directories" => %w[dir ancestor index_path present synthesized count types tags subdirs body listing],
|
|
47
47
|
"dirs" => %w[dir ancestor count subtree subdirs],
|
|
48
|
-
"bundles" => %w[slug title dir mount default missing]
|
|
48
|
+
"bundles" => %w[slug title dir mount default missing link origin]
|
|
49
49
|
}.freeze
|
|
50
50
|
|
|
51
51
|
# Runs a Rack app under WEBrick until interrupted. Injected into the CLI so
|