@credda/cli 0.1.6 → 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/dist/args.d.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * A small argument parser for the `credda` command surface.
3
+ *
4
+ * Hand-rolled deliberately: a dependency here would be a supply-chain risk
5
+ * taken on for string parsing. The surface has outgrown the "six commands and a
6
+ * dozen flags" this line used to claim -- it is 13 commands, 3 aliases and 27
7
+ * flags -- and the argument holds better at that size, not worse.
8
+ * The parser is spec-driven so `--help` is generated from the same data the
9
+ * parser validates against and cannot drift from it.
10
+ */
11
+ export type FlagKind = 'boolean' | 'string' | 'number';
12
+ export interface FlagSpec {
13
+ readonly kind: FlagKind;
14
+ readonly description: string;
15
+ /** Single-character alias, without the leading dash. */
16
+ readonly alias?: string;
17
+ /** Permitted values for a string flag. */
18
+ readonly choices?: readonly string[];
19
+ /** Placeholder shown in help, e.g. `<n>`. */
20
+ readonly valueName?: string;
21
+ /** Documented default, shown in help only. Never applied by the parser. */
22
+ readonly defaultNote?: string;
23
+ }
24
+ export interface CommandSpec {
25
+ readonly name: string;
26
+ readonly summary: string;
27
+ /**
28
+ * The argument portion of the usage line, without `credda ` or the command
29
+ * name. Both are composed from `name`, so an alias renders its own usage
30
+ * rather than the canonical command's.
31
+ */
32
+ readonly args: string;
33
+ readonly flags: Readonly<Record<string, FlagSpec>>;
34
+ /** Longer explanation appended to `credda <command> --help`. */
35
+ readonly details?: readonly string[];
36
+ /**
37
+ * The command this one is a spelling of. Parsing and help are unchanged; only
38
+ * dispatch resolves through it, so an alias cannot drift from its target.
39
+ */
40
+ readonly aliasOf?: string;
41
+ }
42
+ /** A user error: bad flag, missing value, unknown command. Never a bug. */
43
+ export declare class UsageError extends Error {
44
+ /** The command whose usage should be printed, if one was identified. */
45
+ readonly command: string | null;
46
+ constructor(message: string,
47
+ /** The command whose usage should be printed, if one was identified. */
48
+ command?: string | null);
49
+ }
50
+ export interface GlobalFlags {
51
+ readonly help: boolean;
52
+ readonly version: boolean;
53
+ readonly json: boolean;
54
+ readonly quiet: boolean;
55
+ readonly verbose: boolean;
56
+ readonly color: boolean;
57
+ }
58
+ export type FlagValue = string | number | boolean;
59
+ export interface ParsedCommand {
60
+ readonly command: string | null;
61
+ readonly globals: GlobalFlags;
62
+ readonly flags: ReadonlyMap<string, FlagValue>;
63
+ readonly positionals: readonly string[];
64
+ }
65
+ /**
66
+ * A lookup that reads only what a table actually declares.
67
+ *
68
+ * `COMMANDS`, `GLOBAL_FLAGS` and each command's `flags` are plain object
69
+ * literals, so `table[name]` walks the prototype chain: every member of
70
+ * `Object.prototype` answers `!== undefined` and is accepted as a real command
71
+ * or flag.
72
+ *
73
+ * That is not theoretical. Against the shipped build:
74
+ *
75
+ * credda investigate --constructor ./repo
76
+ *
77
+ * was accepted, and the flag SWALLOWED the next token as its value — so the
78
+ * repository path silently disappeared and the run began without it. The same
79
+ * argv with `--nope` correctly threw "Unknown flag". `credda toString` resolved
80
+ * as a command instead of "Unknown command", and `commandUsage('toString')`
81
+ * then threw a TypeError rather than printing usage.
82
+ *
83
+ * A CLI's whole contract is that it refuses what it does not understand. This
84
+ * makes the lookup answer for declared keys only.
85
+ */
86
+ export declare function own<T>(table: Readonly<Record<string, T>>, name: string): T | undefined;
87
+ export declare const GLOBAL_FLAGS: Readonly<Record<string, FlagSpec>>;
88
+ /**
89
+ * Parses argv against a command table.
90
+ *
91
+ * `--` ends flag parsing; everything after it is positional. A bare `-` is a
92
+ * positional (it means stdin to `credda investigate`), not a flag.
93
+ */
94
+ export declare function parseArgs(argv: readonly string[], commands: Readonly<Record<string, CommandSpec>>): ParsedCommand;
95
+ /** Reads a string flag, or null when absent. */
96
+ export declare function stringFlag(parsed: ParsedCommand, name: string): string | null;
97
+ /** Reads a number flag, or null when absent. */
98
+ export declare function numberFlag(parsed: ParsedCommand, name: string): number | null;
99
+ export declare function boolFlag(parsed: ParsedCommand, name: string): boolean;
package/dist/args.js ADDED
@@ -0,0 +1,222 @@
1
+ /**
2
+ * A small argument parser for the `credda` command surface.
3
+ *
4
+ * Hand-rolled deliberately: a dependency here would be a supply-chain risk
5
+ * taken on for string parsing. The surface has outgrown the "six commands and a
6
+ * dozen flags" this line used to claim -- it is 13 commands, 3 aliases and 27
7
+ * flags -- and the argument holds better at that size, not worse.
8
+ * The parser is spec-driven so `--help` is generated from the same data the
9
+ * parser validates against and cannot drift from it.
10
+ */
11
+ /** A user error: bad flag, missing value, unknown command. Never a bug. */
12
+ export class UsageError extends Error {
13
+ command;
14
+ constructor(message,
15
+ /** The command whose usage should be printed, if one was identified. */
16
+ command = null) {
17
+ super(message);
18
+ this.command = command;
19
+ this.name = 'UsageError';
20
+ }
21
+ }
22
+ /**
23
+ * A lookup that reads only what a table actually declares.
24
+ *
25
+ * `COMMANDS`, `GLOBAL_FLAGS` and each command's `flags` are plain object
26
+ * literals, so `table[name]` walks the prototype chain: every member of
27
+ * `Object.prototype` answers `!== undefined` and is accepted as a real command
28
+ * or flag.
29
+ *
30
+ * That is not theoretical. Against the shipped build:
31
+ *
32
+ * credda investigate --constructor ./repo
33
+ *
34
+ * was accepted, and the flag SWALLOWED the next token as its value — so the
35
+ * repository path silently disappeared and the run began without it. The same
36
+ * argv with `--nope` correctly threw "Unknown flag". `credda toString` resolved
37
+ * as a command instead of "Unknown command", and `commandUsage('toString')`
38
+ * then threw a TypeError rather than printing usage.
39
+ *
40
+ * A CLI's whole contract is that it refuses what it does not understand. This
41
+ * makes the lookup answer for declared keys only.
42
+ */
43
+ export function own(table, name) {
44
+ return Object.prototype.hasOwnProperty.call(table, name) ? table[name] : undefined;
45
+ }
46
+ export const GLOBAL_FLAGS = {
47
+ help: { kind: 'boolean', alias: 'h', description: 'Show help for the command and exit' },
48
+ version: { kind: 'boolean', description: 'Print the credda version and exit' },
49
+ json: { kind: 'boolean', description: 'Machine-readable JSONL on stdout; nothing else on stdout' },
50
+ quiet: { kind: 'boolean', description: 'Print only the final outcome line' },
51
+ verbose: { kind: 'boolean', description: 'Include debug-severity events' },
52
+ 'no-color': { kind: 'boolean', description: 'Disable ANSI colour (also honoured: NO_COLOR)' },
53
+ };
54
+ /**
55
+ * Parses argv against a command table.
56
+ *
57
+ * `--` ends flag parsing; everything after it is positional. A bare `-` is a
58
+ * positional (it means stdin to `credda investigate`), not a flag.
59
+ */
60
+ export function parseArgs(argv, commands) {
61
+ const globals = {
62
+ help: false,
63
+ version: false,
64
+ json: false,
65
+ quiet: false,
66
+ verbose: false,
67
+ 'no-color': false,
68
+ };
69
+ const flags = new Map();
70
+ const positionals = [];
71
+ let command = null;
72
+ let spec = null;
73
+ let endOfFlags = false;
74
+ for (let i = 0; i < argv.length; i += 1) {
75
+ const token = argv[i];
76
+ if (token === undefined)
77
+ continue;
78
+ if (endOfFlags) {
79
+ positionals.push(token);
80
+ continue;
81
+ }
82
+ if (token === '--') {
83
+ endOfFlags = true;
84
+ continue;
85
+ }
86
+ if (token.startsWith('-') && token !== '-') {
87
+ const { name, inlineValue } = splitFlag(token);
88
+ const resolved = resolveFlag(name, spec, command);
89
+ if (resolved.kind === 'boolean') {
90
+ if (inlineValue !== null) {
91
+ throw new UsageError(`The flag --${resolved.name} does not take a value.`, command);
92
+ }
93
+ if (resolved.global)
94
+ globals[resolved.name] = true;
95
+ else
96
+ flags.set(resolved.name, true);
97
+ continue;
98
+ }
99
+ let raw = inlineValue;
100
+ if (raw === null) {
101
+ const next = argv[i + 1];
102
+ // A negative number is a value, not a flag: `--since -2` must reach the
103
+ // command's own validation and produce its message, not "unknown flag".
104
+ const looksLikeFlag = next !== undefined &&
105
+ next.startsWith('-') &&
106
+ next !== '-' &&
107
+ !(resolved.kind === 'number' && /^-\d/.test(next));
108
+ if (next === undefined || looksLikeFlag) {
109
+ const placeholder = resolved.spec.valueName ?? `<${resolved.kind}>`;
110
+ throw new UsageError(`The flag --${resolved.name} needs a value: --${resolved.name} ${placeholder}`, command);
111
+ }
112
+ raw = next;
113
+ i += 1;
114
+ }
115
+ flags.set(resolved.name, coerce(resolved.name, raw, resolved.spec, command));
116
+ continue;
117
+ }
118
+ if (command === null && positionals.length === 0) {
119
+ const found = own(commands, token);
120
+ if (found !== undefined) {
121
+ command = token;
122
+ spec = found;
123
+ continue;
124
+ }
125
+ // An unknown first word is an unknown command, not a positional.
126
+ throw new UsageError(`Unknown command '${token}'. Run 'credda --help' for the list of commands.`, null);
127
+ }
128
+ positionals.push(token);
129
+ }
130
+ return {
131
+ command,
132
+ globals: {
133
+ help: globals['help'] === true,
134
+ version: globals['version'] === true,
135
+ json: globals['json'] === true,
136
+ quiet: globals['quiet'] === true,
137
+ verbose: globals['verbose'] === true,
138
+ color: globals['no-color'] !== true,
139
+ },
140
+ flags,
141
+ positionals,
142
+ };
143
+ }
144
+ function splitFlag(token) {
145
+ const body = token.startsWith('--') ? token.slice(2) : token.slice(1);
146
+ const eq = body.indexOf('=');
147
+ if (eq === -1)
148
+ return { name: body, inlineValue: null };
149
+ return { name: body.slice(0, eq), inlineValue: body.slice(eq + 1) };
150
+ }
151
+ function resolveFlag(name, spec, command) {
152
+ const commandFlags = spec?.flags ?? {};
153
+ const direct = own(commandFlags, name);
154
+ if (direct !== undefined)
155
+ return { name, kind: direct.kind, spec: direct, global: false };
156
+ const globalSpec = own(GLOBAL_FLAGS, name);
157
+ if (globalSpec !== undefined)
158
+ return { name, kind: globalSpec.kind, spec: globalSpec, global: true };
159
+ for (const [key, candidate] of Object.entries(commandFlags)) {
160
+ if (candidate.alias === name)
161
+ return { name: key, kind: candidate.kind, spec: candidate, global: false };
162
+ }
163
+ for (const [key, candidate] of Object.entries(GLOBAL_FLAGS)) {
164
+ if (candidate.alias === name)
165
+ return { name: key, kind: candidate.kind, spec: candidate, global: true };
166
+ }
167
+ const known = [...Object.keys(commandFlags), ...Object.keys(GLOBAL_FLAGS)];
168
+ const suggestion = closest(name, known);
169
+ const where = command === null ? '' : ` for 'credda ${command}'`;
170
+ throw new UsageError(`Unknown flag '--${name}'${where}.${suggestion === null ? '' : ` Did you mean '--${suggestion}'?`}`, command);
171
+ }
172
+ function coerce(name, raw, spec, command) {
173
+ if (spec.kind === 'number') {
174
+ const value = Number(raw);
175
+ if (!Number.isFinite(value)) {
176
+ throw new UsageError(`The flag --${name} needs a number, but got '${raw}'.`, command);
177
+ }
178
+ return value;
179
+ }
180
+ if (spec.choices !== undefined && !spec.choices.includes(raw)) {
181
+ throw new UsageError(`Invalid value '${raw}' for --${name}. Expected one of: ${spec.choices.join(', ')}.`, command);
182
+ }
183
+ return raw;
184
+ }
185
+ /** Levenshtein-based suggestion, only offered when the edit distance is small. */
186
+ function closest(input, candidates) {
187
+ let best = null;
188
+ let bestDistance = Number.POSITIVE_INFINITY;
189
+ for (const candidate of candidates) {
190
+ const distance = editDistance(input, candidate);
191
+ if (distance < bestDistance) {
192
+ bestDistance = distance;
193
+ best = candidate;
194
+ }
195
+ }
196
+ return bestDistance <= Math.max(1, Math.floor(input.length / 3)) ? best : null;
197
+ }
198
+ function editDistance(a, b) {
199
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
200
+ for (let i = 1; i <= a.length; i += 1) {
201
+ const current = [i, ...new Array(b.length).fill(0)];
202
+ for (let j = 1; j <= b.length; j += 1) {
203
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
204
+ current[j] = Math.min((current[j - 1] ?? 0) + 1, (previous[j] ?? 0) + 1, (previous[j - 1] ?? 0) + cost);
205
+ }
206
+ previous = current;
207
+ }
208
+ return previous[b.length] ?? 0;
209
+ }
210
+ /** Reads a string flag, or null when absent. */
211
+ export function stringFlag(parsed, name) {
212
+ const value = parsed.flags.get(name);
213
+ return typeof value === 'string' ? value : null;
214
+ }
215
+ /** Reads a number flag, or null when absent. */
216
+ export function numberFlag(parsed, name) {
217
+ const value = parsed.flags.get(name);
218
+ return typeof value === 'number' ? value : null;
219
+ }
220
+ export function boolFlag(parsed, name) {
221
+ return parsed.flags.get(name) === true;
222
+ }
@@ -0,0 +1,153 @@
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 { type CommandSpec } 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 declare const EXIT: {
65
+ /** Success, including NO_CHANGE_REQUIRED and INCONCLUSIVE. */
66
+ readonly SUCCESS: 0;
67
+ /** Credda itself failed: internal error, unreadable database, crash. */
68
+ readonly INTERNAL_ERROR: 1;
69
+ /** The command line or its inputs were wrong. Nothing was run. */
70
+ readonly USAGE_ERROR: 2;
71
+ /** A change was produced and independent verification rejected it. */
72
+ readonly PATCH_REJECTED: 3;
73
+ /** The run was cancelled (Ctrl-C). */
74
+ readonly 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
+ readonly 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
+ readonly 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
+ readonly 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 declare const RESERVED_EXIT_CODES: Readonly<Record<number, string>>;
143
+ export declare const EXIT_CODE_HELP: readonly string[];
144
+ export declare const COMMANDS: Readonly<Record<string, CommandSpec>>;
145
+ /**
146
+ * The command an alias dispatches to. Unknown names are returned unchanged so
147
+ * the caller's own "unknown command" path still owns that message.
148
+ */
149
+ export declare function canonicalCommand(name: string): string;
150
+ /** Alias name to the command it stands for, for the root usage. */
151
+ export declare function aliases(): readonly (readonly [string, string])[];
152
+ export declare function rootUsage(): string;
153
+ export declare function commandUsage(name: string): string;