featureparity 0.0.3 → 0.0.4
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 +2 -1
- data/lib/fp/commands/config_cmd.rb +10 -1
- data/lib/fp/commands/help.rb +6 -0
- data/lib/fp/commands/profile.rb +28 -7
- data/lib/fp/commands/setup.rb +45 -6
- data/lib/fp/commands/whoami.rb +55 -0
- data/lib/fp/commands.rb +1 -0
- data/lib/fp/config.rb +165 -12
- data/lib/fp/version.rb +1 -1
- data/skills/fp/references/cli.md +30 -4
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b5b8ef2f8e259e3d8d7ecd0c547ad8bef6f6202e0b3564bd8d789de905661ed
|
|
4
|
+
data.tar.gz: 79d02f1bfdd3c95b1d94a40b2cda406d656c4970a0c6b3f60b792415b4417c62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e737b8e544deaa42e6ada2f6af065c5afdeaa46afa51d5365c5f8a92fa77572e1f5c1a13715a11eb0655416a08e487f4b8e9bb7f7ffa9efc3a6042abcc1c45ad
|
|
7
|
+
data.tar.gz: 247e22500f20fcd047a617232020f29a87987ac2376a78176a69969a467b8d8164b4dd6a427c73c558965b7aa9543ffb2e0b659416974ec5272f76d2618c415e
|
data/lib/fp/cli.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Fp
|
|
|
15
15
|
'config' => Commands::ConfigCmd,
|
|
16
16
|
'repos' => Commands::Repos,
|
|
17
17
|
'setup' => Commands::Setup,
|
|
18
|
+
'whoami' => Commands::Whoami,
|
|
18
19
|
'version' => Commands::Version,
|
|
19
20
|
'help' => Commands::Help
|
|
20
21
|
}.freeze
|
|
@@ -35,7 +36,7 @@ module Fp
|
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
# Profile/config commands don't need auth
|
|
38
|
-
needs_auth = !%w[profile config repos setup version help].include?(command_name)
|
|
39
|
+
needs_auth = !%w[profile config repos setup whoami version help].include?(command_name)
|
|
39
40
|
|
|
40
41
|
if needs_auth
|
|
41
42
|
config = Config.new(profile: global_opts[:profile], api_url: global_opts[:api_url])
|
|
@@ -19,6 +19,9 @@ module Fp
|
|
|
19
19
|
unset_setting(args)
|
|
20
20
|
when 'list'
|
|
21
21
|
list_settings
|
|
22
|
+
when 'whoami'
|
|
23
|
+
# Alias for `fp whoami` — show the fully resolved configuration.
|
|
24
|
+
Whoami.new(client: client, output: output, config: config).run(args)
|
|
22
25
|
else
|
|
23
26
|
show_usage
|
|
24
27
|
end
|
|
@@ -105,17 +108,22 @@ module Fp
|
|
|
105
108
|
puts "Global settings:"
|
|
106
109
|
settings.each { |k, v| puts " #{k} = #{v}" }
|
|
107
110
|
end
|
|
111
|
+
puts
|
|
112
|
+
puts "This shows only global settings. To see the fully resolved config"
|
|
113
|
+
puts "(profile, environment, API URL, and key — with the source of each):"
|
|
114
|
+
puts " fp whoami"
|
|
108
115
|
end
|
|
109
116
|
end
|
|
110
117
|
|
|
111
118
|
def show_usage
|
|
112
|
-
output.error('Usage: fp config <set|get|unset|list>')
|
|
119
|
+
output.error('Usage: fp config <set|get|unset|list|whoami>')
|
|
113
120
|
puts
|
|
114
121
|
puts 'Commands:'
|
|
115
122
|
puts ' fp config set <key> <value> Set a global setting'
|
|
116
123
|
puts ' fp config get <key> Get a setting value'
|
|
117
124
|
puts ' fp config unset <key> Remove a setting'
|
|
118
125
|
puts ' fp config list Show all global settings'
|
|
126
|
+
puts ' fp config whoami Show the resolved config (alias of `fp whoami`)'
|
|
119
127
|
puts
|
|
120
128
|
puts 'Available settings:'
|
|
121
129
|
puts ' api_url Base URL for the FeatureParity API'
|
|
@@ -123,6 +131,7 @@ module Fp
|
|
|
123
131
|
puts 'Examples:'
|
|
124
132
|
puts ' fp config set api_url https://api.dev.featureparity.dev'
|
|
125
133
|
puts ' fp config get api_url'
|
|
134
|
+
puts ' fp config whoami'
|
|
126
135
|
exit 1
|
|
127
136
|
end
|
|
128
137
|
end
|
data/lib/fp/commands/help.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Fp
|
|
|
22
22
|
matrix --project <slug> Show the parity matrix
|
|
23
23
|
profile add|list Manage named profiles
|
|
24
24
|
config set|get|unset|list Manage global settings
|
|
25
|
+
whoami Show the resolved config (profile, env, API URL, key)
|
|
25
26
|
repos set|get|list|unset Map surfaces to local repo paths
|
|
26
27
|
setup Interactive first-time setup
|
|
27
28
|
version Show version
|
|
@@ -56,6 +57,11 @@ module Fp
|
|
|
56
57
|
# First-time setup (interactive)
|
|
57
58
|
fp setup
|
|
58
59
|
|
|
60
|
+
# Show the resolved configuration (which profile/env/API URL/key is in effect)
|
|
61
|
+
fp whoami
|
|
62
|
+
fp whoami --json
|
|
63
|
+
fp --profile staging whoami
|
|
64
|
+
|
|
59
65
|
# Non-interactive setup for CI/automation
|
|
60
66
|
fp setup --api-key fp_... --profile ci-agent --non-interactive
|
|
61
67
|
|
data/lib/fp/commands/profile.rb
CHANGED
|
@@ -7,7 +7,8 @@ module Fp
|
|
|
7
7
|
class Profile < Base
|
|
8
8
|
KNOWN_FLAGS = {
|
|
9
9
|
api_key: :string,
|
|
10
|
-
api_url: :string
|
|
10
|
+
api_url: :string,
|
|
11
|
+
environment: :string
|
|
11
12
|
}.freeze
|
|
12
13
|
|
|
13
14
|
def run(args)
|
|
@@ -42,11 +43,18 @@ module Fp
|
|
|
42
43
|
exit 1
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
# The global CLI parser consumes --api-url before the command runs, so
|
|
47
|
+
# honor it via the config object when the local flag wasn't captured.
|
|
48
|
+
api_url = opts[:api_url] || config&.explicit_api_url
|
|
49
|
+
Config.save_profile(name, api_key: opts[:api_key], api_url: api_url, environment: opts[:environment])
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
effective_url = api_url || Config.get_setting('api_url') || Config::DEFAULT_API_URL
|
|
52
|
+
environment = opts[:environment] || Config.environment_for(effective_url)
|
|
53
|
+
|
|
54
|
+
output.success({ profile: name, api_url: effective_url, environment: environment }) do
|
|
48
55
|
puts "Profile '#{name}' saved."
|
|
49
|
-
puts " API URL:
|
|
56
|
+
puts " API URL: #{effective_url}"
|
|
57
|
+
puts " Environment: #{environment}" if environment
|
|
50
58
|
puts
|
|
51
59
|
puts 'Use this profile with:'
|
|
52
60
|
puts " fp --profile #{name} projects"
|
|
@@ -65,11 +73,23 @@ module Fp
|
|
|
65
73
|
puts ' fp profile add <name> --api-key fp_...'
|
|
66
74
|
end
|
|
67
75
|
else
|
|
68
|
-
|
|
76
|
+
raw = Config.load_profiles
|
|
77
|
+
detailed = profiles.map do |name|
|
|
78
|
+
p = raw[name] || {}
|
|
79
|
+
url = p['api_url'] || Config.get_setting('api_url') || Config::DEFAULT_API_URL
|
|
80
|
+
env = p['environment'] || Config.environment_for(url)
|
|
81
|
+
{ name: name, environment: env, api_url: url }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
output.success({ profiles: detailed }) do
|
|
69
85
|
puts 'Configured profiles:'
|
|
70
|
-
|
|
86
|
+
detailed.each do |d|
|
|
87
|
+
tag = d[:environment] ? " (#{d[:environment]})" : ''
|
|
88
|
+
puts " - #{d[:name]}#{tag}"
|
|
89
|
+
end
|
|
71
90
|
puts
|
|
72
91
|
puts 'Note: API keys are never displayed. Use --profile <name> to select.'
|
|
92
|
+
puts 'Run `fp whoami` to see the fully resolved configuration.'
|
|
73
93
|
end
|
|
74
94
|
end
|
|
75
95
|
end
|
|
@@ -78,7 +98,8 @@ module Fp
|
|
|
78
98
|
output.error('Usage: fp profile <add|list>')
|
|
79
99
|
puts
|
|
80
100
|
puts 'Commands:'
|
|
81
|
-
puts ' fp profile add <name> --api-key fp_...
|
|
101
|
+
puts ' fp profile add <name> --api-key fp_... [--api-url URL] [--environment ENV]'
|
|
102
|
+
puts ' Add a named profile'
|
|
82
103
|
puts ' fp profile list List configured profiles'
|
|
83
104
|
exit 1
|
|
84
105
|
end
|
data/lib/fp/commands/setup.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Fp
|
|
|
10
10
|
api_key: :string,
|
|
11
11
|
profile: :string,
|
|
12
12
|
api_url: :string,
|
|
13
|
+
environment: :string,
|
|
13
14
|
non_interactive: :boolean
|
|
14
15
|
}.freeze
|
|
15
16
|
|
|
@@ -22,19 +23,22 @@ module Fp
|
|
|
22
23
|
|
|
23
24
|
api_key = resolve_api_key(opts)
|
|
24
25
|
profile_name = resolve_profile_name(opts)
|
|
25
|
-
api_url = opts
|
|
26
|
+
api_url = resolve_api_url(opts)
|
|
27
|
+
environment = opts[:environment] || Config.environment_for(api_url || Config::DEFAULT_API_URL)
|
|
26
28
|
|
|
27
29
|
puts "Validating API key..."
|
|
28
30
|
validate_key!(api_key, api_url)
|
|
29
31
|
|
|
30
|
-
save_profile!(profile_name, api_key, api_url)
|
|
32
|
+
save_profile!(profile_name, api_key, api_url, environment)
|
|
31
33
|
|
|
32
34
|
effective_url = api_url || Config::DEFAULT_API_URL
|
|
33
|
-
domain = URI.parse(effective_url).host.sub(/^api\./, '') rescue 'featureparity.dev'
|
|
34
35
|
|
|
35
36
|
puts
|
|
36
37
|
puts "✅ Setup complete!"
|
|
37
38
|
puts
|
|
39
|
+
puts " API URL: #{effective_url}"
|
|
40
|
+
puts " Environment: #{environment}" if environment
|
|
41
|
+
puts
|
|
38
42
|
puts "Your profile '#{profile_name}' is ready. Usage:"
|
|
39
43
|
puts
|
|
40
44
|
puts " fp --profile #{profile_name} projects"
|
|
@@ -42,6 +46,9 @@ module Fp
|
|
|
42
46
|
puts
|
|
43
47
|
puts "Or set FP_API_KEY to skip profiles:"
|
|
44
48
|
puts " export FP_API_KEY=#{api_key[0..6]}..."
|
|
49
|
+
puts
|
|
50
|
+
puts "Verify the resolved configuration any time with:"
|
|
51
|
+
puts " fp whoami"
|
|
45
52
|
end
|
|
46
53
|
|
|
47
54
|
private
|
|
@@ -57,7 +64,7 @@ module Fp
|
|
|
57
64
|
exit 1
|
|
58
65
|
end
|
|
59
66
|
|
|
60
|
-
api_url = opts[:api_url] || Config.get_setting('api_url')
|
|
67
|
+
api_url = opts[:api_url] || config&.explicit_api_url || Config.get_setting('api_url')
|
|
61
68
|
effective_url = api_url || Config::DEFAULT_API_URL
|
|
62
69
|
domain = URI.parse(effective_url).host.sub(/^api\./, '') rescue 'featureparity.dev'
|
|
63
70
|
|
|
@@ -78,10 +85,17 @@ module Fp
|
|
|
78
85
|
end
|
|
79
86
|
|
|
80
87
|
def resolve_profile_name(opts)
|
|
88
|
+
# --profile / FP_PROFILE may be consumed by the global parser; honor it
|
|
89
|
+
# via config, but only when explicitly provided (not the implicit
|
|
90
|
+
# "default" profile fallback) so interactive setup still prompts.
|
|
81
91
|
if opts[:profile]
|
|
82
92
|
return opts[:profile]
|
|
83
93
|
end
|
|
84
94
|
|
|
95
|
+
if config && %w[flag env\ (FP_PROFILE)].include?(config.profile_source)
|
|
96
|
+
return config.profile_name
|
|
97
|
+
end
|
|
98
|
+
|
|
85
99
|
if opts[:non_interactive]
|
|
86
100
|
return 'default'
|
|
87
101
|
end
|
|
@@ -93,6 +107,31 @@ module Fp
|
|
|
93
107
|
name.nil? || name.empty? ? 'default' : name
|
|
94
108
|
end
|
|
95
109
|
|
|
110
|
+
# Resolve the API URL for setup. Priority: --api-url flag > global setting.
|
|
111
|
+
# When neither is set and we're interactive, prompt the user so they can
|
|
112
|
+
# target a non-default environment (self-hosted, staging, ephemeral PR env)
|
|
113
|
+
# instead of silently defaulting to production.
|
|
114
|
+
def resolve_api_url(opts)
|
|
115
|
+
return opts[:api_url] if opts[:api_url] && !opts[:api_url].empty?
|
|
116
|
+
|
|
117
|
+
# The global CLI parser consumes --api-url before setup runs; honor it.
|
|
118
|
+
flag_url = config&.explicit_api_url
|
|
119
|
+
return flag_url if flag_url
|
|
120
|
+
|
|
121
|
+
existing = Config.get_setting('api_url')
|
|
122
|
+
return existing if existing && !existing.empty?
|
|
123
|
+
|
|
124
|
+
# Non-interactive: fall back to the default (nil => DEFAULT_API_URL).
|
|
125
|
+
return nil if opts[:non_interactive]
|
|
126
|
+
|
|
127
|
+
default = Config::DEFAULT_API_URL
|
|
128
|
+
print "API URL (default: #{default}): "
|
|
129
|
+
$stdout.flush
|
|
130
|
+
url = $stdin.gets&.strip
|
|
131
|
+
|
|
132
|
+
url.nil? || url.empty? ? nil : url
|
|
133
|
+
end
|
|
134
|
+
|
|
96
135
|
def validate_key_format!(key)
|
|
97
136
|
unless key.start_with?('fp_')
|
|
98
137
|
output.error("API key must start with 'fp_' prefix. Got: #{key[0..3]}...")
|
|
@@ -146,8 +185,8 @@ module Fp
|
|
|
146
185
|
exit 1
|
|
147
186
|
end
|
|
148
187
|
|
|
149
|
-
def save_profile!(name, api_key, api_url)
|
|
150
|
-
Config.save_profile(name, api_key: api_key, api_url: api_url)
|
|
188
|
+
def save_profile!(name, api_key, api_url, environment = nil)
|
|
189
|
+
Config.save_profile(name, api_key: api_key, api_url: api_url, environment: environment)
|
|
151
190
|
puts " ✓ Profile '#{name}' saved to ~/.config/fp/config.yml"
|
|
152
191
|
end
|
|
153
192
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fp
|
|
4
|
+
module Commands
|
|
5
|
+
# fp whoami
|
|
6
|
+
#
|
|
7
|
+
# Show the *resolved* configuration the CLI would actually use for this
|
|
8
|
+
# invocation: which profile is active, the API URL and where it came from,
|
|
9
|
+
# the environment that URL targets, and whether an API key is present
|
|
10
|
+
# (masked — never the full token). Purely local; makes no API calls, so it
|
|
11
|
+
# works even without a valid key. Honors --profile / --api-url / --json.
|
|
12
|
+
class Whoami < Base
|
|
13
|
+
def run(_args)
|
|
14
|
+
r = config.resolved
|
|
15
|
+
|
|
16
|
+
output.success(r) do
|
|
17
|
+
puts 'Resolved FeatureParity CLI configuration:'
|
|
18
|
+
puts
|
|
19
|
+
puts " Profile: #{format_value(r[:profile], r[:profile_source])}"
|
|
20
|
+
puts " Environment: #{r[:environment] || '(unknown)'}"
|
|
21
|
+
puts " API URL: #{format_value(r[:api_url], r[:api_url_source])}"
|
|
22
|
+
puts " API key: #{key_line(r)}"
|
|
23
|
+
puts " Config file: #{r[:config_file]}"
|
|
24
|
+
|
|
25
|
+
if r[:profile_environment] && r[:profile_environment] != r[:environment]
|
|
26
|
+
puts
|
|
27
|
+
puts " ⚠ Profile is tagged for environment '#{r[:profile_environment]}' " \
|
|
28
|
+
"but the resolved API URL points at '#{r[:environment]}'."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
unless r[:authenticated]
|
|
32
|
+
puts
|
|
33
|
+
puts 'No API key resolved. Set one with:'
|
|
34
|
+
puts ' export FP_API_KEY=fp_...'
|
|
35
|
+
puts ' fp setup'
|
|
36
|
+
puts ' fp profile add <name> --api-key fp_...'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def format_value(value, source)
|
|
44
|
+
display = value.nil? || value.to_s.empty? ? '(none)' : value
|
|
45
|
+
source && source != 'none' ? "#{display} [from #{source}]" : display.to_s
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def key_line(r)
|
|
49
|
+
return '(not set)' unless r[:api_key_present]
|
|
50
|
+
|
|
51
|
+
"#{r[:api_key_hint]} [from #{r[:api_key_source]}]"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/fp/commands.rb
CHANGED
|
@@ -12,6 +12,7 @@ require_relative 'commands/profile'
|
|
|
12
12
|
require_relative 'commands/config_cmd'
|
|
13
13
|
require_relative 'commands/repos'
|
|
14
14
|
require_relative 'commands/setup'
|
|
15
|
+
require_relative 'commands/whoami'
|
|
15
16
|
require_relative 'commands/version'
|
|
16
17
|
require_relative 'commands/help'
|
|
17
18
|
|
data/lib/fp/config.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'uri'
|
|
5
6
|
|
|
6
7
|
module Fp
|
|
7
8
|
# Handles configuration: profiles, API keys, API URL.
|
|
@@ -29,9 +30,12 @@ module Fp
|
|
|
29
30
|
|
|
30
31
|
attr_reader :api_key, :api_url, :profile_name
|
|
31
32
|
|
|
33
|
+
# Where a resolved value came from, for `fp whoami` transparency.
|
|
34
|
+
attr_reader :api_key_source, :api_url_source, :profile_source
|
|
35
|
+
|
|
32
36
|
def initialize(profile: nil, api_url: nil)
|
|
33
|
-
@profile_name = resolve_profile_name(profile)
|
|
34
37
|
@explicit_api_url = api_url
|
|
38
|
+
@profile_name = resolve_profile_name(profile)
|
|
35
39
|
@api_key = resolve_api_key
|
|
36
40
|
@api_url = resolve_api_url
|
|
37
41
|
end
|
|
@@ -40,6 +44,56 @@ module Fp
|
|
|
40
44
|
!@api_key.nil? && !@api_key.empty?
|
|
41
45
|
end
|
|
42
46
|
|
|
47
|
+
# The API URL passed via the global --api-url flag, if any (nil otherwise).
|
|
48
|
+
# `profile add` / `setup` use this so `fp profile add x --api-url URL` still
|
|
49
|
+
# records URL even though the global parser consumes the flag first.
|
|
50
|
+
def explicit_api_url
|
|
51
|
+
return nil if @explicit_api_url.nil? || @explicit_api_url.empty?
|
|
52
|
+
|
|
53
|
+
@explicit_api_url
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Short, human-readable environment label derived from the API host, e.g.
|
|
57
|
+
# https://api.dev.featureparity.dev -> "dev"
|
|
58
|
+
# https://api.staging.featureparity.dev -> "staging"
|
|
59
|
+
# https://api.featureparity.dev -> "production"
|
|
60
|
+
# http://localhost:9292 -> "local"
|
|
61
|
+
# This is what "tags" a profile/key to an environment: the API URL it targets.
|
|
62
|
+
def environment
|
|
63
|
+
self.class.environment_for(@api_url)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The environment explicitly recorded on the active profile, if any.
|
|
67
|
+
# Falls back to nil when unset (callers derive from api_url instead).
|
|
68
|
+
def profile_environment
|
|
69
|
+
return nil unless @profile_name
|
|
70
|
+
|
|
71
|
+
profile = self.class.load_profiles[@profile_name]
|
|
72
|
+
profile && profile['environment']
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Masked hint for display — never reveal the full key.
|
|
76
|
+
def masked_api_key
|
|
77
|
+
self.class.mask_key(@api_key)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Full resolved snapshot for `fp whoami` (safe to print / JSON-encode).
|
|
81
|
+
def resolved
|
|
82
|
+
{
|
|
83
|
+
profile: @profile_name,
|
|
84
|
+
profile_source: @profile_source,
|
|
85
|
+
api_url: @api_url,
|
|
86
|
+
api_url_source: @api_url_source,
|
|
87
|
+
environment: environment,
|
|
88
|
+
profile_environment: profile_environment,
|
|
89
|
+
authenticated: valid?,
|
|
90
|
+
api_key_present: valid?,
|
|
91
|
+
api_key_hint: masked_api_key,
|
|
92
|
+
api_key_source: @api_key_source,
|
|
93
|
+
config_file: self.class::CONFIG_FILE
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
43
97
|
def validation_error
|
|
44
98
|
return nil if valid?
|
|
45
99
|
|
|
@@ -64,7 +118,7 @@ module Fp
|
|
|
64
118
|
config['profiles'] || {}
|
|
65
119
|
end
|
|
66
120
|
|
|
67
|
-
def save_profile(name, api_key:, api_url: nil)
|
|
121
|
+
def save_profile(name, api_key:, api_url: nil, environment: nil)
|
|
68
122
|
FileUtils.mkdir_p(CONFIG_DIR)
|
|
69
123
|
ensure_config_file!
|
|
70
124
|
|
|
@@ -72,6 +126,10 @@ module Fp
|
|
|
72
126
|
config['profiles'] ||= {}
|
|
73
127
|
config['profiles'][name] = { 'api_key' => api_key }
|
|
74
128
|
config['profiles'][name]['api_url'] = api_url if api_url
|
|
129
|
+
# Tag the key/profile with the environment it targets. If not given
|
|
130
|
+
# explicitly, derive it from the api_url so keys are always tagged.
|
|
131
|
+
env = environment || environment_for(api_url || get_setting('api_url') || DEFAULT_API_URL)
|
|
132
|
+
config['profiles'][name]['environment'] = env if env
|
|
75
133
|
|
|
76
134
|
write_config!(config)
|
|
77
135
|
end
|
|
@@ -82,6 +140,67 @@ module Fp
|
|
|
82
140
|
config[key.to_s]
|
|
83
141
|
end
|
|
84
142
|
|
|
143
|
+
# Derive a short environment label from an API URL's host. This mirrors
|
|
144
|
+
# the Belt DNS convention (see infrastructure/modules/app/dns.tf), which is
|
|
145
|
+
# what actually "tags" a profile/key to an environment: the API host tells
|
|
146
|
+
# you which FeatureParity environment the key talks to.
|
|
147
|
+
#
|
|
148
|
+
# Standalone environments use a dotted "api." prefix:
|
|
149
|
+
# api.featureparity.dev -> "production"
|
|
150
|
+
# api.dev.featureparity.dev -> "dev"
|
|
151
|
+
# api.staging.featureparity.dev -> "staging"
|
|
152
|
+
#
|
|
153
|
+
# Nested / ephemeral (per-branch) environments use a dashed "api-" prefix,
|
|
154
|
+
# because SSL wildcards are single-level (*.dev.featureparity.dev covers
|
|
155
|
+
# api-<env>.dev.featureparity.dev but not api.<env>.dev.featureparity.dev).
|
|
156
|
+
# The env slug is everything after "api-" up to the first dot, and may
|
|
157
|
+
# itself contain dashes:
|
|
158
|
+
# api-abc-1234.dev.featureparity.dev -> "abc-1234"
|
|
159
|
+
# api-fizzy123.dev.featureparity.dev -> "fizzy123"
|
|
160
|
+
#
|
|
161
|
+
# Anything local:
|
|
162
|
+
# localhost / 127.0.0.1 / 0.0.0.0 -> "local"
|
|
163
|
+
def environment_for(url)
|
|
164
|
+
return nil if url.nil? || url.to_s.empty?
|
|
165
|
+
|
|
166
|
+
host = begin
|
|
167
|
+
URI.parse(url.to_s).host
|
|
168
|
+
rescue URI::InvalidURIError
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
return nil if host.nil? || host.empty?
|
|
172
|
+
|
|
173
|
+
return 'local' if host == 'localhost' || host.start_with?('127.') || host == '0.0.0.0'
|
|
174
|
+
|
|
175
|
+
first, *rest = host.split('.')
|
|
176
|
+
|
|
177
|
+
# Nested/ephemeral: "api-<env>" where <env> may contain dashes.
|
|
178
|
+
return first.sub(/\Aapi-/, '') if first.start_with?('api-')
|
|
179
|
+
|
|
180
|
+
# Standalone dotted "api." prefix: the env is the next label.
|
|
181
|
+
if first == 'api'
|
|
182
|
+
# api.featureparity.dev -> ["featureparity", "dev"] -> production
|
|
183
|
+
return 'production' if rest.length <= 2
|
|
184
|
+
|
|
185
|
+
return rest.first
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# No recognizable "api" prefix — best effort: treat a bare apex
|
|
189
|
+
# (two labels, e.g. featureparity.dev) as production, else the left label.
|
|
190
|
+
return 'production' if rest.length <= 1
|
|
191
|
+
|
|
192
|
+
first
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Mask an API key for display: keep a short prefix + last 4 chars.
|
|
196
|
+
# fp_abcdefgh1234 -> fp_…1234
|
|
197
|
+
def mask_key(key)
|
|
198
|
+
return nil if key.nil? || key.empty?
|
|
199
|
+
return '****' if key.length <= 8
|
|
200
|
+
|
|
201
|
+
"#{key[0, 3]}…#{key[-4..]}"
|
|
202
|
+
end
|
|
203
|
+
|
|
85
204
|
# Set a global setting (top-level key in config.yml)
|
|
86
205
|
def set_setting(key, value)
|
|
87
206
|
FileUtils.mkdir_p(CONFIG_DIR)
|
|
@@ -190,50 +309,84 @@ module Fp
|
|
|
190
309
|
|
|
191
310
|
def resolve_profile_name(explicit_profile)
|
|
192
311
|
# 1. Explicit --profile flag 2. FP_PROFILE env var
|
|
193
|
-
|
|
194
|
-
|
|
312
|
+
if explicit_profile && !explicit_profile.empty?
|
|
313
|
+
@profile_source = 'flag'
|
|
314
|
+
return explicit_profile
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
if ENV['FP_PROFILE'] && !ENV['FP_PROFILE'].empty?
|
|
318
|
+
@profile_source = 'env (FP_PROFILE)'
|
|
319
|
+
return ENV['FP_PROFILE']
|
|
320
|
+
end
|
|
195
321
|
|
|
196
322
|
# 3. Fall back to a profile literally named "default", if one exists.
|
|
197
323
|
# This mirrors AWS/git/gcloud, which all consult a default profile when
|
|
198
324
|
# nothing else is specified.
|
|
199
|
-
|
|
325
|
+
if self.class.profile_exists?(DEFAULT_PROFILE_NAME)
|
|
326
|
+
@profile_source = 'default profile'
|
|
327
|
+
return DEFAULT_PROFILE_NAME
|
|
328
|
+
end
|
|
200
329
|
|
|
330
|
+
@profile_source = 'none'
|
|
201
331
|
nil
|
|
202
332
|
end
|
|
203
333
|
|
|
204
334
|
def resolve_api_key
|
|
205
335
|
# FP_API_KEY env var always wins
|
|
206
|
-
|
|
336
|
+
if ENV['FP_API_KEY'] && !ENV['FP_API_KEY'].empty?
|
|
337
|
+
@api_key_source = 'env (FP_API_KEY)'
|
|
338
|
+
return ENV['FP_API_KEY']
|
|
339
|
+
end
|
|
207
340
|
|
|
208
341
|
# Otherwise, use profile if specified
|
|
209
|
-
|
|
342
|
+
unless @profile_name
|
|
343
|
+
@api_key_source = 'none'
|
|
344
|
+
return nil
|
|
345
|
+
end
|
|
210
346
|
|
|
211
347
|
profiles = self.class.load_profiles
|
|
212
348
|
profile = profiles[@profile_name]
|
|
213
|
-
|
|
349
|
+
unless profile
|
|
350
|
+
@api_key_source = 'none'
|
|
351
|
+
return nil
|
|
352
|
+
end
|
|
214
353
|
|
|
354
|
+
@api_key_source = "profile (#{@profile_name})"
|
|
215
355
|
profile['api_key']
|
|
216
356
|
end
|
|
217
357
|
|
|
218
358
|
def resolve_api_url
|
|
219
359
|
# 1. Explicit --api-url flag
|
|
220
|
-
|
|
360
|
+
if @explicit_api_url && !@explicit_api_url.empty?
|
|
361
|
+
@api_url_source = 'flag'
|
|
362
|
+
return @explicit_api_url
|
|
363
|
+
end
|
|
221
364
|
|
|
222
365
|
# 2. FP_API_URL env var
|
|
223
|
-
|
|
366
|
+
if ENV['FP_API_URL'] && !ENV['FP_API_URL'].empty?
|
|
367
|
+
@api_url_source = 'env (FP_API_URL)'
|
|
368
|
+
return ENV['FP_API_URL']
|
|
369
|
+
end
|
|
224
370
|
|
|
225
371
|
# 3. Profile-specific URL
|
|
226
372
|
if @profile_name
|
|
227
373
|
profiles = self.class.load_profiles
|
|
228
374
|
profile = profiles[@profile_name]
|
|
229
|
-
|
|
375
|
+
if profile && profile['api_url']
|
|
376
|
+
@api_url_source = "profile (#{@profile_name})"
|
|
377
|
+
return profile['api_url']
|
|
378
|
+
end
|
|
230
379
|
end
|
|
231
380
|
|
|
232
381
|
# 4. Global api_url setting
|
|
233
382
|
global_url = self.class.get_setting('api_url')
|
|
234
|
-
|
|
383
|
+
if global_url && !global_url.empty?
|
|
384
|
+
@api_url_source = 'global setting'
|
|
385
|
+
return global_url
|
|
386
|
+
end
|
|
235
387
|
|
|
236
388
|
# 5. Default
|
|
389
|
+
@api_url_source = 'default'
|
|
237
390
|
DEFAULT_API_URL
|
|
238
391
|
end
|
|
239
392
|
end
|
data/lib/fp/version.rb
CHANGED
data/skills/fp/references/cli.md
CHANGED
|
@@ -159,15 +159,26 @@ fp matrix --project stowzilla --json # JSON
|
|
|
159
159
|
|
|
160
160
|
### fp profile
|
|
161
161
|
|
|
162
|
-
Manage named profiles.
|
|
162
|
+
Manage named profiles. Each profile stores an API key and (optionally) the
|
|
163
|
+
API URL and environment it targets, so a key is always tagged with the
|
|
164
|
+
environment it talks to.
|
|
163
165
|
|
|
164
166
|
```bash
|
|
165
167
|
fp profile list
|
|
166
|
-
fp profile add staging --api-key fp_...
|
|
167
|
-
fp profile
|
|
168
|
-
fp profile show staging
|
|
168
|
+
fp profile add staging --api-key fp_... --api-url https://api.staging.featureparity.dev
|
|
169
|
+
fp profile add staging --api-key fp_... --environment staging
|
|
169
170
|
```
|
|
170
171
|
|
|
172
|
+
If `--environment` is omitted it is derived from the API URL's host, following
|
|
173
|
+
the Belt DNS convention (`infrastructure/modules/app/dns.tf`):
|
|
174
|
+
|
|
175
|
+
- `api.featureparity.dev` → `production`
|
|
176
|
+
- `api.staging.featureparity.dev` → `staging`
|
|
177
|
+
- `api-abc-1234.dev.featureparity.dev` → `abc-1234` (nested/ephemeral env; `api-` prefix)
|
|
178
|
+
- `localhost` / `127.0.0.1` → `local`
|
|
179
|
+
|
|
180
|
+
`fp profile list` shows the tag.
|
|
181
|
+
|
|
171
182
|
### fp config
|
|
172
183
|
|
|
173
184
|
Manage global settings.
|
|
@@ -177,6 +188,21 @@ fp config list
|
|
|
177
188
|
fp config get api_url
|
|
178
189
|
fp config set api_url https://api.dev.featureparity.dev
|
|
179
190
|
fp config unset api_url
|
|
191
|
+
fp config whoami # alias of `fp whoami`
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### fp whoami
|
|
195
|
+
|
|
196
|
+
Show the *resolved* configuration this invocation would use — the active
|
|
197
|
+
profile, the environment, the API URL (and where each value came from), and
|
|
198
|
+
whether an API key is present (masked, never the full token). Makes no API
|
|
199
|
+
calls, so it works even without a valid key.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
fp whoami
|
|
203
|
+
fp whoami --json
|
|
204
|
+
fp --profile staging whoami # inspect a specific profile
|
|
205
|
+
fp --api-url http://localhost:9292 whoami
|
|
180
206
|
```
|
|
181
207
|
|
|
182
208
|
### fp repos
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: net-http
|
|
@@ -108,6 +108,7 @@ files:
|
|
|
108
108
|
- lib/fp/commands/show.rb
|
|
109
109
|
- lib/fp/commands/surfaces.rb
|
|
110
110
|
- lib/fp/commands/version.rb
|
|
111
|
+
- lib/fp/commands/whoami.rb
|
|
111
112
|
- lib/fp/config.rb
|
|
112
113
|
- lib/fp/junit.rb
|
|
113
114
|
- lib/fp/output.rb
|