featureparity 0.0.1 → 0.0.3

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: 695b5f522e34a1e886e07177cbbe5ca87b515caf6845e15b50ca9e1c4fb3735e
4
- data.tar.gz: 18ff57dfb177341dd4a6f2d8dc05e862a53d8e24eb0c2da4172e022986533cc1
3
+ metadata.gz: 166fe16ad8636be840cc889d1104ad4e6a5f3c84cd046f0a94f291412083f344
4
+ data.tar.gz: e88add572a4cfd638768ae4d65de5a207b6a64c450ca0fdce426f48e8e51427e
5
5
  SHA512:
6
- metadata.gz: 9851ef3ea41669276c356fbe2e5a200ec06d59cea82a9d852eaca71a20297319b4eeb32e5a684921cfba3b298bf1e8931f45bc89ad0c59d802128a5f5940b157
7
- data.tar.gz: 1e075a99c1b02368908639649ff45ba78be33f8e0a03756b0f7782275758f326e01992488508c6930d1f5ac65e47eb6ef7b5e24438482e6eac2b855fe06b2557
6
+ metadata.gz: c3d5bee44367c02c8329c10bdb3c0615978c405c30d46fb002b1ca7fe6676a61075dc83bf4ba02ba01faf828d80fa7f56af2116fd5287f662166bf547d9c5468
7
+ data.tar.gz: 5b627165e1205cca331626fd46b35d7ad71b9ed666fd63ebe96569b2e2989b4b14ff35426d099a1e7dfd8afe7ac16c7984e53ec1052262a374914505c950404c
data/lib/fp/cli.rb CHANGED
@@ -12,6 +12,8 @@ module Fp
12
12
  'report' => Commands::Report,
13
13
  'matrix' => Commands::Matrix,
14
14
  'profile' => Commands::Profile,
15
+ 'config' => Commands::ConfigCmd,
16
+ 'repos' => Commands::Repos,
15
17
  'setup' => Commands::Setup,
16
18
  'version' => Commands::Version,
17
19
  'help' => Commands::Help
@@ -32,17 +34,19 @@ module Fp
32
34
  exit 1
33
35
  end
34
36
 
35
- # Profile commands don't need auth
36
- needs_auth = !%w[profile setup version help].include?(command_name)
37
+ # Profile/config commands don't need auth
38
+ needs_auth = !%w[profile config repos setup version help].include?(command_name)
37
39
 
38
40
  if needs_auth
39
- config = Config.new(profile: global_opts[:profile])
41
+ config = Config.new(profile: global_opts[:profile], api_url: global_opts[:api_url])
40
42
  unless config.valid?
41
43
  output = Output.new(json: global_opts[:json])
42
44
  output.error(config.validation_error.strip)
43
45
  exit 1
44
46
  end
45
47
  client = Client.new(config)
48
+ else
49
+ config = Config.new(profile: global_opts[:profile], api_url: global_opts[:api_url])
46
50
  end
47
51
 
48
52
  output = Output.new(json: global_opts[:json])
@@ -60,7 +64,7 @@ module Fp
60
64
  private
61
65
 
62
66
  def extract_global_options(args)
63
- opts = { json: false, profile: nil }
67
+ opts = { json: false, profile: nil, api_url: nil }
64
68
 
65
69
  # Extract --json flag
66
70
  if args.delete('--json')
@@ -89,6 +93,17 @@ module Fp
89
93
  opts[:profile] = args.delete_at(idx)
90
94
  end
91
95
 
96
+ # Extract --api-url flag
97
+ if (idx = args.index('--api-url'))
98
+ args.delete_at(idx)
99
+ value = args[idx]
100
+ if value.nil? || value.start_with?('-')
101
+ $stderr.puts "Error: --api-url requires a value"
102
+ exit 1
103
+ end
104
+ opts[:api_url] = args.delete_at(idx)
105
+ end
106
+
92
107
  opts
93
108
  end
94
109
  end
data/lib/fp/client.rb CHANGED
@@ -12,21 +12,27 @@ module Fp
12
12
  @base_url = config.api_url.chomp('/')
13
13
  end
14
14
 
15
- # GET /workspaces (called "projects" in the CLI)
15
+ # GET /projects
16
16
  def list_projects
17
- get('/api/workspaces')
17
+ get('/api/projects')
18
18
  end
19
19
 
20
- # GET /workspaces/:id
20
+ # GET /projects/:id
21
21
  def get_project(id)
