featureparity 0.0.1 → 0.0.2
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 +4 -4
- data/lib/fp/cli.rb +18 -4
- data/lib/fp/commands/config_cmd.rb +130 -0
- data/lib/fp/commands/help.rb +2 -0
- data/lib/fp/commands/list.rb +27 -2
- data/lib/fp/commands/matrix.rb +0 -2
- data/lib/fp/commands/propose.rb +32 -2
- data/lib/fp/commands/setup.rb +9 -2
- data/lib/fp/commands/show.rb +1 -0
- data/lib/fp/commands.rb +1 -0
- data/lib/fp/config.rb +69 -21
- data/lib/fp/output.rb +1 -1
- data/lib/fp/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7fec4ef9bda94cba09b19e5209ed867ff87d6a5312c6c6441234691e406bd2a2
|
|
4
|
+
data.tar.gz: 7fd7a58df29f9a69ce342ed60e5b7ff60b796f286404b81a56108be1f5c83f1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b198114877fb8ca6695f14c38796334b2cf4269e4164509a97a7b0d12cf4fa7f86ecbd4036028fad15fb80eb00d95da5a410542eda6cdcbd7366b33e6c0dab11
|
|
7
|
+
data.tar.gz: 7a6a248d554a4fa13bc5b33591be0868f2714dfc483c81da0ceafaa4fa951f5f915c229ac8096fbda59efae8319c39e135e4b36e4403944c6ceec80cc7f74832
|
data/lib/fp/cli.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Fp
|
|
|
12
12
|
'report' => Commands::Report,
|
|
13
13
|
'matrix' => Commands::Matrix,
|
|
14
14
|
'profile' => Commands::Profile,
|
|
15
|
+
'config' => Commands::ConfigCmd,
|
|
15
16
|
'setup' => Commands::Setup,
|
|
16
17
|
'version' => Commands::Version,
|
|
17
18
|
'help' => Commands::Help
|
|
@@ -32,17 +33,19 @@ module Fp
|
|
|
32
33
|
exit 1
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
# Profile commands don't need auth
|
|
36
|
-
needs_auth = !%w[profile setup version help].include?(command_name)
|
|
36
|
+
# Profile/config commands don't need auth
|
|
37
|
+
needs_auth = !%w[profile config setup version help].include?(command_name)
|
|
37
38
|
|
|
38
39
|
if needs_auth
|
|
39
|
-
config = Config.new(profile: global_opts[:profile])
|
|
40
|
+
config = Config.new(profile: global_opts[:profile], api_url: global_opts[:api_url])
|
|
40
41
|
unless config.valid?
|
|
41
42
|
output = Output.new(json: global_opts[:json])
|
|
42
43
|
output.error(config.validation_error.strip)
|
|
43
44
|
exit 1
|
|
44
45
|
end
|
|
45
46
|
client = Client.new(config)
|
|
47
|
+
else
|
|
48
|
+
config = Config.new(profile: global_opts[:profile], api_url: global_opts[:api_url])
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
output = Output.new(json: global_opts[:json])
|
|
@@ -60,7 +63,7 @@ module Fp
|
|
|
60
63
|
private
|
|
61
64
|
|
|
62
65
|
def extract_global_options(args)
|
|
63
|
-
opts = { json: false, profile: nil }
|
|
66
|
+
opts = { json: false, profile: nil, api_url: nil }
|
|
64
67
|
|
|
65
68
|
# Extract --json flag
|
|
66
69
|
if args.delete('--json')
|
|
@@ -89,6 +92,17 @@ module Fp
|
|
|
89
92
|
opts[:profile] = args.delete_at(idx)
|
|
90
93
|
end
|
|
91
94
|
|
|
95
|
+
# Extract --api-url flag
|
|
96
|
+
if (idx = args.index('--api-url'))
|
|
97
|
+
args.delete_at(idx)
|
|
98
|
+
value = args[idx]
|
|
99
|
+
if value.nil? || value.start_with?('-')
|
|
100
|
+
$stderr.puts "Error: --api-url requires a value"
|
|
101
|
+
exit 1
|
|
102
|
+
end
|
|
103
|
+
opts[:api_url] = args.delete_at(idx)
|
|
104
|
+
end
|
|
105
|
+
|
|
92
106
|
opts
|
|
93
107
|
end
|
|
94
108
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fp
|
|
4
|
+
module Commands
|
|
5
|
+
# fp config get|set|unset|list
|
|
6
|
+
# Manage global CLI settings (stored in ~/.config/fp/config.yml)
|
|
7
|
+
class ConfigCmd < Base
|
|
8
|
+
ALLOWED_SETTINGS = %w[api_url].freeze
|
|
9
|
+
|
|
10
|
+
def run(args)
|
|
11
|
+
subcommand = args.shift
|
|
12
|
+
|
|
13
|
+
case subcommand
|
|
14
|
+
when 'set'
|
|
15
|
+
set_setting(args)
|
|
16
|
+
when 'get'
|
|
17
|
+
get_setting(args)
|
|
18
|
+
when 'unset'
|
|
19
|
+
unset_setting(args)
|
|
20
|
+
when 'list'
|
|
21
|
+
list_settings
|
|
22
|
+
else
|
|
23
|
+
show_usage
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def set_setting(args)
|
|
30
|
+
key = args.shift
|
|
31
|
+
value = args.shift
|
|
32
|
+
|
|
33
|
+
unless key
|
|
34
|
+
output.error('Usage: fp config set <key> <value>')
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
unless ALLOWED_SETTINGS.include?(key)
|
|
39
|
+
output.error("Unknown setting: '#{key}'. Allowed: #{ALLOWED_SETTINGS.join(', ')}")
|
|
40
|
+
exit 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
unless value
|
|
44
|
+
output.error("Value is required. Usage: fp config set #{key} <value>")
|
|
45
|
+
exit 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Config.set_setting(key, value)
|
|
49
|
+
|
|
50
|
+
output.success({ key: key, value: value }) do
|
|
51
|
+
puts "Set #{key} = #{value}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def get_setting(args)
|
|
56
|
+
key = args.shift
|
|
57
|
+
|
|
58
|
+
unless key
|
|
59
|
+
output.error('Usage: fp config get <key>')
|
|
60
|
+
exit 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
value = Config.get_setting(key)
|
|
64
|
+
|
|
65
|
+
if value
|
|
66
|
+
output.success({ key: key, value: value }) do
|
|
67
|
+
puts value
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
output.success({ key: key, value: nil }) do
|
|
71
|
+
puts "(not set)"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def unset_setting(args)
|
|
77
|
+
key = args.shift
|
|
78
|
+
|
|
79
|
+
unless key
|
|
80
|
+
output.error('Usage: fp config unset <key>')
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
Config.set_setting(key, nil)
|
|
85
|
+
|
|
86
|
+
output.success({ key: key }) do
|
|
87
|
+
puts "Unset #{key}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def list_settings
|
|
92
|
+
settings = {}
|
|
93
|
+
ALLOWED_SETTINGS.each do |key|
|
|
94
|
+
value = Config.get_setting(key)
|
|
95
|
+
settings[key] = value if value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
output.success({ settings: settings }) do
|
|
99
|
+
if settings.empty?
|
|
100
|
+
puts "No global settings configured."
|
|
101
|
+
puts
|
|
102
|
+
puts "Set one with:"
|
|
103
|
+
puts " fp config set api_url https://api.dev.featureparity.dev"
|
|
104
|
+
else
|
|
105
|
+
puts "Global settings:"
|
|
106
|
+
settings.each { |k, v| puts " #{k} = #{v}" }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def show_usage
|
|
112
|
+
output.error('Usage: fp config <set|get|unset|list>')
|
|
113
|
+
puts
|
|
114
|
+
puts 'Commands:'
|
|
115
|
+
puts ' fp config set <key> <value> Set a global setting'
|
|
116
|
+
puts ' fp config get <key> Get a setting value'
|
|
117
|
+
puts ' fp config unset <key> Remove a setting'
|
|
118
|
+
puts ' fp config list Show all global settings'
|
|
119
|
+
puts
|
|
120
|
+
puts 'Available settings:'
|
|
121
|
+
puts ' api_url Base URL for the FeatureParity API'
|
|
122
|
+
puts
|
|
123
|
+
puts 'Examples:'
|
|
124
|
+
puts ' fp config set api_url https://api.dev.featureparity.dev'
|
|
125
|
+
puts ' fp config get api_url'
|
|
126
|
+
exit 1
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/fp/commands/help.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Fp
|
|
|
20
20
|
report <slug> --project <slug>... Report evidence for a requirement
|
|
21
21
|
matrix --project <slug> Show the parity matrix
|
|
22
22
|
profile add|list Manage named profiles
|
|
23
|
+
config set|get|unset|list Manage global settings
|
|
23
24
|
setup Interactive first-time setup
|
|
24
25
|
version Show version
|
|
25
26
|
help Show this help
|
|
@@ -28,6 +29,7 @@ module Fp
|
|
|
28
29
|
--json Output as JSON envelope
|
|
29
30
|
--profile <name> Use a named profile
|
|
30
31
|
-p <name> Alias for --profile
|
|
32
|
+
--api-url <url> Override API URL for this command
|
|
31
33
|
|
|
32
34
|
AUTHENTICATION
|
|
33
35
|
Set FP_API_KEY environment variable, or use a profile:
|
data/lib/fp/commands/list.rb
CHANGED
|
@@ -52,11 +52,36 @@ module Fp
|
|
|
52
52
|
if requirements.empty?
|
|
53
53
|
puts "No #{status_filter} requirements found."
|
|
54
54
|
else
|
|
55
|
+
# Group into tree structure: parents first, then children indented
|
|
56
|
+
parents = requirements.select { |r| r['parent_id'].nil? || r['parent_id'] == '' }
|
|
57
|
+
children_by_parent = requirements
|
|
58
|
+
.select { |r| r['parent_id'] && r['parent_id'] != '' }
|
|
59
|
+
.group_by { |r| r['parent_id'] }
|
|
60
|
+
|
|
61
|
+
# Orphaned children (parent not in filtered set) show at top level
|
|
62
|
+
parent_ids = parents.map { |p| p['id'] }.to_set
|
|
63
|
+
orphans = requirements.select { |r| r['parent_id'] && r['parent_id'] != '' && !parent_ids.include?(r['parent_id']) }
|
|
64
|
+
|
|
55
65
|
headers = %w[SLUG TITLE SURFACES STATUS]
|
|
56
|
-
rows =
|
|
66
|
+
rows = []
|
|
67
|
+
|
|
68
|
+
parents.each do |r|
|
|
57
69
|
surfaces = (r['required_surfaces'] || []).join(', ')
|
|
58
|
-
[r['slug'], truncate(r['title'], 40), surfaces.empty? ? '-' : surfaces, r['status']]
|
|
70
|
+
rows << [r['slug'], truncate(r['title'], 40), surfaces.empty? ? '-' : surfaces, r['status']]
|
|
71
|
+
|
|
72
|
+
# Add children indented
|
|
73
|
+
(children_by_parent[r['id']] || []).each do |child|
|
|
74
|
+
child_surfaces = (child['required_surfaces'] || []).join(', ')
|
|
75
|
+
rows << [" └ #{child['slug']}", truncate(child['title'], 36), child_surfaces.empty? ? '-' : child_surfaces, child['status']]
|
|
76
|
+
end
|
|
59
77
|
end
|
|
78
|
+
|
|
79
|
+
# Show orphans at top level
|
|
80
|
+
orphans.each do |r|
|
|
81
|
+
surfaces = (r['required_surfaces'] || []).join(', ')
|
|
82
|
+
rows << [r['slug'], truncate(r['title'], 40), surfaces.empty? ? '-' : surfaces, r['status']]
|
|
83
|
+
end
|
|
84
|
+
|
|
60
85
|
output.table(headers, rows)
|
|
61
86
|
end
|
|
62
87
|
end
|
data/lib/fp/commands/matrix.rb
CHANGED
data/lib/fp/commands/propose.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Fp
|
|
4
4
|
module Commands
|
|
5
|
-
# fp propose --project <slug> --slug X --name "..." --why "..." --required a,b,c --acceptance "..."
|
|
5
|
+
# fp propose --project <slug> --slug X --name "..." --why "..." --required a,b,c --acceptance "..." --parent <slug>
|
|
6
6
|
# Create a new requirement as draft
|
|
7
7
|
class Propose < Base
|
|
8
8
|
KNOWN_FLAGS = {
|
|
@@ -11,7 +11,8 @@ module Fp
|
|
|
11
11
|
name: :string,
|
|
12
12
|
why: :string,
|
|
13
13
|
required: :string,
|
|
14
|
-
acceptance: :string
|
|
14
|
+
acceptance: :string,
|
|
15
|
+
parent: :string
|
|
15
16
|
}.freeze
|
|
16
17
|
|
|
17
18
|
def run(args)
|
|
@@ -38,6 +39,13 @@ module Fp
|
|
|
38
39
|
}
|
|
39
40
|
params[:why] = opts[:why] if opts[:why]
|
|
40
41
|
params[:acceptance] = opts[:acceptance] if opts[:acceptance]
|
|
42
|
+
|
|
43
|
+
# Resolve parent slug to parent_id if provided
|
|
44
|
+
if opts[:parent]
|
|
45
|
+
parent_id = resolve_parent_id(workspace_id, opts[:parent])
|
|
46
|
+
params[:parent_id] = parent_id
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
# Note: status is NOT sent - the API enforces draft for agents
|
|
42
50
|
|
|
43
51
|
result = client.create_requirement(params)
|
|
@@ -55,12 +63,34 @@ module Fp
|
|
|
55
63
|
puts " Title: #{requirement['title']}"
|
|
56
64
|
puts " Status: #{requirement['status']}"
|
|
57
65
|
puts " Surfaces: #{(requirement['required_surfaces'] || []).join(', ')}"
|
|
66
|
+
puts " Parent: #{opts[:parent] || '(none)'}" if opts[:parent]
|
|
58
67
|
puts
|
|
59
68
|
puts '⚠️ This requirement is a DRAFT.'
|
|
60
69
|
puts ' A human must activate it in the web app before it appears in the parity matrix.'
|
|
61
70
|
puts ' Agents cannot activate requirements.'
|
|
62
71
|
end
|
|
63
72
|
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Resolve a parent requirement slug to its ID
|
|
77
|
+
def resolve_parent_id(workspace_id, parent_slug)
|
|
78
|
+
result = client.list_requirements(workspace_id: workspace_id)
|
|
79
|
+
unless result[:ok]
|
|
80
|
+
output.error("Failed to resolve parent slug '#{parent_slug}': #{result[:error]}")
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
requirements = result[:data]['requirements'] || []
|
|
85
|
+
parent = requirements.find { |r| r['slug'] == parent_slug }
|
|
86
|
+
|
|
87
|
+
unless parent
|
|
88
|
+
output.error("Parent requirement '#{parent_slug}' not found in this workspace")
|
|
89
|
+
exit 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
parent['id']
|
|
93
|
+
end
|
|
64
94
|
end
|
|
65
95
|
end
|
|
66
96
|
end
|
data/lib/fp/commands/setup.rb
CHANGED
|
@@ -22,13 +22,16 @@ module Fp
|
|
|
22
22
|
|
|
23
23
|
api_key = resolve_api_key(opts)
|
|
24
24
|
profile_name = resolve_profile_name(opts)
|
|
25
|
-
api_url = opts[:api_url]
|
|
25
|
+
api_url = opts[:api_url] || Config.get_setting('api_url')
|
|
26
26
|
|
|
27
27
|
puts "Validating API key..."
|
|
28
28
|
validate_key!(api_key, api_url)
|
|
29
29
|
|
|
30
30
|
save_profile!(profile_name, api_key, api_url)
|
|
31
31
|
|
|
32
|
+
effective_url = api_url || Config::DEFAULT_API_URL
|
|
33
|
+
domain = URI.parse(effective_url).host.sub(/^api\./, '') rescue 'featureparity.dev'
|
|
34
|
+
|
|
32
35
|
puts
|
|
33
36
|
puts "✅ Setup complete!"
|
|
34
37
|
puts
|
|
@@ -54,7 +57,11 @@ module Fp
|
|
|
54
57
|
exit 1
|
|
55
58
|
end
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
api_url = opts[:api_url] || Config.get_setting('api_url')
|
|
61
|
+
effective_url = api_url || Config::DEFAULT_API_URL
|
|
62
|
+
domain = URI.parse(effective_url).host.sub(/^api\./, '') rescue 'featureparity.dev'
|
|
63
|
+
|
|
64
|
+
puts "Get your API key from: https://#{domain}"
|
|
58
65
|
puts "(Workspace Settings → API Keys → Mint Key)"
|
|
59
66
|
puts
|
|
60
67
|
print "Paste your API key (starts with fp_): "
|
data/lib/fp/commands/show.rb
CHANGED
data/lib/fp/commands.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative 'commands/propose'
|
|
|
9
9
|
require_relative 'commands/report'
|
|
10
10
|
require_relative 'commands/matrix'
|
|
11
11
|
require_relative 'commands/profile'
|
|
12
|
+
require_relative 'commands/config_cmd'
|
|
12
13
|
require_relative 'commands/setup'
|
|
13
14
|
require_relative 'commands/version'
|
|
14
15
|
require_relative 'commands/help'
|
data/lib/fp/config.rb
CHANGED
|
@@ -5,7 +5,17 @@ require 'fileutils'
|
|
|
5
5
|
|
|
6
6
|
module Fp
|
|
7
7
|
# Handles configuration: profiles, API keys, API URL.
|
|
8
|
-
#
|
|
8
|
+
#
|
|
9
|
+
# API URL resolution priority (highest wins):
|
|
10
|
+
# 1. --api-url CLI flag (passed via constructor)
|
|
11
|
+
# 2. FP_API_URL env var
|
|
12
|
+
# 3. Profile-specific api_url (in profile config)
|
|
13
|
+
# 4. Global api_url setting (top-level in config.yml)
|
|
14
|
+
# 5. DEFAULT_API_URL
|
|
15
|
+
#
|
|
16
|
+
# API key resolution priority:
|
|
17
|
+
# 1. FP_API_KEY env var
|
|
18
|
+
# 2. Profile api_key (--profile flag > FP_PROFILE env var)
|
|
9
19
|
class Config
|
|
10
20
|
DEFAULT_API_URL = 'https://api.featureparity.dev'
|
|
11
21
|
CONFIG_DIR = File.expand_path('~/.config/fp')
|
|
@@ -13,8 +23,9 @@ module Fp
|
|
|
13
23
|
|
|
14
24
|
attr_reader :api_key, :api_url, :profile_name
|
|
15
25
|
|
|
16
|
-
def initialize(profile: nil)
|
|
26
|
+
def initialize(profile: nil, api_url: nil)
|
|
17
27
|
@profile_name = resolve_profile_name(profile)
|
|
28
|
+
@explicit_api_url = api_url
|
|
18
29
|
@api_key = resolve_api_key
|
|
19
30
|
@api_url = resolve_api_url
|
|
20
31
|
end
|
|
@@ -33,34 +44,51 @@ module Fp
|
|
|
33
44
|
end
|
|
34
45
|
|
|
35
46
|
class << self
|
|
36
|
-
def
|
|
47
|
+
def load_config
|
|
37
48
|
return {} unless File.exist?(CONFIG_FILE)
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
config['profiles'] || {}
|
|
50
|
+
YAML.safe_load_file(CONFIG_FILE, permitted_classes: [Symbol]) || {}
|
|
41
51
|
rescue StandardError => e
|
|
42
52
|
warn "Warning: Could not load config file: #{e.message}"
|
|
43
53
|
{}
|
|
44
54
|
end
|
|
45
55
|
|
|
56
|
+
def load_profiles
|
|
57
|
+
config = load_config
|
|
58
|
+
config['profiles'] || {}
|
|
59
|
+
end
|
|
60
|
+
|
|
46
61
|
def save_profile(name, api_key:, api_url: nil)
|
|
47
62
|
FileUtils.mkdir_p(CONFIG_DIR)
|
|
63
|
+
ensure_config_file!
|
|
48
64
|
|
|
49
|
-
|
|
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]) || {}
|
|
65
|
+
config = load_config
|
|
56
66
|
config['profiles'] ||= {}
|
|
57
|
-
config['profiles'][name] = {
|
|
58
|
-
'api_key' => api_key
|
|
59
|
-
}
|
|
67
|
+
config['profiles'][name] = { 'api_key' => api_key }
|
|
60
68
|
config['profiles'][name]['api_url'] = api_url if api_url
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
write_config!(config)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get a global setting (top-level key in config.yml)
|
|
74
|
+
def get_setting(key)
|
|
75
|
+
config = load_config
|
|
76
|
+
config[key.to_s]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Set a global setting (top-level key in config.yml)
|
|
80
|
+
def set_setting(key, value)
|
|
81
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
|
82
|
+
ensure_config_file!
|
|
83
|
+
|
|
84
|
+
config = load_config
|
|
85
|
+
if value.nil?
|
|
86
|
+
config.delete(key.to_s)
|
|
87
|
+
else
|
|
88
|
+
config[key.to_s] = value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
write_config!(config)
|
|
64
92
|
end
|
|
65
93
|
|
|
66
94
|
def list_profiles
|
|
@@ -70,12 +98,25 @@ module Fp
|
|
|
70
98
|
def profile_exists?(name)
|
|
71
99
|
load_profiles.key?(name)
|
|
72
100
|
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def ensure_config_file!
|
|
105
|
+
return if File.exist?(CONFIG_FILE)
|
|
106
|
+
|
|
107
|
+
File.write(CONFIG_FILE, "---\nprofiles: {}\n")
|
|
108
|
+
File.chmod(0o600, CONFIG_FILE)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def write_config!(config)
|
|
112
|
+
File.write(CONFIG_FILE, YAML.dump(config))
|
|
113
|
+
File.chmod(0o600, CONFIG_FILE)
|
|
114
|
+
end
|
|
73
115
|
end
|
|
74
116
|
|
|
75
117
|
private
|
|
76
118
|
|
|
77
119
|
def resolve_profile_name(explicit_profile)
|
|
78
|
-
# Explicit --profile flag takes precedence over FP_PROFILE env var
|
|
79
120
|
explicit_profile || ENV['FP_PROFILE']
|
|
80
121
|
end
|
|
81
122
|
|
|
@@ -94,17 +135,24 @@ module Fp
|
|
|
94
135
|
end
|
|
95
136
|
|
|
96
137
|
def resolve_api_url
|
|
97
|
-
#
|
|
138
|
+
# 1. Explicit --api-url flag
|
|
139
|
+
return @explicit_api_url if @explicit_api_url && !@explicit_api_url.empty?
|
|
140
|
+
|
|
141
|
+
# 2. FP_API_URL env var
|
|
98
142
|
return ENV['FP_API_URL'] if ENV['FP_API_URL'] && !ENV['FP_API_URL'].empty?
|
|
99
143
|
|
|
100
|
-
#
|
|
144
|
+
# 3. Profile-specific URL
|
|
101
145
|
if @profile_name
|
|
102
146
|
profiles = self.class.load_profiles
|
|
103
147
|
profile = profiles[@profile_name]
|
|
104
148
|
return profile['api_url'] if profile && profile['api_url']
|
|
105
149
|
end
|
|
106
150
|
|
|
107
|
-
#
|
|
151
|
+
# 4. Global api_url setting
|
|
152
|
+
global_url = self.class.get_setting('api_url')
|
|
153
|
+
return global_url if global_url && !global_url.empty?
|
|
154
|
+
|
|
155
|
+
# 5. Default
|
|
108
156
|
DEFAULT_API_URL
|
|
109
157
|
end
|
|
110
158
|
end
|
data/lib/fp/output.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require 'csv'
|
|
5
4
|
|
|
6
5
|
module Fp
|
|
7
6
|
# Output formatting for CLI responses
|
|
@@ -61,6 +60,7 @@ module Fp
|
|
|
61
60
|
end
|
|
62
61
|
|
|
63
62
|
def csv(headers, rows)
|
|
63
|
+
require 'csv'
|
|
64
64
|
csv_string = CSV.generate do |csv|
|
|
65
65
|
csv << headers
|
|
66
66
|
rows.each { |row| csv << row }
|
data/lib/fp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: featureparity
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
@@ -95,6 +95,7 @@ files:
|
|
|
95
95
|
- lib/fp/client.rb
|
|
96
96
|
- lib/fp/commands.rb
|
|
97
97
|
- lib/fp/commands/base.rb
|
|
98
|
+
- lib/fp/commands/config_cmd.rb
|
|
98
99
|
- lib/fp/commands/help.rb
|
|
99
100
|
- lib/fp/commands/list.rb
|
|
100
101
|
- lib/fp/commands/matrix.rb
|