sferik 0.0.1
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 +7 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +72 -0
- data/LICENSE.md +16 -0
- data/README.md +517 -0
- data/exe/sferik +6 -0
- data/lib/sferik/api/code_endpoints.rb +37 -0
- data/lib/sferik/api/profile_endpoints.rb +113 -0
- data/lib/sferik/api/resume_endpoints.rb +41 -0
- data/lib/sferik/api/site_endpoints.rb +247 -0
- data/lib/sferik/api/talk_endpoints.rb +45 -0
- data/lib/sferik/api.rb +23 -0
- data/lib/sferik/block.rb +25 -0
- data/lib/sferik/body.rb +54 -0
- data/lib/sferik/cache.rb +361 -0
- data/lib/sferik/cli.rb +369 -0
- data/lib/sferik/client.rb +453 -0
- data/lib/sferik/collection.rb +82 -0
- data/lib/sferik/configuration.rb +129 -0
- data/lib/sferik/connections.rb +151 -0
- data/lib/sferik/contributions.rb +86 -0
- data/lib/sferik/day.rb +35 -0
- data/lib/sferik/dependency.rb +26 -0
- data/lib/sferik/deployment.rb +36 -0
- data/lib/sferik/errors.rb +234 -0
- data/lib/sferik/figure.rb +83 -0
- data/lib/sferik/finger.rb +74 -0
- data/lib/sferik/freshness.rb +54 -0
- data/lib/sferik/home/pages.rb +27 -0
- data/lib/sferik/home/profile.rb +51 -0
- data/lib/sferik/home/section.rb +27 -0
- data/lib/sferik/home.rb +38 -0
- data/lib/sferik/json_parsing.rb +28 -0
- data/lib/sferik/name_change.rb +49 -0
- data/lib/sferik/place.rb +33 -0
- data/lib/sferik/podcast.rb +41 -0
- data/lib/sferik/project.rb +49 -0
- data/lib/sferik/projects.rb +138 -0
- data/lib/sferik/push.rb +33 -0
- data/lib/sferik/resource.rb +373 -0
- data/lib/sferik/resume/award.rb +43 -0
- data/lib/sferik/resume/basics.rb +77 -0
- data/lib/sferik/resume/education.rb +59 -0
- data/lib/sferik/resume/location.rb +35 -0
- data/lib/sferik/resume/meta.rb +35 -0
- data/lib/sferik/resume/patent.rb +43 -0
- data/lib/sferik/resume/profile.rb +35 -0
- data/lib/sferik/resume/project.rb +27 -0
- data/lib/sferik/resume/skill.rb +27 -0
- data/lib/sferik/resume/speaking.rb +19 -0
- data/lib/sferik/resume/volunteer.rb +59 -0
- data/lib/sferik/resume/work.rb +51 -0
- data/lib/sferik/resume.rb +125 -0
- data/lib/sferik/session.rb +41 -0
- data/lib/sferik/social_profile.rb +49 -0
- data/lib/sferik/status/github.rb +38 -0
- data/lib/sferik/status/loaded.rb +48 -0
- data/lib/sferik/status.rb +33 -0
- data/lib/sferik/talk.rb +73 -0
- data/lib/sferik/talks.rb +79 -0
- data/lib/sferik/validation.rb +119 -0
- data/lib/sferik/version.rb +7 -0
- data/lib/sferik/web_finger/link.rb +46 -0
- data/lib/sferik/web_finger.rb +43 -0
- data/lib/sferik/who.rb +53 -0
- data/lib/sferik/whoami.rb +36 -0
- data/lib/sferik/wrapping.rb +114 -0
- data/lib/sferik.rb +246 -0
- data/sig/manifest.yaml +13 -0
- data/sig/sferik.rbs +825 -0
- metadata +118 -0
data/lib/sferik/cli.rb
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "version"
|
|
5
|
+
|
|
6
|
+
module Sferik
|
|
7
|
+
# The sferik command: prints what the shell on sferik.net prints, in a real terminal
|
|
8
|
+
#
|
|
9
|
+
# Each command prints a resource of the site as text, the same text `curl sferik.net/finger` gets, or with --json
|
|
10
|
+
# as JSON. The resume also comes as a PDF with --pdf, and as LaTeX with --latex, and finger as a contact card with
|
|
11
|
+
# --vcard. The write command sends me what it reads from standard input, as the shell's write sferik does, and the
|
|
12
|
+
# check-in command logs in a terminal, as each browser tab on the site does, and prints its name: with --watch, it
|
|
13
|
+
# keeps it logged in until it's interrupted. It asks sferik.net, or a copy of the site at the URL that --host or
|
|
14
|
+
# the SFERIK_HOST environment variable names.
|
|
15
|
+
#
|
|
16
|
+
# @api public
|
|
17
|
+
# @example
|
|
18
|
+
# Sferik::CLI.new.run(["finger"]) # prints how to reach me, and returns 0
|
|
19
|
+
# Sferik::CLI.new.run(["talks", "--json"]) # prints my talks as JSON
|
|
20
|
+
# Sferik::CLI.new.run(["resume", "--pdf"]) # prints my resume as a PDF
|
|
21
|
+
# Sferik::CLI.new.run(["signature"]) # prints my motto
|
|
22
|
+
# Sferik::CLI.new.run(["write"]) # sends me a message, read from standard input
|
|
23
|
+
# Sferik::CLI.new.run(["check-in"]) # logs in a terminal, and prints its name
|
|
24
|
+
# Sferik::CLI.new.run(["check-in", "--watch"]) # and keeps it logged in, until it's interrupted
|
|
25
|
+
class CLI
|
|
26
|
+
# The commands, and the path of the resource each one prints
|
|
27
|
+
COMMANDS = {
|
|
28
|
+
"finger" => "/finger", "whoami" => "/whoami", "talks" => "/talks", "podcasts" => "/podcasts", "resume" => "/resume", "who" => "/who",
|
|
29
|
+
"contributions" => "/contributions", "src" => "/src", "name" => "/name", "dependency" => "/dependency"
|
|
30
|
+
}.freeze
|
|
31
|
+
private_constant :COMMANDS
|
|
32
|
+
|
|
33
|
+
# What sferik --help prints
|
|
34
|
+
USAGE = <<~TEXT
|
|
35
|
+
Usage: sferik [options] [command]
|
|
36
|
+
|
|
37
|
+
finger how to reach me
|
|
38
|
+
whoami who is this
|
|
39
|
+
talks my talks and podcasts
|
|
40
|
+
podcasts my podcast appearances
|
|
41
|
+
resume my resume, as a man page
|
|
42
|
+
contributions a year of GitHub contributions
|
|
43
|
+
src my open source projects
|
|
44
|
+
name my name change, as a git commit
|
|
45
|
+
dependency the xkcd comic, in words
|
|
46
|
+
who who's reading sferik.net
|
|
47
|
+
signature my motto
|
|
48
|
+
webfinger where sferik@sferik.net points to, as JSON
|
|
49
|
+
feed my talks, as an Atom feed
|
|
50
|
+
deployment which commit of the site is deployed, and when, as JSON
|
|
51
|
+
status whether GitHub answers the site with its token, as JSON
|
|
52
|
+
openapi the description of the site's API, as JSON
|
|
53
|
+
write send me a message, read from standard input
|
|
54
|
+
check-in log in a terminal, as a browser tab does, and print its name
|
|
55
|
+
help print this
|
|
56
|
+
|
|
57
|
+
With no command, sferik prints the home page.
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
--json print JSON, not text
|
|
61
|
+
--pdf print a PDF, for the resume: redirect it to a file
|
|
62
|
+
--latex print LaTeX, for the resume
|
|
63
|
+
--vcard print a contact card, for finger
|
|
64
|
+
--tty NAME for write: the terminal the message is from, as check-in names it
|
|
65
|
+
--token KEY for check-in: the terminal's token, to keep its name (random, by default)
|
|
66
|
+
--watch for check-in: stay logged in until interrupted (Ctrl-C)
|
|
67
|
+
--host URL ask a copy of the site at URL (or set SFERIK_HOST)
|
|
68
|
+
-h, --help print this
|
|
69
|
+
-v, --version print the version
|
|
70
|
+
TEXT
|
|
71
|
+
private_constant :USAGE
|
|
72
|
+
|
|
73
|
+
# What sferik write says before it reads a message from a terminal
|
|
74
|
+
PROMPT = "Type your message, then Ctrl-D to send it, or Ctrl-C to cancel. Include your email address if you'd like a reply.\n"
|
|
75
|
+
private_constant :PROMPT
|
|
76
|
+
|
|
77
|
+
# The options that name a format, and the media type each asks for
|
|
78
|
+
FORMATS = {"--json" => "application/json", "--pdf" => "application/pdf", "--latex" => "application/x-latex", "--vcard" => "text/vcard"}.freeze
|
|
79
|
+
|
|
80
|
+
# The options that are noted as they're given, and what each is noted as: its value, for one that takes a value,
|
|
81
|
+
# and true for one that takes none
|
|
82
|
+
VALUES = {"--tty NAME" => :tty, "--token KEY" => :token, "--host URL" => :host, "--watch" => :watch}.freeze
|
|
83
|
+
|
|
84
|
+
# The options that are for one command alone, and the command each is for
|
|
85
|
+
OWNERS = {tty: "write", token: "check-in", watch: "check-in"}.freeze
|
|
86
|
+
|
|
87
|
+
# What prints no resource of the site, by the command or option that asks for it, and the method that does each.
|
|
88
|
+
# Or what there is of the site in one format alone, which is printed as that: the method, then the path and the format
|
|
89
|
+
ACTIONS = {:usage => [:usage], "help" => [:usage], :version => [:version], "write" => [:write], "check-in" => [:check_in],
|
|
90
|
+
"feed" => [:only, "/talks.atom", "application/atom+xml"], "deployment" => [:only, "/version", "application/json"], "status" => [:only, "/status", "application/json"], "openapi" => [:only, "/openapi.json", "application/json"], "signature" => [:only, "/.signature", "text/plain"], "webfinger" => [:only, "/.well-known/webfinger?resource=acct%3Asferik%40sferik.net", "application/jrd+json"]}.freeze
|
|
91
|
+
private_constant :FORMATS, :VALUES, :OWNERS, :ACTIONS
|
|
92
|
+
|
|
93
|
+
# Initialize a new CLI
|
|
94
|
+
#
|
|
95
|
+
# @api public
|
|
96
|
+
# @param client [#new, nil] what makes the client for a host: the {Client} class, unless something else is given
|
|
97
|
+
# @param input [IO] where sferik write reads a message from
|
|
98
|
+
# @param out [IO] where output goes
|
|
99
|
+
# @param err [IO] where errors go
|
|
100
|
+
# @param env [#fetch] the environment, for SFERIK_HOST
|
|
101
|
+
# @return [CLI] a new instance
|
|
102
|
+
# @example
|
|
103
|
+
# Sferik::CLI.new(env: {"SFERIK_HOST" => "http://localhost:3745"})
|
|
104
|
+
def initialize(client: nil, input: $stdin, out: $stdout, err: $stderr, env: ENV)
|
|
105
|
+
@client = client
|
|
106
|
+
@input = input
|
|
107
|
+
@out = out
|
|
108
|
+
@err = err
|
|
109
|
+
@env = env
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Run a command
|
|
113
|
+
#
|
|
114
|
+
# @api public
|
|
115
|
+
# @param argv [Array<String>] the command line, without the program's name
|
|
116
|
+
# @return [Integer] the exit status: 0, 1 for a request that fails, 2 for a command line that's wrong (an unknown
|
|
117
|
+
# command or option, more than one format, a format for what prints no resource, or an option for another
|
|
118
|
+
# command), or 130 when interrupted, as check-in --watch always is
|
|
119
|
+
# @example
|
|
120
|
+
# Sferik::CLI.new.run(["talks"]) # => 0
|
|
121
|
+
def run(argv)
|
|
122
|
+
dispatch(argv)
|
|
123
|
+
rescue OptionParser::ParseError => e
|
|
124
|
+
misuse(e)
|
|
125
|
+
rescue Errno::EPIPE # what reads the output (head, say) has all it wants
|
|
126
|
+
0
|
|
127
|
+
rescue Interrupt
|
|
128
|
+
130
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
# Run the command a command line names
|
|
134
|
+
#
|
|
135
|
+
# @api private
|
|
136
|
+
# @param argv [Array<String>] the command line
|
|
137
|
+
# @return [Integer] the exit status
|
|
138
|
+
# @raise [OptionParser::ParseError] if an option is unknown, or --host has no URL
|
|
139
|
+
def dispatch(argv)
|
|
140
|
+
options = {accept: []} #: Hash[Symbol, untyped]
|
|
141
|
+
command, extra = parser(options).permute(argv) # parse would take no option after the command with POSIXLY_CORRECT set
|
|
142
|
+
return misuse("unexpected argument: #{extra}") if extra
|
|
143
|
+
|
|
144
|
+
option, owner = OWNERS.find { |name, its| options.key?(name) && !its.eql?(command) }
|
|
145
|
+
option ? misuse("--#{option} is for #{owner}") : perform(command, options)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Do what a command line asks for
|
|
149
|
+
#
|
|
150
|
+
# That's to print a resource, or what an option or a command that prints none names.
|
|
151
|
+
#
|
|
152
|
+
# @api private
|
|
153
|
+
# @param command [String, nil] the command, or nil for the home page
|
|
154
|
+
# @param options [Hash{Symbol => Object}] the options of the command line
|
|
155
|
+
# @return [Integer] the exit status
|
|
156
|
+
def perform(command, options)
|
|
157
|
+
action = ACTIONS[options.fetch(:print, command)]
|
|
158
|
+
return unformatted(options) { __send__(*action, options) } if action
|
|
159
|
+
|
|
160
|
+
path = command ? COMMANDS[command] : "/"
|
|
161
|
+
path ? show(path, options) : misuse("unknown command: #{command}")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Print the usage
|
|
165
|
+
#
|
|
166
|
+
# @api private
|
|
167
|
+
# @param _options [Hash{Symbol => Object}] the options of the command line, which make no difference
|
|
168
|
+
# @return [Integer] the exit status
|
|
169
|
+
def usage(_options) = say(USAGE)
|
|
170
|
+
|
|
171
|
+
# Print the version
|
|
172
|
+
#
|
|
173
|
+
# @api private
|
|
174
|
+
# @param _options [Hash{Symbol => Object}] the options of the command line, which make no difference
|
|
175
|
+
# @return [Integer] the exit status
|
|
176
|
+
def version(_options) = say("#{VERSION}\n")
|
|
177
|
+
|
|
178
|
+
# What reads the options of a command line
|
|
179
|
+
#
|
|
180
|
+
# @api private
|
|
181
|
+
# @param options [Hash{Symbol => Object}] where to note the options: the media types to :accept, which it adds each
|
|
182
|
+
# format named to, the :host to ask, what to :print instead of a resource (:usage or :version), the :tty a
|
|
183
|
+
# message is from, the :token to check in with, and whether to :watch, which is to stay logged in
|
|
184
|
+
# @return [OptionParser] the parser
|
|
185
|
+
def parser(options)
|
|
186
|
+
OptionParser.new do |flags|
|
|
187
|
+
FORMATS.each { |flag, type| flags.on(flag) { options[:accept] |= [type] } }
|
|
188
|
+
VALUES.each { |flag, name| flags.on(flag) { |value| options[name] = value } }
|
|
189
|
+
flags.on("-h", "--help") { options[:print] = :usage }
|
|
190
|
+
flags.on("-v", "--version") { options[:print] = :version }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Print a resource of the site, as text or in the format an option names
|
|
195
|
+
#
|
|
196
|
+
# A PDF is printed as it is: Windows would otherwise write each of its line feeds as a carriage return and one.
|
|
197
|
+
# It isn't printed to a terminal, which its bytes would garble, and isn't asked for either, when that's where
|
|
198
|
+
# it would go. A resource comes in one format at a time, so options that name two are the command line's mistake.
|
|
199
|
+
#
|
|
200
|
+
# @api private
|
|
201
|
+
# @param path [String] the resource's path
|
|
202
|
+
# @param options [Hash{Symbol => Object}] the options of the command line
|
|
203
|
+
# @return [Integer] the exit status
|
|
204
|
+
def show(path, options)
|
|
205
|
+
return misuse("pick one format: --json, --pdf, --latex, or --vcard") if options.fetch(:accept).size > 1
|
|
206
|
+
|
|
207
|
+
accept = [*options.fetch(:accept), "text/plain"].first
|
|
208
|
+
ask(options) { |client| client.get(path, accept: accept.eql?(FORMATS.fetch("--pdf")) ? redirected(accept) : accept) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Do what takes no format, since it prints no resource of the site
|
|
212
|
+
#
|
|
213
|
+
# That's the usage, the version, sending a message, and checking in: an option that names a format for one of them is the
|
|
214
|
+
# command line's mistake, and nothing is done.
|
|
215
|
+
#
|
|
216
|
+
# @api private
|
|
217
|
+
# @param options [Hash{Symbol => Object}] the options of the command line
|
|
218
|
+
# @yield what to do, if no option names a format
|
|
219
|
+
# @yieldreturn [Integer] the exit status
|
|
220
|
+
# @return [Integer] the exit status
|
|
221
|
+
def unformatted(options) = options.fetch(:accept).any? ? misuse("--json, --pdf, --latex, and --vcard are for the commands that print a resource") : yield
|
|
222
|
+
|
|
223
|
+
# Send me the message that standard input has, and print what the server says
|
|
224
|
+
#
|
|
225
|
+
# A terminal is told how to end the message first, on standard error, so that the output is the server's alone.
|
|
226
|
+
# With --tty, the message says which terminal it's from.
|
|
227
|
+
#
|
|
228
|
+
# @api private
|
|
229
|
+
# @param options [Hash{Symbol => Object}] the options of the command line
|
|
230
|
+
# @return [Integer] the exit status
|
|
231
|
+
def write(options)
|
|
232
|
+
@err.print(PROMPT) if @input.tty?
|
|
233
|
+
ask(options) { |client| "#{client.write(@input.read, tty: options[:tty])}\n" }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Log in a terminal, as each browser tab on the site does, and print its name
|
|
237
|
+
#
|
|
238
|
+
# The name is what write's --tty takes. A terminal keeps it for as long as it checks in with the same token, at
|
|
239
|
+
# least every three minutes: without --token, each check-in is a new terminal's. With --watch, the command goes
|
|
240
|
+
# on checking it in, every minute, until it's interrupted.
|
|
241
|
+
#
|
|
242
|
+
# @api private
|
|
243
|
+
# @param options [Hash{Symbol => Object}] the options of the command line
|
|
244
|
+
# @return [Integer] the exit status: 1 if every terminal is taken
|
|
245
|
+
def check_in(options)
|
|
246
|
+
ask(options) { |client| options.key?(:watch) ? stay(client, token(options)) : named(client.check_in(token(options))) }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# The token a terminal checks in with
|
|
250
|
+
#
|
|
251
|
+
# @api private
|
|
252
|
+
# @param options [Hash{Symbol => Object}] the options of the command line
|
|
253
|
+
# @return [String] the one --token gives, or else a random one, which is a new terminal's
|
|
254
|
+
def token(options) = options.fetch(:token) { SecureRandom.uuid }
|
|
255
|
+
|
|
256
|
+
# Log in a terminal, print its name, and keep it logged in until interrupted
|
|
257
|
+
#
|
|
258
|
+
# The name is printed at once, and not kept for the end, which never comes: the client checks the terminal in
|
|
259
|
+
# again every minute for as long as its block runs, and the block sleeps until Ctrl-C wakes it. It's flushed,
|
|
260
|
+
# too, since what reads it (a script that wants the name for write's --tty) may be waiting for it.
|
|
261
|
+
#
|
|
262
|
+
# @api private
|
|
263
|
+
# @param client [Client] the client
|
|
264
|
+
# @param token [String] the terminal's token
|
|
265
|
+
# @return [String] nothing more to print, if the sleep ends without an interrupt
|
|
266
|
+
# @raise [Error] if every terminal is taken
|
|
267
|
+
# @raise [Interrupt] when the command is interrupted, which is how it ends
|
|
268
|
+
def stay(client, token) = client.check_in(token) { |who| say(named(who)) && @out.flush && Kernel.sleep }.then { "" }
|
|
269
|
+
|
|
270
|
+
# The name of the terminal that checked in, as a line
|
|
271
|
+
#
|
|
272
|
+
# @api private
|
|
273
|
+
# @param who [Who] what the site answered the check-in with
|
|
274
|
+
# @return [String] the terminal's name, and a newline
|
|
275
|
+
# @raise [Error] if every terminal is taken
|
|
276
|
+
def named(who) = who.you ? "#{who.you}\n" : raise(Error, "every terminal is taken: try again in a few minutes")
|
|
277
|
+
|
|
278
|
+
# Print what there is of the site in one format alone
|
|
279
|
+
#
|
|
280
|
+
# That's the feed of talks, the deployed version, the API's description, the motto, and where the account points to.
|
|
281
|
+
#
|
|
282
|
+
# @api private
|
|
283
|
+
# @param path [String] the path
|
|
284
|
+
# @param accept [String] the media type it comes as
|
|
285
|
+
# @param options [Hash{Symbol => Object}] the options of the command line
|
|
286
|
+
# @return [Integer] the exit status
|
|
287
|
+
def only(path, accept, options) = ask(options) { |client| client.get(path, accept:) }
|
|
288
|
+
|
|
289
|
+
# Ask the site for something, and print its response, or what went wrong
|
|
290
|
+
#
|
|
291
|
+
# A host that isn't an http or https URL is the command line's mistake, or the environment's, not a request that
|
|
292
|
+
# failed, so it has the exit status of one. Only the client being built is taken for that: an ArgumentError from
|
|
293
|
+
# the request, or from printing its response, is a bug, and is raised as one.
|
|
294
|
+
#
|
|
295
|
+
# @api private
|
|
296
|
+
# @param options [Hash{Symbol => Object}] the options of the command line
|
|
297
|
+
# @yield [client] the request to make
|
|
298
|
+
# @yieldparam client [Client] a client for the host the options or the environment name, or else the configured one
|
|
299
|
+
# @yieldreturn [String] the body of the response
|
|
300
|
+
# @return [Integer] the exit status: 0, 1 for a request that fails, or 2 for a host that isn't a URL
|
|
301
|
+
# @raise [ArgumentError] if the request, or printing its response, raises one
|
|
302
|
+
def ask(options)
|
|
303
|
+
maker = clients # which loads the library, before anything here asks it for its host, or for an error
|
|
304
|
+
named = @env.fetch("SFERIK_HOST", "") # one that's set but empty names no host, as if it weren't set
|
|
305
|
+
host = options.fetch(:host) { named.empty? ? Sferik.host : named }
|
|
306
|
+
client = maker.new(host:)
|
|
307
|
+
print_body(yield(client))
|
|
308
|
+
rescue Error, ArgumentError => e
|
|
309
|
+
raise if e.is_a?(ArgumentError) && client # only one from building the client is about the host
|
|
310
|
+
|
|
311
|
+
@err.puts("sferik: #{e}")
|
|
312
|
+
e.is_a?(Error) ? 1 : 2
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# What makes the client for a host, once the library is loaded
|
|
316
|
+
#
|
|
317
|
+
# @api private
|
|
318
|
+
# @return [#new] what the command was given to make clients with, or else the {Client} class
|
|
319
|
+
def clients = library.then { @client || Client }
|
|
320
|
+
|
|
321
|
+
# Load the library, when a command first asks the site for something
|
|
322
|
+
#
|
|
323
|
+
# Printing the usage or the version asks for nothing, and loading the client, with everything it's built of,
|
|
324
|
+
# takes about as long as the rest of either does: so only a command that needs it waits for it.
|
|
325
|
+
#
|
|
326
|
+
# @api private
|
|
327
|
+
# @return [Boolean] whether this is what loaded it: the client, its errors, and the configuration
|
|
328
|
+
def library = require_relative("../sferik")
|
|
329
|
+
|
|
330
|
+
# Print the body of a response: a binary one, as a PDF is, in binary mode
|
|
331
|
+
#
|
|
332
|
+
# @api private
|
|
333
|
+
# @param body [String] the body
|
|
334
|
+
# @return [Integer] the exit status
|
|
335
|
+
# @raise [Error] if the body is binary and the output is a terminal
|
|
336
|
+
def print_body(body)
|
|
337
|
+
redirected(@out).binmode if body.encoding.equal?(Encoding::BINARY)
|
|
338
|
+
say(body)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# What's for binary output, unless the output is a terminal: then this fails
|
|
342
|
+
#
|
|
343
|
+
# @api private
|
|
344
|
+
# @param binary [Object] what's for binary output: the media type to ask for it as, or where to print it
|
|
345
|
+
# @return [Object] what was given, if the output isn't a terminal
|
|
346
|
+
# @raise [Error] if it is
|
|
347
|
+
def redirected(binary) = @out.tty? ? raise(Error, "binary output would garble the terminal: redirect it to a file (sferik resume --pdf > resume.pdf)") : binary
|
|
348
|
+
|
|
349
|
+
# Print text
|
|
350
|
+
#
|
|
351
|
+
# @api private
|
|
352
|
+
# @param text [String] the text
|
|
353
|
+
# @return [Integer] the exit status: 0
|
|
354
|
+
def say(text)
|
|
355
|
+
@out.print(text)
|
|
356
|
+
0
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Say what's wrong with a command line, then the usage
|
|
360
|
+
#
|
|
361
|
+
# @api private
|
|
362
|
+
# @param problem [#to_s] what's wrong: a message, or an error with one
|
|
363
|
+
# @return [Integer] the exit status: 2, which tells a command line that's wrong from a request that fails
|
|
364
|
+
def misuse(problem)
|
|
365
|
+
@err.print("sferik: #{problem}\n\n#{USAGE}")
|
|
366
|
+
2
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|