featureparity 0.0.20 → 0.0.21
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/client.rb +10 -0
- data/lib/fp/commands/help.rb +8 -0
- data/lib/fp/commands/history.rb +12 -1
- data/lib/fp/commands/surfaces.rb +145 -0
- data/lib/fp/version.rb +1 -1
- data/skills/fp/references/cli.md +14 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ae3f4be081207da9feb300fbdfaad6d2afddf2ffb021ffc8136508da964f0c2
|
|
4
|
+
data.tar.gz: 74d2cf3feb18ea77cac72f38be15c5eb6870d237d76f2a21a3b859853649b02d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f14f50d9f5e6d620341081a55ee98734bafd6893ade078cb424246c6ddb16a2f85a8aff6b5d28dc1d969505708b0e59806628699c47146736b024f90c0e0c6e6
|
|
7
|
+
data.tar.gz: 156d5c4e679017b8d5ec7825eb51d7eb64acec51daf02700a263f58e20251c2136efadab399244856f4ce943fe832825e89e45a75f57a814368e8dbc54a5189e
|
data/lib/fp/client.rb
CHANGED
|
@@ -72,6 +72,16 @@ module Fp
|
|
|
72
72
|
post("/api/projects/#{project_id}/surfaces", params)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# PUT /projects/:id/surfaces/:surface_id
|
|
76
|
+
def update_surface(project_id, surface_id, params)
|
|
77
|
+
put("/api/projects/#{project_id}/surfaces/#{surface_id}", params)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# DELETE /projects/:id/surfaces/:surface_id
|
|
81
|
+
def delete_surface(project_id, surface_id)
|
|
82
|
+
delete("/api/projects/#{project_id}/surfaces/#{surface_id}")
|
|
83
|
+
end
|
|
84
|
+
|
|
75
85
|
# GET /requirements
|
|
76
86
|
def list_requirements(project_id: nil, status: nil, category: nil, tag: nil)
|
|
77
87
|
params = {}
|
data/lib/fp/commands/help.rb
CHANGED
|
@@ -21,6 +21,8 @@ module Fp
|
|
|
21
21
|
surfaces --project <slug> List surfaces for a project
|
|
22
22
|
surfaces show <key> --project ... View a single surface's details
|
|
23
23
|
surfaces add <key> --project ... Create a surface
|
|
24
|
+
surfaces edit <key> --project ... Change a surface (name, kind, repo, key...)
|
|
25
|
+
surfaces delete <key> --project . Delete a surface (asks first; --yes to skip)
|
|
24
26
|
list --project <slug> List active requirements
|
|
25
27
|
show <slug> --project <slug> Show requirement details
|
|
26
28
|
propose --project <slug> ... Propose a new requirement (as draft)
|
|
@@ -178,6 +180,12 @@ module Fp
|
|
|
178
180
|
--kind android \\
|
|
179
181
|
--repo stowzilla/customer-android
|
|
180
182
|
|
|
183
|
+
# Fix up an existing surface (add refuses a key that already exists)
|
|
184
|
+
fp surfaces edit web --project stowzilla --kind web --repo stowzilla/web
|
|
185
|
+
|
|
186
|
+
# Delete a surface (type the key back, or --yes non-interactively)
|
|
187
|
+
fp surfaces delete web --project stowzilla --yes
|
|
188
|
+
|
|
181
189
|
# List requirements for a project
|
|
182
190
|
fp list --project stowzilla
|
|
183
191
|
|
data/lib/fp/commands/history.rb
CHANGED
|
@@ -22,7 +22,16 @@ module Fp
|
|
|
22
22
|
'change_proposed' => 'change proposed',
|
|
23
23
|
'removal_proposed' => 'deletion proposed',
|
|
24
24
|
'change_approved' => 'change approved',
|
|
25
|
-
'change_rejected' => 'change rejected'
|
|
25
|
+
'change_rejected' => 'change rejected',
|
|
26
|
+
'decision_recorded' => 'decision recorded'
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# A member's own stance on a requirement that already has a status (Fizzy #1478),
|
|
30
|
+
# including a request for changes (Fizzy #1481) — its note says what to change.
|
|
31
|
+
DECISION_LABELS = {
|
|
32
|
+
'approve' => 'approves',
|
|
33
|
+
'request_changes' => 'requests changes',
|
|
34
|
+
'reject' => 'rejects'
|
|
26
35
|
}.freeze
|
|
27
36
|
|
|
28
37
|
def run(args)
|
|
@@ -86,11 +95,13 @@ module Fp
|
|
|
86
95
|
|
|
87
96
|
def print_entry(change)
|
|
88
97
|
label = TYPE_LABELS[change['type']] || change['type']
|
|
98
|
+
label = DECISION_LABELS.fetch(change['decision'], label) if change['type'] == 'decision_recorded'
|
|
89
99
|
actor = change['actor'] || 'unknown'
|
|
90
100
|
kind = change['actor_kind'] == 'agent' ? 'agent' : 'human'
|
|
91
101
|
|
|
92
102
|
puts "* #{label} — #{actor} (#{kind}) at #{format_time(change['recorded_at'])}"
|
|
93
103
|
puts " #{change['reason']}" if present?(change['reason'])
|
|
104
|
+
puts " note: #{change['note']}" if change['type'] == 'decision_recorded' && present?(change['note'])
|
|
94
105
|
print_source(change['source'])
|
|
95
106
|
print_diff(change['changes'])
|
|
96
107
|
print_evidence_diff(change['evidence_diff'])
|
data/lib/fp/commands/surfaces.rb
CHANGED
|
@@ -7,10 +7,17 @@ module Fp
|
|
|
7
7
|
# fp surfaces --project <slug> - list available surfaces for a project
|
|
8
8
|
# fp surfaces show <key> --project <slug> - view a single surface's details
|
|
9
9
|
# fp surfaces add <key> --project <slug> - create a surface
|
|
10
|
+
# fp surfaces edit <key> --project <slug> - change an existing surface
|
|
11
|
+
# fp surfaces delete <key> --project <slug> - delete a surface, with confirmation
|
|
10
12
|
#
|
|
11
13
|
# `add` exists because a requirement can only name surfaces that already exist, so an
|
|
12
14
|
# agent onboarding a project it hasn't seen before would otherwise be blocked waiting
|
|
13
15
|
# on a human to click through the web app.
|
|
16
|
+
#
|
|
17
|
+
# `edit` and `delete` exist because `add` on an existing key used to make a second
|
|
18
|
+
# surface with the same key, and there was no way to fix that short of a raw API call
|
|
19
|
+
# (Fizzy #1480). The server now refuses a duplicate key with a 409; `add` turns that
|
|
20
|
+
# into a pointer at `edit`. `delete` is guarded like `fp projects delete`.
|
|
14
21
|
class Surfaces < Base
|
|
15
22
|
ADD_FLAGS = {
|
|
16
23
|
project: :string,
|
|
@@ -21,6 +28,13 @@ module Fp
|
|
|
21
28
|
default_branch: :string
|
|
22
29
|
}.freeze
|
|
23
30
|
|
|
31
|
+
EDIT_FLAGS = ADD_FLAGS.merge(key: :string).freeze
|
|
32
|
+
|
|
33
|
+
DELETE_FLAGS = {
|
|
34
|
+
project: :string,
|
|
35
|
+
yes: :boolean
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
24
38
|
VALID_KINDS = %w[api web android ios other].freeze
|
|
25
39
|
VALID_AUDIENCES = %w[customer ops both internal].freeze
|
|
26
40
|
|
|
@@ -32,6 +46,10 @@ module Fp
|
|
|
32
46
|
add(args[1..])
|
|
33
47
|
when 'show'
|
|
34
48
|
show(args[1..])
|
|
49
|
+
when 'edit'
|
|
50
|
+
edit(args[1..])
|
|
51
|
+
when 'delete'
|
|
52
|
+
delete(args[1..])
|
|
35
53
|
else
|
|
36
54
|
list(args)
|
|
37
55
|
end
|
|
@@ -116,6 +134,15 @@ module Fp
|
|
|
116
134
|
|
|
117
135
|
result = client.create_surface(project_id, params)
|
|
118
136
|
|
|
137
|
+
if !result[:ok] && result[:status] == 409
|
|
138
|
+
output.error(
|
|
139
|
+
"Surface '#{key}' already exists in project '#{opts[:project]}'. " \
|
|
140
|
+
"Change it with: fp surfaces edit #{key} --project #{opts[:project]} [--name ...] [--kind ...] [--repo ...]",
|
|
141
|
+
status: 409
|
|
142
|
+
)
|
|
143
|
+
exit 1
|
|
144
|
+
end
|
|
145
|
+
|
|
119
146
|
unless result[:ok]
|
|
120
147
|
output.error(result[:error], status: result[:status])
|
|
121
148
|
exit 1
|
|
@@ -133,6 +160,124 @@ module Fp
|
|
|
133
160
|
end
|
|
134
161
|
end
|
|
135
162
|
|
|
163
|
+
def edit(args)
|
|
164
|
+
opts, positional = parse_flags(args, EDIT_FLAGS)
|
|
165
|
+
require_project!(opts)
|
|
166
|
+
|
|
167
|
+
key = positional.first
|
|
168
|
+
if key.nil? || key.strip.empty?
|
|
169
|
+
output.error('Usage: fp surfaces edit <key> --project <slug> [--name "Name"] [--kind ...] ' \
|
|
170
|
+
'[--audience ...] [--repo org/repo] [--default-branch main] [--key new_key]')
|
|
171
|
+
exit 1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
validate_choice!(opts[:kind], VALID_KINDS, '--kind')
|
|
175
|
+
validate_choice!(opts[:audience], VALID_AUDIENCES, '--audience')
|
|
176
|
+
|
|
177
|
+
# Only what was passed is sent, so an edit never blanks a field it wasn't asked about.
|
|
178
|
+
params = {}
|
|
179
|
+
%i[key name kind audience repo default_branch].each do |field|
|
|
180
|
+
params[field] = opts[field] if opts.key?(field)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if params.empty?
|
|
184
|
+
output.error('Nothing to change. Pass --name, --kind, --audience, --repo, --default-branch, or --key.')
|
|
185
|
+
exit 1
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
project_id = resolve_project_id(opts[:project])
|
|
189
|
+
surface = find_surface!(project_id, key)
|
|
190
|
+
|
|
191
|
+
result = client.update_surface(project_id, surface['id'], params)
|
|
192
|
+
|
|
193
|
+
unless result[:ok]
|
|
194
|
+
output.error(result[:error], status: result[:status])
|
|
195
|
+
exit 1
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The update action answers with update!'s return value, not the record, so read
|
|
199
|
+
# the surface back rather than trusting the response body.
|
|
200
|
+
updated = client.get_surface(project_id, surface['id'])
|
|
201
|
+
record = (updated[:ok] && updated[:data]['surface']) || surface.merge(params.transform_keys(&:to_s))
|
|
202
|
+
|
|
203
|
+
output.success({ surface: record, project: opts[:project] }) do
|
|
204
|
+
puts "Updated surface '#{record['key']}' in project '#{opts[:project]}'."
|
|
205
|
+
puts
|
|
206
|
+
puts " Name: #{record['name']}"
|
|
207
|
+
puts " Kind: #{record['kind']}"
|
|
208
|
+
puts " Audience: #{record['audience']}"
|
|
209
|
+
puts " Repo: #{record['repo'] || '(none)'}"
|
|
210
|
+
puts " Branch: #{record['default_branch'] || '(none)'}"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def delete(args)
|
|
215
|
+
opts, positional = parse_flags(args, DELETE_FLAGS)
|
|
216
|
+
require_project!(opts)
|
|
217
|
+
|
|
218
|
+
key = positional.first
|
|
219
|
+
if key.nil? || key.strip.empty?
|
|
220
|
+
output.error('Usage: fp surfaces delete <key> --project <slug> [--yes]')
|
|
221
|
+
exit 1
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
project_id = resolve_project_id(opts[:project])
|
|
225
|
+
surface = find_surface!(project_id, key)
|
|
226
|
+
confirm_delete!(surface, opts)
|
|
227
|
+
|
|
228
|
+
result = client.delete_surface(project_id, surface['id'])
|
|
229
|
+
|
|
230
|
+
unless result[:ok]
|
|
231
|
+
output.error(result[:error], status: result[:status])
|
|
232
|
+
exit 1
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
output.success({ deleted: true, surface: { 'id' => surface['id'], 'key' => surface['key'] },
|
|
236
|
+
project: opts[:project] }) do
|
|
237
|
+
puts "Deleted surface '#{surface['key']}' from project '#{opts[:project]}'."
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Same guard as `fp projects delete`: --yes skips the prompt, an interactive
|
|
242
|
+
# terminal types the key back, and anything else is refused.
|
|
243
|
+
def confirm_delete!(surface, opts)
|
|
244
|
+
return if opts[:yes]
|
|
245
|
+
|
|
246
|
+
unless interactive?
|
|
247
|
+
output.error(
|
|
248
|
+
"Refusing to delete surface '#{surface['key']}' without confirmation. " \
|
|
249
|
+
'Re-run with --yes to confirm from a non-interactive session.'
|
|
250
|
+
)
|
|
251
|
+
exit 1
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
warn "This will delete surface '#{surface['key']}' (#{surface['name']}) from project '#{opts[:project]}'."
|
|
255
|
+
warn 'Evidence filed against it stays behind with nothing to measure.'
|
|
256
|
+
$stderr.print "Type '#{surface['key']}' to confirm: "
|
|
257
|
+
$stderr.flush
|
|
258
|
+
|
|
259
|
+
typed = $stdin.gets&.strip
|
|
260
|
+
return if typed == surface['key']
|
|
261
|
+
|
|
262
|
+
output.error('Aborted. The key did not match; nothing was deleted.')
|
|
263
|
+
exit 1
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def interactive?
|
|
267
|
+
$stdin.respond_to?(:tty?) && $stdin.tty?
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def find_surface!(project_id, key)
|
|
271
|
+
result = client.find_surface_by_key(project_id, key)
|
|
272
|
+
|
|
273
|
+
unless result[:ok]
|
|
274
|
+
output.error(result[:error], status: result[:status])
|
|
275
|
+
exit 1
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
result[:data]['surface']
|
|
279
|
+
end
|
|
280
|
+
|
|
136
281
|
def validate_choice!(value, allowed, flag)
|
|
137
282
|
return if value.nil? || allowed.include?(value)
|
|
138
283
|
|
data/lib/fp/version.rb
CHANGED
data/skills/fp/references/cli.md
CHANGED
|
@@ -54,13 +54,26 @@ owner — or a key minted by one — may delete; a member's key gets 403.
|
|
|
54
54
|
|
|
55
55
|
### fp surfaces
|
|
56
56
|
|
|
57
|
-
List
|
|
57
|
+
List, view, create, edit and delete a project's surfaces.
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
fp surfaces --project stowzilla
|
|
61
61
|
fp surfaces --project stowzilla --json
|
|
62
|
+
fp surfaces show <key> --project stowzilla
|
|
63
|
+
fp surfaces add <key> --project stowzilla [--name "..."] [--kind api|web|android|ios|other] [--audience ...] [--repo org/repo] [--default-branch main]
|
|
64
|
+
fp surfaces edit <key> --project stowzilla [--name "..."] [--kind ...] [--audience ...] [--repo ...] [--default-branch ...] [--key new_key]
|
|
65
|
+
fp surfaces delete <key> --project stowzilla [--yes]
|
|
62
66
|
```
|
|
63
67
|
|
|
68
|
+
A key is unique within a project. `add` on a key that already exists fails and points
|
|
69
|
+
you at `edit` — use `edit` to fill in the kind or repo of a surface that `fp projects
|
|
70
|
+
create --surfaces` made with defaults. `edit` only sends the flags you pass. `delete`
|
|
71
|
+
asks you to type the key back in a terminal and refuses without `--yes` otherwise.
|
|
72
|
+
|
|
73
|
+
A surface that requirements still require can't be deleted or re-keyed — the server
|
|
74
|
+
refuses and names them. Taking a surface off a requirement is a human's call, so ask
|
|
75
|
+
one rather than working around it.
|
|
76
|
+
|
|
64
77
|
### fp list
|
|
65
78
|
|
|
66
79
|
List requirements.
|
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.21
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: net-http
|