featureparity 0.0.19 → 0.0.20
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 +15 -0
- data/lib/fp/commands/help.rb +15 -0
- data/lib/fp/commands/projects.rb +275 -9
- data/lib/fp/version.rb +1 -1
- data/skills/fp/SKILL.md +15 -0
- data/skills/fp/references/cli.md +18 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65fc8c40a37a8d05f6fd566fd6362a5b5f489c3ee516ea4643c37afedb0d4534
|
|
4
|
+
data.tar.gz: 2a05968f741537fbc82fc0cc12cb67d7f4bf9fa2c5cebd11bc3514a9637e1745
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9dc847a7281c8b018547b97fb3c06b9bbcf0bcaee596cf1afb098428f025a48af5b01acaad7b76583ed18f0385602dbc9ccae47f5a9605220bacfe31f543723
|
|
7
|
+
data.tar.gz: 95909562928c8f9780eafed047018e8d9e752d6a3e10a5feeb09cdd716ac14945e39948d692aa04102d233f1403570f8e4e926225257093e56a7ed08c1af7b1e
|
data/lib/fp/client.rb
CHANGED
|
@@ -22,6 +22,21 @@ module Fp
|
|
|
22
22
|
get("/api/projects/#{id}")
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# POST /projects
|
|
26
|
+
def create_project(params)
|
|
27
|
+
post('/api/projects', params)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# PUT /projects/:id
|
|
31
|
+
def update_project(id, params)
|
|
32
|
+
put("/api/projects/#{id}", params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# DELETE /projects/:id
|
|
36
|
+
def delete_project(id)
|
|
37
|
+
delete("/api/projects/#{id}")
|
|
38
|
+
end
|
|
39
|
+
|
|
25
40
|
# GET /projects/:id/surfaces
|
|
26
41
|
def list_surfaces(project_id)
|
|
27
42
|
get("/api/projects/#{project_id}/surfaces")
|
data/lib/fp/commands/help.rb
CHANGED
|
@@ -14,6 +14,10 @@ module Fp
|
|
|
14
14
|
COMMANDS
|
|
15
15
|
init --project <slug> Onboard this repo: config, CI gate, agent docs
|
|
16
16
|
projects List projects you have access to
|
|
17
|
+
projects show <slug> View a project and its surfaces
|
|
18
|
+
projects create <slug> --name ... Create a project (owned by your key's owner)
|
|
19
|
+
projects edit <slug> --name ... Update a project's name or description
|
|
20
|
+
projects delete <slug> [--yes] Delete a project (asks you to type its slug)
|
|
17
21
|
surfaces --project <slug> List surfaces for a project
|
|
18
22
|
surfaces show <key> --project ... View a single surface's details
|
|
19
23
|
surfaces add <key> --project ... Create a surface
|
|
@@ -151,6 +155,17 @@ module Fp
|
|
|
151
155
|
# List projects
|
|
152
156
|
fp projects
|
|
153
157
|
|
|
158
|
+
# Create a project (agents can do this — the project lands on the account of
|
|
159
|
+
# whoever minted the key, and the same key reaches it immediately)
|
|
160
|
+
fp projects create calib-rate --name "Calib Rate" --surfaces api,web
|
|
161
|
+
fp projects show calib-rate
|
|
162
|
+
fp projects edit calib-rate --description "Calibration tooling"
|
|
163
|
+
|
|
164
|
+
# Delete a project. Interactive: type the slug to confirm. Non-interactive
|
|
165
|
+
# sessions must pass --yes, or the command refuses. Owners only.
|
|
166
|
+
fp projects delete calib-rate
|
|
167
|
+
fp projects delete calib-rate --yes
|
|
168
|
+
|
|
154
169
|
# List the surfaces a project tracks (requirements may only name these)
|
|
155
170
|
fp surfaces --project stowzilla
|
|
156
171
|
|
data/lib/fp/commands/projects.rb
CHANGED
|
@@ -2,9 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
module Fp
|
|
4
4
|
module Commands
|
|
5
|
-
# fp projects
|
|
5
|
+
# fp projects - list projects the API key can reach
|
|
6
|
+
# fp projects show <slug> - view one project and its surfaces
|
|
7
|
+
# fp projects create <slug> --name "..." - create a project (owned by the key's owner)
|
|
8
|
+
# fp projects edit <slug> --name/--description - update a project's details
|
|
9
|
+
# fp projects delete <slug> [--yes] - delete a project, with confirmation
|
|
10
|
+
#
|
|
11
|
+
# `create` exists because the only way to start a project used to be the web app, so
|
|
12
|
+
# an agent onboarding a repo FeatureParity had never seen was stuck waiting on a human
|
|
13
|
+
# (Fizzy #1473). Keys are account-level (Fizzy #1472): the project lands on the key
|
|
14
|
+
# owner's account, and the same key reaches it immediately.
|
|
15
|
+
#
|
|
16
|
+
# `delete` is modelled on `gh repo delete`: it asks you to type the project's slug,
|
|
17
|
+
# or takes --yes to skip the prompt, and a non-interactive session without --yes is
|
|
18
|
+
# refused rather than guessed at. The server adds its own check — only an owner (or an
|
|
19
|
+
# owner's key) may delete.
|
|
6
20
|
class Projects < Base
|
|
7
|
-
|
|
21
|
+
CREATE_FLAGS = {
|
|
22
|
+
name: :string,
|
|
23
|
+
description: :string,
|
|
24
|
+
surfaces: :string
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
EDIT_FLAGS = {
|
|
28
|
+
name: :string,
|
|
29
|
+
description: :string
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
DELETE_FLAGS = {
|
|
33
|
+
yes: :boolean
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def run(args)
|
|
37
|
+
# Subcommand dispatch. Bare `fp projects` stays the list, so existing callers
|
|
38
|
+
# (the setup wizard's "test access with fp projects") keep working.
|
|
39
|
+
case args.first
|
|
40
|
+
when 'show'
|
|
41
|
+
show(args[1..])
|
|
42
|
+
when 'create'
|
|
43
|
+
create(args[1..])
|
|
44
|
+
when 'edit'
|
|
45
|
+
edit(args[1..])
|
|
46
|
+
when 'delete'
|
|
47
|
+
delete(args[1..])
|
|
48
|
+
else
|
|
49
|
+
list
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def list
|
|
8
56
|
result = client.list_projects
|
|
9
57
|
|
|
10
58
|
unless result[:ok]
|
|
@@ -17,22 +65,240 @@ module Fp
|
|
|
17
65
|
output.success(result[:data]) do
|
|
18
66
|
if projects.empty?
|
|
19
67
|
puts 'No projects found.'
|
|
68
|
+
puts 'Create one with: fp projects create <slug> --name "<Name>"'
|
|
20
69
|
else
|
|
21
70
|
headers = %w[SLUG NAME SURFACES]
|
|
22
71
|
rows = projects.map do |w|
|
|
23
|
-
|
|
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(', ')
|
|
72
|
+
surfaces = surface_keys_for(w['id']).join(', ')
|
|
30
73
|
[w['slug'], w['name'], surfaces.empty? ? '-' : surfaces]
|
|
31
74
|
end
|
|
32
75
|
output.table(headers, rows)
|
|
33
76
|
end
|
|
34
77
|
end
|
|
35
78
|
end
|
|
79
|
+
|
|
80
|
+
def show(args)
|
|
81
|
+
_opts, positional = parse_flags(args)
|
|
82
|
+
slug = require_slug!(positional, 'fp projects show <slug>')
|
|
83
|
+
|
|
84
|
+
project = find_project!(slug)
|
|
85
|
+
result = client.get_project(project['id'])
|
|
86
|
+
|
|
87
|
+
unless result[:ok]
|
|
88
|
+
output.error(result[:error], status: result[:status])
|
|
89
|
+
exit 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
record = result[:data]['project'] || project
|
|
93
|
+
surfaces = surface_keys_for(record['id'])
|
|
94
|
+
|
|
95
|
+
output.success({ project: record, surfaces: surfaces }) do
|
|
96
|
+
print_project(record, surfaces)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def create(args)
|
|
101
|
+
opts, positional = parse_flags(args, CREATE_FLAGS)
|
|
102
|
+
slug = require_slug!(positional, 'fp projects create <slug> --name "<Name>" [--description "..."] [--surfaces api,web]')
|
|
103
|
+
|
|
104
|
+
# The slug is the project's permanent key and the name is what people see. Let
|
|
105
|
+
# `fp projects create calib-rate` work on its own: the name defaults to the slug
|
|
106
|
+
# and can be fixed up later with `fp projects edit`.
|
|
107
|
+
params = { slug: slug, name: opts[:name] || slug }
|
|
108
|
+
params[:description] = opts[:description] if opts[:description]
|
|
109
|
+
|
|
110
|
+
surfaces = split_list(opts[:surfaces])
|
|
111
|
+
params[:surfaces] = surfaces unless surfaces.empty?
|
|
112
|
+
|
|
113
|
+
result = client.create_project(params)
|
|
114
|
+
|
|
115
|
+
unless result[:ok]
|
|
116
|
+
output.error(result[:error], status: result[:status])
|
|
117
|
+
exit 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
project = result[:data]['project'] || {}
|
|
121
|
+
created_surfaces = (result[:data]['surfaces'] || []).map { |s| s['key'] }
|
|
122
|
+
|
|
123
|
+
output.success(result[:data]) do
|
|
124
|
+
puts "Created project '#{project['slug'] || slug}'."
|
|
125
|
+
puts
|
|
126
|
+
puts " Name: #{project['name']}"
|
|
127
|
+
puts " Slug: #{project['slug'] || slug}"
|
|
128
|
+
puts " Surfaces: #{created_surfaces.empty? ? '(none yet)' : created_surfaces.join(', ')}"
|
|
129
|
+
puts
|
|
130
|
+
puts 'Next:'
|
|
131
|
+
puts " fp surfaces add <key> --project #{project['slug'] || slug} --name \"<Name>\"" if created_surfaces.empty?
|
|
132
|
+
puts " fp init --project #{project['slug'] || slug} --surface <key> # onboard a repo"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def edit(args)
|
|
137
|
+
opts, positional = parse_flags(args, EDIT_FLAGS)
|
|
138
|
+
slug = require_slug!(positional, 'fp projects edit <slug> [--name "..."] [--description "..."]')
|
|
139
|
+
|
|
140
|
+
params = {}
|
|
141
|
+
params[:name] = opts[:name] if opts[:name]
|
|
142
|
+
params[:description] = opts[:description] if opts.key?(:description)
|
|
143
|
+
|
|
144
|
+
if params.empty?
|
|
145
|
+
output.error('Nothing to change. Pass --name and/or --description.')
|
|
146
|
+
exit 1
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
project = find_project!(slug)
|
|
150
|
+
result = client.update_project(project['id'], params)
|
|
151
|
+
|
|
152
|
+
unless result[:ok]
|
|
153
|
+
output.error(result[:error], status: result[:status])
|
|
154
|
+
exit 1
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
record = result[:data]['project'] || {}
|
|
158
|
+
|
|
159
|
+
output.success(result[:data]) do
|
|
160
|
+
puts "Updated project '#{record['slug'] || slug}'."
|
|
161
|
+
puts
|
|
162
|
+
puts " Name: #{record['name']}"
|
|
163
|
+
puts " Description: #{blank?(record['description']) ? '(none)' : record['description']}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def delete(args)
|
|
168
|
+
opts, positional = parse_flags(args, DELETE_FLAGS)
|
|
169
|
+
slug = require_slug!(positional, 'fp projects delete <slug> [--yes]')
|
|
170
|
+
|
|
171
|
+
project = find_project!(slug)
|
|
172
|
+
confirm_delete!(project, opts)
|
|
173
|
+
|
|
174
|
+
result = client.delete_project(project['id'])
|
|
175
|
+
|
|
176
|
+
unless result[:ok]
|
|
177
|
+
output.error(result[:error], status: result[:status])
|
|
178
|
+
exit 1
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
output.success({ deleted: true, project: { 'id' => project['id'], 'slug' => project['slug'] } }) do
|
|
182
|
+
puts "Deleted project '#{project['slug']}'."
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# The gh-style guard. Three ways through:
|
|
187
|
+
# --yes → no prompt (scripts, agents, CI)
|
|
188
|
+
# interactive terminal → type the slug back, exactly
|
|
189
|
+
# neither → refuse; a pipeline must say --yes out loud
|
|
190
|
+
#
|
|
191
|
+
# The prompt shows what is about to go with the project (surfaces, requirements)
|
|
192
|
+
# so "the wrong project" is caught by a human reading, not only by typing.
|
|
193
|
+
def confirm_delete!(project, opts)
|
|
194
|
+
return if opts[:yes]
|
|
195
|
+
|
|
196
|
+
unless interactive?
|
|
197
|
+
output.error(
|
|
198
|
+
"Refusing to delete '#{project['slug']}' without confirmation. " \
|
|
199
|
+
'Re-run with --yes to confirm from a non-interactive session.'
|
|
200
|
+
)
|
|
201
|
+
exit 1
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
surfaces = surface_keys_for(project['id'])
|
|
205
|
+
requirements = requirement_count_for(project['id'])
|
|
206
|
+
|
|
207
|
+
warn "This will permanently delete project '#{project['slug']}' (#{project['name']})."
|
|
208
|
+
warn " Surfaces: #{surfaces.empty? ? 'none' : surfaces.join(', ')}"
|
|
209
|
+
warn " Requirements: #{requirements.nil? ? 'unknown' : requirements}"
|
|
210
|
+
warn 'Its requirements, evidence, and memberships will no longer be reachable.'
|
|
211
|
+
$stderr.print "Type '#{project['slug']}' to confirm: "
|
|
212
|
+
$stderr.flush
|
|
213
|
+
|
|
214
|
+
typed = $stdin.gets&.strip
|
|
215
|
+
return if typed == project['slug']
|
|
216
|
+
|
|
217
|
+
output.error('Aborted. The slug did not match; nothing was deleted.')
|
|
218
|
+
exit 1
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def interactive?
|
|
222
|
+
$stdin.respond_to?(:tty?) && $stdin.tty?
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# --- helpers ----------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def require_slug!(positional, usage)
|
|
228
|
+
slug = positional.first
|
|
229
|
+
if slug.nil? || slug.strip.empty?
|
|
230
|
+
output.error("Usage: #{usage}")
|
|
231
|
+
exit 1
|
|
232
|
+
end
|
|
233
|
+
slug.strip
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# The project record (not just its id) by slug or id, or exit 1. Mirrors
|
|
237
|
+
# Base#resolve_project_id, but the commands here print name/slug too.
|
|
238
|
+
def find_project!(slug_or_id)
|
|
239
|
+
result = client.list_projects
|
|
240
|
+
unless result[:ok]
|
|
241
|
+
output.error(result[:error], status: result[:status])
|
|
242
|
+
exit 1
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
projects = result[:data]['projects'] || []
|
|
246
|
+
project = projects.find { |w| w['slug'] == slug_or_id || w['id'] == slug_or_id }
|
|
247
|
+
|
|
248
|
+
unless project
|
|
249
|
+
output.error("Project '#{slug_or_id}' not found")
|
|
250
|
+
exit 1
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
project
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def surface_keys_for(project_id)
|
|
257
|
+
result = client.list_surfaces(project_id)
|
|
258
|
+
return [] unless result[:ok]
|
|
259
|
+
|
|
260
|
+
(result[:data]['surfaces'] || []).map { |s| s['key'] }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Nil when the read fails — the confirmation prompt says "unknown" rather than
|
|
264
|
+
# claiming a project is empty.
|
|
265
|
+
def requirement_count_for(project_id)
|
|
266
|
+
result = client.list_requirements(project_id: project_id)
|
|
267
|
+
return nil unless result[:ok]
|
|
268
|
+
|
|
269
|
+
(result[:data]['requirements'] || []).size
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def print_project(project, surfaces)
|
|
273
|
+
name = project['name'] || project['slug'].to_s
|
|
274
|
+
puts name
|
|
275
|
+
puts '=' * name.length
|
|
276
|
+
puts
|
|
277
|
+
puts "Slug: #{project['slug']}"
|
|
278
|
+
puts "Description: #{blank?(project['description']) ? '(none)' : project['description']}"
|
|
279
|
+
puts "Enforcement: #{project['enforcement_level']}" if project['enforcement_level']
|
|
280
|
+
puts "Surfaces: #{surfaces.empty? ? '(none)' : surfaces.join(', ')}"
|
|
281
|
+
|
|
282
|
+
vocabulary = Array(project['test_type_vocabulary'])
|
|
283
|
+
puts "Test types: #{vocabulary.join(', ')}" unless vocabulary.empty?
|
|
284
|
+
defaults = Array(project['default_test_types'])
|
|
285
|
+
puts "Default types: #{defaults.join(', ')}" unless defaults.empty?
|
|
286
|
+
|
|
287
|
+
puts
|
|
288
|
+
puts "ID: #{project['id']}" if project['id']
|
|
289
|
+
puts "Created: #{project['created_at']}" if project['created_at']
|
|
290
|
+
puts "Updated: #{project['updated_at']}" if project['updated_at']
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def split_list(value)
|
|
294
|
+
return [] if value.nil? || value == true
|
|
295
|
+
|
|
296
|
+
value.to_s.split(',').map(&:strip).reject(&:empty?)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def blank?(value)
|
|
300
|
+
value.nil? || value.to_s.strip.empty?
|
|
301
|
+
end
|
|
36
302
|
end
|
|
37
303
|
end
|
|
38
304
|
end
|
data/lib/fp/version.rb
CHANGED
data/skills/fp/SKILL.md
CHANGED
|
@@ -352,6 +352,19 @@ FeatureParity section in `AGENTS.md`):
|
|
|
352
352
|
fp init --project <slug> --surface <surface>
|
|
353
353
|
```
|
|
354
354
|
|
|
355
|
+
If the project itself doesn't exist yet, create it first — you don't need a human to do
|
|
356
|
+
this in the web app. The project lands on the account of whoever minted your key, and
|
|
357
|
+
your key reaches it immediately:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
fp projects create <slug> --name "<Name>" --surfaces api,web
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Creation is subject to the owner's plan allowance; a free account gets one project, and
|
|
364
|
+
the command relays the upgrade message if that's used up. `fp projects delete` exists but
|
|
365
|
+
asks a human to type the slug back (or `--yes` in scripts) and is owner-only — don't reach
|
|
366
|
+
for it to tidy up; a project's requirements, evidence, and history go with it.
|
|
367
|
+
|
|
355
368
|
### Triage requirement quality (`fp analyze`)
|
|
356
369
|
|
|
357
370
|
`fp analyze` checks how well requirements are written and whether they belong in the
|
|
@@ -404,6 +417,8 @@ not as the fix — you still write the proposal.
|
|
|
404
417
|
| View matrix | `fp matrix --project X` |
|
|
405
418
|
| List surfaces | `fp surfaces --project X` |
|
|
406
419
|
| List projects | `fp projects` |
|
|
420
|
+
| Create a project | `fp projects create <slug> --name "..." --surfaces api,web` |
|
|
421
|
+
| View / edit a project | `fp projects show <slug>`, `fp projects edit <slug> --name "..."` |
|
|
407
422
|
|
|
408
423
|
All commands support `--json` for machine-readable output.
|
|
409
424
|
|
data/skills/fp/references/cli.md
CHANGED
|
@@ -29,13 +29,29 @@ fp setup --api-key fp_... --profile ci # Named profile
|
|
|
29
29
|
|
|
30
30
|
### fp projects
|
|
31
31
|
|
|
32
|
-
List accessible projects.
|
|
32
|
+
List accessible projects, or view, create, edit and delete one.
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
fp projects
|
|
35
|
+
fp projects # list
|
|
36
36
|
fp projects --json
|
|
37
|
+
fp projects show <slug> # details + surfaces
|
|
38
|
+
fp projects create <slug> --name "Name" [--description "..."] [--surfaces api,web]
|
|
39
|
+
fp projects edit <slug> [--name "..."] [--description "..."]
|
|
40
|
+
fp projects delete <slug> [--yes]
|
|
37
41
|
```
|
|
38
42
|
|
|
43
|
+
`create` is open to agents: the project is owned by the human who minted the key
|
|
44
|
+
(it counts against *their* plan allowance — a free account gets one project, and the
|
|
45
|
+
command relays the server's upgrade message if that's used up), and the same key reaches
|
|
46
|
+
the new project on its next call. `--name` defaults to the slug. `--surfaces` takes
|
|
47
|
+
surface names, comma-separated; each becomes a surface with a derived key and default
|
|
48
|
+
kind/audience, the same as `fp surfaces add` without flags.
|
|
49
|
+
|
|
50
|
+
`delete` is guarded the way `gh repo delete` is. In a terminal it shows the project's
|
|
51
|
+
surfaces and requirement count and asks you to type the slug back; a mismatch aborts.
|
|
52
|
+
Outside a terminal it refuses unless `--yes` is passed. Server-side, only a project
|
|
53
|
+
owner — or a key minted by one — may delete; a member's key gets 403.
|
|
54
|
+
|
|
39
55
|
### fp surfaces
|
|
40
56
|
|
|
41
57
|
List surfaces for a project.
|