@vincentt-xr/cli 0.14.1 → 0.15.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/cli/index.js +40 -0
- package/dist/cli/usage-text.js +5 -1
- package/dist/cli/verb-surface.d.ts +2 -1
- package/dist/cli/verb-surface.js +5 -0
- package/dist/dx/report.d.ts +102 -0
- package/dist/dx/report.js +161 -0
- package/dist/dx/reportIo.d.ts +16 -0
- package/dist/dx/reportIo.js +29 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1033,6 +1033,44 @@ async function dx() {
|
|
|
1033
1033
|
accountConsent: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now),
|
|
1034
1034
|
}, options);
|
|
1035
1035
|
}
|
|
1036
|
+
// ---- report -----------------------------------------------------------------
|
|
1037
|
+
/**
|
|
1038
|
+
* `vincentt report "<what went wrong>"`.
|
|
1039
|
+
*
|
|
1040
|
+
* Shares `dx --send`'s consent arms, auth and disclosure screen deliberately —
|
|
1041
|
+
* it is the same act of transmitting, differing only in what is carried. What it
|
|
1042
|
+
* does NOT share is the friction precondition: this path never consults the
|
|
1043
|
+
* journal for clusters, because the report it sends is about something the
|
|
1044
|
+
* journal could not detect.
|
|
1045
|
+
*/
|
|
1046
|
+
async function report() {
|
|
1047
|
+
const { parseReportOptions, reportCommand } = await import("../dx/report.js");
|
|
1048
|
+
const { postObservationReport } = await import("../dx/reportIo.js");
|
|
1049
|
+
const now = Date.now();
|
|
1050
|
+
const interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1051
|
+
const enablement = await loadEnablement(now);
|
|
1052
|
+
process.exitCode = await reportCommand({
|
|
1053
|
+
print: (l) => console.log(l),
|
|
1054
|
+
now: () => now,
|
|
1055
|
+
cliVersion,
|
|
1056
|
+
// Best-effort: the context line is a courtesy, and a journal that is off or
|
|
1057
|
+
// unreadable must never block someone from sending what they typed.
|
|
1058
|
+
entries: () => {
|
|
1059
|
+
try {
|
|
1060
|
+
return readJournal();
|
|
1061
|
+
}
|
|
1062
|
+
catch {
|
|
1063
|
+
return [];
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
auth: dxAuth,
|
|
1067
|
+
post: (apiUrl, pat, r) => postObservationReport(apiUrl, pat, r),
|
|
1068
|
+
confirm: (q) => stdinConfirm()(`${q} [y/N] `, false),
|
|
1069
|
+
interactive,
|
|
1070
|
+
enablement: () => enablement,
|
|
1071
|
+
accountConsent: (auth) => httpConsentBackend().read(auth.apiUrl, auth.pat, now),
|
|
1072
|
+
}, parseReportOptions(argv.slice(1)));
|
|
1073
|
+
}
|
|
1036
1074
|
// ---- assets (f5) ------------------------------------------------------------
|
|
1037
1075
|
/**
|
|
1038
1076
|
* `vincentt assets <search|show|add>` — the CLI's FIRST nested subcommand group,
|
|
@@ -1192,6 +1230,8 @@ async function main() {
|
|
|
1192
1230
|
return assets();
|
|
1193
1231
|
case "dx":
|
|
1194
1232
|
return dx();
|
|
1233
|
+
case "report":
|
|
1234
|
+
return report();
|
|
1195
1235
|
}
|
|
1196
1236
|
}
|
|
1197
1237
|
// Every invocation is journaled here, at the one site that sees all of them —
|
package/dist/cli/usage-text.js
CHANGED
|
@@ -100,7 +100,11 @@ export const USAGE_TEXT = `Vincentt CLI — local-first AR dev loop\n\n` +
|
|
|
100
100
|
` Asks you to confirm in your terminal. Stays on until you\n` +
|
|
101
101
|
` disable it, and is shown on \`vincentt dx\` and\n` +
|
|
102
102
|
` \`vincentt version\` while on.\n` +
|
|
103
|
-
` dx disable Turn that off again.\n
|
|
103
|
+
` dx disable Turn that off again.\n` +
|
|
104
|
+
` report "<text>" Tell us something went wrong, in your own words.\n` +
|
|
105
|
+
` For when nothing failed but something looked wrong —\n` +
|
|
106
|
+
` the case \`dx --send\` cannot see. Shows the exact\n` +
|
|
107
|
+
` payload and asks first. [--no-context] [--yes]\n\n` +
|
|
104
108
|
`Common options: --dir <path> (where to work, default cwd — for \`create\` this is\n` +
|
|
105
109
|
` the PARENT of the new folder, not the folder itself)\n` +
|
|
106
110
|
` --allow-custom-api honor a binding's apiUrl override\n`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PINNED_VERBS: readonly ["version", "login", "logout", "whoami", "init", "create", "link", "unlink", "publish", "preview", "logs", "network", "trace", "feedback", "relay", "assets", "dx", "projects"];
|
|
1
|
+
export declare const PINNED_VERBS: readonly ["version", "login", "logout", "whoami", "init", "create", "link", "unlink", "publish", "preview", "logs", "network", "trace", "feedback", "relay", "assets", "dx", "projects", "report"];
|
|
2
2
|
export type PinnedVerb = (typeof PINNED_VERBS)[number];
|
|
3
3
|
export declare const VERB_HANDLERS: {
|
|
4
4
|
readonly version: true;
|
|
@@ -19,6 +19,7 @@ export declare const VERB_HANDLERS: {
|
|
|
19
19
|
readonly assets: true;
|
|
20
20
|
readonly dx: true;
|
|
21
21
|
readonly projects: true;
|
|
22
|
+
readonly report: true;
|
|
22
23
|
};
|
|
23
24
|
export type DispatchVerb = keyof typeof VERB_HANDLERS;
|
|
24
25
|
export declare function resolveVerb(cmd: string | undefined): DispatchVerb | undefined;
|
package/dist/cli/verb-surface.js
CHANGED
|
@@ -12,6 +12,10 @@ export const PINNED_VERBS = [
|
|
|
12
12
|
// Appended: a read-only listing. Nothing above was renamed or removed, which
|
|
13
13
|
// is the whole of what B-F3-8 pins — an addition is always permitted.
|
|
14
14
|
"projects",
|
|
15
|
+
// Appended: the hand-written report. `dx --send` transmits DETECTED friction
|
|
16
|
+
// and refuses when it finds none; this carries what a creator TYPED, which is
|
|
17
|
+
// the half a journal keyed on exit codes cannot see. Also an addition.
|
|
18
|
+
"report",
|
|
15
19
|
];
|
|
16
20
|
// The dispatch's verb set, as data. `main()` switches over this, so the two can
|
|
17
21
|
// never disagree: a verb added to the CLI is a key added here, and QA-AHG-01
|
|
@@ -35,6 +39,7 @@ export const VERB_HANDLERS = {
|
|
|
35
39
|
assets: true,
|
|
36
40
|
dx: true,
|
|
37
41
|
projects: true,
|
|
42
|
+
report: true,
|
|
38
43
|
};
|
|
39
44
|
// Answers "is this a verb this CLI accepts" without running it. The
|
|
40
45
|
// console-redirect strings are NOT accepted here: they reach their explanation
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vincentt report "<what went wrong>"` — the creator's own words, sent.
|
|
3
|
+
*
|
|
4
|
+
* This exists because of a gap `dx --send` could not close. That command reports
|
|
5
|
+
* DETECTED friction: it reads the journal, finds clusters of retries and errors,
|
|
6
|
+
* and refuses when it finds none. But the friction a creator most wants to report
|
|
7
|
+
* is routinely the kind the journal cannot see — a command that exits 0 and puts
|
|
8
|
+
* the wrong thing on screen produces no error, no retry and no cluster. Asked to
|
|
9
|
+
* carry a note about one, `--send` had nothing to attach it to and said "no
|
|
10
|
+
* friction detected — nothing to send", which is the tool contradicting the
|
|
11
|
+
* person using it.
|
|
12
|
+
*
|
|
13
|
+
* So the note stops being a rider on a finding and becomes the payload. An
|
|
14
|
+
* observation carries no counts because there is nothing to count.
|
|
15
|
+
*
|
|
16
|
+
* The consent model is INHERITED, not relaxed. Sending is still gated on the
|
|
17
|
+
* same three arms as `--send` (a terminal, a local `dx enable`, or an account
|
|
18
|
+
* grant), the payload is still shown in full before it goes, and a note is still
|
|
19
|
+
* shown under a warning that it is transmitted exactly as written. This verb
|
|
20
|
+
* removes a refusal; it does not open a new door.
|
|
21
|
+
*/
|
|
22
|
+
import type { Enablement } from "./enablement.js";
|
|
23
|
+
import type { JournalEntry } from "./journal.js";
|
|
24
|
+
/** An observation as it goes on the wire. Mirrors the API's dxObservationRequest. */
|
|
25
|
+
export interface WireObservation {
|
|
26
|
+
note: string;
|
|
27
|
+
context?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface WireObservationReport {
|
|
30
|
+
cliVersion: string;
|
|
31
|
+
platform: string;
|
|
32
|
+
findings: never[];
|
|
33
|
+
observations: WireObservation[];
|
|
34
|
+
}
|
|
35
|
+
/** Matches the server's MaxDXNote. Checked here so a refusal costs no round trip. */
|
|
36
|
+
export declare const MAX_NOTE = 2000;
|
|
37
|
+
export interface ReportOptions {
|
|
38
|
+
note: string;
|
|
39
|
+
yes: boolean;
|
|
40
|
+
/** Suppress the context line — for a creator who would rather send the note alone. */
|
|
41
|
+
noContext: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse `vincentt report <note>`.
|
|
45
|
+
*
|
|
46
|
+
* The note is POSITIONAL and is read as the single argument after the verb,
|
|
47
|
+
* matching how `dx --note` already reads its own. A flag form would be worse
|
|
48
|
+
* here: the note routinely contains spaces and quotes, and the failure mode of a
|
|
49
|
+
* half-quoted flag value is a note that silently loses its tail.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseReportOptions(args: string[]): ReportOptions;
|
|
52
|
+
/**
|
|
53
|
+
* The skeleton of the last command this creator ran, for context.
|
|
54
|
+
*
|
|
55
|
+
* Read from the journal rather than reconstructed, so it carries the same
|
|
56
|
+
* value-elided shape the journal already enforces — this is the one field on an
|
|
57
|
+
* observation not typed by the creator, and it must not become the field that
|
|
58
|
+
* leaks a path. The `report` invocation itself is excluded: the interesting
|
|
59
|
+
* command is the one BEFORE this one.
|
|
60
|
+
*
|
|
61
|
+
* Absent is a normal answer. The journal may be off, empty, or hold nothing but
|
|
62
|
+
* this run, and none of those is a reason to refuse to send what someone typed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function lastContext(entries: JournalEntry[]): string | undefined;
|
|
65
|
+
export declare function buildObservationReport(note: string, cliVersion: string, context?: string): WireObservationReport;
|
|
66
|
+
/**
|
|
67
|
+
* Render the payload for the creator to read BEFORE it is sent.
|
|
68
|
+
*
|
|
69
|
+
* As in `send.ts`, this is the payload formatted, not a summary of it. The note
|
|
70
|
+
* warning is unconditional here, where it is conditional there: an observation
|
|
71
|
+
* is ALWAYS a note, so there is no path through this screen that does not need
|
|
72
|
+
* it.
|
|
73
|
+
*/
|
|
74
|
+
export declare function renderObservation(report: WireObservationReport): string[];
|
|
75
|
+
export interface ReportDeps {
|
|
76
|
+
print: (line: string) => void;
|
|
77
|
+
now: () => number;
|
|
78
|
+
cliVersion: () => string;
|
|
79
|
+
entries: () => JournalEntry[];
|
|
80
|
+
auth: () => Promise<{
|
|
81
|
+
apiUrl: string;
|
|
82
|
+
pat: string;
|
|
83
|
+
} | undefined>;
|
|
84
|
+
post: (apiUrl: string, pat: string, report: WireObservationReport) => Promise<{
|
|
85
|
+
ref: string;
|
|
86
|
+
expiresAt: string;
|
|
87
|
+
}>;
|
|
88
|
+
confirm: (question: string) => Promise<boolean>;
|
|
89
|
+
interactive: () => boolean;
|
|
90
|
+
enablement?: () => Enablement;
|
|
91
|
+
accountConsent?: (auth: {
|
|
92
|
+
apiUrl: string;
|
|
93
|
+
pat: string;
|
|
94
|
+
}) => Promise<Enablement>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `vincentt report`.
|
|
98
|
+
*
|
|
99
|
+
* Same order as `dx --send`, for the same reason: build the payload, SHOW it,
|
|
100
|
+
* ask, and only then send.
|
|
101
|
+
*/
|
|
102
|
+
export declare function reportCommand(deps: ReportDeps, options: ReportOptions): Promise<number>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { enablementLine } from "./enablement.js";
|
|
2
|
+
import { DXSendError, platformTag } from "./send.js";
|
|
3
|
+
/** Matches the server's MaxDXNote. Checked here so a refusal costs no round trip. */
|
|
4
|
+
export const MAX_NOTE = 2000;
|
|
5
|
+
/**
|
|
6
|
+
* Parse `vincentt report <note>`.
|
|
7
|
+
*
|
|
8
|
+
* The note is POSITIONAL and is read as the single argument after the verb,
|
|
9
|
+
* matching how `dx --note` already reads its own. A flag form would be worse
|
|
10
|
+
* here: the note routinely contains spaces and quotes, and the failure mode of a
|
|
11
|
+
* half-quoted flag value is a note that silently loses its tail.
|
|
12
|
+
*/
|
|
13
|
+
export function parseReportOptions(args) {
|
|
14
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
15
|
+
return {
|
|
16
|
+
note: positionals[0] ?? "",
|
|
17
|
+
yes: args.includes("--yes"),
|
|
18
|
+
noContext: args.includes("--no-context"),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The skeleton of the last command this creator ran, for context.
|
|
23
|
+
*
|
|
24
|
+
* Read from the journal rather than reconstructed, so it carries the same
|
|
25
|
+
* value-elided shape the journal already enforces — this is the one field on an
|
|
26
|
+
* observation not typed by the creator, and it must not become the field that
|
|
27
|
+
* leaks a path. The `report` invocation itself is excluded: the interesting
|
|
28
|
+
* command is the one BEFORE this one.
|
|
29
|
+
*
|
|
30
|
+
* Absent is a normal answer. The journal may be off, empty, or hold nothing but
|
|
31
|
+
* this run, and none of those is a reason to refuse to send what someone typed.
|
|
32
|
+
*/
|
|
33
|
+
export function lastContext(entries) {
|
|
34
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
35
|
+
const e = entries[i];
|
|
36
|
+
if (e.verb === "report")
|
|
37
|
+
continue;
|
|
38
|
+
return e.skel !== "" ? e.skel : undefined;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
export function buildObservationReport(note, cliVersion, context) {
|
|
43
|
+
const o = { note: note.trim() };
|
|
44
|
+
if (context !== undefined && context !== "")
|
|
45
|
+
o.context = context;
|
|
46
|
+
return {
|
|
47
|
+
cliVersion,
|
|
48
|
+
platform: platformTag(),
|
|
49
|
+
// Always present and always empty: the server accepts a report with either
|
|
50
|
+
// carrier, and sending the key explicitly says "no findings" rather than
|
|
51
|
+
// leaving the shape to be inferred from an absent field.
|
|
52
|
+
findings: [],
|
|
53
|
+
observations: [o],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Render the payload for the creator to read BEFORE it is sent.
|
|
58
|
+
*
|
|
59
|
+
* As in `send.ts`, this is the payload formatted, not a summary of it. The note
|
|
60
|
+
* warning is unconditional here, where it is conditional there: an observation
|
|
61
|
+
* is ALWAYS a note, so there is no path through this screen that does not need
|
|
62
|
+
* it.
|
|
63
|
+
*/
|
|
64
|
+
export function renderObservation(report) {
|
|
65
|
+
return [
|
|
66
|
+
"This is what would be sent:",
|
|
67
|
+
"",
|
|
68
|
+
...JSON.stringify(report, null, 2)
|
|
69
|
+
.split("\n")
|
|
70
|
+
.map((l) => ` ${l}`),
|
|
71
|
+
"",
|
|
72
|
+
" YOUR NOTE IS SENT EXACTLY AS WRITTEN — check it for paths, URLs,",
|
|
73
|
+
" tokens, or customer names before sending.",
|
|
74
|
+
" The context line is the shape of your last command, with values removed.",
|
|
75
|
+
" Your journal stays on this machine.",
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* `vincentt report`.
|
|
80
|
+
*
|
|
81
|
+
* Same order as `dx --send`, for the same reason: build the payload, SHOW it,
|
|
82
|
+
* ask, and only then send.
|
|
83
|
+
*/
|
|
84
|
+
export async function reportCommand(deps, options) {
|
|
85
|
+
const note = options.note.trim();
|
|
86
|
+
if (note === "") {
|
|
87
|
+
deps.print('Usage: vincentt report "what went wrong"');
|
|
88
|
+
deps.print("");
|
|
89
|
+
deps.print(" Sends one note about the tooling to Vincentt. Use it when");
|
|
90
|
+
deps.print(" something looked wrong but no command failed.");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
if ([...note].length > MAX_NOTE) {
|
|
94
|
+
// Refused here rather than truncated, matching every other bound in this
|
|
95
|
+
// feature: silently sending half of what someone wrote makes the report a
|
|
96
|
+
// worse record than not having it.
|
|
97
|
+
deps.print(`That note is longer than ${MAX_NOTE} characters. Please shorten it.`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
// The gate, inherited from `dx --send` verbatim. An agent composing a note and
|
|
101
|
+
// confirming it with --yes on a machine nobody granted is the case this stops.
|
|
102
|
+
const localEnablement = deps.enablement?.() ?? { enabled: false };
|
|
103
|
+
const interactive = deps.interactive();
|
|
104
|
+
const auth = await deps.auth();
|
|
105
|
+
let accountEnablement = { enabled: false };
|
|
106
|
+
if (!interactive && !localEnablement.enabled && auth !== undefined) {
|
|
107
|
+
try {
|
|
108
|
+
accountEnablement = (await deps.accountConsent?.(auth)) ?? { enabled: false };
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Fail closed. An unreachable API never authorizes a send.
|
|
112
|
+
accountEnablement = { enabled: false };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const enablement = localEnablement.enabled
|
|
116
|
+
? localEnablement
|
|
117
|
+
: accountEnablement;
|
|
118
|
+
if (!interactive && !enablement.enabled) {
|
|
119
|
+
deps.print("Sending a report needs consent that has not been given on this account.");
|
|
120
|
+
deps.print("`vincentt dx enable` opens the Vincentt console so you can allow it.");
|
|
121
|
+
deps.print("It takes one click, and it works from here.");
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
if (auth === undefined) {
|
|
125
|
+
deps.print("Not signed in. Run `vincentt login` to send a report.");
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
const context = options.noContext ? undefined : lastContext(deps.entries());
|
|
129
|
+
const report = buildObservationReport(note, deps.cliVersion(), context);
|
|
130
|
+
const unattended = !interactive && enablement.enabled;
|
|
131
|
+
if (unattended) {
|
|
132
|
+
const line = enablementLine(enablement, deps.now());
|
|
133
|
+
if (line !== undefined) {
|
|
134
|
+
deps.print("");
|
|
135
|
+
deps.print(`${line}. Not asking.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const l of renderObservation(report))
|
|
139
|
+
deps.print(l);
|
|
140
|
+
deps.print("");
|
|
141
|
+
// The ask is skipped unattended or under --yes; the SHOW above never is.
|
|
142
|
+
if (!unattended && !options.yes) {
|
|
143
|
+
if (!(await deps.confirm("Send this report?"))) {
|
|
144
|
+
deps.print("Not sent.");
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const res = await deps.post(auth.apiUrl, auth.pat, report);
|
|
150
|
+
deps.print(`Sent. Reference ${res.ref}.`);
|
|
151
|
+
deps.print("Thank you — this is the kind of thing we cannot detect on our own.");
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
if (err instanceof DXSendError) {
|
|
156
|
+
deps.print(err.message);
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The network half of `vincentt report`, kept out of `report.ts` so the command
|
|
3
|
+
* itself stays a pure function of its deps and needs no socket to test.
|
|
4
|
+
*
|
|
5
|
+
* It posts to the SAME endpoint `dx --send` does. An observation is not a
|
|
6
|
+
* different kind of thing from a report — it is a report whose carrier is a
|
|
7
|
+
* sentence rather than a cluster — and giving it its own route would have meant
|
|
8
|
+
* a second consent path, a second rate limiter and a second TTL to keep in step
|
|
9
|
+
* with the first.
|
|
10
|
+
*/
|
|
11
|
+
import type { WireObservationReport } from "./report.js";
|
|
12
|
+
export interface ObservationResult {
|
|
13
|
+
ref: string;
|
|
14
|
+
expiresAt: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function postObservationReport(apiUrl: string, pat: string, report: WireObservationReport, fetchImpl?: typeof fetch): Promise<ObservationResult>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DXSendError } from "./send.js";
|
|
2
|
+
export async function postObservationReport(apiUrl, pat, report, fetchImpl = fetch) {
|
|
3
|
+
const res = await fetchImpl(`${apiUrl}/dx-reports`, {
|
|
4
|
+
method: "POST",
|
|
5
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${pat}` },
|
|
6
|
+
body: JSON.stringify(report),
|
|
7
|
+
});
|
|
8
|
+
if (!res.ok) {
|
|
9
|
+
let detail = "";
|
|
10
|
+
try {
|
|
11
|
+
const body = (await res.json());
|
|
12
|
+
detail = body.message ?? "";
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// A non-JSON body is not more informative than the status.
|
|
16
|
+
}
|
|
17
|
+
if (res.status === 401) {
|
|
18
|
+
throw new DXSendError("Your access token is no longer valid — run `vincentt login`.", 401);
|
|
19
|
+
}
|
|
20
|
+
if (res.status === 429) {
|
|
21
|
+
// Distinguished from the generic path because it is the one refusal the
|
|
22
|
+
// creator can act on by simply waiting, and a bare "not accepted" would
|
|
23
|
+
// read as their report being rejected on its merits.
|
|
24
|
+
throw new DXSendError("Too many reports from this account just now — try again shortly.", 429);
|
|
25
|
+
}
|
|
26
|
+
throw new DXSendError(detail !== "" ? detail : `The report was not accepted (${res.status}).`, res.status);
|
|
27
|
+
}
|
|
28
|
+
return (await res.json());
|
|
29
|
+
}
|
package/package.json
CHANGED