@credda/cli 0.1.5 → 1.0.0
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.
- package/CHANGELOG.md +71 -0
- package/README.md +223 -274
- package/dist/args.d.ts +99 -0
- package/dist/args.js +222 -0
- package/dist/commands.d.ts +153 -0
- package/dist/commands.js +1192 -0
- package/dist/index.d.ts +41 -4
- package/dist/index.js +41 -55
- package/package.json +25 -12
- package/dist/cli.d.ts +0 -108
- package/dist/cli.js +0 -1303
- package/dist/listener.d.ts +0 -17
- package/dist/listener.js +0 -73
package/dist/commands.js
ADDED
|
@@ -0,0 +1,1192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command table. This is the single source of truth for what the CLI
|
|
3
|
+
* accepts: the parser validates against it and `--help` is rendered from it,
|
|
4
|
+
* so documented flags and accepted flags cannot drift apart.
|
|
5
|
+
*
|
|
6
|
+
* ## What this CLI claims, and what it does not
|
|
7
|
+
*
|
|
8
|
+
* A run prepares an environment, reproduces the reported failure, captures its
|
|
9
|
+
* signature as evidence, diagnoses a cause where the evidence supports one,
|
|
10
|
+
* writes the patch and proves it with a test that fails before and passes
|
|
11
|
+
* after. It ends at a diff for a person to review. **It proposes and never
|
|
12
|
+
* merges**, and nothing here takes write access to a repository to do it.
|
|
13
|
+
*
|
|
14
|
+
* **How far a run goes is decided by the provider, not by a flag.** The fix
|
|
15
|
+
* stage is on the investigation path (ADR 0019) and is entered only when the
|
|
16
|
+
* configured provider can author code. Under `CREDDA_PROVIDER=auto` with no
|
|
17
|
+
* key the engine degrades to rule-based reasoning and stops after the
|
|
18
|
+
* diagnosis, because a rule-based patch is worse than none. `PATCH_PATH_STATES`
|
|
19
|
+
* in `packages/shared/src/states.ts` carries that gate and the evidence behind
|
|
20
|
+
* it. There is deliberately no switch here that overrides it: a flag would put
|
|
21
|
+
* an unevidenced claim one environment variable away from a customer.
|
|
22
|
+
*
|
|
23
|
+
* Two consequences this file carries: the old command names keep working and
|
|
24
|
+
* stop describing an output (see {@link INVESTIGATE}), and exit code 3 is
|
|
25
|
+
* reserved rather than reused (see {@link RESERVED_EXIT_CODES}).
|
|
26
|
+
*/
|
|
27
|
+
import { GLOBAL_FLAGS, own } from './args.js';
|
|
28
|
+
/**
|
|
29
|
+
* Exit codes. Documented here, in `--help`, and in docs/cli.md.
|
|
30
|
+
*
|
|
31
|
+
* An investigation that abstains is a success: NO_CHANGE_REQUIRED and
|
|
32
|
+
* INCONCLUSIVE both exit 0. Every non-zero code an investigation can return is
|
|
33
|
+
* a genuine failure.
|
|
34
|
+
*
|
|
35
|
+
* `credda triage` is the one command with two successful codes, and the second of
|
|
36
|
+
* them is non-zero. It is not an investigation and has no Outcome: it executes
|
|
37
|
+
* nothing, so "did it reach a verdict" is not a question about it. What a caller
|
|
38
|
+
* needs from it is which of two correct answers it gave, and 0 is the silent
|
|
39
|
+
* one. See {@link EXIT.COMMENT_READY} for why that way round.
|
|
40
|
+
*
|
|
41
|
+
* The report record (ADR 0012) adds no code, and the omission is a decision. Its
|
|
42
|
+
* confidence class is the obvious candidate -- something like "8:
|
|
43
|
+
* NOT_ESTABLISHED", the next free number -- and it is the wrong thing to encode.
|
|
44
|
+
* `NOT_ESTABLISHED` is the *correct* class for an abstention, which is the
|
|
45
|
+
* outcome this table already insists is a success: code 0 covers it for an
|
|
46
|
+
* investigation and for triage alike, and it is the only one of these eight
|
|
47
|
+
* codes that does. Giving it a non-zero code
|
|
48
|
+
* would make every CI that treats non-zero as failure fail on exactly the runs
|
|
49
|
+
* Credda gets right, and would create a second, contradictory answer to a
|
|
50
|
+
* question `outcome` already answers. The confidence class is a property of the
|
|
51
|
+
* record, readable with `credda report <id> --json`, and the exit code stays a
|
|
52
|
+
* statement about whether the run reached a verdict.
|
|
53
|
+
*
|
|
54
|
+
* ## Code 3 was held open, and is returned again
|
|
55
|
+
*
|
|
56
|
+
* `PATCH_REJECTED` is the exit code of a run that produced a change and then
|
|
57
|
+
* threw it away. ADR 0015 stopped anything from producing changes and held the
|
|
58
|
+
* code open rather than renumbering the table; ADR 0019 put the fix stage back
|
|
59
|
+
* on the path, so runs return 3 again and it means exactly what the old scripts
|
|
60
|
+
* were written against. {@link RESERVED_EXIT_CODES} is empty as a result, and
|
|
61
|
+
* kept, because a test reads it and a code moving between reserved and returned
|
|
62
|
+
* should move its reason with it.
|
|
63
|
+
*/
|
|
64
|
+
export const EXIT = {
|
|
65
|
+
/** Success, including NO_CHANGE_REQUIRED and INCONCLUSIVE. */
|
|
66
|
+
SUCCESS: 0,
|
|
67
|
+
/** Credda itself failed: internal error, unreadable database, crash. */
|
|
68
|
+
INTERNAL_ERROR: 1,
|
|
69
|
+
/** The command line or its inputs were wrong. Nothing was run. */
|
|
70
|
+
USAGE_ERROR: 2,
|
|
71
|
+
/** A change was produced and independent verification rejected it. */
|
|
72
|
+
PATCH_REJECTED: 3,
|
|
73
|
+
/** The run was cancelled (Ctrl-C). */
|
|
74
|
+
CANCELLED: 4,
|
|
75
|
+
/**
|
|
76
|
+
* NO_RUNNABLE_CHECK: nothing runnable could be derived from the report, so
|
|
77
|
+
* nothing was executed against the repository. Not a success and not a crash.
|
|
78
|
+
* See `exitCodeFor` for why it is neither 0 nor 1.
|
|
79
|
+
*/
|
|
80
|
+
NO_RUNNABLE_CHECK: 5,
|
|
81
|
+
/**
|
|
82
|
+
* `credda triage` produced a comment, and it is on stdout. Nothing failed.
|
|
83
|
+
*
|
|
84
|
+
* ## Why the comment is the non-zero side and silence is 0
|
|
85
|
+
*
|
|
86
|
+
* Silence is the common case, not the exceptional one: half of real inbound
|
|
87
|
+
* produces nothing worth saying (`bench/harvest`, 50.6% of 729 issues). A
|
|
88
|
+
* code that turned every second opened issue into a red job would be switched
|
|
89
|
+
* off inside a week, and this repository's standing rule is already that
|
|
90
|
+
* abstention is a success. So silence exits 0, and it is 0 for the same
|
|
91
|
+
* reason NO_CHANGE_REQUIRED is.
|
|
92
|
+
*
|
|
93
|
+
* That leaves the comment needing a code of its own, because "post this" and
|
|
94
|
+
* "post nothing" are the two answers a caller has to tell apart and stdout
|
|
95
|
+
* being empty is a weaker signal than a number. Giving it a non-zero one is
|
|
96
|
+
* deliberate rather than reluctant: **every way of misreading this code then
|
|
97
|
+
* fails towards not posting.** A shell under `set -e` stops before the
|
|
98
|
+
* posting step; a caller that ignores the code and pipes stdout gets an empty
|
|
99
|
+
* document on the silent path; a caller that tests for 0 posts only silence,
|
|
100
|
+
* which posts nothing. The failure this product cannot afford is a
|
|
101
|
+
* confidently wrong refusal on a stranger's issue -- the dominant rule is
|
|
102
|
+
* still wrong 8.7% of the times it fires (Credda-io/core#7) -- so the
|
|
103
|
+
* direction of every mistake here has to be silence.
|
|
104
|
+
*
|
|
105
|
+
* 6 rather than reusing 5: NO_RUNNABLE_CHECK is a statement that nothing was
|
|
106
|
+
* executed against the repository, which is true of *every* triage run by
|
|
107
|
+
* design, so the two would stop meaning different things.
|
|
108
|
+
*/
|
|
109
|
+
COMMENT_READY: 6,
|
|
110
|
+
/**
|
|
111
|
+
* `credda cancel` reached a run that is still executing and asked it to stop.
|
|
112
|
+
* The request is delivered; the run has not stopped yet.
|
|
113
|
+
*
|
|
114
|
+
* ## Why this is not 0, and not 4
|
|
115
|
+
*
|
|
116
|
+
* `apps/api/src/routes/investigations.ts` answers the same question with two
|
|
117
|
+
* different HTTP statuses -- 200 CANCELLED when the run is genuinely over, 202
|
|
118
|
+
* CANCELLATION_REQUESTED when a process is still inside it holding a sandbox
|
|
119
|
+
* and a model budget. A shell has no status line to read. It has this number,
|
|
120
|
+
* and if both answers were 0 then `credda cancel $id && echo stopped` would
|
|
121
|
+
* print "stopped" over a container that is still running and still spending.
|
|
122
|
+
* That is the one false claim this whole route was written to avoid, so the
|
|
123
|
+
* two answers get two codes.
|
|
124
|
+
*
|
|
125
|
+
* 4 is the run's own code, returned by `credda investigate` when the run it
|
|
126
|
+
* was executing was cancelled. It is a statement that a run ended. This is a
|
|
127
|
+
* statement that one was asked to, made by a different process that cannot
|
|
128
|
+
* see whether it did. Reusing 4 would collapse exactly the distinction.
|
|
129
|
+
*
|
|
130
|
+
* Every way of misreading 7 fails towards waiting rather than towards
|
|
131
|
+
* assuming: `set -e` stops, a test for 0 does not proceed. `credda events
|
|
132
|
+
* <id> --follow` is how a caller learns the run actually ended.
|
|
133
|
+
*/
|
|
134
|
+
CANCELLATION_REQUESTED: 7,
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Codes no run of this version can return, and the reason each is held open.
|
|
138
|
+
*
|
|
139
|
+
* A test reads this, so a code cannot quietly move between "reserved" and
|
|
140
|
+
* "returned" without the reason moving with it.
|
|
141
|
+
*/
|
|
142
|
+
export const RESERVED_EXIT_CODES = {};
|
|
143
|
+
export const EXIT_CODE_HELP = [
|
|
144
|
+
' 0 Credda reached the answer it was asked for and nothing failed. For a run',
|
|
145
|
+
' that executed something against this repository, that means its finding is',
|
|
146
|
+
' on record: REPRODUCED_AND_DIAGNOSED, REPRODUCED_NOT_DIAGNOSED,',
|
|
147
|
+
' NO_CHANGE_REQUIRED or INCONCLUSIVE. Abstention is a success here, and',
|
|
148
|
+
' Credda declining to allege a defect it did not demonstrate is a feature.',
|
|
149
|
+
' For `credda triage`, which executes nothing at all, 0 means it correctly had',
|
|
150
|
+
' nothing to say -- see 6.',
|
|
151
|
+
' 1 Internal error. Credda failed; the investigation did not reach a verdict.',
|
|
152
|
+
' 2 Usage error. A bad flag, a missing value, or an input that could not be read.',
|
|
153
|
+
' 3 PATCH_REJECTED. Credda wrote a change and independent verification rejected it,',
|
|
154
|
+
' so it was discarded and the workspace restored. No change is on offer. The',
|
|
155
|
+
' diagnosis still stands and is worth reading.',
|
|
156
|
+
' 4 Cancelled by the operator (Ctrl-C).',
|
|
157
|
+
' 5 NO_RUNNABLE_CHECK. Nothing runnable could be derived from the report, so',
|
|
158
|
+
' nothing was executed. This is a fact about the report, not about your code,',
|
|
159
|
+
' and it is separated from 0 so `credda ... && deploy` cannot read it as a pass.',
|
|
160
|
+
' 6 COMMENT_READY, from `credda triage` only: there is a comment to post and it is',
|
|
161
|
+
' on stdout. Nothing failed. Triage exits 0 when it correctly has nothing to',
|
|
162
|
+
' say, which is about half of real issues, so 0 there is silence and 6 is the',
|
|
163
|
+
' one that means speak.',
|
|
164
|
+
' 7 CANCELLATION_REQUESTED, from `credda cancel` only: a run is still executing',
|
|
165
|
+
' and has been asked to stop. It has NOT stopped. The process tears its sandbox',
|
|
166
|
+
' down and writes its own terminal state when it reaches its next checkpoint;',
|
|
167
|
+
' follow it with `credda events <id> --follow`. 0 from `credda cancel` means',
|
|
168
|
+
' nothing is running, which is a different and stronger claim.',
|
|
169
|
+
];
|
|
170
|
+
/**
|
|
171
|
+
* `investigate`, with `resolve` and `fix` as permanent aliases for it.
|
|
172
|
+
*
|
|
173
|
+
* ## Why the name moved twice
|
|
174
|
+
*
|
|
175
|
+
* `fix` named a stage. `resolve` named the whole workflow. Both were accurate
|
|
176
|
+
* about the destination and wrong about the guarantee: a run reproduces,
|
|
177
|
+
* diagnoses, and then patches and verifies only where the evidence and the
|
|
178
|
+
* provider let it, and a name that promises a fix promises an outcome no run
|
|
179
|
+
* can commit to in advance. `investigate` names what every run does; how far
|
|
180
|
+
* it gets is reported rather than asserted by the verb.
|
|
181
|
+
*
|
|
182
|
+
* ## Why both old names still work
|
|
183
|
+
*
|
|
184
|
+
* Neither is deprecated and neither will be removed. `fix` and `resolve` appear
|
|
185
|
+
* in docs/cli.md, README.md, docs/setup.md and in bench/external's harness
|
|
186
|
+
* invocation, and they are in people's fingers. A command name that silently
|
|
187
|
+
* stops working is a worse failure than an inconsistent one. All three
|
|
188
|
+
* spellings parse identically, print their own name in usage and errors, and
|
|
189
|
+
* dispatch through {@link canonicalCommand}.
|
|
190
|
+
*
|
|
191
|
+
* What the old names must NOT do is carry their old promise. `credda fix --help`
|
|
192
|
+
* prints this command's summary, which says what the run produces, so nobody
|
|
193
|
+
* reads the name as a description of the output.
|
|
194
|
+
*/
|
|
195
|
+
const INVESTIGATE = {
|
|
196
|
+
name: 'investigate',
|
|
197
|
+
summary: 'Reproduce a reported failure, diagnose it, and fix it where the provider allows',
|
|
198
|
+
args: '<repo-path> <description | @file | -> [options]',
|
|
199
|
+
flags: {
|
|
200
|
+
sandbox: {
|
|
201
|
+
kind: 'string',
|
|
202
|
+
choices: ['local', 'native', 'docker'],
|
|
203
|
+
valueName: '<local|native|docker>',
|
|
204
|
+
description: 'Execution plane. local and native both run repository code directly on\n' +
|
|
205
|
+
' this host and only a local credda invocation may select them;\n' +
|
|
206
|
+
' docker isolates it. Credda never falls back silently',
|
|
207
|
+
defaultNote: 'local',
|
|
208
|
+
},
|
|
209
|
+
provider: {
|
|
210
|
+
kind: 'string',
|
|
211
|
+
choices: ['auto', 'heuristic', 'openai-compatible'],
|
|
212
|
+
valueName: '<auto|heuristic|openai-compatible>',
|
|
213
|
+
description: 'auto uses ANTHROPIC_API_KEY then CREDDA_OPENAI_API_KEY when present;\n' +
|
|
214
|
+
' heuristic forces rule-based reasoning; openai-compatible\n' +
|
|
215
|
+
' targets an OpenAI-compatible endpoint (NVIDIA NIM by default)',
|
|
216
|
+
defaultNote: 'auto',
|
|
217
|
+
},
|
|
218
|
+
'budget-minutes': {
|
|
219
|
+
kind: 'number',
|
|
220
|
+
valueName: '<n>',
|
|
221
|
+
description: 'Wall-clock budget for the investigation',
|
|
222
|
+
defaultNote: '20',
|
|
223
|
+
},
|
|
224
|
+
'max-turns': {
|
|
225
|
+
kind: 'number',
|
|
226
|
+
valueName: '<n>',
|
|
227
|
+
description: 'Maximum model calls across all agent roles',
|
|
228
|
+
defaultNote: '120',
|
|
229
|
+
},
|
|
230
|
+
out: {
|
|
231
|
+
kind: 'string',
|
|
232
|
+
valueName: '<file>',
|
|
233
|
+
description: 'Also write the machine-readable result of this run to <file> as JSON',
|
|
234
|
+
},
|
|
235
|
+
/*
|
|
236
|
+
* Where the report came from, recorded on the run.
|
|
237
|
+
*
|
|
238
|
+
* The engine API's create route has accepted `issueRef` since it existed;
|
|
239
|
+
* a terminal had no way to set it, so every locally started run recorded
|
|
240
|
+
* nothing about its own origin. That was tolerable while every local run
|
|
241
|
+
* was a person pasting a sentence they had written. It stopped being
|
|
242
|
+
* tolerable when `credda discover` began writing reports, because a run
|
|
243
|
+
* started from a report Credda wrote itself is a different claim from one
|
|
244
|
+
* a person filed and a reader has to be able to tell which.
|
|
245
|
+
*
|
|
246
|
+
* It is a general flag and not a discovery flag on purpose: nothing
|
|
247
|
+
* downstream branches on the value, and a person pasting a tracker URL
|
|
248
|
+
* here is using it exactly as intended. ADR 0024 is explicit that
|
|
249
|
+
* discovery adds no stage, no terminal and no refusal, so its only trace
|
|
250
|
+
* in the pipeline is this string on the record.
|
|
251
|
+
*/
|
|
252
|
+
ref: {
|
|
253
|
+
kind: 'string',
|
|
254
|
+
valueName: '<ref>',
|
|
255
|
+
description: 'Record where this report came from -- an issue reference, a URL, or the\n' +
|
|
256
|
+
' ref `credda discover` prints. Stored on the run and shown by\n' +
|
|
257
|
+
' `credda report`. Nothing branches on it',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
details: [
|
|
261
|
+
'What a run does, and where it stops:',
|
|
262
|
+
' prepare an environment, reproduce the reported failure, capture its',
|
|
263
|
+
' failure signature, and diagnose a cause where the evidence supports one.',
|
|
264
|
+
' With a model-backed provider it then attempts a fix and verifies it, and',
|
|
265
|
+
' it reports all of it. Every stage runs in a disposable copy, so this',
|
|
266
|
+
' command changes nothing in your working tree.',
|
|
267
|
+
'',
|
|
268
|
+
'Description sources:',
|
|
269
|
+
' "text" a short description given inline',
|
|
270
|
+
' @file read the report from a file (shells truncate multi-line arguments)',
|
|
271
|
+
' - read the report from stdin',
|
|
272
|
+
'',
|
|
273
|
+
'Recording where the report came from:',
|
|
274
|
+
' --ref <ref> is written to the run and printed by `credda report`. Use it',
|
|
275
|
+
' for the issue this came from, or paste the ref that',
|
|
276
|
+
' `credda discover` prints beside a candidate it wrote.',
|
|
277
|
+
'',
|
|
278
|
+
'Configuration precedence, highest first:',
|
|
279
|
+
' 1. the CLI flag on this command line',
|
|
280
|
+
' 2. the environment variable (CREDDA_SANDBOX, CREDDA_PROVIDER)',
|
|
281
|
+
' 3. credda.config.json, searched upward from the working directory,',
|
|
282
|
+
' then $CREDDA_HOME/credda.config.json',
|
|
283
|
+
' 4. the built-in default',
|
|
284
|
+
'',
|
|
285
|
+
'Create a config file with: credda init',
|
|
286
|
+
'',
|
|
287
|
+
'The report this produces: credda report <id>',
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* `report`, with `resolution` as a permanent alias for it.
|
|
292
|
+
*
|
|
293
|
+
* ADR 0012 named this record a *resolution* when the pipeline ended in a patch
|
|
294
|
+
* and a pull request. ADR 0015 then took the Fix and Verify stages off the V1
|
|
295
|
+
* path, and for that stretch a record produced today could carry neither.
|
|
296
|
+
* ADR 0019 (2026-08-27) put both stages back: a run with a model-backed
|
|
297
|
+
* provider writes a patch and verifies it, so Change and Verification are
|
|
298
|
+
* filled in again from that run's own records.
|
|
299
|
+
*
|
|
300
|
+
* `report` is still the better name. It is what the command does -- show what
|
|
301
|
+
* the run established -- whether or not the run reached the fix stage, and it
|
|
302
|
+
* does not promise a resolution to a run that stopped at the diagnosis.
|
|
303
|
+
*
|
|
304
|
+
* `resolution` keeps working, for the same reason `fix` does.
|
|
305
|
+
*/
|
|
306
|
+
const REPORT = {
|
|
307
|
+
name: 'report',
|
|
308
|
+
summary: 'Show what an investigation established, and what it did not',
|
|
309
|
+
args: '<investigation-id-or-prefix> [--json] [--markdown] [--patch]',
|
|
310
|
+
flags: {
|
|
311
|
+
markdown: {
|
|
312
|
+
kind: 'boolean',
|
|
313
|
+
description: 'Emit the report as Markdown: the same document Credda posts to a\n' +
|
|
314
|
+
' pull request. Pipe it, paste it, or commit it',
|
|
315
|
+
},
|
|
316
|
+
patch: {
|
|
317
|
+
kind: 'boolean',
|
|
318
|
+
description: 'Emit the recorded unified diff and nothing else. Exits non-zero\n' +
|
|
319
|
+
' when the run recorded no patch, so a script cannot\n' +
|
|
320
|
+
' mistake an empty document for an empty change',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
details: [
|
|
324
|
+
'The record (ADR 0012): Bug, Evidence, Reproduction, Root Cause and',
|
|
325
|
+
'Confidence, and, when the run reached the fix stage, Change and',
|
|
326
|
+
'Verification.',
|
|
327
|
+
'',
|
|
328
|
+
'What the run behind this record does: Credda is handed a LABELLED bug',
|
|
329
|
+
'report. It reproduces the failure, diagnoses the cause, writes a patch, and',
|
|
330
|
+
'proves the patch with a test that fails before it and passes after. It',
|
|
331
|
+
'NEVER merges, and it does not scan a codebase looking for unknown bugs.',
|
|
332
|
+
'',
|
|
333
|
+
'Change and Verification are printed from the run\'s own records. Reaching',
|
|
334
|
+
'them depends on the provider (ADR 0019): a run with no model-backed',
|
|
335
|
+
'provider stops at the diagnosis and records neither, and both sections say',
|
|
336
|
+
'that rather than going quiet.',
|
|
337
|
+
'',
|
|
338
|
+
'Every section is derived from something that was executed and recorded, or',
|
|
339
|
+
'it is absent. A section with nothing behind it is not filled in -- the hole',
|
|
340
|
+
'is named under Confidence instead.',
|
|
341
|
+
'',
|
|
342
|
+
'Confidence is an ordinal class -- ESTABLISHED, PARTIALLY_ESTABLISHED or',
|
|
343
|
+
'NOT_ESTABLISHED -- and the list of what this record does not establish. It',
|
|
344
|
+
'is never a percentage: Credda has no calibrated probability model, and the',
|
|
345
|
+
'field a reviewer reads to decide how much to trust a finding is the worst',
|
|
346
|
+
'place to invent a number.',
|
|
347
|
+
'',
|
|
348
|
+
'Any unambiguous prefix of an investigation id is accepted.',
|
|
349
|
+
'',
|
|
350
|
+
'--markdown emits the same document the forge delivery posts, which until now',
|
|
351
|
+
'was reachable only from a webhook. It leads with what the investigation did',
|
|
352
|
+
'NOT establish, and its "What was not done" section states, from the record,',
|
|
353
|
+
'whether code was written and what has not been shown. That section is the',
|
|
354
|
+
'point; do not strip it before sharing the rest.',
|
|
355
|
+
'',
|
|
356
|
+
'Under --json neither document is emitted: a JSONL stream may carry nothing',
|
|
357
|
+
'but objects. The suppressed flag is named on stderr rather than dropped in',
|
|
358
|
+
'silence, and the document is one command away without --json.',
|
|
359
|
+
'',
|
|
360
|
+
'--patch writes the unified diff this run recorded, on stdout, with nothing',
|
|
361
|
+
'around it. It exists so a delivery surface can commit what the run actually',
|
|
362
|
+
'produced instead of re-deriving a change from prose; whether that diff may',
|
|
363
|
+
'be PROPOSED to anyone is a separate question, answered by the delivery',
|
|
364
|
+
'block in the result file that `credda investigate --out` writes.',
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
/**
|
|
368
|
+
* `credda triage`: read one report, say what Credda could not use in it, or say
|
|
369
|
+
* nothing at all.
|
|
370
|
+
*
|
|
371
|
+
* ## Why it is called triage, and what the name must never come to mean
|
|
372
|
+
*
|
|
373
|
+
* Every other name considered here promised something the command does not do,
|
|
374
|
+
* which is the mistake {@link INVESTIGATE} spent two renames undoing. `decline`
|
|
375
|
+
* and `reply` both take the issue as their object -- "decline this issue" reads
|
|
376
|
+
* as a verdict on the reporter, and the copy this command prints is built
|
|
377
|
+
* around never being one. `decline-reply`, after the package it renders
|
|
378
|
+
* through, names the artefact exactly but is the only hyphenated command in a
|
|
379
|
+
* table of single words.
|
|
380
|
+
*
|
|
381
|
+
* `triage` is the maintainer's own word for the thing this does: look at an
|
|
382
|
+
* inbound report without doing the work, and say what would be needed. It is
|
|
383
|
+
* accurate today and it has one way to go wrong, so it is written down -- **this
|
|
384
|
+
* command must never label, close, assign, prioritise or otherwise decide
|
|
385
|
+
* anything about an issue.** It reads a file and prints a comment or nothing.
|
|
386
|
+
* The day it does more than that, the name is a promise again and has to move.
|
|
387
|
+
*
|
|
388
|
+
* ## Why it takes a file and never a string
|
|
389
|
+
*
|
|
390
|
+
* The body is text a stranger typed. The launcher's `run.mjs`
|
|
391
|
+
* (Credda-io/action) documents at length why
|
|
392
|
+
* it may never reach a shell, and the same reasoning applies one layer up: an
|
|
393
|
+
* argument goes through a shell, a file name does not. {@link INVESTIGATE}
|
|
394
|
+
* accepts `@file` alongside inline text because a person at a terminal has a
|
|
395
|
+
* sentence in their head; this command exists to be invoked by a workflow on
|
|
396
|
+
* text nobody has read, so the inline form is not offered at all.
|
|
397
|
+
*/
|
|
398
|
+
const TRIAGE = {
|
|
399
|
+
name: 'triage',
|
|
400
|
+
summary: 'Say what Credda could not use in a report, or say nothing',
|
|
401
|
+
args: '<issue-file> [--repo <path>]',
|
|
402
|
+
flags: {
|
|
403
|
+
repo: {
|
|
404
|
+
kind: 'string',
|
|
405
|
+
valueName: '<path>',
|
|
406
|
+
description: 'A checkout of the repository the report was filed against. Without it\n' +
|
|
407
|
+
' Credda assumes it knows nothing about the repository, which is\n' +
|
|
408
|
+
' the reading that invents the least',
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
details: [
|
|
412
|
+
'What a run does, and what it costs:',
|
|
413
|
+
' it reads the report, mines it for a runnable reproduction exactly as an',
|
|
414
|
+
' investigation would, and renders the refusals into one short comment. No',
|
|
415
|
+
' sandbox, no container, no install, no network, no model call and no API',
|
|
416
|
+
' key -- nothing is executed and nothing is written. It is cheap enough to',
|
|
417
|
+
' run on every issue the moment it is opened.',
|
|
418
|
+
'',
|
|
419
|
+
'What it prints:',
|
|
420
|
+
' the comment on stdout, or nothing on stdout. There is no third form.',
|
|
421
|
+
' Diagnostics go to stderr, so `credda triage issue.md > comment.md` yields',
|
|
422
|
+
' either the comment or an empty file.',
|
|
423
|
+
'',
|
|
424
|
+
'Silence is the common outcome and it is a correct one. Measured over 729',
|
|
425
|
+
'real inbound issues, about half contain nothing Credda could ask for and a',
|
|
426
|
+
'quarter produce a specific request (bench/harvest). A comment that names',
|
|
427
|
+
'nothing the reporter could act on is not written, because a bot that posts',
|
|
428
|
+
'generic advice on every issue is a bot that gets muted.',
|
|
429
|
+
'',
|
|
430
|
+
'Exit code, not stdout, is what says which happened:',
|
|
431
|
+
' 6 there is a comment, and it is on stdout',
|
|
432
|
+
' 0 there is correctly nothing to say',
|
|
433
|
+
'Do NOT write `credda triage issue.md > c.md && post c.md`: that posts on the',
|
|
434
|
+
'silent path and stays quiet on the speaking one, which is the wrong way',
|
|
435
|
+
'round twice.',
|
|
436
|
+
'',
|
|
437
|
+
'This is not an investigation and makes no claim about the repository. It',
|
|
438
|
+
'never says a bug is absent, because it never ran anything. The full',
|
|
439
|
+
'reproduce-and-report run is: credda investigate <repo-path> @<issue-file>',
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* The vocabularies the validation flags accept, written out here rather than
|
|
444
|
+
* imported from `@credda/shared`.
|
|
445
|
+
*
|
|
446
|
+
* This file is mirrored byte-for-byte into the public `@credda/cli` package,
|
|
447
|
+
* which depends on nothing and builds outside this monorepo. An import of
|
|
448
|
+
* `@credda/shared` here would compile in `core` and break the mirror, so the
|
|
449
|
+
* only import this file may ever take is `./args.js`, which is mirrored
|
|
450
|
+
* alongside it.
|
|
451
|
+
*
|
|
452
|
+
* The copies are held to their originals by a test
|
|
453
|
+
* (`apps/cli/test/commands.test.ts`) that compares each list to the shared
|
|
454
|
+
* constant it duplicates. A vocabulary that drifts fails there rather than
|
|
455
|
+
* turning into a flag the API rejects.
|
|
456
|
+
*/
|
|
457
|
+
/**
|
|
458
|
+
* `INVESTIGATION_STATES` and `OUTCOMES` from `packages/shared/src/states.ts`,
|
|
459
|
+
* written out for the same reason the validation vocabularies below are: the
|
|
460
|
+
* mirror package depends on nothing and may not import `@credda/shared`.
|
|
461
|
+
* `apps/cli/test/commands.test.ts` holds both lists to their originals.
|
|
462
|
+
*/
|
|
463
|
+
const INVESTIGATION_STATE_CHOICES = [
|
|
464
|
+
'CREATED',
|
|
465
|
+
'PREPARING_ENVIRONMENT',
|
|
466
|
+
'ANALYZING_REPOSITORY',
|
|
467
|
+
'UNDERSTANDING_ISSUE',
|
|
468
|
+
'INVESTIGATING',
|
|
469
|
+
'ATTEMPTING_REPRODUCTION',
|
|
470
|
+
'REPRODUCED',
|
|
471
|
+
'DIAGNOSING',
|
|
472
|
+
'ROOT_CAUSE_IDENTIFIED',
|
|
473
|
+
'REPRODUCED_AND_DIAGNOSED',
|
|
474
|
+
'REPRODUCED_NOT_DIAGNOSED',
|
|
475
|
+
'CONTRADICTS_SPECIFICATION',
|
|
476
|
+
'ISSUE_ALREADY_RESOLVED',
|
|
477
|
+
'REPORT_REFUTED',
|
|
478
|
+
'NO_CHANGE_REQUIRED',
|
|
479
|
+
'NO_RUNNABLE_CHECK',
|
|
480
|
+
'REPRODUCTION_FAILED',
|
|
481
|
+
'INSUFFICIENT_EVIDENCE',
|
|
482
|
+
'GENERATING_PATCH',
|
|
483
|
+
'TESTING_PATCH',
|
|
484
|
+
'VERIFYING',
|
|
485
|
+
'VERIFIED',
|
|
486
|
+
'READY_FOR_REVIEW',
|
|
487
|
+
'VERIFICATION_FAILED',
|
|
488
|
+
'PATCH_REJECTED',
|
|
489
|
+
'NEEDS_HUMAN_INPUT',
|
|
490
|
+
'CANCELLED',
|
|
491
|
+
'FAILED',
|
|
492
|
+
];
|
|
493
|
+
const OUTCOME_CHOICES = [
|
|
494
|
+
'REPRODUCED_AND_DIAGNOSED',
|
|
495
|
+
'REPRODUCED_NOT_DIAGNOSED',
|
|
496
|
+
'CONTRADICTS_SPECIFICATION',
|
|
497
|
+
'NO_CHANGE_REQUIRED',
|
|
498
|
+
'NO_RUNNABLE_CHECK',
|
|
499
|
+
'INCONCLUSIVE',
|
|
500
|
+
'VERIFIED',
|
|
501
|
+
'PARTIALLY_VERIFIED',
|
|
502
|
+
'PATCH_REJECTED',
|
|
503
|
+
'CANCELLED',
|
|
504
|
+
'ERRORED',
|
|
505
|
+
];
|
|
506
|
+
const VALIDATION_STATE_CHOICES = [
|
|
507
|
+
'CREATED',
|
|
508
|
+
'ANALYZING_CHANGE',
|
|
509
|
+
'UNDERSTANDING_INTENT',
|
|
510
|
+
'PLANNING',
|
|
511
|
+
'PREPARING_ENVIRONMENT',
|
|
512
|
+
'RUNNING',
|
|
513
|
+
'CONFIRMING_FINDINGS',
|
|
514
|
+
'INVESTIGATING_FINDING',
|
|
515
|
+
'COMPLETED',
|
|
516
|
+
'CANCELLED',
|
|
517
|
+
'FAILED',
|
|
518
|
+
];
|
|
519
|
+
const VALIDATION_OUTCOME_CHOICES = [
|
|
520
|
+
'VERIFIED',
|
|
521
|
+
'FAILED',
|
|
522
|
+
'BLOCKED',
|
|
523
|
+
'INCONCLUSIVE',
|
|
524
|
+
'NO_CHANGE_REQUIRED',
|
|
525
|
+
'CANCELLED',
|
|
526
|
+
'ERRORED',
|
|
527
|
+
];
|
|
528
|
+
const FINDING_SEVERITY_CHOICES = ['HIGH', 'MEDIUM', 'LOW'];
|
|
529
|
+
const FINDING_STATUS_CHOICES = ['OPEN', 'DISMISSED', 'ENVIRONMENT_RELATED', 'RESOLVED'];
|
|
530
|
+
/**
|
|
531
|
+
* `credda validations` and `credda validation`: the change-scoped run, read
|
|
532
|
+
* from a terminal.
|
|
533
|
+
*
|
|
534
|
+
* ## Why the object is separate from an investigation, and the commands with it
|
|
535
|
+
*
|
|
536
|
+
* An investigation asks whether one reported defect is fixed and answers with
|
|
537
|
+
* one Outcome. A validation asks whether a change works, which does not
|
|
538
|
+
* decompose into one question -- it decomposes into n checks that pass, fail,
|
|
539
|
+
* or turn out to be impossible to run, independently of one another (ADR 0010,
|
|
540
|
+
* and `packages/shared/src/validation.ts`). `status` and `inspect` cannot be
|
|
541
|
+
* widened to cover both without one of the two objects reading as the other,
|
|
542
|
+
* so the pair below mirrors them rather than absorbing them: `validations`
|
|
543
|
+
* lists, `validation` reads one in full.
|
|
544
|
+
*
|
|
545
|
+
* ## What these two commands may never come to mean
|
|
546
|
+
*
|
|
547
|
+
* They READ. Nothing here starts a validation, and nothing here writes,
|
|
548
|
+
* merges, closes or comments. They are the terminal's view of records the
|
|
549
|
+
* engine already wrote, and a validation is scoped to a change somebody
|
|
550
|
+
* proposed -- Credda does not go looking through a repository for defects
|
|
551
|
+
* nobody reported.
|
|
552
|
+
*
|
|
553
|
+
* The filters are exactly the ones `apps/api/src/routes/validations.ts`
|
|
554
|
+
* accepts, under the same names and the same vocabularies, because a filter
|
|
555
|
+
* that means something different on two surfaces is worse than a missing one.
|
|
556
|
+
*/
|
|
557
|
+
const VALIDATIONS = {
|
|
558
|
+
name: 'validations',
|
|
559
|
+
summary: 'List change-scoped validation runs',
|
|
560
|
+
args: '[--repository <path-or-id>] [--state <state>] [--outcome <outcome>] [--limit <n>] [--offset <n>]',
|
|
561
|
+
flags: {
|
|
562
|
+
repository: {
|
|
563
|
+
kind: 'string',
|
|
564
|
+
valueName: '<path-or-id>',
|
|
565
|
+
description: 'Only validations of one repository. A path to a checkout or the\n' +
|
|
566
|
+
' repository id; an unknown one is refused rather than answered\n' +
|
|
567
|
+
' with an empty list',
|
|
568
|
+
},
|
|
569
|
+
state: {
|
|
570
|
+
kind: 'string',
|
|
571
|
+
choices: VALIDATION_STATE_CHOICES,
|
|
572
|
+
valueName: '<state>',
|
|
573
|
+
description: 'Only validations in this state',
|
|
574
|
+
},
|
|
575
|
+
outcome: {
|
|
576
|
+
kind: 'string',
|
|
577
|
+
choices: VALIDATION_OUTCOME_CHOICES,
|
|
578
|
+
valueName: '<outcome>',
|
|
579
|
+
description: 'Only validations that concluded this',
|
|
580
|
+
},
|
|
581
|
+
limit: { kind: 'number', valueName: '<n>', description: 'How many to list', defaultNote: '50' },
|
|
582
|
+
offset: { kind: 'number', valueName: '<n>', description: 'Skip this many first', defaultNote: '0' },
|
|
583
|
+
},
|
|
584
|
+
details: [
|
|
585
|
+
'A validation is the change-scoped run: it takes a change somebody proposed',
|
|
586
|
+
'and asks, check by check, whether it works. It is a different object from an',
|
|
587
|
+
'investigation, which takes one reported defect and asks whether it is fixed.',
|
|
588
|
+
' credda status lists investigations instead',
|
|
589
|
+
'',
|
|
590
|
+
'STATE is where the run got to. OUTCOME is what it concluded, and only the',
|
|
591
|
+
'outcome is a verdict: a run that finished and found two failures is as',
|
|
592
|
+
'COMPLETED as one that found none.',
|
|
593
|
+
'',
|
|
594
|
+
'VERIFIED requires at least one check to have actually passed. A run with no',
|
|
595
|
+
'passing check is INCONCLUSIVE, never a clean bill of health, and BLOCKED',
|
|
596
|
+
'means the environment would not come up so nothing was asked of the change',
|
|
597
|
+
'at all.',
|
|
598
|
+
'',
|
|
599
|
+
'Read one of them in full with: credda validation <id>',
|
|
600
|
+
],
|
|
601
|
+
};
|
|
602
|
+
const VALIDATION = {
|
|
603
|
+
name: 'validation',
|
|
604
|
+
summary: 'Show one validation: its checks, and the findings they raised',
|
|
605
|
+
args: '<validation-id-or-prefix> [--severity <s>] [--status <s>] [--limit <n>] [--offset <n>]',
|
|
606
|
+
flags: {
|
|
607
|
+
severity: {
|
|
608
|
+
kind: 'string',
|
|
609
|
+
choices: FINDING_SEVERITY_CHOICES,
|
|
610
|
+
valueName: '<severity>',
|
|
611
|
+
description: 'Only findings of this severity',
|
|
612
|
+
},
|
|
613
|
+
status: {
|
|
614
|
+
kind: 'string',
|
|
615
|
+
choices: FINDING_STATUS_CHOICES,
|
|
616
|
+
valueName: '<status>',
|
|
617
|
+
description: 'Only findings with this status',
|
|
618
|
+
},
|
|
619
|
+
limit: {
|
|
620
|
+
kind: 'number',
|
|
621
|
+
valueName: '<n>',
|
|
622
|
+
description: 'How many findings to show',
|
|
623
|
+
defaultNote: '50',
|
|
624
|
+
},
|
|
625
|
+
offset: {
|
|
626
|
+
kind: 'number',
|
|
627
|
+
valueName: '<n>',
|
|
628
|
+
description: 'Skip this many findings first',
|
|
629
|
+
defaultNote: '0',
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
details: [
|
|
633
|
+
'Any unambiguous prefix of a validation id is accepted.',
|
|
634
|
+
' credda validations lists recent validations',
|
|
635
|
+
'',
|
|
636
|
+
'The plan is printed whole, in the order it was executed, and every check is',
|
|
637
|
+
'shown with the status it reached. A check that was never run is printed as',
|
|
638
|
+
'PENDING rather than omitted, because a silently missing check reads as a',
|
|
639
|
+
'passing one.',
|
|
640
|
+
'',
|
|
641
|
+
'Check statuses that are not failures, and are not successes either:',
|
|
642
|
+
' PRE_EXISTING_FAILURE it fails on this change and fails identically on the',
|
|
643
|
+
' base commit, so this change did not cause it. Shown',
|
|
644
|
+
' as context and never raised as a finding.',
|
|
645
|
+
' BLOCKED it could not be executed at all, so nothing was',
|
|
646
|
+
' observed about the change in either direction.',
|
|
647
|
+
'',
|
|
648
|
+
'A finding is narrower than a failure: a check reaches FAILED only after the',
|
|
649
|
+
'base commit was re-run and passed there, so every finding below carries the',
|
|
650
|
+
'fact that this change caused it.',
|
|
651
|
+
'',
|
|
652
|
+
'This command reads records. It starts nothing, writes nothing, and Credda',
|
|
653
|
+
'never merges a change.',
|
|
654
|
+
'',
|
|
655
|
+
'The findings filters narrow the findings only; the plan above them is always',
|
|
656
|
+
'printed whole, because a plan cut to a filter is a plan a reader cannot',
|
|
657
|
+
'check the outcome against.',
|
|
658
|
+
],
|
|
659
|
+
};
|
|
660
|
+
/**
|
|
661
|
+
* Stopping a run that is already going.
|
|
662
|
+
*
|
|
663
|
+
* ## Why this command exists at all
|
|
664
|
+
*
|
|
665
|
+
* Ctrl-C stops a run from the terminal that started it. That covers the case
|
|
666
|
+
* where the operator is still sitting there, and it is the only case Credda
|
|
667
|
+
* covered: a run started in a terminal that has since been closed, backgrounded,
|
|
668
|
+
* or left on another tab could not be stopped by anything short of `kill`, and
|
|
669
|
+
* `kill` leaves the sandbox container running -- which is why `credda reap`
|
|
670
|
+
* exists.
|
|
671
|
+
*
|
|
672
|
+
* `apps/api` has the same shape of problem from the other side and refuses to
|
|
673
|
+
* paper over it: `POST /api/investigations/:id/cancel` answers a CLI-started run
|
|
674
|
+
* with 409 NOT_CANCELLABLE, because the job queue never saw that run and the API
|
|
675
|
+
* cannot reach the process executing it. This command is the reach that answer
|
|
676
|
+
* says is missing, for the one machine where it is possible: `credda
|
|
677
|
+
* investigate` records its pid beside the store, and this reads it and sends the
|
|
678
|
+
* interrupt the running process already handles.
|
|
679
|
+
*
|
|
680
|
+
* ## What it may never say
|
|
681
|
+
*
|
|
682
|
+
* A cancel that reports success without stopping the run is worse than no
|
|
683
|
+
* cancel at all -- it tells an operator something false about their own machine
|
|
684
|
+
* and their own bill. So the two answers stay apart everywhere they are
|
|
685
|
+
* expressed: in the text, in the exit code (0 stopped, 7 asked), and in
|
|
686
|
+
* `CancelOutcome`, where `stopped: true` exists only on the outcomes for which
|
|
687
|
+
* it is true and a renderer that prints "Cancelled." on a request does not
|
|
688
|
+
* compile.
|
|
689
|
+
*/
|
|
690
|
+
const CANCEL = {
|
|
691
|
+
name: 'cancel',
|
|
692
|
+
summary: 'Stop a running investigation, or say why it cannot be stopped',
|
|
693
|
+
args: '<investigation-id-or-prefix> [--reason <text>]',
|
|
694
|
+
flags: {
|
|
695
|
+
reason: {
|
|
696
|
+
kind: 'string',
|
|
697
|
+
valueName: '<text>',
|
|
698
|
+
description: 'Recorded on the investigation. Not required: a cancel with nothing\n' +
|
|
699
|
+
' said is still a cancel',
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
details: [
|
|
703
|
+
'Any unambiguous prefix of an investigation id is accepted.',
|
|
704
|
+
'',
|
|
705
|
+
'There are two good answers and they are not the same answer:',
|
|
706
|
+
'',
|
|
707
|
+
' stopped nothing is running. The run had not started, or its process is',
|
|
708
|
+
' already gone. The record is CANCELLED. Exit code 0.',
|
|
709
|
+
' asked a process on this machine is inside the run, holding a sandbox',
|
|
710
|
+
' and possibly a model call. It was signalled. It has not stopped:',
|
|
711
|
+
' it stops at its next checkpoint, tears the sandbox down, and',
|
|
712
|
+
' writes its own terminal state. This command does not write that',
|
|
713
|
+
' state and cannot say when it will be written. Exit code 7.',
|
|
714
|
+
'',
|
|
715
|
+
'Follow the second one to its end with: credda events <id> --follow',
|
|
716
|
+
'',
|
|
717
|
+
'A run that already finished cannot be stopped and cannot be undone, and a run',
|
|
718
|
+
'this machine cannot reach is reported as unreachable rather than marked',
|
|
719
|
+
'cancelled: marking it would be a state the still-running engine overwrites',
|
|
720
|
+
'minutes later, having spent the whole budget you thought you had stopped.',
|
|
721
|
+
'Both exit 2.',
|
|
722
|
+
'',
|
|
723
|
+
'Cancelling a run that was killed rather than interrupted also leaves its',
|
|
724
|
+
'sandbox container behind. Clean those up with: credda reap',
|
|
725
|
+
],
|
|
726
|
+
};
|
|
727
|
+
/**
|
|
728
|
+
* `credda discover`: read a checkout and write the bug reports nobody filed.
|
|
729
|
+
*
|
|
730
|
+
* ## What it is, and the sentence it must never be read as
|
|
731
|
+
*
|
|
732
|
+
* ADR 0024 decides that discovery produces a REPORT and the existing pipeline
|
|
733
|
+
* decides what it is worth. That is the whole design and this command is the
|
|
734
|
+
* only thing that makes it reachable: `discoverFromRepository()` in
|
|
735
|
+
* `@credda/repository` walks a tree, runs both rule sets -- the security
|
|
736
|
+
* classes and the locally decidable defect classes -- and returns candidates in
|
|
737
|
+
* the same `{title, body}` slot a forge issue and a rendered signal fill. Until
|
|
738
|
+
* this existed nothing read them.
|
|
739
|
+
*
|
|
740
|
+
* "Credda finds bugs and security vulnerabilities" is the sentence this command
|
|
741
|
+
* must never be read as, wherever it is written. It finds SHAPES and writes
|
|
742
|
+
* reports about them, and measured against 160 real cases from 50 real
|
|
743
|
+
* repositories it confirmed none of them: 103 candidates, none of which fell
|
|
744
|
+
* silent at the maintainer's fix, and on no case did a finder name the defect
|
|
745
|
+
* the case pins (ADR 0024, amendment). A candidate is a report,
|
|
746
|
+
* not a finding and not a vulnerability disclosure, and every one of them is
|
|
747
|
+
* still owed a reproduction before it is anything at all -- which is why the
|
|
748
|
+
* output leads with the observation that would refute each one, and why it
|
|
749
|
+
* states no severity. Severity is a judgement about exposure and a single-file
|
|
750
|
+
* rule knows nothing about what a repository is exposed to.
|
|
751
|
+
*
|
|
752
|
+
* ## Why it does not start runs
|
|
753
|
+
*
|
|
754
|
+
* Discovery finding something is not consent to spend a model budget on it.
|
|
755
|
+
* This command writes reports and stops; the operator decides which of them is
|
|
756
|
+
* worth a sandbox, and starts it with `credda investigate` like any other
|
|
757
|
+
* report. That is the same discipline as `start: true` defaulting to false on
|
|
758
|
+
* the API's create route, and it is the reason this command may be run on
|
|
759
|
+
* anything without asking what it will cost.
|
|
760
|
+
*
|
|
761
|
+
* ## What it may never come to do
|
|
762
|
+
*
|
|
763
|
+
* It reads. It executes nothing -- not the repository's code and not the
|
|
764
|
+
* programs it emits, whose entire defect in the ReDoS case is that running them
|
|
765
|
+
* hangs a CPU (ADR 0005). The day this command runs one, it is an unsandboxed
|
|
766
|
+
* execution path opened by a scanner, and the name is a promise again.
|
|
767
|
+
*/
|
|
768
|
+
const DISCOVER = {
|
|
769
|
+
name: 'discover',
|
|
770
|
+
summary: 'Read a checkout and write the bug reports nobody filed. Starts nothing',
|
|
771
|
+
args: '<repo-path> [--out <dir>] [--max-files <n>] [--json]',
|
|
772
|
+
flags: {
|
|
773
|
+
out: {
|
|
774
|
+
kind: 'string',
|
|
775
|
+
valueName: '<dir>',
|
|
776
|
+
description: 'Write each candidate report to a file in <dir>, and print the exact\n' +
|
|
777
|
+
' investigate command for it. Without this, the candidates are\n' +
|
|
778
|
+
' listed and nothing is written',
|
|
779
|
+
},
|
|
780
|
+
'max-files': {
|
|
781
|
+
kind: 'number',
|
|
782
|
+
valueName: '<n>',
|
|
783
|
+
description: 'How many source files to read',
|
|
784
|
+
defaultNote: '400',
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
details: [
|
|
788
|
+
'What a run does, and what it costs:',
|
|
789
|
+
' it walks the checkout, reads its JavaScript and TypeScript source, runs',
|
|
790
|
+
' both rule sets over it -- the security classes and the locally decidable',
|
|
791
|
+
' defect classes -- and writes an ordinary bug report about each shape it',
|
|
792
|
+
' saw. The listing counts the classes off those lists rather than naming a',
|
|
793
|
+
' number here, which is a number that rots. No sandbox, no container, no',
|
|
794
|
+
' install, no network, no model call and no API key. Nothing in the',
|
|
795
|
+
' repository is executed and nothing in it is written to.',
|
|
796
|
+
'',
|
|
797
|
+
'What a candidate is:',
|
|
798
|
+
' a REPORT Credda wrote instead of waiting for somebody to write one. It is',
|
|
799
|
+
' not a finding, it is not a vulnerability disclosure, and it states no',
|
|
800
|
+
' severity -- severity is a judgement about exposure, and a rule reading one',
|
|
801
|
+
' file knows nothing about what this repository is exposed to. Each report',
|
|
802
|
+
' carries a program that decides the question by running, and that program',
|
|
803
|
+
' is written so it can come back saying no. Most of them do.',
|
|
804
|
+
'',
|
|
805
|
+
'Nothing is started:',
|
|
806
|
+
' discovery finding something is not consent to spend a model budget on it.',
|
|
807
|
+
' This command creates no investigation. You choose which candidate is worth',
|
|
808
|
+
' one, and start it yourself:',
|
|
809
|
+
'',
|
|
810
|
+
' credda discover ./my-app --out ./candidates',
|
|
811
|
+
' credda investigate ./my-app @./candidates/01-redos-src-parse-ts-84.md \\',
|
|
812
|
+
' --ref discovery:REDOS:src/parse.ts:84',
|
|
813
|
+
'',
|
|
814
|
+
' The --ref is printed for you beside each candidate. It is what records',
|
|
815
|
+
' that Credda wrote the report, so the run reads as its own claim rather',
|
|
816
|
+
' than as somebody else\'s. Nothing downstream branches on it: a discovered',
|
|
817
|
+
' report is put through the identical pipeline, with the identical stages',
|
|
818
|
+
' and the identical refusals, and a candidate that cannot be reproduced',
|
|
819
|
+
' produces nothing. That is the correct outcome, and the common one.',
|
|
820
|
+
'',
|
|
821
|
+
'No candidates is not a clean bill of health. Two rule sets are looked for --',
|
|
822
|
+
'the security shapes and the locally decidable defect shapes -- and the',
|
|
823
|
+
'listing names which one spoke for each candidate. The output says how many',
|
|
824
|
+
'files were read and whether the walk stopped short, because "we did not',
|
|
825
|
+
'see it" and "it cannot happen" are different claims.',
|
|
826
|
+
'',
|
|
827
|
+
'What this has been measured to do, so a candidate is read for what it is:',
|
|
828
|
+
' against 160 cases from 50 real repositories -- each at a commit where a',
|
|
829
|
+
' defect is present and again at the maintainer\'s fix -- the rules emitted',
|
|
830
|
+
' 103 candidates, none of them fell silent at the fix, and on no case did a',
|
|
831
|
+
' rule name the defect the case pins. Read a candidate as a report worth a',
|
|
832
|
+
' reproduction, never as a defect this found.',
|
|
833
|
+
'',
|
|
834
|
+
'Exit code is 0 whether or not anything was found. A list of reports is not',
|
|
835
|
+
'a failed check.',
|
|
836
|
+
],
|
|
837
|
+
};
|
|
838
|
+
export const COMMANDS = {
|
|
839
|
+
investigate: INVESTIGATE,
|
|
840
|
+
triage: TRIAGE,
|
|
841
|
+
discover: DISCOVER,
|
|
842
|
+
doctor: {
|
|
843
|
+
name: 'doctor',
|
|
844
|
+
summary: 'Check that this environment can reproduce a bug',
|
|
845
|
+
args: '[<repo-path>] [--deep]',
|
|
846
|
+
flags: {
|
|
847
|
+
deep: {
|
|
848
|
+
kind: 'boolean',
|
|
849
|
+
description: 'Also build or pull the sandbox image and probe its toolchain. Uses\n' +
|
|
850
|
+
' the network and can take minutes, so it is opt-in',
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
details: [
|
|
854
|
+
'Reports pass / warn / fail for eight checks that always run: the Node',
|
|
855
|
+
'version, git, configuration, the active model provider, the selected',
|
|
856
|
+
'sandbox, the container plane, CREDDA_HOME and the database. The sandbox',
|
|
857
|
+
'image is a ninth, reported only when docker is the selected plane or',
|
|
858
|
+
'--deep was given. Exits non-zero only when something is genuinely broken;',
|
|
859
|
+
'warnings alone exit 0.',
|
|
860
|
+
'',
|
|
861
|
+
'With a <repo-path>, it also reports whether that repository could be',
|
|
862
|
+
'prepared at all: the package manager and the exact install command a run',
|
|
863
|
+
'would use, and the test, build and typecheck commands it would find. That',
|
|
864
|
+
'is a plan read off the repository, not a run of it -- nothing is installed',
|
|
865
|
+
'and nothing is executed.',
|
|
866
|
+
'',
|
|
867
|
+
'--deep additionally builds or pulls the docker sandbox image and probes its',
|
|
868
|
+
'toolchain, which is the step that decides whether reproduction can happen',
|
|
869
|
+
'at all. It uses the network and can take minutes, so it is opt-in.',
|
|
870
|
+
],
|
|
871
|
+
},
|
|
872
|
+
reap: {
|
|
873
|
+
name: 'reap',
|
|
874
|
+
summary: 'Remove sandbox containers left behind by an interrupted run',
|
|
875
|
+
args: '[--dry-run] [--max-age-hours <n>]',
|
|
876
|
+
flags: {
|
|
877
|
+
'dry-run': {
|
|
878
|
+
kind: 'boolean',
|
|
879
|
+
description: 'List what would be removed and remove nothing',
|
|
880
|
+
},
|
|
881
|
+
'max-age-hours': {
|
|
882
|
+
kind: 'string',
|
|
883
|
+
description: 'Only reap containers older than this. Default 4, which is far above\n' +
|
|
884
|
+
' any real investigation',
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
details: [
|
|
888
|
+
'A sandbox container is removed when the investigation that made it finishes.',
|
|
889
|
+
'If that process is killed instead -- SIGKILL, a crash, a closed terminal on a',
|
|
890
|
+
'long benchmark -- nothing removes it, and it keeps its memory reservation and',
|
|
891
|
+
'its volume until somebody notices. Three of them once ran for seventeen hours',
|
|
892
|
+
'on this machine.',
|
|
893
|
+
'',
|
|
894
|
+
'This removes sandbox containers older than --max-age-hours, then the sandbox',
|
|
895
|
+
'volumes nothing references any more. Age is the test because it has no false',
|
|
896
|
+
'positives above a threshold no real run reaches; the default of four hours is',
|
|
897
|
+
'roughly twenty times the slowest case ever measured.',
|
|
898
|
+
'',
|
|
899
|
+
'It acts on containers this process did not create, so on a shared docker',
|
|
900
|
+
'daemon it can reach somebody else\'s run. Read --dry-run first.',
|
|
901
|
+
],
|
|
902
|
+
},
|
|
903
|
+
init: {
|
|
904
|
+
name: 'init',
|
|
905
|
+
summary: 'Write a credda.config.json with documented defaults',
|
|
906
|
+
args: '[--global] [--force]',
|
|
907
|
+
flags: {
|
|
908
|
+
global: {
|
|
909
|
+
kind: 'boolean',
|
|
910
|
+
description: 'Write to $CREDDA_HOME/credda.config.json instead of the current directory',
|
|
911
|
+
},
|
|
912
|
+
force: { kind: 'boolean', description: 'Overwrite an existing config file' },
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
cancel: CANCEL,
|
|
916
|
+
/*
|
|
917
|
+
* The filters are the ones `apps/api/src/routes/investigations.ts` accepts,
|
|
918
|
+
* under the same names and the same vocabularies, for the reason
|
|
919
|
+
* {@link VALIDATIONS} gives: a filter that means something different on two
|
|
920
|
+
* surfaces is worse than a missing one.
|
|
921
|
+
*
|
|
922
|
+
* This command had only `--limit` while its younger sibling `validations`
|
|
923
|
+
* shipped with the full set, so the two questions most often asked of a queue
|
|
924
|
+
* -- whose repository, and how did it end -- could be asked of a validation
|
|
925
|
+
* from a terminal and not of an investigation. Every one of these is a filter
|
|
926
|
+
* the local store has always supported; nothing new is read.
|
|
927
|
+
*
|
|
928
|
+
* `--signal` and `--hasSignal` are the API filters deliberately absent. A
|
|
929
|
+
* signal is a row this CLI never writes: a run started from a terminal is
|
|
930
|
+
* started by the person at it, so `signalId` is null on every investigation
|
|
931
|
+
* in a local store, and `--signal` could only ever return nothing while
|
|
932
|
+
* `--hasSignal false` could only ever return everything.
|
|
933
|
+
*
|
|
934
|
+
* `--ref` is the opposite case and is here for it. Provenance is a column
|
|
935
|
+
* this CLI DOES write -- `credda investigate --ref` records it, and `credda
|
|
936
|
+
* discover` prints a ref for every candidate it writes precisely so the run
|
|
937
|
+
* started from one says Credda wrote the report -- so until this flag the
|
|
938
|
+
* terminal wrote a fact it could not then ask a question about.
|
|
939
|
+
*/
|
|
940
|
+
status: {
|
|
941
|
+
name: 'status',
|
|
942
|
+
summary: 'List recent investigations',
|
|
943
|
+
args: '[--repository <path-or-id>] [--state <state>] [--outcome <outcome>] ' +
|
|
944
|
+
'[--ref <ref>] [--limit <n>] [--offset <n>] [--json]',
|
|
945
|
+
flags: {
|
|
946
|
+
repository: {
|
|
947
|
+
kind: 'string',
|
|
948
|
+
valueName: '<path-or-id>',
|
|
949
|
+
description: 'Only investigations of one repository. A path to a checkout or the\n' +
|
|
950
|
+
' repository id; an unknown one is refused rather than answered\n' +
|
|
951
|
+
' with an empty list',
|
|
952
|
+
},
|
|
953
|
+
state: {
|
|
954
|
+
kind: 'string',
|
|
955
|
+
choices: INVESTIGATION_STATE_CHOICES,
|
|
956
|
+
valueName: '<state>',
|
|
957
|
+
description: 'Only investigations in this state',
|
|
958
|
+
},
|
|
959
|
+
outcome: {
|
|
960
|
+
kind: 'string',
|
|
961
|
+
choices: OUTCOME_CHOICES,
|
|
962
|
+
valueName: '<outcome>',
|
|
963
|
+
description: 'Only investigations that concluded this',
|
|
964
|
+
},
|
|
965
|
+
ref: {
|
|
966
|
+
kind: 'string',
|
|
967
|
+
valueName: '<ref>',
|
|
968
|
+
description: 'Only investigations recorded as coming from this ref, matched whole.\n' +
|
|
969
|
+
' The value `credda investigate --ref` stored, and the one\n' +
|
|
970
|
+
' `credda discover` prints beside a candidate',
|
|
971
|
+
},
|
|
972
|
+
limit: { kind: 'number', valueName: '<n>', description: 'How many to list', defaultNote: '20' },
|
|
973
|
+
offset: { kind: 'number', valueName: '<n>', description: 'Skip this many first', defaultNote: '0' },
|
|
974
|
+
},
|
|
975
|
+
details: [
|
|
976
|
+
'STATE is where the run got to, including the terminal it stopped on.',
|
|
977
|
+
'OUTCOME is what it concluded, and a run still in flight has none -- so it',
|
|
978
|
+
'matches no --outcome value, and --state is the way to ask for it.',
|
|
979
|
+
'',
|
|
980
|
+
'Abstaining is a conclusion, not a gap: NO_CHANGE_REQUIRED means the',
|
|
981
|
+
'reported thing did not happen, and INCONCLUSIVE means the run would not',
|
|
982
|
+
'claim what it had not established. Both are successes and both exit 0.',
|
|
983
|
+
'',
|
|
984
|
+
'--ref asks where a run came from. It matches the whole string, which is',
|
|
985
|
+
'the form both writers of it produce: an issue reference or URL you passed',
|
|
986
|
+
'to `credda investigate --ref`, or the `discovery:<CLASS>:<file>:<line>`',
|
|
987
|
+
'ref `credda discover` prints beside a candidate. A run started with no ref',
|
|
988
|
+
'matches no value here.',
|
|
989
|
+
'',
|
|
990
|
+
' credda validations lists validation runs instead',
|
|
991
|
+
],
|
|
992
|
+
},
|
|
993
|
+
report: REPORT,
|
|
994
|
+
validations: VALIDATIONS,
|
|
995
|
+
validation: VALIDATION,
|
|
996
|
+
inspect: {
|
|
997
|
+
name: 'inspect',
|
|
998
|
+
summary: 'Show everything one run recorded, in full',
|
|
999
|
+
args: '<investigation-id-or-prefix>',
|
|
1000
|
+
flags: {},
|
|
1001
|
+
details: [
|
|
1002
|
+
'Any unambiguous prefix of an investigation id is accepted.',
|
|
1003
|
+
'',
|
|
1004
|
+
'This is the run: the reproduction, every hypothesis including the refuted',
|
|
1005
|
+
'ones, and the evidence records. For what the run established and what it',
|
|
1006
|
+
'did not, use: credda report <id>',
|
|
1007
|
+
'',
|
|
1008
|
+
'What it spent is printed from the run\'s own cost record, and under --json',
|
|
1009
|
+
'as `cost`, beside the ceiling that bounded it as `effectiveBudget`. Both',
|
|
1010
|
+
'are absent rather than zero when nothing recorded them: a run still going,',
|
|
1011
|
+
'and one that died before it could record, did not measure zero. A run',
|
|
1012
|
+
'started from this terminal is bounded by the flags in its own banner, and',
|
|
1013
|
+
'nothing writes that ceiling down, so `effectiveBudget` is null for it.',
|
|
1014
|
+
],
|
|
1015
|
+
},
|
|
1016
|
+
events: {
|
|
1017
|
+
name: 'events',
|
|
1018
|
+
summary: 'Show the event timeline for an investigation',
|
|
1019
|
+
args: '<investigation-id-or-prefix> [--since <n>] [--follow] [--json]',
|
|
1020
|
+
flags: {
|
|
1021
|
+
since: {
|
|
1022
|
+
kind: 'number',
|
|
1023
|
+
valueName: '<n>',
|
|
1024
|
+
description: 'Only events with a sequence number greater than <n>',
|
|
1025
|
+
},
|
|
1026
|
+
follow: {
|
|
1027
|
+
kind: 'boolean',
|
|
1028
|
+
alias: 'f',
|
|
1029
|
+
description: 'Tail a running investigation until it reaches a terminal state',
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
details: ['Any unambiguous prefix of an investigation id is accepted.'],
|
|
1033
|
+
},
|
|
1034
|
+
/*
|
|
1035
|
+
* The two former names. Kept permanently, and kept honest: each takes the
|
|
1036
|
+
* canonical command's flags, details and behaviour, and only the summary line
|
|
1037
|
+
* differs, so `credda fix --help` never reads as a promise to write one.
|
|
1038
|
+
*/
|
|
1039
|
+
resolve: {
|
|
1040
|
+
...INVESTIGATE,
|
|
1041
|
+
name: 'resolve',
|
|
1042
|
+
summary: "Alias for 'credda investigate', kept because scripts and docs use it",
|
|
1043
|
+
aliasOf: 'investigate',
|
|
1044
|
+
},
|
|
1045
|
+
fix: {
|
|
1046
|
+
...INVESTIGATE,
|
|
1047
|
+
name: 'fix',
|
|
1048
|
+
summary: "Alias for 'credda investigate', kept because scripts and docs use it",
|
|
1049
|
+
aliasOf: 'investigate',
|
|
1050
|
+
},
|
|
1051
|
+
resolution: {
|
|
1052
|
+
...REPORT,
|
|
1053
|
+
name: 'resolution',
|
|
1054
|
+
summary: "Alias for 'credda report', kept because scripts and docs use it",
|
|
1055
|
+
aliasOf: 'report',
|
|
1056
|
+
},
|
|
1057
|
+
};
|
|
1058
|
+
/**
|
|
1059
|
+
* The command an alias dispatches to. Unknown names are returned unchanged so
|
|
1060
|
+
* the caller's own "unknown command" path still owns that message.
|
|
1061
|
+
*/
|
|
1062
|
+
export function canonicalCommand(name) {
|
|
1063
|
+
return own(COMMANDS, name)?.aliasOf ?? name;
|
|
1064
|
+
}
|
|
1065
|
+
/** Alias name to the command it stands for, for the root usage. */
|
|
1066
|
+
export function aliases() {
|
|
1067
|
+
return Object.entries(COMMANDS)
|
|
1068
|
+
.filter(([, spec]) => spec.aliasOf !== undefined)
|
|
1069
|
+
.map(([name, spec]) => [name, spec.aliasOf]);
|
|
1070
|
+
}
|
|
1071
|
+
const ENVIRONMENT = [
|
|
1072
|
+
['CREDDA_HOME', 'Where Credda stores its database and evidence (default ./.credda)'],
|
|
1073
|
+
[
|
|
1074
|
+
'ANTHROPIC_API_KEY',
|
|
1075
|
+
'Enables the Anthropic provider. Without it reasoning is rule-based,\n' +
|
|
1076
|
+
' which reaches a reproduction but rarely a diagnosis; every\n' +
|
|
1077
|
+
' report says which provider produced it.',
|
|
1078
|
+
],
|
|
1079
|
+
[
|
|
1080
|
+
'CREDDA_PROVIDER',
|
|
1081
|
+
"'auto', 'heuristic' or 'openai-compatible'. 'heuristic' forces the\n" +
|
|
1082
|
+
' deterministic provider',
|
|
1083
|
+
],
|
|
1084
|
+
['CREDDA_MODEL', 'Overrides the Anthropic model id'],
|
|
1085
|
+
[
|
|
1086
|
+
'CREDDA_OPENAI_API_KEY',
|
|
1087
|
+
'Enables the openai-compatible provider (NVIDIA NIM by default).\n' +
|
|
1088
|
+
' NVIDIA_API_KEY is accepted as a second name.',
|
|
1089
|
+
],
|
|
1090
|
+
['CREDDA_OPENAI_BASE_URL', 'OpenAI-compatible base URL (default NVIDIA NIM)'],
|
|
1091
|
+
['CREDDA_OPENAI_MODEL', 'Model id served by that endpoint'],
|
|
1092
|
+
[
|
|
1093
|
+
'CREDDA_OPENAI_RPM',
|
|
1094
|
+
'Client-side request pacing (default 40, NVIDIA free-tier limit)',
|
|
1095
|
+
],
|
|
1096
|
+
[
|
|
1097
|
+
'CREDDA_SANDBOX',
|
|
1098
|
+
"'local' (this command's default), 'native' or 'docker'. local and native\n" +
|
|
1099
|
+
' run repository code directly on this host, and only a local credda\n' +
|
|
1100
|
+
' invocation may select them: a repository arriving any other way\n' +
|
|
1101
|
+
' is refused them and must use docker. Credda never falls back\n' +
|
|
1102
|
+
' silently in either direction.',
|
|
1103
|
+
],
|
|
1104
|
+
['CREDDA_SANDBOX_IMAGE', 'Overrides the image the docker plane builds or pulls'],
|
|
1105
|
+
['CREDDA_LOG_LEVEL', 'debug | info | warn | error (default warn)'],
|
|
1106
|
+
['NO_COLOR', 'Set to any value to disable ANSI colour'],
|
|
1107
|
+
['CREDDA_ASCII', 'Set to any value to draw with ASCII instead of box characters'],
|
|
1108
|
+
['TERM', "'dumb' is treated the same as NO_COLOR"],
|
|
1109
|
+
];
|
|
1110
|
+
export function rootUsage() {
|
|
1111
|
+
const lines = [
|
|
1112
|
+
'credda - something broke, find out what',
|
|
1113
|
+
'',
|
|
1114
|
+
'Usage: credda <command> [options]',
|
|
1115
|
+
'',
|
|
1116
|
+
'Workflow: signal -> investigate -> reproduce -> diagnose -> report.',
|
|
1117
|
+
'Credda reports what it found and stops there. It changes nothing in your',
|
|
1118
|
+
'working tree: any fix it attempts is made in a disposable copy.',
|
|
1119
|
+
'',
|
|
1120
|
+
'Commands:',
|
|
1121
|
+
];
|
|
1122
|
+
const named = Object.values(COMMANDS).filter((command) => command.aliasOf === undefined);
|
|
1123
|
+
const width = Math.max(...named.map((c) => c.name.length));
|
|
1124
|
+
for (const command of named) {
|
|
1125
|
+
lines.push(` ${command.name.padEnd(width)} ${command.summary}`);
|
|
1126
|
+
}
|
|
1127
|
+
const aliased = aliases();
|
|
1128
|
+
if (aliased.length > 0) {
|
|
1129
|
+
lines.push('', 'Aliases:');
|
|
1130
|
+
for (const [alias, target] of aliased) {
|
|
1131
|
+
lines.push(` ${alias.padEnd(width)} ${target}, under its former name. Both are supported.`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
lines.push('', 'Global options:');
|
|
1135
|
+
lines.push(...renderFlags(GLOBAL_FLAGS));
|
|
1136
|
+
lines.push('', 'Environment:');
|
|
1137
|
+
for (const [name, description] of ENVIRONMENT) {
|
|
1138
|
+
lines.push(` ${name.padEnd(19)} ${description}`);
|
|
1139
|
+
}
|
|
1140
|
+
lines.push('', 'Exit codes:');
|
|
1141
|
+
lines.push(...EXIT_CODE_HELP);
|
|
1142
|
+
lines.push('', "Run 'credda <command> --help' for options specific to a command.");
|
|
1143
|
+
return lines.join('\n');
|
|
1144
|
+
}
|
|
1145
|
+
export function commandUsage(name) {
|
|
1146
|
+
const spec = own(COMMANDS, name);
|
|
1147
|
+
if (spec === undefined)
|
|
1148
|
+
return rootUsage();
|
|
1149
|
+
const lines = [
|
|
1150
|
+
`credda ${spec.name} - ${spec.summary}`,
|
|
1151
|
+
'',
|
|
1152
|
+
`Usage: credda ${spec.name} ${spec.args}`.trimEnd(),
|
|
1153
|
+
];
|
|
1154
|
+
if (spec.aliasOf !== undefined) {
|
|
1155
|
+
/*
|
|
1156
|
+
* What the command DOES, before the note about what it is called.
|
|
1157
|
+
*
|
|
1158
|
+
* An alias whose summary line reads "Alias for 'credda investigate'" tells a
|
|
1159
|
+
* reader nothing about the output, and this is the help a person reaches
|
|
1160
|
+
* when the name in their fingers is the one that used to promise a patch.
|
|
1161
|
+
* The canonical summary is restated here so the answer is on the screen
|
|
1162
|
+
* rather than one command away.
|
|
1163
|
+
*/
|
|
1164
|
+
const target = own(COMMANDS, spec.aliasOf);
|
|
1165
|
+
if (target !== undefined)
|
|
1166
|
+
lines.push('', `What it does: ${target.summary.toLowerCase()}.`);
|
|
1167
|
+
lines.push('', `'credda ${spec.name}' and 'credda ${spec.aliasOf}' are the same command. ${spec.aliasOf} is the`, 'current name; this one is kept permanently, because docs, scripts and the', "external benchmark harness use it and a name in someone's fingers that stops", 'working is worse than one that is out of date. The name is the only thing', 'that is out of date.');
|
|
1168
|
+
}
|
|
1169
|
+
if (Object.keys(spec.flags).length > 0) {
|
|
1170
|
+
lines.push('', 'Options:');
|
|
1171
|
+
lines.push(...renderFlags(spec.flags));
|
|
1172
|
+
}
|
|
1173
|
+
lines.push('', 'Global options:');
|
|
1174
|
+
lines.push(...renderFlags(GLOBAL_FLAGS));
|
|
1175
|
+
if (spec.details !== undefined)
|
|
1176
|
+
lines.push('', ...spec.details);
|
|
1177
|
+
lines.push('', 'Exit codes:');
|
|
1178
|
+
lines.push(...EXIT_CODE_HELP);
|
|
1179
|
+
return lines.join('\n');
|
|
1180
|
+
}
|
|
1181
|
+
function renderFlags(flags) {
|
|
1182
|
+
const entries = Object.entries(flags).map(([name, spec]) => {
|
|
1183
|
+
const alias = spec.alias === undefined ? ' ' : `-${spec.alias}, `;
|
|
1184
|
+
const value = spec.kind === 'boolean' ? '' : ` ${spec.valueName ?? '<value>'}`;
|
|
1185
|
+
return [` ${alias}--${name}${value}`, spec, name];
|
|
1186
|
+
});
|
|
1187
|
+
const width = Math.max(...entries.map(([left]) => left.length));
|
|
1188
|
+
return entries.map(([left, spec]) => {
|
|
1189
|
+
const suffix = spec.defaultNote === undefined ? '' : ` (default: ${spec.defaultNote})`;
|
|
1190
|
+
return `${left.padEnd(width)} ${spec.description}${suffix}`;
|
|
1191
|
+
});
|
|
1192
|
+
}
|