featureparity 0.0.8 → 0.0.9

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: c622199401f105760e517583ace2bb9ec9da7f6f125d960144783ec82e2922b4
4
- data.tar.gz: ab0b372febeff5b863f46d80d093ff8ac789bb4fdd2ae9ceab443f81a8b9a2f5
3
+ metadata.gz: 20c34d118adff460a3de1eaaa15f66718593100ae0261f9dc064f63b65d8b3f2
4
+ data.tar.gz: e14ab1bf7bbf6c646f42e0b9a9291653e072f47055b8dff8ccf2294d10effa7a
5
5
  SHA512:
6
- metadata.gz: ed416b54e77e1aa6e9ce490283811ac5b81443f34aef97cf9b47987719bc5b5497456c4fd3bb74b1f308b735ab07bc5682a06a4d13548cd48272135160692cd2
7
- data.tar.gz: 6316ceda063c279ad46dd763c5ab9b1366a27cd00c1eb8d6f52b7ce21995c203040f86667e76a4a921e77d7a14c380ec85aaedb1ad0c4468e8284b185cf69a5c
6
+ metadata.gz: 248bf8d80901ed3a4075e44e501169bd37803dc4fad78b3b342cfd79307a4649b824664a0b7ce5013f3fd323b2f64ed7deaa2318813389604e21d8c212cd2c36
7
+ data.tar.gz: 1e22a8d5b38f77581693dd4cb0d912aedb0d09afa8cdffaa96332eca3afc1d9c62893ff2c0a09e9ca7f4d44d46fd6126d71b915cb3bfa357099c2a3726ce2006
data/lib/fp/client.rb CHANGED
@@ -27,6 +27,31 @@ module Fp
27
27
  get("/api/projects/#{project_id}/surfaces")
28
28
  end
29
29
 
30
+ # GET /projects/:id/surfaces/:surface_id
31
+ def get_surface(project_id, surface_id)
32
+ get("/api/projects/#{project_id}/surfaces/#{surface_id}")
33
+ end
34
+
35
+ # Find a single surface by its key within a project.
36
+ #
37
+ # Agents speak surface *keys* ("customer_android"), not the KSUID the surface is
38
+ # stored under, so — the same way find_requirement_by_slug resolves a slug — list the
39
+ # project's surfaces and match on key. Returns the same { ok:, data: } envelope the
40
+ # rest of the client speaks, with a 404 when no surface has that key.
41
+ def find_surface_by_key(project_id, key)
42
+ result = list_surfaces(project_id)
43
+ return result unless result[:ok]
44
+
45
+ surfaces = result[:data]['surfaces'] || []
46
+ surface = surfaces.find { |s| s['key'] == key }
47
+
48
+ if surface
49
+ { ok: true, data: { 'surface' => surface }, status: 200 }
50
+ else
51
+ { ok: false, error: "Surface '#{key}' not found in project", status: 404 }
52
+ end
53
+ end
54
+
30
55
  # POST /projects/:id/surfaces
31
56
  def create_surface(project_id, params)
32
57
  post("/api/projects/#{project_id}/surfaces", params)
@@ -15,6 +15,7 @@ module Fp
15
15
  init --project <slug> Onboard this repo: config, CI gate, agent docs
16
16
  projects List projects you have access to
17
17
  surfaces --project <slug> List surfaces for a project
18
+ surfaces show <key> --project ... View a single surface's details
18
19
  surfaces add <key> --project ... Create a surface
19
20
  list --project <slug> List active requirements
20
21
  show <slug> --project <slug> Show requirement details
@@ -121,6 +122,9 @@ module Fp
121
122
  # List the surfaces a project tracks (requirements may only name these)
122
123
  fp surfaces --project stowzilla
123
124
 
125
+ # View a single surface's details (owner, repo, kind, test globs)
126
+ fp surfaces show customer_android --project stowzilla
127
+
124
128
  # Create a surface (agents can do this — no need to wait on the web app)
125
129
  fp surfaces add customer_android --project stowzilla \\
126
130
  --name "Customer Android App" \\
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Fp
4
6
  module Commands
5
7
  # fp surfaces --project <slug> - list available surfaces for a project