22
- get("/api/workspaces/#{id}")
22
+ get("/api/projects/#{id}")
23
+ end
24
+
25
+ # GET /projects/:id/surfaces
26
+ def list_surfaces(project_id)
27
+ get("/api/projects/#{project_id}/surfaces")
23
28
  end
24
29
 
25
30
  # GET /requirements
26
- def list_requirements(workspace_id: nil, status: nil)
31
+ def list_requirements(project_id: nil, status: nil, category: nil)
27
32
  params = {}
28
- params[:workspace_id] = workspace_id if workspace_id
33
+ params[:project_id] = project_id if project_id
29
34
  params[:status] = status if status
35
+ params[:category] = category if category
30
36
  get('/api/requirements', params)
31
37
  end
32
38
 
@@ -35,10 +41,10 @@ module Fp
35
41
  get("/api/requirements/#{id}")
36
42
  end
37
43
 
38
- # GET /requirements by slug within a workspace
39
- def find_requirement_by_slug(workspace_id, slug)
40
- # List all requirements for the workspace and find by slug
41
- result = list_requirements(workspace_id: workspace_id)
44
+ # GET /requirements by slug within a project
45
+ def find_requirement_by_slug(project_id, slug)
46
+ # List all requirements for the project and find by slug
47
+ result = list_requirements(project_id: project_id)
42
48
  return result unless result[:ok]
43
49
 
44
50
  requirements = result[:data]['requirements'] || []
@@ -47,7 +53,7 @@ module Fp
47
53
  if requirement
48
54
  { ok: true, data: { 'requirement' => requirement } }
49
55
  else
50
- { ok: false, error: "Requirement '#{slug}' not found in workspace", status: 404 }
56
+ { ok: false, error: "Requirement '#{slug}' not found in project", status: 404 }
51
57
  end
52
58
  end
53
59
 
@@ -61,20 +67,21 @@ module Fp
61
67
  put("/api/requirements/#{id}", params)
62
68
  end
63
69
 
64
- # POST /evidence (when API is ready)
70
+ # POST /projects/:project_id/evidence (direct reporting)
65
71
  def report_evidence(params)
66
- post('/api/evidence', params)
72
+ project_id = params.delete(:project_id)
73
+ post("/api/projects/#{project_id}/evidence", params)
67
74
  end
68
75
 
69
76
  # GET /matrix (when API is ready)
70
- def get_matrix(workspace_id, format: :json)
77
+ def get_matrix(project_id, format: :json)
71
78
  path = format == :csv ? '/api/matrix.csv' : '/api/matrix'
72
- get(path, { workspace_id: workspace_id })
79
+ get(path, { project_id: project_id })
73
80
  end
74
81
 
75
82
  # GET /gaps (when API is ready)
76
- def get_gaps(workspace_id, surface: nil)
77
- params = { workspace_id: workspace_id }
83
+ def get_gaps(project_id, surface: nil)
84
+ params = { project_id: project_id }
78
85
  params[:surface] = surface if surface
79
86
  get('/api/gaps', params)
80
87
  end
@@ -50,26 +50,26 @@ module Fp
50
50
  [opts, positional]
51
51
  end
52
52
 
53
- # Resolve workspace ID from --project flag (can be slug or ID)
54
- def resolve_workspace_id(project_slug_or_id)
53
+ # Resolve project ID from --project flag (can be slug or ID)
54
+ def resolve_project_id(project_slug_or_id)
55
55
  return nil unless project_slug_or_id
56
56
 
57
- # First try to find by listing workspaces
57
+ # First try to find by listing projects
58
58
  result = client.list_projects
59
59
  unless result[:ok]
60
60
  output.error(result[:error], status: result[:status])
61
61
  exit 1
62
62
  end
63
63
 
64
- workspaces = result[:data]['workspaces'] || []
65
- workspace = workspaces.find { |w| w['slug'] == project_slug_or_id || w['id'] == project_slug_or_id }
64
+ projects = result[:data]['projects'] || []
65
+ project = projects.find { |w| w['slug'] == project_slug_or_id || w['id'] == project_slug_or_id }
66
66
 
67
- unless workspace
67
+ unless project
68
68
  output.error("Project '#{project_slug_or_id}' not found")
69
69
  exit 1
70
70
  end
71
71
 
72
- workspace['id']
72
+ project['id']
73
73
  end
74
74
 
75
75
  def require_flag(opts, flag, message = nil)
@@ -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
@@ -18,8 +18,11 @@ module Fp
18
18
  show <slug> --project <slug> Show requirement details
19
19
  propose --project <slug> ... Propose a new requirement (as draft)
20
20
  report <slug> --project <slug>... Report evidence for a requirement
