letthemquibble 1.0.1 → 1.2.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 +81 -15
- package/dist/quibble.js +2976 -653
- package/package.json +3 -1
package/dist/quibble.js
CHANGED
|
@@ -51,11 +51,11 @@ var require_error = __commonJS({
|
|
|
51
51
|
* @param {string} code an id string representing the error
|
|
52
52
|
* @param {string} message human-readable description of the error
|
|
53
53
|
*/
|
|
54
|
-
constructor(exitCode,
|
|
54
|
+
constructor(exitCode, code2, message) {
|
|
55
55
|
super(message);
|
|
56
56
|
Error.captureStackTrace(this, this.constructor);
|
|
57
57
|
this.name = this.constructor.name;
|
|
58
|
-
this.code =
|
|
58
|
+
this.code = code2;
|
|
59
59
|
this.exitCode = exitCode;
|
|
60
60
|
this.nestedError = void 0;
|
|
61
61
|
}
|
|
@@ -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);
|
|
@@ -1649,9 +1649,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1649
1649
|
* @return never
|
|
1650
1650
|
* @private
|
|
1651
1651
|
*/
|
|
1652
|
-
_exit(exitCode,
|
|
1652
|
+
_exit(exitCode, code2, message) {
|
|
1653
1653
|
if (this._exitCallback) {
|
|
1654
|
-
this._exitCallback(new CommanderError2(exitCode,
|
|
1654
|
+
this._exitCallback(new CommanderError2(exitCode, code2, message));
|
|
1655
1655
|
}
|
|
1656
1656
|
process3.exit(exitCode);
|
|
1657
1657
|
}
|
|
@@ -2289,14 +2289,14 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2289
2289
|
});
|
|
2290
2290
|
}
|
|
2291
2291
|
const exitCallback = this._exitCallback;
|
|
2292
|
-
proc.on("close", (
|
|
2293
|
-
|
|
2292
|
+
proc.on("close", (code2) => {
|
|
2293
|
+
code2 = code2 ?? 1;
|
|
2294
2294
|
if (!exitCallback) {
|
|
2295
|
-
process3.exit(
|
|
2295
|
+
process3.exit(code2);
|
|
2296
2296
|
} else {
|
|
2297
2297
|
exitCallback(
|
|
2298
2298
|
new CommanderError2(
|
|
2299
|
-
|
|
2299
|
+
code2,
|
|
2300
2300
|
"commander.executeSubCommandAsync",
|
|
2301
2301
|
"(close)"
|
|
2302
2302
|
)
|
|
@@ -2803,8 +2803,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2803
2803
|
}
|
|
2804
2804
|
const config2 = errorOptions || {};
|
|
2805
2805
|
const exitCode = config2.exitCode || 1;
|
|
2806
|
-
const
|
|
2807
|
-
this._exit(exitCode,
|
|
2806
|
+
const code2 = config2.code || "commander.error";
|
|
2807
|
+
this._exit(exitCode, code2, message);
|
|
2808
2808
|
}
|
|
2809
2809
|
/**
|
|
2810
2810
|
* Apply any option related environment variables, if option does
|
|
@@ -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;
|
|
@@ -6682,12 +6578,12 @@ function isIso7064Mod97(iban3) {
|
|
|
6682
6578
|
let remainder = 0;
|
|
6683
6579
|
const len = iban3.length;
|
|
6684
6580
|
for (let i = 4; i < len; i++) {
|
|
6685
|
-
const
|
|
6686
|
-
remainder = (
|
|
6581
|
+
const code2 = iban3.charCodeAt(i);
|
|
6582
|
+
remainder = (code2 >= 65 ? remainder * 100 + (code2 - 55) : remainder * 10 + (code2 - 48)) % 97;
|
|
6687
6583
|
}
|
|
6688
6584
|
for (let i = 0; i < 4; i++) {
|
|
6689
|
-
const
|
|
6690
|
-
remainder = (
|
|
6585
|
+
const code2 = iban3.charCodeAt(i);
|
|
6586
|
+
remainder = (code2 >= 65 ? remainder * 100 + (code2 - 55) : remainder * 10 + (code2 - 48)) % 97;
|
|
6691
6587
|
}
|
|
6692
6588
|
return remainder === 1;
|
|
6693
6589
|
}
|
|
@@ -7117,7 +7013,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
7117
7013
|
}
|
|
7118
7014
|
return propValues;
|
|
7119
7015
|
});
|
|
7120
|
-
const
|
|
7016
|
+
const isObject3 = isObject;
|
|
7121
7017
|
const catchall = def.catchall;
|
|
7122
7018
|
let value;
|
|
7123
7019
|
const memo2 = globalConfig.memoizer;
|
|
@@ -7125,7 +7021,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
7125
7021
|
inst._zod.parse = (payload, ctx) => {
|
|
7126
7022
|
value ?? (value = _normalized.value);
|
|
7127
7023
|
const input2 = payload.value;
|
|
7128
|
-
if (!
|
|
7024
|
+
if (!isObject3(input2)) {
|
|
7129
7025
|
payload.issues.push({
|
|
7130
7026
|
expected: "object",
|
|
7131
7027
|
code: "invalid_type",
|
|
@@ -7261,7 +7157,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
7261
7157
|
return doc.compile();
|
|
7262
7158
|
};
|
|
7263
7159
|
let fastpass;
|
|
7264
|
-
const
|
|
7160
|
+
const isObject3 = isObject;
|
|
7265
7161
|
const jit = !globalConfig.jitless;
|
|
7266
7162
|
const allowsEval2 = allowsEval;
|
|
7267
7163
|
const fastEnabled = jit && allowsEval2.value;
|
|
@@ -7270,7 +7166,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
7270
7166
|
inst._zod.parse = (payload, ctx) => {
|
|
7271
7167
|
value ?? (value = _normalized.value);
|
|
7272
7168
|
const input2 = payload.value;
|
|
7273
|
-
if (!
|
|
7169
|
+
if (!isObject3(input2)) {
|
|
7274
7170
|
payload.issues.push({
|
|
7275
7171
|
expected: "object",
|
|
7276
7172
|
code: "invalid_type",
|
|
@@ -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
|
}
|
|
@@ -16605,12 +16501,12 @@ function compileFn(schema, options) {
|
|
|
16605
16501
|
doc.write(outputAccessor === null ? `return true;` : `return ${outputAccessor};`);
|
|
16606
16502
|
const constantNames = ["INVALID", ...ctx.constants.keys()];
|
|
16607
16503
|
const constantValues = [INVALID, ...ctx.constants.values()];
|
|
16608
|
-
const
|
|
16504
|
+
const code2 = doc.content.join("\n");
|
|
16609
16505
|
const fullCode = options?.debug ? constantNames.length > 0 ? `// Constants: ${constantNames.join(", ")}
|
|
16610
|
-
${
|
|
16506
|
+
${code2}` : code2 : "";
|
|
16611
16507
|
const F = Function;
|
|
16612
16508
|
const factoryCode = `return (input) => {
|
|
16613
|
-
${
|
|
16509
|
+
${code2}
|
|
16614
16510
|
}`;
|
|
16615
16511
|
let fn;
|
|
16616
16512
|
try {
|
|
@@ -19536,8 +19432,8 @@ function finalize(ctx, schema) {
|
|
|
19536
19432
|
compactTypeUnion(entry[1].def ?? entry[1].schema);
|
|
19537
19433
|
}
|
|
19538
19434
|
}
|
|
19539
|
-
for (const
|
|
19540
|
-
|
|
19435
|
+
for (const rewrite2 of ctx.deferred)
|
|
19436
|
+
rewrite2();
|
|
19541
19437
|
if (ctx.intersections.length) {
|
|
19542
19438
|
const carriers = /* @__PURE__ */ new Map();
|
|
19543
19439
|
for (const seen of ctx.seen.values()) {
|
|
@@ -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-z0-9][a-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(code2, message, options) {
|
|
23242
|
+
super(message, options);
|
|
23243
|
+
this.code = code2;
|
|
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}`;
|
|
@@ -23348,13 +23476,20 @@ var timestampSchema = external_exports.string({ message: TIMESTAMP_MESSAGE }).re
|
|
|
23348
23476
|
function toMilliseconds(timestamp) {
|
|
23349
23477
|
return new Date(timestamp).toISOString();
|
|
23350
23478
|
}
|
|
23351
|
-
|
|
23479
|
+
function foldProjectName(name) {
|
|
23480
|
+
return name.replace(/[A-Z]+/g, (letters) => letters.toLowerCase());
|
|
23481
|
+
}
|
|
23482
|
+
var projectNameSchema = external_exports.string({ message: PROJECT_MESSAGE }).overwrite(foldProjectName).regex(PROJECT_NAME_RE, { message: PROJECT_MESSAGE });
|
|
23352
23483
|
var orgSlugSchema = external_exports.string({ message: SLUG_MESSAGE }).refine((value) => ORG_SLUG_RE.test(value) && !value.includes("--"), {
|
|
23353
23484
|
message: SLUG_MESSAGE
|
|
23354
23485
|
});
|
|
23355
23486
|
var severitySchema = external_exports.enum(SEVERITIES, { message: SEVERITY_MESSAGE });
|
|
23356
23487
|
var statusSchema = external_exports.enum(STATUSES, { message: STATUS_MESSAGE });
|
|
23357
|
-
var
|
|
23488
|
+
var REASON_MESSAGE = `must be one of ${REASONS.join(", ")}`;
|
|
23489
|
+
var reasonSchema = external_exports.enum(REASONS, { message: REASON_MESSAGE });
|
|
23490
|
+
var idSchema = boundedString(ID_MAX, ID_MESSAGE);
|
|
23491
|
+
var resolutionSchema = freeText(RESOLUTION_MAX);
|
|
23492
|
+
var storedResolutionSchema = boundedString(RESOLUTION_MAX);
|
|
23358
23493
|
var TAG_LENGTH_MESSAGE = lengthMessage(1, TAG_MAX);
|
|
23359
23494
|
var SUGGESTION_LENGTH_MESSAGE = `must be at most ${TAG_MAX} characters after "${OTHER_TAG_PREFIX}"`;
|
|
23360
23495
|
var tagSchema = external_exports.string({ message: "must be a string" }).trim().min(1, { message: TAG_LENGTH_MESSAGE }).superRefine((tag, ctx) => {
|
|
@@ -23370,8 +23505,8 @@ var tagSchema = external_exports.string({ message: "must be a string" }).trim().
|
|
|
23370
23505
|
}
|
|
23371
23506
|
});
|
|
23372
23507
|
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}"
|
|
23508
|
+
var UNTAGGED_TAG_MESSAGE = `must not be "${UNTAGGED_TAG}" (reserved for untagged)`;
|
|
23509
|
+
var RESERVED_TAG_MESSAGE = `must not be "${OTHER_TAG}" or start with "${OTHER_TAG_PREFIX}" (reserved)`;
|
|
23375
23510
|
var ALLOWED_TAG_MESSAGE = 'must use only letters, digits, ".", "_" and "-", and start with a letter or digit';
|
|
23376
23511
|
function normalizeTag(tag) {
|
|
23377
23512
|
const lower = tag.toLowerCase();
|
|
@@ -23386,7 +23521,7 @@ var complaintTagSchema = tagSchema.refine((tag) => tag !== UNTAGGED_TAG, {
|
|
|
23386
23521
|
var tagsSchema = external_exports.array(complaintTagSchema, { message: TAGS_TYPE_MESSAGE }).transform(normalizeTags).refine((tags) => tags.length <= TAGS_MAX, {
|
|
23387
23522
|
message: `must have at most ${TAGS_MAX} tags`
|
|
23388
23523
|
});
|
|
23389
|
-
var tagsChangeObject = external_exports.
|
|
23524
|
+
var tagsChangeObject = external_exports.strictObject({
|
|
23390
23525
|
tags: tagsSchema.refine((tags) => tags.length > 0, {
|
|
23391
23526
|
message: "must list at least 1 tag"
|
|
23392
23527
|
})
|
|
@@ -23474,17 +23609,21 @@ var limitSchema = external_exports.preprocess((value) => {
|
|
|
23474
23609
|
const parsed = Number(trimmed);
|
|
23475
23610
|
return Number.isNaN(parsed) ? value : parsed;
|
|
23476
23611
|
}, external_exports.number({ message: LIMIT_MESSAGE }).int({ message: LIMIT_MESSAGE }).min(1, { message: LIMIT_MESSAGE }).max(MAX_LIMIT, { message: LIMIT_MESSAGE }));
|
|
23477
|
-
var
|
|
23612
|
+
var agentSchema = freeText(AGENT_MAX);
|
|
23613
|
+
var newComplaintShape = {
|
|
23478
23614
|
org: orgSlugSchema,
|
|
23479
23615
|
project: projectNameSchema,
|
|
23480
|
-
message:
|
|
23616
|
+
message: freeText(MESSAGE_MAX),
|
|
23481
23617
|
severity: severitySchema.optional(),
|
|
23482
23618
|
tags: tagsSchema.optional(),
|
|
23483
|
-
agent:
|
|
23484
|
-
reporter:
|
|
23485
|
-
reporterName:
|
|
23486
|
-
cwd: cwdSchema.optional()
|
|
23487
|
-
|
|
23619
|
+
agent: agentSchema.optional(),
|
|
23620
|
+
reporter: freeText(REPORTER_MAX).optional(),
|
|
23621
|
+
reporterName: freeText(REPORTER_MAX).optional(),
|
|
23622
|
+
cwd: cwdSchema.optional(),
|
|
23623
|
+
// Free text: it is trimmed and may hold newlines, and nothing parses it.
|
|
23624
|
+
cmd: freeText(CMD_MAX).optional()
|
|
23625
|
+
};
|
|
23626
|
+
var newComplaintObject = external_exports.strictObject(newComplaintShape);
|
|
23488
23627
|
function withDefaults(input2) {
|
|
23489
23628
|
return {
|
|
23490
23629
|
org: input2.org,
|
|
@@ -23496,74 +23635,207 @@ function withDefaults(input2) {
|
|
|
23496
23635
|
reporter: input2.reporter ?? DEFAULT_REPORTER,
|
|
23497
23636
|
// The server sets the display name, so it stays absent when nobody did.
|
|
23498
23637
|
...input2.reporterName === void 0 ? {} : { reporterName: input2.reporterName },
|
|
23499
|
-
...input2.cwd === void 0 ? {} : { cwd: input2.cwd }
|
|
23638
|
+
...input2.cwd === void 0 ? {} : { cwd: input2.cwd },
|
|
23639
|
+
...input2.cmd === void 0 ? {} : { cmd: input2.cmd }
|
|
23500
23640
|
};
|
|
23501
23641
|
}
|
|
23502
|
-
var newComplaintSchema = external_exports.preprocess(
|
|
23503
|
-
var
|
|
23642
|
+
var newComplaintSchema = external_exports.preprocess(complaintInput(NEW_NULLABLE_KEYS, NEW_BLANKABLE_KEYS), newComplaintObject).transform(withDefaults);
|
|
23643
|
+
var actorSchema = boundedString(REPORTER_MAX);
|
|
23644
|
+
var storedComplaintObject = external_exports.object({
|
|
23645
|
+
...newComplaintShape,
|
|
23646
|
+
// The free text of a stored record is read without the control rule, so a
|
|
23647
|
+
// record written before it stays readable.
|
|
23648
|
+
message: boundedString(MESSAGE_MAX),
|
|
23649
|
+
agent: boundedString(AGENT_MAX).optional(),
|
|
23650
|
+
reporter: boundedString(REPORTER_MAX).optional(),
|
|
23651
|
+
reporterName: boundedString(REPORTER_MAX).optional(),
|
|
23652
|
+
cmd: boundedString(CMD_MAX).optional(),
|
|
23504
23653
|
id: external_exports.string({ message: ID_MESSAGE }).trim().min(1, { message: ID_MESSAGE }),
|
|
23654
|
+
fingerprint: external_exports.string({ message: ID_MESSAGE }).min(1, { message: ID_MESSAGE }).optional(),
|
|
23655
|
+
recurred: external_exports.boolean({ message: "must be a boolean" }).optional(),
|
|
23505
23656
|
createdAt: timestampSchema,
|
|
23506
23657
|
status: statusSchema.optional(),
|
|
23507
|
-
resolution:
|
|
23658
|
+
resolution: storedResolutionSchema.optional(),
|
|
23659
|
+
reason: reasonSchema.optional(),
|
|
23660
|
+
duplicateOf: idSchema.optional(),
|
|
23661
|
+
statusChangedAt: timestampSchema.optional(),
|
|
23662
|
+
statusChangedBy: actorSchema.optional(),
|
|
23663
|
+
statusChangedByName: actorSchema.optional(),
|
|
23508
23664
|
updatedAt: timestampSchema.optional()
|
|
23509
23665
|
});
|
|
23510
|
-
var complaintSchema = external_exports.preprocess(
|
|
23666
|
+
var complaintSchema = external_exports.preprocess(complaintInput(STORED_NULLABLE_KEYS, STORED_BLANKABLE_KEYS), storedComplaintObject).transform((input2) => ({
|
|
23511
23667
|
id: input2.id,
|
|
23512
23668
|
...withDefaults(input2),
|
|
23669
|
+
fingerprint: input2.fingerprint ?? fingerprintOf(input2.message),
|
|
23670
|
+
// The store writes the flag only when it is true.
|
|
23671
|
+
...input2.recurred === true ? { recurred: true } : {},
|
|
23513
23672
|
// Millisecond precision, so a record written without a fraction still
|
|
23514
23673
|
// compares by time against a `since` and an `until`.
|
|
23515
23674
|
createdAt: toMilliseconds(input2.createdAt),
|
|
23516
23675
|
status: input2.status ?? DEFAULT_STATUS,
|
|
23517
23676
|
...input2.resolution === void 0 ? {} : { resolution: input2.resolution },
|
|
23677
|
+
...input2.reason === void 0 ? {} : { reason: input2.reason },
|
|
23678
|
+
...input2.duplicateOf === void 0 ? {} : { duplicateOf: input2.duplicateOf },
|
|
23679
|
+
...input2.statusChangedAt === void 0 ? {} : { statusChangedAt: toMilliseconds(input2.statusChangedAt) },
|
|
23680
|
+
...input2.statusChangedBy === void 0 ? {} : { statusChangedBy: input2.statusChangedBy },
|
|
23681
|
+
...input2.statusChangedByName === void 0 ? {} : { statusChangedByName: input2.statusChangedByName },
|
|
23518
23682
|
...input2.updatedAt === void 0 ? {} : { updatedAt: toMilliseconds(input2.updatedAt) }
|
|
23519
23683
|
}));
|
|
23520
|
-
var
|
|
23684
|
+
var REASON_STATUS_MESSAGE = "only a resolve or a dismiss takes a reason";
|
|
23685
|
+
var DUPLICATE_OF_MESSAGE = `needs reason ${DUPLICATE_REASON}`;
|
|
23686
|
+
var statusChangeShape = {
|
|
23521
23687
|
status: statusSchema,
|
|
23522
|
-
resolution: resolutionSchema.optional()
|
|
23523
|
-
|
|
23524
|
-
|
|
23525
|
-
|
|
23526
|
-
|
|
23527
|
-
|
|
23528
|
-
|
|
23529
|
-
|
|
23530
|
-
|
|
23531
|
-
|
|
23532
|
-
|
|
23533
|
-
|
|
23534
|
-
|
|
23535
|
-
|
|
23536
|
-
}
|
|
23537
|
-
|
|
23688
|
+
resolution: resolutionSchema.optional(),
|
|
23689
|
+
reason: reasonSchema.optional(),
|
|
23690
|
+
duplicateOf: idSchema.optional()
|
|
23691
|
+
};
|
|
23692
|
+
function checkReason(change, ctx) {
|
|
23693
|
+
if (change.reason !== void 0 && change.status !== "resolved" && change.status !== "dismissed") {
|
|
23694
|
+
ctx.addIssue({ code: "custom", path: ["reason"], message: REASON_STATUS_MESSAGE });
|
|
23695
|
+
}
|
|
23696
|
+
if (change.duplicateOf !== void 0 && change.reason !== DUPLICATE_REASON) {
|
|
23697
|
+
ctx.addIssue({
|
|
23698
|
+
code: "custom",
|
|
23699
|
+
path: ["duplicateOf"],
|
|
23700
|
+
message: DUPLICATE_OF_MESSAGE
|
|
23701
|
+
});
|
|
23702
|
+
}
|
|
23703
|
+
}
|
|
23704
|
+
function toStatusChange(input2) {
|
|
23705
|
+
return {
|
|
23706
|
+
status: input2.status,
|
|
23707
|
+
...input2.resolution === void 0 ? {} : { resolution: input2.resolution },
|
|
23708
|
+
...input2.reason === void 0 ? {} : { reason: input2.reason },
|
|
23709
|
+
...input2.duplicateOf === void 0 ? {} : { duplicateOf: input2.duplicateOf }
|
|
23710
|
+
};
|
|
23711
|
+
}
|
|
23712
|
+
var statusChangeSchema = external_exports.preprocess((value) => normalizeKeys(value, STATUS_CHANGE_KEYS), external_exports.strictObject(statusChangeShape).superRefine(checkReason)).transform(toStatusChange);
|
|
23713
|
+
var BULK_IDS_MESSAGE = `must list 1 to ${BULK_MAX} ids`;
|
|
23714
|
+
var BULK_BOTH_MESSAGE = "must change the status or the tags, not both";
|
|
23715
|
+
var BULK_NEITHER_MESSAGE = "must carry a status, addTags or removeTags";
|
|
23716
|
+
var BULK_NO_TAG_MESSAGE = "must carry at least 1 tag in addTags or removeTags";
|
|
23717
|
+
var BULK_TAG_KEYS = ["addTags", "removeTags"];
|
|
23718
|
+
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)]);
|
|
23719
|
+
var bulkChangeSchema = external_exports.preprocess((value) => dropNulls(normalizeKeys(value, STATUS_CHANGE_KEYS), BULK_TAG_KEYS), external_exports.strictObject({
|
|
23720
|
+
ids: bulkIdsSchema,
|
|
23721
|
+
status: statusChangeShape.status.optional(),
|
|
23722
|
+
resolution: statusChangeShape.resolution,
|
|
23723
|
+
reason: statusChangeShape.reason,
|
|
23724
|
+
duplicateOf: statusChangeShape.duplicateOf,
|
|
23725
|
+
addTags: tagsSchema.optional(),
|
|
23726
|
+
removeTags: tagsSchema.optional()
|
|
23727
|
+
}).superRefine((input2, ctx) => {
|
|
23728
|
+
const status = STATUS_CHANGE_KEYS.some((key) => input2[key] !== void 0);
|
|
23729
|
+
const tags = BULK_TAG_KEYS.some((key) => input2[key] !== void 0);
|
|
23730
|
+
if (status && tags) {
|
|
23731
|
+
ctx.addIssue({ code: "custom", message: BULK_BOTH_MESSAGE });
|
|
23732
|
+
return;
|
|
23733
|
+
}
|
|
23734
|
+
if (!status && !tags) {
|
|
23735
|
+
ctx.addIssue({ code: "custom", message: BULK_NEITHER_MESSAGE });
|
|
23736
|
+
return;
|
|
23737
|
+
}
|
|
23738
|
+
if (status) {
|
|
23739
|
+
if (input2.status === void 0) {
|
|
23740
|
+
ctx.addIssue({ code: "custom", path: ["status"], message: STATUS_MESSAGE });
|
|
23741
|
+
return;
|
|
23742
|
+
}
|
|
23743
|
+
checkReason({ ...input2, status: input2.status }, ctx);
|
|
23744
|
+
return;
|
|
23745
|
+
}
|
|
23746
|
+
const addTags = input2.addTags ?? [];
|
|
23747
|
+
const removeTags = input2.removeTags ?? [];
|
|
23748
|
+
if (addTags.length + removeTags.length === 0) {
|
|
23749
|
+
ctx.addIssue({ code: "custom", message: BULK_NO_TAG_MESSAGE });
|
|
23750
|
+
return;
|
|
23751
|
+
}
|
|
23752
|
+
for (const tag of removeTags) {
|
|
23753
|
+
if (!addTags.includes(tag))
|
|
23754
|
+
continue;
|
|
23755
|
+
ctx.addIssue({
|
|
23756
|
+
code: "custom",
|
|
23757
|
+
path: ["removeTags"],
|
|
23758
|
+
message: `${tag} is also in addTags`
|
|
23759
|
+
});
|
|
23760
|
+
}
|
|
23761
|
+
})).transform((input2) => {
|
|
23762
|
+
if (input2.status !== void 0) {
|
|
23763
|
+
return {
|
|
23764
|
+
ids: input2.ids,
|
|
23765
|
+
kind: "status",
|
|
23766
|
+
change: toStatusChange({ ...input2, status: input2.status })
|
|
23767
|
+
};
|
|
23768
|
+
}
|
|
23769
|
+
return {
|
|
23770
|
+
ids: input2.ids,
|
|
23771
|
+
kind: "tags",
|
|
23772
|
+
addTags: input2.addTags ?? [],
|
|
23773
|
+
removeTags: input2.removeTags ?? []
|
|
23774
|
+
};
|
|
23775
|
+
});
|
|
23776
|
+
var BEFORE_UNTIL_MESSAGE = "must be before until";
|
|
23777
|
+
function checkWindow(window, ctx) {
|
|
23778
|
+
const { since, until } = window;
|
|
23779
|
+
if (since === void 0 || until === void 0)
|
|
23780
|
+
return;
|
|
23781
|
+
if (Date.parse(since) < Date.parse(until))
|
|
23782
|
+
return;
|
|
23783
|
+
ctx.addIssue({ code: "custom", path: ["since"], message: BEFORE_UNTIL_MESSAGE });
|
|
23784
|
+
}
|
|
23785
|
+
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, {
|
|
23786
|
+
message: `must have at most ${TAGS_MAX} tags`
|
|
23787
|
+
});
|
|
23788
|
+
var FINGERPRINT_MESSAGE = lengthMessage(1, FINGERPRINT_MAX);
|
|
23789
|
+
var listFilterShape = {
|
|
23538
23790
|
org: orgSlugSchema,
|
|
23539
23791
|
project: projectNameSchema.optional(),
|
|
23540
23792
|
severity: severitySchema.optional(),
|
|
23541
23793
|
status: statusSchema.optional(),
|
|
23542
|
-
|
|
23543
|
-
tag: tagSchema.optional(),
|
|
23794
|
+
tags: filterTagsSchema.optional(),
|
|
23544
23795
|
reporter: boundedString(REPORTER_MAX).optional(),
|
|
23796
|
+
// Exact, so it is never trimmed, and never empty, because no fingerprint is.
|
|
23797
|
+
fingerprint: external_exports.string({ message: FINGERPRINT_MESSAGE }).min(1, { message: FINGERPRINT_MESSAGE }).max(FINGERPRINT_MAX, { message: FINGERPRINT_MESSAGE }).optional(),
|
|
23798
|
+
// The search is case-insensitive in every store.
|
|
23799
|
+
q: boundedString(SEARCH_MAX).transform((value) => value.toLowerCase()).optional(),
|
|
23545
23800
|
since: timestampSchema.optional(),
|
|
23546
23801
|
until: timestampSchema.optional(),
|
|
23547
23802
|
limit: limitSchema.optional()
|
|
23548
|
-
}
|
|
23549
|
-
var
|
|
23803
|
+
};
|
|
23804
|
+
var LIST_FILTER_KEYS = Object.keys(listFilterShape);
|
|
23805
|
+
var listFilterObject = external_exports.strictObject(listFilterShape).superRefine(checkWindow);
|
|
23806
|
+
var listFilterSchema = external_exports.preprocess((value) => dropNulls(normalizeKeys(value ?? {}, FILTER_KEYS), FILTER_NULLABLE_KEYS), listFilterObject).transform((input2) => ({
|
|
23550
23807
|
org: input2.org,
|
|
23551
23808
|
...input2.project === void 0 ? {} : { project: input2.project },
|
|
23552
23809
|
...input2.severity === void 0 ? {} : { severity: input2.severity },
|
|
23553
23810
|
...input2.status === void 0 ? {} : { status: input2.status },
|
|
23554
|
-
|
|
23555
|
-
...input2.tag === void 0 ? {} : { tag: normalizeTag(input2.tag) },
|
|
23811
|
+
...input2.tags === void 0 ? {} : { tags: input2.tags },
|
|
23556
23812
|
...input2.reporter === void 0 ? {} : { reporter: input2.reporter },
|
|
23813
|
+
...input2.fingerprint === void 0 ? {} : { fingerprint: input2.fingerprint },
|
|
23814
|
+
...input2.q === void 0 ? {} : { q: input2.q },
|
|
23557
23815
|
...input2.since === void 0 ? {} : { since: toMilliseconds(input2.since) },
|
|
23558
23816
|
...input2.until === void 0 ? {} : { until: toMilliseconds(input2.until) },
|
|
23559
23817
|
limit: input2.limit ?? DEFAULT_LIMIT
|
|
23560
23818
|
}));
|
|
23561
23819
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
23562
23820
|
var WINDOW_MESSAGE = `the window must not exceed ${STATS_MAX_WINDOW_DAYS} days`;
|
|
23563
|
-
var
|
|
23821
|
+
var ALL_WINDOW = "all";
|
|
23822
|
+
var ALL_WINDOW_MESSAGE = `must be ${ALL_WINDOW}`;
|
|
23823
|
+
var ALL_WINDOW_BOUNDS_MESSAGE = `${ALL_WINDOW} takes no since or until`;
|
|
23824
|
+
var statsQueryObject = external_exports.strictObject({
|
|
23564
23825
|
since: timestampSchema.optional(),
|
|
23565
23826
|
until: timestampSchema.optional(),
|
|
23566
|
-
status: statusSchema.optional()
|
|
23827
|
+
status: statusSchema.optional(),
|
|
23828
|
+
window: external_exports.literal(ALL_WINDOW, { message: ALL_WINDOW_MESSAGE }).optional()
|
|
23829
|
+
}).superRefine((input2, ctx) => {
|
|
23830
|
+
if (input2.window === void 0)
|
|
23831
|
+
return;
|
|
23832
|
+
if (input2.since === void 0 && input2.until === void 0)
|
|
23833
|
+
return;
|
|
23834
|
+
ctx.addIssue({
|
|
23835
|
+
code: "custom",
|
|
23836
|
+
path: ["window"],
|
|
23837
|
+
message: ALL_WINDOW_BOUNDS_MESSAGE
|
|
23838
|
+
});
|
|
23567
23839
|
});
|
|
23568
23840
|
var ROLE_MESSAGE = `must be one of ${ROLES.join(", ")}`;
|
|
23569
23841
|
var ORG_ROLE_MESSAGE = `must be one of ${ORG_ROLES.join(", ")}`;
|
|
@@ -23576,8 +23848,8 @@ var orgRoleSchema = external_exports.enum(ORG_ROLES, { message: ORG_ROLE_MESSAGE
|
|
|
23576
23848
|
var colourSchema = external_exports.enum(ORG_COLOURS, { message: COLOUR_MESSAGE });
|
|
23577
23849
|
var emailSchema = boundedString(EMAIL_MAX, EMAIL_MESSAGE).refine((value) => EMAIL_RE.test(value), { message: EMAIL_MESSAGE });
|
|
23578
23850
|
var inviteEmailSchema = emailSchema.transform((value) => value.toLowerCase());
|
|
23579
|
-
var displayNameSchema =
|
|
23580
|
-
var newOrgSchema = external_exports.preprocess((value) => normalizeKeys(value, ["colour"]), external_exports.
|
|
23851
|
+
var displayNameSchema = freeText(DISPLAY_NAME_MAX);
|
|
23852
|
+
var newOrgSchema = external_exports.preprocess((value) => normalizeKeys(value, ["colour"]), external_exports.strictObject({
|
|
23581
23853
|
slug: orgSlugSchema.refine((value) => !RESERVED_SLUGS.some((reserved) => reserved === value), { message: RESERVED_SLUG_MESSAGE }),
|
|
23582
23854
|
name: displayNameSchema,
|
|
23583
23855
|
colour: colourSchema.optional()
|
|
@@ -23586,18 +23858,21 @@ var newOrgSchema = external_exports.preprocess((value) => normalizeKeys(value, [
|
|
|
23586
23858
|
name: input2.name,
|
|
23587
23859
|
colour: input2.colour ?? DEFAULT_ORG_COLOUR
|
|
23588
23860
|
}));
|
|
23589
|
-
var orgPatchSchema = external_exports.preprocess((value) => normalizeKeys(value, ["name", "colour"]), external_exports.
|
|
23861
|
+
var orgPatchSchema = external_exports.preprocess((value) => normalizeKeys(value, ["name", "colour"]), external_exports.strictObject({
|
|
23862
|
+
name: displayNameSchema.optional(),
|
|
23863
|
+
colour: colourSchema.optional()
|
|
23864
|
+
})).transform((input2) => ({
|
|
23590
23865
|
...input2.name === void 0 ? {} : { name: input2.name },
|
|
23591
23866
|
...input2.colour === void 0 ? {} : { colour: input2.colour }
|
|
23592
23867
|
})).refine((patch) => Object.keys(patch).length > 0, { message: ORG_PATCH_MESSAGE });
|
|
23593
23868
|
var PLATFORM_PATCH_MESSAGE = "must set disabled, caps, or both";
|
|
23594
23869
|
var CAP_MESSAGE = "must be a positive whole number";
|
|
23595
|
-
var orgCapsSchema = external_exports.
|
|
23870
|
+
var orgCapsSchema = external_exports.strictObject({
|
|
23596
23871
|
projectsPerOrg: external_exports.number({ message: CAP_MESSAGE }).int({ message: CAP_MESSAGE }).min(1, { message: CAP_MESSAGE }).optional()
|
|
23597
23872
|
}).transform((input2) => ({
|
|
23598
23873
|
...input2.projectsPerOrg === void 0 ? {} : { projectsPerOrg: input2.projectsPerOrg }
|
|
23599
23874
|
}));
|
|
23600
|
-
var platformOrgPatchSchema = external_exports.
|
|
23875
|
+
var platformOrgPatchSchema = external_exports.strictObject({
|
|
23601
23876
|
disabled: external_exports.boolean({ message: "must be a boolean" }).optional(),
|
|
23602
23877
|
caps: orgCapsSchema.nullable().optional()
|
|
23603
23878
|
}).transform((input2) => ({
|
|
@@ -23606,30 +23881,41 @@ var platformOrgPatchSchema = external_exports.object({
|
|
|
23606
23881
|
})).refine((patch) => Object.keys(patch).length > 0, {
|
|
23607
23882
|
message: PLATFORM_PATCH_MESSAGE
|
|
23608
23883
|
});
|
|
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.
|
|
23884
|
+
var orgInviteSchema = external_exports.strictObject({ email: inviteEmailSchema, role: orgRoleSchema }).transform((input2) => ({ email: input2.email, role: input2.role }));
|
|
23885
|
+
var orgRoleChangeSchema = external_exports.strictObject({ role: orgRoleSchema }).transform((input2) => ({ role: input2.role }));
|
|
23886
|
+
var profileSchema = external_exports.preprocess((value) => normalizeKeys(value, ["name"]), external_exports.strictObject({ name: displayNameSchema })).transform((input2) => ({ name: input2.name }));
|
|
23887
|
+
var newProjectSchema = external_exports.preprocess((value) => normalizeKeys(value, ["tags"]), external_exports.strictObject({
|
|
23613
23888
|
project: projectNameSchema,
|
|
23614
23889
|
tags: allowedTagsSchema.optional()
|
|
23615
23890
|
})).transform((input2) => ({
|
|
23616
23891
|
project: input2.project,
|
|
23617
23892
|
...input2.tags === void 0 ? {} : { tags: input2.tags }
|
|
23618
23893
|
}));
|
|
23619
|
-
var projectUpdateSchema = external_exports.
|
|
23620
|
-
var tagPromotionSchema = external_exports.
|
|
23621
|
-
var roleChangeSchema = external_exports.
|
|
23622
|
-
var
|
|
23623
|
-
|
|
23624
|
-
expiresAt
|
|
23625
|
-
|
|
23626
|
-
|
|
23627
|
-
|
|
23628
|
-
}))
|
|
23894
|
+
var projectUpdateSchema = external_exports.strictObject({ tags: allowedTagsSchema.nullable() }).transform((input2) => ({ tags: input2.tags }));
|
|
23895
|
+
var tagPromotionSchema = external_exports.strictObject({ tag: allowedTagSchema }).transform((input2) => ({ tag: normalizeTag(input2.tag) }));
|
|
23896
|
+
var roleChangeSchema = external_exports.strictObject({ role: roleSchema }).transform((input2) => ({ role: input2.role }));
|
|
23897
|
+
var FUTURE_MESSAGE = "must be in the future";
|
|
23898
|
+
function newTokenSchema(now) {
|
|
23899
|
+
return external_exports.preprocess((value) => normalizeKeys(value, ["name", "expiresAt"]), external_exports.strictObject({
|
|
23900
|
+
name: freeText(TOKEN_NAME_MAX),
|
|
23901
|
+
expiresAt: timestampSchema.refine((value) => Date.parse(value) > now.getTime(), {
|
|
23902
|
+
message: FUTURE_MESSAGE
|
|
23903
|
+
}).optional()
|
|
23904
|
+
})).transform((input2) => ({
|
|
23905
|
+
name: input2.name,
|
|
23906
|
+
...input2.expiresAt === void 0 ? {} : { expiresAt: input2.expiresAt }
|
|
23907
|
+
}));
|
|
23908
|
+
}
|
|
23909
|
+
var UNKNOWN_KEY_MESSAGE = "unknown key";
|
|
23910
|
+
var emptyBodySchema = external_exports.strictObject({});
|
|
23629
23911
|
function formatIssues(error62) {
|
|
23630
|
-
return error62.issues.
|
|
23631
|
-
const path = issue2.path.map((part) => String(part))
|
|
23632
|
-
|
|
23912
|
+
return error62.issues.flatMap((issue2) => {
|
|
23913
|
+
const path = issue2.path.map((part) => String(part));
|
|
23914
|
+
if (issue2.code === "unrecognized_keys") {
|
|
23915
|
+
return issue2.keys.map((key) => `${[...path, key].join(".")}: ${UNKNOWN_KEY_MESSAGE}`);
|
|
23916
|
+
}
|
|
23917
|
+
const name = path.join(".");
|
|
23918
|
+
return [name === "" ? issue2.message : `${name}: ${issue2.message}`];
|
|
23633
23919
|
});
|
|
23634
23920
|
}
|
|
23635
23921
|
function run(schema, input2) {
|
|
@@ -23648,6 +23934,13 @@ function parseComplaint(input2) {
|
|
|
23648
23934
|
function parseStatusChange(input2) {
|
|
23649
23935
|
return run(statusChangeSchema, input2);
|
|
23650
23936
|
}
|
|
23937
|
+
var statusActorSchema = external_exports.strictObject({ by: actorSchema, byName: actorSchema.optional() }).transform((input2) => ({
|
|
23938
|
+
by: input2.by,
|
|
23939
|
+
...input2.byName === void 0 ? {} : { byName: input2.byName }
|
|
23940
|
+
}));
|
|
23941
|
+
function parseStatusActor(input2) {
|
|
23942
|
+
return run(statusActorSchema, input2);
|
|
23943
|
+
}
|
|
23651
23944
|
function parseTagsChange(input2) {
|
|
23652
23945
|
return run(tagsChangeSchema, input2);
|
|
23653
23946
|
}
|
|
@@ -23663,6 +23956,14 @@ function parseProjectName(input2) {
|
|
|
23663
23956
|
function parseTagPromotion(input2) {
|
|
23664
23957
|
return run(tagPromotionSchema, input2);
|
|
23665
23958
|
}
|
|
23959
|
+
function parseNewToken(input2, now = /* @__PURE__ */ new Date()) {
|
|
23960
|
+
return run(newTokenSchema(now), input2);
|
|
23961
|
+
}
|
|
23962
|
+
function parseAgentName(input2) {
|
|
23963
|
+
return run(external_exports.object({ agent: agentSchema }), { agent: input2 }).agent;
|
|
23964
|
+
}
|
|
23965
|
+
var IDEMPOTENCY_KEY_RE = new RegExp(`^[A-Za-z0-9_-]{1,${IDEMPOTENCY_KEY_MAX}}$`);
|
|
23966
|
+
var IDEMPOTENCY_KEY_MESSAGE = `Idempotency-Key: must be 1-${IDEMPOTENCY_KEY_MAX} letters, digits, "-" or "_"`;
|
|
23666
23967
|
function parseOrgSlug(input2) {
|
|
23667
23968
|
return run(orgSlugSchema, input2);
|
|
23668
23969
|
}
|
|
@@ -23718,8 +24019,8 @@ function summarizeComplaints(complaints) {
|
|
|
23718
24019
|
var SUMMARY_WORDS = 20;
|
|
23719
24020
|
var ELLIPSIS = "\u2026";
|
|
23720
24021
|
function summarize(message) {
|
|
23721
|
-
const line = message
|
|
23722
|
-
if (line ===
|
|
24022
|
+
const line = headlineOf(message);
|
|
24023
|
+
if (line === "")
|
|
23723
24024
|
return "";
|
|
23724
24025
|
const words = line.split(/\s+/);
|
|
23725
24026
|
if (words.length <= SUMMARY_WORDS)
|
|
@@ -23727,8 +24028,57 @@ function summarize(message) {
|
|
|
23727
24028
|
return `${words.slice(0, SUMMARY_WORDS).join(" ")}${ELLIPSIS}`;
|
|
23728
24029
|
}
|
|
23729
24030
|
|
|
24031
|
+
// ../core/dist/query.js
|
|
24032
|
+
var TAG_KEY = "tag";
|
|
24033
|
+
var SINGLE_KEYS = new Set(LIST_FILTER_KEYS.filter((key) => key !== "org" && key !== "tags"));
|
|
24034
|
+
function listQueryOf(filter) {
|
|
24035
|
+
const pairs = [];
|
|
24036
|
+
for (const key of LIST_FILTER_KEYS) {
|
|
24037
|
+
if (key === "org")
|
|
24038
|
+
continue;
|
|
24039
|
+
const value = filter[key];
|
|
24040
|
+
if (value === void 0)
|
|
24041
|
+
continue;
|
|
24042
|
+
if (key === "tags") {
|
|
24043
|
+
for (const tag of filter.tags ?? [])
|
|
24044
|
+
pairs.push([TAG_KEY, tag]);
|
|
24045
|
+
continue;
|
|
24046
|
+
}
|
|
24047
|
+
pairs.push([key, String(value)]);
|
|
24048
|
+
}
|
|
24049
|
+
return pairs;
|
|
24050
|
+
}
|
|
24051
|
+
|
|
24052
|
+
// ../core/dist/endpoint.js
|
|
24053
|
+
var DEFAULT_ENDPOINT = "https://api.letthemquibble.com";
|
|
24054
|
+
var DISCOVERY_PATH = "/.well-known/quibble.json";
|
|
24055
|
+
var LOCAL_ENDPOINT = "local";
|
|
24056
|
+
var LOOPBACK_HOSTS = ["localhost", "127.0.0.1"];
|
|
24057
|
+
function trimSlash(url2) {
|
|
24058
|
+
return url2.replace(/\/+$/, "");
|
|
24059
|
+
}
|
|
24060
|
+
function normalizeEndpoint(url2) {
|
|
24061
|
+
const text = trimSlash(url2.trim());
|
|
24062
|
+
try {
|
|
24063
|
+
const parsed = new URL(text);
|
|
24064
|
+
return `${parsed.protocol}//${parsed.host}${trimSlash(parsed.pathname)}`;
|
|
24065
|
+
} catch {
|
|
24066
|
+
return text;
|
|
24067
|
+
}
|
|
24068
|
+
}
|
|
24069
|
+
function isDefaultEndpoint(url2) {
|
|
24070
|
+
return normalizeEndpoint(url2) === DEFAULT_ENDPOINT;
|
|
24071
|
+
}
|
|
24072
|
+
function isLoopbackUrl(url2) {
|
|
24073
|
+
try {
|
|
24074
|
+
return LOOPBACK_HOSTS.includes(new URL(url2).hostname);
|
|
24075
|
+
} catch {
|
|
24076
|
+
return false;
|
|
24077
|
+
}
|
|
24078
|
+
}
|
|
24079
|
+
|
|
23730
24080
|
// ../core/dist/service.js
|
|
23731
|
-
import { randomUUID } from "node:crypto";
|
|
24081
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
23732
24082
|
function applyTagRules(tags, allowedTags) {
|
|
23733
24083
|
if (allowedTags === void 0)
|
|
23734
24084
|
return [...tags];
|
|
@@ -23744,23 +24094,57 @@ async function createComplaint(store, input2, options = {}) {
|
|
|
23744
24094
|
id: options.id === void 0 ? randomUUID() : options.id(),
|
|
23745
24095
|
...normalized,
|
|
23746
24096
|
tags,
|
|
24097
|
+
fingerprint: fingerprintOf(normalized.message),
|
|
23747
24098
|
createdAt: stamp(options),
|
|
23748
24099
|
status: DEFAULT_STATUS
|
|
23749
24100
|
};
|
|
23750
|
-
|
|
23751
|
-
|
|
24101
|
+
if (options.reserve !== void 0)
|
|
24102
|
+
await options.reserve(complaint);
|
|
24103
|
+
const match = options.keyed === true ? await store.create(complaint, { keyed: true }) : await store.create(complaint);
|
|
24104
|
+
return {
|
|
24105
|
+
...complaint,
|
|
24106
|
+
openMatches: match.open,
|
|
24107
|
+
recurred: match.resolved
|
|
24108
|
+
};
|
|
23752
24109
|
}
|
|
23753
24110
|
async function listComplaints(store, filter) {
|
|
23754
24111
|
const parsed = parseListFilter(filter);
|
|
23755
24112
|
return store.list(parsed);
|
|
23756
24113
|
}
|
|
24114
|
+
async function listComplaintPage(store, filter) {
|
|
24115
|
+
const parsed = parseListFilter(filter);
|
|
24116
|
+
if (store.listPage !== void 0)
|
|
24117
|
+
return store.listPage(parsed);
|
|
24118
|
+
return { complaints: await store.list(parsed), truncated: false };
|
|
24119
|
+
}
|
|
24120
|
+
function scanComplaints(store, filter) {
|
|
24121
|
+
return store.scan(parseListFilter(filter));
|
|
24122
|
+
}
|
|
23757
24123
|
async function getComplaint(store, id) {
|
|
23758
24124
|
return store.get(id);
|
|
23759
24125
|
}
|
|
23760
|
-
async function updateComplaintStatus(store, id, change, options
|
|
24126
|
+
async function updateComplaintStatus(store, id, change, options) {
|
|
23761
24127
|
const parsed = parseStatusChange(change);
|
|
23762
|
-
const
|
|
23763
|
-
|
|
24128
|
+
const actor = parseStatusActor(options.actor);
|
|
24129
|
+
if (parsed.duplicateOf !== void 0) {
|
|
24130
|
+
const target = options.current ?? await store.get(id);
|
|
24131
|
+
if (target === void 0)
|
|
24132
|
+
return void 0;
|
|
24133
|
+
if (parsed.duplicateOf === id) {
|
|
24134
|
+
throw new ValidationError(["duplicateOf: must name another complaint"]);
|
|
24135
|
+
}
|
|
24136
|
+
const original = await store.get(parsed.duplicateOf);
|
|
24137
|
+
if (original === void 0 || original.org !== target.org) {
|
|
24138
|
+
throw new ValidationError([`duplicateOf: no complaint ${parsed.duplicateOf}`]);
|
|
24139
|
+
}
|
|
24140
|
+
}
|
|
24141
|
+
const update = {
|
|
24142
|
+
...parsed,
|
|
24143
|
+
updatedAt: stamp(options),
|
|
24144
|
+
changedBy: actor.by,
|
|
24145
|
+
...actor.byName === void 0 ? {} : { changedByName: actor.byName }
|
|
24146
|
+
};
|
|
24147
|
+
return store.update(id, update, options.current);
|
|
23764
24148
|
}
|
|
23765
24149
|
async function updateComplaintTags(store, id, change, options = {}) {
|
|
23766
24150
|
const parsed = parseTagsChange(change);
|
|
@@ -23768,7 +24152,7 @@ async function updateComplaintTags(store, id, change, options = {}) {
|
|
|
23768
24152
|
tags: applyTagRules(parsed.tags, options.allowedTags),
|
|
23769
24153
|
updatedAt: stamp(options)
|
|
23770
24154
|
};
|
|
23771
|
-
return store.updateTags(id, update);
|
|
24155
|
+
return store.updateTags(id, update, options.current);
|
|
23772
24156
|
}
|
|
23773
24157
|
|
|
23774
24158
|
// ../core/dist/file-store.js
|
|
@@ -23776,15 +24160,36 @@ import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
|
23776
24160
|
import { dirname } from "node:path";
|
|
23777
24161
|
var STATUS_OP = "status";
|
|
23778
24162
|
var TAGS_OP = "tags";
|
|
24163
|
+
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
23779
24164
|
var FileStore = class {
|
|
23780
24165
|
/** Path of the JSONL file. */
|
|
23781
24166
|
path;
|
|
24167
|
+
/** The creates of this process, one after the other. */
|
|
24168
|
+
#creating = Promise.resolve();
|
|
23782
24169
|
constructor(path) {
|
|
23783
24170
|
this.path = path;
|
|
23784
24171
|
}
|
|
23785
|
-
/**
|
|
23786
|
-
|
|
23787
|
-
|
|
24172
|
+
/**
|
|
24173
|
+
* Counts what the complaint's project holds under its fingerprint, from the
|
|
24174
|
+
* folded records, then appends the one complaint line, with `recurred: true`
|
|
24175
|
+
* when a resolved match created in the last `RECURRENCE_WINDOW_DAYS` exists.
|
|
24176
|
+
* Creates the parent directory on the first write. A keyed complaint whose
|
|
24177
|
+
* id a record already holds is a `DuplicateIdError` carrying that record,
|
|
24178
|
+
* and nothing is written. The creates of one process run one at a time.
|
|
24179
|
+
*/
|
|
24180
|
+
create(complaint, options = {}) {
|
|
24181
|
+
const write = this.#creating.then(async () => {
|
|
24182
|
+
const records = await this.#readAll();
|
|
24183
|
+
const existing = records.find((record2) => record2.id === complaint.id);
|
|
24184
|
+
if (options.keyed === true && existing !== void 0) {
|
|
24185
|
+
throw new DuplicateIdError(complaint.id, existing);
|
|
24186
|
+
}
|
|
24187
|
+
const match = matchOf(records, complaint);
|
|
24188
|
+
await this.#append(match.resolved ? { ...complaint, recurred: true } : complaint);
|
|
24189
|
+
return match;
|
|
24190
|
+
});
|
|
24191
|
+
this.#creating = write.catch(() => void 0);
|
|
24192
|
+
return write;
|
|
23788
24193
|
}
|
|
23789
24194
|
/** Returns the matching complaints, newest first. */
|
|
23790
24195
|
async list(filter) {
|
|
@@ -23796,6 +24201,11 @@ var FileStore = class {
|
|
|
23796
24201
|
for (const complaint of await this.#matches(filter))
|
|
23797
24202
|
yield complaint;
|
|
23798
24203
|
}
|
|
24204
|
+
/** Returns how many complaints match, whatever the limit says. */
|
|
24205
|
+
async count(filter) {
|
|
24206
|
+
const complaints = await this.#readAll();
|
|
24207
|
+
return complaints.filter((complaint) => matchesFilter(complaint, filter)).length;
|
|
24208
|
+
}
|
|
23799
24209
|
/** Returns the complaint with that id, or `undefined`. */
|
|
23800
24210
|
async get(id) {
|
|
23801
24211
|
const complaints = await this.#readAll();
|
|
@@ -23804,26 +24214,32 @@ var FileStore = class {
|
|
|
23804
24214
|
/**
|
|
23805
24215
|
* Appends one status line and returns the updated complaint. Returns
|
|
23806
24216
|
* `undefined`, and writes nothing, when no complaint has that id. A deleted
|
|
23807
|
-
* complaint is reached like any other, so a reopen restores it.
|
|
24217
|
+
* complaint is reached like any other, so a reopen restores it. `current` is
|
|
24218
|
+
* ignored, because a fold reads every record anyway.
|
|
23808
24219
|
*/
|
|
23809
|
-
async update(id, change) {
|
|
24220
|
+
async update(id, change, _current) {
|
|
23810
24221
|
const complaint = await this.get(id);
|
|
23811
24222
|
if (complaint === void 0)
|
|
23812
24223
|
return void 0;
|
|
23813
|
-
|
|
24224
|
+
const line = {
|
|
23814
24225
|
op: STATUS_OP,
|
|
23815
24226
|
id,
|
|
23816
24227
|
status: change.status,
|
|
23817
24228
|
...change.resolution === void 0 ? {} : { resolution: change.resolution },
|
|
24229
|
+
...change.reason === void 0 ? {} : { reason: change.reason },
|
|
24230
|
+
...change.duplicateOf === void 0 ? {} : { duplicateOf: change.duplicateOf },
|
|
24231
|
+
changedBy: change.changedBy,
|
|
24232
|
+
...change.changedByName === void 0 ? {} : { changedByName: change.changedByName },
|
|
23818
24233
|
updatedAt: change.updatedAt
|
|
23819
|
-
}
|
|
23820
|
-
|
|
24234
|
+
};
|
|
24235
|
+
await this.#append(line);
|
|
24236
|
+
return applyStatus(complaint, line);
|
|
23821
24237
|
}
|
|
23822
24238
|
/**
|
|
23823
24239
|
* Appends one tags line and returns the updated complaint. Returns
|
|
23824
24240
|
* `undefined`, and writes nothing, when no complaint has that id.
|
|
23825
24241
|
*/
|
|
23826
|
-
async updateTags(id, change) {
|
|
24242
|
+
async updateTags(id, change, _current) {
|
|
23827
24243
|
const complaint = await this.get(id);
|
|
23828
24244
|
if (complaint === void 0)
|
|
23829
24245
|
return void 0;
|
|
@@ -23946,31 +24362,70 @@ var FileStore = class {
|
|
|
23946
24362
|
}
|
|
23947
24363
|
const change = parseStatusChange({
|
|
23948
24364
|
status: parsed["status"],
|
|
23949
|
-
resolution: parsed["resolution"]
|
|
24365
|
+
resolution: parsed["resolution"],
|
|
24366
|
+
reason: parsed["reason"],
|
|
24367
|
+
duplicateOf: parsed["duplicateOf"]
|
|
23950
24368
|
});
|
|
23951
|
-
|
|
24369
|
+
const changedBy = this.#actor(parsed, "changedBy", bad);
|
|
24370
|
+
const changedByName = this.#actor(parsed, "changedByName", bad);
|
|
24371
|
+
return {
|
|
24372
|
+
op,
|
|
24373
|
+
id,
|
|
24374
|
+
...change,
|
|
24375
|
+
...changedBy === void 0 ? {} : { changedBy },
|
|
24376
|
+
...changedByName === void 0 ? {} : { changedByName },
|
|
24377
|
+
updatedAt
|
|
24378
|
+
};
|
|
23952
24379
|
} catch (error62) {
|
|
23953
24380
|
if (!(error62 instanceof ValidationError))
|
|
23954
24381
|
throw error62;
|
|
23955
24382
|
throw bad(error62.message, error62);
|
|
23956
24383
|
}
|
|
23957
24384
|
}
|
|
24385
|
+
/** Reads the optional actor key of a status line. */
|
|
24386
|
+
#actor(parsed, key, bad) {
|
|
24387
|
+
const value = parsed[key];
|
|
24388
|
+
if (value === void 0)
|
|
24389
|
+
return void 0;
|
|
24390
|
+
if (typeof value !== "string" || value.trim() === "" || value.length > REPORTER_MAX) {
|
|
24391
|
+
throw bad(`${key}: must be 1-${REPORTER_MAX} characters`);
|
|
24392
|
+
}
|
|
24393
|
+
return value;
|
|
24394
|
+
}
|
|
23958
24395
|
/** The prefix that every malformed complaint line shares. */
|
|
23959
24396
|
#malformed(lineNumber) {
|
|
23960
24397
|
return `malformed complaint on line ${lineNumber} of ${this.path}`;
|
|
23961
24398
|
}
|
|
23962
24399
|
};
|
|
23963
24400
|
function applyStatus(complaint, change) {
|
|
23964
|
-
const
|
|
23965
|
-
|
|
24401
|
+
const { resolution: _resolution, reason: _reason, duplicateOf: _duplicateOf, statusChangedBy: _statusChangedBy, statusChangedByName: _statusChangedByName, ...rest } = complaint;
|
|
24402
|
+
return {
|
|
24403
|
+
...rest,
|
|
23966
24404
|
status: change.status,
|
|
24405
|
+
...change.resolution === void 0 ? {} : { resolution: change.resolution },
|
|
24406
|
+
...change.reason === void 0 ? {} : { reason: change.reason },
|
|
24407
|
+
...change.duplicateOf === void 0 ? {} : { duplicateOf: change.duplicateOf },
|
|
24408
|
+
statusChangedAt: change.updatedAt,
|
|
24409
|
+
...change.changedBy === void 0 ? {} : { statusChangedBy: change.changedBy },
|
|
24410
|
+
...change.changedByName === void 0 ? {} : { statusChangedByName: change.changedByName },
|
|
23967
24411
|
updatedAt: change.updatedAt
|
|
23968
24412
|
};
|
|
23969
|
-
|
|
23970
|
-
|
|
23971
|
-
|
|
23972
|
-
|
|
23973
|
-
|
|
24413
|
+
}
|
|
24414
|
+
function matchOf(complaints, complaint) {
|
|
24415
|
+
const since = new Date(Date.parse(complaint.createdAt) - RECURRENCE_WINDOW_DAYS * DAY_MS2).toISOString();
|
|
24416
|
+
let open2 = 0;
|
|
24417
|
+
let resolved = false;
|
|
24418
|
+
for (const each of complaints) {
|
|
24419
|
+
if (each.project !== complaint.project)
|
|
24420
|
+
continue;
|
|
24421
|
+
if (each.fingerprint !== complaint.fingerprint)
|
|
24422
|
+
continue;
|
|
24423
|
+
if (each.status === "open")
|
|
24424
|
+
open2 += 1;
|
|
24425
|
+
if (each.status === "resolved" && each.createdAt >= since)
|
|
24426
|
+
resolved = true;
|
|
24427
|
+
}
|
|
24428
|
+
return { open: open2, resolved };
|
|
23974
24429
|
}
|
|
23975
24430
|
function applyTags(complaint, change) {
|
|
23976
24431
|
return { ...complaint, tags: [...change.tags], updatedAt: change.updatedAt };
|
|
@@ -23990,11 +24445,17 @@ function matchesFilter(complaint, filter) {
|
|
|
23990
24445
|
if (filter.reporter !== void 0 && complaint.reporter !== filter.reporter) {
|
|
23991
24446
|
return false;
|
|
23992
24447
|
}
|
|
23993
|
-
if (filter.
|
|
23994
|
-
const kept = filter.tag === UNTAGGED_TAG ? complaint.tags.length === 0 : complaint.tags.includes(
|
|
24448
|
+
if (filter.tags !== void 0) {
|
|
24449
|
+
const kept = filter.tags.some((tag) => tag === UNTAGGED_TAG ? complaint.tags.length === 0 : complaint.tags.includes(tag));
|
|
23995
24450
|
if (!kept)
|
|
23996
24451
|
return false;
|
|
23997
24452
|
}
|
|
24453
|
+
if (filter.fingerprint !== void 0 && complaint.fingerprint !== filter.fingerprint) {
|
|
24454
|
+
return false;
|
|
24455
|
+
}
|
|
24456
|
+
if (filter.q !== void 0 && !complaint.message.toLowerCase().includes(filter.q)) {
|
|
24457
|
+
return false;
|
|
24458
|
+
}
|
|
23998
24459
|
if (filter.since !== void 0 && complaint.createdAt < filter.since)
|
|
23999
24460
|
return false;
|
|
24000
24461
|
if (filter.until !== void 0 && complaint.createdAt >= filter.until)
|
|
@@ -24028,6 +24489,10 @@ function defaultConfigPath(env = process.env) {
|
|
|
24028
24489
|
return join(defaultQuibbleHome(env), CONFIG_FILENAME);
|
|
24029
24490
|
}
|
|
24030
24491
|
|
|
24492
|
+
// src/config.ts
|
|
24493
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
24494
|
+
import { dirname as dirname3, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
|
|
24495
|
+
|
|
24031
24496
|
// src/credentials.ts
|
|
24032
24497
|
import { randomBytes } from "node:crypto";
|
|
24033
24498
|
import {
|
|
@@ -24039,11 +24504,98 @@ import {
|
|
|
24039
24504
|
writeFileSync
|
|
24040
24505
|
} from "node:fs";
|
|
24041
24506
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
24507
|
+
var CREDENTIALS_FILENAME = "credentials.json";
|
|
24508
|
+
var CREDENTIALS_MODE = 384;
|
|
24509
|
+
var HOME_MODE = 448;
|
|
24510
|
+
function defaultCredentialsPath(env) {
|
|
24511
|
+
return join2(defaultQuibbleHome(env), CREDENTIALS_FILENAME);
|
|
24512
|
+
}
|
|
24513
|
+
function readEntry(value) {
|
|
24514
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
24515
|
+
return void 0;
|
|
24516
|
+
}
|
|
24517
|
+
const raw = value;
|
|
24518
|
+
const keys = ["endpoint", "refreshToken", "accessToken", "expiresAt", "sub", "email"];
|
|
24519
|
+
for (const key of keys) {
|
|
24520
|
+
if (typeof raw[key] !== "string" || raw[key] === "") return void 0;
|
|
24521
|
+
}
|
|
24522
|
+
return {
|
|
24523
|
+
endpoint: raw["endpoint"],
|
|
24524
|
+
refreshToken: raw["refreshToken"],
|
|
24525
|
+
accessToken: raw["accessToken"],
|
|
24526
|
+
expiresAt: raw["expiresAt"],
|
|
24527
|
+
sub: raw["sub"],
|
|
24528
|
+
email: raw["email"]
|
|
24529
|
+
};
|
|
24530
|
+
}
|
|
24531
|
+
function readCredentials(env) {
|
|
24532
|
+
const path = defaultCredentialsPath(env);
|
|
24533
|
+
let text;
|
|
24534
|
+
try {
|
|
24535
|
+
text = readFileSync(path, "utf8");
|
|
24536
|
+
} catch (error62) {
|
|
24537
|
+
if (isNotFound(error62)) return {};
|
|
24538
|
+
throw new ConfigError(`cannot read ${path}`, path, { cause: error62 });
|
|
24539
|
+
}
|
|
24540
|
+
let parsed;
|
|
24541
|
+
try {
|
|
24542
|
+
parsed = JSON.parse(text);
|
|
24543
|
+
} catch (error62) {
|
|
24544
|
+
throw new ConfigError(`invalid JSON in ${path}`, path, { cause: error62 });
|
|
24545
|
+
}
|
|
24546
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
24547
|
+
throw new ConfigError(`invalid credentials in ${path}: must be a JSON object`, path);
|
|
24548
|
+
}
|
|
24549
|
+
const entries = {};
|
|
24550
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
24551
|
+
const entry = readEntry(value);
|
|
24552
|
+
if (entry !== void 0) entries[key] = entry;
|
|
24553
|
+
}
|
|
24554
|
+
return entries;
|
|
24555
|
+
}
|
|
24556
|
+
function findCredential(env, endpoint) {
|
|
24557
|
+
return readCredentials(env)[normalizeEndpoint(endpoint)];
|
|
24558
|
+
}
|
|
24559
|
+
function writeCredentials(env, entries) {
|
|
24560
|
+
const path = defaultCredentialsPath(env);
|
|
24561
|
+
mkdirSync(dirname2(path), { recursive: true, mode: HOME_MODE });
|
|
24562
|
+
const temp = join2(
|
|
24563
|
+
dirname2(path),
|
|
24564
|
+
`.${CREDENTIALS_FILENAME}.${randomBytes(6).toString("hex")}`
|
|
24565
|
+
);
|
|
24566
|
+
try {
|
|
24567
|
+
writeFileSync(temp, `${JSON.stringify(entries, null, 2)}
|
|
24568
|
+
`, {
|
|
24569
|
+
encoding: "utf8",
|
|
24570
|
+
mode: CREDENTIALS_MODE
|
|
24571
|
+
});
|
|
24572
|
+
chmodSync(temp, CREDENTIALS_MODE);
|
|
24573
|
+
renameSync(temp, path);
|
|
24574
|
+
} catch (error62) {
|
|
24575
|
+
rmSync(temp, { force: true });
|
|
24576
|
+
throw error62;
|
|
24577
|
+
}
|
|
24578
|
+
}
|
|
24579
|
+
function saveCredential(env, credential) {
|
|
24580
|
+
const key = normalizeEndpoint(credential.endpoint);
|
|
24581
|
+
const stored = { ...credential, endpoint: key };
|
|
24582
|
+
writeCredentials(env, { ...readCredentials(env), [key]: stored });
|
|
24583
|
+
return stored;
|
|
24584
|
+
}
|
|
24585
|
+
function removeCredential(env, endpoint) {
|
|
24586
|
+
const entries = readCredentials(env);
|
|
24587
|
+
const key = normalizeEndpoint(endpoint);
|
|
24588
|
+
if (entries[key] === void 0) return false;
|
|
24589
|
+
delete entries[key];
|
|
24590
|
+
writeCredentials(env, entries);
|
|
24591
|
+
return true;
|
|
24592
|
+
}
|
|
24042
24593
|
|
|
24043
24594
|
// src/errors.ts
|
|
24595
|
+
var AMBIGUOUS_ID = "ambiguous_id";
|
|
24044
24596
|
var UsageError = class extends QuibbleError {
|
|
24045
24597
|
constructor(message, options) {
|
|
24046
|
-
super("usage_error", message, options);
|
|
24598
|
+
super(options?.code ?? "usage_error", message, options);
|
|
24047
24599
|
}
|
|
24048
24600
|
};
|
|
24049
24601
|
var NotFoundError = class extends QuibbleError {
|
|
@@ -24058,9 +24610,18 @@ var RemoteError = class extends QuibbleError {
|
|
|
24058
24610
|
* other one stays exit 2.
|
|
24059
24611
|
*/
|
|
24060
24612
|
status;
|
|
24613
|
+
/** The `error.code` of the answer, when it carried one. */
|
|
24614
|
+
serverCode;
|
|
24615
|
+
/** The server's own message, when the answer carried one. */
|
|
24616
|
+
serverMessage;
|
|
24617
|
+
/** True when no server answered, which the offline queue of 5.13 reads. */
|
|
24618
|
+
unanswered;
|
|
24061
24619
|
constructor(message, options) {
|
|
24062
24620
|
super("network_error", message, options);
|
|
24063
24621
|
this.status = options?.status;
|
|
24622
|
+
this.serverCode = options?.serverCode;
|
|
24623
|
+
this.serverMessage = options?.serverMessage;
|
|
24624
|
+
this.unanswered = options?.unanswered === true;
|
|
24064
24625
|
}
|
|
24065
24626
|
};
|
|
24066
24627
|
function exitCodeFor(error62) {
|
|
@@ -24081,7 +24642,7 @@ var DEBUG_ENV = "QUIBBLE_DEBUG";
|
|
|
24081
24642
|
function debugEnabled(env) {
|
|
24082
24643
|
return env[DEBUG_ENV] === "1";
|
|
24083
24644
|
}
|
|
24084
|
-
function
|
|
24645
|
+
function trimSlash2(url2) {
|
|
24085
24646
|
return url2.replace(/\/+$/, "");
|
|
24086
24647
|
}
|
|
24087
24648
|
function originOf(url2) {
|
|
@@ -24117,12 +24678,12 @@ async function send(url2, request) {
|
|
|
24117
24678
|
if (isTimeout(error62)) {
|
|
24118
24679
|
throw new RemoteError(
|
|
24119
24680
|
`request to ${originOf(url2)} timed out after ${timeoutMs / 1e3}s${detail(site)}`,
|
|
24120
|
-
{ cause: error62 }
|
|
24681
|
+
{ cause: error62, unanswered: true }
|
|
24121
24682
|
);
|
|
24122
24683
|
}
|
|
24123
24684
|
throw new RemoteError(
|
|
24124
24685
|
`cannot reach ${originOf(url2)}: ${describeError(error62)}${detail(site)}`,
|
|
24125
|
-
{ cause: error62 }
|
|
24686
|
+
{ cause: error62, unanswered: true }
|
|
24126
24687
|
);
|
|
24127
24688
|
}
|
|
24128
24689
|
}
|
|
@@ -24132,7 +24693,7 @@ async function readText(response, site) {
|
|
|
24132
24693
|
} catch (error62) {
|
|
24133
24694
|
throw new RemoteError(
|
|
24134
24695
|
`cannot read the response from ${originOf(site.url)}: ${describeError(error62)}${detail(site, response.status)}`,
|
|
24135
|
-
{ cause: error62 }
|
|
24696
|
+
{ cause: error62, unanswered: true }
|
|
24136
24697
|
);
|
|
24137
24698
|
}
|
|
24138
24699
|
}
|
|
@@ -24140,7 +24701,7 @@ function parseJson(text, site, status) {
|
|
|
24140
24701
|
try {
|
|
24141
24702
|
return JSON.parse(text);
|
|
24142
24703
|
} catch (error62) {
|
|
24143
|
-
throw new RemoteError(`the API returned
|
|
24704
|
+
throw new RemoteError(`the API returned invalid JSON${detail(site, status)}`, {
|
|
24144
24705
|
cause: error62
|
|
24145
24706
|
});
|
|
24146
24707
|
}
|
|
@@ -24176,134 +24737,40 @@ function serverCode(text) {
|
|
|
24176
24737
|
return stringOf(asRecord(record2?.["error"]), "code");
|
|
24177
24738
|
}
|
|
24178
24739
|
function remoteFailure(site, response, text) {
|
|
24179
|
-
const
|
|
24740
|
+
const own2 = serverMessage(text, response);
|
|
24741
|
+
const message = own2 ?? `the API returned HTTP ${response.status}`;
|
|
24180
24742
|
return new RemoteError(`${message}${detail(site, response.status)}`, {
|
|
24181
|
-
status: response.status
|
|
24743
|
+
status: response.status,
|
|
24744
|
+
serverCode: serverCode(text),
|
|
24745
|
+
serverMessage: own2
|
|
24182
24746
|
});
|
|
24183
24747
|
}
|
|
24184
24748
|
|
|
24185
|
-
// src/
|
|
24186
|
-
var
|
|
24187
|
-
var
|
|
24188
|
-
var
|
|
24189
|
-
|
|
24190
|
-
|
|
24191
|
-
|
|
24192
|
-
|
|
24193
|
-
|
|
24194
|
-
|
|
24195
|
-
|
|
24196
|
-
|
|
24197
|
-
|
|
24198
|
-
|
|
24199
|
-
|
|
24200
|
-
|
|
24201
|
-
|
|
24202
|
-
|
|
24203
|
-
|
|
24204
|
-
|
|
24205
|
-
|
|
24206
|
-
const keys = ["endpoint", "refreshToken", "accessToken", "expiresAt", "sub", "email"];
|
|
24207
|
-
for (const key of keys) {
|
|
24208
|
-
if (typeof raw[key] !== "string" || raw[key] === "") return void 0;
|
|
24749
|
+
// src/cognito.ts
|
|
24750
|
+
var TARGET_PREFIX = "AWSCognitoIdentityProviderService.";
|
|
24751
|
+
var CONTENT_TYPE = "application/x-amz-json-1.1";
|
|
24752
|
+
var DEFAULT_EXPIRES_IN = 3600;
|
|
24753
|
+
var UNREACHABLE_LINE = "Couldn't reach the sign-in service.";
|
|
24754
|
+
var WRONG_CREDENTIALS_LINE = "Wrong email or password.";
|
|
24755
|
+
var TOO_MANY_LINE = "Too many attempts. Try again later.";
|
|
24756
|
+
var WRONG_CODE_LINE = "Wrong code.";
|
|
24757
|
+
var EXPIRED_CODE_LINE = "That code has expired. Request a new one.";
|
|
24758
|
+
var PASSWORD_POLICY_LINE = "Use at least 12 characters, with an uppercase letter, a lowercase letter, a number and a symbol.";
|
|
24759
|
+
var UNREACHABLE_TYPE = "Unreachable";
|
|
24760
|
+
var INCORRECT_CREDENTIALS_MESSAGE = "Incorrect username or password.";
|
|
24761
|
+
var CognitoError = class extends RemoteError {
|
|
24762
|
+
/** The `__type`, with any prefix up to a `#` dropped, or `Unreachable`. */
|
|
24763
|
+
type;
|
|
24764
|
+
/** Cognito's own `message`, or `undefined` when the answer carried none. */
|
|
24765
|
+
detail;
|
|
24766
|
+
constructor(type, detail2, line, suffix = "") {
|
|
24767
|
+
super(`${line}${suffix}`, { unanswered: type === UNREACHABLE_TYPE });
|
|
24768
|
+
this.type = type;
|
|
24769
|
+
this.detail = detail2;
|
|
24209
24770
|
}
|
|
24210
|
-
|
|
24211
|
-
|
|
24212
|
-
|
|
24213
|
-
accessToken: raw["accessToken"],
|
|
24214
|
-
expiresAt: raw["expiresAt"],
|
|
24215
|
-
sub: raw["sub"],
|
|
24216
|
-
email: raw["email"]
|
|
24217
|
-
};
|
|
24218
|
-
}
|
|
24219
|
-
function readCredentials(env) {
|
|
24220
|
-
const path = defaultCredentialsPath(env);
|
|
24221
|
-
let text;
|
|
24222
|
-
try {
|
|
24223
|
-
text = readFileSync(path, "utf8");
|
|
24224
|
-
} catch (error62) {
|
|
24225
|
-
if (isNotFound(error62)) return {};
|
|
24226
|
-
throw new ConfigError(`cannot read ${path}`, path, { cause: error62 });
|
|
24227
|
-
}
|
|
24228
|
-
let parsed;
|
|
24229
|
-
try {
|
|
24230
|
-
parsed = JSON.parse(text);
|
|
24231
|
-
} catch (error62) {
|
|
24232
|
-
throw new ConfigError(`invalid JSON in ${path}`, path, { cause: error62 });
|
|
24233
|
-
}
|
|
24234
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
24235
|
-
throw new ConfigError(`invalid credentials in ${path}: must be a JSON object`, path);
|
|
24236
|
-
}
|
|
24237
|
-
const entries = {};
|
|
24238
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
24239
|
-
const entry = readEntry(value);
|
|
24240
|
-
if (entry !== void 0) entries[key] = entry;
|
|
24241
|
-
}
|
|
24242
|
-
return entries;
|
|
24243
|
-
}
|
|
24244
|
-
function findCredential(env, endpoint) {
|
|
24245
|
-
return readCredentials(env)[normalizeEndpoint(endpoint)];
|
|
24246
|
-
}
|
|
24247
|
-
function writeCredentials(env, entries) {
|
|
24248
|
-
const path = defaultCredentialsPath(env);
|
|
24249
|
-
mkdirSync(dirname2(path), { recursive: true, mode: HOME_MODE });
|
|
24250
|
-
const temp = join2(
|
|
24251
|
-
dirname2(path),
|
|
24252
|
-
`.${CREDENTIALS_FILENAME}.${randomBytes(6).toString("hex")}`
|
|
24253
|
-
);
|
|
24254
|
-
try {
|
|
24255
|
-
writeFileSync(temp, `${JSON.stringify(entries, null, 2)}
|
|
24256
|
-
`, {
|
|
24257
|
-
encoding: "utf8",
|
|
24258
|
-
mode: CREDENTIALS_MODE
|
|
24259
|
-
});
|
|
24260
|
-
chmodSync(temp, CREDENTIALS_MODE);
|
|
24261
|
-
renameSync(temp, path);
|
|
24262
|
-
} catch (error62) {
|
|
24263
|
-
rmSync(temp, { force: true });
|
|
24264
|
-
throw error62;
|
|
24265
|
-
}
|
|
24266
|
-
}
|
|
24267
|
-
function saveCredential(env, credential) {
|
|
24268
|
-
const key = normalizeEndpoint(credential.endpoint);
|
|
24269
|
-
const stored = { ...credential, endpoint: key };
|
|
24270
|
-
writeCredentials(env, { ...readCredentials(env), [key]: stored });
|
|
24271
|
-
return stored;
|
|
24272
|
-
}
|
|
24273
|
-
function removeCredential(env, endpoint) {
|
|
24274
|
-
const entries = readCredentials(env);
|
|
24275
|
-
const key = normalizeEndpoint(endpoint);
|
|
24276
|
-
if (entries[key] === void 0) return false;
|
|
24277
|
-
delete entries[key];
|
|
24278
|
-
writeCredentials(env, entries);
|
|
24279
|
-
return true;
|
|
24280
|
-
}
|
|
24281
|
-
|
|
24282
|
-
// src/cognito.ts
|
|
24283
|
-
var TARGET_PREFIX = "AWSCognitoIdentityProviderService.";
|
|
24284
|
-
var CONTENT_TYPE = "application/x-amz-json-1.1";
|
|
24285
|
-
var DEFAULT_EXPIRES_IN = 3600;
|
|
24286
|
-
var UNREACHABLE_LINE = "The sign-in service could not be reached.";
|
|
24287
|
-
var WRONG_CREDENTIALS_LINE = "Wrong email or password.";
|
|
24288
|
-
var TOO_MANY_LINE = "Too many attempts, try again later.";
|
|
24289
|
-
var WRONG_CODE_LINE = "Wrong code.";
|
|
24290
|
-
var EXPIRED_CODE_LINE = "That code has expired, request a new one.";
|
|
24291
|
-
var PASSWORD_POLICY_LINE = "A password needs 12 characters with a lowercase letter, an uppercase letter, a digit and a symbol.";
|
|
24292
|
-
var UNREACHABLE_TYPE = "Unreachable";
|
|
24293
|
-
var INCORRECT_CREDENTIALS_MESSAGE = "Incorrect username or password.";
|
|
24294
|
-
var CognitoError = class extends RemoteError {
|
|
24295
|
-
/** The `__type`, with any prefix up to a `#` dropped, or `Unreachable`. */
|
|
24296
|
-
type;
|
|
24297
|
-
/** Cognito's own `message`, or `undefined` when the answer carried none. */
|
|
24298
|
-
detail;
|
|
24299
|
-
constructor(type, detail2, line, suffix = "") {
|
|
24300
|
-
super(`${line}${suffix}`);
|
|
24301
|
-
this.type = type;
|
|
24302
|
-
this.detail = detail2;
|
|
24303
|
-
}
|
|
24304
|
-
};
|
|
24305
|
-
function endpointUrl(region) {
|
|
24306
|
-
return `https://cognito-idp.${region}.amazonaws.com/`;
|
|
24771
|
+
};
|
|
24772
|
+
function endpointUrl(region) {
|
|
24773
|
+
return `https://cognito-idp.${region}.amazonaws.com/`;
|
|
24307
24774
|
}
|
|
24308
24775
|
function errorLineOf(type, detail2, signIn2) {
|
|
24309
24776
|
switch (type) {
|
|
@@ -24394,7 +24861,7 @@ function authAnswerOf(body) {
|
|
|
24394
24861
|
const shape = result;
|
|
24395
24862
|
const accessToken = shape["AccessToken"];
|
|
24396
24863
|
if (typeof accessToken !== "string" || accessToken === "") {
|
|
24397
|
-
throw new RemoteError("
|
|
24864
|
+
throw new RemoteError("sign-in didn't return an access token");
|
|
24398
24865
|
}
|
|
24399
24866
|
const refreshToken = shape["RefreshToken"];
|
|
24400
24867
|
const expiresIn = shape["ExpiresIn"];
|
|
@@ -24409,7 +24876,7 @@ function authAnswerOf(body) {
|
|
|
24409
24876
|
}
|
|
24410
24877
|
const challengeName = body["ChallengeName"];
|
|
24411
24878
|
if (typeof challengeName !== "string" || challengeName === "") {
|
|
24412
|
-
throw new RemoteError("
|
|
24879
|
+
throw new RemoteError("sign-in returned an unexpected response");
|
|
24413
24880
|
}
|
|
24414
24881
|
const session = body["Session"];
|
|
24415
24882
|
const parameters = body["ChallengeParameters"];
|
|
@@ -24478,9 +24945,8 @@ function actAsRefused(sub, endpoint) {
|
|
|
24478
24945
|
function clockOf(options) {
|
|
24479
24946
|
return options.now ?? (() => /* @__PURE__ */ new Date());
|
|
24480
24947
|
}
|
|
24481
|
-
var LOCAL_HOSTS = ["localhost", "127.0.0.1"];
|
|
24482
24948
|
async function fetchAuthConfig(endpoint, options = {}) {
|
|
24483
|
-
const url2 = `${
|
|
24949
|
+
const url2 = `${trimSlash2(endpoint)}${AUTH_CONFIG_PATH}`;
|
|
24484
24950
|
const site = { method: "GET", url: url2, debug: options.debug };
|
|
24485
24951
|
const response = await send(url2, { method: "GET", ...options });
|
|
24486
24952
|
const text = await readText(response, site);
|
|
@@ -24496,7 +24962,7 @@ async function fetchAuthConfig(endpoint, options = {}) {
|
|
|
24496
24962
|
const webUrl = stringOf(body, "webUrl");
|
|
24497
24963
|
if (region === void 0 || cliClientId === void 0 || webUrl === void 0) {
|
|
24498
24964
|
throw new RemoteError(
|
|
24499
|
-
`the API
|
|
24965
|
+
`unexpected response from the API: auth config has no region${detail(site, response.status)}`
|
|
24500
24966
|
);
|
|
24501
24967
|
}
|
|
24502
24968
|
return { mode: "cognito", region, cliClientId, webUrl };
|
|
@@ -24509,13 +24975,6 @@ function bearerAuthorizer(token) {
|
|
|
24509
24975
|
};
|
|
24510
24976
|
}
|
|
24511
24977
|
var DEV_TOKEN = "dev";
|
|
24512
|
-
function isLoopback(endpoint) {
|
|
24513
|
-
try {
|
|
24514
|
-
return LOCAL_HOSTS.includes(new URL(endpoint).hostname);
|
|
24515
|
-
} catch {
|
|
24516
|
-
return false;
|
|
24517
|
-
}
|
|
24518
|
-
}
|
|
24519
24978
|
var DevAuthorizer = class {
|
|
24520
24979
|
actAs;
|
|
24521
24980
|
#endpoint;
|
|
@@ -24601,7 +25060,9 @@ var SessionAuthorizer = class {
|
|
|
24601
25060
|
throw error62;
|
|
24602
25061
|
}
|
|
24603
25062
|
if (answer2.kind === "challenge") {
|
|
24604
|
-
throw new RemoteError(
|
|
25063
|
+
throw new RemoteError(
|
|
25064
|
+
`unexpected sign-in step during refresh (${answer2.challenge.challengeName})`
|
|
25065
|
+
);
|
|
24605
25066
|
}
|
|
24606
25067
|
const now = clockOf(this.#options)();
|
|
24607
25068
|
return saveCredential(this.#options.env, {
|
|
@@ -24618,12 +25079,97 @@ function createAuthorizer(endpoint, options) {
|
|
|
24618
25079
|
return new SessionAuthorizer(endpoint, options);
|
|
24619
25080
|
}
|
|
24620
25081
|
|
|
25082
|
+
// src/endpoint.ts
|
|
25083
|
+
var DISCOVERY_TIMEOUT_MS = 5e3;
|
|
25084
|
+
var SCHEMES = ["http:", "https:"];
|
|
25085
|
+
function parseEndpoint(value, from) {
|
|
25086
|
+
const text = value.trim();
|
|
25087
|
+
if (text.toLowerCase() === LOCAL_ENDPOINT) return { kind: "local" };
|
|
25088
|
+
const invalid = new ConfigError(
|
|
25089
|
+
`invalid endpoint "${text}" from ${from}: pass an https URL, a host, or local`
|
|
25090
|
+
);
|
|
25091
|
+
let url2 = text;
|
|
25092
|
+
if (!text.includes("://")) {
|
|
25093
|
+
url2 = isLoopbackUrl(`http://${text}`) ? `http://${text}` : `https://${text}`;
|
|
25094
|
+
}
|
|
25095
|
+
let parsed;
|
|
25096
|
+
try {
|
|
25097
|
+
parsed = new URL(url2);
|
|
25098
|
+
} catch {
|
|
25099
|
+
throw invalid;
|
|
25100
|
+
}
|
|
25101
|
+
if (!SCHEMES.includes(parsed.protocol) || parsed.hostname === "") throw invalid;
|
|
25102
|
+
return { kind: "url", url: normalizeEndpoint(url2) };
|
|
25103
|
+
}
|
|
25104
|
+
var discoveries = /* @__PURE__ */ new Map();
|
|
25105
|
+
function resetDiscovery() {
|
|
25106
|
+
discoveries.clear();
|
|
25107
|
+
}
|
|
25108
|
+
function discoverApiUrl(url2, options = {}) {
|
|
25109
|
+
const key = normalizeEndpoint(url2);
|
|
25110
|
+
if (isDefaultEndpoint(key)) return Promise.resolve(DEFAULT_ENDPOINT);
|
|
25111
|
+
const known = discoveries.get(key);
|
|
25112
|
+
if (known !== void 0) return known;
|
|
25113
|
+
const found = lookup(key, options).then((api) => {
|
|
25114
|
+
if (!discoveries.has(api)) discoveries.set(api, Promise.resolve(api));
|
|
25115
|
+
return api;
|
|
25116
|
+
});
|
|
25117
|
+
discoveries.set(key, found);
|
|
25118
|
+
return found;
|
|
25119
|
+
}
|
|
25120
|
+
async function lookup(url2, options) {
|
|
25121
|
+
const target = `${url2}${DISCOVERY_PATH}`;
|
|
25122
|
+
try {
|
|
25123
|
+
const response = await send(target, {
|
|
25124
|
+
method: "GET",
|
|
25125
|
+
headers: { accept: "application/json" },
|
|
25126
|
+
fetch: options.fetch,
|
|
25127
|
+
debug: options.debug,
|
|
25128
|
+
timeoutMs: options.timeoutMs ?? DISCOVERY_TIMEOUT_MS
|
|
25129
|
+
});
|
|
25130
|
+
const text = await readText(response, { method: "GET", url: target });
|
|
25131
|
+
if (!response.ok) return url2;
|
|
25132
|
+
const api = stringOf(asRecord(parseQuietly(text)), "api");
|
|
25133
|
+
if (api === void 0) return url2;
|
|
25134
|
+
const parsed = new URL(api.trim());
|
|
25135
|
+
if (!SCHEMES.includes(parsed.protocol)) return url2;
|
|
25136
|
+
return normalizeEndpoint(api);
|
|
25137
|
+
} catch {
|
|
25138
|
+
return url2;
|
|
25139
|
+
}
|
|
25140
|
+
}
|
|
25141
|
+
|
|
24621
25142
|
// src/config.ts
|
|
24622
|
-
var NO_ENDPOINT_MESSAGE = "no endpoint: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc";
|
|
24623
25143
|
function requireEndpoint(context) {
|
|
24624
|
-
if (context.endpoint === void 0)
|
|
25144
|
+
if (context.endpoint === void 0) {
|
|
25145
|
+
throw new ConfigError(`local mode has no endpoint: ${context.localFix ?? ""}`);
|
|
25146
|
+
}
|
|
24625
25147
|
return context.endpoint;
|
|
24626
25148
|
}
|
|
25149
|
+
async function resolveEndpoint(context, io) {
|
|
25150
|
+
const endpoint = requireEndpoint(context);
|
|
25151
|
+
if (context.discovered) return endpoint;
|
|
25152
|
+
const api = await discoverApiUrl(endpoint, {
|
|
25153
|
+
fetch: io.fetch,
|
|
25154
|
+
debug: context.debug,
|
|
25155
|
+
timeoutMs: context.timeoutMs
|
|
25156
|
+
});
|
|
25157
|
+
context.endpoint = api;
|
|
25158
|
+
context.credential = findCredential(io.env, api);
|
|
25159
|
+
context.discovered = true;
|
|
25160
|
+
return api;
|
|
25161
|
+
}
|
|
25162
|
+
function foreignOrgMessage(context) {
|
|
25163
|
+
const slug = context.slug;
|
|
25164
|
+
if (context.mode !== "local" || slug === void 0 || slug === LOCAL_ORG) {
|
|
25165
|
+
return void 0;
|
|
25166
|
+
}
|
|
25167
|
+
return `local mode holds the organization ${LOCAL_ORG} only, not ${slug}: ${context.localFix ?? ""} to file to ${slug} on a server, or pass --org ${LOCAL_ORG}`;
|
|
25168
|
+
}
|
|
25169
|
+
function refuseForeignOrg(context) {
|
|
25170
|
+
const message = foreignOrgMessage(context);
|
|
25171
|
+
if (message !== void 0) throw new ConfigError(message);
|
|
25172
|
+
}
|
|
24627
25173
|
var NO_ORG_MESSAGE = "no organization: pass --org <slug> or run `quibble init --org <slug>`";
|
|
24628
25174
|
function requireOrg(context) {
|
|
24629
25175
|
if (context.org === void 0) throw new ConfigError(NO_ORG_MESSAGE);
|
|
@@ -24747,32 +25293,91 @@ function firstValue(...values) {
|
|
|
24747
25293
|
}
|
|
24748
25294
|
return void 0;
|
|
24749
25295
|
}
|
|
25296
|
+
function localFixOf(candidate) {
|
|
25297
|
+
switch (candidate.source) {
|
|
25298
|
+
case "--endpoint":
|
|
25299
|
+
return "drop --endpoint local";
|
|
25300
|
+
case "QUIBBLE_ENDPOINT":
|
|
25301
|
+
return "unset QUIBBLE_ENDPOINT";
|
|
25302
|
+
default:
|
|
25303
|
+
return `remove "endpoint": "local" from ${candidate.path ?? ""}`;
|
|
25304
|
+
}
|
|
25305
|
+
}
|
|
25306
|
+
function resolveEndpointSource(options, project, user) {
|
|
25307
|
+
if (options.local === true) {
|
|
25308
|
+
return {
|
|
25309
|
+
mode: "local",
|
|
25310
|
+
endpoint: void 0,
|
|
25311
|
+
endpointSource: "--local",
|
|
25312
|
+
localFix: "drop --local"
|
|
25313
|
+
};
|
|
25314
|
+
}
|
|
25315
|
+
const candidates2 = [
|
|
25316
|
+
{ source: "--endpoint", value: options.endpoint },
|
|
25317
|
+
{ source: "QUIBBLE_ENDPOINT", value: options.env["QUIBBLE_ENDPOINT"] },
|
|
25318
|
+
{ source: ".quibblerc", value: project?.endpoint, path: project?.path },
|
|
25319
|
+
{
|
|
25320
|
+
source: "config.json",
|
|
25321
|
+
value: user.endpoint,
|
|
25322
|
+
path: defaultConfigPath(options.env)
|
|
25323
|
+
}
|
|
25324
|
+
];
|
|
25325
|
+
const chosen = candidates2.find(
|
|
25326
|
+
(candidate) => candidate.value !== void 0 && candidate.value.trim() !== ""
|
|
25327
|
+
);
|
|
25328
|
+
if (chosen === void 0) {
|
|
25329
|
+
return {
|
|
25330
|
+
mode: "remote",
|
|
25331
|
+
endpoint: DEFAULT_ENDPOINT,
|
|
25332
|
+
endpointSource: "default",
|
|
25333
|
+
localFix: void 0
|
|
25334
|
+
};
|
|
25335
|
+
}
|
|
25336
|
+
const value = parseEndpoint(chosen.value ?? "", chosen.path ?? chosen.source);
|
|
25337
|
+
if (value.kind === "local") {
|
|
25338
|
+
return {
|
|
25339
|
+
mode: "local",
|
|
25340
|
+
endpoint: void 0,
|
|
25341
|
+
endpointSource: chosen.source,
|
|
25342
|
+
localFix: localFixOf(chosen)
|
|
25343
|
+
};
|
|
25344
|
+
}
|
|
25345
|
+
return {
|
|
25346
|
+
mode: "remote",
|
|
25347
|
+
endpoint: value.url,
|
|
25348
|
+
endpointSource: chosen.source,
|
|
25349
|
+
localFix: void 0
|
|
25350
|
+
};
|
|
25351
|
+
}
|
|
24750
25352
|
function resolveContext(options) {
|
|
24751
25353
|
const project = findProjectConfig(options.cwd);
|
|
24752
25354
|
const user = loadUserConfig(options.env);
|
|
24753
|
-
const endpoint =
|
|
24754
|
-
options
|
|
24755
|
-
|
|
24756
|
-
|
|
24757
|
-
user.endpoint
|
|
25355
|
+
const { mode, endpoint, endpointSource, localFix } = resolveEndpointSource(
|
|
25356
|
+
options,
|
|
25357
|
+
project,
|
|
25358
|
+
user
|
|
24758
25359
|
);
|
|
24759
|
-
const
|
|
24760
|
-
const
|
|
24761
|
-
const
|
|
25360
|
+
const given = firstValue(options.project, project?.project);
|
|
25361
|
+
const name = given === void 0 ? void 0 : foldProjectName(given);
|
|
25362
|
+
const org = firstValue(options.org, project?.org);
|
|
25363
|
+
const slug = org === void 0 ? void 0 : parseOrgSlug(org);
|
|
24762
25364
|
const token = firstValue(options.env[TOKEN_ENV]);
|
|
24763
|
-
const
|
|
25365
|
+
const discovered = endpoint !== void 0 && isDefaultEndpoint(endpoint);
|
|
24764
25366
|
return {
|
|
24765
25367
|
project: name,
|
|
24766
25368
|
org: mode === "local" ? LOCAL_ORG : slug,
|
|
25369
|
+
slug,
|
|
24767
25370
|
projectRoot: project?.root,
|
|
24768
25371
|
configPath: project?.path,
|
|
24769
25372
|
cwd: project === void 0 ? void 0 : relativeCwd(project.root, options.cwd),
|
|
24770
25373
|
mode,
|
|
24771
25374
|
endpoint,
|
|
25375
|
+
endpointSource,
|
|
25376
|
+
localFix,
|
|
25377
|
+
discovered,
|
|
25378
|
+
timeoutMs: void 0,
|
|
24772
25379
|
token,
|
|
24773
|
-
|
|
24774
|
-
// and no command has to read the file to describe the session.
|
|
24775
|
-
credential: endpoint === void 0 ? void 0 : findCredential(options.env, endpoint),
|
|
25380
|
+
credential: discovered && endpoint !== void 0 ? findCredential(options.env, endpoint) : void 0,
|
|
24776
25381
|
actAs: firstValue(options.env[ACT_AS_ENV]),
|
|
24777
25382
|
debug: debugEnabled(options.env),
|
|
24778
25383
|
agent: firstValue(options.agent, user.agent) ?? DEFAULT_AGENT,
|
|
@@ -24785,6 +25390,88 @@ function resolveContext(options) {
|
|
|
24785
25390
|
projectConfig: project
|
|
24786
25391
|
};
|
|
24787
25392
|
}
|
|
25393
|
+
function rememberEndpoint(env, endpoint) {
|
|
25394
|
+
const path = defaultConfigPath(env);
|
|
25395
|
+
const raw = readJsonFile(path) ?? {};
|
|
25396
|
+
if (stringField(raw, "endpoint", path) !== void 0) return void 0;
|
|
25397
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
25398
|
+
writeFileSync2(path, `${JSON.stringify({ ...raw, endpoint }, null, 2)}
|
|
25399
|
+
`, "utf8");
|
|
25400
|
+
return path;
|
|
25401
|
+
}
|
|
25402
|
+
|
|
25403
|
+
// src/ids.ts
|
|
25404
|
+
var FULL_ID_LENGTH = 36;
|
|
25405
|
+
var SHORT_ID_MIN = 6;
|
|
25406
|
+
var CANDIDATES_SHOWN = 5;
|
|
25407
|
+
var SHORT_ID_RE = /^[0-9a-f-]+$/;
|
|
25408
|
+
function readIdValue(value) {
|
|
25409
|
+
const trimmed = value.trim();
|
|
25410
|
+
if (trimmed.length === FULL_ID_LENGTH) return { id: trimmed, prefix: false };
|
|
25411
|
+
const lower = trimmed.toLowerCase();
|
|
25412
|
+
if (!SHORT_ID_RE.test(lower)) throw new UsageError(`${value} is not an id`);
|
|
25413
|
+
if (lower.length < SHORT_ID_MIN) {
|
|
25414
|
+
throw new UsageError(
|
|
25415
|
+
`${value} is too short: pass at least ${SHORT_ID_MIN} characters of the id`
|
|
25416
|
+
);
|
|
25417
|
+
}
|
|
25418
|
+
return { id: lower, prefix: true };
|
|
25419
|
+
}
|
|
25420
|
+
function ambiguousError(prefix, ids, noun) {
|
|
25421
|
+
const shown = ids.slice(0, CANDIDATES_SHOWN).join(", ");
|
|
25422
|
+
const more = ids.length > CANDIDATES_SHOWN ? ` and ${ids.length - CANDIDATES_SHOWN} more` : "";
|
|
25423
|
+
return new UsageError(`${prefix} matches ${ids.length} ${noun}: ${shown}${more}`, {
|
|
25424
|
+
code: AMBIGUOUS_ID
|
|
25425
|
+
});
|
|
25426
|
+
}
|
|
25427
|
+
async function candidates(source) {
|
|
25428
|
+
const org = requireOrg(source.context);
|
|
25429
|
+
const ids = [];
|
|
25430
|
+
if (source.local !== void 0) {
|
|
25431
|
+
for (const status of [void 0, "deleted"]) {
|
|
25432
|
+
const filter = { org, ...status === void 0 ? {} : { status } };
|
|
25433
|
+
for await (const complaint of scanComplaints(source.local, filter)) {
|
|
25434
|
+
ids.push(complaint.id);
|
|
25435
|
+
}
|
|
25436
|
+
}
|
|
25437
|
+
return ids;
|
|
25438
|
+
}
|
|
25439
|
+
const project = source.context.projectConfig?.project;
|
|
25440
|
+
for (const status of [void 0, "deleted"]) {
|
|
25441
|
+
const filter = {
|
|
25442
|
+
org,
|
|
25443
|
+
limit: MAX_LIMIT,
|
|
25444
|
+
...project === void 0 ? {} : { project },
|
|
25445
|
+
...status === void 0 ? {} : { status }
|
|
25446
|
+
};
|
|
25447
|
+
for (const complaint of await listComplaints(source.store, filter)) {
|
|
25448
|
+
ids.push(complaint.id);
|
|
25449
|
+
}
|
|
25450
|
+
}
|
|
25451
|
+
return ids;
|
|
25452
|
+
}
|
|
25453
|
+
async function resolveIds(values, source) {
|
|
25454
|
+
const read = values.map((value) => readIdValue(value));
|
|
25455
|
+
let pool;
|
|
25456
|
+
const resolved = [];
|
|
25457
|
+
for (const value of read) {
|
|
25458
|
+
if (!value.prefix) {
|
|
25459
|
+
resolved.push(value.id);
|
|
25460
|
+
continue;
|
|
25461
|
+
}
|
|
25462
|
+
pool ??= candidates(source);
|
|
25463
|
+
const matches = [...new Set((await pool).filter((id) => id.startsWith(value.id)))];
|
|
25464
|
+
if (matches.length === 1) {
|
|
25465
|
+
resolved.push(matches[0]);
|
|
25466
|
+
continue;
|
|
25467
|
+
}
|
|
25468
|
+
if (matches.length > 1) throw ambiguousError(value.id, matches, "complaints");
|
|
25469
|
+
throw new NotFoundError(
|
|
25470
|
+
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}`
|
|
25471
|
+
);
|
|
25472
|
+
}
|
|
25473
|
+
return resolved;
|
|
25474
|
+
}
|
|
24788
25475
|
|
|
24789
25476
|
// src/io.ts
|
|
24790
25477
|
async function readStdin(stream) {
|
|
@@ -24803,9 +25490,10 @@ function writeJson(stream, value) {
|
|
|
24803
25490
|
}
|
|
24804
25491
|
|
|
24805
25492
|
// src/client.ts
|
|
24806
|
-
|
|
25493
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
25494
|
+
var NOT_A_COMPLAINT = "unexpected response from the API: expected a complaint";
|
|
24807
25495
|
var ORG_NOT_FOUND = "org_not_found";
|
|
24808
|
-
var NOT_A_PROMOTION = "
|
|
25496
|
+
var NOT_A_PROMOTION = "unexpected response from the API: expected a tag promotion";
|
|
24809
25497
|
function readRoles(value) {
|
|
24810
25498
|
if (!Array.isArray(value)) return [];
|
|
24811
25499
|
const roles = [];
|
|
@@ -24838,7 +25526,7 @@ function parseMe(body) {
|
|
|
24838
25526
|
const record2 = asRecord(body);
|
|
24839
25527
|
const reporter = stringOf(record2, "reporter");
|
|
24840
25528
|
if (record2 === void 0 || reporter === void 0) {
|
|
24841
|
-
throw new RemoteError("the API
|
|
25529
|
+
throw new RemoteError("unexpected response from the API: identity has no reporter");
|
|
24842
25530
|
}
|
|
24843
25531
|
return {
|
|
24844
25532
|
kind: stringOf(record2, "kind") ?? "user",
|
|
@@ -24857,7 +25545,7 @@ function parseMe(body) {
|
|
|
24857
25545
|
function parseOrgAnswer(body, slug) {
|
|
24858
25546
|
const record2 = asRecord(body);
|
|
24859
25547
|
if (record2 === void 0) {
|
|
24860
|
-
throw new RemoteError("
|
|
25548
|
+
throw new RemoteError("unexpected response from the API: expected an organization");
|
|
24861
25549
|
}
|
|
24862
25550
|
return {
|
|
24863
25551
|
org: stringOf(record2, "org") ?? slug,
|
|
@@ -24867,6 +25555,35 @@ function parseOrgAnswer(body, slug) {
|
|
|
24867
25555
|
body: record2
|
|
24868
25556
|
};
|
|
24869
25557
|
}
|
|
25558
|
+
function readToken(value) {
|
|
25559
|
+
const record2 = asRecord(value);
|
|
25560
|
+
const id = stringOf(record2, "id");
|
|
25561
|
+
const name = stringOf(record2, "name");
|
|
25562
|
+
const createdAt = stringOf(record2, "createdAt");
|
|
25563
|
+
if (id === void 0 || name === void 0 || createdAt === void 0) {
|
|
25564
|
+
throw new RemoteError("unexpected response from the API: expected a token");
|
|
25565
|
+
}
|
|
25566
|
+
return {
|
|
25567
|
+
id,
|
|
25568
|
+
name,
|
|
25569
|
+
createdAt,
|
|
25570
|
+
expiresAt: stringOf(record2, "expiresAt"),
|
|
25571
|
+
lastUsedAt: stringOf(record2, "lastUsedAt"),
|
|
25572
|
+
revokedAt: stringOf(record2, "revokedAt")
|
|
25573
|
+
};
|
|
25574
|
+
}
|
|
25575
|
+
function readProject(body, name) {
|
|
25576
|
+
const record2 = asRecord(body);
|
|
25577
|
+
if (record2 === void 0) {
|
|
25578
|
+
throw new RemoteError("unexpected response from the API: expected a project");
|
|
25579
|
+
}
|
|
25580
|
+
const tags = record2["tags"];
|
|
25581
|
+
return {
|
|
25582
|
+
project: stringOf(record2, "project") ?? name,
|
|
25583
|
+
tags: Array.isArray(tags) ? tags.map((tag) => String(tag)) : void 0,
|
|
25584
|
+
body: record2
|
|
25585
|
+
};
|
|
25586
|
+
}
|
|
24870
25587
|
function readComplaint(body) {
|
|
24871
25588
|
try {
|
|
24872
25589
|
return parseComplaint(body);
|
|
@@ -24881,24 +25598,37 @@ var ApiClient = class {
|
|
|
24881
25598
|
#base;
|
|
24882
25599
|
#auth;
|
|
24883
25600
|
#http;
|
|
25601
|
+
#beforeFirst;
|
|
24884
25602
|
constructor(options) {
|
|
24885
25603
|
this.endpoint = options.endpoint;
|
|
24886
25604
|
this.org = options.org;
|
|
24887
|
-
this.#base =
|
|
25605
|
+
this.#base = trimSlash2(options.endpoint);
|
|
24888
25606
|
this.#auth = options.authorizer;
|
|
25607
|
+
this.#beforeFirst = options.beforeFirst;
|
|
24889
25608
|
this.#http = {
|
|
24890
25609
|
fetch: options.fetch,
|
|
24891
25610
|
timeoutMs: options.timeoutMs,
|
|
24892
25611
|
debug: options.debug
|
|
24893
25612
|
};
|
|
24894
25613
|
}
|
|
24895
|
-
/**
|
|
24896
|
-
|
|
24897
|
-
|
|
25614
|
+
/**
|
|
25615
|
+
* Sends one request and returns its body. A non-2xx answer is an error.
|
|
25616
|
+
* `headers` are sent beside the auth headers, such as the idempotency key
|
|
25617
|
+
* of a filing.
|
|
25618
|
+
*/
|
|
25619
|
+
async request(method, path, body, headers) {
|
|
25620
|
+
return (await this.#call(method, path, body, false, headers)).body;
|
|
25621
|
+
}
|
|
25622
|
+
/**
|
|
25623
|
+
* The same, and the headers of the answer, which carry what the body does
|
|
25624
|
+
* not, such as the truncation mark of a list.
|
|
25625
|
+
*/
|
|
25626
|
+
async requestWithHeaders(method, path) {
|
|
25627
|
+
return this.#call(method, path, void 0, false);
|
|
24898
25628
|
}
|
|
24899
25629
|
/** The same, except that a `404` answers `undefined` instead of failing. */
|
|
24900
25630
|
async requestOptional(method, path, body) {
|
|
24901
|
-
return this.#call(method, path, body, true);
|
|
25631
|
+
return (await this.#call(method, path, body, true)).body;
|
|
24902
25632
|
}
|
|
24903
25633
|
/**
|
|
24904
25634
|
* The prefix of every org-scoped path. Every command that reaches one
|
|
@@ -24917,7 +25647,9 @@ var ApiClient = class {
|
|
|
24917
25647
|
async orgs() {
|
|
24918
25648
|
const body = asRecord(await this.request("GET", "/v1/orgs"));
|
|
24919
25649
|
if (body === void 0) {
|
|
24920
|
-
throw new RemoteError(
|
|
25650
|
+
throw new RemoteError(
|
|
25651
|
+
"unexpected response from the API: expected an organization list"
|
|
25652
|
+
);
|
|
24921
25653
|
}
|
|
24922
25654
|
return { orgs: readOrgs(body["orgs"]), body };
|
|
24923
25655
|
}
|
|
@@ -24926,16 +25658,62 @@ var ApiClient = class {
|
|
|
24926
25658
|
const slug = this.org ?? "";
|
|
24927
25659
|
return parseOrgAnswer(await this.request("GET", this.orgPath()), slug);
|
|
24928
25660
|
}
|
|
25661
|
+
/** The path of one project of the organization. */
|
|
25662
|
+
projectPath(name) {
|
|
25663
|
+
return `${this.orgPath()}/projects/${encodeURIComponent(name)}`;
|
|
25664
|
+
}
|
|
24929
25665
|
/** Reads the project record, which owns the allowed tag list in remote mode. */
|
|
24930
25666
|
async project(name) {
|
|
24931
|
-
|
|
24932
|
-
|
|
25667
|
+
return readProject(await this.request("GET", this.projectPath(name)), name);
|
|
25668
|
+
}
|
|
25669
|
+
/**
|
|
25670
|
+
* Replaces the allowed tag list of a project, or clears it with `null`. The
|
|
25671
|
+
* route retags the stored complaints and answers how many it rewrote.
|
|
25672
|
+
*/
|
|
25673
|
+
async setProjectTags(name, tags) {
|
|
25674
|
+
const body = await this.request(
|
|
25675
|
+
"PATCH",
|
|
25676
|
+
this.projectPath(name),
|
|
25677
|
+
JSON.stringify({ tags })
|
|
25678
|
+
);
|
|
25679
|
+
const answer2 = readProject(body, name);
|
|
25680
|
+
const retagged = answer2.body["retagged"];
|
|
25681
|
+
return { ...answer2, retagged: typeof retagged === "number" ? retagged : 0 };
|
|
25682
|
+
}
|
|
25683
|
+
/**
|
|
25684
|
+
* Soft-deletes a project (section 7.5). The route hides it and its
|
|
25685
|
+
* complaints everywhere and revokes its tokens, and answers `204`.
|
|
25686
|
+
*/
|
|
25687
|
+
async deleteProject(name) {
|
|
25688
|
+
await this.request("DELETE", this.projectPath(name));
|
|
25689
|
+
}
|
|
25690
|
+
/** Lists the tokens of a project, newest first, as the route sorts them. */
|
|
25691
|
+
async tokens(project) {
|
|
25692
|
+
const body = await this.request("GET", `${this.projectPath(project)}/tokens`);
|
|
25693
|
+
if (!Array.isArray(body)) {
|
|
25694
|
+
throw new RemoteError("unexpected response from the API: expected a token list");
|
|
25695
|
+
}
|
|
25696
|
+
return { tokens: body.map((entry) => readToken(entry)), body };
|
|
25697
|
+
}
|
|
25698
|
+
/** Mints a token. The answer carries the plaintext, once. */
|
|
25699
|
+
async createToken(project, input2) {
|
|
25700
|
+
const body = await this.request(
|
|
25701
|
+
"POST",
|
|
25702
|
+
`${this.projectPath(project)}/tokens`,
|
|
25703
|
+
JSON.stringify(input2)
|
|
25704
|
+
);
|
|
25705
|
+
const plaintext = stringOf(asRecord(body), "plaintext");
|
|
25706
|
+
if (plaintext === void 0) {
|
|
25707
|
+
throw new RemoteError("unexpected response from the API: token has no plaintext");
|
|
25708
|
+
}
|
|
25709
|
+
return { token: readToken(body), plaintext, body };
|
|
25710
|
+
}
|
|
25711
|
+
/** Revokes one token. A repeat answers `204` again. */
|
|
25712
|
+
async revokeToken(project, id) {
|
|
25713
|
+
await this.request(
|
|
25714
|
+
"DELETE",
|
|
25715
|
+
`${this.projectPath(project)}/tokens/${encodeURIComponent(id)}`
|
|
24933
25716
|
);
|
|
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
25717
|
}
|
|
24940
25718
|
/**
|
|
24941
25719
|
* Promotes one suggestion to a real tag. The route needs `maintainer`, so a
|
|
@@ -24956,15 +25734,36 @@ var ApiClient = class {
|
|
|
24956
25734
|
}
|
|
24957
25735
|
return { tags: tags.map((entry) => String(entry)), retagged };
|
|
24958
25736
|
}
|
|
24959
|
-
|
|
25737
|
+
/**
|
|
25738
|
+
* Sends one request with the auth rules of section 5.7 and returns the
|
|
25739
|
+
* answer whatever its status. It never runs `beforeFirst`, so the offline
|
|
25740
|
+
* queue sends through it. A request that gets no answer still throws.
|
|
25741
|
+
*/
|
|
25742
|
+
async exchange(method, path, body, headers) {
|
|
25743
|
+
const { response, text } = await this.#exchange(method, path, body, headers);
|
|
25744
|
+
return { status: response.status, text };
|
|
25745
|
+
}
|
|
25746
|
+
async #call(method, path, body, notFoundIsUndefined, headers) {
|
|
25747
|
+
const first = this.#beforeFirst;
|
|
25748
|
+
this.#beforeFirst = void 0;
|
|
25749
|
+
if (first !== void 0) await first(this);
|
|
25750
|
+
const { response, text, site } = await this.#exchange(method, path, body, headers);
|
|
25751
|
+
if (response.status === 404 && notFoundIsUndefined && serverCode(text) !== ORG_NOT_FOUND) {
|
|
25752
|
+
return { body: void 0, headers: response.headers };
|
|
25753
|
+
}
|
|
25754
|
+
if (!response.ok) throw remoteFailure(site, response, text);
|
|
25755
|
+
if (response.status === 204) return { body: void 0, headers: response.headers };
|
|
25756
|
+
return { body: parseJson(text, site, response.status), headers: response.headers };
|
|
25757
|
+
}
|
|
25758
|
+
async #exchange(method, path, body, extra) {
|
|
24960
25759
|
const url2 = `${this.#base}${path}`;
|
|
24961
25760
|
const site = { method, url: url2, debug: this.#http.debug };
|
|
24962
|
-
let response = await this.#send(url2, method, await this.#auth.token(), body);
|
|
25761
|
+
let response = await this.#send(url2, method, await this.#auth.token(), body, extra);
|
|
24963
25762
|
if (response.status === 401) {
|
|
24964
25763
|
const fresh = await this.#auth.retry();
|
|
24965
25764
|
if (fresh !== void 0) {
|
|
24966
25765
|
await readText(response, site);
|
|
24967
|
-
response = await this.#send(url2, method, fresh, body);
|
|
25766
|
+
response = await this.#send(url2, method, fresh, body, extra);
|
|
24968
25767
|
if (response.status === 401) throw sessionExpired(this.endpoint);
|
|
24969
25768
|
} else if (this.#auth.actAs !== void 0) {
|
|
24970
25769
|
await readText(response, site);
|
|
@@ -24972,14 +25771,13 @@ var ApiClient = class {
|
|
|
24972
25771
|
}
|
|
24973
25772
|
}
|
|
24974
25773
|
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);
|
|
25774
|
+
return { response, text, site };
|
|
24980
25775
|
}
|
|
24981
|
-
#send(url2, method, bearer, body) {
|
|
24982
|
-
const headers = {
|
|
25776
|
+
#send(url2, method, bearer, body, extra = {}) {
|
|
25777
|
+
const headers = {
|
|
25778
|
+
...extra,
|
|
25779
|
+
authorization: `Bearer ${bearer}`
|
|
25780
|
+
};
|
|
24983
25781
|
const actAs = this.#auth.actAs;
|
|
24984
25782
|
if (actAs !== void 0) headers[ACT_AS_HEADER] = actAs;
|
|
24985
25783
|
if (body !== void 0) headers["content-type"] = "application/json";
|
|
@@ -24991,6 +25789,67 @@ var ApiClient = class {
|
|
|
24991
25789
|
});
|
|
24992
25790
|
}
|
|
24993
25791
|
};
|
|
25792
|
+
var UndeliveredError = class extends RemoteError {
|
|
25793
|
+
body;
|
|
25794
|
+
reason;
|
|
25795
|
+
/**
|
|
25796
|
+
* The idempotency key the request carried, which the queued entry keeps,
|
|
25797
|
+
* so a request that did reach the server files nothing new when it is sent
|
|
25798
|
+
* again.
|
|
25799
|
+
*/
|
|
25800
|
+
key;
|
|
25801
|
+
constructor(body, reason, key, cause) {
|
|
25802
|
+
super(cause.message, { cause, status: cause.status, unanswered: cause.unanswered });
|
|
25803
|
+
this.body = body;
|
|
25804
|
+
this.reason = reason;
|
|
25805
|
+
this.key = key;
|
|
25806
|
+
}
|
|
25807
|
+
};
|
|
25808
|
+
function retryableReason(error62) {
|
|
25809
|
+
if (!(error62 instanceof RemoteError)) return void 0;
|
|
25810
|
+
if (error62.unanswered) return "the API is unreachable";
|
|
25811
|
+
const status = error62.status;
|
|
25812
|
+
if (status === void 0) return void 0;
|
|
25813
|
+
if (status === 429) return error62.serverMessage ?? `the API answered ${status}`;
|
|
25814
|
+
if (status >= 500) return `the API answered ${status}`;
|
|
25815
|
+
return void 0;
|
|
25816
|
+
}
|
|
25817
|
+
function complaintBody(complaint) {
|
|
25818
|
+
return {
|
|
25819
|
+
project: complaint.project,
|
|
25820
|
+
message: complaint.message,
|
|
25821
|
+
severity: complaint.severity,
|
|
25822
|
+
tags: complaint.tags,
|
|
25823
|
+
agent: complaint.agent,
|
|
25824
|
+
...complaint.cwd === void 0 ? {} : { cwd: complaint.cwd },
|
|
25825
|
+
...complaint.cmd === void 0 ? {} : { cmd: complaint.cmd }
|
|
25826
|
+
};
|
|
25827
|
+
}
|
|
25828
|
+
function readBulkResults(body) {
|
|
25829
|
+
const results = asRecord(body)?.["results"];
|
|
25830
|
+
if (!Array.isArray(results)) {
|
|
25831
|
+
throw new RemoteError("unexpected response from the API: expected bulk results");
|
|
25832
|
+
}
|
|
25833
|
+
return results.map((entry) => {
|
|
25834
|
+
const record2 = asRecord(entry);
|
|
25835
|
+
const id = stringOf(record2, "id");
|
|
25836
|
+
if (record2 === void 0 || id === void 0) {
|
|
25837
|
+
throw new RemoteError("unexpected response from the API: expected a bulk result");
|
|
25838
|
+
}
|
|
25839
|
+
if (record2["ok"] === true) {
|
|
25840
|
+
return { id, ok: true, complaint: readComplaint(record2["complaint"]) };
|
|
25841
|
+
}
|
|
25842
|
+
const error62 = asRecord(record2["error"]);
|
|
25843
|
+
return {
|
|
25844
|
+
id,
|
|
25845
|
+
ok: false,
|
|
25846
|
+
error: {
|
|
25847
|
+
code: stringOf(error62, "code") ?? "error",
|
|
25848
|
+
message: stringOf(error62, "message") ?? "the change failed"
|
|
25849
|
+
}
|
|
25850
|
+
};
|
|
25851
|
+
});
|
|
25852
|
+
}
|
|
24994
25853
|
function adopt(target, source) {
|
|
24995
25854
|
for (const key of Object.keys(target)) {
|
|
24996
25855
|
delete target[key];
|
|
@@ -25007,39 +25866,55 @@ var RemoteStore = class {
|
|
|
25007
25866
|
return `${this.#client.orgPath()}/complaints${suffix}`;
|
|
25008
25867
|
}
|
|
25009
25868
|
/**
|
|
25010
|
-
* Posts the complaint. The API assigns the stored `id
|
|
25011
|
-
* the validated record of the response
|
|
25869
|
+
* Posts the complaint. The API assigns the stored `id`, `createdAt`,
|
|
25870
|
+
* `fingerprint` and `recurred`, so the validated record of the response
|
|
25871
|
+
* replaces the record passed in, and the answer's match is returned.
|
|
25012
25872
|
*/
|
|
25013
25873
|
async create(complaint) {
|
|
25014
|
-
const
|
|
25015
|
-
|
|
25016
|
-
|
|
25017
|
-
|
|
25018
|
-
|
|
25019
|
-
|
|
25020
|
-
|
|
25021
|
-
}
|
|
25022
|
-
|
|
25874
|
+
const input2 = complaintBody(complaint);
|
|
25875
|
+
const key = randomUUID2();
|
|
25876
|
+
let body;
|
|
25877
|
+
try {
|
|
25878
|
+
body = await this.#client.request("POST", this.#path(), JSON.stringify(input2), {
|
|
25879
|
+
[IDEMPOTENCY_HEADER]: key
|
|
25880
|
+
});
|
|
25881
|
+
} catch (error62) {
|
|
25882
|
+
const reason = retryableReason(error62);
|
|
25883
|
+
if (reason !== void 0 && error62 instanceof RemoteError) {
|
|
25884
|
+
throw new UndeliveredError(input2, reason, key, error62);
|
|
25885
|
+
}
|
|
25886
|
+
throw error62;
|
|
25887
|
+
}
|
|
25023
25888
|
adopt(complaint, readComplaint(body));
|
|
25889
|
+
const answer2 = asRecord(body);
|
|
25890
|
+
const openMatches = answer2?.["openMatches"];
|
|
25891
|
+
return {
|
|
25892
|
+
open: typeof openMatches === "number" ? openMatches : 0,
|
|
25893
|
+
resolved: answer2?.["recurred"] === true
|
|
25894
|
+
};
|
|
25024
25895
|
}
|
|
25025
25896
|
/** Returns the matching complaints, newest first. */
|
|
25026
25897
|
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();
|
|
25898
|
+
return (await this.listPage(filter)).complaints;
|
|
25899
|
+
}
|
|
25900
|
+
/**
|
|
25901
|
+
* Returns the matching complaints, newest first, and whether the server
|
|
25902
|
+
* stopped at its read budget, which the answer marks with a header.
|
|
25903
|
+
*/
|
|
25904
|
+
async listPage(filter) {
|
|
25905
|
+
const search = new URLSearchParams(listQueryOf(filter)).toString();
|
|
25037
25906
|
const suffix = search === "" ? "" : `?${search}`;
|
|
25038
|
-
const body = await this.#client.
|
|
25907
|
+
const { body, headers } = await this.#client.requestWithHeaders(
|
|
25908
|
+
"GET",
|
|
25909
|
+
this.#path(suffix)
|
|
25910
|
+
);
|
|
25039
25911
|
if (!Array.isArray(body)) {
|
|
25040
|
-
throw new RemoteError("the API
|
|
25912
|
+
throw new RemoteError("unexpected response from the API: expected a list");
|
|
25041
25913
|
}
|
|
25042
|
-
return
|
|
25914
|
+
return {
|
|
25915
|
+
complaints: body.map((item) => readComplaint(item)),
|
|
25916
|
+
truncated: headers.get(TRUNCATED_HEADER) === "1"
|
|
25917
|
+
};
|
|
25043
25918
|
}
|
|
25044
25919
|
/** Returns the complaint with that id, or `undefined` when the API says 404. */
|
|
25045
25920
|
async get(id) {
|
|
@@ -25059,11 +25934,26 @@ var RemoteStore = class {
|
|
|
25059
25934
|
this.#path(`/${encodeURIComponent(id)}`),
|
|
25060
25935
|
JSON.stringify({
|
|
25061
25936
|
status: change.status,
|
|
25062
|
-
...change.resolution === void 0 ? {} : { resolution: change.resolution }
|
|
25937
|
+
...change.resolution === void 0 ? {} : { resolution: change.resolution },
|
|
25938
|
+
...change.reason === void 0 ? {} : { reason: change.reason },
|
|
25939
|
+
...change.duplicateOf === void 0 ? {} : { duplicateOf: change.duplicateOf }
|
|
25063
25940
|
})
|
|
25064
25941
|
);
|
|
25065
25942
|
return body === void 0 ? void 0 : readComplaint(body);
|
|
25066
25943
|
}
|
|
25944
|
+
/**
|
|
25945
|
+
* Applies one status change to many complaints with the bulk route, in one
|
|
25946
|
+
* request. A per-id `not_found` reads as an absent record, exactly like the
|
|
25947
|
+
* `404` of one id, so its result carries no complaint and the code.
|
|
25948
|
+
*/
|
|
25949
|
+
async bulkStatus(ids, change) {
|
|
25950
|
+
const body = await this.#client.request(
|
|
25951
|
+
"POST",
|
|
25952
|
+
this.#path("/bulk"),
|
|
25953
|
+
JSON.stringify({ ids, ...change })
|
|
25954
|
+
);
|
|
25955
|
+
return readBulkResults(body);
|
|
25956
|
+
}
|
|
25067
25957
|
/**
|
|
25068
25958
|
* Replaces the tags. The API stamps the change with its own time, so the
|
|
25069
25959
|
* request carries the list only.
|
|
@@ -25078,7 +25968,375 @@ var RemoteStore = class {
|
|
|
25078
25968
|
}
|
|
25079
25969
|
};
|
|
25080
25970
|
|
|
25971
|
+
// src/queue.ts
|
|
25972
|
+
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as randomUUID3 } from "node:crypto";
|
|
25973
|
+
import {
|
|
25974
|
+
appendFileSync,
|
|
25975
|
+
closeSync,
|
|
25976
|
+
fstatSync,
|
|
25977
|
+
linkSync,
|
|
25978
|
+
mkdirSync as mkdirSync3,
|
|
25979
|
+
openSync,
|
|
25980
|
+
readFileSync as readFileSync3,
|
|
25981
|
+
readSync,
|
|
25982
|
+
renameSync as renameSync2,
|
|
25983
|
+
rmSync as rmSync2,
|
|
25984
|
+
statSync,
|
|
25985
|
+
writeSync
|
|
25986
|
+
} from "node:fs";
|
|
25987
|
+
import { hostname as hostname3 } from "node:os";
|
|
25988
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
25989
|
+
|
|
25990
|
+
// src/text.ts
|
|
25991
|
+
var CONTROL_RE2 = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
25992
|
+
function escape(char) {
|
|
25993
|
+
return `\\x${(char.codePointAt(0) ?? 0).toString(16).padStart(2, "0")}`;
|
|
25994
|
+
}
|
|
25995
|
+
function printable(text) {
|
|
25996
|
+
return text.replace(/\s+/g, " ").replace(CONTROL_RE2, escape);
|
|
25997
|
+
}
|
|
25998
|
+
function printableLines(text) {
|
|
25999
|
+
return text.replace(/\r\n?/g, "\n").replace(
|
|
26000
|
+
CONTROL_RE2,
|
|
26001
|
+
(char) => char === "\n" || char === " " ? char : escape(char)
|
|
26002
|
+
);
|
|
26003
|
+
}
|
|
26004
|
+
|
|
26005
|
+
// src/queue.ts
|
|
26006
|
+
var QUEUE_FILENAME = "unsent.jsonl";
|
|
26007
|
+
var LOCK_FILENAME = "unsent.lock";
|
|
26008
|
+
var LOCK_STALE_MS = 5 * 6e4;
|
|
26009
|
+
var QUEUE_MODE = CREDENTIALS_MODE;
|
|
26010
|
+
function queuePath(env) {
|
|
26011
|
+
return join4(defaultQuibbleHome(env), QUEUE_FILENAME);
|
|
26012
|
+
}
|
|
26013
|
+
function lockPathOf(queue) {
|
|
26014
|
+
return join4(dirname4(queue), LOCK_FILENAME);
|
|
26015
|
+
}
|
|
26016
|
+
function authLabel(context) {
|
|
26017
|
+
if (context.token !== void 0) {
|
|
26018
|
+
const hash2 = createHash2("sha256").update(context.token).digest("hex");
|
|
26019
|
+
return `token:${hash2.slice(0, 16)}`;
|
|
26020
|
+
}
|
|
26021
|
+
if (context.credential !== void 0) return `session:${context.credential.sub}`;
|
|
26022
|
+
return `dev:${context.actAs ?? ""}`;
|
|
26023
|
+
}
|
|
26024
|
+
function readOptional(path) {
|
|
26025
|
+
try {
|
|
26026
|
+
return readFileSync3(path, "utf8");
|
|
26027
|
+
} catch (error62) {
|
|
26028
|
+
if (isNotFound(error62)) return "";
|
|
26029
|
+
throw new StorageError(`cannot read ${path}`, { cause: error62 });
|
|
26030
|
+
}
|
|
26031
|
+
}
|
|
26032
|
+
function endsInsideLine(path) {
|
|
26033
|
+
let fd;
|
|
26034
|
+
try {
|
|
26035
|
+
fd = openSync(path, "r");
|
|
26036
|
+
} catch (error62) {
|
|
26037
|
+
if (isNotFound(error62)) return false;
|
|
26038
|
+
throw error62;
|
|
26039
|
+
}
|
|
26040
|
+
try {
|
|
26041
|
+
const size = fstatSync(fd).size;
|
|
26042
|
+
if (size === 0) return false;
|
|
26043
|
+
const last = Buffer.alloc(1);
|
|
26044
|
+
readSync(fd, last, 0, 1, size - 1);
|
|
26045
|
+
return last[0] !== 10;
|
|
26046
|
+
} finally {
|
|
26047
|
+
closeSync(fd);
|
|
26048
|
+
}
|
|
26049
|
+
}
|
|
26050
|
+
function appendLine(path, value) {
|
|
26051
|
+
try {
|
|
26052
|
+
mkdirSync3(dirname4(path), { recursive: true, mode: HOME_MODE });
|
|
26053
|
+
const lead = endsInsideLine(path) ? "\n" : "";
|
|
26054
|
+
appendFileSync(path, `${lead}${JSON.stringify(value)}
|
|
26055
|
+
`, {
|
|
26056
|
+
encoding: "utf8",
|
|
26057
|
+
mode: QUEUE_MODE
|
|
26058
|
+
});
|
|
26059
|
+
} catch (error62) {
|
|
26060
|
+
throw new StorageError(`cannot write ${path}`, { cause: error62 });
|
|
26061
|
+
}
|
|
26062
|
+
}
|
|
26063
|
+
function badLine(path, line, why) {
|
|
26064
|
+
return new StorageError(`${path}: line ${line}: ${why}`);
|
|
26065
|
+
}
|
|
26066
|
+
function readQueuedLine(record2, path, line) {
|
|
26067
|
+
const body = asRecord(record2["body"]);
|
|
26068
|
+
const fields = ["key", "endpoint", "org", "auth", "queuedAt"];
|
|
26069
|
+
for (const field of fields) {
|
|
26070
|
+
if (typeof record2[field] !== "string") {
|
|
26071
|
+
throw badLine(path, line, `${field} must be a string`);
|
|
26072
|
+
}
|
|
26073
|
+
}
|
|
26074
|
+
if (body === void 0) throw badLine(path, line, "body must be an object");
|
|
26075
|
+
return {
|
|
26076
|
+
key: record2["key"],
|
|
26077
|
+
endpoint: record2["endpoint"],
|
|
26078
|
+
org: record2["org"],
|
|
26079
|
+
auth: record2["auth"],
|
|
26080
|
+
body,
|
|
26081
|
+
queuedAt: record2["queuedAt"]
|
|
26082
|
+
};
|
|
26083
|
+
}
|
|
26084
|
+
function readQueue(path) {
|
|
26085
|
+
const pending = /* @__PURE__ */ new Map();
|
|
26086
|
+
const known = /* @__PURE__ */ new Set();
|
|
26087
|
+
const lines = readOptional(path).split("\n");
|
|
26088
|
+
for (const [index, text] of lines.entries()) {
|
|
26089
|
+
const line = index + 1;
|
|
26090
|
+
if (text.trim() === "") continue;
|
|
26091
|
+
let record2;
|
|
26092
|
+
try {
|
|
26093
|
+
record2 = asRecord(JSON.parse(text));
|
|
26094
|
+
} catch {
|
|
26095
|
+
throw badLine(path, line, "not JSON");
|
|
26096
|
+
}
|
|
26097
|
+
if (record2 === void 0) throw badLine(path, line, "not a JSON object");
|
|
26098
|
+
const op = record2["op"];
|
|
26099
|
+
if (op === "queued") {
|
|
26100
|
+
const entry = readQueuedLine(record2, path, line);
|
|
26101
|
+
known.add(entry.key);
|
|
26102
|
+
pending.set(entry.key, entry);
|
|
26103
|
+
continue;
|
|
26104
|
+
}
|
|
26105
|
+
if (op === "sent" || op === "dropped") {
|
|
26106
|
+
const key = record2["key"];
|
|
26107
|
+
if (typeof key !== "string" || !known.has(key)) {
|
|
26108
|
+
throw badLine(path, line, `${op} names no queued complaint`);
|
|
26109
|
+
}
|
|
26110
|
+
pending.delete(key);
|
|
26111
|
+
continue;
|
|
26112
|
+
}
|
|
26113
|
+
throw badLine(path, line, `unknown op ${JSON.stringify(op)}`);
|
|
26114
|
+
}
|
|
26115
|
+
return [...pending.values()];
|
|
26116
|
+
}
|
|
26117
|
+
function pendingFor(path, endpoint) {
|
|
26118
|
+
const key = normalizeEndpoint(endpoint);
|
|
26119
|
+
return readQueue(path).filter((entry) => entry.endpoint === key);
|
|
26120
|
+
}
|
|
26121
|
+
function enqueue(path, input2, now = /* @__PURE__ */ new Date()) {
|
|
26122
|
+
const entry = {
|
|
26123
|
+
key: input2.key ?? randomUUID3(),
|
|
26124
|
+
endpoint: normalizeEndpoint(input2.endpoint),
|
|
26125
|
+
org: input2.org,
|
|
26126
|
+
auth: input2.auth,
|
|
26127
|
+
body: input2.body,
|
|
26128
|
+
queuedAt: now.toISOString()
|
|
26129
|
+
};
|
|
26130
|
+
appendLine(path, { op: "queued", ...entry });
|
|
26131
|
+
return entry;
|
|
26132
|
+
}
|
|
26133
|
+
function processRuns(pid) {
|
|
26134
|
+
try {
|
|
26135
|
+
process.kill(pid, 0);
|
|
26136
|
+
return true;
|
|
26137
|
+
} catch (error62) {
|
|
26138
|
+
return error62.code === "EPERM";
|
|
26139
|
+
}
|
|
26140
|
+
}
|
|
26141
|
+
function judgeLock(lock, now = /* @__PURE__ */ new Date()) {
|
|
26142
|
+
let text;
|
|
26143
|
+
try {
|
|
26144
|
+
text = readFileSync3(lock, "utf8");
|
|
26145
|
+
} catch (error62) {
|
|
26146
|
+
if (isNotFound(error62)) return void 0;
|
|
26147
|
+
throw new StorageError(`cannot read ${lock}`, { cause: error62 });
|
|
26148
|
+
}
|
|
26149
|
+
const record2 = asRecord(parseQuietly(text));
|
|
26150
|
+
const pid = record2?.["pid"];
|
|
26151
|
+
const host = stringOf(record2, "host");
|
|
26152
|
+
const at = Date.parse(stringOf(record2, "at") ?? "");
|
|
26153
|
+
if (typeof pid !== "number" || host === void 0 || Number.isNaN(at)) {
|
|
26154
|
+
let written;
|
|
26155
|
+
try {
|
|
26156
|
+
written = statSync(lock).mtimeMs;
|
|
26157
|
+
} catch (error62) {
|
|
26158
|
+
if (isNotFound(error62)) return void 0;
|
|
26159
|
+
throw new StorageError(`cannot read ${lock}`, { cause: error62 });
|
|
26160
|
+
}
|
|
26161
|
+
return { text, stale: now.getTime() - written > LOCK_STALE_MS };
|
|
26162
|
+
}
|
|
26163
|
+
const dead = host === hostname3() && !processRuns(pid);
|
|
26164
|
+
return { text, stale: dead || now.getTime() - at > LOCK_STALE_MS };
|
|
26165
|
+
}
|
|
26166
|
+
function createLock(lock, owner = { pid: process.pid, host: hostname3(), at: (/* @__PURE__ */ new Date()).toISOString() }) {
|
|
26167
|
+
const text = JSON.stringify(owner);
|
|
26168
|
+
let fd;
|
|
26169
|
+
try {
|
|
26170
|
+
mkdirSync3(dirname4(lock), { recursive: true, mode: HOME_MODE });
|
|
26171
|
+
fd = openSync(lock, "wx", QUEUE_MODE);
|
|
26172
|
+
} catch (error62) {
|
|
26173
|
+
if (error62.code === "EEXIST") return void 0;
|
|
26174
|
+
throw new StorageError(`cannot create ${lock}`, { cause: error62 });
|
|
26175
|
+
}
|
|
26176
|
+
try {
|
|
26177
|
+
writeSync(fd, text);
|
|
26178
|
+
} finally {
|
|
26179
|
+
closeSync(fd);
|
|
26180
|
+
}
|
|
26181
|
+
return text;
|
|
26182
|
+
}
|
|
26183
|
+
function takeOverLock(lock, judged, owner) {
|
|
26184
|
+
const moved = `${lock}.${process.pid}.${randomBytes2(6).toString("hex")}`;
|
|
26185
|
+
try {
|
|
26186
|
+
renameSync2(lock, moved);
|
|
26187
|
+
} catch (error62) {
|
|
26188
|
+
if (isNotFound(error62)) return void 0;
|
|
26189
|
+
throw new StorageError(`cannot move ${lock}`, { cause: error62 });
|
|
26190
|
+
}
|
|
26191
|
+
const text = readOptional(moved);
|
|
26192
|
+
if (text === judged.text) {
|
|
26193
|
+
rmSync2(moved, { force: true });
|
|
26194
|
+
return createLock(lock, owner);
|
|
26195
|
+
}
|
|
26196
|
+
try {
|
|
26197
|
+
linkSync(moved, lock);
|
|
26198
|
+
} catch (error62) {
|
|
26199
|
+
if (error62.code !== "EEXIST") {
|
|
26200
|
+
throw new StorageError(`cannot restore ${lock}`, { cause: error62 });
|
|
26201
|
+
}
|
|
26202
|
+
} finally {
|
|
26203
|
+
rmSync2(moved, { force: true });
|
|
26204
|
+
}
|
|
26205
|
+
return void 0;
|
|
26206
|
+
}
|
|
26207
|
+
function acquireLock(lock) {
|
|
26208
|
+
const created = createLock(lock);
|
|
26209
|
+
if (created !== void 0) return created;
|
|
26210
|
+
const judged = judgeLock(lock);
|
|
26211
|
+
if (judged === void 0) return createLock(lock);
|
|
26212
|
+
if (!judged.stale) return void 0;
|
|
26213
|
+
return takeOverLock(lock, judged);
|
|
26214
|
+
}
|
|
26215
|
+
function releaseLock(lock, text) {
|
|
26216
|
+
if (readOptional(lock) === text) rmSync2(lock, { force: true });
|
|
26217
|
+
}
|
|
26218
|
+
function stopReason(status, text) {
|
|
26219
|
+
if (status >= 500) return `the API answered ${status}`;
|
|
26220
|
+
const code2 = serverCode(text);
|
|
26221
|
+
const stops = status === 429 || status === 401 || status === 403 || status === 404 && code2 === ORG_NOT_FOUND;
|
|
26222
|
+
if (!stops) return void 0;
|
|
26223
|
+
const message = stringOf(asRecord(asRecord(parseQuietly(text))?.["error"]), "message");
|
|
26224
|
+
return message ?? `the API answered ${status}`;
|
|
26225
|
+
}
|
|
26226
|
+
function projectOf(entry) {
|
|
26227
|
+
const project = entry.body["project"];
|
|
26228
|
+
return typeof project === "string" ? project : "?";
|
|
26229
|
+
}
|
|
26230
|
+
function headlineOfEntry(entry) {
|
|
26231
|
+
const message = entry.body["message"];
|
|
26232
|
+
return typeof message === "string" ? printable(summarize(message)) : "?";
|
|
26233
|
+
}
|
|
26234
|
+
function queuedCount(n) {
|
|
26235
|
+
return `${n} queued ${n === 1 ? "complaint" : "complaints"}`;
|
|
26236
|
+
}
|
|
26237
|
+
async function flushQueue(options) {
|
|
26238
|
+
const endpoint = normalizeEndpoint(options.endpoint);
|
|
26239
|
+
const mine = () => readQueue(options.path).filter(
|
|
26240
|
+
(entry) => entry.endpoint === endpoint && entry.auth === options.auth
|
|
26241
|
+
);
|
|
26242
|
+
const result = {
|
|
26243
|
+
sent: [],
|
|
26244
|
+
dropped: 0,
|
|
26245
|
+
left: 0,
|
|
26246
|
+
locked: false,
|
|
26247
|
+
stopped: void 0
|
|
26248
|
+
};
|
|
26249
|
+
if (mine().length === 0) return result;
|
|
26250
|
+
const lock = lockPathOf(options.path);
|
|
26251
|
+
const held = acquireLock(lock);
|
|
26252
|
+
if (held === void 0) {
|
|
26253
|
+
return { ...result, locked: true, left: mine().length };
|
|
26254
|
+
}
|
|
26255
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
26256
|
+
try {
|
|
26257
|
+
const entries = mine();
|
|
26258
|
+
for (const [index, entry] of entries.entries()) {
|
|
26259
|
+
let status;
|
|
26260
|
+
let text;
|
|
26261
|
+
try {
|
|
26262
|
+
({ status, text } = await options.client.exchange(
|
|
26263
|
+
"POST",
|
|
26264
|
+
`/v1/orgs/${encodeURIComponent(entry.org)}/complaints`,
|
|
26265
|
+
JSON.stringify(entry.body),
|
|
26266
|
+
{ [IDEMPOTENCY_HEADER]: entry.key }
|
|
26267
|
+
));
|
|
26268
|
+
} catch (error62) {
|
|
26269
|
+
result.stopped = errorMessage(error62);
|
|
26270
|
+
result.left = entries.length - index;
|
|
26271
|
+
return result;
|
|
26272
|
+
}
|
|
26273
|
+
if (status >= 200 && status < 300) {
|
|
26274
|
+
const id = stringOf(asRecord(parseQuietly(text)), "id") ?? "";
|
|
26275
|
+
appendLine(options.path, {
|
|
26276
|
+
op: "sent",
|
|
26277
|
+
key: entry.key,
|
|
26278
|
+
id,
|
|
26279
|
+
sentAt: now().toISOString()
|
|
26280
|
+
});
|
|
26281
|
+
result.sent.push(id);
|
|
26282
|
+
continue;
|
|
26283
|
+
}
|
|
26284
|
+
const reason = stopReason(status, text);
|
|
26285
|
+
if (reason !== void 0) {
|
|
26286
|
+
result.stopped = reason;
|
|
26287
|
+
result.left = entries.length - index;
|
|
26288
|
+
return result;
|
|
26289
|
+
}
|
|
26290
|
+
const record2 = asRecord(asRecord(parseQuietly(text))?.["error"]);
|
|
26291
|
+
const code2 = stringOf(record2, "code") ?? `http_${status}`;
|
|
26292
|
+
const message = stringOf(record2, "message") ?? `the API answered ${status}`;
|
|
26293
|
+
appendLine(options.path, {
|
|
26294
|
+
op: "dropped",
|
|
26295
|
+
key: entry.key,
|
|
26296
|
+
code: code2,
|
|
26297
|
+
message,
|
|
26298
|
+
droppedAt: now().toISOString()
|
|
26299
|
+
});
|
|
26300
|
+
result.dropped += 1;
|
|
26301
|
+
writeLine(
|
|
26302
|
+
options.stderr,
|
|
26303
|
+
`quibble: dropped a queued complaint for ${entry.org}/${projectOf(entry)}, "${headlineOfEntry(entry)}": ${printable(message)}`
|
|
26304
|
+
);
|
|
26305
|
+
}
|
|
26306
|
+
return result;
|
|
26307
|
+
} finally {
|
|
26308
|
+
releaseLock(lock, held);
|
|
26309
|
+
}
|
|
26310
|
+
}
|
|
26311
|
+
|
|
25081
26312
|
// src/store.ts
|
|
26313
|
+
function queueSender(context, endpoint, io) {
|
|
26314
|
+
return async (client) => {
|
|
26315
|
+
let result;
|
|
26316
|
+
try {
|
|
26317
|
+
result = await flushQueue({
|
|
26318
|
+
path: queuePath(io.env),
|
|
26319
|
+
endpoint,
|
|
26320
|
+
auth: authLabel(context),
|
|
26321
|
+
client,
|
|
26322
|
+
stderr: io.stderr
|
|
26323
|
+
});
|
|
26324
|
+
} catch (error62) {
|
|
26325
|
+
if (!(error62 instanceof StorageError)) throw error62;
|
|
26326
|
+
writeLine(
|
|
26327
|
+
io.stderr,
|
|
26328
|
+
`quibble: warning: cannot send the queue: ${errorMessage(error62)}`
|
|
26329
|
+
);
|
|
26330
|
+
return;
|
|
26331
|
+
}
|
|
26332
|
+
if (result.stopped !== void 0) {
|
|
26333
|
+
writeLine(
|
|
26334
|
+
io.stderr,
|
|
26335
|
+
`quibble: ${queuedCount(result.left)} still unsent: ${result.stopped}`
|
|
26336
|
+
);
|
|
26337
|
+
}
|
|
26338
|
+
};
|
|
26339
|
+
}
|
|
25082
26340
|
function actAsProblem(context) {
|
|
25083
26341
|
if (context.actAs === void 0) return void 0;
|
|
25084
26342
|
const endpoint = context.endpoint;
|
|
@@ -25087,22 +26345,25 @@ function actAsProblem(context) {
|
|
|
25087
26345
|
}
|
|
25088
26346
|
if (context.token !== void 0) return "QUIBBLE_TOKEN is set";
|
|
25089
26347
|
if (context.credential !== void 0) {
|
|
25090
|
-
return `
|
|
26348
|
+
return `you are logged in to ${endpoint}`;
|
|
25091
26349
|
}
|
|
25092
|
-
if (!
|
|
26350
|
+
if (!isLoopbackUrl(endpoint)) return `${endpoint} is not a loopback endpoint`;
|
|
25093
26351
|
return void 0;
|
|
25094
26352
|
}
|
|
25095
26353
|
function checkActAs(context) {
|
|
25096
26354
|
const problem = actAsProblem(context);
|
|
25097
26355
|
if (problem !== void 0) throw actAsNotApplicable(problem);
|
|
25098
26356
|
}
|
|
25099
|
-
function openClient(context, io) {
|
|
26357
|
+
async function openClient(context, io, options = {}) {
|
|
26358
|
+
const endpoint = await resolveEndpoint(context, io);
|
|
25100
26359
|
checkActAs(context);
|
|
25101
|
-
const
|
|
25102
|
-
const
|
|
25103
|
-
|
|
26360
|
+
const http = { debug: context.debug, fetch: io.fetch, timeoutMs: context.timeoutMs };
|
|
26361
|
+
const org = {
|
|
26362
|
+
...context.org === void 0 ? {} : { org: context.org },
|
|
26363
|
+
...options.queue === false ? {} : { beforeFirst: queueSender(context, endpoint, io) }
|
|
26364
|
+
};
|
|
25104
26365
|
if (context.token === void 0 && context.credential === void 0) {
|
|
25105
|
-
if (!
|
|
26366
|
+
if (!isLoopbackUrl(endpoint)) throw notLoggedIn(endpoint);
|
|
25106
26367
|
return new ApiClient({
|
|
25107
26368
|
endpoint,
|
|
25108
26369
|
authorizer: devAuthorizer(endpoint, context.actAs, http),
|
|
@@ -25117,25 +26378,35 @@ function openClient(context, io) {
|
|
|
25117
26378
|
...http
|
|
25118
26379
|
});
|
|
25119
26380
|
}
|
|
25120
|
-
function openOrgClient(context, io) {
|
|
25121
|
-
checkActAs(context);
|
|
26381
|
+
async function openOrgClient(context, io, options = {}) {
|
|
26382
|
+
if (context.mode === "local") checkActAs(context);
|
|
25122
26383
|
requireOrg(context);
|
|
25123
|
-
return openClient(context, io);
|
|
26384
|
+
return openClient(context, io, options);
|
|
25124
26385
|
}
|
|
25125
|
-
function openBackend(context, io) {
|
|
26386
|
+
async function openBackend(context, io) {
|
|
25126
26387
|
if (context.mode === "local") {
|
|
25127
26388
|
checkActAs(context);
|
|
25128
|
-
|
|
26389
|
+
refuseForeignOrg(context);
|
|
26390
|
+
const local = new FileStore(context.storePath);
|
|
26391
|
+
return { store: local, client: void 0, local };
|
|
25129
26392
|
}
|
|
25130
|
-
const client = openOrgClient(context, io);
|
|
25131
|
-
return { store: new RemoteStore(client), client };
|
|
26393
|
+
const client = await openOrgClient(context, io);
|
|
26394
|
+
return { store: new RemoteStore(client), client, local: void 0 };
|
|
25132
26395
|
}
|
|
25133
|
-
function openStore(context, io) {
|
|
25134
|
-
return openBackend(context, io).store;
|
|
26396
|
+
async function openStore(context, io) {
|
|
26397
|
+
return (await openBackend(context, io)).store;
|
|
26398
|
+
}
|
|
26399
|
+
function needsServer(command, context) {
|
|
26400
|
+
return new ConfigError(
|
|
26401
|
+
`${command} needs a server: ${context.localFix ?? "drop --local"}`
|
|
26402
|
+
);
|
|
26403
|
+
}
|
|
26404
|
+
function requireServer(command, context) {
|
|
26405
|
+
if (context.mode !== "remote") throw needsServer(command, context);
|
|
25135
26406
|
}
|
|
25136
26407
|
|
|
25137
26408
|
// src/commands/complain.ts
|
|
25138
|
-
var REMOTE_REPORTER_MESSAGE = "--reporter
|
|
26409
|
+
var REMOTE_REPORTER_MESSAGE = "--reporter only works in local mode: the server sets the reporter";
|
|
25139
26410
|
async function runComplain(message, options, context, io) {
|
|
25140
26411
|
const project = requireProject(context);
|
|
25141
26412
|
const org = requireOrg(context);
|
|
@@ -25143,7 +26414,7 @@ async function runComplain(message, options, context, io) {
|
|
|
25143
26414
|
throw new UsageError(REMOTE_REPORTER_MESSAGE);
|
|
25144
26415
|
}
|
|
25145
26416
|
const body = await readMessage(message, io);
|
|
25146
|
-
const store = openStore(context, io);
|
|
26417
|
+
const store = await openStore(context, io);
|
|
25147
26418
|
const input2 = {
|
|
25148
26419
|
org,
|
|
25149
26420
|
project,
|
|
@@ -25153,13 +26424,21 @@ async function runComplain(message, options, context, io) {
|
|
|
25153
26424
|
...options.tag === void 0 || options.tag.length === 0 ? {} : { tags: options.tag },
|
|
25154
26425
|
agent: context.agent,
|
|
25155
26426
|
reporter: context.reporter,
|
|
25156
|
-
...context.cwd === void 0 ? {} : { cwd: context.cwd }
|
|
26427
|
+
...context.cwd === void 0 ? {} : { cwd: context.cwd },
|
|
26428
|
+
...options.cmd === void 0 ? {} : { cmd: options.cmd }
|
|
25157
26429
|
};
|
|
25158
|
-
|
|
25159
|
-
|
|
25160
|
-
|
|
26430
|
+
let complaint;
|
|
26431
|
+
try {
|
|
26432
|
+
complaint = await createComplaint(store, input2, {
|
|
26433
|
+
allowedTags: context.allowedTags
|
|
26434
|
+
});
|
|
26435
|
+
} catch (error62) {
|
|
26436
|
+
if (!(error62 instanceof UndeliveredError)) throw error62;
|
|
26437
|
+
return queueComplaint(error62, org, project, context, io, options.json === true);
|
|
26438
|
+
}
|
|
25161
26439
|
if (options.json === true) {
|
|
25162
26440
|
writeJson(io.stdout, complaint);
|
|
26441
|
+
localNotice(context, io);
|
|
25163
26442
|
return 0;
|
|
25164
26443
|
}
|
|
25165
26444
|
const scope = context.mode === "remote" ? `${complaint.org}/${complaint.project}` : complaint.project;
|
|
@@ -25167,6 +26446,43 @@ async function runComplain(message, options, context, io) {
|
|
|
25167
26446
|
io.stdout,
|
|
25168
26447
|
`Filed complaint ${complaint.id} for ${scope} (${complaint.severity})`
|
|
25169
26448
|
);
|
|
26449
|
+
for (const line of matchLines(complaint)) writeLine(io.stdout, line);
|
|
26450
|
+
localNotice(context, io);
|
|
26451
|
+
return 0;
|
|
26452
|
+
}
|
|
26453
|
+
function localNotice(context, io) {
|
|
26454
|
+
if (context.mode !== "local") return;
|
|
26455
|
+
writeLine(
|
|
26456
|
+
io.stderr,
|
|
26457
|
+
`quibble: local mode: nothing was sent; the complaint is in ${context.storePath}`
|
|
26458
|
+
);
|
|
26459
|
+
}
|
|
26460
|
+
function matchLines(complaint) {
|
|
26461
|
+
const lines = [];
|
|
26462
|
+
if (complaint.openMatches === 1) {
|
|
26463
|
+
lines.push("1 open complaint already shares this headline.");
|
|
26464
|
+
} else if (complaint.openMatches > 1) {
|
|
26465
|
+
lines.push(`${complaint.openMatches} open complaints already share this headline.`);
|
|
26466
|
+
}
|
|
26467
|
+
if (complaint.recurred) {
|
|
26468
|
+
lines.push("A resolved complaint shares this headline: this one is marked recurred.");
|
|
26469
|
+
}
|
|
26470
|
+
return lines;
|
|
26471
|
+
}
|
|
26472
|
+
function queueComplaint(error62, org, project, context, io, json2) {
|
|
26473
|
+
const path = queuePath(io.env);
|
|
26474
|
+
enqueue(path, {
|
|
26475
|
+
endpoint: requireEndpoint(context),
|
|
26476
|
+
org,
|
|
26477
|
+
auth: authLabel(context),
|
|
26478
|
+
body: error62.body,
|
|
26479
|
+
key: error62.key
|
|
26480
|
+
});
|
|
26481
|
+
writeLine(
|
|
26482
|
+
io.stderr,
|
|
26483
|
+
`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\`.`
|
|
26484
|
+
);
|
|
26485
|
+
if (json2) writeJson(io.stdout, { queued: true, org, project, reason: error62.reason });
|
|
25170
26486
|
return 0;
|
|
25171
26487
|
}
|
|
25172
26488
|
async function readMessage(message, io) {
|
|
@@ -25184,7 +26500,7 @@ function describeCredential(context) {
|
|
|
25184
26500
|
if (context.credential !== void 0) {
|
|
25185
26501
|
return `logged in as ${context.credential.email}`;
|
|
25186
26502
|
}
|
|
25187
|
-
if (context.endpoint !== void 0 &&
|
|
26503
|
+
if (context.endpoint !== void 0 && isLoopbackUrl(context.endpoint)) {
|
|
25188
26504
|
return "loopback dev token";
|
|
25189
26505
|
}
|
|
25190
26506
|
return "not logged in";
|
|
@@ -25196,57 +26512,150 @@ function describeAuth(context) {
|
|
|
25196
26512
|
if (problem === void 0) return `${credential}, acting as ${context.actAs}`;
|
|
25197
26513
|
return `${credential}; ${ACT_AS_ENV} does not apply: ${problem}`;
|
|
25198
26514
|
}
|
|
25199
|
-
function runConfig(options, context, io) {
|
|
26515
|
+
async function runConfig(options, context, io) {
|
|
26516
|
+
if (context.mode === "remote") await resolveEndpoint(context, io);
|
|
26517
|
+
const ignored = foreignOrgMessage(context) === void 0 ? void 0 : context.slug;
|
|
25200
26518
|
const report = {
|
|
25201
26519
|
project: context.project ?? "none",
|
|
25202
26520
|
org: context.org ?? null,
|
|
26521
|
+
orgIgnored: ignored ?? null,
|
|
25203
26522
|
projectRoot: context.projectRoot ?? null,
|
|
25204
26523
|
configPath: context.configPath ?? null,
|
|
25205
26524
|
mode: context.mode,
|
|
25206
|
-
endpoint: context.endpoint ??
|
|
26525
|
+
endpoint: context.endpoint ?? "local",
|
|
26526
|
+
endpointSource: context.endpointSource,
|
|
25207
26527
|
storePath: context.storePath,
|
|
25208
26528
|
agent: context.agent,
|
|
25209
26529
|
reporter: context.reporter,
|
|
25210
26530
|
actAs: context.actAs ?? null,
|
|
25211
|
-
auth: describeAuth(context)
|
|
26531
|
+
auth: describeAuth(context),
|
|
26532
|
+
// Every credential counts, and nothing is sent.
|
|
26533
|
+
unsent: context.mode === "remote" && context.endpoint !== void 0 ? pendingFor(queuePath(io.env), context.endpoint).length : 0
|
|
25212
26534
|
};
|
|
25213
26535
|
if (options.json === true) {
|
|
25214
26536
|
writeJson(io.stdout, report);
|
|
25215
26537
|
return 0;
|
|
25216
26538
|
}
|
|
25217
|
-
const
|
|
25218
|
-
|
|
25219
|
-
|
|
26539
|
+
const { orgIgnored, ...lines } = report;
|
|
26540
|
+
const text = { ...lines };
|
|
26541
|
+
if (orgIgnored !== null) {
|
|
26542
|
+
text["org"] = `${report.org ?? ""}; ${orgIgnored} does not apply: ${context.localFix ?? ""}`;
|
|
26543
|
+
}
|
|
26544
|
+
const width = Math.max(...Object.keys(text).map((key) => key.length));
|
|
26545
|
+
for (const [key, value] of Object.entries(text)) {
|
|
26546
|
+
writeLine(io.stdout, `${key.padEnd(width)} ${String(value ?? "none")}`);
|
|
25220
26547
|
}
|
|
25221
26548
|
return 0;
|
|
25222
26549
|
}
|
|
25223
26550
|
|
|
25224
26551
|
// src/commands/init.ts
|
|
25225
|
-
import { existsSync, writeFileSync as
|
|
25226
|
-
import { basename, join as
|
|
25227
|
-
var SNIPPET_INTRO = `## Filing complaints
|
|
26552
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
26553
|
+
import { basename as basename2, join as join7, resolve as resolve3 } from "node:path";
|
|
25228
26554
|
|
|
25229
|
-
|
|
25230
|
-
|
|
25231
|
-
|
|
25232
|
-
var
|
|
25233
|
-
|
|
25234
|
-
|
|
25235
|
-
|
|
25236
|
-
|
|
25237
|
-
|
|
25238
|
-
|
|
25239
|
-
}
|
|
25240
|
-
|
|
25241
|
-
|
|
25242
|
-
);
|
|
26555
|
+
// src/hook.ts
|
|
26556
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
26557
|
+
import { dirname as dirname5, join as join5 } from "node:path";
|
|
26558
|
+
var SETTINGS_PATH = ".claude/settings.json";
|
|
26559
|
+
var HOOK_COMMAND = "command -v quibble >/dev/null 2>&1 && quibble sync --quiet || true";
|
|
26560
|
+
var HOOK_MARK = "quibble sync --quiet";
|
|
26561
|
+
function hookEntry() {
|
|
26562
|
+
return {
|
|
26563
|
+
matcher: "startup",
|
|
26564
|
+
hooks: [{ type: "command", command: HOOK_COMMAND }]
|
|
26565
|
+
};
|
|
26566
|
+
}
|
|
26567
|
+
function isObject2(value) {
|
|
26568
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26569
|
+
}
|
|
26570
|
+
function runsSync(entry) {
|
|
26571
|
+
if (!isObject2(entry) || !Array.isArray(entry["hooks"])) return false;
|
|
26572
|
+
return entry["hooks"].some(
|
|
26573
|
+
(hook) => isObject2(hook) && typeof hook["command"] === "string" && hook["command"].includes(HOOK_MARK)
|
|
26574
|
+
);
|
|
26575
|
+
}
|
|
26576
|
+
function planHook(root) {
|
|
26577
|
+
const path = join5(root, SETTINGS_PATH);
|
|
26578
|
+
let text;
|
|
26579
|
+
try {
|
|
26580
|
+
text = readFileSync4(path, "utf8");
|
|
26581
|
+
} catch (error62) {
|
|
26582
|
+
if (!isNotFound(error62)) throw error62;
|
|
26583
|
+
const created = { hooks: { SessionStart: [hookEntry()] } };
|
|
26584
|
+
return { path, text: `${JSON.stringify(created, null, 2)}
|
|
26585
|
+
` };
|
|
26586
|
+
}
|
|
26587
|
+
const invalid = new UsageError(
|
|
26588
|
+
`${path} is not valid JSON: fix it, then run the command again`
|
|
26589
|
+
);
|
|
26590
|
+
let settings;
|
|
26591
|
+
try {
|
|
26592
|
+
settings = JSON.parse(text);
|
|
26593
|
+
} catch {
|
|
26594
|
+
throw invalid;
|
|
26595
|
+
}
|
|
26596
|
+
if (!isObject2(settings)) throw invalid;
|
|
26597
|
+
const hooks = settings["hooks"] === void 0 ? {} : settings["hooks"];
|
|
26598
|
+
if (!isObject2(hooks)) throw new UsageError(`${path}: hooks must be an object`);
|
|
26599
|
+
const start = hooks["SessionStart"] === void 0 ? [] : hooks["SessionStart"];
|
|
26600
|
+
if (!Array.isArray(start)) {
|
|
26601
|
+
throw new UsageError(`${path}: hooks.SessionStart must be an array`);
|
|
26602
|
+
}
|
|
26603
|
+
if (start.some(runsSync)) return { path, text: void 0 };
|
|
26604
|
+
const merged = {
|
|
26605
|
+
...settings,
|
|
26606
|
+
hooks: { ...hooks, SessionStart: [...start, hookEntry()] }
|
|
26607
|
+
};
|
|
26608
|
+
return { path, text: `${JSON.stringify(merged, null, 2)}
|
|
26609
|
+
` };
|
|
26610
|
+
}
|
|
26611
|
+
function applyHook(plan) {
|
|
26612
|
+
if (plan.text === void 0) return `${plan.path} already runs quibble sync`;
|
|
26613
|
+
mkdirSync4(dirname5(plan.path), { recursive: true });
|
|
26614
|
+
writeFileSync3(plan.path, plan.text, "utf8");
|
|
26615
|
+
return `Added the quibble sync hook to ${plan.path}`;
|
|
26616
|
+
}
|
|
26617
|
+
|
|
26618
|
+
// src/snippet.ts
|
|
26619
|
+
import { basename } from "node:path";
|
|
26620
|
+
var SNIPPET_HEADING = "## Filing complaints";
|
|
26621
|
+
var SKILL_HEADING = "# Filing complaints";
|
|
26622
|
+
var SKILL_PATH = ".claude/skills/quibble/SKILL.md";
|
|
26623
|
+
var DEFAULT_SNIPPET_AGENT = "claude";
|
|
26624
|
+
var AGENTS_FILE_AGENT = "codex";
|
|
26625
|
+
var SNIPPET_INTRO = `This project uses Quibble. When the build, tests, docs or tooling waste your
|
|
26626
|
+
time, file a complaint instead of quietly working around the problem.`;
|
|
26627
|
+
var SNIPPET_USAGE = `- Set severity with \`-s low|medium|high\`. Add tags with \`-t <tag>\` (repeatable).
|
|
26628
|
+
- Put a one-line summary first; it is the headline everywhere. Add details below.
|
|
26629
|
+
- Name the command that failed with \`--cmd "<command>"\`.
|
|
26630
|
+
- Pipe a long message on stdin with \`quibble complain -\`.
|
|
26631
|
+
- File one complaint per problem, as soon as you hit it, not at the end of the task.`;
|
|
26632
|
+
var SKILL_FRONT_MATTER = `---
|
|
26633
|
+
name: quibble
|
|
26634
|
+
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.
|
|
26635
|
+
---`;
|
|
25243
26636
|
var SNIPPET_LOGIN = "Run `quibble login` once before the first complaint.";
|
|
25244
26637
|
function orgSentence(org) {
|
|
25245
26638
|
return `Complaints go to the \`${org}\` organization.`;
|
|
25246
26639
|
}
|
|
26640
|
+
function exampleCommand(tag, agent) {
|
|
26641
|
+
return ` quibble complain "what went wrong, and what you expected" -s medium -t ${tag} --agent ${agent}`;
|
|
26642
|
+
}
|
|
26643
|
+
function tagRules(tags) {
|
|
26644
|
+
const list = tags.map((tag) => `\`${tag}\``).join(", ");
|
|
26645
|
+
return `Every complaint needs at least one of these tags: ${list}.
|
|
26646
|
+
If none fits, pass \`-t ${OTHER_TAG_PREFIX}<name>\` naming the tag you wanted, such as
|
|
26647
|
+
\`-t ${OTHER_TAG_PREFIX}network\`. Run \`quibble tags\` to see the list.`;
|
|
26648
|
+
}
|
|
25247
26649
|
function agentSnippet(tags, scope = {}) {
|
|
25248
26650
|
const first = tags?.[0];
|
|
25249
|
-
const
|
|
26651
|
+
const agent = scope.agent ?? DEFAULT_SNIPPET_AGENT;
|
|
26652
|
+
const blocks = [
|
|
26653
|
+
SNIPPET_HEADING,
|
|
26654
|
+
SNIPPET_INTRO,
|
|
26655
|
+
exampleCommand(first ?? "build", agent)
|
|
26656
|
+
];
|
|
26657
|
+
if (tags !== void 0 && first !== void 0) blocks.push(tagRules(tags));
|
|
26658
|
+
blocks.push(SNIPPET_USAGE);
|
|
25250
26659
|
const sentences = [];
|
|
25251
26660
|
const { endpoint, org } = scope;
|
|
25252
26661
|
if (endpoint !== void 0 && endpoint !== "") sentences.push(SNIPPET_LOGIN);
|
|
@@ -25254,54 +26663,300 @@ function agentSnippet(tags, scope = {}) {
|
|
|
25254
26663
|
if (sentences.length > 0) blocks.push(sentences.join(" "));
|
|
25255
26664
|
return blocks.join("\n\n");
|
|
25256
26665
|
}
|
|
25257
|
-
function
|
|
25258
|
-
const
|
|
25259
|
-
return
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25265
|
-
|
|
25266
|
-
|
|
26666
|
+
function skillFile(snippet) {
|
|
26667
|
+
const body = snippet.startsWith(SNIPPET_HEADING) ? `${SKILL_HEADING}${snippet.slice(SNIPPET_HEADING.length)}` : snippet;
|
|
26668
|
+
return `${SKILL_FRONT_MATTER}
|
|
26669
|
+
|
|
26670
|
+
${body}
|
|
26671
|
+
`;
|
|
26672
|
+
}
|
|
26673
|
+
function agentForFile(path) {
|
|
26674
|
+
return basename(path).toLowerCase() === "agents.md" ? AGENTS_FILE_AGENT : DEFAULT_SNIPPET_AGENT;
|
|
26675
|
+
}
|
|
26676
|
+
function hasSnippet(text) {
|
|
26677
|
+
return text.split(/\r?\n/).some((line) => line.trimEnd() === SNIPPET_HEADING);
|
|
26678
|
+
}
|
|
26679
|
+
function splitLines(text) {
|
|
26680
|
+
const lines = [];
|
|
26681
|
+
const pattern = /([^\r\n]*)(\r\n|\n|\r)?/g;
|
|
26682
|
+
let match;
|
|
26683
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
26684
|
+
const [whole, content = "", eol = ""] = match;
|
|
26685
|
+
if (whole === "") break;
|
|
26686
|
+
lines.push({ text: content, eol });
|
|
26687
|
+
}
|
|
26688
|
+
return lines;
|
|
26689
|
+
}
|
|
26690
|
+
function fenceOf(text) {
|
|
26691
|
+
if (text.startsWith("```")) return "```";
|
|
26692
|
+
if (text.startsWith("~~~")) return "~~~";
|
|
26693
|
+
return void 0;
|
|
26694
|
+
}
|
|
26695
|
+
function endsSection(text) {
|
|
26696
|
+
return text.startsWith("# ") || text.startsWith("## ");
|
|
26697
|
+
}
|
|
26698
|
+
function agentOfText(lines) {
|
|
26699
|
+
const line = lines.find(
|
|
26700
|
+
(text) => text.includes("quibble complain") && text.includes("--agent")
|
|
26701
|
+
);
|
|
26702
|
+
if (line === void 0) return void 0;
|
|
26703
|
+
return /--agent\s+(\S+)/.exec(line)?.[1];
|
|
26704
|
+
}
|
|
26705
|
+
function findSection(text) {
|
|
26706
|
+
const lines = splitLines(text);
|
|
26707
|
+
const start = lines.findIndex((line) => line.text.trimEnd() === SNIPPET_HEADING);
|
|
26708
|
+
if (start < 0) return void 0;
|
|
26709
|
+
let next = lines.length;
|
|
26710
|
+
let fence;
|
|
26711
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
26712
|
+
const content = lines[index]?.text ?? "";
|
|
26713
|
+
if (fence !== void 0) {
|
|
26714
|
+
if (content.startsWith(fence)) fence = void 0;
|
|
26715
|
+
continue;
|
|
26716
|
+
}
|
|
26717
|
+
fence = fenceOf(content);
|
|
26718
|
+
if (fence === void 0 && endsSection(content)) {
|
|
26719
|
+
next = index;
|
|
26720
|
+
break;
|
|
26721
|
+
}
|
|
26722
|
+
}
|
|
26723
|
+
let end = next;
|
|
26724
|
+
while (end > start + 1 && (lines[end - 1]?.text.trim() ?? "") === "") end -= 1;
|
|
26725
|
+
const eol = lines[start]?.eol || "\n";
|
|
26726
|
+
return { lines, start, end, eol: eol === "\r\n" ? "\r\n" : "\n" };
|
|
26727
|
+
}
|
|
26728
|
+
function sectionText(section) {
|
|
26729
|
+
return section.lines.slice(section.start, section.end).map((line) => line.text);
|
|
26730
|
+
}
|
|
26731
|
+
function replaceSection(section, snippet) {
|
|
26732
|
+
const join9 = (lines) => lines.map((line) => `${line.text}${line.eol}`).join("");
|
|
26733
|
+
const body = snippet.split("\n").map((line) => `${line}${section.eol}`).join("");
|
|
26734
|
+
return `${join9(section.lines.slice(0, section.start))}${body}${join9(section.lines.slice(section.end))}`;
|
|
26735
|
+
}
|
|
26736
|
+
function withLineBreak(text, old) {
|
|
26737
|
+
return old.includes("\r\n") ? text.replace(/\r?\n/g, "\r\n") : text;
|
|
26738
|
+
}
|
|
26739
|
+
function orList(items) {
|
|
26740
|
+
if (items.length <= 1) return items.join("");
|
|
26741
|
+
return `${items.slice(0, -1).join(", ")} or ${items.at(-1) ?? ""}`;
|
|
26742
|
+
}
|
|
26743
|
+
function refreshedLine(project, tags) {
|
|
26744
|
+
const lead = `Quibble refreshed its instructions for ${project}:`;
|
|
26745
|
+
if (tags === void 0 || tags.length === 0) return `${lead} any tag is allowed.`;
|
|
26746
|
+
return `${lead} tag each complaint with ${orList(tags)}, or ${OTHER_TAG_PREFIX}<name> when none fits.`;
|
|
26747
|
+
}
|
|
26748
|
+
|
|
26749
|
+
// src/commands/snippet.ts
|
|
26750
|
+
import { existsSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
26751
|
+
import { dirname as dirname6, join as join6, resolve as resolve2 } from "node:path";
|
|
26752
|
+
function existsError(path) {
|
|
26753
|
+
return new UsageError(`${path} already exists: pass --force to overwrite it`);
|
|
26754
|
+
}
|
|
26755
|
+
function readOptional2(path) {
|
|
26756
|
+
try {
|
|
26757
|
+
return readFileSync5(path, "utf8");
|
|
26758
|
+
} catch (error62) {
|
|
26759
|
+
if (isNotFound(error62)) return "";
|
|
26760
|
+
throw error62;
|
|
26761
|
+
}
|
|
26762
|
+
}
|
|
26763
|
+
function writeSkill(path, snippet) {
|
|
26764
|
+
mkdirSync5(dirname6(path), { recursive: true });
|
|
26765
|
+
writeFileSync4(path, skillFile(snippet), "utf8");
|
|
26766
|
+
}
|
|
26767
|
+
async function snippetSource(project, context, io, options = {}) {
|
|
26768
|
+
let tags;
|
|
26769
|
+
if (context.mode === "remote") {
|
|
26770
|
+
tags = (await (await openOrgClient(context, io, options)).project(project)).tags;
|
|
26771
|
+
} else {
|
|
26772
|
+
checkActAs(context);
|
|
26773
|
+
tags = context.allowedTags;
|
|
26774
|
+
}
|
|
26775
|
+
const scope = {
|
|
26776
|
+
...context.mode === "remote" ? { endpoint: context.endpoint } : {},
|
|
26777
|
+
...context.slug === void 0 ? {} : { org: context.slug }
|
|
26778
|
+
};
|
|
26779
|
+
return { tags, scope };
|
|
26780
|
+
}
|
|
26781
|
+
async function runSnippet(options, context, io) {
|
|
26782
|
+
const project = requireProject(context);
|
|
26783
|
+
const override = options.agent === void 0 ? void 0 : parseAgentName(options.agent);
|
|
26784
|
+
const appendPath = options.append === void 0 ? void 0 : resolve2(io.cwd, options.append);
|
|
26785
|
+
const skillPath = options.skill === true ? join6(context.projectRoot ?? io.cwd, SKILL_PATH) : void 0;
|
|
26786
|
+
const existing = appendPath === void 0 ? "" : readOptional2(appendPath);
|
|
26787
|
+
if (options.append !== void 0 && hasSnippet(existing)) {
|
|
26788
|
+
throw new UsageError(
|
|
26789
|
+
`${options.append} already has a "${SNIPPET_HEADING}" section: run quibble sync to refresh it`
|
|
26790
|
+
);
|
|
26791
|
+
}
|
|
26792
|
+
if (skillPath !== void 0 && existsSync(skillPath) && options.force !== true) {
|
|
26793
|
+
throw existsError(skillPath);
|
|
26794
|
+
}
|
|
26795
|
+
const hook = options.hook === true ? planHook(context.projectRoot ?? io.cwd) : void 0;
|
|
26796
|
+
const { tags, scope } = await snippetSource(project, context, io);
|
|
26797
|
+
let shown = { agent: override ?? DEFAULT_SNIPPET_AGENT, snippet: "" };
|
|
26798
|
+
shown.snippet = agentSnippet(tags, { ...scope, agent: shown.agent });
|
|
26799
|
+
if (appendPath !== void 0) {
|
|
26800
|
+
const agent = override ?? agentForFile(appendPath);
|
|
26801
|
+
const snippet = agentSnippet(tags, { ...scope, agent });
|
|
26802
|
+
const kept = existing.replace(/\s+$/, "");
|
|
26803
|
+
writeFileSync4(appendPath, kept === "" ? `${snippet}
|
|
26804
|
+
` : `${kept}
|
|
26805
|
+
|
|
26806
|
+
${snippet}
|
|
26807
|
+
`);
|
|
26808
|
+
shown = { agent, snippet };
|
|
26809
|
+
}
|
|
26810
|
+
if (skillPath !== void 0) {
|
|
26811
|
+
const agent = override ?? DEFAULT_SNIPPET_AGENT;
|
|
26812
|
+
const snippet = agentSnippet(tags, { ...scope, agent });
|
|
26813
|
+
writeSkill(skillPath, snippet);
|
|
26814
|
+
if (appendPath === void 0) shown = { agent, snippet };
|
|
26815
|
+
}
|
|
26816
|
+
const hookLine = hook === void 0 ? void 0 : applyHook(hook);
|
|
26817
|
+
if (options.json === true) {
|
|
26818
|
+
writeJson(io.stdout, {
|
|
26819
|
+
snippet: shown.snippet,
|
|
26820
|
+
agent: shown.agent,
|
|
26821
|
+
appended: appendPath ?? null,
|
|
26822
|
+
skill: skillPath ?? null,
|
|
26823
|
+
hook: hook?.path ?? null
|
|
26824
|
+
});
|
|
26825
|
+
return 0;
|
|
26826
|
+
}
|
|
26827
|
+
if (appendPath === void 0 && skillPath === void 0) {
|
|
26828
|
+
writeLine(io.stdout, shown.snippet);
|
|
26829
|
+
}
|
|
26830
|
+
if (appendPath !== void 0) {
|
|
26831
|
+
writeLine(io.stdout, `Appended the Quibble snippet to ${appendPath}`);
|
|
26832
|
+
}
|
|
26833
|
+
if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
|
|
26834
|
+
if (hookLine !== void 0) writeLine(io.stdout, hookLine);
|
|
26835
|
+
return 0;
|
|
26836
|
+
}
|
|
26837
|
+
|
|
26838
|
+
// src/commands/init.ts
|
|
26839
|
+
var CODEX_LINE = "For Codex, run `quibble snippet --append AGENTS.md`, which names the agent codex.";
|
|
26840
|
+
function agentLead(agent) {
|
|
26841
|
+
return agent === DEFAULT_SNIPPET_AGENT ? "Add this to your CLAUDE.md:" : "Add this to your agent's instructions file:";
|
|
26842
|
+
}
|
|
26843
|
+
async function runInit(options, io) {
|
|
26844
|
+
const directory = resolve3(io.cwd);
|
|
26845
|
+
const path = join7(directory, PROJECT_CONFIG_FILENAME);
|
|
26846
|
+
if (existsSync2(path) && options.force !== true) {
|
|
25267
26847
|
throw new ConfigError(`${path} already exists: pass --force to overwrite it`, path);
|
|
25268
26848
|
}
|
|
26849
|
+
const skillPath = options.skill === true ? join7(directory, SKILL_PATH) : void 0;
|
|
26850
|
+
if (skillPath !== void 0 && existsSync2(skillPath) && options.force !== true) {
|
|
26851
|
+
throw existsError(skillPath);
|
|
26852
|
+
}
|
|
25269
26853
|
const project = resolveProjectName(options.project, directory);
|
|
25270
26854
|
const org = options.org === void 0 || options.org.trim() === "" ? void 0 : parseOrgSlug(options.org);
|
|
25271
|
-
const endpoint = options.endpoint?.trim();
|
|
25272
26855
|
const tags = options.tag === void 0 ? void 0 : parseAllowedTags(options.tag);
|
|
26856
|
+
const agent = options.agent === void 0 ? DEFAULT_SNIPPET_AGENT : parseAgentName(options.agent);
|
|
26857
|
+
const hook = options.hook === true ? planHook(directory) : void 0;
|
|
26858
|
+
const endpoint = await endpointToWrite(options.endpoint, io);
|
|
25273
26859
|
const config2 = {
|
|
25274
26860
|
project,
|
|
25275
26861
|
...org === void 0 ? {} : { org },
|
|
25276
|
-
...endpoint === void 0
|
|
26862
|
+
...endpoint === void 0 ? {} : { endpoint },
|
|
25277
26863
|
...tags === void 0 ? {} : { tags }
|
|
25278
26864
|
};
|
|
25279
|
-
|
|
26865
|
+
writeFileSync5(path, `${JSON.stringify(config2, null, 2)}
|
|
25280
26866
|
`, "utf8");
|
|
26867
|
+
const context = {
|
|
26868
|
+
...resolveContext({
|
|
26869
|
+
cwd: io.cwd,
|
|
26870
|
+
env: io.env,
|
|
26871
|
+
endpoint: options.endpoint,
|
|
26872
|
+
org: options.org,
|
|
26873
|
+
local: options.local
|
|
26874
|
+
}),
|
|
26875
|
+
// `init` acts as nobody, so it ignores `QUIBBLE_ACT_AS`.
|
|
26876
|
+
actAs: void 0
|
|
26877
|
+
};
|
|
26878
|
+
const snippet = agentSnippet(tags, {
|
|
26879
|
+
agent,
|
|
26880
|
+
...context.mode === "remote" ? { endpoint: context.endpoint } : {},
|
|
26881
|
+
...org === void 0 ? {} : { org }
|
|
26882
|
+
});
|
|
26883
|
+
if (skillPath !== void 0) writeSkill(skillPath, snippet);
|
|
26884
|
+
const hookLine = hook === void 0 ? void 0 : applyHook(hook);
|
|
26885
|
+
const check2 = await checkServer(context, project, io);
|
|
25281
26886
|
if (options.json === true) {
|
|
25282
|
-
writeJson(io.stdout, {
|
|
26887
|
+
writeJson(io.stdout, {
|
|
26888
|
+
path,
|
|
26889
|
+
project,
|
|
26890
|
+
org: org ?? null,
|
|
26891
|
+
tags: tags ?? null,
|
|
26892
|
+
skill: skillPath ?? null,
|
|
26893
|
+
hook: hook?.path ?? null,
|
|
26894
|
+
server: check2.server
|
|
26895
|
+
});
|
|
25283
26896
|
return 0;
|
|
25284
26897
|
}
|
|
25285
26898
|
writeLine(io.stdout, `Wrote ${path}`);
|
|
26899
|
+
if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
|
|
26900
|
+
if (hookLine !== void 0) writeLine(io.stdout, hookLine);
|
|
25286
26901
|
writeLine(io.stdout, `Project: ${project}`);
|
|
25287
26902
|
if (org !== void 0) writeLine(io.stdout, `Organization: ${org}`);
|
|
25288
26903
|
if (tags !== void 0) writeLine(io.stdout, `Tags: ${tags.join(", ")}`);
|
|
25289
26904
|
writeLine(io.stdout);
|
|
25290
|
-
writeLine(io.stdout,
|
|
26905
|
+
writeLine(io.stdout, agentLead(agent));
|
|
25291
26906
|
writeLine(io.stdout);
|
|
25292
|
-
writeLine(
|
|
25293
|
-
|
|
25294
|
-
|
|
25295
|
-
|
|
25296
|
-
|
|
25297
|
-
|
|
25298
|
-
|
|
26907
|
+
writeLine(io.stdout, snippet);
|
|
26908
|
+
if (agent === DEFAULT_SNIPPET_AGENT) {
|
|
26909
|
+
writeLine(io.stdout);
|
|
26910
|
+
writeLine(io.stdout, CODEX_LINE);
|
|
26911
|
+
}
|
|
26912
|
+
if (check2.line !== void 0) {
|
|
26913
|
+
writeLine(io.stdout);
|
|
26914
|
+
writeLine(io.stdout, check2.line);
|
|
26915
|
+
}
|
|
25299
26916
|
return 0;
|
|
25300
26917
|
}
|
|
26918
|
+
async function endpointToWrite(given, io) {
|
|
26919
|
+
if (given === void 0 || given.trim() === "") return void 0;
|
|
26920
|
+
const value = parseEndpoint(given, "--endpoint");
|
|
26921
|
+
if (value.kind === "local") return LOCAL_ENDPOINT;
|
|
26922
|
+
const api = await discoverApiUrl(value.url, {
|
|
26923
|
+
fetch: io.fetch,
|
|
26924
|
+
debug: debugEnabled(io.env)
|
|
26925
|
+
});
|
|
26926
|
+
return isDefaultEndpoint(api) ? void 0 : api;
|
|
26927
|
+
}
|
|
26928
|
+
async function checkServer(context, project, io) {
|
|
26929
|
+
const org = context.org;
|
|
26930
|
+
if (context.mode !== "remote" || org === void 0) return { server: "unchecked" };
|
|
26931
|
+
const endpoint = await resolveEndpoint(context, io);
|
|
26932
|
+
if (context.token === void 0 && context.credential === void 0 && !isLoopbackUrl(endpoint)) {
|
|
26933
|
+
return { server: "unchecked" };
|
|
26934
|
+
}
|
|
26935
|
+
const warn = (text) => {
|
|
26936
|
+
writeLine(io.stderr, `quibble: warning: ${text}`);
|
|
26937
|
+
};
|
|
26938
|
+
try {
|
|
26939
|
+
await (await openClient(context, io, { queue: false })).project(project);
|
|
26940
|
+
return { server: "found", line: `Project ${org}/${project} exists on ${endpoint}.` };
|
|
26941
|
+
} catch (error62) {
|
|
26942
|
+
if (error62 instanceof RemoteError && error62.status === 404) {
|
|
26943
|
+
if (error62.serverCode === ORG_NOT_FOUND) {
|
|
26944
|
+
warn(`no organization ${org} on ${endpoint} for this account`);
|
|
26945
|
+
return { server: "no_org" };
|
|
26946
|
+
}
|
|
26947
|
+
warn(
|
|
26948
|
+
`${org} has no project ${project} on ${endpoint}: an admin creates it in the organization settings`
|
|
26949
|
+
);
|
|
26950
|
+
return { server: "no_project" };
|
|
26951
|
+
}
|
|
26952
|
+
warn(`could not check the project on ${endpoint}: ${describeError(error62)}`);
|
|
26953
|
+
return { server: "unreachable" };
|
|
26954
|
+
}
|
|
26955
|
+
}
|
|
25301
26956
|
function resolveProjectName(given, directory) {
|
|
25302
26957
|
const candidate = given?.trim();
|
|
25303
26958
|
if (candidate !== void 0 && candidate !== "") return parseProjectName(candidate);
|
|
25304
|
-
const name =
|
|
26959
|
+
const name = basename2(directory);
|
|
25305
26960
|
try {
|
|
25306
26961
|
return parseProjectName(name);
|
|
25307
26962
|
} catch (error62) {
|
|
@@ -25332,25 +26987,30 @@ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
25332
26987
|
var TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
25333
26988
|
var MINUTE_MS = 6e4;
|
|
25334
26989
|
var HOUR_MS = 60 * MINUTE_MS;
|
|
25335
|
-
var
|
|
26990
|
+
var DAY_MS3 = 24 * HOUR_MS;
|
|
25336
26991
|
var UNIT_MS = {
|
|
25337
26992
|
m: MINUTE_MS,
|
|
25338
26993
|
h: HOUR_MS,
|
|
25339
|
-
d:
|
|
25340
|
-
w: 7 *
|
|
26994
|
+
d: DAY_MS3,
|
|
26995
|
+
w: 7 * DAY_MS3
|
|
25341
26996
|
};
|
|
25342
|
-
|
|
25343
|
-
|
|
26997
|
+
var EXPIRES_MESSAGE = "--expires must be a duration like 30d, a date like 2026-12-01, or an ISO 8601 UTC timestamp";
|
|
26998
|
+
function reject(message = SINCE_MESSAGE) {
|
|
26999
|
+
throw new UsageError(message);
|
|
25344
27000
|
}
|
|
25345
27001
|
function iso(at) {
|
|
25346
27002
|
if (Number.isNaN(at.getTime())) reject();
|
|
25347
27003
|
return at.toISOString();
|
|
25348
27004
|
}
|
|
25349
|
-
function
|
|
25350
|
-
if (digits.startsWith("0"))
|
|
27005
|
+
function durationMs(digits, unit) {
|
|
27006
|
+
if (digits.startsWith("0")) return void 0;
|
|
25351
27007
|
const step = UNIT_MS[unit];
|
|
25352
|
-
|
|
25353
|
-
|
|
27008
|
+
return step === void 0 ? void 0 : Number(digits) * step;
|
|
27009
|
+
}
|
|
27010
|
+
function fromDuration(digits, unit, now) {
|
|
27011
|
+
const span = durationMs(digits, unit);
|
|
27012
|
+
if (span === void 0) reject();
|
|
27013
|
+
return iso(new Date(now.getTime() - span));
|
|
25354
27014
|
}
|
|
25355
27015
|
function fromDate(value) {
|
|
25356
27016
|
const at = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
@@ -25368,6 +27028,27 @@ function parseSince(text, now) {
|
|
|
25368
27028
|
if (TIMESTAMP_RE.test(value)) return iso(new Date(value));
|
|
25369
27029
|
return reject();
|
|
25370
27030
|
}
|
|
27031
|
+
function parseExpires(text, now) {
|
|
27032
|
+
const value = text.trim();
|
|
27033
|
+
const duration3 = DURATION_RE.exec(value);
|
|
27034
|
+
let stamp2;
|
|
27035
|
+
try {
|
|
27036
|
+
if (duration3 !== null) {
|
|
27037
|
+
const span = durationMs(duration3[1] ?? "", duration3[2] ?? "");
|
|
27038
|
+
if (span === void 0) reject(EXPIRES_MESSAGE);
|
|
27039
|
+
stamp2 = iso(new Date(now.getTime() + span));
|
|
27040
|
+
} else if (DATE_RE.test(value)) {
|
|
27041
|
+
stamp2 = fromDate(value);
|
|
27042
|
+
} else if (TIMESTAMP_RE.test(value)) {
|
|
27043
|
+
stamp2 = iso(new Date(value));
|
|
27044
|
+
} else {
|
|
27045
|
+
return reject(EXPIRES_MESSAGE);
|
|
27046
|
+
}
|
|
27047
|
+
} catch {
|
|
27048
|
+
return reject(EXPIRES_MESSAGE);
|
|
27049
|
+
}
|
|
27050
|
+
return stamp2;
|
|
27051
|
+
}
|
|
25371
27052
|
|
|
25372
27053
|
// src/scope.ts
|
|
25373
27054
|
function resolveScope(options, context, now = /* @__PURE__ */ new Date()) {
|
|
@@ -25387,6 +27068,9 @@ function resolveScope(options, context, now = /* @__PURE__ */ new Date()) {
|
|
|
25387
27068
|
...options.since === void 0 ? {} : { since: parseSince(options.since, now) }
|
|
25388
27069
|
};
|
|
25389
27070
|
}
|
|
27071
|
+
function tagFilter(tags) {
|
|
27072
|
+
return tags === void 0 || tags.length === 0 ? {} : { tags: [...tags] };
|
|
27073
|
+
}
|
|
25390
27074
|
|
|
25391
27075
|
// src/commands/list.ts
|
|
25392
27076
|
var COLUMNS = [
|
|
@@ -25396,67 +27080,114 @@ var COLUMNS = [
|
|
|
25396
27080
|
"status",
|
|
25397
27081
|
"project",
|
|
25398
27082
|
"reporter",
|
|
27083
|
+
"tags",
|
|
27084
|
+
"group",
|
|
25399
27085
|
"summary"
|
|
25400
27086
|
];
|
|
25401
27087
|
var REPORTER_WIDTH = 20;
|
|
27088
|
+
var TAGS_WIDTH = 24;
|
|
25402
27089
|
var SUMMARY_MAX = 60;
|
|
27090
|
+
var RECURRED_MARK = "\u21BB";
|
|
27091
|
+
function cut2(text, width) {
|
|
27092
|
+
if (text.length <= width) return text;
|
|
27093
|
+
return `${text.slice(0, width - 3)}...`;
|
|
27094
|
+
}
|
|
25403
27095
|
function summaryOf(complaint) {
|
|
25404
|
-
|
|
25405
|
-
if (summary.length <= SUMMARY_MAX) return summary;
|
|
25406
|
-
return `${summary.slice(0, SUMMARY_MAX - 3)}...`;
|
|
27096
|
+
return cut2(printable(summarize(complaint.message)), SUMMARY_MAX);
|
|
25407
27097
|
}
|
|
27098
|
+
function limitNote(count, limit, more) {
|
|
27099
|
+
if (limit >= MAX_LIMIT) {
|
|
27100
|
+
return count >= MAX_LIMIT ? `Showing the newest ${MAX_LIMIT} complaints: older ones may exist, narrow the filters to see them.` : void 0;
|
|
27101
|
+
}
|
|
27102
|
+
return more ? `Showing the newest ${limit} complaints: pass --limit to see more (up to ${MAX_LIMIT}).` : void 0;
|
|
27103
|
+
}
|
|
27104
|
+
var BUDGET_NOTE = "The server stopped searching early, so older matches may exist: narrow the filters or pass --since.";
|
|
25408
27105
|
async function runList(options, context, io) {
|
|
25409
|
-
|
|
27106
|
+
if (options.ids === true && options.json === true) {
|
|
27107
|
+
throw new UsageError("--ids and --json cannot be used together");
|
|
27108
|
+
}
|
|
27109
|
+
const { store, client } = await openBackend(context, io);
|
|
25410
27110
|
const reporter = options.mine === true ? await resolveMine(context, client, io.env) : void 0;
|
|
25411
27111
|
const filter = parseListFilter({
|
|
25412
27112
|
...resolveScope(options, context),
|
|
25413
27113
|
...options.severity === void 0 ? {} : { severity: options.severity },
|
|
25414
|
-
...options.tag
|
|
27114
|
+
...tagFilter(options.tag),
|
|
27115
|
+
...options.search === void 0 ? {} : { q: options.search },
|
|
25415
27116
|
...reporter === void 0 ? {} : { reporter },
|
|
25416
27117
|
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
25417
27118
|
});
|
|
25418
|
-
const
|
|
27119
|
+
const limit = filter.limit ?? DEFAULT_LIMIT;
|
|
27120
|
+
const page = await listComplaintPage(store, {
|
|
27121
|
+
...filter,
|
|
27122
|
+
limit: Math.min(limit + 1, MAX_LIMIT)
|
|
27123
|
+
});
|
|
27124
|
+
const complaints = page.complaints.slice(0, limit);
|
|
25419
27125
|
if (options.json === true) {
|
|
25420
27126
|
writeJson(io.stdout, complaints);
|
|
25421
27127
|
return 0;
|
|
25422
27128
|
}
|
|
27129
|
+
const note = limitNote(complaints.length, limit, page.complaints.length > limit);
|
|
27130
|
+
if (note !== void 0) writeLine(io.stderr, note);
|
|
27131
|
+
if (page.truncated) writeLine(io.stderr, BUDGET_NOTE);
|
|
27132
|
+
if (options.ids === true) {
|
|
27133
|
+
for (const complaint of complaints) writeLine(io.stdout, complaint.id);
|
|
27134
|
+
return 0;
|
|
27135
|
+
}
|
|
25423
27136
|
if (complaints.length === 0) {
|
|
25424
27137
|
writeLine(io.stdout, "No complaints found.");
|
|
25425
27138
|
return 0;
|
|
25426
27139
|
}
|
|
25427
|
-
|
|
27140
|
+
const lines = options.ungroup === true ? formatTable(complaints.map((complaint) => ({ complaint }))) : formatTable(
|
|
27141
|
+
groupComplaints(complaints).map((group) => ({
|
|
27142
|
+
complaint: group.head,
|
|
27143
|
+
group
|
|
27144
|
+
}))
|
|
27145
|
+
);
|
|
27146
|
+
for (const line of lines) writeLine(io.stdout, line);
|
|
25428
27147
|
return 0;
|
|
25429
27148
|
}
|
|
25430
27149
|
function reporterOf(complaint) {
|
|
25431
|
-
|
|
25432
|
-
|
|
25433
|
-
|
|
25434
|
-
}
|
|
25435
|
-
|
|
25436
|
-
|
|
25437
|
-
|
|
25438
|
-
|
|
25439
|
-
|
|
25440
|
-
|
|
25441
|
-
|
|
25442
|
-
|
|
25443
|
-
|
|
25444
|
-
|
|
25445
|
-
|
|
25446
|
-
|
|
27150
|
+
return cut2(printable(complaint.reporterName ?? complaint.reporter), REPORTER_WIDTH);
|
|
27151
|
+
}
|
|
27152
|
+
function groupCell(group) {
|
|
27153
|
+
const count = group.count > 1 ? `\xD7${group.count}` : "";
|
|
27154
|
+
return `${count}${group.recurred ? RECURRED_MARK : ""}`;
|
|
27155
|
+
}
|
|
27156
|
+
function formatTable(rows2) {
|
|
27157
|
+
const grouped = rows2.some((row) => row.group !== void 0);
|
|
27158
|
+
const columns = COLUMNS.filter((name) => grouped || name !== "group");
|
|
27159
|
+
const cells = rows2.map(({ complaint, group }) => {
|
|
27160
|
+
const values = {
|
|
27161
|
+
id: complaint.id.slice(0, 8),
|
|
27162
|
+
created: complaint.createdAt.slice(0, 16),
|
|
27163
|
+
severity: complaint.severity,
|
|
27164
|
+
status: complaint.status,
|
|
27165
|
+
project: complaint.project,
|
|
27166
|
+
reporter: reporterOf(complaint),
|
|
27167
|
+
tags: cut2(printable(complaint.tags.join(",")), TAGS_WIDTH),
|
|
27168
|
+
group: group === void 0 ? "" : groupCell(group),
|
|
27169
|
+
summary: summaryOf(complaint)
|
|
27170
|
+
};
|
|
27171
|
+
return columns.map((name) => values[name]);
|
|
27172
|
+
});
|
|
27173
|
+
const widths = columns.map(
|
|
27174
|
+
(name, column) => cells.reduce(
|
|
27175
|
+
(width, row) => Math.max(width, (row[column] ?? "").length),
|
|
27176
|
+
name.length
|
|
27177
|
+
)
|
|
25447
27178
|
);
|
|
25448
|
-
const format = (
|
|
25449
|
-
return [format(
|
|
27179
|
+
const format = (line) => line.map((cell2, column) => cell2.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
|
|
27180
|
+
return [format(columns), ...cells.map(format)];
|
|
25450
27181
|
}
|
|
25451
27182
|
|
|
25452
27183
|
// src/commands/orgs.ts
|
|
25453
|
-
var ORGS_NEEDS_SERVER = "orgs needs a server: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc";
|
|
25454
27184
|
var ORGS_PAGE = "/orgs";
|
|
25455
27185
|
var COLUMNS2 = ["SLUG", "NAME", "ROLE", "OPEN"];
|
|
25456
27186
|
async function runOrgs(options, context, io) {
|
|
25457
|
-
|
|
27187
|
+
requireServer("orgs", context);
|
|
27188
|
+
const client = await openClient(context, io);
|
|
25458
27189
|
const endpoint = requireEndpoint(context);
|
|
25459
|
-
const answer2 = await
|
|
27190
|
+
const answer2 = await client.orgs();
|
|
25460
27191
|
if (options.json === true) {
|
|
25461
27192
|
writeJson(io.stdout, answer2.body);
|
|
25462
27193
|
return 0;
|
|
@@ -25464,9 +27195,10 @@ async function runOrgs(options, context, io) {
|
|
|
25464
27195
|
if (answer2.orgs.length === 0) {
|
|
25465
27196
|
const config2 = await fetchAuthConfig(endpoint, {
|
|
25466
27197
|
debug: context.debug,
|
|
25467
|
-
fetch: io.fetch
|
|
27198
|
+
fetch: io.fetch,
|
|
27199
|
+
timeoutMs: context.timeoutMs
|
|
25468
27200
|
});
|
|
25469
|
-
const webUrl = config2.mode === "cognito" ?
|
|
27201
|
+
const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
|
|
25470
27202
|
writeLine(io.stdout, `No organizations yet: create one at ${webUrl}${ORGS_PAGE}.`);
|
|
25471
27203
|
return 0;
|
|
25472
27204
|
}
|
|
@@ -25526,19 +27258,19 @@ var NodeTerminal = class {
|
|
|
25526
27258
|
#interface() {
|
|
25527
27259
|
if (this.#rl !== void 0) return this.#rl;
|
|
25528
27260
|
const output2 = this.#output;
|
|
25529
|
-
const
|
|
27261
|
+
const sink2 = new Writable({
|
|
25530
27262
|
write: (chunk, _encoding, done) => {
|
|
25531
27263
|
if (!this.#muted) output2.write(chunk);
|
|
25532
27264
|
done();
|
|
25533
27265
|
}
|
|
25534
27266
|
});
|
|
25535
|
-
Object.defineProperties(
|
|
27267
|
+
Object.defineProperties(sink2, {
|
|
25536
27268
|
columns: { get: () => output2.columns },
|
|
25537
27269
|
rows: { get: () => output2.rows }
|
|
25538
27270
|
});
|
|
25539
27271
|
const rl = createInterface({
|
|
25540
27272
|
input: this.#input,
|
|
25541
|
-
output:
|
|
27273
|
+
output: sink2,
|
|
25542
27274
|
terminal: true,
|
|
25543
27275
|
// No answer enters the history, so an arrow key recalls no password.
|
|
25544
27276
|
historySize: 0
|
|
@@ -25571,8 +27303,8 @@ var NodeTerminal = class {
|
|
|
25571
27303
|
const queued = this.#lines.shift();
|
|
25572
27304
|
if (queued !== void 0) return Promise.resolve(queued);
|
|
25573
27305
|
if (this.#closed) return Promise.resolve("");
|
|
25574
|
-
return new Promise((
|
|
25575
|
-
this.#waiting =
|
|
27306
|
+
return new Promise((resolve4) => {
|
|
27307
|
+
this.#waiting = resolve4;
|
|
25576
27308
|
});
|
|
25577
27309
|
}
|
|
25578
27310
|
async #read(label, hidden) {
|
|
@@ -25610,7 +27342,7 @@ function nodeTerminal(input2, output2) {
|
|
|
25610
27342
|
|
|
25611
27343
|
// src/commands/login.ts
|
|
25612
27344
|
var LOCAL_IDENTITY_MESSAGE = "local mode: no identity.";
|
|
25613
|
-
var NEEDS_TERMINAL_MESSAGE = "quibble login needs a terminal:
|
|
27345
|
+
var NEEDS_TERMINAL_MESSAGE = "quibble login needs a terminal: in CI, set QUIBBLE_TOKEN instead";
|
|
25614
27346
|
var MFA_CHALLENGE = "SOFTWARE_TOKEN_MFA";
|
|
25615
27347
|
var NEW_PASSWORD_CHALLENGE = "NEW_PASSWORD_REQUIRED";
|
|
25616
27348
|
var LOGIN_PAGE = "/login";
|
|
@@ -25638,29 +27370,29 @@ async function signIn(terminal, identity, config2) {
|
|
|
25638
27370
|
password
|
|
25639
27371
|
});
|
|
25640
27372
|
if (result.kind === "challenge" && result.challenge.challengeName === MFA_CHALLENGE) {
|
|
25641
|
-
const
|
|
27373
|
+
const code2 = await answer(terminal, "Code:");
|
|
25642
27374
|
result = await respondToAuthChallenge(identity, {
|
|
25643
27375
|
clientId,
|
|
25644
27376
|
challenge: result.challenge,
|
|
25645
27377
|
email: email3,
|
|
25646
|
-
code
|
|
27378
|
+
code: code2
|
|
25647
27379
|
});
|
|
25648
27380
|
}
|
|
25649
27381
|
if (result.kind === "challenge") {
|
|
25650
27382
|
const name = result.challenge.challengeName;
|
|
25651
27383
|
if (name === NEW_PASSWORD_CHALLENGE) {
|
|
25652
27384
|
throw new ConfigError(
|
|
25653
|
-
`finish your first sign-in at ${
|
|
27385
|
+
`finish your first sign-in at ${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`
|
|
25654
27386
|
);
|
|
25655
27387
|
}
|
|
25656
27388
|
throw new RemoteError(
|
|
25657
|
-
`
|
|
27389
|
+
`this account needs a sign-in step that quibble doesn't support (${name})`
|
|
25658
27390
|
);
|
|
25659
27391
|
}
|
|
25660
27392
|
return result.tokens;
|
|
25661
27393
|
} catch (error62) {
|
|
25662
27394
|
if (error62 instanceof CognitoError) {
|
|
25663
|
-
const page = `${
|
|
27395
|
+
const page = `${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`;
|
|
25664
27396
|
if (error62.type === "PasswordResetRequiredException") {
|
|
25665
27397
|
throw new ConfigError(`reset your password at ${page}`);
|
|
25666
27398
|
}
|
|
@@ -25672,13 +27404,14 @@ async function signIn(terminal, identity, config2) {
|
|
|
25672
27404
|
}
|
|
25673
27405
|
}
|
|
25674
27406
|
async function runLogin(options, context, io) {
|
|
25675
|
-
|
|
27407
|
+
requireServer("login", context);
|
|
25676
27408
|
const terminal = terminalOf(io);
|
|
25677
27409
|
if (!terminal.isTTY) throw new UsageError(NEEDS_TERMINAL_MESSAGE);
|
|
27410
|
+
const endpoint = await resolveEndpoint(context, io);
|
|
25678
27411
|
const http = { debug: context.debug, fetch: io.fetch };
|
|
25679
27412
|
const config2 = await fetchAuthConfig(endpoint, http);
|
|
25680
27413
|
if (config2.mode === "local") {
|
|
25681
|
-
writeLine(io.stdout, `${endpoint}
|
|
27414
|
+
writeLine(io.stdout, `${endpoint} is in local mode: no login needed.`);
|
|
25682
27415
|
return 0;
|
|
25683
27416
|
}
|
|
25684
27417
|
let tokens;
|
|
@@ -25688,7 +27421,7 @@ async function runLogin(options, context, io) {
|
|
|
25688
27421
|
terminal.close();
|
|
25689
27422
|
}
|
|
25690
27423
|
if (tokens.refreshToken === void 0) {
|
|
25691
|
-
throw new RemoteError("
|
|
27424
|
+
throw new RemoteError("sign-in didn't return a refresh token");
|
|
25692
27425
|
}
|
|
25693
27426
|
const now = /* @__PURE__ */ new Date();
|
|
25694
27427
|
const me = await readProfile(
|
|
@@ -25700,7 +27433,9 @@ async function runLogin(options, context, io) {
|
|
|
25700
27433
|
config2.webUrl
|
|
25701
27434
|
);
|
|
25702
27435
|
if (me.sub === void 0 || me.email === void 0) {
|
|
25703
|
-
throw new RemoteError(
|
|
27436
|
+
throw new RemoteError(
|
|
27437
|
+
"unexpected response from the API: identity has no sub or email"
|
|
27438
|
+
);
|
|
25704
27439
|
}
|
|
25705
27440
|
saveCredential(io.env, {
|
|
25706
27441
|
endpoint,
|
|
@@ -25710,25 +27445,36 @@ async function runLogin(options, context, io) {
|
|
|
25710
27445
|
sub: me.sub,
|
|
25711
27446
|
email: me.email
|
|
25712
27447
|
});
|
|
27448
|
+
const saved = saveEndpoint(context, endpoint, io.env);
|
|
25713
27449
|
if (options.json === true) {
|
|
25714
|
-
writeJson(io.stdout, { ...me.body, endpoint });
|
|
27450
|
+
writeJson(io.stdout, { ...me.body, endpoint, saved: saved ?? null });
|
|
25715
27451
|
return 0;
|
|
25716
27452
|
}
|
|
25717
27453
|
writeLine(io.stdout, `Logged in to ${endpoint} as ${me.email}.`);
|
|
27454
|
+
if (saved !== void 0) {
|
|
27455
|
+
writeLine(io.stdout, `Saved ${endpoint} as your endpoint in ${saved}.`);
|
|
27456
|
+
}
|
|
25718
27457
|
return 0;
|
|
25719
27458
|
}
|
|
27459
|
+
function saveEndpoint(context, endpoint, env) {
|
|
27460
|
+
if (context.endpointSource !== "--endpoint" || isDefaultEndpoint(endpoint)) {
|
|
27461
|
+
return void 0;
|
|
27462
|
+
}
|
|
27463
|
+
return rememberEndpoint(env, endpoint);
|
|
27464
|
+
}
|
|
25720
27465
|
async function readProfile(client, webUrl) {
|
|
25721
27466
|
try {
|
|
25722
27467
|
return await client.me();
|
|
25723
27468
|
} catch (error62) {
|
|
25724
27469
|
if (error62 instanceof RemoteError && NO_PROFILE_STATUSES.has(error62.status ?? 0)) {
|
|
25725
|
-
throw new ConfigError(`finish your account at ${
|
|
27470
|
+
throw new ConfigError(`finish your account at ${trimSlash2(webUrl)}${LOGIN_PAGE}`);
|
|
25726
27471
|
}
|
|
25727
27472
|
throw error62;
|
|
25728
27473
|
}
|
|
25729
27474
|
}
|
|
25730
27475
|
async function runLogout(context, io) {
|
|
25731
|
-
|
|
27476
|
+
requireServer("logout", context);
|
|
27477
|
+
const endpoint = await resolveEndpoint(context, io);
|
|
25732
27478
|
const credential = findCredential(io.env, endpoint);
|
|
25733
27479
|
if (credential === void 0) {
|
|
25734
27480
|
writeLine(io.stdout, `Not logged in to ${endpoint}.`);
|
|
@@ -25764,8 +27510,8 @@ async function runWhoami(options, context, io) {
|
|
|
25764
27510
|
writeLine(io.stdout, LOCAL_IDENTITY_MESSAGE);
|
|
25765
27511
|
return 0;
|
|
25766
27512
|
}
|
|
27513
|
+
const client = await openClient(context, io);
|
|
25767
27514
|
const endpoint = requireEndpoint(context);
|
|
25768
|
-
const client = openClient(context, io);
|
|
25769
27515
|
const me = await client.me();
|
|
25770
27516
|
const org = me.kind === "token" || client.org === void 0 ? void 0 : await client.organization();
|
|
25771
27517
|
if (options.json === true) {
|
|
@@ -25782,7 +27528,7 @@ async function runWhoami(options, context, io) {
|
|
|
25782
27528
|
debug: context.debug,
|
|
25783
27529
|
fetch: io.fetch
|
|
25784
27530
|
});
|
|
25785
|
-
const webUrl = config2.mode === "cognito" ?
|
|
27531
|
+
const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
|
|
25786
27532
|
lines.push("", `Create one at ${webUrl}${ORGS_PAGE2}.`);
|
|
25787
27533
|
}
|
|
25788
27534
|
for (const line of lines) writeLine(io.stdout, line);
|
|
@@ -25822,18 +27568,40 @@ function whoamiLines(endpoint, me, org) {
|
|
|
25822
27568
|
return lines;
|
|
25823
27569
|
}
|
|
25824
27570
|
|
|
27571
|
+
// src/commands/project.ts
|
|
27572
|
+
var DELETE_NEEDS_YES = "project delete needs --yes when stdin is not a terminal";
|
|
27573
|
+
var DELETE_MISMATCH = "the name did not match: nothing was deleted";
|
|
27574
|
+
async function confirm(org, project, io) {
|
|
27575
|
+
const terminal = io.terminal ?? nodeTerminal(io.stdin, io.stderr);
|
|
27576
|
+
if (!terminal.isTTY) throw new UsageError(DELETE_NEEDS_YES);
|
|
27577
|
+
let answer2;
|
|
27578
|
+
try {
|
|
27579
|
+
answer2 = await terminal.prompt(`Type the project name to delete ${org}/${project}:`);
|
|
27580
|
+
} finally {
|
|
27581
|
+
terminal.close();
|
|
27582
|
+
}
|
|
27583
|
+
if (foldProjectName(answer2.trim()) !== project) throw new UsageError(DELETE_MISMATCH);
|
|
27584
|
+
}
|
|
27585
|
+
async function runProjectDelete(name, options, context, io) {
|
|
27586
|
+
requireServer("project delete", context);
|
|
27587
|
+
const project = parseProjectName(name);
|
|
27588
|
+
const org = requireOrg(context);
|
|
27589
|
+
if (options.yes !== true) await confirm(org, project, io);
|
|
27590
|
+
await (await openOrgClient(context, io)).deleteProject(project);
|
|
27591
|
+
if (options.json === true) {
|
|
27592
|
+
writeJson(io.stdout, { org, project, deleted: true });
|
|
27593
|
+
return 0;
|
|
27594
|
+
}
|
|
27595
|
+
writeLine(io.stdout, `Deleted project ${org}/${project}.`);
|
|
27596
|
+
return 0;
|
|
27597
|
+
}
|
|
27598
|
+
|
|
25825
27599
|
// src/commands/promote.ts
|
|
25826
|
-
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, because the project record of the server owns the allowed tag list";
|
|
25828
27600
|
async function runPromote(tag, options, context, io) {
|
|
25829
|
-
|
|
25830
|
-
throw new ConfigError(
|
|
25831
|
-
context.endpoint === void 0 ? PROMOTE_NEEDS_SERVER : PROMOTE_NEEDS_SERVER_LOCAL
|
|
25832
|
-
);
|
|
25833
|
-
}
|
|
27601
|
+
requireServer("promote", context);
|
|
25834
27602
|
const project = requireProject(context);
|
|
25835
27603
|
const promotion = parseTagPromotion({ tag });
|
|
25836
|
-
const result = await openOrgClient(context, io).promoteTag(project, promotion.tag);
|
|
27604
|
+
const result = await (await openOrgClient(context, io)).promoteTag(project, promotion.tag);
|
|
25837
27605
|
if (options.json === true) {
|
|
25838
27606
|
writeJson(io.stdout, {
|
|
25839
27607
|
project,
|
|
@@ -25855,10 +27623,12 @@ async function runPromote(tag, options, context, io) {
|
|
|
25855
27623
|
|
|
25856
27624
|
// src/commands/retag.ts
|
|
25857
27625
|
var NO_TAG_MESSAGE = "retag needs at least one --tag";
|
|
25858
|
-
async function runRetag(
|
|
27626
|
+
async function runRetag(value, options, context, io) {
|
|
25859
27627
|
const tags = options.tag ?? [];
|
|
25860
27628
|
if (tags.length === 0) throw new UsageError(NO_TAG_MESSAGE);
|
|
25861
|
-
const { store, client } = openBackend(context, io);
|
|
27629
|
+
const { store, client, local } = await openBackend(context, io);
|
|
27630
|
+
const [id] = await resolveIds([value], { store, local, context });
|
|
27631
|
+
if (id === void 0) throw new NotFoundError(`no complaint with id ${value}`);
|
|
25862
27632
|
const allowedTags = client === void 0 ? await localAllowedTags(store, id, context) : void 0;
|
|
25863
27633
|
const complaint = await updateComplaintTags(
|
|
25864
27634
|
store,
|
|
@@ -25882,56 +27652,255 @@ async function localAllowedTags(store, id, context) {
|
|
|
25882
27652
|
return complaint.project === config2.project ? config2.tags : void 0;
|
|
25883
27653
|
}
|
|
25884
27654
|
|
|
27655
|
+
// src/commands/retry.ts
|
|
27656
|
+
async function runRetry(options, context, io) {
|
|
27657
|
+
requireServer("retry", context);
|
|
27658
|
+
const client = await openClient(context, io, { queue: false });
|
|
27659
|
+
const endpoint = requireEndpoint(context);
|
|
27660
|
+
const path = queuePath(io.env);
|
|
27661
|
+
const result = await flushQueue({
|
|
27662
|
+
path,
|
|
27663
|
+
endpoint,
|
|
27664
|
+
auth: authLabel(context),
|
|
27665
|
+
client,
|
|
27666
|
+
stderr: io.stderr
|
|
27667
|
+
});
|
|
27668
|
+
if (result.stopped !== void 0) {
|
|
27669
|
+
throw new RemoteError(`${queuedCount(result.left)} still unsent: ${result.stopped}`);
|
|
27670
|
+
}
|
|
27671
|
+
const auth = authLabel(context);
|
|
27672
|
+
const left = pendingFor(path, endpoint).filter((entry) => entry.auth === auth).length;
|
|
27673
|
+
if (options.json === true) {
|
|
27674
|
+
writeJson(io.stdout, { sent: result.sent, dropped: result.dropped, left });
|
|
27675
|
+
return 0;
|
|
27676
|
+
}
|
|
27677
|
+
if (result.locked) {
|
|
27678
|
+
writeLine(io.stdout, "Another quibble process is sending the queue.");
|
|
27679
|
+
return 0;
|
|
27680
|
+
}
|
|
27681
|
+
if (result.sent.length === 0 && result.dropped === 0) {
|
|
27682
|
+
writeLine(io.stdout, "No queued complaints.");
|
|
27683
|
+
return 0;
|
|
27684
|
+
}
|
|
27685
|
+
const sent = result.sent.length;
|
|
27686
|
+
writeLine(
|
|
27687
|
+
io.stdout,
|
|
27688
|
+
`Sent ${sent} queued ${sent === 1 ? "complaint" : "complaints"}; ${left} left.`
|
|
27689
|
+
);
|
|
27690
|
+
return 0;
|
|
27691
|
+
}
|
|
27692
|
+
|
|
27693
|
+
// src/commands/show.ts
|
|
27694
|
+
var LABEL_WIDTH = "duplicate".length;
|
|
27695
|
+
function groupLine(count) {
|
|
27696
|
+
if (count >= MAX_LIMIT) return `${MAX_LIMIT}+ complaints share this headline`;
|
|
27697
|
+
return count === 1 ? "1 complaint shares this headline" : `${count} complaints share this headline`;
|
|
27698
|
+
}
|
|
27699
|
+
function groupCountOf(complaint, members2) {
|
|
27700
|
+
const others = members2.filter((member) => member.id !== complaint.id).length;
|
|
27701
|
+
return Math.min(others + 1, MAX_LIMIT);
|
|
27702
|
+
}
|
|
27703
|
+
function statusLine(complaint) {
|
|
27704
|
+
const reason = complaint.reason === void 0 ? "" : ` (${complaint.reason})`;
|
|
27705
|
+
const by = complaint.statusChangedByName ?? complaint.statusChangedBy;
|
|
27706
|
+
const when = by === void 0 || complaint.statusChangedAt === void 0 ? "" : ` by ${by} at ${complaint.statusChangedAt}`;
|
|
27707
|
+
return `${complaint.status}${reason}${when}`;
|
|
27708
|
+
}
|
|
27709
|
+
function showLines(complaint, groupCount, remote) {
|
|
27710
|
+
const reporter = complaint.reporterName === void 0 ? complaint.reporter : `${complaint.reporterName} (${complaint.reporter})`;
|
|
27711
|
+
const rows2 = [
|
|
27712
|
+
["id", complaint.id],
|
|
27713
|
+
["project", remote ? `${complaint.org}/${complaint.project}` : complaint.project],
|
|
27714
|
+
["status", statusLine(complaint)],
|
|
27715
|
+
["note", complaint.resolution],
|
|
27716
|
+
["duplicate", complaint.duplicateOf],
|
|
27717
|
+
["severity", complaint.severity],
|
|
27718
|
+
["tags", complaint.tags.length === 0 ? void 0 : complaint.tags.join(", ")],
|
|
27719
|
+
["agent", complaint.agent],
|
|
27720
|
+
["reporter", reporter],
|
|
27721
|
+
["created", complaint.createdAt],
|
|
27722
|
+
["updated", complaint.updatedAt],
|
|
27723
|
+
["cwd", complaint.cwd],
|
|
27724
|
+
["cmd", complaint.cmd],
|
|
27725
|
+
["group", groupLine(groupCount)],
|
|
27726
|
+
["recurred", complaint.recurred === true ? "yes" : void 0]
|
|
27727
|
+
];
|
|
27728
|
+
const lines = rows2.filter((row) => row[1] !== void 0).map(([label, value]) => `${label.padEnd(LABEL_WIDTH)} ${printable(value)}`);
|
|
27729
|
+
return [...lines, "", printableLines(complaint.message)];
|
|
27730
|
+
}
|
|
27731
|
+
async function runShow(value, options, context, io) {
|
|
27732
|
+
const { store, local } = await openBackend(context, io);
|
|
27733
|
+
const [id] = await resolveIds([value], { store, local, context });
|
|
27734
|
+
const complaint = id === void 0 ? void 0 : await getComplaint(store, id);
|
|
27735
|
+
if (id === void 0 || complaint === void 0) {
|
|
27736
|
+
throw new NotFoundError(`no complaint with id ${id ?? value}`);
|
|
27737
|
+
}
|
|
27738
|
+
const group = await listComplaints(store, {
|
|
27739
|
+
org: complaint.org,
|
|
27740
|
+
project: complaint.project,
|
|
27741
|
+
fingerprint: complaint.fingerprint,
|
|
27742
|
+
limit: MAX_LIMIT
|
|
27743
|
+
});
|
|
27744
|
+
const groupCount = groupCountOf(complaint, group);
|
|
27745
|
+
if (options.json === true) {
|
|
27746
|
+
writeJson(io.stdout, { ...complaint, groupCount });
|
|
27747
|
+
return 0;
|
|
27748
|
+
}
|
|
27749
|
+
for (const line of showLines(complaint, groupCount, context.mode === "remote")) {
|
|
27750
|
+
writeLine(io.stdout, line);
|
|
27751
|
+
}
|
|
27752
|
+
return 0;
|
|
27753
|
+
}
|
|
27754
|
+
|
|
25885
27755
|
// src/commands/status.ts
|
|
27756
|
+
var REMOTE_ACTOR = "server";
|
|
25886
27757
|
var VERBS = {
|
|
25887
27758
|
open: "Reopened",
|
|
25888
27759
|
resolved: "Resolved",
|
|
25889
27760
|
dismissed: "Dismissed",
|
|
25890
27761
|
deleted: "Deleted"
|
|
25891
27762
|
};
|
|
25892
|
-
|
|
25893
|
-
|
|
25894
|
-
|
|
27763
|
+
var NOT_FOUND = "not_found";
|
|
27764
|
+
function noComplaint(id) {
|
|
27765
|
+
return `no complaint with id ${id}`;
|
|
27766
|
+
}
|
|
27767
|
+
async function readIdArguments(values, io) {
|
|
27768
|
+
if (!values.includes("-")) return [...values];
|
|
27769
|
+
if (values.length > 1) {
|
|
27770
|
+
throw new UsageError("- reads the ids from stdin, so it takes no other id");
|
|
27771
|
+
}
|
|
27772
|
+
if (io.stdin.isTTY === true) throw new UsageError("no ids: pipe them on stdin");
|
|
27773
|
+
const ids = (await readStdin(io.stdin)).split(/\s+/).filter((id) => id !== "");
|
|
27774
|
+
if (ids.length === 0) throw new UsageError("no ids on stdin");
|
|
27775
|
+
return ids;
|
|
27776
|
+
}
|
|
27777
|
+
async function runStatusChange(status, values, options, context, io) {
|
|
27778
|
+
const reason = options.reason ?? (options.duplicateOf === void 0 ? void 0 : DUPLICATE_REASON);
|
|
27779
|
+
const draft = {
|
|
25895
27780
|
status,
|
|
25896
|
-
...options.note === void 0 ? {} : { resolution: options.note }
|
|
27781
|
+
...options.note === void 0 ? {} : { resolution: options.note },
|
|
27782
|
+
...reason === void 0 ? {} : { reason },
|
|
27783
|
+
...options.duplicateOf === void 0 ? {} : { duplicateOf: options.duplicateOf }
|
|
27784
|
+
};
|
|
27785
|
+
parseStatusChange(draft);
|
|
27786
|
+
const given = await readIdArguments(values, io);
|
|
27787
|
+
const backend = await openBackend(context, io);
|
|
27788
|
+
const extra = options.duplicateOf === void 0 ? [] : [options.duplicateOf];
|
|
27789
|
+
const resolved = await resolveIds([...given, ...extra], {
|
|
27790
|
+
store: backend.store,
|
|
27791
|
+
local: backend.local,
|
|
27792
|
+
context
|
|
25897
27793
|
});
|
|
25898
|
-
|
|
25899
|
-
|
|
25900
|
-
|
|
27794
|
+
const ids = [...new Set(resolved.slice(0, given.length))];
|
|
27795
|
+
const duplicateOf = options.duplicateOf === void 0 ? void 0 : resolved.at(-1);
|
|
27796
|
+
const change = parseStatusChange({
|
|
27797
|
+
...draft,
|
|
27798
|
+
...duplicateOf === void 0 ? {} : { duplicateOf }
|
|
27799
|
+
});
|
|
27800
|
+
const actor = { by: context.mode === "remote" ? REMOTE_ACTOR : context.reporter };
|
|
27801
|
+
if (ids.length === 1) {
|
|
27802
|
+
const id = ids[0];
|
|
27803
|
+
const complaint = await updateComplaintStatus(backend.store, id, change, { actor });
|
|
27804
|
+
if (complaint === void 0) throw new NotFoundError(noComplaint(id));
|
|
27805
|
+
if (options.json === true) {
|
|
27806
|
+
writeJson(io.stdout, complaint);
|
|
27807
|
+
return 0;
|
|
27808
|
+
}
|
|
27809
|
+
writeLine(io.stdout, successLine(status, complaint));
|
|
25901
27810
|
return 0;
|
|
25902
27811
|
}
|
|
25903
|
-
|
|
25904
|
-
|
|
25905
|
-
|
|
25906
|
-
|
|
25907
|
-
|
|
27812
|
+
const { results, error: error62 } = backend.store instanceof RemoteStore ? await remoteResults(backend.store, ids, change) : {
|
|
27813
|
+
results: await localResults(
|
|
27814
|
+
ids,
|
|
27815
|
+
async (id) => updateComplaintStatus(backend.store, id, change, { actor })
|
|
27816
|
+
),
|
|
27817
|
+
error: void 0
|
|
27818
|
+
};
|
|
27819
|
+
const failures = [];
|
|
27820
|
+
for (const result of results) {
|
|
27821
|
+
if (result.ok && result.complaint !== void 0) {
|
|
27822
|
+
if (options.json !== true)
|
|
27823
|
+
writeLine(io.stdout, successLine(status, result.complaint));
|
|
27824
|
+
continue;
|
|
27825
|
+
}
|
|
27826
|
+
const code2 = result.error?.code ?? NOT_FOUND;
|
|
27827
|
+
const message = code2 === NOT_FOUND ? noComplaint(result.id) : result.error?.message;
|
|
27828
|
+
writeLine(io.stderr, `quibble: ${result.id}: ${message ?? "the change failed"}`);
|
|
27829
|
+
failures.push({ remote: backend.store instanceof RemoteStore && code2 !== NOT_FOUND });
|
|
27830
|
+
}
|
|
27831
|
+
if (options.json === true) writeJson(io.stdout, results);
|
|
27832
|
+
if (error62 !== void 0) throw error62;
|
|
27833
|
+
if (failures.length === 0) return 0;
|
|
27834
|
+
return failures.some((failure2) => failure2.remote) ? 2 : 1;
|
|
27835
|
+
}
|
|
27836
|
+
function successLine(status, complaint) {
|
|
27837
|
+
return `${VERBS[status]} complaint ${complaint.id} for ${complaint.project}`;
|
|
27838
|
+
}
|
|
27839
|
+
async function localResults(ids, apply) {
|
|
27840
|
+
const results = [];
|
|
27841
|
+
for (const id of ids) {
|
|
27842
|
+
try {
|
|
27843
|
+
const complaint = await apply(id);
|
|
27844
|
+
results.push(
|
|
27845
|
+
complaint === void 0 ? { id, ok: false, error: { code: NOT_FOUND, message: noComplaint(id) } } : { id, ok: true, complaint }
|
|
27846
|
+
);
|
|
27847
|
+
} catch (error62) {
|
|
27848
|
+
if (!(error62 instanceof ValidationError)) throw error62;
|
|
27849
|
+
results.push({
|
|
27850
|
+
id,
|
|
27851
|
+
ok: false,
|
|
27852
|
+
error: { code: error62.code, message: errorMessage(error62) }
|
|
27853
|
+
});
|
|
27854
|
+
}
|
|
27855
|
+
}
|
|
27856
|
+
return results;
|
|
27857
|
+
}
|
|
27858
|
+
async function remoteResults(store, ids, change) {
|
|
27859
|
+
const results = [];
|
|
27860
|
+
for (let start = 0; start < ids.length; start += BULK_MAX) {
|
|
27861
|
+
try {
|
|
27862
|
+
results.push(
|
|
27863
|
+
...await store.bulkStatus(ids.slice(start, start + BULK_MAX), change)
|
|
27864
|
+
);
|
|
27865
|
+
} catch (error62) {
|
|
27866
|
+
return { results, error: error62 };
|
|
27867
|
+
}
|
|
27868
|
+
}
|
|
27869
|
+
return { results, error: void 0 };
|
|
25908
27870
|
}
|
|
25909
27871
|
|
|
25910
27872
|
// src/commands/summary.ts
|
|
25911
27873
|
var FORMATS = ["text", "markdown"];
|
|
25912
27874
|
var SEVERITY_ORDER = [...SEVERITIES].reverse();
|
|
25913
27875
|
var TRUNCATED_MESSAGE = `Counting the newest ${MAX_LIMIT} complaints only.`;
|
|
27876
|
+
var BUDGET_MESSAGE = "The server stopped searching early, so the counts are a floor: narrow the filters or pass --since.";
|
|
25914
27877
|
var EMPTY_MESSAGE = "No complaints found.";
|
|
25915
27878
|
async function runSummary(options, context, io) {
|
|
25916
27879
|
if (options.format !== void 0 && options.json === true) {
|
|
25917
27880
|
throw new UsageError("--json and --format cannot be used together");
|
|
25918
27881
|
}
|
|
25919
27882
|
const format = readFormat(options.format);
|
|
25920
|
-
const { store, client } = openBackend(context, io);
|
|
27883
|
+
const { store, client } = await openBackend(context, io);
|
|
25921
27884
|
const reporter = options.mine === true ? await resolveMine(context, client, io.env) : void 0;
|
|
25922
27885
|
const filter = parseListFilter({
|
|
25923
27886
|
...resolveScope(options, context),
|
|
27887
|
+
...options.severity === void 0 ? {} : { severity: options.severity },
|
|
27888
|
+
...tagFilter(options.tag),
|
|
25924
27889
|
...reporter === void 0 ? {} : { reporter },
|
|
25925
27890
|
limit: MAX_LIMIT
|
|
25926
27891
|
});
|
|
25927
|
-
const
|
|
25928
|
-
const
|
|
27892
|
+
const page = await listComplaintPage(store, filter);
|
|
27893
|
+
const complaints = page.complaints;
|
|
27894
|
+
const capped = complaints.length === MAX_LIMIT;
|
|
27895
|
+
const truncated = capped || page.truncated;
|
|
25929
27896
|
const summary = summarizeComplaints(complaints);
|
|
25930
27897
|
if (options.json === true) {
|
|
25931
27898
|
writeJson(io.stdout, {
|
|
25932
27899
|
scope: {
|
|
25933
27900
|
project: filter.project ?? null,
|
|
25934
27901
|
status: filter.status ?? null,
|
|
27902
|
+
severity: filter.severity ?? null,
|
|
27903
|
+
tags: filter.tags ?? null,
|
|
25935
27904
|
since: filter.since ?? null
|
|
25936
27905
|
},
|
|
25937
27906
|
truncated,
|
|
@@ -25943,7 +27912,8 @@ async function runSummary(options, context, io) {
|
|
|
25943
27912
|
writeLine(io.stdout, EMPTY_MESSAGE);
|
|
25944
27913
|
return 0;
|
|
25945
27914
|
}
|
|
25946
|
-
const
|
|
27915
|
+
const note = capped ? TRUNCATED_MESSAGE : page.truncated ? BUDGET_MESSAGE : void 0;
|
|
27916
|
+
const lines = format === "markdown" ? markdownLines(filter, summary, note) : textLines(filter, summary, note);
|
|
25947
27917
|
for (const line of lines) writeLine(io.stdout, line);
|
|
25948
27918
|
return 0;
|
|
25949
27919
|
}
|
|
@@ -25958,7 +27928,9 @@ function scopeLabel(filter) {
|
|
|
25958
27928
|
const name = filter.project ?? "all projects";
|
|
25959
27929
|
const since = filter.since === void 0 ? "" : ` since ${filter.since}`;
|
|
25960
27930
|
const status = filter.status === void 0 ? "" : ` with status ${filter.status}`;
|
|
25961
|
-
|
|
27931
|
+
const severity = filter.severity === void 0 ? "" : ` with severity ${filter.severity}`;
|
|
27932
|
+
const tags = filter.tags === void 0 ? "" : ` tagged ${filter.tags.map((tag) => tag === UNTAGGED_TAG ? "untagged" : oneLine(tag)).join(" or ")}`;
|
|
27933
|
+
return `Complaints for ${name}${since}${status}${severity}${tags}`;
|
|
25962
27934
|
}
|
|
25963
27935
|
function blocksOf(filter, summary) {
|
|
25964
27936
|
const severity = SEVERITY_ORDER.filter((name) => summary.bySeverity[name] > 0).map(
|
|
@@ -25982,13 +27954,11 @@ function statusCounts(summary) {
|
|
|
25982
27954
|
const { open: open2, resolved, dismissed, deleted } = summary.byStatus;
|
|
25983
27955
|
return `open ${open2}, resolved ${resolved}, dismissed ${dismissed}, deleted ${deleted}`;
|
|
25984
27956
|
}
|
|
25985
|
-
|
|
25986
|
-
return name.replace(/\s+/g, " ");
|
|
25987
|
-
}
|
|
27957
|
+
var oneLine = printable;
|
|
25988
27958
|
function cell(name) {
|
|
25989
27959
|
return oneLine(name).replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
25990
27960
|
}
|
|
25991
|
-
function textLines(filter, summary,
|
|
27961
|
+
function textLines(filter, summary, note) {
|
|
25992
27962
|
const lines = [`${scopeLabel(filter)}: ${summary.total} (${statusCounts(summary)})`];
|
|
25993
27963
|
for (const block2 of blocksOf(filter, summary)) {
|
|
25994
27964
|
const names = block2.rows.map((row) => oneLine(row.name));
|
|
@@ -25998,10 +27968,10 @@ function textLines(filter, summary, truncated) {
|
|
|
25998
27968
|
lines.push(` ${(names[index] ?? "").padEnd(width)} ${row.count}`);
|
|
25999
27969
|
}
|
|
26000
27970
|
}
|
|
26001
|
-
if (
|
|
27971
|
+
if (note !== void 0) lines.push("", note);
|
|
26002
27972
|
return lines;
|
|
26003
27973
|
}
|
|
26004
|
-
function markdownLines(filter, summary,
|
|
27974
|
+
function markdownLines(filter, summary, note) {
|
|
26005
27975
|
const { open: open2, resolved, dismissed, deleted } = summary.byStatus;
|
|
26006
27976
|
const noun = summary.total === 1 ? "complaint" : "complaints";
|
|
26007
27977
|
const counts = `${open2} open, ${resolved} resolved, ${dismissed} dismissed, ${deleted} deleted`;
|
|
@@ -26016,25 +27986,275 @@ function markdownLines(filter, summary, truncated) {
|
|
|
26016
27986
|
);
|
|
26017
27987
|
for (const row of block2.rows) lines.push(`| ${cell(row.name)} | ${row.count} |`);
|
|
26018
27988
|
}
|
|
26019
|
-
if (
|
|
27989
|
+
if (note !== void 0) lines.push("", note);
|
|
26020
27990
|
return lines;
|
|
26021
27991
|
}
|
|
26022
27992
|
|
|
27993
|
+
// src/commands/sync.ts
|
|
27994
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
27995
|
+
import { join as join8 } from "node:path";
|
|
27996
|
+
import { Writable as Writable2 } from "node:stream";
|
|
27997
|
+
var SYNC_TIMEOUT_MS = 3e3;
|
|
27998
|
+
var SECTION_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
27999
|
+
function readOptional3(path) {
|
|
28000
|
+
try {
|
|
28001
|
+
return readFileSync6(path, "utf8");
|
|
28002
|
+
} catch (error62) {
|
|
28003
|
+
if (isNotFound(error62)) return void 0;
|
|
28004
|
+
throw error62;
|
|
28005
|
+
}
|
|
28006
|
+
}
|
|
28007
|
+
function holders(root) {
|
|
28008
|
+
const found = [];
|
|
28009
|
+
for (const name of SECTION_FILES) {
|
|
28010
|
+
const path = join8(root, name);
|
|
28011
|
+
const text = readOptional3(path);
|
|
28012
|
+
if (text !== void 0 && findSection(text) !== void 0) {
|
|
28013
|
+
found.push({ path, text, skill: false });
|
|
28014
|
+
}
|
|
28015
|
+
}
|
|
28016
|
+
const skill = join8(root, SKILL_PATH);
|
|
28017
|
+
if (existsSync3(skill)) {
|
|
28018
|
+
found.push({ path: skill, text: readOptional3(skill) ?? "", skill: true });
|
|
28019
|
+
}
|
|
28020
|
+
return found;
|
|
28021
|
+
}
|
|
28022
|
+
function agentOf(lines, path) {
|
|
28023
|
+
const named = agentOfText(lines);
|
|
28024
|
+
if (named !== void 0) {
|
|
28025
|
+
try {
|
|
28026
|
+
return parseAgentName(named);
|
|
28027
|
+
} catch {
|
|
28028
|
+
}
|
|
28029
|
+
}
|
|
28030
|
+
return agentForFile(path);
|
|
28031
|
+
}
|
|
28032
|
+
function rewrite(holder, tags, scope) {
|
|
28033
|
+
if (holder.skill) {
|
|
28034
|
+
const agent2 = agentOf(holder.text.split(/\r?\n/), holder.path);
|
|
28035
|
+
return withLineBreak(skillFile(agentSnippet(tags, { ...scope, agent: agent2 })), holder.text);
|
|
28036
|
+
}
|
|
28037
|
+
const section = findSection(holder.text);
|
|
28038
|
+
if (section === void 0) return holder.text;
|
|
28039
|
+
const agent = agentOf(sectionText(section), holder.path);
|
|
28040
|
+
return replaceSection(section, agentSnippet(tags, { ...scope, agent }));
|
|
28041
|
+
}
|
|
28042
|
+
function sink() {
|
|
28043
|
+
return new Writable2({
|
|
28044
|
+
write(_chunk, _encoding, done) {
|
|
28045
|
+
done();
|
|
28046
|
+
}
|
|
28047
|
+
});
|
|
28048
|
+
}
|
|
28049
|
+
async function runSync(options, contextOf, io) {
|
|
28050
|
+
if (options.quiet !== true) return sync(options, contextOf(), io);
|
|
28051
|
+
const silent = { ...io, stdout: sink(), stderr: sink() };
|
|
28052
|
+
try {
|
|
28053
|
+
const context = contextOf();
|
|
28054
|
+
context.timeoutMs = SYNC_TIMEOUT_MS;
|
|
28055
|
+
const result = await syncFiles({ hook: options.hook }, context, silent);
|
|
28056
|
+
if (result.files.some((file2) => file2.updated)) {
|
|
28057
|
+
writeLine(io.stdout, refreshedLine(result.project, result.tags));
|
|
28058
|
+
}
|
|
28059
|
+
} catch {
|
|
28060
|
+
}
|
|
28061
|
+
return 0;
|
|
28062
|
+
}
|
|
28063
|
+
async function sync(options, context, io) {
|
|
28064
|
+
const result = await syncFiles(options, context, io);
|
|
28065
|
+
if (options.json === true) {
|
|
28066
|
+
writeJson(io.stdout, {
|
|
28067
|
+
project: result.project,
|
|
28068
|
+
tags: result.tags ?? null,
|
|
28069
|
+
files: result.files,
|
|
28070
|
+
hook: result.hook ?? null
|
|
28071
|
+
});
|
|
28072
|
+
return 0;
|
|
28073
|
+
}
|
|
28074
|
+
for (const file2 of result.files) {
|
|
28075
|
+
writeLine(
|
|
28076
|
+
io.stdout,
|
|
28077
|
+
file2.updated ? `Updated ${file2.path}` : `${file2.path} is up to date`
|
|
28078
|
+
);
|
|
28079
|
+
}
|
|
28080
|
+
return 0;
|
|
28081
|
+
}
|
|
28082
|
+
async function syncFiles(options, context, io) {
|
|
28083
|
+
const project = requireProject(context);
|
|
28084
|
+
const root = context.projectRoot ?? io.cwd;
|
|
28085
|
+
let hook;
|
|
28086
|
+
if (options.hook === true) {
|
|
28087
|
+
const plan = planHook(root);
|
|
28088
|
+
const line = applyHook(plan);
|
|
28089
|
+
hook = plan.path;
|
|
28090
|
+
if (options.json !== true) writeLine(io.stdout, line);
|
|
28091
|
+
}
|
|
28092
|
+
const found = holders(root);
|
|
28093
|
+
if (found.length === 0) {
|
|
28094
|
+
throw new UsageError(
|
|
28095
|
+
`no Quibble snippet under ${root}: run quibble snippet --append CLAUDE.md`
|
|
28096
|
+
);
|
|
28097
|
+
}
|
|
28098
|
+
const { tags, scope } = await snippetSource(project, context, io, { queue: false });
|
|
28099
|
+
const files = [];
|
|
28100
|
+
for (const holder of found) {
|
|
28101
|
+
const next = rewrite(holder, tags, scope);
|
|
28102
|
+
const updated = next !== holder.text;
|
|
28103
|
+
if (updated) writeFileSync6(holder.path, next, "utf8");
|
|
28104
|
+
files.push({ path: holder.path, updated });
|
|
28105
|
+
}
|
|
28106
|
+
return { project, tags, files, hook };
|
|
28107
|
+
}
|
|
28108
|
+
|
|
26023
28109
|
// src/commands/tags.ts
|
|
26024
28110
|
async function runTags(options, context, io) {
|
|
26025
28111
|
const project = requireProject(context);
|
|
26026
28112
|
if (context.mode !== "remote") checkActAs(context);
|
|
26027
|
-
const tags = context.mode === "remote" ? (await openOrgClient(context, io).project(project)).tags : context.allowedTags;
|
|
28113
|
+
const tags = context.mode === "remote" ? (await (await openOrgClient(context, io)).project(project)).tags : context.allowedTags;
|
|
26028
28114
|
if (options.json === true) {
|
|
26029
28115
|
writeJson(io.stdout, { project, tags: tags ?? null });
|
|
26030
28116
|
return 0;
|
|
26031
28117
|
}
|
|
26032
|
-
|
|
28118
|
+
printList(project, tags, io);
|
|
28119
|
+
return 0;
|
|
28120
|
+
}
|
|
28121
|
+
function printList(project, tags, io) {
|
|
28122
|
+
if (tags === void 0 || tags.length === 0) {
|
|
26033
28123
|
writeLine(io.stdout, `No tags configured for ${project}: any tag is allowed.`);
|
|
26034
|
-
return
|
|
28124
|
+
return;
|
|
26035
28125
|
}
|
|
26036
28126
|
for (const tag of tags) writeLine(io.stdout, tag);
|
|
26037
28127
|
writeLine(io.stdout, OTHER_TAG_HINT);
|
|
28128
|
+
}
|
|
28129
|
+
async function runTagsEdit(edit, given, options, context, io) {
|
|
28130
|
+
requireServer("tags", context);
|
|
28131
|
+
const project = requireProject(context);
|
|
28132
|
+
if (given.length === 0) throw new UsageError(`tags ${edit} needs at least one tag`);
|
|
28133
|
+
const named = parseAllowedTags(given);
|
|
28134
|
+
const client = await openOrgClient(context, io);
|
|
28135
|
+
let next;
|
|
28136
|
+
if (edit === "set") {
|
|
28137
|
+
next = named;
|
|
28138
|
+
} else {
|
|
28139
|
+
const current = (await client.project(project)).tags ?? [];
|
|
28140
|
+
const combined = edit === "add" ? [...current, ...named.filter((tag) => !current.includes(tag))] : current.filter((tag) => !named.includes(tag));
|
|
28141
|
+
next = combined.length === 0 ? null : parseAllowedTags(combined);
|
|
28142
|
+
}
|
|
28143
|
+
const answer2 = await client.setProjectTags(project, next);
|
|
28144
|
+
if (options.json === true) {
|
|
28145
|
+
writeJson(io.stdout, answer2.body);
|
|
28146
|
+
return 0;
|
|
28147
|
+
}
|
|
28148
|
+
const noun = answer2.retagged === 1 ? "complaint" : "complaints";
|
|
28149
|
+
writeLine(
|
|
28150
|
+
io.stdout,
|
|
28151
|
+
`Updated the tags of ${project}: ${answer2.retagged} ${noun} retagged`
|
|
28152
|
+
);
|
|
28153
|
+
printList(project, answer2.tags, io);
|
|
28154
|
+
return 0;
|
|
28155
|
+
}
|
|
28156
|
+
|
|
28157
|
+
// src/commands/token.ts
|
|
28158
|
+
var COLUMNS3 = ["ID", "NAME", "CREATED", "LAST USED", "EXPIRES", "STATE"];
|
|
28159
|
+
function minute(stamp2) {
|
|
28160
|
+
return stamp2.slice(0, 16).replace("T", " ");
|
|
28161
|
+
}
|
|
28162
|
+
function stateOf(token, now) {
|
|
28163
|
+
if (token.revokedAt !== void 0) return "revoked";
|
|
28164
|
+
if (token.expiresAt !== void 0 && Date.parse(token.expiresAt) <= now.getTime()) {
|
|
28165
|
+
return "expired";
|
|
28166
|
+
}
|
|
28167
|
+
return "active";
|
|
28168
|
+
}
|
|
28169
|
+
function formatTokens(tokens, now) {
|
|
28170
|
+
const rows2 = tokens.map((token) => [
|
|
28171
|
+
token.id.slice(0, 8),
|
|
28172
|
+
token.name,
|
|
28173
|
+
minute(token.createdAt),
|
|
28174
|
+
token.lastUsedAt === void 0 ? "\u2014" : minute(token.lastUsedAt),
|
|
28175
|
+
token.expiresAt === void 0 ? "never" : minute(token.expiresAt),
|
|
28176
|
+
stateOf(token, now)
|
|
28177
|
+
]);
|
|
28178
|
+
const widths = COLUMNS3.map(
|
|
28179
|
+
(name, column) => rows2.reduce((width, row) => Math.max(width, (row[column] ?? "").length), name.length)
|
|
28180
|
+
);
|
|
28181
|
+
const format = (cells) => cells.map((cell2, column) => cell2.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
|
|
28182
|
+
return [format(COLUMNS3), ...rows2.map(format)];
|
|
28183
|
+
}
|
|
28184
|
+
function scopeOf(context) {
|
|
28185
|
+
requireServer("token", context);
|
|
28186
|
+
return { project: requireProject(context), org: requireOrg(context) };
|
|
28187
|
+
}
|
|
28188
|
+
async function runTokenCreate(name, options, context, io, now = /* @__PURE__ */ new Date()) {
|
|
28189
|
+
const { project, org } = scopeOf(context);
|
|
28190
|
+
const expiresAt = options.expires === void 0 ? void 0 : parseExpires(options.expires, now);
|
|
28191
|
+
const body = parseNewToken(
|
|
28192
|
+
{ name, ...expiresAt === void 0 ? {} : { expiresAt } },
|
|
28193
|
+
now
|
|
28194
|
+
);
|
|
28195
|
+
const client = await openOrgClient(context, io);
|
|
28196
|
+
const minted = await client.createToken(project, body);
|
|
28197
|
+
if (options.json === true) {
|
|
28198
|
+
writeJson(io.stdout, minted.body);
|
|
28199
|
+
return 0;
|
|
28200
|
+
}
|
|
28201
|
+
writeLine(
|
|
28202
|
+
io.stdout,
|
|
28203
|
+
`Created token ${minted.token.name} for ${org}/${project}. It is shown once:`
|
|
28204
|
+
);
|
|
28205
|
+
writeLine(io.stdout, `QUIBBLE_ENDPOINT=${trimSlash2(client.endpoint)}`);
|
|
28206
|
+
writeLine(io.stdout, `QUIBBLE_TOKEN=${minted.plaintext}`);
|
|
28207
|
+
return 0;
|
|
28208
|
+
}
|
|
28209
|
+
async function runTokenList(options, context, io, now = /* @__PURE__ */ new Date()) {
|
|
28210
|
+
const { project } = scopeOf(context);
|
|
28211
|
+
const answer2 = await (await openOrgClient(context, io)).tokens(project);
|
|
28212
|
+
if (options.json === true) {
|
|
28213
|
+
writeJson(io.stdout, answer2.body);
|
|
28214
|
+
return 0;
|
|
28215
|
+
}
|
|
28216
|
+
if (answer2.tokens.length === 0) {
|
|
28217
|
+
writeLine(io.stdout, `No tokens for ${project}.`);
|
|
28218
|
+
return 0;
|
|
28219
|
+
}
|
|
28220
|
+
for (const line of formatTokens(answer2.tokens, now)) writeLine(io.stdout, line);
|
|
28221
|
+
return 0;
|
|
28222
|
+
}
|
|
28223
|
+
function findToken(tokens, value) {
|
|
28224
|
+
const named = tokens.find((token) => token.name === value.trim());
|
|
28225
|
+
if (named !== void 0) return named;
|
|
28226
|
+
let read;
|
|
28227
|
+
try {
|
|
28228
|
+
read = readIdValue(value);
|
|
28229
|
+
} catch (error62) {
|
|
28230
|
+
if (error62 instanceof UsageError)
|
|
28231
|
+
throw new NotFoundError(`no token is named ${value}`);
|
|
28232
|
+
throw error62;
|
|
28233
|
+
}
|
|
28234
|
+
const { id, prefix } = read;
|
|
28235
|
+
const matches = tokens.filter(
|
|
28236
|
+
(token) => prefix ? token.id.startsWith(id) : token.id === id
|
|
28237
|
+
);
|
|
28238
|
+
if (matches.length === 1) return matches[0];
|
|
28239
|
+
if (matches.length > 1) {
|
|
28240
|
+
throw ambiguousError(
|
|
28241
|
+
id,
|
|
28242
|
+
matches.map((token) => token.id),
|
|
28243
|
+
"tokens"
|
|
28244
|
+
);
|
|
28245
|
+
}
|
|
28246
|
+
throw new NotFoundError(`no token is named or starts with ${id}`);
|
|
28247
|
+
}
|
|
28248
|
+
async function runTokenRevoke(value, options, context, io) {
|
|
28249
|
+
const { project, org } = scopeOf(context);
|
|
28250
|
+
const client = await openOrgClient(context, io);
|
|
28251
|
+
const token = findToken((await client.tokens(project)).tokens, value);
|
|
28252
|
+
await client.revokeToken(project, token.id);
|
|
28253
|
+
if (options.json === true) {
|
|
28254
|
+
writeJson(io.stdout, { id: token.id, name: token.name, revoked: true });
|
|
28255
|
+
return 0;
|
|
28256
|
+
}
|
|
28257
|
+
writeLine(io.stdout, `Revoked token ${token.name} of ${org}/${project}.`);
|
|
26038
28258
|
return 0;
|
|
26039
28259
|
}
|
|
26040
28260
|
|
|
@@ -26049,12 +28269,20 @@ var NON_EMPTY_OPTIONS = [
|
|
|
26049
28269
|
"note"
|
|
26050
28270
|
];
|
|
26051
28271
|
var STATUS_COMMANDS = [
|
|
26052
|
-
["resolve", "resolved", "mark
|
|
26053
|
-
["dismiss", "dismissed", "dismiss
|
|
26054
|
-
["delete", "deleted", "hide
|
|
26055
|
-
["reopen", "open", "reopen
|
|
28272
|
+
["resolve", "resolved", "mark complaints resolved"],
|
|
28273
|
+
["dismiss", "dismissed", "dismiss duplicates, won't-fixes or noise"],
|
|
28274
|
+
["delete", "deleted", "hide complaints from the default views"],
|
|
28275
|
+
["reopen", "open", "reopen complaints and clear their notes"]
|
|
28276
|
+
];
|
|
28277
|
+
var TAGS_EDITS = [
|
|
28278
|
+
["set", "replace the project's allowed tag list"],
|
|
28279
|
+
["add", "add tags to the allowed tag list"],
|
|
28280
|
+
["remove", "remove tags from the allowed tag list"]
|
|
26056
28281
|
];
|
|
28282
|
+
var ID_HELP = `complaint id, or a unique prefix of at least ${SHORT_ID_MIN} characters`;
|
|
28283
|
+
var REASON_HELP = `${REASONS.slice(0, -1).join(", ")} or ${REASONS.at(-1) ?? ""}`;
|
|
26057
28284
|
function packageVersion() {
|
|
28285
|
+
if (typeof QUIBBLE_BUILD_VERSION === "string") return QUIBBLE_BUILD_VERSION;
|
|
26058
28286
|
const require2 = createRequire(import.meta.url);
|
|
26059
28287
|
const manifest = require2("../package.json");
|
|
26060
28288
|
return typeof manifest.version === "string" ? manifest.version : "0.0.0";
|
|
@@ -26068,7 +28296,7 @@ function rejectEmptyOptions(options) {
|
|
|
26068
28296
|
}
|
|
26069
28297
|
}
|
|
26070
28298
|
function withGlobalOptions(command) {
|
|
26071
|
-
return command.option("--local", "use the local store,
|
|
28299
|
+
return command.option("--local", "use the local store, whatever the endpoint says").option("--endpoint <url>", "API URL, web URL, host, or local").option("--org <slug>", "organization to use in remote mode").option("--json", "print JSON instead of text");
|
|
26072
28300
|
}
|
|
26073
28301
|
function globals(program2, command) {
|
|
26074
28302
|
const outer = program2.opts();
|
|
@@ -26106,88 +28334,174 @@ function buildProgram(io, setCode) {
|
|
|
26106
28334
|
}).exitOverride();
|
|
26107
28335
|
withGlobalOptions(program2);
|
|
26108
28336
|
withGlobalOptions(
|
|
26109
|
-
program2.command("init").description("write a .quibblerc in the current directory").option("--project <name>", "project name
|
|
26110
|
-
).action(function() {
|
|
28337
|
+
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("--hook", "also add the quibble sync hook to .claude/settings.json").option("--force", "overwrite an existing .quibblerc or skill file")
|
|
28338
|
+
).action(async function() {
|
|
28339
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
28340
|
+
rejectEmptyOptions(options);
|
|
28341
|
+
setCode(await runInit(options, io));
|
|
28342
|
+
});
|
|
28343
|
+
withGlobalOptions(
|
|
28344
|
+
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("--hook", "add the quibble sync hook to .claude/settings.json").option("--agent <name>", "agent named in the snippet").option("--project <name>", "project name (overrides .quibblerc)").option("--force", "overwrite an existing skill file")
|
|
28345
|
+
).action(async function() {
|
|
26111
28346
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26112
28347
|
rejectEmptyOptions(options);
|
|
26113
|
-
setCode(
|
|
28348
|
+
setCode(await runSnippet(options, contextFor(options, io), io));
|
|
28349
|
+
});
|
|
28350
|
+
withGlobalOptions(
|
|
28351
|
+
program2.command("sync").description("rewrite the agent snippet in place with the current tags").option("--hook", "also add the quibble sync hook to .claude/settings.json").option("--quiet", "print nothing but a changed tag list, and always exit 0").option("--project <name>", "project name (overrides .quibblerc)")
|
|
28352
|
+
).action(async function() {
|
|
28353
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
28354
|
+
setCode(
|
|
28355
|
+
await runSync(
|
|
28356
|
+
options,
|
|
28357
|
+
() => {
|
|
28358
|
+
rejectEmptyOptions(options);
|
|
28359
|
+
return contextFor(options, io);
|
|
28360
|
+
},
|
|
28361
|
+
io
|
|
28362
|
+
)
|
|
28363
|
+
);
|
|
26114
28364
|
});
|
|
26115
28365
|
withGlobalOptions(
|
|
26116
|
-
program2.command("complain").description("file a complaint
|
|
28366
|
+
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
28367
|
).action(async function(message) {
|
|
26118
28368
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26119
28369
|
rejectEmptyOptions(options);
|
|
26120
28370
|
setCode(await runComplain(message, options, contextFor(options, io), io));
|
|
26121
28371
|
});
|
|
26122
28372
|
withGlobalOptions(
|
|
26123
|
-
program2.command("list").description("list complaints, newest first").option("--project <name>", "list one project").option("--all", "list
|
|
28373
|
+
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(
|
|
28374
|
+
"-t, --tag <tag>",
|
|
28375
|
+
"complaints with any of these tags (repeatable, `-` for untagged)",
|
|
28376
|
+
collect
|
|
28377
|
+
).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
28378
|
).action(async function() {
|
|
26125
28379
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26126
28380
|
rejectEmptyOptions(options);
|
|
26127
28381
|
setCode(await runList(options, contextFor(options, io), io));
|
|
26128
28382
|
});
|
|
28383
|
+
withGlobalOptions(
|
|
28384
|
+
program2.command("show").description("print one whole complaint").argument("<id>", ID_HELP)
|
|
28385
|
+
).action(async function(id) {
|
|
28386
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
28387
|
+
rejectEmptyOptions(options);
|
|
28388
|
+
setCode(await runShow(id, options, contextFor(options, io), io));
|
|
28389
|
+
});
|
|
26129
28390
|
for (const [name, status, description] of STATUS_COMMANDS) {
|
|
26130
|
-
const command = program2.command(name).description(description).argument("<id
|
|
28391
|
+
const command = program2.command(name).description(description).argument("<id...>", `${ID_HELP}; \`-\` reads the ids from stdin`);
|
|
26131
28392
|
if (status !== "open") {
|
|
26132
|
-
command.option("--note <text>", "note
|
|
28393
|
+
command.option("--note <text>", "note explaining the change");
|
|
26133
28394
|
}
|
|
26134
|
-
|
|
28395
|
+
if (status === "resolved" || status === "dismissed") {
|
|
28396
|
+
command.option("--reason <reason>", REASON_HELP).option("--duplicate-of <id>", "the complaint these repeat (implies duplicate)");
|
|
28397
|
+
}
|
|
28398
|
+
withGlobalOptions(command).action(async function(ids) {
|
|
26135
28399
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26136
28400
|
rejectEmptyOptions(options);
|
|
26137
|
-
setCode(await runStatusChange(status,
|
|
28401
|
+
setCode(await runStatusChange(status, ids, options, contextFor(options, io), io));
|
|
26138
28402
|
});
|
|
26139
28403
|
}
|
|
26140
28404
|
withGlobalOptions(
|
|
26141
|
-
program2.command("retag").description("replace
|
|
28405
|
+
program2.command("retag").description("replace a complaint's tags").argument("<id>", ID_HELP).option("-t, --tag <tag>", "tag (repeatable, at least one)", collect)
|
|
26142
28406
|
).action(async function(id) {
|
|
26143
28407
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26144
28408
|
rejectEmptyOptions(options);
|
|
26145
28409
|
setCode(await runRetag(id, options, contextFor(options, io), io));
|
|
26146
28410
|
});
|
|
26147
28411
|
withGlobalOptions(
|
|
26148
|
-
program2.command("summary").description("count
|
|
28412
|
+
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(
|
|
28413
|
+
"-t, --tag <tag>",
|
|
28414
|
+
"complaints with any of these tags (repeatable, `-` for untagged)",
|
|
28415
|
+
collect
|
|
28416
|
+
).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
28417
|
).action(async function() {
|
|
26150
28418
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26151
28419
|
rejectEmptyOptions(options);
|
|
26152
28420
|
setCode(await runSummary(options, contextFor(options, io), io));
|
|
26153
28421
|
});
|
|
26154
|
-
withGlobalOptions(
|
|
26155
|
-
program2.command("tags").description("print the allowed tags
|
|
28422
|
+
const tags = withGlobalOptions(
|
|
28423
|
+
program2.command("tags").description("print or edit the project's allowed tags").option("--project <name>", "project name (overrides .quibblerc)")
|
|
26156
28424
|
).action(async function() {
|
|
26157
28425
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26158
28426
|
rejectEmptyOptions(options);
|
|
26159
28427
|
setCode(await runTags(options, contextFor(options, io), io));
|
|
26160
28428
|
});
|
|
28429
|
+
for (const [edit, description] of TAGS_EDITS) {
|
|
28430
|
+
withGlobalOptions(
|
|
28431
|
+
tags.command(edit).description(`${description} (remote only)`).argument("[tag...]", "tags").option("--project <name>", "project name (overrides .quibblerc)")
|
|
28432
|
+
).action(async function(given) {
|
|
28433
|
+
const options = { ...tags.opts(), ...this.opts(), ...globals(program2, this) };
|
|
28434
|
+
rejectEmptyOptions(options);
|
|
28435
|
+
setCode(await runTagsEdit(edit, given, options, contextFor(options, io), io));
|
|
28436
|
+
});
|
|
28437
|
+
}
|
|
28438
|
+
const token = program2.command("token").description("manage the project's tokens (remote only)");
|
|
28439
|
+
withGlobalOptions(
|
|
28440
|
+
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)")
|
|
28441
|
+
).action(async function(name) {
|
|
28442
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
28443
|
+
rejectEmptyOptions(options);
|
|
28444
|
+
setCode(await runTokenCreate(name, options, contextFor(options, io), io));
|
|
28445
|
+
});
|
|
28446
|
+
withGlobalOptions(
|
|
28447
|
+
token.command("list").description("list the project's tokens").option("--project <name>", "project name (overrides .quibblerc)")
|
|
28448
|
+
).action(async function() {
|
|
28449
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
28450
|
+
rejectEmptyOptions(options);
|
|
28451
|
+
setCode(await runTokenList(options, contextFor(options, io), io));
|
|
28452
|
+
});
|
|
26161
28453
|
withGlobalOptions(
|
|
26162
|
-
|
|
28454
|
+
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)")
|
|
28455
|
+
).action(async function(value) {
|
|
28456
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
28457
|
+
rejectEmptyOptions(options);
|
|
28458
|
+
setCode(await runTokenRevoke(value, options, contextFor(options, io), io));
|
|
28459
|
+
});
|
|
28460
|
+
const projectCommand = program2.command("project").description("manage the projects of the organization (remote only)");
|
|
28461
|
+
withGlobalOptions(
|
|
28462
|
+
projectCommand.command("delete").description("delete a project and hide its complaints").argument("<name>", "project name").option("--yes", "skip the question")
|
|
28463
|
+
).action(async function(name) {
|
|
28464
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
28465
|
+
rejectEmptyOptions(options);
|
|
28466
|
+
setCode(await runProjectDelete(name, options, contextFor(options, io), io));
|
|
28467
|
+
});
|
|
28468
|
+
withGlobalOptions(
|
|
28469
|
+
program2.command("retry").description("send the complaints queued while offline")
|
|
28470
|
+
).action(async function() {
|
|
28471
|
+
const options = { ...this.opts(), ...globals(program2, this) };
|
|
28472
|
+
rejectEmptyOptions(options);
|
|
28473
|
+
setCode(await runRetry(options, contextFor(options, io), io));
|
|
28474
|
+
});
|
|
28475
|
+
withGlobalOptions(
|
|
28476
|
+
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
28477
|
).action(async function(tag) {
|
|
26164
28478
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26165
28479
|
rejectEmptyOptions(options);
|
|
26166
28480
|
setCode(await runPromote(tag, options, contextFor(options, io), io));
|
|
26167
28481
|
});
|
|
26168
28482
|
withGlobalOptions(
|
|
26169
|
-
program2.command("orgs").description("list
|
|
28483
|
+
program2.command("orgs").description("list your organizations")
|
|
26170
28484
|
).action(async function() {
|
|
26171
28485
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26172
28486
|
rejectEmptyOptions(options);
|
|
26173
28487
|
setCode(await runOrgs(options, contextFor(options, io), io));
|
|
26174
28488
|
});
|
|
26175
28489
|
withGlobalOptions(
|
|
26176
|
-
program2.command("login").description("
|
|
28490
|
+
program2.command("login").description("log in to the endpoint")
|
|
26177
28491
|
).action(async function() {
|
|
26178
28492
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26179
28493
|
rejectEmptyOptions(options);
|
|
26180
28494
|
setCode(await runLogin(options, contextFor(options, io), io));
|
|
26181
28495
|
});
|
|
26182
28496
|
withGlobalOptions(
|
|
26183
|
-
program2.command("logout").description("
|
|
28497
|
+
program2.command("logout").description("log out of the endpoint")
|
|
26184
28498
|
).action(async function() {
|
|
26185
28499
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26186
28500
|
rejectEmptyOptions(options);
|
|
26187
28501
|
setCode(await runLogout(contextFor(options, io), io));
|
|
26188
28502
|
});
|
|
26189
28503
|
withGlobalOptions(
|
|
26190
|
-
program2.command("whoami").description("
|
|
28504
|
+
program2.command("whoami").description("show the current identity")
|
|
26191
28505
|
).action(async function() {
|
|
26192
28506
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26193
28507
|
rejectEmptyOptions(options);
|
|
@@ -26195,10 +28509,10 @@ function buildProgram(io, setCode) {
|
|
|
26195
28509
|
});
|
|
26196
28510
|
withGlobalOptions(
|
|
26197
28511
|
program2.command("config").description("print the resolved configuration")
|
|
26198
|
-
).action(function() {
|
|
28512
|
+
).action(async function() {
|
|
26199
28513
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
26200
28514
|
rejectEmptyOptions(options);
|
|
26201
|
-
setCode(runConfig(options, contextFor(options, io), io));
|
|
28515
|
+
setCode(await runConfig(options, contextFor(options, io), io));
|
|
26202
28516
|
});
|
|
26203
28517
|
return program2;
|
|
26204
28518
|
}
|
|
@@ -26206,9 +28520,10 @@ function stripErrorPrefix(message) {
|
|
|
26206
28520
|
return message.replace(/^error:\s*/i, "");
|
|
26207
28521
|
}
|
|
26208
28522
|
async function main(argv, io) {
|
|
26209
|
-
|
|
28523
|
+
resetDiscovery();
|
|
28524
|
+
let code2 = 0;
|
|
26210
28525
|
const program2 = buildProgram(io, (value) => {
|
|
26211
|
-
|
|
28526
|
+
code2 = value;
|
|
26212
28527
|
});
|
|
26213
28528
|
if (argv.length === 0) {
|
|
26214
28529
|
io.stdout.write(program2.helpInformation());
|
|
@@ -26216,7 +28531,7 @@ async function main(argv, io) {
|
|
|
26216
28531
|
}
|
|
26217
28532
|
try {
|
|
26218
28533
|
await program2.parseAsync(argv, { from: "user" });
|
|
26219
|
-
return
|
|
28534
|
+
return code2;
|
|
26220
28535
|
} catch (error62) {
|
|
26221
28536
|
if (error62 instanceof CommanderError) {
|
|
26222
28537
|
if (error62.code === "commander.help" || error62.code === "commander.helpDisplayed") {
|
|
@@ -26238,10 +28553,18 @@ for (const stream of [process2.stdout, process2.stderr]) {
|
|
|
26238
28553
|
throw error62;
|
|
26239
28554
|
});
|
|
26240
28555
|
}
|
|
26241
|
-
|
|
28556
|
+
function flushed(stream) {
|
|
28557
|
+
return new Promise((resolve4) => {
|
|
28558
|
+
stream.write("", () => resolve4());
|
|
28559
|
+
});
|
|
28560
|
+
}
|
|
28561
|
+
var code = await main(process2.argv.slice(2), {
|
|
26242
28562
|
stdin: process2.stdin,
|
|
26243
28563
|
stdout: process2.stdout,
|
|
26244
28564
|
stderr: process2.stderr,
|
|
26245
28565
|
env: process2.env,
|
|
26246
28566
|
cwd: process2.cwd()
|
|
26247
28567
|
});
|
|
28568
|
+
process2.exitCode = code;
|
|
28569
|
+
await Promise.all([flushed(process2.stdout), flushed(process2.stderr)]);
|
|
28570
|
+
process2.exit(code);
|