letthemquibble 1.0.0 → 1.1.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/LICENSE +21 -0
- package/README.md +40 -16
- package/dist/quibble.js +2247 -454
- package/package.json +3 -2
package/dist/quibble.js
CHANGED
|
@@ -1156,18 +1156,18 @@ var require_suggestSimilar = __commonJS({
|
|
|
1156
1156
|
}
|
|
1157
1157
|
return d[a.length][b.length];
|
|
1158
1158
|
}
|
|
1159
|
-
function suggestSimilar(word,
|
|
1160
|
-
if (!
|
|
1161
|
-
|
|
1159
|
+
function suggestSimilar(word, candidates2) {
|
|
1160
|
+
if (!candidates2 || candidates2.length === 0) return "";
|
|
1161
|
+
candidates2 = Array.from(new Set(candidates2));
|
|
1162
1162
|
const searchingOptions = word.startsWith("--");
|
|
1163
1163
|
if (searchingOptions) {
|
|
1164
1164
|
word = word.slice(2);
|
|
1165
|
-
|
|
1165
|
+
candidates2 = candidates2.map((candidate) => candidate.slice(2));
|
|
1166
1166
|
}
|
|
1167
1167
|
let similar = [];
|
|
1168
1168
|
let bestDistance = maxDistance;
|
|
1169
1169
|
const minSimilarity = 0.4;
|
|
1170
|
-
|
|
1170
|
+
candidates2.forEach((candidate) => {
|
|
1171
1171
|
if (candidate.length <= 1) return;
|
|
1172
1172
|
const distance = editDistance(word, candidate);
|
|
1173
1173
|
const length = Math.max(word.length, candidate.length);
|
|
@@ -3490,110 +3490,6 @@ var {
|
|
|
3490
3490
|
Help
|
|
3491
3491
|
} = import_index.default;
|
|
3492
3492
|
|
|
3493
|
-
// src/config.ts
|
|
3494
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
3495
|
-
import { dirname as dirname3, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
|
|
3496
|
-
|
|
3497
|
-
// ../core/dist/types.js
|
|
3498
|
-
var SEVERITIES = ["low", "medium", "high"];
|
|
3499
|
-
var STATUSES = ["open", "resolved", "dismissed", "deleted"];
|
|
3500
|
-
var DELETED_STATUS = "deleted";
|
|
3501
|
-
var MESSAGE_MAX = 4e3;
|
|
3502
|
-
var RESOLUTION_MAX = 500;
|
|
3503
|
-
var TAGS_MAX = 10;
|
|
3504
|
-
var TAG_MAX = 32;
|
|
3505
|
-
var ALLOWED_TAGS_MAX = 50;
|
|
3506
|
-
var OTHER_TAG = "other";
|
|
3507
|
-
var OTHER_TAG_PREFIX = `${OTHER_TAG}:`;
|
|
3508
|
-
var OTHER_TAG_HINT = `${OTHER_TAG_PREFIX}<suggestion>`;
|
|
3509
|
-
var ALLOWED_TAG_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
3510
|
-
var AGENT_MAX = 64;
|
|
3511
|
-
var REPORTER_MAX = 128;
|
|
3512
|
-
var EMAIL_MAX = REPORTER_MAX;
|
|
3513
|
-
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3514
|
-
var TOKEN_NAME_MAX = 64;
|
|
3515
|
-
var CWD_MAX = 1024;
|
|
3516
|
-
var UNTAGGED_TAG = "-";
|
|
3517
|
-
var STATS_MAX_WINDOW_DAYS = 90;
|
|
3518
|
-
var ROLES = ["filer", "viewer", "maintainer"];
|
|
3519
|
-
var ORG_ROLES = ["member", "admin", "owner"];
|
|
3520
|
-
var ORG_COLOURS = [
|
|
3521
|
-
"slate",
|
|
3522
|
-
"moss",
|
|
3523
|
-
"ember",
|
|
3524
|
-
"rust",
|
|
3525
|
-
"teal",
|
|
3526
|
-
"plum",
|
|
3527
|
-
"snowcat",
|
|
3528
|
-
"bone"
|
|
3529
|
-
];
|
|
3530
|
-
var DEFAULT_ORG_COLOUR = "slate";
|
|
3531
|
-
var ORG_SLUG_RE = /^[a-z0-9][a-z0-9-]{1,37}[a-z0-9]$/;
|
|
3532
|
-
var RESERVED_SLUGS = [
|
|
3533
|
-
"api",
|
|
3534
|
-
"admin",
|
|
3535
|
-
"login",
|
|
3536
|
-
"logout",
|
|
3537
|
-
"o",
|
|
3538
|
-
"orgs",
|
|
3539
|
-
"new",
|
|
3540
|
-
"platform",
|
|
3541
|
-
"local",
|
|
3542
|
-
"www",
|
|
3543
|
-
"app",
|
|
3544
|
-
"settings",
|
|
3545
|
-
"signup"
|
|
3546
|
-
];
|
|
3547
|
-
var LOCAL_ORG = "local";
|
|
3548
|
-
var DISPLAY_NAME_MAX = 64;
|
|
3549
|
-
var PROJECT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
3550
|
-
var DEFAULT_LIMIT = 50;
|
|
3551
|
-
var MAX_LIMIT = 200;
|
|
3552
|
-
var DEFAULT_SEVERITY = "medium";
|
|
3553
|
-
var DEFAULT_STATUS = "open";
|
|
3554
|
-
var DEFAULT_AGENT = "unknown";
|
|
3555
|
-
var DEFAULT_REPORTER = "unknown";
|
|
3556
|
-
var ACT_AS_HEADER = "x-quibble-act-as";
|
|
3557
|
-
|
|
3558
|
-
// ../core/dist/errors.js
|
|
3559
|
-
var QuibbleError = class extends Error {
|
|
3560
|
-
/** Stable machine-readable code, also used by the API error body. */
|
|
3561
|
-
code;
|
|
3562
|
-
constructor(code, message, options) {
|
|
3563
|
-
super(message, options);
|
|
3564
|
-
this.code = code;
|
|
3565
|
-
this.name = new.target.name;
|
|
3566
|
-
}
|
|
3567
|
-
};
|
|
3568
|
-
var ValidationError = class extends QuibbleError {
|
|
3569
|
-
/** One readable line per problem, such as `message: must be 1-4000 characters`. */
|
|
3570
|
-
issues;
|
|
3571
|
-
constructor(issues, options) {
|
|
3572
|
-
const list = issues.length > 0 ? issues : ["invalid input"];
|
|
3573
|
-
super("validation_error", list.join("; "), options);
|
|
3574
|
-
this.issues = [...list];
|
|
3575
|
-
}
|
|
3576
|
-
};
|
|
3577
|
-
var StorageError = class extends QuibbleError {
|
|
3578
|
-
constructor(message, options) {
|
|
3579
|
-
super("storage_error", message, options);
|
|
3580
|
-
}
|
|
3581
|
-
};
|
|
3582
|
-
var ConfigError = class extends QuibbleError {
|
|
3583
|
-
/** The file that holds the bad configuration, when one exists. */
|
|
3584
|
-
path;
|
|
3585
|
-
constructor(message, path, options) {
|
|
3586
|
-
super("config_error", message, options);
|
|
3587
|
-
this.path = path;
|
|
3588
|
-
}
|
|
3589
|
-
};
|
|
3590
|
-
function isNotFound(error62) {
|
|
3591
|
-
return typeof error62 === "object" && error62 !== null && error62.code === "ENOENT";
|
|
3592
|
-
}
|
|
3593
|
-
function describeError(error62) {
|
|
3594
|
-
return error62 instanceof Error ? error62.message : String(error62);
|
|
3595
|
-
}
|
|
3596
|
-
|
|
3597
3493
|
// ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/classic/external.js
|
|
3598
3494
|
var external_exports = {};
|
|
3599
3495
|
__export(external_exports, {
|
|
@@ -6407,9 +6303,9 @@ var asciiTabOrNewline = /[\t\n\r]/g;
|
|
|
6407
6303
|
function stripTabAndNewline(value) {
|
|
6408
6304
|
return value.replace(asciiTabOrNewline, "");
|
|
6409
6305
|
}
|
|
6410
|
-
function urlHostnameOk(url2,
|
|
6411
|
-
|
|
6412
|
-
return
|
|
6306
|
+
function urlHostnameOk(url2, hostname4) {
|
|
6307
|
+
hostname4.lastIndex = 0;
|
|
6308
|
+
return hostname4.test(url2.hostname);
|
|
6413
6309
|
}
|
|
6414
6310
|
function urlProtocolOk(url2, protocol) {
|
|
6415
6311
|
protocol.lastIndex = 0;
|
|
@@ -8620,7 +8516,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
8620
8516
|
var NONE = 0;
|
|
8621
8517
|
var ASSUMED = 1;
|
|
8622
8518
|
var PROVEN = 2;
|
|
8623
|
-
function isRecursive(inst, stack,
|
|
8519
|
+
function isRecursive(inst, stack, resolve4) {
|
|
8624
8520
|
const cached2 = recursive.get(inst);
|
|
8625
8521
|
if (cached2 !== void 0)
|
|
8626
8522
|
return cached2 ? PROVEN : NONE;
|
|
@@ -8630,7 +8526,7 @@ function isRecursive(inst, stack, resolve3) {
|
|
|
8630
8526
|
let result = NONE;
|
|
8631
8527
|
const check2 = (child) => {
|
|
8632
8528
|
if (result !== PROVEN && child?._zod) {
|
|
8633
|
-
const answer2 = isRecursive(child, stack,
|
|
8529
|
+
const answer2 = isRecursive(child, stack, resolve4);
|
|
8634
8530
|
if (answer2 > result)
|
|
8635
8531
|
result = answer2;
|
|
8636
8532
|
}
|
|
@@ -8641,7 +8537,7 @@ function isRecursive(inst, stack, resolve3) {
|
|
|
8641
8537
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
8642
8538
|
if (spread && !desc.enumerable)
|
|
8643
8539
|
continue;
|
|
8644
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
8540
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve4) : NONE;
|
|
8645
8541
|
if (child > answer2)
|
|
8646
8542
|
answer2 = child;
|
|
8647
8543
|
}
|
|
@@ -8705,7 +8601,7 @@ function isRecursive(inst, stack, resolve3) {
|
|
|
8705
8601
|
break;
|
|
8706
8602
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
8707
8603
|
case "lazy": {
|
|
8708
|
-
const inner = def._cachedInner ?? (
|
|
8604
|
+
const inner = def._cachedInner ?? (resolve4 ? inst._zod.innerType : void 0);
|
|
8709
8605
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
8710
8606
|
break;
|
|
8711
8607
|
}
|
|
@@ -23262,66 +23158,292 @@ function date4(params) {
|
|
|
23262
23158
|
return _coercedDate(ZodDate, params);
|
|
23263
23159
|
}
|
|
23264
23160
|
|
|
23161
|
+
// ../core/dist/jitless.js
|
|
23162
|
+
external_exports.config({ jitless: true });
|
|
23163
|
+
|
|
23164
|
+
// ../core/dist/types.js
|
|
23165
|
+
var SEVERITIES = ["low", "medium", "high"];
|
|
23166
|
+
var STATUSES = ["open", "resolved", "dismissed", "deleted"];
|
|
23167
|
+
var DELETED_STATUS = "deleted";
|
|
23168
|
+
var REASONS = ["fixed", "duplicate", "wontfix", "noise"];
|
|
23169
|
+
var DUPLICATE_REASON = "duplicate";
|
|
23170
|
+
var MESSAGE_MAX = 4e3;
|
|
23171
|
+
var RESOLUTION_MAX = 500;
|
|
23172
|
+
var TAGS_MAX = 10;
|
|
23173
|
+
var TAG_MAX = 32;
|
|
23174
|
+
var ALLOWED_TAGS_MAX = 50;
|
|
23175
|
+
var OTHER_TAG = "other";
|
|
23176
|
+
var OTHER_TAG_PREFIX = `${OTHER_TAG}:`;
|
|
23177
|
+
var OTHER_TAG_HINT = `${OTHER_TAG_PREFIX}<suggestion>`;
|
|
23178
|
+
var ALLOWED_TAG_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
23179
|
+
var AGENT_MAX = 64;
|
|
23180
|
+
var REPORTER_MAX = 128;
|
|
23181
|
+
var EMAIL_MAX = REPORTER_MAX;
|
|
23182
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
23183
|
+
var TOKEN_NAME_MAX = 64;
|
|
23184
|
+
var CWD_MAX = 1024;
|
|
23185
|
+
var CMD_MAX = 1e3;
|
|
23186
|
+
var FINGERPRINT_MAX = 200;
|
|
23187
|
+
var EMPTY_FINGERPRINT = "<empty>";
|
|
23188
|
+
var RECURRENCE_WINDOW_DAYS = 90;
|
|
23189
|
+
var SEARCH_MAX = 200;
|
|
23190
|
+
var BULK_MAX = 100;
|
|
23191
|
+
var ID_MAX = 64;
|
|
23192
|
+
var UNTAGGED_TAG = "-";
|
|
23193
|
+
var STATS_MAX_WINDOW_DAYS = 90;
|
|
23194
|
+
var ROLES = ["filer", "viewer", "maintainer"];
|
|
23195
|
+
var ORG_ROLES = ["member", "admin", "owner"];
|
|
23196
|
+
var ORG_COLOURS = [
|
|
23197
|
+
"slate",
|
|
23198
|
+
"moss",
|
|
23199
|
+
"ember",
|
|
23200
|
+
"rust",
|
|
23201
|
+
"teal",
|
|
23202
|
+
"plum",
|
|
23203
|
+
"snowcat",
|
|
23204
|
+
"bone"
|
|
23205
|
+
];
|
|
23206
|
+
var DEFAULT_ORG_COLOUR = "slate";
|
|
23207
|
+
var ORG_SLUG_RE = /^[a-z0-9][a-z0-9-]{1,37}[a-z0-9]$/;
|
|
23208
|
+
var RESERVED_SLUGS = [
|
|
23209
|
+
"api",
|
|
23210
|
+
"admin",
|
|
23211
|
+
"login",
|
|
23212
|
+
"logout",
|
|
23213
|
+
"o",
|
|
23214
|
+
"orgs",
|
|
23215
|
+
"new",
|
|
23216
|
+
"platform",
|
|
23217
|
+
"local",
|
|
23218
|
+
"www",
|
|
23219
|
+
"app",
|
|
23220
|
+
"settings",
|
|
23221
|
+
"signup"
|
|
23222
|
+
];
|
|
23223
|
+
var LOCAL_ORG = "local";
|
|
23224
|
+
var DISPLAY_NAME_MAX = 64;
|
|
23225
|
+
var PROJECT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
23226
|
+
var DEFAULT_LIMIT = 50;
|
|
23227
|
+
var MAX_LIMIT = 200;
|
|
23228
|
+
var DEFAULT_SEVERITY = "medium";
|
|
23229
|
+
var DEFAULT_STATUS = "open";
|
|
23230
|
+
var DEFAULT_AGENT = "unknown";
|
|
23231
|
+
var DEFAULT_REPORTER = "unknown";
|
|
23232
|
+
var ACT_AS_HEADER = "x-quibble-act-as";
|
|
23233
|
+
var TRUNCATED_HEADER = "x-quibble-truncated";
|
|
23234
|
+
var IDEMPOTENCY_HEADER = "idempotency-key";
|
|
23235
|
+
var IDEMPOTENCY_KEY_MAX = 128;
|
|
23236
|
+
|
|
23237
|
+
// ../core/dist/errors.js
|
|
23238
|
+
var QuibbleError = class extends Error {
|
|
23239
|
+
/** Stable machine-readable code, also used by the API error body. */
|
|
23240
|
+
code;
|
|
23241
|
+
constructor(code, message, options) {
|
|
23242
|
+
super(message, options);
|
|
23243
|
+
this.code = code;
|
|
23244
|
+
this.name = new.target.name;
|
|
23245
|
+
}
|
|
23246
|
+
};
|
|
23247
|
+
var ValidationError = class extends QuibbleError {
|
|
23248
|
+
/** One readable line per problem, such as `message: must be 1-4000 characters`. */
|
|
23249
|
+
issues;
|
|
23250
|
+
constructor(issues, options) {
|
|
23251
|
+
const list = issues.length > 0 ? issues : ["invalid input"];
|
|
23252
|
+
super("validation_error", list.join("; "), options);
|
|
23253
|
+
this.issues = [...list];
|
|
23254
|
+
}
|
|
23255
|
+
};
|
|
23256
|
+
var StorageError = class extends QuibbleError {
|
|
23257
|
+
constructor(message, options) {
|
|
23258
|
+
super("storage_error", message, options);
|
|
23259
|
+
}
|
|
23260
|
+
};
|
|
23261
|
+
var DuplicateIdError = class extends StorageError {
|
|
23262
|
+
/** The id that a record already holds. */
|
|
23263
|
+
id;
|
|
23264
|
+
/**
|
|
23265
|
+
* The record that won, when the store read it back, so the caller answers
|
|
23266
|
+
* it without a read of its own that an index may not see yet.
|
|
23267
|
+
*/
|
|
23268
|
+
existing;
|
|
23269
|
+
constructor(id, existing, options) {
|
|
23270
|
+
super(`complaint ${id} already exists`, options);
|
|
23271
|
+
this.id = id;
|
|
23272
|
+
this.existing = existing;
|
|
23273
|
+
}
|
|
23274
|
+
};
|
|
23275
|
+
var ConfigError = class extends QuibbleError {
|
|
23276
|
+
/** The file that holds the bad configuration, when one exists. */
|
|
23277
|
+
path;
|
|
23278
|
+
constructor(message, path, options) {
|
|
23279
|
+
super("config_error", message, options);
|
|
23280
|
+
this.path = path;
|
|
23281
|
+
}
|
|
23282
|
+
};
|
|
23283
|
+
function isNotFound(error62) {
|
|
23284
|
+
return typeof error62 === "object" && error62 !== null && error62.code === "ENOENT";
|
|
23285
|
+
}
|
|
23286
|
+
function describeError(error62) {
|
|
23287
|
+
return error62 instanceof Error ? error62.message : String(error62);
|
|
23288
|
+
}
|
|
23289
|
+
|
|
23290
|
+
// ../core/dist/fingerprint.js
|
|
23291
|
+
function headlineOf(message) {
|
|
23292
|
+
for (const line of message.split("\n")) {
|
|
23293
|
+
const trimmed = line.trim();
|
|
23294
|
+
if (trimmed !== "")
|
|
23295
|
+
return trimmed;
|
|
23296
|
+
}
|
|
23297
|
+
return "";
|
|
23298
|
+
}
|
|
23299
|
+
var WORD_CHAR = "[\\p{L}\\p{N}]";
|
|
23300
|
+
var QUOTED_RE = new RegExp(`"[^"]*"|\`[^\`]*\`|(?<!${WORD_CHAR})'.*?'(?!${WORD_CHAR})`, "gu");
|
|
23301
|
+
var PATH_RE = /[^\s]*[/\\][^\s]*/gu;
|
|
23302
|
+
var UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gu;
|
|
23303
|
+
var HEX_LITERAL_RE = /0x[0-9a-f]+/gu;
|
|
23304
|
+
var HEX_RUN_RE = new RegExp(`(?<!${WORD_CHAR})[0-9a-f]{7,}(?!${WORD_CHAR})`, "gu");
|
|
23305
|
+
var NUMBER_RE = new RegExp("\\p{N}+(?:\\.\\p{N}+)*", "gu");
|
|
23306
|
+
var PUNCTUATION_RE = /[^\p{L}\p{N}\s<>]/gu;
|
|
23307
|
+
function isHexId(run2) {
|
|
23308
|
+
return /[a-f]/.test(run2) && /[0-9]/.test(run2);
|
|
23309
|
+
}
|
|
23310
|
+
function cut(text, max) {
|
|
23311
|
+
if (text.length <= max)
|
|
23312
|
+
return text;
|
|
23313
|
+
const head = text.slice(0, max);
|
|
23314
|
+
const last = head.charCodeAt(head.length - 1);
|
|
23315
|
+
return last >= 55296 && last <= 56319 ? head.slice(0, -1) : head;
|
|
23316
|
+
}
|
|
23317
|
+
function fingerprintOf(message) {
|
|
23318
|
+
let text = headlineOf(message).toLowerCase();
|
|
23319
|
+
text = text.replace(/[<>]/gu, " ");
|
|
23320
|
+
text = text.replace(QUOTED_RE, " <str> ");
|
|
23321
|
+
text = text.replace(PATH_RE, "<path>");
|
|
23322
|
+
text = text.replace(UUID_RE, "<hex>");
|
|
23323
|
+
text = text.replace(HEX_LITERAL_RE, "<hex>");
|
|
23324
|
+
text = text.replace(HEX_RUN_RE, (run2) => isHexId(run2) ? "<hex>" : run2);
|
|
23325
|
+
text = text.replace(NUMBER_RE, "<n>");
|
|
23326
|
+
text = text.replace(PUNCTUATION_RE, " ");
|
|
23327
|
+
text = text.replace(/\s+/gu, " ").trim();
|
|
23328
|
+
text = cut(text, FINGERPRINT_MAX).trim();
|
|
23329
|
+
return text === "" ? EMPTY_FINGERPRINT : text;
|
|
23330
|
+
}
|
|
23331
|
+
function groupComplaints(complaints) {
|
|
23332
|
+
const groups = /* @__PURE__ */ new Map();
|
|
23333
|
+
for (const complaint of complaints) {
|
|
23334
|
+
const key = `${complaint.org}/${complaint.project}
|
|
23335
|
+
${complaint.fingerprint}`;
|
|
23336
|
+
const group = groups.get(key);
|
|
23337
|
+
const recurred = complaint.status === "open" && complaint.recurred === true;
|
|
23338
|
+
if (group === void 0) {
|
|
23339
|
+
groups.set(key, {
|
|
23340
|
+
project: complaint.project,
|
|
23341
|
+
fingerprint: complaint.fingerprint,
|
|
23342
|
+
head: complaint,
|
|
23343
|
+
members: [complaint],
|
|
23344
|
+
count: 1,
|
|
23345
|
+
recurred
|
|
23346
|
+
});
|
|
23347
|
+
continue;
|
|
23348
|
+
}
|
|
23349
|
+
group.members.push(complaint);
|
|
23350
|
+
group.count = group.members.length;
|
|
23351
|
+
group.recurred ||= recurred;
|
|
23352
|
+
}
|
|
23353
|
+
return [...groups.values()];
|
|
23354
|
+
}
|
|
23355
|
+
|
|
23265
23356
|
// ../core/dist/validate.js
|
|
23266
|
-
var
|
|
23357
|
+
var NEW_NULLABLE_KEYS = [
|
|
23267
23358
|
"severity",
|
|
23268
23359
|
"tags",
|
|
23269
23360
|
"agent",
|
|
23270
23361
|
"reporter",
|
|
23271
23362
|
"reporterName",
|
|
23272
23363
|
"cwd",
|
|
23273
|
-
"
|
|
23274
|
-
"resolution",
|
|
23275
|
-
"updatedAt"
|
|
23364
|
+
"cmd"
|
|
23276
23365
|
];
|
|
23277
|
-
var
|
|
23366
|
+
var NEW_BLANKABLE_KEYS = [
|
|
23278
23367
|
"severity",
|
|
23279
23368
|
"agent",
|
|
23280
23369
|
"reporter",
|
|
23281
23370
|
"reporterName",
|
|
23371
|
+
"cmd"
|
|
23372
|
+
];
|
|
23373
|
+
var STORED_STATUS_KEYS = [
|
|
23282
23374
|
"status",
|
|
23283
23375
|
"resolution",
|
|
23376
|
+
"reason",
|
|
23377
|
+
"duplicateOf",
|
|
23378
|
+
"statusChangedAt",
|
|
23379
|
+
"statusChangedBy",
|
|
23380
|
+
"statusChangedByName",
|
|
23284
23381
|
"updatedAt"
|
|
23285
23382
|
];
|
|
23383
|
+
var STORED_NULLABLE_KEYS = [
|
|
23384
|
+
...NEW_NULLABLE_KEYS,
|
|
23385
|
+
...STORED_STATUS_KEYS,
|
|
23386
|
+
"fingerprint",
|
|
23387
|
+
"recurred"
|
|
23388
|
+
];
|
|
23389
|
+
var STORED_BLANKABLE_KEYS = [
|
|
23390
|
+
...NEW_BLANKABLE_KEYS,
|
|
23391
|
+
...STORED_STATUS_KEYS,
|
|
23392
|
+
"fingerprint"
|
|
23393
|
+
];
|
|
23286
23394
|
var FILTER_KEYS = [
|
|
23287
23395
|
"project",
|
|
23288
23396
|
"severity",
|
|
23289
23397
|
"status",
|
|
23290
|
-
"
|
|
23398
|
+
"tags",
|
|
23291
23399
|
"reporter",
|
|
23400
|
+
"q",
|
|
23292
23401
|
"since",
|
|
23293
23402
|
"until",
|
|
23294
23403
|
"limit"
|
|
23295
23404
|
];
|
|
23296
|
-
var
|
|
23405
|
+
var FILTER_NULLABLE_KEYS = ["fingerprint"];
|
|
23406
|
+
var STATUS_CHANGE_KEYS = ["status", "resolution", "reason", "duplicateOf"];
|
|
23297
23407
|
function isPlainObject2(value) {
|
|
23298
23408
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23299
23409
|
}
|
|
23300
23410
|
function isBlank(value) {
|
|
23301
23411
|
return typeof value === "string" && value.trim() === "";
|
|
23302
23412
|
}
|
|
23303
|
-
function
|
|
23413
|
+
function complaintInput(nullable2, blankable) {
|
|
23414
|
+
return (value) => {
|
|
23415
|
+
if (!isPlainObject2(value))
|
|
23416
|
+
return value;
|
|
23417
|
+
const out = { ...value };
|
|
23418
|
+
for (const key of nullable2) {
|
|
23419
|
+
if (out[key] === null)
|
|
23420
|
+
delete out[key];
|
|
23421
|
+
}
|
|
23422
|
+
for (const key of blankable) {
|
|
23423
|
+
if (isBlank(out[key]))
|
|
23424
|
+
delete out[key];
|
|
23425
|
+
}
|
|
23426
|
+
if (isBlank(out["cwd"]))
|
|
23427
|
+
out["cwd"] = ".";
|
|
23428
|
+
return out;
|
|
23429
|
+
};
|
|
23430
|
+
}
|
|
23431
|
+
function normalizeKeys(value, keys) {
|
|
23304
23432
|
if (!isPlainObject2(value))
|
|
23305
23433
|
return value;
|
|
23306
23434
|
const out = { ...value };
|
|
23307
|
-
for (const key of
|
|
23308
|
-
if (out[key] === null)
|
|
23309
|
-
delete out[key];
|
|
23310
|
-
}
|
|
23311
|
-
for (const key of BLANKABLE_KEYS) {
|
|
23312
|
-
if (isBlank(out[key]))
|
|
23435
|
+
for (const key of keys) {
|
|
23436
|
+
if (out[key] === null || isBlank(out[key]))
|
|
23313
23437
|
delete out[key];
|
|
23314
23438
|
}
|
|
23315
|
-
if (isBlank(out["cwd"]))
|
|
23316
|
-
out["cwd"] = ".";
|
|
23317
23439
|
return out;
|
|
23318
23440
|
}
|
|
23319
|
-
function
|
|
23441
|
+
function dropNulls(value, keys) {
|
|
23320
23442
|
if (!isPlainObject2(value))
|
|
23321
23443
|
return value;
|
|
23322
23444
|
const out = { ...value };
|
|
23323
23445
|
for (const key of keys) {
|
|
23324
|
-
if (out[key] === null
|
|
23446
|
+
if (out[key] === null)
|
|
23325
23447
|
delete out[key];
|
|
23326
23448
|
}
|
|
23327
23449
|
return out;
|
|
@@ -23333,6 +23455,12 @@ function boundedString(max, typeMessage) {
|
|
|
23333
23455
|
const message = lengthMessage(1, max);
|
|
23334
23456
|
return external_exports.string({ message: typeMessage ?? message }).trim().min(1, { message }).max(max, { message });
|
|
23335
23457
|
}
|
|
23458
|
+
var CONTROL_RE = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/;
|
|
23459
|
+
var CONTROL_MESSAGE = "must not contain control characters";
|
|
23460
|
+
function freeText(max, typeMessage) {
|
|
23461
|
+
const message = lengthMessage(1, max);
|
|
23462
|
+
return external_exports.string({ message: typeMessage ?? message }).overwrite((value) => value.replace(/\r\n?/g, "\n")).trim().min(1, { message }).max(max, { message }).refine((value) => !CONTROL_RE.test(value), { message: CONTROL_MESSAGE });
|
|
23463
|
+
}
|
|
23336
23464
|
var SEVERITY_MESSAGE = `must be one of ${SEVERITIES.join(", ")}`;
|
|
23337
23465
|
var STATUS_MESSAGE = `must be one of ${STATUSES.join(", ")}`;
|
|
23338
23466
|
var LIMIT_MESSAGE = `must be an integer between 1 and ${MAX_LIMIT}`;
|
|
@@ -23354,7 +23482,11 @@ var orgSlugSchema = external_exports.string({ message: SLUG_MESSAGE }).refine((v
|
|
|
23354
23482
|
});
|
|
23355
23483
|
var severitySchema = external_exports.enum(SEVERITIES, { message: SEVERITY_MESSAGE });
|
|
23356
23484
|
var statusSchema = external_exports.enum(STATUSES, { message: STATUS_MESSAGE });
|
|
23357
|
-
var
|
|
23485
|
+
var REASON_MESSAGE = `must be one of ${REASONS.join(", ")}`;
|
|
23486
|
+
var reasonSchema = external_exports.enum(REASONS, { message: REASON_MESSAGE });
|
|
23487
|
+
var idSchema = boundedString(ID_MAX, ID_MESSAGE);
|
|
23488
|
+
var resolutionSchema = freeText(RESOLUTION_MAX);
|
|
23489
|
+
var storedResolutionSchema = boundedString(RESOLUTION_MAX);
|
|
23358
23490
|
var TAG_LENGTH_MESSAGE = lengthMessage(1, TAG_MAX);
|
|
23359
23491
|
var SUGGESTION_LENGTH_MESSAGE = `must be at most ${TAG_MAX} characters after "${OTHER_TAG_PREFIX}"`;
|
|
23360
23492
|
var tagSchema = external_exports.string({ message: "must be a string" }).trim().min(1, { message: TAG_LENGTH_MESSAGE }).superRefine((tag, ctx) => {
|
|
@@ -23370,8 +23502,8 @@ var tagSchema = external_exports.string({ message: "must be a string" }).trim().
|
|
|
23370
23502
|
}
|
|
23371
23503
|
});
|
|
23372
23504
|
var TAGS_TYPE_MESSAGE = "must be an array of strings";
|
|
23373
|
-
var UNTAGGED_TAG_MESSAGE = `must not be "${UNTAGGED_TAG}"
|
|
23374
|
-
var RESERVED_TAG_MESSAGE = `must not be "${OTHER_TAG}" or start with "${OTHER_TAG_PREFIX}"
|
|
23505
|
+
var UNTAGGED_TAG_MESSAGE = `must not be "${UNTAGGED_TAG}" (reserved for untagged)`;
|
|
23506
|
+
var RESERVED_TAG_MESSAGE = `must not be "${OTHER_TAG}" or start with "${OTHER_TAG_PREFIX}" (reserved)`;
|
|
23375
23507
|
var ALLOWED_TAG_MESSAGE = 'must use only letters, digits, ".", "_" and "-", and start with a letter or digit';
|
|
23376
23508
|
function normalizeTag(tag) {
|
|
23377
23509
|
const lower = tag.toLowerCase();
|
|
@@ -23386,7 +23518,7 @@ var complaintTagSchema = tagSchema.refine((tag) => tag !== UNTAGGED_TAG, {
|
|
|
23386
23518
|
var tagsSchema = external_exports.array(complaintTagSchema, { message: TAGS_TYPE_MESSAGE }).transform(normalizeTags).refine((tags) => tags.length <= TAGS_MAX, {
|
|
23387
23519
|
message: `must have at most ${TAGS_MAX} tags`
|
|
23388
23520
|
});
|
|
23389
|
-
var tagsChangeObject = external_exports.
|
|
23521
|
+
var tagsChangeObject = external_exports.strictObject({
|
|
23390
23522
|
tags: tagsSchema.refine((tags) => tags.length > 0, {
|
|
23391
23523
|
message: "must list at least 1 tag"
|
|
23392
23524
|
})
|
|
@@ -23474,17 +23606,21 @@ var limitSchema = external_exports.preprocess((value) => {
|
|
|
23474
23606
|
const parsed = Number(trimmed);
|
|
23475
23607
|
return Number.isNaN(parsed) ? value : parsed;
|
|
23476
23608
|
}, external_exports.number({ message: LIMIT_MESSAGE }).int({ message: LIMIT_MESSAGE }).min(1, { message: LIMIT_MESSAGE }).max(MAX_LIMIT, { message: LIMIT_MESSAGE }));
|
|
23477
|
-
var
|
|
23609
|
+
var agentSchema = freeText(AGENT_MAX);
|
|
23610
|
+
var newComplaintShape = {
|
|
23478
23611
|
org: orgSlugSchema,
|
|
23479
23612
|
project: projectNameSchema,
|
|
23480
|
-
message:
|
|
23613
|
+
message: freeText(MESSAGE_MAX),
|
|
23481
23614
|
severity: severitySchema.optional(),
|
|
23482
23615
|
tags: tagsSchema.optional(),
|
|
23483
|
-
agent:
|
|
23484
|
-
reporter:
|
|
23485
|
-
reporterName:
|
|
23486
|
-
cwd: cwdSchema.optional()
|
|
23487
|
-
|
|
23616
|
+
agent: agentSchema.optional(),
|
|
23617
|
+
reporter: freeText(REPORTER_MAX).optional(),
|
|
23618
|
+
reporterName: freeText(REPORTER_MAX).optional(),
|
|
23619
|
+
cwd: cwdSchema.optional(),
|
|
23620
|
+
// Free text: it is trimmed and may hold newlines, and nothing parses it.
|
|
23621
|
+
cmd: freeText(CMD_MAX).optional()
|
|
23622
|
+
};
|
|
23623
|
+
var newComplaintObject = external_exports.strictObject(newComplaintShape);
|
|
23488
23624
|
function withDefaults(input2) {
|
|
23489
23625
|
return {
|
|
23490
23626
|
org: input2.org,
|
|
@@ -23496,74 +23632,207 @@ function withDefaults(input2) {
|
|
|
23496
23632
|
reporter: input2.reporter ?? DEFAULT_REPORTER,
|
|
23497
23633
|
// The server sets the display name, so it stays absent when nobody did.
|
|
23498
23634
|
...input2.reporterName === void 0 ? {} : { reporterName: input2.reporterName },
|
|
23499
|
-
...input2.cwd === void 0 ? {} : { cwd: input2.cwd }
|
|
23635
|
+
...input2.cwd === void 0 ? {} : { cwd: input2.cwd },
|
|
23636
|
+
...input2.cmd === void 0 ? {} : { cmd: input2.cmd }
|
|
23500
23637
|
};
|
|
23501
23638
|
}
|
|
23502
|
-
var newComplaintSchema = external_exports.preprocess(
|
|
23503
|
-
var
|
|
23639
|
+
var newComplaintSchema = external_exports.preprocess(complaintInput(NEW_NULLABLE_KEYS, NEW_BLANKABLE_KEYS), newComplaintObject).transform(withDefaults);
|
|
23640
|
+
var actorSchema = boundedString(REPORTER_MAX);
|
|
23641
|
+
var storedComplaintObject = external_exports.object({
|
|
23642
|
+
...newComplaintShape,
|
|
23643
|
+
// The free text of a stored record is read without the control rule, so a
|
|
23644
|
+
// record written before it stays readable.
|
|
23645
|
+
message: boundedString(MESSAGE_MAX),
|
|
23646
|
+
agent: boundedString(AGENT_MAX).optional(),
|
|
23647
|
+
reporter: boundedString(REPORTER_MAX).optional(),
|
|
23648
|
+
reporterName: boundedString(REPORTER_MAX).optional(),
|
|
23649
|
+
cmd: boundedString(CMD_MAX).optional(),
|
|
23504
23650
|
id: external_exports.string({ message: ID_MESSAGE }).trim().min(1, { message: ID_MESSAGE }),
|
|
23651
|
+
fingerprint: external_exports.string({ message: ID_MESSAGE }).min(1, { message: ID_MESSAGE }).optional(),
|
|
23652
|
+
recurred: external_exports.boolean({ message: "must be a boolean" }).optional(),
|
|
23505
23653
|
createdAt: timestampSchema,
|
|
23506
23654
|
status: statusSchema.optional(),
|
|
23507
|
-
resolution:
|
|
23655
|
+
resolution: storedResolutionSchema.optional(),
|
|
23656
|
+
reason: reasonSchema.optional(),
|
|
23657
|
+
duplicateOf: idSchema.optional(),
|
|
23658
|
+
statusChangedAt: timestampSchema.optional(),
|
|
23659
|
+
statusChangedBy: actorSchema.optional(),
|
|
23660
|
+
statusChangedByName: actorSchema.optional(),
|
|
23508
23661
|
updatedAt: timestampSchema.optional()
|
|
23509
23662
|
});
|
|
23510
|
-
var complaintSchema = external_exports.preprocess(
|
|
23663
|
+
var complaintSchema = external_exports.preprocess(complaintInput(STORED_NULLABLE_KEYS, STORED_BLANKABLE_KEYS), storedComplaintObject).transform((input2) => ({
|
|
23511
23664
|
id: input2.id,
|
|
23512
23665
|
...withDefaults(input2),
|
|
23666
|
+
fingerprint: input2.fingerprint ?? fingerprintOf(input2.message),
|
|
23667
|
+
// The store writes the flag only when it is true.
|
|
23668
|
+
...input2.recurred === true ? { recurred: true } : {},
|
|
23513
23669
|
// Millisecond precision, so a record written without a fraction still
|
|
23514
23670
|
// compares by time against a `since` and an `until`.
|
|
23515
23671
|
createdAt: toMilliseconds(input2.createdAt),
|
|
23516
23672
|
status: input2.status ?? DEFAULT_STATUS,
|
|
23517
23673
|
...input2.resolution === void 0 ? {} : { resolution: input2.resolution },
|
|
23674
|
+
...input2.reason === void 0 ? {} : { reason: input2.reason },
|
|
23675
|
+
...input2.duplicateOf === void 0 ? {} : { duplicateOf: input2.duplicateOf },
|
|
23676
|
+
...input2.statusChangedAt === void 0 ? {} : { statusChangedAt: toMilliseconds(input2.statusChangedAt) },
|
|
23677
|
+
...input2.statusChangedBy === void 0 ? {} : { statusChangedBy: input2.statusChangedBy },
|
|
23678
|
+
...input2.statusChangedByName === void 0 ? {} : { statusChangedByName: input2.statusChangedByName },
|
|
23518
23679
|
...input2.updatedAt === void 0 ? {} : { updatedAt: toMilliseconds(input2.updatedAt) }
|
|
23519
23680
|
}));
|
|
23520
|
-
var
|
|
23681
|
+
var REASON_STATUS_MESSAGE = "only a resolve or a dismiss takes a reason";
|
|
23682
|
+
var DUPLICATE_OF_MESSAGE = `needs reason ${DUPLICATE_REASON}`;
|
|
23683
|
+
var statusChangeShape = {
|
|
23521
23684
|
status: statusSchema,
|
|
23522
|
-
resolution: resolutionSchema.optional()
|
|
23523
|
-
|
|
23524
|
-
|
|
23525
|
-
|
|
23526
|
-
|
|
23527
|
-
|
|
23528
|
-
|
|
23529
|
-
|
|
23530
|
-
|
|
23685
|
+
resolution: resolutionSchema.optional(),
|
|
23686
|
+
reason: reasonSchema.optional(),
|
|
23687
|
+
duplicateOf: idSchema.optional()
|
|
23688
|
+
};
|
|
23689
|
+
function checkReason(change, ctx) {
|
|
23690
|
+
if (change.reason !== void 0 && change.status !== "resolved" && change.status !== "dismissed") {
|
|
23691
|
+
ctx.addIssue({ code: "custom", path: ["reason"], message: REASON_STATUS_MESSAGE });
|
|
23692
|
+
}
|
|
23693
|
+
if (change.duplicateOf !== void 0 && change.reason !== DUPLICATE_REASON) {
|
|
23694
|
+
ctx.addIssue({
|
|
23695
|
+
code: "custom",
|
|
23696
|
+
path: ["duplicateOf"],
|
|
23697
|
+
message: DUPLICATE_OF_MESSAGE
|
|
23698
|
+
});
|
|
23699
|
+
}
|
|
23700
|
+
}
|
|
23701
|
+
function toStatusChange(input2) {
|
|
23702
|
+
return {
|
|
23703
|
+
status: input2.status,
|
|
23704
|
+
...input2.resolution === void 0 ? {} : { resolution: input2.resolution },
|
|
23705
|
+
...input2.reason === void 0 ? {} : { reason: input2.reason },
|
|
23706
|
+
...input2.duplicateOf === void 0 ? {} : { duplicateOf: input2.duplicateOf }
|
|
23707
|
+
};
|
|
23708
|
+
}
|
|
23709
|
+
var statusChangeSchema = external_exports.preprocess((value) => normalizeKeys(value, STATUS_CHANGE_KEYS), external_exports.strictObject(statusChangeShape).superRefine(checkReason)).transform(toStatusChange);
|
|
23710
|
+
var BULK_IDS_MESSAGE = `must list 1 to ${BULK_MAX} ids`;
|
|
23711
|
+
var BULK_BOTH_MESSAGE = "must change the status or the tags, not both";
|
|
23712
|
+
var BULK_NEITHER_MESSAGE = "must carry a status, addTags or removeTags";
|
|
23713
|
+
var BULK_NO_TAG_MESSAGE = "must carry at least 1 tag in addTags or removeTags";
|
|
23714
|
+
var BULK_TAG_KEYS = ["addTags", "removeTags"];
|
|
23715
|
+
var bulkIdsSchema = external_exports.array(idSchema, { message: BULK_IDS_MESSAGE }).min(1, { message: BULK_IDS_MESSAGE }).max(BULK_MAX, { message: BULK_IDS_MESSAGE }).transform((ids) => [...new Set(ids)]);
|
|
23716
|
+
var bulkChangeSchema = external_exports.preprocess((value) => dropNulls(normalizeKeys(value, STATUS_CHANGE_KEYS), BULK_TAG_KEYS), external_exports.strictObject({
|
|
23717
|
+
ids: bulkIdsSchema,
|
|
23718
|
+
status: statusChangeShape.status.optional(),
|
|
23719
|
+
resolution: statusChangeShape.resolution,
|
|
23720
|
+
reason: statusChangeShape.reason,
|
|
23721
|
+
duplicateOf: statusChangeShape.duplicateOf,
|
|
23722
|
+
addTags: tagsSchema.optional(),
|
|
23723
|
+
removeTags: tagsSchema.optional()
|
|
23724
|
+
}).superRefine((input2, ctx) => {
|
|
23725
|
+
const status = STATUS_CHANGE_KEYS.some((key) => input2[key] !== void 0);
|
|
23726
|
+
const tags = BULK_TAG_KEYS.some((key) => input2[key] !== void 0);
|
|
23727
|
+
if (status && tags) {
|
|
23728
|
+
ctx.addIssue({ code: "custom", message: BULK_BOTH_MESSAGE });
|
|
23729
|
+
return;
|
|
23730
|
+
}
|
|
23731
|
+
if (!status && !tags) {
|
|
23732
|
+
ctx.addIssue({ code: "custom", message: BULK_NEITHER_MESSAGE });
|
|
23733
|
+
return;
|
|
23734
|
+
}
|
|
23735
|
+
if (status) {
|
|
23736
|
+
if (input2.status === void 0) {
|
|
23737
|
+
ctx.addIssue({ code: "custom", path: ["status"], message: STATUS_MESSAGE });
|
|
23738
|
+
return;
|
|
23739
|
+
}
|
|
23740
|
+
checkReason({ ...input2, status: input2.status }, ctx);
|
|
23741
|
+
return;
|
|
23742
|
+
}
|
|
23743
|
+
const addTags = input2.addTags ?? [];
|
|
23744
|
+
const removeTags = input2.removeTags ?? [];
|
|
23745
|
+
if (addTags.length + removeTags.length === 0) {
|
|
23746
|
+
ctx.addIssue({ code: "custom", message: BULK_NO_TAG_MESSAGE });
|
|
23747
|
+
return;
|
|
23748
|
+
}
|
|
23749
|
+
for (const tag of removeTags) {
|
|
23750
|
+
if (!addTags.includes(tag))
|
|
23751
|
+
continue;
|
|
23752
|
+
ctx.addIssue({
|
|
23753
|
+
code: "custom",
|
|
23754
|
+
path: ["removeTags"],
|
|
23755
|
+
message: `${tag} is also in addTags`
|
|
23756
|
+
});
|
|
23757
|
+
}
|
|
23758
|
+
})).transform((input2) => {
|
|
23759
|
+
if (input2.status !== void 0) {
|
|
23760
|
+
return {
|
|
23761
|
+
ids: input2.ids,
|
|
23762
|
+
kind: "status",
|
|
23763
|
+
change: toStatusChange({ ...input2, status: input2.status })
|
|
23764
|
+
};
|
|
23765
|
+
}
|
|
23766
|
+
return {
|
|
23767
|
+
ids: input2.ids,
|
|
23768
|
+
kind: "tags",
|
|
23769
|
+
addTags: input2.addTags ?? [],
|
|
23770
|
+
removeTags: input2.removeTags ?? []
|
|
23771
|
+
};
|
|
23772
|
+
});
|
|
23773
|
+
var BEFORE_UNTIL_MESSAGE = "must be before until";
|
|
23774
|
+
function checkWindow(window, ctx) {
|
|
23775
|
+
const { since, until } = window;
|
|
23531
23776
|
if (since === void 0 || until === void 0)
|
|
23532
23777
|
return;
|
|
23533
23778
|
if (Date.parse(since) < Date.parse(until))
|
|
23534
23779
|
return;
|
|
23535
23780
|
ctx.addIssue({ code: "custom", path: ["since"], message: BEFORE_UNTIL_MESSAGE });
|
|
23536
23781
|
}
|
|
23537
|
-
var
|
|
23782
|
+
var filterTagsSchema = external_exports.array(tagSchema, { message: TAGS_TYPE_MESSAGE }).transform(normalizeTags).refine((tags) => tags.length > 0, { message: "must list at least 1 tag" }).refine((tags) => tags.length <= TAGS_MAX, {
|
|
23783
|
+
message: `must have at most ${TAGS_MAX} tags`
|
|
23784
|
+
});
|
|
23785
|
+
var FINGERPRINT_MESSAGE = lengthMessage(1, FINGERPRINT_MAX);
|
|
23786
|
+
var listFilterShape = {
|
|
23538
23787
|
org: orgSlugSchema,
|
|
23539
23788
|
project: projectNameSchema.optional(),
|
|
23540
23789
|
severity: severitySchema.optional(),
|
|
23541
23790
|
status: statusSchema.optional(),
|
|
23542
|
-
|
|
23543
|
-
tag: tagSchema.optional(),
|
|
23791
|
+
tags: filterTagsSchema.optional(),
|
|
23544
23792
|
reporter: boundedString(REPORTER_MAX).optional(),
|
|
23793
|
+
// Exact, so it is never trimmed, and never empty, because no fingerprint is.
|
|
23794
|
+
fingerprint: external_exports.string({ message: FINGERPRINT_MESSAGE }).min(1, { message: FINGERPRINT_MESSAGE }).max(FINGERPRINT_MAX, { message: FINGERPRINT_MESSAGE }).optional(),
|
|
23795
|
+
// The search is case-insensitive in every store.
|
|
23796
|
+
q: boundedString(SEARCH_MAX).transform((value) => value.toLowerCase()).optional(),
|
|
23545
23797
|
since: timestampSchema.optional(),
|
|
23546
23798
|
until: timestampSchema.optional(),
|
|
23547
23799
|
limit: limitSchema.optional()
|
|
23548
|
-
}
|
|
23549
|
-
var
|
|
23800
|
+
};
|
|
23801
|
+
var LIST_FILTER_KEYS = Object.keys(listFilterShape);
|
|
23802
|
+
var listFilterObject = external_exports.strictObject(listFilterShape).superRefine(checkWindow);
|
|
23803
|
+
var listFilterSchema = external_exports.preprocess((value) => dropNulls(normalizeKeys(value ?? {}, FILTER_KEYS), FILTER_NULLABLE_KEYS), listFilterObject).transform((input2) => ({
|
|
23550
23804
|
org: input2.org,
|
|
23551
23805
|
...input2.project === void 0 ? {} : { project: input2.project },
|
|
23552
23806
|
...input2.severity === void 0 ? {} : { severity: input2.severity },
|
|
23553
23807
|
...input2.status === void 0 ? {} : { status: input2.status },
|
|
23554
|
-
|
|
23555
|
-
...input2.tag === void 0 ? {} : { tag: normalizeTag(input2.tag) },
|
|
23808
|
+
...input2.tags === void 0 ? {} : { tags: input2.tags },
|
|
23556
23809
|
...input2.reporter === void 0 ? {} : { reporter: input2.reporter },
|
|
23810
|
+
...input2.fingerprint === void 0 ? {} : { fingerprint: input2.fingerprint },
|
|
23811
|
+
...input2.q === void 0 ? {} : { q: input2.q },
|
|
23557
23812
|
...input2.since === void 0 ? {} : { since: toMilliseconds(input2.since) },
|
|
23558
23813
|
...input2.until === void 0 ? {} : { until: toMilliseconds(input2.until) },
|
|
23559
23814
|
limit: input2.limit ?? DEFAULT_LIMIT
|
|
23560
23815
|
}));
|
|
23561
23816
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
23562
23817
|
var WINDOW_MESSAGE = `the window must not exceed ${STATS_MAX_WINDOW_DAYS} days`;
|
|
23563
|
-
var
|
|
23818
|
+
var ALL_WINDOW = "all";
|
|
23819
|
+
var ALL_WINDOW_MESSAGE = `must be ${ALL_WINDOW}`;
|
|
23820
|
+
var ALL_WINDOW_BOUNDS_MESSAGE = `${ALL_WINDOW} takes no since or until`;
|
|
23821
|
+
var statsQueryObject = external_exports.strictObject({
|
|
23564
23822
|
since: timestampSchema.optional(),
|
|
23565
23823
|
until: timestampSchema.optional(),
|
|
23566
|
-
status: statusSchema.optional()
|
|
23824
|
+
status: statusSchema.optional(),
|
|
23825
|
+
window: external_exports.literal(ALL_WINDOW, { message: ALL_WINDOW_MESSAGE }).optional()
|
|
23826
|
+
}).superRefine((input2, ctx) => {
|
|
23827
|
+
if (input2.window === void 0)
|
|
23828
|
+
return;
|
|
23829
|
+
if (input2.since === void 0 && input2.until === void 0)
|
|
23830
|
+
return;
|
|
23831
|
+
ctx.addIssue({
|
|
23832
|
+
code: "custom",
|
|
23833
|
+
path: ["window"],
|
|
23834
|
+
message: ALL_WINDOW_BOUNDS_MESSAGE
|
|
23835
|
+
});
|
|
23567
23836
|
});
|
|
23568
23837
|
var ROLE_MESSAGE = `must be one of ${ROLES.join(", ")}`;
|
|
23569
23838
|
var ORG_ROLE_MESSAGE = `must be one of ${ORG_ROLES.join(", ")}`;
|
|
@@ -23576,8 +23845,8 @@ var orgRoleSchema = external_exports.enum(ORG_ROLES, { message: ORG_ROLE_MESSAGE
|
|
|
23576
23845
|
var colourSchema = external_exports.enum(ORG_COLOURS, { message: COLOUR_MESSAGE });
|
|
23577
23846
|
var emailSchema = boundedString(EMAIL_MAX, EMAIL_MESSAGE).refine((value) => EMAIL_RE.test(value), { message: EMAIL_MESSAGE });
|
|
23578
23847
|
var inviteEmailSchema = emailSchema.transform((value) => value.toLowerCase());
|
|
23579
|
-
var displayNameSchema =
|
|
23580
|
-
var newOrgSchema = external_exports.preprocess((value) => normalizeKeys(value, ["colour"]), external_exports.
|
|
23848
|
+
var displayNameSchema = freeText(DISPLAY_NAME_MAX);
|
|
23849
|
+
var newOrgSchema = external_exports.preprocess((value) => normalizeKeys(value, ["colour"]), external_exports.strictObject({
|
|
23581
23850
|
slug: orgSlugSchema.refine((value) => !RESERVED_SLUGS.some((reserved) => reserved === value), { message: RESERVED_SLUG_MESSAGE }),
|
|
23582
23851
|
name: displayNameSchema,
|
|
23583
23852
|
colour: colourSchema.optional()
|
|
@@ -23586,18 +23855,21 @@ var newOrgSchema = external_exports.preprocess((value) => normalizeKeys(value, [
|
|
|
23586
23855
|
name: input2.name,
|
|
23587
23856
|
colour: input2.colour ?? DEFAULT_ORG_COLOUR
|
|
23588
23857
|
}));
|
|
23589
|
-
var orgPatchSchema = external_exports.preprocess((value) => normalizeKeys(value, ["name", "colour"]), external_exports.
|
|
23858
|
+
var orgPatchSchema = external_exports.preprocess((value) => normalizeKeys(value, ["name", "colour"]), external_exports.strictObject({
|
|
23859
|
+
name: displayNameSchema.optional(),
|
|
23860
|
+
colour: colourSchema.optional()
|
|
23861
|
+
})).transform((input2) => ({
|
|
23590
23862
|
...input2.name === void 0 ? {} : { name: input2.name },
|
|
23591
23863
|
...input2.colour === void 0 ? {} : { colour: input2.colour }
|
|
23592
23864
|
})).refine((patch) => Object.keys(patch).length > 0, { message: ORG_PATCH_MESSAGE });
|
|
23593
23865
|
var PLATFORM_PATCH_MESSAGE = "must set disabled, caps, or both";
|
|
23594
23866
|
var CAP_MESSAGE = "must be a positive whole number";
|
|
23595
|
-
var orgCapsSchema = external_exports.
|
|
23867
|
+
var orgCapsSchema = external_exports.strictObject({
|
|
23596
23868
|
projectsPerOrg: external_exports.number({ message: CAP_MESSAGE }).int({ message: CAP_MESSAGE }).min(1, { message: CAP_MESSAGE }).optional()
|
|
23597
23869
|
}).transform((input2) => ({
|
|
23598
23870
|
...input2.projectsPerOrg === void 0 ? {} : { projectsPerOrg: input2.projectsPerOrg }
|
|
23599
23871
|
}));
|
|
23600
|
-
var platformOrgPatchSchema = external_exports.
|
|
23872
|
+
var platformOrgPatchSchema = external_exports.strictObject({
|
|
23601
23873
|
disabled: external_exports.boolean({ message: "must be a boolean" }).optional(),
|
|
23602
23874
|
caps: orgCapsSchema.nullable().optional()
|
|
23603
23875
|
}).transform((input2) => ({
|
|
@@ -23606,30 +23878,41 @@ var platformOrgPatchSchema = external_exports.object({
|
|
|
23606
23878
|
})).refine((patch) => Object.keys(patch).length > 0, {
|
|
23607
23879
|
message: PLATFORM_PATCH_MESSAGE
|
|
23608
23880
|
});
|
|
23609
|
-
var orgInviteSchema = external_exports.
|
|
23610
|
-
var orgRoleChangeSchema = external_exports.
|
|
23611
|
-
var profileSchema = external_exports.preprocess((value) => normalizeKeys(value, ["name"]), external_exports.
|
|
23612
|
-
var newProjectSchema = external_exports.preprocess((value) => normalizeKeys(value, ["tags"]), external_exports.
|
|
23881
|
+
var orgInviteSchema = external_exports.strictObject({ email: inviteEmailSchema, role: orgRoleSchema }).transform((input2) => ({ email: input2.email, role: input2.role }));
|
|
23882
|
+
var orgRoleChangeSchema = external_exports.strictObject({ role: orgRoleSchema }).transform((input2) => ({ role: input2.role }));
|
|
23883
|
+
var profileSchema = external_exports.preprocess((value) => normalizeKeys(value, ["name"]), external_exports.strictObject({ name: displayNameSchema })).transform((input2) => ({ name: input2.name }));
|
|
23884
|
+
var newProjectSchema = external_exports.preprocess((value) => normalizeKeys(value, ["tags"]), external_exports.strictObject({
|
|
23613
23885
|
project: projectNameSchema,
|
|
23614
23886
|
tags: allowedTagsSchema.optional()
|
|
23615
23887
|
})).transform((input2) => ({
|
|
23616
23888
|
project: input2.project,
|
|
23617
23889
|
...input2.tags === void 0 ? {} : { tags: input2.tags }
|
|
23618
23890
|
}));
|
|
23619
|
-
var projectUpdateSchema = external_exports.
|
|
23620
|
-
var tagPromotionSchema = external_exports.
|
|
23621
|
-
var roleChangeSchema = external_exports.
|
|
23622
|
-
var
|
|
23623
|
-
|
|
23624
|
-
expiresAt
|
|
23625
|
-
|
|
23626
|
-
|
|
23627
|
-
|
|
23628
|
-
}))
|
|
23891
|
+
var projectUpdateSchema = external_exports.strictObject({ tags: allowedTagsSchema.nullable() }).transform((input2) => ({ tags: input2.tags }));
|
|
23892
|
+
var tagPromotionSchema = external_exports.strictObject({ tag: allowedTagSchema }).transform((input2) => ({ tag: normalizeTag(input2.tag) }));
|
|
23893
|
+
var roleChangeSchema = external_exports.strictObject({ role: roleSchema }).transform((input2) => ({ role: input2.role }));
|
|
23894
|
+
var FUTURE_MESSAGE = "must be in the future";
|
|
23895
|
+
function newTokenSchema(now) {
|
|
23896
|
+
return external_exports.preprocess((value) => normalizeKeys(value, ["name", "expiresAt"]), external_exports.strictObject({
|
|
23897
|
+
name: freeText(TOKEN_NAME_MAX),
|
|
23898
|
+
expiresAt: timestampSchema.refine((value) => Date.parse(value) > now.getTime(), {
|
|
23899
|
+
message: FUTURE_MESSAGE
|
|
23900
|
+
}).optional()
|
|
23901
|
+
})).transform((input2) => ({
|
|
23902
|
+
name: input2.name,
|
|
23903
|
+
...input2.expiresAt === void 0 ? {} : { expiresAt: input2.expiresAt }
|
|
23904
|
+
}));
|
|
23905
|
+
}
|
|
23906
|
+
var UNKNOWN_KEY_MESSAGE = "unknown key";
|
|
23907
|
+
var emptyBodySchema = external_exports.strictObject({});
|
|
23629
23908
|
function formatIssues(error62) {
|
|
23630
|
-
return error62.issues.
|
|
23631
|
-
const path = issue2.path.map((part) => String(part))
|
|
23632
|
-
|
|
23909
|
+
return error62.issues.flatMap((issue2) => {
|
|
23910
|
+
const path = issue2.path.map((part) => String(part));
|
|
23911
|
+
if (issue2.code === "unrecognized_keys") {
|
|
23912
|
+
return issue2.keys.map((key) => `${[...path, key].join(".")}: ${UNKNOWN_KEY_MESSAGE}`);
|
|
23913
|
+
}
|
|
23914
|
+
const name = path.join(".");
|
|
23915
|
+
return [name === "" ? issue2.message : `${name}: ${issue2.message}`];
|
|
23633
23916
|
});
|
|
23634
23917
|
}
|
|
23635
23918
|
function run(schema, input2) {
|
|
@@ -23648,6 +23931,13 @@ function parseComplaint(input2) {
|
|
|
23648
23931
|
function parseStatusChange(input2) {
|
|
23649
23932
|
return run(statusChangeSchema, input2);
|
|
23650
23933
|
}
|
|
23934
|
+
var statusActorSchema = external_exports.strictObject({ by: actorSchema, byName: actorSchema.optional() }).transform((input2) => ({
|
|
23935
|
+
by: input2.by,
|
|
23936
|
+
...input2.byName === void 0 ? {} : { byName: input2.byName }
|
|
23937
|
+
}));
|
|
23938
|
+
function parseStatusActor(input2) {
|
|
23939
|
+
return run(statusActorSchema, input2);
|
|
23940
|
+
}
|
|
23651
23941
|
function parseTagsChange(input2) {
|
|
23652
23942
|
return run(tagsChangeSchema, input2);
|
|
23653
23943
|
}
|
|
@@ -23663,6 +23953,14 @@ function parseProjectName(input2) {
|
|
|
23663
23953
|
function parseTagPromotion(input2) {
|
|
23664
23954
|
return run(tagPromotionSchema, input2);
|
|
23665
23955
|
}
|
|
23956
|
+
function parseNewToken(input2, now = /* @__PURE__ */ new Date()) {
|
|
23957
|
+
return run(newTokenSchema(now), input2);
|
|
23958
|
+
}
|
|
23959
|
+
function parseAgentName(input2) {
|
|
23960
|
+
return run(external_exports.object({ agent: agentSchema }), { agent: input2 }).agent;
|
|
23961
|
+
}
|
|
23962
|
+
var IDEMPOTENCY_KEY_RE = new RegExp(`^[A-Za-z0-9_-]{1,${IDEMPOTENCY_KEY_MAX}}$`);
|
|
23963
|
+
var IDEMPOTENCY_KEY_MESSAGE = `Idempotency-Key: must be 1-${IDEMPOTENCY_KEY_MAX} letters, digits, "-" or "_"`;
|
|
23666
23964
|
function parseOrgSlug(input2) {
|
|
23667
23965
|
return run(orgSlugSchema, input2);
|
|
23668
23966
|
}
|
|
@@ -23718,8 +24016,8 @@ function summarizeComplaints(complaints) {
|
|
|
23718
24016
|
var SUMMARY_WORDS = 20;
|
|
23719
24017
|
var ELLIPSIS = "\u2026";
|
|
23720
24018
|
function summarize(message) {
|
|
23721
|
-
const line = message
|
|
23722
|
-
if (line ===
|
|
24019
|
+
const line = headlineOf(message);
|
|
24020
|
+
if (line === "")
|
|
23723
24021
|
return "";
|
|
23724
24022
|
const words = line.split(/\s+/);
|
|
23725
24023
|
if (words.length <= SUMMARY_WORDS)
|
|
@@ -23727,8 +24025,29 @@ function summarize(message) {
|
|
|
23727
24025
|
return `${words.slice(0, SUMMARY_WORDS).join(" ")}${ELLIPSIS}`;
|
|
23728
24026
|
}
|
|
23729
24027
|
|
|
24028
|
+
// ../core/dist/query.js
|
|
24029
|
+
var TAG_KEY = "tag";
|
|
24030
|
+
var SINGLE_KEYS = new Set(LIST_FILTER_KEYS.filter((key) => key !== "org" && key !== "tags"));
|
|
24031
|
+
function listQueryOf(filter) {
|
|
24032
|
+
const pairs = [];
|
|
24033
|
+
for (const key of LIST_FILTER_KEYS) {
|
|
24034
|
+
if (key === "org")
|
|
24035
|
+
continue;
|
|
24036
|
+
const value = filter[key];
|
|
24037
|
+
if (value === void 0)
|
|
24038
|
+
continue;
|
|
24039
|
+
if (key === "tags") {
|
|
24040
|
+
for (const tag of filter.tags ?? [])
|
|
24041
|
+
pairs.push([TAG_KEY, tag]);
|
|
24042
|
+
continue;
|
|
24043
|
+
}
|
|
24044
|
+
pairs.push([key, String(value)]);
|
|
24045
|
+
}
|
|
24046
|
+
return pairs;
|
|
24047
|
+
}
|
|
24048
|
+
|
|
23730
24049
|
// ../core/dist/service.js
|
|
23731
|
-
import { randomUUID } from "node:crypto";
|
|
24050
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
23732
24051
|
function applyTagRules(tags, allowedTags) {
|
|
23733
24052
|
if (allowedTags === void 0)
|
|
23734
24053
|
return [...tags];
|
|
@@ -23744,23 +24063,57 @@ async function createComplaint(store, input2, options = {}) {
|
|
|
23744
24063
|
id: options.id === void 0 ? randomUUID() : options.id(),
|
|
23745
24064
|
...normalized,
|
|
23746
24065
|
tags,
|
|
24066
|
+
fingerprint: fingerprintOf(normalized.message),
|
|
23747
24067
|
createdAt: stamp(options),
|
|
23748
24068
|
status: DEFAULT_STATUS
|
|
23749
24069
|
};
|
|
23750
|
-
|
|
23751
|
-
|
|
24070
|
+
if (options.reserve !== void 0)
|
|
24071
|
+
await options.reserve(complaint);
|
|
24072
|
+
const match = options.keyed === true ? await store.create(complaint, { keyed: true }) : await store.create(complaint);
|
|
24073
|
+
return {
|
|
24074
|
+
...complaint,
|
|
24075
|
+
openMatches: match.open,
|
|
24076
|
+
recurred: match.resolved
|
|
24077
|
+
};
|
|
23752
24078
|
}
|
|
23753
24079
|
async function listComplaints(store, filter) {
|
|
23754
24080
|
const parsed = parseListFilter(filter);
|
|
23755
24081
|
return store.list(parsed);
|
|
23756
24082
|
}
|
|
24083
|
+
async function listComplaintPage(store, filter) {
|
|
24084
|
+
const parsed = parseListFilter(filter);
|
|
24085
|
+
if (store.listPage !== void 0)
|
|
24086
|
+
return store.listPage(parsed);
|
|
24087
|
+
return { complaints: await store.list(parsed), truncated: false };
|
|
24088
|
+
}
|
|
24089
|
+
function scanComplaints(store, filter) {
|
|
24090
|
+
return store.scan(parseListFilter(filter));
|
|
24091
|
+
}
|
|
23757
24092
|
async function getComplaint(store, id) {
|
|
23758
24093
|
return store.get(id);
|
|
23759
24094
|
}
|
|
23760
|
-
async function updateComplaintStatus(store, id, change, options
|
|
24095
|
+
async function updateComplaintStatus(store, id, change, options) {
|
|
23761
24096
|
const parsed = parseStatusChange(change);
|
|
23762
|
-
const
|
|
23763
|
-
|
|
24097
|
+
const actor = parseStatusActor(options.actor);
|
|
24098
|
+
if (parsed.duplicateOf !== void 0) {
|
|
24099
|
+
const target = options.current ?? await store.get(id);
|
|
24100
|
+
if (target === void 0)
|
|
24101
|
+
return void 0;
|
|
24102
|
+
if (parsed.duplicateOf === id) {
|
|
24103
|
+
throw new ValidationError(["duplicateOf: must name another complaint"]);
|
|
24104
|
+
}
|
|
24105
|
+
const original = await store.get(parsed.duplicateOf);
|
|
24106
|
+
if (original === void 0 || original.org !== target.org) {
|
|
24107
|
+
throw new ValidationError([`duplicateOf: no complaint ${parsed.duplicateOf}`]);
|
|
24108
|
+
}
|
|
24109
|
+
}
|
|
24110
|
+
const update = {
|
|
24111
|
+
...parsed,
|
|
24112
|
+
updatedAt: stamp(options),
|
|
24113
|
+
changedBy: actor.by,
|
|
24114
|
+
...actor.byName === void 0 ? {} : { changedByName: actor.byName }
|
|
24115
|
+
};
|
|
24116
|
+
return store.update(id, update, options.current);
|
|
23764
24117
|
}
|
|
23765
24118
|
async function updateComplaintTags(store, id, change, options = {}) {
|
|
23766
24119
|
const parsed = parseTagsChange(change);
|
|
@@ -23768,7 +24121,7 @@ async function updateComplaintTags(store, id, change, options = {}) {
|
|
|
23768
24121
|
tags: applyTagRules(parsed.tags, options.allowedTags),
|
|
23769
24122
|
updatedAt: stamp(options)
|
|
23770
24123
|
};
|
|
23771
|
-
return store.updateTags(id, update);
|
|
24124
|
+
return store.updateTags(id, update, options.current);
|
|
23772
24125
|
}
|
|
23773
24126
|
|
|
23774
24127
|
// ../core/dist/file-store.js
|
|
@@ -23776,15 +24129,36 @@ import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
|
23776
24129
|
import { dirname } from "node:path";
|
|
23777
24130
|
var STATUS_OP = "status";
|
|
23778
24131
|
var TAGS_OP = "tags";
|
|
24132
|
+
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
23779
24133
|
var FileStore = class {
|
|
23780
24134
|
/** Path of the JSONL file. */
|
|
23781
24135
|
path;
|
|
24136
|
+
/** The creates of this process, one after the other. */
|
|
24137
|
+
#creating = Promise.resolve();
|
|
23782
24138
|
constructor(path) {
|
|
23783
24139
|
this.path = path;
|
|
23784
24140
|
}
|
|
23785
|
-
/**
|
|
23786
|
-
|
|
23787
|
-
|
|
24141
|
+
/**
|
|
24142
|
+
* Counts what the complaint's project holds under its fingerprint, from the
|
|
24143
|
+
* folded records, then appends the one complaint line, with `recurred: true`
|
|
24144
|
+
* when a resolved match created in the last `RECURRENCE_WINDOW_DAYS` exists.
|
|
24145
|
+
* Creates the parent directory on the first write. A keyed complaint whose
|
|
24146
|
+
* id a record already holds is a `DuplicateIdError` carrying that record,
|
|
24147
|
+
* and nothing is written. The creates of one process run one at a time.
|
|
24148
|
+
*/
|
|
24149
|
+
create(complaint, options = {}) {
|
|
24150
|
+
const write = this.#creating.then(async () => {
|
|
24151
|
+
const records = await this.#readAll();
|
|
24152
|
+
const existing = records.find((record2) => record2.id === complaint.id);
|
|
24153
|
+
if (options.keyed === true && existing !== void 0) {
|
|
24154
|
+
throw new DuplicateIdError(complaint.id, existing);
|
|
24155
|
+
}
|
|
24156
|
+
const match = matchOf(records, complaint);
|
|
24157
|
+
await this.#append(match.resolved ? { ...complaint, recurred: true } : complaint);
|
|
24158
|
+
return match;
|
|
24159
|
+
});
|
|
24160
|
+
this.#creating = write.catch(() => void 0);
|
|
24161
|
+
return write;
|
|
23788
24162
|
}
|
|
23789
24163
|
/** Returns the matching complaints, newest first. */
|
|
23790
24164
|
async list(filter) {
|
|
@@ -23796,6 +24170,11 @@ var FileStore = class {
|
|
|
23796
24170
|
for (const complaint of await this.#matches(filter))
|
|
23797
24171
|
yield complaint;
|
|
23798
24172
|
}
|
|
24173
|
+
/** Returns how many complaints match, whatever the limit says. */
|
|
24174
|
+
async count(filter) {
|
|
24175
|
+
const complaints = await this.#readAll();
|
|
24176
|
+
return complaints.filter((complaint) => matchesFilter(complaint, filter)).length;
|
|
24177
|
+
}
|
|
23799
24178
|
/** Returns the complaint with that id, or `undefined`. */
|
|
23800
24179
|
async get(id) {
|
|
23801
24180
|
const complaints = await this.#readAll();
|
|
@@ -23804,26 +24183,32 @@ var FileStore = class {
|
|
|
23804
24183
|
/**
|
|
23805
24184
|
* Appends one status line and returns the updated complaint. Returns
|
|
23806
24185
|
* `undefined`, and writes nothing, when no complaint has that id. A deleted
|
|
23807
|
-
* complaint is reached like any other, so a reopen restores it.
|
|
24186
|
+
* complaint is reached like any other, so a reopen restores it. `current` is
|
|
24187
|
+
* ignored, because a fold reads every record anyway.
|
|
23808
24188
|
*/
|
|
23809
|
-
async update(id, change) {
|
|
24189
|
+
async update(id, change, _current) {
|
|
23810
24190
|
const complaint = await this.get(id);
|
|
23811
24191
|
if (complaint === void 0)
|
|
23812
24192
|
return void 0;
|
|
23813
|
-
|
|
24193
|
+
const line = {
|
|
23814
24194
|
op: STATUS_OP,
|
|
23815
24195
|
id,
|
|
23816
24196
|
status: change.status,
|
|
23817
24197
|
...change.resolution === void 0 ? {} : { resolution: change.resolution },
|
|
24198
|
+
...change.reason === void 0 ? {} : { reason: change.reason },
|
|
24199
|
+
...change.duplicateOf === void 0 ? {} : { duplicateOf: change.duplicateOf },
|
|
24200
|
+
changedBy: change.changedBy,
|
|
24201
|
+
...change.changedByName === void 0 ? {} : { changedByName: change.changedByName },
|
|
23818
24202
|
updatedAt: change.updatedAt
|
|
23819
|
-
}
|
|
23820
|
-
|
|
24203
|
+
};
|
|
24204
|
+
await this.#append(line);
|
|
24205
|
+
return applyStatus(complaint, line);
|
|
23821
24206
|
}
|
|
23822
24207
|
/**
|
|
23823
24208
|
* Appends one tags line and returns the updated complaint. Returns
|
|
23824
24209
|
* `undefined`, and writes nothing, when no complaint has that id.
|
|
23825
24210
|
*/
|
|
23826
|
-
async updateTags(id, change) {
|
|
24211
|
+
async updateTags(id, change, _current) {
|
|
23827
24212
|
const complaint = await this.get(id);
|
|
23828
24213
|
if (complaint === void 0)
|
|
23829
24214
|
return void 0;
|
|
@@ -23946,31 +24331,70 @@ var FileStore = class {
|
|
|
23946
24331
|
}
|
|
23947
24332
|
const change = parseStatusChange({
|
|
23948
24333
|
status: parsed["status"],
|
|
23949
|
-
resolution: parsed["resolution"]
|
|
24334
|
+
resolution: parsed["resolution"],
|
|
24335
|
+
reason: parsed["reason"],
|
|
24336
|
+
duplicateOf: parsed["duplicateOf"]
|
|
23950
24337
|
});
|
|
23951
|
-
|
|
24338
|
+
const changedBy = this.#actor(parsed, "changedBy", bad);
|
|
24339
|
+
const changedByName = this.#actor(parsed, "changedByName", bad);
|
|
24340
|
+
return {
|
|
24341
|
+
op,
|
|
24342
|
+
id,
|
|
24343
|
+
...change,
|
|
24344
|
+
...changedBy === void 0 ? {} : { changedBy },
|
|
24345
|
+
...changedByName === void 0 ? {} : { changedByName },
|
|
24346
|
+
updatedAt
|
|
24347
|
+
};
|
|
23952
24348
|
} catch (error62) {
|
|
23953
24349
|
if (!(error62 instanceof ValidationError))
|
|
23954
24350
|
throw error62;
|
|
23955
24351
|
throw bad(error62.message, error62);
|
|
23956
24352
|
}
|
|
23957
24353
|
}
|
|
24354
|
+
/** Reads the optional actor key of a status line. */
|
|
24355
|
+
#actor(parsed, key, bad) {
|
|
24356
|
+
const value = parsed[key];
|
|
24357
|
+
if (value === void 0)
|
|
24358
|
+
return void 0;
|
|
24359
|
+
if (typeof value !== "string" || value.trim() === "" || value.length > REPORTER_MAX) {
|
|
24360
|
+
throw bad(`${key}: must be 1-${REPORTER_MAX} characters`);
|
|
24361
|
+
}
|
|
24362
|
+
return value;
|
|
24363
|
+
}
|
|
23958
24364
|
/** The prefix that every malformed complaint line shares. */
|
|
23959
24365
|
#malformed(lineNumber) {
|
|
23960
24366
|
return `malformed complaint on line ${lineNumber} of ${this.path}`;
|
|
23961
24367
|
}
|
|
23962
24368
|
};
|
|
23963
24369
|
function applyStatus(complaint, change) {
|
|
23964
|
-
const
|
|
23965
|
-
|
|
24370
|
+
const { resolution: _resolution, reason: _reason, duplicateOf: _duplicateOf, statusChangedBy: _statusChangedBy, statusChangedByName: _statusChangedByName, ...rest } = complaint;
|
|
24371
|
+
return {
|
|
24372
|
+
...rest,
|
|
23966
24373
|
status: change.status,
|
|
24374
|
+
...change.resolution === void 0 ? {} : { resolution: change.resolution },
|
|
24375
|
+
...change.reason === void 0 ? {} : { reason: change.reason },
|
|
24376
|
+
...change.duplicateOf === void 0 ? {} : { duplicateOf: change.duplicateOf },
|
|
24377
|
+
statusChangedAt: change.updatedAt,
|
|
24378
|
+
...change.changedBy === void 0 ? {} : { statusChangedBy: change.changedBy },
|
|
24379
|
+
...change.changedByName === void 0 ? {} : { statusChangedByName: change.changedByName },
|
|
23967
24380
|
updatedAt: change.updatedAt
|
|
23968
24381
|
};
|
|
23969
|
-
|
|
23970
|
-
|
|
23971
|
-
|
|
23972
|
-
|
|
23973
|
-
|
|
24382
|
+
}
|
|
24383
|
+
function matchOf(complaints, complaint) {
|
|
24384
|
+
const since = new Date(Date.parse(complaint.createdAt) - RECURRENCE_WINDOW_DAYS * DAY_MS2).toISOString();
|
|
24385
|
+
let open2 = 0;
|
|
24386
|
+
let resolved = false;
|
|
24387
|
+
for (const each of complaints) {
|
|
24388
|
+
if (each.project !== complaint.project)
|
|
24389
|
+
continue;
|
|
24390
|
+
if (each.fingerprint !== complaint.fingerprint)
|
|
24391
|
+
continue;
|
|
24392
|
+
if (each.status === "open")
|
|
24393
|
+
open2 += 1;
|
|
24394
|
+
if (each.status === "resolved" && each.createdAt >= since)
|
|
24395
|
+
resolved = true;
|
|
24396
|
+
}
|
|
24397
|
+
return { open: open2, resolved };
|
|
23974
24398
|
}
|
|
23975
24399
|
function applyTags(complaint, change) {
|
|
23976
24400
|
return { ...complaint, tags: [...change.tags], updatedAt: change.updatedAt };
|
|
@@ -23990,11 +24414,17 @@ function matchesFilter(complaint, filter) {
|
|
|
23990
24414
|
if (filter.reporter !== void 0 && complaint.reporter !== filter.reporter) {
|
|
23991
24415
|
return false;
|
|
23992
24416
|
}
|
|
23993
|
-
if (filter.
|
|
23994
|
-
const kept = filter.tag === UNTAGGED_TAG ? complaint.tags.length === 0 : complaint.tags.includes(
|
|
24417
|
+
if (filter.tags !== void 0) {
|
|
24418
|
+
const kept = filter.tags.some((tag) => tag === UNTAGGED_TAG ? complaint.tags.length === 0 : complaint.tags.includes(tag));
|
|
23995
24419
|
if (!kept)
|
|
23996
24420
|
return false;
|
|
23997
24421
|
}
|
|
24422
|
+
if (filter.fingerprint !== void 0 && complaint.fingerprint !== filter.fingerprint) {
|
|
24423
|
+
return false;
|
|
24424
|
+
}
|
|
24425
|
+
if (filter.q !== void 0 && !complaint.message.toLowerCase().includes(filter.q)) {
|
|
24426
|
+
return false;
|
|
24427
|
+
}
|
|
23998
24428
|
if (filter.since !== void 0 && complaint.createdAt < filter.since)
|
|
23999
24429
|
return false;
|
|
24000
24430
|
if (filter.until !== void 0 && complaint.createdAt >= filter.until)
|
|
@@ -24028,6 +24458,10 @@ function defaultConfigPath(env = process.env) {
|
|
|
24028
24458
|
return join(defaultQuibbleHome(env), CONFIG_FILENAME);
|
|
24029
24459
|
}
|
|
24030
24460
|
|
|
24461
|
+
// src/config.ts
|
|
24462
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
24463
|
+
import { dirname as dirname3, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
|
|
24464
|
+
|
|
24031
24465
|
// src/credentials.ts
|
|
24032
24466
|
import { randomBytes } from "node:crypto";
|
|
24033
24467
|
import {
|
|
@@ -24041,9 +24475,10 @@ import {
|
|
|
24041
24475
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
24042
24476
|
|
|
24043
24477
|
// src/errors.ts
|
|
24478
|
+
var AMBIGUOUS_ID = "ambiguous_id";
|
|
24044
24479
|
var UsageError = class extends QuibbleError {
|
|
24045
24480
|
constructor(message, options) {
|
|
24046
|
-
super("usage_error", message, options);
|
|
24481
|
+
super(options?.code ?? "usage_error", message, options);
|
|
24047
24482
|
}
|
|
24048
24483
|
};
|
|
24049
24484
|
var NotFoundError = class extends QuibbleError {
|
|
@@ -24058,9 +24493,18 @@ var RemoteError = class extends QuibbleError {
|
|
|
24058
24493
|
* other one stays exit 2.
|
|
24059
24494
|
*/
|
|
24060
24495
|
status;
|
|
24496
|
+
/** The `error.code` of the answer, when it carried one. */
|
|
24497
|
+
serverCode;
|
|
24498
|
+
/** The server's own message, when the answer carried one. */
|
|
24499
|
+
serverMessage;
|
|
24500
|
+
/** True when no server answered, which the offline queue of 5.13 reads. */
|
|
24501
|
+
unanswered;
|
|
24061
24502
|
constructor(message, options) {
|
|
24062
24503
|
super("network_error", message, options);
|
|
24063
24504
|
this.status = options?.status;
|
|
24505
|
+
this.serverCode = options?.serverCode;
|
|
24506
|
+
this.serverMessage = options?.serverMessage;
|
|
24507
|
+
this.unanswered = options?.unanswered === true;
|
|
24064
24508
|
}
|
|
24065
24509
|
};
|
|
24066
24510
|
function exitCodeFor(error62) {
|
|
@@ -24117,12 +24561,12 @@ async function send(url2, request) {
|
|
|
24117
24561
|
if (isTimeout(error62)) {
|
|
24118
24562
|
throw new RemoteError(
|
|
24119
24563
|
`request to ${originOf(url2)} timed out after ${timeoutMs / 1e3}s${detail(site)}`,
|
|
24120
|
-
{ cause: error62 }
|
|
24564
|
+
{ cause: error62, unanswered: true }
|
|
24121
24565
|
);
|
|
24122
24566
|
}
|
|
24123
24567
|
throw new RemoteError(
|
|
24124
24568
|
`cannot reach ${originOf(url2)}: ${describeError(error62)}${detail(site)}`,
|
|
24125
|
-
{ cause: error62 }
|
|
24569
|
+
{ cause: error62, unanswered: true }
|
|
24126
24570
|
);
|
|
24127
24571
|
}
|
|
24128
24572
|
}
|
|
@@ -24132,7 +24576,7 @@ async function readText(response, site) {
|
|
|
24132
24576
|
} catch (error62) {
|
|
24133
24577
|
throw new RemoteError(
|
|
24134
24578
|
`cannot read the response from ${originOf(site.url)}: ${describeError(error62)}${detail(site, response.status)}`,
|
|
24135
|
-
{ cause: error62 }
|
|
24579
|
+
{ cause: error62, unanswered: true }
|
|
24136
24580
|
);
|
|
24137
24581
|
}
|
|
24138
24582
|
}
|
|
@@ -24140,7 +24584,7 @@ function parseJson(text, site, status) {
|
|
|
24140
24584
|
try {
|
|
24141
24585
|
return JSON.parse(text);
|
|
24142
24586
|
} catch (error62) {
|
|
24143
|
-
throw new RemoteError(`the API returned
|
|
24587
|
+
throw new RemoteError(`the API returned invalid JSON${detail(site, status)}`, {
|
|
24144
24588
|
cause: error62
|
|
24145
24589
|
});
|
|
24146
24590
|
}
|
|
@@ -24176,9 +24620,12 @@ function serverCode(text) {
|
|
|
24176
24620
|
return stringOf(asRecord(record2?.["error"]), "code");
|
|
24177
24621
|
}
|
|
24178
24622
|
function remoteFailure(site, response, text) {
|
|
24179
|
-
const
|
|
24623
|
+
const own2 = serverMessage(text, response);
|
|
24624
|
+
const message = own2 ?? `the API returned HTTP ${response.status}`;
|
|
24180
24625
|
return new RemoteError(`${message}${detail(site, response.status)}`, {
|
|
24181
|
-
status: response.status
|
|
24626
|
+
status: response.status,
|
|
24627
|
+
serverCode: serverCode(text),
|
|
24628
|
+
serverMessage: own2
|
|
24182
24629
|
});
|
|
24183
24630
|
}
|
|
24184
24631
|
|
|
@@ -24283,12 +24730,12 @@ function removeCredential(env, endpoint) {
|
|
|
24283
24730
|
var TARGET_PREFIX = "AWSCognitoIdentityProviderService.";
|
|
24284
24731
|
var CONTENT_TYPE = "application/x-amz-json-1.1";
|
|
24285
24732
|
var DEFAULT_EXPIRES_IN = 3600;
|
|
24286
|
-
var UNREACHABLE_LINE = "
|
|
24733
|
+
var UNREACHABLE_LINE = "Couldn't reach the sign-in service.";
|
|
24287
24734
|
var WRONG_CREDENTIALS_LINE = "Wrong email or password.";
|
|
24288
|
-
var TOO_MANY_LINE = "Too many attempts
|
|
24735
|
+
var TOO_MANY_LINE = "Too many attempts. Try again later.";
|
|
24289
24736
|
var WRONG_CODE_LINE = "Wrong code.";
|
|
24290
|
-
var EXPIRED_CODE_LINE = "That code has expired
|
|
24291
|
-
var PASSWORD_POLICY_LINE = "
|
|
24737
|
+
var EXPIRED_CODE_LINE = "That code has expired. Request a new one.";
|
|
24738
|
+
var PASSWORD_POLICY_LINE = "Use at least 12 characters, with an uppercase letter, a lowercase letter, a number and a symbol.";
|
|
24292
24739
|
var UNREACHABLE_TYPE = "Unreachable";
|
|
24293
24740
|
var INCORRECT_CREDENTIALS_MESSAGE = "Incorrect username or password.";
|
|
24294
24741
|
var CognitoError = class extends RemoteError {
|
|
@@ -24297,7 +24744,7 @@ var CognitoError = class extends RemoteError {
|
|
|
24297
24744
|
/** Cognito's own `message`, or `undefined` when the answer carried none. */
|
|
24298
24745
|
detail;
|
|
24299
24746
|
constructor(type, detail2, line, suffix = "") {
|
|
24300
|
-
super(`${line}${suffix}
|
|
24747
|
+
super(`${line}${suffix}`, { unanswered: type === UNREACHABLE_TYPE });
|
|
24301
24748
|
this.type = type;
|
|
24302
24749
|
this.detail = detail2;
|
|
24303
24750
|
}
|
|
@@ -24394,7 +24841,7 @@ function authAnswerOf(body) {
|
|
|
24394
24841
|
const shape = result;
|
|
24395
24842
|
const accessToken = shape["AccessToken"];
|
|
24396
24843
|
if (typeof accessToken !== "string" || accessToken === "") {
|
|
24397
|
-
throw new RemoteError("
|
|
24844
|
+
throw new RemoteError("sign-in didn't return an access token");
|
|
24398
24845
|
}
|
|
24399
24846
|
const refreshToken = shape["RefreshToken"];
|
|
24400
24847
|
const expiresIn = shape["ExpiresIn"];
|
|
@@ -24409,7 +24856,7 @@ function authAnswerOf(body) {
|
|
|
24409
24856
|
}
|
|
24410
24857
|
const challengeName = body["ChallengeName"];
|
|
24411
24858
|
if (typeof challengeName !== "string" || challengeName === "") {
|
|
24412
|
-
throw new RemoteError("
|
|
24859
|
+
throw new RemoteError("sign-in returned an unexpected response");
|
|
24413
24860
|
}
|
|
24414
24861
|
const session = body["Session"];
|
|
24415
24862
|
const parameters = body["ChallengeParameters"];
|
|
@@ -24496,7 +24943,7 @@ async function fetchAuthConfig(endpoint, options = {}) {
|
|
|
24496
24943
|
const webUrl = stringOf(body, "webUrl");
|
|
24497
24944
|
if (region === void 0 || cliClientId === void 0 || webUrl === void 0) {
|
|
24498
24945
|
throw new RemoteError(
|
|
24499
|
-
`the API
|
|
24946
|
+
`unexpected response from the API: auth config has no region${detail(site, response.status)}`
|
|
24500
24947
|
);
|
|
24501
24948
|
}
|
|
24502
24949
|
return { mode: "cognito", region, cliClientId, webUrl };
|
|
@@ -24601,7 +25048,9 @@ var SessionAuthorizer = class {
|
|
|
24601
25048
|
throw error62;
|
|
24602
25049
|
}
|
|
24603
25050
|
if (answer2.kind === "challenge") {
|
|
24604
|
-
throw new RemoteError(
|
|
25051
|
+
throw new RemoteError(
|
|
25052
|
+
`unexpected sign-in step during refresh (${answer2.challenge.challengeName})`
|
|
25053
|
+
);
|
|
24605
25054
|
}
|
|
24606
25055
|
const now = clockOf(this.#options)();
|
|
24607
25056
|
return saveCredential(this.#options.env, {
|
|
@@ -24764,6 +25213,7 @@ function resolveContext(options) {
|
|
|
24764
25213
|
return {
|
|
24765
25214
|
project: name,
|
|
24766
25215
|
org: mode === "local" ? LOCAL_ORG : slug,
|
|
25216
|
+
slug,
|
|
24767
25217
|
projectRoot: project?.root,
|
|
24768
25218
|
configPath: project?.path,
|
|
24769
25219
|
cwd: project === void 0 ? void 0 : relativeCwd(project.root, options.cwd),
|
|
@@ -24786,6 +25236,79 @@ function resolveContext(options) {
|
|
|
24786
25236
|
};
|
|
24787
25237
|
}
|
|
24788
25238
|
|
|
25239
|
+
// src/ids.ts
|
|
25240
|
+
var FULL_ID_LENGTH = 36;
|
|
25241
|
+
var SHORT_ID_MIN = 6;
|
|
25242
|
+
var CANDIDATES_SHOWN = 5;
|
|
25243
|
+
var SHORT_ID_RE = /^[0-9a-f-]+$/;
|
|
25244
|
+
function readIdValue(value) {
|
|
25245
|
+
const trimmed = value.trim();
|
|
25246
|
+
if (trimmed.length === FULL_ID_LENGTH) return { id: trimmed, prefix: false };
|
|
25247
|
+
const lower = trimmed.toLowerCase();
|
|
25248
|
+
if (!SHORT_ID_RE.test(lower)) throw new UsageError(`${value} is not an id`);
|
|
25249
|
+
if (lower.length < SHORT_ID_MIN) {
|
|
25250
|
+
throw new UsageError(
|
|
25251
|
+
`${value} is too short: pass at least ${SHORT_ID_MIN} characters of the id`
|
|
25252
|
+
);
|
|
25253
|
+
}
|
|
25254
|
+
return { id: lower, prefix: true };
|
|
25255
|
+
}
|
|
25256
|
+
function ambiguousError(prefix, ids, noun) {
|
|
25257
|
+
const shown = ids.slice(0, CANDIDATES_SHOWN).join(", ");
|
|
25258
|
+
const more = ids.length > CANDIDATES_SHOWN ? ` and ${ids.length - CANDIDATES_SHOWN} more` : "";
|
|
25259
|
+
return new UsageError(`${prefix} matches ${ids.length} ${noun}: ${shown}${more}`, {
|
|
25260
|
+
code: AMBIGUOUS_ID
|
|
25261
|
+
});
|
|
25262
|
+
}
|
|
25263
|
+
async function candidates(source) {
|
|
25264
|
+
const org = requireOrg(source.context);
|
|
25265
|
+
const ids = [];
|
|
25266
|
+
if (source.local !== void 0) {
|
|
25267
|
+
for (const status of [void 0, "deleted"]) {
|
|
25268
|
+
const filter = { org, ...status === void 0 ? {} : { status } };
|
|
25269
|
+
for await (const complaint of scanComplaints(source.local, filter)) {
|
|
25270
|
+
ids.push(complaint.id);
|
|
25271
|
+
}
|
|
25272
|
+
}
|
|
25273
|
+
return ids;
|
|
25274
|
+
}
|
|
25275
|
+
const project = source.context.projectConfig?.project;
|
|
25276
|
+
for (const status of [void 0, "deleted"]) {
|
|
25277
|
+
const filter = {
|
|
25278
|
+
org,
|
|
25279
|
+
limit: MAX_LIMIT,
|
|
25280
|
+
...project === void 0 ? {} : { project },
|
|
25281
|
+
...status === void 0 ? {} : { status }
|
|
25282
|
+
};
|
|
25283
|
+
for (const complaint of await listComplaints(source.store, filter)) {
|
|
25284
|
+
ids.push(complaint.id);
|
|
25285
|
+
}
|
|
25286
|
+
}
|
|
25287
|
+
return ids;
|
|
25288
|
+
}
|
|
25289
|
+
async function resolveIds(values, source) {
|
|
25290
|
+
const read = values.map((value) => readIdValue(value));
|
|
25291
|
+
let pool;
|
|
25292
|
+
const resolved = [];
|
|
25293
|
+
for (const value of read) {
|
|
25294
|
+
if (!value.prefix) {
|
|
25295
|
+
resolved.push(value.id);
|
|
25296
|
+
continue;
|
|
25297
|
+
}
|
|
25298
|
+
pool ??= candidates(source);
|
|
25299
|
+
const matches = [...new Set((await pool).filter((id) => id.startsWith(value.id)))];
|
|
25300
|
+
if (matches.length === 1) {
|
|
25301
|
+
resolved.push(matches[0]);
|
|
25302
|
+
continue;
|
|
25303
|
+
}
|
|
25304
|
+
if (matches.length > 1) throw ambiguousError(value.id, matches, "complaints");
|
|
25305
|
+
throw new NotFoundError(
|
|
25306
|
+
source.local === void 0 ? `no complaint among the newest ${MAX_LIMIT} starts with ${value.id}: pass the full id` : `no complaint starts with ${value.id}`
|
|
25307
|
+
);
|
|
25308
|
+
}
|
|
25309
|
+
return resolved;
|
|
25310
|
+
}
|
|
25311
|
+
|
|
24789
25312
|
// src/io.ts
|
|
24790
25313
|
async function readStdin(stream) {
|
|
24791
25314
|
const chunks = [];
|
|
@@ -24803,9 +25326,10 @@ function writeJson(stream, value) {
|
|
|
24803
25326
|
}
|
|
24804
25327
|
|
|
24805
25328
|
// src/client.ts
|
|
24806
|
-
|
|
25329
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
25330
|
+
var NOT_A_COMPLAINT = "unexpected response from the API: expected a complaint";
|
|
24807
25331
|
var ORG_NOT_FOUND = "org_not_found";
|
|
24808
|
-
var NOT_A_PROMOTION = "
|
|
25332
|
+
var NOT_A_PROMOTION = "unexpected response from the API: expected a tag promotion";
|
|
24809
25333
|
function readRoles(value) {
|
|
24810
25334
|
if (!Array.isArray(value)) return [];
|
|
24811
25335
|
const roles = [];
|
|
@@ -24838,7 +25362,7 @@ function parseMe(body) {
|
|
|
24838
25362
|
const record2 = asRecord(body);
|
|
24839
25363
|
const reporter = stringOf(record2, "reporter");
|
|
24840
25364
|
if (record2 === void 0 || reporter === void 0) {
|
|
24841
|
-
throw new RemoteError("the API
|
|
25365
|
+
throw new RemoteError("unexpected response from the API: identity has no reporter");
|
|
24842
25366
|
}
|
|
24843
25367
|
return {
|
|
24844
25368
|
kind: stringOf(record2, "kind") ?? "user",
|
|
@@ -24857,7 +25381,7 @@ function parseMe(body) {
|
|
|
24857
25381
|
function parseOrgAnswer(body, slug) {
|
|
24858
25382
|
const record2 = asRecord(body);
|
|
24859
25383
|
if (record2 === void 0) {
|
|
24860
|
-
throw new RemoteError("
|
|
25384
|
+
throw new RemoteError("unexpected response from the API: expected an organization");
|
|
24861
25385
|
}
|
|
24862
25386
|
return {
|
|
24863
25387
|
org: stringOf(record2, "org") ?? slug,
|
|
@@ -24867,6 +25391,35 @@ function parseOrgAnswer(body, slug) {
|
|
|
24867
25391
|
body: record2
|
|
24868
25392
|
};
|
|
24869
25393
|
}
|
|
25394
|
+
function readToken(value) {
|
|
25395
|
+
const record2 = asRecord(value);
|
|
25396
|
+
const id = stringOf(record2, "id");
|
|
25397
|
+
const name = stringOf(record2, "name");
|
|
25398
|
+
const createdAt = stringOf(record2, "createdAt");
|
|
25399
|
+
if (id === void 0 || name === void 0 || createdAt === void 0) {
|
|
25400
|
+
throw new RemoteError("unexpected response from the API: expected a token");
|
|
25401
|
+
}
|
|
25402
|
+
return {
|
|
25403
|
+
id,
|
|
25404
|
+
name,
|
|
25405
|
+
createdAt,
|
|
25406
|
+
expiresAt: stringOf(record2, "expiresAt"),
|
|
25407
|
+
lastUsedAt: stringOf(record2, "lastUsedAt"),
|
|
25408
|
+
revokedAt: stringOf(record2, "revokedAt")
|
|
25409
|
+
};
|
|
25410
|
+
}
|
|
25411
|
+
function readProject(body, name) {
|
|
25412
|
+
const record2 = asRecord(body);
|
|
25413
|
+
if (record2 === void 0) {
|
|
25414
|
+
throw new RemoteError("unexpected response from the API: expected a project");
|
|
25415
|
+
}
|
|
25416
|
+
const tags = record2["tags"];
|
|
25417
|
+
return {
|
|
25418
|
+
project: stringOf(record2, "project") ?? name,
|
|
25419
|
+
tags: Array.isArray(tags) ? tags.map((tag) => String(tag)) : void 0,
|
|
25420
|
+
body: record2
|
|
25421
|
+
};
|
|
25422
|
+
}
|
|
24870
25423
|
function readComplaint(body) {
|
|
24871
25424
|
try {
|
|
24872
25425
|
return parseComplaint(body);
|
|
@@ -24881,24 +25434,37 @@ var ApiClient = class {
|
|
|
24881
25434
|
#base;
|
|
24882
25435
|
#auth;
|
|
24883
25436
|
#http;
|
|
25437
|
+
#beforeFirst;
|
|
24884
25438
|
constructor(options) {
|
|
24885
25439
|
this.endpoint = options.endpoint;
|
|
24886
25440
|
this.org = options.org;
|
|
24887
25441
|
this.#base = trimSlash(options.endpoint);
|
|
24888
25442
|
this.#auth = options.authorizer;
|
|
25443
|
+
this.#beforeFirst = options.beforeFirst;
|
|
24889
25444
|
this.#http = {
|
|
24890
25445
|
fetch: options.fetch,
|
|
24891
25446
|
timeoutMs: options.timeoutMs,
|
|
24892
25447
|
debug: options.debug
|
|
24893
25448
|
};
|
|
24894
25449
|
}
|
|
24895
|
-
/**
|
|
24896
|
-
|
|
24897
|
-
|
|
25450
|
+
/**
|
|
25451
|
+
* Sends one request and returns its body. A non-2xx answer is an error.
|
|
25452
|
+
* `headers` are sent beside the auth headers, such as the idempotency key
|
|
25453
|
+
* of a filing.
|
|
25454
|
+
*/
|
|
25455
|
+
async request(method, path, body, headers) {
|
|
25456
|
+
return (await this.#call(method, path, body, false, headers)).body;
|
|
25457
|
+
}
|
|
25458
|
+
/**
|
|
25459
|
+
* The same, and the headers of the answer, which carry what the body does
|
|
25460
|
+
* not, such as the truncation mark of a list.
|
|
25461
|
+
*/
|
|
25462
|
+
async requestWithHeaders(method, path) {
|
|
25463
|
+
return this.#call(method, path, void 0, false);
|
|
24898
25464
|
}
|
|
24899
25465
|
/** The same, except that a `404` answers `undefined` instead of failing. */
|
|
24900
25466
|
async requestOptional(method, path, body) {
|
|
24901
|
-
return this.#call(method, path, body, true);
|
|
25467
|
+
return (await this.#call(method, path, body, true)).body;
|
|
24902
25468
|
}
|
|
24903
25469
|
/**
|
|
24904
25470
|
* The prefix of every org-scoped path. Every command that reaches one
|
|
@@ -24917,7 +25483,9 @@ var ApiClient = class {
|
|
|
24917
25483
|
async orgs() {
|
|
24918
25484
|
const body = asRecord(await this.request("GET", "/v1/orgs"));
|
|
24919
25485
|
if (body === void 0) {
|
|
24920
|
-
throw new RemoteError(
|
|
25486
|
+
throw new RemoteError(
|
|
25487
|
+
"unexpected response from the API: expected an organization list"
|
|
25488
|
+
);
|
|
24921
25489
|
}
|
|
24922
25490
|
return { orgs: readOrgs(body["orgs"]), body };
|
|
24923
25491
|
}
|
|
@@ -24926,16 +25494,55 @@ var ApiClient = class {
|
|
|
24926
25494
|
const slug = this.org ?? "";
|
|
24927
25495
|
return parseOrgAnswer(await this.request("GET", this.orgPath()), slug);
|
|
24928
25496
|
}
|
|
25497
|
+
/** The path of one project of the organization. */
|
|
25498
|
+
projectPath(name) {
|
|
25499
|
+
return `${this.orgPath()}/projects/${encodeURIComponent(name)}`;
|
|
25500
|
+
}
|
|
24929
25501
|
/** Reads the project record, which owns the allowed tag list in remote mode. */
|
|
24930
25502
|
async project(name) {
|
|
24931
|
-
|
|
24932
|
-
|
|
25503
|
+
return readProject(await this.request("GET", this.projectPath(name)), name);
|
|
25504
|
+
}
|
|
25505
|
+
/**
|
|
25506
|
+
* Replaces the allowed tag list of a project, or clears it with `null`. The
|
|
25507
|
+
* route retags the stored complaints and answers how many it rewrote.
|
|
25508
|
+
*/
|
|
25509
|
+
async setProjectTags(name, tags) {
|
|
25510
|
+
const body = await this.request(
|
|
25511
|
+
"PATCH",
|
|
25512
|
+
this.projectPath(name),
|
|
25513
|
+
JSON.stringify({ tags })
|
|
25514
|
+
);
|
|
25515
|
+
const answer2 = readProject(body, name);
|
|
25516
|
+
const retagged = answer2.body["retagged"];
|
|
25517
|
+
return { ...answer2, retagged: typeof retagged === "number" ? retagged : 0 };
|
|
25518
|
+
}
|
|
25519
|
+
/** Lists the tokens of a project, newest first, as the route sorts them. */
|
|
25520
|
+
async tokens(project) {
|
|
25521
|
+
const body = await this.request("GET", `${this.projectPath(project)}/tokens`);
|
|
25522
|
+
if (!Array.isArray(body)) {
|
|
25523
|
+
throw new RemoteError("unexpected response from the API: expected a token list");
|
|
25524
|
+
}
|
|
25525
|
+
return { tokens: body.map((entry) => readToken(entry)), body };
|
|
25526
|
+
}
|
|
25527
|
+
/** Mints a token. The answer carries the plaintext, once. */
|
|
25528
|
+
async createToken(project, input2) {
|
|
25529
|
+
const body = await this.request(
|
|
25530
|
+
"POST",
|
|
25531
|
+
`${this.projectPath(project)}/tokens`,
|
|
25532
|
+
JSON.stringify(input2)
|
|
25533
|
+
);
|
|
25534
|
+
const plaintext = stringOf(asRecord(body), "plaintext");
|
|
25535
|
+
if (plaintext === void 0) {
|
|
25536
|
+
throw new RemoteError("unexpected response from the API: token has no plaintext");
|
|
25537
|
+
}
|
|
25538
|
+
return { token: readToken(body), plaintext, body };
|
|
25539
|
+
}
|
|
25540
|
+
/** Revokes one token. A repeat answers `204` again. */
|
|
25541
|
+
async revokeToken(project, id) {
|
|
25542
|
+
await this.request(
|
|
25543
|
+
"DELETE",
|
|
25544
|
+
`${this.projectPath(project)}/tokens/${encodeURIComponent(id)}`
|
|
24933
25545
|
);
|
|
24934
|
-
const tags = body?.["tags"];
|
|
24935
|
-
return {
|
|
24936
|
-
project: stringOf(body, "project") ?? name,
|
|
24937
|
-
tags: Array.isArray(tags) ? tags.map((tag) => String(tag)) : void 0
|
|
24938
|
-
};
|
|
24939
25546
|
}
|
|
24940
25547
|
/**
|
|
24941
25548
|
* Promotes one suggestion to a real tag. The route needs `maintainer`, so a
|
|
@@ -24956,15 +25563,36 @@ var ApiClient = class {
|
|
|
24956
25563
|
}
|
|
24957
25564
|
return { tags: tags.map((entry) => String(entry)), retagged };
|
|
24958
25565
|
}
|
|
24959
|
-
|
|
25566
|
+
/**
|
|
25567
|
+
* Sends one request with the auth rules of section 5.7 and returns the
|
|
25568
|
+
* answer whatever its status. It never runs `beforeFirst`, so the offline
|
|
25569
|
+
* queue sends through it. A request that gets no answer still throws.
|
|
25570
|
+
*/
|
|
25571
|
+
async exchange(method, path, body, headers) {
|
|
25572
|
+
const { response, text } = await this.#exchange(method, path, body, headers);
|
|
25573
|
+
return { status: response.status, text };
|
|
25574
|
+
}
|
|
25575
|
+
async #call(method, path, body, notFoundIsUndefined, headers) {
|
|
25576
|
+
const first = this.#beforeFirst;
|
|
25577
|
+
this.#beforeFirst = void 0;
|
|
25578
|
+
if (first !== void 0) await first(this);
|
|
25579
|
+
const { response, text, site } = await this.#exchange(method, path, body, headers);
|
|
25580
|
+
if (response.status === 404 && notFoundIsUndefined && serverCode(text) !== ORG_NOT_FOUND) {
|
|
25581
|
+
return { body: void 0, headers: response.headers };
|
|
25582
|
+
}
|
|
25583
|
+
if (!response.ok) throw remoteFailure(site, response, text);
|
|
25584
|
+
if (response.status === 204) return { body: void 0, headers: response.headers };
|
|
25585
|
+
return { body: parseJson(text, site, response.status), headers: response.headers };
|
|
25586
|
+
}
|
|
25587
|
+
async #exchange(method, path, body, extra) {
|
|
24960
25588
|
const url2 = `${this.#base}${path}`;
|
|
24961
25589
|
const site = { method, url: url2, debug: this.#http.debug };
|
|
24962
|
-
let response = await this.#send(url2, method, await this.#auth.token(), body);
|
|
25590
|
+
let response = await this.#send(url2, method, await this.#auth.token(), body, extra);
|
|
24963
25591
|
if (response.status === 401) {
|
|
24964
25592
|
const fresh = await this.#auth.retry();
|
|
24965
25593
|
if (fresh !== void 0) {
|
|
24966
25594
|
await readText(response, site);
|
|
24967
|
-
response = await this.#send(url2, method, fresh, body);
|
|
25595
|
+
response = await this.#send(url2, method, fresh, body, extra);
|
|
24968
25596
|
if (response.status === 401) throw sessionExpired(this.endpoint);
|
|
24969
25597
|
} else if (this.#auth.actAs !== void 0) {
|
|
24970
25598
|
await readText(response, site);
|
|
@@ -24972,14 +25600,13 @@ var ApiClient = class {
|
|
|
24972
25600
|
}
|
|
24973
25601
|
}
|
|
24974
25602
|
const text = await readText(response, site);
|
|
24975
|
-
|
|
24976
|
-
return void 0;
|
|
24977
|
-
}
|
|
24978
|
-
if (!response.ok) throw remoteFailure(site, response, text);
|
|
24979
|
-
return parseJson(text, site, response.status);
|
|
25603
|
+
return { response, text, site };
|
|
24980
25604
|
}
|
|
24981
|
-
#send(url2, method, bearer, body) {
|
|
24982
|
-
const headers = {
|
|
25605
|
+
#send(url2, method, bearer, body, extra = {}) {
|
|
25606
|
+
const headers = {
|
|
25607
|
+
...extra,
|
|
25608
|
+
authorization: `Bearer ${bearer}`
|
|
25609
|
+
};
|
|
24983
25610
|
const actAs = this.#auth.actAs;
|
|
24984
25611
|
if (actAs !== void 0) headers[ACT_AS_HEADER] = actAs;
|
|
24985
25612
|
if (body !== void 0) headers["content-type"] = "application/json";
|
|
@@ -24991,6 +25618,67 @@ var ApiClient = class {
|
|
|
24991
25618
|
});
|
|
24992
25619
|
}
|
|
24993
25620
|
};
|
|
25621
|
+
var UndeliveredError = class extends RemoteError {
|
|
25622
|
+
body;
|
|
25623
|
+
reason;
|
|
25624
|
+
/**
|
|
25625
|
+
* The idempotency key the request carried, which the queued entry keeps,
|
|
25626
|
+
* so a request that did reach the server files nothing new when it is sent
|
|
25627
|
+
* again.
|
|
25628
|
+
*/
|
|
25629
|
+
key;
|
|
25630
|
+
constructor(body, reason, key, cause) {
|
|
25631
|
+
super(cause.message, { cause, status: cause.status, unanswered: cause.unanswered });
|
|
25632
|
+
this.body = body;
|
|
25633
|
+
this.reason = reason;
|
|
25634
|
+
this.key = key;
|
|
25635
|
+
}
|
|
25636
|
+
};
|
|
25637
|
+
function retryableReason(error62) {
|
|
25638
|
+
if (!(error62 instanceof RemoteError)) return void 0;
|
|
25639
|
+
if (error62.unanswered) return "the API is unreachable";
|
|
25640
|
+
const status = error62.status;
|
|
25641
|
+
if (status === void 0) return void 0;
|
|
25642
|
+
if (status === 429) return error62.serverMessage ?? `the API answered ${status}`;
|
|
25643
|
+
if (status >= 500) return `the API answered ${status}`;
|
|
25644
|
+
return void 0;
|
|
25645
|
+
}
|
|
25646
|
+
function complaintBody(complaint) {
|
|
25647
|
+
return {
|
|
25648
|
+
project: complaint.project,
|
|
25649
|
+
message: complaint.message,
|
|
25650
|
+
severity: complaint.severity,
|
|
25651
|
+
tags: complaint.tags,
|
|
25652
|
+
agent: complaint.agent,
|
|
25653
|
+
...complaint.cwd === void 0 ? {} : { cwd: complaint.cwd },
|
|
25654
|
+
...complaint.cmd === void 0 ? {} : { cmd: complaint.cmd }
|
|
25655
|
+
};
|
|
25656
|
+
}
|
|
25657
|
+
function readBulkResults(body) {
|
|
25658
|
+
const results = asRecord(body)?.["results"];
|
|
25659
|
+
if (!Array.isArray(results)) {
|
|
25660
|
+
throw new RemoteError("unexpected response from the API: expected bulk results");
|
|
25661
|
+
}
|
|
25662
|
+
return results.map((entry) => {
|
|
25663
|
+
const record2 = asRecord(entry);
|
|
25664
|
+
const id = stringOf(record2, "id");
|
|
25665
|
+
if (record2 === void 0 || id === void 0) {
|
|
25666
|
+
throw new RemoteError("unexpected response from the API: expected a bulk result");
|
|
25667
|
+
}
|
|
25668
|
+
if (record2["ok"] === true) {
|
|
25669
|
+
return { id, ok: true, complaint: readComplaint(record2["complaint"]) };
|
|
25670
|
+
}
|
|
25671
|
+
const error62 = asRecord(record2["error"]);
|
|
25672
|
+
return {
|
|
25673
|
+
id,
|
|
25674
|
+
ok: false,
|
|
25675
|
+
error: {
|
|
25676
|
+
code: stringOf(error62, "code") ?? "error",
|
|
25677
|
+
message: stringOf(error62, "message") ?? "the change failed"
|
|
25678
|
+
}
|
|
25679
|
+
};
|
|
25680
|
+
});
|
|
25681
|
+
}
|
|
24994
25682
|
function adopt(target, source) {
|
|
24995
25683
|
for (const key of Object.keys(target)) {
|
|
24996
25684
|
delete target[key];
|
|
@@ -25007,39 +25695,55 @@ var RemoteStore = class {
|
|
|
25007
25695
|
return `${this.#client.orgPath()}/complaints${suffix}`;
|
|
25008
25696
|
}
|
|
25009
25697
|
/**
|
|
25010
|
-
* Posts the complaint. The API assigns the stored `id
|
|
25011
|
-
* the validated record of the response
|
|
25698
|
+
* Posts the complaint. The API assigns the stored `id`, `createdAt`,
|
|
25699
|
+
* `fingerprint` and `recurred`, so the validated record of the response
|
|
25700
|
+
* replaces the record passed in, and the answer's match is returned.
|
|
25012
25701
|
*/
|
|
25013
25702
|
async create(complaint) {
|
|
25014
|
-
const
|
|
25015
|
-
|
|
25016
|
-
|
|
25017
|
-
|
|
25018
|
-
|
|
25019
|
-
|
|
25020
|
-
|
|
25021
|
-
}
|
|
25022
|
-
|
|
25703
|
+
const input2 = complaintBody(complaint);
|
|
25704
|
+
const key = randomUUID2();
|
|
25705
|
+
let body;
|
|
25706
|
+
try {
|
|
25707
|
+
body = await this.#client.request("POST", this.#path(), JSON.stringify(input2), {
|
|
25708
|
+
[IDEMPOTENCY_HEADER]: key
|
|
25709
|
+
});
|
|
25710
|
+
} catch (error62) {
|
|
25711
|
+
const reason = retryableReason(error62);
|
|
25712
|
+
if (reason !== void 0 && error62 instanceof RemoteError) {
|
|
25713
|
+
throw new UndeliveredError(input2, reason, key, error62);
|
|
25714
|
+
}
|
|
25715
|
+
throw error62;
|
|
25716
|
+
}
|
|
25023
25717
|
adopt(complaint, readComplaint(body));
|
|
25718
|
+
const answer2 = asRecord(body);
|
|
25719
|
+
const openMatches = answer2?.["openMatches"];
|
|
25720
|
+
return {
|
|
25721
|
+
open: typeof openMatches === "number" ? openMatches : 0,
|
|
25722
|
+
resolved: answer2?.["recurred"] === true
|
|
25723
|
+
};
|
|
25024
25724
|
}
|
|
25025
25725
|
/** Returns the matching complaints, newest first. */
|
|
25026
25726
|
async list(filter) {
|
|
25027
|
-
|
|
25028
|
-
|
|
25029
|
-
|
|
25030
|
-
|
|
25031
|
-
|
|
25032
|
-
|
|
25033
|
-
|
|
25034
|
-
|
|
25035
|
-
if (filter.limit !== void 0) query.set("limit", String(filter.limit));
|
|
25036
|
-
const search = query.toString();
|
|
25727
|
+
return (await this.listPage(filter)).complaints;
|
|
25728
|
+
}
|
|
25729
|
+
/**
|
|
25730
|
+
* Returns the matching complaints, newest first, and whether the server
|
|
25731
|
+
* stopped at its read budget, which the answer marks with a header.
|
|
25732
|
+
*/
|
|
25733
|
+
async listPage(filter) {
|
|
25734
|
+
const search = new URLSearchParams(listQueryOf(filter)).toString();
|
|
25037
25735
|
const suffix = search === "" ? "" : `?${search}`;
|
|
25038
|
-
const body = await this.#client.
|
|
25736
|
+
const { body, headers } = await this.#client.requestWithHeaders(
|
|
25737
|
+
"GET",
|
|
25738
|
+
this.#path(suffix)
|
|
25739
|
+
);
|
|
25039
25740
|
if (!Array.isArray(body)) {
|
|
25040
|
-
throw new RemoteError("the API
|
|
25741
|
+
throw new RemoteError("unexpected response from the API: expected a list");
|
|
25041
25742
|
}
|
|
25042
|
-
return
|
|
25743
|
+
return {
|
|
25744
|
+
complaints: body.map((item) => readComplaint(item)),
|
|
25745
|
+
truncated: headers.get(TRUNCATED_HEADER) === "1"
|
|
25746
|
+
};
|
|
25043
25747
|
}
|
|
25044
25748
|
/** Returns the complaint with that id, or `undefined` when the API says 404. */
|
|
25045
25749
|
async get(id) {
|
|
@@ -25059,11 +25763,26 @@ var RemoteStore = class {
|
|
|
25059
25763
|
this.#path(`/${encodeURIComponent(id)}`),
|
|
25060
25764
|
JSON.stringify({
|
|
25061
25765
|
status: change.status,
|
|
25062
|
-
...change.resolution === void 0 ? {} : { resolution: change.resolution }
|
|
25766
|
+
...change.resolution === void 0 ? {} : { resolution: change.resolution },
|
|
25767
|
+
...change.reason === void 0 ? {} : { reason: change.reason },
|
|
25768
|
+
...change.duplicateOf === void 0 ? {} : { duplicateOf: change.duplicateOf }
|
|
25063
25769
|
})
|
|
25064
25770
|
);
|
|
25065
25771
|
return body === void 0 ? void 0 : readComplaint(body);
|
|
25066
25772
|
}
|
|
25773
|
+
/**
|
|
25774
|
+
* Applies one status change to many complaints with the bulk route, in one
|
|
25775
|
+
* request. A per-id `not_found` reads as an absent record, exactly like the
|
|
25776
|
+
* `404` of one id, so its result carries no complaint and the code.
|
|
25777
|
+
*/
|
|
25778
|
+
async bulkStatus(ids, change) {
|
|
25779
|
+
const body = await this.#client.request(
|
|
25780
|
+
"POST",
|
|
25781
|
+
this.#path("/bulk"),
|
|
25782
|
+
JSON.stringify({ ids, ...change })
|
|
25783
|
+
);
|
|
25784
|
+
return readBulkResults(body);
|
|
25785
|
+
}
|
|
25067
25786
|
/**
|
|
25068
25787
|
* Replaces the tags. The API stamps the change with its own time, so the
|
|
25069
25788
|
* request carries the list only.
|
|
@@ -25078,40 +25797,411 @@ var RemoteStore = class {
|
|
|
25078
25797
|
}
|
|
25079
25798
|
};
|
|
25080
25799
|
|
|
25081
|
-
// src/
|
|
25082
|
-
|
|
25083
|
-
|
|
25084
|
-
|
|
25085
|
-
|
|
25086
|
-
|
|
25087
|
-
|
|
25088
|
-
|
|
25089
|
-
|
|
25090
|
-
|
|
25800
|
+
// src/queue.ts
|
|
25801
|
+
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as randomUUID3 } from "node:crypto";
|
|
25802
|
+
import {
|
|
25803
|
+
appendFileSync,
|
|
25804
|
+
closeSync,
|
|
25805
|
+
fstatSync,
|
|
25806
|
+
linkSync,
|
|
25807
|
+
mkdirSync as mkdirSync2,
|
|
25808
|
+
openSync,
|
|
25809
|
+
readFileSync as readFileSync3,
|
|
25810
|
+
readSync,
|
|
25811
|
+
renameSync as renameSync2,
|
|
25812
|
+
rmSync as rmSync2,
|
|
25813
|
+
statSync,
|
|
25814
|
+
writeSync
|
|
25815
|
+
} from "node:fs";
|
|
25816
|
+
import { hostname as hostname3 } from "node:os";
|
|
25817
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
25818
|
+
|
|
25819
|
+
// src/text.ts
|
|
25820
|
+
var CONTROL_RE2 = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
25821
|
+
function escape(char) {
|
|
25822
|
+
return `\\x${(char.codePointAt(0) ?? 0).toString(16).padStart(2, "0")}`;
|
|
25823
|
+
}
|
|
25824
|
+
function printable(text) {
|
|
25825
|
+
return text.replace(/\s+/g, " ").replace(CONTROL_RE2, escape);
|
|
25826
|
+
}
|
|
25827
|
+
function printableLines(text) {
|
|
25828
|
+
return text.replace(/\r\n?/g, "\n").replace(
|
|
25829
|
+
CONTROL_RE2,
|
|
25830
|
+
(char) => char === "\n" || char === " " ? char : escape(char)
|
|
25831
|
+
);
|
|
25832
|
+
}
|
|
25833
|
+
|
|
25834
|
+
// src/queue.ts
|
|
25835
|
+
var QUEUE_FILENAME = "unsent.jsonl";
|
|
25836
|
+
var LOCK_FILENAME = "unsent.lock";
|
|
25837
|
+
var LOCK_STALE_MS = 5 * 6e4;
|
|
25838
|
+
var QUEUE_MODE = CREDENTIALS_MODE;
|
|
25839
|
+
function queuePath(env) {
|
|
25840
|
+
return join4(defaultQuibbleHome(env), QUEUE_FILENAME);
|
|
25841
|
+
}
|
|
25842
|
+
function lockPathOf(queue) {
|
|
25843
|
+
return join4(dirname4(queue), LOCK_FILENAME);
|
|
25844
|
+
}
|
|
25845
|
+
function authLabel(context) {
|
|
25846
|
+
if (context.token !== void 0) {
|
|
25847
|
+
const hash2 = createHash2("sha256").update(context.token).digest("hex");
|
|
25848
|
+
return `token:${hash2.slice(0, 16)}`;
|
|
25849
|
+
}
|
|
25850
|
+
if (context.credential !== void 0) return `session:${context.credential.sub}`;
|
|
25851
|
+
return `dev:${context.actAs ?? ""}`;
|
|
25852
|
+
}
|
|
25853
|
+
function readOptional(path) {
|
|
25854
|
+
try {
|
|
25855
|
+
return readFileSync3(path, "utf8");
|
|
25856
|
+
} catch (error62) {
|
|
25857
|
+
if (isNotFound(error62)) return "";
|
|
25858
|
+
throw new StorageError(`cannot read ${path}`, { cause: error62 });
|
|
25091
25859
|
}
|
|
25092
|
-
if (!isLoopback(endpoint)) return `${endpoint} is not a loopback endpoint`;
|
|
25093
|
-
return void 0;
|
|
25094
25860
|
}
|
|
25095
|
-
function
|
|
25096
|
-
|
|
25097
|
-
|
|
25861
|
+
function endsInsideLine(path) {
|
|
25862
|
+
let fd;
|
|
25863
|
+
try {
|
|
25864
|
+
fd = openSync(path, "r");
|
|
25865
|
+
} catch (error62) {
|
|
25866
|
+
if (isNotFound(error62)) return false;
|
|
25867
|
+
throw error62;
|
|
25868
|
+
}
|
|
25869
|
+
try {
|
|
25870
|
+
const size = fstatSync(fd).size;
|
|
25871
|
+
if (size === 0) return false;
|
|
25872
|
+
const last = Buffer.alloc(1);
|
|
25873
|
+
readSync(fd, last, 0, 1, size - 1);
|
|
25874
|
+
return last[0] !== 10;
|
|
25875
|
+
} finally {
|
|
25876
|
+
closeSync(fd);
|
|
25877
|
+
}
|
|
25098
25878
|
}
|
|
25099
|
-
function
|
|
25100
|
-
|
|
25101
|
-
|
|
25102
|
-
|
|
25103
|
-
|
|
25104
|
-
|
|
25105
|
-
|
|
25106
|
-
|
|
25107
|
-
endpoint,
|
|
25108
|
-
authorizer: devAuthorizer(endpoint, context.actAs, http),
|
|
25109
|
-
...org,
|
|
25110
|
-
...http
|
|
25879
|
+
function appendLine(path, value) {
|
|
25880
|
+
try {
|
|
25881
|
+
mkdirSync2(dirname4(path), { recursive: true, mode: HOME_MODE });
|
|
25882
|
+
const lead = endsInsideLine(path) ? "\n" : "";
|
|
25883
|
+
appendFileSync(path, `${lead}${JSON.stringify(value)}
|
|
25884
|
+
`, {
|
|
25885
|
+
encoding: "utf8",
|
|
25886
|
+
mode: QUEUE_MODE
|
|
25111
25887
|
});
|
|
25888
|
+
} catch (error62) {
|
|
25889
|
+
throw new StorageError(`cannot write ${path}`, { cause: error62 });
|
|
25112
25890
|
}
|
|
25113
|
-
|
|
25114
|
-
|
|
25891
|
+
}
|
|
25892
|
+
function badLine(path, line, why) {
|
|
25893
|
+
return new StorageError(`${path}: line ${line}: ${why}`);
|
|
25894
|
+
}
|
|
25895
|
+
function readQueuedLine(record2, path, line) {
|
|
25896
|
+
const body = asRecord(record2["body"]);
|
|
25897
|
+
const fields = ["key", "endpoint", "org", "auth", "queuedAt"];
|
|
25898
|
+
for (const field of fields) {
|
|
25899
|
+
if (typeof record2[field] !== "string") {
|
|
25900
|
+
throw badLine(path, line, `${field} must be a string`);
|
|
25901
|
+
}
|
|
25902
|
+
}
|
|
25903
|
+
if (body === void 0) throw badLine(path, line, "body must be an object");
|
|
25904
|
+
return {
|
|
25905
|
+
key: record2["key"],
|
|
25906
|
+
endpoint: record2["endpoint"],
|
|
25907
|
+
org: record2["org"],
|
|
25908
|
+
auth: record2["auth"],
|
|
25909
|
+
body,
|
|
25910
|
+
queuedAt: record2["queuedAt"]
|
|
25911
|
+
};
|
|
25912
|
+
}
|
|
25913
|
+
function readQueue(path) {
|
|
25914
|
+
const pending = /* @__PURE__ */ new Map();
|
|
25915
|
+
const known = /* @__PURE__ */ new Set();
|
|
25916
|
+
const lines = readOptional(path).split("\n");
|
|
25917
|
+
for (const [index, text] of lines.entries()) {
|
|
25918
|
+
const line = index + 1;
|
|
25919
|
+
if (text.trim() === "") continue;
|
|
25920
|
+
let record2;
|
|
25921
|
+
try {
|
|
25922
|
+
record2 = asRecord(JSON.parse(text));
|
|
25923
|
+
} catch {
|
|
25924
|
+
throw badLine(path, line, "not JSON");
|
|
25925
|
+
}
|
|
25926
|
+
if (record2 === void 0) throw badLine(path, line, "not a JSON object");
|
|
25927
|
+
const op = record2["op"];
|
|
25928
|
+
if (op === "queued") {
|
|
25929
|
+
const entry = readQueuedLine(record2, path, line);
|
|
25930
|
+
known.add(entry.key);
|
|
25931
|
+
pending.set(entry.key, entry);
|
|
25932
|
+
continue;
|
|
25933
|
+
}
|
|
25934
|
+
if (op === "sent" || op === "dropped") {
|
|
25935
|
+
const key = record2["key"];
|
|
25936
|
+
if (typeof key !== "string" || !known.has(key)) {
|
|
25937
|
+
throw badLine(path, line, `${op} names no queued complaint`);
|
|
25938
|
+
}
|
|
25939
|
+
pending.delete(key);
|
|
25940
|
+
continue;
|
|
25941
|
+
}
|
|
25942
|
+
throw badLine(path, line, `unknown op ${JSON.stringify(op)}`);
|
|
25943
|
+
}
|
|
25944
|
+
return [...pending.values()];
|
|
25945
|
+
}
|
|
25946
|
+
function pendingFor(path, endpoint) {
|
|
25947
|
+
const key = normalizeEndpoint(endpoint);
|
|
25948
|
+
return readQueue(path).filter((entry) => entry.endpoint === key);
|
|
25949
|
+
}
|
|
25950
|
+
function enqueue(path, input2, now = /* @__PURE__ */ new Date()) {
|
|
25951
|
+
const entry = {
|
|
25952
|
+
key: input2.key ?? randomUUID3(),
|
|
25953
|
+
endpoint: normalizeEndpoint(input2.endpoint),
|
|
25954
|
+
org: input2.org,
|
|
25955
|
+
auth: input2.auth,
|
|
25956
|
+
body: input2.body,
|
|
25957
|
+
queuedAt: now.toISOString()
|
|
25958
|
+
};
|
|
25959
|
+
appendLine(path, { op: "queued", ...entry });
|
|
25960
|
+
return entry;
|
|
25961
|
+
}
|
|
25962
|
+
function processRuns(pid) {
|
|
25963
|
+
try {
|
|
25964
|
+
process.kill(pid, 0);
|
|
25965
|
+
return true;
|
|
25966
|
+
} catch (error62) {
|
|
25967
|
+
return error62.code === "EPERM";
|
|
25968
|
+
}
|
|
25969
|
+
}
|
|
25970
|
+
function judgeLock(lock, now = /* @__PURE__ */ new Date()) {
|
|
25971
|
+
let text;
|
|
25972
|
+
try {
|
|
25973
|
+
text = readFileSync3(lock, "utf8");
|
|
25974
|
+
} catch (error62) {
|
|
25975
|
+
if (isNotFound(error62)) return void 0;
|
|
25976
|
+
throw new StorageError(`cannot read ${lock}`, { cause: error62 });
|
|
25977
|
+
}
|
|
25978
|
+
const record2 = asRecord(parseQuietly(text));
|
|
25979
|
+
const pid = record2?.["pid"];
|
|
25980
|
+
const host = stringOf(record2, "host");
|
|
25981
|
+
const at = Date.parse(stringOf(record2, "at") ?? "");
|
|
25982
|
+
if (typeof pid !== "number" || host === void 0 || Number.isNaN(at)) {
|
|
25983
|
+
let written;
|
|
25984
|
+
try {
|
|
25985
|
+
written = statSync(lock).mtimeMs;
|
|
25986
|
+
} catch (error62) {
|
|
25987
|
+
if (isNotFound(error62)) return void 0;
|
|
25988
|
+
throw new StorageError(`cannot read ${lock}`, { cause: error62 });
|
|
25989
|
+
}
|
|
25990
|
+
return { text, stale: now.getTime() - written > LOCK_STALE_MS };
|
|
25991
|
+
}
|
|
25992
|
+
const dead = host === hostname3() && !processRuns(pid);
|
|
25993
|
+
return { text, stale: dead || now.getTime() - at > LOCK_STALE_MS };
|
|
25994
|
+
}
|
|
25995
|
+
function createLock(lock, owner = { pid: process.pid, host: hostname3(), at: (/* @__PURE__ */ new Date()).toISOString() }) {
|
|
25996
|
+
const text = JSON.stringify(owner);
|
|
25997
|
+
let fd;
|
|
25998
|
+
try {
|
|
25999
|
+
mkdirSync2(dirname4(lock), { recursive: true, mode: HOME_MODE });
|
|
26000
|
+
fd = openSync(lock, "wx", QUEUE_MODE);
|
|
26001
|
+
} catch (error62) {
|
|
26002
|
+
if (error62.code === "EEXIST") return void 0;
|
|
26003
|
+
throw new StorageError(`cannot create ${lock}`, { cause: error62 });
|
|
26004
|
+
}
|
|
26005
|
+
try {
|
|
26006
|
+
writeSync(fd, text);
|
|
26007
|
+
} finally {
|
|
26008
|
+
closeSync(fd);
|
|
26009
|
+
}
|
|
26010
|
+
return text;
|
|
26011
|
+
}
|
|
26012
|
+
function takeOverLock(lock, judged, owner) {
|
|
26013
|
+
const moved = `${lock}.${process.pid}.${randomBytes2(6).toString("hex")}`;
|
|
26014
|
+
try {
|
|
26015
|
+
renameSync2(lock, moved);
|
|
26016
|
+
} catch (error62) {
|
|
26017
|
+
if (isNotFound(error62)) return void 0;
|
|
26018
|
+
throw new StorageError(`cannot move ${lock}`, { cause: error62 });
|
|
26019
|
+
}
|
|
26020
|
+
const text = readOptional(moved);
|
|
26021
|
+
if (text === judged.text) {
|
|
26022
|
+
rmSync2(moved, { force: true });
|
|
26023
|
+
return createLock(lock, owner);
|
|
26024
|
+
}
|
|
26025
|
+
try {
|
|
26026
|
+
linkSync(moved, lock);
|
|
26027
|
+
} catch (error62) {
|
|
26028
|
+
if (error62.code !== "EEXIST") {
|
|
26029
|
+
throw new StorageError(`cannot restore ${lock}`, { cause: error62 });
|
|
26030
|
+
}
|
|
26031
|
+
} finally {
|
|
26032
|
+
rmSync2(moved, { force: true });
|
|
26033
|
+
}
|
|
26034
|
+
return void 0;
|
|
26035
|
+
}
|
|
26036
|
+
function acquireLock(lock) {
|
|
26037
|
+
const created = createLock(lock);
|
|
26038
|
+
if (created !== void 0) return created;
|
|
26039
|
+
const judged = judgeLock(lock);
|
|
26040
|
+
if (judged === void 0) return createLock(lock);
|
|
26041
|
+
if (!judged.stale) return void 0;
|
|
26042
|
+
return takeOverLock(lock, judged);
|
|
26043
|
+
}
|
|
26044
|
+
function releaseLock(lock, text) {
|
|
26045
|
+
if (readOptional(lock) === text) rmSync2(lock, { force: true });
|
|
26046
|
+
}
|
|
26047
|
+
function stopReason(status, text) {
|
|
26048
|
+
if (status >= 500) return `the API answered ${status}`;
|
|
26049
|
+
const code = serverCode(text);
|
|
26050
|
+
const stops = status === 429 || status === 401 || status === 403 || status === 404 && code === ORG_NOT_FOUND;
|
|
26051
|
+
if (!stops) return void 0;
|
|
26052
|
+
const message = stringOf(asRecord(asRecord(parseQuietly(text))?.["error"]), "message");
|
|
26053
|
+
return message ?? `the API answered ${status}`;
|
|
26054
|
+
}
|
|
26055
|
+
function projectOf(entry) {
|
|
26056
|
+
const project = entry.body["project"];
|
|
26057
|
+
return typeof project === "string" ? project : "?";
|
|
26058
|
+
}
|
|
26059
|
+
function headlineOfEntry(entry) {
|
|
26060
|
+
const message = entry.body["message"];
|
|
26061
|
+
return typeof message === "string" ? printable(summarize(message)) : "?";
|
|
26062
|
+
}
|
|
26063
|
+
function queuedCount(n) {
|
|
26064
|
+
return `${n} queued ${n === 1 ? "complaint" : "complaints"}`;
|
|
26065
|
+
}
|
|
26066
|
+
async function flushQueue(options) {
|
|
26067
|
+
const endpoint = normalizeEndpoint(options.endpoint);
|
|
26068
|
+
const mine = () => readQueue(options.path).filter(
|
|
26069
|
+
(entry) => entry.endpoint === endpoint && entry.auth === options.auth
|
|
26070
|
+
);
|
|
26071
|
+
const result = {
|
|
26072
|
+
sent: [],
|
|
26073
|
+
dropped: 0,
|
|
26074
|
+
left: 0,
|
|
26075
|
+
locked: false,
|
|
26076
|
+
stopped: void 0
|
|
26077
|
+
};
|
|
26078
|
+
if (mine().length === 0) return result;
|
|
26079
|
+
const lock = lockPathOf(options.path);
|
|
26080
|
+
const held = acquireLock(lock);
|
|
26081
|
+
if (held === void 0) {
|
|
26082
|
+
return { ...result, locked: true, left: mine().length };
|
|
26083
|
+
}
|
|
26084
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
26085
|
+
try {
|
|
26086
|
+
const entries = mine();
|
|
26087
|
+
for (const [index, entry] of entries.entries()) {
|
|
26088
|
+
let status;
|
|
26089
|
+
let text;
|
|
26090
|
+
try {
|
|
26091
|
+
({ status, text } = await options.client.exchange(
|
|
26092
|
+
"POST",
|
|
26093
|
+
`/v1/orgs/${encodeURIComponent(entry.org)}/complaints`,
|
|
26094
|
+
JSON.stringify(entry.body),
|
|
26095
|
+
{ [IDEMPOTENCY_HEADER]: entry.key }
|
|
26096
|
+
));
|
|
26097
|
+
} catch (error62) {
|
|
26098
|
+
result.stopped = errorMessage(error62);
|
|
26099
|
+
result.left = entries.length - index;
|
|
26100
|
+
return result;
|
|
26101
|
+
}
|
|
26102
|
+
if (status >= 200 && status < 300) {
|
|
26103
|
+
const id = stringOf(asRecord(parseQuietly(text)), "id") ?? "";
|
|
26104
|
+
appendLine(options.path, {
|
|
26105
|
+
op: "sent",
|
|
26106
|
+
key: entry.key,
|
|
26107
|
+
id,
|
|
26108
|
+
sentAt: now().toISOString()
|
|
26109
|
+
});
|
|
26110
|
+
result.sent.push(id);
|
|
26111
|
+
continue;
|
|
26112
|
+
}
|
|
26113
|
+
const reason = stopReason(status, text);
|
|
26114
|
+
if (reason !== void 0) {
|
|
26115
|
+
result.stopped = reason;
|
|
26116
|
+
result.left = entries.length - index;
|
|
26117
|
+
return result;
|
|
26118
|
+
}
|
|
26119
|
+
const record2 = asRecord(asRecord(parseQuietly(text))?.["error"]);
|
|
26120
|
+
const code = stringOf(record2, "code") ?? `http_${status}`;
|
|
26121
|
+
const message = stringOf(record2, "message") ?? `the API answered ${status}`;
|
|
26122
|
+
appendLine(options.path, {
|
|
26123
|
+
op: "dropped",
|
|
26124
|
+
key: entry.key,
|
|
26125
|
+
code,
|
|
26126
|
+
message,
|
|
26127
|
+
droppedAt: now().toISOString()
|
|
26128
|
+
});
|
|
26129
|
+
result.dropped += 1;
|
|
26130
|
+
writeLine(
|
|
26131
|
+
options.stderr,
|
|
26132
|
+
`quibble: dropped a queued complaint for ${entry.org}/${projectOf(entry)}, "${headlineOfEntry(entry)}": ${printable(message)}`
|
|
26133
|
+
);
|
|
26134
|
+
}
|
|
26135
|
+
return result;
|
|
26136
|
+
} finally {
|
|
26137
|
+
releaseLock(lock, held);
|
|
26138
|
+
}
|
|
26139
|
+
}
|
|
26140
|
+
|
|
26141
|
+
// src/store.ts
|
|
26142
|
+
function queueSender(context, endpoint, io) {
|
|
26143
|
+
return async (client) => {
|
|
26144
|
+
let result;
|
|
26145
|
+
try {
|
|
26146
|
+
result = await flushQueue({
|
|
26147
|
+
path: queuePath(io.env),
|
|
26148
|
+
endpoint,
|
|
26149
|
+
auth: authLabel(context),
|
|
26150
|
+
client,
|
|
26151
|
+
stderr: io.stderr
|
|
26152
|
+
});
|
|
26153
|
+
} catch (error62) {
|
|
26154
|
+
if (!(error62 instanceof StorageError)) throw error62;
|
|
26155
|
+
writeLine(
|
|
26156
|
+
io.stderr,
|
|
26157
|
+
`quibble: warning: cannot send the queue: ${errorMessage(error62)}`
|
|
26158
|
+
);
|
|
26159
|
+
return;
|
|
26160
|
+
}
|
|
26161
|
+
if (result.stopped !== void 0) {
|
|
26162
|
+
writeLine(
|
|
26163
|
+
io.stderr,
|
|
26164
|
+
`quibble: ${queuedCount(result.left)} still unsent: ${result.stopped}`
|
|
26165
|
+
);
|
|
26166
|
+
}
|
|
26167
|
+
};
|
|
26168
|
+
}
|
|
26169
|
+
function actAsProblem(context) {
|
|
26170
|
+
if (context.actAs === void 0) return void 0;
|
|
26171
|
+
const endpoint = context.endpoint;
|
|
26172
|
+
if (context.mode === "local" || endpoint === void 0) {
|
|
26173
|
+
return "local mode is in use";
|
|
26174
|
+
}
|
|
26175
|
+
if (context.token !== void 0) return "QUIBBLE_TOKEN is set";
|
|
26176
|
+
if (context.credential !== void 0) {
|
|
26177
|
+
return `you are logged in to ${endpoint}`;
|
|
26178
|
+
}
|
|
26179
|
+
if (!isLoopback(endpoint)) return `${endpoint} is not a loopback endpoint`;
|
|
26180
|
+
return void 0;
|
|
26181
|
+
}
|
|
26182
|
+
function checkActAs(context) {
|
|
26183
|
+
const problem = actAsProblem(context);
|
|
26184
|
+
if (problem !== void 0) throw actAsNotApplicable(problem);
|
|
26185
|
+
}
|
|
26186
|
+
function openClient(context, io, options = {}) {
|
|
26187
|
+
checkActAs(context);
|
|
26188
|
+
const endpoint = requireEndpoint(context);
|
|
26189
|
+
const http = { debug: context.debug, fetch: io.fetch };
|
|
26190
|
+
const org = {
|
|
26191
|
+
...context.org === void 0 ? {} : { org: context.org },
|
|
26192
|
+
...options.queue === false ? {} : { beforeFirst: queueSender(context, endpoint, io) }
|
|
26193
|
+
};
|
|
26194
|
+
if (context.token === void 0 && context.credential === void 0) {
|
|
26195
|
+
if (!isLoopback(endpoint)) throw notLoggedIn(endpoint);
|
|
26196
|
+
return new ApiClient({
|
|
26197
|
+
endpoint,
|
|
26198
|
+
authorizer: devAuthorizer(endpoint, context.actAs, http),
|
|
26199
|
+
...org,
|
|
26200
|
+
...http
|
|
26201
|
+
});
|
|
26202
|
+
}
|
|
26203
|
+
return new ApiClient({
|
|
26204
|
+
endpoint,
|
|
25115
26205
|
authorizer: createAuthorizer(endpoint, { env: io.env, ...http }),
|
|
25116
26206
|
...org,
|
|
25117
26207
|
...http
|
|
@@ -25125,17 +26215,26 @@ function openOrgClient(context, io) {
|
|
|
25125
26215
|
function openBackend(context, io) {
|
|
25126
26216
|
if (context.mode === "local") {
|
|
25127
26217
|
checkActAs(context);
|
|
25128
|
-
|
|
26218
|
+
const local = new FileStore(context.storePath);
|
|
26219
|
+
return { store: local, client: void 0, local };
|
|
25129
26220
|
}
|
|
25130
26221
|
const client = openOrgClient(context, io);
|
|
25131
|
-
return { store: new RemoteStore(client), client };
|
|
26222
|
+
return { store: new RemoteStore(client), client, local: void 0 };
|
|
25132
26223
|
}
|
|
25133
26224
|
function openStore(context, io) {
|
|
25134
26225
|
return openBackend(context, io).store;
|
|
25135
26226
|
}
|
|
26227
|
+
function needsServer(command, context) {
|
|
26228
|
+
return new ConfigError(
|
|
26229
|
+
context.endpoint === void 0 ? `${command} needs a server: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc` : `${command} needs a server: drop --local`
|
|
26230
|
+
);
|
|
26231
|
+
}
|
|
26232
|
+
function requireServer(command, context) {
|
|
26233
|
+
if (context.mode !== "remote") throw needsServer(command, context);
|
|
26234
|
+
}
|
|
25136
26235
|
|
|
25137
26236
|
// src/commands/complain.ts
|
|
25138
|
-
var REMOTE_REPORTER_MESSAGE = "--reporter
|
|
26237
|
+
var REMOTE_REPORTER_MESSAGE = "--reporter only works in local mode: the server sets the reporter";
|
|
25139
26238
|
async function runComplain(message, options, context, io) {
|
|
25140
26239
|
const project = requireProject(context);
|
|
25141
26240
|
const org = requireOrg(context);
|
|
@@ -25153,11 +26252,18 @@ async function runComplain(message, options, context, io) {
|
|
|
25153
26252
|
...options.tag === void 0 || options.tag.length === 0 ? {} : { tags: options.tag },
|
|
25154
26253
|
agent: context.agent,
|
|
25155
26254
|
reporter: context.reporter,
|
|
25156
|
-
...context.cwd === void 0 ? {} : { cwd: context.cwd }
|
|
26255
|
+
...context.cwd === void 0 ? {} : { cwd: context.cwd },
|
|
26256
|
+
...options.cmd === void 0 ? {} : { cmd: options.cmd }
|
|
25157
26257
|
};
|
|
25158
|
-
|
|
25159
|
-
|
|
25160
|
-
|
|
26258
|
+
let complaint;
|
|
26259
|
+
try {
|
|
26260
|
+
complaint = await createComplaint(store, input2, {
|
|
26261
|
+
allowedTags: context.allowedTags
|
|
26262
|
+
});
|
|
26263
|
+
} catch (error62) {
|
|
26264
|
+
if (!(error62 instanceof UndeliveredError)) throw error62;
|
|
26265
|
+
return queueComplaint(error62, org, project, context, io, options.json === true);
|
|
26266
|
+
}
|
|
25161
26267
|
if (options.json === true) {
|
|
25162
26268
|
writeJson(io.stdout, complaint);
|
|
25163
26269
|
return 0;
|
|
@@ -25167,6 +26273,35 @@ async function runComplain(message, options, context, io) {
|
|
|
25167
26273
|
io.stdout,
|
|
25168
26274
|
`Filed complaint ${complaint.id} for ${scope} (${complaint.severity})`
|
|
25169
26275
|
);
|
|
26276
|
+
for (const line of matchLines(complaint)) writeLine(io.stdout, line);
|
|
26277
|
+
return 0;
|
|
26278
|
+
}
|
|
26279
|
+
function matchLines(complaint) {
|
|
26280
|
+
const lines = [];
|
|
26281
|
+
if (complaint.openMatches === 1) {
|
|
26282
|
+
lines.push("1 open complaint already shares this headline.");
|
|
26283
|
+
} else if (complaint.openMatches > 1) {
|
|
26284
|
+
lines.push(`${complaint.openMatches} open complaints already share this headline.`);
|
|
26285
|
+
}
|
|
26286
|
+
if (complaint.recurred) {
|
|
26287
|
+
lines.push("A resolved complaint shares this headline: this one is marked recurred.");
|
|
26288
|
+
}
|
|
26289
|
+
return lines;
|
|
26290
|
+
}
|
|
26291
|
+
function queueComplaint(error62, org, project, context, io, json2) {
|
|
26292
|
+
const path = queuePath(io.env);
|
|
26293
|
+
enqueue(path, {
|
|
26294
|
+
endpoint: requireEndpoint(context),
|
|
26295
|
+
org,
|
|
26296
|
+
auth: authLabel(context),
|
|
26297
|
+
body: error62.body,
|
|
26298
|
+
key: error62.key
|
|
26299
|
+
});
|
|
26300
|
+
writeLine(
|
|
26301
|
+
io.stderr,
|
|
26302
|
+
`quibble: queued the complaint for ${org}/${project} in ${path}: ${error62.reason}. It is sent with the next remote command under the same credential, or run \`quibble retry\`.`
|
|
26303
|
+
);
|
|
26304
|
+
if (json2) writeJson(io.stdout, { queued: true, org, project, reason: error62.reason });
|
|
25170
26305
|
return 0;
|
|
25171
26306
|
}
|
|
25172
26307
|
async function readMessage(message, io) {
|
|
@@ -25208,7 +26343,9 @@ function runConfig(options, context, io) {
|
|
|
25208
26343
|
agent: context.agent,
|
|
25209
26344
|
reporter: context.reporter,
|
|
25210
26345
|
actAs: context.actAs ?? null,
|
|
25211
|
-
auth: describeAuth(context)
|
|
26346
|
+
auth: describeAuth(context),
|
|
26347
|
+
// Every credential counts, and nothing is sent.
|
|
26348
|
+
unsent: context.mode === "remote" && context.endpoint !== void 0 ? pendingFor(queuePath(io.env), context.endpoint).length : 0
|
|
25212
26349
|
};
|
|
25213
26350
|
if (options.json === true) {
|
|
25214
26351
|
writeJson(io.stdout, report);
|
|
@@ -25222,31 +26359,50 @@ function runConfig(options, context, io) {
|
|
|
25222
26359
|
}
|
|
25223
26360
|
|
|
25224
26361
|
// src/commands/init.ts
|
|
25225
|
-
import { existsSync, writeFileSync as
|
|
25226
|
-
import { basename, join as
|
|
25227
|
-
var SNIPPET_INTRO = `## Filing complaints
|
|
26362
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
26363
|
+
import { basename as basename2, join as join6, resolve as resolve3 } from "node:path";
|
|
25228
26364
|
|
|
25229
|
-
|
|
25230
|
-
|
|
25231
|
-
|
|
25232
|
-
var
|
|
25233
|
-
|
|
25234
|
-
|
|
25235
|
-
|
|
25236
|
-
|
|
25237
|
-
|
|
25238
|
-
|
|
25239
|
-
|
|
25240
|
-
|
|
25241
|
-
|
|
25242
|
-
|
|
26365
|
+
// src/snippet.ts
|
|
26366
|
+
import { basename } from "node:path";
|
|
26367
|
+
var SNIPPET_HEADING = "## Filing complaints";
|
|
26368
|
+
var SKILL_HEADING = "# Filing complaints";
|
|
26369
|
+
var SKILL_PATH = ".claude/skills/quibble/SKILL.md";
|
|
26370
|
+
var DEFAULT_SNIPPET_AGENT = "claude";
|
|
26371
|
+
var AGENTS_FILE_AGENT = "codex";
|
|
26372
|
+
var SNIPPET_INTRO = `This project uses Quibble. When the build, tests, docs or tooling waste your
|
|
26373
|
+
time, file a complaint instead of quietly working around the problem.`;
|
|
26374
|
+
var SNIPPET_USAGE = `- Set severity with \`-s low|medium|high\`. Add tags with \`-t <tag>\` (repeatable).
|
|
26375
|
+
- Put a one-line summary first; it is the headline everywhere. Add details below.
|
|
26376
|
+
- Name the command that failed with \`--cmd "<command>"\`.
|
|
26377
|
+
- Pipe a long message on stdin with \`quibble complain -\`.
|
|
26378
|
+
- File one complaint per problem, as soon as you hit it, not at the end of the task.`;
|
|
26379
|
+
var SKILL_FRONT_MATTER = `---
|
|
26380
|
+
name: quibble
|
|
26381
|
+
description: File a Quibble complaint when the build, tests, docs or tooling of this project waste your time. Use it as soon as you hit the problem, not at the end of the task.
|
|
26382
|
+
---`;
|
|
25243
26383
|
var SNIPPET_LOGIN = "Run `quibble login` once before the first complaint.";
|
|
25244
26384
|
function orgSentence(org) {
|
|
25245
26385
|
return `Complaints go to the \`${org}\` organization.`;
|
|
25246
26386
|
}
|
|
26387
|
+
function exampleCommand(tag, agent) {
|
|
26388
|
+
return ` quibble complain "what went wrong, and what you expected" -s medium -t ${tag} --agent ${agent}`;
|
|
26389
|
+
}
|
|
26390
|
+
function tagRules(tags) {
|
|
26391
|
+
const list = tags.map((tag) => `\`${tag}\``).join(", ");
|
|
26392
|
+
return `Every complaint needs at least one of these tags: ${list}.
|
|
26393
|
+
If none fits, pass \`-t ${OTHER_TAG_PREFIX}<name>\` naming the tag you wanted, such as
|
|
26394
|
+
\`-t ${OTHER_TAG_PREFIX}network\`. Run \`quibble tags\` to see the list.`;
|
|
26395
|
+
}
|
|
25247
26396
|
function agentSnippet(tags, scope = {}) {
|
|
25248
26397
|
const first = tags?.[0];
|
|
25249
|
-
const
|
|
26398
|
+
const agent = scope.agent ?? DEFAULT_SNIPPET_AGENT;
|
|
26399
|
+
const blocks = [
|
|
26400
|
+
SNIPPET_HEADING,
|
|
26401
|
+
SNIPPET_INTRO,
|
|
26402
|
+
exampleCommand(first ?? "build", agent)
|
|
26403
|
+
];
|
|
26404
|
+
if (tags !== void 0 && first !== void 0) blocks.push(tagRules(tags));
|
|
26405
|
+
blocks.push(SNIPPET_USAGE);
|
|
25250
26406
|
const sentences = [];
|
|
25251
26407
|
const { endpoint, org } = scope;
|
|
25252
26408
|
if (endpoint !== void 0 && endpoint !== "") sentences.push(SNIPPET_LOGIN);
|
|
@@ -25254,54 +26410,207 @@ function agentSnippet(tags, scope = {}) {
|
|
|
25254
26410
|
if (sentences.length > 0) blocks.push(sentences.join(" "));
|
|
25255
26411
|
return blocks.join("\n\n");
|
|
25256
26412
|
}
|
|
25257
|
-
function
|
|
25258
|
-
const
|
|
25259
|
-
return
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25265
|
-
|
|
25266
|
-
|
|
26413
|
+
function skillFile(snippet) {
|
|
26414
|
+
const body = snippet.startsWith(SNIPPET_HEADING) ? `${SKILL_HEADING}${snippet.slice(SNIPPET_HEADING.length)}` : snippet;
|
|
26415
|
+
return `${SKILL_FRONT_MATTER}
|
|
26416
|
+
|
|
26417
|
+
${body}
|
|
26418
|
+
`;
|
|
26419
|
+
}
|
|
26420
|
+
function agentForFile(path) {
|
|
26421
|
+
return basename(path).toLowerCase() === "agents.md" ? AGENTS_FILE_AGENT : DEFAULT_SNIPPET_AGENT;
|
|
26422
|
+
}
|
|
26423
|
+
function hasSnippet(text) {
|
|
26424
|
+
return text.split(/\r?\n/).some((line) => line.trimEnd() === SNIPPET_HEADING);
|
|
26425
|
+
}
|
|
26426
|
+
|
|
26427
|
+
// src/commands/snippet.ts
|
|
26428
|
+
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
26429
|
+
import { dirname as dirname5, join as join5, resolve as resolve2 } from "node:path";
|
|
26430
|
+
function existsError(path) {
|
|
26431
|
+
return new UsageError(`${path} already exists: pass --force to overwrite it`);
|
|
26432
|
+
}
|
|
26433
|
+
function readOptional2(path) {
|
|
26434
|
+
try {
|
|
26435
|
+
return readFileSync4(path, "utf8");
|
|
26436
|
+
} catch (error62) {
|
|
26437
|
+
if (isNotFound(error62)) return "";
|
|
26438
|
+
throw error62;
|
|
26439
|
+
}
|
|
26440
|
+
}
|
|
26441
|
+
function writeSkill(path, snippet) {
|
|
26442
|
+
mkdirSync3(dirname5(path), { recursive: true });
|
|
26443
|
+
writeFileSync2(path, skillFile(snippet), "utf8");
|
|
26444
|
+
}
|
|
26445
|
+
async function runSnippet(options, context, io) {
|
|
26446
|
+
const project = requireProject(context);
|
|
26447
|
+
const override = options.agent === void 0 ? void 0 : parseAgentName(options.agent);
|
|
26448
|
+
const appendPath = options.append === void 0 ? void 0 : resolve2(io.cwd, options.append);
|
|
26449
|
+
const skillPath = options.skill === true ? join5(context.projectRoot ?? io.cwd, SKILL_PATH) : void 0;
|
|
26450
|
+
const existing = appendPath === void 0 ? "" : readOptional2(appendPath);
|
|
26451
|
+
if (options.append !== void 0 && hasSnippet(existing)) {
|
|
26452
|
+
throw new UsageError(
|
|
26453
|
+
`${options.append} already has a "${SNIPPET_HEADING}" section: edit it by hand`
|
|
26454
|
+
);
|
|
26455
|
+
}
|
|
26456
|
+
if (skillPath !== void 0 && existsSync(skillPath) && options.force !== true) {
|
|
26457
|
+
throw existsError(skillPath);
|
|
26458
|
+
}
|
|
26459
|
+
let tags;
|
|
26460
|
+
if (context.mode === "remote") {
|
|
26461
|
+
tags = (await openOrgClient(context, io).project(project)).tags;
|
|
26462
|
+
} else {
|
|
26463
|
+
checkActAs(context);
|
|
26464
|
+
tags = context.allowedTags;
|
|
26465
|
+
}
|
|
26466
|
+
const scope = {
|
|
26467
|
+
...context.mode === "remote" ? { endpoint: context.endpoint } : {},
|
|
26468
|
+
...context.slug === void 0 ? {} : { org: context.slug }
|
|
26469
|
+
};
|
|
26470
|
+
let shown = { agent: override ?? DEFAULT_SNIPPET_AGENT, snippet: "" };
|
|
26471
|
+
shown.snippet = agentSnippet(tags, { ...scope, agent: shown.agent });
|
|
26472
|
+
if (appendPath !== void 0) {
|
|
26473
|
+
const agent = override ?? agentForFile(appendPath);
|
|
26474
|
+
const snippet = agentSnippet(tags, { ...scope, agent });
|
|
26475
|
+
const kept = existing.replace(/\s+$/, "");
|
|
26476
|
+
writeFileSync2(appendPath, kept === "" ? `${snippet}
|
|
26477
|
+
` : `${kept}
|
|
26478
|
+
|
|
26479
|
+
${snippet}
|
|
26480
|
+
`);
|
|
26481
|
+
shown = { agent, snippet };
|
|
26482
|
+
}
|
|
26483
|
+
if (skillPath !== void 0) {
|
|
26484
|
+
const agent = override ?? DEFAULT_SNIPPET_AGENT;
|
|
26485
|
+
const snippet = agentSnippet(tags, { ...scope, agent });
|
|
26486
|
+
writeSkill(skillPath, snippet);
|
|
26487
|
+
if (appendPath === void 0) shown = { agent, snippet };
|
|
26488
|
+
}
|
|
26489
|
+
if (options.json === true) {
|
|
26490
|
+
writeJson(io.stdout, {
|
|
26491
|
+
snippet: shown.snippet,
|
|
26492
|
+
agent: shown.agent,
|
|
26493
|
+
appended: appendPath ?? null,
|
|
26494
|
+
skill: skillPath ?? null
|
|
26495
|
+
});
|
|
26496
|
+
return 0;
|
|
26497
|
+
}
|
|
26498
|
+
if (appendPath === void 0 && skillPath === void 0) {
|
|
26499
|
+
writeLine(io.stdout, shown.snippet);
|
|
26500
|
+
return 0;
|
|
26501
|
+
}
|
|
26502
|
+
if (appendPath !== void 0) {
|
|
26503
|
+
writeLine(io.stdout, `Appended the Quibble snippet to ${appendPath}`);
|
|
26504
|
+
}
|
|
26505
|
+
if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
|
|
26506
|
+
return 0;
|
|
26507
|
+
}
|
|
26508
|
+
|
|
26509
|
+
// src/commands/init.ts
|
|
26510
|
+
var CODEX_LINE = "For Codex, run `quibble snippet --append AGENTS.md`, which names the agent codex.";
|
|
26511
|
+
function agentLead(agent) {
|
|
26512
|
+
return agent === DEFAULT_SNIPPET_AGENT ? "Add this to your CLAUDE.md:" : "Add this to your agent's instructions file:";
|
|
26513
|
+
}
|
|
26514
|
+
async function runInit(options, io) {
|
|
26515
|
+
const directory = resolve3(io.cwd);
|
|
26516
|
+
const path = join6(directory, PROJECT_CONFIG_FILENAME);
|
|
26517
|
+
if (existsSync2(path) && options.force !== true) {
|
|
25267
26518
|
throw new ConfigError(`${path} already exists: pass --force to overwrite it`, path);
|
|
25268
26519
|
}
|
|
26520
|
+
const skillPath = options.skill === true ? join6(directory, SKILL_PATH) : void 0;
|
|
26521
|
+
if (skillPath !== void 0 && existsSync2(skillPath) && options.force !== true) {
|
|
26522
|
+
throw existsError(skillPath);
|
|
26523
|
+
}
|
|
25269
26524
|
const project = resolveProjectName(options.project, directory);
|
|
25270
26525
|
const org = options.org === void 0 || options.org.trim() === "" ? void 0 : parseOrgSlug(options.org);
|
|
25271
26526
|
const endpoint = options.endpoint?.trim();
|
|
25272
26527
|
const tags = options.tag === void 0 ? void 0 : parseAllowedTags(options.tag);
|
|
26528
|
+
const agent = options.agent === void 0 ? DEFAULT_SNIPPET_AGENT : parseAgentName(options.agent);
|
|
25273
26529
|
const config2 = {
|
|
25274
26530
|
project,
|
|
25275
26531
|
...org === void 0 ? {} : { org },
|
|
25276
26532
|
...endpoint === void 0 || endpoint === "" ? {} : { endpoint },
|
|
25277
26533
|
...tags === void 0 ? {} : { tags }
|
|
25278
26534
|
};
|
|
25279
|
-
|
|
26535
|
+
writeFileSync3(path, `${JSON.stringify(config2, null, 2)}
|
|
25280
26536
|
`, "utf8");
|
|
26537
|
+
const snippet = agentSnippet(tags, {
|
|
26538
|
+
agent,
|
|
26539
|
+
...endpoint === void 0 ? {} : { endpoint },
|
|
26540
|
+
...org === void 0 ? {} : { org }
|
|
26541
|
+
});
|
|
26542
|
+
if (skillPath !== void 0) writeSkill(skillPath, snippet);
|
|
26543
|
+
const check2 = await checkServer(options, project, io);
|
|
25281
26544
|
if (options.json === true) {
|
|
25282
|
-
writeJson(io.stdout, {
|
|
26545
|
+
writeJson(io.stdout, {
|
|
26546
|
+
path,
|
|
26547
|
+
project,
|
|
26548
|
+
org: org ?? null,
|
|
26549
|
+
tags: tags ?? null,
|
|
26550
|
+
skill: skillPath ?? null,
|
|
26551
|
+
server: check2.server
|
|
26552
|
+
});
|
|
25283
26553
|
return 0;
|
|
25284
26554
|
}
|
|
25285
26555
|
writeLine(io.stdout, `Wrote ${path}`);
|
|
26556
|
+
if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
|
|
25286
26557
|
writeLine(io.stdout, `Project: ${project}`);
|
|
25287
26558
|
if (org !== void 0) writeLine(io.stdout, `Organization: ${org}`);
|
|
25288
26559
|
if (tags !== void 0) writeLine(io.stdout, `Tags: ${tags.join(", ")}`);
|
|
25289
26560
|
writeLine(io.stdout);
|
|
25290
|
-
writeLine(io.stdout,
|
|
26561
|
+
writeLine(io.stdout, agentLead(agent));
|
|
25291
26562
|
writeLine(io.stdout);
|
|
25292
|
-
writeLine(
|
|
25293
|
-
|
|
25294
|
-
|
|
25295
|
-
|
|
25296
|
-
|
|
25297
|
-
|
|
25298
|
-
|
|
26563
|
+
writeLine(io.stdout, snippet);
|
|
26564
|
+
if (agent === DEFAULT_SNIPPET_AGENT) {
|
|
26565
|
+
writeLine(io.stdout);
|
|
26566
|
+
writeLine(io.stdout, CODEX_LINE);
|
|
26567
|
+
}
|
|
26568
|
+
if (check2.line !== void 0) {
|
|
26569
|
+
writeLine(io.stdout);
|
|
26570
|
+
writeLine(io.stdout, check2.line);
|
|
26571
|
+
}
|
|
25299
26572
|
return 0;
|
|
25300
26573
|
}
|
|
26574
|
+
async function checkServer(options, project, io) {
|
|
26575
|
+
const context = {
|
|
26576
|
+
...resolveContext({
|
|
26577
|
+
cwd: io.cwd,
|
|
26578
|
+
env: io.env,
|
|
26579
|
+
endpoint: options.endpoint,
|
|
26580
|
+
org: options.org,
|
|
26581
|
+
local: options.local
|
|
26582
|
+
}),
|
|
26583
|
+
actAs: void 0
|
|
26584
|
+
};
|
|
26585
|
+
const { endpoint, org } = context;
|
|
26586
|
+
if (context.mode !== "remote" || endpoint === void 0 || org === void 0 || context.token === void 0 && context.credential === void 0 && !isLoopback(endpoint)) {
|
|
26587
|
+
return { server: "unchecked" };
|
|
26588
|
+
}
|
|
26589
|
+
const warn = (text) => {
|
|
26590
|
+
writeLine(io.stderr, `quibble: warning: ${text}`);
|
|
26591
|
+
};
|
|
26592
|
+
try {
|
|
26593
|
+
await openClient(context, io, { queue: false }).project(project);
|
|
26594
|
+
return { server: "found", line: `Project ${org}/${project} exists on ${endpoint}.` };
|
|
26595
|
+
} catch (error62) {
|
|
26596
|
+
if (error62 instanceof RemoteError && error62.status === 404) {
|
|
26597
|
+
if (error62.serverCode === ORG_NOT_FOUND) {
|
|
26598
|
+
warn(`no organization ${org} on ${endpoint} for this account`);
|
|
26599
|
+
return { server: "no_org" };
|
|
26600
|
+
}
|
|
26601
|
+
warn(
|
|
26602
|
+
`${org} has no project ${project} on ${endpoint}: an admin creates it in the organization settings`
|
|
26603
|
+
);
|
|
26604
|
+
return { server: "no_project" };
|
|
26605
|
+
}
|
|
26606
|
+
warn(`could not check the project on ${endpoint}: ${describeError(error62)}`);
|
|
26607
|
+
return { server: "unreachable" };
|
|
26608
|
+
}
|
|
26609
|
+
}
|
|
25301
26610
|
function resolveProjectName(given, directory) {
|
|
25302
26611
|
const candidate = given?.trim();
|
|
25303
26612
|
if (candidate !== void 0 && candidate !== "") return parseProjectName(candidate);
|
|
25304
|
-
const name =
|
|
26613
|
+
const name = basename2(directory);
|
|
25305
26614
|
try {
|
|
25306
26615
|
return parseProjectName(name);
|
|
25307
26616
|
} catch (error62) {
|
|
@@ -25332,25 +26641,30 @@ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
25332
26641
|
var TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
25333
26642
|
var MINUTE_MS = 6e4;
|
|
25334
26643
|
var HOUR_MS = 60 * MINUTE_MS;
|
|
25335
|
-
var
|
|
26644
|
+
var DAY_MS3 = 24 * HOUR_MS;
|
|
25336
26645
|
var UNIT_MS = {
|
|
25337
26646
|
m: MINUTE_MS,
|
|
25338
26647
|
h: HOUR_MS,
|
|
25339
|
-
d:
|
|
25340
|
-
w: 7 *
|
|
26648
|
+
d: DAY_MS3,
|
|
26649
|
+
w: 7 * DAY_MS3
|
|
25341
26650
|
};
|
|
25342
|
-
|
|
25343
|
-
|
|
26651
|
+
var EXPIRES_MESSAGE = "--expires must be a duration like 30d, a date like 2026-12-01, or an ISO 8601 UTC timestamp";
|
|
26652
|
+
function reject(message = SINCE_MESSAGE) {
|
|
26653
|
+
throw new UsageError(message);
|
|
25344
26654
|
}
|
|
25345
26655
|
function iso(at) {
|
|
25346
26656
|
if (Number.isNaN(at.getTime())) reject();
|
|
25347
26657
|
return at.toISOString();
|
|
25348
26658
|
}
|
|
25349
|
-
function
|
|
25350
|
-
if (digits.startsWith("0"))
|
|
26659
|
+
function durationMs(digits, unit) {
|
|
26660
|
+
if (digits.startsWith("0")) return void 0;
|
|
25351
26661
|
const step = UNIT_MS[unit];
|
|
25352
|
-
|
|
25353
|
-
|
|
26662
|
+
return step === void 0 ? void 0 : Number(digits) * step;
|
|
26663
|
+
}
|
|
26664
|
+
function fromDuration(digits, unit, now) {
|
|
26665
|
+
const span = durationMs(digits, unit);
|
|
26666
|
+
if (span === void 0) reject();
|
|
26667
|
+
return iso(new Date(now.getTime() - span));
|
|
25354
26668
|
}
|
|
25355
26669
|
function fromDate(value) {
|
|
25356
26670
|
const at = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
@@ -25368,6 +26682,27 @@ function parseSince(text, now) {
|
|
|
25368
26682
|
if (TIMESTAMP_RE.test(value)) return iso(new Date(value));
|
|
25369
26683
|
return reject();
|
|
25370
26684
|
}
|
|
26685
|
+
function parseExpires(text, now) {
|
|
26686
|
+
const value = text.trim();
|
|
26687
|
+
const duration3 = DURATION_RE.exec(value);
|
|
26688
|
+
let stamp2;
|
|
26689
|
+
try {
|
|
26690
|
+
if (duration3 !== null) {
|
|
26691
|
+
const span = durationMs(duration3[1] ?? "", duration3[2] ?? "");
|
|
26692
|
+
if (span === void 0) reject(EXPIRES_MESSAGE);
|
|
26693
|
+
stamp2 = iso(new Date(now.getTime() + span));
|
|
26694
|
+
} else if (DATE_RE.test(value)) {
|
|
26695
|
+
stamp2 = fromDate(value);
|
|
26696
|
+
} else if (TIMESTAMP_RE.test(value)) {
|
|
26697
|
+
stamp2 = iso(new Date(value));
|
|
26698
|
+
} else {
|
|
26699
|
+
return reject(EXPIRES_MESSAGE);
|
|
26700
|
+
}
|
|
26701
|
+
} catch {
|
|
26702
|
+
return reject(EXPIRES_MESSAGE);
|
|
26703
|
+
}
|
|
26704
|
+
return stamp2;
|
|
26705
|
+
}
|
|
25371
26706
|
|
|
25372
26707
|
// src/scope.ts
|
|
25373
26708
|
function resolveScope(options, context, now = /* @__PURE__ */ new Date()) {
|
|
@@ -25387,6 +26722,9 @@ function resolveScope(options, context, now = /* @__PURE__ */ new Date()) {
|
|
|
25387
26722
|
...options.since === void 0 ? {} : { since: parseSince(options.since, now) }
|
|
25388
26723
|
};
|
|
25389
26724
|
}
|
|
26725
|
+
function tagFilter(tags) {
|
|
26726
|
+
return tags === void 0 || tags.length === 0 ? {} : { tags: [...tags] };
|
|
26727
|
+
}
|
|
25390
26728
|
|
|
25391
26729
|
// src/commands/list.ts
|
|
25392
26730
|
var COLUMNS = [
|
|
@@ -25396,57 +26734,104 @@ var COLUMNS = [
|
|
|
25396
26734
|
"status",
|
|
25397
26735
|
"project",
|
|
25398
26736
|
"reporter",
|
|
26737
|
+
"tags",
|
|
26738
|
+
"group",
|
|
25399
26739
|
"summary"
|
|
25400
26740
|
];
|
|
25401
26741
|
var REPORTER_WIDTH = 20;
|
|
26742
|
+
var TAGS_WIDTH = 24;
|
|
25402
26743
|
var SUMMARY_MAX = 60;
|
|
26744
|
+
var RECURRED_MARK = "\u21BB";
|
|
26745
|
+
function cut2(text, width) {
|
|
26746
|
+
if (text.length <= width) return text;
|
|
26747
|
+
return `${text.slice(0, width - 3)}...`;
|
|
26748
|
+
}
|
|
25403
26749
|
function summaryOf(complaint) {
|
|
25404
|
-
|
|
25405
|
-
if (summary.length <= SUMMARY_MAX) return summary;
|
|
25406
|
-
return `${summary.slice(0, SUMMARY_MAX - 3)}...`;
|
|
26750
|
+
return cut2(printable(summarize(complaint.message)), SUMMARY_MAX);
|
|
25407
26751
|
}
|
|
26752
|
+
function limitNote(count, limit, more) {
|
|
26753
|
+
if (limit >= MAX_LIMIT) {
|
|
26754
|
+
return count >= MAX_LIMIT ? `Showing the newest ${MAX_LIMIT} complaints: older ones may exist, narrow the filters to see them.` : void 0;
|
|
26755
|
+
}
|
|
26756
|
+
return more ? `Showing the newest ${limit} complaints: pass --limit to see more (up to ${MAX_LIMIT}).` : void 0;
|
|
26757
|
+
}
|
|
26758
|
+
var BUDGET_NOTE = "The server stopped searching early, so older matches may exist: narrow the filters or pass --since.";
|
|
25408
26759
|
async function runList(options, context, io) {
|
|
26760
|
+
if (options.ids === true && options.json === true) {
|
|
26761
|
+
throw new UsageError("--ids and --json cannot be used together");
|
|
26762
|
+
}
|
|
25409
26763
|
const { store, client } = openBackend(context, io);
|
|
25410
26764
|
const reporter = options.mine === true ? await resolveMine(context, client, io.env) : void 0;
|
|
25411
26765
|
const filter = parseListFilter({
|
|
25412
26766
|
...resolveScope(options, context),
|
|
25413
26767
|
...options.severity === void 0 ? {} : { severity: options.severity },
|
|
25414
|
-
...options.tag
|
|
26768
|
+
...tagFilter(options.tag),
|
|
26769
|
+
...options.search === void 0 ? {} : { q: options.search },
|
|
25415
26770
|
...reporter === void 0 ? {} : { reporter },
|
|
25416
26771
|
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
25417
26772
|
});
|
|
25418
|
-
const
|
|
26773
|
+
const limit = filter.limit ?? DEFAULT_LIMIT;
|
|
26774
|
+
const page = await listComplaintPage(store, {
|
|
26775
|
+
...filter,
|
|
26776
|
+
limit: Math.min(limit + 1, MAX_LIMIT)
|
|
26777
|
+
});
|
|
26778
|
+
const complaints = page.complaints.slice(0, limit);
|
|
25419
26779
|
if (options.json === true) {
|
|
25420
26780
|
writeJson(io.stdout, complaints);
|
|
25421
26781
|
return 0;
|
|
25422
26782
|
}
|
|
26783
|
+
const note = limitNote(complaints.length, limit, page.complaints.length > limit);
|
|
26784
|
+
if (note !== void 0) writeLine(io.stderr, note);
|
|
26785
|
+
if (page.truncated) writeLine(io.stderr, BUDGET_NOTE);
|
|
26786
|
+
if (options.ids === true) {
|
|
26787
|
+
for (const complaint of complaints) writeLine(io.stdout, complaint.id);
|
|
26788
|
+
return 0;
|
|
26789
|
+
}
|
|
25423
26790
|
if (complaints.length === 0) {
|
|
25424
26791
|
writeLine(io.stdout, "No complaints found.");
|
|
25425
26792
|
return 0;
|
|
25426
26793
|
}
|
|
25427
|
-
|
|
26794
|
+
const lines = options.ungroup === true ? formatTable(complaints.map((complaint) => ({ complaint }))) : formatTable(
|
|
26795
|
+
groupComplaints(complaints).map((group) => ({
|
|
26796
|
+
complaint: group.head,
|
|
26797
|
+
group
|
|
26798
|
+
}))
|
|
26799
|
+
);
|
|
26800
|
+
for (const line of lines) writeLine(io.stdout, line);
|
|
25428
26801
|
return 0;
|
|
25429
26802
|
}
|
|
25430
26803
|
function reporterOf(complaint) {
|
|
25431
|
-
|
|
25432
|
-
|
|
25433
|
-
|
|
25434
|
-
}
|
|
25435
|
-
|
|
25436
|
-
|
|
25437
|
-
|
|
25438
|
-
|
|
25439
|
-
|
|
25440
|
-
|
|
25441
|
-
|
|
25442
|
-
|
|
25443
|
-
|
|
25444
|
-
|
|
25445
|
-
|
|
25446
|
-
|
|
26804
|
+
return cut2(printable(complaint.reporterName ?? complaint.reporter), REPORTER_WIDTH);
|
|
26805
|
+
}
|
|
26806
|
+
function groupCell(group) {
|
|
26807
|
+
const count = group.count > 1 ? `\xD7${group.count}` : "";
|
|
26808
|
+
return `${count}${group.recurred ? RECURRED_MARK : ""}`;
|
|
26809
|
+
}
|
|
26810
|
+
function formatTable(rows2) {
|
|
26811
|
+
const grouped = rows2.some((row) => row.group !== void 0);
|
|
26812
|
+
const columns = COLUMNS.filter((name) => grouped || name !== "group");
|
|
26813
|
+
const cells = rows2.map(({ complaint, group }) => {
|
|
26814
|
+
const values = {
|
|
26815
|
+
id: complaint.id.slice(0, 8),
|
|
26816
|
+
created: complaint.createdAt.slice(0, 16),
|
|
26817
|
+
severity: complaint.severity,
|
|
26818
|
+
status: complaint.status,
|
|
26819
|
+
project: complaint.project,
|
|
26820
|
+
reporter: reporterOf(complaint),
|
|
26821
|
+
tags: cut2(printable(complaint.tags.join(",")), TAGS_WIDTH),
|
|
26822
|
+
group: group === void 0 ? "" : groupCell(group),
|
|
26823
|
+
summary: summaryOf(complaint)
|
|
26824
|
+
};
|
|
26825
|
+
return columns.map((name) => values[name]);
|
|
26826
|
+
});
|
|
26827
|
+
const widths = columns.map(
|
|
26828
|
+
(name, column) => cells.reduce(
|
|
26829
|
+
(width, row) => Math.max(width, (row[column] ?? "").length),
|
|
26830
|
+
name.length
|
|
26831
|
+
)
|
|
25447
26832
|
);
|
|
25448
|
-
const format = (
|
|
25449
|
-
return [format(
|
|
26833
|
+
const format = (line) => line.map((cell2, column) => cell2.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
|
|
26834
|
+
return [format(columns), ...cells.map(format)];
|
|
25450
26835
|
}
|
|
25451
26836
|
|
|
25452
26837
|
// src/commands/orgs.ts
|
|
@@ -25571,8 +26956,8 @@ var NodeTerminal = class {
|
|
|
25571
26956
|
const queued = this.#lines.shift();
|
|
25572
26957
|
if (queued !== void 0) return Promise.resolve(queued);
|
|
25573
26958
|
if (this.#closed) return Promise.resolve("");
|
|
25574
|
-
return new Promise((
|
|
25575
|
-
this.#waiting =
|
|
26959
|
+
return new Promise((resolve4) => {
|
|
26960
|
+
this.#waiting = resolve4;
|
|
25576
26961
|
});
|
|
25577
26962
|
}
|
|
25578
26963
|
async #read(label, hidden) {
|
|
@@ -25610,7 +26995,7 @@ function nodeTerminal(input2, output2) {
|
|
|
25610
26995
|
|
|
25611
26996
|
// src/commands/login.ts
|
|
25612
26997
|
var LOCAL_IDENTITY_MESSAGE = "local mode: no identity.";
|
|
25613
|
-
var NEEDS_TERMINAL_MESSAGE = "quibble login needs a terminal:
|
|
26998
|
+
var NEEDS_TERMINAL_MESSAGE = "quibble login needs a terminal: in CI, set QUIBBLE_TOKEN instead";
|
|
25614
26999
|
var MFA_CHALLENGE = "SOFTWARE_TOKEN_MFA";
|
|
25615
27000
|
var NEW_PASSWORD_CHALLENGE = "NEW_PASSWORD_REQUIRED";
|
|
25616
27001
|
var LOGIN_PAGE = "/login";
|
|
@@ -25654,7 +27039,7 @@ async function signIn(terminal, identity, config2) {
|
|
|
25654
27039
|
);
|
|
25655
27040
|
}
|
|
25656
27041
|
throw new RemoteError(
|
|
25657
|
-
`
|
|
27042
|
+
`this account needs a sign-in step that quibble doesn't support (${name})`
|
|
25658
27043
|
);
|
|
25659
27044
|
}
|
|
25660
27045
|
return result.tokens;
|
|
@@ -25678,7 +27063,7 @@ async function runLogin(options, context, io) {
|
|
|
25678
27063
|
const http = { debug: context.debug, fetch: io.fetch };
|
|
25679
27064
|
const config2 = await fetchAuthConfig(endpoint, http);
|
|
25680
27065
|
if (config2.mode === "local") {
|
|
25681
|
-
writeLine(io.stdout, `${endpoint}
|
|
27066
|
+
writeLine(io.stdout, `${endpoint} is in local mode: no login needed.`);
|
|
25682
27067
|
return 0;
|
|
25683
27068
|
}
|
|
25684
27069
|
let tokens;
|
|
@@ -25688,7 +27073,7 @@ async function runLogin(options, context, io) {
|
|
|
25688
27073
|
terminal.close();
|
|
25689
27074
|
}
|
|
25690
27075
|
if (tokens.refreshToken === void 0) {
|
|
25691
|
-
throw new RemoteError("
|
|
27076
|
+
throw new RemoteError("sign-in didn't return a refresh token");
|
|
25692
27077
|
}
|
|
25693
27078
|
const now = /* @__PURE__ */ new Date();
|
|
25694
27079
|
const me = await readProfile(
|
|
@@ -25700,7 +27085,9 @@ async function runLogin(options, context, io) {
|
|
|
25700
27085
|
config2.webUrl
|
|
25701
27086
|
);
|
|
25702
27087
|
if (me.sub === void 0 || me.email === void 0) {
|
|
25703
|
-
throw new RemoteError(
|
|
27088
|
+
throw new RemoteError(
|
|
27089
|
+
"unexpected response from the API: identity has no sub or email"
|
|
27090
|
+
);
|
|
25704
27091
|
}
|
|
25705
27092
|
saveCredential(io.env, {
|
|
25706
27093
|
endpoint,
|
|
@@ -25824,7 +27211,7 @@ function whoamiLines(endpoint, me, org) {
|
|
|
25824
27211
|
|
|
25825
27212
|
// src/commands/promote.ts
|
|
25826
27213
|
var PROMOTE_NEEDS_SERVER = "promote needs a server: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc";
|
|
25827
|
-
var PROMOTE_NEEDS_SERVER_LOCAL = "promote needs a server: drop --local
|
|
27214
|
+
var PROMOTE_NEEDS_SERVER_LOCAL = "promote needs a server: drop --local";
|
|
25828
27215
|
async function runPromote(tag, options, context, io) {
|
|
25829
27216
|
if (context.mode !== "remote") {
|
|
25830
27217
|
throw new ConfigError(
|
|
@@ -25855,10 +27242,12 @@ async function runPromote(tag, options, context, io) {
|
|
|
25855
27242
|
|
|
25856
27243
|
// src/commands/retag.ts
|
|
25857
27244
|
var NO_TAG_MESSAGE = "retag needs at least one --tag";
|
|
25858
|
-
async function runRetag(
|
|
27245
|
+
async function runRetag(value, options, context, io) {
|
|
25859
27246
|
const tags = options.tag ?? [];
|
|
25860
27247
|
if (tags.length === 0) throw new UsageError(NO_TAG_MESSAGE);
|
|
25861
|
-
const { store, client } = openBackend(context, io);
|
|
27248
|
+
const { store, client, local } = openBackend(context, io);
|
|
27249
|
+
const [id] = await resolveIds([value], { store, local, context });
|
|
27250
|
+
if (id === void 0) throw new NotFoundError(`no complaint with id ${value}`);
|
|
25862
27251
|
const allowedTags = client === void 0 ? await localAllowedTags(store, id, context) : void 0;
|
|
25863
27252
|
const complaint = await updateComplaintTags(
|
|
25864
27253
|
store,
|
|
@@ -25882,35 +27271,228 @@ async function localAllowedTags(store, id, context) {
|
|
|
25882
27271
|
return complaint.project === config2.project ? config2.tags : void 0;
|
|
25883
27272
|
}
|
|
25884
27273
|
|
|
27274
|
+
// src/commands/retry.ts
|
|
27275
|
+
async function runRetry(options, context, io) {
|
|
27276
|
+
requireServer("retry", context);
|
|
27277
|
+
const endpoint = requireEndpoint(context);
|
|
27278
|
+
const path = queuePath(io.env);
|
|
27279
|
+
const client = openClient(context, io, { queue: false });
|
|
27280
|
+
const result = await flushQueue({
|
|
27281
|
+
path,
|
|
27282
|
+
endpoint,
|
|
27283
|
+
auth: authLabel(context),
|
|
27284
|
+
client,
|
|
27285
|
+
stderr: io.stderr
|
|
27286
|
+
});
|
|
27287
|
+
if (result.stopped !== void 0) {
|
|
27288
|
+
throw new RemoteError(`${queuedCount(result.left)} still unsent: ${result.stopped}`);
|
|
27289
|
+
}
|
|
27290
|
+
const auth = authLabel(context);
|
|
27291
|
+
const left = pendingFor(path, endpoint).filter((entry) => entry.auth === auth).length;
|
|
27292
|
+
if (options.json === true) {
|
|
27293
|
+
writeJson(io.stdout, { sent: result.sent, dropped: result.dropped, left });
|
|
27294
|
+
return 0;
|
|
27295
|
+
}
|
|
27296
|
+
if (result.locked) {
|
|
27297
|
+
writeLine(io.stdout, "Another quibble process is sending the queue.");
|
|
27298
|
+
return 0;
|
|
27299
|
+
}
|
|
27300
|
+
if (result.sent.length === 0 && result.dropped === 0) {
|
|
27301
|
+
writeLine(io.stdout, "No queued complaints.");
|
|
27302
|
+
return 0;
|
|
27303
|
+
}
|
|
27304
|
+
const sent = result.sent.length;
|
|
27305
|
+
writeLine(
|
|
27306
|
+
io.stdout,
|
|
27307
|
+
`Sent ${sent} queued ${sent === 1 ? "complaint" : "complaints"}; ${left} left.`
|
|
27308
|
+
);
|
|
27309
|
+
return 0;
|
|
27310
|
+
}
|
|
27311
|
+
|
|
27312
|
+
// src/commands/show.ts
|
|
27313
|
+
var LABEL_WIDTH = "duplicate".length;
|
|
27314
|
+
function groupLine(count) {
|
|
27315
|
+
if (count >= MAX_LIMIT) return `${MAX_LIMIT}+ complaints share this headline`;
|
|
27316
|
+
return count === 1 ? "1 complaint shares this headline" : `${count} complaints share this headline`;
|
|
27317
|
+
}
|
|
27318
|
+
function groupCountOf(complaint, members2) {
|
|
27319
|
+
const others = members2.filter((member) => member.id !== complaint.id).length;
|
|
27320
|
+
return Math.min(others + 1, MAX_LIMIT);
|
|
27321
|
+
}
|
|
27322
|
+
function statusLine(complaint) {
|
|
27323
|
+
const reason = complaint.reason === void 0 ? "" : ` (${complaint.reason})`;
|
|
27324
|
+
const by = complaint.statusChangedByName ?? complaint.statusChangedBy;
|
|
27325
|
+
const when = by === void 0 || complaint.statusChangedAt === void 0 ? "" : ` by ${by} at ${complaint.statusChangedAt}`;
|
|
27326
|
+
return `${complaint.status}${reason}${when}`;
|
|
27327
|
+
}
|
|
27328
|
+
function showLines(complaint, groupCount, remote) {
|
|
27329
|
+
const reporter = complaint.reporterName === void 0 ? complaint.reporter : `${complaint.reporterName} (${complaint.reporter})`;
|
|
27330
|
+
const rows2 = [
|
|
27331
|
+
["id", complaint.id],
|
|
27332
|
+
["project", remote ? `${complaint.org}/${complaint.project}` : complaint.project],
|
|
27333
|
+
["status", statusLine(complaint)],
|
|
27334
|
+
["note", complaint.resolution],
|
|
27335
|
+
["duplicate", complaint.duplicateOf],
|
|
27336
|
+
["severity", complaint.severity],
|
|
27337
|
+
["tags", complaint.tags.length === 0 ? void 0 : complaint.tags.join(", ")],
|
|
27338
|
+
["agent", complaint.agent],
|
|
27339
|
+
["reporter", reporter],
|
|
27340
|
+
["created", complaint.createdAt],
|
|
27341
|
+
["updated", complaint.updatedAt],
|
|
27342
|
+
["cwd", complaint.cwd],
|
|
27343
|
+
["cmd", complaint.cmd],
|
|
27344
|
+
["group", groupLine(groupCount)],
|
|
27345
|
+
["recurred", complaint.recurred === true ? "yes" : void 0]
|
|
27346
|
+
];
|
|
27347
|
+
const lines = rows2.filter((row) => row[1] !== void 0).map(([label, value]) => `${label.padEnd(LABEL_WIDTH)} ${printable(value)}`);
|
|
27348
|
+
return [...lines, "", printableLines(complaint.message)];
|
|
27349
|
+
}
|
|
27350
|
+
async function runShow(value, options, context, io) {
|
|
27351
|
+
const { store, local } = openBackend(context, io);
|
|
27352
|
+
const [id] = await resolveIds([value], { store, local, context });
|
|
27353
|
+
const complaint = id === void 0 ? void 0 : await getComplaint(store, id);
|
|
27354
|
+
if (id === void 0 || complaint === void 0) {
|
|
27355
|
+
throw new NotFoundError(`no complaint with id ${id ?? value}`);
|
|
27356
|
+
}
|
|
27357
|
+
const group = await listComplaints(store, {
|
|
27358
|
+
org: complaint.org,
|
|
27359
|
+
project: complaint.project,
|
|
27360
|
+
fingerprint: complaint.fingerprint,
|
|
27361
|
+
limit: MAX_LIMIT
|
|
27362
|
+
});
|
|
27363
|
+
const groupCount = groupCountOf(complaint, group);
|
|
27364
|
+
if (options.json === true) {
|
|
27365
|
+
writeJson(io.stdout, { ...complaint, groupCount });
|
|
27366
|
+
return 0;
|
|
27367
|
+
}
|
|
27368
|
+
for (const line of showLines(complaint, groupCount, context.mode === "remote")) {
|
|
27369
|
+
writeLine(io.stdout, line);
|
|
27370
|
+
}
|
|
27371
|
+
return 0;
|
|
27372
|
+
}
|
|
27373
|
+
|
|
25885
27374
|
// src/commands/status.ts
|
|
27375
|
+
var REMOTE_ACTOR = "server";
|
|
25886
27376
|
var VERBS = {
|
|
25887
27377
|
open: "Reopened",
|
|
25888
27378
|
resolved: "Resolved",
|
|
25889
27379
|
dismissed: "Dismissed",
|
|
25890
27380
|
deleted: "Deleted"
|
|
25891
27381
|
};
|
|
25892
|
-
|
|
25893
|
-
|
|
25894
|
-
|
|
27382
|
+
var NOT_FOUND = "not_found";
|
|
27383
|
+
function noComplaint(id) {
|
|
27384
|
+
return `no complaint with id ${id}`;
|
|
27385
|
+
}
|
|
27386
|
+
async function readIdArguments(values, io) {
|
|
27387
|
+
if (!values.includes("-")) return [...values];
|
|
27388
|
+
if (values.length > 1) {
|
|
27389
|
+
throw new UsageError("- reads the ids from stdin, so it takes no other id");
|
|
27390
|
+
}
|
|
27391
|
+
if (io.stdin.isTTY === true) throw new UsageError("no ids: pipe them on stdin");
|
|
27392
|
+
const ids = (await readStdin(io.stdin)).split(/\s+/).filter((id) => id !== "");
|
|
27393
|
+
if (ids.length === 0) throw new UsageError("no ids on stdin");
|
|
27394
|
+
return ids;
|
|
27395
|
+
}
|
|
27396
|
+
async function runStatusChange(status, values, options, context, io) {
|
|
27397
|
+
const reason = options.reason ?? (options.duplicateOf === void 0 ? void 0 : DUPLICATE_REASON);
|
|
27398
|
+
const draft = {
|
|
25895
27399
|
status,
|
|
25896
|
-
...options.note === void 0 ? {} : { resolution: options.note }
|
|
27400
|
+
...options.note === void 0 ? {} : { resolution: options.note },
|
|
27401
|
+
...reason === void 0 ? {} : { reason },
|
|
27402
|
+
...options.duplicateOf === void 0 ? {} : { duplicateOf: options.duplicateOf }
|
|
27403
|
+
};
|
|
27404
|
+
parseStatusChange(draft);
|
|
27405
|
+
const given = await readIdArguments(values, io);
|
|
27406
|
+
const backend = openBackend(context, io);
|
|
27407
|
+
const extra = options.duplicateOf === void 0 ? [] : [options.duplicateOf];
|
|
27408
|
+
const resolved = await resolveIds([...given, ...extra], {
|
|
27409
|
+
store: backend.store,
|
|
27410
|
+
local: backend.local,
|
|
27411
|
+
context
|
|
25897
27412
|
});
|
|
25898
|
-
|
|
25899
|
-
|
|
25900
|
-
|
|
27413
|
+
const ids = [...new Set(resolved.slice(0, given.length))];
|
|
27414
|
+
const duplicateOf = options.duplicateOf === void 0 ? void 0 : resolved.at(-1);
|
|
27415
|
+
const change = parseStatusChange({
|
|
27416
|
+
...draft,
|
|
27417
|
+
...duplicateOf === void 0 ? {} : { duplicateOf }
|
|
27418
|
+
});
|
|
27419
|
+
const actor = { by: context.mode === "remote" ? REMOTE_ACTOR : context.reporter };
|
|
27420
|
+
if (ids.length === 1) {
|
|
27421
|
+
const id = ids[0];
|
|
27422
|
+
const complaint = await updateComplaintStatus(backend.store, id, change, { actor });
|
|
27423
|
+
if (complaint === void 0) throw new NotFoundError(noComplaint(id));
|
|
27424
|
+
if (options.json === true) {
|
|
27425
|
+
writeJson(io.stdout, complaint);
|
|
27426
|
+
return 0;
|
|
27427
|
+
}
|
|
27428
|
+
writeLine(io.stdout, successLine(status, complaint));
|
|
25901
27429
|
return 0;
|
|
25902
27430
|
}
|
|
25903
|
-
|
|
25904
|
-
|
|
25905
|
-
|
|
25906
|
-
|
|
25907
|
-
|
|
27431
|
+
const { results, error: error62 } = backend.store instanceof RemoteStore ? await remoteResults(backend.store, ids, change) : {
|
|
27432
|
+
results: await localResults(
|
|
27433
|
+
ids,
|
|
27434
|
+
async (id) => updateComplaintStatus(backend.store, id, change, { actor })
|
|
27435
|
+
),
|
|
27436
|
+
error: void 0
|
|
27437
|
+
};
|
|
27438
|
+
const failures = [];
|
|
27439
|
+
for (const result of results) {
|
|
27440
|
+
if (result.ok && result.complaint !== void 0) {
|
|
27441
|
+
if (options.json !== true)
|
|
27442
|
+
writeLine(io.stdout, successLine(status, result.complaint));
|
|
27443
|
+
continue;
|
|
27444
|
+
}
|
|
27445
|
+
const code = result.error?.code ?? NOT_FOUND;
|
|
27446
|
+
const message = code === NOT_FOUND ? noComplaint(result.id) : result.error?.message;
|
|
27447
|
+
writeLine(io.stderr, `quibble: ${result.id}: ${message ?? "the change failed"}`);
|
|
27448
|
+
failures.push({ remote: backend.store instanceof RemoteStore && code !== NOT_FOUND });
|
|
27449
|
+
}
|
|
27450
|
+
if (options.json === true) writeJson(io.stdout, results);
|
|
27451
|
+
if (error62 !== void 0) throw error62;
|
|
27452
|
+
if (failures.length === 0) return 0;
|
|
27453
|
+
return failures.some((failure2) => failure2.remote) ? 2 : 1;
|
|
27454
|
+
}
|
|
27455
|
+
function successLine(status, complaint) {
|
|
27456
|
+
return `${VERBS[status]} complaint ${complaint.id} for ${complaint.project}`;
|
|
27457
|
+
}
|
|
27458
|
+
async function localResults(ids, apply) {
|
|
27459
|
+
const results = [];
|
|
27460
|
+
for (const id of ids) {
|
|
27461
|
+
try {
|
|
27462
|
+
const complaint = await apply(id);
|
|
27463
|
+
results.push(
|
|
27464
|
+
complaint === void 0 ? { id, ok: false, error: { code: NOT_FOUND, message: noComplaint(id) } } : { id, ok: true, complaint }
|
|
27465
|
+
);
|
|
27466
|
+
} catch (error62) {
|
|
27467
|
+
if (!(error62 instanceof ValidationError)) throw error62;
|
|
27468
|
+
results.push({
|
|
27469
|
+
id,
|
|
27470
|
+
ok: false,
|
|
27471
|
+
error: { code: error62.code, message: errorMessage(error62) }
|
|
27472
|
+
});
|
|
27473
|
+
}
|
|
27474
|
+
}
|
|
27475
|
+
return results;
|
|
27476
|
+
}
|
|
27477
|
+
async function remoteResults(store, ids, change) {
|
|
27478
|
+
const results = [];
|
|
27479
|
+
for (let start = 0; start < ids.length; start += BULK_MAX) {
|
|
27480
|
+
try {
|
|
27481
|
+
results.push(
|
|
27482
|
+
...await store.bulkStatus(ids.slice(start, start + BULK_MAX), change)
|
|
27483
|
+
);
|
|
27484
|
+
} catch (error62) {
|
|
27485
|
+
return { results, error: error62 };
|
|
27486
|
+
}
|
|
27487
|
+
}
|
|
27488
|
+
return { results, error: void 0 };
|
|
25908
27489
|
}
|
|
25909
27490
|
|
|
25910
27491
|
// src/commands/summary.ts
|
|
25911
27492
|
var FORMATS = ["text", "markdown"];
|
|
25912
27493
|
var SEVERITY_ORDER = [...SEVERITIES].reverse();
|
|
25913
27494
|
var TRUNCATED_MESSAGE = `Counting the newest ${MAX_LIMIT} complaints only.`;
|
|
27495
|
+
var BUDGET_MESSAGE = "The server stopped searching early, so the counts are a floor: narrow the filters or pass --since.";
|
|
25914
27496
|
var EMPTY_MESSAGE = "No complaints found.";
|
|
25915
27497
|
async function runSummary(options, context, io) {
|
|
25916
27498
|
if (options.format !== void 0 && options.json === true) {
|
|
@@ -25921,17 +27503,23 @@ async function runSummary(options, context, io) {
|
|
|
25921
27503
|
const reporter = options.mine === true ? await resolveMine(context, client, io.env) : void 0;
|
|
25922
27504
|
const filter = parseListFilter({
|
|
25923
27505
|
...resolveScope(options, context),
|
|
27506
|
+
...options.severity === void 0 ? {} : { severity: options.severity },
|
|
27507
|
+
...tagFilter(options.tag),
|
|
25924
27508
|
...reporter === void 0 ? {} : { reporter },
|
|
25925
27509
|
limit: MAX_LIMIT
|
|
25926
27510
|
});
|
|
25927
|
-
const
|
|
25928
|
-
const
|
|
27511
|
+
const page = await listComplaintPage(store, filter);
|
|
27512
|
+
const complaints = page.complaints;
|
|
27513
|
+
const capped = complaints.length === MAX_LIMIT;
|
|
27514
|
+
const truncated = capped || page.truncated;
|
|
25929
27515
|
const summary = summarizeComplaints(complaints);
|
|
25930
27516
|
if (options.json === true) {
|
|
25931
27517
|
writeJson(io.stdout, {
|
|
25932
27518
|
scope: {
|
|
25933
27519
|
project: filter.project ?? null,
|
|
25934
27520
|
status: filter.status ?? null,
|
|
27521
|
+
severity: filter.severity ?? null,
|
|
27522
|
+
tags: filter.tags ?? null,
|
|
25935
27523
|
since: filter.since ?? null
|
|
25936
27524
|
},
|
|
25937
27525
|
truncated,
|
|
@@ -25943,7 +27531,8 @@ async function runSummary(options, context, io) {
|
|
|
25943
27531
|
writeLine(io.stdout, EMPTY_MESSAGE);
|
|
25944
27532
|
return 0;
|
|
25945
27533
|
}
|
|
25946
|
-
const
|
|
27534
|
+
const note = capped ? TRUNCATED_MESSAGE : page.truncated ? BUDGET_MESSAGE : void 0;
|
|
27535
|
+
const lines = format === "markdown" ? markdownLines(filter, summary, note) : textLines(filter, summary, note);
|
|
25947
27536
|
for (const line of lines) writeLine(io.stdout, line);
|
|
25948
27537
|
return 0;
|
|
25949
27538
|
}
|
|
@@ -25958,7 +27547,9 @@ function scopeLabel(filter) {
|
|
|
25958
27547
|
const name = filter.project ?? "all projects";
|
|
25959
27548
|
const since = filter.since === void 0 ? "" : ` since ${filter.since}`;
|
|
25960
27549
|
const status = filter.status === void 0 ? "" : ` with status ${filter.status}`;
|
|
25961
|
-
|
|
27550
|
+
const severity = filter.severity === void 0 ? "" : ` with severity ${filter.severity}`;
|
|
27551
|
+
const tags = filter.tags === void 0 ? "" : ` tagged ${filter.tags.map((tag) => tag === UNTAGGED_TAG ? "untagged" : oneLine(tag)).join(" or ")}`;
|
|
27552
|
+
return `Complaints for ${name}${since}${status}${severity}${tags}`;
|
|
25962
27553
|
}
|
|
25963
27554
|
function blocksOf(filter, summary) {
|
|
25964
27555
|
const severity = SEVERITY_ORDER.filter((name) => summary.bySeverity[name] > 0).map(
|
|
@@ -25982,13 +27573,11 @@ function statusCounts(summary) {
|
|
|
25982
27573
|
const { open: open2, resolved, dismissed, deleted } = summary.byStatus;
|
|
25983
27574
|
return `open ${open2}, resolved ${resolved}, dismissed ${dismissed}, deleted ${deleted}`;
|
|
25984
27575
|
}
|
|
25985
|
-
|
|
25986
|
-
return name.replace(/\s+/g, " ");
|
|
25987
|
-
}
|
|
27576
|
+
var oneLine = printable;
|
|
25988
27577
|
function cell(name) {
|
|
25989
27578
|
return oneLine(name).replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
25990
27579
|
}
|
|
25991
|
-
function textLines(filter, summary,
|
|
27580
|
+
function textLines(filter, summary, note) {
|
|
25992
27581
|
const lines = [`${scopeLabel(filter)}: ${summary.total} (${statusCounts(summary)})`];
|
|
25993
27582
|
for (const block2 of blocksOf(filter, summary)) {
|
|
25994
27583
|
const names = block2.rows.map((row) => oneLine(row.name));
|
|
@@ -25998,10 +27587,10 @@ function textLines(filter, summary, truncated) {
|
|
|
25998
27587
|
lines.push(` ${(names[index] ?? "").padEnd(width)} ${row.count}`);
|
|
25999
27588
|
}
|
|
26000
27589
|
}
|
|
26001
|
-
if (
|
|
27590
|
+
if (note !== void 0) lines.push("", note);
|
|
26002
27591
|
return lines;
|
|
26003
27592
|
}
|
|
26004
|
-
function markdownLines(filter, summary,
|
|
27593
|
+
function markdownLines(filter, summary, note) {
|
|
26005
27594
|
const { open: open2, resolved, dismissed, deleted } = summary.byStatus;
|
|
26006
27595
|
const noun = summary.total === 1 ? "complaint" : "complaints";
|
|
26007
27596
|
const counts = `${open2} open, ${resolved} resolved, ${dismissed} dismissed, ${deleted} deleted`;
|
|
@@ -26016,7 +27605,7 @@ function markdownLines(filter, summary, truncated) {
|
|
|
26016
27605
|
);
|
|
26017
27606
|
for (const row of block2.rows) lines.push(`| ${cell(row.name)} | ${row.count} |`);
|
|
26018
27607
|
}
|
|
26019
|
-
if (
|
|
27608
|
+
if (note !== void 0) lines.push("", note);
|
|
26020
27609
|
return lines;
|
|
26021
27610
|
}
|
|
26022
27611
|
|
|
@@ -26029,12 +27618,146 @@ async function runTags(options, context, io) {
|
|
|
26029
27618
|
writeJson(io.stdout, { project, tags: tags ?? null });
|
|
26030
27619
|
return 0;
|
|
26031
27620
|
}
|
|
26032
|
-
|
|
27621
|
+
printList(project, tags, io);
|
|
27622
|
+
return 0;
|
|
27623
|
+
}
|
|
27624
|
+
function printList(project, tags, io) {
|
|
27625
|
+
if (tags === void 0 || tags.length === 0) {
|
|
26033
27626
|
writeLine(io.stdout, `No tags configured for ${project}: any tag is allowed.`);
|
|
26034
|
-
return
|
|
27627
|
+
return;
|
|
26035
27628
|
}
|
|
26036
27629
|
for (const tag of tags) writeLine(io.stdout, tag);
|
|
26037
27630
|
writeLine(io.stdout, OTHER_TAG_HINT);
|
|
27631
|
+
}
|
|
27632
|
+
async function runTagsEdit(edit, given, options, context, io) {
|
|
27633
|
+
requireServer("tags", context);
|
|
27634
|
+
const project = requireProject(context);
|
|
27635
|
+
if (given.length === 0) throw new UsageError(`tags ${edit} needs at least one tag`);
|
|
27636
|
+
const named = parseAllowedTags(given);
|
|
27637
|
+
const client = openOrgClient(context, io);
|
|
27638
|
+
let next;
|
|
27639
|
+
if (edit === "set") {
|
|
27640
|
+
next = named;
|
|
27641
|
+
} else {
|
|
27642
|
+
const current = (await client.project(project)).tags ?? [];
|
|
27643
|
+
const combined = edit === "add" ? [...current, ...named.filter((tag) => !current.includes(tag))] : current.filter((tag) => !named.includes(tag));
|
|
27644
|
+
next = combined.length === 0 ? null : parseAllowedTags(combined);
|
|
27645
|
+
}
|
|
27646
|
+
const answer2 = await client.setProjectTags(project, next);
|
|
27647
|
+
if (options.json === true) {
|
|
27648
|
+
writeJson(io.stdout, answer2.body);
|
|
27649
|
+
return 0;
|
|
27650
|
+
}
|
|
27651
|
+
const noun = answer2.retagged === 1 ? "complaint" : "complaints";
|
|
27652
|
+
writeLine(
|
|
27653
|
+
io.stdout,
|
|
27654
|
+
`Updated the tags of ${project}: ${answer2.retagged} ${noun} retagged`
|
|
27655
|
+
);
|
|
27656
|
+
printList(project, answer2.tags, io);
|
|
27657
|
+
return 0;
|
|
27658
|
+
}
|
|
27659
|
+
|
|
27660
|
+
// src/commands/token.ts
|
|
27661
|
+
var COLUMNS3 = ["ID", "NAME", "CREATED", "LAST USED", "EXPIRES", "STATE"];
|
|
27662
|
+
function minute(stamp2) {
|
|
27663
|
+
return stamp2.slice(0, 16).replace("T", " ");
|
|
27664
|
+
}
|
|
27665
|
+
function stateOf(token, now) {
|
|
27666
|
+
if (token.revokedAt !== void 0) return "revoked";
|
|
27667
|
+
if (token.expiresAt !== void 0 && Date.parse(token.expiresAt) <= now.getTime()) {
|
|
27668
|
+
return "expired";
|
|
27669
|
+
}
|
|
27670
|
+
return "active";
|
|
27671
|
+
}
|
|
27672
|
+
function formatTokens(tokens, now) {
|
|
27673
|
+
const rows2 = tokens.map((token) => [
|
|
27674
|
+
token.id.slice(0, 8),
|
|
27675
|
+
token.name,
|
|
27676
|
+
minute(token.createdAt),
|
|
27677
|
+
token.lastUsedAt === void 0 ? "\u2014" : minute(token.lastUsedAt),
|
|
27678
|
+
token.expiresAt === void 0 ? "never" : minute(token.expiresAt),
|
|
27679
|
+
stateOf(token, now)
|
|
27680
|
+
]);
|
|
27681
|
+
const widths = COLUMNS3.map(
|
|
27682
|
+
(name, column) => rows2.reduce((width, row) => Math.max(width, (row[column] ?? "").length), name.length)
|
|
27683
|
+
);
|
|
27684
|
+
const format = (cells) => cells.map((cell2, column) => cell2.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
|
|
27685
|
+
return [format(COLUMNS3), ...rows2.map(format)];
|
|
27686
|
+
}
|
|
27687
|
+
function scopeOf(context) {
|
|
27688
|
+
requireServer("token", context);
|
|
27689
|
+
return { project: requireProject(context), org: requireOrg(context) };
|
|
27690
|
+
}
|
|
27691
|
+
async function runTokenCreate(name, options, context, io, now = /* @__PURE__ */ new Date()) {
|
|
27692
|
+
const { project, org } = scopeOf(context);
|
|
27693
|
+
const expiresAt = options.expires === void 0 ? void 0 : parseExpires(options.expires, now);
|
|
27694
|
+
const body = parseNewToken(
|
|
27695
|
+
{ name, ...expiresAt === void 0 ? {} : { expiresAt } },
|
|
27696
|
+
now
|
|
27697
|
+
);
|
|
27698
|
+
const client = openOrgClient(context, io);
|
|
27699
|
+
const minted = await client.createToken(project, body);
|
|
27700
|
+
if (options.json === true) {
|
|
27701
|
+
writeJson(io.stdout, minted.body);
|
|
27702
|
+
return 0;
|
|
27703
|
+
}
|
|
27704
|
+
writeLine(
|
|
27705
|
+
io.stdout,
|
|
27706
|
+
`Created token ${minted.token.name} for ${org}/${project}. It is shown once:`
|
|
27707
|
+
);
|
|
27708
|
+
writeLine(io.stdout, `QUIBBLE_ENDPOINT=${trimSlash(client.endpoint)}`);
|
|
27709
|
+
writeLine(io.stdout, `QUIBBLE_TOKEN=${minted.plaintext}`);
|
|
27710
|
+
return 0;
|
|
27711
|
+
}
|
|
27712
|
+
async function runTokenList(options, context, io, now = /* @__PURE__ */ new Date()) {
|
|
27713
|
+
const { project } = scopeOf(context);
|
|
27714
|
+
const answer2 = await openOrgClient(context, io).tokens(project);
|
|
27715
|
+
if (options.json === true) {
|
|
27716
|
+
writeJson(io.stdout, answer2.body);
|
|
27717
|
+
return 0;
|
|
27718
|
+
}
|
|
27719
|
+
if (answer2.tokens.length === 0) {
|
|
27720
|
+
writeLine(io.stdout, `No tokens for ${project}.`);
|
|
27721
|
+
return 0;
|
|
27722
|
+
}
|
|
27723
|
+
for (const line of formatTokens(answer2.tokens, now)) writeLine(io.stdout, line);
|
|
27724
|
+
return 0;
|
|
27725
|
+
}
|
|
27726
|
+
function findToken(tokens, value) {
|
|
27727
|
+
const named = tokens.find((token) => token.name === value.trim());
|
|
27728
|
+
if (named !== void 0) return named;
|
|
27729
|
+
let read;
|
|
27730
|
+
try {
|
|
27731
|
+
read = readIdValue(value);
|
|
27732
|
+
} catch (error62) {
|
|
27733
|
+
if (error62 instanceof UsageError)
|
|
27734
|
+
throw new NotFoundError(`no token is named ${value}`);
|
|
27735
|
+
throw error62;
|
|
27736
|
+
}
|
|
27737
|
+
const { id, prefix } = read;
|
|
27738
|
+
const matches = tokens.filter(
|
|
27739
|
+
(token) => prefix ? token.id.startsWith(id) : token.id === id
|
|
27740
|
+
);
|
|
27741
|
+
if (matches.length === 1) return matches[0];
|
|
27742
|
+
if (matches.length > 1) {
|
|
27743
|
+
throw ambiguousError(
|
|
27744
|
+
id,
|
|
27745
|
+
matches.map((token) => token.id),
|
|
27746
|
+
"tokens"
|
|
27747
|
+
);
|
|
27748
|
+
}
|
|
27749
|
+
throw new NotFoundError(`no token is named or starts with ${id}`);
|
|
27750
|
+
}
|
|
27751
|
+
async function runTokenRevoke(value, options, context, io) {
|
|
27752
|
+
const { project, org } = scopeOf(context);
|
|
27753
|
+
const client = openOrgClient(context, io);
|
|
27754
|
+
const token = findToken((await client.tokens(project)).tokens, value);
|
|
27755
|
+
await client.revokeToken(project, token.id);
|
|
27756
|
+
if (options.json === true) {
|
|
27757
|
+
writeJson(io.stdout, { id: token.id, name: token.name, revoked: true });
|
|
27758
|
+
return 0;
|
|
27759
|
+
}
|
|
27760
|
+
writeLine(io.stdout, `Revoked token ${token.name} of ${org}/${project}.`);
|
|
26038
27761
|
return 0;
|
|
26039
27762
|
}
|
|
26040
27763
|
|
|
@@ -26049,11 +27772,18 @@ var NON_EMPTY_OPTIONS = [
|
|
|
26049
27772
|
"note"
|
|
26050
27773
|
];
|
|
26051
27774
|
var STATUS_COMMANDS = [
|
|
26052
|
-
["resolve", "resolved", "mark
|
|
26053
|
-
["dismiss", "dismissed", "dismiss
|
|
26054
|
-
["delete", "deleted", "hide
|
|
26055
|
-
["reopen", "open", "reopen
|
|
27775
|
+
["resolve", "resolved", "mark complaints resolved"],
|
|
27776
|
+
["dismiss", "dismissed", "dismiss duplicates, won't-fixes or noise"],
|
|
27777
|
+
["delete", "deleted", "hide complaints from the default views"],
|
|
27778
|
+
["reopen", "open", "reopen complaints and clear their notes"]
|
|
27779
|
+
];
|
|
27780
|
+
var TAGS_EDITS = [
|
|
27781
|
+
["set", "replace the project's allowed tag list"],
|
|
27782
|
+
["add", "add tags to the allowed tag list"],
|
|
27783
|
+
["remove", "remove tags from the allowed tag list"]
|
|
26056
27784
|
];
|
|
27785
|
+
var ID_HELP = `complaint id, or a unique prefix of at least ${SHORT_ID_MIN} characters`;
|
|
27786
|
+
var REASON_HELP = `${REASONS.slice(0, -1).join(", ")} or ${REASONS.at(-1) ?? ""}`;
|
|
26057
27787
|
function packageVersion() {
|
|
26058
27788
|
const require2 = createRequire(import.meta.url);
|
|
26059
27789
|
const manifest = require2("../package.json");
|
|
@@ -26068,7 +27798,7 @@ function rejectEmptyOptions(options) {
|
|
|
26068
27798
|
}
|
|
26069
27799
|
}
|
|
26070
27800
|
function withGlobalOptions(command) {
|
|
26071
|
-
return command.option("--local", "use the local store
|
|
27801
|
+
return command.option("--local", "use the local store even if an endpoint is set").option("--endpoint <url>", "base URL of the Quibble API").option("--org <slug>", "organization to use in remote mode").option("--json", "print JSON instead of text");
|
|
26072
27802
|
}
|
|
26073
27803
|
function globals(program2, command) {
|
|
26074
27804
|
const outer = program2.opts();
|
|
@@ -26106,88 +27836,151 @@ function buildProgram(io, setCode) {
|
|
|
26106
27836
|
}).exitOverride();
|
|
26107
27837
|
withGlobalOptions(program2);
|
|
26108
27838
|
withGlobalOptions(
|
|
26109
|
-
program2.command("init").description("write a .quibblerc in the current directory").option("--project <name>", "project name
|
|
26110
|
-
).action(function() {
|
|
27839
|
+
program2.command("init").description("write a .quibblerc in the current directory").option("--project <name>", "project name (default: the directory name)").option("-t, --tag <tag>", "allowed tag (repeatable)", collect).option("--agent <name>", "agent named in the snippet (default: claude)").option("--skill", "also write .claude/skills/quibble/SKILL.md").option("--force", "overwrite an existing .quibblerc or skill file")
|
|
27840
|
+
).action(async function() {
|
|
27841
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
27842
|
+
rejectEmptyOptions(options);
|
|
27843
|
+
setCode(await runInit(options, io));
|
|
27844
|
+
});
|
|
27845
|
+
withGlobalOptions(
|
|
27846
|
+
program2.command("snippet").description("print or write the agent snippet of this project").option("--append <file>", "append the snippet to a file such as CLAUDE.md").option("--skill", "write .claude/skills/quibble/SKILL.md").option("--agent <name>", "agent named in the snippet").option("--project <name>", "project name (overrides .quibblerc)").option("--force", "overwrite an existing skill file")
|
|
27847
|
+
).action(async function() {
|
|
26111
27848
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26112
27849
|
rejectEmptyOptions(options);
|
|
26113
|
-
setCode(
|
|
27850
|
+
setCode(await runSnippet(options, contextFor(options, io), io));
|
|
26114
27851
|
});
|
|
26115
27852
|
withGlobalOptions(
|
|
26116
|
-
program2.command("complain").description("file a complaint
|
|
27853
|
+
program2.command("complain").description("file a complaint").argument("[message]", "what went wrong; omit it or pass `-` to read stdin").option("-s, --severity <level>", "low, medium or high").option("-t, --tag <tag>", "tag (repeatable)", collect).option("--agent <name>", "name of the agent filing it").option("--reporter <name>", "person you are filing for (local mode only)").option("--project <name>", "project name (overrides .quibblerc)").option("--cmd <command>", "the command that failed")
|
|
26117
27854
|
).action(async function(message) {
|
|
26118
27855
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26119
27856
|
rejectEmptyOptions(options);
|
|
26120
27857
|
setCode(await runComplain(message, options, contextFor(options, io), io));
|
|
26121
27858
|
});
|
|
26122
27859
|
withGlobalOptions(
|
|
26123
|
-
program2.command("list").description("list complaints, newest first").option("--project <name>", "list one project").option("--all", "list
|
|
27860
|
+
program2.command("list").description("list complaints, newest first").option("--project <name>", "list one project").option("--all", "list all projects").option("-s, --severity <level>", "low, medium or high").option("--status <status>", "open, resolved, dismissed or deleted").option("--open", "same as --status open").option(
|
|
27861
|
+
"-t, --tag <tag>",
|
|
27862
|
+
"complaints with any of these tags (repeatable, `-` for untagged)",
|
|
27863
|
+
collect
|
|
27864
|
+
).option("--mine", "only your own complaints").option("--since <when>", "a duration like 7d, a date or a UTC timestamp").option("--search <text>", "complaints whose message contains the text").option("--limit <n>", "maximum number of complaints").option("--ungroup", "one row per complaint instead of one per group").option("--ids", "print the full id of every complaint, one per line")
|
|
26124
27865
|
).action(async function() {
|
|
26125
27866
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26126
27867
|
rejectEmptyOptions(options);
|
|
26127
27868
|
setCode(await runList(options, contextFor(options, io), io));
|
|
26128
27869
|
});
|
|
27870
|
+
withGlobalOptions(
|
|
27871
|
+
program2.command("show").description("print one whole complaint").argument("<id>", ID_HELP)
|
|
27872
|
+
).action(async function(id) {
|
|
27873
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
27874
|
+
rejectEmptyOptions(options);
|
|
27875
|
+
setCode(await runShow(id, options, contextFor(options, io), io));
|
|
27876
|
+
});
|
|
26129
27877
|
for (const [name, status, description] of STATUS_COMMANDS) {
|
|
26130
|
-
const command = program2.command(name).description(description).argument("<id
|
|
27878
|
+
const command = program2.command(name).description(description).argument("<id...>", `${ID_HELP}; \`-\` reads the ids from stdin`);
|
|
26131
27879
|
if (status !== "open") {
|
|
26132
|
-
command.option("--note <text>", "note
|
|
27880
|
+
command.option("--note <text>", "note explaining the change");
|
|
26133
27881
|
}
|
|
26134
|
-
|
|
27882
|
+
if (status === "resolved" || status === "dismissed") {
|
|
27883
|
+
command.option("--reason <reason>", REASON_HELP).option("--duplicate-of <id>", "the complaint these repeat (implies duplicate)");
|
|
27884
|
+
}
|
|
27885
|
+
withGlobalOptions(command).action(async function(ids) {
|
|
26135
27886
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26136
27887
|
rejectEmptyOptions(options);
|
|
26137
|
-
setCode(await runStatusChange(status,
|
|
27888
|
+
setCode(await runStatusChange(status, ids, options, contextFor(options, io), io));
|
|
26138
27889
|
});
|
|
26139
27890
|
}
|
|
26140
27891
|
withGlobalOptions(
|
|
26141
|
-
program2.command("retag").description("replace
|
|
27892
|
+
program2.command("retag").description("replace a complaint's tags").argument("<id>", ID_HELP).option("-t, --tag <tag>", "tag (repeatable, at least one)", collect)
|
|
26142
27893
|
).action(async function(id) {
|
|
26143
27894
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26144
27895
|
rejectEmptyOptions(options);
|
|
26145
27896
|
setCode(await runRetag(id, options, contextFor(options, io), io));
|
|
26146
27897
|
});
|
|
26147
27898
|
withGlobalOptions(
|
|
26148
|
-
program2.command("summary").description("count
|
|
27899
|
+
program2.command("summary").description("count complaints by status, severity, project and tag").option("--project <name>", "summarize one project").option("--all", "summarize all projects").option("--status <status>", "open, resolved, dismissed or deleted").option("--open", "same as --status open").option("-s, --severity <level>", "low, medium or high").option(
|
|
27900
|
+
"-t, --tag <tag>",
|
|
27901
|
+
"complaints with any of these tags (repeatable, `-` for untagged)",
|
|
27902
|
+
collect
|
|
27903
|
+
).option("--mine", "only your own complaints").option("--since <when>", "a duration like 7d, a date or a UTC timestamp").option("--format <format>", "text or markdown")
|
|
26149
27904
|
).action(async function() {
|
|
26150
27905
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26151
27906
|
rejectEmptyOptions(options);
|
|
26152
27907
|
setCode(await runSummary(options, contextFor(options, io), io));
|
|
26153
27908
|
});
|
|
26154
|
-
withGlobalOptions(
|
|
26155
|
-
program2.command("tags").description("print the allowed tags
|
|
27909
|
+
const tags = withGlobalOptions(
|
|
27910
|
+
program2.command("tags").description("print or edit the project's allowed tags").option("--project <name>", "project name (overrides .quibblerc)")
|
|
26156
27911
|
).action(async function() {
|
|
26157
27912
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26158
27913
|
rejectEmptyOptions(options);
|
|
26159
27914
|
setCode(await runTags(options, contextFor(options, io), io));
|
|
26160
27915
|
});
|
|
27916
|
+
for (const [edit, description] of TAGS_EDITS) {
|
|
27917
|
+
withGlobalOptions(
|
|
27918
|
+
tags.command(edit).description(`${description} (remote only)`).argument("[tag...]", "tags").option("--project <name>", "project name (overrides .quibblerc)")
|
|
27919
|
+
).action(async function(given) {
|
|
27920
|
+
const options = { ...tags.opts(), ...this.opts(), ...globals(program2, this) };
|
|
27921
|
+
rejectEmptyOptions(options);
|
|
27922
|
+
setCode(await runTagsEdit(edit, given, options, contextFor(options, io), io));
|
|
27923
|
+
});
|
|
27924
|
+
}
|
|
27925
|
+
const token = program2.command("token").description("manage the project's tokens (remote only)");
|
|
27926
|
+
withGlobalOptions(
|
|
27927
|
+
token.command("create").description("mint a project token and print it once").argument("<name>", "token name, unique in the project").option("--expires <when>", "a duration like 30d, a date or a UTC timestamp").option("--project <name>", "project name (overrides .quibblerc)")
|
|
27928
|
+
).action(async function(name) {
|
|
27929
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
27930
|
+
rejectEmptyOptions(options);
|
|
27931
|
+
setCode(await runTokenCreate(name, options, contextFor(options, io), io));
|
|
27932
|
+
});
|
|
27933
|
+
withGlobalOptions(
|
|
27934
|
+
token.command("list").description("list the project's tokens").option("--project <name>", "project name (overrides .quibblerc)")
|
|
27935
|
+
).action(async function() {
|
|
27936
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
27937
|
+
rejectEmptyOptions(options);
|
|
27938
|
+
setCode(await runTokenList(options, contextFor(options, io), io));
|
|
27939
|
+
});
|
|
27940
|
+
withGlobalOptions(
|
|
27941
|
+
token.command("revoke").description("revoke a token").argument("<token>", "token name, or an id prefix of at least 6 characters").option("--project <name>", "project name (overrides .quibblerc)")
|
|
27942
|
+
).action(async function(value) {
|
|
27943
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
27944
|
+
rejectEmptyOptions(options);
|
|
27945
|
+
setCode(await runTokenRevoke(value, options, contextFor(options, io), io));
|
|
27946
|
+
});
|
|
27947
|
+
withGlobalOptions(
|
|
27948
|
+
program2.command("retry").description("send the complaints queued while offline")
|
|
27949
|
+
).action(async function() {
|
|
27950
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
27951
|
+
rejectEmptyOptions(options);
|
|
27952
|
+
setCode(await runRetry(options, contextFor(options, io), io));
|
|
27953
|
+
});
|
|
26161
27954
|
withGlobalOptions(
|
|
26162
|
-
program2.command("promote").description("
|
|
27955
|
+
program2.command("promote").description("turn an `other:` suggestion into an allowed tag and retag complaints").argument("<tag>", "tag to promote, without `other:`").option("--project <name>", "project name (overrides .quibblerc)")
|
|
26163
27956
|
).action(async function(tag) {
|
|
26164
27957
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26165
27958
|
rejectEmptyOptions(options);
|
|
26166
27959
|
setCode(await runPromote(tag, options, contextFor(options, io), io));
|
|
26167
27960
|
});
|
|
26168
27961
|
withGlobalOptions(
|
|
26169
|
-
program2.command("orgs").description("list
|
|
27962
|
+
program2.command("orgs").description("list your organizations")
|
|
26170
27963
|
).action(async function() {
|
|
26171
27964
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26172
27965
|
rejectEmptyOptions(options);
|
|
26173
27966
|
setCode(await runOrgs(options, contextFor(options, io), io));
|
|
26174
27967
|
});
|
|
26175
27968
|
withGlobalOptions(
|
|
26176
|
-
program2.command("login").description("
|
|
27969
|
+
program2.command("login").description("log in to the endpoint")
|
|
26177
27970
|
).action(async function() {
|
|
26178
27971
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26179
27972
|
rejectEmptyOptions(options);
|
|
26180
27973
|
setCode(await runLogin(options, contextFor(options, io), io));
|
|
26181
27974
|
});
|
|
26182
27975
|
withGlobalOptions(
|
|
26183
|
-
program2.command("logout").description("
|
|
27976
|
+
program2.command("logout").description("log out of the endpoint")
|
|
26184
27977
|
).action(async function() {
|
|
26185
27978
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26186
27979
|
rejectEmptyOptions(options);
|
|
26187
27980
|
setCode(await runLogout(contextFor(options, io), io));
|
|
26188
27981
|
});
|
|
26189
27982
|
withGlobalOptions(
|
|
26190
|
-
program2.command("whoami").description("
|
|
27983
|
+
program2.command("whoami").description("show the current identity")
|
|
26191
27984
|
).action(async function() {
|
|
26192
27985
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26193
27986
|
rejectEmptyOptions(options);
|