2pass 1.2.0 → 2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbffa6c36a23a40c4ec93bc9f0277cf43d3e2388b61459a89aad0bb89458451e
4
- data.tar.gz: 5ab72d0c03fb7b718aaec180fdb530755ad69394acb6f808918b978044174e47
3
+ metadata.gz: 135196a393e121fb8b71d5295fdedc19f3bccb24885ce81cf630be59d2b7c64e
4
+ data.tar.gz: 7f5684e81780732115fcf6b9d5a071cfd1e8a05eaa48246e35f7fd3841a702ce
5
5
  SHA512:
6
- metadata.gz: 0f0bce6612b408712dac5d313331b8721703f89a4a695ff0d7fd23076d6fcaf7bd2fdec67bdf75c1504f98d59dd400fe04bfa70f729847e234b13ab114d03eb6
7
- data.tar.gz: 06d625e2325531c59eff3d197eab64ed3265de9c93ab4046ff39c81f7057ceecb3c2cd640cccc70f9073da3975a3eac9e14fdbaf185242ff4141a0ea1bc6a517
6
+ metadata.gz: e1c997e62173b57c596adf21c9636bfd7ed3417a5f0611d497db1caa486a18e113ad37b234bec8b3334fcea282c5f99f2ec9dbd1b4e328222b1439f281629586
7
+ data.tar.gz: 9697b86fe839dc5d29f7786ab2e278986fbdb64bd2fc89e142f6e8bc812f919a66d3b57afcb97e71e099d806cc1411c56c12f5117facf6174ee780de71812b14
data/2pass.gemspec CHANGED
@@ -24,4 +24,6 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "rake", "~> 13.0"
25
25
  spec.add_development_dependency "minitest", "~> 5.0"
26
26
  spec.add_development_dependency "minitest-reporters", "~> 1.3", ">= 1.3.0"
27
+
28
+ spec.add_dependency "terminal-table", "~> 4.0"
27
29
  end
data/README.md CHANGED
@@ -17,13 +17,12 @@ An array of hashes with the following keys:
17
17
 
18
18
  - id
19
19
  - value
20
- - uuid
21
20
 
22
21
  ```sh
23
22
  touch ~/.2pass/vault_name.yml
24
23
  ```
25
24
 
26
- Alternatively, if you have a vault file in a different location, you can link it.
25
+ Alternatively, if you already have a vault file in a different location, you can link it.
27
26
 
28
27
  ```sh
29
28
  2pass link vault_name /path/to/vault_name.yml
@@ -40,10 +39,18 @@ Then build the gem and install it.
40
39
 
41
40
  ```sh
42
41
  2pass -h