21
+ report --junit <file> ... Upload evidence from a JUnit XML report (no CI)
21
22
  matrix --project <slug> Show the parity matrix
22
23
  profile add|list Manage named profiles
24
+ config set|get|unset|list Manage global settings
25
+ repos set|get|list|unset Map surfaces to local repo paths
23
26
  setup Interactive first-time setup
24
27
  version Show version
25
28
  help Show this help
@@ -28,6 +31,7 @@ module Fp
28
31
  --json Output as JSON envelope
29
32
  --profile <name> Use a named profile
30
33
  -p <name> Alias for --profile
34
+ --api-url <url> Override API URL for this command
31
35
 
32
36
  AUTHENTICATION
33
37
  Set FP_API_KEY environment variable, or use a profile:
@@ -61,6 +65,9 @@ module Fp
61
65
  # List requirements for a project
62
66
  fp list --project stowzilla
63
67
 
68
+ # List requirements filtered by category
69
+ fp list --project stowzilla --category security
70
+
64
71
  # Show gaps (requirements without evidence)
65
72
  fp list --project stowzilla --gaps --surface customer_android
66
73
 
@@ -73,7 +80,8 @@ module Fp
73
80
  --name "Print QR code on container label" \\
74
81
  --why "Customers scan to track containers" \\
75
82
  --required api,customer_android,customer_ios \\
76
- --acceptance "QR code is printed and scannable"
83
+ --acceptance "QR code is printed and scannable" \\
84
+ --category functional
77
85
 
78
86
  # Report evidence for a requirement
79
87
  fp report print_container_qr --project stowzilla \\
@@ -84,12 +92,31 @@ module Fp
84
92
  --work-item https://app.fizzy.do/123/cards/456 \\
85
93
  --title "prints a QR code onto the container label"
86
94
 
95
+ # Upload evidence without CI: run your suite, emit JUnit XML, upload it.
96
+ # fp parses the report, finds the fp:<slug> markers in the test files,
97
+ # and reports evidence for every requirement the suite covered.
98
+ # rspec --format RspecJunitFormatter --out junit.xml # (or your runner's equivalent)
99
+ fp report --junit junit.xml --project stowzilla \\
100
+ --surface api \\
101
+ --repo stowzilla/marketplace \\
102
+ --work-item https://app.fizzy.do/123/cards/456
103
+ # --base-dir sets where relative test file paths in the report resolve from
104
+ # Skipped tests are reported as 'stub'; everything else as 'present'.
105
+
87
106
  # Show the parity matrix
88
107
  fp matrix --project stowzilla
89
108
 
90
109
  # Export matrix as CSV
91
110
  fp matrix --project stowzilla --csv > matrix.csv
92
111
 
112
+ # Map a surface to a local repo on this machine
113
+ fp repos set customer_android ~/code/customer-android \\
114
+ --project stowzilla --repo stowzilla/customer-android
115
+
116
+ # Find where a surface's code lives locally (for agents)
117
+ fp repos get customer_android --project stowzilla
118
+ cd "$(fp repos get customer_android --project stowzilla)"
119
+
93
120
  MARKER CONVENTION
94
121
  Tests are bound to requirements using the fp:<slug> marker in the test file.
95
122
  The CLI does not rewrite test files - you add the marker yourself.
@@ -113,6 +140,18 @@ module Fp
113
140
  ...
114
141
  }
115
142
 
143
+ PINNING SURFACES IN THE MARKER
144
+ A marker may target one or more surfaces with an @suffix. This is
145
+ handy when a single (e.g. backend) test satisfies several surfaces:
146
+
147
+ # fp:print_container_qr -> uses the --surface flag
148
+ # fp:print_container_qr@api -> reports for the api surface
149
+ # fp:print_container_qr@api,web -> reports for api AND web
150
+
151
+ With `fp report --junit`, each pinned surface produces its own
152
+ evidence upload. Markers without an @surface fall back to --surface;
153
+ if a marker pins none and no --surface is given, the run errors.
154
+
116
155
  MORE INFORMATION
117
156
  https://featureparity.dev/docs
118
157
  HELP
@@ -2,12 +2,13 @@
2
2
 
3
3
  module Fp
4
4
  module Commands
5
- # fp list --project <slug> [--surface X] [--gaps] [--status draft]
5
+ # fp list --project <slug> [--surface X] [--category Y] [--gaps] [--status draft]
6
6
  # List requirements, optionally filtered
7
7
  class List < Base