8
+ # fp surfaces show <key> --project <slug> - view a single surface's details
6
9
  # fp surfaces add <key> --project <slug> - create a surface
7
10
  #
8
11
  # `add` exists because a requirement can only name surfaces that already exist, so an
@@ -24,8 +27,11 @@ module Fp
24
27
  def run(args)
25
28
  # Subcommand dispatch. Bare `fp surfaces --project X` stays a list, so existing
26
29
  # callers (and the docs) keep working.
27
- if args.first == 'add'
30
+ case args.first
31
+ when 'add'
28
32
  add(args[1..])
33
+ when 'show'
34
+ show(args[1..])
29
35
  else
30
36
  list(args)
31
37
  end
@@ -59,6 +65,31 @@ module Fp
59
65
  end
60
66
  end
61
67
 
68
+ def show(args)
69
+ opts, positional = parse_flags(args)
70
+ require_project!(opts)
71
+
72
+ key = positional.first
73
+ if key.nil? || key.strip.empty?
74
+ output.error('Usage: fp surfaces show <key> --project <slug>')
75
+ exit 1
76
+ end
77
+
78
+ project_id = resolve_project_id(opts[:project])
79
+ result = client.find_surface_by_key(project_id, key)
80
+
81
+ unless result[:ok]
82
+ output.error(result[:error], status: result[:status])
83
+ exit 1
84
+ end
85
+
86
+ surface = result[:data]['surface'] || {}
87
+
88
+ output.success(result[:data]) do
89
+ print_surface(surface, project: opts[:project])
90
+ end
91
+ end
92
+
62
93
  def add(args)
63
94
  opts, positional = parse_flags(args, ADD_FLAGS)
64
95
  require_project!(opts)
@@ -108,6 +139,54 @@ module Fp
108
139
  output.error("Invalid #{flag} '#{value}'. Must be one of: #{allowed.join(', ')}")
109
140
  exit 1
110
141
  end
142
+
143
+ # Human-readable detail view for a single surface. Mirrors the shape of
144
+ # `fp show` for requirements: title underline, then the fields that matter.
145
+ def print_surface(surface, project:)
146
+ name = surface['name'] || surface['key'].to_s
147
+ puts name
148
+ puts '=' * name.length
149
+ puts
150
+ puts "Key: #{surface['key']}"
151
+ puts "Project: #{project}"
152
+ puts "Kind: #{surface['kind'] || '(none)'}"
153
+ puts "Audience: #{surface['audience'] || '(none)'}"
154
+ puts "Repo: #{surface['repo'] || '(none)'}"
155
+ puts "Branch: #{surface['default_branch'] || '(none)'}" if surface.key?('default_branch')
156
+
157
+ owner = surface['owner_id']
158
+ team = surface['team']
159
+ puts "Owner: #{owner && !owner.to_s.empty? ? owner : '(unassigned)'}"
160
+ puts "Team: #{team && !team.to_s.empty? ? team : '(none)'}"
161
+
162
+ globs = normalize_test_globs(surface['test_globs'])
163
+ unless globs.empty?
164
+ puts
165
+ puts 'Test Globs:'
166
+ globs.each { |g| puts " - #{g}" }
167
+ end
168
+
169
+ puts
170
+ puts "ID: #{surface['id']}" if surface['id']
171
+ puts "Created: #{surface['created_at']}" if surface['created_at']
172
+ puts "Updated: #{surface['updated_at']}" if surface['updated_at']
173
+ end
174
+
175
+ # test_globs may arrive as a JSON string or an array; normalize to an array for
176
+ # display. Blank/unparseable → empty (the server falls back to kind defaults).
177
+ def normalize_test_globs(value)
178
+ case value
179
+ when Array then value
180
+ when String
181
+ stripped = value.strip
182
+ return [] if stripped.empty?
183
+
184
+ parsed = JSON.parse(stripped) rescue nil
185
+ parsed.is_a?(Array) ? parsed : []
186
+ else
187
+ []
188
+ end
189
+ end
111
190
  end
112
191
  end
113
192
  end
data/lib/fp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fp
4
- VERSION = '0.0.8'
4
+ VERSION = '0.0.9'
5
5
  end
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.8
4
+ version: 0.0.9
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-17 00:00:00.000000000 Z
11
+ date: 2026-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-http