@greatwait/cli 0.1.0-alpha.3 → 0.1.0-alpha.4

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 CHANGED
@@ -22,7 +22,7 @@ export interface ParsedArguments {
22
22
  * both spellings — attached and separated — deserve the same answer. Neither
23
23
  * repeats the value back.
24
24
  */
25
- export declare const SECRET_ARGUMENT_ADVICE = "A credential given as an argument is visible to every process on this machine and is written to your shell history \u2014 treat the one you just typed as exposed and replace it. Run the command with a bare --api-key and paste the secret when asked, or pipe it in: printf %s \"$KEY\" | greatwait login --api-key";
25
+ export declare function secretArgumentAdvice(programName: string): string;
26
26
  export declare function parseArguments(argv: readonly string[],
27
27
  /**
28
28
  * The words that begin a command, so the rule below can tell a subcommand
@@ -35,7 +35,7 @@ export declare function parseArguments(argv: readonly string[],
35
35
  * than imported, because `commands.ts` reads this module and the cycle would
36
36
  * be the wrong way round.
37
37
  */
38
- commandWords?: ReadonlySet<string>): ParsedArguments;
38
+ commandWords?: ReadonlySet<string>, programName?: string): ParsedArguments;
39
39
  /**
40
40
  * A duration written the way a person writes one: `30s`, `5m`, `2h`.
41
41
  *
package/dist/args.js CHANGED
@@ -1,16 +1,18 @@
1
+ import { CANONICAL_PROGRAM_NAME, COMMAND_CATALOG } from "./catalog.js";
1
2
  import { CliError } from "./runtime.js";
2
- /** Options that take a value. Anything else is a boolean flag. */
3
- const VALUED_OPTIONS = new Set([
4
- "--api-url",
5
- "--at",
6
- "--data",
7
- "--endpoint",
8
- "--idempotency-key",
9
- "--in",
10
- "--scope",
11
- "--timeout",
12
- "--url",
13
- ]);
3
+ /**
4
+ * Options that take a value. Anything else is a boolean flag.
5
+ *
6
+ * **Derived from the catalog rather than listed here.** A second list was the
7
+ * first design and it is a drift a test does not notice: an option documented
8
+ * as `--limit <count>` that this set does not know is parsed as a flag, so the
9
+ * count becomes a positional argument and the command refuses it as unexpected
10
+ * — while the reference page says it is supported. One declaration, read by
11
+ * the parser and by the page, cannot disagree with itself.
12
+ */
13
+ const VALUED_OPTIONS = new Set(COMMAND_CATALOG.flatMap((command) => command.options
14
+ .filter((option) => option.value !== undefined)
15
+ .map((option) => option.flag)));
14
16
  /**
15
17
  * Options that must never take a value, whatever they look like.
16
18
  *
@@ -26,7 +28,9 @@ const SECRET_BEARING_FLAGS = new Set(["--api-key"]);
26
28
  * both spellings — attached and separated — deserve the same answer. Neither
27
29
  * repeats the value back.
28
30
  */
