asterism 0.7.1 → 0.9.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/CHANGELOG.md +100 -0
- data/README.md +288 -5
- data/exe/asterism +8 -0
- data/lib/asterism/cli/allow.rb +53 -0
- data/lib/asterism/cli/complete.rb +282 -0
- data/lib/asterism/cli/hz.rb +77 -0
- data/lib/asterism/cli/literal.rb +132 -0
- data/lib/asterism/cli/mcp.rb +272 -0
- data/lib/asterism/cli/nouns.rb +396 -0
- data/lib/asterism/cli/survey.rb +206 -0
- data/lib/asterism/cli/text.rb +256 -0
- data/lib/asterism/cli.rb +701 -0
- data/lib/asterism/cruby/credentials.rb +61 -116
- data/lib/asterism/cruby/objects.rb +60 -7
- data/lib/asterism/version.rb +1 -1
- data/lib/asterism.rb +1 -1
- data/mrblib/asterism.rb +206 -23
- data/mrblib/credentials.rb +197 -0
- data/mrblib/status.rb +198 -0
- metadata +17 -4
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# The credentials of one node or router as one file: the .tar an Asterism
|
|
2
|
+
# console hands out when it issues a certificate ("Save all (.tar)" on the
|
|
3
|
+
# certificate page; the archive is made in the browser that made the key).
|
|
4
|
+
# It holds, at its top or in one directory:
|
|
5
|
+
#
|
|
6
|
+
# <name>.key the private key (PKCS#8 PEM)
|
|
7
|
+
# <name>.pem the certificate (its common name is <name>)
|
|
8
|
+
# ca.pem the CA that signed it (and the router's certificate)
|
|
9
|
+
# zenoh.json5 an example configuration (for zenohd or another client)
|
|
10
|
+
# asterism.json {"name": ..., "router": "tls/host:port", "not_after": ..., ...}
|
|
11
|
+
#
|
|
12
|
+
# Asterism.connect(credentials: "/home/asterism/alice.tar", app: "game")
|
|
13
|
+
#
|
|
14
|
+
# The certificates go to zenoh as base64 (transport/link/tls/*_base64) with
|
|
15
|
+
# enable_mtls, and the router's locator and the node ID come from
|
|
16
|
+
# asterism.json unless given. This is the portable part, in plain Ruby for
|
|
17
|
+
# the boards (mruby / PicoRuby: no Regexp, no pack, no File.binread): the
|
|
18
|
+
# file is read whole and the tar taken apart with byteslice / getbyte.
|
|
19
|
+
# CRuby replaces the reading and adds checks (lib/asterism/cruby/credentials.rb:
|
|
20
|
+
# the standard library's tar reader, file modes, the key pair).
|
|
21
|
+
module Asterism
|
|
22
|
+
class Credentials
|
|
23
|
+
# What a credentials file is missing or holds wrong.
|
|
24
|
+
class Error < ::Asterism::Error; end
|
|
25
|
+
|
|
26
|
+
TLS = "transport/link/tls"
|
|
27
|
+
# The largest file read (the real ones are a few KB).
|
|
28
|
+
MAX_SIZE = 1024 * 1024
|
|
29
|
+
BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
30
|
+
|
|
31
|
+
attr_reader :path, :name, :kind, :router, :not_after, :key_pem, :cert_pem, :ca_pem, :info
|
|
32
|
+
|
|
33
|
+
# Reads path (a String of a .tar). warn: false skips the check of who
|
|
34
|
+
# may read the file (CRuby; the boards have no file modes).
|
|
35
|
+
def self.load(path, warn: true)
|
|
36
|
+
new(path, warn: warn)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A credentials file (a path or a Credentials) and the arguments of a
|
|
40
|
+
# connection -> [locator, config Hash, credentials]. The locator given
|
|
41
|
+
# wins over asterism.json's router; config given (a Hash) is laid over
|
|
42
|
+
# the TLS keys. @api private
|
|
43
|
+
def self.merge(credentials, locator, config)
|
|
44
|
+
creds = credentials.is_a?(Credentials) ? credentials : load(credentials)
|
|
45
|
+
if config.is_a?(String)
|
|
46
|
+
raise ArgumentError, "credentials: and a config String cannot be combined; pass config: as a Hash"
|
|
47
|
+
end
|
|
48
|
+
merged = creds.zenoh_config
|
|
49
|
+
(config || {}).each { |k, v| merged[k] = v }
|
|
50
|
+
[locator || creds.router, merged, creds]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# {basename => content} of the regular files of a ustar archive (data,
|
|
54
|
+
# a String of its bytes), at its top or in one directory. @api private
|
|
55
|
+
def self.untar(data, path = "the file")
|
|
56
|
+
files = {}
|
|
57
|
+
pos = 0
|
|
58
|
+
total = data.bytesize
|
|
59
|
+
while pos + 512 <= total
|
|
60
|
+
break if data.getbyte(pos) == 0 # the end: blocks of zeros
|
|
61
|
+
name = field(data, pos, 100)
|
|
62
|
+
prefix = field(data, pos + 345, 155)
|
|
63
|
+
size_text = field(data, pos + 124, 12).strip
|
|
64
|
+
type = data.getbyte(pos + 156)
|
|
65
|
+
size = size_text.empty? ? 0 : size_text.to_i(8)
|
|
66
|
+
pos += 512
|
|
67
|
+
raise Error, "#{path}: not a tar file (a member runs past the end)" if pos + size > total
|
|
68
|
+
if type == 48 || type == 0 # '0' or NUL: a regular file
|
|
69
|
+
full = prefix.empty? ? name : "#{prefix}/#{name}"
|
|
70
|
+
base = full.split("/").last.to_s
|
|
71
|
+
raise Error, "#{path}: #{base} is in it twice" if files.key?(base)
|
|
72
|
+
files[base] = data.byteslice(pos, size) unless base.empty?
|
|
73
|
+
end
|
|
74
|
+
pos += (size + 511) / 512 * 512
|
|
75
|
+
end
|
|
76
|
+
raise Error, "#{path}: not a tar file with credentials in it" if files.empty?
|
|
77
|
+
files
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The text of a NUL-terminated header field. @api private
|
|
81
|
+
def self.field(data, pos, len)
|
|
82
|
+
n = 0
|
|
83
|
+
n += 1 while n < len && data.getbyte(pos + n) != 0
|
|
84
|
+
data.byteslice(pos, n).to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# base64 of s without line breaks (CRuby's [s].pack("m0")). @api private
|
|
88
|
+
def self.base64(s)
|
|
89
|
+
out = ""
|
|
90
|
+
n = s.bytesize
|
|
91
|
+
i = 0
|
|
92
|
+
while i < n
|
|
93
|
+
b0 = s.getbyte(i)
|
|
94
|
+
b1 = i + 1 < n ? s.getbyte(i + 1) : 0
|
|
95
|
+
b2 = i + 2 < n ? s.getbyte(i + 2) : 0
|
|
96
|
+
v = (b0 << 16) | (b1 << 8) | b2
|
|
97
|
+
out << BASE64[(v >> 18) & 63] << BASE64[(v >> 12) & 63]
|
|
98
|
+
out << (i + 1 < n ? BASE64[(v >> 6) & 63] : "=")
|
|
99
|
+
out << (i + 2 < n ? BASE64[v & 63] : "=")
|
|
100
|
+
i += 3
|
|
101
|
+
end
|
|
102
|
+
out
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The string fields of asterism.json without a JSON library (only for
|
|
106
|
+
# a build that has none). @api private
|
|
107
|
+
def self.json_strings(text)
|
|
108
|
+
h = {}
|
|
109
|
+
%w[format name kind router serial not_after fingerprint console issued_at].each do |k|
|
|
110
|
+
key = "\"#{k}\""
|
|
111
|
+
i = text.index(key)
|
|
112
|
+
next unless i
|
|
113
|
+
rest = text[i + key.size, text.size - i - key.size].to_s
|
|
114
|
+
colon = rest.index(":")
|
|
115
|
+
next unless colon
|
|
116
|
+
q1 = colon + 1
|
|
117
|
+
q1 += 1 while rest[q1] == " "
|
|
118
|
+
next unless rest[q1] == "\""
|
|
119
|
+
q2 = rest.index("\"", q1 + 1)
|
|
120
|
+
next unless q2
|
|
121
|
+
h[k] = rest[q1 + 1, q2 - q1 - 1]
|
|
122
|
+
end
|
|
123
|
+
h
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def initialize(path, warn: true)
|
|
127
|
+
@path = path.to_s
|
|
128
|
+
check_mode if warn
|
|
129
|
+
files = read_tar
|
|
130
|
+
@info = parse_info(files["asterism.json"])
|
|
131
|
+
keys = files.keys.select { |f| f.end_with?(".key") }
|
|
132
|
+
raise Error, "#{@path}: no private key (<name>.key) in it" if keys.empty?
|
|
133
|
+
raise Error, "#{@path}: more than one private key in it (#{keys.sort.join(', ')})" if keys.size > 1
|
|
134
|
+
@name = @info["name"].to_s
|
|
135
|
+
@name = keys.first[0, keys.first.size - 4] if @name.empty?
|
|
136
|
+
@key_pem = files["#{@name}.key"] or raise Error, "#{@path}: the key is not named after #{@name} (#{keys.first})"
|
|
137
|
+
@cert_pem = files["#{@name}.pem"] or raise Error, "#{@path}: no certificate #{@name}.pem in it"
|
|
138
|
+
@ca_pem = files["ca.pem"] or raise Error, "#{@path}: no ca.pem in it"
|
|
139
|
+
@kind = @info["kind"]
|
|
140
|
+
@router = @info["router"].to_s.empty? ? nil : @info["router"].to_s
|
|
141
|
+
@not_after = @info["not_after"]
|
|
142
|
+
check_pair
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The zenoh configuration keys (a Hash for Session.open's config:):
|
|
146
|
+
# the CA, the certificate and the key as base64, and mutual TLS.
|
|
147
|
+
def zenoh_config
|
|
148
|
+
{
|
|
149
|
+
"#{TLS}/root_ca_certificate_base64" => b64(@ca_pem),
|
|
150
|
+
"#{TLS}/connect_certificate_base64" => b64(@cert_pem),
|
|
151
|
+
"#{TLS}/connect_private_key_base64" => b64(@key_pem),
|
|
152
|
+
"#{TLS}/enable_mtls" => true
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def inspect
|
|
157
|
+
"#<Asterism::Credentials #{@name} #{@router || '(no router)'}#{@not_after ? " until #{@not_after}" : ''}>"
|
|
158
|
+
end
|
|
159
|
+
alias to_s inspect
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def b64(pem)
|
|
164
|
+
Credentials.base64(pem)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# The boards have no file modes (CRuby warns like ssh).
|
|
168
|
+
def check_mode; end
|
|
169
|
+
|
|
170
|
+
# The key pair is checked by CRuby (openssl); on the boards the router
|
|
171
|
+
# refuses a wrong one.
|
|
172
|
+
def check_pair; end
|
|
173
|
+
|
|
174
|
+
def read_tar
|
|
175
|
+
data = begin
|
|
176
|
+
::File.open(@path, "r") { |f| f.read }
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
raise Error, "#{@path}: #{e.message}"
|
|
179
|
+
end
|
|
180
|
+
raise Error, "#{@path}: cannot be read" if data.nil?
|
|
181
|
+
raise Error, "#{@path}: too large for a credentials file" if data.bytesize > MAX_SIZE
|
|
182
|
+
Credentials.untar(data, @path)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def parse_info(text)
|
|
186
|
+
return {} if text.nil?
|
|
187
|
+
return Credentials.json_strings(text) unless Object.const_defined?(:JSON)
|
|
188
|
+
h = ::JSON.parse(text)
|
|
189
|
+
raise Error, "#{@path}: asterism.json is not an object" unless h.is_a?(Hash)
|
|
190
|
+
h
|
|
191
|
+
rescue Error
|
|
192
|
+
raise
|
|
193
|
+
rescue StandardError => e
|
|
194
|
+
raise Error, "#{@path}: asterism.json: #{e.message}"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
data/mrblib/status.rb
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# An application's status: a small Hash of its own that changes while it
|
|
2
|
+
# runs (how many tables are open, who is seated, a battery level), told by
|
|
3
|
+
# the application itself.
|
|
4
|
+
#
|
|
5
|
+
# Asterism.status(tables: 3, humans: 2) # sets those fields, keeps the others
|
|
6
|
+
# Asterism.status({ "mahjong1" => {...} }) # the whole status (a Hash)
|
|
7
|
+
# Asterism.status # => {"tables" => 3, ...} or nil
|
|
8
|
+
# Asterism.status_of("games", "games") # another application's, or nil
|
|
9
|
+
#
|
|
10
|
+
# Key: asterism/<node>/<app>/@status (docs/protocol.md 8). The application
|
|
11
|
+
# answers queries there with its status, and puts it on the same key when
|
|
12
|
+
# it changes (only then: the same status again puts nothing), at connect
|
|
13
|
+
# and after a lost connection comes back. An application that never set a
|
|
14
|
+
# status declares nothing there and answers nothing.
|
|
15
|
+
#
|
|
16
|
+
# The value is a MessagePack map with str keys; the values are any values
|
|
17
|
+
# of the protocol nested at most MAX_DEPTH levels below the map, and the
|
|
18
|
+
# whole map packs into MAX_BYTES at most. It is not the profile
|
|
19
|
+
# (profile.rb): a profile says once what a node is; a status changes.
|
|
20
|
+
module Asterism
|
|
21
|
+
module Status
|
|
22
|
+
KEY = "@status"
|
|
23
|
+
MAX_BYTES = 1024 # the packed map
|
|
24
|
+
MAX_DEPTH = 4 # levels of arrays and maps below the map
|
|
25
|
+
|
|
26
|
+
# A received status, cleaned: the entries with a non-empty str key; nil
|
|
27
|
+
# when it is not a map or packs into more than MAX_BYTES.
|
|
28
|
+
def self.check(h)
|
|
29
|
+
return nil unless h.is_a?(Hash)
|
|
30
|
+
out = {}
|
|
31
|
+
h.each { |k, v| out[k] = v if k.is_a?(String) && !k.empty? }
|
|
32
|
+
begin
|
|
33
|
+
return nil if ::MessagePack.pack(out).bytesize > MAX_BYTES
|
|
34
|
+
rescue
|
|
35
|
+
return nil
|
|
36
|
+
end
|
|
37
|
+
out
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# [node, app, status] of a notice put on asterism/<node>/<app>/@status
|
|
41
|
+
# (key and payload as a subscriber gets them), or nil when it is not one.
|
|
42
|
+
def self.parse(key, payload)
|
|
43
|
+
parts = key.to_s.split("/")
|
|
44
|
+
return nil unless parts.size == 4 && parts[0] == ROOT && parts[3] == KEY
|
|
45
|
+
st = check(Codec.unpack(payload))
|
|
46
|
+
st ? [parts[1], parts[2], st] : nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The checks of Asterism.status: a copy with String keys, or
|
|
50
|
+
# ArgumentError with the reason.
|
|
51
|
+
# @api private
|
|
52
|
+
def self.check!(h)
|
|
53
|
+
raise ArgumentError, "status: expected a Hash, got #{h.class}" unless h.is_a?(Hash)
|
|
54
|
+
out = {}
|
|
55
|
+
h.each do |k, v|
|
|
56
|
+
key = k.is_a?(Symbol) ? k.to_s : k
|
|
57
|
+
unless key.is_a?(String) && !key.empty?
|
|
58
|
+
raise ArgumentError, "status: the keys are Strings or Symbols (not empty), got #{k.inspect}"
|
|
59
|
+
end
|
|
60
|
+
raise ArgumentError, "status #{key}: nested deeper than #{MAX_DEPTH} levels" if too_deep?(v, 1)
|
|
61
|
+
begin
|
|
62
|
+
out[key] = Codec.check(v)
|
|
63
|
+
rescue EncodeError => e
|
|
64
|
+
raise ArgumentError, "status #{key}: #{e.message}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
n = Codec.pack(out).bytesize
|
|
68
|
+
raise ArgumentError, "status: #{n} bytes packed (#{MAX_BYTES} at most)" if n > MAX_BYTES
|
|
69
|
+
out
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @api private
|
|
73
|
+
def self.too_deep?(v, level)
|
|
74
|
+
if v.is_a?(Array) || v.is_a?(Hash)
|
|
75
|
+
return true if level > MAX_DEPTH
|
|
76
|
+
if v.is_a?(Array)
|
|
77
|
+
v.each { |e| return true if too_deep?(e, level + 1) }
|
|
78
|
+
else
|
|
79
|
+
v.each { |k, e| return true if too_deep?(k, level + 1) || too_deep?(e, level + 1) }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# This application's status. With a Hash, it is the whole status; with
|
|
87
|
+
# keywords, those fields change (nil removes one) and the others stay;
|
|
88
|
+
# both: the keywords on top of the Hash. Returns the status. Without
|
|
89
|
+
# arguments, returns it (nil when none was set). Raises ArgumentError for
|
|
90
|
+
# a status it does not accept (see Asterism::Status).
|
|
91
|
+
#
|
|
92
|
+
# Kept across connect and close (it belongs to the process, or the VM on
|
|
93
|
+
# a board). While connected, a change is put on asterism/<node>/<app>/@status;
|
|
94
|
+
# setting the same status again puts nothing.
|
|
95
|
+
def self.status(h = nil, **fields)
|
|
96
|
+
if h.nil? && fields.empty?
|
|
97
|
+
return @status ? @status.dup : nil
|
|
98
|
+
end
|
|
99
|
+
base = h.nil? ? (@status ? @status.dup : {}) : Status.check!(h)
|
|
100
|
+
fields.each do |k, v|
|
|
101
|
+
if v.nil?
|
|
102
|
+
base.delete(k.to_s)
|
|
103
|
+
else
|
|
104
|
+
base[k.to_s] = v
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
@status = Status.check!(base)
|
|
108
|
+
tell_status(false)
|
|
109
|
+
@status.dup
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Another application's status (Asterism::Status.check has cleaned it),
|
|
113
|
+
# or nil when it does not answer in time (no status set, an older
|
|
114
|
+
# version of Asterism, an application that is gone). timeout: in seconds
|
|
115
|
+
# (or timeout_ms:), 2.0 by default. Waits like a call (polling, answering
|
|
116
|
+
# calls meanwhile); raises Disconnected when not connected. This
|
|
117
|
+
# application's own status comes without the network.
|
|
118
|
+
def self.status_of(node, app, timeout: nil, timeout_ms: nil)
|
|
119
|
+
f = status_async(node, app, timeout, timeout_ms)
|
|
120
|
+
begin
|
|
121
|
+
raw = f.raw_value
|
|
122
|
+
rescue Disconnected
|
|
123
|
+
raise
|
|
124
|
+
rescue Error
|
|
125
|
+
return nil # no answer in time, or not MessagePack
|
|
126
|
+
end
|
|
127
|
+
Status.check(raw)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @api private
|
|
131
|
+
def self.status_async(node, app, timeout, timeout_ms)
|
|
132
|
+
node = check_name("node", node)
|
|
133
|
+
app = check_name("app", app)
|
|
134
|
+
ms = time_ms("Asterism.status_of", timeout, timeout_ms, nil, DEFAULT_TIMEOUT_MS)
|
|
135
|
+
path = "#{node}/#{app}/#{Status::KEY}"
|
|
136
|
+
if @session && node == @node && app == @app
|
|
137
|
+
return Future.answered(path, "status", Codec.pack(@status)) if @status
|
|
138
|
+
return Future.answered(path, "status", Codec.pack(nil))
|
|
139
|
+
end
|
|
140
|
+
begin
|
|
141
|
+
g = session!.get("#{ROOT}/#{path}", timeout_ms: ms)
|
|
142
|
+
rescue Asterism::Zenoh::Error => e
|
|
143
|
+
lost_if_closed(e.message)
|
|
144
|
+
raise Disconnected, e.message
|
|
145
|
+
end
|
|
146
|
+
Future.new(path, "status", g, now_ms + ms, ms)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The applications alive now ("<node>/<app>", from their liveliness
|
|
150
|
+
# tokens; this one included) whose path matches the pattern (* is one
|
|
151
|
+
# chunk, ** any number).
|
|
152
|
+
def self.apps(pattern = "*/*")
|
|
153
|
+
list = []
|
|
154
|
+
return list unless @session
|
|
155
|
+
own = "#{@node}/#{@app}"
|
|
156
|
+
list << own if glob?(pattern, own)
|
|
157
|
+
@apps.each_key { |path| list << path if path != own && glob?(pattern, path) }
|
|
158
|
+
list
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Puts the status on its key while connected: when it changed since the
|
|
162
|
+
# last put of this connection, or always with force (a connection that
|
|
163
|
+
# came back). Declares the status queryable with the first put.
|
|
164
|
+
# @api private
|
|
165
|
+
def self.tell_status(force)
|
|
166
|
+
s = @session
|
|
167
|
+
return unless s && @status
|
|
168
|
+
bytes = Codec.pack(@status)
|
|
169
|
+
return if !force && !@status_put.nil? && @status_put == bytes
|
|
170
|
+
key = "#{ROOT}/#{@node}/#{@app}/#{Status::KEY}"
|
|
171
|
+
@status_queryable ||= s.queryable(key, depth: 4)
|
|
172
|
+
s.put(key, bytes)
|
|
173
|
+
@status_put = bytes
|
|
174
|
+
rescue Asterism::Zenoh::Error
|
|
175
|
+
nil # the next poll notices a lost connection
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @api private
|
|
179
|
+
def self.answer_status
|
|
180
|
+
qa = @status_queryable
|
|
181
|
+
return unless qa && qa.pending > 0
|
|
182
|
+
queries = qa.each_pending
|
|
183
|
+
i = 0
|
|
184
|
+
while i < queries.size
|
|
185
|
+
q = queries[i]
|
|
186
|
+
i += 1
|
|
187
|
+
begin
|
|
188
|
+
q.reply("#{ROOT}/#{@node}/#{@app}/#{Status::KEY}", Codec.pack(@status || {}))
|
|
189
|
+
ensure
|
|
190
|
+
begin
|
|
191
|
+
q.finish
|
|
192
|
+
rescue Asterism::Zenoh::Error
|
|
193
|
+
# the session went away meanwhile
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: asterism
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Katsuhiko Kageyama
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: asterism-zenoh
|
|
@@ -48,7 +48,8 @@ description: Proxies for objects exposed by other Ruby processes and boards, a R
|
|
|
48
48
|
2 node speaking rmw_zenoh's wire format, CDR and generated message types. Pure Ruby;
|
|
49
49
|
the same files are the mrbgem for mruby / PicoRuby.
|
|
50
50
|
email:
|
|
51
|
-
executables:
|
|
51
|
+
executables:
|
|
52
|
+
- asterism
|
|
52
53
|
extensions: []
|
|
53
54
|
extra_rdoc_files: []
|
|
54
55
|
files:
|
|
@@ -144,7 +145,17 @@ files:
|
|
|
144
145
|
- data/msgs/visualization_msgs/msg/MarkerArray.rb
|
|
145
146
|
- data/msgs/visualization_msgs/msg/MeshFile.rb
|
|
146
147
|
- data/msgs/visualization_msgs/msg/UVCoordinate.rb
|
|
148
|
+
- exe/asterism
|
|
147
149
|
- lib/asterism.rb
|
|
150
|
+
- lib/asterism/cli.rb
|
|
151
|
+
- lib/asterism/cli/allow.rb
|
|
152
|
+
- lib/asterism/cli/complete.rb
|
|
153
|
+
- lib/asterism/cli/hz.rb
|
|
154
|
+
- lib/asterism/cli/literal.rb
|
|
155
|
+
- lib/asterism/cli/mcp.rb
|
|
156
|
+
- lib/asterism/cli/nouns.rb
|
|
157
|
+
- lib/asterism/cli/survey.rb
|
|
158
|
+
- lib/asterism/cli/text.rb
|
|
148
159
|
- lib/asterism/cruby.rb
|
|
149
160
|
- lib/asterism/cruby/credentials.rb
|
|
150
161
|
- lib/asterism/cruby/objects.rb
|
|
@@ -154,10 +165,12 @@ files:
|
|
|
154
165
|
- lib/asterism/version.rb
|
|
155
166
|
- mrblib/asterism.rb
|
|
156
167
|
- mrblib/cdr.rb
|
|
168
|
+
- mrblib/credentials.rb
|
|
157
169
|
- mrblib/future.rb
|
|
158
170
|
- mrblib/profile.rb
|
|
159
171
|
- mrblib/proxy.rb
|
|
160
172
|
- mrblib/ros.rb
|
|
173
|
+
- mrblib/status.rb
|
|
161
174
|
- tools/asterism_msggen.rb
|
|
162
175
|
homepage: https://github.com/ruby-asterism/asterism
|
|
163
176
|
licenses:
|