featureparity 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/bin/fp +6 -0
- data/lib/fp/cli.rb +96 -0
- data/lib/fp/client.rb +151 -0
- data/lib/fp/commands/base.rb +90 -0
- data/lib/fp/commands/help.rb +122 -0
- data/lib/fp/commands/list.rb +104 -0
- data/lib/fp/commands/matrix.rb +79 -0
- data/lib/fp/commands/profile.rb +87 -0
- data/lib/fp/commands/projects.rb +32 -0
- data/lib/fp/commands/propose.rb +66 -0
- data/lib/fp/commands/report.rb +117 -0
- data/lib/fp/commands/setup.rb +148 -0
- data/lib/fp/commands/show.rb +92 -0
- data/lib/fp/commands/surfaces.rb +34 -0
- data/lib/fp/commands/version.rb +14 -0
- data/lib/fp/commands.rb +19 -0
- data/lib/fp/config.rb +111 -0
- data/lib/fp/output.rb +71 -0
- data/lib/fp/version.rb +5 -0
- data/lib/fp.rb +14 -0
- metadata +139 -0
data/lib/fp/config.rb
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Fp
|
|
7
|
+
# Handles configuration: profiles, API keys, API URL.
|
|
8
|
+
# Priority: FP_API_KEY env var > --profile flag > FP_PROFILE env var > default profile
|
|
9
|
+
class Config
|
|
10
|
+
DEFAULT_API_URL = 'https://api.featureparity.dev'
|
|
11
|
+
CONFIG_DIR = File.expand_path('~/.config/fp')
|
|
12
|
+
CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml')
|
|
13
|
+
|
|
14
|
+
attr_reader :api_key, :api_url, :profile_name
|
|
15
|
+
|
|
16
|
+
def initialize(profile: nil)
|
|
17
|
+
@profile_name = resolve_profile_name(profile)
|
|
18
|
+
@api_key = resolve_api_key
|
|
19
|
+
@api_url = resolve_api_url
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def valid?
|
|
23
|
+
!@api_key.nil? && !@api_key.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validation_error
|
|
27
|
+
return nil if valid?
|
|
28
|
+
|
|
29
|
+
<<~MSG
|
|
30
|
+
No API key found. Set FP_API_KEY environment variable or add a profile:
|
|
31
|
+
fp profile add <name> --api-key fp_...
|
|
32
|
+
MSG
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
def load_profiles
|
|
37
|
+
return {} unless File.exist?(CONFIG_FILE)
|
|
38
|
+
|
|
39
|
+
config = YAML.safe_load_file(CONFIG_FILE, permitted_classes: [Symbol]) || {}
|
|
40
|
+
config['profiles'] || {}
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
warn "Warning: Could not load config file: #{e.message}"
|
|
43
|
+
{}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def save_profile(name, api_key:, api_url: nil)
|
|
47
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
|
48
|
+
|
|
49
|
+
# Ensure config file has restricted permissions
|
|
50
|
+
unless File.exist?(CONFIG_FILE)
|
|
51
|
+
File.write(CONFIG_FILE, "---\nprofiles: {}\n")
|
|
52
|
+
File.chmod(0o600, CONFIG_FILE)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
config = YAML.safe_load_file(CONFIG_FILE, permitted_classes: [Symbol]) || {}
|
|
56
|
+
config['profiles'] ||= {}
|
|
57
|
+
config['profiles'][name] = {
|
|
58
|
+
'api_key' => api_key
|
|
59
|
+
}
|
|
60
|
+
config['profiles'][name]['api_url'] = api_url if api_url
|
|
61
|
+
|
|
62
|
+
File.write(CONFIG_FILE, YAML.dump(config))
|
|
63
|
+
File.chmod(0o600, CONFIG_FILE)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def list_profiles
|
|
67
|
+
load_profiles.keys
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def profile_exists?(name)
|
|
71
|
+
load_profiles.key?(name)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def resolve_profile_name(explicit_profile)
|
|
78
|
+
# Explicit --profile flag takes precedence over FP_PROFILE env var
|
|
79
|
+
explicit_profile || ENV['FP_PROFILE']
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def resolve_api_key
|
|
83
|
+
# FP_API_KEY env var always wins
|
|
84
|
+
return ENV['FP_API_KEY'] if ENV['FP_API_KEY'] && !ENV['FP_API_KEY'].empty?
|
|
85
|
+
|
|
86
|
+
# Otherwise, use profile if specified
|
|
87
|
+
return nil unless @profile_name
|
|
88
|
+
|
|
89
|
+
profiles = self.class.load_profiles
|
|
90
|
+
profile = profiles[@profile_name]
|
|
91
|
+
return nil unless profile
|
|
92
|
+
|
|
93
|
+
profile['api_key']
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def resolve_api_url
|
|
97
|
+
# FP_API_URL env var takes precedence
|
|
98
|
+
return ENV['FP_API_URL'] if ENV['FP_API_URL'] && !ENV['FP_API_URL'].empty?
|
|
99
|
+
|
|
100
|
+
# Then profile-specific URL
|
|
101
|
+
if @profile_name
|
|
102
|
+
profiles = self.class.load_profiles
|
|
103
|
+
profile = profiles[@profile_name]
|
|
104
|
+
return profile['api_url'] if profile && profile['api_url']
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Finally, default
|
|
108
|
+
DEFAULT_API_URL
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/fp/output.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'csv'
|
|
5
|
+
|
|
6
|
+
module Fp
|
|
7
|
+
# Output formatting for CLI responses
|
|
8
|
+
# Follows Fizzy CLI conventions: { ok: true/false, data: ..., error: ... }
|
|
9
|
+
class Output
|
|
10
|
+
def initialize(json: false)
|
|
11
|
+
@json = json
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def json?
|
|
15
|
+
@json
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def success(data, summary: nil)
|
|
19
|
+
if @json
|
|
20
|
+
puts JSON.pretty_generate({ ok: true, data: data })
|
|
21
|
+
else
|
|
22
|
+
yield if block_given?
|
|
23
|
+
puts summary if summary
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def error(message, status: nil)
|
|
28
|
+
if @json
|
|
29
|
+
result = { ok: false, error: message }
|
|
30
|
+
result[:status] = status if status
|
|
31
|
+
puts JSON.pretty_generate(result)
|
|
32
|
+
else
|
|
33
|
+
warn "Error: #{message}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def table(headers, rows)
|
|
38
|
+
return if rows.empty?
|
|
39
|
+
|
|
40
|
+
if @json
|
|
41
|
+
# For JSON output, return data as array of hashes
|
|
42
|
+
data = rows.map { |row| headers.zip(row).to_h }
|
|
43
|
+
puts JSON.pretty_generate({ ok: true, data: data })
|
|
44
|
+
else
|
|
45
|
+
# ASCII table
|
|
46
|
+
widths = headers.map.with_index do |header, i|
|
|
47
|
+
[header.to_s.length, *rows.map { |r| r[i].to_s.length }].max
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
separator = '+' + widths.map { |w| '-' * (w + 2) }.join('+') + '+'
|
|
51
|
+
format_row = lambda do |row|
|
|
52
|
+
'| ' + row.map.with_index { |cell, i| cell.to_s.ljust(widths[i]) }.join(' | ') + ' |'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
puts separator
|
|
56
|
+
puts format_row.call(headers)
|
|
57
|
+
puts separator
|
|
58
|
+
rows.each { |row| puts format_row.call(row) }
|
|
59
|
+
puts separator
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def csv(headers, rows)
|
|
64
|
+
csv_string = CSV.generate do |csv|
|
|
65
|
+
csv << headers
|
|
66
|
+
rows.each { |row| csv << row }
|
|
67
|
+
end
|
|
68
|
+
puts csv_string
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/fp/version.rb
ADDED
data/lib/fp.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'fp/version'
|
|
4
|
+
require_relative 'fp/config'
|
|
5
|
+
require_relative 'fp/client'
|
|
6
|
+
require_relative 'fp/output'
|
|
7
|
+
require_relative 'fp/commands'
|
|
8
|
+
require_relative 'fp/cli'
|
|
9
|
+
|
|
10
|
+
module Fp
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
class AuthError < Error; end
|
|
13
|
+
class APIError < Error; end
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: featureparity
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Stowzilla
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: net-http
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.3'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.3'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: json
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: yaml
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.2'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.2'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: uri
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0.12'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.12'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: csv
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
description: Command-line interface for FeatureParity - report evidence, propose requirements,
|
|
84
|
+
view parity matrix.
|
|
85
|
+
email:
|
|
86
|
+
- team@stowzilla.com
|
|
87
|
+
executables:
|
|
88
|
+
- fp
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- bin/fp
|
|
93
|
+
- lib/fp.rb
|
|
94
|
+
- lib/fp/cli.rb
|
|
95
|
+
- lib/fp/client.rb
|
|
96
|
+
- lib/fp/commands.rb
|
|
97
|
+
- lib/fp/commands/base.rb
|
|
98
|
+
- lib/fp/commands/help.rb
|
|
99
|
+
- lib/fp/commands/list.rb
|
|
100
|
+
- lib/fp/commands/matrix.rb
|
|
101
|
+
- lib/fp/commands/profile.rb
|
|
102
|
+
- lib/fp/commands/projects.rb
|
|
103
|
+
- lib/fp/commands/propose.rb
|
|
104
|
+
- lib/fp/commands/report.rb
|
|
105
|
+
- lib/fp/commands/setup.rb
|
|
106
|
+
- lib/fp/commands/show.rb
|
|
107
|
+
- lib/fp/commands/surfaces.rb
|
|
108
|
+
- lib/fp/commands/version.rb
|
|
109
|
+
- lib/fp/config.rb
|
|
110
|
+
- lib/fp/output.rb
|
|
111
|
+
- lib/fp/version.rb
|
|
112
|
+
homepage: https://featureparity.dev
|
|
113
|
+
licenses:
|
|
114
|
+
- MIT
|
|
115
|
+
metadata:
|
|
116
|
+
rubygems_mfa_required: 'true'
|
|
117
|
+
homepage_uri: https://featureparity.dev
|
|
118
|
+
source_code_uri: https://github.com/stowzilla/feature_parity
|
|
119
|
+
changelog_uri: https://github.com/stowzilla/feature_parity/blob/master/cli/CHANGELOG.md
|
|
120
|
+
post_install_message:
|
|
121
|
+
rdoc_options: []
|
|
122
|
+
require_paths:
|
|
123
|
+
- lib
|
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - ">="
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '3.0'
|
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - ">="
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: '0'
|
|
134
|
+
requirements: []
|
|
135
|
+
rubygems_version: 3.5.22
|
|
136
|
+
signing_key:
|
|
137
|
+
specification_version: 4
|
|
138
|
+
summary: FeatureParity CLI
|
|
139
|
+
test_files: []
|