8
8
  KNOWN_FLAGS = {
9
9
  project: :string,
10
10
  surface: :string,
11
+ category: :string,
11
12
  status: :string,
12
13
  gaps: :boolean
13
14
  }.freeze
@@ -16,19 +17,19 @@ module Fp
16
17
  opts, = parse_flags(args, KNOWN_FLAGS)
17
18
  require_flag(opts, :project)
18
19
 
19
- workspace_id = resolve_workspace_id(opts[:project])
20
+ project_id = resolve_project_id(opts[:project])
20
21
 
21
22
  if opts[:gaps]
22
- list_gaps(workspace_id, opts)
23
+ list_gaps(project_id, opts)
23
24
  else
24
- list_requirements(workspace_id, opts)
25
+ list_requirements(project_id, opts)
25
26
  end
26
27
  end
27
28
 
28
29
  private
29
30
 
30
- def list_requirements(workspace_id, opts)
31
- result = client.list_requirements(workspace_id: workspace_id)
31
+ def list_requirements(project_id, opts)
32
+ result = client.list_requirements(project_id: project_id, category: opts[:category])
32
33
 
33
34
  unless result[:ok]
34
35
  output.error(result[:error], status: result[:status])
@@ -50,22 +51,53 @@ module Fp
50
51
 
51
52
  output.success({ requirements: requirements, project: opts[:project] }) do
52
53
  if requirements.empty?
53
- puts "No #{status_filter} requirements found."
54
+ filter_desc = [status_filter]
55
+ filter_desc << "category: #{opts[:category]}" if opts[:category]
56
+ filter_desc << "surface: #{opts[:surface]}" if opts[:surface]
57
+ puts "No requirements found (#{filter_desc.join(', ')})."
54
58
  else
55
- headers = %w[SLUG TITLE SURFACES STATUS]
56
- rows = requirements.map do |r|
59
+ # Group into tree structure: parents first, then children indented
60
+ parents = requirements.select { |r| r['parent_id'].nil? || r['parent_id'] == '' }
61
+ children_by_parent = requirements
62
+ .select { |r| r['parent_id'] && r['parent_id'] != '' }
63
+ .group_by { |r| r['parent_id'] }
64
+
65
+ # Orphaned children (parent not in filtered set) show at top level
66
+ parent_ids = parents.map { |p| p['id'] }.to_set
67
+ orphans = requirements.select { |r| r['parent_id'] && r['parent_id'] != '' && !parent_ids.include?(r['parent_id']) }
68
+
69
+ headers = %w[SLUG TITLE CATEGORY SURFACES STATUS]
70
+ rows = []
71
+
72
+ parents.each do |r|
57
73
  surfaces = (r['required_surfaces'] || []).join(', ')
58
- [r['slug'], truncate(r['title'], 40), surfaces.empty? ? '-' : surfaces, r['status']]
74
+ category = r['category'] || '-'
75
+ rows << [r['slug'], truncate(r['title'], 40), category, surfaces.empty? ? '-' : surfaces, r['status']]
76
+
77
+ # Add children indented
78
+ (children_by_parent[r['id']] || []).each do |child|
79
+ child_surfaces = (child['required_surfaces'] || []).join(', ')
80
+ child_category = child['category'] || '-'
81
+ rows << [" └ #{child['slug']}", truncate(child['title'], 36), child_category, child_surfaces.empty? ? '-' : child_surfaces, child['status']]
82
+ end
59
83
  end
84
+
85
+ # Show orphans at top level
86
+ orphans.each do |r|
87
+ surfaces = (r['required_surfaces'] || []).join(', ')
88
+ category = r['category'] || '-'
89
+ rows << [r['slug'], truncate(r['title'], 40), category, surfaces.empty? ? '-' : surfaces, r['status']]
90
+ end
91
+
60
92
  output.table(headers, rows)
61
93
  end
62
94
  end
63
95
  end
64
96
 
65
- def list_gaps(workspace_id, opts)
97
+ def list_gaps(project_id, opts)
66
98
  # Gaps endpoint may not exist yet - fall back to computing from requirements
67
99
  # For now, list requirements that don't have evidence for a surface
68
- result = client.list_requirements(workspace_id: workspace_id)
100
+ result = client.list_requirements(project_id: project_id, category: opts[:category])
69
101
 
70
102
  unless result[:ok]
71
103
  output.error(result[:error], status: result[:status])
@@ -84,15 +116,24 @@ module Fp
84
116
 
85
117
  # For now, gaps = all requirements (evidence API is #1214)
86
118
  # Once evidence API exists, filter to only those without evidence