43
- 2pass get <vault_name> <id>
44
- 2pass list <vault_name>
45
- 2pass add <vault_name> <id> <value>
42
+ 2pass get <vault_name> <id> [env]
43
+ 2pass list <vault_name> [env]
44
+ 2pass list <vault_name> [env] --json
45
+ 2pass list <vault_name> [env] --dotenv
46
+ 2pass add <vault_name> <id> <value> [env]
47
+ 2pass link <vault_name> <target_path> [env]
48
+ 2pass update
46
49
  ```
50
+ You can also pass an optional environment to most commands:
51
+ `2pass get myproject mykey production`
52
+
53
+ This will search for `mykey` within `myproject.production.yml`
47
54
 
48
55
  ## Development
49
56
 
data/bin/2pass CHANGED
@@ -5,12 +5,14 @@ require_relative "../lib/2pass"
5
5
  def help_message
6
6
  puts <<~HELP
7
7
  Usage:
8
- 2pass add <vault_name> <id> <value> - Add new secret
9
- 2pass get <vault_name> <id> - Get content by ID from the specified vault
10
- 2pass list <vault_name> - List the content of the specified vault as key-value pairs
11
- 2pass list <vault_name> --json - List the content of the specified vault as JSON
12
- 2pass link <vault_name> <target_path> - Create a symlink for an existing vault. Useful when the vault is stored in a synced place (iCloud, Dropbox, etc.)
13
- 2pass -h - Display this help message
8
+ 2pass add <vault_name> <id> <value> [env] - Add new secret. env is optional
9
+ 2pass get <vault_name> <id> [env] - Get content by ID from the specified vault. env is optional
10
+ 2pass list <vault_name> [env] - List the content of the specified vault as key=value pairs. env is optional
11
+ 2pass list <vault_name> [env] --json - List the content of the specified vault as JSON. env is optional
12
+ 2pass list <vault_name> [env] --dotenv - List the content of the specified vault as key=value pairs. env is optional
13
+ 2pass link <vault_name> <target_path> [env] - Create a symlink for an existing vault. Useful when the vault is stored in a synced place (iCloud, Dropbox, etc.). env is optional
14
+ 2pass update - Update 2pass to the latest RubyGems release
15
+ 2pass -h - Display this help message
14
16
  HELP
15
17
  end
16
18
 
@@ -24,12 +26,12 @@ OptionParser.new do |opts|
24
26
  opts.on("-v", "--version", "Display the version") do
25
27
  options[:version] = true
26
28
  end
27
- opts.on("-f", "--format", "Specify output format (e.g., json)") do |format|
28
- options[:format] = format == "json" ? :json : :text
29
- end
30
- opts.on("--json", "Use JSON output format (same as -f json)") do
29
+ opts.on("--json", "Use JSON output format") do
31
30
  options[:format] = :json
32
31
  end
32
+ opts.on("--dotenv", "Use key=value output format") do
33
+ options[:format] = :dotenv
34
+ end
33
35
  end.parse!
34
36
 
35
37
  if options[:help]
@@ -42,46 +44,61 @@ if options[:version]
42
44
  exit
43
45
  end
44
46
 
45
- if ARGV.length < 2
47
+ if ARGV.empty?
46
48
  help_message
47
49
  exit(1)
48
50
  end
49
51
 
50
- command, vault_name, *args = ARGV
52
+ command, *args = ARGV
51
53
 
52
54
  begin
53
55
  case command&.to_sym
54
56
  when :get
55
- if args.length < 1
57
+ if args.length < 2 || args.length > 3
56
58
  help_message
57
59
  exit(1)
58
60
  end
59
- id = args[0]
60
- puts TwoPass.get_secret(vault_name, id)
61
+ vault_name, id, env = args[0], args[1], args[2]
62
+ puts TwoPass.get_secret(vault_name, id, env: env)
61
63
  when :add
62
- if args.length != 2
64
+ if args.length < 3 || args.length > 4
63
65
  help_message
64
66
  exit(1)
65
67
  end
66
- TwoPass.add_secret(vault_name, args[0], args[1])
68
+ vault_name, id, value, env = args[0], args[1], args[2], args[3]
69
+ TwoPass.add_secret(vault_name, id, value, env: env)
67
70
  when :list
71
+ if args.length < 1 || args.length > 2
72
+ help_message
73
+ exit(1)
74
+ end
75
+ vault_name, env = args[0], args[1]
68
76
  if options[:format] == :json
69
- puts TwoPass.list_content(vault_name, format: :json)
77
+ puts TwoPass.list_content(vault_name, env: env, format: :json)
78
+ elsif options[:format] == :dotenv
79
+ puts TwoPass.list_content(vault_name, env: env, format: :dotenv)
70
80
  else
71
- puts TwoPass.list_content(vault_name)
81
+ puts TwoPass.list_content(vault_name, env: env)
72
82
  end
73
- exit(1)
83
+ exit(0)
74
84
  when :link
75
- if args.length < 1
85
+ if args.length < 2 || args.length > 3
76
86
  help_message
77
87
  exit(1)
78
88
  end
79
- target_path = args[0]
80
- TwoPass.create_symlink(vault_name, target_path)
89
+ vault_name, target_path, env = args[0], args[1], args[2]
90
+ TwoPass.create_symlink(vault_name, target_path, env: env)
91
+ when :update
92
+ if args.any?
93
+ help_message
94
+ exit(1)
95
+ end
96
+ puts TwoPass.update_cli
81
97
  when :help
82
98
  help_message
83
99
  else
84
100
  help_message
101
+ exit(1)
85
102
  end
86
103
  rescue => e
87
104
  STDERR.puts e.message
data/lib/2pass/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module TwoPass
2
- VERSION = "1.2.0"
2
+ VERSION = "2.1.0"
3
3
  end
data/lib/2pass.rb CHANGED
@@ -1,8 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "optparse"
2
4
  require "json"
3
5
  require "yaml"
4
6
  require "fileutils"
5
- require "securerandom"
7
+ require "net/http"
8
+ require "uri"
9
+ require "rubygems/version"
10
+ require "terminal-table"
6
11
 
7
12
  require_relative "2pass/version"
8
13
 
@@ -10,42 +15,62 @@ module TwoPass
10
15
  VAULT_DIR = "#{Dir.home}/.2pass"
11
16
 
12
17
  class << self
13
- def create_symlink(vault_name, target_path)
18
+ def create_symlink(vault_name, target_path, env: nil)
14
19
  FileUtils.mkdir_p(VAULT_DIR)
15
- symlink_path = "#{VAULT_DIR}/#{vault_name}.yml"
20
+ symlink_path = vault_file_path(vault_name, env: env)
21
+ expanded_target_path = File.expand_path(target_path)
16
22
 
17
- if File.exist?(symlink_path)
18
- puts "Symlink already exists: #{symlink_path}"
19
- else
20
- File.symlink(target_path, symlink_path)
21
- puts "Created symlink: #{symlink_path} -> #{target_path}"
23
+ unless File.exist?(expanded_target_path)
24
+ raise "Target vault file does not exist: #{expanded_target_path}"
22
25
  end
23
- end
24
26
 
25
- def load_vault(vault_name)
26
- file_path = "#{VAULT_DIR}/#{vault_name}.yml"
27
- return [] unless File.exist?(file_path)
27
+ unless File.readable?(expanded_target_path)
28
+ raise "Target vault file is not readable: #{expanded_target_path}"
29
+ end
28
30
 
29
- YAML.load_file(file_path, symbolize_names: true) || []
30
- rescue Psych::SyntaxError => e
31
- STDERR.puts "Error parsing YAML file: #{e.message}"
32
- []
31
+ if File.symlink?(symlink_path)
32
+ existing_target = File.expand_path(File.readlink(symlink_path), File.dirname(symlink_path))
33
+ if existing_target == expanded_target_path
34
+ puts "Symlink already exists: #{symlink_path} -> #{expanded_target_path}"
35
+ return
36
+ end
37
+
38
+ raise "A different symlink already exists at #{symlink_path}: #{existing_target}"
39
+ end
40
+
41
+ if File.exist?(symlink_path)
42
+ raise "A regular file already exists at #{symlink_path}. Remove it before creating a symlink."
43
+ end
44
+
45
+ File.symlink(expanded_target_path, symlink_path)
46
+ puts "Created symlink: #{symlink_path} -> #{expanded_target_path}"
33
47
  end
34
48
 
35
- def save_vault(vault_name, data)
49
+ def save_vault(vault_name, data, env: nil)
36
50
  FileUtils.mkdir_p(VAULT_DIR)
37
- file_path = "#{VAULT_DIR}/#{vault_name}.yml"
51
+ file_path = vault_file_path(vault_name, env: env)
38
52
  File.write(file_path, YAML.dump(data))
39
53
  end
40
54
 
41
- def list_content(vault_name, format: :text)
42
- vault = load_vault(vault_name)
43
- if format == :text
55
+ def list_content(vault_name, env: nil, format: :table)
56
+ vault = load_vault(vault_name, env: env)
57
+ if format == :table
58
+ rows = vault
59
+ .sort_by { |hash| hash[:id] }
60
+ .each_with_object([]) do |h, arr|
61
+ arr << [h[:id], h[:value]]
62
+ end
63
+ Terminal::Table.new(
64
+ rows: rows,
65
+ title: env || nil,
66
+ style: {all_separators: true}
67
+ ).to_s
68
+ elsif format == :dotenv
44
69
  vault
45
70
  .map { |h| "#{h[:id]}=#{h[:value]}" }
46
71
  .sort
47
72
  .join("\n")
48
- else
73
+ elsif format == :json
49
74
  JSON.pretty_generate(
50
75
  vault
51
76
  .map { |hash| hash.slice(:id, :value) }
@@ -54,23 +79,22 @@ module TwoPass
54
79
  end
55
80
  end
56
81
 
57
- def add_secret(vault_name, id, value)
82
+ def add_secret(vault_name, id, value, env: nil)
58
83
  new_secret = {
59
84
  id: id,
60
- value: value,
61
- uuid: SecureRandom.uuid
85
+ value: value
62
86
  }
63
- data = load_vault(vault_name)
87
+ data = load_vault(vault_name, env: env)
64
88
  existing_entry_id = data.find_index { |hash| hash[:id] == new_secret[:id] }
65
89
  if existing_entry_id
66
90
  raise "The secret already exists"
67
91
  end
68
92
  data << new_secret
69
- save_vault(vault_name, data)
93
+ save_vault(vault_name, data, env: env)
70
94
  end
71
95
 
72
- def get_secret(vault_name, id)
73
- vault = load_vault(vault_name)
96
+ def get_secret(vault_name, id, env: nil)
97
+ vault = load_vault(vault_name, env: env)
74
98
  entry = vault.find { |hash| hash[:id] == id }
75
99
  if entry
76
100
  entry[:value]
@@ -78,5 +102,92 @@ module TwoPass
78
102
  raise "Entry not found"
79
103
  end
80
104
  end
105
+
106
+ def update_cli
107
+ latest_version = fetch_latest_version
108
+ current = Gem::Version.new(VERSION)
109
+ latest = Gem::Version.new(latest_version)
110
+
111
+ if current >= latest
112
+ return "2pass is already up to date (#{VERSION})."
113
+ end
114
+
115
+ unless run_update_command
116
+ raise "Failed to update 2pass. Try running: #{update_command.join(' ')}"
117
+ end
118
+
119
+ "Updated 2pass from #{VERSION} to #{latest_version}. Restart your shell session if needed."
120
+ end
121
+
122
+ private
123
+
124
+ def load_vault(vault_name, env: nil)
125
+ file_path = self.vault_file_path(vault_name, env: env)
126
+ return [] unless File.exist?(file_path)
127
+
128
+ YAML.load_file(file_path, symbolize_names: true) || []
129
+ rescue Errno::EACCES, Errno::EPERM => e
130
+ raise permission_error_message(file_path, e)
131
+ rescue Psych::SyntaxError => e
132
+ STDERR.puts "Error parsing YAML file: #{e.message}"
133
+ []
134
+ end
135
+
136
+ def vault_file_path(vault_name, env: nil)
137
+ [
138
+ VAULT_DIR,
139
+ [vault_name, env].compact.join(".")
140
+ ].join("/") + ".yml"
141
+ end
142
+
143
+ def permission_error_message(file_path, error)
144
+ message = +"Unable to read vault file: #{file_path}\n#{error.class}: #{error.message}"
145
+ symlink_target = nil
146
+
147
+ if File.symlink?(file_path)
148
+ symlink_target = File.expand_path(File.readlink(file_path), File.dirname(file_path))
149
+ message << "\nSymlink target: #{symlink_target}"
150
+ end
151
+
152
+ path_to_check = symlink_target || file_path
153
+ if path_to_check.include?("/Library/Mobile Documents/")
154
+ message << "\nThe vault points to iCloud Drive. macOS may be blocking your terminal app."
155
+ message << "\nCheck System Settings > Privacy & Security > Files and Folders"
156
+ message << "\nfor your terminal app and enable iCloud Drive (and Full Disk Access if needed)."
157
+ end
158
+
159
+ message
160
+ rescue StandardError
161
+ "Unable to read vault file: #{file_path}\n#{error.class}: #{error.message}"
162
+ end
163
+
164
+ def fetch_latest_version
165
+ uri = URI("https://rubygems.org/api/v1/gems/2pass.json")
166
+ response = Net::HTTP.start(
167
+ uri.host,
168
+ uri.port,
169
+ use_ssl: true,
170
+ open_timeout: 5,
171
+ read_timeout: 5
172
+ ) do |http|
173
+ http.get(uri.request_uri)
174
+ end
175
+
176
+ unless response.is_a?(Net::HTTPSuccess)
177
+ raise "Unable to check latest version (HTTP #{response.code})"
178
+ end
179
+
180
+ JSON.parse(response.body).fetch("version")
181
+ rescue JSON::ParserError, KeyError => e
182
+ raise "Unable to parse latest version from RubyGems: #{e.message}"
183
+ end
184
+
185
+ def run_update_command
186
+ system(*update_command)
187
+ end
188
+
189
+ def update_command
190
+ [Gem.ruby, "-S", "gem", "update", "2pass"]
191
+ end
81
192
  end
82
193
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: 2pass
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Olivier
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-01-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -72,6 +71,20 @@ dependencies:
72
71
  - - ">="
73
72
  - !ruby/object:Gem::Version
74
73
  version: 1.3.0
74
+ - !ruby/object:Gem::Dependency
75
+ name: terminal-table
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '4.0'
81
+ type: :runtime
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '4.0'
75
88
  description: 2pass is a CLI application for managing YAML-based vaults.
76
89
  email:
77
90
  - contact@yafoy.com
@@ -91,7 +104,6 @@ homepage: https://2pass.xyz
91
104
  licenses:
92
105
  - MIT
93
106
  metadata: {}
94
- post_install_message:
95
107
  rdoc_options: []
96
108
  require_paths:
97
109
  - lib
@@ -106,8 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
118
  - !ruby/object:Gem::Version
107
119
  version: '0'
108
120
  requirements: []
109
- rubygems_version: 3.5.11
110
- signing_key:
121
+ rubygems_version: 4.0.3
111
122
  specification_version: 4
112
123
  summary: A CLI app for managing secrets
113
124
  test_files: []