29
- export const SECRET_ARGUMENT_ADVICE = 'A credential given as an argument is visible to every process on this machine and is written to your shell history — treat the one you just typed as exposed and replace it. Run the command with a bare --api-key and paste the secret when asked, or pipe it in: printf %s "$KEY" | greatwait login --api-key';
31
+ export function secretArgumentAdvice(programName) {
32
+ return `A credential given as an argument is visible to every process on this machine and is written to your shell history — treat the one you just typed as exposed and replace it. Run the command with a bare --api-key and paste the secret when asked, or pipe it in: printf %s "$KEY" | ${programName} login --api-key`;
33
+ }
30
34
  export function parseArguments(argv,
31
35
  /**
32
36
  * The words that begin a command, so the rule below can tell a subcommand
@@ -39,7 +43,8 @@ export function parseArguments(argv,
39
43
  * than imported, because `commands.ts` reads this module and the cycle would
40
44
  * be the wrong way round.
41
45
  */
42
- commandWords = new Set()) {
46
+ commandWords = new Set(), programName = CANONICAL_PROGRAM_NAME) {
47
+ const secretAdvice = secretArgumentAdvice(programName);
43
48
  const command = [];
44
49
  const options = {};
45
50
  const booleans = new Set();
@@ -55,7 +60,7 @@ commandWords = new Set()) {
55
60
  const name = equals === -1 ? token : token.slice(0, equals);
56
61
  if (SECRET_BEARING_FLAGS.has(name)) {
57
62
  if (equals !== -1) {
58
- throw new CliError(`${name} does not take a value on the command line`, "usage", SECRET_ARGUMENT_ADVICE);
63
+ throw new CliError(`${name} does not take a value on the command line`, "usage", secretAdvice);
59
64
  }
60
65
  // **The next token is refused here, rather than left to a command.**
61
66
  // Leaving it to become a positional argument was the first design, and a
@@ -72,13 +77,25 @@ commandWords = new Set()) {
72
77
  if (following !== undefined &&
73
78
  !following.startsWith("--") &&
74
79
  !commandWords.has(following)) {
75
- throw new CliError(`${name} does not take a value on the command line`, "usage", SECRET_ARGUMENT_ADVICE);
80
+ throw new CliError(`${name} does not take a value on the command line`, "usage", secretAdvice);
76
81
  }
77
82
  booleans.add(name);
78
83
  index += 1;
79
84
  continue;
80
85
  }
81
86
  if (VALUED_OPTIONS.has(name)) {
87
+ // **A second occurrence is refused rather than overwriting the first.**
88
+ // Two reviewers found the same defect through it: `--scope` documented
89
+ // itself as repeatable, the record this parses into is one value per
90
+ // name, and `--scope schedules:read --scope schedules:write` therefore
91
+ // asked for the second and silently dropped the first — surfacing much
92
+ // later as a `scope_missing` with nothing to connect it back. The same
93
+ // shape is worse for `--data` and `--limit`, where the dropped value is
94
+ // the one somebody meant. Refusing costs a retype; overwriting costs a
95
+ // debugging session, and always the wrong one.
96
+ if (Object.hasOwn(options, name)) {
97
+ throw new CliError(`${name} was given twice`, "usage", "Give it once. An option that takes a list takes the values in one argument, separated by spaces.");
98
+ }
82
99
  if (equals !== -1) {
83
100
  options[name] = token.slice(equals + 1);
84
101
  index += 1;
@@ -0,0 +1,196 @@
1
+ import { type OperationDefinition } from "@greatwait/contracts";
2
+ import { EXIT_CLASSES, EXIT_CLASS_MEANINGS, type ExitClass } from "./exit.js";
3
+ /**
4
+ * What the command line covers, declared once and checked against the registry.
5
+ *
6
+ * **This module is metadata and rules, and it imports no client.** The
7
+ * reference page is generated from it by a script that must not drag the
8
+ * generated API client into the contract generator's own dependency graph —
9
+ * the generator writes that client — so the declarations live apart from the
10
+ * implementations that use them. `commands.ts` binds each declaration to a
11
+ * function; `coverageProblems` is what refuses a declaration that no longer
12
+ * matches the registry.
13
+ *
14
+ * The story's criterion 2 asks that an added, removed, renamed, scope-changed,
15
+ * approval-changed, or unavailable operation cannot leave the CLI and its
16
+ * reference silently stale. That is this file: every operation is either
17
+ * covered by a command or omitted on a stated ground, the grounds that can be
18
+ * derived from the registry are checked against it rather than believed, and
19
+ * the scopes `login` asks for are the union of what the covered operations
20
+ * need rather than a list somebody maintains beside them.
21
+ */
22
+ /** A positional argument, in the order the command reads them. */
23
+ export interface CommandPositional {
24
+ /** As it appears in the reference: `<schedule>`. */
25
+ readonly name: string;
26
+ readonly summary: string;
27
+ readonly optional?: boolean;
28
+ }
29
+ /** An option. `value` absent means it is a flag and takes none. */
30
+ export interface CommandOption {
31
+ readonly flag: string;
32
+ readonly summary: string;
33
+ readonly value?: string;
34
+ }
35
+ export interface CommandDeclaration {
36
+ /** The words that select it, one or two. The dispatcher tries two first. */
37
+ readonly words: string;
38
+ /** The heading it appears under in help and in the reference. */
39
+ readonly group: string;
40
+ readonly summary: string;
41
+ readonly positionals: readonly CommandPositional[];
42
+ readonly options: readonly CommandOption[];
43
+ /** Registry operations this command reaches. Empty for a purely local one. */
44
+ readonly operations: readonly string[];
45
+ /**
46
+ * Previews its exact scope and asks before acting.
47
+ *
48
+ * Required wherever `confirmationGround` says so and permitted anywhere else:
49
+ * a command may be more careful than the registry demands, and one of them
50
+ * is — rotating an endpoint signing key mints a secret shown once, which the
51
+ * registry describes but has no hint for.
52
+ */
53
+ readonly confirms?: boolean;
54
+ }
55
+ /**
56
+ * Why an operation has no command.
57
+ *
58
+ * Two of the three are properties of the registry entry, so the rule below
59
+ * checks them rather than taking the claim at face value — a ground that
60
+ * cannot be wrong is a ground nobody has to re-read when the entry changes.
61
+ * The third carries prose because no property of the entry states it.
62
+ */
63
+ export type OmissionGround =
64
+ /** No principal this command line can hold may call it. */
65
+ "principal-unreachable"
66
+ /** Not served by the API at all. */
67
+ | "not-on-the-api"
68
+ /** A product decision, stated in `because`. */
69
+ | "declared";
70
+ export interface OperationOmission {
71
+ readonly operation: string;
72
+ readonly ground: OmissionGround;
73
+ readonly because: string;
74
+ }
75
+ /**
76
+ * The name every written instruction uses.
77
+ *
78
+ * The package installs two executables — `greatwait` and `gw` — and each states
79
+ * its own name, so what a person is told to type next is what they just typed.
80
+ * The *published* reference, the quickstarts and the first-value journey use
81
+ * this one, because a written instruction has to be followable by somebody who
82
+ * installed either.
83
+ */
84
+ export declare const CANONICAL_PROGRAM_NAME = "greatwait";
85
+ /** The principals a command line can present. Never a browser session. */
86
+ export declare const CLI_PRINCIPALS: readonly ["api-credential", "oauth"];
87
+ /** Whether any principal this command line can hold may call it. */
88
+ export declare function reachableByCli(operation: OperationDefinition): boolean;
89
+ /** Whether the API serves it at all, as opposed to the contract mock alone. */
90
+ export declare function servedByApi(operation: OperationDefinition): boolean;
91
+ /**
92
+ * Why a command covering this operation must confirm before acting, if it must.
93
+ *
94
+ * **Derived from the registry's own hints rather than from a list here**, so an
95
+ * operation that becomes destructive tomorrow makes a command that does not ask
96
+ * fail this gate rather than quietly keep shipping.
97
+ *
98
+ * Two grounds, and the second is the one that took a correction. Every
99
+ * externally visible call is *not* a confirmation: `schedules create` reaches a
100
+ * receiver by design and is the first thing anybody runs, so demanding a
101
+ * keystroke there would put a prompt in the middle of the published first-value
102
+ * journey. What separates it from `occurrences redeliver` — which does deserve
103
+ * one — is that redelivery acts on something that already exists and sends it
104
+ * again. So the ground is an outward-facing change to an *addressed* object,
105
+ * which is exactly an operation with a path parameter.
106
+ */
107
+ export declare function confirmationGround(operation: OperationDefinition): "destructive" | "outward-facing-change" | undefined;
108
+ /**
109
+ * The request headers the registry declares for an operation.
110
+ *
111
+ * Read off the schema's own shape rather than listed here, because the point of
112
+ * the rules below is that a header appearing or disappearing changes what the
113
+ * command line must offer. `endpoints.disable` is the case that makes it worth
114
+ * checking: every other endpoint write accepts `If-Match` and that one does
115
+ * not, so a command offering the flag there would document a guard the service
116
+ * ignores — which reads to somebody operating carefully as protection they do
117
+ * not have.
118
+ */
119
+ export declare function declaredRequestHeaders(operation: OperationDefinition): readonly string[];
120
+ /**
121
+ * Every command, in the order the reference and `--help` present them.
122
+ *
123
+ * Ordered by what somebody does rather than alphabetically: sign in, look
124
+ * around, set up a destination, schedule something, watch it, then recover from
125
+ * it. The groups are the headings.
126
+ */
127
+ export declare const COMMAND_CATALOG: readonly CommandDeclaration[];
128
+ /**
129
+ * Every operation with no command, and why.
130
+ *
131
+ * The first six are the same fact stated six times: they need a browser
132
+ * session, and a command line holds an API credential or an OAuth grant. That
133
+ * is a property of the registry entry, so the rule checks it rather than
134
+ * believing this list — and it is also why the story says credential and grant
135
+ * operations appear "only where the current principal rules can support them
136
+ * honestly". Creating a credential from a credential is the escalation the
137
+ * control plane refuses on purpose.
138
+ */
139
+ export declare const OPERATION_OMISSIONS: readonly OperationOmission[];
140
+ /**
141
+ * The one-line spelling of a command, used by `--help` and by the reference.
142
+ *
143
+ * **Rendered from the declaration in both places rather than written twice.**
144
+ * The story asks that the reference be generated or exhaustively checked
145
+ * against the executable; sharing the renderer is the version of that with
146
+ * nothing to check, because a page and a help screen produced by the same
147
+ * function cannot disagree about what a command takes.
148
+ */
149
+ export declare function usageLine(command: CommandDeclaration, programName?: string): string;
150
+ /** The help for one command: what it does, what it takes, what each means. */
151
+ export declare function commandHelp(command: CommandDeclaration, programName?: string): string;
152
+ /** Every command, grouped, for the overview `--help` prints. */
153
+ export declare function overview(commands?: readonly CommandDeclaration[]): string;
154
+ export interface CoverageInputs {
155
+ readonly commands: readonly CommandDeclaration[];
156
+ readonly omissions: readonly OperationOmission[];
157
+ readonly operations: readonly OperationDefinition[];
158
+ }
159
+ /**
160
+ * The scopes `login` asks a person to grant.
161
+ *
162
+ * **Derived from what the commands actually call.** A list maintained beside
163
+ * the catalog is a list that goes stale in the direction nobody notices: a
164
+ * command added for an operation needing a scope nobody requested fails at
165
+ * runtime with `scope_missing`, against a grant the person cannot widen without
166
+ * signing in again. Deriving it also means a widening shows up in the generated
167
+ * reference's diff, which is where a reviewer will see it.
168
+ */
169
+ export declare function requestedScopes(inputs: CoverageInputs): readonly string[];
170
+ /**
171
+ * Everything wrong with the declarations, as sentences.
172
+ *
173
+ * A pure function of its inputs so the cases below can hand it a catalog that
174
+ * violates one rule at a time. Empty means the CLI, its reference and the
175
+ * registry agree.
176
+ */
177
+ export declare function coverageProblems(inputs: CoverageInputs): string[];
178
+ /** The declarations as they stand, against the real registry. */
179
+ export declare function catalogInputs(): CoverageInputs;
180
+ /**
181
+ * The scopes `login` asks for, from what the commands call.
182
+ *
183
+ * Asking for everything the server offers would be asking a person to grant a
184
+ * command line permissions it has no code to use; asking for less than the
185
+ * commands need is a `scope_missing` on a grant they cannot widen without
186
+ * signing in again. Neither is a list anybody maintains: this is that union.
187
+ */
188
+ export declare const REQUESTED_SCOPES: readonly string[];
189
+ /**
190
+ * Re-exported so the reference generator has one import.
191
+ *
192
+ * The page publishes the exit numbers and what they mean, and both come from
193
+ * the module the runtime reads. A generator reaching into `./exit` separately
194
+ * would work; this keeps the catalog the single thing a projection depends on.
195
+ */
196
+ export { EXIT_CLASSES, EXIT_CLASS_MEANINGS, type ExitClass };