87
- output.success({ gaps: requirements, project: opts[:project], surface: opts[:surface] }) do
119
+ output.success({ gaps: requirements, project: opts[:project], surface: opts[:surface], category: opts[:category] }) do
88
120
  if requirements.empty?
89
- puts 'No gaps found.'
121
+ filter_desc = []
122
+ filter_desc << "category: #{opts[:category]}" if opts[:category]
123
+ filter_desc << "surface: #{opts[:surface]}" if opts[:surface]
124
+ msg = 'No gaps found'
125
+ msg += " (#{filter_desc.join(', ')})" unless filter_desc.empty?
126
+ puts "#{msg}."
90
127
  else
91
- puts "Gaps for project '#{opts[:project]}'" + (opts[:surface] ? " (surface: #{opts[:surface]})" : '') + ':'
92
- headers = %w[SLUG TITLE SURFACES]
128
+ desc = "Gaps for project '#{opts[:project]}'"
129
+ desc += " (surface: #{opts[:surface]})" if opts[:surface]
130
+ desc += " (category: #{opts[:category]})" if opts[:category]
131
+ puts "#{desc}:"
132
+ headers = %w[SLUG TITLE CATEGORY SURFACES]
93
133
  rows = requirements.map do |r|
94
134
  surfaces = (r['required_surfaces'] || []).join(', ')
95
- [r['slug'], truncate(r['title'], 40), surfaces]
135
+ category = r['category'] || '-'
136
+ [r['slug'], truncate(r['title'], 40), category, surfaces]
96
137
  end
97
138
  output.table(headers, rows)
98
139
  end
@@ -14,20 +14,19 @@ module Fp
14
14
  opts, = parse_flags(args, KNOWN_FLAGS)
15
15
  require_flag(opts, :project)
16
16
 
17
- workspace_id = resolve_workspace_id(opts[:project])
17
+ project_id = resolve_project_id(opts[:project])
18
18
 
19
- # Get workspace for surfaces
20
- ws_result = client.get_project(workspace_id)
21
- unless ws_result[:ok]
22
- output.error(ws_result[:error], status: ws_result[:status])
19
+ # Get surfaces for the project
20
+ surfaces_result = client.list_surfaces(project_id)
21
+ unless surfaces_result[:ok]
22
+ output.error(surfaces_result[:error], status: surfaces_result[:status])
23
23
  exit 1
24
24
  end
25
25
 
26
- workspace = ws_result[:data]['workspace']
27
- surfaces = workspace['available_surfaces'] || []
26
+ surfaces = (surfaces_result[:data]['surfaces'] || []).map { |s| s['key'] }
28
27
 
29
28
  # Get active requirements
30
- req_result = client.list_requirements(workspace_id: workspace_id)
29
+ req_result = client.list_requirements(project_id: project_id)
31
30
  unless req_result[:ok]
32
31
  output.error(req_result[:error], status: req_result[:status])
33
32
  exit 1
@@ -68,8 +67,6 @@ module Fp
68
67
  output.table(headers, rows)
69
68
  puts
70
69
  puts 'Legend: ✅ = passing, ⬜ = required (no evidence), - = not required'
71
- puts
72
- puts '(Evidence status will show when #1214 is complete)'
73
70
  end
74
71
  end
75
72
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Fp
4
4
  module Commands
5
- # fp projects - list projects/workspaces the API key has access to
5
+ # fp projects - list projects the API key has access to
6
6
  class Projects < Base
7
7
  def run(_args)
8
8
  result = client.list_projects
@@ -12,15 +12,21 @@ module Fp
12
12
  exit 1
13
13
  end
14
14
 
15
- workspaces = result[:data]['workspaces'] || []
15
+ projects = result[:data]['projects'] || []
16
16
 
17
17
  output.success(result[:data]) do
18
- if workspaces.empty?
18
+ if projects.empty?
19
19
  puts 'No projects found.'
20
20
  else
21
21
  headers = %w[SLUG NAME SURFACES]
22
- rows = workspaces.map do |w|
23
- surfaces = (w['available_surfaces'] || []).join(', ')
22
+ rows = projects.map do |w|
23
+ surfaces_result = client.list_surfaces(w['id'])
24
+ surface_keys = if surfaces_result[:ok]
25
+ (surfaces_result[:data]['surfaces'] || []).map { |s| s['key'] }
26
+ else
27
+ []
28
+ end
29
+ surfaces = surface_keys.join(', ')
24
30
  [w['slug'], w['name'], surfaces.empty? ? '-' : surfaces]
25
31
  end
26
32
  output.table(headers, rows)