easy_creds 1.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/CHANGELOG.md +51 -0
- data/LICENSE.txt +21 -0
- data/README.md +165 -0
- data/exe/easy_creds +6 -0
- data/lib/easy_creds/actions/edit.rb +156 -0
- data/lib/easy_creds/actions/editor_edit.rb +70 -0
- data/lib/easy_creds/actions/init.rb +93 -0
- data/lib/easy_creds/actions/local/delete.rb +44 -0
- data/lib/easy_creds/actions/local/edit.rb +13 -0
- data/lib/easy_creds/actions/local/editor_edit.rb +13 -0
- data/lib/easy_creds/actions/local/init.rb +32 -0
- data/lib/easy_creds/actions/local/status.rb +49 -0
- data/lib/easy_creds/actions/local/sync_key.rb +63 -0
- data/lib/easy_creds/actions/local.rb +31 -0
- data/lib/easy_creds/actions/pull.rb +142 -0
- data/lib/easy_creds/actions/push.rb +149 -0
- data/lib/easy_creds/actions/status.rb +82 -0
- data/lib/easy_creds/cli.rb +147 -0
- data/lib/easy_creds/config.rb +52 -0
- data/lib/easy_creds/configuration.rb +91 -0
- data/lib/easy_creds/credentials_io.rb +128 -0
- data/lib/easy_creds/differ.rb +41 -0
- data/lib/easy_creds/doctor.rb +100 -0
- data/lib/easy_creds/env_picker.rb +24 -0
- data/lib/easy_creds/flatten.rb +25 -0
- data/lib/easy_creds/generators.rb +113 -0
- data/lib/easy_creds/init_state.rb +65 -0
- data/lib/easy_creds/installer.rb +55 -0
- data/lib/easy_creds/onboarding/gem_setup.rb +98 -0
- data/lib/easy_creds/onboarding/project_wizard.rb +106 -0
- data/lib/easy_creds/onboarding/register_prompt.rb +47 -0
- data/lib/easy_creds/onboarding/runner.rb +17 -0
- data/lib/easy_creds/onboarding/template_picker.rb +36 -0
- data/lib/easy_creds/overlay.rb +71 -0
- data/lib/easy_creds/project.rb +74 -0
- data/lib/easy_creds/projects/registry.rb +135 -0
- data/lib/easy_creds/provider.rb +30 -0
- data/lib/easy_creds/providers/base.rb +25 -0
- data/lib/easy_creds/providers/one_password.rb +187 -0
- data/lib/easy_creds/railtie.rb +10 -0
- data/lib/easy_creds/templates/files/default-beastmode.yml +33 -0
- data/lib/easy_creds/templates/files/microservice-minimal.yml +7 -0
- data/lib/easy_creds/templates/files/rails-api.yml +20 -0
- data/lib/easy_creds/templates/files/rails-fullstack.yml +37 -0
- data/lib/easy_creds/templates/registry.rb +62 -0
- data/lib/easy_creds/templates/renderer.rb +37 -0
- data/lib/easy_creds/theme.rb +129 -0
- data/lib/easy_creds/thor_cli.rb +299 -0
- data/lib/easy_creds/vault_picker.rb +56 -0
- data/lib/easy_creds/version.rb +5 -0
- data/lib/easy_creds/views/diff_table.rb +36 -0
- data/lib/easy_creds/views/header.rb +40 -0
- data/lib/easy_creds/views/init_dispatch.rb +132 -0
- data/lib/easy_creds/views/init_tree.rb +250 -0
- data/lib/easy_creds/views/local_menu.rb +38 -0
- data/lib/easy_creds/views/menu.rb +55 -0
- data/lib/easy_creds/views/project_picker.rb +56 -0
- data/lib/easy_creds/views/settings_menu.rb +108 -0
- data/lib/easy_creds/views/templates_menu.rb +142 -0
- data/lib/easy_creds/views/welcome_screen.rb +131 -0
- data/lib/easy_creds.rb +54 -0
- data/lib/tasks/easy_creds.rake +23 -0
- metadata +292 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'shellwords'
|
|
6
|
+
require 'tty-screen'
|
|
7
|
+
|
|
8
|
+
module EasyCreds
|
|
9
|
+
class ThorCLI < Thor
|
|
10
|
+
def self.exit_on_failure? = true
|
|
11
|
+
|
|
12
|
+
# No-args invocation → styled welcome instead of plain Thor help.
|
|
13
|
+
def self.start(given_args = ARGV, config = {})
|
|
14
|
+
given_args.empty? ? new.send(:welcome) : super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc 'sync [ENV]', 'Interactive TUI to sync credentials with 1Password'
|
|
18
|
+
method_option :vault, type: :string, desc: 'Override vault name'
|
|
19
|
+
method_option :'no-register', type: :boolean, default: false,
|
|
20
|
+
desc: 'Skip the auto-register prompt for unregistered projects'
|
|
21
|
+
def sync(env = nil)
|
|
22
|
+
EasyCreds.load_global_config!
|
|
23
|
+
EasyCreds.ensure_default_template!
|
|
24
|
+
check_first_run!
|
|
25
|
+
|
|
26
|
+
EasyCreds.configure { |c| c.default_vault = options[:vault] } if options[:vault]
|
|
27
|
+
|
|
28
|
+
registry = Projects::Registry.load
|
|
29
|
+
project = EasyCreds::Project.detect(Dir.pwd)
|
|
30
|
+
|
|
31
|
+
if (known = registry.find_by_path(project.root))
|
|
32
|
+
registry.touch(known)
|
|
33
|
+
return EasyCreds::CLI.start(project: project, default_env: env)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if looks_like_project?(project) && !skip_register?
|
|
37
|
+
result = Onboarding::RegisterPrompt.run(
|
|
38
|
+
prompt: build_prompt, project: project, registry: registry
|
|
39
|
+
)
|
|
40
|
+
return if result == :quit
|
|
41
|
+
|
|
42
|
+
return EasyCreds::CLI.start(project: project, default_env: env)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
outside_picker_loop(registry)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
desc 'init', 'First-time gem setup (global config + vault selection)'
|
|
49
|
+
method_option :'global-dir', type: :string, desc: 'Path to global easy_creds dir'
|
|
50
|
+
def init
|
|
51
|
+
EasyCreds.configure { |c| c.global_dir = options[:'global-dir'] } if options[:'global-dir']
|
|
52
|
+
prompt = build_prompt
|
|
53
|
+
EasyCreds::Onboarding::GemSetup.run(prompt: prompt)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc 'onboard', 'Set up example.yml for the current project'
|
|
57
|
+
method_option :template, type: :string, desc: 'Template name to use'
|
|
58
|
+
method_option :force, type: :boolean, default: false, desc: 'Overwrite existing example.yml'
|
|
59
|
+
def onboard
|
|
60
|
+
EasyCreds.load_global_config!
|
|
61
|
+
project = EasyCreds::Project.detect(Dir.pwd)
|
|
62
|
+
EasyCreds::Onboarding::Runner.start(project: project, template: options[:template], force: options[:force])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
desc 'doctor', 'Check gem health (op CLI, auth, project detection)'
|
|
66
|
+
method_option :verbose, type: :boolean, default: false
|
|
67
|
+
def doctor
|
|
68
|
+
EasyCreds.load_global_config!
|
|
69
|
+
ok = EasyCreds::Doctor.new(verbose: options[:verbose]).run
|
|
70
|
+
exit 1 unless ok
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
desc 'install', 'Install overlay initializer into current Rails project'
|
|
74
|
+
def install
|
|
75
|
+
project = EasyCreds::Project.detect(Dir.pwd)
|
|
76
|
+
unless project.is_a?(EasyCreds::Project::Rails)
|
|
77
|
+
puts EasyCreds::Theme.warn('Not a Rails project — nothing to install.')
|
|
78
|
+
exit 3
|
|
79
|
+
end
|
|
80
|
+
EasyCreds::Installer.new(project.root).run
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
desc 'projects', 'Browse and manage registered projects'
|
|
84
|
+
def projects
|
|
85
|
+
EasyCreds.load_global_config!
|
|
86
|
+
registry = Projects::Registry.load
|
|
87
|
+
outside_picker_loop(registry)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
desc 'templates', 'Browse and manage global template library'
|
|
91
|
+
def templates
|
|
92
|
+
EasyCreds.load_global_config!
|
|
93
|
+
Views::TemplatesMenu.run(build_prompt)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
desc 'settings', 'Edit global easy_creds settings'
|
|
97
|
+
def settings
|
|
98
|
+
EasyCreds.load_global_config!
|
|
99
|
+
Views::SettingsMenu.run(build_prompt)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
desc 'tree', 'Print a tree of all available commands'
|
|
103
|
+
def tree
|
|
104
|
+
render_command_tree
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
desc 'version', 'Print easy_creds version'
|
|
108
|
+
def version
|
|
109
|
+
beastmode_version
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
COMMAND_GROUPS = {
|
|
115
|
+
'Setup' => %w[init onboard install doctor],
|
|
116
|
+
'Sync' => %w[sync],
|
|
117
|
+
'Manage' => %w[projects templates settings],
|
|
118
|
+
'Utility' => %w[tree version help]
|
|
119
|
+
}.freeze
|
|
120
|
+
private_constant :COMMAND_GROUPS
|
|
121
|
+
|
|
122
|
+
def welcome
|
|
123
|
+
EasyCreds.load_global_config!
|
|
124
|
+
width = [TTY::Screen.width, 100].min
|
|
125
|
+
inner = width - 4
|
|
126
|
+
|
|
127
|
+
# ASCII art banner in rounded box
|
|
128
|
+
art_lines = Theme::BANNER_ART.map { |l| " #{Theme.accent(l)}" }
|
|
129
|
+
vault = EasyCreds.config.default_vault
|
|
130
|
+
art_lines << ''
|
|
131
|
+
art_lines << " #{Theme.dim("v#{EasyCreds::VERSION} · credentials ↔ 1Password sync · vault: #{vault || '(not set)'}")}"
|
|
132
|
+
rows = art_lines.map { |l| "#{Theme.box_side} #{Theme.rpad(l, inner)} #{Theme.box_side}" }
|
|
133
|
+
puts ''
|
|
134
|
+
puts Theme.box_top(width)
|
|
135
|
+
puts rows
|
|
136
|
+
puts Theme.box_bottom(width)
|
|
137
|
+
puts ''
|
|
138
|
+
puts " #{Theme.tprompt_line}"
|
|
139
|
+
puts ''
|
|
140
|
+
|
|
141
|
+
registry = Projects::Registry.load
|
|
142
|
+
if registry.projects.any?
|
|
143
|
+
puts " #{Theme.bold('Recent projects:')}"
|
|
144
|
+
puts ''
|
|
145
|
+
registry.projects
|
|
146
|
+
.sort_by { |pr| -(pr.last_seen_at&.to_i || 0) }
|
|
147
|
+
.first(3)
|
|
148
|
+
.each do |pr|
|
|
149
|
+
seen = Views::ProjectPicker.relative_time(pr.last_seen_at)
|
|
150
|
+
kind = pr.rails? ? Theme.ok('[rails]') : Theme.accent('[std]')
|
|
151
|
+
puts " #{Theme.accent('›')} #{Theme.bold(pr.name.ljust(22))} #{kind} #{Theme.dim(pr.display_path)} #{Theme.dim(seen)}"
|
|
152
|
+
end
|
|
153
|
+
puts ''
|
|
154
|
+
elsif first_run?
|
|
155
|
+
puts " #{Theme.warn('→ First time?')} Run #{Theme.accent(Theme.bold('easy_creds init'))} to get started."
|
|
156
|
+
puts ''
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
puts " #{Theme.bold('Commands:')}"
|
|
160
|
+
puts ''
|
|
161
|
+
cmds = self.class.commands
|
|
162
|
+
max_w = cmds.keys.map(&:length).max || 0
|
|
163
|
+
cmds.sort.each do |name, cmd|
|
|
164
|
+
puts " easy_creds #{Theme.accent(Theme.bold(name.ljust(max_w + 1)))} #{Theme.dim('# ' + cmd.description)}"
|
|
165
|
+
end
|
|
166
|
+
puts ''
|
|
167
|
+
puts " #{Theme.dim('Run')} #{Theme.bold('easy_creds help [COMMAND]')} #{Theme.dim('for details.')}"
|
|
168
|
+
puts ''
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def outside_picker_loop(registry)
|
|
172
|
+
prompt = build_prompt
|
|
173
|
+
loop do
|
|
174
|
+
system('clear')
|
|
175
|
+
choice = Views::WelcomeScreen.run(registry)
|
|
176
|
+
|
|
177
|
+
case choice
|
|
178
|
+
when :quit then break
|
|
179
|
+
when :templates
|
|
180
|
+
system('clear')
|
|
181
|
+
Views::TemplatesMenu.run(prompt)
|
|
182
|
+
when :settings
|
|
183
|
+
system('clear')
|
|
184
|
+
Views::SettingsMenu.run(prompt)
|
|
185
|
+
when :init_here
|
|
186
|
+
system('clear')
|
|
187
|
+
init_here(prompt, registry) and break
|
|
188
|
+
when EasyCreds::Projects::Project
|
|
189
|
+
handle_project_pick(choice, registry)
|
|
190
|
+
break
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def handle_project_pick(project, registry)
|
|
196
|
+
unless project.exists_on_disk?
|
|
197
|
+
prompt = build_prompt
|
|
198
|
+
if prompt.yes?("Path #{project.path} no longer exists. Remove from registry?", default: true)
|
|
199
|
+
registry.remove(project)
|
|
200
|
+
end
|
|
201
|
+
return
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
registry.touch(project)
|
|
205
|
+
Dir.chdir(project.path) do
|
|
206
|
+
detected = EasyCreds::Project.detect(project.path)
|
|
207
|
+
EasyCreds::CLI.start(project: detected, default_env: nil)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def init_here(prompt, registry)
|
|
212
|
+
project = EasyCreds::Project.detect(Dir.pwd)
|
|
213
|
+
result = Onboarding::RegisterPrompt.run(prompt: prompt, project: project, registry: registry)
|
|
214
|
+
return false if result == :quit
|
|
215
|
+
|
|
216
|
+
EasyCreds::CLI.start(project: project, default_env: nil)
|
|
217
|
+
true
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def render_command_tree
|
|
221
|
+
p = Theme.pastel
|
|
222
|
+
puts ''
|
|
223
|
+
puts " #{Theme.header('easy_creds')} #{Theme.dim('v' + EasyCreds::VERSION)}"
|
|
224
|
+
puts " #{Theme.dim('─' * 50)}"
|
|
225
|
+
puts ''
|
|
226
|
+
|
|
227
|
+
rendered = []
|
|
228
|
+
COMMAND_GROUPS.each do |group_name, cmd_names|
|
|
229
|
+
available = cmd_names.select { |n| self.class.commands.key?(n) }
|
|
230
|
+
next if available.empty?
|
|
231
|
+
|
|
232
|
+
puts " #{Theme.bold(group_name)}"
|
|
233
|
+
available.each_with_index do |name, i|
|
|
234
|
+
cmd = self.class.commands[name]
|
|
235
|
+
connector = i == available.size - 1 ? '└─' : '├─'
|
|
236
|
+
puts " #{Theme.dim(connector)} #{Theme.accent(name.ljust(12))} #{Theme.dim(cmd.description)}"
|
|
237
|
+
rendered << name
|
|
238
|
+
end
|
|
239
|
+
puts ''
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
remaining = self.class.commands.keys.sort - rendered
|
|
243
|
+
if remaining.any?
|
|
244
|
+
puts " #{Theme.bold('Other')}"
|
|
245
|
+
remaining.each_with_index do |name, i|
|
|
246
|
+
cmd = self.class.commands[name]
|
|
247
|
+
connector = i == remaining.size - 1 ? '└─' : '├─'
|
|
248
|
+
puts " #{Theme.dim(connector)} #{Theme.accent(name.ljust(12))} #{Theme.dim(cmd.description)}"
|
|
249
|
+
end
|
|
250
|
+
puts ''
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def beastmode_version
|
|
255
|
+
width = 48
|
|
256
|
+
inner = width - 4
|
|
257
|
+
|
|
258
|
+
lines = [
|
|
259
|
+
" #{Theme.header("#{Theme::ICONS[:lock]} easy_creds")} #{Theme.dim("v#{EasyCreds::VERSION}")}",
|
|
260
|
+
'',
|
|
261
|
+
" #{Theme.dim("Ruby #{RUBY_VERSION} · #{RUBY_PLATFORM}")}"
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
rows = lines.map { |l| "#{Theme.box_side} #{Theme.rpad(l, inner)} #{Theme.box_side}" }
|
|
265
|
+
box = ([Theme.box_top(width)] + rows + [Theme.box_bottom(width)]).join("\n")
|
|
266
|
+
|
|
267
|
+
puts ''
|
|
268
|
+
puts box.split("\n").map { |l| " #{l}" }.join("\n")
|
|
269
|
+
puts ''
|
|
270
|
+
puts " #{Theme.accent(Theme.bold('B E A S T M O D E'))} #{Theme.dim('activated ⚡')}"
|
|
271
|
+
puts ''
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def check_first_run!
|
|
275
|
+
return unless first_run?
|
|
276
|
+
|
|
277
|
+
puts ''
|
|
278
|
+
puts " #{Theme.warn("#{Theme::ICONS[:warn]} First run detected!")} #{Theme.dim('Setting up easy_creds...')}"
|
|
279
|
+
puts ''
|
|
280
|
+
EasyCreds::Onboarding::GemSetup.run(prompt: build_prompt)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def first_run?
|
|
284
|
+
!File.exist?(File.join(EasyCreds.config.global_dir, 'config.yml'))
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def looks_like_project?(project)
|
|
288
|
+
project.rails? || File.directory?(File.join(project.root.to_s, '.git'))
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def skip_register?
|
|
292
|
+
options[:'no-register'] || ENV['EASY_CREDS_NO_REGISTER']
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def build_prompt
|
|
296
|
+
TTY::Prompt.new(interrupt: :exit, symbols: { selector: '›' })
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
module VaultPicker
|
|
5
|
+
CREATE_SENTINEL = :'__create_new__'
|
|
6
|
+
DEFAULT_NEW_NAME = 'easy_creds'
|
|
7
|
+
|
|
8
|
+
# Shows existing 1Password vaults + a "create new" option.
|
|
9
|
+
# Returns the chosen vault name (string), or nil if the user aborts.
|
|
10
|
+
def self.pick(prompt)
|
|
11
|
+
vaults = fetch_vaults
|
|
12
|
+
|
|
13
|
+
choices = vaults.map { |v| { name: " #{v}", value: v } }
|
|
14
|
+
|
|
15
|
+
if choices.any?
|
|
16
|
+
choices << { name: Theme.pastel.dim(' ─────────────────────────────────'), value: nil, disabled: '' }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
choices << { name: " #{Theme.pastel.bold('✚ create new vault…')}", value: CREATE_SENTINEL }
|
|
20
|
+
|
|
21
|
+
selection = prompt.select('Select default vault:', choices, cycle: true, filter: false,
|
|
22
|
+
per_page: [choices.size, 20].min)
|
|
23
|
+
|
|
24
|
+
return nil if selection.nil?
|
|
25
|
+
return selection unless selection == CREATE_SENTINEL
|
|
26
|
+
|
|
27
|
+
create_vault(prompt)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.fetch_vaults
|
|
31
|
+
raw = `op vault list --format=json 2>/dev/null`
|
|
32
|
+
return [] unless $CHILD_STATUS.success?
|
|
33
|
+
|
|
34
|
+
JSON.parse(raw).map { |v| v['name'] }
|
|
35
|
+
rescue JSON::ParserError
|
|
36
|
+
[]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.create_vault(prompt)
|
|
40
|
+
name = prompt.ask('New vault name:', default: DEFAULT_NEW_NAME) do |q|
|
|
41
|
+
q.validate(/\S+/, 'Vault name cannot be blank.')
|
|
42
|
+
end
|
|
43
|
+
return nil unless name
|
|
44
|
+
|
|
45
|
+
name = name.strip
|
|
46
|
+
output = `op vault create #{Shellwords.escape(name)} 2>&1`
|
|
47
|
+
if $CHILD_STATUS.success?
|
|
48
|
+
Theme.success("Created vault '#{name}'")
|
|
49
|
+
name
|
|
50
|
+
else
|
|
51
|
+
Theme.failure("Could not create vault: #{output.strip}")
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tty-table'
|
|
4
|
+
|
|
5
|
+
module EasyCreds
|
|
6
|
+
module Views
|
|
7
|
+
module DiffTable
|
|
8
|
+
HEADER = %w[change key where].freeze
|
|
9
|
+
|
|
10
|
+
CHANGE_LABELS = { added: 'remote-only', removed: 'local-only', modified: 'differs' }.freeze
|
|
11
|
+
|
|
12
|
+
def self.render(changes)
|
|
13
|
+
if changes.empty?
|
|
14
|
+
puts Theme.ok(' (no changes — already in sync)')
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
puts build_table(changes)
|
|
19
|
+
puts Theme.dim(" #{changes.size} change(s) — values not shown")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private_class_method def self.build_table(changes)
|
|
23
|
+
rows = changes.map { |c| change_row(c) }
|
|
24
|
+
TTY::Table.new(HEADER, rows).render(:unicode, padding: [0, 1])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private_class_method def self.change_row(change)
|
|
28
|
+
[
|
|
29
|
+
"#{Theme.change_color(change.kind)} #{CHANGE_LABELS[change.kind]}",
|
|
30
|
+
Theme.key_tag(change.key),
|
|
31
|
+
Theme.dim(change.side.to_s)
|
|
32
|
+
]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
module Views
|
|
5
|
+
module Header
|
|
6
|
+
def self.render(ctx)
|
|
7
|
+
system('clear') || system('cls')
|
|
8
|
+
puts Theme.banner(ctx.config.vault, EnvPicker.available(ctx.root))
|
|
9
|
+
puts ''
|
|
10
|
+
puts " #{Theme.tprompt_line}"
|
|
11
|
+
puts ''
|
|
12
|
+
puts sign_in_line(ctx.op&.account_email)
|
|
13
|
+
puts env_vault_line(ctx)
|
|
14
|
+
puts project_line(ctx.root)
|
|
15
|
+
puts ''
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private_class_method def self.sign_in_line(email)
|
|
19
|
+
if email
|
|
20
|
+
" #{Theme.ok(Theme::ICONS[:ok])} signed in to 1Password #{Theme.dim("(#{email})")}"
|
|
21
|
+
else
|
|
22
|
+
" #{Theme.error(Theme::ICONS[:error])} not signed in to 1Password"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private_class_method def self.env_vault_line(ctx)
|
|
27
|
+
env_label = ctx.env ? Theme.env_tag(ctx.env) : Theme.dim('none selected')
|
|
28
|
+
vault_label = ctx.config.vault ? Theme.bold(ctx.config.vault) : Theme.dim('(not set)')
|
|
29
|
+
" #{Theme.dim('env')} #{env_label} #{Theme.dim('vault')} #{vault_label}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private_class_method def self.project_line(root)
|
|
33
|
+
project = Project.detect(root)
|
|
34
|
+
kind = project.rails? ? Theme.ok('Rails') : Theme.accent('Standalone')
|
|
35
|
+
path = Theme.dim(root.to_s.gsub(Dir.home, '~'))
|
|
36
|
+
" #{Theme.dim('project')} #{kind} #{path}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCreds
|
|
4
|
+
module Views
|
|
5
|
+
# Pure keystroke → state mutation dispatcher.
|
|
6
|
+
# Returns nil (normal redraw), or a signal symbol:
|
|
7
|
+
# :manual_entry — caller must prompt for hidden input
|
|
8
|
+
# :save — caller should write the file and exit
|
|
9
|
+
# :quit — caller should confirm and exit
|
|
10
|
+
# :toggle_help — caller flips the help overlay flag
|
|
11
|
+
module InitDispatch
|
|
12
|
+
UP = ["\e[A", 'k'].freeze
|
|
13
|
+
DOWN = ["\e[B", 'j'].freeze
|
|
14
|
+
PAGE_UP = ["\e[5~", "\e[I"].freeze
|
|
15
|
+
PAGE_DOWN = ["\e[6~", "\e[G"].freeze
|
|
16
|
+
HOME_KEY = ["\e[H", "\e[1~", "\eOH"].freeze
|
|
17
|
+
END_KEY = ["\e[F", "\e[4~", "\eOF"].freeze
|
|
18
|
+
|
|
19
|
+
# Keys that return a signal directly without touching state.
|
|
20
|
+
SIGNAL_KEYS = {
|
|
21
|
+
'm' => :manual_entry,
|
|
22
|
+
's' => :save, 'S' => :save,
|
|
23
|
+
'q' => :quit, "\e" => :quit,
|
|
24
|
+
'?' => :toggle_help, 'h' => :toggle_help
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Returns nil (redraw) or a signal symbol (:manual_entry, :save, :quit, :toggle_help).
|
|
28
|
+
def self.handle(key, state, root: nil)
|
|
29
|
+
nav = navigate(key, state)
|
|
30
|
+
return nav unless nav == :not_navigation
|
|
31
|
+
|
|
32
|
+
SIGNAL_KEYS.fetch(key) { mutate(key, state, root: root) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private_class_method def self.navigate(key, state)
|
|
36
|
+
case key
|
|
37
|
+
when *UP then state.move_up
|
|
38
|
+
when *DOWN then state.move_down
|
|
39
|
+
when *PAGE_UP then state.move_up(10)
|
|
40
|
+
when *PAGE_DOWN then state.move_down(10)
|
|
41
|
+
when *HOME_KEY then state.move_first
|
|
42
|
+
when *END_KEY then state.move_last
|
|
43
|
+
else return :not_navigation
|
|
44
|
+
end
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private_class_method def self.mutate(key, state, root: nil)
|
|
49
|
+
case key
|
|
50
|
+
when 'g' then apply_generate(state, root: root)
|
|
51
|
+
when 'G' then apply_generate_alt(state)
|
|
52
|
+
when 'b' then apply_ar_batch(state, root: root)
|
|
53
|
+
when 't' then apply_template(state)
|
|
54
|
+
when 'o' then apply_from_op(state)
|
|
55
|
+
when 'c' then state.clear_entry(state.current)
|
|
56
|
+
when 'T' then template_all(state)
|
|
57
|
+
when 'R' then generate_suggested(state, root: root)
|
|
58
|
+
when 'O' then from_op_all(state)
|
|
59
|
+
end
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private_class_method def self.apply_generate(state, root: nil)
|
|
64
|
+
entry = state.current
|
|
65
|
+
if entry.hint == :ar_encryption
|
|
66
|
+
apply_ar_batch(state, root: root)
|
|
67
|
+
else
|
|
68
|
+
val = Generators.default_for(entry.hint, root: root)
|
|
69
|
+
state.set_entry(entry, value: val.to_s, source: :generate) if val
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private_class_method def self.apply_generate_alt(state)
|
|
74
|
+
entry = state.current
|
|
75
|
+
return if entry.hint == :ar_encryption
|
|
76
|
+
|
|
77
|
+
val = Generators.alternative_for(entry.hint)
|
|
78
|
+
state.set_entry(entry, value: val.to_s, source: :generate_alt) if val
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private_class_method def self.apply_ar_batch(state, root: nil)
|
|
82
|
+
keys = Generators.ar_encryption_keys(root)
|
|
83
|
+
return unless keys
|
|
84
|
+
|
|
85
|
+
keys.each do |key, val|
|
|
86
|
+
entry = state.entries.find { |e| e.key == key }
|
|
87
|
+
state.set_entry(entry, value: val.to_s, source: :ar_batch) if entry
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private_class_method def self.apply_template(state)
|
|
92
|
+
entry = state.current
|
|
93
|
+
return unless entry.placeholder
|
|
94
|
+
|
|
95
|
+
state.set_entry(entry, value: entry.placeholder.to_s, source: :template)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private_class_method def self.apply_from_op(state)
|
|
99
|
+
entry = state.current
|
|
100
|
+
val = entry.op_value
|
|
101
|
+
state.set_entry(entry, value: val.to_s, source: :from_op) if val
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private_class_method def self.template_all(state)
|
|
105
|
+
state.entries.each do |entry|
|
|
106
|
+
next if entry.set? || entry.placeholder.nil?
|
|
107
|
+
|
|
108
|
+
state.set_entry(entry, value: entry.placeholder.to_s, source: :template)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private_class_method def self.generate_suggested(state, root: nil)
|
|
113
|
+
apply_ar_batch(state, root: root) if state.entries.any? { |e| e.hint == :ar_encryption && !e.set? }
|
|
114
|
+
|
|
115
|
+
state.entries.each do |entry|
|
|
116
|
+
next if entry.set? || entry.hint.nil? || entry.hint == :ar_encryption
|
|
117
|
+
|
|
118
|
+
val = Generators.bulk_for(entry.hint)
|
|
119
|
+
state.set_entry(entry, value: val.to_s, source: :generate) if val
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private_class_method def self.from_op_all(state)
|
|
124
|
+
state.entries.each do |entry|
|
|
125
|
+
next if entry.set? || entry.op_value.nil?
|
|
126
|
+
|
|
127
|
+
state.set_entry(entry, value: entry.op_value.to_s, source: :from_op)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|