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,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module User
7
+ class Password < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'input',
12
+ 'output',
13
+ 'screen',
14
+ 'prompt'
15
+ )
16
+
17
+ desc 'Rotate the password of the logged-in administrator.'
18
+
19
+ def call(*)
20
+ spinner = nil
21
+ response = api_manager.ensure_authenticated do
22
+ spinner = SimpleSpinnerBar.start('Retrieving user...', output)
23
+ r = api.me
24
+ if r.failure?
25
+ spinner.failure
26
+ end
27
+ r
28
+ end
29
+
30
+ if response.success?
31
+ spinner.success
32
+ else
33
+ output.puts
34
+ Output::ServerError.show(response, output, screen)
35
+ return CommandOutput.continue
36
+ end
37
+
38
+ output.puts
39
+ output.puts "This action will regenerate #{response.json['name']}'s password and display it once."
40
+ output.puts
41
+
42
+ if prompt.no?(Pl.red('Are you sure you want to continue?'))
43
+ output.puts 'Command aborted.'
44
+ return CommandOutput.continue
45
+ end
46
+
47
+ output.puts
48
+ spinner = nil
49
+ response = api_manager.ensure_authenticated do
50
+ spinner = SimpleSpinnerBar.start('Regenerating password...', output)
51
+ r = api.rotate_password
52
+ if r.failure?
53
+ spinner.failure
54
+ end
55
+ r
56
+ end
57
+
58
+ if response.success?
59
+ spinner.success("Password #{Pl.green('regenerated')}")
60
+ output.puts
61
+ output.puts Pl.yellow('New password: ') + response.json['password']
62
+ output.puts
63
+ output.print 'Copy the password and press any key when finished...'
64
+ input.gets
65
+ output.clear_screen
66
+ else
67
+ output.puts
68
+ Output::ServerError.show(response, output, screen)
69
+ end
70
+
71
+ CommandOutput.continue
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module User
7
+ class Show < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'output',
12
+ 'screen'
13
+ )
14
+
15
+ desc 'Display information about the logged-in administrator.'
16
+
17
+ def call(*)
18
+ response = api_manager.ensure_authenticated do
19
+ spinner = SimpleSpinnerBar.start('Retrieving user...', output)
20
+ r = api.me
21
+ if r.failure?
22
+ spinner.failure
23
+ else
24
+ spinner.success
25
+ end
26
+ r
27
+ end
28
+
29
+ if response.success?
30
+ output.puts
31
+ output.puts TTY::Table.new(rows: [
32
+ [Pl.bold('ID'), response.json['id']],
33
+ [Pl.bold('Name'), response.json['name']],
34
+ [Pl.bold('Username'), response.json['username']]
35
+ ]).render(:ascii)
36
+ else
37
+ output.puts
38
+ Output::ServerError.show(response, output, screen)
39
+ end
40
+
41
+ CommandOutput.continue
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module User
7
+ class Totp < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'input',
12
+ 'output',
13
+ 'screen',
14
+ 'prompt',
15
+ 'qr'
16
+ )
17
+
18
+ desc 'Rotate the TOTP secret of the logged-in administrator.'
19
+
20
+ def call(*)
21
+ spinner = nil
22
+ response = api_manager.ensure_authenticated do
23
+ spinner = SimpleSpinnerBar.start('Retrieving user...', output)
24
+ r = api.me
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
+ return CommandOutput.continue
37
+ end
38
+
39
+ output.puts
40
+ output.puts "This action will regenerate #{response.json['name']}'s TOTP secret and display the corresponding QR code once."
41
+ output.puts
42
+
43
+ if prompt.no?(Pl.red('Are you sure you want to continue?'))
44
+ output.puts 'Command aborted.'
45
+ return CommandOutput.continue
46
+ end
47
+
48
+ output.puts
49
+ response = api_manager.ensure_authenticated do
50
+ spinner = SimpleSpinnerBar.start('Regenerating TOTP...', output)
51
+ r = api.rotate_totp
52
+ if r.failure?
53
+ spinner.failure
54
+ end
55
+ r
56
+ end
57
+
58
+ if response.success?
59
+ spinner.success("TOTP #{Pl.green('regenerated')}")
60
+ output.puts
61
+ output.puts Pl.yellow('New TOTP code:')
62
+ qr.show(response.json['totpUrl'])
63
+ output.puts
64
+ output.print 'Scan the QR with your authenticator app and press any key when finished...'
65
+ input.gets
66
+ output.clear_screen
67
+ else
68
+ output.puts
69
+ Output::ServerError.show(response, output, screen)
70
+ end
71
+
72
+ CommandOutput.continue
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Users
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',
21
+ 'api_manager',
22
+ 'output',
23
+ 'screen',
24
+ 'prompt'
25
+ )
26
+
27
+ desc 'Change name or username of an administrator.'
28
+
29
+ argument :user, desc: 'The user id or username to change.'
30
+
31
+ field_option :name, type: :boolean, default: nil, desc: 'Whether to change the name'
32
+ field_option :username, type: :boolean, default: nil, desc: 'Whether to change the username'
33
+
34
+ option :all, type: :boolean, default: false, desc: 'Change all fields'
35
+
36
+ def call(user: nil, all:, **args)
37
+ field_args = args.slice(*self.class.field_options)
38
+
39
+ spinner = nil
40
+ response = api_manager.ensure_authenticated do
41
+ spinner = SimpleSpinnerBar.start('Retrieving users...', output)
42
+ r = api.users
43
+ if r.failure?
44
+ spinner.failure
45
+ else
46
+ spinner.success
47
+ end
48
+ r
49
+ end
50
+
51
+ if response.failure?
52
+ output.puts
53
+ Output::ServerError.show(response, output, screen)
54
+ return
55
+ end
56
+
57
+ users = response.json
58
+
59
+ unless user
60
+ choices = users.map { |u| { name: "#{u['name']} (#{u['username']})", value: u['id'] } }
61
+ user = prompt.select('Which user would you like to change?', choices)
62
+ end
63
+
64
+ user = users.find { |u| u['id'] === user || u['username'].downcase === user.to_s.downcase }
65
+ unless user
66
+ output.puts(Pl.yellow('That user does not exist.'))
67
+ return CommandOutput.continue
68
+ end
69
+
70
+ fields = nil
71
+
72
+ if all == false && field_args.empty?
73
+ fields = prompt
74
+ .multi_select('Which fields do you want to change?', self.class.field_options)
75
+ .map { |n| [n, true] }
76
+ .to_h
77
+ else
78
+ begin
79
+ fields = Util::Cli.analyze_fields_flags(all, self.class.field_options, field_args)
80
+ rescue Util::Cli::FieldsFlagsError => e
81
+ output.puts Pl.yellow(e.message)
82
+ return CommandOutput.continue
83
+ end
84
+ end
85
+
86
+ payload = {}
87
+
88
+ if fields[:name]
89
+ name = prompt.ask('Name', default: user['name'])
90
+ payload['name'] = name if name != user['name']
91
+ end
92
+
93
+ if fields[:username]
94
+ username = prompt.ask('Username', default: user['username'])
95
+ payload['username'] = username if username != user['username']
96
+ end
97
+
98
+ if payload.empty?
99
+ output.puts Pl.yellow('Nothing to change!')
100
+ return CommandOutput.continue
101
+ end
102
+
103
+ response = api_manager.ensure_authenticated do
104
+ spinner = SimpleSpinnerBar.start('Changing user...', output)
105
+ r = api.change_user(user['id'], payload)
106
+ if r.failure?
107
+ spinner.failure
108
+ end
109
+ r
110
+ end
111
+
112
+ if response.success?
113
+ spinner.success("User #{Pl.green('changed')}")
114
+
115
+ output.puts
116
+ output.puts TTY::Table.new(rows: [
117
+ [Pl.bold('ID'), response.json['id']],
118
+ [Pl.bold('Name'), response.json['name']],
119
+ [Pl.bold('Username'), response.json['username']]
120
+ ]).render(:ascii)
121
+ else
122
+ spinner.failure
123
+
124
+ output.puts
125
+ Output::ServerError.show(response, output, screen)
126
+ end
127
+
128
+ CommandOutput.continue
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Users
7
+ class Create < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'input',
12
+ 'output',
13
+ 'screen',
14
+ 'prompt'
15
+ )
16
+
17
+ desc 'Create a new administrator.'
18
+
19
+ def call(**args)
20
+ payload = {}
21
+
22
+ name = prompt.ask('Name')
23
+ payload['name'] = name
24
+
25
+ username = prompt.ask('Username')
26
+ payload['username'] = username
27
+
28
+ spinner = nil
29
+ response = api_manager.ensure_authenticated do
30
+ spinner = SimpleSpinnerBar.start('Creating user...', output)
31
+ r = api.create_user(payload)
32
+ if r.failure?
33
+ spinner.failure
34
+ end
35
+ r
36
+ end
37
+
38
+ if response.success?
39
+ spinner.success("User #{Pl.green('created')}")
40
+
41
+ output.puts
42
+ output.puts Pl.yellow('New password: ') + response.json['password']
43
+ output.puts
44
+ output.print 'Copy the password and press any key when finished...'
45
+ input.gets
46
+ output.clear_screen
47
+
48
+ output.puts
49
+ output.puts TTY::Table.new(rows: [
50
+ [Pl.bold('ID'), response.json['id']],
51
+ [Pl.bold('Name'), response.json['name']],
52
+ [Pl.bold('Username'), response.json['username']]
53
+ ]).render(:ascii)
54
+ else
55
+ spinner.failure
56
+
57
+ output.puts
58
+ Output::ServerError.show(response, output, screen)
59
+ end
60
+
61
+ CommandOutput.continue
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Users
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 admin users.'
16
+
17
+ def call(*)
18
+ spinner = nil
19
+ response = api_manager.ensure_authenticated do
20
+ spinner = SimpleSpinnerBar.start('Retrieving users...', output)
21
+ r = api.users
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('Username'), Pl.bold('Name')
36
+ ],
37
+ rows: response.json.map { |u| [u['id'], u['username'], 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,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Users
7
+ class Remove < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'input',
12
+ 'output',
13
+ 'screen',
14
+ 'prompt'
15
+ )
16
+
17
+ desc 'Remove an administrator.'
18
+
19
+ argument :user, desc: 'The user to delete.'
20
+
21
+ def call(user: nil, **args)
22
+ spinner = nil
23
+ response = api_manager.ensure_authenticated do
24
+ spinner = SimpleSpinnerBar.start('Retrieving users...', output)
25
+ r = api.users
26
+ if r.failure?
27
+ spinner.failure
28
+ else
29
+ spinner.success
30
+ end
31
+ r
32
+ end
33
+
34
+ if response.failure?
35
+ output.puts
36
+ Output::ServerError.show(response, output, screen)
37
+ return
38
+ end
39
+
40
+ users = response.json
41
+
42
+ unless user
43
+ choices = users.map { |u| { name: "#{u['name']} (#{u['username']})", value: u['id'] } }
44
+ user = prompt.select('Which user would you like to remove?', choices)
45
+ end
46
+
47
+ user = users.find { |u| u['id'] === user || u['username'].downcase === user.to_s.downcase }
48
+ unless user
49
+ output.puts(Pl.yellow('That user does not exist.'))
50
+ return CommandOutput.continue
51
+ end
52
+
53
+ output.puts
54
+ output.puts "This action will permanently remove #{user['name']} as an administrator."
55
+ output.puts
56
+
57
+ if prompt.no?(Pl.red('Are you sure you want to continue?'))
58
+ output.puts 'Command aborted.'
59
+ return CommandOutput.continue
60
+ end
61
+
62
+ spinner = nil
63
+ response = api_manager.ensure_authenticated do
64
+ spinner = SimpleSpinnerBar.start('Removing user...', output)
65
+ r = api.remove_user(user['id'])
66
+ if r.failure?
67
+ spinner.failure
68
+ end
69
+ r
70
+ end
71
+
72
+ if response.success?
73
+ spinner.success("User #{Pl.green('removed')}")
74
+ else
75
+ output.puts
76
+ Output::ServerError.show(response, output, screen)
77
+ end
78
+
79
+ CommandOutput.continue
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Prompt
6
+ delegate_missing_to :base
7
+
8
+ def initialize(input:, output:)
9
+ @base = TTY::Prompt.new(input: input, output: output)
10
+ end
11
+
12
+ def self.dependencies
13
+ %w[input output]
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :base
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Qr
6
+ include Depends.on('cmd', 'output', 'which')
7
+
8
+ def show(content)
9
+ if which.exist?('qr')
10
+ out, err = cmd.new(pty: true, printer: :null).run(" qr '#{content}'")
11
+ output.puts out
12
+ else
13
+ output.puts RQRCode::QRCode.new(code).as_ansi
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Steps
6
+ class Authenticate < Step
7
+ include Depends.on('authenticate')
8
+
9
+ spaced
10
+
11
+ def run(_prev)
12
+ authenticate.run ? success : failure
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Steps
6
+ class Connect < Step
7
+ include Depends.on(
8
+ 'config.endpoint',
9
+ 'api',
10
+ 'output',
11
+ 'screen'
12
+ )
13
+
14
+ spaced
15
+
16
+ def run(_prev)
17
+ spinner = SimpleSpinnerBar.start("Connecting to #{endpoint}", output)
18
+
19
+ response = api.info
20
+
21
+ if response.success?
22
+ v = response.json['version']
23
+ spinner.success("#{Pl.green('Connected')} to #{endpoint}, #{Pl.yellow("v#{v}")}")
24
+ success
25
+ else
26
+ spinner.failure
27
+ output.puts
28
+ Output::ServerError.show(response, output, screen)
29
+ failure
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Steps
6
+ class Greeting < Step
7
+ include Depends.on('output')
8
+
9
+ spaced
10
+
11
+ def run(_prev)
12
+ output.puts "The Ponsqb Admin CLI #{Pl.green("v#{Pcli::VERSION}")}, at your service!"
13
+ success
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end