pcli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +13 -0
  3. data/CHANGELOG.md +5 -0
  4. data/Gemfile +12 -0
  5. data/Gemfile.lock +176 -0
  6. data/Rakefile +12 -0
  7. data/exe/pcli +6 -0
  8. data/lib/pcli/api_request.rb +60 -0
  9. data/lib/pcli/api_response.rb +82 -0
  10. data/lib/pcli/command_output.rb +23 -0
  11. data/lib/pcli/container.rb +54 -0
  12. data/lib/pcli/depends.rb +43 -0
  13. data/lib/pcli/main.rb +22 -0
  14. data/lib/pcli/options.rb +41 -0
  15. data/lib/pcli/output/padded.rb +32 -0
  16. data/lib/pcli/output/server_error.rb +24 -0
  17. data/lib/pcli/output.rb +6 -0
  18. data/lib/pcli/pl.rb +15 -0
  19. data/lib/pcli/services/all_commands.rb +52 -0
  20. data/lib/pcli/services/all_steps.rb +23 -0
  21. data/lib/pcli/services/api.rb +82 -0
  22. data/lib/pcli/services/api_manager.rb +28 -0
  23. data/lib/pcli/services/app.rb +31 -0
  24. data/lib/pcli/services/authenticate.rb +60 -0
  25. data/lib/pcli/services/client.rb +24 -0
  26. data/lib/pcli/services/cmd.rb +13 -0
  27. data/lib/pcli/services/commander.rb +38 -0
  28. data/lib/pcli/services/commands/login.rb +22 -0
  29. data/lib/pcli/services/commands/logout.rb +24 -0
  30. data/lib/pcli/services/commands/templates/change.rb +95 -0
  31. data/lib/pcli/services/commands/templates/list.rb +52 -0
  32. data/lib/pcli/services/commands/user/change.rb +114 -0
  33. data/lib/pcli/services/commands/user/password.rb +77 -0
  34. data/lib/pcli/services/commands/user/show.rb +47 -0
  35. data/lib/pcli/services/commands/user/totp.rb +78 -0
  36. data/lib/pcli/services/commands/users/change.rb +134 -0
  37. data/lib/pcli/services/commands/users/create.rb +67 -0
  38. data/lib/pcli/services/commands/users/list.rb +52 -0
  39. data/lib/pcli/services/commands/users/remove.rb +85 -0
  40. data/lib/pcli/services/prompt.rb +21 -0
  41. data/lib/pcli/services/qr.rb +18 -0
  42. data/lib/pcli/services/steps/authenticate.rb +17 -0
  43. data/lib/pcli/services/steps/connect.rb +35 -0
  44. data/lib/pcli/services/steps/greeting.rb +18 -0
  45. data/lib/pcli/services/steps/main.rb +51 -0
  46. data/lib/pcli/services/steps/valediction.rb +21 -0
  47. data/lib/pcli/simple_spinner_bar.rb +38 -0
  48. data/lib/pcli/step.rb +69 -0
  49. data/lib/pcli/util/cli.rb +32 -0
  50. data/lib/pcli/util/hash.rb +25 -0
  51. data/lib/pcli/version.rb +5 -0
  52. data/lib/pcli.rb +24 -0
  53. metadata +389 -0
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class AllCommands
6
+ include Depends.on(
7
+ user_show: 'commands.user.show',
8
+ user_change: 'commands.user.change',
9
+ user_password: 'commands.user.password',
10
+ user_totp: 'commands.user.totp',
11
+ users_list: 'commands.users.list',
12
+ users_create: 'commands.users.create',
13
+ users_change: 'commands.users.change',
14
+ users_remove: 'commands.users.remove',
15
+ templates_list: 'commands.templates.list',
16
+ templates_change: 'commands.templates.change',
17
+ login: 'commands.login',
18
+ logout: 'commands.logout',
19
+ )
20
+
21
+ def registry
22
+ me = self
23
+
24
+ Module.new do
25
+ extend Dry::CLI::Registry
26
+
27
+ register 'user', aliases: %w[me myself profile] do |p|
28
+ p.register 'show', me.user_show, aliases: %w[sh view]
29
+ p.register 'change', me.user_change, aliases: %w[ch chng update up modify mod]
30
+ p.register 'password', me.user_password, aliases: %w[passwd pass pwd pw]
31
+ p.register 'totp', me.user_totp, aliases: %w[code otp]
32
+ end
33
+
34
+ register 'users', aliases: %w[admins u] do |p|
35
+ p.register 'list', me.users_list, aliases: %w[l all index]
36
+ p.register 'change', me.users_change, aliases: %w[ch chng update up modify mod]
37
+ p.register 'create', me.users_create, aliases: %w[make new n]
38
+ p.register 'remove', me.users_remove, aliases: %w[rem r delete del d destroy]
39
+ end
40
+
41
+ register 'templates', aliases: %w[temps temp tmp t] do |p|
42
+ p.register 'list', me.templates_list, aliases: %w[l all index]
43
+ p.register 'change', me.templates_change, aliases: %w[ch chng update up modify mod]
44
+ end
45
+
46
+ register 'login', me.login, aliases: %w[signin authenticate auth]
47
+ register 'logout', me.logout, aliases: %w[signout deauthenticate deauth unauthenticate unauth]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class AllSteps
6
+ include Depends.on(
7
+ 'steps.greeting',
8
+ 'steps.connect',
9
+ 'steps.main',
10
+ 'steps.valediction'
11
+ )
12
+
13
+ def all
14
+ [
15
+ greeting,
16
+ connect,
17
+ main,
18
+ valediction
19
+ ]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Api
6
+ include Depends.on('client')
7
+ attr_accessor :token
8
+
9
+ def send(request)
10
+ client.send(request)
11
+ end
12
+
13
+ def info
14
+ send ApiRequest.new('info')
15
+ end
16
+
17
+ def auth(username, password, totp)
18
+ send ApiRequest.new('admin/auth')
19
+ .method(:post)
20
+ .params(
21
+ username: username,
22
+ password: password,
23
+ totp: totp
24
+ )
25
+ end
26
+
27
+ def me
28
+ send authenticated(ApiRequest.new('admin/me'))
29
+ end
30
+
31
+ def change_me(payload)
32
+ send authenticated(ApiRequest.new('admin/me')
33
+ .method(:patch)
34
+ .params(payload))
35
+ end
36
+
37
+ def rotate_password
38
+ send authenticated(ApiRequest.new('admin/me/password').method(:post))
39
+ end
40
+
41
+ def rotate_totp
42
+ send authenticated(ApiRequest.new('admin/me/totp').method(:post))
43
+ end
44
+
45
+ def users
46
+ send authenticated(ApiRequest.new('admin/admins'))
47
+ end
48
+
49
+ def change_user(id, payload)
50
+ send authenticated(ApiRequest.new("admin/admins/#{id}"))
51
+ .method(:patch)
52
+ .params(payload)
53
+ end
54
+
55
+ def create_user(payload)
56
+ send authenticated(ApiRequest.new('admin/admins'))
57
+ .method(:post)
58
+ .params(payload)
59
+ end
60
+
61
+ def remove_user(id)
62
+ send authenticated(ApiRequest.new("admin/admins/#{id}")).method(:delete)
63
+ end
64
+
65
+ def templates
66
+ send authenticated(ApiRequest.new('admin/templates'))
67
+ end
68
+
69
+ def change_template(id, payload)
70
+ send authenticated(ApiRequest.new("admin/templates/#{id}"))
71
+ .method(:patch)
72
+ .params(payload)
73
+ end
74
+
75
+ private
76
+
77
+ def authenticated(request)
78
+ request.header('Authorization', "Bearer #{token}")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class ApiManager
6
+ include Depends.on('output', 'authenticate', 'api')
7
+
8
+ def ensure_authenticated(&block)
9
+ unless api.token
10
+ return ensure_authenticated(&block) if authenticate.run(true)
11
+ end
12
+
13
+ response = block.call
14
+
15
+ return if response === false
16
+
17
+ if response.failure? && response.known_error? && response.error.type == 'unauthenticated'
18
+ output.puts
19
+ output.puts(Pl.dim("You've been logged out. Please log in again."))
20
+ output.puts
21
+ return ensure_authenticated(&block) if authenticate.run(false)
22
+ else
23
+ response
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class App
6
+ include Depends.on(
7
+ 'output',
8
+ steps: 'all_steps'
9
+ )
10
+
11
+ def run
12
+ result = nil
13
+ prev_space = false
14
+ steps.all.each.with_index do |step, i|
15
+ break if result&.halt? && !step.ensured?
16
+
17
+ output.puts if i.positive? && step.spaced? && !prev_space
18
+
19
+ result = step.run(result)
20
+
21
+ if i < steps.all.count - 1 && step.spaced?
22
+ prev_space = true
23
+ output.puts
24
+ else
25
+ prev_space = false
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Authenticate
6
+ include ActionView::Helpers::DateHelper
7
+
8
+ include Depends.on(
9
+ 'output', 'prompt', 'api', 'screen'
10
+ )
11
+
12
+ def run(should_prompt = true)
13
+ if should_prompt
14
+ output.puts Pl.dim('Please login.')
15
+ output.puts
16
+ end
17
+
18
+ username = prompt.ask('Username:')
19
+ password = prompt.mask('Password:')
20
+ totp = prompt.ask('TOTP Code:')
21
+ output.puts
22
+
23
+ spinner = SimpleSpinnerBar.start('Authenticating...', output)
24
+ response = api.auth(username, password, totp)
25
+
26
+ if response.success?
27
+ api.token = response.json['token']
28
+ expires_at = DateTime.parse(response.json['expiresAt'])
29
+ time = distance_of_time_in_words(DateTime.now, expires_at)
30
+
31
+ spinner.success("#{Pl.green('Authenticated')} for the next #{Pl.yellow(time)}")
32
+ true
33
+ else
34
+ spinner.failure
35
+
36
+ output.puts
37
+ if response.known_error?
38
+ if response.error.type == 'invalid_credentials'
39
+ output.puts Pl.yellow('Invalid credentials!')
40
+ output.puts
41
+ return run(false)
42
+ end
43
+
44
+ puts 'Server:'
45
+ Output::Padded.show([
46
+ "#{Pl.yellow(response.error.title)} #{Pl.dim("(#{response.error.status})")}",
47
+ '',
48
+ response.error.message
49
+ ], output, screen)
50
+ else
51
+ output.puts "Server #{Pl.yellow("(#{response.code})")}:"
52
+ output.puts Output::Padded.show(response.body, output, screen)
53
+ end
54
+
55
+ false
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Client
6
+ include Depends.on('config.endpoint')
7
+
8
+ def send(request)
9
+ r = HTTP.send(
10
+ request.method,
11
+ URI.join(endpoint, request.path),
12
+ body: request.params.to_json,
13
+ headers: request.headers
14
+ )
15
+ ApiResponse.new(
16
+ r.code,
17
+ r.status.success?,
18
+ r.status.reason,
19
+ r.to_s
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Cmd
6
+ include Depends.on('output')
7
+
8
+ def new(**options)
9
+ TTY::Command.new(output: output, **options)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class CLI
5
+ def exit(code); end
6
+
7
+ def perform_registry(arguments)
8
+ result = registry.get(arguments)
9
+ return usage(result) unless result.found?
10
+
11
+ command, args = parse(result.command, result.arguments, result.names)
12
+
13
+ result.before_callbacks.run(command, args)
14
+ output = command.call(**args)
15
+ result.after_callbacks.run(command, args)
16
+
17
+ output
18
+ end
19
+ end
20
+ end
21
+
22
+ module Pcli
23
+ module Services
24
+ class Commander
25
+ include Depends.on('output', commands: 'all_commands')
26
+
27
+ def initialize(**args)
28
+ super
29
+
30
+ @cli = Dry::CLI.new(commands.registry)
31
+ end
32
+
33
+ def run(cmd)
34
+ @cli.call(arguments: cmd.split, out: output, err: output)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ module Pcli
2
+ module Services
3
+ module Commands
4
+ class Login < Dry::CLI::Command
5
+ include Depends.on('authenticate', 'api', 'prompt', 'output')
6
+
7
+ desc 'Log in.'
8
+
9
+ def call(*)
10
+ if api.token && !prompt.yes?('You are already logged in. Would you like to log out?')
11
+ output.puts('Aborted.')
12
+ return
13
+ end
14
+
15
+ authenticate.run(true)
16
+
17
+ CommandOutput.continue
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module Pcli
2
+ module Services
3
+ module Commands
4
+ class Logout < Dry::CLI::Command
5
+ include Depends.on('authenticate', 'api', 'output')
6
+
7
+ desc 'Log out.'
8
+
9
+ def call(*)
10
+ unless api.token
11
+ output.puts('You are not logged in.')
12
+ return
13
+ end
14
+
15
+ api.token = nil
16
+
17
+ output.puts(Pl.green('Logged out.'))
18
+
19
+ CommandOutput.continue
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Templates
7
+ class Change < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'output',
12
+ 'screen',
13
+ 'editor',
14
+ 'prompt'
15
+ )
16
+
17
+ desc 'Change or view a template.'
18
+
19
+ argument :template, desc: 'The template id to change.'
20
+
21
+ def call(template: nil, **args)
22
+ response = api_manager.ensure_authenticated do
23
+ spinner = SimpleSpinnerBar.start('Retrieving templates...', output)
24
+ r = api.templates
25
+ if r.failure?
26
+ spinner.failure
27
+ else
28
+ spinner.success
29
+ end
30
+ r
31
+ end
32
+
33
+ if response.failure?
34
+ output.puts
35
+ Output::ServerError.show(response, output, screen)
36
+ end
37
+
38
+ templates = response.json
39
+
40
+ unless template
41
+ choices = templates.map { |t| { name: "#{t['name']} (#{t['id']})", value: t['id'] } }
42
+ template = prompt.select('Which template would you like to view/change?', choices)
43
+ end
44
+
45
+ template = templates.find { |t| t['id'] === template }
46
+ unless template
47
+ output.puts(Pl.yellow('That template does not exist.'))
48
+ return CommandOutput.continue
49
+ end
50
+
51
+ file = Tempfile.new('pcli_open_template')
52
+ file.write(template['templateXML'])
53
+ file.close
54
+
55
+ unless editor.open(file.path)
56
+ output.puts('Aborted.')
57
+ return CommandOutput.continue
58
+ end
59
+
60
+ file.open
61
+ new_template = file.read
62
+
63
+ if template['templateXML'] == new_template
64
+ output.puts('Nothing to change.')
65
+ return CommandOutput.continue
66
+ end
67
+
68
+ unless prompt.yes?('Save changes?')
69
+ output.puts('Aborted.')
70
+ return CommandOutput.continue
71
+ end
72
+
73
+ response = api_manager.ensure_authenticated do
74
+ spinner = SimpleSpinnerBar.start('Changing template...', output)
75
+ r = api.change_template(template['id'], { 'templateXML' => new_template })
76
+ if r.failure?
77
+ spinner.failure
78
+ else
79
+ spinner.success("Template #{Pl.green('changed')}")
80
+ end
81
+ r
82
+ end
83
+
84
+ if response.failure?
85
+ output.puts
86
+ Output::ServerError.show(response, output, screen)
87
+ end
88
+
89
+ CommandOutput.continue
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Templates
7
+ class List < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'output',
12
+ 'screen'
13
+ )
14
+
15
+ desc 'Get a list of all templates.'
16
+
17
+ def call(*)
18
+ spinner = nil
19
+ response = api_manager.ensure_authenticated do
20
+ spinner = SimpleSpinnerBar.start('Retrieving templates...', output)
21
+ r = api.templates
22
+ if r.failure?
23
+ spinner.failure
24
+ else
25
+ spinner.success
26
+ end
27
+ r
28
+ end
29
+
30
+ if response.success?
31
+ spinner.success
32
+ output.puts
33
+ output.puts TTY::Table.new(
34
+ header: [
35
+ Pl.bold('ID'), Pl.bold('Name')
36
+ ],
37
+ rows: response.json.map { |u| [u['id'], u['name']] }
38
+ ).render(:ascii)
39
+ else
40
+ spinner.failure
41
+
42
+ output.puts
43
+ Output::ServerError.show(response, output, screen)
44
+ end
45
+
46
+ CommandOutput.continue
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module User
7
+ class Change < Dry::CLI::Command
8
+ @field_options = []
9
+
10
+ class << self
11
+ attr_reader :field_options
12
+
13
+ def field_option(name, *args, **kw_args)
14
+ @field_options << name
15
+ option name, *args, **kw_args
16
+ end
17
+ end
18
+
19
+ include Depends.on(
20
+ 'api_manager',
21
+ 'api',
22
+ 'output',
23
+ 'screen',
24
+ 'prompt'
25
+ )
26
+
27
+ desc 'Change name or username of the logged-in administrator.'
28
+
29
+ field_option :name, type: :boolean, default: nil, desc: 'Whether to change the name'
30
+ field_option :username, type: :boolean, default: nil, desc: 'Whether to change the username'
31
+ option :all, type: :boolean, default: false, desc: 'Change all fields'
32
+
33
+ def call(all:, **args)
34
+ field_args = args.slice(*self.class.field_options)
35
+
36
+ response = api_manager.ensure_authenticated do
37
+ spinner = SimpleSpinnerBar.start('Retrieving user...', output)
38
+ r = api.me
39
+ if r.failure?
40
+ spinner.failure
41
+ else
42
+ spinner.success
43
+ end
44
+ r
45
+ end
46
+
47
+ if response.success?
48
+ spinner.success
49
+ else
50
+ spinner.failure
51
+
52
+ output.puts
53
+ Output::ServerError.show(response, output, screen)
54
+ end
55
+
56
+ fields = nil
57
+
58
+ if all == false && field_args.empty?
59
+ fields = prompt
60
+ .multi_select('Which fields do you want to change?', self.class.field_options)
61
+ .map { |n| [n, true] }
62
+ .to_h
63
+ else
64
+ begin
65
+ fields = Util::Cli.analyze_fields_flags(all, self.class.field_options, field_args)
66
+ rescue Util::Cli::FieldsFlagsError => e
67
+ output.puts Pl.yellow(e.message)
68
+ return CommandOutput.continue
69
+ end
70
+ end
71
+
72
+ payload = {}
73
+
74
+ if fields[:name]
75
+ name = prompt.ask('Name', default: response.json['name'])
76
+ payload['name'] = name if name != response.json['name']
77
+ end
78
+
79
+ if fields[:username]
80
+ username = prompt.ask('Username', default: response.json['username'])
81
+ payload['username'] = username if username != response.json['username']
82
+ end
83
+
84
+ if payload.empty?
85
+ output.puts Pl.yellow('Nothing to change!')
86
+ return CommandOutput.continue
87
+ end
88
+
89
+ spinner = SimpleSpinnerBar.start('Changing user...', output)
90
+ response = api_manager.ensure_authenticated { api.change_me(payload) }
91
+
92
+ if response.success?
93
+ spinner.success("User #{Pl.green('changed')}")
94
+
95
+ output.puts
96
+ output.puts TTY::Table.new(rows: [
97
+ [Pl.bold('ID'), response.json['id']],
98
+ [Pl.bold('Name'), response.json['name']],
99
+ [Pl.bold('Username'), response.json['username']]
100
+ ]).render(:ascii)
101
+ else
102
+ spinner.failure
103
+
104
+ output.puts
105
+ Output::ServerError.show(response, output, screen)
106
+ end
107
+
108
+ CommandOutput.continue
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end